diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 9581c2fd7b508..0000000000000 --- a/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -.github -node_modules -package-lock.json -pnpm-lock.yaml -platform/dist diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index ef96a1e33c839..0000000000000 --- a/.eslintrc +++ /dev/null @@ -1,233 +0,0 @@ -{ - "env": { - "commonjs": true, - "es2020": true, - "node": true - }, - "extends": [ - "eslint:recommended", - "plugin:jsonc/recommended-with-jsonc", - "plugin:jest/recommended" - ], - "ignorePatterns": ["scripts", "components/**/test-event.mjs"], - "overrides": [ - { - "files": [ - ".eslintrc", - "*.json" - ], - "parser": "jsonc-eslint-parser" - }, - { - "files": [ - "**/actions/**/*.*js", - "**/sources/**/*.*js" - ], - "rules": { - "pipedream/required-properties-key": "error", - "pipedream/required-properties-name": "error", - "pipedream/required-properties-version": "error", - "pipedream/required-properties-description": "error", - "pipedream/required-properties-type": "error", - "pipedream/props-label": "warn", - "pipedream/props-description": "warn", - "pipedream/source-name": "warn", - "pipedream/source-description": "warn", - "pipedream/no-ts-version": "warn" - } - }, - { - "files": [ - "**/actions/**/common*.*js", - "**/actions/common/*", - "**/sources/common/*", - "**/sources/**/common*.*js", - "**/sources/**/constant*.*js", - "**/actions/**/constant*.*js" - ], - "rules": { - "pipedream/required-properties-key": "off", - "pipedream/required-properties-name": "off", - "pipedream/required-properties-version": "off", - "pipedream/required-properties-description": "off", - "pipedream/required-properties-type": "off", - "pipedream/source-name": "off", - "pipedream/source-description": "off" - } - }, - { - "files": [ - "**/*.app.*js" - ], - "rules": { - "pipedream/props-label": "warn", - "pipedream/props-description": "warn" - } - }, - { - "files": [ - "**/components/bash/**/*.*js", - "**/components/go/**/*.*js", - "**/components/node/**/*.*js", - "**/components/python/**/*.*js" - ], - "rules": { - "no-unused-vars": "off" - } - }, - { - "files": [ - "*.ts", - "*.mts" - ], - "extends": [ - "plugin:@typescript-eslint/recommended" - ], - "parser": "@typescript-eslint/parser", - "rules": { - "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], - "max-len": "off", - // note you must disable the base rule as it can report incorrect errors - "semi": "off", - "@typescript-eslint/semi": ["error"], - "@typescript-eslint/member-delimiter-style": ["error", { - "multiline": { - "delimiter": "semi", - "requireLast": true - }, - "singleline": { - "delimiter": "semi", - "requireLast": true - }, - "multilineDetection": "brackets", - "overrides": { - "interface": { - "multiline": { - "delimiter": "semi", - "requireLast": true - } - } - } - }] - } - } - ], - "parserOptions": { - "ecmaVersion": 12, - "sourceType": "module" - }, - "plugins": [ - "jsonc", - "putout", - "pipedream", - "@typescript-eslint", - "jest" - ], - "root": true, - "rules": { - "arrow-parens": "error", - "arrow-spacing": "error", - "array-bracket-newline": [ - "error", - { - "minItems": 1 - } - ], - "array-element-newline": [ - "error", - "always" - ], - "comma-dangle": [ - "error", - "always-multiline" - ], - "comma-spacing": "error", - "eol-last": "error", - "function-call-argument-newline": [ - "error", - "consistent" - ], - "function-paren-newline": [ - "error", - "consistent" - ], - "indent": [ - "error", - 2 - ], - "key-spacing": "error", - "keyword-spacing": "error", - "max-len": [ - "error", - { - "code": 100, - "tabWidth": 2, - "ignoreTrailingComments": true, - "ignoreUrls": true, - "ignoreStrings": true, - "ignoreTemplateLiterals": true, - "ignoreRegExpLiterals": true - } - ], - "multiline-ternary": [ - "error", - "always" - ], - "newline-per-chained-call": "error", - "no-constant-condition": [ - "error", - { - "checkLoops": false - } - ], - "no-multiple-empty-lines": [ - "error", - { - "max": 1, - "maxBOF": 0, - "maxEOF": 1 - } - ], - "no-trailing-spaces": "error", - "no-unused-vars": "error", - "object-curly-newline": [ - "error", - { - "ExportDeclaration": "always", - "ImportDeclaration": { - "minProperties": 2, - "multiline": true - }, - "ObjectExpression": { - "minProperties": 1, - "multiline": true - }, - "ObjectPattern": { - "minProperties": 2, - "multiline": true - } - } - ], - "object-curly-spacing": [ - "error", - "always" - ], - "object-property-newline": [ - "error", - { - "allowAllPropertiesOnSameLine": false - } - ], - "quote-props": [ - "error", - "consistent" - ], - "quotes": "error", - "semi": "error", - "space-before-blocks": [ - "error", - "always" - ], - "space-infix-ops": "error" - } -} diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 06bf03b746ebf..d3a84c353b197 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -8,7 +8,7 @@ assignees: '' --- **Describe the bug** -A clear and concise description of what the bug is. +A clear and concise description of what the bug is. Note that this issue will be displayed publicly. **To Reproduce** Steps to reproduce the behavior: @@ -21,7 +21,7 @@ Steps to reproduce the behavior: A clear and concise description of what you expected to happen. **Screenshots** -If applicable, add screenshots to help explain your problem. +If applicable, add screenshots to help explain your problem. Note that the screenshots will be displayed publicly. **Desktop (please complete the following information):** - OS: [e.g. iOS] diff --git a/.github/ISSUE_TEMPLATE/scopes-request.md b/.github/ISSUE_TEMPLATE/scopes-request.md index 512ef13904ac5..c170f71879704 100644 --- a/.github/ISSUE_TEMPLATE/scopes-request.md +++ b/.github/ISSUE_TEMPLATE/scopes-request.md @@ -2,7 +2,7 @@ name: OAuth Scopes Request about: Request to add a new scope to an OAuth integrated application title: "[OAUTH SCOPE]" -labels: scope request +labels: '' assignees: '' --- diff --git a/.github/actions/git-diff-on-components/README.md b/.github/actions/git-diff-on-components/README.md index cff8d551b9a4c..f0b838e6215f2 100644 --- a/.github/actions/git-diff-on-components/README.md +++ b/.github/actions/git-diff-on-components/README.md @@ -14,12 +14,14 @@ This action takes care of all components with dependencies that were modified bu ### `all_files` -**Required** List of all files comming from `changed_files` step in `check_version` job github action workflow. It is necessary to set the action `jitterbit/get-changed-files@v1` output in json format like +**Required** List of all files comming from `changed_files` step in `check_version` job github action workflow. It is necessary to set the action `Ana06/get-changed-files@v2.3.0` output in json format like + ``` ... with: format: json ``` + in that way `steps.changed_files.outputs.all` will be converted in array of strings ## Example usage @@ -34,8 +36,10 @@ in that way `steps.changed_files.outputs.all` will be converted in array of stri ``` ## Build + You need to push all files generated in `dist` folder once you are finished with the build to test the new version of the github action in case you want to make modifications. + ``` $ cd .github/actions/git-diff-on-components/ -$ npm i && npm run build -``` \ No newline at end of file +$ pnpm i && pnpm run build +``` diff --git a/.github/actions/git-diff-on-components/dist/index.js b/.github/actions/git-diff-on-components/dist/index.js index 3f35263e16973..b9ce8af9400e5 100644 --- a/.github/actions/git-diff-on-components/dist/index.js +++ b/.github/actions/git-diff-on-components/dist/index.js @@ -282852,15 +282852,17 @@ async function run() { } if (componentsDiffContents.length) { - let command = '' + let linuxCommand = '' + let macCommand = '' - for ({ dependencyFilePath, componentFilePath } of componentsDiffContents) { + for ({ componentFilePath } of componentsDiffContents) { try { const content = await readFile(componentFilePath, "utf-8") const currentVersion = getVersion(content) const increasedVersion = increaseVersion(currentVersion) - command += `${command.length ? " && " : ""}sed -i 0,/${currentVersion}/{s/${currentVersion}/${increasedVersion}/} ${getComponentFilePath(componentFilePath)}` + linuxCommand += `${linuxCommand.length ? " && " : ""}sed -i 0,/${currentVersion}/{s/${currentVersion}/${increasedVersion}/} ${getComponentFilePath(componentFilePath)}` + macCommand += `${macCommand.length ? " && " : ""}sed -i '' 's/${currentVersion}/${increasedVersion}/' ${getComponentFilePath(componentFilePath)}` } catch (error) { console.error(`❌ Error checking component diff of ${getComponentFilePath(componentFilePath)}: ${error}`); } @@ -282869,7 +282871,12 @@ async function run() { core.setFailed(`❌ Version of ${componentsDiffContents.length} dependencies needs to be increased.`) core.setFailed(`🚀 To fix the versions, in your terminal go to project root path and run the command below:`) - console.log(`\n${command}\n`) + console.log(`\n# Linux +${linuxCommand} +\n +# MacOS +${macCommand}\n` + ) } diff --git a/.github/actions/git-diff-on-components/package.json b/.github/actions/git-diff-on-components/package.json index 44506d4f02619..d9e38528402ad 100644 --- a/.github/actions/git-diff-on-components/package.json +++ b/.github/actions/git-diff-on-components/package.json @@ -5,7 +5,7 @@ "main": "dist/index.js", "scripts": { "clean": "rm -rf ./dist", - "build": "npm run clean && ncc build src/index.js --license licenses.txt" + "build": "pnpm run clean && ncc build src/index.js --license licenses.txt" }, "keywords": [], "author": "pipedream", diff --git a/.github/actions/git-diff-on-components/src/index.js b/.github/actions/git-diff-on-components/src/index.js index e071cf9bf231c..495544e514925 100755 --- a/.github/actions/git-diff-on-components/src/index.js +++ b/.github/actions/git-diff-on-components/src/index.js @@ -348,15 +348,17 @@ async function run() { } if (componentsDiffContents.length) { - let command = '' + let linuxCommand = '' + let macCommand = '' - for ({ dependencyFilePath, componentFilePath } of componentsDiffContents) { + for ({ componentFilePath } of componentsDiffContents) { try { const content = await readFile(componentFilePath, "utf-8") const currentVersion = getVersion(content) const increasedVersion = increaseVersion(currentVersion) - command += `${command.length ? " && " : ""}sed -i 0,/${currentVersion}/{s/${currentVersion}/${increasedVersion}/} ${getComponentFilePath(componentFilePath)}` + linuxCommand += `${linuxCommand.length ? " && " : ""}sed -i 0,/${currentVersion}/{s/${currentVersion}/${increasedVersion}/} ${getComponentFilePath(componentFilePath)}` + macCommand += `${macCommand.length ? " && " : ""}sed -i '' 's/${currentVersion}/${increasedVersion}/' ${getComponentFilePath(componentFilePath)}` } catch (error) { console.error(`❌ Error checking component diff of ${getComponentFilePath(componentFilePath)}: ${error}`); } @@ -365,7 +367,12 @@ async function run() { core.setFailed(`❌ Version of ${componentsDiffContents.length} dependencies needs to be increased.`) core.setFailed(`🚀 To fix the versions, in your terminal go to project root path and run the command below:`) - console.log(`\n${command}\n`) + console.log(`\n# Linux +${linuxCommand} +\n +# MacOS +${macCommand}\n` + ) } diff --git a/.github/actions/push-registry-app-files-supabase/README.md b/.github/actions/push-registry-app-files-supabase/README.md new file mode 100644 index 0000000000000..81253df684c0e --- /dev/null +++ b/.github/actions/push-registry-app-files-supabase/README.md @@ -0,0 +1,67 @@ +# Push registry app files to Supabase + +This action pushes new and modified `*.app.mjs/*.app.ts` files to Supabase. + +## Inputs + +### `changed_files` + +**Required** List of changed files. + +### `supabase_anon_key` + +**Required** Supabase key + +``` +... +with: + format: json +``` + +in that way `steps.changed_files.outputs.all` will be converted into an array of strings + +## Example usage + +```yaml + - name: Upload modified or newly added *.app.mjs files to Supabase + uses: ./.github/actions/push-registry-app-files-supabase + with: + changed_files: ${{ steps.changed_files.outputs.all }} + supabase_anon_key: ${{ secrets.SUPABASE_ANON_KEY }} +``` + +## Local Development + Build + +Run the following command from this action directory to update `dist/index.js` if `src/index.js` has been modified: +```bash +cd .github/actions/push-registry-app-files-supabase +npm run dev +``` + +The above command will build and run the dist/index.js. Commit the `dist` directory. +You might want to change the `on` trigger to `push` event on your local branch when making changes to the action so you +can test it on GitHub: + +## Master +```yaml +# .github/workflows/push-registry-app-files-supabase.yaml +on: + pull_request: + branches: + - master + paths: + - 'components/**' +``` + +## During development +```yaml +# .github/workflows/push-registry-app-files-supabase.yaml +on: + push: + branches: + - feature-branch-name + paths: + - 'components/**' + - '.github/actions/push-registry-app-files-supabase/**' +``` + diff --git a/.github/actions/push-registry-app-files-supabase/action.yml b/.github/actions/push-registry-app-files-supabase/action.yml new file mode 100644 index 0000000000000..dee2d915e6202 --- /dev/null +++ b/.github/actions/push-registry-app-files-supabase/action.yml @@ -0,0 +1,15 @@ +name: "Push registry app files to Supabase" +description: "Push registry app files that have changed to Supabase. If any *app.ts files have changed, run pnpm build for the affected app to generate the *.app.mjs file." +inputs: + changed_files: + description: "List of all changed files coming from the push_to_supabase job of a github action workflow" + required: true + supabase_anon_key: + description: "Supabase anon key" + required: true + supabase_uri: + description: "Supabase URI" + required: true +runs: + using: "node20" + main: "dist/index.js" \ No newline at end of file diff --git a/.github/actions/push-registry-app-files-supabase/dist/index.js b/.github/actions/push-registry-app-files-supabase/dist/index.js new file mode 100644 index 0000000000000..1847f8988087b --- /dev/null +++ b/.github/actions/push-registry-app-files-supabase/dist/index.js @@ -0,0 +1,44911 @@ +/******/ (() => { // webpackBootstrap +/******/ var __webpack_modules__ = ({ + +/***/ 7351: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.issue = exports.issueCommand = void 0; +const os = __importStar(__nccwpck_require__(2037)); +const utils_1 = __nccwpck_require__(5278); +/** + * Commands + * + * Command Format: + * ::name key=value,key=value::message + * + * Examples: + * ::warning::This is the message + * ::set-env name=MY_VAR::some value + */ +function issueCommand(command, properties, message) { + const cmd = new Command(command, properties, message); + process.stdout.write(cmd.toString() + os.EOL); +} +exports.issueCommand = issueCommand; +function issue(name, message = '') { + issueCommand(name, {}, message); +} +exports.issue = issue; +const CMD_STRING = '::'; +class Command { + constructor(command, properties, message) { + if (!command) { + command = 'missing.command'; + } + this.command = command; + this.properties = properties; + this.message = message; + } + toString() { + let cmdStr = CMD_STRING + this.command; + if (this.properties && Object.keys(this.properties).length > 0) { + cmdStr += ' '; + let first = true; + for (const key in this.properties) { + if (this.properties.hasOwnProperty(key)) { + const val = this.properties[key]; + if (val) { + if (first) { + first = false; + } + else { + cmdStr += ','; + } + cmdStr += `${key}=${escapeProperty(val)}`; + } + } + } + } + cmdStr += `${CMD_STRING}${escapeData(this.message)}`; + return cmdStr; + } +} +function escapeData(s) { + return (0, utils_1.toCommandValue)(s) + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A'); +} +function escapeProperty(s) { + return (0, utils_1.toCommandValue)(s) + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A') + .replace(/:/g, '%3A') + .replace(/,/g, '%2C'); +} +//# sourceMappingURL=command.js.map + +/***/ }), + +/***/ 2186: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.platform = exports.toPlatformPath = exports.toWin32Path = exports.toPosixPath = exports.markdownSummary = exports.summary = exports.getIDToken = exports.getState = exports.saveState = exports.group = exports.endGroup = exports.startGroup = exports.info = exports.notice = exports.warning = exports.error = exports.debug = exports.isDebug = exports.setFailed = exports.setCommandEcho = exports.setOutput = exports.getBooleanInput = exports.getMultilineInput = exports.getInput = exports.addPath = exports.setSecret = exports.exportVariable = exports.ExitCode = void 0; +const command_1 = __nccwpck_require__(7351); +const file_command_1 = __nccwpck_require__(717); +const utils_1 = __nccwpck_require__(5278); +const os = __importStar(__nccwpck_require__(2037)); +const path = __importStar(__nccwpck_require__(1017)); +const oidc_utils_1 = __nccwpck_require__(8041); +/** + * The code to exit an action + */ +var ExitCode; +(function (ExitCode) { + /** + * A code indicating that the action was successful + */ + ExitCode[ExitCode["Success"] = 0] = "Success"; + /** + * A code indicating that the action was a failure + */ + ExitCode[ExitCode["Failure"] = 1] = "Failure"; +})(ExitCode || (exports.ExitCode = ExitCode = {})); +//----------------------------------------------------------------------- +// Variables +//----------------------------------------------------------------------- +/** + * Sets env variable for this action and future actions in the job + * @param name the name of the variable to set + * @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function exportVariable(name, val) { + const convertedVal = (0, utils_1.toCommandValue)(val); + process.env[name] = convertedVal; + const filePath = process.env['GITHUB_ENV'] || ''; + if (filePath) { + return (0, file_command_1.issueFileCommand)('ENV', (0, file_command_1.prepareKeyValueMessage)(name, val)); + } + (0, command_1.issueCommand)('set-env', { name }, convertedVal); +} +exports.exportVariable = exportVariable; +/** + * Registers a secret which will get masked from logs + * @param secret value of the secret + */ +function setSecret(secret) { + (0, command_1.issueCommand)('add-mask', {}, secret); +} +exports.setSecret = setSecret; +/** + * Prepends inputPath to the PATH (for this action and future actions) + * @param inputPath + */ +function addPath(inputPath) { + const filePath = process.env['GITHUB_PATH'] || ''; + if (filePath) { + (0, file_command_1.issueFileCommand)('PATH', inputPath); + } + else { + (0, command_1.issueCommand)('add-path', {}, inputPath); + } + process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`; +} +exports.addPath = addPath; +/** + * Gets the value of an input. + * Unless trimWhitespace is set to false in InputOptions, the value is also trimmed. + * Returns an empty string if the value is not defined. + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns string + */ +function getInput(name, options) { + const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || ''; + if (options && options.required && !val) { + throw new Error(`Input required and not supplied: ${name}`); + } + if (options && options.trimWhitespace === false) { + return val; + } + return val.trim(); +} +exports.getInput = getInput; +/** + * Gets the values of an multiline input. Each value is also trimmed. + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns string[] + * + */ +function getMultilineInput(name, options) { + const inputs = getInput(name, options) + .split('\n') + .filter(x => x !== ''); + if (options && options.trimWhitespace === false) { + return inputs; + } + return inputs.map(input => input.trim()); +} +exports.getMultilineInput = getMultilineInput; +/** + * Gets the input value of the boolean type in the YAML 1.2 "core schema" specification. + * Support boolean input list: `true | True | TRUE | false | False | FALSE` . + * The return value is also in boolean type. + * ref: https://yaml.org/spec/1.2/spec.html#id2804923 + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns boolean + */ +function getBooleanInput(name, options) { + const trueValue = ['true', 'True', 'TRUE']; + const falseValue = ['false', 'False', 'FALSE']; + const val = getInput(name, options); + if (trueValue.includes(val)) + return true; + if (falseValue.includes(val)) + return false; + throw new TypeError(`Input does not meet YAML 1.2 "Core Schema" specification: ${name}\n` + + `Support boolean input list: \`true | True | TRUE | false | False | FALSE\``); +} +exports.getBooleanInput = getBooleanInput; +/** + * Sets the value of an output. + * + * @param name name of the output to set + * @param value value to store. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function setOutput(name, value) { + const filePath = process.env['GITHUB_OUTPUT'] || ''; + if (filePath) { + return (0, file_command_1.issueFileCommand)('OUTPUT', (0, file_command_1.prepareKeyValueMessage)(name, value)); + } + process.stdout.write(os.EOL); + (0, command_1.issueCommand)('set-output', { name }, (0, utils_1.toCommandValue)(value)); +} +exports.setOutput = setOutput; +/** + * Enables or disables the echoing of commands into stdout for the rest of the step. + * Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set. + * + */ +function setCommandEcho(enabled) { + (0, command_1.issue)('echo', enabled ? 'on' : 'off'); +} +exports.setCommandEcho = setCommandEcho; +//----------------------------------------------------------------------- +// Results +//----------------------------------------------------------------------- +/** + * Sets the action status to failed. + * When the action exits it will be with an exit code of 1 + * @param message add error issue message + */ +function setFailed(message) { + process.exitCode = ExitCode.Failure; + error(message); +} +exports.setFailed = setFailed; +//----------------------------------------------------------------------- +// Logging Commands +//----------------------------------------------------------------------- +/** + * Gets whether Actions Step Debug is on or not + */ +function isDebug() { + return process.env['RUNNER_DEBUG'] === '1'; +} +exports.isDebug = isDebug; +/** + * Writes debug message to user log + * @param message debug message + */ +function debug(message) { + (0, command_1.issueCommand)('debug', {}, message); +} +exports.debug = debug; +/** + * Adds an error issue + * @param message error issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. + */ +function error(message, properties = {}) { + (0, command_1.issueCommand)('error', (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message); +} +exports.error = error; +/** + * Adds a warning issue + * @param message warning issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. + */ +function warning(message, properties = {}) { + (0, command_1.issueCommand)('warning', (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message); +} +exports.warning = warning; +/** + * Adds a notice issue + * @param message notice issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. + */ +function notice(message, properties = {}) { + (0, command_1.issueCommand)('notice', (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message); +} +exports.notice = notice; +/** + * Writes info to log with console.log. + * @param message info message + */ +function info(message) { + process.stdout.write(message + os.EOL); +} +exports.info = info; +/** + * Begin an output group. + * + * Output until the next `groupEnd` will be foldable in this group + * + * @param name The name of the output group + */ +function startGroup(name) { + (0, command_1.issue)('group', name); +} +exports.startGroup = startGroup; +/** + * End an output group. + */ +function endGroup() { + (0, command_1.issue)('endgroup'); +} +exports.endGroup = endGroup; +/** + * Wrap an asynchronous function call in a group. + * + * Returns the same type as the function itself. + * + * @param name The name of the group + * @param fn The function to wrap in the group + */ +function group(name, fn) { + return __awaiter(this, void 0, void 0, function* () { + startGroup(name); + let result; + try { + result = yield fn(); + } + finally { + endGroup(); + } + return result; + }); +} +exports.group = group; +//----------------------------------------------------------------------- +// Wrapper action state +//----------------------------------------------------------------------- +/** + * Saves state for current action, the state can only be retrieved by this action's post job execution. + * + * @param name name of the state to store + * @param value value to store. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function saveState(name, value) { + const filePath = process.env['GITHUB_STATE'] || ''; + if (filePath) { + return (0, file_command_1.issueFileCommand)('STATE', (0, file_command_1.prepareKeyValueMessage)(name, value)); + } + (0, command_1.issueCommand)('save-state', { name }, (0, utils_1.toCommandValue)(value)); +} +exports.saveState = saveState; +/** + * Gets the value of an state set by this action's main execution. + * + * @param name name of the state to get + * @returns string + */ +function getState(name) { + return process.env[`STATE_${name}`] || ''; +} +exports.getState = getState; +function getIDToken(aud) { + return __awaiter(this, void 0, void 0, function* () { + return yield oidc_utils_1.OidcClient.getIDToken(aud); + }); +} +exports.getIDToken = getIDToken; +/** + * Summary exports + */ +var summary_1 = __nccwpck_require__(1327); +Object.defineProperty(exports, "summary", ({ enumerable: true, get: function () { return summary_1.summary; } })); +/** + * @deprecated use core.summary + */ +var summary_2 = __nccwpck_require__(1327); +Object.defineProperty(exports, "markdownSummary", ({ enumerable: true, get: function () { return summary_2.markdownSummary; } })); +/** + * Path exports + */ +var path_utils_1 = __nccwpck_require__(2981); +Object.defineProperty(exports, "toPosixPath", ({ enumerable: true, get: function () { return path_utils_1.toPosixPath; } })); +Object.defineProperty(exports, "toWin32Path", ({ enumerable: true, get: function () { return path_utils_1.toWin32Path; } })); +Object.defineProperty(exports, "toPlatformPath", ({ enumerable: true, get: function () { return path_utils_1.toPlatformPath; } })); +/** + * Platform utilities exports + */ +exports.platform = __importStar(__nccwpck_require__(5243)); +//# sourceMappingURL=core.js.map + +/***/ }), + +/***/ 717: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +// For internal use, subject to change. +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.prepareKeyValueMessage = exports.issueFileCommand = void 0; +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +const crypto = __importStar(__nccwpck_require__(6113)); +const fs = __importStar(__nccwpck_require__(7147)); +const os = __importStar(__nccwpck_require__(2037)); +const utils_1 = __nccwpck_require__(5278); +function issueFileCommand(command, message) { + const filePath = process.env[`GITHUB_${command}`]; + if (!filePath) { + throw new Error(`Unable to find environment variable for file command ${command}`); + } + if (!fs.existsSync(filePath)) { + throw new Error(`Missing file at path: ${filePath}`); + } + fs.appendFileSync(filePath, `${(0, utils_1.toCommandValue)(message)}${os.EOL}`, { + encoding: 'utf8' + }); +} +exports.issueFileCommand = issueFileCommand; +function prepareKeyValueMessage(key, value) { + const delimiter = `ghadelimiter_${crypto.randomUUID()}`; + const convertedValue = (0, utils_1.toCommandValue)(value); + // These should realistically never happen, but just in case someone finds a + // way to exploit uuid generation let's not allow keys or values that contain + // the delimiter. + if (key.includes(delimiter)) { + throw new Error(`Unexpected input: name should not contain the delimiter "${delimiter}"`); + } + if (convertedValue.includes(delimiter)) { + throw new Error(`Unexpected input: value should not contain the delimiter "${delimiter}"`); + } + return `${key}<<${delimiter}${os.EOL}${convertedValue}${os.EOL}${delimiter}`; +} +exports.prepareKeyValueMessage = prepareKeyValueMessage; +//# sourceMappingURL=file-command.js.map + +/***/ }), + +/***/ 8041: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.OidcClient = void 0; +const http_client_1 = __nccwpck_require__(6255); +const auth_1 = __nccwpck_require__(5526); +const core_1 = __nccwpck_require__(2186); +class OidcClient { + static createHttpClient(allowRetry = true, maxRetry = 10) { + const requestOptions = { + allowRetries: allowRetry, + maxRetries: maxRetry + }; + return new http_client_1.HttpClient('actions/oidc-client', [new auth_1.BearerCredentialHandler(OidcClient.getRequestToken())], requestOptions); + } + static getRequestToken() { + const token = process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN']; + if (!token) { + throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_TOKEN env variable'); + } + return token; + } + static getIDTokenUrl() { + const runtimeUrl = process.env['ACTIONS_ID_TOKEN_REQUEST_URL']; + if (!runtimeUrl) { + throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable'); + } + return runtimeUrl; + } + static getCall(id_token_url) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + const httpclient = OidcClient.createHttpClient(); + const res = yield httpclient + .getJson(id_token_url) + .catch(error => { + throw new Error(`Failed to get ID Token. \n + Error Code : ${error.statusCode}\n + Error Message: ${error.message}`); + }); + const id_token = (_a = res.result) === null || _a === void 0 ? void 0 : _a.value; + if (!id_token) { + throw new Error('Response json body do not have ID Token field'); + } + return id_token; + }); + } + static getIDToken(audience) { + return __awaiter(this, void 0, void 0, function* () { + try { + // New ID Token is requested from action service + let id_token_url = OidcClient.getIDTokenUrl(); + if (audience) { + const encodedAudience = encodeURIComponent(audience); + id_token_url = `${id_token_url}&audience=${encodedAudience}`; + } + (0, core_1.debug)(`ID token url is ${id_token_url}`); + const id_token = yield OidcClient.getCall(id_token_url); + (0, core_1.setSecret)(id_token); + return id_token; + } + catch (error) { + throw new Error(`Error message: ${error.message}`); + } + }); + } +} +exports.OidcClient = OidcClient; +//# sourceMappingURL=oidc-utils.js.map + +/***/ }), + +/***/ 2981: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.toPlatformPath = exports.toWin32Path = exports.toPosixPath = void 0; +const path = __importStar(__nccwpck_require__(1017)); +/** + * toPosixPath converts the given path to the posix form. On Windows, \\ will be + * replaced with /. + * + * @param pth. Path to transform. + * @return string Posix path. + */ +function toPosixPath(pth) { + return pth.replace(/[\\]/g, '/'); +} +exports.toPosixPath = toPosixPath; +/** + * toWin32Path converts the given path to the win32 form. On Linux, / will be + * replaced with \\. + * + * @param pth. Path to transform. + * @return string Win32 path. + */ +function toWin32Path(pth) { + return pth.replace(/[/]/g, '\\'); +} +exports.toWin32Path = toWin32Path; +/** + * toPlatformPath converts the given path to a platform-specific path. It does + * this by replacing instances of / and \ with the platform-specific path + * separator. + * + * @param pth The path to platformize. + * @return string The platform-specific path. + */ +function toPlatformPath(pth) { + return pth.replace(/[/\\]/g, path.sep); +} +exports.toPlatformPath = toPlatformPath; +//# sourceMappingURL=path-utils.js.map + +/***/ }), + +/***/ 5243: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getDetails = exports.isLinux = exports.isMacOS = exports.isWindows = exports.arch = exports.platform = void 0; +const os_1 = __importDefault(__nccwpck_require__(2037)); +const exec = __importStar(__nccwpck_require__(1514)); +const getWindowsInfo = () => __awaiter(void 0, void 0, void 0, function* () { + const { stdout: version } = yield exec.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Version"', undefined, { + silent: true + }); + const { stdout: name } = yield exec.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Caption"', undefined, { + silent: true + }); + return { + name: name.trim(), + version: version.trim() + }; +}); +const getMacOsInfo = () => __awaiter(void 0, void 0, void 0, function* () { + var _a, _b, _c, _d; + const { stdout } = yield exec.getExecOutput('sw_vers', undefined, { + silent: true + }); + const version = (_b = (_a = stdout.match(/ProductVersion:\s*(.+)/)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : ''; + const name = (_d = (_c = stdout.match(/ProductName:\s*(.+)/)) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : ''; + return { + name, + version + }; +}); +const getLinuxInfo = () => __awaiter(void 0, void 0, void 0, function* () { + const { stdout } = yield exec.getExecOutput('lsb_release', ['-i', '-r', '-s'], { + silent: true + }); + const [name, version] = stdout.trim().split('\n'); + return { + name, + version + }; +}); +exports.platform = os_1.default.platform(); +exports.arch = os_1.default.arch(); +exports.isWindows = exports.platform === 'win32'; +exports.isMacOS = exports.platform === 'darwin'; +exports.isLinux = exports.platform === 'linux'; +function getDetails() { + return __awaiter(this, void 0, void 0, function* () { + return Object.assign(Object.assign({}, (yield (exports.isWindows + ? getWindowsInfo() + : exports.isMacOS + ? getMacOsInfo() + : getLinuxInfo()))), { platform: exports.platform, + arch: exports.arch, + isWindows: exports.isWindows, + isMacOS: exports.isMacOS, + isLinux: exports.isLinux }); + }); +} +exports.getDetails = getDetails; +//# sourceMappingURL=platform.js.map + +/***/ }), + +/***/ 1327: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0; +const os_1 = __nccwpck_require__(2037); +const fs_1 = __nccwpck_require__(7147); +const { access, appendFile, writeFile } = fs_1.promises; +exports.SUMMARY_ENV_VAR = 'GITHUB_STEP_SUMMARY'; +exports.SUMMARY_DOCS_URL = 'https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary'; +class Summary { + constructor() { + this._buffer = ''; + } + /** + * Finds the summary file path from the environment, rejects if env var is not found or file does not exist + * Also checks r/w permissions. + * + * @returns step summary file path + */ + filePath() { + return __awaiter(this, void 0, void 0, function* () { + if (this._filePath) { + return this._filePath; + } + const pathFromEnv = process.env[exports.SUMMARY_ENV_VAR]; + if (!pathFromEnv) { + throw new Error(`Unable to find environment variable for $${exports.SUMMARY_ENV_VAR}. Check if your runtime environment supports job summaries.`); + } + try { + yield access(pathFromEnv, fs_1.constants.R_OK | fs_1.constants.W_OK); + } + catch (_a) { + throw new Error(`Unable to access summary file: '${pathFromEnv}'. Check if the file has correct read/write permissions.`); + } + this._filePath = pathFromEnv; + return this._filePath; + }); + } + /** + * Wraps content in an HTML tag, adding any HTML attributes + * + * @param {string} tag HTML tag to wrap + * @param {string | null} content content within the tag + * @param {[attribute: string]: string} attrs key-value list of HTML attributes to add + * + * @returns {string} content wrapped in HTML element + */ + wrap(tag, content, attrs = {}) { + const htmlAttrs = Object.entries(attrs) + .map(([key, value]) => ` ${key}="${value}"`) + .join(''); + if (!content) { + return `<${tag}${htmlAttrs}>`; + } + return `<${tag}${htmlAttrs}>${content}`; + } + /** + * Writes text in the buffer to the summary buffer file and empties buffer. Will append by default. + * + * @param {SummaryWriteOptions} [options] (optional) options for write operation + * + * @returns {Promise} summary instance + */ + write(options) { + return __awaiter(this, void 0, void 0, function* () { + const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite); + const filePath = yield this.filePath(); + const writeFunc = overwrite ? writeFile : appendFile; + yield writeFunc(filePath, this._buffer, { encoding: 'utf8' }); + return this.emptyBuffer(); + }); + } + /** + * Clears the summary buffer and wipes the summary file + * + * @returns {Summary} summary instance + */ + clear() { + return __awaiter(this, void 0, void 0, function* () { + return this.emptyBuffer().write({ overwrite: true }); + }); + } + /** + * Returns the current summary buffer as a string + * + * @returns {string} string of summary buffer + */ + stringify() { + return this._buffer; + } + /** + * If the summary buffer is empty + * + * @returns {boolen} true if the buffer is empty + */ + isEmptyBuffer() { + return this._buffer.length === 0; + } + /** + * Resets the summary buffer without writing to summary file + * + * @returns {Summary} summary instance + */ + emptyBuffer() { + this._buffer = ''; + return this; + } + /** + * Adds raw text to the summary buffer + * + * @param {string} text content to add + * @param {boolean} [addEOL=false] (optional) append an EOL to the raw text (default: false) + * + * @returns {Summary} summary instance + */ + addRaw(text, addEOL = false) { + this._buffer += text; + return addEOL ? this.addEOL() : this; + } + /** + * Adds the operating system-specific end-of-line marker to the buffer + * + * @returns {Summary} summary instance + */ + addEOL() { + return this.addRaw(os_1.EOL); + } + /** + * Adds an HTML codeblock to the summary buffer + * + * @param {string} code content to render within fenced code block + * @param {string} lang (optional) language to syntax highlight code + * + * @returns {Summary} summary instance + */ + addCodeBlock(code, lang) { + const attrs = Object.assign({}, (lang && { lang })); + const element = this.wrap('pre', this.wrap('code', code), attrs); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML list to the summary buffer + * + * @param {string[]} items list of items to render + * @param {boolean} [ordered=false] (optional) if the rendered list should be ordered or not (default: false) + * + * @returns {Summary} summary instance + */ + addList(items, ordered = false) { + const tag = ordered ? 'ol' : 'ul'; + const listItems = items.map(item => this.wrap('li', item)).join(''); + const element = this.wrap(tag, listItems); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML table to the summary buffer + * + * @param {SummaryTableCell[]} rows table rows + * + * @returns {Summary} summary instance + */ + addTable(rows) { + const tableBody = rows + .map(row => { + const cells = row + .map(cell => { + if (typeof cell === 'string') { + return this.wrap('td', cell); + } + const { header, data, colspan, rowspan } = cell; + const tag = header ? 'th' : 'td'; + const attrs = Object.assign(Object.assign({}, (colspan && { colspan })), (rowspan && { rowspan })); + return this.wrap(tag, data, attrs); + }) + .join(''); + return this.wrap('tr', cells); + }) + .join(''); + const element = this.wrap('table', tableBody); + return this.addRaw(element).addEOL(); + } + /** + * Adds a collapsable HTML details element to the summary buffer + * + * @param {string} label text for the closed state + * @param {string} content collapsable content + * + * @returns {Summary} summary instance + */ + addDetails(label, content) { + const element = this.wrap('details', this.wrap('summary', label) + content); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML image tag to the summary buffer + * + * @param {string} src path to the image you to embed + * @param {string} alt text description of the image + * @param {SummaryImageOptions} options (optional) addition image attributes + * + * @returns {Summary} summary instance + */ + addImage(src, alt, options) { + const { width, height } = options || {}; + const attrs = Object.assign(Object.assign({}, (width && { width })), (height && { height })); + const element = this.wrap('img', null, Object.assign({ src, alt }, attrs)); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML section heading element + * + * @param {string} text heading text + * @param {number | string} [level=1] (optional) the heading level, default: 1 + * + * @returns {Summary} summary instance + */ + addHeading(text, level) { + const tag = `h${level}`; + const allowedTag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag) + ? tag + : 'h1'; + const element = this.wrap(allowedTag, text); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML thematic break (
) to the summary buffer + * + * @returns {Summary} summary instance + */ + addSeparator() { + const element = this.wrap('hr', null); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML line break (
) to the summary buffer + * + * @returns {Summary} summary instance + */ + addBreak() { + const element = this.wrap('br', null); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML blockquote to the summary buffer + * + * @param {string} text quote text + * @param {string} cite (optional) citation url + * + * @returns {Summary} summary instance + */ + addQuote(text, cite) { + const attrs = Object.assign({}, (cite && { cite })); + const element = this.wrap('blockquote', text, attrs); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML anchor tag to the summary buffer + * + * @param {string} text link text/content + * @param {string} href hyperlink + * + * @returns {Summary} summary instance + */ + addLink(text, href) { + const element = this.wrap('a', text, { href }); + return this.addRaw(element).addEOL(); + } +} +const _summary = new Summary(); +/** + * @deprecated use `core.summary` + */ +exports.markdownSummary = _summary; +exports.summary = _summary; +//# sourceMappingURL=summary.js.map + +/***/ }), + +/***/ 5278: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.toCommandProperties = exports.toCommandValue = void 0; +/** + * Sanitizes an input into a string so it can be passed into issueCommand safely + * @param input input to sanitize into a string + */ +function toCommandValue(input) { + if (input === null || input === undefined) { + return ''; + } + else if (typeof input === 'string' || input instanceof String) { + return input; + } + return JSON.stringify(input); +} +exports.toCommandValue = toCommandValue; +/** + * + * @param annotationProperties + * @returns The command properties to send with the actual annotation command + * See IssueCommandProperties: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs#L646 + */ +function toCommandProperties(annotationProperties) { + if (!Object.keys(annotationProperties).length) { + return {}; + } + return { + title: annotationProperties.title, + file: annotationProperties.file, + line: annotationProperties.startLine, + endLine: annotationProperties.endLine, + col: annotationProperties.startColumn, + endColumn: annotationProperties.endColumn + }; +} +exports.toCommandProperties = toCommandProperties; +//# sourceMappingURL=utils.js.map + +/***/ }), + +/***/ 1514: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getExecOutput = exports.exec = void 0; +const string_decoder_1 = __nccwpck_require__(1576); +const tr = __importStar(__nccwpck_require__(8159)); +/** + * Exec a command. + * Output will be streamed to the live console. + * Returns promise with return code + * + * @param commandLine command to execute (can include additional args). Must be correctly escaped. + * @param args optional arguments for tool. Escaping is handled by the lib. + * @param options optional exec options. See ExecOptions + * @returns Promise exit code + */ +function exec(commandLine, args, options) { + return __awaiter(this, void 0, void 0, function* () { + const commandArgs = tr.argStringToArray(commandLine); + if (commandArgs.length === 0) { + throw new Error(`Parameter 'commandLine' cannot be null or empty.`); + } + // Path to tool to execute should be first arg + const toolPath = commandArgs[0]; + args = commandArgs.slice(1).concat(args || []); + const runner = new tr.ToolRunner(toolPath, args, options); + return runner.exec(); + }); +} +exports.exec = exec; +/** + * Exec a command and get the output. + * Output will be streamed to the live console. + * Returns promise with the exit code and collected stdout and stderr + * + * @param commandLine command to execute (can include additional args). Must be correctly escaped. + * @param args optional arguments for tool. Escaping is handled by the lib. + * @param options optional exec options. See ExecOptions + * @returns Promise exit code, stdout, and stderr + */ +function getExecOutput(commandLine, args, options) { + var _a, _b; + return __awaiter(this, void 0, void 0, function* () { + let stdout = ''; + let stderr = ''; + //Using string decoder covers the case where a mult-byte character is split + const stdoutDecoder = new string_decoder_1.StringDecoder('utf8'); + const stderrDecoder = new string_decoder_1.StringDecoder('utf8'); + const originalStdoutListener = (_a = options === null || options === void 0 ? void 0 : options.listeners) === null || _a === void 0 ? void 0 : _a.stdout; + const originalStdErrListener = (_b = options === null || options === void 0 ? void 0 : options.listeners) === null || _b === void 0 ? void 0 : _b.stderr; + const stdErrListener = (data) => { + stderr += stderrDecoder.write(data); + if (originalStdErrListener) { + originalStdErrListener(data); + } + }; + const stdOutListener = (data) => { + stdout += stdoutDecoder.write(data); + if (originalStdoutListener) { + originalStdoutListener(data); + } + }; + const listeners = Object.assign(Object.assign({}, options === null || options === void 0 ? void 0 : options.listeners), { stdout: stdOutListener, stderr: stdErrListener }); + const exitCode = yield exec(commandLine, args, Object.assign(Object.assign({}, options), { listeners })); + //flush any remaining characters + stdout += stdoutDecoder.end(); + stderr += stderrDecoder.end(); + return { + exitCode, + stdout, + stderr + }; + }); +} +exports.getExecOutput = getExecOutput; +//# sourceMappingURL=exec.js.map + +/***/ }), + +/***/ 8159: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.argStringToArray = exports.ToolRunner = void 0; +const os = __importStar(__nccwpck_require__(2037)); +const events = __importStar(__nccwpck_require__(2361)); +const child = __importStar(__nccwpck_require__(2081)); +const path = __importStar(__nccwpck_require__(1017)); +const io = __importStar(__nccwpck_require__(7436)); +const ioUtil = __importStar(__nccwpck_require__(1962)); +const timers_1 = __nccwpck_require__(9512); +/* eslint-disable @typescript-eslint/unbound-method */ +const IS_WINDOWS = process.platform === 'win32'; +/* + * Class for running command line tools. Handles quoting and arg parsing in a platform agnostic way. + */ +class ToolRunner extends events.EventEmitter { + constructor(toolPath, args, options) { + super(); + if (!toolPath) { + throw new Error("Parameter 'toolPath' cannot be null or empty."); + } + this.toolPath = toolPath; + this.args = args || []; + this.options = options || {}; + } + _debug(message) { + if (this.options.listeners && this.options.listeners.debug) { + this.options.listeners.debug(message); + } + } + _getCommandString(options, noPrefix) { + const toolPath = this._getSpawnFileName(); + const args = this._getSpawnArgs(options); + let cmd = noPrefix ? '' : '[command]'; // omit prefix when piped to a second tool + if (IS_WINDOWS) { + // Windows + cmd file + if (this._isCmdFile()) { + cmd += toolPath; + for (const a of args) { + cmd += ` ${a}`; + } + } + // Windows + verbatim + else if (options.windowsVerbatimArguments) { + cmd += `"${toolPath}"`; + for (const a of args) { + cmd += ` ${a}`; + } + } + // Windows (regular) + else { + cmd += this._windowsQuoteCmdArg(toolPath); + for (const a of args) { + cmd += ` ${this._windowsQuoteCmdArg(a)}`; + } + } + } + else { + // OSX/Linux - this can likely be improved with some form of quoting. + // creating processes on Unix is fundamentally different than Windows. + // on Unix, execvp() takes an arg array. + cmd += toolPath; + for (const a of args) { + cmd += ` ${a}`; + } + } + return cmd; + } + _processLineBuffer(data, strBuffer, onLine) { + try { + let s = strBuffer + data.toString(); + let n = s.indexOf(os.EOL); + while (n > -1) { + const line = s.substring(0, n); + onLine(line); + // the rest of the string ... + s = s.substring(n + os.EOL.length); + n = s.indexOf(os.EOL); + } + return s; + } + catch (err) { + // streaming lines to console is best effort. Don't fail a build. + this._debug(`error processing line. Failed with error ${err}`); + return ''; + } + } + _getSpawnFileName() { + if (IS_WINDOWS) { + if (this._isCmdFile()) { + return process.env['COMSPEC'] || 'cmd.exe'; + } + } + return this.toolPath; + } + _getSpawnArgs(options) { + if (IS_WINDOWS) { + if (this._isCmdFile()) { + let argline = `/D /S /C "${this._windowsQuoteCmdArg(this.toolPath)}`; + for (const a of this.args) { + argline += ' '; + argline += options.windowsVerbatimArguments + ? a + : this._windowsQuoteCmdArg(a); + } + argline += '"'; + return [argline]; + } + } + return this.args; + } + _endsWith(str, end) { + return str.endsWith(end); + } + _isCmdFile() { + const upperToolPath = this.toolPath.toUpperCase(); + return (this._endsWith(upperToolPath, '.CMD') || + this._endsWith(upperToolPath, '.BAT')); + } + _windowsQuoteCmdArg(arg) { + // for .exe, apply the normal quoting rules that libuv applies + if (!this._isCmdFile()) { + return this._uvQuoteCmdArg(arg); + } + // otherwise apply quoting rules specific to the cmd.exe command line parser. + // the libuv rules are generic and are not designed specifically for cmd.exe + // command line parser. + // + // for a detailed description of the cmd.exe command line parser, refer to + // http://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/7970912#7970912 + // need quotes for empty arg + if (!arg) { + return '""'; + } + // determine whether the arg needs to be quoted + const cmdSpecialChars = [ + ' ', + '\t', + '&', + '(', + ')', + '[', + ']', + '{', + '}', + '^', + '=', + ';', + '!', + "'", + '+', + ',', + '`', + '~', + '|', + '<', + '>', + '"' + ]; + let needsQuotes = false; + for (const char of arg) { + if (cmdSpecialChars.some(x => x === char)) { + needsQuotes = true; + break; + } + } + // short-circuit if quotes not needed + if (!needsQuotes) { + return arg; + } + // the following quoting rules are very similar to the rules that by libuv applies. + // + // 1) wrap the string in quotes + // + // 2) double-up quotes - i.e. " => "" + // + // this is different from the libuv quoting rules. libuv replaces " with \", which unfortunately + // doesn't work well with a cmd.exe command line. + // + // note, replacing " with "" also works well if the arg is passed to a downstream .NET console app. + // for example, the command line: + // foo.exe "myarg:""my val""" + // is parsed by a .NET console app into an arg array: + // [ "myarg:\"my val\"" ] + // which is the same end result when applying libuv quoting rules. although the actual + // command line from libuv quoting rules would look like: + // foo.exe "myarg:\"my val\"" + // + // 3) double-up slashes that precede a quote, + // e.g. hello \world => "hello \world" + // hello\"world => "hello\\""world" + // hello\\"world => "hello\\\\""world" + // hello world\ => "hello world\\" + // + // technically this is not required for a cmd.exe command line, or the batch argument parser. + // the reasons for including this as a .cmd quoting rule are: + // + // a) this is optimized for the scenario where the argument is passed from the .cmd file to an + // external program. many programs (e.g. .NET console apps) rely on the slash-doubling rule. + // + // b) it's what we've been doing previously (by deferring to node default behavior) and we + // haven't heard any complaints about that aspect. + // + // note, a weakness of the quoting rules chosen here, is that % is not escaped. in fact, % cannot be + // escaped when used on the command line directly - even though within a .cmd file % can be escaped + // by using %%. + // + // the saving grace is, on the command line, %var% is left as-is if var is not defined. this contrasts + // the line parsing rules within a .cmd file, where if var is not defined it is replaced with nothing. + // + // one option that was explored was replacing % with ^% - i.e. %var% => ^%var^%. this hack would + // often work, since it is unlikely that var^ would exist, and the ^ character is removed when the + // variable is used. the problem, however, is that ^ is not removed when %* is used to pass the args + // to an external program. + // + // an unexplored potential solution for the % escaping problem, is to create a wrapper .cmd file. + // % can be escaped within a .cmd file. + let reverse = '"'; + let quoteHit = true; + for (let i = arg.length; i > 0; i--) { + // walk the string in reverse + reverse += arg[i - 1]; + if (quoteHit && arg[i - 1] === '\\') { + reverse += '\\'; // double the slash + } + else if (arg[i - 1] === '"') { + quoteHit = true; + reverse += '"'; // double the quote + } + else { + quoteHit = false; + } + } + reverse += '"'; + return reverse + .split('') + .reverse() + .join(''); + } + _uvQuoteCmdArg(arg) { + // Tool runner wraps child_process.spawn() and needs to apply the same quoting as + // Node in certain cases where the undocumented spawn option windowsVerbatimArguments + // is used. + // + // Since this function is a port of quote_cmd_arg from Node 4.x (technically, lib UV, + // see https://github.com/nodejs/node/blob/v4.x/deps/uv/src/win/process.c for details), + // pasting copyright notice from Node within this function: + // + // Copyright Joyent, Inc. and other Node contributors. All rights reserved. + // + // Permission is hereby granted, free of charge, to any person obtaining a copy + // of this software and associated documentation files (the "Software"), to + // deal in the Software without restriction, including without limitation the + // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + // sell copies of the Software, and to permit persons to whom the Software is + // furnished to do so, subject to the following conditions: + // + // The above copyright notice and this permission notice shall be included in + // all copies or substantial portions of the Software. + // + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + // IN THE SOFTWARE. + if (!arg) { + // Need double quotation for empty argument + return '""'; + } + if (!arg.includes(' ') && !arg.includes('\t') && !arg.includes('"')) { + // No quotation needed + return arg; + } + if (!arg.includes('"') && !arg.includes('\\')) { + // No embedded double quotes or backslashes, so I can just wrap + // quote marks around the whole thing. + return `"${arg}"`; + } + // Expected input/output: + // input : hello"world + // output: "hello\"world" + // input : hello""world + // output: "hello\"\"world" + // input : hello\world + // output: hello\world + // input : hello\\world + // output: hello\\world + // input : hello\"world + // output: "hello\\\"world" + // input : hello\\"world + // output: "hello\\\\\"world" + // input : hello world\ + // output: "hello world\\" - note the comment in libuv actually reads "hello world\" + // but it appears the comment is wrong, it should be "hello world\\" + let reverse = '"'; + let quoteHit = true; + for (let i = arg.length; i > 0; i--) { + // walk the string in reverse + reverse += arg[i - 1]; + if (quoteHit && arg[i - 1] === '\\') { + reverse += '\\'; + } + else if (arg[i - 1] === '"') { + quoteHit = true; + reverse += '\\'; + } + else { + quoteHit = false; + } + } + reverse += '"'; + return reverse + .split('') + .reverse() + .join(''); + } + _cloneExecOptions(options) { + options = options || {}; + const result = { + cwd: options.cwd || process.cwd(), + env: options.env || process.env, + silent: options.silent || false, + windowsVerbatimArguments: options.windowsVerbatimArguments || false, + failOnStdErr: options.failOnStdErr || false, + ignoreReturnCode: options.ignoreReturnCode || false, + delay: options.delay || 10000 + }; + result.outStream = options.outStream || process.stdout; + result.errStream = options.errStream || process.stderr; + return result; + } + _getSpawnOptions(options, toolPath) { + options = options || {}; + const result = {}; + result.cwd = options.cwd; + result.env = options.env; + result['windowsVerbatimArguments'] = + options.windowsVerbatimArguments || this._isCmdFile(); + if (options.windowsVerbatimArguments) { + result.argv0 = `"${toolPath}"`; + } + return result; + } + /** + * Exec a tool. + * Output will be streamed to the live console. + * Returns promise with return code + * + * @param tool path to tool to exec + * @param options optional exec options. See ExecOptions + * @returns number + */ + exec() { + return __awaiter(this, void 0, void 0, function* () { + // root the tool path if it is unrooted and contains relative pathing + if (!ioUtil.isRooted(this.toolPath) && + (this.toolPath.includes('/') || + (IS_WINDOWS && this.toolPath.includes('\\')))) { + // prefer options.cwd if it is specified, however options.cwd may also need to be rooted + this.toolPath = path.resolve(process.cwd(), this.options.cwd || process.cwd(), this.toolPath); + } + // if the tool is only a file name, then resolve it from the PATH + // otherwise verify it exists (add extension on Windows if necessary) + this.toolPath = yield io.which(this.toolPath, true); + return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { + this._debug(`exec tool: ${this.toolPath}`); + this._debug('arguments:'); + for (const arg of this.args) { + this._debug(` ${arg}`); + } + const optionsNonNull = this._cloneExecOptions(this.options); + if (!optionsNonNull.silent && optionsNonNull.outStream) { + optionsNonNull.outStream.write(this._getCommandString(optionsNonNull) + os.EOL); + } + const state = new ExecState(optionsNonNull, this.toolPath); + state.on('debug', (message) => { + this._debug(message); + }); + if (this.options.cwd && !(yield ioUtil.exists(this.options.cwd))) { + return reject(new Error(`The cwd: ${this.options.cwd} does not exist!`)); + } + const fileName = this._getSpawnFileName(); + const cp = child.spawn(fileName, this._getSpawnArgs(optionsNonNull), this._getSpawnOptions(this.options, fileName)); + let stdbuffer = ''; + if (cp.stdout) { + cp.stdout.on('data', (data) => { + if (this.options.listeners && this.options.listeners.stdout) { + this.options.listeners.stdout(data); + } + if (!optionsNonNull.silent && optionsNonNull.outStream) { + optionsNonNull.outStream.write(data); + } + stdbuffer = this._processLineBuffer(data, stdbuffer, (line) => { + if (this.options.listeners && this.options.listeners.stdline) { + this.options.listeners.stdline(line); + } + }); + }); + } + let errbuffer = ''; + if (cp.stderr) { + cp.stderr.on('data', (data) => { + state.processStderr = true; + if (this.options.listeners && this.options.listeners.stderr) { + this.options.listeners.stderr(data); + } + if (!optionsNonNull.silent && + optionsNonNull.errStream && + optionsNonNull.outStream) { + const s = optionsNonNull.failOnStdErr + ? optionsNonNull.errStream + : optionsNonNull.outStream; + s.write(data); + } + errbuffer = this._processLineBuffer(data, errbuffer, (line) => { + if (this.options.listeners && this.options.listeners.errline) { + this.options.listeners.errline(line); + } + }); + }); + } + cp.on('error', (err) => { + state.processError = err.message; + state.processExited = true; + state.processClosed = true; + state.CheckComplete(); + }); + cp.on('exit', (code) => { + state.processExitCode = code; + state.processExited = true; + this._debug(`Exit code ${code} received from tool '${this.toolPath}'`); + state.CheckComplete(); + }); + cp.on('close', (code) => { + state.processExitCode = code; + state.processExited = true; + state.processClosed = true; + this._debug(`STDIO streams have closed for tool '${this.toolPath}'`); + state.CheckComplete(); + }); + state.on('done', (error, exitCode) => { + if (stdbuffer.length > 0) { + this.emit('stdline', stdbuffer); + } + if (errbuffer.length > 0) { + this.emit('errline', errbuffer); + } + cp.removeAllListeners(); + if (error) { + reject(error); + } + else { + resolve(exitCode); + } + }); + if (this.options.input) { + if (!cp.stdin) { + throw new Error('child process missing stdin'); + } + cp.stdin.end(this.options.input); + } + })); + }); + } +} +exports.ToolRunner = ToolRunner; +/** + * Convert an arg string to an array of args. Handles escaping + * + * @param argString string of arguments + * @returns string[] array of arguments + */ +function argStringToArray(argString) { + const args = []; + let inQuotes = false; + let escaped = false; + let arg = ''; + function append(c) { + // we only escape double quotes. + if (escaped && c !== '"') { + arg += '\\'; + } + arg += c; + escaped = false; + } + for (let i = 0; i < argString.length; i++) { + const c = argString.charAt(i); + if (c === '"') { + if (!escaped) { + inQuotes = !inQuotes; + } + else { + append(c); + } + continue; + } + if (c === '\\' && escaped) { + append(c); + continue; + } + if (c === '\\' && inQuotes) { + escaped = true; + continue; + } + if (c === ' ' && !inQuotes) { + if (arg.length > 0) { + args.push(arg); + arg = ''; + } + continue; + } + append(c); + } + if (arg.length > 0) { + args.push(arg.trim()); + } + return args; +} +exports.argStringToArray = argStringToArray; +class ExecState extends events.EventEmitter { + constructor(options, toolPath) { + super(); + this.processClosed = false; // tracks whether the process has exited and stdio is closed + this.processError = ''; + this.processExitCode = 0; + this.processExited = false; // tracks whether the process has exited + this.processStderr = false; // tracks whether stderr was written to + this.delay = 10000; // 10 seconds + this.done = false; + this.timeout = null; + if (!toolPath) { + throw new Error('toolPath must not be empty'); + } + this.options = options; + this.toolPath = toolPath; + if (options.delay) { + this.delay = options.delay; + } + } + CheckComplete() { + if (this.done) { + return; + } + if (this.processClosed) { + this._setResult(); + } + else if (this.processExited) { + this.timeout = timers_1.setTimeout(ExecState.HandleTimeout, this.delay, this); + } + } + _debug(message) { + this.emit('debug', message); + } + _setResult() { + // determine whether there is an error + let error; + if (this.processExited) { + if (this.processError) { + error = new Error(`There was an error when attempting to execute the process '${this.toolPath}'. This may indicate the process failed to start. Error: ${this.processError}`); + } + else if (this.processExitCode !== 0 && !this.options.ignoreReturnCode) { + error = new Error(`The process '${this.toolPath}' failed with exit code ${this.processExitCode}`); + } + else if (this.processStderr && this.options.failOnStdErr) { + error = new Error(`The process '${this.toolPath}' failed because one or more lines were written to the STDERR stream`); + } + } + // clear the timeout + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = null; + } + this.done = true; + this.emit('done', error, this.processExitCode); + } + static HandleTimeout(state) { + if (state.done) { + return; + } + if (!state.processClosed && state.processExited) { + const message = `The STDIO streams did not close within ${state.delay / + 1000} seconds of the exit event from process '${state.toolPath}'. This may indicate a child process inherited the STDIO streams and has not yet exited.`; + state._debug(message); + } + state._setResult(); + } +} +//# sourceMappingURL=toolrunner.js.map + +/***/ }), + +/***/ 5526: +/***/ (function(__unused_webpack_module, exports) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.PersonalAccessTokenCredentialHandler = exports.BearerCredentialHandler = exports.BasicCredentialHandler = void 0; +class BasicCredentialHandler { + constructor(username, password) { + this.username = username; + this.password = password; + } + prepareRequest(options) { + if (!options.headers) { + throw Error('The request has no headers'); + } + options.headers['Authorization'] = `Basic ${Buffer.from(`${this.username}:${this.password}`).toString('base64')}`; + } + // This handler cannot handle 401 + canHandleAuthentication() { + return false; + } + handleAuthentication() { + return __awaiter(this, void 0, void 0, function* () { + throw new Error('not implemented'); + }); + } +} +exports.BasicCredentialHandler = BasicCredentialHandler; +class BearerCredentialHandler { + constructor(token) { + this.token = token; + } + // currently implements pre-authorization + // TODO: support preAuth = false where it hooks on 401 + prepareRequest(options) { + if (!options.headers) { + throw Error('The request has no headers'); + } + options.headers['Authorization'] = `Bearer ${this.token}`; + } + // This handler cannot handle 401 + canHandleAuthentication() { + return false; + } + handleAuthentication() { + return __awaiter(this, void 0, void 0, function* () { + throw new Error('not implemented'); + }); + } +} +exports.BearerCredentialHandler = BearerCredentialHandler; +class PersonalAccessTokenCredentialHandler { + constructor(token) { + this.token = token; + } + // currently implements pre-authorization + // TODO: support preAuth = false where it hooks on 401 + prepareRequest(options) { + if (!options.headers) { + throw Error('The request has no headers'); + } + options.headers['Authorization'] = `Basic ${Buffer.from(`PAT:${this.token}`).toString('base64')}`; + } + // This handler cannot handle 401 + canHandleAuthentication() { + return false; + } + handleAuthentication() { + return __awaiter(this, void 0, void 0, function* () { + throw new Error('not implemented'); + }); + } +} +exports.PersonalAccessTokenCredentialHandler = PersonalAccessTokenCredentialHandler; +//# sourceMappingURL=auth.js.map + +/***/ }), + +/***/ 6255: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.HttpClient = exports.isHttps = exports.HttpClientResponse = exports.HttpClientError = exports.getProxyUrl = exports.MediaTypes = exports.Headers = exports.HttpCodes = void 0; +const http = __importStar(__nccwpck_require__(3685)); +const https = __importStar(__nccwpck_require__(5687)); +const pm = __importStar(__nccwpck_require__(9835)); +const tunnel = __importStar(__nccwpck_require__(4294)); +const undici_1 = __nccwpck_require__(1773); +var HttpCodes; +(function (HttpCodes) { + HttpCodes[HttpCodes["OK"] = 200] = "OK"; + HttpCodes[HttpCodes["MultipleChoices"] = 300] = "MultipleChoices"; + HttpCodes[HttpCodes["MovedPermanently"] = 301] = "MovedPermanently"; + HttpCodes[HttpCodes["ResourceMoved"] = 302] = "ResourceMoved"; + HttpCodes[HttpCodes["SeeOther"] = 303] = "SeeOther"; + HttpCodes[HttpCodes["NotModified"] = 304] = "NotModified"; + HttpCodes[HttpCodes["UseProxy"] = 305] = "UseProxy"; + HttpCodes[HttpCodes["SwitchProxy"] = 306] = "SwitchProxy"; + HttpCodes[HttpCodes["TemporaryRedirect"] = 307] = "TemporaryRedirect"; + HttpCodes[HttpCodes["PermanentRedirect"] = 308] = "PermanentRedirect"; + HttpCodes[HttpCodes["BadRequest"] = 400] = "BadRequest"; + HttpCodes[HttpCodes["Unauthorized"] = 401] = "Unauthorized"; + HttpCodes[HttpCodes["PaymentRequired"] = 402] = "PaymentRequired"; + HttpCodes[HttpCodes["Forbidden"] = 403] = "Forbidden"; + HttpCodes[HttpCodes["NotFound"] = 404] = "NotFound"; + HttpCodes[HttpCodes["MethodNotAllowed"] = 405] = "MethodNotAllowed"; + HttpCodes[HttpCodes["NotAcceptable"] = 406] = "NotAcceptable"; + HttpCodes[HttpCodes["ProxyAuthenticationRequired"] = 407] = "ProxyAuthenticationRequired"; + HttpCodes[HttpCodes["RequestTimeout"] = 408] = "RequestTimeout"; + HttpCodes[HttpCodes["Conflict"] = 409] = "Conflict"; + HttpCodes[HttpCodes["Gone"] = 410] = "Gone"; + HttpCodes[HttpCodes["TooManyRequests"] = 429] = "TooManyRequests"; + HttpCodes[HttpCodes["InternalServerError"] = 500] = "InternalServerError"; + HttpCodes[HttpCodes["NotImplemented"] = 501] = "NotImplemented"; + HttpCodes[HttpCodes["BadGateway"] = 502] = "BadGateway"; + HttpCodes[HttpCodes["ServiceUnavailable"] = 503] = "ServiceUnavailable"; + HttpCodes[HttpCodes["GatewayTimeout"] = 504] = "GatewayTimeout"; +})(HttpCodes || (exports.HttpCodes = HttpCodes = {})); +var Headers; +(function (Headers) { + Headers["Accept"] = "accept"; + Headers["ContentType"] = "content-type"; +})(Headers || (exports.Headers = Headers = {})); +var MediaTypes; +(function (MediaTypes) { + MediaTypes["ApplicationJson"] = "application/json"; +})(MediaTypes || (exports.MediaTypes = MediaTypes = {})); +/** + * Returns the proxy URL, depending upon the supplied url and proxy environment variables. + * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com + */ +function getProxyUrl(serverUrl) { + const proxyUrl = pm.getProxyUrl(new URL(serverUrl)); + return proxyUrl ? proxyUrl.href : ''; +} +exports.getProxyUrl = getProxyUrl; +const HttpRedirectCodes = [ + HttpCodes.MovedPermanently, + HttpCodes.ResourceMoved, + HttpCodes.SeeOther, + HttpCodes.TemporaryRedirect, + HttpCodes.PermanentRedirect +]; +const HttpResponseRetryCodes = [ + HttpCodes.BadGateway, + HttpCodes.ServiceUnavailable, + HttpCodes.GatewayTimeout +]; +const RetryableHttpVerbs = ['OPTIONS', 'GET', 'DELETE', 'HEAD']; +const ExponentialBackoffCeiling = 10; +const ExponentialBackoffTimeSlice = 5; +class HttpClientError extends Error { + constructor(message, statusCode) { + super(message); + this.name = 'HttpClientError'; + this.statusCode = statusCode; + Object.setPrototypeOf(this, HttpClientError.prototype); + } +} +exports.HttpClientError = HttpClientError; +class HttpClientResponse { + constructor(message) { + this.message = message; + } + readBody() { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { + let output = Buffer.alloc(0); + this.message.on('data', (chunk) => { + output = Buffer.concat([output, chunk]); + }); + this.message.on('end', () => { + resolve(output.toString()); + }); + })); + }); + } + readBodyBuffer() { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { + const chunks = []; + this.message.on('data', (chunk) => { + chunks.push(chunk); + }); + this.message.on('end', () => { + resolve(Buffer.concat(chunks)); + }); + })); + }); + } +} +exports.HttpClientResponse = HttpClientResponse; +function isHttps(requestUrl) { + const parsedUrl = new URL(requestUrl); + return parsedUrl.protocol === 'https:'; +} +exports.isHttps = isHttps; +class HttpClient { + constructor(userAgent, handlers, requestOptions) { + this._ignoreSslError = false; + this._allowRedirects = true; + this._allowRedirectDowngrade = false; + this._maxRedirects = 50; + this._allowRetries = false; + this._maxRetries = 1; + this._keepAlive = false; + this._disposed = false; + this.userAgent = userAgent; + this.handlers = handlers || []; + this.requestOptions = requestOptions; + if (requestOptions) { + if (requestOptions.ignoreSslError != null) { + this._ignoreSslError = requestOptions.ignoreSslError; + } + this._socketTimeout = requestOptions.socketTimeout; + if (requestOptions.allowRedirects != null) { + this._allowRedirects = requestOptions.allowRedirects; + } + if (requestOptions.allowRedirectDowngrade != null) { + this._allowRedirectDowngrade = requestOptions.allowRedirectDowngrade; + } + if (requestOptions.maxRedirects != null) { + this._maxRedirects = Math.max(requestOptions.maxRedirects, 0); + } + if (requestOptions.keepAlive != null) { + this._keepAlive = requestOptions.keepAlive; + } + if (requestOptions.allowRetries != null) { + this._allowRetries = requestOptions.allowRetries; + } + if (requestOptions.maxRetries != null) { + this._maxRetries = requestOptions.maxRetries; + } + } + } + options(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('OPTIONS', requestUrl, null, additionalHeaders || {}); + }); + } + get(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('GET', requestUrl, null, additionalHeaders || {}); + }); + } + del(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('DELETE', requestUrl, null, additionalHeaders || {}); + }); + } + post(requestUrl, data, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('POST', requestUrl, data, additionalHeaders || {}); + }); + } + patch(requestUrl, data, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('PATCH', requestUrl, data, additionalHeaders || {}); + }); + } + put(requestUrl, data, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('PUT', requestUrl, data, additionalHeaders || {}); + }); + } + head(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('HEAD', requestUrl, null, additionalHeaders || {}); + }); + } + sendStream(verb, requestUrl, stream, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request(verb, requestUrl, stream, additionalHeaders); + }); + } + /** + * Gets a typed object from an endpoint + * Be aware that not found returns a null. Other errors (4xx, 5xx) reject the promise + */ + getJson(requestUrl, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + const res = yield this.get(requestUrl, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + postJson(requestUrl, obj, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + const data = JSON.stringify(obj, null, 2); + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); + const res = yield this.post(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + putJson(requestUrl, obj, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + const data = JSON.stringify(obj, null, 2); + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); + const res = yield this.put(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + patchJson(requestUrl, obj, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + const data = JSON.stringify(obj, null, 2); + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); + const res = yield this.patch(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + /** + * Makes a raw http request. + * All other methods such as get, post, patch, and request ultimately call this. + * Prefer get, del, post and patch + */ + request(verb, requestUrl, data, headers) { + return __awaiter(this, void 0, void 0, function* () { + if (this._disposed) { + throw new Error('Client has already been disposed.'); + } + const parsedUrl = new URL(requestUrl); + let info = this._prepareRequest(verb, parsedUrl, headers); + // Only perform retries on reads since writes may not be idempotent. + const maxTries = this._allowRetries && RetryableHttpVerbs.includes(verb) + ? this._maxRetries + 1 + : 1; + let numTries = 0; + let response; + do { + response = yield this.requestRaw(info, data); + // Check if it's an authentication challenge + if (response && + response.message && + response.message.statusCode === HttpCodes.Unauthorized) { + let authenticationHandler; + for (const handler of this.handlers) { + if (handler.canHandleAuthentication(response)) { + authenticationHandler = handler; + break; + } + } + if (authenticationHandler) { + return authenticationHandler.handleAuthentication(this, info, data); + } + else { + // We have received an unauthorized response but have no handlers to handle it. + // Let the response return to the caller. + return response; + } + } + let redirectsRemaining = this._maxRedirects; + while (response.message.statusCode && + HttpRedirectCodes.includes(response.message.statusCode) && + this._allowRedirects && + redirectsRemaining > 0) { + const redirectUrl = response.message.headers['location']; + if (!redirectUrl) { + // if there's no location to redirect to, we won't + break; + } + const parsedRedirectUrl = new URL(redirectUrl); + if (parsedUrl.protocol === 'https:' && + parsedUrl.protocol !== parsedRedirectUrl.protocol && + !this._allowRedirectDowngrade) { + throw new Error('Redirect from HTTPS to HTTP protocol. This downgrade is not allowed for security reasons. If you want to allow this behavior, set the allowRedirectDowngrade option to true.'); + } + // we need to finish reading the response before reassigning response + // which will leak the open socket. + yield response.readBody(); + // strip authorization header if redirected to a different hostname + if (parsedRedirectUrl.hostname !== parsedUrl.hostname) { + for (const header in headers) { + // header names are case insensitive + if (header.toLowerCase() === 'authorization') { + delete headers[header]; + } + } + } + // let's make the request with the new redirectUrl + info = this._prepareRequest(verb, parsedRedirectUrl, headers); + response = yield this.requestRaw(info, data); + redirectsRemaining--; + } + if (!response.message.statusCode || + !HttpResponseRetryCodes.includes(response.message.statusCode)) { + // If not a retry code, return immediately instead of retrying + return response; + } + numTries += 1; + if (numTries < maxTries) { + yield response.readBody(); + yield this._performExponentialBackoff(numTries); + } + } while (numTries < maxTries); + return response; + }); + } + /** + * Needs to be called if keepAlive is set to true in request options. + */ + dispose() { + if (this._agent) { + this._agent.destroy(); + } + this._disposed = true; + } + /** + * Raw request. + * @param info + * @param data + */ + requestRaw(info, data) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve, reject) => { + function callbackForResult(err, res) { + if (err) { + reject(err); + } + else if (!res) { + // If `err` is not passed, then `res` must be passed. + reject(new Error('Unknown error')); + } + else { + resolve(res); + } + } + this.requestRawWithCallback(info, data, callbackForResult); + }); + }); + } + /** + * Raw request with callback. + * @param info + * @param data + * @param onResult + */ + requestRawWithCallback(info, data, onResult) { + if (typeof data === 'string') { + if (!info.options.headers) { + info.options.headers = {}; + } + info.options.headers['Content-Length'] = Buffer.byteLength(data, 'utf8'); + } + let callbackCalled = false; + function handleResult(err, res) { + if (!callbackCalled) { + callbackCalled = true; + onResult(err, res); + } + } + const req = info.httpModule.request(info.options, (msg) => { + const res = new HttpClientResponse(msg); + handleResult(undefined, res); + }); + let socket; + req.on('socket', sock => { + socket = sock; + }); + // If we ever get disconnected, we want the socket to timeout eventually + req.setTimeout(this._socketTimeout || 3 * 60000, () => { + if (socket) { + socket.end(); + } + handleResult(new Error(`Request timeout: ${info.options.path}`)); + }); + req.on('error', function (err) { + // err has statusCode property + // res should have headers + handleResult(err); + }); + if (data && typeof data === 'string') { + req.write(data, 'utf8'); + } + if (data && typeof data !== 'string') { + data.on('close', function () { + req.end(); + }); + data.pipe(req); + } + else { + req.end(); + } + } + /** + * Gets an http agent. This function is useful when you need an http agent that handles + * routing through a proxy server - depending upon the url and proxy environment variables. + * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com + */ + getAgent(serverUrl) { + const parsedUrl = new URL(serverUrl); + return this._getAgent(parsedUrl); + } + getAgentDispatcher(serverUrl) { + const parsedUrl = new URL(serverUrl); + const proxyUrl = pm.getProxyUrl(parsedUrl); + const useProxy = proxyUrl && proxyUrl.hostname; + if (!useProxy) { + return; + } + return this._getProxyAgentDispatcher(parsedUrl, proxyUrl); + } + _prepareRequest(method, requestUrl, headers) { + const info = {}; + info.parsedUrl = requestUrl; + const usingSsl = info.parsedUrl.protocol === 'https:'; + info.httpModule = usingSsl ? https : http; + const defaultPort = usingSsl ? 443 : 80; + info.options = {}; + info.options.host = info.parsedUrl.hostname; + info.options.port = info.parsedUrl.port + ? parseInt(info.parsedUrl.port) + : defaultPort; + info.options.path = + (info.parsedUrl.pathname || '') + (info.parsedUrl.search || ''); + info.options.method = method; + info.options.headers = this._mergeHeaders(headers); + if (this.userAgent != null) { + info.options.headers['user-agent'] = this.userAgent; + } + info.options.agent = this._getAgent(info.parsedUrl); + // gives handlers an opportunity to participate + if (this.handlers) { + for (const handler of this.handlers) { + handler.prepareRequest(info.options); + } + } + return info; + } + _mergeHeaders(headers) { + if (this.requestOptions && this.requestOptions.headers) { + return Object.assign({}, lowercaseKeys(this.requestOptions.headers), lowercaseKeys(headers || {})); + } + return lowercaseKeys(headers || {}); + } + _getExistingOrDefaultHeader(additionalHeaders, header, _default) { + let clientHeader; + if (this.requestOptions && this.requestOptions.headers) { + clientHeader = lowercaseKeys(this.requestOptions.headers)[header]; + } + return additionalHeaders[header] || clientHeader || _default; + } + _getAgent(parsedUrl) { + let agent; + const proxyUrl = pm.getProxyUrl(parsedUrl); + const useProxy = proxyUrl && proxyUrl.hostname; + if (this._keepAlive && useProxy) { + agent = this._proxyAgent; + } + if (!useProxy) { + agent = this._agent; + } + // if agent is already assigned use that agent. + if (agent) { + return agent; + } + const usingSsl = parsedUrl.protocol === 'https:'; + let maxSockets = 100; + if (this.requestOptions) { + maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets; + } + // This is `useProxy` again, but we need to check `proxyURl` directly for TypeScripts's flow analysis. + if (proxyUrl && proxyUrl.hostname) { + const agentOptions = { + maxSockets, + keepAlive: this._keepAlive, + proxy: Object.assign(Object.assign({}, ((proxyUrl.username || proxyUrl.password) && { + proxyAuth: `${proxyUrl.username}:${proxyUrl.password}` + })), { host: proxyUrl.hostname, port: proxyUrl.port }) + }; + let tunnelAgent; + const overHttps = proxyUrl.protocol === 'https:'; + if (usingSsl) { + tunnelAgent = overHttps ? tunnel.httpsOverHttps : tunnel.httpsOverHttp; + } + else { + tunnelAgent = overHttps ? tunnel.httpOverHttps : tunnel.httpOverHttp; + } + agent = tunnelAgent(agentOptions); + this._proxyAgent = agent; + } + // if tunneling agent isn't assigned create a new agent + if (!agent) { + const options = { keepAlive: this._keepAlive, maxSockets }; + agent = usingSsl ? new https.Agent(options) : new http.Agent(options); + this._agent = agent; + } + if (usingSsl && this._ignoreSslError) { + // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process + // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options + // we have to cast it to any and change it directly + agent.options = Object.assign(agent.options || {}, { + rejectUnauthorized: false + }); + } + return agent; + } + _getProxyAgentDispatcher(parsedUrl, proxyUrl) { + let proxyAgent; + if (this._keepAlive) { + proxyAgent = this._proxyAgentDispatcher; + } + // if agent is already assigned use that agent. + if (proxyAgent) { + return proxyAgent; + } + const usingSsl = parsedUrl.protocol === 'https:'; + proxyAgent = new undici_1.ProxyAgent(Object.assign({ uri: proxyUrl.href, pipelining: !this._keepAlive ? 0 : 1 }, ((proxyUrl.username || proxyUrl.password) && { + token: `Basic ${Buffer.from(`${proxyUrl.username}:${proxyUrl.password}`).toString('base64')}` + }))); + this._proxyAgentDispatcher = proxyAgent; + if (usingSsl && this._ignoreSslError) { + // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process + // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options + // we have to cast it to any and change it directly + proxyAgent.options = Object.assign(proxyAgent.options.requestTls || {}, { + rejectUnauthorized: false + }); + } + return proxyAgent; + } + _performExponentialBackoff(retryNumber) { + return __awaiter(this, void 0, void 0, function* () { + retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber); + const ms = ExponentialBackoffTimeSlice * Math.pow(2, retryNumber); + return new Promise(resolve => setTimeout(() => resolve(), ms)); + }); + } + _processResponse(res, options) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { + const statusCode = res.message.statusCode || 0; + const response = { + statusCode, + result: null, + headers: {} + }; + // not found leads to null obj returned + if (statusCode === HttpCodes.NotFound) { + resolve(response); + } + // get the result from the body + function dateTimeDeserializer(key, value) { + if (typeof value === 'string') { + const a = new Date(value); + if (!isNaN(a.valueOf())) { + return a; + } + } + return value; + } + let obj; + let contents; + try { + contents = yield res.readBody(); + if (contents && contents.length > 0) { + if (options && options.deserializeDates) { + obj = JSON.parse(contents, dateTimeDeserializer); + } + else { + obj = JSON.parse(contents); + } + response.result = obj; + } + response.headers = res.message.headers; + } + catch (err) { + // Invalid resource (contents not json); leaving result obj null + } + // note that 3xx redirects are handled by the http layer. + if (statusCode > 299) { + let msg; + // if exception/error in body, attempt to get better error + if (obj && obj.message) { + msg = obj.message; + } + else if (contents && contents.length > 0) { + // it may be the case that the exception is in the body message as string + msg = contents; + } + else { + msg = `Failed request: (${statusCode})`; + } + const err = new HttpClientError(msg, statusCode); + err.result = response.result; + reject(err); + } + else { + resolve(response); + } + })); + }); + } +} +exports.HttpClient = HttpClient; +const lowercaseKeys = (obj) => Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {}); +//# sourceMappingURL=index.js.map + +/***/ }), + +/***/ 9835: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.checkBypass = exports.getProxyUrl = void 0; +function getProxyUrl(reqUrl) { + const usingSsl = reqUrl.protocol === 'https:'; + if (checkBypass(reqUrl)) { + return undefined; + } + const proxyVar = (() => { + if (usingSsl) { + return process.env['https_proxy'] || process.env['HTTPS_PROXY']; + } + else { + return process.env['http_proxy'] || process.env['HTTP_PROXY']; + } + })(); + if (proxyVar) { + try { + return new DecodedURL(proxyVar); + } + catch (_a) { + if (!proxyVar.startsWith('http://') && !proxyVar.startsWith('https://')) + return new DecodedURL(`http://${proxyVar}`); + } + } + else { + return undefined; + } +} +exports.getProxyUrl = getProxyUrl; +function checkBypass(reqUrl) { + if (!reqUrl.hostname) { + return false; + } + const reqHost = reqUrl.hostname; + if (isLoopbackAddress(reqHost)) { + return true; + } + const noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || ''; + if (!noProxy) { + return false; + } + // Determine the request port + let reqPort; + if (reqUrl.port) { + reqPort = Number(reqUrl.port); + } + else if (reqUrl.protocol === 'http:') { + reqPort = 80; + } + else if (reqUrl.protocol === 'https:') { + reqPort = 443; + } + // Format the request hostname and hostname with port + const upperReqHosts = [reqUrl.hostname.toUpperCase()]; + if (typeof reqPort === 'number') { + upperReqHosts.push(`${upperReqHosts[0]}:${reqPort}`); + } + // Compare request host against noproxy + for (const upperNoProxyItem of noProxy + .split(',') + .map(x => x.trim().toUpperCase()) + .filter(x => x)) { + if (upperNoProxyItem === '*' || + upperReqHosts.some(x => x === upperNoProxyItem || + x.endsWith(`.${upperNoProxyItem}`) || + (upperNoProxyItem.startsWith('.') && + x.endsWith(`${upperNoProxyItem}`)))) { + return true; + } + } + return false; +} +exports.checkBypass = checkBypass; +function isLoopbackAddress(host) { + const hostLower = host.toLowerCase(); + return (hostLower === 'localhost' || + hostLower.startsWith('127.') || + hostLower.startsWith('[::1]') || + hostLower.startsWith('[0:0:0:0:0:0:0:1]')); +} +class DecodedURL extends URL { + constructor(url, base) { + super(url, base); + this._decodedUsername = decodeURIComponent(super.username); + this._decodedPassword = decodeURIComponent(super.password); + } + get username() { + return this._decodedUsername; + } + get password() { + return this._decodedPassword; + } +} +//# sourceMappingURL=proxy.js.map + +/***/ }), + +/***/ 1962: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var _a; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getCmdPath = exports.tryGetExecutablePath = exports.isRooted = exports.isDirectory = exports.exists = exports.READONLY = exports.UV_FS_O_EXLOCK = exports.IS_WINDOWS = exports.unlink = exports.symlink = exports.stat = exports.rmdir = exports.rm = exports.rename = exports.readlink = exports.readdir = exports.open = exports.mkdir = exports.lstat = exports.copyFile = exports.chmod = void 0; +const fs = __importStar(__nccwpck_require__(7147)); +const path = __importStar(__nccwpck_require__(1017)); +_a = fs.promises +// export const {open} = 'fs' +, exports.chmod = _a.chmod, exports.copyFile = _a.copyFile, exports.lstat = _a.lstat, exports.mkdir = _a.mkdir, exports.open = _a.open, exports.readdir = _a.readdir, exports.readlink = _a.readlink, exports.rename = _a.rename, exports.rm = _a.rm, exports.rmdir = _a.rmdir, exports.stat = _a.stat, exports.symlink = _a.symlink, exports.unlink = _a.unlink; +// export const {open} = 'fs' +exports.IS_WINDOWS = process.platform === 'win32'; +// See https://github.com/nodejs/node/blob/d0153aee367422d0858105abec186da4dff0a0c5/deps/uv/include/uv/win.h#L691 +exports.UV_FS_O_EXLOCK = 0x10000000; +exports.READONLY = fs.constants.O_RDONLY; +function exists(fsPath) { + return __awaiter(this, void 0, void 0, function* () { + try { + yield exports.stat(fsPath); + } + catch (err) { + if (err.code === 'ENOENT') { + return false; + } + throw err; + } + return true; + }); +} +exports.exists = exists; +function isDirectory(fsPath, useStat = false) { + return __awaiter(this, void 0, void 0, function* () { + const stats = useStat ? yield exports.stat(fsPath) : yield exports.lstat(fsPath); + return stats.isDirectory(); + }); +} +exports.isDirectory = isDirectory; +/** + * On OSX/Linux, true if path starts with '/'. On Windows, true for paths like: + * \, \hello, \\hello\share, C:, and C:\hello (and corresponding alternate separator cases). + */ +function isRooted(p) { + p = normalizeSeparators(p); + if (!p) { + throw new Error('isRooted() parameter "p" cannot be empty'); + } + if (exports.IS_WINDOWS) { + return (p.startsWith('\\') || /^[A-Z]:/i.test(p) // e.g. \ or \hello or \\hello + ); // e.g. C: or C:\hello + } + return p.startsWith('/'); +} +exports.isRooted = isRooted; +/** + * Best effort attempt to determine whether a file exists and is executable. + * @param filePath file path to check + * @param extensions additional file extensions to try + * @return if file exists and is executable, returns the file path. otherwise empty string. + */ +function tryGetExecutablePath(filePath, extensions) { + return __awaiter(this, void 0, void 0, function* () { + let stats = undefined; + try { + // test file exists + stats = yield exports.stat(filePath); + } + catch (err) { + if (err.code !== 'ENOENT') { + // eslint-disable-next-line no-console + console.log(`Unexpected error attempting to determine if executable file exists '${filePath}': ${err}`); + } + } + if (stats && stats.isFile()) { + if (exports.IS_WINDOWS) { + // on Windows, test for valid extension + const upperExt = path.extname(filePath).toUpperCase(); + if (extensions.some(validExt => validExt.toUpperCase() === upperExt)) { + return filePath; + } + } + else { + if (isUnixExecutable(stats)) { + return filePath; + } + } + } + // try each extension + const originalFilePath = filePath; + for (const extension of extensions) { + filePath = originalFilePath + extension; + stats = undefined; + try { + stats = yield exports.stat(filePath); + } + catch (err) { + if (err.code !== 'ENOENT') { + // eslint-disable-next-line no-console + console.log(`Unexpected error attempting to determine if executable file exists '${filePath}': ${err}`); + } + } + if (stats && stats.isFile()) { + if (exports.IS_WINDOWS) { + // preserve the case of the actual file (since an extension was appended) + try { + const directory = path.dirname(filePath); + const upperName = path.basename(filePath).toUpperCase(); + for (const actualName of yield exports.readdir(directory)) { + if (upperName === actualName.toUpperCase()) { + filePath = path.join(directory, actualName); + break; + } + } + } + catch (err) { + // eslint-disable-next-line no-console + console.log(`Unexpected error attempting to determine the actual case of the file '${filePath}': ${err}`); + } + return filePath; + } + else { + if (isUnixExecutable(stats)) { + return filePath; + } + } + } + } + return ''; + }); +} +exports.tryGetExecutablePath = tryGetExecutablePath; +function normalizeSeparators(p) { + p = p || ''; + if (exports.IS_WINDOWS) { + // convert slashes on Windows + p = p.replace(/\//g, '\\'); + // remove redundant slashes + return p.replace(/\\\\+/g, '\\'); + } + // remove redundant slashes + return p.replace(/\/\/+/g, '/'); +} +// on Mac/Linux, test the execute bit +// R W X R W X R W X +// 256 128 64 32 16 8 4 2 1 +function isUnixExecutable(stats) { + return ((stats.mode & 1) > 0 || + ((stats.mode & 8) > 0 && stats.gid === process.getgid()) || + ((stats.mode & 64) > 0 && stats.uid === process.getuid())); +} +// Get the path of cmd.exe in windows +function getCmdPath() { + var _a; + return (_a = process.env['COMSPEC']) !== null && _a !== void 0 ? _a : `cmd.exe`; +} +exports.getCmdPath = getCmdPath; +//# sourceMappingURL=io-util.js.map + +/***/ }), + +/***/ 7436: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.findInPath = exports.which = exports.mkdirP = exports.rmRF = exports.mv = exports.cp = void 0; +const assert_1 = __nccwpck_require__(9491); +const path = __importStar(__nccwpck_require__(1017)); +const ioUtil = __importStar(__nccwpck_require__(1962)); +/** + * Copies a file or folder. + * Based off of shelljs - https://github.com/shelljs/shelljs/blob/9237f66c52e5daa40458f94f9565e18e8132f5a6/src/cp.js + * + * @param source source path + * @param dest destination path + * @param options optional. See CopyOptions. + */ +function cp(source, dest, options = {}) { + return __awaiter(this, void 0, void 0, function* () { + const { force, recursive, copySourceDirectory } = readCopyOptions(options); + const destStat = (yield ioUtil.exists(dest)) ? yield ioUtil.stat(dest) : null; + // Dest is an existing file, but not forcing + if (destStat && destStat.isFile() && !force) { + return; + } + // If dest is an existing directory, should copy inside. + const newDest = destStat && destStat.isDirectory() && copySourceDirectory + ? path.join(dest, path.basename(source)) + : dest; + if (!(yield ioUtil.exists(source))) { + throw new Error(`no such file or directory: ${source}`); + } + const sourceStat = yield ioUtil.stat(source); + if (sourceStat.isDirectory()) { + if (!recursive) { + throw new Error(`Failed to copy. ${source} is a directory, but tried to copy without recursive flag.`); + } + else { + yield cpDirRecursive(source, newDest, 0, force); + } + } + else { + if (path.relative(source, newDest) === '') { + // a file cannot be copied to itself + throw new Error(`'${newDest}' and '${source}' are the same file`); + } + yield copyFile(source, newDest, force); + } + }); +} +exports.cp = cp; +/** + * Moves a path. + * + * @param source source path + * @param dest destination path + * @param options optional. See MoveOptions. + */ +function mv(source, dest, options = {}) { + return __awaiter(this, void 0, void 0, function* () { + if (yield ioUtil.exists(dest)) { + let destExists = true; + if (yield ioUtil.isDirectory(dest)) { + // If dest is directory copy src into dest + dest = path.join(dest, path.basename(source)); + destExists = yield ioUtil.exists(dest); + } + if (destExists) { + if (options.force == null || options.force) { + yield rmRF(dest); + } + else { + throw new Error('Destination already exists'); + } + } + } + yield mkdirP(path.dirname(dest)); + yield ioUtil.rename(source, dest); + }); +} +exports.mv = mv; +/** + * Remove a path recursively with force + * + * @param inputPath path to remove + */ +function rmRF(inputPath) { + return __awaiter(this, void 0, void 0, function* () { + if (ioUtil.IS_WINDOWS) { + // Check for invalid characters + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + if (/[*"<>|]/.test(inputPath)) { + throw new Error('File path must not contain `*`, `"`, `<`, `>` or `|` on Windows'); + } + } + try { + // note if path does not exist, error is silent + yield ioUtil.rm(inputPath, { + force: true, + maxRetries: 3, + recursive: true, + retryDelay: 300 + }); + } + catch (err) { + throw new Error(`File was unable to be removed ${err}`); + } + }); +} +exports.rmRF = rmRF; +/** + * Make a directory. Creates the full path with folders in between + * Will throw if it fails + * + * @param fsPath path to create + * @returns Promise + */ +function mkdirP(fsPath) { + return __awaiter(this, void 0, void 0, function* () { + assert_1.ok(fsPath, 'a path argument must be provided'); + yield ioUtil.mkdir(fsPath, { recursive: true }); + }); +} +exports.mkdirP = mkdirP; +/** + * Returns path of a tool had the tool actually been invoked. Resolves via paths. + * If you check and the tool does not exist, it will throw. + * + * @param tool name of the tool + * @param check whether to check if tool exists + * @returns Promise path to tool + */ +function which(tool, check) { + return __awaiter(this, void 0, void 0, function* () { + if (!tool) { + throw new Error("parameter 'tool' is required"); + } + // recursive when check=true + if (check) { + const result = yield which(tool, false); + if (!result) { + if (ioUtil.IS_WINDOWS) { + throw new Error(`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also verify the file has a valid extension for an executable file.`); + } + else { + throw new Error(`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also check the file mode to verify the file is executable.`); + } + } + return result; + } + const matches = yield findInPath(tool); + if (matches && matches.length > 0) { + return matches[0]; + } + return ''; + }); +} +exports.which = which; +/** + * Returns a list of all occurrences of the given tool on the system path. + * + * @returns Promise the paths of the tool + */ +function findInPath(tool) { + return __awaiter(this, void 0, void 0, function* () { + if (!tool) { + throw new Error("parameter 'tool' is required"); + } + // build the list of extensions to try + const extensions = []; + if (ioUtil.IS_WINDOWS && process.env['PATHEXT']) { + for (const extension of process.env['PATHEXT'].split(path.delimiter)) { + if (extension) { + extensions.push(extension); + } + } + } + // if it's rooted, return it if exists. otherwise return empty. + if (ioUtil.isRooted(tool)) { + const filePath = yield ioUtil.tryGetExecutablePath(tool, extensions); + if (filePath) { + return [filePath]; + } + return []; + } + // if any path separators, return empty + if (tool.includes(path.sep)) { + return []; + } + // build the list of directories + // + // Note, technically "where" checks the current directory on Windows. From a toolkit perspective, + // it feels like we should not do this. Checking the current directory seems like more of a use + // case of a shell, and the which() function exposed by the toolkit should strive for consistency + // across platforms. + const directories = []; + if (process.env.PATH) { + for (const p of process.env.PATH.split(path.delimiter)) { + if (p) { + directories.push(p); + } + } + } + // find all matches + const matches = []; + for (const directory of directories) { + const filePath = yield ioUtil.tryGetExecutablePath(path.join(directory, tool), extensions); + if (filePath) { + matches.push(filePath); + } + } + return matches; + }); +} +exports.findInPath = findInPath; +function readCopyOptions(options) { + const force = options.force == null ? true : options.force; + const recursive = Boolean(options.recursive); + const copySourceDirectory = options.copySourceDirectory == null + ? true + : Boolean(options.copySourceDirectory); + return { force, recursive, copySourceDirectory }; +} +function cpDirRecursive(sourceDir, destDir, currentDepth, force) { + return __awaiter(this, void 0, void 0, function* () { + // Ensure there is not a run away recursive copy + if (currentDepth >= 255) + return; + currentDepth++; + yield mkdirP(destDir); + const files = yield ioUtil.readdir(sourceDir); + for (const fileName of files) { + const srcFile = `${sourceDir}/${fileName}`; + const destFile = `${destDir}/${fileName}`; + const srcFileStat = yield ioUtil.lstat(srcFile); + if (srcFileStat.isDirectory()) { + // Recurse + yield cpDirRecursive(srcFile, destFile, currentDepth, force); + } + else { + yield copyFile(srcFile, destFile, force); + } + } + // Change the mode for the newly created directory + yield ioUtil.chmod(destDir, (yield ioUtil.stat(sourceDir)).mode); + }); +} +// Buffered file copy +function copyFile(srcFile, destFile, force) { + return __awaiter(this, void 0, void 0, function* () { + if ((yield ioUtil.lstat(srcFile)).isSymbolicLink()) { + // unlink/re-link it + try { + yield ioUtil.lstat(destFile); + yield ioUtil.unlink(destFile); + } + catch (e) { + // Try to override file permission + if (e.code === 'EPERM') { + yield ioUtil.chmod(destFile, '0666'); + yield ioUtil.unlink(destFile); + } + // other errors = it doesn't exist, no work to do + } + // Copy over symlink + const symlinkFull = yield ioUtil.readlink(srcFile); + yield ioUtil.symlink(symlinkFull, destFile, ioUtil.IS_WINDOWS ? 'junction' : null); + } + else if (!(yield ioUtil.exists(destFile)) || force) { + yield ioUtil.copyFile(srcFile, destFile); + } + }); +} +//# sourceMappingURL=io.js.map + +/***/ }), + +/***/ 9809: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const GoTrueAdminApi_1 = __importDefault(__nccwpck_require__(6575)); +const AuthAdminApi = GoTrueAdminApi_1.default; +exports["default"] = AuthAdminApi; +//# sourceMappingURL=AuthAdminApi.js.map + +/***/ }), + +/***/ 4525: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const GoTrueClient_1 = __importDefault(__nccwpck_require__(313)); +const AuthClient = GoTrueClient_1.default; +exports["default"] = AuthClient; +//# sourceMappingURL=AuthClient.js.map + +/***/ }), + +/***/ 6575: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const fetch_1 = __nccwpck_require__(7072); +const helpers_1 = __nccwpck_require__(4044); +const errors_1 = __nccwpck_require__(9938); +class GoTrueAdminApi { + constructor({ url = '', headers = {}, fetch, }) { + this.url = url; + this.headers = headers; + this.fetch = (0, helpers_1.resolveFetch)(fetch); + this.mfa = { + listFactors: this._listFactors.bind(this), + deleteFactor: this._deleteFactor.bind(this), + }; + } + /** + * Removes a logged-in session. + * @param jwt A valid, logged-in JWT. + * @param scope The logout sope. + */ + async signOut(jwt, scope = 'global') { + try { + await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/logout?scope=${scope}`, { + headers: this.headers, + jwt, + noResolveJson: true, + }); + return { data: null, error: null }; + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: null, error }; + } + throw error; + } + } + /** + * Sends an invite link to an email address. + * @param email The email address of the user. + * @param options Additional options to be included when inviting. + */ + async inviteUserByEmail(email, options = {}) { + try { + return await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/invite`, { + body: { email, data: options.data }, + headers: this.headers, + redirectTo: options.redirectTo, + xform: fetch_1._userResponse, + }); + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { user: null }, error }; + } + throw error; + } + } + /** + * Generates email links and OTPs to be sent via a custom email provider. + * @param email The user's email. + * @param options.password User password. For signup only. + * @param options.data Optional user metadata. For signup only. + * @param options.redirectTo The redirect url which should be appended to the generated link + */ + async generateLink(params) { + try { + const { options } = params, rest = __rest(params, ["options"]); + const body = Object.assign(Object.assign({}, rest), options); + if ('newEmail' in rest) { + // replace newEmail with new_email in request body + body.new_email = rest === null || rest === void 0 ? void 0 : rest.newEmail; + delete body['newEmail']; + } + return await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/admin/generate_link`, { + body: body, + headers: this.headers, + xform: fetch_1._generateLinkResponse, + redirectTo: options === null || options === void 0 ? void 0 : options.redirectTo, + }); + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { + data: { + properties: null, + user: null, + }, + error, + }; + } + throw error; + } + } + // User Admin API + /** + * Creates a new user. + * This function should only be called on a server. Never expose your `service_role` key in the browser. + */ + async createUser(attributes) { + try { + return await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/admin/users`, { + body: attributes, + headers: this.headers, + xform: fetch_1._userResponse, + }); + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { user: null }, error }; + } + throw error; + } + } + /** + * Get a list of users. + * + * This function should only be called on a server. Never expose your `service_role` key in the browser. + * @param params An object which supports `page` and `perPage` as numbers, to alter the paginated results. + */ + async listUsers(params) { + var _a, _b, _c, _d, _e, _f, _g; + try { + const pagination = { nextPage: null, lastPage: 0, total: 0 }; + const response = await (0, fetch_1._request)(this.fetch, 'GET', `${this.url}/admin/users`, { + headers: this.headers, + noResolveJson: true, + query: { + page: (_b = (_a = params === null || params === void 0 ? void 0 : params.page) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : '', + per_page: (_d = (_c = params === null || params === void 0 ? void 0 : params.perPage) === null || _c === void 0 ? void 0 : _c.toString()) !== null && _d !== void 0 ? _d : '', + }, + xform: fetch_1._noResolveJsonResponse, + }); + if (response.error) + throw response.error; + const users = await response.json(); + const total = (_e = response.headers.get('x-total-count')) !== null && _e !== void 0 ? _e : 0; + const links = (_g = (_f = response.headers.get('link')) === null || _f === void 0 ? void 0 : _f.split(',')) !== null && _g !== void 0 ? _g : []; + if (links.length > 0) { + links.forEach((link) => { + const page = parseInt(link.split(';')[0].split('=')[1].substring(0, 1)); + const rel = JSON.parse(link.split(';')[1].split('=')[1]); + pagination[`${rel}Page`] = page; + }); + pagination.total = parseInt(total); + } + return { data: Object.assign(Object.assign({}, users), pagination), error: null }; + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { users: [] }, error }; + } + throw error; + } + } + /** + * Get user by id. + * + * @param uid The user's unique identifier + * + * This function should only be called on a server. Never expose your `service_role` key in the browser. + */ + async getUserById(uid) { + try { + return await (0, fetch_1._request)(this.fetch, 'GET', `${this.url}/admin/users/${uid}`, { + headers: this.headers, + xform: fetch_1._userResponse, + }); + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { user: null }, error }; + } + throw error; + } + } + /** + * Updates the user data. + * + * @param attributes The data you want to update. + * + * This function should only be called on a server. Never expose your `service_role` key in the browser. + */ + async updateUserById(uid, attributes) { + try { + return await (0, fetch_1._request)(this.fetch, 'PUT', `${this.url}/admin/users/${uid}`, { + body: attributes, + headers: this.headers, + xform: fetch_1._userResponse, + }); + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { user: null }, error }; + } + throw error; + } + } + /** + * Delete a user. Requires a `service_role` key. + * + * @param id The user id you want to remove. + * @param shouldSoftDelete If true, then the user will be soft-deleted from the auth schema. Soft deletion allows user identification from the hashed user ID but is not reversible. + * Defaults to false for backward compatibility. + * + * This function should only be called on a server. Never expose your `service_role` key in the browser. + */ + async deleteUser(id, shouldSoftDelete = false) { + try { + return await (0, fetch_1._request)(this.fetch, 'DELETE', `${this.url}/admin/users/${id}`, { + headers: this.headers, + body: { + should_soft_delete: shouldSoftDelete, + }, + xform: fetch_1._userResponse, + }); + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { user: null }, error }; + } + throw error; + } + } + async _listFactors(params) { + try { + const { data, error } = await (0, fetch_1._request)(this.fetch, 'GET', `${this.url}/admin/users/${params.userId}/factors`, { + headers: this.headers, + xform: (factors) => { + return { data: { factors }, error: null }; + }, + }); + return { data, error }; + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: null, error }; + } + throw error; + } + } + async _deleteFactor(params) { + try { + const data = await (0, fetch_1._request)(this.fetch, 'DELETE', `${this.url}/admin/users/${params.userId}/factors/${params.id}`, { + headers: this.headers, + }); + return { data, error: null }; + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: null, error }; + } + throw error; + } + } +} +exports["default"] = GoTrueAdminApi; +//# sourceMappingURL=GoTrueAdminApi.js.map + +/***/ }), + +/***/ 313: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const GoTrueAdminApi_1 = __importDefault(__nccwpck_require__(6575)); +const constants_1 = __nccwpck_require__(518); +const errors_1 = __nccwpck_require__(9938); +const fetch_1 = __nccwpck_require__(7072); +const helpers_1 = __nccwpck_require__(4044); +const local_storage_1 = __nccwpck_require__(1479); +const polyfills_1 = __nccwpck_require__(1897); +const version_1 = __nccwpck_require__(6677); +const locks_1 = __nccwpck_require__(163); +(0, polyfills_1.polyfillGlobalThis)(); // Make "globalThis" available +const DEFAULT_OPTIONS = { + url: constants_1.GOTRUE_URL, + storageKey: constants_1.STORAGE_KEY, + autoRefreshToken: true, + persistSession: true, + detectSessionInUrl: true, + headers: constants_1.DEFAULT_HEADERS, + flowType: 'implicit', + debug: false, + hasCustomAuthorizationHeader: false, +}; +/** Current session will be checked for refresh at this interval. */ +const AUTO_REFRESH_TICK_DURATION = 30 * 1000; +/** + * A token refresh will be attempted this many ticks before the current session expires. */ +const AUTO_REFRESH_TICK_THRESHOLD = 3; +async function lockNoOp(name, acquireTimeout, fn) { + return await fn(); +} +class GoTrueClient { + /** + * Create a new client for use in the browser. + */ + constructor(options) { + var _a, _b; + this.memoryStorage = null; + this.stateChangeEmitters = new Map(); + this.autoRefreshTicker = null; + this.visibilityChangedCallback = null; + this.refreshingDeferred = null; + /** + * Keeps track of the async client initialization. + * When null or not yet resolved the auth state is `unknown` + * Once resolved the the auth state is known and it's save to call any further client methods. + * Keep extra care to never reject or throw uncaught errors + */ + this.initializePromise = null; + this.detectSessionInUrl = true; + this.hasCustomAuthorizationHeader = false; + this.suppressGetSessionWarning = false; + this.lockAcquired = false; + this.pendingInLock = []; + /** + * Used to broadcast state change events to other tabs listening. + */ + this.broadcastChannel = null; + this.logger = console.log; + this.instanceID = GoTrueClient.nextInstanceID; + GoTrueClient.nextInstanceID += 1; + if (this.instanceID > 0 && (0, helpers_1.isBrowser)()) { + console.warn('Multiple GoTrueClient instances detected in the same browser context. It is not an error, but this should be avoided as it may produce undefined behavior when used concurrently under the same storage key.'); + } + const settings = Object.assign(Object.assign({}, DEFAULT_OPTIONS), options); + this.logDebugMessages = !!settings.debug; + if (typeof settings.debug === 'function') { + this.logger = settings.debug; + } + this.persistSession = settings.persistSession; + this.storageKey = settings.storageKey; + this.autoRefreshToken = settings.autoRefreshToken; + this.admin = new GoTrueAdminApi_1.default({ + url: settings.url, + headers: settings.headers, + fetch: settings.fetch, + }); + this.url = settings.url; + this.headers = settings.headers; + this.fetch = (0, helpers_1.resolveFetch)(settings.fetch); + this.lock = settings.lock || lockNoOp; + this.detectSessionInUrl = settings.detectSessionInUrl; + this.flowType = settings.flowType; + this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader; + if (settings.lock) { + this.lock = settings.lock; + } + else if ((0, helpers_1.isBrowser)() && ((_a = globalThis === null || globalThis === void 0 ? void 0 : globalThis.navigator) === null || _a === void 0 ? void 0 : _a.locks)) { + this.lock = locks_1.navigatorLock; + } + else { + this.lock = lockNoOp; + } + this.mfa = { + verify: this._verify.bind(this), + enroll: this._enroll.bind(this), + unenroll: this._unenroll.bind(this), + challenge: this._challenge.bind(this), + listFactors: this._listFactors.bind(this), + challengeAndVerify: this._challengeAndVerify.bind(this), + getAuthenticatorAssuranceLevel: this._getAuthenticatorAssuranceLevel.bind(this), + }; + if (this.persistSession) { + if (settings.storage) { + this.storage = settings.storage; + } + else { + if ((0, helpers_1.supportsLocalStorage)()) { + this.storage = local_storage_1.localStorageAdapter; + } + else { + this.memoryStorage = {}; + this.storage = (0, local_storage_1.memoryLocalStorageAdapter)(this.memoryStorage); + } + } + } + else { + this.memoryStorage = {}; + this.storage = (0, local_storage_1.memoryLocalStorageAdapter)(this.memoryStorage); + } + if ((0, helpers_1.isBrowser)() && globalThis.BroadcastChannel && this.persistSession && this.storageKey) { + try { + this.broadcastChannel = new globalThis.BroadcastChannel(this.storageKey); + } + catch (e) { + console.error('Failed to create a new BroadcastChannel, multi-tab state changes will not be available', e); + } + (_b = this.broadcastChannel) === null || _b === void 0 ? void 0 : _b.addEventListener('message', async (event) => { + this._debug('received broadcast notification from other tab or client', event); + await this._notifyAllSubscribers(event.data.event, event.data.session, false); // broadcast = false so we don't get an endless loop of messages + }); + } + this.initialize(); + } + _debug(...args) { + if (this.logDebugMessages) { + this.logger(`GoTrueClient@${this.instanceID} (${version_1.version}) ${new Date().toISOString()}`, ...args); + } + return this; + } + /** + * Initializes the client session either from the url or from storage. + * This method is automatically called when instantiating the client, but should also be called + * manually when checking for an error from an auth redirect (oauth, magiclink, password recovery, etc). + */ + async initialize() { + if (this.initializePromise) { + return await this.initializePromise; + } + this.initializePromise = (async () => { + return await this._acquireLock(-1, async () => { + return await this._initialize(); + }); + })(); + return await this.initializePromise; + } + /** + * IMPORTANT: + * 1. Never throw in this method, as it is called from the constructor + * 2. Never return a session from this method as it would be cached over + * the whole lifetime of the client + */ + async _initialize() { + var _a; + try { + const params = (0, helpers_1.parseParametersFromURL)(window.location.href); + let callbackUrlType = 'none'; + if (this._isImplicitGrantCallback(params)) { + callbackUrlType = 'implicit'; + } + else if (await this._isPKCECallback(params)) { + callbackUrlType = 'pkce'; + } + /** + * Attempt to get the session from the URL only if these conditions are fulfilled + * + * Note: If the URL isn't one of the callback url types (implicit or pkce), + * then there could be an existing session so we don't want to prematurely remove it + */ + if ((0, helpers_1.isBrowser)() && this.detectSessionInUrl && callbackUrlType !== 'none') { + const { data, error } = await this._getSessionFromURL(params, callbackUrlType); + if (error) { + this._debug('#_initialize()', 'error detecting session from URL', error); + if ((0, errors_1.isAuthImplicitGrantRedirectError)(error)) { + const errorCode = (_a = error.details) === null || _a === void 0 ? void 0 : _a.code; + if (errorCode === 'identity_already_exists' || + errorCode === 'identity_not_found' || + errorCode === 'single_identity_not_deletable') { + return { error }; + } + } + // failed login attempt via url, + // remove old session as in verifyOtp, signUp and signInWith* + await this._removeSession(); + return { error }; + } + const { session, redirectType } = data; + this._debug('#_initialize()', 'detected session in URL', session, 'redirect type', redirectType); + await this._saveSession(session); + setTimeout(async () => { + if (redirectType === 'recovery') { + await this._notifyAllSubscribers('PASSWORD_RECOVERY', session); + } + else { + await this._notifyAllSubscribers('SIGNED_IN', session); + } + }, 0); + return { error: null }; + } + // no login attempt via callback url try to recover session from storage + await this._recoverAndRefresh(); + return { error: null }; + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { error }; + } + return { + error: new errors_1.AuthUnknownError('Unexpected error during initialization', error), + }; + } + finally { + await this._handleVisibilityChange(); + this._debug('#_initialize()', 'end'); + } + } + /** + * Creates a new anonymous user. + * + * @returns A session where the is_anonymous claim in the access token JWT set to true + */ + async signInAnonymously(credentials) { + var _a, _b, _c; + try { + const res = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/signup`, { + headers: this.headers, + body: { + data: (_b = (_a = credentials === null || credentials === void 0 ? void 0 : credentials.options) === null || _a === void 0 ? void 0 : _a.data) !== null && _b !== void 0 ? _b : {}, + gotrue_meta_security: { captcha_token: (_c = credentials === null || credentials === void 0 ? void 0 : credentials.options) === null || _c === void 0 ? void 0 : _c.captchaToken }, + }, + xform: fetch_1._sessionResponse, + }); + const { data, error } = res; + if (error || !data) { + return { data: { user: null, session: null }, error: error }; + } + const session = data.session; + const user = data.user; + if (data.session) { + await this._saveSession(data.session); + await this._notifyAllSubscribers('SIGNED_IN', session); + } + return { data: { user, session }, error: null }; + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { user: null, session: null }, error }; + } + throw error; + } + } + /** + * Creates a new user. + * + * Be aware that if a user account exists in the system you may get back an + * error message that attempts to hide this information from the user. + * This method has support for PKCE via email signups. The PKCE flow cannot be used when autoconfirm is enabled. + * + * @returns A logged-in session if the server has "autoconfirm" ON + * @returns A user if the server has "autoconfirm" OFF + */ + async signUp(credentials) { + var _a, _b, _c; + try { + let res; + if ('email' in credentials) { + const { email, password, options } = credentials; + let codeChallenge = null; + let codeChallengeMethod = null; + if (this.flowType === 'pkce') { + ; + [codeChallenge, codeChallengeMethod] = await (0, helpers_1.getCodeChallengeAndMethod)(this.storage, this.storageKey); + } + res = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/signup`, { + headers: this.headers, + redirectTo: options === null || options === void 0 ? void 0 : options.emailRedirectTo, + body: { + email, + password, + data: (_a = options === null || options === void 0 ? void 0 : options.data) !== null && _a !== void 0 ? _a : {}, + gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken }, + code_challenge: codeChallenge, + code_challenge_method: codeChallengeMethod, + }, + xform: fetch_1._sessionResponse, + }); + } + else if ('phone' in credentials) { + const { phone, password, options } = credentials; + res = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/signup`, { + headers: this.headers, + body: { + phone, + password, + data: (_b = options === null || options === void 0 ? void 0 : options.data) !== null && _b !== void 0 ? _b : {}, + channel: (_c = options === null || options === void 0 ? void 0 : options.channel) !== null && _c !== void 0 ? _c : 'sms', + gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken }, + }, + xform: fetch_1._sessionResponse, + }); + } + else { + throw new errors_1.AuthInvalidCredentialsError('You must provide either an email or phone number and a password'); + } + const { data, error } = res; + if (error || !data) { + return { data: { user: null, session: null }, error: error }; + } + const session = data.session; + const user = data.user; + if (data.session) { + await this._saveSession(data.session); + await this._notifyAllSubscribers('SIGNED_IN', session); + } + return { data: { user, session }, error: null }; + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { user: null, session: null }, error }; + } + throw error; + } + } + /** + * Log in an existing user with an email and password or phone and password. + * + * Be aware that you may get back an error message that will not distinguish + * between the cases where the account does not exist or that the + * email/phone and password combination is wrong or that the account can only + * be accessed via social login. + */ + async signInWithPassword(credentials) { + try { + let res; + if ('email' in credentials) { + const { email, password, options } = credentials; + res = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/token?grant_type=password`, { + headers: this.headers, + body: { + email, + password, + gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken }, + }, + xform: fetch_1._sessionResponsePassword, + }); + } + else if ('phone' in credentials) { + const { phone, password, options } = credentials; + res = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/token?grant_type=password`, { + headers: this.headers, + body: { + phone, + password, + gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken }, + }, + xform: fetch_1._sessionResponsePassword, + }); + } + else { + throw new errors_1.AuthInvalidCredentialsError('You must provide either an email or phone number and a password'); + } + const { data, error } = res; + if (error) { + return { data: { user: null, session: null }, error }; + } + else if (!data || !data.session || !data.user) { + return { data: { user: null, session: null }, error: new errors_1.AuthInvalidTokenResponseError() }; + } + if (data.session) { + await this._saveSession(data.session); + await this._notifyAllSubscribers('SIGNED_IN', data.session); + } + return { + data: Object.assign({ user: data.user, session: data.session }, (data.weak_password ? { weakPassword: data.weak_password } : null)), + error, + }; + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { user: null, session: null }, error }; + } + throw error; + } + } + /** + * Log in an existing user via a third-party provider. + * This method supports the PKCE flow. + */ + async signInWithOAuth(credentials) { + var _a, _b, _c, _d; + return await this._handleProviderSignIn(credentials.provider, { + redirectTo: (_a = credentials.options) === null || _a === void 0 ? void 0 : _a.redirectTo, + scopes: (_b = credentials.options) === null || _b === void 0 ? void 0 : _b.scopes, + queryParams: (_c = credentials.options) === null || _c === void 0 ? void 0 : _c.queryParams, + skipBrowserRedirect: (_d = credentials.options) === null || _d === void 0 ? void 0 : _d.skipBrowserRedirect, + }); + } + /** + * Log in an existing user by exchanging an Auth Code issued during the PKCE flow. + */ + async exchangeCodeForSession(authCode) { + await this.initializePromise; + return this._acquireLock(-1, async () => { + return this._exchangeCodeForSession(authCode); + }); + } + async _exchangeCodeForSession(authCode) { + const storageItem = await (0, helpers_1.getItemAsync)(this.storage, `${this.storageKey}-code-verifier`); + const [codeVerifier, redirectType] = (storageItem !== null && storageItem !== void 0 ? storageItem : '').split('/'); + try { + const { data, error } = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/token?grant_type=pkce`, { + headers: this.headers, + body: { + auth_code: authCode, + code_verifier: codeVerifier, + }, + xform: fetch_1._sessionResponse, + }); + await (0, helpers_1.removeItemAsync)(this.storage, `${this.storageKey}-code-verifier`); + if (error) { + throw error; + } + if (!data || !data.session || !data.user) { + return { + data: { user: null, session: null, redirectType: null }, + error: new errors_1.AuthInvalidTokenResponseError(), + }; + } + if (data.session) { + await this._saveSession(data.session); + await this._notifyAllSubscribers('SIGNED_IN', data.session); + } + return { data: Object.assign(Object.assign({}, data), { redirectType: redirectType !== null && redirectType !== void 0 ? redirectType : null }), error }; + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { user: null, session: null, redirectType: null }, error }; + } + throw error; + } + } + /** + * Allows signing in with an OIDC ID token. The authentication provider used + * should be enabled and configured. + */ + async signInWithIdToken(credentials) { + try { + const { options, provider, token, access_token, nonce } = credentials; + const res = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/token?grant_type=id_token`, { + headers: this.headers, + body: { + provider, + id_token: token, + access_token, + nonce, + gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken }, + }, + xform: fetch_1._sessionResponse, + }); + const { data, error } = res; + if (error) { + return { data: { user: null, session: null }, error }; + } + else if (!data || !data.session || !data.user) { + return { + data: { user: null, session: null }, + error: new errors_1.AuthInvalidTokenResponseError(), + }; + } + if (data.session) { + await this._saveSession(data.session); + await this._notifyAllSubscribers('SIGNED_IN', data.session); + } + return { data, error }; + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { user: null, session: null }, error }; + } + throw error; + } + } + /** + * Log in a user using magiclink or a one-time password (OTP). + * + * If the `{{ .ConfirmationURL }}` variable is specified in the email template, a magiclink will be sent. + * If the `{{ .Token }}` variable is specified in the email template, an OTP will be sent. + * If you're using phone sign-ins, only an OTP will be sent. You won't be able to send a magiclink for phone sign-ins. + * + * Be aware that you may get back an error message that will not distinguish + * between the cases where the account does not exist or, that the account + * can only be accessed via social login. + * + * Do note that you will need to configure a Whatsapp sender on Twilio + * if you are using phone sign in with the 'whatsapp' channel. The whatsapp + * channel is not supported on other providers + * at this time. + * This method supports PKCE when an email is passed. + */ + async signInWithOtp(credentials) { + var _a, _b, _c, _d, _e; + try { + if ('email' in credentials) { + const { email, options } = credentials; + let codeChallenge = null; + let codeChallengeMethod = null; + if (this.flowType === 'pkce') { + ; + [codeChallenge, codeChallengeMethod] = await (0, helpers_1.getCodeChallengeAndMethod)(this.storage, this.storageKey); + } + const { error } = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/otp`, { + headers: this.headers, + body: { + email, + data: (_a = options === null || options === void 0 ? void 0 : options.data) !== null && _a !== void 0 ? _a : {}, + create_user: (_b = options === null || options === void 0 ? void 0 : options.shouldCreateUser) !== null && _b !== void 0 ? _b : true, + gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken }, + code_challenge: codeChallenge, + code_challenge_method: codeChallengeMethod, + }, + redirectTo: options === null || options === void 0 ? void 0 : options.emailRedirectTo, + }); + return { data: { user: null, session: null }, error }; + } + if ('phone' in credentials) { + const { phone, options } = credentials; + const { data, error } = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/otp`, { + headers: this.headers, + body: { + phone, + data: (_c = options === null || options === void 0 ? void 0 : options.data) !== null && _c !== void 0 ? _c : {}, + create_user: (_d = options === null || options === void 0 ? void 0 : options.shouldCreateUser) !== null && _d !== void 0 ? _d : true, + gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken }, + channel: (_e = options === null || options === void 0 ? void 0 : options.channel) !== null && _e !== void 0 ? _e : 'sms', + }, + }); + return { data: { user: null, session: null, messageId: data === null || data === void 0 ? void 0 : data.message_id }, error }; + } + throw new errors_1.AuthInvalidCredentialsError('You must provide either an email or phone number.'); + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { user: null, session: null }, error }; + } + throw error; + } + } + /** + * Log in a user given a User supplied OTP or TokenHash received through mobile or email. + */ + async verifyOtp(params) { + var _a, _b; + try { + let redirectTo = undefined; + let captchaToken = undefined; + if ('options' in params) { + redirectTo = (_a = params.options) === null || _a === void 0 ? void 0 : _a.redirectTo; + captchaToken = (_b = params.options) === null || _b === void 0 ? void 0 : _b.captchaToken; + } + const { data, error } = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/verify`, { + headers: this.headers, + body: Object.assign(Object.assign({}, params), { gotrue_meta_security: { captcha_token: captchaToken } }), + redirectTo, + xform: fetch_1._sessionResponse, + }); + if (error) { + throw error; + } + if (!data) { + throw new Error('An error occurred on token verification.'); + } + const session = data.session; + const user = data.user; + if (session === null || session === void 0 ? void 0 : session.access_token) { + await this._saveSession(session); + await this._notifyAllSubscribers(params.type == 'recovery' ? 'PASSWORD_RECOVERY' : 'SIGNED_IN', session); + } + return { data: { user, session }, error: null }; + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { user: null, session: null }, error }; + } + throw error; + } + } + /** + * Attempts a single-sign on using an enterprise Identity Provider. A + * successful SSO attempt will redirect the current page to the identity + * provider authorization page. The redirect URL is implementation and SSO + * protocol specific. + * + * You can use it by providing a SSO domain. Typically you can extract this + * domain by asking users for their email address. If this domain is + * registered on the Auth instance the redirect will use that organization's + * currently active SSO Identity Provider for the login. + * + * If you have built an organization-specific login page, you can use the + * organization's SSO Identity Provider UUID directly instead. + */ + async signInWithSSO(params) { + var _a, _b, _c; + try { + let codeChallenge = null; + let codeChallengeMethod = null; + if (this.flowType === 'pkce') { + ; + [codeChallenge, codeChallengeMethod] = await (0, helpers_1.getCodeChallengeAndMethod)(this.storage, this.storageKey); + } + return await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/sso`, { + body: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, ('providerId' in params ? { provider_id: params.providerId } : null)), ('domain' in params ? { domain: params.domain } : null)), { redirect_to: (_b = (_a = params.options) === null || _a === void 0 ? void 0 : _a.redirectTo) !== null && _b !== void 0 ? _b : undefined }), (((_c = params === null || params === void 0 ? void 0 : params.options) === null || _c === void 0 ? void 0 : _c.captchaToken) + ? { gotrue_meta_security: { captcha_token: params.options.captchaToken } } + : null)), { skip_http_redirect: true, code_challenge: codeChallenge, code_challenge_method: codeChallengeMethod }), + headers: this.headers, + xform: fetch_1._ssoResponse, + }); + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: null, error }; + } + throw error; + } + } + /** + * Sends a reauthentication OTP to the user's email or phone number. + * Requires the user to be signed-in. + */ + async reauthenticate() { + await this.initializePromise; + return await this._acquireLock(-1, async () => { + return await this._reauthenticate(); + }); + } + async _reauthenticate() { + try { + return await this._useSession(async (result) => { + const { data: { session }, error: sessionError, } = result; + if (sessionError) + throw sessionError; + if (!session) + throw new errors_1.AuthSessionMissingError(); + const { error } = await (0, fetch_1._request)(this.fetch, 'GET', `${this.url}/reauthenticate`, { + headers: this.headers, + jwt: session.access_token, + }); + return { data: { user: null, session: null }, error }; + }); + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { user: null, session: null }, error }; + } + throw error; + } + } + /** + * Resends an existing signup confirmation email, email change email, SMS OTP or phone change OTP. + */ + async resend(credentials) { + try { + const endpoint = `${this.url}/resend`; + if ('email' in credentials) { + const { email, type, options } = credentials; + const { error } = await (0, fetch_1._request)(this.fetch, 'POST', endpoint, { + headers: this.headers, + body: { + email, + type, + gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken }, + }, + redirectTo: options === null || options === void 0 ? void 0 : options.emailRedirectTo, + }); + return { data: { user: null, session: null }, error }; + } + else if ('phone' in credentials) { + const { phone, type, options } = credentials; + const { data, error } = await (0, fetch_1._request)(this.fetch, 'POST', endpoint, { + headers: this.headers, + body: { + phone, + type, + gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken }, + }, + }); + return { data: { user: null, session: null, messageId: data === null || data === void 0 ? void 0 : data.message_id }, error }; + } + throw new errors_1.AuthInvalidCredentialsError('You must provide either an email or phone number and a type'); + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { user: null, session: null }, error }; + } + throw error; + } + } + /** + * Returns the session, refreshing it if necessary. + * + * The session returned can be null if the session is not detected which can happen in the event a user is not signed-in or has logged out. + * + * **IMPORTANT:** This method loads values directly from the storage attached + * to the client. If that storage is based on request cookies for example, + * the values in it may not be authentic and therefore it's strongly advised + * against using this method and its results in such circumstances. A warning + * will be emitted if this is detected. Use {@link #getUser()} instead. + */ + async getSession() { + await this.initializePromise; + const result = await this._acquireLock(-1, async () => { + return this._useSession(async (result) => { + return result; + }); + }); + return result; + } + /** + * Acquires a global lock based on the storage key. + */ + async _acquireLock(acquireTimeout, fn) { + this._debug('#_acquireLock', 'begin', acquireTimeout); + try { + if (this.lockAcquired) { + const last = this.pendingInLock.length + ? this.pendingInLock[this.pendingInLock.length - 1] + : Promise.resolve(); + const result = (async () => { + await last; + return await fn(); + })(); + this.pendingInLock.push((async () => { + try { + await result; + } + catch (e) { + // we just care if it finished + } + })()); + return result; + } + return await this.lock(`lock:${this.storageKey}`, acquireTimeout, async () => { + this._debug('#_acquireLock', 'lock acquired for storage key', this.storageKey); + try { + this.lockAcquired = true; + const result = fn(); + this.pendingInLock.push((async () => { + try { + await result; + } + catch (e) { + // we just care if it finished + } + })()); + await result; + // keep draining the queue until there's nothing to wait on + while (this.pendingInLock.length) { + const waitOn = [...this.pendingInLock]; + await Promise.all(waitOn); + this.pendingInLock.splice(0, waitOn.length); + } + return await result; + } + finally { + this._debug('#_acquireLock', 'lock released for storage key', this.storageKey); + this.lockAcquired = false; + } + }); + } + finally { + this._debug('#_acquireLock', 'end'); + } + } + /** + * Use instead of {@link #getSession} inside the library. It is + * semantically usually what you want, as getting a session involves some + * processing afterwards that requires only one client operating on the + * session at once across multiple tabs or processes. + */ + async _useSession(fn) { + this._debug('#_useSession', 'begin'); + try { + // the use of __loadSession here is the only correct use of the function! + const result = await this.__loadSession(); + return await fn(result); + } + finally { + this._debug('#_useSession', 'end'); + } + } + /** + * NEVER USE DIRECTLY! + * + * Always use {@link #_useSession}. + */ + async __loadSession() { + this._debug('#__loadSession()', 'begin'); + if (!this.lockAcquired) { + this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack); + } + try { + let currentSession = null; + const maybeSession = await (0, helpers_1.getItemAsync)(this.storage, this.storageKey); + this._debug('#getSession()', 'session from storage', maybeSession); + if (maybeSession !== null) { + if (this._isValidSession(maybeSession)) { + currentSession = maybeSession; + } + else { + this._debug('#getSession()', 'session from storage is not valid'); + await this._removeSession(); + } + } + if (!currentSession) { + return { data: { session: null }, error: null }; + } + const hasExpired = currentSession.expires_at + ? currentSession.expires_at <= Date.now() / 1000 + : false; + this._debug('#__loadSession()', `session has${hasExpired ? '' : ' not'} expired`, 'expires_at', currentSession.expires_at); + if (!hasExpired) { + if (this.storage.isServer) { + let suppressWarning = this.suppressGetSessionWarning; + const proxySession = new Proxy(currentSession, { + get: (target, prop, receiver) => { + if (!suppressWarning && prop === 'user') { + // only show warning when the user object is being accessed from the server + console.warn('Using the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.'); + suppressWarning = true; // keeps this proxy instance from logging additional warnings + this.suppressGetSessionWarning = true; // keeps this client's future proxy instances from warning + } + return Reflect.get(target, prop, receiver); + }, + }); + currentSession = proxySession; + } + return { data: { session: currentSession }, error: null }; + } + const { session, error } = await this._callRefreshToken(currentSession.refresh_token); + if (error) { + return { data: { session: null }, error }; + } + return { data: { session }, error: null }; + } + finally { + this._debug('#__loadSession()', 'end'); + } + } + /** + * Gets the current user details if there is an existing session. This method + * performs a network request to the Supabase Auth server, so the returned + * value is authentic and can be used to base authorization rules on. + * + * @param jwt Takes in an optional access token JWT. If no JWT is provided, the JWT from the current session is used. + */ + async getUser(jwt) { + if (jwt) { + return await this._getUser(jwt); + } + await this.initializePromise; + const result = await this._acquireLock(-1, async () => { + return await this._getUser(); + }); + return result; + } + async _getUser(jwt) { + try { + if (jwt) { + return await (0, fetch_1._request)(this.fetch, 'GET', `${this.url}/user`, { + headers: this.headers, + jwt: jwt, + xform: fetch_1._userResponse, + }); + } + return await this._useSession(async (result) => { + var _a, _b, _c; + const { data, error } = result; + if (error) { + throw error; + } + // returns an error if there is no access_token or custom authorization header + if (!((_a = data.session) === null || _a === void 0 ? void 0 : _a.access_token) && !this.hasCustomAuthorizationHeader) { + return { data: { user: null }, error: new errors_1.AuthSessionMissingError() }; + } + return await (0, fetch_1._request)(this.fetch, 'GET', `${this.url}/user`, { + headers: this.headers, + jwt: (_c = (_b = data.session) === null || _b === void 0 ? void 0 : _b.access_token) !== null && _c !== void 0 ? _c : undefined, + xform: fetch_1._userResponse, + }); + }); + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + if ((0, errors_1.isAuthSessionMissingError)(error)) { + // JWT contains a `session_id` which does not correspond to an active + // session in the database, indicating the user is signed out. + await this._removeSession(); + await (0, helpers_1.removeItemAsync)(this.storage, `${this.storageKey}-code-verifier`); + } + return { data: { user: null }, error }; + } + throw error; + } + } + /** + * Updates user data for a logged in user. + */ + async updateUser(attributes, options = {}) { + await this.initializePromise; + return await this._acquireLock(-1, async () => { + return await this._updateUser(attributes, options); + }); + } + async _updateUser(attributes, options = {}) { + try { + return await this._useSession(async (result) => { + const { data: sessionData, error: sessionError } = result; + if (sessionError) { + throw sessionError; + } + if (!sessionData.session) { + throw new errors_1.AuthSessionMissingError(); + } + const session = sessionData.session; + let codeChallenge = null; + let codeChallengeMethod = null; + if (this.flowType === 'pkce' && attributes.email != null) { + ; + [codeChallenge, codeChallengeMethod] = await (0, helpers_1.getCodeChallengeAndMethod)(this.storage, this.storageKey); + } + const { data, error: userError } = await (0, fetch_1._request)(this.fetch, 'PUT', `${this.url}/user`, { + headers: this.headers, + redirectTo: options === null || options === void 0 ? void 0 : options.emailRedirectTo, + body: Object.assign(Object.assign({}, attributes), { code_challenge: codeChallenge, code_challenge_method: codeChallengeMethod }), + jwt: session.access_token, + xform: fetch_1._userResponse, + }); + if (userError) + throw userError; + session.user = data.user; + await this._saveSession(session); + await this._notifyAllSubscribers('USER_UPDATED', session); + return { data: { user: session.user }, error: null }; + }); + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { user: null }, error }; + } + throw error; + } + } + /** + * Decodes a JWT (without performing any validation). + */ + _decodeJWT(jwt) { + return (0, helpers_1.decodeJWTPayload)(jwt); + } + /** + * Sets the session data from the current session. If the current session is expired, setSession will take care of refreshing it to obtain a new session. + * If the refresh token or access token in the current session is invalid, an error will be thrown. + * @param currentSession The current session that minimally contains an access token and refresh token. + */ + async setSession(currentSession) { + await this.initializePromise; + return await this._acquireLock(-1, async () => { + return await this._setSession(currentSession); + }); + } + async _setSession(currentSession) { + try { + if (!currentSession.access_token || !currentSession.refresh_token) { + throw new errors_1.AuthSessionMissingError(); + } + const timeNow = Date.now() / 1000; + let expiresAt = timeNow; + let hasExpired = true; + let session = null; + const payload = (0, helpers_1.decodeJWTPayload)(currentSession.access_token); + if (payload.exp) { + expiresAt = payload.exp; + hasExpired = expiresAt <= timeNow; + } + if (hasExpired) { + const { session: refreshedSession, error } = await this._callRefreshToken(currentSession.refresh_token); + if (error) { + return { data: { user: null, session: null }, error: error }; + } + if (!refreshedSession) { + return { data: { user: null, session: null }, error: null }; + } + session = refreshedSession; + } + else { + const { data, error } = await this._getUser(currentSession.access_token); + if (error) { + throw error; + } + session = { + access_token: currentSession.access_token, + refresh_token: currentSession.refresh_token, + user: data.user, + token_type: 'bearer', + expires_in: expiresAt - timeNow, + expires_at: expiresAt, + }; + await this._saveSession(session); + await this._notifyAllSubscribers('SIGNED_IN', session); + } + return { data: { user: session.user, session }, error: null }; + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { session: null, user: null }, error }; + } + throw error; + } + } + /** + * Returns a new session, regardless of expiry status. + * Takes in an optional current session. If not passed in, then refreshSession() will attempt to retrieve it from getSession(). + * If the current session's refresh token is invalid, an error will be thrown. + * @param currentSession The current session. If passed in, it must contain a refresh token. + */ + async refreshSession(currentSession) { + await this.initializePromise; + return await this._acquireLock(-1, async () => { + return await this._refreshSession(currentSession); + }); + } + async _refreshSession(currentSession) { + try { + return await this._useSession(async (result) => { + var _a; + if (!currentSession) { + const { data, error } = result; + if (error) { + throw error; + } + currentSession = (_a = data.session) !== null && _a !== void 0 ? _a : undefined; + } + if (!(currentSession === null || currentSession === void 0 ? void 0 : currentSession.refresh_token)) { + throw new errors_1.AuthSessionMissingError(); + } + const { session, error } = await this._callRefreshToken(currentSession.refresh_token); + if (error) { + return { data: { user: null, session: null }, error: error }; + } + if (!session) { + return { data: { user: null, session: null }, error: null }; + } + return { data: { user: session.user, session }, error: null }; + }); + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { user: null, session: null }, error }; + } + throw error; + } + } + /** + * Gets the session data from a URL string + */ + async _getSessionFromURL(params, callbackUrlType) { + try { + if (!(0, helpers_1.isBrowser)()) + throw new errors_1.AuthImplicitGrantRedirectError('No browser detected.'); + // If there's an error in the URL, it doesn't matter what flow it is, we just return the error. + if (params.error || params.error_description || params.error_code) { + // The error class returned implies that the redirect is from an implicit grant flow + // but it could also be from a redirect error from a PKCE flow. + throw new errors_1.AuthImplicitGrantRedirectError(params.error_description || 'Error in URL with unspecified error_description', { + error: params.error || 'unspecified_error', + code: params.error_code || 'unspecified_code', + }); + } + // Checks for mismatches between the flowType initialised in the client and the URL parameters + switch (callbackUrlType) { + case 'implicit': + if (this.flowType === 'pkce') { + throw new errors_1.AuthPKCEGrantCodeExchangeError('Not a valid PKCE flow url.'); + } + break; + case 'pkce': + if (this.flowType === 'implicit') { + throw new errors_1.AuthImplicitGrantRedirectError('Not a valid implicit grant flow url.'); + } + break; + default: + // there's no mismatch so we continue + } + // Since this is a redirect for PKCE, we attempt to retrieve the code from the URL for the code exchange + if (callbackUrlType === 'pkce') { + this._debug('#_initialize()', 'begin', 'is PKCE flow', true); + if (!params.code) + throw new errors_1.AuthPKCEGrantCodeExchangeError('No code detected.'); + const { data, error } = await this._exchangeCodeForSession(params.code); + if (error) + throw error; + const url = new URL(window.location.href); + url.searchParams.delete('code'); + window.history.replaceState(window.history.state, '', url.toString()); + return { data: { session: data.session, redirectType: null }, error: null }; + } + const { provider_token, provider_refresh_token, access_token, refresh_token, expires_in, expires_at, token_type, } = params; + if (!access_token || !expires_in || !refresh_token || !token_type) { + throw new errors_1.AuthImplicitGrantRedirectError('No session defined in URL'); + } + const timeNow = Math.round(Date.now() / 1000); + const expiresIn = parseInt(expires_in); + let expiresAt = timeNow + expiresIn; + if (expires_at) { + expiresAt = parseInt(expires_at); + } + const actuallyExpiresIn = expiresAt - timeNow; + if (actuallyExpiresIn * 1000 <= AUTO_REFRESH_TICK_DURATION) { + console.warn(`@supabase/gotrue-js: Session as retrieved from URL expires in ${actuallyExpiresIn}s, should have been closer to ${expiresIn}s`); + } + const issuedAt = expiresAt - expiresIn; + if (timeNow - issuedAt >= 120) { + console.warn('@supabase/gotrue-js: Session as retrieved from URL was issued over 120s ago, URL could be stale', issuedAt, expiresAt, timeNow); + } + else if (timeNow - issuedAt < 0) { + console.warn('@supabase/gotrue-js: Session as retrieved from URL was issued in the future? Check the device clock for skew', issuedAt, expiresAt, timeNow); + } + const { data, error } = await this._getUser(access_token); + if (error) + throw error; + const session = { + provider_token, + provider_refresh_token, + access_token, + expires_in: expiresIn, + expires_at: expiresAt, + refresh_token, + token_type, + user: data.user, + }; + // Remove tokens from URL + window.location.hash = ''; + this._debug('#_getSessionFromURL()', 'clearing window.location.hash'); + return { data: { session, redirectType: params.type }, error: null }; + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { session: null, redirectType: null }, error }; + } + throw error; + } + } + /** + * Checks if the current URL contains parameters given by an implicit oauth grant flow (https://www.rfc-editor.org/rfc/rfc6749.html#section-4.2) + */ + _isImplicitGrantCallback(params) { + return Boolean(params.access_token || params.error_description); + } + /** + * Checks if the current URL and backing storage contain parameters given by a PKCE flow + */ + async _isPKCECallback(params) { + const currentStorageContent = await (0, helpers_1.getItemAsync)(this.storage, `${this.storageKey}-code-verifier`); + return !!(params.code && currentStorageContent); + } + /** + * Inside a browser context, `signOut()` will remove the logged in user from the browser session and log them out - removing all items from localstorage and then trigger a `"SIGNED_OUT"` event. + * + * For server-side management, you can revoke all refresh tokens for a user by passing a user's JWT through to `auth.api.signOut(JWT: string)`. + * There is no way to revoke a user's access token jwt until it expires. It is recommended to set a shorter expiry on the jwt for this reason. + * + * If using `others` scope, no `SIGNED_OUT` event is fired! + */ + async signOut(options = { scope: 'global' }) { + await this.initializePromise; + return await this._acquireLock(-1, async () => { + return await this._signOut(options); + }); + } + async _signOut({ scope } = { scope: 'global' }) { + return await this._useSession(async (result) => { + var _a; + const { data, error: sessionError } = result; + if (sessionError) { + return { error: sessionError }; + } + const accessToken = (_a = data.session) === null || _a === void 0 ? void 0 : _a.access_token; + if (accessToken) { + const { error } = await this.admin.signOut(accessToken, scope); + if (error) { + // ignore 404s since user might not exist anymore + // ignore 401s since an invalid or expired JWT should sign out the current session + if (!((0, errors_1.isAuthApiError)(error) && + (error.status === 404 || error.status === 401 || error.status === 403))) { + return { error }; + } + } + } + if (scope !== 'others') { + await this._removeSession(); + await (0, helpers_1.removeItemAsync)(this.storage, `${this.storageKey}-code-verifier`); + } + return { error: null }; + }); + } + /** + * Receive a notification every time an auth event happens. + * @param callback A callback function to be invoked when an auth event happens. + */ + onAuthStateChange(callback) { + const id = (0, helpers_1.uuid)(); + const subscription = { + id, + callback, + unsubscribe: () => { + this._debug('#unsubscribe()', 'state change callback with id removed', id); + this.stateChangeEmitters.delete(id); + }, + }; + this._debug('#onAuthStateChange()', 'registered callback with id', id); + this.stateChangeEmitters.set(id, subscription); + (async () => { + await this.initializePromise; + await this._acquireLock(-1, async () => { + this._emitInitialSession(id); + }); + })(); + return { data: { subscription } }; + } + async _emitInitialSession(id) { + return await this._useSession(async (result) => { + var _a, _b; + try { + const { data: { session }, error, } = result; + if (error) + throw error; + await ((_a = this.stateChangeEmitters.get(id)) === null || _a === void 0 ? void 0 : _a.callback('INITIAL_SESSION', session)); + this._debug('INITIAL_SESSION', 'callback id', id, 'session', session); + } + catch (err) { + await ((_b = this.stateChangeEmitters.get(id)) === null || _b === void 0 ? void 0 : _b.callback('INITIAL_SESSION', null)); + this._debug('INITIAL_SESSION', 'callback id', id, 'error', err); + console.error(err); + } + }); + } + /** + * Sends a password reset request to an email address. This method supports the PKCE flow. + * + * @param email The email address of the user. + * @param options.redirectTo The URL to send the user to after they click the password reset link. + * @param options.captchaToken Verification token received when the user completes the captcha on the site. + */ + async resetPasswordForEmail(email, options = {}) { + let codeChallenge = null; + let codeChallengeMethod = null; + if (this.flowType === 'pkce') { + ; + [codeChallenge, codeChallengeMethod] = await (0, helpers_1.getCodeChallengeAndMethod)(this.storage, this.storageKey, true // isPasswordRecovery + ); + } + try { + return await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/recover`, { + body: { + email, + code_challenge: codeChallenge, + code_challenge_method: codeChallengeMethod, + gotrue_meta_security: { captcha_token: options.captchaToken }, + }, + headers: this.headers, + redirectTo: options.redirectTo, + }); + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: null, error }; + } + throw error; + } + } + /** + * Gets all the identities linked to a user. + */ + async getUserIdentities() { + var _a; + try { + const { data, error } = await this.getUser(); + if (error) + throw error; + return { data: { identities: (_a = data.user.identities) !== null && _a !== void 0 ? _a : [] }, error: null }; + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: null, error }; + } + throw error; + } + } + /** + * Links an oauth identity to an existing user. + * This method supports the PKCE flow. + */ + async linkIdentity(credentials) { + var _a; + try { + const { data, error } = await this._useSession(async (result) => { + var _a, _b, _c, _d, _e; + const { data, error } = result; + if (error) + throw error; + const url = await this._getUrlForProvider(`${this.url}/user/identities/authorize`, credentials.provider, { + redirectTo: (_a = credentials.options) === null || _a === void 0 ? void 0 : _a.redirectTo, + scopes: (_b = credentials.options) === null || _b === void 0 ? void 0 : _b.scopes, + queryParams: (_c = credentials.options) === null || _c === void 0 ? void 0 : _c.queryParams, + skipBrowserRedirect: true, + }); + return await (0, fetch_1._request)(this.fetch, 'GET', url, { + headers: this.headers, + jwt: (_e = (_d = data.session) === null || _d === void 0 ? void 0 : _d.access_token) !== null && _e !== void 0 ? _e : undefined, + }); + }); + if (error) + throw error; + if ((0, helpers_1.isBrowser)() && !((_a = credentials.options) === null || _a === void 0 ? void 0 : _a.skipBrowserRedirect)) { + window.location.assign(data === null || data === void 0 ? void 0 : data.url); + } + return { data: { provider: credentials.provider, url: data === null || data === void 0 ? void 0 : data.url }, error: null }; + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: { provider: credentials.provider, url: null }, error }; + } + throw error; + } + } + /** + * Unlinks an identity from a user by deleting it. The user will no longer be able to sign in with that identity once it's unlinked. + */ + async unlinkIdentity(identity) { + try { + return await this._useSession(async (result) => { + var _a, _b; + const { data, error } = result; + if (error) { + throw error; + } + return await (0, fetch_1._request)(this.fetch, 'DELETE', `${this.url}/user/identities/${identity.identity_id}`, { + headers: this.headers, + jwt: (_b = (_a = data.session) === null || _a === void 0 ? void 0 : _a.access_token) !== null && _b !== void 0 ? _b : undefined, + }); + }); + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: null, error }; + } + throw error; + } + } + /** + * Generates a new JWT. + * @param refreshToken A valid refresh token that was returned on login. + */ + async _refreshAccessToken(refreshToken) { + const debugName = `#_refreshAccessToken(${refreshToken.substring(0, 5)}...)`; + this._debug(debugName, 'begin'); + try { + const startedAt = Date.now(); + // will attempt to refresh the token with exponential backoff + return await (0, helpers_1.retryable)(async (attempt) => { + if (attempt > 0) { + await (0, helpers_1.sleep)(200 * Math.pow(2, attempt - 1)); // 200, 400, 800, ... + } + this._debug(debugName, 'refreshing attempt', attempt); + return await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/token?grant_type=refresh_token`, { + body: { refresh_token: refreshToken }, + headers: this.headers, + xform: fetch_1._sessionResponse, + }); + }, (attempt, error) => { + const nextBackOffInterval = 200 * Math.pow(2, attempt); + return (error && + (0, errors_1.isAuthRetryableFetchError)(error) && + // retryable only if the request can be sent before the backoff overflows the tick duration + Date.now() + nextBackOffInterval - startedAt < AUTO_REFRESH_TICK_DURATION); + }); + } + catch (error) { + this._debug(debugName, 'error', error); + if ((0, errors_1.isAuthError)(error)) { + return { data: { session: null, user: null }, error }; + } + throw error; + } + finally { + this._debug(debugName, 'end'); + } + } + _isValidSession(maybeSession) { + const isValidSession = typeof maybeSession === 'object' && + maybeSession !== null && + 'access_token' in maybeSession && + 'refresh_token' in maybeSession && + 'expires_at' in maybeSession; + return isValidSession; + } + async _handleProviderSignIn(provider, options) { + const url = await this._getUrlForProvider(`${this.url}/authorize`, provider, { + redirectTo: options.redirectTo, + scopes: options.scopes, + queryParams: options.queryParams, + }); + this._debug('#_handleProviderSignIn()', 'provider', provider, 'options', options, 'url', url); + // try to open on the browser + if ((0, helpers_1.isBrowser)() && !options.skipBrowserRedirect) { + window.location.assign(url); + } + return { data: { provider, url }, error: null }; + } + /** + * Recovers the session from LocalStorage and refreshes the token + * Note: this method is async to accommodate for AsyncStorage e.g. in React native. + */ + async _recoverAndRefresh() { + var _a; + const debugName = '#_recoverAndRefresh()'; + this._debug(debugName, 'begin'); + try { + const currentSession = await (0, helpers_1.getItemAsync)(this.storage, this.storageKey); + this._debug(debugName, 'session from storage', currentSession); + if (!this._isValidSession(currentSession)) { + this._debug(debugName, 'session is not valid'); + if (currentSession !== null) { + await this._removeSession(); + } + return; + } + const timeNow = Math.round(Date.now() / 1000); + const expiresWithMargin = ((_a = currentSession.expires_at) !== null && _a !== void 0 ? _a : Infinity) < timeNow + constants_1.EXPIRY_MARGIN; + this._debug(debugName, `session has${expiresWithMargin ? '' : ' not'} expired with margin of ${constants_1.EXPIRY_MARGIN}s`); + if (expiresWithMargin) { + if (this.autoRefreshToken && currentSession.refresh_token) { + const { error } = await this._callRefreshToken(currentSession.refresh_token); + if (error) { + console.error(error); + if (!(0, errors_1.isAuthRetryableFetchError)(error)) { + this._debug(debugName, 'refresh failed with a non-retryable error, removing the session', error); + await this._removeSession(); + } + } + } + } + else { + // no need to persist currentSession again, as we just loaded it from + // local storage; persisting it again may overwrite a value saved by + // another client with access to the same local storage + await this._notifyAllSubscribers('SIGNED_IN', currentSession); + } + } + catch (err) { + this._debug(debugName, 'error', err); + console.error(err); + return; + } + finally { + this._debug(debugName, 'end'); + } + } + async _callRefreshToken(refreshToken) { + var _a, _b; + if (!refreshToken) { + throw new errors_1.AuthSessionMissingError(); + } + // refreshing is already in progress + if (this.refreshingDeferred) { + return this.refreshingDeferred.promise; + } + const debugName = `#_callRefreshToken(${refreshToken.substring(0, 5)}...)`; + this._debug(debugName, 'begin'); + try { + this.refreshingDeferred = new helpers_1.Deferred(); + const { data, error } = await this._refreshAccessToken(refreshToken); + if (error) + throw error; + if (!data.session) + throw new errors_1.AuthSessionMissingError(); + await this._saveSession(data.session); + await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session); + const result = { session: data.session, error: null }; + this.refreshingDeferred.resolve(result); + return result; + } + catch (error) { + this._debug(debugName, 'error', error); + if ((0, errors_1.isAuthError)(error)) { + const result = { session: null, error }; + if (!(0, errors_1.isAuthRetryableFetchError)(error)) { + await this._removeSession(); + } + (_a = this.refreshingDeferred) === null || _a === void 0 ? void 0 : _a.resolve(result); + return result; + } + (_b = this.refreshingDeferred) === null || _b === void 0 ? void 0 : _b.reject(error); + throw error; + } + finally { + this.refreshingDeferred = null; + this._debug(debugName, 'end'); + } + } + async _notifyAllSubscribers(event, session, broadcast = true) { + const debugName = `#_notifyAllSubscribers(${event})`; + this._debug(debugName, 'begin', session, `broadcast = ${broadcast}`); + try { + if (this.broadcastChannel && broadcast) { + this.broadcastChannel.postMessage({ event, session }); + } + const errors = []; + const promises = Array.from(this.stateChangeEmitters.values()).map(async (x) => { + try { + await x.callback(event, session); + } + catch (e) { + errors.push(e); + } + }); + await Promise.all(promises); + if (errors.length > 0) { + for (let i = 0; i < errors.length; i += 1) { + console.error(errors[i]); + } + throw errors[0]; + } + } + finally { + this._debug(debugName, 'end'); + } + } + /** + * set currentSession and currentUser + * process to _startAutoRefreshToken if possible + */ + async _saveSession(session) { + this._debug('#_saveSession()', session); + // _saveSession is always called whenever a new session has been acquired + // so we can safely suppress the warning returned by future getSession calls + this.suppressGetSessionWarning = true; + await (0, helpers_1.setItemAsync)(this.storage, this.storageKey, session); + } + async _removeSession() { + this._debug('#_removeSession()'); + await (0, helpers_1.removeItemAsync)(this.storage, this.storageKey); + await this._notifyAllSubscribers('SIGNED_OUT', null); + } + /** + * Removes any registered visibilitychange callback. + * + * {@see #startAutoRefresh} + * {@see #stopAutoRefresh} + */ + _removeVisibilityChangedCallback() { + this._debug('#_removeVisibilityChangedCallback()'); + const callback = this.visibilityChangedCallback; + this.visibilityChangedCallback = null; + try { + if (callback && (0, helpers_1.isBrowser)() && (window === null || window === void 0 ? void 0 : window.removeEventListener)) { + window.removeEventListener('visibilitychange', callback); + } + } + catch (e) { + console.error('removing visibilitychange callback failed', e); + } + } + /** + * This is the private implementation of {@link #startAutoRefresh}. Use this + * within the library. + */ + async _startAutoRefresh() { + await this._stopAutoRefresh(); + this._debug('#_startAutoRefresh()'); + const ticker = setInterval(() => this._autoRefreshTokenTick(), AUTO_REFRESH_TICK_DURATION); + this.autoRefreshTicker = ticker; + if (ticker && typeof ticker === 'object' && typeof ticker.unref === 'function') { + // ticker is a NodeJS Timeout object that has an `unref` method + // https://nodejs.org/api/timers.html#timeoutunref + // When auto refresh is used in NodeJS (like for testing) the + // `setInterval` is preventing the process from being marked as + // finished and tests run endlessly. This can be prevented by calling + // `unref()` on the returned object. + ticker.unref(); + // @ts-expect-error TS has no context of Deno + } + else if (typeof Deno !== 'undefined' && typeof Deno.unrefTimer === 'function') { + // similar like for NodeJS, but with the Deno API + // https://deno.land/api@latest?unstable&s=Deno.unrefTimer + // @ts-expect-error TS has no context of Deno + Deno.unrefTimer(ticker); + } + // run the tick immediately, but in the next pass of the event loop so that + // #_initialize can be allowed to complete without recursively waiting on + // itself + setTimeout(async () => { + await this.initializePromise; + await this._autoRefreshTokenTick(); + }, 0); + } + /** + * This is the private implementation of {@link #stopAutoRefresh}. Use this + * within the library. + */ + async _stopAutoRefresh() { + this._debug('#_stopAutoRefresh()'); + const ticker = this.autoRefreshTicker; + this.autoRefreshTicker = null; + if (ticker) { + clearInterval(ticker); + } + } + /** + * Starts an auto-refresh process in the background. The session is checked + * every few seconds. Close to the time of expiration a process is started to + * refresh the session. If refreshing fails it will be retried for as long as + * necessary. + * + * If you set the {@link GoTrueClientOptions#autoRefreshToken} you don't need + * to call this function, it will be called for you. + * + * On browsers the refresh process works only when the tab/window is in the + * foreground to conserve resources as well as prevent race conditions and + * flooding auth with requests. If you call this method any managed + * visibility change callback will be removed and you must manage visibility + * changes on your own. + * + * On non-browser platforms the refresh process works *continuously* in the + * background, which may not be desirable. You should hook into your + * platform's foreground indication mechanism and call these methods + * appropriately to conserve resources. + * + * {@see #stopAutoRefresh} + */ + async startAutoRefresh() { + this._removeVisibilityChangedCallback(); + await this._startAutoRefresh(); + } + /** + * Stops an active auto refresh process running in the background (if any). + * + * If you call this method any managed visibility change callback will be + * removed and you must manage visibility changes on your own. + * + * See {@link #startAutoRefresh} for more details. + */ + async stopAutoRefresh() { + this._removeVisibilityChangedCallback(); + await this._stopAutoRefresh(); + } + /** + * Runs the auto refresh token tick. + */ + async _autoRefreshTokenTick() { + this._debug('#_autoRefreshTokenTick()', 'begin'); + try { + await this._acquireLock(0, async () => { + try { + const now = Date.now(); + try { + return await this._useSession(async (result) => { + const { data: { session }, } = result; + if (!session || !session.refresh_token || !session.expires_at) { + this._debug('#_autoRefreshTokenTick()', 'no session'); + return; + } + // session will expire in this many ticks (or has already expired if <= 0) + const expiresInTicks = Math.floor((session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION); + this._debug('#_autoRefreshTokenTick()', `access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`); + if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) { + await this._callRefreshToken(session.refresh_token); + } + }); + } + catch (e) { + console.error('Auto refresh tick failed with error. This is likely a transient error.', e); + } + } + finally { + this._debug('#_autoRefreshTokenTick()', 'end'); + } + }); + } + catch (e) { + if (e.isAcquireTimeout || e instanceof locks_1.LockAcquireTimeoutError) { + this._debug('auto refresh token tick lock not available'); + } + else { + throw e; + } + } + } + /** + * Registers callbacks on the browser / platform, which in-turn run + * algorithms when the browser window/tab are in foreground. On non-browser + * platforms it assumes always foreground. + */ + async _handleVisibilityChange() { + this._debug('#_handleVisibilityChange()'); + if (!(0, helpers_1.isBrowser)() || !(window === null || window === void 0 ? void 0 : window.addEventListener)) { + if (this.autoRefreshToken) { + // in non-browser environments the refresh token ticker runs always + this.startAutoRefresh(); + } + return false; + } + try { + this.visibilityChangedCallback = async () => await this._onVisibilityChanged(false); + window === null || window === void 0 ? void 0 : window.addEventListener('visibilitychange', this.visibilityChangedCallback); + // now immediately call the visbility changed callback to setup with the + // current visbility state + await this._onVisibilityChanged(true); // initial call + } + catch (error) { + console.error('_handleVisibilityChange', error); + } + } + /** + * Callback registered with `window.addEventListener('visibilitychange')`. + */ + async _onVisibilityChanged(calledFromInitialize) { + const methodName = `#_onVisibilityChanged(${calledFromInitialize})`; + this._debug(methodName, 'visibilityState', document.visibilityState); + if (document.visibilityState === 'visible') { + if (this.autoRefreshToken) { + // in browser environments the refresh token ticker runs only on focused tabs + // which prevents race conditions + this._startAutoRefresh(); + } + if (!calledFromInitialize) { + // called when the visibility has changed, i.e. the browser + // transitioned from hidden -> visible so we need to see if the session + // should be recovered immediately... but to do that we need to acquire + // the lock first asynchronously + await this.initializePromise; + await this._acquireLock(-1, async () => { + if (document.visibilityState !== 'visible') { + this._debug(methodName, 'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting'); + // visibility has changed while waiting for the lock, abort + return; + } + // recover the session + await this._recoverAndRefresh(); + }); + } + } + else if (document.visibilityState === 'hidden') { + if (this.autoRefreshToken) { + this._stopAutoRefresh(); + } + } + } + /** + * Generates the relevant login URL for a third-party provider. + * @param options.redirectTo A URL or mobile address to send the user to after they are confirmed. + * @param options.scopes A space-separated list of scopes granted to the OAuth application. + * @param options.queryParams An object of key-value pairs containing query parameters granted to the OAuth application. + */ + async _getUrlForProvider(url, provider, options) { + const urlParams = [`provider=${encodeURIComponent(provider)}`]; + if (options === null || options === void 0 ? void 0 : options.redirectTo) { + urlParams.push(`redirect_to=${encodeURIComponent(options.redirectTo)}`); + } + if (options === null || options === void 0 ? void 0 : options.scopes) { + urlParams.push(`scopes=${encodeURIComponent(options.scopes)}`); + } + if (this.flowType === 'pkce') { + const [codeChallenge, codeChallengeMethod] = await (0, helpers_1.getCodeChallengeAndMethod)(this.storage, this.storageKey); + const flowParams = new URLSearchParams({ + code_challenge: `${encodeURIComponent(codeChallenge)}`, + code_challenge_method: `${encodeURIComponent(codeChallengeMethod)}`, + }); + urlParams.push(flowParams.toString()); + } + if (options === null || options === void 0 ? void 0 : options.queryParams) { + const query = new URLSearchParams(options.queryParams); + urlParams.push(query.toString()); + } + if (options === null || options === void 0 ? void 0 : options.skipBrowserRedirect) { + urlParams.push(`skip_http_redirect=${options.skipBrowserRedirect}`); + } + return `${url}?${urlParams.join('&')}`; + } + async _unenroll(params) { + try { + return await this._useSession(async (result) => { + var _a; + const { data: sessionData, error: sessionError } = result; + if (sessionError) { + return { data: null, error: sessionError }; + } + return await (0, fetch_1._request)(this.fetch, 'DELETE', `${this.url}/factors/${params.factorId}`, { + headers: this.headers, + jwt: (_a = sessionData === null || sessionData === void 0 ? void 0 : sessionData.session) === null || _a === void 0 ? void 0 : _a.access_token, + }); + }); + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: null, error }; + } + throw error; + } + } + async _enroll(params) { + try { + return await this._useSession(async (result) => { + var _a, _b; + const { data: sessionData, error: sessionError } = result; + if (sessionError) { + return { data: null, error: sessionError }; + } + const body = Object.assign({ friendly_name: params.friendlyName, factor_type: params.factorType }, (params.factorType === 'phone' ? { phone: params.phone } : { issuer: params.issuer })); + const { data, error } = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/factors`, { + body, + headers: this.headers, + jwt: (_a = sessionData === null || sessionData === void 0 ? void 0 : sessionData.session) === null || _a === void 0 ? void 0 : _a.access_token, + }); + if (error) { + return { data: null, error }; + } + if (params.factorType === 'totp' && ((_b = data === null || data === void 0 ? void 0 : data.totp) === null || _b === void 0 ? void 0 : _b.qr_code)) { + data.totp.qr_code = `data:image/svg+xml;utf-8,${data.totp.qr_code}`; + } + return { data, error: null }; + }); + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: null, error }; + } + throw error; + } + } + /** + * {@see GoTrueMFAApi#verify} + */ + async _verify(params) { + return this._acquireLock(-1, async () => { + try { + return await this._useSession(async (result) => { + var _a; + const { data: sessionData, error: sessionError } = result; + if (sessionError) { + return { data: null, error: sessionError }; + } + const { data, error } = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/factors/${params.factorId}/verify`, { + body: { code: params.code, challenge_id: params.challengeId }, + headers: this.headers, + jwt: (_a = sessionData === null || sessionData === void 0 ? void 0 : sessionData.session) === null || _a === void 0 ? void 0 : _a.access_token, + }); + if (error) { + return { data: null, error }; + } + await this._saveSession(Object.assign({ expires_at: Math.round(Date.now() / 1000) + data.expires_in }, data)); + await this._notifyAllSubscribers('MFA_CHALLENGE_VERIFIED', data); + return { data, error }; + }); + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: null, error }; + } + throw error; + } + }); + } + /** + * {@see GoTrueMFAApi#challenge} + */ + async _challenge(params) { + return this._acquireLock(-1, async () => { + try { + return await this._useSession(async (result) => { + var _a; + const { data: sessionData, error: sessionError } = result; + if (sessionError) { + return { data: null, error: sessionError }; + } + return await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/factors/${params.factorId}/challenge`, { + body: { channel: params.channel }, + headers: this.headers, + jwt: (_a = sessionData === null || sessionData === void 0 ? void 0 : sessionData.session) === null || _a === void 0 ? void 0 : _a.access_token, + }); + }); + } + catch (error) { + if ((0, errors_1.isAuthError)(error)) { + return { data: null, error }; + } + throw error; + } + }); + } + /** + * {@see GoTrueMFAApi#challengeAndVerify} + */ + async _challengeAndVerify(params) { + // both _challenge and _verify independently acquire the lock, so no need + // to acquire it here + const { data: challengeData, error: challengeError } = await this._challenge({ + factorId: params.factorId, + }); + if (challengeError) { + return { data: null, error: challengeError }; + } + return await this._verify({ + factorId: params.factorId, + challengeId: challengeData.id, + code: params.code, + }); + } + /** + * {@see GoTrueMFAApi#listFactors} + */ + async _listFactors() { + // use #getUser instead of #_getUser as the former acquires a lock + const { data: { user }, error: userError, } = await this.getUser(); + if (userError) { + return { data: null, error: userError }; + } + const factors = (user === null || user === void 0 ? void 0 : user.factors) || []; + const totp = factors.filter((factor) => factor.factor_type === 'totp' && factor.status === 'verified'); + const phone = factors.filter((factor) => factor.factor_type === 'phone' && factor.status === 'verified'); + return { + data: { + all: factors, + totp, + phone, + }, + error: null, + }; + } + /** + * {@see GoTrueMFAApi#getAuthenticatorAssuranceLevel} + */ + async _getAuthenticatorAssuranceLevel() { + return this._acquireLock(-1, async () => { + return await this._useSession(async (result) => { + var _a, _b; + const { data: { session }, error: sessionError, } = result; + if (sessionError) { + return { data: null, error: sessionError }; + } + if (!session) { + return { + data: { currentLevel: null, nextLevel: null, currentAuthenticationMethods: [] }, + error: null, + }; + } + const payload = this._decodeJWT(session.access_token); + let currentLevel = null; + if (payload.aal) { + currentLevel = payload.aal; + } + let nextLevel = currentLevel; + const verifiedFactors = (_b = (_a = session.user.factors) === null || _a === void 0 ? void 0 : _a.filter((factor) => factor.status === 'verified')) !== null && _b !== void 0 ? _b : []; + if (verifiedFactors.length > 0) { + nextLevel = 'aal2'; + } + const currentAuthenticationMethods = payload.amr || []; + return { data: { currentLevel, nextLevel, currentAuthenticationMethods }, error: null }; + }); + }); + } +} +exports["default"] = GoTrueClient; +GoTrueClient.nextInstanceID = 0; +//# sourceMappingURL=GoTrueClient.js.map + +/***/ }), + +/***/ 6748: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.lockInternals = exports.NavigatorLockAcquireTimeoutError = exports.navigatorLock = exports.AuthClient = exports.AuthAdminApi = exports.GoTrueClient = exports.GoTrueAdminApi = void 0; +const GoTrueAdminApi_1 = __importDefault(__nccwpck_require__(6575)); +exports.GoTrueAdminApi = GoTrueAdminApi_1.default; +const GoTrueClient_1 = __importDefault(__nccwpck_require__(313)); +exports.GoTrueClient = GoTrueClient_1.default; +const AuthAdminApi_1 = __importDefault(__nccwpck_require__(9809)); +exports.AuthAdminApi = AuthAdminApi_1.default; +const AuthClient_1 = __importDefault(__nccwpck_require__(4525)); +exports.AuthClient = AuthClient_1.default; +__exportStar(__nccwpck_require__(1852), exports); +__exportStar(__nccwpck_require__(9938), exports); +var locks_1 = __nccwpck_require__(163); +Object.defineProperty(exports, "navigatorLock", ({ enumerable: true, get: function () { return locks_1.navigatorLock; } })); +Object.defineProperty(exports, "NavigatorLockAcquireTimeoutError", ({ enumerable: true, get: function () { return locks_1.NavigatorLockAcquireTimeoutError; } })); +Object.defineProperty(exports, "lockInternals", ({ enumerable: true, get: function () { return locks_1.internals; } })); +//# sourceMappingURL=index.js.map + +/***/ }), + +/***/ 518: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.API_VERSIONS = exports.API_VERSION_HEADER_NAME = exports.NETWORK_FAILURE = exports.EXPIRY_MARGIN = exports.DEFAULT_HEADERS = exports.AUDIENCE = exports.STORAGE_KEY = exports.GOTRUE_URL = void 0; +const version_1 = __nccwpck_require__(6677); +exports.GOTRUE_URL = 'http://localhost:9999'; +exports.STORAGE_KEY = 'supabase.auth.token'; +exports.AUDIENCE = ''; +exports.DEFAULT_HEADERS = { 'X-Client-Info': `gotrue-js/${version_1.version}` }; +exports.EXPIRY_MARGIN = 10; // in seconds +exports.NETWORK_FAILURE = { + MAX_RETRIES: 10, + RETRY_INTERVAL: 2, // in deciseconds +}; +exports.API_VERSION_HEADER_NAME = 'X-Supabase-Api-Version'; +exports.API_VERSIONS = { + '2024-01-01': { + timestamp: Date.parse('2024-01-01T00:00:00.0Z'), + name: '2024-01-01', + }, +}; +//# sourceMappingURL=constants.js.map + +/***/ }), + +/***/ 9938: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.isAuthWeakPasswordError = exports.AuthWeakPasswordError = exports.isAuthRetryableFetchError = exports.AuthRetryableFetchError = exports.AuthPKCEGrantCodeExchangeError = exports.isAuthImplicitGrantRedirectError = exports.AuthImplicitGrantRedirectError = exports.AuthInvalidCredentialsError = exports.AuthInvalidTokenResponseError = exports.isAuthSessionMissingError = exports.AuthSessionMissingError = exports.CustomAuthError = exports.AuthUnknownError = exports.isAuthApiError = exports.AuthApiError = exports.isAuthError = exports.AuthError = void 0; +class AuthError extends Error { + constructor(message, status, code) { + super(message); + this.__isAuthError = true; + this.name = 'AuthError'; + this.status = status; + this.code = code; + } +} +exports.AuthError = AuthError; +function isAuthError(error) { + return typeof error === 'object' && error !== null && '__isAuthError' in error; +} +exports.isAuthError = isAuthError; +class AuthApiError extends AuthError { + constructor(message, status, code) { + super(message, status, code); + this.name = 'AuthApiError'; + this.status = status; + this.code = code; + } +} +exports.AuthApiError = AuthApiError; +function isAuthApiError(error) { + return isAuthError(error) && error.name === 'AuthApiError'; +} +exports.isAuthApiError = isAuthApiError; +class AuthUnknownError extends AuthError { + constructor(message, originalError) { + super(message); + this.name = 'AuthUnknownError'; + this.originalError = originalError; + } +} +exports.AuthUnknownError = AuthUnknownError; +class CustomAuthError extends AuthError { + constructor(message, name, status, code) { + super(message, status, code); + this.name = name; + this.status = status; + } +} +exports.CustomAuthError = CustomAuthError; +class AuthSessionMissingError extends CustomAuthError { + constructor() { + super('Auth session missing!', 'AuthSessionMissingError', 400, undefined); + } +} +exports.AuthSessionMissingError = AuthSessionMissingError; +function isAuthSessionMissingError(error) { + return isAuthError(error) && error.name === 'AuthSessionMissingError'; +} +exports.isAuthSessionMissingError = isAuthSessionMissingError; +class AuthInvalidTokenResponseError extends CustomAuthError { + constructor() { + super('Auth session or user missing', 'AuthInvalidTokenResponseError', 500, undefined); + } +} +exports.AuthInvalidTokenResponseError = AuthInvalidTokenResponseError; +class AuthInvalidCredentialsError extends CustomAuthError { + constructor(message) { + super(message, 'AuthInvalidCredentialsError', 400, undefined); + } +} +exports.AuthInvalidCredentialsError = AuthInvalidCredentialsError; +class AuthImplicitGrantRedirectError extends CustomAuthError { + constructor(message, details = null) { + super(message, 'AuthImplicitGrantRedirectError', 500, undefined); + this.details = null; + this.details = details; + } + toJSON() { + return { + name: this.name, + message: this.message, + status: this.status, + details: this.details, + }; + } +} +exports.AuthImplicitGrantRedirectError = AuthImplicitGrantRedirectError; +function isAuthImplicitGrantRedirectError(error) { + return isAuthError(error) && error.name === 'AuthImplicitGrantRedirectError'; +} +exports.isAuthImplicitGrantRedirectError = isAuthImplicitGrantRedirectError; +class AuthPKCEGrantCodeExchangeError extends CustomAuthError { + constructor(message, details = null) { + super(message, 'AuthPKCEGrantCodeExchangeError', 500, undefined); + this.details = null; + this.details = details; + } + toJSON() { + return { + name: this.name, + message: this.message, + status: this.status, + details: this.details, + }; + } +} +exports.AuthPKCEGrantCodeExchangeError = AuthPKCEGrantCodeExchangeError; +class AuthRetryableFetchError extends CustomAuthError { + constructor(message, status) { + super(message, 'AuthRetryableFetchError', status, undefined); + } +} +exports.AuthRetryableFetchError = AuthRetryableFetchError; +function isAuthRetryableFetchError(error) { + return isAuthError(error) && error.name === 'AuthRetryableFetchError'; +} +exports.isAuthRetryableFetchError = isAuthRetryableFetchError; +/** + * This error is thrown on certain methods when the password used is deemed + * weak. Inspect the reasons to identify what password strength rules are + * inadequate. + */ +class AuthWeakPasswordError extends CustomAuthError { + constructor(message, status, reasons) { + super(message, 'AuthWeakPasswordError', status, 'weak_password'); + this.reasons = reasons; + } +} +exports.AuthWeakPasswordError = AuthWeakPasswordError; +function isAuthWeakPasswordError(error) { + return isAuthError(error) && error.name === 'AuthWeakPasswordError'; +} +exports.isAuthWeakPasswordError = isAuthWeakPasswordError; +//# sourceMappingURL=errors.js.map + +/***/ }), + +/***/ 7072: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports._noResolveJsonResponse = exports._generateLinkResponse = exports._ssoResponse = exports._userResponse = exports._sessionResponsePassword = exports._sessionResponse = exports._request = exports.handleError = void 0; +const constants_1 = __nccwpck_require__(518); +const helpers_1 = __nccwpck_require__(4044); +const errors_1 = __nccwpck_require__(9938); +const _getErrorMessage = (err) => err.msg || err.message || err.error_description || err.error || JSON.stringify(err); +const NETWORK_ERROR_CODES = [502, 503, 504]; +async function handleError(error) { + var _a; + if (!(0, helpers_1.looksLikeFetchResponse)(error)) { + throw new errors_1.AuthRetryableFetchError(_getErrorMessage(error), 0); + } + if (NETWORK_ERROR_CODES.includes(error.status)) { + // status in 500...599 range - server had an error, request might be retryed. + throw new errors_1.AuthRetryableFetchError(_getErrorMessage(error), error.status); + } + let data; + try { + data = await error.json(); + } + catch (e) { + throw new errors_1.AuthUnknownError(_getErrorMessage(e), e); + } + let errorCode = undefined; + const responseAPIVersion = (0, helpers_1.parseResponseAPIVersion)(error); + if (responseAPIVersion && + responseAPIVersion.getTime() >= constants_1.API_VERSIONS['2024-01-01'].timestamp && + typeof data === 'object' && + data && + typeof data.code === 'string') { + errorCode = data.code; + } + else if (typeof data === 'object' && data && typeof data.error_code === 'string') { + errorCode = data.error_code; + } + if (!errorCode) { + // Legacy support for weak password errors, when there were no error codes + if (typeof data === 'object' && + data && + typeof data.weak_password === 'object' && + data.weak_password && + Array.isArray(data.weak_password.reasons) && + data.weak_password.reasons.length && + data.weak_password.reasons.reduce((a, i) => a && typeof i === 'string', true)) { + throw new errors_1.AuthWeakPasswordError(_getErrorMessage(data), error.status, data.weak_password.reasons); + } + } + else if (errorCode === 'weak_password') { + throw new errors_1.AuthWeakPasswordError(_getErrorMessage(data), error.status, ((_a = data.weak_password) === null || _a === void 0 ? void 0 : _a.reasons) || []); + } + else if (errorCode === 'session_not_found') { + // The `session_id` inside the JWT does not correspond to a row in the + // `sessions` table. This usually means the user has signed out, has been + // deleted, or their session has somehow been terminated. + throw new errors_1.AuthSessionMissingError(); + } + throw new errors_1.AuthApiError(_getErrorMessage(data), error.status || 500, errorCode); +} +exports.handleError = handleError; +const _getRequestParams = (method, options, parameters, body) => { + const params = { method, headers: (options === null || options === void 0 ? void 0 : options.headers) || {} }; + if (method === 'GET') { + return params; + } + params.headers = Object.assign({ 'Content-Type': 'application/json;charset=UTF-8' }, options === null || options === void 0 ? void 0 : options.headers); + params.body = JSON.stringify(body); + return Object.assign(Object.assign({}, params), parameters); +}; +async function _request(fetcher, method, url, options) { + var _a; + const headers = Object.assign({}, options === null || options === void 0 ? void 0 : options.headers); + if (!headers[constants_1.API_VERSION_HEADER_NAME]) { + headers[constants_1.API_VERSION_HEADER_NAME] = constants_1.API_VERSIONS['2024-01-01'].name; + } + if (options === null || options === void 0 ? void 0 : options.jwt) { + headers['Authorization'] = `Bearer ${options.jwt}`; + } + const qs = (_a = options === null || options === void 0 ? void 0 : options.query) !== null && _a !== void 0 ? _a : {}; + if (options === null || options === void 0 ? void 0 : options.redirectTo) { + qs['redirect_to'] = options.redirectTo; + } + const queryString = Object.keys(qs).length ? '?' + new URLSearchParams(qs).toString() : ''; + const data = await _handleRequest(fetcher, method, url + queryString, { + headers, + noResolveJson: options === null || options === void 0 ? void 0 : options.noResolveJson, + }, {}, options === null || options === void 0 ? void 0 : options.body); + return (options === null || options === void 0 ? void 0 : options.xform) ? options === null || options === void 0 ? void 0 : options.xform(data) : { data: Object.assign({}, data), error: null }; +} +exports._request = _request; +async function _handleRequest(fetcher, method, url, options, parameters, body) { + const requestParams = _getRequestParams(method, options, parameters, body); + let result; + try { + result = await fetcher(url, Object.assign({}, requestParams)); + } + catch (e) { + console.error(e); + // fetch failed, likely due to a network or CORS error + throw new errors_1.AuthRetryableFetchError(_getErrorMessage(e), 0); + } + if (!result.ok) { + await handleError(result); + } + if (options === null || options === void 0 ? void 0 : options.noResolveJson) { + return result; + } + try { + return await result.json(); + } + catch (e) { + await handleError(e); + } +} +function _sessionResponse(data) { + var _a; + let session = null; + if (hasSession(data)) { + session = Object.assign({}, data); + if (!data.expires_at) { + session.expires_at = (0, helpers_1.expiresAt)(data.expires_in); + } + } + const user = (_a = data.user) !== null && _a !== void 0 ? _a : data; + return { data: { session, user }, error: null }; +} +exports._sessionResponse = _sessionResponse; +function _sessionResponsePassword(data) { + const response = _sessionResponse(data); + if (!response.error && + data.weak_password && + typeof data.weak_password === 'object' && + Array.isArray(data.weak_password.reasons) && + data.weak_password.reasons.length && + data.weak_password.message && + typeof data.weak_password.message === 'string' && + data.weak_password.reasons.reduce((a, i) => a && typeof i === 'string', true)) { + response.data.weak_password = data.weak_password; + } + return response; +} +exports._sessionResponsePassword = _sessionResponsePassword; +function _userResponse(data) { + var _a; + const user = (_a = data.user) !== null && _a !== void 0 ? _a : data; + return { data: { user }, error: null }; +} +exports._userResponse = _userResponse; +function _ssoResponse(data) { + return { data, error: null }; +} +exports._ssoResponse = _ssoResponse; +function _generateLinkResponse(data) { + const { action_link, email_otp, hashed_token, redirect_to, verification_type } = data, rest = __rest(data, ["action_link", "email_otp", "hashed_token", "redirect_to", "verification_type"]); + const properties = { + action_link, + email_otp, + hashed_token, + redirect_to, + verification_type, + }; + const user = Object.assign({}, rest); + return { + data: { + properties, + user, + }, + error: null, + }; +} +exports._generateLinkResponse = _generateLinkResponse; +function _noResolveJsonResponse(data) { + return data; +} +exports._noResolveJsonResponse = _noResolveJsonResponse; +/** + * hasSession checks if the response object contains a valid session + * @param data A response object + * @returns true if a session is in the response + */ +function hasSession(data) { + return data.access_token && data.refresh_token && data.expires_in; +} +//# sourceMappingURL=fetch.js.map + +/***/ }), + +/***/ 4044: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.parseResponseAPIVersion = exports.getCodeChallengeAndMethod = exports.generatePKCEChallenge = exports.generatePKCEVerifier = exports.retryable = exports.sleep = exports.decodeJWTPayload = exports.Deferred = exports.decodeBase64URL = exports.removeItemAsync = exports.getItemAsync = exports.setItemAsync = exports.looksLikeFetchResponse = exports.resolveFetch = exports.parseParametersFromURL = exports.supportsLocalStorage = exports.isBrowser = exports.uuid = exports.expiresAt = void 0; +const constants_1 = __nccwpck_require__(518); +function expiresAt(expiresIn) { + const timeNow = Math.round(Date.now() / 1000); + return timeNow + expiresIn; +} +exports.expiresAt = expiresAt; +function uuid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} +exports.uuid = uuid; +const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined'; +exports.isBrowser = isBrowser; +const localStorageWriteTests = { + tested: false, + writable: false, +}; +/** + * Checks whether localStorage is supported on this browser. + */ +const supportsLocalStorage = () => { + if (!(0, exports.isBrowser)()) { + return false; + } + try { + if (typeof globalThis.localStorage !== 'object') { + return false; + } + } + catch (e) { + // DOM exception when accessing `localStorage` + return false; + } + if (localStorageWriteTests.tested) { + return localStorageWriteTests.writable; + } + const randomKey = `lswt-${Math.random()}${Math.random()}`; + try { + globalThis.localStorage.setItem(randomKey, randomKey); + globalThis.localStorage.removeItem(randomKey); + localStorageWriteTests.tested = true; + localStorageWriteTests.writable = true; + } + catch (e) { + // localStorage can't be written to + // https://www.chromium.org/for-testers/bug-reporting-guidelines/uncaught-securityerror-failed-to-read-the-localstorage-property-from-window-access-is-denied-for-this-document + localStorageWriteTests.tested = true; + localStorageWriteTests.writable = false; + } + return localStorageWriteTests.writable; +}; +exports.supportsLocalStorage = supportsLocalStorage; +/** + * Extracts parameters encoded in the URL both in the query and fragment. + */ +function parseParametersFromURL(href) { + const result = {}; + const url = new URL(href); + if (url.hash && url.hash[0] === '#') { + try { + const hashSearchParams = new URLSearchParams(url.hash.substring(1)); + hashSearchParams.forEach((value, key) => { + result[key] = value; + }); + } + catch (e) { + // hash is not a query string + } + } + // search parameters take precedence over hash parameters + url.searchParams.forEach((value, key) => { + result[key] = value; + }); + return result; +} +exports.parseParametersFromURL = parseParametersFromURL; +const resolveFetch = (customFetch) => { + let _fetch; + if (customFetch) { + _fetch = customFetch; + } + else if (typeof fetch === 'undefined') { + _fetch = (...args) => Promise.resolve().then(() => __importStar(__nccwpck_require__(2668))).then(({ default: fetch }) => fetch(...args)); + } + else { + _fetch = fetch; + } + return (...args) => _fetch(...args); +}; +exports.resolveFetch = resolveFetch; +const looksLikeFetchResponse = (maybeResponse) => { + return (typeof maybeResponse === 'object' && + maybeResponse !== null && + 'status' in maybeResponse && + 'ok' in maybeResponse && + 'json' in maybeResponse && + typeof maybeResponse.json === 'function'); +}; +exports.looksLikeFetchResponse = looksLikeFetchResponse; +// Storage helpers +const setItemAsync = async (storage, key, data) => { + await storage.setItem(key, JSON.stringify(data)); +}; +exports.setItemAsync = setItemAsync; +const getItemAsync = async (storage, key) => { + const value = await storage.getItem(key); + if (!value) { + return null; + } + try { + return JSON.parse(value); + } + catch (_a) { + return value; + } +}; +exports.getItemAsync = getItemAsync; +const removeItemAsync = async (storage, key) => { + await storage.removeItem(key); +}; +exports.removeItemAsync = removeItemAsync; +function decodeBase64URL(value) { + const key = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + let base64 = ''; + let chr1, chr2, chr3; + let enc1, enc2, enc3, enc4; + let i = 0; + value = value.replace('-', '+').replace('_', '/'); + while (i < value.length) { + enc1 = key.indexOf(value.charAt(i++)); + enc2 = key.indexOf(value.charAt(i++)); + enc3 = key.indexOf(value.charAt(i++)); + enc4 = key.indexOf(value.charAt(i++)); + chr1 = (enc1 << 2) | (enc2 >> 4); + chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); + chr3 = ((enc3 & 3) << 6) | enc4; + base64 = base64 + String.fromCharCode(chr1); + if (enc3 != 64 && chr2 != 0) { + base64 = base64 + String.fromCharCode(chr2); + } + if (enc4 != 64 && chr3 != 0) { + base64 = base64 + String.fromCharCode(chr3); + } + } + return base64; +} +exports.decodeBase64URL = decodeBase64URL; +/** + * A deferred represents some asynchronous work that is not yet finished, which + * may or may not culminate in a value. + * Taken from: https://github.com/mike-north/types/blob/master/src/async.ts + */ +class Deferred { + constructor() { + // eslint-disable-next-line @typescript-eslint/no-extra-semi + ; + this.promise = new Deferred.promiseConstructor((res, rej) => { + // eslint-disable-next-line @typescript-eslint/no-extra-semi + ; + this.resolve = res; + this.reject = rej; + }); + } +} +exports.Deferred = Deferred; +Deferred.promiseConstructor = Promise; +// Taken from: https://stackoverflow.com/questions/38552003/how-to-decode-jwt-token-in-javascript-without-using-a-library +function decodeJWTPayload(token) { + // Regex checks for base64url format + const base64UrlRegex = /^([a-z0-9_-]{4})*($|[a-z0-9_-]{3}=?$|[a-z0-9_-]{2}(==)?$)$/i; + const parts = token.split('.'); + if (parts.length !== 3) { + throw new Error('JWT is not valid: not a JWT structure'); + } + if (!base64UrlRegex.test(parts[1])) { + throw new Error('JWT is not valid: payload is not in base64url format'); + } + const base64Url = parts[1]; + return JSON.parse(decodeBase64URL(base64Url)); +} +exports.decodeJWTPayload = decodeJWTPayload; +/** + * Creates a promise that resolves to null after some time. + */ +async function sleep(time) { + return await new Promise((accept) => { + setTimeout(() => accept(null), time); + }); +} +exports.sleep = sleep; +/** + * Converts the provided async function into a retryable function. Each result + * or thrown error is sent to the isRetryable function which should return true + * if the function should run again. + */ +function retryable(fn, isRetryable) { + const promise = new Promise((accept, reject) => { + // eslint-disable-next-line @typescript-eslint/no-extra-semi + ; + (async () => { + for (let attempt = 0; attempt < Infinity; attempt++) { + try { + const result = await fn(attempt); + if (!isRetryable(attempt, null, result)) { + accept(result); + return; + } + } + catch (e) { + if (!isRetryable(attempt, e)) { + reject(e); + return; + } + } + } + })(); + }); + return promise; +} +exports.retryable = retryable; +function dec2hex(dec) { + return ('0' + dec.toString(16)).substr(-2); +} +// Functions below taken from: https://stackoverflow.com/questions/63309409/creating-a-code-verifier-and-challenge-for-pkce-auth-on-spotify-api-in-reactjs +function generatePKCEVerifier() { + const verifierLength = 56; + const array = new Uint32Array(verifierLength); + if (typeof crypto === 'undefined') { + const charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; + const charSetLen = charSet.length; + let verifier = ''; + for (let i = 0; i < verifierLength; i++) { + verifier += charSet.charAt(Math.floor(Math.random() * charSetLen)); + } + return verifier; + } + crypto.getRandomValues(array); + return Array.from(array, dec2hex).join(''); +} +exports.generatePKCEVerifier = generatePKCEVerifier; +async function sha256(randomString) { + const encoder = new TextEncoder(); + const encodedData = encoder.encode(randomString); + const hash = await crypto.subtle.digest('SHA-256', encodedData); + const bytes = new Uint8Array(hash); + return Array.from(bytes) + .map((c) => String.fromCharCode(c)) + .join(''); +} +function base64urlencode(str) { + return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} +async function generatePKCEChallenge(verifier) { + const hasCryptoSupport = typeof crypto !== 'undefined' && + typeof crypto.subtle !== 'undefined' && + typeof TextEncoder !== 'undefined'; + if (!hasCryptoSupport) { + console.warn('WebCrypto API is not supported. Code challenge method will default to use plain instead of sha256.'); + return verifier; + } + const hashed = await sha256(verifier); + return base64urlencode(hashed); +} +exports.generatePKCEChallenge = generatePKCEChallenge; +async function getCodeChallengeAndMethod(storage, storageKey, isPasswordRecovery = false) { + const codeVerifier = generatePKCEVerifier(); + let storedCodeVerifier = codeVerifier; + if (isPasswordRecovery) { + storedCodeVerifier += '/PASSWORD_RECOVERY'; + } + await (0, exports.setItemAsync)(storage, `${storageKey}-code-verifier`, storedCodeVerifier); + const codeChallenge = await generatePKCEChallenge(codeVerifier); + const codeChallengeMethod = codeVerifier === codeChallenge ? 'plain' : 's256'; + return [codeChallenge, codeChallengeMethod]; +} +exports.getCodeChallengeAndMethod = getCodeChallengeAndMethod; +/** Parses the API version which is 2YYY-MM-DD. */ +const API_VERSION_REGEX = /^2[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-9]|3[0-1])$/i; +function parseResponseAPIVersion(response) { + const apiVersion = response.headers.get(constants_1.API_VERSION_HEADER_NAME); + if (!apiVersion) { + return null; + } + if (!apiVersion.match(API_VERSION_REGEX)) { + return null; + } + try { + const date = new Date(`${apiVersion}T00:00:00.0Z`); + return date; + } + catch (e) { + return null; + } +} +exports.parseResponseAPIVersion = parseResponseAPIVersion; +//# sourceMappingURL=helpers.js.map + +/***/ }), + +/***/ 1479: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.memoryLocalStorageAdapter = exports.localStorageAdapter = void 0; +const helpers_1 = __nccwpck_require__(4044); +/** + * Provides safe access to the globalThis.localStorage property. + */ +exports.localStorageAdapter = { + getItem: (key) => { + if (!(0, helpers_1.supportsLocalStorage)()) { + return null; + } + return globalThis.localStorage.getItem(key); + }, + setItem: (key, value) => { + if (!(0, helpers_1.supportsLocalStorage)()) { + return; + } + globalThis.localStorage.setItem(key, value); + }, + removeItem: (key) => { + if (!(0, helpers_1.supportsLocalStorage)()) { + return; + } + globalThis.localStorage.removeItem(key); + }, +}; +/** + * Returns a localStorage-like object that stores the key-value pairs in + * memory. + */ +function memoryLocalStorageAdapter(store = {}) { + return { + getItem: (key) => { + return store[key] || null; + }, + setItem: (key, value) => { + store[key] = value; + }, + removeItem: (key) => { + delete store[key]; + }, + }; +} +exports.memoryLocalStorageAdapter = memoryLocalStorageAdapter; +//# sourceMappingURL=local-storage.js.map + +/***/ }), + +/***/ 163: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.processLock = exports.navigatorLock = exports.ProcessLockAcquireTimeoutError = exports.NavigatorLockAcquireTimeoutError = exports.LockAcquireTimeoutError = exports.internals = void 0; +const helpers_1 = __nccwpck_require__(4044); +/** + * @experimental + */ +exports.internals = { + /** + * @experimental + */ + debug: !!(globalThis && + (0, helpers_1.supportsLocalStorage)() && + globalThis.localStorage && + globalThis.localStorage.getItem('supabase.gotrue-js.locks.debug') === 'true'), +}; +/** + * An error thrown when a lock cannot be acquired after some amount of time. + * + * Use the {@link #isAcquireTimeout} property instead of checking with `instanceof`. + */ +class LockAcquireTimeoutError extends Error { + constructor(message) { + super(message); + this.isAcquireTimeout = true; + } +} +exports.LockAcquireTimeoutError = LockAcquireTimeoutError; +class NavigatorLockAcquireTimeoutError extends LockAcquireTimeoutError { +} +exports.NavigatorLockAcquireTimeoutError = NavigatorLockAcquireTimeoutError; +class ProcessLockAcquireTimeoutError extends LockAcquireTimeoutError { +} +exports.ProcessLockAcquireTimeoutError = ProcessLockAcquireTimeoutError; +/** + * Implements a global exclusive lock using the Navigator LockManager API. It + * is available on all browsers released after 2022-03-15 with Safari being the + * last one to release support. If the API is not available, this function will + * throw. Make sure you check availablility before configuring {@link + * GoTrueClient}. + * + * You can turn on debugging by setting the `supabase.gotrue-js.locks.debug` + * local storage item to `true`. + * + * Internals: + * + * Since the LockManager API does not preserve stack traces for the async + * function passed in the `request` method, a trick is used where acquiring the + * lock releases a previously started promise to run the operation in the `fn` + * function. The lock waits for that promise to finish (with or without error), + * while the function will finally wait for the result anyway. + * + * @param name Name of the lock to be acquired. + * @param acquireTimeout If negative, no timeout. If 0 an error is thrown if + * the lock can't be acquired without waiting. If positive, the lock acquire + * will time out after so many milliseconds. An error is + * a timeout if it has `isAcquireTimeout` set to true. + * @param fn The operation to run once the lock is acquired. + */ +async function navigatorLock(name, acquireTimeout, fn) { + if (exports.internals.debug) { + console.log('@supabase/gotrue-js: navigatorLock: acquire lock', name, acquireTimeout); + } + const abortController = new globalThis.AbortController(); + if (acquireTimeout > 0) { + setTimeout(() => { + abortController.abort(); + if (exports.internals.debug) { + console.log('@supabase/gotrue-js: navigatorLock acquire timed out', name); + } + }, acquireTimeout); + } + // MDN article: https://developer.mozilla.org/en-US/docs/Web/API/LockManager/request + // Wrapping navigator.locks.request() with a plain Promise is done as some + // libraries like zone.js patch the Promise object to track the execution + // context. However, it appears that most browsers use an internal promise + // implementation when using the navigator.locks.request() API causing them + // to lose context and emit confusing log messages or break certain features. + // This wrapping is believed to help zone.js track the execution context + // better. + return await Promise.resolve().then(() => globalThis.navigator.locks.request(name, acquireTimeout === 0 + ? { + mode: 'exclusive', + ifAvailable: true, + } + : { + mode: 'exclusive', + signal: abortController.signal, + }, async (lock) => { + if (lock) { + if (exports.internals.debug) { + console.log('@supabase/gotrue-js: navigatorLock: acquired', name, lock.name); + } + try { + return await fn(); + } + finally { + if (exports.internals.debug) { + console.log('@supabase/gotrue-js: navigatorLock: released', name, lock.name); + } + } + } + else { + if (acquireTimeout === 0) { + if (exports.internals.debug) { + console.log('@supabase/gotrue-js: navigatorLock: not immediately available', name); + } + throw new NavigatorLockAcquireTimeoutError(`Acquiring an exclusive Navigator LockManager lock "${name}" immediately failed`); + } + else { + if (exports.internals.debug) { + try { + const result = await globalThis.navigator.locks.query(); + console.log('@supabase/gotrue-js: Navigator LockManager state', JSON.stringify(result, null, ' ')); + } + catch (e) { + console.warn('@supabase/gotrue-js: Error when querying Navigator LockManager state', e); + } + } + // Browser is not following the Navigator LockManager spec, it + // returned a null lock when we didn't use ifAvailable. So we can + // pretend the lock is acquired in the name of backward compatibility + // and user experience and just run the function. + console.warn('@supabase/gotrue-js: Navigator LockManager returned a null lock when using #request without ifAvailable set to true, it appears this browser is not following the LockManager spec https://developer.mozilla.org/en-US/docs/Web/API/LockManager/request'); + return await fn(); + } + } + })); +} +exports.navigatorLock = navigatorLock; +const PROCESS_LOCKS = {}; +/** + * Implements a global exclusive lock that works only in the current process. + * Useful for environments like React Native or other non-browser + * single-process (i.e. no concept of "tabs") environments. + * + * Use {@link #navigatorLock} in browser environments. + * + * @param name Name of the lock to be acquired. + * @param acquireTimeout If negative, no timeout. If 0 an error is thrown if + * the lock can't be acquired without waiting. If positive, the lock acquire + * will time out after so many milliseconds. An error is + * a timeout if it has `isAcquireTimeout` set to true. + * @param fn The operation to run once the lock is acquired. + */ +async function processLock(name, acquireTimeout, fn) { + var _a; + const previousOperation = (_a = PROCESS_LOCKS[name]) !== null && _a !== void 0 ? _a : Promise.resolve(); + const currentOperation = Promise.race([ + previousOperation.catch(() => { + // ignore error of previous operation that we're waiting to finish + return null; + }), + acquireTimeout >= 0 + ? new Promise((_, reject) => { + setTimeout(() => { + reject(new ProcessLockAcquireTimeoutError(`Acquring process lock with name "${name}" timed out`)); + }, acquireTimeout); + }) + : null, + ].filter((x) => x)) + .catch((e) => { + if (e && e.isAcquireTimeout) { + throw e; + } + return null; + }) + .then(async () => { + // previous operations finished and we didn't get a race on the acquire + // timeout, so the current operation can finally start + return await fn(); + }); + PROCESS_LOCKS[name] = currentOperation.catch(async (e) => { + if (e && e.isAcquireTimeout) { + // if the current operation timed out, it doesn't mean that the previous + // operation finished, so we need contnue waiting for it to finish + await previousOperation; + return null; + } + throw e; + }); + // finally wait for the current operation to finish successfully, with an + // error or with an acquire timeout error + return await currentOperation; +} +exports.processLock = processLock; +//# sourceMappingURL=locks.js.map + +/***/ }), + +/***/ 1897: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.polyfillGlobalThis = void 0; +/** + * https://mathiasbynens.be/notes/globalthis + */ +function polyfillGlobalThis() { + if (typeof globalThis === 'object') + return; + try { + Object.defineProperty(Object.prototype, '__magic__', { + get: function () { + return this; + }, + configurable: true, + }); + // @ts-expect-error 'Allow access to magic' + __magic__.globalThis = __magic__; + // @ts-expect-error 'Allow access to magic' + delete Object.prototype.__magic__; + } + catch (e) { + if (typeof self !== 'undefined') { + // @ts-expect-error 'Allow access to globals' + self.globalThis = self; + } + } +} +exports.polyfillGlobalThis = polyfillGlobalThis; +//# sourceMappingURL=polyfills.js.map + +/***/ }), + +/***/ 1852: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +//# sourceMappingURL=types.js.map + +/***/ }), + +/***/ 6677: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.version = void 0; +exports.version = '2.67.3'; +//# sourceMappingURL=version.js.map + +/***/ }), + +/***/ 9904: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.FunctionsClient = void 0; +const helper_1 = __nccwpck_require__(618); +const types_1 = __nccwpck_require__(3136); +class FunctionsClient { + constructor(url, { headers = {}, customFetch, region = types_1.FunctionRegion.Any, } = {}) { + this.url = url; + this.headers = headers; + this.region = region; + this.fetch = (0, helper_1.resolveFetch)(customFetch); + } + /** + * Updates the authorization header + * @param token - the new jwt token sent in the authorisation header + */ + setAuth(token) { + this.headers.Authorization = `Bearer ${token}`; + } + /** + * Invokes a function + * @param functionName - The name of the Function to invoke. + * @param options - Options for invoking the Function. + */ + invoke(functionName, options = {}) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + try { + const { headers, method, body: functionArgs } = options; + let _headers = {}; + let { region } = options; + if (!region) { + region = this.region; + } + if (region && region !== 'any') { + _headers['x-region'] = region; + } + let body; + if (functionArgs && + ((headers && !Object.prototype.hasOwnProperty.call(headers, 'Content-Type')) || !headers)) { + if ((typeof Blob !== 'undefined' && functionArgs instanceof Blob) || + functionArgs instanceof ArrayBuffer) { + // will work for File as File inherits Blob + // also works for ArrayBuffer as it is the same underlying structure as a Blob + _headers['Content-Type'] = 'application/octet-stream'; + body = functionArgs; + } + else if (typeof functionArgs === 'string') { + // plain string + _headers['Content-Type'] = 'text/plain'; + body = functionArgs; + } + else if (typeof FormData !== 'undefined' && functionArgs instanceof FormData) { + // don't set content-type headers + // Request will automatically add the right boundary value + body = functionArgs; + } + else { + // default, assume this is JSON + _headers['Content-Type'] = 'application/json'; + body = JSON.stringify(functionArgs); + } + } + const response = yield this.fetch(`${this.url}/${functionName}`, { + method: method || 'POST', + // headers priority is (high to low): + // 1. invoke-level headers + // 2. client-level headers + // 3. default Content-Type header + headers: Object.assign(Object.assign(Object.assign({}, _headers), this.headers), headers), + body, + }).catch((fetchError) => { + throw new types_1.FunctionsFetchError(fetchError); + }); + const isRelayError = response.headers.get('x-relay-error'); + if (isRelayError && isRelayError === 'true') { + throw new types_1.FunctionsRelayError(response); + } + if (!response.ok) { + throw new types_1.FunctionsHttpError(response); + } + let responseType = ((_a = response.headers.get('Content-Type')) !== null && _a !== void 0 ? _a : 'text/plain').split(';')[0].trim(); + let data; + if (responseType === 'application/json') { + data = yield response.json(); + } + else if (responseType === 'application/octet-stream') { + data = yield response.blob(); + } + else if (responseType === 'text/event-stream') { + data = response; + } + else if (responseType === 'multipart/form-data') { + data = yield response.formData(); + } + else { + // default to text + data = yield response.text(); + } + return { data, error: null }; + } + catch (error) { + return { data: null, error }; + } + }); + } +} +exports.FunctionsClient = FunctionsClient; +//# sourceMappingURL=FunctionsClient.js.map + +/***/ }), + +/***/ 618: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.resolveFetch = void 0; +const resolveFetch = (customFetch) => { + let _fetch; + if (customFetch) { + _fetch = customFetch; + } + else if (typeof fetch === 'undefined') { + _fetch = (...args) => Promise.resolve().then(() => __importStar(__nccwpck_require__(2668))).then(({ default: fetch }) => fetch(...args)); + } + else { + _fetch = fetch; + } + return (...args) => _fetch(...args); +}; +exports.resolveFetch = resolveFetch; +//# sourceMappingURL=helper.js.map + +/***/ }), + +/***/ 8519: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.FunctionRegion = exports.FunctionsRelayError = exports.FunctionsHttpError = exports.FunctionsFetchError = exports.FunctionsError = exports.FunctionsClient = void 0; +var FunctionsClient_1 = __nccwpck_require__(9904); +Object.defineProperty(exports, "FunctionsClient", ({ enumerable: true, get: function () { return FunctionsClient_1.FunctionsClient; } })); +var types_1 = __nccwpck_require__(3136); +Object.defineProperty(exports, "FunctionsError", ({ enumerable: true, get: function () { return types_1.FunctionsError; } })); +Object.defineProperty(exports, "FunctionsFetchError", ({ enumerable: true, get: function () { return types_1.FunctionsFetchError; } })); +Object.defineProperty(exports, "FunctionsHttpError", ({ enumerable: true, get: function () { return types_1.FunctionsHttpError; } })); +Object.defineProperty(exports, "FunctionsRelayError", ({ enumerable: true, get: function () { return types_1.FunctionsRelayError; } })); +Object.defineProperty(exports, "FunctionRegion", ({ enumerable: true, get: function () { return types_1.FunctionRegion; } })); +//# sourceMappingURL=index.js.map + +/***/ }), + +/***/ 3136: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.FunctionRegion = exports.FunctionsHttpError = exports.FunctionsRelayError = exports.FunctionsFetchError = exports.FunctionsError = void 0; +class FunctionsError extends Error { + constructor(message, name = 'FunctionsError', context) { + super(message); + this.name = name; + this.context = context; + } +} +exports.FunctionsError = FunctionsError; +class FunctionsFetchError extends FunctionsError { + constructor(context) { + super('Failed to send a request to the Edge Function', 'FunctionsFetchError', context); + } +} +exports.FunctionsFetchError = FunctionsFetchError; +class FunctionsRelayError extends FunctionsError { + constructor(context) { + super('Relay Error invoking the Edge Function', 'FunctionsRelayError', context); + } +} +exports.FunctionsRelayError = FunctionsRelayError; +class FunctionsHttpError extends FunctionsError { + constructor(context) { + super('Edge Function returned a non-2xx status code', 'FunctionsHttpError', context); + } +} +exports.FunctionsHttpError = FunctionsHttpError; +// Define the enum for the 'region' property +var FunctionRegion; +(function (FunctionRegion) { + FunctionRegion["Any"] = "any"; + FunctionRegion["ApNortheast1"] = "ap-northeast-1"; + FunctionRegion["ApNortheast2"] = "ap-northeast-2"; + FunctionRegion["ApSouth1"] = "ap-south-1"; + FunctionRegion["ApSoutheast1"] = "ap-southeast-1"; + FunctionRegion["ApSoutheast2"] = "ap-southeast-2"; + FunctionRegion["CaCentral1"] = "ca-central-1"; + FunctionRegion["EuCentral1"] = "eu-central-1"; + FunctionRegion["EuWest1"] = "eu-west-1"; + FunctionRegion["EuWest2"] = "eu-west-2"; + FunctionRegion["EuWest3"] = "eu-west-3"; + FunctionRegion["SaEast1"] = "sa-east-1"; + FunctionRegion["UsEast1"] = "us-east-1"; + FunctionRegion["UsWest1"] = "us-west-1"; + FunctionRegion["UsWest2"] = "us-west-2"; +})(FunctionRegion = exports.FunctionRegion || (exports.FunctionRegion = {})); +//# sourceMappingURL=types.js.map + +/***/ }), + +/***/ 2668: +/***/ ((module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ value: true })); + +function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } + +var Stream = _interopDefault(__nccwpck_require__(2781)); +var http = _interopDefault(__nccwpck_require__(3685)); +var Url = _interopDefault(__nccwpck_require__(7310)); +var whatwgUrl = _interopDefault(__nccwpck_require__(8665)); +var https = _interopDefault(__nccwpck_require__(5687)); +var zlib = _interopDefault(__nccwpck_require__(9796)); + +// Based on https://github.com/tmpvar/jsdom/blob/aa85b2abf07766ff7bf5c1f6daafb3726f2f2db5/lib/jsdom/living/blob.js + +// fix for "Readable" isn't a named export issue +const Readable = Stream.Readable; + +const BUFFER = Symbol('buffer'); +const TYPE = Symbol('type'); + +class Blob { + constructor() { + this[TYPE] = ''; + + const blobParts = arguments[0]; + const options = arguments[1]; + + const buffers = []; + let size = 0; + + if (blobParts) { + const a = blobParts; + const length = Number(a.length); + for (let i = 0; i < length; i++) { + const element = a[i]; + let buffer; + if (element instanceof Buffer) { + buffer = element; + } else if (ArrayBuffer.isView(element)) { + buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength); + } else if (element instanceof ArrayBuffer) { + buffer = Buffer.from(element); + } else if (element instanceof Blob) { + buffer = element[BUFFER]; + } else { + buffer = Buffer.from(typeof element === 'string' ? element : String(element)); + } + size += buffer.length; + buffers.push(buffer); + } + } + + this[BUFFER] = Buffer.concat(buffers); + + let type = options && options.type !== undefined && String(options.type).toLowerCase(); + if (type && !/[^\u0020-\u007E]/.test(type)) { + this[TYPE] = type; + } + } + get size() { + return this[BUFFER].length; + } + get type() { + return this[TYPE]; + } + text() { + return Promise.resolve(this[BUFFER].toString()); + } + arrayBuffer() { + const buf = this[BUFFER]; + const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + return Promise.resolve(ab); + } + stream() { + const readable = new Readable(); + readable._read = function () {}; + readable.push(this[BUFFER]); + readable.push(null); + return readable; + } + toString() { + return '[object Blob]'; + } + slice() { + const size = this.size; + + const start = arguments[0]; + const end = arguments[1]; + let relativeStart, relativeEnd; + if (start === undefined) { + relativeStart = 0; + } else if (start < 0) { + relativeStart = Math.max(size + start, 0); + } else { + relativeStart = Math.min(start, size); + } + if (end === undefined) { + relativeEnd = size; + } else if (end < 0) { + relativeEnd = Math.max(size + end, 0); + } else { + relativeEnd = Math.min(end, size); + } + const span = Math.max(relativeEnd - relativeStart, 0); + + const buffer = this[BUFFER]; + const slicedBuffer = buffer.slice(relativeStart, relativeStart + span); + const blob = new Blob([], { type: arguments[2] }); + blob[BUFFER] = slicedBuffer; + return blob; + } +} + +Object.defineProperties(Blob.prototype, { + size: { enumerable: true }, + type: { enumerable: true }, + slice: { enumerable: true } +}); + +Object.defineProperty(Blob.prototype, Symbol.toStringTag, { + value: 'Blob', + writable: false, + enumerable: false, + configurable: true +}); + +/** + * fetch-error.js + * + * FetchError interface for operational errors + */ + +/** + * Create FetchError instance + * + * @param String message Error message for human + * @param String type Error type for machine + * @param String systemError For Node.js system error + * @return FetchError + */ +function FetchError(message, type, systemError) { + Error.call(this, message); + + this.message = message; + this.type = type; + + // when err.type is `system`, err.code contains system error code + if (systemError) { + this.code = this.errno = systemError.code; + } + + // hide custom error implementation details from end-users + Error.captureStackTrace(this, this.constructor); +} + +FetchError.prototype = Object.create(Error.prototype); +FetchError.prototype.constructor = FetchError; +FetchError.prototype.name = 'FetchError'; + +let convert; + +const INTERNALS = Symbol('Body internals'); + +// fix an issue where "PassThrough" isn't a named export for node <10 +const PassThrough = Stream.PassThrough; + +/** + * Body mixin + * + * Ref: https://fetch.spec.whatwg.org/#body + * + * @param Stream body Readable stream + * @param Object opts Response options + * @return Void + */ +function Body(body) { + var _this = this; + + var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, + _ref$size = _ref.size; + + let size = _ref$size === undefined ? 0 : _ref$size; + var _ref$timeout = _ref.timeout; + let timeout = _ref$timeout === undefined ? 0 : _ref$timeout; + + if (body == null) { + // body is undefined or null + body = null; + } else if (isURLSearchParams(body)) { + // body is a URLSearchParams + body = Buffer.from(body.toString()); + } else if (isBlob(body)) ; else if (Buffer.isBuffer(body)) ; else if (Object.prototype.toString.call(body) === '[object ArrayBuffer]') { + // body is ArrayBuffer + body = Buffer.from(body); + } else if (ArrayBuffer.isView(body)) { + // body is ArrayBufferView + body = Buffer.from(body.buffer, body.byteOffset, body.byteLength); + } else if (body instanceof Stream) ; else { + // none of the above + // coerce to string then buffer + body = Buffer.from(String(body)); + } + this[INTERNALS] = { + body, + disturbed: false, + error: null + }; + this.size = size; + this.timeout = timeout; + + if (body instanceof Stream) { + body.on('error', function (err) { + const error = err.name === 'AbortError' ? err : new FetchError(`Invalid response body while trying to fetch ${_this.url}: ${err.message}`, 'system', err); + _this[INTERNALS].error = error; + }); + } +} + +Body.prototype = { + get body() { + return this[INTERNALS].body; + }, + + get bodyUsed() { + return this[INTERNALS].disturbed; + }, + + /** + * Decode response as ArrayBuffer + * + * @return Promise + */ + arrayBuffer() { + return consumeBody.call(this).then(function (buf) { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + }); + }, + + /** + * Return raw response as Blob + * + * @return Promise + */ + blob() { + let ct = this.headers && this.headers.get('content-type') || ''; + return consumeBody.call(this).then(function (buf) { + return Object.assign( + // Prevent copying + new Blob([], { + type: ct.toLowerCase() + }), { + [BUFFER]: buf + }); + }); + }, + + /** + * Decode response as json + * + * @return Promise + */ + json() { + var _this2 = this; + + return consumeBody.call(this).then(function (buffer) { + try { + return JSON.parse(buffer.toString()); + } catch (err) { + return Body.Promise.reject(new FetchError(`invalid json response body at ${_this2.url} reason: ${err.message}`, 'invalid-json')); + } + }); + }, + + /** + * Decode response as text + * + * @return Promise + */ + text() { + return consumeBody.call(this).then(function (buffer) { + return buffer.toString(); + }); + }, + + /** + * Decode response as buffer (non-spec api) + * + * @return Promise + */ + buffer() { + return consumeBody.call(this); + }, + + /** + * Decode response as text, while automatically detecting the encoding and + * trying to decode to UTF-8 (non-spec api) + * + * @return Promise + */ + textConverted() { + var _this3 = this; + + return consumeBody.call(this).then(function (buffer) { + return convertBody(buffer, _this3.headers); + }); + } +}; + +// In browsers, all properties are enumerable. +Object.defineProperties(Body.prototype, { + body: { enumerable: true }, + bodyUsed: { enumerable: true }, + arrayBuffer: { enumerable: true }, + blob: { enumerable: true }, + json: { enumerable: true }, + text: { enumerable: true } +}); + +Body.mixIn = function (proto) { + for (const name of Object.getOwnPropertyNames(Body.prototype)) { + // istanbul ignore else: future proof + if (!(name in proto)) { + const desc = Object.getOwnPropertyDescriptor(Body.prototype, name); + Object.defineProperty(proto, name, desc); + } + } +}; + +/** + * Consume and convert an entire Body to a Buffer. + * + * Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body + * + * @return Promise + */ +function consumeBody() { + var _this4 = this; + + if (this[INTERNALS].disturbed) { + return Body.Promise.reject(new TypeError(`body used already for: ${this.url}`)); + } + + this[INTERNALS].disturbed = true; + + if (this[INTERNALS].error) { + return Body.Promise.reject(this[INTERNALS].error); + } + + let body = this.body; + + // body is null + if (body === null) { + return Body.Promise.resolve(Buffer.alloc(0)); + } + + // body is blob + if (isBlob(body)) { + body = body.stream(); + } + + // body is buffer + if (Buffer.isBuffer(body)) { + return Body.Promise.resolve(body); + } + + // istanbul ignore if: should never happen + if (!(body instanceof Stream)) { + return Body.Promise.resolve(Buffer.alloc(0)); + } + + // body is stream + // get ready to actually consume the body + let accum = []; + let accumBytes = 0; + let abort = false; + + return new Body.Promise(function (resolve, reject) { + let resTimeout; + + // allow timeout on slow response body + if (_this4.timeout) { + resTimeout = setTimeout(function () { + abort = true; + reject(new FetchError(`Response timeout while trying to fetch ${_this4.url} (over ${_this4.timeout}ms)`, 'body-timeout')); + }, _this4.timeout); + } + + // handle stream errors + body.on('error', function (err) { + if (err.name === 'AbortError') { + // if the request was aborted, reject with this Error + abort = true; + reject(err); + } else { + // other errors, such as incorrect content-encoding + reject(new FetchError(`Invalid response body while trying to fetch ${_this4.url}: ${err.message}`, 'system', err)); + } + }); + + body.on('data', function (chunk) { + if (abort || chunk === null) { + return; + } + + if (_this4.size && accumBytes + chunk.length > _this4.size) { + abort = true; + reject(new FetchError(`content size at ${_this4.url} over limit: ${_this4.size}`, 'max-size')); + return; + } + + accumBytes += chunk.length; + accum.push(chunk); + }); + + body.on('end', function () { + if (abort) { + return; + } + + clearTimeout(resTimeout); + + try { + resolve(Buffer.concat(accum, accumBytes)); + } catch (err) { + // handle streams that have accumulated too much data (issue #414) + reject(new FetchError(`Could not create Buffer from response body for ${_this4.url}: ${err.message}`, 'system', err)); + } + }); + }); +} + +/** + * Detect buffer encoding and convert to target encoding + * ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding + * + * @param Buffer buffer Incoming buffer + * @param String encoding Target encoding + * @return String + */ +function convertBody(buffer, headers) { + { + throw new Error('The package `encoding` must be installed to use the textConverted() function'); + } + + const ct = headers.get('content-type'); + let charset = 'utf-8'; + let res, str; + + // header + if (ct) { + res = /charset=([^;]*)/i.exec(ct); + } + + // no charset in content type, peek at response body for at most 1024 bytes + str = buffer.slice(0, 1024).toString(); + + // html5 + if (!res && str) { + res = / 0 && arguments[0] !== undefined ? arguments[0] : undefined; + + this[MAP] = Object.create(null); + + if (init instanceof Headers) { + const rawHeaders = init.raw(); + const headerNames = Object.keys(rawHeaders); + + for (const headerName of headerNames) { + for (const value of rawHeaders[headerName]) { + this.append(headerName, value); + } + } + + return; + } + + // We don't worry about converting prop to ByteString here as append() + // will handle it. + if (init == null) ; else if (typeof init === 'object') { + const method = init[Symbol.iterator]; + if (method != null) { + if (typeof method !== 'function') { + throw new TypeError('Header pairs must be iterable'); + } + + // sequence> + // Note: per spec we have to first exhaust the lists then process them + const pairs = []; + for (const pair of init) { + if (typeof pair !== 'object' || typeof pair[Symbol.iterator] !== 'function') { + throw new TypeError('Each header pair must be iterable'); + } + pairs.push(Array.from(pair)); + } + + for (const pair of pairs) { + if (pair.length !== 2) { + throw new TypeError('Each header pair must be a name/value tuple'); + } + this.append(pair[0], pair[1]); + } + } else { + // record + for (const key of Object.keys(init)) { + const value = init[key]; + this.append(key, value); + } + } + } else { + throw new TypeError('Provided initializer must be an object'); + } + } + + /** + * Return combined header value given name + * + * @param String name Header name + * @return Mixed + */ + get(name) { + name = `${name}`; + validateName(name); + const key = find(this[MAP], name); + if (key === undefined) { + return null; + } + + return this[MAP][key].join(', '); + } + + /** + * Iterate over all headers + * + * @param Function callback Executed for each item with parameters (value, name, thisArg) + * @param Boolean thisArg `this` context for callback function + * @return Void + */ + forEach(callback) { + let thisArg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined; + + let pairs = getHeaders(this); + let i = 0; + while (i < pairs.length) { + var _pairs$i = pairs[i]; + const name = _pairs$i[0], + value = _pairs$i[1]; + + callback.call(thisArg, value, name, this); + pairs = getHeaders(this); + i++; + } + } + + /** + * Overwrite header values given name + * + * @param String name Header name + * @param String value Header value + * @return Void + */ + set(name, value) { + name = `${name}`; + value = `${value}`; + validateName(name); + validateValue(value); + const key = find(this[MAP], name); + this[MAP][key !== undefined ? key : name] = [value]; + } + + /** + * Append a value onto existing header + * + * @param String name Header name + * @param String value Header value + * @return Void + */ + append(name, value) { + name = `${name}`; + value = `${value}`; + validateName(name); + validateValue(value); + const key = find(this[MAP], name); + if (key !== undefined) { + this[MAP][key].push(value); + } else { + this[MAP][name] = [value]; + } + } + + /** + * Check for header name existence + * + * @param String name Header name + * @return Boolean + */ + has(name) { + name = `${name}`; + validateName(name); + return find(this[MAP], name) !== undefined; + } + + /** + * Delete all header values given name + * + * @param String name Header name + * @return Void + */ + delete(name) { + name = `${name}`; + validateName(name); + const key = find(this[MAP], name); + if (key !== undefined) { + delete this[MAP][key]; + } + } + + /** + * Return raw headers (non-spec api) + * + * @return Object + */ + raw() { + return this[MAP]; + } + + /** + * Get an iterator on keys. + * + * @return Iterator + */ + keys() { + return createHeadersIterator(this, 'key'); + } + + /** + * Get an iterator on values. + * + * @return Iterator + */ + values() { + return createHeadersIterator(this, 'value'); + } + + /** + * Get an iterator on entries. + * + * This is the default iterator of the Headers object. + * + * @return Iterator + */ + [Symbol.iterator]() { + return createHeadersIterator(this, 'key+value'); + } +} +Headers.prototype.entries = Headers.prototype[Symbol.iterator]; + +Object.defineProperty(Headers.prototype, Symbol.toStringTag, { + value: 'Headers', + writable: false, + enumerable: false, + configurable: true +}); + +Object.defineProperties(Headers.prototype, { + get: { enumerable: true }, + forEach: { enumerable: true }, + set: { enumerable: true }, + append: { enumerable: true }, + has: { enumerable: true }, + delete: { enumerable: true }, + keys: { enumerable: true }, + values: { enumerable: true }, + entries: { enumerable: true } +}); + +function getHeaders(headers) { + let kind = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'key+value'; + + const keys = Object.keys(headers[MAP]).sort(); + return keys.map(kind === 'key' ? function (k) { + return k.toLowerCase(); + } : kind === 'value' ? function (k) { + return headers[MAP][k].join(', '); + } : function (k) { + return [k.toLowerCase(), headers[MAP][k].join(', ')]; + }); +} + +const INTERNAL = Symbol('internal'); + +function createHeadersIterator(target, kind) { + const iterator = Object.create(HeadersIteratorPrototype); + iterator[INTERNAL] = { + target, + kind, + index: 0 + }; + return iterator; +} + +const HeadersIteratorPrototype = Object.setPrototypeOf({ + next() { + // istanbul ignore if + if (!this || Object.getPrototypeOf(this) !== HeadersIteratorPrototype) { + throw new TypeError('Value of `this` is not a HeadersIterator'); + } + + var _INTERNAL = this[INTERNAL]; + const target = _INTERNAL.target, + kind = _INTERNAL.kind, + index = _INTERNAL.index; + + const values = getHeaders(target, kind); + const len = values.length; + if (index >= len) { + return { + value: undefined, + done: true + }; + } + + this[INTERNAL].index = index + 1; + + return { + value: values[index], + done: false + }; + } +}, Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))); + +Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, { + value: 'HeadersIterator', + writable: false, + enumerable: false, + configurable: true +}); + +/** + * Export the Headers object in a form that Node.js can consume. + * + * @param Headers headers + * @return Object + */ +function exportNodeCompatibleHeaders(headers) { + const obj = Object.assign({ __proto__: null }, headers[MAP]); + + // http.request() only supports string as Host header. This hack makes + // specifying custom Host header possible. + const hostHeaderKey = find(headers[MAP], 'Host'); + if (hostHeaderKey !== undefined) { + obj[hostHeaderKey] = obj[hostHeaderKey][0]; + } + + return obj; +} + +/** + * Create a Headers object from an object of headers, ignoring those that do + * not conform to HTTP grammar productions. + * + * @param Object obj Object of headers + * @return Headers + */ +function createHeadersLenient(obj) { + const headers = new Headers(); + for (const name of Object.keys(obj)) { + if (invalidTokenRegex.test(name)) { + continue; + } + if (Array.isArray(obj[name])) { + for (const val of obj[name]) { + if (invalidHeaderCharRegex.test(val)) { + continue; + } + if (headers[MAP][name] === undefined) { + headers[MAP][name] = [val]; + } else { + headers[MAP][name].push(val); + } + } + } else if (!invalidHeaderCharRegex.test(obj[name])) { + headers[MAP][name] = [obj[name]]; + } + } + return headers; +} + +const INTERNALS$1 = Symbol('Response internals'); + +// fix an issue where "STATUS_CODES" aren't a named export for node <10 +const STATUS_CODES = http.STATUS_CODES; + +/** + * Response class + * + * @param Stream body Readable stream + * @param Object opts Response options + * @return Void + */ +class Response { + constructor() { + let body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + let opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + Body.call(this, body, opts); + + const status = opts.status || 200; + const headers = new Headers(opts.headers); + + if (body != null && !headers.has('Content-Type')) { + const contentType = extractContentType(body); + if (contentType) { + headers.append('Content-Type', contentType); + } + } + + this[INTERNALS$1] = { + url: opts.url, + status, + statusText: opts.statusText || STATUS_CODES[status], + headers, + counter: opts.counter + }; + } + + get url() { + return this[INTERNALS$1].url || ''; + } + + get status() { + return this[INTERNALS$1].status; + } + + /** + * Convenience property representing if the request ended normally + */ + get ok() { + return this[INTERNALS$1].status >= 200 && this[INTERNALS$1].status < 300; + } + + get redirected() { + return this[INTERNALS$1].counter > 0; + } + + get statusText() { + return this[INTERNALS$1].statusText; + } + + get headers() { + return this[INTERNALS$1].headers; + } + + /** + * Clone this response + * + * @return Response + */ + clone() { + return new Response(clone(this), { + url: this.url, + status: this.status, + statusText: this.statusText, + headers: this.headers, + ok: this.ok, + redirected: this.redirected + }); + } +} + +Body.mixIn(Response.prototype); + +Object.defineProperties(Response.prototype, { + url: { enumerable: true }, + status: { enumerable: true }, + ok: { enumerable: true }, + redirected: { enumerable: true }, + statusText: { enumerable: true }, + headers: { enumerable: true }, + clone: { enumerable: true } +}); + +Object.defineProperty(Response.prototype, Symbol.toStringTag, { + value: 'Response', + writable: false, + enumerable: false, + configurable: true +}); + +const INTERNALS$2 = Symbol('Request internals'); +const URL = Url.URL || whatwgUrl.URL; + +// fix an issue where "format", "parse" aren't a named export for node <10 +const parse_url = Url.parse; +const format_url = Url.format; + +/** + * Wrapper around `new URL` to handle arbitrary URLs + * + * @param {string} urlStr + * @return {void} + */ +function parseURL(urlStr) { + /* + Check whether the URL is absolute or not + Scheme: https://tools.ietf.org/html/rfc3986#section-3.1 + Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3 + */ + if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.exec(urlStr)) { + urlStr = new URL(urlStr).toString(); + } + + // Fallback to old implementation for arbitrary URLs + return parse_url(urlStr); +} + +const streamDestructionSupported = 'destroy' in Stream.Readable.prototype; + +/** + * Check if a value is an instance of Request. + * + * @param Mixed input + * @return Boolean + */ +function isRequest(input) { + return typeof input === 'object' && typeof input[INTERNALS$2] === 'object'; +} + +function isAbortSignal(signal) { + const proto = signal && typeof signal === 'object' && Object.getPrototypeOf(signal); + return !!(proto && proto.constructor.name === 'AbortSignal'); +} + +/** + * Request class + * + * @param Mixed input Url or Request instance + * @param Object init Custom options + * @return Void + */ +class Request { + constructor(input) { + let init = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + let parsedURL; + + // normalize input + if (!isRequest(input)) { + if (input && input.href) { + // in order to support Node.js' Url objects; though WHATWG's URL objects + // will fall into this branch also (since their `toString()` will return + // `href` property anyway) + parsedURL = parseURL(input.href); + } else { + // coerce input to a string before attempting to parse + parsedURL = parseURL(`${input}`); + } + input = {}; + } else { + parsedURL = parseURL(input.url); + } + + let method = init.method || input.method || 'GET'; + method = method.toUpperCase(); + + if ((init.body != null || isRequest(input) && input.body !== null) && (method === 'GET' || method === 'HEAD')) { + throw new TypeError('Request with GET/HEAD method cannot have body'); + } + + let inputBody = init.body != null ? init.body : isRequest(input) && input.body !== null ? clone(input) : null; + + Body.call(this, inputBody, { + timeout: init.timeout || input.timeout || 0, + size: init.size || input.size || 0 + }); + + const headers = new Headers(init.headers || input.headers || {}); + + if (inputBody != null && !headers.has('Content-Type')) { + const contentType = extractContentType(inputBody); + if (contentType) { + headers.append('Content-Type', contentType); + } + } + + let signal = isRequest(input) ? input.signal : null; + if ('signal' in init) signal = init.signal; + + if (signal != null && !isAbortSignal(signal)) { + throw new TypeError('Expected signal to be an instanceof AbortSignal'); + } + + this[INTERNALS$2] = { + method, + redirect: init.redirect || input.redirect || 'follow', + headers, + parsedURL, + signal + }; + + // node-fetch-only options + this.follow = init.follow !== undefined ? init.follow : input.follow !== undefined ? input.follow : 20; + this.compress = init.compress !== undefined ? init.compress : input.compress !== undefined ? input.compress : true; + this.counter = init.counter || input.counter || 0; + this.agent = init.agent || input.agent; + } + + get method() { + return this[INTERNALS$2].method; + } + + get url() { + return format_url(this[INTERNALS$2].parsedURL); + } + + get headers() { + return this[INTERNALS$2].headers; + } + + get redirect() { + return this[INTERNALS$2].redirect; + } + + get signal() { + return this[INTERNALS$2].signal; + } + + /** + * Clone this request + * + * @return Request + */ + clone() { + return new Request(this); + } +} + +Body.mixIn(Request.prototype); + +Object.defineProperty(Request.prototype, Symbol.toStringTag, { + value: 'Request', + writable: false, + enumerable: false, + configurable: true +}); + +Object.defineProperties(Request.prototype, { + method: { enumerable: true }, + url: { enumerable: true }, + headers: { enumerable: true }, + redirect: { enumerable: true }, + clone: { enumerable: true }, + signal: { enumerable: true } +}); + +/** + * Convert a Request to Node.js http request options. + * + * @param Request A Request instance + * @return Object The options object to be passed to http.request + */ +function getNodeRequestOptions(request) { + const parsedURL = request[INTERNALS$2].parsedURL; + const headers = new Headers(request[INTERNALS$2].headers); + + // fetch step 1.3 + if (!headers.has('Accept')) { + headers.set('Accept', '*/*'); + } + + // Basic fetch + if (!parsedURL.protocol || !parsedURL.hostname) { + throw new TypeError('Only absolute URLs are supported'); + } + + if (!/^https?:$/.test(parsedURL.protocol)) { + throw new TypeError('Only HTTP(S) protocols are supported'); + } + + if (request.signal && request.body instanceof Stream.Readable && !streamDestructionSupported) { + throw new Error('Cancellation of streamed requests with AbortSignal is not supported in node < 8'); + } + + // HTTP-network-or-cache fetch steps 2.4-2.7 + let contentLengthValue = null; + if (request.body == null && /^(POST|PUT)$/i.test(request.method)) { + contentLengthValue = '0'; + } + if (request.body != null) { + const totalBytes = getTotalBytes(request); + if (typeof totalBytes === 'number') { + contentLengthValue = String(totalBytes); + } + } + if (contentLengthValue) { + headers.set('Content-Length', contentLengthValue); + } + + // HTTP-network-or-cache fetch step 2.11 + if (!headers.has('User-Agent')) { + headers.set('User-Agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)'); + } + + // HTTP-network-or-cache fetch step 2.15 + if (request.compress && !headers.has('Accept-Encoding')) { + headers.set('Accept-Encoding', 'gzip,deflate'); + } + + let agent = request.agent; + if (typeof agent === 'function') { + agent = agent(parsedURL); + } + + if (!headers.has('Connection') && !agent) { + headers.set('Connection', 'close'); + } + + // HTTP-network fetch step 4.2 + // chunked encoding is handled by Node.js + + return Object.assign({}, parsedURL, { + method: request.method, + headers: exportNodeCompatibleHeaders(headers), + agent + }); +} + +/** + * abort-error.js + * + * AbortError interface for cancelled requests + */ + +/** + * Create AbortError instance + * + * @param String message Error message for human + * @return AbortError + */ +function AbortError(message) { + Error.call(this, message); + + this.type = 'aborted'; + this.message = message; + + // hide custom error implementation details from end-users + Error.captureStackTrace(this, this.constructor); +} + +AbortError.prototype = Object.create(Error.prototype); +AbortError.prototype.constructor = AbortError; +AbortError.prototype.name = 'AbortError'; + +const URL$1 = Url.URL || whatwgUrl.URL; + +// fix an issue where "PassThrough", "resolve" aren't a named export for node <10 +const PassThrough$1 = Stream.PassThrough; + +const isDomainOrSubdomain = function isDomainOrSubdomain(destination, original) { + const orig = new URL$1(original).hostname; + const dest = new URL$1(destination).hostname; + + return orig === dest || orig[orig.length - dest.length - 1] === '.' && orig.endsWith(dest); +}; + +/** + * isSameProtocol reports whether the two provided URLs use the same protocol. + * + * Both domains must already be in canonical form. + * @param {string|URL} original + * @param {string|URL} destination + */ +const isSameProtocol = function isSameProtocol(destination, original) { + const orig = new URL$1(original).protocol; + const dest = new URL$1(destination).protocol; + + return orig === dest; +}; + +/** + * Fetch function + * + * @param Mixed url Absolute url or Request instance + * @param Object opts Fetch options + * @return Promise + */ +function fetch(url, opts) { + + // allow custom promise + if (!fetch.Promise) { + throw new Error('native promise missing, set fetch.Promise to your favorite alternative'); + } + + Body.Promise = fetch.Promise; + + // wrap http.request into fetch + return new fetch.Promise(function (resolve, reject) { + // build request object + const request = new Request(url, opts); + const options = getNodeRequestOptions(request); + + const send = (options.protocol === 'https:' ? https : http).request; + const signal = request.signal; + + let response = null; + + const abort = function abort() { + let error = new AbortError('The user aborted a request.'); + reject(error); + if (request.body && request.body instanceof Stream.Readable) { + destroyStream(request.body, error); + } + if (!response || !response.body) return; + response.body.emit('error', error); + }; + + if (signal && signal.aborted) { + abort(); + return; + } + + const abortAndFinalize = function abortAndFinalize() { + abort(); + finalize(); + }; + + // send request + const req = send(options); + let reqTimeout; + + if (signal) { + signal.addEventListener('abort', abortAndFinalize); + } + + function finalize() { + req.abort(); + if (signal) signal.removeEventListener('abort', abortAndFinalize); + clearTimeout(reqTimeout); + } + + if (request.timeout) { + req.once('socket', function (socket) { + reqTimeout = setTimeout(function () { + reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout')); + finalize(); + }, request.timeout); + }); + } + + req.on('error', function (err) { + reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); + + if (response && response.body) { + destroyStream(response.body, err); + } + + finalize(); + }); + + fixResponseChunkedTransferBadEnding(req, function (err) { + if (signal && signal.aborted) { + return; + } + + if (response && response.body) { + destroyStream(response.body, err); + } + }); + + /* c8 ignore next 18 */ + if (parseInt(process.version.substring(1)) < 14) { + // Before Node.js 14, pipeline() does not fully support async iterators and does not always + // properly handle when the socket close/end events are out of order. + req.on('socket', function (s) { + s.addListener('close', function (hadError) { + // if a data listener is still present we didn't end cleanly + const hasDataListener = s.listenerCount('data') > 0; + + // if end happened before close but the socket didn't emit an error, do it now + if (response && hasDataListener && !hadError && !(signal && signal.aborted)) { + const err = new Error('Premature close'); + err.code = 'ERR_STREAM_PREMATURE_CLOSE'; + response.body.emit('error', err); + } + }); + }); + } + + req.on('response', function (res) { + clearTimeout(reqTimeout); + + const headers = createHeadersLenient(res.headers); + + // HTTP fetch step 5 + if (fetch.isRedirect(res.statusCode)) { + // HTTP fetch step 5.2 + const location = headers.get('Location'); + + // HTTP fetch step 5.3 + let locationURL = null; + try { + locationURL = location === null ? null : new URL$1(location, request.url).toString(); + } catch (err) { + // error here can only be invalid URL in Location: header + // do not throw when options.redirect == manual + // let the user extract the errorneous redirect URL + if (request.redirect !== 'manual') { + reject(new FetchError(`uri requested responds with an invalid redirect URL: ${location}`, 'invalid-redirect')); + finalize(); + return; + } + } + + // HTTP fetch step 5.5 + switch (request.redirect) { + case 'error': + reject(new FetchError(`uri requested responds with a redirect, redirect mode is set to error: ${request.url}`, 'no-redirect')); + finalize(); + return; + case 'manual': + // node-fetch-specific step: make manual redirect a bit easier to use by setting the Location header value to the resolved URL. + if (locationURL !== null) { + // handle corrupted header + try { + headers.set('Location', locationURL); + } catch (err) { + // istanbul ignore next: nodejs server prevent invalid response headers, we can't test this through normal request + reject(err); + } + } + break; + case 'follow': + // HTTP-redirect fetch step 2 + if (locationURL === null) { + break; + } + + // HTTP-redirect fetch step 5 + if (request.counter >= request.follow) { + reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect')); + finalize(); + return; + } + + // HTTP-redirect fetch step 6 (counter increment) + // Create a new Request object. + const requestOpts = { + headers: new Headers(request.headers), + follow: request.follow, + counter: request.counter + 1, + agent: request.agent, + compress: request.compress, + method: request.method, + body: request.body, + signal: request.signal, + timeout: request.timeout, + size: request.size + }; + + if (!isDomainOrSubdomain(request.url, locationURL) || !isSameProtocol(request.url, locationURL)) { + for (const name of ['authorization', 'www-authenticate', 'cookie', 'cookie2']) { + requestOpts.headers.delete(name); + } + } + + // HTTP-redirect fetch step 9 + if (res.statusCode !== 303 && request.body && getTotalBytes(request) === null) { + reject(new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect')); + finalize(); + return; + } + + // HTTP-redirect fetch step 11 + if (res.statusCode === 303 || (res.statusCode === 301 || res.statusCode === 302) && request.method === 'POST') { + requestOpts.method = 'GET'; + requestOpts.body = undefined; + requestOpts.headers.delete('content-length'); + } + + // HTTP-redirect fetch step 15 + resolve(fetch(new Request(locationURL, requestOpts))); + finalize(); + return; + } + } + + // prepare response + res.once('end', function () { + if (signal) signal.removeEventListener('abort', abortAndFinalize); + }); + let body = res.pipe(new PassThrough$1()); + + const response_options = { + url: request.url, + status: res.statusCode, + statusText: res.statusMessage, + headers: headers, + size: request.size, + timeout: request.timeout, + counter: request.counter + }; + + // HTTP-network fetch step 12.1.1.3 + const codings = headers.get('Content-Encoding'); + + // HTTP-network fetch step 12.1.1.4: handle content codings + + // in following scenarios we ignore compression support + // 1. compression support is disabled + // 2. HEAD request + // 3. no Content-Encoding header + // 4. no content response (204) + // 5. content not modified response (304) + if (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) { + response = new Response(body, response_options); + resolve(response); + return; + } + + // For Node v6+ + // Be less strict when decoding compressed responses, since sometimes + // servers send slightly invalid responses that are still accepted + // by common browsers. + // Always using Z_SYNC_FLUSH is what cURL does. + const zlibOptions = { + flush: zlib.Z_SYNC_FLUSH, + finishFlush: zlib.Z_SYNC_FLUSH + }; + + // for gzip + if (codings == 'gzip' || codings == 'x-gzip') { + body = body.pipe(zlib.createGunzip(zlibOptions)); + response = new Response(body, response_options); + resolve(response); + return; + } + + // for deflate + if (codings == 'deflate' || codings == 'x-deflate') { + // handle the infamous raw deflate response from old servers + // a hack for old IIS and Apache servers + const raw = res.pipe(new PassThrough$1()); + raw.once('data', function (chunk) { + // see http://stackoverflow.com/questions/37519828 + if ((chunk[0] & 0x0F) === 0x08) { + body = body.pipe(zlib.createInflate()); + } else { + body = body.pipe(zlib.createInflateRaw()); + } + response = new Response(body, response_options); + resolve(response); + }); + raw.on('end', function () { + // some old IIS servers return zero-length OK deflate responses, so 'data' is never emitted. + if (!response) { + response = new Response(body, response_options); + resolve(response); + } + }); + return; + } + + // for br + if (codings == 'br' && typeof zlib.createBrotliDecompress === 'function') { + body = body.pipe(zlib.createBrotliDecompress()); + response = new Response(body, response_options); + resolve(response); + return; + } + + // otherwise, use response as-is + response = new Response(body, response_options); + resolve(response); + }); + + writeToStream(req, request); + }); +} +function fixResponseChunkedTransferBadEnding(request, errorCallback) { + let socket; + + request.on('socket', function (s) { + socket = s; + }); + + request.on('response', function (response) { + const headers = response.headers; + + if (headers['transfer-encoding'] === 'chunked' && !headers['content-length']) { + response.once('close', function (hadError) { + // tests for socket presence, as in some situations the + // the 'socket' event is not triggered for the request + // (happens in deno), avoids `TypeError` + // if a data listener is still present we didn't end cleanly + const hasDataListener = socket && socket.listenerCount('data') > 0; + + if (hasDataListener && !hadError) { + const err = new Error('Premature close'); + err.code = 'ERR_STREAM_PREMATURE_CLOSE'; + errorCallback(err); + } + }); + } + }); +} + +function destroyStream(stream, err) { + if (stream.destroy) { + stream.destroy(err); + } else { + // node < 8 + stream.emit('error', err); + stream.end(); + } +} + +/** + * Redirect code matching + * + * @param Number code Status code + * @return Boolean + */ +fetch.isRedirect = function (code) { + return code === 301 || code === 302 || code === 303 || code === 307 || code === 308; +}; + +// expose Promise +fetch.Promise = global.Promise; + +module.exports = exports = fetch; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports["default"] = exports; +exports.Headers = Headers; +exports.Request = Request; +exports.Response = Response; +exports.FetchError = FetchError; + + +/***/ }), + +/***/ 1049: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +// @ts-ignore +const node_fetch_1 = __importDefault(__nccwpck_require__(2668)); +const PostgrestError_1 = __importDefault(__nccwpck_require__(7317)); +class PostgrestBuilder { + constructor(builder) { + this.shouldThrowOnError = false; + this.method = builder.method; + this.url = builder.url; + this.headers = builder.headers; + this.schema = builder.schema; + this.body = builder.body; + this.shouldThrowOnError = builder.shouldThrowOnError; + this.signal = builder.signal; + this.isMaybeSingle = builder.isMaybeSingle; + if (builder.fetch) { + this.fetch = builder.fetch; + } + else if (typeof fetch === 'undefined') { + this.fetch = node_fetch_1.default; + } + else { + this.fetch = fetch; + } + } + /** + * If there's an error with the query, throwOnError will reject the promise by + * throwing the error instead of returning it as part of a successful response. + * + * {@link https://github.com/supabase/supabase-js/issues/92} + */ + throwOnError() { + this.shouldThrowOnError = true; + return this; + } + /** + * Set an HTTP header for the request. + */ + setHeader(name, value) { + this.headers = Object.assign({}, this.headers); + this.headers[name] = value; + return this; + } + then(onfulfilled, onrejected) { + // https://postgrest.org/en/stable/api.html#switching-schemas + if (this.schema === undefined) { + // skip + } + else if (['GET', 'HEAD'].includes(this.method)) { + this.headers['Accept-Profile'] = this.schema; + } + else { + this.headers['Content-Profile'] = this.schema; + } + if (this.method !== 'GET' && this.method !== 'HEAD') { + this.headers['Content-Type'] = 'application/json'; + } + // NOTE: Invoke w/o `this` to avoid illegal invocation error. + // https://github.com/supabase/postgrest-js/pull/247 + const _fetch = this.fetch; + let res = _fetch(this.url.toString(), { + method: this.method, + headers: this.headers, + body: JSON.stringify(this.body), + signal: this.signal, + }).then(async (res) => { + var _a, _b, _c; + let error = null; + let data = null; + let count = null; + let status = res.status; + let statusText = res.statusText; + if (res.ok) { + if (this.method !== 'HEAD') { + const body = await res.text(); + if (body === '') { + // Prefer: return=minimal + } + else if (this.headers['Accept'] === 'text/csv') { + data = body; + } + else if (this.headers['Accept'] && + this.headers['Accept'].includes('application/vnd.pgrst.plan+text')) { + data = body; + } + else { + data = JSON.parse(body); + } + } + const countHeader = (_a = this.headers['Prefer']) === null || _a === void 0 ? void 0 : _a.match(/count=(exact|planned|estimated)/); + const contentRange = (_b = res.headers.get('content-range')) === null || _b === void 0 ? void 0 : _b.split('/'); + if (countHeader && contentRange && contentRange.length > 1) { + count = parseInt(contentRange[1]); + } + // Temporary partial fix for https://github.com/supabase/postgrest-js/issues/361 + // Issue persists e.g. for `.insert([...]).select().maybeSingle()` + if (this.isMaybeSingle && this.method === 'GET' && Array.isArray(data)) { + if (data.length > 1) { + error = { + // https://github.com/PostgREST/postgrest/blob/a867d79c42419af16c18c3fb019eba8df992626f/src/PostgREST/Error.hs#L553 + code: 'PGRST116', + details: `Results contain ${data.length} rows, application/vnd.pgrst.object+json requires 1 row`, + hint: null, + message: 'JSON object requested, multiple (or no) rows returned', + }; + data = null; + count = null; + status = 406; + statusText = 'Not Acceptable'; + } + else if (data.length === 1) { + data = data[0]; + } + else { + data = null; + } + } + } + else { + const body = await res.text(); + try { + error = JSON.parse(body); + // Workaround for https://github.com/supabase/postgrest-js/issues/295 + if (Array.isArray(error) && res.status === 404) { + data = []; + error = null; + status = 200; + statusText = 'OK'; + } + } + catch (_d) { + // Workaround for https://github.com/supabase/postgrest-js/issues/295 + if (res.status === 404 && body === '') { + status = 204; + statusText = 'No Content'; + } + else { + error = { + message: body, + }; + } + } + if (error && this.isMaybeSingle && ((_c = error === null || error === void 0 ? void 0 : error.details) === null || _c === void 0 ? void 0 : _c.includes('0 rows'))) { + error = null; + status = 200; + statusText = 'OK'; + } + if (error && this.shouldThrowOnError) { + throw new PostgrestError_1.default(error); + } + } + const postgrestResponse = { + error, + data, + count, + status, + statusText, + }; + return postgrestResponse; + }); + if (!this.shouldThrowOnError) { + res = res.catch((fetchError) => { + var _a, _b, _c; + return ({ + error: { + message: `${(_a = fetchError === null || fetchError === void 0 ? void 0 : fetchError.name) !== null && _a !== void 0 ? _a : 'FetchError'}: ${fetchError === null || fetchError === void 0 ? void 0 : fetchError.message}`, + details: `${(_b = fetchError === null || fetchError === void 0 ? void 0 : fetchError.stack) !== null && _b !== void 0 ? _b : ''}`, + hint: '', + code: `${(_c = fetchError === null || fetchError === void 0 ? void 0 : fetchError.code) !== null && _c !== void 0 ? _c : ''}`, + }, + data: null, + count: null, + status: 0, + statusText: '', + }); + }); + } + return res.then(onfulfilled, onrejected); + } +} +exports["default"] = PostgrestBuilder; +//# sourceMappingURL=PostgrestBuilder.js.map + +/***/ }), + +/***/ 1526: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const PostgrestQueryBuilder_1 = __importDefault(__nccwpck_require__(50)); +const PostgrestFilterBuilder_1 = __importDefault(__nccwpck_require__(6671)); +const constants_1 = __nccwpck_require__(7796); +/** + * PostgREST client. + * + * @typeParam Database - Types for the schema from the [type + * generator](https://supabase.com/docs/reference/javascript/next/typescript-support) + * + * @typeParam SchemaName - Postgres schema to switch to. Must be a string + * literal, the same one passed to the constructor. If the schema is not + * `"public"`, this must be supplied manually. + */ +class PostgrestClient { + // TODO: Add back shouldThrowOnError once we figure out the typings + /** + * Creates a PostgREST client. + * + * @param url - URL of the PostgREST endpoint + * @param options - Named parameters + * @param options.headers - Custom headers + * @param options.schema - Postgres schema to switch to + * @param options.fetch - Custom fetch + */ + constructor(url, { headers = {}, schema, fetch, } = {}) { + this.url = url; + this.headers = Object.assign(Object.assign({}, constants_1.DEFAULT_HEADERS), headers); + this.schemaName = schema; + this.fetch = fetch; + } + /** + * Perform a query on a table or a view. + * + * @param relation - The table or view name to query + */ + from(relation) { + const url = new URL(`${this.url}/${relation}`); + return new PostgrestQueryBuilder_1.default(url, { + headers: Object.assign({}, this.headers), + schema: this.schemaName, + fetch: this.fetch, + }); + } + /** + * Select a schema to query or perform an function (rpc) call. + * + * The schema needs to be on the list of exposed schemas inside Supabase. + * + * @param schema - The schema to query + */ + schema(schema) { + return new PostgrestClient(this.url, { + headers: this.headers, + schema, + fetch: this.fetch, + }); + } + /** + * Perform a function call. + * + * @param fn - The function name to call + * @param args - The arguments to pass to the function call + * @param options - Named parameters + * @param options.head - When set to `true`, `data` will not be returned. + * Useful if you only need the count. + * @param options.get - When set to `true`, the function will be called with + * read-only access mode. + * @param options.count - Count algorithm to use to count rows returned by the + * function. Only applicable for [set-returning + * functions](https://www.postgresql.org/docs/current/functions-srf.html). + * + * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the + * hood. + * + * `"planned"`: Approximated but fast count algorithm. Uses the Postgres + * statistics under the hood. + * + * `"estimated"`: Uses exact count for low numbers and planned count for high + * numbers. + */ + rpc(fn, args = {}, { head = false, get = false, count, } = {}) { + let method; + const url = new URL(`${this.url}/rpc/${fn}`); + let body; + if (head || get) { + method = head ? 'HEAD' : 'GET'; + Object.entries(args) + // params with undefined value needs to be filtered out, otherwise it'll + // show up as `?param=undefined` + .filter(([_, value]) => value !== undefined) + // array values need special syntax + .map(([name, value]) => [name, Array.isArray(value) ? `{${value.join(',')}}` : `${value}`]) + .forEach(([name, value]) => { + url.searchParams.append(name, value); + }); + } + else { + method = 'POST'; + body = args; + } + const headers = Object.assign({}, this.headers); + if (count) { + headers['Prefer'] = `count=${count}`; + } + return new PostgrestFilterBuilder_1.default({ + method, + url, + headers, + schema: this.schemaName, + body, + fetch: this.fetch, + allowEmpty: false, + }); + } +} +exports["default"] = PostgrestClient; +//# sourceMappingURL=PostgrestClient.js.map + +/***/ }), + +/***/ 7317: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +/** + * Error format + * + * {@link https://postgrest.org/en/stable/api.html?highlight=options#errors-and-http-status-codes} + */ +class PostgrestError extends Error { + constructor(context) { + super(context.message); + this.name = 'PostgrestError'; + this.details = context.details; + this.hint = context.hint; + this.code = context.code; + } +} +exports["default"] = PostgrestError; +//# sourceMappingURL=PostgrestError.js.map + +/***/ }), + +/***/ 6671: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const PostgrestTransformBuilder_1 = __importDefault(__nccwpck_require__(1566)); +class PostgrestFilterBuilder extends PostgrestTransformBuilder_1.default { + /** + * Match only rows where `column` is equal to `value`. + * + * To check if the value of `column` is NULL, you should use `.is()` instead. + * + * @param column - The column to filter on + * @param value - The value to filter with + */ + eq(column, value) { + this.url.searchParams.append(column, `eq.${value}`); + return this; + } + /** + * Match only rows where `column` is not equal to `value`. + * + * @param column - The column to filter on + * @param value - The value to filter with + */ + neq(column, value) { + this.url.searchParams.append(column, `neq.${value}`); + return this; + } + /** + * Match only rows where `column` is greater than `value`. + * + * @param column - The column to filter on + * @param value - The value to filter with + */ + gt(column, value) { + this.url.searchParams.append(column, `gt.${value}`); + return this; + } + /** + * Match only rows where `column` is greater than or equal to `value`. + * + * @param column - The column to filter on + * @param value - The value to filter with + */ + gte(column, value) { + this.url.searchParams.append(column, `gte.${value}`); + return this; + } + /** + * Match only rows where `column` is less than `value`. + * + * @param column - The column to filter on + * @param value - The value to filter with + */ + lt(column, value) { + this.url.searchParams.append(column, `lt.${value}`); + return this; + } + /** + * Match only rows where `column` is less than or equal to `value`. + * + * @param column - The column to filter on + * @param value - The value to filter with + */ + lte(column, value) { + this.url.searchParams.append(column, `lte.${value}`); + return this; + } + /** + * Match only rows where `column` matches `pattern` case-sensitively. + * + * @param column - The column to filter on + * @param pattern - The pattern to match with + */ + like(column, pattern) { + this.url.searchParams.append(column, `like.${pattern}`); + return this; + } + /** + * Match only rows where `column` matches all of `patterns` case-sensitively. + * + * @param column - The column to filter on + * @param patterns - The patterns to match with + */ + likeAllOf(column, patterns) { + this.url.searchParams.append(column, `like(all).{${patterns.join(',')}}`); + return this; + } + /** + * Match only rows where `column` matches any of `patterns` case-sensitively. + * + * @param column - The column to filter on + * @param patterns - The patterns to match with + */ + likeAnyOf(column, patterns) { + this.url.searchParams.append(column, `like(any).{${patterns.join(',')}}`); + return this; + } + /** + * Match only rows where `column` matches `pattern` case-insensitively. + * + * @param column - The column to filter on + * @param pattern - The pattern to match with + */ + ilike(column, pattern) { + this.url.searchParams.append(column, `ilike.${pattern}`); + return this; + } + /** + * Match only rows where `column` matches all of `patterns` case-insensitively. + * + * @param column - The column to filter on + * @param patterns - The patterns to match with + */ + ilikeAllOf(column, patterns) { + this.url.searchParams.append(column, `ilike(all).{${patterns.join(',')}}`); + return this; + } + /** + * Match only rows where `column` matches any of `patterns` case-insensitively. + * + * @param column - The column to filter on + * @param patterns - The patterns to match with + */ + ilikeAnyOf(column, patterns) { + this.url.searchParams.append(column, `ilike(any).{${patterns.join(',')}}`); + return this; + } + /** + * Match only rows where `column` IS `value`. + * + * For non-boolean columns, this is only relevant for checking if the value of + * `column` is NULL by setting `value` to `null`. + * + * For boolean columns, you can also set `value` to `true` or `false` and it + * will behave the same way as `.eq()`. + * + * @param column - The column to filter on + * @param value - The value to filter with + */ + is(column, value) { + this.url.searchParams.append(column, `is.${value}`); + return this; + } + /** + * Match only rows where `column` is included in the `values` array. + * + * @param column - The column to filter on + * @param values - The values array to filter with + */ + in(column, values) { + const cleanedValues = Array.from(new Set(values)) + .map((s) => { + // handle postgrest reserved characters + // https://postgrest.org/en/v7.0.0/api.html#reserved-characters + if (typeof s === 'string' && new RegExp('[,()]').test(s)) + return `"${s}"`; + else + return `${s}`; + }) + .join(','); + this.url.searchParams.append(column, `in.(${cleanedValues})`); + return this; + } + /** + * Only relevant for jsonb, array, and range columns. Match only rows where + * `column` contains every element appearing in `value`. + * + * @param column - The jsonb, array, or range column to filter on + * @param value - The jsonb, array, or range value to filter with + */ + contains(column, value) { + if (typeof value === 'string') { + // range types can be inclusive '[', ']' or exclusive '(', ')' so just + // keep it simple and accept a string + this.url.searchParams.append(column, `cs.${value}`); + } + else if (Array.isArray(value)) { + // array + this.url.searchParams.append(column, `cs.{${value.join(',')}}`); + } + else { + // json + this.url.searchParams.append(column, `cs.${JSON.stringify(value)}`); + } + return this; + } + /** + * Only relevant for jsonb, array, and range columns. Match only rows where + * every element appearing in `column` is contained by `value`. + * + * @param column - The jsonb, array, or range column to filter on + * @param value - The jsonb, array, or range value to filter with + */ + containedBy(column, value) { + if (typeof value === 'string') { + // range + this.url.searchParams.append(column, `cd.${value}`); + } + else if (Array.isArray(value)) { + // array + this.url.searchParams.append(column, `cd.{${value.join(',')}}`); + } + else { + // json + this.url.searchParams.append(column, `cd.${JSON.stringify(value)}`); + } + return this; + } + /** + * Only relevant for range columns. Match only rows where every element in + * `column` is greater than any element in `range`. + * + * @param column - The range column to filter on + * @param range - The range to filter with + */ + rangeGt(column, range) { + this.url.searchParams.append(column, `sr.${range}`); + return this; + } + /** + * Only relevant for range columns. Match only rows where every element in + * `column` is either contained in `range` or greater than any element in + * `range`. + * + * @param column - The range column to filter on + * @param range - The range to filter with + */ + rangeGte(column, range) { + this.url.searchParams.append(column, `nxl.${range}`); + return this; + } + /** + * Only relevant for range columns. Match only rows where every element in + * `column` is less than any element in `range`. + * + * @param column - The range column to filter on + * @param range - The range to filter with + */ + rangeLt(column, range) { + this.url.searchParams.append(column, `sl.${range}`); + return this; + } + /** + * Only relevant for range columns. Match only rows where every element in + * `column` is either contained in `range` or less than any element in + * `range`. + * + * @param column - The range column to filter on + * @param range - The range to filter with + */ + rangeLte(column, range) { + this.url.searchParams.append(column, `nxr.${range}`); + return this; + } + /** + * Only relevant for range columns. Match only rows where `column` is + * mutually exclusive to `range` and there can be no element between the two + * ranges. + * + * @param column - The range column to filter on + * @param range - The range to filter with + */ + rangeAdjacent(column, range) { + this.url.searchParams.append(column, `adj.${range}`); + return this; + } + /** + * Only relevant for array and range columns. Match only rows where + * `column` and `value` have an element in common. + * + * @param column - The array or range column to filter on + * @param value - The array or range value to filter with + */ + overlaps(column, value) { + if (typeof value === 'string') { + // range + this.url.searchParams.append(column, `ov.${value}`); + } + else { + // array + this.url.searchParams.append(column, `ov.{${value.join(',')}}`); + } + return this; + } + /** + * Only relevant for text and tsvector columns. Match only rows where + * `column` matches the query string in `query`. + * + * @param column - The text or tsvector column to filter on + * @param query - The query text to match with + * @param options - Named parameters + * @param options.config - The text search configuration to use + * @param options.type - Change how the `query` text is interpreted + */ + textSearch(column, query, { config, type } = {}) { + let typePart = ''; + if (type === 'plain') { + typePart = 'pl'; + } + else if (type === 'phrase') { + typePart = 'ph'; + } + else if (type === 'websearch') { + typePart = 'w'; + } + const configPart = config === undefined ? '' : `(${config})`; + this.url.searchParams.append(column, `${typePart}fts${configPart}.${query}`); + return this; + } + /** + * Match only rows where each column in `query` keys is equal to its + * associated value. Shorthand for multiple `.eq()`s. + * + * @param query - The object to filter with, with column names as keys mapped + * to their filter values + */ + match(query) { + Object.entries(query).forEach(([column, value]) => { + this.url.searchParams.append(column, `eq.${value}`); + }); + return this; + } + /** + * Match only rows which doesn't satisfy the filter. + * + * Unlike most filters, `opearator` and `value` are used as-is and need to + * follow [PostgREST + * syntax](https://postgrest.org/en/stable/api.html#operators). You also need + * to make sure they are properly sanitized. + * + * @param column - The column to filter on + * @param operator - The operator to be negated to filter with, following + * PostgREST syntax + * @param value - The value to filter with, following PostgREST syntax + */ + not(column, operator, value) { + this.url.searchParams.append(column, `not.${operator}.${value}`); + return this; + } + /** + * Match only rows which satisfy at least one of the filters. + * + * Unlike most filters, `filters` is used as-is and needs to follow [PostgREST + * syntax](https://postgrest.org/en/stable/api.html#operators). You also need + * to make sure it's properly sanitized. + * + * It's currently not possible to do an `.or()` filter across multiple tables. + * + * @param filters - The filters to use, following PostgREST syntax + * @param options - Named parameters + * @param options.referencedTable - Set this to filter on referenced tables + * instead of the parent table + * @param options.foreignTable - Deprecated, use `referencedTable` instead + */ + or(filters, { foreignTable, referencedTable = foreignTable, } = {}) { + const key = referencedTable ? `${referencedTable}.or` : 'or'; + this.url.searchParams.append(key, `(${filters})`); + return this; + } + /** + * Match only rows which satisfy the filter. This is an escape hatch - you + * should use the specific filter methods wherever possible. + * + * Unlike most filters, `opearator` and `value` are used as-is and need to + * follow [PostgREST + * syntax](https://postgrest.org/en/stable/api.html#operators). You also need + * to make sure they are properly sanitized. + * + * @param column - The column to filter on + * @param operator - The operator to filter with, following PostgREST syntax + * @param value - The value to filter with, following PostgREST syntax + */ + filter(column, operator, value) { + this.url.searchParams.append(column, `${operator}.${value}`); + return this; + } +} +exports["default"] = PostgrestFilterBuilder; +//# sourceMappingURL=PostgrestFilterBuilder.js.map + +/***/ }), + +/***/ 50: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const PostgrestFilterBuilder_1 = __importDefault(__nccwpck_require__(6671)); +class PostgrestQueryBuilder { + constructor(url, { headers = {}, schema, fetch, }) { + this.url = url; + this.headers = headers; + this.schema = schema; + this.fetch = fetch; + } + /** + * Perform a SELECT query on the table or view. + * + * @param columns - The columns to retrieve, separated by commas. Columns can be renamed when returned with `customName:columnName` + * + * @param options - Named parameters + * + * @param options.head - When set to `true`, `data` will not be returned. + * Useful if you only need the count. + * + * @param options.count - Count algorithm to use to count rows in the table or view. + * + * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the + * hood. + * + * `"planned"`: Approximated but fast count algorithm. Uses the Postgres + * statistics under the hood. + * + * `"estimated"`: Uses exact count for low numbers and planned count for high + * numbers. + */ + select(columns, { head = false, count, } = {}) { + const method = head ? 'HEAD' : 'GET'; + // Remove whitespaces except when quoted + let quoted = false; + const cleanedColumns = (columns !== null && columns !== void 0 ? columns : '*') + .split('') + .map((c) => { + if (/\s/.test(c) && !quoted) { + return ''; + } + if (c === '"') { + quoted = !quoted; + } + return c; + }) + .join(''); + this.url.searchParams.set('select', cleanedColumns); + if (count) { + this.headers['Prefer'] = `count=${count}`; + } + return new PostgrestFilterBuilder_1.default({ + method, + url: this.url, + headers: this.headers, + schema: this.schema, + fetch: this.fetch, + allowEmpty: false, + }); + } + /** + * Perform an INSERT into the table or view. + * + * By default, inserted rows are not returned. To return it, chain the call + * with `.select()`. + * + * @param values - The values to insert. Pass an object to insert a single row + * or an array to insert multiple rows. + * + * @param options - Named parameters + * + * @param options.count - Count algorithm to use to count inserted rows. + * + * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the + * hood. + * + * `"planned"`: Approximated but fast count algorithm. Uses the Postgres + * statistics under the hood. + * + * `"estimated"`: Uses exact count for low numbers and planned count for high + * numbers. + * + * @param options.defaultToNull - Make missing fields default to `null`. + * Otherwise, use the default value for the column. Only applies for bulk + * inserts. + */ + insert(values, { count, defaultToNull = true, } = {}) { + const method = 'POST'; + const prefersHeaders = []; + if (this.headers['Prefer']) { + prefersHeaders.push(this.headers['Prefer']); + } + if (count) { + prefersHeaders.push(`count=${count}`); + } + if (!defaultToNull) { + prefersHeaders.push('missing=default'); + } + this.headers['Prefer'] = prefersHeaders.join(','); + if (Array.isArray(values)) { + const columns = values.reduce((acc, x) => acc.concat(Object.keys(x)), []); + if (columns.length > 0) { + const uniqueColumns = [...new Set(columns)].map((column) => `"${column}"`); + this.url.searchParams.set('columns', uniqueColumns.join(',')); + } + } + return new PostgrestFilterBuilder_1.default({ + method, + url: this.url, + headers: this.headers, + schema: this.schema, + body: values, + fetch: this.fetch, + allowEmpty: false, + }); + } + /** + * Perform an UPSERT on the table or view. Depending on the column(s) passed + * to `onConflict`, `.upsert()` allows you to perform the equivalent of + * `.insert()` if a row with the corresponding `onConflict` columns doesn't + * exist, or if it does exist, perform an alternative action depending on + * `ignoreDuplicates`. + * + * By default, upserted rows are not returned. To return it, chain the call + * with `.select()`. + * + * @param values - The values to upsert with. Pass an object to upsert a + * single row or an array to upsert multiple rows. + * + * @param options - Named parameters + * + * @param options.onConflict - Comma-separated UNIQUE column(s) to specify how + * duplicate rows are determined. Two rows are duplicates if all the + * `onConflict` columns are equal. + * + * @param options.ignoreDuplicates - If `true`, duplicate rows are ignored. If + * `false`, duplicate rows are merged with existing rows. + * + * @param options.count - Count algorithm to use to count upserted rows. + * + * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the + * hood. + * + * `"planned"`: Approximated but fast count algorithm. Uses the Postgres + * statistics under the hood. + * + * `"estimated"`: Uses exact count for low numbers and planned count for high + * numbers. + * + * @param options.defaultToNull - Make missing fields default to `null`. + * Otherwise, use the default value for the column. This only applies when + * inserting new rows, not when merging with existing rows under + * `ignoreDuplicates: false`. This also only applies when doing bulk upserts. + */ + upsert(values, { onConflict, ignoreDuplicates = false, count, defaultToNull = true, } = {}) { + const method = 'POST'; + const prefersHeaders = [`resolution=${ignoreDuplicates ? 'ignore' : 'merge'}-duplicates`]; + if (onConflict !== undefined) + this.url.searchParams.set('on_conflict', onConflict); + if (this.headers['Prefer']) { + prefersHeaders.push(this.headers['Prefer']); + } + if (count) { + prefersHeaders.push(`count=${count}`); + } + if (!defaultToNull) { + prefersHeaders.push('missing=default'); + } + this.headers['Prefer'] = prefersHeaders.join(','); + if (Array.isArray(values)) { + const columns = values.reduce((acc, x) => acc.concat(Object.keys(x)), []); + if (columns.length > 0) { + const uniqueColumns = [...new Set(columns)].map((column) => `"${column}"`); + this.url.searchParams.set('columns', uniqueColumns.join(',')); + } + } + return new PostgrestFilterBuilder_1.default({ + method, + url: this.url, + headers: this.headers, + schema: this.schema, + body: values, + fetch: this.fetch, + allowEmpty: false, + }); + } + /** + * Perform an UPDATE on the table or view. + * + * By default, updated rows are not returned. To return it, chain the call + * with `.select()` after filters. + * + * @param values - The values to update with + * + * @param options - Named parameters + * + * @param options.count - Count algorithm to use to count updated rows. + * + * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the + * hood. + * + * `"planned"`: Approximated but fast count algorithm. Uses the Postgres + * statistics under the hood. + * + * `"estimated"`: Uses exact count for low numbers and planned count for high + * numbers. + */ + update(values, { count, } = {}) { + const method = 'PATCH'; + const prefersHeaders = []; + if (this.headers['Prefer']) { + prefersHeaders.push(this.headers['Prefer']); + } + if (count) { + prefersHeaders.push(`count=${count}`); + } + this.headers['Prefer'] = prefersHeaders.join(','); + return new PostgrestFilterBuilder_1.default({ + method, + url: this.url, + headers: this.headers, + schema: this.schema, + body: values, + fetch: this.fetch, + allowEmpty: false, + }); + } + /** + * Perform a DELETE on the table or view. + * + * By default, deleted rows are not returned. To return it, chain the call + * with `.select()` after filters. + * + * @param options - Named parameters + * + * @param options.count - Count algorithm to use to count deleted rows. + * + * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the + * hood. + * + * `"planned"`: Approximated but fast count algorithm. Uses the Postgres + * statistics under the hood. + * + * `"estimated"`: Uses exact count for low numbers and planned count for high + * numbers. + */ + delete({ count, } = {}) { + const method = 'DELETE'; + const prefersHeaders = []; + if (count) { + prefersHeaders.push(`count=${count}`); + } + if (this.headers['Prefer']) { + prefersHeaders.unshift(this.headers['Prefer']); + } + this.headers['Prefer'] = prefersHeaders.join(','); + return new PostgrestFilterBuilder_1.default({ + method, + url: this.url, + headers: this.headers, + schema: this.schema, + fetch: this.fetch, + allowEmpty: false, + }); + } +} +exports["default"] = PostgrestQueryBuilder; +//# sourceMappingURL=PostgrestQueryBuilder.js.map + +/***/ }), + +/***/ 1566: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const PostgrestBuilder_1 = __importDefault(__nccwpck_require__(1049)); +class PostgrestTransformBuilder extends PostgrestBuilder_1.default { + /** + * Perform a SELECT on the query result. + * + * By default, `.insert()`, `.update()`, `.upsert()`, and `.delete()` do not + * return modified rows. By calling this method, modified rows are returned in + * `data`. + * + * @param columns - The columns to retrieve, separated by commas + */ + select(columns) { + // Remove whitespaces except when quoted + let quoted = false; + const cleanedColumns = (columns !== null && columns !== void 0 ? columns : '*') + .split('') + .map((c) => { + if (/\s/.test(c) && !quoted) { + return ''; + } + if (c === '"') { + quoted = !quoted; + } + return c; + }) + .join(''); + this.url.searchParams.set('select', cleanedColumns); + if (this.headers['Prefer']) { + this.headers['Prefer'] += ','; + } + this.headers['Prefer'] += 'return=representation'; + return this; + } + /** + * Order the query result by `column`. + * + * You can call this method multiple times to order by multiple columns. + * + * You can order referenced tables, but it only affects the ordering of the + * parent table if you use `!inner` in the query. + * + * @param column - The column to order by + * @param options - Named parameters + * @param options.ascending - If `true`, the result will be in ascending order + * @param options.nullsFirst - If `true`, `null`s appear first. If `false`, + * `null`s appear last. + * @param options.referencedTable - Set this to order a referenced table by + * its columns + * @param options.foreignTable - Deprecated, use `options.referencedTable` + * instead + */ + order(column, { ascending = true, nullsFirst, foreignTable, referencedTable = foreignTable, } = {}) { + const key = referencedTable ? `${referencedTable}.order` : 'order'; + const existingOrder = this.url.searchParams.get(key); + this.url.searchParams.set(key, `${existingOrder ? `${existingOrder},` : ''}${column}.${ascending ? 'asc' : 'desc'}${nullsFirst === undefined ? '' : nullsFirst ? '.nullsfirst' : '.nullslast'}`); + return this; + } + /** + * Limit the query result by `count`. + * + * @param count - The maximum number of rows to return + * @param options - Named parameters + * @param options.referencedTable - Set this to limit rows of referenced + * tables instead of the parent table + * @param options.foreignTable - Deprecated, use `options.referencedTable` + * instead + */ + limit(count, { foreignTable, referencedTable = foreignTable, } = {}) { + const key = typeof referencedTable === 'undefined' ? 'limit' : `${referencedTable}.limit`; + this.url.searchParams.set(key, `${count}`); + return this; + } + /** + * Limit the query result by starting at an offset `from` and ending at the offset `to`. + * Only records within this range are returned. + * This respects the query order and if there is no order clause the range could behave unexpectedly. + * The `from` and `to` values are 0-based and inclusive: `range(1, 3)` will include the second, third + * and fourth rows of the query. + * + * @param from - The starting index from which to limit the result + * @param to - The last index to which to limit the result + * @param options - Named parameters + * @param options.referencedTable - Set this to limit rows of referenced + * tables instead of the parent table + * @param options.foreignTable - Deprecated, use `options.referencedTable` + * instead + */ + range(from, to, { foreignTable, referencedTable = foreignTable, } = {}) { + const keyOffset = typeof referencedTable === 'undefined' ? 'offset' : `${referencedTable}.offset`; + const keyLimit = typeof referencedTable === 'undefined' ? 'limit' : `${referencedTable}.limit`; + this.url.searchParams.set(keyOffset, `${from}`); + // Range is inclusive, so add 1 + this.url.searchParams.set(keyLimit, `${to - from + 1}`); + return this; + } + /** + * Set the AbortSignal for the fetch request. + * + * @param signal - The AbortSignal to use for the fetch request + */ + abortSignal(signal) { + this.signal = signal; + return this; + } + /** + * Return `data` as a single object instead of an array of objects. + * + * Query result must be one row (e.g. using `.limit(1)`), otherwise this + * returns an error. + */ + single() { + this.headers['Accept'] = 'application/vnd.pgrst.object+json'; + return this; + } + /** + * Return `data` as a single object instead of an array of objects. + * + * Query result must be zero or one row (e.g. using `.limit(1)`), otherwise + * this returns an error. + */ + maybeSingle() { + // Temporary partial fix for https://github.com/supabase/postgrest-js/issues/361 + // Issue persists e.g. for `.insert([...]).select().maybeSingle()` + if (this.method === 'GET') { + this.headers['Accept'] = 'application/json'; + } + else { + this.headers['Accept'] = 'application/vnd.pgrst.object+json'; + } + this.isMaybeSingle = true; + return this; + } + /** + * Return `data` as a string in CSV format. + */ + csv() { + this.headers['Accept'] = 'text/csv'; + return this; + } + /** + * Return `data` as an object in [GeoJSON](https://geojson.org) format. + */ + geojson() { + this.headers['Accept'] = 'application/geo+json'; + return this; + } + /** + * Return `data` as the EXPLAIN plan for the query. + * + * You need to enable the + * [db_plan_enabled](https://supabase.com/docs/guides/database/debugging-performance#enabling-explain) + * setting before using this method. + * + * @param options - Named parameters + * + * @param options.analyze - If `true`, the query will be executed and the + * actual run time will be returned + * + * @param options.verbose - If `true`, the query identifier will be returned + * and `data` will include the output columns of the query + * + * @param options.settings - If `true`, include information on configuration + * parameters that affect query planning + * + * @param options.buffers - If `true`, include information on buffer usage + * + * @param options.wal - If `true`, include information on WAL record generation + * + * @param options.format - The format of the output, can be `"text"` (default) + * or `"json"` + */ + explain({ analyze = false, verbose = false, settings = false, buffers = false, wal = false, format = 'text', } = {}) { + var _a; + const options = [ + analyze ? 'analyze' : null, + verbose ? 'verbose' : null, + settings ? 'settings' : null, + buffers ? 'buffers' : null, + wal ? 'wal' : null, + ] + .filter(Boolean) + .join('|'); + // An Accept header can carry multiple media types but postgrest-js always sends one + const forMediatype = (_a = this.headers['Accept']) !== null && _a !== void 0 ? _a : 'application/json'; + this.headers['Accept'] = `application/vnd.pgrst.plan+${format}; for="${forMediatype}"; options=${options};`; + if (format === 'json') + return this; + else + return this; + } + /** + * Rollback the query. + * + * `data` will still be returned, but the query is not committed. + */ + rollback() { + var _a; + if (((_a = this.headers['Prefer']) !== null && _a !== void 0 ? _a : '').trim().length > 0) { + this.headers['Prefer'] += ',tx=rollback'; + } + else { + this.headers['Prefer'] = 'tx=rollback'; + } + return this; + } + /** + * Override the type of the returned `data`. + * + * @typeParam NewResult - The new result type to override with + */ + returns() { + return this; + } +} +exports["default"] = PostgrestTransformBuilder; +//# sourceMappingURL=PostgrestTransformBuilder.js.map + +/***/ }), + +/***/ 7796: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DEFAULT_HEADERS = void 0; +const version_1 = __nccwpck_require__(6832); +exports.DEFAULT_HEADERS = { 'X-Client-Info': `postgrest-js/${version_1.version}` }; +//# sourceMappingURL=constants.js.map + +/***/ }), + +/***/ 1178: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.PostgrestError = exports.PostgrestBuilder = exports.PostgrestTransformBuilder = exports.PostgrestFilterBuilder = exports.PostgrestQueryBuilder = exports.PostgrestClient = void 0; +// Always update wrapper.mjs when updating this file. +const PostgrestClient_1 = __importDefault(__nccwpck_require__(1526)); +exports.PostgrestClient = PostgrestClient_1.default; +const PostgrestQueryBuilder_1 = __importDefault(__nccwpck_require__(50)); +exports.PostgrestQueryBuilder = PostgrestQueryBuilder_1.default; +const PostgrestFilterBuilder_1 = __importDefault(__nccwpck_require__(6671)); +exports.PostgrestFilterBuilder = PostgrestFilterBuilder_1.default; +const PostgrestTransformBuilder_1 = __importDefault(__nccwpck_require__(1566)); +exports.PostgrestTransformBuilder = PostgrestTransformBuilder_1.default; +const PostgrestBuilder_1 = __importDefault(__nccwpck_require__(1049)); +exports.PostgrestBuilder = PostgrestBuilder_1.default; +const PostgrestError_1 = __importDefault(__nccwpck_require__(7317)); +exports.PostgrestError = PostgrestError_1.default; +exports["default"] = { + PostgrestClient: PostgrestClient_1.default, + PostgrestQueryBuilder: PostgrestQueryBuilder_1.default, + PostgrestFilterBuilder: PostgrestFilterBuilder_1.default, + PostgrestTransformBuilder: PostgrestTransformBuilder_1.default, + PostgrestBuilder: PostgrestBuilder_1.default, + PostgrestError: PostgrestError_1.default, +}; +//# sourceMappingURL=index.js.map + +/***/ }), + +/***/ 6832: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.version = void 0; +exports.version = '0.0.0-automated'; +//# sourceMappingURL=version.js.map + +/***/ }), + +/***/ 9911: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.REALTIME_CHANNEL_STATES = exports.REALTIME_SUBSCRIBE_STATES = exports.REALTIME_LISTEN_TYPES = exports.REALTIME_POSTGRES_CHANGES_LISTEN_EVENT = void 0; +const constants_1 = __nccwpck_require__(88); +const push_1 = __importDefault(__nccwpck_require__(2292)); +const timer_1 = __importDefault(__nccwpck_require__(2983)); +const RealtimePresence_1 = __importDefault(__nccwpck_require__(5583)); +const Transformers = __importStar(__nccwpck_require__(1140)); +const transformers_1 = __nccwpck_require__(1140); +var REALTIME_POSTGRES_CHANGES_LISTEN_EVENT; +(function (REALTIME_POSTGRES_CHANGES_LISTEN_EVENT) { + REALTIME_POSTGRES_CHANGES_LISTEN_EVENT["ALL"] = "*"; + REALTIME_POSTGRES_CHANGES_LISTEN_EVENT["INSERT"] = "INSERT"; + REALTIME_POSTGRES_CHANGES_LISTEN_EVENT["UPDATE"] = "UPDATE"; + REALTIME_POSTGRES_CHANGES_LISTEN_EVENT["DELETE"] = "DELETE"; +})(REALTIME_POSTGRES_CHANGES_LISTEN_EVENT = exports.REALTIME_POSTGRES_CHANGES_LISTEN_EVENT || (exports.REALTIME_POSTGRES_CHANGES_LISTEN_EVENT = {})); +var REALTIME_LISTEN_TYPES; +(function (REALTIME_LISTEN_TYPES) { + REALTIME_LISTEN_TYPES["BROADCAST"] = "broadcast"; + REALTIME_LISTEN_TYPES["PRESENCE"] = "presence"; + REALTIME_LISTEN_TYPES["POSTGRES_CHANGES"] = "postgres_changes"; + REALTIME_LISTEN_TYPES["SYSTEM"] = "system"; +})(REALTIME_LISTEN_TYPES = exports.REALTIME_LISTEN_TYPES || (exports.REALTIME_LISTEN_TYPES = {})); +var REALTIME_SUBSCRIBE_STATES; +(function (REALTIME_SUBSCRIBE_STATES) { + REALTIME_SUBSCRIBE_STATES["SUBSCRIBED"] = "SUBSCRIBED"; + REALTIME_SUBSCRIBE_STATES["TIMED_OUT"] = "TIMED_OUT"; + REALTIME_SUBSCRIBE_STATES["CLOSED"] = "CLOSED"; + REALTIME_SUBSCRIBE_STATES["CHANNEL_ERROR"] = "CHANNEL_ERROR"; +})(REALTIME_SUBSCRIBE_STATES = exports.REALTIME_SUBSCRIBE_STATES || (exports.REALTIME_SUBSCRIBE_STATES = {})); +exports.REALTIME_CHANNEL_STATES = constants_1.CHANNEL_STATES; +/** A channel is the basic building block of Realtime + * and narrows the scope of data flow to subscribed clients. + * You can think of a channel as a chatroom where participants are able to see who's online + * and send and receive messages. + */ +class RealtimeChannel { + constructor( + /** Topic name can be any string. */ + topic, params = { config: {} }, socket) { + this.topic = topic; + this.params = params; + this.socket = socket; + this.bindings = {}; + this.state = constants_1.CHANNEL_STATES.closed; + this.joinedOnce = false; + this.pushBuffer = []; + this.subTopic = topic.replace(/^realtime:/i, ''); + this.params.config = Object.assign({ + broadcast: { ack: false, self: false }, + presence: { key: '' }, + private: false, + }, params.config); + this.timeout = this.socket.timeout; + this.joinPush = new push_1.default(this, constants_1.CHANNEL_EVENTS.join, this.params, this.timeout); + this.rejoinTimer = new timer_1.default(() => this._rejoinUntilConnected(), this.socket.reconnectAfterMs); + this.joinPush.receive('ok', () => { + this.state = constants_1.CHANNEL_STATES.joined; + this.rejoinTimer.reset(); + this.pushBuffer.forEach((pushEvent) => pushEvent.send()); + this.pushBuffer = []; + }); + this._onClose(() => { + this.rejoinTimer.reset(); + this.socket.log('channel', `close ${this.topic} ${this._joinRef()}`); + this.state = constants_1.CHANNEL_STATES.closed; + this.socket._remove(this); + }); + this._onError((reason) => { + if (this._isLeaving() || this._isClosed()) { + return; + } + this.socket.log('channel', `error ${this.topic}`, reason); + this.state = constants_1.CHANNEL_STATES.errored; + this.rejoinTimer.scheduleTimeout(); + }); + this.joinPush.receive('timeout', () => { + if (!this._isJoining()) { + return; + } + this.socket.log('channel', `timeout ${this.topic}`, this.joinPush.timeout); + this.state = constants_1.CHANNEL_STATES.errored; + this.rejoinTimer.scheduleTimeout(); + }); + this._on(constants_1.CHANNEL_EVENTS.reply, {}, (payload, ref) => { + this._trigger(this._replyEventName(ref), payload); + }); + this.presence = new RealtimePresence_1.default(this); + this.broadcastEndpointURL = + (0, transformers_1.httpEndpointURL)(this.socket.endPoint) + '/api/broadcast'; + this.private = this.params.config.private || false; + } + /** Subscribe registers your client with the server */ + subscribe(callback, timeout = this.timeout) { + var _a, _b; + if (!this.socket.isConnected()) { + this.socket.connect(); + } + if (this.joinedOnce) { + throw `tried to subscribe multiple times. 'subscribe' can only be called a single time per channel instance`; + } + else { + const { config: { broadcast, presence, private: isPrivate }, } = this.params; + this._onError((e) => callback === null || callback === void 0 ? void 0 : callback(REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR, e)); + this._onClose(() => callback === null || callback === void 0 ? void 0 : callback(REALTIME_SUBSCRIBE_STATES.CLOSED)); + const accessTokenPayload = {}; + const config = { + broadcast, + presence, + postgres_changes: (_b = (_a = this.bindings.postgres_changes) === null || _a === void 0 ? void 0 : _a.map((r) => r.filter)) !== null && _b !== void 0 ? _b : [], + private: isPrivate, + }; + if (this.socket.accessTokenValue) { + accessTokenPayload.access_token = this.socket.accessTokenValue; + } + this.updateJoinPayload(Object.assign({ config }, accessTokenPayload)); + this.joinedOnce = true; + this._rejoin(timeout); + this.joinPush + .receive('ok', async ({ postgres_changes }) => { + var _a; + this.socket.setAuth(); + if (postgres_changes === undefined) { + callback === null || callback === void 0 ? void 0 : callback(REALTIME_SUBSCRIBE_STATES.SUBSCRIBED); + return; + } + else { + const clientPostgresBindings = this.bindings.postgres_changes; + const bindingsLen = (_a = clientPostgresBindings === null || clientPostgresBindings === void 0 ? void 0 : clientPostgresBindings.length) !== null && _a !== void 0 ? _a : 0; + const newPostgresBindings = []; + for (let i = 0; i < bindingsLen; i++) { + const clientPostgresBinding = clientPostgresBindings[i]; + const { filter: { event, schema, table, filter }, } = clientPostgresBinding; + const serverPostgresFilter = postgres_changes && postgres_changes[i]; + if (serverPostgresFilter && + serverPostgresFilter.event === event && + serverPostgresFilter.schema === schema && + serverPostgresFilter.table === table && + serverPostgresFilter.filter === filter) { + newPostgresBindings.push(Object.assign(Object.assign({}, clientPostgresBinding), { id: serverPostgresFilter.id })); + } + else { + this.unsubscribe(); + callback === null || callback === void 0 ? void 0 : callback(REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR, new Error('mismatch between server and client bindings for postgres changes')); + return; + } + } + this.bindings.postgres_changes = newPostgresBindings; + callback && callback(REALTIME_SUBSCRIBE_STATES.SUBSCRIBED); + return; + } + }) + .receive('error', (error) => { + callback === null || callback === void 0 ? void 0 : callback(REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR, new Error(JSON.stringify(Object.values(error).join(', ') || 'error'))); + return; + }) + .receive('timeout', () => { + callback === null || callback === void 0 ? void 0 : callback(REALTIME_SUBSCRIBE_STATES.TIMED_OUT); + return; + }); + } + return this; + } + presenceState() { + return this.presence.state; + } + async track(payload, opts = {}) { + return await this.send({ + type: 'presence', + event: 'track', + payload, + }, opts.timeout || this.timeout); + } + async untrack(opts = {}) { + return await this.send({ + type: 'presence', + event: 'untrack', + }, opts); + } + on(type, filter, callback) { + return this._on(type, filter, callback); + } + /** + * Sends a message into the channel. + * + * @param args Arguments to send to channel + * @param args.type The type of event to send + * @param args.event The name of the event being sent + * @param args.payload Payload to be sent + * @param opts Options to be used during the send process + */ + async send(args, opts = {}) { + var _a, _b; + if (!this._canPush() && args.type === 'broadcast') { + const { event, payload: endpoint_payload } = args; + const authorization = this.socket.accessTokenValue + ? `Bearer ${this.socket.accessTokenValue}` + : ''; + const options = { + method: 'POST', + headers: { + Authorization: authorization, + apikey: this.socket.apiKey ? this.socket.apiKey : '', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + messages: [ + { + topic: this.subTopic, + event, + payload: endpoint_payload, + private: this.private, + }, + ], + }), + }; + try { + const response = await this._fetchWithTimeout(this.broadcastEndpointURL, options, (_a = opts.timeout) !== null && _a !== void 0 ? _a : this.timeout); + await ((_b = response.body) === null || _b === void 0 ? void 0 : _b.cancel()); + return response.ok ? 'ok' : 'error'; + } + catch (error) { + if (error.name === 'AbortError') { + return 'timed out'; + } + else { + return 'error'; + } + } + } + else { + return new Promise((resolve) => { + var _a, _b, _c; + const push = this._push(args.type, args, opts.timeout || this.timeout); + if (args.type === 'broadcast' && !((_c = (_b = (_a = this.params) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.broadcast) === null || _c === void 0 ? void 0 : _c.ack)) { + resolve('ok'); + } + push.receive('ok', () => resolve('ok')); + push.receive('error', () => resolve('error')); + push.receive('timeout', () => resolve('timed out')); + }); + } + } + updateJoinPayload(payload) { + this.joinPush.updatePayload(payload); + } + /** + * Leaves the channel. + * + * Unsubscribes from server events, and instructs channel to terminate on server. + * Triggers onClose() hooks. + * + * To receive leave acknowledgements, use the a `receive` hook to bind to the server ack, ie: + * channel.unsubscribe().receive("ok", () => alert("left!") ) + */ + unsubscribe(timeout = this.timeout) { + this.state = constants_1.CHANNEL_STATES.leaving; + const onClose = () => { + this.socket.log('channel', `leave ${this.topic}`); + this._trigger(constants_1.CHANNEL_EVENTS.close, 'leave', this._joinRef()); + }; + this.rejoinTimer.reset(); + // Destroy joinPush to avoid connection timeouts during unscription phase + this.joinPush.destroy(); + return new Promise((resolve) => { + const leavePush = new push_1.default(this, constants_1.CHANNEL_EVENTS.leave, {}, timeout); + leavePush + .receive('ok', () => { + onClose(); + resolve('ok'); + }) + .receive('timeout', () => { + onClose(); + resolve('timed out'); + }) + .receive('error', () => { + resolve('error'); + }); + leavePush.send(); + if (!this._canPush()) { + leavePush.trigger('ok', {}); + } + }); + } + /** @internal */ + async _fetchWithTimeout(url, options, timeout) { + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeout); + const response = await this.socket.fetch(url, Object.assign(Object.assign({}, options), { signal: controller.signal })); + clearTimeout(id); + return response; + } + /** @internal */ + _push(event, payload, timeout = this.timeout) { + if (!this.joinedOnce) { + throw `tried to push '${event}' to '${this.topic}' before joining. Use channel.subscribe() before pushing events`; + } + let pushEvent = new push_1.default(this, event, payload, timeout); + if (this._canPush()) { + pushEvent.send(); + } + else { + pushEvent.startTimeout(); + this.pushBuffer.push(pushEvent); + } + return pushEvent; + } + /** + * Overridable message hook + * + * Receives all events for specialized message handling before dispatching to the channel callbacks. + * Must return the payload, modified or unmodified. + * + * @internal + */ + _onMessage(_event, payload, _ref) { + return payload; + } + /** @internal */ + _isMember(topic) { + return this.topic === topic; + } + /** @internal */ + _joinRef() { + return this.joinPush.ref; + } + /** @internal */ + _trigger(type, payload, ref) { + var _a, _b; + const typeLower = type.toLocaleLowerCase(); + const { close, error, leave, join } = constants_1.CHANNEL_EVENTS; + const events = [close, error, leave, join]; + if (ref && events.indexOf(typeLower) >= 0 && ref !== this._joinRef()) { + return; + } + let handledPayload = this._onMessage(typeLower, payload, ref); + if (payload && !handledPayload) { + throw 'channel onMessage callbacks must return the payload, modified or unmodified'; + } + if (['insert', 'update', 'delete'].includes(typeLower)) { + (_a = this.bindings.postgres_changes) === null || _a === void 0 ? void 0 : _a.filter((bind) => { + var _a, _b, _c; + return (((_a = bind.filter) === null || _a === void 0 ? void 0 : _a.event) === '*' || + ((_c = (_b = bind.filter) === null || _b === void 0 ? void 0 : _b.event) === null || _c === void 0 ? void 0 : _c.toLocaleLowerCase()) === typeLower); + }).map((bind) => bind.callback(handledPayload, ref)); + } + else { + (_b = this.bindings[typeLower]) === null || _b === void 0 ? void 0 : _b.filter((bind) => { + var _a, _b, _c, _d, _e, _f; + if (['broadcast', 'presence', 'postgres_changes'].includes(typeLower)) { + if ('id' in bind) { + const bindId = bind.id; + const bindEvent = (_a = bind.filter) === null || _a === void 0 ? void 0 : _a.event; + return (bindId && + ((_b = payload.ids) === null || _b === void 0 ? void 0 : _b.includes(bindId)) && + (bindEvent === '*' || + (bindEvent === null || bindEvent === void 0 ? void 0 : bindEvent.toLocaleLowerCase()) === + ((_c = payload.data) === null || _c === void 0 ? void 0 : _c.type.toLocaleLowerCase()))); + } + else { + const bindEvent = (_e = (_d = bind === null || bind === void 0 ? void 0 : bind.filter) === null || _d === void 0 ? void 0 : _d.event) === null || _e === void 0 ? void 0 : _e.toLocaleLowerCase(); + return (bindEvent === '*' || + bindEvent === ((_f = payload === null || payload === void 0 ? void 0 : payload.event) === null || _f === void 0 ? void 0 : _f.toLocaleLowerCase())); + } + } + else { + return bind.type.toLocaleLowerCase() === typeLower; + } + }).map((bind) => { + if (typeof handledPayload === 'object' && 'ids' in handledPayload) { + const postgresChanges = handledPayload.data; + const { schema, table, commit_timestamp, type, errors } = postgresChanges; + const enrichedPayload = { + schema: schema, + table: table, + commit_timestamp: commit_timestamp, + eventType: type, + new: {}, + old: {}, + errors: errors, + }; + handledPayload = Object.assign(Object.assign({}, enrichedPayload), this._getPayloadRecords(postgresChanges)); + } + bind.callback(handledPayload, ref); + }); + } + } + /** @internal */ + _isClosed() { + return this.state === constants_1.CHANNEL_STATES.closed; + } + /** @internal */ + _isJoined() { + return this.state === constants_1.CHANNEL_STATES.joined; + } + /** @internal */ + _isJoining() { + return this.state === constants_1.CHANNEL_STATES.joining; + } + /** @internal */ + _isLeaving() { + return this.state === constants_1.CHANNEL_STATES.leaving; + } + /** @internal */ + _replyEventName(ref) { + return `chan_reply_${ref}`; + } + /** @internal */ + _on(type, filter, callback) { + const typeLower = type.toLocaleLowerCase(); + const binding = { + type: typeLower, + filter: filter, + callback: callback, + }; + if (this.bindings[typeLower]) { + this.bindings[typeLower].push(binding); + } + else { + this.bindings[typeLower] = [binding]; + } + return this; + } + /** @internal */ + _off(type, filter) { + const typeLower = type.toLocaleLowerCase(); + this.bindings[typeLower] = this.bindings[typeLower].filter((bind) => { + var _a; + return !(((_a = bind.type) === null || _a === void 0 ? void 0 : _a.toLocaleLowerCase()) === typeLower && + RealtimeChannel.isEqual(bind.filter, filter)); + }); + return this; + } + /** @internal */ + static isEqual(obj1, obj2) { + if (Object.keys(obj1).length !== Object.keys(obj2).length) { + return false; + } + for (const k in obj1) { + if (obj1[k] !== obj2[k]) { + return false; + } + } + return true; + } + /** @internal */ + _rejoinUntilConnected() { + this.rejoinTimer.scheduleTimeout(); + if (this.socket.isConnected()) { + this._rejoin(); + } + } + /** + * Registers a callback that will be executed when the channel closes. + * + * @internal + */ + _onClose(callback) { + this._on(constants_1.CHANNEL_EVENTS.close, {}, callback); + } + /** + * Registers a callback that will be executed when the channel encounteres an error. + * + * @internal + */ + _onError(callback) { + this._on(constants_1.CHANNEL_EVENTS.error, {}, (reason) => callback(reason)); + } + /** + * Returns `true` if the socket is connected and the channel has been joined. + * + * @internal + */ + _canPush() { + return this.socket.isConnected() && this._isJoined(); + } + /** @internal */ + _rejoin(timeout = this.timeout) { + if (this._isLeaving()) { + return; + } + this.socket._leaveOpenTopic(this.topic); + this.state = constants_1.CHANNEL_STATES.joining; + this.joinPush.resend(timeout); + } + /** @internal */ + _getPayloadRecords(payload) { + const records = { + new: {}, + old: {}, + }; + if (payload.type === 'INSERT' || payload.type === 'UPDATE') { + records.new = Transformers.convertChangeData(payload.columns, payload.record); + } + if (payload.type === 'UPDATE' || payload.type === 'DELETE') { + records.old = Transformers.convertChangeData(payload.columns, payload.old_record); + } + return records; + } +} +exports["default"] = RealtimeChannel; +//# sourceMappingURL=RealtimeChannel.js.map + +/***/ }), + +/***/ 9103: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const constants_1 = __nccwpck_require__(88); +const serializer_1 = __importDefault(__nccwpck_require__(5360)); +const timer_1 = __importDefault(__nccwpck_require__(2983)); +const transformers_1 = __nccwpck_require__(1140); +const RealtimeChannel_1 = __importDefault(__nccwpck_require__(9911)); +const noop = () => { }; +const NATIVE_WEBSOCKET_AVAILABLE = typeof WebSocket !== 'undefined'; +const WORKER_SCRIPT = ` + addEventListener("message", (e) => { + if (e.data.event === "start") { + setInterval(() => postMessage({ event: "keepAlive" }), e.data.interval); + } + });`; +class RealtimeClient { + /** + * Initializes the Socket. + * + * @param endPoint The string WebSocket endpoint, ie, "ws://example.com/socket", "wss://example.com", "/socket" (inherited host & protocol) + * @param httpEndpoint The string HTTP endpoint, ie, "https://example.com", "/" (inherited host & protocol) + * @param options.transport The Websocket Transport, for example WebSocket. + * @param options.timeout The default timeout in milliseconds to trigger push timeouts. + * @param options.params The optional params to pass when connecting. + * @param options.headers The optional headers to pass when connecting. + * @param options.heartbeatIntervalMs The millisec interval to send a heartbeat message. + * @param options.logger The optional function for specialized logging, ie: logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) } + * @param options.encode The function to encode outgoing messages. Defaults to JSON: (payload, callback) => callback(JSON.stringify(payload)) + * @param options.decode The function to decode incoming messages. Defaults to Serializer's decode. + * @param options.reconnectAfterMs he optional function that returns the millsec reconnect interval. Defaults to stepped backoff off. + * @param options.worker Use Web Worker to set a side flow. Defaults to false. + * @param options.workerUrl The URL of the worker script. Defaults to https://realtime.supabase.com/worker.js that includes a heartbeat event call to keep the connection alive. + */ + constructor(endPoint, options) { + var _a; + this.accessTokenValue = null; + this.apiKey = null; + this.channels = []; + this.endPoint = ''; + this.httpEndpoint = ''; + this.headers = constants_1.DEFAULT_HEADERS; + this.params = {}; + this.timeout = constants_1.DEFAULT_TIMEOUT; + this.heartbeatIntervalMs = 30000; + this.heartbeatTimer = undefined; + this.pendingHeartbeatRef = null; + this.ref = 0; + this.logger = noop; + this.conn = null; + this.sendBuffer = []; + this.serializer = new serializer_1.default(); + this.stateChangeCallbacks = { + open: [], + close: [], + error: [], + message: [], + }; + this.accessToken = null; + /** + * Use either custom fetch, if provided, or default fetch to make HTTP requests + * + * @internal + */ + this._resolveFetch = (customFetch) => { + let _fetch; + if (customFetch) { + _fetch = customFetch; + } + else if (typeof fetch === 'undefined') { + _fetch = (...args) => Promise.resolve().then(() => __importStar(__nccwpck_require__(2668))).then(({ default: fetch }) => fetch(...args)); + } + else { + _fetch = fetch; + } + return (...args) => _fetch(...args); + }; + this.endPoint = `${endPoint}/${constants_1.TRANSPORTS.websocket}`; + this.httpEndpoint = (0, transformers_1.httpEndpointURL)(endPoint); + if (options === null || options === void 0 ? void 0 : options.transport) { + this.transport = options.transport; + } + else { + this.transport = null; + } + if (options === null || options === void 0 ? void 0 : options.params) + this.params = options.params; + if (options === null || options === void 0 ? void 0 : options.headers) + this.headers = Object.assign(Object.assign({}, this.headers), options.headers); + if (options === null || options === void 0 ? void 0 : options.timeout) + this.timeout = options.timeout; + if (options === null || options === void 0 ? void 0 : options.logger) + this.logger = options.logger; + if (options === null || options === void 0 ? void 0 : options.heartbeatIntervalMs) + this.heartbeatIntervalMs = options.heartbeatIntervalMs; + const accessTokenValue = (_a = options === null || options === void 0 ? void 0 : options.params) === null || _a === void 0 ? void 0 : _a.apikey; + if (accessTokenValue) { + this.accessTokenValue = accessTokenValue; + this.apiKey = accessTokenValue; + } + this.reconnectAfterMs = (options === null || options === void 0 ? void 0 : options.reconnectAfterMs) + ? options.reconnectAfterMs + : (tries) => { + return [1000, 2000, 5000, 10000][tries - 1] || 10000; + }; + this.encode = (options === null || options === void 0 ? void 0 : options.encode) + ? options.encode + : (payload, callback) => { + return callback(JSON.stringify(payload)); + }; + this.decode = (options === null || options === void 0 ? void 0 : options.decode) + ? options.decode + : this.serializer.decode.bind(this.serializer); + this.reconnectTimer = new timer_1.default(async () => { + this.disconnect(); + this.connect(); + }, this.reconnectAfterMs); + this.fetch = this._resolveFetch(options === null || options === void 0 ? void 0 : options.fetch); + if (options === null || options === void 0 ? void 0 : options.worker) { + if (typeof window !== 'undefined' && !window.Worker) { + throw new Error('Web Worker is not supported'); + } + this.worker = (options === null || options === void 0 ? void 0 : options.worker) || false; + this.workerUrl = options === null || options === void 0 ? void 0 : options.workerUrl; + } + this.accessToken = (options === null || options === void 0 ? void 0 : options.accessToken) || null; + } + /** + * Connects the socket, unless already connected. + */ + connect() { + if (this.conn) { + return; + } + if (this.transport) { + this.conn = new this.transport(this.endpointURL(), undefined, { + headers: this.headers, + }); + return; + } + if (NATIVE_WEBSOCKET_AVAILABLE) { + this.conn = new WebSocket(this.endpointURL()); + this.setupConnection(); + return; + } + this.conn = new WSWebSocketDummy(this.endpointURL(), undefined, { + close: () => { + this.conn = null; + }, + }); + Promise.resolve().then(() => __importStar(__nccwpck_require__(906))).then(({ default: WS }) => { + this.conn = new WS(this.endpointURL(), undefined, { + headers: this.headers, + }); + this.setupConnection(); + }); + } + /** + * Returns the URL of the websocket. + * @returns string The URL of the websocket. + */ + endpointURL() { + return this._appendParams(this.endPoint, Object.assign({}, this.params, { vsn: constants_1.VSN })); + } + /** + * Disconnects the socket. + * + * @param code A numeric status code to send on disconnect. + * @param reason A custom reason for the disconnect. + */ + disconnect(code, reason) { + if (this.conn) { + this.conn.onclose = function () { }; // noop + if (code) { + this.conn.close(code, reason !== null && reason !== void 0 ? reason : ''); + } + else { + this.conn.close(); + } + this.conn = null; + // remove open handles + this.heartbeatTimer && clearInterval(this.heartbeatTimer); + this.reconnectTimer.reset(); + } + } + /** + * Returns all created channels + */ + getChannels() { + return this.channels; + } + /** + * Unsubscribes and removes a single channel + * @param channel A RealtimeChannel instance + */ + async removeChannel(channel) { + const status = await channel.unsubscribe(); + if (this.channels.length === 0) { + this.disconnect(); + } + return status; + } + /** + * Unsubscribes and removes all channels + */ + async removeAllChannels() { + const values_1 = await Promise.all(this.channels.map((channel) => channel.unsubscribe())); + this.disconnect(); + return values_1; + } + /** + * Logs the message. + * + * For customized logging, `this.logger` can be overridden. + */ + log(kind, msg, data) { + this.logger(kind, msg, data); + } + /** + * Returns the current state of the socket. + */ + connectionState() { + switch (this.conn && this.conn.readyState) { + case constants_1.SOCKET_STATES.connecting: + return constants_1.CONNECTION_STATE.Connecting; + case constants_1.SOCKET_STATES.open: + return constants_1.CONNECTION_STATE.Open; + case constants_1.SOCKET_STATES.closing: + return constants_1.CONNECTION_STATE.Closing; + default: + return constants_1.CONNECTION_STATE.Closed; + } + } + /** + * Returns `true` is the connection is open. + */ + isConnected() { + return this.connectionState() === constants_1.CONNECTION_STATE.Open; + } + channel(topic, params = { config: {} }) { + const chan = new RealtimeChannel_1.default(`realtime:${topic}`, params, this); + this.channels.push(chan); + return chan; + } + /** + * Push out a message if the socket is connected. + * + * If the socket is not connected, the message gets enqueued within a local buffer, and sent out when a connection is next established. + */ + push(data) { + const { topic, event, payload, ref } = data; + const callback = () => { + this.encode(data, (result) => { + var _a; + (_a = this.conn) === null || _a === void 0 ? void 0 : _a.send(result); + }); + }; + this.log('push', `${topic} ${event} (${ref})`, payload); + if (this.isConnected()) { + callback(); + } + else { + this.sendBuffer.push(callback); + } + } + /** + * Sets the JWT access token used for channel subscription authorization and Realtime RLS. + * + * If param is null it will use the `accessToken` callback function or the token set on the client. + * + * On callback used, it will set the value of the token internal to the client. + * + * @param token A JWT string to override the token set on the client. + */ + async setAuth(token = null) { + let tokenToSend = token || + (this.accessToken && (await this.accessToken())) || + this.accessTokenValue; + if (tokenToSend) { + let parsed = null; + try { + parsed = JSON.parse(atob(tokenToSend.split('.')[1])); + } + catch (_error) { } + if (parsed && parsed.exp) { + let now = Math.floor(Date.now() / 1000); + let valid = now - parsed.exp < 0; + if (!valid) { + this.log('auth', `InvalidJWTToken: Invalid value for JWT claim "exp" with value ${parsed.exp}`); + return Promise.reject(`InvalidJWTToken: Invalid value for JWT claim "exp" with value ${parsed.exp}`); + } + } + this.accessTokenValue = tokenToSend; + this.channels.forEach((channel) => { + tokenToSend && channel.updateJoinPayload({ access_token: tokenToSend }); + if (channel.joinedOnce && channel._isJoined()) { + channel._push(constants_1.CHANNEL_EVENTS.access_token, { + access_token: tokenToSend, + }); + } + }); + } + } + /** + * Sends a heartbeat message if the socket is connected. + */ + async sendHeartbeat() { + var _a; + if (!this.isConnected()) { + return; + } + if (this.pendingHeartbeatRef) { + this.pendingHeartbeatRef = null; + this.log('transport', 'heartbeat timeout. Attempting to re-establish connection'); + (_a = this.conn) === null || _a === void 0 ? void 0 : _a.close(constants_1.WS_CLOSE_NORMAL, 'hearbeat timeout'); + return; + } + this.pendingHeartbeatRef = this._makeRef(); + this.push({ + topic: 'phoenix', + event: 'heartbeat', + payload: {}, + ref: this.pendingHeartbeatRef, + }); + this.setAuth(); + } + /** + * Flushes send buffer + */ + flushSendBuffer() { + if (this.isConnected() && this.sendBuffer.length > 0) { + this.sendBuffer.forEach((callback) => callback()); + this.sendBuffer = []; + } + } + /** + * Return the next message ref, accounting for overflows + * + * @internal + */ + _makeRef() { + let newRef = this.ref + 1; + if (newRef === this.ref) { + this.ref = 0; + } + else { + this.ref = newRef; + } + return this.ref.toString(); + } + /** + * Unsubscribe from channels with the specified topic. + * + * @internal + */ + _leaveOpenTopic(topic) { + let dupChannel = this.channels.find((c) => c.topic === topic && (c._isJoined() || c._isJoining())); + if (dupChannel) { + this.log('transport', `leaving duplicate topic "${topic}"`); + dupChannel.unsubscribe(); + } + } + /** + * Removes a subscription from the socket. + * + * @param channel An open subscription. + * + * @internal + */ + _remove(channel) { + this.channels = this.channels.filter((c) => c._joinRef() !== channel._joinRef()); + } + /** + * Sets up connection handlers. + * + * @internal + */ + setupConnection() { + if (this.conn) { + this.conn.binaryType = 'arraybuffer'; + this.conn.onopen = () => this._onConnOpen(); + this.conn.onerror = (error) => this._onConnError(error); + this.conn.onmessage = (event) => this._onConnMessage(event); + this.conn.onclose = (event) => this._onConnClose(event); + } + } + /** @internal */ + _onConnMessage(rawMessage) { + this.decode(rawMessage.data, (msg) => { + let { topic, event, payload, ref } = msg; + if (ref && ref === this.pendingHeartbeatRef) { + this.pendingHeartbeatRef = null; + } + this.log('receive', `${payload.status || ''} ${topic} ${event} ${(ref && '(' + ref + ')') || ''}`, payload); + this.channels + .filter((channel) => channel._isMember(topic)) + .forEach((channel) => channel._trigger(event, payload, ref)); + this.stateChangeCallbacks.message.forEach((callback) => callback(msg)); + }); + } + /** @internal */ + async _onConnOpen() { + this.log('transport', `connected to ${this.endpointURL()}`); + this.flushSendBuffer(); + this.reconnectTimer.reset(); + if (!this.worker) { + this.heartbeatTimer && clearInterval(this.heartbeatTimer); + this.heartbeatTimer = setInterval(() => this.sendHeartbeat(), this.heartbeatIntervalMs); + } + else { + if (this.workerUrl) { + this.log('worker', `starting worker for from ${this.workerUrl}`); + } + else { + this.log('worker', `starting default worker`); + } + const objectUrl = this._workerObjectUrl(this.workerUrl); + this.workerRef = new Worker(objectUrl); + this.workerRef.onerror = (error) => { + this.log('worker', 'worker error', error.message); + this.workerRef.terminate(); + }; + this.workerRef.onmessage = (event) => { + if (event.data.event === 'keepAlive') { + this.sendHeartbeat(); + } + }; + this.workerRef.postMessage({ + event: 'start', + interval: this.heartbeatIntervalMs, + }); + } + this.stateChangeCallbacks.open.forEach((callback) => callback()); + } + /** @internal */ + _onConnClose(event) { + this.log('transport', 'close', event); + this._triggerChanError(); + this.heartbeatTimer && clearInterval(this.heartbeatTimer); + this.reconnectTimer.scheduleTimeout(); + this.stateChangeCallbacks.close.forEach((callback) => callback(event)); + } + /** @internal */ + _onConnError(error) { + this.log('transport', error.message); + this._triggerChanError(); + this.stateChangeCallbacks.error.forEach((callback) => callback(error)); + } + /** @internal */ + _triggerChanError() { + this.channels.forEach((channel) => channel._trigger(constants_1.CHANNEL_EVENTS.error)); + } + /** @internal */ + _appendParams(url, params) { + if (Object.keys(params).length === 0) { + return url; + } + const prefix = url.match(/\?/) ? '&' : '?'; + const query = new URLSearchParams(params); + return `${url}${prefix}${query}`; + } + _workerObjectUrl(url) { + let result_url; + if (url) { + result_url = url; + } + else { + const blob = new Blob([WORKER_SCRIPT], { type: 'application/javascript' }); + result_url = URL.createObjectURL(blob); + } + return result_url; + } +} +exports["default"] = RealtimeClient; +class WSWebSocketDummy { + constructor(address, _protocols, options) { + this.binaryType = 'arraybuffer'; + this.onclose = () => { }; + this.onerror = () => { }; + this.onmessage = () => { }; + this.onopen = () => { }; + this.readyState = constants_1.SOCKET_STATES.connecting; + this.send = () => { }; + this.url = null; + this.url = address; + this.close = options.close; + } +} +//# sourceMappingURL=RealtimeClient.js.map + +/***/ }), + +/***/ 5583: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +/* + This file draws heavily from https://github.com/phoenixframework/phoenix/blob/d344ec0a732ab4ee204215b31de69cf4be72e3bf/assets/js/phoenix/presence.js + License: https://github.com/phoenixframework/phoenix/blob/d344ec0a732ab4ee204215b31de69cf4be72e3bf/LICENSE.md +*/ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.REALTIME_PRESENCE_LISTEN_EVENTS = void 0; +var REALTIME_PRESENCE_LISTEN_EVENTS; +(function (REALTIME_PRESENCE_LISTEN_EVENTS) { + REALTIME_PRESENCE_LISTEN_EVENTS["SYNC"] = "sync"; + REALTIME_PRESENCE_LISTEN_EVENTS["JOIN"] = "join"; + REALTIME_PRESENCE_LISTEN_EVENTS["LEAVE"] = "leave"; +})(REALTIME_PRESENCE_LISTEN_EVENTS = exports.REALTIME_PRESENCE_LISTEN_EVENTS || (exports.REALTIME_PRESENCE_LISTEN_EVENTS = {})); +class RealtimePresence { + /** + * Initializes the Presence. + * + * @param channel - The RealtimeChannel + * @param opts - The options, + * for example `{events: {state: 'state', diff: 'diff'}}` + */ + constructor(channel, opts) { + this.channel = channel; + this.state = {}; + this.pendingDiffs = []; + this.joinRef = null; + this.caller = { + onJoin: () => { }, + onLeave: () => { }, + onSync: () => { }, + }; + const events = (opts === null || opts === void 0 ? void 0 : opts.events) || { + state: 'presence_state', + diff: 'presence_diff', + }; + this.channel._on(events.state, {}, (newState) => { + const { onJoin, onLeave, onSync } = this.caller; + this.joinRef = this.channel._joinRef(); + this.state = RealtimePresence.syncState(this.state, newState, onJoin, onLeave); + this.pendingDiffs.forEach((diff) => { + this.state = RealtimePresence.syncDiff(this.state, diff, onJoin, onLeave); + }); + this.pendingDiffs = []; + onSync(); + }); + this.channel._on(events.diff, {}, (diff) => { + const { onJoin, onLeave, onSync } = this.caller; + if (this.inPendingSyncState()) { + this.pendingDiffs.push(diff); + } + else { + this.state = RealtimePresence.syncDiff(this.state, diff, onJoin, onLeave); + onSync(); + } + }); + this.onJoin((key, currentPresences, newPresences) => { + this.channel._trigger('presence', { + event: 'join', + key, + currentPresences, + newPresences, + }); + }); + this.onLeave((key, currentPresences, leftPresences) => { + this.channel._trigger('presence', { + event: 'leave', + key, + currentPresences, + leftPresences, + }); + }); + this.onSync(() => { + this.channel._trigger('presence', { event: 'sync' }); + }); + } + /** + * Used to sync the list of presences on the server with the + * client's state. + * + * An optional `onJoin` and `onLeave` callback can be provided to + * react to changes in the client's local presences across + * disconnects and reconnects with the server. + * + * @internal + */ + static syncState(currentState, newState, onJoin, onLeave) { + const state = this.cloneDeep(currentState); + const transformedState = this.transformState(newState); + const joins = {}; + const leaves = {}; + this.map(state, (key, presences) => { + if (!transformedState[key]) { + leaves[key] = presences; + } + }); + this.map(transformedState, (key, newPresences) => { + const currentPresences = state[key]; + if (currentPresences) { + const newPresenceRefs = newPresences.map((m) => m.presence_ref); + const curPresenceRefs = currentPresences.map((m) => m.presence_ref); + const joinedPresences = newPresences.filter((m) => curPresenceRefs.indexOf(m.presence_ref) < 0); + const leftPresences = currentPresences.filter((m) => newPresenceRefs.indexOf(m.presence_ref) < 0); + if (joinedPresences.length > 0) { + joins[key] = joinedPresences; + } + if (leftPresences.length > 0) { + leaves[key] = leftPresences; + } + } + else { + joins[key] = newPresences; + } + }); + return this.syncDiff(state, { joins, leaves }, onJoin, onLeave); + } + /** + * Used to sync a diff of presence join and leave events from the + * server, as they happen. + * + * Like `syncState`, `syncDiff` accepts optional `onJoin` and + * `onLeave` callbacks to react to a user joining or leaving from a + * device. + * + * @internal + */ + static syncDiff(state, diff, onJoin, onLeave) { + const { joins, leaves } = { + joins: this.transformState(diff.joins), + leaves: this.transformState(diff.leaves), + }; + if (!onJoin) { + onJoin = () => { }; + } + if (!onLeave) { + onLeave = () => { }; + } + this.map(joins, (key, newPresences) => { + var _a; + const currentPresences = (_a = state[key]) !== null && _a !== void 0 ? _a : []; + state[key] = this.cloneDeep(newPresences); + if (currentPresences.length > 0) { + const joinedPresenceRefs = state[key].map((m) => m.presence_ref); + const curPresences = currentPresences.filter((m) => joinedPresenceRefs.indexOf(m.presence_ref) < 0); + state[key].unshift(...curPresences); + } + onJoin(key, currentPresences, newPresences); + }); + this.map(leaves, (key, leftPresences) => { + let currentPresences = state[key]; + if (!currentPresences) + return; + const presenceRefsToRemove = leftPresences.map((m) => m.presence_ref); + currentPresences = currentPresences.filter((m) => presenceRefsToRemove.indexOf(m.presence_ref) < 0); + state[key] = currentPresences; + onLeave(key, currentPresences, leftPresences); + if (currentPresences.length === 0) + delete state[key]; + }); + return state; + } + /** @internal */ + static map(obj, func) { + return Object.getOwnPropertyNames(obj).map((key) => func(key, obj[key])); + } + /** + * Remove 'metas' key + * Change 'phx_ref' to 'presence_ref' + * Remove 'phx_ref' and 'phx_ref_prev' + * + * @example + * // returns { + * abc123: [ + * { presence_ref: '2', user_id: 1 }, + * { presence_ref: '3', user_id: 2 } + * ] + * } + * RealtimePresence.transformState({ + * abc123: { + * metas: [ + * { phx_ref: '2', phx_ref_prev: '1' user_id: 1 }, + * { phx_ref: '3', user_id: 2 } + * ] + * } + * }) + * + * @internal + */ + static transformState(state) { + state = this.cloneDeep(state); + return Object.getOwnPropertyNames(state).reduce((newState, key) => { + const presences = state[key]; + if ('metas' in presences) { + newState[key] = presences.metas.map((presence) => { + presence['presence_ref'] = presence['phx_ref']; + delete presence['phx_ref']; + delete presence['phx_ref_prev']; + return presence; + }); + } + else { + newState[key] = presences; + } + return newState; + }, {}); + } + /** @internal */ + static cloneDeep(obj) { + return JSON.parse(JSON.stringify(obj)); + } + /** @internal */ + onJoin(callback) { + this.caller.onJoin = callback; + } + /** @internal */ + onLeave(callback) { + this.caller.onLeave = callback; + } + /** @internal */ + onSync(callback) { + this.caller.onSync = callback; + } + /** @internal */ + inPendingSyncState() { + return !this.joinRef || this.joinRef !== this.channel._joinRef(); + } +} +exports["default"] = RealtimePresence; +//# sourceMappingURL=RealtimePresence.js.map + +/***/ }), + +/***/ 442: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.REALTIME_CHANNEL_STATES = exports.REALTIME_SUBSCRIBE_STATES = exports.REALTIME_PRESENCE_LISTEN_EVENTS = exports.REALTIME_POSTGRES_CHANGES_LISTEN_EVENT = exports.REALTIME_LISTEN_TYPES = exports.RealtimeClient = exports.RealtimeChannel = exports.RealtimePresence = void 0; +const RealtimeClient_1 = __importDefault(__nccwpck_require__(9103)); +exports.RealtimeClient = RealtimeClient_1.default; +const RealtimeChannel_1 = __importStar(__nccwpck_require__(9911)); +exports.RealtimeChannel = RealtimeChannel_1.default; +Object.defineProperty(exports, "REALTIME_LISTEN_TYPES", ({ enumerable: true, get: function () { return RealtimeChannel_1.REALTIME_LISTEN_TYPES; } })); +Object.defineProperty(exports, "REALTIME_POSTGRES_CHANGES_LISTEN_EVENT", ({ enumerable: true, get: function () { return RealtimeChannel_1.REALTIME_POSTGRES_CHANGES_LISTEN_EVENT; } })); +Object.defineProperty(exports, "REALTIME_SUBSCRIBE_STATES", ({ enumerable: true, get: function () { return RealtimeChannel_1.REALTIME_SUBSCRIBE_STATES; } })); +Object.defineProperty(exports, "REALTIME_CHANNEL_STATES", ({ enumerable: true, get: function () { return RealtimeChannel_1.REALTIME_CHANNEL_STATES; } })); +const RealtimePresence_1 = __importStar(__nccwpck_require__(5583)); +exports.RealtimePresence = RealtimePresence_1.default; +Object.defineProperty(exports, "REALTIME_PRESENCE_LISTEN_EVENTS", ({ enumerable: true, get: function () { return RealtimePresence_1.REALTIME_PRESENCE_LISTEN_EVENTS; } })); +//# sourceMappingURL=index.js.map + +/***/ }), + +/***/ 88: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.CONNECTION_STATE = exports.TRANSPORTS = exports.CHANNEL_EVENTS = exports.CHANNEL_STATES = exports.SOCKET_STATES = exports.WS_CLOSE_NORMAL = exports.DEFAULT_TIMEOUT = exports.VSN = exports.DEFAULT_HEADERS = void 0; +const version_1 = __nccwpck_require__(318); +exports.DEFAULT_HEADERS = { 'X-Client-Info': `realtime-js/${version_1.version}` }; +exports.VSN = '1.0.0'; +exports.DEFAULT_TIMEOUT = 10000; +exports.WS_CLOSE_NORMAL = 1000; +var SOCKET_STATES; +(function (SOCKET_STATES) { + SOCKET_STATES[SOCKET_STATES["connecting"] = 0] = "connecting"; + SOCKET_STATES[SOCKET_STATES["open"] = 1] = "open"; + SOCKET_STATES[SOCKET_STATES["closing"] = 2] = "closing"; + SOCKET_STATES[SOCKET_STATES["closed"] = 3] = "closed"; +})(SOCKET_STATES = exports.SOCKET_STATES || (exports.SOCKET_STATES = {})); +var CHANNEL_STATES; +(function (CHANNEL_STATES) { + CHANNEL_STATES["closed"] = "closed"; + CHANNEL_STATES["errored"] = "errored"; + CHANNEL_STATES["joined"] = "joined"; + CHANNEL_STATES["joining"] = "joining"; + CHANNEL_STATES["leaving"] = "leaving"; +})(CHANNEL_STATES = exports.CHANNEL_STATES || (exports.CHANNEL_STATES = {})); +var CHANNEL_EVENTS; +(function (CHANNEL_EVENTS) { + CHANNEL_EVENTS["close"] = "phx_close"; + CHANNEL_EVENTS["error"] = "phx_error"; + CHANNEL_EVENTS["join"] = "phx_join"; + CHANNEL_EVENTS["reply"] = "phx_reply"; + CHANNEL_EVENTS["leave"] = "phx_leave"; + CHANNEL_EVENTS["access_token"] = "access_token"; +})(CHANNEL_EVENTS = exports.CHANNEL_EVENTS || (exports.CHANNEL_EVENTS = {})); +var TRANSPORTS; +(function (TRANSPORTS) { + TRANSPORTS["websocket"] = "websocket"; +})(TRANSPORTS = exports.TRANSPORTS || (exports.TRANSPORTS = {})); +var CONNECTION_STATE; +(function (CONNECTION_STATE) { + CONNECTION_STATE["Connecting"] = "connecting"; + CONNECTION_STATE["Open"] = "open"; + CONNECTION_STATE["Closing"] = "closing"; + CONNECTION_STATE["Closed"] = "closed"; +})(CONNECTION_STATE = exports.CONNECTION_STATE || (exports.CONNECTION_STATE = {})); +//# sourceMappingURL=constants.js.map + +/***/ }), + +/***/ 2292: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +const constants_1 = __nccwpck_require__(88); +class Push { + /** + * Initializes the Push + * + * @param channel The Channel + * @param event The event, for example `"phx_join"` + * @param payload The payload, for example `{user_id: 123}` + * @param timeout The push timeout in milliseconds + */ + constructor(channel, event, payload = {}, timeout = constants_1.DEFAULT_TIMEOUT) { + this.channel = channel; + this.event = event; + this.payload = payload; + this.timeout = timeout; + this.sent = false; + this.timeoutTimer = undefined; + this.ref = ''; + this.receivedResp = null; + this.recHooks = []; + this.refEvent = null; + } + resend(timeout) { + this.timeout = timeout; + this._cancelRefEvent(); + this.ref = ''; + this.refEvent = null; + this.receivedResp = null; + this.sent = false; + this.send(); + } + send() { + if (this._hasReceived('timeout')) { + return; + } + this.startTimeout(); + this.sent = true; + this.channel.socket.push({ + topic: this.channel.topic, + event: this.event, + payload: this.payload, + ref: this.ref, + join_ref: this.channel._joinRef(), + }); + } + updatePayload(payload) { + this.payload = Object.assign(Object.assign({}, this.payload), payload); + } + receive(status, callback) { + var _a; + if (this._hasReceived(status)) { + callback((_a = this.receivedResp) === null || _a === void 0 ? void 0 : _a.response); + } + this.recHooks.push({ status, callback }); + return this; + } + startTimeout() { + if (this.timeoutTimer) { + return; + } + this.ref = this.channel.socket._makeRef(); + this.refEvent = this.channel._replyEventName(this.ref); + const callback = (payload) => { + this._cancelRefEvent(); + this._cancelTimeout(); + this.receivedResp = payload; + this._matchReceive(payload); + }; + this.channel._on(this.refEvent, {}, callback); + this.timeoutTimer = setTimeout(() => { + this.trigger('timeout', {}); + }, this.timeout); + } + trigger(status, response) { + if (this.refEvent) + this.channel._trigger(this.refEvent, { status, response }); + } + destroy() { + this._cancelRefEvent(); + this._cancelTimeout(); + } + _cancelRefEvent() { + if (!this.refEvent) { + return; + } + this.channel._off(this.refEvent, {}); + } + _cancelTimeout() { + clearTimeout(this.timeoutTimer); + this.timeoutTimer = undefined; + } + _matchReceive({ status, response, }) { + this.recHooks + .filter((h) => h.status === status) + .forEach((h) => h.callback(response)); + } + _hasReceived(status) { + return this.receivedResp && this.receivedResp.status === status; + } +} +exports["default"] = Push; +//# sourceMappingURL=push.js.map + +/***/ }), + +/***/ 5360: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +// This file draws heavily from https://github.com/phoenixframework/phoenix/commit/cf098e9cf7a44ee6479d31d911a97d3c7430c6fe +// License: https://github.com/phoenixframework/phoenix/blob/master/LICENSE.md +Object.defineProperty(exports, "__esModule", ({ value: true })); +class Serializer { + constructor() { + this.HEADER_LENGTH = 1; + } + decode(rawPayload, callback) { + if (rawPayload.constructor === ArrayBuffer) { + return callback(this._binaryDecode(rawPayload)); + } + if (typeof rawPayload === 'string') { + return callback(JSON.parse(rawPayload)); + } + return callback({}); + } + _binaryDecode(buffer) { + const view = new DataView(buffer); + const decoder = new TextDecoder(); + return this._decodeBroadcast(buffer, view, decoder); + } + _decodeBroadcast(buffer, view, decoder) { + const topicSize = view.getUint8(1); + const eventSize = view.getUint8(2); + let offset = this.HEADER_LENGTH + 2; + const topic = decoder.decode(buffer.slice(offset, offset + topicSize)); + offset = offset + topicSize; + const event = decoder.decode(buffer.slice(offset, offset + eventSize)); + offset = offset + eventSize; + const data = JSON.parse(decoder.decode(buffer.slice(offset, buffer.byteLength))); + return { ref: null, topic: topic, event: event, payload: data }; + } +} +exports["default"] = Serializer; +//# sourceMappingURL=serializer.js.map + +/***/ }), + +/***/ 2983: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +/** + * Creates a timer that accepts a `timerCalc` function to perform calculated timeout retries, such as exponential backoff. + * + * @example + * let reconnectTimer = new Timer(() => this.connect(), function(tries){ + * return [1000, 5000, 10000][tries - 1] || 10000 + * }) + * reconnectTimer.scheduleTimeout() // fires after 1000 + * reconnectTimer.scheduleTimeout() // fires after 5000 + * reconnectTimer.reset() + * reconnectTimer.scheduleTimeout() // fires after 1000 + */ +class Timer { + constructor(callback, timerCalc) { + this.callback = callback; + this.timerCalc = timerCalc; + this.timer = undefined; + this.tries = 0; + this.callback = callback; + this.timerCalc = timerCalc; + } + reset() { + this.tries = 0; + clearTimeout(this.timer); + } + // Cancels any previous scheduleTimeout and schedules callback + scheduleTimeout() { + clearTimeout(this.timer); + this.timer = setTimeout(() => { + this.tries = this.tries + 1; + this.callback(); + }, this.timerCalc(this.tries + 1)); + } +} +exports["default"] = Timer; +//# sourceMappingURL=timer.js.map + +/***/ }), + +/***/ 1140: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +/** + * Helpers to convert the change Payload into native JS types. + */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.httpEndpointURL = exports.toTimestampString = exports.toArray = exports.toJson = exports.toNumber = exports.toBoolean = exports.convertCell = exports.convertColumn = exports.convertChangeData = exports.PostgresTypes = void 0; +// Adapted from epgsql (src/epgsql_binary.erl), this module licensed under +// 3-clause BSD found here: https://raw.githubusercontent.com/epgsql/epgsql/devel/LICENSE +var PostgresTypes; +(function (PostgresTypes) { + PostgresTypes["abstime"] = "abstime"; + PostgresTypes["bool"] = "bool"; + PostgresTypes["date"] = "date"; + PostgresTypes["daterange"] = "daterange"; + PostgresTypes["float4"] = "float4"; + PostgresTypes["float8"] = "float8"; + PostgresTypes["int2"] = "int2"; + PostgresTypes["int4"] = "int4"; + PostgresTypes["int4range"] = "int4range"; + PostgresTypes["int8"] = "int8"; + PostgresTypes["int8range"] = "int8range"; + PostgresTypes["json"] = "json"; + PostgresTypes["jsonb"] = "jsonb"; + PostgresTypes["money"] = "money"; + PostgresTypes["numeric"] = "numeric"; + PostgresTypes["oid"] = "oid"; + PostgresTypes["reltime"] = "reltime"; + PostgresTypes["text"] = "text"; + PostgresTypes["time"] = "time"; + PostgresTypes["timestamp"] = "timestamp"; + PostgresTypes["timestamptz"] = "timestamptz"; + PostgresTypes["timetz"] = "timetz"; + PostgresTypes["tsrange"] = "tsrange"; + PostgresTypes["tstzrange"] = "tstzrange"; +})(PostgresTypes = exports.PostgresTypes || (exports.PostgresTypes = {})); +/** + * Takes an array of columns and an object of string values then converts each string value + * to its mapped type. + * + * @param {{name: String, type: String}[]} columns + * @param {Object} record + * @param {Object} options The map of various options that can be applied to the mapper + * @param {Array} options.skipTypes The array of types that should not be converted + * + * @example convertChangeData([{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], {first_name: 'Paul', age:'33'}, {}) + * //=>{ first_name: 'Paul', age: 33 } + */ +const convertChangeData = (columns, record, options = {}) => { + var _a; + const skipTypes = (_a = options.skipTypes) !== null && _a !== void 0 ? _a : []; + return Object.keys(record).reduce((acc, rec_key) => { + acc[rec_key] = (0, exports.convertColumn)(rec_key, columns, record, skipTypes); + return acc; + }, {}); +}; +exports.convertChangeData = convertChangeData; +/** + * Converts the value of an individual column. + * + * @param {String} columnName The column that you want to convert + * @param {{name: String, type: String}[]} columns All of the columns + * @param {Object} record The map of string values + * @param {Array} skipTypes An array of types that should not be converted + * @return {object} Useless information + * + * @example convertColumn('age', [{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], {first_name: 'Paul', age: '33'}, []) + * //=> 33 + * @example convertColumn('age', [{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], {first_name: 'Paul', age: '33'}, ['int4']) + * //=> "33" + */ +const convertColumn = (columnName, columns, record, skipTypes) => { + const column = columns.find((x) => x.name === columnName); + const colType = column === null || column === void 0 ? void 0 : column.type; + const value = record[columnName]; + if (colType && !skipTypes.includes(colType)) { + return (0, exports.convertCell)(colType, value); + } + return noop(value); +}; +exports.convertColumn = convertColumn; +/** + * If the value of the cell is `null`, returns null. + * Otherwise converts the string value to the correct type. + * @param {String} type A postgres column type + * @param {String} value The cell value + * + * @example convertCell('bool', 't') + * //=> true + * @example convertCell('int8', '10') + * //=> 10 + * @example convertCell('_int4', '{1,2,3,4}') + * //=> [1,2,3,4] + */ +const convertCell = (type, value) => { + // if data type is an array + if (type.charAt(0) === '_') { + const dataType = type.slice(1, type.length); + return (0, exports.toArray)(value, dataType); + } + // If not null, convert to correct type. + switch (type) { + case PostgresTypes.bool: + return (0, exports.toBoolean)(value); + case PostgresTypes.float4: + case PostgresTypes.float8: + case PostgresTypes.int2: + case PostgresTypes.int4: + case PostgresTypes.int8: + case PostgresTypes.numeric: + case PostgresTypes.oid: + return (0, exports.toNumber)(value); + case PostgresTypes.json: + case PostgresTypes.jsonb: + return (0, exports.toJson)(value); + case PostgresTypes.timestamp: + return (0, exports.toTimestampString)(value); // Format to be consistent with PostgREST + case PostgresTypes.abstime: // To allow users to cast it based on Timezone + case PostgresTypes.date: // To allow users to cast it based on Timezone + case PostgresTypes.daterange: + case PostgresTypes.int4range: + case PostgresTypes.int8range: + case PostgresTypes.money: + case PostgresTypes.reltime: // To allow users to cast it based on Timezone + case PostgresTypes.text: + case PostgresTypes.time: // To allow users to cast it based on Timezone + case PostgresTypes.timestamptz: // To allow users to cast it based on Timezone + case PostgresTypes.timetz: // To allow users to cast it based on Timezone + case PostgresTypes.tsrange: + case PostgresTypes.tstzrange: + return noop(value); + default: + // Return the value for remaining types + return noop(value); + } +}; +exports.convertCell = convertCell; +const noop = (value) => { + return value; +}; +const toBoolean = (value) => { + switch (value) { + case 't': + return true; + case 'f': + return false; + default: + return value; + } +}; +exports.toBoolean = toBoolean; +const toNumber = (value) => { + if (typeof value === 'string') { + const parsedValue = parseFloat(value); + if (!Number.isNaN(parsedValue)) { + return parsedValue; + } + } + return value; +}; +exports.toNumber = toNumber; +const toJson = (value) => { + if (typeof value === 'string') { + try { + return JSON.parse(value); + } + catch (error) { + console.log(`JSON parse error: ${error}`); + return value; + } + } + return value; +}; +exports.toJson = toJson; +/** + * Converts a Postgres Array into a native JS array + * + * @example toArray('{}', 'int4') + * //=> [] + * @example toArray('{"[2021-01-01,2021-12-31)","(2021-01-01,2021-12-32]"}', 'daterange') + * //=> ['[2021-01-01,2021-12-31)', '(2021-01-01,2021-12-32]'] + * @example toArray([1,2,3,4], 'int4') + * //=> [1,2,3,4] + */ +const toArray = (value, type) => { + if (typeof value !== 'string') { + return value; + } + const lastIdx = value.length - 1; + const closeBrace = value[lastIdx]; + const openBrace = value[0]; + // Confirm value is a Postgres array by checking curly brackets + if (openBrace === '{' && closeBrace === '}') { + let arr; + const valTrim = value.slice(1, lastIdx); + // TODO: find a better solution to separate Postgres array data + try { + arr = JSON.parse('[' + valTrim + ']'); + } + catch (_) { + // WARNING: splitting on comma does not cover all edge cases + arr = valTrim ? valTrim.split(',') : []; + } + return arr.map((val) => (0, exports.convertCell)(type, val)); + } + return value; +}; +exports.toArray = toArray; +/** + * Fixes timestamp to be ISO-8601. Swaps the space between the date and time for a 'T' + * See https://github.com/supabase/supabase/issues/18 + * + * @example toTimestampString('2019-09-10 00:00:00') + * //=> '2019-09-10T00:00:00' + */ +const toTimestampString = (value) => { + if (typeof value === 'string') { + return value.replace(' ', 'T'); + } + return value; +}; +exports.toTimestampString = toTimestampString; +const httpEndpointURL = (socketUrl) => { + let url = socketUrl; + url = url.replace(/^ws/i, 'http'); + url = url.replace(/(\/socket\/websocket|\/socket|\/websocket)\/?$/i, ''); + return url.replace(/\/+$/, ''); +}; +exports.httpEndpointURL = httpEndpointURL; +//# sourceMappingURL=transformers.js.map + +/***/ }), + +/***/ 318: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.version = void 0; +exports.version = '2.11.2'; +//# sourceMappingURL=version.js.map + +/***/ }), + +/***/ 906: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const WebSocket = __nccwpck_require__(8393); + +WebSocket.createWebSocketStream = __nccwpck_require__(1677); +WebSocket.Server = __nccwpck_require__(2806); +WebSocket.Receiver = __nccwpck_require__(5934); +WebSocket.Sender = __nccwpck_require__(3721); + +WebSocket.WebSocket = WebSocket; +WebSocket.WebSocketServer = WebSocket.Server; + +module.exports = WebSocket; + + +/***/ }), + +/***/ 5533: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { EMPTY_BUFFER } = __nccwpck_require__(5350); + +const FastBuffer = Buffer[Symbol.species]; + +/** + * Merges an array of buffers into a new buffer. + * + * @param {Buffer[]} list The array of buffers to concat + * @param {Number} totalLength The total length of buffers in the list + * @return {Buffer} The resulting buffer + * @public + */ +function concat(list, totalLength) { + if (list.length === 0) return EMPTY_BUFFER; + if (list.length === 1) return list[0]; + + const target = Buffer.allocUnsafe(totalLength); + let offset = 0; + + for (let i = 0; i < list.length; i++) { + const buf = list[i]; + target.set(buf, offset); + offset += buf.length; + } + + if (offset < totalLength) { + return new FastBuffer(target.buffer, target.byteOffset, offset); + } + + return target; +} + +/** + * Masks a buffer using the given mask. + * + * @param {Buffer} source The buffer to mask + * @param {Buffer} mask The mask to use + * @param {Buffer} output The buffer where to store the result + * @param {Number} offset The offset at which to start writing + * @param {Number} length The number of bytes to mask. + * @public + */ +function _mask(source, mask, output, offset, length) { + for (let i = 0; i < length; i++) { + output[offset + i] = source[i] ^ mask[i & 3]; + } +} + +/** + * Unmasks a buffer using the given mask. + * + * @param {Buffer} buffer The buffer to unmask + * @param {Buffer} mask The mask to use + * @public + */ +function _unmask(buffer, mask) { + for (let i = 0; i < buffer.length; i++) { + buffer[i] ^= mask[i & 3]; + } +} + +/** + * Converts a buffer to an `ArrayBuffer`. + * + * @param {Buffer} buf The buffer to convert + * @return {ArrayBuffer} Converted buffer + * @public + */ +function toArrayBuffer(buf) { + if (buf.length === buf.buffer.byteLength) { + return buf.buffer; + } + + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.length); +} + +/** + * Converts `data` to a `Buffer`. + * + * @param {*} data The data to convert + * @return {Buffer} The buffer + * @throws {TypeError} + * @public + */ +function toBuffer(data) { + toBuffer.readOnly = true; + + if (Buffer.isBuffer(data)) return data; + + let buf; + + if (data instanceof ArrayBuffer) { + buf = new FastBuffer(data); + } else if (ArrayBuffer.isView(data)) { + buf = new FastBuffer(data.buffer, data.byteOffset, data.byteLength); + } else { + buf = Buffer.from(data); + toBuffer.readOnly = false; + } + + return buf; +} + +module.exports = { + concat, + mask: _mask, + toArrayBuffer, + toBuffer, + unmask: _unmask +}; + +/* istanbul ignore else */ +if (!process.env.WS_NO_BUFFER_UTIL) { + try { + const bufferUtil = __nccwpck_require__(1269); + + module.exports.mask = function (source, mask, output, offset, length) { + if (length < 48) _mask(source, mask, output, offset, length); + else bufferUtil.mask(source, mask, output, offset, length); + }; + + module.exports.unmask = function (buffer, mask) { + if (buffer.length < 32) _unmask(buffer, mask); + else bufferUtil.unmask(buffer, mask); + }; + } catch (e) { + // Continue regardless of the error. + } +} + + +/***/ }), + +/***/ 5350: +/***/ ((module) => { + +"use strict"; + + +const BINARY_TYPES = ['nodebuffer', 'arraybuffer', 'fragments']; +const hasBlob = typeof Blob !== 'undefined'; + +if (hasBlob) BINARY_TYPES.push('blob'); + +module.exports = { + BINARY_TYPES, + EMPTY_BUFFER: Buffer.alloc(0), + GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', + hasBlob, + kForOnEventAttribute: Symbol('kIsForOnEventAttribute'), + kListener: Symbol('kListener'), + kStatusCode: Symbol('status-code'), + kWebSocket: Symbol('websocket'), + NOOP: () => {} +}; + + +/***/ }), + +/***/ 9593: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kForOnEventAttribute, kListener } = __nccwpck_require__(5350); + +const kCode = Symbol('kCode'); +const kData = Symbol('kData'); +const kError = Symbol('kError'); +const kMessage = Symbol('kMessage'); +const kReason = Symbol('kReason'); +const kTarget = Symbol('kTarget'); +const kType = Symbol('kType'); +const kWasClean = Symbol('kWasClean'); + +/** + * Class representing an event. + */ +class Event { + /** + * Create a new `Event`. + * + * @param {String} type The name of the event + * @throws {TypeError} If the `type` argument is not specified + */ + constructor(type) { + this[kTarget] = null; + this[kType] = type; + } + + /** + * @type {*} + */ + get target() { + return this[kTarget]; + } + + /** + * @type {String} + */ + get type() { + return this[kType]; + } +} + +Object.defineProperty(Event.prototype, 'target', { enumerable: true }); +Object.defineProperty(Event.prototype, 'type', { enumerable: true }); + +/** + * Class representing a close event. + * + * @extends Event + */ +class CloseEvent extends Event { + /** + * Create a new `CloseEvent`. + * + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {Number} [options.code=0] The status code explaining why the + * connection was closed + * @param {String} [options.reason=''] A human-readable string explaining why + * the connection was closed + * @param {Boolean} [options.wasClean=false] Indicates whether or not the + * connection was cleanly closed + */ + constructor(type, options = {}) { + super(type); + + this[kCode] = options.code === undefined ? 0 : options.code; + this[kReason] = options.reason === undefined ? '' : options.reason; + this[kWasClean] = options.wasClean === undefined ? false : options.wasClean; + } + + /** + * @type {Number} + */ + get code() { + return this[kCode]; + } + + /** + * @type {String} + */ + get reason() { + return this[kReason]; + } + + /** + * @type {Boolean} + */ + get wasClean() { + return this[kWasClean]; + } +} + +Object.defineProperty(CloseEvent.prototype, 'code', { enumerable: true }); +Object.defineProperty(CloseEvent.prototype, 'reason', { enumerable: true }); +Object.defineProperty(CloseEvent.prototype, 'wasClean', { enumerable: true }); + +/** + * Class representing an error event. + * + * @extends Event + */ +class ErrorEvent extends Event { + /** + * Create a new `ErrorEvent`. + * + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {*} [options.error=null] The error that generated this event + * @param {String} [options.message=''] The error message + */ + constructor(type, options = {}) { + super(type); + + this[kError] = options.error === undefined ? null : options.error; + this[kMessage] = options.message === undefined ? '' : options.message; + } + + /** + * @type {*} + */ + get error() { + return this[kError]; + } + + /** + * @type {String} + */ + get message() { + return this[kMessage]; + } +} + +Object.defineProperty(ErrorEvent.prototype, 'error', { enumerable: true }); +Object.defineProperty(ErrorEvent.prototype, 'message', { enumerable: true }); + +/** + * Class representing a message event. + * + * @extends Event + */ +class MessageEvent extends Event { + /** + * Create a new `MessageEvent`. + * + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {*} [options.data=null] The message content + */ + constructor(type, options = {}) { + super(type); + + this[kData] = options.data === undefined ? null : options.data; + } + + /** + * @type {*} + */ + get data() { + return this[kData]; + } +} + +Object.defineProperty(MessageEvent.prototype, 'data', { enumerable: true }); + +/** + * This provides methods for emulating the `EventTarget` interface. It's not + * meant to be used directly. + * + * @mixin + */ +const EventTarget = { + /** + * Register an event listener. + * + * @param {String} type A string representing the event type to listen for + * @param {(Function|Object)} handler The listener to add + * @param {Object} [options] An options object specifies characteristics about + * the event listener + * @param {Boolean} [options.once=false] A `Boolean` indicating that the + * listener should be invoked at most once after being added. If `true`, + * the listener would be automatically removed when invoked. + * @public + */ + addEventListener(type, handler, options = {}) { + for (const listener of this.listeners(type)) { + if ( + !options[kForOnEventAttribute] && + listener[kListener] === handler && + !listener[kForOnEventAttribute] + ) { + return; + } + } + + let wrapper; + + if (type === 'message') { + wrapper = function onMessage(data, isBinary) { + const event = new MessageEvent('message', { + data: isBinary ? data : data.toString() + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; + } else if (type === 'close') { + wrapper = function onClose(code, message) { + const event = new CloseEvent('close', { + code, + reason: message.toString(), + wasClean: this._closeFrameReceived && this._closeFrameSent + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; + } else if (type === 'error') { + wrapper = function onError(error) { + const event = new ErrorEvent('error', { + error, + message: error.message + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; + } else if (type === 'open') { + wrapper = function onOpen() { + const event = new Event('open'); + + event[kTarget] = this; + callListener(handler, this, event); + }; + } else { + return; + } + + wrapper[kForOnEventAttribute] = !!options[kForOnEventAttribute]; + wrapper[kListener] = handler; + + if (options.once) { + this.once(type, wrapper); + } else { + this.on(type, wrapper); + } + }, + + /** + * Remove an event listener. + * + * @param {String} type A string representing the event type to remove + * @param {(Function|Object)} handler The listener to remove + * @public + */ + removeEventListener(type, handler) { + for (const listener of this.listeners(type)) { + if (listener[kListener] === handler && !listener[kForOnEventAttribute]) { + this.removeListener(type, listener); + break; + } + } + } +}; + +module.exports = { + CloseEvent, + ErrorEvent, + Event, + EventTarget, + MessageEvent +}; + +/** + * Call an event listener + * + * @param {(Function|Object)} listener The listener to call + * @param {*} thisArg The value to use as `this`` when calling the listener + * @param {Event} event The event to pass to the listener + * @private + */ +function callListener(listener, thisArg, event) { + if (typeof listener === 'object' && listener.handleEvent) { + listener.handleEvent.call(listener, event); + } else { + listener.call(thisArg, event); + } +} + + +/***/ }), + +/***/ 2118: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { tokenChars } = __nccwpck_require__(2444); + +/** + * Adds an offer to the map of extension offers or a parameter to the map of + * parameters. + * + * @param {Object} dest The map of extension offers or parameters + * @param {String} name The extension or parameter name + * @param {(Object|Boolean|String)} elem The extension parameters or the + * parameter value + * @private + */ +function push(dest, name, elem) { + if (dest[name] === undefined) dest[name] = [elem]; + else dest[name].push(elem); +} + +/** + * Parses the `Sec-WebSocket-Extensions` header into an object. + * + * @param {String} header The field value of the header + * @return {Object} The parsed object + * @public + */ +function parse(header) { + const offers = Object.create(null); + let params = Object.create(null); + let mustUnescape = false; + let isEscaping = false; + let inQuotes = false; + let extensionName; + let paramName; + let start = -1; + let code = -1; + let end = -1; + let i = 0; + + for (; i < header.length; i++) { + code = header.charCodeAt(i); + + if (extensionName === undefined) { + if (end === -1 && tokenChars[code] === 1) { + if (start === -1) start = i; + } else if ( + i !== 0 && + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ + ) { + if (end === -1 && start !== -1) end = i; + } else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + const name = header.slice(start, end); + if (code === 0x2c) { + push(offers, name, params); + params = Object.create(null); + } else { + extensionName = name; + } + + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } else if (paramName === undefined) { + if (end === -1 && tokenChars[code] === 1) { + if (start === -1) start = i; + } else if (code === 0x20 || code === 0x09) { + if (end === -1 && start !== -1) end = i; + } else if (code === 0x3b || code === 0x2c) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + push(params, header.slice(start, end), true); + if (code === 0x2c) { + push(offers, extensionName, params); + params = Object.create(null); + extensionName = undefined; + } + + start = end = -1; + } else if (code === 0x3d /* '=' */ && start !== -1 && end === -1) { + paramName = header.slice(start, i); + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } else { + // + // The value of a quoted-string after unescaping must conform to the + // token ABNF, so only token characters are valid. + // Ref: https://tools.ietf.org/html/rfc6455#section-9.1 + // + if (isEscaping) { + if (tokenChars[code] !== 1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + if (start === -1) start = i; + else if (!mustUnescape) mustUnescape = true; + isEscaping = false; + } else if (inQuotes) { + if (tokenChars[code] === 1) { + if (start === -1) start = i; + } else if (code === 0x22 /* '"' */ && start !== -1) { + inQuotes = false; + end = i; + } else if (code === 0x5c /* '\' */) { + isEscaping = true; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) { + inQuotes = true; + } else if (end === -1 && tokenChars[code] === 1) { + if (start === -1) start = i; + } else if (start !== -1 && (code === 0x20 || code === 0x09)) { + if (end === -1) end = i; + } else if (code === 0x3b || code === 0x2c) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + let value = header.slice(start, end); + if (mustUnescape) { + value = value.replace(/\\/g, ''); + mustUnescape = false; + } + push(params, paramName, value); + if (code === 0x2c) { + push(offers, extensionName, params); + params = Object.create(null); + extensionName = undefined; + } + + paramName = undefined; + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } + } + + if (start === -1 || inQuotes || code === 0x20 || code === 0x09) { + throw new SyntaxError('Unexpected end of input'); + } + + if (end === -1) end = i; + const token = header.slice(start, end); + if (extensionName === undefined) { + push(offers, token, params); + } else { + if (paramName === undefined) { + push(params, token, true); + } else if (mustUnescape) { + push(params, paramName, token.replace(/\\/g, '')); + } else { + push(params, paramName, token); + } + push(offers, extensionName, params); + } + + return offers; +} + +/** + * Builds the `Sec-WebSocket-Extensions` header field value. + * + * @param {Object} extensions The map of extensions and parameters to format + * @return {String} A string representing the given object + * @public + */ +function format(extensions) { + return Object.keys(extensions) + .map((extension) => { + let configurations = extensions[extension]; + if (!Array.isArray(configurations)) configurations = [configurations]; + return configurations + .map((params) => { + return [extension] + .concat( + Object.keys(params).map((k) => { + let values = params[k]; + if (!Array.isArray(values)) values = [values]; + return values + .map((v) => (v === true ? k : `${k}=${v}`)) + .join('; '); + }) + ) + .join('; '); + }) + .join(', '); + }) + .join(', '); +} + +module.exports = { format, parse }; + + +/***/ }), + +/***/ 9050: +/***/ ((module) => { + +"use strict"; + + +const kDone = Symbol('kDone'); +const kRun = Symbol('kRun'); + +/** + * A very simple job queue with adjustable concurrency. Adapted from + * https://github.com/STRML/async-limiter + */ +class Limiter { + /** + * Creates a new `Limiter`. + * + * @param {Number} [concurrency=Infinity] The maximum number of jobs allowed + * to run concurrently + */ + constructor(concurrency) { + this[kDone] = () => { + this.pending--; + this[kRun](); + }; + this.concurrency = concurrency || Infinity; + this.jobs = []; + this.pending = 0; + } + + /** + * Adds a job to the queue. + * + * @param {Function} job The job to run + * @public + */ + add(job) { + this.jobs.push(job); + this[kRun](); + } + + /** + * Removes a job from the queue and runs it if possible. + * + * @private + */ + [kRun]() { + if (this.pending === this.concurrency) return; + + if (this.jobs.length) { + const job = this.jobs.shift(); + + this.pending++; + job(this[kDone]); + } + } +} + +module.exports = Limiter; + + +/***/ }), + +/***/ 6447: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const zlib = __nccwpck_require__(9796); + +const bufferUtil = __nccwpck_require__(5533); +const Limiter = __nccwpck_require__(9050); +const { kStatusCode } = __nccwpck_require__(5350); + +const FastBuffer = Buffer[Symbol.species]; +const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); +const kPerMessageDeflate = Symbol('permessage-deflate'); +const kTotalLength = Symbol('total-length'); +const kCallback = Symbol('callback'); +const kBuffers = Symbol('buffers'); +const kError = Symbol('error'); + +// +// We limit zlib concurrency, which prevents severe memory fragmentation +// as documented in https://github.com/nodejs/node/issues/8871#issuecomment-250915913 +// and https://github.com/websockets/ws/issues/1202 +// +// Intentionally global; it's the global thread pool that's an issue. +// +let zlibLimiter; + +/** + * permessage-deflate implementation. + */ +class PerMessageDeflate { + /** + * Creates a PerMessageDeflate instance. + * + * @param {Object} [options] Configuration options + * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support + * for, or request, a custom client window size + * @param {Boolean} [options.clientNoContextTakeover=false] Advertise/ + * acknowledge disabling of client context takeover + * @param {Number} [options.concurrencyLimit=10] The number of concurrent + * calls to zlib + * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the + * use of a custom server window size + * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept + * disabling of server context takeover + * @param {Number} [options.threshold=1024] Size (in bytes) below which + * messages should not be compressed if context takeover is disabled + * @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on + * deflate + * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on + * inflate + * @param {Boolean} [isServer=false] Create the instance in either server or + * client mode + * @param {Number} [maxPayload=0] The maximum allowed message length + */ + constructor(options, isServer, maxPayload) { + this._maxPayload = maxPayload | 0; + this._options = options || {}; + this._threshold = + this._options.threshold !== undefined ? this._options.threshold : 1024; + this._isServer = !!isServer; + this._deflate = null; + this._inflate = null; + + this.params = null; + + if (!zlibLimiter) { + const concurrency = + this._options.concurrencyLimit !== undefined + ? this._options.concurrencyLimit + : 10; + zlibLimiter = new Limiter(concurrency); + } + } + + /** + * @type {String} + */ + static get extensionName() { + return 'permessage-deflate'; + } + + /** + * Create an extension negotiation offer. + * + * @return {Object} Extension parameters + * @public + */ + offer() { + const params = {}; + + if (this._options.serverNoContextTakeover) { + params.server_no_context_takeover = true; + } + if (this._options.clientNoContextTakeover) { + params.client_no_context_takeover = true; + } + if (this._options.serverMaxWindowBits) { + params.server_max_window_bits = this._options.serverMaxWindowBits; + } + if (this._options.clientMaxWindowBits) { + params.client_max_window_bits = this._options.clientMaxWindowBits; + } else if (this._options.clientMaxWindowBits == null) { + params.client_max_window_bits = true; + } + + return params; + } + + /** + * Accept an extension negotiation offer/response. + * + * @param {Array} configurations The extension negotiation offers/reponse + * @return {Object} Accepted configuration + * @public + */ + accept(configurations) { + configurations = this.normalizeParams(configurations); + + this.params = this._isServer + ? this.acceptAsServer(configurations) + : this.acceptAsClient(configurations); + + return this.params; + } + + /** + * Releases all resources used by the extension. + * + * @public + */ + cleanup() { + if (this._inflate) { + this._inflate.close(); + this._inflate = null; + } + + if (this._deflate) { + const callback = this._deflate[kCallback]; + + this._deflate.close(); + this._deflate = null; + + if (callback) { + callback( + new Error( + 'The deflate stream was closed while data was being processed' + ) + ); + } + } + } + + /** + * Accept an extension negotiation offer. + * + * @param {Array} offers The extension negotiation offers + * @return {Object} Accepted configuration + * @private + */ + acceptAsServer(offers) { + const opts = this._options; + const accepted = offers.find((params) => { + if ( + (opts.serverNoContextTakeover === false && + params.server_no_context_takeover) || + (params.server_max_window_bits && + (opts.serverMaxWindowBits === false || + (typeof opts.serverMaxWindowBits === 'number' && + opts.serverMaxWindowBits > params.server_max_window_bits))) || + (typeof opts.clientMaxWindowBits === 'number' && + !params.client_max_window_bits) + ) { + return false; + } + + return true; + }); + + if (!accepted) { + throw new Error('None of the extension offers can be accepted'); + } + + if (opts.serverNoContextTakeover) { + accepted.server_no_context_takeover = true; + } + if (opts.clientNoContextTakeover) { + accepted.client_no_context_takeover = true; + } + if (typeof opts.serverMaxWindowBits === 'number') { + accepted.server_max_window_bits = opts.serverMaxWindowBits; + } + if (typeof opts.clientMaxWindowBits === 'number') { + accepted.client_max_window_bits = opts.clientMaxWindowBits; + } else if ( + accepted.client_max_window_bits === true || + opts.clientMaxWindowBits === false + ) { + delete accepted.client_max_window_bits; + } + + return accepted; + } + + /** + * Accept the extension negotiation response. + * + * @param {Array} response The extension negotiation response + * @return {Object} Accepted configuration + * @private + */ + acceptAsClient(response) { + const params = response[0]; + + if ( + this._options.clientNoContextTakeover === false && + params.client_no_context_takeover + ) { + throw new Error('Unexpected parameter "client_no_context_takeover"'); + } + + if (!params.client_max_window_bits) { + if (typeof this._options.clientMaxWindowBits === 'number') { + params.client_max_window_bits = this._options.clientMaxWindowBits; + } + } else if ( + this._options.clientMaxWindowBits === false || + (typeof this._options.clientMaxWindowBits === 'number' && + params.client_max_window_bits > this._options.clientMaxWindowBits) + ) { + throw new Error( + 'Unexpected or invalid parameter "client_max_window_bits"' + ); + } + + return params; + } + + /** + * Normalize parameters. + * + * @param {Array} configurations The extension negotiation offers/reponse + * @return {Array} The offers/response with normalized parameters + * @private + */ + normalizeParams(configurations) { + configurations.forEach((params) => { + Object.keys(params).forEach((key) => { + let value = params[key]; + + if (value.length > 1) { + throw new Error(`Parameter "${key}" must have only a single value`); + } + + value = value[0]; + + if (key === 'client_max_window_bits') { + if (value !== true) { + const num = +value; + if (!Number.isInteger(num) || num < 8 || num > 15) { + throw new TypeError( + `Invalid value for parameter "${key}": ${value}` + ); + } + value = num; + } else if (!this._isServer) { + throw new TypeError( + `Invalid value for parameter "${key}": ${value}` + ); + } + } else if (key === 'server_max_window_bits') { + const num = +value; + if (!Number.isInteger(num) || num < 8 || num > 15) { + throw new TypeError( + `Invalid value for parameter "${key}": ${value}` + ); + } + value = num; + } else if ( + key === 'client_no_context_takeover' || + key === 'server_no_context_takeover' + ) { + if (value !== true) { + throw new TypeError( + `Invalid value for parameter "${key}": ${value}` + ); + } + } else { + throw new Error(`Unknown parameter "${key}"`); + } + + params[key] = value; + }); + }); + + return configurations; + } + + /** + * Decompress data. Concurrency limited. + * + * @param {Buffer} data Compressed data + * @param {Boolean} fin Specifies whether or not this is the last fragment + * @param {Function} callback Callback + * @public + */ + decompress(data, fin, callback) { + zlibLimiter.add((done) => { + this._decompress(data, fin, (err, result) => { + done(); + callback(err, result); + }); + }); + } + + /** + * Compress data. Concurrency limited. + * + * @param {(Buffer|String)} data Data to compress + * @param {Boolean} fin Specifies whether or not this is the last fragment + * @param {Function} callback Callback + * @public + */ + compress(data, fin, callback) { + zlibLimiter.add((done) => { + this._compress(data, fin, (err, result) => { + done(); + callback(err, result); + }); + }); + } + + /** + * Decompress data. + * + * @param {Buffer} data Compressed data + * @param {Boolean} fin Specifies whether or not this is the last fragment + * @param {Function} callback Callback + * @private + */ + _decompress(data, fin, callback) { + const endpoint = this._isServer ? 'client' : 'server'; + + if (!this._inflate) { + const key = `${endpoint}_max_window_bits`; + const windowBits = + typeof this.params[key] !== 'number' + ? zlib.Z_DEFAULT_WINDOWBITS + : this.params[key]; + + this._inflate = zlib.createInflateRaw({ + ...this._options.zlibInflateOptions, + windowBits + }); + this._inflate[kPerMessageDeflate] = this; + this._inflate[kTotalLength] = 0; + this._inflate[kBuffers] = []; + this._inflate.on('error', inflateOnError); + this._inflate.on('data', inflateOnData); + } + + this._inflate[kCallback] = callback; + + this._inflate.write(data); + if (fin) this._inflate.write(TRAILER); + + this._inflate.flush(() => { + const err = this._inflate[kError]; + + if (err) { + this._inflate.close(); + this._inflate = null; + callback(err); + return; + } + + const data = bufferUtil.concat( + this._inflate[kBuffers], + this._inflate[kTotalLength] + ); + + if (this._inflate._readableState.endEmitted) { + this._inflate.close(); + this._inflate = null; + } else { + this._inflate[kTotalLength] = 0; + this._inflate[kBuffers] = []; + + if (fin && this.params[`${endpoint}_no_context_takeover`]) { + this._inflate.reset(); + } + } + + callback(null, data); + }); + } + + /** + * Compress data. + * + * @param {(Buffer|String)} data Data to compress + * @param {Boolean} fin Specifies whether or not this is the last fragment + * @param {Function} callback Callback + * @private + */ + _compress(data, fin, callback) { + const endpoint = this._isServer ? 'server' : 'client'; + + if (!this._deflate) { + const key = `${endpoint}_max_window_bits`; + const windowBits = + typeof this.params[key] !== 'number' + ? zlib.Z_DEFAULT_WINDOWBITS + : this.params[key]; + + this._deflate = zlib.createDeflateRaw({ + ...this._options.zlibDeflateOptions, + windowBits + }); + + this._deflate[kTotalLength] = 0; + this._deflate[kBuffers] = []; + + this._deflate.on('data', deflateOnData); + } + + this._deflate[kCallback] = callback; + + this._deflate.write(data); + this._deflate.flush(zlib.Z_SYNC_FLUSH, () => { + if (!this._deflate) { + // + // The deflate stream was closed while data was being processed. + // + return; + } + + let data = bufferUtil.concat( + this._deflate[kBuffers], + this._deflate[kTotalLength] + ); + + if (fin) { + data = new FastBuffer(data.buffer, data.byteOffset, data.length - 4); + } + + // + // Ensure that the callback will not be called again in + // `PerMessageDeflate#cleanup()`. + // + this._deflate[kCallback] = null; + + this._deflate[kTotalLength] = 0; + this._deflate[kBuffers] = []; + + if (fin && this.params[`${endpoint}_no_context_takeover`]) { + this._deflate.reset(); + } + + callback(null, data); + }); + } +} + +module.exports = PerMessageDeflate; + +/** + * The listener of the `zlib.DeflateRaw` stream `'data'` event. + * + * @param {Buffer} chunk A chunk of data + * @private + */ +function deflateOnData(chunk) { + this[kBuffers].push(chunk); + this[kTotalLength] += chunk.length; +} + +/** + * The listener of the `zlib.InflateRaw` stream `'data'` event. + * + * @param {Buffer} chunk A chunk of data + * @private + */ +function inflateOnData(chunk) { + this[kTotalLength] += chunk.length; + + if ( + this[kPerMessageDeflate]._maxPayload < 1 || + this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload + ) { + this[kBuffers].push(chunk); + return; + } + + this[kError] = new RangeError('Max payload size exceeded'); + this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'; + this[kError][kStatusCode] = 1009; + this.removeListener('data', inflateOnData); + this.reset(); +} + +/** + * The listener of the `zlib.InflateRaw` stream `'error'` event. + * + * @param {Error} err The emitted error + * @private + */ +function inflateOnError(err) { + // + // There is no need to call `Zlib#close()` as the handle is automatically + // closed when an error is emitted. + // + this[kPerMessageDeflate]._inflate = null; + err[kStatusCode] = 1007; + this[kCallback](err); +} + + +/***/ }), + +/***/ 5934: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { Writable } = __nccwpck_require__(2781); + +const PerMessageDeflate = __nccwpck_require__(6447); +const { + BINARY_TYPES, + EMPTY_BUFFER, + kStatusCode, + kWebSocket +} = __nccwpck_require__(5350); +const { concat, toArrayBuffer, unmask } = __nccwpck_require__(5533); +const { isValidStatusCode, isValidUTF8 } = __nccwpck_require__(2444); + +const FastBuffer = Buffer[Symbol.species]; + +const GET_INFO = 0; +const GET_PAYLOAD_LENGTH_16 = 1; +const GET_PAYLOAD_LENGTH_64 = 2; +const GET_MASK = 3; +const GET_DATA = 4; +const INFLATING = 5; +const DEFER_EVENT = 6; + +/** + * HyBi Receiver implementation. + * + * @extends Writable + */ +class Receiver extends Writable { + /** + * Creates a Receiver instance. + * + * @param {Object} [options] Options object + * @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether + * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted + * multiple times in the same tick + * @param {String} [options.binaryType=nodebuffer] The type for binary data + * @param {Object} [options.extensions] An object containing the negotiated + * extensions + * @param {Boolean} [options.isServer=false] Specifies whether to operate in + * client or server mode + * @param {Number} [options.maxPayload=0] The maximum allowed message length + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages + */ + constructor(options = {}) { + super(); + + this._allowSynchronousEvents = + options.allowSynchronousEvents !== undefined + ? options.allowSynchronousEvents + : true; + this._binaryType = options.binaryType || BINARY_TYPES[0]; + this._extensions = options.extensions || {}; + this._isServer = !!options.isServer; + this._maxPayload = options.maxPayload | 0; + this._skipUTF8Validation = !!options.skipUTF8Validation; + this[kWebSocket] = undefined; + + this._bufferedBytes = 0; + this._buffers = []; + + this._compressed = false; + this._payloadLength = 0; + this._mask = undefined; + this._fragmented = 0; + this._masked = false; + this._fin = false; + this._opcode = 0; + + this._totalPayloadLength = 0; + this._messageLength = 0; + this._fragments = []; + + this._errored = false; + this._loop = false; + this._state = GET_INFO; + } + + /** + * Implements `Writable.prototype._write()`. + * + * @param {Buffer} chunk The chunk of data to write + * @param {String} encoding The character encoding of `chunk` + * @param {Function} cb Callback + * @private + */ + _write(chunk, encoding, cb) { + if (this._opcode === 0x08 && this._state == GET_INFO) return cb(); + + this._bufferedBytes += chunk.length; + this._buffers.push(chunk); + this.startLoop(cb); + } + + /** + * Consumes `n` bytes from the buffered data. + * + * @param {Number} n The number of bytes to consume + * @return {Buffer} The consumed bytes + * @private + */ + consume(n) { + this._bufferedBytes -= n; + + if (n === this._buffers[0].length) return this._buffers.shift(); + + if (n < this._buffers[0].length) { + const buf = this._buffers[0]; + this._buffers[0] = new FastBuffer( + buf.buffer, + buf.byteOffset + n, + buf.length - n + ); + + return new FastBuffer(buf.buffer, buf.byteOffset, n); + } + + const dst = Buffer.allocUnsafe(n); + + do { + const buf = this._buffers[0]; + const offset = dst.length - n; + + if (n >= buf.length) { + dst.set(this._buffers.shift(), offset); + } else { + dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset); + this._buffers[0] = new FastBuffer( + buf.buffer, + buf.byteOffset + n, + buf.length - n + ); + } + + n -= buf.length; + } while (n > 0); + + return dst; + } + + /** + * Starts the parsing loop. + * + * @param {Function} cb Callback + * @private + */ + startLoop(cb) { + this._loop = true; + + do { + switch (this._state) { + case GET_INFO: + this.getInfo(cb); + break; + case GET_PAYLOAD_LENGTH_16: + this.getPayloadLength16(cb); + break; + case GET_PAYLOAD_LENGTH_64: + this.getPayloadLength64(cb); + break; + case GET_MASK: + this.getMask(); + break; + case GET_DATA: + this.getData(cb); + break; + case INFLATING: + case DEFER_EVENT: + this._loop = false; + return; + } + } while (this._loop); + + if (!this._errored) cb(); + } + + /** + * Reads the first two bytes of a frame. + * + * @param {Function} cb Callback + * @private + */ + getInfo(cb) { + if (this._bufferedBytes < 2) { + this._loop = false; + return; + } + + const buf = this.consume(2); + + if ((buf[0] & 0x30) !== 0x00) { + const error = this.createError( + RangeError, + 'RSV2 and RSV3 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_2_3' + ); + + cb(error); + return; + } + + const compressed = (buf[0] & 0x40) === 0x40; + + if (compressed && !this._extensions[PerMessageDeflate.extensionName]) { + const error = this.createError( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); + + cb(error); + return; + } + + this._fin = (buf[0] & 0x80) === 0x80; + this._opcode = buf[0] & 0x0f; + this._payloadLength = buf[1] & 0x7f; + + if (this._opcode === 0x00) { + if (compressed) { + const error = this.createError( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); + + cb(error); + return; + } + + if (!this._fragmented) { + const error = this.createError( + RangeError, + 'invalid opcode 0', + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); + + cb(error); + return; + } + + this._opcode = this._fragmented; + } else if (this._opcode === 0x01 || this._opcode === 0x02) { + if (this._fragmented) { + const error = this.createError( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); + + cb(error); + return; + } + + this._compressed = compressed; + } else if (this._opcode > 0x07 && this._opcode < 0x0b) { + if (!this._fin) { + const error = this.createError( + RangeError, + 'FIN must be set', + true, + 1002, + 'WS_ERR_EXPECTED_FIN' + ); + + cb(error); + return; + } + + if (compressed) { + const error = this.createError( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); + + cb(error); + return; + } + + if ( + this._payloadLength > 0x7d || + (this._opcode === 0x08 && this._payloadLength === 1) + ) { + const error = this.createError( + RangeError, + `invalid payload length ${this._payloadLength}`, + true, + 1002, + 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' + ); + + cb(error); + return; + } + } else { + const error = this.createError( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); + + cb(error); + return; + } + + if (!this._fin && !this._fragmented) this._fragmented = this._opcode; + this._masked = (buf[1] & 0x80) === 0x80; + + if (this._isServer) { + if (!this._masked) { + const error = this.createError( + RangeError, + 'MASK must be set', + true, + 1002, + 'WS_ERR_EXPECTED_MASK' + ); + + cb(error); + return; + } + } else if (this._masked) { + const error = this.createError( + RangeError, + 'MASK must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_MASK' + ); + + cb(error); + return; + } + + if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16; + else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64; + else this.haveLength(cb); + } + + /** + * Gets extended payload length (7+16). + * + * @param {Function} cb Callback + * @private + */ + getPayloadLength16(cb) { + if (this._bufferedBytes < 2) { + this._loop = false; + return; + } + + this._payloadLength = this.consume(2).readUInt16BE(0); + this.haveLength(cb); + } + + /** + * Gets extended payload length (7+64). + * + * @param {Function} cb Callback + * @private + */ + getPayloadLength64(cb) { + if (this._bufferedBytes < 8) { + this._loop = false; + return; + } + + const buf = this.consume(8); + const num = buf.readUInt32BE(0); + + // + // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned + // if payload length is greater than this number. + // + if (num > Math.pow(2, 53 - 32) - 1) { + const error = this.createError( + RangeError, + 'Unsupported WebSocket frame: payload length > 2^53 - 1', + false, + 1009, + 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH' + ); + + cb(error); + return; + } + + this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4); + this.haveLength(cb); + } + + /** + * Payload length has been read. + * + * @param {Function} cb Callback + * @private + */ + haveLength(cb) { + if (this._payloadLength && this._opcode < 0x08) { + this._totalPayloadLength += this._payloadLength; + if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { + const error = this.createError( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ); + + cb(error); + return; + } + } + + if (this._masked) this._state = GET_MASK; + else this._state = GET_DATA; + } + + /** + * Reads mask bytes. + * + * @private + */ + getMask() { + if (this._bufferedBytes < 4) { + this._loop = false; + return; + } + + this._mask = this.consume(4); + this._state = GET_DATA; + } + + /** + * Reads data bytes. + * + * @param {Function} cb Callback + * @private + */ + getData(cb) { + let data = EMPTY_BUFFER; + + if (this._payloadLength) { + if (this._bufferedBytes < this._payloadLength) { + this._loop = false; + return; + } + + data = this.consume(this._payloadLength); + + if ( + this._masked && + (this._mask[0] | this._mask[1] | this._mask[2] | this._mask[3]) !== 0 + ) { + unmask(data, this._mask); + } + } + + if (this._opcode > 0x07) { + this.controlMessage(data, cb); + return; + } + + if (this._compressed) { + this._state = INFLATING; + this.decompress(data, cb); + return; + } + + if (data.length) { + // + // This message is not compressed so its length is the sum of the payload + // length of all fragments. + // + this._messageLength = this._totalPayloadLength; + this._fragments.push(data); + } + + this.dataMessage(cb); + } + + /** + * Decompresses data. + * + * @param {Buffer} data Compressed data + * @param {Function} cb Callback + * @private + */ + decompress(data, cb) { + const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; + + perMessageDeflate.decompress(data, this._fin, (err, buf) => { + if (err) return cb(err); + + if (buf.length) { + this._messageLength += buf.length; + if (this._messageLength > this._maxPayload && this._maxPayload > 0) { + const error = this.createError( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ); + + cb(error); + return; + } + + this._fragments.push(buf); + } + + this.dataMessage(cb); + if (this._state === GET_INFO) this.startLoop(cb); + }); + } + + /** + * Handles a data message. + * + * @param {Function} cb Callback + * @private + */ + dataMessage(cb) { + if (!this._fin) { + this._state = GET_INFO; + return; + } + + const messageLength = this._messageLength; + const fragments = this._fragments; + + this._totalPayloadLength = 0; + this._messageLength = 0; + this._fragmented = 0; + this._fragments = []; + + if (this._opcode === 2) { + let data; + + if (this._binaryType === 'nodebuffer') { + data = concat(fragments, messageLength); + } else if (this._binaryType === 'arraybuffer') { + data = toArrayBuffer(concat(fragments, messageLength)); + } else if (this._binaryType === 'blob') { + data = new Blob(fragments); + } else { + data = fragments; + } + + if (this._allowSynchronousEvents) { + this.emit('message', data, true); + this._state = GET_INFO; + } else { + this._state = DEFER_EVENT; + setImmediate(() => { + this.emit('message', data, true); + this._state = GET_INFO; + this.startLoop(cb); + }); + } + } else { + const buf = concat(fragments, messageLength); + + if (!this._skipUTF8Validation && !isValidUTF8(buf)) { + const error = this.createError( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); + + cb(error); + return; + } + + if (this._state === INFLATING || this._allowSynchronousEvents) { + this.emit('message', buf, false); + this._state = GET_INFO; + } else { + this._state = DEFER_EVENT; + setImmediate(() => { + this.emit('message', buf, false); + this._state = GET_INFO; + this.startLoop(cb); + }); + } + } + } + + /** + * Handles a control message. + * + * @param {Buffer} data Data to handle + * @return {(Error|RangeError|undefined)} A possible error + * @private + */ + controlMessage(data, cb) { + if (this._opcode === 0x08) { + if (data.length === 0) { + this._loop = false; + this.emit('conclude', 1005, EMPTY_BUFFER); + this.end(); + } else { + const code = data.readUInt16BE(0); + + if (!isValidStatusCode(code)) { + const error = this.createError( + RangeError, + `invalid status code ${code}`, + true, + 1002, + 'WS_ERR_INVALID_CLOSE_CODE' + ); + + cb(error); + return; + } + + const buf = new FastBuffer( + data.buffer, + data.byteOffset + 2, + data.length - 2 + ); + + if (!this._skipUTF8Validation && !isValidUTF8(buf)) { + const error = this.createError( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); + + cb(error); + return; + } + + this._loop = false; + this.emit('conclude', code, buf); + this.end(); + } + + this._state = GET_INFO; + return; + } + + if (this._allowSynchronousEvents) { + this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data); + this._state = GET_INFO; + } else { + this._state = DEFER_EVENT; + setImmediate(() => { + this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data); + this._state = GET_INFO; + this.startLoop(cb); + }); + } + } + + /** + * Builds an error object. + * + * @param {function(new:Error|RangeError)} ErrorCtor The error constructor + * @param {String} message The error message + * @param {Boolean} prefix Specifies whether or not to add a default prefix to + * `message` + * @param {Number} statusCode The status code + * @param {String} errorCode The exposed error code + * @return {(Error|RangeError)} The error + * @private + */ + createError(ErrorCtor, message, prefix, statusCode, errorCode) { + this._loop = false; + this._errored = true; + + const err = new ErrorCtor( + prefix ? `Invalid WebSocket frame: ${message}` : message + ); + + Error.captureStackTrace(err, this.createError); + err.code = errorCode; + err[kStatusCode] = statusCode; + return err; + } +} + +module.exports = Receiver; + + +/***/ }), + +/***/ 3721: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex" }] */ + + + +const { Duplex } = __nccwpck_require__(2781); +const { randomFillSync } = __nccwpck_require__(6113); + +const PerMessageDeflate = __nccwpck_require__(6447); +const { EMPTY_BUFFER, kWebSocket, NOOP } = __nccwpck_require__(5350); +const { isBlob, isValidStatusCode } = __nccwpck_require__(2444); +const { mask: applyMask, toBuffer } = __nccwpck_require__(5533); + +const kByteLength = Symbol('kByteLength'); +const maskBuffer = Buffer.alloc(4); +const RANDOM_POOL_SIZE = 8 * 1024; +let randomPool; +let randomPoolPointer = RANDOM_POOL_SIZE; + +const DEFAULT = 0; +const DEFLATING = 1; +const GET_BLOB_DATA = 2; + +/** + * HyBi Sender implementation. + */ +class Sender { + /** + * Creates a Sender instance. + * + * @param {Duplex} socket The connection socket + * @param {Object} [extensions] An object containing the negotiated extensions + * @param {Function} [generateMask] The function used to generate the masking + * key + */ + constructor(socket, extensions, generateMask) { + this._extensions = extensions || {}; + + if (generateMask) { + this._generateMask = generateMask; + this._maskBuffer = Buffer.alloc(4); + } + + this._socket = socket; + + this._firstFragment = true; + this._compress = false; + + this._bufferedBytes = 0; + this._queue = []; + this._state = DEFAULT; + this.onerror = NOOP; + this[kWebSocket] = undefined; + } + + /** + * Frames a piece of data according to the HyBi WebSocket protocol. + * + * @param {(Buffer|String)} data The data to frame + * @param {Object} options Options object + * @param {Boolean} [options.fin=false] Specifies whether or not to set the + * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key + * @param {Boolean} [options.mask=false] Specifies whether or not to mask + * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified + * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the + * RSV1 bit + * @return {(Buffer|String)[]} The framed data + * @public + */ + static frame(data, options) { + let mask; + let merge = false; + let offset = 2; + let skipMasking = false; + + if (options.mask) { + mask = options.maskBuffer || maskBuffer; + + if (options.generateMask) { + options.generateMask(mask); + } else { + if (randomPoolPointer === RANDOM_POOL_SIZE) { + /* istanbul ignore else */ + if (randomPool === undefined) { + // + // This is lazily initialized because server-sent frames must not + // be masked so it may never be used. + // + randomPool = Buffer.alloc(RANDOM_POOL_SIZE); + } + + randomFillSync(randomPool, 0, RANDOM_POOL_SIZE); + randomPoolPointer = 0; + } + + mask[0] = randomPool[randomPoolPointer++]; + mask[1] = randomPool[randomPoolPointer++]; + mask[2] = randomPool[randomPoolPointer++]; + mask[3] = randomPool[randomPoolPointer++]; + } + + skipMasking = (mask[0] | mask[1] | mask[2] | mask[3]) === 0; + offset = 6; + } + + let dataLength; + + if (typeof data === 'string') { + if ( + (!options.mask || skipMasking) && + options[kByteLength] !== undefined + ) { + dataLength = options[kByteLength]; + } else { + data = Buffer.from(data); + dataLength = data.length; + } + } else { + dataLength = data.length; + merge = options.mask && options.readOnly && !skipMasking; + } + + let payloadLength = dataLength; + + if (dataLength >= 65536) { + offset += 8; + payloadLength = 127; + } else if (dataLength > 125) { + offset += 2; + payloadLength = 126; + } + + const target = Buffer.allocUnsafe(merge ? dataLength + offset : offset); + + target[0] = options.fin ? options.opcode | 0x80 : options.opcode; + if (options.rsv1) target[0] |= 0x40; + + target[1] = payloadLength; + + if (payloadLength === 126) { + target.writeUInt16BE(dataLength, 2); + } else if (payloadLength === 127) { + target[2] = target[3] = 0; + target.writeUIntBE(dataLength, 4, 6); + } + + if (!options.mask) return [target, data]; + + target[1] |= 0x80; + target[offset - 4] = mask[0]; + target[offset - 3] = mask[1]; + target[offset - 2] = mask[2]; + target[offset - 1] = mask[3]; + + if (skipMasking) return [target, data]; + + if (merge) { + applyMask(data, mask, target, offset, dataLength); + return [target]; + } + + applyMask(data, mask, data, 0, dataLength); + return [target, data]; + } + + /** + * Sends a close message to the other peer. + * + * @param {Number} [code] The status code component of the body + * @param {(String|Buffer)} [data] The message component of the body + * @param {Boolean} [mask=false] Specifies whether or not to mask the message + * @param {Function} [cb] Callback + * @public + */ + close(code, data, mask, cb) { + let buf; + + if (code === undefined) { + buf = EMPTY_BUFFER; + } else if (typeof code !== 'number' || !isValidStatusCode(code)) { + throw new TypeError('First argument must be a valid error code number'); + } else if (data === undefined || !data.length) { + buf = Buffer.allocUnsafe(2); + buf.writeUInt16BE(code, 0); + } else { + const length = Buffer.byteLength(data); + + if (length > 123) { + throw new RangeError('The message must not be greater than 123 bytes'); + } + + buf = Buffer.allocUnsafe(2 + length); + buf.writeUInt16BE(code, 0); + + if (typeof data === 'string') { + buf.write(data, 2); + } else { + buf.set(data, 2); + } + } + + const options = { + [kByteLength]: buf.length, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x08, + readOnly: false, + rsv1: false + }; + + if (this._state !== DEFAULT) { + this.enqueue([this.dispatch, buf, false, options, cb]); + } else { + this.sendFrame(Sender.frame(buf, options), cb); + } + } + + /** + * Sends a ping message to the other peer. + * + * @param {*} data The message to send + * @param {Boolean} [mask=false] Specifies whether or not to mask `data` + * @param {Function} [cb] Callback + * @public + */ + ping(data, mask, cb) { + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else if (isBlob(data)) { + byteLength = data.size; + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } + + if (byteLength > 125) { + throw new RangeError('The data size must not be greater than 125 bytes'); + } + + const options = { + [kByteLength]: byteLength, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x09, + readOnly, + rsv1: false + }; + + if (isBlob(data)) { + if (this._state !== DEFAULT) { + this.enqueue([this.getBlobData, data, false, options, cb]); + } else { + this.getBlobData(data, false, options, cb); + } + } else if (this._state !== DEFAULT) { + this.enqueue([this.dispatch, data, false, options, cb]); + } else { + this.sendFrame(Sender.frame(data, options), cb); + } + } + + /** + * Sends a pong message to the other peer. + * + * @param {*} data The message to send + * @param {Boolean} [mask=false] Specifies whether or not to mask `data` + * @param {Function} [cb] Callback + * @public + */ + pong(data, mask, cb) { + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else if (isBlob(data)) { + byteLength = data.size; + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } + + if (byteLength > 125) { + throw new RangeError('The data size must not be greater than 125 bytes'); + } + + const options = { + [kByteLength]: byteLength, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x0a, + readOnly, + rsv1: false + }; + + if (isBlob(data)) { + if (this._state !== DEFAULT) { + this.enqueue([this.getBlobData, data, false, options, cb]); + } else { + this.getBlobData(data, false, options, cb); + } + } else if (this._state !== DEFAULT) { + this.enqueue([this.dispatch, data, false, options, cb]); + } else { + this.sendFrame(Sender.frame(data, options), cb); + } + } + + /** + * Sends a data message to the other peer. + * + * @param {*} data The message to send + * @param {Object} options Options object + * @param {Boolean} [options.binary=false] Specifies whether `data` is binary + * or text + * @param {Boolean} [options.compress=false] Specifies whether or not to + * compress `data` + * @param {Boolean} [options.fin=false] Specifies whether the fragment is the + * last one + * @param {Boolean} [options.mask=false] Specifies whether or not to mask + * `data` + * @param {Function} [cb] Callback + * @public + */ + send(data, options, cb) { + const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; + let opcode = options.binary ? 2 : 1; + let rsv1 = options.compress; + + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else if (isBlob(data)) { + byteLength = data.size; + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } + + if (this._firstFragment) { + this._firstFragment = false; + if ( + rsv1 && + perMessageDeflate && + perMessageDeflate.params[ + perMessageDeflate._isServer + ? 'server_no_context_takeover' + : 'client_no_context_takeover' + ] + ) { + rsv1 = byteLength >= perMessageDeflate._threshold; + } + this._compress = rsv1; + } else { + rsv1 = false; + opcode = 0; + } + + if (options.fin) this._firstFragment = true; + + const opts = { + [kByteLength]: byteLength, + fin: options.fin, + generateMask: this._generateMask, + mask: options.mask, + maskBuffer: this._maskBuffer, + opcode, + readOnly, + rsv1 + }; + + if (isBlob(data)) { + if (this._state !== DEFAULT) { + this.enqueue([this.getBlobData, data, this._compress, opts, cb]); + } else { + this.getBlobData(data, this._compress, opts, cb); + } + } else if (this._state !== DEFAULT) { + this.enqueue([this.dispatch, data, this._compress, opts, cb]); + } else { + this.dispatch(data, this._compress, opts, cb); + } + } + + /** + * Gets the contents of a blob as binary data. + * + * @param {Blob} blob The blob + * @param {Boolean} [compress=false] Specifies whether or not to compress + * the data + * @param {Object} options Options object + * @param {Boolean} [options.fin=false] Specifies whether or not to set the + * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key + * @param {Boolean} [options.mask=false] Specifies whether or not to mask + * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified + * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the + * RSV1 bit + * @param {Function} [cb] Callback + * @private + */ + getBlobData(blob, compress, options, cb) { + this._bufferedBytes += options[kByteLength]; + this._state = GET_BLOB_DATA; + + blob + .arrayBuffer() + .then((arrayBuffer) => { + if (this._socket.destroyed) { + const err = new Error( + 'The socket was closed while the blob was being read' + ); + + // + // `callCallbacks` is called in the next tick to ensure that errors + // that might be thrown in the callbacks behave like errors thrown + // outside the promise chain. + // + process.nextTick(callCallbacks, this, err, cb); + return; + } + + this._bufferedBytes -= options[kByteLength]; + const data = toBuffer(arrayBuffer); + + if (!compress) { + this._state = DEFAULT; + this.sendFrame(Sender.frame(data, options), cb); + this.dequeue(); + } else { + this.dispatch(data, compress, options, cb); + } + }) + .catch((err) => { + // + // `onError` is called in the next tick for the same reason that + // `callCallbacks` above is. + // + process.nextTick(onError, this, err, cb); + }); + } + + /** + * Dispatches a message. + * + * @param {(Buffer|String)} data The message to send + * @param {Boolean} [compress=false] Specifies whether or not to compress + * `data` + * @param {Object} options Options object + * @param {Boolean} [options.fin=false] Specifies whether or not to set the + * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key + * @param {Boolean} [options.mask=false] Specifies whether or not to mask + * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified + * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the + * RSV1 bit + * @param {Function} [cb] Callback + * @private + */ + dispatch(data, compress, options, cb) { + if (!compress) { + this.sendFrame(Sender.frame(data, options), cb); + return; + } + + const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; + + this._bufferedBytes += options[kByteLength]; + this._state = DEFLATING; + perMessageDeflate.compress(data, options.fin, (_, buf) => { + if (this._socket.destroyed) { + const err = new Error( + 'The socket was closed while data was being compressed' + ); + + callCallbacks(this, err, cb); + return; + } + + this._bufferedBytes -= options[kByteLength]; + this._state = DEFAULT; + options.readOnly = false; + this.sendFrame(Sender.frame(buf, options), cb); + this.dequeue(); + }); + } + + /** + * Executes queued send operations. + * + * @private + */ + dequeue() { + while (this._state === DEFAULT && this._queue.length) { + const params = this._queue.shift(); + + this._bufferedBytes -= params[3][kByteLength]; + Reflect.apply(params[0], this, params.slice(1)); + } + } + + /** + * Enqueues a send operation. + * + * @param {Array} params Send operation parameters. + * @private + */ + enqueue(params) { + this._bufferedBytes += params[3][kByteLength]; + this._queue.push(params); + } + + /** + * Sends a frame. + * + * @param {Buffer[]} list The frame to send + * @param {Function} [cb] Callback + * @private + */ + sendFrame(list, cb) { + if (list.length === 2) { + this._socket.cork(); + this._socket.write(list[0]); + this._socket.write(list[1], cb); + this._socket.uncork(); + } else { + this._socket.write(list[0], cb); + } + } +} + +module.exports = Sender; + +/** + * Calls queued callbacks with an error. + * + * @param {Sender} sender The `Sender` instance + * @param {Error} err The error to call the callbacks with + * @param {Function} [cb] The first callback + * @private + */ +function callCallbacks(sender, err, cb) { + if (typeof cb === 'function') cb(err); + + for (let i = 0; i < sender._queue.length; i++) { + const params = sender._queue[i]; + const callback = params[params.length - 1]; + + if (typeof callback === 'function') callback(err); + } +} + +/** + * Handles a `Sender` error. + * + * @param {Sender} sender The `Sender` instance + * @param {Error} err The error + * @param {Function} [cb] The first pending callback + * @private + */ +function onError(sender, err, cb) { + callCallbacks(sender, err, cb); + sender.onerror(err); +} + + +/***/ }), + +/***/ 1677: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { Duplex } = __nccwpck_require__(2781); + +/** + * Emits the `'close'` event on a stream. + * + * @param {Duplex} stream The stream. + * @private + */ +function emitClose(stream) { + stream.emit('close'); +} + +/** + * The listener of the `'end'` event. + * + * @private + */ +function duplexOnEnd() { + if (!this.destroyed && this._writableState.finished) { + this.destroy(); + } +} + +/** + * The listener of the `'error'` event. + * + * @param {Error} err The error + * @private + */ +function duplexOnError(err) { + this.removeListener('error', duplexOnError); + this.destroy(); + if (this.listenerCount('error') === 0) { + // Do not suppress the throwing behavior. + this.emit('error', err); + } +} + +/** + * Wraps a `WebSocket` in a duplex stream. + * + * @param {WebSocket} ws The `WebSocket` to wrap + * @param {Object} [options] The options for the `Duplex` constructor + * @return {Duplex} The duplex stream + * @public + */ +function createWebSocketStream(ws, options) { + let terminateOnDestroy = true; + + const duplex = new Duplex({ + ...options, + autoDestroy: false, + emitClose: false, + objectMode: false, + writableObjectMode: false + }); + + ws.on('message', function message(msg, isBinary) { + const data = + !isBinary && duplex._readableState.objectMode ? msg.toString() : msg; + + if (!duplex.push(data)) ws.pause(); + }); + + ws.once('error', function error(err) { + if (duplex.destroyed) return; + + // Prevent `ws.terminate()` from being called by `duplex._destroy()`. + // + // - If the `'error'` event is emitted before the `'open'` event, then + // `ws.terminate()` is a noop as no socket is assigned. + // - Otherwise, the error is re-emitted by the listener of the `'error'` + // event of the `Receiver` object. The listener already closes the + // connection by calling `ws.close()`. This allows a close frame to be + // sent to the other peer. If `ws.terminate()` is called right after this, + // then the close frame might not be sent. + terminateOnDestroy = false; + duplex.destroy(err); + }); + + ws.once('close', function close() { + if (duplex.destroyed) return; + + duplex.push(null); + }); + + duplex._destroy = function (err, callback) { + if (ws.readyState === ws.CLOSED) { + callback(err); + process.nextTick(emitClose, duplex); + return; + } + + let called = false; + + ws.once('error', function error(err) { + called = true; + callback(err); + }); + + ws.once('close', function close() { + if (!called) callback(err); + process.nextTick(emitClose, duplex); + }); + + if (terminateOnDestroy) ws.terminate(); + }; + + duplex._final = function (callback) { + if (ws.readyState === ws.CONNECTING) { + ws.once('open', function open() { + duplex._final(callback); + }); + return; + } + + // If the value of the `_socket` property is `null` it means that `ws` is a + // client websocket and the handshake failed. In fact, when this happens, a + // socket is never assigned to the websocket. Wait for the `'error'` event + // that will be emitted by the websocket. + if (ws._socket === null) return; + + if (ws._socket._writableState.finished) { + callback(); + if (duplex._readableState.endEmitted) duplex.destroy(); + } else { + ws._socket.once('finish', function finish() { + // `duplex` is not destroyed here because the `'end'` event will be + // emitted on `duplex` after this `'finish'` event. The EOF signaling + // `null` chunk is, in fact, pushed when the websocket emits `'close'`. + callback(); + }); + ws.close(); + } + }; + + duplex._read = function () { + if (ws.isPaused) ws.resume(); + }; + + duplex._write = function (chunk, encoding, callback) { + if (ws.readyState === ws.CONNECTING) { + ws.once('open', function open() { + duplex._write(chunk, encoding, callback); + }); + return; + } + + ws.send(chunk, callback); + }; + + duplex.on('end', duplexOnEnd); + duplex.on('error', duplexOnError); + return duplex; +} + +module.exports = createWebSocketStream; + + +/***/ }), + +/***/ 3412: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { tokenChars } = __nccwpck_require__(2444); + +/** + * Parses the `Sec-WebSocket-Protocol` header into a set of subprotocol names. + * + * @param {String} header The field value of the header + * @return {Set} The subprotocol names + * @public + */ +function parse(header) { + const protocols = new Set(); + let start = -1; + let end = -1; + let i = 0; + + for (i; i < header.length; i++) { + const code = header.charCodeAt(i); + + if (end === -1 && tokenChars[code] === 1) { + if (start === -1) start = i; + } else if ( + i !== 0 && + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ + ) { + if (end === -1 && start !== -1) end = i; + } else if (code === 0x2c /* ',' */) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + + const protocol = header.slice(start, end); + + if (protocols.has(protocol)) { + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); + } + + protocols.add(protocol); + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } + + if (start === -1 || end !== -1) { + throw new SyntaxError('Unexpected end of input'); + } + + const protocol = header.slice(start, i); + + if (protocols.has(protocol)) { + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); + } + + protocols.add(protocol); + return protocols; +} + +module.exports = { parse }; + + +/***/ }), + +/***/ 2444: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { isUtf8 } = __nccwpck_require__(4300); + +const { hasBlob } = __nccwpck_require__(5350); + +// +// Allowed token characters: +// +// '!', '#', '$', '%', '&', ''', '*', '+', '-', +// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' +// +// tokenChars[32] === 0 // ' ' +// tokenChars[33] === 1 // '!' +// tokenChars[34] === 0 // '"' +// ... +// +// prettier-ignore +const tokenChars = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 +]; + +/** + * Checks if a status code is allowed in a close frame. + * + * @param {Number} code The status code + * @return {Boolean} `true` if the status code is valid, else `false` + * @public + */ +function isValidStatusCode(code) { + return ( + (code >= 1000 && + code <= 1014 && + code !== 1004 && + code !== 1005 && + code !== 1006) || + (code >= 3000 && code <= 4999) + ); +} + +/** + * Checks if a given buffer contains only correct UTF-8. + * Ported from https://www.cl.cam.ac.uk/%7Emgk25/ucs/utf8_check.c by + * Markus Kuhn. + * + * @param {Buffer} buf The buffer to check + * @return {Boolean} `true` if `buf` contains only correct UTF-8, else `false` + * @public + */ +function _isValidUTF8(buf) { + const len = buf.length; + let i = 0; + + while (i < len) { + if ((buf[i] & 0x80) === 0) { + // 0xxxxxxx + i++; + } else if ((buf[i] & 0xe0) === 0xc0) { + // 110xxxxx 10xxxxxx + if ( + i + 1 === len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i] & 0xfe) === 0xc0 // Overlong + ) { + return false; + } + + i += 2; + } else if ((buf[i] & 0xf0) === 0xe0) { + // 1110xxxx 10xxxxxx 10xxxxxx + if ( + i + 2 >= len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i + 2] & 0xc0) !== 0x80 || + (buf[i] === 0xe0 && (buf[i + 1] & 0xe0) === 0x80) || // Overlong + (buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF) + ) { + return false; + } + + i += 3; + } else if ((buf[i] & 0xf8) === 0xf0) { + // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + if ( + i + 3 >= len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i + 2] & 0xc0) !== 0x80 || + (buf[i + 3] & 0xc0) !== 0x80 || + (buf[i] === 0xf0 && (buf[i + 1] & 0xf0) === 0x80) || // Overlong + (buf[i] === 0xf4 && buf[i + 1] > 0x8f) || + buf[i] > 0xf4 // > U+10FFFF + ) { + return false; + } + + i += 4; + } else { + return false; + } + } + + return true; +} + +/** + * Determines whether a value is a `Blob`. + * + * @param {*} value The value to be tested + * @return {Boolean} `true` if `value` is a `Blob`, else `false` + * @private + */ +function isBlob(value) { + return ( + hasBlob && + typeof value === 'object' && + typeof value.arrayBuffer === 'function' && + typeof value.type === 'string' && + typeof value.stream === 'function' && + (value[Symbol.toStringTag] === 'Blob' || + value[Symbol.toStringTag] === 'File') + ); +} + +module.exports = { + isBlob, + isValidStatusCode, + isValidUTF8: _isValidUTF8, + tokenChars +}; + +if (isUtf8) { + module.exports.isValidUTF8 = function (buf) { + return buf.length < 24 ? _isValidUTF8(buf) : isUtf8(buf); + }; +} /* istanbul ignore else */ else if (!process.env.WS_NO_UTF_8_VALIDATE) { + try { + const isValidUTF8 = __nccwpck_require__(4592); + + module.exports.isValidUTF8 = function (buf) { + return buf.length < 32 ? _isValidUTF8(buf) : isValidUTF8(buf); + }; + } catch (e) { + // Continue regardless of the error. + } +} + + +/***/ }), + +/***/ 2806: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex$", "caughtErrors": "none" }] */ + + + +const EventEmitter = __nccwpck_require__(2361); +const http = __nccwpck_require__(3685); +const { Duplex } = __nccwpck_require__(2781); +const { createHash } = __nccwpck_require__(6113); + +const extension = __nccwpck_require__(2118); +const PerMessageDeflate = __nccwpck_require__(6447); +const subprotocol = __nccwpck_require__(3412); +const WebSocket = __nccwpck_require__(8393); +const { GUID, kWebSocket } = __nccwpck_require__(5350); + +const keyRegex = /^[+/0-9A-Za-z]{22}==$/; + +const RUNNING = 0; +const CLOSING = 1; +const CLOSED = 2; + +/** + * Class representing a WebSocket server. + * + * @extends EventEmitter + */ +class WebSocketServer extends EventEmitter { + /** + * Create a `WebSocketServer` instance. + * + * @param {Object} options Configuration options + * @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether + * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted + * multiple times in the same tick + * @param {Boolean} [options.autoPong=true] Specifies whether or not to + * automatically send a pong in response to a ping + * @param {Number} [options.backlog=511] The maximum length of the queue of + * pending connections + * @param {Boolean} [options.clientTracking=true] Specifies whether or not to + * track clients + * @param {Function} [options.handleProtocols] A hook to handle protocols + * @param {String} [options.host] The hostname where to bind the server + * @param {Number} [options.maxPayload=104857600] The maximum allowed message + * size + * @param {Boolean} [options.noServer=false] Enable no server mode + * @param {String} [options.path] Accept only connections matching this path + * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable + * permessage-deflate + * @param {Number} [options.port] The port where to bind the server + * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S + * server to use + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages + * @param {Function} [options.verifyClient] A hook to reject connections + * @param {Function} [options.WebSocket=WebSocket] Specifies the `WebSocket` + * class to use. It must be the `WebSocket` class or class that extends it + * @param {Function} [callback] A listener for the `listening` event + */ + constructor(options, callback) { + super(); + + options = { + allowSynchronousEvents: true, + autoPong: true, + maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, + perMessageDeflate: false, + handleProtocols: null, + clientTracking: true, + verifyClient: null, + noServer: false, + backlog: null, // use default (511 as implemented in net.js) + server: null, + host: null, + path: null, + port: null, + WebSocket, + ...options + }; + + if ( + (options.port == null && !options.server && !options.noServer) || + (options.port != null && (options.server || options.noServer)) || + (options.server && options.noServer) + ) { + throw new TypeError( + 'One and only one of the "port", "server", or "noServer" options ' + + 'must be specified' + ); + } + + if (options.port != null) { + this._server = http.createServer((req, res) => { + const body = http.STATUS_CODES[426]; + + res.writeHead(426, { + 'Content-Length': body.length, + 'Content-Type': 'text/plain' + }); + res.end(body); + }); + this._server.listen( + options.port, + options.host, + options.backlog, + callback + ); + } else if (options.server) { + this._server = options.server; + } + + if (this._server) { + const emitConnection = this.emit.bind(this, 'connection'); + + this._removeListeners = addListeners(this._server, { + listening: this.emit.bind(this, 'listening'), + error: this.emit.bind(this, 'error'), + upgrade: (req, socket, head) => { + this.handleUpgrade(req, socket, head, emitConnection); + } + }); + } + + if (options.perMessageDeflate === true) options.perMessageDeflate = {}; + if (options.clientTracking) { + this.clients = new Set(); + this._shouldEmitClose = false; + } + + this.options = options; + this._state = RUNNING; + } + + /** + * Returns the bound address, the address family name, and port of the server + * as reported by the operating system if listening on an IP socket. + * If the server is listening on a pipe or UNIX domain socket, the name is + * returned as a string. + * + * @return {(Object|String|null)} The address of the server + * @public + */ + address() { + if (this.options.noServer) { + throw new Error('The server is operating in "noServer" mode'); + } + + if (!this._server) return null; + return this._server.address(); + } + + /** + * Stop the server from accepting new connections and emit the `'close'` event + * when all existing connections are closed. + * + * @param {Function} [cb] A one-time listener for the `'close'` event + * @public + */ + close(cb) { + if (this._state === CLOSED) { + if (cb) { + this.once('close', () => { + cb(new Error('The server is not running')); + }); + } + + process.nextTick(emitClose, this); + return; + } + + if (cb) this.once('close', cb); + + if (this._state === CLOSING) return; + this._state = CLOSING; + + if (this.options.noServer || this.options.server) { + if (this._server) { + this._removeListeners(); + this._removeListeners = this._server = null; + } + + if (this.clients) { + if (!this.clients.size) { + process.nextTick(emitClose, this); + } else { + this._shouldEmitClose = true; + } + } else { + process.nextTick(emitClose, this); + } + } else { + const server = this._server; + + this._removeListeners(); + this._removeListeners = this._server = null; + + // + // The HTTP/S server was created internally. Close it, and rely on its + // `'close'` event. + // + server.close(() => { + emitClose(this); + }); + } + } + + /** + * See if a given request should be handled by this server instance. + * + * @param {http.IncomingMessage} req Request object to inspect + * @return {Boolean} `true` if the request is valid, else `false` + * @public + */ + shouldHandle(req) { + if (this.options.path) { + const index = req.url.indexOf('?'); + const pathname = index !== -1 ? req.url.slice(0, index) : req.url; + + if (pathname !== this.options.path) return false; + } + + return true; + } + + /** + * Handle a HTTP Upgrade request. + * + * @param {http.IncomingMessage} req The request object + * @param {Duplex} socket The network socket between the server and client + * @param {Buffer} head The first packet of the upgraded stream + * @param {Function} cb Callback + * @public + */ + handleUpgrade(req, socket, head, cb) { + socket.on('error', socketOnError); + + const key = req.headers['sec-websocket-key']; + const upgrade = req.headers.upgrade; + const version = +req.headers['sec-websocket-version']; + + if (req.method !== 'GET') { + const message = 'Invalid HTTP method'; + abortHandshakeOrEmitwsClientError(this, req, socket, 405, message); + return; + } + + if (upgrade === undefined || upgrade.toLowerCase() !== 'websocket') { + const message = 'Invalid Upgrade header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (key === undefined || !keyRegex.test(key)) { + const message = 'Missing or invalid Sec-WebSocket-Key header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (version !== 8 && version !== 13) { + const message = 'Missing or invalid Sec-WebSocket-Version header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (!this.shouldHandle(req)) { + abortHandshake(socket, 400); + return; + } + + const secWebSocketProtocol = req.headers['sec-websocket-protocol']; + let protocols = new Set(); + + if (secWebSocketProtocol !== undefined) { + try { + protocols = subprotocol.parse(secWebSocketProtocol); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Protocol header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + } + + const secWebSocketExtensions = req.headers['sec-websocket-extensions']; + const extensions = {}; + + if ( + this.options.perMessageDeflate && + secWebSocketExtensions !== undefined + ) { + const perMessageDeflate = new PerMessageDeflate( + this.options.perMessageDeflate, + true, + this.options.maxPayload + ); + + try { + const offers = extension.parse(secWebSocketExtensions); + + if (offers[PerMessageDeflate.extensionName]) { + perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]); + extensions[PerMessageDeflate.extensionName] = perMessageDeflate; + } + } catch (err) { + const message = + 'Invalid or unacceptable Sec-WebSocket-Extensions header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + } + + // + // Optionally call external client verification handler. + // + if (this.options.verifyClient) { + const info = { + origin: + req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`], + secure: !!(req.socket.authorized || req.socket.encrypted), + req + }; + + if (this.options.verifyClient.length === 2) { + this.options.verifyClient(info, (verified, code, message, headers) => { + if (!verified) { + return abortHandshake(socket, code || 401, message, headers); + } + + this.completeUpgrade( + extensions, + key, + protocols, + req, + socket, + head, + cb + ); + }); + return; + } + + if (!this.options.verifyClient(info)) return abortHandshake(socket, 401); + } + + this.completeUpgrade(extensions, key, protocols, req, socket, head, cb); + } + + /** + * Upgrade the connection to WebSocket. + * + * @param {Object} extensions The accepted extensions + * @param {String} key The value of the `Sec-WebSocket-Key` header + * @param {Set} protocols The subprotocols + * @param {http.IncomingMessage} req The request object + * @param {Duplex} socket The network socket between the server and client + * @param {Buffer} head The first packet of the upgraded stream + * @param {Function} cb Callback + * @throws {Error} If called more than once with the same socket + * @private + */ + completeUpgrade(extensions, key, protocols, req, socket, head, cb) { + // + // Destroy the socket if the client has already sent a FIN packet. + // + if (!socket.readable || !socket.writable) return socket.destroy(); + + if (socket[kWebSocket]) { + throw new Error( + 'server.handleUpgrade() was called more than once with the same ' + + 'socket, possibly due to a misconfiguration' + ); + } + + if (this._state > RUNNING) return abortHandshake(socket, 503); + + const digest = createHash('sha1') + .update(key + GUID) + .digest('base64'); + + const headers = [ + 'HTTP/1.1 101 Switching Protocols', + 'Upgrade: websocket', + 'Connection: Upgrade', + `Sec-WebSocket-Accept: ${digest}` + ]; + + const ws = new this.options.WebSocket(null, undefined, this.options); + + if (protocols.size) { + // + // Optionally call external protocol selection handler. + // + const protocol = this.options.handleProtocols + ? this.options.handleProtocols(protocols, req) + : protocols.values().next().value; + + if (protocol) { + headers.push(`Sec-WebSocket-Protocol: ${protocol}`); + ws._protocol = protocol; + } + } + + if (extensions[PerMessageDeflate.extensionName]) { + const params = extensions[PerMessageDeflate.extensionName].params; + const value = extension.format({ + [PerMessageDeflate.extensionName]: [params] + }); + headers.push(`Sec-WebSocket-Extensions: ${value}`); + ws._extensions = extensions; + } + + // + // Allow external modification/inspection of handshake headers. + // + this.emit('headers', headers, req); + + socket.write(headers.concat('\r\n').join('\r\n')); + socket.removeListener('error', socketOnError); + + ws.setSocket(socket, head, { + allowSynchronousEvents: this.options.allowSynchronousEvents, + maxPayload: this.options.maxPayload, + skipUTF8Validation: this.options.skipUTF8Validation + }); + + if (this.clients) { + this.clients.add(ws); + ws.on('close', () => { + this.clients.delete(ws); + + if (this._shouldEmitClose && !this.clients.size) { + process.nextTick(emitClose, this); + } + }); + } + + cb(ws, req); + } +} + +module.exports = WebSocketServer; + +/** + * Add event listeners on an `EventEmitter` using a map of + * pairs. + * + * @param {EventEmitter} server The event emitter + * @param {Object.} map The listeners to add + * @return {Function} A function that will remove the added listeners when + * called + * @private + */ +function addListeners(server, map) { + for (const event of Object.keys(map)) server.on(event, map[event]); + + return function removeListeners() { + for (const event of Object.keys(map)) { + server.removeListener(event, map[event]); + } + }; +} + +/** + * Emit a `'close'` event on an `EventEmitter`. + * + * @param {EventEmitter} server The event emitter + * @private + */ +function emitClose(server) { + server._state = CLOSED; + server.emit('close'); +} + +/** + * Handle socket errors. + * + * @private + */ +function socketOnError() { + this.destroy(); +} + +/** + * Close the connection when preconditions are not fulfilled. + * + * @param {Duplex} socket The socket of the upgrade request + * @param {Number} code The HTTP response status code + * @param {String} [message] The HTTP response body + * @param {Object} [headers] Additional HTTP response headers + * @private + */ +function abortHandshake(socket, code, message, headers) { + // + // The socket is writable unless the user destroyed or ended it before calling + // `server.handleUpgrade()` or in the `verifyClient` function, which is a user + // error. Handling this does not make much sense as the worst that can happen + // is that some of the data written by the user might be discarded due to the + // call to `socket.end()` below, which triggers an `'error'` event that in + // turn causes the socket to be destroyed. + // + message = message || http.STATUS_CODES[code]; + headers = { + Connection: 'close', + 'Content-Type': 'text/html', + 'Content-Length': Buffer.byteLength(message), + ...headers + }; + + socket.once('finish', socket.destroy); + + socket.end( + `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + + Object.keys(headers) + .map((h) => `${h}: ${headers[h]}`) + .join('\r\n') + + '\r\n\r\n' + + message + ); +} + +/** + * Emit a `'wsClientError'` event on a `WebSocketServer` if there is at least + * one listener for it, otherwise call `abortHandshake()`. + * + * @param {WebSocketServer} server The WebSocket server + * @param {http.IncomingMessage} req The request object + * @param {Duplex} socket The socket of the upgrade request + * @param {Number} code The HTTP response status code + * @param {String} message The HTTP response body + * @private + */ +function abortHandshakeOrEmitwsClientError(server, req, socket, code, message) { + if (server.listenerCount('wsClientError')) { + const err = new Error(message); + Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError); + + server.emit('wsClientError', err, socket, req); + } else { + abortHandshake(socket, code, message); + } +} + + +/***/ }), + +/***/ 8393: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex|Readable$", "caughtErrors": "none" }] */ + + + +const EventEmitter = __nccwpck_require__(2361); +const https = __nccwpck_require__(5687); +const http = __nccwpck_require__(3685); +const net = __nccwpck_require__(1808); +const tls = __nccwpck_require__(4404); +const { randomBytes, createHash } = __nccwpck_require__(6113); +const { Duplex, Readable } = __nccwpck_require__(2781); +const { URL } = __nccwpck_require__(7310); + +const PerMessageDeflate = __nccwpck_require__(6447); +const Receiver = __nccwpck_require__(5934); +const Sender = __nccwpck_require__(3721); +const { isBlob } = __nccwpck_require__(2444); + +const { + BINARY_TYPES, + EMPTY_BUFFER, + GUID, + kForOnEventAttribute, + kListener, + kStatusCode, + kWebSocket, + NOOP +} = __nccwpck_require__(5350); +const { + EventTarget: { addEventListener, removeEventListener } +} = __nccwpck_require__(9593); +const { format, parse } = __nccwpck_require__(2118); +const { toBuffer } = __nccwpck_require__(5533); + +const closeTimeout = 30 * 1000; +const kAborted = Symbol('kAborted'); +const protocolVersions = [8, 13]; +const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; +const subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/; + +/** + * Class representing a WebSocket. + * + * @extends EventEmitter + */ +class WebSocket extends EventEmitter { + /** + * Create a new `WebSocket`. + * + * @param {(String|URL)} address The URL to which to connect + * @param {(String|String[])} [protocols] The subprotocols + * @param {Object} [options] Connection options + */ + constructor(address, protocols, options) { + super(); + + this._binaryType = BINARY_TYPES[0]; + this._closeCode = 1006; + this._closeFrameReceived = false; + this._closeFrameSent = false; + this._closeMessage = EMPTY_BUFFER; + this._closeTimer = null; + this._errorEmitted = false; + this._extensions = {}; + this._paused = false; + this._protocol = ''; + this._readyState = WebSocket.CONNECTING; + this._receiver = null; + this._sender = null; + this._socket = null; + + if (address !== null) { + this._bufferedAmount = 0; + this._isServer = false; + this._redirects = 0; + + if (protocols === undefined) { + protocols = []; + } else if (!Array.isArray(protocols)) { + if (typeof protocols === 'object' && protocols !== null) { + options = protocols; + protocols = []; + } else { + protocols = [protocols]; + } + } + + initAsClient(this, address, protocols, options); + } else { + this._autoPong = options.autoPong; + this._isServer = true; + } + } + + /** + * For historical reasons, the custom "nodebuffer" type is used by the default + * instead of "blob". + * + * @type {String} + */ + get binaryType() { + return this._binaryType; + } + + set binaryType(type) { + if (!BINARY_TYPES.includes(type)) return; + + this._binaryType = type; + + // + // Allow to change `binaryType` on the fly. + // + if (this._receiver) this._receiver._binaryType = type; + } + + /** + * @type {Number} + */ + get bufferedAmount() { + if (!this._socket) return this._bufferedAmount; + + return this._socket._writableState.length + this._sender._bufferedBytes; + } + + /** + * @type {String} + */ + get extensions() { + return Object.keys(this._extensions).join(); + } + + /** + * @type {Boolean} + */ + get isPaused() { + return this._paused; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onclose() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onerror() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onopen() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onmessage() { + return null; + } + + /** + * @type {String} + */ + get protocol() { + return this._protocol; + } + + /** + * @type {Number} + */ + get readyState() { + return this._readyState; + } + + /** + * @type {String} + */ + get url() { + return this._url; + } + + /** + * Set up the socket and the internal resources. + * + * @param {Duplex} socket The network socket between the server and client + * @param {Buffer} head The first packet of the upgraded stream + * @param {Object} options Options object + * @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether + * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted + * multiple times in the same tick + * @param {Function} [options.generateMask] The function used to generate the + * masking key + * @param {Number} [options.maxPayload=0] The maximum allowed message size + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages + * @private + */ + setSocket(socket, head, options) { + const receiver = new Receiver({ + allowSynchronousEvents: options.allowSynchronousEvents, + binaryType: this.binaryType, + extensions: this._extensions, + isServer: this._isServer, + maxPayload: options.maxPayload, + skipUTF8Validation: options.skipUTF8Validation + }); + + const sender = new Sender(socket, this._extensions, options.generateMask); + + this._receiver = receiver; + this._sender = sender; + this._socket = socket; + + receiver[kWebSocket] = this; + sender[kWebSocket] = this; + socket[kWebSocket] = this; + + receiver.on('conclude', receiverOnConclude); + receiver.on('drain', receiverOnDrain); + receiver.on('error', receiverOnError); + receiver.on('message', receiverOnMessage); + receiver.on('ping', receiverOnPing); + receiver.on('pong', receiverOnPong); + + sender.onerror = senderOnError; + + // + // These methods may not be available if `socket` is just a `Duplex`. + // + if (socket.setTimeout) socket.setTimeout(0); + if (socket.setNoDelay) socket.setNoDelay(); + + if (head.length > 0) socket.unshift(head); + + socket.on('close', socketOnClose); + socket.on('data', socketOnData); + socket.on('end', socketOnEnd); + socket.on('error', socketOnError); + + this._readyState = WebSocket.OPEN; + this.emit('open'); + } + + /** + * Emit the `'close'` event. + * + * @private + */ + emitClose() { + if (!this._socket) { + this._readyState = WebSocket.CLOSED; + this.emit('close', this._closeCode, this._closeMessage); + return; + } + + if (this._extensions[PerMessageDeflate.extensionName]) { + this._extensions[PerMessageDeflate.extensionName].cleanup(); + } + + this._receiver.removeAllListeners(); + this._readyState = WebSocket.CLOSED; + this.emit('close', this._closeCode, this._closeMessage); + } + + /** + * Start a closing handshake. + * + * +----------+ +-----------+ +----------+ + * - - -|ws.close()|-->|close frame|-->|ws.close()|- - - + * | +----------+ +-----------+ +----------+ | + * +----------+ +-----------+ | + * CLOSING |ws.close()|<--|close frame|<--+-----+ CLOSING + * +----------+ +-----------+ | + * | | | +---+ | + * +------------------------+-->|fin| - - - - + * | +---+ | +---+ + * - - - - -|fin|<---------------------+ + * +---+ + * + * @param {Number} [code] Status code explaining why the connection is closing + * @param {(String|Buffer)} [data] The reason why the connection is + * closing + * @public + */ + close(code, data) { + if (this.readyState === WebSocket.CLOSED) return; + if (this.readyState === WebSocket.CONNECTING) { + const msg = 'WebSocket was closed before the connection was established'; + abortHandshake(this, this._req, msg); + return; + } + + if (this.readyState === WebSocket.CLOSING) { + if ( + this._closeFrameSent && + (this._closeFrameReceived || this._receiver._writableState.errorEmitted) + ) { + this._socket.end(); + } + + return; + } + + this._readyState = WebSocket.CLOSING; + this._sender.close(code, data, !this._isServer, (err) => { + // + // This error is handled by the `'error'` listener on the socket. We only + // want to know if the close frame has been sent here. + // + if (err) return; + + this._closeFrameSent = true; + + if ( + this._closeFrameReceived || + this._receiver._writableState.errorEmitted + ) { + this._socket.end(); + } + }); + + setCloseTimer(this); + } + + /** + * Pause the socket. + * + * @public + */ + pause() { + if ( + this.readyState === WebSocket.CONNECTING || + this.readyState === WebSocket.CLOSED + ) { + return; + } + + this._paused = true; + this._socket.pause(); + } + + /** + * Send a ping. + * + * @param {*} [data] The data to send + * @param {Boolean} [mask] Indicates whether or not to mask `data` + * @param {Function} [cb] Callback which is executed when the ping is sent + * @public + */ + ping(data, mask, cb) { + if (this.readyState === WebSocket.CONNECTING) { + throw new Error('WebSocket is not open: readyState 0 (CONNECTING)'); + } + + if (typeof data === 'function') { + cb = data; + data = mask = undefined; + } else if (typeof mask === 'function') { + cb = mask; + mask = undefined; + } + + if (typeof data === 'number') data = data.toString(); + + if (this.readyState !== WebSocket.OPEN) { + sendAfterClose(this, data, cb); + return; + } + + if (mask === undefined) mask = !this._isServer; + this._sender.ping(data || EMPTY_BUFFER, mask, cb); + } + + /** + * Send a pong. + * + * @param {*} [data] The data to send + * @param {Boolean} [mask] Indicates whether or not to mask `data` + * @param {Function} [cb] Callback which is executed when the pong is sent + * @public + */ + pong(data, mask, cb) { + if (this.readyState === WebSocket.CONNECTING) { + throw new Error('WebSocket is not open: readyState 0 (CONNECTING)'); + } + + if (typeof data === 'function') { + cb = data; + data = mask = undefined; + } else if (typeof mask === 'function') { + cb = mask; + mask = undefined; + } + + if (typeof data === 'number') data = data.toString(); + + if (this.readyState !== WebSocket.OPEN) { + sendAfterClose(this, data, cb); + return; + } + + if (mask === undefined) mask = !this._isServer; + this._sender.pong(data || EMPTY_BUFFER, mask, cb); + } + + /** + * Resume the socket. + * + * @public + */ + resume() { + if ( + this.readyState === WebSocket.CONNECTING || + this.readyState === WebSocket.CLOSED + ) { + return; + } + + this._paused = false; + if (!this._receiver._writableState.needDrain) this._socket.resume(); + } + + /** + * Send a data message. + * + * @param {*} data The message to send + * @param {Object} [options] Options object + * @param {Boolean} [options.binary] Specifies whether `data` is binary or + * text + * @param {Boolean} [options.compress] Specifies whether or not to compress + * `data` + * @param {Boolean} [options.fin=true] Specifies whether the fragment is the + * last one + * @param {Boolean} [options.mask] Specifies whether or not to mask `data` + * @param {Function} [cb] Callback which is executed when data is written out + * @public + */ + send(data, options, cb) { + if (this.readyState === WebSocket.CONNECTING) { + throw new Error('WebSocket is not open: readyState 0 (CONNECTING)'); + } + + if (typeof options === 'function') { + cb = options; + options = {}; + } + + if (typeof data === 'number') data = data.toString(); + + if (this.readyState !== WebSocket.OPEN) { + sendAfterClose(this, data, cb); + return; + } + + const opts = { + binary: typeof data !== 'string', + mask: !this._isServer, + compress: true, + fin: true, + ...options + }; + + if (!this._extensions[PerMessageDeflate.extensionName]) { + opts.compress = false; + } + + this._sender.send(data || EMPTY_BUFFER, opts, cb); + } + + /** + * Forcibly close the connection. + * + * @public + */ + terminate() { + if (this.readyState === WebSocket.CLOSED) return; + if (this.readyState === WebSocket.CONNECTING) { + const msg = 'WebSocket was closed before the connection was established'; + abortHandshake(this, this._req, msg); + return; + } + + if (this._socket) { + this._readyState = WebSocket.CLOSING; + this._socket.destroy(); + } + } +} + +/** + * @constant {Number} CONNECTING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} CONNECTING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') +}); + +[ + 'binaryType', + 'bufferedAmount', + 'extensions', + 'isPaused', + 'protocol', + 'readyState', + 'url' +].forEach((property) => { + Object.defineProperty(WebSocket.prototype, property, { enumerable: true }); +}); + +// +// Add the `onopen`, `onerror`, `onclose`, and `onmessage` attributes. +// See https://html.spec.whatwg.org/multipage/comms.html#the-websocket-interface +// +['open', 'error', 'close', 'message'].forEach((method) => { + Object.defineProperty(WebSocket.prototype, `on${method}`, { + enumerable: true, + get() { + for (const listener of this.listeners(method)) { + if (listener[kForOnEventAttribute]) return listener[kListener]; + } + + return null; + }, + set(handler) { + for (const listener of this.listeners(method)) { + if (listener[kForOnEventAttribute]) { + this.removeListener(method, listener); + break; + } + } + + if (typeof handler !== 'function') return; + + this.addEventListener(method, handler, { + [kForOnEventAttribute]: true + }); + } + }); +}); + +WebSocket.prototype.addEventListener = addEventListener; +WebSocket.prototype.removeEventListener = removeEventListener; + +module.exports = WebSocket; + +/** + * Initialize a WebSocket client. + * + * @param {WebSocket} websocket The client to initialize + * @param {(String|URL)} address The URL to which to connect + * @param {Array} protocols The subprotocols + * @param {Object} [options] Connection options + * @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether any + * of the `'message'`, `'ping'`, and `'pong'` events can be emitted multiple + * times in the same tick + * @param {Boolean} [options.autoPong=true] Specifies whether or not to + * automatically send a pong in response to a ping + * @param {Function} [options.finishRequest] A function which can be used to + * customize the headers of each http request before it is sent + * @param {Boolean} [options.followRedirects=false] Whether or not to follow + * redirects + * @param {Function} [options.generateMask] The function used to generate the + * masking key + * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the + * handshake request + * @param {Number} [options.maxPayload=104857600] The maximum allowed message + * size + * @param {Number} [options.maxRedirects=10] The maximum number of redirects + * allowed + * @param {String} [options.origin] Value of the `Origin` or + * `Sec-WebSocket-Origin` header + * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable + * permessage-deflate + * @param {Number} [options.protocolVersion=13] Value of the + * `Sec-WebSocket-Version` header + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages + * @private + */ +function initAsClient(websocket, address, protocols, options) { + const opts = { + allowSynchronousEvents: true, + autoPong: true, + protocolVersion: protocolVersions[1], + maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, + perMessageDeflate: true, + followRedirects: false, + maxRedirects: 10, + ...options, + socketPath: undefined, + hostname: undefined, + protocol: undefined, + timeout: undefined, + method: 'GET', + host: undefined, + path: undefined, + port: undefined + }; + + websocket._autoPong = opts.autoPong; + + if (!protocolVersions.includes(opts.protocolVersion)) { + throw new RangeError( + `Unsupported protocol version: ${opts.protocolVersion} ` + + `(supported versions: ${protocolVersions.join(', ')})` + ); + } + + let parsedUrl; + + if (address instanceof URL) { + parsedUrl = address; + } else { + try { + parsedUrl = new URL(address); + } catch (e) { + throw new SyntaxError(`Invalid URL: ${address}`); + } + } + + if (parsedUrl.protocol === 'http:') { + parsedUrl.protocol = 'ws:'; + } else if (parsedUrl.protocol === 'https:') { + parsedUrl.protocol = 'wss:'; + } + + websocket._url = parsedUrl.href; + + const isSecure = parsedUrl.protocol === 'wss:'; + const isIpcUrl = parsedUrl.protocol === 'ws+unix:'; + let invalidUrlMessage; + + if (parsedUrl.protocol !== 'ws:' && !isSecure && !isIpcUrl) { + invalidUrlMessage = + 'The URL\'s protocol must be one of "ws:", "wss:", ' + + '"http:", "https", or "ws+unix:"'; + } else if (isIpcUrl && !parsedUrl.pathname) { + invalidUrlMessage = "The URL's pathname is empty"; + } else if (parsedUrl.hash) { + invalidUrlMessage = 'The URL contains a fragment identifier'; + } + + if (invalidUrlMessage) { + const err = new SyntaxError(invalidUrlMessage); + + if (websocket._redirects === 0) { + throw err; + } else { + emitErrorAndClose(websocket, err); + return; + } + } + + const defaultPort = isSecure ? 443 : 80; + const key = randomBytes(16).toString('base64'); + const request = isSecure ? https.request : http.request; + const protocolSet = new Set(); + let perMessageDeflate; + + opts.createConnection = + opts.createConnection || (isSecure ? tlsConnect : netConnect); + opts.defaultPort = opts.defaultPort || defaultPort; + opts.port = parsedUrl.port || defaultPort; + opts.host = parsedUrl.hostname.startsWith('[') + ? parsedUrl.hostname.slice(1, -1) + : parsedUrl.hostname; + opts.headers = { + ...opts.headers, + 'Sec-WebSocket-Version': opts.protocolVersion, + 'Sec-WebSocket-Key': key, + Connection: 'Upgrade', + Upgrade: 'websocket' + }; + opts.path = parsedUrl.pathname + parsedUrl.search; + opts.timeout = opts.handshakeTimeout; + + if (opts.perMessageDeflate) { + perMessageDeflate = new PerMessageDeflate( + opts.perMessageDeflate !== true ? opts.perMessageDeflate : {}, + false, + opts.maxPayload + ); + opts.headers['Sec-WebSocket-Extensions'] = format({ + [PerMessageDeflate.extensionName]: perMessageDeflate.offer() + }); + } + if (protocols.length) { + for (const protocol of protocols) { + if ( + typeof protocol !== 'string' || + !subprotocolRegex.test(protocol) || + protocolSet.has(protocol) + ) { + throw new SyntaxError( + 'An invalid or duplicated subprotocol was specified' + ); + } + + protocolSet.add(protocol); + } + + opts.headers['Sec-WebSocket-Protocol'] = protocols.join(','); + } + if (opts.origin) { + if (opts.protocolVersion < 13) { + opts.headers['Sec-WebSocket-Origin'] = opts.origin; + } else { + opts.headers.Origin = opts.origin; + } + } + if (parsedUrl.username || parsedUrl.password) { + opts.auth = `${parsedUrl.username}:${parsedUrl.password}`; + } + + if (isIpcUrl) { + const parts = opts.path.split(':'); + + opts.socketPath = parts[0]; + opts.path = parts[1]; + } + + let req; + + if (opts.followRedirects) { + if (websocket._redirects === 0) { + websocket._originalIpc = isIpcUrl; + websocket._originalSecure = isSecure; + websocket._originalHostOrSocketPath = isIpcUrl + ? opts.socketPath + : parsedUrl.host; + + const headers = options && options.headers; + + // + // Shallow copy the user provided options so that headers can be changed + // without mutating the original object. + // + options = { ...options, headers: {} }; + + if (headers) { + for (const [key, value] of Object.entries(headers)) { + options.headers[key.toLowerCase()] = value; + } + } + } else if (websocket.listenerCount('redirect') === 0) { + const isSameHost = isIpcUrl + ? websocket._originalIpc + ? opts.socketPath === websocket._originalHostOrSocketPath + : false + : websocket._originalIpc + ? false + : parsedUrl.host === websocket._originalHostOrSocketPath; + + if (!isSameHost || (websocket._originalSecure && !isSecure)) { + // + // Match curl 7.77.0 behavior and drop the following headers. These + // headers are also dropped when following a redirect to a subdomain. + // + delete opts.headers.authorization; + delete opts.headers.cookie; + + if (!isSameHost) delete opts.headers.host; + + opts.auth = undefined; + } + } + + // + // Match curl 7.77.0 behavior and make the first `Authorization` header win. + // If the `Authorization` header is set, then there is nothing to do as it + // will take precedence. + // + if (opts.auth && !options.headers.authorization) { + options.headers.authorization = + 'Basic ' + Buffer.from(opts.auth).toString('base64'); + } + + req = websocket._req = request(opts); + + if (websocket._redirects) { + // + // Unlike what is done for the `'upgrade'` event, no early exit is + // triggered here if the user calls `websocket.close()` or + // `websocket.terminate()` from a listener of the `'redirect'` event. This + // is because the user can also call `request.destroy()` with an error + // before calling `websocket.close()` or `websocket.terminate()` and this + // would result in an error being emitted on the `request` object with no + // `'error'` event listeners attached. + // + websocket.emit('redirect', websocket.url, req); + } + } else { + req = websocket._req = request(opts); + } + + if (opts.timeout) { + req.on('timeout', () => { + abortHandshake(websocket, req, 'Opening handshake has timed out'); + }); + } + + req.on('error', (err) => { + if (req === null || req[kAborted]) return; + + req = websocket._req = null; + emitErrorAndClose(websocket, err); + }); + + req.on('response', (res) => { + const location = res.headers.location; + const statusCode = res.statusCode; + + if ( + location && + opts.followRedirects && + statusCode >= 300 && + statusCode < 400 + ) { + if (++websocket._redirects > opts.maxRedirects) { + abortHandshake(websocket, req, 'Maximum redirects exceeded'); + return; + } + + req.abort(); + + let addr; + + try { + addr = new URL(location, address); + } catch (e) { + const err = new SyntaxError(`Invalid URL: ${location}`); + emitErrorAndClose(websocket, err); + return; + } + + initAsClient(websocket, addr, protocols, options); + } else if (!websocket.emit('unexpected-response', req, res)) { + abortHandshake( + websocket, + req, + `Unexpected server response: ${res.statusCode}` + ); + } + }); + + req.on('upgrade', (res, socket, head) => { + websocket.emit('upgrade', res); + + // + // The user may have closed the connection from a listener of the + // `'upgrade'` event. + // + if (websocket.readyState !== WebSocket.CONNECTING) return; + + req = websocket._req = null; + + const upgrade = res.headers.upgrade; + + if (upgrade === undefined || upgrade.toLowerCase() !== 'websocket') { + abortHandshake(websocket, socket, 'Invalid Upgrade header'); + return; + } + + const digest = createHash('sha1') + .update(key + GUID) + .digest('base64'); + + if (res.headers['sec-websocket-accept'] !== digest) { + abortHandshake(websocket, socket, 'Invalid Sec-WebSocket-Accept header'); + return; + } + + const serverProt = res.headers['sec-websocket-protocol']; + let protError; + + if (serverProt !== undefined) { + if (!protocolSet.size) { + protError = 'Server sent a subprotocol but none was requested'; + } else if (!protocolSet.has(serverProt)) { + protError = 'Server sent an invalid subprotocol'; + } + } else if (protocolSet.size) { + protError = 'Server sent no subprotocol'; + } + + if (protError) { + abortHandshake(websocket, socket, protError); + return; + } + + if (serverProt) websocket._protocol = serverProt; + + const secWebSocketExtensions = res.headers['sec-websocket-extensions']; + + if (secWebSocketExtensions !== undefined) { + if (!perMessageDeflate) { + const message = + 'Server sent a Sec-WebSocket-Extensions header but no extension ' + + 'was requested'; + abortHandshake(websocket, socket, message); + return; + } + + let extensions; + + try { + extensions = parse(secWebSocketExtensions); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; + } + + const extensionNames = Object.keys(extensions); + + if ( + extensionNames.length !== 1 || + extensionNames[0] !== PerMessageDeflate.extensionName + ) { + const message = 'Server indicated an extension that was not requested'; + abortHandshake(websocket, socket, message); + return; + } + + try { + perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; + } + + websocket._extensions[PerMessageDeflate.extensionName] = + perMessageDeflate; + } + + websocket.setSocket(socket, head, { + allowSynchronousEvents: opts.allowSynchronousEvents, + generateMask: opts.generateMask, + maxPayload: opts.maxPayload, + skipUTF8Validation: opts.skipUTF8Validation + }); + }); + + if (opts.finishRequest) { + opts.finishRequest(req, websocket); + } else { + req.end(); + } +} + +/** + * Emit the `'error'` and `'close'` events. + * + * @param {WebSocket} websocket The WebSocket instance + * @param {Error} The error to emit + * @private + */ +function emitErrorAndClose(websocket, err) { + websocket._readyState = WebSocket.CLOSING; + // + // The following assignment is practically useless and is done only for + // consistency. + // + websocket._errorEmitted = true; + websocket.emit('error', err); + websocket.emitClose(); +} + +/** + * Create a `net.Socket` and initiate a connection. + * + * @param {Object} options Connection options + * @return {net.Socket} The newly created socket used to start the connection + * @private + */ +function netConnect(options) { + options.path = options.socketPath; + return net.connect(options); +} + +/** + * Create a `tls.TLSSocket` and initiate a connection. + * + * @param {Object} options Connection options + * @return {tls.TLSSocket} The newly created socket used to start the connection + * @private + */ +function tlsConnect(options) { + options.path = undefined; + + if (!options.servername && options.servername !== '') { + options.servername = net.isIP(options.host) ? '' : options.host; + } + + return tls.connect(options); +} + +/** + * Abort the handshake and emit an error. + * + * @param {WebSocket} websocket The WebSocket instance + * @param {(http.ClientRequest|net.Socket|tls.Socket)} stream The request to + * abort or the socket to destroy + * @param {String} message The error message + * @private + */ +function abortHandshake(websocket, stream, message) { + websocket._readyState = WebSocket.CLOSING; + + const err = new Error(message); + Error.captureStackTrace(err, abortHandshake); + + if (stream.setHeader) { + stream[kAborted] = true; + stream.abort(); + + if (stream.socket && !stream.socket.destroyed) { + // + // On Node.js >= 14.3.0 `request.abort()` does not destroy the socket if + // called after the request completed. See + // https://github.com/websockets/ws/issues/1869. + // + stream.socket.destroy(); + } + + process.nextTick(emitErrorAndClose, websocket, err); + } else { + stream.destroy(err); + stream.once('error', websocket.emit.bind(websocket, 'error')); + stream.once('close', websocket.emitClose.bind(websocket)); + } +} + +/** + * Handle cases where the `ping()`, `pong()`, or `send()` methods are called + * when the `readyState` attribute is `CLOSING` or `CLOSED`. + * + * @param {WebSocket} websocket The WebSocket instance + * @param {*} [data] The data to send + * @param {Function} [cb] Callback + * @private + */ +function sendAfterClose(websocket, data, cb) { + if (data) { + const length = isBlob(data) ? data.size : toBuffer(data).length; + + // + // The `_bufferedAmount` property is used only when the peer is a client and + // the opening handshake fails. Under these circumstances, in fact, the + // `setSocket()` method is not called, so the `_socket` and `_sender` + // properties are set to `null`. + // + if (websocket._socket) websocket._sender._bufferedBytes += length; + else websocket._bufferedAmount += length; + } + + if (cb) { + const err = new Error( + `WebSocket is not open: readyState ${websocket.readyState} ` + + `(${readyStates[websocket.readyState]})` + ); + process.nextTick(cb, err); + } +} + +/** + * The listener of the `Receiver` `'conclude'` event. + * + * @param {Number} code The status code + * @param {Buffer} reason The reason for closing + * @private + */ +function receiverOnConclude(code, reason) { + const websocket = this[kWebSocket]; + + websocket._closeFrameReceived = true; + websocket._closeMessage = reason; + websocket._closeCode = code; + + if (websocket._socket[kWebSocket] === undefined) return; + + websocket._socket.removeListener('data', socketOnData); + process.nextTick(resume, websocket._socket); + + if (code === 1005) websocket.close(); + else websocket.close(code, reason); +} + +/** + * The listener of the `Receiver` `'drain'` event. + * + * @private + */ +function receiverOnDrain() { + const websocket = this[kWebSocket]; + + if (!websocket.isPaused) websocket._socket.resume(); +} + +/** + * The listener of the `Receiver` `'error'` event. + * + * @param {(RangeError|Error)} err The emitted error + * @private + */ +function receiverOnError(err) { + const websocket = this[kWebSocket]; + + if (websocket._socket[kWebSocket] !== undefined) { + websocket._socket.removeListener('data', socketOnData); + + // + // On Node.js < 14.0.0 the `'error'` event is emitted synchronously. See + // https://github.com/websockets/ws/issues/1940. + // + process.nextTick(resume, websocket._socket); + + websocket.close(err[kStatusCode]); + } + + if (!websocket._errorEmitted) { + websocket._errorEmitted = true; + websocket.emit('error', err); + } +} + +/** + * The listener of the `Receiver` `'finish'` event. + * + * @private + */ +function receiverOnFinish() { + this[kWebSocket].emitClose(); +} + +/** + * The listener of the `Receiver` `'message'` event. + * + * @param {Buffer|ArrayBuffer|Buffer[])} data The message + * @param {Boolean} isBinary Specifies whether the message is binary or not + * @private + */ +function receiverOnMessage(data, isBinary) { + this[kWebSocket].emit('message', data, isBinary); +} + +/** + * The listener of the `Receiver` `'ping'` event. + * + * @param {Buffer} data The data included in the ping frame + * @private + */ +function receiverOnPing(data) { + const websocket = this[kWebSocket]; + + if (websocket._autoPong) websocket.pong(data, !this._isServer, NOOP); + websocket.emit('ping', data); +} + +/** + * The listener of the `Receiver` `'pong'` event. + * + * @param {Buffer} data The data included in the pong frame + * @private + */ +function receiverOnPong(data) { + this[kWebSocket].emit('pong', data); +} + +/** + * Resume a readable stream + * + * @param {Readable} stream The readable stream + * @private + */ +function resume(stream) { + stream.resume(); +} + +/** + * The `Sender` error event handler. + * + * @param {Error} The error + * @private + */ +function senderOnError(err) { + const websocket = this[kWebSocket]; + + if (websocket.readyState === WebSocket.CLOSED) return; + if (websocket.readyState === WebSocket.OPEN) { + websocket._readyState = WebSocket.CLOSING; + setCloseTimer(websocket); + } + + // + // `socket.end()` is used instead of `socket.destroy()` to allow the other + // peer to finish sending queued data. There is no need to set a timer here + // because `CLOSING` means that it is already set or not needed. + // + this._socket.end(); + + if (!websocket._errorEmitted) { + websocket._errorEmitted = true; + websocket.emit('error', err); + } +} + +/** + * Set a timer to destroy the underlying raw socket of a WebSocket. + * + * @param {WebSocket} websocket The WebSocket instance + * @private + */ +function setCloseTimer(websocket) { + websocket._closeTimer = setTimeout( + websocket._socket.destroy.bind(websocket._socket), + closeTimeout + ); +} + +/** + * The listener of the socket `'close'` event. + * + * @private + */ +function socketOnClose() { + const websocket = this[kWebSocket]; + + this.removeListener('close', socketOnClose); + this.removeListener('data', socketOnData); + this.removeListener('end', socketOnEnd); + + websocket._readyState = WebSocket.CLOSING; + + let chunk; + + // + // The close frame might not have been received or the `'end'` event emitted, + // for example, if the socket was destroyed due to an error. Ensure that the + // `receiver` stream is closed after writing any remaining buffered data to + // it. If the readable side of the socket is in flowing mode then there is no + // buffered data as everything has been already written and `readable.read()` + // will return `null`. If instead, the socket is paused, any possible buffered + // data will be read as a single chunk. + // + if ( + !this._readableState.endEmitted && + !websocket._closeFrameReceived && + !websocket._receiver._writableState.errorEmitted && + (chunk = websocket._socket.read()) !== null + ) { + websocket._receiver.write(chunk); + } + + websocket._receiver.end(); + + this[kWebSocket] = undefined; + + clearTimeout(websocket._closeTimer); + + if ( + websocket._receiver._writableState.finished || + websocket._receiver._writableState.errorEmitted + ) { + websocket.emitClose(); + } else { + websocket._receiver.on('error', receiverOnFinish); + websocket._receiver.on('finish', receiverOnFinish); + } +} + +/** + * The listener of the socket `'data'` event. + * + * @param {Buffer} chunk A chunk of data + * @private + */ +function socketOnData(chunk) { + if (!this[kWebSocket]._receiver.write(chunk)) { + this.pause(); + } +} + +/** + * The listener of the socket `'end'` event. + * + * @private + */ +function socketOnEnd() { + const websocket = this[kWebSocket]; + + websocket._readyState = WebSocket.CLOSING; + websocket._receiver.end(); + this.end(); +} + +/** + * The listener of the socket `'error'` event. + * + * @private + */ +function socketOnError() { + const websocket = this[kWebSocket]; + + this.removeListener('error', socketOnError); + this.on('error', NOOP); + + if (websocket) { + websocket._readyState = WebSocket.CLOSING; + this.destroy(); + } +} + + +/***/ }), + +/***/ 4249: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.StorageClient = void 0; +const StorageFileApi_1 = __importDefault(__nccwpck_require__(710)); +const StorageBucketApi_1 = __importDefault(__nccwpck_require__(3528)); +class StorageClient extends StorageBucketApi_1.default { + constructor(url, headers = {}, fetch) { + super(url, headers, fetch); + } + /** + * Perform file operation in a bucket. + * + * @param id The bucket id to operate on. + */ + from(id) { + return new StorageFileApi_1.default(this.url, this.headers, id, this.fetch); + } +} +exports.StorageClient = StorageClient; +//# sourceMappingURL=StorageClient.js.map + +/***/ }), + +/***/ 5852: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.StorageClient = void 0; +var StorageClient_1 = __nccwpck_require__(4249); +Object.defineProperty(exports, "StorageClient", ({ enumerable: true, get: function () { return StorageClient_1.StorageClient; } })); +__exportStar(__nccwpck_require__(7222), exports); +__exportStar(__nccwpck_require__(2758), exports); +//# sourceMappingURL=index.js.map + +/***/ }), + +/***/ 9754: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DEFAULT_HEADERS = void 0; +const version_1 = __nccwpck_require__(4499); +exports.DEFAULT_HEADERS = { 'X-Client-Info': `storage-js/${version_1.version}` }; +//# sourceMappingURL=constants.js.map + +/***/ }), + +/***/ 2758: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.StorageUnknownError = exports.StorageApiError = exports.isStorageError = exports.StorageError = void 0; +class StorageError extends Error { + constructor(message) { + super(message); + this.__isStorageError = true; + this.name = 'StorageError'; + } +} +exports.StorageError = StorageError; +function isStorageError(error) { + return typeof error === 'object' && error !== null && '__isStorageError' in error; +} +exports.isStorageError = isStorageError; +class StorageApiError extends StorageError { + constructor(message, status) { + super(message); + this.name = 'StorageApiError'; + this.status = status; + } + toJSON() { + return { + name: this.name, + message: this.message, + status: this.status, + }; + } +} +exports.StorageApiError = StorageApiError; +class StorageUnknownError extends StorageError { + constructor(message, originalError) { + super(message); + this.name = 'StorageUnknownError'; + this.originalError = originalError; + } +} +exports.StorageUnknownError = StorageUnknownError; +//# sourceMappingURL=errors.js.map + +/***/ }), + +/***/ 3146: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.remove = exports.head = exports.put = exports.post = exports.get = void 0; +const errors_1 = __nccwpck_require__(2758); +const helpers_1 = __nccwpck_require__(5430); +const _getErrorMessage = (err) => err.msg || err.message || err.error_description || err.error || JSON.stringify(err); +const handleError = (error, reject, options) => __awaiter(void 0, void 0, void 0, function* () { + const Res = yield (0, helpers_1.resolveResponse)(); + if (error instanceof Res && !(options === null || options === void 0 ? void 0 : options.noResolveJson)) { + error + .json() + .then((err) => { + reject(new errors_1.StorageApiError(_getErrorMessage(err), error.status || 500)); + }) + .catch((err) => { + reject(new errors_1.StorageUnknownError(_getErrorMessage(err), err)); + }); + } + else { + reject(new errors_1.StorageUnknownError(_getErrorMessage(error), error)); + } +}); +const _getRequestParams = (method, options, parameters, body) => { + const params = { method, headers: (options === null || options === void 0 ? void 0 : options.headers) || {} }; + if (method === 'GET') { + return params; + } + params.headers = Object.assign({ 'Content-Type': 'application/json' }, options === null || options === void 0 ? void 0 : options.headers); + if (body) { + params.body = JSON.stringify(body); + } + return Object.assign(Object.assign({}, params), parameters); +}; +function _handleRequest(fetcher, method, url, options, parameters, body) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve, reject) => { + fetcher(url, _getRequestParams(method, options, parameters, body)) + .then((result) => { + if (!result.ok) + throw result; + if (options === null || options === void 0 ? void 0 : options.noResolveJson) + return result; + return result.json(); + }) + .then((data) => resolve(data)) + .catch((error) => handleError(error, reject, options)); + }); + }); +} +function get(fetcher, url, options, parameters) { + return __awaiter(this, void 0, void 0, function* () { + return _handleRequest(fetcher, 'GET', url, options, parameters); + }); +} +exports.get = get; +function post(fetcher, url, body, options, parameters) { + return __awaiter(this, void 0, void 0, function* () { + return _handleRequest(fetcher, 'POST', url, options, parameters, body); + }); +} +exports.post = post; +function put(fetcher, url, body, options, parameters) { + return __awaiter(this, void 0, void 0, function* () { + return _handleRequest(fetcher, 'PUT', url, options, parameters, body); + }); +} +exports.put = put; +function head(fetcher, url, options, parameters) { + return __awaiter(this, void 0, void 0, function* () { + return _handleRequest(fetcher, 'HEAD', url, Object.assign(Object.assign({}, options), { noResolveJson: true }), parameters); + }); +} +exports.head = head; +function remove(fetcher, url, body, options, parameters) { + return __awaiter(this, void 0, void 0, function* () { + return _handleRequest(fetcher, 'DELETE', url, options, parameters, body); + }); +} +exports.remove = remove; +//# sourceMappingURL=fetch.js.map + +/***/ }), + +/***/ 5430: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.recursiveToCamel = exports.resolveResponse = exports.resolveFetch = void 0; +const resolveFetch = (customFetch) => { + let _fetch; + if (customFetch) { + _fetch = customFetch; + } + else if (typeof fetch === 'undefined') { + _fetch = (...args) => Promise.resolve().then(() => __importStar(__nccwpck_require__(2668))).then(({ default: fetch }) => fetch(...args)); + } + else { + _fetch = fetch; + } + return (...args) => _fetch(...args); +}; +exports.resolveFetch = resolveFetch; +const resolveResponse = () => __awaiter(void 0, void 0, void 0, function* () { + if (typeof Response === 'undefined') { + // @ts-ignore + return (yield Promise.resolve().then(() => __importStar(__nccwpck_require__(2668)))).Response; + } + return Response; +}); +exports.resolveResponse = resolveResponse; +const recursiveToCamel = (item) => { + if (Array.isArray(item)) { + return item.map((el) => (0, exports.recursiveToCamel)(el)); + } + else if (typeof item === 'function' || item !== Object(item)) { + return item; + } + const result = {}; + Object.entries(item).forEach(([key, value]) => { + const newKey = key.replace(/([-_][a-z])/gi, (c) => c.toUpperCase().replace(/[-_]/g, '')); + result[newKey] = (0, exports.recursiveToCamel)(value); + }); + return result; +}; +exports.recursiveToCamel = recursiveToCamel; +//# sourceMappingURL=helpers.js.map + +/***/ }), + +/***/ 7222: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +//# sourceMappingURL=types.js.map + +/***/ }), + +/***/ 4499: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.version = void 0; +// generated by genversion +exports.version = '2.7.1'; +//# sourceMappingURL=version.js.map + +/***/ }), + +/***/ 3528: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const constants_1 = __nccwpck_require__(9754); +const errors_1 = __nccwpck_require__(2758); +const fetch_1 = __nccwpck_require__(3146); +const helpers_1 = __nccwpck_require__(5430); +class StorageBucketApi { + constructor(url, headers = {}, fetch) { + this.url = url; + this.headers = Object.assign(Object.assign({}, constants_1.DEFAULT_HEADERS), headers); + this.fetch = (0, helpers_1.resolveFetch)(fetch); + } + /** + * Retrieves the details of all Storage buckets within an existing project. + */ + listBuckets() { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield (0, fetch_1.get)(this.fetch, `${this.url}/bucket`, { headers: this.headers }); + return { data, error: null }; + } + catch (error) { + if ((0, errors_1.isStorageError)(error)) { + return { data: null, error }; + } + throw error; + } + }); + } + /** + * Retrieves the details of an existing Storage bucket. + * + * @param id The unique identifier of the bucket you would like to retrieve. + */ + getBucket(id) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield (0, fetch_1.get)(this.fetch, `${this.url}/bucket/${id}`, { headers: this.headers }); + return { data, error: null }; + } + catch (error) { + if ((0, errors_1.isStorageError)(error)) { + return { data: null, error }; + } + throw error; + } + }); + } + /** + * Creates a new Storage bucket + * + * @param id A unique identifier for the bucket you are creating. + * @param options.public The visibility of the bucket. Public buckets don't require an authorization token to download objects, but still require a valid token for all other operations. By default, buckets are private. + * @param options.fileSizeLimit specifies the max file size in bytes that can be uploaded to this bucket. + * The global file size limit takes precedence over this value. + * The default value is null, which doesn't set a per bucket file size limit. + * @param options.allowedMimeTypes specifies the allowed mime types that this bucket can accept during upload. + * The default value is null, which allows files with all mime types to be uploaded. + * Each mime type specified can be a wildcard, e.g. image/*, or a specific mime type, e.g. image/png. + * @returns newly created bucket id + */ + createBucket(id, options = { + public: false, + }) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield (0, fetch_1.post)(this.fetch, `${this.url}/bucket`, { + id, + name: id, + public: options.public, + file_size_limit: options.fileSizeLimit, + allowed_mime_types: options.allowedMimeTypes, + }, { headers: this.headers }); + return { data, error: null }; + } + catch (error) { + if ((0, errors_1.isStorageError)(error)) { + return { data: null, error }; + } + throw error; + } + }); + } + /** + * Updates a Storage bucket + * + * @param id A unique identifier for the bucket you are updating. + * @param options.public The visibility of the bucket. Public buckets don't require an authorization token to download objects, but still require a valid token for all other operations. + * @param options.fileSizeLimit specifies the max file size in bytes that can be uploaded to this bucket. + * The global file size limit takes precedence over this value. + * The default value is null, which doesn't set a per bucket file size limit. + * @param options.allowedMimeTypes specifies the allowed mime types that this bucket can accept during upload. + * The default value is null, which allows files with all mime types to be uploaded. + * Each mime type specified can be a wildcard, e.g. image/*, or a specific mime type, e.g. image/png. + */ + updateBucket(id, options) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield (0, fetch_1.put)(this.fetch, `${this.url}/bucket/${id}`, { + id, + name: id, + public: options.public, + file_size_limit: options.fileSizeLimit, + allowed_mime_types: options.allowedMimeTypes, + }, { headers: this.headers }); + return { data, error: null }; + } + catch (error) { + if ((0, errors_1.isStorageError)(error)) { + return { data: null, error }; + } + throw error; + } + }); + } + /** + * Removes all objects inside a single bucket. + * + * @param id The unique identifier of the bucket you would like to empty. + */ + emptyBucket(id) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield (0, fetch_1.post)(this.fetch, `${this.url}/bucket/${id}/empty`, {}, { headers: this.headers }); + return { data, error: null }; + } + catch (error) { + if ((0, errors_1.isStorageError)(error)) { + return { data: null, error }; + } + throw error; + } + }); + } + /** + * Deletes an existing bucket. A bucket can't be deleted with existing objects inside it. + * You must first `empty()` the bucket. + * + * @param id The unique identifier of the bucket you would like to delete. + */ + deleteBucket(id) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield (0, fetch_1.remove)(this.fetch, `${this.url}/bucket/${id}`, {}, { headers: this.headers }); + return { data, error: null }; + } + catch (error) { + if ((0, errors_1.isStorageError)(error)) { + return { data: null, error }; + } + throw error; + } + }); + } +} +exports["default"] = StorageBucketApi; +//# sourceMappingURL=StorageBucketApi.js.map + +/***/ }), + +/***/ 710: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const errors_1 = __nccwpck_require__(2758); +const fetch_1 = __nccwpck_require__(3146); +const helpers_1 = __nccwpck_require__(5430); +const DEFAULT_SEARCH_OPTIONS = { + limit: 100, + offset: 0, + sortBy: { + column: 'name', + order: 'asc', + }, +}; +const DEFAULT_FILE_OPTIONS = { + cacheControl: '3600', + contentType: 'text/plain;charset=UTF-8', + upsert: false, +}; +class StorageFileApi { + constructor(url, headers = {}, bucketId, fetch) { + this.url = url; + this.headers = headers; + this.bucketId = bucketId; + this.fetch = (0, helpers_1.resolveFetch)(fetch); + } + /** + * Uploads a file to an existing bucket or replaces an existing file at the specified path with a new one. + * + * @param method HTTP method. + * @param path The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload. + * @param fileBody The body of the file to be stored in the bucket. + */ + uploadOrUpdate(method, path, fileBody, fileOptions) { + return __awaiter(this, void 0, void 0, function* () { + try { + let body; + const options = Object.assign(Object.assign({}, DEFAULT_FILE_OPTIONS), fileOptions); + let headers = Object.assign(Object.assign({}, this.headers), (method === 'POST' && { 'x-upsert': String(options.upsert) })); + const metadata = options.metadata; + if (typeof Blob !== 'undefined' && fileBody instanceof Blob) { + body = new FormData(); + body.append('cacheControl', options.cacheControl); + if (metadata) { + body.append('metadata', this.encodeMetadata(metadata)); + } + body.append('', fileBody); + } + else if (typeof FormData !== 'undefined' && fileBody instanceof FormData) { + body = fileBody; + body.append('cacheControl', options.cacheControl); + if (metadata) { + body.append('metadata', this.encodeMetadata(metadata)); + } + } + else { + body = fileBody; + headers['cache-control'] = `max-age=${options.cacheControl}`; + headers['content-type'] = options.contentType; + if (metadata) { + headers['x-metadata'] = this.toBase64(this.encodeMetadata(metadata)); + } + } + if (fileOptions === null || fileOptions === void 0 ? void 0 : fileOptions.headers) { + headers = Object.assign(Object.assign({}, headers), fileOptions.headers); + } + const cleanPath = this._removeEmptyFolders(path); + const _path = this._getFinalPath(cleanPath); + const res = yield this.fetch(`${this.url}/object/${_path}`, Object.assign({ method, body: body, headers }, ((options === null || options === void 0 ? void 0 : options.duplex) ? { duplex: options.duplex } : {}))); + const data = yield res.json(); + if (res.ok) { + return { + data: { path: cleanPath, id: data.Id, fullPath: data.Key }, + error: null, + }; + } + else { + const error = data; + return { data: null, error }; + } + } + catch (error) { + if ((0, errors_1.isStorageError)(error)) { + return { data: null, error }; + } + throw error; + } + }); + } + /** + * Uploads a file to an existing bucket. + * + * @param path The file path, including the file name. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload. + * @param fileBody The body of the file to be stored in the bucket. + */ + upload(path, fileBody, fileOptions) { + return __awaiter(this, void 0, void 0, function* () { + return this.uploadOrUpdate('POST', path, fileBody, fileOptions); + }); + } + /** + * Upload a file with a token generated from `createSignedUploadUrl`. + * @param path The file path, including the file name. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload. + * @param token The token generated from `createSignedUploadUrl` + * @param fileBody The body of the file to be stored in the bucket. + */ + uploadToSignedUrl(path, token, fileBody, fileOptions) { + return __awaiter(this, void 0, void 0, function* () { + const cleanPath = this._removeEmptyFolders(path); + const _path = this._getFinalPath(cleanPath); + const url = new URL(this.url + `/object/upload/sign/${_path}`); + url.searchParams.set('token', token); + try { + let body; + const options = Object.assign({ upsert: DEFAULT_FILE_OPTIONS.upsert }, fileOptions); + const headers = Object.assign(Object.assign({}, this.headers), { 'x-upsert': String(options.upsert) }); + if (typeof Blob !== 'undefined' && fileBody instanceof Blob) { + body = new FormData(); + body.append('cacheControl', options.cacheControl); + body.append('', fileBody); + } + else if (typeof FormData !== 'undefined' && fileBody instanceof FormData) { + body = fileBody; + body.append('cacheControl', options.cacheControl); + } + else { + body = fileBody; + headers['cache-control'] = `max-age=${options.cacheControl}`; + headers['content-type'] = options.contentType; + } + const res = yield this.fetch(url.toString(), { + method: 'PUT', + body: body, + headers, + }); + const data = yield res.json(); + if (res.ok) { + return { + data: { path: cleanPath, fullPath: data.Key }, + error: null, + }; + } + else { + const error = data; + return { data: null, error }; + } + } + catch (error) { + if ((0, errors_1.isStorageError)(error)) { + return { data: null, error }; + } + throw error; + } + }); + } + /** + * Creates a signed upload URL. + * Signed upload URLs can be used to upload files to the bucket without further authentication. + * They are valid for 2 hours. + * @param path The file path, including the current file name. For example `folder/image.png`. + * @param options.upsert If set to true, allows the file to be overwritten if it already exists. + */ + createSignedUploadUrl(path, options) { + return __awaiter(this, void 0, void 0, function* () { + try { + let _path = this._getFinalPath(path); + const headers = Object.assign({}, this.headers); + if (options === null || options === void 0 ? void 0 : options.upsert) { + headers['x-upsert'] = 'true'; + } + const data = yield (0, fetch_1.post)(this.fetch, `${this.url}/object/upload/sign/${_path}`, {}, { headers }); + const url = new URL(this.url + data.url); + const token = url.searchParams.get('token'); + if (!token) { + throw new errors_1.StorageError('No token returned by API'); + } + return { data: { signedUrl: url.toString(), path, token }, error: null }; + } + catch (error) { + if ((0, errors_1.isStorageError)(error)) { + return { data: null, error }; + } + throw error; + } + }); + } + /** + * Replaces an existing file at the specified path with a new one. + * + * @param path The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to update. + * @param fileBody The body of the file to be stored in the bucket. + */ + update(path, fileBody, fileOptions) { + return __awaiter(this, void 0, void 0, function* () { + return this.uploadOrUpdate('PUT', path, fileBody, fileOptions); + }); + } + /** + * Moves an existing file to a new path in the same bucket. + * + * @param fromPath The original file path, including the current file name. For example `folder/image.png`. + * @param toPath The new file path, including the new file name. For example `folder/image-new.png`. + * @param options The destination options. + */ + move(fromPath, toPath, options) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield (0, fetch_1.post)(this.fetch, `${this.url}/object/move`, { + bucketId: this.bucketId, + sourceKey: fromPath, + destinationKey: toPath, + destinationBucket: options === null || options === void 0 ? void 0 : options.destinationBucket, + }, { headers: this.headers }); + return { data, error: null }; + } + catch (error) { + if ((0, errors_1.isStorageError)(error)) { + return { data: null, error }; + } + throw error; + } + }); + } + /** + * Copies an existing file to a new path in the same bucket. + * + * @param fromPath The original file path, including the current file name. For example `folder/image.png`. + * @param toPath The new file path, including the new file name. For example `folder/image-copy.png`. + * @param options The destination options. + */ + copy(fromPath, toPath, options) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield (0, fetch_1.post)(this.fetch, `${this.url}/object/copy`, { + bucketId: this.bucketId, + sourceKey: fromPath, + destinationKey: toPath, + destinationBucket: options === null || options === void 0 ? void 0 : options.destinationBucket, + }, { headers: this.headers }); + return { data: { path: data.Key }, error: null }; + } + catch (error) { + if ((0, errors_1.isStorageError)(error)) { + return { data: null, error }; + } + throw error; + } + }); + } + /** + * Creates a signed URL. Use a signed URL to share a file for a fixed amount of time. + * + * @param path The file path, including the current file name. For example `folder/image.png`. + * @param expiresIn The number of seconds until the signed URL expires. For example, `60` for a URL which is valid for one minute. + * @param options.download triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename. + * @param options.transform Transform the asset before serving it to the client. + */ + createSignedUrl(path, expiresIn, options) { + return __awaiter(this, void 0, void 0, function* () { + try { + let _path = this._getFinalPath(path); + let data = yield (0, fetch_1.post)(this.fetch, `${this.url}/object/sign/${_path}`, Object.assign({ expiresIn }, ((options === null || options === void 0 ? void 0 : options.transform) ? { transform: options.transform } : {})), { headers: this.headers }); + const downloadQueryParam = (options === null || options === void 0 ? void 0 : options.download) + ? `&download=${options.download === true ? '' : options.download}` + : ''; + const signedUrl = encodeURI(`${this.url}${data.signedURL}${downloadQueryParam}`); + data = { signedUrl }; + return { data, error: null }; + } + catch (error) { + if ((0, errors_1.isStorageError)(error)) { + return { data: null, error }; + } + throw error; + } + }); + } + /** + * Creates multiple signed URLs. Use a signed URL to share a file for a fixed amount of time. + * + * @param paths The file paths to be downloaded, including the current file names. For example `['folder/image.png', 'folder2/image2.png']`. + * @param expiresIn The number of seconds until the signed URLs expire. For example, `60` for URLs which are valid for one minute. + * @param options.download triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename. + */ + createSignedUrls(paths, expiresIn, options) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield (0, fetch_1.post)(this.fetch, `${this.url}/object/sign/${this.bucketId}`, { expiresIn, paths }, { headers: this.headers }); + const downloadQueryParam = (options === null || options === void 0 ? void 0 : options.download) + ? `&download=${options.download === true ? '' : options.download}` + : ''; + return { + data: data.map((datum) => (Object.assign(Object.assign({}, datum), { signedUrl: datum.signedURL + ? encodeURI(`${this.url}${datum.signedURL}${downloadQueryParam}`) + : null }))), + error: null, + }; + } + catch (error) { + if ((0, errors_1.isStorageError)(error)) { + return { data: null, error }; + } + throw error; + } + }); + } + /** + * Downloads a file from a private bucket. For public buckets, make a request to the URL returned from `getPublicUrl` instead. + * + * @param path The full path and file name of the file to be downloaded. For example `folder/image.png`. + * @param options.transform Transform the asset before serving it to the client. + */ + download(path, options) { + return __awaiter(this, void 0, void 0, function* () { + const wantsTransformation = typeof (options === null || options === void 0 ? void 0 : options.transform) !== 'undefined'; + const renderPath = wantsTransformation ? 'render/image/authenticated' : 'object'; + const transformationQuery = this.transformOptsToQueryString((options === null || options === void 0 ? void 0 : options.transform) || {}); + const queryString = transformationQuery ? `?${transformationQuery}` : ''; + try { + const _path = this._getFinalPath(path); + const res = yield (0, fetch_1.get)(this.fetch, `${this.url}/${renderPath}/${_path}${queryString}`, { + headers: this.headers, + noResolveJson: true, + }); + const data = yield res.blob(); + return { data, error: null }; + } + catch (error) { + if ((0, errors_1.isStorageError)(error)) { + return { data: null, error }; + } + throw error; + } + }); + } + /** + * Retrieves the details of an existing file. + * @param path + */ + info(path) { + return __awaiter(this, void 0, void 0, function* () { + const _path = this._getFinalPath(path); + try { + const data = yield (0, fetch_1.get)(this.fetch, `${this.url}/object/info/${_path}`, { + headers: this.headers, + }); + return { data: (0, helpers_1.recursiveToCamel)(data), error: null }; + } + catch (error) { + if ((0, errors_1.isStorageError)(error)) { + return { data: null, error }; + } + throw error; + } + }); + } + /** + * Checks the existence of a file. + * @param path + */ + exists(path) { + return __awaiter(this, void 0, void 0, function* () { + const _path = this._getFinalPath(path); + try { + yield (0, fetch_1.head)(this.fetch, `${this.url}/object/${_path}`, { + headers: this.headers, + }); + return { data: true, error: null }; + } + catch (error) { + if ((0, errors_1.isStorageError)(error) && error instanceof errors_1.StorageUnknownError) { + const originalError = error.originalError; + if ([400, 404].includes(originalError === null || originalError === void 0 ? void 0 : originalError.status)) { + return { data: false, error }; + } + } + throw error; + } + }); + } + /** + * A simple convenience function to get the URL for an asset in a public bucket. If you do not want to use this function, you can construct the public URL by concatenating the bucket URL with the path to the asset. + * This function does not verify if the bucket is public. If a public URL is created for a bucket which is not public, you will not be able to download the asset. + * + * @param path The path and name of the file to generate the public URL for. For example `folder/image.png`. + * @param options.download Triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename. + * @param options.transform Transform the asset before serving it to the client. + */ + getPublicUrl(path, options) { + const _path = this._getFinalPath(path); + const _queryString = []; + const downloadQueryParam = (options === null || options === void 0 ? void 0 : options.download) + ? `download=${options.download === true ? '' : options.download}` + : ''; + if (downloadQueryParam !== '') { + _queryString.push(downloadQueryParam); + } + const wantsTransformation = typeof (options === null || options === void 0 ? void 0 : options.transform) !== 'undefined'; + const renderPath = wantsTransformation ? 'render/image' : 'object'; + const transformationQuery = this.transformOptsToQueryString((options === null || options === void 0 ? void 0 : options.transform) || {}); + if (transformationQuery !== '') { + _queryString.push(transformationQuery); + } + let queryString = _queryString.join('&'); + if (queryString !== '') { + queryString = `?${queryString}`; + } + return { + data: { publicUrl: encodeURI(`${this.url}/${renderPath}/public/${_path}${queryString}`) }, + }; + } + /** + * Deletes files within the same bucket + * + * @param paths An array of files to delete, including the path and file name. For example [`'folder/image.png'`]. + */ + remove(paths) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield (0, fetch_1.remove)(this.fetch, `${this.url}/object/${this.bucketId}`, { prefixes: paths }, { headers: this.headers }); + return { data, error: null }; + } + catch (error) { + if ((0, errors_1.isStorageError)(error)) { + return { data: null, error }; + } + throw error; + } + }); + } + /** + * Get file metadata + * @param id the file id to retrieve metadata + */ + // async getMetadata( + // id: string + // ): Promise< + // | { + // data: Metadata + // error: null + // } + // | { + // data: null + // error: StorageError + // } + // > { + // try { + // const data = await get(this.fetch, `${this.url}/metadata/${id}`, { headers: this.headers }) + // return { data, error: null } + // } catch (error) { + // if (isStorageError(error)) { + // return { data: null, error } + // } + // throw error + // } + // } + /** + * Update file metadata + * @param id the file id to update metadata + * @param meta the new file metadata + */ + // async updateMetadata( + // id: string, + // meta: Metadata + // ): Promise< + // | { + // data: Metadata + // error: null + // } + // | { + // data: null + // error: StorageError + // } + // > { + // try { + // const data = await post( + // this.fetch, + // `${this.url}/metadata/${id}`, + // { ...meta }, + // { headers: this.headers } + // ) + // return { data, error: null } + // } catch (error) { + // if (isStorageError(error)) { + // return { data: null, error } + // } + // throw error + // } + // } + /** + * Lists all the files within a bucket. + * @param path The folder path. + */ + list(path, options, parameters) { + return __awaiter(this, void 0, void 0, function* () { + try { + const body = Object.assign(Object.assign(Object.assign({}, DEFAULT_SEARCH_OPTIONS), options), { prefix: path || '' }); + const data = yield (0, fetch_1.post)(this.fetch, `${this.url}/object/list/${this.bucketId}`, body, { headers: this.headers }, parameters); + return { data, error: null }; + } + catch (error) { + if ((0, errors_1.isStorageError)(error)) { + return { data: null, error }; + } + throw error; + } + }); + } + encodeMetadata(metadata) { + return JSON.stringify(metadata); + } + toBase64(data) { + if (typeof Buffer !== 'undefined') { + return Buffer.from(data).toString('base64'); + } + return btoa(data); + } + _getFinalPath(path) { + return `${this.bucketId}/${path}`; + } + _removeEmptyFolders(path) { + return path.replace(/^\/|\/$/g, '').replace(/\/+/g, '/'); + } + transformOptsToQueryString(transform) { + const params = []; + if (transform.width) { + params.push(`width=${transform.width}`); + } + if (transform.height) { + params.push(`height=${transform.height}`); + } + if (transform.resize) { + params.push(`resize=${transform.resize}`); + } + if (transform.format) { + params.push(`format=${transform.format}`); + } + if (transform.quality) { + params.push(`quality=${transform.quality}`); + } + return params.join('&'); + } +} +exports["default"] = StorageFileApi; +//# sourceMappingURL=StorageFileApi.js.map + +/***/ }), + +/***/ 1807: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const functions_js_1 = __nccwpck_require__(8519); +const postgrest_js_1 = __nccwpck_require__(1178); +const realtime_js_1 = __nccwpck_require__(442); +const storage_js_1 = __nccwpck_require__(5852); +const constants_1 = __nccwpck_require__(4868); +const fetch_1 = __nccwpck_require__(785); +const helpers_1 = __nccwpck_require__(3575); +const SupabaseAuthClient_1 = __nccwpck_require__(7620); +/** + * Supabase Client. + * + * An isomorphic Javascript client for interacting with Postgres. + */ +class SupabaseClient { + /** + * Create a new client for use in the browser. + * @param supabaseUrl The unique Supabase URL which is supplied when you create a new project in your project dashboard. + * @param supabaseKey The unique Supabase Key which is supplied when you create a new project in your project dashboard. + * @param options.db.schema You can switch in between schemas. The schema needs to be on the list of exposed schemas inside Supabase. + * @param options.auth.autoRefreshToken Set to "true" if you want to automatically refresh the token before expiring. + * @param options.auth.persistSession Set to "true" if you want to automatically save the user session into local storage. + * @param options.auth.detectSessionInUrl Set to "true" if you want to automatically detects OAuth grants in the URL and signs in the user. + * @param options.realtime Options passed along to realtime-js constructor. + * @param options.global.fetch A custom fetch implementation. + * @param options.global.headers Any additional headers to send with each network request. + */ + constructor(supabaseUrl, supabaseKey, options) { + var _a, _b, _c; + this.supabaseUrl = supabaseUrl; + this.supabaseKey = supabaseKey; + if (!supabaseUrl) + throw new Error('supabaseUrl is required.'); + if (!supabaseKey) + throw new Error('supabaseKey is required.'); + const _supabaseUrl = (0, helpers_1.stripTrailingSlash)(supabaseUrl); + this.realtimeUrl = `${_supabaseUrl}/realtime/v1`.replace(/^http/i, 'ws'); + this.authUrl = `${_supabaseUrl}/auth/v1`; + this.storageUrl = `${_supabaseUrl}/storage/v1`; + this.functionsUrl = `${_supabaseUrl}/functions/v1`; + // default storage key uses the supabase project ref as a namespace + const defaultStorageKey = `sb-${new URL(this.authUrl).hostname.split('.')[0]}-auth-token`; + const DEFAULTS = { + db: constants_1.DEFAULT_DB_OPTIONS, + realtime: constants_1.DEFAULT_REALTIME_OPTIONS, + auth: Object.assign(Object.assign({}, constants_1.DEFAULT_AUTH_OPTIONS), { storageKey: defaultStorageKey }), + global: constants_1.DEFAULT_GLOBAL_OPTIONS, + }; + const settings = (0, helpers_1.applySettingDefaults)(options !== null && options !== void 0 ? options : {}, DEFAULTS); + this.storageKey = (_a = settings.auth.storageKey) !== null && _a !== void 0 ? _a : ''; + this.headers = (_b = settings.global.headers) !== null && _b !== void 0 ? _b : {}; + if (!settings.accessToken) { + this.auth = this._initSupabaseAuthClient((_c = settings.auth) !== null && _c !== void 0 ? _c : {}, this.headers, settings.global.fetch); + } + else { + this.accessToken = settings.accessToken; + this.auth = new Proxy({}, { + get: (_, prop) => { + throw new Error(`@supabase/supabase-js: Supabase Client is configured with the accessToken option, accessing supabase.auth.${String(prop)} is not possible`); + }, + }); + } + this.fetch = (0, fetch_1.fetchWithAuth)(supabaseKey, this._getAccessToken.bind(this), settings.global.fetch); + this.realtime = this._initRealtimeClient(Object.assign({ headers: this.headers, accessToken: this._getAccessToken.bind(this) }, settings.realtime)); + this.rest = new postgrest_js_1.PostgrestClient(`${_supabaseUrl}/rest/v1`, { + headers: this.headers, + schema: settings.db.schema, + fetch: this.fetch, + }); + if (!settings.accessToken) { + this._listenForAuthEvents(); + } + } + /** + * Supabase Functions allows you to deploy and invoke edge functions. + */ + get functions() { + return new functions_js_1.FunctionsClient(this.functionsUrl, { + headers: this.headers, + customFetch: this.fetch, + }); + } + /** + * Supabase Storage allows you to manage user-generated content, such as photos or videos. + */ + get storage() { + return new storage_js_1.StorageClient(this.storageUrl, this.headers, this.fetch); + } + /** + * Perform a query on a table or a view. + * + * @param relation - The table or view name to query + */ + from(relation) { + return this.rest.from(relation); + } + // NOTE: signatures must be kept in sync with PostgrestClient.schema + /** + * Select a schema to query or perform an function (rpc) call. + * + * The schema needs to be on the list of exposed schemas inside Supabase. + * + * @param schema - The schema to query + */ + schema(schema) { + return this.rest.schema(schema); + } + // NOTE: signatures must be kept in sync with PostgrestClient.rpc + /** + * Perform a function call. + * + * @param fn - The function name to call + * @param args - The arguments to pass to the function call + * @param options - Named parameters + * @param options.head - When set to `true`, `data` will not be returned. + * Useful if you only need the count. + * @param options.get - When set to `true`, the function will be called with + * read-only access mode. + * @param options.count - Count algorithm to use to count rows returned by the + * function. Only applicable for [set-returning + * functions](https://www.postgresql.org/docs/current/functions-srf.html). + * + * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the + * hood. + * + * `"planned"`: Approximated but fast count algorithm. Uses the Postgres + * statistics under the hood. + * + * `"estimated"`: Uses exact count for low numbers and planned count for high + * numbers. + */ + rpc(fn, args = {}, options = {}) { + return this.rest.rpc(fn, args, options); + } + /** + * Creates a Realtime channel with Broadcast, Presence, and Postgres Changes. + * + * @param {string} name - The name of the Realtime channel. + * @param {Object} opts - The options to pass to the Realtime channel. + * + */ + channel(name, opts = { config: {} }) { + return this.realtime.channel(name, opts); + } + /** + * Returns all Realtime channels. + */ + getChannels() { + return this.realtime.getChannels(); + } + /** + * Unsubscribes and removes Realtime channel from Realtime client. + * + * @param {RealtimeChannel} channel - The name of the Realtime channel. + * + */ + removeChannel(channel) { + return this.realtime.removeChannel(channel); + } + /** + * Unsubscribes and removes all Realtime channels from Realtime client. + */ + removeAllChannels() { + return this.realtime.removeAllChannels(); + } + _getAccessToken() { + var _a, _b; + return __awaiter(this, void 0, void 0, function* () { + if (this.accessToken) { + return yield this.accessToken(); + } + const { data } = yield this.auth.getSession(); + return (_b = (_a = data.session) === null || _a === void 0 ? void 0 : _a.access_token) !== null && _b !== void 0 ? _b : null; + }); + } + _initSupabaseAuthClient({ autoRefreshToken, persistSession, detectSessionInUrl, storage, storageKey, flowType, lock, debug, }, headers, fetch) { + const authHeaders = { + Authorization: `Bearer ${this.supabaseKey}`, + apikey: `${this.supabaseKey}`, + }; + return new SupabaseAuthClient_1.SupabaseAuthClient({ + url: this.authUrl, + headers: Object.assign(Object.assign({}, authHeaders), headers), + storageKey: storageKey, + autoRefreshToken, + persistSession, + detectSessionInUrl, + storage, + flowType, + lock, + debug, + fetch, + // auth checks if there is a custom authorizaiton header using this flag + // so it knows whether to return an error when getUser is called with no session + hasCustomAuthorizationHeader: 'Authorization' in this.headers, + }); + } + _initRealtimeClient(options) { + return new realtime_js_1.RealtimeClient(this.realtimeUrl, Object.assign(Object.assign({}, options), { params: Object.assign({ apikey: this.supabaseKey }, options === null || options === void 0 ? void 0 : options.params) })); + } + _listenForAuthEvents() { + let data = this.auth.onAuthStateChange((event, session) => { + this._handleTokenChanged(event, 'CLIENT', session === null || session === void 0 ? void 0 : session.access_token); + }); + return data; + } + _handleTokenChanged(event, source, token) { + if ((event === 'TOKEN_REFRESHED' || event === 'SIGNED_IN') && + this.changedAccessToken !== token) { + this.changedAccessToken = token; + } + else if (event === 'SIGNED_OUT') { + this.realtime.setAuth(); + if (source == 'STORAGE') + this.auth.signOut(); + this.changedAccessToken = undefined; + } + } +} +exports["default"] = SupabaseClient; +//# sourceMappingURL=SupabaseClient.js.map + +/***/ }), + +/***/ 1206: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.createClient = exports.SupabaseClient = exports.FunctionRegion = exports.FunctionsError = exports.FunctionsRelayError = exports.FunctionsFetchError = exports.FunctionsHttpError = exports.PostgrestError = void 0; +const SupabaseClient_1 = __importDefault(__nccwpck_require__(1807)); +__exportStar(__nccwpck_require__(6748), exports); +var postgrest_js_1 = __nccwpck_require__(1178); +Object.defineProperty(exports, "PostgrestError", ({ enumerable: true, get: function () { return postgrest_js_1.PostgrestError; } })); +var functions_js_1 = __nccwpck_require__(8519); +Object.defineProperty(exports, "FunctionsHttpError", ({ enumerable: true, get: function () { return functions_js_1.FunctionsHttpError; } })); +Object.defineProperty(exports, "FunctionsFetchError", ({ enumerable: true, get: function () { return functions_js_1.FunctionsFetchError; } })); +Object.defineProperty(exports, "FunctionsRelayError", ({ enumerable: true, get: function () { return functions_js_1.FunctionsRelayError; } })); +Object.defineProperty(exports, "FunctionsError", ({ enumerable: true, get: function () { return functions_js_1.FunctionsError; } })); +Object.defineProperty(exports, "FunctionRegion", ({ enumerable: true, get: function () { return functions_js_1.FunctionRegion; } })); +__exportStar(__nccwpck_require__(442), exports); +var SupabaseClient_2 = __nccwpck_require__(1807); +Object.defineProperty(exports, "SupabaseClient", ({ enumerable: true, get: function () { return __importDefault(SupabaseClient_2).default; } })); +/** + * Creates a new Supabase Client. + */ +const createClient = (supabaseUrl, supabaseKey, options) => { + return new SupabaseClient_1.default(supabaseUrl, supabaseKey, options); +}; +exports.createClient = createClient; +//# sourceMappingURL=index.js.map + +/***/ }), + +/***/ 7620: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SupabaseAuthClient = void 0; +const auth_js_1 = __nccwpck_require__(6748); +class SupabaseAuthClient extends auth_js_1.AuthClient { + constructor(options) { + super(options); + } +} +exports.SupabaseAuthClient = SupabaseAuthClient; +//# sourceMappingURL=SupabaseAuthClient.js.map + +/***/ }), + +/***/ 4868: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DEFAULT_REALTIME_OPTIONS = exports.DEFAULT_AUTH_OPTIONS = exports.DEFAULT_DB_OPTIONS = exports.DEFAULT_GLOBAL_OPTIONS = exports.DEFAULT_HEADERS = void 0; +const version_1 = __nccwpck_require__(6136); +let JS_ENV = ''; +// @ts-ignore +if (typeof Deno !== 'undefined') { + JS_ENV = 'deno'; +} +else if (typeof document !== 'undefined') { + JS_ENV = 'web'; +} +else if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') { + JS_ENV = 'react-native'; +} +else { + JS_ENV = 'node'; +} +exports.DEFAULT_HEADERS = { 'X-Client-Info': `supabase-js-${JS_ENV}/${version_1.version}` }; +exports.DEFAULT_GLOBAL_OPTIONS = { + headers: exports.DEFAULT_HEADERS, +}; +exports.DEFAULT_DB_OPTIONS = { + schema: 'public', +}; +exports.DEFAULT_AUTH_OPTIONS = { + autoRefreshToken: true, + persistSession: true, + detectSessionInUrl: true, + flowType: 'implicit', +}; +exports.DEFAULT_REALTIME_OPTIONS = {}; +//# sourceMappingURL=constants.js.map + +/***/ }), + +/***/ 785: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.fetchWithAuth = exports.resolveHeadersConstructor = exports.resolveFetch = void 0; +// @ts-ignore +const node_fetch_1 = __importStar(__nccwpck_require__(2668)); +const resolveFetch = (customFetch) => { + let _fetch; + if (customFetch) { + _fetch = customFetch; + } + else if (typeof fetch === 'undefined') { + _fetch = node_fetch_1.default; + } + else { + _fetch = fetch; + } + return (...args) => _fetch(...args); +}; +exports.resolveFetch = resolveFetch; +const resolveHeadersConstructor = () => { + if (typeof Headers === 'undefined') { + return node_fetch_1.Headers; + } + return Headers; +}; +exports.resolveHeadersConstructor = resolveHeadersConstructor; +const fetchWithAuth = (supabaseKey, getAccessToken, customFetch) => { + const fetch = (0, exports.resolveFetch)(customFetch); + const HeadersConstructor = (0, exports.resolveHeadersConstructor)(); + return (input, init) => __awaiter(void 0, void 0, void 0, function* () { + var _a; + const accessToken = (_a = (yield getAccessToken())) !== null && _a !== void 0 ? _a : supabaseKey; + let headers = new HeadersConstructor(init === null || init === void 0 ? void 0 : init.headers); + if (!headers.has('apikey')) { + headers.set('apikey', supabaseKey); + } + if (!headers.has('Authorization')) { + headers.set('Authorization', `Bearer ${accessToken}`); + } + return fetch(input, Object.assign(Object.assign({}, init), { headers })); + }); +}; +exports.fetchWithAuth = fetchWithAuth; +//# sourceMappingURL=fetch.js.map + +/***/ }), + +/***/ 3575: +/***/ (function(__unused_webpack_module, exports) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.applySettingDefaults = exports.isBrowser = exports.stripTrailingSlash = exports.uuid = void 0; +function uuid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} +exports.uuid = uuid; +function stripTrailingSlash(url) { + return url.replace(/\/$/, ''); +} +exports.stripTrailingSlash = stripTrailingSlash; +const isBrowser = () => typeof window !== 'undefined'; +exports.isBrowser = isBrowser; +function applySettingDefaults(options, defaults) { + const { db: dbOptions, auth: authOptions, realtime: realtimeOptions, global: globalOptions, } = options; + const { db: DEFAULT_DB_OPTIONS, auth: DEFAULT_AUTH_OPTIONS, realtime: DEFAULT_REALTIME_OPTIONS, global: DEFAULT_GLOBAL_OPTIONS, } = defaults; + const result = { + db: Object.assign(Object.assign({}, DEFAULT_DB_OPTIONS), dbOptions), + auth: Object.assign(Object.assign({}, DEFAULT_AUTH_OPTIONS), authOptions), + realtime: Object.assign(Object.assign({}, DEFAULT_REALTIME_OPTIONS), realtimeOptions), + global: Object.assign(Object.assign({}, DEFAULT_GLOBAL_OPTIONS), globalOptions), + accessToken: () => __awaiter(this, void 0, void 0, function* () { return ''; }), + }; + if (options.accessToken) { + result.accessToken = options.accessToken; + } + else { + // hack around Required<> + delete result.accessToken; + } + return result; +} +exports.applySettingDefaults = applySettingDefaults; +//# sourceMappingURL=helpers.js.map + +/***/ }), + +/***/ 6136: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.version = void 0; +exports.version = '2.48.1'; +//# sourceMappingURL=version.js.map + +/***/ }), + +/***/ 4256: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +var punycode = __nccwpck_require__(5477); +var mappingTable = __nccwpck_require__(2020); + +var PROCESSING_OPTIONS = { + TRANSITIONAL: 0, + NONTRANSITIONAL: 1 +}; + +function normalize(str) { // fix bug in v8 + return str.split('\u0000').map(function (s) { return s.normalize('NFC'); }).join('\u0000'); +} + +function findStatus(val) { + var start = 0; + var end = mappingTable.length - 1; + + while (start <= end) { + var mid = Math.floor((start + end) / 2); + + var target = mappingTable[mid]; + if (target[0][0] <= val && target[0][1] >= val) { + return target; + } else if (target[0][0] > val) { + end = mid - 1; + } else { + start = mid + 1; + } + } + + return null; +} + +var regexAstralSymbols = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; + +function countSymbols(string) { + return string + // replace every surrogate pair with a BMP symbol + .replace(regexAstralSymbols, '_') + // then get the length + .length; +} + +function mapChars(domain_name, useSTD3, processing_option) { + var hasError = false; + var processed = ""; + + var len = countSymbols(domain_name); + for (var i = 0; i < len; ++i) { + var codePoint = domain_name.codePointAt(i); + var status = findStatus(codePoint); + + switch (status[1]) { + case "disallowed": + hasError = true; + processed += String.fromCodePoint(codePoint); + break; + case "ignored": + break; + case "mapped": + processed += String.fromCodePoint.apply(String, status[2]); + break; + case "deviation": + if (processing_option === PROCESSING_OPTIONS.TRANSITIONAL) { + processed += String.fromCodePoint.apply(String, status[2]); + } else { + processed += String.fromCodePoint(codePoint); + } + break; + case "valid": + processed += String.fromCodePoint(codePoint); + break; + case "disallowed_STD3_mapped": + if (useSTD3) { + hasError = true; + processed += String.fromCodePoint(codePoint); + } else { + processed += String.fromCodePoint.apply(String, status[2]); + } + break; + case "disallowed_STD3_valid": + if (useSTD3) { + hasError = true; + } + + processed += String.fromCodePoint(codePoint); + break; + } + } + + return { + string: processed, + error: hasError + }; +} + +var combiningMarksRegex = /[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08E4-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D01-\u0D03\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u18A9\u1920-\u192B\u1930-\u193B\u19B0-\u19C0\u19C8\u19C9\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF5\u1DFC-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C4\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2D]|\uD800[\uDDFD\uDEE0\uDF76-\uDF7A]|\uD802[\uDE01-\uDE03\uDE05\uDE06\uDE0C-\uDE0F\uDE38-\uDE3A\uDE3F\uDEE5\uDEE6]|\uD804[\uDC00-\uDC02\uDC38-\uDC46\uDC7F-\uDC82\uDCB0-\uDCBA\uDD00-\uDD02\uDD27-\uDD34\uDD73\uDD80-\uDD82\uDDB3-\uDDC0\uDE2C-\uDE37\uDEDF-\uDEEA\uDF01-\uDF03\uDF3C\uDF3E-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF57\uDF62\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDCB0-\uDCC3\uDDAF-\uDDB5\uDDB8-\uDDC0\uDE30-\uDE40\uDEAB-\uDEB7]|\uD81A[\uDEF0-\uDEF4\uDF30-\uDF36]|\uD81B[\uDF51-\uDF7E\uDF8F-\uDF92]|\uD82F[\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD83A[\uDCD0-\uDCD6]|\uDB40[\uDD00-\uDDEF]/; + +function validateLabel(label, processing_option) { + if (label.substr(0, 4) === "xn--") { + label = punycode.toUnicode(label); + processing_option = PROCESSING_OPTIONS.NONTRANSITIONAL; + } + + var error = false; + + if (normalize(label) !== label || + (label[3] === "-" && label[4] === "-") || + label[0] === "-" || label[label.length - 1] === "-" || + label.indexOf(".") !== -1 || + label.search(combiningMarksRegex) === 0) { + error = true; + } + + var len = countSymbols(label); + for (var i = 0; i < len; ++i) { + var status = findStatus(label.codePointAt(i)); + if ((processing === PROCESSING_OPTIONS.TRANSITIONAL && status[1] !== "valid") || + (processing === PROCESSING_OPTIONS.NONTRANSITIONAL && + status[1] !== "valid" && status[1] !== "deviation")) { + error = true; + break; + } + } + + return { + label: label, + error: error + }; +} + +function processing(domain_name, useSTD3, processing_option) { + var result = mapChars(domain_name, useSTD3, processing_option); + result.string = normalize(result.string); + + var labels = result.string.split("."); + for (var i = 0; i < labels.length; ++i) { + try { + var validation = validateLabel(labels[i]); + labels[i] = validation.label; + result.error = result.error || validation.error; + } catch(e) { + result.error = true; + } + } + + return { + string: labels.join("."), + error: result.error + }; +} + +module.exports.toASCII = function(domain_name, useSTD3, processing_option, verifyDnsLength) { + var result = processing(domain_name, useSTD3, processing_option); + var labels = result.string.split("."); + labels = labels.map(function(l) { + try { + return punycode.toASCII(l); + } catch(e) { + result.error = true; + return l; + } + }); + + if (verifyDnsLength) { + var total = labels.slice(0, labels.length - 1).join(".").length; + if (total.length > 253 || total.length === 0) { + result.error = true; + } + + for (var i=0; i < labels.length; ++i) { + if (labels.length > 63 || labels.length === 0) { + result.error = true; + break; + } + } + } + + if (result.error) return null; + return labels.join("."); +}; + +module.exports.toUnicode = function(domain_name, useSTD3) { + var result = processing(domain_name, useSTD3, PROCESSING_OPTIONS.NONTRANSITIONAL); + + return { + domain: result.string, + error: result.error + }; +}; + +module.exports.PROCESSING_OPTIONS = PROCESSING_OPTIONS; + + +/***/ }), + +/***/ 4294: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +module.exports = __nccwpck_require__(4219); + + +/***/ }), + +/***/ 4219: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +var net = __nccwpck_require__(1808); +var tls = __nccwpck_require__(4404); +var http = __nccwpck_require__(3685); +var https = __nccwpck_require__(5687); +var events = __nccwpck_require__(2361); +var assert = __nccwpck_require__(9491); +var util = __nccwpck_require__(3837); + + +exports.httpOverHttp = httpOverHttp; +exports.httpsOverHttp = httpsOverHttp; +exports.httpOverHttps = httpOverHttps; +exports.httpsOverHttps = httpsOverHttps; + + +function httpOverHttp(options) { + var agent = new TunnelingAgent(options); + agent.request = http.request; + return agent; +} + +function httpsOverHttp(options) { + var agent = new TunnelingAgent(options); + agent.request = http.request; + agent.createSocket = createSecureSocket; + agent.defaultPort = 443; + return agent; +} + +function httpOverHttps(options) { + var agent = new TunnelingAgent(options); + agent.request = https.request; + return agent; +} + +function httpsOverHttps(options) { + var agent = new TunnelingAgent(options); + agent.request = https.request; + agent.createSocket = createSecureSocket; + agent.defaultPort = 443; + return agent; +} + + +function TunnelingAgent(options) { + var self = this; + self.options = options || {}; + self.proxyOptions = self.options.proxy || {}; + self.maxSockets = self.options.maxSockets || http.Agent.defaultMaxSockets; + self.requests = []; + self.sockets = []; + + self.on('free', function onFree(socket, host, port, localAddress) { + var options = toOptions(host, port, localAddress); + for (var i = 0, len = self.requests.length; i < len; ++i) { + var pending = self.requests[i]; + if (pending.host === options.host && pending.port === options.port) { + // Detect the request to connect same origin server, + // reuse the connection. + self.requests.splice(i, 1); + pending.request.onSocket(socket); + return; + } + } + socket.destroy(); + self.removeSocket(socket); + }); +} +util.inherits(TunnelingAgent, events.EventEmitter); + +TunnelingAgent.prototype.addRequest = function addRequest(req, host, port, localAddress) { + var self = this; + var options = mergeOptions({request: req}, self.options, toOptions(host, port, localAddress)); + + if (self.sockets.length >= this.maxSockets) { + // We are over limit so we'll add it to the queue. + self.requests.push(options); + return; + } + + // If we are under maxSockets create a new one. + self.createSocket(options, function(socket) { + socket.on('free', onFree); + socket.on('close', onCloseOrRemove); + socket.on('agentRemove', onCloseOrRemove); + req.onSocket(socket); + + function onFree() { + self.emit('free', socket, options); + } + + function onCloseOrRemove(err) { + self.removeSocket(socket); + socket.removeListener('free', onFree); + socket.removeListener('close', onCloseOrRemove); + socket.removeListener('agentRemove', onCloseOrRemove); + } + }); +}; + +TunnelingAgent.prototype.createSocket = function createSocket(options, cb) { + var self = this; + var placeholder = {}; + self.sockets.push(placeholder); + + var connectOptions = mergeOptions({}, self.proxyOptions, { + method: 'CONNECT', + path: options.host + ':' + options.port, + agent: false, + headers: { + host: options.host + ':' + options.port + } + }); + if (options.localAddress) { + connectOptions.localAddress = options.localAddress; + } + if (connectOptions.proxyAuth) { + connectOptions.headers = connectOptions.headers || {}; + connectOptions.headers['Proxy-Authorization'] = 'Basic ' + + new Buffer(connectOptions.proxyAuth).toString('base64'); + } + + debug('making CONNECT request'); + var connectReq = self.request(connectOptions); + connectReq.useChunkedEncodingByDefault = false; // for v0.6 + connectReq.once('response', onResponse); // for v0.6 + connectReq.once('upgrade', onUpgrade); // for v0.6 + connectReq.once('connect', onConnect); // for v0.7 or later + connectReq.once('error', onError); + connectReq.end(); + + function onResponse(res) { + // Very hacky. This is necessary to avoid http-parser leaks. + res.upgrade = true; + } + + function onUpgrade(res, socket, head) { + // Hacky. + process.nextTick(function() { + onConnect(res, socket, head); + }); + } + + function onConnect(res, socket, head) { + connectReq.removeAllListeners(); + socket.removeAllListeners(); + + if (res.statusCode !== 200) { + debug('tunneling socket could not be established, statusCode=%d', + res.statusCode); + socket.destroy(); + var error = new Error('tunneling socket could not be established, ' + + 'statusCode=' + res.statusCode); + error.code = 'ECONNRESET'; + options.request.emit('error', error); + self.removeSocket(placeholder); + return; + } + if (head.length > 0) { + debug('got illegal response body from proxy'); + socket.destroy(); + var error = new Error('got illegal response body from proxy'); + error.code = 'ECONNRESET'; + options.request.emit('error', error); + self.removeSocket(placeholder); + return; + } + debug('tunneling connection has established'); + self.sockets[self.sockets.indexOf(placeholder)] = socket; + return cb(socket); + } + + function onError(cause) { + connectReq.removeAllListeners(); + + debug('tunneling socket could not be established, cause=%s\n', + cause.message, cause.stack); + var error = new Error('tunneling socket could not be established, ' + + 'cause=' + cause.message); + error.code = 'ECONNRESET'; + options.request.emit('error', error); + self.removeSocket(placeholder); + } +}; + +TunnelingAgent.prototype.removeSocket = function removeSocket(socket) { + var pos = this.sockets.indexOf(socket) + if (pos === -1) { + return; + } + this.sockets.splice(pos, 1); + + var pending = this.requests.shift(); + if (pending) { + // If we have pending requests and a socket gets closed a new one + // needs to be created to take over in the pool for the one that closed. + this.createSocket(pending, function(socket) { + pending.request.onSocket(socket); + }); + } +}; + +function createSecureSocket(options, cb) { + var self = this; + TunnelingAgent.prototype.createSocket.call(self, options, function(socket) { + var hostHeader = options.request.getHeader('host'); + var tlsOptions = mergeOptions({}, self.options, { + socket: socket, + servername: hostHeader ? hostHeader.replace(/:.*$/, '') : options.host + }); + + // 0 is dummy port for v0.6 + var secureSocket = tls.connect(0, tlsOptions); + self.sockets[self.sockets.indexOf(socket)] = secureSocket; + cb(secureSocket); + }); +} + + +function toOptions(host, port, localAddress) { + if (typeof host === 'string') { // since v0.10 + return { + host: host, + port: port, + localAddress: localAddress + }; + } + return host; // for v0.11 or later +} + +function mergeOptions(target) { + for (var i = 1, len = arguments.length; i < len; ++i) { + var overrides = arguments[i]; + if (typeof overrides === 'object') { + var keys = Object.keys(overrides); + for (var j = 0, keyLen = keys.length; j < keyLen; ++j) { + var k = keys[j]; + if (overrides[k] !== undefined) { + target[k] = overrides[k]; + } + } + } + } + return target; +} + + +var debug; +if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) { + debug = function() { + var args = Array.prototype.slice.call(arguments); + if (typeof args[0] === 'string') { + args[0] = 'TUNNEL: ' + args[0]; + } else { + args.unshift('TUNNEL:'); + } + console.error.apply(console, args); + } +} else { + debug = function() {}; +} +exports.debug = debug; // for test + + +/***/ }), + +/***/ 1773: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Client = __nccwpck_require__(3598) +const Dispatcher = __nccwpck_require__(412) +const errors = __nccwpck_require__(8045) +const Pool = __nccwpck_require__(4634) +const BalancedPool = __nccwpck_require__(7931) +const Agent = __nccwpck_require__(7890) +const util = __nccwpck_require__(3983) +const { InvalidArgumentError } = errors +const api = __nccwpck_require__(4059) +const buildConnector = __nccwpck_require__(2067) +const MockClient = __nccwpck_require__(8687) +const MockAgent = __nccwpck_require__(6771) +const MockPool = __nccwpck_require__(6193) +const mockErrors = __nccwpck_require__(888) +const ProxyAgent = __nccwpck_require__(7858) +const RetryHandler = __nccwpck_require__(2286) +const { getGlobalDispatcher, setGlobalDispatcher } = __nccwpck_require__(1892) +const DecoratorHandler = __nccwpck_require__(6930) +const RedirectHandler = __nccwpck_require__(2860) +const createRedirectInterceptor = __nccwpck_require__(8861) + +let hasCrypto +try { + __nccwpck_require__(6113) + hasCrypto = true +} catch { + hasCrypto = false +} + +Object.assign(Dispatcher.prototype, api) + +module.exports.Dispatcher = Dispatcher +module.exports.Client = Client +module.exports.Pool = Pool +module.exports.BalancedPool = BalancedPool +module.exports.Agent = Agent +module.exports.ProxyAgent = ProxyAgent +module.exports.RetryHandler = RetryHandler + +module.exports.DecoratorHandler = DecoratorHandler +module.exports.RedirectHandler = RedirectHandler +module.exports.createRedirectInterceptor = createRedirectInterceptor + +module.exports.buildConnector = buildConnector +module.exports.errors = errors + +function makeDispatcher (fn) { + return (url, opts, handler) => { + if (typeof opts === 'function') { + handler = opts + opts = null + } + + if (!url || (typeof url !== 'string' && typeof url !== 'object' && !(url instanceof URL))) { + throw new InvalidArgumentError('invalid url') + } + + if (opts != null && typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (opts && opts.path != null) { + if (typeof opts.path !== 'string') { + throw new InvalidArgumentError('invalid opts.path') + } + + let path = opts.path + if (!opts.path.startsWith('/')) { + path = `/${path}` + } + + url = new URL(util.parseOrigin(url).origin + path) + } else { + if (!opts) { + opts = typeof url === 'object' ? url : {} + } + + url = util.parseURL(url) + } + + const { agent, dispatcher = getGlobalDispatcher() } = opts + + if (agent) { + throw new InvalidArgumentError('unsupported opts.agent. Did you mean opts.client?') + } + + return fn.call(dispatcher, { + ...opts, + origin: url.origin, + path: url.search ? `${url.pathname}${url.search}` : url.pathname, + method: opts.method || (opts.body ? 'PUT' : 'GET') + }, handler) + } +} + +module.exports.setGlobalDispatcher = setGlobalDispatcher +module.exports.getGlobalDispatcher = getGlobalDispatcher + +if (util.nodeMajor > 16 || (util.nodeMajor === 16 && util.nodeMinor >= 8)) { + let fetchImpl = null + module.exports.fetch = async function fetch (resource) { + if (!fetchImpl) { + fetchImpl = (__nccwpck_require__(4881).fetch) + } + + try { + return await fetchImpl(...arguments) + } catch (err) { + if (typeof err === 'object') { + Error.captureStackTrace(err, this) + } + + throw err + } + } + module.exports.Headers = __nccwpck_require__(554).Headers + module.exports.Response = __nccwpck_require__(7823).Response + module.exports.Request = __nccwpck_require__(8359).Request + module.exports.FormData = __nccwpck_require__(2015).FormData + module.exports.File = __nccwpck_require__(8511).File + module.exports.FileReader = __nccwpck_require__(1446).FileReader + + const { setGlobalOrigin, getGlobalOrigin } = __nccwpck_require__(1246) + + module.exports.setGlobalOrigin = setGlobalOrigin + module.exports.getGlobalOrigin = getGlobalOrigin + + const { CacheStorage } = __nccwpck_require__(7907) + const { kConstruct } = __nccwpck_require__(9174) + + // Cache & CacheStorage are tightly coupled with fetch. Even if it may run + // in an older version of Node, it doesn't have any use without fetch. + module.exports.caches = new CacheStorage(kConstruct) +} + +if (util.nodeMajor >= 16) { + const { deleteCookie, getCookies, getSetCookies, setCookie } = __nccwpck_require__(1724) + + module.exports.deleteCookie = deleteCookie + module.exports.getCookies = getCookies + module.exports.getSetCookies = getSetCookies + module.exports.setCookie = setCookie + + const { parseMIMEType, serializeAMimeType } = __nccwpck_require__(685) + + module.exports.parseMIMEType = parseMIMEType + module.exports.serializeAMimeType = serializeAMimeType +} + +if (util.nodeMajor >= 18 && hasCrypto) { + const { WebSocket } = __nccwpck_require__(4284) + + module.exports.WebSocket = WebSocket +} + +module.exports.request = makeDispatcher(api.request) +module.exports.stream = makeDispatcher(api.stream) +module.exports.pipeline = makeDispatcher(api.pipeline) +module.exports.connect = makeDispatcher(api.connect) +module.exports.upgrade = makeDispatcher(api.upgrade) + +module.exports.MockClient = MockClient +module.exports.MockPool = MockPool +module.exports.MockAgent = MockAgent +module.exports.mockErrors = mockErrors + + +/***/ }), + +/***/ 7890: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { InvalidArgumentError } = __nccwpck_require__(8045) +const { kClients, kRunning, kClose, kDestroy, kDispatch, kInterceptors } = __nccwpck_require__(2785) +const DispatcherBase = __nccwpck_require__(4839) +const Pool = __nccwpck_require__(4634) +const Client = __nccwpck_require__(3598) +const util = __nccwpck_require__(3983) +const createRedirectInterceptor = __nccwpck_require__(8861) +const { WeakRef, FinalizationRegistry } = __nccwpck_require__(6436)() + +const kOnConnect = Symbol('onConnect') +const kOnDisconnect = Symbol('onDisconnect') +const kOnConnectionError = Symbol('onConnectionError') +const kMaxRedirections = Symbol('maxRedirections') +const kOnDrain = Symbol('onDrain') +const kFactory = Symbol('factory') +const kFinalizer = Symbol('finalizer') +const kOptions = Symbol('options') + +function defaultFactory (origin, opts) { + return opts && opts.connections === 1 + ? new Client(origin, opts) + : new Pool(origin, opts) +} + +class Agent extends DispatcherBase { + constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { + super() + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (!Number.isInteger(maxRedirections) || maxRedirections < 0) { + throw new InvalidArgumentError('maxRedirections must be a positive number') + } + + if (connect && typeof connect !== 'function') { + connect = { ...connect } + } + + this[kInterceptors] = options.interceptors && options.interceptors.Agent && Array.isArray(options.interceptors.Agent) + ? options.interceptors.Agent + : [createRedirectInterceptor({ maxRedirections })] + + this[kOptions] = { ...util.deepClone(options), connect } + this[kOptions].interceptors = options.interceptors + ? { ...options.interceptors } + : undefined + this[kMaxRedirections] = maxRedirections + this[kFactory] = factory + this[kClients] = new Map() + this[kFinalizer] = new FinalizationRegistry(/* istanbul ignore next: gc is undeterministic */ key => { + const ref = this[kClients].get(key) + if (ref !== undefined && ref.deref() === undefined) { + this[kClients].delete(key) + } + }) + + const agent = this + + this[kOnDrain] = (origin, targets) => { + agent.emit('drain', origin, [agent, ...targets]) + } + + this[kOnConnect] = (origin, targets) => { + agent.emit('connect', origin, [agent, ...targets]) + } + + this[kOnDisconnect] = (origin, targets, err) => { + agent.emit('disconnect', origin, [agent, ...targets], err) + } + + this[kOnConnectionError] = (origin, targets, err) => { + agent.emit('connectionError', origin, [agent, ...targets], err) + } + } + + get [kRunning] () { + let ret = 0 + for (const ref of this[kClients].values()) { + const client = ref.deref() + /* istanbul ignore next: gc is undeterministic */ + if (client) { + ret += client[kRunning] + } + } + return ret + } + + [kDispatch] (opts, handler) { + let key + if (opts.origin && (typeof opts.origin === 'string' || opts.origin instanceof URL)) { + key = String(opts.origin) + } else { + throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.') + } + + const ref = this[kClients].get(key) + + let dispatcher = ref ? ref.deref() : null + if (!dispatcher) { + dispatcher = this[kFactory](opts.origin, this[kOptions]) + .on('drain', this[kOnDrain]) + .on('connect', this[kOnConnect]) + .on('disconnect', this[kOnDisconnect]) + .on('connectionError', this[kOnConnectionError]) + + this[kClients].set(key, new WeakRef(dispatcher)) + this[kFinalizer].register(dispatcher, key) + } + + return dispatcher.dispatch(opts, handler) + } + + async [kClose] () { + const closePromises = [] + for (const ref of this[kClients].values()) { + const client = ref.deref() + /* istanbul ignore else: gc is undeterministic */ + if (client) { + closePromises.push(client.close()) + } + } + + await Promise.all(closePromises) + } + + async [kDestroy] (err) { + const destroyPromises = [] + for (const ref of this[kClients].values()) { + const client = ref.deref() + /* istanbul ignore else: gc is undeterministic */ + if (client) { + destroyPromises.push(client.destroy(err)) + } + } + + await Promise.all(destroyPromises) + } +} + +module.exports = Agent + + +/***/ }), + +/***/ 7032: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const { addAbortListener } = __nccwpck_require__(3983) +const { RequestAbortedError } = __nccwpck_require__(8045) + +const kListener = Symbol('kListener') +const kSignal = Symbol('kSignal') + +function abort (self) { + if (self.abort) { + self.abort() + } else { + self.onError(new RequestAbortedError()) + } +} + +function addSignal (self, signal) { + self[kSignal] = null + self[kListener] = null + + if (!signal) { + return + } + + if (signal.aborted) { + abort(self) + return + } + + self[kSignal] = signal + self[kListener] = () => { + abort(self) + } + + addAbortListener(self[kSignal], self[kListener]) +} + +function removeSignal (self) { + if (!self[kSignal]) { + return + } + + if ('removeEventListener' in self[kSignal]) { + self[kSignal].removeEventListener('abort', self[kListener]) + } else { + self[kSignal].removeListener('abort', self[kListener]) + } + + self[kSignal] = null + self[kListener] = null +} + +module.exports = { + addSignal, + removeSignal +} + + +/***/ }), + +/***/ 9744: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { AsyncResource } = __nccwpck_require__(852) +const { InvalidArgumentError, RequestAbortedError, SocketError } = __nccwpck_require__(8045) +const util = __nccwpck_require__(3983) +const { addSignal, removeSignal } = __nccwpck_require__(7032) + +class ConnectHandler extends AsyncResource { + constructor (opts, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + const { signal, opaque, responseHeaders } = opts + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + super('UNDICI_CONNECT') + + this.opaque = opaque || null + this.responseHeaders = responseHeaders || null + this.callback = callback + this.abort = null + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders () { + throw new SocketError('bad connect', null) + } + + onUpgrade (statusCode, rawHeaders, socket) { + const { callback, opaque, context } = this + + removeSignal(this) + + this.callback = null + + let headers = rawHeaders + // Indicates is an HTTP2Session + if (headers != null) { + headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + } + + this.runInAsyncScope(callback, null, null, { + statusCode, + headers, + socket, + opaque, + context + }) + } + + onError (err) { + const { callback, opaque } = this + + removeSignal(this) + + if (callback) { + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + } +} + +function connect (opts, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + connect.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + const connectHandler = new ConnectHandler(opts, callback) + this.dispatch({ ...opts, method: 'CONNECT' }, connectHandler) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = connect + + +/***/ }), + +/***/ 8752: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + Readable, + Duplex, + PassThrough +} = __nccwpck_require__(2781) +const { + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError +} = __nccwpck_require__(8045) +const util = __nccwpck_require__(3983) +const { AsyncResource } = __nccwpck_require__(852) +const { addSignal, removeSignal } = __nccwpck_require__(7032) +const assert = __nccwpck_require__(9491) + +const kResume = Symbol('resume') + +class PipelineRequest extends Readable { + constructor () { + super({ autoDestroy: true }) + + this[kResume] = null + } + + _read () { + const { [kResume]: resume } = this + + if (resume) { + this[kResume] = null + resume() + } + } + + _destroy (err, callback) { + this._read() + + callback(err) + } +} + +class PipelineResponse extends Readable { + constructor (resume) { + super({ autoDestroy: true }) + this[kResume] = resume + } + + _read () { + this[kResume]() + } + + _destroy (err, callback) { + if (!err && !this._readableState.endEmitted) { + err = new RequestAbortedError() + } + + callback(err) + } +} + +class PipelineHandler extends AsyncResource { + constructor (opts, handler) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (typeof handler !== 'function') { + throw new InvalidArgumentError('invalid handler') + } + + const { signal, method, opaque, onInfo, responseHeaders } = opts + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + if (method === 'CONNECT') { + throw new InvalidArgumentError('invalid method') + } + + if (onInfo && typeof onInfo !== 'function') { + throw new InvalidArgumentError('invalid onInfo callback') + } + + super('UNDICI_PIPELINE') + + this.opaque = opaque || null + this.responseHeaders = responseHeaders || null + this.handler = handler + this.abort = null + this.context = null + this.onInfo = onInfo || null + + this.req = new PipelineRequest().on('error', util.nop) + + this.ret = new Duplex({ + readableObjectMode: opts.objectMode, + autoDestroy: true, + read: () => { + const { body } = this + + if (body && body.resume) { + body.resume() + } + }, + write: (chunk, encoding, callback) => { + const { req } = this + + if (req.push(chunk, encoding) || req._readableState.destroyed) { + callback() + } else { + req[kResume] = callback + } + }, + destroy: (err, callback) => { + const { body, req, res, ret, abort } = this + + if (!err && !ret._readableState.endEmitted) { + err = new RequestAbortedError() + } + + if (abort && err) { + abort() + } + + util.destroy(body, err) + util.destroy(req, err) + util.destroy(res, err) + + removeSignal(this) + + callback(err) + } + }).on('prefinish', () => { + const { req } = this + + // Node < 15 does not call _final in same tick. + req.push(null) + }) + + this.res = null + + addSignal(this, signal) + } + + onConnect (abort, context) { + const { ret, res } = this + + assert(!res, 'pipeline cannot be retried') + + if (ret.destroyed) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders (statusCode, rawHeaders, resume) { + const { opaque, handler, context } = this + + if (statusCode < 200) { + if (this.onInfo) { + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + this.onInfo({ statusCode, headers }) + } + return + } + + this.res = new PipelineResponse(resume) + + let body + try { + this.handler = null + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + body = this.runInAsyncScope(handler, null, { + statusCode, + headers, + opaque, + body: this.res, + context + }) + } catch (err) { + this.res.on('error', util.nop) + throw err + } + + if (!body || typeof body.on !== 'function') { + throw new InvalidReturnValueError('expected Readable') + } + + body + .on('data', (chunk) => { + const { ret, body } = this + + if (!ret.push(chunk) && body.pause) { + body.pause() + } + }) + .on('error', (err) => { + const { ret } = this + + util.destroy(ret, err) + }) + .on('end', () => { + const { ret } = this + + ret.push(null) + }) + .on('close', () => { + const { ret } = this + + if (!ret._readableState.ended) { + util.destroy(ret, new RequestAbortedError()) + } + }) + + this.body = body + } + + onData (chunk) { + const { res } = this + return res.push(chunk) + } + + onComplete (trailers) { + const { res } = this + res.push(null) + } + + onError (err) { + const { ret } = this + this.handler = null + util.destroy(ret, err) + } +} + +function pipeline (opts, handler) { + try { + const pipelineHandler = new PipelineHandler(opts, handler) + this.dispatch({ ...opts, body: pipelineHandler.req }, pipelineHandler) + return pipelineHandler.ret + } catch (err) { + return new PassThrough().destroy(err) + } +} + +module.exports = pipeline + + +/***/ }), + +/***/ 5448: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Readable = __nccwpck_require__(3858) +const { + InvalidArgumentError, + RequestAbortedError +} = __nccwpck_require__(8045) +const util = __nccwpck_require__(3983) +const { getResolveErrorBodyCallback } = __nccwpck_require__(7474) +const { AsyncResource } = __nccwpck_require__(852) +const { addSignal, removeSignal } = __nccwpck_require__(7032) + +class RequestHandler extends AsyncResource { + constructor (opts, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError, highWaterMark } = opts + + try { + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (highWaterMark && (typeof highWaterMark !== 'number' || highWaterMark < 0)) { + throw new InvalidArgumentError('invalid highWaterMark') + } + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + if (method === 'CONNECT') { + throw new InvalidArgumentError('invalid method') + } + + if (onInfo && typeof onInfo !== 'function') { + throw new InvalidArgumentError('invalid onInfo callback') + } + + super('UNDICI_REQUEST') + } catch (err) { + if (util.isStream(body)) { + util.destroy(body.on('error', util.nop), err) + } + throw err + } + + this.responseHeaders = responseHeaders || null + this.opaque = opaque || null + this.callback = callback + this.res = null + this.abort = null + this.body = body + this.trailers = {} + this.context = null + this.onInfo = onInfo || null + this.throwOnError = throwOnError + this.highWaterMark = highWaterMark + + if (util.isStream(body)) { + body.on('error', (err) => { + this.onError(err) + }) + } + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + const { callback, opaque, abort, context, responseHeaders, highWaterMark } = this + + const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + + if (statusCode < 200) { + if (this.onInfo) { + this.onInfo({ statusCode, headers }) + } + return + } + + const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers + const contentType = parsedHeaders['content-type'] + const body = new Readable({ resume, abort, contentType, highWaterMark }) + + this.callback = null + this.res = body + if (callback !== null) { + if (this.throwOnError && statusCode >= 400) { + this.runInAsyncScope(getResolveErrorBodyCallback, null, + { callback, body, contentType, statusCode, statusMessage, headers } + ) + } else { + this.runInAsyncScope(callback, null, null, { + statusCode, + headers, + trailers: this.trailers, + opaque, + body, + context + }) + } + } + } + + onData (chunk) { + const { res } = this + return res.push(chunk) + } + + onComplete (trailers) { + const { res } = this + + removeSignal(this) + + util.parseHeaders(trailers, this.trailers) + + res.push(null) + } + + onError (err) { + const { res, callback, body, opaque } = this + + removeSignal(this) + + if (callback) { + // TODO: Does this need queueMicrotask? + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + + if (res) { + this.res = null + // Ensure all queued handlers are invoked before destroying res. + queueMicrotask(() => { + util.destroy(res, err) + }) + } + + if (body) { + this.body = null + util.destroy(body, err) + } + } +} + +function request (opts, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + request.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + this.dispatch(opts, new RequestHandler(opts, callback)) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = request +module.exports.RequestHandler = RequestHandler + + +/***/ }), + +/***/ 5395: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { finished, PassThrough } = __nccwpck_require__(2781) +const { + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError +} = __nccwpck_require__(8045) +const util = __nccwpck_require__(3983) +const { getResolveErrorBodyCallback } = __nccwpck_require__(7474) +const { AsyncResource } = __nccwpck_require__(852) +const { addSignal, removeSignal } = __nccwpck_require__(7032) + +class StreamHandler extends AsyncResource { + constructor (opts, factory, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError } = opts + + try { + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('invalid factory') + } + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + if (method === 'CONNECT') { + throw new InvalidArgumentError('invalid method') + } + + if (onInfo && typeof onInfo !== 'function') { + throw new InvalidArgumentError('invalid onInfo callback') + } + + super('UNDICI_STREAM') + } catch (err) { + if (util.isStream(body)) { + util.destroy(body.on('error', util.nop), err) + } + throw err + } + + this.responseHeaders = responseHeaders || null + this.opaque = opaque || null + this.factory = factory + this.callback = callback + this.res = null + this.abort = null + this.context = null + this.trailers = null + this.body = body + this.onInfo = onInfo || null + this.throwOnError = throwOnError || false + + if (util.isStream(body)) { + body.on('error', (err) => { + this.onError(err) + }) + } + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + const { factory, opaque, context, callback, responseHeaders } = this + + const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + + if (statusCode < 200) { + if (this.onInfo) { + this.onInfo({ statusCode, headers }) + } + return + } + + this.factory = null + + let res + + if (this.throwOnError && statusCode >= 400) { + const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers + const contentType = parsedHeaders['content-type'] + res = new PassThrough() + + this.callback = null + this.runInAsyncScope(getResolveErrorBodyCallback, null, + { callback, body: res, contentType, statusCode, statusMessage, headers } + ) + } else { + if (factory === null) { + return + } + + res = this.runInAsyncScope(factory, null, { + statusCode, + headers, + opaque, + context + }) + + if ( + !res || + typeof res.write !== 'function' || + typeof res.end !== 'function' || + typeof res.on !== 'function' + ) { + throw new InvalidReturnValueError('expected Writable') + } + + // TODO: Avoid finished. It registers an unnecessary amount of listeners. + finished(res, { readable: false }, (err) => { + const { callback, res, opaque, trailers, abort } = this + + this.res = null + if (err || !res.readable) { + util.destroy(res, err) + } + + this.callback = null + this.runInAsyncScope(callback, null, err || null, { opaque, trailers }) + + if (err) { + abort() + } + }) + } + + res.on('drain', resume) + + this.res = res + + const needDrain = res.writableNeedDrain !== undefined + ? res.writableNeedDrain + : res._writableState && res._writableState.needDrain + + return needDrain !== true + } + + onData (chunk) { + const { res } = this + + return res ? res.write(chunk) : true + } + + onComplete (trailers) { + const { res } = this + + removeSignal(this) + + if (!res) { + return + } + + this.trailers = util.parseHeaders(trailers) + + res.end() + } + + onError (err) { + const { res, callback, opaque, body } = this + + removeSignal(this) + + this.factory = null + + if (res) { + this.res = null + util.destroy(res, err) + } else if (callback) { + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + + if (body) { + this.body = null + util.destroy(body, err) + } + } +} + +function stream (opts, factory, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + stream.call(this, opts, factory, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + this.dispatch(opts, new StreamHandler(opts, factory, callback)) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = stream + + +/***/ }), + +/***/ 6923: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { InvalidArgumentError, RequestAbortedError, SocketError } = __nccwpck_require__(8045) +const { AsyncResource } = __nccwpck_require__(852) +const util = __nccwpck_require__(3983) +const { addSignal, removeSignal } = __nccwpck_require__(7032) +const assert = __nccwpck_require__(9491) + +class UpgradeHandler extends AsyncResource { + constructor (opts, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + const { signal, opaque, responseHeaders } = opts + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + super('UNDICI_UPGRADE') + + this.responseHeaders = responseHeaders || null + this.opaque = opaque || null + this.callback = callback + this.abort = null + this.context = null + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = null + } + + onHeaders () { + throw new SocketError('bad upgrade', null) + } + + onUpgrade (statusCode, rawHeaders, socket) { + const { callback, opaque, context } = this + + assert.strictEqual(statusCode, 101) + + removeSignal(this) + + this.callback = null + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + this.runInAsyncScope(callback, null, null, { + headers, + socket, + opaque, + context + }) + } + + onError (err) { + const { callback, opaque } = this + + removeSignal(this) + + if (callback) { + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + } +} + +function upgrade (opts, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + upgrade.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + const upgradeHandler = new UpgradeHandler(opts, callback) + this.dispatch({ + ...opts, + method: opts.method || 'GET', + upgrade: opts.protocol || 'Websocket' + }, upgradeHandler) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = upgrade + + +/***/ }), + +/***/ 4059: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +module.exports.request = __nccwpck_require__(5448) +module.exports.stream = __nccwpck_require__(5395) +module.exports.pipeline = __nccwpck_require__(8752) +module.exports.upgrade = __nccwpck_require__(6923) +module.exports.connect = __nccwpck_require__(9744) + + +/***/ }), + +/***/ 3858: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +// Ported from https://github.com/nodejs/undici/pull/907 + + + +const assert = __nccwpck_require__(9491) +const { Readable } = __nccwpck_require__(2781) +const { RequestAbortedError, NotSupportedError, InvalidArgumentError } = __nccwpck_require__(8045) +const util = __nccwpck_require__(3983) +const { ReadableStreamFrom, toUSVString } = __nccwpck_require__(3983) + +let Blob + +const kConsume = Symbol('kConsume') +const kReading = Symbol('kReading') +const kBody = Symbol('kBody') +const kAbort = Symbol('abort') +const kContentType = Symbol('kContentType') + +const noop = () => {} + +module.exports = class BodyReadable extends Readable { + constructor ({ + resume, + abort, + contentType = '', + highWaterMark = 64 * 1024 // Same as nodejs fs streams. + }) { + super({ + autoDestroy: true, + read: resume, + highWaterMark + }) + + this._readableState.dataEmitted = false + + this[kAbort] = abort + this[kConsume] = null + this[kBody] = null + this[kContentType] = contentType + + // Is stream being consumed through Readable API? + // This is an optimization so that we avoid checking + // for 'data' and 'readable' listeners in the hot path + // inside push(). + this[kReading] = false + } + + destroy (err) { + if (this.destroyed) { + // Node < 16 + return this + } + + if (!err && !this._readableState.endEmitted) { + err = new RequestAbortedError() + } + + if (err) { + this[kAbort]() + } + + return super.destroy(err) + } + + emit (ev, ...args) { + if (ev === 'data') { + // Node < 16.7 + this._readableState.dataEmitted = true + } else if (ev === 'error') { + // Node < 16 + this._readableState.errorEmitted = true + } + return super.emit(ev, ...args) + } + + on (ev, ...args) { + if (ev === 'data' || ev === 'readable') { + this[kReading] = true + } + return super.on(ev, ...args) + } + + addListener (ev, ...args) { + return this.on(ev, ...args) + } + + off (ev, ...args) { + const ret = super.off(ev, ...args) + if (ev === 'data' || ev === 'readable') { + this[kReading] = ( + this.listenerCount('data') > 0 || + this.listenerCount('readable') > 0 + ) + } + return ret + } + + removeListener (ev, ...args) { + return this.off(ev, ...args) + } + + push (chunk) { + if (this[kConsume] && chunk !== null && this.readableLength === 0) { + consumePush(this[kConsume], chunk) + return this[kReading] ? super.push(chunk) : true + } + return super.push(chunk) + } + + // https://fetch.spec.whatwg.org/#dom-body-text + async text () { + return consume(this, 'text') + } + + // https://fetch.spec.whatwg.org/#dom-body-json + async json () { + return consume(this, 'json') + } + + // https://fetch.spec.whatwg.org/#dom-body-blob + async blob () { + return consume(this, 'blob') + } + + // https://fetch.spec.whatwg.org/#dom-body-arraybuffer + async arrayBuffer () { + return consume(this, 'arrayBuffer') + } + + // https://fetch.spec.whatwg.org/#dom-body-formdata + async formData () { + // TODO: Implement. + throw new NotSupportedError() + } + + // https://fetch.spec.whatwg.org/#dom-body-bodyused + get bodyUsed () { + return util.isDisturbed(this) + } + + // https://fetch.spec.whatwg.org/#dom-body-body + get body () { + if (!this[kBody]) { + this[kBody] = ReadableStreamFrom(this) + if (this[kConsume]) { + // TODO: Is this the best way to force a lock? + this[kBody].getReader() // Ensure stream is locked. + assert(this[kBody].locked) + } + } + return this[kBody] + } + + dump (opts) { + let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144 + const signal = opts && opts.signal + + if (signal) { + try { + if (typeof signal !== 'object' || !('aborted' in signal)) { + throw new InvalidArgumentError('signal must be an AbortSignal') + } + util.throwIfAborted(signal) + } catch (err) { + return Promise.reject(err) + } + } + + if (this.closed) { + return Promise.resolve(null) + } + + return new Promise((resolve, reject) => { + const signalListenerCleanup = signal + ? util.addAbortListener(signal, () => { + this.destroy() + }) + : noop + + this + .on('close', function () { + signalListenerCleanup() + if (signal && signal.aborted) { + reject(signal.reason || Object.assign(new Error('The operation was aborted'), { name: 'AbortError' })) + } else { + resolve(null) + } + }) + .on('error', noop) + .on('data', function (chunk) { + limit -= chunk.length + if (limit <= 0) { + this.destroy() + } + }) + .resume() + }) + } +} + +// https://streams.spec.whatwg.org/#readablestream-locked +function isLocked (self) { + // Consume is an implicit lock. + return (self[kBody] && self[kBody].locked === true) || self[kConsume] +} + +// https://fetch.spec.whatwg.org/#body-unusable +function isUnusable (self) { + return util.isDisturbed(self) || isLocked(self) +} + +async function consume (stream, type) { + if (isUnusable(stream)) { + throw new TypeError('unusable') + } + + assert(!stream[kConsume]) + + return new Promise((resolve, reject) => { + stream[kConsume] = { + type, + stream, + resolve, + reject, + length: 0, + body: [] + } + + stream + .on('error', function (err) { + consumeFinish(this[kConsume], err) + }) + .on('close', function () { + if (this[kConsume].body !== null) { + consumeFinish(this[kConsume], new RequestAbortedError()) + } + }) + + process.nextTick(consumeStart, stream[kConsume]) + }) +} + +function consumeStart (consume) { + if (consume.body === null) { + return + } + + const { _readableState: state } = consume.stream + + for (const chunk of state.buffer) { + consumePush(consume, chunk) + } + + if (state.endEmitted) { + consumeEnd(this[kConsume]) + } else { + consume.stream.on('end', function () { + consumeEnd(this[kConsume]) + }) + } + + consume.stream.resume() + + while (consume.stream.read() != null) { + // Loop + } +} + +function consumeEnd (consume) { + const { type, body, resolve, stream, length } = consume + + try { + if (type === 'text') { + resolve(toUSVString(Buffer.concat(body))) + } else if (type === 'json') { + resolve(JSON.parse(Buffer.concat(body))) + } else if (type === 'arrayBuffer') { + const dst = new Uint8Array(length) + + let pos = 0 + for (const buf of body) { + dst.set(buf, pos) + pos += buf.byteLength + } + + resolve(dst.buffer) + } else if (type === 'blob') { + if (!Blob) { + Blob = (__nccwpck_require__(4300).Blob) + } + resolve(new Blob(body, { type: stream[kContentType] })) + } + + consumeFinish(consume) + } catch (err) { + stream.destroy(err) + } +} + +function consumePush (consume, chunk) { + consume.length += chunk.length + consume.body.push(chunk) +} + +function consumeFinish (consume, err) { + if (consume.body === null) { + return + } + + if (err) { + consume.reject(err) + } else { + consume.resolve() + } + + consume.type = null + consume.stream = null + consume.resolve = null + consume.reject = null + consume.length = 0 + consume.body = null +} + + +/***/ }), + +/***/ 7474: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const assert = __nccwpck_require__(9491) +const { + ResponseStatusCodeError +} = __nccwpck_require__(8045) +const { toUSVString } = __nccwpck_require__(3983) + +async function getResolveErrorBodyCallback ({ callback, body, contentType, statusCode, statusMessage, headers }) { + assert(body) + + let chunks = [] + let limit = 0 + + for await (const chunk of body) { + chunks.push(chunk) + limit += chunk.length + if (limit > 128 * 1024) { + chunks = null + break + } + } + + if (statusCode === 204 || !contentType || !chunks) { + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) + return + } + + try { + if (contentType.startsWith('application/json')) { + const payload = JSON.parse(toUSVString(Buffer.concat(chunks))) + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) + return + } + + if (contentType.startsWith('text/')) { + const payload = toUSVString(Buffer.concat(chunks)) + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) + return + } + } catch (err) { + // Process in a fallback if error + } + + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) +} + +module.exports = { getResolveErrorBodyCallback } + + +/***/ }), + +/***/ 7931: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + BalancedPoolMissingUpstreamError, + InvalidArgumentError +} = __nccwpck_require__(8045) +const { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kRemoveClient, + kGetDispatcher +} = __nccwpck_require__(3198) +const Pool = __nccwpck_require__(4634) +const { kUrl, kInterceptors } = __nccwpck_require__(2785) +const { parseOrigin } = __nccwpck_require__(3983) +const kFactory = Symbol('factory') + +const kOptions = Symbol('options') +const kGreatestCommonDivisor = Symbol('kGreatestCommonDivisor') +const kCurrentWeight = Symbol('kCurrentWeight') +const kIndex = Symbol('kIndex') +const kWeight = Symbol('kWeight') +const kMaxWeightPerServer = Symbol('kMaxWeightPerServer') +const kErrorPenalty = Symbol('kErrorPenalty') + +function getGreatestCommonDivisor (a, b) { + if (b === 0) return a + return getGreatestCommonDivisor(b, a % b) +} + +function defaultFactory (origin, opts) { + return new Pool(origin, opts) +} + +class BalancedPool extends PoolBase { + constructor (upstreams = [], { factory = defaultFactory, ...opts } = {}) { + super() + + this[kOptions] = opts + this[kIndex] = -1 + this[kCurrentWeight] = 0 + + this[kMaxWeightPerServer] = this[kOptions].maxWeightPerServer || 100 + this[kErrorPenalty] = this[kOptions].errorPenalty || 15 + + if (!Array.isArray(upstreams)) { + upstreams = [upstreams] + } + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + this[kInterceptors] = opts.interceptors && opts.interceptors.BalancedPool && Array.isArray(opts.interceptors.BalancedPool) + ? opts.interceptors.BalancedPool + : [] + this[kFactory] = factory + + for (const upstream of upstreams) { + this.addUpstream(upstream) + } + this._updateBalancedPoolStats() + } + + addUpstream (upstream) { + const upstreamOrigin = parseOrigin(upstream).origin + + if (this[kClients].find((pool) => ( + pool[kUrl].origin === upstreamOrigin && + pool.closed !== true && + pool.destroyed !== true + ))) { + return this + } + const pool = this[kFactory](upstreamOrigin, Object.assign({}, this[kOptions])) + + this[kAddClient](pool) + pool.on('connect', () => { + pool[kWeight] = Math.min(this[kMaxWeightPerServer], pool[kWeight] + this[kErrorPenalty]) + }) + + pool.on('connectionError', () => { + pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty]) + this._updateBalancedPoolStats() + }) + + pool.on('disconnect', (...args) => { + const err = args[2] + if (err && err.code === 'UND_ERR_SOCKET') { + // decrease the weight of the pool. + pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty]) + this._updateBalancedPoolStats() + } + }) + + for (const client of this[kClients]) { + client[kWeight] = this[kMaxWeightPerServer] + } + + this._updateBalancedPoolStats() + + return this + } + + _updateBalancedPoolStats () { + this[kGreatestCommonDivisor] = this[kClients].map(p => p[kWeight]).reduce(getGreatestCommonDivisor, 0) + } + + removeUpstream (upstream) { + const upstreamOrigin = parseOrigin(upstream).origin + + const pool = this[kClients].find((pool) => ( + pool[kUrl].origin === upstreamOrigin && + pool.closed !== true && + pool.destroyed !== true + )) + + if (pool) { + this[kRemoveClient](pool) + } + + return this + } + + get upstreams () { + return this[kClients] + .filter(dispatcher => dispatcher.closed !== true && dispatcher.destroyed !== true) + .map((p) => p[kUrl].origin) + } + + [kGetDispatcher] () { + // We validate that pools is greater than 0, + // otherwise we would have to wait until an upstream + // is added, which might never happen. + if (this[kClients].length === 0) { + throw new BalancedPoolMissingUpstreamError() + } + + const dispatcher = this[kClients].find(dispatcher => ( + !dispatcher[kNeedDrain] && + dispatcher.closed !== true && + dispatcher.destroyed !== true + )) + + if (!dispatcher) { + return + } + + const allClientsBusy = this[kClients].map(pool => pool[kNeedDrain]).reduce((a, b) => a && b, true) + + if (allClientsBusy) { + return + } + + let counter = 0 + + let maxWeightIndex = this[kClients].findIndex(pool => !pool[kNeedDrain]) + + while (counter++ < this[kClients].length) { + this[kIndex] = (this[kIndex] + 1) % this[kClients].length + const pool = this[kClients][this[kIndex]] + + // find pool index with the largest weight + if (pool[kWeight] > this[kClients][maxWeightIndex][kWeight] && !pool[kNeedDrain]) { + maxWeightIndex = this[kIndex] + } + + // decrease the current weight every `this[kClients].length`. + if (this[kIndex] === 0) { + // Set the current weight to the next lower weight. + this[kCurrentWeight] = this[kCurrentWeight] - this[kGreatestCommonDivisor] + + if (this[kCurrentWeight] <= 0) { + this[kCurrentWeight] = this[kMaxWeightPerServer] + } + } + if (pool[kWeight] >= this[kCurrentWeight] && (!pool[kNeedDrain])) { + return pool + } + } + + this[kCurrentWeight] = this[kClients][maxWeightIndex][kWeight] + this[kIndex] = maxWeightIndex + return this[kClients][maxWeightIndex] + } +} + +module.exports = BalancedPool + + +/***/ }), + +/***/ 6101: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kConstruct } = __nccwpck_require__(9174) +const { urlEquals, fieldValues: getFieldValues } = __nccwpck_require__(2396) +const { kEnumerableProperty, isDisturbed } = __nccwpck_require__(3983) +const { kHeadersList } = __nccwpck_require__(2785) +const { webidl } = __nccwpck_require__(1744) +const { Response, cloneResponse } = __nccwpck_require__(7823) +const { Request } = __nccwpck_require__(8359) +const { kState, kHeaders, kGuard, kRealm } = __nccwpck_require__(5861) +const { fetching } = __nccwpck_require__(4881) +const { urlIsHttpHttpsScheme, createDeferredPromise, readAllBytes } = __nccwpck_require__(2538) +const assert = __nccwpck_require__(9491) +const { getGlobalDispatcher } = __nccwpck_require__(1892) + +/** + * @see https://w3c.github.io/ServiceWorker/#dfn-cache-batch-operation + * @typedef {Object} CacheBatchOperation + * @property {'delete' | 'put'} type + * @property {any} request + * @property {any} response + * @property {import('../../types/cache').CacheQueryOptions} options + */ + +/** + * @see https://w3c.github.io/ServiceWorker/#dfn-request-response-list + * @typedef {[any, any][]} requestResponseList + */ + +class Cache { + /** + * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-request-response-list + * @type {requestResponseList} + */ + #relevantRequestResponseList + + constructor () { + if (arguments[0] !== kConstruct) { + webidl.illegalConstructor() + } + + this.#relevantRequestResponseList = arguments[1] + } + + async match (request, options = {}) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.match' }) + + request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + const p = await this.matchAll(request, options) + + if (p.length === 0) { + return + } + + return p[0] + } + + async matchAll (request = undefined, options = {}) { + webidl.brandCheck(this, Cache) + + if (request !== undefined) request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + // 1. + let r = null + + // 2. + if (request !== undefined) { + if (request instanceof Request) { + // 2.1.1 + r = request[kState] + + // 2.1.2 + if (r.method !== 'GET' && !options.ignoreMethod) { + return [] + } + } else if (typeof request === 'string') { + // 2.2.1 + r = new Request(request)[kState] + } + } + + // 5. + // 5.1 + const responses = [] + + // 5.2 + if (request === undefined) { + // 5.2.1 + for (const requestResponse of this.#relevantRequestResponseList) { + responses.push(requestResponse[1]) + } + } else { // 5.3 + // 5.3.1 + const requestResponses = this.#queryCache(r, options) + + // 5.3.2 + for (const requestResponse of requestResponses) { + responses.push(requestResponse[1]) + } + } + + // 5.4 + // We don't implement CORs so we don't need to loop over the responses, yay! + + // 5.5.1 + const responseList = [] + + // 5.5.2 + for (const response of responses) { + // 5.5.2.1 + const responseObject = new Response(response.body?.source ?? null) + const body = responseObject[kState].body + responseObject[kState] = response + responseObject[kState].body = body + responseObject[kHeaders][kHeadersList] = response.headersList + responseObject[kHeaders][kGuard] = 'immutable' + + responseList.push(responseObject) + } + + // 6. + return Object.freeze(responseList) + } + + async add (request) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.add' }) + + request = webidl.converters.RequestInfo(request) + + // 1. + const requests = [request] + + // 2. + const responseArrayPromise = this.addAll(requests) + + // 3. + return await responseArrayPromise + } + + async addAll (requests) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.addAll' }) + + requests = webidl.converters['sequence'](requests) + + // 1. + const responsePromises = [] + + // 2. + const requestList = [] + + // 3. + for (const request of requests) { + if (typeof request === 'string') { + continue + } + + // 3.1 + const r = request[kState] + + // 3.2 + if (!urlIsHttpHttpsScheme(r.url) || r.method !== 'GET') { + throw webidl.errors.exception({ + header: 'Cache.addAll', + message: 'Expected http/s scheme when method is not GET.' + }) + } + } + + // 4. + /** @type {ReturnType[]} */ + const fetchControllers = [] + + // 5. + for (const request of requests) { + // 5.1 + const r = new Request(request)[kState] + + // 5.2 + if (!urlIsHttpHttpsScheme(r.url)) { + throw webidl.errors.exception({ + header: 'Cache.addAll', + message: 'Expected http/s scheme.' + }) + } + + // 5.4 + r.initiator = 'fetch' + r.destination = 'subresource' + + // 5.5 + requestList.push(r) + + // 5.6 + const responsePromise = createDeferredPromise() + + // 5.7 + fetchControllers.push(fetching({ + request: r, + dispatcher: getGlobalDispatcher(), + processResponse (response) { + // 1. + if (response.type === 'error' || response.status === 206 || response.status < 200 || response.status > 299) { + responsePromise.reject(webidl.errors.exception({ + header: 'Cache.addAll', + message: 'Received an invalid status code or the request failed.' + })) + } else if (response.headersList.contains('vary')) { // 2. + // 2.1 + const fieldValues = getFieldValues(response.headersList.get('vary')) + + // 2.2 + for (const fieldValue of fieldValues) { + // 2.2.1 + if (fieldValue === '*') { + responsePromise.reject(webidl.errors.exception({ + header: 'Cache.addAll', + message: 'invalid vary field value' + })) + + for (const controller of fetchControllers) { + controller.abort() + } + + return + } + } + } + }, + processResponseEndOfBody (response) { + // 1. + if (response.aborted) { + responsePromise.reject(new DOMException('aborted', 'AbortError')) + return + } + + // 2. + responsePromise.resolve(response) + } + })) + + // 5.8 + responsePromises.push(responsePromise.promise) + } + + // 6. + const p = Promise.all(responsePromises) + + // 7. + const responses = await p + + // 7.1 + const operations = [] + + // 7.2 + let index = 0 + + // 7.3 + for (const response of responses) { + // 7.3.1 + /** @type {CacheBatchOperation} */ + const operation = { + type: 'put', // 7.3.2 + request: requestList[index], // 7.3.3 + response // 7.3.4 + } + + operations.push(operation) // 7.3.5 + + index++ // 7.3.6 + } + + // 7.5 + const cacheJobPromise = createDeferredPromise() + + // 7.6.1 + let errorData = null + + // 7.6.2 + try { + this.#batchCacheOperations(operations) + } catch (e) { + errorData = e + } + + // 7.6.3 + queueMicrotask(() => { + // 7.6.3.1 + if (errorData === null) { + cacheJobPromise.resolve(undefined) + } else { + // 7.6.3.2 + cacheJobPromise.reject(errorData) + } + }) + + // 7.7 + return cacheJobPromise.promise + } + + async put (request, response) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 2, { header: 'Cache.put' }) + + request = webidl.converters.RequestInfo(request) + response = webidl.converters.Response(response) + + // 1. + let innerRequest = null + + // 2. + if (request instanceof Request) { + innerRequest = request[kState] + } else { // 3. + innerRequest = new Request(request)[kState] + } + + // 4. + if (!urlIsHttpHttpsScheme(innerRequest.url) || innerRequest.method !== 'GET') { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Expected an http/s scheme when method is not GET' + }) + } + + // 5. + const innerResponse = response[kState] + + // 6. + if (innerResponse.status === 206) { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Got 206 status' + }) + } + + // 7. + if (innerResponse.headersList.contains('vary')) { + // 7.1. + const fieldValues = getFieldValues(innerResponse.headersList.get('vary')) + + // 7.2. + for (const fieldValue of fieldValues) { + // 7.2.1 + if (fieldValue === '*') { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Got * vary field value' + }) + } + } + } + + // 8. + if (innerResponse.body && (isDisturbed(innerResponse.body.stream) || innerResponse.body.stream.locked)) { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Response body is locked or disturbed' + }) + } + + // 9. + const clonedResponse = cloneResponse(innerResponse) + + // 10. + const bodyReadPromise = createDeferredPromise() + + // 11. + if (innerResponse.body != null) { + // 11.1 + const stream = innerResponse.body.stream + + // 11.2 + const reader = stream.getReader() + + // 11.3 + readAllBytes(reader).then(bodyReadPromise.resolve, bodyReadPromise.reject) + } else { + bodyReadPromise.resolve(undefined) + } + + // 12. + /** @type {CacheBatchOperation[]} */ + const operations = [] + + // 13. + /** @type {CacheBatchOperation} */ + const operation = { + type: 'put', // 14. + request: innerRequest, // 15. + response: clonedResponse // 16. + } + + // 17. + operations.push(operation) + + // 19. + const bytes = await bodyReadPromise.promise + + if (clonedResponse.body != null) { + clonedResponse.body.source = bytes + } + + // 19.1 + const cacheJobPromise = createDeferredPromise() + + // 19.2.1 + let errorData = null + + // 19.2.2 + try { + this.#batchCacheOperations(operations) + } catch (e) { + errorData = e + } + + // 19.2.3 + queueMicrotask(() => { + // 19.2.3.1 + if (errorData === null) { + cacheJobPromise.resolve() + } else { // 19.2.3.2 + cacheJobPromise.reject(errorData) + } + }) + + return cacheJobPromise.promise + } + + async delete (request, options = {}) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.delete' }) + + request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + /** + * @type {Request} + */ + let r = null + + if (request instanceof Request) { + r = request[kState] + + if (r.method !== 'GET' && !options.ignoreMethod) { + return false + } + } else { + assert(typeof request === 'string') + + r = new Request(request)[kState] + } + + /** @type {CacheBatchOperation[]} */ + const operations = [] + + /** @type {CacheBatchOperation} */ + const operation = { + type: 'delete', + request: r, + options + } + + operations.push(operation) + + const cacheJobPromise = createDeferredPromise() + + let errorData = null + let requestResponses + + try { + requestResponses = this.#batchCacheOperations(operations) + } catch (e) { + errorData = e + } + + queueMicrotask(() => { + if (errorData === null) { + cacheJobPromise.resolve(!!requestResponses?.length) + } else { + cacheJobPromise.reject(errorData) + } + }) + + return cacheJobPromise.promise + } + + /** + * @see https://w3c.github.io/ServiceWorker/#dom-cache-keys + * @param {any} request + * @param {import('../../types/cache').CacheQueryOptions} options + * @returns {readonly Request[]} + */ + async keys (request = undefined, options = {}) { + webidl.brandCheck(this, Cache) + + if (request !== undefined) request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + // 1. + let r = null + + // 2. + if (request !== undefined) { + // 2.1 + if (request instanceof Request) { + // 2.1.1 + r = request[kState] + + // 2.1.2 + if (r.method !== 'GET' && !options.ignoreMethod) { + return [] + } + } else if (typeof request === 'string') { // 2.2 + r = new Request(request)[kState] + } + } + + // 4. + const promise = createDeferredPromise() + + // 5. + // 5.1 + const requests = [] + + // 5.2 + if (request === undefined) { + // 5.2.1 + for (const requestResponse of this.#relevantRequestResponseList) { + // 5.2.1.1 + requests.push(requestResponse[0]) + } + } else { // 5.3 + // 5.3.1 + const requestResponses = this.#queryCache(r, options) + + // 5.3.2 + for (const requestResponse of requestResponses) { + // 5.3.2.1 + requests.push(requestResponse[0]) + } + } + + // 5.4 + queueMicrotask(() => { + // 5.4.1 + const requestList = [] + + // 5.4.2 + for (const request of requests) { + const requestObject = new Request('https://a') + requestObject[kState] = request + requestObject[kHeaders][kHeadersList] = request.headersList + requestObject[kHeaders][kGuard] = 'immutable' + requestObject[kRealm] = request.client + + // 5.4.2.1 + requestList.push(requestObject) + } + + // 5.4.3 + promise.resolve(Object.freeze(requestList)) + }) + + return promise.promise + } + + /** + * @see https://w3c.github.io/ServiceWorker/#batch-cache-operations-algorithm + * @param {CacheBatchOperation[]} operations + * @returns {requestResponseList} + */ + #batchCacheOperations (operations) { + // 1. + const cache = this.#relevantRequestResponseList + + // 2. + const backupCache = [...cache] + + // 3. + const addedItems = [] + + // 4.1 + const resultList = [] + + try { + // 4.2 + for (const operation of operations) { + // 4.2.1 + if (operation.type !== 'delete' && operation.type !== 'put') { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'operation type does not match "delete" or "put"' + }) + } + + // 4.2.2 + if (operation.type === 'delete' && operation.response != null) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'delete operation should not have an associated response' + }) + } + + // 4.2.3 + if (this.#queryCache(operation.request, operation.options, addedItems).length) { + throw new DOMException('???', 'InvalidStateError') + } + + // 4.2.4 + let requestResponses + + // 4.2.5 + if (operation.type === 'delete') { + // 4.2.5.1 + requestResponses = this.#queryCache(operation.request, operation.options) + + // TODO: the spec is wrong, this is needed to pass WPTs + if (requestResponses.length === 0) { + return [] + } + + // 4.2.5.2 + for (const requestResponse of requestResponses) { + const idx = cache.indexOf(requestResponse) + assert(idx !== -1) + + // 4.2.5.2.1 + cache.splice(idx, 1) + } + } else if (operation.type === 'put') { // 4.2.6 + // 4.2.6.1 + if (operation.response == null) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'put operation should have an associated response' + }) + } + + // 4.2.6.2 + const r = operation.request + + // 4.2.6.3 + if (!urlIsHttpHttpsScheme(r.url)) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'expected http or https scheme' + }) + } + + // 4.2.6.4 + if (r.method !== 'GET') { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'not get method' + }) + } + + // 4.2.6.5 + if (operation.options != null) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'options must not be defined' + }) + } + + // 4.2.6.6 + requestResponses = this.#queryCache(operation.request) + + // 4.2.6.7 + for (const requestResponse of requestResponses) { + const idx = cache.indexOf(requestResponse) + assert(idx !== -1) + + // 4.2.6.7.1 + cache.splice(idx, 1) + } + + // 4.2.6.8 + cache.push([operation.request, operation.response]) + + // 4.2.6.10 + addedItems.push([operation.request, operation.response]) + } + + // 4.2.7 + resultList.push([operation.request, operation.response]) + } + + // 4.3 + return resultList + } catch (e) { // 5. + // 5.1 + this.#relevantRequestResponseList.length = 0 + + // 5.2 + this.#relevantRequestResponseList = backupCache + + // 5.3 + throw e + } + } + + /** + * @see https://w3c.github.io/ServiceWorker/#query-cache + * @param {any} requestQuery + * @param {import('../../types/cache').CacheQueryOptions} options + * @param {requestResponseList} targetStorage + * @returns {requestResponseList} + */ + #queryCache (requestQuery, options, targetStorage) { + /** @type {requestResponseList} */ + const resultList = [] + + const storage = targetStorage ?? this.#relevantRequestResponseList + + for (const requestResponse of storage) { + const [cachedRequest, cachedResponse] = requestResponse + if (this.#requestMatchesCachedItem(requestQuery, cachedRequest, cachedResponse, options)) { + resultList.push(requestResponse) + } + } + + return resultList + } + + /** + * @see https://w3c.github.io/ServiceWorker/#request-matches-cached-item-algorithm + * @param {any} requestQuery + * @param {any} request + * @param {any | null} response + * @param {import('../../types/cache').CacheQueryOptions | undefined} options + * @returns {boolean} + */ + #requestMatchesCachedItem (requestQuery, request, response = null, options) { + // if (options?.ignoreMethod === false && request.method === 'GET') { + // return false + // } + + const queryURL = new URL(requestQuery.url) + + const cachedURL = new URL(request.url) + + if (options?.ignoreSearch) { + cachedURL.search = '' + + queryURL.search = '' + } + + if (!urlEquals(queryURL, cachedURL, true)) { + return false + } + + if ( + response == null || + options?.ignoreVary || + !response.headersList.contains('vary') + ) { + return true + } + + const fieldValues = getFieldValues(response.headersList.get('vary')) + + for (const fieldValue of fieldValues) { + if (fieldValue === '*') { + return false + } + + const requestValue = request.headersList.get(fieldValue) + const queryValue = requestQuery.headersList.get(fieldValue) + + // If one has the header and the other doesn't, or one has + // a different value than the other, return false + if (requestValue !== queryValue) { + return false + } + } + + return true + } +} + +Object.defineProperties(Cache.prototype, { + [Symbol.toStringTag]: { + value: 'Cache', + configurable: true + }, + match: kEnumerableProperty, + matchAll: kEnumerableProperty, + add: kEnumerableProperty, + addAll: kEnumerableProperty, + put: kEnumerableProperty, + delete: kEnumerableProperty, + keys: kEnumerableProperty +}) + +const cacheQueryOptionConverters = [ + { + key: 'ignoreSearch', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'ignoreMethod', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'ignoreVary', + converter: webidl.converters.boolean, + defaultValue: false + } +] + +webidl.converters.CacheQueryOptions = webidl.dictionaryConverter(cacheQueryOptionConverters) + +webidl.converters.MultiCacheQueryOptions = webidl.dictionaryConverter([ + ...cacheQueryOptionConverters, + { + key: 'cacheName', + converter: webidl.converters.DOMString + } +]) + +webidl.converters.Response = webidl.interfaceConverter(Response) + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.RequestInfo +) + +module.exports = { + Cache +} + + +/***/ }), + +/***/ 7907: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kConstruct } = __nccwpck_require__(9174) +const { Cache } = __nccwpck_require__(6101) +const { webidl } = __nccwpck_require__(1744) +const { kEnumerableProperty } = __nccwpck_require__(3983) + +class CacheStorage { + /** + * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-name-to-cache-map + * @type {Map} + */ + async has (cacheName) { + webidl.brandCheck(this, CacheStorage) + webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.has' }) + + cacheName = webidl.converters.DOMString(cacheName) + + // 2.1.1 + // 2.2 + return this.#caches.has(cacheName) + } + + /** + * @see https://w3c.github.io/ServiceWorker/#dom-cachestorage-open + * @param {string} cacheName + * @returns {Promise} + */ + async open (cacheName) { + webidl.brandCheck(this, CacheStorage) + webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.open' }) + + cacheName = webidl.converters.DOMString(cacheName) + + // 2.1 + if (this.#caches.has(cacheName)) { + // await caches.open('v1') !== await caches.open('v1') + + // 2.1.1 + const cache = this.#caches.get(cacheName) + + // 2.1.1.1 + return new Cache(kConstruct, cache) + } + + // 2.2 + const cache = [] + + // 2.3 + this.#caches.set(cacheName, cache) + + // 2.4 + return new Cache(kConstruct, cache) + } + + /** + * @see https://w3c.github.io/ServiceWorker/#cache-storage-delete + * @param {string} cacheName + * @returns {Promise} + */ + async delete (cacheName) { + webidl.brandCheck(this, CacheStorage) + webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.delete' }) + + cacheName = webidl.converters.DOMString(cacheName) + + return this.#caches.delete(cacheName) + } + + /** + * @see https://w3c.github.io/ServiceWorker/#cache-storage-keys + * @returns {string[]} + */ + async keys () { + webidl.brandCheck(this, CacheStorage) + + // 2.1 + const keys = this.#caches.keys() + + // 2.2 + return [...keys] + } +} + +Object.defineProperties(CacheStorage.prototype, { + [Symbol.toStringTag]: { + value: 'CacheStorage', + configurable: true + }, + match: kEnumerableProperty, + has: kEnumerableProperty, + open: kEnumerableProperty, + delete: kEnumerableProperty, + keys: kEnumerableProperty +}) + +module.exports = { + CacheStorage +} + + +/***/ }), + +/***/ 9174: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +module.exports = { + kConstruct: (__nccwpck_require__(2785).kConstruct) +} + + +/***/ }), + +/***/ 2396: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const assert = __nccwpck_require__(9491) +const { URLSerializer } = __nccwpck_require__(685) +const { isValidHeaderName } = __nccwpck_require__(2538) + +/** + * @see https://url.spec.whatwg.org/#concept-url-equals + * @param {URL} A + * @param {URL} B + * @param {boolean | undefined} excludeFragment + * @returns {boolean} + */ +function urlEquals (A, B, excludeFragment = false) { + const serializedA = URLSerializer(A, excludeFragment) + + const serializedB = URLSerializer(B, excludeFragment) + + return serializedA === serializedB +} + +/** + * @see https://github.com/chromium/chromium/blob/694d20d134cb553d8d89e5500b9148012b1ba299/content/browser/cache_storage/cache_storage_cache.cc#L260-L262 + * @param {string} header + */ +function fieldValues (header) { + assert(header !== null) + + const values = [] + + for (let value of header.split(',')) { + value = value.trim() + + if (!value.length) { + continue + } else if (!isValidHeaderName(value)) { + continue + } + + values.push(value) + } + + return values +} + +module.exports = { + urlEquals, + fieldValues +} + + +/***/ }), + +/***/ 3598: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +// @ts-check + + + +/* global WebAssembly */ + +const assert = __nccwpck_require__(9491) +const net = __nccwpck_require__(1808) +const http = __nccwpck_require__(3685) +const { pipeline } = __nccwpck_require__(2781) +const util = __nccwpck_require__(3983) +const timers = __nccwpck_require__(9459) +const Request = __nccwpck_require__(2905) +const DispatcherBase = __nccwpck_require__(4839) +const { + RequestContentLengthMismatchError, + ResponseContentLengthMismatchError, + InvalidArgumentError, + RequestAbortedError, + HeadersTimeoutError, + HeadersOverflowError, + SocketError, + InformationalError, + BodyTimeoutError, + HTTPParserError, + ResponseExceededMaxSizeError, + ClientDestroyedError +} = __nccwpck_require__(8045) +const buildConnector = __nccwpck_require__(2067) +const { + kUrl, + kReset, + kServerName, + kClient, + kBusy, + kParser, + kConnect, + kBlocking, + kResuming, + kRunning, + kPending, + kSize, + kWriting, + kQueue, + kConnected, + kConnecting, + kNeedDrain, + kNoRef, + kKeepAliveDefaultTimeout, + kHostHeader, + kPendingIdx, + kRunningIdx, + kError, + kPipelining, + kSocket, + kKeepAliveTimeoutValue, + kMaxHeadersSize, + kKeepAliveMaxTimeout, + kKeepAliveTimeoutThreshold, + kHeadersTimeout, + kBodyTimeout, + kStrictContentLength, + kConnector, + kMaxRedirections, + kMaxRequests, + kCounter, + kClose, + kDestroy, + kDispatch, + kInterceptors, + kLocalAddress, + kMaxResponseSize, + kHTTPConnVersion, + // HTTP2 + kHost, + kHTTP2Session, + kHTTP2SessionState, + kHTTP2BuildRequest, + kHTTP2CopyHeaders, + kHTTP1BuildRequest +} = __nccwpck_require__(2785) + +/** @type {import('http2')} */ +let http2 +try { + http2 = __nccwpck_require__(5158) +} catch { + // @ts-ignore + http2 = { constants: {} } +} + +const { + constants: { + HTTP2_HEADER_AUTHORITY, + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_HEADER_SCHEME, + HTTP2_HEADER_CONTENT_LENGTH, + HTTP2_HEADER_EXPECT, + HTTP2_HEADER_STATUS + } +} = http2 + +// Experimental +let h2ExperimentalWarned = false + +const FastBuffer = Buffer[Symbol.species] + +const kClosedResolve = Symbol('kClosedResolve') + +const channels = {} + +try { + const diagnosticsChannel = __nccwpck_require__(7643) + channels.sendHeaders = diagnosticsChannel.channel('undici:client:sendHeaders') + channels.beforeConnect = diagnosticsChannel.channel('undici:client:beforeConnect') + channels.connectError = diagnosticsChannel.channel('undici:client:connectError') + channels.connected = diagnosticsChannel.channel('undici:client:connected') +} catch { + channels.sendHeaders = { hasSubscribers: false } + channels.beforeConnect = { hasSubscribers: false } + channels.connectError = { hasSubscribers: false } + channels.connected = { hasSubscribers: false } +} + +/** + * @type {import('../types/client').default} + */ +class Client extends DispatcherBase { + /** + * + * @param {string|URL} url + * @param {import('../types/client').Client.Options} options + */ + constructor (url, { + interceptors, + maxHeaderSize, + headersTimeout, + socketTimeout, + requestTimeout, + connectTimeout, + bodyTimeout, + idleTimeout, + keepAlive, + keepAliveTimeout, + maxKeepAliveTimeout, + keepAliveMaxTimeout, + keepAliveTimeoutThreshold, + socketPath, + pipelining, + tls, + strictContentLength, + maxCachedSessions, + maxRedirections, + connect, + maxRequestsPerClient, + localAddress, + maxResponseSize, + autoSelectFamily, + autoSelectFamilyAttemptTimeout, + // h2 + allowH2, + maxConcurrentStreams + } = {}) { + super() + + if (keepAlive !== undefined) { + throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead') + } + + if (socketTimeout !== undefined) { + throw new InvalidArgumentError('unsupported socketTimeout, use headersTimeout & bodyTimeout instead') + } + + if (requestTimeout !== undefined) { + throw new InvalidArgumentError('unsupported requestTimeout, use headersTimeout & bodyTimeout instead') + } + + if (idleTimeout !== undefined) { + throw new InvalidArgumentError('unsupported idleTimeout, use keepAliveTimeout instead') + } + + if (maxKeepAliveTimeout !== undefined) { + throw new InvalidArgumentError('unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead') + } + + if (maxHeaderSize != null && !Number.isFinite(maxHeaderSize)) { + throw new InvalidArgumentError('invalid maxHeaderSize') + } + + if (socketPath != null && typeof socketPath !== 'string') { + throw new InvalidArgumentError('invalid socketPath') + } + + if (connectTimeout != null && (!Number.isFinite(connectTimeout) || connectTimeout < 0)) { + throw new InvalidArgumentError('invalid connectTimeout') + } + + if (keepAliveTimeout != null && (!Number.isFinite(keepAliveTimeout) || keepAliveTimeout <= 0)) { + throw new InvalidArgumentError('invalid keepAliveTimeout') + } + + if (keepAliveMaxTimeout != null && (!Number.isFinite(keepAliveMaxTimeout) || keepAliveMaxTimeout <= 0)) { + throw new InvalidArgumentError('invalid keepAliveMaxTimeout') + } + + if (keepAliveTimeoutThreshold != null && !Number.isFinite(keepAliveTimeoutThreshold)) { + throw new InvalidArgumentError('invalid keepAliveTimeoutThreshold') + } + + if (headersTimeout != null && (!Number.isInteger(headersTimeout) || headersTimeout < 0)) { + throw new InvalidArgumentError('headersTimeout must be a positive integer or zero') + } + + if (bodyTimeout != null && (!Number.isInteger(bodyTimeout) || bodyTimeout < 0)) { + throw new InvalidArgumentError('bodyTimeout must be a positive integer or zero') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) { + throw new InvalidArgumentError('maxRedirections must be a positive number') + } + + if (maxRequestsPerClient != null && (!Number.isInteger(maxRequestsPerClient) || maxRequestsPerClient < 0)) { + throw new InvalidArgumentError('maxRequestsPerClient must be a positive number') + } + + if (localAddress != null && (typeof localAddress !== 'string' || net.isIP(localAddress) === 0)) { + throw new InvalidArgumentError('localAddress must be valid string IP address') + } + + if (maxResponseSize != null && (!Number.isInteger(maxResponseSize) || maxResponseSize < -1)) { + throw new InvalidArgumentError('maxResponseSize must be a positive number') + } + + if ( + autoSelectFamilyAttemptTimeout != null && + (!Number.isInteger(autoSelectFamilyAttemptTimeout) || autoSelectFamilyAttemptTimeout < -1) + ) { + throw new InvalidArgumentError('autoSelectFamilyAttemptTimeout must be a positive number') + } + + // h2 + if (allowH2 != null && typeof allowH2 !== 'boolean') { + throw new InvalidArgumentError('allowH2 must be a valid boolean value') + } + + if (maxConcurrentStreams != null && (typeof maxConcurrentStreams !== 'number' || maxConcurrentStreams < 1)) { + throw new InvalidArgumentError('maxConcurrentStreams must be a possitive integer, greater than 0') + } + + if (typeof connect !== 'function') { + connect = buildConnector({ + ...tls, + maxCachedSessions, + allowH2, + socketPath, + timeout: connectTimeout, + ...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), + ...connect + }) + } + + this[kInterceptors] = interceptors && interceptors.Client && Array.isArray(interceptors.Client) + ? interceptors.Client + : [createRedirectInterceptor({ maxRedirections })] + this[kUrl] = util.parseOrigin(url) + this[kConnector] = connect + this[kSocket] = null + this[kPipelining] = pipelining != null ? pipelining : 1 + this[kMaxHeadersSize] = maxHeaderSize || http.maxHeaderSize + this[kKeepAliveDefaultTimeout] = keepAliveTimeout == null ? 4e3 : keepAliveTimeout + this[kKeepAliveMaxTimeout] = keepAliveMaxTimeout == null ? 600e3 : keepAliveMaxTimeout + this[kKeepAliveTimeoutThreshold] = keepAliveTimeoutThreshold == null ? 1e3 : keepAliveTimeoutThreshold + this[kKeepAliveTimeoutValue] = this[kKeepAliveDefaultTimeout] + this[kServerName] = null + this[kLocalAddress] = localAddress != null ? localAddress : null + this[kResuming] = 0 // 0, idle, 1, scheduled, 2 resuming + this[kNeedDrain] = 0 // 0, idle, 1, scheduled, 2 resuming + this[kHostHeader] = `host: ${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ''}\r\n` + this[kBodyTimeout] = bodyTimeout != null ? bodyTimeout : 300e3 + this[kHeadersTimeout] = headersTimeout != null ? headersTimeout : 300e3 + this[kStrictContentLength] = strictContentLength == null ? true : strictContentLength + this[kMaxRedirections] = maxRedirections + this[kMaxRequests] = maxRequestsPerClient + this[kClosedResolve] = null + this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1 + this[kHTTPConnVersion] = 'h1' + + // HTTP/2 + this[kHTTP2Session] = null + this[kHTTP2SessionState] = !allowH2 + ? null + : { + // streams: null, // Fixed queue of streams - For future support of `push` + openStreams: 0, // Keep track of them to decide wether or not unref the session + maxConcurrentStreams: maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server + } + this[kHost] = `${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ''}` + + // kQueue is built up of 3 sections separated by + // the kRunningIdx and kPendingIdx indices. + // | complete | running | pending | + // ^ kRunningIdx ^ kPendingIdx ^ kQueue.length + // kRunningIdx points to the first running element. + // kPendingIdx points to the first pending element. + // This implements a fast queue with an amortized + // time of O(1). + + this[kQueue] = [] + this[kRunningIdx] = 0 + this[kPendingIdx] = 0 + } + + get pipelining () { + return this[kPipelining] + } + + set pipelining (value) { + this[kPipelining] = value + resume(this, true) + } + + get [kPending] () { + return this[kQueue].length - this[kPendingIdx] + } + + get [kRunning] () { + return this[kPendingIdx] - this[kRunningIdx] + } + + get [kSize] () { + return this[kQueue].length - this[kRunningIdx] + } + + get [kConnected] () { + return !!this[kSocket] && !this[kConnecting] && !this[kSocket].destroyed + } + + get [kBusy] () { + const socket = this[kSocket] + return ( + (socket && (socket[kReset] || socket[kWriting] || socket[kBlocking])) || + (this[kSize] >= (this[kPipelining] || 1)) || + this[kPending] > 0 + ) + } + + /* istanbul ignore: only used for test */ + [kConnect] (cb) { + connect(this) + this.once('connect', cb) + } + + [kDispatch] (opts, handler) { + const origin = opts.origin || this[kUrl].origin + + const request = this[kHTTPConnVersion] === 'h2' + ? Request[kHTTP2BuildRequest](origin, opts, handler) + : Request[kHTTP1BuildRequest](origin, opts, handler) + + this[kQueue].push(request) + if (this[kResuming]) { + // Do nothing. + } else if (util.bodyLength(request.body) == null && util.isIterable(request.body)) { + // Wait a tick in case stream/iterator is ended in the same tick. + this[kResuming] = 1 + process.nextTick(resume, this) + } else { + resume(this, true) + } + + if (this[kResuming] && this[kNeedDrain] !== 2 && this[kBusy]) { + this[kNeedDrain] = 2 + } + + return this[kNeedDrain] < 2 + } + + async [kClose] () { + // TODO: for H2 we need to gracefully flush the remaining enqueued + // request and close each stream. + return new Promise((resolve) => { + if (!this[kSize]) { + resolve(null) + } else { + this[kClosedResolve] = resolve + } + }) + } + + async [kDestroy] (err) { + return new Promise((resolve) => { + const requests = this[kQueue].splice(this[kPendingIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(this, request, err) + } + + const callback = () => { + if (this[kClosedResolve]) { + // TODO (fix): Should we error here with ClientDestroyedError? + this[kClosedResolve]() + this[kClosedResolve] = null + } + resolve() + } + + if (this[kHTTP2Session] != null) { + util.destroy(this[kHTTP2Session], err) + this[kHTTP2Session] = null + this[kHTTP2SessionState] = null + } + + if (!this[kSocket]) { + queueMicrotask(callback) + } else { + util.destroy(this[kSocket].on('close', callback), err) + } + + resume(this) + }) + } +} + +function onHttp2SessionError (err) { + assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') + + this[kSocket][kError] = err + + onError(this[kClient], err) +} + +function onHttp2FrameError (type, code, id) { + const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) + + if (id === 0) { + this[kSocket][kError] = err + onError(this[kClient], err) + } +} + +function onHttp2SessionEnd () { + util.destroy(this, new SocketError('other side closed')) + util.destroy(this[kSocket], new SocketError('other side closed')) +} + +function onHTTP2GoAway (code) { + const client = this[kClient] + const err = new InformationalError(`HTTP/2: "GOAWAY" frame received with code ${code}`) + client[kSocket] = null + client[kHTTP2Session] = null + + if (client.destroyed) { + assert(this[kPending] === 0) + + // Fail entire queue. + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(this, request, err) + } + } else if (client[kRunning] > 0) { + // Fail head of pipeline. + const request = client[kQueue][client[kRunningIdx]] + client[kQueue][client[kRunningIdx]++] = null + + errorRequest(client, request, err) + } + + client[kPendingIdx] = client[kRunningIdx] + + assert(client[kRunning] === 0) + + client.emit('disconnect', + client[kUrl], + [client], + err + ) + + resume(client) +} + +const constants = __nccwpck_require__(953) +const createRedirectInterceptor = __nccwpck_require__(8861) +const EMPTY_BUF = Buffer.alloc(0) + +async function lazyllhttp () { + const llhttpWasmData = process.env.JEST_WORKER_ID ? __nccwpck_require__(1145) : undefined + + let mod + try { + mod = await WebAssembly.compile(Buffer.from(__nccwpck_require__(5627), 'base64')) + } catch (e) { + /* istanbul ignore next */ + + // We could check if the error was caused by the simd option not + // being enabled, but the occurring of this other error + // * https://github.com/emscripten-core/emscripten/issues/11495 + // got me to remove that check to avoid breaking Node 12. + mod = await WebAssembly.compile(Buffer.from(llhttpWasmData || __nccwpck_require__(1145), 'base64')) + } + + return await WebAssembly.instantiate(mod, { + env: { + /* eslint-disable camelcase */ + + wasm_on_url: (p, at, len) => { + /* istanbul ignore next */ + return 0 + }, + wasm_on_status: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_message_begin: (p) => { + assert.strictEqual(currentParser.ptr, p) + return currentParser.onMessageBegin() || 0 + }, + wasm_on_header_field: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_header_value: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => { + assert.strictEqual(currentParser.ptr, p) + return currentParser.onHeadersComplete(statusCode, Boolean(upgrade), Boolean(shouldKeepAlive)) || 0 + }, + wasm_on_body: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_message_complete: (p) => { + assert.strictEqual(currentParser.ptr, p) + return currentParser.onMessageComplete() || 0 + } + + /* eslint-enable camelcase */ + } + }) +} + +let llhttpInstance = null +let llhttpPromise = lazyllhttp() +llhttpPromise.catch() + +let currentParser = null +let currentBufferRef = null +let currentBufferSize = 0 +let currentBufferPtr = null + +const TIMEOUT_HEADERS = 1 +const TIMEOUT_BODY = 2 +const TIMEOUT_IDLE = 3 + +class Parser { + constructor (client, socket, { exports }) { + assert(Number.isFinite(client[kMaxHeadersSize]) && client[kMaxHeadersSize] > 0) + + this.llhttp = exports + this.ptr = this.llhttp.llhttp_alloc(constants.TYPE.RESPONSE) + this.client = client + this.socket = socket + this.timeout = null + this.timeoutValue = null + this.timeoutType = null + this.statusCode = null + this.statusText = '' + this.upgrade = false + this.headers = [] + this.headersSize = 0 + this.headersMaxSize = client[kMaxHeadersSize] + this.shouldKeepAlive = false + this.paused = false + this.resume = this.resume.bind(this) + + this.bytesRead = 0 + + this.keepAlive = '' + this.contentLength = '' + this.connection = '' + this.maxResponseSize = client[kMaxResponseSize] + } + + setTimeout (value, type) { + this.timeoutType = type + if (value !== this.timeoutValue) { + timers.clearTimeout(this.timeout) + if (value) { + this.timeout = timers.setTimeout(onParserTimeout, value, this) + // istanbul ignore else: only for jest + if (this.timeout.unref) { + this.timeout.unref() + } + } else { + this.timeout = null + } + this.timeoutValue = value + } else if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + } + + resume () { + if (this.socket.destroyed || !this.paused) { + return + } + + assert(this.ptr != null) + assert(currentParser == null) + + this.llhttp.llhttp_resume(this.ptr) + + assert(this.timeoutType === TIMEOUT_BODY) + if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + this.paused = false + this.execute(this.socket.read() || EMPTY_BUF) // Flush parser. + this.readMore() + } + + readMore () { + while (!this.paused && this.ptr) { + const chunk = this.socket.read() + if (chunk === null) { + break + } + this.execute(chunk) + } + } + + execute (data) { + assert(this.ptr != null) + assert(currentParser == null) + assert(!this.paused) + + const { socket, llhttp } = this + + if (data.length > currentBufferSize) { + if (currentBufferPtr) { + llhttp.free(currentBufferPtr) + } + currentBufferSize = Math.ceil(data.length / 4096) * 4096 + currentBufferPtr = llhttp.malloc(currentBufferSize) + } + + new Uint8Array(llhttp.memory.buffer, currentBufferPtr, currentBufferSize).set(data) + + // Call `execute` on the wasm parser. + // We pass the `llhttp_parser` pointer address, the pointer address of buffer view data, + // and finally the length of bytes to parse. + // The return value is an error code or `constants.ERROR.OK`. + try { + let ret + + try { + currentBufferRef = data + currentParser = this + ret = llhttp.llhttp_execute(this.ptr, currentBufferPtr, data.length) + /* eslint-disable-next-line no-useless-catch */ + } catch (err) { + /* istanbul ignore next: difficult to make a test case for */ + throw err + } finally { + currentParser = null + currentBufferRef = null + } + + const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr + + if (ret === constants.ERROR.PAUSED_UPGRADE) { + this.onUpgrade(data.slice(offset)) + } else if (ret === constants.ERROR.PAUSED) { + this.paused = true + socket.unshift(data.slice(offset)) + } else if (ret !== constants.ERROR.OK) { + const ptr = llhttp.llhttp_get_error_reason(this.ptr) + let message = '' + /* istanbul ignore else: difficult to make a test case for */ + if (ptr) { + const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0) + message = + 'Response does not match the HTTP/1.1 protocol (' + + Buffer.from(llhttp.memory.buffer, ptr, len).toString() + + ')' + } + throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset)) + } + } catch (err) { + util.destroy(socket, err) + } + } + + destroy () { + assert(this.ptr != null) + assert(currentParser == null) + + this.llhttp.llhttp_free(this.ptr) + this.ptr = null + + timers.clearTimeout(this.timeout) + this.timeout = null + this.timeoutValue = null + this.timeoutType = null + + this.paused = false + } + + onStatus (buf) { + this.statusText = buf.toString() + } + + onMessageBegin () { + const { socket, client } = this + + /* istanbul ignore next: difficult to make a test case for */ + if (socket.destroyed) { + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + if (!request) { + return -1 + } + } + + onHeaderField (buf) { + const len = this.headers.length + + if ((len & 1) === 0) { + this.headers.push(buf) + } else { + this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf]) + } + + this.trackHeader(buf.length) + } + + onHeaderValue (buf) { + let len = this.headers.length + + if ((len & 1) === 1) { + this.headers.push(buf) + len += 1 + } else { + this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf]) + } + + const key = this.headers[len - 2] + if (key.length === 10 && key.toString().toLowerCase() === 'keep-alive') { + this.keepAlive += buf.toString() + } else if (key.length === 10 && key.toString().toLowerCase() === 'connection') { + this.connection += buf.toString() + } else if (key.length === 14 && key.toString().toLowerCase() === 'content-length') { + this.contentLength += buf.toString() + } + + this.trackHeader(buf.length) + } + + trackHeader (len) { + this.headersSize += len + if (this.headersSize >= this.headersMaxSize) { + util.destroy(this.socket, new HeadersOverflowError()) + } + } + + onUpgrade (head) { + const { upgrade, client, socket, headers, statusCode } = this + + assert(upgrade) + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + assert(!socket.destroyed) + assert(socket === client[kSocket]) + assert(!this.paused) + assert(request.upgrade || request.method === 'CONNECT') + + this.statusCode = null + this.statusText = '' + this.shouldKeepAlive = null + + assert(this.headers.length % 2 === 0) + this.headers = [] + this.headersSize = 0 + + socket.unshift(head) + + socket[kParser].destroy() + socket[kParser] = null + + socket[kClient] = null + socket[kError] = null + socket + .removeListener('error', onSocketError) + .removeListener('readable', onSocketReadable) + .removeListener('end', onSocketEnd) + .removeListener('close', onSocketClose) + + client[kSocket] = null + client[kQueue][client[kRunningIdx]++] = null + client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade')) + + try { + request.onUpgrade(statusCode, headers, socket) + } catch (err) { + util.destroy(socket, err) + } + + resume(client) + } + + onHeadersComplete (statusCode, upgrade, shouldKeepAlive) { + const { client, socket, headers, statusText } = this + + /* istanbul ignore next: difficult to make a test case for */ + if (socket.destroyed) { + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + + /* istanbul ignore next: difficult to make a test case for */ + if (!request) { + return -1 + } + + assert(!this.upgrade) + assert(this.statusCode < 200) + + if (statusCode === 100) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) + return -1 + } + + /* this can only happen if server is misbehaving */ + if (upgrade && !request.upgrade) { + util.destroy(socket, new SocketError('bad upgrade', util.getSocketInfo(socket))) + return -1 + } + + assert.strictEqual(this.timeoutType, TIMEOUT_HEADERS) + + this.statusCode = statusCode + this.shouldKeepAlive = ( + shouldKeepAlive || + // Override llhttp value which does not allow keepAlive for HEAD. + (request.method === 'HEAD' && !socket[kReset] && this.connection.toLowerCase() === 'keep-alive') + ) + + if (this.statusCode >= 200) { + const bodyTimeout = request.bodyTimeout != null + ? request.bodyTimeout + : client[kBodyTimeout] + this.setTimeout(bodyTimeout, TIMEOUT_BODY) + } else if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + if (request.method === 'CONNECT') { + assert(client[kRunning] === 1) + this.upgrade = true + return 2 + } + + if (upgrade) { + assert(client[kRunning] === 1) + this.upgrade = true + return 2 + } + + assert(this.headers.length % 2 === 0) + this.headers = [] + this.headersSize = 0 + + if (this.shouldKeepAlive && client[kPipelining]) { + const keepAliveTimeout = this.keepAlive ? util.parseKeepAliveTimeout(this.keepAlive) : null + + if (keepAliveTimeout != null) { + const timeout = Math.min( + keepAliveTimeout - client[kKeepAliveTimeoutThreshold], + client[kKeepAliveMaxTimeout] + ) + if (timeout <= 0) { + socket[kReset] = true + } else { + client[kKeepAliveTimeoutValue] = timeout + } + } else { + client[kKeepAliveTimeoutValue] = client[kKeepAliveDefaultTimeout] + } + } else { + // Stop more requests from being dispatched. + socket[kReset] = true + } + + const pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false + + if (request.aborted) { + return -1 + } + + if (request.method === 'HEAD') { + return 1 + } + + if (statusCode < 200) { + return 1 + } + + if (socket[kBlocking]) { + socket[kBlocking] = false + resume(client) + } + + return pause ? constants.ERROR.PAUSED : 0 + } + + onBody (buf) { + const { client, socket, statusCode, maxResponseSize } = this + + if (socket.destroyed) { + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + assert.strictEqual(this.timeoutType, TIMEOUT_BODY) + if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + assert(statusCode >= 200) + + if (maxResponseSize > -1 && this.bytesRead + buf.length > maxResponseSize) { + util.destroy(socket, new ResponseExceededMaxSizeError()) + return -1 + } + + this.bytesRead += buf.length + + if (request.onData(buf) === false) { + return constants.ERROR.PAUSED + } + } + + onMessageComplete () { + const { client, socket, statusCode, upgrade, headers, contentLength, bytesRead, shouldKeepAlive } = this + + if (socket.destroyed && (!statusCode || shouldKeepAlive)) { + return -1 + } + + if (upgrade) { + return + } + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + assert(statusCode >= 100) + + this.statusCode = null + this.statusText = '' + this.bytesRead = 0 + this.contentLength = '' + this.keepAlive = '' + this.connection = '' + + assert(this.headers.length % 2 === 0) + this.headers = [] + this.headersSize = 0 + + if (statusCode < 200) { + return + } + + /* istanbul ignore next: should be handled by llhttp? */ + if (request.method !== 'HEAD' && contentLength && bytesRead !== parseInt(contentLength, 10)) { + util.destroy(socket, new ResponseContentLengthMismatchError()) + return -1 + } + + request.onComplete(headers) + + client[kQueue][client[kRunningIdx]++] = null + + if (socket[kWriting]) { + assert.strictEqual(client[kRunning], 0) + // Response completed before request. + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (!shouldKeepAlive) { + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (socket[kReset] && client[kRunning] === 0) { + // Destroy socket once all requests have completed. + // The request at the tail of the pipeline is the one + // that requested reset and no further requests should + // have been queued since then. + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (client[kPipelining] === 1) { + // We must wait a full event loop cycle to reuse this socket to make sure + // that non-spec compliant servers are not closing the connection even if they + // said they won't. + setImmediate(resume, client) + } else { + resume(client) + } + } +} + +function onParserTimeout (parser) { + const { socket, timeoutType, client } = parser + + /* istanbul ignore else */ + if (timeoutType === TIMEOUT_HEADERS) { + if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) { + assert(!parser.paused, 'cannot be paused while waiting for headers') + util.destroy(socket, new HeadersTimeoutError()) + } + } else if (timeoutType === TIMEOUT_BODY) { + if (!parser.paused) { + util.destroy(socket, new BodyTimeoutError()) + } + } else if (timeoutType === TIMEOUT_IDLE) { + assert(client[kRunning] === 0 && client[kKeepAliveTimeoutValue]) + util.destroy(socket, new InformationalError('socket idle timeout')) + } +} + +function onSocketReadable () { + const { [kParser]: parser } = this + if (parser) { + parser.readMore() + } +} + +function onSocketError (err) { + const { [kClient]: client, [kParser]: parser } = this + + assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') + + if (client[kHTTPConnVersion] !== 'h2') { + // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded + // to the user. + if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so for as a valid response. + parser.onMessageComplete() + return + } + } + + this[kError] = err + + onError(this[kClient], err) +} + +function onError (client, err) { + if ( + client[kRunning] === 0 && + err.code !== 'UND_ERR_INFO' && + err.code !== 'UND_ERR_SOCKET' + ) { + // Error is not caused by running request and not a recoverable + // socket error. + + assert(client[kPendingIdx] === client[kRunningIdx]) + + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(client, request, err) + } + assert(client[kSize] === 0) + } +} + +function onSocketEnd () { + const { [kParser]: parser, [kClient]: client } = this + + if (client[kHTTPConnVersion] !== 'h2') { + if (parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so far as a valid response. + parser.onMessageComplete() + return + } + } + + util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this))) +} + +function onSocketClose () { + const { [kClient]: client, [kParser]: parser } = this + + if (client[kHTTPConnVersion] === 'h1' && parser) { + if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so far as a valid response. + parser.onMessageComplete() + } + + this[kParser].destroy() + this[kParser] = null + } + + const err = this[kError] || new SocketError('closed', util.getSocketInfo(this)) + + client[kSocket] = null + + if (client.destroyed) { + assert(client[kPending] === 0) + + // Fail entire queue. + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(client, request, err) + } + } else if (client[kRunning] > 0 && err.code !== 'UND_ERR_INFO') { + // Fail head of pipeline. + const request = client[kQueue][client[kRunningIdx]] + client[kQueue][client[kRunningIdx]++] = null + + errorRequest(client, request, err) + } + + client[kPendingIdx] = client[kRunningIdx] + + assert(client[kRunning] === 0) + + client.emit('disconnect', client[kUrl], [client], err) + + resume(client) +} + +async function connect (client) { + assert(!client[kConnecting]) + assert(!client[kSocket]) + + let { host, hostname, protocol, port } = client[kUrl] + + // Resolve ipv6 + if (hostname[0] === '[') { + const idx = hostname.indexOf(']') + + assert(idx !== -1) + const ip = hostname.substring(1, idx) + + assert(net.isIP(ip)) + hostname = ip + } + + client[kConnecting] = true + + if (channels.beforeConnect.hasSubscribers) { + channels.beforeConnect.publish({ + connectParams: { + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector] + }) + } + + try { + const socket = await new Promise((resolve, reject) => { + client[kConnector]({ + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, (err, socket) => { + if (err) { + reject(err) + } else { + resolve(socket) + } + }) + }) + + if (client.destroyed) { + util.destroy(socket.on('error', () => {}), new ClientDestroyedError()) + return + } + + client[kConnecting] = false + + assert(socket) + + const isH2 = socket.alpnProtocol === 'h2' + if (isH2) { + if (!h2ExperimentalWarned) { + h2ExperimentalWarned = true + process.emitWarning('H2 support is experimental, expect them to change at any time.', { + code: 'UNDICI-H2' + }) + } + + const session = http2.connect(client[kUrl], { + createConnection: () => socket, + peerMaxConcurrentStreams: client[kHTTP2SessionState].maxConcurrentStreams + }) + + client[kHTTPConnVersion] = 'h2' + session[kClient] = client + session[kSocket] = socket + session.on('error', onHttp2SessionError) + session.on('frameError', onHttp2FrameError) + session.on('end', onHttp2SessionEnd) + session.on('goaway', onHTTP2GoAway) + session.on('close', onSocketClose) + session.unref() + + client[kHTTP2Session] = session + socket[kHTTP2Session] = session + } else { + if (!llhttpInstance) { + llhttpInstance = await llhttpPromise + llhttpPromise = null + } + + socket[kNoRef] = false + socket[kWriting] = false + socket[kReset] = false + socket[kBlocking] = false + socket[kParser] = new Parser(client, socket, llhttpInstance) + } + + socket[kCounter] = 0 + socket[kMaxRequests] = client[kMaxRequests] + socket[kClient] = client + socket[kError] = null + + socket + .on('error', onSocketError) + .on('readable', onSocketReadable) + .on('end', onSocketEnd) + .on('close', onSocketClose) + + client[kSocket] = socket + + if (channels.connected.hasSubscribers) { + channels.connected.publish({ + connectParams: { + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector], + socket + }) + } + client.emit('connect', client[kUrl], [client]) + } catch (err) { + if (client.destroyed) { + return + } + + client[kConnecting] = false + + if (channels.connectError.hasSubscribers) { + channels.connectError.publish({ + connectParams: { + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector], + error: err + }) + } + + if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') { + assert(client[kRunning] === 0) + while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) { + const request = client[kQueue][client[kPendingIdx]++] + errorRequest(client, request, err) + } + } else { + onError(client, err) + } + + client.emit('connectionError', client[kUrl], [client], err) + } + + resume(client) +} + +function emitDrain (client) { + client[kNeedDrain] = 0 + client.emit('drain', client[kUrl], [client]) +} + +function resume (client, sync) { + if (client[kResuming] === 2) { + return + } + + client[kResuming] = 2 + + _resume(client, sync) + client[kResuming] = 0 + + if (client[kRunningIdx] > 256) { + client[kQueue].splice(0, client[kRunningIdx]) + client[kPendingIdx] -= client[kRunningIdx] + client[kRunningIdx] = 0 + } +} + +function _resume (client, sync) { + while (true) { + if (client.destroyed) { + assert(client[kPending] === 0) + return + } + + if (client[kClosedResolve] && !client[kSize]) { + client[kClosedResolve]() + client[kClosedResolve] = null + return + } + + const socket = client[kSocket] + + if (socket && !socket.destroyed && socket.alpnProtocol !== 'h2') { + if (client[kSize] === 0) { + if (!socket[kNoRef] && socket.unref) { + socket.unref() + socket[kNoRef] = true + } + } else if (socket[kNoRef] && socket.ref) { + socket.ref() + socket[kNoRef] = false + } + + if (client[kSize] === 0) { + if (socket[kParser].timeoutType !== TIMEOUT_IDLE) { + socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_IDLE) + } + } else if (client[kRunning] > 0 && socket[kParser].statusCode < 200) { + if (socket[kParser].timeoutType !== TIMEOUT_HEADERS) { + const request = client[kQueue][client[kRunningIdx]] + const headersTimeout = request.headersTimeout != null + ? request.headersTimeout + : client[kHeadersTimeout] + socket[kParser].setTimeout(headersTimeout, TIMEOUT_HEADERS) + } + } + } + + if (client[kBusy]) { + client[kNeedDrain] = 2 + } else if (client[kNeedDrain] === 2) { + if (sync) { + client[kNeedDrain] = 1 + process.nextTick(emitDrain, client) + } else { + emitDrain(client) + } + continue + } + + if (client[kPending] === 0) { + return + } + + if (client[kRunning] >= (client[kPipelining] || 1)) { + return + } + + const request = client[kQueue][client[kPendingIdx]] + + if (client[kUrl].protocol === 'https:' && client[kServerName] !== request.servername) { + if (client[kRunning] > 0) { + return + } + + client[kServerName] = request.servername + + if (socket && socket.servername !== request.servername) { + util.destroy(socket, new InformationalError('servername changed')) + return + } + } + + if (client[kConnecting]) { + return + } + + if (!socket && !client[kHTTP2Session]) { + connect(client) + return + } + + if (socket.destroyed || socket[kWriting] || socket[kReset] || socket[kBlocking]) { + return + } + + if (client[kRunning] > 0 && !request.idempotent) { + // Non-idempotent request cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + return + } + + if (client[kRunning] > 0 && (request.upgrade || request.method === 'CONNECT')) { + // Don't dispatch an upgrade until all preceding requests have completed. + // A misbehaving server might upgrade the connection before all pipelined + // request has completed. + return + } + + if (client[kRunning] > 0 && util.bodyLength(request.body) !== 0 && + (util.isStream(request.body) || util.isAsyncIterable(request.body))) { + // Request with stream or iterator body can error while other requests + // are inflight and indirectly error those as well. + // Ensure this doesn't happen by waiting for inflight + // to complete before dispatching. + + // Request with stream or iterator body cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + return + } + + if (!request.aborted && write(client, request)) { + client[kPendingIdx]++ + } else { + client[kQueue].splice(client[kPendingIdx], 1) + } + } +} + +// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2 +function shouldSendContentLength (method) { + return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT' +} + +function write (client, request) { + if (client[kHTTPConnVersion] === 'h2') { + writeH2(client, client[kHTTP2Session], request) + return + } + + const { body, method, path, host, upgrade, headers, blocking, reset } = request + + // https://tools.ietf.org/html/rfc7231#section-4.3.1 + // https://tools.ietf.org/html/rfc7231#section-4.3.2 + // https://tools.ietf.org/html/rfc7231#section-4.3.5 + + // Sending a payload body on a request that does not + // expect it can cause undefined behavior on some + // servers and corrupt connection state. Do not + // re-use the connection for further requests. + + const expectsPayload = ( + method === 'PUT' || + method === 'POST' || + method === 'PATCH' + ) + + if (body && typeof body.read === 'function') { + // Try to read EOF in order to get length. + body.read(0) + } + + const bodyLength = util.bodyLength(body) + + let contentLength = bodyLength + + if (contentLength === null) { + contentLength = request.contentLength + } + + if (contentLength === 0 && !expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD NOT send a Content-Length header field when + // the request message does not contain a payload body and the method + // semantics do not anticipate such a body. + + contentLength = null + } + + // https://github.com/nodejs/undici/issues/2046 + // A user agent may send a Content-Length header with 0 value, this should be allowed. + if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength !== null && request.contentLength !== contentLength) { + if (client[kStrictContentLength]) { + errorRequest(client, request, new RequestContentLengthMismatchError()) + return false + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + const socket = client[kSocket] + + try { + request.onConnect((err) => { + if (request.aborted || request.completed) { + return + } + + errorRequest(client, request, err || new RequestAbortedError()) + + util.destroy(socket, new InformationalError('aborted')) + }) + } catch (err) { + errorRequest(client, request, err) + } + + if (request.aborted) { + return false + } + + if (method === 'HEAD') { + // https://github.com/mcollina/undici/issues/258 + // Close after a HEAD request to interop with misbehaving servers + // that may send a body in the response. + + socket[kReset] = true + } + + if (upgrade || method === 'CONNECT') { + // On CONNECT or upgrade, block pipeline from dispatching further + // requests on this connection. + + socket[kReset] = true + } + + if (reset != null) { + socket[kReset] = reset + } + + if (client[kMaxRequests] && socket[kCounter]++ >= client[kMaxRequests]) { + socket[kReset] = true + } + + if (blocking) { + socket[kBlocking] = true + } + + let header = `${method} ${path} HTTP/1.1\r\n` + + if (typeof host === 'string') { + header += `host: ${host}\r\n` + } else { + header += client[kHostHeader] + } + + if (upgrade) { + header += `connection: upgrade\r\nupgrade: ${upgrade}\r\n` + } else if (client[kPipelining] && !socket[kReset]) { + header += 'connection: keep-alive\r\n' + } else { + header += 'connection: close\r\n' + } + + if (headers) { + header += headers + } + + if (channels.sendHeaders.hasSubscribers) { + channels.sendHeaders.publish({ request, headers: header, socket }) + } + + /* istanbul ignore else: assertion */ + if (!body || bodyLength === 0) { + if (contentLength === 0) { + socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') + } else { + assert(contentLength === null, 'no body must not have content length') + socket.write(`${header}\r\n`, 'latin1') + } + request.onRequestSent() + } else if (util.isBuffer(body)) { + assert(contentLength === body.byteLength, 'buffer body must have content length') + + socket.cork() + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') + socket.write(body) + socket.uncork() + request.onBodySent(body) + request.onRequestSent() + if (!expectsPayload) { + socket[kReset] = true + } + } else if (util.isBlobLike(body)) { + if (typeof body.stream === 'function') { + writeIterable({ body: body.stream(), client, request, socket, contentLength, header, expectsPayload }) + } else { + writeBlob({ body, client, request, socket, contentLength, header, expectsPayload }) + } + } else if (util.isStream(body)) { + writeStream({ body, client, request, socket, contentLength, header, expectsPayload }) + } else if (util.isIterable(body)) { + writeIterable({ body, client, request, socket, contentLength, header, expectsPayload }) + } else { + assert(false) + } + + return true +} + +function writeH2 (client, session, request) { + const { body, method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request + + let headers + if (typeof reqHeaders === 'string') headers = Request[kHTTP2CopyHeaders](reqHeaders.trim()) + else headers = reqHeaders + + if (upgrade) { + errorRequest(client, request, new Error('Upgrade not supported for H2')) + return false + } + + try { + // TODO(HTTP/2): Should we call onConnect immediately or on stream ready event? + request.onConnect((err) => { + if (request.aborted || request.completed) { + return + } + + errorRequest(client, request, err || new RequestAbortedError()) + }) + } catch (err) { + errorRequest(client, request, err) + } + + if (request.aborted) { + return false + } + + /** @type {import('node:http2').ClientHttp2Stream} */ + let stream + const h2State = client[kHTTP2SessionState] + + headers[HTTP2_HEADER_AUTHORITY] = host || client[kHost] + headers[HTTP2_HEADER_METHOD] = method + + if (method === 'CONNECT') { + session.ref() + // we are already connected, streams are pending, first request + // will create a new stream. We trigger a request to create the stream and wait until + // `ready` event is triggered + // We disabled endStream to allow the user to write to the stream + stream = session.request(headers, { endStream: false, signal }) + + if (stream.id && !stream.pending) { + request.onUpgrade(null, null, stream) + ++h2State.openStreams + } else { + stream.once('ready', () => { + request.onUpgrade(null, null, stream) + ++h2State.openStreams + }) + } + + stream.once('close', () => { + h2State.openStreams -= 1 + // TODO(HTTP/2): unref only if current streams count is 0 + if (h2State.openStreams === 0) session.unref() + }) + + return true + } + + // https://tools.ietf.org/html/rfc7540#section-8.3 + // :path and :scheme headers must be omited when sending CONNECT + + headers[HTTP2_HEADER_PATH] = path + headers[HTTP2_HEADER_SCHEME] = 'https' + + // https://tools.ietf.org/html/rfc7231#section-4.3.1 + // https://tools.ietf.org/html/rfc7231#section-4.3.2 + // https://tools.ietf.org/html/rfc7231#section-4.3.5 + + // Sending a payload body on a request that does not + // expect it can cause undefined behavior on some + // servers and corrupt connection state. Do not + // re-use the connection for further requests. + + const expectsPayload = ( + method === 'PUT' || + method === 'POST' || + method === 'PATCH' + ) + + if (body && typeof body.read === 'function') { + // Try to read EOF in order to get length. + body.read(0) + } + + let contentLength = util.bodyLength(body) + + if (contentLength == null) { + contentLength = request.contentLength + } + + if (contentLength === 0 || !expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD NOT send a Content-Length header field when + // the request message does not contain a payload body and the method + // semantics do not anticipate such a body. + + contentLength = null + } + + // https://github.com/nodejs/undici/issues/2046 + // A user agent may send a Content-Length header with 0 value, this should be allowed. + if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength != null && request.contentLength !== contentLength) { + if (client[kStrictContentLength]) { + errorRequest(client, request, new RequestContentLengthMismatchError()) + return false + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + if (contentLength != null) { + assert(body, 'no body must not have content length') + headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}` + } + + session.ref() + + const shouldEndStream = method === 'GET' || method === 'HEAD' + if (expectContinue) { + headers[HTTP2_HEADER_EXPECT] = '100-continue' + stream = session.request(headers, { endStream: shouldEndStream, signal }) + + stream.once('continue', writeBodyH2) + } else { + stream = session.request(headers, { + endStream: shouldEndStream, + signal + }) + writeBodyH2() + } + + // Increment counter as we have new several streams open + ++h2State.openStreams + + stream.once('response', headers => { + const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers + + if (request.onHeaders(Number(statusCode), realHeaders, stream.resume.bind(stream), '') === false) { + stream.pause() + } + }) + + stream.once('end', () => { + request.onComplete([]) + }) + + stream.on('data', (chunk) => { + if (request.onData(chunk) === false) { + stream.pause() + } + }) + + stream.once('close', () => { + h2State.openStreams -= 1 + // TODO(HTTP/2): unref only if current streams count is 0 + if (h2State.openStreams === 0) { + session.unref() + } + }) + + stream.once('error', function (err) { + if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) { + h2State.streams -= 1 + util.destroy(stream, err) + } + }) + + stream.once('frameError', (type, code) => { + const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) + errorRequest(client, request, err) + + if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) { + h2State.streams -= 1 + util.destroy(stream, err) + } + }) + + // stream.on('aborted', () => { + // // TODO(HTTP/2): Support aborted + // }) + + // stream.on('timeout', () => { + // // TODO(HTTP/2): Support timeout + // }) + + // stream.on('push', headers => { + // // TODO(HTTP/2): Suppor push + // }) + + // stream.on('trailers', headers => { + // // TODO(HTTP/2): Support trailers + // }) + + return true + + function writeBodyH2 () { + /* istanbul ignore else: assertion */ + if (!body) { + request.onRequestSent() + } else if (util.isBuffer(body)) { + assert(contentLength === body.byteLength, 'buffer body must have content length') + stream.cork() + stream.write(body) + stream.uncork() + stream.end() + request.onBodySent(body) + request.onRequestSent() + } else if (util.isBlobLike(body)) { + if (typeof body.stream === 'function') { + writeIterable({ + client, + request, + contentLength, + h2stream: stream, + expectsPayload, + body: body.stream(), + socket: client[kSocket], + header: '' + }) + } else { + writeBlob({ + body, + client, + request, + contentLength, + expectsPayload, + h2stream: stream, + header: '', + socket: client[kSocket] + }) + } + } else if (util.isStream(body)) { + writeStream({ + body, + client, + request, + contentLength, + expectsPayload, + socket: client[kSocket], + h2stream: stream, + header: '' + }) + } else if (util.isIterable(body)) { + writeIterable({ + body, + client, + request, + contentLength, + expectsPayload, + header: '', + h2stream: stream, + socket: client[kSocket] + }) + } else { + assert(false) + } + } +} + +function writeStream ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined') + + if (client[kHTTPConnVersion] === 'h2') { + // For HTTP/2, is enough to pipe the stream + const pipe = pipeline( + body, + h2stream, + (err) => { + if (err) { + util.destroy(body, err) + util.destroy(h2stream, err) + } else { + request.onRequestSent() + } + } + ) + + pipe.on('data', onPipeData) + pipe.once('end', () => { + pipe.removeListener('data', onPipeData) + util.destroy(pipe) + }) + + function onPipeData (chunk) { + request.onBodySent(chunk) + } + + return + } + + let finished = false + + const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header }) + + const onData = function (chunk) { + if (finished) { + return + } + + try { + if (!writer.write(chunk) && this.pause) { + this.pause() + } + } catch (err) { + util.destroy(this, err) + } + } + const onDrain = function () { + if (finished) { + return + } + + if (body.resume) { + body.resume() + } + } + const onAbort = function () { + if (finished) { + return + } + const err = new RequestAbortedError() + queueMicrotask(() => onFinished(err)) + } + const onFinished = function (err) { + if (finished) { + return + } + + finished = true + + assert(socket.destroyed || (socket[kWriting] && client[kRunning] <= 1)) + + socket + .off('drain', onDrain) + .off('error', onFinished) + + body + .removeListener('data', onData) + .removeListener('end', onFinished) + .removeListener('error', onFinished) + .removeListener('close', onAbort) + + if (!err) { + try { + writer.end() + } catch (er) { + err = er + } + } + + writer.destroy(err) + + if (err && (err.code !== 'UND_ERR_INFO' || err.message !== 'reset')) { + util.destroy(body, err) + } else { + util.destroy(body) + } + } + + body + .on('data', onData) + .on('end', onFinished) + .on('error', onFinished) + .on('close', onAbort) + + if (body.resume) { + body.resume() + } + + socket + .on('drain', onDrain) + .on('error', onFinished) +} + +async function writeBlob ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength === body.size, 'blob body must have content length') + + const isH2 = client[kHTTPConnVersion] === 'h2' + try { + if (contentLength != null && contentLength !== body.size) { + throw new RequestContentLengthMismatchError() + } + + const buffer = Buffer.from(await body.arrayBuffer()) + + if (isH2) { + h2stream.cork() + h2stream.write(buffer) + h2stream.uncork() + } else { + socket.cork() + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') + socket.write(buffer) + socket.uncork() + } + + request.onBodySent(buffer) + request.onRequestSent() + + if (!expectsPayload) { + socket[kReset] = true + } + + resume(client) + } catch (err) { + util.destroy(isH2 ? h2stream : socket, err) + } +} + +async function writeIterable ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined') + + let callback = null + function onDrain () { + if (callback) { + const cb = callback + callback = null + cb() + } + } + + const waitForDrain = () => new Promise((resolve, reject) => { + assert(callback === null) + + if (socket[kError]) { + reject(socket[kError]) + } else { + callback = resolve + } + }) + + if (client[kHTTPConnVersion] === 'h2') { + h2stream + .on('close', onDrain) + .on('drain', onDrain) + + try { + // It's up to the user to somehow abort the async iterable. + for await (const chunk of body) { + if (socket[kError]) { + throw socket[kError] + } + + const res = h2stream.write(chunk) + request.onBodySent(chunk) + if (!res) { + await waitForDrain() + } + } + } catch (err) { + h2stream.destroy(err) + } finally { + request.onRequestSent() + h2stream.end() + h2stream + .off('close', onDrain) + .off('drain', onDrain) + } + + return + } + + socket + .on('close', onDrain) + .on('drain', onDrain) + + const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header }) + try { + // It's up to the user to somehow abort the async iterable. + for await (const chunk of body) { + if (socket[kError]) { + throw socket[kError] + } + + if (!writer.write(chunk)) { + await waitForDrain() + } + } + + writer.end() + } catch (err) { + writer.destroy(err) + } finally { + socket + .off('close', onDrain) + .off('drain', onDrain) + } +} + +class AsyncWriter { + constructor ({ socket, request, contentLength, client, expectsPayload, header }) { + this.socket = socket + this.request = request + this.contentLength = contentLength + this.client = client + this.bytesWritten = 0 + this.expectsPayload = expectsPayload + this.header = header + + socket[kWriting] = true + } + + write (chunk) { + const { socket, request, contentLength, client, bytesWritten, expectsPayload, header } = this + + if (socket[kError]) { + throw socket[kError] + } + + if (socket.destroyed) { + return false + } + + const len = Buffer.byteLength(chunk) + if (!len) { + return true + } + + // We should defer writing chunks. + if (contentLength !== null && bytesWritten + len > contentLength) { + if (client[kStrictContentLength]) { + throw new RequestContentLengthMismatchError() + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + socket.cork() + + if (bytesWritten === 0) { + if (!expectsPayload) { + socket[kReset] = true + } + + if (contentLength === null) { + socket.write(`${header}transfer-encoding: chunked\r\n`, 'latin1') + } else { + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') + } + } + + if (contentLength === null) { + socket.write(`\r\n${len.toString(16)}\r\n`, 'latin1') + } + + this.bytesWritten += len + + const ret = socket.write(chunk) + + socket.uncork() + + request.onBodySent(chunk) + + if (!ret) { + if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { + // istanbul ignore else: only for jest + if (socket[kParser].timeout.refresh) { + socket[kParser].timeout.refresh() + } + } + } + + return ret + } + + end () { + const { socket, contentLength, client, bytesWritten, expectsPayload, header, request } = this + request.onRequestSent() + + socket[kWriting] = false + + if (socket[kError]) { + throw socket[kError] + } + + if (socket.destroyed) { + return + } + + if (bytesWritten === 0) { + if (expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD send a Content-Length in a request message when + // no Transfer-Encoding is sent and the request method defines a meaning + // for an enclosed payload body. + + socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') + } else { + socket.write(`${header}\r\n`, 'latin1') + } + } else if (contentLength === null) { + socket.write('\r\n0\r\n\r\n', 'latin1') + } + + if (contentLength !== null && bytesWritten !== contentLength) { + if (client[kStrictContentLength]) { + throw new RequestContentLengthMismatchError() + } else { + process.emitWarning(new RequestContentLengthMismatchError()) + } + } + + if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { + // istanbul ignore else: only for jest + if (socket[kParser].timeout.refresh) { + socket[kParser].timeout.refresh() + } + } + + resume(client) + } + + destroy (err) { + const { socket, client } = this + + socket[kWriting] = false + + if (err) { + assert(client[kRunning] <= 1, 'pipeline should only contain this request') + util.destroy(socket, err) + } + } +} + +function errorRequest (client, request, err) { + try { + request.onError(err) + assert(request.aborted) + } catch (err) { + client.emit('error', err) + } +} + +module.exports = Client + + +/***/ }), + +/***/ 6436: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +/* istanbul ignore file: only for Node 12 */ + +const { kConnected, kSize } = __nccwpck_require__(2785) + +class CompatWeakRef { + constructor (value) { + this.value = value + } + + deref () { + return this.value[kConnected] === 0 && this.value[kSize] === 0 + ? undefined + : this.value + } +} + +class CompatFinalizer { + constructor (finalizer) { + this.finalizer = finalizer + } + + register (dispatcher, key) { + if (dispatcher.on) { + dispatcher.on('disconnect', () => { + if (dispatcher[kConnected] === 0 && dispatcher[kSize] === 0) { + this.finalizer(key) + } + }) + } + } +} + +module.exports = function () { + // FIXME: remove workaround when the Node bug is fixed + // https://github.com/nodejs/node/issues/49344#issuecomment-1741776308 + if (process.env.NODE_V8_COVERAGE) { + return { + WeakRef: CompatWeakRef, + FinalizationRegistry: CompatFinalizer + } + } + return { + WeakRef: global.WeakRef || CompatWeakRef, + FinalizationRegistry: global.FinalizationRegistry || CompatFinalizer + } +} + + +/***/ }), + +/***/ 663: +/***/ ((module) => { + +"use strict"; + + +// https://wicg.github.io/cookie-store/#cookie-maximum-attribute-value-size +const maxAttributeValueSize = 1024 + +// https://wicg.github.io/cookie-store/#cookie-maximum-name-value-pair-size +const maxNameValuePairSize = 4096 + +module.exports = { + maxAttributeValueSize, + maxNameValuePairSize +} + + +/***/ }), + +/***/ 1724: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { parseSetCookie } = __nccwpck_require__(4408) +const { stringify, getHeadersList } = __nccwpck_require__(3121) +const { webidl } = __nccwpck_require__(1744) +const { Headers } = __nccwpck_require__(554) + +/** + * @typedef {Object} Cookie + * @property {string} name + * @property {string} value + * @property {Date|number|undefined} expires + * @property {number|undefined} maxAge + * @property {string|undefined} domain + * @property {string|undefined} path + * @property {boolean|undefined} secure + * @property {boolean|undefined} httpOnly + * @property {'Strict'|'Lax'|'None'} sameSite + * @property {string[]} unparsed + */ + +/** + * @param {Headers} headers + * @returns {Record} + */ +function getCookies (headers) { + webidl.argumentLengthCheck(arguments, 1, { header: 'getCookies' }) + + webidl.brandCheck(headers, Headers, { strict: false }) + + const cookie = headers.get('cookie') + const out = {} + + if (!cookie) { + return out + } + + for (const piece of cookie.split(';')) { + const [name, ...value] = piece.split('=') + + out[name.trim()] = value.join('=') + } + + return out +} + +/** + * @param {Headers} headers + * @param {string} name + * @param {{ path?: string, domain?: string }|undefined} attributes + * @returns {void} + */ +function deleteCookie (headers, name, attributes) { + webidl.argumentLengthCheck(arguments, 2, { header: 'deleteCookie' }) + + webidl.brandCheck(headers, Headers, { strict: false }) + + name = webidl.converters.DOMString(name) + attributes = webidl.converters.DeleteCookieAttributes(attributes) + + // Matches behavior of + // https://github.com/denoland/deno_std/blob/63827b16330b82489a04614027c33b7904e08be5/http/cookie.ts#L278 + setCookie(headers, { + name, + value: '', + expires: new Date(0), + ...attributes + }) +} + +/** + * @param {Headers} headers + * @returns {Cookie[]} + */ +function getSetCookies (headers) { + webidl.argumentLengthCheck(arguments, 1, { header: 'getSetCookies' }) + + webidl.brandCheck(headers, Headers, { strict: false }) + + const cookies = getHeadersList(headers).cookies + + if (!cookies) { + return [] + } + + // In older versions of undici, cookies is a list of name:value. + return cookies.map((pair) => parseSetCookie(Array.isArray(pair) ? pair[1] : pair)) +} + +/** + * @param {Headers} headers + * @param {Cookie} cookie + * @returns {void} + */ +function setCookie (headers, cookie) { + webidl.argumentLengthCheck(arguments, 2, { header: 'setCookie' }) + + webidl.brandCheck(headers, Headers, { strict: false }) + + cookie = webidl.converters.Cookie(cookie) + + const str = stringify(cookie) + + if (str) { + headers.append('Set-Cookie', stringify(cookie)) + } +} + +webidl.converters.DeleteCookieAttributes = webidl.dictionaryConverter([ + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'path', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'domain', + defaultValue: null + } +]) + +webidl.converters.Cookie = webidl.dictionaryConverter([ + { + converter: webidl.converters.DOMString, + key: 'name' + }, + { + converter: webidl.converters.DOMString, + key: 'value' + }, + { + converter: webidl.nullableConverter((value) => { + if (typeof value === 'number') { + return webidl.converters['unsigned long long'](value) + } + + return new Date(value) + }), + key: 'expires', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters['long long']), + key: 'maxAge', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'domain', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'path', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.boolean), + key: 'secure', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.boolean), + key: 'httpOnly', + defaultValue: null + }, + { + converter: webidl.converters.USVString, + key: 'sameSite', + allowedValues: ['Strict', 'Lax', 'None'] + }, + { + converter: webidl.sequenceConverter(webidl.converters.DOMString), + key: 'unparsed', + defaultValue: [] + } +]) + +module.exports = { + getCookies, + deleteCookie, + getSetCookies, + setCookie +} + + +/***/ }), + +/***/ 4408: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { maxNameValuePairSize, maxAttributeValueSize } = __nccwpck_require__(663) +const { isCTLExcludingHtab } = __nccwpck_require__(3121) +const { collectASequenceOfCodePointsFast } = __nccwpck_require__(685) +const assert = __nccwpck_require__(9491) + +/** + * @description Parses the field-value attributes of a set-cookie header string. + * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4 + * @param {string} header + * @returns if the header is invalid, null will be returned + */ +function parseSetCookie (header) { + // 1. If the set-cookie-string contains a %x00-08 / %x0A-1F / %x7F + // character (CTL characters excluding HTAB): Abort these steps and + // ignore the set-cookie-string entirely. + if (isCTLExcludingHtab(header)) { + return null + } + + let nameValuePair = '' + let unparsedAttributes = '' + let name = '' + let value = '' + + // 2. If the set-cookie-string contains a %x3B (";") character: + if (header.includes(';')) { + // 1. The name-value-pair string consists of the characters up to, + // but not including, the first %x3B (";"), and the unparsed- + // attributes consist of the remainder of the set-cookie-string + // (including the %x3B (";") in question). + const position = { position: 0 } + + nameValuePair = collectASequenceOfCodePointsFast(';', header, position) + unparsedAttributes = header.slice(position.position) + } else { + // Otherwise: + + // 1. The name-value-pair string consists of all the characters + // contained in the set-cookie-string, and the unparsed- + // attributes is the empty string. + nameValuePair = header + } + + // 3. If the name-value-pair string lacks a %x3D ("=") character, then + // the name string is empty, and the value string is the value of + // name-value-pair. + if (!nameValuePair.includes('=')) { + value = nameValuePair + } else { + // Otherwise, the name string consists of the characters up to, but + // not including, the first %x3D ("=") character, and the (possibly + // empty) value string consists of the characters after the first + // %x3D ("=") character. + const position = { position: 0 } + name = collectASequenceOfCodePointsFast( + '=', + nameValuePair, + position + ) + value = nameValuePair.slice(position.position + 1) + } + + // 4. Remove any leading or trailing WSP characters from the name + // string and the value string. + name = name.trim() + value = value.trim() + + // 5. If the sum of the lengths of the name string and the value string + // is more than 4096 octets, abort these steps and ignore the set- + // cookie-string entirely. + if (name.length + value.length > maxNameValuePairSize) { + return null + } + + // 6. The cookie-name is the name string, and the cookie-value is the + // value string. + return { + name, value, ...parseUnparsedAttributes(unparsedAttributes) + } +} + +/** + * Parses the remaining attributes of a set-cookie header + * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4 + * @param {string} unparsedAttributes + * @param {[Object.]={}} cookieAttributeList + */ +function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) { + // 1. If the unparsed-attributes string is empty, skip the rest of + // these steps. + if (unparsedAttributes.length === 0) { + return cookieAttributeList + } + + // 2. Discard the first character of the unparsed-attributes (which + // will be a %x3B (";") character). + assert(unparsedAttributes[0] === ';') + unparsedAttributes = unparsedAttributes.slice(1) + + let cookieAv = '' + + // 3. If the remaining unparsed-attributes contains a %x3B (";") + // character: + if (unparsedAttributes.includes(';')) { + // 1. Consume the characters of the unparsed-attributes up to, but + // not including, the first %x3B (";") character. + cookieAv = collectASequenceOfCodePointsFast( + ';', + unparsedAttributes, + { position: 0 } + ) + unparsedAttributes = unparsedAttributes.slice(cookieAv.length) + } else { + // Otherwise: + + // 1. Consume the remainder of the unparsed-attributes. + cookieAv = unparsedAttributes + unparsedAttributes = '' + } + + // Let the cookie-av string be the characters consumed in this step. + + let attributeName = '' + let attributeValue = '' + + // 4. If the cookie-av string contains a %x3D ("=") character: + if (cookieAv.includes('=')) { + // 1. The (possibly empty) attribute-name string consists of the + // characters up to, but not including, the first %x3D ("=") + // character, and the (possibly empty) attribute-value string + // consists of the characters after the first %x3D ("=") + // character. + const position = { position: 0 } + + attributeName = collectASequenceOfCodePointsFast( + '=', + cookieAv, + position + ) + attributeValue = cookieAv.slice(position.position + 1) + } else { + // Otherwise: + + // 1. The attribute-name string consists of the entire cookie-av + // string, and the attribute-value string is empty. + attributeName = cookieAv + } + + // 5. Remove any leading or trailing WSP characters from the attribute- + // name string and the attribute-value string. + attributeName = attributeName.trim() + attributeValue = attributeValue.trim() + + // 6. If the attribute-value is longer than 1024 octets, ignore the + // cookie-av string and return to Step 1 of this algorithm. + if (attributeValue.length > maxAttributeValueSize) { + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) + } + + // 7. Process the attribute-name and attribute-value according to the + // requirements in the following subsections. (Notice that + // attributes with unrecognized attribute-names are ignored.) + const attributeNameLowercase = attributeName.toLowerCase() + + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.1 + // If the attribute-name case-insensitively matches the string + // "Expires", the user agent MUST process the cookie-av as follows. + if (attributeNameLowercase === 'expires') { + // 1. Let the expiry-time be the result of parsing the attribute-value + // as cookie-date (see Section 5.1.1). + const expiryTime = new Date(attributeValue) + + // 2. If the attribute-value failed to parse as a cookie date, ignore + // the cookie-av. + + cookieAttributeList.expires = expiryTime + } else if (attributeNameLowercase === 'max-age') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.2 + // If the attribute-name case-insensitively matches the string "Max- + // Age", the user agent MUST process the cookie-av as follows. + + // 1. If the first character of the attribute-value is not a DIGIT or a + // "-" character, ignore the cookie-av. + const charCode = attributeValue.charCodeAt(0) + + if ((charCode < 48 || charCode > 57) && attributeValue[0] !== '-') { + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) + } + + // 2. If the remainder of attribute-value contains a non-DIGIT + // character, ignore the cookie-av. + if (!/^\d+$/.test(attributeValue)) { + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) + } + + // 3. Let delta-seconds be the attribute-value converted to an integer. + const deltaSeconds = Number(attributeValue) + + // 4. Let cookie-age-limit be the maximum age of the cookie (which + // SHOULD be 400 days or less, see Section 4.1.2.2). + + // 5. Set delta-seconds to the smaller of its present value and cookie- + // age-limit. + // deltaSeconds = Math.min(deltaSeconds * 1000, maxExpiresMs) + + // 6. If delta-seconds is less than or equal to zero (0), let expiry- + // time be the earliest representable date and time. Otherwise, let + // the expiry-time be the current date and time plus delta-seconds + // seconds. + // const expiryTime = deltaSeconds <= 0 ? Date.now() : Date.now() + deltaSeconds + + // 7. Append an attribute to the cookie-attribute-list with an + // attribute-name of Max-Age and an attribute-value of expiry-time. + cookieAttributeList.maxAge = deltaSeconds + } else if (attributeNameLowercase === 'domain') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.3 + // If the attribute-name case-insensitively matches the string "Domain", + // the user agent MUST process the cookie-av as follows. + + // 1. Let cookie-domain be the attribute-value. + let cookieDomain = attributeValue + + // 2. If cookie-domain starts with %x2E ("."), let cookie-domain be + // cookie-domain without its leading %x2E ("."). + if (cookieDomain[0] === '.') { + cookieDomain = cookieDomain.slice(1) + } + + // 3. Convert the cookie-domain to lower case. + cookieDomain = cookieDomain.toLowerCase() + + // 4. Append an attribute to the cookie-attribute-list with an + // attribute-name of Domain and an attribute-value of cookie-domain. + cookieAttributeList.domain = cookieDomain + } else if (attributeNameLowercase === 'path') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.4 + // If the attribute-name case-insensitively matches the string "Path", + // the user agent MUST process the cookie-av as follows. + + // 1. If the attribute-value is empty or if the first character of the + // attribute-value is not %x2F ("/"): + let cookiePath = '' + if (attributeValue.length === 0 || attributeValue[0] !== '/') { + // 1. Let cookie-path be the default-path. + cookiePath = '/' + } else { + // Otherwise: + + // 1. Let cookie-path be the attribute-value. + cookiePath = attributeValue + } + + // 2. Append an attribute to the cookie-attribute-list with an + // attribute-name of Path and an attribute-value of cookie-path. + cookieAttributeList.path = cookiePath + } else if (attributeNameLowercase === 'secure') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.5 + // If the attribute-name case-insensitively matches the string "Secure", + // the user agent MUST append an attribute to the cookie-attribute-list + // with an attribute-name of Secure and an empty attribute-value. + + cookieAttributeList.secure = true + } else if (attributeNameLowercase === 'httponly') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.6 + // If the attribute-name case-insensitively matches the string + // "HttpOnly", the user agent MUST append an attribute to the cookie- + // attribute-list with an attribute-name of HttpOnly and an empty + // attribute-value. + + cookieAttributeList.httpOnly = true + } else if (attributeNameLowercase === 'samesite') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7 + // If the attribute-name case-insensitively matches the string + // "SameSite", the user agent MUST process the cookie-av as follows: + + // 1. Let enforcement be "Default". + let enforcement = 'Default' + + const attributeValueLowercase = attributeValue.toLowerCase() + // 2. If cookie-av's attribute-value is a case-insensitive match for + // "None", set enforcement to "None". + if (attributeValueLowercase.includes('none')) { + enforcement = 'None' + } + + // 3. If cookie-av's attribute-value is a case-insensitive match for + // "Strict", set enforcement to "Strict". + if (attributeValueLowercase.includes('strict')) { + enforcement = 'Strict' + } + + // 4. If cookie-av's attribute-value is a case-insensitive match for + // "Lax", set enforcement to "Lax". + if (attributeValueLowercase.includes('lax')) { + enforcement = 'Lax' + } + + // 5. Append an attribute to the cookie-attribute-list with an + // attribute-name of "SameSite" and an attribute-value of + // enforcement. + cookieAttributeList.sameSite = enforcement + } else { + cookieAttributeList.unparsed ??= [] + + cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`) + } + + // 8. Return to Step 1 of this algorithm. + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) +} + +module.exports = { + parseSetCookie, + parseUnparsedAttributes +} + + +/***/ }), + +/***/ 3121: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const assert = __nccwpck_require__(9491) +const { kHeadersList } = __nccwpck_require__(2785) + +function isCTLExcludingHtab (value) { + if (value.length === 0) { + return false + } + + for (const char of value) { + const code = char.charCodeAt(0) + + if ( + (code >= 0x00 || code <= 0x08) || + (code >= 0x0A || code <= 0x1F) || + code === 0x7F + ) { + return false + } + } +} + +/** + CHAR = + token = 1* + separators = "(" | ")" | "<" | ">" | "@" + | "," | ";" | ":" | "\" | <"> + | "/" | "[" | "]" | "?" | "=" + | "{" | "}" | SP | HT + * @param {string} name + */ +function validateCookieName (name) { + for (const char of name) { + const code = char.charCodeAt(0) + + if ( + (code <= 0x20 || code > 0x7F) || + char === '(' || + char === ')' || + char === '>' || + char === '<' || + char === '@' || + char === ',' || + char === ';' || + char === ':' || + char === '\\' || + char === '"' || + char === '/' || + char === '[' || + char === ']' || + char === '?' || + char === '=' || + char === '{' || + char === '}' + ) { + throw new Error('Invalid cookie name') + } + } +} + +/** + cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) + cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E + ; US-ASCII characters excluding CTLs, + ; whitespace DQUOTE, comma, semicolon, + ; and backslash + * @param {string} value + */ +function validateCookieValue (value) { + for (const char of value) { + const code = char.charCodeAt(0) + + if ( + code < 0x21 || // exclude CTLs (0-31) + code === 0x22 || + code === 0x2C || + code === 0x3B || + code === 0x5C || + code > 0x7E // non-ascii + ) { + throw new Error('Invalid header value') + } + } +} + +/** + * path-value = + * @param {string} path + */ +function validateCookiePath (path) { + for (const char of path) { + const code = char.charCodeAt(0) + + if (code < 0x21 || char === ';') { + throw new Error('Invalid cookie path') + } + } +} + +/** + * I have no idea why these values aren't allowed to be honest, + * but Deno tests these. - Khafra + * @param {string} domain + */ +function validateCookieDomain (domain) { + if ( + domain.startsWith('-') || + domain.endsWith('.') || + domain.endsWith('-') + ) { + throw new Error('Invalid cookie domain') + } +} + +/** + * @see https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1 + * @param {number|Date} date + IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT + ; fixed length/zone/capitalization subset of the format + ; see Section 3.3 of [RFC5322] + + day-name = %x4D.6F.6E ; "Mon", case-sensitive + / %x54.75.65 ; "Tue", case-sensitive + / %x57.65.64 ; "Wed", case-sensitive + / %x54.68.75 ; "Thu", case-sensitive + / %x46.72.69 ; "Fri", case-sensitive + / %x53.61.74 ; "Sat", case-sensitive + / %x53.75.6E ; "Sun", case-sensitive + date1 = day SP month SP year + ; e.g., 02 Jun 1982 + + day = 2DIGIT + month = %x4A.61.6E ; "Jan", case-sensitive + / %x46.65.62 ; "Feb", case-sensitive + / %x4D.61.72 ; "Mar", case-sensitive + / %x41.70.72 ; "Apr", case-sensitive + / %x4D.61.79 ; "May", case-sensitive + / %x4A.75.6E ; "Jun", case-sensitive + / %x4A.75.6C ; "Jul", case-sensitive + / %x41.75.67 ; "Aug", case-sensitive + / %x53.65.70 ; "Sep", case-sensitive + / %x4F.63.74 ; "Oct", case-sensitive + / %x4E.6F.76 ; "Nov", case-sensitive + / %x44.65.63 ; "Dec", case-sensitive + year = 4DIGIT + + GMT = %x47.4D.54 ; "GMT", case-sensitive + + time-of-day = hour ":" minute ":" second + ; 00:00:00 - 23:59:60 (leap second) + + hour = 2DIGIT + minute = 2DIGIT + second = 2DIGIT + */ +function toIMFDate (date) { + if (typeof date === 'number') { + date = new Date(date) + } + + const days = [ + 'Sun', 'Mon', 'Tue', 'Wed', + 'Thu', 'Fri', 'Sat' + ] + + const months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ] + + const dayName = days[date.getUTCDay()] + const day = date.getUTCDate().toString().padStart(2, '0') + const month = months[date.getUTCMonth()] + const year = date.getUTCFullYear() + const hour = date.getUTCHours().toString().padStart(2, '0') + const minute = date.getUTCMinutes().toString().padStart(2, '0') + const second = date.getUTCSeconds().toString().padStart(2, '0') + + return `${dayName}, ${day} ${month} ${year} ${hour}:${minute}:${second} GMT` +} + +/** + max-age-av = "Max-Age=" non-zero-digit *DIGIT + ; In practice, both expires-av and max-age-av + ; are limited to dates representable by the + ; user agent. + * @param {number} maxAge + */ +function validateCookieMaxAge (maxAge) { + if (maxAge < 0) { + throw new Error('Invalid cookie max-age') + } +} + +/** + * @see https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1 + * @param {import('./index').Cookie} cookie + */ +function stringify (cookie) { + if (cookie.name.length === 0) { + return null + } + + validateCookieName(cookie.name) + validateCookieValue(cookie.value) + + const out = [`${cookie.name}=${cookie.value}`] + + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.1 + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.2 + if (cookie.name.startsWith('__Secure-')) { + cookie.secure = true + } + + if (cookie.name.startsWith('__Host-')) { + cookie.secure = true + cookie.domain = null + cookie.path = '/' + } + + if (cookie.secure) { + out.push('Secure') + } + + if (cookie.httpOnly) { + out.push('HttpOnly') + } + + if (typeof cookie.maxAge === 'number') { + validateCookieMaxAge(cookie.maxAge) + out.push(`Max-Age=${cookie.maxAge}`) + } + + if (cookie.domain) { + validateCookieDomain(cookie.domain) + out.push(`Domain=${cookie.domain}`) + } + + if (cookie.path) { + validateCookiePath(cookie.path) + out.push(`Path=${cookie.path}`) + } + + if (cookie.expires && cookie.expires.toString() !== 'Invalid Date') { + out.push(`Expires=${toIMFDate(cookie.expires)}`) + } + + if (cookie.sameSite) { + out.push(`SameSite=${cookie.sameSite}`) + } + + for (const part of cookie.unparsed) { + if (!part.includes('=')) { + throw new Error('Invalid unparsed') + } + + const [key, ...value] = part.split('=') + + out.push(`${key.trim()}=${value.join('=')}`) + } + + return out.join('; ') +} + +let kHeadersListNode + +function getHeadersList (headers) { + if (headers[kHeadersList]) { + return headers[kHeadersList] + } + + if (!kHeadersListNode) { + kHeadersListNode = Object.getOwnPropertySymbols(headers).find( + (symbol) => symbol.description === 'headers list' + ) + + assert(kHeadersListNode, 'Headers cannot be parsed') + } + + const headersList = headers[kHeadersListNode] + assert(headersList) + + return headersList +} + +module.exports = { + isCTLExcludingHtab, + stringify, + getHeadersList +} + + +/***/ }), + +/***/ 2067: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const net = __nccwpck_require__(1808) +const assert = __nccwpck_require__(9491) +const util = __nccwpck_require__(3983) +const { InvalidArgumentError, ConnectTimeoutError } = __nccwpck_require__(8045) + +let tls // include tls conditionally since it is not always available + +// TODO: session re-use does not wait for the first +// connection to resolve the session and might therefore +// resolve the same servername multiple times even when +// re-use is enabled. + +let SessionCache +// FIXME: remove workaround when the Node bug is fixed +// https://github.com/nodejs/node/issues/49344#issuecomment-1741776308 +if (global.FinalizationRegistry && !process.env.NODE_V8_COVERAGE) { + SessionCache = class WeakSessionCache { + constructor (maxCachedSessions) { + this._maxCachedSessions = maxCachedSessions + this._sessionCache = new Map() + this._sessionRegistry = new global.FinalizationRegistry((key) => { + if (this._sessionCache.size < this._maxCachedSessions) { + return + } + + const ref = this._sessionCache.get(key) + if (ref !== undefined && ref.deref() === undefined) { + this._sessionCache.delete(key) + } + }) + } + + get (sessionKey) { + const ref = this._sessionCache.get(sessionKey) + return ref ? ref.deref() : null + } + + set (sessionKey, session) { + if (this._maxCachedSessions === 0) { + return + } + + this._sessionCache.set(sessionKey, new WeakRef(session)) + this._sessionRegistry.register(session, sessionKey) + } + } +} else { + SessionCache = class SimpleSessionCache { + constructor (maxCachedSessions) { + this._maxCachedSessions = maxCachedSessions + this._sessionCache = new Map() + } + + get (sessionKey) { + return this._sessionCache.get(sessionKey) + } + + set (sessionKey, session) { + if (this._maxCachedSessions === 0) { + return + } + + if (this._sessionCache.size >= this._maxCachedSessions) { + // remove the oldest session + const { value: oldestKey } = this._sessionCache.keys().next() + this._sessionCache.delete(oldestKey) + } + + this._sessionCache.set(sessionKey, session) + } + } +} + +function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, ...opts }) { + if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) { + throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero') + } + + const options = { path: socketPath, ...opts } + const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions) + timeout = timeout == null ? 10e3 : timeout + allowH2 = allowH2 != null ? allowH2 : false + return function connect ({ hostname, host, protocol, port, servername, localAddress, httpSocket }, callback) { + let socket + if (protocol === 'https:') { + if (!tls) { + tls = __nccwpck_require__(4404) + } + servername = servername || options.servername || util.getServerName(host) || null + + const sessionKey = servername || hostname + const session = sessionCache.get(sessionKey) || null + + assert(sessionKey) + + socket = tls.connect({ + highWaterMark: 16384, // TLS in node can't have bigger HWM anyway... + ...options, + servername, + session, + localAddress, + // TODO(HTTP/2): Add support for h2c + ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'], + socket: httpSocket, // upgrade socket connection + port: port || 443, + host: hostname + }) + + socket + .on('session', function (session) { + // TODO (fix): Can a session become invalid once established? Don't think so? + sessionCache.set(sessionKey, session) + }) + } else { + assert(!httpSocket, 'httpSocket can only be sent on TLS update') + socket = net.connect({ + highWaterMark: 64 * 1024, // Same as nodejs fs streams. + ...options, + localAddress, + port: port || 80, + host: hostname + }) + } + + // Set TCP keep alive options on the socket here instead of in connect() for the case of assigning the socket + if (options.keepAlive == null || options.keepAlive) { + const keepAliveInitialDelay = options.keepAliveInitialDelay === undefined ? 60e3 : options.keepAliveInitialDelay + socket.setKeepAlive(true, keepAliveInitialDelay) + } + + const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout) + + socket + .setNoDelay(true) + .once(protocol === 'https:' ? 'secureConnect' : 'connect', function () { + cancelTimeout() + + if (callback) { + const cb = callback + callback = null + cb(null, this) + } + }) + .on('error', function (err) { + cancelTimeout() + + if (callback) { + const cb = callback + callback = null + cb(err) + } + }) + + return socket + } +} + +function setupTimeout (onConnectTimeout, timeout) { + if (!timeout) { + return () => {} + } + + let s1 = null + let s2 = null + const timeoutId = setTimeout(() => { + // setImmediate is added to make sure that we priotorise socket error events over timeouts + s1 = setImmediate(() => { + if (process.platform === 'win32') { + // Windows needs an extra setImmediate probably due to implementation differences in the socket logic + s2 = setImmediate(() => onConnectTimeout()) + } else { + onConnectTimeout() + } + }) + }, timeout) + return () => { + clearTimeout(timeoutId) + clearImmediate(s1) + clearImmediate(s2) + } +} + +function onConnectTimeout (socket) { + util.destroy(socket, new ConnectTimeoutError()) +} + +module.exports = buildConnector + + +/***/ }), + +/***/ 4462: +/***/ ((module) => { + +"use strict"; + + +/** @type {Record} */ +const headerNameLowerCasedRecord = {} + +// https://developer.mozilla.org/docs/Web/HTTP/Headers +const wellknownHeaderNames = [ + 'Accept', + 'Accept-Encoding', + 'Accept-Language', + 'Accept-Ranges', + 'Access-Control-Allow-Credentials', + 'Access-Control-Allow-Headers', + 'Access-Control-Allow-Methods', + 'Access-Control-Allow-Origin', + 'Access-Control-Expose-Headers', + 'Access-Control-Max-Age', + 'Access-Control-Request-Headers', + 'Access-Control-Request-Method', + 'Age', + 'Allow', + 'Alt-Svc', + 'Alt-Used', + 'Authorization', + 'Cache-Control', + 'Clear-Site-Data', + 'Connection', + 'Content-Disposition', + 'Content-Encoding', + 'Content-Language', + 'Content-Length', + 'Content-Location', + 'Content-Range', + 'Content-Security-Policy', + 'Content-Security-Policy-Report-Only', + 'Content-Type', + 'Cookie', + 'Cross-Origin-Embedder-Policy', + 'Cross-Origin-Opener-Policy', + 'Cross-Origin-Resource-Policy', + 'Date', + 'Device-Memory', + 'Downlink', + 'ECT', + 'ETag', + 'Expect', + 'Expect-CT', + 'Expires', + 'Forwarded', + 'From', + 'Host', + 'If-Match', + 'If-Modified-Since', + 'If-None-Match', + 'If-Range', + 'If-Unmodified-Since', + 'Keep-Alive', + 'Last-Modified', + 'Link', + 'Location', + 'Max-Forwards', + 'Origin', + 'Permissions-Policy', + 'Pragma', + 'Proxy-Authenticate', + 'Proxy-Authorization', + 'RTT', + 'Range', + 'Referer', + 'Referrer-Policy', + 'Refresh', + 'Retry-After', + 'Sec-WebSocket-Accept', + 'Sec-WebSocket-Extensions', + 'Sec-WebSocket-Key', + 'Sec-WebSocket-Protocol', + 'Sec-WebSocket-Version', + 'Server', + 'Server-Timing', + 'Service-Worker-Allowed', + 'Service-Worker-Navigation-Preload', + 'Set-Cookie', + 'SourceMap', + 'Strict-Transport-Security', + 'Supports-Loading-Mode', + 'TE', + 'Timing-Allow-Origin', + 'Trailer', + 'Transfer-Encoding', + 'Upgrade', + 'Upgrade-Insecure-Requests', + 'User-Agent', + 'Vary', + 'Via', + 'WWW-Authenticate', + 'X-Content-Type-Options', + 'X-DNS-Prefetch-Control', + 'X-Frame-Options', + 'X-Permitted-Cross-Domain-Policies', + 'X-Powered-By', + 'X-Requested-With', + 'X-XSS-Protection' +] + +for (let i = 0; i < wellknownHeaderNames.length; ++i) { + const key = wellknownHeaderNames[i] + const lowerCasedKey = key.toLowerCase() + headerNameLowerCasedRecord[key] = headerNameLowerCasedRecord[lowerCasedKey] = + lowerCasedKey +} + +// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`. +Object.setPrototypeOf(headerNameLowerCasedRecord, null) + +module.exports = { + wellknownHeaderNames, + headerNameLowerCasedRecord +} + + +/***/ }), + +/***/ 8045: +/***/ ((module) => { + +"use strict"; + + +class UndiciError extends Error { + constructor (message) { + super(message) + this.name = 'UndiciError' + this.code = 'UND_ERR' + } +} + +class ConnectTimeoutError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ConnectTimeoutError) + this.name = 'ConnectTimeoutError' + this.message = message || 'Connect Timeout Error' + this.code = 'UND_ERR_CONNECT_TIMEOUT' + } +} + +class HeadersTimeoutError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, HeadersTimeoutError) + this.name = 'HeadersTimeoutError' + this.message = message || 'Headers Timeout Error' + this.code = 'UND_ERR_HEADERS_TIMEOUT' + } +} + +class HeadersOverflowError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, HeadersOverflowError) + this.name = 'HeadersOverflowError' + this.message = message || 'Headers Overflow Error' + this.code = 'UND_ERR_HEADERS_OVERFLOW' + } +} + +class BodyTimeoutError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, BodyTimeoutError) + this.name = 'BodyTimeoutError' + this.message = message || 'Body Timeout Error' + this.code = 'UND_ERR_BODY_TIMEOUT' + } +} + +class ResponseStatusCodeError extends UndiciError { + constructor (message, statusCode, headers, body) { + super(message) + Error.captureStackTrace(this, ResponseStatusCodeError) + this.name = 'ResponseStatusCodeError' + this.message = message || 'Response Status Code Error' + this.code = 'UND_ERR_RESPONSE_STATUS_CODE' + this.body = body + this.status = statusCode + this.statusCode = statusCode + this.headers = headers + } +} + +class InvalidArgumentError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, InvalidArgumentError) + this.name = 'InvalidArgumentError' + this.message = message || 'Invalid Argument Error' + this.code = 'UND_ERR_INVALID_ARG' + } +} + +class InvalidReturnValueError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, InvalidReturnValueError) + this.name = 'InvalidReturnValueError' + this.message = message || 'Invalid Return Value Error' + this.code = 'UND_ERR_INVALID_RETURN_VALUE' + } +} + +class RequestAbortedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, RequestAbortedError) + this.name = 'AbortError' + this.message = message || 'Request aborted' + this.code = 'UND_ERR_ABORTED' + } +} + +class InformationalError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, InformationalError) + this.name = 'InformationalError' + this.message = message || 'Request information' + this.code = 'UND_ERR_INFO' + } +} + +class RequestContentLengthMismatchError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, RequestContentLengthMismatchError) + this.name = 'RequestContentLengthMismatchError' + this.message = message || 'Request body length does not match content-length header' + this.code = 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' + } +} + +class ResponseContentLengthMismatchError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ResponseContentLengthMismatchError) + this.name = 'ResponseContentLengthMismatchError' + this.message = message || 'Response body length does not match content-length header' + this.code = 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH' + } +} + +class ClientDestroyedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ClientDestroyedError) + this.name = 'ClientDestroyedError' + this.message = message || 'The client is destroyed' + this.code = 'UND_ERR_DESTROYED' + } +} + +class ClientClosedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ClientClosedError) + this.name = 'ClientClosedError' + this.message = message || 'The client is closed' + this.code = 'UND_ERR_CLOSED' + } +} + +class SocketError extends UndiciError { + constructor (message, socket) { + super(message) + Error.captureStackTrace(this, SocketError) + this.name = 'SocketError' + this.message = message || 'Socket error' + this.code = 'UND_ERR_SOCKET' + this.socket = socket + } +} + +class NotSupportedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, NotSupportedError) + this.name = 'NotSupportedError' + this.message = message || 'Not supported error' + this.code = 'UND_ERR_NOT_SUPPORTED' + } +} + +class BalancedPoolMissingUpstreamError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, NotSupportedError) + this.name = 'MissingUpstreamError' + this.message = message || 'No upstream has been added to the BalancedPool' + this.code = 'UND_ERR_BPL_MISSING_UPSTREAM' + } +} + +class HTTPParserError extends Error { + constructor (message, code, data) { + super(message) + Error.captureStackTrace(this, HTTPParserError) + this.name = 'HTTPParserError' + this.code = code ? `HPE_${code}` : undefined + this.data = data ? data.toString() : undefined + } +} + +class ResponseExceededMaxSizeError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ResponseExceededMaxSizeError) + this.name = 'ResponseExceededMaxSizeError' + this.message = message || 'Response content exceeded max size' + this.code = 'UND_ERR_RES_EXCEEDED_MAX_SIZE' + } +} + +class RequestRetryError extends UndiciError { + constructor (message, code, { headers, data }) { + super(message) + Error.captureStackTrace(this, RequestRetryError) + this.name = 'RequestRetryError' + this.message = message || 'Request retry error' + this.code = 'UND_ERR_REQ_RETRY' + this.statusCode = code + this.data = data + this.headers = headers + } +} + +module.exports = { + HTTPParserError, + UndiciError, + HeadersTimeoutError, + HeadersOverflowError, + BodyTimeoutError, + RequestContentLengthMismatchError, + ConnectTimeoutError, + ResponseStatusCodeError, + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError, + ClientDestroyedError, + ClientClosedError, + InformationalError, + SocketError, + NotSupportedError, + ResponseContentLengthMismatchError, + BalancedPoolMissingUpstreamError, + ResponseExceededMaxSizeError, + RequestRetryError +} + + +/***/ }), + +/***/ 2905: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + InvalidArgumentError, + NotSupportedError +} = __nccwpck_require__(8045) +const assert = __nccwpck_require__(9491) +const { kHTTP2BuildRequest, kHTTP2CopyHeaders, kHTTP1BuildRequest } = __nccwpck_require__(2785) +const util = __nccwpck_require__(3983) + +// tokenRegExp and headerCharRegex have been lifted from +// https://github.com/nodejs/node/blob/main/lib/_http_common.js + +/** + * Verifies that the given val is a valid HTTP token + * per the rules defined in RFC 7230 + * See https://tools.ietf.org/html/rfc7230#section-3.2.6 + */ +const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/ + +/** + * Matches if val contains an invalid field-vchar + * field-value = *( field-content / obs-fold ) + * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + * field-vchar = VCHAR / obs-text + */ +const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/ + +// Verifies that a given path is valid does not contain control chars \x00 to \x20 +const invalidPathRegex = /[^\u0021-\u00ff]/ + +const kHandler = Symbol('handler') + +const channels = {} + +let extractBody + +try { + const diagnosticsChannel = __nccwpck_require__(7643) + channels.create = diagnosticsChannel.channel('undici:request:create') + channels.bodySent = diagnosticsChannel.channel('undici:request:bodySent') + channels.headers = diagnosticsChannel.channel('undici:request:headers') + channels.trailers = diagnosticsChannel.channel('undici:request:trailers') + channels.error = diagnosticsChannel.channel('undici:request:error') +} catch { + channels.create = { hasSubscribers: false } + channels.bodySent = { hasSubscribers: false } + channels.headers = { hasSubscribers: false } + channels.trailers = { hasSubscribers: false } + channels.error = { hasSubscribers: false } +} + +class Request { + constructor (origin, { + path, + method, + body, + headers, + query, + idempotent, + blocking, + upgrade, + headersTimeout, + bodyTimeout, + reset, + throwOnError, + expectContinue + }, handler) { + if (typeof path !== 'string') { + throw new InvalidArgumentError('path must be a string') + } else if ( + path[0] !== '/' && + !(path.startsWith('http://') || path.startsWith('https://')) && + method !== 'CONNECT' + ) { + throw new InvalidArgumentError('path must be an absolute URL or start with a slash') + } else if (invalidPathRegex.exec(path) !== null) { + throw new InvalidArgumentError('invalid request path') + } + + if (typeof method !== 'string') { + throw new InvalidArgumentError('method must be a string') + } else if (tokenRegExp.exec(method) === null) { + throw new InvalidArgumentError('invalid request method') + } + + if (upgrade && typeof upgrade !== 'string') { + throw new InvalidArgumentError('upgrade must be a string') + } + + if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) { + throw new InvalidArgumentError('invalid headersTimeout') + } + + if (bodyTimeout != null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) { + throw new InvalidArgumentError('invalid bodyTimeout') + } + + if (reset != null && typeof reset !== 'boolean') { + throw new InvalidArgumentError('invalid reset') + } + + if (expectContinue != null && typeof expectContinue !== 'boolean') { + throw new InvalidArgumentError('invalid expectContinue') + } + + this.headersTimeout = headersTimeout + + this.bodyTimeout = bodyTimeout + + this.throwOnError = throwOnError === true + + this.method = method + + this.abort = null + + if (body == null) { + this.body = null + } else if (util.isStream(body)) { + this.body = body + + const rState = this.body._readableState + if (!rState || !rState.autoDestroy) { + this.endHandler = function autoDestroy () { + util.destroy(this) + } + this.body.on('end', this.endHandler) + } + + this.errorHandler = err => { + if (this.abort) { + this.abort(err) + } else { + this.error = err + } + } + this.body.on('error', this.errorHandler) + } else if (util.isBuffer(body)) { + this.body = body.byteLength ? body : null + } else if (ArrayBuffer.isView(body)) { + this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null + } else if (body instanceof ArrayBuffer) { + this.body = body.byteLength ? Buffer.from(body) : null + } else if (typeof body === 'string') { + this.body = body.length ? Buffer.from(body) : null + } else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) { + this.body = body + } else { + throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable') + } + + this.completed = false + + this.aborted = false + + this.upgrade = upgrade || null + + this.path = query ? util.buildURL(path, query) : path + + this.origin = origin + + this.idempotent = idempotent == null + ? method === 'HEAD' || method === 'GET' + : idempotent + + this.blocking = blocking == null ? false : blocking + + this.reset = reset == null ? null : reset + + this.host = null + + this.contentLength = null + + this.contentType = null + + this.headers = '' + + // Only for H2 + this.expectContinue = expectContinue != null ? expectContinue : false + + if (Array.isArray(headers)) { + if (headers.length % 2 !== 0) { + throw new InvalidArgumentError('headers array must be even') + } + for (let i = 0; i < headers.length; i += 2) { + processHeader(this, headers[i], headers[i + 1]) + } + } else if (headers && typeof headers === 'object') { + const keys = Object.keys(headers) + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + processHeader(this, key, headers[key]) + } + } else if (headers != null) { + throw new InvalidArgumentError('headers must be an object or an array') + } + + if (util.isFormDataLike(this.body)) { + if (util.nodeMajor < 16 || (util.nodeMajor === 16 && util.nodeMinor < 8)) { + throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.8 and newer.') + } + + if (!extractBody) { + extractBody = (__nccwpck_require__(1472).extractBody) + } + + const [bodyStream, contentType] = extractBody(body) + if (this.contentType == null) { + this.contentType = contentType + this.headers += `content-type: ${contentType}\r\n` + } + this.body = bodyStream.stream + this.contentLength = bodyStream.length + } else if (util.isBlobLike(body) && this.contentType == null && body.type) { + this.contentType = body.type + this.headers += `content-type: ${body.type}\r\n` + } + + util.validateHandler(handler, method, upgrade) + + this.servername = util.getServerName(this.host) + + this[kHandler] = handler + + if (channels.create.hasSubscribers) { + channels.create.publish({ request: this }) + } + } + + onBodySent (chunk) { + if (this[kHandler].onBodySent) { + try { + return this[kHandler].onBodySent(chunk) + } catch (err) { + this.abort(err) + } + } + } + + onRequestSent () { + if (channels.bodySent.hasSubscribers) { + channels.bodySent.publish({ request: this }) + } + + if (this[kHandler].onRequestSent) { + try { + return this[kHandler].onRequestSent() + } catch (err) { + this.abort(err) + } + } + } + + onConnect (abort) { + assert(!this.aborted) + assert(!this.completed) + + if (this.error) { + abort(this.error) + } else { + this.abort = abort + return this[kHandler].onConnect(abort) + } + } + + onHeaders (statusCode, headers, resume, statusText) { + assert(!this.aborted) + assert(!this.completed) + + if (channels.headers.hasSubscribers) { + channels.headers.publish({ request: this, response: { statusCode, headers, statusText } }) + } + + try { + return this[kHandler].onHeaders(statusCode, headers, resume, statusText) + } catch (err) { + this.abort(err) + } + } + + onData (chunk) { + assert(!this.aborted) + assert(!this.completed) + + try { + return this[kHandler].onData(chunk) + } catch (err) { + this.abort(err) + return false + } + } + + onUpgrade (statusCode, headers, socket) { + assert(!this.aborted) + assert(!this.completed) + + return this[kHandler].onUpgrade(statusCode, headers, socket) + } + + onComplete (trailers) { + this.onFinally() + + assert(!this.aborted) + + this.completed = true + if (channels.trailers.hasSubscribers) { + channels.trailers.publish({ request: this, trailers }) + } + + try { + return this[kHandler].onComplete(trailers) + } catch (err) { + // TODO (fix): This might be a bad idea? + this.onError(err) + } + } + + onError (error) { + this.onFinally() + + if (channels.error.hasSubscribers) { + channels.error.publish({ request: this, error }) + } + + if (this.aborted) { + return + } + this.aborted = true + + return this[kHandler].onError(error) + } + + onFinally () { + if (this.errorHandler) { + this.body.off('error', this.errorHandler) + this.errorHandler = null + } + + if (this.endHandler) { + this.body.off('end', this.endHandler) + this.endHandler = null + } + } + + // TODO: adjust to support H2 + addHeader (key, value) { + processHeader(this, key, value) + return this + } + + static [kHTTP1BuildRequest] (origin, opts, handler) { + // TODO: Migrate header parsing here, to make Requests + // HTTP agnostic + return new Request(origin, opts, handler) + } + + static [kHTTP2BuildRequest] (origin, opts, handler) { + const headers = opts.headers + opts = { ...opts, headers: null } + + const request = new Request(origin, opts, handler) + + request.headers = {} + + if (Array.isArray(headers)) { + if (headers.length % 2 !== 0) { + throw new InvalidArgumentError('headers array must be even') + } + for (let i = 0; i < headers.length; i += 2) { + processHeader(request, headers[i], headers[i + 1], true) + } + } else if (headers && typeof headers === 'object') { + const keys = Object.keys(headers) + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + processHeader(request, key, headers[key], true) + } + } else if (headers != null) { + throw new InvalidArgumentError('headers must be an object or an array') + } + + return request + } + + static [kHTTP2CopyHeaders] (raw) { + const rawHeaders = raw.split('\r\n') + const headers = {} + + for (const header of rawHeaders) { + const [key, value] = header.split(': ') + + if (value == null || value.length === 0) continue + + if (headers[key]) headers[key] += `,${value}` + else headers[key] = value + } + + return headers + } +} + +function processHeaderValue (key, val, skipAppend) { + if (val && typeof val === 'object') { + throw new InvalidArgumentError(`invalid ${key} header`) + } + + val = val != null ? `${val}` : '' + + if (headerCharRegex.exec(val) !== null) { + throw new InvalidArgumentError(`invalid ${key} header`) + } + + return skipAppend ? val : `${key}: ${val}\r\n` +} + +function processHeader (request, key, val, skipAppend = false) { + if (val && (typeof val === 'object' && !Array.isArray(val))) { + throw new InvalidArgumentError(`invalid ${key} header`) + } else if (val === undefined) { + return + } + + if ( + request.host === null && + key.length === 4 && + key.toLowerCase() === 'host' + ) { + if (headerCharRegex.exec(val) !== null) { + throw new InvalidArgumentError(`invalid ${key} header`) + } + // Consumed by Client + request.host = val + } else if ( + request.contentLength === null && + key.length === 14 && + key.toLowerCase() === 'content-length' + ) { + request.contentLength = parseInt(val, 10) + if (!Number.isFinite(request.contentLength)) { + throw new InvalidArgumentError('invalid content-length header') + } + } else if ( + request.contentType === null && + key.length === 12 && + key.toLowerCase() === 'content-type' + ) { + request.contentType = val + if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend) + else request.headers += processHeaderValue(key, val) + } else if ( + key.length === 17 && + key.toLowerCase() === 'transfer-encoding' + ) { + throw new InvalidArgumentError('invalid transfer-encoding header') + } else if ( + key.length === 10 && + key.toLowerCase() === 'connection' + ) { + const value = typeof val === 'string' ? val.toLowerCase() : null + if (value !== 'close' && value !== 'keep-alive') { + throw new InvalidArgumentError('invalid connection header') + } else if (value === 'close') { + request.reset = true + } + } else if ( + key.length === 10 && + key.toLowerCase() === 'keep-alive' + ) { + throw new InvalidArgumentError('invalid keep-alive header') + } else if ( + key.length === 7 && + key.toLowerCase() === 'upgrade' + ) { + throw new InvalidArgumentError('invalid upgrade header') + } else if ( + key.length === 6 && + key.toLowerCase() === 'expect' + ) { + throw new NotSupportedError('expect header not supported') + } else if (tokenRegExp.exec(key) === null) { + throw new InvalidArgumentError('invalid header key') + } else { + if (Array.isArray(val)) { + for (let i = 0; i < val.length; i++) { + if (skipAppend) { + if (request.headers[key]) request.headers[key] += `,${processHeaderValue(key, val[i], skipAppend)}` + else request.headers[key] = processHeaderValue(key, val[i], skipAppend) + } else { + request.headers += processHeaderValue(key, val[i]) + } + } + } else { + if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend) + else request.headers += processHeaderValue(key, val) + } + } +} + +module.exports = Request + + +/***/ }), + +/***/ 2785: +/***/ ((module) => { + +module.exports = { + kClose: Symbol('close'), + kDestroy: Symbol('destroy'), + kDispatch: Symbol('dispatch'), + kUrl: Symbol('url'), + kWriting: Symbol('writing'), + kResuming: Symbol('resuming'), + kQueue: Symbol('queue'), + kConnect: Symbol('connect'), + kConnecting: Symbol('connecting'), + kHeadersList: Symbol('headers list'), + kKeepAliveDefaultTimeout: Symbol('default keep alive timeout'), + kKeepAliveMaxTimeout: Symbol('max keep alive timeout'), + kKeepAliveTimeoutThreshold: Symbol('keep alive timeout threshold'), + kKeepAliveTimeoutValue: Symbol('keep alive timeout'), + kKeepAlive: Symbol('keep alive'), + kHeadersTimeout: Symbol('headers timeout'), + kBodyTimeout: Symbol('body timeout'), + kServerName: Symbol('server name'), + kLocalAddress: Symbol('local address'), + kHost: Symbol('host'), + kNoRef: Symbol('no ref'), + kBodyUsed: Symbol('used'), + kRunning: Symbol('running'), + kBlocking: Symbol('blocking'), + kPending: Symbol('pending'), + kSize: Symbol('size'), + kBusy: Symbol('busy'), + kQueued: Symbol('queued'), + kFree: Symbol('free'), + kConnected: Symbol('connected'), + kClosed: Symbol('closed'), + kNeedDrain: Symbol('need drain'), + kReset: Symbol('reset'), + kDestroyed: Symbol.for('nodejs.stream.destroyed'), + kMaxHeadersSize: Symbol('max headers size'), + kRunningIdx: Symbol('running index'), + kPendingIdx: Symbol('pending index'), + kError: Symbol('error'), + kClients: Symbol('clients'), + kClient: Symbol('client'), + kParser: Symbol('parser'), + kOnDestroyed: Symbol('destroy callbacks'), + kPipelining: Symbol('pipelining'), + kSocket: Symbol('socket'), + kHostHeader: Symbol('host header'), + kConnector: Symbol('connector'), + kStrictContentLength: Symbol('strict content length'), + kMaxRedirections: Symbol('maxRedirections'), + kMaxRequests: Symbol('maxRequestsPerClient'), + kProxy: Symbol('proxy agent options'), + kCounter: Symbol('socket request counter'), + kInterceptors: Symbol('dispatch interceptors'), + kMaxResponseSize: Symbol('max response size'), + kHTTP2Session: Symbol('http2Session'), + kHTTP2SessionState: Symbol('http2Session state'), + kHTTP2BuildRequest: Symbol('http2 build request'), + kHTTP1BuildRequest: Symbol('http1 build request'), + kHTTP2CopyHeaders: Symbol('http2 copy headers'), + kHTTPConnVersion: Symbol('http connection version'), + kRetryHandlerDefaultRetry: Symbol('retry agent default retry'), + kConstruct: Symbol('constructable') +} + + +/***/ }), + +/***/ 3983: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const assert = __nccwpck_require__(9491) +const { kDestroyed, kBodyUsed } = __nccwpck_require__(2785) +const { IncomingMessage } = __nccwpck_require__(3685) +const stream = __nccwpck_require__(2781) +const net = __nccwpck_require__(1808) +const { InvalidArgumentError } = __nccwpck_require__(8045) +const { Blob } = __nccwpck_require__(4300) +const nodeUtil = __nccwpck_require__(3837) +const { stringify } = __nccwpck_require__(3477) +const { headerNameLowerCasedRecord } = __nccwpck_require__(4462) + +const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v)) + +function nop () {} + +function isStream (obj) { + return obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function' +} + +// based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License) +function isBlobLike (object) { + return (Blob && object instanceof Blob) || ( + object && + typeof object === 'object' && + (typeof object.stream === 'function' || + typeof object.arrayBuffer === 'function') && + /^(Blob|File)$/.test(object[Symbol.toStringTag]) + ) +} + +function buildURL (url, queryParams) { + if (url.includes('?') || url.includes('#')) { + throw new Error('Query params cannot be passed when url already contains "?" or "#".') + } + + const stringified = stringify(queryParams) + + if (stringified) { + url += '?' + stringified + } + + return url +} + +function parseURL (url) { + if (typeof url === 'string') { + url = new URL(url) + + if (!/^https?:/.test(url.origin || url.protocol)) { + throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') + } + + return url + } + + if (!url || typeof url !== 'object') { + throw new InvalidArgumentError('Invalid URL: The URL argument must be a non-null object.') + } + + if (!/^https?:/.test(url.origin || url.protocol)) { + throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') + } + + if (!(url instanceof URL)) { + if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) { + throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.') + } + + if (url.path != null && typeof url.path !== 'string') { + throw new InvalidArgumentError('Invalid URL path: the path must be a string or null/undefined.') + } + + if (url.pathname != null && typeof url.pathname !== 'string') { + throw new InvalidArgumentError('Invalid URL pathname: the pathname must be a string or null/undefined.') + } + + if (url.hostname != null && typeof url.hostname !== 'string') { + throw new InvalidArgumentError('Invalid URL hostname: the hostname must be a string or null/undefined.') + } + + if (url.origin != null && typeof url.origin !== 'string') { + throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.') + } + + const port = url.port != null + ? url.port + : (url.protocol === 'https:' ? 443 : 80) + let origin = url.origin != null + ? url.origin + : `${url.protocol}//${url.hostname}:${port}` + let path = url.path != null + ? url.path + : `${url.pathname || ''}${url.search || ''}` + + if (origin.endsWith('/')) { + origin = origin.substring(0, origin.length - 1) + } + + if (path && !path.startsWith('/')) { + path = `/${path}` + } + // new URL(path, origin) is unsafe when `path` contains an absolute URL + // From https://developer.mozilla.org/en-US/docs/Web/API/URL/URL: + // If first parameter is a relative URL, second param is required, and will be used as the base URL. + // If first parameter is an absolute URL, a given second param will be ignored. + url = new URL(origin + path) + } + + return url +} + +function parseOrigin (url) { + url = parseURL(url) + + if (url.pathname !== '/' || url.search || url.hash) { + throw new InvalidArgumentError('invalid url') + } + + return url +} + +function getHostname (host) { + if (host[0] === '[') { + const idx = host.indexOf(']') + + assert(idx !== -1) + return host.substring(1, idx) + } + + const idx = host.indexOf(':') + if (idx === -1) return host + + return host.substring(0, idx) +} + +// IP addresses are not valid server names per RFC6066 +// > Currently, the only server names supported are DNS hostnames +function getServerName (host) { + if (!host) { + return null + } + + assert.strictEqual(typeof host, 'string') + + const servername = getHostname(host) + if (net.isIP(servername)) { + return '' + } + + return servername +} + +function deepClone (obj) { + return JSON.parse(JSON.stringify(obj)) +} + +function isAsyncIterable (obj) { + return !!(obj != null && typeof obj[Symbol.asyncIterator] === 'function') +} + +function isIterable (obj) { + return !!(obj != null && (typeof obj[Symbol.iterator] === 'function' || typeof obj[Symbol.asyncIterator] === 'function')) +} + +function bodyLength (body) { + if (body == null) { + return 0 + } else if (isStream(body)) { + const state = body._readableState + return state && state.objectMode === false && state.ended === true && Number.isFinite(state.length) + ? state.length + : null + } else if (isBlobLike(body)) { + return body.size != null ? body.size : null + } else if (isBuffer(body)) { + return body.byteLength + } + + return null +} + +function isDestroyed (stream) { + return !stream || !!(stream.destroyed || stream[kDestroyed]) +} + +function isReadableAborted (stream) { + const state = stream && stream._readableState + return isDestroyed(stream) && state && !state.endEmitted +} + +function destroy (stream, err) { + if (stream == null || !isStream(stream) || isDestroyed(stream)) { + return + } + + if (typeof stream.destroy === 'function') { + if (Object.getPrototypeOf(stream).constructor === IncomingMessage) { + // See: https://github.com/nodejs/node/pull/38505/files + stream.socket = null + } + + stream.destroy(err) + } else if (err) { + process.nextTick((stream, err) => { + stream.emit('error', err) + }, stream, err) + } + + if (stream.destroyed !== true) { + stream[kDestroyed] = true + } +} + +const KEEPALIVE_TIMEOUT_EXPR = /timeout=(\d+)/ +function parseKeepAliveTimeout (val) { + const m = val.toString().match(KEEPALIVE_TIMEOUT_EXPR) + return m ? parseInt(m[1], 10) * 1000 : null +} + +/** + * Retrieves a header name and returns its lowercase value. + * @param {string | Buffer} value Header name + * @returns {string} + */ +function headerNameToString (value) { + return headerNameLowerCasedRecord[value] || value.toLowerCase() +} + +function parseHeaders (headers, obj = {}) { + // For H2 support + if (!Array.isArray(headers)) return headers + + for (let i = 0; i < headers.length; i += 2) { + const key = headers[i].toString().toLowerCase() + let val = obj[key] + + if (!val) { + if (Array.isArray(headers[i + 1])) { + obj[key] = headers[i + 1].map(x => x.toString('utf8')) + } else { + obj[key] = headers[i + 1].toString('utf8') + } + } else { + if (!Array.isArray(val)) { + val = [val] + obj[key] = val + } + val.push(headers[i + 1].toString('utf8')) + } + } + + // See https://github.com/nodejs/node/pull/46528 + if ('content-length' in obj && 'content-disposition' in obj) { + obj['content-disposition'] = Buffer.from(obj['content-disposition']).toString('latin1') + } + + return obj +} + +function parseRawHeaders (headers) { + const ret = [] + let hasContentLength = false + let contentDispositionIdx = -1 + + for (let n = 0; n < headers.length; n += 2) { + const key = headers[n + 0].toString() + const val = headers[n + 1].toString('utf8') + + if (key.length === 14 && (key === 'content-length' || key.toLowerCase() === 'content-length')) { + ret.push(key, val) + hasContentLength = true + } else if (key.length === 19 && (key === 'content-disposition' || key.toLowerCase() === 'content-disposition')) { + contentDispositionIdx = ret.push(key, val) - 1 + } else { + ret.push(key, val) + } + } + + // See https://github.com/nodejs/node/pull/46528 + if (hasContentLength && contentDispositionIdx !== -1) { + ret[contentDispositionIdx] = Buffer.from(ret[contentDispositionIdx]).toString('latin1') + } + + return ret +} + +function isBuffer (buffer) { + // See, https://github.com/mcollina/undici/pull/319 + return buffer instanceof Uint8Array || Buffer.isBuffer(buffer) +} + +function validateHandler (handler, method, upgrade) { + if (!handler || typeof handler !== 'object') { + throw new InvalidArgumentError('handler must be an object') + } + + if (typeof handler.onConnect !== 'function') { + throw new InvalidArgumentError('invalid onConnect method') + } + + if (typeof handler.onError !== 'function') { + throw new InvalidArgumentError('invalid onError method') + } + + if (typeof handler.onBodySent !== 'function' && handler.onBodySent !== undefined) { + throw new InvalidArgumentError('invalid onBodySent method') + } + + if (upgrade || method === 'CONNECT') { + if (typeof handler.onUpgrade !== 'function') { + throw new InvalidArgumentError('invalid onUpgrade method') + } + } else { + if (typeof handler.onHeaders !== 'function') { + throw new InvalidArgumentError('invalid onHeaders method') + } + + if (typeof handler.onData !== 'function') { + throw new InvalidArgumentError('invalid onData method') + } + + if (typeof handler.onComplete !== 'function') { + throw new InvalidArgumentError('invalid onComplete method') + } + } +} + +// A body is disturbed if it has been read from and it cannot +// be re-used without losing state or data. +function isDisturbed (body) { + return !!(body && ( + stream.isDisturbed + ? stream.isDisturbed(body) || body[kBodyUsed] // TODO (fix): Why is body[kBodyUsed] needed? + : body[kBodyUsed] || + body.readableDidRead || + (body._readableState && body._readableState.dataEmitted) || + isReadableAborted(body) + )) +} + +function isErrored (body) { + return !!(body && ( + stream.isErrored + ? stream.isErrored(body) + : /state: 'errored'/.test(nodeUtil.inspect(body) + ))) +} + +function isReadable (body) { + return !!(body && ( + stream.isReadable + ? stream.isReadable(body) + : /state: 'readable'/.test(nodeUtil.inspect(body) + ))) +} + +function getSocketInfo (socket) { + return { + localAddress: socket.localAddress, + localPort: socket.localPort, + remoteAddress: socket.remoteAddress, + remotePort: socket.remotePort, + remoteFamily: socket.remoteFamily, + timeout: socket.timeout, + bytesWritten: socket.bytesWritten, + bytesRead: socket.bytesRead + } +} + +async function * convertIterableToBuffer (iterable) { + for await (const chunk of iterable) { + yield Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk) + } +} + +let ReadableStream +function ReadableStreamFrom (iterable) { + if (!ReadableStream) { + ReadableStream = (__nccwpck_require__(5356).ReadableStream) + } + + if (ReadableStream.from) { + return ReadableStream.from(convertIterableToBuffer(iterable)) + } + + let iterator + return new ReadableStream( + { + async start () { + iterator = iterable[Symbol.asyncIterator]() + }, + async pull (controller) { + const { done, value } = await iterator.next() + if (done) { + queueMicrotask(() => { + controller.close() + }) + } else { + const buf = Buffer.isBuffer(value) ? value : Buffer.from(value) + controller.enqueue(new Uint8Array(buf)) + } + return controller.desiredSize > 0 + }, + async cancel (reason) { + await iterator.return() + } + }, + 0 + ) +} + +// The chunk should be a FormData instance and contains +// all the required methods. +function isFormDataLike (object) { + return ( + object && + typeof object === 'object' && + typeof object.append === 'function' && + typeof object.delete === 'function' && + typeof object.get === 'function' && + typeof object.getAll === 'function' && + typeof object.has === 'function' && + typeof object.set === 'function' && + object[Symbol.toStringTag] === 'FormData' + ) +} + +function throwIfAborted (signal) { + if (!signal) { return } + if (typeof signal.throwIfAborted === 'function') { + signal.throwIfAborted() + } else { + if (signal.aborted) { + // DOMException not available < v17.0.0 + const err = new Error('The operation was aborted') + err.name = 'AbortError' + throw err + } + } +} + +function addAbortListener (signal, listener) { + if ('addEventListener' in signal) { + signal.addEventListener('abort', listener, { once: true }) + return () => signal.removeEventListener('abort', listener) + } + signal.addListener('abort', listener) + return () => signal.removeListener('abort', listener) +} + +const hasToWellFormed = !!String.prototype.toWellFormed + +/** + * @param {string} val + */ +function toUSVString (val) { + if (hasToWellFormed) { + return `${val}`.toWellFormed() + } else if (nodeUtil.toUSVString) { + return nodeUtil.toUSVString(val) + } + + return `${val}` +} + +// Parsed accordingly to RFC 9110 +// https://www.rfc-editor.org/rfc/rfc9110#field.content-range +function parseRangeHeader (range) { + if (range == null || range === '') return { start: 0, end: null, size: null } + + const m = range ? range.match(/^bytes (\d+)-(\d+)\/(\d+)?$/) : null + return m + ? { + start: parseInt(m[1]), + end: m[2] ? parseInt(m[2]) : null, + size: m[3] ? parseInt(m[3]) : null + } + : null +} + +const kEnumerableProperty = Object.create(null) +kEnumerableProperty.enumerable = true + +module.exports = { + kEnumerableProperty, + nop, + isDisturbed, + isErrored, + isReadable, + toUSVString, + isReadableAborted, + isBlobLike, + parseOrigin, + parseURL, + getServerName, + isStream, + isIterable, + isAsyncIterable, + isDestroyed, + headerNameToString, + parseRawHeaders, + parseHeaders, + parseKeepAliveTimeout, + destroy, + bodyLength, + deepClone, + ReadableStreamFrom, + isBuffer, + validateHandler, + getSocketInfo, + isFormDataLike, + buildURL, + throwIfAborted, + addAbortListener, + parseRangeHeader, + nodeMajor, + nodeMinor, + nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13), + safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'] +} + + +/***/ }), + +/***/ 4839: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Dispatcher = __nccwpck_require__(412) +const { + ClientDestroyedError, + ClientClosedError, + InvalidArgumentError +} = __nccwpck_require__(8045) +const { kDestroy, kClose, kDispatch, kInterceptors } = __nccwpck_require__(2785) + +const kDestroyed = Symbol('destroyed') +const kClosed = Symbol('closed') +const kOnDestroyed = Symbol('onDestroyed') +const kOnClosed = Symbol('onClosed') +const kInterceptedDispatch = Symbol('Intercepted Dispatch') + +class DispatcherBase extends Dispatcher { + constructor () { + super() + + this[kDestroyed] = false + this[kOnDestroyed] = null + this[kClosed] = false + this[kOnClosed] = [] + } + + get destroyed () { + return this[kDestroyed] + } + + get closed () { + return this[kClosed] + } + + get interceptors () { + return this[kInterceptors] + } + + set interceptors (newInterceptors) { + if (newInterceptors) { + for (let i = newInterceptors.length - 1; i >= 0; i--) { + const interceptor = this[kInterceptors][i] + if (typeof interceptor !== 'function') { + throw new InvalidArgumentError('interceptor must be an function') + } + } + } + + this[kInterceptors] = newInterceptors + } + + close (callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + this.close((err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (this[kDestroyed]) { + queueMicrotask(() => callback(new ClientDestroyedError(), null)) + return + } + + if (this[kClosed]) { + if (this[kOnClosed]) { + this[kOnClosed].push(callback) + } else { + queueMicrotask(() => callback(null, null)) + } + return + } + + this[kClosed] = true + this[kOnClosed].push(callback) + + const onClosed = () => { + const callbacks = this[kOnClosed] + this[kOnClosed] = null + for (let i = 0; i < callbacks.length; i++) { + callbacks[i](null, null) + } + } + + // Should not error. + this[kClose]() + .then(() => this.destroy()) + .then(() => { + queueMicrotask(onClosed) + }) + } + + destroy (err, callback) { + if (typeof err === 'function') { + callback = err + err = null + } + + if (callback === undefined) { + return new Promise((resolve, reject) => { + this.destroy(err, (err, data) => { + return err ? /* istanbul ignore next: should never error */ reject(err) : resolve(data) + }) + }) + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (this[kDestroyed]) { + if (this[kOnDestroyed]) { + this[kOnDestroyed].push(callback) + } else { + queueMicrotask(() => callback(null, null)) + } + return + } + + if (!err) { + err = new ClientDestroyedError() + } + + this[kDestroyed] = true + this[kOnDestroyed] = this[kOnDestroyed] || [] + this[kOnDestroyed].push(callback) + + const onDestroyed = () => { + const callbacks = this[kOnDestroyed] + this[kOnDestroyed] = null + for (let i = 0; i < callbacks.length; i++) { + callbacks[i](null, null) + } + } + + // Should not error. + this[kDestroy](err).then(() => { + queueMicrotask(onDestroyed) + }) + } + + [kInterceptedDispatch] (opts, handler) { + if (!this[kInterceptors] || this[kInterceptors].length === 0) { + this[kInterceptedDispatch] = this[kDispatch] + return this[kDispatch](opts, handler) + } + + let dispatch = this[kDispatch].bind(this) + for (let i = this[kInterceptors].length - 1; i >= 0; i--) { + dispatch = this[kInterceptors][i](dispatch) + } + this[kInterceptedDispatch] = dispatch + return dispatch(opts, handler) + } + + dispatch (opts, handler) { + if (!handler || typeof handler !== 'object') { + throw new InvalidArgumentError('handler must be an object') + } + + try { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('opts must be an object.') + } + + if (this[kDestroyed] || this[kOnDestroyed]) { + throw new ClientDestroyedError() + } + + if (this[kClosed]) { + throw new ClientClosedError() + } + + return this[kInterceptedDispatch](opts, handler) + } catch (err) { + if (typeof handler.onError !== 'function') { + throw new InvalidArgumentError('invalid onError method') + } + + handler.onError(err) + + return false + } + } +} + +module.exports = DispatcherBase + + +/***/ }), + +/***/ 412: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const EventEmitter = __nccwpck_require__(2361) + +class Dispatcher extends EventEmitter { + dispatch () { + throw new Error('not implemented') + } + + close () { + throw new Error('not implemented') + } + + destroy () { + throw new Error('not implemented') + } +} + +module.exports = Dispatcher + + +/***/ }), + +/***/ 1472: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Busboy = __nccwpck_require__(727) +const util = __nccwpck_require__(3983) +const { + ReadableStreamFrom, + isBlobLike, + isReadableStreamLike, + readableStreamClose, + createDeferredPromise, + fullyReadBody +} = __nccwpck_require__(2538) +const { FormData } = __nccwpck_require__(2015) +const { kState } = __nccwpck_require__(5861) +const { webidl } = __nccwpck_require__(1744) +const { DOMException, structuredClone } = __nccwpck_require__(1037) +const { Blob, File: NativeFile } = __nccwpck_require__(4300) +const { kBodyUsed } = __nccwpck_require__(2785) +const assert = __nccwpck_require__(9491) +const { isErrored } = __nccwpck_require__(3983) +const { isUint8Array, isArrayBuffer } = __nccwpck_require__(4978) +const { File: UndiciFile } = __nccwpck_require__(8511) +const { parseMIMEType, serializeAMimeType } = __nccwpck_require__(685) + +let random +try { + const crypto = __nccwpck_require__(6005) + random = (max) => crypto.randomInt(0, max) +} catch { + random = (max) => Math.floor(Math.random(max)) +} + +let ReadableStream = globalThis.ReadableStream + +/** @type {globalThis['File']} */ +const File = NativeFile ?? UndiciFile +const textEncoder = new TextEncoder() +const textDecoder = new TextDecoder() + +// https://fetch.spec.whatwg.org/#concept-bodyinit-extract +function extractBody (object, keepalive = false) { + if (!ReadableStream) { + ReadableStream = (__nccwpck_require__(5356).ReadableStream) + } + + // 1. Let stream be null. + let stream = null + + // 2. If object is a ReadableStream object, then set stream to object. + if (object instanceof ReadableStream) { + stream = object + } else if (isBlobLike(object)) { + // 3. Otherwise, if object is a Blob object, set stream to the + // result of running object’s get stream. + stream = object.stream() + } else { + // 4. Otherwise, set stream to a new ReadableStream object, and set + // up stream. + stream = new ReadableStream({ + async pull (controller) { + controller.enqueue( + typeof source === 'string' ? textEncoder.encode(source) : source + ) + queueMicrotask(() => readableStreamClose(controller)) + }, + start () {}, + type: undefined + }) + } + + // 5. Assert: stream is a ReadableStream object. + assert(isReadableStreamLike(stream)) + + // 6. Let action be null. + let action = null + + // 7. Let source be null. + let source = null + + // 8. Let length be null. + let length = null + + // 9. Let type be null. + let type = null + + // 10. Switch on object: + if (typeof object === 'string') { + // Set source to the UTF-8 encoding of object. + // Note: setting source to a Uint8Array here breaks some mocking assumptions. + source = object + + // Set type to `text/plain;charset=UTF-8`. + type = 'text/plain;charset=UTF-8' + } else if (object instanceof URLSearchParams) { + // URLSearchParams + + // spec says to run application/x-www-form-urlencoded on body.list + // this is implemented in Node.js as apart of an URLSearchParams instance toString method + // See: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L490 + // and https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L1100 + + // Set source to the result of running the application/x-www-form-urlencoded serializer with object’s list. + source = object.toString() + + // Set type to `application/x-www-form-urlencoded;charset=UTF-8`. + type = 'application/x-www-form-urlencoded;charset=UTF-8' + } else if (isArrayBuffer(object)) { + // BufferSource/ArrayBuffer + + // Set source to a copy of the bytes held by object. + source = new Uint8Array(object.slice()) + } else if (ArrayBuffer.isView(object)) { + // BufferSource/ArrayBufferView + + // Set source to a copy of the bytes held by object. + source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength)) + } else if (util.isFormDataLike(object)) { + const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, '0')}` + const prefix = `--${boundary}\r\nContent-Disposition: form-data` + + /*! formdata-polyfill. MIT License. Jimmy Wärting */ + const escape = (str) => + str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22') + const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n') + + // Set action to this step: run the multipart/form-data + // encoding algorithm, with object’s entry list and UTF-8. + // - This ensures that the body is immutable and can't be changed afterwords + // - That the content-length is calculated in advance. + // - And that all parts are pre-encoded and ready to be sent. + + const blobParts = [] + const rn = new Uint8Array([13, 10]) // '\r\n' + length = 0 + let hasUnknownSizeValue = false + + for (const [name, value] of object) { + if (typeof value === 'string') { + const chunk = textEncoder.encode(prefix + + `; name="${escape(normalizeLinefeeds(name))}"` + + `\r\n\r\n${normalizeLinefeeds(value)}\r\n`) + blobParts.push(chunk) + length += chunk.byteLength + } else { + const chunk = textEncoder.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` + + (value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' + + `Content-Type: ${ + value.type || 'application/octet-stream' + }\r\n\r\n`) + blobParts.push(chunk, value, rn) + if (typeof value.size === 'number') { + length += chunk.byteLength + value.size + rn.byteLength + } else { + hasUnknownSizeValue = true + } + } + } + + const chunk = textEncoder.encode(`--${boundary}--`) + blobParts.push(chunk) + length += chunk.byteLength + if (hasUnknownSizeValue) { + length = null + } + + // Set source to object. + source = object + + action = async function * () { + for (const part of blobParts) { + if (part.stream) { + yield * part.stream() + } else { + yield part + } + } + } + + // Set type to `multipart/form-data; boundary=`, + // followed by the multipart/form-data boundary string generated + // by the multipart/form-data encoding algorithm. + type = 'multipart/form-data; boundary=' + boundary + } else if (isBlobLike(object)) { + // Blob + + // Set source to object. + source = object + + // Set length to object’s size. + length = object.size + + // If object’s type attribute is not the empty byte sequence, set + // type to its value. + if (object.type) { + type = object.type + } + } else if (typeof object[Symbol.asyncIterator] === 'function') { + // If keepalive is true, then throw a TypeError. + if (keepalive) { + throw new TypeError('keepalive') + } + + // If object is disturbed or locked, then throw a TypeError. + if (util.isDisturbed(object) || object.locked) { + throw new TypeError( + 'Response body object should not be disturbed or locked' + ) + } + + stream = + object instanceof ReadableStream ? object : ReadableStreamFrom(object) + } + + // 11. If source is a byte sequence, then set action to a + // step that returns source and length to source’s length. + if (typeof source === 'string' || util.isBuffer(source)) { + length = Buffer.byteLength(source) + } + + // 12. If action is non-null, then run these steps in in parallel: + if (action != null) { + // Run action. + let iterator + stream = new ReadableStream({ + async start () { + iterator = action(object)[Symbol.asyncIterator]() + }, + async pull (controller) { + const { value, done } = await iterator.next() + if (done) { + // When running action is done, close stream. + queueMicrotask(() => { + controller.close() + }) + } else { + // Whenever one or more bytes are available and stream is not errored, + // enqueue a Uint8Array wrapping an ArrayBuffer containing the available + // bytes into stream. + if (!isErrored(stream)) { + controller.enqueue(new Uint8Array(value)) + } + } + return controller.desiredSize > 0 + }, + async cancel (reason) { + await iterator.return() + }, + type: undefined + }) + } + + // 13. Let body be a body whose stream is stream, source is source, + // and length is length. + const body = { stream, source, length } + + // 14. Return (body, type). + return [body, type] +} + +// https://fetch.spec.whatwg.org/#bodyinit-safely-extract +function safelyExtractBody (object, keepalive = false) { + if (!ReadableStream) { + // istanbul ignore next + ReadableStream = (__nccwpck_require__(5356).ReadableStream) + } + + // To safely extract a body and a `Content-Type` value from + // a byte sequence or BodyInit object object, run these steps: + + // 1. If object is a ReadableStream object, then: + if (object instanceof ReadableStream) { + // Assert: object is neither disturbed nor locked. + // istanbul ignore next + assert(!util.isDisturbed(object), 'The body has already been consumed.') + // istanbul ignore next + assert(!object.locked, 'The stream is locked.') + } + + // 2. Return the results of extracting object. + return extractBody(object, keepalive) +} + +function cloneBody (body) { + // To clone a body body, run these steps: + + // https://fetch.spec.whatwg.org/#concept-body-clone + + // 1. Let « out1, out2 » be the result of teeing body’s stream. + const [out1, out2] = body.stream.tee() + const out2Clone = structuredClone(out2, { transfer: [out2] }) + // This, for whatever reasons, unrefs out2Clone which allows + // the process to exit by itself. + const [, finalClone] = out2Clone.tee() + + // 2. Set body’s stream to out1. + body.stream = out1 + + // 3. Return a body whose stream is out2 and other members are copied from body. + return { + stream: finalClone, + length: body.length, + source: body.source + } +} + +async function * consumeBody (body) { + if (body) { + if (isUint8Array(body)) { + yield body + } else { + const stream = body.stream + + if (util.isDisturbed(stream)) { + throw new TypeError('The body has already been consumed.') + } + + if (stream.locked) { + throw new TypeError('The stream is locked.') + } + + // Compat. + stream[kBodyUsed] = true + + yield * stream + } + } +} + +function throwIfAborted (state) { + if (state.aborted) { + throw new DOMException('The operation was aborted.', 'AbortError') + } +} + +function bodyMixinMethods (instance) { + const methods = { + blob () { + // The blob() method steps are to return the result of + // running consume body with this and the following step + // given a byte sequence bytes: return a Blob whose + // contents are bytes and whose type attribute is this’s + // MIME type. + return specConsumeBody(this, (bytes) => { + let mimeType = bodyMimeType(this) + + if (mimeType === 'failure') { + mimeType = '' + } else if (mimeType) { + mimeType = serializeAMimeType(mimeType) + } + + // Return a Blob whose contents are bytes and type attribute + // is mimeType. + return new Blob([bytes], { type: mimeType }) + }, instance) + }, + + arrayBuffer () { + // The arrayBuffer() method steps are to return the result + // of running consume body with this and the following step + // given a byte sequence bytes: return a new ArrayBuffer + // whose contents are bytes. + return specConsumeBody(this, (bytes) => { + return new Uint8Array(bytes).buffer + }, instance) + }, + + text () { + // The text() method steps are to return the result of running + // consume body with this and UTF-8 decode. + return specConsumeBody(this, utf8DecodeBytes, instance) + }, + + json () { + // The json() method steps are to return the result of running + // consume body with this and parse JSON from bytes. + return specConsumeBody(this, parseJSONFromBytes, instance) + }, + + async formData () { + webidl.brandCheck(this, instance) + + throwIfAborted(this[kState]) + + const contentType = this.headers.get('Content-Type') + + // If mimeType’s essence is "multipart/form-data", then: + if (/multipart\/form-data/.test(contentType)) { + const headers = {} + for (const [key, value] of this.headers) headers[key.toLowerCase()] = value + + const responseFormData = new FormData() + + let busboy + + try { + busboy = new Busboy({ + headers, + preservePath: true + }) + } catch (err) { + throw new DOMException(`${err}`, 'AbortError') + } + + busboy.on('field', (name, value) => { + responseFormData.append(name, value) + }) + busboy.on('file', (name, value, filename, encoding, mimeType) => { + const chunks = [] + + if (encoding === 'base64' || encoding.toLowerCase() === 'base64') { + let base64chunk = '' + + value.on('data', (chunk) => { + base64chunk += chunk.toString().replace(/[\r\n]/gm, '') + + const end = base64chunk.length - base64chunk.length % 4 + chunks.push(Buffer.from(base64chunk.slice(0, end), 'base64')) + + base64chunk = base64chunk.slice(end) + }) + value.on('end', () => { + chunks.push(Buffer.from(base64chunk, 'base64')) + responseFormData.append(name, new File(chunks, filename, { type: mimeType })) + }) + } else { + value.on('data', (chunk) => { + chunks.push(chunk) + }) + value.on('end', () => { + responseFormData.append(name, new File(chunks, filename, { type: mimeType })) + }) + } + }) + + const busboyResolve = new Promise((resolve, reject) => { + busboy.on('finish', resolve) + busboy.on('error', (err) => reject(new TypeError(err))) + }) + + if (this.body !== null) for await (const chunk of consumeBody(this[kState].body)) busboy.write(chunk) + busboy.end() + await busboyResolve + + return responseFormData + } else if (/application\/x-www-form-urlencoded/.test(contentType)) { + // Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then: + + // 1. Let entries be the result of parsing bytes. + let entries + try { + let text = '' + // application/x-www-form-urlencoded parser will keep the BOM. + // https://url.spec.whatwg.org/#concept-urlencoded-parser + // Note that streaming decoder is stateful and cannot be reused + const streamingDecoder = new TextDecoder('utf-8', { ignoreBOM: true }) + + for await (const chunk of consumeBody(this[kState].body)) { + if (!isUint8Array(chunk)) { + throw new TypeError('Expected Uint8Array chunk') + } + text += streamingDecoder.decode(chunk, { stream: true }) + } + text += streamingDecoder.decode() + entries = new URLSearchParams(text) + } catch (err) { + // istanbul ignore next: Unclear when new URLSearchParams can fail on a string. + // 2. If entries is failure, then throw a TypeError. + throw Object.assign(new TypeError(), { cause: err }) + } + + // 3. Return a new FormData object whose entries are entries. + const formData = new FormData() + for (const [name, value] of entries) { + formData.append(name, value) + } + return formData + } else { + // Wait a tick before checking if the request has been aborted. + // Otherwise, a TypeError can be thrown when an AbortError should. + await Promise.resolve() + + throwIfAborted(this[kState]) + + // Otherwise, throw a TypeError. + throw webidl.errors.exception({ + header: `${instance.name}.formData`, + message: 'Could not parse content as FormData.' + }) + } + } + } + + return methods +} + +function mixinBody (prototype) { + Object.assign(prototype.prototype, bodyMixinMethods(prototype)) +} + +/** + * @see https://fetch.spec.whatwg.org/#concept-body-consume-body + * @param {Response|Request} object + * @param {(value: unknown) => unknown} convertBytesToJSValue + * @param {Response|Request} instance + */ +async function specConsumeBody (object, convertBytesToJSValue, instance) { + webidl.brandCheck(object, instance) + + throwIfAborted(object[kState]) + + // 1. If object is unusable, then return a promise rejected + // with a TypeError. + if (bodyUnusable(object[kState].body)) { + throw new TypeError('Body is unusable') + } + + // 2. Let promise be a new promise. + const promise = createDeferredPromise() + + // 3. Let errorSteps given error be to reject promise with error. + const errorSteps = (error) => promise.reject(error) + + // 4. Let successSteps given a byte sequence data be to resolve + // promise with the result of running convertBytesToJSValue + // with data. If that threw an exception, then run errorSteps + // with that exception. + const successSteps = (data) => { + try { + promise.resolve(convertBytesToJSValue(data)) + } catch (e) { + errorSteps(e) + } + } + + // 5. If object’s body is null, then run successSteps with an + // empty byte sequence. + if (object[kState].body == null) { + successSteps(new Uint8Array()) + return promise.promise + } + + // 6. Otherwise, fully read object’s body given successSteps, + // errorSteps, and object’s relevant global object. + await fullyReadBody(object[kState].body, successSteps, errorSteps) + + // 7. Return promise. + return promise.promise +} + +// https://fetch.spec.whatwg.org/#body-unusable +function bodyUnusable (body) { + // An object including the Body interface mixin is + // said to be unusable if its body is non-null and + // its body’s stream is disturbed or locked. + return body != null && (body.stream.locked || util.isDisturbed(body.stream)) +} + +/** + * @see https://encoding.spec.whatwg.org/#utf-8-decode + * @param {Buffer} buffer + */ +function utf8DecodeBytes (buffer) { + if (buffer.length === 0) { + return '' + } + + // 1. Let buffer be the result of peeking three bytes from + // ioQueue, converted to a byte sequence. + + // 2. If buffer is 0xEF 0xBB 0xBF, then read three + // bytes from ioQueue. (Do nothing with those bytes.) + if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) { + buffer = buffer.subarray(3) + } + + // 3. Process a queue with an instance of UTF-8’s + // decoder, ioQueue, output, and "replacement". + const output = textDecoder.decode(buffer) + + // 4. Return output. + return output +} + +/** + * @see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value + * @param {Uint8Array} bytes + */ +function parseJSONFromBytes (bytes) { + return JSON.parse(utf8DecodeBytes(bytes)) +} + +/** + * @see https://fetch.spec.whatwg.org/#concept-body-mime-type + * @param {import('./response').Response|import('./request').Request} object + */ +function bodyMimeType (object) { + const { headersList } = object[kState] + const contentType = headersList.get('content-type') + + if (contentType === null) { + return 'failure' + } + + return parseMIMEType(contentType) +} + +module.exports = { + extractBody, + safelyExtractBody, + cloneBody, + mixinBody +} + + +/***/ }), + +/***/ 1037: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { MessageChannel, receiveMessageOnPort } = __nccwpck_require__(1267) + +const corsSafeListedMethods = ['GET', 'HEAD', 'POST'] +const corsSafeListedMethodsSet = new Set(corsSafeListedMethods) + +const nullBodyStatus = [101, 204, 205, 304] + +const redirectStatus = [301, 302, 303, 307, 308] +const redirectStatusSet = new Set(redirectStatus) + +// https://fetch.spec.whatwg.org/#block-bad-port +const badPorts = [ + '1', '7', '9', '11', '13', '15', '17', '19', '20', '21', '22', '23', '25', '37', '42', '43', '53', '69', '77', '79', + '87', '95', '101', '102', '103', '104', '109', '110', '111', '113', '115', '117', '119', '123', '135', '137', + '139', '143', '161', '179', '389', '427', '465', '512', '513', '514', '515', '526', '530', '531', '532', + '540', '548', '554', '556', '563', '587', '601', '636', '989', '990', '993', '995', '1719', '1720', '1723', + '2049', '3659', '4045', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6697', + '10080' +] + +const badPortsSet = new Set(badPorts) + +// https://w3c.github.io/webappsec-referrer-policy/#referrer-policies +const referrerPolicy = [ + '', + 'no-referrer', + 'no-referrer-when-downgrade', + 'same-origin', + 'origin', + 'strict-origin', + 'origin-when-cross-origin', + 'strict-origin-when-cross-origin', + 'unsafe-url' +] +const referrerPolicySet = new Set(referrerPolicy) + +const requestRedirect = ['follow', 'manual', 'error'] + +const safeMethods = ['GET', 'HEAD', 'OPTIONS', 'TRACE'] +const safeMethodsSet = new Set(safeMethods) + +const requestMode = ['navigate', 'same-origin', 'no-cors', 'cors'] + +const requestCredentials = ['omit', 'same-origin', 'include'] + +const requestCache = [ + 'default', + 'no-store', + 'reload', + 'no-cache', + 'force-cache', + 'only-if-cached' +] + +// https://fetch.spec.whatwg.org/#request-body-header-name +const requestBodyHeader = [ + 'content-encoding', + 'content-language', + 'content-location', + 'content-type', + // See https://github.com/nodejs/undici/issues/2021 + // 'Content-Length' is a forbidden header name, which is typically + // removed in the Headers implementation. However, undici doesn't + // filter out headers, so we add it here. + 'content-length' +] + +// https://fetch.spec.whatwg.org/#enumdef-requestduplex +const requestDuplex = [ + 'half' +] + +// http://fetch.spec.whatwg.org/#forbidden-method +const forbiddenMethods = ['CONNECT', 'TRACE', 'TRACK'] +const forbiddenMethodsSet = new Set(forbiddenMethods) + +const subresource = [ + 'audio', + 'audioworklet', + 'font', + 'image', + 'manifest', + 'paintworklet', + 'script', + 'style', + 'track', + 'video', + 'xslt', + '' +] +const subresourceSet = new Set(subresource) + +/** @type {globalThis['DOMException']} */ +const DOMException = globalThis.DOMException ?? (() => { + // DOMException was only made a global in Node v17.0.0, + // but fetch supports >= v16.8. + try { + atob('~') + } catch (err) { + return Object.getPrototypeOf(err).constructor + } +})() + +let channel + +/** @type {globalThis['structuredClone']} */ +const structuredClone = + globalThis.structuredClone ?? + // https://github.com/nodejs/node/blob/b27ae24dcc4251bad726d9d84baf678d1f707fed/lib/internal/structured_clone.js + // structuredClone was added in v17.0.0, but fetch supports v16.8 + function structuredClone (value, options = undefined) { + if (arguments.length === 0) { + throw new TypeError('missing argument') + } + + if (!channel) { + channel = new MessageChannel() + } + channel.port1.unref() + channel.port2.unref() + channel.port1.postMessage(value, options?.transfer) + return receiveMessageOnPort(channel.port2).message + } + +module.exports = { + DOMException, + structuredClone, + subresource, + forbiddenMethods, + requestBodyHeader, + referrerPolicy, + requestRedirect, + requestMode, + requestCredentials, + requestCache, + redirectStatus, + corsSafeListedMethods, + nullBodyStatus, + safeMethods, + badPorts, + requestDuplex, + subresourceSet, + badPortsSet, + redirectStatusSet, + corsSafeListedMethodsSet, + safeMethodsSet, + forbiddenMethodsSet, + referrerPolicySet +} + + +/***/ }), + +/***/ 685: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const assert = __nccwpck_require__(9491) +const { atob } = __nccwpck_require__(4300) +const { isomorphicDecode } = __nccwpck_require__(2538) + +const encoder = new TextEncoder() + +/** + * @see https://mimesniff.spec.whatwg.org/#http-token-code-point + */ +const HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+-.^_|~A-Za-z0-9]+$/ +const HTTP_WHITESPACE_REGEX = /(\u000A|\u000D|\u0009|\u0020)/ // eslint-disable-line +/** + * @see https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point + */ +const HTTP_QUOTED_STRING_TOKENS = /[\u0009|\u0020-\u007E|\u0080-\u00FF]/ // eslint-disable-line + +// https://fetch.spec.whatwg.org/#data-url-processor +/** @param {URL} dataURL */ +function dataURLProcessor (dataURL) { + // 1. Assert: dataURL’s scheme is "data". + assert(dataURL.protocol === 'data:') + + // 2. Let input be the result of running the URL + // serializer on dataURL with exclude fragment + // set to true. + let input = URLSerializer(dataURL, true) + + // 3. Remove the leading "data:" string from input. + input = input.slice(5) + + // 4. Let position point at the start of input. + const position = { position: 0 } + + // 5. Let mimeType be the result of collecting a + // sequence of code points that are not equal + // to U+002C (,), given position. + let mimeType = collectASequenceOfCodePointsFast( + ',', + input, + position + ) + + // 6. Strip leading and trailing ASCII whitespace + // from mimeType. + // Undici implementation note: we need to store the + // length because if the mimetype has spaces removed, + // the wrong amount will be sliced from the input in + // step #9 + const mimeTypeLength = mimeType.length + mimeType = removeASCIIWhitespace(mimeType, true, true) + + // 7. If position is past the end of input, then + // return failure + if (position.position >= input.length) { + return 'failure' + } + + // 8. Advance position by 1. + position.position++ + + // 9. Let encodedBody be the remainder of input. + const encodedBody = input.slice(mimeTypeLength + 1) + + // 10. Let body be the percent-decoding of encodedBody. + let body = stringPercentDecode(encodedBody) + + // 11. If mimeType ends with U+003B (;), followed by + // zero or more U+0020 SPACE, followed by an ASCII + // case-insensitive match for "base64", then: + if (/;(\u0020){0,}base64$/i.test(mimeType)) { + // 1. Let stringBody be the isomorphic decode of body. + const stringBody = isomorphicDecode(body) + + // 2. Set body to the forgiving-base64 decode of + // stringBody. + body = forgivingBase64(stringBody) + + // 3. If body is failure, then return failure. + if (body === 'failure') { + return 'failure' + } + + // 4. Remove the last 6 code points from mimeType. + mimeType = mimeType.slice(0, -6) + + // 5. Remove trailing U+0020 SPACE code points from mimeType, + // if any. + mimeType = mimeType.replace(/(\u0020)+$/, '') + + // 6. Remove the last U+003B (;) code point from mimeType. + mimeType = mimeType.slice(0, -1) + } + + // 12. If mimeType starts with U+003B (;), then prepend + // "text/plain" to mimeType. + if (mimeType.startsWith(';')) { + mimeType = 'text/plain' + mimeType + } + + // 13. Let mimeTypeRecord be the result of parsing + // mimeType. + let mimeTypeRecord = parseMIMEType(mimeType) + + // 14. If mimeTypeRecord is failure, then set + // mimeTypeRecord to text/plain;charset=US-ASCII. + if (mimeTypeRecord === 'failure') { + mimeTypeRecord = parseMIMEType('text/plain;charset=US-ASCII') + } + + // 15. Return a new data: URL struct whose MIME + // type is mimeTypeRecord and body is body. + // https://fetch.spec.whatwg.org/#data-url-struct + return { mimeType: mimeTypeRecord, body } +} + +// https://url.spec.whatwg.org/#concept-url-serializer +/** + * @param {URL} url + * @param {boolean} excludeFragment + */ +function URLSerializer (url, excludeFragment = false) { + if (!excludeFragment) { + return url.href + } + + const href = url.href + const hashLength = url.hash.length + + return hashLength === 0 ? href : href.substring(0, href.length - hashLength) +} + +// https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points +/** + * @param {(char: string) => boolean} condition + * @param {string} input + * @param {{ position: number }} position + */ +function collectASequenceOfCodePoints (condition, input, position) { + // 1. Let result be the empty string. + let result = '' + + // 2. While position doesn’t point past the end of input and the + // code point at position within input meets the condition condition: + while (position.position < input.length && condition(input[position.position])) { + // 1. Append that code point to the end of result. + result += input[position.position] + + // 2. Advance position by 1. + position.position++ + } + + // 3. Return result. + return result +} + +/** + * A faster collectASequenceOfCodePoints that only works when comparing a single character. + * @param {string} char + * @param {string} input + * @param {{ position: number }} position + */ +function collectASequenceOfCodePointsFast (char, input, position) { + const idx = input.indexOf(char, position.position) + const start = position.position + + if (idx === -1) { + position.position = input.length + return input.slice(start) + } + + position.position = idx + return input.slice(start, position.position) +} + +// https://url.spec.whatwg.org/#string-percent-decode +/** @param {string} input */ +function stringPercentDecode (input) { + // 1. Let bytes be the UTF-8 encoding of input. + const bytes = encoder.encode(input) + + // 2. Return the percent-decoding of bytes. + return percentDecode(bytes) +} + +// https://url.spec.whatwg.org/#percent-decode +/** @param {Uint8Array} input */ +function percentDecode (input) { + // 1. Let output be an empty byte sequence. + /** @type {number[]} */ + const output = [] + + // 2. For each byte byte in input: + for (let i = 0; i < input.length; i++) { + const byte = input[i] + + // 1. If byte is not 0x25 (%), then append byte to output. + if (byte !== 0x25) { + output.push(byte) + + // 2. Otherwise, if byte is 0x25 (%) and the next two bytes + // after byte in input are not in the ranges + // 0x30 (0) to 0x39 (9), 0x41 (A) to 0x46 (F), + // and 0x61 (a) to 0x66 (f), all inclusive, append byte + // to output. + } else if ( + byte === 0x25 && + !/^[0-9A-Fa-f]{2}$/i.test(String.fromCharCode(input[i + 1], input[i + 2])) + ) { + output.push(0x25) + + // 3. Otherwise: + } else { + // 1. Let bytePoint be the two bytes after byte in input, + // decoded, and then interpreted as hexadecimal number. + const nextTwoBytes = String.fromCharCode(input[i + 1], input[i + 2]) + const bytePoint = Number.parseInt(nextTwoBytes, 16) + + // 2. Append a byte whose value is bytePoint to output. + output.push(bytePoint) + + // 3. Skip the next two bytes in input. + i += 2 + } + } + + // 3. Return output. + return Uint8Array.from(output) +} + +// https://mimesniff.spec.whatwg.org/#parse-a-mime-type +/** @param {string} input */ +function parseMIMEType (input) { + // 1. Remove any leading and trailing HTTP whitespace + // from input. + input = removeHTTPWhitespace(input, true, true) + + // 2. Let position be a position variable for input, + // initially pointing at the start of input. + const position = { position: 0 } + + // 3. Let type be the result of collecting a sequence + // of code points that are not U+002F (/) from + // input, given position. + const type = collectASequenceOfCodePointsFast( + '/', + input, + position + ) + + // 4. If type is the empty string or does not solely + // contain HTTP token code points, then return failure. + // https://mimesniff.spec.whatwg.org/#http-token-code-point + if (type.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(type)) { + return 'failure' + } + + // 5. If position is past the end of input, then return + // failure + if (position.position > input.length) { + return 'failure' + } + + // 6. Advance position by 1. (This skips past U+002F (/).) + position.position++ + + // 7. Let subtype be the result of collecting a sequence of + // code points that are not U+003B (;) from input, given + // position. + let subtype = collectASequenceOfCodePointsFast( + ';', + input, + position + ) + + // 8. Remove any trailing HTTP whitespace from subtype. + subtype = removeHTTPWhitespace(subtype, false, true) + + // 9. If subtype is the empty string or does not solely + // contain HTTP token code points, then return failure. + if (subtype.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(subtype)) { + return 'failure' + } + + const typeLowercase = type.toLowerCase() + const subtypeLowercase = subtype.toLowerCase() + + // 10. Let mimeType be a new MIME type record whose type + // is type, in ASCII lowercase, and subtype is subtype, + // in ASCII lowercase. + // https://mimesniff.spec.whatwg.org/#mime-type + const mimeType = { + type: typeLowercase, + subtype: subtypeLowercase, + /** @type {Map} */ + parameters: new Map(), + // https://mimesniff.spec.whatwg.org/#mime-type-essence + essence: `${typeLowercase}/${subtypeLowercase}` + } + + // 11. While position is not past the end of input: + while (position.position < input.length) { + // 1. Advance position by 1. (This skips past U+003B (;).) + position.position++ + + // 2. Collect a sequence of code points that are HTTP + // whitespace from input given position. + collectASequenceOfCodePoints( + // https://fetch.spec.whatwg.org/#http-whitespace + char => HTTP_WHITESPACE_REGEX.test(char), + input, + position + ) + + // 3. Let parameterName be the result of collecting a + // sequence of code points that are not U+003B (;) + // or U+003D (=) from input, given position. + let parameterName = collectASequenceOfCodePoints( + (char) => char !== ';' && char !== '=', + input, + position + ) + + // 4. Set parameterName to parameterName, in ASCII + // lowercase. + parameterName = parameterName.toLowerCase() + + // 5. If position is not past the end of input, then: + if (position.position < input.length) { + // 1. If the code point at position within input is + // U+003B (;), then continue. + if (input[position.position] === ';') { + continue + } + + // 2. Advance position by 1. (This skips past U+003D (=).) + position.position++ + } + + // 6. If position is past the end of input, then break. + if (position.position > input.length) { + break + } + + // 7. Let parameterValue be null. + let parameterValue = null + + // 8. If the code point at position within input is + // U+0022 ("), then: + if (input[position.position] === '"') { + // 1. Set parameterValue to the result of collecting + // an HTTP quoted string from input, given position + // and the extract-value flag. + parameterValue = collectAnHTTPQuotedString(input, position, true) + + // 2. Collect a sequence of code points that are not + // U+003B (;) from input, given position. + collectASequenceOfCodePointsFast( + ';', + input, + position + ) + + // 9. Otherwise: + } else { + // 1. Set parameterValue to the result of collecting + // a sequence of code points that are not U+003B (;) + // from input, given position. + parameterValue = collectASequenceOfCodePointsFast( + ';', + input, + position + ) + + // 2. Remove any trailing HTTP whitespace from parameterValue. + parameterValue = removeHTTPWhitespace(parameterValue, false, true) + + // 3. If parameterValue is the empty string, then continue. + if (parameterValue.length === 0) { + continue + } + } + + // 10. If all of the following are true + // - parameterName is not the empty string + // - parameterName solely contains HTTP token code points + // - parameterValue solely contains HTTP quoted-string token code points + // - mimeType’s parameters[parameterName] does not exist + // then set mimeType’s parameters[parameterName] to parameterValue. + if ( + parameterName.length !== 0 && + HTTP_TOKEN_CODEPOINTS.test(parameterName) && + (parameterValue.length === 0 || HTTP_QUOTED_STRING_TOKENS.test(parameterValue)) && + !mimeType.parameters.has(parameterName) + ) { + mimeType.parameters.set(parameterName, parameterValue) + } + } + + // 12. Return mimeType. + return mimeType +} + +// https://infra.spec.whatwg.org/#forgiving-base64-decode +/** @param {string} data */ +function forgivingBase64 (data) { + // 1. Remove all ASCII whitespace from data. + data = data.replace(/[\u0009\u000A\u000C\u000D\u0020]/g, '') // eslint-disable-line + + // 2. If data’s code point length divides by 4 leaving + // no remainder, then: + if (data.length % 4 === 0) { + // 1. If data ends with one or two U+003D (=) code points, + // then remove them from data. + data = data.replace(/=?=$/, '') + } + + // 3. If data’s code point length divides by 4 leaving + // a remainder of 1, then return failure. + if (data.length % 4 === 1) { + return 'failure' + } + + // 4. If data contains a code point that is not one of + // U+002B (+) + // U+002F (/) + // ASCII alphanumeric + // then return failure. + if (/[^+/0-9A-Za-z]/.test(data)) { + return 'failure' + } + + const binary = atob(data) + const bytes = new Uint8Array(binary.length) + + for (let byte = 0; byte < binary.length; byte++) { + bytes[byte] = binary.charCodeAt(byte) + } + + return bytes +} + +// https://fetch.spec.whatwg.org/#collect-an-http-quoted-string +// tests: https://fetch.spec.whatwg.org/#example-http-quoted-string +/** + * @param {string} input + * @param {{ position: number }} position + * @param {boolean?} extractValue + */ +function collectAnHTTPQuotedString (input, position, extractValue) { + // 1. Let positionStart be position. + const positionStart = position.position + + // 2. Let value be the empty string. + let value = '' + + // 3. Assert: the code point at position within input + // is U+0022 ("). + assert(input[position.position] === '"') + + // 4. Advance position by 1. + position.position++ + + // 5. While true: + while (true) { + // 1. Append the result of collecting a sequence of code points + // that are not U+0022 (") or U+005C (\) from input, given + // position, to value. + value += collectASequenceOfCodePoints( + (char) => char !== '"' && char !== '\\', + input, + position + ) + + // 2. If position is past the end of input, then break. + if (position.position >= input.length) { + break + } + + // 3. Let quoteOrBackslash be the code point at position within + // input. + const quoteOrBackslash = input[position.position] + + // 4. Advance position by 1. + position.position++ + + // 5. If quoteOrBackslash is U+005C (\), then: + if (quoteOrBackslash === '\\') { + // 1. If position is past the end of input, then append + // U+005C (\) to value and break. + if (position.position >= input.length) { + value += '\\' + break + } + + // 2. Append the code point at position within input to value. + value += input[position.position] + + // 3. Advance position by 1. + position.position++ + + // 6. Otherwise: + } else { + // 1. Assert: quoteOrBackslash is U+0022 ("). + assert(quoteOrBackslash === '"') + + // 2. Break. + break + } + } + + // 6. If the extract-value flag is set, then return value. + if (extractValue) { + return value + } + + // 7. Return the code points from positionStart to position, + // inclusive, within input. + return input.slice(positionStart, position.position) +} + +/** + * @see https://mimesniff.spec.whatwg.org/#serialize-a-mime-type + */ +function serializeAMimeType (mimeType) { + assert(mimeType !== 'failure') + const { parameters, essence } = mimeType + + // 1. Let serialization be the concatenation of mimeType’s + // type, U+002F (/), and mimeType’s subtype. + let serialization = essence + + // 2. For each name → value of mimeType’s parameters: + for (let [name, value] of parameters.entries()) { + // 1. Append U+003B (;) to serialization. + serialization += ';' + + // 2. Append name to serialization. + serialization += name + + // 3. Append U+003D (=) to serialization. + serialization += '=' + + // 4. If value does not solely contain HTTP token code + // points or value is the empty string, then: + if (!HTTP_TOKEN_CODEPOINTS.test(value)) { + // 1. Precede each occurence of U+0022 (") or + // U+005C (\) in value with U+005C (\). + value = value.replace(/(\\|")/g, '\\$1') + + // 2. Prepend U+0022 (") to value. + value = '"' + value + + // 3. Append U+0022 (") to value. + value += '"' + } + + // 5. Append value to serialization. + serialization += value + } + + // 3. Return serialization. + return serialization +} + +/** + * @see https://fetch.spec.whatwg.org/#http-whitespace + * @param {string} char + */ +function isHTTPWhiteSpace (char) { + return char === '\r' || char === '\n' || char === '\t' || char === ' ' +} + +/** + * @see https://fetch.spec.whatwg.org/#http-whitespace + * @param {string} str + */ +function removeHTTPWhitespace (str, leading = true, trailing = true) { + let lead = 0 + let trail = str.length - 1 + + if (leading) { + for (; lead < str.length && isHTTPWhiteSpace(str[lead]); lead++); + } + + if (trailing) { + for (; trail > 0 && isHTTPWhiteSpace(str[trail]); trail--); + } + + return str.slice(lead, trail + 1) +} + +/** + * @see https://infra.spec.whatwg.org/#ascii-whitespace + * @param {string} char + */ +function isASCIIWhitespace (char) { + return char === '\r' || char === '\n' || char === '\t' || char === '\f' || char === ' ' +} + +/** + * @see https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace + */ +function removeASCIIWhitespace (str, leading = true, trailing = true) { + let lead = 0 + let trail = str.length - 1 + + if (leading) { + for (; lead < str.length && isASCIIWhitespace(str[lead]); lead++); + } + + if (trailing) { + for (; trail > 0 && isASCIIWhitespace(str[trail]); trail--); + } + + return str.slice(lead, trail + 1) +} + +module.exports = { + dataURLProcessor, + URLSerializer, + collectASequenceOfCodePoints, + collectASequenceOfCodePointsFast, + stringPercentDecode, + parseMIMEType, + collectAnHTTPQuotedString, + serializeAMimeType +} + + +/***/ }), + +/***/ 8511: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { Blob, File: NativeFile } = __nccwpck_require__(4300) +const { types } = __nccwpck_require__(3837) +const { kState } = __nccwpck_require__(5861) +const { isBlobLike } = __nccwpck_require__(2538) +const { webidl } = __nccwpck_require__(1744) +const { parseMIMEType, serializeAMimeType } = __nccwpck_require__(685) +const { kEnumerableProperty } = __nccwpck_require__(3983) +const encoder = new TextEncoder() + +class File extends Blob { + constructor (fileBits, fileName, options = {}) { + // The File constructor is invoked with two or three parameters, depending + // on whether the optional dictionary parameter is used. When the File() + // constructor is invoked, user agents must run the following steps: + webidl.argumentLengthCheck(arguments, 2, { header: 'File constructor' }) + + fileBits = webidl.converters['sequence'](fileBits) + fileName = webidl.converters.USVString(fileName) + options = webidl.converters.FilePropertyBag(options) + + // 1. Let bytes be the result of processing blob parts given fileBits and + // options. + // Note: Blob handles this for us + + // 2. Let n be the fileName argument to the constructor. + const n = fileName + + // 3. Process FilePropertyBag dictionary argument by running the following + // substeps: + + // 1. If the type member is provided and is not the empty string, let t + // be set to the type dictionary member. If t contains any characters + // outside the range U+0020 to U+007E, then set t to the empty string + // and return from these substeps. + // 2. Convert every character in t to ASCII lowercase. + let t = options.type + let d + + // eslint-disable-next-line no-labels + substep: { + if (t) { + t = parseMIMEType(t) + + if (t === 'failure') { + t = '' + // eslint-disable-next-line no-labels + break substep + } + + t = serializeAMimeType(t).toLowerCase() + } + + // 3. If the lastModified member is provided, let d be set to the + // lastModified dictionary member. If it is not provided, set d to the + // current date and time represented as the number of milliseconds since + // the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]). + d = options.lastModified + } + + // 4. Return a new File object F such that: + // F refers to the bytes byte sequence. + // F.size is set to the number of total bytes in bytes. + // F.name is set to n. + // F.type is set to t. + // F.lastModified is set to d. + + super(processBlobParts(fileBits, options), { type: t }) + this[kState] = { + name: n, + lastModified: d, + type: t + } + } + + get name () { + webidl.brandCheck(this, File) + + return this[kState].name + } + + get lastModified () { + webidl.brandCheck(this, File) + + return this[kState].lastModified + } + + get type () { + webidl.brandCheck(this, File) + + return this[kState].type + } +} + +class FileLike { + constructor (blobLike, fileName, options = {}) { + // TODO: argument idl type check + + // The File constructor is invoked with two or three parameters, depending + // on whether the optional dictionary parameter is used. When the File() + // constructor is invoked, user agents must run the following steps: + + // 1. Let bytes be the result of processing blob parts given fileBits and + // options. + + // 2. Let n be the fileName argument to the constructor. + const n = fileName + + // 3. Process FilePropertyBag dictionary argument by running the following + // substeps: + + // 1. If the type member is provided and is not the empty string, let t + // be set to the type dictionary member. If t contains any characters + // outside the range U+0020 to U+007E, then set t to the empty string + // and return from these substeps. + // TODO + const t = options.type + + // 2. Convert every character in t to ASCII lowercase. + // TODO + + // 3. If the lastModified member is provided, let d be set to the + // lastModified dictionary member. If it is not provided, set d to the + // current date and time represented as the number of milliseconds since + // the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]). + const d = options.lastModified ?? Date.now() + + // 4. Return a new File object F such that: + // F refers to the bytes byte sequence. + // F.size is set to the number of total bytes in bytes. + // F.name is set to n. + // F.type is set to t. + // F.lastModified is set to d. + + this[kState] = { + blobLike, + name: n, + type: t, + lastModified: d + } + } + + stream (...args) { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.stream(...args) + } + + arrayBuffer (...args) { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.arrayBuffer(...args) + } + + slice (...args) { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.slice(...args) + } + + text (...args) { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.text(...args) + } + + get size () { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.size + } + + get type () { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.type + } + + get name () { + webidl.brandCheck(this, FileLike) + + return this[kState].name + } + + get lastModified () { + webidl.brandCheck(this, FileLike) + + return this[kState].lastModified + } + + get [Symbol.toStringTag] () { + return 'File' + } +} + +Object.defineProperties(File.prototype, { + [Symbol.toStringTag]: { + value: 'File', + configurable: true + }, + name: kEnumerableProperty, + lastModified: kEnumerableProperty +}) + +webidl.converters.Blob = webidl.interfaceConverter(Blob) + +webidl.converters.BlobPart = function (V, opts) { + if (webidl.util.Type(V) === 'Object') { + if (isBlobLike(V)) { + return webidl.converters.Blob(V, { strict: false }) + } + + if ( + ArrayBuffer.isView(V) || + types.isAnyArrayBuffer(V) + ) { + return webidl.converters.BufferSource(V, opts) + } + } + + return webidl.converters.USVString(V, opts) +} + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.BlobPart +) + +// https://www.w3.org/TR/FileAPI/#dfn-FilePropertyBag +webidl.converters.FilePropertyBag = webidl.dictionaryConverter([ + { + key: 'lastModified', + converter: webidl.converters['long long'], + get defaultValue () { + return Date.now() + } + }, + { + key: 'type', + converter: webidl.converters.DOMString, + defaultValue: '' + }, + { + key: 'endings', + converter: (value) => { + value = webidl.converters.DOMString(value) + value = value.toLowerCase() + + if (value !== 'native') { + value = 'transparent' + } + + return value + }, + defaultValue: 'transparent' + } +]) + +/** + * @see https://www.w3.org/TR/FileAPI/#process-blob-parts + * @param {(NodeJS.TypedArray|Blob|string)[]} parts + * @param {{ type: string, endings: string }} options + */ +function processBlobParts (parts, options) { + // 1. Let bytes be an empty sequence of bytes. + /** @type {NodeJS.TypedArray[]} */ + const bytes = [] + + // 2. For each element in parts: + for (const element of parts) { + // 1. If element is a USVString, run the following substeps: + if (typeof element === 'string') { + // 1. Let s be element. + let s = element + + // 2. If the endings member of options is "native", set s + // to the result of converting line endings to native + // of element. + if (options.endings === 'native') { + s = convertLineEndingsNative(s) + } + + // 3. Append the result of UTF-8 encoding s to bytes. + bytes.push(encoder.encode(s)) + } else if ( + types.isAnyArrayBuffer(element) || + types.isTypedArray(element) + ) { + // 2. If element is a BufferSource, get a copy of the + // bytes held by the buffer source, and append those + // bytes to bytes. + if (!element.buffer) { // ArrayBuffer + bytes.push(new Uint8Array(element)) + } else { + bytes.push( + new Uint8Array(element.buffer, element.byteOffset, element.byteLength) + ) + } + } else if (isBlobLike(element)) { + // 3. If element is a Blob, append the bytes it represents + // to bytes. + bytes.push(element) + } + } + + // 3. Return bytes. + return bytes +} + +/** + * @see https://www.w3.org/TR/FileAPI/#convert-line-endings-to-native + * @param {string} s + */ +function convertLineEndingsNative (s) { + // 1. Let native line ending be be the code point U+000A LF. + let nativeLineEnding = '\n' + + // 2. If the underlying platform’s conventions are to + // represent newlines as a carriage return and line feed + // sequence, set native line ending to the code point + // U+000D CR followed by the code point U+000A LF. + if (process.platform === 'win32') { + nativeLineEnding = '\r\n' + } + + return s.replace(/\r?\n/g, nativeLineEnding) +} + +// If this function is moved to ./util.js, some tools (such as +// rollup) will warn about circular dependencies. See: +// https://github.com/nodejs/undici/issues/1629 +function isFileLike (object) { + return ( + (NativeFile && object instanceof NativeFile) || + object instanceof File || ( + object && + (typeof object.stream === 'function' || + typeof object.arrayBuffer === 'function') && + object[Symbol.toStringTag] === 'File' + ) + ) +} + +module.exports = { File, FileLike, isFileLike } + + +/***/ }), + +/***/ 2015: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { isBlobLike, toUSVString, makeIterator } = __nccwpck_require__(2538) +const { kState } = __nccwpck_require__(5861) +const { File: UndiciFile, FileLike, isFileLike } = __nccwpck_require__(8511) +const { webidl } = __nccwpck_require__(1744) +const { Blob, File: NativeFile } = __nccwpck_require__(4300) + +/** @type {globalThis['File']} */ +const File = NativeFile ?? UndiciFile + +// https://xhr.spec.whatwg.org/#formdata +class FormData { + constructor (form) { + if (form !== undefined) { + throw webidl.errors.conversionFailed({ + prefix: 'FormData constructor', + argument: 'Argument 1', + types: ['undefined'] + }) + } + + this[kState] = [] + } + + append (name, value, filename = undefined) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.append' }) + + if (arguments.length === 3 && !isBlobLike(value)) { + throw new TypeError( + "Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'" + ) + } + + // 1. Let value be value if given; otherwise blobValue. + + name = webidl.converters.USVString(name) + value = isBlobLike(value) + ? webidl.converters.Blob(value, { strict: false }) + : webidl.converters.USVString(value) + filename = arguments.length === 3 + ? webidl.converters.USVString(filename) + : undefined + + // 2. Let entry be the result of creating an entry with + // name, value, and filename if given. + const entry = makeEntry(name, value, filename) + + // 3. Append entry to this’s entry list. + this[kState].push(entry) + } + + delete (name) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.delete' }) + + name = webidl.converters.USVString(name) + + // The delete(name) method steps are to remove all entries whose name + // is name from this’s entry list. + this[kState] = this[kState].filter(entry => entry.name !== name) + } + + get (name) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.get' }) + + name = webidl.converters.USVString(name) + + // 1. If there is no entry whose name is name in this’s entry list, + // then return null. + const idx = this[kState].findIndex((entry) => entry.name === name) + if (idx === -1) { + return null + } + + // 2. Return the value of the first entry whose name is name from + // this’s entry list. + return this[kState][idx].value + } + + getAll (name) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.getAll' }) + + name = webidl.converters.USVString(name) + + // 1. If there is no entry whose name is name in this’s entry list, + // then return the empty list. + // 2. Return the values of all entries whose name is name, in order, + // from this’s entry list. + return this[kState] + .filter((entry) => entry.name === name) + .map((entry) => entry.value) + } + + has (name) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.has' }) + + name = webidl.converters.USVString(name) + + // The has(name) method steps are to return true if there is an entry + // whose name is name in this’s entry list; otherwise false. + return this[kState].findIndex((entry) => entry.name === name) !== -1 + } + + set (name, value, filename = undefined) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.set' }) + + if (arguments.length === 3 && !isBlobLike(value)) { + throw new TypeError( + "Failed to execute 'set' on 'FormData': parameter 2 is not of type 'Blob'" + ) + } + + // The set(name, value) and set(name, blobValue, filename) method steps + // are: + + // 1. Let value be value if given; otherwise blobValue. + + name = webidl.converters.USVString(name) + value = isBlobLike(value) + ? webidl.converters.Blob(value, { strict: false }) + : webidl.converters.USVString(value) + filename = arguments.length === 3 + ? toUSVString(filename) + : undefined + + // 2. Let entry be the result of creating an entry with name, value, and + // filename if given. + const entry = makeEntry(name, value, filename) + + // 3. If there are entries in this’s entry list whose name is name, then + // replace the first such entry with entry and remove the others. + const idx = this[kState].findIndex((entry) => entry.name === name) + if (idx !== -1) { + this[kState] = [ + ...this[kState].slice(0, idx), + entry, + ...this[kState].slice(idx + 1).filter((entry) => entry.name !== name) + ] + } else { + // 4. Otherwise, append entry to this’s entry list. + this[kState].push(entry) + } + } + + entries () { + webidl.brandCheck(this, FormData) + + return makeIterator( + () => this[kState].map(pair => [pair.name, pair.value]), + 'FormData', + 'key+value' + ) + } + + keys () { + webidl.brandCheck(this, FormData) + + return makeIterator( + () => this[kState].map(pair => [pair.name, pair.value]), + 'FormData', + 'key' + ) + } + + values () { + webidl.brandCheck(this, FormData) + + return makeIterator( + () => this[kState].map(pair => [pair.name, pair.value]), + 'FormData', + 'value' + ) + } + + /** + * @param {(value: string, key: string, self: FormData) => void} callbackFn + * @param {unknown} thisArg + */ + forEach (callbackFn, thisArg = globalThis) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.forEach' }) + + if (typeof callbackFn !== 'function') { + throw new TypeError( + "Failed to execute 'forEach' on 'FormData': parameter 1 is not of type 'Function'." + ) + } + + for (const [key, value] of this) { + callbackFn.apply(thisArg, [value, key, this]) + } + } +} + +FormData.prototype[Symbol.iterator] = FormData.prototype.entries + +Object.defineProperties(FormData.prototype, { + [Symbol.toStringTag]: { + value: 'FormData', + configurable: true + } +}) + +/** + * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry + * @param {string} name + * @param {string|Blob} value + * @param {?string} filename + * @returns + */ +function makeEntry (name, value, filename) { + // 1. Set name to the result of converting name into a scalar value string. + // "To convert a string into a scalar value string, replace any surrogates + // with U+FFFD." + // see: https://nodejs.org/dist/latest-v18.x/docs/api/buffer.html#buftostringencoding-start-end + name = Buffer.from(name).toString('utf8') + + // 2. If value is a string, then set value to the result of converting + // value into a scalar value string. + if (typeof value === 'string') { + value = Buffer.from(value).toString('utf8') + } else { + // 3. Otherwise: + + // 1. If value is not a File object, then set value to a new File object, + // representing the same bytes, whose name attribute value is "blob" + if (!isFileLike(value)) { + value = value instanceof Blob + ? new File([value], 'blob', { type: value.type }) + : new FileLike(value, 'blob', { type: value.type }) + } + + // 2. If filename is given, then set value to a new File object, + // representing the same bytes, whose name attribute is filename. + if (filename !== undefined) { + /** @type {FilePropertyBag} */ + const options = { + type: value.type, + lastModified: value.lastModified + } + + value = (NativeFile && value instanceof NativeFile) || value instanceof UndiciFile + ? new File([value], filename, options) + : new FileLike(value, filename, options) + } + } + + // 4. Return an entry whose name is name and whose value is value. + return { name, value } +} + +module.exports = { FormData } + + +/***/ }), + +/***/ 1246: +/***/ ((module) => { + +"use strict"; + + +// In case of breaking changes, increase the version +// number to avoid conflicts. +const globalOrigin = Symbol.for('undici.globalOrigin.1') + +function getGlobalOrigin () { + return globalThis[globalOrigin] +} + +function setGlobalOrigin (newOrigin) { + if (newOrigin === undefined) { + Object.defineProperty(globalThis, globalOrigin, { + value: undefined, + writable: true, + enumerable: false, + configurable: false + }) + + return + } + + const parsedURL = new URL(newOrigin) + + if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:') { + throw new TypeError(`Only http & https urls are allowed, received ${parsedURL.protocol}`) + } + + Object.defineProperty(globalThis, globalOrigin, { + value: parsedURL, + writable: true, + enumerable: false, + configurable: false + }) +} + +module.exports = { + getGlobalOrigin, + setGlobalOrigin +} + + +/***/ }), + +/***/ 554: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +// https://github.com/Ethan-Arrowood/undici-fetch + + + +const { kHeadersList, kConstruct } = __nccwpck_require__(2785) +const { kGuard } = __nccwpck_require__(5861) +const { kEnumerableProperty } = __nccwpck_require__(3983) +const { + makeIterator, + isValidHeaderName, + isValidHeaderValue +} = __nccwpck_require__(2538) +const { webidl } = __nccwpck_require__(1744) +const assert = __nccwpck_require__(9491) + +const kHeadersMap = Symbol('headers map') +const kHeadersSortedMap = Symbol('headers map sorted') + +/** + * @param {number} code + */ +function isHTTPWhiteSpaceCharCode (code) { + return code === 0x00a || code === 0x00d || code === 0x009 || code === 0x020 +} + +/** + * @see https://fetch.spec.whatwg.org/#concept-header-value-normalize + * @param {string} potentialValue + */ +function headerValueNormalize (potentialValue) { + // To normalize a byte sequence potentialValue, remove + // any leading and trailing HTTP whitespace bytes from + // potentialValue. + let i = 0; let j = potentialValue.length + + while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(j - 1))) --j + while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i + + return i === 0 && j === potentialValue.length ? potentialValue : potentialValue.substring(i, j) +} + +function fill (headers, object) { + // To fill a Headers object headers with a given object object, run these steps: + + // 1. If object is a sequence, then for each header in object: + // Note: webidl conversion to array has already been done. + if (Array.isArray(object)) { + for (let i = 0; i < object.length; ++i) { + const header = object[i] + // 1. If header does not contain exactly two items, then throw a TypeError. + if (header.length !== 2) { + throw webidl.errors.exception({ + header: 'Headers constructor', + message: `expected name/value pair to be length 2, found ${header.length}.` + }) + } + + // 2. Append (header’s first item, header’s second item) to headers. + appendHeader(headers, header[0], header[1]) + } + } else if (typeof object === 'object' && object !== null) { + // Note: null should throw + + // 2. Otherwise, object is a record, then for each key → value in object, + // append (key, value) to headers + const keys = Object.keys(object) + for (let i = 0; i < keys.length; ++i) { + appendHeader(headers, keys[i], object[keys[i]]) + } + } else { + throw webidl.errors.conversionFailed({ + prefix: 'Headers constructor', + argument: 'Argument 1', + types: ['sequence>', 'record'] + }) + } +} + +/** + * @see https://fetch.spec.whatwg.org/#concept-headers-append + */ +function appendHeader (headers, name, value) { + // 1. Normalize value. + value = headerValueNormalize(value) + + // 2. If name is not a header name or value is not a + // header value, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.append', + value: name, + type: 'header name' + }) + } else if (!isValidHeaderValue(value)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.append', + value, + type: 'header value' + }) + } + + // 3. If headers’s guard is "immutable", then throw a TypeError. + // 4. Otherwise, if headers’s guard is "request" and name is a + // forbidden header name, return. + // Note: undici does not implement forbidden header names + if (headers[kGuard] === 'immutable') { + throw new TypeError('immutable') + } else if (headers[kGuard] === 'request-no-cors') { + // 5. Otherwise, if headers’s guard is "request-no-cors": + // TODO + } + + // 6. Otherwise, if headers’s guard is "response" and name is a + // forbidden response-header name, return. + + // 7. Append (name, value) to headers’s header list. + return headers[kHeadersList].append(name, value) + + // 8. If headers’s guard is "request-no-cors", then remove + // privileged no-CORS request headers from headers +} + +class HeadersList { + /** @type {[string, string][]|null} */ + cookies = null + + constructor (init) { + if (init instanceof HeadersList) { + this[kHeadersMap] = new Map(init[kHeadersMap]) + this[kHeadersSortedMap] = init[kHeadersSortedMap] + this.cookies = init.cookies === null ? null : [...init.cookies] + } else { + this[kHeadersMap] = new Map(init) + this[kHeadersSortedMap] = null + } + } + + // https://fetch.spec.whatwg.org/#header-list-contains + contains (name) { + // A header list list contains a header name name if list + // contains a header whose name is a byte-case-insensitive + // match for name. + name = name.toLowerCase() + + return this[kHeadersMap].has(name) + } + + clear () { + this[kHeadersMap].clear() + this[kHeadersSortedMap] = null + this.cookies = null + } + + // https://fetch.spec.whatwg.org/#concept-header-list-append + append (name, value) { + this[kHeadersSortedMap] = null + + // 1. If list contains name, then set name to the first such + // header’s name. + const lowercaseName = name.toLowerCase() + const exists = this[kHeadersMap].get(lowercaseName) + + // 2. Append (name, value) to list. + if (exists) { + const delimiter = lowercaseName === 'cookie' ? '; ' : ', ' + this[kHeadersMap].set(lowercaseName, { + name: exists.name, + value: `${exists.value}${delimiter}${value}` + }) + } else { + this[kHeadersMap].set(lowercaseName, { name, value }) + } + + if (lowercaseName === 'set-cookie') { + this.cookies ??= [] + this.cookies.push(value) + } + } + + // https://fetch.spec.whatwg.org/#concept-header-list-set + set (name, value) { + this[kHeadersSortedMap] = null + const lowercaseName = name.toLowerCase() + + if (lowercaseName === 'set-cookie') { + this.cookies = [value] + } + + // 1. If list contains name, then set the value of + // the first such header to value and remove the + // others. + // 2. Otherwise, append header (name, value) to list. + this[kHeadersMap].set(lowercaseName, { name, value }) + } + + // https://fetch.spec.whatwg.org/#concept-header-list-delete + delete (name) { + this[kHeadersSortedMap] = null + + name = name.toLowerCase() + + if (name === 'set-cookie') { + this.cookies = null + } + + this[kHeadersMap].delete(name) + } + + // https://fetch.spec.whatwg.org/#concept-header-list-get + get (name) { + const value = this[kHeadersMap].get(name.toLowerCase()) + + // 1. If list does not contain name, then return null. + // 2. Return the values of all headers in list whose name + // is a byte-case-insensitive match for name, + // separated from each other by 0x2C 0x20, in order. + return value === undefined ? null : value.value + } + + * [Symbol.iterator] () { + // use the lowercased name + for (const [name, { value }] of this[kHeadersMap]) { + yield [name, value] + } + } + + get entries () { + const headers = {} + + if (this[kHeadersMap].size) { + for (const { name, value } of this[kHeadersMap].values()) { + headers[name] = value + } + } + + return headers + } +} + +// https://fetch.spec.whatwg.org/#headers-class +class Headers { + constructor (init = undefined) { + if (init === kConstruct) { + return + } + this[kHeadersList] = new HeadersList() + + // The new Headers(init) constructor steps are: + + // 1. Set this’s guard to "none". + this[kGuard] = 'none' + + // 2. If init is given, then fill this with init. + if (init !== undefined) { + init = webidl.converters.HeadersInit(init) + fill(this, init) + } + } + + // https://fetch.spec.whatwg.org/#dom-headers-append + append (name, value) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.append' }) + + name = webidl.converters.ByteString(name) + value = webidl.converters.ByteString(value) + + return appendHeader(this, name, value) + } + + // https://fetch.spec.whatwg.org/#dom-headers-delete + delete (name) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.delete' }) + + name = webidl.converters.ByteString(name) + + // 1. If name is not a header name, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.delete', + value: name, + type: 'header name' + }) + } + + // 2. If this’s guard is "immutable", then throw a TypeError. + // 3. Otherwise, if this’s guard is "request" and name is a + // forbidden header name, return. + // 4. Otherwise, if this’s guard is "request-no-cors", name + // is not a no-CORS-safelisted request-header name, and + // name is not a privileged no-CORS request-header name, + // return. + // 5. Otherwise, if this’s guard is "response" and name is + // a forbidden response-header name, return. + // Note: undici does not implement forbidden header names + if (this[kGuard] === 'immutable') { + throw new TypeError('immutable') + } else if (this[kGuard] === 'request-no-cors') { + // TODO + } + + // 6. If this’s header list does not contain name, then + // return. + if (!this[kHeadersList].contains(name)) { + return + } + + // 7. Delete name from this’s header list. + // 8. If this’s guard is "request-no-cors", then remove + // privileged no-CORS request headers from this. + this[kHeadersList].delete(name) + } + + // https://fetch.spec.whatwg.org/#dom-headers-get + get (name) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.get' }) + + name = webidl.converters.ByteString(name) + + // 1. If name is not a header name, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.get', + value: name, + type: 'header name' + }) + } + + // 2. Return the result of getting name from this’s header + // list. + return this[kHeadersList].get(name) + } + + // https://fetch.spec.whatwg.org/#dom-headers-has + has (name) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.has' }) + + name = webidl.converters.ByteString(name) + + // 1. If name is not a header name, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.has', + value: name, + type: 'header name' + }) + } + + // 2. Return true if this’s header list contains name; + // otherwise false. + return this[kHeadersList].contains(name) + } + + // https://fetch.spec.whatwg.org/#dom-headers-set + set (name, value) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.set' }) + + name = webidl.converters.ByteString(name) + value = webidl.converters.ByteString(value) + + // 1. Normalize value. + value = headerValueNormalize(value) + + // 2. If name is not a header name or value is not a + // header value, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.set', + value: name, + type: 'header name' + }) + } else if (!isValidHeaderValue(value)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.set', + value, + type: 'header value' + }) + } + + // 3. If this’s guard is "immutable", then throw a TypeError. + // 4. Otherwise, if this’s guard is "request" and name is a + // forbidden header name, return. + // 5. Otherwise, if this’s guard is "request-no-cors" and + // name/value is not a no-CORS-safelisted request-header, + // return. + // 6. Otherwise, if this’s guard is "response" and name is a + // forbidden response-header name, return. + // Note: undici does not implement forbidden header names + if (this[kGuard] === 'immutable') { + throw new TypeError('immutable') + } else if (this[kGuard] === 'request-no-cors') { + // TODO + } + + // 7. Set (name, value) in this’s header list. + // 8. If this’s guard is "request-no-cors", then remove + // privileged no-CORS request headers from this + this[kHeadersList].set(name, value) + } + + // https://fetch.spec.whatwg.org/#dom-headers-getsetcookie + getSetCookie () { + webidl.brandCheck(this, Headers) + + // 1. If this’s header list does not contain `Set-Cookie`, then return « ». + // 2. Return the values of all headers in this’s header list whose name is + // a byte-case-insensitive match for `Set-Cookie`, in order. + + const list = this[kHeadersList].cookies + + if (list) { + return [...list] + } + + return [] + } + + // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine + get [kHeadersSortedMap] () { + if (this[kHeadersList][kHeadersSortedMap]) { + return this[kHeadersList][kHeadersSortedMap] + } + + // 1. Let headers be an empty list of headers with the key being the name + // and value the value. + const headers = [] + + // 2. Let names be the result of convert header names to a sorted-lowercase + // set with all the names of the headers in list. + const names = [...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1) + const cookies = this[kHeadersList].cookies + + // 3. For each name of names: + for (let i = 0; i < names.length; ++i) { + const [name, value] = names[i] + // 1. If name is `set-cookie`, then: + if (name === 'set-cookie') { + // 1. Let values be a list of all values of headers in list whose name + // is a byte-case-insensitive match for name, in order. + + // 2. For each value of values: + // 1. Append (name, value) to headers. + for (let j = 0; j < cookies.length; ++j) { + headers.push([name, cookies[j]]) + } + } else { + // 2. Otherwise: + + // 1. Let value be the result of getting name from list. + + // 2. Assert: value is non-null. + assert(value !== null) + + // 3. Append (name, value) to headers. + headers.push([name, value]) + } + } + + this[kHeadersList][kHeadersSortedMap] = headers + + // 4. Return headers. + return headers + } + + keys () { + webidl.brandCheck(this, Headers) + + if (this[kGuard] === 'immutable') { + const value = this[kHeadersSortedMap] + return makeIterator(() => value, 'Headers', + 'key') + } + + return makeIterator( + () => [...this[kHeadersSortedMap].values()], + 'Headers', + 'key' + ) + } + + values () { + webidl.brandCheck(this, Headers) + + if (this[kGuard] === 'immutable') { + const value = this[kHeadersSortedMap] + return makeIterator(() => value, 'Headers', + 'value') + } + + return makeIterator( + () => [...this[kHeadersSortedMap].values()], + 'Headers', + 'value' + ) + } + + entries () { + webidl.brandCheck(this, Headers) + + if (this[kGuard] === 'immutable') { + const value = this[kHeadersSortedMap] + return makeIterator(() => value, 'Headers', + 'key+value') + } + + return makeIterator( + () => [...this[kHeadersSortedMap].values()], + 'Headers', + 'key+value' + ) + } + + /** + * @param {(value: string, key: string, self: Headers) => void} callbackFn + * @param {unknown} thisArg + */ + forEach (callbackFn, thisArg = globalThis) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.forEach' }) + + if (typeof callbackFn !== 'function') { + throw new TypeError( + "Failed to execute 'forEach' on 'Headers': parameter 1 is not of type 'Function'." + ) + } + + for (const [key, value] of this) { + callbackFn.apply(thisArg, [value, key, this]) + } + } + + [Symbol.for('nodejs.util.inspect.custom')] () { + webidl.brandCheck(this, Headers) + + return this[kHeadersList] + } +} + +Headers.prototype[Symbol.iterator] = Headers.prototype.entries + +Object.defineProperties(Headers.prototype, { + append: kEnumerableProperty, + delete: kEnumerableProperty, + get: kEnumerableProperty, + has: kEnumerableProperty, + set: kEnumerableProperty, + getSetCookie: kEnumerableProperty, + keys: kEnumerableProperty, + values: kEnumerableProperty, + entries: kEnumerableProperty, + forEach: kEnumerableProperty, + [Symbol.iterator]: { enumerable: false }, + [Symbol.toStringTag]: { + value: 'Headers', + configurable: true + } +}) + +webidl.converters.HeadersInit = function (V) { + if (webidl.util.Type(V) === 'Object') { + if (V[Symbol.iterator]) { + return webidl.converters['sequence>'](V) + } + + return webidl.converters['record'](V) + } + + throw webidl.errors.conversionFailed({ + prefix: 'Headers constructor', + argument: 'Argument 1', + types: ['sequence>', 'record'] + }) +} + +module.exports = { + fill, + Headers, + HeadersList +} + + +/***/ }), + +/***/ 4881: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +// https://github.com/Ethan-Arrowood/undici-fetch + + + +const { + Response, + makeNetworkError, + makeAppropriateNetworkError, + filterResponse, + makeResponse +} = __nccwpck_require__(7823) +const { Headers } = __nccwpck_require__(554) +const { Request, makeRequest } = __nccwpck_require__(8359) +const zlib = __nccwpck_require__(9796) +const { + bytesMatch, + makePolicyContainer, + clonePolicyContainer, + requestBadPort, + TAOCheck, + appendRequestOriginHeader, + responseLocationURL, + requestCurrentURL, + setRequestReferrerPolicyOnRedirect, + tryUpgradeRequestToAPotentiallyTrustworthyURL, + createOpaqueTimingInfo, + appendFetchMetadata, + corsCheck, + crossOriginResourcePolicyCheck, + determineRequestsReferrer, + coarsenedSharedCurrentTime, + createDeferredPromise, + isBlobLike, + sameOrigin, + isCancelled, + isAborted, + isErrorLike, + fullyReadBody, + readableStreamClose, + isomorphicEncode, + urlIsLocal, + urlIsHttpHttpsScheme, + urlHasHttpsScheme +} = __nccwpck_require__(2538) +const { kState, kHeaders, kGuard, kRealm } = __nccwpck_require__(5861) +const assert = __nccwpck_require__(9491) +const { safelyExtractBody } = __nccwpck_require__(1472) +const { + redirectStatusSet, + nullBodyStatus, + safeMethodsSet, + requestBodyHeader, + subresourceSet, + DOMException +} = __nccwpck_require__(1037) +const { kHeadersList } = __nccwpck_require__(2785) +const EE = __nccwpck_require__(2361) +const { Readable, pipeline } = __nccwpck_require__(2781) +const { addAbortListener, isErrored, isReadable, nodeMajor, nodeMinor } = __nccwpck_require__(3983) +const { dataURLProcessor, serializeAMimeType } = __nccwpck_require__(685) +const { TransformStream } = __nccwpck_require__(5356) +const { getGlobalDispatcher } = __nccwpck_require__(1892) +const { webidl } = __nccwpck_require__(1744) +const { STATUS_CODES } = __nccwpck_require__(3685) +const GET_OR_HEAD = ['GET', 'HEAD'] + +/** @type {import('buffer').resolveObjectURL} */ +let resolveObjectURL +let ReadableStream = globalThis.ReadableStream + +class Fetch extends EE { + constructor (dispatcher) { + super() + + this.dispatcher = dispatcher + this.connection = null + this.dump = false + this.state = 'ongoing' + // 2 terminated listeners get added per request, + // but only 1 gets removed. If there are 20 redirects, + // 21 listeners will be added. + // See https://github.com/nodejs/undici/issues/1711 + // TODO (fix): Find and fix root cause for leaked listener. + this.setMaxListeners(21) + } + + terminate (reason) { + if (this.state !== 'ongoing') { + return + } + + this.state = 'terminated' + this.connection?.destroy(reason) + this.emit('terminated', reason) + } + + // https://fetch.spec.whatwg.org/#fetch-controller-abort + abort (error) { + if (this.state !== 'ongoing') { + return + } + + // 1. Set controller’s state to "aborted". + this.state = 'aborted' + + // 2. Let fallbackError be an "AbortError" DOMException. + // 3. Set error to fallbackError if it is not given. + if (!error) { + error = new DOMException('The operation was aborted.', 'AbortError') + } + + // 4. Let serializedError be StructuredSerialize(error). + // If that threw an exception, catch it, and let + // serializedError be StructuredSerialize(fallbackError). + + // 5. Set controller’s serialized abort reason to serializedError. + this.serializedAbortReason = error + + this.connection?.destroy(error) + this.emit('terminated', error) + } +} + +// https://fetch.spec.whatwg.org/#fetch-method +function fetch (input, init = {}) { + webidl.argumentLengthCheck(arguments, 1, { header: 'globalThis.fetch' }) + + // 1. Let p be a new promise. + const p = createDeferredPromise() + + // 2. Let requestObject be the result of invoking the initial value of + // Request as constructor with input and init as arguments. If this throws + // an exception, reject p with it and return p. + let requestObject + + try { + requestObject = new Request(input, init) + } catch (e) { + p.reject(e) + return p.promise + } + + // 3. Let request be requestObject’s request. + const request = requestObject[kState] + + // 4. If requestObject’s signal’s aborted flag is set, then: + if (requestObject.signal.aborted) { + // 1. Abort the fetch() call with p, request, null, and + // requestObject’s signal’s abort reason. + abortFetch(p, request, null, requestObject.signal.reason) + + // 2. Return p. + return p.promise + } + + // 5. Let globalObject be request’s client’s global object. + const globalObject = request.client.globalObject + + // 6. If globalObject is a ServiceWorkerGlobalScope object, then set + // request’s service-workers mode to "none". + if (globalObject?.constructor?.name === 'ServiceWorkerGlobalScope') { + request.serviceWorkers = 'none' + } + + // 7. Let responseObject be null. + let responseObject = null + + // 8. Let relevantRealm be this’s relevant Realm. + const relevantRealm = null + + // 9. Let locallyAborted be false. + let locallyAborted = false + + // 10. Let controller be null. + let controller = null + + // 11. Add the following abort steps to requestObject’s signal: + addAbortListener( + requestObject.signal, + () => { + // 1. Set locallyAborted to true. + locallyAborted = true + + // 2. Assert: controller is non-null. + assert(controller != null) + + // 3. Abort controller with requestObject’s signal’s abort reason. + controller.abort(requestObject.signal.reason) + + // 4. Abort the fetch() call with p, request, responseObject, + // and requestObject’s signal’s abort reason. + abortFetch(p, request, responseObject, requestObject.signal.reason) + } + ) + + // 12. Let handleFetchDone given response response be to finalize and + // report timing with response, globalObject, and "fetch". + const handleFetchDone = (response) => + finalizeAndReportTiming(response, 'fetch') + + // 13. Set controller to the result of calling fetch given request, + // with processResponseEndOfBody set to handleFetchDone, and processResponse + // given response being these substeps: + + const processResponse = (response) => { + // 1. If locallyAborted is true, terminate these substeps. + if (locallyAborted) { + return Promise.resolve() + } + + // 2. If response’s aborted flag is set, then: + if (response.aborted) { + // 1. Let deserializedError be the result of deserialize a serialized + // abort reason given controller’s serialized abort reason and + // relevantRealm. + + // 2. Abort the fetch() call with p, request, responseObject, and + // deserializedError. + + abortFetch(p, request, responseObject, controller.serializedAbortReason) + return Promise.resolve() + } + + // 3. If response is a network error, then reject p with a TypeError + // and terminate these substeps. + if (response.type === 'error') { + p.reject( + Object.assign(new TypeError('fetch failed'), { cause: response.error }) + ) + return Promise.resolve() + } + + // 4. Set responseObject to the result of creating a Response object, + // given response, "immutable", and relevantRealm. + responseObject = new Response() + responseObject[kState] = response + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kHeadersList] = response.headersList + responseObject[kHeaders][kGuard] = 'immutable' + responseObject[kHeaders][kRealm] = relevantRealm + + // 5. Resolve p with responseObject. + p.resolve(responseObject) + } + + controller = fetching({ + request, + processResponseEndOfBody: handleFetchDone, + processResponse, + dispatcher: init.dispatcher ?? getGlobalDispatcher() // undici + }) + + // 14. Return p. + return p.promise +} + +// https://fetch.spec.whatwg.org/#finalize-and-report-timing +function finalizeAndReportTiming (response, initiatorType = 'other') { + // 1. If response is an aborted network error, then return. + if (response.type === 'error' && response.aborted) { + return + } + + // 2. If response’s URL list is null or empty, then return. + if (!response.urlList?.length) { + return + } + + // 3. Let originalURL be response’s URL list[0]. + const originalURL = response.urlList[0] + + // 4. Let timingInfo be response’s timing info. + let timingInfo = response.timingInfo + + // 5. Let cacheState be response’s cache state. + let cacheState = response.cacheState + + // 6. If originalURL’s scheme is not an HTTP(S) scheme, then return. + if (!urlIsHttpHttpsScheme(originalURL)) { + return + } + + // 7. If timingInfo is null, then return. + if (timingInfo === null) { + return + } + + // 8. If response’s timing allow passed flag is not set, then: + if (!response.timingAllowPassed) { + // 1. Set timingInfo to a the result of creating an opaque timing info for timingInfo. + timingInfo = createOpaqueTimingInfo({ + startTime: timingInfo.startTime + }) + + // 2. Set cacheState to the empty string. + cacheState = '' + } + + // 9. Set timingInfo’s end time to the coarsened shared current time + // given global’s relevant settings object’s cross-origin isolated + // capability. + // TODO: given global’s relevant settings object’s cross-origin isolated + // capability? + timingInfo.endTime = coarsenedSharedCurrentTime() + + // 10. Set response’s timing info to timingInfo. + response.timingInfo = timingInfo + + // 11. Mark resource timing for timingInfo, originalURL, initiatorType, + // global, and cacheState. + markResourceTiming( + timingInfo, + originalURL, + initiatorType, + globalThis, + cacheState + ) +} + +// https://w3c.github.io/resource-timing/#dfn-mark-resource-timing +function markResourceTiming (timingInfo, originalURL, initiatorType, globalThis, cacheState) { + if (nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 2)) { + performance.markResourceTiming(timingInfo, originalURL.href, initiatorType, globalThis, cacheState) + } +} + +// https://fetch.spec.whatwg.org/#abort-fetch +function abortFetch (p, request, responseObject, error) { + // Note: AbortSignal.reason was added in node v17.2.0 + // which would give us an undefined error to reject with. + // Remove this once node v16 is no longer supported. + if (!error) { + error = new DOMException('The operation was aborted.', 'AbortError') + } + + // 1. Reject promise with error. + p.reject(error) + + // 2. If request’s body is not null and is readable, then cancel request’s + // body with error. + if (request.body != null && isReadable(request.body?.stream)) { + request.body.stream.cancel(error).catch((err) => { + if (err.code === 'ERR_INVALID_STATE') { + // Node bug? + return + } + throw err + }) + } + + // 3. If responseObject is null, then return. + if (responseObject == null) { + return + } + + // 4. Let response be responseObject’s response. + const response = responseObject[kState] + + // 5. If response’s body is not null and is readable, then error response’s + // body with error. + if (response.body != null && isReadable(response.body?.stream)) { + response.body.stream.cancel(error).catch((err) => { + if (err.code === 'ERR_INVALID_STATE') { + // Node bug? + return + } + throw err + }) + } +} + +// https://fetch.spec.whatwg.org/#fetching +function fetching ({ + request, + processRequestBodyChunkLength, + processRequestEndOfBody, + processResponse, + processResponseEndOfBody, + processResponseConsumeBody, + useParallelQueue = false, + dispatcher // undici +}) { + // 1. Let taskDestination be null. + let taskDestination = null + + // 2. Let crossOriginIsolatedCapability be false. + let crossOriginIsolatedCapability = false + + // 3. If request’s client is non-null, then: + if (request.client != null) { + // 1. Set taskDestination to request’s client’s global object. + taskDestination = request.client.globalObject + + // 2. Set crossOriginIsolatedCapability to request’s client’s cross-origin + // isolated capability. + crossOriginIsolatedCapability = + request.client.crossOriginIsolatedCapability + } + + // 4. If useParallelQueue is true, then set taskDestination to the result of + // starting a new parallel queue. + // TODO + + // 5. Let timingInfo be a new fetch timing info whose start time and + // post-redirect start time are the coarsened shared current time given + // crossOriginIsolatedCapability. + const currenTime = coarsenedSharedCurrentTime(crossOriginIsolatedCapability) + const timingInfo = createOpaqueTimingInfo({ + startTime: currenTime + }) + + // 6. Let fetchParams be a new fetch params whose + // request is request, + // timing info is timingInfo, + // process request body chunk length is processRequestBodyChunkLength, + // process request end-of-body is processRequestEndOfBody, + // process response is processResponse, + // process response consume body is processResponseConsumeBody, + // process response end-of-body is processResponseEndOfBody, + // task destination is taskDestination, + // and cross-origin isolated capability is crossOriginIsolatedCapability. + const fetchParams = { + controller: new Fetch(dispatcher), + request, + timingInfo, + processRequestBodyChunkLength, + processRequestEndOfBody, + processResponse, + processResponseConsumeBody, + processResponseEndOfBody, + taskDestination, + crossOriginIsolatedCapability + } + + // 7. If request’s body is a byte sequence, then set request’s body to + // request’s body as a body. + // NOTE: Since fetching is only called from fetch, body should already be + // extracted. + assert(!request.body || request.body.stream) + + // 8. If request’s window is "client", then set request’s window to request’s + // client, if request’s client’s global object is a Window object; otherwise + // "no-window". + if (request.window === 'client') { + // TODO: What if request.client is null? + request.window = + request.client?.globalObject?.constructor?.name === 'Window' + ? request.client + : 'no-window' + } + + // 9. If request’s origin is "client", then set request’s origin to request’s + // client’s origin. + if (request.origin === 'client') { + // TODO: What if request.client is null? + request.origin = request.client?.origin + } + + // 10. If all of the following conditions are true: + // TODO + + // 11. If request’s policy container is "client", then: + if (request.policyContainer === 'client') { + // 1. If request’s client is non-null, then set request’s policy + // container to a clone of request’s client’s policy container. [HTML] + if (request.client != null) { + request.policyContainer = clonePolicyContainer( + request.client.policyContainer + ) + } else { + // 2. Otherwise, set request’s policy container to a new policy + // container. + request.policyContainer = makePolicyContainer() + } + } + + // 12. If request’s header list does not contain `Accept`, then: + if (!request.headersList.contains('accept')) { + // 1. Let value be `*/*`. + const value = '*/*' + + // 2. A user agent should set value to the first matching statement, if + // any, switching on request’s destination: + // "document" + // "frame" + // "iframe" + // `text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8` + // "image" + // `image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5` + // "style" + // `text/css,*/*;q=0.1` + // TODO + + // 3. Append `Accept`/value to request’s header list. + request.headersList.append('accept', value) + } + + // 13. If request’s header list does not contain `Accept-Language`, then + // user agents should append `Accept-Language`/an appropriate value to + // request’s header list. + if (!request.headersList.contains('accept-language')) { + request.headersList.append('accept-language', '*') + } + + // 14. If request’s priority is null, then use request’s initiator and + // destination appropriately in setting request’s priority to a + // user-agent-defined object. + if (request.priority === null) { + // TODO + } + + // 15. If request is a subresource request, then: + if (subresourceSet.has(request.destination)) { + // TODO + } + + // 16. Run main fetch given fetchParams. + mainFetch(fetchParams) + .catch(err => { + fetchParams.controller.terminate(err) + }) + + // 17. Return fetchParam's controller + return fetchParams.controller +} + +// https://fetch.spec.whatwg.org/#concept-main-fetch +async function mainFetch (fetchParams, recursive = false) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let response be null. + let response = null + + // 3. If request’s local-URLs-only flag is set and request’s current URL is + // not local, then set response to a network error. + if (request.localURLsOnly && !urlIsLocal(requestCurrentURL(request))) { + response = makeNetworkError('local URLs only') + } + + // 4. Run report Content Security Policy violations for request. + // TODO + + // 5. Upgrade request to a potentially trustworthy URL, if appropriate. + tryUpgradeRequestToAPotentiallyTrustworthyURL(request) + + // 6. If should request be blocked due to a bad port, should fetching request + // be blocked as mixed content, or should request be blocked by Content + // Security Policy returns blocked, then set response to a network error. + if (requestBadPort(request) === 'blocked') { + response = makeNetworkError('bad port') + } + // TODO: should fetching request be blocked as mixed content? + // TODO: should request be blocked by Content Security Policy? + + // 7. If request’s referrer policy is the empty string, then set request’s + // referrer policy to request’s policy container’s referrer policy. + if (request.referrerPolicy === '') { + request.referrerPolicy = request.policyContainer.referrerPolicy + } + + // 8. If request’s referrer is not "no-referrer", then set request’s + // referrer to the result of invoking determine request’s referrer. + if (request.referrer !== 'no-referrer') { + request.referrer = determineRequestsReferrer(request) + } + + // 9. Set request’s current URL’s scheme to "https" if all of the following + // conditions are true: + // - request’s current URL’s scheme is "http" + // - request’s current URL’s host is a domain + // - Matching request’s current URL’s host per Known HSTS Host Domain Name + // Matching results in either a superdomain match with an asserted + // includeSubDomains directive or a congruent match (with or without an + // asserted includeSubDomains directive). [HSTS] + // TODO + + // 10. If recursive is false, then run the remaining steps in parallel. + // TODO + + // 11. If response is null, then set response to the result of running + // the steps corresponding to the first matching statement: + if (response === null) { + response = await (async () => { + const currentURL = requestCurrentURL(request) + + if ( + // - request’s current URL’s origin is same origin with request’s origin, + // and request’s response tainting is "basic" + (sameOrigin(currentURL, request.url) && request.responseTainting === 'basic') || + // request’s current URL’s scheme is "data" + (currentURL.protocol === 'data:') || + // - request’s mode is "navigate" or "websocket" + (request.mode === 'navigate' || request.mode === 'websocket') + ) { + // 1. Set request’s response tainting to "basic". + request.responseTainting = 'basic' + + // 2. Return the result of running scheme fetch given fetchParams. + return await schemeFetch(fetchParams) + } + + // request’s mode is "same-origin" + if (request.mode === 'same-origin') { + // 1. Return a network error. + return makeNetworkError('request mode cannot be "same-origin"') + } + + // request’s mode is "no-cors" + if (request.mode === 'no-cors') { + // 1. If request’s redirect mode is not "follow", then return a network + // error. + if (request.redirect !== 'follow') { + return makeNetworkError( + 'redirect mode cannot be "follow" for "no-cors" request' + ) + } + + // 2. Set request’s response tainting to "opaque". + request.responseTainting = 'opaque' + + // 3. Return the result of running scheme fetch given fetchParams. + return await schemeFetch(fetchParams) + } + + // request’s current URL’s scheme is not an HTTP(S) scheme + if (!urlIsHttpHttpsScheme(requestCurrentURL(request))) { + // Return a network error. + return makeNetworkError('URL scheme must be a HTTP(S) scheme') + } + + // - request’s use-CORS-preflight flag is set + // - request’s unsafe-request flag is set and either request’s method is + // not a CORS-safelisted method or CORS-unsafe request-header names with + // request’s header list is not empty + // 1. Set request’s response tainting to "cors". + // 2. Let corsWithPreflightResponse be the result of running HTTP fetch + // given fetchParams and true. + // 3. If corsWithPreflightResponse is a network error, then clear cache + // entries using request. + // 4. Return corsWithPreflightResponse. + // TODO + + // Otherwise + // 1. Set request’s response tainting to "cors". + request.responseTainting = 'cors' + + // 2. Return the result of running HTTP fetch given fetchParams. + return await httpFetch(fetchParams) + })() + } + + // 12. If recursive is true, then return response. + if (recursive) { + return response + } + + // 13. If response is not a network error and response is not a filtered + // response, then: + if (response.status !== 0 && !response.internalResponse) { + // If request’s response tainting is "cors", then: + if (request.responseTainting === 'cors') { + // 1. Let headerNames be the result of extracting header list values + // given `Access-Control-Expose-Headers` and response’s header list. + // TODO + // 2. If request’s credentials mode is not "include" and headerNames + // contains `*`, then set response’s CORS-exposed header-name list to + // all unique header names in response’s header list. + // TODO + // 3. Otherwise, if headerNames is not null or failure, then set + // response’s CORS-exposed header-name list to headerNames. + // TODO + } + + // Set response to the following filtered response with response as its + // internal response, depending on request’s response tainting: + if (request.responseTainting === 'basic') { + response = filterResponse(response, 'basic') + } else if (request.responseTainting === 'cors') { + response = filterResponse(response, 'cors') + } else if (request.responseTainting === 'opaque') { + response = filterResponse(response, 'opaque') + } else { + assert(false) + } + } + + // 14. Let internalResponse be response, if response is a network error, + // and response’s internal response otherwise. + let internalResponse = + response.status === 0 ? response : response.internalResponse + + // 15. If internalResponse’s URL list is empty, then set it to a clone of + // request’s URL list. + if (internalResponse.urlList.length === 0) { + internalResponse.urlList.push(...request.urlList) + } + + // 16. If request’s timing allow failed flag is unset, then set + // internalResponse’s timing allow passed flag. + if (!request.timingAllowFailed) { + response.timingAllowPassed = true + } + + // 17. If response is not a network error and any of the following returns + // blocked + // - should internalResponse to request be blocked as mixed content + // - should internalResponse to request be blocked by Content Security Policy + // - should internalResponse to request be blocked due to its MIME type + // - should internalResponse to request be blocked due to nosniff + // TODO + + // 18. If response’s type is "opaque", internalResponse’s status is 206, + // internalResponse’s range-requested flag is set, and request’s header + // list does not contain `Range`, then set response and internalResponse + // to a network error. + if ( + response.type === 'opaque' && + internalResponse.status === 206 && + internalResponse.rangeRequested && + !request.headers.contains('range') + ) { + response = internalResponse = makeNetworkError() + } + + // 19. If response is not a network error and either request’s method is + // `HEAD` or `CONNECT`, or internalResponse’s status is a null body status, + // set internalResponse’s body to null and disregard any enqueuing toward + // it (if any). + if ( + response.status !== 0 && + (request.method === 'HEAD' || + request.method === 'CONNECT' || + nullBodyStatus.includes(internalResponse.status)) + ) { + internalResponse.body = null + fetchParams.controller.dump = true + } + + // 20. If request’s integrity metadata is not the empty string, then: + if (request.integrity) { + // 1. Let processBodyError be this step: run fetch finale given fetchParams + // and a network error. + const processBodyError = (reason) => + fetchFinale(fetchParams, makeNetworkError(reason)) + + // 2. If request’s response tainting is "opaque", or response’s body is null, + // then run processBodyError and abort these steps. + if (request.responseTainting === 'opaque' || response.body == null) { + processBodyError(response.error) + return + } + + // 3. Let processBody given bytes be these steps: + const processBody = (bytes) => { + // 1. If bytes do not match request’s integrity metadata, + // then run processBodyError and abort these steps. [SRI] + if (!bytesMatch(bytes, request.integrity)) { + processBodyError('integrity mismatch') + return + } + + // 2. Set response’s body to bytes as a body. + response.body = safelyExtractBody(bytes)[0] + + // 3. Run fetch finale given fetchParams and response. + fetchFinale(fetchParams, response) + } + + // 4. Fully read response’s body given processBody and processBodyError. + await fullyReadBody(response.body, processBody, processBodyError) + } else { + // 21. Otherwise, run fetch finale given fetchParams and response. + fetchFinale(fetchParams, response) + } +} + +// https://fetch.spec.whatwg.org/#concept-scheme-fetch +// given a fetch params fetchParams +function schemeFetch (fetchParams) { + // Note: since the connection is destroyed on redirect, which sets fetchParams to a + // cancelled state, we do not want this condition to trigger *unless* there have been + // no redirects. See https://github.com/nodejs/undici/issues/1776 + // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams. + if (isCancelled(fetchParams) && fetchParams.request.redirectCount === 0) { + return Promise.resolve(makeAppropriateNetworkError(fetchParams)) + } + + // 2. Let request be fetchParams’s request. + const { request } = fetchParams + + const { protocol: scheme } = requestCurrentURL(request) + + // 3. Switch on request’s current URL’s scheme and run the associated steps: + switch (scheme) { + case 'about:': { + // If request’s current URL’s path is the string "blank", then return a new response + // whose status message is `OK`, header list is « (`Content-Type`, `text/html;charset=utf-8`) », + // and body is the empty byte sequence as a body. + + // Otherwise, return a network error. + return Promise.resolve(makeNetworkError('about scheme is not supported')) + } + case 'blob:': { + if (!resolveObjectURL) { + resolveObjectURL = (__nccwpck_require__(4300).resolveObjectURL) + } + + // 1. Let blobURLEntry be request’s current URL’s blob URL entry. + const blobURLEntry = requestCurrentURL(request) + + // https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L52-L56 + // Buffer.resolveObjectURL does not ignore URL queries. + if (blobURLEntry.search.length !== 0) { + return Promise.resolve(makeNetworkError('NetworkError when attempting to fetch resource.')) + } + + const blobURLEntryObject = resolveObjectURL(blobURLEntry.toString()) + + // 2. If request’s method is not `GET`, blobURLEntry is null, or blobURLEntry’s + // object is not a Blob object, then return a network error. + if (request.method !== 'GET' || !isBlobLike(blobURLEntryObject)) { + return Promise.resolve(makeNetworkError('invalid method')) + } + + // 3. Let bodyWithType be the result of safely extracting blobURLEntry’s object. + const bodyWithType = safelyExtractBody(blobURLEntryObject) + + // 4. Let body be bodyWithType’s body. + const body = bodyWithType[0] + + // 5. Let length be body’s length, serialized and isomorphic encoded. + const length = isomorphicEncode(`${body.length}`) + + // 6. Let type be bodyWithType’s type if it is non-null; otherwise the empty byte sequence. + const type = bodyWithType[1] ?? '' + + // 7. Return a new response whose status message is `OK`, header list is + // « (`Content-Length`, length), (`Content-Type`, type) », and body is body. + const response = makeResponse({ + statusText: 'OK', + headersList: [ + ['content-length', { name: 'Content-Length', value: length }], + ['content-type', { name: 'Content-Type', value: type }] + ] + }) + + response.body = body + + return Promise.resolve(response) + } + case 'data:': { + // 1. Let dataURLStruct be the result of running the + // data: URL processor on request’s current URL. + const currentURL = requestCurrentURL(request) + const dataURLStruct = dataURLProcessor(currentURL) + + // 2. If dataURLStruct is failure, then return a + // network error. + if (dataURLStruct === 'failure') { + return Promise.resolve(makeNetworkError('failed to fetch the data URL')) + } + + // 3. Let mimeType be dataURLStruct’s MIME type, serialized. + const mimeType = serializeAMimeType(dataURLStruct.mimeType) + + // 4. Return a response whose status message is `OK`, + // header list is « (`Content-Type`, mimeType) », + // and body is dataURLStruct’s body as a body. + return Promise.resolve(makeResponse({ + statusText: 'OK', + headersList: [ + ['content-type', { name: 'Content-Type', value: mimeType }] + ], + body: safelyExtractBody(dataURLStruct.body)[0] + })) + } + case 'file:': { + // For now, unfortunate as it is, file URLs are left as an exercise for the reader. + // When in doubt, return a network error. + return Promise.resolve(makeNetworkError('not implemented... yet...')) + } + case 'http:': + case 'https:': { + // Return the result of running HTTP fetch given fetchParams. + + return httpFetch(fetchParams) + .catch((err) => makeNetworkError(err)) + } + default: { + return Promise.resolve(makeNetworkError('unknown scheme')) + } + } +} + +// https://fetch.spec.whatwg.org/#finalize-response +function finalizeResponse (fetchParams, response) { + // 1. Set fetchParams’s request’s done flag. + fetchParams.request.done = true + + // 2, If fetchParams’s process response done is not null, then queue a fetch + // task to run fetchParams’s process response done given response, with + // fetchParams’s task destination. + if (fetchParams.processResponseDone != null) { + queueMicrotask(() => fetchParams.processResponseDone(response)) + } +} + +// https://fetch.spec.whatwg.org/#fetch-finale +function fetchFinale (fetchParams, response) { + // 1. If response is a network error, then: + if (response.type === 'error') { + // 1. Set response’s URL list to « fetchParams’s request’s URL list[0] ». + response.urlList = [fetchParams.request.urlList[0]] + + // 2. Set response’s timing info to the result of creating an opaque timing + // info for fetchParams’s timing info. + response.timingInfo = createOpaqueTimingInfo({ + startTime: fetchParams.timingInfo.startTime + }) + } + + // 2. Let processResponseEndOfBody be the following steps: + const processResponseEndOfBody = () => { + // 1. Set fetchParams’s request’s done flag. + fetchParams.request.done = true + + // If fetchParams’s process response end-of-body is not null, + // then queue a fetch task to run fetchParams’s process response + // end-of-body given response with fetchParams’s task destination. + if (fetchParams.processResponseEndOfBody != null) { + queueMicrotask(() => fetchParams.processResponseEndOfBody(response)) + } + } + + // 3. If fetchParams’s process response is non-null, then queue a fetch task + // to run fetchParams’s process response given response, with fetchParams’s + // task destination. + if (fetchParams.processResponse != null) { + queueMicrotask(() => fetchParams.processResponse(response)) + } + + // 4. If response’s body is null, then run processResponseEndOfBody. + if (response.body == null) { + processResponseEndOfBody() + } else { + // 5. Otherwise: + + // 1. Let transformStream be a new a TransformStream. + + // 2. Let identityTransformAlgorithm be an algorithm which, given chunk, + // enqueues chunk in transformStream. + const identityTransformAlgorithm = (chunk, controller) => { + controller.enqueue(chunk) + } + + // 3. Set up transformStream with transformAlgorithm set to identityTransformAlgorithm + // and flushAlgorithm set to processResponseEndOfBody. + const transformStream = new TransformStream({ + start () {}, + transform: identityTransformAlgorithm, + flush: processResponseEndOfBody + }, { + size () { + return 1 + } + }, { + size () { + return 1 + } + }) + + // 4. Set response’s body to the result of piping response’s body through transformStream. + response.body = { stream: response.body.stream.pipeThrough(transformStream) } + } + + // 6. If fetchParams’s process response consume body is non-null, then: + if (fetchParams.processResponseConsumeBody != null) { + // 1. Let processBody given nullOrBytes be this step: run fetchParams’s + // process response consume body given response and nullOrBytes. + const processBody = (nullOrBytes) => fetchParams.processResponseConsumeBody(response, nullOrBytes) + + // 2. Let processBodyError be this step: run fetchParams’s process + // response consume body given response and failure. + const processBodyError = (failure) => fetchParams.processResponseConsumeBody(response, failure) + + // 3. If response’s body is null, then queue a fetch task to run processBody + // given null, with fetchParams’s task destination. + if (response.body == null) { + queueMicrotask(() => processBody(null)) + } else { + // 4. Otherwise, fully read response’s body given processBody, processBodyError, + // and fetchParams’s task destination. + return fullyReadBody(response.body, processBody, processBodyError) + } + return Promise.resolve() + } +} + +// https://fetch.spec.whatwg.org/#http-fetch +async function httpFetch (fetchParams) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let response be null. + let response = null + + // 3. Let actualResponse be null. + let actualResponse = null + + // 4. Let timingInfo be fetchParams’s timing info. + const timingInfo = fetchParams.timingInfo + + // 5. If request’s service-workers mode is "all", then: + if (request.serviceWorkers === 'all') { + // TODO + } + + // 6. If response is null, then: + if (response === null) { + // 1. If makeCORSPreflight is true and one of these conditions is true: + // TODO + + // 2. If request’s redirect mode is "follow", then set request’s + // service-workers mode to "none". + if (request.redirect === 'follow') { + request.serviceWorkers = 'none' + } + + // 3. Set response and actualResponse to the result of running + // HTTP-network-or-cache fetch given fetchParams. + actualResponse = response = await httpNetworkOrCacheFetch(fetchParams) + + // 4. If request’s response tainting is "cors" and a CORS check + // for request and response returns failure, then return a network error. + if ( + request.responseTainting === 'cors' && + corsCheck(request, response) === 'failure' + ) { + return makeNetworkError('cors failure') + } + + // 5. If the TAO check for request and response returns failure, then set + // request’s timing allow failed flag. + if (TAOCheck(request, response) === 'failure') { + request.timingAllowFailed = true + } + } + + // 7. If either request’s response tainting or response’s type + // is "opaque", and the cross-origin resource policy check with + // request’s origin, request’s client, request’s destination, + // and actualResponse returns blocked, then return a network error. + if ( + (request.responseTainting === 'opaque' || response.type === 'opaque') && + crossOriginResourcePolicyCheck( + request.origin, + request.client, + request.destination, + actualResponse + ) === 'blocked' + ) { + return makeNetworkError('blocked') + } + + // 8. If actualResponse’s status is a redirect status, then: + if (redirectStatusSet.has(actualResponse.status)) { + // 1. If actualResponse’s status is not 303, request’s body is not null, + // and the connection uses HTTP/2, then user agents may, and are even + // encouraged to, transmit an RST_STREAM frame. + // See, https://github.com/whatwg/fetch/issues/1288 + if (request.redirect !== 'manual') { + fetchParams.controller.connection.destroy() + } + + // 2. Switch on request’s redirect mode: + if (request.redirect === 'error') { + // Set response to a network error. + response = makeNetworkError('unexpected redirect') + } else if (request.redirect === 'manual') { + // Set response to an opaque-redirect filtered response whose internal + // response is actualResponse. + // NOTE(spec): On the web this would return an `opaqueredirect` response, + // but that doesn't make sense server side. + // See https://github.com/nodejs/undici/issues/1193. + response = actualResponse + } else if (request.redirect === 'follow') { + // Set response to the result of running HTTP-redirect fetch given + // fetchParams and response. + response = await httpRedirectFetch(fetchParams, response) + } else { + assert(false) + } + } + + // 9. Set response’s timing info to timingInfo. + response.timingInfo = timingInfo + + // 10. Return response. + return response +} + +// https://fetch.spec.whatwg.org/#http-redirect-fetch +function httpRedirectFetch (fetchParams, response) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let actualResponse be response, if response is not a filtered response, + // and response’s internal response otherwise. + const actualResponse = response.internalResponse + ? response.internalResponse + : response + + // 3. Let locationURL be actualResponse’s location URL given request’s current + // URL’s fragment. + let locationURL + + try { + locationURL = responseLocationURL( + actualResponse, + requestCurrentURL(request).hash + ) + + // 4. If locationURL is null, then return response. + if (locationURL == null) { + return response + } + } catch (err) { + // 5. If locationURL is failure, then return a network error. + return Promise.resolve(makeNetworkError(err)) + } + + // 6. If locationURL’s scheme is not an HTTP(S) scheme, then return a network + // error. + if (!urlIsHttpHttpsScheme(locationURL)) { + return Promise.resolve(makeNetworkError('URL scheme must be a HTTP(S) scheme')) + } + + // 7. If request’s redirect count is 20, then return a network error. + if (request.redirectCount === 20) { + return Promise.resolve(makeNetworkError('redirect count exceeded')) + } + + // 8. Increase request’s redirect count by 1. + request.redirectCount += 1 + + // 9. If request’s mode is "cors", locationURL includes credentials, and + // request’s origin is not same origin with locationURL’s origin, then return + // a network error. + if ( + request.mode === 'cors' && + (locationURL.username || locationURL.password) && + !sameOrigin(request, locationURL) + ) { + return Promise.resolve(makeNetworkError('cross origin not allowed for request mode "cors"')) + } + + // 10. If request’s response tainting is "cors" and locationURL includes + // credentials, then return a network error. + if ( + request.responseTainting === 'cors' && + (locationURL.username || locationURL.password) + ) { + return Promise.resolve(makeNetworkError( + 'URL cannot contain credentials for request mode "cors"' + )) + } + + // 11. If actualResponse’s status is not 303, request’s body is non-null, + // and request’s body’s source is null, then return a network error. + if ( + actualResponse.status !== 303 && + request.body != null && + request.body.source == null + ) { + return Promise.resolve(makeNetworkError()) + } + + // 12. If one of the following is true + // - actualResponse’s status is 301 or 302 and request’s method is `POST` + // - actualResponse’s status is 303 and request’s method is not `GET` or `HEAD` + if ( + ([301, 302].includes(actualResponse.status) && request.method === 'POST') || + (actualResponse.status === 303 && + !GET_OR_HEAD.includes(request.method)) + ) { + // then: + // 1. Set request’s method to `GET` and request’s body to null. + request.method = 'GET' + request.body = null + + // 2. For each headerName of request-body-header name, delete headerName from + // request’s header list. + for (const headerName of requestBodyHeader) { + request.headersList.delete(headerName) + } + } + + // 13. If request’s current URL’s origin is not same origin with locationURL’s + // origin, then for each headerName of CORS non-wildcard request-header name, + // delete headerName from request’s header list. + if (!sameOrigin(requestCurrentURL(request), locationURL)) { + // https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name + request.headersList.delete('authorization') + + // https://fetch.spec.whatwg.org/#authentication-entries + request.headersList.delete('proxy-authorization', true) + + // "Cookie" and "Host" are forbidden request-headers, which undici doesn't implement. + request.headersList.delete('cookie') + request.headersList.delete('host') + } + + // 14. If request’s body is non-null, then set request’s body to the first return + // value of safely extracting request’s body’s source. + if (request.body != null) { + assert(request.body.source != null) + request.body = safelyExtractBody(request.body.source)[0] + } + + // 15. Let timingInfo be fetchParams’s timing info. + const timingInfo = fetchParams.timingInfo + + // 16. Set timingInfo’s redirect end time and post-redirect start time to the + // coarsened shared current time given fetchParams’s cross-origin isolated + // capability. + timingInfo.redirectEndTime = timingInfo.postRedirectStartTime = + coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability) + + // 17. If timingInfo’s redirect start time is 0, then set timingInfo’s + // redirect start time to timingInfo’s start time. + if (timingInfo.redirectStartTime === 0) { + timingInfo.redirectStartTime = timingInfo.startTime + } + + // 18. Append locationURL to request’s URL list. + request.urlList.push(locationURL) + + // 19. Invoke set request’s referrer policy on redirect on request and + // actualResponse. + setRequestReferrerPolicyOnRedirect(request, actualResponse) + + // 20. Return the result of running main fetch given fetchParams and true. + return mainFetch(fetchParams, true) +} + +// https://fetch.spec.whatwg.org/#http-network-or-cache-fetch +async function httpNetworkOrCacheFetch ( + fetchParams, + isAuthenticationFetch = false, + isNewConnectionFetch = false +) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let httpFetchParams be null. + let httpFetchParams = null + + // 3. Let httpRequest be null. + let httpRequest = null + + // 4. Let response be null. + let response = null + + // 5. Let storedResponse be null. + // TODO: cache + + // 6. Let httpCache be null. + const httpCache = null + + // 7. Let the revalidatingFlag be unset. + const revalidatingFlag = false + + // 8. Run these steps, but abort when the ongoing fetch is terminated: + + // 1. If request’s window is "no-window" and request’s redirect mode is + // "error", then set httpFetchParams to fetchParams and httpRequest to + // request. + if (request.window === 'no-window' && request.redirect === 'error') { + httpFetchParams = fetchParams + httpRequest = request + } else { + // Otherwise: + + // 1. Set httpRequest to a clone of request. + httpRequest = makeRequest(request) + + // 2. Set httpFetchParams to a copy of fetchParams. + httpFetchParams = { ...fetchParams } + + // 3. Set httpFetchParams’s request to httpRequest. + httpFetchParams.request = httpRequest + } + + // 3. Let includeCredentials be true if one of + const includeCredentials = + request.credentials === 'include' || + (request.credentials === 'same-origin' && + request.responseTainting === 'basic') + + // 4. Let contentLength be httpRequest’s body’s length, if httpRequest’s + // body is non-null; otherwise null. + const contentLength = httpRequest.body ? httpRequest.body.length : null + + // 5. Let contentLengthHeaderValue be null. + let contentLengthHeaderValue = null + + // 6. If httpRequest’s body is null and httpRequest’s method is `POST` or + // `PUT`, then set contentLengthHeaderValue to `0`. + if ( + httpRequest.body == null && + ['POST', 'PUT'].includes(httpRequest.method) + ) { + contentLengthHeaderValue = '0' + } + + // 7. If contentLength is non-null, then set contentLengthHeaderValue to + // contentLength, serialized and isomorphic encoded. + if (contentLength != null) { + contentLengthHeaderValue = isomorphicEncode(`${contentLength}`) + } + + // 8. If contentLengthHeaderValue is non-null, then append + // `Content-Length`/contentLengthHeaderValue to httpRequest’s header + // list. + if (contentLengthHeaderValue != null) { + httpRequest.headersList.append('content-length', contentLengthHeaderValue) + } + + // 9. If contentLengthHeaderValue is non-null, then append (`Content-Length`, + // contentLengthHeaderValue) to httpRequest’s header list. + + // 10. If contentLength is non-null and httpRequest’s keepalive is true, + // then: + if (contentLength != null && httpRequest.keepalive) { + // NOTE: keepalive is a noop outside of browser context. + } + + // 11. If httpRequest’s referrer is a URL, then append + // `Referer`/httpRequest’s referrer, serialized and isomorphic encoded, + // to httpRequest’s header list. + if (httpRequest.referrer instanceof URL) { + httpRequest.headersList.append('referer', isomorphicEncode(httpRequest.referrer.href)) + } + + // 12. Append a request `Origin` header for httpRequest. + appendRequestOriginHeader(httpRequest) + + // 13. Append the Fetch metadata headers for httpRequest. [FETCH-METADATA] + appendFetchMetadata(httpRequest) + + // 14. If httpRequest’s header list does not contain `User-Agent`, then + // user agents should append `User-Agent`/default `User-Agent` value to + // httpRequest’s header list. + if (!httpRequest.headersList.contains('user-agent')) { + httpRequest.headersList.append('user-agent', typeof esbuildDetection === 'undefined' ? 'undici' : 'node') + } + + // 15. If httpRequest’s cache mode is "default" and httpRequest’s header + // list contains `If-Modified-Since`, `If-None-Match`, + // `If-Unmodified-Since`, `If-Match`, or `If-Range`, then set + // httpRequest’s cache mode to "no-store". + if ( + httpRequest.cache === 'default' && + (httpRequest.headersList.contains('if-modified-since') || + httpRequest.headersList.contains('if-none-match') || + httpRequest.headersList.contains('if-unmodified-since') || + httpRequest.headersList.contains('if-match') || + httpRequest.headersList.contains('if-range')) + ) { + httpRequest.cache = 'no-store' + } + + // 16. If httpRequest’s cache mode is "no-cache", httpRequest’s prevent + // no-cache cache-control header modification flag is unset, and + // httpRequest’s header list does not contain `Cache-Control`, then append + // `Cache-Control`/`max-age=0` to httpRequest’s header list. + if ( + httpRequest.cache === 'no-cache' && + !httpRequest.preventNoCacheCacheControlHeaderModification && + !httpRequest.headersList.contains('cache-control') + ) { + httpRequest.headersList.append('cache-control', 'max-age=0') + } + + // 17. If httpRequest’s cache mode is "no-store" or "reload", then: + if (httpRequest.cache === 'no-store' || httpRequest.cache === 'reload') { + // 1. If httpRequest’s header list does not contain `Pragma`, then append + // `Pragma`/`no-cache` to httpRequest’s header list. + if (!httpRequest.headersList.contains('pragma')) { + httpRequest.headersList.append('pragma', 'no-cache') + } + + // 2. If httpRequest’s header list does not contain `Cache-Control`, + // then append `Cache-Control`/`no-cache` to httpRequest’s header list. + if (!httpRequest.headersList.contains('cache-control')) { + httpRequest.headersList.append('cache-control', 'no-cache') + } + } + + // 18. If httpRequest’s header list contains `Range`, then append + // `Accept-Encoding`/`identity` to httpRequest’s header list. + if (httpRequest.headersList.contains('range')) { + httpRequest.headersList.append('accept-encoding', 'identity') + } + + // 19. Modify httpRequest’s header list per HTTP. Do not append a given + // header if httpRequest’s header list contains that header’s name. + // TODO: https://github.com/whatwg/fetch/issues/1285#issuecomment-896560129 + if (!httpRequest.headersList.contains('accept-encoding')) { + if (urlHasHttpsScheme(requestCurrentURL(httpRequest))) { + httpRequest.headersList.append('accept-encoding', 'br, gzip, deflate') + } else { + httpRequest.headersList.append('accept-encoding', 'gzip, deflate') + } + } + + httpRequest.headersList.delete('host') + + // 20. If includeCredentials is true, then: + if (includeCredentials) { + // 1. If the user agent is not configured to block cookies for httpRequest + // (see section 7 of [COOKIES]), then: + // TODO: credentials + // 2. If httpRequest’s header list does not contain `Authorization`, then: + // TODO: credentials + } + + // 21. If there’s a proxy-authentication entry, use it as appropriate. + // TODO: proxy-authentication + + // 22. Set httpCache to the result of determining the HTTP cache + // partition, given httpRequest. + // TODO: cache + + // 23. If httpCache is null, then set httpRequest’s cache mode to + // "no-store". + if (httpCache == null) { + httpRequest.cache = 'no-store' + } + + // 24. If httpRequest’s cache mode is neither "no-store" nor "reload", + // then: + if (httpRequest.mode !== 'no-store' && httpRequest.mode !== 'reload') { + // TODO: cache + } + + // 9. If aborted, then return the appropriate network error for fetchParams. + // TODO + + // 10. If response is null, then: + if (response == null) { + // 1. If httpRequest’s cache mode is "only-if-cached", then return a + // network error. + if (httpRequest.mode === 'only-if-cached') { + return makeNetworkError('only if cached') + } + + // 2. Let forwardResponse be the result of running HTTP-network fetch + // given httpFetchParams, includeCredentials, and isNewConnectionFetch. + const forwardResponse = await httpNetworkFetch( + httpFetchParams, + includeCredentials, + isNewConnectionFetch + ) + + // 3. If httpRequest’s method is unsafe and forwardResponse’s status is + // in the range 200 to 399, inclusive, invalidate appropriate stored + // responses in httpCache, as per the "Invalidation" chapter of HTTP + // Caching, and set storedResponse to null. [HTTP-CACHING] + if ( + !safeMethodsSet.has(httpRequest.method) && + forwardResponse.status >= 200 && + forwardResponse.status <= 399 + ) { + // TODO: cache + } + + // 4. If the revalidatingFlag is set and forwardResponse’s status is 304, + // then: + if (revalidatingFlag && forwardResponse.status === 304) { + // TODO: cache + } + + // 5. If response is null, then: + if (response == null) { + // 1. Set response to forwardResponse. + response = forwardResponse + + // 2. Store httpRequest and forwardResponse in httpCache, as per the + // "Storing Responses in Caches" chapter of HTTP Caching. [HTTP-CACHING] + // TODO: cache + } + } + + // 11. Set response’s URL list to a clone of httpRequest’s URL list. + response.urlList = [...httpRequest.urlList] + + // 12. If httpRequest’s header list contains `Range`, then set response’s + // range-requested flag. + if (httpRequest.headersList.contains('range')) { + response.rangeRequested = true + } + + // 13. Set response’s request-includes-credentials to includeCredentials. + response.requestIncludesCredentials = includeCredentials + + // 14. If response’s status is 401, httpRequest’s response tainting is not + // "cors", includeCredentials is true, and request’s window is an environment + // settings object, then: + // TODO + + // 15. If response’s status is 407, then: + if (response.status === 407) { + // 1. If request’s window is "no-window", then return a network error. + if (request.window === 'no-window') { + return makeNetworkError() + } + + // 2. ??? + + // 3. If fetchParams is canceled, then return the appropriate network error for fetchParams. + if (isCancelled(fetchParams)) { + return makeAppropriateNetworkError(fetchParams) + } + + // 4. Prompt the end user as appropriate in request’s window and store + // the result as a proxy-authentication entry. [HTTP-AUTH] + // TODO: Invoke some kind of callback? + + // 5. Set response to the result of running HTTP-network-or-cache fetch given + // fetchParams. + // TODO + return makeNetworkError('proxy authentication required') + } + + // 16. If all of the following are true + if ( + // response’s status is 421 + response.status === 421 && + // isNewConnectionFetch is false + !isNewConnectionFetch && + // request’s body is null, or request’s body is non-null and request’s body’s source is non-null + (request.body == null || request.body.source != null) + ) { + // then: + + // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams. + if (isCancelled(fetchParams)) { + return makeAppropriateNetworkError(fetchParams) + } + + // 2. Set response to the result of running HTTP-network-or-cache + // fetch given fetchParams, isAuthenticationFetch, and true. + + // TODO (spec): The spec doesn't specify this but we need to cancel + // the active response before we can start a new one. + // https://github.com/whatwg/fetch/issues/1293 + fetchParams.controller.connection.destroy() + + response = await httpNetworkOrCacheFetch( + fetchParams, + isAuthenticationFetch, + true + ) + } + + // 17. If isAuthenticationFetch is true, then create an authentication entry + if (isAuthenticationFetch) { + // TODO + } + + // 18. Return response. + return response +} + +// https://fetch.spec.whatwg.org/#http-network-fetch +async function httpNetworkFetch ( + fetchParams, + includeCredentials = false, + forceNewConnection = false +) { + assert(!fetchParams.controller.connection || fetchParams.controller.connection.destroyed) + + fetchParams.controller.connection = { + abort: null, + destroyed: false, + destroy (err) { + if (!this.destroyed) { + this.destroyed = true + this.abort?.(err ?? new DOMException('The operation was aborted.', 'AbortError')) + } + } + } + + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let response be null. + let response = null + + // 3. Let timingInfo be fetchParams’s timing info. + const timingInfo = fetchParams.timingInfo + + // 4. Let httpCache be the result of determining the HTTP cache partition, + // given request. + // TODO: cache + const httpCache = null + + // 5. If httpCache is null, then set request’s cache mode to "no-store". + if (httpCache == null) { + request.cache = 'no-store' + } + + // 6. Let networkPartitionKey be the result of determining the network + // partition key given request. + // TODO + + // 7. Let newConnection be "yes" if forceNewConnection is true; otherwise + // "no". + const newConnection = forceNewConnection ? 'yes' : 'no' // eslint-disable-line no-unused-vars + + // 8. Switch on request’s mode: + if (request.mode === 'websocket') { + // Let connection be the result of obtaining a WebSocket connection, + // given request’s current URL. + // TODO + } else { + // Let connection be the result of obtaining a connection, given + // networkPartitionKey, request’s current URL’s origin, + // includeCredentials, and forceNewConnection. + // TODO + } + + // 9. Run these steps, but abort when the ongoing fetch is terminated: + + // 1. If connection is failure, then return a network error. + + // 2. Set timingInfo’s final connection timing info to the result of + // calling clamp and coarsen connection timing info with connection’s + // timing info, timingInfo’s post-redirect start time, and fetchParams’s + // cross-origin isolated capability. + + // 3. If connection is not an HTTP/2 connection, request’s body is non-null, + // and request’s body’s source is null, then append (`Transfer-Encoding`, + // `chunked`) to request’s header list. + + // 4. Set timingInfo’s final network-request start time to the coarsened + // shared current time given fetchParams’s cross-origin isolated + // capability. + + // 5. Set response to the result of making an HTTP request over connection + // using request with the following caveats: + + // - Follow the relevant requirements from HTTP. [HTTP] [HTTP-SEMANTICS] + // [HTTP-COND] [HTTP-CACHING] [HTTP-AUTH] + + // - If request’s body is non-null, and request’s body’s source is null, + // then the user agent may have a buffer of up to 64 kibibytes and store + // a part of request’s body in that buffer. If the user agent reads from + // request’s body beyond that buffer’s size and the user agent needs to + // resend request, then instead return a network error. + + // - Set timingInfo’s final network-response start time to the coarsened + // shared current time given fetchParams’s cross-origin isolated capability, + // immediately after the user agent’s HTTP parser receives the first byte + // of the response (e.g., frame header bytes for HTTP/2 or response status + // line for HTTP/1.x). + + // - Wait until all the headers are transmitted. + + // - Any responses whose status is in the range 100 to 199, inclusive, + // and is not 101, are to be ignored, except for the purposes of setting + // timingInfo’s final network-response start time above. + + // - If request’s header list contains `Transfer-Encoding`/`chunked` and + // response is transferred via HTTP/1.0 or older, then return a network + // error. + + // - If the HTTP request results in a TLS client certificate dialog, then: + + // 1. If request’s window is an environment settings object, make the + // dialog available in request’s window. + + // 2. Otherwise, return a network error. + + // To transmit request’s body body, run these steps: + let requestBody = null + // 1. If body is null and fetchParams’s process request end-of-body is + // non-null, then queue a fetch task given fetchParams’s process request + // end-of-body and fetchParams’s task destination. + if (request.body == null && fetchParams.processRequestEndOfBody) { + queueMicrotask(() => fetchParams.processRequestEndOfBody()) + } else if (request.body != null) { + // 2. Otherwise, if body is non-null: + + // 1. Let processBodyChunk given bytes be these steps: + const processBodyChunk = async function * (bytes) { + // 1. If the ongoing fetch is terminated, then abort these steps. + if (isCancelled(fetchParams)) { + return + } + + // 2. Run this step in parallel: transmit bytes. + yield bytes + + // 3. If fetchParams’s process request body is non-null, then run + // fetchParams’s process request body given bytes’s length. + fetchParams.processRequestBodyChunkLength?.(bytes.byteLength) + } + + // 2. Let processEndOfBody be these steps: + const processEndOfBody = () => { + // 1. If fetchParams is canceled, then abort these steps. + if (isCancelled(fetchParams)) { + return + } + + // 2. If fetchParams’s process request end-of-body is non-null, + // then run fetchParams’s process request end-of-body. + if (fetchParams.processRequestEndOfBody) { + fetchParams.processRequestEndOfBody() + } + } + + // 3. Let processBodyError given e be these steps: + const processBodyError = (e) => { + // 1. If fetchParams is canceled, then abort these steps. + if (isCancelled(fetchParams)) { + return + } + + // 2. If e is an "AbortError" DOMException, then abort fetchParams’s controller. + if (e.name === 'AbortError') { + fetchParams.controller.abort() + } else { + fetchParams.controller.terminate(e) + } + } + + // 4. Incrementally read request’s body given processBodyChunk, processEndOfBody, + // processBodyError, and fetchParams’s task destination. + requestBody = (async function * () { + try { + for await (const bytes of request.body.stream) { + yield * processBodyChunk(bytes) + } + processEndOfBody() + } catch (err) { + processBodyError(err) + } + })() + } + + try { + // socket is only provided for websockets + const { body, status, statusText, headersList, socket } = await dispatch({ body: requestBody }) + + if (socket) { + response = makeResponse({ status, statusText, headersList, socket }) + } else { + const iterator = body[Symbol.asyncIterator]() + fetchParams.controller.next = () => iterator.next() + + response = makeResponse({ status, statusText, headersList }) + } + } catch (err) { + // 10. If aborted, then: + if (err.name === 'AbortError') { + // 1. If connection uses HTTP/2, then transmit an RST_STREAM frame. + fetchParams.controller.connection.destroy() + + // 2. Return the appropriate network error for fetchParams. + return makeAppropriateNetworkError(fetchParams, err) + } + + return makeNetworkError(err) + } + + // 11. Let pullAlgorithm be an action that resumes the ongoing fetch + // if it is suspended. + const pullAlgorithm = () => { + fetchParams.controller.resume() + } + + // 12. Let cancelAlgorithm be an algorithm that aborts fetchParams’s + // controller with reason, given reason. + const cancelAlgorithm = (reason) => { + fetchParams.controller.abort(reason) + } + + // 13. Let highWaterMark be a non-negative, non-NaN number, chosen by + // the user agent. + // TODO + + // 14. Let sizeAlgorithm be an algorithm that accepts a chunk object + // and returns a non-negative, non-NaN, non-infinite number, chosen by the user agent. + // TODO + + // 15. Let stream be a new ReadableStream. + // 16. Set up stream with pullAlgorithm set to pullAlgorithm, + // cancelAlgorithm set to cancelAlgorithm, highWaterMark set to + // highWaterMark, and sizeAlgorithm set to sizeAlgorithm. + if (!ReadableStream) { + ReadableStream = (__nccwpck_require__(5356).ReadableStream) + } + + const stream = new ReadableStream( + { + async start (controller) { + fetchParams.controller.controller = controller + }, + async pull (controller) { + await pullAlgorithm(controller) + }, + async cancel (reason) { + await cancelAlgorithm(reason) + } + }, + { + highWaterMark: 0, + size () { + return 1 + } + } + ) + + // 17. Run these steps, but abort when the ongoing fetch is terminated: + + // 1. Set response’s body to a new body whose stream is stream. + response.body = { stream } + + // 2. If response is not a network error and request’s cache mode is + // not "no-store", then update response in httpCache for request. + // TODO + + // 3. If includeCredentials is true and the user agent is not configured + // to block cookies for request (see section 7 of [COOKIES]), then run the + // "set-cookie-string" parsing algorithm (see section 5.2 of [COOKIES]) on + // the value of each header whose name is a byte-case-insensitive match for + // `Set-Cookie` in response’s header list, if any, and request’s current URL. + // TODO + + // 18. If aborted, then: + // TODO + + // 19. Run these steps in parallel: + + // 1. Run these steps, but abort when fetchParams is canceled: + fetchParams.controller.on('terminated', onAborted) + fetchParams.controller.resume = async () => { + // 1. While true + while (true) { + // 1-3. See onData... + + // 4. Set bytes to the result of handling content codings given + // codings and bytes. + let bytes + let isFailure + try { + const { done, value } = await fetchParams.controller.next() + + if (isAborted(fetchParams)) { + break + } + + bytes = done ? undefined : value + } catch (err) { + if (fetchParams.controller.ended && !timingInfo.encodedBodySize) { + // zlib doesn't like empty streams. + bytes = undefined + } else { + bytes = err + + // err may be propagated from the result of calling readablestream.cancel, + // which might not be an error. https://github.com/nodejs/undici/issues/2009 + isFailure = true + } + } + + if (bytes === undefined) { + // 2. Otherwise, if the bytes transmission for response’s message + // body is done normally and stream is readable, then close + // stream, finalize response for fetchParams and response, and + // abort these in-parallel steps. + readableStreamClose(fetchParams.controller.controller) + + finalizeResponse(fetchParams, response) + + return + } + + // 5. Increase timingInfo’s decoded body size by bytes’s length. + timingInfo.decodedBodySize += bytes?.byteLength ?? 0 + + // 6. If bytes is failure, then terminate fetchParams’s controller. + if (isFailure) { + fetchParams.controller.terminate(bytes) + return + } + + // 7. Enqueue a Uint8Array wrapping an ArrayBuffer containing bytes + // into stream. + fetchParams.controller.controller.enqueue(new Uint8Array(bytes)) + + // 8. If stream is errored, then terminate the ongoing fetch. + if (isErrored(stream)) { + fetchParams.controller.terminate() + return + } + + // 9. If stream doesn’t need more data ask the user agent to suspend + // the ongoing fetch. + if (!fetchParams.controller.controller.desiredSize) { + return + } + } + } + + // 2. If aborted, then: + function onAborted (reason) { + // 2. If fetchParams is aborted, then: + if (isAborted(fetchParams)) { + // 1. Set response’s aborted flag. + response.aborted = true + + // 2. If stream is readable, then error stream with the result of + // deserialize a serialized abort reason given fetchParams’s + // controller’s serialized abort reason and an + // implementation-defined realm. + if (isReadable(stream)) { + fetchParams.controller.controller.error( + fetchParams.controller.serializedAbortReason + ) + } + } else { + // 3. Otherwise, if stream is readable, error stream with a TypeError. + if (isReadable(stream)) { + fetchParams.controller.controller.error(new TypeError('terminated', { + cause: isErrorLike(reason) ? reason : undefined + })) + } + } + + // 4. If connection uses HTTP/2, then transmit an RST_STREAM frame. + // 5. Otherwise, the user agent should close connection unless it would be bad for performance to do so. + fetchParams.controller.connection.destroy() + } + + // 20. Return response. + return response + + async function dispatch ({ body }) { + const url = requestCurrentURL(request) + /** @type {import('../..').Agent} */ + const agent = fetchParams.controller.dispatcher + + return new Promise((resolve, reject) => agent.dispatch( + { + path: url.pathname + url.search, + origin: url.origin, + method: request.method, + body: fetchParams.controller.dispatcher.isMockActive ? request.body && (request.body.source || request.body.stream) : body, + headers: request.headersList.entries, + maxRedirections: 0, + upgrade: request.mode === 'websocket' ? 'websocket' : undefined + }, + { + body: null, + abort: null, + + onConnect (abort) { + // TODO (fix): Do we need connection here? + const { connection } = fetchParams.controller + + if (connection.destroyed) { + abort(new DOMException('The operation was aborted.', 'AbortError')) + } else { + fetchParams.controller.on('terminated', abort) + this.abort = connection.abort = abort + } + }, + + onHeaders (status, headersList, resume, statusText) { + if (status < 200) { + return + } + + let codings = [] + let location = '' + + const headers = new Headers() + + // For H2, the headers are a plain JS object + // We distinguish between them and iterate accordingly + if (Array.isArray(headersList)) { + for (let n = 0; n < headersList.length; n += 2) { + const key = headersList[n + 0].toString('latin1') + const val = headersList[n + 1].toString('latin1') + if (key.toLowerCase() === 'content-encoding') { + // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 + // "All content-coding values are case-insensitive..." + codings = val.toLowerCase().split(',').map((x) => x.trim()) + } else if (key.toLowerCase() === 'location') { + location = val + } + + headers[kHeadersList].append(key, val) + } + } else { + const keys = Object.keys(headersList) + for (const key of keys) { + const val = headersList[key] + if (key.toLowerCase() === 'content-encoding') { + // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 + // "All content-coding values are case-insensitive..." + codings = val.toLowerCase().split(',').map((x) => x.trim()).reverse() + } else if (key.toLowerCase() === 'location') { + location = val + } + + headers[kHeadersList].append(key, val) + } + } + + this.body = new Readable({ read: resume }) + + const decoders = [] + + const willFollow = request.redirect === 'follow' && + location && + redirectStatusSet.has(status) + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding + if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) { + for (const coding of codings) { + // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2 + if (coding === 'x-gzip' || coding === 'gzip') { + decoders.push(zlib.createGunzip({ + // Be less strict when decoding compressed responses, since sometimes + // servers send slightly invalid responses that are still accepted + // by common browsers. + // Always using Z_SYNC_FLUSH is what cURL does. + flush: zlib.constants.Z_SYNC_FLUSH, + finishFlush: zlib.constants.Z_SYNC_FLUSH + })) + } else if (coding === 'deflate') { + decoders.push(zlib.createInflate()) + } else if (coding === 'br') { + decoders.push(zlib.createBrotliDecompress()) + } else { + decoders.length = 0 + break + } + } + } + + resolve({ + status, + statusText, + headersList: headers[kHeadersList], + body: decoders.length + ? pipeline(this.body, ...decoders, () => { }) + : this.body.on('error', () => {}) + }) + + return true + }, + + onData (chunk) { + if (fetchParams.controller.dump) { + return + } + + // 1. If one or more bytes have been transmitted from response’s + // message body, then: + + // 1. Let bytes be the transmitted bytes. + const bytes = chunk + + // 2. Let codings be the result of extracting header list values + // given `Content-Encoding` and response’s header list. + // See pullAlgorithm. + + // 3. Increase timingInfo’s encoded body size by bytes’s length. + timingInfo.encodedBodySize += bytes.byteLength + + // 4. See pullAlgorithm... + + return this.body.push(bytes) + }, + + onComplete () { + if (this.abort) { + fetchParams.controller.off('terminated', this.abort) + } + + fetchParams.controller.ended = true + + this.body.push(null) + }, + + onError (error) { + if (this.abort) { + fetchParams.controller.off('terminated', this.abort) + } + + this.body?.destroy(error) + + fetchParams.controller.terminate(error) + + reject(error) + }, + + onUpgrade (status, headersList, socket) { + if (status !== 101) { + return + } + + const headers = new Headers() + + for (let n = 0; n < headersList.length; n += 2) { + const key = headersList[n + 0].toString('latin1') + const val = headersList[n + 1].toString('latin1') + + headers[kHeadersList].append(key, val) + } + + resolve({ + status, + statusText: STATUS_CODES[status], + headersList: headers[kHeadersList], + socket + }) + + return true + } + } + )) + } +} + +module.exports = { + fetch, + Fetch, + fetching, + finalizeAndReportTiming +} + + +/***/ }), + +/***/ 8359: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +/* globals AbortController */ + + + +const { extractBody, mixinBody, cloneBody } = __nccwpck_require__(1472) +const { Headers, fill: fillHeaders, HeadersList } = __nccwpck_require__(554) +const { FinalizationRegistry } = __nccwpck_require__(6436)() +const util = __nccwpck_require__(3983) +const { + isValidHTTPToken, + sameOrigin, + normalizeMethod, + makePolicyContainer, + normalizeMethodRecord +} = __nccwpck_require__(2538) +const { + forbiddenMethodsSet, + corsSafeListedMethodsSet, + referrerPolicy, + requestRedirect, + requestMode, + requestCredentials, + requestCache, + requestDuplex +} = __nccwpck_require__(1037) +const { kEnumerableProperty } = util +const { kHeaders, kSignal, kState, kGuard, kRealm } = __nccwpck_require__(5861) +const { webidl } = __nccwpck_require__(1744) +const { getGlobalOrigin } = __nccwpck_require__(1246) +const { URLSerializer } = __nccwpck_require__(685) +const { kHeadersList, kConstruct } = __nccwpck_require__(2785) +const assert = __nccwpck_require__(9491) +const { getMaxListeners, setMaxListeners, getEventListeners, defaultMaxListeners } = __nccwpck_require__(2361) + +let TransformStream = globalThis.TransformStream + +const kAbortController = Symbol('abortController') + +const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => { + signal.removeEventListener('abort', abort) +}) + +// https://fetch.spec.whatwg.org/#request-class +class Request { + // https://fetch.spec.whatwg.org/#dom-request + constructor (input, init = {}) { + if (input === kConstruct) { + return + } + + webidl.argumentLengthCheck(arguments, 1, { header: 'Request constructor' }) + + input = webidl.converters.RequestInfo(input) + init = webidl.converters.RequestInit(init) + + // https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object + this[kRealm] = { + settingsObject: { + baseUrl: getGlobalOrigin(), + get origin () { + return this.baseUrl?.origin + }, + policyContainer: makePolicyContainer() + } + } + + // 1. Let request be null. + let request = null + + // 2. Let fallbackMode be null. + let fallbackMode = null + + // 3. Let baseURL be this’s relevant settings object’s API base URL. + const baseUrl = this[kRealm].settingsObject.baseUrl + + // 4. Let signal be null. + let signal = null + + // 5. If input is a string, then: + if (typeof input === 'string') { + // 1. Let parsedURL be the result of parsing input with baseURL. + // 2. If parsedURL is failure, then throw a TypeError. + let parsedURL + try { + parsedURL = new URL(input, baseUrl) + } catch (err) { + throw new TypeError('Failed to parse URL from ' + input, { cause: err }) + } + + // 3. If parsedURL includes credentials, then throw a TypeError. + if (parsedURL.username || parsedURL.password) { + throw new TypeError( + 'Request cannot be constructed from a URL that includes credentials: ' + + input + ) + } + + // 4. Set request to a new request whose URL is parsedURL. + request = makeRequest({ urlList: [parsedURL] }) + + // 5. Set fallbackMode to "cors". + fallbackMode = 'cors' + } else { + // 6. Otherwise: + + // 7. Assert: input is a Request object. + assert(input instanceof Request) + + // 8. Set request to input’s request. + request = input[kState] + + // 9. Set signal to input’s signal. + signal = input[kSignal] + } + + // 7. Let origin be this’s relevant settings object’s origin. + const origin = this[kRealm].settingsObject.origin + + // 8. Let window be "client". + let window = 'client' + + // 9. If request’s window is an environment settings object and its origin + // is same origin with origin, then set window to request’s window. + if ( + request.window?.constructor?.name === 'EnvironmentSettingsObject' && + sameOrigin(request.window, origin) + ) { + window = request.window + } + + // 10. If init["window"] exists and is non-null, then throw a TypeError. + if (init.window != null) { + throw new TypeError(`'window' option '${window}' must be null`) + } + + // 11. If init["window"] exists, then set window to "no-window". + if ('window' in init) { + window = 'no-window' + } + + // 12. Set request to a new request with the following properties: + request = makeRequest({ + // URL request’s URL. + // undici implementation note: this is set as the first item in request's urlList in makeRequest + // method request’s method. + method: request.method, + // header list A copy of request’s header list. + // undici implementation note: headersList is cloned in makeRequest + headersList: request.headersList, + // unsafe-request flag Set. + unsafeRequest: request.unsafeRequest, + // client This’s relevant settings object. + client: this[kRealm].settingsObject, + // window window. + window, + // priority request’s priority. + priority: request.priority, + // origin request’s origin. The propagation of the origin is only significant for navigation requests + // being handled by a service worker. In this scenario a request can have an origin that is different + // from the current client. + origin: request.origin, + // referrer request’s referrer. + referrer: request.referrer, + // referrer policy request’s referrer policy. + referrerPolicy: request.referrerPolicy, + // mode request’s mode. + mode: request.mode, + // credentials mode request’s credentials mode. + credentials: request.credentials, + // cache mode request’s cache mode. + cache: request.cache, + // redirect mode request’s redirect mode. + redirect: request.redirect, + // integrity metadata request’s integrity metadata. + integrity: request.integrity, + // keepalive request’s keepalive. + keepalive: request.keepalive, + // reload-navigation flag request’s reload-navigation flag. + reloadNavigation: request.reloadNavigation, + // history-navigation flag request’s history-navigation flag. + historyNavigation: request.historyNavigation, + // URL list A clone of request’s URL list. + urlList: [...request.urlList] + }) + + const initHasKey = Object.keys(init).length !== 0 + + // 13. If init is not empty, then: + if (initHasKey) { + // 1. If request’s mode is "navigate", then set it to "same-origin". + if (request.mode === 'navigate') { + request.mode = 'same-origin' + } + + // 2. Unset request’s reload-navigation flag. + request.reloadNavigation = false + + // 3. Unset request’s history-navigation flag. + request.historyNavigation = false + + // 4. Set request’s origin to "client". + request.origin = 'client' + + // 5. Set request’s referrer to "client" + request.referrer = 'client' + + // 6. Set request’s referrer policy to the empty string. + request.referrerPolicy = '' + + // 7. Set request’s URL to request’s current URL. + request.url = request.urlList[request.urlList.length - 1] + + // 8. Set request’s URL list to « request’s URL ». + request.urlList = [request.url] + } + + // 14. If init["referrer"] exists, then: + if (init.referrer !== undefined) { + // 1. Let referrer be init["referrer"]. + const referrer = init.referrer + + // 2. If referrer is the empty string, then set request’s referrer to "no-referrer". + if (referrer === '') { + request.referrer = 'no-referrer' + } else { + // 1. Let parsedReferrer be the result of parsing referrer with + // baseURL. + // 2. If parsedReferrer is failure, then throw a TypeError. + let parsedReferrer + try { + parsedReferrer = new URL(referrer, baseUrl) + } catch (err) { + throw new TypeError(`Referrer "${referrer}" is not a valid URL.`, { cause: err }) + } + + // 3. If one of the following is true + // - parsedReferrer’s scheme is "about" and path is the string "client" + // - parsedReferrer’s origin is not same origin with origin + // then set request’s referrer to "client". + if ( + (parsedReferrer.protocol === 'about:' && parsedReferrer.hostname === 'client') || + (origin && !sameOrigin(parsedReferrer, this[kRealm].settingsObject.baseUrl)) + ) { + request.referrer = 'client' + } else { + // 4. Otherwise, set request’s referrer to parsedReferrer. + request.referrer = parsedReferrer + } + } + } + + // 15. If init["referrerPolicy"] exists, then set request’s referrer policy + // to it. + if (init.referrerPolicy !== undefined) { + request.referrerPolicy = init.referrerPolicy + } + + // 16. Let mode be init["mode"] if it exists, and fallbackMode otherwise. + let mode + if (init.mode !== undefined) { + mode = init.mode + } else { + mode = fallbackMode + } + + // 17. If mode is "navigate", then throw a TypeError. + if (mode === 'navigate') { + throw webidl.errors.exception({ + header: 'Request constructor', + message: 'invalid request mode navigate.' + }) + } + + // 18. If mode is non-null, set request’s mode to mode. + if (mode != null) { + request.mode = mode + } + + // 19. If init["credentials"] exists, then set request’s credentials mode + // to it. + if (init.credentials !== undefined) { + request.credentials = init.credentials + } + + // 18. If init["cache"] exists, then set request’s cache mode to it. + if (init.cache !== undefined) { + request.cache = init.cache + } + + // 21. If request’s cache mode is "only-if-cached" and request’s mode is + // not "same-origin", then throw a TypeError. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + throw new TypeError( + "'only-if-cached' can be set only with 'same-origin' mode" + ) + } + + // 22. If init["redirect"] exists, then set request’s redirect mode to it. + if (init.redirect !== undefined) { + request.redirect = init.redirect + } + + // 23. If init["integrity"] exists, then set request’s integrity metadata to it. + if (init.integrity != null) { + request.integrity = String(init.integrity) + } + + // 24. If init["keepalive"] exists, then set request’s keepalive to it. + if (init.keepalive !== undefined) { + request.keepalive = Boolean(init.keepalive) + } + + // 25. If init["method"] exists, then: + if (init.method !== undefined) { + // 1. Let method be init["method"]. + let method = init.method + + // 2. If method is not a method or method is a forbidden method, then + // throw a TypeError. + if (!isValidHTTPToken(method)) { + throw new TypeError(`'${method}' is not a valid HTTP method.`) + } + + if (forbiddenMethodsSet.has(method.toUpperCase())) { + throw new TypeError(`'${method}' HTTP method is unsupported.`) + } + + // 3. Normalize method. + method = normalizeMethodRecord[method] ?? normalizeMethod(method) + + // 4. Set request’s method to method. + request.method = method + } + + // 26. If init["signal"] exists, then set signal to it. + if (init.signal !== undefined) { + signal = init.signal + } + + // 27. Set this’s request to request. + this[kState] = request + + // 28. Set this’s signal to a new AbortSignal object with this’s relevant + // Realm. + // TODO: could this be simplified with AbortSignal.any + // (https://dom.spec.whatwg.org/#dom-abortsignal-any) + const ac = new AbortController() + this[kSignal] = ac.signal + this[kSignal][kRealm] = this[kRealm] + + // 29. If signal is not null, then make this’s signal follow signal. + if (signal != null) { + if ( + !signal || + typeof signal.aborted !== 'boolean' || + typeof signal.addEventListener !== 'function' + ) { + throw new TypeError( + "Failed to construct 'Request': member signal is not of type AbortSignal." + ) + } + + if (signal.aborted) { + ac.abort(signal.reason) + } else { + // Keep a strong ref to ac while request object + // is alive. This is needed to prevent AbortController + // from being prematurely garbage collected. + // See, https://github.com/nodejs/undici/issues/1926. + this[kAbortController] = ac + + const acRef = new WeakRef(ac) + const abort = function () { + const ac = acRef.deref() + if (ac !== undefined) { + ac.abort(this.reason) + } + } + + // Third-party AbortControllers may not work with these. + // See, https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619. + try { + // If the max amount of listeners is equal to the default, increase it + // This is only available in node >= v19.9.0 + if (typeof getMaxListeners === 'function' && getMaxListeners(signal) === defaultMaxListeners) { + setMaxListeners(100, signal) + } else if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) { + setMaxListeners(100, signal) + } + } catch {} + + util.addAbortListener(signal, abort) + requestFinalizer.register(ac, { signal, abort }) + } + } + + // 30. Set this’s headers to a new Headers object with this’s relevant + // Realm, whose header list is request’s header list and guard is + // "request". + this[kHeaders] = new Headers(kConstruct) + this[kHeaders][kHeadersList] = request.headersList + this[kHeaders][kGuard] = 'request' + this[kHeaders][kRealm] = this[kRealm] + + // 31. If this’s request’s mode is "no-cors", then: + if (mode === 'no-cors') { + // 1. If this’s request’s method is not a CORS-safelisted method, + // then throw a TypeError. + if (!corsSafeListedMethodsSet.has(request.method)) { + throw new TypeError( + `'${request.method} is unsupported in no-cors mode.` + ) + } + + // 2. Set this’s headers’s guard to "request-no-cors". + this[kHeaders][kGuard] = 'request-no-cors' + } + + // 32. If init is not empty, then: + if (initHasKey) { + /** @type {HeadersList} */ + const headersList = this[kHeaders][kHeadersList] + // 1. Let headers be a copy of this’s headers and its associated header + // list. + // 2. If init["headers"] exists, then set headers to init["headers"]. + const headers = init.headers !== undefined ? init.headers : new HeadersList(headersList) + + // 3. Empty this’s headers’s header list. + headersList.clear() + + // 4. If headers is a Headers object, then for each header in its header + // list, append header’s name/header’s value to this’s headers. + if (headers instanceof HeadersList) { + for (const [key, val] of headers) { + headersList.append(key, val) + } + // Note: Copy the `set-cookie` meta-data. + headersList.cookies = headers.cookies + } else { + // 5. Otherwise, fill this’s headers with headers. + fillHeaders(this[kHeaders], headers) + } + } + + // 33. Let inputBody be input’s request’s body if input is a Request + // object; otherwise null. + const inputBody = input instanceof Request ? input[kState].body : null + + // 34. If either init["body"] exists and is non-null or inputBody is + // non-null, and request’s method is `GET` or `HEAD`, then throw a + // TypeError. + if ( + (init.body != null || inputBody != null) && + (request.method === 'GET' || request.method === 'HEAD') + ) { + throw new TypeError('Request with GET/HEAD method cannot have body.') + } + + // 35. Let initBody be null. + let initBody = null + + // 36. If init["body"] exists and is non-null, then: + if (init.body != null) { + // 1. Let Content-Type be null. + // 2. Set initBody and Content-Type to the result of extracting + // init["body"], with keepalive set to request’s keepalive. + const [extractedBody, contentType] = extractBody( + init.body, + request.keepalive + ) + initBody = extractedBody + + // 3, If Content-Type is non-null and this’s headers’s header list does + // not contain `Content-Type`, then append `Content-Type`/Content-Type to + // this’s headers. + if (contentType && !this[kHeaders][kHeadersList].contains('content-type')) { + this[kHeaders].append('content-type', contentType) + } + } + + // 37. Let inputOrInitBody be initBody if it is non-null; otherwise + // inputBody. + const inputOrInitBody = initBody ?? inputBody + + // 38. If inputOrInitBody is non-null and inputOrInitBody’s source is + // null, then: + if (inputOrInitBody != null && inputOrInitBody.source == null) { + // 1. If initBody is non-null and init["duplex"] does not exist, + // then throw a TypeError. + if (initBody != null && init.duplex == null) { + throw new TypeError('RequestInit: duplex option is required when sending a body.') + } + + // 2. If this’s request’s mode is neither "same-origin" nor "cors", + // then throw a TypeError. + if (request.mode !== 'same-origin' && request.mode !== 'cors') { + throw new TypeError( + 'If request is made from ReadableStream, mode should be "same-origin" or "cors"' + ) + } + + // 3. Set this’s request’s use-CORS-preflight flag. + request.useCORSPreflightFlag = true + } + + // 39. Let finalBody be inputOrInitBody. + let finalBody = inputOrInitBody + + // 40. If initBody is null and inputBody is non-null, then: + if (initBody == null && inputBody != null) { + // 1. If input is unusable, then throw a TypeError. + if (util.isDisturbed(inputBody.stream) || inputBody.stream.locked) { + throw new TypeError( + 'Cannot construct a Request with a Request object that has already been used.' + ) + } + + // 2. Set finalBody to the result of creating a proxy for inputBody. + if (!TransformStream) { + TransformStream = (__nccwpck_require__(5356).TransformStream) + } + + // https://streams.spec.whatwg.org/#readablestream-create-a-proxy + const identityTransform = new TransformStream() + inputBody.stream.pipeThrough(identityTransform) + finalBody = { + source: inputBody.source, + length: inputBody.length, + stream: identityTransform.readable + } + } + + // 41. Set this’s request’s body to finalBody. + this[kState].body = finalBody + } + + // Returns request’s HTTP method, which is "GET" by default. + get method () { + webidl.brandCheck(this, Request) + + // The method getter steps are to return this’s request’s method. + return this[kState].method + } + + // Returns the URL of request as a string. + get url () { + webidl.brandCheck(this, Request) + + // The url getter steps are to return this’s request’s URL, serialized. + return URLSerializer(this[kState].url) + } + + // Returns a Headers object consisting of the headers associated with request. + // Note that headers added in the network layer by the user agent will not + // be accounted for in this object, e.g., the "Host" header. + get headers () { + webidl.brandCheck(this, Request) + + // The headers getter steps are to return this’s headers. + return this[kHeaders] + } + + // Returns the kind of resource requested by request, e.g., "document" + // or "script". + get destination () { + webidl.brandCheck(this, Request) + + // The destination getter are to return this’s request’s destination. + return this[kState].destination + } + + // Returns the referrer of request. Its value can be a same-origin URL if + // explicitly set in init, the empty string to indicate no referrer, and + // "about:client" when defaulting to the global’s default. This is used + // during fetching to determine the value of the `Referer` header of the + // request being made. + get referrer () { + webidl.brandCheck(this, Request) + + // 1. If this’s request’s referrer is "no-referrer", then return the + // empty string. + if (this[kState].referrer === 'no-referrer') { + return '' + } + + // 2. If this’s request’s referrer is "client", then return + // "about:client". + if (this[kState].referrer === 'client') { + return 'about:client' + } + + // Return this’s request’s referrer, serialized. + return this[kState].referrer.toString() + } + + // Returns the referrer policy associated with request. + // This is used during fetching to compute the value of the request’s + // referrer. + get referrerPolicy () { + webidl.brandCheck(this, Request) + + // The referrerPolicy getter steps are to return this’s request’s referrer policy. + return this[kState].referrerPolicy + } + + // Returns the mode associated with request, which is a string indicating + // whether the request will use CORS, or will be restricted to same-origin + // URLs. + get mode () { + webidl.brandCheck(this, Request) + + // The mode getter steps are to return this’s request’s mode. + return this[kState].mode + } + + // Returns the credentials mode associated with request, + // which is a string indicating whether credentials will be sent with the + // request always, never, or only when sent to a same-origin URL. + get credentials () { + // The credentials getter steps are to return this’s request’s credentials mode. + return this[kState].credentials + } + + // Returns the cache mode associated with request, + // which is a string indicating how the request will + // interact with the browser’s cache when fetching. + get cache () { + webidl.brandCheck(this, Request) + + // The cache getter steps are to return this’s request’s cache mode. + return this[kState].cache + } + + // Returns the redirect mode associated with request, + // which is a string indicating how redirects for the + // request will be handled during fetching. A request + // will follow redirects by default. + get redirect () { + webidl.brandCheck(this, Request) + + // The redirect getter steps are to return this’s request’s redirect mode. + return this[kState].redirect + } + + // Returns request’s subresource integrity metadata, which is a + // cryptographic hash of the resource being fetched. Its value + // consists of multiple hashes separated by whitespace. [SRI] + get integrity () { + webidl.brandCheck(this, Request) + + // The integrity getter steps are to return this’s request’s integrity + // metadata. + return this[kState].integrity + } + + // Returns a boolean indicating whether or not request can outlive the + // global in which it was created. + get keepalive () { + webidl.brandCheck(this, Request) + + // The keepalive getter steps are to return this’s request’s keepalive. + return this[kState].keepalive + } + + // Returns a boolean indicating whether or not request is for a reload + // navigation. + get isReloadNavigation () { + webidl.brandCheck(this, Request) + + // The isReloadNavigation getter steps are to return true if this’s + // request’s reload-navigation flag is set; otherwise false. + return this[kState].reloadNavigation + } + + // Returns a boolean indicating whether or not request is for a history + // navigation (a.k.a. back-foward navigation). + get isHistoryNavigation () { + webidl.brandCheck(this, Request) + + // The isHistoryNavigation getter steps are to return true if this’s request’s + // history-navigation flag is set; otherwise false. + return this[kState].historyNavigation + } + + // Returns the signal associated with request, which is an AbortSignal + // object indicating whether or not request has been aborted, and its + // abort event handler. + get signal () { + webidl.brandCheck(this, Request) + + // The signal getter steps are to return this’s signal. + return this[kSignal] + } + + get body () { + webidl.brandCheck(this, Request) + + return this[kState].body ? this[kState].body.stream : null + } + + get bodyUsed () { + webidl.brandCheck(this, Request) + + return !!this[kState].body && util.isDisturbed(this[kState].body.stream) + } + + get duplex () { + webidl.brandCheck(this, Request) + + return 'half' + } + + // Returns a clone of request. + clone () { + webidl.brandCheck(this, Request) + + // 1. If this is unusable, then throw a TypeError. + if (this.bodyUsed || this.body?.locked) { + throw new TypeError('unusable') + } + + // 2. Let clonedRequest be the result of cloning this’s request. + const clonedRequest = cloneRequest(this[kState]) + + // 3. Let clonedRequestObject be the result of creating a Request object, + // given clonedRequest, this’s headers’s guard, and this’s relevant Realm. + const clonedRequestObject = new Request(kConstruct) + clonedRequestObject[kState] = clonedRequest + clonedRequestObject[kRealm] = this[kRealm] + clonedRequestObject[kHeaders] = new Headers(kConstruct) + clonedRequestObject[kHeaders][kHeadersList] = clonedRequest.headersList + clonedRequestObject[kHeaders][kGuard] = this[kHeaders][kGuard] + clonedRequestObject[kHeaders][kRealm] = this[kHeaders][kRealm] + + // 4. Make clonedRequestObject’s signal follow this’s signal. + const ac = new AbortController() + if (this.signal.aborted) { + ac.abort(this.signal.reason) + } else { + util.addAbortListener( + this.signal, + () => { + ac.abort(this.signal.reason) + } + ) + } + clonedRequestObject[kSignal] = ac.signal + + // 4. Return clonedRequestObject. + return clonedRequestObject + } +} + +mixinBody(Request) + +function makeRequest (init) { + // https://fetch.spec.whatwg.org/#requests + const request = { + method: 'GET', + localURLsOnly: false, + unsafeRequest: false, + body: null, + client: null, + reservedClient: null, + replacesClientId: '', + window: 'client', + keepalive: false, + serviceWorkers: 'all', + initiator: '', + destination: '', + priority: null, + origin: 'client', + policyContainer: 'client', + referrer: 'client', + referrerPolicy: '', + mode: 'no-cors', + useCORSPreflightFlag: false, + credentials: 'same-origin', + useCredentials: false, + cache: 'default', + redirect: 'follow', + integrity: '', + cryptoGraphicsNonceMetadata: '', + parserMetadata: '', + reloadNavigation: false, + historyNavigation: false, + userActivation: false, + taintedOrigin: false, + redirectCount: 0, + responseTainting: 'basic', + preventNoCacheCacheControlHeaderModification: false, + done: false, + timingAllowFailed: false, + ...init, + headersList: init.headersList + ? new HeadersList(init.headersList) + : new HeadersList() + } + request.url = request.urlList[0] + return request +} + +// https://fetch.spec.whatwg.org/#concept-request-clone +function cloneRequest (request) { + // To clone a request request, run these steps: + + // 1. Let newRequest be a copy of request, except for its body. + const newRequest = makeRequest({ ...request, body: null }) + + // 2. If request’s body is non-null, set newRequest’s body to the + // result of cloning request’s body. + if (request.body != null) { + newRequest.body = cloneBody(request.body) + } + + // 3. Return newRequest. + return newRequest +} + +Object.defineProperties(Request.prototype, { + method: kEnumerableProperty, + url: kEnumerableProperty, + headers: kEnumerableProperty, + redirect: kEnumerableProperty, + clone: kEnumerableProperty, + signal: kEnumerableProperty, + duplex: kEnumerableProperty, + destination: kEnumerableProperty, + body: kEnumerableProperty, + bodyUsed: kEnumerableProperty, + isHistoryNavigation: kEnumerableProperty, + isReloadNavigation: kEnumerableProperty, + keepalive: kEnumerableProperty, + integrity: kEnumerableProperty, + cache: kEnumerableProperty, + credentials: kEnumerableProperty, + attribute: kEnumerableProperty, + referrerPolicy: kEnumerableProperty, + referrer: kEnumerableProperty, + mode: kEnumerableProperty, + [Symbol.toStringTag]: { + value: 'Request', + configurable: true + } +}) + +webidl.converters.Request = webidl.interfaceConverter( + Request +) + +// https://fetch.spec.whatwg.org/#requestinfo +webidl.converters.RequestInfo = function (V) { + if (typeof V === 'string') { + return webidl.converters.USVString(V) + } + + if (V instanceof Request) { + return webidl.converters.Request(V) + } + + return webidl.converters.USVString(V) +} + +webidl.converters.AbortSignal = webidl.interfaceConverter( + AbortSignal +) + +// https://fetch.spec.whatwg.org/#requestinit +webidl.converters.RequestInit = webidl.dictionaryConverter([ + { + key: 'method', + converter: webidl.converters.ByteString + }, + { + key: 'headers', + converter: webidl.converters.HeadersInit + }, + { + key: 'body', + converter: webidl.nullableConverter( + webidl.converters.BodyInit + ) + }, + { + key: 'referrer', + converter: webidl.converters.USVString + }, + { + key: 'referrerPolicy', + converter: webidl.converters.DOMString, + // https://w3c.github.io/webappsec-referrer-policy/#referrer-policy + allowedValues: referrerPolicy + }, + { + key: 'mode', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#concept-request-mode + allowedValues: requestMode + }, + { + key: 'credentials', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestcredentials + allowedValues: requestCredentials + }, + { + key: 'cache', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestcache + allowedValues: requestCache + }, + { + key: 'redirect', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestredirect + allowedValues: requestRedirect + }, + { + key: 'integrity', + converter: webidl.converters.DOMString + }, + { + key: 'keepalive', + converter: webidl.converters.boolean + }, + { + key: 'signal', + converter: webidl.nullableConverter( + (signal) => webidl.converters.AbortSignal( + signal, + { strict: false } + ) + ) + }, + { + key: 'window', + converter: webidl.converters.any + }, + { + key: 'duplex', + converter: webidl.converters.DOMString, + allowedValues: requestDuplex + } +]) + +module.exports = { Request, makeRequest } + + +/***/ }), + +/***/ 7823: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { Headers, HeadersList, fill } = __nccwpck_require__(554) +const { extractBody, cloneBody, mixinBody } = __nccwpck_require__(1472) +const util = __nccwpck_require__(3983) +const { kEnumerableProperty } = util +const { + isValidReasonPhrase, + isCancelled, + isAborted, + isBlobLike, + serializeJavascriptValueToJSONString, + isErrorLike, + isomorphicEncode +} = __nccwpck_require__(2538) +const { + redirectStatusSet, + nullBodyStatus, + DOMException +} = __nccwpck_require__(1037) +const { kState, kHeaders, kGuard, kRealm } = __nccwpck_require__(5861) +const { webidl } = __nccwpck_require__(1744) +const { FormData } = __nccwpck_require__(2015) +const { getGlobalOrigin } = __nccwpck_require__(1246) +const { URLSerializer } = __nccwpck_require__(685) +const { kHeadersList, kConstruct } = __nccwpck_require__(2785) +const assert = __nccwpck_require__(9491) +const { types } = __nccwpck_require__(3837) + +const ReadableStream = globalThis.ReadableStream || (__nccwpck_require__(5356).ReadableStream) +const textEncoder = new TextEncoder('utf-8') + +// https://fetch.spec.whatwg.org/#response-class +class Response { + // Creates network error Response. + static error () { + // TODO + const relevantRealm = { settingsObject: {} } + + // The static error() method steps are to return the result of creating a + // Response object, given a new network error, "immutable", and this’s + // relevant Realm. + const responseObject = new Response() + responseObject[kState] = makeNetworkError() + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kHeadersList] = responseObject[kState].headersList + responseObject[kHeaders][kGuard] = 'immutable' + responseObject[kHeaders][kRealm] = relevantRealm + return responseObject + } + + // https://fetch.spec.whatwg.org/#dom-response-json + static json (data, init = {}) { + webidl.argumentLengthCheck(arguments, 1, { header: 'Response.json' }) + + if (init !== null) { + init = webidl.converters.ResponseInit(init) + } + + // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data. + const bytes = textEncoder.encode( + serializeJavascriptValueToJSONString(data) + ) + + // 2. Let body be the result of extracting bytes. + const body = extractBody(bytes) + + // 3. Let responseObject be the result of creating a Response object, given a new response, + // "response", and this’s relevant Realm. + const relevantRealm = { settingsObject: {} } + const responseObject = new Response() + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kGuard] = 'response' + responseObject[kHeaders][kRealm] = relevantRealm + + // 4. Perform initialize a response given responseObject, init, and (body, "application/json"). + initializeResponse(responseObject, init, { body: body[0], type: 'application/json' }) + + // 5. Return responseObject. + return responseObject + } + + // Creates a redirect Response that redirects to url with status status. + static redirect (url, status = 302) { + const relevantRealm = { settingsObject: {} } + + webidl.argumentLengthCheck(arguments, 1, { header: 'Response.redirect' }) + + url = webidl.converters.USVString(url) + status = webidl.converters['unsigned short'](status) + + // 1. Let parsedURL be the result of parsing url with current settings + // object’s API base URL. + // 2. If parsedURL is failure, then throw a TypeError. + // TODO: base-URL? + let parsedURL + try { + parsedURL = new URL(url, getGlobalOrigin()) + } catch (err) { + throw Object.assign(new TypeError('Failed to parse URL from ' + url), { + cause: err + }) + } + + // 3. If status is not a redirect status, then throw a RangeError. + if (!redirectStatusSet.has(status)) { + throw new RangeError('Invalid status code ' + status) + } + + // 4. Let responseObject be the result of creating a Response object, + // given a new response, "immutable", and this’s relevant Realm. + const responseObject = new Response() + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kGuard] = 'immutable' + responseObject[kHeaders][kRealm] = relevantRealm + + // 5. Set responseObject’s response’s status to status. + responseObject[kState].status = status + + // 6. Let value be parsedURL, serialized and isomorphic encoded. + const value = isomorphicEncode(URLSerializer(parsedURL)) + + // 7. Append `Location`/value to responseObject’s response’s header list. + responseObject[kState].headersList.append('location', value) + + // 8. Return responseObject. + return responseObject + } + + // https://fetch.spec.whatwg.org/#dom-response + constructor (body = null, init = {}) { + if (body !== null) { + body = webidl.converters.BodyInit(body) + } + + init = webidl.converters.ResponseInit(init) + + // TODO + this[kRealm] = { settingsObject: {} } + + // 1. Set this’s response to a new response. + this[kState] = makeResponse({}) + + // 2. Set this’s headers to a new Headers object with this’s relevant + // Realm, whose header list is this’s response’s header list and guard + // is "response". + this[kHeaders] = new Headers(kConstruct) + this[kHeaders][kGuard] = 'response' + this[kHeaders][kHeadersList] = this[kState].headersList + this[kHeaders][kRealm] = this[kRealm] + + // 3. Let bodyWithType be null. + let bodyWithType = null + + // 4. If body is non-null, then set bodyWithType to the result of extracting body. + if (body != null) { + const [extractedBody, type] = extractBody(body) + bodyWithType = { body: extractedBody, type } + } + + // 5. Perform initialize a response given this, init, and bodyWithType. + initializeResponse(this, init, bodyWithType) + } + + // Returns response’s type, e.g., "cors". + get type () { + webidl.brandCheck(this, Response) + + // The type getter steps are to return this’s response’s type. + return this[kState].type + } + + // Returns response’s URL, if it has one; otherwise the empty string. + get url () { + webidl.brandCheck(this, Response) + + const urlList = this[kState].urlList + + // The url getter steps are to return the empty string if this’s + // response’s URL is null; otherwise this’s response’s URL, + // serialized with exclude fragment set to true. + const url = urlList[urlList.length - 1] ?? null + + if (url === null) { + return '' + } + + return URLSerializer(url, true) + } + + // Returns whether response was obtained through a redirect. + get redirected () { + webidl.brandCheck(this, Response) + + // The redirected getter steps are to return true if this’s response’s URL + // list has more than one item; otherwise false. + return this[kState].urlList.length > 1 + } + + // Returns response’s status. + get status () { + webidl.brandCheck(this, Response) + + // The status getter steps are to return this’s response’s status. + return this[kState].status + } + + // Returns whether response’s status is an ok status. + get ok () { + webidl.brandCheck(this, Response) + + // The ok getter steps are to return true if this’s response’s status is an + // ok status; otherwise false. + return this[kState].status >= 200 && this[kState].status <= 299 + } + + // Returns response’s status message. + get statusText () { + webidl.brandCheck(this, Response) + + // The statusText getter steps are to return this’s response’s status + // message. + return this[kState].statusText + } + + // Returns response’s headers as Headers. + get headers () { + webidl.brandCheck(this, Response) + + // The headers getter steps are to return this’s headers. + return this[kHeaders] + } + + get body () { + webidl.brandCheck(this, Response) + + return this[kState].body ? this[kState].body.stream : null + } + + get bodyUsed () { + webidl.brandCheck(this, Response) + + return !!this[kState].body && util.isDisturbed(this[kState].body.stream) + } + + // Returns a clone of response. + clone () { + webidl.brandCheck(this, Response) + + // 1. If this is unusable, then throw a TypeError. + if (this.bodyUsed || (this.body && this.body.locked)) { + throw webidl.errors.exception({ + header: 'Response.clone', + message: 'Body has already been consumed.' + }) + } + + // 2. Let clonedResponse be the result of cloning this’s response. + const clonedResponse = cloneResponse(this[kState]) + + // 3. Return the result of creating a Response object, given + // clonedResponse, this’s headers’s guard, and this’s relevant Realm. + const clonedResponseObject = new Response() + clonedResponseObject[kState] = clonedResponse + clonedResponseObject[kRealm] = this[kRealm] + clonedResponseObject[kHeaders][kHeadersList] = clonedResponse.headersList + clonedResponseObject[kHeaders][kGuard] = this[kHeaders][kGuard] + clonedResponseObject[kHeaders][kRealm] = this[kHeaders][kRealm] + + return clonedResponseObject + } +} + +mixinBody(Response) + +Object.defineProperties(Response.prototype, { + type: kEnumerableProperty, + url: kEnumerableProperty, + status: kEnumerableProperty, + ok: kEnumerableProperty, + redirected: kEnumerableProperty, + statusText: kEnumerableProperty, + headers: kEnumerableProperty, + clone: kEnumerableProperty, + body: kEnumerableProperty, + bodyUsed: kEnumerableProperty, + [Symbol.toStringTag]: { + value: 'Response', + configurable: true + } +}) + +Object.defineProperties(Response, { + json: kEnumerableProperty, + redirect: kEnumerableProperty, + error: kEnumerableProperty +}) + +// https://fetch.spec.whatwg.org/#concept-response-clone +function cloneResponse (response) { + // To clone a response response, run these steps: + + // 1. If response is a filtered response, then return a new identical + // filtered response whose internal response is a clone of response’s + // internal response. + if (response.internalResponse) { + return filterResponse( + cloneResponse(response.internalResponse), + response.type + ) + } + + // 2. Let newResponse be a copy of response, except for its body. + const newResponse = makeResponse({ ...response, body: null }) + + // 3. If response’s body is non-null, then set newResponse’s body to the + // result of cloning response’s body. + if (response.body != null) { + newResponse.body = cloneBody(response.body) + } + + // 4. Return newResponse. + return newResponse +} + +function makeResponse (init) { + return { + aborted: false, + rangeRequested: false, + timingAllowPassed: false, + requestIncludesCredentials: false, + type: 'default', + status: 200, + timingInfo: null, + cacheState: '', + statusText: '', + ...init, + headersList: init.headersList + ? new HeadersList(init.headersList) + : new HeadersList(), + urlList: init.urlList ? [...init.urlList] : [] + } +} + +function makeNetworkError (reason) { + const isError = isErrorLike(reason) + return makeResponse({ + type: 'error', + status: 0, + error: isError + ? reason + : new Error(reason ? String(reason) : reason), + aborted: reason && reason.name === 'AbortError' + }) +} + +function makeFilteredResponse (response, state) { + state = { + internalResponse: response, + ...state + } + + return new Proxy(response, { + get (target, p) { + return p in state ? state[p] : target[p] + }, + set (target, p, value) { + assert(!(p in state)) + target[p] = value + return true + } + }) +} + +// https://fetch.spec.whatwg.org/#concept-filtered-response +function filterResponse (response, type) { + // Set response to the following filtered response with response as its + // internal response, depending on request’s response tainting: + if (type === 'basic') { + // A basic filtered response is a filtered response whose type is "basic" + // and header list excludes any headers in internal response’s header list + // whose name is a forbidden response-header name. + + // Note: undici does not implement forbidden response-header names + return makeFilteredResponse(response, { + type: 'basic', + headersList: response.headersList + }) + } else if (type === 'cors') { + // A CORS filtered response is a filtered response whose type is "cors" + // and header list excludes any headers in internal response’s header + // list whose name is not a CORS-safelisted response-header name, given + // internal response’s CORS-exposed header-name list. + + // Note: undici does not implement CORS-safelisted response-header names + return makeFilteredResponse(response, { + type: 'cors', + headersList: response.headersList + }) + } else if (type === 'opaque') { + // An opaque filtered response is a filtered response whose type is + // "opaque", URL list is the empty list, status is 0, status message + // is the empty byte sequence, header list is empty, and body is null. + + return makeFilteredResponse(response, { + type: 'opaque', + urlList: Object.freeze([]), + status: 0, + statusText: '', + body: null + }) + } else if (type === 'opaqueredirect') { + // An opaque-redirect filtered response is a filtered response whose type + // is "opaqueredirect", status is 0, status message is the empty byte + // sequence, header list is empty, and body is null. + + return makeFilteredResponse(response, { + type: 'opaqueredirect', + status: 0, + statusText: '', + headersList: [], + body: null + }) + } else { + assert(false) + } +} + +// https://fetch.spec.whatwg.org/#appropriate-network-error +function makeAppropriateNetworkError (fetchParams, err = null) { + // 1. Assert: fetchParams is canceled. + assert(isCancelled(fetchParams)) + + // 2. Return an aborted network error if fetchParams is aborted; + // otherwise return a network error. + return isAborted(fetchParams) + ? makeNetworkError(Object.assign(new DOMException('The operation was aborted.', 'AbortError'), { cause: err })) + : makeNetworkError(Object.assign(new DOMException('Request was cancelled.'), { cause: err })) +} + +// https://whatpr.org/fetch/1392.html#initialize-a-response +function initializeResponse (response, init, body) { + // 1. If init["status"] is not in the range 200 to 599, inclusive, then + // throw a RangeError. + if (init.status !== null && (init.status < 200 || init.status > 599)) { + throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.') + } + + // 2. If init["statusText"] does not match the reason-phrase token production, + // then throw a TypeError. + if ('statusText' in init && init.statusText != null) { + // See, https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2: + // reason-phrase = *( HTAB / SP / VCHAR / obs-text ) + if (!isValidReasonPhrase(String(init.statusText))) { + throw new TypeError('Invalid statusText') + } + } + + // 3. Set response’s response’s status to init["status"]. + if ('status' in init && init.status != null) { + response[kState].status = init.status + } + + // 4. Set response’s response’s status message to init["statusText"]. + if ('statusText' in init && init.statusText != null) { + response[kState].statusText = init.statusText + } + + // 5. If init["headers"] exists, then fill response’s headers with init["headers"]. + if ('headers' in init && init.headers != null) { + fill(response[kHeaders], init.headers) + } + + // 6. If body was given, then: + if (body) { + // 1. If response's status is a null body status, then throw a TypeError. + if (nullBodyStatus.includes(response.status)) { + throw webidl.errors.exception({ + header: 'Response constructor', + message: 'Invalid response status code ' + response.status + }) + } + + // 2. Set response's body to body's body. + response[kState].body = body.body + + // 3. If body's type is non-null and response's header list does not contain + // `Content-Type`, then append (`Content-Type`, body's type) to response's header list. + if (body.type != null && !response[kState].headersList.contains('Content-Type')) { + response[kState].headersList.append('content-type', body.type) + } + } +} + +webidl.converters.ReadableStream = webidl.interfaceConverter( + ReadableStream +) + +webidl.converters.FormData = webidl.interfaceConverter( + FormData +) + +webidl.converters.URLSearchParams = webidl.interfaceConverter( + URLSearchParams +) + +// https://fetch.spec.whatwg.org/#typedefdef-xmlhttprequestbodyinit +webidl.converters.XMLHttpRequestBodyInit = function (V) { + if (typeof V === 'string') { + return webidl.converters.USVString(V) + } + + if (isBlobLike(V)) { + return webidl.converters.Blob(V, { strict: false }) + } + + if (types.isArrayBuffer(V) || types.isTypedArray(V) || types.isDataView(V)) { + return webidl.converters.BufferSource(V) + } + + if (util.isFormDataLike(V)) { + return webidl.converters.FormData(V, { strict: false }) + } + + if (V instanceof URLSearchParams) { + return webidl.converters.URLSearchParams(V) + } + + return webidl.converters.DOMString(V) +} + +// https://fetch.spec.whatwg.org/#bodyinit +webidl.converters.BodyInit = function (V) { + if (V instanceof ReadableStream) { + return webidl.converters.ReadableStream(V) + } + + // Note: the spec doesn't include async iterables, + // this is an undici extension. + if (V?.[Symbol.asyncIterator]) { + return V + } + + return webidl.converters.XMLHttpRequestBodyInit(V) +} + +webidl.converters.ResponseInit = webidl.dictionaryConverter([ + { + key: 'status', + converter: webidl.converters['unsigned short'], + defaultValue: 200 + }, + { + key: 'statusText', + converter: webidl.converters.ByteString, + defaultValue: '' + }, + { + key: 'headers', + converter: webidl.converters.HeadersInit + } +]) + +module.exports = { + makeNetworkError, + makeResponse, + makeAppropriateNetworkError, + filterResponse, + Response, + cloneResponse +} + + +/***/ }), + +/***/ 5861: +/***/ ((module) => { + +"use strict"; + + +module.exports = { + kUrl: Symbol('url'), + kHeaders: Symbol('headers'), + kSignal: Symbol('signal'), + kState: Symbol('state'), + kGuard: Symbol('guard'), + kRealm: Symbol('realm') +} + + +/***/ }), + +/***/ 2538: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = __nccwpck_require__(1037) +const { getGlobalOrigin } = __nccwpck_require__(1246) +const { performance } = __nccwpck_require__(4074) +const { isBlobLike, toUSVString, ReadableStreamFrom } = __nccwpck_require__(3983) +const assert = __nccwpck_require__(9491) +const { isUint8Array } = __nccwpck_require__(4978) + +let supportedHashes = [] + +// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable +/** @type {import('crypto')|undefined} */ +let crypto + +try { + crypto = __nccwpck_require__(6113) + const possibleRelevantHashes = ['sha256', 'sha384', 'sha512'] + supportedHashes = crypto.getHashes().filter((hash) => possibleRelevantHashes.includes(hash)) +/* c8 ignore next 3 */ +} catch { +} + +function responseURL (response) { + // https://fetch.spec.whatwg.org/#responses + // A response has an associated URL. It is a pointer to the last URL + // in response’s URL list and null if response’s URL list is empty. + const urlList = response.urlList + const length = urlList.length + return length === 0 ? null : urlList[length - 1].toString() +} + +// https://fetch.spec.whatwg.org/#concept-response-location-url +function responseLocationURL (response, requestFragment) { + // 1. If response’s status is not a redirect status, then return null. + if (!redirectStatusSet.has(response.status)) { + return null + } + + // 2. Let location be the result of extracting header list values given + // `Location` and response’s header list. + let location = response.headersList.get('location') + + // 3. If location is a header value, then set location to the result of + // parsing location with response’s URL. + if (location !== null && isValidHeaderValue(location)) { + location = new URL(location, responseURL(response)) + } + + // 4. If location is a URL whose fragment is null, then set location’s + // fragment to requestFragment. + if (location && !location.hash) { + location.hash = requestFragment + } + + // 5. Return location. + return location +} + +/** @returns {URL} */ +function requestCurrentURL (request) { + return request.urlList[request.urlList.length - 1] +} + +function requestBadPort (request) { + // 1. Let url be request’s current URL. + const url = requestCurrentURL(request) + + // 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port, + // then return blocked. + if (urlIsHttpHttpsScheme(url) && badPortsSet.has(url.port)) { + return 'blocked' + } + + // 3. Return allowed. + return 'allowed' +} + +function isErrorLike (object) { + return object instanceof Error || ( + object?.constructor?.name === 'Error' || + object?.constructor?.name === 'DOMException' + ) +} + +// Check whether |statusText| is a ByteString and +// matches the Reason-Phrase token production. +// RFC 2616: https://tools.ietf.org/html/rfc2616 +// RFC 7230: https://tools.ietf.org/html/rfc7230 +// "reason-phrase = *( HTAB / SP / VCHAR / obs-text )" +// https://github.com/chromium/chromium/blob/94.0.4604.1/third_party/blink/renderer/core/fetch/response.cc#L116 +function isValidReasonPhrase (statusText) { + for (let i = 0; i < statusText.length; ++i) { + const c = statusText.charCodeAt(i) + if ( + !( + ( + c === 0x09 || // HTAB + (c >= 0x20 && c <= 0x7e) || // SP / VCHAR + (c >= 0x80 && c <= 0xff) + ) // obs-text + ) + ) { + return false + } + } + return true +} + +/** + * @see https://tools.ietf.org/html/rfc7230#section-3.2.6 + * @param {number} c + */ +function isTokenCharCode (c) { + switch (c) { + case 0x22: + case 0x28: + case 0x29: + case 0x2c: + case 0x2f: + case 0x3a: + case 0x3b: + case 0x3c: + case 0x3d: + case 0x3e: + case 0x3f: + case 0x40: + case 0x5b: + case 0x5c: + case 0x5d: + case 0x7b: + case 0x7d: + // DQUOTE and "(),/:;<=>?@[\]{}" + return false + default: + // VCHAR %x21-7E + return c >= 0x21 && c <= 0x7e + } +} + +/** + * @param {string} characters + */ +function isValidHTTPToken (characters) { + if (characters.length === 0) { + return false + } + for (let i = 0; i < characters.length; ++i) { + if (!isTokenCharCode(characters.charCodeAt(i))) { + return false + } + } + return true +} + +/** + * @see https://fetch.spec.whatwg.org/#header-name + * @param {string} potentialValue + */ +function isValidHeaderName (potentialValue) { + return isValidHTTPToken(potentialValue) +} + +/** + * @see https://fetch.spec.whatwg.org/#header-value + * @param {string} potentialValue + */ +function isValidHeaderValue (potentialValue) { + // - Has no leading or trailing HTTP tab or space bytes. + // - Contains no 0x00 (NUL) or HTTP newline bytes. + if ( + potentialValue.startsWith('\t') || + potentialValue.startsWith(' ') || + potentialValue.endsWith('\t') || + potentialValue.endsWith(' ') + ) { + return false + } + + if ( + potentialValue.includes('\0') || + potentialValue.includes('\r') || + potentialValue.includes('\n') + ) { + return false + } + + return true +} + +// https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect +function setRequestReferrerPolicyOnRedirect (request, actualResponse) { + // Given a request request and a response actualResponse, this algorithm + // updates request’s referrer policy according to the Referrer-Policy + // header (if any) in actualResponse. + + // 1. Let policy be the result of executing § 8.1 Parse a referrer policy + // from a Referrer-Policy header on actualResponse. + + // 8.1 Parse a referrer policy from a Referrer-Policy header + // 1. Let policy-tokens be the result of extracting header list values given `Referrer-Policy` and response’s header list. + const { headersList } = actualResponse + // 2. Let policy be the empty string. + // 3. For each token in policy-tokens, if token is a referrer policy and token is not the empty string, then set policy to token. + // 4. Return policy. + const policyHeader = (headersList.get('referrer-policy') ?? '').split(',') + + // Note: As the referrer-policy can contain multiple policies + // separated by comma, we need to loop through all of them + // and pick the first valid one. + // Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#specify_a_fallback_policy + let policy = '' + if (policyHeader.length > 0) { + // The right-most policy takes precedence. + // The left-most policy is the fallback. + for (let i = policyHeader.length; i !== 0; i--) { + const token = policyHeader[i - 1].trim() + if (referrerPolicyTokens.has(token)) { + policy = token + break + } + } + } + + // 2. If policy is not the empty string, then set request’s referrer policy to policy. + if (policy !== '') { + request.referrerPolicy = policy + } +} + +// https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check +function crossOriginResourcePolicyCheck () { + // TODO + return 'allowed' +} + +// https://fetch.spec.whatwg.org/#concept-cors-check +function corsCheck () { + // TODO + return 'success' +} + +// https://fetch.spec.whatwg.org/#concept-tao-check +function TAOCheck () { + // TODO + return 'success' +} + +function appendFetchMetadata (httpRequest) { + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-dest-header + // TODO + + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-mode-header + + // 1. Assert: r’s url is a potentially trustworthy URL. + // TODO + + // 2. Let header be a Structured Header whose value is a token. + let header = null + + // 3. Set header’s value to r’s mode. + header = httpRequest.mode + + // 4. Set a structured field value `Sec-Fetch-Mode`/header in r’s header list. + httpRequest.headersList.set('sec-fetch-mode', header) + + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-site-header + // TODO + + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-user-header + // TODO +} + +// https://fetch.spec.whatwg.org/#append-a-request-origin-header +function appendRequestOriginHeader (request) { + // 1. Let serializedOrigin be the result of byte-serializing a request origin with request. + let serializedOrigin = request.origin + + // 2. If request’s response tainting is "cors" or request’s mode is "websocket", then append (`Origin`, serializedOrigin) to request’s header list. + if (request.responseTainting === 'cors' || request.mode === 'websocket') { + if (serializedOrigin) { + request.headersList.append('origin', serializedOrigin) + } + + // 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then: + } else if (request.method !== 'GET' && request.method !== 'HEAD') { + // 1. Switch on request’s referrer policy: + switch (request.referrerPolicy) { + case 'no-referrer': + // Set serializedOrigin to `null`. + serializedOrigin = null + break + case 'no-referrer-when-downgrade': + case 'strict-origin': + case 'strict-origin-when-cross-origin': + // If request’s origin is a tuple origin, its scheme is "https", and request’s current URL’s scheme is not "https", then set serializedOrigin to `null`. + if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) { + serializedOrigin = null + } + break + case 'same-origin': + // If request’s origin is not same origin with request’s current URL’s origin, then set serializedOrigin to `null`. + if (!sameOrigin(request, requestCurrentURL(request))) { + serializedOrigin = null + } + break + default: + // Do nothing. + } + + if (serializedOrigin) { + // 2. Append (`Origin`, serializedOrigin) to request’s header list. + request.headersList.append('origin', serializedOrigin) + } + } +} + +function coarsenedSharedCurrentTime (crossOriginIsolatedCapability) { + // TODO + return performance.now() +} + +// https://fetch.spec.whatwg.org/#create-an-opaque-timing-info +function createOpaqueTimingInfo (timingInfo) { + return { + startTime: timingInfo.startTime ?? 0, + redirectStartTime: 0, + redirectEndTime: 0, + postRedirectStartTime: timingInfo.startTime ?? 0, + finalServiceWorkerStartTime: 0, + finalNetworkResponseStartTime: 0, + finalNetworkRequestStartTime: 0, + endTime: 0, + encodedBodySize: 0, + decodedBodySize: 0, + finalConnectionTimingInfo: null + } +} + +// https://html.spec.whatwg.org/multipage/origin.html#policy-container +function makePolicyContainer () { + // Note: the fetch spec doesn't make use of embedder policy or CSP list + return { + referrerPolicy: 'strict-origin-when-cross-origin' + } +} + +// https://html.spec.whatwg.org/multipage/origin.html#clone-a-policy-container +function clonePolicyContainer (policyContainer) { + return { + referrerPolicy: policyContainer.referrerPolicy + } +} + +// https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer +function determineRequestsReferrer (request) { + // 1. Let policy be request's referrer policy. + const policy = request.referrerPolicy + + // Note: policy cannot (shouldn't) be null or an empty string. + assert(policy) + + // 2. Let environment be request’s client. + + let referrerSource = null + + // 3. Switch on request’s referrer: + if (request.referrer === 'client') { + // Note: node isn't a browser and doesn't implement document/iframes, + // so we bypass this step and replace it with our own. + + const globalOrigin = getGlobalOrigin() + + if (!globalOrigin || globalOrigin.origin === 'null') { + return 'no-referrer' + } + + // note: we need to clone it as it's mutated + referrerSource = new URL(globalOrigin) + } else if (request.referrer instanceof URL) { + // Let referrerSource be request’s referrer. + referrerSource = request.referrer + } + + // 4. Let request’s referrerURL be the result of stripping referrerSource for + // use as a referrer. + let referrerURL = stripURLForReferrer(referrerSource) + + // 5. Let referrerOrigin be the result of stripping referrerSource for use as + // a referrer, with the origin-only flag set to true. + const referrerOrigin = stripURLForReferrer(referrerSource, true) + + // 6. If the result of serializing referrerURL is a string whose length is + // greater than 4096, set referrerURL to referrerOrigin. + if (referrerURL.toString().length > 4096) { + referrerURL = referrerOrigin + } + + const areSameOrigin = sameOrigin(request, referrerURL) + const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerURL) && + !isURLPotentiallyTrustworthy(request.url) + + // 8. Execute the switch statements corresponding to the value of policy: + switch (policy) { + case 'origin': return referrerOrigin != null ? referrerOrigin : stripURLForReferrer(referrerSource, true) + case 'unsafe-url': return referrerURL + case 'same-origin': + return areSameOrigin ? referrerOrigin : 'no-referrer' + case 'origin-when-cross-origin': + return areSameOrigin ? referrerURL : referrerOrigin + case 'strict-origin-when-cross-origin': { + const currentURL = requestCurrentURL(request) + + // 1. If the origin of referrerURL and the origin of request’s current + // URL are the same, then return referrerURL. + if (sameOrigin(referrerURL, currentURL)) { + return referrerURL + } + + // 2. If referrerURL is a potentially trustworthy URL and request’s + // current URL is not a potentially trustworthy URL, then return no + // referrer. + if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) { + return 'no-referrer' + } + + // 3. Return referrerOrigin. + return referrerOrigin + } + case 'strict-origin': // eslint-disable-line + /** + * 1. If referrerURL is a potentially trustworthy URL and + * request’s current URL is not a potentially trustworthy URL, + * then return no referrer. + * 2. Return referrerOrigin + */ + case 'no-referrer-when-downgrade': // eslint-disable-line + /** + * 1. If referrerURL is a potentially trustworthy URL and + * request’s current URL is not a potentially trustworthy URL, + * then return no referrer. + * 2. Return referrerOrigin + */ + + default: // eslint-disable-line + return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin + } +} + +/** + * @see https://w3c.github.io/webappsec-referrer-policy/#strip-url + * @param {URL} url + * @param {boolean|undefined} originOnly + */ +function stripURLForReferrer (url, originOnly) { + // 1. Assert: url is a URL. + assert(url instanceof URL) + + // 2. If url’s scheme is a local scheme, then return no referrer. + if (url.protocol === 'file:' || url.protocol === 'about:' || url.protocol === 'blank:') { + return 'no-referrer' + } + + // 3. Set url’s username to the empty string. + url.username = '' + + // 4. Set url’s password to the empty string. + url.password = '' + + // 5. Set url’s fragment to null. + url.hash = '' + + // 6. If the origin-only flag is true, then: + if (originOnly) { + // 1. Set url’s path to « the empty string ». + url.pathname = '' + + // 2. Set url’s query to null. + url.search = '' + } + + // 7. Return url. + return url +} + +function isURLPotentiallyTrustworthy (url) { + if (!(url instanceof URL)) { + return false + } + + // If child of about, return true + if (url.href === 'about:blank' || url.href === 'about:srcdoc') { + return true + } + + // If scheme is data, return true + if (url.protocol === 'data:') return true + + // If file, return true + if (url.protocol === 'file:') return true + + return isOriginPotentiallyTrustworthy(url.origin) + + function isOriginPotentiallyTrustworthy (origin) { + // If origin is explicitly null, return false + if (origin == null || origin === 'null') return false + + const originAsURL = new URL(origin) + + // If secure, return true + if (originAsURL.protocol === 'https:' || originAsURL.protocol === 'wss:') { + return true + } + + // If localhost or variants, return true + if (/^127(?:\.[0-9]+){0,2}\.[0-9]+$|^\[(?:0*:)*?:?0*1\]$/.test(originAsURL.hostname) || + (originAsURL.hostname === 'localhost' || originAsURL.hostname.includes('localhost.')) || + (originAsURL.hostname.endsWith('.localhost'))) { + return true + } + + // If any other, return false + return false + } +} + +/** + * @see https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist + * @param {Uint8Array} bytes + * @param {string} metadataList + */ +function bytesMatch (bytes, metadataList) { + // If node is not built with OpenSSL support, we cannot check + // a request's integrity, so allow it by default (the spec will + // allow requests if an invalid hash is given, as precedence). + /* istanbul ignore if: only if node is built with --without-ssl */ + if (crypto === undefined) { + return true + } + + // 1. Let parsedMetadata be the result of parsing metadataList. + const parsedMetadata = parseMetadata(metadataList) + + // 2. If parsedMetadata is no metadata, return true. + if (parsedMetadata === 'no metadata') { + return true + } + + // 3. If response is not eligible for integrity validation, return false. + // TODO + + // 4. If parsedMetadata is the empty set, return true. + if (parsedMetadata.length === 0) { + return true + } + + // 5. Let metadata be the result of getting the strongest + // metadata from parsedMetadata. + const strongest = getStrongestMetadata(parsedMetadata) + const metadata = filterMetadataListByAlgorithm(parsedMetadata, strongest) + + // 6. For each item in metadata: + for (const item of metadata) { + // 1. Let algorithm be the alg component of item. + const algorithm = item.algo + + // 2. Let expectedValue be the val component of item. + const expectedValue = item.hash + + // See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e + // "be liberal with padding". This is annoying, and it's not even in the spec. + + // 3. Let actualValue be the result of applying algorithm to bytes. + let actualValue = crypto.createHash(algorithm).update(bytes).digest('base64') + + if (actualValue[actualValue.length - 1] === '=') { + if (actualValue[actualValue.length - 2] === '=') { + actualValue = actualValue.slice(0, -2) + } else { + actualValue = actualValue.slice(0, -1) + } + } + + // 4. If actualValue is a case-sensitive match for expectedValue, + // return true. + if (compareBase64Mixed(actualValue, expectedValue)) { + return true + } + } + + // 7. Return false. + return false +} + +// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options +// https://www.w3.org/TR/CSP2/#source-list-syntax +// https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1 +const parseHashWithOptions = /(?sha256|sha384|sha512)-((?[A-Za-z0-9+/]+|[A-Za-z0-9_-]+)={0,2}(?:\s|$)( +[!-~]*)?)?/i + +/** + * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata + * @param {string} metadata + */ +function parseMetadata (metadata) { + // 1. Let result be the empty set. + /** @type {{ algo: string, hash: string }[]} */ + const result = [] + + // 2. Let empty be equal to true. + let empty = true + + // 3. For each token returned by splitting metadata on spaces: + for (const token of metadata.split(' ')) { + // 1. Set empty to false. + empty = false + + // 2. Parse token as a hash-with-options. + const parsedToken = parseHashWithOptions.exec(token) + + // 3. If token does not parse, continue to the next token. + if ( + parsedToken === null || + parsedToken.groups === undefined || + parsedToken.groups.algo === undefined + ) { + // Note: Chromium blocks the request at this point, but Firefox + // gives a warning that an invalid integrity was given. The + // correct behavior is to ignore these, and subsequently not + // check the integrity of the resource. + continue + } + + // 4. Let algorithm be the hash-algo component of token. + const algorithm = parsedToken.groups.algo.toLowerCase() + + // 5. If algorithm is a hash function recognized by the user + // agent, add the parsed token to result. + if (supportedHashes.includes(algorithm)) { + result.push(parsedToken.groups) + } + } + + // 4. Return no metadata if empty is true, otherwise return result. + if (empty === true) { + return 'no metadata' + } + + return result +} + +/** + * @param {{ algo: 'sha256' | 'sha384' | 'sha512' }[]} metadataList + */ +function getStrongestMetadata (metadataList) { + // Let algorithm be the algo component of the first item in metadataList. + // Can be sha256 + let algorithm = metadataList[0].algo + // If the algorithm is sha512, then it is the strongest + // and we can return immediately + if (algorithm[3] === '5') { + return algorithm + } + + for (let i = 1; i < metadataList.length; ++i) { + const metadata = metadataList[i] + // If the algorithm is sha512, then it is the strongest + // and we can break the loop immediately + if (metadata.algo[3] === '5') { + algorithm = 'sha512' + break + // If the algorithm is sha384, then a potential sha256 or sha384 is ignored + } else if (algorithm[3] === '3') { + continue + // algorithm is sha256, check if algorithm is sha384 and if so, set it as + // the strongest + } else if (metadata.algo[3] === '3') { + algorithm = 'sha384' + } + } + return algorithm +} + +function filterMetadataListByAlgorithm (metadataList, algorithm) { + if (metadataList.length === 1) { + return metadataList + } + + let pos = 0 + for (let i = 0; i < metadataList.length; ++i) { + if (metadataList[i].algo === algorithm) { + metadataList[pos++] = metadataList[i] + } + } + + metadataList.length = pos + + return metadataList +} + +/** + * Compares two base64 strings, allowing for base64url + * in the second string. + * +* @param {string} actualValue always base64 + * @param {string} expectedValue base64 or base64url + * @returns {boolean} + */ +function compareBase64Mixed (actualValue, expectedValue) { + if (actualValue.length !== expectedValue.length) { + return false + } + for (let i = 0; i < actualValue.length; ++i) { + if (actualValue[i] !== expectedValue[i]) { + if ( + (actualValue[i] === '+' && expectedValue[i] === '-') || + (actualValue[i] === '/' && expectedValue[i] === '_') + ) { + continue + } + return false + } + } + + return true +} + +// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request +function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) { + // TODO +} + +/** + * @link {https://html.spec.whatwg.org/multipage/origin.html#same-origin} + * @param {URL} A + * @param {URL} B + */ +function sameOrigin (A, B) { + // 1. If A and B are the same opaque origin, then return true. + if (A.origin === B.origin && A.origin === 'null') { + return true + } + + // 2. If A and B are both tuple origins and their schemes, + // hosts, and port are identical, then return true. + if (A.protocol === B.protocol && A.hostname === B.hostname && A.port === B.port) { + return true + } + + // 3. Return false. + return false +} + +function createDeferredPromise () { + let res + let rej + const promise = new Promise((resolve, reject) => { + res = resolve + rej = reject + }) + + return { promise, resolve: res, reject: rej } +} + +function isAborted (fetchParams) { + return fetchParams.controller.state === 'aborted' +} + +function isCancelled (fetchParams) { + return fetchParams.controller.state === 'aborted' || + fetchParams.controller.state === 'terminated' +} + +const normalizeMethodRecord = { + delete: 'DELETE', + DELETE: 'DELETE', + get: 'GET', + GET: 'GET', + head: 'HEAD', + HEAD: 'HEAD', + options: 'OPTIONS', + OPTIONS: 'OPTIONS', + post: 'POST', + POST: 'POST', + put: 'PUT', + PUT: 'PUT' +} + +// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`. +Object.setPrototypeOf(normalizeMethodRecord, null) + +/** + * @see https://fetch.spec.whatwg.org/#concept-method-normalize + * @param {string} method + */ +function normalizeMethod (method) { + return normalizeMethodRecord[method.toLowerCase()] ?? method +} + +// https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string +function serializeJavascriptValueToJSONString (value) { + // 1. Let result be ? Call(%JSON.stringify%, undefined, « value »). + const result = JSON.stringify(value) + + // 2. If result is undefined, then throw a TypeError. + if (result === undefined) { + throw new TypeError('Value is not JSON serializable') + } + + // 3. Assert: result is a string. + assert(typeof result === 'string') + + // 4. Return result. + return result +} + +// https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object +const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())) + +/** + * @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object + * @param {() => unknown[]} iterator + * @param {string} name name of the instance + * @param {'key'|'value'|'key+value'} kind + */ +function makeIterator (iterator, name, kind) { + const object = { + index: 0, + kind, + target: iterator + } + + const i = { + next () { + // 1. Let interface be the interface for which the iterator prototype object exists. + + // 2. Let thisValue be the this value. + + // 3. Let object be ? ToObject(thisValue). + + // 4. If object is a platform object, then perform a security + // check, passing: + + // 5. If object is not a default iterator object for interface, + // then throw a TypeError. + if (Object.getPrototypeOf(this) !== i) { + throw new TypeError( + `'next' called on an object that does not implement interface ${name} Iterator.` + ) + } + + // 6. Let index be object’s index. + // 7. Let kind be object’s kind. + // 8. Let values be object’s target's value pairs to iterate over. + const { index, kind, target } = object + const values = target() + + // 9. Let len be the length of values. + const len = values.length + + // 10. If index is greater than or equal to len, then return + // CreateIterResultObject(undefined, true). + if (index >= len) { + return { value: undefined, done: true } + } + + // 11. Let pair be the entry in values at index index. + const pair = values[index] + + // 12. Set object’s index to index + 1. + object.index = index + 1 + + // 13. Return the iterator result for pair and kind. + return iteratorResult(pair, kind) + }, + // The class string of an iterator prototype object for a given interface is the + // result of concatenating the identifier of the interface and the string " Iterator". + [Symbol.toStringTag]: `${name} Iterator` + } + + // The [[Prototype]] internal slot of an iterator prototype object must be %IteratorPrototype%. + Object.setPrototypeOf(i, esIteratorPrototype) + // esIteratorPrototype needs to be the prototype of i + // which is the prototype of an empty object. Yes, it's confusing. + return Object.setPrototypeOf({}, i) +} + +// https://webidl.spec.whatwg.org/#iterator-result +function iteratorResult (pair, kind) { + let result + + // 1. Let result be a value determined by the value of kind: + switch (kind) { + case 'key': { + // 1. Let idlKey be pair’s key. + // 2. Let key be the result of converting idlKey to an + // ECMAScript value. + // 3. result is key. + result = pair[0] + break + } + case 'value': { + // 1. Let idlValue be pair’s value. + // 2. Let value be the result of converting idlValue to + // an ECMAScript value. + // 3. result is value. + result = pair[1] + break + } + case 'key+value': { + // 1. Let idlKey be pair’s key. + // 2. Let idlValue be pair’s value. + // 3. Let key be the result of converting idlKey to an + // ECMAScript value. + // 4. Let value be the result of converting idlValue to + // an ECMAScript value. + // 5. Let array be ! ArrayCreate(2). + // 6. Call ! CreateDataProperty(array, "0", key). + // 7. Call ! CreateDataProperty(array, "1", value). + // 8. result is array. + result = pair + break + } + } + + // 2. Return CreateIterResultObject(result, false). + return { value: result, done: false } +} + +/** + * @see https://fetch.spec.whatwg.org/#body-fully-read + */ +async function fullyReadBody (body, processBody, processBodyError) { + // 1. If taskDestination is null, then set taskDestination to + // the result of starting a new parallel queue. + + // 2. Let successSteps given a byte sequence bytes be to queue a + // fetch task to run processBody given bytes, with taskDestination. + const successSteps = processBody + + // 3. Let errorSteps be to queue a fetch task to run processBodyError, + // with taskDestination. + const errorSteps = processBodyError + + // 4. Let reader be the result of getting a reader for body’s stream. + // If that threw an exception, then run errorSteps with that + // exception and return. + let reader + + try { + reader = body.stream.getReader() + } catch (e) { + errorSteps(e) + return + } + + // 5. Read all bytes from reader, given successSteps and errorSteps. + try { + const result = await readAllBytes(reader) + successSteps(result) + } catch (e) { + errorSteps(e) + } +} + +/** @type {ReadableStream} */ +let ReadableStream = globalThis.ReadableStream + +function isReadableStreamLike (stream) { + if (!ReadableStream) { + ReadableStream = (__nccwpck_require__(5356).ReadableStream) + } + + return stream instanceof ReadableStream || ( + stream[Symbol.toStringTag] === 'ReadableStream' && + typeof stream.tee === 'function' + ) +} + +const MAXIMUM_ARGUMENT_LENGTH = 65535 + +/** + * @see https://infra.spec.whatwg.org/#isomorphic-decode + * @param {number[]|Uint8Array} input + */ +function isomorphicDecode (input) { + // 1. To isomorphic decode a byte sequence input, return a string whose code point + // length is equal to input’s length and whose code points have the same values + // as the values of input’s bytes, in the same order. + + if (input.length < MAXIMUM_ARGUMENT_LENGTH) { + return String.fromCharCode(...input) + } + + return input.reduce((previous, current) => previous + String.fromCharCode(current), '') +} + +/** + * @param {ReadableStreamController} controller + */ +function readableStreamClose (controller) { + try { + controller.close() + } catch (err) { + // TODO: add comment explaining why this error occurs. + if (!err.message.includes('Controller is already closed')) { + throw err + } + } +} + +/** + * @see https://infra.spec.whatwg.org/#isomorphic-encode + * @param {string} input + */ +function isomorphicEncode (input) { + // 1. Assert: input contains no code points greater than U+00FF. + for (let i = 0; i < input.length; i++) { + assert(input.charCodeAt(i) <= 0xFF) + } + + // 2. Return a byte sequence whose length is equal to input’s code + // point length and whose bytes have the same values as the + // values of input’s code points, in the same order + return input +} + +/** + * @see https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes + * @see https://streams.spec.whatwg.org/#read-loop + * @param {ReadableStreamDefaultReader} reader + */ +async function readAllBytes (reader) { + const bytes = [] + let byteLength = 0 + + while (true) { + const { done, value: chunk } = await reader.read() + + if (done) { + // 1. Call successSteps with bytes. + return Buffer.concat(bytes, byteLength) + } + + // 1. If chunk is not a Uint8Array object, call failureSteps + // with a TypeError and abort these steps. + if (!isUint8Array(chunk)) { + throw new TypeError('Received non-Uint8Array chunk') + } + + // 2. Append the bytes represented by chunk to bytes. + bytes.push(chunk) + byteLength += chunk.length + + // 3. Read-loop given reader, bytes, successSteps, and failureSteps. + } +} + +/** + * @see https://fetch.spec.whatwg.org/#is-local + * @param {URL} url + */ +function urlIsLocal (url) { + assert('protocol' in url) // ensure it's a url object + + const protocol = url.protocol + + return protocol === 'about:' || protocol === 'blob:' || protocol === 'data:' +} + +/** + * @param {string|URL} url + */ +function urlHasHttpsScheme (url) { + if (typeof url === 'string') { + return url.startsWith('https:') + } + + return url.protocol === 'https:' +} + +/** + * @see https://fetch.spec.whatwg.org/#http-scheme + * @param {URL} url + */ +function urlIsHttpHttpsScheme (url) { + assert('protocol' in url) // ensure it's a url object + + const protocol = url.protocol + + return protocol === 'http:' || protocol === 'https:' +} + +/** + * Fetch supports node >= 16.8.0, but Object.hasOwn was added in v16.9.0. + */ +const hasOwn = Object.hasOwn || ((dict, key) => Object.prototype.hasOwnProperty.call(dict, key)) + +module.exports = { + isAborted, + isCancelled, + createDeferredPromise, + ReadableStreamFrom, + toUSVString, + tryUpgradeRequestToAPotentiallyTrustworthyURL, + coarsenedSharedCurrentTime, + determineRequestsReferrer, + makePolicyContainer, + clonePolicyContainer, + appendFetchMetadata, + appendRequestOriginHeader, + TAOCheck, + corsCheck, + crossOriginResourcePolicyCheck, + createOpaqueTimingInfo, + setRequestReferrerPolicyOnRedirect, + isValidHTTPToken, + requestBadPort, + requestCurrentURL, + responseURL, + responseLocationURL, + isBlobLike, + isURLPotentiallyTrustworthy, + isValidReasonPhrase, + sameOrigin, + normalizeMethod, + serializeJavascriptValueToJSONString, + makeIterator, + isValidHeaderName, + isValidHeaderValue, + hasOwn, + isErrorLike, + fullyReadBody, + bytesMatch, + isReadableStreamLike, + readableStreamClose, + isomorphicEncode, + isomorphicDecode, + urlIsLocal, + urlHasHttpsScheme, + urlIsHttpHttpsScheme, + readAllBytes, + normalizeMethodRecord, + parseMetadata +} + + +/***/ }), + +/***/ 1744: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { types } = __nccwpck_require__(3837) +const { hasOwn, toUSVString } = __nccwpck_require__(2538) + +/** @type {import('../../types/webidl').Webidl} */ +const webidl = {} +webidl.converters = {} +webidl.util = {} +webidl.errors = {} + +webidl.errors.exception = function (message) { + return new TypeError(`${message.header}: ${message.message}`) +} + +webidl.errors.conversionFailed = function (context) { + const plural = context.types.length === 1 ? '' : ' one of' + const message = + `${context.argument} could not be converted to` + + `${plural}: ${context.types.join(', ')}.` + + return webidl.errors.exception({ + header: context.prefix, + message + }) +} + +webidl.errors.invalidArgument = function (context) { + return webidl.errors.exception({ + header: context.prefix, + message: `"${context.value}" is an invalid ${context.type}.` + }) +} + +// https://webidl.spec.whatwg.org/#implements +webidl.brandCheck = function (V, I, opts = undefined) { + if (opts?.strict !== false && !(V instanceof I)) { + throw new TypeError('Illegal invocation') + } else { + return V?.[Symbol.toStringTag] === I.prototype[Symbol.toStringTag] + } +} + +webidl.argumentLengthCheck = function ({ length }, min, ctx) { + if (length < min) { + throw webidl.errors.exception({ + message: `${min} argument${min !== 1 ? 's' : ''} required, ` + + `but${length ? ' only' : ''} ${length} found.`, + ...ctx + }) + } +} + +webidl.illegalConstructor = function () { + throw webidl.errors.exception({ + header: 'TypeError', + message: 'Illegal constructor' + }) +} + +// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values +webidl.util.Type = function (V) { + switch (typeof V) { + case 'undefined': return 'Undefined' + case 'boolean': return 'Boolean' + case 'string': return 'String' + case 'symbol': return 'Symbol' + case 'number': return 'Number' + case 'bigint': return 'BigInt' + case 'function': + case 'object': { + if (V === null) { + return 'Null' + } + + return 'Object' + } + } +} + +// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint +webidl.util.ConvertToInt = function (V, bitLength, signedness, opts = {}) { + let upperBound + let lowerBound + + // 1. If bitLength is 64, then: + if (bitLength === 64) { + // 1. Let upperBound be 2^53 − 1. + upperBound = Math.pow(2, 53) - 1 + + // 2. If signedness is "unsigned", then let lowerBound be 0. + if (signedness === 'unsigned') { + lowerBound = 0 + } else { + // 3. Otherwise let lowerBound be −2^53 + 1. + lowerBound = Math.pow(-2, 53) + 1 + } + } else if (signedness === 'unsigned') { + // 2. Otherwise, if signedness is "unsigned", then: + + // 1. Let lowerBound be 0. + lowerBound = 0 + + // 2. Let upperBound be 2^bitLength − 1. + upperBound = Math.pow(2, bitLength) - 1 + } else { + // 3. Otherwise: + + // 1. Let lowerBound be -2^bitLength − 1. + lowerBound = Math.pow(-2, bitLength) - 1 + + // 2. Let upperBound be 2^bitLength − 1 − 1. + upperBound = Math.pow(2, bitLength - 1) - 1 + } + + // 4. Let x be ? ToNumber(V). + let x = Number(V) + + // 5. If x is −0, then set x to +0. + if (x === 0) { + x = 0 + } + + // 6. If the conversion is to an IDL type associated + // with the [EnforceRange] extended attribute, then: + if (opts.enforceRange === true) { + // 1. If x is NaN, +∞, or −∞, then throw a TypeError. + if ( + Number.isNaN(x) || + x === Number.POSITIVE_INFINITY || + x === Number.NEGATIVE_INFINITY + ) { + throw webidl.errors.exception({ + header: 'Integer conversion', + message: `Could not convert ${V} to an integer.` + }) + } + + // 2. Set x to IntegerPart(x). + x = webidl.util.IntegerPart(x) + + // 3. If x < lowerBound or x > upperBound, then + // throw a TypeError. + if (x < lowerBound || x > upperBound) { + throw webidl.errors.exception({ + header: 'Integer conversion', + message: `Value must be between ${lowerBound}-${upperBound}, got ${x}.` + }) + } + + // 4. Return x. + return x + } + + // 7. If x is not NaN and the conversion is to an IDL + // type associated with the [Clamp] extended + // attribute, then: + if (!Number.isNaN(x) && opts.clamp === true) { + // 1. Set x to min(max(x, lowerBound), upperBound). + x = Math.min(Math.max(x, lowerBound), upperBound) + + // 2. Round x to the nearest integer, choosing the + // even integer if it lies halfway between two, + // and choosing +0 rather than −0. + if (Math.floor(x) % 2 === 0) { + x = Math.floor(x) + } else { + x = Math.ceil(x) + } + + // 3. Return x. + return x + } + + // 8. If x is NaN, +0, +∞, or −∞, then return +0. + if ( + Number.isNaN(x) || + (x === 0 && Object.is(0, x)) || + x === Number.POSITIVE_INFINITY || + x === Number.NEGATIVE_INFINITY + ) { + return 0 + } + + // 9. Set x to IntegerPart(x). + x = webidl.util.IntegerPart(x) + + // 10. Set x to x modulo 2^bitLength. + x = x % Math.pow(2, bitLength) + + // 11. If signedness is "signed" and x ≥ 2^bitLength − 1, + // then return x − 2^bitLength. + if (signedness === 'signed' && x >= Math.pow(2, bitLength) - 1) { + return x - Math.pow(2, bitLength) + } + + // 12. Otherwise, return x. + return x +} + +// https://webidl.spec.whatwg.org/#abstract-opdef-integerpart +webidl.util.IntegerPart = function (n) { + // 1. Let r be floor(abs(n)). + const r = Math.floor(Math.abs(n)) + + // 2. If n < 0, then return -1 × r. + if (n < 0) { + return -1 * r + } + + // 3. Otherwise, return r. + return r +} + +// https://webidl.spec.whatwg.org/#es-sequence +webidl.sequenceConverter = function (converter) { + return (V) => { + // 1. If Type(V) is not Object, throw a TypeError. + if (webidl.util.Type(V) !== 'Object') { + throw webidl.errors.exception({ + header: 'Sequence', + message: `Value of type ${webidl.util.Type(V)} is not an Object.` + }) + } + + // 2. Let method be ? GetMethod(V, @@iterator). + /** @type {Generator} */ + const method = V?.[Symbol.iterator]?.() + const seq = [] + + // 3. If method is undefined, throw a TypeError. + if ( + method === undefined || + typeof method.next !== 'function' + ) { + throw webidl.errors.exception({ + header: 'Sequence', + message: 'Object is not an iterator.' + }) + } + + // https://webidl.spec.whatwg.org/#create-sequence-from-iterable + while (true) { + const { done, value } = method.next() + + if (done) { + break + } + + seq.push(converter(value)) + } + + return seq + } +} + +// https://webidl.spec.whatwg.org/#es-to-record +webidl.recordConverter = function (keyConverter, valueConverter) { + return (O) => { + // 1. If Type(O) is not Object, throw a TypeError. + if (webidl.util.Type(O) !== 'Object') { + throw webidl.errors.exception({ + header: 'Record', + message: `Value of type ${webidl.util.Type(O)} is not an Object.` + }) + } + + // 2. Let result be a new empty instance of record. + const result = {} + + if (!types.isProxy(O)) { + // Object.keys only returns enumerable properties + const keys = Object.keys(O) + + for (const key of keys) { + // 1. Let typedKey be key converted to an IDL value of type K. + const typedKey = keyConverter(key) + + // 2. Let value be ? Get(O, key). + // 3. Let typedValue be value converted to an IDL value of type V. + const typedValue = valueConverter(O[key]) + + // 4. Set result[typedKey] to typedValue. + result[typedKey] = typedValue + } + + // 5. Return result. + return result + } + + // 3. Let keys be ? O.[[OwnPropertyKeys]](). + const keys = Reflect.ownKeys(O) + + // 4. For each key of keys. + for (const key of keys) { + // 1. Let desc be ? O.[[GetOwnProperty]](key). + const desc = Reflect.getOwnPropertyDescriptor(O, key) + + // 2. If desc is not undefined and desc.[[Enumerable]] is true: + if (desc?.enumerable) { + // 1. Let typedKey be key converted to an IDL value of type K. + const typedKey = keyConverter(key) + + // 2. Let value be ? Get(O, key). + // 3. Let typedValue be value converted to an IDL value of type V. + const typedValue = valueConverter(O[key]) + + // 4. Set result[typedKey] to typedValue. + result[typedKey] = typedValue + } + } + + // 5. Return result. + return result + } +} + +webidl.interfaceConverter = function (i) { + return (V, opts = {}) => { + if (opts.strict !== false && !(V instanceof i)) { + throw webidl.errors.exception({ + header: i.name, + message: `Expected ${V} to be an instance of ${i.name}.` + }) + } + + return V + } +} + +webidl.dictionaryConverter = function (converters) { + return (dictionary) => { + const type = webidl.util.Type(dictionary) + const dict = {} + + if (type === 'Null' || type === 'Undefined') { + return dict + } else if (type !== 'Object') { + throw webidl.errors.exception({ + header: 'Dictionary', + message: `Expected ${dictionary} to be one of: Null, Undefined, Object.` + }) + } + + for (const options of converters) { + const { key, defaultValue, required, converter } = options + + if (required === true) { + if (!hasOwn(dictionary, key)) { + throw webidl.errors.exception({ + header: 'Dictionary', + message: `Missing required key "${key}".` + }) + } + } + + let value = dictionary[key] + const hasDefault = hasOwn(options, 'defaultValue') + + // Only use defaultValue if value is undefined and + // a defaultValue options was provided. + if (hasDefault && value !== null) { + value = value ?? defaultValue + } + + // A key can be optional and have no default value. + // When this happens, do not perform a conversion, + // and do not assign the key a value. + if (required || hasDefault || value !== undefined) { + value = converter(value) + + if ( + options.allowedValues && + !options.allowedValues.includes(value) + ) { + throw webidl.errors.exception({ + header: 'Dictionary', + message: `${value} is not an accepted type. Expected one of ${options.allowedValues.join(', ')}.` + }) + } + + dict[key] = value + } + } + + return dict + } +} + +webidl.nullableConverter = function (converter) { + return (V) => { + if (V === null) { + return V + } + + return converter(V) + } +} + +// https://webidl.spec.whatwg.org/#es-DOMString +webidl.converters.DOMString = function (V, opts = {}) { + // 1. If V is null and the conversion is to an IDL type + // associated with the [LegacyNullToEmptyString] + // extended attribute, then return the DOMString value + // that represents the empty string. + if (V === null && opts.legacyNullToEmptyString) { + return '' + } + + // 2. Let x be ? ToString(V). + if (typeof V === 'symbol') { + throw new TypeError('Could not convert argument of type symbol to string.') + } + + // 3. Return the IDL DOMString value that represents the + // same sequence of code units as the one the + // ECMAScript String value x represents. + return String(V) +} + +// https://webidl.spec.whatwg.org/#es-ByteString +webidl.converters.ByteString = function (V) { + // 1. Let x be ? ToString(V). + // Note: DOMString converter perform ? ToString(V) + const x = webidl.converters.DOMString(V) + + // 2. If the value of any element of x is greater than + // 255, then throw a TypeError. + for (let index = 0; index < x.length; index++) { + if (x.charCodeAt(index) > 255) { + throw new TypeError( + 'Cannot convert argument to a ByteString because the character at ' + + `index ${index} has a value of ${x.charCodeAt(index)} which is greater than 255.` + ) + } + } + + // 3. Return an IDL ByteString value whose length is the + // length of x, and where the value of each element is + // the value of the corresponding element of x. + return x +} + +// https://webidl.spec.whatwg.org/#es-USVString +webidl.converters.USVString = toUSVString + +// https://webidl.spec.whatwg.org/#es-boolean +webidl.converters.boolean = function (V) { + // 1. Let x be the result of computing ToBoolean(V). + const x = Boolean(V) + + // 2. Return the IDL boolean value that is the one that represents + // the same truth value as the ECMAScript Boolean value x. + return x +} + +// https://webidl.spec.whatwg.org/#es-any +webidl.converters.any = function (V) { + return V +} + +// https://webidl.spec.whatwg.org/#es-long-long +webidl.converters['long long'] = function (V) { + // 1. Let x be ? ConvertToInt(V, 64, "signed"). + const x = webidl.util.ConvertToInt(V, 64, 'signed') + + // 2. Return the IDL long long value that represents + // the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#es-unsigned-long-long +webidl.converters['unsigned long long'] = function (V) { + // 1. Let x be ? ConvertToInt(V, 64, "unsigned"). + const x = webidl.util.ConvertToInt(V, 64, 'unsigned') + + // 2. Return the IDL unsigned long long value that + // represents the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#es-unsigned-long +webidl.converters['unsigned long'] = function (V) { + // 1. Let x be ? ConvertToInt(V, 32, "unsigned"). + const x = webidl.util.ConvertToInt(V, 32, 'unsigned') + + // 2. Return the IDL unsigned long value that + // represents the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#es-unsigned-short +webidl.converters['unsigned short'] = function (V, opts) { + // 1. Let x be ? ConvertToInt(V, 16, "unsigned"). + const x = webidl.util.ConvertToInt(V, 16, 'unsigned', opts) + + // 2. Return the IDL unsigned short value that represents + // the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#idl-ArrayBuffer +webidl.converters.ArrayBuffer = function (V, opts = {}) { + // 1. If Type(V) is not Object, or V does not have an + // [[ArrayBufferData]] internal slot, then throw a + // TypeError. + // see: https://tc39.es/ecma262/#sec-properties-of-the-arraybuffer-instances + // see: https://tc39.es/ecma262/#sec-properties-of-the-sharedarraybuffer-instances + if ( + webidl.util.Type(V) !== 'Object' || + !types.isAnyArrayBuffer(V) + ) { + throw webidl.errors.conversionFailed({ + prefix: `${V}`, + argument: `${V}`, + types: ['ArrayBuffer'] + }) + } + + // 2. If the conversion is not to an IDL type associated + // with the [AllowShared] extended attribute, and + // IsSharedArrayBuffer(V) is true, then throw a + // TypeError. + if (opts.allowShared === false && types.isSharedArrayBuffer(V)) { + throw webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'SharedArrayBuffer is not allowed.' + }) + } + + // 3. If the conversion is not to an IDL type associated + // with the [AllowResizable] extended attribute, and + // IsResizableArrayBuffer(V) is true, then throw a + // TypeError. + // Note: resizable ArrayBuffers are currently a proposal. + + // 4. Return the IDL ArrayBuffer value that is a + // reference to the same object as V. + return V +} + +webidl.converters.TypedArray = function (V, T, opts = {}) { + // 1. Let T be the IDL type V is being converted to. + + // 2. If Type(V) is not Object, or V does not have a + // [[TypedArrayName]] internal slot with a value + // equal to T’s name, then throw a TypeError. + if ( + webidl.util.Type(V) !== 'Object' || + !types.isTypedArray(V) || + V.constructor.name !== T.name + ) { + throw webidl.errors.conversionFailed({ + prefix: `${T.name}`, + argument: `${V}`, + types: [T.name] + }) + } + + // 3. If the conversion is not to an IDL type associated + // with the [AllowShared] extended attribute, and + // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is + // true, then throw a TypeError. + if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + throw webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'SharedArrayBuffer is not allowed.' + }) + } + + // 4. If the conversion is not to an IDL type associated + // with the [AllowResizable] extended attribute, and + // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is + // true, then throw a TypeError. + // Note: resizable array buffers are currently a proposal + + // 5. Return the IDL value of type T that is a reference + // to the same object as V. + return V +} + +webidl.converters.DataView = function (V, opts = {}) { + // 1. If Type(V) is not Object, or V does not have a + // [[DataView]] internal slot, then throw a TypeError. + if (webidl.util.Type(V) !== 'Object' || !types.isDataView(V)) { + throw webidl.errors.exception({ + header: 'DataView', + message: 'Object is not a DataView.' + }) + } + + // 2. If the conversion is not to an IDL type associated + // with the [AllowShared] extended attribute, and + // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is true, + // then throw a TypeError. + if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + throw webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'SharedArrayBuffer is not allowed.' + }) + } + + // 3. If the conversion is not to an IDL type associated + // with the [AllowResizable] extended attribute, and + // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is + // true, then throw a TypeError. + // Note: resizable ArrayBuffers are currently a proposal + + // 4. Return the IDL DataView value that is a reference + // to the same object as V. + return V +} + +// https://webidl.spec.whatwg.org/#BufferSource +webidl.converters.BufferSource = function (V, opts = {}) { + if (types.isAnyArrayBuffer(V)) { + return webidl.converters.ArrayBuffer(V, opts) + } + + if (types.isTypedArray(V)) { + return webidl.converters.TypedArray(V, V.constructor) + } + + if (types.isDataView(V)) { + return webidl.converters.DataView(V, opts) + } + + throw new TypeError(`Could not convert ${V} to a BufferSource.`) +} + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.ByteString +) + +webidl.converters['sequence>'] = webidl.sequenceConverter( + webidl.converters['sequence'] +) + +webidl.converters['record'] = webidl.recordConverter( + webidl.converters.ByteString, + webidl.converters.ByteString +) + +module.exports = { + webidl +} + + +/***/ }), + +/***/ 4854: +/***/ ((module) => { + +"use strict"; + + +/** + * @see https://encoding.spec.whatwg.org/#concept-encoding-get + * @param {string|undefined} label + */ +function getEncoding (label) { + if (!label) { + return 'failure' + } + + // 1. Remove any leading and trailing ASCII whitespace from label. + // 2. If label is an ASCII case-insensitive match for any of the + // labels listed in the table below, then return the + // corresponding encoding; otherwise return failure. + switch (label.trim().toLowerCase()) { + case 'unicode-1-1-utf-8': + case 'unicode11utf8': + case 'unicode20utf8': + case 'utf-8': + case 'utf8': + case 'x-unicode20utf8': + return 'UTF-8' + case '866': + case 'cp866': + case 'csibm866': + case 'ibm866': + return 'IBM866' + case 'csisolatin2': + case 'iso-8859-2': + case 'iso-ir-101': + case 'iso8859-2': + case 'iso88592': + case 'iso_8859-2': + case 'iso_8859-2:1987': + case 'l2': + case 'latin2': + return 'ISO-8859-2' + case 'csisolatin3': + case 'iso-8859-3': + case 'iso-ir-109': + case 'iso8859-3': + case 'iso88593': + case 'iso_8859-3': + case 'iso_8859-3:1988': + case 'l3': + case 'latin3': + return 'ISO-8859-3' + case 'csisolatin4': + case 'iso-8859-4': + case 'iso-ir-110': + case 'iso8859-4': + case 'iso88594': + case 'iso_8859-4': + case 'iso_8859-4:1988': + case 'l4': + case 'latin4': + return 'ISO-8859-4' + case 'csisolatincyrillic': + case 'cyrillic': + case 'iso-8859-5': + case 'iso-ir-144': + case 'iso8859-5': + case 'iso88595': + case 'iso_8859-5': + case 'iso_8859-5:1988': + return 'ISO-8859-5' + case 'arabic': + case 'asmo-708': + case 'csiso88596e': + case 'csiso88596i': + case 'csisolatinarabic': + case 'ecma-114': + case 'iso-8859-6': + case 'iso-8859-6-e': + case 'iso-8859-6-i': + case 'iso-ir-127': + case 'iso8859-6': + case 'iso88596': + case 'iso_8859-6': + case 'iso_8859-6:1987': + return 'ISO-8859-6' + case 'csisolatingreek': + case 'ecma-118': + case 'elot_928': + case 'greek': + case 'greek8': + case 'iso-8859-7': + case 'iso-ir-126': + case 'iso8859-7': + case 'iso88597': + case 'iso_8859-7': + case 'iso_8859-7:1987': + case 'sun_eu_greek': + return 'ISO-8859-7' + case 'csiso88598e': + case 'csisolatinhebrew': + case 'hebrew': + case 'iso-8859-8': + case 'iso-8859-8-e': + case 'iso-ir-138': + case 'iso8859-8': + case 'iso88598': + case 'iso_8859-8': + case 'iso_8859-8:1988': + case 'visual': + return 'ISO-8859-8' + case 'csiso88598i': + case 'iso-8859-8-i': + case 'logical': + return 'ISO-8859-8-I' + case 'csisolatin6': + case 'iso-8859-10': + case 'iso-ir-157': + case 'iso8859-10': + case 'iso885910': + case 'l6': + case 'latin6': + return 'ISO-8859-10' + case 'iso-8859-13': + case 'iso8859-13': + case 'iso885913': + return 'ISO-8859-13' + case 'iso-8859-14': + case 'iso8859-14': + case 'iso885914': + return 'ISO-8859-14' + case 'csisolatin9': + case 'iso-8859-15': + case 'iso8859-15': + case 'iso885915': + case 'iso_8859-15': + case 'l9': + return 'ISO-8859-15' + case 'iso-8859-16': + return 'ISO-8859-16' + case 'cskoi8r': + case 'koi': + case 'koi8': + case 'koi8-r': + case 'koi8_r': + return 'KOI8-R' + case 'koi8-ru': + case 'koi8-u': + return 'KOI8-U' + case 'csmacintosh': + case 'mac': + case 'macintosh': + case 'x-mac-roman': + return 'macintosh' + case 'iso-8859-11': + case 'iso8859-11': + case 'iso885911': + case 'tis-620': + case 'windows-874': + return 'windows-874' + case 'cp1250': + case 'windows-1250': + case 'x-cp1250': + return 'windows-1250' + case 'cp1251': + case 'windows-1251': + case 'x-cp1251': + return 'windows-1251' + case 'ansi_x3.4-1968': + case 'ascii': + case 'cp1252': + case 'cp819': + case 'csisolatin1': + case 'ibm819': + case 'iso-8859-1': + case 'iso-ir-100': + case 'iso8859-1': + case 'iso88591': + case 'iso_8859-1': + case 'iso_8859-1:1987': + case 'l1': + case 'latin1': + case 'us-ascii': + case 'windows-1252': + case 'x-cp1252': + return 'windows-1252' + case 'cp1253': + case 'windows-1253': + case 'x-cp1253': + return 'windows-1253' + case 'cp1254': + case 'csisolatin5': + case 'iso-8859-9': + case 'iso-ir-148': + case 'iso8859-9': + case 'iso88599': + case 'iso_8859-9': + case 'iso_8859-9:1989': + case 'l5': + case 'latin5': + case 'windows-1254': + case 'x-cp1254': + return 'windows-1254' + case 'cp1255': + case 'windows-1255': + case 'x-cp1255': + return 'windows-1255' + case 'cp1256': + case 'windows-1256': + case 'x-cp1256': + return 'windows-1256' + case 'cp1257': + case 'windows-1257': + case 'x-cp1257': + return 'windows-1257' + case 'cp1258': + case 'windows-1258': + case 'x-cp1258': + return 'windows-1258' + case 'x-mac-cyrillic': + case 'x-mac-ukrainian': + return 'x-mac-cyrillic' + case 'chinese': + case 'csgb2312': + case 'csiso58gb231280': + case 'gb2312': + case 'gb_2312': + case 'gb_2312-80': + case 'gbk': + case 'iso-ir-58': + case 'x-gbk': + return 'GBK' + case 'gb18030': + return 'gb18030' + case 'big5': + case 'big5-hkscs': + case 'cn-big5': + case 'csbig5': + case 'x-x-big5': + return 'Big5' + case 'cseucpkdfmtjapanese': + case 'euc-jp': + case 'x-euc-jp': + return 'EUC-JP' + case 'csiso2022jp': + case 'iso-2022-jp': + return 'ISO-2022-JP' + case 'csshiftjis': + case 'ms932': + case 'ms_kanji': + case 'shift-jis': + case 'shift_jis': + case 'sjis': + case 'windows-31j': + case 'x-sjis': + return 'Shift_JIS' + case 'cseuckr': + case 'csksc56011987': + case 'euc-kr': + case 'iso-ir-149': + case 'korean': + case 'ks_c_5601-1987': + case 'ks_c_5601-1989': + case 'ksc5601': + case 'ksc_5601': + case 'windows-949': + return 'EUC-KR' + case 'csiso2022kr': + case 'hz-gb-2312': + case 'iso-2022-cn': + case 'iso-2022-cn-ext': + case 'iso-2022-kr': + case 'replacement': + return 'replacement' + case 'unicodefffe': + case 'utf-16be': + return 'UTF-16BE' + case 'csunicode': + case 'iso-10646-ucs-2': + case 'ucs-2': + case 'unicode': + case 'unicodefeff': + case 'utf-16': + case 'utf-16le': + return 'UTF-16LE' + case 'x-user-defined': + return 'x-user-defined' + default: return 'failure' + } +} + +module.exports = { + getEncoding +} + + +/***/ }), + +/***/ 1446: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + staticPropertyDescriptors, + readOperation, + fireAProgressEvent +} = __nccwpck_require__(7530) +const { + kState, + kError, + kResult, + kEvents, + kAborted +} = __nccwpck_require__(9054) +const { webidl } = __nccwpck_require__(1744) +const { kEnumerableProperty } = __nccwpck_require__(3983) + +class FileReader extends EventTarget { + constructor () { + super() + + this[kState] = 'empty' + this[kResult] = null + this[kError] = null + this[kEvents] = { + loadend: null, + error: null, + abort: null, + load: null, + progress: null, + loadstart: null + } + } + + /** + * @see https://w3c.github.io/FileAPI/#dfn-readAsArrayBuffer + * @param {import('buffer').Blob} blob + */ + readAsArrayBuffer (blob) { + webidl.brandCheck(this, FileReader) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsArrayBuffer' }) + + blob = webidl.converters.Blob(blob, { strict: false }) + + // The readAsArrayBuffer(blob) method, when invoked, + // must initiate a read operation for blob with ArrayBuffer. + readOperation(this, blob, 'ArrayBuffer') + } + + /** + * @see https://w3c.github.io/FileAPI/#readAsBinaryString + * @param {import('buffer').Blob} blob + */ + readAsBinaryString (blob) { + webidl.brandCheck(this, FileReader) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsBinaryString' }) + + blob = webidl.converters.Blob(blob, { strict: false }) + + // The readAsBinaryString(blob) method, when invoked, + // must initiate a read operation for blob with BinaryString. + readOperation(this, blob, 'BinaryString') + } + + /** + * @see https://w3c.github.io/FileAPI/#readAsDataText + * @param {import('buffer').Blob} blob + * @param {string?} encoding + */ + readAsText (blob, encoding = undefined) { + webidl.brandCheck(this, FileReader) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsText' }) + + blob = webidl.converters.Blob(blob, { strict: false }) + + if (encoding !== undefined) { + encoding = webidl.converters.DOMString(encoding) + } + + // The readAsText(blob, encoding) method, when invoked, + // must initiate a read operation for blob with Text and encoding. + readOperation(this, blob, 'Text', encoding) + } + + /** + * @see https://w3c.github.io/FileAPI/#dfn-readAsDataURL + * @param {import('buffer').Blob} blob + */ + readAsDataURL (blob) { + webidl.brandCheck(this, FileReader) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsDataURL' }) + + blob = webidl.converters.Blob(blob, { strict: false }) + + // The readAsDataURL(blob) method, when invoked, must + // initiate a read operation for blob with DataURL. + readOperation(this, blob, 'DataURL') + } + + /** + * @see https://w3c.github.io/FileAPI/#dfn-abort + */ + abort () { + // 1. If this's state is "empty" or if this's state is + // "done" set this's result to null and terminate + // this algorithm. + if (this[kState] === 'empty' || this[kState] === 'done') { + this[kResult] = null + return + } + + // 2. If this's state is "loading" set this's state to + // "done" and set this's result to null. + if (this[kState] === 'loading') { + this[kState] = 'done' + this[kResult] = null + } + + // 3. If there are any tasks from this on the file reading + // task source in an affiliated task queue, then remove + // those tasks from that task queue. + this[kAborted] = true + + // 4. Terminate the algorithm for the read method being processed. + // TODO + + // 5. Fire a progress event called abort at this. + fireAProgressEvent('abort', this) + + // 6. If this's state is not "loading", fire a progress + // event called loadend at this. + if (this[kState] !== 'loading') { + fireAProgressEvent('loadend', this) + } + } + + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-readystate + */ + get readyState () { + webidl.brandCheck(this, FileReader) + + switch (this[kState]) { + case 'empty': return this.EMPTY + case 'loading': return this.LOADING + case 'done': return this.DONE + } + } + + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-result + */ + get result () { + webidl.brandCheck(this, FileReader) + + // The result attribute’s getter, when invoked, must return + // this's result. + return this[kResult] + } + + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-error + */ + get error () { + webidl.brandCheck(this, FileReader) + + // The error attribute’s getter, when invoked, must return + // this's error. + return this[kError] + } + + get onloadend () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].loadend + } + + set onloadend (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].loadend) { + this.removeEventListener('loadend', this[kEvents].loadend) + } + + if (typeof fn === 'function') { + this[kEvents].loadend = fn + this.addEventListener('loadend', fn) + } else { + this[kEvents].loadend = null + } + } + + get onerror () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].error + } + + set onerror (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].error) { + this.removeEventListener('error', this[kEvents].error) + } + + if (typeof fn === 'function') { + this[kEvents].error = fn + this.addEventListener('error', fn) + } else { + this[kEvents].error = null + } + } + + get onloadstart () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].loadstart + } + + set onloadstart (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].loadstart) { + this.removeEventListener('loadstart', this[kEvents].loadstart) + } + + if (typeof fn === 'function') { + this[kEvents].loadstart = fn + this.addEventListener('loadstart', fn) + } else { + this[kEvents].loadstart = null + } + } + + get onprogress () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].progress + } + + set onprogress (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].progress) { + this.removeEventListener('progress', this[kEvents].progress) + } + + if (typeof fn === 'function') { + this[kEvents].progress = fn + this.addEventListener('progress', fn) + } else { + this[kEvents].progress = null + } + } + + get onload () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].load + } + + set onload (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].load) { + this.removeEventListener('load', this[kEvents].load) + } + + if (typeof fn === 'function') { + this[kEvents].load = fn + this.addEventListener('load', fn) + } else { + this[kEvents].load = null + } + } + + get onabort () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].abort + } + + set onabort (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].abort) { + this.removeEventListener('abort', this[kEvents].abort) + } + + if (typeof fn === 'function') { + this[kEvents].abort = fn + this.addEventListener('abort', fn) + } else { + this[kEvents].abort = null + } + } +} + +// https://w3c.github.io/FileAPI/#dom-filereader-empty +FileReader.EMPTY = FileReader.prototype.EMPTY = 0 +// https://w3c.github.io/FileAPI/#dom-filereader-loading +FileReader.LOADING = FileReader.prototype.LOADING = 1 +// https://w3c.github.io/FileAPI/#dom-filereader-done +FileReader.DONE = FileReader.prototype.DONE = 2 + +Object.defineProperties(FileReader.prototype, { + EMPTY: staticPropertyDescriptors, + LOADING: staticPropertyDescriptors, + DONE: staticPropertyDescriptors, + readAsArrayBuffer: kEnumerableProperty, + readAsBinaryString: kEnumerableProperty, + readAsText: kEnumerableProperty, + readAsDataURL: kEnumerableProperty, + abort: kEnumerableProperty, + readyState: kEnumerableProperty, + result: kEnumerableProperty, + error: kEnumerableProperty, + onloadstart: kEnumerableProperty, + onprogress: kEnumerableProperty, + onload: kEnumerableProperty, + onabort: kEnumerableProperty, + onerror: kEnumerableProperty, + onloadend: kEnumerableProperty, + [Symbol.toStringTag]: { + value: 'FileReader', + writable: false, + enumerable: false, + configurable: true + } +}) + +Object.defineProperties(FileReader, { + EMPTY: staticPropertyDescriptors, + LOADING: staticPropertyDescriptors, + DONE: staticPropertyDescriptors +}) + +module.exports = { + FileReader +} + + +/***/ }), + +/***/ 5504: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { webidl } = __nccwpck_require__(1744) + +const kState = Symbol('ProgressEvent state') + +/** + * @see https://xhr.spec.whatwg.org/#progressevent + */ +class ProgressEvent extends Event { + constructor (type, eventInitDict = {}) { + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.ProgressEventInit(eventInitDict ?? {}) + + super(type, eventInitDict) + + this[kState] = { + lengthComputable: eventInitDict.lengthComputable, + loaded: eventInitDict.loaded, + total: eventInitDict.total + } + } + + get lengthComputable () { + webidl.brandCheck(this, ProgressEvent) + + return this[kState].lengthComputable + } + + get loaded () { + webidl.brandCheck(this, ProgressEvent) + + return this[kState].loaded + } + + get total () { + webidl.brandCheck(this, ProgressEvent) + + return this[kState].total + } +} + +webidl.converters.ProgressEventInit = webidl.dictionaryConverter([ + { + key: 'lengthComputable', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'loaded', + converter: webidl.converters['unsigned long long'], + defaultValue: 0 + }, + { + key: 'total', + converter: webidl.converters['unsigned long long'], + defaultValue: 0 + }, + { + key: 'bubbles', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'cancelable', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'composed', + converter: webidl.converters.boolean, + defaultValue: false + } +]) + +module.exports = { + ProgressEvent +} + + +/***/ }), + +/***/ 9054: +/***/ ((module) => { + +"use strict"; + + +module.exports = { + kState: Symbol('FileReader state'), + kResult: Symbol('FileReader result'), + kError: Symbol('FileReader error'), + kLastProgressEventFired: Symbol('FileReader last progress event fired timestamp'), + kEvents: Symbol('FileReader events'), + kAborted: Symbol('FileReader aborted') +} + + +/***/ }), + +/***/ 7530: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + kState, + kError, + kResult, + kAborted, + kLastProgressEventFired +} = __nccwpck_require__(9054) +const { ProgressEvent } = __nccwpck_require__(5504) +const { getEncoding } = __nccwpck_require__(4854) +const { DOMException } = __nccwpck_require__(1037) +const { serializeAMimeType, parseMIMEType } = __nccwpck_require__(685) +const { types } = __nccwpck_require__(3837) +const { StringDecoder } = __nccwpck_require__(1576) +const { btoa } = __nccwpck_require__(4300) + +/** @type {PropertyDescriptor} */ +const staticPropertyDescriptors = { + enumerable: true, + writable: false, + configurable: false +} + +/** + * @see https://w3c.github.io/FileAPI/#readOperation + * @param {import('./filereader').FileReader} fr + * @param {import('buffer').Blob} blob + * @param {string} type + * @param {string?} encodingName + */ +function readOperation (fr, blob, type, encodingName) { + // 1. If fr’s state is "loading", throw an InvalidStateError + // DOMException. + if (fr[kState] === 'loading') { + throw new DOMException('Invalid state', 'InvalidStateError') + } + + // 2. Set fr’s state to "loading". + fr[kState] = 'loading' + + // 3. Set fr’s result to null. + fr[kResult] = null + + // 4. Set fr’s error to null. + fr[kError] = null + + // 5. Let stream be the result of calling get stream on blob. + /** @type {import('stream/web').ReadableStream} */ + const stream = blob.stream() + + // 6. Let reader be the result of getting a reader from stream. + const reader = stream.getReader() + + // 7. Let bytes be an empty byte sequence. + /** @type {Uint8Array[]} */ + const bytes = [] + + // 8. Let chunkPromise be the result of reading a chunk from + // stream with reader. + let chunkPromise = reader.read() + + // 9. Let isFirstChunk be true. + let isFirstChunk = true + + // 10. In parallel, while true: + // Note: "In parallel" just means non-blocking + // Note 2: readOperation itself cannot be async as double + // reading the body would then reject the promise, instead + // of throwing an error. + ;(async () => { + while (!fr[kAborted]) { + // 1. Wait for chunkPromise to be fulfilled or rejected. + try { + const { done, value } = await chunkPromise + + // 2. If chunkPromise is fulfilled, and isFirstChunk is + // true, queue a task to fire a progress event called + // loadstart at fr. + if (isFirstChunk && !fr[kAborted]) { + queueMicrotask(() => { + fireAProgressEvent('loadstart', fr) + }) + } + + // 3. Set isFirstChunk to false. + isFirstChunk = false + + // 4. If chunkPromise is fulfilled with an object whose + // done property is false and whose value property is + // a Uint8Array object, run these steps: + if (!done && types.isUint8Array(value)) { + // 1. Let bs be the byte sequence represented by the + // Uint8Array object. + + // 2. Append bs to bytes. + bytes.push(value) + + // 3. If roughly 50ms have passed since these steps + // were last invoked, queue a task to fire a + // progress event called progress at fr. + if ( + ( + fr[kLastProgressEventFired] === undefined || + Date.now() - fr[kLastProgressEventFired] >= 50 + ) && + !fr[kAborted] + ) { + fr[kLastProgressEventFired] = Date.now() + queueMicrotask(() => { + fireAProgressEvent('progress', fr) + }) + } + + // 4. Set chunkPromise to the result of reading a + // chunk from stream with reader. + chunkPromise = reader.read() + } else if (done) { + // 5. Otherwise, if chunkPromise is fulfilled with an + // object whose done property is true, queue a task + // to run the following steps and abort this algorithm: + queueMicrotask(() => { + // 1. Set fr’s state to "done". + fr[kState] = 'done' + + // 2. Let result be the result of package data given + // bytes, type, blob’s type, and encodingName. + try { + const result = packageData(bytes, type, blob.type, encodingName) + + // 4. Else: + + if (fr[kAborted]) { + return + } + + // 1. Set fr’s result to result. + fr[kResult] = result + + // 2. Fire a progress event called load at the fr. + fireAProgressEvent('load', fr) + } catch (error) { + // 3. If package data threw an exception error: + + // 1. Set fr’s error to error. + fr[kError] = error + + // 2. Fire a progress event called error at fr. + fireAProgressEvent('error', fr) + } + + // 5. If fr’s state is not "loading", fire a progress + // event called loadend at the fr. + if (fr[kState] !== 'loading') { + fireAProgressEvent('loadend', fr) + } + }) + + break + } + } catch (error) { + if (fr[kAborted]) { + return + } + + // 6. Otherwise, if chunkPromise is rejected with an + // error error, queue a task to run the following + // steps and abort this algorithm: + queueMicrotask(() => { + // 1. Set fr’s state to "done". + fr[kState] = 'done' + + // 2. Set fr’s error to error. + fr[kError] = error + + // 3. Fire a progress event called error at fr. + fireAProgressEvent('error', fr) + + // 4. If fr’s state is not "loading", fire a progress + // event called loadend at fr. + if (fr[kState] !== 'loading') { + fireAProgressEvent('loadend', fr) + } + }) + + break + } + } + })() +} + +/** + * @see https://w3c.github.io/FileAPI/#fire-a-progress-event + * @see https://dom.spec.whatwg.org/#concept-event-fire + * @param {string} e The name of the event + * @param {import('./filereader').FileReader} reader + */ +function fireAProgressEvent (e, reader) { + // The progress event e does not bubble. e.bubbles must be false + // The progress event e is NOT cancelable. e.cancelable must be false + const event = new ProgressEvent(e, { + bubbles: false, + cancelable: false + }) + + reader.dispatchEvent(event) +} + +/** + * @see https://w3c.github.io/FileAPI/#blob-package-data + * @param {Uint8Array[]} bytes + * @param {string} type + * @param {string?} mimeType + * @param {string?} encodingName + */ +function packageData (bytes, type, mimeType, encodingName) { + // 1. A Blob has an associated package data algorithm, given + // bytes, a type, a optional mimeType, and a optional + // encodingName, which switches on type and runs the + // associated steps: + + switch (type) { + case 'DataURL': { + // 1. Return bytes as a DataURL [RFC2397] subject to + // the considerations below: + // * Use mimeType as part of the Data URL if it is + // available in keeping with the Data URL + // specification [RFC2397]. + // * If mimeType is not available return a Data URL + // without a media-type. [RFC2397]. + + // https://datatracker.ietf.org/doc/html/rfc2397#section-3 + // dataurl := "data:" [ mediatype ] [ ";base64" ] "," data + // mediatype := [ type "/" subtype ] *( ";" parameter ) + // data := *urlchar + // parameter := attribute "=" value + let dataURL = 'data:' + + const parsed = parseMIMEType(mimeType || 'application/octet-stream') + + if (parsed !== 'failure') { + dataURL += serializeAMimeType(parsed) + } + + dataURL += ';base64,' + + const decoder = new StringDecoder('latin1') + + for (const chunk of bytes) { + dataURL += btoa(decoder.write(chunk)) + } + + dataURL += btoa(decoder.end()) + + return dataURL + } + case 'Text': { + // 1. Let encoding be failure + let encoding = 'failure' + + // 2. If the encodingName is present, set encoding to the + // result of getting an encoding from encodingName. + if (encodingName) { + encoding = getEncoding(encodingName) + } + + // 3. If encoding is failure, and mimeType is present: + if (encoding === 'failure' && mimeType) { + // 1. Let type be the result of parse a MIME type + // given mimeType. + const type = parseMIMEType(mimeType) + + // 2. If type is not failure, set encoding to the result + // of getting an encoding from type’s parameters["charset"]. + if (type !== 'failure') { + encoding = getEncoding(type.parameters.get('charset')) + } + } + + // 4. If encoding is failure, then set encoding to UTF-8. + if (encoding === 'failure') { + encoding = 'UTF-8' + } + + // 5. Decode bytes using fallback encoding encoding, and + // return the result. + return decode(bytes, encoding) + } + case 'ArrayBuffer': { + // Return a new ArrayBuffer whose contents are bytes. + const sequence = combineByteSequences(bytes) + + return sequence.buffer + } + case 'BinaryString': { + // Return bytes as a binary string, in which every byte + // is represented by a code unit of equal value [0..255]. + let binaryString = '' + + const decoder = new StringDecoder('latin1') + + for (const chunk of bytes) { + binaryString += decoder.write(chunk) + } + + binaryString += decoder.end() + + return binaryString + } + } +} + +/** + * @see https://encoding.spec.whatwg.org/#decode + * @param {Uint8Array[]} ioQueue + * @param {string} encoding + */ +function decode (ioQueue, encoding) { + const bytes = combineByteSequences(ioQueue) + + // 1. Let BOMEncoding be the result of BOM sniffing ioQueue. + const BOMEncoding = BOMSniffing(bytes) + + let slice = 0 + + // 2. If BOMEncoding is non-null: + if (BOMEncoding !== null) { + // 1. Set encoding to BOMEncoding. + encoding = BOMEncoding + + // 2. Read three bytes from ioQueue, if BOMEncoding is + // UTF-8; otherwise read two bytes. + // (Do nothing with those bytes.) + slice = BOMEncoding === 'UTF-8' ? 3 : 2 + } + + // 3. Process a queue with an instance of encoding’s + // decoder, ioQueue, output, and "replacement". + + // 4. Return output. + + const sliced = bytes.slice(slice) + return new TextDecoder(encoding).decode(sliced) +} + +/** + * @see https://encoding.spec.whatwg.org/#bom-sniff + * @param {Uint8Array} ioQueue + */ +function BOMSniffing (ioQueue) { + // 1. Let BOM be the result of peeking 3 bytes from ioQueue, + // converted to a byte sequence. + const [a, b, c] = ioQueue + + // 2. For each of the rows in the table below, starting with + // the first one and going down, if BOM starts with the + // bytes given in the first column, then return the + // encoding given in the cell in the second column of that + // row. Otherwise, return null. + if (a === 0xEF && b === 0xBB && c === 0xBF) { + return 'UTF-8' + } else if (a === 0xFE && b === 0xFF) { + return 'UTF-16BE' + } else if (a === 0xFF && b === 0xFE) { + return 'UTF-16LE' + } + + return null +} + +/** + * @param {Uint8Array[]} sequences + */ +function combineByteSequences (sequences) { + const size = sequences.reduce((a, b) => { + return a + b.byteLength + }, 0) + + let offset = 0 + + return sequences.reduce((a, b) => { + a.set(b, offset) + offset += b.byteLength + return a + }, new Uint8Array(size)) +} + +module.exports = { + staticPropertyDescriptors, + readOperation, + fireAProgressEvent +} + + +/***/ }), + +/***/ 1892: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +// We include a version number for the Dispatcher API. In case of breaking changes, +// this version number must be increased to avoid conflicts. +const globalDispatcher = Symbol.for('undici.globalDispatcher.1') +const { InvalidArgumentError } = __nccwpck_require__(8045) +const Agent = __nccwpck_require__(7890) + +if (getGlobalDispatcher() === undefined) { + setGlobalDispatcher(new Agent()) +} + +function setGlobalDispatcher (agent) { + if (!agent || typeof agent.dispatch !== 'function') { + throw new InvalidArgumentError('Argument agent must implement Agent') + } + Object.defineProperty(globalThis, globalDispatcher, { + value: agent, + writable: true, + enumerable: false, + configurable: false + }) +} + +function getGlobalDispatcher () { + return globalThis[globalDispatcher] +} + +module.exports = { + setGlobalDispatcher, + getGlobalDispatcher +} + + +/***/ }), + +/***/ 6930: +/***/ ((module) => { + +"use strict"; + + +module.exports = class DecoratorHandler { + constructor (handler) { + this.handler = handler + } + + onConnect (...args) { + return this.handler.onConnect(...args) + } + + onError (...args) { + return this.handler.onError(...args) + } + + onUpgrade (...args) { + return this.handler.onUpgrade(...args) + } + + onHeaders (...args) { + return this.handler.onHeaders(...args) + } + + onData (...args) { + return this.handler.onData(...args) + } + + onComplete (...args) { + return this.handler.onComplete(...args) + } + + onBodySent (...args) { + return this.handler.onBodySent(...args) + } +} + + +/***/ }), + +/***/ 2860: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const util = __nccwpck_require__(3983) +const { kBodyUsed } = __nccwpck_require__(2785) +const assert = __nccwpck_require__(9491) +const { InvalidArgumentError } = __nccwpck_require__(8045) +const EE = __nccwpck_require__(2361) + +const redirectableStatusCodes = [300, 301, 302, 303, 307, 308] + +const kBody = Symbol('body') + +class BodyAsyncIterable { + constructor (body) { + this[kBody] = body + this[kBodyUsed] = false + } + + async * [Symbol.asyncIterator] () { + assert(!this[kBodyUsed], 'disturbed') + this[kBodyUsed] = true + yield * this[kBody] + } +} + +class RedirectHandler { + constructor (dispatch, maxRedirections, opts, handler) { + if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) { + throw new InvalidArgumentError('maxRedirections must be a positive number') + } + + util.validateHandler(handler, opts.method, opts.upgrade) + + this.dispatch = dispatch + this.location = null + this.abort = null + this.opts = { ...opts, maxRedirections: 0 } // opts must be a copy + this.maxRedirections = maxRedirections + this.handler = handler + this.history = [] + + if (util.isStream(this.opts.body)) { + // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp + // so that it can be dispatched again? + // TODO (fix): Do we need 100-expect support to provide a way to do this properly? + if (util.bodyLength(this.opts.body) === 0) { + this.opts.body + .on('data', function () { + assert(false) + }) + } + + if (typeof this.opts.body.readableDidRead !== 'boolean') { + this.opts.body[kBodyUsed] = false + EE.prototype.on.call(this.opts.body, 'data', function () { + this[kBodyUsed] = true + }) + } + } else if (this.opts.body && typeof this.opts.body.pipeTo === 'function') { + // TODO (fix): We can't access ReadableStream internal state + // to determine whether or not it has been disturbed. This is just + // a workaround. + this.opts.body = new BodyAsyncIterable(this.opts.body) + } else if ( + this.opts.body && + typeof this.opts.body !== 'string' && + !ArrayBuffer.isView(this.opts.body) && + util.isIterable(this.opts.body) + ) { + // TODO: Should we allow re-using iterable if !this.opts.idempotent + // or through some other flag? + this.opts.body = new BodyAsyncIterable(this.opts.body) + } + } + + onConnect (abort) { + this.abort = abort + this.handler.onConnect(abort, { history: this.history }) + } + + onUpgrade (statusCode, headers, socket) { + this.handler.onUpgrade(statusCode, headers, socket) + } + + onError (error) { + this.handler.onError(error) + } + + onHeaders (statusCode, headers, resume, statusText) { + this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body) + ? null + : parseLocation(statusCode, headers) + + if (this.opts.origin) { + this.history.push(new URL(this.opts.path, this.opts.origin)) + } + + if (!this.location) { + return this.handler.onHeaders(statusCode, headers, resume, statusText) + } + + const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin))) + const path = search ? `${pathname}${search}` : pathname + + // Remove headers referring to the original URL. + // By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers. + // https://tools.ietf.org/html/rfc7231#section-6.4 + this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin) + this.opts.path = path + this.opts.origin = origin + this.opts.maxRedirections = 0 + this.opts.query = null + + // https://tools.ietf.org/html/rfc7231#section-6.4.4 + // In case of HTTP 303, always replace method to be either HEAD or GET + if (statusCode === 303 && this.opts.method !== 'HEAD') { + this.opts.method = 'GET' + this.opts.body = null + } + } + + onData (chunk) { + if (this.location) { + /* + https://tools.ietf.org/html/rfc7231#section-6.4 + + TLDR: undici always ignores 3xx response bodies. + + Redirection is used to serve the requested resource from another URL, so it is assumes that + no body is generated (and thus can be ignored). Even though generating a body is not prohibited. + + For status 301, 302, 303, 307 and 308 (the latter from RFC 7238), the specs mention that the body usually + (which means it's optional and not mandated) contain just an hyperlink to the value of + the Location response header, so the body can be ignored safely. + + For status 300, which is "Multiple Choices", the spec mentions both generating a Location + response header AND a response body with the other possible location to follow. + Since the spec explicitily chooses not to specify a format for such body and leave it to + servers and browsers implementors, we ignore the body as there is no specified way to eventually parse it. + */ + } else { + return this.handler.onData(chunk) + } + } + + onComplete (trailers) { + if (this.location) { + /* + https://tools.ietf.org/html/rfc7231#section-6.4 + + TLDR: undici always ignores 3xx response trailers as they are not expected in case of redirections + and neither are useful if present. + + See comment on onData method above for more detailed informations. + */ + + this.location = null + this.abort = null + + this.dispatch(this.opts, this) + } else { + this.handler.onComplete(trailers) + } + } + + onBodySent (chunk) { + if (this.handler.onBodySent) { + this.handler.onBodySent(chunk) + } + } +} + +function parseLocation (statusCode, headers) { + if (redirectableStatusCodes.indexOf(statusCode) === -1) { + return null + } + + for (let i = 0; i < headers.length; i += 2) { + if (headers[i].toString().toLowerCase() === 'location') { + return headers[i + 1] + } + } +} + +// https://tools.ietf.org/html/rfc7231#section-6.4.4 +function shouldRemoveHeader (header, removeContent, unknownOrigin) { + if (header.length === 4) { + return util.headerNameToString(header) === 'host' + } + if (removeContent && util.headerNameToString(header).startsWith('content-')) { + return true + } + if (unknownOrigin && (header.length === 13 || header.length === 6 || header.length === 19)) { + const name = util.headerNameToString(header) + return name === 'authorization' || name === 'cookie' || name === 'proxy-authorization' + } + return false +} + +// https://tools.ietf.org/html/rfc7231#section-6.4 +function cleanRequestHeaders (headers, removeContent, unknownOrigin) { + const ret = [] + if (Array.isArray(headers)) { + for (let i = 0; i < headers.length; i += 2) { + if (!shouldRemoveHeader(headers[i], removeContent, unknownOrigin)) { + ret.push(headers[i], headers[i + 1]) + } + } + } else if (headers && typeof headers === 'object') { + for (const key of Object.keys(headers)) { + if (!shouldRemoveHeader(key, removeContent, unknownOrigin)) { + ret.push(key, headers[key]) + } + } + } else { + assert(headers == null, 'headers must be an object or an array') + } + return ret +} + +module.exports = RedirectHandler + + +/***/ }), + +/***/ 2286: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const assert = __nccwpck_require__(9491) + +const { kRetryHandlerDefaultRetry } = __nccwpck_require__(2785) +const { RequestRetryError } = __nccwpck_require__(8045) +const { isDisturbed, parseHeaders, parseRangeHeader } = __nccwpck_require__(3983) + +function calculateRetryAfterHeader (retryAfter) { + const current = Date.now() + const diff = new Date(retryAfter).getTime() - current + + return diff +} + +class RetryHandler { + constructor (opts, handlers) { + const { retryOptions, ...dispatchOpts } = opts + const { + // Retry scoped + retry: retryFn, + maxRetries, + maxTimeout, + minTimeout, + timeoutFactor, + // Response scoped + methods, + errorCodes, + retryAfter, + statusCodes + } = retryOptions ?? {} + + this.dispatch = handlers.dispatch + this.handler = handlers.handler + this.opts = dispatchOpts + this.abort = null + this.aborted = false + this.retryOpts = { + retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry], + retryAfter: retryAfter ?? true, + maxTimeout: maxTimeout ?? 30 * 1000, // 30s, + timeout: minTimeout ?? 500, // .5s + timeoutFactor: timeoutFactor ?? 2, + maxRetries: maxRetries ?? 5, + // What errors we should retry + methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'], + // Indicates which errors to retry + statusCodes: statusCodes ?? [500, 502, 503, 504, 429], + // List of errors to retry + errorCodes: errorCodes ?? [ + 'ECONNRESET', + 'ECONNREFUSED', + 'ENOTFOUND', + 'ENETDOWN', + 'ENETUNREACH', + 'EHOSTDOWN', + 'EHOSTUNREACH', + 'EPIPE' + ] + } + + this.retryCount = 0 + this.start = 0 + this.end = null + this.etag = null + this.resume = null + + // Handle possible onConnect duplication + this.handler.onConnect(reason => { + this.aborted = true + if (this.abort) { + this.abort(reason) + } else { + this.reason = reason + } + }) + } + + onRequestSent () { + if (this.handler.onRequestSent) { + this.handler.onRequestSent() + } + } + + onUpgrade (statusCode, headers, socket) { + if (this.handler.onUpgrade) { + this.handler.onUpgrade(statusCode, headers, socket) + } + } + + onConnect (abort) { + if (this.aborted) { + abort(this.reason) + } else { + this.abort = abort + } + } + + onBodySent (chunk) { + if (this.handler.onBodySent) return this.handler.onBodySent(chunk) + } + + static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) { + const { statusCode, code, headers } = err + const { method, retryOptions } = opts + const { + maxRetries, + timeout, + maxTimeout, + timeoutFactor, + statusCodes, + errorCodes, + methods + } = retryOptions + let { counter, currentTimeout } = state + + currentTimeout = + currentTimeout != null && currentTimeout > 0 ? currentTimeout : timeout + + // Any code that is not a Undici's originated and allowed to retry + if ( + code && + code !== 'UND_ERR_REQ_RETRY' && + code !== 'UND_ERR_SOCKET' && + !errorCodes.includes(code) + ) { + cb(err) + return + } + + // If a set of method are provided and the current method is not in the list + if (Array.isArray(methods) && !methods.includes(method)) { + cb(err) + return + } + + // If a set of status code are provided and the current status code is not in the list + if ( + statusCode != null && + Array.isArray(statusCodes) && + !statusCodes.includes(statusCode) + ) { + cb(err) + return + } + + // If we reached the max number of retries + if (counter > maxRetries) { + cb(err) + return + } + + let retryAfterHeader = headers != null && headers['retry-after'] + if (retryAfterHeader) { + retryAfterHeader = Number(retryAfterHeader) + retryAfterHeader = isNaN(retryAfterHeader) + ? calculateRetryAfterHeader(retryAfterHeader) + : retryAfterHeader * 1e3 // Retry-After is in seconds + } + + const retryTimeout = + retryAfterHeader > 0 + ? Math.min(retryAfterHeader, maxTimeout) + : Math.min(currentTimeout * timeoutFactor ** counter, maxTimeout) + + state.currentTimeout = retryTimeout + + setTimeout(() => cb(null), retryTimeout) + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + const headers = parseHeaders(rawHeaders) + + this.retryCount += 1 + + if (statusCode >= 300) { + this.abort( + new RequestRetryError('Request failed', statusCode, { + headers, + count: this.retryCount + }) + ) + return false + } + + // Checkpoint for resume from where we left it + if (this.resume != null) { + this.resume = null + + if (statusCode !== 206) { + return true + } + + const contentRange = parseRangeHeader(headers['content-range']) + // If no content range + if (!contentRange) { + this.abort( + new RequestRetryError('Content-Range mismatch', statusCode, { + headers, + count: this.retryCount + }) + ) + return false + } + + // Let's start with a weak etag check + if (this.etag != null && this.etag !== headers.etag) { + this.abort( + new RequestRetryError('ETag mismatch', statusCode, { + headers, + count: this.retryCount + }) + ) + return false + } + + const { start, size, end = size } = contentRange + + assert(this.start === start, 'content-range mismatch') + assert(this.end == null || this.end === end, 'content-range mismatch') + + this.resume = resume + return true + } + + if (this.end == null) { + if (statusCode === 206) { + // First time we receive 206 + const range = parseRangeHeader(headers['content-range']) + + if (range == null) { + return this.handler.onHeaders( + statusCode, + rawHeaders, + resume, + statusMessage + ) + } + + const { start, size, end = size } = range + + assert( + start != null && Number.isFinite(start) && this.start !== start, + 'content-range mismatch' + ) + assert(Number.isFinite(start)) + assert( + end != null && Number.isFinite(end) && this.end !== end, + 'invalid content-length' + ) + + this.start = start + this.end = end + } + + // We make our best to checkpoint the body for further range headers + if (this.end == null) { + const contentLength = headers['content-length'] + this.end = contentLength != null ? Number(contentLength) : null + } + + assert(Number.isFinite(this.start)) + assert( + this.end == null || Number.isFinite(this.end), + 'invalid content-length' + ) + + this.resume = resume + this.etag = headers.etag != null ? headers.etag : null + + return this.handler.onHeaders( + statusCode, + rawHeaders, + resume, + statusMessage + ) + } + + const err = new RequestRetryError('Request failed', statusCode, { + headers, + count: this.retryCount + }) + + this.abort(err) + + return false + } + + onData (chunk) { + this.start += chunk.length + + return this.handler.onData(chunk) + } + + onComplete (rawTrailers) { + this.retryCount = 0 + return this.handler.onComplete(rawTrailers) + } + + onError (err) { + if (this.aborted || isDisturbed(this.opts.body)) { + return this.handler.onError(err) + } + + this.retryOpts.retry( + err, + { + state: { counter: this.retryCount++, currentTimeout: this.retryAfter }, + opts: { retryOptions: this.retryOpts, ...this.opts } + }, + onRetry.bind(this) + ) + + function onRetry (err) { + if (err != null || this.aborted || isDisturbed(this.opts.body)) { + return this.handler.onError(err) + } + + if (this.start !== 0) { + this.opts = { + ...this.opts, + headers: { + ...this.opts.headers, + range: `bytes=${this.start}-${this.end ?? ''}` + } + } + } + + try { + this.dispatch(this.opts, this) + } catch (err) { + this.handler.onError(err) + } + } + } +} + +module.exports = RetryHandler + + +/***/ }), + +/***/ 8861: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const RedirectHandler = __nccwpck_require__(2860) + +function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections }) { + return (dispatch) => { + return function Intercept (opts, handler) { + const { maxRedirections = defaultMaxRedirections } = opts + + if (!maxRedirections) { + return dispatch(opts, handler) + } + + const redirectHandler = new RedirectHandler(dispatch, maxRedirections, opts, handler) + opts = { ...opts, maxRedirections: 0 } // Stop sub dispatcher from also redirecting. + return dispatch(opts, redirectHandler) + } + } +} + +module.exports = createRedirectInterceptor + + +/***/ }), + +/***/ 953: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SPECIAL_HEADERS = exports.HEADER_STATE = exports.MINOR = exports.MAJOR = exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS = exports.TOKEN = exports.STRICT_TOKEN = exports.HEX = exports.URL_CHAR = exports.STRICT_URL_CHAR = exports.USERINFO_CHARS = exports.MARK = exports.ALPHANUM = exports.NUM = exports.HEX_MAP = exports.NUM_MAP = exports.ALPHA = exports.FINISH = exports.H_METHOD_MAP = exports.METHOD_MAP = exports.METHODS_RTSP = exports.METHODS_ICE = exports.METHODS_HTTP = exports.METHODS = exports.LENIENT_FLAGS = exports.FLAGS = exports.TYPE = exports.ERROR = void 0; +const utils_1 = __nccwpck_require__(1891); +// C headers +var ERROR; +(function (ERROR) { + ERROR[ERROR["OK"] = 0] = "OK"; + ERROR[ERROR["INTERNAL"] = 1] = "INTERNAL"; + ERROR[ERROR["STRICT"] = 2] = "STRICT"; + ERROR[ERROR["LF_EXPECTED"] = 3] = "LF_EXPECTED"; + ERROR[ERROR["UNEXPECTED_CONTENT_LENGTH"] = 4] = "UNEXPECTED_CONTENT_LENGTH"; + ERROR[ERROR["CLOSED_CONNECTION"] = 5] = "CLOSED_CONNECTION"; + ERROR[ERROR["INVALID_METHOD"] = 6] = "INVALID_METHOD"; + ERROR[ERROR["INVALID_URL"] = 7] = "INVALID_URL"; + ERROR[ERROR["INVALID_CONSTANT"] = 8] = "INVALID_CONSTANT"; + ERROR[ERROR["INVALID_VERSION"] = 9] = "INVALID_VERSION"; + ERROR[ERROR["INVALID_HEADER_TOKEN"] = 10] = "INVALID_HEADER_TOKEN"; + ERROR[ERROR["INVALID_CONTENT_LENGTH"] = 11] = "INVALID_CONTENT_LENGTH"; + ERROR[ERROR["INVALID_CHUNK_SIZE"] = 12] = "INVALID_CHUNK_SIZE"; + ERROR[ERROR["INVALID_STATUS"] = 13] = "INVALID_STATUS"; + ERROR[ERROR["INVALID_EOF_STATE"] = 14] = "INVALID_EOF_STATE"; + ERROR[ERROR["INVALID_TRANSFER_ENCODING"] = 15] = "INVALID_TRANSFER_ENCODING"; + ERROR[ERROR["CB_MESSAGE_BEGIN"] = 16] = "CB_MESSAGE_BEGIN"; + ERROR[ERROR["CB_HEADERS_COMPLETE"] = 17] = "CB_HEADERS_COMPLETE"; + ERROR[ERROR["CB_MESSAGE_COMPLETE"] = 18] = "CB_MESSAGE_COMPLETE"; + ERROR[ERROR["CB_CHUNK_HEADER"] = 19] = "CB_CHUNK_HEADER"; + ERROR[ERROR["CB_CHUNK_COMPLETE"] = 20] = "CB_CHUNK_COMPLETE"; + ERROR[ERROR["PAUSED"] = 21] = "PAUSED"; + ERROR[ERROR["PAUSED_UPGRADE"] = 22] = "PAUSED_UPGRADE"; + ERROR[ERROR["PAUSED_H2_UPGRADE"] = 23] = "PAUSED_H2_UPGRADE"; + ERROR[ERROR["USER"] = 24] = "USER"; +})(ERROR = exports.ERROR || (exports.ERROR = {})); +var TYPE; +(function (TYPE) { + TYPE[TYPE["BOTH"] = 0] = "BOTH"; + TYPE[TYPE["REQUEST"] = 1] = "REQUEST"; + TYPE[TYPE["RESPONSE"] = 2] = "RESPONSE"; +})(TYPE = exports.TYPE || (exports.TYPE = {})); +var FLAGS; +(function (FLAGS) { + FLAGS[FLAGS["CONNECTION_KEEP_ALIVE"] = 1] = "CONNECTION_KEEP_ALIVE"; + FLAGS[FLAGS["CONNECTION_CLOSE"] = 2] = "CONNECTION_CLOSE"; + FLAGS[FLAGS["CONNECTION_UPGRADE"] = 4] = "CONNECTION_UPGRADE"; + FLAGS[FLAGS["CHUNKED"] = 8] = "CHUNKED"; + FLAGS[FLAGS["UPGRADE"] = 16] = "UPGRADE"; + FLAGS[FLAGS["CONTENT_LENGTH"] = 32] = "CONTENT_LENGTH"; + FLAGS[FLAGS["SKIPBODY"] = 64] = "SKIPBODY"; + FLAGS[FLAGS["TRAILING"] = 128] = "TRAILING"; + // 1 << 8 is unused + FLAGS[FLAGS["TRANSFER_ENCODING"] = 512] = "TRANSFER_ENCODING"; +})(FLAGS = exports.FLAGS || (exports.FLAGS = {})); +var LENIENT_FLAGS; +(function (LENIENT_FLAGS) { + LENIENT_FLAGS[LENIENT_FLAGS["HEADERS"] = 1] = "HEADERS"; + LENIENT_FLAGS[LENIENT_FLAGS["CHUNKED_LENGTH"] = 2] = "CHUNKED_LENGTH"; + LENIENT_FLAGS[LENIENT_FLAGS["KEEP_ALIVE"] = 4] = "KEEP_ALIVE"; +})(LENIENT_FLAGS = exports.LENIENT_FLAGS || (exports.LENIENT_FLAGS = {})); +var METHODS; +(function (METHODS) { + METHODS[METHODS["DELETE"] = 0] = "DELETE"; + METHODS[METHODS["GET"] = 1] = "GET"; + METHODS[METHODS["HEAD"] = 2] = "HEAD"; + METHODS[METHODS["POST"] = 3] = "POST"; + METHODS[METHODS["PUT"] = 4] = "PUT"; + /* pathological */ + METHODS[METHODS["CONNECT"] = 5] = "CONNECT"; + METHODS[METHODS["OPTIONS"] = 6] = "OPTIONS"; + METHODS[METHODS["TRACE"] = 7] = "TRACE"; + /* WebDAV */ + METHODS[METHODS["COPY"] = 8] = "COPY"; + METHODS[METHODS["LOCK"] = 9] = "LOCK"; + METHODS[METHODS["MKCOL"] = 10] = "MKCOL"; + METHODS[METHODS["MOVE"] = 11] = "MOVE"; + METHODS[METHODS["PROPFIND"] = 12] = "PROPFIND"; + METHODS[METHODS["PROPPATCH"] = 13] = "PROPPATCH"; + METHODS[METHODS["SEARCH"] = 14] = "SEARCH"; + METHODS[METHODS["UNLOCK"] = 15] = "UNLOCK"; + METHODS[METHODS["BIND"] = 16] = "BIND"; + METHODS[METHODS["REBIND"] = 17] = "REBIND"; + METHODS[METHODS["UNBIND"] = 18] = "UNBIND"; + METHODS[METHODS["ACL"] = 19] = "ACL"; + /* subversion */ + METHODS[METHODS["REPORT"] = 20] = "REPORT"; + METHODS[METHODS["MKACTIVITY"] = 21] = "MKACTIVITY"; + METHODS[METHODS["CHECKOUT"] = 22] = "CHECKOUT"; + METHODS[METHODS["MERGE"] = 23] = "MERGE"; + /* upnp */ + METHODS[METHODS["M-SEARCH"] = 24] = "M-SEARCH"; + METHODS[METHODS["NOTIFY"] = 25] = "NOTIFY"; + METHODS[METHODS["SUBSCRIBE"] = 26] = "SUBSCRIBE"; + METHODS[METHODS["UNSUBSCRIBE"] = 27] = "UNSUBSCRIBE"; + /* RFC-5789 */ + METHODS[METHODS["PATCH"] = 28] = "PATCH"; + METHODS[METHODS["PURGE"] = 29] = "PURGE"; + /* CalDAV */ + METHODS[METHODS["MKCALENDAR"] = 30] = "MKCALENDAR"; + /* RFC-2068, section 19.6.1.2 */ + METHODS[METHODS["LINK"] = 31] = "LINK"; + METHODS[METHODS["UNLINK"] = 32] = "UNLINK"; + /* icecast */ + METHODS[METHODS["SOURCE"] = 33] = "SOURCE"; + /* RFC-7540, section 11.6 */ + METHODS[METHODS["PRI"] = 34] = "PRI"; + /* RFC-2326 RTSP */ + METHODS[METHODS["DESCRIBE"] = 35] = "DESCRIBE"; + METHODS[METHODS["ANNOUNCE"] = 36] = "ANNOUNCE"; + METHODS[METHODS["SETUP"] = 37] = "SETUP"; + METHODS[METHODS["PLAY"] = 38] = "PLAY"; + METHODS[METHODS["PAUSE"] = 39] = "PAUSE"; + METHODS[METHODS["TEARDOWN"] = 40] = "TEARDOWN"; + METHODS[METHODS["GET_PARAMETER"] = 41] = "GET_PARAMETER"; + METHODS[METHODS["SET_PARAMETER"] = 42] = "SET_PARAMETER"; + METHODS[METHODS["REDIRECT"] = 43] = "REDIRECT"; + METHODS[METHODS["RECORD"] = 44] = "RECORD"; + /* RAOP */ + METHODS[METHODS["FLUSH"] = 45] = "FLUSH"; +})(METHODS = exports.METHODS || (exports.METHODS = {})); +exports.METHODS_HTTP = [ + METHODS.DELETE, + METHODS.GET, + METHODS.HEAD, + METHODS.POST, + METHODS.PUT, + METHODS.CONNECT, + METHODS.OPTIONS, + METHODS.TRACE, + METHODS.COPY, + METHODS.LOCK, + METHODS.MKCOL, + METHODS.MOVE, + METHODS.PROPFIND, + METHODS.PROPPATCH, + METHODS.SEARCH, + METHODS.UNLOCK, + METHODS.BIND, + METHODS.REBIND, + METHODS.UNBIND, + METHODS.ACL, + METHODS.REPORT, + METHODS.MKACTIVITY, + METHODS.CHECKOUT, + METHODS.MERGE, + METHODS['M-SEARCH'], + METHODS.NOTIFY, + METHODS.SUBSCRIBE, + METHODS.UNSUBSCRIBE, + METHODS.PATCH, + METHODS.PURGE, + METHODS.MKCALENDAR, + METHODS.LINK, + METHODS.UNLINK, + METHODS.PRI, + // TODO(indutny): should we allow it with HTTP? + METHODS.SOURCE, +]; +exports.METHODS_ICE = [ + METHODS.SOURCE, +]; +exports.METHODS_RTSP = [ + METHODS.OPTIONS, + METHODS.DESCRIBE, + METHODS.ANNOUNCE, + METHODS.SETUP, + METHODS.PLAY, + METHODS.PAUSE, + METHODS.TEARDOWN, + METHODS.GET_PARAMETER, + METHODS.SET_PARAMETER, + METHODS.REDIRECT, + METHODS.RECORD, + METHODS.FLUSH, + // For AirPlay + METHODS.GET, + METHODS.POST, +]; +exports.METHOD_MAP = utils_1.enumToMap(METHODS); +exports.H_METHOD_MAP = {}; +Object.keys(exports.METHOD_MAP).forEach((key) => { + if (/^H/.test(key)) { + exports.H_METHOD_MAP[key] = exports.METHOD_MAP[key]; + } +}); +var FINISH; +(function (FINISH) { + FINISH[FINISH["SAFE"] = 0] = "SAFE"; + FINISH[FINISH["SAFE_WITH_CB"] = 1] = "SAFE_WITH_CB"; + FINISH[FINISH["UNSAFE"] = 2] = "UNSAFE"; +})(FINISH = exports.FINISH || (exports.FINISH = {})); +exports.ALPHA = []; +for (let i = 'A'.charCodeAt(0); i <= 'Z'.charCodeAt(0); i++) { + // Upper case + exports.ALPHA.push(String.fromCharCode(i)); + // Lower case + exports.ALPHA.push(String.fromCharCode(i + 0x20)); +} +exports.NUM_MAP = { + 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, + 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, +}; +exports.HEX_MAP = { + 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, + 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, + A: 0XA, B: 0XB, C: 0XC, D: 0XD, E: 0XE, F: 0XF, + a: 0xa, b: 0xb, c: 0xc, d: 0xd, e: 0xe, f: 0xf, +}; +exports.NUM = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', +]; +exports.ALPHANUM = exports.ALPHA.concat(exports.NUM); +exports.MARK = ['-', '_', '.', '!', '~', '*', '\'', '(', ')']; +exports.USERINFO_CHARS = exports.ALPHANUM + .concat(exports.MARK) + .concat(['%', ';', ':', '&', '=', '+', '$', ',']); +// TODO(indutny): use RFC +exports.STRICT_URL_CHAR = [ + '!', '"', '$', '%', '&', '\'', + '(', ')', '*', '+', ',', '-', '.', '/', + ':', ';', '<', '=', '>', + '@', '[', '\\', ']', '^', '_', + '`', + '{', '|', '}', '~', +].concat(exports.ALPHANUM); +exports.URL_CHAR = exports.STRICT_URL_CHAR + .concat(['\t', '\f']); +// All characters with 0x80 bit set to 1 +for (let i = 0x80; i <= 0xff; i++) { + exports.URL_CHAR.push(i); +} +exports.HEX = exports.NUM.concat(['a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F']); +/* Tokens as defined by rfc 2616. Also lowercases them. + * token = 1* + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT + */ +exports.STRICT_TOKEN = [ + '!', '#', '$', '%', '&', '\'', + '*', '+', '-', '.', + '^', '_', '`', + '|', '~', +].concat(exports.ALPHANUM); +exports.TOKEN = exports.STRICT_TOKEN.concat([' ']); +/* + * Verify that a char is a valid visible (printable) US-ASCII + * character or %x80-FF + */ +exports.HEADER_CHARS = ['\t']; +for (let i = 32; i <= 255; i++) { + if (i !== 127) { + exports.HEADER_CHARS.push(i); + } +} +// ',' = \x44 +exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS.filter((c) => c !== 44); +exports.MAJOR = exports.NUM_MAP; +exports.MINOR = exports.MAJOR; +var HEADER_STATE; +(function (HEADER_STATE) { + HEADER_STATE[HEADER_STATE["GENERAL"] = 0] = "GENERAL"; + HEADER_STATE[HEADER_STATE["CONNECTION"] = 1] = "CONNECTION"; + HEADER_STATE[HEADER_STATE["CONTENT_LENGTH"] = 2] = "CONTENT_LENGTH"; + HEADER_STATE[HEADER_STATE["TRANSFER_ENCODING"] = 3] = "TRANSFER_ENCODING"; + HEADER_STATE[HEADER_STATE["UPGRADE"] = 4] = "UPGRADE"; + HEADER_STATE[HEADER_STATE["CONNECTION_KEEP_ALIVE"] = 5] = "CONNECTION_KEEP_ALIVE"; + HEADER_STATE[HEADER_STATE["CONNECTION_CLOSE"] = 6] = "CONNECTION_CLOSE"; + HEADER_STATE[HEADER_STATE["CONNECTION_UPGRADE"] = 7] = "CONNECTION_UPGRADE"; + HEADER_STATE[HEADER_STATE["TRANSFER_ENCODING_CHUNKED"] = 8] = "TRANSFER_ENCODING_CHUNKED"; +})(HEADER_STATE = exports.HEADER_STATE || (exports.HEADER_STATE = {})); +exports.SPECIAL_HEADERS = { + 'connection': HEADER_STATE.CONNECTION, + 'content-length': HEADER_STATE.CONTENT_LENGTH, + 'proxy-connection': HEADER_STATE.CONNECTION, + 'transfer-encoding': HEADER_STATE.TRANSFER_ENCODING, + 'upgrade': HEADER_STATE.UPGRADE, +}; +//# sourceMappingURL=constants.js.map + +/***/ }), + +/***/ 1145: +/***/ ((module) => { + +module.exports = 'AGFzbQEAAAABMAhgAX8Bf2ADf39/AX9gBH9/f38Bf2AAAGADf39/AGABfwBgAn9/AGAGf39/f39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQACA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAA0ZFAwMEAAAFAAAAAAAABQEFAAUFBQAABgAAAAAGBgYGAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAABAQcAAAUFAwABBAUBcAESEgUDAQACBggBfwFBgNQECwfRBSIGbWVtb3J5AgALX2luaXRpYWxpemUACRlfX2luZGlyZWN0X2Z1bmN0aW9uX3RhYmxlAQALbGxodHRwX2luaXQAChhsbGh0dHBfc2hvdWxkX2tlZXBfYWxpdmUAQQxsbGh0dHBfYWxsb2MADAZtYWxsb2MARgtsbGh0dHBfZnJlZQANBGZyZWUASA9sbGh0dHBfZ2V0X3R5cGUADhVsbGh0dHBfZ2V0X2h0dHBfbWFqb3IADxVsbGh0dHBfZ2V0X2h0dHBfbWlub3IAEBFsbGh0dHBfZ2V0X21ldGhvZAARFmxsaHR0cF9nZXRfc3RhdHVzX2NvZGUAEhJsbGh0dHBfZ2V0X3VwZ3JhZGUAEwxsbGh0dHBfcmVzZXQAFA5sbGh0dHBfZXhlY3V0ZQAVFGxsaHR0cF9zZXR0aW5nc19pbml0ABYNbGxodHRwX2ZpbmlzaAAXDGxsaHR0cF9wYXVzZQAYDWxsaHR0cF9yZXN1bWUAGRtsbGh0dHBfcmVzdW1lX2FmdGVyX3VwZ3JhZGUAGhBsbGh0dHBfZ2V0X2Vycm5vABsXbGxodHRwX2dldF9lcnJvcl9yZWFzb24AHBdsbGh0dHBfc2V0X2Vycm9yX3JlYXNvbgAdFGxsaHR0cF9nZXRfZXJyb3JfcG9zAB4RbGxodHRwX2Vycm5vX25hbWUAHxJsbGh0dHBfbWV0aG9kX25hbWUAIBJsbGh0dHBfc3RhdHVzX25hbWUAIRpsbGh0dHBfc2V0X2xlbmllbnRfaGVhZGVycwAiIWxsaHR0cF9zZXRfbGVuaWVudF9jaHVua2VkX2xlbmd0aAAjHWxsaHR0cF9zZXRfbGVuaWVudF9rZWVwX2FsaXZlACQkbGxodHRwX3NldF9sZW5pZW50X3RyYW5zZmVyX2VuY29kaW5nACUYbGxodHRwX21lc3NhZ2VfbmVlZHNfZW9mAD8JFwEAQQELEQECAwQFCwYHNTk3MS8tJyspCsLgAkUCAAsIABCIgICAAAsZACAAEMKAgIAAGiAAIAI2AjggACABOgAoCxwAIAAgAC8BMiAALQAuIAAQwYCAgAAQgICAgAALKgEBf0HAABDGgICAACIBEMKAgIAAGiABQYCIgIAANgI4IAEgADoAKCABCwoAIAAQyICAgAALBwAgAC0AKAsHACAALQAqCwcAIAAtACsLBwAgAC0AKQsHACAALwEyCwcAIAAtAC4LRQEEfyAAKAIYIQEgAC0ALSECIAAtACghAyAAKAI4IQQgABDCgICAABogACAENgI4IAAgAzoAKCAAIAI6AC0gACABNgIYCxEAIAAgASABIAJqEMOAgIAACxAAIABBAEHcABDMgICAABoLZwEBf0EAIQECQCAAKAIMDQACQAJAAkACQCAALQAvDgMBAAMCCyAAKAI4IgFFDQAgASgCLCIBRQ0AIAAgARGAgICAAAAiAQ0DC0EADwsQyoCAgAAACyAAQcOWgIAANgIQQQ4hAQsgAQseAAJAIAAoAgwNACAAQdGbgIAANgIQIABBFTYCDAsLFgACQCAAKAIMQRVHDQAgAEEANgIMCwsWAAJAIAAoAgxBFkcNACAAQQA2AgwLCwcAIAAoAgwLBwAgACgCEAsJACAAIAE2AhALBwAgACgCFAsiAAJAIABBJEkNABDKgICAAAALIABBAnRBoLOAgABqKAIACyIAAkAgAEEuSQ0AEMqAgIAAAAsgAEECdEGwtICAAGooAgAL7gsBAX9B66iAgAAhAQJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIABBnH9qDvQDY2IAAWFhYWFhYQIDBAVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhBgcICQoLDA0OD2FhYWFhEGFhYWFhYWFhYWFhEWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYRITFBUWFxgZGhthYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2YTc4OTphYWFhYWFhYTthYWE8YWFhYT0+P2FhYWFhYWFhQGFhQWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYUJDREVGR0hJSktMTU5PUFFSU2FhYWFhYWFhVFVWV1hZWlthXF1hYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFeYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhX2BhC0Hhp4CAAA8LQaShgIAADwtBy6yAgAAPC0H+sYCAAA8LQcCkgIAADwtBq6SAgAAPC0GNqICAAA8LQeKmgIAADwtBgLCAgAAPC0G5r4CAAA8LQdekgIAADwtB75+AgAAPC0Hhn4CAAA8LQfqfgIAADwtB8qCAgAAPC0Gor4CAAA8LQa6ygIAADwtBiLCAgAAPC0Hsp4CAAA8LQYKigIAADwtBjp2AgAAPC0HQroCAAA8LQcqjgIAADwtBxbKAgAAPC0HfnICAAA8LQdKcgIAADwtBxKCAgAAPC0HXoICAAA8LQaKfgIAADwtB7a6AgAAPC0GrsICAAA8LQdSlgIAADwtBzK6AgAAPC0H6roCAAA8LQfyrgIAADwtB0rCAgAAPC0HxnYCAAA8LQbuggIAADwtB96uAgAAPC0GQsYCAAA8LQdexgIAADwtBoq2AgAAPC0HUp4CAAA8LQeCrgIAADwtBn6yAgAAPC0HrsYCAAA8LQdWfgIAADwtByrGAgAAPC0HepYCAAA8LQdSegIAADwtB9JyAgAAPC0GnsoCAAA8LQbGdgIAADwtBoJ2AgAAPC0G5sYCAAA8LQbywgIAADwtBkqGAgAAPC0GzpoCAAA8LQemsgIAADwtBrJ6AgAAPC0HUq4CAAA8LQfemgIAADwtBgKaAgAAPC0GwoYCAAA8LQf6egIAADwtBjaOAgAAPC0GJrYCAAA8LQfeigIAADwtBoLGAgAAPC0Gun4CAAA8LQcalgIAADwtB6J6AgAAPC0GTooCAAA8LQcKvgIAADwtBw52AgAAPC0GLrICAAA8LQeGdgIAADwtBja+AgAAPC0HqoYCAAA8LQbStgIAADwtB0q+AgAAPC0HfsoCAAA8LQdKygIAADwtB8LCAgAAPC0GpooCAAA8LQfmjgIAADwtBmZ6AgAAPC0G1rICAAA8LQZuwgIAADwtBkrKAgAAPC0G2q4CAAA8LQcKigIAADwtB+LKAgAAPC0GepYCAAA8LQdCigIAADwtBup6AgAAPC0GBnoCAAA8LEMqAgIAAAAtB1qGAgAAhAQsgAQsWACAAIAAtAC1B/gFxIAFBAEdyOgAtCxkAIAAgAC0ALUH9AXEgAUEAR0EBdHI6AC0LGQAgACAALQAtQfsBcSABQQBHQQJ0cjoALQsZACAAIAAtAC1B9wFxIAFBAEdBA3RyOgAtCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAgAiBEUNACAAIAQRgICAgAAAIQMLIAMLSQECf0EAIQMCQCAAKAI4IgRFDQAgBCgCBCIERQ0AIAAgASACIAFrIAQRgYCAgAAAIgNBf0cNACAAQcaRgIAANgIQQRghAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIwIgRFDQAgACAEEYCAgIAAACEDCyADC0kBAn9BACEDAkAgACgCOCIERQ0AIAQoAggiBEUNACAAIAEgAiABayAEEYGAgIAAACIDQX9HDQAgAEH2ioCAADYCEEEYIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCNCIERQ0AIAAgBBGAgICAAAAhAwsgAwtJAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIMIgRFDQAgACABIAIgAWsgBBGBgICAAAAiA0F/Rw0AIABB7ZqAgAA2AhBBGCEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAjgiBEUNACAAIAQRgICAgAAAIQMLIAMLSQECf0EAIQMCQCAAKAI4IgRFDQAgBCgCECIERQ0AIAAgASACIAFrIAQRgYCAgAAAIgNBf0cNACAAQZWQgIAANgIQQRghAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAI8IgRFDQAgACAEEYCAgIAAACEDCyADC0kBAn9BACEDAkAgACgCOCIERQ0AIAQoAhQiBEUNACAAIAEgAiABayAEEYGAgIAAACIDQX9HDQAgAEGqm4CAADYCEEEYIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCQCIERQ0AIAAgBBGAgICAAAAhAwsgAwtJAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIYIgRFDQAgACABIAIgAWsgBBGBgICAAAAiA0F/Rw0AIABB7ZOAgAA2AhBBGCEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAkQiBEUNACAAIAQRgICAgAAAIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCJCIERQ0AIAAgBBGAgICAAAAhAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIsIgRFDQAgACAEEYCAgIAAACEDCyADC0kBAn9BACEDAkAgACgCOCIERQ0AIAQoAigiBEUNACAAIAEgAiABayAEEYGAgIAAACIDQX9HDQAgAEH2iICAADYCEEEYIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCUCIERQ0AIAAgBBGAgICAAAAhAwsgAwtJAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIcIgRFDQAgACABIAIgAWsgBBGBgICAAAAiA0F/Rw0AIABBwpmAgAA2AhBBGCEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAkgiBEUNACAAIAQRgICAgAAAIQMLIAMLSQECf0EAIQMCQCAAKAI4IgRFDQAgBCgCICIERQ0AIAAgASACIAFrIAQRgYCAgAAAIgNBf0cNACAAQZSUgIAANgIQQRghAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAJMIgRFDQAgACAEEYCAgIAAACEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAlQiBEUNACAAIAQRgICAgAAAIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCWCIERQ0AIAAgBBGAgICAAAAhAwsgAwtFAQF/AkACQCAALwEwQRRxQRRHDQBBASEDIAAtAChBAUYNASAALwEyQeUARiEDDAELIAAtAClBBUYhAwsgACADOgAuQQAL/gEBA39BASEDAkAgAC8BMCIEQQhxDQAgACkDIEIAUiEDCwJAAkAgAC0ALkUNAEEBIQUgAC0AKUEFRg0BQQEhBSAEQcAAcUUgA3FBAUcNAQtBACEFIARBwABxDQBBAiEFIARB//8DcSIDQQhxDQACQCADQYAEcUUNAAJAIAAtAChBAUcNACAALQAtQQpxDQBBBQ8LQQQPCwJAIANBIHENAAJAIAAtAChBAUYNACAALwEyQf//A3EiAEGcf2pB5ABJDQAgAEHMAUYNACAAQbACRg0AQQQhBSAEQShxRQ0CIANBiARxQYAERg0CC0EADwtBAEEDIAApAyBQGyEFCyAFC2IBAn9BACEBAkAgAC0AKEEBRg0AIAAvATJB//8DcSICQZx/akHkAEkNACACQcwBRg0AIAJBsAJGDQAgAC8BMCIAQcAAcQ0AQQEhASAAQYgEcUGABEYNACAAQShxRSEBCyABC6cBAQN/AkACQAJAIAAtACpFDQAgAC0AK0UNAEEAIQMgAC8BMCIEQQJxRQ0BDAILQQAhAyAALwEwIgRBAXFFDQELQQEhAyAALQAoQQFGDQAgAC8BMkH//wNxIgVBnH9qQeQASQ0AIAVBzAFGDQAgBUGwAkYNACAEQcAAcQ0AQQAhAyAEQYgEcUGABEYNACAEQShxQQBHIQMLIABBADsBMCAAQQA6AC8gAwuZAQECfwJAAkACQCAALQAqRQ0AIAAtACtFDQBBACEBIAAvATAiAkECcUUNAQwCC0EAIQEgAC8BMCICQQFxRQ0BC0EBIQEgAC0AKEEBRg0AIAAvATJB//8DcSIAQZx/akHkAEkNACAAQcwBRg0AIABBsAJGDQAgAkHAAHENAEEAIQEgAkGIBHFBgARGDQAgAkEocUEARyEBCyABC1kAIABBGGpCADcDACAAQgA3AwAgAEE4akIANwMAIABBMGpCADcDACAAQShqQgA3AwAgAEEgakIANwMAIABBEGpCADcDACAAQQhqQgA3AwAgAEHdATYCHEEAC3sBAX8CQCAAKAIMIgMNAAJAIAAoAgRFDQAgACABNgIECwJAIAAgASACEMSAgIAAIgMNACAAKAIMDwsgACADNgIcQQAhAyAAKAIEIgFFDQAgACABIAIgACgCCBGBgICAAAAiAUUNACAAIAI2AhQgACABNgIMIAEhAwsgAwvk8wEDDn8DfgR/I4CAgIAAQRBrIgMkgICAgAAgASEEIAEhBSABIQYgASEHIAEhCCABIQkgASEKIAEhCyABIQwgASENIAEhDiABIQ8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgACgCHCIQQX9qDt0B2gEB2QECAwQFBgcICQoLDA0O2AEPENcBERLWARMUFRYXGBkaG+AB3wEcHR7VAR8gISIjJCXUASYnKCkqKyzTAdIBLS7RAdABLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVG2wFHSElKzwHOAUvNAUzMAU1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1+f4ABgQGCAYMBhAGFAYYBhwGIAYkBigGLAYwBjQGOAY8BkAGRAZIBkwGUAZUBlgGXAZgBmQGaAZsBnAGdAZ4BnwGgAaEBogGjAaQBpQGmAacBqAGpAaoBqwGsAa0BrgGvAbABsQGyAbMBtAG1AbYBtwHLAcoBuAHJAbkByAG6AbsBvAG9Ab4BvwHAAcEBwgHDAcQBxQHGAQDcAQtBACEQDMYBC0EOIRAMxQELQQ0hEAzEAQtBDyEQDMMBC0EQIRAMwgELQRMhEAzBAQtBFCEQDMABC0EVIRAMvwELQRYhEAy+AQtBFyEQDL0BC0EYIRAMvAELQRkhEAy7AQtBGiEQDLoBC0EbIRAMuQELQRwhEAy4AQtBCCEQDLcBC0EdIRAMtgELQSAhEAy1AQtBHyEQDLQBC0EHIRAMswELQSEhEAyyAQtBIiEQDLEBC0EeIRAMsAELQSMhEAyvAQtBEiEQDK4BC0ERIRAMrQELQSQhEAysAQtBJSEQDKsBC0EmIRAMqgELQSchEAypAQtBwwEhEAyoAQtBKSEQDKcBC0ErIRAMpgELQSwhEAylAQtBLSEQDKQBC0EuIRAMowELQS8hEAyiAQtBxAEhEAyhAQtBMCEQDKABC0E0IRAMnwELQQwhEAyeAQtBMSEQDJ0BC0EyIRAMnAELQTMhEAybAQtBOSEQDJoBC0E1IRAMmQELQcUBIRAMmAELQQshEAyXAQtBOiEQDJYBC0E2IRAMlQELQQohEAyUAQtBNyEQDJMBC0E4IRAMkgELQTwhEAyRAQtBOyEQDJABC0E9IRAMjwELQQkhEAyOAQtBKCEQDI0BC0E+IRAMjAELQT8hEAyLAQtBwAAhEAyKAQtBwQAhEAyJAQtBwgAhEAyIAQtBwwAhEAyHAQtBxAAhEAyGAQtBxQAhEAyFAQtBxgAhEAyEAQtBKiEQDIMBC0HHACEQDIIBC0HIACEQDIEBC0HJACEQDIABC0HKACEQDH8LQcsAIRAMfgtBzQAhEAx9C0HMACEQDHwLQc4AIRAMewtBzwAhEAx6C0HQACEQDHkLQdEAIRAMeAtB0gAhEAx3C0HTACEQDHYLQdQAIRAMdQtB1gAhEAx0C0HVACEQDHMLQQYhEAxyC0HXACEQDHELQQUhEAxwC0HYACEQDG8LQQQhEAxuC0HZACEQDG0LQdoAIRAMbAtB2wAhEAxrC0HcACEQDGoLQQMhEAxpC0HdACEQDGgLQd4AIRAMZwtB3wAhEAxmC0HhACEQDGULQeAAIRAMZAtB4gAhEAxjC0HjACEQDGILQQIhEAxhC0HkACEQDGALQeUAIRAMXwtB5gAhEAxeC0HnACEQDF0LQegAIRAMXAtB6QAhEAxbC0HqACEQDFoLQesAIRAMWQtB7AAhEAxYC0HtACEQDFcLQe4AIRAMVgtB7wAhEAxVC0HwACEQDFQLQfEAIRAMUwtB8gAhEAxSC0HzACEQDFELQfQAIRAMUAtB9QAhEAxPC0H2ACEQDE4LQfcAIRAMTQtB+AAhEAxMC0H5ACEQDEsLQfoAIRAMSgtB+wAhEAxJC0H8ACEQDEgLQf0AIRAMRwtB/gAhEAxGC0H/ACEQDEULQYABIRAMRAtBgQEhEAxDC0GCASEQDEILQYMBIRAMQQtBhAEhEAxAC0GFASEQDD8LQYYBIRAMPgtBhwEhEAw9C0GIASEQDDwLQYkBIRAMOwtBigEhEAw6C0GLASEQDDkLQYwBIRAMOAtBjQEhEAw3C0GOASEQDDYLQY8BIRAMNQtBkAEhEAw0C0GRASEQDDMLQZIBIRAMMgtBkwEhEAwxC0GUASEQDDALQZUBIRAMLwtBlgEhEAwuC0GXASEQDC0LQZgBIRAMLAtBmQEhEAwrC0GaASEQDCoLQZsBIRAMKQtBnAEhEAwoC0GdASEQDCcLQZ4BIRAMJgtBnwEhEAwlC0GgASEQDCQLQaEBIRAMIwtBogEhEAwiC0GjASEQDCELQaQBIRAMIAtBpQEhEAwfC0GmASEQDB4LQacBIRAMHQtBqAEhEAwcC0GpASEQDBsLQaoBIRAMGgtBqwEhEAwZC0GsASEQDBgLQa0BIRAMFwtBrgEhEAwWC0EBIRAMFQtBrwEhEAwUC0GwASEQDBMLQbEBIRAMEgtBswEhEAwRC0GyASEQDBALQbQBIRAMDwtBtQEhEAwOC0G2ASEQDA0LQbcBIRAMDAtBuAEhEAwLC0G5ASEQDAoLQboBIRAMCQtBuwEhEAwIC0HGASEQDAcLQbwBIRAMBgtBvQEhEAwFC0G+ASEQDAQLQb8BIRAMAwtBwAEhEAwCC0HCASEQDAELQcEBIRALA0ACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAQDscBAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxweHyAhIyUoP0BBREVGR0hJSktMTU9QUVJT3gNXWVtcXWBiZWZnaGlqa2xtb3BxcnN0dXZ3eHl6e3x9foABggGFAYYBhwGJAYsBjAGNAY4BjwGQAZEBlAGVAZYBlwGYAZkBmgGbAZwBnQGeAZ8BoAGhAaIBowGkAaUBpgGnAagBqQGqAasBrAGtAa4BrwGwAbEBsgGzAbQBtQG2AbcBuAG5AboBuwG8Ab0BvgG/AcABwQHCAcMBxAHFAcYBxwHIAckBygHLAcwBzQHOAc8B0AHRAdIB0wHUAdUB1gHXAdgB2QHaAdsB3AHdAd4B4AHhAeIB4wHkAeUB5gHnAegB6QHqAesB7AHtAe4B7wHwAfEB8gHzAZkCpAKwAv4C/gILIAEiBCACRw3zAUHdASEQDP8DCyABIhAgAkcN3QFBwwEhEAz+AwsgASIBIAJHDZABQfcAIRAM/QMLIAEiASACRw2GAUHvACEQDPwDCyABIgEgAkcNf0HqACEQDPsDCyABIgEgAkcNe0HoACEQDPoDCyABIgEgAkcNeEHmACEQDPkDCyABIgEgAkcNGkEYIRAM+AMLIAEiASACRw0UQRIhEAz3AwsgASIBIAJHDVlBxQAhEAz2AwsgASIBIAJHDUpBPyEQDPUDCyABIgEgAkcNSEE8IRAM9AMLIAEiASACRw1BQTEhEAzzAwsgAC0ALkEBRg3rAwyHAgsgACABIgEgAhDAgICAAEEBRw3mASAAQgA3AyAM5wELIAAgASIBIAIQtICAgAAiEA3nASABIQEM9QILAkAgASIBIAJHDQBBBiEQDPADCyAAIAFBAWoiASACELuAgIAAIhAN6AEgASEBDDELIABCADcDIEESIRAM1QMLIAEiECACRw0rQR0hEAztAwsCQCABIgEgAkYNACABQQFqIQFBECEQDNQDC0EHIRAM7AMLIABCACAAKQMgIhEgAiABIhBrrSISfSITIBMgEVYbNwMgIBEgElYiFEUN5QFBCCEQDOsDCwJAIAEiASACRg0AIABBiYCAgAA2AgggACABNgIEIAEhAUEUIRAM0gMLQQkhEAzqAwsgASEBIAApAyBQDeQBIAEhAQzyAgsCQCABIgEgAkcNAEELIRAM6QMLIAAgAUEBaiIBIAIQtoCAgAAiEA3lASABIQEM8gILIAAgASIBIAIQuICAgAAiEA3lASABIQEM8gILIAAgASIBIAIQuICAgAAiEA3mASABIQEMDQsgACABIgEgAhC6gICAACIQDecBIAEhAQzwAgsCQCABIgEgAkcNAEEPIRAM5QMLIAEtAAAiEEE7Rg0IIBBBDUcN6AEgAUEBaiEBDO8CCyAAIAEiASACELqAgIAAIhAN6AEgASEBDPICCwNAAkAgAS0AAEHwtYCAAGotAAAiEEEBRg0AIBBBAkcN6wEgACgCBCEQIABBADYCBCAAIBAgAUEBaiIBELmAgIAAIhAN6gEgASEBDPQCCyABQQFqIgEgAkcNAAtBEiEQDOIDCyAAIAEiASACELqAgIAAIhAN6QEgASEBDAoLIAEiASACRw0GQRshEAzgAwsCQCABIgEgAkcNAEEWIRAM4AMLIABBioCAgAA2AgggACABNgIEIAAgASACELiAgIAAIhAN6gEgASEBQSAhEAzGAwsCQCABIgEgAkYNAANAAkAgAS0AAEHwt4CAAGotAAAiEEECRg0AAkAgEEF/ag4E5QHsAQDrAewBCyABQQFqIQFBCCEQDMgDCyABQQFqIgEgAkcNAAtBFSEQDN8DC0EVIRAM3gMLA0ACQCABLQAAQfC5gIAAai0AACIQQQJGDQAgEEF/ag4E3gHsAeAB6wHsAQsgAUEBaiIBIAJHDQALQRghEAzdAwsCQCABIgEgAkYNACAAQYuAgIAANgIIIAAgATYCBCABIQFBByEQDMQDC0EZIRAM3AMLIAFBAWohAQwCCwJAIAEiFCACRw0AQRohEAzbAwsgFCEBAkAgFC0AAEFzag4U3QLuAu4C7gLuAu4C7gLuAu4C7gLuAu4C7gLuAu4C7gLuAu4C7gIA7gILQQAhECAAQQA2AhwgAEGvi4CAADYCECAAQQI2AgwgACAUQQFqNgIUDNoDCwJAIAEtAAAiEEE7Rg0AIBBBDUcN6AEgAUEBaiEBDOUCCyABQQFqIQELQSIhEAy/AwsCQCABIhAgAkcNAEEcIRAM2AMLQgAhESAQIQEgEC0AAEFQag435wHmAQECAwQFBgcIAAAAAAAAAAkKCwwNDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADxAREhMUAAtBHiEQDL0DC0ICIREM5QELQgMhEQzkAQtCBCERDOMBC0IFIREM4gELQgYhEQzhAQtCByERDOABC0IIIREM3wELQgkhEQzeAQtCCiERDN0BC0ILIREM3AELQgwhEQzbAQtCDSERDNoBC0IOIREM2QELQg8hEQzYAQtCCiERDNcBC0ILIREM1gELQgwhEQzVAQtCDSERDNQBC0IOIREM0wELQg8hEQzSAQtCACERAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAQLQAAQVBqDjflAeQBAAECAwQFBgfmAeYB5gHmAeYB5gHmAQgJCgsMDeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gEODxAREhPmAQtCAiERDOQBC0IDIREM4wELQgQhEQziAQtCBSERDOEBC0IGIREM4AELQgchEQzfAQtCCCERDN4BC0IJIREM3QELQgohEQzcAQtCCyERDNsBC0IMIREM2gELQg0hEQzZAQtCDiERDNgBC0IPIREM1wELQgohEQzWAQtCCyERDNUBC0IMIREM1AELQg0hEQzTAQtCDiERDNIBC0IPIREM0QELIABCACAAKQMgIhEgAiABIhBrrSISfSITIBMgEVYbNwMgIBEgElYiFEUN0gFBHyEQDMADCwJAIAEiASACRg0AIABBiYCAgAA2AgggACABNgIEIAEhAUEkIRAMpwMLQSAhEAy/AwsgACABIhAgAhC+gICAAEF/ag4FtgEAxQIB0QHSAQtBESEQDKQDCyAAQQE6AC8gECEBDLsDCyABIgEgAkcN0gFBJCEQDLsDCyABIg0gAkcNHkHGACEQDLoDCyAAIAEiASACELKAgIAAIhAN1AEgASEBDLUBCyABIhAgAkcNJkHQACEQDLgDCwJAIAEiASACRw0AQSghEAy4AwsgAEEANgIEIABBjICAgAA2AgggACABIAEQsYCAgAAiEA3TASABIQEM2AELAkAgASIQIAJHDQBBKSEQDLcDCyAQLQAAIgFBIEYNFCABQQlHDdMBIBBBAWohAQwVCwJAIAEiASACRg0AIAFBAWohAQwXC0EqIRAMtQMLAkAgASIQIAJHDQBBKyEQDLUDCwJAIBAtAAAiAUEJRg0AIAFBIEcN1QELIAAtACxBCEYN0wEgECEBDJEDCwJAIAEiASACRw0AQSwhEAy0AwsgAS0AAEEKRw3VASABQQFqIQEMyQILIAEiDiACRw3VAUEvIRAMsgMLA0ACQCABLQAAIhBBIEYNAAJAIBBBdmoOBADcAdwBANoBCyABIQEM4AELIAFBAWoiASACRw0AC0ExIRAMsQMLQTIhECABIhQgAkYNsAMgAiAUayAAKAIAIgFqIRUgFCABa0EDaiEWAkADQCAULQAAIhdBIHIgFyAXQb9/akH/AXFBGkkbQf8BcSABQfC7gIAAai0AAEcNAQJAIAFBA0cNAEEGIQEMlgMLIAFBAWohASAUQQFqIhQgAkcNAAsgACAVNgIADLEDCyAAQQA2AgAgFCEBDNkBC0EzIRAgASIUIAJGDa8DIAIgFGsgACgCACIBaiEVIBQgAWtBCGohFgJAA0AgFC0AACIXQSByIBcgF0G/f2pB/wFxQRpJG0H/AXEgAUH0u4CAAGotAABHDQECQCABQQhHDQBBBSEBDJUDCyABQQFqIQEgFEEBaiIUIAJHDQALIAAgFTYCAAywAwsgAEEANgIAIBQhAQzYAQtBNCEQIAEiFCACRg2uAyACIBRrIAAoAgAiAWohFSAUIAFrQQVqIRYCQANAIBQtAAAiF0EgciAXIBdBv39qQf8BcUEaSRtB/wFxIAFB0MKAgABqLQAARw0BAkAgAUEFRw0AQQchAQyUAwsgAUEBaiEBIBRBAWoiFCACRw0ACyAAIBU2AgAMrwMLIABBADYCACAUIQEM1wELAkAgASIBIAJGDQADQAJAIAEtAABBgL6AgABqLQAAIhBBAUYNACAQQQJGDQogASEBDN0BCyABQQFqIgEgAkcNAAtBMCEQDK4DC0EwIRAMrQMLAkAgASIBIAJGDQADQAJAIAEtAAAiEEEgRg0AIBBBdmoOBNkB2gHaAdkB2gELIAFBAWoiASACRw0AC0E4IRAMrQMLQTghEAysAwsDQAJAIAEtAAAiEEEgRg0AIBBBCUcNAwsgAUEBaiIBIAJHDQALQTwhEAyrAwsDQAJAIAEtAAAiEEEgRg0AAkACQCAQQXZqDgTaAQEB2gEACyAQQSxGDdsBCyABIQEMBAsgAUEBaiIBIAJHDQALQT8hEAyqAwsgASEBDNsBC0HAACEQIAEiFCACRg2oAyACIBRrIAAoAgAiAWohFiAUIAFrQQZqIRcCQANAIBQtAABBIHIgAUGAwICAAGotAABHDQEgAUEGRg2OAyABQQFqIQEgFEEBaiIUIAJHDQALIAAgFjYCAAypAwsgAEEANgIAIBQhAQtBNiEQDI4DCwJAIAEiDyACRw0AQcEAIRAMpwMLIABBjICAgAA2AgggACAPNgIEIA8hASAALQAsQX9qDgTNAdUB1wHZAYcDCyABQQFqIQEMzAELAkAgASIBIAJGDQADQAJAIAEtAAAiEEEgciAQIBBBv39qQf8BcUEaSRtB/wFxIhBBCUYNACAQQSBGDQACQAJAAkACQCAQQZ1/ag4TAAMDAwMDAwMBAwMDAwMDAwMDAgMLIAFBAWohAUExIRAMkQMLIAFBAWohAUEyIRAMkAMLIAFBAWohAUEzIRAMjwMLIAEhAQzQAQsgAUEBaiIBIAJHDQALQTUhEAylAwtBNSEQDKQDCwJAIAEiASACRg0AA0ACQCABLQAAQYC8gIAAai0AAEEBRg0AIAEhAQzTAQsgAUEBaiIBIAJHDQALQT0hEAykAwtBPSEQDKMDCyAAIAEiASACELCAgIAAIhAN1gEgASEBDAELIBBBAWohAQtBPCEQDIcDCwJAIAEiASACRw0AQcIAIRAMoAMLAkADQAJAIAEtAABBd2oOGAAC/gL+AoQD/gL+Av4C/gL+Av4C/gL+Av4C/gL+Av4C/gL+Av4C/gL+Av4CAP4CCyABQQFqIgEgAkcNAAtBwgAhEAygAwsgAUEBaiEBIAAtAC1BAXFFDb0BIAEhAQtBLCEQDIUDCyABIgEgAkcN0wFBxAAhEAydAwsDQAJAIAEtAABBkMCAgABqLQAAQQFGDQAgASEBDLcCCyABQQFqIgEgAkcNAAtBxQAhEAycAwsgDS0AACIQQSBGDbMBIBBBOkcNgQMgACgCBCEBIABBADYCBCAAIAEgDRCvgICAACIBDdABIA1BAWohAQyzAgtBxwAhECABIg0gAkYNmgMgAiANayAAKAIAIgFqIRYgDSABa0EFaiEXA0AgDS0AACIUQSByIBQgFEG/f2pB/wFxQRpJG0H/AXEgAUGQwoCAAGotAABHDYADIAFBBUYN9AIgAUEBaiEBIA1BAWoiDSACRw0ACyAAIBY2AgAMmgMLQcgAIRAgASINIAJGDZkDIAIgDWsgACgCACIBaiEWIA0gAWtBCWohFwNAIA0tAAAiFEEgciAUIBRBv39qQf8BcUEaSRtB/wFxIAFBlsKAgABqLQAARw3/AgJAIAFBCUcNAEECIQEM9QILIAFBAWohASANQQFqIg0gAkcNAAsgACAWNgIADJkDCwJAIAEiDSACRw0AQckAIRAMmQMLAkACQCANLQAAIgFBIHIgASABQb9/akH/AXFBGkkbQf8BcUGSf2oOBwCAA4ADgAOAA4ADAYADCyANQQFqIQFBPiEQDIADCyANQQFqIQFBPyEQDP8CC0HKACEQIAEiDSACRg2XAyACIA1rIAAoAgAiAWohFiANIAFrQQFqIRcDQCANLQAAIhRBIHIgFCAUQb9/akH/AXFBGkkbQf8BcSABQaDCgIAAai0AAEcN/QIgAUEBRg3wAiABQQFqIQEgDUEBaiINIAJHDQALIAAgFjYCAAyXAwtBywAhECABIg0gAkYNlgMgAiANayAAKAIAIgFqIRYgDSABa0EOaiEXA0AgDS0AACIUQSByIBQgFEG/f2pB/wFxQRpJG0H/AXEgAUGiwoCAAGotAABHDfwCIAFBDkYN8AIgAUEBaiEBIA1BAWoiDSACRw0ACyAAIBY2AgAMlgMLQcwAIRAgASINIAJGDZUDIAIgDWsgACgCACIBaiEWIA0gAWtBD2ohFwNAIA0tAAAiFEEgciAUIBRBv39qQf8BcUEaSRtB/wFxIAFBwMKAgABqLQAARw37AgJAIAFBD0cNAEEDIQEM8QILIAFBAWohASANQQFqIg0gAkcNAAsgACAWNgIADJUDC0HNACEQIAEiDSACRg2UAyACIA1rIAAoAgAiAWohFiANIAFrQQVqIRcDQCANLQAAIhRBIHIgFCAUQb9/akH/AXFBGkkbQf8BcSABQdDCgIAAai0AAEcN+gICQCABQQVHDQBBBCEBDPACCyABQQFqIQEgDUEBaiINIAJHDQALIAAgFjYCAAyUAwsCQCABIg0gAkcNAEHOACEQDJQDCwJAAkACQAJAIA0tAAAiAUEgciABIAFBv39qQf8BcUEaSRtB/wFxQZ1/ag4TAP0C/QL9Av0C/QL9Av0C/QL9Av0C/QL9AgH9Av0C/QICA/0CCyANQQFqIQFBwQAhEAz9AgsgDUEBaiEBQcIAIRAM/AILIA1BAWohAUHDACEQDPsCCyANQQFqIQFBxAAhEAz6AgsCQCABIgEgAkYNACAAQY2AgIAANgIIIAAgATYCBCABIQFBxQAhEAz6AgtBzwAhEAySAwsgECEBAkACQCAQLQAAQXZqDgQBqAKoAgCoAgsgEEEBaiEBC0EnIRAM+AILAkAgASIBIAJHDQBB0QAhEAyRAwsCQCABLQAAQSBGDQAgASEBDI0BCyABQQFqIQEgAC0ALUEBcUUNxwEgASEBDIwBCyABIhcgAkcNyAFB0gAhEAyPAwtB0wAhECABIhQgAkYNjgMgAiAUayAAKAIAIgFqIRYgFCABa0EBaiEXA0AgFC0AACABQdbCgIAAai0AAEcNzAEgAUEBRg3HASABQQFqIQEgFEEBaiIUIAJHDQALIAAgFjYCAAyOAwsCQCABIgEgAkcNAEHVACEQDI4DCyABLQAAQQpHDcwBIAFBAWohAQzHAQsCQCABIgEgAkcNAEHWACEQDI0DCwJAAkAgAS0AAEF2ag4EAM0BzQEBzQELIAFBAWohAQzHAQsgAUEBaiEBQcoAIRAM8wILIAAgASIBIAIQroCAgAAiEA3LASABIQFBzQAhEAzyAgsgAC0AKUEiRg2FAwymAgsCQCABIgEgAkcNAEHbACEQDIoDC0EAIRRBASEXQQEhFkEAIRACQAJAAkACQAJAAkACQAJAAkAgAS0AAEFQag4K1AHTAQABAgMEBQYI1QELQQIhEAwGC0EDIRAMBQtBBCEQDAQLQQUhEAwDC0EGIRAMAgtBByEQDAELQQghEAtBACEXQQAhFkEAIRQMzAELQQkhEEEBIRRBACEXQQAhFgzLAQsCQCABIgEgAkcNAEHdACEQDIkDCyABLQAAQS5HDcwBIAFBAWohAQymAgsgASIBIAJHDcwBQd8AIRAMhwMLAkAgASIBIAJGDQAgAEGOgICAADYCCCAAIAE2AgQgASEBQdAAIRAM7gILQeAAIRAMhgMLQeEAIRAgASIBIAJGDYUDIAIgAWsgACgCACIUaiEWIAEgFGtBA2ohFwNAIAEtAAAgFEHiwoCAAGotAABHDc0BIBRBA0YNzAEgFEEBaiEUIAFBAWoiASACRw0ACyAAIBY2AgAMhQMLQeIAIRAgASIBIAJGDYQDIAIgAWsgACgCACIUaiEWIAEgFGtBAmohFwNAIAEtAAAgFEHmwoCAAGotAABHDcwBIBRBAkYNzgEgFEEBaiEUIAFBAWoiASACRw0ACyAAIBY2AgAMhAMLQeMAIRAgASIBIAJGDYMDIAIgAWsgACgCACIUaiEWIAEgFGtBA2ohFwNAIAEtAAAgFEHpwoCAAGotAABHDcsBIBRBA0YNzgEgFEEBaiEUIAFBAWoiASACRw0ACyAAIBY2AgAMgwMLAkAgASIBIAJHDQBB5QAhEAyDAwsgACABQQFqIgEgAhCogICAACIQDc0BIAEhAUHWACEQDOkCCwJAIAEiASACRg0AA0ACQCABLQAAIhBBIEYNAAJAAkACQCAQQbh/ag4LAAHPAc8BzwHPAc8BzwHPAc8BAs8BCyABQQFqIQFB0gAhEAztAgsgAUEBaiEBQdMAIRAM7AILIAFBAWohAUHUACEQDOsCCyABQQFqIgEgAkcNAAtB5AAhEAyCAwtB5AAhEAyBAwsDQAJAIAEtAABB8MKAgABqLQAAIhBBAUYNACAQQX5qDgPPAdAB0QHSAQsgAUEBaiIBIAJHDQALQeYAIRAMgAMLAkAgASIBIAJGDQAgAUEBaiEBDAMLQecAIRAM/wILA0ACQCABLQAAQfDEgIAAai0AACIQQQFGDQACQCAQQX5qDgTSAdMB1AEA1QELIAEhAUHXACEQDOcCCyABQQFqIgEgAkcNAAtB6AAhEAz+AgsCQCABIgEgAkcNAEHpACEQDP4CCwJAIAEtAAAiEEF2ag4augHVAdUBvAHVAdUB1QHVAdUB1QHVAdUB1QHVAdUB1QHVAdUB1QHVAdUB1QHKAdUB1QEA0wELIAFBAWohAQtBBiEQDOMCCwNAAkAgAS0AAEHwxoCAAGotAABBAUYNACABIQEMngILIAFBAWoiASACRw0AC0HqACEQDPsCCwJAIAEiASACRg0AIAFBAWohAQwDC0HrACEQDPoCCwJAIAEiASACRw0AQewAIRAM+gILIAFBAWohAQwBCwJAIAEiASACRw0AQe0AIRAM+QILIAFBAWohAQtBBCEQDN4CCwJAIAEiFCACRw0AQe4AIRAM9wILIBQhAQJAAkACQCAULQAAQfDIgIAAai0AAEF/ag4H1AHVAdYBAJwCAQLXAQsgFEEBaiEBDAoLIBRBAWohAQzNAQtBACEQIABBADYCHCAAQZuSgIAANgIQIABBBzYCDCAAIBRBAWo2AhQM9gILAkADQAJAIAEtAABB8MiAgABqLQAAIhBBBEYNAAJAAkAgEEF/ag4H0gHTAdQB2QEABAHZAQsgASEBQdoAIRAM4AILIAFBAWohAUHcACEQDN8CCyABQQFqIgEgAkcNAAtB7wAhEAz2AgsgAUEBaiEBDMsBCwJAIAEiFCACRw0AQfAAIRAM9QILIBQtAABBL0cN1AEgFEEBaiEBDAYLAkAgASIUIAJHDQBB8QAhEAz0AgsCQCAULQAAIgFBL0cNACAUQQFqIQFB3QAhEAzbAgsgAUF2aiIEQRZLDdMBQQEgBHRBiYCAAnFFDdMBDMoCCwJAIAEiASACRg0AIAFBAWohAUHeACEQDNoCC0HyACEQDPICCwJAIAEiFCACRw0AQfQAIRAM8gILIBQhAQJAIBQtAABB8MyAgABqLQAAQX9qDgPJApQCANQBC0HhACEQDNgCCwJAIAEiFCACRg0AA0ACQCAULQAAQfDKgIAAai0AACIBQQNGDQACQCABQX9qDgLLAgDVAQsgFCEBQd8AIRAM2gILIBRBAWoiFCACRw0AC0HzACEQDPECC0HzACEQDPACCwJAIAEiASACRg0AIABBj4CAgAA2AgggACABNgIEIAEhAUHgACEQDNcCC0H1ACEQDO8CCwJAIAEiASACRw0AQfYAIRAM7wILIABBj4CAgAA2AgggACABNgIEIAEhAQtBAyEQDNQCCwNAIAEtAABBIEcNwwIgAUEBaiIBIAJHDQALQfcAIRAM7AILAkAgASIBIAJHDQBB+AAhEAzsAgsgAS0AAEEgRw3OASABQQFqIQEM7wELIAAgASIBIAIQrICAgAAiEA3OASABIQEMjgILAkAgASIEIAJHDQBB+gAhEAzqAgsgBC0AAEHMAEcN0QEgBEEBaiEBQRMhEAzPAQsCQCABIgQgAkcNAEH7ACEQDOkCCyACIARrIAAoAgAiAWohFCAEIAFrQQVqIRADQCAELQAAIAFB8M6AgABqLQAARw3QASABQQVGDc4BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQfsAIRAM6AILAkAgASIEIAJHDQBB/AAhEAzoAgsCQAJAIAQtAABBvX9qDgwA0QHRAdEB0QHRAdEB0QHRAdEB0QEB0QELIARBAWohAUHmACEQDM8CCyAEQQFqIQFB5wAhEAzOAgsCQCABIgQgAkcNAEH9ACEQDOcCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHtz4CAAGotAABHDc8BIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEH9ACEQDOcCCyAAQQA2AgAgEEEBaiEBQRAhEAzMAQsCQCABIgQgAkcNAEH+ACEQDOYCCyACIARrIAAoAgAiAWohFCAEIAFrQQVqIRACQANAIAQtAAAgAUH2zoCAAGotAABHDc4BIAFBBUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEH+ACEQDOYCCyAAQQA2AgAgEEEBaiEBQRYhEAzLAQsCQCABIgQgAkcNAEH/ACEQDOUCCyACIARrIAAoAgAiAWohFCAEIAFrQQNqIRACQANAIAQtAAAgAUH8zoCAAGotAABHDc0BIAFBA0YNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEH/ACEQDOUCCyAAQQA2AgAgEEEBaiEBQQUhEAzKAQsCQCABIgQgAkcNAEGAASEQDOQCCyAELQAAQdkARw3LASAEQQFqIQFBCCEQDMkBCwJAIAEiBCACRw0AQYEBIRAM4wILAkACQCAELQAAQbJ/ag4DAMwBAcwBCyAEQQFqIQFB6wAhEAzKAgsgBEEBaiEBQewAIRAMyQILAkAgASIEIAJHDQBBggEhEAziAgsCQAJAIAQtAABBuH9qDggAywHLAcsBywHLAcsBAcsBCyAEQQFqIQFB6gAhEAzJAgsgBEEBaiEBQe0AIRAMyAILAkAgASIEIAJHDQBBgwEhEAzhAgsgAiAEayAAKAIAIgFqIRAgBCABa0ECaiEUAkADQCAELQAAIAFBgM+AgABqLQAARw3JASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBA2AgBBgwEhEAzhAgtBACEQIABBADYCACAUQQFqIQEMxgELAkAgASIEIAJHDQBBhAEhEAzgAgsgAiAEayAAKAIAIgFqIRQgBCABa0EEaiEQAkADQCAELQAAIAFBg8+AgABqLQAARw3IASABQQRGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBhAEhEAzgAgsgAEEANgIAIBBBAWohAUEjIRAMxQELAkAgASIEIAJHDQBBhQEhEAzfAgsCQAJAIAQtAABBtH9qDggAyAHIAcgByAHIAcgBAcgBCyAEQQFqIQFB7wAhEAzGAgsgBEEBaiEBQfAAIRAMxQILAkAgASIEIAJHDQBBhgEhEAzeAgsgBC0AAEHFAEcNxQEgBEEBaiEBDIMCCwJAIAEiBCACRw0AQYcBIRAM3QILIAIgBGsgACgCACIBaiEUIAQgAWtBA2ohEAJAA0AgBC0AACABQYjPgIAAai0AAEcNxQEgAUEDRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQYcBIRAM3QILIABBADYCACAQQQFqIQFBLSEQDMIBCwJAIAEiBCACRw0AQYgBIRAM3AILIAIgBGsgACgCACIBaiEUIAQgAWtBCGohEAJAA0AgBC0AACABQdDPgIAAai0AAEcNxAEgAUEIRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQYgBIRAM3AILIABBADYCACAQQQFqIQFBKSEQDMEBCwJAIAEiASACRw0AQYkBIRAM2wILQQEhECABLQAAQd8ARw3AASABQQFqIQEMgQILAkAgASIEIAJHDQBBigEhEAzaAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQA0AgBC0AACABQYzPgIAAai0AAEcNwQEgAUEBRg2vAiABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGKASEQDNkCCwJAIAEiBCACRw0AQYsBIRAM2QILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQY7PgIAAai0AAEcNwQEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQYsBIRAM2QILIABBADYCACAQQQFqIQFBAiEQDL4BCwJAIAEiBCACRw0AQYwBIRAM2AILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQfDPgIAAai0AAEcNwAEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQYwBIRAM2AILIABBADYCACAQQQFqIQFBHyEQDL0BCwJAIAEiBCACRw0AQY0BIRAM1wILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQfLPgIAAai0AAEcNvwEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQY0BIRAM1wILIABBADYCACAQQQFqIQFBCSEQDLwBCwJAIAEiBCACRw0AQY4BIRAM1gILAkACQCAELQAAQbd/ag4HAL8BvwG/Ab8BvwEBvwELIARBAWohAUH4ACEQDL0CCyAEQQFqIQFB+QAhEAy8AgsCQCABIgQgAkcNAEGPASEQDNUCCyACIARrIAAoAgAiAWohFCAEIAFrQQVqIRACQANAIAQtAAAgAUGRz4CAAGotAABHDb0BIAFBBUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGPASEQDNUCCyAAQQA2AgAgEEEBaiEBQRghEAy6AQsCQCABIgQgAkcNAEGQASEQDNQCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUGXz4CAAGotAABHDbwBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGQASEQDNQCCyAAQQA2AgAgEEEBaiEBQRchEAy5AQsCQCABIgQgAkcNAEGRASEQDNMCCyACIARrIAAoAgAiAWohFCAEIAFrQQZqIRACQANAIAQtAAAgAUGaz4CAAGotAABHDbsBIAFBBkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGRASEQDNMCCyAAQQA2AgAgEEEBaiEBQRUhEAy4AQsCQCABIgQgAkcNAEGSASEQDNICCyACIARrIAAoAgAiAWohFCAEIAFrQQVqIRACQANAIAQtAAAgAUGhz4CAAGotAABHDboBIAFBBUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGSASEQDNICCyAAQQA2AgAgEEEBaiEBQR4hEAy3AQsCQCABIgQgAkcNAEGTASEQDNECCyAELQAAQcwARw24ASAEQQFqIQFBCiEQDLYBCwJAIAQgAkcNAEGUASEQDNACCwJAAkAgBC0AAEG/f2oODwC5AbkBuQG5AbkBuQG5AbkBuQG5AbkBuQG5AQG5AQsgBEEBaiEBQf4AIRAMtwILIARBAWohAUH/ACEQDLYCCwJAIAQgAkcNAEGVASEQDM8CCwJAAkAgBC0AAEG/f2oOAwC4AQG4AQsgBEEBaiEBQf0AIRAMtgILIARBAWohBEGAASEQDLUCCwJAIAQgAkcNAEGWASEQDM4CCyACIARrIAAoAgAiAWohFCAEIAFrQQFqIRACQANAIAQtAAAgAUGnz4CAAGotAABHDbYBIAFBAUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGWASEQDM4CCyAAQQA2AgAgEEEBaiEBQQshEAyzAQsCQCAEIAJHDQBBlwEhEAzNAgsCQAJAAkACQCAELQAAQVNqDiMAuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AQG4AbgBuAG4AbgBArgBuAG4AQO4AQsgBEEBaiEBQfsAIRAMtgILIARBAWohAUH8ACEQDLUCCyAEQQFqIQRBgQEhEAy0AgsgBEEBaiEEQYIBIRAMswILAkAgBCACRw0AQZgBIRAMzAILIAIgBGsgACgCACIBaiEUIAQgAWtBBGohEAJAA0AgBC0AACABQanPgIAAai0AAEcNtAEgAUEERg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZgBIRAMzAILIABBADYCACAQQQFqIQFBGSEQDLEBCwJAIAQgAkcNAEGZASEQDMsCCyACIARrIAAoAgAiAWohFCAEIAFrQQVqIRACQANAIAQtAAAgAUGuz4CAAGotAABHDbMBIAFBBUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGZASEQDMsCCyAAQQA2AgAgEEEBaiEBQQYhEAywAQsCQCAEIAJHDQBBmgEhEAzKAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFBtM+AgABqLQAARw2yASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBmgEhEAzKAgsgAEEANgIAIBBBAWohAUEcIRAMrwELAkAgBCACRw0AQZsBIRAMyQILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQbbPgIAAai0AAEcNsQEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZsBIRAMyQILIABBADYCACAQQQFqIQFBJyEQDK4BCwJAIAQgAkcNAEGcASEQDMgCCwJAAkAgBC0AAEGsf2oOAgABsQELIARBAWohBEGGASEQDK8CCyAEQQFqIQRBhwEhEAyuAgsCQCAEIAJHDQBBnQEhEAzHAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFBuM+AgABqLQAARw2vASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBnQEhEAzHAgsgAEEANgIAIBBBAWohAUEmIRAMrAELAkAgBCACRw0AQZ4BIRAMxgILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQbrPgIAAai0AAEcNrgEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZ4BIRAMxgILIABBADYCACAQQQFqIQFBAyEQDKsBCwJAIAQgAkcNAEGfASEQDMUCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHtz4CAAGotAABHDa0BIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGfASEQDMUCCyAAQQA2AgAgEEEBaiEBQQwhEAyqAQsCQCAEIAJHDQBBoAEhEAzEAgsgAiAEayAAKAIAIgFqIRQgBCABa0EDaiEQAkADQCAELQAAIAFBvM+AgABqLQAARw2sASABQQNGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBoAEhEAzEAgsgAEEANgIAIBBBAWohAUENIRAMqQELAkAgBCACRw0AQaEBIRAMwwILAkACQCAELQAAQbp/ag4LAKwBrAGsAawBrAGsAawBrAGsAQGsAQsgBEEBaiEEQYsBIRAMqgILIARBAWohBEGMASEQDKkCCwJAIAQgAkcNAEGiASEQDMICCyAELQAAQdAARw2pASAEQQFqIQQM6QELAkAgBCACRw0AQaMBIRAMwQILAkACQCAELQAAQbd/ag4HAaoBqgGqAaoBqgEAqgELIARBAWohBEGOASEQDKgCCyAEQQFqIQFBIiEQDKYBCwJAIAQgAkcNAEGkASEQDMACCyACIARrIAAoAgAiAWohFCAEIAFrQQFqIRACQANAIAQtAAAgAUHAz4CAAGotAABHDagBIAFBAUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGkASEQDMACCyAAQQA2AgAgEEEBaiEBQR0hEAylAQsCQCAEIAJHDQBBpQEhEAy/AgsCQAJAIAQtAABBrn9qDgMAqAEBqAELIARBAWohBEGQASEQDKYCCyAEQQFqIQFBBCEQDKQBCwJAIAQgAkcNAEGmASEQDL4CCwJAAkACQAJAAkAgBC0AAEG/f2oOFQCqAaoBqgGqAaoBqgGqAaoBqgGqAQGqAaoBAqoBqgEDqgGqAQSqAQsgBEEBaiEEQYgBIRAMqAILIARBAWohBEGJASEQDKcCCyAEQQFqIQRBigEhEAymAgsgBEEBaiEEQY8BIRAMpQILIARBAWohBEGRASEQDKQCCwJAIAQgAkcNAEGnASEQDL0CCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHtz4CAAGotAABHDaUBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGnASEQDL0CCyAAQQA2AgAgEEEBaiEBQREhEAyiAQsCQCAEIAJHDQBBqAEhEAy8AgsgAiAEayAAKAIAIgFqIRQgBCABa0ECaiEQAkADQCAELQAAIAFBws+AgABqLQAARw2kASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBqAEhEAy8AgsgAEEANgIAIBBBAWohAUEsIRAMoQELAkAgBCACRw0AQakBIRAMuwILIAIgBGsgACgCACIBaiEUIAQgAWtBBGohEAJAA0AgBC0AACABQcXPgIAAai0AAEcNowEgAUEERg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQakBIRAMuwILIABBADYCACAQQQFqIQFBKyEQDKABCwJAIAQgAkcNAEGqASEQDLoCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHKz4CAAGotAABHDaIBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGqASEQDLoCCyAAQQA2AgAgEEEBaiEBQRQhEAyfAQsCQCAEIAJHDQBBqwEhEAy5AgsCQAJAAkACQCAELQAAQb5/ag4PAAECpAGkAaQBpAGkAaQBpAGkAaQBpAGkAQOkAQsgBEEBaiEEQZMBIRAMogILIARBAWohBEGUASEQDKECCyAEQQFqIQRBlQEhEAygAgsgBEEBaiEEQZYBIRAMnwILAkAgBCACRw0AQawBIRAMuAILIAQtAABBxQBHDZ8BIARBAWohBAzgAQsCQCAEIAJHDQBBrQEhEAy3AgsgAiAEayAAKAIAIgFqIRQgBCABa0ECaiEQAkADQCAELQAAIAFBzc+AgABqLQAARw2fASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBrQEhEAy3AgsgAEEANgIAIBBBAWohAUEOIRAMnAELAkAgBCACRw0AQa4BIRAMtgILIAQtAABB0ABHDZ0BIARBAWohAUElIRAMmwELAkAgBCACRw0AQa8BIRAMtQILIAIgBGsgACgCACIBaiEUIAQgAWtBCGohEAJAA0AgBC0AACABQdDPgIAAai0AAEcNnQEgAUEIRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQa8BIRAMtQILIABBADYCACAQQQFqIQFBKiEQDJoBCwJAIAQgAkcNAEGwASEQDLQCCwJAAkAgBC0AAEGrf2oOCwCdAZ0BnQGdAZ0BnQGdAZ0BnQEBnQELIARBAWohBEGaASEQDJsCCyAEQQFqIQRBmwEhEAyaAgsCQCAEIAJHDQBBsQEhEAyzAgsCQAJAIAQtAABBv39qDhQAnAGcAZwBnAGcAZwBnAGcAZwBnAGcAZwBnAGcAZwBnAGcAZwBAZwBCyAEQQFqIQRBmQEhEAyaAgsgBEEBaiEEQZwBIRAMmQILAkAgBCACRw0AQbIBIRAMsgILIAIgBGsgACgCACIBaiEUIAQgAWtBA2ohEAJAA0AgBC0AACABQdnPgIAAai0AAEcNmgEgAUEDRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQbIBIRAMsgILIABBADYCACAQQQFqIQFBISEQDJcBCwJAIAQgAkcNAEGzASEQDLECCyACIARrIAAoAgAiAWohFCAEIAFrQQZqIRACQANAIAQtAAAgAUHdz4CAAGotAABHDZkBIAFBBkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGzASEQDLECCyAAQQA2AgAgEEEBaiEBQRohEAyWAQsCQCAEIAJHDQBBtAEhEAywAgsCQAJAAkAgBC0AAEG7f2oOEQCaAZoBmgGaAZoBmgGaAZoBmgEBmgGaAZoBmgGaAQKaAQsgBEEBaiEEQZ0BIRAMmAILIARBAWohBEGeASEQDJcCCyAEQQFqIQRBnwEhEAyWAgsCQCAEIAJHDQBBtQEhEAyvAgsgAiAEayAAKAIAIgFqIRQgBCABa0EFaiEQAkADQCAELQAAIAFB5M+AgABqLQAARw2XASABQQVGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBtQEhEAyvAgsgAEEANgIAIBBBAWohAUEoIRAMlAELAkAgBCACRw0AQbYBIRAMrgILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQerPgIAAai0AAEcNlgEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQbYBIRAMrgILIABBADYCACAQQQFqIQFBByEQDJMBCwJAIAQgAkcNAEG3ASEQDK0CCwJAAkAgBC0AAEG7f2oODgCWAZYBlgGWAZYBlgGWAZYBlgGWAZYBlgEBlgELIARBAWohBEGhASEQDJQCCyAEQQFqIQRBogEhEAyTAgsCQCAEIAJHDQBBuAEhEAysAgsgAiAEayAAKAIAIgFqIRQgBCABa0ECaiEQAkADQCAELQAAIAFB7c+AgABqLQAARw2UASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBuAEhEAysAgsgAEEANgIAIBBBAWohAUESIRAMkQELAkAgBCACRw0AQbkBIRAMqwILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQfDPgIAAai0AAEcNkwEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQbkBIRAMqwILIABBADYCACAQQQFqIQFBICEQDJABCwJAIAQgAkcNAEG6ASEQDKoCCyACIARrIAAoAgAiAWohFCAEIAFrQQFqIRACQANAIAQtAAAgAUHyz4CAAGotAABHDZIBIAFBAUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEG6ASEQDKoCCyAAQQA2AgAgEEEBaiEBQQ8hEAyPAQsCQCAEIAJHDQBBuwEhEAypAgsCQAJAIAQtAABBt39qDgcAkgGSAZIBkgGSAQGSAQsgBEEBaiEEQaUBIRAMkAILIARBAWohBEGmASEQDI8CCwJAIAQgAkcNAEG8ASEQDKgCCyACIARrIAAoAgAiAWohFCAEIAFrQQdqIRACQANAIAQtAAAgAUH0z4CAAGotAABHDZABIAFBB0YNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEG8ASEQDKgCCyAAQQA2AgAgEEEBaiEBQRshEAyNAQsCQCAEIAJHDQBBvQEhEAynAgsCQAJAAkAgBC0AAEG+f2oOEgCRAZEBkQGRAZEBkQGRAZEBkQEBkQGRAZEBkQGRAZEBApEBCyAEQQFqIQRBpAEhEAyPAgsgBEEBaiEEQacBIRAMjgILIARBAWohBEGoASEQDI0CCwJAIAQgAkcNAEG+ASEQDKYCCyAELQAAQc4ARw2NASAEQQFqIQQMzwELAkAgBCACRw0AQb8BIRAMpQILAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgBC0AAEG/f2oOFQABAgOcAQQFBpwBnAGcAQcICQoLnAEMDQ4PnAELIARBAWohAUHoACEQDJoCCyAEQQFqIQFB6QAhEAyZAgsgBEEBaiEBQe4AIRAMmAILIARBAWohAUHyACEQDJcCCyAEQQFqIQFB8wAhEAyWAgsgBEEBaiEBQfYAIRAMlQILIARBAWohAUH3ACEQDJQCCyAEQQFqIQFB+gAhEAyTAgsgBEEBaiEEQYMBIRAMkgILIARBAWohBEGEASEQDJECCyAEQQFqIQRBhQEhEAyQAgsgBEEBaiEEQZIBIRAMjwILIARBAWohBEGYASEQDI4CCyAEQQFqIQRBoAEhEAyNAgsgBEEBaiEEQaMBIRAMjAILIARBAWohBEGqASEQDIsCCwJAIAQgAkYNACAAQZCAgIAANgIIIAAgBDYCBEGrASEQDIsCC0HAASEQDKMCCyAAIAUgAhCqgICAACIBDYsBIAUhAQxcCwJAIAYgAkYNACAGQQFqIQUMjQELQcIBIRAMoQILA0ACQCAQLQAAQXZqDgSMAQAAjwEACyAQQQFqIhAgAkcNAAtBwwEhEAygAgsCQCAHIAJGDQAgAEGRgICAADYCCCAAIAc2AgQgByEBQQEhEAyHAgtBxAEhEAyfAgsCQCAHIAJHDQBBxQEhEAyfAgsCQAJAIActAABBdmoOBAHOAc4BAM4BCyAHQQFqIQYMjQELIAdBAWohBQyJAQsCQCAHIAJHDQBBxgEhEAyeAgsCQAJAIActAABBdmoOFwGPAY8BAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAQCPAQsgB0EBaiEHC0GwASEQDIQCCwJAIAggAkcNAEHIASEQDJ0CCyAILQAAQSBHDY0BIABBADsBMiAIQQFqIQFBswEhEAyDAgsgASEXAkADQCAXIgcgAkYNASAHLQAAQVBqQf8BcSIQQQpPDcwBAkAgAC8BMiIUQZkzSw0AIAAgFEEKbCIUOwEyIBBB//8DcyAUQf7/A3FJDQAgB0EBaiEXIAAgFCAQaiIQOwEyIBBB//8DcUHoB0kNAQsLQQAhECAAQQA2AhwgAEHBiYCAADYCECAAQQ02AgwgACAHQQFqNgIUDJwCC0HHASEQDJsCCyAAIAggAhCugICAACIQRQ3KASAQQRVHDYwBIABByAE2AhwgACAINgIUIABByZeAgAA2AhAgAEEVNgIMQQAhEAyaAgsCQCAJIAJHDQBBzAEhEAyaAgtBACEUQQEhF0EBIRZBACEQAkACQAJAAkACQAJAAkACQAJAIAktAABBUGoOCpYBlQEAAQIDBAUGCJcBC0ECIRAMBgtBAyEQDAULQQQhEAwEC0EFIRAMAwtBBiEQDAILQQchEAwBC0EIIRALQQAhF0EAIRZBACEUDI4BC0EJIRBBASEUQQAhF0EAIRYMjQELAkAgCiACRw0AQc4BIRAMmQILIAotAABBLkcNjgEgCkEBaiEJDMoBCyALIAJHDY4BQdABIRAMlwILAkAgCyACRg0AIABBjoCAgAA2AgggACALNgIEQbcBIRAM/gELQdEBIRAMlgILAkAgBCACRw0AQdIBIRAMlgILIAIgBGsgACgCACIQaiEUIAQgEGtBBGohCwNAIAQtAAAgEEH8z4CAAGotAABHDY4BIBBBBEYN6QEgEEEBaiEQIARBAWoiBCACRw0ACyAAIBQ2AgBB0gEhEAyVAgsgACAMIAIQrICAgAAiAQ2NASAMIQEMuAELAkAgBCACRw0AQdQBIRAMlAILIAIgBGsgACgCACIQaiEUIAQgEGtBAWohDANAIAQtAAAgEEGB0ICAAGotAABHDY8BIBBBAUYNjgEgEEEBaiEQIARBAWoiBCACRw0ACyAAIBQ2AgBB1AEhEAyTAgsCQCAEIAJHDQBB1gEhEAyTAgsgAiAEayAAKAIAIhBqIRQgBCAQa0ECaiELA0AgBC0AACAQQYPQgIAAai0AAEcNjgEgEEECRg2QASAQQQFqIRAgBEEBaiIEIAJHDQALIAAgFDYCAEHWASEQDJICCwJAIAQgAkcNAEHXASEQDJICCwJAAkAgBC0AAEG7f2oOEACPAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BAY8BCyAEQQFqIQRBuwEhEAz5AQsgBEEBaiEEQbwBIRAM+AELAkAgBCACRw0AQdgBIRAMkQILIAQtAABByABHDYwBIARBAWohBAzEAQsCQCAEIAJGDQAgAEGQgICAADYCCCAAIAQ2AgRBvgEhEAz3AQtB2QEhEAyPAgsCQCAEIAJHDQBB2gEhEAyPAgsgBC0AAEHIAEYNwwEgAEEBOgAoDLkBCyAAQQI6AC8gACAEIAIQpoCAgAAiEA2NAUHCASEQDPQBCyAALQAoQX9qDgK3AbkBuAELA0ACQCAELQAAQXZqDgQAjgGOAQCOAQsgBEEBaiIEIAJHDQALQd0BIRAMiwILIABBADoALyAALQAtQQRxRQ2EAgsgAEEAOgAvIABBAToANCABIQEMjAELIBBBFUYN2gEgAEEANgIcIAAgATYCFCAAQaeOgIAANgIQIABBEjYCDEEAIRAMiAILAkAgACAQIAIQtICAgAAiBA0AIBAhAQyBAgsCQCAEQRVHDQAgAEEDNgIcIAAgEDYCFCAAQbCYgIAANgIQIABBFTYCDEEAIRAMiAILIABBADYCHCAAIBA2AhQgAEGnjoCAADYCECAAQRI2AgxBACEQDIcCCyAQQRVGDdYBIABBADYCHCAAIAE2AhQgAEHajYCAADYCECAAQRQ2AgxBACEQDIYCCyAAKAIEIRcgAEEANgIEIBAgEadqIhYhASAAIBcgECAWIBQbIhAQtYCAgAAiFEUNjQEgAEEHNgIcIAAgEDYCFCAAIBQ2AgxBACEQDIUCCyAAIAAvATBBgAFyOwEwIAEhAQtBKiEQDOoBCyAQQRVGDdEBIABBADYCHCAAIAE2AhQgAEGDjICAADYCECAAQRM2AgxBACEQDIICCyAQQRVGDc8BIABBADYCHCAAIAE2AhQgAEGaj4CAADYCECAAQSI2AgxBACEQDIECCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQt4CAgAAiEA0AIAFBAWohAQyNAQsgAEEMNgIcIAAgEDYCDCAAIAFBAWo2AhRBACEQDIACCyAQQRVGDcwBIABBADYCHCAAIAE2AhQgAEGaj4CAADYCECAAQSI2AgxBACEQDP8BCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQt4CAgAAiEA0AIAFBAWohAQyMAQsgAEENNgIcIAAgEDYCDCAAIAFBAWo2AhRBACEQDP4BCyAQQRVGDckBIABBADYCHCAAIAE2AhQgAEHGjICAADYCECAAQSM2AgxBACEQDP0BCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQuYCAgAAiEA0AIAFBAWohAQyLAQsgAEEONgIcIAAgEDYCDCAAIAFBAWo2AhRBACEQDPwBCyAAQQA2AhwgACABNgIUIABBwJWAgAA2AhAgAEECNgIMQQAhEAz7AQsgEEEVRg3FASAAQQA2AhwgACABNgIUIABBxoyAgAA2AhAgAEEjNgIMQQAhEAz6AQsgAEEQNgIcIAAgATYCFCAAIBA2AgxBACEQDPkBCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQuYCAgAAiBA0AIAFBAWohAQzxAQsgAEERNgIcIAAgBDYCDCAAIAFBAWo2AhRBACEQDPgBCyAQQRVGDcEBIABBADYCHCAAIAE2AhQgAEHGjICAADYCECAAQSM2AgxBACEQDPcBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQuYCAgAAiEA0AIAFBAWohAQyIAQsgAEETNgIcIAAgEDYCDCAAIAFBAWo2AhRBACEQDPYBCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQuYCAgAAiBA0AIAFBAWohAQztAQsgAEEUNgIcIAAgBDYCDCAAIAFBAWo2AhRBACEQDPUBCyAQQRVGDb0BIABBADYCHCAAIAE2AhQgAEGaj4CAADYCECAAQSI2AgxBACEQDPQBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQt4CAgAAiEA0AIAFBAWohAQyGAQsgAEEWNgIcIAAgEDYCDCAAIAFBAWo2AhRBACEQDPMBCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQt4CAgAAiBA0AIAFBAWohAQzpAQsgAEEXNgIcIAAgBDYCDCAAIAFBAWo2AhRBACEQDPIBCyAAQQA2AhwgACABNgIUIABBzZOAgAA2AhAgAEEMNgIMQQAhEAzxAQtCASERCyAQQQFqIQECQCAAKQMgIhJC//////////8PVg0AIAAgEkIEhiARhDcDICABIQEMhAELIABBADYCHCAAIAE2AhQgAEGtiYCAADYCECAAQQw2AgxBACEQDO8BCyAAQQA2AhwgACAQNgIUIABBzZOAgAA2AhAgAEEMNgIMQQAhEAzuAQsgACgCBCEXIABBADYCBCAQIBGnaiIWIQEgACAXIBAgFiAUGyIQELWAgIAAIhRFDXMgAEEFNgIcIAAgEDYCFCAAIBQ2AgxBACEQDO0BCyAAQQA2AhwgACAQNgIUIABBqpyAgAA2AhAgAEEPNgIMQQAhEAzsAQsgACAQIAIQtICAgAAiAQ0BIBAhAQtBDiEQDNEBCwJAIAFBFUcNACAAQQI2AhwgACAQNgIUIABBsJiAgAA2AhAgAEEVNgIMQQAhEAzqAQsgAEEANgIcIAAgEDYCFCAAQaeOgIAANgIQIABBEjYCDEEAIRAM6QELIAFBAWohEAJAIAAvATAiAUGAAXFFDQACQCAAIBAgAhC7gICAACIBDQAgECEBDHALIAFBFUcNugEgAEEFNgIcIAAgEDYCFCAAQfmXgIAANgIQIABBFTYCDEEAIRAM6QELAkAgAUGgBHFBoARHDQAgAC0ALUECcQ0AIABBADYCHCAAIBA2AhQgAEGWk4CAADYCECAAQQQ2AgxBACEQDOkBCyAAIBAgAhC9gICAABogECEBAkACQAJAAkACQCAAIBAgAhCzgICAAA4WAgEABAQEBAQEBAQEBAQEBAQEBAQEAwQLIABBAToALgsgACAALwEwQcAAcjsBMCAQIQELQSYhEAzRAQsgAEEjNgIcIAAgEDYCFCAAQaWWgIAANgIQIABBFTYCDEEAIRAM6QELIABBADYCHCAAIBA2AhQgAEHVi4CAADYCECAAQRE2AgxBACEQDOgBCyAALQAtQQFxRQ0BQcMBIRAMzgELAkAgDSACRg0AA0ACQCANLQAAQSBGDQAgDSEBDMQBCyANQQFqIg0gAkcNAAtBJSEQDOcBC0ElIRAM5gELIAAoAgQhBCAAQQA2AgQgACAEIA0Qr4CAgAAiBEUNrQEgAEEmNgIcIAAgBDYCDCAAIA1BAWo2AhRBACEQDOUBCyAQQRVGDasBIABBADYCHCAAIAE2AhQgAEH9jYCAADYCECAAQR02AgxBACEQDOQBCyAAQSc2AhwgACABNgIUIAAgEDYCDEEAIRAM4wELIBAhAUEBIRQCQAJAAkACQAJAAkACQCAALQAsQX5qDgcGBQUDAQIABQsgACAALwEwQQhyOwEwDAMLQQIhFAwBC0EEIRQLIABBAToALCAAIAAvATAgFHI7ATALIBAhAQtBKyEQDMoBCyAAQQA2AhwgACAQNgIUIABBq5KAgAA2AhAgAEELNgIMQQAhEAziAQsgAEEANgIcIAAgATYCFCAAQeGPgIAANgIQIABBCjYCDEEAIRAM4QELIABBADoALCAQIQEMvQELIBAhAUEBIRQCQAJAAkACQAJAIAAtACxBe2oOBAMBAgAFCyAAIAAvATBBCHI7ATAMAwtBAiEUDAELQQQhFAsgAEEBOgAsIAAgAC8BMCAUcjsBMAsgECEBC0EpIRAMxQELIABBADYCHCAAIAE2AhQgAEHwlICAADYCECAAQQM2AgxBACEQDN0BCwJAIA4tAABBDUcNACAAKAIEIQEgAEEANgIEAkAgACABIA4QsYCAgAAiAQ0AIA5BAWohAQx1CyAAQSw2AhwgACABNgIMIAAgDkEBajYCFEEAIRAM3QELIAAtAC1BAXFFDQFBxAEhEAzDAQsCQCAOIAJHDQBBLSEQDNwBCwJAAkADQAJAIA4tAABBdmoOBAIAAAMACyAOQQFqIg4gAkcNAAtBLSEQDN0BCyAAKAIEIQEgAEEANgIEAkAgACABIA4QsYCAgAAiAQ0AIA4hAQx0CyAAQSw2AhwgACAONgIUIAAgATYCDEEAIRAM3AELIAAoAgQhASAAQQA2AgQCQCAAIAEgDhCxgICAACIBDQAgDkEBaiEBDHMLIABBLDYCHCAAIAE2AgwgACAOQQFqNgIUQQAhEAzbAQsgACgCBCEEIABBADYCBCAAIAQgDhCxgICAACIEDaABIA4hAQzOAQsgEEEsRw0BIAFBAWohEEEBIQECQAJAAkACQAJAIAAtACxBe2oOBAMBAgQACyAQIQEMBAtBAiEBDAELQQQhAQsgAEEBOgAsIAAgAC8BMCABcjsBMCAQIQEMAQsgACAALwEwQQhyOwEwIBAhAQtBOSEQDL8BCyAAQQA6ACwgASEBC0E0IRAMvQELIAAgAC8BMEEgcjsBMCABIQEMAgsgACgCBCEEIABBADYCBAJAIAAgBCABELGAgIAAIgQNACABIQEMxwELIABBNzYCHCAAIAE2AhQgACAENgIMQQAhEAzUAQsgAEEIOgAsIAEhAQtBMCEQDLkBCwJAIAAtAChBAUYNACABIQEMBAsgAC0ALUEIcUUNkwEgASEBDAMLIAAtADBBIHENlAFBxQEhEAy3AQsCQCAPIAJGDQACQANAAkAgDy0AAEFQaiIBQf8BcUEKSQ0AIA8hAUE1IRAMugELIAApAyAiEUKZs+bMmbPmzBlWDQEgACARQgp+IhE3AyAgESABrUL/AYMiEkJ/hVYNASAAIBEgEnw3AyAgD0EBaiIPIAJHDQALQTkhEAzRAQsgACgCBCECIABBADYCBCAAIAIgD0EBaiIEELGAgIAAIgINlQEgBCEBDMMBC0E5IRAMzwELAkAgAC8BMCIBQQhxRQ0AIAAtAChBAUcNACAALQAtQQhxRQ2QAQsgACABQff7A3FBgARyOwEwIA8hAQtBNyEQDLQBCyAAIAAvATBBEHI7ATAMqwELIBBBFUYNiwEgAEEANgIcIAAgATYCFCAAQfCOgIAANgIQIABBHDYCDEEAIRAMywELIABBwwA2AhwgACABNgIMIAAgDUEBajYCFEEAIRAMygELAkAgAS0AAEE6Rw0AIAAoAgQhECAAQQA2AgQCQCAAIBAgARCvgICAACIQDQAgAUEBaiEBDGMLIABBwwA2AhwgACAQNgIMIAAgAUEBajYCFEEAIRAMygELIABBADYCHCAAIAE2AhQgAEGxkYCAADYCECAAQQo2AgxBACEQDMkBCyAAQQA2AhwgACABNgIUIABBoJmAgAA2AhAgAEEeNgIMQQAhEAzIAQsgAEEANgIACyAAQYASOwEqIAAgF0EBaiIBIAIQqICAgAAiEA0BIAEhAQtBxwAhEAysAQsgEEEVRw2DASAAQdEANgIcIAAgATYCFCAAQeOXgIAANgIQIABBFTYCDEEAIRAMxAELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDF4LIABB0gA2AhwgACABNgIUIAAgEDYCDEEAIRAMwwELIABBADYCHCAAIBQ2AhQgAEHBqICAADYCECAAQQc2AgwgAEEANgIAQQAhEAzCAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMXQsgAEHTADYCHCAAIAE2AhQgACAQNgIMQQAhEAzBAQtBACEQIABBADYCHCAAIAE2AhQgAEGAkYCAADYCECAAQQk2AgwMwAELIBBBFUYNfSAAQQA2AhwgACABNgIUIABBlI2AgAA2AhAgAEEhNgIMQQAhEAy/AQtBASEWQQAhF0EAIRRBASEQCyAAIBA6ACsgAUEBaiEBAkACQCAALQAtQRBxDQACQAJAAkAgAC0AKg4DAQACBAsgFkUNAwwCCyAUDQEMAgsgF0UNAQsgACgCBCEQIABBADYCBAJAIAAgECABEK2AgIAAIhANACABIQEMXAsgAEHYADYCHCAAIAE2AhQgACAQNgIMQQAhEAy+AQsgACgCBCEEIABBADYCBAJAIAAgBCABEK2AgIAAIgQNACABIQEMrQELIABB2QA2AhwgACABNgIUIAAgBDYCDEEAIRAMvQELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARCtgICAACIEDQAgASEBDKsBCyAAQdoANgIcIAAgATYCFCAAIAQ2AgxBACEQDLwBCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQrYCAgAAiBA0AIAEhAQypAQsgAEHcADYCHCAAIAE2AhQgACAENgIMQQAhEAy7AQsCQCABLQAAQVBqIhBB/wFxQQpPDQAgACAQOgAqIAFBAWohAUHPACEQDKIBCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQrYCAgAAiBA0AIAEhAQynAQsgAEHeADYCHCAAIAE2AhQgACAENgIMQQAhEAy6AQsgAEEANgIAIBdBAWohAQJAIAAtAClBI08NACABIQEMWQsgAEEANgIcIAAgATYCFCAAQdOJgIAANgIQIABBCDYCDEEAIRAMuQELIABBADYCAAtBACEQIABBADYCHCAAIAE2AhQgAEGQs4CAADYCECAAQQg2AgwMtwELIABBADYCACAXQQFqIQECQCAALQApQSFHDQAgASEBDFYLIABBADYCHCAAIAE2AhQgAEGbioCAADYCECAAQQg2AgxBACEQDLYBCyAAQQA2AgAgF0EBaiEBAkAgAC0AKSIQQV1qQQtPDQAgASEBDFULAkAgEEEGSw0AQQEgEHRBygBxRQ0AIAEhAQxVC0EAIRAgAEEANgIcIAAgATYCFCAAQfeJgIAANgIQIABBCDYCDAy1AQsgEEEVRg1xIABBADYCHCAAIAE2AhQgAEG5jYCAADYCECAAQRo2AgxBACEQDLQBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxUCyAAQeUANgIcIAAgATYCFCAAIBA2AgxBACEQDLMBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxNCyAAQdIANgIcIAAgATYCFCAAIBA2AgxBACEQDLIBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxNCyAAQdMANgIcIAAgATYCFCAAIBA2AgxBACEQDLEBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxRCyAAQeUANgIcIAAgATYCFCAAIBA2AgxBACEQDLABCyAAQQA2AhwgACABNgIUIABBxoqAgAA2AhAgAEEHNgIMQQAhEAyvAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMSQsgAEHSADYCHCAAIAE2AhQgACAQNgIMQQAhEAyuAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMSQsgAEHTADYCHCAAIAE2AhQgACAQNgIMQQAhEAytAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMTQsgAEHlADYCHCAAIAE2AhQgACAQNgIMQQAhEAysAQsgAEEANgIcIAAgATYCFCAAQdyIgIAANgIQIABBBzYCDEEAIRAMqwELIBBBP0cNASABQQFqIQELQQUhEAyQAQtBACEQIABBADYCHCAAIAE2AhQgAEH9koCAADYCECAAQQc2AgwMqAELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDEILIABB0gA2AhwgACABNgIUIAAgEDYCDEEAIRAMpwELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDEILIABB0wA2AhwgACABNgIUIAAgEDYCDEEAIRAMpgELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDEYLIABB5QA2AhwgACABNgIUIAAgEDYCDEEAIRAMpQELIAAoAgQhASAAQQA2AgQCQCAAIAEgFBCngICAACIBDQAgFCEBDD8LIABB0gA2AhwgACAUNgIUIAAgATYCDEEAIRAMpAELIAAoAgQhASAAQQA2AgQCQCAAIAEgFBCngICAACIBDQAgFCEBDD8LIABB0wA2AhwgACAUNgIUIAAgATYCDEEAIRAMowELIAAoAgQhASAAQQA2AgQCQCAAIAEgFBCngICAACIBDQAgFCEBDEMLIABB5QA2AhwgACAUNgIUIAAgATYCDEEAIRAMogELIABBADYCHCAAIBQ2AhQgAEHDj4CAADYCECAAQQc2AgxBACEQDKEBCyAAQQA2AhwgACABNgIUIABBw4+AgAA2AhAgAEEHNgIMQQAhEAygAQtBACEQIABBADYCHCAAIBQ2AhQgAEGMnICAADYCECAAQQc2AgwMnwELIABBADYCHCAAIBQ2AhQgAEGMnICAADYCECAAQQc2AgxBACEQDJ4BCyAAQQA2AhwgACAUNgIUIABB/pGAgAA2AhAgAEEHNgIMQQAhEAydAQsgAEEANgIcIAAgATYCFCAAQY6bgIAANgIQIABBBjYCDEEAIRAMnAELIBBBFUYNVyAAQQA2AhwgACABNgIUIABBzI6AgAA2AhAgAEEgNgIMQQAhEAybAQsgAEEANgIAIBBBAWohAUEkIRALIAAgEDoAKSAAKAIEIRAgAEEANgIEIAAgECABEKuAgIAAIhANVCABIQEMPgsgAEEANgIAC0EAIRAgAEEANgIcIAAgBDYCFCAAQfGbgIAANgIQIABBBjYCDAyXAQsgAUEVRg1QIABBADYCHCAAIAU2AhQgAEHwjICAADYCECAAQRs2AgxBACEQDJYBCyAAKAIEIQUgAEEANgIEIAAgBSAQEKmAgIAAIgUNASAQQQFqIQULQa0BIRAMewsgAEHBATYCHCAAIAU2AgwgACAQQQFqNgIUQQAhEAyTAQsgACgCBCEGIABBADYCBCAAIAYgEBCpgICAACIGDQEgEEEBaiEGC0GuASEQDHgLIABBwgE2AhwgACAGNgIMIAAgEEEBajYCFEEAIRAMkAELIABBADYCHCAAIAc2AhQgAEGXi4CAADYCECAAQQ02AgxBACEQDI8BCyAAQQA2AhwgACAINgIUIABB45CAgAA2AhAgAEEJNgIMQQAhEAyOAQsgAEEANgIcIAAgCDYCFCAAQZSNgIAANgIQIABBITYCDEEAIRAMjQELQQEhFkEAIRdBACEUQQEhEAsgACAQOgArIAlBAWohCAJAAkAgAC0ALUEQcQ0AAkACQAJAIAAtACoOAwEAAgQLIBZFDQMMAgsgFA0BDAILIBdFDQELIAAoAgQhECAAQQA2AgQgACAQIAgQrYCAgAAiEEUNPSAAQckBNgIcIAAgCDYCFCAAIBA2AgxBACEQDIwBCyAAKAIEIQQgAEEANgIEIAAgBCAIEK2AgIAAIgRFDXYgAEHKATYCHCAAIAg2AhQgACAENgIMQQAhEAyLAQsgACgCBCEEIABBADYCBCAAIAQgCRCtgICAACIERQ10IABBywE2AhwgACAJNgIUIAAgBDYCDEEAIRAMigELIAAoAgQhBCAAQQA2AgQgACAEIAoQrYCAgAAiBEUNciAAQc0BNgIcIAAgCjYCFCAAIAQ2AgxBACEQDIkBCwJAIAstAABBUGoiEEH/AXFBCk8NACAAIBA6ACogC0EBaiEKQbYBIRAMcAsgACgCBCEEIABBADYCBCAAIAQgCxCtgICAACIERQ1wIABBzwE2AhwgACALNgIUIAAgBDYCDEEAIRAMiAELIABBADYCHCAAIAQ2AhQgAEGQs4CAADYCECAAQQg2AgwgAEEANgIAQQAhEAyHAQsgAUEVRg0/IABBADYCHCAAIAw2AhQgAEHMjoCAADYCECAAQSA2AgxBACEQDIYBCyAAQYEEOwEoIAAoAgQhECAAQgA3AwAgACAQIAxBAWoiDBCrgICAACIQRQ04IABB0wE2AhwgACAMNgIUIAAgEDYCDEEAIRAMhQELIABBADYCAAtBACEQIABBADYCHCAAIAQ2AhQgAEHYm4CAADYCECAAQQg2AgwMgwELIAAoAgQhECAAQgA3AwAgACAQIAtBAWoiCxCrgICAACIQDQFBxgEhEAxpCyAAQQI6ACgMVQsgAEHVATYCHCAAIAs2AhQgACAQNgIMQQAhEAyAAQsgEEEVRg03IABBADYCHCAAIAQ2AhQgAEGkjICAADYCECAAQRA2AgxBACEQDH8LIAAtADRBAUcNNCAAIAQgAhC8gICAACIQRQ00IBBBFUcNNSAAQdwBNgIcIAAgBDYCFCAAQdWWgIAANgIQIABBFTYCDEEAIRAMfgtBACEQIABBADYCHCAAQa+LgIAANgIQIABBAjYCDCAAIBRBAWo2AhQMfQtBACEQDGMLQQIhEAxiC0ENIRAMYQtBDyEQDGALQSUhEAxfC0ETIRAMXgtBFSEQDF0LQRYhEAxcC0EXIRAMWwtBGCEQDFoLQRkhEAxZC0EaIRAMWAtBGyEQDFcLQRwhEAxWC0EdIRAMVQtBHyEQDFQLQSEhEAxTC0EjIRAMUgtBxgAhEAxRC0EuIRAMUAtBLyEQDE8LQTshEAxOC0E9IRAMTQtByAAhEAxMC0HJACEQDEsLQcsAIRAMSgtBzAAhEAxJC0HOACEQDEgLQdEAIRAMRwtB1QAhEAxGC0HYACEQDEULQdkAIRAMRAtB2wAhEAxDC0HkACEQDEILQeUAIRAMQQtB8QAhEAxAC0H0ACEQDD8LQY0BIRAMPgtBlwEhEAw9C0GpASEQDDwLQawBIRAMOwtBwAEhEAw6C0G5ASEQDDkLQa8BIRAMOAtBsQEhEAw3C0GyASEQDDYLQbQBIRAMNQtBtQEhEAw0C0G6ASEQDDMLQb0BIRAMMgtBvwEhEAwxC0HBASEQDDALIABBADYCHCAAIAQ2AhQgAEHpi4CAADYCECAAQR82AgxBACEQDEgLIABB2wE2AhwgACAENgIUIABB+paAgAA2AhAgAEEVNgIMQQAhEAxHCyAAQfgANgIcIAAgDDYCFCAAQcqYgIAANgIQIABBFTYCDEEAIRAMRgsgAEHRADYCHCAAIAU2AhQgAEGwl4CAADYCECAAQRU2AgxBACEQDEULIABB+QA2AhwgACABNgIUIAAgEDYCDEEAIRAMRAsgAEH4ADYCHCAAIAE2AhQgAEHKmICAADYCECAAQRU2AgxBACEQDEMLIABB5AA2AhwgACABNgIUIABB45eAgAA2AhAgAEEVNgIMQQAhEAxCCyAAQdcANgIcIAAgATYCFCAAQcmXgIAANgIQIABBFTYCDEEAIRAMQQsgAEEANgIcIAAgATYCFCAAQbmNgIAANgIQIABBGjYCDEEAIRAMQAsgAEHCADYCHCAAIAE2AhQgAEHjmICAADYCECAAQRU2AgxBACEQDD8LIABBADYCBCAAIA8gDxCxgICAACIERQ0BIABBOjYCHCAAIAQ2AgwgACAPQQFqNgIUQQAhEAw+CyAAKAIEIQQgAEEANgIEAkAgACAEIAEQsYCAgAAiBEUNACAAQTs2AhwgACAENgIMIAAgAUEBajYCFEEAIRAMPgsgAUEBaiEBDC0LIA9BAWohAQwtCyAAQQA2AhwgACAPNgIUIABB5JKAgAA2AhAgAEEENgIMQQAhEAw7CyAAQTY2AhwgACAENgIUIAAgAjYCDEEAIRAMOgsgAEEuNgIcIAAgDjYCFCAAIAQ2AgxBACEQDDkLIABB0AA2AhwgACABNgIUIABBkZiAgAA2AhAgAEEVNgIMQQAhEAw4CyANQQFqIQEMLAsgAEEVNgIcIAAgATYCFCAAQYKZgIAANgIQIABBFTYCDEEAIRAMNgsgAEEbNgIcIAAgATYCFCAAQZGXgIAANgIQIABBFTYCDEEAIRAMNQsgAEEPNgIcIAAgATYCFCAAQZGXgIAANgIQIABBFTYCDEEAIRAMNAsgAEELNgIcIAAgATYCFCAAQZGXgIAANgIQIABBFTYCDEEAIRAMMwsgAEEaNgIcIAAgATYCFCAAQYKZgIAANgIQIABBFTYCDEEAIRAMMgsgAEELNgIcIAAgATYCFCAAQYKZgIAANgIQIABBFTYCDEEAIRAMMQsgAEEKNgIcIAAgATYCFCAAQeSWgIAANgIQIABBFTYCDEEAIRAMMAsgAEEeNgIcIAAgATYCFCAAQfmXgIAANgIQIABBFTYCDEEAIRAMLwsgAEEANgIcIAAgEDYCFCAAQdqNgIAANgIQIABBFDYCDEEAIRAMLgsgAEEENgIcIAAgATYCFCAAQbCYgIAANgIQIABBFTYCDEEAIRAMLQsgAEEANgIAIAtBAWohCwtBuAEhEAwSCyAAQQA2AgAgEEEBaiEBQfUAIRAMEQsgASEBAkAgAC0AKUEFRw0AQeMAIRAMEQtB4gAhEAwQC0EAIRAgAEEANgIcIABB5JGAgAA2AhAgAEEHNgIMIAAgFEEBajYCFAwoCyAAQQA2AgAgF0EBaiEBQcAAIRAMDgtBASEBCyAAIAE6ACwgAEEANgIAIBdBAWohAQtBKCEQDAsLIAEhAQtBOCEQDAkLAkAgASIPIAJGDQADQAJAIA8tAABBgL6AgABqLQAAIgFBAUYNACABQQJHDQMgD0EBaiEBDAQLIA9BAWoiDyACRw0AC0E+IRAMIgtBPiEQDCELIABBADoALCAPIQEMAQtBCyEQDAYLQTohEAwFCyABQQFqIQFBLSEQDAQLIAAgAToALCAAQQA2AgAgFkEBaiEBQQwhEAwDCyAAQQA2AgAgF0EBaiEBQQohEAwCCyAAQQA2AgALIABBADoALCANIQFBCSEQDAALC0EAIRAgAEEANgIcIAAgCzYCFCAAQc2QgIAANgIQIABBCTYCDAwXC0EAIRAgAEEANgIcIAAgCjYCFCAAQemKgIAANgIQIABBCTYCDAwWC0EAIRAgAEEANgIcIAAgCTYCFCAAQbeQgIAANgIQIABBCTYCDAwVC0EAIRAgAEEANgIcIAAgCDYCFCAAQZyRgIAANgIQIABBCTYCDAwUC0EAIRAgAEEANgIcIAAgATYCFCAAQc2QgIAANgIQIABBCTYCDAwTC0EAIRAgAEEANgIcIAAgATYCFCAAQemKgIAANgIQIABBCTYCDAwSC0EAIRAgAEEANgIcIAAgATYCFCAAQbeQgIAANgIQIABBCTYCDAwRC0EAIRAgAEEANgIcIAAgATYCFCAAQZyRgIAANgIQIABBCTYCDAwQC0EAIRAgAEEANgIcIAAgATYCFCAAQZeVgIAANgIQIABBDzYCDAwPC0EAIRAgAEEANgIcIAAgATYCFCAAQZeVgIAANgIQIABBDzYCDAwOC0EAIRAgAEEANgIcIAAgATYCFCAAQcCSgIAANgIQIABBCzYCDAwNC0EAIRAgAEEANgIcIAAgATYCFCAAQZWJgIAANgIQIABBCzYCDAwMC0EAIRAgAEEANgIcIAAgATYCFCAAQeGPgIAANgIQIABBCjYCDAwLC0EAIRAgAEEANgIcIAAgATYCFCAAQfuPgIAANgIQIABBCjYCDAwKC0EAIRAgAEEANgIcIAAgATYCFCAAQfGZgIAANgIQIABBAjYCDAwJC0EAIRAgAEEANgIcIAAgATYCFCAAQcSUgIAANgIQIABBAjYCDAwIC0EAIRAgAEEANgIcIAAgATYCFCAAQfKVgIAANgIQIABBAjYCDAwHCyAAQQI2AhwgACABNgIUIABBnJqAgAA2AhAgAEEWNgIMQQAhEAwGC0EBIRAMBQtB1AAhECABIgQgAkYNBCADQQhqIAAgBCACQdjCgIAAQQoQxYCAgAAgAygCDCEEIAMoAggOAwEEAgALEMqAgIAAAAsgAEEANgIcIABBtZqAgAA2AhAgAEEXNgIMIAAgBEEBajYCFEEAIRAMAgsgAEEANgIcIAAgBDYCFCAAQcqagIAANgIQIABBCTYCDEEAIRAMAQsCQCABIgQgAkcNAEEiIRAMAQsgAEGJgICAADYCCCAAIAQ2AgRBISEQCyADQRBqJICAgIAAIBALrwEBAn8gASgCACEGAkACQCACIANGDQAgBCAGaiEEIAYgA2ogAmshByACIAZBf3MgBWoiBmohBQNAAkAgAi0AACAELQAARg0AQQIhBAwDCwJAIAYNAEEAIQQgBSECDAMLIAZBf2ohBiAEQQFqIQQgAkEBaiICIANHDQALIAchBiADIQILIABBATYCACABIAY2AgAgACACNgIEDwsgAUEANgIAIAAgBDYCACAAIAI2AgQLCgAgABDHgICAAAvyNgELfyOAgICAAEEQayIBJICAgIAAAkBBACgCoNCAgAANAEEAEMuAgIAAQYDUhIAAayICQdkASQ0AQQAhAwJAQQAoAuDTgIAAIgQNAEEAQn83AuzTgIAAQQBCgICEgICAwAA3AuTTgIAAQQAgAUEIakFwcUHYqtWqBXMiBDYC4NOAgABBAEEANgL004CAAEEAQQA2AsTTgIAAC0EAIAI2AszTgIAAQQBBgNSEgAA2AsjTgIAAQQBBgNSEgAA2ApjQgIAAQQAgBDYCrNCAgABBAEF/NgKo0ICAAANAIANBxNCAgABqIANBuNCAgABqIgQ2AgAgBCADQbDQgIAAaiIFNgIAIANBvNCAgABqIAU2AgAgA0HM0ICAAGogA0HA0ICAAGoiBTYCACAFIAQ2AgAgA0HU0ICAAGogA0HI0ICAAGoiBDYCACAEIAU2AgAgA0HQ0ICAAGogBDYCACADQSBqIgNBgAJHDQALQYDUhIAAQXhBgNSEgABrQQ9xQQBBgNSEgABBCGpBD3EbIgNqIgRBBGogAkFIaiIFIANrIgNBAXI2AgBBAEEAKALw04CAADYCpNCAgABBACADNgKU0ICAAEEAIAQ2AqDQgIAAQYDUhIAAIAVqQTg2AgQLAkACQAJAAkACQAJAAkACQAJAAkACQAJAIABB7AFLDQACQEEAKAKI0ICAACIGQRAgAEETakFwcSAAQQtJGyICQQN2IgR2IgNBA3FFDQACQAJAIANBAXEgBHJBAXMiBUEDdCIEQbDQgIAAaiIDIARBuNCAgABqKAIAIgQoAggiAkcNAEEAIAZBfiAFd3E2AojQgIAADAELIAMgAjYCCCACIAM2AgwLIARBCGohAyAEIAVBA3QiBUEDcjYCBCAEIAVqIgQgBCgCBEEBcjYCBAwMCyACQQAoApDQgIAAIgdNDQECQCADRQ0AAkACQCADIAR0QQIgBHQiA0EAIANrcnEiA0EAIANrcUF/aiIDIANBDHZBEHEiA3YiBEEFdkEIcSIFIANyIAQgBXYiA0ECdkEEcSIEciADIAR2IgNBAXZBAnEiBHIgAyAEdiIDQQF2QQFxIgRyIAMgBHZqIgRBA3QiA0Gw0ICAAGoiBSADQbjQgIAAaigCACIDKAIIIgBHDQBBACAGQX4gBHdxIgY2AojQgIAADAELIAUgADYCCCAAIAU2AgwLIAMgAkEDcjYCBCADIARBA3QiBGogBCACayIFNgIAIAMgAmoiACAFQQFyNgIEAkAgB0UNACAHQXhxQbDQgIAAaiECQQAoApzQgIAAIQQCQAJAIAZBASAHQQN2dCIIcQ0AQQAgBiAIcjYCiNCAgAAgAiEIDAELIAIoAgghCAsgCCAENgIMIAIgBDYCCCAEIAI2AgwgBCAINgIICyADQQhqIQNBACAANgKc0ICAAEEAIAU2ApDQgIAADAwLQQAoAozQgIAAIglFDQEgCUEAIAlrcUF/aiIDIANBDHZBEHEiA3YiBEEFdkEIcSIFIANyIAQgBXYiA0ECdkEEcSIEciADIAR2IgNBAXZBAnEiBHIgAyAEdiIDQQF2QQFxIgRyIAMgBHZqQQJ0QbjSgIAAaigCACIAKAIEQXhxIAJrIQQgACEFAkADQAJAIAUoAhAiAw0AIAVBFGooAgAiA0UNAgsgAygCBEF4cSACayIFIAQgBSAESSIFGyEEIAMgACAFGyEAIAMhBQwACwsgACgCGCEKAkAgACgCDCIIIABGDQAgACgCCCIDQQAoApjQgIAASRogCCADNgIIIAMgCDYCDAwLCwJAIABBFGoiBSgCACIDDQAgACgCECIDRQ0DIABBEGohBQsDQCAFIQsgAyIIQRRqIgUoAgAiAw0AIAhBEGohBSAIKAIQIgMNAAsgC0EANgIADAoLQX8hAiAAQb9/Sw0AIABBE2oiA0FwcSECQQAoAozQgIAAIgdFDQBBACELAkAgAkGAAkkNAEEfIQsgAkH///8HSw0AIANBCHYiAyADQYD+P2pBEHZBCHEiA3QiBCAEQYDgH2pBEHZBBHEiBHQiBSAFQYCAD2pBEHZBAnEiBXRBD3YgAyAEciAFcmsiA0EBdCACIANBFWp2QQFxckEcaiELC0EAIAJrIQQCQAJAAkACQCALQQJ0QbjSgIAAaigCACIFDQBBACEDQQAhCAwBC0EAIQMgAkEAQRkgC0EBdmsgC0EfRht0IQBBACEIA0ACQCAFKAIEQXhxIAJrIgYgBE8NACAGIQQgBSEIIAYNAEEAIQQgBSEIIAUhAwwDCyADIAVBFGooAgAiBiAGIAUgAEEddkEEcWpBEGooAgAiBUYbIAMgBhshAyAAQQF0IQAgBQ0ACwsCQCADIAhyDQBBACEIQQIgC3QiA0EAIANrciAHcSIDRQ0DIANBACADa3FBf2oiAyADQQx2QRBxIgN2IgVBBXZBCHEiACADciAFIAB2IgNBAnZBBHEiBXIgAyAFdiIDQQF2QQJxIgVyIAMgBXYiA0EBdkEBcSIFciADIAV2akECdEG40oCAAGooAgAhAwsgA0UNAQsDQCADKAIEQXhxIAJrIgYgBEkhAAJAIAMoAhAiBQ0AIANBFGooAgAhBQsgBiAEIAAbIQQgAyAIIAAbIQggBSEDIAUNAAsLIAhFDQAgBEEAKAKQ0ICAACACa08NACAIKAIYIQsCQCAIKAIMIgAgCEYNACAIKAIIIgNBACgCmNCAgABJGiAAIAM2AgggAyAANgIMDAkLAkAgCEEUaiIFKAIAIgMNACAIKAIQIgNFDQMgCEEQaiEFCwNAIAUhBiADIgBBFGoiBSgCACIDDQAgAEEQaiEFIAAoAhAiAw0ACyAGQQA2AgAMCAsCQEEAKAKQ0ICAACIDIAJJDQBBACgCnNCAgAAhBAJAAkAgAyACayIFQRBJDQAgBCACaiIAIAVBAXI2AgRBACAFNgKQ0ICAAEEAIAA2ApzQgIAAIAQgA2ogBTYCACAEIAJBA3I2AgQMAQsgBCADQQNyNgIEIAQgA2oiAyADKAIEQQFyNgIEQQBBADYCnNCAgABBAEEANgKQ0ICAAAsgBEEIaiEDDAoLAkBBACgClNCAgAAiACACTQ0AQQAoAqDQgIAAIgMgAmoiBCAAIAJrIgVBAXI2AgRBACAFNgKU0ICAAEEAIAQ2AqDQgIAAIAMgAkEDcjYCBCADQQhqIQMMCgsCQAJAQQAoAuDTgIAARQ0AQQAoAujTgIAAIQQMAQtBAEJ/NwLs04CAAEEAQoCAhICAgMAANwLk04CAAEEAIAFBDGpBcHFB2KrVqgVzNgLg04CAAEEAQQA2AvTTgIAAQQBBADYCxNOAgABBgIAEIQQLQQAhAwJAIAQgAkHHAGoiB2oiBkEAIARrIgtxIgggAksNAEEAQTA2AvjTgIAADAoLAkBBACgCwNOAgAAiA0UNAAJAQQAoArjTgIAAIgQgCGoiBSAETQ0AIAUgA00NAQtBACEDQQBBMDYC+NOAgAAMCgtBAC0AxNOAgABBBHENBAJAAkACQEEAKAKg0ICAACIERQ0AQcjTgIAAIQMDQAJAIAMoAgAiBSAESw0AIAUgAygCBGogBEsNAwsgAygCCCIDDQALC0EAEMuAgIAAIgBBf0YNBSAIIQYCQEEAKALk04CAACIDQX9qIgQgAHFFDQAgCCAAayAEIABqQQAgA2txaiEGCyAGIAJNDQUgBkH+////B0sNBQJAQQAoAsDTgIAAIgNFDQBBACgCuNOAgAAiBCAGaiIFIARNDQYgBSADSw0GCyAGEMuAgIAAIgMgAEcNAQwHCyAGIABrIAtxIgZB/v///wdLDQQgBhDLgICAACIAIAMoAgAgAygCBGpGDQMgACEDCwJAIANBf0YNACACQcgAaiAGTQ0AAkAgByAGa0EAKALo04CAACIEakEAIARrcSIEQf7///8HTQ0AIAMhAAwHCwJAIAQQy4CAgABBf0YNACAEIAZqIQYgAyEADAcLQQAgBmsQy4CAgAAaDAQLIAMhACADQX9HDQUMAwtBACEIDAcLQQAhAAwFCyAAQX9HDQILQQBBACgCxNOAgABBBHI2AsTTgIAACyAIQf7///8HSw0BIAgQy4CAgAAhAEEAEMuAgIAAIQMgAEF/Rg0BIANBf0YNASAAIANPDQEgAyAAayIGIAJBOGpNDQELQQBBACgCuNOAgAAgBmoiAzYCuNOAgAACQCADQQAoArzTgIAATQ0AQQAgAzYCvNOAgAALAkACQAJAAkBBACgCoNCAgAAiBEUNAEHI04CAACEDA0AgACADKAIAIgUgAygCBCIIakYNAiADKAIIIgMNAAwDCwsCQAJAQQAoApjQgIAAIgNFDQAgACADTw0BC0EAIAA2ApjQgIAAC0EAIQNBACAGNgLM04CAAEEAIAA2AsjTgIAAQQBBfzYCqNCAgABBAEEAKALg04CAADYCrNCAgABBAEEANgLU04CAAANAIANBxNCAgABqIANBuNCAgABqIgQ2AgAgBCADQbDQgIAAaiIFNgIAIANBvNCAgABqIAU2AgAgA0HM0ICAAGogA0HA0ICAAGoiBTYCACAFIAQ2AgAgA0HU0ICAAGogA0HI0ICAAGoiBDYCACAEIAU2AgAgA0HQ0ICAAGogBDYCACADQSBqIgNBgAJHDQALIABBeCAAa0EPcUEAIABBCGpBD3EbIgNqIgQgBkFIaiIFIANrIgNBAXI2AgRBAEEAKALw04CAADYCpNCAgABBACADNgKU0ICAAEEAIAQ2AqDQgIAAIAAgBWpBODYCBAwCCyADLQAMQQhxDQAgBCAFSQ0AIAQgAE8NACAEQXggBGtBD3FBACAEQQhqQQ9xGyIFaiIAQQAoApTQgIAAIAZqIgsgBWsiBUEBcjYCBCADIAggBmo2AgRBAEEAKALw04CAADYCpNCAgABBACAFNgKU0ICAAEEAIAA2AqDQgIAAIAQgC2pBODYCBAwBCwJAIABBACgCmNCAgAAiCE8NAEEAIAA2ApjQgIAAIAAhCAsgACAGaiEFQcjTgIAAIQMCQAJAAkACQAJAAkACQANAIAMoAgAgBUYNASADKAIIIgMNAAwCCwsgAy0ADEEIcUUNAQtByNOAgAAhAwNAAkAgAygCACIFIARLDQAgBSADKAIEaiIFIARLDQMLIAMoAgghAwwACwsgAyAANgIAIAMgAygCBCAGajYCBCAAQXggAGtBD3FBACAAQQhqQQ9xG2oiCyACQQNyNgIEIAVBeCAFa0EPcUEAIAVBCGpBD3EbaiIGIAsgAmoiAmshAwJAIAYgBEcNAEEAIAI2AqDQgIAAQQBBACgClNCAgAAgA2oiAzYClNCAgAAgAiADQQFyNgIEDAMLAkAgBkEAKAKc0ICAAEcNAEEAIAI2ApzQgIAAQQBBACgCkNCAgAAgA2oiAzYCkNCAgAAgAiADQQFyNgIEIAIgA2ogAzYCAAwDCwJAIAYoAgQiBEEDcUEBRw0AIARBeHEhBwJAAkAgBEH/AUsNACAGKAIIIgUgBEEDdiIIQQN0QbDQgIAAaiIARhoCQCAGKAIMIgQgBUcNAEEAQQAoAojQgIAAQX4gCHdxNgKI0ICAAAwCCyAEIABGGiAEIAU2AgggBSAENgIMDAELIAYoAhghCQJAAkAgBigCDCIAIAZGDQAgBigCCCIEIAhJGiAAIAQ2AgggBCAANgIMDAELAkAgBkEUaiIEKAIAIgUNACAGQRBqIgQoAgAiBQ0AQQAhAAwBCwNAIAQhCCAFIgBBFGoiBCgCACIFDQAgAEEQaiEEIAAoAhAiBQ0ACyAIQQA2AgALIAlFDQACQAJAIAYgBigCHCIFQQJ0QbjSgIAAaiIEKAIARw0AIAQgADYCACAADQFBAEEAKAKM0ICAAEF+IAV3cTYCjNCAgAAMAgsgCUEQQRQgCSgCECAGRhtqIAA2AgAgAEUNAQsgACAJNgIYAkAgBigCECIERQ0AIAAgBDYCECAEIAA2AhgLIAYoAhQiBEUNACAAQRRqIAQ2AgAgBCAANgIYCyAHIANqIQMgBiAHaiIGKAIEIQQLIAYgBEF+cTYCBCACIANqIAM2AgAgAiADQQFyNgIEAkAgA0H/AUsNACADQXhxQbDQgIAAaiEEAkACQEEAKAKI0ICAACIFQQEgA0EDdnQiA3ENAEEAIAUgA3I2AojQgIAAIAQhAwwBCyAEKAIIIQMLIAMgAjYCDCAEIAI2AgggAiAENgIMIAIgAzYCCAwDC0EfIQQCQCADQf///wdLDQAgA0EIdiIEIARBgP4/akEQdkEIcSIEdCIFIAVBgOAfakEQdkEEcSIFdCIAIABBgIAPakEQdkECcSIAdEEPdiAEIAVyIAByayIEQQF0IAMgBEEVanZBAXFyQRxqIQQLIAIgBDYCHCACQgA3AhAgBEECdEG40oCAAGohBQJAQQAoAozQgIAAIgBBASAEdCIIcQ0AIAUgAjYCAEEAIAAgCHI2AozQgIAAIAIgBTYCGCACIAI2AgggAiACNgIMDAMLIANBAEEZIARBAXZrIARBH0YbdCEEIAUoAgAhAANAIAAiBSgCBEF4cSADRg0CIARBHXYhACAEQQF0IQQgBSAAQQRxakEQaiIIKAIAIgANAAsgCCACNgIAIAIgBTYCGCACIAI2AgwgAiACNgIIDAILIABBeCAAa0EPcUEAIABBCGpBD3EbIgNqIgsgBkFIaiIIIANrIgNBAXI2AgQgACAIakE4NgIEIAQgBUE3IAVrQQ9xQQAgBUFJakEPcRtqQUFqIgggCCAEQRBqSRsiCEEjNgIEQQBBACgC8NOAgAA2AqTQgIAAQQAgAzYClNCAgABBACALNgKg0ICAACAIQRBqQQApAtDTgIAANwIAIAhBACkCyNOAgAA3AghBACAIQQhqNgLQ04CAAEEAIAY2AszTgIAAQQAgADYCyNOAgABBAEEANgLU04CAACAIQSRqIQMDQCADQQc2AgAgA0EEaiIDIAVJDQALIAggBEYNAyAIIAgoAgRBfnE2AgQgCCAIIARrIgA2AgAgBCAAQQFyNgIEAkAgAEH/AUsNACAAQXhxQbDQgIAAaiEDAkACQEEAKAKI0ICAACIFQQEgAEEDdnQiAHENAEEAIAUgAHI2AojQgIAAIAMhBQwBCyADKAIIIQULIAUgBDYCDCADIAQ2AgggBCADNgIMIAQgBTYCCAwEC0EfIQMCQCAAQf///wdLDQAgAEEIdiIDIANBgP4/akEQdkEIcSIDdCIFIAVBgOAfakEQdkEEcSIFdCIIIAhBgIAPakEQdkECcSIIdEEPdiADIAVyIAhyayIDQQF0IAAgA0EVanZBAXFyQRxqIQMLIAQgAzYCHCAEQgA3AhAgA0ECdEG40oCAAGohBQJAQQAoAozQgIAAIghBASADdCIGcQ0AIAUgBDYCAEEAIAggBnI2AozQgIAAIAQgBTYCGCAEIAQ2AgggBCAENgIMDAQLIABBAEEZIANBAXZrIANBH0YbdCEDIAUoAgAhCANAIAgiBSgCBEF4cSAARg0DIANBHXYhCCADQQF0IQMgBSAIQQRxakEQaiIGKAIAIggNAAsgBiAENgIAIAQgBTYCGCAEIAQ2AgwgBCAENgIIDAMLIAUoAggiAyACNgIMIAUgAjYCCCACQQA2AhggAiAFNgIMIAIgAzYCCAsgC0EIaiEDDAULIAUoAggiAyAENgIMIAUgBDYCCCAEQQA2AhggBCAFNgIMIAQgAzYCCAtBACgClNCAgAAiAyACTQ0AQQAoAqDQgIAAIgQgAmoiBSADIAJrIgNBAXI2AgRBACADNgKU0ICAAEEAIAU2AqDQgIAAIAQgAkEDcjYCBCAEQQhqIQMMAwtBACEDQQBBMDYC+NOAgAAMAgsCQCALRQ0AAkACQCAIIAgoAhwiBUECdEG40oCAAGoiAygCAEcNACADIAA2AgAgAA0BQQAgB0F+IAV3cSIHNgKM0ICAAAwCCyALQRBBFCALKAIQIAhGG2ogADYCACAARQ0BCyAAIAs2AhgCQCAIKAIQIgNFDQAgACADNgIQIAMgADYCGAsgCEEUaigCACIDRQ0AIABBFGogAzYCACADIAA2AhgLAkACQCAEQQ9LDQAgCCAEIAJqIgNBA3I2AgQgCCADaiIDIAMoAgRBAXI2AgQMAQsgCCACaiIAIARBAXI2AgQgCCACQQNyNgIEIAAgBGogBDYCAAJAIARB/wFLDQAgBEF4cUGw0ICAAGohAwJAAkBBACgCiNCAgAAiBUEBIARBA3Z0IgRxDQBBACAFIARyNgKI0ICAACADIQQMAQsgAygCCCEECyAEIAA2AgwgAyAANgIIIAAgAzYCDCAAIAQ2AggMAQtBHyEDAkAgBEH///8HSw0AIARBCHYiAyADQYD+P2pBEHZBCHEiA3QiBSAFQYDgH2pBEHZBBHEiBXQiAiACQYCAD2pBEHZBAnEiAnRBD3YgAyAFciACcmsiA0EBdCAEIANBFWp2QQFxckEcaiEDCyAAIAM2AhwgAEIANwIQIANBAnRBuNKAgABqIQUCQCAHQQEgA3QiAnENACAFIAA2AgBBACAHIAJyNgKM0ICAACAAIAU2AhggACAANgIIIAAgADYCDAwBCyAEQQBBGSADQQF2ayADQR9GG3QhAyAFKAIAIQICQANAIAIiBSgCBEF4cSAERg0BIANBHXYhAiADQQF0IQMgBSACQQRxakEQaiIGKAIAIgINAAsgBiAANgIAIAAgBTYCGCAAIAA2AgwgACAANgIIDAELIAUoAggiAyAANgIMIAUgADYCCCAAQQA2AhggACAFNgIMIAAgAzYCCAsgCEEIaiEDDAELAkAgCkUNAAJAAkAgACAAKAIcIgVBAnRBuNKAgABqIgMoAgBHDQAgAyAINgIAIAgNAUEAIAlBfiAFd3E2AozQgIAADAILIApBEEEUIAooAhAgAEYbaiAINgIAIAhFDQELIAggCjYCGAJAIAAoAhAiA0UNACAIIAM2AhAgAyAINgIYCyAAQRRqKAIAIgNFDQAgCEEUaiADNgIAIAMgCDYCGAsCQAJAIARBD0sNACAAIAQgAmoiA0EDcjYCBCAAIANqIgMgAygCBEEBcjYCBAwBCyAAIAJqIgUgBEEBcjYCBCAAIAJBA3I2AgQgBSAEaiAENgIAAkAgB0UNACAHQXhxQbDQgIAAaiECQQAoApzQgIAAIQMCQAJAQQEgB0EDdnQiCCAGcQ0AQQAgCCAGcjYCiNCAgAAgAiEIDAELIAIoAgghCAsgCCADNgIMIAIgAzYCCCADIAI2AgwgAyAINgIIC0EAIAU2ApzQgIAAQQAgBDYCkNCAgAALIABBCGohAwsgAUEQaiSAgICAACADCwoAIAAQyYCAgAAL4g0BB38CQCAARQ0AIABBeGoiASAAQXxqKAIAIgJBeHEiAGohAwJAIAJBAXENACACQQNxRQ0BIAEgASgCACICayIBQQAoApjQgIAAIgRJDQEgAiAAaiEAAkAgAUEAKAKc0ICAAEYNAAJAIAJB/wFLDQAgASgCCCIEIAJBA3YiBUEDdEGw0ICAAGoiBkYaAkAgASgCDCICIARHDQBBAEEAKAKI0ICAAEF+IAV3cTYCiNCAgAAMAwsgAiAGRhogAiAENgIIIAQgAjYCDAwCCyABKAIYIQcCQAJAIAEoAgwiBiABRg0AIAEoAggiAiAESRogBiACNgIIIAIgBjYCDAwBCwJAIAFBFGoiAigCACIEDQAgAUEQaiICKAIAIgQNAEEAIQYMAQsDQCACIQUgBCIGQRRqIgIoAgAiBA0AIAZBEGohAiAGKAIQIgQNAAsgBUEANgIACyAHRQ0BAkACQCABIAEoAhwiBEECdEG40oCAAGoiAigCAEcNACACIAY2AgAgBg0BQQBBACgCjNCAgABBfiAEd3E2AozQgIAADAMLIAdBEEEUIAcoAhAgAUYbaiAGNgIAIAZFDQILIAYgBzYCGAJAIAEoAhAiAkUNACAGIAI2AhAgAiAGNgIYCyABKAIUIgJFDQEgBkEUaiACNgIAIAIgBjYCGAwBCyADKAIEIgJBA3FBA0cNACADIAJBfnE2AgRBACAANgKQ0ICAACABIABqIAA2AgAgASAAQQFyNgIEDwsgASADTw0AIAMoAgQiAkEBcUUNAAJAAkAgAkECcQ0AAkAgA0EAKAKg0ICAAEcNAEEAIAE2AqDQgIAAQQBBACgClNCAgAAgAGoiADYClNCAgAAgASAAQQFyNgIEIAFBACgCnNCAgABHDQNBAEEANgKQ0ICAAEEAQQA2ApzQgIAADwsCQCADQQAoApzQgIAARw0AQQAgATYCnNCAgABBAEEAKAKQ0ICAACAAaiIANgKQ0ICAACABIABBAXI2AgQgASAAaiAANgIADwsgAkF4cSAAaiEAAkACQCACQf8BSw0AIAMoAggiBCACQQN2IgVBA3RBsNCAgABqIgZGGgJAIAMoAgwiAiAERw0AQQBBACgCiNCAgABBfiAFd3E2AojQgIAADAILIAIgBkYaIAIgBDYCCCAEIAI2AgwMAQsgAygCGCEHAkACQCADKAIMIgYgA0YNACADKAIIIgJBACgCmNCAgABJGiAGIAI2AgggAiAGNgIMDAELAkAgA0EUaiICKAIAIgQNACADQRBqIgIoAgAiBA0AQQAhBgwBCwNAIAIhBSAEIgZBFGoiAigCACIEDQAgBkEQaiECIAYoAhAiBA0ACyAFQQA2AgALIAdFDQACQAJAIAMgAygCHCIEQQJ0QbjSgIAAaiICKAIARw0AIAIgBjYCACAGDQFBAEEAKAKM0ICAAEF+IAR3cTYCjNCAgAAMAgsgB0EQQRQgBygCECADRhtqIAY2AgAgBkUNAQsgBiAHNgIYAkAgAygCECICRQ0AIAYgAjYCECACIAY2AhgLIAMoAhQiAkUNACAGQRRqIAI2AgAgAiAGNgIYCyABIABqIAA2AgAgASAAQQFyNgIEIAFBACgCnNCAgABHDQFBACAANgKQ0ICAAA8LIAMgAkF+cTYCBCABIABqIAA2AgAgASAAQQFyNgIECwJAIABB/wFLDQAgAEF4cUGw0ICAAGohAgJAAkBBACgCiNCAgAAiBEEBIABBA3Z0IgBxDQBBACAEIAByNgKI0ICAACACIQAMAQsgAigCCCEACyAAIAE2AgwgAiABNgIIIAEgAjYCDCABIAA2AggPC0EfIQICQCAAQf///wdLDQAgAEEIdiICIAJBgP4/akEQdkEIcSICdCIEIARBgOAfakEQdkEEcSIEdCIGIAZBgIAPakEQdkECcSIGdEEPdiACIARyIAZyayICQQF0IAAgAkEVanZBAXFyQRxqIQILIAEgAjYCHCABQgA3AhAgAkECdEG40oCAAGohBAJAAkBBACgCjNCAgAAiBkEBIAJ0IgNxDQAgBCABNgIAQQAgBiADcjYCjNCAgAAgASAENgIYIAEgATYCCCABIAE2AgwMAQsgAEEAQRkgAkEBdmsgAkEfRht0IQIgBCgCACEGAkADQCAGIgQoAgRBeHEgAEYNASACQR12IQYgAkEBdCECIAQgBkEEcWpBEGoiAygCACIGDQALIAMgATYCACABIAQ2AhggASABNgIMIAEgATYCCAwBCyAEKAIIIgAgATYCDCAEIAE2AgggAUEANgIYIAEgBDYCDCABIAA2AggLQQBBACgCqNCAgABBf2oiAUF/IAEbNgKo0ICAAAsLBAAAAAtOAAJAIAANAD8AQRB0DwsCQCAAQf//A3ENACAAQX9MDQACQCAAQRB2QAAiAEF/Rw0AQQBBMDYC+NOAgABBfw8LIABBEHQPCxDKgICAAAAL8gICA38BfgJAIAJFDQAgACABOgAAIAIgAGoiA0F/aiABOgAAIAJBA0kNACAAIAE6AAIgACABOgABIANBfWogAToAACADQX5qIAE6AAAgAkEHSQ0AIAAgAToAAyADQXxqIAE6AAAgAkEJSQ0AIABBACAAa0EDcSIEaiIDIAFB/wFxQYGChAhsIgE2AgAgAyACIARrQXxxIgRqIgJBfGogATYCACAEQQlJDQAgAyABNgIIIAMgATYCBCACQXhqIAE2AgAgAkF0aiABNgIAIARBGUkNACADIAE2AhggAyABNgIUIAMgATYCECADIAE2AgwgAkFwaiABNgIAIAJBbGogATYCACACQWhqIAE2AgAgAkFkaiABNgIAIAQgA0EEcUEYciIFayICQSBJDQAgAa1CgYCAgBB+IQYgAyAFaiEBA0AgASAGNwMYIAEgBjcDECABIAY3AwggASAGNwMAIAFBIGohASACQWBqIgJBH0sNAAsLIAALC45IAQBBgAgLhkgBAAAAAgAAAAMAAAAAAAAAAAAAAAQAAAAFAAAAAAAAAAAAAAAGAAAABwAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEludmFsaWQgY2hhciBpbiB1cmwgcXVlcnkAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9ib2R5AENvbnRlbnQtTGVuZ3RoIG92ZXJmbG93AENodW5rIHNpemUgb3ZlcmZsb3cAUmVzcG9uc2Ugb3ZlcmZsb3cASW52YWxpZCBtZXRob2QgZm9yIEhUVFAveC54IHJlcXVlc3QASW52YWxpZCBtZXRob2QgZm9yIFJUU1AveC54IHJlcXVlc3QARXhwZWN0ZWQgU09VUkNFIG1ldGhvZCBmb3IgSUNFL3gueCByZXF1ZXN0AEludmFsaWQgY2hhciBpbiB1cmwgZnJhZ21lbnQgc3RhcnQARXhwZWN0ZWQgZG90AFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fc3RhdHVzAEludmFsaWQgcmVzcG9uc2Ugc3RhdHVzAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMAVXNlciBjYWxsYmFjayBlcnJvcgBgb25fcmVzZXRgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19oZWFkZXJgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2JlZ2luYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlYCBjYWxsYmFjayBlcnJvcgBgb25fc3RhdHVzX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdmVyc2lvbl9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3VybF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWVzc2FnZV9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX21ldGhvZF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lYCBjYWxsYmFjayBlcnJvcgBVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNlcnZlcgBJbnZhbGlkIGhlYWRlciB2YWx1ZSBjaGFyAEludmFsaWQgaGVhZGVyIGZpZWxkIGNoYXIAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl92ZXJzaW9uAEludmFsaWQgbWlub3IgdmVyc2lvbgBJbnZhbGlkIG1ham9yIHZlcnNpb24ARXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgdmVyc2lvbgBFeHBlY3RlZCBDUkxGIGFmdGVyIHZlcnNpb24ASW52YWxpZCBIVFRQIHZlcnNpb24ASW52YWxpZCBoZWFkZXIgdG9rZW4AU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl91cmwASW52YWxpZCBjaGFyYWN0ZXJzIGluIHVybABVbmV4cGVjdGVkIHN0YXJ0IGNoYXIgaW4gdXJsAERvdWJsZSBAIGluIHVybABFbXB0eSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXJhY3RlciBpbiBDb250ZW50LUxlbmd0aABEdXBsaWNhdGUgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyIGluIHVybCBwYXRoAENvbnRlbnQtTGVuZ3RoIGNhbid0IGJlIHByZXNlbnQgd2l0aCBUcmFuc2Zlci1FbmNvZGluZwBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBzaXplAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25faGVhZGVyX3ZhbHVlAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgdmFsdWUATWlzc2luZyBleHBlY3RlZCBMRiBhZnRlciBoZWFkZXIgdmFsdWUASW52YWxpZCBgVHJhbnNmZXItRW5jb2RpbmdgIGhlYWRlciB2YWx1ZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIHF1b3RlIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAFBhdXNlZCBieSBvbl9oZWFkZXJzX2NvbXBsZXRlAEludmFsaWQgRU9GIHN0YXRlAG9uX3Jlc2V0IHBhdXNlAG9uX2NodW5rX2hlYWRlciBwYXVzZQBvbl9tZXNzYWdlX2JlZ2luIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZSBwYXVzZQBvbl9zdGF0dXNfY29tcGxldGUgcGF1c2UAb25fdmVyc2lvbl9jb21wbGV0ZSBwYXVzZQBvbl91cmxfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlIHBhdXNlAG9uX21lc3NhZ2VfY29tcGxldGUgcGF1c2UAb25fbWV0aG9kX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fbmFtZSBwYXVzZQBVbmV4cGVjdGVkIHNwYWNlIGFmdGVyIHN0YXJ0IGxpbmUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fbmFtZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIG5hbWUAUGF1c2Ugb24gQ09OTkVDVC9VcGdyYWRlAFBhdXNlIG9uIFBSSS9VcGdyYWRlAEV4cGVjdGVkIEhUVFAvMiBDb25uZWN0aW9uIFByZWZhY2UAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9tZXRob2QARXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgbWV0aG9kAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25faGVhZGVyX2ZpZWxkAFBhdXNlZABJbnZhbGlkIHdvcmQgZW5jb3VudGVyZWQASW52YWxpZCBtZXRob2QgZW5jb3VudGVyZWQAVW5leHBlY3RlZCBjaGFyIGluIHVybCBzY2hlbWEAUmVxdWVzdCBoYXMgaW52YWxpZCBgVHJhbnNmZXItRW5jb2RpbmdgAFNXSVRDSF9QUk9YWQBVU0VfUFJPWFkATUtBQ1RJVklUWQBVTlBST0NFU1NBQkxFX0VOVElUWQBDT1BZAE1PVkVEX1BFUk1BTkVOVExZAFRPT19FQVJMWQBOT1RJRlkARkFJTEVEX0RFUEVOREVOQ1kAQkFEX0dBVEVXQVkAUExBWQBQVVQAQ0hFQ0tPVVQAR0FURVdBWV9USU1FT1VUAFJFUVVFU1RfVElNRU9VVABORVRXT1JLX0NPTk5FQ1RfVElNRU9VVABDT05ORUNUSU9OX1RJTUVPVVQATE9HSU5fVElNRU9VVABORVRXT1JLX1JFQURfVElNRU9VVABQT1NUAE1JU0RJUkVDVEVEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfTE9BRF9CQUxBTkNFRF9SRVFVRVNUAEJBRF9SRVFVRVNUAEhUVFBfUkVRVUVTVF9TRU5UX1RPX0hUVFBTX1BPUlQAUkVQT1JUAElNX0FfVEVBUE9UAFJFU0VUX0NPTlRFTlQATk9fQ09OVEVOVABQQVJUSUFMX0NPTlRFTlQASFBFX0lOVkFMSURfQ09OU1RBTlQASFBFX0NCX1JFU0VUAEdFVABIUEVfU1RSSUNUAENPTkZMSUNUAFRFTVBPUkFSWV9SRURJUkVDVABQRVJNQU5FTlRfUkVESVJFQ1QAQ09OTkVDVABNVUxUSV9TVEFUVVMASFBFX0lOVkFMSURfU1RBVFVTAFRPT19NQU5ZX1JFUVVFU1RTAEVBUkxZX0hJTlRTAFVOQVZBSUxBQkxFX0ZPUl9MRUdBTF9SRUFTT05TAE9QVElPTlMAU1dJVENISU5HX1BST1RPQ09MUwBWQVJJQU5UX0FMU09fTkVHT1RJQVRFUwBNVUxUSVBMRV9DSE9JQ0VTAElOVEVSTkFMX1NFUlZFUl9FUlJPUgBXRUJfU0VSVkVSX1VOS05PV05fRVJST1IAUkFJTEdVTl9FUlJPUgBJREVOVElUWV9QUk9WSURFUl9BVVRIRU5USUNBVElPTl9FUlJPUgBTU0xfQ0VSVElGSUNBVEVfRVJST1IASU5WQUxJRF9YX0ZPUldBUkRFRF9GT1IAU0VUX1BBUkFNRVRFUgBHRVRfUEFSQU1FVEVSAEhQRV9VU0VSAFNFRV9PVEhFUgBIUEVfQ0JfQ0hVTktfSEVBREVSAE1LQ0FMRU5EQVIAU0VUVVAAV0VCX1NFUlZFUl9JU19ET1dOAFRFQVJET1dOAEhQRV9DTE9TRURfQ09OTkVDVElPTgBIRVVSSVNUSUNfRVhQSVJBVElPTgBESVNDT05ORUNURURfT1BFUkFUSU9OAE5PTl9BVVRIT1JJVEFUSVZFX0lORk9STUFUSU9OAEhQRV9JTlZBTElEX1ZFUlNJT04ASFBFX0NCX01FU1NBR0VfQkVHSU4AU0lURV9JU19GUk9aRU4ASFBFX0lOVkFMSURfSEVBREVSX1RPS0VOAElOVkFMSURfVE9LRU4ARk9SQklEREVOAEVOSEFOQ0VfWU9VUl9DQUxNAEhQRV9JTlZBTElEX1VSTABCTE9DS0VEX0JZX1BBUkVOVEFMX0NPTlRST0wATUtDT0wAQUNMAEhQRV9JTlRFUk5BTABSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFX1VOT0ZGSUNJQUwASFBFX09LAFVOTElOSwBVTkxPQ0sAUFJJAFJFVFJZX1dJVEgASFBFX0lOVkFMSURfQ09OVEVOVF9MRU5HVEgASFBFX1VORVhQRUNURURfQ09OVEVOVF9MRU5HVEgARkxVU0gAUFJPUFBBVENIAE0tU0VBUkNIAFVSSV9UT09fTE9ORwBQUk9DRVNTSU5HAE1JU0NFTExBTkVPVVNfUEVSU0lTVEVOVF9XQVJOSU5HAE1JU0NFTExBTkVPVVNfV0FSTklORwBIUEVfSU5WQUxJRF9UUkFOU0ZFUl9FTkNPRElORwBFeHBlY3RlZCBDUkxGAEhQRV9JTlZBTElEX0NIVU5LX1NJWkUATU9WRQBDT05USU5VRQBIUEVfQ0JfU1RBVFVTX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJTX0NPTVBMRVRFAEhQRV9DQl9WRVJTSU9OX0NPTVBMRVRFAEhQRV9DQl9VUkxfQ09NUExFVEUASFBFX0NCX0NIVU5LX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJfVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9WQUxVRV9DT01QTEVURQBIUEVfQ0JfQ0hVTktfRVhURU5TSU9OX05BTUVfQ09NUExFVEUASFBFX0NCX01FU1NBR0VfQ09NUExFVEUASFBFX0NCX01FVEhPRF9DT01QTEVURQBIUEVfQ0JfSEVBREVSX0ZJRUxEX0NPTVBMRVRFAERFTEVURQBIUEVfSU5WQUxJRF9FT0ZfU1RBVEUASU5WQUxJRF9TU0xfQ0VSVElGSUNBVEUAUEFVU0UATk9fUkVTUE9OU0UAVU5TVVBQT1JURURfTUVESUFfVFlQRQBHT05FAE5PVF9BQ0NFUFRBQkxFAFNFUlZJQ0VfVU5BVkFJTEFCTEUAUkFOR0VfTk9UX1NBVElTRklBQkxFAE9SSUdJTl9JU19VTlJFQUNIQUJMRQBSRVNQT05TRV9JU19TVEFMRQBQVVJHRQBNRVJHRQBSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFAFJFUVVFU1RfSEVBREVSX1RPT19MQVJHRQBQQVlMT0FEX1RPT19MQVJHRQBJTlNVRkZJQ0lFTlRfU1RPUkFHRQBIUEVfUEFVU0VEX1VQR1JBREUASFBFX1BBVVNFRF9IMl9VUEdSQURFAFNPVVJDRQBBTk5PVU5DRQBUUkFDRQBIUEVfVU5FWFBFQ1RFRF9TUEFDRQBERVNDUklCRQBVTlNVQlNDUklCRQBSRUNPUkQASFBFX0lOVkFMSURfTUVUSE9EAE5PVF9GT1VORABQUk9QRklORABVTkJJTkQAUkVCSU5EAFVOQVVUSE9SSVpFRABNRVRIT0RfTk9UX0FMTE9XRUQASFRUUF9WRVJTSU9OX05PVF9TVVBQT1JURUQAQUxSRUFEWV9SRVBPUlRFRABBQ0NFUFRFRABOT1RfSU1QTEVNRU5URUQATE9PUF9ERVRFQ1RFRABIUEVfQ1JfRVhQRUNURUQASFBFX0xGX0VYUEVDVEVEAENSRUFURUQASU1fVVNFRABIUEVfUEFVU0VEAFRJTUVPVVRfT0NDVVJFRABQQVlNRU5UX1JFUVVJUkVEAFBSRUNPTkRJVElPTl9SRVFVSVJFRABQUk9YWV9BVVRIRU5USUNBVElPTl9SRVFVSVJFRABORVRXT1JLX0FVVEhFTlRJQ0FUSU9OX1JFUVVJUkVEAExFTkdUSF9SRVFVSVJFRABTU0xfQ0VSVElGSUNBVEVfUkVRVUlSRUQAVVBHUkFERV9SRVFVSVJFRABQQUdFX0VYUElSRUQAUFJFQ09ORElUSU9OX0ZBSUxFRABFWFBFQ1RBVElPTl9GQUlMRUQAUkVWQUxJREFUSU9OX0ZBSUxFRABTU0xfSEFORFNIQUtFX0ZBSUxFRABMT0NLRUQAVFJBTlNGT1JNQVRJT05fQVBQTElFRABOT1RfTU9ESUZJRUQATk9UX0VYVEVOREVEAEJBTkRXSURUSF9MSU1JVF9FWENFRURFRABTSVRFX0lTX09WRVJMT0FERUQASEVBRABFeHBlY3RlZCBIVFRQLwAAXhMAACYTAAAwEAAA8BcAAJ0TAAAVEgAAORcAAPASAAAKEAAAdRIAAK0SAACCEwAATxQAAH8QAACgFQAAIxQAAIkSAACLFAAATRUAANQRAADPFAAAEBgAAMkWAADcFgAAwREAAOAXAAC7FAAAdBQAAHwVAADlFAAACBcAAB8QAABlFQAAoxQAACgVAAACFQAAmRUAACwQAACLGQAATw8AANQOAABqEAAAzhAAAAIXAACJDgAAbhMAABwTAABmFAAAVhcAAMETAADNEwAAbBMAAGgXAABmFwAAXxcAACITAADODwAAaQ4AANgOAABjFgAAyxMAAKoOAAAoFwAAJhcAAMUTAABdFgAA6BEAAGcTAABlEwAA8hYAAHMTAAAdFwAA+RYAAPMRAADPDgAAzhUAAAwSAACzEQAApREAAGEQAAAyFwAAuxMAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIDAgICAgIAAAICAAICAAICAgICAgICAgIABAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgIAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgICAgACAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAICAgICAAACAgACAgACAgICAgICAgICAAMABAAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbG9zZWVlcC1hbGl2ZQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBY2h1bmtlZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAQEBAQEAAAEBAAEBAAEBAQEBAQEBAQEAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABlY3Rpb25lbnQtbGVuZ3Rob25yb3h5LWNvbm5lY3Rpb24AAAAAAAAAAAAAAAAAAAByYW5zZmVyLWVuY29kaW5ncGdyYWRlDQoNCg0KU00NCg0KVFRQL0NFL1RTUC8AAAAAAAAAAAAAAAABAgABAwAAAAAAAAAAAAAAAAAAAAAAAAQBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAQIAAQMAAAAAAAAAAAAAAAAAAAAAAAAEAQEFAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAQAAAgAAAAAAAAAAAAAAAAAAAAAAAAMEAAAEBAQEBAQEBAQEBAUEBAQEBAQEBAQEBAQABAAGBwQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAIAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABOT1VOQ0VFQ0tPVVRORUNURVRFQ1JJQkVMVVNIRVRFQURTRUFSQ0hSR0VDVElWSVRZTEVOREFSVkVPVElGWVBUSU9OU0NIU0VBWVNUQVRDSEdFT1JESVJFQ1RPUlRSQ0hQQVJBTUVURVJVUkNFQlNDUklCRUFSRE9XTkFDRUlORE5LQ0tVQlNDUklCRUhUVFAvQURUUC8=' + + +/***/ }), + +/***/ 5627: +/***/ ((module) => { + +module.exports = 'AGFzbQEAAAABMAhgAX8Bf2ADf39/AX9gBH9/f38Bf2AAAGADf39/AGABfwBgAn9/AGAGf39/f39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQACA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAA0ZFAwMEAAAFAAAAAAAABQEFAAUFBQAABgAAAAAGBgYGAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAABAQcAAAUFAwABBAUBcAESEgUDAQACBggBfwFBgNQECwfRBSIGbWVtb3J5AgALX2luaXRpYWxpemUACRlfX2luZGlyZWN0X2Z1bmN0aW9uX3RhYmxlAQALbGxodHRwX2luaXQAChhsbGh0dHBfc2hvdWxkX2tlZXBfYWxpdmUAQQxsbGh0dHBfYWxsb2MADAZtYWxsb2MARgtsbGh0dHBfZnJlZQANBGZyZWUASA9sbGh0dHBfZ2V0X3R5cGUADhVsbGh0dHBfZ2V0X2h0dHBfbWFqb3IADxVsbGh0dHBfZ2V0X2h0dHBfbWlub3IAEBFsbGh0dHBfZ2V0X21ldGhvZAARFmxsaHR0cF9nZXRfc3RhdHVzX2NvZGUAEhJsbGh0dHBfZ2V0X3VwZ3JhZGUAEwxsbGh0dHBfcmVzZXQAFA5sbGh0dHBfZXhlY3V0ZQAVFGxsaHR0cF9zZXR0aW5nc19pbml0ABYNbGxodHRwX2ZpbmlzaAAXDGxsaHR0cF9wYXVzZQAYDWxsaHR0cF9yZXN1bWUAGRtsbGh0dHBfcmVzdW1lX2FmdGVyX3VwZ3JhZGUAGhBsbGh0dHBfZ2V0X2Vycm5vABsXbGxodHRwX2dldF9lcnJvcl9yZWFzb24AHBdsbGh0dHBfc2V0X2Vycm9yX3JlYXNvbgAdFGxsaHR0cF9nZXRfZXJyb3JfcG9zAB4RbGxodHRwX2Vycm5vX25hbWUAHxJsbGh0dHBfbWV0aG9kX25hbWUAIBJsbGh0dHBfc3RhdHVzX25hbWUAIRpsbGh0dHBfc2V0X2xlbmllbnRfaGVhZGVycwAiIWxsaHR0cF9zZXRfbGVuaWVudF9jaHVua2VkX2xlbmd0aAAjHWxsaHR0cF9zZXRfbGVuaWVudF9rZWVwX2FsaXZlACQkbGxodHRwX3NldF9sZW5pZW50X3RyYW5zZmVyX2VuY29kaW5nACUYbGxodHRwX21lc3NhZ2VfbmVlZHNfZW9mAD8JFwEAQQELEQECAwQFCwYHNTk3MS8tJyspCrLgAkUCAAsIABCIgICAAAsZACAAEMKAgIAAGiAAIAI2AjggACABOgAoCxwAIAAgAC8BMiAALQAuIAAQwYCAgAAQgICAgAALKgEBf0HAABDGgICAACIBEMKAgIAAGiABQYCIgIAANgI4IAEgADoAKCABCwoAIAAQyICAgAALBwAgAC0AKAsHACAALQAqCwcAIAAtACsLBwAgAC0AKQsHACAALwEyCwcAIAAtAC4LRQEEfyAAKAIYIQEgAC0ALSECIAAtACghAyAAKAI4IQQgABDCgICAABogACAENgI4IAAgAzoAKCAAIAI6AC0gACABNgIYCxEAIAAgASABIAJqEMOAgIAACxAAIABBAEHcABDMgICAABoLZwEBf0EAIQECQCAAKAIMDQACQAJAAkACQCAALQAvDgMBAAMCCyAAKAI4IgFFDQAgASgCLCIBRQ0AIAAgARGAgICAAAAiAQ0DC0EADwsQyoCAgAAACyAAQcOWgIAANgIQQQ4hAQsgAQseAAJAIAAoAgwNACAAQdGbgIAANgIQIABBFTYCDAsLFgACQCAAKAIMQRVHDQAgAEEANgIMCwsWAAJAIAAoAgxBFkcNACAAQQA2AgwLCwcAIAAoAgwLBwAgACgCEAsJACAAIAE2AhALBwAgACgCFAsiAAJAIABBJEkNABDKgICAAAALIABBAnRBoLOAgABqKAIACyIAAkAgAEEuSQ0AEMqAgIAAAAsgAEECdEGwtICAAGooAgAL7gsBAX9B66iAgAAhAQJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIABBnH9qDvQDY2IAAWFhYWFhYQIDBAVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhBgcICQoLDA0OD2FhYWFhEGFhYWFhYWFhYWFhEWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYRITFBUWFxgZGhthYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2YTc4OTphYWFhYWFhYTthYWE8YWFhYT0+P2FhYWFhYWFhQGFhQWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYUJDREVGR0hJSktMTU5PUFFSU2FhYWFhYWFhVFVWV1hZWlthXF1hYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFeYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhX2BhC0Hhp4CAAA8LQaShgIAADwtBy6yAgAAPC0H+sYCAAA8LQcCkgIAADwtBq6SAgAAPC0GNqICAAA8LQeKmgIAADwtBgLCAgAAPC0G5r4CAAA8LQdekgIAADwtB75+AgAAPC0Hhn4CAAA8LQfqfgIAADwtB8qCAgAAPC0Gor4CAAA8LQa6ygIAADwtBiLCAgAAPC0Hsp4CAAA8LQYKigIAADwtBjp2AgAAPC0HQroCAAA8LQcqjgIAADwtBxbKAgAAPC0HfnICAAA8LQdKcgIAADwtBxKCAgAAPC0HXoICAAA8LQaKfgIAADwtB7a6AgAAPC0GrsICAAA8LQdSlgIAADwtBzK6AgAAPC0H6roCAAA8LQfyrgIAADwtB0rCAgAAPC0HxnYCAAA8LQbuggIAADwtB96uAgAAPC0GQsYCAAA8LQdexgIAADwtBoq2AgAAPC0HUp4CAAA8LQeCrgIAADwtBn6yAgAAPC0HrsYCAAA8LQdWfgIAADwtByrGAgAAPC0HepYCAAA8LQdSegIAADwtB9JyAgAAPC0GnsoCAAA8LQbGdgIAADwtBoJ2AgAAPC0G5sYCAAA8LQbywgIAADwtBkqGAgAAPC0GzpoCAAA8LQemsgIAADwtBrJ6AgAAPC0HUq4CAAA8LQfemgIAADwtBgKaAgAAPC0GwoYCAAA8LQf6egIAADwtBjaOAgAAPC0GJrYCAAA8LQfeigIAADwtBoLGAgAAPC0Gun4CAAA8LQcalgIAADwtB6J6AgAAPC0GTooCAAA8LQcKvgIAADwtBw52AgAAPC0GLrICAAA8LQeGdgIAADwtBja+AgAAPC0HqoYCAAA8LQbStgIAADwtB0q+AgAAPC0HfsoCAAA8LQdKygIAADwtB8LCAgAAPC0GpooCAAA8LQfmjgIAADwtBmZ6AgAAPC0G1rICAAA8LQZuwgIAADwtBkrKAgAAPC0G2q4CAAA8LQcKigIAADwtB+LKAgAAPC0GepYCAAA8LQdCigIAADwtBup6AgAAPC0GBnoCAAA8LEMqAgIAAAAtB1qGAgAAhAQsgAQsWACAAIAAtAC1B/gFxIAFBAEdyOgAtCxkAIAAgAC0ALUH9AXEgAUEAR0EBdHI6AC0LGQAgACAALQAtQfsBcSABQQBHQQJ0cjoALQsZACAAIAAtAC1B9wFxIAFBAEdBA3RyOgAtCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAgAiBEUNACAAIAQRgICAgAAAIQMLIAMLSQECf0EAIQMCQCAAKAI4IgRFDQAgBCgCBCIERQ0AIAAgASACIAFrIAQRgYCAgAAAIgNBf0cNACAAQcaRgIAANgIQQRghAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIwIgRFDQAgACAEEYCAgIAAACEDCyADC0kBAn9BACEDAkAgACgCOCIERQ0AIAQoAggiBEUNACAAIAEgAiABayAEEYGAgIAAACIDQX9HDQAgAEH2ioCAADYCEEEYIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCNCIERQ0AIAAgBBGAgICAAAAhAwsgAwtJAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIMIgRFDQAgACABIAIgAWsgBBGBgICAAAAiA0F/Rw0AIABB7ZqAgAA2AhBBGCEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAjgiBEUNACAAIAQRgICAgAAAIQMLIAMLSQECf0EAIQMCQCAAKAI4IgRFDQAgBCgCECIERQ0AIAAgASACIAFrIAQRgYCAgAAAIgNBf0cNACAAQZWQgIAANgIQQRghAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAI8IgRFDQAgACAEEYCAgIAAACEDCyADC0kBAn9BACEDAkAgACgCOCIERQ0AIAQoAhQiBEUNACAAIAEgAiABayAEEYGAgIAAACIDQX9HDQAgAEGqm4CAADYCEEEYIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCQCIERQ0AIAAgBBGAgICAAAAhAwsgAwtJAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIYIgRFDQAgACABIAIgAWsgBBGBgICAAAAiA0F/Rw0AIABB7ZOAgAA2AhBBGCEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAkQiBEUNACAAIAQRgICAgAAAIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCJCIERQ0AIAAgBBGAgICAAAAhAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIsIgRFDQAgACAEEYCAgIAAACEDCyADC0kBAn9BACEDAkAgACgCOCIERQ0AIAQoAigiBEUNACAAIAEgAiABayAEEYGAgIAAACIDQX9HDQAgAEH2iICAADYCEEEYIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCUCIERQ0AIAAgBBGAgICAAAAhAwsgAwtJAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIcIgRFDQAgACABIAIgAWsgBBGBgICAAAAiA0F/Rw0AIABBwpmAgAA2AhBBGCEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAkgiBEUNACAAIAQRgICAgAAAIQMLIAMLSQECf0EAIQMCQCAAKAI4IgRFDQAgBCgCICIERQ0AIAAgASACIAFrIAQRgYCAgAAAIgNBf0cNACAAQZSUgIAANgIQQRghAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAJMIgRFDQAgACAEEYCAgIAAACEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAlQiBEUNACAAIAQRgICAgAAAIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCWCIERQ0AIAAgBBGAgICAAAAhAwsgAwtFAQF/AkACQCAALwEwQRRxQRRHDQBBASEDIAAtAChBAUYNASAALwEyQeUARiEDDAELIAAtAClBBUYhAwsgACADOgAuQQAL/gEBA39BASEDAkAgAC8BMCIEQQhxDQAgACkDIEIAUiEDCwJAAkAgAC0ALkUNAEEBIQUgAC0AKUEFRg0BQQEhBSAEQcAAcUUgA3FBAUcNAQtBACEFIARBwABxDQBBAiEFIARB//8DcSIDQQhxDQACQCADQYAEcUUNAAJAIAAtAChBAUcNACAALQAtQQpxDQBBBQ8LQQQPCwJAIANBIHENAAJAIAAtAChBAUYNACAALwEyQf//A3EiAEGcf2pB5ABJDQAgAEHMAUYNACAAQbACRg0AQQQhBSAEQShxRQ0CIANBiARxQYAERg0CC0EADwtBAEEDIAApAyBQGyEFCyAFC2IBAn9BACEBAkAgAC0AKEEBRg0AIAAvATJB//8DcSICQZx/akHkAEkNACACQcwBRg0AIAJBsAJGDQAgAC8BMCIAQcAAcQ0AQQEhASAAQYgEcUGABEYNACAAQShxRSEBCyABC6cBAQN/AkACQAJAIAAtACpFDQAgAC0AK0UNAEEAIQMgAC8BMCIEQQJxRQ0BDAILQQAhAyAALwEwIgRBAXFFDQELQQEhAyAALQAoQQFGDQAgAC8BMkH//wNxIgVBnH9qQeQASQ0AIAVBzAFGDQAgBUGwAkYNACAEQcAAcQ0AQQAhAyAEQYgEcUGABEYNACAEQShxQQBHIQMLIABBADsBMCAAQQA6AC8gAwuZAQECfwJAAkACQCAALQAqRQ0AIAAtACtFDQBBACEBIAAvATAiAkECcUUNAQwCC0EAIQEgAC8BMCICQQFxRQ0BC0EBIQEgAC0AKEEBRg0AIAAvATJB//8DcSIAQZx/akHkAEkNACAAQcwBRg0AIABBsAJGDQAgAkHAAHENAEEAIQEgAkGIBHFBgARGDQAgAkEocUEARyEBCyABC0kBAXsgAEEQav0MAAAAAAAAAAAAAAAAAAAAACIB/QsDACAAIAH9CwMAIABBMGogAf0LAwAgAEEgaiAB/QsDACAAQd0BNgIcQQALewEBfwJAIAAoAgwiAw0AAkAgACgCBEUNACAAIAE2AgQLAkAgACABIAIQxICAgAAiAw0AIAAoAgwPCyAAIAM2AhxBACEDIAAoAgQiAUUNACAAIAEgAiAAKAIIEYGAgIAAACIBRQ0AIAAgAjYCFCAAIAE2AgwgASEDCyADC+TzAQMOfwN+BH8jgICAgABBEGsiAySAgICAACABIQQgASEFIAEhBiABIQcgASEIIAEhCSABIQogASELIAEhDCABIQ0gASEOIAEhDwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAAKAIcIhBBf2oO3QHaAQHZAQIDBAUGBwgJCgsMDQ7YAQ8Q1wEREtYBExQVFhcYGRob4AHfARwdHtUBHyAhIiMkJdQBJicoKSorLNMB0gEtLtEB0AEvMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUbbAUdISUrPAc4BS80BTMwBTU5PUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ent8fX5/gAGBAYIBgwGEAYUBhgGHAYgBiQGKAYsBjAGNAY4BjwGQAZEBkgGTAZQBlQGWAZcBmAGZAZoBmwGcAZ0BngGfAaABoQGiAaMBpAGlAaYBpwGoAakBqgGrAawBrQGuAa8BsAGxAbIBswG0AbUBtgG3AcsBygG4AckBuQHIAboBuwG8Ab0BvgG/AcABwQHCAcMBxAHFAcYBANwBC0EAIRAMxgELQQ4hEAzFAQtBDSEQDMQBC0EPIRAMwwELQRAhEAzCAQtBEyEQDMEBC0EUIRAMwAELQRUhEAy/AQtBFiEQDL4BC0EXIRAMvQELQRghEAy8AQtBGSEQDLsBC0EaIRAMugELQRshEAy5AQtBHCEQDLgBC0EIIRAMtwELQR0hEAy2AQtBICEQDLUBC0EfIRAMtAELQQchEAyzAQtBISEQDLIBC0EiIRAMsQELQR4hEAywAQtBIyEQDK8BC0ESIRAMrgELQREhEAytAQtBJCEQDKwBC0ElIRAMqwELQSYhEAyqAQtBJyEQDKkBC0HDASEQDKgBC0EpIRAMpwELQSshEAymAQtBLCEQDKUBC0EtIRAMpAELQS4hEAyjAQtBLyEQDKIBC0HEASEQDKEBC0EwIRAMoAELQTQhEAyfAQtBDCEQDJ4BC0ExIRAMnQELQTIhEAycAQtBMyEQDJsBC0E5IRAMmgELQTUhEAyZAQtBxQEhEAyYAQtBCyEQDJcBC0E6IRAMlgELQTYhEAyVAQtBCiEQDJQBC0E3IRAMkwELQTghEAySAQtBPCEQDJEBC0E7IRAMkAELQT0hEAyPAQtBCSEQDI4BC0EoIRAMjQELQT4hEAyMAQtBPyEQDIsBC0HAACEQDIoBC0HBACEQDIkBC0HCACEQDIgBC0HDACEQDIcBC0HEACEQDIYBC0HFACEQDIUBC0HGACEQDIQBC0EqIRAMgwELQccAIRAMggELQcgAIRAMgQELQckAIRAMgAELQcoAIRAMfwtBywAhEAx+C0HNACEQDH0LQcwAIRAMfAtBzgAhEAx7C0HPACEQDHoLQdAAIRAMeQtB0QAhEAx4C0HSACEQDHcLQdMAIRAMdgtB1AAhEAx1C0HWACEQDHQLQdUAIRAMcwtBBiEQDHILQdcAIRAMcQtBBSEQDHALQdgAIRAMbwtBBCEQDG4LQdkAIRAMbQtB2gAhEAxsC0HbACEQDGsLQdwAIRAMagtBAyEQDGkLQd0AIRAMaAtB3gAhEAxnC0HfACEQDGYLQeEAIRAMZQtB4AAhEAxkC0HiACEQDGMLQeMAIRAMYgtBAiEQDGELQeQAIRAMYAtB5QAhEAxfC0HmACEQDF4LQecAIRAMXQtB6AAhEAxcC0HpACEQDFsLQeoAIRAMWgtB6wAhEAxZC0HsACEQDFgLQe0AIRAMVwtB7gAhEAxWC0HvACEQDFULQfAAIRAMVAtB8QAhEAxTC0HyACEQDFILQfMAIRAMUQtB9AAhEAxQC0H1ACEQDE8LQfYAIRAMTgtB9wAhEAxNC0H4ACEQDEwLQfkAIRAMSwtB+gAhEAxKC0H7ACEQDEkLQfwAIRAMSAtB/QAhEAxHC0H+ACEQDEYLQf8AIRAMRQtBgAEhEAxEC0GBASEQDEMLQYIBIRAMQgtBgwEhEAxBC0GEASEQDEALQYUBIRAMPwtBhgEhEAw+C0GHASEQDD0LQYgBIRAMPAtBiQEhEAw7C0GKASEQDDoLQYsBIRAMOQtBjAEhEAw4C0GNASEQDDcLQY4BIRAMNgtBjwEhEAw1C0GQASEQDDQLQZEBIRAMMwtBkgEhEAwyC0GTASEQDDELQZQBIRAMMAtBlQEhEAwvC0GWASEQDC4LQZcBIRAMLQtBmAEhEAwsC0GZASEQDCsLQZoBIRAMKgtBmwEhEAwpC0GcASEQDCgLQZ0BIRAMJwtBngEhEAwmC0GfASEQDCULQaABIRAMJAtBoQEhEAwjC0GiASEQDCILQaMBIRAMIQtBpAEhEAwgC0GlASEQDB8LQaYBIRAMHgtBpwEhEAwdC0GoASEQDBwLQakBIRAMGwtBqgEhEAwaC0GrASEQDBkLQawBIRAMGAtBrQEhEAwXC0GuASEQDBYLQQEhEAwVC0GvASEQDBQLQbABIRAMEwtBsQEhEAwSC0GzASEQDBELQbIBIRAMEAtBtAEhEAwPC0G1ASEQDA4LQbYBIRAMDQtBtwEhEAwMC0G4ASEQDAsLQbkBIRAMCgtBugEhEAwJC0G7ASEQDAgLQcYBIRAMBwtBvAEhEAwGC0G9ASEQDAULQb4BIRAMBAtBvwEhEAwDC0HAASEQDAILQcIBIRAMAQtBwQEhEAsDQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIBAOxwEAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB4fICEjJSg/QEFERUZHSElKS0xNT1BRUlPeA1dZW1xdYGJlZmdoaWprbG1vcHFyc3R1dnd4eXp7fH1+gAGCAYUBhgGHAYkBiwGMAY0BjgGPAZABkQGUAZUBlgGXAZgBmQGaAZsBnAGdAZ4BnwGgAaEBogGjAaQBpQGmAacBqAGpAaoBqwGsAa0BrgGvAbABsQGyAbMBtAG1AbYBtwG4AbkBugG7AbwBvQG+Ab8BwAHBAcIBwwHEAcUBxgHHAcgByQHKAcsBzAHNAc4BzwHQAdEB0gHTAdQB1QHWAdcB2AHZAdoB2wHcAd0B3gHgAeEB4gHjAeQB5QHmAecB6AHpAeoB6wHsAe0B7gHvAfAB8QHyAfMBmQKkArAC/gL+AgsgASIEIAJHDfMBQd0BIRAM/wMLIAEiECACRw3dAUHDASEQDP4DCyABIgEgAkcNkAFB9wAhEAz9AwsgASIBIAJHDYYBQe8AIRAM/AMLIAEiASACRw1/QeoAIRAM+wMLIAEiASACRw17QegAIRAM+gMLIAEiASACRw14QeYAIRAM+QMLIAEiASACRw0aQRghEAz4AwsgASIBIAJHDRRBEiEQDPcDCyABIgEgAkcNWUHFACEQDPYDCyABIgEgAkcNSkE/IRAM9QMLIAEiASACRw1IQTwhEAz0AwsgASIBIAJHDUFBMSEQDPMDCyAALQAuQQFGDesDDIcCCyAAIAEiASACEMCAgIAAQQFHDeYBIABCADcDIAznAQsgACABIgEgAhC0gICAACIQDecBIAEhAQz1AgsCQCABIgEgAkcNAEEGIRAM8AMLIAAgAUEBaiIBIAIQu4CAgAAiEA3oASABIQEMMQsgAEIANwMgQRIhEAzVAwsgASIQIAJHDStBHSEQDO0DCwJAIAEiASACRg0AIAFBAWohAUEQIRAM1AMLQQchEAzsAwsgAEIAIAApAyAiESACIAEiEGutIhJ9IhMgEyARVhs3AyAgESASViIURQ3lAUEIIRAM6wMLAkAgASIBIAJGDQAgAEGJgICAADYCCCAAIAE2AgQgASEBQRQhEAzSAwtBCSEQDOoDCyABIQEgACkDIFAN5AEgASEBDPICCwJAIAEiASACRw0AQQshEAzpAwsgACABQQFqIgEgAhC2gICAACIQDeUBIAEhAQzyAgsgACABIgEgAhC4gICAACIQDeUBIAEhAQzyAgsgACABIgEgAhC4gICAACIQDeYBIAEhAQwNCyAAIAEiASACELqAgIAAIhAN5wEgASEBDPACCwJAIAEiASACRw0AQQ8hEAzlAwsgAS0AACIQQTtGDQggEEENRw3oASABQQFqIQEM7wILIAAgASIBIAIQuoCAgAAiEA3oASABIQEM8gILA0ACQCABLQAAQfC1gIAAai0AACIQQQFGDQAgEEECRw3rASAAKAIEIRAgAEEANgIEIAAgECABQQFqIgEQuYCAgAAiEA3qASABIQEM9AILIAFBAWoiASACRw0AC0ESIRAM4gMLIAAgASIBIAIQuoCAgAAiEA3pASABIQEMCgsgASIBIAJHDQZBGyEQDOADCwJAIAEiASACRw0AQRYhEAzgAwsgAEGKgICAADYCCCAAIAE2AgQgACABIAIQuICAgAAiEA3qASABIQFBICEQDMYDCwJAIAEiASACRg0AA0ACQCABLQAAQfC3gIAAai0AACIQQQJGDQACQCAQQX9qDgTlAewBAOsB7AELIAFBAWohAUEIIRAMyAMLIAFBAWoiASACRw0AC0EVIRAM3wMLQRUhEAzeAwsDQAJAIAEtAABB8LmAgABqLQAAIhBBAkYNACAQQX9qDgTeAewB4AHrAewBCyABQQFqIgEgAkcNAAtBGCEQDN0DCwJAIAEiASACRg0AIABBi4CAgAA2AgggACABNgIEIAEhAUEHIRAMxAMLQRkhEAzcAwsgAUEBaiEBDAILAkAgASIUIAJHDQBBGiEQDNsDCyAUIQECQCAULQAAQXNqDhTdAu4C7gLuAu4C7gLuAu4C7gLuAu4C7gLuAu4C7gLuAu4C7gLuAgDuAgtBACEQIABBADYCHCAAQa+LgIAANgIQIABBAjYCDCAAIBRBAWo2AhQM2gMLAkAgAS0AACIQQTtGDQAgEEENRw3oASABQQFqIQEM5QILIAFBAWohAQtBIiEQDL8DCwJAIAEiECACRw0AQRwhEAzYAwtCACERIBAhASAQLQAAQVBqDjfnAeYBAQIDBAUGBwgAAAAAAAAACQoLDA0OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPEBESExQAC0EeIRAMvQMLQgIhEQzlAQtCAyERDOQBC0IEIREM4wELQgUhEQziAQtCBiERDOEBC0IHIREM4AELQgghEQzfAQtCCSERDN4BC0IKIREM3QELQgshEQzcAQtCDCERDNsBC0INIREM2gELQg4hEQzZAQtCDyERDNgBC0IKIREM1wELQgshEQzWAQtCDCERDNUBC0INIREM1AELQg4hEQzTAQtCDyERDNIBC0IAIRECQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIBAtAABBUGoON+UB5AEAAQIDBAUGB+YB5gHmAeYB5gHmAeYBCAkKCwwN5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAQ4PEBESE+YBC0ICIREM5AELQgMhEQzjAQtCBCERDOIBC0IFIREM4QELQgYhEQzgAQtCByERDN8BC0IIIREM3gELQgkhEQzdAQtCCiERDNwBC0ILIREM2wELQgwhEQzaAQtCDSERDNkBC0IOIREM2AELQg8hEQzXAQtCCiERDNYBC0ILIREM1QELQgwhEQzUAQtCDSERDNMBC0IOIREM0gELQg8hEQzRAQsgAEIAIAApAyAiESACIAEiEGutIhJ9IhMgEyARVhs3AyAgESASViIURQ3SAUEfIRAMwAMLAkAgASIBIAJGDQAgAEGJgICAADYCCCAAIAE2AgQgASEBQSQhEAynAwtBICEQDL8DCyAAIAEiECACEL6AgIAAQX9qDgW2AQDFAgHRAdIBC0ERIRAMpAMLIABBAToALyAQIQEMuwMLIAEiASACRw3SAUEkIRAMuwMLIAEiDSACRw0eQcYAIRAMugMLIAAgASIBIAIQsoCAgAAiEA3UASABIQEMtQELIAEiECACRw0mQdAAIRAMuAMLAkAgASIBIAJHDQBBKCEQDLgDCyAAQQA2AgQgAEGMgICAADYCCCAAIAEgARCxgICAACIQDdMBIAEhAQzYAQsCQCABIhAgAkcNAEEpIRAMtwMLIBAtAAAiAUEgRg0UIAFBCUcN0wEgEEEBaiEBDBULAkAgASIBIAJGDQAgAUEBaiEBDBcLQSohEAy1AwsCQCABIhAgAkcNAEErIRAMtQMLAkAgEC0AACIBQQlGDQAgAUEgRw3VAQsgAC0ALEEIRg3TASAQIQEMkQMLAkAgASIBIAJHDQBBLCEQDLQDCyABLQAAQQpHDdUBIAFBAWohAQzJAgsgASIOIAJHDdUBQS8hEAyyAwsDQAJAIAEtAAAiEEEgRg0AAkAgEEF2ag4EANwB3AEA2gELIAEhAQzgAQsgAUEBaiIBIAJHDQALQTEhEAyxAwtBMiEQIAEiFCACRg2wAyACIBRrIAAoAgAiAWohFSAUIAFrQQNqIRYCQANAIBQtAAAiF0EgciAXIBdBv39qQf8BcUEaSRtB/wFxIAFB8LuAgABqLQAARw0BAkAgAUEDRw0AQQYhAQyWAwsgAUEBaiEBIBRBAWoiFCACRw0ACyAAIBU2AgAMsQMLIABBADYCACAUIQEM2QELQTMhECABIhQgAkYNrwMgAiAUayAAKAIAIgFqIRUgFCABa0EIaiEWAkADQCAULQAAIhdBIHIgFyAXQb9/akH/AXFBGkkbQf8BcSABQfS7gIAAai0AAEcNAQJAIAFBCEcNAEEFIQEMlQMLIAFBAWohASAUQQFqIhQgAkcNAAsgACAVNgIADLADCyAAQQA2AgAgFCEBDNgBC0E0IRAgASIUIAJGDa4DIAIgFGsgACgCACIBaiEVIBQgAWtBBWohFgJAA0AgFC0AACIXQSByIBcgF0G/f2pB/wFxQRpJG0H/AXEgAUHQwoCAAGotAABHDQECQCABQQVHDQBBByEBDJQDCyABQQFqIQEgFEEBaiIUIAJHDQALIAAgFTYCAAyvAwsgAEEANgIAIBQhAQzXAQsCQCABIgEgAkYNAANAAkAgAS0AAEGAvoCAAGotAAAiEEEBRg0AIBBBAkYNCiABIQEM3QELIAFBAWoiASACRw0AC0EwIRAMrgMLQTAhEAytAwsCQCABIgEgAkYNAANAAkAgAS0AACIQQSBGDQAgEEF2ag4E2QHaAdoB2QHaAQsgAUEBaiIBIAJHDQALQTghEAytAwtBOCEQDKwDCwNAAkAgAS0AACIQQSBGDQAgEEEJRw0DCyABQQFqIgEgAkcNAAtBPCEQDKsDCwNAAkAgAS0AACIQQSBGDQACQAJAIBBBdmoOBNoBAQHaAQALIBBBLEYN2wELIAEhAQwECyABQQFqIgEgAkcNAAtBPyEQDKoDCyABIQEM2wELQcAAIRAgASIUIAJGDagDIAIgFGsgACgCACIBaiEWIBQgAWtBBmohFwJAA0AgFC0AAEEgciABQYDAgIAAai0AAEcNASABQQZGDY4DIAFBAWohASAUQQFqIhQgAkcNAAsgACAWNgIADKkDCyAAQQA2AgAgFCEBC0E2IRAMjgMLAkAgASIPIAJHDQBBwQAhEAynAwsgAEGMgICAADYCCCAAIA82AgQgDyEBIAAtACxBf2oOBM0B1QHXAdkBhwMLIAFBAWohAQzMAQsCQCABIgEgAkYNAANAAkAgAS0AACIQQSByIBAgEEG/f2pB/wFxQRpJG0H/AXEiEEEJRg0AIBBBIEYNAAJAAkACQAJAIBBBnX9qDhMAAwMDAwMDAwEDAwMDAwMDAwMCAwsgAUEBaiEBQTEhEAyRAwsgAUEBaiEBQTIhEAyQAwsgAUEBaiEBQTMhEAyPAwsgASEBDNABCyABQQFqIgEgAkcNAAtBNSEQDKUDC0E1IRAMpAMLAkAgASIBIAJGDQADQAJAIAEtAABBgLyAgABqLQAAQQFGDQAgASEBDNMBCyABQQFqIgEgAkcNAAtBPSEQDKQDC0E9IRAMowMLIAAgASIBIAIQsICAgAAiEA3WASABIQEMAQsgEEEBaiEBC0E8IRAMhwMLAkAgASIBIAJHDQBBwgAhEAygAwsCQANAAkAgAS0AAEF3ag4YAAL+Av4ChAP+Av4C/gL+Av4C/gL+Av4C/gL+Av4C/gL+Av4C/gL+Av4C/gIA/gILIAFBAWoiASACRw0AC0HCACEQDKADCyABQQFqIQEgAC0ALUEBcUUNvQEgASEBC0EsIRAMhQMLIAEiASACRw3TAUHEACEQDJ0DCwNAAkAgAS0AAEGQwICAAGotAABBAUYNACABIQEMtwILIAFBAWoiASACRw0AC0HFACEQDJwDCyANLQAAIhBBIEYNswEgEEE6Rw2BAyAAKAIEIQEgAEEANgIEIAAgASANEK+AgIAAIgEN0AEgDUEBaiEBDLMCC0HHACEQIAEiDSACRg2aAyACIA1rIAAoAgAiAWohFiANIAFrQQVqIRcDQCANLQAAIhRBIHIgFCAUQb9/akH/AXFBGkkbQf8BcSABQZDCgIAAai0AAEcNgAMgAUEFRg30AiABQQFqIQEgDUEBaiINIAJHDQALIAAgFjYCAAyaAwtByAAhECABIg0gAkYNmQMgAiANayAAKAIAIgFqIRYgDSABa0EJaiEXA0AgDS0AACIUQSByIBQgFEG/f2pB/wFxQRpJG0H/AXEgAUGWwoCAAGotAABHDf8CAkAgAUEJRw0AQQIhAQz1AgsgAUEBaiEBIA1BAWoiDSACRw0ACyAAIBY2AgAMmQMLAkAgASINIAJHDQBByQAhEAyZAwsCQAJAIA0tAAAiAUEgciABIAFBv39qQf8BcUEaSRtB/wFxQZJ/ag4HAIADgAOAA4ADgAMBgAMLIA1BAWohAUE+IRAMgAMLIA1BAWohAUE/IRAM/wILQcoAIRAgASINIAJGDZcDIAIgDWsgACgCACIBaiEWIA0gAWtBAWohFwNAIA0tAAAiFEEgciAUIBRBv39qQf8BcUEaSRtB/wFxIAFBoMKAgABqLQAARw39AiABQQFGDfACIAFBAWohASANQQFqIg0gAkcNAAsgACAWNgIADJcDC0HLACEQIAEiDSACRg2WAyACIA1rIAAoAgAiAWohFiANIAFrQQ5qIRcDQCANLQAAIhRBIHIgFCAUQb9/akH/AXFBGkkbQf8BcSABQaLCgIAAai0AAEcN/AIgAUEORg3wAiABQQFqIQEgDUEBaiINIAJHDQALIAAgFjYCAAyWAwtBzAAhECABIg0gAkYNlQMgAiANayAAKAIAIgFqIRYgDSABa0EPaiEXA0AgDS0AACIUQSByIBQgFEG/f2pB/wFxQRpJG0H/AXEgAUHAwoCAAGotAABHDfsCAkAgAUEPRw0AQQMhAQzxAgsgAUEBaiEBIA1BAWoiDSACRw0ACyAAIBY2AgAMlQMLQc0AIRAgASINIAJGDZQDIAIgDWsgACgCACIBaiEWIA0gAWtBBWohFwNAIA0tAAAiFEEgciAUIBRBv39qQf8BcUEaSRtB/wFxIAFB0MKAgABqLQAARw36AgJAIAFBBUcNAEEEIQEM8AILIAFBAWohASANQQFqIg0gAkcNAAsgACAWNgIADJQDCwJAIAEiDSACRw0AQc4AIRAMlAMLAkACQAJAAkAgDS0AACIBQSByIAEgAUG/f2pB/wFxQRpJG0H/AXFBnX9qDhMA/QL9Av0C/QL9Av0C/QL9Av0C/QL9Av0CAf0C/QL9AgID/QILIA1BAWohAUHBACEQDP0CCyANQQFqIQFBwgAhEAz8AgsgDUEBaiEBQcMAIRAM+wILIA1BAWohAUHEACEQDPoCCwJAIAEiASACRg0AIABBjYCAgAA2AgggACABNgIEIAEhAUHFACEQDPoCC0HPACEQDJIDCyAQIQECQAJAIBAtAABBdmoOBAGoAqgCAKgCCyAQQQFqIQELQSchEAz4AgsCQCABIgEgAkcNAEHRACEQDJEDCwJAIAEtAABBIEYNACABIQEMjQELIAFBAWohASAALQAtQQFxRQ3HASABIQEMjAELIAEiFyACRw3IAUHSACEQDI8DC0HTACEQIAEiFCACRg2OAyACIBRrIAAoAgAiAWohFiAUIAFrQQFqIRcDQCAULQAAIAFB1sKAgABqLQAARw3MASABQQFGDccBIAFBAWohASAUQQFqIhQgAkcNAAsgACAWNgIADI4DCwJAIAEiASACRw0AQdUAIRAMjgMLIAEtAABBCkcNzAEgAUEBaiEBDMcBCwJAIAEiASACRw0AQdYAIRAMjQMLAkACQCABLQAAQXZqDgQAzQHNAQHNAQsgAUEBaiEBDMcBCyABQQFqIQFBygAhEAzzAgsgACABIgEgAhCugICAACIQDcsBIAEhAUHNACEQDPICCyAALQApQSJGDYUDDKYCCwJAIAEiASACRw0AQdsAIRAMigMLQQAhFEEBIRdBASEWQQAhEAJAAkACQAJAAkACQAJAAkACQCABLQAAQVBqDgrUAdMBAAECAwQFBgjVAQtBAiEQDAYLQQMhEAwFC0EEIRAMBAtBBSEQDAMLQQYhEAwCC0EHIRAMAQtBCCEQC0EAIRdBACEWQQAhFAzMAQtBCSEQQQEhFEEAIRdBACEWDMsBCwJAIAEiASACRw0AQd0AIRAMiQMLIAEtAABBLkcNzAEgAUEBaiEBDKYCCyABIgEgAkcNzAFB3wAhEAyHAwsCQCABIgEgAkYNACAAQY6AgIAANgIIIAAgATYCBCABIQFB0AAhEAzuAgtB4AAhEAyGAwtB4QAhECABIgEgAkYNhQMgAiABayAAKAIAIhRqIRYgASAUa0EDaiEXA0AgAS0AACAUQeLCgIAAai0AAEcNzQEgFEEDRg3MASAUQQFqIRQgAUEBaiIBIAJHDQALIAAgFjYCAAyFAwtB4gAhECABIgEgAkYNhAMgAiABayAAKAIAIhRqIRYgASAUa0ECaiEXA0AgAS0AACAUQebCgIAAai0AAEcNzAEgFEECRg3OASAUQQFqIRQgAUEBaiIBIAJHDQALIAAgFjYCAAyEAwtB4wAhECABIgEgAkYNgwMgAiABayAAKAIAIhRqIRYgASAUa0EDaiEXA0AgAS0AACAUQenCgIAAai0AAEcNywEgFEEDRg3OASAUQQFqIRQgAUEBaiIBIAJHDQALIAAgFjYCAAyDAwsCQCABIgEgAkcNAEHlACEQDIMDCyAAIAFBAWoiASACEKiAgIAAIhANzQEgASEBQdYAIRAM6QILAkAgASIBIAJGDQADQAJAIAEtAAAiEEEgRg0AAkACQAJAIBBBuH9qDgsAAc8BzwHPAc8BzwHPAc8BzwECzwELIAFBAWohAUHSACEQDO0CCyABQQFqIQFB0wAhEAzsAgsgAUEBaiEBQdQAIRAM6wILIAFBAWoiASACRw0AC0HkACEQDIIDC0HkACEQDIEDCwNAAkAgAS0AAEHwwoCAAGotAAAiEEEBRg0AIBBBfmoOA88B0AHRAdIBCyABQQFqIgEgAkcNAAtB5gAhEAyAAwsCQCABIgEgAkYNACABQQFqIQEMAwtB5wAhEAz/AgsDQAJAIAEtAABB8MSAgABqLQAAIhBBAUYNAAJAIBBBfmoOBNIB0wHUAQDVAQsgASEBQdcAIRAM5wILIAFBAWoiASACRw0AC0HoACEQDP4CCwJAIAEiASACRw0AQekAIRAM/gILAkAgAS0AACIQQXZqDhq6AdUB1QG8AdUB1QHVAdUB1QHVAdUB1QHVAdUB1QHVAdUB1QHVAdUB1QHVAcoB1QHVAQDTAQsgAUEBaiEBC0EGIRAM4wILA0ACQCABLQAAQfDGgIAAai0AAEEBRg0AIAEhAQyeAgsgAUEBaiIBIAJHDQALQeoAIRAM+wILAkAgASIBIAJGDQAgAUEBaiEBDAMLQesAIRAM+gILAkAgASIBIAJHDQBB7AAhEAz6AgsgAUEBaiEBDAELAkAgASIBIAJHDQBB7QAhEAz5AgsgAUEBaiEBC0EEIRAM3gILAkAgASIUIAJHDQBB7gAhEAz3AgsgFCEBAkACQAJAIBQtAABB8MiAgABqLQAAQX9qDgfUAdUB1gEAnAIBAtcBCyAUQQFqIQEMCgsgFEEBaiEBDM0BC0EAIRAgAEEANgIcIABBm5KAgAA2AhAgAEEHNgIMIAAgFEEBajYCFAz2AgsCQANAAkAgAS0AAEHwyICAAGotAAAiEEEERg0AAkACQCAQQX9qDgfSAdMB1AHZAQAEAdkBCyABIQFB2gAhEAzgAgsgAUEBaiEBQdwAIRAM3wILIAFBAWoiASACRw0AC0HvACEQDPYCCyABQQFqIQEMywELAkAgASIUIAJHDQBB8AAhEAz1AgsgFC0AAEEvRw3UASAUQQFqIQEMBgsCQCABIhQgAkcNAEHxACEQDPQCCwJAIBQtAAAiAUEvRw0AIBRBAWohAUHdACEQDNsCCyABQXZqIgRBFksN0wFBASAEdEGJgIACcUUN0wEMygILAkAgASIBIAJGDQAgAUEBaiEBQd4AIRAM2gILQfIAIRAM8gILAkAgASIUIAJHDQBB9AAhEAzyAgsgFCEBAkAgFC0AAEHwzICAAGotAABBf2oOA8kClAIA1AELQeEAIRAM2AILAkAgASIUIAJGDQADQAJAIBQtAABB8MqAgABqLQAAIgFBA0YNAAJAIAFBf2oOAssCANUBCyAUIQFB3wAhEAzaAgsgFEEBaiIUIAJHDQALQfMAIRAM8QILQfMAIRAM8AILAkAgASIBIAJGDQAgAEGPgICAADYCCCAAIAE2AgQgASEBQeAAIRAM1wILQfUAIRAM7wILAkAgASIBIAJHDQBB9gAhEAzvAgsgAEGPgICAADYCCCAAIAE2AgQgASEBC0EDIRAM1AILA0AgAS0AAEEgRw3DAiABQQFqIgEgAkcNAAtB9wAhEAzsAgsCQCABIgEgAkcNAEH4ACEQDOwCCyABLQAAQSBHDc4BIAFBAWohAQzvAQsgACABIgEgAhCsgICAACIQDc4BIAEhAQyOAgsCQCABIgQgAkcNAEH6ACEQDOoCCyAELQAAQcwARw3RASAEQQFqIQFBEyEQDM8BCwJAIAEiBCACRw0AQfsAIRAM6QILIAIgBGsgACgCACIBaiEUIAQgAWtBBWohEANAIAQtAAAgAUHwzoCAAGotAABHDdABIAFBBUYNzgEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBB+wAhEAzoAgsCQCABIgQgAkcNAEH8ACEQDOgCCwJAAkAgBC0AAEG9f2oODADRAdEB0QHRAdEB0QHRAdEB0QHRAQHRAQsgBEEBaiEBQeYAIRAMzwILIARBAWohAUHnACEQDM4CCwJAIAEiBCACRw0AQf0AIRAM5wILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQe3PgIAAai0AAEcNzwEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQf0AIRAM5wILIABBADYCACAQQQFqIQFBECEQDMwBCwJAIAEiBCACRw0AQf4AIRAM5gILIAIgBGsgACgCACIBaiEUIAQgAWtBBWohEAJAA0AgBC0AACABQfbOgIAAai0AAEcNzgEgAUEFRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQf4AIRAM5gILIABBADYCACAQQQFqIQFBFiEQDMsBCwJAIAEiBCACRw0AQf8AIRAM5QILIAIgBGsgACgCACIBaiEUIAQgAWtBA2ohEAJAA0AgBC0AACABQfzOgIAAai0AAEcNzQEgAUEDRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQf8AIRAM5QILIABBADYCACAQQQFqIQFBBSEQDMoBCwJAIAEiBCACRw0AQYABIRAM5AILIAQtAABB2QBHDcsBIARBAWohAUEIIRAMyQELAkAgASIEIAJHDQBBgQEhEAzjAgsCQAJAIAQtAABBsn9qDgMAzAEBzAELIARBAWohAUHrACEQDMoCCyAEQQFqIQFB7AAhEAzJAgsCQCABIgQgAkcNAEGCASEQDOICCwJAAkAgBC0AAEG4f2oOCADLAcsBywHLAcsBywEBywELIARBAWohAUHqACEQDMkCCyAEQQFqIQFB7QAhEAzIAgsCQCABIgQgAkcNAEGDASEQDOECCyACIARrIAAoAgAiAWohECAEIAFrQQJqIRQCQANAIAQtAAAgAUGAz4CAAGotAABHDckBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgEDYCAEGDASEQDOECC0EAIRAgAEEANgIAIBRBAWohAQzGAQsCQCABIgQgAkcNAEGEASEQDOACCyACIARrIAAoAgAiAWohFCAEIAFrQQRqIRACQANAIAQtAAAgAUGDz4CAAGotAABHDcgBIAFBBEYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGEASEQDOACCyAAQQA2AgAgEEEBaiEBQSMhEAzFAQsCQCABIgQgAkcNAEGFASEQDN8CCwJAAkAgBC0AAEG0f2oOCADIAcgByAHIAcgByAEByAELIARBAWohAUHvACEQDMYCCyAEQQFqIQFB8AAhEAzFAgsCQCABIgQgAkcNAEGGASEQDN4CCyAELQAAQcUARw3FASAEQQFqIQEMgwILAkAgASIEIAJHDQBBhwEhEAzdAgsgAiAEayAAKAIAIgFqIRQgBCABa0EDaiEQAkADQCAELQAAIAFBiM+AgABqLQAARw3FASABQQNGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBhwEhEAzdAgsgAEEANgIAIBBBAWohAUEtIRAMwgELAkAgASIEIAJHDQBBiAEhEAzcAgsgAiAEayAAKAIAIgFqIRQgBCABa0EIaiEQAkADQCAELQAAIAFB0M+AgABqLQAARw3EASABQQhGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBiAEhEAzcAgsgAEEANgIAIBBBAWohAUEpIRAMwQELAkAgASIBIAJHDQBBiQEhEAzbAgtBASEQIAEtAABB3wBHDcABIAFBAWohAQyBAgsCQCABIgQgAkcNAEGKASEQDNoCCyACIARrIAAoAgAiAWohFCAEIAFrQQFqIRADQCAELQAAIAFBjM+AgABqLQAARw3BASABQQFGDa8CIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQYoBIRAM2QILAkAgASIEIAJHDQBBiwEhEAzZAgsgAiAEayAAKAIAIgFqIRQgBCABa0ECaiEQAkADQCAELQAAIAFBjs+AgABqLQAARw3BASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBiwEhEAzZAgsgAEEANgIAIBBBAWohAUECIRAMvgELAkAgASIEIAJHDQBBjAEhEAzYAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFB8M+AgABqLQAARw3AASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBjAEhEAzYAgsgAEEANgIAIBBBAWohAUEfIRAMvQELAkAgASIEIAJHDQBBjQEhEAzXAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFB8s+AgABqLQAARw2/ASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBjQEhEAzXAgsgAEEANgIAIBBBAWohAUEJIRAMvAELAkAgASIEIAJHDQBBjgEhEAzWAgsCQAJAIAQtAABBt39qDgcAvwG/Ab8BvwG/AQG/AQsgBEEBaiEBQfgAIRAMvQILIARBAWohAUH5ACEQDLwCCwJAIAEiBCACRw0AQY8BIRAM1QILIAIgBGsgACgCACIBaiEUIAQgAWtBBWohEAJAA0AgBC0AACABQZHPgIAAai0AAEcNvQEgAUEFRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQY8BIRAM1QILIABBADYCACAQQQFqIQFBGCEQDLoBCwJAIAEiBCACRw0AQZABIRAM1AILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQZfPgIAAai0AAEcNvAEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZABIRAM1AILIABBADYCACAQQQFqIQFBFyEQDLkBCwJAIAEiBCACRw0AQZEBIRAM0wILIAIgBGsgACgCACIBaiEUIAQgAWtBBmohEAJAA0AgBC0AACABQZrPgIAAai0AAEcNuwEgAUEGRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZEBIRAM0wILIABBADYCACAQQQFqIQFBFSEQDLgBCwJAIAEiBCACRw0AQZIBIRAM0gILIAIgBGsgACgCACIBaiEUIAQgAWtBBWohEAJAA0AgBC0AACABQaHPgIAAai0AAEcNugEgAUEFRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZIBIRAM0gILIABBADYCACAQQQFqIQFBHiEQDLcBCwJAIAEiBCACRw0AQZMBIRAM0QILIAQtAABBzABHDbgBIARBAWohAUEKIRAMtgELAkAgBCACRw0AQZQBIRAM0AILAkACQCAELQAAQb9/ag4PALkBuQG5AbkBuQG5AbkBuQG5AbkBuQG5AbkBAbkBCyAEQQFqIQFB/gAhEAy3AgsgBEEBaiEBQf8AIRAMtgILAkAgBCACRw0AQZUBIRAMzwILAkACQCAELQAAQb9/ag4DALgBAbgBCyAEQQFqIQFB/QAhEAy2AgsgBEEBaiEEQYABIRAMtQILAkAgBCACRw0AQZYBIRAMzgILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQafPgIAAai0AAEcNtgEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZYBIRAMzgILIABBADYCACAQQQFqIQFBCyEQDLMBCwJAIAQgAkcNAEGXASEQDM0CCwJAAkACQAJAIAQtAABBU2oOIwC4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBAbgBuAG4AbgBuAECuAG4AbgBA7gBCyAEQQFqIQFB+wAhEAy2AgsgBEEBaiEBQfwAIRAMtQILIARBAWohBEGBASEQDLQCCyAEQQFqIQRBggEhEAyzAgsCQCAEIAJHDQBBmAEhEAzMAgsgAiAEayAAKAIAIgFqIRQgBCABa0EEaiEQAkADQCAELQAAIAFBqc+AgABqLQAARw20ASABQQRGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBmAEhEAzMAgsgAEEANgIAIBBBAWohAUEZIRAMsQELAkAgBCACRw0AQZkBIRAMywILIAIgBGsgACgCACIBaiEUIAQgAWtBBWohEAJAA0AgBC0AACABQa7PgIAAai0AAEcNswEgAUEFRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZkBIRAMywILIABBADYCACAQQQFqIQFBBiEQDLABCwJAIAQgAkcNAEGaASEQDMoCCyACIARrIAAoAgAiAWohFCAEIAFrQQFqIRACQANAIAQtAAAgAUG0z4CAAGotAABHDbIBIAFBAUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGaASEQDMoCCyAAQQA2AgAgEEEBaiEBQRwhEAyvAQsCQCAEIAJHDQBBmwEhEAzJAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFBts+AgABqLQAARw2xASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBmwEhEAzJAgsgAEEANgIAIBBBAWohAUEnIRAMrgELAkAgBCACRw0AQZwBIRAMyAILAkACQCAELQAAQax/ag4CAAGxAQsgBEEBaiEEQYYBIRAMrwILIARBAWohBEGHASEQDK4CCwJAIAQgAkcNAEGdASEQDMcCCyACIARrIAAoAgAiAWohFCAEIAFrQQFqIRACQANAIAQtAAAgAUG4z4CAAGotAABHDa8BIAFBAUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGdASEQDMcCCyAAQQA2AgAgEEEBaiEBQSYhEAysAQsCQCAEIAJHDQBBngEhEAzGAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFBus+AgABqLQAARw2uASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBngEhEAzGAgsgAEEANgIAIBBBAWohAUEDIRAMqwELAkAgBCACRw0AQZ8BIRAMxQILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQe3PgIAAai0AAEcNrQEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZ8BIRAMxQILIABBADYCACAQQQFqIQFBDCEQDKoBCwJAIAQgAkcNAEGgASEQDMQCCyACIARrIAAoAgAiAWohFCAEIAFrQQNqIRACQANAIAQtAAAgAUG8z4CAAGotAABHDawBIAFBA0YNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGgASEQDMQCCyAAQQA2AgAgEEEBaiEBQQ0hEAypAQsCQCAEIAJHDQBBoQEhEAzDAgsCQAJAIAQtAABBun9qDgsArAGsAawBrAGsAawBrAGsAawBAawBCyAEQQFqIQRBiwEhEAyqAgsgBEEBaiEEQYwBIRAMqQILAkAgBCACRw0AQaIBIRAMwgILIAQtAABB0ABHDakBIARBAWohBAzpAQsCQCAEIAJHDQBBowEhEAzBAgsCQAJAIAQtAABBt39qDgcBqgGqAaoBqgGqAQCqAQsgBEEBaiEEQY4BIRAMqAILIARBAWohAUEiIRAMpgELAkAgBCACRw0AQaQBIRAMwAILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQcDPgIAAai0AAEcNqAEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQaQBIRAMwAILIABBADYCACAQQQFqIQFBHSEQDKUBCwJAIAQgAkcNAEGlASEQDL8CCwJAAkAgBC0AAEGuf2oOAwCoAQGoAQsgBEEBaiEEQZABIRAMpgILIARBAWohAUEEIRAMpAELAkAgBCACRw0AQaYBIRAMvgILAkACQAJAAkACQCAELQAAQb9/ag4VAKoBqgGqAaoBqgGqAaoBqgGqAaoBAaoBqgECqgGqAQOqAaoBBKoBCyAEQQFqIQRBiAEhEAyoAgsgBEEBaiEEQYkBIRAMpwILIARBAWohBEGKASEQDKYCCyAEQQFqIQRBjwEhEAylAgsgBEEBaiEEQZEBIRAMpAILAkAgBCACRw0AQacBIRAMvQILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQe3PgIAAai0AAEcNpQEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQacBIRAMvQILIABBADYCACAQQQFqIQFBESEQDKIBCwJAIAQgAkcNAEGoASEQDLwCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHCz4CAAGotAABHDaQBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGoASEQDLwCCyAAQQA2AgAgEEEBaiEBQSwhEAyhAQsCQCAEIAJHDQBBqQEhEAy7AgsgAiAEayAAKAIAIgFqIRQgBCABa0EEaiEQAkADQCAELQAAIAFBxc+AgABqLQAARw2jASABQQRGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBqQEhEAy7AgsgAEEANgIAIBBBAWohAUErIRAMoAELAkAgBCACRw0AQaoBIRAMugILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQcrPgIAAai0AAEcNogEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQaoBIRAMugILIABBADYCACAQQQFqIQFBFCEQDJ8BCwJAIAQgAkcNAEGrASEQDLkCCwJAAkACQAJAIAQtAABBvn9qDg8AAQKkAaQBpAGkAaQBpAGkAaQBpAGkAaQBA6QBCyAEQQFqIQRBkwEhEAyiAgsgBEEBaiEEQZQBIRAMoQILIARBAWohBEGVASEQDKACCyAEQQFqIQRBlgEhEAyfAgsCQCAEIAJHDQBBrAEhEAy4AgsgBC0AAEHFAEcNnwEgBEEBaiEEDOABCwJAIAQgAkcNAEGtASEQDLcCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHNz4CAAGotAABHDZ8BIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGtASEQDLcCCyAAQQA2AgAgEEEBaiEBQQ4hEAycAQsCQCAEIAJHDQBBrgEhEAy2AgsgBC0AAEHQAEcNnQEgBEEBaiEBQSUhEAybAQsCQCAEIAJHDQBBrwEhEAy1AgsgAiAEayAAKAIAIgFqIRQgBCABa0EIaiEQAkADQCAELQAAIAFB0M+AgABqLQAARw2dASABQQhGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBrwEhEAy1AgsgAEEANgIAIBBBAWohAUEqIRAMmgELAkAgBCACRw0AQbABIRAMtAILAkACQCAELQAAQat/ag4LAJ0BnQGdAZ0BnQGdAZ0BnQGdAQGdAQsgBEEBaiEEQZoBIRAMmwILIARBAWohBEGbASEQDJoCCwJAIAQgAkcNAEGxASEQDLMCCwJAAkAgBC0AAEG/f2oOFACcAZwBnAGcAZwBnAGcAZwBnAGcAZwBnAGcAZwBnAGcAZwBnAEBnAELIARBAWohBEGZASEQDJoCCyAEQQFqIQRBnAEhEAyZAgsCQCAEIAJHDQBBsgEhEAyyAgsgAiAEayAAKAIAIgFqIRQgBCABa0EDaiEQAkADQCAELQAAIAFB2c+AgABqLQAARw2aASABQQNGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBsgEhEAyyAgsgAEEANgIAIBBBAWohAUEhIRAMlwELAkAgBCACRw0AQbMBIRAMsQILIAIgBGsgACgCACIBaiEUIAQgAWtBBmohEAJAA0AgBC0AACABQd3PgIAAai0AAEcNmQEgAUEGRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQbMBIRAMsQILIABBADYCACAQQQFqIQFBGiEQDJYBCwJAIAQgAkcNAEG0ASEQDLACCwJAAkACQCAELQAAQbt/ag4RAJoBmgGaAZoBmgGaAZoBmgGaAQGaAZoBmgGaAZoBApoBCyAEQQFqIQRBnQEhEAyYAgsgBEEBaiEEQZ4BIRAMlwILIARBAWohBEGfASEQDJYCCwJAIAQgAkcNAEG1ASEQDK8CCyACIARrIAAoAgAiAWohFCAEIAFrQQVqIRACQANAIAQtAAAgAUHkz4CAAGotAABHDZcBIAFBBUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEG1ASEQDK8CCyAAQQA2AgAgEEEBaiEBQSghEAyUAQsCQCAEIAJHDQBBtgEhEAyuAgsgAiAEayAAKAIAIgFqIRQgBCABa0ECaiEQAkADQCAELQAAIAFB6s+AgABqLQAARw2WASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBtgEhEAyuAgsgAEEANgIAIBBBAWohAUEHIRAMkwELAkAgBCACRw0AQbcBIRAMrQILAkACQCAELQAAQbt/ag4OAJYBlgGWAZYBlgGWAZYBlgGWAZYBlgGWAQGWAQsgBEEBaiEEQaEBIRAMlAILIARBAWohBEGiASEQDJMCCwJAIAQgAkcNAEG4ASEQDKwCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHtz4CAAGotAABHDZQBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEG4ASEQDKwCCyAAQQA2AgAgEEEBaiEBQRIhEAyRAQsCQCAEIAJHDQBBuQEhEAyrAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFB8M+AgABqLQAARw2TASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBuQEhEAyrAgsgAEEANgIAIBBBAWohAUEgIRAMkAELAkAgBCACRw0AQboBIRAMqgILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQfLPgIAAai0AAEcNkgEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQboBIRAMqgILIABBADYCACAQQQFqIQFBDyEQDI8BCwJAIAQgAkcNAEG7ASEQDKkCCwJAAkAgBC0AAEG3f2oOBwCSAZIBkgGSAZIBAZIBCyAEQQFqIQRBpQEhEAyQAgsgBEEBaiEEQaYBIRAMjwILAkAgBCACRw0AQbwBIRAMqAILIAIgBGsgACgCACIBaiEUIAQgAWtBB2ohEAJAA0AgBC0AACABQfTPgIAAai0AAEcNkAEgAUEHRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQbwBIRAMqAILIABBADYCACAQQQFqIQFBGyEQDI0BCwJAIAQgAkcNAEG9ASEQDKcCCwJAAkACQCAELQAAQb5/ag4SAJEBkQGRAZEBkQGRAZEBkQGRAQGRAZEBkQGRAZEBkQECkQELIARBAWohBEGkASEQDI8CCyAEQQFqIQRBpwEhEAyOAgsgBEEBaiEEQagBIRAMjQILAkAgBCACRw0AQb4BIRAMpgILIAQtAABBzgBHDY0BIARBAWohBAzPAQsCQCAEIAJHDQBBvwEhEAylAgsCQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAELQAAQb9/ag4VAAECA5wBBAUGnAGcAZwBBwgJCgucAQwNDg+cAQsgBEEBaiEBQegAIRAMmgILIARBAWohAUHpACEQDJkCCyAEQQFqIQFB7gAhEAyYAgsgBEEBaiEBQfIAIRAMlwILIARBAWohAUHzACEQDJYCCyAEQQFqIQFB9gAhEAyVAgsgBEEBaiEBQfcAIRAMlAILIARBAWohAUH6ACEQDJMCCyAEQQFqIQRBgwEhEAySAgsgBEEBaiEEQYQBIRAMkQILIARBAWohBEGFASEQDJACCyAEQQFqIQRBkgEhEAyPAgsgBEEBaiEEQZgBIRAMjgILIARBAWohBEGgASEQDI0CCyAEQQFqIQRBowEhEAyMAgsgBEEBaiEEQaoBIRAMiwILAkAgBCACRg0AIABBkICAgAA2AgggACAENgIEQasBIRAMiwILQcABIRAMowILIAAgBSACEKqAgIAAIgENiwEgBSEBDFwLAkAgBiACRg0AIAZBAWohBQyNAQtBwgEhEAyhAgsDQAJAIBAtAABBdmoOBIwBAACPAQALIBBBAWoiECACRw0AC0HDASEQDKACCwJAIAcgAkYNACAAQZGAgIAANgIIIAAgBzYCBCAHIQFBASEQDIcCC0HEASEQDJ8CCwJAIAcgAkcNAEHFASEQDJ8CCwJAAkAgBy0AAEF2ag4EAc4BzgEAzgELIAdBAWohBgyNAQsgB0EBaiEFDIkBCwJAIAcgAkcNAEHGASEQDJ4CCwJAAkAgBy0AAEF2ag4XAY8BjwEBjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BAI8BCyAHQQFqIQcLQbABIRAMhAILAkAgCCACRw0AQcgBIRAMnQILIAgtAABBIEcNjQEgAEEAOwEyIAhBAWohAUGzASEQDIMCCyABIRcCQANAIBciByACRg0BIActAABBUGpB/wFxIhBBCk8NzAECQCAALwEyIhRBmTNLDQAgACAUQQpsIhQ7ATIgEEH//wNzIBRB/v8DcUkNACAHQQFqIRcgACAUIBBqIhA7ATIgEEH//wNxQegHSQ0BCwtBACEQIABBADYCHCAAQcGJgIAANgIQIABBDTYCDCAAIAdBAWo2AhQMnAILQccBIRAMmwILIAAgCCACEK6AgIAAIhBFDcoBIBBBFUcNjAEgAEHIATYCHCAAIAg2AhQgAEHJl4CAADYCECAAQRU2AgxBACEQDJoCCwJAIAkgAkcNAEHMASEQDJoCC0EAIRRBASEXQQEhFkEAIRACQAJAAkACQAJAAkACQAJAAkAgCS0AAEFQag4KlgGVAQABAgMEBQYIlwELQQIhEAwGC0EDIRAMBQtBBCEQDAQLQQUhEAwDC0EGIRAMAgtBByEQDAELQQghEAtBACEXQQAhFkEAIRQMjgELQQkhEEEBIRRBACEXQQAhFgyNAQsCQCAKIAJHDQBBzgEhEAyZAgsgCi0AAEEuRw2OASAKQQFqIQkMygELIAsgAkcNjgFB0AEhEAyXAgsCQCALIAJGDQAgAEGOgICAADYCCCAAIAs2AgRBtwEhEAz+AQtB0QEhEAyWAgsCQCAEIAJHDQBB0gEhEAyWAgsgAiAEayAAKAIAIhBqIRQgBCAQa0EEaiELA0AgBC0AACAQQfzPgIAAai0AAEcNjgEgEEEERg3pASAQQQFqIRAgBEEBaiIEIAJHDQALIAAgFDYCAEHSASEQDJUCCyAAIAwgAhCsgICAACIBDY0BIAwhAQy4AQsCQCAEIAJHDQBB1AEhEAyUAgsgAiAEayAAKAIAIhBqIRQgBCAQa0EBaiEMA0AgBC0AACAQQYHQgIAAai0AAEcNjwEgEEEBRg2OASAQQQFqIRAgBEEBaiIEIAJHDQALIAAgFDYCAEHUASEQDJMCCwJAIAQgAkcNAEHWASEQDJMCCyACIARrIAAoAgAiEGohFCAEIBBrQQJqIQsDQCAELQAAIBBBg9CAgABqLQAARw2OASAQQQJGDZABIBBBAWohECAEQQFqIgQgAkcNAAsgACAUNgIAQdYBIRAMkgILAkAgBCACRw0AQdcBIRAMkgILAkACQCAELQAAQbt/ag4QAI8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwEBjwELIARBAWohBEG7ASEQDPkBCyAEQQFqIQRBvAEhEAz4AQsCQCAEIAJHDQBB2AEhEAyRAgsgBC0AAEHIAEcNjAEgBEEBaiEEDMQBCwJAIAQgAkYNACAAQZCAgIAANgIIIAAgBDYCBEG+ASEQDPcBC0HZASEQDI8CCwJAIAQgAkcNAEHaASEQDI8CCyAELQAAQcgARg3DASAAQQE6ACgMuQELIABBAjoALyAAIAQgAhCmgICAACIQDY0BQcIBIRAM9AELIAAtAChBf2oOArcBuQG4AQsDQAJAIAQtAABBdmoOBACOAY4BAI4BCyAEQQFqIgQgAkcNAAtB3QEhEAyLAgsgAEEAOgAvIAAtAC1BBHFFDYQCCyAAQQA6AC8gAEEBOgA0IAEhAQyMAQsgEEEVRg3aASAAQQA2AhwgACABNgIUIABBp46AgAA2AhAgAEESNgIMQQAhEAyIAgsCQCAAIBAgAhC0gICAACIEDQAgECEBDIECCwJAIARBFUcNACAAQQM2AhwgACAQNgIUIABBsJiAgAA2AhAgAEEVNgIMQQAhEAyIAgsgAEEANgIcIAAgEDYCFCAAQaeOgIAANgIQIABBEjYCDEEAIRAMhwILIBBBFUYN1gEgAEEANgIcIAAgATYCFCAAQdqNgIAANgIQIABBFDYCDEEAIRAMhgILIAAoAgQhFyAAQQA2AgQgECARp2oiFiEBIAAgFyAQIBYgFBsiEBC1gICAACIURQ2NASAAQQc2AhwgACAQNgIUIAAgFDYCDEEAIRAMhQILIAAgAC8BMEGAAXI7ATAgASEBC0EqIRAM6gELIBBBFUYN0QEgAEEANgIcIAAgATYCFCAAQYOMgIAANgIQIABBEzYCDEEAIRAMggILIBBBFUYNzwEgAEEANgIcIAAgATYCFCAAQZqPgIAANgIQIABBIjYCDEEAIRAMgQILIAAoAgQhECAAQQA2AgQCQCAAIBAgARC3gICAACIQDQAgAUEBaiEBDI0BCyAAQQw2AhwgACAQNgIMIAAgAUEBajYCFEEAIRAMgAILIBBBFUYNzAEgAEEANgIcIAAgATYCFCAAQZqPgIAANgIQIABBIjYCDEEAIRAM/wELIAAoAgQhECAAQQA2AgQCQCAAIBAgARC3gICAACIQDQAgAUEBaiEBDIwBCyAAQQ02AhwgACAQNgIMIAAgAUEBajYCFEEAIRAM/gELIBBBFUYNyQEgAEEANgIcIAAgATYCFCAAQcaMgIAANgIQIABBIzYCDEEAIRAM/QELIAAoAgQhECAAQQA2AgQCQCAAIBAgARC5gICAACIQDQAgAUEBaiEBDIsBCyAAQQ42AhwgACAQNgIMIAAgAUEBajYCFEEAIRAM/AELIABBADYCHCAAIAE2AhQgAEHAlYCAADYCECAAQQI2AgxBACEQDPsBCyAQQRVGDcUBIABBADYCHCAAIAE2AhQgAEHGjICAADYCECAAQSM2AgxBACEQDPoBCyAAQRA2AhwgACABNgIUIAAgEDYCDEEAIRAM+QELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARC5gICAACIEDQAgAUEBaiEBDPEBCyAAQRE2AhwgACAENgIMIAAgAUEBajYCFEEAIRAM+AELIBBBFUYNwQEgAEEANgIcIAAgATYCFCAAQcaMgIAANgIQIABBIzYCDEEAIRAM9wELIAAoAgQhECAAQQA2AgQCQCAAIBAgARC5gICAACIQDQAgAUEBaiEBDIgBCyAAQRM2AhwgACAQNgIMIAAgAUEBajYCFEEAIRAM9gELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARC5gICAACIEDQAgAUEBaiEBDO0BCyAAQRQ2AhwgACAENgIMIAAgAUEBajYCFEEAIRAM9QELIBBBFUYNvQEgAEEANgIcIAAgATYCFCAAQZqPgIAANgIQIABBIjYCDEEAIRAM9AELIAAoAgQhECAAQQA2AgQCQCAAIBAgARC3gICAACIQDQAgAUEBaiEBDIYBCyAAQRY2AhwgACAQNgIMIAAgAUEBajYCFEEAIRAM8wELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARC3gICAACIEDQAgAUEBaiEBDOkBCyAAQRc2AhwgACAENgIMIAAgAUEBajYCFEEAIRAM8gELIABBADYCHCAAIAE2AhQgAEHNk4CAADYCECAAQQw2AgxBACEQDPEBC0IBIRELIBBBAWohAQJAIAApAyAiEkL//////////w9WDQAgACASQgSGIBGENwMgIAEhAQyEAQsgAEEANgIcIAAgATYCFCAAQa2JgIAANgIQIABBDDYCDEEAIRAM7wELIABBADYCHCAAIBA2AhQgAEHNk4CAADYCECAAQQw2AgxBACEQDO4BCyAAKAIEIRcgAEEANgIEIBAgEadqIhYhASAAIBcgECAWIBQbIhAQtYCAgAAiFEUNcyAAQQU2AhwgACAQNgIUIAAgFDYCDEEAIRAM7QELIABBADYCHCAAIBA2AhQgAEGqnICAADYCECAAQQ82AgxBACEQDOwBCyAAIBAgAhC0gICAACIBDQEgECEBC0EOIRAM0QELAkAgAUEVRw0AIABBAjYCHCAAIBA2AhQgAEGwmICAADYCECAAQRU2AgxBACEQDOoBCyAAQQA2AhwgACAQNgIUIABBp46AgAA2AhAgAEESNgIMQQAhEAzpAQsgAUEBaiEQAkAgAC8BMCIBQYABcUUNAAJAIAAgECACELuAgIAAIgENACAQIQEMcAsgAUEVRw26ASAAQQU2AhwgACAQNgIUIABB+ZeAgAA2AhAgAEEVNgIMQQAhEAzpAQsCQCABQaAEcUGgBEcNACAALQAtQQJxDQAgAEEANgIcIAAgEDYCFCAAQZaTgIAANgIQIABBBDYCDEEAIRAM6QELIAAgECACEL2AgIAAGiAQIQECQAJAAkACQAJAIAAgECACELOAgIAADhYCAQAEBAQEBAQEBAQEBAQEBAQEBAQDBAsgAEEBOgAuCyAAIAAvATBBwAByOwEwIBAhAQtBJiEQDNEBCyAAQSM2AhwgACAQNgIUIABBpZaAgAA2AhAgAEEVNgIMQQAhEAzpAQsgAEEANgIcIAAgEDYCFCAAQdWLgIAANgIQIABBETYCDEEAIRAM6AELIAAtAC1BAXFFDQFBwwEhEAzOAQsCQCANIAJGDQADQAJAIA0tAABBIEYNACANIQEMxAELIA1BAWoiDSACRw0AC0ElIRAM5wELQSUhEAzmAQsgACgCBCEEIABBADYCBCAAIAQgDRCvgICAACIERQ2tASAAQSY2AhwgACAENgIMIAAgDUEBajYCFEEAIRAM5QELIBBBFUYNqwEgAEEANgIcIAAgATYCFCAAQf2NgIAANgIQIABBHTYCDEEAIRAM5AELIABBJzYCHCAAIAE2AhQgACAQNgIMQQAhEAzjAQsgECEBQQEhFAJAAkACQAJAAkACQAJAIAAtACxBfmoOBwYFBQMBAgAFCyAAIAAvATBBCHI7ATAMAwtBAiEUDAELQQQhFAsgAEEBOgAsIAAgAC8BMCAUcjsBMAsgECEBC0ErIRAMygELIABBADYCHCAAIBA2AhQgAEGrkoCAADYCECAAQQs2AgxBACEQDOIBCyAAQQA2AhwgACABNgIUIABB4Y+AgAA2AhAgAEEKNgIMQQAhEAzhAQsgAEEAOgAsIBAhAQy9AQsgECEBQQEhFAJAAkACQAJAAkAgAC0ALEF7ag4EAwECAAULIAAgAC8BMEEIcjsBMAwDC0ECIRQMAQtBBCEUCyAAQQE6ACwgACAALwEwIBRyOwEwCyAQIQELQSkhEAzFAQsgAEEANgIcIAAgATYCFCAAQfCUgIAANgIQIABBAzYCDEEAIRAM3QELAkAgDi0AAEENRw0AIAAoAgQhASAAQQA2AgQCQCAAIAEgDhCxgICAACIBDQAgDkEBaiEBDHULIABBLDYCHCAAIAE2AgwgACAOQQFqNgIUQQAhEAzdAQsgAC0ALUEBcUUNAUHEASEQDMMBCwJAIA4gAkcNAEEtIRAM3AELAkACQANAAkAgDi0AAEF2ag4EAgAAAwALIA5BAWoiDiACRw0AC0EtIRAM3QELIAAoAgQhASAAQQA2AgQCQCAAIAEgDhCxgICAACIBDQAgDiEBDHQLIABBLDYCHCAAIA42AhQgACABNgIMQQAhEAzcAQsgACgCBCEBIABBADYCBAJAIAAgASAOELGAgIAAIgENACAOQQFqIQEMcwsgAEEsNgIcIAAgATYCDCAAIA5BAWo2AhRBACEQDNsBCyAAKAIEIQQgAEEANgIEIAAgBCAOELGAgIAAIgQNoAEgDiEBDM4BCyAQQSxHDQEgAUEBaiEQQQEhAQJAAkACQAJAAkAgAC0ALEF7ag4EAwECBAALIBAhAQwEC0ECIQEMAQtBBCEBCyAAQQE6ACwgACAALwEwIAFyOwEwIBAhAQwBCyAAIAAvATBBCHI7ATAgECEBC0E5IRAMvwELIABBADoALCABIQELQTQhEAy9AQsgACAALwEwQSByOwEwIAEhAQwCCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQsYCAgAAiBA0AIAEhAQzHAQsgAEE3NgIcIAAgATYCFCAAIAQ2AgxBACEQDNQBCyAAQQg6ACwgASEBC0EwIRAMuQELAkAgAC0AKEEBRg0AIAEhAQwECyAALQAtQQhxRQ2TASABIQEMAwsgAC0AMEEgcQ2UAUHFASEQDLcBCwJAIA8gAkYNAAJAA0ACQCAPLQAAQVBqIgFB/wFxQQpJDQAgDyEBQTUhEAy6AQsgACkDICIRQpmz5syZs+bMGVYNASAAIBFCCn4iETcDICARIAGtQv8BgyISQn+FVg0BIAAgESASfDcDICAPQQFqIg8gAkcNAAtBOSEQDNEBCyAAKAIEIQIgAEEANgIEIAAgAiAPQQFqIgQQsYCAgAAiAg2VASAEIQEMwwELQTkhEAzPAQsCQCAALwEwIgFBCHFFDQAgAC0AKEEBRw0AIAAtAC1BCHFFDZABCyAAIAFB9/sDcUGABHI7ATAgDyEBC0E3IRAMtAELIAAgAC8BMEEQcjsBMAyrAQsgEEEVRg2LASAAQQA2AhwgACABNgIUIABB8I6AgAA2AhAgAEEcNgIMQQAhEAzLAQsgAEHDADYCHCAAIAE2AgwgACANQQFqNgIUQQAhEAzKAQsCQCABLQAAQTpHDQAgACgCBCEQIABBADYCBAJAIAAgECABEK+AgIAAIhANACABQQFqIQEMYwsgAEHDADYCHCAAIBA2AgwgACABQQFqNgIUQQAhEAzKAQsgAEEANgIcIAAgATYCFCAAQbGRgIAANgIQIABBCjYCDEEAIRAMyQELIABBADYCHCAAIAE2AhQgAEGgmYCAADYCECAAQR42AgxBACEQDMgBCyAAQQA2AgALIABBgBI7ASogACAXQQFqIgEgAhCogICAACIQDQEgASEBC0HHACEQDKwBCyAQQRVHDYMBIABB0QA2AhwgACABNgIUIABB45eAgAA2AhAgAEEVNgIMQQAhEAzEAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMXgsgAEHSADYCHCAAIAE2AhQgACAQNgIMQQAhEAzDAQsgAEEANgIcIAAgFDYCFCAAQcGogIAANgIQIABBBzYCDCAAQQA2AgBBACEQDMIBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxdCyAAQdMANgIcIAAgATYCFCAAIBA2AgxBACEQDMEBC0EAIRAgAEEANgIcIAAgATYCFCAAQYCRgIAANgIQIABBCTYCDAzAAQsgEEEVRg19IABBADYCHCAAIAE2AhQgAEGUjYCAADYCECAAQSE2AgxBACEQDL8BC0EBIRZBACEXQQAhFEEBIRALIAAgEDoAKyABQQFqIQECQAJAIAAtAC1BEHENAAJAAkACQCAALQAqDgMBAAIECyAWRQ0DDAILIBQNAQwCCyAXRQ0BCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQrYCAgAAiEA0AIAEhAQxcCyAAQdgANgIcIAAgATYCFCAAIBA2AgxBACEQDL4BCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQrYCAgAAiBA0AIAEhAQytAQsgAEHZADYCHCAAIAE2AhQgACAENgIMQQAhEAy9AQsgACgCBCEEIABBADYCBAJAIAAgBCABEK2AgIAAIgQNACABIQEMqwELIABB2gA2AhwgACABNgIUIAAgBDYCDEEAIRAMvAELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARCtgICAACIEDQAgASEBDKkBCyAAQdwANgIcIAAgATYCFCAAIAQ2AgxBACEQDLsBCwJAIAEtAABBUGoiEEH/AXFBCk8NACAAIBA6ACogAUEBaiEBQc8AIRAMogELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARCtgICAACIEDQAgASEBDKcBCyAAQd4ANgIcIAAgATYCFCAAIAQ2AgxBACEQDLoBCyAAQQA2AgAgF0EBaiEBAkAgAC0AKUEjTw0AIAEhAQxZCyAAQQA2AhwgACABNgIUIABB04mAgAA2AhAgAEEINgIMQQAhEAy5AQsgAEEANgIAC0EAIRAgAEEANgIcIAAgATYCFCAAQZCzgIAANgIQIABBCDYCDAy3AQsgAEEANgIAIBdBAWohAQJAIAAtAClBIUcNACABIQEMVgsgAEEANgIcIAAgATYCFCAAQZuKgIAANgIQIABBCDYCDEEAIRAMtgELIABBADYCACAXQQFqIQECQCAALQApIhBBXWpBC08NACABIQEMVQsCQCAQQQZLDQBBASAQdEHKAHFFDQAgASEBDFULQQAhECAAQQA2AhwgACABNgIUIABB94mAgAA2AhAgAEEINgIMDLUBCyAQQRVGDXEgAEEANgIcIAAgATYCFCAAQbmNgIAANgIQIABBGjYCDEEAIRAMtAELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDFQLIABB5QA2AhwgACABNgIUIAAgEDYCDEEAIRAMswELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDE0LIABB0gA2AhwgACABNgIUIAAgEDYCDEEAIRAMsgELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDE0LIABB0wA2AhwgACABNgIUIAAgEDYCDEEAIRAMsQELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDFELIABB5QA2AhwgACABNgIUIAAgEDYCDEEAIRAMsAELIABBADYCHCAAIAE2AhQgAEHGioCAADYCECAAQQc2AgxBACEQDK8BCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxJCyAAQdIANgIcIAAgATYCFCAAIBA2AgxBACEQDK4BCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxJCyAAQdMANgIcIAAgATYCFCAAIBA2AgxBACEQDK0BCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxNCyAAQeUANgIcIAAgATYCFCAAIBA2AgxBACEQDKwBCyAAQQA2AhwgACABNgIUIABB3IiAgAA2AhAgAEEHNgIMQQAhEAyrAQsgEEE/Rw0BIAFBAWohAQtBBSEQDJABC0EAIRAgAEEANgIcIAAgATYCFCAAQf2SgIAANgIQIABBBzYCDAyoAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMQgsgAEHSADYCHCAAIAE2AhQgACAQNgIMQQAhEAynAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMQgsgAEHTADYCHCAAIAE2AhQgACAQNgIMQQAhEAymAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMRgsgAEHlADYCHCAAIAE2AhQgACAQNgIMQQAhEAylAQsgACgCBCEBIABBADYCBAJAIAAgASAUEKeAgIAAIgENACAUIQEMPwsgAEHSADYCHCAAIBQ2AhQgACABNgIMQQAhEAykAQsgACgCBCEBIABBADYCBAJAIAAgASAUEKeAgIAAIgENACAUIQEMPwsgAEHTADYCHCAAIBQ2AhQgACABNgIMQQAhEAyjAQsgACgCBCEBIABBADYCBAJAIAAgASAUEKeAgIAAIgENACAUIQEMQwsgAEHlADYCHCAAIBQ2AhQgACABNgIMQQAhEAyiAQsgAEEANgIcIAAgFDYCFCAAQcOPgIAANgIQIABBBzYCDEEAIRAMoQELIABBADYCHCAAIAE2AhQgAEHDj4CAADYCECAAQQc2AgxBACEQDKABC0EAIRAgAEEANgIcIAAgFDYCFCAAQYycgIAANgIQIABBBzYCDAyfAQsgAEEANgIcIAAgFDYCFCAAQYycgIAANgIQIABBBzYCDEEAIRAMngELIABBADYCHCAAIBQ2AhQgAEH+kYCAADYCECAAQQc2AgxBACEQDJ0BCyAAQQA2AhwgACABNgIUIABBjpuAgAA2AhAgAEEGNgIMQQAhEAycAQsgEEEVRg1XIABBADYCHCAAIAE2AhQgAEHMjoCAADYCECAAQSA2AgxBACEQDJsBCyAAQQA2AgAgEEEBaiEBQSQhEAsgACAQOgApIAAoAgQhECAAQQA2AgQgACAQIAEQq4CAgAAiEA1UIAEhAQw+CyAAQQA2AgALQQAhECAAQQA2AhwgACAENgIUIABB8ZuAgAA2AhAgAEEGNgIMDJcBCyABQRVGDVAgAEEANgIcIAAgBTYCFCAAQfCMgIAANgIQIABBGzYCDEEAIRAMlgELIAAoAgQhBSAAQQA2AgQgACAFIBAQqYCAgAAiBQ0BIBBBAWohBQtBrQEhEAx7CyAAQcEBNgIcIAAgBTYCDCAAIBBBAWo2AhRBACEQDJMBCyAAKAIEIQYgAEEANgIEIAAgBiAQEKmAgIAAIgYNASAQQQFqIQYLQa4BIRAMeAsgAEHCATYCHCAAIAY2AgwgACAQQQFqNgIUQQAhEAyQAQsgAEEANgIcIAAgBzYCFCAAQZeLgIAANgIQIABBDTYCDEEAIRAMjwELIABBADYCHCAAIAg2AhQgAEHjkICAADYCECAAQQk2AgxBACEQDI4BCyAAQQA2AhwgACAINgIUIABBlI2AgAA2AhAgAEEhNgIMQQAhEAyNAQtBASEWQQAhF0EAIRRBASEQCyAAIBA6ACsgCUEBaiEIAkACQCAALQAtQRBxDQACQAJAAkAgAC0AKg4DAQACBAsgFkUNAwwCCyAUDQEMAgsgF0UNAQsgACgCBCEQIABBADYCBCAAIBAgCBCtgICAACIQRQ09IABByQE2AhwgACAINgIUIAAgEDYCDEEAIRAMjAELIAAoAgQhBCAAQQA2AgQgACAEIAgQrYCAgAAiBEUNdiAAQcoBNgIcIAAgCDYCFCAAIAQ2AgxBACEQDIsBCyAAKAIEIQQgAEEANgIEIAAgBCAJEK2AgIAAIgRFDXQgAEHLATYCHCAAIAk2AhQgACAENgIMQQAhEAyKAQsgACgCBCEEIABBADYCBCAAIAQgChCtgICAACIERQ1yIABBzQE2AhwgACAKNgIUIAAgBDYCDEEAIRAMiQELAkAgCy0AAEFQaiIQQf8BcUEKTw0AIAAgEDoAKiALQQFqIQpBtgEhEAxwCyAAKAIEIQQgAEEANgIEIAAgBCALEK2AgIAAIgRFDXAgAEHPATYCHCAAIAs2AhQgACAENgIMQQAhEAyIAQsgAEEANgIcIAAgBDYCFCAAQZCzgIAANgIQIABBCDYCDCAAQQA2AgBBACEQDIcBCyABQRVGDT8gAEEANgIcIAAgDDYCFCAAQcyOgIAANgIQIABBIDYCDEEAIRAMhgELIABBgQQ7ASggACgCBCEQIABCADcDACAAIBAgDEEBaiIMEKuAgIAAIhBFDTggAEHTATYCHCAAIAw2AhQgACAQNgIMQQAhEAyFAQsgAEEANgIAC0EAIRAgAEEANgIcIAAgBDYCFCAAQdibgIAANgIQIABBCDYCDAyDAQsgACgCBCEQIABCADcDACAAIBAgC0EBaiILEKuAgIAAIhANAUHGASEQDGkLIABBAjoAKAxVCyAAQdUBNgIcIAAgCzYCFCAAIBA2AgxBACEQDIABCyAQQRVGDTcgAEEANgIcIAAgBDYCFCAAQaSMgIAANgIQIABBEDYCDEEAIRAMfwsgAC0ANEEBRw00IAAgBCACELyAgIAAIhBFDTQgEEEVRw01IABB3AE2AhwgACAENgIUIABB1ZaAgAA2AhAgAEEVNgIMQQAhEAx+C0EAIRAgAEEANgIcIABBr4uAgAA2AhAgAEECNgIMIAAgFEEBajYCFAx9C0EAIRAMYwtBAiEQDGILQQ0hEAxhC0EPIRAMYAtBJSEQDF8LQRMhEAxeC0EVIRAMXQtBFiEQDFwLQRchEAxbC0EYIRAMWgtBGSEQDFkLQRohEAxYC0EbIRAMVwtBHCEQDFYLQR0hEAxVC0EfIRAMVAtBISEQDFMLQSMhEAxSC0HGACEQDFELQS4hEAxQC0EvIRAMTwtBOyEQDE4LQT0hEAxNC0HIACEQDEwLQckAIRAMSwtBywAhEAxKC0HMACEQDEkLQc4AIRAMSAtB0QAhEAxHC0HVACEQDEYLQdgAIRAMRQtB2QAhEAxEC0HbACEQDEMLQeQAIRAMQgtB5QAhEAxBC0HxACEQDEALQfQAIRAMPwtBjQEhEAw+C0GXASEQDD0LQakBIRAMPAtBrAEhEAw7C0HAASEQDDoLQbkBIRAMOQtBrwEhEAw4C0GxASEQDDcLQbIBIRAMNgtBtAEhEAw1C0G1ASEQDDQLQboBIRAMMwtBvQEhEAwyC0G/ASEQDDELQcEBIRAMMAsgAEEANgIcIAAgBDYCFCAAQemLgIAANgIQIABBHzYCDEEAIRAMSAsgAEHbATYCHCAAIAQ2AhQgAEH6loCAADYCECAAQRU2AgxBACEQDEcLIABB+AA2AhwgACAMNgIUIABBypiAgAA2AhAgAEEVNgIMQQAhEAxGCyAAQdEANgIcIAAgBTYCFCAAQbCXgIAANgIQIABBFTYCDEEAIRAMRQsgAEH5ADYCHCAAIAE2AhQgACAQNgIMQQAhEAxECyAAQfgANgIcIAAgATYCFCAAQcqYgIAANgIQIABBFTYCDEEAIRAMQwsgAEHkADYCHCAAIAE2AhQgAEHjl4CAADYCECAAQRU2AgxBACEQDEILIABB1wA2AhwgACABNgIUIABByZeAgAA2AhAgAEEVNgIMQQAhEAxBCyAAQQA2AhwgACABNgIUIABBuY2AgAA2AhAgAEEaNgIMQQAhEAxACyAAQcIANgIcIAAgATYCFCAAQeOYgIAANgIQIABBFTYCDEEAIRAMPwsgAEEANgIEIAAgDyAPELGAgIAAIgRFDQEgAEE6NgIcIAAgBDYCDCAAIA9BAWo2AhRBACEQDD4LIAAoAgQhBCAAQQA2AgQCQCAAIAQgARCxgICAACIERQ0AIABBOzYCHCAAIAQ2AgwgACABQQFqNgIUQQAhEAw+CyABQQFqIQEMLQsgD0EBaiEBDC0LIABBADYCHCAAIA82AhQgAEHkkoCAADYCECAAQQQ2AgxBACEQDDsLIABBNjYCHCAAIAQ2AhQgACACNgIMQQAhEAw6CyAAQS42AhwgACAONgIUIAAgBDYCDEEAIRAMOQsgAEHQADYCHCAAIAE2AhQgAEGRmICAADYCECAAQRU2AgxBACEQDDgLIA1BAWohAQwsCyAAQRU2AhwgACABNgIUIABBgpmAgAA2AhAgAEEVNgIMQQAhEAw2CyAAQRs2AhwgACABNgIUIABBkZeAgAA2AhAgAEEVNgIMQQAhEAw1CyAAQQ82AhwgACABNgIUIABBkZeAgAA2AhAgAEEVNgIMQQAhEAw0CyAAQQs2AhwgACABNgIUIABBkZeAgAA2AhAgAEEVNgIMQQAhEAwzCyAAQRo2AhwgACABNgIUIABBgpmAgAA2AhAgAEEVNgIMQQAhEAwyCyAAQQs2AhwgACABNgIUIABBgpmAgAA2AhAgAEEVNgIMQQAhEAwxCyAAQQo2AhwgACABNgIUIABB5JaAgAA2AhAgAEEVNgIMQQAhEAwwCyAAQR42AhwgACABNgIUIABB+ZeAgAA2AhAgAEEVNgIMQQAhEAwvCyAAQQA2AhwgACAQNgIUIABB2o2AgAA2AhAgAEEUNgIMQQAhEAwuCyAAQQQ2AhwgACABNgIUIABBsJiAgAA2AhAgAEEVNgIMQQAhEAwtCyAAQQA2AgAgC0EBaiELC0G4ASEQDBILIABBADYCACAQQQFqIQFB9QAhEAwRCyABIQECQCAALQApQQVHDQBB4wAhEAwRC0HiACEQDBALQQAhECAAQQA2AhwgAEHkkYCAADYCECAAQQc2AgwgACAUQQFqNgIUDCgLIABBADYCACAXQQFqIQFBwAAhEAwOC0EBIQELIAAgAToALCAAQQA2AgAgF0EBaiEBC0EoIRAMCwsgASEBC0E4IRAMCQsCQCABIg8gAkYNAANAAkAgDy0AAEGAvoCAAGotAAAiAUEBRg0AIAFBAkcNAyAPQQFqIQEMBAsgD0EBaiIPIAJHDQALQT4hEAwiC0E+IRAMIQsgAEEAOgAsIA8hAQwBC0ELIRAMBgtBOiEQDAULIAFBAWohAUEtIRAMBAsgACABOgAsIABBADYCACAWQQFqIQFBDCEQDAMLIABBADYCACAXQQFqIQFBCiEQDAILIABBADYCAAsgAEEAOgAsIA0hAUEJIRAMAAsLQQAhECAAQQA2AhwgACALNgIUIABBzZCAgAA2AhAgAEEJNgIMDBcLQQAhECAAQQA2AhwgACAKNgIUIABB6YqAgAA2AhAgAEEJNgIMDBYLQQAhECAAQQA2AhwgACAJNgIUIABBt5CAgAA2AhAgAEEJNgIMDBULQQAhECAAQQA2AhwgACAINgIUIABBnJGAgAA2AhAgAEEJNgIMDBQLQQAhECAAQQA2AhwgACABNgIUIABBzZCAgAA2AhAgAEEJNgIMDBMLQQAhECAAQQA2AhwgACABNgIUIABB6YqAgAA2AhAgAEEJNgIMDBILQQAhECAAQQA2AhwgACABNgIUIABBt5CAgAA2AhAgAEEJNgIMDBELQQAhECAAQQA2AhwgACABNgIUIABBnJGAgAA2AhAgAEEJNgIMDBALQQAhECAAQQA2AhwgACABNgIUIABBl5WAgAA2AhAgAEEPNgIMDA8LQQAhECAAQQA2AhwgACABNgIUIABBl5WAgAA2AhAgAEEPNgIMDA4LQQAhECAAQQA2AhwgACABNgIUIABBwJKAgAA2AhAgAEELNgIMDA0LQQAhECAAQQA2AhwgACABNgIUIABBlYmAgAA2AhAgAEELNgIMDAwLQQAhECAAQQA2AhwgACABNgIUIABB4Y+AgAA2AhAgAEEKNgIMDAsLQQAhECAAQQA2AhwgACABNgIUIABB+4+AgAA2AhAgAEEKNgIMDAoLQQAhECAAQQA2AhwgACABNgIUIABB8ZmAgAA2AhAgAEECNgIMDAkLQQAhECAAQQA2AhwgACABNgIUIABBxJSAgAA2AhAgAEECNgIMDAgLQQAhECAAQQA2AhwgACABNgIUIABB8pWAgAA2AhAgAEECNgIMDAcLIABBAjYCHCAAIAE2AhQgAEGcmoCAADYCECAAQRY2AgxBACEQDAYLQQEhEAwFC0HUACEQIAEiBCACRg0EIANBCGogACAEIAJB2MKAgABBChDFgICAACADKAIMIQQgAygCCA4DAQQCAAsQyoCAgAAACyAAQQA2AhwgAEG1moCAADYCECAAQRc2AgwgACAEQQFqNgIUQQAhEAwCCyAAQQA2AhwgACAENgIUIABBypqAgAA2AhAgAEEJNgIMQQAhEAwBCwJAIAEiBCACRw0AQSIhEAwBCyAAQYmAgIAANgIIIAAgBDYCBEEhIRALIANBEGokgICAgAAgEAuvAQECfyABKAIAIQYCQAJAIAIgA0YNACAEIAZqIQQgBiADaiACayEHIAIgBkF/cyAFaiIGaiEFA0ACQCACLQAAIAQtAABGDQBBAiEEDAMLAkAgBg0AQQAhBCAFIQIMAwsgBkF/aiEGIARBAWohBCACQQFqIgIgA0cNAAsgByEGIAMhAgsgAEEBNgIAIAEgBjYCACAAIAI2AgQPCyABQQA2AgAgACAENgIAIAAgAjYCBAsKACAAEMeAgIAAC/I2AQt/I4CAgIAAQRBrIgEkgICAgAACQEEAKAKg0ICAAA0AQQAQy4CAgABBgNSEgABrIgJB2QBJDQBBACEDAkBBACgC4NOAgAAiBA0AQQBCfzcC7NOAgABBAEKAgISAgIDAADcC5NOAgABBACABQQhqQXBxQdiq1aoFcyIENgLg04CAAEEAQQA2AvTTgIAAQQBBADYCxNOAgAALQQAgAjYCzNOAgABBAEGA1ISAADYCyNOAgABBAEGA1ISAADYCmNCAgABBACAENgKs0ICAAEEAQX82AqjQgIAAA0AgA0HE0ICAAGogA0G40ICAAGoiBDYCACAEIANBsNCAgABqIgU2AgAgA0G80ICAAGogBTYCACADQczQgIAAaiADQcDQgIAAaiIFNgIAIAUgBDYCACADQdTQgIAAaiADQcjQgIAAaiIENgIAIAQgBTYCACADQdDQgIAAaiAENgIAIANBIGoiA0GAAkcNAAtBgNSEgABBeEGA1ISAAGtBD3FBAEGA1ISAAEEIakEPcRsiA2oiBEEEaiACQUhqIgUgA2siA0EBcjYCAEEAQQAoAvDTgIAANgKk0ICAAEEAIAM2ApTQgIAAQQAgBDYCoNCAgABBgNSEgAAgBWpBODYCBAsCQAJAAkACQAJAAkACQAJAAkACQAJAAkAgAEHsAUsNAAJAQQAoAojQgIAAIgZBECAAQRNqQXBxIABBC0kbIgJBA3YiBHYiA0EDcUUNAAJAAkAgA0EBcSAEckEBcyIFQQN0IgRBsNCAgABqIgMgBEG40ICAAGooAgAiBCgCCCICRw0AQQAgBkF+IAV3cTYCiNCAgAAMAQsgAyACNgIIIAIgAzYCDAsgBEEIaiEDIAQgBUEDdCIFQQNyNgIEIAQgBWoiBCAEKAIEQQFyNgIEDAwLIAJBACgCkNCAgAAiB00NAQJAIANFDQACQAJAIAMgBHRBAiAEdCIDQQAgA2tycSIDQQAgA2txQX9qIgMgA0EMdkEQcSIDdiIEQQV2QQhxIgUgA3IgBCAFdiIDQQJ2QQRxIgRyIAMgBHYiA0EBdkECcSIEciADIAR2IgNBAXZBAXEiBHIgAyAEdmoiBEEDdCIDQbDQgIAAaiIFIANBuNCAgABqKAIAIgMoAggiAEcNAEEAIAZBfiAEd3EiBjYCiNCAgAAMAQsgBSAANgIIIAAgBTYCDAsgAyACQQNyNgIEIAMgBEEDdCIEaiAEIAJrIgU2AgAgAyACaiIAIAVBAXI2AgQCQCAHRQ0AIAdBeHFBsNCAgABqIQJBACgCnNCAgAAhBAJAAkAgBkEBIAdBA3Z0IghxDQBBACAGIAhyNgKI0ICAACACIQgMAQsgAigCCCEICyAIIAQ2AgwgAiAENgIIIAQgAjYCDCAEIAg2AggLIANBCGohA0EAIAA2ApzQgIAAQQAgBTYCkNCAgAAMDAtBACgCjNCAgAAiCUUNASAJQQAgCWtxQX9qIgMgA0EMdkEQcSIDdiIEQQV2QQhxIgUgA3IgBCAFdiIDQQJ2QQRxIgRyIAMgBHYiA0EBdkECcSIEciADIAR2IgNBAXZBAXEiBHIgAyAEdmpBAnRBuNKAgABqKAIAIgAoAgRBeHEgAmshBCAAIQUCQANAAkAgBSgCECIDDQAgBUEUaigCACIDRQ0CCyADKAIEQXhxIAJrIgUgBCAFIARJIgUbIQQgAyAAIAUbIQAgAyEFDAALCyAAKAIYIQoCQCAAKAIMIgggAEYNACAAKAIIIgNBACgCmNCAgABJGiAIIAM2AgggAyAINgIMDAsLAkAgAEEUaiIFKAIAIgMNACAAKAIQIgNFDQMgAEEQaiEFCwNAIAUhCyADIghBFGoiBSgCACIDDQAgCEEQaiEFIAgoAhAiAw0ACyALQQA2AgAMCgtBfyECIABBv39LDQAgAEETaiIDQXBxIQJBACgCjNCAgAAiB0UNAEEAIQsCQCACQYACSQ0AQR8hCyACQf///wdLDQAgA0EIdiIDIANBgP4/akEQdkEIcSIDdCIEIARBgOAfakEQdkEEcSIEdCIFIAVBgIAPakEQdkECcSIFdEEPdiADIARyIAVyayIDQQF0IAIgA0EVanZBAXFyQRxqIQsLQQAgAmshBAJAAkACQAJAIAtBAnRBuNKAgABqKAIAIgUNAEEAIQNBACEIDAELQQAhAyACQQBBGSALQQF2ayALQR9GG3QhAEEAIQgDQAJAIAUoAgRBeHEgAmsiBiAETw0AIAYhBCAFIQggBg0AQQAhBCAFIQggBSEDDAMLIAMgBUEUaigCACIGIAYgBSAAQR12QQRxakEQaigCACIFRhsgAyAGGyEDIABBAXQhACAFDQALCwJAIAMgCHINAEEAIQhBAiALdCIDQQAgA2tyIAdxIgNFDQMgA0EAIANrcUF/aiIDIANBDHZBEHEiA3YiBUEFdkEIcSIAIANyIAUgAHYiA0ECdkEEcSIFciADIAV2IgNBAXZBAnEiBXIgAyAFdiIDQQF2QQFxIgVyIAMgBXZqQQJ0QbjSgIAAaigCACEDCyADRQ0BCwNAIAMoAgRBeHEgAmsiBiAESSEAAkAgAygCECIFDQAgA0EUaigCACEFCyAGIAQgABshBCADIAggABshCCAFIQMgBQ0ACwsgCEUNACAEQQAoApDQgIAAIAJrTw0AIAgoAhghCwJAIAgoAgwiACAIRg0AIAgoAggiA0EAKAKY0ICAAEkaIAAgAzYCCCADIAA2AgwMCQsCQCAIQRRqIgUoAgAiAw0AIAgoAhAiA0UNAyAIQRBqIQULA0AgBSEGIAMiAEEUaiIFKAIAIgMNACAAQRBqIQUgACgCECIDDQALIAZBADYCAAwICwJAQQAoApDQgIAAIgMgAkkNAEEAKAKc0ICAACEEAkACQCADIAJrIgVBEEkNACAEIAJqIgAgBUEBcjYCBEEAIAU2ApDQgIAAQQAgADYCnNCAgAAgBCADaiAFNgIAIAQgAkEDcjYCBAwBCyAEIANBA3I2AgQgBCADaiIDIAMoAgRBAXI2AgRBAEEANgKc0ICAAEEAQQA2ApDQgIAACyAEQQhqIQMMCgsCQEEAKAKU0ICAACIAIAJNDQBBACgCoNCAgAAiAyACaiIEIAAgAmsiBUEBcjYCBEEAIAU2ApTQgIAAQQAgBDYCoNCAgAAgAyACQQNyNgIEIANBCGohAwwKCwJAAkBBACgC4NOAgABFDQBBACgC6NOAgAAhBAwBC0EAQn83AuzTgIAAQQBCgICEgICAwAA3AuTTgIAAQQAgAUEMakFwcUHYqtWqBXM2AuDTgIAAQQBBADYC9NOAgABBAEEANgLE04CAAEGAgAQhBAtBACEDAkAgBCACQccAaiIHaiIGQQAgBGsiC3EiCCACSw0AQQBBMDYC+NOAgAAMCgsCQEEAKALA04CAACIDRQ0AAkBBACgCuNOAgAAiBCAIaiIFIARNDQAgBSADTQ0BC0EAIQNBAEEwNgL404CAAAwKC0EALQDE04CAAEEEcQ0EAkACQAJAQQAoAqDQgIAAIgRFDQBByNOAgAAhAwNAAkAgAygCACIFIARLDQAgBSADKAIEaiAESw0DCyADKAIIIgMNAAsLQQAQy4CAgAAiAEF/Rg0FIAghBgJAQQAoAuTTgIAAIgNBf2oiBCAAcUUNACAIIABrIAQgAGpBACADa3FqIQYLIAYgAk0NBSAGQf7///8HSw0FAkBBACgCwNOAgAAiA0UNAEEAKAK404CAACIEIAZqIgUgBE0NBiAFIANLDQYLIAYQy4CAgAAiAyAARw0BDAcLIAYgAGsgC3EiBkH+////B0sNBCAGEMuAgIAAIgAgAygCACADKAIEakYNAyAAIQMLAkAgA0F/Rg0AIAJByABqIAZNDQACQCAHIAZrQQAoAujTgIAAIgRqQQAgBGtxIgRB/v///wdNDQAgAyEADAcLAkAgBBDLgICAAEF/Rg0AIAQgBmohBiADIQAMBwtBACAGaxDLgICAABoMBAsgAyEAIANBf0cNBQwDC0EAIQgMBwtBACEADAULIABBf0cNAgtBAEEAKALE04CAAEEEcjYCxNOAgAALIAhB/v///wdLDQEgCBDLgICAACEAQQAQy4CAgAAhAyAAQX9GDQEgA0F/Rg0BIAAgA08NASADIABrIgYgAkE4ak0NAQtBAEEAKAK404CAACAGaiIDNgK404CAAAJAIANBACgCvNOAgABNDQBBACADNgK804CAAAsCQAJAAkACQEEAKAKg0ICAACIERQ0AQcjTgIAAIQMDQCAAIAMoAgAiBSADKAIEIghqRg0CIAMoAggiAw0ADAMLCwJAAkBBACgCmNCAgAAiA0UNACAAIANPDQELQQAgADYCmNCAgAALQQAhA0EAIAY2AszTgIAAQQAgADYCyNOAgABBAEF/NgKo0ICAAEEAQQAoAuDTgIAANgKs0ICAAEEAQQA2AtTTgIAAA0AgA0HE0ICAAGogA0G40ICAAGoiBDYCACAEIANBsNCAgABqIgU2AgAgA0G80ICAAGogBTYCACADQczQgIAAaiADQcDQgIAAaiIFNgIAIAUgBDYCACADQdTQgIAAaiADQcjQgIAAaiIENgIAIAQgBTYCACADQdDQgIAAaiAENgIAIANBIGoiA0GAAkcNAAsgAEF4IABrQQ9xQQAgAEEIakEPcRsiA2oiBCAGQUhqIgUgA2siA0EBcjYCBEEAQQAoAvDTgIAANgKk0ICAAEEAIAM2ApTQgIAAQQAgBDYCoNCAgAAgACAFakE4NgIEDAILIAMtAAxBCHENACAEIAVJDQAgBCAATw0AIARBeCAEa0EPcUEAIARBCGpBD3EbIgVqIgBBACgClNCAgAAgBmoiCyAFayIFQQFyNgIEIAMgCCAGajYCBEEAQQAoAvDTgIAANgKk0ICAAEEAIAU2ApTQgIAAQQAgADYCoNCAgAAgBCALakE4NgIEDAELAkAgAEEAKAKY0ICAACIITw0AQQAgADYCmNCAgAAgACEICyAAIAZqIQVByNOAgAAhAwJAAkACQAJAAkACQAJAA0AgAygCACAFRg0BIAMoAggiAw0ADAILCyADLQAMQQhxRQ0BC0HI04CAACEDA0ACQCADKAIAIgUgBEsNACAFIAMoAgRqIgUgBEsNAwsgAygCCCEDDAALCyADIAA2AgAgAyADKAIEIAZqNgIEIABBeCAAa0EPcUEAIABBCGpBD3EbaiILIAJBA3I2AgQgBUF4IAVrQQ9xQQAgBUEIakEPcRtqIgYgCyACaiICayEDAkAgBiAERw0AQQAgAjYCoNCAgABBAEEAKAKU0ICAACADaiIDNgKU0ICAACACIANBAXI2AgQMAwsCQCAGQQAoApzQgIAARw0AQQAgAjYCnNCAgABBAEEAKAKQ0ICAACADaiIDNgKQ0ICAACACIANBAXI2AgQgAiADaiADNgIADAMLAkAgBigCBCIEQQNxQQFHDQAgBEF4cSEHAkACQCAEQf8BSw0AIAYoAggiBSAEQQN2IghBA3RBsNCAgABqIgBGGgJAIAYoAgwiBCAFRw0AQQBBACgCiNCAgABBfiAId3E2AojQgIAADAILIAQgAEYaIAQgBTYCCCAFIAQ2AgwMAQsgBigCGCEJAkACQCAGKAIMIgAgBkYNACAGKAIIIgQgCEkaIAAgBDYCCCAEIAA2AgwMAQsCQCAGQRRqIgQoAgAiBQ0AIAZBEGoiBCgCACIFDQBBACEADAELA0AgBCEIIAUiAEEUaiIEKAIAIgUNACAAQRBqIQQgACgCECIFDQALIAhBADYCAAsgCUUNAAJAAkAgBiAGKAIcIgVBAnRBuNKAgABqIgQoAgBHDQAgBCAANgIAIAANAUEAQQAoAozQgIAAQX4gBXdxNgKM0ICAAAwCCyAJQRBBFCAJKAIQIAZGG2ogADYCACAARQ0BCyAAIAk2AhgCQCAGKAIQIgRFDQAgACAENgIQIAQgADYCGAsgBigCFCIERQ0AIABBFGogBDYCACAEIAA2AhgLIAcgA2ohAyAGIAdqIgYoAgQhBAsgBiAEQX5xNgIEIAIgA2ogAzYCACACIANBAXI2AgQCQCADQf8BSw0AIANBeHFBsNCAgABqIQQCQAJAQQAoAojQgIAAIgVBASADQQN2dCIDcQ0AQQAgBSADcjYCiNCAgAAgBCEDDAELIAQoAgghAwsgAyACNgIMIAQgAjYCCCACIAQ2AgwgAiADNgIIDAMLQR8hBAJAIANB////B0sNACADQQh2IgQgBEGA/j9qQRB2QQhxIgR0IgUgBUGA4B9qQRB2QQRxIgV0IgAgAEGAgA9qQRB2QQJxIgB0QQ92IAQgBXIgAHJrIgRBAXQgAyAEQRVqdkEBcXJBHGohBAsgAiAENgIcIAJCADcCECAEQQJ0QbjSgIAAaiEFAkBBACgCjNCAgAAiAEEBIAR0IghxDQAgBSACNgIAQQAgACAIcjYCjNCAgAAgAiAFNgIYIAIgAjYCCCACIAI2AgwMAwsgA0EAQRkgBEEBdmsgBEEfRht0IQQgBSgCACEAA0AgACIFKAIEQXhxIANGDQIgBEEddiEAIARBAXQhBCAFIABBBHFqQRBqIggoAgAiAA0ACyAIIAI2AgAgAiAFNgIYIAIgAjYCDCACIAI2AggMAgsgAEF4IABrQQ9xQQAgAEEIakEPcRsiA2oiCyAGQUhqIgggA2siA0EBcjYCBCAAIAhqQTg2AgQgBCAFQTcgBWtBD3FBACAFQUlqQQ9xG2pBQWoiCCAIIARBEGpJGyIIQSM2AgRBAEEAKALw04CAADYCpNCAgABBACADNgKU0ICAAEEAIAs2AqDQgIAAIAhBEGpBACkC0NOAgAA3AgAgCEEAKQLI04CAADcCCEEAIAhBCGo2AtDTgIAAQQAgBjYCzNOAgABBACAANgLI04CAAEEAQQA2AtTTgIAAIAhBJGohAwNAIANBBzYCACADQQRqIgMgBUkNAAsgCCAERg0DIAggCCgCBEF+cTYCBCAIIAggBGsiADYCACAEIABBAXI2AgQCQCAAQf8BSw0AIABBeHFBsNCAgABqIQMCQAJAQQAoAojQgIAAIgVBASAAQQN2dCIAcQ0AQQAgBSAAcjYCiNCAgAAgAyEFDAELIAMoAgghBQsgBSAENgIMIAMgBDYCCCAEIAM2AgwgBCAFNgIIDAQLQR8hAwJAIABB////B0sNACAAQQh2IgMgA0GA/j9qQRB2QQhxIgN0IgUgBUGA4B9qQRB2QQRxIgV0IgggCEGAgA9qQRB2QQJxIgh0QQ92IAMgBXIgCHJrIgNBAXQgACADQRVqdkEBcXJBHGohAwsgBCADNgIcIARCADcCECADQQJ0QbjSgIAAaiEFAkBBACgCjNCAgAAiCEEBIAN0IgZxDQAgBSAENgIAQQAgCCAGcjYCjNCAgAAgBCAFNgIYIAQgBDYCCCAEIAQ2AgwMBAsgAEEAQRkgA0EBdmsgA0EfRht0IQMgBSgCACEIA0AgCCIFKAIEQXhxIABGDQMgA0EddiEIIANBAXQhAyAFIAhBBHFqQRBqIgYoAgAiCA0ACyAGIAQ2AgAgBCAFNgIYIAQgBDYCDCAEIAQ2AggMAwsgBSgCCCIDIAI2AgwgBSACNgIIIAJBADYCGCACIAU2AgwgAiADNgIICyALQQhqIQMMBQsgBSgCCCIDIAQ2AgwgBSAENgIIIARBADYCGCAEIAU2AgwgBCADNgIIC0EAKAKU0ICAACIDIAJNDQBBACgCoNCAgAAiBCACaiIFIAMgAmsiA0EBcjYCBEEAIAM2ApTQgIAAQQAgBTYCoNCAgAAgBCACQQNyNgIEIARBCGohAwwDC0EAIQNBAEEwNgL404CAAAwCCwJAIAtFDQACQAJAIAggCCgCHCIFQQJ0QbjSgIAAaiIDKAIARw0AIAMgADYCACAADQFBACAHQX4gBXdxIgc2AozQgIAADAILIAtBEEEUIAsoAhAgCEYbaiAANgIAIABFDQELIAAgCzYCGAJAIAgoAhAiA0UNACAAIAM2AhAgAyAANgIYCyAIQRRqKAIAIgNFDQAgAEEUaiADNgIAIAMgADYCGAsCQAJAIARBD0sNACAIIAQgAmoiA0EDcjYCBCAIIANqIgMgAygCBEEBcjYCBAwBCyAIIAJqIgAgBEEBcjYCBCAIIAJBA3I2AgQgACAEaiAENgIAAkAgBEH/AUsNACAEQXhxQbDQgIAAaiEDAkACQEEAKAKI0ICAACIFQQEgBEEDdnQiBHENAEEAIAUgBHI2AojQgIAAIAMhBAwBCyADKAIIIQQLIAQgADYCDCADIAA2AgggACADNgIMIAAgBDYCCAwBC0EfIQMCQCAEQf///wdLDQAgBEEIdiIDIANBgP4/akEQdkEIcSIDdCIFIAVBgOAfakEQdkEEcSIFdCICIAJBgIAPakEQdkECcSICdEEPdiADIAVyIAJyayIDQQF0IAQgA0EVanZBAXFyQRxqIQMLIAAgAzYCHCAAQgA3AhAgA0ECdEG40oCAAGohBQJAIAdBASADdCICcQ0AIAUgADYCAEEAIAcgAnI2AozQgIAAIAAgBTYCGCAAIAA2AgggACAANgIMDAELIARBAEEZIANBAXZrIANBH0YbdCEDIAUoAgAhAgJAA0AgAiIFKAIEQXhxIARGDQEgA0EddiECIANBAXQhAyAFIAJBBHFqQRBqIgYoAgAiAg0ACyAGIAA2AgAgACAFNgIYIAAgADYCDCAAIAA2AggMAQsgBSgCCCIDIAA2AgwgBSAANgIIIABBADYCGCAAIAU2AgwgACADNgIICyAIQQhqIQMMAQsCQCAKRQ0AAkACQCAAIAAoAhwiBUECdEG40oCAAGoiAygCAEcNACADIAg2AgAgCA0BQQAgCUF+IAV3cTYCjNCAgAAMAgsgCkEQQRQgCigCECAARhtqIAg2AgAgCEUNAQsgCCAKNgIYAkAgACgCECIDRQ0AIAggAzYCECADIAg2AhgLIABBFGooAgAiA0UNACAIQRRqIAM2AgAgAyAINgIYCwJAAkAgBEEPSw0AIAAgBCACaiIDQQNyNgIEIAAgA2oiAyADKAIEQQFyNgIEDAELIAAgAmoiBSAEQQFyNgIEIAAgAkEDcjYCBCAFIARqIAQ2AgACQCAHRQ0AIAdBeHFBsNCAgABqIQJBACgCnNCAgAAhAwJAAkBBASAHQQN2dCIIIAZxDQBBACAIIAZyNgKI0ICAACACIQgMAQsgAigCCCEICyAIIAM2AgwgAiADNgIIIAMgAjYCDCADIAg2AggLQQAgBTYCnNCAgABBACAENgKQ0ICAAAsgAEEIaiEDCyABQRBqJICAgIAAIAMLCgAgABDJgICAAAviDQEHfwJAIABFDQAgAEF4aiIBIABBfGooAgAiAkF4cSIAaiEDAkAgAkEBcQ0AIAJBA3FFDQEgASABKAIAIgJrIgFBACgCmNCAgAAiBEkNASACIABqIQACQCABQQAoApzQgIAARg0AAkAgAkH/AUsNACABKAIIIgQgAkEDdiIFQQN0QbDQgIAAaiIGRhoCQCABKAIMIgIgBEcNAEEAQQAoAojQgIAAQX4gBXdxNgKI0ICAAAwDCyACIAZGGiACIAQ2AgggBCACNgIMDAILIAEoAhghBwJAAkAgASgCDCIGIAFGDQAgASgCCCICIARJGiAGIAI2AgggAiAGNgIMDAELAkAgAUEUaiICKAIAIgQNACABQRBqIgIoAgAiBA0AQQAhBgwBCwNAIAIhBSAEIgZBFGoiAigCACIEDQAgBkEQaiECIAYoAhAiBA0ACyAFQQA2AgALIAdFDQECQAJAIAEgASgCHCIEQQJ0QbjSgIAAaiICKAIARw0AIAIgBjYCACAGDQFBAEEAKAKM0ICAAEF+IAR3cTYCjNCAgAAMAwsgB0EQQRQgBygCECABRhtqIAY2AgAgBkUNAgsgBiAHNgIYAkAgASgCECICRQ0AIAYgAjYCECACIAY2AhgLIAEoAhQiAkUNASAGQRRqIAI2AgAgAiAGNgIYDAELIAMoAgQiAkEDcUEDRw0AIAMgAkF+cTYCBEEAIAA2ApDQgIAAIAEgAGogADYCACABIABBAXI2AgQPCyABIANPDQAgAygCBCICQQFxRQ0AAkACQCACQQJxDQACQCADQQAoAqDQgIAARw0AQQAgATYCoNCAgABBAEEAKAKU0ICAACAAaiIANgKU0ICAACABIABBAXI2AgQgAUEAKAKc0ICAAEcNA0EAQQA2ApDQgIAAQQBBADYCnNCAgAAPCwJAIANBACgCnNCAgABHDQBBACABNgKc0ICAAEEAQQAoApDQgIAAIABqIgA2ApDQgIAAIAEgAEEBcjYCBCABIABqIAA2AgAPCyACQXhxIABqIQACQAJAIAJB/wFLDQAgAygCCCIEIAJBA3YiBUEDdEGw0ICAAGoiBkYaAkAgAygCDCICIARHDQBBAEEAKAKI0ICAAEF+IAV3cTYCiNCAgAAMAgsgAiAGRhogAiAENgIIIAQgAjYCDAwBCyADKAIYIQcCQAJAIAMoAgwiBiADRg0AIAMoAggiAkEAKAKY0ICAAEkaIAYgAjYCCCACIAY2AgwMAQsCQCADQRRqIgIoAgAiBA0AIANBEGoiAigCACIEDQBBACEGDAELA0AgAiEFIAQiBkEUaiICKAIAIgQNACAGQRBqIQIgBigCECIEDQALIAVBADYCAAsgB0UNAAJAAkAgAyADKAIcIgRBAnRBuNKAgABqIgIoAgBHDQAgAiAGNgIAIAYNAUEAQQAoAozQgIAAQX4gBHdxNgKM0ICAAAwCCyAHQRBBFCAHKAIQIANGG2ogBjYCACAGRQ0BCyAGIAc2AhgCQCADKAIQIgJFDQAgBiACNgIQIAIgBjYCGAsgAygCFCICRQ0AIAZBFGogAjYCACACIAY2AhgLIAEgAGogADYCACABIABBAXI2AgQgAUEAKAKc0ICAAEcNAUEAIAA2ApDQgIAADwsgAyACQX5xNgIEIAEgAGogADYCACABIABBAXI2AgQLAkAgAEH/AUsNACAAQXhxQbDQgIAAaiECAkACQEEAKAKI0ICAACIEQQEgAEEDdnQiAHENAEEAIAQgAHI2AojQgIAAIAIhAAwBCyACKAIIIQALIAAgATYCDCACIAE2AgggASACNgIMIAEgADYCCA8LQR8hAgJAIABB////B0sNACAAQQh2IgIgAkGA/j9qQRB2QQhxIgJ0IgQgBEGA4B9qQRB2QQRxIgR0IgYgBkGAgA9qQRB2QQJxIgZ0QQ92IAIgBHIgBnJrIgJBAXQgACACQRVqdkEBcXJBHGohAgsgASACNgIcIAFCADcCECACQQJ0QbjSgIAAaiEEAkACQEEAKAKM0ICAACIGQQEgAnQiA3ENACAEIAE2AgBBACAGIANyNgKM0ICAACABIAQ2AhggASABNgIIIAEgATYCDAwBCyAAQQBBGSACQQF2ayACQR9GG3QhAiAEKAIAIQYCQANAIAYiBCgCBEF4cSAARg0BIAJBHXYhBiACQQF0IQIgBCAGQQRxakEQaiIDKAIAIgYNAAsgAyABNgIAIAEgBDYCGCABIAE2AgwgASABNgIIDAELIAQoAggiACABNgIMIAQgATYCCCABQQA2AhggASAENgIMIAEgADYCCAtBAEEAKAKo0ICAAEF/aiIBQX8gARs2AqjQgIAACwsEAAAAC04AAkAgAA0APwBBEHQPCwJAIABB//8DcQ0AIABBf0wNAAJAIABBEHZAACIAQX9HDQBBAEEwNgL404CAAEF/DwsgAEEQdA8LEMqAgIAAAAvyAgIDfwF+AkAgAkUNACAAIAE6AAAgAiAAaiIDQX9qIAE6AAAgAkEDSQ0AIAAgAToAAiAAIAE6AAEgA0F9aiABOgAAIANBfmogAToAACACQQdJDQAgACABOgADIANBfGogAToAACACQQlJDQAgAEEAIABrQQNxIgRqIgMgAUH/AXFBgYKECGwiATYCACADIAIgBGtBfHEiBGoiAkF8aiABNgIAIARBCUkNACADIAE2AgggAyABNgIEIAJBeGogATYCACACQXRqIAE2AgAgBEEZSQ0AIAMgATYCGCADIAE2AhQgAyABNgIQIAMgATYCDCACQXBqIAE2AgAgAkFsaiABNgIAIAJBaGogATYCACACQWRqIAE2AgAgBCADQQRxQRhyIgVrIgJBIEkNACABrUKBgICAEH4hBiADIAVqIQEDQCABIAY3AxggASAGNwMQIAEgBjcDCCABIAY3AwAgAUEgaiEBIAJBYGoiAkEfSw0ACwsgAAsLjkgBAEGACAuGSAEAAAACAAAAAwAAAAAAAAAAAAAABAAAAAUAAAAAAAAAAAAAAAYAAAAHAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASW52YWxpZCBjaGFyIGluIHVybCBxdWVyeQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2JvZHkAQ29udGVudC1MZW5ndGggb3ZlcmZsb3cAQ2h1bmsgc2l6ZSBvdmVyZmxvdwBSZXNwb25zZSBvdmVyZmxvdwBJbnZhbGlkIG1ldGhvZCBmb3IgSFRUUC94LnggcmVxdWVzdABJbnZhbGlkIG1ldGhvZCBmb3IgUlRTUC94LnggcmVxdWVzdABFeHBlY3RlZCBTT1VSQ0UgbWV0aG9kIGZvciBJQ0UveC54IHJlcXVlc3QASW52YWxpZCBjaGFyIGluIHVybCBmcmFnbWVudCBzdGFydABFeHBlY3RlZCBkb3QAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9zdGF0dXMASW52YWxpZCByZXNwb25zZSBzdGF0dXMASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucwBVc2VyIGNhbGxiYWNrIGVycm9yAGBvbl9yZXNldGAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2hlYWRlcmAgY2FsbGJhY2sgZXJyb3IAYG9uX21lc3NhZ2VfYmVnaW5gIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19leHRlbnNpb25fdmFsdWVgIGNhbGxiYWNrIGVycm9yAGBvbl9zdGF0dXNfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl92ZXJzaW9uX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdXJsX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWV0aG9kX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX25hbWVgIGNhbGxiYWNrIGVycm9yAFVuZXhwZWN0ZWQgY2hhciBpbiB1cmwgc2VydmVyAEludmFsaWQgaGVhZGVyIHZhbHVlIGNoYXIASW52YWxpZCBoZWFkZXIgZmllbGQgY2hhcgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3ZlcnNpb24ASW52YWxpZCBtaW5vciB2ZXJzaW9uAEludmFsaWQgbWFqb3IgdmVyc2lvbgBFeHBlY3RlZCBzcGFjZSBhZnRlciB2ZXJzaW9uAEV4cGVjdGVkIENSTEYgYWZ0ZXIgdmVyc2lvbgBJbnZhbGlkIEhUVFAgdmVyc2lvbgBJbnZhbGlkIGhlYWRlciB0b2tlbgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3VybABJbnZhbGlkIGNoYXJhY3RlcnMgaW4gdXJsAFVuZXhwZWN0ZWQgc3RhcnQgY2hhciBpbiB1cmwARG91YmxlIEAgaW4gdXJsAEVtcHR5IENvbnRlbnQtTGVuZ3RoAEludmFsaWQgY2hhcmFjdGVyIGluIENvbnRlbnQtTGVuZ3RoAER1cGxpY2F0ZSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXIgaW4gdXJsIHBhdGgAQ29udGVudC1MZW5ndGggY2FuJ3QgYmUgcHJlc2VudCB3aXRoIFRyYW5zZmVyLUVuY29kaW5nAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIHNpemUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfdmFsdWUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyB2YWx1ZQBNaXNzaW5nIGV4cGVjdGVkIExGIGFmdGVyIGhlYWRlciB2YWx1ZQBJbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AgaGVhZGVyIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGUgdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBxdW90ZWQgdmFsdWUAUGF1c2VkIGJ5IG9uX2hlYWRlcnNfY29tcGxldGUASW52YWxpZCBFT0Ygc3RhdGUAb25fcmVzZXQgcGF1c2UAb25fY2h1bmtfaGVhZGVyIHBhdXNlAG9uX21lc3NhZ2VfYmVnaW4gcGF1c2UAb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlIHBhdXNlAG9uX3N0YXR1c19jb21wbGV0ZSBwYXVzZQBvbl92ZXJzaW9uX2NvbXBsZXRlIHBhdXNlAG9uX3VybF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19jb21wbGV0ZSBwYXVzZQBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGUgcGF1c2UAb25fbWVzc2FnZV9jb21wbGV0ZSBwYXVzZQBvbl9tZXRob2RfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lIHBhdXNlAFVuZXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgc3RhcnQgbGluZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgbmFtZQBQYXVzZSBvbiBDT05ORUNUL1VwZ3JhZGUAUGF1c2Ugb24gUFJJL1VwZ3JhZGUARXhwZWN0ZWQgSFRUUC8yIENvbm5lY3Rpb24gUHJlZmFjZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX21ldGhvZABFeHBlY3RlZCBzcGFjZSBhZnRlciBtZXRob2QAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfZmllbGQAUGF1c2VkAEludmFsaWQgd29yZCBlbmNvdW50ZXJlZABJbnZhbGlkIG1ldGhvZCBlbmNvdW50ZXJlZABVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNjaGVtYQBSZXF1ZXN0IGhhcyBpbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AAU1dJVENIX1BST1hZAFVTRV9QUk9YWQBNS0FDVElWSVRZAFVOUFJPQ0VTU0FCTEVfRU5USVRZAENPUFkATU9WRURfUEVSTUFORU5UTFkAVE9PX0VBUkxZAE5PVElGWQBGQUlMRURfREVQRU5ERU5DWQBCQURfR0FURVdBWQBQTEFZAFBVVABDSEVDS09VVABHQVRFV0FZX1RJTUVPVVQAUkVRVUVTVF9USU1FT1VUAE5FVFdPUktfQ09OTkVDVF9USU1FT1VUAENPTk5FQ1RJT05fVElNRU9VVABMT0dJTl9USU1FT1VUAE5FVFdPUktfUkVBRF9USU1FT1VUAFBPU1QATUlTRElSRUNURURfUkVRVUVTVABDTElFTlRfQ0xPU0VEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9MT0FEX0JBTEFOQ0VEX1JFUVVFU1QAQkFEX1JFUVVFU1QASFRUUF9SRVFVRVNUX1NFTlRfVE9fSFRUUFNfUE9SVABSRVBPUlQASU1fQV9URUFQT1QAUkVTRVRfQ09OVEVOVABOT19DT05URU5UAFBBUlRJQUxfQ09OVEVOVABIUEVfSU5WQUxJRF9DT05TVEFOVABIUEVfQ0JfUkVTRVQAR0VUAEhQRV9TVFJJQ1QAQ09ORkxJQ1QAVEVNUE9SQVJZX1JFRElSRUNUAFBFUk1BTkVOVF9SRURJUkVDVABDT05ORUNUAE1VTFRJX1NUQVRVUwBIUEVfSU5WQUxJRF9TVEFUVVMAVE9PX01BTllfUkVRVUVTVFMARUFSTFlfSElOVFMAVU5BVkFJTEFCTEVfRk9SX0xFR0FMX1JFQVNPTlMAT1BUSU9OUwBTV0lUQ0hJTkdfUFJPVE9DT0xTAFZBUklBTlRfQUxTT19ORUdPVElBVEVTAE1VTFRJUExFX0NIT0lDRVMASU5URVJOQUxfU0VSVkVSX0VSUk9SAFdFQl9TRVJWRVJfVU5LTk9XTl9FUlJPUgBSQUlMR1VOX0VSUk9SAElERU5USVRZX1BST1ZJREVSX0FVVEhFTlRJQ0FUSU9OX0VSUk9SAFNTTF9DRVJUSUZJQ0FURV9FUlJPUgBJTlZBTElEX1hfRk9SV0FSREVEX0ZPUgBTRVRfUEFSQU1FVEVSAEdFVF9QQVJBTUVURVIASFBFX1VTRVIAU0VFX09USEVSAEhQRV9DQl9DSFVOS19IRUFERVIATUtDQUxFTkRBUgBTRVRVUABXRUJfU0VSVkVSX0lTX0RPV04AVEVBUkRPV04ASFBFX0NMT1NFRF9DT05ORUNUSU9OAEhFVVJJU1RJQ19FWFBJUkFUSU9OAERJU0NPTk5FQ1RFRF9PUEVSQVRJT04ATk9OX0FVVEhPUklUQVRJVkVfSU5GT1JNQVRJT04ASFBFX0lOVkFMSURfVkVSU0lPTgBIUEVfQ0JfTUVTU0FHRV9CRUdJTgBTSVRFX0lTX0ZST1pFTgBIUEVfSU5WQUxJRF9IRUFERVJfVE9LRU4ASU5WQUxJRF9UT0tFTgBGT1JCSURERU4ARU5IQU5DRV9ZT1VSX0NBTE0ASFBFX0lOVkFMSURfVVJMAEJMT0NLRURfQllfUEFSRU5UQUxfQ09OVFJPTABNS0NPTABBQ0wASFBFX0lOVEVSTkFMAFJFUVVFU1RfSEVBREVSX0ZJRUxEU19UT09fTEFSR0VfVU5PRkZJQ0lBTABIUEVfT0sAVU5MSU5LAFVOTE9DSwBQUkkAUkVUUllfV0lUSABIUEVfSU5WQUxJRF9DT05URU5UX0xFTkdUSABIUEVfVU5FWFBFQ1RFRF9DT05URU5UX0xFTkdUSABGTFVTSABQUk9QUEFUQ0gATS1TRUFSQ0gAVVJJX1RPT19MT05HAFBST0NFU1NJTkcATUlTQ0VMTEFORU9VU19QRVJTSVNURU5UX1dBUk5JTkcATUlTQ0VMTEFORU9VU19XQVJOSU5HAEhQRV9JTlZBTElEX1RSQU5TRkVSX0VOQ09ESU5HAEV4cGVjdGVkIENSTEYASFBFX0lOVkFMSURfQ0hVTktfU0laRQBNT1ZFAENPTlRJTlVFAEhQRV9DQl9TVEFUVVNfQ09NUExFVEUASFBFX0NCX0hFQURFUlNfQ09NUExFVEUASFBFX0NCX1ZFUlNJT05fQ09NUExFVEUASFBFX0NCX1VSTF9DT01QTEVURQBIUEVfQ0JfQ0hVTktfQ09NUExFVEUASFBFX0NCX0hFQURFUl9WQUxVRV9DT01QTEVURQBIUEVfQ0JfQ0hVTktfRVhURU5TSU9OX1ZBTFVFX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19FWFRFTlNJT05fTkFNRV9DT01QTEVURQBIUEVfQ0JfTUVTU0FHRV9DT01QTEVURQBIUEVfQ0JfTUVUSE9EX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJfRklFTERfQ09NUExFVEUAREVMRVRFAEhQRV9JTlZBTElEX0VPRl9TVEFURQBJTlZBTElEX1NTTF9DRVJUSUZJQ0FURQBQQVVTRQBOT19SRVNQT05TRQBVTlNVUFBPUlRFRF9NRURJQV9UWVBFAEdPTkUATk9UX0FDQ0VQVEFCTEUAU0VSVklDRV9VTkFWQUlMQUJMRQBSQU5HRV9OT1RfU0FUSVNGSUFCTEUAT1JJR0lOX0lTX1VOUkVBQ0hBQkxFAFJFU1BPTlNFX0lTX1NUQUxFAFBVUkdFAE1FUkdFAFJFUVVFU1RfSEVBREVSX0ZJRUxEU19UT09fTEFSR0UAUkVRVUVTVF9IRUFERVJfVE9PX0xBUkdFAFBBWUxPQURfVE9PX0xBUkdFAElOU1VGRklDSUVOVF9TVE9SQUdFAEhQRV9QQVVTRURfVVBHUkFERQBIUEVfUEFVU0VEX0gyX1VQR1JBREUAU09VUkNFAEFOTk9VTkNFAFRSQUNFAEhQRV9VTkVYUEVDVEVEX1NQQUNFAERFU0NSSUJFAFVOU1VCU0NSSUJFAFJFQ09SRABIUEVfSU5WQUxJRF9NRVRIT0QATk9UX0ZPVU5EAFBST1BGSU5EAFVOQklORABSRUJJTkQAVU5BVVRIT1JJWkVEAE1FVEhPRF9OT1RfQUxMT1dFRABIVFRQX1ZFUlNJT05fTk9UX1NVUFBPUlRFRABBTFJFQURZX1JFUE9SVEVEAEFDQ0VQVEVEAE5PVF9JTVBMRU1FTlRFRABMT09QX0RFVEVDVEVEAEhQRV9DUl9FWFBFQ1RFRABIUEVfTEZfRVhQRUNURUQAQ1JFQVRFRABJTV9VU0VEAEhQRV9QQVVTRUQAVElNRU9VVF9PQ0NVUkVEAFBBWU1FTlRfUkVRVUlSRUQAUFJFQ09ORElUSU9OX1JFUVVJUkVEAFBST1hZX0FVVEhFTlRJQ0FUSU9OX1JFUVVJUkVEAE5FVFdPUktfQVVUSEVOVElDQVRJT05fUkVRVUlSRUQATEVOR1RIX1JFUVVJUkVEAFNTTF9DRVJUSUZJQ0FURV9SRVFVSVJFRABVUEdSQURFX1JFUVVJUkVEAFBBR0VfRVhQSVJFRABQUkVDT05ESVRJT05fRkFJTEVEAEVYUEVDVEFUSU9OX0ZBSUxFRABSRVZBTElEQVRJT05fRkFJTEVEAFNTTF9IQU5EU0hBS0VfRkFJTEVEAExPQ0tFRABUUkFOU0ZPUk1BVElPTl9BUFBMSUVEAE5PVF9NT0RJRklFRABOT1RfRVhURU5ERUQAQkFORFdJRFRIX0xJTUlUX0VYQ0VFREVEAFNJVEVfSVNfT1ZFUkxPQURFRABIRUFEAEV4cGVjdGVkIEhUVFAvAABeEwAAJhMAADAQAADwFwAAnRMAABUSAAA5FwAA8BIAAAoQAAB1EgAArRIAAIITAABPFAAAfxAAAKAVAAAjFAAAiRIAAIsUAABNFQAA1BEAAM8UAAAQGAAAyRYAANwWAADBEQAA4BcAALsUAAB0FAAAfBUAAOUUAAAIFwAAHxAAAGUVAACjFAAAKBUAAAIVAACZFQAALBAAAIsZAABPDwAA1A4AAGoQAADOEAAAAhcAAIkOAABuEwAAHBMAAGYUAABWFwAAwRMAAM0TAABsEwAAaBcAAGYXAABfFwAAIhMAAM4PAABpDgAA2A4AAGMWAADLEwAAqg4AACgXAAAmFwAAxRMAAF0WAADoEQAAZxMAAGUTAADyFgAAcxMAAB0XAAD5FgAA8xEAAM8OAADOFQAADBIAALMRAAClEQAAYRAAADIXAAC7EwAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAgEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgMCAgICAgAAAgIAAgIAAgICAgICAgICAgAEAAAAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAgICAAIAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAgICAgIAAAICAAICAAICAgICAgICAgIAAwAEAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgIAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgICAgACAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABsb3NlZWVwLWFsaXZlAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAgEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQFjaHVua2VkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQABAQEBAQAAAQEAAQEAAQEBAQEBAQEBAQAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGVjdGlvbmVudC1sZW5ndGhvbnJveHktY29ubmVjdGlvbgAAAAAAAAAAAAAAAAAAAHJhbnNmZXItZW5jb2RpbmdwZ3JhZGUNCg0KDQpTTQ0KDQpUVFAvQ0UvVFNQLwAAAAAAAAAAAAAAAAECAAEDAAAAAAAAAAAAAAAAAAAAAAAABAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAABAgABAwAAAAAAAAAAAAAAAAAAAAAAAAQBAQUBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAQAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAABAAACAAAAAAAAAAAAAAAAAAAAAAAAAwQAAAQEBAQEBAQEBAQEBQQEBAQEBAQEBAQEBAAEAAYHBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQABAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAgAAAAACAAAAAAAAAAAAAAAAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE5PVU5DRUVDS09VVE5FQ1RFVEVDUklCRUxVU0hFVEVBRFNFQVJDSFJHRUNUSVZJVFlMRU5EQVJWRU9USUZZUFRJT05TQ0hTRUFZU1RBVENIR0VPUkRJUkVDVE9SVFJDSFBBUkFNRVRFUlVSQ0VCU0NSSUJFQVJET1dOQUNFSU5ETktDS1VCU0NSSUJFSFRUUC9BRFRQLw==' + + +/***/ }), + +/***/ 1891: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.enumToMap = void 0; +function enumToMap(obj) { + const res = {}; + Object.keys(obj).forEach((key) => { + const value = obj[key]; + if (typeof value === 'number') { + res[key] = value; + } + }); + return res; +} +exports.enumToMap = enumToMap; +//# sourceMappingURL=utils.js.map + +/***/ }), + +/***/ 6771: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kClients } = __nccwpck_require__(2785) +const Agent = __nccwpck_require__(7890) +const { + kAgent, + kMockAgentSet, + kMockAgentGet, + kDispatches, + kIsMockActive, + kNetConnect, + kGetNetConnect, + kOptions, + kFactory +} = __nccwpck_require__(4347) +const MockClient = __nccwpck_require__(8687) +const MockPool = __nccwpck_require__(6193) +const { matchValue, buildMockOptions } = __nccwpck_require__(9323) +const { InvalidArgumentError, UndiciError } = __nccwpck_require__(8045) +const Dispatcher = __nccwpck_require__(412) +const Pluralizer = __nccwpck_require__(8891) +const PendingInterceptorsFormatter = __nccwpck_require__(6823) + +class FakeWeakRef { + constructor (value) { + this.value = value + } + + deref () { + return this.value + } +} + +class MockAgent extends Dispatcher { + constructor (opts) { + super(opts) + + this[kNetConnect] = true + this[kIsMockActive] = true + + // Instantiate Agent and encapsulate + if ((opts && opts.agent && typeof opts.agent.dispatch !== 'function')) { + throw new InvalidArgumentError('Argument opts.agent must implement Agent') + } + const agent = opts && opts.agent ? opts.agent : new Agent(opts) + this[kAgent] = agent + + this[kClients] = agent[kClients] + this[kOptions] = buildMockOptions(opts) + } + + get (origin) { + let dispatcher = this[kMockAgentGet](origin) + + if (!dispatcher) { + dispatcher = this[kFactory](origin) + this[kMockAgentSet](origin, dispatcher) + } + return dispatcher + } + + dispatch (opts, handler) { + // Call MockAgent.get to perform additional setup before dispatching as normal + this.get(opts.origin) + return this[kAgent].dispatch(opts, handler) + } + + async close () { + await this[kAgent].close() + this[kClients].clear() + } + + deactivate () { + this[kIsMockActive] = false + } + + activate () { + this[kIsMockActive] = true + } + + enableNetConnect (matcher) { + if (typeof matcher === 'string' || typeof matcher === 'function' || matcher instanceof RegExp) { + if (Array.isArray(this[kNetConnect])) { + this[kNetConnect].push(matcher) + } else { + this[kNetConnect] = [matcher] + } + } else if (typeof matcher === 'undefined') { + this[kNetConnect] = true + } else { + throw new InvalidArgumentError('Unsupported matcher. Must be one of String|Function|RegExp.') + } + } + + disableNetConnect () { + this[kNetConnect] = false + } + + // This is required to bypass issues caused by using global symbols - see: + // https://github.com/nodejs/undici/issues/1447 + get isMockActive () { + return this[kIsMockActive] + } + + [kMockAgentSet] (origin, dispatcher) { + this[kClients].set(origin, new FakeWeakRef(dispatcher)) + } + + [kFactory] (origin) { + const mockOptions = Object.assign({ agent: this }, this[kOptions]) + return this[kOptions] && this[kOptions].connections === 1 + ? new MockClient(origin, mockOptions) + : new MockPool(origin, mockOptions) + } + + [kMockAgentGet] (origin) { + // First check if we can immediately find it + const ref = this[kClients].get(origin) + if (ref) { + return ref.deref() + } + + // If the origin is not a string create a dummy parent pool and return to user + if (typeof origin !== 'string') { + const dispatcher = this[kFactory]('http://localhost:9999') + this[kMockAgentSet](origin, dispatcher) + return dispatcher + } + + // If we match, create a pool and assign the same dispatches + for (const [keyMatcher, nonExplicitRef] of Array.from(this[kClients])) { + const nonExplicitDispatcher = nonExplicitRef.deref() + if (nonExplicitDispatcher && typeof keyMatcher !== 'string' && matchValue(keyMatcher, origin)) { + const dispatcher = this[kFactory](origin) + this[kMockAgentSet](origin, dispatcher) + dispatcher[kDispatches] = nonExplicitDispatcher[kDispatches] + return dispatcher + } + } + } + + [kGetNetConnect] () { + return this[kNetConnect] + } + + pendingInterceptors () { + const mockAgentClients = this[kClients] + + return Array.from(mockAgentClients.entries()) + .flatMap(([origin, scope]) => scope.deref()[kDispatches].map(dispatch => ({ ...dispatch, origin }))) + .filter(({ pending }) => pending) + } + + assertNoPendingInterceptors ({ pendingInterceptorsFormatter = new PendingInterceptorsFormatter() } = {}) { + const pending = this.pendingInterceptors() + + if (pending.length === 0) { + return + } + + const pluralizer = new Pluralizer('interceptor', 'interceptors').pluralize(pending.length) + + throw new UndiciError(` +${pluralizer.count} ${pluralizer.noun} ${pluralizer.is} pending: + +${pendingInterceptorsFormatter.format(pending)} +`.trim()) + } +} + +module.exports = MockAgent + + +/***/ }), + +/***/ 8687: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { promisify } = __nccwpck_require__(3837) +const Client = __nccwpck_require__(3598) +const { buildMockDispatch } = __nccwpck_require__(9323) +const { + kDispatches, + kMockAgent, + kClose, + kOriginalClose, + kOrigin, + kOriginalDispatch, + kConnected +} = __nccwpck_require__(4347) +const { MockInterceptor } = __nccwpck_require__(410) +const Symbols = __nccwpck_require__(2785) +const { InvalidArgumentError } = __nccwpck_require__(8045) + +/** + * MockClient provides an API that extends the Client to influence the mockDispatches. + */ +class MockClient extends Client { + constructor (origin, opts) { + super(origin, opts) + + if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') { + throw new InvalidArgumentError('Argument opts.agent must implement Agent') + } + + this[kMockAgent] = opts.agent + this[kOrigin] = origin + this[kDispatches] = [] + this[kConnected] = 1 + this[kOriginalDispatch] = this.dispatch + this[kOriginalClose] = this.close.bind(this) + + this.dispatch = buildMockDispatch.call(this) + this.close = this[kClose] + } + + get [Symbols.kConnected] () { + return this[kConnected] + } + + /** + * Sets up the base interceptor for mocking replies from undici. + */ + intercept (opts) { + return new MockInterceptor(opts, this[kDispatches]) + } + + async [kClose] () { + await promisify(this[kOriginalClose])() + this[kConnected] = 0 + this[kMockAgent][Symbols.kClients].delete(this[kOrigin]) + } +} + +module.exports = MockClient + + +/***/ }), + +/***/ 888: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { UndiciError } = __nccwpck_require__(8045) + +class MockNotMatchedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, MockNotMatchedError) + this.name = 'MockNotMatchedError' + this.message = message || 'The request does not match any registered mock dispatches' + this.code = 'UND_MOCK_ERR_MOCK_NOT_MATCHED' + } +} + +module.exports = { + MockNotMatchedError +} + + +/***/ }), + +/***/ 410: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { getResponseData, buildKey, addMockDispatch } = __nccwpck_require__(9323) +const { + kDispatches, + kDispatchKey, + kDefaultHeaders, + kDefaultTrailers, + kContentLength, + kMockDispatch +} = __nccwpck_require__(4347) +const { InvalidArgumentError } = __nccwpck_require__(8045) +const { buildURL } = __nccwpck_require__(3983) + +/** + * Defines the scope API for an interceptor reply + */ +class MockScope { + constructor (mockDispatch) { + this[kMockDispatch] = mockDispatch + } + + /** + * Delay a reply by a set amount in ms. + */ + delay (waitInMs) { + if (typeof waitInMs !== 'number' || !Number.isInteger(waitInMs) || waitInMs <= 0) { + throw new InvalidArgumentError('waitInMs must be a valid integer > 0') + } + + this[kMockDispatch].delay = waitInMs + return this + } + + /** + * For a defined reply, never mark as consumed. + */ + persist () { + this[kMockDispatch].persist = true + return this + } + + /** + * Allow one to define a reply for a set amount of matching requests. + */ + times (repeatTimes) { + if (typeof repeatTimes !== 'number' || !Number.isInteger(repeatTimes) || repeatTimes <= 0) { + throw new InvalidArgumentError('repeatTimes must be a valid integer > 0') + } + + this[kMockDispatch].times = repeatTimes + return this + } +} + +/** + * Defines an interceptor for a Mock + */ +class MockInterceptor { + constructor (opts, mockDispatches) { + if (typeof opts !== 'object') { + throw new InvalidArgumentError('opts must be an object') + } + if (typeof opts.path === 'undefined') { + throw new InvalidArgumentError('opts.path must be defined') + } + if (typeof opts.method === 'undefined') { + opts.method = 'GET' + } + // See https://github.com/nodejs/undici/issues/1245 + // As per RFC 3986, clients are not supposed to send URI + // fragments to servers when they retrieve a document, + if (typeof opts.path === 'string') { + if (opts.query) { + opts.path = buildURL(opts.path, opts.query) + } else { + // Matches https://github.com/nodejs/undici/blob/main/lib/fetch/index.js#L1811 + const parsedURL = new URL(opts.path, 'data://') + opts.path = parsedURL.pathname + parsedURL.search + } + } + if (typeof opts.method === 'string') { + opts.method = opts.method.toUpperCase() + } + + this[kDispatchKey] = buildKey(opts) + this[kDispatches] = mockDispatches + this[kDefaultHeaders] = {} + this[kDefaultTrailers] = {} + this[kContentLength] = false + } + + createMockScopeDispatchData (statusCode, data, responseOptions = {}) { + const responseData = getResponseData(data) + const contentLength = this[kContentLength] ? { 'content-length': responseData.length } : {} + const headers = { ...this[kDefaultHeaders], ...contentLength, ...responseOptions.headers } + const trailers = { ...this[kDefaultTrailers], ...responseOptions.trailers } + + return { statusCode, data, headers, trailers } + } + + validateReplyParameters (statusCode, data, responseOptions) { + if (typeof statusCode === 'undefined') { + throw new InvalidArgumentError('statusCode must be defined') + } + if (typeof data === 'undefined') { + throw new InvalidArgumentError('data must be defined') + } + if (typeof responseOptions !== 'object') { + throw new InvalidArgumentError('responseOptions must be an object') + } + } + + /** + * Mock an undici request with a defined reply. + */ + reply (replyData) { + // Values of reply aren't available right now as they + // can only be available when the reply callback is invoked. + if (typeof replyData === 'function') { + // We'll first wrap the provided callback in another function, + // this function will properly resolve the data from the callback + // when invoked. + const wrappedDefaultsCallback = (opts) => { + // Our reply options callback contains the parameter for statusCode, data and options. + const resolvedData = replyData(opts) + + // Check if it is in the right format + if (typeof resolvedData !== 'object') { + throw new InvalidArgumentError('reply options callback must return an object') + } + + const { statusCode, data = '', responseOptions = {} } = resolvedData + this.validateReplyParameters(statusCode, data, responseOptions) + // Since the values can be obtained immediately we return them + // from this higher order function that will be resolved later. + return { + ...this.createMockScopeDispatchData(statusCode, data, responseOptions) + } + } + + // Add usual dispatch data, but this time set the data parameter to function that will eventually provide data. + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], wrappedDefaultsCallback) + return new MockScope(newMockDispatch) + } + + // We can have either one or three parameters, if we get here, + // we should have 1-3 parameters. So we spread the arguments of + // this function to obtain the parameters, since replyData will always + // just be the statusCode. + const [statusCode, data = '', responseOptions = {}] = [...arguments] + this.validateReplyParameters(statusCode, data, responseOptions) + + // Send in-already provided data like usual + const dispatchData = this.createMockScopeDispatchData(statusCode, data, responseOptions) + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], dispatchData) + return new MockScope(newMockDispatch) + } + + /** + * Mock an undici request with a defined error. + */ + replyWithError (error) { + if (typeof error === 'undefined') { + throw new InvalidArgumentError('error must be defined') + } + + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], { error }) + return new MockScope(newMockDispatch) + } + + /** + * Set default reply headers on the interceptor for subsequent replies + */ + defaultReplyHeaders (headers) { + if (typeof headers === 'undefined') { + throw new InvalidArgumentError('headers must be defined') + } + + this[kDefaultHeaders] = headers + return this + } + + /** + * Set default reply trailers on the interceptor for subsequent replies + */ + defaultReplyTrailers (trailers) { + if (typeof trailers === 'undefined') { + throw new InvalidArgumentError('trailers must be defined') + } + + this[kDefaultTrailers] = trailers + return this + } + + /** + * Set reply content length header for replies on the interceptor + */ + replyContentLength () { + this[kContentLength] = true + return this + } +} + +module.exports.MockInterceptor = MockInterceptor +module.exports.MockScope = MockScope + + +/***/ }), + +/***/ 6193: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { promisify } = __nccwpck_require__(3837) +const Pool = __nccwpck_require__(4634) +const { buildMockDispatch } = __nccwpck_require__(9323) +const { + kDispatches, + kMockAgent, + kClose, + kOriginalClose, + kOrigin, + kOriginalDispatch, + kConnected +} = __nccwpck_require__(4347) +const { MockInterceptor } = __nccwpck_require__(410) +const Symbols = __nccwpck_require__(2785) +const { InvalidArgumentError } = __nccwpck_require__(8045) + +/** + * MockPool provides an API that extends the Pool to influence the mockDispatches. + */ +class MockPool extends Pool { + constructor (origin, opts) { + super(origin, opts) + + if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') { + throw new InvalidArgumentError('Argument opts.agent must implement Agent') + } + + this[kMockAgent] = opts.agent + this[kOrigin] = origin + this[kDispatches] = [] + this[kConnected] = 1 + this[kOriginalDispatch] = this.dispatch + this[kOriginalClose] = this.close.bind(this) + + this.dispatch = buildMockDispatch.call(this) + this.close = this[kClose] + } + + get [Symbols.kConnected] () { + return this[kConnected] + } + + /** + * Sets up the base interceptor for mocking replies from undici. + */ + intercept (opts) { + return new MockInterceptor(opts, this[kDispatches]) + } + + async [kClose] () { + await promisify(this[kOriginalClose])() + this[kConnected] = 0 + this[kMockAgent][Symbols.kClients].delete(this[kOrigin]) + } +} + +module.exports = MockPool + + +/***/ }), + +/***/ 4347: +/***/ ((module) => { + +"use strict"; + + +module.exports = { + kAgent: Symbol('agent'), + kOptions: Symbol('options'), + kFactory: Symbol('factory'), + kDispatches: Symbol('dispatches'), + kDispatchKey: Symbol('dispatch key'), + kDefaultHeaders: Symbol('default headers'), + kDefaultTrailers: Symbol('default trailers'), + kContentLength: Symbol('content length'), + kMockAgent: Symbol('mock agent'), + kMockAgentSet: Symbol('mock agent set'), + kMockAgentGet: Symbol('mock agent get'), + kMockDispatch: Symbol('mock dispatch'), + kClose: Symbol('close'), + kOriginalClose: Symbol('original agent close'), + kOrigin: Symbol('origin'), + kIsMockActive: Symbol('is mock active'), + kNetConnect: Symbol('net connect'), + kGetNetConnect: Symbol('get net connect'), + kConnected: Symbol('connected') +} + + +/***/ }), + +/***/ 9323: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { MockNotMatchedError } = __nccwpck_require__(888) +const { + kDispatches, + kMockAgent, + kOriginalDispatch, + kOrigin, + kGetNetConnect +} = __nccwpck_require__(4347) +const { buildURL, nop } = __nccwpck_require__(3983) +const { STATUS_CODES } = __nccwpck_require__(3685) +const { + types: { + isPromise + } +} = __nccwpck_require__(3837) + +function matchValue (match, value) { + if (typeof match === 'string') { + return match === value + } + if (match instanceof RegExp) { + return match.test(value) + } + if (typeof match === 'function') { + return match(value) === true + } + return false +} + +function lowerCaseEntries (headers) { + return Object.fromEntries( + Object.entries(headers).map(([headerName, headerValue]) => { + return [headerName.toLocaleLowerCase(), headerValue] + }) + ) +} + +/** + * @param {import('../../index').Headers|string[]|Record} headers + * @param {string} key + */ +function getHeaderByName (headers, key) { + if (Array.isArray(headers)) { + for (let i = 0; i < headers.length; i += 2) { + if (headers[i].toLocaleLowerCase() === key.toLocaleLowerCase()) { + return headers[i + 1] + } + } + + return undefined + } else if (typeof headers.get === 'function') { + return headers.get(key) + } else { + return lowerCaseEntries(headers)[key.toLocaleLowerCase()] + } +} + +/** @param {string[]} headers */ +function buildHeadersFromArray (headers) { // fetch HeadersList + const clone = headers.slice() + const entries = [] + for (let index = 0; index < clone.length; index += 2) { + entries.push([clone[index], clone[index + 1]]) + } + return Object.fromEntries(entries) +} + +function matchHeaders (mockDispatch, headers) { + if (typeof mockDispatch.headers === 'function') { + if (Array.isArray(headers)) { // fetch HeadersList + headers = buildHeadersFromArray(headers) + } + return mockDispatch.headers(headers ? lowerCaseEntries(headers) : {}) + } + if (typeof mockDispatch.headers === 'undefined') { + return true + } + if (typeof headers !== 'object' || typeof mockDispatch.headers !== 'object') { + return false + } + + for (const [matchHeaderName, matchHeaderValue] of Object.entries(mockDispatch.headers)) { + const headerValue = getHeaderByName(headers, matchHeaderName) + + if (!matchValue(matchHeaderValue, headerValue)) { + return false + } + } + return true +} + +function safeUrl (path) { + if (typeof path !== 'string') { + return path + } + + const pathSegments = path.split('?') + + if (pathSegments.length !== 2) { + return path + } + + const qp = new URLSearchParams(pathSegments.pop()) + qp.sort() + return [...pathSegments, qp.toString()].join('?') +} + +function matchKey (mockDispatch, { path, method, body, headers }) { + const pathMatch = matchValue(mockDispatch.path, path) + const methodMatch = matchValue(mockDispatch.method, method) + const bodyMatch = typeof mockDispatch.body !== 'undefined' ? matchValue(mockDispatch.body, body) : true + const headersMatch = matchHeaders(mockDispatch, headers) + return pathMatch && methodMatch && bodyMatch && headersMatch +} + +function getResponseData (data) { + if (Buffer.isBuffer(data)) { + return data + } else if (typeof data === 'object') { + return JSON.stringify(data) + } else { + return data.toString() + } +} + +function getMockDispatch (mockDispatches, key) { + const basePath = key.query ? buildURL(key.path, key.query) : key.path + const resolvedPath = typeof basePath === 'string' ? safeUrl(basePath) : basePath + + // Match path + let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path }) => matchValue(safeUrl(path), resolvedPath)) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`) + } + + // Match method + matchedMockDispatches = matchedMockDispatches.filter(({ method }) => matchValue(method, key.method)) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for method '${key.method}'`) + } + + // Match body + matchedMockDispatches = matchedMockDispatches.filter(({ body }) => typeof body !== 'undefined' ? matchValue(body, key.body) : true) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for body '${key.body}'`) + } + + // Match headers + matchedMockDispatches = matchedMockDispatches.filter((mockDispatch) => matchHeaders(mockDispatch, key.headers)) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for headers '${typeof key.headers === 'object' ? JSON.stringify(key.headers) : key.headers}'`) + } + + return matchedMockDispatches[0] +} + +function addMockDispatch (mockDispatches, key, data) { + const baseData = { timesInvoked: 0, times: 1, persist: false, consumed: false } + const replyData = typeof data === 'function' ? { callback: data } : { ...data } + const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } } + mockDispatches.push(newMockDispatch) + return newMockDispatch +} + +function deleteMockDispatch (mockDispatches, key) { + const index = mockDispatches.findIndex(dispatch => { + if (!dispatch.consumed) { + return false + } + return matchKey(dispatch, key) + }) + if (index !== -1) { + mockDispatches.splice(index, 1) + } +} + +function buildKey (opts) { + const { path, method, body, headers, query } = opts + return { + path, + method, + body, + headers, + query + } +} + +function generateKeyValues (data) { + return Object.entries(data).reduce((keyValuePairs, [key, value]) => [ + ...keyValuePairs, + Buffer.from(`${key}`), + Array.isArray(value) ? value.map(x => Buffer.from(`${x}`)) : Buffer.from(`${value}`) + ], []) +} + +/** + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status + * @param {number} statusCode + */ +function getStatusText (statusCode) { + return STATUS_CODES[statusCode] || 'unknown' +} + +async function getResponse (body) { + const buffers = [] + for await (const data of body) { + buffers.push(data) + } + return Buffer.concat(buffers).toString('utf8') +} + +/** + * Mock dispatch function used to simulate undici dispatches + */ +function mockDispatch (opts, handler) { + // Get mock dispatch from built key + const key = buildKey(opts) + const mockDispatch = getMockDispatch(this[kDispatches], key) + + mockDispatch.timesInvoked++ + + // Here's where we resolve a callback if a callback is present for the dispatch data. + if (mockDispatch.data.callback) { + mockDispatch.data = { ...mockDispatch.data, ...mockDispatch.data.callback(opts) } + } + + // Parse mockDispatch data + const { data: { statusCode, data, headers, trailers, error }, delay, persist } = mockDispatch + const { timesInvoked, times } = mockDispatch + + // If it's used up and not persistent, mark as consumed + mockDispatch.consumed = !persist && timesInvoked >= times + mockDispatch.pending = timesInvoked < times + + // If specified, trigger dispatch error + if (error !== null) { + deleteMockDispatch(this[kDispatches], key) + handler.onError(error) + return true + } + + // Handle the request with a delay if necessary + if (typeof delay === 'number' && delay > 0) { + setTimeout(() => { + handleReply(this[kDispatches]) + }, delay) + } else { + handleReply(this[kDispatches]) + } + + function handleReply (mockDispatches, _data = data) { + // fetch's HeadersList is a 1D string array + const optsHeaders = Array.isArray(opts.headers) + ? buildHeadersFromArray(opts.headers) + : opts.headers + const body = typeof _data === 'function' + ? _data({ ...opts, headers: optsHeaders }) + : _data + + // util.types.isPromise is likely needed for jest. + if (isPromise(body)) { + // If handleReply is asynchronous, throwing an error + // in the callback will reject the promise, rather than + // synchronously throw the error, which breaks some tests. + // Rather, we wait for the callback to resolve if it is a + // promise, and then re-run handleReply with the new body. + body.then((newData) => handleReply(mockDispatches, newData)) + return + } + + const responseData = getResponseData(body) + const responseHeaders = generateKeyValues(headers) + const responseTrailers = generateKeyValues(trailers) + + handler.abort = nop + handler.onHeaders(statusCode, responseHeaders, resume, getStatusText(statusCode)) + handler.onData(Buffer.from(responseData)) + handler.onComplete(responseTrailers) + deleteMockDispatch(mockDispatches, key) + } + + function resume () {} + + return true +} + +function buildMockDispatch () { + const agent = this[kMockAgent] + const origin = this[kOrigin] + const originalDispatch = this[kOriginalDispatch] + + return function dispatch (opts, handler) { + if (agent.isMockActive) { + try { + mockDispatch.call(this, opts, handler) + } catch (error) { + if (error instanceof MockNotMatchedError) { + const netConnect = agent[kGetNetConnect]() + if (netConnect === false) { + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`) + } + if (checkNetConnect(netConnect, origin)) { + originalDispatch.call(this, opts, handler) + } else { + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)`) + } + } else { + throw error + } + } + } else { + originalDispatch.call(this, opts, handler) + } + } +} + +function checkNetConnect (netConnect, origin) { + const url = new URL(origin) + if (netConnect === true) { + return true + } else if (Array.isArray(netConnect) && netConnect.some((matcher) => matchValue(matcher, url.host))) { + return true + } + return false +} + +function buildMockOptions (opts) { + if (opts) { + const { agent, ...mockOptions } = opts + return mockOptions + } +} + +module.exports = { + getResponseData, + getMockDispatch, + addMockDispatch, + deleteMockDispatch, + buildKey, + generateKeyValues, + matchValue, + getResponse, + getStatusText, + mockDispatch, + buildMockDispatch, + checkNetConnect, + buildMockOptions, + getHeaderByName +} + + +/***/ }), + +/***/ 6823: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { Transform } = __nccwpck_require__(2781) +const { Console } = __nccwpck_require__(6206) + +/** + * Gets the output of `console.table(…)` as a string. + */ +module.exports = class PendingInterceptorsFormatter { + constructor ({ disableColors } = {}) { + this.transform = new Transform({ + transform (chunk, _enc, cb) { + cb(null, chunk) + } + }) + + this.logger = new Console({ + stdout: this.transform, + inspectOptions: { + colors: !disableColors && !process.env.CI + } + }) + } + + format (pendingInterceptors) { + const withPrettyHeaders = pendingInterceptors.map( + ({ method, path, data: { statusCode }, persist, times, timesInvoked, origin }) => ({ + Method: method, + Origin: origin, + Path: path, + 'Status code': statusCode, + Persistent: persist ? '✅' : '❌', + Invocations: timesInvoked, + Remaining: persist ? Infinity : times - timesInvoked + })) + + this.logger.table(withPrettyHeaders) + return this.transform.read().toString() + } +} + + +/***/ }), + +/***/ 8891: +/***/ ((module) => { + +"use strict"; + + +const singulars = { + pronoun: 'it', + is: 'is', + was: 'was', + this: 'this' +} + +const plurals = { + pronoun: 'they', + is: 'are', + was: 'were', + this: 'these' +} + +module.exports = class Pluralizer { + constructor (singular, plural) { + this.singular = singular + this.plural = plural + } + + pluralize (count) { + const one = count === 1 + const keys = one ? singulars : plurals + const noun = one ? this.singular : this.plural + return { ...keys, count, noun } + } +} + + +/***/ }), + +/***/ 8266: +/***/ ((module) => { + +"use strict"; +/* eslint-disable */ + + + +// Extracted from node/lib/internal/fixed_queue.js + +// Currently optimal queue size, tested on V8 6.0 - 6.6. Must be power of two. +const kSize = 2048; +const kMask = kSize - 1; + +// The FixedQueue is implemented as a singly-linked list of fixed-size +// circular buffers. It looks something like this: +// +// head tail +// | | +// v v +// +-----------+ <-----\ +-----------+ <------\ +-----------+ +// | [null] | \----- | next | \------- | next | +// +-----------+ +-----------+ +-----------+ +// | item | <-- bottom | item | <-- bottom | [empty] | +// | item | | item | | [empty] | +// | item | | item | | [empty] | +// | item | | item | | [empty] | +// | item | | item | bottom --> | item | +// | item | | item | | item | +// | ... | | ... | | ... | +// | item | | item | | item | +// | item | | item | | item | +// | [empty] | <-- top | item | | item | +// | [empty] | | item | | item | +// | [empty] | | [empty] | <-- top top --> | [empty] | +// +-----------+ +-----------+ +-----------+ +// +// Or, if there is only one circular buffer, it looks something +// like either of these: +// +// head tail head tail +// | | | | +// v v v v +// +-----------+ +-----------+ +// | [null] | | [null] | +// +-----------+ +-----------+ +// | [empty] | | item | +// | [empty] | | item | +// | item | <-- bottom top --> | [empty] | +// | item | | [empty] | +// | [empty] | <-- top bottom --> | item | +// | [empty] | | item | +// +-----------+ +-----------+ +// +// Adding a value means moving `top` forward by one, removing means +// moving `bottom` forward by one. After reaching the end, the queue +// wraps around. +// +// When `top === bottom` the current queue is empty and when +// `top + 1 === bottom` it's full. This wastes a single space of storage +// but allows much quicker checks. + +class FixedCircularBuffer { + constructor() { + this.bottom = 0; + this.top = 0; + this.list = new Array(kSize); + this.next = null; + } + + isEmpty() { + return this.top === this.bottom; + } + + isFull() { + return ((this.top + 1) & kMask) === this.bottom; + } + + push(data) { + this.list[this.top] = data; + this.top = (this.top + 1) & kMask; + } + + shift() { + const nextItem = this.list[this.bottom]; + if (nextItem === undefined) + return null; + this.list[this.bottom] = undefined; + this.bottom = (this.bottom + 1) & kMask; + return nextItem; + } +} + +module.exports = class FixedQueue { + constructor() { + this.head = this.tail = new FixedCircularBuffer(); + } + + isEmpty() { + return this.head.isEmpty(); + } + + push(data) { + if (this.head.isFull()) { + // Head is full: Creates a new queue, sets the old queue's `.next` to it, + // and sets it as the new main queue. + this.head = this.head.next = new FixedCircularBuffer(); + } + this.head.push(data); + } + + shift() { + const tail = this.tail; + const next = tail.shift(); + if (tail.isEmpty() && tail.next !== null) { + // If there is another queue, it forms the new tail. + this.tail = tail.next; + } + return next; + } +}; + + +/***/ }), + +/***/ 3198: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const DispatcherBase = __nccwpck_require__(4839) +const FixedQueue = __nccwpck_require__(8266) +const { kConnected, kSize, kRunning, kPending, kQueued, kBusy, kFree, kUrl, kClose, kDestroy, kDispatch } = __nccwpck_require__(2785) +const PoolStats = __nccwpck_require__(9689) + +const kClients = Symbol('clients') +const kNeedDrain = Symbol('needDrain') +const kQueue = Symbol('queue') +const kClosedResolve = Symbol('closed resolve') +const kOnDrain = Symbol('onDrain') +const kOnConnect = Symbol('onConnect') +const kOnDisconnect = Symbol('onDisconnect') +const kOnConnectionError = Symbol('onConnectionError') +const kGetDispatcher = Symbol('get dispatcher') +const kAddClient = Symbol('add client') +const kRemoveClient = Symbol('remove client') +const kStats = Symbol('stats') + +class PoolBase extends DispatcherBase { + constructor () { + super() + + this[kQueue] = new FixedQueue() + this[kClients] = [] + this[kQueued] = 0 + + const pool = this + + this[kOnDrain] = function onDrain (origin, targets) { + const queue = pool[kQueue] + + let needDrain = false + + while (!needDrain) { + const item = queue.shift() + if (!item) { + break + } + pool[kQueued]-- + needDrain = !this.dispatch(item.opts, item.handler) + } + + this[kNeedDrain] = needDrain + + if (!this[kNeedDrain] && pool[kNeedDrain]) { + pool[kNeedDrain] = false + pool.emit('drain', origin, [pool, ...targets]) + } + + if (pool[kClosedResolve] && queue.isEmpty()) { + Promise + .all(pool[kClients].map(c => c.close())) + .then(pool[kClosedResolve]) + } + } + + this[kOnConnect] = (origin, targets) => { + pool.emit('connect', origin, [pool, ...targets]) + } + + this[kOnDisconnect] = (origin, targets, err) => { + pool.emit('disconnect', origin, [pool, ...targets], err) + } + + this[kOnConnectionError] = (origin, targets, err) => { + pool.emit('connectionError', origin, [pool, ...targets], err) + } + + this[kStats] = new PoolStats(this) + } + + get [kBusy] () { + return this[kNeedDrain] + } + + get [kConnected] () { + return this[kClients].filter(client => client[kConnected]).length + } + + get [kFree] () { + return this[kClients].filter(client => client[kConnected] && !client[kNeedDrain]).length + } + + get [kPending] () { + let ret = this[kQueued] + for (const { [kPending]: pending } of this[kClients]) { + ret += pending + } + return ret + } + + get [kRunning] () { + let ret = 0 + for (const { [kRunning]: running } of this[kClients]) { + ret += running + } + return ret + } + + get [kSize] () { + let ret = this[kQueued] + for (const { [kSize]: size } of this[kClients]) { + ret += size + } + return ret + } + + get stats () { + return this[kStats] + } + + async [kClose] () { + if (this[kQueue].isEmpty()) { + return Promise.all(this[kClients].map(c => c.close())) + } else { + return new Promise((resolve) => { + this[kClosedResolve] = resolve + }) + } + } + + async [kDestroy] (err) { + while (true) { + const item = this[kQueue].shift() + if (!item) { + break + } + item.handler.onError(err) + } + + return Promise.all(this[kClients].map(c => c.destroy(err))) + } + + [kDispatch] (opts, handler) { + const dispatcher = this[kGetDispatcher]() + + if (!dispatcher) { + this[kNeedDrain] = true + this[kQueue].push({ opts, handler }) + this[kQueued]++ + } else if (!dispatcher.dispatch(opts, handler)) { + dispatcher[kNeedDrain] = true + this[kNeedDrain] = !this[kGetDispatcher]() + } + + return !this[kNeedDrain] + } + + [kAddClient] (client) { + client + .on('drain', this[kOnDrain]) + .on('connect', this[kOnConnect]) + .on('disconnect', this[kOnDisconnect]) + .on('connectionError', this[kOnConnectionError]) + + this[kClients].push(client) + + if (this[kNeedDrain]) { + process.nextTick(() => { + if (this[kNeedDrain]) { + this[kOnDrain](client[kUrl], [this, client]) + } + }) + } + + return this + } + + [kRemoveClient] (client) { + client.close(() => { + const idx = this[kClients].indexOf(client) + if (idx !== -1) { + this[kClients].splice(idx, 1) + } + }) + + this[kNeedDrain] = this[kClients].some(dispatcher => ( + !dispatcher[kNeedDrain] && + dispatcher.closed !== true && + dispatcher.destroyed !== true + )) + } +} + +module.exports = { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kRemoveClient, + kGetDispatcher +} + + +/***/ }), + +/***/ 9689: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const { kFree, kConnected, kPending, kQueued, kRunning, kSize } = __nccwpck_require__(2785) +const kPool = Symbol('pool') + +class PoolStats { + constructor (pool) { + this[kPool] = pool + } + + get connected () { + return this[kPool][kConnected] + } + + get free () { + return this[kPool][kFree] + } + + get pending () { + return this[kPool][kPending] + } + + get queued () { + return this[kPool][kQueued] + } + + get running () { + return this[kPool][kRunning] + } + + get size () { + return this[kPool][kSize] + } +} + +module.exports = PoolStats + + +/***/ }), + +/***/ 4634: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kGetDispatcher +} = __nccwpck_require__(3198) +const Client = __nccwpck_require__(3598) +const { + InvalidArgumentError +} = __nccwpck_require__(8045) +const util = __nccwpck_require__(3983) +const { kUrl, kInterceptors } = __nccwpck_require__(2785) +const buildConnector = __nccwpck_require__(2067) + +const kOptions = Symbol('options') +const kConnections = Symbol('connections') +const kFactory = Symbol('factory') + +function defaultFactory (origin, opts) { + return new Client(origin, opts) +} + +class Pool extends PoolBase { + constructor (origin, { + connections, + factory = defaultFactory, + connect, + connectTimeout, + tls, + maxCachedSessions, + socketPath, + autoSelectFamily, + autoSelectFamilyAttemptTimeout, + allowH2, + ...options + } = {}) { + super() + + if (connections != null && (!Number.isFinite(connections) || connections < 0)) { + throw new InvalidArgumentError('invalid connections') + } + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (typeof connect !== 'function') { + connect = buildConnector({ + ...tls, + maxCachedSessions, + allowH2, + socketPath, + timeout: connectTimeout, + ...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), + ...connect + }) + } + + this[kInterceptors] = options.interceptors && options.interceptors.Pool && Array.isArray(options.interceptors.Pool) + ? options.interceptors.Pool + : [] + this[kConnections] = connections || null + this[kUrl] = util.parseOrigin(origin) + this[kOptions] = { ...util.deepClone(options), connect, allowH2 } + this[kOptions].interceptors = options.interceptors + ? { ...options.interceptors } + : undefined + this[kFactory] = factory + } + + [kGetDispatcher] () { + let dispatcher = this[kClients].find(dispatcher => !dispatcher[kNeedDrain]) + + if (dispatcher) { + return dispatcher + } + + if (!this[kConnections] || this[kClients].length < this[kConnections]) { + dispatcher = this[kFactory](this[kUrl], this[kOptions]) + this[kAddClient](dispatcher) + } + + return dispatcher + } +} + +module.exports = Pool + + +/***/ }), + +/***/ 7858: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kProxy, kClose, kDestroy, kInterceptors } = __nccwpck_require__(2785) +const { URL } = __nccwpck_require__(7310) +const Agent = __nccwpck_require__(7890) +const Pool = __nccwpck_require__(4634) +const DispatcherBase = __nccwpck_require__(4839) +const { InvalidArgumentError, RequestAbortedError } = __nccwpck_require__(8045) +const buildConnector = __nccwpck_require__(2067) + +const kAgent = Symbol('proxy agent') +const kClient = Symbol('proxy client') +const kProxyHeaders = Symbol('proxy headers') +const kRequestTls = Symbol('request tls settings') +const kProxyTls = Symbol('proxy tls settings') +const kConnectEndpoint = Symbol('connect endpoint function') + +function defaultProtocolPort (protocol) { + return protocol === 'https:' ? 443 : 80 +} + +function buildProxyOptions (opts) { + if (typeof opts === 'string') { + opts = { uri: opts } + } + + if (!opts || !opts.uri) { + throw new InvalidArgumentError('Proxy opts.uri is mandatory') + } + + return { + uri: opts.uri, + protocol: opts.protocol || 'https' + } +} + +function defaultFactory (origin, opts) { + return new Pool(origin, opts) +} + +class ProxyAgent extends DispatcherBase { + constructor (opts) { + super(opts) + this[kProxy] = buildProxyOptions(opts) + this[kAgent] = new Agent(opts) + this[kInterceptors] = opts.interceptors && opts.interceptors.ProxyAgent && Array.isArray(opts.interceptors.ProxyAgent) + ? opts.interceptors.ProxyAgent + : [] + + if (typeof opts === 'string') { + opts = { uri: opts } + } + + if (!opts || !opts.uri) { + throw new InvalidArgumentError('Proxy opts.uri is mandatory') + } + + const { clientFactory = defaultFactory } = opts + + if (typeof clientFactory !== 'function') { + throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.') + } + + this[kRequestTls] = opts.requestTls + this[kProxyTls] = opts.proxyTls + this[kProxyHeaders] = opts.headers || {} + + const resolvedUrl = new URL(opts.uri) + const { origin, port, host, username, password } = resolvedUrl + + if (opts.auth && opts.token) { + throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token') + } else if (opts.auth) { + /* @deprecated in favour of opts.token */ + this[kProxyHeaders]['proxy-authorization'] = `Basic ${opts.auth}` + } else if (opts.token) { + this[kProxyHeaders]['proxy-authorization'] = opts.token + } else if (username && password) { + this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}` + } + + const connect = buildConnector({ ...opts.proxyTls }) + this[kConnectEndpoint] = buildConnector({ ...opts.requestTls }) + this[kClient] = clientFactory(resolvedUrl, { connect }) + this[kAgent] = new Agent({ + ...opts, + connect: async (opts, callback) => { + let requestedHost = opts.host + if (!opts.port) { + requestedHost += `:${defaultProtocolPort(opts.protocol)}` + } + try { + const { socket, statusCode } = await this[kClient].connect({ + origin, + port, + path: requestedHost, + signal: opts.signal, + headers: { + ...this[kProxyHeaders], + host + } + }) + if (statusCode !== 200) { + socket.on('error', () => {}).destroy() + callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`)) + } + if (opts.protocol !== 'https:') { + callback(null, socket) + return + } + let servername + if (this[kRequestTls]) { + servername = this[kRequestTls].servername + } else { + servername = opts.servername + } + this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback) + } catch (err) { + callback(err) + } + } + }) + } + + dispatch (opts, handler) { + const { host } = new URL(opts.origin) + const headers = buildHeaders(opts.headers) + throwIfProxyAuthIsSent(headers) + return this[kAgent].dispatch( + { + ...opts, + headers: { + ...headers, + host + } + }, + handler + ) + } + + async [kClose] () { + await this[kAgent].close() + await this[kClient].close() + } + + async [kDestroy] () { + await this[kAgent].destroy() + await this[kClient].destroy() + } +} + +/** + * @param {string[] | Record} headers + * @returns {Record} + */ +function buildHeaders (headers) { + // When using undici.fetch, the headers list is stored + // as an array. + if (Array.isArray(headers)) { + /** @type {Record} */ + const headersPair = {} + + for (let i = 0; i < headers.length; i += 2) { + headersPair[headers[i]] = headers[i + 1] + } + + return headersPair + } + + return headers +} + +/** + * @param {Record} headers + * + * Previous versions of ProxyAgent suggests the Proxy-Authorization in request headers + * Nevertheless, it was changed and to avoid a security vulnerability by end users + * this check was created. + * It should be removed in the next major version for performance reasons + */ +function throwIfProxyAuthIsSent (headers) { + const existProxyAuth = headers && Object.keys(headers) + .find((key) => key.toLowerCase() === 'proxy-authorization') + if (existProxyAuth) { + throw new InvalidArgumentError('Proxy-Authorization should be sent in ProxyAgent constructor') + } +} + +module.exports = ProxyAgent + + +/***/ }), + +/***/ 9459: +/***/ ((module) => { + +"use strict"; + + +let fastNow = Date.now() +let fastNowTimeout + +const fastTimers = [] + +function onTimeout () { + fastNow = Date.now() + + let len = fastTimers.length + let idx = 0 + while (idx < len) { + const timer = fastTimers[idx] + + if (timer.state === 0) { + timer.state = fastNow + timer.delay + } else if (timer.state > 0 && fastNow >= timer.state) { + timer.state = -1 + timer.callback(timer.opaque) + } + + if (timer.state === -1) { + timer.state = -2 + if (idx !== len - 1) { + fastTimers[idx] = fastTimers.pop() + } else { + fastTimers.pop() + } + len -= 1 + } else { + idx += 1 + } + } + + if (fastTimers.length > 0) { + refreshTimeout() + } +} + +function refreshTimeout () { + if (fastNowTimeout && fastNowTimeout.refresh) { + fastNowTimeout.refresh() + } else { + clearTimeout(fastNowTimeout) + fastNowTimeout = setTimeout(onTimeout, 1e3) + if (fastNowTimeout.unref) { + fastNowTimeout.unref() + } + } +} + +class Timeout { + constructor (callback, delay, opaque) { + this.callback = callback + this.delay = delay + this.opaque = opaque + + // -2 not in timer list + // -1 in timer list but inactive + // 0 in timer list waiting for time + // > 0 in timer list waiting for time to expire + this.state = -2 + + this.refresh() + } + + refresh () { + if (this.state === -2) { + fastTimers.push(this) + if (!fastNowTimeout || fastTimers.length === 1) { + refreshTimeout() + } + } + + this.state = 0 + } + + clear () { + this.state = -1 + } +} + +module.exports = { + setTimeout (callback, delay, opaque) { + return delay < 1e3 + ? setTimeout(callback, delay, opaque) + : new Timeout(callback, delay, opaque) + }, + clearTimeout (timeout) { + if (timeout instanceof Timeout) { + timeout.clear() + } else { + clearTimeout(timeout) + } + } +} + + +/***/ }), + +/***/ 5354: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const diagnosticsChannel = __nccwpck_require__(7643) +const { uid, states } = __nccwpck_require__(9188) +const { + kReadyState, + kSentClose, + kByteParser, + kReceivedClose +} = __nccwpck_require__(7578) +const { fireEvent, failWebsocketConnection } = __nccwpck_require__(5515) +const { CloseEvent } = __nccwpck_require__(2611) +const { makeRequest } = __nccwpck_require__(8359) +const { fetching } = __nccwpck_require__(4881) +const { Headers } = __nccwpck_require__(554) +const { getGlobalDispatcher } = __nccwpck_require__(1892) +const { kHeadersList } = __nccwpck_require__(2785) + +const channels = {} +channels.open = diagnosticsChannel.channel('undici:websocket:open') +channels.close = diagnosticsChannel.channel('undici:websocket:close') +channels.socketError = diagnosticsChannel.channel('undici:websocket:socket_error') + +/** @type {import('crypto')} */ +let crypto +try { + crypto = __nccwpck_require__(6113) +} catch { + +} + +/** + * @see https://websockets.spec.whatwg.org/#concept-websocket-establish + * @param {URL} url + * @param {string|string[]} protocols + * @param {import('./websocket').WebSocket} ws + * @param {(response: any) => void} onEstablish + * @param {Partial} options + */ +function establishWebSocketConnection (url, protocols, ws, onEstablish, options) { + // 1. Let requestURL be a copy of url, with its scheme set to "http", if url’s + // scheme is "ws", and to "https" otherwise. + const requestURL = url + + requestURL.protocol = url.protocol === 'ws:' ? 'http:' : 'https:' + + // 2. Let request be a new request, whose URL is requestURL, client is client, + // service-workers mode is "none", referrer is "no-referrer", mode is + // "websocket", credentials mode is "include", cache mode is "no-store" , + // and redirect mode is "error". + const request = makeRequest({ + urlList: [requestURL], + serviceWorkers: 'none', + referrer: 'no-referrer', + mode: 'websocket', + credentials: 'include', + cache: 'no-store', + redirect: 'error' + }) + + // Note: undici extension, allow setting custom headers. + if (options.headers) { + const headersList = new Headers(options.headers)[kHeadersList] + + request.headersList = headersList + } + + // 3. Append (`Upgrade`, `websocket`) to request’s header list. + // 4. Append (`Connection`, `Upgrade`) to request’s header list. + // Note: both of these are handled by undici currently. + // https://github.com/nodejs/undici/blob/68c269c4144c446f3f1220951338daef4a6b5ec4/lib/client.js#L1397 + + // 5. Let keyValue be a nonce consisting of a randomly selected + // 16-byte value that has been forgiving-base64-encoded and + // isomorphic encoded. + const keyValue = crypto.randomBytes(16).toString('base64') + + // 6. Append (`Sec-WebSocket-Key`, keyValue) to request’s + // header list. + request.headersList.append('sec-websocket-key', keyValue) + + // 7. Append (`Sec-WebSocket-Version`, `13`) to request’s + // header list. + request.headersList.append('sec-websocket-version', '13') + + // 8. For each protocol in protocols, combine + // (`Sec-WebSocket-Protocol`, protocol) in request’s header + // list. + for (const protocol of protocols) { + request.headersList.append('sec-websocket-protocol', protocol) + } + + // 9. Let permessageDeflate be a user-agent defined + // "permessage-deflate" extension header value. + // https://github.com/mozilla/gecko-dev/blob/ce78234f5e653a5d3916813ff990f053510227bc/netwerk/protocol/websocket/WebSocketChannel.cpp#L2673 + // TODO: enable once permessage-deflate is supported + const permessageDeflate = '' // 'permessage-deflate; 15' + + // 10. Append (`Sec-WebSocket-Extensions`, permessageDeflate) to + // request’s header list. + // request.headersList.append('sec-websocket-extensions', permessageDeflate) + + // 11. Fetch request with useParallelQueue set to true, and + // processResponse given response being these steps: + const controller = fetching({ + request, + useParallelQueue: true, + dispatcher: options.dispatcher ?? getGlobalDispatcher(), + processResponse (response) { + // 1. If response is a network error or its status is not 101, + // fail the WebSocket connection. + if (response.type === 'error' || response.status !== 101) { + failWebsocketConnection(ws, 'Received network error or non-101 status code.') + return + } + + // 2. If protocols is not the empty list and extracting header + // list values given `Sec-WebSocket-Protocol` and response’s + // header list results in null, failure, or the empty byte + // sequence, then fail the WebSocket connection. + if (protocols.length !== 0 && !response.headersList.get('Sec-WebSocket-Protocol')) { + failWebsocketConnection(ws, 'Server did not respond with sent protocols.') + return + } + + // 3. Follow the requirements stated step 2 to step 6, inclusive, + // of the last set of steps in section 4.1 of The WebSocket + // Protocol to validate response. This either results in fail + // the WebSocket connection or the WebSocket connection is + // established. + + // 2. If the response lacks an |Upgrade| header field or the |Upgrade| + // header field contains a value that is not an ASCII case- + // insensitive match for the value "websocket", the client MUST + // _Fail the WebSocket Connection_. + if (response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') { + failWebsocketConnection(ws, 'Server did not set Upgrade header to "websocket".') + return + } + + // 3. If the response lacks a |Connection| header field or the + // |Connection| header field doesn't contain a token that is an + // ASCII case-insensitive match for the value "Upgrade", the client + // MUST _Fail the WebSocket Connection_. + if (response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') { + failWebsocketConnection(ws, 'Server did not set Connection header to "upgrade".') + return + } + + // 4. If the response lacks a |Sec-WebSocket-Accept| header field or + // the |Sec-WebSocket-Accept| contains a value other than the + // base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket- + // Key| (as a string, not base64-decoded) with the string "258EAFA5- + // E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and + // trailing whitespace, the client MUST _Fail the WebSocket + // Connection_. + const secWSAccept = response.headersList.get('Sec-WebSocket-Accept') + const digest = crypto.createHash('sha1').update(keyValue + uid).digest('base64') + if (secWSAccept !== digest) { + failWebsocketConnection(ws, 'Incorrect hash received in Sec-WebSocket-Accept header.') + return + } + + // 5. If the response includes a |Sec-WebSocket-Extensions| header + // field and this header field indicates the use of an extension + // that was not present in the client's handshake (the server has + // indicated an extension not requested by the client), the client + // MUST _Fail the WebSocket Connection_. (The parsing of this + // header field to determine which extensions are requested is + // discussed in Section 9.1.) + const secExtension = response.headersList.get('Sec-WebSocket-Extensions') + + if (secExtension !== null && secExtension !== permessageDeflate) { + failWebsocketConnection(ws, 'Received different permessage-deflate than the one set.') + return + } + + // 6. If the response includes a |Sec-WebSocket-Protocol| header field + // and this header field indicates the use of a subprotocol that was + // not present in the client's handshake (the server has indicated a + // subprotocol not requested by the client), the client MUST _Fail + // the WebSocket Connection_. + const secProtocol = response.headersList.get('Sec-WebSocket-Protocol') + + if (secProtocol !== null && secProtocol !== request.headersList.get('Sec-WebSocket-Protocol')) { + failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.') + return + } + + response.socket.on('data', onSocketData) + response.socket.on('close', onSocketClose) + response.socket.on('error', onSocketError) + + if (channels.open.hasSubscribers) { + channels.open.publish({ + address: response.socket.address(), + protocol: secProtocol, + extensions: secExtension + }) + } + + onEstablish(response) + } + }) + + return controller +} + +/** + * @param {Buffer} chunk + */ +function onSocketData (chunk) { + if (!this.ws[kByteParser].write(chunk)) { + this.pause() + } +} + +/** + * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol + * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.4 + */ +function onSocketClose () { + const { ws } = this + + // If the TCP connection was closed after the + // WebSocket closing handshake was completed, the WebSocket connection + // is said to have been closed _cleanly_. + const wasClean = ws[kSentClose] && ws[kReceivedClose] + + let code = 1005 + let reason = '' + + const result = ws[kByteParser].closingInfo + + if (result) { + code = result.code ?? 1005 + reason = result.reason + } else if (!ws[kSentClose]) { + // If _The WebSocket + // Connection is Closed_ and no Close control frame was received by the + // endpoint (such as could occur if the underlying transport connection + // is lost), _The WebSocket Connection Close Code_ is considered to be + // 1006. + code = 1006 + } + + // 1. Change the ready state to CLOSED (3). + ws[kReadyState] = states.CLOSED + + // 2. If the user agent was required to fail the WebSocket + // connection, or if the WebSocket connection was closed + // after being flagged as full, fire an event named error + // at the WebSocket object. + // TODO + + // 3. Fire an event named close at the WebSocket object, + // using CloseEvent, with the wasClean attribute + // initialized to true if the connection closed cleanly + // and false otherwise, the code attribute initialized to + // the WebSocket connection close code, and the reason + // attribute initialized to the result of applying UTF-8 + // decode without BOM to the WebSocket connection close + // reason. + fireEvent('close', ws, CloseEvent, { + wasClean, code, reason + }) + + if (channels.close.hasSubscribers) { + channels.close.publish({ + websocket: ws, + code, + reason + }) + } +} + +function onSocketError (error) { + const { ws } = this + + ws[kReadyState] = states.CLOSING + + if (channels.socketError.hasSubscribers) { + channels.socketError.publish(error) + } + + this.destroy() +} + +module.exports = { + establishWebSocketConnection +} + + +/***/ }), + +/***/ 9188: +/***/ ((module) => { + +"use strict"; + + +// This is a Globally Unique Identifier unique used +// to validate that the endpoint accepts websocket +// connections. +// See https://www.rfc-editor.org/rfc/rfc6455.html#section-1.3 +const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + +/** @type {PropertyDescriptor} */ +const staticPropertyDescriptors = { + enumerable: true, + writable: false, + configurable: false +} + +const states = { + CONNECTING: 0, + OPEN: 1, + CLOSING: 2, + CLOSED: 3 +} + +const opcodes = { + CONTINUATION: 0x0, + TEXT: 0x1, + BINARY: 0x2, + CLOSE: 0x8, + PING: 0x9, + PONG: 0xA +} + +const maxUnsigned16Bit = 2 ** 16 - 1 // 65535 + +const parserStates = { + INFO: 0, + PAYLOADLENGTH_16: 2, + PAYLOADLENGTH_64: 3, + READ_DATA: 4 +} + +const emptyBuffer = Buffer.allocUnsafe(0) + +module.exports = { + uid, + staticPropertyDescriptors, + states, + opcodes, + maxUnsigned16Bit, + parserStates, + emptyBuffer +} + + +/***/ }), + +/***/ 2611: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { webidl } = __nccwpck_require__(1744) +const { kEnumerableProperty } = __nccwpck_require__(3983) +const { MessagePort } = __nccwpck_require__(1267) + +/** + * @see https://html.spec.whatwg.org/multipage/comms.html#messageevent + */ +class MessageEvent extends Event { + #eventInit + + constructor (type, eventInitDict = {}) { + webidl.argumentLengthCheck(arguments, 1, { header: 'MessageEvent constructor' }) + + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.MessageEventInit(eventInitDict) + + super(type, eventInitDict) + + this.#eventInit = eventInitDict + } + + get data () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.data + } + + get origin () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.origin + } + + get lastEventId () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.lastEventId + } + + get source () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.source + } + + get ports () { + webidl.brandCheck(this, MessageEvent) + + if (!Object.isFrozen(this.#eventInit.ports)) { + Object.freeze(this.#eventInit.ports) + } + + return this.#eventInit.ports + } + + initMessageEvent ( + type, + bubbles = false, + cancelable = false, + data = null, + origin = '', + lastEventId = '', + source = null, + ports = [] + ) { + webidl.brandCheck(this, MessageEvent) + + webidl.argumentLengthCheck(arguments, 1, { header: 'MessageEvent.initMessageEvent' }) + + return new MessageEvent(type, { + bubbles, cancelable, data, origin, lastEventId, source, ports + }) + } +} + +/** + * @see https://websockets.spec.whatwg.org/#the-closeevent-interface + */ +class CloseEvent extends Event { + #eventInit + + constructor (type, eventInitDict = {}) { + webidl.argumentLengthCheck(arguments, 1, { header: 'CloseEvent constructor' }) + + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.CloseEventInit(eventInitDict) + + super(type, eventInitDict) + + this.#eventInit = eventInitDict + } + + get wasClean () { + webidl.brandCheck(this, CloseEvent) + + return this.#eventInit.wasClean + } + + get code () { + webidl.brandCheck(this, CloseEvent) + + return this.#eventInit.code + } + + get reason () { + webidl.brandCheck(this, CloseEvent) + + return this.#eventInit.reason + } +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#the-errorevent-interface +class ErrorEvent extends Event { + #eventInit + + constructor (type, eventInitDict) { + webidl.argumentLengthCheck(arguments, 1, { header: 'ErrorEvent constructor' }) + + super(type, eventInitDict) + + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.ErrorEventInit(eventInitDict ?? {}) + + this.#eventInit = eventInitDict + } + + get message () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.message + } + + get filename () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.filename + } + + get lineno () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.lineno + } + + get colno () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.colno + } + + get error () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.error + } +} + +Object.defineProperties(MessageEvent.prototype, { + [Symbol.toStringTag]: { + value: 'MessageEvent', + configurable: true + }, + data: kEnumerableProperty, + origin: kEnumerableProperty, + lastEventId: kEnumerableProperty, + source: kEnumerableProperty, + ports: kEnumerableProperty, + initMessageEvent: kEnumerableProperty +}) + +Object.defineProperties(CloseEvent.prototype, { + [Symbol.toStringTag]: { + value: 'CloseEvent', + configurable: true + }, + reason: kEnumerableProperty, + code: kEnumerableProperty, + wasClean: kEnumerableProperty +}) + +Object.defineProperties(ErrorEvent.prototype, { + [Symbol.toStringTag]: { + value: 'ErrorEvent', + configurable: true + }, + message: kEnumerableProperty, + filename: kEnumerableProperty, + lineno: kEnumerableProperty, + colno: kEnumerableProperty, + error: kEnumerableProperty +}) + +webidl.converters.MessagePort = webidl.interfaceConverter(MessagePort) + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.MessagePort +) + +const eventInit = [ + { + key: 'bubbles', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'cancelable', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'composed', + converter: webidl.converters.boolean, + defaultValue: false + } +] + +webidl.converters.MessageEventInit = webidl.dictionaryConverter([ + ...eventInit, + { + key: 'data', + converter: webidl.converters.any, + defaultValue: null + }, + { + key: 'origin', + converter: webidl.converters.USVString, + defaultValue: '' + }, + { + key: 'lastEventId', + converter: webidl.converters.DOMString, + defaultValue: '' + }, + { + key: 'source', + // Node doesn't implement WindowProxy or ServiceWorker, so the only + // valid value for source is a MessagePort. + converter: webidl.nullableConverter(webidl.converters.MessagePort), + defaultValue: null + }, + { + key: 'ports', + converter: webidl.converters['sequence'], + get defaultValue () { + return [] + } + } +]) + +webidl.converters.CloseEventInit = webidl.dictionaryConverter([ + ...eventInit, + { + key: 'wasClean', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'code', + converter: webidl.converters['unsigned short'], + defaultValue: 0 + }, + { + key: 'reason', + converter: webidl.converters.USVString, + defaultValue: '' + } +]) + +webidl.converters.ErrorEventInit = webidl.dictionaryConverter([ + ...eventInit, + { + key: 'message', + converter: webidl.converters.DOMString, + defaultValue: '' + }, + { + key: 'filename', + converter: webidl.converters.USVString, + defaultValue: '' + }, + { + key: 'lineno', + converter: webidl.converters['unsigned long'], + defaultValue: 0 + }, + { + key: 'colno', + converter: webidl.converters['unsigned long'], + defaultValue: 0 + }, + { + key: 'error', + converter: webidl.converters.any + } +]) + +module.exports = { + MessageEvent, + CloseEvent, + ErrorEvent +} + + +/***/ }), + +/***/ 5444: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { maxUnsigned16Bit } = __nccwpck_require__(9188) + +/** @type {import('crypto')} */ +let crypto +try { + crypto = __nccwpck_require__(6113) +} catch { + +} + +class WebsocketFrameSend { + /** + * @param {Buffer|undefined} data + */ + constructor (data) { + this.frameData = data + this.maskKey = crypto.randomBytes(4) + } + + createFrame (opcode) { + const bodyLength = this.frameData?.byteLength ?? 0 + + /** @type {number} */ + let payloadLength = bodyLength // 0-125 + let offset = 6 + + if (bodyLength > maxUnsigned16Bit) { + offset += 8 // payload length is next 8 bytes + payloadLength = 127 + } else if (bodyLength > 125) { + offset += 2 // payload length is next 2 bytes + payloadLength = 126 + } + + const buffer = Buffer.allocUnsafe(bodyLength + offset) + + // Clear first 2 bytes, everything else is overwritten + buffer[0] = buffer[1] = 0 + buffer[0] |= 0x80 // FIN + buffer[0] = (buffer[0] & 0xF0) + opcode // opcode + + /*! ws. MIT License. Einar Otto Stangvik */ + buffer[offset - 4] = this.maskKey[0] + buffer[offset - 3] = this.maskKey[1] + buffer[offset - 2] = this.maskKey[2] + buffer[offset - 1] = this.maskKey[3] + + buffer[1] = payloadLength + + if (payloadLength === 126) { + buffer.writeUInt16BE(bodyLength, 2) + } else if (payloadLength === 127) { + // Clear extended payload length + buffer[2] = buffer[3] = 0 + buffer.writeUIntBE(bodyLength, 4, 6) + } + + buffer[1] |= 0x80 // MASK + + // mask body + for (let i = 0; i < bodyLength; i++) { + buffer[offset + i] = this.frameData[i] ^ this.maskKey[i % 4] + } + + return buffer + } +} + +module.exports = { + WebsocketFrameSend +} + + +/***/ }), + +/***/ 1688: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { Writable } = __nccwpck_require__(2781) +const diagnosticsChannel = __nccwpck_require__(7643) +const { parserStates, opcodes, states, emptyBuffer } = __nccwpck_require__(9188) +const { kReadyState, kSentClose, kResponse, kReceivedClose } = __nccwpck_require__(7578) +const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived } = __nccwpck_require__(5515) +const { WebsocketFrameSend } = __nccwpck_require__(5444) + +// This code was influenced by ws released under the MIT license. +// Copyright (c) 2011 Einar Otto Stangvik +// Copyright (c) 2013 Arnout Kazemier and contributors +// Copyright (c) 2016 Luigi Pinca and contributors + +const channels = {} +channels.ping = diagnosticsChannel.channel('undici:websocket:ping') +channels.pong = diagnosticsChannel.channel('undici:websocket:pong') + +class ByteParser extends Writable { + #buffers = [] + #byteOffset = 0 + + #state = parserStates.INFO + + #info = {} + #fragments = [] + + constructor (ws) { + super() + + this.ws = ws + } + + /** + * @param {Buffer} chunk + * @param {() => void} callback + */ + _write (chunk, _, callback) { + this.#buffers.push(chunk) + this.#byteOffset += chunk.length + + this.run(callback) + } + + /** + * Runs whenever a new chunk is received. + * Callback is called whenever there are no more chunks buffering, + * or not enough bytes are buffered to parse. + */ + run (callback) { + while (true) { + if (this.#state === parserStates.INFO) { + // If there aren't enough bytes to parse the payload length, etc. + if (this.#byteOffset < 2) { + return callback() + } + + const buffer = this.consume(2) + + this.#info.fin = (buffer[0] & 0x80) !== 0 + this.#info.opcode = buffer[0] & 0x0F + + // If we receive a fragmented message, we use the type of the first + // frame to parse the full message as binary/text, when it's terminated + this.#info.originalOpcode ??= this.#info.opcode + + this.#info.fragmented = !this.#info.fin && this.#info.opcode !== opcodes.CONTINUATION + + if (this.#info.fragmented && this.#info.opcode !== opcodes.BINARY && this.#info.opcode !== opcodes.TEXT) { + // Only text and binary frames can be fragmented + failWebsocketConnection(this.ws, 'Invalid frame type was fragmented.') + return + } + + const payloadLength = buffer[1] & 0x7F + + if (payloadLength <= 125) { + this.#info.payloadLength = payloadLength + this.#state = parserStates.READ_DATA + } else if (payloadLength === 126) { + this.#state = parserStates.PAYLOADLENGTH_16 + } else if (payloadLength === 127) { + this.#state = parserStates.PAYLOADLENGTH_64 + } + + if (this.#info.fragmented && payloadLength > 125) { + // A fragmented frame can't be fragmented itself + failWebsocketConnection(this.ws, 'Fragmented frame exceeded 125 bytes.') + return + } else if ( + (this.#info.opcode === opcodes.PING || + this.#info.opcode === opcodes.PONG || + this.#info.opcode === opcodes.CLOSE) && + payloadLength > 125 + ) { + // Control frames can have a payload length of 125 bytes MAX + failWebsocketConnection(this.ws, 'Payload length for control frame exceeded 125 bytes.') + return + } else if (this.#info.opcode === opcodes.CLOSE) { + if (payloadLength === 1) { + failWebsocketConnection(this.ws, 'Received close frame with a 1-byte body.') + return + } + + const body = this.consume(payloadLength) + + this.#info.closeInfo = this.parseCloseBody(false, body) + + if (!this.ws[kSentClose]) { + // If an endpoint receives a Close frame and did not previously send a + // Close frame, the endpoint MUST send a Close frame in response. (When + // sending a Close frame in response, the endpoint typically echos the + // status code it received.) + const body = Buffer.allocUnsafe(2) + body.writeUInt16BE(this.#info.closeInfo.code, 0) + const closeFrame = new WebsocketFrameSend(body) + + this.ws[kResponse].socket.write( + closeFrame.createFrame(opcodes.CLOSE), + (err) => { + if (!err) { + this.ws[kSentClose] = true + } + } + ) + } + + // Upon either sending or receiving a Close control frame, it is said + // that _The WebSocket Closing Handshake is Started_ and that the + // WebSocket connection is in the CLOSING state. + this.ws[kReadyState] = states.CLOSING + this.ws[kReceivedClose] = true + + this.end() + + return + } else if (this.#info.opcode === opcodes.PING) { + // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in + // response, unless it already received a Close frame. + // A Pong frame sent in response to a Ping frame must have identical + // "Application data" + + const body = this.consume(payloadLength) + + if (!this.ws[kReceivedClose]) { + const frame = new WebsocketFrameSend(body) + + this.ws[kResponse].socket.write(frame.createFrame(opcodes.PONG)) + + if (channels.ping.hasSubscribers) { + channels.ping.publish({ + payload: body + }) + } + } + + this.#state = parserStates.INFO + + if (this.#byteOffset > 0) { + continue + } else { + callback() + return + } + } else if (this.#info.opcode === opcodes.PONG) { + // A Pong frame MAY be sent unsolicited. This serves as a + // unidirectional heartbeat. A response to an unsolicited Pong frame is + // not expected. + + const body = this.consume(payloadLength) + + if (channels.pong.hasSubscribers) { + channels.pong.publish({ + payload: body + }) + } + + if (this.#byteOffset > 0) { + continue + } else { + callback() + return + } + } + } else if (this.#state === parserStates.PAYLOADLENGTH_16) { + if (this.#byteOffset < 2) { + return callback() + } + + const buffer = this.consume(2) + + this.#info.payloadLength = buffer.readUInt16BE(0) + this.#state = parserStates.READ_DATA + } else if (this.#state === parserStates.PAYLOADLENGTH_64) { + if (this.#byteOffset < 8) { + return callback() + } + + const buffer = this.consume(8) + const upper = buffer.readUInt32BE(0) + + // 2^31 is the maxinimum bytes an arraybuffer can contain + // on 32-bit systems. Although, on 64-bit systems, this is + // 2^53-1 bytes. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length + // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275 + // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e + if (upper > 2 ** 31 - 1) { + failWebsocketConnection(this.ws, 'Received payload length > 2^31 bytes.') + return + } + + const lower = buffer.readUInt32BE(4) + + this.#info.payloadLength = (upper << 8) + lower + this.#state = parserStates.READ_DATA + } else if (this.#state === parserStates.READ_DATA) { + if (this.#byteOffset < this.#info.payloadLength) { + // If there is still more data in this chunk that needs to be read + return callback() + } else if (this.#byteOffset >= this.#info.payloadLength) { + // If the server sent multiple frames in a single chunk + + const body = this.consume(this.#info.payloadLength) + + this.#fragments.push(body) + + // If the frame is unfragmented, or a fragmented frame was terminated, + // a message was received + if (!this.#info.fragmented || (this.#info.fin && this.#info.opcode === opcodes.CONTINUATION)) { + const fullMessage = Buffer.concat(this.#fragments) + + websocketMessageReceived(this.ws, this.#info.originalOpcode, fullMessage) + + this.#info = {} + this.#fragments.length = 0 + } + + this.#state = parserStates.INFO + } + } + + if (this.#byteOffset > 0) { + continue + } else { + callback() + break + } + } + } + + /** + * Take n bytes from the buffered Buffers + * @param {number} n + * @returns {Buffer|null} + */ + consume (n) { + if (n > this.#byteOffset) { + return null + } else if (n === 0) { + return emptyBuffer + } + + if (this.#buffers[0].length === n) { + this.#byteOffset -= this.#buffers[0].length + return this.#buffers.shift() + } + + const buffer = Buffer.allocUnsafe(n) + let offset = 0 + + while (offset !== n) { + const next = this.#buffers[0] + const { length } = next + + if (length + offset === n) { + buffer.set(this.#buffers.shift(), offset) + break + } else if (length + offset > n) { + buffer.set(next.subarray(0, n - offset), offset) + this.#buffers[0] = next.subarray(n - offset) + break + } else { + buffer.set(this.#buffers.shift(), offset) + offset += next.length + } + } + + this.#byteOffset -= n + + return buffer + } + + parseCloseBody (onlyCode, data) { + // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5 + /** @type {number|undefined} */ + let code + + if (data.length >= 2) { + // _The WebSocket Connection Close Code_ is + // defined as the status code (Section 7.4) contained in the first Close + // control frame received by the application + code = data.readUInt16BE(0) + } + + if (onlyCode) { + if (!isValidStatusCode(code)) { + return null + } + + return { code } + } + + // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6 + /** @type {Buffer} */ + let reason = data.subarray(2) + + // Remove BOM + if (reason[0] === 0xEF && reason[1] === 0xBB && reason[2] === 0xBF) { + reason = reason.subarray(3) + } + + if (code !== undefined && !isValidStatusCode(code)) { + return null + } + + try { + // TODO: optimize this + reason = new TextDecoder('utf-8', { fatal: true }).decode(reason) + } catch { + return null + } + + return { code, reason } + } + + get closingInfo () { + return this.#info.closeInfo + } +} + +module.exports = { + ByteParser +} + + +/***/ }), + +/***/ 7578: +/***/ ((module) => { + +"use strict"; + + +module.exports = { + kWebSocketURL: Symbol('url'), + kReadyState: Symbol('ready state'), + kController: Symbol('controller'), + kResponse: Symbol('response'), + kBinaryType: Symbol('binary type'), + kSentClose: Symbol('sent close'), + kReceivedClose: Symbol('received close'), + kByteParser: Symbol('byte parser') +} + + +/***/ }), + +/***/ 5515: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kReadyState, kController, kResponse, kBinaryType, kWebSocketURL } = __nccwpck_require__(7578) +const { states, opcodes } = __nccwpck_require__(9188) +const { MessageEvent, ErrorEvent } = __nccwpck_require__(2611) + +/* globals Blob */ + +/** + * @param {import('./websocket').WebSocket} ws + */ +function isEstablished (ws) { + // If the server's response is validated as provided for above, it is + // said that _The WebSocket Connection is Established_ and that the + // WebSocket Connection is in the OPEN state. + return ws[kReadyState] === states.OPEN +} + +/** + * @param {import('./websocket').WebSocket} ws + */ +function isClosing (ws) { + // Upon either sending or receiving a Close control frame, it is said + // that _The WebSocket Closing Handshake is Started_ and that the + // WebSocket connection is in the CLOSING state. + return ws[kReadyState] === states.CLOSING +} + +/** + * @param {import('./websocket').WebSocket} ws + */ +function isClosed (ws) { + return ws[kReadyState] === states.CLOSED +} + +/** + * @see https://dom.spec.whatwg.org/#concept-event-fire + * @param {string} e + * @param {EventTarget} target + * @param {EventInit | undefined} eventInitDict + */ +function fireEvent (e, target, eventConstructor = Event, eventInitDict) { + // 1. If eventConstructor is not given, then let eventConstructor be Event. + + // 2. Let event be the result of creating an event given eventConstructor, + // in the relevant realm of target. + // 3. Initialize event’s type attribute to e. + const event = new eventConstructor(e, eventInitDict) // eslint-disable-line new-cap + + // 4. Initialize any other IDL attributes of event as described in the + // invocation of this algorithm. + + // 5. Return the result of dispatching event at target, with legacy target + // override flag set if set. + target.dispatchEvent(event) +} + +/** + * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol + * @param {import('./websocket').WebSocket} ws + * @param {number} type Opcode + * @param {Buffer} data application data + */ +function websocketMessageReceived (ws, type, data) { + // 1. If ready state is not OPEN (1), then return. + if (ws[kReadyState] !== states.OPEN) { + return + } + + // 2. Let dataForEvent be determined by switching on type and binary type: + let dataForEvent + + if (type === opcodes.TEXT) { + // -> type indicates that the data is Text + // a new DOMString containing data + try { + dataForEvent = new TextDecoder('utf-8', { fatal: true }).decode(data) + } catch { + failWebsocketConnection(ws, 'Received invalid UTF-8 in text frame.') + return + } + } else if (type === opcodes.BINARY) { + if (ws[kBinaryType] === 'blob') { + // -> type indicates that the data is Binary and binary type is "blob" + // a new Blob object, created in the relevant Realm of the WebSocket + // object, that represents data as its raw data + dataForEvent = new Blob([data]) + } else { + // -> type indicates that the data is Binary and binary type is "arraybuffer" + // a new ArrayBuffer object, created in the relevant Realm of the + // WebSocket object, whose contents are data + dataForEvent = new Uint8Array(data).buffer + } + } + + // 3. Fire an event named message at the WebSocket object, using MessageEvent, + // with the origin attribute initialized to the serialization of the WebSocket + // object’s url's origin, and the data attribute initialized to dataForEvent. + fireEvent('message', ws, MessageEvent, { + origin: ws[kWebSocketURL].origin, + data: dataForEvent + }) +} + +/** + * @see https://datatracker.ietf.org/doc/html/rfc6455 + * @see https://datatracker.ietf.org/doc/html/rfc2616 + * @see https://bugs.chromium.org/p/chromium/issues/detail?id=398407 + * @param {string} protocol + */ +function isValidSubprotocol (protocol) { + // If present, this value indicates one + // or more comma-separated subprotocol the client wishes to speak, + // ordered by preference. The elements that comprise this value + // MUST be non-empty strings with characters in the range U+0021 to + // U+007E not including separator characters as defined in + // [RFC2616] and MUST all be unique strings. + if (protocol.length === 0) { + return false + } + + for (const char of protocol) { + const code = char.charCodeAt(0) + + if ( + code < 0x21 || + code > 0x7E || + char === '(' || + char === ')' || + char === '<' || + char === '>' || + char === '@' || + char === ',' || + char === ';' || + char === ':' || + char === '\\' || + char === '"' || + char === '/' || + char === '[' || + char === ']' || + char === '?' || + char === '=' || + char === '{' || + char === '}' || + code === 32 || // SP + code === 9 // HT + ) { + return false + } + } + + return true +} + +/** + * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7-4 + * @param {number} code + */ +function isValidStatusCode (code) { + if (code >= 1000 && code < 1015) { + return ( + code !== 1004 && // reserved + code !== 1005 && // "MUST NOT be set as a status code" + code !== 1006 // "MUST NOT be set as a status code" + ) + } + + return code >= 3000 && code <= 4999 +} + +/** + * @param {import('./websocket').WebSocket} ws + * @param {string|undefined} reason + */ +function failWebsocketConnection (ws, reason) { + const { [kController]: controller, [kResponse]: response } = ws + + controller.abort() + + if (response?.socket && !response.socket.destroyed) { + response.socket.destroy() + } + + if (reason) { + fireEvent('error', ws, ErrorEvent, { + error: new Error(reason) + }) + } +} + +module.exports = { + isEstablished, + isClosing, + isClosed, + fireEvent, + isValidSubprotocol, + isValidStatusCode, + failWebsocketConnection, + websocketMessageReceived +} + + +/***/ }), + +/***/ 4284: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { webidl } = __nccwpck_require__(1744) +const { DOMException } = __nccwpck_require__(1037) +const { URLSerializer } = __nccwpck_require__(685) +const { getGlobalOrigin } = __nccwpck_require__(1246) +const { staticPropertyDescriptors, states, opcodes, emptyBuffer } = __nccwpck_require__(9188) +const { + kWebSocketURL, + kReadyState, + kController, + kBinaryType, + kResponse, + kSentClose, + kByteParser +} = __nccwpck_require__(7578) +const { isEstablished, isClosing, isValidSubprotocol, failWebsocketConnection, fireEvent } = __nccwpck_require__(5515) +const { establishWebSocketConnection } = __nccwpck_require__(5354) +const { WebsocketFrameSend } = __nccwpck_require__(5444) +const { ByteParser } = __nccwpck_require__(1688) +const { kEnumerableProperty, isBlobLike } = __nccwpck_require__(3983) +const { getGlobalDispatcher } = __nccwpck_require__(1892) +const { types } = __nccwpck_require__(3837) + +let experimentalWarned = false + +// https://websockets.spec.whatwg.org/#interface-definition +class WebSocket extends EventTarget { + #events = { + open: null, + error: null, + close: null, + message: null + } + + #bufferedAmount = 0 + #protocol = '' + #extensions = '' + + /** + * @param {string} url + * @param {string|string[]} protocols + */ + constructor (url, protocols = []) { + super() + + webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket constructor' }) + + if (!experimentalWarned) { + experimentalWarned = true + process.emitWarning('WebSockets are experimental, expect them to change at any time.', { + code: 'UNDICI-WS' + }) + } + + const options = webidl.converters['DOMString or sequence or WebSocketInit'](protocols) + + url = webidl.converters.USVString(url) + protocols = options.protocols + + // 1. Let baseURL be this's relevant settings object's API base URL. + const baseURL = getGlobalOrigin() + + // 1. Let urlRecord be the result of applying the URL parser to url with baseURL. + let urlRecord + + try { + urlRecord = new URL(url, baseURL) + } catch (e) { + // 3. If urlRecord is failure, then throw a "SyntaxError" DOMException. + throw new DOMException(e, 'SyntaxError') + } + + // 4. If urlRecord’s scheme is "http", then set urlRecord’s scheme to "ws". + if (urlRecord.protocol === 'http:') { + urlRecord.protocol = 'ws:' + } else if (urlRecord.protocol === 'https:') { + // 5. Otherwise, if urlRecord’s scheme is "https", set urlRecord’s scheme to "wss". + urlRecord.protocol = 'wss:' + } + + // 6. If urlRecord’s scheme is not "ws" or "wss", then throw a "SyntaxError" DOMException. + if (urlRecord.protocol !== 'ws:' && urlRecord.protocol !== 'wss:') { + throw new DOMException( + `Expected a ws: or wss: protocol, got ${urlRecord.protocol}`, + 'SyntaxError' + ) + } + + // 7. If urlRecord’s fragment is non-null, then throw a "SyntaxError" + // DOMException. + if (urlRecord.hash || urlRecord.href.endsWith('#')) { + throw new DOMException('Got fragment', 'SyntaxError') + } + + // 8. If protocols is a string, set protocols to a sequence consisting + // of just that string. + if (typeof protocols === 'string') { + protocols = [protocols] + } + + // 9. If any of the values in protocols occur more than once or otherwise + // fail to match the requirements for elements that comprise the value + // of `Sec-WebSocket-Protocol` fields as defined by The WebSocket + // protocol, then throw a "SyntaxError" DOMException. + if (protocols.length !== new Set(protocols.map(p => p.toLowerCase())).size) { + throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError') + } + + if (protocols.length > 0 && !protocols.every(p => isValidSubprotocol(p))) { + throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError') + } + + // 10. Set this's url to urlRecord. + this[kWebSocketURL] = new URL(urlRecord.href) + + // 11. Let client be this's relevant settings object. + + // 12. Run this step in parallel: + + // 1. Establish a WebSocket connection given urlRecord, protocols, + // and client. + this[kController] = establishWebSocketConnection( + urlRecord, + protocols, + this, + (response) => this.#onConnectionEstablished(response), + options + ) + + // Each WebSocket object has an associated ready state, which is a + // number representing the state of the connection. Initially it must + // be CONNECTING (0). + this[kReadyState] = WebSocket.CONNECTING + + // The extensions attribute must initially return the empty string. + + // The protocol attribute must initially return the empty string. + + // Each WebSocket object has an associated binary type, which is a + // BinaryType. Initially it must be "blob". + this[kBinaryType] = 'blob' + } + + /** + * @see https://websockets.spec.whatwg.org/#dom-websocket-close + * @param {number|undefined} code + * @param {string|undefined} reason + */ + close (code = undefined, reason = undefined) { + webidl.brandCheck(this, WebSocket) + + if (code !== undefined) { + code = webidl.converters['unsigned short'](code, { clamp: true }) + } + + if (reason !== undefined) { + reason = webidl.converters.USVString(reason) + } + + // 1. If code is present, but is neither an integer equal to 1000 nor an + // integer in the range 3000 to 4999, inclusive, throw an + // "InvalidAccessError" DOMException. + if (code !== undefined) { + if (code !== 1000 && (code < 3000 || code > 4999)) { + throw new DOMException('invalid code', 'InvalidAccessError') + } + } + + let reasonByteLength = 0 + + // 2. If reason is present, then run these substeps: + if (reason !== undefined) { + // 1. Let reasonBytes be the result of encoding reason. + // 2. If reasonBytes is longer than 123 bytes, then throw a + // "SyntaxError" DOMException. + reasonByteLength = Buffer.byteLength(reason) + + if (reasonByteLength > 123) { + throw new DOMException( + `Reason must be less than 123 bytes; received ${reasonByteLength}`, + 'SyntaxError' + ) + } + } + + // 3. Run the first matching steps from the following list: + if (this[kReadyState] === WebSocket.CLOSING || this[kReadyState] === WebSocket.CLOSED) { + // If this's ready state is CLOSING (2) or CLOSED (3) + // Do nothing. + } else if (!isEstablished(this)) { + // If the WebSocket connection is not yet established + // Fail the WebSocket connection and set this's ready state + // to CLOSING (2). + failWebsocketConnection(this, 'Connection was closed before it was established.') + this[kReadyState] = WebSocket.CLOSING + } else if (!isClosing(this)) { + // If the WebSocket closing handshake has not yet been started + // Start the WebSocket closing handshake and set this's ready + // state to CLOSING (2). + // - If neither code nor reason is present, the WebSocket Close + // message must not have a body. + // - If code is present, then the status code to use in the + // WebSocket Close message must be the integer given by code. + // - If reason is also present, then reasonBytes must be + // provided in the Close message after the status code. + + const frame = new WebsocketFrameSend() + + // If neither code nor reason is present, the WebSocket Close + // message must not have a body. + + // If code is present, then the status code to use in the + // WebSocket Close message must be the integer given by code. + if (code !== undefined && reason === undefined) { + frame.frameData = Buffer.allocUnsafe(2) + frame.frameData.writeUInt16BE(code, 0) + } else if (code !== undefined && reason !== undefined) { + // If reason is also present, then reasonBytes must be + // provided in the Close message after the status code. + frame.frameData = Buffer.allocUnsafe(2 + reasonByteLength) + frame.frameData.writeUInt16BE(code, 0) + // the body MAY contain UTF-8-encoded data with value /reason/ + frame.frameData.write(reason, 2, 'utf-8') + } else { + frame.frameData = emptyBuffer + } + + /** @type {import('stream').Duplex} */ + const socket = this[kResponse].socket + + socket.write(frame.createFrame(opcodes.CLOSE), (err) => { + if (!err) { + this[kSentClose] = true + } + }) + + // Upon either sending or receiving a Close control frame, it is said + // that _The WebSocket Closing Handshake is Started_ and that the + // WebSocket connection is in the CLOSING state. + this[kReadyState] = states.CLOSING + } else { + // Otherwise + // Set this's ready state to CLOSING (2). + this[kReadyState] = WebSocket.CLOSING + } + } + + /** + * @see https://websockets.spec.whatwg.org/#dom-websocket-send + * @param {NodeJS.TypedArray|ArrayBuffer|Blob|string} data + */ + send (data) { + webidl.brandCheck(this, WebSocket) + + webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket.send' }) + + data = webidl.converters.WebSocketSendData(data) + + // 1. If this's ready state is CONNECTING, then throw an + // "InvalidStateError" DOMException. + if (this[kReadyState] === WebSocket.CONNECTING) { + throw new DOMException('Sent before connected.', 'InvalidStateError') + } + + // 2. Run the appropriate set of steps from the following list: + // https://datatracker.ietf.org/doc/html/rfc6455#section-6.1 + // https://datatracker.ietf.org/doc/html/rfc6455#section-5.2 + + if (!isEstablished(this) || isClosing(this)) { + return + } + + /** @type {import('stream').Duplex} */ + const socket = this[kResponse].socket + + // If data is a string + if (typeof data === 'string') { + // If the WebSocket connection is established and the WebSocket + // closing handshake has not yet started, then the user agent + // must send a WebSocket Message comprised of the data argument + // using a text frame opcode; if the data cannot be sent, e.g. + // because it would need to be buffered but the buffer is full, + // the user agent must flag the WebSocket as full and then close + // the WebSocket connection. Any invocation of this method with a + // string argument that does not throw an exception must increase + // the bufferedAmount attribute by the number of bytes needed to + // express the argument as UTF-8. + + const value = Buffer.from(data) + const frame = new WebsocketFrameSend(value) + const buffer = frame.createFrame(opcodes.TEXT) + + this.#bufferedAmount += value.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= value.byteLength + }) + } else if (types.isArrayBuffer(data)) { + // If the WebSocket connection is established, and the WebSocket + // closing handshake has not yet started, then the user agent must + // send a WebSocket Message comprised of data using a binary frame + // opcode; if the data cannot be sent, e.g. because it would need + // to be buffered but the buffer is full, the user agent must flag + // the WebSocket as full and then close the WebSocket connection. + // The data to be sent is the data stored in the buffer described + // by the ArrayBuffer object. Any invocation of this method with an + // ArrayBuffer argument that does not throw an exception must + // increase the bufferedAmount attribute by the length of the + // ArrayBuffer in bytes. + + const value = Buffer.from(data) + const frame = new WebsocketFrameSend(value) + const buffer = frame.createFrame(opcodes.BINARY) + + this.#bufferedAmount += value.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= value.byteLength + }) + } else if (ArrayBuffer.isView(data)) { + // If the WebSocket connection is established, and the WebSocket + // closing handshake has not yet started, then the user agent must + // send a WebSocket Message comprised of data using a binary frame + // opcode; if the data cannot be sent, e.g. because it would need to + // be buffered but the buffer is full, the user agent must flag the + // WebSocket as full and then close the WebSocket connection. The + // data to be sent is the data stored in the section of the buffer + // described by the ArrayBuffer object that data references. Any + // invocation of this method with this kind of argument that does + // not throw an exception must increase the bufferedAmount attribute + // by the length of data’s buffer in bytes. + + const ab = Buffer.from(data, data.byteOffset, data.byteLength) + + const frame = new WebsocketFrameSend(ab) + const buffer = frame.createFrame(opcodes.BINARY) + + this.#bufferedAmount += ab.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= ab.byteLength + }) + } else if (isBlobLike(data)) { + // If the WebSocket connection is established, and the WebSocket + // closing handshake has not yet started, then the user agent must + // send a WebSocket Message comprised of data using a binary frame + // opcode; if the data cannot be sent, e.g. because it would need to + // be buffered but the buffer is full, the user agent must flag the + // WebSocket as full and then close the WebSocket connection. The data + // to be sent is the raw data represented by the Blob object. Any + // invocation of this method with a Blob argument that does not throw + // an exception must increase the bufferedAmount attribute by the size + // of the Blob object’s raw data, in bytes. + + const frame = new WebsocketFrameSend() + + data.arrayBuffer().then((ab) => { + const value = Buffer.from(ab) + frame.frameData = value + const buffer = frame.createFrame(opcodes.BINARY) + + this.#bufferedAmount += value.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= value.byteLength + }) + }) + } + } + + get readyState () { + webidl.brandCheck(this, WebSocket) + + // The readyState getter steps are to return this's ready state. + return this[kReadyState] + } + + get bufferedAmount () { + webidl.brandCheck(this, WebSocket) + + return this.#bufferedAmount + } + + get url () { + webidl.brandCheck(this, WebSocket) + + // The url getter steps are to return this's url, serialized. + return URLSerializer(this[kWebSocketURL]) + } + + get extensions () { + webidl.brandCheck(this, WebSocket) + + return this.#extensions + } + + get protocol () { + webidl.brandCheck(this, WebSocket) + + return this.#protocol + } + + get onopen () { + webidl.brandCheck(this, WebSocket) + + return this.#events.open + } + + set onopen (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.open) { + this.removeEventListener('open', this.#events.open) + } + + if (typeof fn === 'function') { + this.#events.open = fn + this.addEventListener('open', fn) + } else { + this.#events.open = null + } + } + + get onerror () { + webidl.brandCheck(this, WebSocket) + + return this.#events.error + } + + set onerror (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.error) { + this.removeEventListener('error', this.#events.error) + } + + if (typeof fn === 'function') { + this.#events.error = fn + this.addEventListener('error', fn) + } else { + this.#events.error = null + } + } + + get onclose () { + webidl.brandCheck(this, WebSocket) + + return this.#events.close + } + + set onclose (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.close) { + this.removeEventListener('close', this.#events.close) + } + + if (typeof fn === 'function') { + this.#events.close = fn + this.addEventListener('close', fn) + } else { + this.#events.close = null + } + } + + get onmessage () { + webidl.brandCheck(this, WebSocket) + + return this.#events.message + } + + set onmessage (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.message) { + this.removeEventListener('message', this.#events.message) + } + + if (typeof fn === 'function') { + this.#events.message = fn + this.addEventListener('message', fn) + } else { + this.#events.message = null + } + } + + get binaryType () { + webidl.brandCheck(this, WebSocket) + + return this[kBinaryType] + } + + set binaryType (type) { + webidl.brandCheck(this, WebSocket) + + if (type !== 'blob' && type !== 'arraybuffer') { + this[kBinaryType] = 'blob' + } else { + this[kBinaryType] = type + } + } + + /** + * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol + */ + #onConnectionEstablished (response) { + // processResponse is called when the "response’s header list has been received and initialized." + // once this happens, the connection is open + this[kResponse] = response + + const parser = new ByteParser(this) + parser.on('drain', function onParserDrain () { + this.ws[kResponse].socket.resume() + }) + + response.socket.ws = this + this[kByteParser] = parser + + // 1. Change the ready state to OPEN (1). + this[kReadyState] = states.OPEN + + // 2. Change the extensions attribute’s value to the extensions in use, if + // it is not the null value. + // https://datatracker.ietf.org/doc/html/rfc6455#section-9.1 + const extensions = response.headersList.get('sec-websocket-extensions') + + if (extensions !== null) { + this.#extensions = extensions + } + + // 3. Change the protocol attribute’s value to the subprotocol in use, if + // it is not the null value. + // https://datatracker.ietf.org/doc/html/rfc6455#section-1.9 + const protocol = response.headersList.get('sec-websocket-protocol') + + if (protocol !== null) { + this.#protocol = protocol + } + + // 4. Fire an event named open at the WebSocket object. + fireEvent('open', this) + } +} + +// https://websockets.spec.whatwg.org/#dom-websocket-connecting +WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING +// https://websockets.spec.whatwg.org/#dom-websocket-open +WebSocket.OPEN = WebSocket.prototype.OPEN = states.OPEN +// https://websockets.spec.whatwg.org/#dom-websocket-closing +WebSocket.CLOSING = WebSocket.prototype.CLOSING = states.CLOSING +// https://websockets.spec.whatwg.org/#dom-websocket-closed +WebSocket.CLOSED = WebSocket.prototype.CLOSED = states.CLOSED + +Object.defineProperties(WebSocket.prototype, { + CONNECTING: staticPropertyDescriptors, + OPEN: staticPropertyDescriptors, + CLOSING: staticPropertyDescriptors, + CLOSED: staticPropertyDescriptors, + url: kEnumerableProperty, + readyState: kEnumerableProperty, + bufferedAmount: kEnumerableProperty, + onopen: kEnumerableProperty, + onerror: kEnumerableProperty, + onclose: kEnumerableProperty, + close: kEnumerableProperty, + onmessage: kEnumerableProperty, + binaryType: kEnumerableProperty, + send: kEnumerableProperty, + extensions: kEnumerableProperty, + protocol: kEnumerableProperty, + [Symbol.toStringTag]: { + value: 'WebSocket', + writable: false, + enumerable: false, + configurable: true + } +}) + +Object.defineProperties(WebSocket, { + CONNECTING: staticPropertyDescriptors, + OPEN: staticPropertyDescriptors, + CLOSING: staticPropertyDescriptors, + CLOSED: staticPropertyDescriptors +}) + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.DOMString +) + +webidl.converters['DOMString or sequence'] = function (V) { + if (webidl.util.Type(V) === 'Object' && Symbol.iterator in V) { + return webidl.converters['sequence'](V) + } + + return webidl.converters.DOMString(V) +} + +// This implements the propsal made in https://github.com/whatwg/websockets/issues/42 +webidl.converters.WebSocketInit = webidl.dictionaryConverter([ + { + key: 'protocols', + converter: webidl.converters['DOMString or sequence'], + get defaultValue () { + return [] + } + }, + { + key: 'dispatcher', + converter: (V) => V, + get defaultValue () { + return getGlobalDispatcher() + } + }, + { + key: 'headers', + converter: webidl.nullableConverter(webidl.converters.HeadersInit) + } +]) + +webidl.converters['DOMString or sequence or WebSocketInit'] = function (V) { + if (webidl.util.Type(V) === 'Object' && !(Symbol.iterator in V)) { + return webidl.converters.WebSocketInit(V) + } + + return { protocols: webidl.converters['DOMString or sequence'](V) } +} + +webidl.converters.WebSocketSendData = function (V) { + if (webidl.util.Type(V) === 'Object') { + if (isBlobLike(V)) { + return webidl.converters.Blob(V, { strict: false }) + } + + if (ArrayBuffer.isView(V) || types.isAnyArrayBuffer(V)) { + return webidl.converters.BufferSource(V) + } + } + + return webidl.converters.USVString(V) +} + +module.exports = { + WebSocket +} + + +/***/ }), + +/***/ 4886: +/***/ ((module) => { + +"use strict"; + + +var conversions = {}; +module.exports = conversions; + +function sign(x) { + return x < 0 ? -1 : 1; +} + +function evenRound(x) { + // Round x to the nearest integer, choosing the even integer if it lies halfway between two. + if ((x % 1) === 0.5 && (x & 1) === 0) { // [even number].5; round down (i.e. floor) + return Math.floor(x); + } else { + return Math.round(x); + } +} + +function createNumberConversion(bitLength, typeOpts) { + if (!typeOpts.unsigned) { + --bitLength; + } + const lowerBound = typeOpts.unsigned ? 0 : -Math.pow(2, bitLength); + const upperBound = Math.pow(2, bitLength) - 1; + + const moduloVal = typeOpts.moduloBitLength ? Math.pow(2, typeOpts.moduloBitLength) : Math.pow(2, bitLength); + const moduloBound = typeOpts.moduloBitLength ? Math.pow(2, typeOpts.moduloBitLength - 1) : Math.pow(2, bitLength - 1); + + return function(V, opts) { + if (!opts) opts = {}; + + let x = +V; + + if (opts.enforceRange) { + if (!Number.isFinite(x)) { + throw new TypeError("Argument is not a finite number"); + } + + x = sign(x) * Math.floor(Math.abs(x)); + if (x < lowerBound || x > upperBound) { + throw new TypeError("Argument is not in byte range"); + } + + return x; + } + + if (!isNaN(x) && opts.clamp) { + x = evenRound(x); + + if (x < lowerBound) x = lowerBound; + if (x > upperBound) x = upperBound; + return x; + } + + if (!Number.isFinite(x) || x === 0) { + return 0; + } + + x = sign(x) * Math.floor(Math.abs(x)); + x = x % moduloVal; + + if (!typeOpts.unsigned && x >= moduloBound) { + return x - moduloVal; + } else if (typeOpts.unsigned) { + if (x < 0) { + x += moduloVal; + } else if (x === -0) { // don't return negative zero + return 0; + } + } + + return x; + } +} + +conversions["void"] = function () { + return undefined; +}; + +conversions["boolean"] = function (val) { + return !!val; +}; + +conversions["byte"] = createNumberConversion(8, { unsigned: false }); +conversions["octet"] = createNumberConversion(8, { unsigned: true }); + +conversions["short"] = createNumberConversion(16, { unsigned: false }); +conversions["unsigned short"] = createNumberConversion(16, { unsigned: true }); + +conversions["long"] = createNumberConversion(32, { unsigned: false }); +conversions["unsigned long"] = createNumberConversion(32, { unsigned: true }); + +conversions["long long"] = createNumberConversion(32, { unsigned: false, moduloBitLength: 64 }); +conversions["unsigned long long"] = createNumberConversion(32, { unsigned: true, moduloBitLength: 64 }); + +conversions["double"] = function (V) { + const x = +V; + + if (!Number.isFinite(x)) { + throw new TypeError("Argument is not a finite floating-point value"); + } + + return x; +}; + +conversions["unrestricted double"] = function (V) { + const x = +V; + + if (isNaN(x)) { + throw new TypeError("Argument is NaN"); + } + + return x; +}; + +// not quite valid, but good enough for JS +conversions["float"] = conversions["double"]; +conversions["unrestricted float"] = conversions["unrestricted double"]; + +conversions["DOMString"] = function (V, opts) { + if (!opts) opts = {}; + + if (opts.treatNullAsEmptyString && V === null) { + return ""; + } + + return String(V); +}; + +conversions["ByteString"] = function (V, opts) { + const x = String(V); + let c = undefined; + for (let i = 0; (c = x.codePointAt(i)) !== undefined; ++i) { + if (c > 255) { + throw new TypeError("Argument is not a valid bytestring"); + } + } + + return x; +}; + +conversions["USVString"] = function (V) { + const S = String(V); + const n = S.length; + const U = []; + for (let i = 0; i < n; ++i) { + const c = S.charCodeAt(i); + if (c < 0xD800 || c > 0xDFFF) { + U.push(String.fromCodePoint(c)); + } else if (0xDC00 <= c && c <= 0xDFFF) { + U.push(String.fromCodePoint(0xFFFD)); + } else { + if (i === n - 1) { + U.push(String.fromCodePoint(0xFFFD)); + } else { + const d = S.charCodeAt(i + 1); + if (0xDC00 <= d && d <= 0xDFFF) { + const a = c & 0x3FF; + const b = d & 0x3FF; + U.push(String.fromCodePoint((2 << 15) + (2 << 9) * a + b)); + ++i; + } else { + U.push(String.fromCodePoint(0xFFFD)); + } + } + } + } + + return U.join(''); +}; + +conversions["Date"] = function (V, opts) { + if (!(V instanceof Date)) { + throw new TypeError("Argument is not a Date object"); + } + if (isNaN(V)) { + return undefined; + } + + return V; +}; + +conversions["RegExp"] = function (V, opts) { + if (!(V instanceof RegExp)) { + V = new RegExp(V); + } + + return V; +}; + + +/***/ }), + +/***/ 7537: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +const usm = __nccwpck_require__(2158); + +exports.implementation = class URLImpl { + constructor(constructorArgs) { + const url = constructorArgs[0]; + const base = constructorArgs[1]; + + let parsedBase = null; + if (base !== undefined) { + parsedBase = usm.basicURLParse(base); + if (parsedBase === "failure") { + throw new TypeError("Invalid base URL"); + } + } + + const parsedURL = usm.basicURLParse(url, { baseURL: parsedBase }); + if (parsedURL === "failure") { + throw new TypeError("Invalid URL"); + } + + this._url = parsedURL; + + // TODO: query stuff + } + + get href() { + return usm.serializeURL(this._url); + } + + set href(v) { + const parsedURL = usm.basicURLParse(v); + if (parsedURL === "failure") { + throw new TypeError("Invalid URL"); + } + + this._url = parsedURL; + } + + get origin() { + return usm.serializeURLOrigin(this._url); + } + + get protocol() { + return this._url.scheme + ":"; + } + + set protocol(v) { + usm.basicURLParse(v + ":", { url: this._url, stateOverride: "scheme start" }); + } + + get username() { + return this._url.username; + } + + set username(v) { + if (usm.cannotHaveAUsernamePasswordPort(this._url)) { + return; + } + + usm.setTheUsername(this._url, v); + } + + get password() { + return this._url.password; + } + + set password(v) { + if (usm.cannotHaveAUsernamePasswordPort(this._url)) { + return; + } + + usm.setThePassword(this._url, v); + } + + get host() { + const url = this._url; + + if (url.host === null) { + return ""; + } + + if (url.port === null) { + return usm.serializeHost(url.host); + } + + return usm.serializeHost(url.host) + ":" + usm.serializeInteger(url.port); + } + + set host(v) { + if (this._url.cannotBeABaseURL) { + return; + } + + usm.basicURLParse(v, { url: this._url, stateOverride: "host" }); + } + + get hostname() { + if (this._url.host === null) { + return ""; + } + + return usm.serializeHost(this._url.host); + } + + set hostname(v) { + if (this._url.cannotBeABaseURL) { + return; + } + + usm.basicURLParse(v, { url: this._url, stateOverride: "hostname" }); + } + + get port() { + if (this._url.port === null) { + return ""; + } + + return usm.serializeInteger(this._url.port); + } + + set port(v) { + if (usm.cannotHaveAUsernamePasswordPort(this._url)) { + return; + } + + if (v === "") { + this._url.port = null; + } else { + usm.basicURLParse(v, { url: this._url, stateOverride: "port" }); + } + } + + get pathname() { + if (this._url.cannotBeABaseURL) { + return this._url.path[0]; + } + + if (this._url.path.length === 0) { + return ""; + } + + return "/" + this._url.path.join("/"); + } + + set pathname(v) { + if (this._url.cannotBeABaseURL) { + return; + } + + this._url.path = []; + usm.basicURLParse(v, { url: this._url, stateOverride: "path start" }); + } + + get search() { + if (this._url.query === null || this._url.query === "") { + return ""; + } + + return "?" + this._url.query; + } + + set search(v) { + // TODO: query stuff + + const url = this._url; + + if (v === "") { + url.query = null; + return; + } + + const input = v[0] === "?" ? v.substring(1) : v; + url.query = ""; + usm.basicURLParse(input, { url, stateOverride: "query" }); + } + + get hash() { + if (this._url.fragment === null || this._url.fragment === "") { + return ""; + } + + return "#" + this._url.fragment; + } + + set hash(v) { + if (v === "") { + this._url.fragment = null; + return; + } + + const input = v[0] === "#" ? v.substring(1) : v; + this._url.fragment = ""; + usm.basicURLParse(input, { url: this._url, stateOverride: "fragment" }); + } + + toJSON() { + return this.href; + } +}; + + +/***/ }), + +/***/ 3394: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const conversions = __nccwpck_require__(4886); +const utils = __nccwpck_require__(3185); +const Impl = __nccwpck_require__(7537); + +const impl = utils.implSymbol; + +function URL(url) { + if (!this || this[impl] || !(this instanceof URL)) { + throw new TypeError("Failed to construct 'URL': Please use the 'new' operator, this DOM object constructor cannot be called as a function."); + } + if (arguments.length < 1) { + throw new TypeError("Failed to construct 'URL': 1 argument required, but only " + arguments.length + " present."); + } + const args = []; + for (let i = 0; i < arguments.length && i < 2; ++i) { + args[i] = arguments[i]; + } + args[0] = conversions["USVString"](args[0]); + if (args[1] !== undefined) { + args[1] = conversions["USVString"](args[1]); + } + + module.exports.setup(this, args); +} + +URL.prototype.toJSON = function toJSON() { + if (!this || !module.exports.is(this)) { + throw new TypeError("Illegal invocation"); + } + const args = []; + for (let i = 0; i < arguments.length && i < 0; ++i) { + args[i] = arguments[i]; + } + return this[impl].toJSON.apply(this[impl], args); +}; +Object.defineProperty(URL.prototype, "href", { + get() { + return this[impl].href; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].href = V; + }, + enumerable: true, + configurable: true +}); + +URL.prototype.toString = function () { + if (!this || !module.exports.is(this)) { + throw new TypeError("Illegal invocation"); + } + return this.href; +}; + +Object.defineProperty(URL.prototype, "origin", { + get() { + return this[impl].origin; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(URL.prototype, "protocol", { + get() { + return this[impl].protocol; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].protocol = V; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(URL.prototype, "username", { + get() { + return this[impl].username; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].username = V; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(URL.prototype, "password", { + get() { + return this[impl].password; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].password = V; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(URL.prototype, "host", { + get() { + return this[impl].host; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].host = V; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(URL.prototype, "hostname", { + get() { + return this[impl].hostname; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].hostname = V; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(URL.prototype, "port", { + get() { + return this[impl].port; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].port = V; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(URL.prototype, "pathname", { + get() { + return this[impl].pathname; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].pathname = V; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(URL.prototype, "search", { + get() { + return this[impl].search; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].search = V; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(URL.prototype, "hash", { + get() { + return this[impl].hash; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].hash = V; + }, + enumerable: true, + configurable: true +}); + + +module.exports = { + is(obj) { + return !!obj && obj[impl] instanceof Impl.implementation; + }, + create(constructorArgs, privateData) { + let obj = Object.create(URL.prototype); + this.setup(obj, constructorArgs, privateData); + return obj; + }, + setup(obj, constructorArgs, privateData) { + if (!privateData) privateData = {}; + privateData.wrapper = obj; + + obj[impl] = new Impl.implementation(constructorArgs, privateData); + obj[impl][utils.wrapperSymbol] = obj; + }, + interface: URL, + expose: { + Window: { URL: URL }, + Worker: { URL: URL } + } +}; + + + +/***/ }), + +/***/ 8665: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +exports.URL = __nccwpck_require__(3394)["interface"]; +exports.serializeURL = __nccwpck_require__(2158).serializeURL; +exports.serializeURLOrigin = __nccwpck_require__(2158).serializeURLOrigin; +exports.basicURLParse = __nccwpck_require__(2158).basicURLParse; +exports.setTheUsername = __nccwpck_require__(2158).setTheUsername; +exports.setThePassword = __nccwpck_require__(2158).setThePassword; +exports.serializeHost = __nccwpck_require__(2158).serializeHost; +exports.serializeInteger = __nccwpck_require__(2158).serializeInteger; +exports.parseURL = __nccwpck_require__(2158).parseURL; + + +/***/ }), + +/***/ 2158: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + +const punycode = __nccwpck_require__(5477); +const tr46 = __nccwpck_require__(4256); + +const specialSchemes = { + ftp: 21, + file: null, + gopher: 70, + http: 80, + https: 443, + ws: 80, + wss: 443 +}; + +const failure = Symbol("failure"); + +function countSymbols(str) { + return punycode.ucs2.decode(str).length; +} + +function at(input, idx) { + const c = input[idx]; + return isNaN(c) ? undefined : String.fromCodePoint(c); +} + +function isASCIIDigit(c) { + return c >= 0x30 && c <= 0x39; +} + +function isASCIIAlpha(c) { + return (c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A); +} + +function isASCIIAlphanumeric(c) { + return isASCIIAlpha(c) || isASCIIDigit(c); +} + +function isASCIIHex(c) { + return isASCIIDigit(c) || (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66); +} + +function isSingleDot(buffer) { + return buffer === "." || buffer.toLowerCase() === "%2e"; +} + +function isDoubleDot(buffer) { + buffer = buffer.toLowerCase(); + return buffer === ".." || buffer === "%2e." || buffer === ".%2e" || buffer === "%2e%2e"; +} + +function isWindowsDriveLetterCodePoints(cp1, cp2) { + return isASCIIAlpha(cp1) && (cp2 === 58 || cp2 === 124); +} + +function isWindowsDriveLetterString(string) { + return string.length === 2 && isASCIIAlpha(string.codePointAt(0)) && (string[1] === ":" || string[1] === "|"); +} + +function isNormalizedWindowsDriveLetterString(string) { + return string.length === 2 && isASCIIAlpha(string.codePointAt(0)) && string[1] === ":"; +} + +function containsForbiddenHostCodePoint(string) { + return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|%|\/|:|\?|@|\[|\\|\]/) !== -1; +} + +function containsForbiddenHostCodePointExcludingPercent(string) { + return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|\/|:|\?|@|\[|\\|\]/) !== -1; +} + +function isSpecialScheme(scheme) { + return specialSchemes[scheme] !== undefined; +} + +function isSpecial(url) { + return isSpecialScheme(url.scheme); +} + +function defaultPort(scheme) { + return specialSchemes[scheme]; +} + +function percentEncode(c) { + let hex = c.toString(16).toUpperCase(); + if (hex.length === 1) { + hex = "0" + hex; + } + + return "%" + hex; +} + +function utf8PercentEncode(c) { + const buf = new Buffer(c); + + let str = ""; + + for (let i = 0; i < buf.length; ++i) { + str += percentEncode(buf[i]); + } + + return str; +} + +function utf8PercentDecode(str) { + const input = new Buffer(str); + const output = []; + for (let i = 0; i < input.length; ++i) { + if (input[i] !== 37) { + output.push(input[i]); + } else if (input[i] === 37 && isASCIIHex(input[i + 1]) && isASCIIHex(input[i + 2])) { + output.push(parseInt(input.slice(i + 1, i + 3).toString(), 16)); + i += 2; + } else { + output.push(input[i]); + } + } + return new Buffer(output).toString(); +} + +function isC0ControlPercentEncode(c) { + return c <= 0x1F || c > 0x7E; +} + +const extraPathPercentEncodeSet = new Set([32, 34, 35, 60, 62, 63, 96, 123, 125]); +function isPathPercentEncode(c) { + return isC0ControlPercentEncode(c) || extraPathPercentEncodeSet.has(c); +} + +const extraUserinfoPercentEncodeSet = + new Set([47, 58, 59, 61, 64, 91, 92, 93, 94, 124]); +function isUserinfoPercentEncode(c) { + return isPathPercentEncode(c) || extraUserinfoPercentEncodeSet.has(c); +} + +function percentEncodeChar(c, encodeSetPredicate) { + const cStr = String.fromCodePoint(c); + + if (encodeSetPredicate(c)) { + return utf8PercentEncode(cStr); + } + + return cStr; +} + +function parseIPv4Number(input) { + let R = 10; + + if (input.length >= 2 && input.charAt(0) === "0" && input.charAt(1).toLowerCase() === "x") { + input = input.substring(2); + R = 16; + } else if (input.length >= 2 && input.charAt(0) === "0") { + input = input.substring(1); + R = 8; + } + + if (input === "") { + return 0; + } + + const regex = R === 10 ? /[^0-9]/ : (R === 16 ? /[^0-9A-Fa-f]/ : /[^0-7]/); + if (regex.test(input)) { + return failure; + } + + return parseInt(input, R); +} + +function parseIPv4(input) { + const parts = input.split("."); + if (parts[parts.length - 1] === "") { + if (parts.length > 1) { + parts.pop(); + } + } + + if (parts.length > 4) { + return input; + } + + const numbers = []; + for (const part of parts) { + if (part === "") { + return input; + } + const n = parseIPv4Number(part); + if (n === failure) { + return input; + } + + numbers.push(n); + } + + for (let i = 0; i < numbers.length - 1; ++i) { + if (numbers[i] > 255) { + return failure; + } + } + if (numbers[numbers.length - 1] >= Math.pow(256, 5 - numbers.length)) { + return failure; + } + + let ipv4 = numbers.pop(); + let counter = 0; + + for (const n of numbers) { + ipv4 += n * Math.pow(256, 3 - counter); + ++counter; + } + + return ipv4; +} + +function serializeIPv4(address) { + let output = ""; + let n = address; + + for (let i = 1; i <= 4; ++i) { + output = String(n % 256) + output; + if (i !== 4) { + output = "." + output; + } + n = Math.floor(n / 256); + } + + return output; +} + +function parseIPv6(input) { + const address = [0, 0, 0, 0, 0, 0, 0, 0]; + let pieceIndex = 0; + let compress = null; + let pointer = 0; + + input = punycode.ucs2.decode(input); + + if (input[pointer] === 58) { + if (input[pointer + 1] !== 58) { + return failure; + } + + pointer += 2; + ++pieceIndex; + compress = pieceIndex; + } + + while (pointer < input.length) { + if (pieceIndex === 8) { + return failure; + } + + if (input[pointer] === 58) { + if (compress !== null) { + return failure; + } + ++pointer; + ++pieceIndex; + compress = pieceIndex; + continue; + } + + let value = 0; + let length = 0; + + while (length < 4 && isASCIIHex(input[pointer])) { + value = value * 0x10 + parseInt(at(input, pointer), 16); + ++pointer; + ++length; + } + + if (input[pointer] === 46) { + if (length === 0) { + return failure; + } + + pointer -= length; + + if (pieceIndex > 6) { + return failure; + } + + let numbersSeen = 0; + + while (input[pointer] !== undefined) { + let ipv4Piece = null; + + if (numbersSeen > 0) { + if (input[pointer] === 46 && numbersSeen < 4) { + ++pointer; + } else { + return failure; + } + } + + if (!isASCIIDigit(input[pointer])) { + return failure; + } + + while (isASCIIDigit(input[pointer])) { + const number = parseInt(at(input, pointer)); + if (ipv4Piece === null) { + ipv4Piece = number; + } else if (ipv4Piece === 0) { + return failure; + } else { + ipv4Piece = ipv4Piece * 10 + number; + } + if (ipv4Piece > 255) { + return failure; + } + ++pointer; + } + + address[pieceIndex] = address[pieceIndex] * 0x100 + ipv4Piece; + + ++numbersSeen; + + if (numbersSeen === 2 || numbersSeen === 4) { + ++pieceIndex; + } + } + + if (numbersSeen !== 4) { + return failure; + } + + break; + } else if (input[pointer] === 58) { + ++pointer; + if (input[pointer] === undefined) { + return failure; + } + } else if (input[pointer] !== undefined) { + return failure; + } + + address[pieceIndex] = value; + ++pieceIndex; + } + + if (compress !== null) { + let swaps = pieceIndex - compress; + pieceIndex = 7; + while (pieceIndex !== 0 && swaps > 0) { + const temp = address[compress + swaps - 1]; + address[compress + swaps - 1] = address[pieceIndex]; + address[pieceIndex] = temp; + --pieceIndex; + --swaps; + } + } else if (compress === null && pieceIndex !== 8) { + return failure; + } + + return address; +} + +function serializeIPv6(address) { + let output = ""; + const seqResult = findLongestZeroSequence(address); + const compress = seqResult.idx; + let ignore0 = false; + + for (let pieceIndex = 0; pieceIndex <= 7; ++pieceIndex) { + if (ignore0 && address[pieceIndex] === 0) { + continue; + } else if (ignore0) { + ignore0 = false; + } + + if (compress === pieceIndex) { + const separator = pieceIndex === 0 ? "::" : ":"; + output += separator; + ignore0 = true; + continue; + } + + output += address[pieceIndex].toString(16); + + if (pieceIndex !== 7) { + output += ":"; + } + } + + return output; +} + +function parseHost(input, isSpecialArg) { + if (input[0] === "[") { + if (input[input.length - 1] !== "]") { + return failure; + } + + return parseIPv6(input.substring(1, input.length - 1)); + } + + if (!isSpecialArg) { + return parseOpaqueHost(input); + } + + const domain = utf8PercentDecode(input); + const asciiDomain = tr46.toASCII(domain, false, tr46.PROCESSING_OPTIONS.NONTRANSITIONAL, false); + if (asciiDomain === null) { + return failure; + } + + if (containsForbiddenHostCodePoint(asciiDomain)) { + return failure; + } + + const ipv4Host = parseIPv4(asciiDomain); + if (typeof ipv4Host === "number" || ipv4Host === failure) { + return ipv4Host; + } + + return asciiDomain; +} + +function parseOpaqueHost(input) { + if (containsForbiddenHostCodePointExcludingPercent(input)) { + return failure; + } + + let output = ""; + const decoded = punycode.ucs2.decode(input); + for (let i = 0; i < decoded.length; ++i) { + output += percentEncodeChar(decoded[i], isC0ControlPercentEncode); + } + return output; +} + +function findLongestZeroSequence(arr) { + let maxIdx = null; + let maxLen = 1; // only find elements > 1 + let currStart = null; + let currLen = 0; + + for (let i = 0; i < arr.length; ++i) { + if (arr[i] !== 0) { + if (currLen > maxLen) { + maxIdx = currStart; + maxLen = currLen; + } + + currStart = null; + currLen = 0; + } else { + if (currStart === null) { + currStart = i; + } + ++currLen; + } + } + + // if trailing zeros + if (currLen > maxLen) { + maxIdx = currStart; + maxLen = currLen; + } + + return { + idx: maxIdx, + len: maxLen + }; +} + +function serializeHost(host) { + if (typeof host === "number") { + return serializeIPv4(host); + } + + // IPv6 serializer + if (host instanceof Array) { + return "[" + serializeIPv6(host) + "]"; + } + + return host; +} + +function trimControlChars(url) { + return url.replace(/^[\u0000-\u001F\u0020]+|[\u0000-\u001F\u0020]+$/g, ""); +} + +function trimTabAndNewline(url) { + return url.replace(/\u0009|\u000A|\u000D/g, ""); +} + +function shortenPath(url) { + const path = url.path; + if (path.length === 0) { + return; + } + if (url.scheme === "file" && path.length === 1 && isNormalizedWindowsDriveLetter(path[0])) { + return; + } + + path.pop(); +} + +function includesCredentials(url) { + return url.username !== "" || url.password !== ""; +} + +function cannotHaveAUsernamePasswordPort(url) { + return url.host === null || url.host === "" || url.cannotBeABaseURL || url.scheme === "file"; +} + +function isNormalizedWindowsDriveLetter(string) { + return /^[A-Za-z]:$/.test(string); +} + +function URLStateMachine(input, base, encodingOverride, url, stateOverride) { + this.pointer = 0; + this.input = input; + this.base = base || null; + this.encodingOverride = encodingOverride || "utf-8"; + this.stateOverride = stateOverride; + this.url = url; + this.failure = false; + this.parseError = false; + + if (!this.url) { + this.url = { + scheme: "", + username: "", + password: "", + host: null, + port: null, + path: [], + query: null, + fragment: null, + + cannotBeABaseURL: false + }; + + const res = trimControlChars(this.input); + if (res !== this.input) { + this.parseError = true; + } + this.input = res; + } + + const res = trimTabAndNewline(this.input); + if (res !== this.input) { + this.parseError = true; + } + this.input = res; + + this.state = stateOverride || "scheme start"; + + this.buffer = ""; + this.atFlag = false; + this.arrFlag = false; + this.passwordTokenSeenFlag = false; + + this.input = punycode.ucs2.decode(this.input); + + for (; this.pointer <= this.input.length; ++this.pointer) { + const c = this.input[this.pointer]; + const cStr = isNaN(c) ? undefined : String.fromCodePoint(c); + + // exec state machine + const ret = this["parse " + this.state](c, cStr); + if (!ret) { + break; // terminate algorithm + } else if (ret === failure) { + this.failure = true; + break; + } + } +} + +URLStateMachine.prototype["parse scheme start"] = function parseSchemeStart(c, cStr) { + if (isASCIIAlpha(c)) { + this.buffer += cStr.toLowerCase(); + this.state = "scheme"; + } else if (!this.stateOverride) { + this.state = "no scheme"; + --this.pointer; + } else { + this.parseError = true; + return failure; + } + + return true; +}; + +URLStateMachine.prototype["parse scheme"] = function parseScheme(c, cStr) { + if (isASCIIAlphanumeric(c) || c === 43 || c === 45 || c === 46) { + this.buffer += cStr.toLowerCase(); + } else if (c === 58) { + if (this.stateOverride) { + if (isSpecial(this.url) && !isSpecialScheme(this.buffer)) { + return false; + } + + if (!isSpecial(this.url) && isSpecialScheme(this.buffer)) { + return false; + } + + if ((includesCredentials(this.url) || this.url.port !== null) && this.buffer === "file") { + return false; + } + + if (this.url.scheme === "file" && (this.url.host === "" || this.url.host === null)) { + return false; + } + } + this.url.scheme = this.buffer; + this.buffer = ""; + if (this.stateOverride) { + return false; + } + if (this.url.scheme === "file") { + if (this.input[this.pointer + 1] !== 47 || this.input[this.pointer + 2] !== 47) { + this.parseError = true; + } + this.state = "file"; + } else if (isSpecial(this.url) && this.base !== null && this.base.scheme === this.url.scheme) { + this.state = "special relative or authority"; + } else if (isSpecial(this.url)) { + this.state = "special authority slashes"; + } else if (this.input[this.pointer + 1] === 47) { + this.state = "path or authority"; + ++this.pointer; + } else { + this.url.cannotBeABaseURL = true; + this.url.path.push(""); + this.state = "cannot-be-a-base-URL path"; + } + } else if (!this.stateOverride) { + this.buffer = ""; + this.state = "no scheme"; + this.pointer = -1; + } else { + this.parseError = true; + return failure; + } + + return true; +}; + +URLStateMachine.prototype["parse no scheme"] = function parseNoScheme(c) { + if (this.base === null || (this.base.cannotBeABaseURL && c !== 35)) { + return failure; + } else if (this.base.cannotBeABaseURL && c === 35) { + this.url.scheme = this.base.scheme; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + this.url.fragment = ""; + this.url.cannotBeABaseURL = true; + this.state = "fragment"; + } else if (this.base.scheme === "file") { + this.state = "file"; + --this.pointer; + } else { + this.state = "relative"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse special relative or authority"] = function parseSpecialRelativeOrAuthority(c) { + if (c === 47 && this.input[this.pointer + 1] === 47) { + this.state = "special authority ignore slashes"; + ++this.pointer; + } else { + this.parseError = true; + this.state = "relative"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse path or authority"] = function parsePathOrAuthority(c) { + if (c === 47) { + this.state = "authority"; + } else { + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse relative"] = function parseRelative(c) { + this.url.scheme = this.base.scheme; + if (isNaN(c)) { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + } else if (c === 47) { + this.state = "relative slash"; + } else if (c === 63) { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(); + this.url.query = ""; + this.state = "query"; + } else if (c === 35) { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + this.url.fragment = ""; + this.state = "fragment"; + } else if (isSpecial(this.url) && c === 92) { + this.parseError = true; + this.state = "relative slash"; + } else { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(0, this.base.path.length - 1); + + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse relative slash"] = function parseRelativeSlash(c) { + if (isSpecial(this.url) && (c === 47 || c === 92)) { + if (c === 92) { + this.parseError = true; + } + this.state = "special authority ignore slashes"; + } else if (c === 47) { + this.state = "authority"; + } else { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse special authority slashes"] = function parseSpecialAuthoritySlashes(c) { + if (c === 47 && this.input[this.pointer + 1] === 47) { + this.state = "special authority ignore slashes"; + ++this.pointer; + } else { + this.parseError = true; + this.state = "special authority ignore slashes"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse special authority ignore slashes"] = function parseSpecialAuthorityIgnoreSlashes(c) { + if (c !== 47 && c !== 92) { + this.state = "authority"; + --this.pointer; + } else { + this.parseError = true; + } + + return true; +}; + +URLStateMachine.prototype["parse authority"] = function parseAuthority(c, cStr) { + if (c === 64) { + this.parseError = true; + if (this.atFlag) { + this.buffer = "%40" + this.buffer; + } + this.atFlag = true; + + // careful, this is based on buffer and has its own pointer (this.pointer != pointer) and inner chars + const len = countSymbols(this.buffer); + for (let pointer = 0; pointer < len; ++pointer) { + const codePoint = this.buffer.codePointAt(pointer); + + if (codePoint === 58 && !this.passwordTokenSeenFlag) { + this.passwordTokenSeenFlag = true; + continue; + } + const encodedCodePoints = percentEncodeChar(codePoint, isUserinfoPercentEncode); + if (this.passwordTokenSeenFlag) { + this.url.password += encodedCodePoints; + } else { + this.url.username += encodedCodePoints; + } + } + this.buffer = ""; + } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || + (isSpecial(this.url) && c === 92)) { + if (this.atFlag && this.buffer === "") { + this.parseError = true; + return failure; + } + this.pointer -= countSymbols(this.buffer) + 1; + this.buffer = ""; + this.state = "host"; + } else { + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse hostname"] = +URLStateMachine.prototype["parse host"] = function parseHostName(c, cStr) { + if (this.stateOverride && this.url.scheme === "file") { + --this.pointer; + this.state = "file host"; + } else if (c === 58 && !this.arrFlag) { + if (this.buffer === "") { + this.parseError = true; + return failure; + } + + const host = parseHost(this.buffer, isSpecial(this.url)); + if (host === failure) { + return failure; + } + + this.url.host = host; + this.buffer = ""; + this.state = "port"; + if (this.stateOverride === "hostname") { + return false; + } + } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || + (isSpecial(this.url) && c === 92)) { + --this.pointer; + if (isSpecial(this.url) && this.buffer === "") { + this.parseError = true; + return failure; + } else if (this.stateOverride && this.buffer === "" && + (includesCredentials(this.url) || this.url.port !== null)) { + this.parseError = true; + return false; + } + + const host = parseHost(this.buffer, isSpecial(this.url)); + if (host === failure) { + return failure; + } + + this.url.host = host; + this.buffer = ""; + this.state = "path start"; + if (this.stateOverride) { + return false; + } + } else { + if (c === 91) { + this.arrFlag = true; + } else if (c === 93) { + this.arrFlag = false; + } + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse port"] = function parsePort(c, cStr) { + if (isASCIIDigit(c)) { + this.buffer += cStr; + } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || + (isSpecial(this.url) && c === 92) || + this.stateOverride) { + if (this.buffer !== "") { + const port = parseInt(this.buffer); + if (port > Math.pow(2, 16) - 1) { + this.parseError = true; + return failure; + } + this.url.port = port === defaultPort(this.url.scheme) ? null : port; + this.buffer = ""; + } + if (this.stateOverride) { + return false; + } + this.state = "path start"; + --this.pointer; + } else { + this.parseError = true; + return failure; + } + + return true; +}; + +const fileOtherwiseCodePoints = new Set([47, 92, 63, 35]); + +URLStateMachine.prototype["parse file"] = function parseFile(c) { + this.url.scheme = "file"; + + if (c === 47 || c === 92) { + if (c === 92) { + this.parseError = true; + } + this.state = "file slash"; + } else if (this.base !== null && this.base.scheme === "file") { + if (isNaN(c)) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + } else if (c === 63) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + this.url.query = ""; + this.state = "query"; + } else if (c === 35) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + this.url.fragment = ""; + this.state = "fragment"; + } else { + if (this.input.length - this.pointer - 1 === 0 || // remaining consists of 0 code points + !isWindowsDriveLetterCodePoints(c, this.input[this.pointer + 1]) || + (this.input.length - this.pointer - 1 >= 2 && // remaining has at least 2 code points + !fileOtherwiseCodePoints.has(this.input[this.pointer + 2]))) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + shortenPath(this.url); + } else { + this.parseError = true; + } + + this.state = "path"; + --this.pointer; + } + } else { + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse file slash"] = function parseFileSlash(c) { + if (c === 47 || c === 92) { + if (c === 92) { + this.parseError = true; + } + this.state = "file host"; + } else { + if (this.base !== null && this.base.scheme === "file") { + if (isNormalizedWindowsDriveLetterString(this.base.path[0])) { + this.url.path.push(this.base.path[0]); + } else { + this.url.host = this.base.host; + } + } + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse file host"] = function parseFileHost(c, cStr) { + if (isNaN(c) || c === 47 || c === 92 || c === 63 || c === 35) { + --this.pointer; + if (!this.stateOverride && isWindowsDriveLetterString(this.buffer)) { + this.parseError = true; + this.state = "path"; + } else if (this.buffer === "") { + this.url.host = ""; + if (this.stateOverride) { + return false; + } + this.state = "path start"; + } else { + let host = parseHost(this.buffer, isSpecial(this.url)); + if (host === failure) { + return failure; + } + if (host === "localhost") { + host = ""; + } + this.url.host = host; + + if (this.stateOverride) { + return false; + } + + this.buffer = ""; + this.state = "path start"; + } + } else { + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse path start"] = function parsePathStart(c) { + if (isSpecial(this.url)) { + if (c === 92) { + this.parseError = true; + } + this.state = "path"; + + if (c !== 47 && c !== 92) { + --this.pointer; + } + } else if (!this.stateOverride && c === 63) { + this.url.query = ""; + this.state = "query"; + } else if (!this.stateOverride && c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } else if (c !== undefined) { + this.state = "path"; + if (c !== 47) { + --this.pointer; + } + } + + return true; +}; + +URLStateMachine.prototype["parse path"] = function parsePath(c) { + if (isNaN(c) || c === 47 || (isSpecial(this.url) && c === 92) || + (!this.stateOverride && (c === 63 || c === 35))) { + if (isSpecial(this.url) && c === 92) { + this.parseError = true; + } + + if (isDoubleDot(this.buffer)) { + shortenPath(this.url); + if (c !== 47 && !(isSpecial(this.url) && c === 92)) { + this.url.path.push(""); + } + } else if (isSingleDot(this.buffer) && c !== 47 && + !(isSpecial(this.url) && c === 92)) { + this.url.path.push(""); + } else if (!isSingleDot(this.buffer)) { + if (this.url.scheme === "file" && this.url.path.length === 0 && isWindowsDriveLetterString(this.buffer)) { + if (this.url.host !== "" && this.url.host !== null) { + this.parseError = true; + this.url.host = ""; + } + this.buffer = this.buffer[0] + ":"; + } + this.url.path.push(this.buffer); + } + this.buffer = ""; + if (this.url.scheme === "file" && (c === undefined || c === 63 || c === 35)) { + while (this.url.path.length > 1 && this.url.path[0] === "") { + this.parseError = true; + this.url.path.shift(); + } + } + if (c === 63) { + this.url.query = ""; + this.state = "query"; + } + if (c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } + } else { + // TODO: If c is not a URL code point and not "%", parse error. + + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + this.buffer += percentEncodeChar(c, isPathPercentEncode); + } + + return true; +}; + +URLStateMachine.prototype["parse cannot-be-a-base-URL path"] = function parseCannotBeABaseURLPath(c) { + if (c === 63) { + this.url.query = ""; + this.state = "query"; + } else if (c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } else { + // TODO: Add: not a URL code point + if (!isNaN(c) && c !== 37) { + this.parseError = true; + } + + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + if (!isNaN(c)) { + this.url.path[0] = this.url.path[0] + percentEncodeChar(c, isC0ControlPercentEncode); + } + } + + return true; +}; + +URLStateMachine.prototype["parse query"] = function parseQuery(c, cStr) { + if (isNaN(c) || (!this.stateOverride && c === 35)) { + if (!isSpecial(this.url) || this.url.scheme === "ws" || this.url.scheme === "wss") { + this.encodingOverride = "utf-8"; + } + + const buffer = new Buffer(this.buffer); // TODO: Use encoding override instead + for (let i = 0; i < buffer.length; ++i) { + if (buffer[i] < 0x21 || buffer[i] > 0x7E || buffer[i] === 0x22 || buffer[i] === 0x23 || + buffer[i] === 0x3C || buffer[i] === 0x3E) { + this.url.query += percentEncode(buffer[i]); + } else { + this.url.query += String.fromCodePoint(buffer[i]); + } + } + + this.buffer = ""; + if (c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } + } else { + // TODO: If c is not a URL code point and not "%", parse error. + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse fragment"] = function parseFragment(c) { + if (isNaN(c)) { // do nothing + } else if (c === 0x0) { + this.parseError = true; + } else { + // TODO: If c is not a URL code point and not "%", parse error. + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + this.url.fragment += percentEncodeChar(c, isC0ControlPercentEncode); + } + + return true; +}; + +function serializeURL(url, excludeFragment) { + let output = url.scheme + ":"; + if (url.host !== null) { + output += "//"; + + if (url.username !== "" || url.password !== "") { + output += url.username; + if (url.password !== "") { + output += ":" + url.password; + } + output += "@"; + } + + output += serializeHost(url.host); + + if (url.port !== null) { + output += ":" + url.port; + } + } else if (url.host === null && url.scheme === "file") { + output += "//"; + } + + if (url.cannotBeABaseURL) { + output += url.path[0]; + } else { + for (const string of url.path) { + output += "/" + string; + } + } + + if (url.query !== null) { + output += "?" + url.query; + } + + if (!excludeFragment && url.fragment !== null) { + output += "#" + url.fragment; + } + + return output; +} + +function serializeOrigin(tuple) { + let result = tuple.scheme + "://"; + result += serializeHost(tuple.host); + + if (tuple.port !== null) { + result += ":" + tuple.port; + } + + return result; +} + +module.exports.serializeURL = serializeURL; + +module.exports.serializeURLOrigin = function (url) { + // https://url.spec.whatwg.org/#concept-url-origin + switch (url.scheme) { + case "blob": + try { + return module.exports.serializeURLOrigin(module.exports.parseURL(url.path[0])); + } catch (e) { + // serializing an opaque origin returns "null" + return "null"; + } + case "ftp": + case "gopher": + case "http": + case "https": + case "ws": + case "wss": + return serializeOrigin({ + scheme: url.scheme, + host: url.host, + port: url.port + }); + case "file": + // spec says "exercise to the reader", chrome says "file://" + return "file://"; + default: + // serializing an opaque origin returns "null" + return "null"; + } +}; + +module.exports.basicURLParse = function (input, options) { + if (options === undefined) { + options = {}; + } + + const usm = new URLStateMachine(input, options.baseURL, options.encodingOverride, options.url, options.stateOverride); + if (usm.failure) { + return "failure"; + } + + return usm.url; +}; + +module.exports.setTheUsername = function (url, username) { + url.username = ""; + const decoded = punycode.ucs2.decode(username); + for (let i = 0; i < decoded.length; ++i) { + url.username += percentEncodeChar(decoded[i], isUserinfoPercentEncode); + } +}; + +module.exports.setThePassword = function (url, password) { + url.password = ""; + const decoded = punycode.ucs2.decode(password); + for (let i = 0; i < decoded.length; ++i) { + url.password += percentEncodeChar(decoded[i], isUserinfoPercentEncode); + } +}; + +module.exports.serializeHost = serializeHost; + +module.exports.cannotHaveAUsernamePasswordPort = cannotHaveAUsernamePasswordPort; + +module.exports.serializeInteger = function (integer) { + return String(integer); +}; + +module.exports.parseURL = function (input, options) { + if (options === undefined) { + options = {}; + } + + // We don't handle blobs, so this just delegates: + return module.exports.basicURLParse(input, { baseURL: options.baseURL, encodingOverride: options.encodingOverride }); +}; + + +/***/ }), + +/***/ 3185: +/***/ ((module) => { + +"use strict"; + + +module.exports.mixin = function mixin(target, source) { + const keys = Object.getOwnPropertyNames(source); + for (let i = 0; i < keys.length; ++i) { + Object.defineProperty(target, keys[i], Object.getOwnPropertyDescriptor(source, keys[i])); + } +}; + +module.exports.wrapperSymbol = Symbol("wrapper"); +module.exports.implSymbol = Symbol("impl"); + +module.exports.wrapperForImpl = function (impl) { + return impl[module.exports.wrapperSymbol]; +}; + +module.exports.implForWrapper = function (wrapper) { + return wrapper[module.exports.implSymbol]; +}; + + + +/***/ }), + +/***/ 1269: +/***/ ((module) => { + +module.exports = eval("require")("bufferutil"); + + +/***/ }), + +/***/ 4592: +/***/ ((module) => { + +module.exports = eval("require")("utf-8-validate"); + + +/***/ }), + +/***/ 4978: +/***/ ((module) => { + +module.exports = eval("require")("util/types"); + + +/***/ }), + +/***/ 9491: +/***/ ((module) => { + +"use strict"; +module.exports = require("assert"); + +/***/ }), + +/***/ 852: +/***/ ((module) => { + +"use strict"; +module.exports = require("async_hooks"); + +/***/ }), + +/***/ 4300: +/***/ ((module) => { + +"use strict"; +module.exports = require("buffer"); + +/***/ }), + +/***/ 2081: +/***/ ((module) => { + +"use strict"; +module.exports = require("child_process"); + +/***/ }), + +/***/ 6206: +/***/ ((module) => { + +"use strict"; +module.exports = require("console"); + +/***/ }), + +/***/ 6113: +/***/ ((module) => { + +"use strict"; +module.exports = require("crypto"); + +/***/ }), + +/***/ 7643: +/***/ ((module) => { + +"use strict"; +module.exports = require("diagnostics_channel"); + +/***/ }), + +/***/ 2361: +/***/ ((module) => { + +"use strict"; +module.exports = require("events"); + +/***/ }), + +/***/ 7147: +/***/ ((module) => { + +"use strict"; +module.exports = require("fs"); + +/***/ }), + +/***/ 3685: +/***/ ((module) => { + +"use strict"; +module.exports = require("http"); + +/***/ }), + +/***/ 5158: +/***/ ((module) => { + +"use strict"; +module.exports = require("http2"); + +/***/ }), + +/***/ 5687: +/***/ ((module) => { + +"use strict"; +module.exports = require("https"); + +/***/ }), + +/***/ 1808: +/***/ ((module) => { + +"use strict"; +module.exports = require("net"); + +/***/ }), + +/***/ 6005: +/***/ ((module) => { + +"use strict"; +module.exports = require("node:crypto"); + +/***/ }), + +/***/ 5673: +/***/ ((module) => { + +"use strict"; +module.exports = require("node:events"); + +/***/ }), + +/***/ 4492: +/***/ ((module) => { + +"use strict"; +module.exports = require("node:stream"); + +/***/ }), + +/***/ 7261: +/***/ ((module) => { + +"use strict"; +module.exports = require("node:util"); + +/***/ }), + +/***/ 2037: +/***/ ((module) => { + +"use strict"; +module.exports = require("os"); + +/***/ }), + +/***/ 1017: +/***/ ((module) => { + +"use strict"; +module.exports = require("path"); + +/***/ }), + +/***/ 4074: +/***/ ((module) => { + +"use strict"; +module.exports = require("perf_hooks"); + +/***/ }), + +/***/ 5477: +/***/ ((module) => { + +"use strict"; +module.exports = require("punycode"); + +/***/ }), + +/***/ 3477: +/***/ ((module) => { + +"use strict"; +module.exports = require("querystring"); + +/***/ }), + +/***/ 2781: +/***/ ((module) => { + +"use strict"; +module.exports = require("stream"); + +/***/ }), + +/***/ 5356: +/***/ ((module) => { + +"use strict"; +module.exports = require("stream/web"); + +/***/ }), + +/***/ 1576: +/***/ ((module) => { + +"use strict"; +module.exports = require("string_decoder"); + +/***/ }), + +/***/ 9512: +/***/ ((module) => { + +"use strict"; +module.exports = require("timers"); + +/***/ }), + +/***/ 4404: +/***/ ((module) => { + +"use strict"; +module.exports = require("tls"); + +/***/ }), + +/***/ 7310: +/***/ ((module) => { + +"use strict"; +module.exports = require("url"); + +/***/ }), + +/***/ 3837: +/***/ ((module) => { + +"use strict"; +module.exports = require("util"); + +/***/ }), + +/***/ 1267: +/***/ ((module) => { + +"use strict"; +module.exports = require("worker_threads"); + +/***/ }), + +/***/ 9796: +/***/ ((module) => { + +"use strict"; +module.exports = require("zlib"); + +/***/ }), + +/***/ 2960: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const WritableStream = (__nccwpck_require__(4492).Writable) +const inherits = (__nccwpck_require__(7261).inherits) + +const StreamSearch = __nccwpck_require__(1142) + +const PartStream = __nccwpck_require__(1620) +const HeaderParser = __nccwpck_require__(2032) + +const DASH = 45 +const B_ONEDASH = Buffer.from('-') +const B_CRLF = Buffer.from('\r\n') +const EMPTY_FN = function () {} + +function Dicer (cfg) { + if (!(this instanceof Dicer)) { return new Dicer(cfg) } + WritableStream.call(this, cfg) + + if (!cfg || (!cfg.headerFirst && typeof cfg.boundary !== 'string')) { throw new TypeError('Boundary required') } + + if (typeof cfg.boundary === 'string') { this.setBoundary(cfg.boundary) } else { this._bparser = undefined } + + this._headerFirst = cfg.headerFirst + + this._dashes = 0 + this._parts = 0 + this._finished = false + this._realFinish = false + this._isPreamble = true + this._justMatched = false + this._firstWrite = true + this._inHeader = true + this._part = undefined + this._cb = undefined + this._ignoreData = false + this._partOpts = { highWaterMark: cfg.partHwm } + this._pause = false + + const self = this + this._hparser = new HeaderParser(cfg) + this._hparser.on('header', function (header) { + self._inHeader = false + self._part.emit('header', header) + }) +} +inherits(Dicer, WritableStream) + +Dicer.prototype.emit = function (ev) { + if (ev === 'finish' && !this._realFinish) { + if (!this._finished) { + const self = this + process.nextTick(function () { + self.emit('error', new Error('Unexpected end of multipart data')) + if (self._part && !self._ignoreData) { + const type = (self._isPreamble ? 'Preamble' : 'Part') + self._part.emit('error', new Error(type + ' terminated early due to unexpected end of multipart data')) + self._part.push(null) + process.nextTick(function () { + self._realFinish = true + self.emit('finish') + self._realFinish = false + }) + return + } + self._realFinish = true + self.emit('finish') + self._realFinish = false + }) + } + } else { WritableStream.prototype.emit.apply(this, arguments) } +} + +Dicer.prototype._write = function (data, encoding, cb) { + // ignore unexpected data (e.g. extra trailer data after finished) + if (!this._hparser && !this._bparser) { return cb() } + + if (this._headerFirst && this._isPreamble) { + if (!this._part) { + this._part = new PartStream(this._partOpts) + if (this.listenerCount('preamble') !== 0) { this.emit('preamble', this._part) } else { this._ignore() } + } + const r = this._hparser.push(data) + if (!this._inHeader && r !== undefined && r < data.length) { data = data.slice(r) } else { return cb() } + } + + // allows for "easier" testing + if (this._firstWrite) { + this._bparser.push(B_CRLF) + this._firstWrite = false + } + + this._bparser.push(data) + + if (this._pause) { this._cb = cb } else { cb() } +} + +Dicer.prototype.reset = function () { + this._part = undefined + this._bparser = undefined + this._hparser = undefined +} + +Dicer.prototype.setBoundary = function (boundary) { + const self = this + this._bparser = new StreamSearch('\r\n--' + boundary) + this._bparser.on('info', function (isMatch, data, start, end) { + self._oninfo(isMatch, data, start, end) + }) +} + +Dicer.prototype._ignore = function () { + if (this._part && !this._ignoreData) { + this._ignoreData = true + this._part.on('error', EMPTY_FN) + // we must perform some kind of read on the stream even though we are + // ignoring the data, otherwise node's Readable stream will not emit 'end' + // after pushing null to the stream + this._part.resume() + } +} + +Dicer.prototype._oninfo = function (isMatch, data, start, end) { + let buf; const self = this; let i = 0; let r; let shouldWriteMore = true + + if (!this._part && this._justMatched && data) { + while (this._dashes < 2 && (start + i) < end) { + if (data[start + i] === DASH) { + ++i + ++this._dashes + } else { + if (this._dashes) { buf = B_ONEDASH } + this._dashes = 0 + break + } + } + if (this._dashes === 2) { + if ((start + i) < end && this.listenerCount('trailer') !== 0) { this.emit('trailer', data.slice(start + i, end)) } + this.reset() + this._finished = true + // no more parts will be added + if (self._parts === 0) { + self._realFinish = true + self.emit('finish') + self._realFinish = false + } + } + if (this._dashes) { return } + } + if (this._justMatched) { this._justMatched = false } + if (!this._part) { + this._part = new PartStream(this._partOpts) + this._part._read = function (n) { + self._unpause() + } + if (this._isPreamble && this.listenerCount('preamble') !== 0) { + this.emit('preamble', this._part) + } else if (this._isPreamble !== true && this.listenerCount('part') !== 0) { + this.emit('part', this._part) + } else { + this._ignore() + } + if (!this._isPreamble) { this._inHeader = true } + } + if (data && start < end && !this._ignoreData) { + if (this._isPreamble || !this._inHeader) { + if (buf) { shouldWriteMore = this._part.push(buf) } + shouldWriteMore = this._part.push(data.slice(start, end)) + if (!shouldWriteMore) { this._pause = true } + } else if (!this._isPreamble && this._inHeader) { + if (buf) { this._hparser.push(buf) } + r = this._hparser.push(data.slice(start, end)) + if (!this._inHeader && r !== undefined && r < end) { this._oninfo(false, data, start + r, end) } + } + } + if (isMatch) { + this._hparser.reset() + if (this._isPreamble) { this._isPreamble = false } else { + if (start !== end) { + ++this._parts + this._part.on('end', function () { + if (--self._parts === 0) { + if (self._finished) { + self._realFinish = true + self.emit('finish') + self._realFinish = false + } else { + self._unpause() + } + } + }) + } + } + this._part.push(null) + this._part = undefined + this._ignoreData = false + this._justMatched = true + this._dashes = 0 + } +} + +Dicer.prototype._unpause = function () { + if (!this._pause) { return } + + this._pause = false + if (this._cb) { + const cb = this._cb + this._cb = undefined + cb() + } +} + +module.exports = Dicer + + +/***/ }), + +/***/ 2032: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const EventEmitter = (__nccwpck_require__(5673).EventEmitter) +const inherits = (__nccwpck_require__(7261).inherits) +const getLimit = __nccwpck_require__(1467) + +const StreamSearch = __nccwpck_require__(1142) + +const B_DCRLF = Buffer.from('\r\n\r\n') +const RE_CRLF = /\r\n/g +const RE_HDR = /^([^:]+):[ \t]?([\x00-\xFF]+)?$/ // eslint-disable-line no-control-regex + +function HeaderParser (cfg) { + EventEmitter.call(this) + + cfg = cfg || {} + const self = this + this.nread = 0 + this.maxed = false + this.npairs = 0 + this.maxHeaderPairs = getLimit(cfg, 'maxHeaderPairs', 2000) + this.maxHeaderSize = getLimit(cfg, 'maxHeaderSize', 80 * 1024) + this.buffer = '' + this.header = {} + this.finished = false + this.ss = new StreamSearch(B_DCRLF) + this.ss.on('info', function (isMatch, data, start, end) { + if (data && !self.maxed) { + if (self.nread + end - start >= self.maxHeaderSize) { + end = self.maxHeaderSize - self.nread + start + self.nread = self.maxHeaderSize + self.maxed = true + } else { self.nread += (end - start) } + + self.buffer += data.toString('binary', start, end) + } + if (isMatch) { self._finish() } + }) +} +inherits(HeaderParser, EventEmitter) + +HeaderParser.prototype.push = function (data) { + const r = this.ss.push(data) + if (this.finished) { return r } +} + +HeaderParser.prototype.reset = function () { + this.finished = false + this.buffer = '' + this.header = {} + this.ss.reset() +} + +HeaderParser.prototype._finish = function () { + if (this.buffer) { this._parseHeader() } + this.ss.matches = this.ss.maxMatches + const header = this.header + this.header = {} + this.buffer = '' + this.finished = true + this.nread = this.npairs = 0 + this.maxed = false + this.emit('header', header) +} + +HeaderParser.prototype._parseHeader = function () { + if (this.npairs === this.maxHeaderPairs) { return } + + const lines = this.buffer.split(RE_CRLF) + const len = lines.length + let m, h + + for (var i = 0; i < len; ++i) { // eslint-disable-line no-var + if (lines[i].length === 0) { continue } + if (lines[i][0] === '\t' || lines[i][0] === ' ') { + // folded header content + // RFC2822 says to just remove the CRLF and not the whitespace following + // it, so we follow the RFC and include the leading whitespace ... + if (h) { + this.header[h][this.header[h].length - 1] += lines[i] + continue + } + } + + const posColon = lines[i].indexOf(':') + if ( + posColon === -1 || + posColon === 0 + ) { + return + } + m = RE_HDR.exec(lines[i]) + h = m[1].toLowerCase() + this.header[h] = this.header[h] || [] + this.header[h].push((m[2] || '')) + if (++this.npairs === this.maxHeaderPairs) { break } + } +} + +module.exports = HeaderParser + + +/***/ }), + +/***/ 1620: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const inherits = (__nccwpck_require__(7261).inherits) +const ReadableStream = (__nccwpck_require__(4492).Readable) + +function PartStream (opts) { + ReadableStream.call(this, opts) +} +inherits(PartStream, ReadableStream) + +PartStream.prototype._read = function (n) {} + +module.exports = PartStream + + +/***/ }), + +/***/ 1142: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +/** + * Copyright Brian White. All rights reserved. + * + * @see https://github.com/mscdex/streamsearch + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + * Based heavily on the Streaming Boyer-Moore-Horspool C++ implementation + * by Hongli Lai at: https://github.com/FooBarWidget/boyer-moore-horspool + */ +const EventEmitter = (__nccwpck_require__(5673).EventEmitter) +const inherits = (__nccwpck_require__(7261).inherits) + +function SBMH (needle) { + if (typeof needle === 'string') { + needle = Buffer.from(needle) + } + + if (!Buffer.isBuffer(needle)) { + throw new TypeError('The needle has to be a String or a Buffer.') + } + + const needleLength = needle.length + + if (needleLength === 0) { + throw new Error('The needle cannot be an empty String/Buffer.') + } + + if (needleLength > 256) { + throw new Error('The needle cannot have a length bigger than 256.') + } + + this.maxMatches = Infinity + this.matches = 0 + + this._occ = new Array(256) + .fill(needleLength) // Initialize occurrence table. + this._lookbehind_size = 0 + this._needle = needle + this._bufpos = 0 + + this._lookbehind = Buffer.alloc(needleLength) + + // Populate occurrence table with analysis of the needle, + // ignoring last letter. + for (var i = 0; i < needleLength - 1; ++i) { // eslint-disable-line no-var + this._occ[needle[i]] = needleLength - 1 - i + } +} +inherits(SBMH, EventEmitter) + +SBMH.prototype.reset = function () { + this._lookbehind_size = 0 + this.matches = 0 + this._bufpos = 0 +} + +SBMH.prototype.push = function (chunk, pos) { + if (!Buffer.isBuffer(chunk)) { + chunk = Buffer.from(chunk, 'binary') + } + const chlen = chunk.length + this._bufpos = pos || 0 + let r + while (r !== chlen && this.matches < this.maxMatches) { r = this._sbmh_feed(chunk) } + return r +} + +SBMH.prototype._sbmh_feed = function (data) { + const len = data.length + const needle = this._needle + const needleLength = needle.length + const lastNeedleChar = needle[needleLength - 1] + + // Positive: points to a position in `data` + // pos == 3 points to data[3] + // Negative: points to a position in the lookbehind buffer + // pos == -2 points to lookbehind[lookbehind_size - 2] + let pos = -this._lookbehind_size + let ch + + if (pos < 0) { + // Lookbehind buffer is not empty. Perform Boyer-Moore-Horspool + // search with character lookup code that considers both the + // lookbehind buffer and the current round's haystack data. + // + // Loop until + // there is a match. + // or until + // we've moved past the position that requires the + // lookbehind buffer. In this case we switch to the + // optimized loop. + // or until + // the character to look at lies outside the haystack. + while (pos < 0 && pos <= len - needleLength) { + ch = this._sbmh_lookup_char(data, pos + needleLength - 1) + + if ( + ch === lastNeedleChar && + this._sbmh_memcmp(data, pos, needleLength - 1) + ) { + this._lookbehind_size = 0 + ++this.matches + this.emit('info', true) + + return (this._bufpos = pos + needleLength) + } + pos += this._occ[ch] + } + + // No match. + + if (pos < 0) { + // There's too few data for Boyer-Moore-Horspool to run, + // so let's use a different algorithm to skip as much as + // we can. + // Forward pos until + // the trailing part of lookbehind + data + // looks like the beginning of the needle + // or until + // pos == 0 + while (pos < 0 && !this._sbmh_memcmp(data, pos, len - pos)) { ++pos } + } + + if (pos >= 0) { + // Discard lookbehind buffer. + this.emit('info', false, this._lookbehind, 0, this._lookbehind_size) + this._lookbehind_size = 0 + } else { + // Cut off part of the lookbehind buffer that has + // been processed and append the entire haystack + // into it. + const bytesToCutOff = this._lookbehind_size + pos + if (bytesToCutOff > 0) { + // The cut off data is guaranteed not to contain the needle. + this.emit('info', false, this._lookbehind, 0, bytesToCutOff) + } + + this._lookbehind.copy(this._lookbehind, 0, bytesToCutOff, + this._lookbehind_size - bytesToCutOff) + this._lookbehind_size -= bytesToCutOff + + data.copy(this._lookbehind, this._lookbehind_size) + this._lookbehind_size += len + + this._bufpos = len + return len + } + } + + pos += (pos >= 0) * this._bufpos + + // Lookbehind buffer is now empty. We only need to check if the + // needle is in the haystack. + if (data.indexOf(needle, pos) !== -1) { + pos = data.indexOf(needle, pos) + ++this.matches + if (pos > 0) { this.emit('info', true, data, this._bufpos, pos) } else { this.emit('info', true) } + + return (this._bufpos = pos + needleLength) + } else { + pos = len - needleLength + } + + // There was no match. If there's trailing haystack data that we cannot + // match yet using the Boyer-Moore-Horspool algorithm (because the trailing + // data is less than the needle size) then match using a modified + // algorithm that starts matching from the beginning instead of the end. + // Whatever trailing data is left after running this algorithm is added to + // the lookbehind buffer. + while ( + pos < len && + ( + data[pos] !== needle[0] || + ( + (Buffer.compare( + data.subarray(pos, pos + len - pos), + needle.subarray(0, len - pos) + ) !== 0) + ) + ) + ) { + ++pos + } + if (pos < len) { + data.copy(this._lookbehind, 0, pos, pos + (len - pos)) + this._lookbehind_size = len - pos + } + + // Everything until pos is guaranteed not to contain needle data. + if (pos > 0) { this.emit('info', false, data, this._bufpos, pos < len ? pos : len) } + + this._bufpos = len + return len +} + +SBMH.prototype._sbmh_lookup_char = function (data, pos) { + return (pos < 0) + ? this._lookbehind[this._lookbehind_size + pos] + : data[pos] +} + +SBMH.prototype._sbmh_memcmp = function (data, pos, len) { + for (var i = 0; i < len; ++i) { // eslint-disable-line no-var + if (this._sbmh_lookup_char(data, pos + i) !== this._needle[i]) { return false } + } + return true +} + +module.exports = SBMH + + +/***/ }), + +/***/ 727: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const WritableStream = (__nccwpck_require__(4492).Writable) +const { inherits } = __nccwpck_require__(7261) +const Dicer = __nccwpck_require__(2960) + +const MultipartParser = __nccwpck_require__(2183) +const UrlencodedParser = __nccwpck_require__(8306) +const parseParams = __nccwpck_require__(1854) + +function Busboy (opts) { + if (!(this instanceof Busboy)) { return new Busboy(opts) } + + if (typeof opts !== 'object') { + throw new TypeError('Busboy expected an options-Object.') + } + if (typeof opts.headers !== 'object') { + throw new TypeError('Busboy expected an options-Object with headers-attribute.') + } + if (typeof opts.headers['content-type'] !== 'string') { + throw new TypeError('Missing Content-Type-header.') + } + + const { + headers, + ...streamOptions + } = opts + + this.opts = { + autoDestroy: false, + ...streamOptions + } + WritableStream.call(this, this.opts) + + this._done = false + this._parser = this.getParserByHeaders(headers) + this._finished = false +} +inherits(Busboy, WritableStream) + +Busboy.prototype.emit = function (ev) { + if (ev === 'finish') { + if (!this._done) { + this._parser?.end() + return + } else if (this._finished) { + return + } + this._finished = true + } + WritableStream.prototype.emit.apply(this, arguments) +} + +Busboy.prototype.getParserByHeaders = function (headers) { + const parsed = parseParams(headers['content-type']) + + const cfg = { + defCharset: this.opts.defCharset, + fileHwm: this.opts.fileHwm, + headers, + highWaterMark: this.opts.highWaterMark, + isPartAFile: this.opts.isPartAFile, + limits: this.opts.limits, + parsedConType: parsed, + preservePath: this.opts.preservePath + } + + if (MultipartParser.detect.test(parsed[0])) { + return new MultipartParser(this, cfg) + } + if (UrlencodedParser.detect.test(parsed[0])) { + return new UrlencodedParser(this, cfg) + } + throw new Error('Unsupported Content-Type.') +} + +Busboy.prototype._write = function (chunk, encoding, cb) { + this._parser.write(chunk, cb) +} + +module.exports = Busboy +module.exports["default"] = Busboy +module.exports.Busboy = Busboy + +module.exports.Dicer = Dicer + + +/***/ }), + +/***/ 2183: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +// TODO: +// * support 1 nested multipart level +// (see second multipart example here: +// http://www.w3.org/TR/html401/interact/forms.html#didx-multipartform-data) +// * support limits.fieldNameSize +// -- this will require modifications to utils.parseParams + +const { Readable } = __nccwpck_require__(4492) +const { inherits } = __nccwpck_require__(7261) + +const Dicer = __nccwpck_require__(2960) + +const parseParams = __nccwpck_require__(1854) +const decodeText = __nccwpck_require__(4619) +const basename = __nccwpck_require__(8647) +const getLimit = __nccwpck_require__(1467) + +const RE_BOUNDARY = /^boundary$/i +const RE_FIELD = /^form-data$/i +const RE_CHARSET = /^charset$/i +const RE_FILENAME = /^filename$/i +const RE_NAME = /^name$/i + +Multipart.detect = /^multipart\/form-data/i +function Multipart (boy, cfg) { + let i + let len + const self = this + let boundary + const limits = cfg.limits + const isPartAFile = cfg.isPartAFile || ((fieldName, contentType, fileName) => (contentType === 'application/octet-stream' || fileName !== undefined)) + const parsedConType = cfg.parsedConType || [] + const defCharset = cfg.defCharset || 'utf8' + const preservePath = cfg.preservePath + const fileOpts = { highWaterMark: cfg.fileHwm } + + for (i = 0, len = parsedConType.length; i < len; ++i) { + if (Array.isArray(parsedConType[i]) && + RE_BOUNDARY.test(parsedConType[i][0])) { + boundary = parsedConType[i][1] + break + } + } + + function checkFinished () { + if (nends === 0 && finished && !boy._done) { + finished = false + self.end() + } + } + + if (typeof boundary !== 'string') { throw new Error('Multipart: Boundary not found') } + + const fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024) + const fileSizeLimit = getLimit(limits, 'fileSize', Infinity) + const filesLimit = getLimit(limits, 'files', Infinity) + const fieldsLimit = getLimit(limits, 'fields', Infinity) + const partsLimit = getLimit(limits, 'parts', Infinity) + const headerPairsLimit = getLimit(limits, 'headerPairs', 2000) + const headerSizeLimit = getLimit(limits, 'headerSize', 80 * 1024) + + let nfiles = 0 + let nfields = 0 + let nends = 0 + let curFile + let curField + let finished = false + + this._needDrain = false + this._pause = false + this._cb = undefined + this._nparts = 0 + this._boy = boy + + const parserCfg = { + boundary, + maxHeaderPairs: headerPairsLimit, + maxHeaderSize: headerSizeLimit, + partHwm: fileOpts.highWaterMark, + highWaterMark: cfg.highWaterMark + } + + this.parser = new Dicer(parserCfg) + this.parser.on('drain', function () { + self._needDrain = false + if (self._cb && !self._pause) { + const cb = self._cb + self._cb = undefined + cb() + } + }).on('part', function onPart (part) { + if (++self._nparts > partsLimit) { + self.parser.removeListener('part', onPart) + self.parser.on('part', skipPart) + boy.hitPartsLimit = true + boy.emit('partsLimit') + return skipPart(part) + } + + // hack because streams2 _always_ doesn't emit 'end' until nextTick, so let + // us emit 'end' early since we know the part has ended if we are already + // seeing the next part + if (curField) { + const field = curField + field.emit('end') + field.removeAllListeners('end') + } + + part.on('header', function (header) { + let contype + let fieldname + let parsed + let charset + let encoding + let filename + let nsize = 0 + + if (header['content-type']) { + parsed = parseParams(header['content-type'][0]) + if (parsed[0]) { + contype = parsed[0].toLowerCase() + for (i = 0, len = parsed.length; i < len; ++i) { + if (RE_CHARSET.test(parsed[i][0])) { + charset = parsed[i][1].toLowerCase() + break + } + } + } + } + + if (contype === undefined) { contype = 'text/plain' } + if (charset === undefined) { charset = defCharset } + + if (header['content-disposition']) { + parsed = parseParams(header['content-disposition'][0]) + if (!RE_FIELD.test(parsed[0])) { return skipPart(part) } + for (i = 0, len = parsed.length; i < len; ++i) { + if (RE_NAME.test(parsed[i][0])) { + fieldname = parsed[i][1] + } else if (RE_FILENAME.test(parsed[i][0])) { + filename = parsed[i][1] + if (!preservePath) { filename = basename(filename) } + } + } + } else { return skipPart(part) } + + if (header['content-transfer-encoding']) { encoding = header['content-transfer-encoding'][0].toLowerCase() } else { encoding = '7bit' } + + let onData, + onEnd + + if (isPartAFile(fieldname, contype, filename)) { + // file/binary field + if (nfiles === filesLimit) { + if (!boy.hitFilesLimit) { + boy.hitFilesLimit = true + boy.emit('filesLimit') + } + return skipPart(part) + } + + ++nfiles + + if (boy.listenerCount('file') === 0) { + self.parser._ignore() + return + } + + ++nends + const file = new FileStream(fileOpts) + curFile = file + file.on('end', function () { + --nends + self._pause = false + checkFinished() + if (self._cb && !self._needDrain) { + const cb = self._cb + self._cb = undefined + cb() + } + }) + file._read = function (n) { + if (!self._pause) { return } + self._pause = false + if (self._cb && !self._needDrain) { + const cb = self._cb + self._cb = undefined + cb() + } + } + boy.emit('file', fieldname, file, filename, encoding, contype) + + onData = function (data) { + if ((nsize += data.length) > fileSizeLimit) { + const extralen = fileSizeLimit - nsize + data.length + if (extralen > 0) { file.push(data.slice(0, extralen)) } + file.truncated = true + file.bytesRead = fileSizeLimit + part.removeAllListeners('data') + file.emit('limit') + return + } else if (!file.push(data)) { self._pause = true } + + file.bytesRead = nsize + } + + onEnd = function () { + curFile = undefined + file.push(null) + } + } else { + // non-file field + if (nfields === fieldsLimit) { + if (!boy.hitFieldsLimit) { + boy.hitFieldsLimit = true + boy.emit('fieldsLimit') + } + return skipPart(part) + } + + ++nfields + ++nends + let buffer = '' + let truncated = false + curField = part + + onData = function (data) { + if ((nsize += data.length) > fieldSizeLimit) { + const extralen = (fieldSizeLimit - (nsize - data.length)) + buffer += data.toString('binary', 0, extralen) + truncated = true + part.removeAllListeners('data') + } else { buffer += data.toString('binary') } + } + + onEnd = function () { + curField = undefined + if (buffer.length) { buffer = decodeText(buffer, 'binary', charset) } + boy.emit('field', fieldname, buffer, false, truncated, encoding, contype) + --nends + checkFinished() + } + } + + /* As of node@2efe4ab761666 (v0.10.29+/v0.11.14+), busboy had become + broken. Streams2/streams3 is a huge black box of confusion, but + somehow overriding the sync state seems to fix things again (and still + seems to work for previous node versions). + */ + part._readableState.sync = false + + part.on('data', onData) + part.on('end', onEnd) + }).on('error', function (err) { + if (curFile) { curFile.emit('error', err) } + }) + }).on('error', function (err) { + boy.emit('error', err) + }).on('finish', function () { + finished = true + checkFinished() + }) +} + +Multipart.prototype.write = function (chunk, cb) { + const r = this.parser.write(chunk) + if (r && !this._pause) { + cb() + } else { + this._needDrain = !r + this._cb = cb + } +} + +Multipart.prototype.end = function () { + const self = this + + if (self.parser.writable) { + self.parser.end() + } else if (!self._boy._done) { + process.nextTick(function () { + self._boy._done = true + self._boy.emit('finish') + }) + } +} + +function skipPart (part) { + part.resume() +} + +function FileStream (opts) { + Readable.call(this, opts) + + this.bytesRead = 0 + + this.truncated = false +} + +inherits(FileStream, Readable) + +FileStream.prototype._read = function (n) {} + +module.exports = Multipart + + +/***/ }), + +/***/ 8306: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Decoder = __nccwpck_require__(7100) +const decodeText = __nccwpck_require__(4619) +const getLimit = __nccwpck_require__(1467) + +const RE_CHARSET = /^charset$/i + +UrlEncoded.detect = /^application\/x-www-form-urlencoded/i +function UrlEncoded (boy, cfg) { + const limits = cfg.limits + const parsedConType = cfg.parsedConType + this.boy = boy + + this.fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024) + this.fieldNameSizeLimit = getLimit(limits, 'fieldNameSize', 100) + this.fieldsLimit = getLimit(limits, 'fields', Infinity) + + let charset + for (var i = 0, len = parsedConType.length; i < len; ++i) { // eslint-disable-line no-var + if (Array.isArray(parsedConType[i]) && + RE_CHARSET.test(parsedConType[i][0])) { + charset = parsedConType[i][1].toLowerCase() + break + } + } + + if (charset === undefined) { charset = cfg.defCharset || 'utf8' } + + this.decoder = new Decoder() + this.charset = charset + this._fields = 0 + this._state = 'key' + this._checkingBytes = true + this._bytesKey = 0 + this._bytesVal = 0 + this._key = '' + this._val = '' + this._keyTrunc = false + this._valTrunc = false + this._hitLimit = false +} + +UrlEncoded.prototype.write = function (data, cb) { + if (this._fields === this.fieldsLimit) { + if (!this.boy.hitFieldsLimit) { + this.boy.hitFieldsLimit = true + this.boy.emit('fieldsLimit') + } + return cb() + } + + let idxeq; let idxamp; let i; let p = 0; const len = data.length + + while (p < len) { + if (this._state === 'key') { + idxeq = idxamp = undefined + for (i = p; i < len; ++i) { + if (!this._checkingBytes) { ++p } + if (data[i] === 0x3D/* = */) { + idxeq = i + break + } else if (data[i] === 0x26/* & */) { + idxamp = i + break + } + if (this._checkingBytes && this._bytesKey === this.fieldNameSizeLimit) { + this._hitLimit = true + break + } else if (this._checkingBytes) { ++this._bytesKey } + } + + if (idxeq !== undefined) { + // key with assignment + if (idxeq > p) { this._key += this.decoder.write(data.toString('binary', p, idxeq)) } + this._state = 'val' + + this._hitLimit = false + this._checkingBytes = true + this._val = '' + this._bytesVal = 0 + this._valTrunc = false + this.decoder.reset() + + p = idxeq + 1 + } else if (idxamp !== undefined) { + // key with no assignment + ++this._fields + let key; const keyTrunc = this._keyTrunc + if (idxamp > p) { key = (this._key += this.decoder.write(data.toString('binary', p, idxamp))) } else { key = this._key } + + this._hitLimit = false + this._checkingBytes = true + this._key = '' + this._bytesKey = 0 + this._keyTrunc = false + this.decoder.reset() + + if (key.length) { + this.boy.emit('field', decodeText(key, 'binary', this.charset), + '', + keyTrunc, + false) + } + + p = idxamp + 1 + if (this._fields === this.fieldsLimit) { return cb() } + } else if (this._hitLimit) { + // we may not have hit the actual limit if there are encoded bytes... + if (i > p) { this._key += this.decoder.write(data.toString('binary', p, i)) } + p = i + if ((this._bytesKey = this._key.length) === this.fieldNameSizeLimit) { + // yep, we actually did hit the limit + this._checkingBytes = false + this._keyTrunc = true + } + } else { + if (p < len) { this._key += this.decoder.write(data.toString('binary', p)) } + p = len + } + } else { + idxamp = undefined + for (i = p; i < len; ++i) { + if (!this._checkingBytes) { ++p } + if (data[i] === 0x26/* & */) { + idxamp = i + break + } + if (this._checkingBytes && this._bytesVal === this.fieldSizeLimit) { + this._hitLimit = true + break + } else if (this._checkingBytes) { ++this._bytesVal } + } + + if (idxamp !== undefined) { + ++this._fields + if (idxamp > p) { this._val += this.decoder.write(data.toString('binary', p, idxamp)) } + this.boy.emit('field', decodeText(this._key, 'binary', this.charset), + decodeText(this._val, 'binary', this.charset), + this._keyTrunc, + this._valTrunc) + this._state = 'key' + + this._hitLimit = false + this._checkingBytes = true + this._key = '' + this._bytesKey = 0 + this._keyTrunc = false + this.decoder.reset() + + p = idxamp + 1 + if (this._fields === this.fieldsLimit) { return cb() } + } else if (this._hitLimit) { + // we may not have hit the actual limit if there are encoded bytes... + if (i > p) { this._val += this.decoder.write(data.toString('binary', p, i)) } + p = i + if ((this._val === '' && this.fieldSizeLimit === 0) || + (this._bytesVal = this._val.length) === this.fieldSizeLimit) { + // yep, we actually did hit the limit + this._checkingBytes = false + this._valTrunc = true + } + } else { + if (p < len) { this._val += this.decoder.write(data.toString('binary', p)) } + p = len + } + } + } + cb() +} + +UrlEncoded.prototype.end = function () { + if (this.boy._done) { return } + + if (this._state === 'key' && this._key.length > 0) { + this.boy.emit('field', decodeText(this._key, 'binary', this.charset), + '', + this._keyTrunc, + false) + } else if (this._state === 'val') { + this.boy.emit('field', decodeText(this._key, 'binary', this.charset), + decodeText(this._val, 'binary', this.charset), + this._keyTrunc, + this._valTrunc) + } + this.boy._done = true + this.boy.emit('finish') +} + +module.exports = UrlEncoded + + +/***/ }), + +/***/ 7100: +/***/ ((module) => { + +"use strict"; + + +const RE_PLUS = /\+/g + +const HEX = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +] + +function Decoder () { + this.buffer = undefined +} +Decoder.prototype.write = function (str) { + // Replace '+' with ' ' before decoding + str = str.replace(RE_PLUS, ' ') + let res = '' + let i = 0; let p = 0; const len = str.length + for (; i < len; ++i) { + if (this.buffer !== undefined) { + if (!HEX[str.charCodeAt(i)]) { + res += '%' + this.buffer + this.buffer = undefined + --i // retry character + } else { + this.buffer += str[i] + ++p + if (this.buffer.length === 2) { + res += String.fromCharCode(parseInt(this.buffer, 16)) + this.buffer = undefined + } + } + } else if (str[i] === '%') { + if (i > p) { + res += str.substring(p, i) + p = i + } + this.buffer = '' + ++p + } + } + if (p < len && this.buffer === undefined) { res += str.substring(p) } + return res +} +Decoder.prototype.reset = function () { + this.buffer = undefined +} + +module.exports = Decoder + + +/***/ }), + +/***/ 8647: +/***/ ((module) => { + +"use strict"; + + +module.exports = function basename (path) { + if (typeof path !== 'string') { return '' } + for (var i = path.length - 1; i >= 0; --i) { // eslint-disable-line no-var + switch (path.charCodeAt(i)) { + case 0x2F: // '/' + case 0x5C: // '\' + path = path.slice(i + 1) + return (path === '..' || path === '.' ? '' : path) + } + } + return (path === '..' || path === '.' ? '' : path) +} + + +/***/ }), + +/***/ 4619: +/***/ (function(module) { + +"use strict"; + + +// Node has always utf-8 +const utf8Decoder = new TextDecoder('utf-8') +const textDecoders = new Map([ + ['utf-8', utf8Decoder], + ['utf8', utf8Decoder] +]) + +function getDecoder (charset) { + let lc + while (true) { + switch (charset) { + case 'utf-8': + case 'utf8': + return decoders.utf8 + case 'latin1': + case 'ascii': // TODO: Make these a separate, strict decoder? + case 'us-ascii': + case 'iso-8859-1': + case 'iso8859-1': + case 'iso88591': + case 'iso_8859-1': + case 'windows-1252': + case 'iso_8859-1:1987': + case 'cp1252': + case 'x-cp1252': + return decoders.latin1 + case 'utf16le': + case 'utf-16le': + case 'ucs2': + case 'ucs-2': + return decoders.utf16le + case 'base64': + return decoders.base64 + default: + if (lc === undefined) { + lc = true + charset = charset.toLowerCase() + continue + } + return decoders.other.bind(charset) + } + } +} + +const decoders = { + utf8: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + return data.utf8Slice(0, data.length) + }, + + latin1: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + return data + } + return data.latin1Slice(0, data.length) + }, + + utf16le: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + return data.ucs2Slice(0, data.length) + }, + + base64: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + return data.base64Slice(0, data.length) + }, + + other: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + + if (textDecoders.has(this.toString())) { + try { + return textDecoders.get(this).decode(data) + } catch {} + } + return typeof data === 'string' + ? data + : data.toString() + } +} + +function decodeText (text, sourceEncoding, destEncoding) { + if (text) { + return getDecoder(destEncoding)(text, sourceEncoding) + } + return text +} + +module.exports = decodeText + + +/***/ }), + +/***/ 1467: +/***/ ((module) => { + +"use strict"; + + +module.exports = function getLimit (limits, name, defaultLimit) { + if ( + !limits || + limits[name] === undefined || + limits[name] === null + ) { return defaultLimit } + + if ( + typeof limits[name] !== 'number' || + isNaN(limits[name]) + ) { throw new TypeError('Limit ' + name + ' is not a valid number') } + + return limits[name] +} + + +/***/ }), + +/***/ 1854: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +/* eslint-disable object-property-newline */ + + +const decodeText = __nccwpck_require__(4619) + +const RE_ENCODED = /%[a-fA-F0-9][a-fA-F0-9]/g + +const EncodedLookup = { + '%00': '\x00', '%01': '\x01', '%02': '\x02', '%03': '\x03', '%04': '\x04', + '%05': '\x05', '%06': '\x06', '%07': '\x07', '%08': '\x08', '%09': '\x09', + '%0a': '\x0a', '%0A': '\x0a', '%0b': '\x0b', '%0B': '\x0b', '%0c': '\x0c', + '%0C': '\x0c', '%0d': '\x0d', '%0D': '\x0d', '%0e': '\x0e', '%0E': '\x0e', + '%0f': '\x0f', '%0F': '\x0f', '%10': '\x10', '%11': '\x11', '%12': '\x12', + '%13': '\x13', '%14': '\x14', '%15': '\x15', '%16': '\x16', '%17': '\x17', + '%18': '\x18', '%19': '\x19', '%1a': '\x1a', '%1A': '\x1a', '%1b': '\x1b', + '%1B': '\x1b', '%1c': '\x1c', '%1C': '\x1c', '%1d': '\x1d', '%1D': '\x1d', + '%1e': '\x1e', '%1E': '\x1e', '%1f': '\x1f', '%1F': '\x1f', '%20': '\x20', + '%21': '\x21', '%22': '\x22', '%23': '\x23', '%24': '\x24', '%25': '\x25', + '%26': '\x26', '%27': '\x27', '%28': '\x28', '%29': '\x29', '%2a': '\x2a', + '%2A': '\x2a', '%2b': '\x2b', '%2B': '\x2b', '%2c': '\x2c', '%2C': '\x2c', + '%2d': '\x2d', '%2D': '\x2d', '%2e': '\x2e', '%2E': '\x2e', '%2f': '\x2f', + '%2F': '\x2f', '%30': '\x30', '%31': '\x31', '%32': '\x32', '%33': '\x33', + '%34': '\x34', '%35': '\x35', '%36': '\x36', '%37': '\x37', '%38': '\x38', + '%39': '\x39', '%3a': '\x3a', '%3A': '\x3a', '%3b': '\x3b', '%3B': '\x3b', + '%3c': '\x3c', '%3C': '\x3c', '%3d': '\x3d', '%3D': '\x3d', '%3e': '\x3e', + '%3E': '\x3e', '%3f': '\x3f', '%3F': '\x3f', '%40': '\x40', '%41': '\x41', + '%42': '\x42', '%43': '\x43', '%44': '\x44', '%45': '\x45', '%46': '\x46', + '%47': '\x47', '%48': '\x48', '%49': '\x49', '%4a': '\x4a', '%4A': '\x4a', + '%4b': '\x4b', '%4B': '\x4b', '%4c': '\x4c', '%4C': '\x4c', '%4d': '\x4d', + '%4D': '\x4d', '%4e': '\x4e', '%4E': '\x4e', '%4f': '\x4f', '%4F': '\x4f', + '%50': '\x50', '%51': '\x51', '%52': '\x52', '%53': '\x53', '%54': '\x54', + '%55': '\x55', '%56': '\x56', '%57': '\x57', '%58': '\x58', '%59': '\x59', + '%5a': '\x5a', '%5A': '\x5a', '%5b': '\x5b', '%5B': '\x5b', '%5c': '\x5c', + '%5C': '\x5c', '%5d': '\x5d', '%5D': '\x5d', '%5e': '\x5e', '%5E': '\x5e', + '%5f': '\x5f', '%5F': '\x5f', '%60': '\x60', '%61': '\x61', '%62': '\x62', + '%63': '\x63', '%64': '\x64', '%65': '\x65', '%66': '\x66', '%67': '\x67', + '%68': '\x68', '%69': '\x69', '%6a': '\x6a', '%6A': '\x6a', '%6b': '\x6b', + '%6B': '\x6b', '%6c': '\x6c', '%6C': '\x6c', '%6d': '\x6d', '%6D': '\x6d', + '%6e': '\x6e', '%6E': '\x6e', '%6f': '\x6f', '%6F': '\x6f', '%70': '\x70', + '%71': '\x71', '%72': '\x72', '%73': '\x73', '%74': '\x74', '%75': '\x75', + '%76': '\x76', '%77': '\x77', '%78': '\x78', '%79': '\x79', '%7a': '\x7a', + '%7A': '\x7a', '%7b': '\x7b', '%7B': '\x7b', '%7c': '\x7c', '%7C': '\x7c', + '%7d': '\x7d', '%7D': '\x7d', '%7e': '\x7e', '%7E': '\x7e', '%7f': '\x7f', + '%7F': '\x7f', '%80': '\x80', '%81': '\x81', '%82': '\x82', '%83': '\x83', + '%84': '\x84', '%85': '\x85', '%86': '\x86', '%87': '\x87', '%88': '\x88', + '%89': '\x89', '%8a': '\x8a', '%8A': '\x8a', '%8b': '\x8b', '%8B': '\x8b', + '%8c': '\x8c', '%8C': '\x8c', '%8d': '\x8d', '%8D': '\x8d', '%8e': '\x8e', + '%8E': '\x8e', '%8f': '\x8f', '%8F': '\x8f', '%90': '\x90', '%91': '\x91', + '%92': '\x92', '%93': '\x93', '%94': '\x94', '%95': '\x95', '%96': '\x96', + '%97': '\x97', '%98': '\x98', '%99': '\x99', '%9a': '\x9a', '%9A': '\x9a', + '%9b': '\x9b', '%9B': '\x9b', '%9c': '\x9c', '%9C': '\x9c', '%9d': '\x9d', + '%9D': '\x9d', '%9e': '\x9e', '%9E': '\x9e', '%9f': '\x9f', '%9F': '\x9f', + '%a0': '\xa0', '%A0': '\xa0', '%a1': '\xa1', '%A1': '\xa1', '%a2': '\xa2', + '%A2': '\xa2', '%a3': '\xa3', '%A3': '\xa3', '%a4': '\xa4', '%A4': '\xa4', + '%a5': '\xa5', '%A5': '\xa5', '%a6': '\xa6', '%A6': '\xa6', '%a7': '\xa7', + '%A7': '\xa7', '%a8': '\xa8', '%A8': '\xa8', '%a9': '\xa9', '%A9': '\xa9', + '%aa': '\xaa', '%Aa': '\xaa', '%aA': '\xaa', '%AA': '\xaa', '%ab': '\xab', + '%Ab': '\xab', '%aB': '\xab', '%AB': '\xab', '%ac': '\xac', '%Ac': '\xac', + '%aC': '\xac', '%AC': '\xac', '%ad': '\xad', '%Ad': '\xad', '%aD': '\xad', + '%AD': '\xad', '%ae': '\xae', '%Ae': '\xae', '%aE': '\xae', '%AE': '\xae', + '%af': '\xaf', '%Af': '\xaf', '%aF': '\xaf', '%AF': '\xaf', '%b0': '\xb0', + '%B0': '\xb0', '%b1': '\xb1', '%B1': '\xb1', '%b2': '\xb2', '%B2': '\xb2', + '%b3': '\xb3', '%B3': '\xb3', '%b4': '\xb4', '%B4': '\xb4', '%b5': '\xb5', + '%B5': '\xb5', '%b6': '\xb6', '%B6': '\xb6', '%b7': '\xb7', '%B7': '\xb7', + '%b8': '\xb8', '%B8': '\xb8', '%b9': '\xb9', '%B9': '\xb9', '%ba': '\xba', + '%Ba': '\xba', '%bA': '\xba', '%BA': '\xba', '%bb': '\xbb', '%Bb': '\xbb', + '%bB': '\xbb', '%BB': '\xbb', '%bc': '\xbc', '%Bc': '\xbc', '%bC': '\xbc', + '%BC': '\xbc', '%bd': '\xbd', '%Bd': '\xbd', '%bD': '\xbd', '%BD': '\xbd', + '%be': '\xbe', '%Be': '\xbe', '%bE': '\xbe', '%BE': '\xbe', '%bf': '\xbf', + '%Bf': '\xbf', '%bF': '\xbf', '%BF': '\xbf', '%c0': '\xc0', '%C0': '\xc0', + '%c1': '\xc1', '%C1': '\xc1', '%c2': '\xc2', '%C2': '\xc2', '%c3': '\xc3', + '%C3': '\xc3', '%c4': '\xc4', '%C4': '\xc4', '%c5': '\xc5', '%C5': '\xc5', + '%c6': '\xc6', '%C6': '\xc6', '%c7': '\xc7', '%C7': '\xc7', '%c8': '\xc8', + '%C8': '\xc8', '%c9': '\xc9', '%C9': '\xc9', '%ca': '\xca', '%Ca': '\xca', + '%cA': '\xca', '%CA': '\xca', '%cb': '\xcb', '%Cb': '\xcb', '%cB': '\xcb', + '%CB': '\xcb', '%cc': '\xcc', '%Cc': '\xcc', '%cC': '\xcc', '%CC': '\xcc', + '%cd': '\xcd', '%Cd': '\xcd', '%cD': '\xcd', '%CD': '\xcd', '%ce': '\xce', + '%Ce': '\xce', '%cE': '\xce', '%CE': '\xce', '%cf': '\xcf', '%Cf': '\xcf', + '%cF': '\xcf', '%CF': '\xcf', '%d0': '\xd0', '%D0': '\xd0', '%d1': '\xd1', + '%D1': '\xd1', '%d2': '\xd2', '%D2': '\xd2', '%d3': '\xd3', '%D3': '\xd3', + '%d4': '\xd4', '%D4': '\xd4', '%d5': '\xd5', '%D5': '\xd5', '%d6': '\xd6', + '%D6': '\xd6', '%d7': '\xd7', '%D7': '\xd7', '%d8': '\xd8', '%D8': '\xd8', + '%d9': '\xd9', '%D9': '\xd9', '%da': '\xda', '%Da': '\xda', '%dA': '\xda', + '%DA': '\xda', '%db': '\xdb', '%Db': '\xdb', '%dB': '\xdb', '%DB': '\xdb', + '%dc': '\xdc', '%Dc': '\xdc', '%dC': '\xdc', '%DC': '\xdc', '%dd': '\xdd', + '%Dd': '\xdd', '%dD': '\xdd', '%DD': '\xdd', '%de': '\xde', '%De': '\xde', + '%dE': '\xde', '%DE': '\xde', '%df': '\xdf', '%Df': '\xdf', '%dF': '\xdf', + '%DF': '\xdf', '%e0': '\xe0', '%E0': '\xe0', '%e1': '\xe1', '%E1': '\xe1', + '%e2': '\xe2', '%E2': '\xe2', '%e3': '\xe3', '%E3': '\xe3', '%e4': '\xe4', + '%E4': '\xe4', '%e5': '\xe5', '%E5': '\xe5', '%e6': '\xe6', '%E6': '\xe6', + '%e7': '\xe7', '%E7': '\xe7', '%e8': '\xe8', '%E8': '\xe8', '%e9': '\xe9', + '%E9': '\xe9', '%ea': '\xea', '%Ea': '\xea', '%eA': '\xea', '%EA': '\xea', + '%eb': '\xeb', '%Eb': '\xeb', '%eB': '\xeb', '%EB': '\xeb', '%ec': '\xec', + '%Ec': '\xec', '%eC': '\xec', '%EC': '\xec', '%ed': '\xed', '%Ed': '\xed', + '%eD': '\xed', '%ED': '\xed', '%ee': '\xee', '%Ee': '\xee', '%eE': '\xee', + '%EE': '\xee', '%ef': '\xef', '%Ef': '\xef', '%eF': '\xef', '%EF': '\xef', + '%f0': '\xf0', '%F0': '\xf0', '%f1': '\xf1', '%F1': '\xf1', '%f2': '\xf2', + '%F2': '\xf2', '%f3': '\xf3', '%F3': '\xf3', '%f4': '\xf4', '%F4': '\xf4', + '%f5': '\xf5', '%F5': '\xf5', '%f6': '\xf6', '%F6': '\xf6', '%f7': '\xf7', + '%F7': '\xf7', '%f8': '\xf8', '%F8': '\xf8', '%f9': '\xf9', '%F9': '\xf9', + '%fa': '\xfa', '%Fa': '\xfa', '%fA': '\xfa', '%FA': '\xfa', '%fb': '\xfb', + '%Fb': '\xfb', '%fB': '\xfb', '%FB': '\xfb', '%fc': '\xfc', '%Fc': '\xfc', + '%fC': '\xfc', '%FC': '\xfc', '%fd': '\xfd', '%Fd': '\xfd', '%fD': '\xfd', + '%FD': '\xfd', '%fe': '\xfe', '%Fe': '\xfe', '%fE': '\xfe', '%FE': '\xfe', + '%ff': '\xff', '%Ff': '\xff', '%fF': '\xff', '%FF': '\xff' +} + +function encodedReplacer (match) { + return EncodedLookup[match] +} + +const STATE_KEY = 0 +const STATE_VALUE = 1 +const STATE_CHARSET = 2 +const STATE_LANG = 3 + +function parseParams (str) { + const res = [] + let state = STATE_KEY + let charset = '' + let inquote = false + let escaping = false + let p = 0 + let tmp = '' + const len = str.length + + for (var i = 0; i < len; ++i) { // eslint-disable-line no-var + const char = str[i] + if (char === '\\' && inquote) { + if (escaping) { escaping = false } else { + escaping = true + continue + } + } else if (char === '"') { + if (!escaping) { + if (inquote) { + inquote = false + state = STATE_KEY + } else { inquote = true } + continue + } else { escaping = false } + } else { + if (escaping && inquote) { tmp += '\\' } + escaping = false + if ((state === STATE_CHARSET || state === STATE_LANG) && char === "'") { + if (state === STATE_CHARSET) { + state = STATE_LANG + charset = tmp.substring(1) + } else { state = STATE_VALUE } + tmp = '' + continue + } else if (state === STATE_KEY && + (char === '*' || char === '=') && + res.length) { + state = char === '*' + ? STATE_CHARSET + : STATE_VALUE + res[p] = [tmp, undefined] + tmp = '' + continue + } else if (!inquote && char === ';') { + state = STATE_KEY + if (charset) { + if (tmp.length) { + tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer), + 'binary', + charset) + } + charset = '' + } else if (tmp.length) { + tmp = decodeText(tmp, 'binary', 'utf8') + } + if (res[p] === undefined) { res[p] = tmp } else { res[p][1] = tmp } + tmp = '' + ++p + continue + } else if (!inquote && (char === ' ' || char === '\t')) { continue } + } + tmp += char + } + if (charset && tmp.length) { + tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer), + 'binary', + charset) + } else if (tmp) { + tmp = decodeText(tmp, 'binary', 'utf8') + } + + if (res[p] === undefined) { + if (tmp) { res[p] = tmp } + } else { res[p][1] = tmp } + + return res +} + +module.exports = parseParams + + +/***/ }), + +/***/ 2020: +/***/ ((module) => { + +"use strict"; +module.exports = JSON.parse('[[[0,44],"disallowed_STD3_valid"],[[45,46],"valid"],[[47,47],"disallowed_STD3_valid"],[[48,57],"valid"],[[58,64],"disallowed_STD3_valid"],[[65,65],"mapped",[97]],[[66,66],"mapped",[98]],[[67,67],"mapped",[99]],[[68,68],"mapped",[100]],[[69,69],"mapped",[101]],[[70,70],"mapped",[102]],[[71,71],"mapped",[103]],[[72,72],"mapped",[104]],[[73,73],"mapped",[105]],[[74,74],"mapped",[106]],[[75,75],"mapped",[107]],[[76,76],"mapped",[108]],[[77,77],"mapped",[109]],[[78,78],"mapped",[110]],[[79,79],"mapped",[111]],[[80,80],"mapped",[112]],[[81,81],"mapped",[113]],[[82,82],"mapped",[114]],[[83,83],"mapped",[115]],[[84,84],"mapped",[116]],[[85,85],"mapped",[117]],[[86,86],"mapped",[118]],[[87,87],"mapped",[119]],[[88,88],"mapped",[120]],[[89,89],"mapped",[121]],[[90,90],"mapped",[122]],[[91,96],"disallowed_STD3_valid"],[[97,122],"valid"],[[123,127],"disallowed_STD3_valid"],[[128,159],"disallowed"],[[160,160],"disallowed_STD3_mapped",[32]],[[161,167],"valid",[],"NV8"],[[168,168],"disallowed_STD3_mapped",[32,776]],[[169,169],"valid",[],"NV8"],[[170,170],"mapped",[97]],[[171,172],"valid",[],"NV8"],[[173,173],"ignored"],[[174,174],"valid",[],"NV8"],[[175,175],"disallowed_STD3_mapped",[32,772]],[[176,177],"valid",[],"NV8"],[[178,178],"mapped",[50]],[[179,179],"mapped",[51]],[[180,180],"disallowed_STD3_mapped",[32,769]],[[181,181],"mapped",[956]],[[182,182],"valid",[],"NV8"],[[183,183],"valid"],[[184,184],"disallowed_STD3_mapped",[32,807]],[[185,185],"mapped",[49]],[[186,186],"mapped",[111]],[[187,187],"valid",[],"NV8"],[[188,188],"mapped",[49,8260,52]],[[189,189],"mapped",[49,8260,50]],[[190,190],"mapped",[51,8260,52]],[[191,191],"valid",[],"NV8"],[[192,192],"mapped",[224]],[[193,193],"mapped",[225]],[[194,194],"mapped",[226]],[[195,195],"mapped",[227]],[[196,196],"mapped",[228]],[[197,197],"mapped",[229]],[[198,198],"mapped",[230]],[[199,199],"mapped",[231]],[[200,200],"mapped",[232]],[[201,201],"mapped",[233]],[[202,202],"mapped",[234]],[[203,203],"mapped",[235]],[[204,204],"mapped",[236]],[[205,205],"mapped",[237]],[[206,206],"mapped",[238]],[[207,207],"mapped",[239]],[[208,208],"mapped",[240]],[[209,209],"mapped",[241]],[[210,210],"mapped",[242]],[[211,211],"mapped",[243]],[[212,212],"mapped",[244]],[[213,213],"mapped",[245]],[[214,214],"mapped",[246]],[[215,215],"valid",[],"NV8"],[[216,216],"mapped",[248]],[[217,217],"mapped",[249]],[[218,218],"mapped",[250]],[[219,219],"mapped",[251]],[[220,220],"mapped",[252]],[[221,221],"mapped",[253]],[[222,222],"mapped",[254]],[[223,223],"deviation",[115,115]],[[224,246],"valid"],[[247,247],"valid",[],"NV8"],[[248,255],"valid"],[[256,256],"mapped",[257]],[[257,257],"valid"],[[258,258],"mapped",[259]],[[259,259],"valid"],[[260,260],"mapped",[261]],[[261,261],"valid"],[[262,262],"mapped",[263]],[[263,263],"valid"],[[264,264],"mapped",[265]],[[265,265],"valid"],[[266,266],"mapped",[267]],[[267,267],"valid"],[[268,268],"mapped",[269]],[[269,269],"valid"],[[270,270],"mapped",[271]],[[271,271],"valid"],[[272,272],"mapped",[273]],[[273,273],"valid"],[[274,274],"mapped",[275]],[[275,275],"valid"],[[276,276],"mapped",[277]],[[277,277],"valid"],[[278,278],"mapped",[279]],[[279,279],"valid"],[[280,280],"mapped",[281]],[[281,281],"valid"],[[282,282],"mapped",[283]],[[283,283],"valid"],[[284,284],"mapped",[285]],[[285,285],"valid"],[[286,286],"mapped",[287]],[[287,287],"valid"],[[288,288],"mapped",[289]],[[289,289],"valid"],[[290,290],"mapped",[291]],[[291,291],"valid"],[[292,292],"mapped",[293]],[[293,293],"valid"],[[294,294],"mapped",[295]],[[295,295],"valid"],[[296,296],"mapped",[297]],[[297,297],"valid"],[[298,298],"mapped",[299]],[[299,299],"valid"],[[300,300],"mapped",[301]],[[301,301],"valid"],[[302,302],"mapped",[303]],[[303,303],"valid"],[[304,304],"mapped",[105,775]],[[305,305],"valid"],[[306,307],"mapped",[105,106]],[[308,308],"mapped",[309]],[[309,309],"valid"],[[310,310],"mapped",[311]],[[311,312],"valid"],[[313,313],"mapped",[314]],[[314,314],"valid"],[[315,315],"mapped",[316]],[[316,316],"valid"],[[317,317],"mapped",[318]],[[318,318],"valid"],[[319,320],"mapped",[108,183]],[[321,321],"mapped",[322]],[[322,322],"valid"],[[323,323],"mapped",[324]],[[324,324],"valid"],[[325,325],"mapped",[326]],[[326,326],"valid"],[[327,327],"mapped",[328]],[[328,328],"valid"],[[329,329],"mapped",[700,110]],[[330,330],"mapped",[331]],[[331,331],"valid"],[[332,332],"mapped",[333]],[[333,333],"valid"],[[334,334],"mapped",[335]],[[335,335],"valid"],[[336,336],"mapped",[337]],[[337,337],"valid"],[[338,338],"mapped",[339]],[[339,339],"valid"],[[340,340],"mapped",[341]],[[341,341],"valid"],[[342,342],"mapped",[343]],[[343,343],"valid"],[[344,344],"mapped",[345]],[[345,345],"valid"],[[346,346],"mapped",[347]],[[347,347],"valid"],[[348,348],"mapped",[349]],[[349,349],"valid"],[[350,350],"mapped",[351]],[[351,351],"valid"],[[352,352],"mapped",[353]],[[353,353],"valid"],[[354,354],"mapped",[355]],[[355,355],"valid"],[[356,356],"mapped",[357]],[[357,357],"valid"],[[358,358],"mapped",[359]],[[359,359],"valid"],[[360,360],"mapped",[361]],[[361,361],"valid"],[[362,362],"mapped",[363]],[[363,363],"valid"],[[364,364],"mapped",[365]],[[365,365],"valid"],[[366,366],"mapped",[367]],[[367,367],"valid"],[[368,368],"mapped",[369]],[[369,369],"valid"],[[370,370],"mapped",[371]],[[371,371],"valid"],[[372,372],"mapped",[373]],[[373,373],"valid"],[[374,374],"mapped",[375]],[[375,375],"valid"],[[376,376],"mapped",[255]],[[377,377],"mapped",[378]],[[378,378],"valid"],[[379,379],"mapped",[380]],[[380,380],"valid"],[[381,381],"mapped",[382]],[[382,382],"valid"],[[383,383],"mapped",[115]],[[384,384],"valid"],[[385,385],"mapped",[595]],[[386,386],"mapped",[387]],[[387,387],"valid"],[[388,388],"mapped",[389]],[[389,389],"valid"],[[390,390],"mapped",[596]],[[391,391],"mapped",[392]],[[392,392],"valid"],[[393,393],"mapped",[598]],[[394,394],"mapped",[599]],[[395,395],"mapped",[396]],[[396,397],"valid"],[[398,398],"mapped",[477]],[[399,399],"mapped",[601]],[[400,400],"mapped",[603]],[[401,401],"mapped",[402]],[[402,402],"valid"],[[403,403],"mapped",[608]],[[404,404],"mapped",[611]],[[405,405],"valid"],[[406,406],"mapped",[617]],[[407,407],"mapped",[616]],[[408,408],"mapped",[409]],[[409,411],"valid"],[[412,412],"mapped",[623]],[[413,413],"mapped",[626]],[[414,414],"valid"],[[415,415],"mapped",[629]],[[416,416],"mapped",[417]],[[417,417],"valid"],[[418,418],"mapped",[419]],[[419,419],"valid"],[[420,420],"mapped",[421]],[[421,421],"valid"],[[422,422],"mapped",[640]],[[423,423],"mapped",[424]],[[424,424],"valid"],[[425,425],"mapped",[643]],[[426,427],"valid"],[[428,428],"mapped",[429]],[[429,429],"valid"],[[430,430],"mapped",[648]],[[431,431],"mapped",[432]],[[432,432],"valid"],[[433,433],"mapped",[650]],[[434,434],"mapped",[651]],[[435,435],"mapped",[436]],[[436,436],"valid"],[[437,437],"mapped",[438]],[[438,438],"valid"],[[439,439],"mapped",[658]],[[440,440],"mapped",[441]],[[441,443],"valid"],[[444,444],"mapped",[445]],[[445,451],"valid"],[[452,454],"mapped",[100,382]],[[455,457],"mapped",[108,106]],[[458,460],"mapped",[110,106]],[[461,461],"mapped",[462]],[[462,462],"valid"],[[463,463],"mapped",[464]],[[464,464],"valid"],[[465,465],"mapped",[466]],[[466,466],"valid"],[[467,467],"mapped",[468]],[[468,468],"valid"],[[469,469],"mapped",[470]],[[470,470],"valid"],[[471,471],"mapped",[472]],[[472,472],"valid"],[[473,473],"mapped",[474]],[[474,474],"valid"],[[475,475],"mapped",[476]],[[476,477],"valid"],[[478,478],"mapped",[479]],[[479,479],"valid"],[[480,480],"mapped",[481]],[[481,481],"valid"],[[482,482],"mapped",[483]],[[483,483],"valid"],[[484,484],"mapped",[485]],[[485,485],"valid"],[[486,486],"mapped",[487]],[[487,487],"valid"],[[488,488],"mapped",[489]],[[489,489],"valid"],[[490,490],"mapped",[491]],[[491,491],"valid"],[[492,492],"mapped",[493]],[[493,493],"valid"],[[494,494],"mapped",[495]],[[495,496],"valid"],[[497,499],"mapped",[100,122]],[[500,500],"mapped",[501]],[[501,501],"valid"],[[502,502],"mapped",[405]],[[503,503],"mapped",[447]],[[504,504],"mapped",[505]],[[505,505],"valid"],[[506,506],"mapped",[507]],[[507,507],"valid"],[[508,508],"mapped",[509]],[[509,509],"valid"],[[510,510],"mapped",[511]],[[511,511],"valid"],[[512,512],"mapped",[513]],[[513,513],"valid"],[[514,514],"mapped",[515]],[[515,515],"valid"],[[516,516],"mapped",[517]],[[517,517],"valid"],[[518,518],"mapped",[519]],[[519,519],"valid"],[[520,520],"mapped",[521]],[[521,521],"valid"],[[522,522],"mapped",[523]],[[523,523],"valid"],[[524,524],"mapped",[525]],[[525,525],"valid"],[[526,526],"mapped",[527]],[[527,527],"valid"],[[528,528],"mapped",[529]],[[529,529],"valid"],[[530,530],"mapped",[531]],[[531,531],"valid"],[[532,532],"mapped",[533]],[[533,533],"valid"],[[534,534],"mapped",[535]],[[535,535],"valid"],[[536,536],"mapped",[537]],[[537,537],"valid"],[[538,538],"mapped",[539]],[[539,539],"valid"],[[540,540],"mapped",[541]],[[541,541],"valid"],[[542,542],"mapped",[543]],[[543,543],"valid"],[[544,544],"mapped",[414]],[[545,545],"valid"],[[546,546],"mapped",[547]],[[547,547],"valid"],[[548,548],"mapped",[549]],[[549,549],"valid"],[[550,550],"mapped",[551]],[[551,551],"valid"],[[552,552],"mapped",[553]],[[553,553],"valid"],[[554,554],"mapped",[555]],[[555,555],"valid"],[[556,556],"mapped",[557]],[[557,557],"valid"],[[558,558],"mapped",[559]],[[559,559],"valid"],[[560,560],"mapped",[561]],[[561,561],"valid"],[[562,562],"mapped",[563]],[[563,563],"valid"],[[564,566],"valid"],[[567,569],"valid"],[[570,570],"mapped",[11365]],[[571,571],"mapped",[572]],[[572,572],"valid"],[[573,573],"mapped",[410]],[[574,574],"mapped",[11366]],[[575,576],"valid"],[[577,577],"mapped",[578]],[[578,578],"valid"],[[579,579],"mapped",[384]],[[580,580],"mapped",[649]],[[581,581],"mapped",[652]],[[582,582],"mapped",[583]],[[583,583],"valid"],[[584,584],"mapped",[585]],[[585,585],"valid"],[[586,586],"mapped",[587]],[[587,587],"valid"],[[588,588],"mapped",[589]],[[589,589],"valid"],[[590,590],"mapped",[591]],[[591,591],"valid"],[[592,680],"valid"],[[681,685],"valid"],[[686,687],"valid"],[[688,688],"mapped",[104]],[[689,689],"mapped",[614]],[[690,690],"mapped",[106]],[[691,691],"mapped",[114]],[[692,692],"mapped",[633]],[[693,693],"mapped",[635]],[[694,694],"mapped",[641]],[[695,695],"mapped",[119]],[[696,696],"mapped",[121]],[[697,705],"valid"],[[706,709],"valid",[],"NV8"],[[710,721],"valid"],[[722,727],"valid",[],"NV8"],[[728,728],"disallowed_STD3_mapped",[32,774]],[[729,729],"disallowed_STD3_mapped",[32,775]],[[730,730],"disallowed_STD3_mapped",[32,778]],[[731,731],"disallowed_STD3_mapped",[32,808]],[[732,732],"disallowed_STD3_mapped",[32,771]],[[733,733],"disallowed_STD3_mapped",[32,779]],[[734,734],"valid",[],"NV8"],[[735,735],"valid",[],"NV8"],[[736,736],"mapped",[611]],[[737,737],"mapped",[108]],[[738,738],"mapped",[115]],[[739,739],"mapped",[120]],[[740,740],"mapped",[661]],[[741,745],"valid",[],"NV8"],[[746,747],"valid",[],"NV8"],[[748,748],"valid"],[[749,749],"valid",[],"NV8"],[[750,750],"valid"],[[751,767],"valid",[],"NV8"],[[768,831],"valid"],[[832,832],"mapped",[768]],[[833,833],"mapped",[769]],[[834,834],"valid"],[[835,835],"mapped",[787]],[[836,836],"mapped",[776,769]],[[837,837],"mapped",[953]],[[838,846],"valid"],[[847,847],"ignored"],[[848,855],"valid"],[[856,860],"valid"],[[861,863],"valid"],[[864,865],"valid"],[[866,866],"valid"],[[867,879],"valid"],[[880,880],"mapped",[881]],[[881,881],"valid"],[[882,882],"mapped",[883]],[[883,883],"valid"],[[884,884],"mapped",[697]],[[885,885],"valid"],[[886,886],"mapped",[887]],[[887,887],"valid"],[[888,889],"disallowed"],[[890,890],"disallowed_STD3_mapped",[32,953]],[[891,893],"valid"],[[894,894],"disallowed_STD3_mapped",[59]],[[895,895],"mapped",[1011]],[[896,899],"disallowed"],[[900,900],"disallowed_STD3_mapped",[32,769]],[[901,901],"disallowed_STD3_mapped",[32,776,769]],[[902,902],"mapped",[940]],[[903,903],"mapped",[183]],[[904,904],"mapped",[941]],[[905,905],"mapped",[942]],[[906,906],"mapped",[943]],[[907,907],"disallowed"],[[908,908],"mapped",[972]],[[909,909],"disallowed"],[[910,910],"mapped",[973]],[[911,911],"mapped",[974]],[[912,912],"valid"],[[913,913],"mapped",[945]],[[914,914],"mapped",[946]],[[915,915],"mapped",[947]],[[916,916],"mapped",[948]],[[917,917],"mapped",[949]],[[918,918],"mapped",[950]],[[919,919],"mapped",[951]],[[920,920],"mapped",[952]],[[921,921],"mapped",[953]],[[922,922],"mapped",[954]],[[923,923],"mapped",[955]],[[924,924],"mapped",[956]],[[925,925],"mapped",[957]],[[926,926],"mapped",[958]],[[927,927],"mapped",[959]],[[928,928],"mapped",[960]],[[929,929],"mapped",[961]],[[930,930],"disallowed"],[[931,931],"mapped",[963]],[[932,932],"mapped",[964]],[[933,933],"mapped",[965]],[[934,934],"mapped",[966]],[[935,935],"mapped",[967]],[[936,936],"mapped",[968]],[[937,937],"mapped",[969]],[[938,938],"mapped",[970]],[[939,939],"mapped",[971]],[[940,961],"valid"],[[962,962],"deviation",[963]],[[963,974],"valid"],[[975,975],"mapped",[983]],[[976,976],"mapped",[946]],[[977,977],"mapped",[952]],[[978,978],"mapped",[965]],[[979,979],"mapped",[973]],[[980,980],"mapped",[971]],[[981,981],"mapped",[966]],[[982,982],"mapped",[960]],[[983,983],"valid"],[[984,984],"mapped",[985]],[[985,985],"valid"],[[986,986],"mapped",[987]],[[987,987],"valid"],[[988,988],"mapped",[989]],[[989,989],"valid"],[[990,990],"mapped",[991]],[[991,991],"valid"],[[992,992],"mapped",[993]],[[993,993],"valid"],[[994,994],"mapped",[995]],[[995,995],"valid"],[[996,996],"mapped",[997]],[[997,997],"valid"],[[998,998],"mapped",[999]],[[999,999],"valid"],[[1000,1000],"mapped",[1001]],[[1001,1001],"valid"],[[1002,1002],"mapped",[1003]],[[1003,1003],"valid"],[[1004,1004],"mapped",[1005]],[[1005,1005],"valid"],[[1006,1006],"mapped",[1007]],[[1007,1007],"valid"],[[1008,1008],"mapped",[954]],[[1009,1009],"mapped",[961]],[[1010,1010],"mapped",[963]],[[1011,1011],"valid"],[[1012,1012],"mapped",[952]],[[1013,1013],"mapped",[949]],[[1014,1014],"valid",[],"NV8"],[[1015,1015],"mapped",[1016]],[[1016,1016],"valid"],[[1017,1017],"mapped",[963]],[[1018,1018],"mapped",[1019]],[[1019,1019],"valid"],[[1020,1020],"valid"],[[1021,1021],"mapped",[891]],[[1022,1022],"mapped",[892]],[[1023,1023],"mapped",[893]],[[1024,1024],"mapped",[1104]],[[1025,1025],"mapped",[1105]],[[1026,1026],"mapped",[1106]],[[1027,1027],"mapped",[1107]],[[1028,1028],"mapped",[1108]],[[1029,1029],"mapped",[1109]],[[1030,1030],"mapped",[1110]],[[1031,1031],"mapped",[1111]],[[1032,1032],"mapped",[1112]],[[1033,1033],"mapped",[1113]],[[1034,1034],"mapped",[1114]],[[1035,1035],"mapped",[1115]],[[1036,1036],"mapped",[1116]],[[1037,1037],"mapped",[1117]],[[1038,1038],"mapped",[1118]],[[1039,1039],"mapped",[1119]],[[1040,1040],"mapped",[1072]],[[1041,1041],"mapped",[1073]],[[1042,1042],"mapped",[1074]],[[1043,1043],"mapped",[1075]],[[1044,1044],"mapped",[1076]],[[1045,1045],"mapped",[1077]],[[1046,1046],"mapped",[1078]],[[1047,1047],"mapped",[1079]],[[1048,1048],"mapped",[1080]],[[1049,1049],"mapped",[1081]],[[1050,1050],"mapped",[1082]],[[1051,1051],"mapped",[1083]],[[1052,1052],"mapped",[1084]],[[1053,1053],"mapped",[1085]],[[1054,1054],"mapped",[1086]],[[1055,1055],"mapped",[1087]],[[1056,1056],"mapped",[1088]],[[1057,1057],"mapped",[1089]],[[1058,1058],"mapped",[1090]],[[1059,1059],"mapped",[1091]],[[1060,1060],"mapped",[1092]],[[1061,1061],"mapped",[1093]],[[1062,1062],"mapped",[1094]],[[1063,1063],"mapped",[1095]],[[1064,1064],"mapped",[1096]],[[1065,1065],"mapped",[1097]],[[1066,1066],"mapped",[1098]],[[1067,1067],"mapped",[1099]],[[1068,1068],"mapped",[1100]],[[1069,1069],"mapped",[1101]],[[1070,1070],"mapped",[1102]],[[1071,1071],"mapped",[1103]],[[1072,1103],"valid"],[[1104,1104],"valid"],[[1105,1116],"valid"],[[1117,1117],"valid"],[[1118,1119],"valid"],[[1120,1120],"mapped",[1121]],[[1121,1121],"valid"],[[1122,1122],"mapped",[1123]],[[1123,1123],"valid"],[[1124,1124],"mapped",[1125]],[[1125,1125],"valid"],[[1126,1126],"mapped",[1127]],[[1127,1127],"valid"],[[1128,1128],"mapped",[1129]],[[1129,1129],"valid"],[[1130,1130],"mapped",[1131]],[[1131,1131],"valid"],[[1132,1132],"mapped",[1133]],[[1133,1133],"valid"],[[1134,1134],"mapped",[1135]],[[1135,1135],"valid"],[[1136,1136],"mapped",[1137]],[[1137,1137],"valid"],[[1138,1138],"mapped",[1139]],[[1139,1139],"valid"],[[1140,1140],"mapped",[1141]],[[1141,1141],"valid"],[[1142,1142],"mapped",[1143]],[[1143,1143],"valid"],[[1144,1144],"mapped",[1145]],[[1145,1145],"valid"],[[1146,1146],"mapped",[1147]],[[1147,1147],"valid"],[[1148,1148],"mapped",[1149]],[[1149,1149],"valid"],[[1150,1150],"mapped",[1151]],[[1151,1151],"valid"],[[1152,1152],"mapped",[1153]],[[1153,1153],"valid"],[[1154,1154],"valid",[],"NV8"],[[1155,1158],"valid"],[[1159,1159],"valid"],[[1160,1161],"valid",[],"NV8"],[[1162,1162],"mapped",[1163]],[[1163,1163],"valid"],[[1164,1164],"mapped",[1165]],[[1165,1165],"valid"],[[1166,1166],"mapped",[1167]],[[1167,1167],"valid"],[[1168,1168],"mapped",[1169]],[[1169,1169],"valid"],[[1170,1170],"mapped",[1171]],[[1171,1171],"valid"],[[1172,1172],"mapped",[1173]],[[1173,1173],"valid"],[[1174,1174],"mapped",[1175]],[[1175,1175],"valid"],[[1176,1176],"mapped",[1177]],[[1177,1177],"valid"],[[1178,1178],"mapped",[1179]],[[1179,1179],"valid"],[[1180,1180],"mapped",[1181]],[[1181,1181],"valid"],[[1182,1182],"mapped",[1183]],[[1183,1183],"valid"],[[1184,1184],"mapped",[1185]],[[1185,1185],"valid"],[[1186,1186],"mapped",[1187]],[[1187,1187],"valid"],[[1188,1188],"mapped",[1189]],[[1189,1189],"valid"],[[1190,1190],"mapped",[1191]],[[1191,1191],"valid"],[[1192,1192],"mapped",[1193]],[[1193,1193],"valid"],[[1194,1194],"mapped",[1195]],[[1195,1195],"valid"],[[1196,1196],"mapped",[1197]],[[1197,1197],"valid"],[[1198,1198],"mapped",[1199]],[[1199,1199],"valid"],[[1200,1200],"mapped",[1201]],[[1201,1201],"valid"],[[1202,1202],"mapped",[1203]],[[1203,1203],"valid"],[[1204,1204],"mapped",[1205]],[[1205,1205],"valid"],[[1206,1206],"mapped",[1207]],[[1207,1207],"valid"],[[1208,1208],"mapped",[1209]],[[1209,1209],"valid"],[[1210,1210],"mapped",[1211]],[[1211,1211],"valid"],[[1212,1212],"mapped",[1213]],[[1213,1213],"valid"],[[1214,1214],"mapped",[1215]],[[1215,1215],"valid"],[[1216,1216],"disallowed"],[[1217,1217],"mapped",[1218]],[[1218,1218],"valid"],[[1219,1219],"mapped",[1220]],[[1220,1220],"valid"],[[1221,1221],"mapped",[1222]],[[1222,1222],"valid"],[[1223,1223],"mapped",[1224]],[[1224,1224],"valid"],[[1225,1225],"mapped",[1226]],[[1226,1226],"valid"],[[1227,1227],"mapped",[1228]],[[1228,1228],"valid"],[[1229,1229],"mapped",[1230]],[[1230,1230],"valid"],[[1231,1231],"valid"],[[1232,1232],"mapped",[1233]],[[1233,1233],"valid"],[[1234,1234],"mapped",[1235]],[[1235,1235],"valid"],[[1236,1236],"mapped",[1237]],[[1237,1237],"valid"],[[1238,1238],"mapped",[1239]],[[1239,1239],"valid"],[[1240,1240],"mapped",[1241]],[[1241,1241],"valid"],[[1242,1242],"mapped",[1243]],[[1243,1243],"valid"],[[1244,1244],"mapped",[1245]],[[1245,1245],"valid"],[[1246,1246],"mapped",[1247]],[[1247,1247],"valid"],[[1248,1248],"mapped",[1249]],[[1249,1249],"valid"],[[1250,1250],"mapped",[1251]],[[1251,1251],"valid"],[[1252,1252],"mapped",[1253]],[[1253,1253],"valid"],[[1254,1254],"mapped",[1255]],[[1255,1255],"valid"],[[1256,1256],"mapped",[1257]],[[1257,1257],"valid"],[[1258,1258],"mapped",[1259]],[[1259,1259],"valid"],[[1260,1260],"mapped",[1261]],[[1261,1261],"valid"],[[1262,1262],"mapped",[1263]],[[1263,1263],"valid"],[[1264,1264],"mapped",[1265]],[[1265,1265],"valid"],[[1266,1266],"mapped",[1267]],[[1267,1267],"valid"],[[1268,1268],"mapped",[1269]],[[1269,1269],"valid"],[[1270,1270],"mapped",[1271]],[[1271,1271],"valid"],[[1272,1272],"mapped",[1273]],[[1273,1273],"valid"],[[1274,1274],"mapped",[1275]],[[1275,1275],"valid"],[[1276,1276],"mapped",[1277]],[[1277,1277],"valid"],[[1278,1278],"mapped",[1279]],[[1279,1279],"valid"],[[1280,1280],"mapped",[1281]],[[1281,1281],"valid"],[[1282,1282],"mapped",[1283]],[[1283,1283],"valid"],[[1284,1284],"mapped",[1285]],[[1285,1285],"valid"],[[1286,1286],"mapped",[1287]],[[1287,1287],"valid"],[[1288,1288],"mapped",[1289]],[[1289,1289],"valid"],[[1290,1290],"mapped",[1291]],[[1291,1291],"valid"],[[1292,1292],"mapped",[1293]],[[1293,1293],"valid"],[[1294,1294],"mapped",[1295]],[[1295,1295],"valid"],[[1296,1296],"mapped",[1297]],[[1297,1297],"valid"],[[1298,1298],"mapped",[1299]],[[1299,1299],"valid"],[[1300,1300],"mapped",[1301]],[[1301,1301],"valid"],[[1302,1302],"mapped",[1303]],[[1303,1303],"valid"],[[1304,1304],"mapped",[1305]],[[1305,1305],"valid"],[[1306,1306],"mapped",[1307]],[[1307,1307],"valid"],[[1308,1308],"mapped",[1309]],[[1309,1309],"valid"],[[1310,1310],"mapped",[1311]],[[1311,1311],"valid"],[[1312,1312],"mapped",[1313]],[[1313,1313],"valid"],[[1314,1314],"mapped",[1315]],[[1315,1315],"valid"],[[1316,1316],"mapped",[1317]],[[1317,1317],"valid"],[[1318,1318],"mapped",[1319]],[[1319,1319],"valid"],[[1320,1320],"mapped",[1321]],[[1321,1321],"valid"],[[1322,1322],"mapped",[1323]],[[1323,1323],"valid"],[[1324,1324],"mapped",[1325]],[[1325,1325],"valid"],[[1326,1326],"mapped",[1327]],[[1327,1327],"valid"],[[1328,1328],"disallowed"],[[1329,1329],"mapped",[1377]],[[1330,1330],"mapped",[1378]],[[1331,1331],"mapped",[1379]],[[1332,1332],"mapped",[1380]],[[1333,1333],"mapped",[1381]],[[1334,1334],"mapped",[1382]],[[1335,1335],"mapped",[1383]],[[1336,1336],"mapped",[1384]],[[1337,1337],"mapped",[1385]],[[1338,1338],"mapped",[1386]],[[1339,1339],"mapped",[1387]],[[1340,1340],"mapped",[1388]],[[1341,1341],"mapped",[1389]],[[1342,1342],"mapped",[1390]],[[1343,1343],"mapped",[1391]],[[1344,1344],"mapped",[1392]],[[1345,1345],"mapped",[1393]],[[1346,1346],"mapped",[1394]],[[1347,1347],"mapped",[1395]],[[1348,1348],"mapped",[1396]],[[1349,1349],"mapped",[1397]],[[1350,1350],"mapped",[1398]],[[1351,1351],"mapped",[1399]],[[1352,1352],"mapped",[1400]],[[1353,1353],"mapped",[1401]],[[1354,1354],"mapped",[1402]],[[1355,1355],"mapped",[1403]],[[1356,1356],"mapped",[1404]],[[1357,1357],"mapped",[1405]],[[1358,1358],"mapped",[1406]],[[1359,1359],"mapped",[1407]],[[1360,1360],"mapped",[1408]],[[1361,1361],"mapped",[1409]],[[1362,1362],"mapped",[1410]],[[1363,1363],"mapped",[1411]],[[1364,1364],"mapped",[1412]],[[1365,1365],"mapped",[1413]],[[1366,1366],"mapped",[1414]],[[1367,1368],"disallowed"],[[1369,1369],"valid"],[[1370,1375],"valid",[],"NV8"],[[1376,1376],"disallowed"],[[1377,1414],"valid"],[[1415,1415],"mapped",[1381,1410]],[[1416,1416],"disallowed"],[[1417,1417],"valid",[],"NV8"],[[1418,1418],"valid",[],"NV8"],[[1419,1420],"disallowed"],[[1421,1422],"valid",[],"NV8"],[[1423,1423],"valid",[],"NV8"],[[1424,1424],"disallowed"],[[1425,1441],"valid"],[[1442,1442],"valid"],[[1443,1455],"valid"],[[1456,1465],"valid"],[[1466,1466],"valid"],[[1467,1469],"valid"],[[1470,1470],"valid",[],"NV8"],[[1471,1471],"valid"],[[1472,1472],"valid",[],"NV8"],[[1473,1474],"valid"],[[1475,1475],"valid",[],"NV8"],[[1476,1476],"valid"],[[1477,1477],"valid"],[[1478,1478],"valid",[],"NV8"],[[1479,1479],"valid"],[[1480,1487],"disallowed"],[[1488,1514],"valid"],[[1515,1519],"disallowed"],[[1520,1524],"valid"],[[1525,1535],"disallowed"],[[1536,1539],"disallowed"],[[1540,1540],"disallowed"],[[1541,1541],"disallowed"],[[1542,1546],"valid",[],"NV8"],[[1547,1547],"valid",[],"NV8"],[[1548,1548],"valid",[],"NV8"],[[1549,1551],"valid",[],"NV8"],[[1552,1557],"valid"],[[1558,1562],"valid"],[[1563,1563],"valid",[],"NV8"],[[1564,1564],"disallowed"],[[1565,1565],"disallowed"],[[1566,1566],"valid",[],"NV8"],[[1567,1567],"valid",[],"NV8"],[[1568,1568],"valid"],[[1569,1594],"valid"],[[1595,1599],"valid"],[[1600,1600],"valid",[],"NV8"],[[1601,1618],"valid"],[[1619,1621],"valid"],[[1622,1624],"valid"],[[1625,1630],"valid"],[[1631,1631],"valid"],[[1632,1641],"valid"],[[1642,1645],"valid",[],"NV8"],[[1646,1647],"valid"],[[1648,1652],"valid"],[[1653,1653],"mapped",[1575,1652]],[[1654,1654],"mapped",[1608,1652]],[[1655,1655],"mapped",[1735,1652]],[[1656,1656],"mapped",[1610,1652]],[[1657,1719],"valid"],[[1720,1721],"valid"],[[1722,1726],"valid"],[[1727,1727],"valid"],[[1728,1742],"valid"],[[1743,1743],"valid"],[[1744,1747],"valid"],[[1748,1748],"valid",[],"NV8"],[[1749,1756],"valid"],[[1757,1757],"disallowed"],[[1758,1758],"valid",[],"NV8"],[[1759,1768],"valid"],[[1769,1769],"valid",[],"NV8"],[[1770,1773],"valid"],[[1774,1775],"valid"],[[1776,1785],"valid"],[[1786,1790],"valid"],[[1791,1791],"valid"],[[1792,1805],"valid",[],"NV8"],[[1806,1806],"disallowed"],[[1807,1807],"disallowed"],[[1808,1836],"valid"],[[1837,1839],"valid"],[[1840,1866],"valid"],[[1867,1868],"disallowed"],[[1869,1871],"valid"],[[1872,1901],"valid"],[[1902,1919],"valid"],[[1920,1968],"valid"],[[1969,1969],"valid"],[[1970,1983],"disallowed"],[[1984,2037],"valid"],[[2038,2042],"valid",[],"NV8"],[[2043,2047],"disallowed"],[[2048,2093],"valid"],[[2094,2095],"disallowed"],[[2096,2110],"valid",[],"NV8"],[[2111,2111],"disallowed"],[[2112,2139],"valid"],[[2140,2141],"disallowed"],[[2142,2142],"valid",[],"NV8"],[[2143,2207],"disallowed"],[[2208,2208],"valid"],[[2209,2209],"valid"],[[2210,2220],"valid"],[[2221,2226],"valid"],[[2227,2228],"valid"],[[2229,2274],"disallowed"],[[2275,2275],"valid"],[[2276,2302],"valid"],[[2303,2303],"valid"],[[2304,2304],"valid"],[[2305,2307],"valid"],[[2308,2308],"valid"],[[2309,2361],"valid"],[[2362,2363],"valid"],[[2364,2381],"valid"],[[2382,2382],"valid"],[[2383,2383],"valid"],[[2384,2388],"valid"],[[2389,2389],"valid"],[[2390,2391],"valid"],[[2392,2392],"mapped",[2325,2364]],[[2393,2393],"mapped",[2326,2364]],[[2394,2394],"mapped",[2327,2364]],[[2395,2395],"mapped",[2332,2364]],[[2396,2396],"mapped",[2337,2364]],[[2397,2397],"mapped",[2338,2364]],[[2398,2398],"mapped",[2347,2364]],[[2399,2399],"mapped",[2351,2364]],[[2400,2403],"valid"],[[2404,2405],"valid",[],"NV8"],[[2406,2415],"valid"],[[2416,2416],"valid",[],"NV8"],[[2417,2418],"valid"],[[2419,2423],"valid"],[[2424,2424],"valid"],[[2425,2426],"valid"],[[2427,2428],"valid"],[[2429,2429],"valid"],[[2430,2431],"valid"],[[2432,2432],"valid"],[[2433,2435],"valid"],[[2436,2436],"disallowed"],[[2437,2444],"valid"],[[2445,2446],"disallowed"],[[2447,2448],"valid"],[[2449,2450],"disallowed"],[[2451,2472],"valid"],[[2473,2473],"disallowed"],[[2474,2480],"valid"],[[2481,2481],"disallowed"],[[2482,2482],"valid"],[[2483,2485],"disallowed"],[[2486,2489],"valid"],[[2490,2491],"disallowed"],[[2492,2492],"valid"],[[2493,2493],"valid"],[[2494,2500],"valid"],[[2501,2502],"disallowed"],[[2503,2504],"valid"],[[2505,2506],"disallowed"],[[2507,2509],"valid"],[[2510,2510],"valid"],[[2511,2518],"disallowed"],[[2519,2519],"valid"],[[2520,2523],"disallowed"],[[2524,2524],"mapped",[2465,2492]],[[2525,2525],"mapped",[2466,2492]],[[2526,2526],"disallowed"],[[2527,2527],"mapped",[2479,2492]],[[2528,2531],"valid"],[[2532,2533],"disallowed"],[[2534,2545],"valid"],[[2546,2554],"valid",[],"NV8"],[[2555,2555],"valid",[],"NV8"],[[2556,2560],"disallowed"],[[2561,2561],"valid"],[[2562,2562],"valid"],[[2563,2563],"valid"],[[2564,2564],"disallowed"],[[2565,2570],"valid"],[[2571,2574],"disallowed"],[[2575,2576],"valid"],[[2577,2578],"disallowed"],[[2579,2600],"valid"],[[2601,2601],"disallowed"],[[2602,2608],"valid"],[[2609,2609],"disallowed"],[[2610,2610],"valid"],[[2611,2611],"mapped",[2610,2620]],[[2612,2612],"disallowed"],[[2613,2613],"valid"],[[2614,2614],"mapped",[2616,2620]],[[2615,2615],"disallowed"],[[2616,2617],"valid"],[[2618,2619],"disallowed"],[[2620,2620],"valid"],[[2621,2621],"disallowed"],[[2622,2626],"valid"],[[2627,2630],"disallowed"],[[2631,2632],"valid"],[[2633,2634],"disallowed"],[[2635,2637],"valid"],[[2638,2640],"disallowed"],[[2641,2641],"valid"],[[2642,2648],"disallowed"],[[2649,2649],"mapped",[2582,2620]],[[2650,2650],"mapped",[2583,2620]],[[2651,2651],"mapped",[2588,2620]],[[2652,2652],"valid"],[[2653,2653],"disallowed"],[[2654,2654],"mapped",[2603,2620]],[[2655,2661],"disallowed"],[[2662,2676],"valid"],[[2677,2677],"valid"],[[2678,2688],"disallowed"],[[2689,2691],"valid"],[[2692,2692],"disallowed"],[[2693,2699],"valid"],[[2700,2700],"valid"],[[2701,2701],"valid"],[[2702,2702],"disallowed"],[[2703,2705],"valid"],[[2706,2706],"disallowed"],[[2707,2728],"valid"],[[2729,2729],"disallowed"],[[2730,2736],"valid"],[[2737,2737],"disallowed"],[[2738,2739],"valid"],[[2740,2740],"disallowed"],[[2741,2745],"valid"],[[2746,2747],"disallowed"],[[2748,2757],"valid"],[[2758,2758],"disallowed"],[[2759,2761],"valid"],[[2762,2762],"disallowed"],[[2763,2765],"valid"],[[2766,2767],"disallowed"],[[2768,2768],"valid"],[[2769,2783],"disallowed"],[[2784,2784],"valid"],[[2785,2787],"valid"],[[2788,2789],"disallowed"],[[2790,2799],"valid"],[[2800,2800],"valid",[],"NV8"],[[2801,2801],"valid",[],"NV8"],[[2802,2808],"disallowed"],[[2809,2809],"valid"],[[2810,2816],"disallowed"],[[2817,2819],"valid"],[[2820,2820],"disallowed"],[[2821,2828],"valid"],[[2829,2830],"disallowed"],[[2831,2832],"valid"],[[2833,2834],"disallowed"],[[2835,2856],"valid"],[[2857,2857],"disallowed"],[[2858,2864],"valid"],[[2865,2865],"disallowed"],[[2866,2867],"valid"],[[2868,2868],"disallowed"],[[2869,2869],"valid"],[[2870,2873],"valid"],[[2874,2875],"disallowed"],[[2876,2883],"valid"],[[2884,2884],"valid"],[[2885,2886],"disallowed"],[[2887,2888],"valid"],[[2889,2890],"disallowed"],[[2891,2893],"valid"],[[2894,2901],"disallowed"],[[2902,2903],"valid"],[[2904,2907],"disallowed"],[[2908,2908],"mapped",[2849,2876]],[[2909,2909],"mapped",[2850,2876]],[[2910,2910],"disallowed"],[[2911,2913],"valid"],[[2914,2915],"valid"],[[2916,2917],"disallowed"],[[2918,2927],"valid"],[[2928,2928],"valid",[],"NV8"],[[2929,2929],"valid"],[[2930,2935],"valid",[],"NV8"],[[2936,2945],"disallowed"],[[2946,2947],"valid"],[[2948,2948],"disallowed"],[[2949,2954],"valid"],[[2955,2957],"disallowed"],[[2958,2960],"valid"],[[2961,2961],"disallowed"],[[2962,2965],"valid"],[[2966,2968],"disallowed"],[[2969,2970],"valid"],[[2971,2971],"disallowed"],[[2972,2972],"valid"],[[2973,2973],"disallowed"],[[2974,2975],"valid"],[[2976,2978],"disallowed"],[[2979,2980],"valid"],[[2981,2983],"disallowed"],[[2984,2986],"valid"],[[2987,2989],"disallowed"],[[2990,2997],"valid"],[[2998,2998],"valid"],[[2999,3001],"valid"],[[3002,3005],"disallowed"],[[3006,3010],"valid"],[[3011,3013],"disallowed"],[[3014,3016],"valid"],[[3017,3017],"disallowed"],[[3018,3021],"valid"],[[3022,3023],"disallowed"],[[3024,3024],"valid"],[[3025,3030],"disallowed"],[[3031,3031],"valid"],[[3032,3045],"disallowed"],[[3046,3046],"valid"],[[3047,3055],"valid"],[[3056,3058],"valid",[],"NV8"],[[3059,3066],"valid",[],"NV8"],[[3067,3071],"disallowed"],[[3072,3072],"valid"],[[3073,3075],"valid"],[[3076,3076],"disallowed"],[[3077,3084],"valid"],[[3085,3085],"disallowed"],[[3086,3088],"valid"],[[3089,3089],"disallowed"],[[3090,3112],"valid"],[[3113,3113],"disallowed"],[[3114,3123],"valid"],[[3124,3124],"valid"],[[3125,3129],"valid"],[[3130,3132],"disallowed"],[[3133,3133],"valid"],[[3134,3140],"valid"],[[3141,3141],"disallowed"],[[3142,3144],"valid"],[[3145,3145],"disallowed"],[[3146,3149],"valid"],[[3150,3156],"disallowed"],[[3157,3158],"valid"],[[3159,3159],"disallowed"],[[3160,3161],"valid"],[[3162,3162],"valid"],[[3163,3167],"disallowed"],[[3168,3169],"valid"],[[3170,3171],"valid"],[[3172,3173],"disallowed"],[[3174,3183],"valid"],[[3184,3191],"disallowed"],[[3192,3199],"valid",[],"NV8"],[[3200,3200],"disallowed"],[[3201,3201],"valid"],[[3202,3203],"valid"],[[3204,3204],"disallowed"],[[3205,3212],"valid"],[[3213,3213],"disallowed"],[[3214,3216],"valid"],[[3217,3217],"disallowed"],[[3218,3240],"valid"],[[3241,3241],"disallowed"],[[3242,3251],"valid"],[[3252,3252],"disallowed"],[[3253,3257],"valid"],[[3258,3259],"disallowed"],[[3260,3261],"valid"],[[3262,3268],"valid"],[[3269,3269],"disallowed"],[[3270,3272],"valid"],[[3273,3273],"disallowed"],[[3274,3277],"valid"],[[3278,3284],"disallowed"],[[3285,3286],"valid"],[[3287,3293],"disallowed"],[[3294,3294],"valid"],[[3295,3295],"disallowed"],[[3296,3297],"valid"],[[3298,3299],"valid"],[[3300,3301],"disallowed"],[[3302,3311],"valid"],[[3312,3312],"disallowed"],[[3313,3314],"valid"],[[3315,3328],"disallowed"],[[3329,3329],"valid"],[[3330,3331],"valid"],[[3332,3332],"disallowed"],[[3333,3340],"valid"],[[3341,3341],"disallowed"],[[3342,3344],"valid"],[[3345,3345],"disallowed"],[[3346,3368],"valid"],[[3369,3369],"valid"],[[3370,3385],"valid"],[[3386,3386],"valid"],[[3387,3388],"disallowed"],[[3389,3389],"valid"],[[3390,3395],"valid"],[[3396,3396],"valid"],[[3397,3397],"disallowed"],[[3398,3400],"valid"],[[3401,3401],"disallowed"],[[3402,3405],"valid"],[[3406,3406],"valid"],[[3407,3414],"disallowed"],[[3415,3415],"valid"],[[3416,3422],"disallowed"],[[3423,3423],"valid"],[[3424,3425],"valid"],[[3426,3427],"valid"],[[3428,3429],"disallowed"],[[3430,3439],"valid"],[[3440,3445],"valid",[],"NV8"],[[3446,3448],"disallowed"],[[3449,3449],"valid",[],"NV8"],[[3450,3455],"valid"],[[3456,3457],"disallowed"],[[3458,3459],"valid"],[[3460,3460],"disallowed"],[[3461,3478],"valid"],[[3479,3481],"disallowed"],[[3482,3505],"valid"],[[3506,3506],"disallowed"],[[3507,3515],"valid"],[[3516,3516],"disallowed"],[[3517,3517],"valid"],[[3518,3519],"disallowed"],[[3520,3526],"valid"],[[3527,3529],"disallowed"],[[3530,3530],"valid"],[[3531,3534],"disallowed"],[[3535,3540],"valid"],[[3541,3541],"disallowed"],[[3542,3542],"valid"],[[3543,3543],"disallowed"],[[3544,3551],"valid"],[[3552,3557],"disallowed"],[[3558,3567],"valid"],[[3568,3569],"disallowed"],[[3570,3571],"valid"],[[3572,3572],"valid",[],"NV8"],[[3573,3584],"disallowed"],[[3585,3634],"valid"],[[3635,3635],"mapped",[3661,3634]],[[3636,3642],"valid"],[[3643,3646],"disallowed"],[[3647,3647],"valid",[],"NV8"],[[3648,3662],"valid"],[[3663,3663],"valid",[],"NV8"],[[3664,3673],"valid"],[[3674,3675],"valid",[],"NV8"],[[3676,3712],"disallowed"],[[3713,3714],"valid"],[[3715,3715],"disallowed"],[[3716,3716],"valid"],[[3717,3718],"disallowed"],[[3719,3720],"valid"],[[3721,3721],"disallowed"],[[3722,3722],"valid"],[[3723,3724],"disallowed"],[[3725,3725],"valid"],[[3726,3731],"disallowed"],[[3732,3735],"valid"],[[3736,3736],"disallowed"],[[3737,3743],"valid"],[[3744,3744],"disallowed"],[[3745,3747],"valid"],[[3748,3748],"disallowed"],[[3749,3749],"valid"],[[3750,3750],"disallowed"],[[3751,3751],"valid"],[[3752,3753],"disallowed"],[[3754,3755],"valid"],[[3756,3756],"disallowed"],[[3757,3762],"valid"],[[3763,3763],"mapped",[3789,3762]],[[3764,3769],"valid"],[[3770,3770],"disallowed"],[[3771,3773],"valid"],[[3774,3775],"disallowed"],[[3776,3780],"valid"],[[3781,3781],"disallowed"],[[3782,3782],"valid"],[[3783,3783],"disallowed"],[[3784,3789],"valid"],[[3790,3791],"disallowed"],[[3792,3801],"valid"],[[3802,3803],"disallowed"],[[3804,3804],"mapped",[3755,3737]],[[3805,3805],"mapped",[3755,3745]],[[3806,3807],"valid"],[[3808,3839],"disallowed"],[[3840,3840],"valid"],[[3841,3850],"valid",[],"NV8"],[[3851,3851],"valid"],[[3852,3852],"mapped",[3851]],[[3853,3863],"valid",[],"NV8"],[[3864,3865],"valid"],[[3866,3871],"valid",[],"NV8"],[[3872,3881],"valid"],[[3882,3892],"valid",[],"NV8"],[[3893,3893],"valid"],[[3894,3894],"valid",[],"NV8"],[[3895,3895],"valid"],[[3896,3896],"valid",[],"NV8"],[[3897,3897],"valid"],[[3898,3901],"valid",[],"NV8"],[[3902,3906],"valid"],[[3907,3907],"mapped",[3906,4023]],[[3908,3911],"valid"],[[3912,3912],"disallowed"],[[3913,3916],"valid"],[[3917,3917],"mapped",[3916,4023]],[[3918,3921],"valid"],[[3922,3922],"mapped",[3921,4023]],[[3923,3926],"valid"],[[3927,3927],"mapped",[3926,4023]],[[3928,3931],"valid"],[[3932,3932],"mapped",[3931,4023]],[[3933,3944],"valid"],[[3945,3945],"mapped",[3904,4021]],[[3946,3946],"valid"],[[3947,3948],"valid"],[[3949,3952],"disallowed"],[[3953,3954],"valid"],[[3955,3955],"mapped",[3953,3954]],[[3956,3956],"valid"],[[3957,3957],"mapped",[3953,3956]],[[3958,3958],"mapped",[4018,3968]],[[3959,3959],"mapped",[4018,3953,3968]],[[3960,3960],"mapped",[4019,3968]],[[3961,3961],"mapped",[4019,3953,3968]],[[3962,3968],"valid"],[[3969,3969],"mapped",[3953,3968]],[[3970,3972],"valid"],[[3973,3973],"valid",[],"NV8"],[[3974,3979],"valid"],[[3980,3983],"valid"],[[3984,3986],"valid"],[[3987,3987],"mapped",[3986,4023]],[[3988,3989],"valid"],[[3990,3990],"valid"],[[3991,3991],"valid"],[[3992,3992],"disallowed"],[[3993,3996],"valid"],[[3997,3997],"mapped",[3996,4023]],[[3998,4001],"valid"],[[4002,4002],"mapped",[4001,4023]],[[4003,4006],"valid"],[[4007,4007],"mapped",[4006,4023]],[[4008,4011],"valid"],[[4012,4012],"mapped",[4011,4023]],[[4013,4013],"valid"],[[4014,4016],"valid"],[[4017,4023],"valid"],[[4024,4024],"valid"],[[4025,4025],"mapped",[3984,4021]],[[4026,4028],"valid"],[[4029,4029],"disallowed"],[[4030,4037],"valid",[],"NV8"],[[4038,4038],"valid"],[[4039,4044],"valid",[],"NV8"],[[4045,4045],"disallowed"],[[4046,4046],"valid",[],"NV8"],[[4047,4047],"valid",[],"NV8"],[[4048,4049],"valid",[],"NV8"],[[4050,4052],"valid",[],"NV8"],[[4053,4056],"valid",[],"NV8"],[[4057,4058],"valid",[],"NV8"],[[4059,4095],"disallowed"],[[4096,4129],"valid"],[[4130,4130],"valid"],[[4131,4135],"valid"],[[4136,4136],"valid"],[[4137,4138],"valid"],[[4139,4139],"valid"],[[4140,4146],"valid"],[[4147,4149],"valid"],[[4150,4153],"valid"],[[4154,4159],"valid"],[[4160,4169],"valid"],[[4170,4175],"valid",[],"NV8"],[[4176,4185],"valid"],[[4186,4249],"valid"],[[4250,4253],"valid"],[[4254,4255],"valid",[],"NV8"],[[4256,4293],"disallowed"],[[4294,4294],"disallowed"],[[4295,4295],"mapped",[11559]],[[4296,4300],"disallowed"],[[4301,4301],"mapped",[11565]],[[4302,4303],"disallowed"],[[4304,4342],"valid"],[[4343,4344],"valid"],[[4345,4346],"valid"],[[4347,4347],"valid",[],"NV8"],[[4348,4348],"mapped",[4316]],[[4349,4351],"valid"],[[4352,4441],"valid",[],"NV8"],[[4442,4446],"valid",[],"NV8"],[[4447,4448],"disallowed"],[[4449,4514],"valid",[],"NV8"],[[4515,4519],"valid",[],"NV8"],[[4520,4601],"valid",[],"NV8"],[[4602,4607],"valid",[],"NV8"],[[4608,4614],"valid"],[[4615,4615],"valid"],[[4616,4678],"valid"],[[4679,4679],"valid"],[[4680,4680],"valid"],[[4681,4681],"disallowed"],[[4682,4685],"valid"],[[4686,4687],"disallowed"],[[4688,4694],"valid"],[[4695,4695],"disallowed"],[[4696,4696],"valid"],[[4697,4697],"disallowed"],[[4698,4701],"valid"],[[4702,4703],"disallowed"],[[4704,4742],"valid"],[[4743,4743],"valid"],[[4744,4744],"valid"],[[4745,4745],"disallowed"],[[4746,4749],"valid"],[[4750,4751],"disallowed"],[[4752,4782],"valid"],[[4783,4783],"valid"],[[4784,4784],"valid"],[[4785,4785],"disallowed"],[[4786,4789],"valid"],[[4790,4791],"disallowed"],[[4792,4798],"valid"],[[4799,4799],"disallowed"],[[4800,4800],"valid"],[[4801,4801],"disallowed"],[[4802,4805],"valid"],[[4806,4807],"disallowed"],[[4808,4814],"valid"],[[4815,4815],"valid"],[[4816,4822],"valid"],[[4823,4823],"disallowed"],[[4824,4846],"valid"],[[4847,4847],"valid"],[[4848,4878],"valid"],[[4879,4879],"valid"],[[4880,4880],"valid"],[[4881,4881],"disallowed"],[[4882,4885],"valid"],[[4886,4887],"disallowed"],[[4888,4894],"valid"],[[4895,4895],"valid"],[[4896,4934],"valid"],[[4935,4935],"valid"],[[4936,4954],"valid"],[[4955,4956],"disallowed"],[[4957,4958],"valid"],[[4959,4959],"valid"],[[4960,4960],"valid",[],"NV8"],[[4961,4988],"valid",[],"NV8"],[[4989,4991],"disallowed"],[[4992,5007],"valid"],[[5008,5017],"valid",[],"NV8"],[[5018,5023],"disallowed"],[[5024,5108],"valid"],[[5109,5109],"valid"],[[5110,5111],"disallowed"],[[5112,5112],"mapped",[5104]],[[5113,5113],"mapped",[5105]],[[5114,5114],"mapped",[5106]],[[5115,5115],"mapped",[5107]],[[5116,5116],"mapped",[5108]],[[5117,5117],"mapped",[5109]],[[5118,5119],"disallowed"],[[5120,5120],"valid",[],"NV8"],[[5121,5740],"valid"],[[5741,5742],"valid",[],"NV8"],[[5743,5750],"valid"],[[5751,5759],"valid"],[[5760,5760],"disallowed"],[[5761,5786],"valid"],[[5787,5788],"valid",[],"NV8"],[[5789,5791],"disallowed"],[[5792,5866],"valid"],[[5867,5872],"valid",[],"NV8"],[[5873,5880],"valid"],[[5881,5887],"disallowed"],[[5888,5900],"valid"],[[5901,5901],"disallowed"],[[5902,5908],"valid"],[[5909,5919],"disallowed"],[[5920,5940],"valid"],[[5941,5942],"valid",[],"NV8"],[[5943,5951],"disallowed"],[[5952,5971],"valid"],[[5972,5983],"disallowed"],[[5984,5996],"valid"],[[5997,5997],"disallowed"],[[5998,6000],"valid"],[[6001,6001],"disallowed"],[[6002,6003],"valid"],[[6004,6015],"disallowed"],[[6016,6067],"valid"],[[6068,6069],"disallowed"],[[6070,6099],"valid"],[[6100,6102],"valid",[],"NV8"],[[6103,6103],"valid"],[[6104,6107],"valid",[],"NV8"],[[6108,6108],"valid"],[[6109,6109],"valid"],[[6110,6111],"disallowed"],[[6112,6121],"valid"],[[6122,6127],"disallowed"],[[6128,6137],"valid",[],"NV8"],[[6138,6143],"disallowed"],[[6144,6149],"valid",[],"NV8"],[[6150,6150],"disallowed"],[[6151,6154],"valid",[],"NV8"],[[6155,6157],"ignored"],[[6158,6158],"disallowed"],[[6159,6159],"disallowed"],[[6160,6169],"valid"],[[6170,6175],"disallowed"],[[6176,6263],"valid"],[[6264,6271],"disallowed"],[[6272,6313],"valid"],[[6314,6314],"valid"],[[6315,6319],"disallowed"],[[6320,6389],"valid"],[[6390,6399],"disallowed"],[[6400,6428],"valid"],[[6429,6430],"valid"],[[6431,6431],"disallowed"],[[6432,6443],"valid"],[[6444,6447],"disallowed"],[[6448,6459],"valid"],[[6460,6463],"disallowed"],[[6464,6464],"valid",[],"NV8"],[[6465,6467],"disallowed"],[[6468,6469],"valid",[],"NV8"],[[6470,6509],"valid"],[[6510,6511],"disallowed"],[[6512,6516],"valid"],[[6517,6527],"disallowed"],[[6528,6569],"valid"],[[6570,6571],"valid"],[[6572,6575],"disallowed"],[[6576,6601],"valid"],[[6602,6607],"disallowed"],[[6608,6617],"valid"],[[6618,6618],"valid",[],"XV8"],[[6619,6621],"disallowed"],[[6622,6623],"valid",[],"NV8"],[[6624,6655],"valid",[],"NV8"],[[6656,6683],"valid"],[[6684,6685],"disallowed"],[[6686,6687],"valid",[],"NV8"],[[6688,6750],"valid"],[[6751,6751],"disallowed"],[[6752,6780],"valid"],[[6781,6782],"disallowed"],[[6783,6793],"valid"],[[6794,6799],"disallowed"],[[6800,6809],"valid"],[[6810,6815],"disallowed"],[[6816,6822],"valid",[],"NV8"],[[6823,6823],"valid"],[[6824,6829],"valid",[],"NV8"],[[6830,6831],"disallowed"],[[6832,6845],"valid"],[[6846,6846],"valid",[],"NV8"],[[6847,6911],"disallowed"],[[6912,6987],"valid"],[[6988,6991],"disallowed"],[[6992,7001],"valid"],[[7002,7018],"valid",[],"NV8"],[[7019,7027],"valid"],[[7028,7036],"valid",[],"NV8"],[[7037,7039],"disallowed"],[[7040,7082],"valid"],[[7083,7085],"valid"],[[7086,7097],"valid"],[[7098,7103],"valid"],[[7104,7155],"valid"],[[7156,7163],"disallowed"],[[7164,7167],"valid",[],"NV8"],[[7168,7223],"valid"],[[7224,7226],"disallowed"],[[7227,7231],"valid",[],"NV8"],[[7232,7241],"valid"],[[7242,7244],"disallowed"],[[7245,7293],"valid"],[[7294,7295],"valid",[],"NV8"],[[7296,7359],"disallowed"],[[7360,7367],"valid",[],"NV8"],[[7368,7375],"disallowed"],[[7376,7378],"valid"],[[7379,7379],"valid",[],"NV8"],[[7380,7410],"valid"],[[7411,7414],"valid"],[[7415,7415],"disallowed"],[[7416,7417],"valid"],[[7418,7423],"disallowed"],[[7424,7467],"valid"],[[7468,7468],"mapped",[97]],[[7469,7469],"mapped",[230]],[[7470,7470],"mapped",[98]],[[7471,7471],"valid"],[[7472,7472],"mapped",[100]],[[7473,7473],"mapped",[101]],[[7474,7474],"mapped",[477]],[[7475,7475],"mapped",[103]],[[7476,7476],"mapped",[104]],[[7477,7477],"mapped",[105]],[[7478,7478],"mapped",[106]],[[7479,7479],"mapped",[107]],[[7480,7480],"mapped",[108]],[[7481,7481],"mapped",[109]],[[7482,7482],"mapped",[110]],[[7483,7483],"valid"],[[7484,7484],"mapped",[111]],[[7485,7485],"mapped",[547]],[[7486,7486],"mapped",[112]],[[7487,7487],"mapped",[114]],[[7488,7488],"mapped",[116]],[[7489,7489],"mapped",[117]],[[7490,7490],"mapped",[119]],[[7491,7491],"mapped",[97]],[[7492,7492],"mapped",[592]],[[7493,7493],"mapped",[593]],[[7494,7494],"mapped",[7426]],[[7495,7495],"mapped",[98]],[[7496,7496],"mapped",[100]],[[7497,7497],"mapped",[101]],[[7498,7498],"mapped",[601]],[[7499,7499],"mapped",[603]],[[7500,7500],"mapped",[604]],[[7501,7501],"mapped",[103]],[[7502,7502],"valid"],[[7503,7503],"mapped",[107]],[[7504,7504],"mapped",[109]],[[7505,7505],"mapped",[331]],[[7506,7506],"mapped",[111]],[[7507,7507],"mapped",[596]],[[7508,7508],"mapped",[7446]],[[7509,7509],"mapped",[7447]],[[7510,7510],"mapped",[112]],[[7511,7511],"mapped",[116]],[[7512,7512],"mapped",[117]],[[7513,7513],"mapped",[7453]],[[7514,7514],"mapped",[623]],[[7515,7515],"mapped",[118]],[[7516,7516],"mapped",[7461]],[[7517,7517],"mapped",[946]],[[7518,7518],"mapped",[947]],[[7519,7519],"mapped",[948]],[[7520,7520],"mapped",[966]],[[7521,7521],"mapped",[967]],[[7522,7522],"mapped",[105]],[[7523,7523],"mapped",[114]],[[7524,7524],"mapped",[117]],[[7525,7525],"mapped",[118]],[[7526,7526],"mapped",[946]],[[7527,7527],"mapped",[947]],[[7528,7528],"mapped",[961]],[[7529,7529],"mapped",[966]],[[7530,7530],"mapped",[967]],[[7531,7531],"valid"],[[7532,7543],"valid"],[[7544,7544],"mapped",[1085]],[[7545,7578],"valid"],[[7579,7579],"mapped",[594]],[[7580,7580],"mapped",[99]],[[7581,7581],"mapped",[597]],[[7582,7582],"mapped",[240]],[[7583,7583],"mapped",[604]],[[7584,7584],"mapped",[102]],[[7585,7585],"mapped",[607]],[[7586,7586],"mapped",[609]],[[7587,7587],"mapped",[613]],[[7588,7588],"mapped",[616]],[[7589,7589],"mapped",[617]],[[7590,7590],"mapped",[618]],[[7591,7591],"mapped",[7547]],[[7592,7592],"mapped",[669]],[[7593,7593],"mapped",[621]],[[7594,7594],"mapped",[7557]],[[7595,7595],"mapped",[671]],[[7596,7596],"mapped",[625]],[[7597,7597],"mapped",[624]],[[7598,7598],"mapped",[626]],[[7599,7599],"mapped",[627]],[[7600,7600],"mapped",[628]],[[7601,7601],"mapped",[629]],[[7602,7602],"mapped",[632]],[[7603,7603],"mapped",[642]],[[7604,7604],"mapped",[643]],[[7605,7605],"mapped",[427]],[[7606,7606],"mapped",[649]],[[7607,7607],"mapped",[650]],[[7608,7608],"mapped",[7452]],[[7609,7609],"mapped",[651]],[[7610,7610],"mapped",[652]],[[7611,7611],"mapped",[122]],[[7612,7612],"mapped",[656]],[[7613,7613],"mapped",[657]],[[7614,7614],"mapped",[658]],[[7615,7615],"mapped",[952]],[[7616,7619],"valid"],[[7620,7626],"valid"],[[7627,7654],"valid"],[[7655,7669],"valid"],[[7670,7675],"disallowed"],[[7676,7676],"valid"],[[7677,7677],"valid"],[[7678,7679],"valid"],[[7680,7680],"mapped",[7681]],[[7681,7681],"valid"],[[7682,7682],"mapped",[7683]],[[7683,7683],"valid"],[[7684,7684],"mapped",[7685]],[[7685,7685],"valid"],[[7686,7686],"mapped",[7687]],[[7687,7687],"valid"],[[7688,7688],"mapped",[7689]],[[7689,7689],"valid"],[[7690,7690],"mapped",[7691]],[[7691,7691],"valid"],[[7692,7692],"mapped",[7693]],[[7693,7693],"valid"],[[7694,7694],"mapped",[7695]],[[7695,7695],"valid"],[[7696,7696],"mapped",[7697]],[[7697,7697],"valid"],[[7698,7698],"mapped",[7699]],[[7699,7699],"valid"],[[7700,7700],"mapped",[7701]],[[7701,7701],"valid"],[[7702,7702],"mapped",[7703]],[[7703,7703],"valid"],[[7704,7704],"mapped",[7705]],[[7705,7705],"valid"],[[7706,7706],"mapped",[7707]],[[7707,7707],"valid"],[[7708,7708],"mapped",[7709]],[[7709,7709],"valid"],[[7710,7710],"mapped",[7711]],[[7711,7711],"valid"],[[7712,7712],"mapped",[7713]],[[7713,7713],"valid"],[[7714,7714],"mapped",[7715]],[[7715,7715],"valid"],[[7716,7716],"mapped",[7717]],[[7717,7717],"valid"],[[7718,7718],"mapped",[7719]],[[7719,7719],"valid"],[[7720,7720],"mapped",[7721]],[[7721,7721],"valid"],[[7722,7722],"mapped",[7723]],[[7723,7723],"valid"],[[7724,7724],"mapped",[7725]],[[7725,7725],"valid"],[[7726,7726],"mapped",[7727]],[[7727,7727],"valid"],[[7728,7728],"mapped",[7729]],[[7729,7729],"valid"],[[7730,7730],"mapped",[7731]],[[7731,7731],"valid"],[[7732,7732],"mapped",[7733]],[[7733,7733],"valid"],[[7734,7734],"mapped",[7735]],[[7735,7735],"valid"],[[7736,7736],"mapped",[7737]],[[7737,7737],"valid"],[[7738,7738],"mapped",[7739]],[[7739,7739],"valid"],[[7740,7740],"mapped",[7741]],[[7741,7741],"valid"],[[7742,7742],"mapped",[7743]],[[7743,7743],"valid"],[[7744,7744],"mapped",[7745]],[[7745,7745],"valid"],[[7746,7746],"mapped",[7747]],[[7747,7747],"valid"],[[7748,7748],"mapped",[7749]],[[7749,7749],"valid"],[[7750,7750],"mapped",[7751]],[[7751,7751],"valid"],[[7752,7752],"mapped",[7753]],[[7753,7753],"valid"],[[7754,7754],"mapped",[7755]],[[7755,7755],"valid"],[[7756,7756],"mapped",[7757]],[[7757,7757],"valid"],[[7758,7758],"mapped",[7759]],[[7759,7759],"valid"],[[7760,7760],"mapped",[7761]],[[7761,7761],"valid"],[[7762,7762],"mapped",[7763]],[[7763,7763],"valid"],[[7764,7764],"mapped",[7765]],[[7765,7765],"valid"],[[7766,7766],"mapped",[7767]],[[7767,7767],"valid"],[[7768,7768],"mapped",[7769]],[[7769,7769],"valid"],[[7770,7770],"mapped",[7771]],[[7771,7771],"valid"],[[7772,7772],"mapped",[7773]],[[7773,7773],"valid"],[[7774,7774],"mapped",[7775]],[[7775,7775],"valid"],[[7776,7776],"mapped",[7777]],[[7777,7777],"valid"],[[7778,7778],"mapped",[7779]],[[7779,7779],"valid"],[[7780,7780],"mapped",[7781]],[[7781,7781],"valid"],[[7782,7782],"mapped",[7783]],[[7783,7783],"valid"],[[7784,7784],"mapped",[7785]],[[7785,7785],"valid"],[[7786,7786],"mapped",[7787]],[[7787,7787],"valid"],[[7788,7788],"mapped",[7789]],[[7789,7789],"valid"],[[7790,7790],"mapped",[7791]],[[7791,7791],"valid"],[[7792,7792],"mapped",[7793]],[[7793,7793],"valid"],[[7794,7794],"mapped",[7795]],[[7795,7795],"valid"],[[7796,7796],"mapped",[7797]],[[7797,7797],"valid"],[[7798,7798],"mapped",[7799]],[[7799,7799],"valid"],[[7800,7800],"mapped",[7801]],[[7801,7801],"valid"],[[7802,7802],"mapped",[7803]],[[7803,7803],"valid"],[[7804,7804],"mapped",[7805]],[[7805,7805],"valid"],[[7806,7806],"mapped",[7807]],[[7807,7807],"valid"],[[7808,7808],"mapped",[7809]],[[7809,7809],"valid"],[[7810,7810],"mapped",[7811]],[[7811,7811],"valid"],[[7812,7812],"mapped",[7813]],[[7813,7813],"valid"],[[7814,7814],"mapped",[7815]],[[7815,7815],"valid"],[[7816,7816],"mapped",[7817]],[[7817,7817],"valid"],[[7818,7818],"mapped",[7819]],[[7819,7819],"valid"],[[7820,7820],"mapped",[7821]],[[7821,7821],"valid"],[[7822,7822],"mapped",[7823]],[[7823,7823],"valid"],[[7824,7824],"mapped",[7825]],[[7825,7825],"valid"],[[7826,7826],"mapped",[7827]],[[7827,7827],"valid"],[[7828,7828],"mapped",[7829]],[[7829,7833],"valid"],[[7834,7834],"mapped",[97,702]],[[7835,7835],"mapped",[7777]],[[7836,7837],"valid"],[[7838,7838],"mapped",[115,115]],[[7839,7839],"valid"],[[7840,7840],"mapped",[7841]],[[7841,7841],"valid"],[[7842,7842],"mapped",[7843]],[[7843,7843],"valid"],[[7844,7844],"mapped",[7845]],[[7845,7845],"valid"],[[7846,7846],"mapped",[7847]],[[7847,7847],"valid"],[[7848,7848],"mapped",[7849]],[[7849,7849],"valid"],[[7850,7850],"mapped",[7851]],[[7851,7851],"valid"],[[7852,7852],"mapped",[7853]],[[7853,7853],"valid"],[[7854,7854],"mapped",[7855]],[[7855,7855],"valid"],[[7856,7856],"mapped",[7857]],[[7857,7857],"valid"],[[7858,7858],"mapped",[7859]],[[7859,7859],"valid"],[[7860,7860],"mapped",[7861]],[[7861,7861],"valid"],[[7862,7862],"mapped",[7863]],[[7863,7863],"valid"],[[7864,7864],"mapped",[7865]],[[7865,7865],"valid"],[[7866,7866],"mapped",[7867]],[[7867,7867],"valid"],[[7868,7868],"mapped",[7869]],[[7869,7869],"valid"],[[7870,7870],"mapped",[7871]],[[7871,7871],"valid"],[[7872,7872],"mapped",[7873]],[[7873,7873],"valid"],[[7874,7874],"mapped",[7875]],[[7875,7875],"valid"],[[7876,7876],"mapped",[7877]],[[7877,7877],"valid"],[[7878,7878],"mapped",[7879]],[[7879,7879],"valid"],[[7880,7880],"mapped",[7881]],[[7881,7881],"valid"],[[7882,7882],"mapped",[7883]],[[7883,7883],"valid"],[[7884,7884],"mapped",[7885]],[[7885,7885],"valid"],[[7886,7886],"mapped",[7887]],[[7887,7887],"valid"],[[7888,7888],"mapped",[7889]],[[7889,7889],"valid"],[[7890,7890],"mapped",[7891]],[[7891,7891],"valid"],[[7892,7892],"mapped",[7893]],[[7893,7893],"valid"],[[7894,7894],"mapped",[7895]],[[7895,7895],"valid"],[[7896,7896],"mapped",[7897]],[[7897,7897],"valid"],[[7898,7898],"mapped",[7899]],[[7899,7899],"valid"],[[7900,7900],"mapped",[7901]],[[7901,7901],"valid"],[[7902,7902],"mapped",[7903]],[[7903,7903],"valid"],[[7904,7904],"mapped",[7905]],[[7905,7905],"valid"],[[7906,7906],"mapped",[7907]],[[7907,7907],"valid"],[[7908,7908],"mapped",[7909]],[[7909,7909],"valid"],[[7910,7910],"mapped",[7911]],[[7911,7911],"valid"],[[7912,7912],"mapped",[7913]],[[7913,7913],"valid"],[[7914,7914],"mapped",[7915]],[[7915,7915],"valid"],[[7916,7916],"mapped",[7917]],[[7917,7917],"valid"],[[7918,7918],"mapped",[7919]],[[7919,7919],"valid"],[[7920,7920],"mapped",[7921]],[[7921,7921],"valid"],[[7922,7922],"mapped",[7923]],[[7923,7923],"valid"],[[7924,7924],"mapped",[7925]],[[7925,7925],"valid"],[[7926,7926],"mapped",[7927]],[[7927,7927],"valid"],[[7928,7928],"mapped",[7929]],[[7929,7929],"valid"],[[7930,7930],"mapped",[7931]],[[7931,7931],"valid"],[[7932,7932],"mapped",[7933]],[[7933,7933],"valid"],[[7934,7934],"mapped",[7935]],[[7935,7935],"valid"],[[7936,7943],"valid"],[[7944,7944],"mapped",[7936]],[[7945,7945],"mapped",[7937]],[[7946,7946],"mapped",[7938]],[[7947,7947],"mapped",[7939]],[[7948,7948],"mapped",[7940]],[[7949,7949],"mapped",[7941]],[[7950,7950],"mapped",[7942]],[[7951,7951],"mapped",[7943]],[[7952,7957],"valid"],[[7958,7959],"disallowed"],[[7960,7960],"mapped",[7952]],[[7961,7961],"mapped",[7953]],[[7962,7962],"mapped",[7954]],[[7963,7963],"mapped",[7955]],[[7964,7964],"mapped",[7956]],[[7965,7965],"mapped",[7957]],[[7966,7967],"disallowed"],[[7968,7975],"valid"],[[7976,7976],"mapped",[7968]],[[7977,7977],"mapped",[7969]],[[7978,7978],"mapped",[7970]],[[7979,7979],"mapped",[7971]],[[7980,7980],"mapped",[7972]],[[7981,7981],"mapped",[7973]],[[7982,7982],"mapped",[7974]],[[7983,7983],"mapped",[7975]],[[7984,7991],"valid"],[[7992,7992],"mapped",[7984]],[[7993,7993],"mapped",[7985]],[[7994,7994],"mapped",[7986]],[[7995,7995],"mapped",[7987]],[[7996,7996],"mapped",[7988]],[[7997,7997],"mapped",[7989]],[[7998,7998],"mapped",[7990]],[[7999,7999],"mapped",[7991]],[[8000,8005],"valid"],[[8006,8007],"disallowed"],[[8008,8008],"mapped",[8000]],[[8009,8009],"mapped",[8001]],[[8010,8010],"mapped",[8002]],[[8011,8011],"mapped",[8003]],[[8012,8012],"mapped",[8004]],[[8013,8013],"mapped",[8005]],[[8014,8015],"disallowed"],[[8016,8023],"valid"],[[8024,8024],"disallowed"],[[8025,8025],"mapped",[8017]],[[8026,8026],"disallowed"],[[8027,8027],"mapped",[8019]],[[8028,8028],"disallowed"],[[8029,8029],"mapped",[8021]],[[8030,8030],"disallowed"],[[8031,8031],"mapped",[8023]],[[8032,8039],"valid"],[[8040,8040],"mapped",[8032]],[[8041,8041],"mapped",[8033]],[[8042,8042],"mapped",[8034]],[[8043,8043],"mapped",[8035]],[[8044,8044],"mapped",[8036]],[[8045,8045],"mapped",[8037]],[[8046,8046],"mapped",[8038]],[[8047,8047],"mapped",[8039]],[[8048,8048],"valid"],[[8049,8049],"mapped",[940]],[[8050,8050],"valid"],[[8051,8051],"mapped",[941]],[[8052,8052],"valid"],[[8053,8053],"mapped",[942]],[[8054,8054],"valid"],[[8055,8055],"mapped",[943]],[[8056,8056],"valid"],[[8057,8057],"mapped",[972]],[[8058,8058],"valid"],[[8059,8059],"mapped",[973]],[[8060,8060],"valid"],[[8061,8061],"mapped",[974]],[[8062,8063],"disallowed"],[[8064,8064],"mapped",[7936,953]],[[8065,8065],"mapped",[7937,953]],[[8066,8066],"mapped",[7938,953]],[[8067,8067],"mapped",[7939,953]],[[8068,8068],"mapped",[7940,953]],[[8069,8069],"mapped",[7941,953]],[[8070,8070],"mapped",[7942,953]],[[8071,8071],"mapped",[7943,953]],[[8072,8072],"mapped",[7936,953]],[[8073,8073],"mapped",[7937,953]],[[8074,8074],"mapped",[7938,953]],[[8075,8075],"mapped",[7939,953]],[[8076,8076],"mapped",[7940,953]],[[8077,8077],"mapped",[7941,953]],[[8078,8078],"mapped",[7942,953]],[[8079,8079],"mapped",[7943,953]],[[8080,8080],"mapped",[7968,953]],[[8081,8081],"mapped",[7969,953]],[[8082,8082],"mapped",[7970,953]],[[8083,8083],"mapped",[7971,953]],[[8084,8084],"mapped",[7972,953]],[[8085,8085],"mapped",[7973,953]],[[8086,8086],"mapped",[7974,953]],[[8087,8087],"mapped",[7975,953]],[[8088,8088],"mapped",[7968,953]],[[8089,8089],"mapped",[7969,953]],[[8090,8090],"mapped",[7970,953]],[[8091,8091],"mapped",[7971,953]],[[8092,8092],"mapped",[7972,953]],[[8093,8093],"mapped",[7973,953]],[[8094,8094],"mapped",[7974,953]],[[8095,8095],"mapped",[7975,953]],[[8096,8096],"mapped",[8032,953]],[[8097,8097],"mapped",[8033,953]],[[8098,8098],"mapped",[8034,953]],[[8099,8099],"mapped",[8035,953]],[[8100,8100],"mapped",[8036,953]],[[8101,8101],"mapped",[8037,953]],[[8102,8102],"mapped",[8038,953]],[[8103,8103],"mapped",[8039,953]],[[8104,8104],"mapped",[8032,953]],[[8105,8105],"mapped",[8033,953]],[[8106,8106],"mapped",[8034,953]],[[8107,8107],"mapped",[8035,953]],[[8108,8108],"mapped",[8036,953]],[[8109,8109],"mapped",[8037,953]],[[8110,8110],"mapped",[8038,953]],[[8111,8111],"mapped",[8039,953]],[[8112,8113],"valid"],[[8114,8114],"mapped",[8048,953]],[[8115,8115],"mapped",[945,953]],[[8116,8116],"mapped",[940,953]],[[8117,8117],"disallowed"],[[8118,8118],"valid"],[[8119,8119],"mapped",[8118,953]],[[8120,8120],"mapped",[8112]],[[8121,8121],"mapped",[8113]],[[8122,8122],"mapped",[8048]],[[8123,8123],"mapped",[940]],[[8124,8124],"mapped",[945,953]],[[8125,8125],"disallowed_STD3_mapped",[32,787]],[[8126,8126],"mapped",[953]],[[8127,8127],"disallowed_STD3_mapped",[32,787]],[[8128,8128],"disallowed_STD3_mapped",[32,834]],[[8129,8129],"disallowed_STD3_mapped",[32,776,834]],[[8130,8130],"mapped",[8052,953]],[[8131,8131],"mapped",[951,953]],[[8132,8132],"mapped",[942,953]],[[8133,8133],"disallowed"],[[8134,8134],"valid"],[[8135,8135],"mapped",[8134,953]],[[8136,8136],"mapped",[8050]],[[8137,8137],"mapped",[941]],[[8138,8138],"mapped",[8052]],[[8139,8139],"mapped",[942]],[[8140,8140],"mapped",[951,953]],[[8141,8141],"disallowed_STD3_mapped",[32,787,768]],[[8142,8142],"disallowed_STD3_mapped",[32,787,769]],[[8143,8143],"disallowed_STD3_mapped",[32,787,834]],[[8144,8146],"valid"],[[8147,8147],"mapped",[912]],[[8148,8149],"disallowed"],[[8150,8151],"valid"],[[8152,8152],"mapped",[8144]],[[8153,8153],"mapped",[8145]],[[8154,8154],"mapped",[8054]],[[8155,8155],"mapped",[943]],[[8156,8156],"disallowed"],[[8157,8157],"disallowed_STD3_mapped",[32,788,768]],[[8158,8158],"disallowed_STD3_mapped",[32,788,769]],[[8159,8159],"disallowed_STD3_mapped",[32,788,834]],[[8160,8162],"valid"],[[8163,8163],"mapped",[944]],[[8164,8167],"valid"],[[8168,8168],"mapped",[8160]],[[8169,8169],"mapped",[8161]],[[8170,8170],"mapped",[8058]],[[8171,8171],"mapped",[973]],[[8172,8172],"mapped",[8165]],[[8173,8173],"disallowed_STD3_mapped",[32,776,768]],[[8174,8174],"disallowed_STD3_mapped",[32,776,769]],[[8175,8175],"disallowed_STD3_mapped",[96]],[[8176,8177],"disallowed"],[[8178,8178],"mapped",[8060,953]],[[8179,8179],"mapped",[969,953]],[[8180,8180],"mapped",[974,953]],[[8181,8181],"disallowed"],[[8182,8182],"valid"],[[8183,8183],"mapped",[8182,953]],[[8184,8184],"mapped",[8056]],[[8185,8185],"mapped",[972]],[[8186,8186],"mapped",[8060]],[[8187,8187],"mapped",[974]],[[8188,8188],"mapped",[969,953]],[[8189,8189],"disallowed_STD3_mapped",[32,769]],[[8190,8190],"disallowed_STD3_mapped",[32,788]],[[8191,8191],"disallowed"],[[8192,8202],"disallowed_STD3_mapped",[32]],[[8203,8203],"ignored"],[[8204,8205],"deviation",[]],[[8206,8207],"disallowed"],[[8208,8208],"valid",[],"NV8"],[[8209,8209],"mapped",[8208]],[[8210,8214],"valid",[],"NV8"],[[8215,8215],"disallowed_STD3_mapped",[32,819]],[[8216,8227],"valid",[],"NV8"],[[8228,8230],"disallowed"],[[8231,8231],"valid",[],"NV8"],[[8232,8238],"disallowed"],[[8239,8239],"disallowed_STD3_mapped",[32]],[[8240,8242],"valid",[],"NV8"],[[8243,8243],"mapped",[8242,8242]],[[8244,8244],"mapped",[8242,8242,8242]],[[8245,8245],"valid",[],"NV8"],[[8246,8246],"mapped",[8245,8245]],[[8247,8247],"mapped",[8245,8245,8245]],[[8248,8251],"valid",[],"NV8"],[[8252,8252],"disallowed_STD3_mapped",[33,33]],[[8253,8253],"valid",[],"NV8"],[[8254,8254],"disallowed_STD3_mapped",[32,773]],[[8255,8262],"valid",[],"NV8"],[[8263,8263],"disallowed_STD3_mapped",[63,63]],[[8264,8264],"disallowed_STD3_mapped",[63,33]],[[8265,8265],"disallowed_STD3_mapped",[33,63]],[[8266,8269],"valid",[],"NV8"],[[8270,8274],"valid",[],"NV8"],[[8275,8276],"valid",[],"NV8"],[[8277,8278],"valid",[],"NV8"],[[8279,8279],"mapped",[8242,8242,8242,8242]],[[8280,8286],"valid",[],"NV8"],[[8287,8287],"disallowed_STD3_mapped",[32]],[[8288,8288],"ignored"],[[8289,8291],"disallowed"],[[8292,8292],"ignored"],[[8293,8293],"disallowed"],[[8294,8297],"disallowed"],[[8298,8303],"disallowed"],[[8304,8304],"mapped",[48]],[[8305,8305],"mapped",[105]],[[8306,8307],"disallowed"],[[8308,8308],"mapped",[52]],[[8309,8309],"mapped",[53]],[[8310,8310],"mapped",[54]],[[8311,8311],"mapped",[55]],[[8312,8312],"mapped",[56]],[[8313,8313],"mapped",[57]],[[8314,8314],"disallowed_STD3_mapped",[43]],[[8315,8315],"mapped",[8722]],[[8316,8316],"disallowed_STD3_mapped",[61]],[[8317,8317],"disallowed_STD3_mapped",[40]],[[8318,8318],"disallowed_STD3_mapped",[41]],[[8319,8319],"mapped",[110]],[[8320,8320],"mapped",[48]],[[8321,8321],"mapped",[49]],[[8322,8322],"mapped",[50]],[[8323,8323],"mapped",[51]],[[8324,8324],"mapped",[52]],[[8325,8325],"mapped",[53]],[[8326,8326],"mapped",[54]],[[8327,8327],"mapped",[55]],[[8328,8328],"mapped",[56]],[[8329,8329],"mapped",[57]],[[8330,8330],"disallowed_STD3_mapped",[43]],[[8331,8331],"mapped",[8722]],[[8332,8332],"disallowed_STD3_mapped",[61]],[[8333,8333],"disallowed_STD3_mapped",[40]],[[8334,8334],"disallowed_STD3_mapped",[41]],[[8335,8335],"disallowed"],[[8336,8336],"mapped",[97]],[[8337,8337],"mapped",[101]],[[8338,8338],"mapped",[111]],[[8339,8339],"mapped",[120]],[[8340,8340],"mapped",[601]],[[8341,8341],"mapped",[104]],[[8342,8342],"mapped",[107]],[[8343,8343],"mapped",[108]],[[8344,8344],"mapped",[109]],[[8345,8345],"mapped",[110]],[[8346,8346],"mapped",[112]],[[8347,8347],"mapped",[115]],[[8348,8348],"mapped",[116]],[[8349,8351],"disallowed"],[[8352,8359],"valid",[],"NV8"],[[8360,8360],"mapped",[114,115]],[[8361,8362],"valid",[],"NV8"],[[8363,8363],"valid",[],"NV8"],[[8364,8364],"valid",[],"NV8"],[[8365,8367],"valid",[],"NV8"],[[8368,8369],"valid",[],"NV8"],[[8370,8373],"valid",[],"NV8"],[[8374,8376],"valid",[],"NV8"],[[8377,8377],"valid",[],"NV8"],[[8378,8378],"valid",[],"NV8"],[[8379,8381],"valid",[],"NV8"],[[8382,8382],"valid",[],"NV8"],[[8383,8399],"disallowed"],[[8400,8417],"valid",[],"NV8"],[[8418,8419],"valid",[],"NV8"],[[8420,8426],"valid",[],"NV8"],[[8427,8427],"valid",[],"NV8"],[[8428,8431],"valid",[],"NV8"],[[8432,8432],"valid",[],"NV8"],[[8433,8447],"disallowed"],[[8448,8448],"disallowed_STD3_mapped",[97,47,99]],[[8449,8449],"disallowed_STD3_mapped",[97,47,115]],[[8450,8450],"mapped",[99]],[[8451,8451],"mapped",[176,99]],[[8452,8452],"valid",[],"NV8"],[[8453,8453],"disallowed_STD3_mapped",[99,47,111]],[[8454,8454],"disallowed_STD3_mapped",[99,47,117]],[[8455,8455],"mapped",[603]],[[8456,8456],"valid",[],"NV8"],[[8457,8457],"mapped",[176,102]],[[8458,8458],"mapped",[103]],[[8459,8462],"mapped",[104]],[[8463,8463],"mapped",[295]],[[8464,8465],"mapped",[105]],[[8466,8467],"mapped",[108]],[[8468,8468],"valid",[],"NV8"],[[8469,8469],"mapped",[110]],[[8470,8470],"mapped",[110,111]],[[8471,8472],"valid",[],"NV8"],[[8473,8473],"mapped",[112]],[[8474,8474],"mapped",[113]],[[8475,8477],"mapped",[114]],[[8478,8479],"valid",[],"NV8"],[[8480,8480],"mapped",[115,109]],[[8481,8481],"mapped",[116,101,108]],[[8482,8482],"mapped",[116,109]],[[8483,8483],"valid",[],"NV8"],[[8484,8484],"mapped",[122]],[[8485,8485],"valid",[],"NV8"],[[8486,8486],"mapped",[969]],[[8487,8487],"valid",[],"NV8"],[[8488,8488],"mapped",[122]],[[8489,8489],"valid",[],"NV8"],[[8490,8490],"mapped",[107]],[[8491,8491],"mapped",[229]],[[8492,8492],"mapped",[98]],[[8493,8493],"mapped",[99]],[[8494,8494],"valid",[],"NV8"],[[8495,8496],"mapped",[101]],[[8497,8497],"mapped",[102]],[[8498,8498],"disallowed"],[[8499,8499],"mapped",[109]],[[8500,8500],"mapped",[111]],[[8501,8501],"mapped",[1488]],[[8502,8502],"mapped",[1489]],[[8503,8503],"mapped",[1490]],[[8504,8504],"mapped",[1491]],[[8505,8505],"mapped",[105]],[[8506,8506],"valid",[],"NV8"],[[8507,8507],"mapped",[102,97,120]],[[8508,8508],"mapped",[960]],[[8509,8510],"mapped",[947]],[[8511,8511],"mapped",[960]],[[8512,8512],"mapped",[8721]],[[8513,8516],"valid",[],"NV8"],[[8517,8518],"mapped",[100]],[[8519,8519],"mapped",[101]],[[8520,8520],"mapped",[105]],[[8521,8521],"mapped",[106]],[[8522,8523],"valid",[],"NV8"],[[8524,8524],"valid",[],"NV8"],[[8525,8525],"valid",[],"NV8"],[[8526,8526],"valid"],[[8527,8527],"valid",[],"NV8"],[[8528,8528],"mapped",[49,8260,55]],[[8529,8529],"mapped",[49,8260,57]],[[8530,8530],"mapped",[49,8260,49,48]],[[8531,8531],"mapped",[49,8260,51]],[[8532,8532],"mapped",[50,8260,51]],[[8533,8533],"mapped",[49,8260,53]],[[8534,8534],"mapped",[50,8260,53]],[[8535,8535],"mapped",[51,8260,53]],[[8536,8536],"mapped",[52,8260,53]],[[8537,8537],"mapped",[49,8260,54]],[[8538,8538],"mapped",[53,8260,54]],[[8539,8539],"mapped",[49,8260,56]],[[8540,8540],"mapped",[51,8260,56]],[[8541,8541],"mapped",[53,8260,56]],[[8542,8542],"mapped",[55,8260,56]],[[8543,8543],"mapped",[49,8260]],[[8544,8544],"mapped",[105]],[[8545,8545],"mapped",[105,105]],[[8546,8546],"mapped",[105,105,105]],[[8547,8547],"mapped",[105,118]],[[8548,8548],"mapped",[118]],[[8549,8549],"mapped",[118,105]],[[8550,8550],"mapped",[118,105,105]],[[8551,8551],"mapped",[118,105,105,105]],[[8552,8552],"mapped",[105,120]],[[8553,8553],"mapped",[120]],[[8554,8554],"mapped",[120,105]],[[8555,8555],"mapped",[120,105,105]],[[8556,8556],"mapped",[108]],[[8557,8557],"mapped",[99]],[[8558,8558],"mapped",[100]],[[8559,8559],"mapped",[109]],[[8560,8560],"mapped",[105]],[[8561,8561],"mapped",[105,105]],[[8562,8562],"mapped",[105,105,105]],[[8563,8563],"mapped",[105,118]],[[8564,8564],"mapped",[118]],[[8565,8565],"mapped",[118,105]],[[8566,8566],"mapped",[118,105,105]],[[8567,8567],"mapped",[118,105,105,105]],[[8568,8568],"mapped",[105,120]],[[8569,8569],"mapped",[120]],[[8570,8570],"mapped",[120,105]],[[8571,8571],"mapped",[120,105,105]],[[8572,8572],"mapped",[108]],[[8573,8573],"mapped",[99]],[[8574,8574],"mapped",[100]],[[8575,8575],"mapped",[109]],[[8576,8578],"valid",[],"NV8"],[[8579,8579],"disallowed"],[[8580,8580],"valid"],[[8581,8584],"valid",[],"NV8"],[[8585,8585],"mapped",[48,8260,51]],[[8586,8587],"valid",[],"NV8"],[[8588,8591],"disallowed"],[[8592,8682],"valid",[],"NV8"],[[8683,8691],"valid",[],"NV8"],[[8692,8703],"valid",[],"NV8"],[[8704,8747],"valid",[],"NV8"],[[8748,8748],"mapped",[8747,8747]],[[8749,8749],"mapped",[8747,8747,8747]],[[8750,8750],"valid",[],"NV8"],[[8751,8751],"mapped",[8750,8750]],[[8752,8752],"mapped",[8750,8750,8750]],[[8753,8799],"valid",[],"NV8"],[[8800,8800],"disallowed_STD3_valid"],[[8801,8813],"valid",[],"NV8"],[[8814,8815],"disallowed_STD3_valid"],[[8816,8945],"valid",[],"NV8"],[[8946,8959],"valid",[],"NV8"],[[8960,8960],"valid",[],"NV8"],[[8961,8961],"valid",[],"NV8"],[[8962,9000],"valid",[],"NV8"],[[9001,9001],"mapped",[12296]],[[9002,9002],"mapped",[12297]],[[9003,9082],"valid",[],"NV8"],[[9083,9083],"valid",[],"NV8"],[[9084,9084],"valid",[],"NV8"],[[9085,9114],"valid",[],"NV8"],[[9115,9166],"valid",[],"NV8"],[[9167,9168],"valid",[],"NV8"],[[9169,9179],"valid",[],"NV8"],[[9180,9191],"valid",[],"NV8"],[[9192,9192],"valid",[],"NV8"],[[9193,9203],"valid",[],"NV8"],[[9204,9210],"valid",[],"NV8"],[[9211,9215],"disallowed"],[[9216,9252],"valid",[],"NV8"],[[9253,9254],"valid",[],"NV8"],[[9255,9279],"disallowed"],[[9280,9290],"valid",[],"NV8"],[[9291,9311],"disallowed"],[[9312,9312],"mapped",[49]],[[9313,9313],"mapped",[50]],[[9314,9314],"mapped",[51]],[[9315,9315],"mapped",[52]],[[9316,9316],"mapped",[53]],[[9317,9317],"mapped",[54]],[[9318,9318],"mapped",[55]],[[9319,9319],"mapped",[56]],[[9320,9320],"mapped",[57]],[[9321,9321],"mapped",[49,48]],[[9322,9322],"mapped",[49,49]],[[9323,9323],"mapped",[49,50]],[[9324,9324],"mapped",[49,51]],[[9325,9325],"mapped",[49,52]],[[9326,9326],"mapped",[49,53]],[[9327,9327],"mapped",[49,54]],[[9328,9328],"mapped",[49,55]],[[9329,9329],"mapped",[49,56]],[[9330,9330],"mapped",[49,57]],[[9331,9331],"mapped",[50,48]],[[9332,9332],"disallowed_STD3_mapped",[40,49,41]],[[9333,9333],"disallowed_STD3_mapped",[40,50,41]],[[9334,9334],"disallowed_STD3_mapped",[40,51,41]],[[9335,9335],"disallowed_STD3_mapped",[40,52,41]],[[9336,9336],"disallowed_STD3_mapped",[40,53,41]],[[9337,9337],"disallowed_STD3_mapped",[40,54,41]],[[9338,9338],"disallowed_STD3_mapped",[40,55,41]],[[9339,9339],"disallowed_STD3_mapped",[40,56,41]],[[9340,9340],"disallowed_STD3_mapped",[40,57,41]],[[9341,9341],"disallowed_STD3_mapped",[40,49,48,41]],[[9342,9342],"disallowed_STD3_mapped",[40,49,49,41]],[[9343,9343],"disallowed_STD3_mapped",[40,49,50,41]],[[9344,9344],"disallowed_STD3_mapped",[40,49,51,41]],[[9345,9345],"disallowed_STD3_mapped",[40,49,52,41]],[[9346,9346],"disallowed_STD3_mapped",[40,49,53,41]],[[9347,9347],"disallowed_STD3_mapped",[40,49,54,41]],[[9348,9348],"disallowed_STD3_mapped",[40,49,55,41]],[[9349,9349],"disallowed_STD3_mapped",[40,49,56,41]],[[9350,9350],"disallowed_STD3_mapped",[40,49,57,41]],[[9351,9351],"disallowed_STD3_mapped",[40,50,48,41]],[[9352,9371],"disallowed"],[[9372,9372],"disallowed_STD3_mapped",[40,97,41]],[[9373,9373],"disallowed_STD3_mapped",[40,98,41]],[[9374,9374],"disallowed_STD3_mapped",[40,99,41]],[[9375,9375],"disallowed_STD3_mapped",[40,100,41]],[[9376,9376],"disallowed_STD3_mapped",[40,101,41]],[[9377,9377],"disallowed_STD3_mapped",[40,102,41]],[[9378,9378],"disallowed_STD3_mapped",[40,103,41]],[[9379,9379],"disallowed_STD3_mapped",[40,104,41]],[[9380,9380],"disallowed_STD3_mapped",[40,105,41]],[[9381,9381],"disallowed_STD3_mapped",[40,106,41]],[[9382,9382],"disallowed_STD3_mapped",[40,107,41]],[[9383,9383],"disallowed_STD3_mapped",[40,108,41]],[[9384,9384],"disallowed_STD3_mapped",[40,109,41]],[[9385,9385],"disallowed_STD3_mapped",[40,110,41]],[[9386,9386],"disallowed_STD3_mapped",[40,111,41]],[[9387,9387],"disallowed_STD3_mapped",[40,112,41]],[[9388,9388],"disallowed_STD3_mapped",[40,113,41]],[[9389,9389],"disallowed_STD3_mapped",[40,114,41]],[[9390,9390],"disallowed_STD3_mapped",[40,115,41]],[[9391,9391],"disallowed_STD3_mapped",[40,116,41]],[[9392,9392],"disallowed_STD3_mapped",[40,117,41]],[[9393,9393],"disallowed_STD3_mapped",[40,118,41]],[[9394,9394],"disallowed_STD3_mapped",[40,119,41]],[[9395,9395],"disallowed_STD3_mapped",[40,120,41]],[[9396,9396],"disallowed_STD3_mapped",[40,121,41]],[[9397,9397],"disallowed_STD3_mapped",[40,122,41]],[[9398,9398],"mapped",[97]],[[9399,9399],"mapped",[98]],[[9400,9400],"mapped",[99]],[[9401,9401],"mapped",[100]],[[9402,9402],"mapped",[101]],[[9403,9403],"mapped",[102]],[[9404,9404],"mapped",[103]],[[9405,9405],"mapped",[104]],[[9406,9406],"mapped",[105]],[[9407,9407],"mapped",[106]],[[9408,9408],"mapped",[107]],[[9409,9409],"mapped",[108]],[[9410,9410],"mapped",[109]],[[9411,9411],"mapped",[110]],[[9412,9412],"mapped",[111]],[[9413,9413],"mapped",[112]],[[9414,9414],"mapped",[113]],[[9415,9415],"mapped",[114]],[[9416,9416],"mapped",[115]],[[9417,9417],"mapped",[116]],[[9418,9418],"mapped",[117]],[[9419,9419],"mapped",[118]],[[9420,9420],"mapped",[119]],[[9421,9421],"mapped",[120]],[[9422,9422],"mapped",[121]],[[9423,9423],"mapped",[122]],[[9424,9424],"mapped",[97]],[[9425,9425],"mapped",[98]],[[9426,9426],"mapped",[99]],[[9427,9427],"mapped",[100]],[[9428,9428],"mapped",[101]],[[9429,9429],"mapped",[102]],[[9430,9430],"mapped",[103]],[[9431,9431],"mapped",[104]],[[9432,9432],"mapped",[105]],[[9433,9433],"mapped",[106]],[[9434,9434],"mapped",[107]],[[9435,9435],"mapped",[108]],[[9436,9436],"mapped",[109]],[[9437,9437],"mapped",[110]],[[9438,9438],"mapped",[111]],[[9439,9439],"mapped",[112]],[[9440,9440],"mapped",[113]],[[9441,9441],"mapped",[114]],[[9442,9442],"mapped",[115]],[[9443,9443],"mapped",[116]],[[9444,9444],"mapped",[117]],[[9445,9445],"mapped",[118]],[[9446,9446],"mapped",[119]],[[9447,9447],"mapped",[120]],[[9448,9448],"mapped",[121]],[[9449,9449],"mapped",[122]],[[9450,9450],"mapped",[48]],[[9451,9470],"valid",[],"NV8"],[[9471,9471],"valid",[],"NV8"],[[9472,9621],"valid",[],"NV8"],[[9622,9631],"valid",[],"NV8"],[[9632,9711],"valid",[],"NV8"],[[9712,9719],"valid",[],"NV8"],[[9720,9727],"valid",[],"NV8"],[[9728,9747],"valid",[],"NV8"],[[9748,9749],"valid",[],"NV8"],[[9750,9751],"valid",[],"NV8"],[[9752,9752],"valid",[],"NV8"],[[9753,9753],"valid",[],"NV8"],[[9754,9839],"valid",[],"NV8"],[[9840,9841],"valid",[],"NV8"],[[9842,9853],"valid",[],"NV8"],[[9854,9855],"valid",[],"NV8"],[[9856,9865],"valid",[],"NV8"],[[9866,9873],"valid",[],"NV8"],[[9874,9884],"valid",[],"NV8"],[[9885,9885],"valid",[],"NV8"],[[9886,9887],"valid",[],"NV8"],[[9888,9889],"valid",[],"NV8"],[[9890,9905],"valid",[],"NV8"],[[9906,9906],"valid",[],"NV8"],[[9907,9916],"valid",[],"NV8"],[[9917,9919],"valid",[],"NV8"],[[9920,9923],"valid",[],"NV8"],[[9924,9933],"valid",[],"NV8"],[[9934,9934],"valid",[],"NV8"],[[9935,9953],"valid",[],"NV8"],[[9954,9954],"valid",[],"NV8"],[[9955,9955],"valid",[],"NV8"],[[9956,9959],"valid",[],"NV8"],[[9960,9983],"valid",[],"NV8"],[[9984,9984],"valid",[],"NV8"],[[9985,9988],"valid",[],"NV8"],[[9989,9989],"valid",[],"NV8"],[[9990,9993],"valid",[],"NV8"],[[9994,9995],"valid",[],"NV8"],[[9996,10023],"valid",[],"NV8"],[[10024,10024],"valid",[],"NV8"],[[10025,10059],"valid",[],"NV8"],[[10060,10060],"valid",[],"NV8"],[[10061,10061],"valid",[],"NV8"],[[10062,10062],"valid",[],"NV8"],[[10063,10066],"valid",[],"NV8"],[[10067,10069],"valid",[],"NV8"],[[10070,10070],"valid",[],"NV8"],[[10071,10071],"valid",[],"NV8"],[[10072,10078],"valid",[],"NV8"],[[10079,10080],"valid",[],"NV8"],[[10081,10087],"valid",[],"NV8"],[[10088,10101],"valid",[],"NV8"],[[10102,10132],"valid",[],"NV8"],[[10133,10135],"valid",[],"NV8"],[[10136,10159],"valid",[],"NV8"],[[10160,10160],"valid",[],"NV8"],[[10161,10174],"valid",[],"NV8"],[[10175,10175],"valid",[],"NV8"],[[10176,10182],"valid",[],"NV8"],[[10183,10186],"valid",[],"NV8"],[[10187,10187],"valid",[],"NV8"],[[10188,10188],"valid",[],"NV8"],[[10189,10189],"valid",[],"NV8"],[[10190,10191],"valid",[],"NV8"],[[10192,10219],"valid",[],"NV8"],[[10220,10223],"valid",[],"NV8"],[[10224,10239],"valid",[],"NV8"],[[10240,10495],"valid",[],"NV8"],[[10496,10763],"valid",[],"NV8"],[[10764,10764],"mapped",[8747,8747,8747,8747]],[[10765,10867],"valid",[],"NV8"],[[10868,10868],"disallowed_STD3_mapped",[58,58,61]],[[10869,10869],"disallowed_STD3_mapped",[61,61]],[[10870,10870],"disallowed_STD3_mapped",[61,61,61]],[[10871,10971],"valid",[],"NV8"],[[10972,10972],"mapped",[10973,824]],[[10973,11007],"valid",[],"NV8"],[[11008,11021],"valid",[],"NV8"],[[11022,11027],"valid",[],"NV8"],[[11028,11034],"valid",[],"NV8"],[[11035,11039],"valid",[],"NV8"],[[11040,11043],"valid",[],"NV8"],[[11044,11084],"valid",[],"NV8"],[[11085,11087],"valid",[],"NV8"],[[11088,11092],"valid",[],"NV8"],[[11093,11097],"valid",[],"NV8"],[[11098,11123],"valid",[],"NV8"],[[11124,11125],"disallowed"],[[11126,11157],"valid",[],"NV8"],[[11158,11159],"disallowed"],[[11160,11193],"valid",[],"NV8"],[[11194,11196],"disallowed"],[[11197,11208],"valid",[],"NV8"],[[11209,11209],"disallowed"],[[11210,11217],"valid",[],"NV8"],[[11218,11243],"disallowed"],[[11244,11247],"valid",[],"NV8"],[[11248,11263],"disallowed"],[[11264,11264],"mapped",[11312]],[[11265,11265],"mapped",[11313]],[[11266,11266],"mapped",[11314]],[[11267,11267],"mapped",[11315]],[[11268,11268],"mapped",[11316]],[[11269,11269],"mapped",[11317]],[[11270,11270],"mapped",[11318]],[[11271,11271],"mapped",[11319]],[[11272,11272],"mapped",[11320]],[[11273,11273],"mapped",[11321]],[[11274,11274],"mapped",[11322]],[[11275,11275],"mapped",[11323]],[[11276,11276],"mapped",[11324]],[[11277,11277],"mapped",[11325]],[[11278,11278],"mapped",[11326]],[[11279,11279],"mapped",[11327]],[[11280,11280],"mapped",[11328]],[[11281,11281],"mapped",[11329]],[[11282,11282],"mapped",[11330]],[[11283,11283],"mapped",[11331]],[[11284,11284],"mapped",[11332]],[[11285,11285],"mapped",[11333]],[[11286,11286],"mapped",[11334]],[[11287,11287],"mapped",[11335]],[[11288,11288],"mapped",[11336]],[[11289,11289],"mapped",[11337]],[[11290,11290],"mapped",[11338]],[[11291,11291],"mapped",[11339]],[[11292,11292],"mapped",[11340]],[[11293,11293],"mapped",[11341]],[[11294,11294],"mapped",[11342]],[[11295,11295],"mapped",[11343]],[[11296,11296],"mapped",[11344]],[[11297,11297],"mapped",[11345]],[[11298,11298],"mapped",[11346]],[[11299,11299],"mapped",[11347]],[[11300,11300],"mapped",[11348]],[[11301,11301],"mapped",[11349]],[[11302,11302],"mapped",[11350]],[[11303,11303],"mapped",[11351]],[[11304,11304],"mapped",[11352]],[[11305,11305],"mapped",[11353]],[[11306,11306],"mapped",[11354]],[[11307,11307],"mapped",[11355]],[[11308,11308],"mapped",[11356]],[[11309,11309],"mapped",[11357]],[[11310,11310],"mapped",[11358]],[[11311,11311],"disallowed"],[[11312,11358],"valid"],[[11359,11359],"disallowed"],[[11360,11360],"mapped",[11361]],[[11361,11361],"valid"],[[11362,11362],"mapped",[619]],[[11363,11363],"mapped",[7549]],[[11364,11364],"mapped",[637]],[[11365,11366],"valid"],[[11367,11367],"mapped",[11368]],[[11368,11368],"valid"],[[11369,11369],"mapped",[11370]],[[11370,11370],"valid"],[[11371,11371],"mapped",[11372]],[[11372,11372],"valid"],[[11373,11373],"mapped",[593]],[[11374,11374],"mapped",[625]],[[11375,11375],"mapped",[592]],[[11376,11376],"mapped",[594]],[[11377,11377],"valid"],[[11378,11378],"mapped",[11379]],[[11379,11379],"valid"],[[11380,11380],"valid"],[[11381,11381],"mapped",[11382]],[[11382,11383],"valid"],[[11384,11387],"valid"],[[11388,11388],"mapped",[106]],[[11389,11389],"mapped",[118]],[[11390,11390],"mapped",[575]],[[11391,11391],"mapped",[576]],[[11392,11392],"mapped",[11393]],[[11393,11393],"valid"],[[11394,11394],"mapped",[11395]],[[11395,11395],"valid"],[[11396,11396],"mapped",[11397]],[[11397,11397],"valid"],[[11398,11398],"mapped",[11399]],[[11399,11399],"valid"],[[11400,11400],"mapped",[11401]],[[11401,11401],"valid"],[[11402,11402],"mapped",[11403]],[[11403,11403],"valid"],[[11404,11404],"mapped",[11405]],[[11405,11405],"valid"],[[11406,11406],"mapped",[11407]],[[11407,11407],"valid"],[[11408,11408],"mapped",[11409]],[[11409,11409],"valid"],[[11410,11410],"mapped",[11411]],[[11411,11411],"valid"],[[11412,11412],"mapped",[11413]],[[11413,11413],"valid"],[[11414,11414],"mapped",[11415]],[[11415,11415],"valid"],[[11416,11416],"mapped",[11417]],[[11417,11417],"valid"],[[11418,11418],"mapped",[11419]],[[11419,11419],"valid"],[[11420,11420],"mapped",[11421]],[[11421,11421],"valid"],[[11422,11422],"mapped",[11423]],[[11423,11423],"valid"],[[11424,11424],"mapped",[11425]],[[11425,11425],"valid"],[[11426,11426],"mapped",[11427]],[[11427,11427],"valid"],[[11428,11428],"mapped",[11429]],[[11429,11429],"valid"],[[11430,11430],"mapped",[11431]],[[11431,11431],"valid"],[[11432,11432],"mapped",[11433]],[[11433,11433],"valid"],[[11434,11434],"mapped",[11435]],[[11435,11435],"valid"],[[11436,11436],"mapped",[11437]],[[11437,11437],"valid"],[[11438,11438],"mapped",[11439]],[[11439,11439],"valid"],[[11440,11440],"mapped",[11441]],[[11441,11441],"valid"],[[11442,11442],"mapped",[11443]],[[11443,11443],"valid"],[[11444,11444],"mapped",[11445]],[[11445,11445],"valid"],[[11446,11446],"mapped",[11447]],[[11447,11447],"valid"],[[11448,11448],"mapped",[11449]],[[11449,11449],"valid"],[[11450,11450],"mapped",[11451]],[[11451,11451],"valid"],[[11452,11452],"mapped",[11453]],[[11453,11453],"valid"],[[11454,11454],"mapped",[11455]],[[11455,11455],"valid"],[[11456,11456],"mapped",[11457]],[[11457,11457],"valid"],[[11458,11458],"mapped",[11459]],[[11459,11459],"valid"],[[11460,11460],"mapped",[11461]],[[11461,11461],"valid"],[[11462,11462],"mapped",[11463]],[[11463,11463],"valid"],[[11464,11464],"mapped",[11465]],[[11465,11465],"valid"],[[11466,11466],"mapped",[11467]],[[11467,11467],"valid"],[[11468,11468],"mapped",[11469]],[[11469,11469],"valid"],[[11470,11470],"mapped",[11471]],[[11471,11471],"valid"],[[11472,11472],"mapped",[11473]],[[11473,11473],"valid"],[[11474,11474],"mapped",[11475]],[[11475,11475],"valid"],[[11476,11476],"mapped",[11477]],[[11477,11477],"valid"],[[11478,11478],"mapped",[11479]],[[11479,11479],"valid"],[[11480,11480],"mapped",[11481]],[[11481,11481],"valid"],[[11482,11482],"mapped",[11483]],[[11483,11483],"valid"],[[11484,11484],"mapped",[11485]],[[11485,11485],"valid"],[[11486,11486],"mapped",[11487]],[[11487,11487],"valid"],[[11488,11488],"mapped",[11489]],[[11489,11489],"valid"],[[11490,11490],"mapped",[11491]],[[11491,11492],"valid"],[[11493,11498],"valid",[],"NV8"],[[11499,11499],"mapped",[11500]],[[11500,11500],"valid"],[[11501,11501],"mapped",[11502]],[[11502,11505],"valid"],[[11506,11506],"mapped",[11507]],[[11507,11507],"valid"],[[11508,11512],"disallowed"],[[11513,11519],"valid",[],"NV8"],[[11520,11557],"valid"],[[11558,11558],"disallowed"],[[11559,11559],"valid"],[[11560,11564],"disallowed"],[[11565,11565],"valid"],[[11566,11567],"disallowed"],[[11568,11621],"valid"],[[11622,11623],"valid"],[[11624,11630],"disallowed"],[[11631,11631],"mapped",[11617]],[[11632,11632],"valid",[],"NV8"],[[11633,11646],"disallowed"],[[11647,11647],"valid"],[[11648,11670],"valid"],[[11671,11679],"disallowed"],[[11680,11686],"valid"],[[11687,11687],"disallowed"],[[11688,11694],"valid"],[[11695,11695],"disallowed"],[[11696,11702],"valid"],[[11703,11703],"disallowed"],[[11704,11710],"valid"],[[11711,11711],"disallowed"],[[11712,11718],"valid"],[[11719,11719],"disallowed"],[[11720,11726],"valid"],[[11727,11727],"disallowed"],[[11728,11734],"valid"],[[11735,11735],"disallowed"],[[11736,11742],"valid"],[[11743,11743],"disallowed"],[[11744,11775],"valid"],[[11776,11799],"valid",[],"NV8"],[[11800,11803],"valid",[],"NV8"],[[11804,11805],"valid",[],"NV8"],[[11806,11822],"valid",[],"NV8"],[[11823,11823],"valid"],[[11824,11824],"valid",[],"NV8"],[[11825,11825],"valid",[],"NV8"],[[11826,11835],"valid",[],"NV8"],[[11836,11842],"valid",[],"NV8"],[[11843,11903],"disallowed"],[[11904,11929],"valid",[],"NV8"],[[11930,11930],"disallowed"],[[11931,11934],"valid",[],"NV8"],[[11935,11935],"mapped",[27597]],[[11936,12018],"valid",[],"NV8"],[[12019,12019],"mapped",[40863]],[[12020,12031],"disallowed"],[[12032,12032],"mapped",[19968]],[[12033,12033],"mapped",[20008]],[[12034,12034],"mapped",[20022]],[[12035,12035],"mapped",[20031]],[[12036,12036],"mapped",[20057]],[[12037,12037],"mapped",[20101]],[[12038,12038],"mapped",[20108]],[[12039,12039],"mapped",[20128]],[[12040,12040],"mapped",[20154]],[[12041,12041],"mapped",[20799]],[[12042,12042],"mapped",[20837]],[[12043,12043],"mapped",[20843]],[[12044,12044],"mapped",[20866]],[[12045,12045],"mapped",[20886]],[[12046,12046],"mapped",[20907]],[[12047,12047],"mapped",[20960]],[[12048,12048],"mapped",[20981]],[[12049,12049],"mapped",[20992]],[[12050,12050],"mapped",[21147]],[[12051,12051],"mapped",[21241]],[[12052,12052],"mapped",[21269]],[[12053,12053],"mapped",[21274]],[[12054,12054],"mapped",[21304]],[[12055,12055],"mapped",[21313]],[[12056,12056],"mapped",[21340]],[[12057,12057],"mapped",[21353]],[[12058,12058],"mapped",[21378]],[[12059,12059],"mapped",[21430]],[[12060,12060],"mapped",[21448]],[[12061,12061],"mapped",[21475]],[[12062,12062],"mapped",[22231]],[[12063,12063],"mapped",[22303]],[[12064,12064],"mapped",[22763]],[[12065,12065],"mapped",[22786]],[[12066,12066],"mapped",[22794]],[[12067,12067],"mapped",[22805]],[[12068,12068],"mapped",[22823]],[[12069,12069],"mapped",[22899]],[[12070,12070],"mapped",[23376]],[[12071,12071],"mapped",[23424]],[[12072,12072],"mapped",[23544]],[[12073,12073],"mapped",[23567]],[[12074,12074],"mapped",[23586]],[[12075,12075],"mapped",[23608]],[[12076,12076],"mapped",[23662]],[[12077,12077],"mapped",[23665]],[[12078,12078],"mapped",[24027]],[[12079,12079],"mapped",[24037]],[[12080,12080],"mapped",[24049]],[[12081,12081],"mapped",[24062]],[[12082,12082],"mapped",[24178]],[[12083,12083],"mapped",[24186]],[[12084,12084],"mapped",[24191]],[[12085,12085],"mapped",[24308]],[[12086,12086],"mapped",[24318]],[[12087,12087],"mapped",[24331]],[[12088,12088],"mapped",[24339]],[[12089,12089],"mapped",[24400]],[[12090,12090],"mapped",[24417]],[[12091,12091],"mapped",[24435]],[[12092,12092],"mapped",[24515]],[[12093,12093],"mapped",[25096]],[[12094,12094],"mapped",[25142]],[[12095,12095],"mapped",[25163]],[[12096,12096],"mapped",[25903]],[[12097,12097],"mapped",[25908]],[[12098,12098],"mapped",[25991]],[[12099,12099],"mapped",[26007]],[[12100,12100],"mapped",[26020]],[[12101,12101],"mapped",[26041]],[[12102,12102],"mapped",[26080]],[[12103,12103],"mapped",[26085]],[[12104,12104],"mapped",[26352]],[[12105,12105],"mapped",[26376]],[[12106,12106],"mapped",[26408]],[[12107,12107],"mapped",[27424]],[[12108,12108],"mapped",[27490]],[[12109,12109],"mapped",[27513]],[[12110,12110],"mapped",[27571]],[[12111,12111],"mapped",[27595]],[[12112,12112],"mapped",[27604]],[[12113,12113],"mapped",[27611]],[[12114,12114],"mapped",[27663]],[[12115,12115],"mapped",[27668]],[[12116,12116],"mapped",[27700]],[[12117,12117],"mapped",[28779]],[[12118,12118],"mapped",[29226]],[[12119,12119],"mapped",[29238]],[[12120,12120],"mapped",[29243]],[[12121,12121],"mapped",[29247]],[[12122,12122],"mapped",[29255]],[[12123,12123],"mapped",[29273]],[[12124,12124],"mapped",[29275]],[[12125,12125],"mapped",[29356]],[[12126,12126],"mapped",[29572]],[[12127,12127],"mapped",[29577]],[[12128,12128],"mapped",[29916]],[[12129,12129],"mapped",[29926]],[[12130,12130],"mapped",[29976]],[[12131,12131],"mapped",[29983]],[[12132,12132],"mapped",[29992]],[[12133,12133],"mapped",[30000]],[[12134,12134],"mapped",[30091]],[[12135,12135],"mapped",[30098]],[[12136,12136],"mapped",[30326]],[[12137,12137],"mapped",[30333]],[[12138,12138],"mapped",[30382]],[[12139,12139],"mapped",[30399]],[[12140,12140],"mapped",[30446]],[[12141,12141],"mapped",[30683]],[[12142,12142],"mapped",[30690]],[[12143,12143],"mapped",[30707]],[[12144,12144],"mapped",[31034]],[[12145,12145],"mapped",[31160]],[[12146,12146],"mapped",[31166]],[[12147,12147],"mapped",[31348]],[[12148,12148],"mapped",[31435]],[[12149,12149],"mapped",[31481]],[[12150,12150],"mapped",[31859]],[[12151,12151],"mapped",[31992]],[[12152,12152],"mapped",[32566]],[[12153,12153],"mapped",[32593]],[[12154,12154],"mapped",[32650]],[[12155,12155],"mapped",[32701]],[[12156,12156],"mapped",[32769]],[[12157,12157],"mapped",[32780]],[[12158,12158],"mapped",[32786]],[[12159,12159],"mapped",[32819]],[[12160,12160],"mapped",[32895]],[[12161,12161],"mapped",[32905]],[[12162,12162],"mapped",[33251]],[[12163,12163],"mapped",[33258]],[[12164,12164],"mapped",[33267]],[[12165,12165],"mapped",[33276]],[[12166,12166],"mapped",[33292]],[[12167,12167],"mapped",[33307]],[[12168,12168],"mapped",[33311]],[[12169,12169],"mapped",[33390]],[[12170,12170],"mapped",[33394]],[[12171,12171],"mapped",[33400]],[[12172,12172],"mapped",[34381]],[[12173,12173],"mapped",[34411]],[[12174,12174],"mapped",[34880]],[[12175,12175],"mapped",[34892]],[[12176,12176],"mapped",[34915]],[[12177,12177],"mapped",[35198]],[[12178,12178],"mapped",[35211]],[[12179,12179],"mapped",[35282]],[[12180,12180],"mapped",[35328]],[[12181,12181],"mapped",[35895]],[[12182,12182],"mapped",[35910]],[[12183,12183],"mapped",[35925]],[[12184,12184],"mapped",[35960]],[[12185,12185],"mapped",[35997]],[[12186,12186],"mapped",[36196]],[[12187,12187],"mapped",[36208]],[[12188,12188],"mapped",[36275]],[[12189,12189],"mapped",[36523]],[[12190,12190],"mapped",[36554]],[[12191,12191],"mapped",[36763]],[[12192,12192],"mapped",[36784]],[[12193,12193],"mapped",[36789]],[[12194,12194],"mapped",[37009]],[[12195,12195],"mapped",[37193]],[[12196,12196],"mapped",[37318]],[[12197,12197],"mapped",[37324]],[[12198,12198],"mapped",[37329]],[[12199,12199],"mapped",[38263]],[[12200,12200],"mapped",[38272]],[[12201,12201],"mapped",[38428]],[[12202,12202],"mapped",[38582]],[[12203,12203],"mapped",[38585]],[[12204,12204],"mapped",[38632]],[[12205,12205],"mapped",[38737]],[[12206,12206],"mapped",[38750]],[[12207,12207],"mapped",[38754]],[[12208,12208],"mapped",[38761]],[[12209,12209],"mapped",[38859]],[[12210,12210],"mapped",[38893]],[[12211,12211],"mapped",[38899]],[[12212,12212],"mapped",[38913]],[[12213,12213],"mapped",[39080]],[[12214,12214],"mapped",[39131]],[[12215,12215],"mapped",[39135]],[[12216,12216],"mapped",[39318]],[[12217,12217],"mapped",[39321]],[[12218,12218],"mapped",[39340]],[[12219,12219],"mapped",[39592]],[[12220,12220],"mapped",[39640]],[[12221,12221],"mapped",[39647]],[[12222,12222],"mapped",[39717]],[[12223,12223],"mapped",[39727]],[[12224,12224],"mapped",[39730]],[[12225,12225],"mapped",[39740]],[[12226,12226],"mapped",[39770]],[[12227,12227],"mapped",[40165]],[[12228,12228],"mapped",[40565]],[[12229,12229],"mapped",[40575]],[[12230,12230],"mapped",[40613]],[[12231,12231],"mapped",[40635]],[[12232,12232],"mapped",[40643]],[[12233,12233],"mapped",[40653]],[[12234,12234],"mapped",[40657]],[[12235,12235],"mapped",[40697]],[[12236,12236],"mapped",[40701]],[[12237,12237],"mapped",[40718]],[[12238,12238],"mapped",[40723]],[[12239,12239],"mapped",[40736]],[[12240,12240],"mapped",[40763]],[[12241,12241],"mapped",[40778]],[[12242,12242],"mapped",[40786]],[[12243,12243],"mapped",[40845]],[[12244,12244],"mapped",[40860]],[[12245,12245],"mapped",[40864]],[[12246,12271],"disallowed"],[[12272,12283],"disallowed"],[[12284,12287],"disallowed"],[[12288,12288],"disallowed_STD3_mapped",[32]],[[12289,12289],"valid",[],"NV8"],[[12290,12290],"mapped",[46]],[[12291,12292],"valid",[],"NV8"],[[12293,12295],"valid"],[[12296,12329],"valid",[],"NV8"],[[12330,12333],"valid"],[[12334,12341],"valid",[],"NV8"],[[12342,12342],"mapped",[12306]],[[12343,12343],"valid",[],"NV8"],[[12344,12344],"mapped",[21313]],[[12345,12345],"mapped",[21316]],[[12346,12346],"mapped",[21317]],[[12347,12347],"valid",[],"NV8"],[[12348,12348],"valid"],[[12349,12349],"valid",[],"NV8"],[[12350,12350],"valid",[],"NV8"],[[12351,12351],"valid",[],"NV8"],[[12352,12352],"disallowed"],[[12353,12436],"valid"],[[12437,12438],"valid"],[[12439,12440],"disallowed"],[[12441,12442],"valid"],[[12443,12443],"disallowed_STD3_mapped",[32,12441]],[[12444,12444],"disallowed_STD3_mapped",[32,12442]],[[12445,12446],"valid"],[[12447,12447],"mapped",[12424,12426]],[[12448,12448],"valid",[],"NV8"],[[12449,12542],"valid"],[[12543,12543],"mapped",[12467,12488]],[[12544,12548],"disallowed"],[[12549,12588],"valid"],[[12589,12589],"valid"],[[12590,12592],"disallowed"],[[12593,12593],"mapped",[4352]],[[12594,12594],"mapped",[4353]],[[12595,12595],"mapped",[4522]],[[12596,12596],"mapped",[4354]],[[12597,12597],"mapped",[4524]],[[12598,12598],"mapped",[4525]],[[12599,12599],"mapped",[4355]],[[12600,12600],"mapped",[4356]],[[12601,12601],"mapped",[4357]],[[12602,12602],"mapped",[4528]],[[12603,12603],"mapped",[4529]],[[12604,12604],"mapped",[4530]],[[12605,12605],"mapped",[4531]],[[12606,12606],"mapped",[4532]],[[12607,12607],"mapped",[4533]],[[12608,12608],"mapped",[4378]],[[12609,12609],"mapped",[4358]],[[12610,12610],"mapped",[4359]],[[12611,12611],"mapped",[4360]],[[12612,12612],"mapped",[4385]],[[12613,12613],"mapped",[4361]],[[12614,12614],"mapped",[4362]],[[12615,12615],"mapped",[4363]],[[12616,12616],"mapped",[4364]],[[12617,12617],"mapped",[4365]],[[12618,12618],"mapped",[4366]],[[12619,12619],"mapped",[4367]],[[12620,12620],"mapped",[4368]],[[12621,12621],"mapped",[4369]],[[12622,12622],"mapped",[4370]],[[12623,12623],"mapped",[4449]],[[12624,12624],"mapped",[4450]],[[12625,12625],"mapped",[4451]],[[12626,12626],"mapped",[4452]],[[12627,12627],"mapped",[4453]],[[12628,12628],"mapped",[4454]],[[12629,12629],"mapped",[4455]],[[12630,12630],"mapped",[4456]],[[12631,12631],"mapped",[4457]],[[12632,12632],"mapped",[4458]],[[12633,12633],"mapped",[4459]],[[12634,12634],"mapped",[4460]],[[12635,12635],"mapped",[4461]],[[12636,12636],"mapped",[4462]],[[12637,12637],"mapped",[4463]],[[12638,12638],"mapped",[4464]],[[12639,12639],"mapped",[4465]],[[12640,12640],"mapped",[4466]],[[12641,12641],"mapped",[4467]],[[12642,12642],"mapped",[4468]],[[12643,12643],"mapped",[4469]],[[12644,12644],"disallowed"],[[12645,12645],"mapped",[4372]],[[12646,12646],"mapped",[4373]],[[12647,12647],"mapped",[4551]],[[12648,12648],"mapped",[4552]],[[12649,12649],"mapped",[4556]],[[12650,12650],"mapped",[4558]],[[12651,12651],"mapped",[4563]],[[12652,12652],"mapped",[4567]],[[12653,12653],"mapped",[4569]],[[12654,12654],"mapped",[4380]],[[12655,12655],"mapped",[4573]],[[12656,12656],"mapped",[4575]],[[12657,12657],"mapped",[4381]],[[12658,12658],"mapped",[4382]],[[12659,12659],"mapped",[4384]],[[12660,12660],"mapped",[4386]],[[12661,12661],"mapped",[4387]],[[12662,12662],"mapped",[4391]],[[12663,12663],"mapped",[4393]],[[12664,12664],"mapped",[4395]],[[12665,12665],"mapped",[4396]],[[12666,12666],"mapped",[4397]],[[12667,12667],"mapped",[4398]],[[12668,12668],"mapped",[4399]],[[12669,12669],"mapped",[4402]],[[12670,12670],"mapped",[4406]],[[12671,12671],"mapped",[4416]],[[12672,12672],"mapped",[4423]],[[12673,12673],"mapped",[4428]],[[12674,12674],"mapped",[4593]],[[12675,12675],"mapped",[4594]],[[12676,12676],"mapped",[4439]],[[12677,12677],"mapped",[4440]],[[12678,12678],"mapped",[4441]],[[12679,12679],"mapped",[4484]],[[12680,12680],"mapped",[4485]],[[12681,12681],"mapped",[4488]],[[12682,12682],"mapped",[4497]],[[12683,12683],"mapped",[4498]],[[12684,12684],"mapped",[4500]],[[12685,12685],"mapped",[4510]],[[12686,12686],"mapped",[4513]],[[12687,12687],"disallowed"],[[12688,12689],"valid",[],"NV8"],[[12690,12690],"mapped",[19968]],[[12691,12691],"mapped",[20108]],[[12692,12692],"mapped",[19977]],[[12693,12693],"mapped",[22235]],[[12694,12694],"mapped",[19978]],[[12695,12695],"mapped",[20013]],[[12696,12696],"mapped",[19979]],[[12697,12697],"mapped",[30002]],[[12698,12698],"mapped",[20057]],[[12699,12699],"mapped",[19993]],[[12700,12700],"mapped",[19969]],[[12701,12701],"mapped",[22825]],[[12702,12702],"mapped",[22320]],[[12703,12703],"mapped",[20154]],[[12704,12727],"valid"],[[12728,12730],"valid"],[[12731,12735],"disallowed"],[[12736,12751],"valid",[],"NV8"],[[12752,12771],"valid",[],"NV8"],[[12772,12783],"disallowed"],[[12784,12799],"valid"],[[12800,12800],"disallowed_STD3_mapped",[40,4352,41]],[[12801,12801],"disallowed_STD3_mapped",[40,4354,41]],[[12802,12802],"disallowed_STD3_mapped",[40,4355,41]],[[12803,12803],"disallowed_STD3_mapped",[40,4357,41]],[[12804,12804],"disallowed_STD3_mapped",[40,4358,41]],[[12805,12805],"disallowed_STD3_mapped",[40,4359,41]],[[12806,12806],"disallowed_STD3_mapped",[40,4361,41]],[[12807,12807],"disallowed_STD3_mapped",[40,4363,41]],[[12808,12808],"disallowed_STD3_mapped",[40,4364,41]],[[12809,12809],"disallowed_STD3_mapped",[40,4366,41]],[[12810,12810],"disallowed_STD3_mapped",[40,4367,41]],[[12811,12811],"disallowed_STD3_mapped",[40,4368,41]],[[12812,12812],"disallowed_STD3_mapped",[40,4369,41]],[[12813,12813],"disallowed_STD3_mapped",[40,4370,41]],[[12814,12814],"disallowed_STD3_mapped",[40,44032,41]],[[12815,12815],"disallowed_STD3_mapped",[40,45208,41]],[[12816,12816],"disallowed_STD3_mapped",[40,45796,41]],[[12817,12817],"disallowed_STD3_mapped",[40,46972,41]],[[12818,12818],"disallowed_STD3_mapped",[40,47560,41]],[[12819,12819],"disallowed_STD3_mapped",[40,48148,41]],[[12820,12820],"disallowed_STD3_mapped",[40,49324,41]],[[12821,12821],"disallowed_STD3_mapped",[40,50500,41]],[[12822,12822],"disallowed_STD3_mapped",[40,51088,41]],[[12823,12823],"disallowed_STD3_mapped",[40,52264,41]],[[12824,12824],"disallowed_STD3_mapped",[40,52852,41]],[[12825,12825],"disallowed_STD3_mapped",[40,53440,41]],[[12826,12826],"disallowed_STD3_mapped",[40,54028,41]],[[12827,12827],"disallowed_STD3_mapped",[40,54616,41]],[[12828,12828],"disallowed_STD3_mapped",[40,51452,41]],[[12829,12829],"disallowed_STD3_mapped",[40,50724,51204,41]],[[12830,12830],"disallowed_STD3_mapped",[40,50724,54980,41]],[[12831,12831],"disallowed"],[[12832,12832],"disallowed_STD3_mapped",[40,19968,41]],[[12833,12833],"disallowed_STD3_mapped",[40,20108,41]],[[12834,12834],"disallowed_STD3_mapped",[40,19977,41]],[[12835,12835],"disallowed_STD3_mapped",[40,22235,41]],[[12836,12836],"disallowed_STD3_mapped",[40,20116,41]],[[12837,12837],"disallowed_STD3_mapped",[40,20845,41]],[[12838,12838],"disallowed_STD3_mapped",[40,19971,41]],[[12839,12839],"disallowed_STD3_mapped",[40,20843,41]],[[12840,12840],"disallowed_STD3_mapped",[40,20061,41]],[[12841,12841],"disallowed_STD3_mapped",[40,21313,41]],[[12842,12842],"disallowed_STD3_mapped",[40,26376,41]],[[12843,12843],"disallowed_STD3_mapped",[40,28779,41]],[[12844,12844],"disallowed_STD3_mapped",[40,27700,41]],[[12845,12845],"disallowed_STD3_mapped",[40,26408,41]],[[12846,12846],"disallowed_STD3_mapped",[40,37329,41]],[[12847,12847],"disallowed_STD3_mapped",[40,22303,41]],[[12848,12848],"disallowed_STD3_mapped",[40,26085,41]],[[12849,12849],"disallowed_STD3_mapped",[40,26666,41]],[[12850,12850],"disallowed_STD3_mapped",[40,26377,41]],[[12851,12851],"disallowed_STD3_mapped",[40,31038,41]],[[12852,12852],"disallowed_STD3_mapped",[40,21517,41]],[[12853,12853],"disallowed_STD3_mapped",[40,29305,41]],[[12854,12854],"disallowed_STD3_mapped",[40,36001,41]],[[12855,12855],"disallowed_STD3_mapped",[40,31069,41]],[[12856,12856],"disallowed_STD3_mapped",[40,21172,41]],[[12857,12857],"disallowed_STD3_mapped",[40,20195,41]],[[12858,12858],"disallowed_STD3_mapped",[40,21628,41]],[[12859,12859],"disallowed_STD3_mapped",[40,23398,41]],[[12860,12860],"disallowed_STD3_mapped",[40,30435,41]],[[12861,12861],"disallowed_STD3_mapped",[40,20225,41]],[[12862,12862],"disallowed_STD3_mapped",[40,36039,41]],[[12863,12863],"disallowed_STD3_mapped",[40,21332,41]],[[12864,12864],"disallowed_STD3_mapped",[40,31085,41]],[[12865,12865],"disallowed_STD3_mapped",[40,20241,41]],[[12866,12866],"disallowed_STD3_mapped",[40,33258,41]],[[12867,12867],"disallowed_STD3_mapped",[40,33267,41]],[[12868,12868],"mapped",[21839]],[[12869,12869],"mapped",[24188]],[[12870,12870],"mapped",[25991]],[[12871,12871],"mapped",[31631]],[[12872,12879],"valid",[],"NV8"],[[12880,12880],"mapped",[112,116,101]],[[12881,12881],"mapped",[50,49]],[[12882,12882],"mapped",[50,50]],[[12883,12883],"mapped",[50,51]],[[12884,12884],"mapped",[50,52]],[[12885,12885],"mapped",[50,53]],[[12886,12886],"mapped",[50,54]],[[12887,12887],"mapped",[50,55]],[[12888,12888],"mapped",[50,56]],[[12889,12889],"mapped",[50,57]],[[12890,12890],"mapped",[51,48]],[[12891,12891],"mapped",[51,49]],[[12892,12892],"mapped",[51,50]],[[12893,12893],"mapped",[51,51]],[[12894,12894],"mapped",[51,52]],[[12895,12895],"mapped",[51,53]],[[12896,12896],"mapped",[4352]],[[12897,12897],"mapped",[4354]],[[12898,12898],"mapped",[4355]],[[12899,12899],"mapped",[4357]],[[12900,12900],"mapped",[4358]],[[12901,12901],"mapped",[4359]],[[12902,12902],"mapped",[4361]],[[12903,12903],"mapped",[4363]],[[12904,12904],"mapped",[4364]],[[12905,12905],"mapped",[4366]],[[12906,12906],"mapped",[4367]],[[12907,12907],"mapped",[4368]],[[12908,12908],"mapped",[4369]],[[12909,12909],"mapped",[4370]],[[12910,12910],"mapped",[44032]],[[12911,12911],"mapped",[45208]],[[12912,12912],"mapped",[45796]],[[12913,12913],"mapped",[46972]],[[12914,12914],"mapped",[47560]],[[12915,12915],"mapped",[48148]],[[12916,12916],"mapped",[49324]],[[12917,12917],"mapped",[50500]],[[12918,12918],"mapped",[51088]],[[12919,12919],"mapped",[52264]],[[12920,12920],"mapped",[52852]],[[12921,12921],"mapped",[53440]],[[12922,12922],"mapped",[54028]],[[12923,12923],"mapped",[54616]],[[12924,12924],"mapped",[52280,44256]],[[12925,12925],"mapped",[51452,51032]],[[12926,12926],"mapped",[50864]],[[12927,12927],"valid",[],"NV8"],[[12928,12928],"mapped",[19968]],[[12929,12929],"mapped",[20108]],[[12930,12930],"mapped",[19977]],[[12931,12931],"mapped",[22235]],[[12932,12932],"mapped",[20116]],[[12933,12933],"mapped",[20845]],[[12934,12934],"mapped",[19971]],[[12935,12935],"mapped",[20843]],[[12936,12936],"mapped",[20061]],[[12937,12937],"mapped",[21313]],[[12938,12938],"mapped",[26376]],[[12939,12939],"mapped",[28779]],[[12940,12940],"mapped",[27700]],[[12941,12941],"mapped",[26408]],[[12942,12942],"mapped",[37329]],[[12943,12943],"mapped",[22303]],[[12944,12944],"mapped",[26085]],[[12945,12945],"mapped",[26666]],[[12946,12946],"mapped",[26377]],[[12947,12947],"mapped",[31038]],[[12948,12948],"mapped",[21517]],[[12949,12949],"mapped",[29305]],[[12950,12950],"mapped",[36001]],[[12951,12951],"mapped",[31069]],[[12952,12952],"mapped",[21172]],[[12953,12953],"mapped",[31192]],[[12954,12954],"mapped",[30007]],[[12955,12955],"mapped",[22899]],[[12956,12956],"mapped",[36969]],[[12957,12957],"mapped",[20778]],[[12958,12958],"mapped",[21360]],[[12959,12959],"mapped",[27880]],[[12960,12960],"mapped",[38917]],[[12961,12961],"mapped",[20241]],[[12962,12962],"mapped",[20889]],[[12963,12963],"mapped",[27491]],[[12964,12964],"mapped",[19978]],[[12965,12965],"mapped",[20013]],[[12966,12966],"mapped",[19979]],[[12967,12967],"mapped",[24038]],[[12968,12968],"mapped",[21491]],[[12969,12969],"mapped",[21307]],[[12970,12970],"mapped",[23447]],[[12971,12971],"mapped",[23398]],[[12972,12972],"mapped",[30435]],[[12973,12973],"mapped",[20225]],[[12974,12974],"mapped",[36039]],[[12975,12975],"mapped",[21332]],[[12976,12976],"mapped",[22812]],[[12977,12977],"mapped",[51,54]],[[12978,12978],"mapped",[51,55]],[[12979,12979],"mapped",[51,56]],[[12980,12980],"mapped",[51,57]],[[12981,12981],"mapped",[52,48]],[[12982,12982],"mapped",[52,49]],[[12983,12983],"mapped",[52,50]],[[12984,12984],"mapped",[52,51]],[[12985,12985],"mapped",[52,52]],[[12986,12986],"mapped",[52,53]],[[12987,12987],"mapped",[52,54]],[[12988,12988],"mapped",[52,55]],[[12989,12989],"mapped",[52,56]],[[12990,12990],"mapped",[52,57]],[[12991,12991],"mapped",[53,48]],[[12992,12992],"mapped",[49,26376]],[[12993,12993],"mapped",[50,26376]],[[12994,12994],"mapped",[51,26376]],[[12995,12995],"mapped",[52,26376]],[[12996,12996],"mapped",[53,26376]],[[12997,12997],"mapped",[54,26376]],[[12998,12998],"mapped",[55,26376]],[[12999,12999],"mapped",[56,26376]],[[13000,13000],"mapped",[57,26376]],[[13001,13001],"mapped",[49,48,26376]],[[13002,13002],"mapped",[49,49,26376]],[[13003,13003],"mapped",[49,50,26376]],[[13004,13004],"mapped",[104,103]],[[13005,13005],"mapped",[101,114,103]],[[13006,13006],"mapped",[101,118]],[[13007,13007],"mapped",[108,116,100]],[[13008,13008],"mapped",[12450]],[[13009,13009],"mapped",[12452]],[[13010,13010],"mapped",[12454]],[[13011,13011],"mapped",[12456]],[[13012,13012],"mapped",[12458]],[[13013,13013],"mapped",[12459]],[[13014,13014],"mapped",[12461]],[[13015,13015],"mapped",[12463]],[[13016,13016],"mapped",[12465]],[[13017,13017],"mapped",[12467]],[[13018,13018],"mapped",[12469]],[[13019,13019],"mapped",[12471]],[[13020,13020],"mapped",[12473]],[[13021,13021],"mapped",[12475]],[[13022,13022],"mapped",[12477]],[[13023,13023],"mapped",[12479]],[[13024,13024],"mapped",[12481]],[[13025,13025],"mapped",[12484]],[[13026,13026],"mapped",[12486]],[[13027,13027],"mapped",[12488]],[[13028,13028],"mapped",[12490]],[[13029,13029],"mapped",[12491]],[[13030,13030],"mapped",[12492]],[[13031,13031],"mapped",[12493]],[[13032,13032],"mapped",[12494]],[[13033,13033],"mapped",[12495]],[[13034,13034],"mapped",[12498]],[[13035,13035],"mapped",[12501]],[[13036,13036],"mapped",[12504]],[[13037,13037],"mapped",[12507]],[[13038,13038],"mapped",[12510]],[[13039,13039],"mapped",[12511]],[[13040,13040],"mapped",[12512]],[[13041,13041],"mapped",[12513]],[[13042,13042],"mapped",[12514]],[[13043,13043],"mapped",[12516]],[[13044,13044],"mapped",[12518]],[[13045,13045],"mapped",[12520]],[[13046,13046],"mapped",[12521]],[[13047,13047],"mapped",[12522]],[[13048,13048],"mapped",[12523]],[[13049,13049],"mapped",[12524]],[[13050,13050],"mapped",[12525]],[[13051,13051],"mapped",[12527]],[[13052,13052],"mapped",[12528]],[[13053,13053],"mapped",[12529]],[[13054,13054],"mapped",[12530]],[[13055,13055],"disallowed"],[[13056,13056],"mapped",[12450,12497,12540,12488]],[[13057,13057],"mapped",[12450,12523,12501,12449]],[[13058,13058],"mapped",[12450,12531,12506,12450]],[[13059,13059],"mapped",[12450,12540,12523]],[[13060,13060],"mapped",[12452,12491,12531,12464]],[[13061,13061],"mapped",[12452,12531,12481]],[[13062,13062],"mapped",[12454,12457,12531]],[[13063,13063],"mapped",[12456,12473,12463,12540,12489]],[[13064,13064],"mapped",[12456,12540,12459,12540]],[[13065,13065],"mapped",[12458,12531,12473]],[[13066,13066],"mapped",[12458,12540,12512]],[[13067,13067],"mapped",[12459,12452,12522]],[[13068,13068],"mapped",[12459,12521,12483,12488]],[[13069,13069],"mapped",[12459,12525,12522,12540]],[[13070,13070],"mapped",[12460,12525,12531]],[[13071,13071],"mapped",[12460,12531,12510]],[[13072,13072],"mapped",[12462,12460]],[[13073,13073],"mapped",[12462,12491,12540]],[[13074,13074],"mapped",[12461,12517,12522,12540]],[[13075,13075],"mapped",[12462,12523,12480,12540]],[[13076,13076],"mapped",[12461,12525]],[[13077,13077],"mapped",[12461,12525,12464,12521,12512]],[[13078,13078],"mapped",[12461,12525,12513,12540,12488,12523]],[[13079,13079],"mapped",[12461,12525,12527,12483,12488]],[[13080,13080],"mapped",[12464,12521,12512]],[[13081,13081],"mapped",[12464,12521,12512,12488,12531]],[[13082,13082],"mapped",[12463,12523,12476,12452,12525]],[[13083,13083],"mapped",[12463,12525,12540,12493]],[[13084,13084],"mapped",[12465,12540,12473]],[[13085,13085],"mapped",[12467,12523,12490]],[[13086,13086],"mapped",[12467,12540,12509]],[[13087,13087],"mapped",[12469,12452,12463,12523]],[[13088,13088],"mapped",[12469,12531,12481,12540,12512]],[[13089,13089],"mapped",[12471,12522,12531,12464]],[[13090,13090],"mapped",[12475,12531,12481]],[[13091,13091],"mapped",[12475,12531,12488]],[[13092,13092],"mapped",[12480,12540,12473]],[[13093,13093],"mapped",[12487,12471]],[[13094,13094],"mapped",[12489,12523]],[[13095,13095],"mapped",[12488,12531]],[[13096,13096],"mapped",[12490,12494]],[[13097,13097],"mapped",[12494,12483,12488]],[[13098,13098],"mapped",[12495,12452,12484]],[[13099,13099],"mapped",[12497,12540,12475,12531,12488]],[[13100,13100],"mapped",[12497,12540,12484]],[[13101,13101],"mapped",[12496,12540,12524,12523]],[[13102,13102],"mapped",[12500,12450,12473,12488,12523]],[[13103,13103],"mapped",[12500,12463,12523]],[[13104,13104],"mapped",[12500,12467]],[[13105,13105],"mapped",[12499,12523]],[[13106,13106],"mapped",[12501,12449,12521,12483,12489]],[[13107,13107],"mapped",[12501,12451,12540,12488]],[[13108,13108],"mapped",[12502,12483,12471,12455,12523]],[[13109,13109],"mapped",[12501,12521,12531]],[[13110,13110],"mapped",[12504,12463,12479,12540,12523]],[[13111,13111],"mapped",[12506,12477]],[[13112,13112],"mapped",[12506,12491,12498]],[[13113,13113],"mapped",[12504,12523,12484]],[[13114,13114],"mapped",[12506,12531,12473]],[[13115,13115],"mapped",[12506,12540,12472]],[[13116,13116],"mapped",[12505,12540,12479]],[[13117,13117],"mapped",[12509,12452,12531,12488]],[[13118,13118],"mapped",[12508,12523,12488]],[[13119,13119],"mapped",[12507,12531]],[[13120,13120],"mapped",[12509,12531,12489]],[[13121,13121],"mapped",[12507,12540,12523]],[[13122,13122],"mapped",[12507,12540,12531]],[[13123,13123],"mapped",[12510,12452,12463,12525]],[[13124,13124],"mapped",[12510,12452,12523]],[[13125,13125],"mapped",[12510,12483,12495]],[[13126,13126],"mapped",[12510,12523,12463]],[[13127,13127],"mapped",[12510,12531,12471,12519,12531]],[[13128,13128],"mapped",[12511,12463,12525,12531]],[[13129,13129],"mapped",[12511,12522]],[[13130,13130],"mapped",[12511,12522,12496,12540,12523]],[[13131,13131],"mapped",[12513,12460]],[[13132,13132],"mapped",[12513,12460,12488,12531]],[[13133,13133],"mapped",[12513,12540,12488,12523]],[[13134,13134],"mapped",[12516,12540,12489]],[[13135,13135],"mapped",[12516,12540,12523]],[[13136,13136],"mapped",[12518,12450,12531]],[[13137,13137],"mapped",[12522,12483,12488,12523]],[[13138,13138],"mapped",[12522,12521]],[[13139,13139],"mapped",[12523,12500,12540]],[[13140,13140],"mapped",[12523,12540,12502,12523]],[[13141,13141],"mapped",[12524,12512]],[[13142,13142],"mapped",[12524,12531,12488,12466,12531]],[[13143,13143],"mapped",[12527,12483,12488]],[[13144,13144],"mapped",[48,28857]],[[13145,13145],"mapped",[49,28857]],[[13146,13146],"mapped",[50,28857]],[[13147,13147],"mapped",[51,28857]],[[13148,13148],"mapped",[52,28857]],[[13149,13149],"mapped",[53,28857]],[[13150,13150],"mapped",[54,28857]],[[13151,13151],"mapped",[55,28857]],[[13152,13152],"mapped",[56,28857]],[[13153,13153],"mapped",[57,28857]],[[13154,13154],"mapped",[49,48,28857]],[[13155,13155],"mapped",[49,49,28857]],[[13156,13156],"mapped",[49,50,28857]],[[13157,13157],"mapped",[49,51,28857]],[[13158,13158],"mapped",[49,52,28857]],[[13159,13159],"mapped",[49,53,28857]],[[13160,13160],"mapped",[49,54,28857]],[[13161,13161],"mapped",[49,55,28857]],[[13162,13162],"mapped",[49,56,28857]],[[13163,13163],"mapped",[49,57,28857]],[[13164,13164],"mapped",[50,48,28857]],[[13165,13165],"mapped",[50,49,28857]],[[13166,13166],"mapped",[50,50,28857]],[[13167,13167],"mapped",[50,51,28857]],[[13168,13168],"mapped",[50,52,28857]],[[13169,13169],"mapped",[104,112,97]],[[13170,13170],"mapped",[100,97]],[[13171,13171],"mapped",[97,117]],[[13172,13172],"mapped",[98,97,114]],[[13173,13173],"mapped",[111,118]],[[13174,13174],"mapped",[112,99]],[[13175,13175],"mapped",[100,109]],[[13176,13176],"mapped",[100,109,50]],[[13177,13177],"mapped",[100,109,51]],[[13178,13178],"mapped",[105,117]],[[13179,13179],"mapped",[24179,25104]],[[13180,13180],"mapped",[26157,21644]],[[13181,13181],"mapped",[22823,27491]],[[13182,13182],"mapped",[26126,27835]],[[13183,13183],"mapped",[26666,24335,20250,31038]],[[13184,13184],"mapped",[112,97]],[[13185,13185],"mapped",[110,97]],[[13186,13186],"mapped",[956,97]],[[13187,13187],"mapped",[109,97]],[[13188,13188],"mapped",[107,97]],[[13189,13189],"mapped",[107,98]],[[13190,13190],"mapped",[109,98]],[[13191,13191],"mapped",[103,98]],[[13192,13192],"mapped",[99,97,108]],[[13193,13193],"mapped",[107,99,97,108]],[[13194,13194],"mapped",[112,102]],[[13195,13195],"mapped",[110,102]],[[13196,13196],"mapped",[956,102]],[[13197,13197],"mapped",[956,103]],[[13198,13198],"mapped",[109,103]],[[13199,13199],"mapped",[107,103]],[[13200,13200],"mapped",[104,122]],[[13201,13201],"mapped",[107,104,122]],[[13202,13202],"mapped",[109,104,122]],[[13203,13203],"mapped",[103,104,122]],[[13204,13204],"mapped",[116,104,122]],[[13205,13205],"mapped",[956,108]],[[13206,13206],"mapped",[109,108]],[[13207,13207],"mapped",[100,108]],[[13208,13208],"mapped",[107,108]],[[13209,13209],"mapped",[102,109]],[[13210,13210],"mapped",[110,109]],[[13211,13211],"mapped",[956,109]],[[13212,13212],"mapped",[109,109]],[[13213,13213],"mapped",[99,109]],[[13214,13214],"mapped",[107,109]],[[13215,13215],"mapped",[109,109,50]],[[13216,13216],"mapped",[99,109,50]],[[13217,13217],"mapped",[109,50]],[[13218,13218],"mapped",[107,109,50]],[[13219,13219],"mapped",[109,109,51]],[[13220,13220],"mapped",[99,109,51]],[[13221,13221],"mapped",[109,51]],[[13222,13222],"mapped",[107,109,51]],[[13223,13223],"mapped",[109,8725,115]],[[13224,13224],"mapped",[109,8725,115,50]],[[13225,13225],"mapped",[112,97]],[[13226,13226],"mapped",[107,112,97]],[[13227,13227],"mapped",[109,112,97]],[[13228,13228],"mapped",[103,112,97]],[[13229,13229],"mapped",[114,97,100]],[[13230,13230],"mapped",[114,97,100,8725,115]],[[13231,13231],"mapped",[114,97,100,8725,115,50]],[[13232,13232],"mapped",[112,115]],[[13233,13233],"mapped",[110,115]],[[13234,13234],"mapped",[956,115]],[[13235,13235],"mapped",[109,115]],[[13236,13236],"mapped",[112,118]],[[13237,13237],"mapped",[110,118]],[[13238,13238],"mapped",[956,118]],[[13239,13239],"mapped",[109,118]],[[13240,13240],"mapped",[107,118]],[[13241,13241],"mapped",[109,118]],[[13242,13242],"mapped",[112,119]],[[13243,13243],"mapped",[110,119]],[[13244,13244],"mapped",[956,119]],[[13245,13245],"mapped",[109,119]],[[13246,13246],"mapped",[107,119]],[[13247,13247],"mapped",[109,119]],[[13248,13248],"mapped",[107,969]],[[13249,13249],"mapped",[109,969]],[[13250,13250],"disallowed"],[[13251,13251],"mapped",[98,113]],[[13252,13252],"mapped",[99,99]],[[13253,13253],"mapped",[99,100]],[[13254,13254],"mapped",[99,8725,107,103]],[[13255,13255],"disallowed"],[[13256,13256],"mapped",[100,98]],[[13257,13257],"mapped",[103,121]],[[13258,13258],"mapped",[104,97]],[[13259,13259],"mapped",[104,112]],[[13260,13260],"mapped",[105,110]],[[13261,13261],"mapped",[107,107]],[[13262,13262],"mapped",[107,109]],[[13263,13263],"mapped",[107,116]],[[13264,13264],"mapped",[108,109]],[[13265,13265],"mapped",[108,110]],[[13266,13266],"mapped",[108,111,103]],[[13267,13267],"mapped",[108,120]],[[13268,13268],"mapped",[109,98]],[[13269,13269],"mapped",[109,105,108]],[[13270,13270],"mapped",[109,111,108]],[[13271,13271],"mapped",[112,104]],[[13272,13272],"disallowed"],[[13273,13273],"mapped",[112,112,109]],[[13274,13274],"mapped",[112,114]],[[13275,13275],"mapped",[115,114]],[[13276,13276],"mapped",[115,118]],[[13277,13277],"mapped",[119,98]],[[13278,13278],"mapped",[118,8725,109]],[[13279,13279],"mapped",[97,8725,109]],[[13280,13280],"mapped",[49,26085]],[[13281,13281],"mapped",[50,26085]],[[13282,13282],"mapped",[51,26085]],[[13283,13283],"mapped",[52,26085]],[[13284,13284],"mapped",[53,26085]],[[13285,13285],"mapped",[54,26085]],[[13286,13286],"mapped",[55,26085]],[[13287,13287],"mapped",[56,26085]],[[13288,13288],"mapped",[57,26085]],[[13289,13289],"mapped",[49,48,26085]],[[13290,13290],"mapped",[49,49,26085]],[[13291,13291],"mapped",[49,50,26085]],[[13292,13292],"mapped",[49,51,26085]],[[13293,13293],"mapped",[49,52,26085]],[[13294,13294],"mapped",[49,53,26085]],[[13295,13295],"mapped",[49,54,26085]],[[13296,13296],"mapped",[49,55,26085]],[[13297,13297],"mapped",[49,56,26085]],[[13298,13298],"mapped",[49,57,26085]],[[13299,13299],"mapped",[50,48,26085]],[[13300,13300],"mapped",[50,49,26085]],[[13301,13301],"mapped",[50,50,26085]],[[13302,13302],"mapped",[50,51,26085]],[[13303,13303],"mapped",[50,52,26085]],[[13304,13304],"mapped",[50,53,26085]],[[13305,13305],"mapped",[50,54,26085]],[[13306,13306],"mapped",[50,55,26085]],[[13307,13307],"mapped",[50,56,26085]],[[13308,13308],"mapped",[50,57,26085]],[[13309,13309],"mapped",[51,48,26085]],[[13310,13310],"mapped",[51,49,26085]],[[13311,13311],"mapped",[103,97,108]],[[13312,19893],"valid"],[[19894,19903],"disallowed"],[[19904,19967],"valid",[],"NV8"],[[19968,40869],"valid"],[[40870,40891],"valid"],[[40892,40899],"valid"],[[40900,40907],"valid"],[[40908,40908],"valid"],[[40909,40917],"valid"],[[40918,40959],"disallowed"],[[40960,42124],"valid"],[[42125,42127],"disallowed"],[[42128,42145],"valid",[],"NV8"],[[42146,42147],"valid",[],"NV8"],[[42148,42163],"valid",[],"NV8"],[[42164,42164],"valid",[],"NV8"],[[42165,42176],"valid",[],"NV8"],[[42177,42177],"valid",[],"NV8"],[[42178,42180],"valid",[],"NV8"],[[42181,42181],"valid",[],"NV8"],[[42182,42182],"valid",[],"NV8"],[[42183,42191],"disallowed"],[[42192,42237],"valid"],[[42238,42239],"valid",[],"NV8"],[[42240,42508],"valid"],[[42509,42511],"valid",[],"NV8"],[[42512,42539],"valid"],[[42540,42559],"disallowed"],[[42560,42560],"mapped",[42561]],[[42561,42561],"valid"],[[42562,42562],"mapped",[42563]],[[42563,42563],"valid"],[[42564,42564],"mapped",[42565]],[[42565,42565],"valid"],[[42566,42566],"mapped",[42567]],[[42567,42567],"valid"],[[42568,42568],"mapped",[42569]],[[42569,42569],"valid"],[[42570,42570],"mapped",[42571]],[[42571,42571],"valid"],[[42572,42572],"mapped",[42573]],[[42573,42573],"valid"],[[42574,42574],"mapped",[42575]],[[42575,42575],"valid"],[[42576,42576],"mapped",[42577]],[[42577,42577],"valid"],[[42578,42578],"mapped",[42579]],[[42579,42579],"valid"],[[42580,42580],"mapped",[42581]],[[42581,42581],"valid"],[[42582,42582],"mapped",[42583]],[[42583,42583],"valid"],[[42584,42584],"mapped",[42585]],[[42585,42585],"valid"],[[42586,42586],"mapped",[42587]],[[42587,42587],"valid"],[[42588,42588],"mapped",[42589]],[[42589,42589],"valid"],[[42590,42590],"mapped",[42591]],[[42591,42591],"valid"],[[42592,42592],"mapped",[42593]],[[42593,42593],"valid"],[[42594,42594],"mapped",[42595]],[[42595,42595],"valid"],[[42596,42596],"mapped",[42597]],[[42597,42597],"valid"],[[42598,42598],"mapped",[42599]],[[42599,42599],"valid"],[[42600,42600],"mapped",[42601]],[[42601,42601],"valid"],[[42602,42602],"mapped",[42603]],[[42603,42603],"valid"],[[42604,42604],"mapped",[42605]],[[42605,42607],"valid"],[[42608,42611],"valid",[],"NV8"],[[42612,42619],"valid"],[[42620,42621],"valid"],[[42622,42622],"valid",[],"NV8"],[[42623,42623],"valid"],[[42624,42624],"mapped",[42625]],[[42625,42625],"valid"],[[42626,42626],"mapped",[42627]],[[42627,42627],"valid"],[[42628,42628],"mapped",[42629]],[[42629,42629],"valid"],[[42630,42630],"mapped",[42631]],[[42631,42631],"valid"],[[42632,42632],"mapped",[42633]],[[42633,42633],"valid"],[[42634,42634],"mapped",[42635]],[[42635,42635],"valid"],[[42636,42636],"mapped",[42637]],[[42637,42637],"valid"],[[42638,42638],"mapped",[42639]],[[42639,42639],"valid"],[[42640,42640],"mapped",[42641]],[[42641,42641],"valid"],[[42642,42642],"mapped",[42643]],[[42643,42643],"valid"],[[42644,42644],"mapped",[42645]],[[42645,42645],"valid"],[[42646,42646],"mapped",[42647]],[[42647,42647],"valid"],[[42648,42648],"mapped",[42649]],[[42649,42649],"valid"],[[42650,42650],"mapped",[42651]],[[42651,42651],"valid"],[[42652,42652],"mapped",[1098]],[[42653,42653],"mapped",[1100]],[[42654,42654],"valid"],[[42655,42655],"valid"],[[42656,42725],"valid"],[[42726,42735],"valid",[],"NV8"],[[42736,42737],"valid"],[[42738,42743],"valid",[],"NV8"],[[42744,42751],"disallowed"],[[42752,42774],"valid",[],"NV8"],[[42775,42778],"valid"],[[42779,42783],"valid"],[[42784,42785],"valid",[],"NV8"],[[42786,42786],"mapped",[42787]],[[42787,42787],"valid"],[[42788,42788],"mapped",[42789]],[[42789,42789],"valid"],[[42790,42790],"mapped",[42791]],[[42791,42791],"valid"],[[42792,42792],"mapped",[42793]],[[42793,42793],"valid"],[[42794,42794],"mapped",[42795]],[[42795,42795],"valid"],[[42796,42796],"mapped",[42797]],[[42797,42797],"valid"],[[42798,42798],"mapped",[42799]],[[42799,42801],"valid"],[[42802,42802],"mapped",[42803]],[[42803,42803],"valid"],[[42804,42804],"mapped",[42805]],[[42805,42805],"valid"],[[42806,42806],"mapped",[42807]],[[42807,42807],"valid"],[[42808,42808],"mapped",[42809]],[[42809,42809],"valid"],[[42810,42810],"mapped",[42811]],[[42811,42811],"valid"],[[42812,42812],"mapped",[42813]],[[42813,42813],"valid"],[[42814,42814],"mapped",[42815]],[[42815,42815],"valid"],[[42816,42816],"mapped",[42817]],[[42817,42817],"valid"],[[42818,42818],"mapped",[42819]],[[42819,42819],"valid"],[[42820,42820],"mapped",[42821]],[[42821,42821],"valid"],[[42822,42822],"mapped",[42823]],[[42823,42823],"valid"],[[42824,42824],"mapped",[42825]],[[42825,42825],"valid"],[[42826,42826],"mapped",[42827]],[[42827,42827],"valid"],[[42828,42828],"mapped",[42829]],[[42829,42829],"valid"],[[42830,42830],"mapped",[42831]],[[42831,42831],"valid"],[[42832,42832],"mapped",[42833]],[[42833,42833],"valid"],[[42834,42834],"mapped",[42835]],[[42835,42835],"valid"],[[42836,42836],"mapped",[42837]],[[42837,42837],"valid"],[[42838,42838],"mapped",[42839]],[[42839,42839],"valid"],[[42840,42840],"mapped",[42841]],[[42841,42841],"valid"],[[42842,42842],"mapped",[42843]],[[42843,42843],"valid"],[[42844,42844],"mapped",[42845]],[[42845,42845],"valid"],[[42846,42846],"mapped",[42847]],[[42847,42847],"valid"],[[42848,42848],"mapped",[42849]],[[42849,42849],"valid"],[[42850,42850],"mapped",[42851]],[[42851,42851],"valid"],[[42852,42852],"mapped",[42853]],[[42853,42853],"valid"],[[42854,42854],"mapped",[42855]],[[42855,42855],"valid"],[[42856,42856],"mapped",[42857]],[[42857,42857],"valid"],[[42858,42858],"mapped",[42859]],[[42859,42859],"valid"],[[42860,42860],"mapped",[42861]],[[42861,42861],"valid"],[[42862,42862],"mapped",[42863]],[[42863,42863],"valid"],[[42864,42864],"mapped",[42863]],[[42865,42872],"valid"],[[42873,42873],"mapped",[42874]],[[42874,42874],"valid"],[[42875,42875],"mapped",[42876]],[[42876,42876],"valid"],[[42877,42877],"mapped",[7545]],[[42878,42878],"mapped",[42879]],[[42879,42879],"valid"],[[42880,42880],"mapped",[42881]],[[42881,42881],"valid"],[[42882,42882],"mapped",[42883]],[[42883,42883],"valid"],[[42884,42884],"mapped",[42885]],[[42885,42885],"valid"],[[42886,42886],"mapped",[42887]],[[42887,42888],"valid"],[[42889,42890],"valid",[],"NV8"],[[42891,42891],"mapped",[42892]],[[42892,42892],"valid"],[[42893,42893],"mapped",[613]],[[42894,42894],"valid"],[[42895,42895],"valid"],[[42896,42896],"mapped",[42897]],[[42897,42897],"valid"],[[42898,42898],"mapped",[42899]],[[42899,42899],"valid"],[[42900,42901],"valid"],[[42902,42902],"mapped",[42903]],[[42903,42903],"valid"],[[42904,42904],"mapped",[42905]],[[42905,42905],"valid"],[[42906,42906],"mapped",[42907]],[[42907,42907],"valid"],[[42908,42908],"mapped",[42909]],[[42909,42909],"valid"],[[42910,42910],"mapped",[42911]],[[42911,42911],"valid"],[[42912,42912],"mapped",[42913]],[[42913,42913],"valid"],[[42914,42914],"mapped",[42915]],[[42915,42915],"valid"],[[42916,42916],"mapped",[42917]],[[42917,42917],"valid"],[[42918,42918],"mapped",[42919]],[[42919,42919],"valid"],[[42920,42920],"mapped",[42921]],[[42921,42921],"valid"],[[42922,42922],"mapped",[614]],[[42923,42923],"mapped",[604]],[[42924,42924],"mapped",[609]],[[42925,42925],"mapped",[620]],[[42926,42927],"disallowed"],[[42928,42928],"mapped",[670]],[[42929,42929],"mapped",[647]],[[42930,42930],"mapped",[669]],[[42931,42931],"mapped",[43859]],[[42932,42932],"mapped",[42933]],[[42933,42933],"valid"],[[42934,42934],"mapped",[42935]],[[42935,42935],"valid"],[[42936,42998],"disallowed"],[[42999,42999],"valid"],[[43000,43000],"mapped",[295]],[[43001,43001],"mapped",[339]],[[43002,43002],"valid"],[[43003,43007],"valid"],[[43008,43047],"valid"],[[43048,43051],"valid",[],"NV8"],[[43052,43055],"disallowed"],[[43056,43065],"valid",[],"NV8"],[[43066,43071],"disallowed"],[[43072,43123],"valid"],[[43124,43127],"valid",[],"NV8"],[[43128,43135],"disallowed"],[[43136,43204],"valid"],[[43205,43213],"disallowed"],[[43214,43215],"valid",[],"NV8"],[[43216,43225],"valid"],[[43226,43231],"disallowed"],[[43232,43255],"valid"],[[43256,43258],"valid",[],"NV8"],[[43259,43259],"valid"],[[43260,43260],"valid",[],"NV8"],[[43261,43261],"valid"],[[43262,43263],"disallowed"],[[43264,43309],"valid"],[[43310,43311],"valid",[],"NV8"],[[43312,43347],"valid"],[[43348,43358],"disallowed"],[[43359,43359],"valid",[],"NV8"],[[43360,43388],"valid",[],"NV8"],[[43389,43391],"disallowed"],[[43392,43456],"valid"],[[43457,43469],"valid",[],"NV8"],[[43470,43470],"disallowed"],[[43471,43481],"valid"],[[43482,43485],"disallowed"],[[43486,43487],"valid",[],"NV8"],[[43488,43518],"valid"],[[43519,43519],"disallowed"],[[43520,43574],"valid"],[[43575,43583],"disallowed"],[[43584,43597],"valid"],[[43598,43599],"disallowed"],[[43600,43609],"valid"],[[43610,43611],"disallowed"],[[43612,43615],"valid",[],"NV8"],[[43616,43638],"valid"],[[43639,43641],"valid",[],"NV8"],[[43642,43643],"valid"],[[43644,43647],"valid"],[[43648,43714],"valid"],[[43715,43738],"disallowed"],[[43739,43741],"valid"],[[43742,43743],"valid",[],"NV8"],[[43744,43759],"valid"],[[43760,43761],"valid",[],"NV8"],[[43762,43766],"valid"],[[43767,43776],"disallowed"],[[43777,43782],"valid"],[[43783,43784],"disallowed"],[[43785,43790],"valid"],[[43791,43792],"disallowed"],[[43793,43798],"valid"],[[43799,43807],"disallowed"],[[43808,43814],"valid"],[[43815,43815],"disallowed"],[[43816,43822],"valid"],[[43823,43823],"disallowed"],[[43824,43866],"valid"],[[43867,43867],"valid",[],"NV8"],[[43868,43868],"mapped",[42791]],[[43869,43869],"mapped",[43831]],[[43870,43870],"mapped",[619]],[[43871,43871],"mapped",[43858]],[[43872,43875],"valid"],[[43876,43877],"valid"],[[43878,43887],"disallowed"],[[43888,43888],"mapped",[5024]],[[43889,43889],"mapped",[5025]],[[43890,43890],"mapped",[5026]],[[43891,43891],"mapped",[5027]],[[43892,43892],"mapped",[5028]],[[43893,43893],"mapped",[5029]],[[43894,43894],"mapped",[5030]],[[43895,43895],"mapped",[5031]],[[43896,43896],"mapped",[5032]],[[43897,43897],"mapped",[5033]],[[43898,43898],"mapped",[5034]],[[43899,43899],"mapped",[5035]],[[43900,43900],"mapped",[5036]],[[43901,43901],"mapped",[5037]],[[43902,43902],"mapped",[5038]],[[43903,43903],"mapped",[5039]],[[43904,43904],"mapped",[5040]],[[43905,43905],"mapped",[5041]],[[43906,43906],"mapped",[5042]],[[43907,43907],"mapped",[5043]],[[43908,43908],"mapped",[5044]],[[43909,43909],"mapped",[5045]],[[43910,43910],"mapped",[5046]],[[43911,43911],"mapped",[5047]],[[43912,43912],"mapped",[5048]],[[43913,43913],"mapped",[5049]],[[43914,43914],"mapped",[5050]],[[43915,43915],"mapped",[5051]],[[43916,43916],"mapped",[5052]],[[43917,43917],"mapped",[5053]],[[43918,43918],"mapped",[5054]],[[43919,43919],"mapped",[5055]],[[43920,43920],"mapped",[5056]],[[43921,43921],"mapped",[5057]],[[43922,43922],"mapped",[5058]],[[43923,43923],"mapped",[5059]],[[43924,43924],"mapped",[5060]],[[43925,43925],"mapped",[5061]],[[43926,43926],"mapped",[5062]],[[43927,43927],"mapped",[5063]],[[43928,43928],"mapped",[5064]],[[43929,43929],"mapped",[5065]],[[43930,43930],"mapped",[5066]],[[43931,43931],"mapped",[5067]],[[43932,43932],"mapped",[5068]],[[43933,43933],"mapped",[5069]],[[43934,43934],"mapped",[5070]],[[43935,43935],"mapped",[5071]],[[43936,43936],"mapped",[5072]],[[43937,43937],"mapped",[5073]],[[43938,43938],"mapped",[5074]],[[43939,43939],"mapped",[5075]],[[43940,43940],"mapped",[5076]],[[43941,43941],"mapped",[5077]],[[43942,43942],"mapped",[5078]],[[43943,43943],"mapped",[5079]],[[43944,43944],"mapped",[5080]],[[43945,43945],"mapped",[5081]],[[43946,43946],"mapped",[5082]],[[43947,43947],"mapped",[5083]],[[43948,43948],"mapped",[5084]],[[43949,43949],"mapped",[5085]],[[43950,43950],"mapped",[5086]],[[43951,43951],"mapped",[5087]],[[43952,43952],"mapped",[5088]],[[43953,43953],"mapped",[5089]],[[43954,43954],"mapped",[5090]],[[43955,43955],"mapped",[5091]],[[43956,43956],"mapped",[5092]],[[43957,43957],"mapped",[5093]],[[43958,43958],"mapped",[5094]],[[43959,43959],"mapped",[5095]],[[43960,43960],"mapped",[5096]],[[43961,43961],"mapped",[5097]],[[43962,43962],"mapped",[5098]],[[43963,43963],"mapped",[5099]],[[43964,43964],"mapped",[5100]],[[43965,43965],"mapped",[5101]],[[43966,43966],"mapped",[5102]],[[43967,43967],"mapped",[5103]],[[43968,44010],"valid"],[[44011,44011],"valid",[],"NV8"],[[44012,44013],"valid"],[[44014,44015],"disallowed"],[[44016,44025],"valid"],[[44026,44031],"disallowed"],[[44032,55203],"valid"],[[55204,55215],"disallowed"],[[55216,55238],"valid",[],"NV8"],[[55239,55242],"disallowed"],[[55243,55291],"valid",[],"NV8"],[[55292,55295],"disallowed"],[[55296,57343],"disallowed"],[[57344,63743],"disallowed"],[[63744,63744],"mapped",[35912]],[[63745,63745],"mapped",[26356]],[[63746,63746],"mapped",[36554]],[[63747,63747],"mapped",[36040]],[[63748,63748],"mapped",[28369]],[[63749,63749],"mapped",[20018]],[[63750,63750],"mapped",[21477]],[[63751,63752],"mapped",[40860]],[[63753,63753],"mapped",[22865]],[[63754,63754],"mapped",[37329]],[[63755,63755],"mapped",[21895]],[[63756,63756],"mapped",[22856]],[[63757,63757],"mapped",[25078]],[[63758,63758],"mapped",[30313]],[[63759,63759],"mapped",[32645]],[[63760,63760],"mapped",[34367]],[[63761,63761],"mapped",[34746]],[[63762,63762],"mapped",[35064]],[[63763,63763],"mapped",[37007]],[[63764,63764],"mapped",[27138]],[[63765,63765],"mapped",[27931]],[[63766,63766],"mapped",[28889]],[[63767,63767],"mapped",[29662]],[[63768,63768],"mapped",[33853]],[[63769,63769],"mapped",[37226]],[[63770,63770],"mapped",[39409]],[[63771,63771],"mapped",[20098]],[[63772,63772],"mapped",[21365]],[[63773,63773],"mapped",[27396]],[[63774,63774],"mapped",[29211]],[[63775,63775],"mapped",[34349]],[[63776,63776],"mapped",[40478]],[[63777,63777],"mapped",[23888]],[[63778,63778],"mapped",[28651]],[[63779,63779],"mapped",[34253]],[[63780,63780],"mapped",[35172]],[[63781,63781],"mapped",[25289]],[[63782,63782],"mapped",[33240]],[[63783,63783],"mapped",[34847]],[[63784,63784],"mapped",[24266]],[[63785,63785],"mapped",[26391]],[[63786,63786],"mapped",[28010]],[[63787,63787],"mapped",[29436]],[[63788,63788],"mapped",[37070]],[[63789,63789],"mapped",[20358]],[[63790,63790],"mapped",[20919]],[[63791,63791],"mapped",[21214]],[[63792,63792],"mapped",[25796]],[[63793,63793],"mapped",[27347]],[[63794,63794],"mapped",[29200]],[[63795,63795],"mapped",[30439]],[[63796,63796],"mapped",[32769]],[[63797,63797],"mapped",[34310]],[[63798,63798],"mapped",[34396]],[[63799,63799],"mapped",[36335]],[[63800,63800],"mapped",[38706]],[[63801,63801],"mapped",[39791]],[[63802,63802],"mapped",[40442]],[[63803,63803],"mapped",[30860]],[[63804,63804],"mapped",[31103]],[[63805,63805],"mapped",[32160]],[[63806,63806],"mapped",[33737]],[[63807,63807],"mapped",[37636]],[[63808,63808],"mapped",[40575]],[[63809,63809],"mapped",[35542]],[[63810,63810],"mapped",[22751]],[[63811,63811],"mapped",[24324]],[[63812,63812],"mapped",[31840]],[[63813,63813],"mapped",[32894]],[[63814,63814],"mapped",[29282]],[[63815,63815],"mapped",[30922]],[[63816,63816],"mapped",[36034]],[[63817,63817],"mapped",[38647]],[[63818,63818],"mapped",[22744]],[[63819,63819],"mapped",[23650]],[[63820,63820],"mapped",[27155]],[[63821,63821],"mapped",[28122]],[[63822,63822],"mapped",[28431]],[[63823,63823],"mapped",[32047]],[[63824,63824],"mapped",[32311]],[[63825,63825],"mapped",[38475]],[[63826,63826],"mapped",[21202]],[[63827,63827],"mapped",[32907]],[[63828,63828],"mapped",[20956]],[[63829,63829],"mapped",[20940]],[[63830,63830],"mapped",[31260]],[[63831,63831],"mapped",[32190]],[[63832,63832],"mapped",[33777]],[[63833,63833],"mapped",[38517]],[[63834,63834],"mapped",[35712]],[[63835,63835],"mapped",[25295]],[[63836,63836],"mapped",[27138]],[[63837,63837],"mapped",[35582]],[[63838,63838],"mapped",[20025]],[[63839,63839],"mapped",[23527]],[[63840,63840],"mapped",[24594]],[[63841,63841],"mapped",[29575]],[[63842,63842],"mapped",[30064]],[[63843,63843],"mapped",[21271]],[[63844,63844],"mapped",[30971]],[[63845,63845],"mapped",[20415]],[[63846,63846],"mapped",[24489]],[[63847,63847],"mapped",[19981]],[[63848,63848],"mapped",[27852]],[[63849,63849],"mapped",[25976]],[[63850,63850],"mapped",[32034]],[[63851,63851],"mapped",[21443]],[[63852,63852],"mapped",[22622]],[[63853,63853],"mapped",[30465]],[[63854,63854],"mapped",[33865]],[[63855,63855],"mapped",[35498]],[[63856,63856],"mapped",[27578]],[[63857,63857],"mapped",[36784]],[[63858,63858],"mapped",[27784]],[[63859,63859],"mapped",[25342]],[[63860,63860],"mapped",[33509]],[[63861,63861],"mapped",[25504]],[[63862,63862],"mapped",[30053]],[[63863,63863],"mapped",[20142]],[[63864,63864],"mapped",[20841]],[[63865,63865],"mapped",[20937]],[[63866,63866],"mapped",[26753]],[[63867,63867],"mapped",[31975]],[[63868,63868],"mapped",[33391]],[[63869,63869],"mapped",[35538]],[[63870,63870],"mapped",[37327]],[[63871,63871],"mapped",[21237]],[[63872,63872],"mapped",[21570]],[[63873,63873],"mapped",[22899]],[[63874,63874],"mapped",[24300]],[[63875,63875],"mapped",[26053]],[[63876,63876],"mapped",[28670]],[[63877,63877],"mapped",[31018]],[[63878,63878],"mapped",[38317]],[[63879,63879],"mapped",[39530]],[[63880,63880],"mapped",[40599]],[[63881,63881],"mapped",[40654]],[[63882,63882],"mapped",[21147]],[[63883,63883],"mapped",[26310]],[[63884,63884],"mapped",[27511]],[[63885,63885],"mapped",[36706]],[[63886,63886],"mapped",[24180]],[[63887,63887],"mapped",[24976]],[[63888,63888],"mapped",[25088]],[[63889,63889],"mapped",[25754]],[[63890,63890],"mapped",[28451]],[[63891,63891],"mapped",[29001]],[[63892,63892],"mapped",[29833]],[[63893,63893],"mapped",[31178]],[[63894,63894],"mapped",[32244]],[[63895,63895],"mapped",[32879]],[[63896,63896],"mapped",[36646]],[[63897,63897],"mapped",[34030]],[[63898,63898],"mapped",[36899]],[[63899,63899],"mapped",[37706]],[[63900,63900],"mapped",[21015]],[[63901,63901],"mapped",[21155]],[[63902,63902],"mapped",[21693]],[[63903,63903],"mapped",[28872]],[[63904,63904],"mapped",[35010]],[[63905,63905],"mapped",[35498]],[[63906,63906],"mapped",[24265]],[[63907,63907],"mapped",[24565]],[[63908,63908],"mapped",[25467]],[[63909,63909],"mapped",[27566]],[[63910,63910],"mapped",[31806]],[[63911,63911],"mapped",[29557]],[[63912,63912],"mapped",[20196]],[[63913,63913],"mapped",[22265]],[[63914,63914],"mapped",[23527]],[[63915,63915],"mapped",[23994]],[[63916,63916],"mapped",[24604]],[[63917,63917],"mapped",[29618]],[[63918,63918],"mapped",[29801]],[[63919,63919],"mapped",[32666]],[[63920,63920],"mapped",[32838]],[[63921,63921],"mapped",[37428]],[[63922,63922],"mapped",[38646]],[[63923,63923],"mapped",[38728]],[[63924,63924],"mapped",[38936]],[[63925,63925],"mapped",[20363]],[[63926,63926],"mapped",[31150]],[[63927,63927],"mapped",[37300]],[[63928,63928],"mapped",[38584]],[[63929,63929],"mapped",[24801]],[[63930,63930],"mapped",[20102]],[[63931,63931],"mapped",[20698]],[[63932,63932],"mapped",[23534]],[[63933,63933],"mapped",[23615]],[[63934,63934],"mapped",[26009]],[[63935,63935],"mapped",[27138]],[[63936,63936],"mapped",[29134]],[[63937,63937],"mapped",[30274]],[[63938,63938],"mapped",[34044]],[[63939,63939],"mapped",[36988]],[[63940,63940],"mapped",[40845]],[[63941,63941],"mapped",[26248]],[[63942,63942],"mapped",[38446]],[[63943,63943],"mapped",[21129]],[[63944,63944],"mapped",[26491]],[[63945,63945],"mapped",[26611]],[[63946,63946],"mapped",[27969]],[[63947,63947],"mapped",[28316]],[[63948,63948],"mapped",[29705]],[[63949,63949],"mapped",[30041]],[[63950,63950],"mapped",[30827]],[[63951,63951],"mapped",[32016]],[[63952,63952],"mapped",[39006]],[[63953,63953],"mapped",[20845]],[[63954,63954],"mapped",[25134]],[[63955,63955],"mapped",[38520]],[[63956,63956],"mapped",[20523]],[[63957,63957],"mapped",[23833]],[[63958,63958],"mapped",[28138]],[[63959,63959],"mapped",[36650]],[[63960,63960],"mapped",[24459]],[[63961,63961],"mapped",[24900]],[[63962,63962],"mapped",[26647]],[[63963,63963],"mapped",[29575]],[[63964,63964],"mapped",[38534]],[[63965,63965],"mapped",[21033]],[[63966,63966],"mapped",[21519]],[[63967,63967],"mapped",[23653]],[[63968,63968],"mapped",[26131]],[[63969,63969],"mapped",[26446]],[[63970,63970],"mapped",[26792]],[[63971,63971],"mapped",[27877]],[[63972,63972],"mapped",[29702]],[[63973,63973],"mapped",[30178]],[[63974,63974],"mapped",[32633]],[[63975,63975],"mapped",[35023]],[[63976,63976],"mapped",[35041]],[[63977,63977],"mapped",[37324]],[[63978,63978],"mapped",[38626]],[[63979,63979],"mapped",[21311]],[[63980,63980],"mapped",[28346]],[[63981,63981],"mapped",[21533]],[[63982,63982],"mapped",[29136]],[[63983,63983],"mapped",[29848]],[[63984,63984],"mapped",[34298]],[[63985,63985],"mapped",[38563]],[[63986,63986],"mapped",[40023]],[[63987,63987],"mapped",[40607]],[[63988,63988],"mapped",[26519]],[[63989,63989],"mapped",[28107]],[[63990,63990],"mapped",[33256]],[[63991,63991],"mapped",[31435]],[[63992,63992],"mapped",[31520]],[[63993,63993],"mapped",[31890]],[[63994,63994],"mapped",[29376]],[[63995,63995],"mapped",[28825]],[[63996,63996],"mapped",[35672]],[[63997,63997],"mapped",[20160]],[[63998,63998],"mapped",[33590]],[[63999,63999],"mapped",[21050]],[[64000,64000],"mapped",[20999]],[[64001,64001],"mapped",[24230]],[[64002,64002],"mapped",[25299]],[[64003,64003],"mapped",[31958]],[[64004,64004],"mapped",[23429]],[[64005,64005],"mapped",[27934]],[[64006,64006],"mapped",[26292]],[[64007,64007],"mapped",[36667]],[[64008,64008],"mapped",[34892]],[[64009,64009],"mapped",[38477]],[[64010,64010],"mapped",[35211]],[[64011,64011],"mapped",[24275]],[[64012,64012],"mapped",[20800]],[[64013,64013],"mapped",[21952]],[[64014,64015],"valid"],[[64016,64016],"mapped",[22618]],[[64017,64017],"valid"],[[64018,64018],"mapped",[26228]],[[64019,64020],"valid"],[[64021,64021],"mapped",[20958]],[[64022,64022],"mapped",[29482]],[[64023,64023],"mapped",[30410]],[[64024,64024],"mapped",[31036]],[[64025,64025],"mapped",[31070]],[[64026,64026],"mapped",[31077]],[[64027,64027],"mapped",[31119]],[[64028,64028],"mapped",[38742]],[[64029,64029],"mapped",[31934]],[[64030,64030],"mapped",[32701]],[[64031,64031],"valid"],[[64032,64032],"mapped",[34322]],[[64033,64033],"valid"],[[64034,64034],"mapped",[35576]],[[64035,64036],"valid"],[[64037,64037],"mapped",[36920]],[[64038,64038],"mapped",[37117]],[[64039,64041],"valid"],[[64042,64042],"mapped",[39151]],[[64043,64043],"mapped",[39164]],[[64044,64044],"mapped",[39208]],[[64045,64045],"mapped",[40372]],[[64046,64046],"mapped",[37086]],[[64047,64047],"mapped",[38583]],[[64048,64048],"mapped",[20398]],[[64049,64049],"mapped",[20711]],[[64050,64050],"mapped",[20813]],[[64051,64051],"mapped",[21193]],[[64052,64052],"mapped",[21220]],[[64053,64053],"mapped",[21329]],[[64054,64054],"mapped",[21917]],[[64055,64055],"mapped",[22022]],[[64056,64056],"mapped",[22120]],[[64057,64057],"mapped",[22592]],[[64058,64058],"mapped",[22696]],[[64059,64059],"mapped",[23652]],[[64060,64060],"mapped",[23662]],[[64061,64061],"mapped",[24724]],[[64062,64062],"mapped",[24936]],[[64063,64063],"mapped",[24974]],[[64064,64064],"mapped",[25074]],[[64065,64065],"mapped",[25935]],[[64066,64066],"mapped",[26082]],[[64067,64067],"mapped",[26257]],[[64068,64068],"mapped",[26757]],[[64069,64069],"mapped",[28023]],[[64070,64070],"mapped",[28186]],[[64071,64071],"mapped",[28450]],[[64072,64072],"mapped",[29038]],[[64073,64073],"mapped",[29227]],[[64074,64074],"mapped",[29730]],[[64075,64075],"mapped",[30865]],[[64076,64076],"mapped",[31038]],[[64077,64077],"mapped",[31049]],[[64078,64078],"mapped",[31048]],[[64079,64079],"mapped",[31056]],[[64080,64080],"mapped",[31062]],[[64081,64081],"mapped",[31069]],[[64082,64082],"mapped",[31117]],[[64083,64083],"mapped",[31118]],[[64084,64084],"mapped",[31296]],[[64085,64085],"mapped",[31361]],[[64086,64086],"mapped",[31680]],[[64087,64087],"mapped",[32244]],[[64088,64088],"mapped",[32265]],[[64089,64089],"mapped",[32321]],[[64090,64090],"mapped",[32626]],[[64091,64091],"mapped",[32773]],[[64092,64092],"mapped",[33261]],[[64093,64094],"mapped",[33401]],[[64095,64095],"mapped",[33879]],[[64096,64096],"mapped",[35088]],[[64097,64097],"mapped",[35222]],[[64098,64098],"mapped",[35585]],[[64099,64099],"mapped",[35641]],[[64100,64100],"mapped",[36051]],[[64101,64101],"mapped",[36104]],[[64102,64102],"mapped",[36790]],[[64103,64103],"mapped",[36920]],[[64104,64104],"mapped",[38627]],[[64105,64105],"mapped",[38911]],[[64106,64106],"mapped",[38971]],[[64107,64107],"mapped",[24693]],[[64108,64108],"mapped",[148206]],[[64109,64109],"mapped",[33304]],[[64110,64111],"disallowed"],[[64112,64112],"mapped",[20006]],[[64113,64113],"mapped",[20917]],[[64114,64114],"mapped",[20840]],[[64115,64115],"mapped",[20352]],[[64116,64116],"mapped",[20805]],[[64117,64117],"mapped",[20864]],[[64118,64118],"mapped",[21191]],[[64119,64119],"mapped",[21242]],[[64120,64120],"mapped",[21917]],[[64121,64121],"mapped",[21845]],[[64122,64122],"mapped",[21913]],[[64123,64123],"mapped",[21986]],[[64124,64124],"mapped",[22618]],[[64125,64125],"mapped",[22707]],[[64126,64126],"mapped",[22852]],[[64127,64127],"mapped",[22868]],[[64128,64128],"mapped",[23138]],[[64129,64129],"mapped",[23336]],[[64130,64130],"mapped",[24274]],[[64131,64131],"mapped",[24281]],[[64132,64132],"mapped",[24425]],[[64133,64133],"mapped",[24493]],[[64134,64134],"mapped",[24792]],[[64135,64135],"mapped",[24910]],[[64136,64136],"mapped",[24840]],[[64137,64137],"mapped",[24974]],[[64138,64138],"mapped",[24928]],[[64139,64139],"mapped",[25074]],[[64140,64140],"mapped",[25140]],[[64141,64141],"mapped",[25540]],[[64142,64142],"mapped",[25628]],[[64143,64143],"mapped",[25682]],[[64144,64144],"mapped",[25942]],[[64145,64145],"mapped",[26228]],[[64146,64146],"mapped",[26391]],[[64147,64147],"mapped",[26395]],[[64148,64148],"mapped",[26454]],[[64149,64149],"mapped",[27513]],[[64150,64150],"mapped",[27578]],[[64151,64151],"mapped",[27969]],[[64152,64152],"mapped",[28379]],[[64153,64153],"mapped",[28363]],[[64154,64154],"mapped",[28450]],[[64155,64155],"mapped",[28702]],[[64156,64156],"mapped",[29038]],[[64157,64157],"mapped",[30631]],[[64158,64158],"mapped",[29237]],[[64159,64159],"mapped",[29359]],[[64160,64160],"mapped",[29482]],[[64161,64161],"mapped",[29809]],[[64162,64162],"mapped",[29958]],[[64163,64163],"mapped",[30011]],[[64164,64164],"mapped",[30237]],[[64165,64165],"mapped",[30239]],[[64166,64166],"mapped",[30410]],[[64167,64167],"mapped",[30427]],[[64168,64168],"mapped",[30452]],[[64169,64169],"mapped",[30538]],[[64170,64170],"mapped",[30528]],[[64171,64171],"mapped",[30924]],[[64172,64172],"mapped",[31409]],[[64173,64173],"mapped",[31680]],[[64174,64174],"mapped",[31867]],[[64175,64175],"mapped",[32091]],[[64176,64176],"mapped",[32244]],[[64177,64177],"mapped",[32574]],[[64178,64178],"mapped",[32773]],[[64179,64179],"mapped",[33618]],[[64180,64180],"mapped",[33775]],[[64181,64181],"mapped",[34681]],[[64182,64182],"mapped",[35137]],[[64183,64183],"mapped",[35206]],[[64184,64184],"mapped",[35222]],[[64185,64185],"mapped",[35519]],[[64186,64186],"mapped",[35576]],[[64187,64187],"mapped",[35531]],[[64188,64188],"mapped",[35585]],[[64189,64189],"mapped",[35582]],[[64190,64190],"mapped",[35565]],[[64191,64191],"mapped",[35641]],[[64192,64192],"mapped",[35722]],[[64193,64193],"mapped",[36104]],[[64194,64194],"mapped",[36664]],[[64195,64195],"mapped",[36978]],[[64196,64196],"mapped",[37273]],[[64197,64197],"mapped",[37494]],[[64198,64198],"mapped",[38524]],[[64199,64199],"mapped",[38627]],[[64200,64200],"mapped",[38742]],[[64201,64201],"mapped",[38875]],[[64202,64202],"mapped",[38911]],[[64203,64203],"mapped",[38923]],[[64204,64204],"mapped",[38971]],[[64205,64205],"mapped",[39698]],[[64206,64206],"mapped",[40860]],[[64207,64207],"mapped",[141386]],[[64208,64208],"mapped",[141380]],[[64209,64209],"mapped",[144341]],[[64210,64210],"mapped",[15261]],[[64211,64211],"mapped",[16408]],[[64212,64212],"mapped",[16441]],[[64213,64213],"mapped",[152137]],[[64214,64214],"mapped",[154832]],[[64215,64215],"mapped",[163539]],[[64216,64216],"mapped",[40771]],[[64217,64217],"mapped",[40846]],[[64218,64255],"disallowed"],[[64256,64256],"mapped",[102,102]],[[64257,64257],"mapped",[102,105]],[[64258,64258],"mapped",[102,108]],[[64259,64259],"mapped",[102,102,105]],[[64260,64260],"mapped",[102,102,108]],[[64261,64262],"mapped",[115,116]],[[64263,64274],"disallowed"],[[64275,64275],"mapped",[1396,1398]],[[64276,64276],"mapped",[1396,1381]],[[64277,64277],"mapped",[1396,1387]],[[64278,64278],"mapped",[1406,1398]],[[64279,64279],"mapped",[1396,1389]],[[64280,64284],"disallowed"],[[64285,64285],"mapped",[1497,1460]],[[64286,64286],"valid"],[[64287,64287],"mapped",[1522,1463]],[[64288,64288],"mapped",[1506]],[[64289,64289],"mapped",[1488]],[[64290,64290],"mapped",[1491]],[[64291,64291],"mapped",[1492]],[[64292,64292],"mapped",[1499]],[[64293,64293],"mapped",[1500]],[[64294,64294],"mapped",[1501]],[[64295,64295],"mapped",[1512]],[[64296,64296],"mapped",[1514]],[[64297,64297],"disallowed_STD3_mapped",[43]],[[64298,64298],"mapped",[1513,1473]],[[64299,64299],"mapped",[1513,1474]],[[64300,64300],"mapped",[1513,1468,1473]],[[64301,64301],"mapped",[1513,1468,1474]],[[64302,64302],"mapped",[1488,1463]],[[64303,64303],"mapped",[1488,1464]],[[64304,64304],"mapped",[1488,1468]],[[64305,64305],"mapped",[1489,1468]],[[64306,64306],"mapped",[1490,1468]],[[64307,64307],"mapped",[1491,1468]],[[64308,64308],"mapped",[1492,1468]],[[64309,64309],"mapped",[1493,1468]],[[64310,64310],"mapped",[1494,1468]],[[64311,64311],"disallowed"],[[64312,64312],"mapped",[1496,1468]],[[64313,64313],"mapped",[1497,1468]],[[64314,64314],"mapped",[1498,1468]],[[64315,64315],"mapped",[1499,1468]],[[64316,64316],"mapped",[1500,1468]],[[64317,64317],"disallowed"],[[64318,64318],"mapped",[1502,1468]],[[64319,64319],"disallowed"],[[64320,64320],"mapped",[1504,1468]],[[64321,64321],"mapped",[1505,1468]],[[64322,64322],"disallowed"],[[64323,64323],"mapped",[1507,1468]],[[64324,64324],"mapped",[1508,1468]],[[64325,64325],"disallowed"],[[64326,64326],"mapped",[1510,1468]],[[64327,64327],"mapped",[1511,1468]],[[64328,64328],"mapped",[1512,1468]],[[64329,64329],"mapped",[1513,1468]],[[64330,64330],"mapped",[1514,1468]],[[64331,64331],"mapped",[1493,1465]],[[64332,64332],"mapped",[1489,1471]],[[64333,64333],"mapped",[1499,1471]],[[64334,64334],"mapped",[1508,1471]],[[64335,64335],"mapped",[1488,1500]],[[64336,64337],"mapped",[1649]],[[64338,64341],"mapped",[1659]],[[64342,64345],"mapped",[1662]],[[64346,64349],"mapped",[1664]],[[64350,64353],"mapped",[1658]],[[64354,64357],"mapped",[1663]],[[64358,64361],"mapped",[1657]],[[64362,64365],"mapped",[1700]],[[64366,64369],"mapped",[1702]],[[64370,64373],"mapped",[1668]],[[64374,64377],"mapped",[1667]],[[64378,64381],"mapped",[1670]],[[64382,64385],"mapped",[1671]],[[64386,64387],"mapped",[1677]],[[64388,64389],"mapped",[1676]],[[64390,64391],"mapped",[1678]],[[64392,64393],"mapped",[1672]],[[64394,64395],"mapped",[1688]],[[64396,64397],"mapped",[1681]],[[64398,64401],"mapped",[1705]],[[64402,64405],"mapped",[1711]],[[64406,64409],"mapped",[1715]],[[64410,64413],"mapped",[1713]],[[64414,64415],"mapped",[1722]],[[64416,64419],"mapped",[1723]],[[64420,64421],"mapped",[1728]],[[64422,64425],"mapped",[1729]],[[64426,64429],"mapped",[1726]],[[64430,64431],"mapped",[1746]],[[64432,64433],"mapped",[1747]],[[64434,64449],"valid",[],"NV8"],[[64450,64466],"disallowed"],[[64467,64470],"mapped",[1709]],[[64471,64472],"mapped",[1735]],[[64473,64474],"mapped",[1734]],[[64475,64476],"mapped",[1736]],[[64477,64477],"mapped",[1735,1652]],[[64478,64479],"mapped",[1739]],[[64480,64481],"mapped",[1733]],[[64482,64483],"mapped",[1737]],[[64484,64487],"mapped",[1744]],[[64488,64489],"mapped",[1609]],[[64490,64491],"mapped",[1574,1575]],[[64492,64493],"mapped",[1574,1749]],[[64494,64495],"mapped",[1574,1608]],[[64496,64497],"mapped",[1574,1735]],[[64498,64499],"mapped",[1574,1734]],[[64500,64501],"mapped",[1574,1736]],[[64502,64504],"mapped",[1574,1744]],[[64505,64507],"mapped",[1574,1609]],[[64508,64511],"mapped",[1740]],[[64512,64512],"mapped",[1574,1580]],[[64513,64513],"mapped",[1574,1581]],[[64514,64514],"mapped",[1574,1605]],[[64515,64515],"mapped",[1574,1609]],[[64516,64516],"mapped",[1574,1610]],[[64517,64517],"mapped",[1576,1580]],[[64518,64518],"mapped",[1576,1581]],[[64519,64519],"mapped",[1576,1582]],[[64520,64520],"mapped",[1576,1605]],[[64521,64521],"mapped",[1576,1609]],[[64522,64522],"mapped",[1576,1610]],[[64523,64523],"mapped",[1578,1580]],[[64524,64524],"mapped",[1578,1581]],[[64525,64525],"mapped",[1578,1582]],[[64526,64526],"mapped",[1578,1605]],[[64527,64527],"mapped",[1578,1609]],[[64528,64528],"mapped",[1578,1610]],[[64529,64529],"mapped",[1579,1580]],[[64530,64530],"mapped",[1579,1605]],[[64531,64531],"mapped",[1579,1609]],[[64532,64532],"mapped",[1579,1610]],[[64533,64533],"mapped",[1580,1581]],[[64534,64534],"mapped",[1580,1605]],[[64535,64535],"mapped",[1581,1580]],[[64536,64536],"mapped",[1581,1605]],[[64537,64537],"mapped",[1582,1580]],[[64538,64538],"mapped",[1582,1581]],[[64539,64539],"mapped",[1582,1605]],[[64540,64540],"mapped",[1587,1580]],[[64541,64541],"mapped",[1587,1581]],[[64542,64542],"mapped",[1587,1582]],[[64543,64543],"mapped",[1587,1605]],[[64544,64544],"mapped",[1589,1581]],[[64545,64545],"mapped",[1589,1605]],[[64546,64546],"mapped",[1590,1580]],[[64547,64547],"mapped",[1590,1581]],[[64548,64548],"mapped",[1590,1582]],[[64549,64549],"mapped",[1590,1605]],[[64550,64550],"mapped",[1591,1581]],[[64551,64551],"mapped",[1591,1605]],[[64552,64552],"mapped",[1592,1605]],[[64553,64553],"mapped",[1593,1580]],[[64554,64554],"mapped",[1593,1605]],[[64555,64555],"mapped",[1594,1580]],[[64556,64556],"mapped",[1594,1605]],[[64557,64557],"mapped",[1601,1580]],[[64558,64558],"mapped",[1601,1581]],[[64559,64559],"mapped",[1601,1582]],[[64560,64560],"mapped",[1601,1605]],[[64561,64561],"mapped",[1601,1609]],[[64562,64562],"mapped",[1601,1610]],[[64563,64563],"mapped",[1602,1581]],[[64564,64564],"mapped",[1602,1605]],[[64565,64565],"mapped",[1602,1609]],[[64566,64566],"mapped",[1602,1610]],[[64567,64567],"mapped",[1603,1575]],[[64568,64568],"mapped",[1603,1580]],[[64569,64569],"mapped",[1603,1581]],[[64570,64570],"mapped",[1603,1582]],[[64571,64571],"mapped",[1603,1604]],[[64572,64572],"mapped",[1603,1605]],[[64573,64573],"mapped",[1603,1609]],[[64574,64574],"mapped",[1603,1610]],[[64575,64575],"mapped",[1604,1580]],[[64576,64576],"mapped",[1604,1581]],[[64577,64577],"mapped",[1604,1582]],[[64578,64578],"mapped",[1604,1605]],[[64579,64579],"mapped",[1604,1609]],[[64580,64580],"mapped",[1604,1610]],[[64581,64581],"mapped",[1605,1580]],[[64582,64582],"mapped",[1605,1581]],[[64583,64583],"mapped",[1605,1582]],[[64584,64584],"mapped",[1605,1605]],[[64585,64585],"mapped",[1605,1609]],[[64586,64586],"mapped",[1605,1610]],[[64587,64587],"mapped",[1606,1580]],[[64588,64588],"mapped",[1606,1581]],[[64589,64589],"mapped",[1606,1582]],[[64590,64590],"mapped",[1606,1605]],[[64591,64591],"mapped",[1606,1609]],[[64592,64592],"mapped",[1606,1610]],[[64593,64593],"mapped",[1607,1580]],[[64594,64594],"mapped",[1607,1605]],[[64595,64595],"mapped",[1607,1609]],[[64596,64596],"mapped",[1607,1610]],[[64597,64597],"mapped",[1610,1580]],[[64598,64598],"mapped",[1610,1581]],[[64599,64599],"mapped",[1610,1582]],[[64600,64600],"mapped",[1610,1605]],[[64601,64601],"mapped",[1610,1609]],[[64602,64602],"mapped",[1610,1610]],[[64603,64603],"mapped",[1584,1648]],[[64604,64604],"mapped",[1585,1648]],[[64605,64605],"mapped",[1609,1648]],[[64606,64606],"disallowed_STD3_mapped",[32,1612,1617]],[[64607,64607],"disallowed_STD3_mapped",[32,1613,1617]],[[64608,64608],"disallowed_STD3_mapped",[32,1614,1617]],[[64609,64609],"disallowed_STD3_mapped",[32,1615,1617]],[[64610,64610],"disallowed_STD3_mapped",[32,1616,1617]],[[64611,64611],"disallowed_STD3_mapped",[32,1617,1648]],[[64612,64612],"mapped",[1574,1585]],[[64613,64613],"mapped",[1574,1586]],[[64614,64614],"mapped",[1574,1605]],[[64615,64615],"mapped",[1574,1606]],[[64616,64616],"mapped",[1574,1609]],[[64617,64617],"mapped",[1574,1610]],[[64618,64618],"mapped",[1576,1585]],[[64619,64619],"mapped",[1576,1586]],[[64620,64620],"mapped",[1576,1605]],[[64621,64621],"mapped",[1576,1606]],[[64622,64622],"mapped",[1576,1609]],[[64623,64623],"mapped",[1576,1610]],[[64624,64624],"mapped",[1578,1585]],[[64625,64625],"mapped",[1578,1586]],[[64626,64626],"mapped",[1578,1605]],[[64627,64627],"mapped",[1578,1606]],[[64628,64628],"mapped",[1578,1609]],[[64629,64629],"mapped",[1578,1610]],[[64630,64630],"mapped",[1579,1585]],[[64631,64631],"mapped",[1579,1586]],[[64632,64632],"mapped",[1579,1605]],[[64633,64633],"mapped",[1579,1606]],[[64634,64634],"mapped",[1579,1609]],[[64635,64635],"mapped",[1579,1610]],[[64636,64636],"mapped",[1601,1609]],[[64637,64637],"mapped",[1601,1610]],[[64638,64638],"mapped",[1602,1609]],[[64639,64639],"mapped",[1602,1610]],[[64640,64640],"mapped",[1603,1575]],[[64641,64641],"mapped",[1603,1604]],[[64642,64642],"mapped",[1603,1605]],[[64643,64643],"mapped",[1603,1609]],[[64644,64644],"mapped",[1603,1610]],[[64645,64645],"mapped",[1604,1605]],[[64646,64646],"mapped",[1604,1609]],[[64647,64647],"mapped",[1604,1610]],[[64648,64648],"mapped",[1605,1575]],[[64649,64649],"mapped",[1605,1605]],[[64650,64650],"mapped",[1606,1585]],[[64651,64651],"mapped",[1606,1586]],[[64652,64652],"mapped",[1606,1605]],[[64653,64653],"mapped",[1606,1606]],[[64654,64654],"mapped",[1606,1609]],[[64655,64655],"mapped",[1606,1610]],[[64656,64656],"mapped",[1609,1648]],[[64657,64657],"mapped",[1610,1585]],[[64658,64658],"mapped",[1610,1586]],[[64659,64659],"mapped",[1610,1605]],[[64660,64660],"mapped",[1610,1606]],[[64661,64661],"mapped",[1610,1609]],[[64662,64662],"mapped",[1610,1610]],[[64663,64663],"mapped",[1574,1580]],[[64664,64664],"mapped",[1574,1581]],[[64665,64665],"mapped",[1574,1582]],[[64666,64666],"mapped",[1574,1605]],[[64667,64667],"mapped",[1574,1607]],[[64668,64668],"mapped",[1576,1580]],[[64669,64669],"mapped",[1576,1581]],[[64670,64670],"mapped",[1576,1582]],[[64671,64671],"mapped",[1576,1605]],[[64672,64672],"mapped",[1576,1607]],[[64673,64673],"mapped",[1578,1580]],[[64674,64674],"mapped",[1578,1581]],[[64675,64675],"mapped",[1578,1582]],[[64676,64676],"mapped",[1578,1605]],[[64677,64677],"mapped",[1578,1607]],[[64678,64678],"mapped",[1579,1605]],[[64679,64679],"mapped",[1580,1581]],[[64680,64680],"mapped",[1580,1605]],[[64681,64681],"mapped",[1581,1580]],[[64682,64682],"mapped",[1581,1605]],[[64683,64683],"mapped",[1582,1580]],[[64684,64684],"mapped",[1582,1605]],[[64685,64685],"mapped",[1587,1580]],[[64686,64686],"mapped",[1587,1581]],[[64687,64687],"mapped",[1587,1582]],[[64688,64688],"mapped",[1587,1605]],[[64689,64689],"mapped",[1589,1581]],[[64690,64690],"mapped",[1589,1582]],[[64691,64691],"mapped",[1589,1605]],[[64692,64692],"mapped",[1590,1580]],[[64693,64693],"mapped",[1590,1581]],[[64694,64694],"mapped",[1590,1582]],[[64695,64695],"mapped",[1590,1605]],[[64696,64696],"mapped",[1591,1581]],[[64697,64697],"mapped",[1592,1605]],[[64698,64698],"mapped",[1593,1580]],[[64699,64699],"mapped",[1593,1605]],[[64700,64700],"mapped",[1594,1580]],[[64701,64701],"mapped",[1594,1605]],[[64702,64702],"mapped",[1601,1580]],[[64703,64703],"mapped",[1601,1581]],[[64704,64704],"mapped",[1601,1582]],[[64705,64705],"mapped",[1601,1605]],[[64706,64706],"mapped",[1602,1581]],[[64707,64707],"mapped",[1602,1605]],[[64708,64708],"mapped",[1603,1580]],[[64709,64709],"mapped",[1603,1581]],[[64710,64710],"mapped",[1603,1582]],[[64711,64711],"mapped",[1603,1604]],[[64712,64712],"mapped",[1603,1605]],[[64713,64713],"mapped",[1604,1580]],[[64714,64714],"mapped",[1604,1581]],[[64715,64715],"mapped",[1604,1582]],[[64716,64716],"mapped",[1604,1605]],[[64717,64717],"mapped",[1604,1607]],[[64718,64718],"mapped",[1605,1580]],[[64719,64719],"mapped",[1605,1581]],[[64720,64720],"mapped",[1605,1582]],[[64721,64721],"mapped",[1605,1605]],[[64722,64722],"mapped",[1606,1580]],[[64723,64723],"mapped",[1606,1581]],[[64724,64724],"mapped",[1606,1582]],[[64725,64725],"mapped",[1606,1605]],[[64726,64726],"mapped",[1606,1607]],[[64727,64727],"mapped",[1607,1580]],[[64728,64728],"mapped",[1607,1605]],[[64729,64729],"mapped",[1607,1648]],[[64730,64730],"mapped",[1610,1580]],[[64731,64731],"mapped",[1610,1581]],[[64732,64732],"mapped",[1610,1582]],[[64733,64733],"mapped",[1610,1605]],[[64734,64734],"mapped",[1610,1607]],[[64735,64735],"mapped",[1574,1605]],[[64736,64736],"mapped",[1574,1607]],[[64737,64737],"mapped",[1576,1605]],[[64738,64738],"mapped",[1576,1607]],[[64739,64739],"mapped",[1578,1605]],[[64740,64740],"mapped",[1578,1607]],[[64741,64741],"mapped",[1579,1605]],[[64742,64742],"mapped",[1579,1607]],[[64743,64743],"mapped",[1587,1605]],[[64744,64744],"mapped",[1587,1607]],[[64745,64745],"mapped",[1588,1605]],[[64746,64746],"mapped",[1588,1607]],[[64747,64747],"mapped",[1603,1604]],[[64748,64748],"mapped",[1603,1605]],[[64749,64749],"mapped",[1604,1605]],[[64750,64750],"mapped",[1606,1605]],[[64751,64751],"mapped",[1606,1607]],[[64752,64752],"mapped",[1610,1605]],[[64753,64753],"mapped",[1610,1607]],[[64754,64754],"mapped",[1600,1614,1617]],[[64755,64755],"mapped",[1600,1615,1617]],[[64756,64756],"mapped",[1600,1616,1617]],[[64757,64757],"mapped",[1591,1609]],[[64758,64758],"mapped",[1591,1610]],[[64759,64759],"mapped",[1593,1609]],[[64760,64760],"mapped",[1593,1610]],[[64761,64761],"mapped",[1594,1609]],[[64762,64762],"mapped",[1594,1610]],[[64763,64763],"mapped",[1587,1609]],[[64764,64764],"mapped",[1587,1610]],[[64765,64765],"mapped",[1588,1609]],[[64766,64766],"mapped",[1588,1610]],[[64767,64767],"mapped",[1581,1609]],[[64768,64768],"mapped",[1581,1610]],[[64769,64769],"mapped",[1580,1609]],[[64770,64770],"mapped",[1580,1610]],[[64771,64771],"mapped",[1582,1609]],[[64772,64772],"mapped",[1582,1610]],[[64773,64773],"mapped",[1589,1609]],[[64774,64774],"mapped",[1589,1610]],[[64775,64775],"mapped",[1590,1609]],[[64776,64776],"mapped",[1590,1610]],[[64777,64777],"mapped",[1588,1580]],[[64778,64778],"mapped",[1588,1581]],[[64779,64779],"mapped",[1588,1582]],[[64780,64780],"mapped",[1588,1605]],[[64781,64781],"mapped",[1588,1585]],[[64782,64782],"mapped",[1587,1585]],[[64783,64783],"mapped",[1589,1585]],[[64784,64784],"mapped",[1590,1585]],[[64785,64785],"mapped",[1591,1609]],[[64786,64786],"mapped",[1591,1610]],[[64787,64787],"mapped",[1593,1609]],[[64788,64788],"mapped",[1593,1610]],[[64789,64789],"mapped",[1594,1609]],[[64790,64790],"mapped",[1594,1610]],[[64791,64791],"mapped",[1587,1609]],[[64792,64792],"mapped",[1587,1610]],[[64793,64793],"mapped",[1588,1609]],[[64794,64794],"mapped",[1588,1610]],[[64795,64795],"mapped",[1581,1609]],[[64796,64796],"mapped",[1581,1610]],[[64797,64797],"mapped",[1580,1609]],[[64798,64798],"mapped",[1580,1610]],[[64799,64799],"mapped",[1582,1609]],[[64800,64800],"mapped",[1582,1610]],[[64801,64801],"mapped",[1589,1609]],[[64802,64802],"mapped",[1589,1610]],[[64803,64803],"mapped",[1590,1609]],[[64804,64804],"mapped",[1590,1610]],[[64805,64805],"mapped",[1588,1580]],[[64806,64806],"mapped",[1588,1581]],[[64807,64807],"mapped",[1588,1582]],[[64808,64808],"mapped",[1588,1605]],[[64809,64809],"mapped",[1588,1585]],[[64810,64810],"mapped",[1587,1585]],[[64811,64811],"mapped",[1589,1585]],[[64812,64812],"mapped",[1590,1585]],[[64813,64813],"mapped",[1588,1580]],[[64814,64814],"mapped",[1588,1581]],[[64815,64815],"mapped",[1588,1582]],[[64816,64816],"mapped",[1588,1605]],[[64817,64817],"mapped",[1587,1607]],[[64818,64818],"mapped",[1588,1607]],[[64819,64819],"mapped",[1591,1605]],[[64820,64820],"mapped",[1587,1580]],[[64821,64821],"mapped",[1587,1581]],[[64822,64822],"mapped",[1587,1582]],[[64823,64823],"mapped",[1588,1580]],[[64824,64824],"mapped",[1588,1581]],[[64825,64825],"mapped",[1588,1582]],[[64826,64826],"mapped",[1591,1605]],[[64827,64827],"mapped",[1592,1605]],[[64828,64829],"mapped",[1575,1611]],[[64830,64831],"valid",[],"NV8"],[[64832,64847],"disallowed"],[[64848,64848],"mapped",[1578,1580,1605]],[[64849,64850],"mapped",[1578,1581,1580]],[[64851,64851],"mapped",[1578,1581,1605]],[[64852,64852],"mapped",[1578,1582,1605]],[[64853,64853],"mapped",[1578,1605,1580]],[[64854,64854],"mapped",[1578,1605,1581]],[[64855,64855],"mapped",[1578,1605,1582]],[[64856,64857],"mapped",[1580,1605,1581]],[[64858,64858],"mapped",[1581,1605,1610]],[[64859,64859],"mapped",[1581,1605,1609]],[[64860,64860],"mapped",[1587,1581,1580]],[[64861,64861],"mapped",[1587,1580,1581]],[[64862,64862],"mapped",[1587,1580,1609]],[[64863,64864],"mapped",[1587,1605,1581]],[[64865,64865],"mapped",[1587,1605,1580]],[[64866,64867],"mapped",[1587,1605,1605]],[[64868,64869],"mapped",[1589,1581,1581]],[[64870,64870],"mapped",[1589,1605,1605]],[[64871,64872],"mapped",[1588,1581,1605]],[[64873,64873],"mapped",[1588,1580,1610]],[[64874,64875],"mapped",[1588,1605,1582]],[[64876,64877],"mapped",[1588,1605,1605]],[[64878,64878],"mapped",[1590,1581,1609]],[[64879,64880],"mapped",[1590,1582,1605]],[[64881,64882],"mapped",[1591,1605,1581]],[[64883,64883],"mapped",[1591,1605,1605]],[[64884,64884],"mapped",[1591,1605,1610]],[[64885,64885],"mapped",[1593,1580,1605]],[[64886,64887],"mapped",[1593,1605,1605]],[[64888,64888],"mapped",[1593,1605,1609]],[[64889,64889],"mapped",[1594,1605,1605]],[[64890,64890],"mapped",[1594,1605,1610]],[[64891,64891],"mapped",[1594,1605,1609]],[[64892,64893],"mapped",[1601,1582,1605]],[[64894,64894],"mapped",[1602,1605,1581]],[[64895,64895],"mapped",[1602,1605,1605]],[[64896,64896],"mapped",[1604,1581,1605]],[[64897,64897],"mapped",[1604,1581,1610]],[[64898,64898],"mapped",[1604,1581,1609]],[[64899,64900],"mapped",[1604,1580,1580]],[[64901,64902],"mapped",[1604,1582,1605]],[[64903,64904],"mapped",[1604,1605,1581]],[[64905,64905],"mapped",[1605,1581,1580]],[[64906,64906],"mapped",[1605,1581,1605]],[[64907,64907],"mapped",[1605,1581,1610]],[[64908,64908],"mapped",[1605,1580,1581]],[[64909,64909],"mapped",[1605,1580,1605]],[[64910,64910],"mapped",[1605,1582,1580]],[[64911,64911],"mapped",[1605,1582,1605]],[[64912,64913],"disallowed"],[[64914,64914],"mapped",[1605,1580,1582]],[[64915,64915],"mapped",[1607,1605,1580]],[[64916,64916],"mapped",[1607,1605,1605]],[[64917,64917],"mapped",[1606,1581,1605]],[[64918,64918],"mapped",[1606,1581,1609]],[[64919,64920],"mapped",[1606,1580,1605]],[[64921,64921],"mapped",[1606,1580,1609]],[[64922,64922],"mapped",[1606,1605,1610]],[[64923,64923],"mapped",[1606,1605,1609]],[[64924,64925],"mapped",[1610,1605,1605]],[[64926,64926],"mapped",[1576,1582,1610]],[[64927,64927],"mapped",[1578,1580,1610]],[[64928,64928],"mapped",[1578,1580,1609]],[[64929,64929],"mapped",[1578,1582,1610]],[[64930,64930],"mapped",[1578,1582,1609]],[[64931,64931],"mapped",[1578,1605,1610]],[[64932,64932],"mapped",[1578,1605,1609]],[[64933,64933],"mapped",[1580,1605,1610]],[[64934,64934],"mapped",[1580,1581,1609]],[[64935,64935],"mapped",[1580,1605,1609]],[[64936,64936],"mapped",[1587,1582,1609]],[[64937,64937],"mapped",[1589,1581,1610]],[[64938,64938],"mapped",[1588,1581,1610]],[[64939,64939],"mapped",[1590,1581,1610]],[[64940,64940],"mapped",[1604,1580,1610]],[[64941,64941],"mapped",[1604,1605,1610]],[[64942,64942],"mapped",[1610,1581,1610]],[[64943,64943],"mapped",[1610,1580,1610]],[[64944,64944],"mapped",[1610,1605,1610]],[[64945,64945],"mapped",[1605,1605,1610]],[[64946,64946],"mapped",[1602,1605,1610]],[[64947,64947],"mapped",[1606,1581,1610]],[[64948,64948],"mapped",[1602,1605,1581]],[[64949,64949],"mapped",[1604,1581,1605]],[[64950,64950],"mapped",[1593,1605,1610]],[[64951,64951],"mapped",[1603,1605,1610]],[[64952,64952],"mapped",[1606,1580,1581]],[[64953,64953],"mapped",[1605,1582,1610]],[[64954,64954],"mapped",[1604,1580,1605]],[[64955,64955],"mapped",[1603,1605,1605]],[[64956,64956],"mapped",[1604,1580,1605]],[[64957,64957],"mapped",[1606,1580,1581]],[[64958,64958],"mapped",[1580,1581,1610]],[[64959,64959],"mapped",[1581,1580,1610]],[[64960,64960],"mapped",[1605,1580,1610]],[[64961,64961],"mapped",[1601,1605,1610]],[[64962,64962],"mapped",[1576,1581,1610]],[[64963,64963],"mapped",[1603,1605,1605]],[[64964,64964],"mapped",[1593,1580,1605]],[[64965,64965],"mapped",[1589,1605,1605]],[[64966,64966],"mapped",[1587,1582,1610]],[[64967,64967],"mapped",[1606,1580,1610]],[[64968,64975],"disallowed"],[[64976,65007],"disallowed"],[[65008,65008],"mapped",[1589,1604,1746]],[[65009,65009],"mapped",[1602,1604,1746]],[[65010,65010],"mapped",[1575,1604,1604,1607]],[[65011,65011],"mapped",[1575,1603,1576,1585]],[[65012,65012],"mapped",[1605,1581,1605,1583]],[[65013,65013],"mapped",[1589,1604,1593,1605]],[[65014,65014],"mapped",[1585,1587,1608,1604]],[[65015,65015],"mapped",[1593,1604,1610,1607]],[[65016,65016],"mapped",[1608,1587,1604,1605]],[[65017,65017],"mapped",[1589,1604,1609]],[[65018,65018],"disallowed_STD3_mapped",[1589,1604,1609,32,1575,1604,1604,1607,32,1593,1604,1610,1607,32,1608,1587,1604,1605]],[[65019,65019],"disallowed_STD3_mapped",[1580,1604,32,1580,1604,1575,1604,1607]],[[65020,65020],"mapped",[1585,1740,1575,1604]],[[65021,65021],"valid",[],"NV8"],[[65022,65023],"disallowed"],[[65024,65039],"ignored"],[[65040,65040],"disallowed_STD3_mapped",[44]],[[65041,65041],"mapped",[12289]],[[65042,65042],"disallowed"],[[65043,65043],"disallowed_STD3_mapped",[58]],[[65044,65044],"disallowed_STD3_mapped",[59]],[[65045,65045],"disallowed_STD3_mapped",[33]],[[65046,65046],"disallowed_STD3_mapped",[63]],[[65047,65047],"mapped",[12310]],[[65048,65048],"mapped",[12311]],[[65049,65049],"disallowed"],[[65050,65055],"disallowed"],[[65056,65059],"valid"],[[65060,65062],"valid"],[[65063,65069],"valid"],[[65070,65071],"valid"],[[65072,65072],"disallowed"],[[65073,65073],"mapped",[8212]],[[65074,65074],"mapped",[8211]],[[65075,65076],"disallowed_STD3_mapped",[95]],[[65077,65077],"disallowed_STD3_mapped",[40]],[[65078,65078],"disallowed_STD3_mapped",[41]],[[65079,65079],"disallowed_STD3_mapped",[123]],[[65080,65080],"disallowed_STD3_mapped",[125]],[[65081,65081],"mapped",[12308]],[[65082,65082],"mapped",[12309]],[[65083,65083],"mapped",[12304]],[[65084,65084],"mapped",[12305]],[[65085,65085],"mapped",[12298]],[[65086,65086],"mapped",[12299]],[[65087,65087],"mapped",[12296]],[[65088,65088],"mapped",[12297]],[[65089,65089],"mapped",[12300]],[[65090,65090],"mapped",[12301]],[[65091,65091],"mapped",[12302]],[[65092,65092],"mapped",[12303]],[[65093,65094],"valid",[],"NV8"],[[65095,65095],"disallowed_STD3_mapped",[91]],[[65096,65096],"disallowed_STD3_mapped",[93]],[[65097,65100],"disallowed_STD3_mapped",[32,773]],[[65101,65103],"disallowed_STD3_mapped",[95]],[[65104,65104],"disallowed_STD3_mapped",[44]],[[65105,65105],"mapped",[12289]],[[65106,65106],"disallowed"],[[65107,65107],"disallowed"],[[65108,65108],"disallowed_STD3_mapped",[59]],[[65109,65109],"disallowed_STD3_mapped",[58]],[[65110,65110],"disallowed_STD3_mapped",[63]],[[65111,65111],"disallowed_STD3_mapped",[33]],[[65112,65112],"mapped",[8212]],[[65113,65113],"disallowed_STD3_mapped",[40]],[[65114,65114],"disallowed_STD3_mapped",[41]],[[65115,65115],"disallowed_STD3_mapped",[123]],[[65116,65116],"disallowed_STD3_mapped",[125]],[[65117,65117],"mapped",[12308]],[[65118,65118],"mapped",[12309]],[[65119,65119],"disallowed_STD3_mapped",[35]],[[65120,65120],"disallowed_STD3_mapped",[38]],[[65121,65121],"disallowed_STD3_mapped",[42]],[[65122,65122],"disallowed_STD3_mapped",[43]],[[65123,65123],"mapped",[45]],[[65124,65124],"disallowed_STD3_mapped",[60]],[[65125,65125],"disallowed_STD3_mapped",[62]],[[65126,65126],"disallowed_STD3_mapped",[61]],[[65127,65127],"disallowed"],[[65128,65128],"disallowed_STD3_mapped",[92]],[[65129,65129],"disallowed_STD3_mapped",[36]],[[65130,65130],"disallowed_STD3_mapped",[37]],[[65131,65131],"disallowed_STD3_mapped",[64]],[[65132,65135],"disallowed"],[[65136,65136],"disallowed_STD3_mapped",[32,1611]],[[65137,65137],"mapped",[1600,1611]],[[65138,65138],"disallowed_STD3_mapped",[32,1612]],[[65139,65139],"valid"],[[65140,65140],"disallowed_STD3_mapped",[32,1613]],[[65141,65141],"disallowed"],[[65142,65142],"disallowed_STD3_mapped",[32,1614]],[[65143,65143],"mapped",[1600,1614]],[[65144,65144],"disallowed_STD3_mapped",[32,1615]],[[65145,65145],"mapped",[1600,1615]],[[65146,65146],"disallowed_STD3_mapped",[32,1616]],[[65147,65147],"mapped",[1600,1616]],[[65148,65148],"disallowed_STD3_mapped",[32,1617]],[[65149,65149],"mapped",[1600,1617]],[[65150,65150],"disallowed_STD3_mapped",[32,1618]],[[65151,65151],"mapped",[1600,1618]],[[65152,65152],"mapped",[1569]],[[65153,65154],"mapped",[1570]],[[65155,65156],"mapped",[1571]],[[65157,65158],"mapped",[1572]],[[65159,65160],"mapped",[1573]],[[65161,65164],"mapped",[1574]],[[65165,65166],"mapped",[1575]],[[65167,65170],"mapped",[1576]],[[65171,65172],"mapped",[1577]],[[65173,65176],"mapped",[1578]],[[65177,65180],"mapped",[1579]],[[65181,65184],"mapped",[1580]],[[65185,65188],"mapped",[1581]],[[65189,65192],"mapped",[1582]],[[65193,65194],"mapped",[1583]],[[65195,65196],"mapped",[1584]],[[65197,65198],"mapped",[1585]],[[65199,65200],"mapped",[1586]],[[65201,65204],"mapped",[1587]],[[65205,65208],"mapped",[1588]],[[65209,65212],"mapped",[1589]],[[65213,65216],"mapped",[1590]],[[65217,65220],"mapped",[1591]],[[65221,65224],"mapped",[1592]],[[65225,65228],"mapped",[1593]],[[65229,65232],"mapped",[1594]],[[65233,65236],"mapped",[1601]],[[65237,65240],"mapped",[1602]],[[65241,65244],"mapped",[1603]],[[65245,65248],"mapped",[1604]],[[65249,65252],"mapped",[1605]],[[65253,65256],"mapped",[1606]],[[65257,65260],"mapped",[1607]],[[65261,65262],"mapped",[1608]],[[65263,65264],"mapped",[1609]],[[65265,65268],"mapped",[1610]],[[65269,65270],"mapped",[1604,1570]],[[65271,65272],"mapped",[1604,1571]],[[65273,65274],"mapped",[1604,1573]],[[65275,65276],"mapped",[1604,1575]],[[65277,65278],"disallowed"],[[65279,65279],"ignored"],[[65280,65280],"disallowed"],[[65281,65281],"disallowed_STD3_mapped",[33]],[[65282,65282],"disallowed_STD3_mapped",[34]],[[65283,65283],"disallowed_STD3_mapped",[35]],[[65284,65284],"disallowed_STD3_mapped",[36]],[[65285,65285],"disallowed_STD3_mapped",[37]],[[65286,65286],"disallowed_STD3_mapped",[38]],[[65287,65287],"disallowed_STD3_mapped",[39]],[[65288,65288],"disallowed_STD3_mapped",[40]],[[65289,65289],"disallowed_STD3_mapped",[41]],[[65290,65290],"disallowed_STD3_mapped",[42]],[[65291,65291],"disallowed_STD3_mapped",[43]],[[65292,65292],"disallowed_STD3_mapped",[44]],[[65293,65293],"mapped",[45]],[[65294,65294],"mapped",[46]],[[65295,65295],"disallowed_STD3_mapped",[47]],[[65296,65296],"mapped",[48]],[[65297,65297],"mapped",[49]],[[65298,65298],"mapped",[50]],[[65299,65299],"mapped",[51]],[[65300,65300],"mapped",[52]],[[65301,65301],"mapped",[53]],[[65302,65302],"mapped",[54]],[[65303,65303],"mapped",[55]],[[65304,65304],"mapped",[56]],[[65305,65305],"mapped",[57]],[[65306,65306],"disallowed_STD3_mapped",[58]],[[65307,65307],"disallowed_STD3_mapped",[59]],[[65308,65308],"disallowed_STD3_mapped",[60]],[[65309,65309],"disallowed_STD3_mapped",[61]],[[65310,65310],"disallowed_STD3_mapped",[62]],[[65311,65311],"disallowed_STD3_mapped",[63]],[[65312,65312],"disallowed_STD3_mapped",[64]],[[65313,65313],"mapped",[97]],[[65314,65314],"mapped",[98]],[[65315,65315],"mapped",[99]],[[65316,65316],"mapped",[100]],[[65317,65317],"mapped",[101]],[[65318,65318],"mapped",[102]],[[65319,65319],"mapped",[103]],[[65320,65320],"mapped",[104]],[[65321,65321],"mapped",[105]],[[65322,65322],"mapped",[106]],[[65323,65323],"mapped",[107]],[[65324,65324],"mapped",[108]],[[65325,65325],"mapped",[109]],[[65326,65326],"mapped",[110]],[[65327,65327],"mapped",[111]],[[65328,65328],"mapped",[112]],[[65329,65329],"mapped",[113]],[[65330,65330],"mapped",[114]],[[65331,65331],"mapped",[115]],[[65332,65332],"mapped",[116]],[[65333,65333],"mapped",[117]],[[65334,65334],"mapped",[118]],[[65335,65335],"mapped",[119]],[[65336,65336],"mapped",[120]],[[65337,65337],"mapped",[121]],[[65338,65338],"mapped",[122]],[[65339,65339],"disallowed_STD3_mapped",[91]],[[65340,65340],"disallowed_STD3_mapped",[92]],[[65341,65341],"disallowed_STD3_mapped",[93]],[[65342,65342],"disallowed_STD3_mapped",[94]],[[65343,65343],"disallowed_STD3_mapped",[95]],[[65344,65344],"disallowed_STD3_mapped",[96]],[[65345,65345],"mapped",[97]],[[65346,65346],"mapped",[98]],[[65347,65347],"mapped",[99]],[[65348,65348],"mapped",[100]],[[65349,65349],"mapped",[101]],[[65350,65350],"mapped",[102]],[[65351,65351],"mapped",[103]],[[65352,65352],"mapped",[104]],[[65353,65353],"mapped",[105]],[[65354,65354],"mapped",[106]],[[65355,65355],"mapped",[107]],[[65356,65356],"mapped",[108]],[[65357,65357],"mapped",[109]],[[65358,65358],"mapped",[110]],[[65359,65359],"mapped",[111]],[[65360,65360],"mapped",[112]],[[65361,65361],"mapped",[113]],[[65362,65362],"mapped",[114]],[[65363,65363],"mapped",[115]],[[65364,65364],"mapped",[116]],[[65365,65365],"mapped",[117]],[[65366,65366],"mapped",[118]],[[65367,65367],"mapped",[119]],[[65368,65368],"mapped",[120]],[[65369,65369],"mapped",[121]],[[65370,65370],"mapped",[122]],[[65371,65371],"disallowed_STD3_mapped",[123]],[[65372,65372],"disallowed_STD3_mapped",[124]],[[65373,65373],"disallowed_STD3_mapped",[125]],[[65374,65374],"disallowed_STD3_mapped",[126]],[[65375,65375],"mapped",[10629]],[[65376,65376],"mapped",[10630]],[[65377,65377],"mapped",[46]],[[65378,65378],"mapped",[12300]],[[65379,65379],"mapped",[12301]],[[65380,65380],"mapped",[12289]],[[65381,65381],"mapped",[12539]],[[65382,65382],"mapped",[12530]],[[65383,65383],"mapped",[12449]],[[65384,65384],"mapped",[12451]],[[65385,65385],"mapped",[12453]],[[65386,65386],"mapped",[12455]],[[65387,65387],"mapped",[12457]],[[65388,65388],"mapped",[12515]],[[65389,65389],"mapped",[12517]],[[65390,65390],"mapped",[12519]],[[65391,65391],"mapped",[12483]],[[65392,65392],"mapped",[12540]],[[65393,65393],"mapped",[12450]],[[65394,65394],"mapped",[12452]],[[65395,65395],"mapped",[12454]],[[65396,65396],"mapped",[12456]],[[65397,65397],"mapped",[12458]],[[65398,65398],"mapped",[12459]],[[65399,65399],"mapped",[12461]],[[65400,65400],"mapped",[12463]],[[65401,65401],"mapped",[12465]],[[65402,65402],"mapped",[12467]],[[65403,65403],"mapped",[12469]],[[65404,65404],"mapped",[12471]],[[65405,65405],"mapped",[12473]],[[65406,65406],"mapped",[12475]],[[65407,65407],"mapped",[12477]],[[65408,65408],"mapped",[12479]],[[65409,65409],"mapped",[12481]],[[65410,65410],"mapped",[12484]],[[65411,65411],"mapped",[12486]],[[65412,65412],"mapped",[12488]],[[65413,65413],"mapped",[12490]],[[65414,65414],"mapped",[12491]],[[65415,65415],"mapped",[12492]],[[65416,65416],"mapped",[12493]],[[65417,65417],"mapped",[12494]],[[65418,65418],"mapped",[12495]],[[65419,65419],"mapped",[12498]],[[65420,65420],"mapped",[12501]],[[65421,65421],"mapped",[12504]],[[65422,65422],"mapped",[12507]],[[65423,65423],"mapped",[12510]],[[65424,65424],"mapped",[12511]],[[65425,65425],"mapped",[12512]],[[65426,65426],"mapped",[12513]],[[65427,65427],"mapped",[12514]],[[65428,65428],"mapped",[12516]],[[65429,65429],"mapped",[12518]],[[65430,65430],"mapped",[12520]],[[65431,65431],"mapped",[12521]],[[65432,65432],"mapped",[12522]],[[65433,65433],"mapped",[12523]],[[65434,65434],"mapped",[12524]],[[65435,65435],"mapped",[12525]],[[65436,65436],"mapped",[12527]],[[65437,65437],"mapped",[12531]],[[65438,65438],"mapped",[12441]],[[65439,65439],"mapped",[12442]],[[65440,65440],"disallowed"],[[65441,65441],"mapped",[4352]],[[65442,65442],"mapped",[4353]],[[65443,65443],"mapped",[4522]],[[65444,65444],"mapped",[4354]],[[65445,65445],"mapped",[4524]],[[65446,65446],"mapped",[4525]],[[65447,65447],"mapped",[4355]],[[65448,65448],"mapped",[4356]],[[65449,65449],"mapped",[4357]],[[65450,65450],"mapped",[4528]],[[65451,65451],"mapped",[4529]],[[65452,65452],"mapped",[4530]],[[65453,65453],"mapped",[4531]],[[65454,65454],"mapped",[4532]],[[65455,65455],"mapped",[4533]],[[65456,65456],"mapped",[4378]],[[65457,65457],"mapped",[4358]],[[65458,65458],"mapped",[4359]],[[65459,65459],"mapped",[4360]],[[65460,65460],"mapped",[4385]],[[65461,65461],"mapped",[4361]],[[65462,65462],"mapped",[4362]],[[65463,65463],"mapped",[4363]],[[65464,65464],"mapped",[4364]],[[65465,65465],"mapped",[4365]],[[65466,65466],"mapped",[4366]],[[65467,65467],"mapped",[4367]],[[65468,65468],"mapped",[4368]],[[65469,65469],"mapped",[4369]],[[65470,65470],"mapped",[4370]],[[65471,65473],"disallowed"],[[65474,65474],"mapped",[4449]],[[65475,65475],"mapped",[4450]],[[65476,65476],"mapped",[4451]],[[65477,65477],"mapped",[4452]],[[65478,65478],"mapped",[4453]],[[65479,65479],"mapped",[4454]],[[65480,65481],"disallowed"],[[65482,65482],"mapped",[4455]],[[65483,65483],"mapped",[4456]],[[65484,65484],"mapped",[4457]],[[65485,65485],"mapped",[4458]],[[65486,65486],"mapped",[4459]],[[65487,65487],"mapped",[4460]],[[65488,65489],"disallowed"],[[65490,65490],"mapped",[4461]],[[65491,65491],"mapped",[4462]],[[65492,65492],"mapped",[4463]],[[65493,65493],"mapped",[4464]],[[65494,65494],"mapped",[4465]],[[65495,65495],"mapped",[4466]],[[65496,65497],"disallowed"],[[65498,65498],"mapped",[4467]],[[65499,65499],"mapped",[4468]],[[65500,65500],"mapped",[4469]],[[65501,65503],"disallowed"],[[65504,65504],"mapped",[162]],[[65505,65505],"mapped",[163]],[[65506,65506],"mapped",[172]],[[65507,65507],"disallowed_STD3_mapped",[32,772]],[[65508,65508],"mapped",[166]],[[65509,65509],"mapped",[165]],[[65510,65510],"mapped",[8361]],[[65511,65511],"disallowed"],[[65512,65512],"mapped",[9474]],[[65513,65513],"mapped",[8592]],[[65514,65514],"mapped",[8593]],[[65515,65515],"mapped",[8594]],[[65516,65516],"mapped",[8595]],[[65517,65517],"mapped",[9632]],[[65518,65518],"mapped",[9675]],[[65519,65528],"disallowed"],[[65529,65531],"disallowed"],[[65532,65532],"disallowed"],[[65533,65533],"disallowed"],[[65534,65535],"disallowed"],[[65536,65547],"valid"],[[65548,65548],"disallowed"],[[65549,65574],"valid"],[[65575,65575],"disallowed"],[[65576,65594],"valid"],[[65595,65595],"disallowed"],[[65596,65597],"valid"],[[65598,65598],"disallowed"],[[65599,65613],"valid"],[[65614,65615],"disallowed"],[[65616,65629],"valid"],[[65630,65663],"disallowed"],[[65664,65786],"valid"],[[65787,65791],"disallowed"],[[65792,65794],"valid",[],"NV8"],[[65795,65798],"disallowed"],[[65799,65843],"valid",[],"NV8"],[[65844,65846],"disallowed"],[[65847,65855],"valid",[],"NV8"],[[65856,65930],"valid",[],"NV8"],[[65931,65932],"valid",[],"NV8"],[[65933,65935],"disallowed"],[[65936,65947],"valid",[],"NV8"],[[65948,65951],"disallowed"],[[65952,65952],"valid",[],"NV8"],[[65953,65999],"disallowed"],[[66000,66044],"valid",[],"NV8"],[[66045,66045],"valid"],[[66046,66175],"disallowed"],[[66176,66204],"valid"],[[66205,66207],"disallowed"],[[66208,66256],"valid"],[[66257,66271],"disallowed"],[[66272,66272],"valid"],[[66273,66299],"valid",[],"NV8"],[[66300,66303],"disallowed"],[[66304,66334],"valid"],[[66335,66335],"valid"],[[66336,66339],"valid",[],"NV8"],[[66340,66351],"disallowed"],[[66352,66368],"valid"],[[66369,66369],"valid",[],"NV8"],[[66370,66377],"valid"],[[66378,66378],"valid",[],"NV8"],[[66379,66383],"disallowed"],[[66384,66426],"valid"],[[66427,66431],"disallowed"],[[66432,66461],"valid"],[[66462,66462],"disallowed"],[[66463,66463],"valid",[],"NV8"],[[66464,66499],"valid"],[[66500,66503],"disallowed"],[[66504,66511],"valid"],[[66512,66517],"valid",[],"NV8"],[[66518,66559],"disallowed"],[[66560,66560],"mapped",[66600]],[[66561,66561],"mapped",[66601]],[[66562,66562],"mapped",[66602]],[[66563,66563],"mapped",[66603]],[[66564,66564],"mapped",[66604]],[[66565,66565],"mapped",[66605]],[[66566,66566],"mapped",[66606]],[[66567,66567],"mapped",[66607]],[[66568,66568],"mapped",[66608]],[[66569,66569],"mapped",[66609]],[[66570,66570],"mapped",[66610]],[[66571,66571],"mapped",[66611]],[[66572,66572],"mapped",[66612]],[[66573,66573],"mapped",[66613]],[[66574,66574],"mapped",[66614]],[[66575,66575],"mapped",[66615]],[[66576,66576],"mapped",[66616]],[[66577,66577],"mapped",[66617]],[[66578,66578],"mapped",[66618]],[[66579,66579],"mapped",[66619]],[[66580,66580],"mapped",[66620]],[[66581,66581],"mapped",[66621]],[[66582,66582],"mapped",[66622]],[[66583,66583],"mapped",[66623]],[[66584,66584],"mapped",[66624]],[[66585,66585],"mapped",[66625]],[[66586,66586],"mapped",[66626]],[[66587,66587],"mapped",[66627]],[[66588,66588],"mapped",[66628]],[[66589,66589],"mapped",[66629]],[[66590,66590],"mapped",[66630]],[[66591,66591],"mapped",[66631]],[[66592,66592],"mapped",[66632]],[[66593,66593],"mapped",[66633]],[[66594,66594],"mapped",[66634]],[[66595,66595],"mapped",[66635]],[[66596,66596],"mapped",[66636]],[[66597,66597],"mapped",[66637]],[[66598,66598],"mapped",[66638]],[[66599,66599],"mapped",[66639]],[[66600,66637],"valid"],[[66638,66717],"valid"],[[66718,66719],"disallowed"],[[66720,66729],"valid"],[[66730,66815],"disallowed"],[[66816,66855],"valid"],[[66856,66863],"disallowed"],[[66864,66915],"valid"],[[66916,66926],"disallowed"],[[66927,66927],"valid",[],"NV8"],[[66928,67071],"disallowed"],[[67072,67382],"valid"],[[67383,67391],"disallowed"],[[67392,67413],"valid"],[[67414,67423],"disallowed"],[[67424,67431],"valid"],[[67432,67583],"disallowed"],[[67584,67589],"valid"],[[67590,67591],"disallowed"],[[67592,67592],"valid"],[[67593,67593],"disallowed"],[[67594,67637],"valid"],[[67638,67638],"disallowed"],[[67639,67640],"valid"],[[67641,67643],"disallowed"],[[67644,67644],"valid"],[[67645,67646],"disallowed"],[[67647,67647],"valid"],[[67648,67669],"valid"],[[67670,67670],"disallowed"],[[67671,67679],"valid",[],"NV8"],[[67680,67702],"valid"],[[67703,67711],"valid",[],"NV8"],[[67712,67742],"valid"],[[67743,67750],"disallowed"],[[67751,67759],"valid",[],"NV8"],[[67760,67807],"disallowed"],[[67808,67826],"valid"],[[67827,67827],"disallowed"],[[67828,67829],"valid"],[[67830,67834],"disallowed"],[[67835,67839],"valid",[],"NV8"],[[67840,67861],"valid"],[[67862,67865],"valid",[],"NV8"],[[67866,67867],"valid",[],"NV8"],[[67868,67870],"disallowed"],[[67871,67871],"valid",[],"NV8"],[[67872,67897],"valid"],[[67898,67902],"disallowed"],[[67903,67903],"valid",[],"NV8"],[[67904,67967],"disallowed"],[[67968,68023],"valid"],[[68024,68027],"disallowed"],[[68028,68029],"valid",[],"NV8"],[[68030,68031],"valid"],[[68032,68047],"valid",[],"NV8"],[[68048,68049],"disallowed"],[[68050,68095],"valid",[],"NV8"],[[68096,68099],"valid"],[[68100,68100],"disallowed"],[[68101,68102],"valid"],[[68103,68107],"disallowed"],[[68108,68115],"valid"],[[68116,68116],"disallowed"],[[68117,68119],"valid"],[[68120,68120],"disallowed"],[[68121,68147],"valid"],[[68148,68151],"disallowed"],[[68152,68154],"valid"],[[68155,68158],"disallowed"],[[68159,68159],"valid"],[[68160,68167],"valid",[],"NV8"],[[68168,68175],"disallowed"],[[68176,68184],"valid",[],"NV8"],[[68185,68191],"disallowed"],[[68192,68220],"valid"],[[68221,68223],"valid",[],"NV8"],[[68224,68252],"valid"],[[68253,68255],"valid",[],"NV8"],[[68256,68287],"disallowed"],[[68288,68295],"valid"],[[68296,68296],"valid",[],"NV8"],[[68297,68326],"valid"],[[68327,68330],"disallowed"],[[68331,68342],"valid",[],"NV8"],[[68343,68351],"disallowed"],[[68352,68405],"valid"],[[68406,68408],"disallowed"],[[68409,68415],"valid",[],"NV8"],[[68416,68437],"valid"],[[68438,68439],"disallowed"],[[68440,68447],"valid",[],"NV8"],[[68448,68466],"valid"],[[68467,68471],"disallowed"],[[68472,68479],"valid",[],"NV8"],[[68480,68497],"valid"],[[68498,68504],"disallowed"],[[68505,68508],"valid",[],"NV8"],[[68509,68520],"disallowed"],[[68521,68527],"valid",[],"NV8"],[[68528,68607],"disallowed"],[[68608,68680],"valid"],[[68681,68735],"disallowed"],[[68736,68736],"mapped",[68800]],[[68737,68737],"mapped",[68801]],[[68738,68738],"mapped",[68802]],[[68739,68739],"mapped",[68803]],[[68740,68740],"mapped",[68804]],[[68741,68741],"mapped",[68805]],[[68742,68742],"mapped",[68806]],[[68743,68743],"mapped",[68807]],[[68744,68744],"mapped",[68808]],[[68745,68745],"mapped",[68809]],[[68746,68746],"mapped",[68810]],[[68747,68747],"mapped",[68811]],[[68748,68748],"mapped",[68812]],[[68749,68749],"mapped",[68813]],[[68750,68750],"mapped",[68814]],[[68751,68751],"mapped",[68815]],[[68752,68752],"mapped",[68816]],[[68753,68753],"mapped",[68817]],[[68754,68754],"mapped",[68818]],[[68755,68755],"mapped",[68819]],[[68756,68756],"mapped",[68820]],[[68757,68757],"mapped",[68821]],[[68758,68758],"mapped",[68822]],[[68759,68759],"mapped",[68823]],[[68760,68760],"mapped",[68824]],[[68761,68761],"mapped",[68825]],[[68762,68762],"mapped",[68826]],[[68763,68763],"mapped",[68827]],[[68764,68764],"mapped",[68828]],[[68765,68765],"mapped",[68829]],[[68766,68766],"mapped",[68830]],[[68767,68767],"mapped",[68831]],[[68768,68768],"mapped",[68832]],[[68769,68769],"mapped",[68833]],[[68770,68770],"mapped",[68834]],[[68771,68771],"mapped",[68835]],[[68772,68772],"mapped",[68836]],[[68773,68773],"mapped",[68837]],[[68774,68774],"mapped",[68838]],[[68775,68775],"mapped",[68839]],[[68776,68776],"mapped",[68840]],[[68777,68777],"mapped",[68841]],[[68778,68778],"mapped",[68842]],[[68779,68779],"mapped",[68843]],[[68780,68780],"mapped",[68844]],[[68781,68781],"mapped",[68845]],[[68782,68782],"mapped",[68846]],[[68783,68783],"mapped",[68847]],[[68784,68784],"mapped",[68848]],[[68785,68785],"mapped",[68849]],[[68786,68786],"mapped",[68850]],[[68787,68799],"disallowed"],[[68800,68850],"valid"],[[68851,68857],"disallowed"],[[68858,68863],"valid",[],"NV8"],[[68864,69215],"disallowed"],[[69216,69246],"valid",[],"NV8"],[[69247,69631],"disallowed"],[[69632,69702],"valid"],[[69703,69709],"valid",[],"NV8"],[[69710,69713],"disallowed"],[[69714,69733],"valid",[],"NV8"],[[69734,69743],"valid"],[[69744,69758],"disallowed"],[[69759,69759],"valid"],[[69760,69818],"valid"],[[69819,69820],"valid",[],"NV8"],[[69821,69821],"disallowed"],[[69822,69825],"valid",[],"NV8"],[[69826,69839],"disallowed"],[[69840,69864],"valid"],[[69865,69871],"disallowed"],[[69872,69881],"valid"],[[69882,69887],"disallowed"],[[69888,69940],"valid"],[[69941,69941],"disallowed"],[[69942,69951],"valid"],[[69952,69955],"valid",[],"NV8"],[[69956,69967],"disallowed"],[[69968,70003],"valid"],[[70004,70005],"valid",[],"NV8"],[[70006,70006],"valid"],[[70007,70015],"disallowed"],[[70016,70084],"valid"],[[70085,70088],"valid",[],"NV8"],[[70089,70089],"valid",[],"NV8"],[[70090,70092],"valid"],[[70093,70093],"valid",[],"NV8"],[[70094,70095],"disallowed"],[[70096,70105],"valid"],[[70106,70106],"valid"],[[70107,70107],"valid",[],"NV8"],[[70108,70108],"valid"],[[70109,70111],"valid",[],"NV8"],[[70112,70112],"disallowed"],[[70113,70132],"valid",[],"NV8"],[[70133,70143],"disallowed"],[[70144,70161],"valid"],[[70162,70162],"disallowed"],[[70163,70199],"valid"],[[70200,70205],"valid",[],"NV8"],[[70206,70271],"disallowed"],[[70272,70278],"valid"],[[70279,70279],"disallowed"],[[70280,70280],"valid"],[[70281,70281],"disallowed"],[[70282,70285],"valid"],[[70286,70286],"disallowed"],[[70287,70301],"valid"],[[70302,70302],"disallowed"],[[70303,70312],"valid"],[[70313,70313],"valid",[],"NV8"],[[70314,70319],"disallowed"],[[70320,70378],"valid"],[[70379,70383],"disallowed"],[[70384,70393],"valid"],[[70394,70399],"disallowed"],[[70400,70400],"valid"],[[70401,70403],"valid"],[[70404,70404],"disallowed"],[[70405,70412],"valid"],[[70413,70414],"disallowed"],[[70415,70416],"valid"],[[70417,70418],"disallowed"],[[70419,70440],"valid"],[[70441,70441],"disallowed"],[[70442,70448],"valid"],[[70449,70449],"disallowed"],[[70450,70451],"valid"],[[70452,70452],"disallowed"],[[70453,70457],"valid"],[[70458,70459],"disallowed"],[[70460,70468],"valid"],[[70469,70470],"disallowed"],[[70471,70472],"valid"],[[70473,70474],"disallowed"],[[70475,70477],"valid"],[[70478,70479],"disallowed"],[[70480,70480],"valid"],[[70481,70486],"disallowed"],[[70487,70487],"valid"],[[70488,70492],"disallowed"],[[70493,70499],"valid"],[[70500,70501],"disallowed"],[[70502,70508],"valid"],[[70509,70511],"disallowed"],[[70512,70516],"valid"],[[70517,70783],"disallowed"],[[70784,70853],"valid"],[[70854,70854],"valid",[],"NV8"],[[70855,70855],"valid"],[[70856,70863],"disallowed"],[[70864,70873],"valid"],[[70874,71039],"disallowed"],[[71040,71093],"valid"],[[71094,71095],"disallowed"],[[71096,71104],"valid"],[[71105,71113],"valid",[],"NV8"],[[71114,71127],"valid",[],"NV8"],[[71128,71133],"valid"],[[71134,71167],"disallowed"],[[71168,71232],"valid"],[[71233,71235],"valid",[],"NV8"],[[71236,71236],"valid"],[[71237,71247],"disallowed"],[[71248,71257],"valid"],[[71258,71295],"disallowed"],[[71296,71351],"valid"],[[71352,71359],"disallowed"],[[71360,71369],"valid"],[[71370,71423],"disallowed"],[[71424,71449],"valid"],[[71450,71452],"disallowed"],[[71453,71467],"valid"],[[71468,71471],"disallowed"],[[71472,71481],"valid"],[[71482,71487],"valid",[],"NV8"],[[71488,71839],"disallowed"],[[71840,71840],"mapped",[71872]],[[71841,71841],"mapped",[71873]],[[71842,71842],"mapped",[71874]],[[71843,71843],"mapped",[71875]],[[71844,71844],"mapped",[71876]],[[71845,71845],"mapped",[71877]],[[71846,71846],"mapped",[71878]],[[71847,71847],"mapped",[71879]],[[71848,71848],"mapped",[71880]],[[71849,71849],"mapped",[71881]],[[71850,71850],"mapped",[71882]],[[71851,71851],"mapped",[71883]],[[71852,71852],"mapped",[71884]],[[71853,71853],"mapped",[71885]],[[71854,71854],"mapped",[71886]],[[71855,71855],"mapped",[71887]],[[71856,71856],"mapped",[71888]],[[71857,71857],"mapped",[71889]],[[71858,71858],"mapped",[71890]],[[71859,71859],"mapped",[71891]],[[71860,71860],"mapped",[71892]],[[71861,71861],"mapped",[71893]],[[71862,71862],"mapped",[71894]],[[71863,71863],"mapped",[71895]],[[71864,71864],"mapped",[71896]],[[71865,71865],"mapped",[71897]],[[71866,71866],"mapped",[71898]],[[71867,71867],"mapped",[71899]],[[71868,71868],"mapped",[71900]],[[71869,71869],"mapped",[71901]],[[71870,71870],"mapped",[71902]],[[71871,71871],"mapped",[71903]],[[71872,71913],"valid"],[[71914,71922],"valid",[],"NV8"],[[71923,71934],"disallowed"],[[71935,71935],"valid"],[[71936,72383],"disallowed"],[[72384,72440],"valid"],[[72441,73727],"disallowed"],[[73728,74606],"valid"],[[74607,74648],"valid"],[[74649,74649],"valid"],[[74650,74751],"disallowed"],[[74752,74850],"valid",[],"NV8"],[[74851,74862],"valid",[],"NV8"],[[74863,74863],"disallowed"],[[74864,74867],"valid",[],"NV8"],[[74868,74868],"valid",[],"NV8"],[[74869,74879],"disallowed"],[[74880,75075],"valid"],[[75076,77823],"disallowed"],[[77824,78894],"valid"],[[78895,82943],"disallowed"],[[82944,83526],"valid"],[[83527,92159],"disallowed"],[[92160,92728],"valid"],[[92729,92735],"disallowed"],[[92736,92766],"valid"],[[92767,92767],"disallowed"],[[92768,92777],"valid"],[[92778,92781],"disallowed"],[[92782,92783],"valid",[],"NV8"],[[92784,92879],"disallowed"],[[92880,92909],"valid"],[[92910,92911],"disallowed"],[[92912,92916],"valid"],[[92917,92917],"valid",[],"NV8"],[[92918,92927],"disallowed"],[[92928,92982],"valid"],[[92983,92991],"valid",[],"NV8"],[[92992,92995],"valid"],[[92996,92997],"valid",[],"NV8"],[[92998,93007],"disallowed"],[[93008,93017],"valid"],[[93018,93018],"disallowed"],[[93019,93025],"valid",[],"NV8"],[[93026,93026],"disallowed"],[[93027,93047],"valid"],[[93048,93052],"disallowed"],[[93053,93071],"valid"],[[93072,93951],"disallowed"],[[93952,94020],"valid"],[[94021,94031],"disallowed"],[[94032,94078],"valid"],[[94079,94094],"disallowed"],[[94095,94111],"valid"],[[94112,110591],"disallowed"],[[110592,110593],"valid"],[[110594,113663],"disallowed"],[[113664,113770],"valid"],[[113771,113775],"disallowed"],[[113776,113788],"valid"],[[113789,113791],"disallowed"],[[113792,113800],"valid"],[[113801,113807],"disallowed"],[[113808,113817],"valid"],[[113818,113819],"disallowed"],[[113820,113820],"valid",[],"NV8"],[[113821,113822],"valid"],[[113823,113823],"valid",[],"NV8"],[[113824,113827],"ignored"],[[113828,118783],"disallowed"],[[118784,119029],"valid",[],"NV8"],[[119030,119039],"disallowed"],[[119040,119078],"valid",[],"NV8"],[[119079,119080],"disallowed"],[[119081,119081],"valid",[],"NV8"],[[119082,119133],"valid",[],"NV8"],[[119134,119134],"mapped",[119127,119141]],[[119135,119135],"mapped",[119128,119141]],[[119136,119136],"mapped",[119128,119141,119150]],[[119137,119137],"mapped",[119128,119141,119151]],[[119138,119138],"mapped",[119128,119141,119152]],[[119139,119139],"mapped",[119128,119141,119153]],[[119140,119140],"mapped",[119128,119141,119154]],[[119141,119154],"valid",[],"NV8"],[[119155,119162],"disallowed"],[[119163,119226],"valid",[],"NV8"],[[119227,119227],"mapped",[119225,119141]],[[119228,119228],"mapped",[119226,119141]],[[119229,119229],"mapped",[119225,119141,119150]],[[119230,119230],"mapped",[119226,119141,119150]],[[119231,119231],"mapped",[119225,119141,119151]],[[119232,119232],"mapped",[119226,119141,119151]],[[119233,119261],"valid",[],"NV8"],[[119262,119272],"valid",[],"NV8"],[[119273,119295],"disallowed"],[[119296,119365],"valid",[],"NV8"],[[119366,119551],"disallowed"],[[119552,119638],"valid",[],"NV8"],[[119639,119647],"disallowed"],[[119648,119665],"valid",[],"NV8"],[[119666,119807],"disallowed"],[[119808,119808],"mapped",[97]],[[119809,119809],"mapped",[98]],[[119810,119810],"mapped",[99]],[[119811,119811],"mapped",[100]],[[119812,119812],"mapped",[101]],[[119813,119813],"mapped",[102]],[[119814,119814],"mapped",[103]],[[119815,119815],"mapped",[104]],[[119816,119816],"mapped",[105]],[[119817,119817],"mapped",[106]],[[119818,119818],"mapped",[107]],[[119819,119819],"mapped",[108]],[[119820,119820],"mapped",[109]],[[119821,119821],"mapped",[110]],[[119822,119822],"mapped",[111]],[[119823,119823],"mapped",[112]],[[119824,119824],"mapped",[113]],[[119825,119825],"mapped",[114]],[[119826,119826],"mapped",[115]],[[119827,119827],"mapped",[116]],[[119828,119828],"mapped",[117]],[[119829,119829],"mapped",[118]],[[119830,119830],"mapped",[119]],[[119831,119831],"mapped",[120]],[[119832,119832],"mapped",[121]],[[119833,119833],"mapped",[122]],[[119834,119834],"mapped",[97]],[[119835,119835],"mapped",[98]],[[119836,119836],"mapped",[99]],[[119837,119837],"mapped",[100]],[[119838,119838],"mapped",[101]],[[119839,119839],"mapped",[102]],[[119840,119840],"mapped",[103]],[[119841,119841],"mapped",[104]],[[119842,119842],"mapped",[105]],[[119843,119843],"mapped",[106]],[[119844,119844],"mapped",[107]],[[119845,119845],"mapped",[108]],[[119846,119846],"mapped",[109]],[[119847,119847],"mapped",[110]],[[119848,119848],"mapped",[111]],[[119849,119849],"mapped",[112]],[[119850,119850],"mapped",[113]],[[119851,119851],"mapped",[114]],[[119852,119852],"mapped",[115]],[[119853,119853],"mapped",[116]],[[119854,119854],"mapped",[117]],[[119855,119855],"mapped",[118]],[[119856,119856],"mapped",[119]],[[119857,119857],"mapped",[120]],[[119858,119858],"mapped",[121]],[[119859,119859],"mapped",[122]],[[119860,119860],"mapped",[97]],[[119861,119861],"mapped",[98]],[[119862,119862],"mapped",[99]],[[119863,119863],"mapped",[100]],[[119864,119864],"mapped",[101]],[[119865,119865],"mapped",[102]],[[119866,119866],"mapped",[103]],[[119867,119867],"mapped",[104]],[[119868,119868],"mapped",[105]],[[119869,119869],"mapped",[106]],[[119870,119870],"mapped",[107]],[[119871,119871],"mapped",[108]],[[119872,119872],"mapped",[109]],[[119873,119873],"mapped",[110]],[[119874,119874],"mapped",[111]],[[119875,119875],"mapped",[112]],[[119876,119876],"mapped",[113]],[[119877,119877],"mapped",[114]],[[119878,119878],"mapped",[115]],[[119879,119879],"mapped",[116]],[[119880,119880],"mapped",[117]],[[119881,119881],"mapped",[118]],[[119882,119882],"mapped",[119]],[[119883,119883],"mapped",[120]],[[119884,119884],"mapped",[121]],[[119885,119885],"mapped",[122]],[[119886,119886],"mapped",[97]],[[119887,119887],"mapped",[98]],[[119888,119888],"mapped",[99]],[[119889,119889],"mapped",[100]],[[119890,119890],"mapped",[101]],[[119891,119891],"mapped",[102]],[[119892,119892],"mapped",[103]],[[119893,119893],"disallowed"],[[119894,119894],"mapped",[105]],[[119895,119895],"mapped",[106]],[[119896,119896],"mapped",[107]],[[119897,119897],"mapped",[108]],[[119898,119898],"mapped",[109]],[[119899,119899],"mapped",[110]],[[119900,119900],"mapped",[111]],[[119901,119901],"mapped",[112]],[[119902,119902],"mapped",[113]],[[119903,119903],"mapped",[114]],[[119904,119904],"mapped",[115]],[[119905,119905],"mapped",[116]],[[119906,119906],"mapped",[117]],[[119907,119907],"mapped",[118]],[[119908,119908],"mapped",[119]],[[119909,119909],"mapped",[120]],[[119910,119910],"mapped",[121]],[[119911,119911],"mapped",[122]],[[119912,119912],"mapped",[97]],[[119913,119913],"mapped",[98]],[[119914,119914],"mapped",[99]],[[119915,119915],"mapped",[100]],[[119916,119916],"mapped",[101]],[[119917,119917],"mapped",[102]],[[119918,119918],"mapped",[103]],[[119919,119919],"mapped",[104]],[[119920,119920],"mapped",[105]],[[119921,119921],"mapped",[106]],[[119922,119922],"mapped",[107]],[[119923,119923],"mapped",[108]],[[119924,119924],"mapped",[109]],[[119925,119925],"mapped",[110]],[[119926,119926],"mapped",[111]],[[119927,119927],"mapped",[112]],[[119928,119928],"mapped",[113]],[[119929,119929],"mapped",[114]],[[119930,119930],"mapped",[115]],[[119931,119931],"mapped",[116]],[[119932,119932],"mapped",[117]],[[119933,119933],"mapped",[118]],[[119934,119934],"mapped",[119]],[[119935,119935],"mapped",[120]],[[119936,119936],"mapped",[121]],[[119937,119937],"mapped",[122]],[[119938,119938],"mapped",[97]],[[119939,119939],"mapped",[98]],[[119940,119940],"mapped",[99]],[[119941,119941],"mapped",[100]],[[119942,119942],"mapped",[101]],[[119943,119943],"mapped",[102]],[[119944,119944],"mapped",[103]],[[119945,119945],"mapped",[104]],[[119946,119946],"mapped",[105]],[[119947,119947],"mapped",[106]],[[119948,119948],"mapped",[107]],[[119949,119949],"mapped",[108]],[[119950,119950],"mapped",[109]],[[119951,119951],"mapped",[110]],[[119952,119952],"mapped",[111]],[[119953,119953],"mapped",[112]],[[119954,119954],"mapped",[113]],[[119955,119955],"mapped",[114]],[[119956,119956],"mapped",[115]],[[119957,119957],"mapped",[116]],[[119958,119958],"mapped",[117]],[[119959,119959],"mapped",[118]],[[119960,119960],"mapped",[119]],[[119961,119961],"mapped",[120]],[[119962,119962],"mapped",[121]],[[119963,119963],"mapped",[122]],[[119964,119964],"mapped",[97]],[[119965,119965],"disallowed"],[[119966,119966],"mapped",[99]],[[119967,119967],"mapped",[100]],[[119968,119969],"disallowed"],[[119970,119970],"mapped",[103]],[[119971,119972],"disallowed"],[[119973,119973],"mapped",[106]],[[119974,119974],"mapped",[107]],[[119975,119976],"disallowed"],[[119977,119977],"mapped",[110]],[[119978,119978],"mapped",[111]],[[119979,119979],"mapped",[112]],[[119980,119980],"mapped",[113]],[[119981,119981],"disallowed"],[[119982,119982],"mapped",[115]],[[119983,119983],"mapped",[116]],[[119984,119984],"mapped",[117]],[[119985,119985],"mapped",[118]],[[119986,119986],"mapped",[119]],[[119987,119987],"mapped",[120]],[[119988,119988],"mapped",[121]],[[119989,119989],"mapped",[122]],[[119990,119990],"mapped",[97]],[[119991,119991],"mapped",[98]],[[119992,119992],"mapped",[99]],[[119993,119993],"mapped",[100]],[[119994,119994],"disallowed"],[[119995,119995],"mapped",[102]],[[119996,119996],"disallowed"],[[119997,119997],"mapped",[104]],[[119998,119998],"mapped",[105]],[[119999,119999],"mapped",[106]],[[120000,120000],"mapped",[107]],[[120001,120001],"mapped",[108]],[[120002,120002],"mapped",[109]],[[120003,120003],"mapped",[110]],[[120004,120004],"disallowed"],[[120005,120005],"mapped",[112]],[[120006,120006],"mapped",[113]],[[120007,120007],"mapped",[114]],[[120008,120008],"mapped",[115]],[[120009,120009],"mapped",[116]],[[120010,120010],"mapped",[117]],[[120011,120011],"mapped",[118]],[[120012,120012],"mapped",[119]],[[120013,120013],"mapped",[120]],[[120014,120014],"mapped",[121]],[[120015,120015],"mapped",[122]],[[120016,120016],"mapped",[97]],[[120017,120017],"mapped",[98]],[[120018,120018],"mapped",[99]],[[120019,120019],"mapped",[100]],[[120020,120020],"mapped",[101]],[[120021,120021],"mapped",[102]],[[120022,120022],"mapped",[103]],[[120023,120023],"mapped",[104]],[[120024,120024],"mapped",[105]],[[120025,120025],"mapped",[106]],[[120026,120026],"mapped",[107]],[[120027,120027],"mapped",[108]],[[120028,120028],"mapped",[109]],[[120029,120029],"mapped",[110]],[[120030,120030],"mapped",[111]],[[120031,120031],"mapped",[112]],[[120032,120032],"mapped",[113]],[[120033,120033],"mapped",[114]],[[120034,120034],"mapped",[115]],[[120035,120035],"mapped",[116]],[[120036,120036],"mapped",[117]],[[120037,120037],"mapped",[118]],[[120038,120038],"mapped",[119]],[[120039,120039],"mapped",[120]],[[120040,120040],"mapped",[121]],[[120041,120041],"mapped",[122]],[[120042,120042],"mapped",[97]],[[120043,120043],"mapped",[98]],[[120044,120044],"mapped",[99]],[[120045,120045],"mapped",[100]],[[120046,120046],"mapped",[101]],[[120047,120047],"mapped",[102]],[[120048,120048],"mapped",[103]],[[120049,120049],"mapped",[104]],[[120050,120050],"mapped",[105]],[[120051,120051],"mapped",[106]],[[120052,120052],"mapped",[107]],[[120053,120053],"mapped",[108]],[[120054,120054],"mapped",[109]],[[120055,120055],"mapped",[110]],[[120056,120056],"mapped",[111]],[[120057,120057],"mapped",[112]],[[120058,120058],"mapped",[113]],[[120059,120059],"mapped",[114]],[[120060,120060],"mapped",[115]],[[120061,120061],"mapped",[116]],[[120062,120062],"mapped",[117]],[[120063,120063],"mapped",[118]],[[120064,120064],"mapped",[119]],[[120065,120065],"mapped",[120]],[[120066,120066],"mapped",[121]],[[120067,120067],"mapped",[122]],[[120068,120068],"mapped",[97]],[[120069,120069],"mapped",[98]],[[120070,120070],"disallowed"],[[120071,120071],"mapped",[100]],[[120072,120072],"mapped",[101]],[[120073,120073],"mapped",[102]],[[120074,120074],"mapped",[103]],[[120075,120076],"disallowed"],[[120077,120077],"mapped",[106]],[[120078,120078],"mapped",[107]],[[120079,120079],"mapped",[108]],[[120080,120080],"mapped",[109]],[[120081,120081],"mapped",[110]],[[120082,120082],"mapped",[111]],[[120083,120083],"mapped",[112]],[[120084,120084],"mapped",[113]],[[120085,120085],"disallowed"],[[120086,120086],"mapped",[115]],[[120087,120087],"mapped",[116]],[[120088,120088],"mapped",[117]],[[120089,120089],"mapped",[118]],[[120090,120090],"mapped",[119]],[[120091,120091],"mapped",[120]],[[120092,120092],"mapped",[121]],[[120093,120093],"disallowed"],[[120094,120094],"mapped",[97]],[[120095,120095],"mapped",[98]],[[120096,120096],"mapped",[99]],[[120097,120097],"mapped",[100]],[[120098,120098],"mapped",[101]],[[120099,120099],"mapped",[102]],[[120100,120100],"mapped",[103]],[[120101,120101],"mapped",[104]],[[120102,120102],"mapped",[105]],[[120103,120103],"mapped",[106]],[[120104,120104],"mapped",[107]],[[120105,120105],"mapped",[108]],[[120106,120106],"mapped",[109]],[[120107,120107],"mapped",[110]],[[120108,120108],"mapped",[111]],[[120109,120109],"mapped",[112]],[[120110,120110],"mapped",[113]],[[120111,120111],"mapped",[114]],[[120112,120112],"mapped",[115]],[[120113,120113],"mapped",[116]],[[120114,120114],"mapped",[117]],[[120115,120115],"mapped",[118]],[[120116,120116],"mapped",[119]],[[120117,120117],"mapped",[120]],[[120118,120118],"mapped",[121]],[[120119,120119],"mapped",[122]],[[120120,120120],"mapped",[97]],[[120121,120121],"mapped",[98]],[[120122,120122],"disallowed"],[[120123,120123],"mapped",[100]],[[120124,120124],"mapped",[101]],[[120125,120125],"mapped",[102]],[[120126,120126],"mapped",[103]],[[120127,120127],"disallowed"],[[120128,120128],"mapped",[105]],[[120129,120129],"mapped",[106]],[[120130,120130],"mapped",[107]],[[120131,120131],"mapped",[108]],[[120132,120132],"mapped",[109]],[[120133,120133],"disallowed"],[[120134,120134],"mapped",[111]],[[120135,120137],"disallowed"],[[120138,120138],"mapped",[115]],[[120139,120139],"mapped",[116]],[[120140,120140],"mapped",[117]],[[120141,120141],"mapped",[118]],[[120142,120142],"mapped",[119]],[[120143,120143],"mapped",[120]],[[120144,120144],"mapped",[121]],[[120145,120145],"disallowed"],[[120146,120146],"mapped",[97]],[[120147,120147],"mapped",[98]],[[120148,120148],"mapped",[99]],[[120149,120149],"mapped",[100]],[[120150,120150],"mapped",[101]],[[120151,120151],"mapped",[102]],[[120152,120152],"mapped",[103]],[[120153,120153],"mapped",[104]],[[120154,120154],"mapped",[105]],[[120155,120155],"mapped",[106]],[[120156,120156],"mapped",[107]],[[120157,120157],"mapped",[108]],[[120158,120158],"mapped",[109]],[[120159,120159],"mapped",[110]],[[120160,120160],"mapped",[111]],[[120161,120161],"mapped",[112]],[[120162,120162],"mapped",[113]],[[120163,120163],"mapped",[114]],[[120164,120164],"mapped",[115]],[[120165,120165],"mapped",[116]],[[120166,120166],"mapped",[117]],[[120167,120167],"mapped",[118]],[[120168,120168],"mapped",[119]],[[120169,120169],"mapped",[120]],[[120170,120170],"mapped",[121]],[[120171,120171],"mapped",[122]],[[120172,120172],"mapped",[97]],[[120173,120173],"mapped",[98]],[[120174,120174],"mapped",[99]],[[120175,120175],"mapped",[100]],[[120176,120176],"mapped",[101]],[[120177,120177],"mapped",[102]],[[120178,120178],"mapped",[103]],[[120179,120179],"mapped",[104]],[[120180,120180],"mapped",[105]],[[120181,120181],"mapped",[106]],[[120182,120182],"mapped",[107]],[[120183,120183],"mapped",[108]],[[120184,120184],"mapped",[109]],[[120185,120185],"mapped",[110]],[[120186,120186],"mapped",[111]],[[120187,120187],"mapped",[112]],[[120188,120188],"mapped",[113]],[[120189,120189],"mapped",[114]],[[120190,120190],"mapped",[115]],[[120191,120191],"mapped",[116]],[[120192,120192],"mapped",[117]],[[120193,120193],"mapped",[118]],[[120194,120194],"mapped",[119]],[[120195,120195],"mapped",[120]],[[120196,120196],"mapped",[121]],[[120197,120197],"mapped",[122]],[[120198,120198],"mapped",[97]],[[120199,120199],"mapped",[98]],[[120200,120200],"mapped",[99]],[[120201,120201],"mapped",[100]],[[120202,120202],"mapped",[101]],[[120203,120203],"mapped",[102]],[[120204,120204],"mapped",[103]],[[120205,120205],"mapped",[104]],[[120206,120206],"mapped",[105]],[[120207,120207],"mapped",[106]],[[120208,120208],"mapped",[107]],[[120209,120209],"mapped",[108]],[[120210,120210],"mapped",[109]],[[120211,120211],"mapped",[110]],[[120212,120212],"mapped",[111]],[[120213,120213],"mapped",[112]],[[120214,120214],"mapped",[113]],[[120215,120215],"mapped",[114]],[[120216,120216],"mapped",[115]],[[120217,120217],"mapped",[116]],[[120218,120218],"mapped",[117]],[[120219,120219],"mapped",[118]],[[120220,120220],"mapped",[119]],[[120221,120221],"mapped",[120]],[[120222,120222],"mapped",[121]],[[120223,120223],"mapped",[122]],[[120224,120224],"mapped",[97]],[[120225,120225],"mapped",[98]],[[120226,120226],"mapped",[99]],[[120227,120227],"mapped",[100]],[[120228,120228],"mapped",[101]],[[120229,120229],"mapped",[102]],[[120230,120230],"mapped",[103]],[[120231,120231],"mapped",[104]],[[120232,120232],"mapped",[105]],[[120233,120233],"mapped",[106]],[[120234,120234],"mapped",[107]],[[120235,120235],"mapped",[108]],[[120236,120236],"mapped",[109]],[[120237,120237],"mapped",[110]],[[120238,120238],"mapped",[111]],[[120239,120239],"mapped",[112]],[[120240,120240],"mapped",[113]],[[120241,120241],"mapped",[114]],[[120242,120242],"mapped",[115]],[[120243,120243],"mapped",[116]],[[120244,120244],"mapped",[117]],[[120245,120245],"mapped",[118]],[[120246,120246],"mapped",[119]],[[120247,120247],"mapped",[120]],[[120248,120248],"mapped",[121]],[[120249,120249],"mapped",[122]],[[120250,120250],"mapped",[97]],[[120251,120251],"mapped",[98]],[[120252,120252],"mapped",[99]],[[120253,120253],"mapped",[100]],[[120254,120254],"mapped",[101]],[[120255,120255],"mapped",[102]],[[120256,120256],"mapped",[103]],[[120257,120257],"mapped",[104]],[[120258,120258],"mapped",[105]],[[120259,120259],"mapped",[106]],[[120260,120260],"mapped",[107]],[[120261,120261],"mapped",[108]],[[120262,120262],"mapped",[109]],[[120263,120263],"mapped",[110]],[[120264,120264],"mapped",[111]],[[120265,120265],"mapped",[112]],[[120266,120266],"mapped",[113]],[[120267,120267],"mapped",[114]],[[120268,120268],"mapped",[115]],[[120269,120269],"mapped",[116]],[[120270,120270],"mapped",[117]],[[120271,120271],"mapped",[118]],[[120272,120272],"mapped",[119]],[[120273,120273],"mapped",[120]],[[120274,120274],"mapped",[121]],[[120275,120275],"mapped",[122]],[[120276,120276],"mapped",[97]],[[120277,120277],"mapped",[98]],[[120278,120278],"mapped",[99]],[[120279,120279],"mapped",[100]],[[120280,120280],"mapped",[101]],[[120281,120281],"mapped",[102]],[[120282,120282],"mapped",[103]],[[120283,120283],"mapped",[104]],[[120284,120284],"mapped",[105]],[[120285,120285],"mapped",[106]],[[120286,120286],"mapped",[107]],[[120287,120287],"mapped",[108]],[[120288,120288],"mapped",[109]],[[120289,120289],"mapped",[110]],[[120290,120290],"mapped",[111]],[[120291,120291],"mapped",[112]],[[120292,120292],"mapped",[113]],[[120293,120293],"mapped",[114]],[[120294,120294],"mapped",[115]],[[120295,120295],"mapped",[116]],[[120296,120296],"mapped",[117]],[[120297,120297],"mapped",[118]],[[120298,120298],"mapped",[119]],[[120299,120299],"mapped",[120]],[[120300,120300],"mapped",[121]],[[120301,120301],"mapped",[122]],[[120302,120302],"mapped",[97]],[[120303,120303],"mapped",[98]],[[120304,120304],"mapped",[99]],[[120305,120305],"mapped",[100]],[[120306,120306],"mapped",[101]],[[120307,120307],"mapped",[102]],[[120308,120308],"mapped",[103]],[[120309,120309],"mapped",[104]],[[120310,120310],"mapped",[105]],[[120311,120311],"mapped",[106]],[[120312,120312],"mapped",[107]],[[120313,120313],"mapped",[108]],[[120314,120314],"mapped",[109]],[[120315,120315],"mapped",[110]],[[120316,120316],"mapped",[111]],[[120317,120317],"mapped",[112]],[[120318,120318],"mapped",[113]],[[120319,120319],"mapped",[114]],[[120320,120320],"mapped",[115]],[[120321,120321],"mapped",[116]],[[120322,120322],"mapped",[117]],[[120323,120323],"mapped",[118]],[[120324,120324],"mapped",[119]],[[120325,120325],"mapped",[120]],[[120326,120326],"mapped",[121]],[[120327,120327],"mapped",[122]],[[120328,120328],"mapped",[97]],[[120329,120329],"mapped",[98]],[[120330,120330],"mapped",[99]],[[120331,120331],"mapped",[100]],[[120332,120332],"mapped",[101]],[[120333,120333],"mapped",[102]],[[120334,120334],"mapped",[103]],[[120335,120335],"mapped",[104]],[[120336,120336],"mapped",[105]],[[120337,120337],"mapped",[106]],[[120338,120338],"mapped",[107]],[[120339,120339],"mapped",[108]],[[120340,120340],"mapped",[109]],[[120341,120341],"mapped",[110]],[[120342,120342],"mapped",[111]],[[120343,120343],"mapped",[112]],[[120344,120344],"mapped",[113]],[[120345,120345],"mapped",[114]],[[120346,120346],"mapped",[115]],[[120347,120347],"mapped",[116]],[[120348,120348],"mapped",[117]],[[120349,120349],"mapped",[118]],[[120350,120350],"mapped",[119]],[[120351,120351],"mapped",[120]],[[120352,120352],"mapped",[121]],[[120353,120353],"mapped",[122]],[[120354,120354],"mapped",[97]],[[120355,120355],"mapped",[98]],[[120356,120356],"mapped",[99]],[[120357,120357],"mapped",[100]],[[120358,120358],"mapped",[101]],[[120359,120359],"mapped",[102]],[[120360,120360],"mapped",[103]],[[120361,120361],"mapped",[104]],[[120362,120362],"mapped",[105]],[[120363,120363],"mapped",[106]],[[120364,120364],"mapped",[107]],[[120365,120365],"mapped",[108]],[[120366,120366],"mapped",[109]],[[120367,120367],"mapped",[110]],[[120368,120368],"mapped",[111]],[[120369,120369],"mapped",[112]],[[120370,120370],"mapped",[113]],[[120371,120371],"mapped",[114]],[[120372,120372],"mapped",[115]],[[120373,120373],"mapped",[116]],[[120374,120374],"mapped",[117]],[[120375,120375],"mapped",[118]],[[120376,120376],"mapped",[119]],[[120377,120377],"mapped",[120]],[[120378,120378],"mapped",[121]],[[120379,120379],"mapped",[122]],[[120380,120380],"mapped",[97]],[[120381,120381],"mapped",[98]],[[120382,120382],"mapped",[99]],[[120383,120383],"mapped",[100]],[[120384,120384],"mapped",[101]],[[120385,120385],"mapped",[102]],[[120386,120386],"mapped",[103]],[[120387,120387],"mapped",[104]],[[120388,120388],"mapped",[105]],[[120389,120389],"mapped",[106]],[[120390,120390],"mapped",[107]],[[120391,120391],"mapped",[108]],[[120392,120392],"mapped",[109]],[[120393,120393],"mapped",[110]],[[120394,120394],"mapped",[111]],[[120395,120395],"mapped",[112]],[[120396,120396],"mapped",[113]],[[120397,120397],"mapped",[114]],[[120398,120398],"mapped",[115]],[[120399,120399],"mapped",[116]],[[120400,120400],"mapped",[117]],[[120401,120401],"mapped",[118]],[[120402,120402],"mapped",[119]],[[120403,120403],"mapped",[120]],[[120404,120404],"mapped",[121]],[[120405,120405],"mapped",[122]],[[120406,120406],"mapped",[97]],[[120407,120407],"mapped",[98]],[[120408,120408],"mapped",[99]],[[120409,120409],"mapped",[100]],[[120410,120410],"mapped",[101]],[[120411,120411],"mapped",[102]],[[120412,120412],"mapped",[103]],[[120413,120413],"mapped",[104]],[[120414,120414],"mapped",[105]],[[120415,120415],"mapped",[106]],[[120416,120416],"mapped",[107]],[[120417,120417],"mapped",[108]],[[120418,120418],"mapped",[109]],[[120419,120419],"mapped",[110]],[[120420,120420],"mapped",[111]],[[120421,120421],"mapped",[112]],[[120422,120422],"mapped",[113]],[[120423,120423],"mapped",[114]],[[120424,120424],"mapped",[115]],[[120425,120425],"mapped",[116]],[[120426,120426],"mapped",[117]],[[120427,120427],"mapped",[118]],[[120428,120428],"mapped",[119]],[[120429,120429],"mapped",[120]],[[120430,120430],"mapped",[121]],[[120431,120431],"mapped",[122]],[[120432,120432],"mapped",[97]],[[120433,120433],"mapped",[98]],[[120434,120434],"mapped",[99]],[[120435,120435],"mapped",[100]],[[120436,120436],"mapped",[101]],[[120437,120437],"mapped",[102]],[[120438,120438],"mapped",[103]],[[120439,120439],"mapped",[104]],[[120440,120440],"mapped",[105]],[[120441,120441],"mapped",[106]],[[120442,120442],"mapped",[107]],[[120443,120443],"mapped",[108]],[[120444,120444],"mapped",[109]],[[120445,120445],"mapped",[110]],[[120446,120446],"mapped",[111]],[[120447,120447],"mapped",[112]],[[120448,120448],"mapped",[113]],[[120449,120449],"mapped",[114]],[[120450,120450],"mapped",[115]],[[120451,120451],"mapped",[116]],[[120452,120452],"mapped",[117]],[[120453,120453],"mapped",[118]],[[120454,120454],"mapped",[119]],[[120455,120455],"mapped",[120]],[[120456,120456],"mapped",[121]],[[120457,120457],"mapped",[122]],[[120458,120458],"mapped",[97]],[[120459,120459],"mapped",[98]],[[120460,120460],"mapped",[99]],[[120461,120461],"mapped",[100]],[[120462,120462],"mapped",[101]],[[120463,120463],"mapped",[102]],[[120464,120464],"mapped",[103]],[[120465,120465],"mapped",[104]],[[120466,120466],"mapped",[105]],[[120467,120467],"mapped",[106]],[[120468,120468],"mapped",[107]],[[120469,120469],"mapped",[108]],[[120470,120470],"mapped",[109]],[[120471,120471],"mapped",[110]],[[120472,120472],"mapped",[111]],[[120473,120473],"mapped",[112]],[[120474,120474],"mapped",[113]],[[120475,120475],"mapped",[114]],[[120476,120476],"mapped",[115]],[[120477,120477],"mapped",[116]],[[120478,120478],"mapped",[117]],[[120479,120479],"mapped",[118]],[[120480,120480],"mapped",[119]],[[120481,120481],"mapped",[120]],[[120482,120482],"mapped",[121]],[[120483,120483],"mapped",[122]],[[120484,120484],"mapped",[305]],[[120485,120485],"mapped",[567]],[[120486,120487],"disallowed"],[[120488,120488],"mapped",[945]],[[120489,120489],"mapped",[946]],[[120490,120490],"mapped",[947]],[[120491,120491],"mapped",[948]],[[120492,120492],"mapped",[949]],[[120493,120493],"mapped",[950]],[[120494,120494],"mapped",[951]],[[120495,120495],"mapped",[952]],[[120496,120496],"mapped",[953]],[[120497,120497],"mapped",[954]],[[120498,120498],"mapped",[955]],[[120499,120499],"mapped",[956]],[[120500,120500],"mapped",[957]],[[120501,120501],"mapped",[958]],[[120502,120502],"mapped",[959]],[[120503,120503],"mapped",[960]],[[120504,120504],"mapped",[961]],[[120505,120505],"mapped",[952]],[[120506,120506],"mapped",[963]],[[120507,120507],"mapped",[964]],[[120508,120508],"mapped",[965]],[[120509,120509],"mapped",[966]],[[120510,120510],"mapped",[967]],[[120511,120511],"mapped",[968]],[[120512,120512],"mapped",[969]],[[120513,120513],"mapped",[8711]],[[120514,120514],"mapped",[945]],[[120515,120515],"mapped",[946]],[[120516,120516],"mapped",[947]],[[120517,120517],"mapped",[948]],[[120518,120518],"mapped",[949]],[[120519,120519],"mapped",[950]],[[120520,120520],"mapped",[951]],[[120521,120521],"mapped",[952]],[[120522,120522],"mapped",[953]],[[120523,120523],"mapped",[954]],[[120524,120524],"mapped",[955]],[[120525,120525],"mapped",[956]],[[120526,120526],"mapped",[957]],[[120527,120527],"mapped",[958]],[[120528,120528],"mapped",[959]],[[120529,120529],"mapped",[960]],[[120530,120530],"mapped",[961]],[[120531,120532],"mapped",[963]],[[120533,120533],"mapped",[964]],[[120534,120534],"mapped",[965]],[[120535,120535],"mapped",[966]],[[120536,120536],"mapped",[967]],[[120537,120537],"mapped",[968]],[[120538,120538],"mapped",[969]],[[120539,120539],"mapped",[8706]],[[120540,120540],"mapped",[949]],[[120541,120541],"mapped",[952]],[[120542,120542],"mapped",[954]],[[120543,120543],"mapped",[966]],[[120544,120544],"mapped",[961]],[[120545,120545],"mapped",[960]],[[120546,120546],"mapped",[945]],[[120547,120547],"mapped",[946]],[[120548,120548],"mapped",[947]],[[120549,120549],"mapped",[948]],[[120550,120550],"mapped",[949]],[[120551,120551],"mapped",[950]],[[120552,120552],"mapped",[951]],[[120553,120553],"mapped",[952]],[[120554,120554],"mapped",[953]],[[120555,120555],"mapped",[954]],[[120556,120556],"mapped",[955]],[[120557,120557],"mapped",[956]],[[120558,120558],"mapped",[957]],[[120559,120559],"mapped",[958]],[[120560,120560],"mapped",[959]],[[120561,120561],"mapped",[960]],[[120562,120562],"mapped",[961]],[[120563,120563],"mapped",[952]],[[120564,120564],"mapped",[963]],[[120565,120565],"mapped",[964]],[[120566,120566],"mapped",[965]],[[120567,120567],"mapped",[966]],[[120568,120568],"mapped",[967]],[[120569,120569],"mapped",[968]],[[120570,120570],"mapped",[969]],[[120571,120571],"mapped",[8711]],[[120572,120572],"mapped",[945]],[[120573,120573],"mapped",[946]],[[120574,120574],"mapped",[947]],[[120575,120575],"mapped",[948]],[[120576,120576],"mapped",[949]],[[120577,120577],"mapped",[950]],[[120578,120578],"mapped",[951]],[[120579,120579],"mapped",[952]],[[120580,120580],"mapped",[953]],[[120581,120581],"mapped",[954]],[[120582,120582],"mapped",[955]],[[120583,120583],"mapped",[956]],[[120584,120584],"mapped",[957]],[[120585,120585],"mapped",[958]],[[120586,120586],"mapped",[959]],[[120587,120587],"mapped",[960]],[[120588,120588],"mapped",[961]],[[120589,120590],"mapped",[963]],[[120591,120591],"mapped",[964]],[[120592,120592],"mapped",[965]],[[120593,120593],"mapped",[966]],[[120594,120594],"mapped",[967]],[[120595,120595],"mapped",[968]],[[120596,120596],"mapped",[969]],[[120597,120597],"mapped",[8706]],[[120598,120598],"mapped",[949]],[[120599,120599],"mapped",[952]],[[120600,120600],"mapped",[954]],[[120601,120601],"mapped",[966]],[[120602,120602],"mapped",[961]],[[120603,120603],"mapped",[960]],[[120604,120604],"mapped",[945]],[[120605,120605],"mapped",[946]],[[120606,120606],"mapped",[947]],[[120607,120607],"mapped",[948]],[[120608,120608],"mapped",[949]],[[120609,120609],"mapped",[950]],[[120610,120610],"mapped",[951]],[[120611,120611],"mapped",[952]],[[120612,120612],"mapped",[953]],[[120613,120613],"mapped",[954]],[[120614,120614],"mapped",[955]],[[120615,120615],"mapped",[956]],[[120616,120616],"mapped",[957]],[[120617,120617],"mapped",[958]],[[120618,120618],"mapped",[959]],[[120619,120619],"mapped",[960]],[[120620,120620],"mapped",[961]],[[120621,120621],"mapped",[952]],[[120622,120622],"mapped",[963]],[[120623,120623],"mapped",[964]],[[120624,120624],"mapped",[965]],[[120625,120625],"mapped",[966]],[[120626,120626],"mapped",[967]],[[120627,120627],"mapped",[968]],[[120628,120628],"mapped",[969]],[[120629,120629],"mapped",[8711]],[[120630,120630],"mapped",[945]],[[120631,120631],"mapped",[946]],[[120632,120632],"mapped",[947]],[[120633,120633],"mapped",[948]],[[120634,120634],"mapped",[949]],[[120635,120635],"mapped",[950]],[[120636,120636],"mapped",[951]],[[120637,120637],"mapped",[952]],[[120638,120638],"mapped",[953]],[[120639,120639],"mapped",[954]],[[120640,120640],"mapped",[955]],[[120641,120641],"mapped",[956]],[[120642,120642],"mapped",[957]],[[120643,120643],"mapped",[958]],[[120644,120644],"mapped",[959]],[[120645,120645],"mapped",[960]],[[120646,120646],"mapped",[961]],[[120647,120648],"mapped",[963]],[[120649,120649],"mapped",[964]],[[120650,120650],"mapped",[965]],[[120651,120651],"mapped",[966]],[[120652,120652],"mapped",[967]],[[120653,120653],"mapped",[968]],[[120654,120654],"mapped",[969]],[[120655,120655],"mapped",[8706]],[[120656,120656],"mapped",[949]],[[120657,120657],"mapped",[952]],[[120658,120658],"mapped",[954]],[[120659,120659],"mapped",[966]],[[120660,120660],"mapped",[961]],[[120661,120661],"mapped",[960]],[[120662,120662],"mapped",[945]],[[120663,120663],"mapped",[946]],[[120664,120664],"mapped",[947]],[[120665,120665],"mapped",[948]],[[120666,120666],"mapped",[949]],[[120667,120667],"mapped",[950]],[[120668,120668],"mapped",[951]],[[120669,120669],"mapped",[952]],[[120670,120670],"mapped",[953]],[[120671,120671],"mapped",[954]],[[120672,120672],"mapped",[955]],[[120673,120673],"mapped",[956]],[[120674,120674],"mapped",[957]],[[120675,120675],"mapped",[958]],[[120676,120676],"mapped",[959]],[[120677,120677],"mapped",[960]],[[120678,120678],"mapped",[961]],[[120679,120679],"mapped",[952]],[[120680,120680],"mapped",[963]],[[120681,120681],"mapped",[964]],[[120682,120682],"mapped",[965]],[[120683,120683],"mapped",[966]],[[120684,120684],"mapped",[967]],[[120685,120685],"mapped",[968]],[[120686,120686],"mapped",[969]],[[120687,120687],"mapped",[8711]],[[120688,120688],"mapped",[945]],[[120689,120689],"mapped",[946]],[[120690,120690],"mapped",[947]],[[120691,120691],"mapped",[948]],[[120692,120692],"mapped",[949]],[[120693,120693],"mapped",[950]],[[120694,120694],"mapped",[951]],[[120695,120695],"mapped",[952]],[[120696,120696],"mapped",[953]],[[120697,120697],"mapped",[954]],[[120698,120698],"mapped",[955]],[[120699,120699],"mapped",[956]],[[120700,120700],"mapped",[957]],[[120701,120701],"mapped",[958]],[[120702,120702],"mapped",[959]],[[120703,120703],"mapped",[960]],[[120704,120704],"mapped",[961]],[[120705,120706],"mapped",[963]],[[120707,120707],"mapped",[964]],[[120708,120708],"mapped",[965]],[[120709,120709],"mapped",[966]],[[120710,120710],"mapped",[967]],[[120711,120711],"mapped",[968]],[[120712,120712],"mapped",[969]],[[120713,120713],"mapped",[8706]],[[120714,120714],"mapped",[949]],[[120715,120715],"mapped",[952]],[[120716,120716],"mapped",[954]],[[120717,120717],"mapped",[966]],[[120718,120718],"mapped",[961]],[[120719,120719],"mapped",[960]],[[120720,120720],"mapped",[945]],[[120721,120721],"mapped",[946]],[[120722,120722],"mapped",[947]],[[120723,120723],"mapped",[948]],[[120724,120724],"mapped",[949]],[[120725,120725],"mapped",[950]],[[120726,120726],"mapped",[951]],[[120727,120727],"mapped",[952]],[[120728,120728],"mapped",[953]],[[120729,120729],"mapped",[954]],[[120730,120730],"mapped",[955]],[[120731,120731],"mapped",[956]],[[120732,120732],"mapped",[957]],[[120733,120733],"mapped",[958]],[[120734,120734],"mapped",[959]],[[120735,120735],"mapped",[960]],[[120736,120736],"mapped",[961]],[[120737,120737],"mapped",[952]],[[120738,120738],"mapped",[963]],[[120739,120739],"mapped",[964]],[[120740,120740],"mapped",[965]],[[120741,120741],"mapped",[966]],[[120742,120742],"mapped",[967]],[[120743,120743],"mapped",[968]],[[120744,120744],"mapped",[969]],[[120745,120745],"mapped",[8711]],[[120746,120746],"mapped",[945]],[[120747,120747],"mapped",[946]],[[120748,120748],"mapped",[947]],[[120749,120749],"mapped",[948]],[[120750,120750],"mapped",[949]],[[120751,120751],"mapped",[950]],[[120752,120752],"mapped",[951]],[[120753,120753],"mapped",[952]],[[120754,120754],"mapped",[953]],[[120755,120755],"mapped",[954]],[[120756,120756],"mapped",[955]],[[120757,120757],"mapped",[956]],[[120758,120758],"mapped",[957]],[[120759,120759],"mapped",[958]],[[120760,120760],"mapped",[959]],[[120761,120761],"mapped",[960]],[[120762,120762],"mapped",[961]],[[120763,120764],"mapped",[963]],[[120765,120765],"mapped",[964]],[[120766,120766],"mapped",[965]],[[120767,120767],"mapped",[966]],[[120768,120768],"mapped",[967]],[[120769,120769],"mapped",[968]],[[120770,120770],"mapped",[969]],[[120771,120771],"mapped",[8706]],[[120772,120772],"mapped",[949]],[[120773,120773],"mapped",[952]],[[120774,120774],"mapped",[954]],[[120775,120775],"mapped",[966]],[[120776,120776],"mapped",[961]],[[120777,120777],"mapped",[960]],[[120778,120779],"mapped",[989]],[[120780,120781],"disallowed"],[[120782,120782],"mapped",[48]],[[120783,120783],"mapped",[49]],[[120784,120784],"mapped",[50]],[[120785,120785],"mapped",[51]],[[120786,120786],"mapped",[52]],[[120787,120787],"mapped",[53]],[[120788,120788],"mapped",[54]],[[120789,120789],"mapped",[55]],[[120790,120790],"mapped",[56]],[[120791,120791],"mapped",[57]],[[120792,120792],"mapped",[48]],[[120793,120793],"mapped",[49]],[[120794,120794],"mapped",[50]],[[120795,120795],"mapped",[51]],[[120796,120796],"mapped",[52]],[[120797,120797],"mapped",[53]],[[120798,120798],"mapped",[54]],[[120799,120799],"mapped",[55]],[[120800,120800],"mapped",[56]],[[120801,120801],"mapped",[57]],[[120802,120802],"mapped",[48]],[[120803,120803],"mapped",[49]],[[120804,120804],"mapped",[50]],[[120805,120805],"mapped",[51]],[[120806,120806],"mapped",[52]],[[120807,120807],"mapped",[53]],[[120808,120808],"mapped",[54]],[[120809,120809],"mapped",[55]],[[120810,120810],"mapped",[56]],[[120811,120811],"mapped",[57]],[[120812,120812],"mapped",[48]],[[120813,120813],"mapped",[49]],[[120814,120814],"mapped",[50]],[[120815,120815],"mapped",[51]],[[120816,120816],"mapped",[52]],[[120817,120817],"mapped",[53]],[[120818,120818],"mapped",[54]],[[120819,120819],"mapped",[55]],[[120820,120820],"mapped",[56]],[[120821,120821],"mapped",[57]],[[120822,120822],"mapped",[48]],[[120823,120823],"mapped",[49]],[[120824,120824],"mapped",[50]],[[120825,120825],"mapped",[51]],[[120826,120826],"mapped",[52]],[[120827,120827],"mapped",[53]],[[120828,120828],"mapped",[54]],[[120829,120829],"mapped",[55]],[[120830,120830],"mapped",[56]],[[120831,120831],"mapped",[57]],[[120832,121343],"valid",[],"NV8"],[[121344,121398],"valid"],[[121399,121402],"valid",[],"NV8"],[[121403,121452],"valid"],[[121453,121460],"valid",[],"NV8"],[[121461,121461],"valid"],[[121462,121475],"valid",[],"NV8"],[[121476,121476],"valid"],[[121477,121483],"valid",[],"NV8"],[[121484,121498],"disallowed"],[[121499,121503],"valid"],[[121504,121504],"disallowed"],[[121505,121519],"valid"],[[121520,124927],"disallowed"],[[124928,125124],"valid"],[[125125,125126],"disallowed"],[[125127,125135],"valid",[],"NV8"],[[125136,125142],"valid"],[[125143,126463],"disallowed"],[[126464,126464],"mapped",[1575]],[[126465,126465],"mapped",[1576]],[[126466,126466],"mapped",[1580]],[[126467,126467],"mapped",[1583]],[[126468,126468],"disallowed"],[[126469,126469],"mapped",[1608]],[[126470,126470],"mapped",[1586]],[[126471,126471],"mapped",[1581]],[[126472,126472],"mapped",[1591]],[[126473,126473],"mapped",[1610]],[[126474,126474],"mapped",[1603]],[[126475,126475],"mapped",[1604]],[[126476,126476],"mapped",[1605]],[[126477,126477],"mapped",[1606]],[[126478,126478],"mapped",[1587]],[[126479,126479],"mapped",[1593]],[[126480,126480],"mapped",[1601]],[[126481,126481],"mapped",[1589]],[[126482,126482],"mapped",[1602]],[[126483,126483],"mapped",[1585]],[[126484,126484],"mapped",[1588]],[[126485,126485],"mapped",[1578]],[[126486,126486],"mapped",[1579]],[[126487,126487],"mapped",[1582]],[[126488,126488],"mapped",[1584]],[[126489,126489],"mapped",[1590]],[[126490,126490],"mapped",[1592]],[[126491,126491],"mapped",[1594]],[[126492,126492],"mapped",[1646]],[[126493,126493],"mapped",[1722]],[[126494,126494],"mapped",[1697]],[[126495,126495],"mapped",[1647]],[[126496,126496],"disallowed"],[[126497,126497],"mapped",[1576]],[[126498,126498],"mapped",[1580]],[[126499,126499],"disallowed"],[[126500,126500],"mapped",[1607]],[[126501,126502],"disallowed"],[[126503,126503],"mapped",[1581]],[[126504,126504],"disallowed"],[[126505,126505],"mapped",[1610]],[[126506,126506],"mapped",[1603]],[[126507,126507],"mapped",[1604]],[[126508,126508],"mapped",[1605]],[[126509,126509],"mapped",[1606]],[[126510,126510],"mapped",[1587]],[[126511,126511],"mapped",[1593]],[[126512,126512],"mapped",[1601]],[[126513,126513],"mapped",[1589]],[[126514,126514],"mapped",[1602]],[[126515,126515],"disallowed"],[[126516,126516],"mapped",[1588]],[[126517,126517],"mapped",[1578]],[[126518,126518],"mapped",[1579]],[[126519,126519],"mapped",[1582]],[[126520,126520],"disallowed"],[[126521,126521],"mapped",[1590]],[[126522,126522],"disallowed"],[[126523,126523],"mapped",[1594]],[[126524,126529],"disallowed"],[[126530,126530],"mapped",[1580]],[[126531,126534],"disallowed"],[[126535,126535],"mapped",[1581]],[[126536,126536],"disallowed"],[[126537,126537],"mapped",[1610]],[[126538,126538],"disallowed"],[[126539,126539],"mapped",[1604]],[[126540,126540],"disallowed"],[[126541,126541],"mapped",[1606]],[[126542,126542],"mapped",[1587]],[[126543,126543],"mapped",[1593]],[[126544,126544],"disallowed"],[[126545,126545],"mapped",[1589]],[[126546,126546],"mapped",[1602]],[[126547,126547],"disallowed"],[[126548,126548],"mapped",[1588]],[[126549,126550],"disallowed"],[[126551,126551],"mapped",[1582]],[[126552,126552],"disallowed"],[[126553,126553],"mapped",[1590]],[[126554,126554],"disallowed"],[[126555,126555],"mapped",[1594]],[[126556,126556],"disallowed"],[[126557,126557],"mapped",[1722]],[[126558,126558],"disallowed"],[[126559,126559],"mapped",[1647]],[[126560,126560],"disallowed"],[[126561,126561],"mapped",[1576]],[[126562,126562],"mapped",[1580]],[[126563,126563],"disallowed"],[[126564,126564],"mapped",[1607]],[[126565,126566],"disallowed"],[[126567,126567],"mapped",[1581]],[[126568,126568],"mapped",[1591]],[[126569,126569],"mapped",[1610]],[[126570,126570],"mapped",[1603]],[[126571,126571],"disallowed"],[[126572,126572],"mapped",[1605]],[[126573,126573],"mapped",[1606]],[[126574,126574],"mapped",[1587]],[[126575,126575],"mapped",[1593]],[[126576,126576],"mapped",[1601]],[[126577,126577],"mapped",[1589]],[[126578,126578],"mapped",[1602]],[[126579,126579],"disallowed"],[[126580,126580],"mapped",[1588]],[[126581,126581],"mapped",[1578]],[[126582,126582],"mapped",[1579]],[[126583,126583],"mapped",[1582]],[[126584,126584],"disallowed"],[[126585,126585],"mapped",[1590]],[[126586,126586],"mapped",[1592]],[[126587,126587],"mapped",[1594]],[[126588,126588],"mapped",[1646]],[[126589,126589],"disallowed"],[[126590,126590],"mapped",[1697]],[[126591,126591],"disallowed"],[[126592,126592],"mapped",[1575]],[[126593,126593],"mapped",[1576]],[[126594,126594],"mapped",[1580]],[[126595,126595],"mapped",[1583]],[[126596,126596],"mapped",[1607]],[[126597,126597],"mapped",[1608]],[[126598,126598],"mapped",[1586]],[[126599,126599],"mapped",[1581]],[[126600,126600],"mapped",[1591]],[[126601,126601],"mapped",[1610]],[[126602,126602],"disallowed"],[[126603,126603],"mapped",[1604]],[[126604,126604],"mapped",[1605]],[[126605,126605],"mapped",[1606]],[[126606,126606],"mapped",[1587]],[[126607,126607],"mapped",[1593]],[[126608,126608],"mapped",[1601]],[[126609,126609],"mapped",[1589]],[[126610,126610],"mapped",[1602]],[[126611,126611],"mapped",[1585]],[[126612,126612],"mapped",[1588]],[[126613,126613],"mapped",[1578]],[[126614,126614],"mapped",[1579]],[[126615,126615],"mapped",[1582]],[[126616,126616],"mapped",[1584]],[[126617,126617],"mapped",[1590]],[[126618,126618],"mapped",[1592]],[[126619,126619],"mapped",[1594]],[[126620,126624],"disallowed"],[[126625,126625],"mapped",[1576]],[[126626,126626],"mapped",[1580]],[[126627,126627],"mapped",[1583]],[[126628,126628],"disallowed"],[[126629,126629],"mapped",[1608]],[[126630,126630],"mapped",[1586]],[[126631,126631],"mapped",[1581]],[[126632,126632],"mapped",[1591]],[[126633,126633],"mapped",[1610]],[[126634,126634],"disallowed"],[[126635,126635],"mapped",[1604]],[[126636,126636],"mapped",[1605]],[[126637,126637],"mapped",[1606]],[[126638,126638],"mapped",[1587]],[[126639,126639],"mapped",[1593]],[[126640,126640],"mapped",[1601]],[[126641,126641],"mapped",[1589]],[[126642,126642],"mapped",[1602]],[[126643,126643],"mapped",[1585]],[[126644,126644],"mapped",[1588]],[[126645,126645],"mapped",[1578]],[[126646,126646],"mapped",[1579]],[[126647,126647],"mapped",[1582]],[[126648,126648],"mapped",[1584]],[[126649,126649],"mapped",[1590]],[[126650,126650],"mapped",[1592]],[[126651,126651],"mapped",[1594]],[[126652,126703],"disallowed"],[[126704,126705],"valid",[],"NV8"],[[126706,126975],"disallowed"],[[126976,127019],"valid",[],"NV8"],[[127020,127023],"disallowed"],[[127024,127123],"valid",[],"NV8"],[[127124,127135],"disallowed"],[[127136,127150],"valid",[],"NV8"],[[127151,127152],"disallowed"],[[127153,127166],"valid",[],"NV8"],[[127167,127167],"valid",[],"NV8"],[[127168,127168],"disallowed"],[[127169,127183],"valid",[],"NV8"],[[127184,127184],"disallowed"],[[127185,127199],"valid",[],"NV8"],[[127200,127221],"valid",[],"NV8"],[[127222,127231],"disallowed"],[[127232,127232],"disallowed"],[[127233,127233],"disallowed_STD3_mapped",[48,44]],[[127234,127234],"disallowed_STD3_mapped",[49,44]],[[127235,127235],"disallowed_STD3_mapped",[50,44]],[[127236,127236],"disallowed_STD3_mapped",[51,44]],[[127237,127237],"disallowed_STD3_mapped",[52,44]],[[127238,127238],"disallowed_STD3_mapped",[53,44]],[[127239,127239],"disallowed_STD3_mapped",[54,44]],[[127240,127240],"disallowed_STD3_mapped",[55,44]],[[127241,127241],"disallowed_STD3_mapped",[56,44]],[[127242,127242],"disallowed_STD3_mapped",[57,44]],[[127243,127244],"valid",[],"NV8"],[[127245,127247],"disallowed"],[[127248,127248],"disallowed_STD3_mapped",[40,97,41]],[[127249,127249],"disallowed_STD3_mapped",[40,98,41]],[[127250,127250],"disallowed_STD3_mapped",[40,99,41]],[[127251,127251],"disallowed_STD3_mapped",[40,100,41]],[[127252,127252],"disallowed_STD3_mapped",[40,101,41]],[[127253,127253],"disallowed_STD3_mapped",[40,102,41]],[[127254,127254],"disallowed_STD3_mapped",[40,103,41]],[[127255,127255],"disallowed_STD3_mapped",[40,104,41]],[[127256,127256],"disallowed_STD3_mapped",[40,105,41]],[[127257,127257],"disallowed_STD3_mapped",[40,106,41]],[[127258,127258],"disallowed_STD3_mapped",[40,107,41]],[[127259,127259],"disallowed_STD3_mapped",[40,108,41]],[[127260,127260],"disallowed_STD3_mapped",[40,109,41]],[[127261,127261],"disallowed_STD3_mapped",[40,110,41]],[[127262,127262],"disallowed_STD3_mapped",[40,111,41]],[[127263,127263],"disallowed_STD3_mapped",[40,112,41]],[[127264,127264],"disallowed_STD3_mapped",[40,113,41]],[[127265,127265],"disallowed_STD3_mapped",[40,114,41]],[[127266,127266],"disallowed_STD3_mapped",[40,115,41]],[[127267,127267],"disallowed_STD3_mapped",[40,116,41]],[[127268,127268],"disallowed_STD3_mapped",[40,117,41]],[[127269,127269],"disallowed_STD3_mapped",[40,118,41]],[[127270,127270],"disallowed_STD3_mapped",[40,119,41]],[[127271,127271],"disallowed_STD3_mapped",[40,120,41]],[[127272,127272],"disallowed_STD3_mapped",[40,121,41]],[[127273,127273],"disallowed_STD3_mapped",[40,122,41]],[[127274,127274],"mapped",[12308,115,12309]],[[127275,127275],"mapped",[99]],[[127276,127276],"mapped",[114]],[[127277,127277],"mapped",[99,100]],[[127278,127278],"mapped",[119,122]],[[127279,127279],"disallowed"],[[127280,127280],"mapped",[97]],[[127281,127281],"mapped",[98]],[[127282,127282],"mapped",[99]],[[127283,127283],"mapped",[100]],[[127284,127284],"mapped",[101]],[[127285,127285],"mapped",[102]],[[127286,127286],"mapped",[103]],[[127287,127287],"mapped",[104]],[[127288,127288],"mapped",[105]],[[127289,127289],"mapped",[106]],[[127290,127290],"mapped",[107]],[[127291,127291],"mapped",[108]],[[127292,127292],"mapped",[109]],[[127293,127293],"mapped",[110]],[[127294,127294],"mapped",[111]],[[127295,127295],"mapped",[112]],[[127296,127296],"mapped",[113]],[[127297,127297],"mapped",[114]],[[127298,127298],"mapped",[115]],[[127299,127299],"mapped",[116]],[[127300,127300],"mapped",[117]],[[127301,127301],"mapped",[118]],[[127302,127302],"mapped",[119]],[[127303,127303],"mapped",[120]],[[127304,127304],"mapped",[121]],[[127305,127305],"mapped",[122]],[[127306,127306],"mapped",[104,118]],[[127307,127307],"mapped",[109,118]],[[127308,127308],"mapped",[115,100]],[[127309,127309],"mapped",[115,115]],[[127310,127310],"mapped",[112,112,118]],[[127311,127311],"mapped",[119,99]],[[127312,127318],"valid",[],"NV8"],[[127319,127319],"valid",[],"NV8"],[[127320,127326],"valid",[],"NV8"],[[127327,127327],"valid",[],"NV8"],[[127328,127337],"valid",[],"NV8"],[[127338,127338],"mapped",[109,99]],[[127339,127339],"mapped",[109,100]],[[127340,127343],"disallowed"],[[127344,127352],"valid",[],"NV8"],[[127353,127353],"valid",[],"NV8"],[[127354,127354],"valid",[],"NV8"],[[127355,127356],"valid",[],"NV8"],[[127357,127358],"valid",[],"NV8"],[[127359,127359],"valid",[],"NV8"],[[127360,127369],"valid",[],"NV8"],[[127370,127373],"valid",[],"NV8"],[[127374,127375],"valid",[],"NV8"],[[127376,127376],"mapped",[100,106]],[[127377,127386],"valid",[],"NV8"],[[127387,127461],"disallowed"],[[127462,127487],"valid",[],"NV8"],[[127488,127488],"mapped",[12411,12363]],[[127489,127489],"mapped",[12467,12467]],[[127490,127490],"mapped",[12469]],[[127491,127503],"disallowed"],[[127504,127504],"mapped",[25163]],[[127505,127505],"mapped",[23383]],[[127506,127506],"mapped",[21452]],[[127507,127507],"mapped",[12487]],[[127508,127508],"mapped",[20108]],[[127509,127509],"mapped",[22810]],[[127510,127510],"mapped",[35299]],[[127511,127511],"mapped",[22825]],[[127512,127512],"mapped",[20132]],[[127513,127513],"mapped",[26144]],[[127514,127514],"mapped",[28961]],[[127515,127515],"mapped",[26009]],[[127516,127516],"mapped",[21069]],[[127517,127517],"mapped",[24460]],[[127518,127518],"mapped",[20877]],[[127519,127519],"mapped",[26032]],[[127520,127520],"mapped",[21021]],[[127521,127521],"mapped",[32066]],[[127522,127522],"mapped",[29983]],[[127523,127523],"mapped",[36009]],[[127524,127524],"mapped",[22768]],[[127525,127525],"mapped",[21561]],[[127526,127526],"mapped",[28436]],[[127527,127527],"mapped",[25237]],[[127528,127528],"mapped",[25429]],[[127529,127529],"mapped",[19968]],[[127530,127530],"mapped",[19977]],[[127531,127531],"mapped",[36938]],[[127532,127532],"mapped",[24038]],[[127533,127533],"mapped",[20013]],[[127534,127534],"mapped",[21491]],[[127535,127535],"mapped",[25351]],[[127536,127536],"mapped",[36208]],[[127537,127537],"mapped",[25171]],[[127538,127538],"mapped",[31105]],[[127539,127539],"mapped",[31354]],[[127540,127540],"mapped",[21512]],[[127541,127541],"mapped",[28288]],[[127542,127542],"mapped",[26377]],[[127543,127543],"mapped",[26376]],[[127544,127544],"mapped",[30003]],[[127545,127545],"mapped",[21106]],[[127546,127546],"mapped",[21942]],[[127547,127551],"disallowed"],[[127552,127552],"mapped",[12308,26412,12309]],[[127553,127553],"mapped",[12308,19977,12309]],[[127554,127554],"mapped",[12308,20108,12309]],[[127555,127555],"mapped",[12308,23433,12309]],[[127556,127556],"mapped",[12308,28857,12309]],[[127557,127557],"mapped",[12308,25171,12309]],[[127558,127558],"mapped",[12308,30423,12309]],[[127559,127559],"mapped",[12308,21213,12309]],[[127560,127560],"mapped",[12308,25943,12309]],[[127561,127567],"disallowed"],[[127568,127568],"mapped",[24471]],[[127569,127569],"mapped",[21487]],[[127570,127743],"disallowed"],[[127744,127776],"valid",[],"NV8"],[[127777,127788],"valid",[],"NV8"],[[127789,127791],"valid",[],"NV8"],[[127792,127797],"valid",[],"NV8"],[[127798,127798],"valid",[],"NV8"],[[127799,127868],"valid",[],"NV8"],[[127869,127869],"valid",[],"NV8"],[[127870,127871],"valid",[],"NV8"],[[127872,127891],"valid",[],"NV8"],[[127892,127903],"valid",[],"NV8"],[[127904,127940],"valid",[],"NV8"],[[127941,127941],"valid",[],"NV8"],[[127942,127946],"valid",[],"NV8"],[[127947,127950],"valid",[],"NV8"],[[127951,127955],"valid",[],"NV8"],[[127956,127967],"valid",[],"NV8"],[[127968,127984],"valid",[],"NV8"],[[127985,127991],"valid",[],"NV8"],[[127992,127999],"valid",[],"NV8"],[[128000,128062],"valid",[],"NV8"],[[128063,128063],"valid",[],"NV8"],[[128064,128064],"valid",[],"NV8"],[[128065,128065],"valid",[],"NV8"],[[128066,128247],"valid",[],"NV8"],[[128248,128248],"valid",[],"NV8"],[[128249,128252],"valid",[],"NV8"],[[128253,128254],"valid",[],"NV8"],[[128255,128255],"valid",[],"NV8"],[[128256,128317],"valid",[],"NV8"],[[128318,128319],"valid",[],"NV8"],[[128320,128323],"valid",[],"NV8"],[[128324,128330],"valid",[],"NV8"],[[128331,128335],"valid",[],"NV8"],[[128336,128359],"valid",[],"NV8"],[[128360,128377],"valid",[],"NV8"],[[128378,128378],"disallowed"],[[128379,128419],"valid",[],"NV8"],[[128420,128420],"disallowed"],[[128421,128506],"valid",[],"NV8"],[[128507,128511],"valid",[],"NV8"],[[128512,128512],"valid",[],"NV8"],[[128513,128528],"valid",[],"NV8"],[[128529,128529],"valid",[],"NV8"],[[128530,128532],"valid",[],"NV8"],[[128533,128533],"valid",[],"NV8"],[[128534,128534],"valid",[],"NV8"],[[128535,128535],"valid",[],"NV8"],[[128536,128536],"valid",[],"NV8"],[[128537,128537],"valid",[],"NV8"],[[128538,128538],"valid",[],"NV8"],[[128539,128539],"valid",[],"NV8"],[[128540,128542],"valid",[],"NV8"],[[128543,128543],"valid",[],"NV8"],[[128544,128549],"valid",[],"NV8"],[[128550,128551],"valid",[],"NV8"],[[128552,128555],"valid",[],"NV8"],[[128556,128556],"valid",[],"NV8"],[[128557,128557],"valid",[],"NV8"],[[128558,128559],"valid",[],"NV8"],[[128560,128563],"valid",[],"NV8"],[[128564,128564],"valid",[],"NV8"],[[128565,128576],"valid",[],"NV8"],[[128577,128578],"valid",[],"NV8"],[[128579,128580],"valid",[],"NV8"],[[128581,128591],"valid",[],"NV8"],[[128592,128639],"valid",[],"NV8"],[[128640,128709],"valid",[],"NV8"],[[128710,128719],"valid",[],"NV8"],[[128720,128720],"valid",[],"NV8"],[[128721,128735],"disallowed"],[[128736,128748],"valid",[],"NV8"],[[128749,128751],"disallowed"],[[128752,128755],"valid",[],"NV8"],[[128756,128767],"disallowed"],[[128768,128883],"valid",[],"NV8"],[[128884,128895],"disallowed"],[[128896,128980],"valid",[],"NV8"],[[128981,129023],"disallowed"],[[129024,129035],"valid",[],"NV8"],[[129036,129039],"disallowed"],[[129040,129095],"valid",[],"NV8"],[[129096,129103],"disallowed"],[[129104,129113],"valid",[],"NV8"],[[129114,129119],"disallowed"],[[129120,129159],"valid",[],"NV8"],[[129160,129167],"disallowed"],[[129168,129197],"valid",[],"NV8"],[[129198,129295],"disallowed"],[[129296,129304],"valid",[],"NV8"],[[129305,129407],"disallowed"],[[129408,129412],"valid",[],"NV8"],[[129413,129471],"disallowed"],[[129472,129472],"valid",[],"NV8"],[[129473,131069],"disallowed"],[[131070,131071],"disallowed"],[[131072,173782],"valid"],[[173783,173823],"disallowed"],[[173824,177972],"valid"],[[177973,177983],"disallowed"],[[177984,178205],"valid"],[[178206,178207],"disallowed"],[[178208,183969],"valid"],[[183970,194559],"disallowed"],[[194560,194560],"mapped",[20029]],[[194561,194561],"mapped",[20024]],[[194562,194562],"mapped",[20033]],[[194563,194563],"mapped",[131362]],[[194564,194564],"mapped",[20320]],[[194565,194565],"mapped",[20398]],[[194566,194566],"mapped",[20411]],[[194567,194567],"mapped",[20482]],[[194568,194568],"mapped",[20602]],[[194569,194569],"mapped",[20633]],[[194570,194570],"mapped",[20711]],[[194571,194571],"mapped",[20687]],[[194572,194572],"mapped",[13470]],[[194573,194573],"mapped",[132666]],[[194574,194574],"mapped",[20813]],[[194575,194575],"mapped",[20820]],[[194576,194576],"mapped",[20836]],[[194577,194577],"mapped",[20855]],[[194578,194578],"mapped",[132380]],[[194579,194579],"mapped",[13497]],[[194580,194580],"mapped",[20839]],[[194581,194581],"mapped",[20877]],[[194582,194582],"mapped",[132427]],[[194583,194583],"mapped",[20887]],[[194584,194584],"mapped",[20900]],[[194585,194585],"mapped",[20172]],[[194586,194586],"mapped",[20908]],[[194587,194587],"mapped",[20917]],[[194588,194588],"mapped",[168415]],[[194589,194589],"mapped",[20981]],[[194590,194590],"mapped",[20995]],[[194591,194591],"mapped",[13535]],[[194592,194592],"mapped",[21051]],[[194593,194593],"mapped",[21062]],[[194594,194594],"mapped",[21106]],[[194595,194595],"mapped",[21111]],[[194596,194596],"mapped",[13589]],[[194597,194597],"mapped",[21191]],[[194598,194598],"mapped",[21193]],[[194599,194599],"mapped",[21220]],[[194600,194600],"mapped",[21242]],[[194601,194601],"mapped",[21253]],[[194602,194602],"mapped",[21254]],[[194603,194603],"mapped",[21271]],[[194604,194604],"mapped",[21321]],[[194605,194605],"mapped",[21329]],[[194606,194606],"mapped",[21338]],[[194607,194607],"mapped",[21363]],[[194608,194608],"mapped",[21373]],[[194609,194611],"mapped",[21375]],[[194612,194612],"mapped",[133676]],[[194613,194613],"mapped",[28784]],[[194614,194614],"mapped",[21450]],[[194615,194615],"mapped",[21471]],[[194616,194616],"mapped",[133987]],[[194617,194617],"mapped",[21483]],[[194618,194618],"mapped",[21489]],[[194619,194619],"mapped",[21510]],[[194620,194620],"mapped",[21662]],[[194621,194621],"mapped",[21560]],[[194622,194622],"mapped",[21576]],[[194623,194623],"mapped",[21608]],[[194624,194624],"mapped",[21666]],[[194625,194625],"mapped",[21750]],[[194626,194626],"mapped",[21776]],[[194627,194627],"mapped",[21843]],[[194628,194628],"mapped",[21859]],[[194629,194630],"mapped",[21892]],[[194631,194631],"mapped",[21913]],[[194632,194632],"mapped",[21931]],[[194633,194633],"mapped",[21939]],[[194634,194634],"mapped",[21954]],[[194635,194635],"mapped",[22294]],[[194636,194636],"mapped",[22022]],[[194637,194637],"mapped",[22295]],[[194638,194638],"mapped",[22097]],[[194639,194639],"mapped",[22132]],[[194640,194640],"mapped",[20999]],[[194641,194641],"mapped",[22766]],[[194642,194642],"mapped",[22478]],[[194643,194643],"mapped",[22516]],[[194644,194644],"mapped",[22541]],[[194645,194645],"mapped",[22411]],[[194646,194646],"mapped",[22578]],[[194647,194647],"mapped",[22577]],[[194648,194648],"mapped",[22700]],[[194649,194649],"mapped",[136420]],[[194650,194650],"mapped",[22770]],[[194651,194651],"mapped",[22775]],[[194652,194652],"mapped",[22790]],[[194653,194653],"mapped",[22810]],[[194654,194654],"mapped",[22818]],[[194655,194655],"mapped",[22882]],[[194656,194656],"mapped",[136872]],[[194657,194657],"mapped",[136938]],[[194658,194658],"mapped",[23020]],[[194659,194659],"mapped",[23067]],[[194660,194660],"mapped",[23079]],[[194661,194661],"mapped",[23000]],[[194662,194662],"mapped",[23142]],[[194663,194663],"mapped",[14062]],[[194664,194664],"disallowed"],[[194665,194665],"mapped",[23304]],[[194666,194667],"mapped",[23358]],[[194668,194668],"mapped",[137672]],[[194669,194669],"mapped",[23491]],[[194670,194670],"mapped",[23512]],[[194671,194671],"mapped",[23527]],[[194672,194672],"mapped",[23539]],[[194673,194673],"mapped",[138008]],[[194674,194674],"mapped",[23551]],[[194675,194675],"mapped",[23558]],[[194676,194676],"disallowed"],[[194677,194677],"mapped",[23586]],[[194678,194678],"mapped",[14209]],[[194679,194679],"mapped",[23648]],[[194680,194680],"mapped",[23662]],[[194681,194681],"mapped",[23744]],[[194682,194682],"mapped",[23693]],[[194683,194683],"mapped",[138724]],[[194684,194684],"mapped",[23875]],[[194685,194685],"mapped",[138726]],[[194686,194686],"mapped",[23918]],[[194687,194687],"mapped",[23915]],[[194688,194688],"mapped",[23932]],[[194689,194689],"mapped",[24033]],[[194690,194690],"mapped",[24034]],[[194691,194691],"mapped",[14383]],[[194692,194692],"mapped",[24061]],[[194693,194693],"mapped",[24104]],[[194694,194694],"mapped",[24125]],[[194695,194695],"mapped",[24169]],[[194696,194696],"mapped",[14434]],[[194697,194697],"mapped",[139651]],[[194698,194698],"mapped",[14460]],[[194699,194699],"mapped",[24240]],[[194700,194700],"mapped",[24243]],[[194701,194701],"mapped",[24246]],[[194702,194702],"mapped",[24266]],[[194703,194703],"mapped",[172946]],[[194704,194704],"mapped",[24318]],[[194705,194706],"mapped",[140081]],[[194707,194707],"mapped",[33281]],[[194708,194709],"mapped",[24354]],[[194710,194710],"mapped",[14535]],[[194711,194711],"mapped",[144056]],[[194712,194712],"mapped",[156122]],[[194713,194713],"mapped",[24418]],[[194714,194714],"mapped",[24427]],[[194715,194715],"mapped",[14563]],[[194716,194716],"mapped",[24474]],[[194717,194717],"mapped",[24525]],[[194718,194718],"mapped",[24535]],[[194719,194719],"mapped",[24569]],[[194720,194720],"mapped",[24705]],[[194721,194721],"mapped",[14650]],[[194722,194722],"mapped",[14620]],[[194723,194723],"mapped",[24724]],[[194724,194724],"mapped",[141012]],[[194725,194725],"mapped",[24775]],[[194726,194726],"mapped",[24904]],[[194727,194727],"mapped",[24908]],[[194728,194728],"mapped",[24910]],[[194729,194729],"mapped",[24908]],[[194730,194730],"mapped",[24954]],[[194731,194731],"mapped",[24974]],[[194732,194732],"mapped",[25010]],[[194733,194733],"mapped",[24996]],[[194734,194734],"mapped",[25007]],[[194735,194735],"mapped",[25054]],[[194736,194736],"mapped",[25074]],[[194737,194737],"mapped",[25078]],[[194738,194738],"mapped",[25104]],[[194739,194739],"mapped",[25115]],[[194740,194740],"mapped",[25181]],[[194741,194741],"mapped",[25265]],[[194742,194742],"mapped",[25300]],[[194743,194743],"mapped",[25424]],[[194744,194744],"mapped",[142092]],[[194745,194745],"mapped",[25405]],[[194746,194746],"mapped",[25340]],[[194747,194747],"mapped",[25448]],[[194748,194748],"mapped",[25475]],[[194749,194749],"mapped",[25572]],[[194750,194750],"mapped",[142321]],[[194751,194751],"mapped",[25634]],[[194752,194752],"mapped",[25541]],[[194753,194753],"mapped",[25513]],[[194754,194754],"mapped",[14894]],[[194755,194755],"mapped",[25705]],[[194756,194756],"mapped",[25726]],[[194757,194757],"mapped",[25757]],[[194758,194758],"mapped",[25719]],[[194759,194759],"mapped",[14956]],[[194760,194760],"mapped",[25935]],[[194761,194761],"mapped",[25964]],[[194762,194762],"mapped",[143370]],[[194763,194763],"mapped",[26083]],[[194764,194764],"mapped",[26360]],[[194765,194765],"mapped",[26185]],[[194766,194766],"mapped",[15129]],[[194767,194767],"mapped",[26257]],[[194768,194768],"mapped",[15112]],[[194769,194769],"mapped",[15076]],[[194770,194770],"mapped",[20882]],[[194771,194771],"mapped",[20885]],[[194772,194772],"mapped",[26368]],[[194773,194773],"mapped",[26268]],[[194774,194774],"mapped",[32941]],[[194775,194775],"mapped",[17369]],[[194776,194776],"mapped",[26391]],[[194777,194777],"mapped",[26395]],[[194778,194778],"mapped",[26401]],[[194779,194779],"mapped",[26462]],[[194780,194780],"mapped",[26451]],[[194781,194781],"mapped",[144323]],[[194782,194782],"mapped",[15177]],[[194783,194783],"mapped",[26618]],[[194784,194784],"mapped",[26501]],[[194785,194785],"mapped",[26706]],[[194786,194786],"mapped",[26757]],[[194787,194787],"mapped",[144493]],[[194788,194788],"mapped",[26766]],[[194789,194789],"mapped",[26655]],[[194790,194790],"mapped",[26900]],[[194791,194791],"mapped",[15261]],[[194792,194792],"mapped",[26946]],[[194793,194793],"mapped",[27043]],[[194794,194794],"mapped",[27114]],[[194795,194795],"mapped",[27304]],[[194796,194796],"mapped",[145059]],[[194797,194797],"mapped",[27355]],[[194798,194798],"mapped",[15384]],[[194799,194799],"mapped",[27425]],[[194800,194800],"mapped",[145575]],[[194801,194801],"mapped",[27476]],[[194802,194802],"mapped",[15438]],[[194803,194803],"mapped",[27506]],[[194804,194804],"mapped",[27551]],[[194805,194805],"mapped",[27578]],[[194806,194806],"mapped",[27579]],[[194807,194807],"mapped",[146061]],[[194808,194808],"mapped",[138507]],[[194809,194809],"mapped",[146170]],[[194810,194810],"mapped",[27726]],[[194811,194811],"mapped",[146620]],[[194812,194812],"mapped",[27839]],[[194813,194813],"mapped",[27853]],[[194814,194814],"mapped",[27751]],[[194815,194815],"mapped",[27926]],[[194816,194816],"mapped",[27966]],[[194817,194817],"mapped",[28023]],[[194818,194818],"mapped",[27969]],[[194819,194819],"mapped",[28009]],[[194820,194820],"mapped",[28024]],[[194821,194821],"mapped",[28037]],[[194822,194822],"mapped",[146718]],[[194823,194823],"mapped",[27956]],[[194824,194824],"mapped",[28207]],[[194825,194825],"mapped",[28270]],[[194826,194826],"mapped",[15667]],[[194827,194827],"mapped",[28363]],[[194828,194828],"mapped",[28359]],[[194829,194829],"mapped",[147153]],[[194830,194830],"mapped",[28153]],[[194831,194831],"mapped",[28526]],[[194832,194832],"mapped",[147294]],[[194833,194833],"mapped",[147342]],[[194834,194834],"mapped",[28614]],[[194835,194835],"mapped",[28729]],[[194836,194836],"mapped",[28702]],[[194837,194837],"mapped",[28699]],[[194838,194838],"mapped",[15766]],[[194839,194839],"mapped",[28746]],[[194840,194840],"mapped",[28797]],[[194841,194841],"mapped",[28791]],[[194842,194842],"mapped",[28845]],[[194843,194843],"mapped",[132389]],[[194844,194844],"mapped",[28997]],[[194845,194845],"mapped",[148067]],[[194846,194846],"mapped",[29084]],[[194847,194847],"disallowed"],[[194848,194848],"mapped",[29224]],[[194849,194849],"mapped",[29237]],[[194850,194850],"mapped",[29264]],[[194851,194851],"mapped",[149000]],[[194852,194852],"mapped",[29312]],[[194853,194853],"mapped",[29333]],[[194854,194854],"mapped",[149301]],[[194855,194855],"mapped",[149524]],[[194856,194856],"mapped",[29562]],[[194857,194857],"mapped",[29579]],[[194858,194858],"mapped",[16044]],[[194859,194859],"mapped",[29605]],[[194860,194861],"mapped",[16056]],[[194862,194862],"mapped",[29767]],[[194863,194863],"mapped",[29788]],[[194864,194864],"mapped",[29809]],[[194865,194865],"mapped",[29829]],[[194866,194866],"mapped",[29898]],[[194867,194867],"mapped",[16155]],[[194868,194868],"mapped",[29988]],[[194869,194869],"mapped",[150582]],[[194870,194870],"mapped",[30014]],[[194871,194871],"mapped",[150674]],[[194872,194872],"mapped",[30064]],[[194873,194873],"mapped",[139679]],[[194874,194874],"mapped",[30224]],[[194875,194875],"mapped",[151457]],[[194876,194876],"mapped",[151480]],[[194877,194877],"mapped",[151620]],[[194878,194878],"mapped",[16380]],[[194879,194879],"mapped",[16392]],[[194880,194880],"mapped",[30452]],[[194881,194881],"mapped",[151795]],[[194882,194882],"mapped",[151794]],[[194883,194883],"mapped",[151833]],[[194884,194884],"mapped",[151859]],[[194885,194885],"mapped",[30494]],[[194886,194887],"mapped",[30495]],[[194888,194888],"mapped",[30538]],[[194889,194889],"mapped",[16441]],[[194890,194890],"mapped",[30603]],[[194891,194891],"mapped",[16454]],[[194892,194892],"mapped",[16534]],[[194893,194893],"mapped",[152605]],[[194894,194894],"mapped",[30798]],[[194895,194895],"mapped",[30860]],[[194896,194896],"mapped",[30924]],[[194897,194897],"mapped",[16611]],[[194898,194898],"mapped",[153126]],[[194899,194899],"mapped",[31062]],[[194900,194900],"mapped",[153242]],[[194901,194901],"mapped",[153285]],[[194902,194902],"mapped",[31119]],[[194903,194903],"mapped",[31211]],[[194904,194904],"mapped",[16687]],[[194905,194905],"mapped",[31296]],[[194906,194906],"mapped",[31306]],[[194907,194907],"mapped",[31311]],[[194908,194908],"mapped",[153980]],[[194909,194910],"mapped",[154279]],[[194911,194911],"disallowed"],[[194912,194912],"mapped",[16898]],[[194913,194913],"mapped",[154539]],[[194914,194914],"mapped",[31686]],[[194915,194915],"mapped",[31689]],[[194916,194916],"mapped",[16935]],[[194917,194917],"mapped",[154752]],[[194918,194918],"mapped",[31954]],[[194919,194919],"mapped",[17056]],[[194920,194920],"mapped",[31976]],[[194921,194921],"mapped",[31971]],[[194922,194922],"mapped",[32000]],[[194923,194923],"mapped",[155526]],[[194924,194924],"mapped",[32099]],[[194925,194925],"mapped",[17153]],[[194926,194926],"mapped",[32199]],[[194927,194927],"mapped",[32258]],[[194928,194928],"mapped",[32325]],[[194929,194929],"mapped",[17204]],[[194930,194930],"mapped",[156200]],[[194931,194931],"mapped",[156231]],[[194932,194932],"mapped",[17241]],[[194933,194933],"mapped",[156377]],[[194934,194934],"mapped",[32634]],[[194935,194935],"mapped",[156478]],[[194936,194936],"mapped",[32661]],[[194937,194937],"mapped",[32762]],[[194938,194938],"mapped",[32773]],[[194939,194939],"mapped",[156890]],[[194940,194940],"mapped",[156963]],[[194941,194941],"mapped",[32864]],[[194942,194942],"mapped",[157096]],[[194943,194943],"mapped",[32880]],[[194944,194944],"mapped",[144223]],[[194945,194945],"mapped",[17365]],[[194946,194946],"mapped",[32946]],[[194947,194947],"mapped",[33027]],[[194948,194948],"mapped",[17419]],[[194949,194949],"mapped",[33086]],[[194950,194950],"mapped",[23221]],[[194951,194951],"mapped",[157607]],[[194952,194952],"mapped",[157621]],[[194953,194953],"mapped",[144275]],[[194954,194954],"mapped",[144284]],[[194955,194955],"mapped",[33281]],[[194956,194956],"mapped",[33284]],[[194957,194957],"mapped",[36766]],[[194958,194958],"mapped",[17515]],[[194959,194959],"mapped",[33425]],[[194960,194960],"mapped",[33419]],[[194961,194961],"mapped",[33437]],[[194962,194962],"mapped",[21171]],[[194963,194963],"mapped",[33457]],[[194964,194964],"mapped",[33459]],[[194965,194965],"mapped",[33469]],[[194966,194966],"mapped",[33510]],[[194967,194967],"mapped",[158524]],[[194968,194968],"mapped",[33509]],[[194969,194969],"mapped",[33565]],[[194970,194970],"mapped",[33635]],[[194971,194971],"mapped",[33709]],[[194972,194972],"mapped",[33571]],[[194973,194973],"mapped",[33725]],[[194974,194974],"mapped",[33767]],[[194975,194975],"mapped",[33879]],[[194976,194976],"mapped",[33619]],[[194977,194977],"mapped",[33738]],[[194978,194978],"mapped",[33740]],[[194979,194979],"mapped",[33756]],[[194980,194980],"mapped",[158774]],[[194981,194981],"mapped",[159083]],[[194982,194982],"mapped",[158933]],[[194983,194983],"mapped",[17707]],[[194984,194984],"mapped",[34033]],[[194985,194985],"mapped",[34035]],[[194986,194986],"mapped",[34070]],[[194987,194987],"mapped",[160714]],[[194988,194988],"mapped",[34148]],[[194989,194989],"mapped",[159532]],[[194990,194990],"mapped",[17757]],[[194991,194991],"mapped",[17761]],[[194992,194992],"mapped",[159665]],[[194993,194993],"mapped",[159954]],[[194994,194994],"mapped",[17771]],[[194995,194995],"mapped",[34384]],[[194996,194996],"mapped",[34396]],[[194997,194997],"mapped",[34407]],[[194998,194998],"mapped",[34409]],[[194999,194999],"mapped",[34473]],[[195000,195000],"mapped",[34440]],[[195001,195001],"mapped",[34574]],[[195002,195002],"mapped",[34530]],[[195003,195003],"mapped",[34681]],[[195004,195004],"mapped",[34600]],[[195005,195005],"mapped",[34667]],[[195006,195006],"mapped",[34694]],[[195007,195007],"disallowed"],[[195008,195008],"mapped",[34785]],[[195009,195009],"mapped",[34817]],[[195010,195010],"mapped",[17913]],[[195011,195011],"mapped",[34912]],[[195012,195012],"mapped",[34915]],[[195013,195013],"mapped",[161383]],[[195014,195014],"mapped",[35031]],[[195015,195015],"mapped",[35038]],[[195016,195016],"mapped",[17973]],[[195017,195017],"mapped",[35066]],[[195018,195018],"mapped",[13499]],[[195019,195019],"mapped",[161966]],[[195020,195020],"mapped",[162150]],[[195021,195021],"mapped",[18110]],[[195022,195022],"mapped",[18119]],[[195023,195023],"mapped",[35488]],[[195024,195024],"mapped",[35565]],[[195025,195025],"mapped",[35722]],[[195026,195026],"mapped",[35925]],[[195027,195027],"mapped",[162984]],[[195028,195028],"mapped",[36011]],[[195029,195029],"mapped",[36033]],[[195030,195030],"mapped",[36123]],[[195031,195031],"mapped",[36215]],[[195032,195032],"mapped",[163631]],[[195033,195033],"mapped",[133124]],[[195034,195034],"mapped",[36299]],[[195035,195035],"mapped",[36284]],[[195036,195036],"mapped",[36336]],[[195037,195037],"mapped",[133342]],[[195038,195038],"mapped",[36564]],[[195039,195039],"mapped",[36664]],[[195040,195040],"mapped",[165330]],[[195041,195041],"mapped",[165357]],[[195042,195042],"mapped",[37012]],[[195043,195043],"mapped",[37105]],[[195044,195044],"mapped",[37137]],[[195045,195045],"mapped",[165678]],[[195046,195046],"mapped",[37147]],[[195047,195047],"mapped",[37432]],[[195048,195048],"mapped",[37591]],[[195049,195049],"mapped",[37592]],[[195050,195050],"mapped",[37500]],[[195051,195051],"mapped",[37881]],[[195052,195052],"mapped",[37909]],[[195053,195053],"mapped",[166906]],[[195054,195054],"mapped",[38283]],[[195055,195055],"mapped",[18837]],[[195056,195056],"mapped",[38327]],[[195057,195057],"mapped",[167287]],[[195058,195058],"mapped",[18918]],[[195059,195059],"mapped",[38595]],[[195060,195060],"mapped",[23986]],[[195061,195061],"mapped",[38691]],[[195062,195062],"mapped",[168261]],[[195063,195063],"mapped",[168474]],[[195064,195064],"mapped",[19054]],[[195065,195065],"mapped",[19062]],[[195066,195066],"mapped",[38880]],[[195067,195067],"mapped",[168970]],[[195068,195068],"mapped",[19122]],[[195069,195069],"mapped",[169110]],[[195070,195071],"mapped",[38923]],[[195072,195072],"mapped",[38953]],[[195073,195073],"mapped",[169398]],[[195074,195074],"mapped",[39138]],[[195075,195075],"mapped",[19251]],[[195076,195076],"mapped",[39209]],[[195077,195077],"mapped",[39335]],[[195078,195078],"mapped",[39362]],[[195079,195079],"mapped",[39422]],[[195080,195080],"mapped",[19406]],[[195081,195081],"mapped",[170800]],[[195082,195082],"mapped",[39698]],[[195083,195083],"mapped",[40000]],[[195084,195084],"mapped",[40189]],[[195085,195085],"mapped",[19662]],[[195086,195086],"mapped",[19693]],[[195087,195087],"mapped",[40295]],[[195088,195088],"mapped",[172238]],[[195089,195089],"mapped",[19704]],[[195090,195090],"mapped",[172293]],[[195091,195091],"mapped",[172558]],[[195092,195092],"mapped",[172689]],[[195093,195093],"mapped",[40635]],[[195094,195094],"mapped",[19798]],[[195095,195095],"mapped",[40697]],[[195096,195096],"mapped",[40702]],[[195097,195097],"mapped",[40709]],[[195098,195098],"mapped",[40719]],[[195099,195099],"mapped",[40726]],[[195100,195100],"mapped",[40763]],[[195101,195101],"mapped",[173568]],[[195102,196605],"disallowed"],[[196606,196607],"disallowed"],[[196608,262141],"disallowed"],[[262142,262143],"disallowed"],[[262144,327677],"disallowed"],[[327678,327679],"disallowed"],[[327680,393213],"disallowed"],[[393214,393215],"disallowed"],[[393216,458749],"disallowed"],[[458750,458751],"disallowed"],[[458752,524285],"disallowed"],[[524286,524287],"disallowed"],[[524288,589821],"disallowed"],[[589822,589823],"disallowed"],[[589824,655357],"disallowed"],[[655358,655359],"disallowed"],[[655360,720893],"disallowed"],[[720894,720895],"disallowed"],[[720896,786429],"disallowed"],[[786430,786431],"disallowed"],[[786432,851965],"disallowed"],[[851966,851967],"disallowed"],[[851968,917501],"disallowed"],[[917502,917503],"disallowed"],[[917504,917504],"disallowed"],[[917505,917505],"disallowed"],[[917506,917535],"disallowed"],[[917536,917631],"disallowed"],[[917632,917759],"disallowed"],[[917760,917999],"ignored"],[[918000,983037],"disallowed"],[[983038,983039],"disallowed"],[[983040,1048573],"disallowed"],[[1048574,1048575],"disallowed"],[[1048576,1114109],"disallowed"],[[1114110,1114111],"disallowed"]]'); + +/***/ }) + +/******/ }); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __nccwpck_require__(moduleId) { +/******/ // Check if module is in cache +/******/ var cachedModule = __webpack_module_cache__[moduleId]; +/******/ if (cachedModule !== undefined) { +/******/ return cachedModule.exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ var threw = true; +/******/ try { +/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __nccwpck_require__); +/******/ threw = false; +/******/ } finally { +/******/ if(threw) delete __webpack_module_cache__[moduleId]; +/******/ } +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/compat get default export */ +/******/ (() => { +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __nccwpck_require__.n = (module) => { +/******/ var getter = module && module.__esModule ? +/******/ () => (module['default']) : +/******/ () => (module); +/******/ __nccwpck_require__.d(getter, { a: getter }); +/******/ return getter; +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __nccwpck_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__nccwpck_require__.o(definition, key) && !__nccwpck_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __nccwpck_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __nccwpck_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/compat */ +/******/ +/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/"; +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; +// This entry need to be wrapped in an IIFE because it need to be in strict mode. +(() => { +"use strict"; +__nccwpck_require__.r(__webpack_exports__); +/* harmony import */ var _supabase_supabase_js__WEBPACK_IMPORTED_MODULE_0__ = __nccwpck_require__(1206); +/* harmony import */ var _supabase_supabase_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__nccwpck_require__.n(_supabase_supabase_js__WEBPACK_IMPORTED_MODULE_0__); +const fs = __nccwpck_require__(7147) +const core = __nccwpck_require__(2186); +const { execSync } = __nccwpck_require__(2081); + + + +const supabaseUrl = core.getInput("supabase_uri"); +const supabaseKey = core.getInput("supabase_anon_key"); +const changedFiles = JSON.parse(core.getInput("changed_files") || "[]"); + +const ignoreDirectories = [ + "/actions/", + "/common/", + "/sources/" +] + +const shouldInclude = (file) => { + if (!(file.endsWith(".app.mjs") || file.endsWith(".app.ts"))) { + return false + } + for (let i = 0; i< ignoreDirectories.length; i++) { + const dirToIgnore = ignoreDirectories[i] + if (file.includes(dirToIgnore)) { + return false + } + } + return true +} + +function createMjsPayload(payload, appMjsFiles) { + for (let i = 0; i < appMjsFiles.length; i++) { + const filePath = appMjsFiles[i] + const app = filePath.split("/").pop().replace(".app.mjs", "") + const content = fs.readFileSync(filePath, { encoding: "utf-8" }) + payload.push({ + app, + app_file: content, + updated_at: new Date().toISOString(), + }); + } +} + +async function createTsPayload(payload, appTsFiles) { + if (appTsFiles.length > 0) { + console.log("Generating mjs files from ts files for: ", appTsFiles) + execSync(`pnpm install -r && pnpm run build`); + } + + for (let i = 0; i < appTsFiles.length; i++) { + const filePath = appTsFiles[i] + const app = filePath.split("/").pop().replace(".app.ts", "") + + console.log(`Building ${app}...`) + + const appDirectory = `components/${app}` + const content = fs.readFileSync(`${appDirectory}/dist/app/${app}.app.mjs`, {encoding: "utf-8"}) + payload.push({ + app, + app_file: content, + updated_at: new Date().toISOString(), + }); + } +} + +async function uploadToSupabase(payload) { + if (payload && payload.length) { + const supabase = (0,_supabase_supabase_js__WEBPACK_IMPORTED_MODULE_0__.createClient)(supabaseUrl, supabaseKey) + const { error } = await supabase + .from("registry_app_files") + .upsert(payload, { onConflict: 'app' }) + if (error) { + console.error(`Error bulk uploading files:`, error) + } else { + console.log(`Successfully bulk uploaded files`) + } + } +} + +async function run() { + const filesToUpsert = changedFiles.filter(shouldInclude) + + const appMjsFiles = filesToUpsert.filter(file => file.endsWith('.app.mjs')) + const appTsFiles = filesToUpsert.filter(file => file.endsWith('.app.ts')) + + const payload = []; + createMjsPayload(payload, appMjsFiles) + await createTsPayload(payload, appTsFiles) + await uploadToSupabase(payload) +} + +run().catch(error => core.setFailed(error ?? error?.message)); + +})(); + +module.exports = __webpack_exports__; +/******/ })() +; \ No newline at end of file diff --git a/.github/actions/push-registry-app-files-supabase/dist/licenses.txt b/.github/actions/push-registry-app-files-supabase/dist/licenses.txt new file mode 100644 index 0000000000000..ee706a4b20666 --- /dev/null +++ b/.github/actions/push-registry-app-files-supabase/dist/licenses.txt @@ -0,0 +1,566 @@ +@actions/core +MIT +The MIT License (MIT) + +Copyright 2019 GitHub + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +@actions/exec +MIT +The MIT License (MIT) + +Copyright 2019 GitHub + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +@actions/http-client +MIT +Actions Http Client for Node.js + +Copyright (c) GitHub, Inc. + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +@actions/io +MIT +The MIT License (MIT) + +Copyright 2019 GitHub + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +@fastify/busboy +MIT +Copyright Brian White. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + +@supabase/auth-js +MIT +MIT License + +Copyright (c) 2020 Supabase + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +@supabase/functions-js +MIT +MIT License + +Copyright (c) 2020 Supabase + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +@supabase/node-fetch +MIT +The MIT License (MIT) + +Copyright (c) 2016 David Frank + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +@supabase/postgrest-js +MIT +MIT License + +Copyright (c) 2020 Supabase + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +@supabase/realtime-js +MIT +# MIT License + +Copyright (c) 2020 Supabase + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +@supabase/storage-js +MIT + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +@supabase/supabase-js +MIT +MIT License + +Copyright (c) 2020 Supabase + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +@vercel/ncc +MIT +Copyright 2018 ZEIT, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +tr46 +MIT + +tunnel +MIT +The MIT License (MIT) + +Copyright (c) 2012 Koichi Kobayashi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +undici +MIT +MIT License + +Copyright (c) Matteo Collina and Undici contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +webidl-conversions +BSD-2-Clause +# The BSD 2-Clause License + +Copyright (c) 2014, Domenic Denicola +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +whatwg-url +MIT +The MIT License (MIT) + +Copyright (c) 2015–2016 Sebastian Mayr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +ws +MIT +Copyright (c) 2011 Einar Otto Stangvik +Copyright (c) 2013 Arnout Kazemier and contributors +Copyright (c) 2016 Luigi Pinca and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/.github/actions/push-registry-app-files-supabase/package-lock.json b/.github/actions/push-registry-app-files-supabase/package-lock.json new file mode 100644 index 0000000000000..68c63902c8b2c --- /dev/null +++ b/.github/actions/push-registry-app-files-supabase/package-lock.json @@ -0,0 +1,407 @@ +{ + "name": "push-registry-app-files-supabase", + "version": "1.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "push-registry-app-files-supabase", + "version": "1.0.1", + "license": "ISC", + "dependencies": { + "@actions/core": "^1.9.0", + "@supabase/supabase-js": "^2.48.1", + "@vercel/ncc": "^0.34.0", + "ncc": "^0.3.6" + } + }, + "node_modules/@actions/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "dependencies": { + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==" + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@supabase/auth-js": { + "version": "2.67.3", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.67.3.tgz", + "integrity": "sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz", + "integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.18.1.tgz", + "integrity": "sha512-dWDnoC0MoDHKhaEOrsEKTadWQcBNknZVQcSgNE/Q2wXh05mhCL1ut/jthRUrSbYcqIw/CEjhaeIPp7dLarT0bg==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz", + "integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14", + "@types/phoenix": "^1.5.4", + "@types/ws": "^8.5.10", + "ws": "^8.18.0" + } + }, + "node_modules/@supabase/realtime-js/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", + "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.48.1", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.48.1.tgz", + "integrity": "sha512-VMD+CYk/KxfwGbI4fqwSUVA7CLr1izXpqfFerhnYPSi6LEKD8GoR4kuO5Cc8a+N43LnfSQwLJu4kVm2e4etEmA==", + "dependencies": { + "@supabase/auth-js": "2.67.3", + "@supabase/functions-js": "2.4.4", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.18.1", + "@supabase/realtime-js": "2.11.2", + "@supabase/storage-js": "2.7.1" + } + }, + "node_modules/@types/node": { + "version": "22.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.2.tgz", + "integrity": "sha512-Z+r8y3XL9ZpI2EY52YYygAFmo2/oWfNSj4BCpAXE2McAexDk8VcnBMGC9Djn9gTKt4d2T/hhXqmPzo4hfIXtTg==", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==" + }, + "node_modules/@types/ws": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", + "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vercel/ncc": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.34.0.tgz", + "integrity": "sha512-G9h5ZLBJ/V57Ou9vz5hI8pda/YQX5HQszCs3AmIus3XzsmRn/0Ptic5otD3xVST8QLKk7AMk7AqpsyQGN7MZ9A==", + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/colors": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.3.tgz", + "integrity": "sha512-qTfM2pNFeMZcLvf/RbrVAzDEVttZjFhaApfx9dplNjvHSX88Ui66zBRb/4YGob/xUWxDceirgoC1lT676asfCQ==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "engines": { + "node": "*" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ncc": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/ncc/-/ncc-0.3.6.tgz", + "integrity": "sha512-OXudTB2Ebt/FnOuDoPQbaa17+tdVqSOWA+gLfPxccWwsNED1uA2zEhpoB1hwdFC9yYbio/mdV5cvOtQI3Zrx1w==", + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "^2.6.1", + "tracer": "^0.8.7", + "ws": "^2.3.1" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/safe-buffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", + "integrity": "sha512-cr7dZWLwOeaFBLTIuZeYdkfO7UzGIKhjYENJFAxUOMKWGaWDm2nJM2rzxNRm5Owu0DH3ApwNo6kx5idXZfb/Iw==" + }, + "node_modules/tinytim": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/tinytim/-/tinytim-0.1.1.tgz", + "integrity": "sha512-NIpsp9lBIxPNzB++HnMmUd4byzJSVbbO4F+As1Gb1IG/YQT5QvmBDjpx8SpDS8fhGC+t+Qw8ldQgbcAIaU+2cA==", + "engines": { + "node": ">= 0.2.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tracer": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/tracer/-/tracer-0.8.15.tgz", + "integrity": "sha512-ZQzlhd6zZFIpAhACiZkxLjl65XqVwi8t8UEBVGRIHAQN6nj55ftJWiFell+WSqWCP/vEycrIbUSuiyMwul+TFw==", + "dependencies": { + "colors": "1.2.3", + "dateformat": "3.0.3", + "mkdirp": "^0.5.1", + "tinytim": "0.1.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" + }, + "node_modules/undici": { + "version": "5.28.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz", + "integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-2.3.1.tgz", + "integrity": "sha512-61a+9LgtYZxTq1hAonhX8Xwpo2riK4IOR/BIVxioFbCfc3QFKmpE4x9dLExfLHKtUfVZigYa36tThVhO57erEw==", + "dependencies": { + "safe-buffer": "~5.0.1", + "ultron": "~1.1.0" + } + } + } +} diff --git a/.github/actions/push-registry-app-files-supabase/package.json b/.github/actions/push-registry-app-files-supabase/package.json new file mode 100644 index 0000000000000..6069f301516d3 --- /dev/null +++ b/.github/actions/push-registry-app-files-supabase/package.json @@ -0,0 +1,20 @@ +{ + "name": "push-registry-app-files-supabase", + "version": "1.0.1", + "description": "Push registry app files to Supabase", + "main": "dist/index.js", + "scripts": { + "dev": "pnpm build && node dist/index.js", + "clean": "rm -rf ./dist", + "build": "pnpm run clean && ncc build src/index.js --license licenses.txt" + }, + "keywords": [], + "author": "pipedream", + "license": "ISC", + "dependencies": { + "@actions/core": "^1.9.0", + "@supabase/supabase-js": "^2.48.1", + "@vercel/ncc": "^0.34.0", + "ncc": "^0.3.6" + } +} diff --git a/.github/actions/push-registry-app-files-supabase/src/index.js b/.github/actions/push-registry-app-files-supabase/src/index.js new file mode 100755 index 0000000000000..0c97159f2a56f --- /dev/null +++ b/.github/actions/push-registry-app-files-supabase/src/index.js @@ -0,0 +1,91 @@ +const fs = require("fs") +const core = require("@actions/core"); +const { execSync } = require("child_process"); + +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = core.getInput("supabase_uri"); +const supabaseKey = core.getInput("supabase_anon_key"); +const changedFiles = JSON.parse(core.getInput("changed_files") || "[]"); + +const ignoreDirectories = [ + "/actions/", + "/common/", + "/sources/" +] + +const shouldInclude = (file) => { + if (!(file.endsWith(".app.mjs") || file.endsWith(".app.ts"))) { + return false + } + for (let i = 0; i< ignoreDirectories.length; i++) { + const dirToIgnore = ignoreDirectories[i] + if (file.includes(dirToIgnore)) { + return false + } + } + return true +} + +function createMjsPayload(payload, appMjsFiles) { + for (let i = 0; i < appMjsFiles.length; i++) { + const filePath = appMjsFiles[i] + const app = filePath.split("/").pop().replace(".app.mjs", "") + const content = fs.readFileSync(filePath, { encoding: "utf-8" }) + payload.push({ + app, + app_file: content, + updated_at: new Date().toISOString(), + }); + } +} + +async function createTsPayload(payload, appTsFiles) { + if (appTsFiles.length > 0) { + console.log("Generating mjs files from ts files for: ", appTsFiles) + execSync(`pnpm install -r && pnpm run build`); + } + + for (let i = 0; i < appTsFiles.length; i++) { + const filePath = appTsFiles[i] + const app = filePath.split("/").pop().replace(".app.ts", "") + + console.log(`Building ${app}...`) + + const appDirectory = `components/${app}` + const content = fs.readFileSync(`${appDirectory}/dist/app/${app}.app.mjs`, {encoding: "utf-8"}) + payload.push({ + app, + app_file: content, + updated_at: new Date().toISOString(), + }); + } +} + +async function uploadToSupabase(payload) { + if (payload && payload.length) { + const supabase = createClient(supabaseUrl, supabaseKey) + const { error } = await supabase + .from("registry_app_files") + .upsert(payload, { onConflict: 'app' }) + if (error) { + console.error(`Error bulk uploading files:`, error) + } else { + console.log(`Successfully bulk uploaded files`) + } + } +} + +async function run() { + const filesToUpsert = changedFiles.filter(shouldInclude) + + const appMjsFiles = filesToUpsert.filter(file => file.endsWith('.app.mjs')) + const appTsFiles = filesToUpsert.filter(file => file.endsWith('.app.ts')) + + const payload = []; + createMjsPayload(payload, appMjsFiles) + await createTsPayload(payload, appTsFiles) + await uploadToSupabase(payload) +} + +run().catch(error => core.setFailed(error ?? error?.message)); diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8a1133fcd9271..6d14b7fc8c360 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,15 +1,3 @@ -## WHAT - -copilot:summary - -copilot:poem - - ## WHY - - -## HOW - -copilot:walkthrough diff --git a/.github/workflows/components-pr.yaml b/.github/workflows/components-pr.yaml index 08bebce1e3521..3911e08c7bb80 100644 --- a/.github/workflows/components-pr.yaml +++ b/.github/workflows/components-pr.yaml @@ -18,7 +18,7 @@ jobs: pull-requests: write steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.1.7 name: Checkout repo with: # See https://github.com/actions/checkout#checkout-v2 @@ -30,7 +30,7 @@ jobs: # we have to fetch the entire history. See # https://github.com/actions/checkout/issues/266#issuecomment-638346893 fetch-depth: 0 - - uses: jitterbit/get-changed-files@v1 + - uses: Ana06/get-changed-files@v2.3.0 id: changed_files name: Get changed files with: @@ -48,10 +48,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4.1.1 - - uses: pnpm/action-setup@v3.0.0 + uses: actions/checkout@v4.1.7 + - uses: pnpm/action-setup@v4.0.0 with: - version: 7.33.6 + version: 9.14.2 - name: Get pnpm store directory id: pnpm-cache run: | @@ -64,7 +64,7 @@ jobs: restore-keys: | ${{ runner.os }}-pnpm-store- - name: Setup Node Env - uses: actions/setup-node@v4.0.2 + uses: actions/setup-node@v4.0.3 with: node-version: 18 registry-url: https://registry.npmjs.org/ @@ -73,10 +73,10 @@ jobs: run: pnpm install -r - name: Compile TypeScript id: compile - run: npm run build > files.txt + run: pnpm run build > files.txt - name: Get Changed Files id: files - uses: jitterbit/get-changed-files@v1 + uses: Ana06/get-changed-files@v2.3.0 with: format: 'csv' - name: Check For Compiled TypeScript Files @@ -137,10 +137,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4.1.1 - - uses: pnpm/action-setup@v3.0.0 + uses: actions/checkout@v4.1.7 + - uses: pnpm/action-setup@v4.0.0 with: - version: 7.33.6 + version: 9.14.2 - name: Get pnpm store directory id: pnpm-cache run: | @@ -153,7 +153,7 @@ jobs: restore-keys: | ${{ runner.os }}-pnpm-store- - name: Setup Node Env - uses: actions/setup-node@v4.0.2 + uses: actions/setup-node@v4.0.3 with: node-version: 18 registry-url: https://registry.npmjs.org/ @@ -162,10 +162,10 @@ jobs: run: pnpm install -r - name: Compile TypeScript id: compile - run: npm run build > files.txt + run: pnpm run build > files.txt - name: Get Changed Files id: files - uses: jitterbit/get-changed-files@v1 + uses: Ana06/get-changed-files@v2.3.0 with: format: 'csv' - name: Publish TypeScript components (dry run) diff --git a/.github/workflows/docs-pr.yaml b/.github/workflows/docs-pr.yaml new file mode 100644 index 0000000000000..45cd4c2f065e6 --- /dev/null +++ b/.github/workflows/docs-pr.yaml @@ -0,0 +1,57 @@ +name: Validate MDX Links + +on: + pull_request: + paths: + - 'docs-v2/pages/**/*.mdx' + push: + branches: + - main + paths: + - 'docs-v2/pages/**/*.mdx' + +jobs: + validate-links: + runs-on: ubuntu-latest + defaults: + run: + working-directory: docs-v2 + + steps: + - name: Checkout Code + uses: actions/checkout@v4.1.7 + with: + # Fetch full history for checking diffs + fetch-depth: 0 + + - uses: pnpm/action-setup@v4.0.0 + with: + version: 9.14.2 + + - name: Get pnpm store directory + id: pnpm-cache + run: | + echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Setup Node Env + uses: actions/setup-node@v4.1.0 + with: + node-version: 18 + registry-url: https://registry.npmjs.org/ + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install -r + + - name: Run link validator + run: node validate-mdx-links.mjs + env: + DEBUG: ${{ vars.DEBUG }} \ No newline at end of file diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml deleted file mode 100644 index b69e9b6171497..0000000000000 --- a/.github/workflows/docs.yaml +++ /dev/null @@ -1,37 +0,0 @@ -name: Pull Request Checks - -# -# Documentation: -# https://help.github.com/en/articles/workflow-syntax-for-github-actions -# - -on: - pull_request: - branches: - - master - paths: - - 'docs/**' - -jobs: - check_broken_links: - name: Ensure no broken links end up in docs - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4.1.1 - name: Checkout repo - with: - # See https://github.com/actions/checkout#checkout-v2 - # This will be slow. The intent is to fetch all commits - # since the merge-base (the commit where we branched off) - # so we can check the git diff against all changed files. - # By default, the checkout action only returns the last commit, - # There's no native way to do this in the checkout action, so - # we have to fetch the entire history. See - # https://github.com/actions/checkout/issues/266#issuecomment-638346893 - fetch-depth: 0 - - name: Install docs deps and check broken links - run: |- - cd docs - yarn install - npx vuepress check-md docs/ \ No newline at end of file diff --git a/.github/workflows/pipedream-sdk-markdown-lint.yaml b/.github/workflows/pipedream-sdk-markdown-lint.yaml new file mode 100644 index 0000000000000..8332eee516a6b --- /dev/null +++ b/.github/workflows/pipedream-sdk-markdown-lint.yaml @@ -0,0 +1,24 @@ +name: Lint SDK Markdown Files + +on: + pull_request: + types: [opened, edited, synchronize] + paths: + - 'packages/sdk/**/*.md' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Lint markdown files + uses: DavidAnson/markdownlint-cli2-action@v17 + with: + globs: 'packages/sdk/**/*.md' diff --git a/.github/workflows/pipedream-sdk-test.yaml b/.github/workflows/pipedream-sdk-test.yaml new file mode 100644 index 0000000000000..a9bdf279681fc --- /dev/null +++ b/.github/workflows/pipedream-sdk-test.yaml @@ -0,0 +1,47 @@ +name: Run SDK Tests + +on: + pull_request: + types: [opened, edited, synchronize] + paths: + - 'packages/sdk/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4.0.0 + with: + version: 9.14.2 + - name: Get pnpm store directory + id: pnpm-cache + run: | + echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '22' + + - name: Install dependencies + run: pnpm install + working-directory: packages/sdk + + - name: Run tests + run: pnpm test + working-directory: packages/sdk diff --git a/.github/workflows/publish-components.yaml b/.github/workflows/publish-components.yaml index e47f582007fcd..e62f5b6190be7 100644 --- a/.github/workflows/publish-components.yaml +++ b/.github/workflows/publish-components.yaml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4.1.1 - - uses: pnpm/action-setup@v3.0.0 + uses: actions/checkout@v4.1.7 + - uses: pnpm/action-setup@v4.0.0 with: - version: 7.33.6 + version: 9.14.2 - name: Get pnpm store directory id: pnpm-cache run: | @@ -29,13 +29,13 @@ jobs: - name: Install dependencies run: pnpm install -r --no-frozen-lockfile - name: Setup Node Env - uses: actions/setup-node@v4.0.2 + uses: actions/setup-node@v4.0.3 with: - node-version: 14 + node-version: 18 registry-url: https://registry.npmjs.org/ cache: 'pnpm' - name: Compile TypeScript - run: npm run build + run: pnpm run build - name: Install pd cli env: PD_API_KEY: ${{ secrets.PD_API_KEY }} @@ -48,7 +48,7 @@ jobs: echo "org_id = $PD_ORG_ID" >> $HOME/.config/pipedream/config - name: Get Changed Files id: files - uses: jitterbit/get-changed-files@v1 + uses: Ana06/get-changed-files@v2.3.0 with: format: 'csv' - name: Publish and add to registry components/*.*js (that aren't .app.js files) @@ -64,12 +64,14 @@ jobs: PUBLISHED="" ERRORS="" SKIPPED="" - mapfile -d ',' -t added_modified_renamed_files < <(printf '%s,%s' '${{ steps.files.outputs.added_modified }}' '${{ steps.files.outputs.renamed }}') - for added_modified_file in "${added_modified_renamed_files[@]}"; do - # starts with components, ends with .*js (e.g. .js and .mjs) and not app.js, - # doesn't end with /common*.*js, and doesn't follow */common/ - if [[ $added_modified_file == components/* ]] && [[ $added_modified_file == *.*js ]] && [[ $added_modified_file != *.app.*js ]] \ - && [[ $added_modified_file != */common*.*js ]] && [[ $added_modified_file != */common/* ]] && [[ $added_modified_file != components/*/test-event.mjs ]] + # Ana06/get-changed-files@v2.3.0 treats renamed files that are modified as modified. We shouldn't be renaming files without modification. + mapfile -d ',' -t added_modified_files < <(printf '%s' '${{ steps.files.outputs.added_modified }}') + for added_modified_file in "${added_modified_files[@]}"; + do + # starts with components, ends with .*js (e.g. .js and .mjs) and not app.js, + # doesn't end with /common*.*js, and doesn't follow */common/ + if [[ $added_modified_file == components/* ]] && [[ $added_modified_file == *.*js ]] && [[ $added_modified_file != *.app.*js ]] \ + && [[ $added_modified_file != */common*.*js ]] && [[ $added_modified_file != */common/* ]] && [[ $added_modified_file != components/*/test-event.mjs ]] then echo "retrieving previous version for ${added_modified_file}" KEY=`echo $added_modified_file | tr '/' ' ' | awk '{ print $2 "-" $4 }'` @@ -85,7 +87,7 @@ jobs: curl "https://api.pipedream.com/graphql" -H "Content-Type: application/json" -H "Authorization: Bearer ${PD_API_KEY}" --data-binary $'{"query":"mutation setComponentRegistry($key: String!, $registry: Boolean!, $gitPath: String, $orgId: String){\\n setComponentRegistry(key: $key, registry: $registry, gitPath: $gitPath, orgId: $orgId){\\n savedComponent{\\n id\\n key\\n gitPath\\n }\\n }\\n}","variables":{"key":"'${KEY}'","registry":'true',"gitPath":"'${added_modified_file}'","orgId":"'${PD_ORG_ID}'"}}' [[ "$PREV_VERSION" == "null" ]] && PUBLISHED+="*${added_modified_file}" || PUBLISHED+="*${added_modified_file}~$PREV_VERSION" else - ERROR=`echo $PD_OUTPUT | jq -r ".error"` + ERROR=`echo $PD_OUTPUT | jq -r ".error" || $PD_OUTPUT` ERROR_MESSAGE="${ERROR} with ${added_modified_file}" echo $ERROR_MESSAGE ERRORS+="*${ERROR_MESSAGE}" @@ -117,10 +119,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4.1.1 - - uses: pnpm/action-setup@v3.0.0 + uses: actions/checkout@v4.1.7 + - uses: pnpm/action-setup@v4.0.0 with: - version: 7.33.6 + version: 9.14.2 - name: Get pnpm store directory id: pnpm-cache run: | @@ -133,9 +135,9 @@ jobs: restore-keys: | ${{ runner.os }}-pnpm-store- - name: Setup Node Env - uses: actions/setup-node@v4.0.2 + uses: actions/setup-node@v4.0.3 with: - node-version: 14 + node-version: 18 registry-url: https://registry.npmjs.org/ cache: 'pnpm' - name: Install Dependencies @@ -152,10 +154,10 @@ jobs: echo "org_id = $PD_ORG_ID" >> $HOME/.config/pipedream/config - name: Compile TypeScript id: compile - run: npm run build > files.txt + run: pnpm run build > files.txt - name: Get Changed Files id: files - uses: jitterbit/get-changed-files@v1 + uses: Ana06/get-changed-files@v2.3.0 with: format: 'csv' - name: Publish TypeScript components (dry run) @@ -178,10 +180,11 @@ jobs: PUBLISHED="" ERRORS="" SKIPPED="" - mapfile -d ',' -t added_modified_renamed_files < <(printf '%s,%s' '${{ steps.files.outputs.added_modified }}' '${{ steps.files.outputs.renamed }}') + # Ana06/get-changed-files@v2.3.0 treats renamed files that are modified as modified. We shouldn't be renaming files without modification. + mapfile -d ',' -t added_modified_files < <(printf '%s' '${{ steps.files.outputs.added_modified }}') # included in the components dir, ends with .ts and not app.ts, # doesn't end with /common*.ts, and doesn't follow */common/ - for added_modified_file in "${added_modified_renamed_files[@]}"; + for added_modified_file in "${added_modified_files[@]}"; do echo "Checking if $added_modified_file is a publishable ts file" if [[ $added_modified_file == components/* ]] && [[ $added_modified_file == *.ts ]] && [[ $added_modified_file != *.app.ts ]] \ @@ -231,8 +234,8 @@ jobs: fi fi else - echo "${f} will not be added to the registry" - SKIPPED+="*${f}" + echo "${added_modified_file} will not be added to the registry" + SKIPPED+="*${added_modified_file}" fi done # print out everything that didn't publish diff --git a/.github/workflows/publish-marketplace-content.yaml b/.github/workflows/publish-marketplace-content.yaml index b8a8cc56c6593..1f0f6e7799088 100644 --- a/.github/workflows/publish-marketplace-content.yaml +++ b/.github/workflows/publish-marketplace-content.yaml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4.1.1 - - uses: pnpm/action-setup@v3.0.0 + uses: actions/checkout@v4.1.7 + - uses: pnpm/action-setup@v4.0.0 with: - version: 7.33.6 + version: 9.14.2 - name: Get pnpm store directory id: pnpm-cache run: | @@ -29,14 +29,14 @@ jobs: - name: Install dependencies run: pnpm install -r --no-frozen-lockfile - name: Setup Node Env - uses: actions/setup-node@v4.0.2 + uses: actions/setup-node@v4.0.3 with: - node-version: 14 + node-version: 18 registry-url: https://registry.npmjs.org/ cache: 'pnpm' - name: Get Changed Files id: files - uses: jitterbit/get-changed-files@v1 + uses: Ana06/get-changed-files@v2.3.0 with: format: 'csv' - name: Publish changes to marketplace content @@ -47,4 +47,4 @@ jobs: shell: bash {0} # don't fast fail run: | printf -v added_modified_renamed_files '%s,%s' '${{ steps.files.outputs.added_modified }}' '${{ steps.files.outputs.renamed }}' - pnpm dlx ts-node --esm scripts/updateMarketplaceReadme.mts $added_modified_renamed_files + pnpm dlx tsx scripts/updateMarketplaceReadme.mts $added_modified_renamed_files diff --git a/.github/workflows/publish-packages.yaml b/.github/workflows/publish-packages.yaml index 24e36c3f98543..96588daa9a099 100644 --- a/.github/workflows/publish-packages.yaml +++ b/.github/workflows/publish-packages.yaml @@ -17,11 +17,13 @@ jobs: publish: name: pnpm publish runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} steps: - - uses: actions/checkout@v4.1.1 - - uses: pnpm/action-setup@v3.0.0 + - uses: actions/checkout@v4.1.7 + - uses: pnpm/action-setup@v4.0.0 with: - version: 7.33.6 + version: 9.14.2 - name: Get pnpm store directory if: github.ref != 'refs/heads/master' # Cache is used only for dry runs id: pnpm-cache @@ -35,7 +37,7 @@ jobs: key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - - uses: actions/setup-node@v4.0.2 + - uses: actions/setup-node@v4.0.3 with: node-version: 18 registry-url: https://registry.npmjs.org/ @@ -43,7 +45,7 @@ jobs: - name: pnpm install run: pnpm install -r --no-frozen-lockfile - name: Compile TypeScript - run: npm run build + run: pnpm run build # See https://pnpm.io/using-changesets - name: Setup npmrc for pnpm publish run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc diff --git a/.github/workflows/publish-platform-package.yaml b/.github/workflows/publish-platform-package.yaml new file mode 100644 index 0000000000000..a062cb9a65db7 --- /dev/null +++ b/.github/workflows/publish-platform-package.yaml @@ -0,0 +1,32 @@ +on: + push: + branches: + - platform-1.x + - platform-2.x + +jobs: + # See https://pnpm.io/continuous-integration#github-actions + publish: + name: pnpm publish + runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + steps: + - uses: actions/checkout@v4.1.7 + - uses: pnpm/action-setup@v4.0.0 + with: + version: 9.14.2 + - uses: actions/setup-node@v4.0.3 + with: + node-version: 18 + registry-url: https://registry.npmjs.org/ + cache: 'pnpm' + - name: pnpm install + run: pnpm install -r --no-frozen-lockfile + - name: Compile TypeScript + run: pnpm run build + # See https://pnpm.io/using-changesets + - name: Setup npmrc for pnpm publish + run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc + - name: pnpm publish (platform) + run: pnpm publish platform --no-git-checks diff --git a/.github/workflows/pull-request-checks.yaml b/.github/workflows/pull-request-checks.yaml index 87a5289267399..6a5a4168d12d2 100644 --- a/.github/workflows/pull-request-checks.yaml +++ b/.github/workflows/pull-request-checks.yaml @@ -16,16 +16,16 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.1.7 name: Checkout - - uses: jitterbit/get-changed-files@v1 + - uses: Ana06/get-changed-files@v2.3.0 id: changed_files name: Get changed files - id: md_changed_files name: Spellcheck Markdown files run: |- files='' - for f in ${{ steps.changed_files.outputs.all }} + for f in ${{ steps.changed_files.outputs.added_modified }} do ext="${f##*.}" if [ $ext = "md" ] || [ $ext = "mdx" ] @@ -35,7 +35,7 @@ jobs: done echo "files=${files}" >> $GITHUB_ENV - - uses: rojopolis/spellcheck-github-actions@0.36.0 + - uses: rojopolis/spellcheck-github-actions@0.42.0 name: Spellcheck if: ${{ env.files }} with: @@ -48,14 +48,20 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.7 with: - # Full git history is needed to get a proper list of changed files - # within `super-linter` + # See https://github.com/actions/checkout#checkout-v2 + # This will be slow. The intent is to fetch all commits + # since the merge-base (the commit where we branched off) + # so we can check the git diff against all changed files. + # By default, the checkout action only returns the last commit, + # There's no native way to do this in the checkout action, so + # we have to fetch the entire history. See + # https://github.com/actions/checkout/issues/266#issuecomment-638346893 fetch-depth: 0 - - uses: pnpm/action-setup@v3.0.0 + - uses: pnpm/action-setup@v4.0.0 with: - version: 7.33.6 + version: 9.14.2 - name: Get pnpm store directory id: pnpm-cache run: | @@ -70,26 +76,22 @@ jobs: - name: Install dependencies run: pnpm install -r - name: Setup Node Env - uses: actions/setup-node@v4.0.2 + uses: actions/setup-node@v4.1.0 with: node-version: 18 registry-url: https://registry.npmjs.org/ cache: 'pnpm' - - name: Compile TypeScript - run: npm run build - - name: Lint Code Base - uses: github/super-linter@v5 - env: - DEFAULT_BRANCH: master - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - JAVASCRIPT_ES_CONFIG_FILE: .eslintrc - LINTER_RULES_PATH: / - VALIDATE_ALL_CODEBASE: false - VALIDATE_JAVASCRIPT_ES: true - VALIDATE_JSON: true - - name: Get Changed Files + # ESLint only on changed files + - name: Get Changed Files (space-separated) + id: changed_files_space + uses: Ana06/get-changed-files@v2.3.0 + with: + format: 'space-delimited' + - name: Lint changed files + run: pnpm exec eslint ${{ steps.changed_files_space.outputs.added_modified }} ${{ steps.changed_files_space.outputs.renamed }} + - name: Get Changed Files (comma-separated) id: changed_files - uses: jitterbit/get-changed-files@v1 + uses: Ana06/get-changed-files@v2.3.0 with: format: 'csv' # NOTE: These steps are kept in this workflow to avoid re-rerunning the rest of the lint job diff --git a/.github/workflows/push-registry-app-files-supabase.yaml b/.github/workflows/push-registry-app-files-supabase.yaml new file mode 100644 index 0000000000000..467a3495e8fb1 --- /dev/null +++ b/.github/workflows/push-registry-app-files-supabase.yaml @@ -0,0 +1,55 @@ +name: Push Registry App Files to Supabase + +on: + push: + branches: + - master + paths: + - 'components/**' + +jobs: + push_to_supabase: + name: Push registry app files to Supabase if they have been modified + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + + steps: + - uses: actions/checkout@v4.1.7 + name: Checkout repo + with: + # https://github.com/actions/checkout/issues/266#issuecomment-638346893 + fetch-depth: 0 + - uses: pnpm/action-setup@v4.0.0 + with: + version: 9.14.2 + - name: Get pnpm store directory + id: pnpm-cache + run: | + echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Setup Node Env + uses: actions/setup-node@v4.0.3 + with: + node-version: 18 + registry-url: https://registry.npmjs.org/ + cache: 'pnpm' + - uses: Ana06/get-changed-files@v2.3.0 + id: changed_files + name: Get changed files + with: + format: json + - name: Upload modified or newly added *.app.mjs files to Supabase + uses: ./.github/actions/push-registry-app-files-supabase + with: + changed_files: ${{ steps.changed_files.outputs.all }} + supabase_anon_key: ${{ secrets.SUPABASE_ANON_KEY }} + supabase_uri: ${{ secrets.SUPABASE_URI }} diff --git a/.gitignore b/.gitignore index 0126dadc99ae1..35fb5a98b2393 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ docs/.vuepress/dist ./package-lock.json components/**/package-lock.json +/packages/evals/ +/packages/sdk/examples/.next/ diff --git a/.npmrc b/.npmrc index c77a533eb676d..9dc7c37f87031 100644 --- a/.npmrc +++ b/.npmrc @@ -1,4 +1,8 @@ auto-install-peers=true enable-pre-post-scripts=true save-workspace-protocol=false -link-workspace-packages=false \ No newline at end of file +link-workspace-packages=false +shamefully-hoist=true +node-linker=hoisted +shared-workspace-lockfile=true +engine-strict=false \ No newline at end of file diff --git a/.tool-versions b/.tool-versions index febd83fdf53b0..8c41b43c01f03 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1,4 @@ -nodejs 18.17.0 -pnpm 7.33.6 +nodejs 18.18.0 +pnpm 9.14.2 python 3.11.5 poetry 1.6.1 diff --git a/README.md b/README.md index 748b496cdb9b1..503e48572e622 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Click the image below to watch a brief demo on YouTube.


- + Pipedream demo static image

diff --git a/blog/pi/poetry.lock b/blog/pi/poetry.lock index d032cfb68c73b..292746354805c 100644 --- a/blog/pi/poetry.lock +++ b/blog/pi/poetry.lock @@ -1,91 +1,103 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "aiohappyeyeballs" +version = "2.3.5" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohappyeyeballs-2.3.5-py3-none-any.whl", hash = "sha256:4d6dea59215537dbc746e93e779caea8178c866856a721c9c660d7a5a7b8be03"}, + {file = "aiohappyeyeballs-2.3.5.tar.gz", hash = "sha256:6fa48b9f1317254f122a07a131a86b71ca6946ca989ce6326fff54a99a920105"}, +] [[package]] name = "aiohttp" -version = "3.9.2" +version = "3.10.2" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.8" files = [ - {file = "aiohttp-3.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:772fbe371788e61c58d6d3d904268e48a594ba866804d08c995ad71b144f94cb"}, - {file = "aiohttp-3.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:edd4f1af2253f227ae311ab3d403d0c506c9b4410c7fc8d9573dec6d9740369f"}, - {file = "aiohttp-3.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cfee9287778399fdef6f8a11c9e425e1cb13cc9920fd3a3df8f122500978292b"}, - {file = "aiohttp-3.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc158466f6a980a6095ee55174d1de5730ad7dec251be655d9a6a9dd7ea1ff9"}, - {file = "aiohttp-3.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54ec82f45d57c9a65a1ead3953b51c704f9587440e6682f689da97f3e8defa35"}, - {file = "aiohttp-3.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abeb813a18eb387f0d835ef51f88568540ad0325807a77a6e501fed4610f864e"}, - {file = "aiohttp-3.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc91d07280d7d169f3a0f9179d8babd0ee05c79d4d891447629ff0d7d8089ec2"}, - {file = "aiohttp-3.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b65e861f4bebfb660f7f0f40fa3eb9f2ab9af10647d05dac824390e7af8f75b7"}, - {file = "aiohttp-3.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:04fd8ffd2be73d42bcf55fd78cde7958eeee6d4d8f73c3846b7cba491ecdb570"}, - {file = "aiohttp-3.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3d8d962b439a859b3ded9a1e111a4615357b01620a546bc601f25b0211f2da81"}, - {file = "aiohttp-3.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:8ceb658afd12b27552597cf9a65d9807d58aef45adbb58616cdd5ad4c258c39e"}, - {file = "aiohttp-3.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:0e4ee4df741670560b1bc393672035418bf9063718fee05e1796bf867e995fad"}, - {file = "aiohttp-3.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2dec87a556f300d3211decf018bfd263424f0690fcca00de94a837949fbcea02"}, - {file = "aiohttp-3.9.2-cp310-cp310-win32.whl", hash = "sha256:3e1a800f988ce7c4917f34096f81585a73dbf65b5c39618b37926b1238cf9bc4"}, - {file = "aiohttp-3.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:ea510718a41b95c236c992b89fdfc3d04cc7ca60281f93aaada497c2b4e05c46"}, - {file = "aiohttp-3.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6aaa6f99256dd1b5756a50891a20f0d252bd7bdb0854c5d440edab4495c9f973"}, - {file = "aiohttp-3.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a27d8c70ad87bcfce2e97488652075a9bdd5b70093f50b10ae051dfe5e6baf37"}, - {file = "aiohttp-3.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:54287bcb74d21715ac8382e9de146d9442b5f133d9babb7e5d9e453faadd005e"}, - {file = "aiohttp-3.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb3d05569aa83011fcb346b5266e00b04180105fcacc63743fc2e4a1862a891"}, - {file = "aiohttp-3.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8534e7d69bb8e8d134fe2be9890d1b863518582f30c9874ed7ed12e48abe3c4"}, - {file = "aiohttp-3.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bd9d5b989d57b41e4ff56ab250c5ddf259f32db17159cce630fd543376bd96b"}, - {file = "aiohttp-3.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa6904088e6642609981f919ba775838ebf7df7fe64998b1a954fb411ffb4663"}, - {file = "aiohttp-3.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bda42eb410be91b349fb4ee3a23a30ee301c391e503996a638d05659d76ea4c2"}, - {file = "aiohttp-3.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:193cc1ccd69d819562cc7f345c815a6fc51d223b2ef22f23c1a0f67a88de9a72"}, - {file = "aiohttp-3.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b9f1cb839b621f84a5b006848e336cf1496688059d2408e617af33e3470ba204"}, - {file = "aiohttp-3.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:d22a0931848b8c7a023c695fa2057c6aaac19085f257d48baa24455e67df97ec"}, - {file = "aiohttp-3.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4112d8ba61fbd0abd5d43a9cb312214565b446d926e282a6d7da3f5a5aa71d36"}, - {file = "aiohttp-3.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c4ad4241b52bb2eb7a4d2bde060d31c2b255b8c6597dd8deac2f039168d14fd7"}, - {file = "aiohttp-3.9.2-cp311-cp311-win32.whl", hash = "sha256:ee2661a3f5b529f4fc8a8ffee9f736ae054adfb353a0d2f78218be90617194b3"}, - {file = "aiohttp-3.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:4deae2c165a5db1ed97df2868ef31ca3cc999988812e82386d22937d9d6fed52"}, - {file = "aiohttp-3.9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6f4cdba12539215aaecf3c310ce9d067b0081a0795dd8a8805fdb67a65c0572a"}, - {file = "aiohttp-3.9.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:84e843b33d5460a5c501c05539809ff3aee07436296ff9fbc4d327e32aa3a326"}, - {file = "aiohttp-3.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8008d0f451d66140a5aa1c17e3eedc9d56e14207568cd42072c9d6b92bf19b52"}, - {file = "aiohttp-3.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61c47ab8ef629793c086378b1df93d18438612d3ed60dca76c3422f4fbafa792"}, - {file = "aiohttp-3.9.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc71f748e12284312f140eaa6599a520389273174b42c345d13c7e07792f4f57"}, - {file = "aiohttp-3.9.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a1c3a4d0ab2f75f22ec80bca62385db2e8810ee12efa8c9e92efea45c1849133"}, - {file = "aiohttp-3.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a87aa0b13bbee025faa59fa58861303c2b064b9855d4c0e45ec70182bbeba1b"}, - {file = "aiohttp-3.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2cc0d04688b9f4a7854c56c18aa7af9e5b0a87a28f934e2e596ba7e14783192"}, - {file = "aiohttp-3.9.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1956e3ac376b1711c1533266dec4efd485f821d84c13ce1217d53e42c9e65f08"}, - {file = "aiohttp-3.9.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:114da29f39eccd71b93a0fcacff178749a5c3559009b4a4498c2c173a6d74dff"}, - {file = "aiohttp-3.9.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:3f17999ae3927d8a9a823a1283b201344a0627272f92d4f3e3a4efe276972fe8"}, - {file = "aiohttp-3.9.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:f31df6a32217a34ae2f813b152a6f348154f948c83213b690e59d9e84020925c"}, - {file = "aiohttp-3.9.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7a75307ffe31329928a8d47eae0692192327c599113d41b278d4c12b54e1bd11"}, - {file = "aiohttp-3.9.2-cp312-cp312-win32.whl", hash = "sha256:972b63d589ff8f305463593050a31b5ce91638918da38139b9d8deaba9e0fed7"}, - {file = "aiohttp-3.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:200dc0246f0cb5405c80d18ac905c8350179c063ea1587580e3335bfc243ba6a"}, - {file = "aiohttp-3.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:158564d0d1020e0d3fe919a81d97aadad35171e13e7b425b244ad4337fc6793a"}, - {file = "aiohttp-3.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:da1346cd0ccb395f0ed16b113ebb626fa43b7b07fd7344fce33e7a4f04a8897a"}, - {file = "aiohttp-3.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:eaa9256de26ea0334ffa25f1913ae15a51e35c529a1ed9af8e6286dd44312554"}, - {file = "aiohttp-3.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1543e7fb00214fb4ccead42e6a7d86f3bb7c34751ec7c605cca7388e525fd0b4"}, - {file = "aiohttp-3.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:186e94570433a004e05f31f632726ae0f2c9dee4762a9ce915769ce9c0a23d89"}, - {file = "aiohttp-3.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d52d20832ac1560f4510d68e7ba8befbc801a2b77df12bd0cd2bcf3b049e52a4"}, - {file = "aiohttp-3.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c45e4e815ac6af3b72ca2bde9b608d2571737bb1e2d42299fc1ffdf60f6f9a1"}, - {file = "aiohttp-3.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa906b9bdfd4a7972dd0628dbbd6413d2062df5b431194486a78f0d2ae87bd55"}, - {file = "aiohttp-3.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:68bbee9e17d66f17bb0010aa15a22c6eb28583edcc8b3212e2b8e3f77f3ebe2a"}, - {file = "aiohttp-3.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4c189b64bd6d9a403a1a3f86a3ab3acbc3dc41a68f73a268a4f683f89a4dec1f"}, - {file = "aiohttp-3.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:8a7876f794523123bca6d44bfecd89c9fec9ec897a25f3dd202ee7fc5c6525b7"}, - {file = "aiohttp-3.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:d23fba734e3dd7b1d679b9473129cd52e4ec0e65a4512b488981a56420e708db"}, - {file = "aiohttp-3.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b141753be581fab842a25cb319f79536d19c2a51995d7d8b29ee290169868eab"}, - {file = "aiohttp-3.9.2-cp38-cp38-win32.whl", hash = "sha256:103daf41ff3b53ba6fa09ad410793e2e76c9d0269151812e5aba4b9dd674a7e8"}, - {file = "aiohttp-3.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:328918a6c2835861ff7afa8c6d2c70c35fdaf996205d5932351bdd952f33fa2f"}, - {file = "aiohttp-3.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5264d7327c9464786f74e4ec9342afbbb6ee70dfbb2ec9e3dfce7a54c8043aa3"}, - {file = "aiohttp-3.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07205ae0015e05c78b3288c1517afa000823a678a41594b3fdc870878d645305"}, - {file = "aiohttp-3.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ae0a1e638cffc3ec4d4784b8b4fd1cf28968febc4bd2718ffa25b99b96a741bd"}, - {file = "aiohttp-3.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d43302a30ba1166325974858e6ef31727a23bdd12db40e725bec0f759abce505"}, - {file = "aiohttp-3.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16a967685907003765855999af11a79b24e70b34dc710f77a38d21cd9fc4f5fe"}, - {file = "aiohttp-3.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fa3ee92cd441d5c2d07ca88d7a9cef50f7ec975f0117cd0c62018022a184308"}, - {file = "aiohttp-3.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b500c5ad9c07639d48615a770f49618130e61be36608fc9bc2d9bae31732b8f"}, - {file = "aiohttp-3.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c07327b368745b1ce2393ae9e1aafed7073d9199e1dcba14e035cc646c7941bf"}, - {file = "aiohttp-3.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc7d6502c23a0ec109687bf31909b3fb7b196faf198f8cff68c81b49eb316ea9"}, - {file = "aiohttp-3.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:07be2be7071723c3509ab5c08108d3a74f2181d4964e869f2504aaab68f8d3e8"}, - {file = "aiohttp-3.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:122468f6fee5fcbe67cb07014a08c195b3d4c41ff71e7b5160a7bcc41d585a5f"}, - {file = "aiohttp-3.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:00a9abcea793c81e7f8778ca195a1714a64f6d7436c4c0bb168ad2a212627000"}, - {file = "aiohttp-3.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7a9825fdd64ecac5c670234d80bb52bdcaa4139d1f839165f548208b3779c6c6"}, - {file = "aiohttp-3.9.2-cp39-cp39-win32.whl", hash = "sha256:5422cd9a4a00f24c7244e1b15aa9b87935c85fb6a00c8ac9b2527b38627a9211"}, - {file = "aiohttp-3.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:7d579dcd5d82a86a46f725458418458fa43686f6a7b252f2966d359033ffc8ab"}, - {file = "aiohttp-3.9.2.tar.gz", hash = "sha256:b0ad0a5e86ce73f5368a164c10ada10504bf91869c05ab75d982c6048217fbf7"}, + {file = "aiohttp-3.10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:95213b3d79c7e387144e9cb7b9d2809092d6ff2c044cb59033aedc612f38fb6d"}, + {file = "aiohttp-3.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1aa005f060aff7124cfadaa2493f00a4e28ed41b232add5869e129a2e395935a"}, + {file = "aiohttp-3.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eabe6bf4c199687592f5de4ccd383945f485779c7ffb62a9b9f1f8a3f9756df8"}, + {file = "aiohttp-3.10.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e010736fc16d21125c7e2dc5c350cd43c528b85085c04bf73a77be328fe944"}, + {file = "aiohttp-3.10.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99f81f9c1529fd8e03be4a7bd7df32d14b4f856e90ef6e9cbad3415dbfa9166c"}, + {file = "aiohttp-3.10.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d611d1a01c25277bcdea06879afbc11472e33ce842322496b211319aa95441bb"}, + {file = "aiohttp-3.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00191d38156e09e8c81ef3d75c0d70d4f209b8381e71622165f22ef7da6f101"}, + {file = "aiohttp-3.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74c091a5ded6cb81785de2d7a8ab703731f26de910dbe0f3934eabef4ae417cc"}, + {file = "aiohttp-3.10.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:18186a80ec5a701816adbf1d779926e1069392cf18504528d6e52e14b5920525"}, + {file = "aiohttp-3.10.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5a7ceb2a0d2280f23a02c64cd0afdc922079bb950400c3dd13a1ab2988428aac"}, + {file = "aiohttp-3.10.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8bd7be6ff6c162a60cb8fce65ee879a684fbb63d5466aba3fa5b9288eb04aefa"}, + {file = "aiohttp-3.10.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fae962b62944eaebff4f4fddcf1a69de919e7b967136a318533d82d93c3c6bd1"}, + {file = "aiohttp-3.10.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a0fde16d284efcacbe15fb0c1013f0967b6c3e379649239d783868230bf1db42"}, + {file = "aiohttp-3.10.2-cp310-cp310-win32.whl", hash = "sha256:f81cd85a0e76ec7b8e2b6636fe02952d35befda4196b8c88f3cec5b4fb512839"}, + {file = "aiohttp-3.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:54ba10eb5a3481c28282eb6afb5f709aedf53cf9c3a31875ffbdc9fc719ffd67"}, + {file = "aiohttp-3.10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:87fab7f948e407444c2f57088286e00e2ed0003ceaf3d8f8cc0f60544ba61d91"}, + {file = "aiohttp-3.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec6ad66ed660d46503243cbec7b2b3d8ddfa020f984209b3b8ef7d98ce69c3f2"}, + {file = "aiohttp-3.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a4be88807283bd96ae7b8e401abde4ca0bab597ba73b5e9a2d98f36d451e9aac"}, + {file = "aiohttp-3.10.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01c98041f90927c2cbd72c22a164bb816fa3010a047d264969cf82e1d4bcf8d1"}, + {file = "aiohttp-3.10.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54e36c67e1a9273ecafab18d6693da0fb5ac48fd48417e4548ac24a918c20998"}, + {file = "aiohttp-3.10.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7de3ddb6f424af54535424082a1b5d1ae8caf8256ebd445be68c31c662354720"}, + {file = "aiohttp-3.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dd9c7db94b4692b827ce51dcee597d61a0e4f4661162424faf65106775b40e7"}, + {file = "aiohttp-3.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e57e21e1167705f8482ca29cc5d02702208d8bf4aff58f766d94bcd6ead838cd"}, + {file = "aiohttp-3.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a1a50e59b720060c29e2951fd9f13c01e1ea9492e5a527b92cfe04dd64453c16"}, + {file = "aiohttp-3.10.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:686c87782481fda5ee6ba572d912a5c26d9f98cc5c243ebd03f95222af3f1b0f"}, + {file = "aiohttp-3.10.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:dafb4abb257c0ed56dc36f4e928a7341b34b1379bd87e5a15ce5d883c2c90574"}, + {file = "aiohttp-3.10.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:494a6f77560e02bd7d1ab579fdf8192390567fc96a603f21370f6e63690b7f3d"}, + {file = "aiohttp-3.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6fe8503b1b917508cc68bf44dae28823ac05e9f091021e0c41f806ebbb23f92f"}, + {file = "aiohttp-3.10.2-cp311-cp311-win32.whl", hash = "sha256:4ddb43d06ce786221c0dfd3c91b4892c318eaa36b903f7c4278e7e2fa0dd5102"}, + {file = "aiohttp-3.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:ca2f5abcb0a9a47e56bac173c01e9f6c6e7f27534d91451c5f22e6a35a5a2093"}, + {file = "aiohttp-3.10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:14eb6b17f6246959fb0b035d4f4ae52caa870c4edfb6170aad14c0de5bfbf478"}, + {file = "aiohttp-3.10.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:465e445ec348d4e4bd349edd8b22db75f025da9d7b6dc1369c48e7935b85581e"}, + {file = "aiohttp-3.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:341f8ece0276a828d95b70cd265d20e257f5132b46bf77d759d7f4e0443f2906"}, + {file = "aiohttp-3.10.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01fbb87b5426381cd9418b3ddcf4fc107e296fa2d3446c18ce6c76642f340a3"}, + {file = "aiohttp-3.10.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c474af073e1a6763e1c5522bbb2d85ff8318197e4c6c919b8d7886e16213345"}, + {file = "aiohttp-3.10.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d9076810a5621236e29b2204e67a68e1fe317c8727ee4c9abbfbb1083b442c38"}, + {file = "aiohttp-3.10.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8f515d6859e673940e08de3922b9c4a2249653b0ac181169313bd6e4b1978ac"}, + {file = "aiohttp-3.10.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:655e583afc639bef06f3b2446972c1726007a21003cd0ef57116a123e44601bc"}, + {file = "aiohttp-3.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8da9449a575133828cc99985536552ea2dcd690e848f9d41b48d8853a149a959"}, + {file = "aiohttp-3.10.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:19073d57d0feb1865d12361e2a1f5a49cb764bf81a4024a3b608ab521568093a"}, + {file = "aiohttp-3.10.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8e98e1845805f184d91fda6f9ab93d7c7b0dddf1c07e0255924bfdb151a8d05"}, + {file = "aiohttp-3.10.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:377220a5efde6f9497c5b74649b8c261d3cce8a84cb661be2ed8099a2196400a"}, + {file = "aiohttp-3.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92f7f4a4dc9cdb5980973a74d43cdbb16286dacf8d1896b6c3023b8ba8436f8e"}, + {file = "aiohttp-3.10.2-cp312-cp312-win32.whl", hash = "sha256:9bb2834a6f11d65374ce97d366d6311a9155ef92c4f0cee543b2155d06dc921f"}, + {file = "aiohttp-3.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:518dc3cb37365255708283d1c1c54485bbacccd84f0a0fb87ed8917ba45eda5b"}, + {file = "aiohttp-3.10.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7f98e70bbbf693086efe4b86d381efad8edac040b8ad02821453083d15ec315f"}, + {file = "aiohttp-3.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9f6f0b252a009e98fe84028a4ec48396a948e7a65b8be06ccfc6ef68cf1f614d"}, + {file = "aiohttp-3.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9360e3ffc7b23565600e729e8c639c3c50d5520e05fdf94aa2bd859eef12c407"}, + {file = "aiohttp-3.10.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3988044d1635c7821dd44f0edfbe47e9875427464e59d548aece447f8c22800a"}, + {file = "aiohttp-3.10.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30a9d59da1543a6f1478c3436fd49ec59be3868bca561a33778b4391005e499d"}, + {file = "aiohttp-3.10.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9f49bdb94809ac56e09a310a62f33e5f22973d6fd351aac72a39cd551e98194"}, + {file = "aiohttp-3.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddfd2dca3f11c365d6857a07e7d12985afc59798458a2fdb2ffa4a0332a3fd43"}, + {file = "aiohttp-3.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:685c1508ec97b2cd3e120bfe309a4ff8e852e8a7460f1ef1de00c2c0ed01e33c"}, + {file = "aiohttp-3.10.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:49904f38667c44c041a0b44c474b3ae36948d16a0398a8f8cd84e2bb3c42a069"}, + {file = "aiohttp-3.10.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:352f3a4e5f11f3241a49b6a48bc5b935fabc35d1165fa0d87f3ca99c1fcca98b"}, + {file = "aiohttp-3.10.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:fc61f39b534c5d5903490478a0dd349df397d2284a939aa3cbaa2fb7a19b8397"}, + {file = "aiohttp-3.10.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:ad2274e707be37420d0b6c3d26a8115295fe9d8e6e530fa6a42487a8ca3ad052"}, + {file = "aiohttp-3.10.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c836bf3c7512100219fe1123743fd8dd9a2b50dd7cfb0c3bb10d041309acab4b"}, + {file = "aiohttp-3.10.2-cp38-cp38-win32.whl", hash = "sha256:53e8898adda402be03ff164b0878abe2d884e3ea03a4701e6ad55399d84b92dc"}, + {file = "aiohttp-3.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:7cc8f65f5b22304693de05a245b6736b14cb5bc9c8a03da6e2ae9ef15f8b458f"}, + {file = "aiohttp-3.10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9dfc906d656e14004c5bc672399c1cccc10db38df2b62a13fb2b6e165a81c316"}, + {file = "aiohttp-3.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:91b10208b222ddf655c3a3d5b727879d7163db12b634492df41a9182a76edaae"}, + {file = "aiohttp-3.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9fd16b5e1a7bdd14668cd6bde60a2a29b49147a535c74f50d8177d11b38433a7"}, + {file = "aiohttp-3.10.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2bfdda4971bd79201f59adbad24ec2728875237e1c83bba5221284dbbf57bda"}, + {file = "aiohttp-3.10.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69d73f869cf29e8a373127fc378014e2b17bcfbe8d89134bc6fb06a2f67f3cb3"}, + {file = "aiohttp-3.10.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df59f8486507c421c0620a2c3dce81fbf1d54018dc20ff4fecdb2c106d6e6abc"}, + {file = "aiohttp-3.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df930015db36b460aa9badbf35eccbc383f00d52d4b6f3de2ccb57d064a6ade"}, + {file = "aiohttp-3.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:562b1153ab7f766ee6b8b357ec777a302770ad017cf18505d34f1c088fccc448"}, + {file = "aiohttp-3.10.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d984db6d855de58e0fde1ef908d48fe9a634cadb3cf715962722b4da1c40619d"}, + {file = "aiohttp-3.10.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:14dc3fcb0d877911d775d511eb617a486a8c48afca0a887276e63db04d3ee920"}, + {file = "aiohttp-3.10.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b52a27a5c97275e254704e1049f4b96a81e67d6205f52fa37a4777d55b0e98ef"}, + {file = "aiohttp-3.10.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:cd33d9de8cfd006a0d0fe85f49b4183c57e91d18ffb7e9004ce855e81928f704"}, + {file = "aiohttp-3.10.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1238fc979160bc03a92fff9ad021375ff1c8799c6aacb0d8ea1b357ea40932bb"}, + {file = "aiohttp-3.10.2-cp39-cp39-win32.whl", hash = "sha256:e2f43d238eae4f0b04f58d4c0df4615697d4ca3e9f9b1963d49555a94f0f5a04"}, + {file = "aiohttp-3.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:947847f07a8f81d7b39b2d0202fd73e61962ebe17ac2d8566f260679e467da7b"}, + {file = "aiohttp-3.10.2.tar.gz", hash = "sha256:4d1f694b5d6e459352e5e925a42e05bac66655bfde44d81c59992463d2897014"}, ] [package.dependencies] +aiohappyeyeballs = ">=2.3.0" aiosignal = ">=1.1.2" async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} attrs = ">=17.3.0" @@ -94,7 +106,7 @@ multidict = ">=4.5,<7.0" yarl = ">=1.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns", "brotlicffi"] +speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] [[package]] name = "aiosignal" @@ -376,13 +388,13 @@ beautifulsoup4 = "*" [[package]] name = "certifi" -version = "2023.7.22" +version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [[package]] @@ -1280,13 +1292,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "idna" -version = "3.4" +version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] @@ -1428,13 +1440,13 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] [[package]] name = "jinja2" -version = "3.1.3" +version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, - {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] [package.dependencies] @@ -1993,17 +2005,16 @@ extended-testing = ["aiosqlite (>=0.19.0,<0.20.0)", "aleph-alpha-client (>=2.15. [[package]] name = "langchain-core" -version = "0.1.29" +version = "0.1.35" description = "Building applications with LLMs through composability" optional = false -python-versions = ">=3.8.1,<4.0" +python-versions = "<4.0,>=3.8.1" files = [ - {file = "langchain_core-0.1.29-py3-none-any.whl", hash = "sha256:b96d599ff98810a7fcba726c151d473a4b938e0f90b9907c460b0bf0a1c7a0f7"}, - {file = "langchain_core-0.1.29.tar.gz", hash = "sha256:6731dabffad03b9213ada2640d54ed7f4ef6b99fce87ade3c71474ae154dd3cc"}, + {file = "langchain_core-0.1.35-py3-none-any.whl", hash = "sha256:9d790446ea211f4cb620886081cc5a5723bc9a2dc90af1f6205aded2ee61bb71"}, + {file = "langchain_core-0.1.35.tar.gz", hash = "sha256:862b8415d4deaf4e06833ef826bcef3614d75c3e7fd82b09b1349cc223f02e9a"}, ] [package.dependencies] -anyio = ">=3,<5" jsonpatch = ">=1.33,<2.0" langsmith = ">=0.1.0,<0.2.0" packaging = ">=23.2,<24.0" @@ -2691,6 +2702,148 @@ files = [ {file = "numpy-1.25.2.tar.gz", hash = "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760"}, ] +[[package]] +name = "nvidia-cublas-cu12" +version = "12.1.3.1" +description = "CUBLAS native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cublas_cu12-12.1.3.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ee53ccca76a6fc08fb9701aa95b6ceb242cdaab118c3bb152af4e579af792728"}, + {file = "nvidia_cublas_cu12-12.1.3.1-py3-none-win_amd64.whl", hash = "sha256:2b964d60e8cf11b5e1073d179d85fa340c120e99b3067558f3cf98dd69d02906"}, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.1.105" +description = "CUDA profiling tools runtime libs." +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cuda_cupti_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:e54fde3983165c624cb79254ae9818a456eb6e87a7fd4d56a2352c24ee542d7e"}, + {file = "nvidia_cuda_cupti_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:bea8236d13a0ac7190bd2919c3e8e6ce1e402104276e6f9694479e48bb0eb2a4"}, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.1.105" +description = "NVRTC native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:339b385f50c309763ca65456ec75e17bbefcbbf2893f462cb8b90584cd27a1c2"}, + {file = "nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:0a98a522d9ff138b96c010a65e145dc1b4850e9ecb75a0172371793752fd46ed"}, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.1.105" +description = "CUDA Runtime native Libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cuda_runtime_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:6e258468ddf5796e25f1dc591a31029fa317d97a0a94ed93468fc86301d61e40"}, + {file = "nvidia_cuda_runtime_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:dfb46ef84d73fababab44cf03e3b83f80700d27ca300e537f85f636fac474344"}, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "8.9.2.26" +description = "cuDNN runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cudnn_cu12-8.9.2.26-py3-none-manylinux1_x86_64.whl", hash = "sha256:5ccb288774fdfb07a7e7025ffec286971c06d8d7b4fb162525334616d7629ff9"}, +] + +[package.dependencies] +nvidia-cublas-cu12 = "*" + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.0.2.54" +description = "CUFFT native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cufft_cu12-11.0.2.54-py3-none-manylinux1_x86_64.whl", hash = "sha256:794e3948a1aa71fd817c3775866943936774d1c14e7628c74f6f7417224cdf56"}, + {file = "nvidia_cufft_cu12-11.0.2.54-py3-none-win_amd64.whl", hash = "sha256:d9ac353f78ff89951da4af698f80870b1534ed69993f10a4cf1d96f21357e253"}, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.2.106" +description = "CURAND native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_curand_cu12-10.3.2.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:9d264c5036dde4e64f1de8c50ae753237c12e0b1348738169cd0f8a536c0e1e0"}, + {file = "nvidia_curand_cu12-10.3.2.106-py3-none-win_amd64.whl", hash = "sha256:75b6b0c574c0037839121317e17fd01f8a69fd2ef8e25853d826fec30bdba74a"}, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.4.5.107" +description = "CUDA solver native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd"}, + {file = "nvidia_cusolver_cu12-11.4.5.107-py3-none-win_amd64.whl", hash = "sha256:74e0c3a24c78612192a74fcd90dd117f1cf21dea4822e66d89e8ea80e3cd2da5"}, +] + +[package.dependencies] +nvidia-cublas-cu12 = "*" +nvidia-cusparse-cu12 = "*" +nvidia-nvjitlink-cu12 = "*" + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.1.0.106" +description = "CUSPARSE native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c"}, + {file = "nvidia_cusparse_cu12-12.1.0.106-py3-none-win_amd64.whl", hash = "sha256:b798237e81b9719373e8fae8d4f091b70a0cf09d9d85c95a557e11df2d8e9a5a"}, +] + +[package.dependencies] +nvidia-nvjitlink-cu12 = "*" + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.19.3" +description = "NVIDIA Collective Communication Library (NCCL) Runtime" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_nccl_cu12-2.19.3-py3-none-manylinux1_x86_64.whl", hash = "sha256:a9734707a2c96443331c1e48c717024aa6678a0e2a4cb66b2c364d18cee6b48d"}, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.5.82" +description = "Nvidia JIT LTO Library" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_nvjitlink_cu12-12.5.82-py3-none-manylinux2014_aarch64.whl", hash = "sha256:98103729cc5226e13ca319a10bbf9433bbbd44ef64fe72f45f067cacc14b8d27"}, + {file = "nvidia_nvjitlink_cu12-12.5.82-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f9b37bc5c8cf7509665cb6ada5aaa0ce65618f2332b7d3e78e9790511f111212"}, + {file = "nvidia_nvjitlink_cu12-12.5.82-py3-none-win_amd64.whl", hash = "sha256:e782564d705ff0bf61ac3e1bf730166da66dd2fe9012f111ede5fc49b64ae697"}, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.1.105" +description = "NVIDIA Tools Extension" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_nvtx_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:dc21cf308ca5691e7c04d962e213f8a4aa9bbfa23d95412f452254c2caeb09e5"}, + {file = "nvidia_nvtx_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:65f4d98982b31b60026e0e6de73fbdfc09d08a96f4656dd3665ca616a11e1e82"}, +] + [[package]] name = "openai" version = "0.27.10" @@ -3056,79 +3209,80 @@ files = [ [[package]] name = "pillow" -version = "10.2.0" +version = "10.3.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.8" files = [ - {file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"}, - {file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"}, - {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"}, - {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"}, - {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"}, - {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"}, - {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"}, - {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"}, - {file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"}, - {file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"}, - {file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"}, - {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"}, - {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"}, - {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"}, - {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"}, - {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"}, - {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"}, - {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"}, - {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"}, - {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"}, - {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"}, - {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"}, - {file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"}, - {file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"}, - {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"}, - {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"}, - {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"}, - {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"}, - {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"}, - {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"}, - {file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"}, - {file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"}, - {file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"}, - {file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"}, - {file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"}, - {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"}, - {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"}, - {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"}, - {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"}, - {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"}, - {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"}, - {file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"}, - {file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"}, - {file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"}, - {file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"}, - {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"}, - {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"}, - {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"}, - {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"}, - {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"}, - {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"}, - {file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"}, - {file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"}, - {file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"}, - {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"}, - {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"}, + {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, + {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, + {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, + {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, + {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, + {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, + {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, + {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, + {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, + {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, + {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, + {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, + {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, + {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, + {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, + {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, + {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, + {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, + {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, + {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, + {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, + {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, + {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, + {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, + {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, + {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, + {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, + {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, + {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, + {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, + {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, + {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, + {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, + {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, + {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, ] [package.extras] @@ -3986,13 +4140,13 @@ files = [ [[package]] name = "requests" -version = "2.31.0" +version = "2.32.0" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.32.0-py3-none-any.whl", hash = "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5"}, + {file = "requests-2.32.0.tar.gz", hash = "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8"}, ] [package.dependencies] @@ -4217,50 +4371,48 @@ files = [ [[package]] name = "scikit-learn" -version = "1.3.1" +version = "1.5.0" description = "A set of python modules for machine learning and data mining" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "scikit-learn-1.3.1.tar.gz", hash = "sha256:1a231cced3ee3fa04756b4a7ab532dc9417acd581a330adff5f2c01ac2831fcf"}, - {file = "scikit_learn-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3153612ff8d36fa4e35ef8b897167119213698ea78f3fd130b4068e6f8d2da5a"}, - {file = "scikit_learn-1.3.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:6bb9490fdb8e7e00f1354621689187bef3cab289c9b869688f805bf724434755"}, - {file = "scikit_learn-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7135a03af71138669f19bc96e7d0cc8081aed4b3565cc3b131135d65fc642ba"}, - {file = "scikit_learn-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d8dee8c1f40eeba49a85fe378bdf70a07bb64aba1a08fda1e0f48d27edfc3e6"}, - {file = "scikit_learn-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:4d379f2b34096105a96bd857b88601dffe7389bd55750f6f29aaa37bc6272eb5"}, - {file = "scikit_learn-1.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14e8775eba072ab10866a7e0596bc9906873e22c4c370a651223372eb62de180"}, - {file = "scikit_learn-1.3.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:58b0c2490eff8355dc26e884487bf8edaccf2ba48d09b194fb2f3a026dd64f9d"}, - {file = "scikit_learn-1.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f66eddfda9d45dd6cadcd706b65669ce1df84b8549875691b1f403730bdef217"}, - {file = "scikit_learn-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6448c37741145b241eeac617028ba6ec2119e1339b1385c9720dae31367f2be"}, - {file = "scikit_learn-1.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c413c2c850241998168bbb3bd1bb59ff03b1195a53864f0b80ab092071af6028"}, - {file = "scikit_learn-1.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ef540e09873e31569bc8b02c8a9f745ee04d8e1263255a15c9969f6f5caa627f"}, - {file = "scikit_learn-1.3.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:9147a3a4df4d401e618713880be023e36109c85d8569b3bf5377e6cd3fecdeac"}, - {file = "scikit_learn-1.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2cd3634695ad192bf71645702b3df498bd1e246fc2d529effdb45a06ab028b4"}, - {file = "scikit_learn-1.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c275a06c5190c5ce00af0acbb61c06374087949f643ef32d355ece12c4db043"}, - {file = "scikit_learn-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:0e1aa8f206d0de814b81b41d60c1ce31f7f2c7354597af38fae46d9c47c45122"}, - {file = "scikit_learn-1.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:52b77cc08bd555969ec5150788ed50276f5ef83abb72e6f469c5b91a0009bbca"}, - {file = "scikit_learn-1.3.1-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a683394bc3f80b7c312c27f9b14ebea7766b1f0a34faf1a2e9158d80e860ec26"}, - {file = "scikit_learn-1.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15d964d9eb181c79c190d3dbc2fff7338786bf017e9039571418a1d53dab236"}, - {file = "scikit_learn-1.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ce9233cdf0cdcf0858a5849d306490bf6de71fa7603a3835124e386e62f2311"}, - {file = "scikit_learn-1.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:1ec668ce003a5b3d12d020d2cde0abd64b262ac5f098b5c84cf9657deb9996a8"}, - {file = "scikit_learn-1.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ccbbedae99325628c1d1cbe3916b7ef58a1ce949672d8d39c8b190e10219fd32"}, - {file = "scikit_learn-1.3.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:845f81c7ceb4ea6bac64ab1c9f2ce8bef0a84d0f21f3bece2126adcc213dfecd"}, - {file = "scikit_learn-1.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8454d57a22d856f1fbf3091bd86f9ebd4bff89088819886dc0c72f47a6c30652"}, - {file = "scikit_learn-1.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d993fb70a1d78c9798b8f2f28705bfbfcd546b661f9e2e67aa85f81052b9c53"}, - {file = "scikit_learn-1.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:66f7bb1fec37d65f4ef85953e1df5d3c98a0f0141d394dcdaead5a6de9170347"}, + {file = "scikit_learn-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12e40ac48555e6b551f0a0a5743cc94cc5a765c9513fe708e01f0aa001da2801"}, + {file = "scikit_learn-1.5.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:f405c4dae288f5f6553b10c4ac9ea7754d5180ec11e296464adb5d6ac68b6ef5"}, + {file = "scikit_learn-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df8ccabbf583315f13160a4bb06037bde99ea7d8211a69787a6b7c5d4ebb6fc3"}, + {file = "scikit_learn-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c75ea812cd83b1385bbfa94ae971f0d80adb338a9523f6bbcb5e0b0381151d4"}, + {file = "scikit_learn-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:a90c5da84829a0b9b4bf00daf62754b2be741e66b5946911f5bdfaa869fcedd6"}, + {file = "scikit_learn-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a65af2d8a6cce4e163a7951a4cfbfa7fceb2d5c013a4b593686c7f16445cf9d"}, + {file = "scikit_learn-1.5.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:4c0c56c3005f2ec1db3787aeaabefa96256580678cec783986836fc64f8ff622"}, + {file = "scikit_learn-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f77547165c00625551e5c250cefa3f03f2fc92c5e18668abd90bfc4be2e0bff"}, + {file = "scikit_learn-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:118a8d229a41158c9f90093e46b3737120a165181a1b58c03461447aa4657415"}, + {file = "scikit_learn-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:a03b09f9f7f09ffe8c5efffe2e9de1196c696d811be6798ad5eddf323c6f4d40"}, + {file = "scikit_learn-1.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:460806030c666addee1f074788b3978329a5bfdc9b7d63e7aad3f6d45c67a210"}, + {file = "scikit_learn-1.5.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:1b94d6440603752b27842eda97f6395f570941857456c606eb1d638efdb38184"}, + {file = "scikit_learn-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d82c2e573f0f2f2f0be897e7a31fcf4e73869247738ab8c3ce7245549af58ab8"}, + {file = "scikit_learn-1.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3a10e1d9e834e84d05e468ec501a356226338778769317ee0b84043c0d8fb06"}, + {file = "scikit_learn-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:855fc5fa8ed9e4f08291203af3d3e5fbdc4737bd617a371559aaa2088166046e"}, + {file = "scikit_learn-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:40fb7d4a9a2db07e6e0cae4dc7bdbb8fada17043bac24104d8165e10e4cff1a2"}, + {file = "scikit_learn-1.5.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:47132440050b1c5beb95f8ba0b2402bbd9057ce96ec0ba86f2f445dd4f34df67"}, + {file = "scikit_learn-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174beb56e3e881c90424e21f576fa69c4ffcf5174632a79ab4461c4c960315ac"}, + {file = "scikit_learn-1.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261fe334ca48f09ed64b8fae13f9b46cc43ac5f580c4a605cbb0a517456c8f71"}, + {file = "scikit_learn-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:057b991ac64b3e75c9c04b5f9395eaf19a6179244c089afdebaad98264bff37c"}, + {file = "scikit_learn-1.5.0.tar.gz", hash = "sha256:789e3db01c750ed6d496fa2db7d50637857b451e57bcae863bff707c1247bef7"}, ] [package.dependencies] -joblib = ">=1.1.1" -numpy = ">=1.17.3,<2.0" -scipy = ">=1.5.0" -threadpoolctl = ">=2.0.0" +joblib = ">=1.2.0" +numpy = ">=1.19.5" +scipy = ">=1.6.0" +threadpoolctl = ">=3.1.0" [package.extras] -benchmark = ["matplotlib (>=3.1.3)", "memory-profiler (>=0.57.0)", "pandas (>=1.0.5)"] -docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.1.3)", "memory-profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)", "sphinx (>=6.0.0)", "sphinx-copybutton (>=0.5.2)", "sphinx-gallery (>=0.10.1)", "sphinx-prompt (>=1.3.0)", "sphinxext-opengraph (>=0.4.2)"] -examples = ["matplotlib (>=3.1.3)", "pandas (>=1.0.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)"] -tests = ["black (>=23.3.0)", "matplotlib (>=3.1.3)", "mypy (>=1.3)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.0.272)", "scikit-image (>=0.16.2)"] +benchmark = ["matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "pandas (>=1.1.5)"] +build = ["cython (>=3.0.10)", "meson-python (>=0.15.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)"] +docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "polars (>=0.20.23)", "pooch (>=1.6.0)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)", "sphinx (>=6.0.0)", "sphinx-copybutton (>=0.5.2)", "sphinx-gallery (>=0.15.0)", "sphinx-prompt (>=1.3.0)", "sphinxext-opengraph (>=0.4.2)"] +examples = ["matplotlib (>=3.3.4)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)"] +install = ["joblib (>=1.2.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)", "threadpoolctl (>=3.1.0)"] +maintenance = ["conda-lock (==2.5.6)"] +tests = ["black (>=24.3.0)", "matplotlib (>=3.3.4)", "mypy (>=1.9)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "polars (>=0.20.23)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pyarrow (>=12.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.2.1)", "scikit-image (>=0.17.2)"] [[package]] name = "scikit-optimize" @@ -4339,19 +4491,18 @@ win32 = ["pywin32"] [[package]] name = "setuptools" -version = "68.2.2" +version = "70.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, - {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, + {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"}, + {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "setuptools-scm" @@ -4791,31 +4942,36 @@ files = [ [[package]] name = "torch" -version = "2.1.0" +version = "2.2.0" description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" optional = false python-versions = ">=3.8.0" files = [ - {file = "torch-2.1.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:bf57f8184b2c317ef81fb33dc233ce4d850cd98ef3f4a38be59c7c1572d175db"}, - {file = "torch-2.1.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a04a0296d47f28960f51c18c5489a8c3472f624ec3b5bcc8e2096314df8c3342"}, - {file = "torch-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0bd691efea319b14ef239ede16d8a45c246916456fa3ed4f217d8af679433cc6"}, - {file = "torch-2.1.0-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:101c139152959cb20ab370fc192672c50093747906ee4ceace44d8dd703f29af"}, - {file = "torch-2.1.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:a6b7438a90a870e4cdeb15301519ae6c043c883fcd224d303c5b118082814767"}, - {file = "torch-2.1.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:2224622407ca52611cbc5b628106fde22ed8e679031f5a99ce286629fc696128"}, - {file = "torch-2.1.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:8132efb782cd181cc2dcca5e58effbe4217cdb2581206ac71466d535bf778867"}, - {file = "torch-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:5c3bfa91ce25ba10116c224c59d5b64cdcce07161321d978bd5a1f15e1ebce72"}, - {file = "torch-2.1.0-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:601b0a2a9d9233fb4b81f7d47dca9680d4f3a78ca3f781078b6ad1ced8a90523"}, - {file = "torch-2.1.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:3cd1dedff13884d890f18eea620184fb4cd8fd3c68ce3300498f427ae93aa962"}, - {file = "torch-2.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fb7bf0cc1a3db484eb5d713942a93172f3bac026fcb377a0cd107093d2eba777"}, - {file = "torch-2.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:761822761fffaa1c18a62c5deb13abaa780862577d3eadc428f1daa632536905"}, - {file = "torch-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:458a6d6d8f7d2ccc348ac4d62ea661b39a3592ad15be385bebd0a31ced7e00f4"}, - {file = "torch-2.1.0-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:c8bf7eaf9514465e5d9101e05195183470a6215bb50295c61b52302a04edb690"}, - {file = "torch-2.1.0-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:05661c32ec14bc3a157193d0f19a7b19d8e61eb787b33353cad30202c295e83b"}, - {file = "torch-2.1.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:556d8dd3e0c290ed9d4d7de598a213fb9f7c59135b4fee144364a8a887016a55"}, - {file = "torch-2.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:de7d63c6ecece118684415a3dbd4805af4a4c1ee1490cccf7405d8c240a481b4"}, - {file = "torch-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:2419cf49aaf3b2336c7aa7a54a1b949fa295b1ae36f77e2aecb3a74e3a947255"}, - {file = "torch-2.1.0-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:6ad491e70dbe4288d17fdbfc7fbfa766d66cbe219bc4871c7a8096f4a37c98df"}, - {file = "torch-2.1.0-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:421739685eba5e0beba42cb649740b15d44b0d565c04e6ed667b41148734a75b"}, + {file = "torch-2.2.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:d366158d6503a3447e67f8c0ad1328d54e6c181d88572d688a625fac61b13a97"}, + {file = "torch-2.2.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:707f2f80402981e9f90d0038d7d481678586251e6642a7a6ef67fc93511cb446"}, + {file = "torch-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:15c8f0a105c66b28496092fca1520346082e734095f8eaf47b5786bac24b8a31"}, + {file = "torch-2.2.0-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:0ca4df4b728515ad009b79f5107b00bcb2c63dc202d991412b9eb3b6a4f24349"}, + {file = "torch-2.2.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:3d3eea2d5969b9a1c9401429ca79efc668120314d443d3463edc3289d7f003c7"}, + {file = "torch-2.2.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:0d1c580e379c0d48f0f0a08ea28d8e373295aa254de4f9ad0631f9ed8bc04c24"}, + {file = "torch-2.2.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:9328e3c1ce628a281d2707526b4d1080eae7c4afab4f81cea75bde1f9441dc78"}, + {file = "torch-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:03c8e660907ac1b8ee07f6d929c4e15cd95be2fb764368799cca02c725a212b8"}, + {file = "torch-2.2.0-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:da0cefe7f84ece3e3b56c11c773b59d1cb2c0fd83ddf6b5f7f1fd1a987b15c3e"}, + {file = "torch-2.2.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:f81d23227034221a4a4ff8ef24cc6cec7901edd98d9e64e32822778ff01be85e"}, + {file = "torch-2.2.0-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:dcbfb2192ac41ca93c756ebe9e2af29df0a4c14ee0e7a0dd78f82c67a63d91d4"}, + {file = "torch-2.2.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:9eeb42971619e24392c9088b5b6d387d896e267889d41d267b1fec334f5227c5"}, + {file = "torch-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:c718b2ca69a6cac28baa36d86d8c0ec708b102cebd1ceb1b6488e404cd9be1d1"}, + {file = "torch-2.2.0-cp312-none-macosx_10_9_x86_64.whl", hash = "sha256:f11d18fceb4f9ecb1ac680dde7c463c120ed29056225d75469c19637e9f98d12"}, + {file = "torch-2.2.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:ee1da852bfd4a7e674135a446d6074c2da7194c1b08549e31eae0b3138c6b4d2"}, + {file = "torch-2.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0d819399819d0862268ac531cf12a501c253007df4f9e6709ede8a0148f1a7b8"}, + {file = "torch-2.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:08f53ccc38c49d839bc703ea1b20769cc8a429e0c4b20b56921a9f64949bf325"}, + {file = "torch-2.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:93bffe3779965a71dab25fc29787538c37c5d54298fd2f2369e372b6fb137d41"}, + {file = "torch-2.2.0-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:c17ec323da778efe8dad49d8fb534381479ca37af1bfc58efdbb8607a9d263a3"}, + {file = "torch-2.2.0-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:c02685118008834e878f676f81eab3a952b7936fa31f474ef8a5ff4b5c78b36d"}, + {file = "torch-2.2.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d9f39d6f53cec240a0e3baa82cb697593340f9d4554cee6d3d6ca07925c2fac0"}, + {file = "torch-2.2.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:51770c065206250dc1222ea7c0eff3f88ab317d3e931cca2aee461b85fbc2472"}, + {file = "torch-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:008e4c6ad703de55af760c73bf937ecdd61a109f9b08f2bbb9c17e7c7017f194"}, + {file = "torch-2.2.0-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:de8680472dd14e316f42ceef2a18a301461a9058cd6e99a1f1b20f78f11412f1"}, + {file = "torch-2.2.0-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:99e1dcecb488e3fd25bcaac56e48cdb3539842904bdc8588b0b255fde03a254c"}, ] [package.dependencies] @@ -4823,41 +4979,54 @@ filelock = "*" fsspec = "*" jinja2 = "*" networkx = "*" +nvidia-cublas-cu12 = {version = "12.1.3.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cuda-cupti-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cuda-nvrtc-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cuda-runtime-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cudnn-cu12 = {version = "8.9.2.26", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cufft-cu12 = {version = "11.0.2.54", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-curand-cu12 = {version = "10.3.2.106", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cusolver-cu12 = {version = "11.4.5.107", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cusparse-cu12 = {version = "12.1.0.106", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-nccl-cu12 = {version = "2.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-nvtx-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} sympy = "*" -typing-extensions = "*" +triton = {version = "2.2.0", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +typing-extensions = ">=4.8.0" [package.extras] opt-einsum = ["opt-einsum (>=3.3)"] +optree = ["optree (>=0.9.1)"] [[package]] name = "tornado" -version = "6.3.3" +version = "6.4.1" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." optional = false -python-versions = ">= 3.8" +python-versions = ">=3.8" files = [ - {file = "tornado-6.3.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:502fba735c84450974fec147340016ad928d29f1e91f49be168c0a4c18181e1d"}, - {file = "tornado-6.3.3-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:805d507b1f588320c26f7f097108eb4023bbaa984d63176d1652e184ba24270a"}, - {file = "tornado-6.3.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd19ca6c16882e4d37368e0152f99c099bad93e0950ce55e71daed74045908f"}, - {file = "tornado-6.3.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ac51f42808cca9b3613f51ffe2a965c8525cb1b00b7b2d56828b8045354f76a"}, - {file = "tornado-6.3.3-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71a8db65160a3c55d61839b7302a9a400074c9c753040455494e2af74e2501f2"}, - {file = "tornado-6.3.3-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ceb917a50cd35882b57600709dd5421a418c29ddc852da8bcdab1f0db33406b0"}, - {file = "tornado-6.3.3-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:7d01abc57ea0dbb51ddfed477dfe22719d376119844e33c661d873bf9c0e4a16"}, - {file = "tornado-6.3.3-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:9dc4444c0defcd3929d5c1eb5706cbe1b116e762ff3e0deca8b715d14bf6ec17"}, - {file = "tornado-6.3.3-cp38-abi3-win32.whl", hash = "sha256:65ceca9500383fbdf33a98c0087cb975b2ef3bfb874cb35b8de8740cf7f41bd3"}, - {file = "tornado-6.3.3-cp38-abi3-win_amd64.whl", hash = "sha256:22d3c2fa10b5793da13c807e6fc38ff49a4f6e1e3868b0a6f4164768bb8e20f5"}, - {file = "tornado-6.3.3.tar.gz", hash = "sha256:e7d8db41c0181c80d76c982aacc442c0783a2c54d6400fe028954201a2e032fe"}, + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, + {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, + {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, + {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, ] [[package]] name = "tqdm" -version = "4.66.1" +version = "4.66.3" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"}, - {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, + {file = "tqdm-4.66.3-py3-none-any.whl", hash = "sha256:4f41d54107ff9a223dca80b53efe4fb654c67efaba7f47bada3ee9d50e05bd53"}, + {file = "tqdm-4.66.3.tar.gz", hash = "sha256:23097a41eba115ba99ecae40d06444c15d1c0c698d527a01c6c8bd1c5d0647e5"}, ] [package.dependencies] @@ -4884,6 +5053,29 @@ files = [ docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] test = ["argcomplete (>=3.0.3)", "mypy (>=1.5.1)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] +[[package]] +name = "triton" +version = "2.2.0" +description = "A language and compiler for custom Deep Learning operations" +optional = false +python-versions = "*" +files = [ + {file = "triton-2.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2294514340cfe4e8f4f9e5c66c702744c4a117d25e618bd08469d0bfed1e2e5"}, + {file = "triton-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da58a152bddb62cafa9a857dd2bc1f886dbf9f9c90a2b5da82157cd2b34392b0"}, + {file = "triton-2.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af58716e721460a61886668b205963dc4d1e4ac20508cc3f623aef0d70283d5"}, + {file = "triton-2.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8fe46d3ab94a8103e291bd44c741cc294b91d1d81c1a2888254cbf7ff846dab"}, + {file = "triton-2.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8ce26093e539d727e7cf6f6f0d932b1ab0574dc02567e684377630d86723ace"}, + {file = "triton-2.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:227cc6f357c5efcb357f3867ac2a8e7ecea2298cd4606a8ba1e931d1d5a947df"}, +] + +[package.dependencies] +filelock = "*" + +[package.extras] +build = ["cmake (>=3.20)", "lit"] +tests = ["autopep8", "flake8", "isort", "numpy", "pytest", "scipy (>=1.7.1)", "torch"] +tutorials = ["matplotlib", "pandas", "tabulate", "torch"] + [[package]] name = "types-python-dateutil" version = "2.8.19.14" @@ -4948,13 +5140,13 @@ dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake [[package]] name = "urllib3" -version = "1.26.18" +version = "1.26.19" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, - {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, + {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"}, + {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"}, ] [package.extras] @@ -5194,4 +5386,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "7ad8d4483748ef27386455a22fe6a2f147b8ad6990c0955505761a7784d48856" +content-hash = "1f15063315eb8c6ace81fb765b03f5183e990ae3198729acfc37c8d6354e948f" diff --git a/blog/pi/pyproject.toml b/blog/pi/pyproject.toml index 1222bce5115c4..be4e38eddc675 100644 --- a/blog/pi/pyproject.toml +++ b/blog/pi/pyproject.toml @@ -21,8 +21,8 @@ python-dotenv = "^1.0.0" snowflake-connector-python = "^3.0.2" openapi3-parser = "^1.1.6" plotly = "^5.14.0" -scikit-learn = "^1.2.2" -torch = "^2.0.0" +scikit-learn = "^1.5.0" +torch = "^2.2.0" matplotlib = "^3.7.1" scikit-optimize = "^0.9.0" supabase = "^1.2.0" diff --git a/components/_0codekit/README.md b/components/_0codekit/README.md new file mode 100644 index 0000000000000..4bb42aa6fdb52 --- /dev/null +++ b/components/_0codekit/README.md @@ -0,0 +1,11 @@ +# Overview + +The 0codekit API offers a suite of tools aimed at improving development processes by providing services like code generation, data conversion, and other utility functions. In Pipedream, you can leverage this API to automate repetitive coding tasks, convert data formats on-the-fly, or integrate seamless code-generation features into your workflows. By harnessing the power of 0codekit within Pipedream's serverless platform, you can build efficient, scalable, and automated solutions that react to various triggers and interact with many apps. + +# Example Use Cases + +- **Automated Code Generation for New Database Entries**: Whenever a new record is added to a database (like Airtable or Google Sheets), trigger a Pipedream workflow that uses 0codekit to generate boilerplate code based on the database schema. This code could then be committed to a GitHub repository automatically. + +- **Dynamic Data Conversion in Response to Webhooks**: Set up a Pipedream workflow that listens to incoming webhooks, and use the 0codekit API to convert the payload data from XML to JSON, or vice versa. Then, send the converted data to other services like Slack for notifications or to AWS S3 for storage. + +- **Scheduled Code Cleanup and Optimization**: Create a weekly scheduled workflow in Pipedream that fetches code from a specified repository, runs it through 0codekit's code optimization services, and then commits the optimized code back to the repo. Integrate with apps like Jira to create tickets if the code optimization suggests significant refactoring. diff --git a/components/_10000ft/README.md b/components/_10000ft/README.md index 623ad3113968a..0076b07847e5b 100644 --- a/components/_10000ft/README.md +++ b/components/_10000ft/README.md @@ -1,9 +1,17 @@ # Overview -With the 10,000ft API, you can build applications that: +The 10,000ft by Smartsheet API opens the door to managing project scheduling, resource allocation, and reporting in a programmatic fashion. This API allows you to automate the retrieval of project data, update assignments, track time and expenses, and generate real-time reports. By leveraging Pipedream's capabilities, you can create custom workflows that respond to events in 10,000ft, synchronize data across multiple systems, and streamline project management tasks. -- Query and update data in 10,000ft -- Automate the creation and updating of projects, people, and places in - 10,000ft -- Visualize data from 10,000ft in custom dashboards and reports -- Integrate data from 10,000ft with other enterprise systems +# Example Use Cases + +**Automated Project Tracking and Notifications** + +- Trigger a workflow in Pipedream when a new project is created in 10,000ft to notify team members via Slack or email. Subsequently, create tasks for the project in a tool like Trello or Asana, ensuring everyone is aligned and ready to start. + +**Resource Allocation Analysis** + +- Set up a Pipedream workflow to run on a schedule, fetching current project assignments from 10,000ft and comparing them against capacity data. If a resource is over-allocated, the workflow can send alerts to a project manager or directly update the allocation in 10,000ft to prevent burnout. + +**Time Tracking Integration** + +- Connect 10,000ft time tracking features with accounting software like QuickBooks. Use Pipedream to detect when a time entry is added in 10,000ft, process the data, and create an invoice in QuickBooks, automating the billing process based on actual hours worked. diff --git a/components/_10000ft/package.json b/components/_10000ft/package.json new file mode 100644 index 0000000000000..43f3196125017 --- /dev/null +++ b/components/_10000ft/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/_10000ft", + "version": "0.6.0", + "description": "Pipedream _10000ft Components", + "main": "_10000ft.app.mjs", + "keywords": [ + "pipedream", + "_10000ft" + ], + "homepage": "https://pipedream.com/apps/_10000ft", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/_1crm/README.md b/components/_1crm/README.md new file mode 100644 index 0000000000000..e4eb3d5738632 --- /dev/null +++ b/components/_1crm/README.md @@ -0,0 +1,11 @@ +# Overview + +The 1CRM API enables robust integration of 1CRM systems with external applications, facilitating automated workflows and data synchronization. With Pipedream, you can leverage this API to connect 1CRM with hundreds of other services, automate repetitive tasks, streamline sales processes, and enhance customer relationship management. By creating workflows on Pipedream, you can trigger actions in 1CRM based on events from other apps, manipulate and analyze CRM data, and improve overall business efficiency without manual intervention. + +# Example Use Cases + +- **Lead Sync from Google Forms to 1CRM**: Automatically create a new lead in 1CRM whenever a Google Form is submitted. This workflow can capture information such as name, email, and phone number directly into your CRM, helping your sales team follow up promptly. + +- **Customer Support Ticket Creation**: When a customer sends an email to a designated support address, parse the email content using Pipedream and create a support case in 1CRM. This ensures that every customer issue is tracked and managed systematically within 1CRM without manual data entry. + +- **Slack Notifications for New Opportunities**: Set up a workflow where new opportunities created in 1CRM trigger a notification in a Slack channel. This keeps your team instantly informed about new sales prospects and can accelerate the response time, enhancing the chance to close deals quicker. diff --git a/components/_1crm/_1crm.app.mjs b/components/_1crm/_1crm.app.mjs new file mode 100644 index 0000000000000..17f48569cf333 --- /dev/null +++ b/components/_1crm/_1crm.app.mjs @@ -0,0 +1,83 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "_1crm", + propDefinitions: { + recordId: { + type: "string", + label: "Contact ID", + description: "ID of the contact", + async options({ + page, model, + }) { + const { records } = await this.listModuleRecords({ + module: model, + params: { + offset: LIMIT * page, + limit: LIMIT, + }, + }); + + return records.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return `${this.$auth.url}/api.php`; + }, + _auth() { + return { + username: `${this.$auth.username}`, + password: `${this.$auth.password}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + auth: this._auth(), + ...opts, + }); + }, + getFields({ module }) { + return this._makeRequest({ + path: `/meta/fields/${module}`, + }); + }, + listModuleRecords({ + module, ...opts + }) { + return this._makeRequest({ + path: `/data/${module}`, + ...opts, + }); + }, + createModel({ + model, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/data/${model}`, + ...opts, + }); + }, + updateModel({ + updateId, model, ...opts + }) { + return this._makeRequest({ + method: "PATCH", + path: `/data/${model}/${updateId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/_1crm/actions/common/base.mjs b/components/_1crm/actions/common/base.mjs new file mode 100644 index 0000000000000..46e3b29d5741f --- /dev/null +++ b/components/_1crm/actions/common/base.mjs @@ -0,0 +1,106 @@ +import _1crm from "../../_1crm.app.mjs"; + +export default { + props: { + _1crm, + checkDuplicates: { + type: "boolean", + label: "Check Duplicates", + description: "Check Duplicates Flag", + default: true, + reloadProps: true, + }, + }, + methods: { + getMethod() { + return "create"; + }, + getUpdateId() { + return ""; + }, + getType(type) { + switch (type) { + case "bool": return "boolean"; + case "int": return "integer"; + case "multienum": return "string[]"; + default: return "string"; + } + }, + filterFields(fields) { + const groups = []; + return Object.keys(fields) + .filter( (key) => !("editable" in fields[key])) + .filter( (key) => { + if (fields[key].multi_select_group && !groups.includes(fields[key].multi_select_group)) { + groups.push(fields[key].multi_select_group); + return true; + } + if (!fields[key].multi_select_group ) return true; + }) + .reduce( (res, key) => (res[key] = fields[key], res), {} ); + }, + fixValues(data) { + return Object.keys(data) + .reduce( (res, key) => (res[key] = (typeof data[key] === "boolean" + ? +data[key] + : (Array.isArray(data[key])) + ? data[key].join(",") + : data[key]), res), {} ); + }, + }, + async additionalProps() { + const method = this.getMethod(); + const props = {}; + let { fields } = await this._1crm.getFields({ + module: this.getModule(), + }); + delete fields.assigned_user; + delete fields.assigned_user_id; + + fields = this.filterFields(fields); + + for (const [ + key, + value, + ] of Object.entries(fields)) { + props[key] = { + type: this.getType(value.type), + label: value.vname, + description: value.comment, + optional: (method === "create") + ? !value.required + : true, + options: value.options, + }; + } + return props; + }, + async run({ $ }) { + const { + _1crm, + checkDuplicates, + ...data + } = this; + + const method = this.getMethod(); + const fn = (method === "create") + ? _1crm.createModel + : _1crm.updateModel; + + const response = await fn({ + $, + data: { + data: this.fixValues(data), + }, + params: { + check_duplicates: checkDuplicates, + }, + updateId: this.getUpdateId(), + model: this.getModule(), + }); + if (response.errors) throw new Error(response.errors); + + $.export("$summary", this.getSummary(response)); + return response; + }, +}; diff --git a/components/_1crm/actions/create-contact/create-contact.mjs b/components/_1crm/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..56d1224cabde0 --- /dev/null +++ b/components/_1crm/actions/create-contact/create-contact.mjs @@ -0,0 +1,19 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "_1crm-create-contact", + name: "Create Contact", + description: "Creates a new contact in the 1CRM system. [See the documentation](https://demo.1crmcloud.com/api.php#endpoint_dataList_post)", + version: "0.0.1", + type: "action", + methods: { + ...common.methods, + getModule() { + return "Contact"; + }, + getSummary({ id }) { + return `Successfully created contact with ID ${id}`; + }, + }, +}; diff --git a/components/_1crm/actions/create-lead/create-lead.mjs b/components/_1crm/actions/create-lead/create-lead.mjs new file mode 100644 index 0000000000000..a2796fce8845b --- /dev/null +++ b/components/_1crm/actions/create-lead/create-lead.mjs @@ -0,0 +1,19 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "_1crm-create-lead", + name: "Create Lead", + description: "Crafts a new lead in 1CRM. [See the documentation](https://demo.1crmcloud.com/api.php#endpoint_dataList_post)", + version: "0.0.1", + type: "action", + methods: { + ...common.methods, + getModule() { + return "Lead"; + }, + getSummary({ id }) { + return `Successfully created lead with ID ${id}`; + }, + }, +}; diff --git a/components/_1crm/actions/update-contact/update-contact.mjs b/components/_1crm/actions/update-contact/update-contact.mjs new file mode 100644 index 0000000000000..78d109d25ec57 --- /dev/null +++ b/components/_1crm/actions/update-contact/update-contact.mjs @@ -0,0 +1,34 @@ +import common from "../create-contact/create-contact.mjs"; + +export default { + ...common, + key: "_1crm-update-contact", + name: "Update Contact", + description: "Modifies an existing contact within the 1CRM system. [See the documentation](https://demo.1crmcloud.com/api.php#endpoint_dataRecord_patch)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + contactId: { + propDefinition: [ + common.props._1crm, + "recordId", + () => ({ + model: "Contact", + }), + ], + }, + }, + methods: { + ...common.methods, + getMethod() { + return "update"; + }, + getUpdateId() { + return this.contactId; + }, + getSummary() { + return `Successfully updated contact with ID ${this.contactId}`; + }, + }, +}; diff --git a/components/_1crm/actions/update-lead/update-lead.mjs b/components/_1crm/actions/update-lead/update-lead.mjs new file mode 100644 index 0000000000000..4cbe05c2db631 --- /dev/null +++ b/components/_1crm/actions/update-lead/update-lead.mjs @@ -0,0 +1,36 @@ +import common from "../create-lead/create-lead.mjs"; + +export default { + ...common, + key: "_1crm-update-lead", + name: "Update Lead", + description: "Updates an existing lead in 1CRM. [See the documentation](https://demo.1crmcloud.com/api.php#endpoint_dataRecord_patch)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + leadId: { + propDefinition: [ + common.props._1crm, + "recordId", + () => ({ + model: "Lead", + }), + ], + label: "Lead ID", + description: "ID of the lead", + }, + }, + methods: { + ...common.methods, + getMethod() { + return "update"; + }, + getUpdateId() { + return this.leadId; + }, + getSummary() { + return `Lead with ID ${this.leadId} updated successfully`; + }, + }, +}; diff --git a/components/_1crm/common/constants.mjs b/components/_1crm/common/constants.mjs new file mode 100644 index 0000000000000..ea830c15a04cb --- /dev/null +++ b/components/_1crm/common/constants.mjs @@ -0,0 +1 @@ +export const LIMIT = 100; diff --git a/components/_1crm/package.json b/components/_1crm/package.json new file mode 100644 index 0000000000000..8b66069d864c9 --- /dev/null +++ b/components/_1crm/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/_1crm", + "version": "0.1.0", + "description": "Pipedream 1CRM Components", + "main": "_1crm.app.mjs", + "keywords": [ + "pipedream", + "_1crm" + ], + "homepage": "https://pipedream.com/apps/_1crm", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/_1crm/yarn.lock b/components/_1crm/yarn.lock new file mode 100644 index 0000000000000..6f39995bded92 --- /dev/null +++ b/components/_1crm/yarn.lock @@ -0,0 +1,85 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@pipedream/platform@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@pipedream/platform/-/platform-3.0.0.tgz#f5d39315aefefb77689719eacf392340f1fe630d" + integrity sha512-qlSJBF0Gn5RloFEPr072hSf6pRSU++Zjs0j6X9ZE2SMmfg777qYR4RQ5HkTHEELEyA948xEOqkBt3bFplpfEbw== + dependencies: + axios "^1.6.5" + fp-ts "^2.0.2" + io-ts "^2.0.0" + querystring "^0.2.1" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^1.6.5: + version "1.7.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" + integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +fp-ts@^2.0.2: + version "2.16.8" + resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.16.8.tgz#dfa1ea1c967ac6794c43ce877aeb8ed76f5e0df7" + integrity sha512-nmDtNqmMZkOxu0M5hkrS9YA15/KPkYkILb6Axg9XBAoUoYEtzg+LFmVWqZrl9FNttsW0qIUpx9RCA9INbv+Bxw== + +io-ts@^2.0.0: + version "2.2.21" + resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.2.21.tgz#4ef754176f7082a1099d04c7d5c4ea53267c530a" + integrity sha512-zz2Z69v9ZIC3mMLYWIeoUcwWD6f+O7yP92FMVVaXEOSZH1jnVBmET/urd/uoarD1WGBY4rCj8TAyMPzsGNzMFQ== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +querystring@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd" + integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg== diff --git a/components/_21risk/README.md b/components/_21risk/README.md new file mode 100644 index 0000000000000..91943a617bf49 --- /dev/null +++ b/components/_21risk/README.md @@ -0,0 +1,11 @@ +# Overview + +The 21RISK API provides functionalities to manage and monitor risk within organizations. Leveraging Pipedream's integration capabilities allows for automated workflows that can streamline risk management processes, trigger alerts, and synchronize risk data across various business tools. This API is particularly useful for organizations looking to automate their risk assessment and mitigation strategies effectively. + +# Example Use Cases + +- **Automated Risk Reporting**: Set up a workflow where risk assessment data from 21RISK is automatically fetched and compiled into a report format. This report can then be emailed weekly to stakeholders using the Gmail app on Pipedream. + +- **Risk Change Alerts**: Create a workflow that monitors for any updates or changes in the risk status within 21RISK. If a risk level changes to a critical status, use Twilio's SMS service on Pipedream to send an immediate alert to relevant team members. + +- **Compliance Check Automation**: Develop a workflow that integrates 21RISK with a compliance tracking app. Automatically fetch the latest risk data and use it to verify compliance with internal or external standards, flagging non-compliant items for review. diff --git a/components/_21risk/_21risk.app.mjs b/components/_21risk/_21risk.app.mjs new file mode 100644 index 0000000000000..38a18d83d05fa --- /dev/null +++ b/components/_21risk/_21risk.app.mjs @@ -0,0 +1,43 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "_21risk", + methods: { + _baseUrl() { + return "https://www.21risk.com/odata/v5"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listActions(opts = {}) { + return this._makeRequest({ + path: "/actions", + ...opts, + }); + }, + listAuditors(opts = {}) { + return this._makeRequest({ + path: "/auditor", + ...opts, + }); + }, + listReports(opts = {}) { + return this._makeRequest({ + path: "/reports", + ...opts, + }); + }, + }, +}; diff --git a/components/_21risk/package.json b/components/_21risk/package.json new file mode 100644 index 0000000000000..3b11744c06118 --- /dev/null +++ b/components/_21risk/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/_21risk", + "version": "0.1.0", + "description": "Pipedream 21RISK Components", + "main": "_21risk.app.mjs", + "keywords": [ + "pipedream", + "_21risk" + ], + "homepage": "https://pipedream.com/apps/_21risk", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/_21risk/sources/common/base.mjs b/components/_21risk/sources/common/base.mjs new file mode 100644 index 0000000000000..fb072e391fa74 --- /dev/null +++ b/components/_21risk/sources/common/base.mjs @@ -0,0 +1,58 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import _21risk from "../../_21risk.app.mjs"; + +export default { + props: { + _21risk, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + async emitEvent(maxResults = false) { + const lastId = this._getLastId(); + const fieldId = this.getFieldId(); + const fn = this.getFunction(); + const { value: response } = await fn(); + + let responseArray = []; + for (const item of response) { + if (item[fieldId] === lastId) break; + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastId(responseArray[0][fieldId]); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item[fieldId], + summary: this.getSummary(item), + ts: Date.parse(new Date()), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/_21risk/sources/new-action/new-action.mjs b/components/_21risk/sources/new-action/new-action.mjs new file mode 100644 index 0000000000000..cffbd58c837aa --- /dev/null +++ b/components/_21risk/sources/new-action/new-action.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "_21risk-new-action", + name: "New Action Created", + description: "Emit new event when a new action is created due to non-compliance in a risk-model category during an audit.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFieldId() { + return "_KeyActionId"; + }, + getFunction() { + return this._21risk.listActions; + }, + getSummary(item) { + return `New Action: ${item._KeyActionId}`; + }, + }, + sampleEmit, +}; diff --git a/components/_21risk/sources/new-action/test-event.mjs b/components/_21risk/sources/new-action/test-event.mjs new file mode 100644 index 0000000000000..f5dc1edd4e939 --- /dev/null +++ b/components/_21risk/sources/new-action/test-event.mjs @@ -0,0 +1,7 @@ +export default { + "_KeyActionId": "6686d07b345869d8f6d59c", + "Action Link": "https://www.21risk.com/actions/table?action=668687543783426d59c", + "Action Duedate": null, + "Action Columns": "{}", + "_KeyOrganizationId": "6686bb76a3ea456745409c9935" +} \ No newline at end of file diff --git a/components/_21risk/sources/new-auditor/new-auditor.mjs b/components/_21risk/sources/new-auditor/new-auditor.mjs new file mode 100644 index 0000000000000..7753387f17a32 --- /dev/null +++ b/components/_21risk/sources/new-auditor/new-auditor.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "_21risk-new-auditor", + name: "New Auditor Created", + description: "Emit new event when a new auditor is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFieldId() { + return "_KeyAuditorId"; + }, + getFunction() { + return this._21risk.listAuditors; + }, + getSummary(item) { + return `New Auditor: ${item["Auditor Name"]}`; + }, + }, + sampleEmit, +}; diff --git a/components/_21risk/sources/new-auditor/test-event.mjs b/components/_21risk/sources/new-auditor/test-event.mjs new file mode 100644 index 0000000000000..462bad7ead0e1 --- /dev/null +++ b/components/_21risk/sources/new-auditor/test-event.mjs @@ -0,0 +1,7 @@ +export default { + "Auditor Name":"auditor Name", + "Auditor Email":"auditor@email.com", + "Auditor isAnonymous":false, + "_KeyAuditorId":"21risk|6686c6453456e6502dd8b90", + "_KeyOrganizationId":"6686b345634a3eac84569c9935" +} \ No newline at end of file diff --git a/components/_21risk/sources/new-report/new-report.mjs b/components/_21risk/sources/new-report/new-report.mjs new file mode 100644 index 0000000000000..5a736207129c6 --- /dev/null +++ b/components/_21risk/sources/new-report/new-report.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "_21risk-new-report", + name: "New Report Created", + description: "Emit new event when a new report is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFieldId() { + return "_KeyReportId"; + }, + getFunction() { + return this._21risk.listReports; + }, + getSummary(item) { + return `New Report with Id: ${item._KeyReportId}`; + }, + }, + sampleEmit, +}; diff --git a/components/_21risk/sources/new-report/test-event.mjs b/components/_21risk/sources/new-report/test-event.mjs new file mode 100644 index 0000000000000..7261db42b4656 --- /dev/null +++ b/components/_21risk/sources/new-report/test-event.mjs @@ -0,0 +1,16 @@ +export default { + "Report Status":"published", + "Report Participants Count":0, + "Report Audit Date":null, + "Report Published Date":"2024-07-04T16:40:33.309Z", + "Report Published Or Last Modified Date":"2024-07-04T16:40:33.309Z", + "Report CreatedDate":"2024-07-04T16:40:27.000Z", + "Report Last Modified":"2024-07-04T16:40:33.309Z", + "Report AuditorId":"21risk|6686bb2343eac814e03453454", + "Report Link":"https://21risk.com/report/6686d0745234565608d8", + "_KeyReportId":"6686d0745234565608d8", + "_KeyLocationId":"6686cf080a8678678106", + "_KeyRiskTypeId":"6686d0034567567990e196", + "_KeyModelId":"6686d0034567567990e197", + "_KeyOrganizationId":"6686bb76a3eac234562456935" +} \ No newline at end of file diff --git a/components/_2chat/README.md b/components/_2chat/README.md new file mode 100644 index 0000000000000..e856e0773933f --- /dev/null +++ b/components/_2chat/README.md @@ -0,0 +1,11 @@ +# Overview + +The 2Chat API allows developers to build and manage chatbots that can engage users in personalized conversations across various platforms like WhatsApp, Telegram, and more. This API facilitates the creation, training, and integration of chatbots with existing applications, enabling automated responses based on user inputs and behaviors. Utilizing Pipedream's capabilities, developers can orchestrate complex workflows that react to events from 2Chat, process data, and trigger actions in other apps, streamlining communication processes and enhancing user interaction. + +# Example Use Cases + +- **Customer Support Automation**: Automate initial customer interactions on your business's WhatsApp number using a 2Chat-powered chatbot. Set up a Pipedream workflow that triggers whenever a new message is received. The workflow could analyze the message, use AI to understand the query, and either provide an instant reply or escalate complex issues to a human agent. Connect this system to a CRM like Salesforce to log interactions automatically. + +- **Feedback Collection**: Use a 2Chat bot to solicit feedback after customer interactions or events. Configure a Pipedream workflow that triggers at the end of a chat session or after a purchase. The workflow can prompt the chatbot to ask for feedback, parse the responses, and save them to a Google Sheets document for analysis and future action. + +- **Event Reminder and Follow-Up**: Integrate 2Chat with a calendar app like Google Calendar via Pipedream to manage event reminders and follow-ups. Set up a workflow where the chatbot sends reminders to participants before the event and collects RSVPs or answers inquiries about the event details. After the event, the chatbot can reach out for feedback or provide additional resources related to the event topics. diff --git a/components/_2chat/_2chat.app.mjs b/components/_2chat/_2chat.app.mjs new file mode 100644 index 0000000000000..a7bd31f002690 --- /dev/null +++ b/components/_2chat/_2chat.app.mjs @@ -0,0 +1,88 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "_2chat", + propDefinitions: { + fromNumber: { + type: "string", + label: "From Phone Number", + description: "The WhatsApp number that you connected to 2Chat", + async options() { + const { numbers } = await this.listPhoneNumbers(); + return numbers.map(({ + phone_number: value, friendly_name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.p.2chat.io/open"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "X-User-API-Key": `${this.$auth.api_key}`, + "Content-Type": "application/json", + }, + }); + }, + createWebhook({ + event, ...opts + }) { + return this._makeRequest({ + path: `/webhooks/subscribe/${event}`, + method: "POST", + ...opts, + }); + }, + deleteWebhook({ + hookId, ...opts + }) { + return this._makeRequest({ + path: `/webhooks/${hookId}`, + method: "DELETE", + ...opts, + }); + }, + listPhoneNumbers(opts = {}) { + return this._makeRequest({ + path: "/whatsapp/get-numbers", + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + path: "/contacts", + method: "POST", + ...opts, + }); + }, + sendMessage(opts = {}) { + return this._makeRequest({ + path: "/whatsapp/send-message", + method: "POST", + ...opts, + }); + }, + checkWhatsAppAccount({ + fromNumber, numberToCheck, ...opts + }) { + return this._makeRequest({ + path: `/whatsapp/check-number/${fromNumber}/${numberToCheck}`, + ...opts, + }); + }, + }, +}; diff --git a/components/_2chat/actions/check-phone-number-whatsapp/check-phone-number-whatsapp.mjs b/components/_2chat/actions/check-phone-number-whatsapp/check-phone-number-whatsapp.mjs new file mode 100644 index 0000000000000..27152767f0113 --- /dev/null +++ b/components/_2chat/actions/check-phone-number-whatsapp/check-phone-number-whatsapp.mjs @@ -0,0 +1,32 @@ +import twoChat from "../../_2chat.app.mjs"; + +export default { + key: "_2chat-check-phone-number-whatsapp", + name: "Check Phone Number for WhatsApp", + description: "Checks if a given phone number has a WhatsApp account. [See the documentation](https://developers.2chat.co/docs/API/WhatsApp/check-number)", + version: "0.0.1", + type: "action", + props: { + twoChat, + fromNumber: { + propDefinition: [ + twoChat, + "fromNumber", + ], + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number to check for a WhatsApp account. Example: `+12121112222`", + }, + }, + async run({ $ }) { + const response = await this.twoChat.checkWhatsAppAccount({ + $, + fromNumber: this.fromNumber, + numberToCheck: this.phoneNumber, + }); + $.export("$summary", `Checked phone number ${this.phoneNumber} for WhatsApp account`); + return response; + }, +}; diff --git a/components/_2chat/actions/create-contact/create-contact.mjs b/components/_2chat/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..6c8a009682f2c --- /dev/null +++ b/components/_2chat/actions/create-contact/create-contact.mjs @@ -0,0 +1,53 @@ +import twoChat from "../../_2chat.app.mjs"; + +export default { + key: "_2chat-create-contact", + name: "Create Contact", + description: "Creates a new contact in 2Chat. [See the documentation](https://developers.2chat.co/docs/API/Contacts/create-contact)", + version: "0.0.1", + type: "action", + props: { + twoChat, + firstName: { + type: "string", + label: "First Name", + description: "First name of the contact", + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the contact", + }, + email: { + type: "string", + label: "Email", + description: "Email address of the contact", + }, + phoneNumber: { + type: "string", + label: "Phone", + description: "Phone number of the contact. Example: `+12121112222`", + }, + }, + async run({ $ }) { + const response = await this.twoChat.createContact({ + $, + data: { + first_name: this.firstName, + last_name: this.lastName, + contact_detail: [ + { + type: "E", + value: this.email, + }, + { + type: "PH", + value: this.phoneNumber, + }, + ], + }, + }); + $.export("$summary", `Successfully created contact ${this.firstName} ${this.lastName}`); + return response; + }, +}; diff --git a/components/_2chat/actions/send-whatsapp-text-message/send-whatsapp-text-message.mjs b/components/_2chat/actions/send-whatsapp-text-message/send-whatsapp-text-message.mjs new file mode 100644 index 0000000000000..01e259d07afb2 --- /dev/null +++ b/components/_2chat/actions/send-whatsapp-text-message/send-whatsapp-text-message.mjs @@ -0,0 +1,40 @@ +import twoChat from "../../_2chat.app.mjs"; + +export default { + key: "_2chat-send-whatsapp-text-message", + name: "Send Whatsapp Text Message", + description: "Sends a text message to a designated whatsapp-enabled phone number. [See the documentation](https://developers.2chat.co/docs/API/WhatsApp/send-message)", + version: "0.0.1", + type: "action", + props: { + twoChat, + fromNumber: { + propDefinition: [ + twoChat, + "fromNumber", + ], + }, + toNumber: { + type: "string", + label: "Phone", + description: "The number you want to send your message to. Example: `+12121112222`", + }, + text: { + type: "string", + label: "Text", + description: "The content of the message you want to send", + }, + }, + async run({ $ }) { + const response = await this.twoChat.sendMessage({ + $, + data: { + from_number: this.fromNumber, + to_number: this.toNumber, + text: this.text, + }, + }); + $.export("$summary", `Successfully sent message to ${this.toNumber}`); + return response; + }, +}; diff --git a/components/_2chat/common/constants.mjs b/components/_2chat/common/constants.mjs new file mode 100644 index 0000000000000..d0ca8b8e044c8 --- /dev/null +++ b/components/_2chat/common/constants.mjs @@ -0,0 +1,30 @@ +const TIME_PERIODS = [ + { + value: "all-time", + label: "Will trigger the new conversation event when the user messages you for the first time ever", + }, + { + value: "hour", + label: "Will trigger the new conversation event after 1 hour of no messages", + }, + { + value: "day", + label: "Will trigger the new conversation event after 1 day of no messages", + }, + { + value: "week", + label: "Will trigger the new conversation event after 1 week of no messages", + }, + { + value: "month", + label: "Will trigger the new conversation event after 1 month of no messages", + }, + { + value: "year", + label: "Will trigger the new conversation event after 1 year of no messages", + }, +]; + +export default { + TIME_PERIODS, +}; diff --git a/components/_2chat/package.json b/components/_2chat/package.json new file mode 100644 index 0000000000000..99e0d256b5096 --- /dev/null +++ b/components/_2chat/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/_2chat", + "version": "0.1.0", + "description": "Pipedream _2chat Components", + "main": "_2chat.app.mjs", + "keywords": [ + "pipedream", + "_2chat" + ], + "homepage": "https://pipedream.com/apps/_2chat", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/_2chat/sources/common/base.mjs b/components/_2chat/sources/common/base.mjs new file mode 100644 index 0000000000000..4b7ffe749f3e2 --- /dev/null +++ b/components/_2chat/sources/common/base.mjs @@ -0,0 +1,58 @@ +import twoChat from "../../_2chat.app.mjs"; + +export default { + props: { + twoChat, + db: "$.service.db", + http: "$.interface.http", + fromNumber: { + propDefinition: [ + twoChat, + "fromNumber", + ], + }, + }, + hooks: { + async activate() { + const { data: { uuid } } = await this.twoChat.createWebhook({ + event: this.getEvent(), + data: { + hook_url: this.http.endpoint, + on_number: this.fromNumber, + ...this.getWebhookParams(), + }, + }); + this._setHookId(uuid); + }, + async deactivate() { + const hookId = this._getHookId(); + if (hookId) { + await this.twoChat.deleteWebhook({ + hookId, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getWebhookParams() { + return {}; + }, + getEvent() { + throw new Error("getEvent is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run(event) { + const { body } = event; + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, +}; diff --git a/components/_2chat/sources/new-conversation-instant/new-conversation-instant.mjs b/components/_2chat/sources/new-conversation-instant/new-conversation-instant.mjs new file mode 100644 index 0000000000000..e607cff63caaa --- /dev/null +++ b/components/_2chat/sources/new-conversation-instant/new-conversation-instant.mjs @@ -0,0 +1,43 @@ +import common from "../common/base.mjs"; +import constants from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "_2chat-new-conversation-instant", + name: "New Conversation (Instant)", + description: "Emit new event when a new WhatsApp conversation is started on the user’s 2chat connected number.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + timePeriod: { + type: "string", + label: "Time Period", + description: "Your preference to consider a conversation new", + options: constants.TIME_PERIODS, + optional: true, + default: "all-time", + }, + }, + methods: { + ...common.methods, + getEvent() { + return "whatsapp.conversation.new"; + }, + getWebhookParams() { + return { + time_period: this.timePeriod, + }; + }, + generateMeta(body) { + return { + id: body.id, + summary: `New Conversation ID: ${body.id}`, + ts: Date.parse(body.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/_2chat/sources/new-conversation-instant/test-event.mjs b/components/_2chat/sources/new-conversation-instant/test-event.mjs new file mode 100644 index 0000000000000..44e4515b7e7c7 --- /dev/null +++ b/components/_2chat/sources/new-conversation-instant/test-event.mjs @@ -0,0 +1,14 @@ +export default { + "id": "MSG58fab6b0-04f6-449e-acf5-42fc6875ba94", + "uuid": "MSG58fab6b0-04f6-449e-acf5-42fc6875ba94", + "session_key": "WW-WPN3281cfa4-9d92-4d20-9e8e-35f695fe0faf-5215533334444@c.us", + "message": { + "text": "hello" + }, + "created_at": "2024-07-05T19:21:51", + "remote_phone_number": "+5215533334444", + "channel_phone_number": "+595981461442", + "sent_by": "api", + "event": "whatsapp.conversation.new", + "time_period": "all-time" +} \ No newline at end of file diff --git a/components/_2chat/sources/new-message-instant/new-message-instant.mjs b/components/_2chat/sources/new-message-instant/new-message-instant.mjs new file mode 100644 index 0000000000000..9e59c9c4eac24 --- /dev/null +++ b/components/_2chat/sources/new-message-instant/new-message-instant.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "_2chat-new-message-instant", + name: "New Message (Instant)", + description: "Emit new event when a new message is either sent or received on 2Chat.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return "whatsapp.message.new"; + }, + generateMeta(body) { + return { + id: body.id, + summary: `New Message ID: ${body.id}`, + ts: Date.parse(body.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/_2chat/sources/new-message-instant/test-event.mjs b/components/_2chat/sources/new-message-instant/test-event.mjs new file mode 100644 index 0000000000000..872a53a0cc4ef --- /dev/null +++ b/components/_2chat/sources/new-message-instant/test-event.mjs @@ -0,0 +1,20 @@ +export default { + "id": "MSGcf20bbdb-0a8a-4eef-bca1-aa2ea22c5294", + "uuid": "MSGcf20bbdb-0a8a-4eef-bca1-aa2ea22c5294", + "created_at": "2023-04-18T22:17:50Z", + "session_key": "WW-WPN1045d068-6c42-4157-8325-41e3c5710dc6-5215533334444@c.us", + "message": { + "media": { + "url": "https://2chat-user-data.s3.amazonaws.com/ACC28526e8e-ac01-4b1f-ac11-939f2b7fca73/example.jpeg", + "type": "image", + "mime_type": "image/jpeg", + }, + }, + "remote_phone_number": "+5215533334444", + "sent_by": "user", + "contact": { + "first_name": "Johny", + "last_name": "Doe", + "profile_pic_url": "https://2chat-user-data.s3.amazonaws.com/example-profile-pic.jpeg", + }, +}; diff --git a/components/_2chat/sources/new-whatsapp-order-instant/new-whatsapp-order-instant.mjs b/components/_2chat/sources/new-whatsapp-order-instant/new-whatsapp-order-instant.mjs new file mode 100644 index 0000000000000..62851bac573c1 --- /dev/null +++ b/components/_2chat/sources/new-whatsapp-order-instant/new-whatsapp-order-instant.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "_2chat-new-whatsapp-order-instant", + name: "New WhatsApp Order (Instant)", + description: "Emit new event when a WhatsApp order is received on user's 2Chat connected number.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return "whatsapp.order.received"; + }, + generateMeta(body) { + return { + id: body.id, + summary: `New Order ID: ${body.id}`, + ts: Date.parse(body.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/_2chat/sources/new-whatsapp-order-instant/test-event.mjs b/components/_2chat/sources/new-whatsapp-order-instant/test-event.mjs new file mode 100644 index 0000000000000..c3714a6e183d7 --- /dev/null +++ b/components/_2chat/sources/new-whatsapp-order-instant/test-event.mjs @@ -0,0 +1,43 @@ +export default { + "id": "MSGa13150ca-13e5-4c0a-b979-f45980830c10", + "uuid": "MSGa13150ca-13e5-4c0a-b979-f45980830c10", + "session_key": "WW-WPN66037eca-9ad1-4c96-9eff-526a90a48c77-5215533334444@c.us", + "message": { + "text": "ADIDAS | CLASSIC BACKPACK | LEGEND INK MULTICOLOUR Regular, NIKE | SWOOSH PRO FLAT PEAK CAP Regular", + "order": { + "subtotal": 0, + "total": 0, + "currency": null, + "created_at": 1710021608, + "products": [ + { + "id": "6562709013816345", + "name": "ADIDAS | CLASSIC BACKPACK | LEGEND INK MULTICOLOUR Regular", + "price": 150.5, + "currency": "USD", + "quantity": 1, + "data": null, + "thumbnail_url": "https://2chat-user-data-dev.s3.amazonaws.com/order/product/6562709013816345.jpeg", + }, + { + "id": "7096834393683427", + "name": "NIKE | SWOOSH PRO FLAT PEAK CAP Regular", + "price": 210.0, + "currency": "USD", + "quantity": 1, + "data": null, + "thumbnail_url": "https://2chat-user-data-dev.s3.amazonaws.com/order/product/7096834393683427.jpeg", + }, + ], + }, + }, + "created_at": "2024-03-09T22:00:08", + "remote_phone_number": "+5215533334444", + "channel_phone_number": "+595981461442", + "sent_by": "user", + "contact": { + "first_name": null, + "last_name": null, + "profile_pic_url": null, + }, +}; diff --git a/components/_2markdown/README.md b/components/_2markdown/README.md new file mode 100644 index 0000000000000..8be4d0abae4ab --- /dev/null +++ b/components/_2markdown/README.md @@ -0,0 +1,11 @@ +# Overview + +The 2markdown API on Pipedream enables you to convert HTML content into Markdown format seamlessly. This functionality is pivotal for content creators, developers, and marketers who often need to transition between web content and Markdown for various platforms like GitHub, blogs, or documentation sites. By leveraging Pipedream's robust integration and automation capabilities, you can set up workflows that trigger the API to process content dynamically, connect with other services for enhanced automation, or even archive and manage your Markdown conversions systematically. + +# Example Use Cases + +- **HTML to Markdown for Documentation**: Automatically convert HTML documentation to Markdown when updating repository README files or wiki pages on code hosting platforms like GitHub or GitLab. Use the GitHub or GitLab app on Pipedream to listen for changes to HTML files, then pass the content through 2markdown API and commit the converted Markdown back to the repository. + +- **Blog Content Syndication**: Syndicate blog content across multiple platforms by automatically converting blog posts from HTML to Markdown. When a new post is published in a CMS like WordPress (hooked via Webhook on Pipedream), trigger a workflow that uses 2markdown to format the post, then distribute the Markdown to platforms like Medium or Dev.to using their respective APIs. + +- **Email to Markdown Archiving**: Convert HTML emails to Markdown to create a searchable archive. When a new email is received in services like Gmail (accessible via the Gmail app on Pipedream), trigger the 2markdown API to convert the email content and store it in a markdown format in a database or a service like Dropbox or Google Drive for easy reference and searchability. diff --git a/components/_2markdown/_2markdown.app.mjs b/components/_2markdown/_2markdown.app.mjs new file mode 100644 index 0000000000000..5996f3464f0b9 --- /dev/null +++ b/components/_2markdown/_2markdown.app.mjs @@ -0,0 +1,76 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "_2markdown", + propDefinitions: { + filePath: { + type: "string", + label: "File Path", + description: "The path to an HTML file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + }, + }, + methods: { + _baseUrl() { + return "https://api.2markdown.com/v1"; + }, + _makeRequest({ + $ = this, + path, + headers, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + ...headers, + "X-Api-Key": this.$auth.api_key, + }, + ...opts, + }); + }, + getJobStatus({ + jobId, ...opts + }) { + return this._makeRequest({ + path: `/pdf2md/${jobId}`, + ...opts, + }); + }, + pdfToMarkdown(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/pdf2md", + ...opts, + }); + }, + urlToMarkdown(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/url2md", + ...opts, + }); + }, + urlToMarkdownWithJs(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/url2mdjs", + ...opts, + }); + }, + htmlFileToMarkdown(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/file2md", + ...opts, + }); + }, + htmlToMarkdown(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/html2md", + ...opts, + }); + }, + }, +}; diff --git a/components/_2markdown/actions/html-file-to-markdown/html-file-to-markdown.mjs b/components/_2markdown/actions/html-file-to-markdown/html-file-to-markdown.mjs new file mode 100644 index 0000000000000..c5e8510e1e639 --- /dev/null +++ b/components/_2markdown/actions/html-file-to-markdown/html-file-to-markdown.mjs @@ -0,0 +1,37 @@ +import _2markdown from "../../_2markdown.app.mjs"; +import fs from "fs"; +import FormData from "form-data"; + +export default { + key: "_2markdown-html-file-to-markdown", + name: "HTML File to Markdown", + description: "Convert an HTML file to Markdown format. [See the documentation](https://2markdown.com/docs#file2md)", + version: "0.0.1", + type: "action", + props: { + _2markdown, + filePath: { + propDefinition: [ + _2markdown, + "filePath", + ], + }, + }, + async run({ $ }) { + const form = new FormData(); + + form.append("document", fs.createReadStream(this.filePath.includes("tmp/") + ? this.filePath + : `/tmp/${this.filePath}`)); + + const response = await this._2markdown.htmlFileToMarkdown({ + $, + headers: form.getHeaders(), + data: form, + }); + + $.export("$summary", "Successfully converted HTML file to markdown."); + + return response; + }, +}; diff --git a/components/_2markdown/actions/html-to-markdown/html-to-markdown.mjs b/components/_2markdown/actions/html-to-markdown/html-to-markdown.mjs new file mode 100644 index 0000000000000..133616d409059 --- /dev/null +++ b/components/_2markdown/actions/html-to-markdown/html-to-markdown.mjs @@ -0,0 +1,29 @@ +import _2markdown from "../../_2markdown.app.mjs"; + +export default { + key: "_2markdown-html-to-markdown", + name: "HTML to Markdown", + description: "Convert raw HTML content to Markdown format. [See the documentation](https://2markdown.com/docs#html2md)", + version: "0.0.1", + type: "action", + props: { + _2markdown, + html: { + type: "string", + label: "HTML", + description: "The HTML content to be converted to Markdown", + }, + }, + async run({ $ }) { + const response = await this._2markdown.htmlToMarkdown({ + $, + data: { + html: this.html, + }, + }); + + $.export("$summary", "Successfully converted HTML to markdown."); + + return response; + }, +}; diff --git a/components/_2markdown/actions/pdf-to-markdown/pdf-to-markdown.mjs b/components/_2markdown/actions/pdf-to-markdown/pdf-to-markdown.mjs new file mode 100644 index 0000000000000..597a80fdb6812 --- /dev/null +++ b/components/_2markdown/actions/pdf-to-markdown/pdf-to-markdown.mjs @@ -0,0 +1,58 @@ +import _2markdown from "../../_2markdown.app.mjs"; +import fs from "fs"; +import FormData from "form-data"; + +export default { + key: "_2markdown-pdf-to-markdown", + name: "PDF to Markdown", + description: "Convert a PDF document to Markdown format. [See the documentation](https://2markdown.com/docs#pdf2md)", + version: "0.0.1", + type: "action", + props: { + _2markdown, + filePath: { + propDefinition: [ + _2markdown, + "filePath", + ], + description: "The path to a PDF file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + }, + waitForCompletion: { + type: "boolean", + label: "Wait for Completion", + description: "Set to `true` to poll the API in 3-second intervals until the job is complete", + optional: true, + }, + }, + async run({ $ }) { + const form = new FormData(); + + form.append("document", fs.createReadStream(this.filePath.includes("tmp/") + ? this.filePath + : `/tmp/${this.filePath}`)); + + let response = await this._2markdown.pdfToMarkdown({ + $, + headers: form.getHeaders(), + data: form, + }); + + if (this.waitForCompletion) { + const timer = (ms) => new Promise((res) => setTimeout(res, ms)); + const jobId = response.jobId; + while (response.status === "processing" || response.status === "pending") { + response = await this._2markdown.getJobStatus({ + $, + jobId, + }); + await timer(3000); + } + } + + $.export("$summary", `${this.waitForCompletion + ? "Finished" + : "Started"} converting PDF file to markdown.`); + + return response; + }, +}; diff --git a/components/_2markdown/actions/url-to-markdown/url-to-markdown.mjs b/components/_2markdown/actions/url-to-markdown/url-to-markdown.mjs new file mode 100644 index 0000000000000..f36e22581f44e --- /dev/null +++ b/components/_2markdown/actions/url-to-markdown/url-to-markdown.mjs @@ -0,0 +1,39 @@ +import _2markdown from "../../_2markdown.app.mjs"; + +export default { + key: "_2markdown-url-to-markdown", + name: "URL to Markdown", + description: "Extract the essential content of a website as plaintext. [See the documentation](https://2markdown.com/docs#url2md)", + version: "0.0.1", + type: "action", + props: { + _2markdown, + url: { + type: "string", + label: "URL", + description: "The URL to be processed. Costs 1 credit per request.", + }, + js: { + type: "boolean", + label: "Include Javascript Support", + description: "Set to `true` to extract content as plaintext including any javascript-rendered resources. Costs an additional credit per request.", + optional: true, + }, + }, + async run({ $ }) { + const fn = this.js + ? this._2markdown.urlToMarkdownWithJs + : this._2markdown.urlToMarkdown; + + const response = await fn({ + $, + data: { + url: this.url, + }, + }); + + $.export("$summary", "Successfully extracted website content."); + + return response; + }, +}; diff --git a/components/_2markdown/package.json b/components/_2markdown/package.json new file mode 100644 index 0000000000000..9df443a7fe99d --- /dev/null +++ b/components/_2markdown/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/_2markdown", + "version": "0.1.0", + "description": "Pipedream 2markdown Components", + "main": "_2markdown.app.mjs", + "keywords": [ + "pipedream", + "_2markdown" + ], + "homepage": "https://pipedream.com/apps/_2markdown", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "form-data": "^4.0.1" + } +} diff --git a/components/_360nrs/README.md b/components/_360nrs/README.md new file mode 100644 index 0000000000000..d2c36eef01776 --- /dev/null +++ b/components/_360nrs/README.md @@ -0,0 +1,11 @@ +# Overview + +The 360NRS API allows the automation of messaging services, providing capabilities to send SMS, email, and push notifications efficiently. This API can be particularly useful in Pipedream to create dynamic, responsive communication workflows that enhance customer engagement and operational efficiency. By integrating 360NRS with other applications, users can streamline notifications, reminders, and marketing campaigns based on user data and interaction triggers from various sources. + +# Example Use Cases + +- **Automated Customer Support Notifications**: Trigger SMS or email notifications from 360NRS in response to customer support tickets created in Help Desk software like Zendesk. When a new ticket is submitted, a Pipedream workflow can automatically send a confirmation message or update via 360NRS, ensuring customers receive timely communication. + +- **E-Commerce Order Alerts**: Connect 360NRS with an e-commerce platform like Shopify. Set up a workflow in Pipedream that sends an SMS or email when a new order is placed, providing customers with immediate order confirmation and expected delivery details. + +- **Event-Driven Marketing Campaigns**: Use Pipedream to monitor user activity from a web analytics tool like Google Analytics. Launch targeted email or SMS marketing campaigns through 360NRS based on specific user actions or milestones, such as visiting a particular product page several times, or achieving a new level in a loyalty program. diff --git a/components/_360nrs/_360nrs.app.mjs b/components/_360nrs/_360nrs.app.mjs new file mode 100644 index 0000000000000..25bfd6a0bfde4 --- /dev/null +++ b/components/_360nrs/_360nrs.app.mjs @@ -0,0 +1,70 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "_360nrs", + propDefinitions: { + message: { + type: "string", + label: "Message", + description: "Text of message. At most, there can be 160 characters. The text must be encoded in UTF-8.", + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "Mobile phone number(s) to receive message. Must include the prefix (e.g. in Spain `34666666666`).", + async options() { + const { data } = await this.listContacts(); + return data.map(({ + email: label, phone: value, + }) => ({ + label, + value, + })); + }, + }, + from: { + type: "string", + label: "From Sender", + description: "Sender text, this label will consist of 15 numbers or 11 alphanumeric characters.", + }, + }, + methods: { + _baseUrl() { + return "https://dashboard.360nrs.com/api/rest"; + }, + getHeaders(headers) { + const { + username, + api_password: password, + } = this.$auth; + const token = Buffer.from(`${username}:${password}`).toString("base64"); + return { + ...headers, + "Content-Type": "application/json", + "Authorization": `Basic ${token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: `${this._baseUrl()}${path}`, + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + listContacts(args = {}) { + return this._makeRequest({ + path: "/contacts", + ...args, + }); + }, + }, +}; diff --git a/components/_360nrs/actions/send-sms/send-sms.mjs b/components/_360nrs/actions/send-sms/send-sms.mjs new file mode 100644 index 0000000000000..9c22aa87f4d79 --- /dev/null +++ b/components/_360nrs/actions/send-sms/send-sms.mjs @@ -0,0 +1,60 @@ +import app from "../../_360nrs.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "_360nrs-send-sms", + name: "Send SMS", + description: "Send an SMS message to one or more recipients. [See the documentation](https://apidocs.360nrs.com/?shell#sms)", + version: "0.0.1", + type: "action", + props: { + app, + message: { + propDefinition: [ + app, + "message", + ], + }, + to: { + type: "string[]", + label: "To Phone Number(s)", + propDefinition: [ + app, + "phoneNumber", + ], + }, + from: { + propDefinition: [ + app, + "from", + ], + }, + }, + methods: { + sendSMS(args = {}) { + return this.app.post({ + path: "/sms", + ...args, + }); + }, + }, + async run({ $ }) { + const { + sendSMS, + message, + from, + to, + } = this; + + const response = await sendSMS({ + data: { + message, + to: utils.parseArray(to), + from, + }, + }); + + $.export("$summary", `Successfully sent SMS with ID \`${response.result[0]?.id}\``); + return response; + }, +}; diff --git a/components/_360nrs/common/utils.mjs b/components/_360nrs/common/utils.mjs new file mode 100644 index 0000000000000..d9df0aa935aef --- /dev/null +++ b/components/_360nrs/common/utils.mjs @@ -0,0 +1,28 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +export default { + parseArray, +}; diff --git a/components/_360nrs/package.json b/components/_360nrs/package.json new file mode 100644 index 0000000000000..4dcdf6ec033b9 --- /dev/null +++ b/components/_360nrs/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/_360nrs", + "version": "0.1.0", + "description": "Pipedream 360NRS Components", + "main": "_360nrs.app.mjs", + "keywords": [ + "pipedream", + "_360nrs" + ], + "homepage": "https://pipedream.com/apps/_360nrs", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/_46elks/README.md b/components/_46elks/README.md new file mode 100644 index 0000000000000..1be182c796963 --- /dev/null +++ b/components/_46elks/README.md @@ -0,0 +1,11 @@ +# Overview + +The 46elks API grants access to a powerful suite of telephony services including SMS, MMS, and voice calls. You can use Pipedream to automate interactions with this API, creating workflows that respond to incoming messages, initiate outbound communication, or integrate with other services for enhanced functionality. Whether you're setting up SMS alerts, automating voice responses, or facilitating customer support, the 46elks API on Pipedream can streamline these processes. + +# Example Use Cases + +- **SMS Customer Feedback Collection**: Trigger a Pipedream workflow when a customer sends an SMS to your 46elks number. Collect feedback, store it in a Google Sheet, and send a thank you reply via SMS, creating a seamless feedback loop. + +- **Appointment Reminders**: Use a scheduled Pipedream workflow to fetch appointments from a Calendar app, send reminders as SMS through 46elks, and handle confirmations or rescheduling requests automatically. + +- **Voice-Activated Order Status**: Build a workflow where a customer can call a 46elks phone number, enter their order ID, and the workflow fetches the order status from an e-commerce platform like Shopify. Then, use 46elks to relay that status back to the customer through a voice message. diff --git a/components/_4dem/README.md b/components/_4dem/README.md new file mode 100644 index 0000000000000..9ab65e78eedc6 --- /dev/null +++ b/components/_4dem/README.md @@ -0,0 +1,11 @@ +# Overview + +The 4Dem API provides robust tools for email marketing, allowing users to manage contacts, send emails, and track campaign effectiveness. Leveraging this API within Pipedream enables users to automate and integrate these email marketing capabilities with other services, enhancing productivity and personalization. Use cases range from syncing subscriber data across platforms to triggering custom email campaigns based on user behavior. + +# Example Use Cases + +- **Automated Welcome Email Sequence**: Trigger a personalized welcome email sequence in 4Dem when a new user signs up via a web form on your site. Use Pipedream to connect the web form (e.g., Typeform, Google Forms) to 4Dem, ensuring new subscribers immediately receive your onboarding emails. + +- **Sync New Shopify Customers to 4Dem Contacts**: Automatically add new customers from Shopify to your 4Dem contact list. Each time a purchase is made, a Pipedream workflow can capture customer data from Shopify and create or update the customer's profile in 4Dem, allowing targeted follow-up campaigns. + +- **Dynamic Content Emails Based on User Activity**: Use Pipedream to monitor user activity from your app (e.g., most viewed products) and trigger customized email campaigns via 4Dem. Integrate with analytics tools like Google Analytics to fetch user behavior data and use it to send highly relevant emails through 4Dem. diff --git a/components/_4dem/_4dem.app.mjs b/components/_4dem/_4dem.app.mjs new file mode 100644 index 0000000000000..269ef52ab7737 --- /dev/null +++ b/components/_4dem/_4dem.app.mjs @@ -0,0 +1,145 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "_4dem", + propDefinitions: { + name: { + type: "string", + label: "Name", + description: "Name of the sender", + }, + email: { + type: "string", + label: "Email", + description: "Email address", + }, + nominative: { + type: "string", + label: "Nominative", + description: "Sender nominativa, i.e. `My Company`", + }, + ivaFCode: { + type: "string", + label: "IVA Code", + description: "Sender IVA code, i.e. `IT123456789`", + }, + address: { + type: "string", + label: "Address", + description: "Sender address, i.e. `Via Roma 1`", + }, + city: { + type: "string", + label: "City", + description: "Sender city, i.e. `Torino`", + }, + province: { + type: "string", + label: "Province", + description: "Abbreviation of the province, i.e. `TO`", + }, + country: { + type: "string", + label: "Country", + description: "Sender country, i.e. `Italia`", + }, + cap: { + type: "string", + label: "Postal Code", + description: "Sender postal code, i.e. `10100`", + }, + telephone: { + type: "string", + label: "Telephone", + description: "Telephone number, i.e.`+393331234567`", + }, + senderId: { + type: "string", + label: "Sender ID", + description: "ID of the sender", + async options() { + const { data: sendersIds } = await this.getSenders(); + + return sendersIds.map(({ + id, name, + }) => ({ + value: id, + label: name, + })); + }, + }, + authCode: { + type: "string", + label: "Auth Code", + description: "Authenticator code sent to the provided email", + }, + subject: { + type: "string", + label: "Email Subject", + description: "Subject of the email", + }, + plain: { + type: "string", + label: "Plain Content", + description: "Plain content of the email", + optional: true, + }, + html: { + type: "string", + label: "HTML Content", + description: "HTML content of the email", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.4dem.it"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + async createSender(args = {}) { + return this._makeRequest({ + method: "post", + path: "/senders/email", + ...args, + }); + }, + async confirmEmail({ + senderId, ...args + }) { + return this._makeRequest({ + method: "post", + path: `/senders/email/${senderId}/confirm`, + ...args, + }); + }, + async createEmail(args = {}) { + return this._makeRequest({ + method: "post", + path: "/contents/emails", + ...args, + }); + }, + async getSenders(args = {}) { + return this._makeRequest({ + path: "/senders/email", + ...args, + }); + }, + }, +}; diff --git a/components/_4dem/actions/confirm-email/confirm-email.mjs b/components/_4dem/actions/confirm-email/confirm-email.mjs new file mode 100644 index 0000000000000..2f937f6da4e43 --- /dev/null +++ b/components/_4dem/actions/confirm-email/confirm-email.mjs @@ -0,0 +1,37 @@ +import app from "../../_4dem.app.mjs"; + +export default { + key: "_4dem-confirm-email", + name: "Confirm Email", + description: "Confirm sender email address. [See the documentation](https://api.4dem.it/#/operations/senders.email.confirm)", + version: "0.0.1", + type: "action", + props: { + app, + senderId: { + propDefinition: [ + app, + "senderId", + ], + }, + authCode: { + propDefinition: [ + app, + "authCode", + ], + }, + }, + async run({ $ }) { + const response = await this.app.confirmEmail({ + $, + senderId: this.senderId, + data: { + auth_code: this.authCode, + }, + }); + + $.export("$summary", "Successfully confirmed the sender email"); + + return response; + }, +}; diff --git a/components/_4dem/actions/create-email/create-email.mjs b/components/_4dem/actions/create-email/create-email.mjs new file mode 100644 index 0000000000000..fc1219709e2fc --- /dev/null +++ b/components/_4dem/actions/create-email/create-email.mjs @@ -0,0 +1,63 @@ +import app from "../../_4dem.app.mjs"; + +export default { + key: "_4dem-create-email", + name: "Create Email", + description: "Create a new email. [See the documentation](https://api.4dem.it/#/operations/contents.email.store)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "name", + ], + description: "Email Name", + }, + subject: { + propDefinition: [ + app, + "subject", + ], + }, + senderId: { + propDefinition: [ + app, + "senderId", + ], + }, + plain: { + propDefinition: [ + app, + "plain", + ], + }, + html: { + propDefinition: [ + app, + "html", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createEmail({ + $, + data: { + name: this.name, + subject: this.subject, + sender: { + id: this.senderId, + }, + content: { + plain: this.plain, + html: this.html, + }, + }, + }); + + $.export("$summary", `Successfully created email '${this.name}'`); + + return response; + }, +}; diff --git a/components/_4dem/actions/create-sender/create-sender.mjs b/components/_4dem/actions/create-sender/create-sender.mjs new file mode 100644 index 0000000000000..8d620fc199dd9 --- /dev/null +++ b/components/_4dem/actions/create-sender/create-sender.mjs @@ -0,0 +1,95 @@ +import app from "../../_4dem.app.mjs"; + +export default { + key: "_4dem-create-sender", + name: "Create Sender", + description: "Create an email sender. You will need to confirm the email address used during creation. [See the documentation](https://api.4dem.it/#/operations/senders.email.store)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "name", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + nominative: { + propDefinition: [ + app, + "nominative", + ], + }, + ivaFCode: { + propDefinition: [ + app, + "ivaFCode", + ], + }, + address: { + propDefinition: [ + app, + "address", + ], + }, + city: { + propDefinition: [ + app, + "city", + ], + }, + province: { + propDefinition: [ + app, + "province", + ], + }, + country: { + propDefinition: [ + app, + "country", + ], + }, + cap: { + propDefinition: [ + app, + "cap", + ], + }, + telephone: { + propDefinition: [ + app, + "telephone", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createSender({ + $, + data: { + name: this.name, + email: this.email, + csa_data: { + nominative: this.nominative, + ivaFCode: this.ivaFCode, + address: this.address, + city: this.city, + province: this.province, + country: this.country, + cap: this.cap, + telephone: this.telephone, + }, + }, + }); + + $.export("$summary", "Successfully created the sender, now it's necessary to confirm it with the code sent to the provided email"); + + return response; + }, +}; diff --git a/components/_4dem/package.json b/components/_4dem/package.json new file mode 100644 index 0000000000000..2bbe4791dced8 --- /dev/null +++ b/components/_4dem/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/_4dem", + "version": "0.1.0", + "description": "Pipedream 4Dem Components", + "main": "_4dem.app.mjs", + "keywords": [ + "pipedream", + "_4dem" + ], + "homepage": "https://pipedream.com/apps/_4dem", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/_8x8_connect/_8x8_connect.app.mjs b/components/_8x8_connect/_8x8_connect.app.mjs new file mode 100644 index 0000000000000..c429dc50d5566 --- /dev/null +++ b/components/_8x8_connect/_8x8_connect.app.mjs @@ -0,0 +1,76 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "_8x8_connect", + propDefinitions: { + destination: { + type: "string", + label: "Destination", + description: "Destination of the SMS, i.e. `+12124567890`", + }, + text: { + type: "string", + label: "Text", + description: "The text of the message", + }, + from: { + type: "string", + label: "From", + description: "The initial date of the log, i.e. `2024-08-14T00:00:00`", + }, + to: { + type: "string", + label: "To", + description: "The initial date of the log, i.e. `2024-08-15T10:00:00`", + }, + jobId: { + type: "string", + label: "Log ID", + description: "The ID of the log", + }, + }, + methods: { + _baseUrl() { + return `${this.$auth.base_url}/api/v1`; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + async sendSms(args = {}) { + return this._makeRequest({ + method: "post", + path: `/subaccounts/${this.$auth.subaccount_id}/messages`, + ...args, + }); + }, + async requestLog(args = {}) { + return this._makeRequest({ + method: "post", + path: `/subaccounts/${this.$auth.subaccount_id}/messages/exports`, + ...args, + }); + }, + async getLog({ + jobId, ...args + }) { + return this._makeRequest({ + path: `/subaccounts/${this.$auth.subaccount_id}/messages/exports/${jobId}`, + ...args, + }); + }, + }, +}; diff --git a/components/_8x8_connect/actions/get-log-result/get-log-result.mjs b/components/_8x8_connect/actions/get-log-result/get-log-result.mjs new file mode 100644 index 0000000000000..d1d68ee61f4c6 --- /dev/null +++ b/components/_8x8_connect/actions/get-log-result/get-log-result.mjs @@ -0,0 +1,28 @@ +import app from "../../_8x8_connect.app.mjs"; + +export default { + key: "_8x8_connect-get-log-result", + name: "Get Log Result", + description: "Check the status of an SMS Logs export job and to get a download link if its generation has succeeded. [See the documentation](https://developer.8x8.com/connect/reference/get-log-export-job-result)", + version: "0.0.1", + type: "action", + props: { + app, + jobId: { + propDefinition: [ + app, + "jobId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getLog({ + $, + jobId: this.jobId, + }); + + $.export("$summary", `Successfully retrieved the log status: '${response.status}'`); + + return response; + }, +}; diff --git a/components/_8x8_connect/actions/request-log/request-log.mjs b/components/_8x8_connect/actions/request-log/request-log.mjs new file mode 100644 index 0000000000000..61ac2001f4a18 --- /dev/null +++ b/components/_8x8_connect/actions/request-log/request-log.mjs @@ -0,0 +1,45 @@ +import app from "../../_8x8_connect.app.mjs"; + +export default { + key: "_8x8_connect-request-log", + name: "Request Log", + description: "Request an SMS log file. [See the documentation](https://developer.8x8.com/connect/reference/start-log-export-job)", + version: "0.0.4", + type: "action", + props: { + app, + destination: { + propDefinition: [ + app, + "destination", + ], + optional: true, + }, + from: { + propDefinition: [ + app, + "from", + ], + }, + to: { + propDefinition: [ + app, + "to", + ], + }, + }, + async run({ $ }) { + const response = await this.app.requestLog({ + $, + data: { + phoneNumber: this.destination, + from: this.from, + to: this.to, + }, + }); + + $.export("$summary", `Successfully requested SMS log. You will need the following ID get the request result: '${response.jobId}'`); + + return response; + }, +}; diff --git a/components/_8x8_connect/actions/send-sms/send-sms.mjs b/components/_8x8_connect/actions/send-sms/send-sms.mjs new file mode 100644 index 0000000000000..194cfcbdb3eba --- /dev/null +++ b/components/_8x8_connect/actions/send-sms/send-sms.mjs @@ -0,0 +1,38 @@ +import app from "../../_8x8_connect.app.mjs"; + +export default { + key: "_8x8_connect-send-sms", + name: "Send SMS", + description: "Send a SMS to the specified destination. [See the documentation](https://developer.8x8.com/connect/reference/send-sms-single)", + version: "0.0.2", + type: "action", + props: { + app, + destination: { + propDefinition: [ + app, + "destination", + ], + }, + text: { + propDefinition: [ + app, + "text", + ], + }, + + }, + async run({ $ }) { + const response = await this.app.sendSms({ + $, + data: { + destination: this.destination, + text: this.text, + }, + }); + + $.export("$summary", `Successfully queued SMS for processing. ID: '${response.umid}'`); + + return response; + }, +}; diff --git a/components/_8x8_connect/package.json b/components/_8x8_connect/package.json new file mode 100644 index 0000000000000..318e0d955f1cf --- /dev/null +++ b/components/_8x8_connect/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/_8x8_connect", + "version": "0.1.0", + "description": "Pipedream 8x8 Connect Components", + "main": "_8x8_connect.app.mjs", + "keywords": [ + "pipedream", + "_8x8_connect" + ], + "homepage": "https://pipedream.com/apps/_8x8_connect", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/__1msg/README.md b/components/__1msg/README.md new file mode 100644 index 0000000000000..8a96b5dad9272 --- /dev/null +++ b/components/__1msg/README.md @@ -0,0 +1,11 @@ +# Overview + +The 1msg API enables users to send SMS messages programmatically. On Pipedream, you can leverage this capability within serverless workflows. This is useful for automating notifications, sending verification codes, or integrating SMS messaging into broader communication strategies. By connecting 1msg with other apps on Pipedream, you can orchestrate complex workflows that trigger SMS messages based on various events. + +# Example Use Cases + +- **E-Commerce Order Confirmation**: After a customer places an order on your e-commerce platform, automatically send them an SMS confirmation using 1msg. This enhances the customer experience with immediate communication. + +- **Appointment Reminders**: Sync 1msg with a calendaring app like Google Calendar. Send SMS reminders to clients for upcoming appointments, reducing no-shows and improving service reliability. + +- **Real-time Alerts for Developers**: Integrate 1msg with GitHub to send SMS notifications to developers when there's a new commit or pull request on important repositories, helping teams stay informed on code changes. diff --git a/components/__1msg/__1msg.app.mjs b/components/__1msg/__1msg.app.mjs new file mode 100644 index 0000000000000..0d62a7e4a26b6 --- /dev/null +++ b/components/__1msg/__1msg.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "__1msg", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/__1msg/package.json b/components/__1msg/package.json new file mode 100644 index 0000000000000..ac31d0da03f4f --- /dev/null +++ b/components/__1msg/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/__1msg", + "version": "0.0.1", + "description": "Pipedream __1msg Components", + "main": "__1msg.app.mjs", + "keywords": [ + "pipedream", + "__1msg" + ], + "homepage": "https://pipedream.com/apps/__1msg", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/_twocaptcha/README.md b/components/_twocaptcha/README.md new file mode 100644 index 0000000000000..20331baa97f36 --- /dev/null +++ b/components/_twocaptcha/README.md @@ -0,0 +1,11 @@ +# Overview + +The 2Captcha API enables automated solving of captchas and reCaptcha. On Pipedream, you can integrate this API into workflows to handle captcha challenges when scraping websites, automating form submissions, or any process that encounters captchas. The API takes the captcha image or reCaptcha data as input and returns the text or token needed to bypass the captcha, effectively allowing your automations to mimic human interaction. + +# Example Use Cases + +- **Automated Account Creation**: Use 2Captcha in a workflow to solve captchas encountered during automated account sign-ups on various platforms. Combine it with HTTP requests or platform-specific APIs to complete the registration processes without manual intervention. + +- **Data Scraping Automation**: Create a workflow that scrapes data from websites protected by captcha. When a captcha is encountered, the 2Captcha service can be called to solve it, allowing your scraping tool, like Puppeteer or Cheerio run on Pipedream, to continue gathering the required data. + +- **Scheduled Form Submissions**: Implement a Pipedream workflow for submitting forms that include captcha validation on a schedule. Use 2Captcha to solve the captcha, and then submit the form with the correct data, automating repetitive submissions that would otherwise require human interaction. diff --git a/components/a123formbuilder/README.md b/components/a123formbuilder/README.md index 38ef84fef36e5..be8c997a2444f 100644 --- a/components/a123formbuilder/README.md +++ b/components/a123formbuilder/README.md @@ -1,12 +1,11 @@ # Overview -With the 123FormBuilder API, you can build forms and surveys to gather data -from users. You can then use this data to generate reports, or take action -based on the responses. Some examples of what you can build with the -123FormBuilder API include: - -- A form to collect contact information from customers -- A survey to gather feedback from employees -- A form to sign up new users for your website or app -- A form to collect orders from customers -- A survey to gather data from research participants +The 123FormBuilder API enables you to automate form and data management tasks, allowing seamless integration with Pipedream's serverless platform. With this API, you can fetch submitted form data in real-time, update form structures, or manage respondents and their submissions programmatically. Harnessing the power of the 123FormBuilder API in Pipedream workflows unlocks the potential for dynamic data collection, efficient data processing, and connectivity with a myriad of other services for an enhanced data-driven ecosystem. + +# Example Use Cases + +- **Automated Data Collection and Storage**: Trigger a Pipedream workflow whenever a new form submission is received on 123FormBuilder. Process the data within the workflow, and then save it to a Google Sheets document or a database like PostgreSQL for persistent storage and analysis. + +- **Dynamic Respondent Follow-up**: After a form submission, use the submission data to send personalized follow-up emails via SendGrid or create a support ticket in Zendesk. This workflow can be tailored to provide different responses based on the submitted data, ensuring a customized interaction with each respondent. + +- **Real-time Notifications and Reporting**: Set up a workflow to monitor form submissions and send real-time notifications to a Slack channel or a Discord server. This allows a team to instantly respond to inquiries or orders. Extend this by integrating with a reporting tool like Power BI to generate insights and visualize submission trends. diff --git a/components/ably/README.md b/components/ably/README.md index 93e0d026b21f9..83ffb3f75199c 100644 --- a/components/ably/README.md +++ b/components/ably/README.md @@ -1,11 +1,11 @@ # Overview -Ably offers a wide range of features that allow developers to build a variety -of applications. Here are some examples of what you can build using the Ably -API: - -- Real-time chat applications -- Multiplayer gaming applications -- Live updates for news and sports applications -- Data visualization applications -- Internet of Things applications +Ably API provides a platform for adding real-time messaging, streaming, and presence capabilities to your apps. With Pipedream, you can harness this power to create dynamic, event-driven workflows. Automate notifications, synchronize data across systems in real time, or react to Ably events to trigger actions in other services, like sending emails, updating databases, or calling external APIs. + +# Example Use Cases + +- **Real-Time Alert System**: Trigger an email or SMS notification via SendGrid or Twilio when a message is published on an Ably channel. This is ideal for real-time user alerts, system status updates, or time-sensitive information dissemination. + +- **Chatbot Integration**: Connect a Slack bot to an Ably channel to facilitate immediate response to customer queries. Whenever a new message appears in Ably, the Slack bot can process the message and respond accordingly, streamlining customer service operations. + +- **Cross-Platform Event Synchronization**: Use Pipedream to listen for Ably events and propagate them to other services like Salesforce, Google Sheets, or a custom API. This is perfect for ensuring that when an event happens in one part of your system, the relevant stakeholders or systems are updated instantly across platforms. diff --git a/components/abstract/README.md b/components/abstract/README.md new file mode 100644 index 0000000000000..e32f0ff8c785b --- /dev/null +++ b/components/abstract/README.md @@ -0,0 +1,11 @@ +# Overview + +The Abstract - Email Verification API allows you to verify the validity and quality of an email address. With this API, you can confirm if an email exists, is formatted correctly, and is not a temporary or disposable address. Integrating this API with Pipedream workflows offers numerous possibilities, such as filtering out fake email addresses from sign-up processes, maintaining email list hygiene, or automating customer outreach by ensuring that communications only go to verified emails. + +# Example Use Cases + +- **Sign-Up Form Validation Workflow**: When a new user signs up on your platform, trigger a Pipedream workflow to verify the email using the Abstract - Email Verification API. If the email is valid, proceed with the sign-up process; if not, prompt the user for a different email. This can cut down on fraudulent sign-ups and maintain the integrity of your user base. + +- **Scheduled Email List Cleaning**: Set up a Pipedream workflow to run on a schedule (e.g., monthly) that iterates through your email list, uses the Abstract - Email Verification API to check each address, and then updates the status in your database. Connect this with a CRM app like Salesforce to automatically keep your customer contact data clean and up-to-date. + +- **Real-Time Lead Verification for Marketing**: Integrate the Abstract - Email Verification API into a workflow that triggers when a lead submits their contact info via a marketing form. Verify the email in real-time and only pass the qualified leads to your sales team or email marketing software like Mailchimp for further engagement. diff --git a/components/abstract/abstract.app.mjs b/components/abstract/abstract.app.mjs new file mode 100644 index 0000000000000..25917066f8dc0 --- /dev/null +++ b/components/abstract/abstract.app.mjs @@ -0,0 +1,44 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "abstract", + propDefinitions: { + emailAddress: { + type: "string", + label: "Email Address", + description: "The email address to validate.", + }, + autoCorrect: { + type: "boolean", + label: "Auto Correct", + description: "You can choose to disable auto correct. By default, it is turned on.", + optional: true, + default: true, + }, + }, + methods: { + _baseUrl() { + return "https://emailvalidation.abstractapi.com"; + }, + _makeRequest({ + $, params, ...args + }) { + return axios($, { + ...args, + baseURL: this._baseUrl(), + params: { + ...params, + api_key: this.$auth.email_validation_api_key, + }, + }); + }, + async checkEmailDeliverability(args) { + return this._makeRequest({ + method: "GET", + url: "/v1", + ...args, + }); + }, + }, +}; diff --git a/components/abstract/actions/validate-email/validate-email.mjs b/components/abstract/actions/validate-email/validate-email.mjs new file mode 100644 index 0000000000000..495e97c9cdcc5 --- /dev/null +++ b/components/abstract/actions/validate-email/validate-email.mjs @@ -0,0 +1,36 @@ +import abstract from "../../abstract.app.mjs"; + +export default { + key: "abstract-validate-email", + name: "Validate Email", + description: "Check the deliverability of a specified email address. [See the documentation](https://docs.abstractapi.com/email-validation)", + version: "0.0.1", + type: "action", + props: { + abstract, + emailAddress: { + propDefinition: [ + abstract, + "emailAddress", + ], + }, + autoCorrect: { + propDefinition: [ + abstract, + "autoCorrect", + ], + }, + }, + async run({ $ }) { + const response = await this.abstract.checkEmailDeliverability({ + $, + params: { + email: this.emailAddress, + auto_correct: this.autoCorrect, + }, + }); + + $.export("$summary", `Checked email ${this.emailAddress} (${response.deliverability})`); + return response; + }, +}; diff --git a/components/abstract/package.json b/components/abstract/package.json new file mode 100644 index 0000000000000..0be3b7981ffe7 --- /dev/null +++ b/components/abstract/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/abstract", + "version": "0.1.0", + "description": "Pipedream Abstract - Email Verification API Components", + "main": "abstract.app.mjs", + "keywords": [ + "pipedream", + "abstract" + ], + "homepage": "https://pipedream.com/apps/abstract", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/abstract_ip_geo/README.md b/components/abstract_ip_geo/README.md index b385bc286dedd..8d638bc416646 100644 --- a/components/abstract_ip_geo/README.md +++ b/components/abstract_ip_geo/README.md @@ -1,12 +1,11 @@ # Overview -The Abstract - IP Geolocation API allows you to request detailed information -about a specific IP address, including country, region, city, latitude and -longitude, and more. This information can be used to build a variety of -applications and services, including: - -- IP address lookup and geolocation -- IP address blocking and filtering -- Geo-targeted content and advertising -- Location-based security and fraud detection -- And more! +The Abstract - IP Geolocation API allows you to identify the geographical location of IP addresses, providing data such as country, city, timezone, and latitude/longitude. This capability can enrich user data for analytics, customize content based on location, and enhance security by detecting unusual access patterns. When integrated with Pipedream, you can automate actions based on geolocation insights, triggering workflows that react to user locations in real-time. + +# Example Use Cases + +- **Content Personalization**: Use the IP Geolocation API to tailor website content dynamically in Pipedream workflows. For example, you could redirect users to a country-specific version of your site or display location-relevant promotions and announcements. + +- **Security Monitoring**: In Pipedream, create a workflow that uses geolocation to flag and notify you of suspicious login attempts. If a user's IP location differs significantly from their usual pattern, you could trigger an alert or require additional authentication steps. + +- **Analytics Enhancement**: Enhance your analytics pipeline in Pipedream by appending geolocation data to user activities. This can offer insights into your user base distribution and help in making data-driven decisions for marketing campaigns or product launches. diff --git a/components/abstract_ip_geo/package.json b/components/abstract_ip_geo/package.json new file mode 100644 index 0000000000000..e3733d1cf0516 --- /dev/null +++ b/components/abstract_ip_geo/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/abstract_ip_geo", + "version": "0.6.0", + "description": "Pipedream abstract_ip_geo Components", + "main": "abstract_ip_geo.app.mjs", + "keywords": [ + "pipedream", + "abstract_ip_geo" + ], + "homepage": "https://pipedream.com/apps/abstract_ip_geo", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/abuselpdb/README.md b/components/abuselpdb/README.md index 93498e0878a38..52b9a806383ea 100644 --- a/components/abuselpdb/README.md +++ b/components/abuselpdb/README.md @@ -1,13 +1,11 @@ # Overview -With the AbuselPDB API, you can build applications that can: +AbuseIPDB is a platform providing an API to report and check IP addresses to detect and prevent malicious activities. With the AbuseIPDB API on Pipedream, you can automate the process of monitoring, reporting, and acting upon incidents involving suspect IP addresses. This can mean auto-generating reports for suspicious traffic, cross-referencing with other security data sources, or triggering alerts in real-time for proactive defense measures. -- Query the AbuselPDB database for abusive IP addresses -- Report abusive IP addresses -- Block abusive IP addresses +# Example Use Cases -Here are some examples of what you can build with the AbuselPDB API: +- **Detect and Report Malicious Activity**: Create a workflow where server logs are monitored for failed login attempts. When the number of attempts from an IP exceeds a threshold, the IP is checked against AbuseIPDB and if it's known for malicious activity, automatically report it back to AbuseIPDB for community awareness. -- A web application that allows users to report abusive IP addresses -- A firewall that blocks abusive IP addresses -- A network monitoring tool that alerts you when an abusive IP address is detected +- **Automate Threat Intelligence Gathering**: Combine AbuseIPDB with other threat intelligence platforms like AlienVault OTX. Whenever a new threat is identified in OTX, check if any related IPs are already listed in AbuseIPDB, adding a new layer of insight to your security analysis and helping prioritize response based on known bad actors. + +- **Real-time Security Alerts**: Set up a Pipedream workflow that listens for webhook alerts from your firewall or intrusion detection system. When an alert contains an IP address, query AbuseIPDB to determine if the IP has a history of abuse. If so, automatically send a notification through Slack, email, or other communication platforms to alert the security team immediately. diff --git a/components/abyssale/README.md b/components/abyssale/README.md new file mode 100644 index 0000000000000..dbdf2cffbf731 --- /dev/null +++ b/components/abyssale/README.md @@ -0,0 +1,11 @@ +# Overview + +The Abyssale API enables automatic generation of banners and images tailored for various digital platforms. Through Pipedream, you can harness this capability within your workflows, dynamically producing visuals for marketing campaigns, social media, or personalized user content. Integrating Abyssale with Pipedream allows for the automation of image creation based on triggers or schedules, data enrichment from different sources, and connecting with various apps to streamline digital asset management and deployment. + +# Example Use Cases + +- **Dynamic Social Media Content Creation**: Trigger a Pipedream workflow with new content from a CMS like WordPress. Automatically pass the content to Abyssale to generate custom images for each article or post, and then publish them directly to your social media channels using apps like Twitter or Facebook. + +- **Personalized Email Marketing Campaigns**: When a new subscriber is added to your Mailchimp list, use Pipedream to trigger a custom image creation in Abyssale that includes the subscriber's name or other personalized elements. Then, email the image to the subscriber as part of a targeted email marketing campaign to boost engagement. + +- **E-commerce Product Ads Generation**: Connect your Shopify store to Pipedream and trigger a workflow whenever a new product is added. Use the Abyssale API to generate unique promotional banners for the product, and then distribute them across your advertising platforms like Google Ads or LinkedIn. diff --git a/components/accelo/README.md b/components/accelo/README.md index a03345c669aae..d3059f7795304 100644 --- a/components/accelo/README.md +++ b/components/accelo/README.md @@ -1,13 +1,11 @@ # Overview -Accelo's API lets you build a wide variety of integrations and automations to -streamline your workflows. Here are some examples of what you can build: - -- A tool to automatically generate invoices and send them to clients -- A system to track billable hours and generate reports on project - profitability -- A way to sync your company's contact list with your CRM -- An integration with your accounting software to automate billing -- A system to automatically generate quotes and send them to prospects -- A project management tool that integrates with Accelo to automatically create - and update project milestones, tasks, and events +Accelo's API provides a gateway to streamline professional service operations, offering endpoints that manage projects, tickets, sales, and more. With Pipedream, you can harness this API to automate routine tasks, sync data across platforms, and trigger custom workflows. Accelo's API lets you manipulate client data, automate service processes, and integrate with other tools for a seamless business ecosystem. + +# Example Use Cases + +- **Sales Lead to Project Conversion**: When a sales lead in Accelo is marked as 'won', automatically create a new project in your project management tool, such as Asana or Trello. This bridges the gap between sales and project execution by immediately translating successful deals into actionable tasks. + +- **Support Ticket Response Sync**: Sync support ticket updates from Accelo with a customer support platform like Zendesk. Whenever a ticket is updated in Accelo, an equivalent update or notification can be sent in Zendesk to keep all customer-facing teams in the loop. + +- **Time Tracking Integration**: Integrate Accelo's time tracking features with an accounting app like QuickBooks. When time entries are added in Accelo, corresponding invoices can be automatically created in QuickBooks, ensuring accurate and timely billing. diff --git a/components/accredible/README.md b/components/accredible/README.md new file mode 100644 index 0000000000000..e2222646c3a07 --- /dev/null +++ b/components/accredible/README.md @@ -0,0 +1,11 @@ +# Overview + +The Accredible API lets you automate the creation and management of digital certificates, badges, and blockchain credentials. Using Pipedream, you can connect the Accredible API to myriad services for streamlined workflow automation. Create digital certificates when a student completes a course, update credentials with new information, or share achievements across social platforms or via email. Pipedream's serverless platform enables you to integrate these actions with other apps, such as learning management systems, CRMs, and communication tools, without writing extensive code. + +# Example Use Cases + +- **Automate Certificate Issuance after Course Completion**: Trigger a Pipedream workflow when a student finishes a course in a Learning Management System like Teachable or Thinkific. The workflow would then call the Accredible API to issue a certificate and email it directly to the student. + +- **Bulk Update Credentials**: Use a webhook to listen for updates from your database or a Google Sheet. When data changes, the Pipedream workflow utilizes the Accredible API to update credentials in bulk, ensuring all information reflects the most current status. + +- **Share Credentials on Social Media**: After a certificate is created, trigger a Pipedream workflow that posts a congratulatory message along with the credential link on the recipient's LinkedIn profile through the LinkedIn API, broadening the reach and recognition of their achievement. diff --git a/components/accuranker/README.md b/components/accuranker/README.md index 240fc634a3b2f..46842b39b3c71 100644 --- a/components/accuranker/README.md +++ b/components/accuranker/README.md @@ -1,8 +1,11 @@ # Overview -With the Accuranker API, you can build applications that can: +Accuranker is an SEO tool that specializes in tracking and reporting on keyword rankings. Using the Accuranker API with Pipedream, you can automate checks on keyword positions, get insights on SERP data, and respond to ranking changes in real time. You can also compile performance reports, set up alerts for ranking changes, and integrate with other platforms to link SEO data with marketing, sales, or content strategies. -- Retrieve data about your website's rankings in search engines -- Monitor your website's ranking changes over time -- Analyze your competitors' websites -- Etc. +# Example Use Cases + +- **Daily Keyword Performance Digest**: Compile a daily report of keyword rankings and performance changes. Using Pipedream's Scheduled Triggers, the workflow can fetch data from Accuranker each day and send a summarized report via email using the SendGrid app or post it to a Slack channel where the SEO team can quickly review it. + +- **Alert System for Ranking Drops**: Set up an alert system that monitors significant ranking drops and notifies the team. If a tracked keyword falls below a certain threshold, Pipedream can trigger a notification to a designated Slack channel or send an SMS via Twilio. This allows for immediate action to investigate and address potential SEO issues. + +- **Competitor Ranking Analysis**: Track and compare your keyword rankings against your main competitors'. The workflow can retrieve competitor ranking data from Accuranker, process it within Pipedream, and then send the analysis to Google Sheets for further review and long-term tracking. This can be invaluable for strategic planning and for identifying content or keyword opportunities. diff --git a/components/accuranker/package.json b/components/accuranker/package.json new file mode 100644 index 0000000000000..7f53f61fc491c --- /dev/null +++ b/components/accuranker/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/accuranker", + "version": "0.6.0", + "description": "Pipedream accuranker Components", + "main": "accuranker.app.mjs", + "keywords": [ + "pipedream", + "accuranker" + ], + "homepage": "https://pipedream.com/apps/accuranker", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/acelle_mail/README.md b/components/acelle_mail/README.md new file mode 100644 index 0000000000000..86db079108dcf --- /dev/null +++ b/components/acelle_mail/README.md @@ -0,0 +1,11 @@ +# Overview + +The Acelle Mail API opens doors for you to engage with your audience through crafted email campaigns, subscriber management, and analytics directly through Pipedream. By leveraging Pipedream's capabilities, you can automate workflows that respond to new subscribers, trigger campaigns based on external events, and analyze the performance of your email marketing efforts, all in real-time. + +# Example Use Cases + +- **Automate Campaign Triggering Based on External Events**: Set up a workflow on Pipedream where, whenever a new sale is recorded in your e-commerce platform (like Shopify), it triggers an Acelle Mail campaign to the customer with a thank-you note and a discount code for future purchases. + +- **Synchronize Subscribers Across Platforms**: Develop a workflow where new subscribers from other platforms, like WordPress sign-ups or webinar attendees from Zoom, are automatically added to your Acelle Mail subscriber list, keeping your outreach efforts streamlined and consistent. + +- **Analytics to Slack for Real-Time Insights**: Design a workflow that fetches campaign analytics from Acelle Mail and posts a summary directly into a Slack channel. This can be set up to run after each campaign ends, offering immediate insights to your team for quick decision-making. diff --git a/components/acquire/README.md b/components/acquire/README.md index 5cdf35dafae44..61f850e3f2390 100644 --- a/components/acquire/README.md +++ b/components/acquire/README.md @@ -1,11 +1,11 @@ # Overview -Acquire is a powerful customer engagement platform that makes it easy to build -custom applications. With Acquire, you can create custom chatbots, live chat -interfaces, and more. +The Acquire API integrates customer support tools into your apps, enabling live chat, voice and video calls, co-browsing, and AI chatbots. With Pipedream, you can automate customer interaction workflows, sync support tickets with other platforms, or trigger actions based on customer behavior. Pipedream's serverless platform allows you to connect Acquire with hundreds of other apps for seamless automation, all with minimal coding. -Here are a few examples of what you can build with the Acquire API: +# Example Use Cases -- A chatbot to help customers with their purchase -- A live chat interface for customer support -- A custom application to help with customer engagement +- **Support Ticket Routing**: Automatically route new support tickets created in Acquire to a designated Slack channel. This keeps your support team immediately informed and helps ensure that no customer queries fall through the cracks. + +- **Customer Feedback Aggregation**: Collect feedback submitted through Acquire and send it to a Google Sheet for analysis. This workflow could trigger follow-up actions, like sending a thank you email from Gmail or a discount code for their next purchase, based on the feedback. + +- **Real-Time Alerts for High-Value Customers**: Set up a workflow that triggers when a high-value customer starts a chat on Acquire. Automatically send a notification to a VIP support agent via Microsoft Teams, along with the customer's purchase history from Salesforce, to provide personalized and immediate service. diff --git a/components/acquire/package.json b/components/acquire/package.json new file mode 100644 index 0000000000000..dcecf38afd95d --- /dev/null +++ b/components/acquire/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/acquire", + "version": "0.6.0", + "description": "Pipedream acquire Components", + "main": "acquire.app.mjs", + "keywords": [ + "pipedream", + "acquire" + ], + "homepage": "https://pipedream.com/apps/acquire", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/acronis_cyber_protect_cloud/acronis_cyber_protect_cloud.app.mjs b/components/acronis_cyber_protect_cloud/acronis_cyber_protect_cloud.app.mjs new file mode 100644 index 0000000000000..43191262b5d24 --- /dev/null +++ b/components/acronis_cyber_protect_cloud/acronis_cyber_protect_cloud.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "acronis_cyber_protect_cloud", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/acronis_cyber_protect_cloud/package.json b/components/acronis_cyber_protect_cloud/package.json new file mode 100644 index 0000000000000..fbeb25172bc59 --- /dev/null +++ b/components/acronis_cyber_protect_cloud/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/acronis_cyber_protect_cloud", + "version": "0.0.1", + "description": "Pipedream Acronis Cyber Protect Cloud Components", + "main": "acronis_cyber_protect_cloud.app.mjs", + "keywords": [ + "pipedream", + "acronis_cyber_protect_cloud" + ], + "homepage": "https://pipedream.com/apps/acronis_cyber_protect_cloud", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/act_365/README.md b/components/act_365/README.md index 18e68409810b0..33867a576b5af 100644 --- a/components/act_365/README.md +++ b/components/act_365/README.md @@ -1,8 +1,11 @@ # Overview -With the Act! 365 API, you can build applications that: +The Act! 365 API is a potent tool for managing customer relationships by interacting programmatically with the Act! 365 CRM platform. With Pipedream, you can harness this API to automate interactions with customer data, streamline sales processes, and enhance customer engagement through personalized communications. The API allows you to create, retrieve, update, and delete records such as contacts, companies, opportunities, and activities, paving the way for a vast array of automated workflows that connect Act! 365 with other apps and services to fuel business growth, save time, and reduce manual toil. -- Synchronize data between Act! and other systems -- Automate repetitive tasks -- Extend the functionality of Act! with customizations -- Create reports and dashboards +# Example Use Cases + +- **Contact Syncing Across Platforms**: Automatically sync new contacts from Act! 365 to email marketing platforms like Mailchimp. When a new contact is added in Act! 365, trigger a workflow in Pipedream to add that contact to a designated Mailchimp audience, ensuring your marketing campaigns reach the latest leads without manual intervention. + +- **Sales Pipeline Automation**: Enhance sales efficiency by creating a workflow that monitors deal stages in Act! 365. When an opportunity moves to a closing stage, the workflow could prompt Pipedream to send a Slack notification to the sales team, schedule a follow-up task, and prepare a draft contract in a document management system like Google Docs. + +- **Customer Support Ticket Creation**: Improve customer service response times with a workflow that detects specific customer activities or issues flagged in Act! 365. Trigger the creation of a support ticket in a helpdesk system like Zendesk and assign it to the appropriate team, ensuring prompt and organized customer support follow-ups. diff --git a/components/act_365/package.json b/components/act_365/package.json new file mode 100644 index 0000000000000..e45f969c65ea7 --- /dev/null +++ b/components/act_365/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/act_365", + "version": "0.6.0", + "description": "Pipedream act_365 Components", + "main": "act_365.app.mjs", + "keywords": [ + "pipedream", + "act_365" + ], + "homepage": "https://pipedream.com/apps/act_365", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/action_builder/README.md b/components/action_builder/README.md index 4f6dc17f17841..beaf062ac8fb0 100644 --- a/components/action_builder/README.md +++ b/components/action_builder/README.md @@ -1,8 +1,11 @@ # Overview -With Action Builder, you can easily create simple or complex automation -workflows to automate any number of tasks. You can use Action Builder to easily -create and manage actions, conditions, and triggers for your apps and services. -Action Builder makes it easy to get started with automating your tasks, and -provides a wide array of options for configuring and customizing your -workflows. +The Action Builder API on Pipedream paves the way for crafting bespoke serverless workflows tailored to specific automation needs. Essentially, it's a playground for developers to create custom actions, integrate with a myriad of services, and automate tasks that were once manual and time-consuming. The API allows you to design, execute, and orchestrate actions that trigger on specific events or schedules, manipulate data with inbuilt functions, and connect disparate systems with ease, fostering a seamless data flow and enhanced productivity. + +# Example Use Cases + +- **Automated Content Distribution**: Use Action Builder to create a custom action that pushes new blog posts from a headless CMS to social media platforms. When a new post is published, the workflow triggers and disseminates the content across Twitter, LinkedIn, and Facebook, ensuring timely updates to your audience. + +- **Customer Support Ticket Routing**: Leverage Action Builder to sort and assign incoming support tickets from a platform like Zendesk to the appropriate teams. The workflow could analyze the content of the ticket, categorize it (e.g., technical, billing, general inquiry), and then route it to the correct queue, streamlining the support process. + +- **Data Backup and Sync**: Design a custom action to automate the backup of critical data from your cloud storage solution to an alternative backup location, like Google Drive or Dropbox. The workflow could be set to run at a predetermined interval, ensuring regular snapshots of your data are saved, reducing the risk of data loss. diff --git a/components/action_network/README.md b/components/action_network/README.md index 80129d4f81fcc..3b017c4c9e0b7 100644 --- a/components/action_network/README.md +++ b/components/action_network/README.md @@ -1,10 +1,9 @@ # Overview -Action Network is a powerful tool that allows you to do a lot with their API. -Here are some examples of what you can do: +The Action Network API lets you tap into a powerful tool for organizing and mobilizing activists. With this API, you can automate the management of events, petitions, fundraising, and email campaigns, syncing data seamlessly to other platforms. It's a perfect fit for political campaigns, non-profits, and grassroots movements looking to streamline their digital efforts and boost engagement through smart automations. -- Build a simple sign up form -- Create an event -- Create a fundraiser -- Send emails to your list -- Get information about your contacts +# Example Use Cases + +- **Sync Supporters to CRM**: Automatically update supporter information in a CRM like Salesforce whenever someone signs a petition or donates. This workflow ensures your supporter records are always up-to-date, allowing for tailored communication strategies. +- **Event Management Automation**: When someone RSVPs to an event, trigger a sequence of actions such as sending a personalized thank you email, adding them to a reminder sequence, and creating a calendar event in Google Calendar to help organizers keep track of attendance. +- **Real-time Advocacy Alerts**: Monitor petition signings in real-time, and when a threshold is reached, trigger a series of social media posts on platforms like Twitter or Facebook to amplify the message and encourage more signatures. diff --git a/components/actitime/actions/create-customer/create-customer.mjs b/components/actitime/actions/create-customer/create-customer.mjs new file mode 100644 index 0000000000000..fb8f84652f6d7 --- /dev/null +++ b/components/actitime/actions/create-customer/create-customer.mjs @@ -0,0 +1,49 @@ +import app from "../../actitime.app.mjs"; + +export default { + key: "actitime-create-customer", + name: "Create Customer", + description: "Creates a new customer. [See the documentation](https://online.actitime.com/pipedream/api/v1/swagger).", + version: "0.0.1", + type: "action", + props: { + app, + name: { + type: "string", + label: "Name", + description: "The name of the customer.", + }, + description: { + type: "string", + label: "Description", + description: "Details about the customer.", + optional: true, + }, + }, + methods: { + createCustomer(args = {}) { + return this.app.post({ + path: "/customers", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createCustomer, + name, + description, + } = this; + + const response = await createCustomer({ + $, + data: { + name, + description, + }, + }); + + $.export("$summary", `Successfully created customer with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/actitime/actions/create-task/create-task.mjs b/components/actitime/actions/create-task/create-task.mjs new file mode 100644 index 0000000000000..5c5fb34974ac7 --- /dev/null +++ b/components/actitime/actions/create-task/create-task.mjs @@ -0,0 +1,107 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../actitime.app.mjs"; + +export default { + key: "actitime-create-task", + name: "Create Task", + description: "Creates a new task. [See the documentation](https://online.actitime.com/pipedream/api/v1/swagger).", + version: "0.0.1", + type: "action", + props: { + app, + projectId: { + propDefinition: [ + app, + "projectId", + ], + }, + name: { + type: "string", + label: "Name", + description: "The name of the task.", + }, + description: { + type: "string", + label: "Description", + description: "Details about the task.", + optional: true, + }, + typeOfWorkId: { + propDefinition: [ + app, + "typeOfWorkId", + ], + }, + deadline: { + type: "string", + label: "Deadline", + description: "The deadline for the task. Eg. `2024-12-31`.", + optional: true, + }, + estimatedTime: { + type: "integer", + label: "Estimated Time", + description: "The estimated time for the task in minutes.", + optional: true, + }, + status: { + type: "string", + label: "Status", + description: "The status of the task. This field is mutually exclusive with **Workflow Status ID**.", + optional: true, + options: [ + "open", + "completed", + ], + }, + workflowStatusId: { + description: "The ID of the workflow status. This field is mutually exclusive with **Status**.", + propDefinition: [ + app, + "workflowStatusId", + ], + }, + }, + methods: { + createTask(args = {}) { + return this.app.post({ + path: "/tasks", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createTask, + name, + description, + status, + workflowStatusId, + typeOfWorkId, + deadline, + estimatedTime, + projectId, + } = this; + + if (status && workflowStatusId) { + throw new ConfigurationError("The **Status** and **Workflow Status ID** fields are mutually exclusive. Please provide only one of them."); + } + + const response = await createTask({ + $, + data: { + name, + description, + status, + workflowStatusId, + typeOfWorkId, + deadline, + estimatedTime, + projectId, + }, + }); + + $.export("$summary", `Successfully created task with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/actitime/actions/modify-leave-time/modify-leave-time.mjs b/components/actitime/actions/modify-leave-time/modify-leave-time.mjs new file mode 100644 index 0000000000000..e9c00d891cc9b --- /dev/null +++ b/components/actitime/actions/modify-leave-time/modify-leave-time.mjs @@ -0,0 +1,67 @@ +import app from "../../actitime.app.mjs"; + +export default { + key: "actitime-modify-leave-time", + name: "Modify Leave Time", + description: "Changes the existing leave time record with a given value in actiTIME. [See the documentation](https://online.actitime.com/pipedream/api/v1/swagger).", + version: "0.0.1", + type: "action", + props: { + app, + userId: { + propDefinition: [ + app, + "userId", + ], + }, + date: { + type: "string", + label: "Date", + description: "The date of the leave record. Date in following format: `YYYY-MM-DD` OR `today`.", + }, + leaveTypeId: { + propDefinition: [ + app, + "leaveTypeId", + ], + }, + leaveTime: { + type: "integer", + label: "Leave Time", + description: "The leave time in minutes.", + }, + }, + methods: { + updateLeaveTime({ + userId, date, leaveTypeId, ...args + } = {}) { + return this.app.patch({ + path: `/leavetime/${userId}/${date}/${leaveTypeId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + updateLeaveTime, + userId, + date, + leaveTypeId, + leaveTime, + } = this; + + const response = await updateLeaveTime({ + $, + userId, + date, + leaveTypeId, + data: { + leaveTime, + }, + }); + + $.export("$summary", "Successfully modified leave time record."); + + return response; + }, +}; diff --git a/components/actitime/actitime.app.mjs b/components/actitime/actitime.app.mjs new file mode 100644 index 0000000000000..1168891511e2f --- /dev/null +++ b/components/actitime/actitime.app.mjs @@ -0,0 +1,213 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "actitime", + propDefinitions: { + userId: { + type: "string", + label: "User ID", + description: "The ID of the user whose working time is updated.", + async options({ prevContext: { nextOffset } }) { + const { + items, + offset, + } = await this.listUsers({ + limit: constants.DEFAULT_LIMIT, + offset: nextOffset, + }); + return { + context: { + nextOffset: offset, + }, + options: items.map(({ + id: value, + username: label, + }) => ({ + value, + label, + })), + }; + }, + }, + workflowStatusId: { + type: "string", + label: "Workflow Status ID", + description: "The ID of the workflow status.", + optional: true, + async options({ prevContext: { nextOffset } }) { + const { + items, + offset, + } = await this.listWorkflowStatuses({ + limit: constants.DEFAULT_LIMIT, + offset: nextOffset, + }); + return { + context: { + nextOffset: offset, + }, + options: items.map(({ + id: value, + name: label, + }) => ({ + value, + label, + })), + }; + }, + }, + typeOfWorkId: { + type: "string", + label: "Type Of Work ID", + description: "The ID of the type of work.", + optional: true, + async options({ prevContext: { nextOffset } }) { + const { + items, + offset, + } = await this.listTypesOfWork({ + limit: constants.DEFAULT_LIMIT, + offset: nextOffset, + }); + return { + context: { + nextOffset: offset, + }, + options: items.map(({ + id: value, + name: label, + }) => ({ + value, + label, + })), + }; + }, + }, + projectId: { + type: "string", + label: "Project ID", + description: "The ID of the project.", + async options({ prevContext: { nextOffset } }) { + const { + items, + offset, + } = await this.listProjects({ + limit: constants.DEFAULT_LIMIT, + offset: nextOffset, + }); + return { + context: { + nextOffset: offset, + }, + options: items.map(({ + id: value, + name: label, + }) => ({ + value, + label, + })), + }; + }, + }, + leaveTypeId: { + type: "string", + label: "Leave Type ID", + description: "The ID of the leave type.", + async options({ prevContext: { nextOffset } }) { + const { + items, + offset, + } = await this.leaveTypes({ + limit: constants.DEFAULT_LIMIT, + offset: nextOffset, + }); + return { + context: { + nextOffset: offset, + }, + options: items.map(({ + id: value, + name: label, + }) => ({ + value, + label, + })), + }; + }, + }, + }, + methods: { + getUrl(path) { + return `${this.$auth.url}${constants.VERSION_PATH}${path}`; + }, + getAuth() { + const { + email: username, + password, + } = this.$auth; + return { + username, + password, + }; + }, + _makeRequest({ + $ = this, path, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + auth: this.getAuth(), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + patch(args = {}) { + return this._makeRequest({ + method: "PATCH", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + listUsers(args = {}) { + return this._makeRequest({ + path: "/users", + ...args, + }); + }, + listWorkflowStatuses(args = {}) { + return this._makeRequest({ + path: "/workflowStatuses", + ...args, + }); + }, + listTypesOfWork(args = {}) { + return this._makeRequest({ + path: "/typesOfWork", + ...args, + }); + }, + listProjects(args = {}) { + return this._makeRequest({ + path: "/projects", + ...args, + }); + }, + leaveTypes(args = {}) { + return this._makeRequest({ + path: "/leaveTypes", + ...args, + }); + }, + }, +}; diff --git a/components/actitime/common/constants.mjs b/components/actitime/common/constants.mjs new file mode 100644 index 0000000000000..8a45488c94c8e --- /dev/null +++ b/components/actitime/common/constants.mjs @@ -0,0 +1,11 @@ +const VERSION_PATH = "/api/v1"; +const DEFAULT_LIMIT = 20; +const DEFAULT_MAX = 100; +const WEBHOOK_ID = "webhookId"; + +export default { + VERSION_PATH, + DEFAULT_LIMIT, + DEFAULT_MAX, + WEBHOOK_ID, +}; diff --git a/components/actitime/package.json b/components/actitime/package.json new file mode 100644 index 0000000000000..75c9e6666539e --- /dev/null +++ b/components/actitime/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/actitime", + "version": "0.1.0", + "description": "Pipedream actiTIME Components", + "main": "actitime.app.mjs", + "keywords": [ + "pipedream", + "actitime" + ], + "homepage": "https://pipedream.com/apps/actitime", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.1" + } +} diff --git a/components/actitime/sources/common/events.mjs b/components/actitime/sources/common/events.mjs new file mode 100644 index 0000000000000..507f7e1911c76 --- /dev/null +++ b/components/actitime/sources/common/events.mjs @@ -0,0 +1,39 @@ +export default { + ALL: "*", + CREATE: "create", + UPDATE: "update", + MOVE: "move", + DELETE: "delete", + TASK: "task", + TASK_CREATE: "task.create", + TASK_UPDATE: "task.update", + TASK_MOVE: "task.move", + TASK_DELETE: "task.delete", + PROJECT: "project", + PROJECT_CREATE: "project.create", + PROJECT_UPDATE: "project.update", + PROJECT_MOVE: "project.move", + PROJECT_DELETE: "project.delete", + CUSTOMER: "customer", + CUSTOMER_CREATE: "customer.create", + CUSTOMER_UPDATE: "customer.update", + CUSTOMER_DELETE: "customer.delete", + DEPARTMENT: "department", + DEPARTMENT_CREATE: "department.create", + DEPARTMENT_UPDATE: "department.update", + DEPARTMENT_DELETE: "department.delete", + USER: "user", + USER_CREATE: "user.create", + USER_UPDATE: "user.update", + USER_DELETE: "user.delete", + TIMEZONEGROUP: "timeZoneGroup", + TIMEZONEGROUP_CREATE: "timeZoneGroup.create", + TIMEZONEGROUP_UPDATE: "timeZoneGroup.update", + TIMEZONEGROUP_DELETE: "timeZoneGroup.delete", + LEAVETIME: "leaveTime", + LEAVETIME_UPDATE: "leaveTime.update", + TIME_TRACK: "timetrack", + TIME_TRACK_UPDATE: "timetrack.update", + TIME_TRACK_APPROVAL_STATUS: "timetrackApprovalStatus", + TIME_TRACK_APPROVAL_STATUS_UPDATE: "timetrackApprovalStatus.update", +}; diff --git a/components/actitime/sources/common/webhook.mjs b/components/actitime/sources/common/webhook.mjs new file mode 100644 index 0000000000000..bc4db89002abc --- /dev/null +++ b/components/actitime/sources/common/webhook.mjs @@ -0,0 +1,100 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../actitime.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + }, + hooks: { + async activate() { + const { + http: { endpoint: targetUrl }, + createWebhook, + getEventName, + setWebhookId, + getFilter, + } = this; + + const response = + await createWebhook({ + data: { + target_url: targetUrl, + event: getEventName(), + filter: getFilter(), + enabled: true, + }, + }); + + setWebhookId(response.id); + }, + async deactivate() { + const { + deleteWebhook, + getWebhookId, + } = this; + + const webhookId = getWebhookId(); + if (webhookId) { + await deleteWebhook({ + webhookId, + }); + } + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + setWebhookId(value) { + this.db.set(constants.WEBHOOK_ID, value); + }, + getWebhookId() { + return this.db.get(constants.WEBHOOK_ID); + }, + getEventName() { + throw new ConfigurationError("getEventName is not implemented"); + }, + getFilter() { + return; + }, + processResource(resource) { + this.$emit(resource, this.generateMeta(resource)); + }, + createWebhook(args = {}) { + return this.app.post({ + debug: true, + path: "/hooks", + ...args, + }); + }, + deleteWebhook({ + webhookId, ...args + } = {}) { + return this.app.delete({ + debug: true, + path: `/hooks/${webhookId}`, + ...args, + }); + }, + }, + async run({ body }) { + const { + http, + processResource, + } = this; + + Array.isArray(body) + ? body.forEach(processResource) + : processResource(body); + + http.respond({ + status: 200, + }); + }, +}; diff --git a/components/actitime/sources/new-customer-instant/new-customer-instant.mjs b/components/actitime/sources/new-customer-instant/new-customer-instant.mjs new file mode 100644 index 0000000000000..c15c670841b34 --- /dev/null +++ b/components/actitime/sources/new-customer-instant/new-customer-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "actitime-new-customer-instant", + name: "New Customer (Instant)", + description: "Emit new event when a new customer is created. [See the documentation](https://www.actitime.com/api-documentation/rest-hooks).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventName() { + return events.CUSTOMER_CREATE; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Customer: ${resource.name}`, + ts: resource.created, + }; + }, + }, + sampleEmit, +}; diff --git a/components/actitime/sources/new-customer-instant/test-event.mjs b/components/actitime/sources/new-customer-instant/test-event.mjs new file mode 100644 index 0000000000000..5a9da826053f6 --- /dev/null +++ b/components/actitime/sources/new-customer-instant/test-event.mjs @@ -0,0 +1,9 @@ +export default { + "event.name": "customer.create", + "id": 12, + "name": "Test 11", + "archived": false, + "created": 1725295489000, + "url": "https://online.actitime.com/pipedream1/tasks/tasklist.do?customerId=12", + "description": "Test 11 Desc" +}; diff --git a/components/actitime/sources/new-task-instant/new-task-instant.mjs b/components/actitime/sources/new-task-instant/new-task-instant.mjs new file mode 100644 index 0000000000000..9800f3dd954ac --- /dev/null +++ b/components/actitime/sources/new-task-instant/new-task-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "actitime-new-task-instant", + name: "New Task (Instant)", + description: "Emit new event when a new task is created. [See the documentation](https://www.actitime.com/api-documentation/rest-hooks).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventName() { + return events.TASK_CREATE; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Task: ${resource.name}`, + ts: resource.created, + }; + }, + }, + sampleEmit, +}; diff --git a/components/actitime/sources/new-task-instant/test-event.mjs b/components/actitime/sources/new-task-instant/test-event.mjs new file mode 100644 index 0000000000000..0419b50bf6516 --- /dev/null +++ b/components/actitime/sources/new-task-instant/test-event.mjs @@ -0,0 +1,19 @@ +export default { + "event.name": "task.create", + "id": 143, + "name": "Test 10", + "description": null, + "created": 1725295638000, + "status": "open", + "workflowStatusId": 1, + "typeOfWorkId": 1, + "url": "https://online.actitime.com/pipedream1/tasks/tasklist.do?taskId=143", + "projectName": "Spaceship building", + "customerName": "Big Bang Company", + "workflowStatusName": "New", + "typeOfWorkName": "engineering", + "deadline": null, + "estimatedTime": null, + "customerId": 6, + "projectId": 13 +}; diff --git a/components/actitime/sources/updated-time-track-instant/test-event.mjs b/components/actitime/sources/updated-time-track-instant/test-event.mjs new file mode 100644 index 0000000000000..b1f9905cf4e5b --- /dev/null +++ b/components/actitime/sources/updated-time-track-instant/test-event.mjs @@ -0,0 +1,11 @@ +export default { + "event.name": "timetrack.update", + "userId": 1, + "taskId": 136, + "taskName": "Meetings", + "time": 15, + "comment": "Test 1", + "date": "2024-09-02", + "projectId": 23, + "customerId": 9 +}; diff --git a/components/actitime/sources/updated-time-track-instant/updated-time-track-instant.mjs b/components/actitime/sources/updated-time-track-instant/updated-time-track-instant.mjs new file mode 100644 index 0000000000000..a535b6ae80019 --- /dev/null +++ b/components/actitime/sources/updated-time-track-instant/updated-time-track-instant.mjs @@ -0,0 +1,28 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "actitime-updated-time-track-instant", + name: "Updated Time Track (Instant)", + description: "Emit new event when the user's working time is updated. [See the documentation](https://www.actitime.com/api-documentation/rest-hooks).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventName() { + return events.TIME_TRACK_UPDATE; + }, + generateMeta(resource) { + const ts = Date.now(); + return { + id: `${resource.id}-${ts}`, + summary: `Time Track Updated: ${resource.taskName}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/active_trail/README.md b/components/active_trail/README.md index e8a0bd82d3127..baaabf699059f 100644 --- a/components/active_trail/README.md +++ b/components/active_trail/README.md @@ -1,10 +1,11 @@ # Overview -ActiveTrail's API lets you integrate ActiveTrail's powerful email marketing and -automation features into your own app or service. With the API, you can: - -- Create and manage ActiveTrail contacts -- Add and remove contacts from ActiveTrail contact lists -- Create and manage ActiveTrail campaigns -- Send and track ActiveTrail email campaigns -- Access ActiveTrail campaign reports and analytics +ActiveTrail’s API allows for the automation of email marketing campaigns, contact management, and event-triggered communications. By integrating ActiveTrail with Pipedream, you can create workflows that react to specific triggers, such as new subscriber sign-ups, and perform actions like updating contact lists or sending personalized emails. This level of automation can save time, improve customer engagement, and ensure timely communications. + +# Example Use Cases + +- **Automate Welcome Email Sequences**: When a new user signs up via your website's form, which is connected to ActiveTrail, use Pipedream to send a series of welcome emails. Start with an immediate thank-you email, followed by targeted information over the coming days. + +- **Sync Contacts with CRM**: Automatically update your CRM when a new contact is added to an ActiveTrail list. This ensures that sales teams have the latest information and can follow up with leads promptly. + +- **Event-Driven Email Campaigns**: Trigger an email campaign in ActiveTrail based on customer actions tracked in your app. For example, if a user abandons their shopping cart, Pipedream can initiate a targeted email sequence to encourage completion of the purchase. diff --git a/components/active_trail/package.json b/components/active_trail/package.json new file mode 100644 index 0000000000000..5230f18d084a8 --- /dev/null +++ b/components/active_trail/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/active_trail", + "version": "0.6.0", + "description": "Pipedream active_trail Components", + "main": "active_trail.app.mjs", + "keywords": [ + "pipedream", + "active_trail" + ], + "homepage": "https://pipedream.com/apps/active_trail", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/activecalculator/activecalculator.app.mjs b/components/activecalculator/activecalculator.app.mjs new file mode 100644 index 0000000000000..c049e35ebde5e --- /dev/null +++ b/components/activecalculator/activecalculator.app.mjs @@ -0,0 +1,67 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "activecalculator", + propDefinitions: { + calculatorIds: { + type: "string[]", + label: "Calculator Ids", + description: "A list of calculator's Ids.", + async options({ page }) { + const { data } = await this.listCalculators({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://app.activecalculator.com/api/v1"; + }, + _headers() { + return { + "x-ac-api-key": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listCalculators(opts = {}) { + return this._makeRequest({ + path: "/calculators", + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + }); + }, + }, +}; diff --git a/components/activecalculator/common/constants.mjs b/components/activecalculator/common/constants.mjs new file mode 100644 index 0000000000000..ea830c15a04cb --- /dev/null +++ b/components/activecalculator/common/constants.mjs @@ -0,0 +1 @@ +export const LIMIT = 100; diff --git a/components/activecalculator/common/utils.mjs b/components/activecalculator/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/activecalculator/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/activecalculator/package.json b/components/activecalculator/package.json new file mode 100644 index 0000000000000..10f7119e2429e --- /dev/null +++ b/components/activecalculator/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/activecalculator", + "version": "0.1.0", + "description": "Pipedream ActiveCalculator Components", + "main": "activecalculator.app.mjs", + "keywords": [ + "pipedream", + "activecalculator" + ], + "homepage": "https://pipedream.com/apps/activecalculator", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/activecalculator/sources/new-submission-instant/new-submission-instant.mjs b/components/activecalculator/sources/new-submission-instant/new-submission-instant.mjs new file mode 100644 index 0000000000000..218cf3d85498c --- /dev/null +++ b/components/activecalculator/sources/new-submission-instant/new-submission-instant.mjs @@ -0,0 +1,62 @@ +import activecalculator from "../../activecalculator.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "activecalculator-new-submission-instant", + name: "New Submission (Instant)", + description: "Emit new event when there's a new submission.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + activecalculator, + http: { + type: "$.interface.http", + customResponse: false, + }, + db: "$.service.db", + name: { + type: "string", + label: "Name", + description: "The name of the webhook.", + optional: true, + }, + calculatorIds: { + propDefinition: [ + activecalculator, + "calculatorIds", + ], + }, + }, + hooks: { + async activate() { + const { data } = await this.activecalculator.createWebhook({ + data: { + url: this.http.endpoint, + name: this.name, + source: "pipedream", + triggers: [ + "newSubmission", + ], + calculators: parseObject(this.calculatorIds), + }, + }); + this.db.set("webhookId", data.id); + }, + async deactivate() { + const webhookId = this.db.get("webhookId"); + if (webhookId) { + await this.activecalculator.deleteWebhook(webhookId); + } + }, + }, + async run({ body }) { + this.$emit(body, { + id: body.data.id, + summary: `New Submission: ${body.data.id}`, + ts: Date.parse(body.data.createdAt), + }); + }, + sampleEmit, +}; diff --git a/components/activecalculator/sources/new-submission-instant/test-event.mjs b/components/activecalculator/sources/new-submission-instant/test-event.mjs new file mode 100644 index 0000000000000..ea49d9c85f961 --- /dev/null +++ b/components/activecalculator/sources/new-submission-instant/test-event.mjs @@ -0,0 +1,31 @@ +export default { + "webhookId": "cm02xzoe0cdm287pcs", + "event": "newSubmission", + "data": { + "id": "cm02xzoe0cdm287pcs", + "createdAt": "2024-08-20T21:34:07.076Z", + "updatedAt": "2024-08-20T21:34:07.076Z", + "calculatorId": "cm02xzoe0cdm287pcs", + "finished": true, + "data": { + "UHvky": "email@test.com", + "tWKDQ": { + "id": "cm02xzoe0cdm287pcs", + "value": 1, + "label": "Basic" + }, + "cm02xzoe0cdm287pcs": 10, + "cm02xzoe0cdm287pcs": 100, + "cm02xzoe0cdm287pcs": 10 + }, + "meta": { + "source": "", + "url": "https://my.activecalculator.com/cm02xzoe0cdm287pcs", + "userAgent": { + "browser": "Chrome", + "os": "Windows" + }, + "country": "BR" + } + } +} \ No newline at end of file diff --git a/components/activecampaign/README.md b/components/activecampaign/README.md index a7f27e39c6383..298b0019614b9 100644 --- a/components/activecampaign/README.md +++ b/components/activecampaign/README.md @@ -1,12 +1,11 @@ # Overview -With ActiveCampaign's API, you can build a variety of integrations and -applications to help streamline your business. Here are just a few examples: - -- A contact importer that pulls in your contacts from another CRM or email - service -- A webhook that automatically updates your ActiveCampaign lists when someone - subscribes or unsubscribes from your website -- An integration that allows you to send SMS messages through ActiveCampaign -- A geolocation tool that tracks where your contacts are located -- And much more! +ActiveCampaign's API provides a powerful interface to automate your email marketing, sales automation, and customer relationship management. By leveraging ActiveCampaign with Pipedream, you can create custom, automated workflows that react to events in real-time, sync data between various platforms, and trigger targeted communication with your users. With the ability to perform actions such as adding contacts, updating deals, and sending out campaigns, the API offers a wide range of possibilities for enhancing your marketing strategies. + +# Example Use Cases + +- **New Subscriber Welcome Sequence**: Trigger a personalized welcome email sequence in ActiveCampaign when a new subscriber is added to your Shopify store, ensuring timely engagement with your brand. + +- **Support Ticket Follow-Up Campaign**: When a support ticket in Zendesk is marked as resolved, automatically enroll the customer in a follow-up campaign in ActiveCampaign to gather feedback and ensure customer satisfaction. + +- **Event Attendee Engagement**: After a contact registers for an event via Eventbrite, use ActiveCampaign to send pre-event information, reminders, and post-event follow-ups, nurturing the relationship and increasing retention. diff --git a/components/activecampaign/actions/create-or-update-contact/create-or-update-contact.mjs b/components/activecampaign/actions/create-or-update-contact/create-or-update-contact.mjs index 76d79654413e4..9265a9c47d380 100644 --- a/components/activecampaign/actions/create-or-update-contact/create-or-update-contact.mjs +++ b/components/activecampaign/actions/create-or-update-contact/create-or-update-contact.mjs @@ -5,7 +5,7 @@ export default { key: "activecampaign-create-or-update-contact", name: "Create or Update Contact", description: "Creates a new contact or updates an existing contact. [See the documentation](https://developers.activecampaign.com/reference/sync-a-contacts-data).", - version: "0.2.5", + version: "0.2.6", type: "action", async run({ $ }) { const response = diff --git a/components/activecampaign/package.json b/components/activecampaign/package.json index 195d5ebd31a0f..50f4bcb083de8 100644 --- a/components/activecampaign/package.json +++ b/components/activecampaign/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/activecampaign", - "version": "0.4.4", + "version": "0.4.5", "description": "Pipedream Activecampaing Components", "main": "activecampaign.app.mjs", "keywords": [ @@ -14,7 +14,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.6.0", + "@pipedream/platform": "^3.0.3", "inflection": "^3.0.0" } } diff --git a/components/acuity_scheduling/README.md b/components/acuity_scheduling/README.md index 3d3786c57c799..386ac1eb23081 100644 --- a/components/acuity_scheduling/README.md +++ b/components/acuity_scheduling/README.md @@ -1,19 +1,11 @@ # Overview -Acuity Scheduling is a cloud-based appointment scheduling software that enables -businesses to manage their appointments and bookings online. The Acuity -Scheduling API allows developers to access and integrate the functionality of -Acuity Scheduling with other applications. +Acuity Scheduling API allows you to tap into the functionality of Acuity Scheduling, a cloud-based appointment scheduling software. With this API on Pipedream, you can automate appointment creation, modifications, and cancellations, as well as sync customer data across your tech stack. The API lets you fetch detailed information about schedules, available time slots, and calendar events. Create dynamic, real-time integrations with CRMs, email marketing platforms, or payment gateways to streamline your scheduling and business processes. -With the Acuity Scheduling API, you can: +# Example Use Cases -- Book appointments -- Cancel appointments -- Reschedule appointments -- Get appointment details -- Get list of appointments -- Get list of available time slots -- Get list of Cancelled appointments -- Get list of Upcoming appointments -- Get list of Past appointments -- And more! +- **Appointment Confirmation Emails**: Automate the process of sending customized confirmation emails via SendGrid or another email service when a new appointment is scheduled in Acuity. You can include appointment details, and even a calendar invite, ensuring clients have all the information they need. + +- **CRM Contact Update**: When a new client books an appointment, automatically create or update their contact details in a CRM like Salesforce. This workflow ensures that client information is always current and can trigger follow-up tasks or lead nurturing processes within the CRM. + +- **Invoice Generation**: After an appointment is completed, trigger an invoice creation in accounting software such as QuickBooks. This workflow can include client details, services rendered, and pricing, and send the invoice straight to the client, streamlining the billing process. diff --git a/components/acuity_scheduling/actions/add-blocked-off-time/add-blocked-off-time.mjs b/components/acuity_scheduling/actions/add-blocked-off-time/add-blocked-off-time.mjs new file mode 100644 index 0000000000000..c50c594c51151 --- /dev/null +++ b/components/acuity_scheduling/actions/add-blocked-off-time/add-blocked-off-time.mjs @@ -0,0 +1,50 @@ +import acuityScheduling from "../../acuity_scheduling.app.mjs"; + +export default { + key: "acuity_scheduling-add-blocked-off-time", + name: "Add Blocked Off Time", + description: "Blocks a specific time slot on your schedule to prevent the scheduling of any appointments during this particular time range. [See the documentation](https://developers.acuityscheduling.com/reference/post-blocks)", + version: "0.0.1", + type: "action", + props: { + acuityScheduling, + startTime: { + propDefinition: [ + acuityScheduling, + "startTime", + ], + }, + endTime: { + propDefinition: [ + acuityScheduling, + "endTime", + ], + }, + calendarId: { + propDefinition: [ + acuityScheduling, + "calendarId", + ], + }, + notes: { + type: "string", + label: "Notes", + description: "Any notes to include for the blocked off time", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.acuityScheduling.blockTime({ + $, + data: { + start: this.startTime, + end: this.endTime, + calendarID: this.calendarId, + notes: this.notes, + }, + }); + + $.export("$summary", `Successfully blocked off time from ${this.startTime} to ${this.endTime}`); + return response; + }, +}; diff --git a/components/acuity_scheduling/actions/find-appointments-by-client-info/find-appointments-by-client-info.mjs b/components/acuity_scheduling/actions/find-appointments-by-client-info/find-appointments-by-client-info.mjs new file mode 100644 index 0000000000000..ff65cd5c29a42 --- /dev/null +++ b/components/acuity_scheduling/actions/find-appointments-by-client-info/find-appointments-by-client-info.mjs @@ -0,0 +1,88 @@ +import acuityScheduling from "../../acuity_scheduling.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "acuity_scheduling-find-appointments-by-client-info", + name: "Find Appointments by Client Info", + description: "Retrieves existing appointments using the client's information, allowing you to track all the appointments associated with a specific client. [See the documentation](https://developers.acuityscheduling.com/reference/get-appointments)", + version: "0.0.1", + type: "action", + props: { + acuityScheduling, + calendarID: { + propDefinition: [ + acuityScheduling, + "calendarId", + ], + optional: true, + }, + appointmentTypeID: { + propDefinition: [ + acuityScheduling, + "appointmentTypeId", + ], + optional: true, + }, + firstName: { + type: "string", + label: "First Name", + description: "Filter appointments for client first name.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Filter appointments for client last name.", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "Filter appointments for client email address.", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Filter appointments for client phone.", + optional: true, + }, + customFields: { + type: "object", + label: "Custom Field", + description: "Filter appointments matching a particular custom intake form field **eg. ?field:1234=Hello**", + optional: true, + }, + showAll: { + type: "boolean", + label: "Show All", + description: "To retrieve both canceled and scheduled appointments.", + optional: true, + }, + }, + async run({ $ }) { + let params = { + calendarID: this.calendarID, + appointmentTypeID: this.appointmentTypeID, + firstName: this.firstName, + lastName: this.lastName, + email: this.email, + phone: this.phone, + showAll: this.showAll, + }; + + if (this.customFields) { + params = { + ...params, + ...parseObject(this.customFields), + }; + } + const appointments = await this.acuityScheduling.listAppointments({ + $, + params, + }); + + $.export("$summary", `Found ${appointments.length} appointments for the search!`); + return appointments; + }, +}; diff --git a/components/acuity_scheduling/acuity_scheduling.app.mjs b/components/acuity_scheduling/acuity_scheduling.app.mjs index a3bdebe205a75..8941d4c701407 100644 --- a/components/acuity_scheduling/acuity_scheduling.app.mjs +++ b/components/acuity_scheduling/acuity_scheduling.app.mjs @@ -1,11 +1,111 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "acuity_scheduling", - propDefinitions: {}, + propDefinitions: { + startTime: { + type: "string", + label: "Start Time", + description: "The starting time to block off (e.g., '2023-01-01T00:00:00Z').", + }, + endTime: { + type: "string", + label: "End Time", + description: "The ending time of the blocked off period (e.g., '2023-01-01T01:00:00Z').", + }, + appointmentTypeId: { + type: "string", + label: "Appointment Type ID", + description: "Show only appointments of this type.", + async options() { + const data = await this.listAppointmentTypes(); + + return data.filter((item) => item.active).map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + calendarId: { + type: "string", + label: "Calendar ID", + description: "Show only appointments on calendar with specified ID.", + async options() { + const data = await this.listCalendars(); + + return data.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://acuityscheduling.com/api/v1"; + }, + _headers() { + return { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: this._headers(), + }); + }, + listAppointmentTypes() { + return this._makeRequest({ + path: "/appointment-types", + }); + }, + listCalendars() { + return this._makeRequest({ + path: "/calendars", + }); + }, + listAppointments(opts = {}) { + return this._makeRequest({ + path: "/appointments", + ...opts, + }); + }, + getAppointment({ + id, ...opts + }) { + return this._makeRequest({ + path: `/appointments/${id}`, + ...opts, + }); + }, + blockTime(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/blocks", + ...opts, + }); + }, + createHook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteHook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + }); }, }, }; diff --git a/components/acuity_scheduling/common/utils.mjs b/components/acuity_scheduling/common/utils.mjs new file mode 100644 index 0000000000000..154701004e72a --- /dev/null +++ b/components/acuity_scheduling/common/utils.mjs @@ -0,0 +1,18 @@ +export const parseObject = (obj) => { + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + return JSON.parse(obj); + } + return obj; +}; diff --git a/components/acuity_scheduling/package.json b/components/acuity_scheduling/package.json new file mode 100644 index 0000000000000..a1d45bbb724d8 --- /dev/null +++ b/components/acuity_scheduling/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/acuity_scheduling", + "version": "0.1.0", + "description": "Pipedream Acuity Scheduling Components", + "main": "acuity_scheduling.app.mjs", + "keywords": [ + "pipedream", + "acuity_scheduling" + ], + "homepage": "https://pipedream.com/apps/acuity_scheduling", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" + } +} + diff --git a/components/acuity_scheduling/sources/appointment-canceled-instant/appointment-canceled-instant.mjs b/components/acuity_scheduling/sources/appointment-canceled-instant/appointment-canceled-instant.mjs new file mode 100644 index 0000000000000..9f47f7c196114 --- /dev/null +++ b/components/acuity_scheduling/sources/appointment-canceled-instant/appointment-canceled-instant.mjs @@ -0,0 +1,21 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "acuity_scheduling-appointment-canceled-instant", + name: "New Appointment Canceled (Instant)", + description: "Emit new event when an appointment is canceled.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + getEvent() { + return "appointment.canceled"; + }, + getSummary(details) { + return `New appointment canceled with Id: ${details.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/acuity_scheduling/sources/appointment-canceled-instant/test-event.mjs b/components/acuity_scheduling/sources/appointment-canceled-instant/test-event.mjs new file mode 100644 index 0000000000000..194ba5d20c90f --- /dev/null +++ b/components/acuity_scheduling/sources/appointment-canceled-instant/test-event.mjs @@ -0,0 +1,58 @@ +export default { + "id": 54321, + "firstName": "Bob", + "lastName": "McTest", + "phone": "", + "email": "bob.mctest@example.com", + "date": "June 17, 2013", + "time": "10:15am", + "endTime": "11:15am", + "dateCreated": "July 2, 2013", + "datetime": "2013-06-17T10:15:00-0700", + "price": "10.00", + "paid": "no", + "amountPaid": "0.00", + "type": "Regular Visit", + "appointmentTypeID": 1, + "classID": null, + "duration": "60", + "calendar": "My Calendar", + "calendarID": 27238, + "canClientCancel": false, + "canClientReschedule": false, + "canceled": true, + "location": "", + "certificate": null, + "confirmationPage": "https://acuityscheduling.com/schedule.php?owner=11145481&id[]=1220aa9f41091c50c0cc659385cfa1d0&action=appt", + "formsText": "...", + "notes": "Notes", + "timezone": "America/New_York", + "scheduledBy": null, + "forms": [ + { + "id": 1, + "name": "Example Intake Form", + "values": [ + { + "value": "yes", + "name": "Is this your first visit?", + "fieldID": 1, + "id": 21502993 + }, + { + "value": "Ninja", + "name": "What is your goal for this appointment?", + "fieldID": 2, + "id": 21502994 + } + ] + } + ], + "labels": [ + { + "id": 3, + "name": "Completed", + "color": "pink" + } + ], +} \ No newline at end of file diff --git a/components/acuity_scheduling/sources/common/base.mjs b/components/acuity_scheduling/sources/common/base.mjs new file mode 100644 index 0000000000000..566daa2318b7f --- /dev/null +++ b/components/acuity_scheduling/sources/common/base.mjs @@ -0,0 +1,47 @@ +import acuityScheduling from "../../acuity_scheduling.app.mjs"; + +export default { + props: { + acuityScheduling, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + }, + hooks: { + async activate() { + const response = await this.acuityScheduling.createHook({ + data: { + event: this.getEvent(), + target: this.http.endpoint, + }, + }); + this.db.set("webhookId", response.id); + }, + async deactivate() { + const webhookId = this.db.get("webhookId"); + await this.acuityScheduling.deleteHook(webhookId); + }, + }, + async run(event) { + const { body } = event; + + const details = await this.acuityScheduling.getAppointment({ + id: body.id, + }); + + this.http.respond({ + status: 200, + body: "Success", + }); + + const ts = Date.parse(details.datetime) || Date.now(); + + this.$emit(details, { + id: details.id, + summary: this.getSummary(details), + ts: ts, + }); + }, +}; diff --git a/components/acuity_scheduling/sources/new-appointment-instant/new-appointment-instant.mjs b/components/acuity_scheduling/sources/new-appointment-instant/new-appointment-instant.mjs new file mode 100644 index 0000000000000..ca04a84759c81 --- /dev/null +++ b/components/acuity_scheduling/sources/new-appointment-instant/new-appointment-instant.mjs @@ -0,0 +1,21 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "acuity_scheduling-new-appointment-instant", + name: "New Appointment (Instant)", + description: "Emit new event when an appointment is scheduled.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + getEvent() { + return "appointment.scheduled"; + }, + getSummary(details) { + return `New appointment scheduled with Id: ${details.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/acuity_scheduling/sources/new-appointment-instant/test-event.mjs b/components/acuity_scheduling/sources/new-appointment-instant/test-event.mjs new file mode 100644 index 0000000000000..c82357d7a91e9 --- /dev/null +++ b/components/acuity_scheduling/sources/new-appointment-instant/test-event.mjs @@ -0,0 +1,57 @@ +export default { + "id": 54321, + "firstName": "Bob", + "lastName": "McTest", + "phone": "", + "email": "bob.mctest@example.com", + "date": "June 17, 2013", + "time": "10:15am", + "endTime": "11:15am", + "dateCreated": "July 2, 2013", + "datetime": "2013-06-17T10:15:00-0700", + "price": "10.00", + "paid": "no", + "amountPaid": "0.00", + "type": "Regular Visit", + "appointmentTypeID": 1, + "classID": null, + "duration": "60", + "calendar": "My Calendar", + "calendarID": 27238, + "canClientCancel": false, + "canClientReschedule": false, + "location": "", + "certificate": null, + "confirmationPage": "https://acuityscheduling.com/schedule.php?owner=11145481&id[]=1220aa9f41091c50c0cc659385cfa1d0&action=appt", + "formsText": "...", + "notes": "Notes", + "timezone": "America/New_York", + "scheduledBy": null, + "forms": [ + { + "id": 1, + "name": "Example Intake Form", + "values": [ + { + "value": "yes", + "name": "Is this your first visit?", + "fieldID": 1, + "id": 21502993 + }, + { + "value": "Ninja", + "name": "What is your goal for this appointment?", + "fieldID": 2, + "id": 21502994 + } + ] + } + ], + "labels": [ + { + "id": 3, + "name": "Completed", + "color": "pink" + } + ], +} \ No newline at end of file diff --git a/components/acumbamail/README.md b/components/acumbamail/README.md index c27d3e5003ef1..a11528db0d364 100644 --- a/components/acumbamail/README.md +++ b/components/acumbamail/README.md @@ -1,12 +1,11 @@ # Overview -With Acumbamail's API, you can build a variety of applications and integrations -to help you automate your email marketing. Here are some examples of what you -can build: - -- A system to automatically send out emails based on certain triggers or - conditions -- A way to track and analyze your email marketing campaigns -- A system to automatically segment your email list based on certain criteria -- A way to integrate your email marketing with other parts of your business, - such as your CRM or e-commerce platform +Acumbamail API offers a suite of features to automate email marketing workflows. It enables users to create, send, and manage email campaigns, subscriber lists, and reports. Using Acumbamail with Pipedream, you can listen to various triggers, such as new subscribers or campaign status updates, and initiate actions in other apps. This integration facilitates seamless synchronization of your email marketing efforts with CRM systems, customer databases, or any other tools in your business stack. + +# Example Use Cases + +- **Automated Subscriber Onboarding Emails**: When a new user signs up for your service, trigger a Pipedream workflow that adds the user to a specific Acumbamail subscriber list and sends a welcome email series designed to engage and educate the new user about your product. + +- **Dynamic Campaigns Triggered by User Actions**: Set up a Pipedream workflow that listens to user activity on your platform (like reaching a certain milestone) and automatically triggers a personalized Acumbamail email campaign to congratulate the user and offer related upsells or rewards. + +- **Synchronization with CRM**: Create a Pipedream workflow that syncs new Acumbamail subscribers to your CRM, such as Salesforce. Whenever a new contact is added to your Acumbamail list, the workflow updates or creates a corresponding lead/contact in Salesforce, ensuring your sales team has the latest data. diff --git a/components/acymailing/actions/add-update-user/add-update-user.mjs b/components/acymailing/actions/add-update-user/add-update-user.mjs new file mode 100644 index 0000000000000..47852f70eb2d9 --- /dev/null +++ b/components/acymailing/actions/add-update-user/add-update-user.mjs @@ -0,0 +1,77 @@ +import acymailing from "../../acymailing.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "acymailing-add-update-user", + name: "Add or Update User", + description: "Creates a new user or updates an existing user in AcyMailing. If the user exists, will update the user's data with provided information. [See the documentation](https://docs.acymailing.com/v/rest-api/users#create-or-update-a-user)", + version: "0.0.1", + type: "action", + props: { + acymailing, + email: { + type: "string", + label: "Email", + description: "The email address is used when updating an existing user.", + }, + name: { + type: "string", + label: "Name", + description: "Any character should be available.", + optional: true, + }, + active: { + type: "boolean", + label: "Active", + description: "Defaults to true.", + optional: true, + }, + confirmed: { + type: "boolean", + label: "Confirmed", + description: "The confirmation is related to the \"Require confirmation\" option in the configuration, tab \"Subscription\".", + optional: true, + }, + cmsId: { + type: "integer", + label: "CMS Id", + description: "The cms_id must match the ID of the corresponding Joomla/WordPress user.", + optional: true, + }, + customFields: { + type: "object", + label: "Custom Fields", + description: "An object of field Ids and values.", + optional: true, + }, + triggers: { + type: "boolean", + label: "Triggers", + description: "Defaults to true. Defines if the saving of the user triggers automated tasks like follow-up campaigns and automations.", + optional: true, + }, + sendConf: { + type: "boolean", + label: "Send Conf", + description: "Defaults to true. Defines if the confirmation email should be sent when a new user is created.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.acymailing.createUserOrUpdate({ + $, + data: { + email: this.email, + name: this.name, + active: this.active, + confirmed: this.confirmed, + cmsId: this.cmsId, + customFields: parseObject(this.customFields), + triggers: this.triggers, + sendConf: this.sendConf, + }, + }); + $.export("$summary", `Successfully added or updated user with email with Id: ${response.userId}`); + return response; + }, +}; diff --git a/components/acymailing/actions/email-user/email-user.mjs b/components/acymailing/actions/email-user/email-user.mjs new file mode 100644 index 0000000000000..d5bb81c9006ff --- /dev/null +++ b/components/acymailing/actions/email-user/email-user.mjs @@ -0,0 +1,55 @@ +import acymailing from "../../acymailing.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "acymailing-email-user", + name: "Email User", + description: "Sends an email to a single AcyMailing user. The user must exist in the AcyMailing database. [See the documentation](https://docs.acymailing.com/v/rest-api/emails#send-an-email-to-a-user)", + version: "0.0.1", + type: "action", + props: { + acymailing, + email: { + type: "string", + label: "Email", + description: "The email address of the receiver.", + }, + autoAddUser: { + type: "boolean", + label: "Auto Add User", + description: "Defaults to false. If the email address doesn't match an existing AcyMailing user, one will be automatically created if this option is set to true.", + optional: true, + }, + emailId: { + type: "integer", + label: "Email Id", + description: "The mail ID to send. This is not a campaign ID but the mail ID of the table xxx_acym_mail in the database, or the mail_id of a campaign.", + }, + trackEmail: { + type: "boolean", + label: "Track Email", + description: "Defaults to true. If true, the open/click statistics will be collected for this email.", + optional: true, + }, + params: { + type: "object", + label: "Params", + description: "An object of shortcodes and values to replace in the body of the sent email. Example: { \"shortcode1\": \"value 1\" }. If the body of the sent email contains the text \"{shortcode1}\", it will be replaced by \"value 1\" in the sent version.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.acymailing.sendEmailToUser({ + $, + data: { + email: this.email, + autoAddUser: this.autoAddUser, + emailId: this.emailId, + trackEmail: this.trackEmail, + params: parseObject(this.params), + }, + }); + $.export("$summary", `Email successfully sent to ${this.email}`); + return response; + }, +}; diff --git a/components/acymailing/actions/subscribe-user/subscribe-user.mjs b/components/acymailing/actions/subscribe-user/subscribe-user.mjs new file mode 100644 index 0000000000000..f567a65d246e2 --- /dev/null +++ b/components/acymailing/actions/subscribe-user/subscribe-user.mjs @@ -0,0 +1,51 @@ +import acymailing from "../../acymailing.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "acymailing-subscribe-user", + name: "Subscribe User to Lists", + description: "Subscribes a user to one or more specified lists in AcyMailing. [See the documentation](https://docs.acymailing.com/v/rest-api/subscription#subscribe-users-to-lists)", + version: "0.0.1", + type: "action", + props: { + acymailing, + emails: { + propDefinition: [ + acymailing, + "emails", + ], + }, + listIds: { + propDefinition: [ + acymailing, + "listIds", + ], + }, + sendWelcomeEmail: { + type: "boolean", + label: "Send Welcome Email", + description: "Defaults to true. If true, the welcome emails will be sent if the lists have one.", + optional: true, + }, + trigger: { + type: "boolean", + label: "Trigger", + description: "Defaults to true. If you want to trigger or not the automation or follow-up when subscribing the user.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.acymailing.subscribeUserToLists({ + $, + data: { + emails: parseObject(this.emails), + listIds: parseObject(this.listIds), + sendWelcomeEmail: this.sendWelcomeEmail, + trigger: this.trigger, + }, + }); + + $.export("$summary", `Successfully subscribed ${this.emails.length} users to lists ${this.listIds.length} lists`); + return response; + }, +}; diff --git a/components/acymailing/acymailing.app.mjs b/components/acymailing/acymailing.app.mjs new file mode 100644 index 0000000000000..abd3af914826f --- /dev/null +++ b/components/acymailing/acymailing.app.mjs @@ -0,0 +1,146 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "acymailing", + propDefinitions: { + listIds: { + type: "integer[]", + label: "List Ids", + description: "Array of list IDs.", + async options({ page }) { + const lists = await this.listLists({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + + return lists.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + emails: { + type: "string[]", + label: "Emails", + description: "The email addresses of users to subscribe to the lists. These must match already existing AcyMailing users.", + async options({ page }) { + const data = await this.listUsers({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + + return data.map(({ email }) => email); + }, + }, + }, + methods: { + _baseUrl() { + return `${this.$auth.url}`; + }, + _headers() { + return { + "Api-Key": `${this.$auth.api_key}`, + "Content-Type": "application/json", + }; + }, + _params(params) { + return { + page: "acymailing_front", + option: "com_acym", + ctrl: "api", + ...params, + }; + }, + _makeRequest({ + $ = this, params, task, ...opts + }) { + return axios($, { + url: `${this._baseUrl()}`, + params: this._params({ + ...params, + task, + }), + headers: this._headers(), + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + task: "getUsers", + ...opts, + }); + }, + listLists(opts = {}) { + return this._makeRequest({ + task: "getLists", + ...opts, + }); + }, + listSubscribersFromLists(opts = {}) { + return this._makeRequest({ + task: "getSubscribersFromLists", + ...opts, + }); + }, + listUnsubscribedUsersFromLists(opts = {}) { + return this._makeRequest({ + task: "getUnsubscribedUsersFromLists", + ...opts, + }); + }, + createUserOrUpdate(opts = {}) { + return this._makeRequest({ + method: "POST", + task: "createOrUpdateUser", + ...opts, + }); + }, + sendEmailToUser(opts = {}) { + return this._makeRequest({ + method: "POST", + task: "sendEmailToSingleUser", + ...opts, + }); + }, + subscribeUserToLists(opts = {}) { + return this._makeRequest({ + method: "POST", + task: "subscribeUsers", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, ...opts + }) { + let hasMore = false; + let page = 0; + + do { + params.limit = LIMIT; + params.offset = LIMIT * page; + page++; + + const data = await fn({ + params, + ...opts, + }); + + for (const d of data) { + yield d; + } + + hasMore = data.length; + + } while (hasMore); + }, + }, + +}; diff --git a/components/acymailing/common/constants.mjs b/components/acymailing/common/constants.mjs new file mode 100644 index 0000000000000..ea830c15a04cb --- /dev/null +++ b/components/acymailing/common/constants.mjs @@ -0,0 +1 @@ +export const LIMIT = 100; diff --git a/components/acymailing/common/utils.mjs b/components/acymailing/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/acymailing/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/acymailing/package.json b/components/acymailing/package.json new file mode 100644 index 0000000000000..ac757258cdc20 --- /dev/null +++ b/components/acymailing/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/acymailing", + "version": "0.1.0", + "description": "Pipedream AcyMailing Components", + "main": "acymailing.app.mjs", + "keywords": [ + "pipedream", + "acymailing" + ], + "homepage": "https://pipedream.com/apps/acymailing", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} + diff --git a/components/acymailing/sources/common/base.mjs b/components/acymailing/sources/common/base.mjs new file mode 100644 index 0000000000000..3d96e5892d799 --- /dev/null +++ b/components/acymailing/sources/common/base.mjs @@ -0,0 +1,68 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import acymailing from "../../acymailing.app.mjs"; + +export default { + props: { + acymailing, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(created) { + this.db.set("lastDate", created); + }, + generateMeta(item) { + return { + id: item.id, + summary: this.getSummary(item), + ts: item.createdAt, + }; + }, + async startEvent(maxResults = 0) { + const lastDate = this._getLastDate(); + + const response = this.acymailing.paginate({ + fn: this.getFn(), + params: this.getParams(), + }); + + const responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + const dateField = this.getDateField(); + + let filteredArray = responseArray + .sort((a, b) => Date.parse(b[dateField]) - Date.parse(a[dateField])); + + filteredArray = filteredArray.filter((item) => Date.parse(item[dateField]) > lastDate); + + if (maxResults && filteredArray.length > maxResults) { + filteredArray.length = maxResults; + } + + if (filteredArray.length) this._setLastDate(Date.parse(filteredArray[0][dateField])); + + for (const item of filteredArray.reverse()) { + this.$emit(item, this.generateMeta(item)); + } + }, + }, + hooks: { + async deploy() { + await this.startEvent(25); + }, + }, + async run() { + await this.startEvent(); + }, +}; diff --git a/components/acymailing/sources/new-confirmed-user/new-confirmed-user.mjs b/components/acymailing/sources/new-confirmed-user/new-confirmed-user.mjs new file mode 100644 index 0000000000000..26143fc507b4e --- /dev/null +++ b/components/acymailing/sources/new-confirmed-user/new-confirmed-user.mjs @@ -0,0 +1,30 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "acymailing-new-confirmed-user", + name: "New Confirmed User", + description: "Emit new event when a user confirms their email address.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSummary({ + email, confirmation_date, + }) { + return `User ${email} confirmed at ${confirmation_date}.`; + }, + getParams() { + return { + "filters[confirmed]": 1, + }; + }, + getFn() { + return this.acymailing.listUsers; + }, + getDateField() { + return "confirmation_date"; + }, + }, +}; diff --git a/components/acymailing/sources/new-subscribed-user/new-subscribed-user.mjs b/components/acymailing/sources/new-subscribed-user/new-subscribed-user.mjs new file mode 100644 index 0000000000000..77bf549c36e21 --- /dev/null +++ b/components/acymailing/sources/new-subscribed-user/new-subscribed-user.mjs @@ -0,0 +1,39 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "acymailing-new-subscribed-user", + name: "New Subscribed User", + description: "Emit new event when a user subscribes to a specified list.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + listIds: { + propDefinition: [ + common.props.acymailing, + "listIds", + ], + }, + }, + methods: { + ...common.methods, + getSummary({ + email, name, + }) { + return `New Subscriber: ${name} (${email})`; + }, + getParams() { + return { + "listIds[]": this.listIds, + }; + }, + getFn() { + return this.acymailing.listSubscribersFromLists; + }, + getDateField() { + return "subscription_date"; + }, + }, +}; diff --git a/components/acymailing/sources/new-unsubscribed-user/new-unsubscribed-user.mjs b/components/acymailing/sources/new-unsubscribed-user/new-unsubscribed-user.mjs new file mode 100644 index 0000000000000..8bfb7cdd774e4 --- /dev/null +++ b/components/acymailing/sources/new-unsubscribed-user/new-unsubscribed-user.mjs @@ -0,0 +1,37 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "acymailing-new-unsubscribed-user", + name: "New Unsubscribed User", + description: "Emit new event when a user unsubscribes from the specified mailing list.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + listIds: { + propDefinition: [ + common.props.acymailing, + "listIds", + ], + }, + }, + methods: { + ...common.methods, + getSummary({ email }) { + return `User ${email} unsubscribed`; + }, + getParams() { + return { + "listIds[]": this.listIds, + }; + }, + getFn() { + return this.acymailing.listUnsubscribedUsersFromLists; + }, + getDateField() { + return "unsubscribe_date"; + }, + }, +}; diff --git a/components/adafruit_io/README.md b/components/adafruit_io/README.md index 9ac1aa06960ad..df94fb4c49c74 100644 --- a/components/adafruit_io/README.md +++ b/components/adafruit_io/README.md @@ -1,8 +1,11 @@ # Overview -You can use the Adafruit IO API to build applications that can: +Adafruit IO is an API designed for building Internet of Things (IoT) applications. It offers a means to store, share, and manage data from your IoT devices. With Adafruit IO, you can create dashboards to display real-time data, trigger events based on data, and even control outputs. It's a versatile platform that's especially friendly for those getting started with IoT. -- Read and write data to Adafruit IO feeds -- Subscribe to and receive updates from Adafruit IO feeds -- Organize and view data from Adafruit IO feeds -- Create and manage Adafruit IO accounts and users +# Example Use Cases + +- **IoT Alert System**: Integrate Adafruit IO with Twilio on Pipedream to create an IoT alert system. When a sensor connected to Adafruit IO reaches a certain threshold—say, a temperature sensor detects a high value—Adafruit IO can trigger a workflow on Pipedream that sends an SMS alert via Twilio, informing the necessary parties to take action. + +- **Home Automation**: Connect Adafruit IO with smart home devices via Pipedream. For instance, when a light sensor on Adafruit IO detects darkness, it could trigger a Pipedream workflow that sends a command to Philips Hue or LIFX to turn on the lights in your home, seamlessly integrating ambient light awareness with home lighting systems. + +- **Data Logging and Analysis**: Use Adafruit IO with Google Sheets on Pipedream to log sensor data over time. Set up a workflow where data points from various sensors on Adafruit IO are periodically pushed into a Google Sheet. This allows for easy historical data analysis and visualization, enabling trends to be spotted and data-driven decisions to be made about IoT deployments. diff --git a/components/adalo/README.md b/components/adalo/README.md index a5931369da2d5..884cd7780f900 100644 --- a/components/adalo/README.md +++ b/components/adalo/README.md @@ -1,12 +1,11 @@ # Overview -Some examples of what you can build with the Adalo API: - -- Todo List -- Pinterest Board -- Shopping List -- Weather App -- Basic Calculator -- Flashcards -- Expense Tracker -- Photo Gallery +The Adalo API allows for the creation and manipulation of database records in your Adalo apps. Automating workflows with Pipedream can enhance your Adalo app's functionality, trigger custom actions based on app events, sync data across platforms, or notify users and team members about important updates. By leveraging Pipedream's ability to connect with hundreds of other apps, you can create sophisticated, multi-step workflows that respond dynamically to your Adalo app's data and events. + +# Example Use Cases + +- **User Onboarding Automation**: Whenever a new user signs up in your Adalo app, Pipedream can automatically send a welcome email through SendGrid, add the user to a Mailchimp list for future newsletters, and create a new record in a Google Sheets spreadsheet for tracking purposes. + +- **Inventory Management Sync**: If your Adalo app manages inventory, Pipedream can listen for changes in your product database. When a product's stock level falls below a certain threshold, it can trigger an order in a supplier's system via their API, update a Slack channel to notify your team, and adjust inventory levels in a connected Shopify store. + +- **Scheduled Content Publishing**: For apps that rely on content delivery, Pipedream can use Adalo's API to schedule and publish content at specific times. It could check a Google Calendar for scheduled posts, retrieve the content from a Google Drive folder, and then update the appropriate records in your Adalo app to make the content live. diff --git a/components/adalo/actions/common/base.mjs b/components/adalo/actions/common/base.mjs deleted file mode 100644 index 70a00ee372a14..0000000000000 --- a/components/adalo/actions/common/base.mjs +++ /dev/null @@ -1,12 +0,0 @@ -import adalo from "../../adalo.app.mjs"; - -export default { - props: { - adalo, - collectionId: { - label: "Collection ID", - description: "The ID of the collection", - type: "string", - }, - }, -}; diff --git a/components/adalo/actions/create-record/create-record.mjs b/components/adalo/actions/create-record/create-record.mjs index f6787c6ebde84..b77a625fd297a 100644 --- a/components/adalo/actions/create-record/create-record.mjs +++ b/components/adalo/actions/create-record/create-record.mjs @@ -1,14 +1,13 @@ -import base from "../common/base.mjs"; +import adalo from "../../adalo.app.mjs"; export default { - ...base, key: "adalo-create-record", name: "Create Record", description: "Create a new record. [See docs here](https://help.adalo.com/integrations/the-adalo-api/collections)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { - ...base.props, + adalo, data: { label: "Data", description: "The data to create record. E.g. `{ \"Email\": \"string\", \"Username\": \"string\", \"Full Name\": \"string\" }`", @@ -18,7 +17,6 @@ export default { async run({ $ }) { const response = await this.adalo.createRecord({ $, - collectionId: this.collectionId, data: JSON.parse(this.data), }); diff --git a/components/adalo/actions/get-records/get-records.mjs b/components/adalo/actions/get-records/get-records.mjs index 5b95edaacaed5..90a8ec515e6d0 100644 --- a/components/adalo/actions/get-records/get-records.mjs +++ b/components/adalo/actions/get-records/get-records.mjs @@ -1,12 +1,14 @@ -import base from "../common/base.mjs"; +import adalo from "../../adalo.app.mjs"; export default { - ...base, key: "adalo-get-records", name: "Get Records", description: "Get all records from a collection. [See docs here](https://help.adalo.com/integrations/the-adalo-api/collections)", - version: "0.0.2", + version: "0.0.4", type: "action", + props: { + adalo, + }, async run({ $ }) { let resources = []; let offset = 0; @@ -18,7 +20,6 @@ export default { requestFn: this.adalo.getRecords, requestArgs: { $, - collectionId: this.collectionId, params: { offset, }, diff --git a/components/adalo/actions/update-record/update-record.mjs b/components/adalo/actions/update-record/update-record.mjs index a8f4f83ddd930..8adbb2fe66df4 100644 --- a/components/adalo/actions/update-record/update-record.mjs +++ b/components/adalo/actions/update-record/update-record.mjs @@ -1,14 +1,13 @@ -import base from "../common/base.mjs"; +import adalo from "../../adalo.app.mjs"; export default { - ...base, key: "adalo-update-record", name: "Update Record", description: "Update a record. [See docs here](https://help.adalo.com/integrations/the-adalo-api/collections)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { - ...base.props, + adalo, recordId: { label: "Record ID", description: "The ID of a record", @@ -23,7 +22,6 @@ export default { async run({ $ }) { const response = await this.adalo.updateRecord({ $, - collectionId: this.collectionId, recordId: this.recordId, data: JSON.parse(this.data), }); diff --git a/components/adalo/adalo.app.mjs b/components/adalo/adalo.app.mjs index 88ecc71c25ccd..8b4de9a3b3694 100644 --- a/components/adalo/adalo.app.mjs +++ b/components/adalo/adalo.app.mjs @@ -3,8 +3,10 @@ import { axios } from "@pipedream/platform"; export default { type: "app", app: "adalo", - propDefinitions: {}, methods: { + _collectionId() { + return this.$auth.collection_id; + }, _apiKey() { return this.$auth.api_key; }, @@ -49,30 +51,24 @@ export default { }, }; }, - async getRecords({ - collectionId, ...args - } = {}) { - const response = await this._makeRequest({ - path: `/collections/${collectionId}`, + getRecords(args = {}) { + return this._makeRequest({ + path: `/collections/${this._collectionId()}`, ...args, }); - - return response.records; }, - async createRecord({ - collectionId, ...args - } = {}) { + createRecord(args = {}) { return this._makeRequest({ - path: `/collections/${collectionId}`, + path: `/collections/${this._collectionId()}`, method: "post", ...args, }); }, - async updateRecord({ - collectionId, recordId, ...args + updateRecord({ + recordId, ...args } = {}) { return this._makeRequest({ - path: `/collections/${collectionId}/${recordId}`, + path: `/collections/${this._collectionId()}/${recordId}`, method: "put", ...args, }); diff --git a/components/adalo/package.json b/components/adalo/package.json index 20f84341bf0d9..b737a874a416b 100644 --- a/components/adalo/package.json +++ b/components/adalo/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/adalo", - "version": "0.0.5", + "version": "0.0.6", "description": "Pipedream Adalo Components", "main": "adalo.app.mjs", "keywords": [ @@ -14,6 +14,6 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.2.0" + "@pipedream/platform": "^3.0.1" } } diff --git a/components/adalo/sources/new-record/new-record.mjs b/components/adalo/sources/new-record/new-record.mjs index ee8810b2bc033..2fb2411665fd9 100644 --- a/components/adalo/sources/new-record/new-record.mjs +++ b/components/adalo/sources/new-record/new-record.mjs @@ -5,7 +5,7 @@ export default { key: "adalo-new-record", name: "New Record", description: "Emit new event when is created a record.", - version: "0.0.4", + version: "0.0.5", type: "source", dedupe: "unique", props: { @@ -16,11 +16,6 @@ export default { intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, }, }, - collectionId: { - label: "Collection ID", - description: "The ID the collection", - type: "string", - }, }, methods: { emitEvent(event) { @@ -33,8 +28,7 @@ export default { }, hooks: { async deploy() { - const records = await this.adalo.getRecords({ - collectionId: this.collectionId, + const { records } = await this.adalo.getRecords({ params: { limit: 10, }, @@ -47,9 +41,8 @@ export default { let offset = 0; while (offset >= 0) { - const records = await this.adalo.getRecords({ + const { records } = await this.adalo.getRecords({ $, - collectionId: this.collectionId, params: { offset, }, diff --git a/components/addevent/README.md b/components/addevent/README.md new file mode 100644 index 0000000000000..601e3ee031a40 --- /dev/null +++ b/components/addevent/README.md @@ -0,0 +1,11 @@ +# Overview + +The AddEvent API enables automation of calendar event management. With it, you can create events, list upcoming events, and manage RSVPs. It’s useful for organizations that schedule multiple events and need to streamline their event-creation process, send out invitations, and track attendee responses. On Pipedream, you can build workflows that trigger on various events to connect AddEvent with other apps, creating a seamless event-management experience. + +# Example Use Cases + +- **Automated Event Creation from Webhooks**: Automate the creation of calendar events on AddEvent whenever a webhook is received. This is useful for adding events from an external trigger, like a form submission or a ticket purchase. + +- **RSVP Tracking with Google Sheets**: Sync AddEvent RSVPs to a Google Sheet to track attendees. This workflow is perfect for event coordinators who need a dynamic attendee list that updates in real-time as RSVPs come in. + +- **Event Reminder Emails via SendGrid**: Set up a workflow that sends reminder emails to attendees using SendGrid a day before the event starts. This helps reduce no-shows and keeps your event top-of-mind for participants. diff --git a/components/addressfinder/actions/verify-address-au/verify-address-au.mjs b/components/addressfinder/actions/verify-address-au/verify-address-au.mjs new file mode 100644 index 0000000000000..2f202f7e24f7a --- /dev/null +++ b/components/addressfinder/actions/verify-address-au/verify-address-au.mjs @@ -0,0 +1,77 @@ +import addressfinder from "../../addressfinder.app.mjs"; + +export default { + key: "addressfinder-verify-address-au", + name: "Verify Australian Address", + description: "Validates an Australian address. [See the documentation](https://addressfinder.com.au/api/au/address/verification/)", + version: "0.0.1", + type: "action", + props: { + addressfinder, + address: { + type: "string", + label: "Address", + description: "The Australian address to be verified, e.g. `30 Hoipo Road, SOMERSBY NSW`", + }, + database: { + type: "string[]", + label: "Database(s)", + description: "Which database(s) to return data from", + options: [ + { + value: "gnaf", + label: "Return physical addresses from the GNAF database.", + }, + { + value: "paf", + label: "Return postal addresses from the PAF database.", + }, + ], + }, + gps: { + type: "boolean", + label: "GPS Coordinates", + description: "Return GPS coordinates from the datasource specified (when available)", + optional: true, + }, + extended: { + type: "boolean", + label: "Extended", + description: "Returns extended information from the GNAF database", + optional: true, + }, + domain: { + propDefinition: [ + addressfinder, + "domain", + ], + }, + }, + async run({ $ }) { + const { + address, database, domain, + } = this; + const response = await this.addressfinder.verifyAustralianAddress({ + $, + params: { + q: address, + ...(database?.includes?.("gnaf") && { + gnaf: 1, + }), + ...(database?.includes?.("paf") && { + paf: 1, + }), + ...(this.gps && { + gps: 1, + }), + ...(this.extended && { + extended: 1, + }), + domain, + }, + }); + + $.export("$summary", `Successfully verified AU address "${address}"`); + return response; + }, +}; diff --git a/components/addressfinder/actions/verify-address-nz/verify-address-nz.mjs b/components/addressfinder/actions/verify-address-nz/verify-address-nz.mjs new file mode 100644 index 0000000000000..66fe5573e1047 --- /dev/null +++ b/components/addressfinder/actions/verify-address-nz/verify-address-nz.mjs @@ -0,0 +1,35 @@ +import addressfinder from "../../addressfinder.app.mjs"; + +export default { + key: "addressfinder-verify-address-nz", + name: "Verify New Zealand Address", + description: "Validates a New Zealand address. [See the documentation](https://addressfinder.com.au/api/nz/address/verification/)", + version: "0.0.1", + type: "action", + props: { + addressfinder, + address: { + type: "string", + label: "New Zealand Address", + description: "The New Zealand address to be verified, e.g. `186 Willis St, Te Aro`", + }, + domain: { + propDefinition: [ + addressfinder, + "domain", + ], + }, + }, + async run({ $ }) { + const { address } = this; + const response = await this.addressfinder.verifyNewZealandAddress({ + $, + params: { + q: address, + domain: this.domain, + }, + }); + $.export("$summary", `Successfully verified NZ address "${address}"`); + return response; + }, +}; diff --git a/components/addressfinder/actions/verify-email/verify-email.mjs b/components/addressfinder/actions/verify-email/verify-email.mjs new file mode 100644 index 0000000000000..25a33dd1e2a21 --- /dev/null +++ b/components/addressfinder/actions/verify-email/verify-email.mjs @@ -0,0 +1,59 @@ +import addressfinder from "../../addressfinder.app.mjs"; + +export default { + key: "addressfinder-verify-email", + name: "Verify Email", + description: "Validates the input email. [See the documentation](https://addressfinder.com.au/api/email/verification/)", + version: "0.0.1", + type: "action", + props: { + addressfinder, + email: { + type: "string", + label: "Email Address", + description: "The email address to be verified", + }, + domain: { + propDefinition: [ + addressfinder, + "domain", + ], + }, + features: { + type: "string[]", + label: "Features", + description: "The methods of verification to be completed. This will impact the query processing time and data returned in the response.", + optional: true, + options: [ + { + label: "Domain - a check to verify that the domain of the email address is configured to receive emails.", + value: "domain", + }, + { + label: "Connection - a check to verify that the email account exists at the provided domain.", + value: "connection", + }, + { + label: "Email provider - a check that determines the underlaying provider of the email service.", + value: "provider", + }, + ], + }, + }, + async run({ $ }) { + const { + email, domain, + } = this; + const response = await this.addressfinder.verifyEmailAddress({ + $, + params: { + email, + domain, + features: this.features?.join?.(), + }, + }); + + $.export("$summary", `Successfully verified email ${email}`); + return response; + }, +}; diff --git a/components/addressfinder/addressfinder.app.mjs b/components/addressfinder/addressfinder.app.mjs new file mode 100644 index 0000000000000..83e41c8f90af3 --- /dev/null +++ b/components/addressfinder/addressfinder.app.mjs @@ -0,0 +1,55 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "addressfinder", + propDefinitions: { + domain: { + type: "string", + label: "Domain", + description: + "Used to identify which of your services is calling the API for activity monitoring purposes. [See the documentation](https://addressfinder.com/r/faq/what-is-the-domain-option-used-for/) for more information.", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.addressfinder.io/api"; + }, + async _makeRequest({ + $ = this, headers, params, ...args + }) { + return axios($, { + baseURL: this._baseUrl(), + headers: { + ...headers, + Authorization: this.$auth.secret, + }, + params: { + ...params, + format: "json", + key: this.$auth.key, + }, + ...args, + }); + }, + async verifyAustralianAddress(args) { + return this._makeRequest({ + url: "/au/address/v2/verification", + ...args, + }); + }, + async verifyEmailAddress(args) { + return this._makeRequest({ + url: "/email/v1/verification", + ...args, + }); + }, + async verifyNewZealandAddress(args) { + return this._makeRequest({ + url: "/nz/address/verification", + ...args, + }); + }, + }, +}; diff --git a/components/addressfinder/package.json b/components/addressfinder/package.json new file mode 100644 index 0000000000000..0b34dffcb5ca9 --- /dev/null +++ b/components/addressfinder/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/addressfinder", + "version": "0.1.0", + "description": "Pipedream Addressfinder Components", + "main": "addressfinder.app.mjs", + "keywords": [ + "pipedream", + "addressfinder" + ], + "homepage": "https://pipedream.com/apps/addressfinder", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/addresszen/actions/freeform-verify-correct-us-address/freeform-verify-correct-us-address.mjs b/components/addresszen/actions/freeform-verify-correct-us-address/freeform-verify-correct-us-address.mjs new file mode 100644 index 0000000000000..43f47bc85aeeb --- /dev/null +++ b/components/addresszen/actions/freeform-verify-correct-us-address/freeform-verify-correct-us-address.mjs @@ -0,0 +1,28 @@ +import addresszen from "../../addresszen.app.mjs"; + +export default { + key: "addresszen-freeform-verify-correct-us-address", + name: "Verify and Correct Freeform US Address", + description: "Verify (CASS) and correct a US address using a complete address. [See the documentation](https://docs.addresszen.com/docs/api)", + version: "0.0.1", + type: "action", + props: { + addresszen, + addressLine: { + propDefinition: [ + addresszen, + "addressLine", + ], + }, + }, + async run({ $ }) { + const response = await this.addresszen.verifyAddress({ + $, + data: { + query: this.addressLine, + }, + }); + $.export("$summary", "Successfully verified and corrected the address"); + return response; + }, +}; diff --git a/components/addresszen/actions/verify-correct-us-address-city-state/verify-correct-us-address-city-state.mjs b/components/addresszen/actions/verify-correct-us-address-city-state/verify-correct-us-address-city-state.mjs new file mode 100644 index 0000000000000..8238c847f557a --- /dev/null +++ b/components/addresszen/actions/verify-correct-us-address-city-state/verify-correct-us-address-city-state.mjs @@ -0,0 +1,48 @@ +import addresszen from "../../addresszen.app.mjs"; +import { STATE_OPTIONS } from "../../common/constants.mjs"; + +export default { + key: "addresszen-verify-correct-us-address-city-state", + name: "Verify and Correct US Address by City and State", + description: "Verifies and corrects a US address using the input of a single address line, city, and state. [See the documentation](https://docs.addresszen.com/docs/api)", + version: "0.0.1", + type: "action", + props: { + addresszen, + addressLine: { + propDefinition: [ + addresszen, + "addressLine", + ], + }, + city: { + propDefinition: [ + addresszen, + "city", + ], + }, + state: { + propDefinition: [ + addresszen, + "state", + ], + }, + }, + async run({ $ }) { + const { + addressLine, city, state, + } = this; + + const parsedState = STATE_OPTIONS[state.toLowerCase()] || state; + const response = await this.addresszen.verifyAddress({ + $, + data: { + query: addressLine, + city, + state: parsedState, + }, + }); + $.export("$summary", `Successfully verified and corrected address for ${addressLine}, ${city}, ${state}`); + return response; + }, +}; diff --git a/components/addresszen/actions/verify-correct-us-address-zip-code/verify-correct-us-address-zip-code.mjs b/components/addresszen/actions/verify-correct-us-address-zip-code/verify-correct-us-address-zip-code.mjs new file mode 100644 index 0000000000000..f0fd8d817557f --- /dev/null +++ b/components/addresszen/actions/verify-correct-us-address-zip-code/verify-correct-us-address-zip-code.mjs @@ -0,0 +1,35 @@ +import addresszen from "../../addresszen.app.mjs"; + +export default { + key: "addresszen-verify-correct-us-address-zip-code", + name: "Verify and Correct US Address by Zip Code", + description: "Verifies and corrects a US address based on a single address line and a zip code. [See the documentation](https://docs.addresszen.com/docs/api)", + version: "0.0.1", + type: "action", + props: { + addresszen, + addressLine: { + propDefinition: [ + addresszen, + "addressLine", + ], + }, + zipCode: { + propDefinition: [ + addresszen, + "zipCode", + ], + }, + }, + async run({ $ }) { + const response = await this.addresszen.verifyAddress({ + $, + data: { + query: this.addressLine, + zip_code: this.zipCode, + }, + }); + $.export("$summary", "Successfully verified and corrected the address"); + return response; + }, +}; diff --git a/components/addresszen/addresszen.app.mjs b/components/addresszen/addresszen.app.mjs new file mode 100644 index 0000000000000..aa232e2695dc3 --- /dev/null +++ b/components/addresszen/addresszen.app.mjs @@ -0,0 +1,55 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "addresszen", + propDefinitions: { + addressLine: { + type: "string", + label: "Address Line", + description: "The US address to verify.", + }, + city: { + type: "string", + label: "City", + description: "The US city of the address.", + }, + state: { + type: "string", + label: "State", + description: "The US state of the address. 2 letter code or full state name are accepted.", + }, + zipCode: { + type: "string", + label: "Zip Code", + description: "The zip code of the address.", + }, + }, + methods: { + _baseUrl() { + return "https://api.addresszen.com/v1"; + }, + _params(params = {}) { + return { + ...params, + api_key: `${this.$auth.key}`, + }; + }, + _makeRequest({ + $ = this, path, params, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + params: this._params(params), + ...opts, + }); + }, + verifyAddress(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/verify/addresses", + ...opts, + }); + }, + }, +}; diff --git a/components/addresszen/common/constants.mjs b/components/addresszen/common/constants.mjs new file mode 100644 index 0000000000000..c8d2a2e7abab0 --- /dev/null +++ b/components/addresszen/common/constants.mjs @@ -0,0 +1,52 @@ +export const STATE_OPTIONS = { + "alabama": "AL", + "alaska": "AK", + "arizona": "AZ", + "arkansas": "AR", + "california": "CA", + "colorado": "CO", + "connecticut": "CT", + "delaware": "DE", + "florida": "FL", + "georgia": "GA", + "hawaii": "HI", + "idaho": "ID", + "illinois": "IL", + "indiana": "IN", + "iowa": "IA", + "kansas": "KS", + "kentucky": "KY", + "louisiana": "LA", + "maine": "ME", + "maryland": "MD", + "massachusetts": "MA", + "michigan": "MI", + "minnesota": "MN", + "mississippi": "MS", + "missouri": "MO", + "montana": "MT", + "nebraska": "NE", + "nevada": "NV", + "new hampshire": "NH", + "new jersey": "NJ", + "new mexico": "NM", + "new york": "NY", + "north carolina": "NC", + "north dakota": "ND", + "ohio": "OH", + "oklahoma": "OK", + "oregon": "OR", + "pennsylvania": "PA", + "rhode island": "RI", + "south carolina": "SC", + "south dakota": "SD", + "tennessee": "TN", + "texas": "TX", + "utah": "UT", + "vermont": "VT", + "virginia": "VA", + "washington": "WA", + "west virginia": "WV", + "wisconsin": "WI", + "wyoming": "WY", +}; diff --git a/components/addresszen/package.json b/components/addresszen/package.json new file mode 100644 index 0000000000000..da0de2b59557b --- /dev/null +++ b/components/addresszen/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/addresszen", + "version": "0.1.0", + "description": "Pipedream AddressZen Components", + "main": "addresszen.app.mjs", + "keywords": [ + "pipedream", + "addresszen" + ], + "homepage": "https://pipedream.com/apps/addresszen", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/adhook/actions/create-calendar-event/create-calendar-event.mjs b/components/adhook/actions/create-calendar-event/create-calendar-event.mjs new file mode 100644 index 0000000000000..8cfe8e244f647 --- /dev/null +++ b/components/adhook/actions/create-calendar-event/create-calendar-event.mjs @@ -0,0 +1,108 @@ +import adhook from "../../adhook.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "adhook-create-calendar-event", + name: "Create Calendar Event", + description: "Generates a personalized calendar event in AdHook. [See the documentation](https://app.adhook.io/api-doc/)", + version: "0.0.1", + type: "action", + props: { + adhook, + title: { + type: "string", + label: "Event Title", + description: "The title of the calendar event", + optional: true, + }, + description: { + type: "string", + label: "Event Description", + description: "The description of the calendar event", + optional: true, + }, + externalId: { + propDefinition: [ + adhook, + "externalId", + ], + optional: true, + }, + start: { + type: "string", + label: "Start Date", + description: "Start date of the event. **Format: YYYY-MM-DDTHH:MM:SSZ**", + optional: true, + }, + end: { + type: "string", + label: "End Date", + description: "End date of the event. **Format: YYYY-MM-DDTHH:MM:SSZ**", + optional: true, + }, + allDay: { + type: "boolean", + label: "All Day", + description: "Whether the event lasts all day or not", + optional: true, + }, + color: { + type: "string", + label: "Color", + description: "The color of the event", + optional: true, + }, + subtenantId: { + propDefinition: [ + adhook, + "subtenantId", + ], + optional: true, + }, + tags: { + propDefinition: [ + adhook, + "tags", + ], + optional: true, + }, + topics: { + propDefinition: [ + adhook, + "topics", + ], + optional: true, + }, + attachments: { + type: "string[]", + label: "Attachments", + description: "A list of objects of attachments for the event. **Format: {\"name\": \"Attachment name\", \"url\":\"https://attachment.com/file.pdf\", \"fileExtension\":\"pdf\"}**", + optional: true, + }, + }, + async run({ $ }) { + const { + adhook, + tags, + topics, + attachments, + ...data + } = this; + + const response = await adhook.createCalendarEvent({ + $, + data: { + type: "EVENT", + ...data, + tags: parseObject(tags), + topics: parseObject(topics)?.map((topic) => ({ + name: topic, + })), + attachments: parseObject(attachments), + }, + }); + + $.export("$summary", `Successfully created calendar event: ${response.id}`); + return response; + }, +}; diff --git a/components/adhook/actions/create-update-post/create-update-post.mjs b/components/adhook/actions/create-update-post/create-update-post.mjs new file mode 100644 index 0000000000000..13bec5285823f --- /dev/null +++ b/components/adhook/actions/create-update-post/create-update-post.mjs @@ -0,0 +1,138 @@ +import adhook from "../../adhook.app.mjs"; +import constants from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "adhook-create-update-post", + name: "Create or Update Post", + description: "Adds a new post or modifies an existing post in Adhook. [See the documentation](https://app.adhook.io/api-doc/)", + version: "0.0.1", + type: "action", + props: { + adhook, + postId: { + propDefinition: [ + adhook, + "postId", + ], + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "The name of the post", + optional: true, + }, + type: { + type: "string", + label: "Type", + description: "The type of the post", + optional: true, + options: constants.TYPE_OPTIONS, + }, + subtenantId: { + propDefinition: [ + adhook, + "subtenantId", + ], + optional: true, + }, + status: { + type: "string", + label: "Status", + description: "The status of the post", + optional: true, + options: constants.STATUS_OPTIONS, + }, + tags: { + propDefinition: [ + adhook, + "tags", + ], + optional: true, + }, + topics: { + propDefinition: [ + adhook, + "topics", + ], + optional: true, + }, + schedule: { + type: "string", + label: "Schedule", + description: "Whether you want to schedule or publish the post", + optional: true, + options: constants.SCHEDULE_OPTIONS, + }, + message: { + type: "string", + label: "Message", + description: "A message to send with the post", + optional: true, + }, + promotionType: { + type: "string", + label: "Promotion Type", + description: "The type of the promotion in the post", + optional: true, + options: constants.PROMOTION_TYPE_OPTIONS, + }, + campaignName: { + type: "string", + label: "Campaign Name", + description: "The name of the campaign", + optional: true, + }, + externalId: { + propDefinition: [ + adhook, + "externalId", + ], + optional: true, + }, + }, + async run({ $ }) { + const { + adhook, + postId, + tags, + topics, + ...data + } = this; + + let postData = {}; + + if (postId) { + const post = await adhook.getPost({ + postId, + }); + postData = post; + } + + postData = { + ...postData, + ...data, + tags: parseObject(tags) || postData.tags, + topics: parseObject(topics)?.map((topic) => ({ + name: topic, + })) || postData.topics, + }; + + const fn = postId + ? adhook.updatePost + : adhook.createPost; + + const response = await fn({ + $, + postId, + data: postData, + }); + + $.export("$summary", `Successfully ${postId + ? "updated" + : "created"} post`); + + return response; + }, +}; diff --git a/components/adhook/adhook.app.mjs b/components/adhook/adhook.app.mjs new file mode 100644 index 0000000000000..0a3b5aa878e5a --- /dev/null +++ b/components/adhook/adhook.app.mjs @@ -0,0 +1,128 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "adhook", + propDefinitions: { + subtenantId: { + type: "string", + label: "Subtenant Id", + description: "The id of the subtenant.", + async options() { + const data = await this.listSubtenants(); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + postId: { + type: "string", + label: "Post ID", + description: "The ID of the post", + async options({ page }) { + const data = await this.listPosts({ + params: { + limit: constants.LIMIT, + skip: constants.LIMIT * page, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + tags: { + type: "string[]", + label: "Tags", + description: "A list of tags.", + }, + topics: { + type: "string[]", + label: "Topics", + description: "A list of topics.", + }, + externalId: { + type: "string", + label: "External Id", + description: "An external Id to the event", + }, + }, + methods: { + _baseUrl() { + return "https://app.adhook.io/api/v1"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createCalendarEvent(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/customEvents", + ...opts, + }); + }, + listPosts(opts = {}) { + return this._makeRequest({ + path: "/posts", + ...opts, + }); + }, + listSubtenants(opts = {}) { + return this._makeRequest({ + path: "/subtenants", + ...opts, + }); + }, + createPost(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/posts", + ...opts, + }); + }, + getPost({ postId }) { + return this._makeRequest({ + path: `/posts/${postId}`, + }); + }, + updatePost({ + postId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/posts/${postId}`, + ...opts, + }); + }, + listCreatedPosts() { + return this._makeRequest({ + path: "/posts/recentlyCreated", + }); + }, + listUpdatedPosts() { + return this._makeRequest({ + path: "/posts/recentlyUpdated", + }); + }, + }, +}; diff --git a/components/adhook/common/constants.mjs b/components/adhook/common/constants.mjs new file mode 100644 index 0000000000000..a917c3200b959 --- /dev/null +++ b/components/adhook/common/constants.mjs @@ -0,0 +1,59 @@ +const LIMIT = 100; + +const TYPE_OPTIONS = [ + "PERSONAL_POST", + "PAGE_POST", + "STORY", + "REELS", + "SHORTS", + "LINK_CAROUSEL", + "DOCUMENT", + "POLL", +]; + +const STATUS_OPTIONS = [ + "CREATED", + "READY", + "PLANNED", + "PUBLISHED", + "IN_REVIEW", + "ACTIVATING", + "MANUALLY_ACTIVATING", + "ERROR", + "DELETED", + "CHECK_REVIEW_FEEDBACK", + "IMPORTING", +]; + +const SCHEDULE_OPTIONS = [ + "PUBLISH_AS_SOON_AS_POSSIBLE", + "PLANNED_POST_PUBLISH", +]; + +const PROMOTION_TYPE_OPTIONS = [ + "TRAFFIC", + "CONVERSIONS", + "RETARGETING", + "SHOPPING", + "CARS", + "REAL_ESTATE", + "JOBS", + "EVENTS", + "APP", + "ENGAGEMENT", + "VIDEO_VIEWS", + "CALLS", + "REACH", + "FOLLOWERS", + "PROFILE_VIEWS", + "LEADS", + "PERFORMANCE_MAX", +]; + +export default { + LIMIT, + TYPE_OPTIONS, + STATUS_OPTIONS, + SCHEDULE_OPTIONS, + PROMOTION_TYPE_OPTIONS, +}; diff --git a/components/adhook/common/utils.mjs b/components/adhook/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/adhook/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/adhook/package.json b/components/adhook/package.json new file mode 100644 index 0000000000000..9348175fade26 --- /dev/null +++ b/components/adhook/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/adhook", + "version": "0.1.0", + "description": "Pipedream Adhook Components", + "main": "adhook.app.mjs", + "keywords": [ + "pipedream", + "adhook" + ], + "homepage": "https://pipedream.com/apps/adhook", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} diff --git a/components/adhook/sources/common/base.mjs b/components/adhook/sources/common/base.mjs new file mode 100644 index 0000000000000..9d139d58d36a3 --- /dev/null +++ b/components/adhook/sources/common/base.mjs @@ -0,0 +1,54 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import adhook from "../../adhook.app.mjs"; + +export default { + props: { + adhook, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const fieldDate = this.getFieldDate(); + const fn = this.getFunction(); + let response = await fn(); + + response = response.filter((item) => Date.parse(item[fieldDate]) > lastDate); + + if (response.length) { + if (maxResults && (response.length > maxResults)) { + response.length = maxResults; + } + this._setLastDate(Date.parse(response[0][fieldDate])); + } + + for (const item of response.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item[fieldDate]), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/adhook/sources/new-post/new-post.mjs b/components/adhook/sources/new-post/new-post.mjs new file mode 100644 index 0000000000000..b4abb56b048f5 --- /dev/null +++ b/components/adhook/sources/new-post/new-post.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "adhook-new-post", + name: "New Post Created", + description: "Emit new event when a new post is created. [See the documentation](https://app.adhook.io/api-doc/)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.adhook.listCreatedPosts; + }, + getFieldDate() { + return "createdAt"; + }, + getSummary(item) { + return `New Post: ${item.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/adhook/sources/new-post/test-event.mjs b/components/adhook/sources/new-post/test-event.mjs new file mode 100644 index 0000000000000..62430e0b0638a --- /dev/null +++ b/components/adhook/sources/new-post/test-event.mjs @@ -0,0 +1,50 @@ +export default { + "id": "66c57199f555a956148eaba0", + "message": "Message text", + "type": "PAGE_POST", + "status": "PUBLISHED", + "createdAt": "2024-08-21T04:48:25.666860864Z", + "createdByUserEmail": "user@email.com", + "createdByUserId": "66c57199f555a956148eaba0", + "tenantId": "66c57199f555a956148eaba0", + "tags": [ + { + "id": "66c57199f555a956148eaba0", + "userId": null, + "tenantId": "66c57199f555a956148eaba0", + "subtenantId": "66c57199f555a956148eaba0", + "text": "LinkedIn", + "color": null + } + ], + "topics": null, + "subtenantId": "66c57199f555a956148eaba0", + "processReviewFeedback": null, + "processTaskAssigneeUserId": null, + "processTaskAssigneeUserIds": null, + "imageUrl": null, + "videoUrl": null, + "videoThumbnailUrl": null, + "promotePost": false, + "nbrOfDays": 7, + "totalBudget": 0.0, + "currency": "USD", + "remark": null, + "impressions": 0, + "reach": 0, + "clicks": 0, + "likes": 0, + "comments": 0, + "shares": 0, + "videoViews": 0, + "reactions": 0, + "publishAt": "2024-02-02T18:39:23.22Z", + "publishedAt": "2024-02-02T18:39:23.22Z", + "schedule": "PLANNED_POST_PUBLISH", + "activationErrorMessage": null, + "activationErrorChannel": null, + "translationStatus": null, + "translationDate": null, + "deliverContentTillDate": null, + "reviewTillDate": null +} \ No newline at end of file diff --git a/components/adhook/sources/updated-post/test-event.mjs b/components/adhook/sources/updated-post/test-event.mjs new file mode 100644 index 0000000000000..62430e0b0638a --- /dev/null +++ b/components/adhook/sources/updated-post/test-event.mjs @@ -0,0 +1,50 @@ +export default { + "id": "66c57199f555a956148eaba0", + "message": "Message text", + "type": "PAGE_POST", + "status": "PUBLISHED", + "createdAt": "2024-08-21T04:48:25.666860864Z", + "createdByUserEmail": "user@email.com", + "createdByUserId": "66c57199f555a956148eaba0", + "tenantId": "66c57199f555a956148eaba0", + "tags": [ + { + "id": "66c57199f555a956148eaba0", + "userId": null, + "tenantId": "66c57199f555a956148eaba0", + "subtenantId": "66c57199f555a956148eaba0", + "text": "LinkedIn", + "color": null + } + ], + "topics": null, + "subtenantId": "66c57199f555a956148eaba0", + "processReviewFeedback": null, + "processTaskAssigneeUserId": null, + "processTaskAssigneeUserIds": null, + "imageUrl": null, + "videoUrl": null, + "videoThumbnailUrl": null, + "promotePost": false, + "nbrOfDays": 7, + "totalBudget": 0.0, + "currency": "USD", + "remark": null, + "impressions": 0, + "reach": 0, + "clicks": 0, + "likes": 0, + "comments": 0, + "shares": 0, + "videoViews": 0, + "reactions": 0, + "publishAt": "2024-02-02T18:39:23.22Z", + "publishedAt": "2024-02-02T18:39:23.22Z", + "schedule": "PLANNED_POST_PUBLISH", + "activationErrorMessage": null, + "activationErrorChannel": null, + "translationStatus": null, + "translationDate": null, + "deliverContentTillDate": null, + "reviewTillDate": null +} \ No newline at end of file diff --git a/components/adhook/sources/updated-post/updated-post.mjs b/components/adhook/sources/updated-post/updated-post.mjs new file mode 100644 index 0000000000000..61458ffcf22ee --- /dev/null +++ b/components/adhook/sources/updated-post/updated-post.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "adhook-updated-post", + name: "New Updated Post", + description: "Emit new event when a post is updated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.adhook.listUpdatedPosts; + }, + getFieldDate() { + return "updatedAt"; + }, + getSummary(post) { + return `Post Updated: ${post.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/adobe_document_generation_api/adobe_document_generation_api.app.mjs b/components/adobe_document_generation_api/adobe_document_generation_api.app.mjs new file mode 100644 index 0000000000000..4415cefa256ea --- /dev/null +++ b/components/adobe_document_generation_api/adobe_document_generation_api.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "adobe_document_generation_api", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/adobe_document_generation_api/package.json b/components/adobe_document_generation_api/package.json new file mode 100644 index 0000000000000..0e3aa1143f5bf --- /dev/null +++ b/components/adobe_document_generation_api/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/adobe_document_generation_api", + "version": "0.0.1", + "description": "Pipedream Adobe Document Generation API Components", + "main": "adobe_document_generation_api.app.mjs", + "keywords": [ + "pipedream", + "adobe_document_generation_api" + ], + "homepage": "https://pipedream.com/apps/adobe_document_generation_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/adobe_pdf_services/README.md b/components/adobe_pdf_services/README.md new file mode 100644 index 0000000000000..bddbfa1218f76 --- /dev/null +++ b/components/adobe_pdf_services/README.md @@ -0,0 +1,11 @@ +# Overview + +The Adobe PDF Services API provides a robust set of tools for manipulating and managing PDF files. With this API, you can create, convert, combine, export, and manipulate PDFs directly in Pipedream. The Pipedream platform enables you to build automated workflows that can interact with this API to streamline document-centric processes, such as generating reports, archiving files, or extracting data from PDFs into other formats. + +# Example Use Cases + +- **Automated Invoice Processing**: Collect invoices as PDFs from an email or a web form, use Adobe PDF Services to extract text and data, and then store the extracted data in a database like Google Sheets or Airtable for easy tracking and analysis. + +- **Document Conversion Pipeline**: Monitor a Dropbox or Google Drive folder for new documents, convert them into PDFs using Adobe PDF Services, and then upload the PDFs to a designated cloud storage location or send them via email to relevant stakeholders. + +- **PDF Generation for Analytics Reports**: After a scheduled time, gather analytics data from sources like Google Analytics, compile the data into a templated PDF report using Adobe PDF Services, and distribute the report to a Slack channel or via email to your team. diff --git a/components/adobe_pdf_services/actions/extract-text-and-tables/extract-text-and-tables.mjs b/components/adobe_pdf_services/actions/extract-text-and-tables/extract-text-and-tables.mjs index c163805042c09..74bfb2d9135bc 100644 --- a/components/adobe_pdf_services/actions/extract-text-and-tables/extract-text-and-tables.mjs +++ b/components/adobe_pdf_services/actions/extract-text-and-tables/extract-text-and-tables.mjs @@ -4,7 +4,7 @@ export default { key: "adobe_pdf_services-extract-text-and-tables", name: "Extract Text and Tables From PDF", description: "Extracts text and table element information from a PDF document and returns a JSON file along with table data in XLSX format within a .zip file saved to the `/tmp` directory. [See the documentation](https://developer.adobe.com/document-services/docs/overview/pdf-extract-api/howtos/extract-api/#extract-text-and-tables)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { adobe, diff --git a/components/adobe_pdf_services/actions/extract-text-from-pdf/extract-text-from-pdf.mjs b/components/adobe_pdf_services/actions/extract-text-from-pdf/extract-text-from-pdf.mjs index 0dd391108fb46..0461442f7acb8 100644 --- a/components/adobe_pdf_services/actions/extract-text-from-pdf/extract-text-from-pdf.mjs +++ b/components/adobe_pdf_services/actions/extract-text-from-pdf/extract-text-from-pdf.mjs @@ -4,7 +4,7 @@ export default { key: "adobe_pdf_services-extract-text-from-pdf", name: "Extract Text From PDF", description: "Extracts text element information from a PDF document and returns a JSON file within a .zip file saved to the `/tmp` directory. [See the documentation](https://developer.adobe.com/document-services/docs/overview/pdf-extract-api/howtos/extract-api/#extract-text-from-a-pdf)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { adobe, diff --git a/components/adobe_pdf_services/adobe_pdf_services.app.mjs b/components/adobe_pdf_services/adobe_pdf_services.app.mjs index 338787ffcd8da..43e6870fd2c28 100644 --- a/components/adobe_pdf_services/adobe_pdf_services.app.mjs +++ b/components/adobe_pdf_services/adobe_pdf_services.app.mjs @@ -50,8 +50,8 @@ export default { ); extractPDFOperation.setInput(input); extractPDFOperation.setOptions(options); - const { saveAsFile } = await extractPDFOperation.execute(executionContext); - return saveAsFile(`/tmp/${filename}`); + const response = await extractPDFOperation.execute(executionContext); + return response.saveAsFile(`/tmp/${filename}`); }, }, }; diff --git a/components/adobe_pdf_services/package.json b/components/adobe_pdf_services/package.json index 0adc169b55f95..93c68d1a912bb 100644 --- a/components/adobe_pdf_services/package.json +++ b/components/adobe_pdf_services/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/adobe_pdf_services", - "version": "0.1.0", + "version": "0.1.1", "description": "Pipedream Adobe PDF Services Components", "main": "adobe_pdf_services.app.mjs", "keywords": [ diff --git a/components/adobe_photoshop/README.md b/components/adobe_photoshop/README.md new file mode 100644 index 0000000000000..d9b7314e2f2c5 --- /dev/null +++ b/components/adobe_photoshop/README.md @@ -0,0 +1,11 @@ +# Overview + +The Adobe Photoshop API allows developers to leverage the extensive capabilities of Photoshop directly within their applications, automating complex editing tasks and integrating sophisticated image manipulation features. Through the API, users can create, edit, and manipulate images on the fly, facilitating dynamic content creation across various digital platforms. When combined with Pipedream's capabilities, this API enables seamless automation of workflows that require image processing, enhancing productivity and creative possibilities. + +# Example Use Cases + +- **Automated Image Processing for E-Commerce**: Automatically enhance and resize product photos when they are uploaded to a cloud storage platform like Amazon S3. The workflow can be set to trigger whenever new images are uploaded to a specific S3 bucket, process them using Photoshop API for consistent quality and size, and then upload the processed images back to the cloud or directly to an e-commerce platform. + +- **Dynamic Social Media Content Creation**: Generate customized social media posts by overlaying text on images based on upcoming events or promotions. This workflow can utilize a calendar event in Google Calendar as the trigger, fetch event details, and use the Photoshop API to create engaging visuals by adding relevant text or design elements to a template image. The final artwork can then be automatically posted to various social media platforms using their respective APIs. + +- **Real-Time Marketing Material Updates**: Automate the update of marketing materials like banners or posters by integrating the Photoshop API with a CMS (Content Management System). Whenever the CMS triggers a change (e.g., price change, new product launch), the Photoshop API can be used to dynamically update images, which are then automatically republished to the website or marketing channels. diff --git a/components/adobe_photoshop/actions/remove-background-from-image/remove-background-from-image.mjs b/components/adobe_photoshop/actions/remove-background-from-image/remove-background-from-image.mjs new file mode 100644 index 0000000000000..06e6f58bd6224 --- /dev/null +++ b/components/adobe_photoshop/actions/remove-background-from-image/remove-background-from-image.mjs @@ -0,0 +1,91 @@ +import adobePhotoshop from "../../adobe_photoshop.app.mjs"; +import { + MASK_FORMAT_OPTIONS, + OPTIMIZE_OPTIONS, + STORAGE_OPTIONS, +} from "../../common/constants.mjs"; + +export default { + key: "adobe_photoshop-remove-background-from-image", + name: "Remove Background from Image", + description: "Removes the background from an image using Adobe Photoshop API. [See the documentation](https://developer.adobe.com/firefly-services/docs/photoshop/api/photoshop_removeBackground/)", + version: "0.0.1", + type: "action", + props: { + adobePhotoshop, + inputHref: { + type: "string", + label: "Input Href", + description: "Any public url or a presignedURL for the asset input.", + }, + inputStorage: { + type: "string", + label: "Input Storage", + description: "Asset stored on an external service (like AWS S3, Azure, Dropbox).", + options: STORAGE_OPTIONS, + default: "external", + }, + optimize: { + type: "string", + label: "Optimize", + description: "The value 'performance' optimizes for speed. 'batch' ensures the job will ultimately run regardless of wait time.", + optional: true, + options: OPTIMIZE_OPTIONS, + }, + outputHref: { + type: "string", + label: "Output Href", + description: "Any public url or a presignedURL for the asset output.", + }, + outputStorage: { + type: "string", + label: "Output Storage", + description: "Asset stored on an external service (like AWS S3, Azure, Dropbox).", + options: STORAGE_OPTIONS, + default: "external", + }, + overwrite: { + type: "boolean", + label: "Overwrite", + description: "If the file already exists, indicates if the output file should be overwritten. Only applies to files stored in Adobe storage.", + optional: true, + }, + maskFormat: { + type: "string", + label: "Mask Format", + description: "A soft (feathered) mask or binary mask.", + options: MASK_FORMAT_OPTIONS, + optional: true, + }, + }, + async run({ $ }) { + const data = { + input: { + href: this.inputHref, + storage: this.inputStorage, + }, + options: { + optimize: this.optimize, + }, + output: { + href: this.outputHref, + storage: this.outputStorage, + overwrite: this.overwrite, + }, + }; + + if (this.maskFormat) { + data.output.mask = { + format: this.maskFormat, + }; + } + + const response = await this.adobePhotoshop.removeBackground({ + $, + data, + }); + + $.export("$summary", "Successfully removed background from the image"); + return response; + }, +}; diff --git a/components/adobe_photoshop/adobe_photoshop.app.mjs b/components/adobe_photoshop/adobe_photoshop.app.mjs new file mode 100644 index 0000000000000..d308ffec21bf2 --- /dev/null +++ b/components/adobe_photoshop/adobe_photoshop.app.mjs @@ -0,0 +1,33 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "adobe_photoshop", + methods: { + _baseUrl() { + return "https://image.adobe.io/sensei"; + }, + _headers() { + return { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "x-api-key": `${this.$auth.client_id}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + removeBackground(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/cutout", + ...opts, + }); + }, + }, +}; diff --git a/components/adobe_photoshop/common/constants.mjs b/components/adobe_photoshop/common/constants.mjs new file mode 100644 index 0000000000000..e65e1078dbe06 --- /dev/null +++ b/components/adobe_photoshop/common/constants.mjs @@ -0,0 +1,14 @@ +export const STORAGE_OPTIONS = [ + "external", + "azure", + "dropbox", + "adobe", +]; +export const MASK_FORMAT_OPTIONS = [ + "soft", + "binary", +]; +export const OPTIMIZE_OPTIONS = [ + "performance", + "batch", +]; diff --git a/components/adobe_photoshop/package.json b/components/adobe_photoshop/package.json new file mode 100644 index 0000000000000..ee1d842af4b5f --- /dev/null +++ b/components/adobe_photoshop/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/adobe_photoshop", + "version": "0.1.0", + "description": "Pipedream Adobe Photoshop Components", + "main": "adobe_photoshop.app.mjs", + "keywords": [ + "pipedream", + "adobe_photoshop" + ], + "homepage": "https://pipedream.com/apps/adobe_photoshop", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/adobe_photoshop_lightroom/adobe_photoshop_lightroom.app.mjs b/components/adobe_photoshop_lightroom/adobe_photoshop_lightroom.app.mjs new file mode 100644 index 0000000000000..d071f5e9fb5fc --- /dev/null +++ b/components/adobe_photoshop_lightroom/adobe_photoshop_lightroom.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "adobe_photoshop_lightroom", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/adobe_photoshop_lightroom/package.json b/components/adobe_photoshop_lightroom/package.json new file mode 100644 index 0000000000000..74b3e02456930 --- /dev/null +++ b/components/adobe_photoshop_lightroom/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/adobe_photoshop_lightroom", + "version": "0.0.1", + "description": "Pipedream Adobe Photoshop Lightroom Components", + "main": "adobe_photoshop_lightroom.app.mjs", + "keywords": [ + "pipedream", + "adobe_photoshop_lightroom" + ], + "homepage": "https://pipedream.com/apps/adobe_photoshop_lightroom", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/adobe_sign/README.md b/components/adobe_sign/README.md new file mode 100644 index 0000000000000..ce03e7a40be1f --- /dev/null +++ b/components/adobe_sign/README.md @@ -0,0 +1,11 @@ +# Overview + +The Adobe Acrobat Sign API lets you embed e-signature processes into your custom applications, automate document workflows, and manage e-signatures. Specifically within Pipedream, you can harness this API to build serverless workflows that automate document signing requests, track status changes, and manage signed documents without ever leaving the platform. You can create workflows that trigger on various events, send out documents to be signed, and handle the responses—all programmatically. + +# Example Use Cases + +- **Automated Contract Signing Workflow**: When a new contract is uploaded to a Google Drive folder, Pipedream detects the upload and triggers a workflow that sends the document to specified recipients for e-signature via Adobe Acrobat Sign. Once signed, the document is then automatically saved back to Google Drive and the signatories receive a confirmation email. + +- **Onboarding Paperwork Automation**: As soon as a new employee record is added to an HR system like BambooHR, Pipedream triggers a workflow that sends the necessary onboarding documents for signature through Adobe Acrobat Sign. The workflow tracks the status of the document, and upon completion, updates the HR system and notifies the team via Slack. + +- **Sales Quote Approval Process**: When a new sales quote is created in a CRM like Salesforce, Pipedream triggers an automated workflow that sends the quote for approval using Adobe Acrobat Sign. Once the quote is signed by the required parties, the workflow updates the deal status in Salesforce and sends a notification to the sales team in Microsoft Teams. diff --git a/components/adobe_sign/package.json b/components/adobe_sign/package.json index 3f050e368972f..3781ea0561851 100644 --- a/components/adobe_sign/package.json +++ b/components/adobe_sign/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/adp/README.md b/components/adp/README.md new file mode 100644 index 0000000000000..c99a348767828 --- /dev/null +++ b/components/adp/README.md @@ -0,0 +1,11 @@ +# Overview + +The ADP API provides access to a breadth of payroll and human capital management services. With Pipedream, you can automate workflows that bridge the gap between ADP and other apps, streamlining your HR processes. By leveraging Pipedream's serverless platform, you can orchestrate data flows, synchronize employee information, manage payroll events, and react to changes in ADP data in real-time without writing extensive code. + +# Example Use Cases + +- **Employee Onboarding Automation**: When a new employee is added in ADP, automatically send a welcome email using SendGrid, create accounts in necessary internal systems, and post a welcome message in Slack to announce their arrival. + +- **Payroll Change Notifications**: Monitor changes in payroll data within ADP and trigger notifications in real-time. Whenever there's an update, you can send an alert via email, post a message to a specific Slack channel, or log the change in Google Sheets for further analysis and record-keeping. + +- **Time-Off Synchronization**: Sync time-off requests from ADP to a Google Calendar to keep everyone informed. When an employee requests time off, a Pipedream workflow can automatically create a calendar event, and if needed, send a notification to their manager for approval via Twilio SMS or a Slack direct message. diff --git a/components/adp/adp.app.mjs b/components/adp/adp.app.mjs new file mode 100644 index 0000000000000..43cfad59ea0c3 --- /dev/null +++ b/components/adp/adp.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "adp", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/adrapid/actions/create-banner/create-banner.mjs b/components/adrapid/actions/create-banner/create-banner.mjs new file mode 100644 index 0000000000000..73c9dba5b2ab2 --- /dev/null +++ b/components/adrapid/actions/create-banner/create-banner.mjs @@ -0,0 +1,185 @@ +import adrapid from "../../adrapid.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "adrapid-create-banner", + name: "Create Banner", + description: "Generates a new banner using provided data. This action can create different types of banners, such as animated HTML5, image, or video banners. [See the documentation](https://docs.adrapid.com/api/overview)", + version: "0.0.1", + type: "action", + props: { + adrapid, + templateId: { + propDefinition: [ + adrapid, + "templateId", + ], + reloadProps: true, + }, + modes: { + type: "string[]", + label: "Modes", + description: "Modes for the resulting banners, previously set on configuration block.", + options: [ + "html5", + "amp", + "png", + "jpeg", + "pdf", + "webp", + "video", + "gif", + ], + reloadProps: true, + optional: true, + }, + overrides: { + type: "object", + label: "Overrides", + description: "With this parameter we override the default template content. We use the name of the item we want to override, valid properties are text, url (for external images) and css. [See the documentation](https://docs.adrapid.com/api/overview).", + optional: true, + }, + sync: { + type: "boolean", + label: "Sync", + description: "Get banner url in the same response. Delay the response until the banner is ready. Usually in a few seconds with a maxtime of two minutes.", + optional: true, + }, + editable: { + type: "boolean", + label: "Editable", + description: "Make banner editable, creating a template that is a copy of the banner.", + optional: true, + }, + excludeBaseSize: { + type: "boolean", + label: "Exclude Base Size", + description: "Exclude base size from the banner. Only applies to multisize banners.", + optional: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.templateId) { + const { sizes } = await this.adrapid.getTemplate({ + templateId: this.templateId, + }); + props.sizeIds = { + type: "string[]", + label: "Size Ids", + description: "The template sizes. To use all of them you can set this parameter to 'all'. If you remove this parameter only the default size of the template will be used.", + reloadProps: true, + optional: true, + options: sizes.map(({ + id: value, name: label, + }) => ({ + value, + label, + })), + }; + } + + if (this.modes) { + if (this.modes.includes("html5")) { + props.html5Packed = { + type: "boolean", + label: "HTML5 Packed", + description: "Set banner in a single file with embedded images.", + }; + props.html5Flexible = { + type: "boolean", + label: "HTML5 Flexible", + description: "Set a flexible banner that will adapt to his container.", + }; + } + if (this.modes.includes("video")) { + props.videoFps = { + type: "integer", + label: "Video FPS", + description: "Frame Per Second of resulting video.", + default: 14, + max: 60, + }; + props.videoCrf = { + type: "integer", + label: "Video CRF", + description: "Constant Rate Factor. Less is more quality.", + default: 18, + max: 60, + }; + props.videoOffset = { + type: "string", + label: "Audio Offset", + description: "Audio offset. Can be negative number.", + }; + props.videoSrc = { + type: "string", + label: "Audio SRC", + description: "URL of the audio file.", + }; + } + if (this.modes.includes("gif")) { + props.gifFps = { + type: "boolean", + label: "GIF FPS", + description: "Frame per second of resulting gif.", + }; + props.gifFrames = { + type: "integer[]", + label: "GIF Frames", + description: "Select specific seconds in the timeline to be used as frames.", + }; + } + } + return props; + }, + async run({ $ }) { + + const modes = {}; + + if (this.modes) { + this.modes.forEach((item) => { + modes[item] = true; + }); + + if (this.modes.includes("html5")) { + modes.html5 = { + packed: this.html5Packed, + flexibe: this.html5Flexible, + }; + } + if (this.modes.includes("video")) { + modes.video = { + fps: this.videoFps, + crf: this.videoCrf, + audio: { + offset: parseInt(this.videoOffset), + src: this.videoSrc, + }, + }; + } + if (this.modes.includes("gif")) { + modes.html5 = { + fps: this.gifFps, + frames: this.gifFrames, + }; + } + } + + const response = await this.adrapid.createBanner({ + $, + data: { + sizeIds: this.sizeIds, + templateId: this.templateId, + modes, + overrides: parseObject(this.overrides), + sync: this.sync, + editable: this.editable, + excludeBaseSize: this.excludeBaseSize, + }, + }); + + $.export("$summary", `Banner created successfully with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/adrapid/actions/get-banner/get-banner.mjs b/components/adrapid/actions/get-banner/get-banner.mjs new file mode 100644 index 0000000000000..a03633a737330 --- /dev/null +++ b/components/adrapid/actions/get-banner/get-banner.mjs @@ -0,0 +1,27 @@ +import adrapid from "../../adrapid.app.mjs"; + +export default { + key: "adrapid-get-banner", + name: "Get Banner", + description: "Retrieves a specified banner. This action should be used after a 'create-banner' action to ensure that the banner is fully processed and ready for use. [See the documentation](https://docs.adrapid.com/api/overview)", + version: "0.0.1", + type: "action", + props: { + adrapid, + bannerId: { + propDefinition: [ + adrapid, + "bannerId", + ], + }, + }, + async run({ $ }) { + const response = await this.adrapid.getBanner({ + $, + bannerId: this.bannerId, + }); + + $.export("$summary", `Successfully retrieved banner: ${response.id}`); + return response; + }, +}; diff --git a/components/adrapid/adrapid.app.mjs b/components/adrapid/adrapid.app.mjs new file mode 100644 index 0000000000000..51a072cb9fe1e --- /dev/null +++ b/components/adrapid/adrapid.app.mjs @@ -0,0 +1,126 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "adrapid", + propDefinitions: { + bannerId: { + type: "string", + label: "Banner Id", + description: "The id of the banner.", + async options({ page }) { + const { rows: data } = await this.listBanners({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + templateId: { + type: "string", + label: "Template Id", + description: "The id of the template to create the banner.", + async options({ page }) { + const { rows: data } = await this.listTemplates({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.adrapid.com"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.api_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listBanners(opts = {}) { + return this._makeRequest({ + path: "/banners", + ...opts, + }); + }, + listTemplates(opts = {}) { + return this._makeRequest({ + path: "/templates", + ...opts, + }); + }, + getTemplate({ templateId }) { + return this._makeRequest({ + path: `/templates/${templateId}`, + }); + }, + getBanner({ + bannerId, ...opts + }) { + return this._makeRequest({ + path: `/banners/${bannerId}`, + ...opts, + }); + }, + createBanner(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/banners", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.limit = LIMIT; + params.offset = LIMIT * page++; + const { rows } = await fn({ + params, + ...opts, + }); + for (const d of rows) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = rows.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/adrapid/common/constants.mjs b/components/adrapid/common/constants.mjs new file mode 100644 index 0000000000000..ea830c15a04cb --- /dev/null +++ b/components/adrapid/common/constants.mjs @@ -0,0 +1 @@ +export const LIMIT = 100; diff --git a/components/adrapid/common/utils.mjs b/components/adrapid/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/adrapid/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/adrapid/package.json b/components/adrapid/package.json new file mode 100644 index 0000000000000..38d38d9533a54 --- /dev/null +++ b/components/adrapid/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/adrapid", + "version": "0.1.0", + "description": "Pipedream Adrapid Components", + "main": "adrapid.app.mjs", + "keywords": [ + "pipedream", + "adrapid" + ], + "homepage": "https://pipedream.com/apps/adrapid", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} diff --git a/components/adrapid/sources/new-banner-ready/new-banner-ready.mjs b/components/adrapid/sources/new-banner-ready/new-banner-ready.mjs new file mode 100644 index 0000000000000..1b95430e88a23 --- /dev/null +++ b/components/adrapid/sources/new-banner-ready/new-banner-ready.mjs @@ -0,0 +1,69 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import adrapid from "../../adrapid.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "adrapid-new-banner-ready", + name: "New Banner Ready", + description: "Emit new event when a new banner is ready.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + adrapid, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01T00:00:00.000Z"; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const response = this.adrapid.paginate({ + fn: this.adrapid.listBanners, + params: { + status: "ready", + }, + }); + + let responseArray = []; + for await (const item of response) { + if (Date.parse(item.createdAt) <= Date.parse(lastDate)) break; + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastDate(responseArray[0].createdAt); + } + + for (const banner of responseArray.reverse()) { + this.$emit(banner, { + id: banner.id, + summary: `New banner ready: ${banner.id}`, + ts: Date.parse(banner.createdAt), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, + sampleEmit, +}; diff --git a/components/adrapid/sources/new-banner-ready/test-event.mjs b/components/adrapid/sources/new-banner-ready/test-event.mjs new file mode 100644 index 0000000000000..87741b1a0af80 --- /dev/null +++ b/components/adrapid/sources/new-banner-ready/test-event.mjs @@ -0,0 +1,44 @@ +export default { + "id": "12345678-1234-1234-1234-123456789012", + "name": "Multi design - 1 Product demo", + "ownerId": "12345678-1234-1234-1234-123456789012", + "files": [ + { + "url": "https://banners.adrapid.com/12345678-1234-1234-1234-123456789012/banners/12345678-1234-1234-1234-123456789012/base/base.png", + "name": "Multi design - 1 Product demo_base.png", + "size": 125751, + "type": "png", + "files": [ + { + "path": "12345678-1234-1234-1234-123456789012/banners/12345678-1234-1234-1234-123456789012/base/base.png", + "size": 125751, + "mimeType": "image/png" + } + ], + "index": "12345678-1234-1234-1234-123456789012/banners/12345678-1234-1234-1234-123456789012/base/base.png", + "width": 300, + "height": 600, + "status": "ready", + "sizeName": "base" + } + ], + "size": null, + "status": "ready", + "failedReason": null, + "templateName": null, + "dataSource": null, + "createdAt": "2024-08-27T15:06:30.434Z", + "updatedAt": "2024-08-27T15:06:30.434Z", + "deletedAt": null, + "templateId": null, + "parentTemplateId": "12345678-1234-1234-1234-123456789012", + "filesCount": 1, + "permissions": [ + "get", + "put", + "del" + ], + "bannerId": "12345678-1234-1234-1234-123456789012", + "groupId": 302, + "updatedat": "2024-08-27T15:06:31.160Z" +} \ No newline at end of file diff --git a/components/adroll/README.md b/components/adroll/README.md index 416e674e2560b..1227383f8f50a 100644 --- a/components/adroll/README.md +++ b/components/adroll/README.md @@ -1,15 +1,14 @@ # Overview -With AdRoll, you can build a variety of targeted ads and retargeting campaigns -to reach your ideal customers. You can reach new audiences with our powerful -targeting options, and then retarget them with personalized ads to close the -sale. - -Here are some examples of what you can build with AdRoll: - -- Targeted ads: reach new audiences with our powerful targeting options -- Retargeting campaigns: reach your ideal customers with personalized ads -- Conversion tracking: track the effectiveness of your campaigns and optimize - accordingly -- Dynamic creatives: create personalized ads for each user for maximum impact -- A/B testing: test different versions of your ads to see what works best +The AdRoll API offers a suite of tools to enhance advertising efforts with targeted ad campaigns and advanced analytics. Harnessing this API within Pipedream allows you to automate interactions with AdRoll, such as synchronizing audiences, updating campaigns, and managing advertisements based on triggers from other services. By integrating AdRoll API with Pipedream, you create dynamic workflows that respond in real-time to your business data, ensuring your marketing efforts are both efficient and scalable. + +# Example Use Cases + +**Ad Audience Sync Workflow** +Automatically sync email lists from CRMs like HubSpot to AdRoll to keep your ad targeting sharp and current. When a new contact is added to a specific list in HubSpot, a Pipedream workflow gets triggered, which then updates your AdRoll audience segment, ensuring your ads reach the right users. + +**E-commerce Retargeting Trigger** +Create a workflow that triggers retargeting campaigns based on customer behavior from e-commerce platforms like Shopify. For example, when a customer abandons their cart, Pipedream detects this event from Shopify and sends the data to AdRoll, initiating a retargeting ad sequence designed to bring the customer back to complete the purchase. + +**Ad Performance Slack Alerts** +Set up a Pipedream workflow that monitors your AdRoll campaign performance metrics and sends periodic reports or alerts to a Slack channel. This workflow can be configured to send daily summaries or immediate alerts if the performance dips below certain thresholds, keeping the marketing team informed and agile in their response. diff --git a/components/adversus/README.md b/components/adversus/README.md index 38dff064cc1d2..2b8eb661c57d8 100644 --- a/components/adversus/README.md +++ b/components/adversus/README.md @@ -1,9 +1,11 @@ # Overview -The Adversus API enables you to create amazing websites and web applications. -With the Adversus API, you can: +Adversus is a powerful dialer and CRM platform tailored to streamline outbound call center operations and enhance sales processes. Leveraging the Adversus API with Pipedream opens up immense possibilities for automating call workflows, syncing lead data, and crafting custom triggers based on call outcomes or performance metrics. By creating serverless workflows on Pipedream, you can connect Adversus to a myriad of other apps and services to optimize lead management, automate repetitive tasks, and gain real-time insights into your sales funnel. -- Build custom websites -- Build custom web applications -- Create amazing user experiences -- And much more! +# Example Use Cases + +- **Automated Lead Syncing with CRM**: Whenever a new lead is added to Adversus, the workflow automatically syncs this lead to your chosen CRM, such as Salesforce or HubSpot. It ensures that any updates made by the sales team in Adversus reflect in the CRM, keeping both systems in harmony and up-to-date. + +- **Real-Time Notification for High-Value Deals**: Set up a Pipedream workflow that listens for successful deal closures in Adversus. When a high-value deal is closed, it triggers a notification in Slack or sends an email to the management team, so they can take immediate congratulatory or follow-up action. + +- **Performance Analytics Reporting**: Implement a workflow that periodically retrieves call logs and performance metrics from Adversus. The data is then processed and pushed to a Google Sheets document or a visualization tool like Tableau, providing accessible insights to the sales team for performance review and strategic planning. diff --git a/components/adversus/package.json b/components/adversus/package.json new file mode 100644 index 0000000000000..76c4aa42dc808 --- /dev/null +++ b/components/adversus/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/adversus", + "version": "0.6.0", + "description": "Pipedream adversus Components", + "main": "adversus.app.mjs", + "keywords": [ + "pipedream", + "adversus" + ], + "homepage": "https://pipedream.com/apps/adversus", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/adyen/actions/cancel-payment/cancel-payment.mjs b/components/adyen/actions/cancel-payment/cancel-payment.mjs new file mode 100644 index 0000000000000..96350172a0497 --- /dev/null +++ b/components/adyen/actions/cancel-payment/cancel-payment.mjs @@ -0,0 +1,49 @@ +import app from "../../adyen.app.mjs"; + +export default { + key: "adyen-cancel-payment", + name: "Cancel Payment", + description: "Cancels a payment that has not yet been captured. [See the documentation](https://docs.adyen.com/api-explorer/checkout/71/post/payments/(paymentpspreference)/cancels)", + version: "0.0.1", + type: "action", + props: { + app, + paymentPspReference: { + propDefinition: [ + app, + "paymentPspReference", + ], + }, + merchantAccount: { + propDefinition: [ + app, + "merchantAccount", + ], + }, + }, + methods: { + cancelPayment({ + paymentPspReference, data, + } = {}) { + return this.app.getCheckoutAPI() + .ModificationsApi + .cancelAuthorisedPaymentByPspReference(paymentPspReference, data); + }, + }, + async run({ $ }) { + const { + cancelPayment, + paymentPspReference, + merchantAccount, + } = this; + + const response = await cancelPayment({ + paymentPspReference, + data: { + merchantAccount, + }, + }); + $.export("$summary", "Successfully cancelled payment."); + return response; + }, +}; diff --git a/components/adyen/actions/capture-payment/capture-payment.mjs b/components/adyen/actions/capture-payment/capture-payment.mjs new file mode 100644 index 0000000000000..c99e6bf4eb9fa --- /dev/null +++ b/components/adyen/actions/capture-payment/capture-payment.mjs @@ -0,0 +1,68 @@ +import app from "../../adyen.app.mjs"; + +export default { + key: "adyen-capture-payment", + name: "Capture Payment", + description: "Captures an authorized payment. This is typically used for delayed capture scenarios, such as when you need to verify the order before capturing the funds.", + version: "0.0.1", + type: "action", + props: { + app, + paymentPspReference: { + propDefinition: [ + app, + "paymentPspReference", + ], + }, + merchantAccount: { + propDefinition: [ + app, + "merchantAccount", + ], + }, + amountCurrency: { + propDefinition: [ + app, + "amountCurrency", + ], + }, + amountValue: { + propDefinition: [ + app, + "amountValue", + ], + }, + }, + methods: { + capturePayment({ + paymentPspReference, data, + } = {}) { + return this.app.getCheckoutAPI() + .ModificationsApi + .captureAuthorisedPayment(paymentPspReference, data); + }, + }, + async run({ $ }) { + const { + capturePayment, + paymentPspReference, + merchantAccount, + amountCurrency, + amountValue, + } = this; + + const response = await capturePayment({ + paymentPspReference, + data: { + merchantAccount, + amount: { + currency: amountCurrency, + value: amountValue, + }, + }, + }); + + $.export("$summary", "Successfully captured payment."); + return response; + }, +}; diff --git a/components/adyen/actions/create-payment/create-payment.mjs b/components/adyen/actions/create-payment/create-payment.mjs new file mode 100644 index 0000000000000..c1a091112fb92 --- /dev/null +++ b/components/adyen/actions/create-payment/create-payment.mjs @@ -0,0 +1,92 @@ +import app from "../../adyen.app.mjs"; + +export default { + key: "adyen-create-payment", + name: "Create Payment", + description: "Creates a payment for a shopper. [See the documentation](https://docs.adyen.com/api-explorer/Checkout/71/post/payments)", + version: "0.0.1", + type: "action", + props: { + app, + merchantAccount: { + propDefinition: [ + app, + "merchantAccount", + ], + }, + amountCurrency: { + propDefinition: [ + app, + "amountCurrency", + ], + }, + amountValue: { + propDefinition: [ + app, + "amountValue", + ], + }, + reference: { + type: "string", + label: "Reference", + description: "The reference to uniquely identify a payment. This reference is used in all communication with you about the payment status. We recommend using a unique value per payment; however, it is not a requirement. If you need to provide multiple references for a transaction, separate them with hyphens (`-`). Maximum length: 80 characters.", + }, + returnUrl: { + type: "string", + label: "Return URL", + description: "The URL to return to in case of a redirection. The format depends on the channel. For more information refer the [documentation](https://docs.adyen.com/api-explorer/Checkout/71/post/payments#request-returnUrl).", + }, + paymentMethodType: { + propDefinition: [ + app, + "paymentMethodType", + ({ merchantAccount }) => ({ + merchantAccount, + }), + ], + }, + paymentMethodDetails: { + type: "object", + label: "Payment Method Details", + description: "The payment method details object required for submitting additional payment details. Should contain relevant payment details fields. For more information refer the [documentation](https://docs.adyen.com/api-explorer/Checkout/71/post/payments#request-paymentMethod).", + optional: true, + }, + }, + methods: { + createPayment({ data } = {}) { + return this.app.getCheckoutAPI() + .PaymentsApi + .payments(data); + }, + }, + async run({ $ }) { + const { + createPayment, + amountCurrency, + amountValue, + merchantAccount, + reference, + returnUrl, + paymentMethodType, + paymentMethodDetails, + } = this; + + const response = await createPayment({ + data: { + amount: { + currency: amountCurrency, + value: amountValue, + }, + merchantAccount, + reference, + returnUrl, + paymentMethod: { + ...paymentMethodDetails, + type: paymentMethodType, + }, + }, + }); + $.export("$summary", "Successfully created payment."); + return response; + }, +}; diff --git a/components/adyen/actions/refund-payment/refund-payment.mjs b/components/adyen/actions/refund-payment/refund-payment.mjs new file mode 100644 index 0000000000000..cb74c1bb9c599 --- /dev/null +++ b/components/adyen/actions/refund-payment/refund-payment.mjs @@ -0,0 +1,67 @@ +import app from "../../adyen.app.mjs"; + +export default { + key: "adyen-refund-payment", + name: "Refund Payment", + description: "Refunds a captured payment. [See the documentation](https://docs.adyen.com/api-explorer/checkout/71/post/payments/(paymentpspreference)/refunds)", + version: "0.0.1", + type: "action", + props: { + app, + paymentPspReference: { + propDefinition: [ + app, + "paymentPspReference", + ], + }, + merchantAccount: { + propDefinition: [ + app, + "merchantAccount", + ], + }, + amountCurrency: { + propDefinition: [ + app, + "amountCurrency", + ], + }, + amountValue: { + propDefinition: [ + app, + "amountValue", + ], + }, + }, + methods: { + refundPayment({ + paymentPspReference, data, + } = {}) { + return this.app.getCheckoutAPI() + .ModificationsApi + .refundCapturedPayment(paymentPspReference, data); + }, + }, + async run({ $ }) { + const { + refundPayment, + paymentPspReference, + merchantAccount, + amountCurrency, + amountValue, + } = this; + + const response = await refundPayment({ + paymentPspReference, + data: { + merchantAccount, + amount: { + currency: amountCurrency, + value: amountValue, + }, + }, + }); + $.export("$summary", `Successfully refunded payment with PSP Reference \`${response.paymentPspReference}\`.`); + return response; + }, +}; diff --git a/components/adyen/actions/submit-details/submit-details.mjs b/components/adyen/actions/submit-details/submit-details.mjs new file mode 100644 index 0000000000000..e3591015c2488 --- /dev/null +++ b/components/adyen/actions/submit-details/submit-details.mjs @@ -0,0 +1,55 @@ +import app from "../../adyen.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "adyen-submit-details", + name: "Submit Additional Payment Details", + description: "Submits additional details for a payment. [See the documentation](https://docs.adyen.com/api-explorer/Checkout/71/post/payments/details)", + version: "0.0.1", + type: "action", + props: { + app, + details: { + type: "object", + label: "Details", + description: "Use this collection to submit the details that were returned as a result of the **Create Payment** action call.", + }, + authenticationData: { + type: "string", + label: "Authentication Data", + description: "The authentication data that you received from the 3D Secure 2 SDK.", + optional: true, + }, + paymentData: { + type: "string", + label: "Payment Data", + description: "The payment data that you received from the **Create Payment** action call. [See the documentation](https://docs.adyen.com/api-explorer/Checkout/71/post/payments/details#request-paymentData).", + optional: true, + }, + }, + methods: { + submitAdditionalDetails({ data } = {}) { + return this.app.getCheckoutAPI() + .PaymentsApi + .paymentsDetails(data); + }, + }, + async run({ $ }) { + const { + submitAdditionalDetails, + details, + authenticationData, + paymentData, + } = this; + + const response = await submitAdditionalDetails({ + data: { + details: utils.parse(details), + authenticationData: utils.parse(authenticationData), + paymentData, + }, + }); + $.export("$summary", "Successfully submitted additional payment details."); + return response; + }, +}; diff --git a/components/adyen/adyen.app.mjs b/components/adyen/adyen.app.mjs new file mode 100644 index 0000000000000..3102d1707a0da --- /dev/null +++ b/components/adyen/adyen.app.mjs @@ -0,0 +1,74 @@ +import adyen from "@adyen/api-library"; + +export default { + type: "app", + app: "adyen", + propDefinitions: { + merchantAccount: { + type: "string", + label: "Merchant Account", + description: "The merchant account identifier, with which you want to process the transaction.", + }, + amountCurrency: { + type: "string", + label: "Currency", + description: "The currency of the payment amount. The three-character [ISO currency code](https://docs.adyen.com/development-resources/currency-codes).", + }, + amountValue: { + type: "integer", + label: "Value", + description: "The amount of the transaction, in [minor units](https://docs.adyen.com/development-resources/currency-codes).", + }, + paymentMethodType: { + type: "string", + label: "Payment Method Type", + description: "The payment method used for the payment. For example `scheme` or `paypal`. For the full list of payment methods, refer to the [documentation](https://docs.adyen.com/api-explorer/Checkout/71/post/payments#request-paymentMethod).", + async options({ merchantAccount }) { + if (!merchantAccount) { + return []; + } + const { paymentMethods } = await this.listPaymentMethods({ + data: { + merchantAccount, + }, + }); + return paymentMethods.map(({ + name: label, type: value, + }) => ({ + label, + value, + })); + }, + }, + paymentPspReference: { + type: "string", + label: "Payment PSP Reference", + description: "The PSP reference of the payment for which you want to perform the action.", + }, + }, + methods: { + getClient() { + const { + api_key: apiKey, + live_endpoint_url_prefix: liveEndpointUrlPrefix, + environment, + } = this.$auth; + const isLiveEnv = environment === "LIVE"; + return new adyen.Client({ + apiKey, + environment, + ...(isLiveEnv && { + liveEndpointUrlPrefix, + }), + }); + }, + getCheckoutAPI() { + return new adyen.CheckoutAPI(this.getClient()); + }, + listPaymentMethods({ + data, options, + } = {}) { + return this.getCheckoutAPI().PaymentsApi.paymentMethods(data, options); + }, + }, +}; diff --git a/components/adyen/common/utils.mjs b/components/adyen/common/utils.mjs new file mode 100644 index 0000000000000..3a9330095d785 --- /dev/null +++ b/components/adyen/common/utils.mjs @@ -0,0 +1,24 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function emptyStrToUndefined(value) { + const trimmed = typeof(value) === "string" && value.trim(); + return trimmed === "" + ? undefined + : value; +} + +function parse(value) { + const valueToParse = emptyStrToUndefined(value); + if (typeof(valueToParse) === "object" || valueToParse === undefined) { + return valueToParse; + } + try { + return JSON.parse(valueToParse); + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid JSON object."); + } +} + +export default { + parse, +}; diff --git a/components/adyen/package.json b/components/adyen/package.json new file mode 100644 index 0000000000000..4ba585f7935ab --- /dev/null +++ b/components/adyen/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/adyen", + "version": "0.1.0", + "description": "Pipedream Adyen Components", + "main": "adyen.app.mjs", + "keywords": [ + "pipedream", + "adyen" + ], + "homepage": "https://pipedream.com/apps/adyen", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@adyen/api-library": "^20.0.0" + } +} diff --git a/components/aerisweather/README.md b/components/aerisweather/README.md index c0b5cd9c82ecd..6013f8574bfde 100644 --- a/components/aerisweather/README.md +++ b/components/aerisweather/README.md @@ -1,10 +1,11 @@ # Overview -With the AerisWeather API, you can build a weather app that does the following: - -- Displays the current weather conditions for a location -- Displays a 5-day forecast for a location -- Displays a 10-day forecast for a location -- Displays radar data for a location -- Displays satellite data for a location -- Displays a map of a location with weather overlays +AerisWeather API offers a vast array of weather data, from current conditions to forecasts and severe weather alerts. Harnessing this data through Pipedream allows for the creation of personalized, automated workflows that react to weather updates in real-time. Whether it's triggering alerts based on weather changes, integrating with smart home systems, or optimizing business operations based on climatic conditions, AerisWeather's rich data sets open up a world of possibilities for automation on Pipedream. + +# Example Use Cases + +- **Severe Weather Alert System**: Create a workflow that monitors AerisWeather for severe weather alerts in specific locations. When an alert is issued, Pipedream can trigger notifications through email, SMS, or push notifications, ensuring that you or your audience are informed and can take necessary precautions. + +- **Smart Home Climate Control**: Integrate AerisWeather with smart home platforms like Philips Hue or Nest on Pipedream. Use local weather forecasts to adjust indoor temperature, lighting, or even close window blinds automatically, optimizing home comfort and energy usage based on external weather conditions. + +- **Agriculture Monitoring**: For agricultural applications, leverage AerisWeather to monitor weather conditions vital for crop health. Pipedream can automate irrigation systems to activate based on the local weather forecast, ensuring efficient water usage. It can also log this data to Google Sheets, aiding in long-term planning and analysis. diff --git a/components/aerisweather/package.json b/components/aerisweather/package.json new file mode 100644 index 0000000000000..e4d76a1916795 --- /dev/null +++ b/components/aerisweather/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/aerisweather", + "version": "0.6.0", + "description": "Pipedream aerisweather Components", + "main": "aerisweather.app.mjs", + "keywords": [ + "pipedream", + "aerisweather" + ], + "homepage": "https://pipedream.com/apps/aerisweather", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/aero_workflow/README.md b/components/aero_workflow/README.md index d456305ddf72e..82e0b94e68ad9 100644 --- a/components/aero_workflow/README.md +++ b/components/aero_workflow/README.md @@ -1,12 +1,11 @@ # Overview -You can use the Aero Workflow API to build from simpleETL processes to -multi-step workflows that encompass a variety of microservices. The following -are examples of what you can build using the Aero Workflow API: - -- A process that ingests data from a remote data source, transforms it, and - then stores it in a database. -- A workflow that aggregates data from multiple data sources, runs some - analysis on it, and then produces a report. -- A process that monitors a data source for new data, ingests it, and then - sends it to a message queue for processing by other elements of your system. +The Aero Workflow API enables the automation of task management and workflow optimization for professional service firms. Using Pipedream, you can harness this API to create powerful automations that streamline the tasks and procedures within your business. This can significantly reduce manual input, error rates, and increase overall efficiency. + +# Example Use Cases + +- **Automated Client Onboarding**: Trigger a workflow in Aero Workflow whenever a new client is added in your CRM, like Salesforce. The workflow can automatically create a series of tasks tailored to onboarding that particular client, such as sending welcome emails, setting up accounts, and scheduling initial meetings. + +- **Project Management Enhancement**: Connect Aero Workflow with project management tools like Trello or Asana. When a certain phase of a project is completed, a trigger can initiate a series of follow-up tasks in Aero Workflow, ensuring nothing falls through the cracks and keeping all team members aligned and aware of next steps. + +- **Invoice and Payment Processing**: Integrate Aero Workflow with accounting software like QuickBooks. When work is marked as completed in Aero Workflow, the API can trigger an invoice creation in QuickBooks, and upon payment receipt, a corresponding task in Aero Workflow can be marked as settled, keeping finances and task statuses in sync. diff --git a/components/aeroleads/README.md b/components/aeroleads/README.md index d09d8da4fd08b..5a80951a2868c 100644 --- a/components/aeroleads/README.md +++ b/components/aeroleads/README.md @@ -1,11 +1,11 @@ # Overview -AeroLeads API enables you to fetch data from various online sources such as -LinkedIn, Twitter, AngelList, Crunchbase and more. With AeroLeads API, you can: - -- Search for leads by name, company, location, etc. -- Get detailed information about a lead, such as name, title, company, - location, email, phone number, etc. -- Follow up with leads automatically with automatic email and phone number - detection. -- Add leads to your CRM or any other third-party applications. +AeroLeads API offers a robust solution for automating lead generation and prospecting activities. By leveraging this API on Pipedream, you can streamline the process of finding potential contacts, enriching their data, and syncing with your CRM or marketing tools. To maximize efficiency, you can set up workflows that react to specific triggers, process the data, and perform actions such as creating leads, updating contact lists, and sending personalized outreach emails. + +# Example Use Cases + +- **Lead Enrichment for New Subscribers**: When a new subscriber signs up via your website form, you can trigger a workflow that uses AeroLeads to enrich the subscriber's data with additional contact details. The enriched data can then be automatically added to your CRM. + +- **Prospect Discovery and Outreach Sequence**: Automate the discovery of new prospects by setting triggers based on specific criteria, such as job title or industry. Use AeroLeads to gather contact info and then employ an email marketing service like SendGrid on Pipedream to dispatch a series of tailored outreach emails. + +- **Real-time CRM Syncing**: Keep your sales team up to date by syncing new leads from AeroLeads to your CRM in real-time. Whenever AeroLeads identifies a potential lead, use Pipedream to add or update contact information in your CRM, ensuring your data is always current. diff --git a/components/aeroleads/package.json b/components/aeroleads/package.json new file mode 100644 index 0000000000000..1bde45d48dcb8 --- /dev/null +++ b/components/aeroleads/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/aeroleads", + "version": "0.6.0", + "description": "Pipedream aeroleads Components", + "main": "aeroleads.app.mjs", + "keywords": [ + "pipedream", + "aeroleads" + ], + "homepage": "https://pipedream.com/apps/aeroleads", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/aevent/README.md b/components/aevent/README.md new file mode 100644 index 0000000000000..c4b8c73c17a84 --- /dev/null +++ b/components/aevent/README.md @@ -0,0 +1,14 @@ +# Overview + +The AEvent API lets you automate event management and attendee engagement by connecting with your Pipedream workflows. Use the API to create and manage events, handle registrations, send out updates, and gather attendee feedback. By integrating with Pipedream, you can trigger actions based on event activities, sync with other services like CRM and email marketing tools, and analyze event data seamlessly. + +# Example Use Cases + +- **Automated Event Creation and Notification** + Whenever a new event is planned in your company, use the AEvent API to create the event and RSVP details. Then, automatically notify attendees by integrating with an email service like SendGrid. This workflow ensures that your event details are dispatched promptly to your potential audience. + +- **Dynamic Attendee Management** + Set up a workflow that listens for new registrations via the AEvent API and updates a Google Sheets spreadsheet. Utilize this data to trigger custom welcome messages via Twilio SMS or emails, providing attendees with personalized information and updates about the event. + +- **Post-Event Feedback Collection** + After an event concludes, trigger a Pipedream workflow to send out feedback forms through Typeform or Google Forms using the AEvent API to gather attendee insights. Compile the feedback for analysis and integrate it into your CRM to inform future event planning. diff --git a/components/aevent/package.json b/components/aevent/package.json index 167f2f5c845cc..6a15cffb9add5 100644 --- a/components/aevent/package.json +++ b/components/aevent/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/affinda/README.md b/components/affinda/README.md new file mode 100644 index 0000000000000..46f01c297a405 --- /dev/null +++ b/components/affinda/README.md @@ -0,0 +1,11 @@ +# Overview + +The Affinda API offers a suite of powerful tools for document analysis and extraction, enabling you to automate the processing of resumes, invoices, bank statements, and other documents. By parsing detailed information from documents, Affinda can transform unstructured data into structured, actionable insights. With Pipedream, you can integrate Affinda's capabilities into serverless workflows, connecting to hundreds of apps to automate tasks like candidate screening, financial data aggregation, or content management. + +# Example Use Cases + +- **Resume Screening Automation**: Process incoming resumes by triggering a Pipedream workflow with an email attachment or a new file in a cloud storage service. Use Affinda to analyze the resume data, then enrich and filter the candidates based on skills and experience, automatically populating a Google Sheets spreadsheet or a Trello board for review. + +- **Invoice Processing and Reconciliation**: Automatically process invoices received via email or a dedicated folder in Dropbox. The Affinda API extracts invoice data, which you can then use in Pipedream to match against purchase orders in an ERP system like QuickBooks, flag discrepancies, and update the payment status, streamlining the accounts payable process. + +- **Bank Statement Analysis for Financial Reporting**: When new bank statements are uploaded to a shared drive, trigger a Pipedream workflow to extract transaction data with Affinda. Analyze spending patterns, categorize transactions, and feed the structured data into financial reporting tools such as Xero or an internal database, aiding in the creation of comprehensive financial reports. diff --git a/components/affinity/README.md b/components/affinity/README.md index 9e389e72568a0..538ce3aa67129 100644 --- a/components/affinity/README.md +++ b/components/affinity/README.md @@ -1,9 +1,11 @@ # Overview -With the Affinity API, you can build a variety of applications that work with -Affinity data, including: +The Affinity API allows for deep integration with Affinity's CRM platform, providing access to a relationship intelligence system that helps you manage relationships, prioritize opportunities, and automate administrative tasks. Utilize the API on Pipedream to create custom, automated workflows that respond to various triggers, such as new leads or interactions, and connect with other apps to enhance data flow and functionality. -- A script to automatically generate PDFs of your design portfolio -- A tool to help you manage your Affinity account -- An app to find and connect with other Affinity users -- A service to help you keep track of your Affinity projects +# Example Use Cases + +- **Automate Lead Assignment**: When a new contact is added in Affinity, use Pipedream to automatically assign the contact to a sales rep based on specific criteria like location or industry. Send a notification via Slack or email to the assigned rep with the contact details and any relevant notes or tasks. + +- **Sync Events with Google Calendar**: Automatically create a Google Calendar event whenever an Affinity calendar event is created for a deal or a meeting. Pipedream can keep your scheduling in sync, ensuring no overlap and that all stakeholders have visibility into upcoming commitments. + +- **Aggregate Relationship Intelligence**: Whenever a new interaction is logged in Affinity, Pipedream can funnel this data to a data analysis tool like Google Sheets or Tableau. This allows for comprehensive analysis of communication patterns, helping to refine engagement strategies and identify areas for improvement. diff --git a/components/affinity/package.json b/components/affinity/package.json new file mode 100644 index 0000000000000..ed0675db15fae --- /dev/null +++ b/components/affinity/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/affinity", + "version": "0.6.0", + "description": "Pipedream affinity Components", + "main": "affinity.app.mjs", + "keywords": [ + "pipedream", + "affinity" + ], + "homepage": "https://pipedream.com/apps/affinity", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/aftership/README.md b/components/aftership/README.md new file mode 100644 index 0000000000000..463fbda32fe08 --- /dev/null +++ b/components/aftership/README.md @@ -0,0 +1,11 @@ +# Overview + +The AfterShip API on Pipedream allows you to seamlessly track shipments across various carriers, get real-time updates, and manage delivery exceptions. It's a gold mine for automating post-purchase customer communication and optimizing logistics operations. By integrating with AfterShip, you can create workflows that trigger upon shipment status changes, delays, or successful deliveries, and connect these events with other apps to streamline your e-commerce processes. + +# Example Use Cases + +- **Customer Notifications on Shipment Updates**: Enhance customer service by setting up a workflow that listens for AfterShip webhook events, and automatically sends an email or SMS via Twilio to the customer with their shipment's latest status. + +- **Sync Tracking Data with a Database**: Keep a centralized record of all shipments by creating a workflow that captures AfterShip tracking updates and stores them in a database like Google Sheets or Airtable. This can help in data analytics, reporting, and ensuring that customer support has up-to-date information. + +- **Aggregate Delivery Feedback**: Build a workflow that triggers a feedback request from customers via an email marketing platform like SendGrid when AfterShip confirms a delivery. This can help you gather valuable insights and improve the overall customer experience. diff --git a/components/agendor/README.md b/components/agendor/README.md index 6e7dba730c278..5277e1a4a1952 100644 --- a/components/agendor/README.md +++ b/components/agendor/README.md @@ -1,5 +1,11 @@ # Overview -The Agendor API can be used to build applications that allow users to manage -their contacts, sales opportunities, and tasks. Additionally, the API can be -used to create custom reports and dashboards. +The Agendor API allows for seamless integration with a CRM platform, enabling automation of sales processes, data synchronization, and enhanced analytics. By leveraging the Agendor API on Pipedream, you can orchestrate workflows that react to events in Agendor, manipulate and analyze sales data, and bridge your sales processes with other tools like email marketing services, customer support platforms, and cloud storage solutions. + +# Example Use Cases + +- **Lead Response Time Optimization**: Automatically trigger an email or SMS to new leads captured in Agendor using the Twilio app immediately after their creation. This can decrease response times and increase conversion rates. + +- **Sales Dashboard Updates**: Sync new deals and their statuses from Agendor to a Google Sheets spreadsheet in real-time. This provides a live sales dashboard that can be used for reporting and forecasting, making it easier to visualize the sales pipeline. + +- **Task Automation for Follow-ups**: Create a workflow that adds follow-up tasks in Agendor for deals that reach a certain stage, and simultaneously sends Slack notifications to the sales team to ensure no deal goes unnoticed. diff --git a/components/agentos/agentos.app.mjs b/components/agentos/agentos.app.mjs new file mode 100644 index 0000000000000..6bec609b21e59 --- /dev/null +++ b/components/agentos/agentos.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "agentos", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/agentos/package.json b/components/agentos/package.json new file mode 100644 index 0000000000000..d8639909c9e82 --- /dev/null +++ b/components/agentos/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/agentos", + "version": "0.0.1", + "description": "Pipedream agentOS Components", + "main": "agentos.app.mjs", + "keywords": [ + "pipedream", + "agentos" + ], + "homepage": "https://pipedream.com/apps/agentos", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/agenty/README.md b/components/agenty/README.md index b9b0d89933fc0..116838ca171af 100644 --- a/components/agenty/README.md +++ b/components/agenty/README.md @@ -1,12 +1,11 @@ # Overview -Agenty API enables you to integrate Agenty account with your application to -automate the web scraping. +Agenty is a powerful scraping and automation tool that enables users to extract and transform data from web pages or documents. Using the Agenty API with Pipedream, you can automate the ingestion of this data, orchestrate its flow to other applications, and build complex workflows. Whether you're aggregating content, monitoring prices, or extracting data for analysis, the Agenty API facilitates these tasks by providing structured data as an output, which you can then process with Pipedream's serverless platform. -With Agenty API you can: +# Example Use Cases -- Automate web scraping jobs -- Schedule scraping jobs -- Get data in JSON or CSV format -- Get data from behind a login -- And much more! +- **Content Aggregation Workflow**: Deploy a Pipedream workflow that triggers daily, using Agenty to scrape the latest articles or posts from specified news sources or blogs. Transform and filter this content with Pipedream's built-in code steps, and then push it to a CMS like WordPress or a database for content curation and publishing. + +- **Price Monitoring System**: Create a workflow that regularly checks product prices on e-commerce sites using Agenty. Compare the scraped pricing data with your internal pricing database using Pipedream's SQL Service to find discrepancies or opportunities. Alert the sales team via email, Slack, or SMS through Twilio when significant price changes are detected. + +- **Lead Generation Engine**: Use Agenty to extract data from online directories or professional networking sites. Process and enrich this data with Pipedream, adding additional information from services like Clearbit or FullContact. Then, automatically feed the enriched leads into a CRM platform like Salesforce or HubSpot, keeping your sales funnel filled with fresh prospects. diff --git a/components/agenty/package.json b/components/agenty/package.json new file mode 100644 index 0000000000000..3179320598383 --- /dev/null +++ b/components/agenty/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/agenty", + "version": "0.6.0", + "description": "Pipedream agenty Components", + "main": "agenty.app.mjs", + "keywords": [ + "pipedream", + "agenty" + ], + "homepage": "https://pipedream.com/apps/agenty", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/agile_crm/README.md b/components/agile_crm/README.md index 1a13b9150c9e3..019ffc135c744 100644 --- a/components/agile_crm/README.md +++ b/components/agile_crm/README.md @@ -1,13 +1,11 @@ # Overview -Agile CRM provides an API that lets developers build a wide range of -applications and integrations with the Agile CRM platform. Here are some -examples of what you can build using the Agile CRM API: - -- A custom application or integration with your existing systems -- A contact management system -- A lead management system -- A sales CRM -- A marketing automation tool -- An appointment scheduling system -- A live chat system +The Agile CRM API interacts with Agile's sales, marketing, and service software, offering a suite of features including contact management, deals, and campaign automation. On Pipedream, you can automate interactions with Agile CRM, such as syncing contacts, updating deals, or triggering campaigns. By leveraging Pipedream's serverless platform, you can connect Agile CRM to hundreds of other apps, creating custom, automated workflows that operate in real-time, and with no server maintenance required. + +# Example Use Cases + +- **Sync New Shopify Customers to Agile CRM**: Automatically add new customers from a Shopify store as contacts in Agile CRM. Use Pipedream to detect new customer events in Shopify and create corresponding contacts in Agile CRM with details like name, email, and purchase history. + +- **Automate Support Ticket Creation from Emails**: When an email is received in a Gmail inbox that matches certain criteria (e.g., subject line contains "Support:"), use Pipedream to parse the email and create a new support ticket in Agile CRM. This can help streamline customer support processes and ensure timely follow-ups. + +- **Sync Agile CRM Deals to Google Sheets**: Keep a Google Sheets spreadsheet updated with the latest deals from Agile CRM. Each time a deal is added or updated in Agile CRM, Pipedream can add or modify the corresponding row in Google Sheets, enabling real-time reporting and analysis without manual data entry. diff --git a/components/agiled/README.md b/components/agiled/README.md new file mode 100644 index 0000000000000..8fecbc0abd31d --- /dev/null +++ b/components/agiled/README.md @@ -0,0 +1,11 @@ +# Overview + +Agiled is a business management platform designed to streamline operations like CRM, project management, finances, and more. Through its API, you can automate various aspects of business management by creating, updating, and retrieving data on leads, customers, projects, tasks, invoices, and payments. Leveraging the Agiled API on Pipedream allows you to connect and automate these operations with hundreds of other apps and services to create custom workflows, facilitating data synchronization, event-driven actions, and time-saving automations. + +# Example Use Cases + +- **CRM Lead Sync with Mailchimp**: When a new lead is added to Agiled, use Pipedream's workflow to automatically add or update the lead's details in a Mailchimp audience list. This ensures your marketing campaigns are always targeting the latest prospects without manual intervention. + +- **Project Management Notifications with Slack**: Set up a Pipedream workflow to monitor project status updates in Agiled. Whenever a project's status changes, send a notification to a designated Slack channel, keeping your team informed and responsive to project phase transitions or completions in real-time. + +- **Invoice Payment Tracking with QuickBooks**: Configure a Pipedream workflow to watch for new invoices in Agiled. When an invoice is paid, trigger an event to record the payment in QuickBooks. This integration helps maintain your accounting records up to date without manual data entry, reducing errors and saving time. diff --git a/components/agiliron/actions/create-contact/create-contact.mjs b/components/agiliron/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..323227d28cfea --- /dev/null +++ b/components/agiliron/actions/create-contact/create-contact.mjs @@ -0,0 +1,291 @@ +import agiliron from "../../agiliron.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "agiliron-create-contact", + name: "Create Contact", + description: "Generates a new contact within Agiliron. [See the documentation](https://api.agiliron.com/docs/add-contact-1)", + version: "0.0.1", + type: "action", + props: { + agiliron, + salutation: { + propDefinition: [ + agiliron, + "salutation", + ], + optional: true, + }, + lastname: { + propDefinition: [ + agiliron, + "lastname", + ], + description: "The last name of the contact.", + }, + firstName: { + propDefinition: [ + agiliron, + "firstName", + ], + description: "The first name of the contact", + optional: true, + }, + officePhone: { + type: "string", + label: "Office Phone", + description: "The office phone number of the contact", + optional: true, + }, + mobile: { + propDefinition: [ + agiliron, + "mobile", + ], + description: "The mobile number of the contact", + optional: true, + }, + homePhone: { + type: "string", + label: "Home Phone", + description: "The home phone number of the contact", + optional: true, + }, + otherPhone: { + type: "string", + label: "Other Phone", + description: "An additional phone number of the contact", + optional: true, + }, + fax: { + propDefinition: [ + agiliron, + "fax", + ], + description: "The fax number of the contact", + optional: true, + }, + accountName: { + type: "string", + label: "Account Name", + description: "The account name of the contact", + optional: true, + }, + accountId: { + type: "string", + label: "Account ID", + description: "The account id of the contact", + optional: true, + }, + vendorName: { + type: "string", + label: "Vendor Name", + description: "The vendor name of the contact", + optional: true, + }, + vendorId: { + type: "string", + label: "Vendor ID", + description: "The vendor id of the contact", + optional: true, + }, + contactType: { + propDefinition: [ + agiliron, + "contactType", + ], + description: "The contact type of the contact", + optional: true, + }, + title: { + type: "string", + label: "Title", + description: "The title of the contact.", + optional: true, + }, + department: { + type: "string", + label: "Department", + description: "The department of the contact.", + optional: true, + }, + email: { + propDefinition: [ + agiliron, + "email", + ], + description: "The email address of the contact", + optional: true, + }, + yahooId: { + propDefinition: [ + agiliron, + "yahooId", + ], + description: "The Yahoo ID of the contact", + optional: true, + }, + emailOptOut: { + propDefinition: [ + agiliron, + "emailOptOut", + ], + description: "The email opt-out status of the contact", + optional: true, + }, + assignedTo: { + propDefinition: [ + agiliron, + "assignedTo", + ], + description: "The user to whom the contact is assigned", + optional: true, + }, + leadSource: { + propDefinition: [ + agiliron, + "leadSource", + ], + description: "The lead source of the contact", + optional: true, + }, + birthday: { + type: "string", + label: "Birthday", + description: "The birthday of the contact.", + optional: true, + }, + mailingStreet: { + type: "string", + label: "Mailing Street", + description: "The mailing street address of the contact", + optional: true, + }, + mailingCity: { + type: "string", + label: "Mailing City", + description: "The mailing city of the contact", + optional: true, + }, + mailingState: { + type: "string", + label: "Mailing State", + description: "The mailing state of the contact", + optional: true, + }, + mailingZip: { + type: "string", + label: "Mailing Zip", + description: "The mailing zip code of the contact", + optional: true, + }, + mailingCountry: { + type: "string", + label: "Mailing Country", + description: "The mailing country of the contact", + optional: true, + }, + otherStreet: { + type: "string", + label: "Other Street", + description: "The other street address of the contact", + optional: true, + }, + otherCity: { + type: "string", + label: "Other City", + description: "The other city of the contact", + optional: true, + }, + otherState: { + type: "string", + label: "Other State", + description: "The other state of the contact", + optional: true, + }, + otherZip: { + type: "string", + label: "Other Zip", + description: "The other zip code of the contact", + optional: true, + }, + otherCountry: { + type: "string", + label: "Other Country", + description: "The other country of the contact", + optional: true, + }, + description: { + propDefinition: [ + agiliron, + "description", + ], + description: "The description of the contact", + optional: true, + }, + customFields: { + propDefinition: [ + agiliron, + "customFields", + ], + description: "An object of custom fields for the contact. **Format: {customFieldName01: \"Value 01\"}**", + optional: true, + }, + }, + async run({ $ }) { + const parsedCustomFields = parseObject(this.customFields); + const customFields = parsedCustomFields && Object.keys(parsedCustomFields).map((key) => ({ + Name: key, + value: parsedCustomFields[key], + })); + const contact = { + Salutation: this.salutation, + LastName: this.lastname, + FirstName: this.firstName, + OfficePhone: this.officePhone, + Mobile: this.mobile, + HomePhone: this.homePhone, + OtherPhone: this.otherPhone, + Fax: this.fax, + AccountName: this.accountName, + AccountID: this.accountId, + VendorName: this.vendorName, + VendorID: this.vendorId, + ContactType: this.contactType, + Title: this.title, + Department: this.department, + Email: this.email, + YahooID: this.yahooId, + EmailOptOut: this.emailOptOut, + AssignedTo: this.assignedTo, + LeadSource: this.leadSource, + Birthday: this.birthday, + MailingStreet: this.mailingStreet, + MailingCity: this.mailingCity, + MailingState: this.mailingState, + MailingZip: this.mailingZip, + MailingCountry: this.mailingCountry, + OtherStreet: this.otherStreet, + OtherCity: this.otherCity, + OtherState: this.otherState, + OtherZip: this.otherZip, + OtherCountry: this.otherCountry, + Description: this.description, + CustomFields: { + CustomField: customFields, + }, + }; + + const response = await this.agiliron.addContact({ + $, + data: { + "Contacts": { + "Contact": contact, + }, + }, + }); + + $.export("$summary", `Successfully created contact with Id: ${response?.MCM?.parameters?.results?.message?.success_message?.contact_id}`); + return response; + }, +}; diff --git a/components/agiliron/actions/create-event/create-event.mjs b/components/agiliron/actions/create-event/create-event.mjs new file mode 100644 index 0000000000000..e0c862cdb77d0 --- /dev/null +++ b/components/agiliron/actions/create-event/create-event.mjs @@ -0,0 +1,204 @@ +import agiliron from "../../agiliron.app.mjs"; +import constants from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "agiliron-create-event", + name: "Create Event", + description: "Creates a new event within Agiliron. [See the documentation](https://api.agiliron.com/docs/add-event-2)", + version: "0.0.1", + type: "action", + props: { + agiliron, + subject: { + type: "string", + label: "Subject", + description: "The subject of the event", + }, + startdate: { + type: "string", + label: "Start Date", + description: "The start date of the event (format: MM-DD-YYYY)", + }, + starttime: { + type: "string", + label: "Start Time", + description: "The start time of the event (format: HH:MM)", + }, + durationInHours: { + type: "string", + label: "Duration in Hours", + description: "The duration of the event in hours", + optional: true, + }, + durationInMinutes: { + type: "string", + label: "Duration in Minutes", + description: "The duration of the event in minutes", + optional: true, + }, + recurringEvents: { + type: "string", + label: "Recurring Events", + description: "Details of the recurring events", + options: constants.RECURRING_EVENT_OPTIONS, + optional: true, + }, + repeatUntil: { + type: "string", + label: "Repeat Until", + description: "The end date of the recurring events (format: MM-DD-YYYY)", + optional: true, + }, + accountName: { + type: "string", + label: "Account Name", + description: "The name of the account related to the event", + optional: true, + }, + contactName: { + propDefinition: [ + agiliron, + "contactName", + ], + optional: true, + }, + sendReminder: { + type: "string", + label: "Send Reminder", + description: "Whether to send a reminder for the event", + options: constants.BOOL_OPTIONS, + optional: true, + }, + reminderDays: { + type: "string", + label: "Reminder Days", + description: "Number of days before the event to send a reminder", + optional: true, + }, + reminderHours: { + type: "string", + label: "Reminder Hours", + description: "Number of hours before the event to send a reminder", + optional: true, + }, + reminderMinutes: { + type: "string", + label: "Reminder Minutes", + description: "Number of minutes before the event to send a reminder", + optional: true, + }, + status: { + type: "string", + label: "Status", + description: "The status of the event", + options: constants.STATUS_OPTIONS, + optional: true, + }, + sendNotification: { + type: "string", + label: "Send Notification", + description: "Whether to send a notification for the event", + options: constants.BOOL_OPTIONS, + optional: true, + }, + activityType: { + type: "string", + label: "Activity Type", + description: "The type of activity for the event", + options: constants.ACTIVITY_TYPE_OPTIONS, + optional: true, + }, + location: { + type: "string", + label: "Location", + description: "The location of the event", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "The description of the event", + optional: true, + }, + customFields: { + propDefinition: [ + agiliron, + "customFields", + ], + optional: true, + }, + relatedToType: { + type: "string", + label: "Related To Type", + description: "The type of the entity related to the event", + options: constants.RELATED_TO_TYPE_OPTIONS, + optional: true, + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.relatedToType) { + props.relatedToValue = { + type: "string", + label: "Related To Value", + description: "The value of the entity related to the event", + options: async ({ page }) => { + return this.agiliron.prepareItems({ + type: this.relatedToType, + page: page + 1, + }); + }, + optional: true, + }; + } + return props; + }, + async run({ $ }) { + const parsedCustomFields = parseObject(this.customFields); + const customFields = parsedCustomFields && Object.keys(parsedCustomFields).map((key) => ({ + Name: key, + value: parsedCustomFields[key], + })); + + const event = { + Subject: this.subject, + StartDate: this.startdate, + StartTime: this.starttime, + DurationInHours: this.durationInHours, + DurationInMinutes: this.durationInMinutes, + RecurringEvents: this.recurringEvents, + RepeatUntil: this.repeatUntil + ? this.repeatUntil + : this.startdate, + AccountName: this.accountName, + ContactName: this.contactName, + RelatedToType: this.relatedToType, + RelatedToValue: this.relatedToValue, + SendReminder: this.sendReminder, + ReminderDays: this.reminderDays, + ReminderHours: this.reminderHours, + ReminderMinutes: this.reminderMinutes, + Status: this.status, + SendNotification: this.sendNotification, + ActivityType: this.activityType, + Location: this.location, + Description: this.description, + EventCustomFields: { + CustomField: customFields, + }, + }; + + const response = await this.agiliron.addEvent({ + $, + data: { + Events: { + Event: event, + }, + }, + }); + $.export("$summary", `Successfully created event with Id: ${response?.MCM?.parameters?.results?.message?.success_message?.event_id}`); + return response; + }, +}; diff --git a/components/agiliron/actions/create-lead/create-lead.mjs b/components/agiliron/actions/create-lead/create-lead.mjs new file mode 100644 index 0000000000000..64ae2e05b2ddb --- /dev/null +++ b/components/agiliron/actions/create-lead/create-lead.mjs @@ -0,0 +1,261 @@ +import { ConfigurationError } from "@pipedream/platform"; +import agiliron from "../../agiliron.app.mjs"; +import constants from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "agiliron-create-lead", + name: "Create Lead", + description: "Establishes a new lead within Agiliron. [See the documentation](https://api.agiliron.com/docs/add-lead-1)", + version: "0.0.1", + type: "action", + props: { + agiliron, + lastname: { + propDefinition: [ + agiliron, + "lastname", + ], + }, + company: { + type: "string", + label: "Company", + description: "The company of the lead", + }, + salutation: { + propDefinition: [ + agiliron, + "salutation", + ], + optional: true, + }, + firstName: { + propDefinition: [ + agiliron, + "firstName", + ], + optional: true, + }, + designation: { + type: "string", + label: "Designation", + description: "The designation of the lead", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The phone number of the lead", + optional: true, + }, + mobile: { + propDefinition: [ + agiliron, + "mobile", + ], + optional: true, + }, + fax: { + propDefinition: [ + agiliron, + "fax", + ], + optional: true, + }, + email: { + propDefinition: [ + agiliron, + "email", + ], + optional: true, + }, + website: { + type: "string", + label: "Website", + description: "The website of the lead. Without http:// or https://", + optional: true, + }, + industry: { + type: "string", + label: "Industry", + description: "The industry of the lead", + options: constants.INDUSTRY_OPTIONS, + optional: true, + }, + sicCode: { + type: "string", + label: "SIC Code", + description: "The SIC code of the lead", + optional: true, + }, + annualRevenue: { + type: "string", + label: "Annual Revenue", + description: "The annual revenue of the lead. **(US Dollar : $)**", + optional: true, + }, + numberOfEmployees: { + type: "string", + label: "Number of Employees", + description: "The number of employees of the lead", + optional: true, + }, + contactType: { + propDefinition: [ + agiliron, + "contactType", + ], + optional: true, + }, + leadSource: { + propDefinition: [ + agiliron, + "leadSource", + ], + optional: true, + }, + leadStatus: { + type: "string", + label: "Lead Status", + description: "The lead status of the lead", + options: constants.LEAD_STATUS_OPTIONS, + optional: true, + }, + rating: { + type: "string", + label: "Rating", + description: "The rating of the lead", + options: constants.RATING_OPTIONS, + optional: true, + }, + emailOptOut: { + propDefinition: [ + agiliron, + "emailOptOut", + ], + optional: true, + }, + yahooId: { + propDefinition: [ + agiliron, + "yahooId", + ], + optional: true, + }, + street: { + type: "string", + label: "Street", + description: "The street address of the lead", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "The city of the lead", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "The state of the lead", + optional: true, + }, + zip: { + type: "string", + label: "Zip", + description: "The zip code of the lead", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "The country of the lead", + optional: true, + }, + description: { + propDefinition: [ + agiliron, + "description", + ], + optional: true, + }, + assignedTo: { + propDefinition: [ + agiliron, + "assignedTo", + ], + optional: true, + }, + assignedGroupName: { + type: "string", + label: "Assigned Group Name", + description: "The group to which the contact or lead is assigned", + options: constants.ASSIGNED_TO_GROUP_OPTIONS, + optional: true, + }, + customFields: { + propDefinition: [ + agiliron, + "customFields", + ], + optional: true, + }, + }, + async run({ $ }) { + if (this.assignedTo && this.assignedGroupName) { + throw new ConfigurationError("You must provite either Assigned To or Assigned to Group Name"); + } + const parsedCustomFields = parseObject(this.customFields); + const leadCustomFields = parsedCustomFields && Object.keys(parsedCustomFields).map((key) => ({ + Name: key, + value: parsedCustomFields[key], + })); + + const lead = { + Salutation: this.salutation, + FirstName: this.firstName, + LastName: this.lastname, + Company: this.company, + Designation: this.designation, + Phone: this.phone, + Mobile: this.mobile, + Fax: this.fax, + Email: this.email, + Website: this.website, + Industry: this.industry, + SICCode: this.sicCode, + AnnualRevenue: this.annualRevenue, + NumberOfEmployees: this.numberOfEmployees, + ContactType: this.contactType, + LeadSource: this.leadSource, + LeadStatus: this.leadStatus, + Rating: this.rating, + EmailOptOut: this.emailOptOut, + YahooID: this.yahooId, + Street: this.street, + City: this.city, + State: this.state, + Zip: this.zip, + Country: this.country, + Description: this.description, + AssignedTo: this.assignedTo, + AssignedGroupName: this.assignedGroupName, + DefaultCurrency: "USD", + LeadCustomFields: { + CustomField: leadCustomFields, + }, + }; + + const response = await this.agiliron.addLead({ + $, + data: { + Leads: { + Lead: lead, + }, + }, + }); + + $.export("$summary", `Successfully created lead with ID ${response?.MCM?.parameters?.results?.message?.success_message?.lead_id}`); + return response; + }, +}; diff --git a/components/agiliron/agiliron.app.mjs b/components/agiliron/agiliron.app.mjs new file mode 100644 index 0000000000000..1ca0154b7ee93 --- /dev/null +++ b/components/agiliron/agiliron.app.mjs @@ -0,0 +1,203 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "agiliron", + propDefinitions: { + contactName: { + type: "string", + label: "Contact Name", + description: "The name of the contact related to the event", + async options({ page }) { + const { Contacts: { Contact } } = await this.getContacts({ + params: { + filter: "CreatedTime,ge,01-01-1970", + page: page + 1, + }, + }); + + return Contact.map(({ + FirstName, LastName, + }) => `${FirstName} ${LastName}`); + }, + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the lead", + }, + lastname: { + type: "string", + label: "Last Name", + description: "The last name of the lead.", + }, + salutation: { + type: "string", + label: "Salutation", + description: "The salutation of the lead", + options: constants.SALUTATION_OPTIONS, + }, + mobile: { + type: "string", + label: "Mobile", + description: "The mobile number of the lead", + }, + fax: { + type: "string", + label: "Fax", + description: "The fax number of the lead", + }, + email: { + type: "string", + label: "Email", + description: "The email address of the lead", + }, + contactType: { + type: "string", + label: "Contact Type", + description: "The contact type of the lead", + options: constants.CONTACT_TYPE_OPTIONS, + }, + leadSource: { + type: "string", + label: "Lead Source", + description: "The lead source of the lead", + options: constants.LEAD_SOURCE_OPTIONS, + }, + emailOptOut: { + type: "string", + label: "Email Opt Out", + description: "The email opt-out status of the lead", + options: constants.BOOL_OPTIONS, + }, + yahooId: { + type: "string", + label: "Yahoo ID", + description: "The Yahoo ID of the lead", + }, + description: { + type: "string", + label: "Description", + description: "The description of the lead", + }, + assignedTo: { + type: "string", + label: "Assigned To", + description: "The user to whom the lead is assigned", + options: constants.ASSIGNED_TO_OPTIONS, + }, + customFields: { + type: "object", + label: "Custom Fields", + description: "An object of custom fields for the lead. **Format: {customFieldName01: \"Value 01\"}**", + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.subdomain}.agiliron.net/agiliron/api-40`; + }, + _headers(headers) { + return { + ...headers, + Accept: "application/json", + apiKey: this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, headers, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(headers), + ...opts, + }); + }, + addContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/Contact", + ...opts, + }); + }, + addLead(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/Leads", + ...opts, + }); + }, + addEvent(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/Event", + ...opts, + }); + }, + getContacts(opts = {}) { + return this._makeRequest({ + path: "/Contact", + ...opts, + }); + }, + getLeads(opts = {}) { + return this._makeRequest({ + path: "/Leads", + ...opts, + }); + }, + getTasks(opts = {}) { + return this._makeRequest({ + path: "/Task", + ...opts, + }); + }, + async prepareItems({ + type, page, + }) { + if (type === "Invoice" || type === "Potentials") return []; + + const parsedType = type.replace(/ +/g, ""); + const typeFields = constants.TYPE_FIELDS[parsedType]; + + const response = await this._makeRequest({ + path: `/${typeFields.path || parsedType}`, + params: { + filter: `${typeFields.filterField},ge,01-01-1970`, + page, + }, + }); + + const items = response[typeFields.item][typeFields.subItem]; + return items.map((item) => { + let nameString = ""; + typeFields.names.map((name) => {nameString += `${item[name]} `;}); + return nameString; + }); + }, + async *paginate({ + fn, params = {}, fields, + }) { + let hasMore = false; + let page = 0; + + do { + try { + params.page = ++page; + const response = await fn({ + params, + }); + const pagination = response[fields.item]; + const data = pagination[fields.subItem]; + for (const d of data) { + yield d; + } + + hasMore = !(pagination.CurrentPage === pagination.TotalPages); + } catch (e) { + return false; + } + } while (hasMore); + }, + }, +}; diff --git a/components/agiliron/common/constants.mjs b/components/agiliron/common/constants.mjs new file mode 100644 index 0000000000000..2217f994be002 --- /dev/null +++ b/components/agiliron/common/constants.mjs @@ -0,0 +1,210 @@ +const SALUTATION_OPTIONS = [ + "--None--", + "Mr.", + "Ms.", + "Mrs.", + "Dr.", + "Prof.", +]; + +const INDUSTRY_OPTIONS = [ + "--None--", + "Apparel", + "Banking", + "Education", + "Electronics", + "Finance", + "Government", + "Healthcare", + "Hospitality", + "Insurance", + "Retail", + "Technology", + "Telecommunications", + "Utilities", + "Other", +]; + +const CONTACT_TYPE_OPTIONS = [ + "--None--", + "Analyst", + "Competitor", + "Customer", + "Integrator", + "Investor", + "Partner", + "Press", + "Prospect", + "Reseller", + "Other", + "Media", + "Suppliers", + "Customers", +]; + +const LEAD_SOURCE_OPTIONS = [ + "--None--", + "Cold Call", + "Existing Customer", + "Self Generated", + "Employee", + "Partner", + "Public Relations", + "Direct Mail", + "Conference", + "Trade Show", + "Web Site", + "Word of mouth", + "Other", +]; + +const LEAD_STATUS_OPTIONS = [ + "--None--", + "Attempted to Contact", + "Cold", + "Contact in Future", + "Contacted", + "Hot", + "Junk Lead", + "Lost Lead", + "Not Contacted", + "Pre Qualified", + "Qualified", + "Warm", +]; + +const RATING_OPTIONS = [ + "--None--", + "Acquired", + "Active", + "Market Failed", + "Project Cancelled", + "Shutdown", +]; + +const BOOL_OPTIONS = [ + "Yes", + "No", +]; + +const ASSIGNED_TO_OPTIONS = [ + "admin", + "posuser", +]; + +const ASSIGNED_TO_GROUP_OPTIONS = [ + "Administrator", + "Channel Management", + "Finance", + "Fulfillment", + "Management", + "Marketing", + "Product Management", + "Production", + "Purchasing", + "Retail Management", + "Retail Sales", + "Sales", + "Sales Reps", + "Support", + "Warehouse Management", +]; + +const RECURRING_EVENT_OPTIONS = [ + "--None--", + "Daily", + "Weekly", + "Monthly", + "Yearly", +]; + +const RELATED_TO_TYPE_OPTIONS = [ + "Leads", + "Accounts", + "Potentials", + "Quotes", + "Purchase Order", + "Sales Order", + "Invoice", +]; + +const STATUS_OPTIONS = [ + "Planned", + "Held", + "Not Held", +]; + +const ACTIVITY_TYPE_OPTIONS = [ + "Call", + "Meeting", +]; + +const TYPE_FIELDS = { + Leads: { + item: "Leads", + subItem: "Lead", + names: [ + "FirstName", + "LastName", + ], + filterField: "CreatedTime", + }, + Accounts: { + item: "Accounts", + subItem: "Account", + names: [ + "AccountName", + ], + filterField: "CreatedTime", + }, + Quotes: { + path: "Quote", + item: "Quotes", + subItem: "Quote", + names: [ + "Subject", + ], + filterField: "OrderDate", + }, + PurchaseOrder: { + item: "POOrders", + subItem: "Order", + names: [ + "Subject", + ], + filterField: "OrderDate", + }, + SalesOrder: { + item: "Orders", + subItem: "Order", + names: [ + "Subject", + ], + filterField: "OrderDate", + }, + Contacts: { + item: "Contacts", + subItem: "Contact", + }, + Tasks: { + item: "Tasks", + subItem: "Task", + }, +}; + +export default { + CONTACT_TYPE_OPTIONS, + INDUSTRY_OPTIONS, + LEAD_SOURCE_OPTIONS, + LEAD_STATUS_OPTIONS, + SALUTATION_OPTIONS, + RATING_OPTIONS, + BOOL_OPTIONS, + ASSIGNED_TO_OPTIONS, + ASSIGNED_TO_GROUP_OPTIONS, + RECURRING_EVENT_OPTIONS, + RELATED_TO_TYPE_OPTIONS, + STATUS_OPTIONS, + ACTIVITY_TYPE_OPTIONS, + TYPE_FIELDS, +}; diff --git a/components/agiliron/common/utils.mjs b/components/agiliron/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/agiliron/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/agiliron/package.json b/components/agiliron/package.json new file mode 100644 index 0000000000000..a9d5a7e44e1ed --- /dev/null +++ b/components/agiliron/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/agiliron", + "version": "0.1.0", + "description": "Pipedream Agiliron Components", + "main": "agiliron.app.mjs", + "keywords": [ + "pipedream", + "agiliron" + ], + "homepage": "https://pipedream.com/apps/agiliron", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} diff --git a/components/agiliron/sources/common/base.mjs b/components/agiliron/sources/common/base.mjs new file mode 100644 index 0000000000000..d00f77a4305d1 --- /dev/null +++ b/components/agiliron/sources/common/base.mjs @@ -0,0 +1,67 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import agiliron from "../../agiliron.app.mjs"; + +export default { + props: { + agiliron, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "01-01-1970"; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const fields = this.getFields(); + + const response = this.agiliron.paginate({ + fn: this.getFunction(), + params: { + filter: `CreatedTime,gt,${lastDate}`, + }, + fields, + }); + + let responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + responseArray = responseArray.sort( + (a, b) => (Date.parse(b[fields.date]) - Date.parse(a[fields.date])), + ); + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastDate(responseArray[0][fields.date]); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item[fields.id], + summary: this.getSummary(item), + ts: Date.parse(item[fields.date]), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/agiliron/sources/new-contact-created/new-contact-created.mjs b/components/agiliron/sources/new-contact-created/new-contact-created.mjs new file mode 100644 index 0000000000000..49dbbc0cdcf15 --- /dev/null +++ b/components/agiliron/sources/new-contact-created/new-contact-created.mjs @@ -0,0 +1,30 @@ +import constants from "../../common/constants.mjs"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "agiliron-new-contact-created", + name: "New Contact Created", + description: "Emit new event when a new contact is created in Agiliron. [See the documentation](https://api.agiliron.com/docs/read-contact-1)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFields() { + return { + date: "CreatedTime", + id: "ContactId", + ...constants.TYPE_FIELDS.Contacts, + }; + }, + getFunction() { + return this.agiliron.getContacts; + }, + getSummary(contact) { + return `New contact: ${contact.FirstName} ${contact.LastName}`; + }, + }, + sampleEmit, +}; diff --git a/components/agiliron/sources/new-contact-created/test-event.mjs b/components/agiliron/sources/new-contact-created/test-event.mjs new file mode 100644 index 0000000000000..067594ab87d9b --- /dev/null +++ b/components/agiliron/sources/new-contact-created/test-event.mjs @@ -0,0 +1,58 @@ +export default { + "ContactId": "123", + "Salutation": "--None--", + "FirstName": "Test", + "LastName": "New Contact", + "OfficePhone": "56412356", + "Mobile": "7412536589", + "HomePhone": "56412356", + "OtherPhone": "", + "Fax": "5689746", + "AccountName": "Account1", + "AccountID": "234", + "VendorName": "Vendor1", + "VendorID": "30", + "ContactType": "Competitor", + "Title": "Test Title", + "Department": "Test Department", + "Email": "abctest123@gmail.com", + "ReportsTo": "", + "ReportsToID": "", + "Assistant": "", + "YahooID": "abc@yahoo.com", + "AssistantPhone": "5989654", + "DoNotCall": "No", + "EmailOptOut": "No", + "AssignedToUser": "Administrator", + "LeadSource": "Self Generated", + "Birthday": "01-21-2020", + "ContactProfile": "Buyer", + "MailingStreet": "1515 Clay Street", + "MailingCity": "Oakland", + "MailingState": "CA", + "MailingZip": "94624", + "MailingCountry": "United States", + "OtherStreet": "1515 Clay Street", + "OtherCity": "Oakland", + "OtherState": "CA", + "OtherZip": "94624", + "OtherCountry": "United States", + "CreatedTime": "01-24-2020 05:32:01", + "CreatedTimeUTC": "01-24-2020 15:01:35", + "ModifiedTime": "01-24-2020 05:32:01", + "ModifiedTimeUTC": "01-24-2020 15:05:56", + "Description": "The Description Goes here.", + "B2BCustomer": "Yes", + "B2BChannels": "Ebay", + "CustomFields": { + "CustomField": [{ + "Name": "CustomFied1", + "Value": "Custom info 1" + }, + { + "Name": "CustomFied2", + "Value": "Custom info 2" + } + ] + } +} \ No newline at end of file diff --git a/components/agiliron/sources/new-lead-created/new-lead-created.mjs b/components/agiliron/sources/new-lead-created/new-lead-created.mjs new file mode 100644 index 0000000000000..0a36a74963332 --- /dev/null +++ b/components/agiliron/sources/new-lead-created/new-lead-created.mjs @@ -0,0 +1,30 @@ +import constants from "../../common/constants.mjs"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "agiliron-new-lead-created", + name: "New Lead Created", + description: "Emit new event when a new lead is created in Agiliron.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFields() { + return { + date: "CreatedTime", + id: "LeadId", + ...constants.TYPE_FIELDS.Leads, + }; + }, + getFunction() { + return this.agiliron.getLeads; + }, + getSummary(lead) { + return `New lead: ${lead.FirstName} ${lead.LastName}`; + }, + }, + sampleEmit, +}; diff --git a/components/agiliron/sources/new-lead-created/test-event.mjs b/components/agiliron/sources/new-lead-created/test-event.mjs new file mode 100644 index 0000000000000..c2141ab665aef --- /dev/null +++ b/components/agiliron/sources/new-lead-created/test-event.mjs @@ -0,0 +1,48 @@ +export default { + "LeadId": "123", + "Salutation": "--None--", + "FirstName": "Test", + "LastName": "New Lead", + "Company": "Test Company", + "Designation": "Test Title", + "Phone": "56412356", + "Mobile": "7412536589", + "Fax": "5689746", + "Email": "abctest123@gmail.com", + "Website": "", + "Industry": "Recreation", + "SICCode": "10", + "AnnualRevenue": "65", + "NumberOfEmployees": "25", + "ContactType": "Customer", + "LeadSource": "Self Generated", + "LeadStatus": "Contacted", + "Rating": "Acquired", + "EmailOptOut": "No", + "YahooID": "abc@yahoo.com", + "CreatedTime": "01-24-2020 05:01:35", + "CreatedTimeUTC": "01-24-2020 15:01:35", + "ModifiedTime": "01-24-2020 05:05:56", + "ModifiedTimeUTC": "01-24-2020 15:05:56", + "Street": "1515 Clay Street", + "City": "Oakland", + "State": "CA", + "Zip": "94624", + "Country": "United States", + "Description": "The Description Goes here.", + "AssignedTo": "admin", + "AssignedGroupName": "", + "DefaultCurrency": "USD", + "LeadCustomFields": { + "CustomField": [ + { + "Name": "CustomFied1", + "Value": "Custom info 1" + }, + { + "Name": "CustomFied2", + "Value": "Custom info 2" + } + ] + } +} \ No newline at end of file diff --git a/components/agiliron/sources/new-task-created/new-task-created.mjs b/components/agiliron/sources/new-task-created/new-task-created.mjs new file mode 100644 index 0000000000000..b9d517fa579fd --- /dev/null +++ b/components/agiliron/sources/new-task-created/new-task-created.mjs @@ -0,0 +1,30 @@ +import constants from "../../common/constants.mjs"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "agiliron-new-task-created", + name: "New Task Created", + description: "Emit new event when a new task is created in Agiliron.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFields() { + return { + date: "CreatedTime", + id: "TaskId", + ...constants.TYPE_FIELDS.Tasks, + }; + }, + getFunction() { + return this.agiliron.getTasks; + }, + getSummary(task) { + return `New task: ${task.Subject}`; + }, + }, + sampleEmit, +}; diff --git a/components/agiliron/sources/new-task-created/test-event.mjs b/components/agiliron/sources/new-task-created/test-event.mjs new file mode 100644 index 0000000000000..01f47d943b0fa --- /dev/null +++ b/components/agiliron/sources/new-task-created/test-event.mjs @@ -0,0 +1,32 @@ +export default { + "TaskId": "3422", + "Subject": "Test New Task", + "StartDate": "01-24-2020", + "StartTime": "05:47:00", + "DueDate": "01-25-2020", + "AccountName": "Account1", + "ContactName": "Contact1", + "RelatedToType": "Accounts", + "RelatedToValue": "Account1", + "Status": "In Progress", + "SendNotification": "No", + "ActivityType": "Task", + "Priority": "High", + "CreatedTime": "01-24-2020 05:51:01", + "CreatedTimeUTC": "01-24-2020 15:01:35", + "ModifiedTime": "01-24-2020 05:53:00", + "ModifiedTimeUTC": "01-24-2020 15:05:56", + "Description": "The Description Goes here.", + "TaskCustomFields": { + "CustomField": [ + { + "Name": "CustomFied1", + "Value": "Custom info 1" + }, + { + "Name": "CustomFied2", + "Value": "Custom info 2" + } + ] + } +} \ No newline at end of file diff --git a/components/agility_cms/actions/get-content-items/get-content-items.mjs b/components/agility_cms/actions/get-content-items/get-content-items.mjs new file mode 100644 index 0000000000000..927bb20da1333 --- /dev/null +++ b/components/agility_cms/actions/get-content-items/get-content-items.mjs @@ -0,0 +1,28 @@ +import app from "../../agility_cms.app.mjs"; + +export default { + key: "agility_cms-get-content-items", + name: "Get Content Items", + description: "Retrieves all content items. [See the documentation](https://api.aglty.io/swagger/index.html#operations-Sync-get__guid___apitype___locale__sync_items)", + version: "0.0.1", + type: "action", + props: { + app, + locale: { + propDefinition: [ + app, + "locale", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getItems({ + $, + locale: this.locale, + }); + + $.export("$summary", `Successfully retrieved ${response.items.length} Items`); + + return response; + }, +}; diff --git a/components/agility_cms/actions/get-content-models/get-content-models.mjs b/components/agility_cms/actions/get-content-models/get-content-models.mjs new file mode 100644 index 0000000000000..5521d2896dc6e --- /dev/null +++ b/components/agility_cms/actions/get-content-models/get-content-models.mjs @@ -0,0 +1,21 @@ +import app from "../../agility_cms.app.mjs"; + +export default { + key: "agility_cms-get-content-models", + name: "Get Content Models", + description: "Retrieve content models for the Agility instance. [See the documentation](https://api.aglty.io/swagger/index.html#operations-ContentModels-get__guid___apitype__contentmodels)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.getContentModels({ + $, + }); + + $.export("$summary", `Successfully retrieved ${Object.keys(response).length} content models`); + + return response; + }, +}; diff --git a/components/agility_cms/actions/get-item/get-item.mjs b/components/agility_cms/actions/get-item/get-item.mjs new file mode 100644 index 0000000000000..fc35ced2d6cc8 --- /dev/null +++ b/components/agility_cms/actions/get-item/get-item.mjs @@ -0,0 +1,38 @@ +import app from "../../agility_cms.app.mjs"; + +export default { + key: "agility_cms-get-item", + name: "Get Item Details", + description: "Get details of the specified item. [See the documentation](https://api.aglty.io/swagger/index.html#operations-Item-get__guid___apitype___locale__item__id_)", + version: "0.0.1", + type: "action", + props: { + app, + locale: { + propDefinition: [ + app, + "locale", + ], + }, + itemId: { + propDefinition: [ + app, + "itemId", + (c) => ({ + locale: c.locale, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.app.getItem({ + $, + locale: this.locale, + itemId: this.itemId, + }); + + $.export("$summary", `Successfully retrieved details of item ID '${response.contentID}'`); + + return response; + }, +}; diff --git a/components/agility_cms/agility_cms.app.mjs b/components/agility_cms/agility_cms.app.mjs new file mode 100644 index 0000000000000..c32458f84bb42 --- /dev/null +++ b/components/agility_cms/agility_cms.app.mjs @@ -0,0 +1,76 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "agility_cms", + propDefinitions: { + locale: { + type: "string", + label: "Locale", + description: "The locale code you want to retrieve content for (it must already be in `Locales Settings` of your Agility account)", + options: constants.LOCALE_OPTIONS, + default: "en-us", + }, + itemId: { + type: "string", + label: "Item ID", + description: "ID of the item to get details of", + async options({ locale }) { + const response = await this.getItems({ + locale, + }); + const itemsIds = response.items; + return itemsIds.map(({ + contentID, properties, + }) => ({ + value: contentID, + label: properties.referenceName, + })); + }, + }, + }, + methods: { + _baseUrl() { + return `${this.$auth.api_url}/${this.$auth.guid}/${this.$auth.api_type}`; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "APIKey": `${this.$auth.api_key}`, + }, + }); + }, + async getContentModels(args = {}) { + return this._makeRequest({ + path: "/contentmodels", + ...args, + }); + }, + async getItems({ + locale, ...args + }) { + return this._makeRequest({ + path: `/${locale}/sync/items`, + ...args, + }); + }, + async getItem({ + locale, itemId, ...args + }) { + return this._makeRequest({ + path: `/${locale}/item/${itemId}`, + ...args, + }); + }, + }, +}; diff --git a/components/agility_cms/common/constants.mjs b/components/agility_cms/common/constants.mjs new file mode 100644 index 0000000000000..26c4cca06e8fc --- /dev/null +++ b/components/agility_cms/common/constants.mjs @@ -0,0 +1,308 @@ +export default { + LOCALE_OPTIONS: [ + { + value: "af-ZA", + label: "Afrikaans (South Africa)", + }, + { + value: "es-HN", + label: "Spanish (Honduras)", + }, + { + value: "ar-AE", + label: "Arabic (United Arab Emirates)", + }, + { + value: "es-LA", + label: "Spanish (Spanish)", + }, + { + value: "ar-AR", + label: "Arabic (Arabic)", + }, + { + value: "es-MX", + label: "Spanish (Mexico)", + }, + { + value: "ar-BH", + label: "Arabic (Bahrain)", + }, + { + value: "es-NI", + label: "Spanish (Nicaragua)", + }, + { + value: "ar-DJ", + label: "Arabic (Djibouti)", + }, + { + value: "es-PA", + label: "Spanish (Panama)", + }, + { + value: "ar-DZ", + label: "Arabic (Algeria)", + }, + { + value: "es-PE", + label: "Spanish (Peru)", + }, + { + value: "ar-EG", + label: "Arabic (Egypt)", + }, + { + value: "es-PR", + label: "Spanish (Puerto Rico)", + }, + { + value: "ar-EH", + label: "Arabic (Western Sahara)", + }, + { + value: "es-PY", + label: "Spanish (Paraguay)", + }, + { + value: "ar-ER", + label: "Arabic (Eritrea)", + }, + { + value: "es-SV", + label: "Spanish (El Salvador)", + }, + { + value: "ar-IL", + label: "Arabic (Israel)", + }, + { + value: "es-US", + label: "Spanish (United States)", + }, + { + value: "ar-IQ", + label: "Arabic (Iraq)", + }, + { + value: "es-UY", + label: "Spanish (Uruguay)", + }, + { + value: "ar-JO", + label: "Arabic (Jordan)", + }, + { + value: "es-VE", + label: "Spanish (Venezuela)", + }, + { + value: "ar-KM", + label: "Arabic (Comoros)", + }, + { + value: "et-EE", + label: "Estonian (Estonia)", + }, + { + value: "ar-KW", + label: "Arabic (Kuwait)", + }, + { + value: "eu-ES", + label: "Basque (Spain)", + }, + { + value: "ar-LB", + label: "Arabic (Lebanon)", + }, + { + value: "fa-IR", + label: "Persian (Iran)", + }, + { + value: "ar-LY", + label: "Arabic (Libya)", + }, + { + value: "fb-LT", + label: "Leet Speak", + }, + { + value: "ar-MA", + label: "Arabic (Morocco)", + }, + { + value: "fi-FI", + label: "Finnish (Finland)", + }, + { + value: "ar-MR", + label: "Arabic (Mauritania)", + }, + { + value: "fo-FO", + label: "Faroese (Faeroe Islands)", + }, + { + value: "ar-OM", + label: "Arabic (Oman)", + }, + { + value: "fr-BE", + label: "French (Belgium)", + }, + { + value: "ar-PS", + label: "Arabic (West Bank and Gaza)", + }, + { + value: "fr-BF", + label: "French (Burkina Faso)", + }, + { + value: "ar-QA", + label: "Arabic (Qatar)", + }, + { + value: "fr-BI", + label: "French (Burundi)", + }, + { + value: "ar-SA", + label: "Arabic (Saudi Arabia)", + }, + { + value: "fr-BJ", + label: "French (Benin)", + }, + { + value: "ar-SD", + label: "Arabic (Sudan)", + }, + { + value: "fr-CA", + label: "French (Canada)", + }, + { + value: "ar-SO", + label: "Arabic (Somalia)", + }, + { + value: "fr-CD", + label: "French (Dem. Rep. Congo)", + }, + { + value: "ar-SY", + label: "Arabic (Syria)", + }, + { + value: "fr-CF", + label: "French (Central African Republic)", + }, + { + value: "ar-TD", + label: "Arabic (Chad)", + }, + { + value: "fr-CG", + label: "French (Congo)", + }, + { + value: "ar-TN", + label: "Arabic (Tunisia)", + }, + { + value: "fr-CH", + label: "French (Switzerland)", + }, + { + value: "ar-YE", + label: "Arabic (Yemen)", + }, + { + value: "fr-CI", + label: "French (Côte d'Ivoire)", + }, + { + value: "az-AZ", + label: "Azerbaijani (Azerbaijan)", + }, + { + value: "fr-CM", + label: "French (Cameroon)", + }, + { + value: "be-BY", + label: "Belarusian (Belarus)", + }, + { + value: "fr-DJ", + label: "French (Djibouti)", + }, + { + value: "bg-BG", + label: "Bulgarian (Bulgaria)", + }, + { + value: "fr-FR", + label: "French (France)", + }, + { + value: "bn-IN", + label: "Bengali (India)", + }, + { + value: "fr-GA", + label: "French (Gabon)", + }, + { + value: "bs-BA", + label: "Bosnian (Bosnia and Herzegovina)", + }, + { + value: "fr-GD", + label: "French (Grenada)", + }, + { + value: "ca-AD", + label: "Catalan (Andorra)", + }, + { + value: "fr-GF", + label: "French (French Guianna)", + }, + { + value: "ca-ES", + label: "Catalan (Spain)", + }, + { + value: "fr-GN", + label: "French (Guinea)", + }, + { + value: "cs-CZ", + label: "Czech (Czech Republic)", + }, + { + value: "fr-GP", + label: "French (Guadeloupe)", + }, + { + value: "cs-SK", + label: "Czech (Slovak Republic)", + }, + { + value: "fr-GQ", + label: "French (Equatorial Guinea)", + }, + { + value: "cy-GB", + label: "Welsh (United Kingdom)", + }, + { + value: "fr-HT", + label: "French (Haiti)", + }, + ], +}; diff --git a/components/agility_cms/package.json b/components/agility_cms/package.json new file mode 100644 index 0000000000000..530c8c9449b53 --- /dev/null +++ b/components/agility_cms/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/agility_cms", + "version": "0.1.0", + "description": "Pipedream Agility CMS Components", + "main": "agility_cms.app.mjs", + "keywords": [ + "pipedream", + "agility_cms" + ], + "homepage": "https://pipedream.com/apps/agility_cms", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.2" + } +} diff --git a/components/agrello/actions/get-document/get-document.mjs b/components/agrello/actions/get-document/get-document.mjs new file mode 100644 index 0000000000000..918475503069a --- /dev/null +++ b/components/agrello/actions/get-document/get-document.mjs @@ -0,0 +1,35 @@ +import agrello from "../../agrello.app.mjs"; + +export default { + key: "agrello-get-document", + name: "Get Document", + description: "Get a document in Agrello. [See the documentation](https://api.agrello.io/public/webjars/swagger-ui/index.html)", + version: "0.0.1", + type: "action", + props: { + agrello, + folderId: { + propDefinition: [ + agrello, + "folderId", + ], + }, + documentId: { + propDefinition: [ + agrello, + "documentId", + ({ folderId }) => ({ + folderId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.agrello.getDocument({ + $, + documentId: this.documentId, + }); + $.export("$summary", `Successfully retrieved document with ID ${this.documentId}`); + return response; + }, +}; diff --git a/components/agrello/agrello.app.mjs b/components/agrello/agrello.app.mjs new file mode 100644 index 0000000000000..a12c7c07fdd1c --- /dev/null +++ b/components/agrello/agrello.app.mjs @@ -0,0 +1,144 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "agrello", + propDefinitions: { + folderId: { + type: "string", + label: "Folder ID", + description: "The ID of the folder", + async options({ page }) { + return await this.listFolders({ + params: { + page, + }, + }); + }, + }, + documentId: { + type: "string", + label: "Document ID", + description: "The ID of the document", + async options({ + folderId, page, + }) { + const { content } = await this.listDocuments({ + folderId, + params: { + page, + }, + }); + return content.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.agrello.io/public/v3"; + }, + _headers(headers = {}) { + return { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(headers), + ...opts, + }); + }, + async listFolders() { + const { content } = await this._makeRequest({ + path: "/folders", + }); + + const folders = []; + for (const parent of content) { + folders.push({ + label: `${parent.name}`, + value: parent.id, + }); + folders.push(...await this.getSubFolders(parent.name, parent.id)); + } + + return folders; + + }, + async getSubFolders(parentName, parentId) { + const folders = []; + const { subspaces } = await this._makeRequest({ + path: `/folders/${parentId}/folders`, + }); + for (const folder of subspaces) { + const label = `${parentName} - ${folder.name}`; + folders.push({ + label, + value: folder.id, + }); + folders.push(...await this.getSubFolders(label, folder.id)); + } + return folders; + }, + listDocuments({ + folderId, ...opts + }) { + return this._makeRequest({ + path: `/folders/${folderId}/containers`, + ...opts, + }); + }, + getDocument({ documentId }) { + return this._makeRequest({ + path: `/containers/${documentId}`, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteWebhook(hookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${hookId}`, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = page++; + const { content } = await fn({ + params, + ...opts, + }); + for (const d of content) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = content.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/agrello/common/utils.mjs b/components/agrello/common/utils.mjs new file mode 100644 index 0000000000000..0cd1a12b6a4ba --- /dev/null +++ b/components/agrello/common/utils.mjs @@ -0,0 +1,31 @@ +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; + +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/agrello/package.json b/components/agrello/package.json new file mode 100644 index 0000000000000..ad8b43dc433d7 --- /dev/null +++ b/components/agrello/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/agrello", + "version": "0.1.0", + "description": "Pipedream Agrello Components", + "main": "agrello.app.mjs", + "keywords": [ + "pipedream", + "agrello" + ], + "homepage": "https://pipedream.com/apps/agrello", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} + diff --git a/components/agrello/sources/common/base.mjs b/components/agrello/sources/common/base.mjs new file mode 100644 index 0000000000000..82aa089f5ecef --- /dev/null +++ b/components/agrello/sources/common/base.mjs @@ -0,0 +1,44 @@ +import agrello from "../../agrello.app.mjs"; + +export default { + props: { + agrello, + http: { + type: "$.interface.http", + customResponse: false, + }, + db: "$.service.db", + }, + methods: { + _setHookId(hookId) { + this.db.set("webhookId", hookId); + }, + _getHookId() { + return this.db.get("webhookId"); + }, + }, + hooks: { + async activate() { + const data = await this.agrello.createWebhook({ + data: { + event: this.getEvent(), + url: this.http.endpoint, + }, + }); + + this._setHookId(data.id); + }, + async deactivate() { + const webhookId = this._getHookId(); + await this.agrello.deleteWebhook(webhookId); + }, + }, + async run({ body: { event } }) { + const ts = Date.parse(new Date()); + this.$emit(event, { + id: `${event.containerId}-${ts}`, + summary: this.getSummary(event), + ts: ts, + }); + }, +}; diff --git a/components/agrello/sources/new-document/new-document.mjs b/components/agrello/sources/new-document/new-document.mjs new file mode 100644 index 0000000000000..d90003afb00f4 --- /dev/null +++ b/components/agrello/sources/new-document/new-document.mjs @@ -0,0 +1,78 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import agrello from "../../agrello.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "agrello-new-document", + name: "New Document Added to Folder", + description: "Emit new event when a user adds a document to a specific folder. [See the documentation](https://api.agrello.io/public/webjars/swagger-ui/index.html)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + agrello, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + folderId: { + propDefinition: [ + agrello, + "folderId", + ], + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + + const response = this.agrello.paginate({ + fn: this.agrello.listDocuments, + folderId: this.folderId, + params: { + sort: "createdAt,DESC", + }, + maxResults, + }); + + let responseArray = []; + for await (const item of response) { + if (Date.parse(item.createdAt) <= lastDate) break; + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastDate(Date.parse(responseArray[0].createdAt)); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: `New Document: ${item.name}`, + ts: Date.parse(item.createdAt), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, + sampleEmit, +}; diff --git a/components/agrello/sources/new-document/test-event.mjs b/components/agrello/sources/new-document/test-event.mjs new file mode 100644 index 0000000000000..20483cbb724ee --- /dev/null +++ b/components/agrello/sources/new-document/test-event.mjs @@ -0,0 +1,70 @@ +export default { + "id": "string", + "name": "string", + "status": "string", + "outputType": "PDF", + "size": 0, + "createdAt": "2024-09-09T21:28:21.391Z", + "updatedAt": "2024-09-09T21:28:21.391Z", + "parties": [ + { + "id": "string", + "createdAt": "2024-09-09T21:28:21.391Z", + "updatedAt": "2024-09-09T21:28:21.391Z", + "role": "VIEWER", + "identityId": "string", + "username": "string" + } + ], + "invitations": [ + { + "id": "string", + "createdAt": "2024-09-09T21:28:21.391Z", + "email": "string", + "expiresAt": "2024-09-09T21:28:21.391Z", + "role": "VIEWER" + } + ], + "files": [ + { + "id": "string", + "name": "string", + "mimeType": "string", + "size": 0, + "createdAt": "2024-09-09T21:28:21.391Z", + "updatedAt": "2024-09-09T21:28:21.391Z", + "variables": [ + { + "id": "string", + "name": "string", + "type": "TEXT", + "value": "string" + } + ] + } + ], + "signatures": [ + { + "id": "string", + "identityId": "string", + "username": "string", + "certificate": "string", + "certificateHash": "string", + "signature": "string", + "visualSignatureUrl": "string", + "contentHash": "string", + "signingTimestamp": "2024-09-09T21:28:21.391Z", + "type": "BASIC", + "signingMethod": "BARE", + "signatureProvider": "AGRELLO_ID", + "displayName": "string" + } + ], + "folderId": "string", + "workspaceId": "string", + "metadata": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string" + } +} \ No newline at end of file diff --git a/components/agrello/sources/new-signature-instant/new-signature-instant.mjs b/components/agrello/sources/new-signature-instant/new-signature-instant.mjs new file mode 100644 index 0000000000000..43f156fbc87c3 --- /dev/null +++ b/components/agrello/sources/new-signature-instant/new-signature-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "agrello-new-signature-instant", + name: "New Signature Added to Document (Instant)", + description: "Emit new event when a signature is added to a container.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return "DOCUMENT_SIGNATURE_ADDED"; + }, + getSummary(event) { + return `New signature added to container: ${event.containerId}`; + }, + }, + sampleEmit, +}; diff --git a/components/agrello/sources/new-signature-instant/test-event.mjs b/components/agrello/sources/new-signature-instant/test-event.mjs new file mode 100644 index 0000000000000..63e08339a8846 --- /dev/null +++ b/components/agrello/sources/new-signature-instant/test-event.mjs @@ -0,0 +1,10 @@ +export default { + "containerId": "068e7102-3565-4591-bf01-71f74e91dd1e", + "ownerId": "068e7102-3565-4591-bf01-71f74e91dd1e", + "userId": "068e7102-3565-4591-bf01-71f74e91dd1e", + "username": "email@test.com", + "spaceId": "068e7102-3565-4591-bf01-71f74e91dd1e", + "spaceOwnerId": "068e7102-3565-4591-bf01-71f74e91dd1e", + "workspaceId": "068e7102-3565-4591-bf01-71f74e91dd1e", + "workspaceOwnerId": "068e7102-3565-4591-bf01-71f74e91dd1e" +} \ No newline at end of file diff --git a/components/agrello/sources/new-signed-document-instant/new-signed-document-instant.mjs b/components/agrello/sources/new-signed-document-instant/new-signed-document-instant.mjs new file mode 100644 index 0000000000000..a53415dccb37d --- /dev/null +++ b/components/agrello/sources/new-signed-document-instant/new-signed-document-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "agrello-new-signed-document-instant", + name: "New Signed Document (Instant)", + description: "Emit new event when a given document is signed by all parties.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return "DOCUMENT_SIGNED"; + }, + getSummary(event) { + return `Document signed: ${event.containerId}`; + }, + }, + sampleEmit, +}; diff --git a/components/agrello/sources/new-signed-document-instant/test-event.mjs b/components/agrello/sources/new-signed-document-instant/test-event.mjs new file mode 100644 index 0000000000000..f30c2f5216387 --- /dev/null +++ b/components/agrello/sources/new-signed-document-instant/test-event.mjs @@ -0,0 +1,9 @@ +export default { + "containerId": "068e7102-3565-4591-bf01-71f74e91dd1e", + "userId": "068e7102-3565-4591-bf01-71f74e91dd1e", + "signerCount": 1, + "spaceId": "068e7102-3565-4591-bf01-71f74e91dd1e", + "spaceOwnerId": "068e7102-3565-4591-bf01-71f74e91dd1e", + "workspaceId": "068e7102-3565-4591-bf01-71f74e91dd1e", + "workspaceOwnerId": "068e7102-3565-4591-bf01-71f74e91dd1e" +} \ No newline at end of file diff --git a/components/aha/README.md b/components/aha/README.md index 94ce6fc555301..f459a636b5a6e 100644 --- a/components/aha/README.md +++ b/components/aha/README.md @@ -1,7 +1,11 @@ # Overview -With the Aha API, you can build integrations that allow you to do things like: +The Aha API provides a gateway to Aha's product roadmapping software, enabling automated interactions with Aha's platform. With this API on Pipedream, you can create, read, update, and delete records within Aha, including features, ideas, and releases. This capability allows for streamlined project management workflows, efficient tracking of product development, and synchronization of data across various platforms. -- Add features or releases to Aha from your own system -- Pull data out of Aha for reporting or analysis -- Trigger actions in your own system when things happen in Aha +# Example Use Cases + +- **Sync Aha Features with JIRA Issues**: When a new feature is created in Aha, use Pipedream to automatically create a corresponding issue in JIRA. This ensures that technical teams using JIRA stay in sync with product planning in Aha without manual intervention. + +- **Automate Idea Backlog Grooming**: Set up a workflow that listens for new ideas submitted to Aha and scores them based on predefined criteria. The highest-scoring ideas can then be automatically promoted to features, helping prioritize the product backlog. + +- **Release Updates Notifications**: Keep stakeholders informed by setting up a Pipedream workflow that sends a notification via Slack, email, or other communication platforms whenever a release is updated or shipped in Aha. This keeps everyone aligned with the product release schedule and progress. diff --git a/components/aha/package.json b/components/aha/package.json new file mode 100644 index 0000000000000..9a84f2f7075d8 --- /dev/null +++ b/components/aha/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/aha", + "version": "0.6.0", + "description": "Pipedream aha Components", + "main": "aha.app.mjs", + "keywords": [ + "pipedream", + "aha" + ], + "homepage": "https://pipedream.com/apps/aha", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/ahrefs/README.md b/components/ahrefs/README.md index a6b490eaf05f2..e6ac123419398 100644 --- a/components/ahrefs/README.md +++ b/components/ahrefs/README.md @@ -1,10 +1,11 @@ # Overview -Ahrefs is a powerful API that allows you to build a variety of applications and -tools. Here are some examples of what you can build with Ahrefs: - -- A keyword research tool -- A competitor analysis tool -- A backlink analysis tool -- A link building tool -- An SEO audit tool +Ahrefs API taps into the vast data reserves of Ahrefs, a robust SEO tool, to programmatically access insights into backlink profiles, keyword rankings, and SEO health. With Pipedream's capabilities, you can automate SEO monitoring, integrate with content management systems, trigger alerts for new or lost backlinks, or gather intelligence for keyword research—all without manual intervention. This unlocks the potential for real-time SEO strategy adjustments and the integration of SEO data into broader business processes or analytics platforms. + +# Example Use Cases + +- **Automated Backlink Monitoring**: Set up a Pipedream workflow to monitor your site's backlink profile. Whenever Ahrefs detects new backlinks or lost links, Pipedream can trigger an event that updates a Google Sheet or sends a Slack message to your marketing team, keeping them informed and responsive to changes in your backlink landscape. + +- **Content Strategy Optimization**: Create a workflow where Pipedream regularly fetches keyword rankings for your target keywords from Ahrefs. Use this data to feed into a content management system, like WordPress, to suggest updates to existing content or ideas for new content based on current SEO performance and keyword opportunities. + +- **Competitor Analysis Alerts**: Leverage Ahrefs to keep an eye on competitors' SEO activities. Set up a Pipedream workflow to alert you via email or a messaging app such as Telegram when there are significant changes in your competitors' backlink profiles or keyword rankings. This instant intel can inform strategic decisions and help you maintain a competitive edge. diff --git a/components/ai_ml_api/ai_ml_api.app.mjs b/components/ai_ml_api/ai_ml_api.app.mjs new file mode 100644 index 0000000000000..7e6bb7e0586bb --- /dev/null +++ b/components/ai_ml_api/ai_ml_api.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "ai_ml_api", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/ai_ml_api/package.json b/components/ai_ml_api/package.json new file mode 100644 index 0000000000000..59e5b50ca364e --- /dev/null +++ b/components/ai_ml_api/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/ai_ml_api", + "version": "0.0.1", + "description": "Pipedream AI/ML API Components", + "main": "ai_ml_api.app.mjs", + "keywords": [ + "pipedream", + "ai_ml_api" + ], + "homepage": "https://pipedream.com/apps/ai_ml_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/ai_textraction/actions/extract-data/extract-data.mjs b/components/ai_textraction/actions/extract-data/extract-data.mjs new file mode 100644 index 0000000000000..7b20c7480b2df --- /dev/null +++ b/components/ai_textraction/actions/extract-data/extract-data.mjs @@ -0,0 +1,66 @@ +import aiTextraction from "../../ai_textraction.app.mjs"; + +export default { + key: "ai_textraction-extract-data", + name: "Extract Data", + description: "Extract custom data from text using AI Textraction. [See the documentation](https://rapidapi.com/textractionai/api/ai-textraction)", + version: "0.0.1", + type: "action", + props: { + aiTextraction, + text: { + type: "string", + label: "Text", + description: "The text to extract entities from", + }, + entities: { + type: "string[]", + label: "Entities", + description: "An array of entity names to extract from the text. Example: `first_name`", + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (!this.entities?.length) { + return props; + } + for (const entity of this.entities) { + props[`${entity}_type`] = { + type: "string", + label: `${entity} - Type`, + description: `The type of results to return for ${entity}`, + options: [ + "string", + "integer", + "array[string]", + "array[integer]", + ], + }; + props[`${entity}_description`] = { + type: "string", + label: `${entity} - Description`, + description: `Description of the entity ${entity}. Example: \`First name of the person\``, + }; + } + return props; + }, + async run({ $ }) { + const entities = this.entities.map((entity) => ({ + var_name: entity, + type: this[`${entity}_type`], + description: this[`${entity}_description`], + })); + + const response = await this.aiTextraction.extractData({ + $, + data: { + text: this.text, + entities, + }, + }); + + $.export("$summary", "Successfully extracted data from text"); + return response; + }, +}; diff --git a/components/ai_textraction/ai_textraction.app.mjs b/components/ai_textraction/ai_textraction.app.mjs new file mode 100644 index 0000000000000..90d31f3a4acc5 --- /dev/null +++ b/components/ai_textraction/ai_textraction.app.mjs @@ -0,0 +1,32 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "ai_textraction", + methods: { + _baseUrl() { + return "https://ai-textraction.p.rapidapi.com"; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "x-rapidapi-host": "ai-textraction.p.rapidapi.com", + "x-rapidapi-key": `${this.$auth.rapid_key}`, + }, + ...opts, + }); + }, + extractData(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/textraction", + ...opts, + }); + }, + }, +}; diff --git a/components/ai_textraction/package.json b/components/ai_textraction/package.json new file mode 100644 index 0000000000000..3212d9c14e4c6 --- /dev/null +++ b/components/ai_textraction/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/ai_textraction", + "version": "0.1.0", + "description": "Pipedream AI Textraction Components", + "main": "ai_textraction.app.mjs", + "keywords": [ + "pipedream", + "ai_textraction" + ], + "homepage": "https://pipedream.com/apps/ai_textraction", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/aidaform/README.md b/components/aidaform/README.md index 7f52a82f76392..302578b6beafd 100644 --- a/components/aidaform/README.md +++ b/components/aidaform/README.md @@ -1,14 +1,11 @@ # Overview -Some things you can build using the Aidaform API include: - -- Web forms -- Calculators -- Polls and surveys -- Quizzes -- Contact forms -- Registration forms -- Order forms -- Payment forms -- Donation forms -- Newsletter signup forms +Aidaform is a service that lets you craft forms for a variety of purposes, from surveys to job applications. By leveraging the Aidaform API on Pipedream, you can automate interactions with your forms and the data they collect. This means instant processes for form responses, seamless data integration with other apps, and dynamic form management. With Pipedream's serverless platform, you can create workflows that trigger actions in other services like CRMs, email marketing tools, or databases whenever a new form submission is received. + +# Example Use Cases + +- **Automated Contact Registration**: When a new contact form submission comes in via Aidaform, Pipedream can catch that event and automatically add or update the contact in a CRM like Salesforce or HubSpot. This removes the need for manual data entry and ensures your CRM is always up-to-date with the latest leads or inquiries. + +- **Survey Response Aggregation**: After collecting responses from an Aidaform survey, Pipedream can aggregate the data and send it to a Google Sheet for easy analysis. You can also leverage this workflow to trigger notifications via Slack or email, alerting your team to new responses or feedback that require immediate attention. + +- **Event Registration and Confirmation**: Use Aidaform for event sign-ups and integrate Pipedream to manage the registration process. When a participant registers, Pipedream can add them to an event management app like Eventbrite, send a personalized confirmation email via SendGrid, and even update a Mailchimp list to keep them informed about event updates. diff --git a/components/aidaform/package.json b/components/aidaform/package.json new file mode 100644 index 0000000000000..ea4905355c16b --- /dev/null +++ b/components/aidaform/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/aidaform", + "version": "0.6.0", + "description": "Pipedream aidaform Components", + "main": "aidaform.app.mjs", + "keywords": [ + "pipedream", + "aidaform" + ], + "homepage": "https://pipedream.com/apps/aidaform", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/aidbase/actions/add-question-to-faq/add-question-to-faq.mjs b/components/aidbase/actions/add-question-to-faq/add-question-to-faq.mjs new file mode 100644 index 0000000000000..29c9163c8f5f2 --- /dev/null +++ b/components/aidbase/actions/add-question-to-faq/add-question-to-faq.mjs @@ -0,0 +1,54 @@ +import aidbase from "../../aidbase.app.mjs"; + +export default { + key: "aidbase-add-question-to-faq", + name: "Add Question to FAQ", + description: "Add a new question to a FAQ in Airbase. [See the documentation](https://docs.aidbase.ai/apis/knowledge-api/reference/#post-knowledgeidfaq-item)", + version: "0.0.1", + type: "action", + props: { + aidbase, + faqId: { + propDefinition: [ + aidbase, + "faqId", + ], + }, + question: { + type: "string", + label: "Question", + description: "The question of the FAQ item", + }, + answer: { + type: "string", + label: "Answer", + description: "The answer of the FAQ item", + }, + sourceUrl: { + type: "string", + label: "Source URL", + description: "The source URL of the FAQ item", + optional: true, + }, + categories: { + type: "string[]", + label: "Categories", + description: "A list of category names for the FAQ. If the category does not exist, it will be created.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.aidbase.createFaqItem({ + $, + faqId: this.faqId, + data: { + question: this.question, + answer: this.answer, + source_url: this.sourceUrl, + categories: this.categories, + }, + }); + $.export("$summary", `Successfully created FAQ item with ID ${response.data.id}`); + return response; + }, +}; diff --git a/components/aidbase/actions/add-video/add-video.mjs b/components/aidbase/actions/add-video/add-video.mjs new file mode 100644 index 0000000000000..0ec3dfd5098d8 --- /dev/null +++ b/components/aidbase/actions/add-video/add-video.mjs @@ -0,0 +1,28 @@ +import aidbase from "../../aidbase.app.mjs"; + +export default { + key: "aidbase-add-video", + name: "Add Video", + description: "Add a video as a piece of knowledge to your Aidbase account. [See the documentation](https://docs.aidbase.ai/apis/knowledge-api/reference/#post-knowledgevideo)", + version: "0.0.1", + type: "action", + props: { + aidbase, + videoUrl: { + type: "string", + label: "Video URL", + description: "The URL of the video to add. Must be a valid YouTube URL.", + }, + }, + async run({ $ }) { + const response = await this.aidbase.createVideo({ + $, + data: { + video_url: this.videoUrl, + video_type: "YOUTUBE", + }, + }); + $.export("$summary", `Successfully created video with ID ${response.data.id}`); + return response; + }, +}; diff --git a/components/aidbase/actions/create-faq/create-faq.mjs b/components/aidbase/actions/create-faq/create-faq.mjs new file mode 100644 index 0000000000000..798800a817f09 --- /dev/null +++ b/components/aidbase/actions/create-faq/create-faq.mjs @@ -0,0 +1,34 @@ +import aidbase from "../../aidbase.app.mjs"; + +export default { + key: "aidbase-create-faq", + name: "Create FAQ", + description: "Create a new frequently asked questions (FAQ) list on your Aidbase account. [See the documentation](https://docs.aidbase.ai/apis/knowledge-api/reference/#post-knowledgefaq)", + version: "0.0.1", + type: "action", + props: { + aidbase, + title: { + type: "string", + label: "Title", + description: "The title of the FAQ", + }, + description: { + type: "string", + label: "Description", + description: "The description of the FAQ", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.aidbase.createFaq({ + $, + data: { + title: this.title, + description: this.description, + }, + }); + $.export("$summary", `Successfully created FAQ with ID ${response.data.id}`); + return response; + }, +}; diff --git a/components/aidbase/actions/start-training/start-training.mjs b/components/aidbase/actions/start-training/start-training.mjs new file mode 100644 index 0000000000000..39003915de99e --- /dev/null +++ b/components/aidbase/actions/start-training/start-training.mjs @@ -0,0 +1,26 @@ +import aidbase from "../../aidbase.app.mjs"; + +export default { + key: "aidbase-start-training", + name: "Start Training", + description: "Kick off a training job for a specific piece of knowledge. [See the documentation](https://docs.aidbase.ai/apis/knowledge-api/reference/#put-knowledgeidtrain)", + version: "0.0.1", + type: "action", + props: { + aidbase, + knowledgeItemId: { + propDefinition: [ + aidbase, + "knowledgeItemId", + ], + }, + }, + async run({ $ }) { + const response = await this.aidbase.startTraining({ + $, + knowledgeItemId: this.knowledgeItemId, + }); + $.export("$summary", `Successfully started training FAQ with ID ${response.data.id}`); + return response; + }, +}; diff --git a/components/aidbase/aidbase.app.mjs b/components/aidbase/aidbase.app.mjs new file mode 100644 index 0000000000000..94a8aaee72522 --- /dev/null +++ b/components/aidbase/aidbase.app.mjs @@ -0,0 +1,180 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "aidbase", + propDefinitions: { + knowledgeItemId: { + type: "string", + label: "Knowledge Item ID", + description: "The ID of the knowledge item to trigger training for", + async options() { + const { data: { items } } = await this.listKnowledgeItems(); + return items?.map(({ + id, type, + }) => ({ + value: id, + label: `${id} - ${type}`, + })) || []; + }, + }, + emailInboxId: { + type: "string", + label: "Email Inbox ID", + description: "The ID of the email inbox", + async options() { + const { data: { items } } = await this.listEmailInboxes(); + return items?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })) || []; + }, + }, + ticketFormId: { + type: "string", + label: "Ticket Form ID", + description: "The ID of the ticket form", + async options() { + const { data: { items } } = await this.listTicketForms(); + return items?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })) || []; + }, + }, + chatbotId: { + type: "string", + label: "Chatbot ID", + description: "The ID of the chatbot", + async options() { + const { data: { items } } = await this.listChatbots(); + return items?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })) || []; + }, + }, + faqId: { + type: "string", + label: "FAQ ID", + description: "The ID of the FAQ to add a question to", + async options() { + const { data: { items } } = await this.listKnowledgeItems(); + const faqs = items?.filter(({ type }) => type === "faq") || []; + return faqs?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.aidbase.ai/v1"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, + path, + ...args + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._headers(), + ...args, + }); + }, + listKnowledgeItems(opts = {}) { + return this._makeRequest({ + path: "/knowledge", + ...opts, + }); + }, + listEmailInboxes(opts = {}) { + return this._makeRequest({ + path: "/email-inboxes", + ...opts, + }); + }, + listTicketForms(opts = {}) { + return this._makeRequest({ + path: "/ticket-forms", + ...opts, + }); + }, + listChatbots(opts = {}) { + return this._makeRequest({ + path: "/chatbots", + ...opts, + }); + }, + listEmailInboxKnowledgeItems({ + emailInboxId, ...opts + }) { + return this._makeRequest({ + path: `/email-inbox/${emailInboxId}/knowledge`, + ...opts, + }); + }, + listTicketFormKnowledgeItems({ + ticketFormId, ...opts + }) { + return this._makeRequest({ + path: `/ticket-form/${ticketFormId}/knowledge`, + ...opts, + }); + }, + listChatbotKnowledgeItems({ + chatbotId, ...opts + }) { + return this._makeRequest({ + path: `/chatbot/${chatbotId}/knowledge`, + ...opts, + }); + }, + createVideo(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/knowledge/video", + ...opts, + }); + }, + createFaq(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/knowledge/faq", + ...opts, + }); + }, + createFaqItem({ + faqId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/knowledge/${faqId}/faq-item`, + ...opts, + }); + }, + startTraining({ + knowledgeItemId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/knowledge/${knowledgeItemId}/train`, + ...opts, + }); + }, + }, +}; diff --git a/components/aidbase/package.json b/components/aidbase/package.json new file mode 100644 index 0000000000000..40ff978c3a1fa --- /dev/null +++ b/components/aidbase/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/aidbase", + "version": "0.1.0", + "description": "Pipedream Aidbase Components", + "main": "aidbase.app.mjs", + "keywords": [ + "pipedream", + "aidbase" + ], + "homepage": "https://pipedream.com/apps/aidbase", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/aidbase/sources/common/base.mjs b/components/aidbase/sources/common/base.mjs new file mode 100644 index 0000000000000..4e4fabf580ccc --- /dev/null +++ b/components/aidbase/sources/common/base.mjs @@ -0,0 +1,78 @@ +import aidbase from "../../aidbase.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + aidbase, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(); + }, + }, + methods: { + _getPreviousIds() { + return this.db.get("previousIds") || {}; + }, + _setPreviousIds(previousIds) { + this.db.set("previousIds", previousIds); + }, + getArgs() { + return {}; + }, + generateMeta(item) { + return { + id: item.id, + summary: `New Knowledge Item: ${item.id}`, + ts: Date.now(), + }; + }, + async processEvent(max) { + const previousIds = this._getPreviousIds(); + const resourceFn = this.getResourceFn(); + let args = this.getArgs(); + args = { + ...args, + params: { + ...args.params, + }, + }; + let more, count = 0; + + do { + const { + data: { + items, next_cursor: next, has_more: hasMore, + }, + } = await resourceFn(args); + for (const item of items) { + if (!previousIds[item.id]) { + previousIds[item.id] = true; + if (!max || count < max) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + count++; + } + } + } + args.params.cursor = next; + more = hasMore; + } while (more); + + this._setPreviousIds(previousIds); + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/aidbase/sources/new-chatbot-knowledge-item/new-chatbot-knowledge-item.mjs b/components/aidbase/sources/new-chatbot-knowledge-item/new-chatbot-knowledge-item.mjs new file mode 100644 index 0000000000000..e3a672e250a2d --- /dev/null +++ b/components/aidbase/sources/new-chatbot-knowledge-item/new-chatbot-knowledge-item.mjs @@ -0,0 +1,33 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "aidbase-new-chatbot-knowledge-item", + name: "New Chatbot Knowledge Item", + description: "Emit new event when a new knowledge item is added to a chatbot.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + chatbotId: { + propDefinition: [ + common.props.aidbase, + "chatbotId", + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.aidbase.listChatbotKnowledgeItems; + }, + getArgs() { + return { + chatbotId: this.chatbotId, + }; + }, + }, + sampleEmit, +}; diff --git a/components/aidbase/sources/new-chatbot-knowledge-item/test-event.mjs b/components/aidbase/sources/new-chatbot-knowledge-item/test-event.mjs new file mode 100644 index 0000000000000..1a877bc718733 --- /dev/null +++ b/components/aidbase/sources/new-chatbot-knowledge-item/test-event.mjs @@ -0,0 +1,6 @@ +export default { + "id": "c3f3b16c-4b1e-422a-b4af-6379339d01c0", + "type": "faq", + "title": "Fun FAQ", + "description": "faq description" +} \ No newline at end of file diff --git a/components/aidbase/sources/new-email-inbox-knowledge-item/new-email-inbox-knowledge-item.mjs b/components/aidbase/sources/new-email-inbox-knowledge-item/new-email-inbox-knowledge-item.mjs new file mode 100644 index 0000000000000..09731131148f7 --- /dev/null +++ b/components/aidbase/sources/new-email-inbox-knowledge-item/new-email-inbox-knowledge-item.mjs @@ -0,0 +1,33 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "aidbase-new-email-inbox-knowledge-item", + name: "New Email Inbox Knowledge Item", + description: "Emit new event when a new knowledge item is added to an email inbox.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + emailInboxId: { + propDefinition: [ + common.props.aidbase, + "emailInboxId", + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.aidbase.listEmailInboxKnowledgeItems; + }, + getArgs() { + return { + emailInboxId: this.emailInboxId, + }; + }, + }, + sampleEmit, +}; diff --git a/components/aidbase/sources/new-email-inbox-knowledge-item/test-event.mjs b/components/aidbase/sources/new-email-inbox-knowledge-item/test-event.mjs new file mode 100644 index 0000000000000..1a877bc718733 --- /dev/null +++ b/components/aidbase/sources/new-email-inbox-knowledge-item/test-event.mjs @@ -0,0 +1,6 @@ +export default { + "id": "c3f3b16c-4b1e-422a-b4af-6379339d01c0", + "type": "faq", + "title": "Fun FAQ", + "description": "faq description" +} \ No newline at end of file diff --git a/components/aidbase/sources/new-ticket-form-knowledge-item/new-ticket-form-knowledge-item.mjs b/components/aidbase/sources/new-ticket-form-knowledge-item/new-ticket-form-knowledge-item.mjs new file mode 100644 index 0000000000000..d9526dc1e1081 --- /dev/null +++ b/components/aidbase/sources/new-ticket-form-knowledge-item/new-ticket-form-knowledge-item.mjs @@ -0,0 +1,33 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "aidbase-new-ticket-form-knowledge-item", + name: "New Ticket Form Knowledge Item", + description: "Emit new event when a new knowledge item is added to a ticket form.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + ticketFormId: { + propDefinition: [ + common.props.aidbase, + "ticketFormId", + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.aidbase.listTicketFormKnowledgeItems; + }, + getArgs() { + return { + ticketFormId: this.ticketFormId, + }; + }, + }, + sampleEmit, +}; diff --git a/components/aidbase/sources/new-ticket-form-knowledge-item/test-event.mjs b/components/aidbase/sources/new-ticket-form-knowledge-item/test-event.mjs new file mode 100644 index 0000000000000..1a877bc718733 --- /dev/null +++ b/components/aidbase/sources/new-ticket-form-knowledge-item/test-event.mjs @@ -0,0 +1,6 @@ +export default { + "id": "c3f3b16c-4b1e-422a-b4af-6379339d01c0", + "type": "faq", + "title": "Fun FAQ", + "description": "faq description" +} \ No newline at end of file diff --git a/components/aimtell/README.md b/components/aimtell/README.md new file mode 100644 index 0000000000000..e22b04e667ecc --- /dev/null +++ b/components/aimtell/README.md @@ -0,0 +1,11 @@ +# Overview + +The Aimtell API provides a suite of web push notification services that enable businesses to send targeted messages directly to their users' browsers. This functionality can be valuable for re-engaging users with personalized content, promoting time-sensitive offers, or providing critical updates. Using Pipedream, you can integrate Aimtell with various apps and services to automate the delivery of web push notifications based on triggers like new email subscribers, eCommerce events, or custom conditions defined by your own data sources. + +# Example Use Cases + +- **Sync New Email Subscribers to Aimtell**: When a new subscriber is added to your email marketing platform (e.g., Mailchimp), use Pipedream to automatically register that subscriber to receive web push notifications from Aimtell, enhancing multi-channel engagement. + +- **Send Push Notifications on New eCommerce Orders**: Connect Aimtell to your eCommerce platform (e.g., Shopify). Whenever a new order is placed, trigger a workflow in Pipedream that sends a personalized push notification to the customer, thanking them for their purchase or offering related products. + +- **Automate Push Notifications for User Milestones**: Combine Aimtell with a CRM platform like Salesforce. You can set up a Pipedream workflow that listens for events like a user reaching a certain loyalty tier or anniversary, then sends a celebratory or exclusive offer via Aimtell push notification. diff --git a/components/air/air.app.mjs b/components/air/air.app.mjs new file mode 100644 index 0000000000000..78ca544043ae9 --- /dev/null +++ b/components/air/air.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "air", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/air/package.json b/components/air/package.json new file mode 100644 index 0000000000000..b3ac6d96dcb2d --- /dev/null +++ b/components/air/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/air", + "version": "0.0.1", + "description": "Pipedream Air Components", + "main": "air.app.mjs", + "keywords": [ + "pipedream", + "air" + ], + "homepage": "https://pipedream.com/apps/air", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/airbrake/README.md b/components/airbrake/README.md index 549c0783aded6..ff0b7511588fc 100644 --- a/components/airbrake/README.md +++ b/components/airbrake/README.md @@ -1,9 +1,11 @@ # Overview -With the Airbrake API, you can build integrations that send data to Airbrake -from your application. This can include: +The Airbrake API facilitates real-time error monitoring and automatic exception reporting for your web applications, giving you instant insight into issues as they arise. With this API, you can create custom notifications, analyze and aggregate error data, and manage your project's error trends. Leveraging Pipedream's capabilities, you can automate workflows that respond to these errors, connect with other services for enhanced issue resolution, and maintain a smooth operation within your development and production environments. -- Error monitoring -- Application performance monitoring -- Custom dashboards -- Integrations with 3rd party services +# Example Use Cases + +- **Automated Error Response System**: Trigger a workflow in Pipedream when Airbrake captures a new error. This could automatically create a GitHub issue for the development team, send an alert message to a Slack channel, and log the error on a Google Sheet for record-keeping. + +- **Error Trend Analysis and Reporting**: Set up a scheduled Pipedream workflow that fetches error reports from Airbrake, analyzes them for frequency and type, and compiles a weekly summary report. This report could then be emailed to the team or posted to a team Confluence page for review. + +- **Customer Support Integration**: When Airbrake registers a critical error that may impact customers, trigger a Pipedream workflow that creates a support ticket in Zendesk or another customer support platform. Include error details and affected systems in the ticket to expedite the support process. diff --git a/components/aircall/README.md b/components/aircall/README.md index b47007e0daf39..229b7bd8156a5 100644 --- a/components/aircall/README.md +++ b/components/aircall/README.md @@ -1,5 +1,11 @@ # Overview -The Aircall API allows developers to integrate Aircall's features into their -own applications. With the API, developers can manage account settings, make -and receive phone calls, send and receive text messages, and more. +The Aircall API allows you to harness telephony features within your applications, enabling you to manage calls, contacts, users, and more programmatically. With Pipedream's capability to leverage this API, you can automate call logging, synchronize contact information across platforms, dynamically route calls based on business logic, and generate analytics and reports. + +# Example Use Cases + +- **Automated Call Logging to CRM**: After a call ends on Aircall, trigger a workflow on Pipedream that captures call details and logs them into a CRM like Salesforce or HubSpot. This ensures that all interaction history is up-to-date and easily accessible for sales and support teams. + +- **Contact Synchronization Workflow**: Set up a Pipedream workflow that listens for new contacts or updates in Aircall and automatically syncs them to other apps such as Google Contacts or Mailchimp. This keeps all contact lists in sync without manual data entry, reducing errors and saving time. + +- **Dynamic Call Routing Based on Support Tickets**: Create a Pipedream workflow that checks incoming calls against open support tickets in a service like Zendesk. Depending on the priority or status of the ticket, the call can be routed to the appropriate support agent or department, improving response times and customer satisfaction. diff --git a/components/airfocus/actions/create-item/create-item.mjs b/components/airfocus/actions/create-item/create-item.mjs new file mode 100644 index 0000000000000..d41a1feecbe03 --- /dev/null +++ b/components/airfocus/actions/create-item/create-item.mjs @@ -0,0 +1,80 @@ +import airfocus from "../../airfocus.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "airfocus-create-item", + name: "Create Item", + description: "Creates a new item in airfocus. [See the documentation](https://developer.airfocus.com/endpoints.html)", + version: "0.1.0", + type: "action", + props: { + airfocus, + workspaceId: { + propDefinition: [ + airfocus, + "workspaceId", + ], + }, + name: { + propDefinition: [ + airfocus, + "name", + ], + }, + statusId: { + propDefinition: [ + airfocus, + "statusId", + ({ workspaceId }) => ({ + workspaceId, + }), + ], + optional: true, + }, + description: { + propDefinition: [ + airfocus, + "description", + ], + optional: true, + }, + fields: { + propDefinition: [ + airfocus, + "fields", + ], + optional: true, + }, + color: { + propDefinition: [ + airfocus, + "color", + ], + optional: true, + }, + archived: { + propDefinition: [ + airfocus, + "archived", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.airfocus.createItem({ + $, + workspaceId: this.workspaceId, + data: { + statusId: this.statusId, + name: this.name, + fields: parseObject(this.fields), + description: this.description, + color: this.color, + archived: this.archived, + }, + }); + + $.export("$summary", `Successfully created item with name ${this.name}`); + return response; + }, +}; diff --git a/components/airfocus/actions/delete-item/delete-item.mjs b/components/airfocus/actions/delete-item/delete-item.mjs new file mode 100644 index 0000000000000..b71bbd6fcd5c9 --- /dev/null +++ b/components/airfocus/actions/delete-item/delete-item.mjs @@ -0,0 +1,37 @@ +import airfocus from "../../airfocus.app.mjs"; + +export default { + key: "airfocus-delete-item", + name: "Delete Item", + description: "Deletes a specific item in airfocus. [See the documentation](https://developer.airfocus.com/endpoints.html)", + version: "0.1.0", + type: "action", + props: { + airfocus, + workspaceId: { + propDefinition: [ + airfocus, + "workspaceId", + ], + }, + itemId: { + propDefinition: [ + airfocus, + "itemId", + ({ workspaceId }) => ({ + workspaceId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.airfocus.deleteItem({ + $, + workspaceId: this.workspaceId, + itemId: this.itemId, + }); + + $.export("$summary", `Successfully deleted item with ID ${this.itemId}`); + return response; + }, +}; diff --git a/components/airfocus/actions/search-item/search-item.mjs b/components/airfocus/actions/search-item/search-item.mjs new file mode 100644 index 0000000000000..958e14a964f59 --- /dev/null +++ b/components/airfocus/actions/search-item/search-item.mjs @@ -0,0 +1,42 @@ +import airfocus from "../../airfocus.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "airfocus-search-item", + name: "Search Item", + description: "Searches items by query in airfocus. [See the documentation](https://developer.airfocus.com/endpoints.html)", + version: "0.1.0", + type: "action", + props: { + airfocus, + workspaceId: { + propDefinition: [ + airfocus, + "workspaceId", + ], + }, + filter: { + type: "object", + label: "Filter", + description: "The object query to match the items. Example: `{\"type\": \"and\",\"inner\": [{\"type\": \"name\",\"mode\": \"contain\",\"text\": \"Test\",\"caseSensitive\": false}]}` [See the documentation](https://developer.airfocus.com/endpoints.html)", + optional: true, + }, + }, + async run({ $ }) { + const response = this.airfocus.paginate({ + $, + workspaceId: this.workspaceId, + fn: this.airfocus.searchItem, + data: { + filter: parseObject(this.filter), + }, + }); + + const responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + $.export("$summary", `Successfully fetched ${responseArray.length} item(s)`); + return responseArray; + }, +}; diff --git a/components/airfocus/actions/update-item/update-item.mjs b/components/airfocus/actions/update-item/update-item.mjs new file mode 100644 index 0000000000000..18277a367b008 --- /dev/null +++ b/components/airfocus/actions/update-item/update-item.mjs @@ -0,0 +1,103 @@ +import airfocus from "../../airfocus.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "airfocus-update-item", + name: "Update Item", + description: "Updates an existing item in airfocus. [See the documentation](https://developer.airfocus.com/endpoints.html)", + version: "0.1.0", + type: "action", + props: { + airfocus, + workspaceId: { + propDefinition: [ + airfocus, + "workspaceId", + ], + }, + itemId: { + propDefinition: [ + airfocus, + "itemId", + ({ workspaceId }) => ({ + workspaceId, + }), + ], + }, + name: { + propDefinition: [ + airfocus, + "name", + ], + optional: true, + }, + statusId: { + propDefinition: [ + airfocus, + "statusId", + ({ workspaceId }) => ({ + workspaceId, + }), + ], + optional: true, + }, + description: { + propDefinition: [ + airfocus, + "description", + ], + optional: true, + }, + fields: { + propDefinition: [ + airfocus, + "fields", + ], + optional: true, + }, + color: { + propDefinition: [ + airfocus, + "color", + ], + optional: true, + }, + archived: { + propDefinition: [ + airfocus, + "archived", + ], + optional: true, + }, + }, + async run({ $ }) { + const { + airfocus, + fields, + ...props + } = this; + const item = await airfocus.getItem({ + workspaceId: this.workspaceId, + itemId: this.itemId, + }); + + const data = { + ...item, + ...props, + }; + + const parsedFields = parseObject(fields); + if (parsedFields) { + data.fields = parsedFields; + } + + const response = await this.airfocus.updateItem({ + $, + workspaceId: this.workspaceId, + itemId: this.itemId, + data, + }); + $.export("$summary", `Successfully updated item with ID ${this.itemId}`); + return response; + }, +}; diff --git a/components/airfocus/airfocus.app.mjs b/components/airfocus/airfocus.app.mjs new file mode 100644 index 0000000000000..968bc8dc0b9d7 --- /dev/null +++ b/components/airfocus/airfocus.app.mjs @@ -0,0 +1,213 @@ +import { axios } from "@pipedream/platform"; +import { + COLOR_OPTIONS, LIMIT, +} from "./common/constants.mjs"; + +export default { + type: "app", + app: "airfocus", + propDefinitions: { + workspaceId: { + type: "string", + label: "Workspace ID", + description: "The ID of the workspace.", + async options({ + workspaceId, page, + }) { + const { items } = await this.listWorkspaces({ + workspaceId, + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + + return items.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + itemId: { + type: "string", + label: "Item ID", + description: "The ID of the item.", + async options({ + workspaceId, page, + }) { + const { items } = await this.listItems({ + workspaceId, + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + + return items.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + name: { + type: "string", + label: "Name", + description: "The name of the item.", + }, + statusId: { + type: "string", + label: "Status ID", + description: "The ID of the status.", + async options({ workspaceId }) { + const { _embedded: { statuses } } = await this.getWorkspace({ + workspaceId, + }); + + return Object.entries(statuses).map(([ + , { + id: value, name: label, + }, + ]) => ({ + label, + value, + })); + }, + }, + fields: { + type: "object", + label: "Fields", + description: "Values of custom fields, where each key is a custom-field ID and each value is a JSON-formatted value of the corresponding field.", + }, + description: { + type: "object", + label: "Description", + description: "An object with content description blocks [See the documentation](https://developer.airfocus.com/endpoints.html).", + }, + color: { + type: "string", + label: "Color", + description: "The color of the item.", + options: COLOR_OPTIONS, + }, + archived: { + type: "boolean", + label: "Archived", + description: "Whether the item is archived.", + }, + }, + methods: { + _baseUrl() { + return "https://api.airfocus.com/api/workspaces/"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path = "", ...opts + } = {}) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + getWorkspace({ workspaceId }) { + return this._makeRequest({ + path: workspaceId, + }); + }, + getItem({ + workspaceId, itemId, + }) { + return this._makeRequest({ + path: `${workspaceId}/items/${itemId}`, + }); + }, + listItems({ + workspaceId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `${workspaceId}/items/search`, + ...opts, + }); + }, + listWorkspaces(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "search", + ...opts, + }); + }, + deleteItem({ + workspaceId, itemId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `${workspaceId}/items/${itemId}`, + ...opts, + }); + }, + createItem({ + workspaceId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `${workspaceId}/items`, + ...opts, + }); + }, + searchItem({ + workspaceId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `${workspaceId}/items/search`, + ...opts, + }); + }, + updateItem({ + workspaceId, itemId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `${workspaceId}/items/${itemId}`, + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.offset = LIMIT * page; + params.limit = LIMIT; + page++; + + const { items } = await fn({ + params, + ...opts, + }); + for (const d of items) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = items.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/airfocus/common/constants.mjs b/components/airfocus/common/constants.mjs new file mode 100644 index 0000000000000..33f427e4ef15c --- /dev/null +++ b/components/airfocus/common/constants.mjs @@ -0,0 +1,23 @@ +export const LIMIT = 100; +export const COLOR_OPTIONS = [ + "amber", + "azure", + "black", + "blue", + "coco", + "daulphine", + "emerald", + "fanta", + "granite", + "great", + "leaf", + "moss", + "ocean", + "orange", + "pink", + "purple", + "red", + "sky", + "violet", + "yellow", +]; diff --git a/components/airfocus/common/utils.mjs b/components/airfocus/common/utils.mjs new file mode 100644 index 0000000000000..9050833bcda0b --- /dev/null +++ b/components/airfocus/common/utils.mjs @@ -0,0 +1,32 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return parseObject(item); + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + if (typeof obj === "object") { + for (const [ + key, + value, + ] of Object.entries(obj)) { + obj[key] = parseObject(value); + } + } + return obj; +}; diff --git a/components/airfocus/package.json b/components/airfocus/package.json new file mode 100644 index 0000000000000..0fa1cbfa03717 --- /dev/null +++ b/components/airfocus/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/airfocus", + "version": "0.1.1", + "description": "Pipedream Airfocus Components", + "main": "airfocus.app.mjs", + "keywords": [ + "pipedream", + "airfocus" + ], + "homepage": "https://pipedream.com/apps/airfocus", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/airfocus/sources/common/base.mjs b/components/airfocus/sources/common/base.mjs new file mode 100644 index 0000000000000..a0396952e74fa --- /dev/null +++ b/components/airfocus/sources/common/base.mjs @@ -0,0 +1,92 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import airfocus from "../../airfocus.app.mjs"; + +export default { + props: { + airfocus, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + workspaceId: { + propDefinition: [ + airfocus, + "workspaceId", + ], + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01"; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + getInnerFilter({ + filterField, lastDate, + }) { + return [ + { + type: filterField, + mode: "afterOrOn", + value: { + date: lastDate, + zoneId: Intl.DateTimeFormat().resolvedOptions().timeZone, + }, + }, + ]; + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const filterField = this.getFilterField(); + + const response = this.airfocus.paginate({ + fn: this.airfocus.listItems, + workspaceId: this.workspaceId, + maxResults, + data: { + filter: { + type: "and", + inner: this.getInnerFilter({ + filterField, + lastDate, + }), + }, + sort: { + type: filterField, + direction: "desc", + }, + }, + }); + + let responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + if (responseArray.length) { + const dateArray = responseArray[0][filterField].split( "T" ); + this._setLastDate(dateArray[0]); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: `${item.id}-${item[filterField]}`, + summary: this.getSummary(item), + ts: Date.parse(item[filterField]), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/airfocus/sources/new-item-created-or-updated/new-item-created-or-updated.mjs b/components/airfocus/sources/new-item-created-or-updated/new-item-created-or-updated.mjs new file mode 100644 index 0000000000000..9fcad72711f41 --- /dev/null +++ b/components/airfocus/sources/new-item-created-or-updated/new-item-created-or-updated.mjs @@ -0,0 +1,49 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "airfocus-new-item-created-or-updated", + name: "New Item Created or Updated", + description: "Emit new event when a new item is created or an existing one is updated.", + version: "0.1.0", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFilterField() { + return "lastUpdatedAt"; + }, + getSummary(item) { + return `New Item ${(item.createdAt === item.lastUpdatedAt) + ? "Created" + : "Updated"}: ${item.name}`; + }, + getInnerFilter({ lastDate }) { + return [ + { + inner: [ + { + type: "createdAt", + mode: "afterOrOn", + value: { + date: lastDate, + zoneId: Intl.DateTimeFormat().resolvedOptions().timeZone, + }, + }, + { + type: "lastUpdatedAt", + mode: "afterOrOn", + value: { + date: lastDate, + zoneId: Intl.DateTimeFormat().resolvedOptions().timeZone, + }, + }, + ], + type: "or", + }, + ]; + }, + }, + sampleEmit, +}; diff --git a/components/airfocus/sources/new-item-created-or-updated/test-event.mjs b/components/airfocus/sources/new-item-created-or-updated/test-event.mjs new file mode 100644 index 0000000000000..8a4762d7b18c2 --- /dev/null +++ b/components/airfocus/sources/new-item-created-or-updated/test-event.mjs @@ -0,0 +1,61 @@ +export default { + "id": "12345678-1234-1234-1234-123456789012", + "workspaceId": "12345678-1234-1234-1234-123456789012", + "statusId": "12345678-1234-1234-1234-123456789012", + "color": "blue", + "name": "Item Name", + "description": { + "blocks": [] + }, + "assigneeUserIds": [], + "fields": { + "12345678-1234-1234-1234-123456789012": { + "selection": [] + }, + "12345678-1234-1234-1234-123456789012": { + "selection": [] + }, + "12345678-1234-1234-1234-123456789012": { + "selection": [] + }, + "12345678-1234-1234-1234-123456789012": {}, + "12345678-1234-1234-1234-123456789012": { + "selection": [] + }, + "12345678-1234-1234-1234-123456789012": {}, + "12345678-1234-1234-1234-123456789012": { + "factorScore": {}, + "criterionScore": {} + }, + "12345678-1234-1234-1234-123456789012": {} + }, + "archived": false, + "createdAt": "2024-08-06T18:48:27.190544Z", + "lastUpdatedAt": "2024-08-06T18:48:27.190544Z", + "statusUpdatedAt": "2024-08-06T18:48:27.190544Z", + "statusCategoryUpdatedAt": "2024-08-06T18:48:27.190544Z", + "order": 2, + "number": 6, + "_embedded": { + "alias": "PME-6", + "parents": [], + "children": [], + "progress": { + "closed": 0, + "total": 0 + }, + "apps": {}, + "constraints": [ + { + "extensionId": "12345678-1234-1234-1234-123456789012", + "property": "fieldid:12345678-1234-1234-1234-123456789012", + "type": "propertyReadOnly" + } + ], + "watched": false, + "linkCount": 0, + "commentCount": 0, + "attachmentCount": 0, + "workspaceItemType": "opportunity" + } +} \ No newline at end of file diff --git a/components/airfocus/sources/new-item-created/new-item-created.mjs b/components/airfocus/sources/new-item-created/new-item-created.mjs new file mode 100644 index 0000000000000..1498a98d69c47 --- /dev/null +++ b/components/airfocus/sources/new-item-created/new-item-created.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "airfocus-new-item-created", + name: "New Item Created", + description: "Emit new event when a fresh item is created.", + version: "0.1.0", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFilterField() { + return "createdAt"; + }, + getSummary(item) { + return `New Item Created: ${item.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/airfocus/sources/new-item-created/test-event.mjs b/components/airfocus/sources/new-item-created/test-event.mjs new file mode 100644 index 0000000000000..8a4762d7b18c2 --- /dev/null +++ b/components/airfocus/sources/new-item-created/test-event.mjs @@ -0,0 +1,61 @@ +export default { + "id": "12345678-1234-1234-1234-123456789012", + "workspaceId": "12345678-1234-1234-1234-123456789012", + "statusId": "12345678-1234-1234-1234-123456789012", + "color": "blue", + "name": "Item Name", + "description": { + "blocks": [] + }, + "assigneeUserIds": [], + "fields": { + "12345678-1234-1234-1234-123456789012": { + "selection": [] + }, + "12345678-1234-1234-1234-123456789012": { + "selection": [] + }, + "12345678-1234-1234-1234-123456789012": { + "selection": [] + }, + "12345678-1234-1234-1234-123456789012": {}, + "12345678-1234-1234-1234-123456789012": { + "selection": [] + }, + "12345678-1234-1234-1234-123456789012": {}, + "12345678-1234-1234-1234-123456789012": { + "factorScore": {}, + "criterionScore": {} + }, + "12345678-1234-1234-1234-123456789012": {} + }, + "archived": false, + "createdAt": "2024-08-06T18:48:27.190544Z", + "lastUpdatedAt": "2024-08-06T18:48:27.190544Z", + "statusUpdatedAt": "2024-08-06T18:48:27.190544Z", + "statusCategoryUpdatedAt": "2024-08-06T18:48:27.190544Z", + "order": 2, + "number": 6, + "_embedded": { + "alias": "PME-6", + "parents": [], + "children": [], + "progress": { + "closed": 0, + "total": 0 + }, + "apps": {}, + "constraints": [ + { + "extensionId": "12345678-1234-1234-1234-123456789012", + "property": "fieldid:12345678-1234-1234-1234-123456789012", + "type": "propertyReadOnly" + } + ], + "watched": false, + "linkCount": 0, + "commentCount": 0, + "attachmentCount": 0, + "workspaceItemType": "opportunity" + } +} \ No newline at end of file diff --git a/components/airfocus/sources/new-item-updated/new-item-updated.mjs b/components/airfocus/sources/new-item-updated/new-item-updated.mjs new file mode 100644 index 0000000000000..d92026503b85b --- /dev/null +++ b/components/airfocus/sources/new-item-updated/new-item-updated.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "airfocus-new-item-updated", + name: "New Item Updated", + description: "Emit new event when an existing item gets updated.", + version: "0.1.0", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFilterField() { + return "lastUpdatedAt"; + }, + getSummary(item) { + return `New Item Updated: ${item.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/airfocus/sources/new-item-updated/test-event.mjs b/components/airfocus/sources/new-item-updated/test-event.mjs new file mode 100644 index 0000000000000..8a4762d7b18c2 --- /dev/null +++ b/components/airfocus/sources/new-item-updated/test-event.mjs @@ -0,0 +1,61 @@ +export default { + "id": "12345678-1234-1234-1234-123456789012", + "workspaceId": "12345678-1234-1234-1234-123456789012", + "statusId": "12345678-1234-1234-1234-123456789012", + "color": "blue", + "name": "Item Name", + "description": { + "blocks": [] + }, + "assigneeUserIds": [], + "fields": { + "12345678-1234-1234-1234-123456789012": { + "selection": [] + }, + "12345678-1234-1234-1234-123456789012": { + "selection": [] + }, + "12345678-1234-1234-1234-123456789012": { + "selection": [] + }, + "12345678-1234-1234-1234-123456789012": {}, + "12345678-1234-1234-1234-123456789012": { + "selection": [] + }, + "12345678-1234-1234-1234-123456789012": {}, + "12345678-1234-1234-1234-123456789012": { + "factorScore": {}, + "criterionScore": {} + }, + "12345678-1234-1234-1234-123456789012": {} + }, + "archived": false, + "createdAt": "2024-08-06T18:48:27.190544Z", + "lastUpdatedAt": "2024-08-06T18:48:27.190544Z", + "statusUpdatedAt": "2024-08-06T18:48:27.190544Z", + "statusCategoryUpdatedAt": "2024-08-06T18:48:27.190544Z", + "order": 2, + "number": 6, + "_embedded": { + "alias": "PME-6", + "parents": [], + "children": [], + "progress": { + "closed": 0, + "total": 0 + }, + "apps": {}, + "constraints": [ + { + "extensionId": "12345678-1234-1234-1234-123456789012", + "property": "fieldid:12345678-1234-1234-1234-123456789012", + "type": "propertyReadOnly" + } + ], + "watched": false, + "linkCount": 0, + "commentCount": 0, + "attachmentCount": 0, + "workspaceItemType": "opportunity" + } +} \ No newline at end of file diff --git a/components/airmeet/README.md b/components/airmeet/README.md new file mode 100644 index 0000000000000..64641e6c25573 --- /dev/null +++ b/components/airmeet/README.md @@ -0,0 +1,11 @@ +# Overview + +The Airmeet API enables you to automate and manage virtual events with ease. By integrating with Pipedream, you can create custom workflows that interact with the Airmeet platform, such as attendee management, event scheduling, and real-time updates. Utilize the API to sync event data, send notifications, and connect with other services for a comprehensive event automation solution. + +# Example Use Cases + +- **Sync Attendees with CRM**: Automatically add new Airmeet event attendees to your CRM system. When a new attendee joins an Airmeet event, Pipedream can capture this event and create or update a contact in platforms like Salesforce or HubSpot, ensuring your sales and marketing teams have the latest information. + +- **Automate Event Follow-ups**: Send personalized follow-up emails or messages to participants after an event. Use Pipedream to listen for the conclusion of an Airmeet event, then trigger an email through SendGrid or a message through Slack or Twilio, thanking attendees and providing additional resources or call-to-actions. + +- **Real-time Analytics Dashboard**: Stream live event data to a dashboard for real-time insights. With Airmeet's API, Pipedream can push attendance numbers, interaction levels, and other key metrics to a Google Sheets or a visualization tool like Grafana, helping you gauge event success instantly. diff --git a/components/airnow/README.md b/components/airnow/README.md index 183199da13531..8f43eec45498a 100644 --- a/components/airnow/README.md +++ b/components/airnow/README.md @@ -1,7 +1,11 @@ # Overview -The AirNow API can be used to retrieve air quality data for current conditions, -as well as historical data dating back to 1980. This data can be used to track -trends in air quality, as well as to monitor individual pollutant levels. The -AirNow API can also be used to retrieve information on air quality advisories -and alerts. +The AirNow API provides access to real-time air quality data from across the United States, offering insights into the AQI (Air Quality Index) for various pollutants. With this data, developers can create applications that inform users about the air quality in their immediate environment or trigger actions based on changes in pollution levels. On Pipedream, one can leverage AirNow's data to automate notifications, integrate environmental data into smart home systems, or even drive data analysis and reporting for health and safety monitoring. + +# Example Use Cases + +- **Smart Home Air Quality Alert System**: Trigger a workflow whenever the AQI exceeds a certain threshold in a specific location. This could connect to smart home devices via services like IFTTT or Philips Hue to change lighting color as a visible alert or activate an air purifier. + +- **Health and Wellness Tracking**: Sync AirNow data with health tracking platforms such as Google Fit or Apple Health. Create a workflow that logs air quality alongside physical activity, providing insights into how air quality may affect respiratory health during outdoor workouts. + +- **Environmental News Updates**: Combine AirNow data with a content management system like WordPress to automatically post air quality updates or warnings to a blog or news site. This could be used by local news agencies or community health organizations to keep residents informed on air quality issues. diff --git a/components/airparser/README.md b/components/airparser/README.md new file mode 100644 index 0000000000000..34445dffd9908 --- /dev/null +++ b/components/airparser/README.md @@ -0,0 +1,11 @@ +# Overview + +The Airparser API simplifies the process of extracting data from emails, such as order confirmations, shipping notifications, or any structured email content. By transforming emails into structured data, it enables seamless integration of email data into various workflows. On Pipedream, you can leverage this API to create automations that act on the parsed data—like updating databases, triggering notifications, or integrating with other apps and services. + +# Example Use Cases + +- **Automated Customer Support Tickets**: Parse incoming support emails with Airparser and automatically create tickets in a helpdesk platform like Zendesk or Freshdesk. Extract details like customer name, issue description, and priority level to streamline support operations. + +- **E-commerce Order Processing**: Convert order confirmation emails into structured data to manage inventory or trigger fulfillment workflows. Connect Airparser to Shopify to update product quantities or to a shipping service to initiate the delivery process. + +- **Expense Tracking**: Extract information from receipts or invoice emails. Use Airparser to parse out details such as vendor, amount, and date, and then feed that data into an accounting app like QuickBooks or an expense tracking spreadsheet. diff --git a/components/airplane/README.md b/components/airplane/README.md new file mode 100644 index 0000000000000..49be37c97678e --- /dev/null +++ b/components/airplane/README.md @@ -0,0 +1,11 @@ +# Overview + +The Airplane API facilitates the creation and management of tasks and runs within the Airplane toolkit, which is geared towards automating developer operations and internal tools. In Pipedream, you can leverage this API to automate workflows, integrate with various services, and handle background tasks. By connecting Airplane to other apps on Pipedream, you can streamline processes like deploying code, managing feature flags, or orchestrating complex workflows that interact with other APIs and services. + +# Example Use Cases + +- **Scheduled Deployment Trigger**: Automatically trigger a deployment task in Airplane using Pipedream's scheduled events. This can be set up to deploy nightly builds or after certain conditions are met in your code repository. + +- **Slack Command to Run Airplane Tasks**: Set up a Pipedream workflow that starts an Airplane task using a Slash command in Slack. This allows for quick, chat-based interactions to execute operations without leaving the conversation. + +- **Error Logging to Airplane from Another Service**: Capture errors from an application monitored by, say, Sentry, and log them to Airplane via Pipedream. This could then trigger a task in Airplane to handle the error or notify the appropriate team. diff --git a/components/airship/README.md b/components/airship/README.md new file mode 100644 index 0000000000000..45db15733707b --- /dev/null +++ b/components/airship/README.md @@ -0,0 +1,11 @@ +# Overview + +The Airship API provides tools to engage customers across SMS, email, app notifications, and more. By leveraging this API, you can automate messaging based on user behavior, preferences, and lifecycle events. Integrating Airship with Pipedream allows you to connect these communication capabilities with hundreds of other services to build robust, automated workflows. This lets you craft personalized customer journeys, respond promptly to user actions, and monitor the effectiveness of different communication strategies. + +# Example Use Cases + +- **Customer Onboarding Automation**: Trigger a welcome email and app notification sequence when a new user signs up in your app. Use Airship to manage the messaging sequence and trigger emails or notifications based on specific user actions or time delays. + +- **Behavior-Triggered Offers**: Send personalized offers via SMS or in-app messages when a user performs a significant action, like adding items to a cart but not completing a purchase. Connect Airship with an e-commerce platform like Shopify to monitor cart status and trigger timely messages aimed at conversion. + +- **Event-Driven Feedback Collection**: Collect feedback through email or mobile messages post-event or interaction. Set up a workflow where Airship sends a feedback request after a customer attends a webinar or a major event, using webinar platforms like Zoom or event management systems to trigger the communication. diff --git a/components/airship/airship.app.mjs b/components/airship/airship.app.mjs new file mode 100644 index 0000000000000..4b1c2540ac19d --- /dev/null +++ b/components/airship/airship.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "airship", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/airship/package.json b/components/airship/package.json new file mode 100644 index 0000000000000..1ae26745d67db --- /dev/null +++ b/components/airship/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/airship", + "version": "0.0.1", + "description": "Pipedream Airship Components", + "main": "airship.app.mjs", + "keywords": [ + "pipedream", + "airship" + ], + "homepage": "https://pipedream.com/apps/airship", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/airslate/README.md b/components/airslate/README.md new file mode 100644 index 0000000000000..a2d12467c561a --- /dev/null +++ b/components/airslate/README.md @@ -0,0 +1,11 @@ +# Overview + +The Airslate API enables automation of document workflows, including creation, updating, and tracking of e-signature processes. In Pipedream, you can leverage this API to streamline document handling, automate data syncing between apps, and manage notifications related to your digital documents. Utilizing Airslate's capabilities within Pipedream workflows allows for easy integration with other services, enhancing productivity and minimizing manual tasks. + +# Example Use Cases + +- **Automated Document Creation and Distribution**: Trigger a workflow in Pipedream when a new entry is added to a CRM like Salesforce. Use the Airslate API to create and send a customized contract, ensuring all new clients receive their agreements without delay. + +- **E-Signature Status Tracking to CRM**: When a document's status changes in Airslate, such as when a signature is completed, you can set up a Pipedream workflow to update the corresponding record in a CRM app like HubSpot, keeping sales teams informed in real-time. + +- **Real-Time Notifications for Document Actions**: Use Airslate API to monitor document activities, and with Pipedream's help, send notifications through communication platforms like Slack or email whenever critical updates (e.g., document viewed, signed, or commented) occur, keeping all stakeholders in the loop. diff --git a/components/airtable_oauth/README.md b/components/airtable_oauth/README.md index 0da5a1bbab6bb..edec0f7af75f8 100644 --- a/components/airtable_oauth/README.md +++ b/components/airtable_oauth/README.md @@ -1,10 +1,11 @@ # Overview -Using the Airtable API, you can build applications that can: - -- Create and manage databases -- Add, update, and delete records -- Search for records -- Sort and filter records -- Display records in a variety of ways -- Import and export data +Airtable (OAuth) API on Pipedream allows you to manipulate and leverage your Airtable data in a myriad of powerful ways. Sync data between Airtable and other apps, trigger workflows on updates, or process bulk data operations asynchronously. By using Airtable's structured databases with Pipedream's serverless platform, you can craft custom automation solutions, integrate with other services seamlessly, and streamline complex data processes. + +# Example Use Cases + +- **Sync Airtable Records with Google Sheets**: Automatically sync new or updated Airtable records to a Google Sheets spreadsheet, enabling real-time data sharing with teams reliant on Google Workspace. This is ideal for maintaining consistent datasets across platforms without manual updates. + +- **Aggregate and Email Weekly Reports**: Collect data from an Airtable base at the end of each week, create a summary report, and send it via email with SendGrid or Gmail. Perfect for providing stakeholders with regular insights without manual report generation. + +- **Trigger SMS Notifications on Record Status Change**: Monitor an Airtable base for specific status changes and use Twilio to send an SMS notification whenever a record meets the criteria. This workflow ensures immediate alerts for time-sensitive operations. diff --git a/components/airtable_oauth/actions/common/common-list.mjs b/components/airtable_oauth/actions/common/common-list.mjs index 6e86ef8858b99..764fb0ad26f66 100644 --- a/components/airtable_oauth/actions/common/common-list.mjs +++ b/components/airtable_oauth/actions/common/common-list.mjs @@ -2,7 +2,7 @@ import airtable from "../../airtable_oauth.app.mjs"; export default { props: { - sortField: { + sortFieldId: { propDefinition: [ airtable, "sortFieldId", @@ -45,31 +45,27 @@ export default { const tableId = this.tableId?.value ?? this.tableId; const viewId = this.viewId?.value ?? this.viewId; - const data = []; - const params = { - returnFieldsByFieldId: this.returnFieldsByFieldId, + const config = { + returnFieldsByFieldId: this.returnFieldsByFieldId || false, }; - if (viewId) { params.view = viewId; } - if (this.filterByFormula) { params.filterByFormula = this.filterByFormula; } - if (this.maxRecords) { params.maxRecords = this.maxRecords; } - if (this.sortField && this.sortDirection) { - params["sort[0][field]"] = this.sortField; - params["sort[0][direction]"] = this.sortDirection; + if (viewId) { config.view = viewId; } + if (this.filterByFormula) { config.filterByFormula = this.filterByFormula; } + if (this.maxRecords) { config.maxRecords = this.maxRecords; } + if (this.sortFieldId && this.sortDirection) { + config.sort = [ + { + field: this.sortFieldId, + direction: this.sortDirection, + }, + ]; } - do { - const { - records, offset, - } = await this.airtable.listRecords({ - baseId, - tableId, - params, - $, - }); - data.push(...records); - params.offset = offset; - } while (params.offset); + const data = await this.airtable.listRecords({ + baseId, + tableId, + params: config, + }); const l = data.length; $.export("$summary", `Fetched ${l} record${l === 1 diff --git a/components/airtable_oauth/actions/common/common.mjs b/components/airtable_oauth/actions/common/common.mjs index 706036784002f..d1feef0608286 100644 --- a/components/airtable_oauth/actions/common/common.mjs +++ b/components/airtable_oauth/actions/common/common.mjs @@ -10,6 +10,11 @@ export default { ], withLabel: true, }, + warningAlert: { + type: "alert", + alertType: "warning", + content: "**Note:** if using a custom expression to specify the `Base` (e.g. `{{steps.mydata.$return_value}}`) you should also use a custom expression for the `Table`, and any other props that depend on it.", + }, tableId: { propDefinition: [ airtable, diff --git a/components/airtable_oauth/actions/create-comment/create-comment.mjs b/components/airtable_oauth/actions/create-comment/create-comment.mjs index 9d14b58011006..63e8bc27c0c7e 100644 --- a/components/airtable_oauth/actions/create-comment/create-comment.mjs +++ b/components/airtable_oauth/actions/create-comment/create-comment.mjs @@ -3,8 +3,8 @@ import common from "../common/common.mjs"; export default { key: "airtable_oauth-create-comment", name: "Create Comment", - description: "Create a new comment on a record. [See the documentation](https://airtable.com/developers/web/api/create-comment)", - version: "0.0.5", + description: "Create a comment on a selected record. [See the documentation](https://airtable.com/developers/web/api/create-comment)", + version: "0.0.8", type: "action", props: { ...common.props, @@ -23,7 +23,7 @@ export default { comment: { type: "string", label: "Comment", - description: "The text comment", + description: "The text comment to create", }, }, async run({ $ }) { diff --git a/components/airtable_oauth/actions/create-field/create-field.mjs b/components/airtable_oauth/actions/create-field/create-field.mjs index 5142a264cd8bd..870e87ff4f5ee 100644 --- a/components/airtable_oauth/actions/create-field/create-field.mjs +++ b/components/airtable_oauth/actions/create-field/create-field.mjs @@ -1,30 +1,53 @@ +import constants from "../../sources/common/constants.mjs"; import common from "../common/common.mjs"; export default { key: "airtable_oauth-create-field", name: "Create Field", description: "Create a new field in a table. [See the documentation](https://airtable.com/developers/web/api/create-field)", - version: "0.0.5", + version: "0.1.0", type: "action", props: { ...common.props, - field: { + name: { type: "string", - label: "Field", - description: "A JSON object representing the field. Refer to [field types](https://airtable.com/developers/web/api/model/field-type) for supported field types, the write format for field options, and other specifics for certain field types.", + label: "Field Name", + description: "The name of the field", + }, + type: { + type: "string", + label: "Field Type", + description: "The field type. [See the documentation](https://airtable.com/developers/web/api/model/field-type) for more information.", + options: constants.FIELD_TYPES, + }, + description: { + type: "string", + label: "Field Description", + description: "The description of the field", + optional: true, + }, + options: { + type: "object", + label: "Field Options", + description: "The options for the field as a JSON object, e.g. `{ \"color\": \"greenBright\" }`. Each type has a specific set of options - [see the documentation](https://airtable.com/developers/web/api/field-model) for more information.", + optional: true, }, }, async run({ $ }) { - const field = typeof this.field === "object" - ? this.field - : JSON.parse(this.field); - const data = { - ...field, - }; + const { + description, name, options, type, + } = this; const response = await this.airtable.createField({ baseId: this.baseId.value, tableId: this.tableId.value, - data, + data: { + name, + type, + description, + options: typeof options === "string" + ? JSON.parse(options) + : options, + }, $, }); diff --git a/components/airtable_oauth/actions/create-multiple-records/create-multiple-records.mjs b/components/airtable_oauth/actions/create-multiple-records/create-multiple-records.mjs index 17d00644f406b..ae42ef3634a6e 100644 --- a/components/airtable_oauth/actions/create-multiple-records/create-multiple-records.mjs +++ b/components/airtable_oauth/actions/create-multiple-records/create-multiple-records.mjs @@ -1,14 +1,15 @@ import chunk from "lodash.chunk"; import airtable from "../../airtable_oauth.app.mjs"; import common from "../common/common.mjs"; +import { ConfigurationError } from "@pipedream/platform"; const BATCH_SIZE = 10; // The Airtable API allows us to update up to 10 rows per request. export default { key: "airtable_oauth-create-multiple-records", name: "Create Multiple Records", - description: "Create one or more records in a table by passing an array of objects containing field names and values as key/value pairs. [See the documentation](https://airtable.com/developers/web/api/create-records)", - version: "0.0.5", + description: "Create one or more records in a table in a single operation with an array. [See the documentation](https://airtable.com/developers/web/api/create-records)", + version: "0.0.8", type: "action", props: { ...common.props, @@ -18,6 +19,15 @@ export default { "records", ], }, + customExpressionInfo: { + type: "alert", + alertType: "info", + content: `You can use a custom expression that evaluates to an object for each entry in the array, e.g. \`{{ { "foo": "bar", "id": 123 } }}\`. +\\ +You can also reference an object exported by a previous step, e.g. \`{{steps.foo.$return_value}}\`. +\\ +If desired, you can use a custom expression in the same fashion for the entire array instead of providing individual values.`, + }, typecast: { propDefinition: [ airtable, @@ -39,9 +49,18 @@ export default { if (!Array.isArray(data)) { data = JSON.parse(data); } - data = data.map((fields) => ({ - fields, - })); + data = data.map((fields, index) => { + if (typeof fields === "string") { + try { + fields = JSON.parse(fields); + } catch (err) { + throw new ConfigurationError(`Error parsing record (index ${index}) as JSON: ${err.message}`); + } + } + return { + fields, + }; + }); if (!data.length) { throw new Error("No Airtable record data passed to step. Please pass at least one record"); } @@ -49,15 +68,14 @@ export default { const responses = []; for (const c of chunk(data, BATCH_SIZE)) { try { - const { records } = await this.airtable.createRecord({ + const records = await this.airtable.createRecord({ baseId, tableId, - data: { + data: c, + opts: { typecast: this.typecast, returnFieldsByFieldId: this.returnFieldsByFieldId, - records: c, }, - $, }); responses.push(...records); } catch (err) { diff --git a/components/airtable_oauth/actions/create-or-update-record/create-or-update-record.mjs b/components/airtable_oauth/actions/create-or-update-record/create-or-update-record.mjs index 59dcef26fb386..fd6862653af4c 100644 --- a/components/airtable_oauth/actions/create-or-update-record/create-or-update-record.mjs +++ b/components/airtable_oauth/actions/create-or-update-record/create-or-update-record.mjs @@ -4,13 +4,12 @@ import commonActions from "../../common/actions.mjs"; export default { key: "airtable_oauth-create-or-update-record", - name: "Create Single Record Or Update", - description: "Updates a record if `recordId` is provided or adds a record to a table.", - version: "0.0.6", + name: "Create or Update Record", + description: "Create a new record or update an existing one. [See the documentation](https://airtable.com/developers/web/api/create-records)", + version: "0.1.0", type: "action", props: { ...common.props, - // eslint-disable-next-line pipedream/props-label,pipedream/props-description tableId: { ...common.props.tableId, reloadProps: true, @@ -27,7 +26,7 @@ export default { }), ], optional: true, - description: "Enter a [record ID](https://support.airtable.com/hc/en-us/articles/360051564873-Record-ID) if you want to update an existing record. Leave blank to create a new record.", + description: "To update an existing record, select it from the list or provide its [Record ID](https://support.airtable.com/hc/en-us/articles/360051564873-Record-ID). If left blank, a new record will be created.", }, typecast: { propDefinition: [ diff --git a/components/airtable_oauth/actions/create-single-record/create-single-record.mjs b/components/airtable_oauth/actions/create-single-record/create-single-record.mjs index 7f0a825823abd..49466af77a122 100644 --- a/components/airtable_oauth/actions/create-single-record/create-single-record.mjs +++ b/components/airtable_oauth/actions/create-single-record/create-single-record.mjs @@ -6,7 +6,7 @@ export default { key: "airtable_oauth-create-single-record", name: "Create Single Record", description: "Adds a record to a table.", - version: "0.0.6", + version: "0.0.9", type: "action", props: { ...common.props, diff --git a/components/airtable_oauth/actions/create-table/create-table.mjs b/components/airtable_oauth/actions/create-table/create-table.mjs index 7e5a6f10a2023..c34f9ad644128 100644 --- a/components/airtable_oauth/actions/create-table/create-table.mjs +++ b/components/airtable_oauth/actions/create-table/create-table.mjs @@ -4,7 +4,7 @@ export default { key: "airtable_oauth-create-table", name: "Create Table", description: "Create a new table. [See the documentation](https://airtable.com/developers/web/api/create-table)", - version: "0.0.5", + version: "0.0.8", type: "action", props: { airtable, @@ -17,18 +17,18 @@ export default { name: { type: "string", label: "Name", - description: "The name for the table", + description: "The name of the table", }, description: { type: "string", label: "Description", - description: "The description for the table", + description: "The description of the table", optional: true, }, fields: { type: "string[]", label: "Fields", - description: "A list of JSON objects representing the fields in the table. Refer to [field types](https://airtable.com/developers/web/api/model/field-type) for supported field types, the write format for field options, and other specifics for certain field types.", + description: "A list of JSON objects representing the fields in the table. [See the documentation](https://airtable.com/developers/web/api/model/field-type) for supported field types, the write format for field options, and other specifics for certain field types.", }, }, async run({ $ }) { diff --git a/components/airtable_oauth/actions/delete-record/delete-record.mjs b/components/airtable_oauth/actions/delete-record/delete-record.mjs index 5c08fbd9fe6ac..9748b28a0ab9a 100644 --- a/components/airtable_oauth/actions/delete-record/delete-record.mjs +++ b/components/airtable_oauth/actions/delete-record/delete-record.mjs @@ -4,8 +4,8 @@ import common from "../common/common.mjs"; export default { key: "airtable_oauth-delete-record", name: "Delete Record", - description: "Delete a record from a table by record ID. [See the documentation](https://airtable.com/developers/web/api/delete-record)", - version: "0.0.5", + description: "Delete a selected record from a table. [See the documentation](https://airtable.com/developers/web/api/delete-record)", + version: "0.0.8", type: "action", props: { ...common.props, @@ -34,7 +34,6 @@ export default { baseId, tableId, recordId, - $, }); } catch (err) { this.airtable.throwFormattedError(err); diff --git a/components/airtable_oauth/actions/get-record-or-create/get-record-or-create.mjs b/components/airtable_oauth/actions/get-record-or-create/get-record-or-create.mjs index 9b360644dd63b..295986aefbe54 100644 --- a/components/airtable_oauth/actions/get-record-or-create/get-record-or-create.mjs +++ b/components/airtable_oauth/actions/get-record-or-create/get-record-or-create.mjs @@ -5,12 +5,11 @@ import commonActions from "../../common/actions.mjs"; export default { key: "airtable_oauth-get-record-or-create", name: "Get Record Or Create", - description: "Get a record from a table by record ID or create a new register.", - version: "0.0.6", + description: "Get a specific record, or create one if it doesn't exist. [See the documentation](https://airtable.com/developers/web/api/create-records)", + version: "0.0.9", type: "action", props: { ...common.props, - // eslint-disable-next-line pipedream/props-label,pipedream/props-description tableId: { ...common.props.tableId, reloadProps: true, diff --git a/components/airtable_oauth/actions/get-record/get-record.mjs b/components/airtable_oauth/actions/get-record/get-record.mjs index e3904b686a4c9..a579faec3b028 100644 --- a/components/airtable_oauth/actions/get-record/get-record.mjs +++ b/components/airtable_oauth/actions/get-record/get-record.mjs @@ -5,8 +5,8 @@ import commonActions from "../../common/actions.mjs"; export default { key: "airtable_oauth-get-record", name: "Get Record", - description: "Get a record from a table by record ID. [See the documentation](https://airtable.com/developers/web/api/get-record)", - version: "0.0.6", + description: "Get data of a selected record from a table. [See the documentation](https://airtable.com/developers/web/api/get-record)", + version: "0.0.9", type: "action", props: { ...common.props, @@ -22,12 +22,6 @@ export default { }), ], }, - returnFieldsByFieldId: { - propDefinition: [ - airtable, - "returnFieldsByFieldId", - ], - }, }, async run({ $ }) { return commonActions.getRecord(this, $); diff --git a/components/airtable_oauth/actions/list-records-in-view/list-records-in-view.mjs b/components/airtable_oauth/actions/list-records-in-view/list-records-in-view.mjs index 8085dbe5d76c1..ae2eeea18e221 100644 --- a/components/airtable_oauth/actions/list-records-in-view/list-records-in-view.mjs +++ b/components/airtable_oauth/actions/list-records-in-view/list-records-in-view.mjs @@ -4,11 +4,16 @@ import commonList from "../common/common-list.mjs"; export default { key: "airtable_oauth-list-records-in-view", name: "List Records in View", - description: "Retrieve records in a view with automatic pagination. Optionally sort and filter results. Only available for Enterprise accounts.", + description: "Retrieve records from a view, optionally sorting and filtering results. [See the documentation](https://airtable.com/developers/web/api/list-views)", type: "action", - version: "0.0.5", + version: "0.0.8", ...commonList, props: { + accountTierAlert: { + type: "alert", + alertType: "info", + content: "Note: views are only available for Airtable Enterprise accounts. [See the documentation](https://airtable.com/developers/web/api/list-views) for more information.", + }, ...common.props, viewId: { propDefinition: [ diff --git a/components/airtable_oauth/actions/list-records/list-records.mjs b/components/airtable_oauth/actions/list-records/list-records.mjs index b69bc7306d12c..c8dcf06f0a013 100644 --- a/components/airtable_oauth/actions/list-records/list-records.mjs +++ b/components/airtable_oauth/actions/list-records/list-records.mjs @@ -4,9 +4,9 @@ import commonList from "../common/common-list.mjs"; export default { key: "airtable_oauth-list-records", name: "List Records", - description: "Retrieve records from a table with automatic pagination. Optionally sort and filter results.", + description: "Retrieve records from a table, optionally sorting and filtering results. [See the documentation](https://airtable.com/developers/web/api/list-records)", type: "action", - version: "0.0.5", + version: "0.0.8", ...commonList, props: { ...common.props, diff --git a/components/airtable_oauth/actions/search-records/search-records.mjs b/components/airtable_oauth/actions/search-records/search-records.mjs index b76ee596bcbe5..3465137d21e1f 100644 --- a/components/airtable_oauth/actions/search-records/search-records.mjs +++ b/components/airtable_oauth/actions/search-records/search-records.mjs @@ -4,8 +4,8 @@ import { fieldTypeToPropType } from "../../common/utils.mjs"; export default { key: "airtable_oauth-search-records", name: "Search Records", - description: "Searches for a record by formula or by field value. [See the documentation](https://airtable.com/developers/web/api/list-records)", - version: "0.0.7", + description: "Search for a record by formula or by field value. [See the documentation](https://airtable.com/developers/web/api/list-records)", + version: "0.0.10", type: "action", props: { ...common.props, @@ -32,7 +32,7 @@ export default { props.searchFormula = { type: "string", label: "Search Formula", - description: "Use an Airtable search formula to find records. For example, if you want to find records with `Tags` includes `test-1`, use `FIND('test-1', {Tags})`. Learn more on [Airtable's website](https://support.airtable.com/docs/formula-field-reference)", + description: "Use an [Airtable search formula (see info on the documentation)](https://support.airtable.com/docs/formula-field-reference) to find records. For example, if you want to find records with `Tags` including `test-1`, use `FIND('test-1', {Tags})`.", optional: true, }; } @@ -90,21 +90,17 @@ export default { const params = { filterByFormula, - returnFieldsByFieldId: this.returnFieldsByFieldId, + returnFieldsByFieldId: this.returnFieldsByFieldId || false, }; - const results = []; - do { - const { - records, offset, - } = await this.airtable.listRecords({ - baseId: this.baseId.value, - tableId: this.tableId.value, - params, - $, - }); - results.push(...records); - params.offset = offset; - } while (params.offset); + + const baseId = this.baseId?.value ?? this.baseId; + const tableId = this.tableId?.value ?? this.tableId; + + const results = await this.airtable.listRecords({ + baseId, + tableId, + params, + }); $.export("$summary", `Found ${results.length} record${results.length === 1 ? "" diff --git a/components/airtable_oauth/actions/update-comment/update-comment.mjs b/components/airtable_oauth/actions/update-comment/update-comment.mjs index 908bbe8e4854b..9ff68dc33def5 100644 --- a/components/airtable_oauth/actions/update-comment/update-comment.mjs +++ b/components/airtable_oauth/actions/update-comment/update-comment.mjs @@ -3,8 +3,8 @@ import common from "../common/common.mjs"; export default { key: "airtable_oauth-update-comment", name: "Update Comment", - description: "Updates an existing comment on a record. [See the documentation](https://airtable.com/developers/web/api/update-comment)", - version: "0.0.5", + description: "Update an existing comment on a selected record. [See the documentation](https://airtable.com/developers/web/api/update-comment)", + version: "0.0.8", type: "action", props: { ...common.props, @@ -36,7 +36,7 @@ export default { comment: { type: "string", label: "Comment", - description: "The text comment", + description: "The new content of the comment", }, }, async run({ $ }) { diff --git a/components/airtable_oauth/actions/update-field/update-field.mjs b/components/airtable_oauth/actions/update-field/update-field.mjs index 2621353d46b31..6662c21391496 100644 --- a/components/airtable_oauth/actions/update-field/update-field.mjs +++ b/components/airtable_oauth/actions/update-field/update-field.mjs @@ -4,8 +4,8 @@ import { ConfigurationError } from "@pipedream/platform"; export default { key: "airtable_oauth-update-field", name: "Update Field", - description: "Updates an existing field in a table. [See the documentation](https://airtable.com/developers/web/api/update-field)", - version: "0.0.5", + description: "Update an existing field in a table. [See the documentation](https://airtable.com/developers/web/api/update-field)", + version: "0.0.8", type: "action", props: { ...common.props, @@ -27,19 +27,19 @@ export default { name: { type: "string", label: "Name", - description: "The name of the field", + description: "The new name of the field", optional: true, }, description: { type: "string", label: "Description", - description: "The description for the field", + description: "The new description of the field", optional: true, }, }, async run({ $ }) { if (!this.name && !this.description) { - throw new ConfigurationError("At least one of `name` or `description` must be provided."); + throw new ConfigurationError("At least one of `Name` or `Description` must be provided."); } const data = {}; diff --git a/components/airtable_oauth/actions/update-record/update-record.mjs b/components/airtable_oauth/actions/update-record/update-record.mjs index 5d0f53da0e7a4..9a8694e726185 100644 --- a/components/airtable_oauth/actions/update-record/update-record.mjs +++ b/components/airtable_oauth/actions/update-record/update-record.mjs @@ -6,7 +6,7 @@ export default { key: "airtable_oauth-update-record", name: "Update Record", description: "Update a single record in a table by Record ID. [See the documentation](https://airtable.com/developers/web/api/update-record)", - version: "0.0.6", + version: "0.0.9", type: "action", props: { ...common.props, diff --git a/components/airtable_oauth/actions/update-table/update-table.mjs b/components/airtable_oauth/actions/update-table/update-table.mjs index 3f6a5db1e3849..44b8c9be1ce71 100644 --- a/components/airtable_oauth/actions/update-table/update-table.mjs +++ b/components/airtable_oauth/actions/update-table/update-table.mjs @@ -3,21 +3,21 @@ import common from "../common/common.mjs"; export default { key: "airtable_oauth-update-table", name: "Update Table", - description: "Updates an existing table. [See the documentation](https://airtable.com/developers/web/api/update-table)", - version: "0.0.5", + description: "Update an existing table. [See the documentation](https://airtable.com/developers/web/api/update-table)", + version: "0.0.8", type: "action", props: { ...common.props, name: { type: "string", label: "Name", - description: "The name for the table", + description: "The updated name of the table", optional: true, }, description: { type: "string", label: "Description", - description: "The description for the table", + description: "The updated description of the table", optional: true, }, }, diff --git a/components/airtable_oauth/airtable_oauth.app.mjs b/components/airtable_oauth/airtable_oauth.app.mjs index 2357eca45d3e8..5d588c6572b59 100644 --- a/components/airtable_oauth/airtable_oauth.app.mjs +++ b/components/airtable_oauth/airtable_oauth.app.mjs @@ -1,3 +1,4 @@ +import Airtable from "airtable"; import { ConfigurationError } from "@pipedream/platform"; import { fieldTypeToPropType } from "./common/utils.mjs"; import { axios } from "@pipedream/platform"; @@ -17,7 +18,7 @@ export default { baseId: { type: "string", label: "Base", - description: "The base ID", + description: "The Base ID.", async options({ prevContext }) { const params = {}; if (prevContext?.newOffset) { @@ -43,7 +44,7 @@ export default { tableId: { type: "string", label: "Table", - description: "The table ID. If referencing a **Base** dynamically using data from another step (e.g., `{{steps.trigger.event.metadata.baseId}}`), you will not be able to select from the list of Tables, and automatic table options will not work when configuring this step. Please enter a custom expression to specify the **Table**.", + description: "The Table ID.", async options({ baseId }) { let tables; try { @@ -62,7 +63,7 @@ export default { viewId: { type: "string", label: "View", - description: "The view ID. If referencing a **Table** dynamically using data from another step (e.g., `{{steps.trigger.event.metadata.tableId}}`), you will not be able to select from the list of Views for this step. Please enter a custom expression to specify the **View**.", + description: "The View ID.", async options({ baseId, tableId, }) { @@ -84,8 +85,8 @@ export default { }, sortFieldId: { type: "string", - label: "Sort: Field", - description: "Optionally select a field to sort results. To sort by multiple fields, use the **Filter by Forumla** field. If referencing a **Table** dynamically using data from another step (e.g., `{{steps.mydata.$return_value}}`), automatic field options won't work when configuring this step. Please enter a custom expression to specify the **Sort: Field**.", + label: "Sort by Field", + description: "Optionally select a field to sort results by. To sort by multiple fields, use the **Filter by Formula** field.", optional: true, async options({ baseId, tableId, @@ -134,29 +135,16 @@ export default { label: "Record ID", description: "Identifier of a record", async options({ - baseId, tableId, prevContext, + baseId, tableId, }) { - const params = {}; - if (prevContext?.newOffset) { - params.offset = prevContext.newOffset; - } - const { - records, offset, - } = await this.listRecords({ + const records = await this.listRecords({ baseId, tableId, - params, }); - const options = (records ?? []).map((record) => ({ + return (records ?? []).map((record) => ({ label: record.fields?.Name || record.id, value: record.id, })); - return { - options, - context: { - newOffset: offset, - }, - }; }, }, commentId: { @@ -192,14 +180,14 @@ export default { }, returnFieldsByFieldId: { type: "boolean", - label: "Return Fields By Field ID", - description: "An optional boolean value that lets you return field objects where the key is the field id. This defaults to `false`, which returns field objects where the key is the field name.", + label: "Return Fields By ID", + description: "If set to `true`, the returned field objects will have the field ID as the key, instead of the field name.", optional: true, }, sortDirection: { type: "string", label: "Sort: Direction", - description: "This field will be ignored if you don't select a field to sort by.", + description: "If sorting by a field, which direction to sort by.", options: SORT_DIRECTION_OPTIONS, default: "desc", optional: true, @@ -207,33 +195,45 @@ export default { maxRecords: { type: "integer", label: "Max Records", - description: "Optionally limit the maximum number of records to return. Leave blank to retrieve all records.", + description: "The maximum number of records to return. Leave blank to retrieve all records.", optional: true, }, filterByFormula: { type: "string", label: "Filter by Formula", - description: "Optionally provide a [formula](https://support.airtable.com/hc/en-us/articles/203255215-Formula-Field-Reference) used to filter records. The formula will be evaluated for each record, and if the result is not `0`, `false`, `\"\"`, `NaN`, `[]`, or `#Error!` the record will be included in the response. For example, to only include records where `Name` isn't empty, pass `NOT({Name} = '')`.", + description: "Optionally provide a [formula (see info on the documentation)](https://support.airtable.com/hc/en-us/articles/203255215-Formula-Field-Reference) used to filter records. The formula will be evaluated for each record, and if the result is not `0`, `false`, `\"\"`, `NaN`, `[]`, or `#Error!` the record will be included in the response. For example, to only include records where `Name` isn't empty, use `NOT({Name} = '')`.", optional: true, }, records: { - type: "string", + type: "string[]", label: "Records", - description: "Provide an array of objects. Each object should represent a new record with the column name as the key and the data to insert as the corresponding value (e.g., passing `[{\"foo\":\"bar\",\"id\":123},{\"foo\":\"baz\",\"id\":456}]` will create two records and with values added to the fields `foo` and `id`). The most common pattern is to reference an array of objects exported by a previous step (e.g., `{{steps.foo.$return_value}}`). You may also enter or construct a string that will `JSON.parse()` to an array of objects.", + description: "Each item in the array should be an object in JSON format, representing a new record. The keys are the column names and the corresponding values are the data to insert.", }, typecast: { type: "boolean", label: "Typecast", - description: "The Airtable API will perform best-effort automatic data conversion from string values if the typecast parameter is `True`. Automatic conversion is disabled by default to ensure data integrity, but it may be helpful for integrating with 3rd party data sources.", + description: "The Airtable API will perform best-effort automatic data conversion from string values if the typecast parameter is `True`. This is disabled by default to ensure data integrity, but it may be helpful for integrating with 3rd party data sources.", optional: true, }, record: { type: "object", label: "Record", - description: "Enter the column name for the key and the corresponding column value. You can include all, some, or none of the field values. You may also pass a JSON object as a custom expression with key/value pairs representing columns and values (e.g., `{{ {\"foo\":\"bar\",\"id\":123} }}`). A common pattern is to reference an object exported by a previous step (e.g., `{{steps.foo.$return_value}}`).", + description: "Enter the column name for the key and the corresponding column value. You can include all, some, or none of the field values. You may also use a custom expression.", + }, + customExpressionInfo: { + type: "alert", + alertType: "info", + content: `A custom expression can be a JSON object with key/value pairs representing columns and values, e.g. \`{{ { "foo": "bar", "id": 123 } }}\`. +\\ +You can also reference an object exported by a previous step, e.g. \`{{steps.foo.$return_value}}\`.`, }, }, methods: { + base(baseId) { + return new Airtable({ + apiKey: this.$auth.oauth_access_token, + }).base(baseId); + }, _baseUrl() { return "https://api.airtable.com/v0"; }, @@ -258,12 +258,10 @@ export default { : axios($, config); }, getRecord({ - baseId, tableId, recordId, ...args + baseId, tableId, recordId, }) { - return this._makeRequest({ - path: `/${baseId}/${tableId}/${recordId}`, - ...args, - }); + const base = this.base(baseId); + return base(tableId).find(recordId); }, listBases(args = {}) { return this._makeRequest({ @@ -279,13 +277,21 @@ export default { ...args, }); }, - listRecords({ - baseId, tableId, ...args + async listRecords({ + baseId, tableId, params = {}, }) { - return this._makeRequest({ - path: `/${baseId}/${tableId}`, - ...args, - }); + const base = this.base(baseId); + const data = []; + await base(tableId).select({ + ...params, + }) + .eachPage(function page(records, fetchNextPage) { + records.forEach(function(record) { + data.push(record._rawJson); + }); + fetchNextPage(); + }); + return data; }, listComments({ baseId, tableId, recordId, ...args @@ -296,13 +302,10 @@ export default { }); }, createRecord({ - baseId, tableId, ...args + baseId, tableId, data, opts, }) { - return this._makeRequest({ - path: `/${baseId}/${tableId}`, - method: "POST", - ...args, - }); + const base = this.base(baseId); + return base(tableId).create(data, opts); }, createTable({ baseId, ...args @@ -332,13 +335,10 @@ export default { }); }, updateRecord({ - baseId, tableId, recordId, ...args + baseId, tableId, recordId, data, opts, }) { - return this._makeRequest({ - path: `/${baseId}/${tableId}/${recordId}`, - method: "PATCH", - ...args, - }); + const base = this.base(baseId); + return base(tableId).update(recordId, data, opts); }, updateTable({ baseId, tableId, ...args @@ -368,13 +368,10 @@ export default { }); }, deleteRecord({ - baseId, tableId, recordId, ...args + baseId, tableId, recordId, }) { - return this._makeRequest({ - path: `/${baseId}/${tableId}/${recordId}`, - method: "DELETE", - ...args, - }); + const base = this.base(baseId); + return base(tableId).destroy(recordId); }, throwFormattedError(err) { throw Error(`${err.error} - ${err.statusCode} - ${err.message}`); @@ -398,5 +395,30 @@ export default { throw new Error("Invalid Record ID. See documentation about Finding Airtable record IDs - https://support.airtable.com/docs/finding-airtable-record-ids."); } }, + createWebhook({ + baseId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/bases/${baseId}/webhooks`, + ...opts, + }); + }, + deleteWebhook({ + baseId, webhookId, + }) { + return this._makeRequest({ + method: "DELETE", + path: `/bases/${baseId}/webhooks/${webhookId}`, + }); + }, + listWebhookPayloads({ + baseId, webhookId, ...opts + }) { + return this._makeRequest({ + path: `/bases/${baseId}/webhooks/${webhookId}/payloads`, + ...opts, + }); + }, }, }; diff --git a/components/airtable_oauth/common/actions.mjs b/components/airtable_oauth/common/actions.mjs index 9c81463093ff7..cac8ad905e6f6 100644 --- a/components/airtable_oauth/common/actions.mjs +++ b/components/airtable_oauth/common/actions.mjs @@ -23,6 +23,7 @@ export default { // Use record propDefinition directly to workaround lack of support // for propDefinition in additionalProps record: airtable.propDefinitions.record, + customExpressionInfo: airtable.propDefinitions.customExpressionInfo, }; } throw new ConfigurationError("Could not find a table for the specified base ID and table ID. Please adjust the action configuration to continue."); @@ -36,19 +37,16 @@ export default { ctx.airtable.validateRecord(record); - const data = { - fields: record, - typecast: ctx.typecast, - returnFieldsByFieldId: ctx.returnFieldsByFieldId, - }; - let response; try { response = await ctx.airtable.createRecord({ baseId, tableId, - data, - $, + data: record, + opts: { + typecast: ctx.typecast, + returnFieldsByFieldId: ctx.returnFieldsByFieldId, + }, }); } catch (err) { ctx.airtable.throwFormattedError(err); @@ -71,12 +69,11 @@ export default { baseId, tableId, recordId, - data: { - fields: record, + data: record, + opts: { typecast: ctx.typecast, returnFieldsByFieldId: ctx.returnFieldsByFieldId, }, - $, }); } catch (err) { ctx.airtable.throwFormattedError(err); @@ -85,28 +82,17 @@ export default { $.export("$summary", `Updated record "${recordId}" in ${ctx.baseId?.label || baseId}: [${ctx.tableId?.label || tableId}](https://airtable.com/${baseId}/${tableId})`); return response; }, - getRecord: async (ctx, $, throwRawError = false) => { + getRecord: async (ctx, $) => { const baseId = ctx.baseId?.value ?? ctx.baseId; const tableId = ctx.tableId?.value ?? ctx.tableId; const recordId = ctx.recordId; - let response; - try { - response = await ctx.airtable.getRecord({ - baseId, - tableId, - recordId, - params: { - returnFieldsByFieldId: ctx.returnFieldsByFieldId, - }, - $, - }); - } catch (err) { - if (throwRawError) { - throw err; - } - ctx.airtable.throwFormattedError(err); - } + ctx.airtable.validateRecordID(recordId); + const response = await ctx.airtable.getRecord({ + baseId, + tableId, + recordId, + }); $.export("$summary", `Fetched record "${recordId}" from ${ctx.baseId?.label || baseId}: [${ctx.tableId?.label || tableId}](https://airtable.com/${baseId}/${tableId})`); return response; diff --git a/components/airtable_oauth/common/constants.mjs b/components/airtable_oauth/common/constants.mjs index 4c0a5c4a4496c..ea291bc0018d0 100644 --- a/components/airtable_oauth/common/constants.mjs +++ b/components/airtable_oauth/common/constants.mjs @@ -32,8 +32,8 @@ const FieldType = { EXTERNAL_SYNC_SOURCE: "externalSyncSource", LAST_MODIFIED_BY: "lastModifiedBy", LAST_MODIFIED_TIME: "lastModifiedTime", - URL: "url", // string + URL: "url", SINGLE_COLLABORATOR: "singleCollaborator", DATE: "date", DATE_TIME: "dateTime", diff --git a/components/airtable_oauth/common/utils.mjs b/components/airtable_oauth/common/utils.mjs index 534de05c41bba..cdf11cd5a576e 100644 --- a/components/airtable_oauth/common/utils.mjs +++ b/components/airtable_oauth/common/utils.mjs @@ -37,9 +37,9 @@ function fieldTypeToPropType(fieldType) { case FieldType.EXTERNAL_SYNC_SOURCE: case FieldType.LAST_MODIFIED_BY: case FieldType.LAST_MODIFIED_TIME: - case FieldType.URL: return "object"; // string + case FieldType.URL: case FieldType.SINGLE_COLLABORATOR: case FieldType.DATE: case FieldType.DATE_TIME: @@ -70,7 +70,7 @@ function fieldToProp(field) { return { type: fieldTypeToPropType(field.type), label: field.name, - description: field.description, + description: field.description ?? `Field type: \`${field.type}\`. Field ID: \`${field.id}\``, optional: true, options: field.options?.choices?.map((choice) => ({ label: choice.name || choice.id, diff --git a/components/airtable_oauth/package.json b/components/airtable_oauth/package.json index 795d36e76e3b6..fe533ec5db12b 100644 --- a/components/airtable_oauth/package.json +++ b/components/airtable_oauth/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/airtable_oauth", - "version": "0.2.6", + "version": "0.4.0", "description": "Pipedream Airtable (OAuth) Components", "main": "airtable_oauth.app.mjs", "keywords": [ @@ -16,7 +16,6 @@ "@pipedream/platform": "^1.6.0", "airtable": "^0.11.1", "bottleneck": "^2.19.5", - "lodash": "^4.17.21", "lodash.chunk": "^4.2.0", "lodash.isempty": "^4.4.0", "moment": "^2.30.1" diff --git a/components/airtable_oauth/sources/common/common-webhook-field.mjs b/components/airtable_oauth/sources/common/common-webhook-field.mjs new file mode 100644 index 0000000000000..90b206ab963eb --- /dev/null +++ b/components/airtable_oauth/sources/common/common-webhook-field.mjs @@ -0,0 +1,74 @@ +import common from "./common-webhook.mjs"; + +export default { + ...common, + methods: { + ...common.methods, + getDataTypes() { + return [ + "tableFields", + ]; + }, + async saveAdditionalData() { + const tableData = await this.airtable.listTables({ + baseId: this.baseId, + }); + const filteredData = tableData?.tables?.map(({ + id, name, fields, + }) => ({ + id, + name, + fields, + })); + if (filteredData?.length) { + this.db.set("tableSchemas", filteredData); + } + }, + async emitEvent(payload) { + const [ + tableId, + tableData, + ] = Object.entries(payload.changedTablesById)[0]; + const [ + operation, + fieldObj, + ] = Object.entries(tableData)[0]; + const [ + fieldId, + fieldUpdateInfo, + ] = Object.entries(fieldObj)[0]; + + const timestamp = Date.parse(payload.timestamp); + if (this.isDuplicateEvent(fieldId, timestamp)) return; + this._setLastObjectId(fieldId); + this._setLastTimestamp(timestamp); + + const updateType = operation === "createdFieldsById" + ? "created" + : "updated"; + + let table = { + id: tableId, + }; + let field = { + id: fieldId, + }; + + const tableSchemas = this.db.get("tableSchemas"); + if (tableSchemas) { + table = tableSchemas.find(({ id }) => id === tableId); + field = table?.fields.find(({ id }) => id === fieldId); + delete table.fields; + } + + const summary = `Field ${updateType}: ${field.name ?? fieldUpdateInfo?.name ?? field.id}`; + + this.$emit({ + originalPayload: payload, + table, + field, + fieldUpdateInfo, + }, this.generateMeta(payload, summary)); + }, + }, +}; diff --git a/components/airtable_oauth/sources/common/common-webhook-record.mjs b/components/airtable_oauth/sources/common/common-webhook-record.mjs new file mode 100644 index 0000000000000..a610d402e830a --- /dev/null +++ b/components/airtable_oauth/sources/common/common-webhook-record.mjs @@ -0,0 +1,74 @@ +import common from "./common-webhook.mjs"; + +export default { + ...common, + methods: { + ...common.methods, + getDataTypes() { + return [ + "tableData", + ]; + }, + async emitEvent(payload) { + const [ + tableId, + tableData, + ] = Object.entries(payload.changedTablesById)[0]; + let [ + operation, + recordObj, + ] = Object.entries(tableData)[0]; + if (operation === "changedViewsById") { + const changedRecord = Object.entries(recordObj)[0]; + operation = changedRecord[0]; + recordObj = changedRecord[1]; + } + + // for deleted record(s) we'll emit only their ids (no other info is available) + if (operation === "destroyedRecordIds" && Array.isArray(recordObj)) { + const { length } = recordObj; + const summary = length === 1 + ? `Record deleted: ${recordObj[0]}` + : `${length} records deleted`; + this.$emit({ + originalPayload: payload, + tableId, + deletedRecordIds: recordObj, + }, this.generateMeta(payload, summary)); + return; + } + + const [ + recordId, + recordUpdateInfo, + ] = Object.entries(recordObj)[0]; + + const timestamp = Date.parse(payload.timestamp); + if (this.isDuplicateEvent(recordId, timestamp)) return; + this._setLastObjectId(recordId); + this._setLastTimestamp(timestamp); + + let updateType = operation === "createdRecordsById" + ? "created" + : "updated"; + + const { fields } = await this.airtable.getRecord({ + baseId: this.baseId, + tableId, + recordId, + }); + + const summary = `Record ${updateType}: ${fields?.name ?? recordId}`; + + this.$emit({ + originalPayload: payload, + tableId, + record: { + id: recordId, + fields, + }, + recordUpdateInfo, + }, this.generateMeta(payload, summary)); + }, + }, +}; diff --git a/components/airtable_oauth/sources/common/common-webhook.mjs b/components/airtable_oauth/sources/common/common-webhook.mjs new file mode 100644 index 0000000000000..8375fcc85fa22 --- /dev/null +++ b/components/airtable_oauth/sources/common/common-webhook.mjs @@ -0,0 +1,186 @@ +import airtable from "../../airtable_oauth.app.mjs"; +import constants from "../common/constants.mjs"; + +export default { + props: { + airtable, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + baseId: { + propDefinition: [ + airtable, + "baseId", + ], + }, + tableId: { + propDefinition: [ + airtable, + "tableId", + (c) => ({ + baseId: c.baseId, + }), + ], + description: "If specified, events will only be emitted for the selected Table", + optional: true, + }, + viewId: { + propDefinition: [ + airtable, + "viewId", + (c) => ({ + baseId: c.baseId, + tableId: c.tableId, + }), + ], + description: "If specified, events will only be emitted for the selected View", + optional: true, + }, + fromSources: { + type: "string[]", + label: "From Sources", + description: "Only emit events for updates from these sources. If omitted, updates from all sources are reported", + options: constants.FROM_SOURCES, + optional: true, + }, + }, + hooks: { + async activate() { + const { id } = await this.airtable.createWebhook({ + baseId: this.baseId, + data: { + notificationUrl: `${this.http.endpoint}/`, + specification: { + options: { + filters: { + recordChangeScope: this.viewId + ? this.viewId + : this.tableId + ? this.tableId + : undefined, + dataTypes: this.getDataTypes(), + changeTypes: this.getChangeTypes(), + fromSources: this.fromSources, + watchDataInFieldIds: this.watchDataInFieldIds, + watchSchemasOfFieldIds: this.watchSchemasOfFieldIds, + }, + includes: { + includeCellValuesInFieldIds: this.includeCellValuesInFieldIds, + includePreviousCellValues: true, + includePreviousFieldDefinitions: true, + }, + }, + }, + }, + }); + this._setHookId(id); + }, + async deactivate() { + const webhookId = this._getHookId(); + if (webhookId) { + await this.airtable.deleteWebhook({ + baseId: this.baseId, + webhookId, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + _getLastObjectId() { + return this.db.get("lastObjectId"); + }, + async _setLastObjectId(id) { + this.db.set("lastObjectId", id); + }, + _getLastTimestamp() { + return this.db.get("lastTimestamp"); + }, + async _setLastTimestamp(ts) { + this.db.set("lastTimestamp", ts); + }, + isDuplicateEvent(id, ts) { + const lastId = this._getLastObjectId(); + const lastTs = this._getLastTimestamp(); + + if (id === lastId && (ts - lastTs < 5000 )) { + console.log("Skipping trigger: another event was emitted for the same object within the last 5 seconds"); + return true; + } + + return false; + }, + getSpecificationOptions() { + throw new Error("getSpecificationOptions is not implemented"); + }, + generateMeta(payload, summary) { + return { + id: payload.baseTransactionNumber, + summary: summary ?? `New Event ${payload.baseTransactionNumber}`, + ts: Date.parse(payload.timestamp), + }; + }, + getDataTypes() { + return this.dataTypes; + }, + getChangeTypes() { + return this.changeTypes; + }, + emitDefaultEvent(payload) { + const meta = this.generateMeta(payload); + this.$emit(payload, meta); + }, + async emitEvent(payload) { + // sources may call this to customize event emission, but it is + // a separate method so that the source may fall back to default + return this.emitDefaultEvent(payload); + }, + async saveAdditionalData() { + // sources may call this to fetch additional data (e.g. field schemas) + // and it can be silently ignored when not required + return true; + }, + }, + async run() { + this.http.respond({ + status: 200, + }); + // webhook pings source, we then fetch webhook events to emit + const webhookId = this._getHookId(); + let hasMore = false; + const params = {}; + try { + await this.saveAdditionalData(); + } catch (err) { + console.log("Error fetching additional data, proceeding to event emission"); + console.log(err); + } + do { + const { + cursor, mightHaveMore, payloads, + } = await this.airtable.listWebhookPayloads({ + baseId: this.baseId, + webhookId, + params, + }); + for (const payload of payloads) { + try { + await this.emitEvent(payload); + } catch (err) { + console.log("Error emitting event, defaulting to default emission"); + console.log(err); + this.emitDefaultEvent(payload); + } + } + params.cursor = cursor; + hasMore = mightHaveMore; + } while (hasMore); + }, +}; diff --git a/components/airtable_oauth/sources/common/constants.mjs b/components/airtable_oauth/sources/common/constants.mjs new file mode 100644 index 0000000000000..dcabb1533a205 --- /dev/null +++ b/components/airtable_oauth/sources/common/constants.mjs @@ -0,0 +1,114 @@ +const DATA_TYPES = [ + { + value: "tableData", + label: "Record and cell value changes", + }, + { + value: "tableFields", + label: "Field changes", + }, + { + value: "tableMetadata", + label: "Table name and description changes", + }, +]; + +const CHANGE_TYPES = [ + { + label: "Created", + value: "add", + }, + { + label: "Deleted", + value: "remove", + }, + { + label: "Updated/Modified", + value: "update", + }, +]; + +const FROM_SOURCES = [ + { + value: "client", + label: "Changes generated by a user through the web or mobile clients", + }, + { + value: "publicApi", + label: "Changes generated through the Airtable API", + }, + { + value: "formSubmission", + label: "Changes generated when a form view is submitted", + }, + { + value: "formPageSubmission", + label: + "Changes generated when an interface form builder page, form layout page, or record creation button page is submitted", + }, + { + value: "automation", + label: "Changes generated through an automation action", + }, + { + value: "system", + label: + "Changes generated by system events, such as processing time function formulas", + }, + { + value: "sync", + label: "Changes generated through Airtable Sync", + }, + { + value: "anonymousUser", + label: "Changes generated by an anonymous user", + }, + { + value: "unknown", + label: "Unknown", + }, +]; + +const FIELD_TYPES = [ + "singleLineText", + "email", + "url", + "multilineText", + "number", + "percent", + "currency", + "singleSelect", + "multipleSelects", + "singleCollaborator", + "multipleCollaborators", + "multipleRecordLinks", + "date", + "dateTime", + "phoneNumber", + "multipleAttachments", + "checkbox", + "formula", + "createdTime", + "rollup", + "count", + "lookup", + "multipleLookupValues", + "autoNumber", + "barcode", + "rating", + "richText", + "duration", + "lastModifiedTime", + "button", + "createdBy", + "lastModifiedBy", + "externalSyncSource", + "aiText", +]; + +export default { + DATA_TYPES, + CHANGE_TYPES, + FROM_SOURCES, + FIELD_TYPES, +}; diff --git a/components/airtable_oauth/sources/new-field/new-field.mjs b/components/airtable_oauth/sources/new-field/new-field.mjs index ac0d7ce0608c0..e23469e6b0088 100644 --- a/components/airtable_oauth/sources/new-field/new-field.mjs +++ b/components/airtable_oauth/sources/new-field/new-field.mjs @@ -1,52 +1,19 @@ -import common from "../common/common.mjs"; +import common from "../common/common-webhook-field.mjs"; export default { - name: "New Field", - description: "Emit new event for each new field created in a table", + ...common, + name: "New Field Created (Instant)", + description: "Emit new event when a field is created in the selected table. [See the documentation](https://airtable.com/developers/web/api/get-base-schema)", key: "airtable_oauth-new-field", - version: "0.0.5", + version: "1.0.0", type: "source", - props: { - ...common.props, - tableId: { - propDefinition: [ - common.props.airtable, - "tableId", - ({ baseId }) => ({ - baseId, - }), - ], - description: "The table ID to watch for changes.", - }, - }, + dedupe: "unique", methods: { - _getFieldIds() { - return this.db.get("fieldIds") || []; + ...common.methods, + getChangeTypes() { + return [ + "add", + ]; }, - _setFieldIds(fieldIds) { - this.db.set("fieldIds", fieldIds); - }, - }, - async run() { - const fieldIds = this._getFieldIds(); - - const { tables } = await this.airtable.listTables({ - baseId: this.baseId, - }); - const { fields } = tables.find(({ id }) => id === this.tableId); - - for (const field of fields) { - if (fieldIds.includes(field.id)) { - continue; - } - fieldIds.push(field.id); - this.$emit(field, { - id: field.id, - summary: field.name, - ts: Date.now(), - }); - } - - this._setFieldIds(fieldIds); }, }; diff --git a/components/airtable_oauth/sources/new-modified-or-deleted-records-instant/new-modified-or-deleted-records-instant.mjs b/components/airtable_oauth/sources/new-modified-or-deleted-records-instant/new-modified-or-deleted-records-instant.mjs new file mode 100644 index 0000000000000..ed93e57ea63e9 --- /dev/null +++ b/components/airtable_oauth/sources/new-modified-or-deleted-records-instant/new-modified-or-deleted-records-instant.mjs @@ -0,0 +1,52 @@ +import common from "../common/common-webhook-record.mjs"; +import constants from "../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; +import airtable from "../../airtable_oauth.app.mjs"; + +export default { + ...common, + name: "New Record Created, Updated or Deleted (Instant)", + description: "Emit new event when a record is added, updated, or deleted in a table or selected view.", + key: "airtable_oauth-new-modified-or-deleted-records-instant", + version: "0.1.0", + type: "source", + dedupe: "unique", + props: { + ...common.props, + changeTypes: { + type: "string[]", + label: "Update Types", + description: "Select the types of record updates that should emit events. If not specified, all updates will emit events.", + options: constants.CHANGE_TYPES, + optional: true, + default: [ + "add", + "remove", + "update", + ], + }, + watchDataInFieldIds: { + propDefinition: [ + airtable, + "sortFieldId", + (c) => ({ + baseId: c.baseId, + tableId: c.tableId, + }), + ], + type: "string[]", + label: "Watch Data In Field Ids", + description: + "Only emit events for updates that modify values in cells in these fields. If omitted, all fields within the table/view/base are watched", + }, + }, + methods: { + ...common.methods, + getDataTypes() { + return [ + "tableData", + ]; + }, + }, + sampleEmit, +}; diff --git a/components/airtable_oauth/sources/new-modified-or-deleted-records-instant/test-event.mjs b/components/airtable_oauth/sources/new-modified-or-deleted-records-instant/test-event.mjs new file mode 100644 index 0000000000000..ddd514fb8edd5 --- /dev/null +++ b/components/airtable_oauth/sources/new-modified-or-deleted-records-instant/test-event.mjs @@ -0,0 +1,29 @@ +export default { + "timestamp": "2024-03-14T18:42:14.226Z", + "baseTransactionNumber": 452, + "actionMetadata": { + "source": "publicApi", + "sourceMetadata": { + "user": { + "id": "usrvPSLNpIJn5awea", + "email": "test@example.com", + "permissionLevel": "create", + "name": "Test User", + } + } + }, + "payloadFormat": "v0", + "changedTablesById": { + "tblMtBkCWEfw6V9sc": { + "changedRecordsById": { + "rec0G1298ylRGkxg5": { + "current": { + "cellValuesByFieldId": { + "fldDGPyvGEFwMSk6v": "test" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/components/airtable_oauth/sources/new-modified-or-deleted-records/new-modified-or-deleted-records.mjs b/components/airtable_oauth/sources/new-modified-or-deleted-records/new-modified-or-deleted-records.mjs index 0f13fac163c23..a288733b85fc5 100644 --- a/components/airtable_oauth/sources/new-modified-or-deleted-records/new-modified-or-deleted-records.mjs +++ b/components/airtable_oauth/sources/new-modified-or-deleted-records/new-modified-or-deleted-records.mjs @@ -4,10 +4,11 @@ import moment from "moment"; export default { ...base, name: "New, Modified or Deleted Records", + description: "Emit new event each time a record is added, updated, or deleted in an Airtable table. Supports tables up to 10,000 records", key: "airtable_oauth-new-modified-or-deleted-records", - version: "0.0.5", + version: "0.0.8", type: "source", - description: "Emit new event each time a record is added, updated, or deleted in an Airtable table. Supports tables up to 10,000 records", + dedupe: "unique", props: { ...base.props, tableId: { @@ -54,10 +55,10 @@ export default { const lastTimestamp = this._getLastTimestamp(); const params = { filterByFormula: `LAST_MODIFIED_TIME() > "${lastTimestamp}"`, - returnFieldsByFieldId: this.returnFieldsByFieldId, + returnFieldsByFieldId: this.returnFieldsByFieldId || false, }; - const data = await this.airtable.listRecords({ + const records = await this.airtable.listRecords({ baseId, tableId, params, @@ -68,8 +69,8 @@ export default { modifiedRecordsCount = 0, deletedRecordsCount = 0; - if (data.records) { - for (const record of data.records) { + if (records) { + for (const record of records) { if (!lastTimestamp || moment(record.createdTime) > moment(lastTimestamp)) { record.type = "new_record"; newRecordsCount++; @@ -89,24 +90,15 @@ export default { delete params.filterByFormula; - while (allRecordIds.length === 0 || params.offset) { - const data = await this.airtable.listRecords({ - baseId, - tableId, - params, - rateLimited: true, - }); - if (!data.records.length || data.records.length === 0) return; - allRecordIds = [ - ...allRecordIds, - ...data.records.map((record) => record.id), - ]; - if (data.offset) { - params.offset = data.offset; - } else { - delete params.offset; - } - } + const data = await this.airtable.listRecords({ + baseId, + tableId, + params, + }); + if (!data.length || data.length === 0) return; + allRecordIds = [ + ...data.map((record) => record.id), + ]; if (prevAllRecordIds) { const deletedRecordIds = prevAllRecordIds.filter( @@ -120,7 +112,7 @@ export default { id: recordID, }; this.$emit(deletedRecordObj, { - summary: "record_deleted", + summary: `Record deleted: ${recordID}`, id: recordID, }); } diff --git a/components/airtable_oauth/sources/new-or-modified-field/new-or-modified-field.mjs b/components/airtable_oauth/sources/new-or-modified-field/new-or-modified-field.mjs index a5466545ed220..4d7c6ea787d45 100644 --- a/components/airtable_oauth/sources/new-or-modified-field/new-or-modified-field.mjs +++ b/components/airtable_oauth/sources/new-or-modified-field/new-or-modified-field.mjs @@ -1,56 +1,38 @@ -import common from "../common/common.mjs"; +import common from "../common/common-webhook-field.mjs"; +import airtable from "../../airtable_oauth.app.mjs"; export default { - name: "New or Modified Field", - description: "Emit new event for each new or modified field in a table", + ...common, + name: "New or Modified Field (Instant)", + description: "Emit new event when a field is created or updated in the selected table", key: "airtable_oauth-new-or-modified-field", - version: "0.0.5", + version: "1.0.0", type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getChangeTypes() { + return [ + "add", + "update", + ]; + }, + }, props: { ...common.props, - tableId: { + watchSchemasOfFieldIds: { propDefinition: [ - common.props.airtable, - "tableId", - ({ baseId }) => ({ - baseId, + airtable, + "sortFieldId", + (c) => ({ + baseId: c.baseId, + tableId: c.tableId, }), ], - description: "The table ID to watch for changes.", - }, - }, - methods: { - _getPrevFields() { - return this.db.get("fieldIds") || {}; + type: "string[]", + label: "Field Schemas to Watch", + description: + "Only emit events for updates that modify the schemas of these fields. If omitted, schemas of all fields within the table/view/base are watched", }, - _setPrevFields(fieldIds) { - this.db.set("fieldIds", fieldIds); - }, - }, - async run() { - const prevFields = this._getPrevFields(); - - const { tables } = await this.airtable.listTables({ - baseId: this.baseId, - }); - const { fields } = tables.find(({ id }) => id === this.tableId); - - for (const field of fields) { - if (prevFields[field.id] && prevFields[field.id] === JSON.stringify(field)) { - continue; - } - const summary = prevFields[field.id] - ? `${field.name} Updated` - : `${field.name} Created`; - prevFields[field.id] = JSON.stringify(field); - - this.$emit(field, { - id: field.id, - summary, - ts: Date.now(), - }); - } - - this._setPrevFields(prevFields); }, }; diff --git a/components/airtable_oauth/sources/new-or-modified-records-in-view/new-or-modified-records-in-view.mjs b/components/airtable_oauth/sources/new-or-modified-records-in-view/new-or-modified-records-in-view.mjs index cad38e5195257..1a70d3d604cbf 100644 --- a/components/airtable_oauth/sources/new-or-modified-records-in-view/new-or-modified-records-in-view.mjs +++ b/components/airtable_oauth/sources/new-or-modified-records-in-view/new-or-modified-records-in-view.mjs @@ -6,8 +6,9 @@ export default { name: "New or Modified Records in View", description: "Emit new event for each new or modified record in a view", key: "airtable_oauth-new-or-modified-records-in-view", - version: "0.0.5", + version: "0.0.8", type: "source", + dedupe: "unique", props: { ...base.props, tableId: { @@ -51,16 +52,16 @@ export default { const params = { view: viewId, filterByFormula: `LAST_MODIFIED_TIME() > "${lastTimestamp}"`, - returnFieldsByFieldId: this.returnFieldsByFieldId, + returnFieldsByFieldId: this.returnFieldsByFieldId || false, }; - const data = await this.airtable.listRecords({ + const records = await this.airtable.listRecords({ baseId, tableId, params, }); - if (!data.records.length) { + if (!records.length) { console.log("No new or modified records."); return; } @@ -72,7 +73,7 @@ export default { }; let newRecords = 0, modifiedRecords = 0; - for (const record of data.records) { + for (const record of records) { if (!lastTimestamp || moment(record.createdTime) > moment(lastTimestamp)) { record.type = "new_record"; newRecords++; diff --git a/components/airtable_oauth/sources/new-or-modified-records/new-or-modified-records.mjs b/components/airtable_oauth/sources/new-or-modified-records/new-or-modified-records.mjs index c7481a6cfeda5..3a4aade0d4a5f 100644 --- a/components/airtable_oauth/sources/new-or-modified-records/new-or-modified-records.mjs +++ b/components/airtable_oauth/sources/new-or-modified-records/new-or-modified-records.mjs @@ -1,155 +1,38 @@ -import base from "../common/common.mjs"; -import sampleEmit from "./test-event.mjs"; -import moment from "moment"; +import common from "../common/common-webhook-record.mjs"; +import airtable from "../../airtable_oauth.app.mjs"; export default { - ...base, - name: "New or Modified Records", + ...common, + name: "New or Modified Records (Instant)", key: "airtable_oauth-new-or-modified-records", - description: "Emit new event for each new or modified record in a table", - version: "0.0.7", + description: "Emit new event for each new or modified record in a table or view", + version: "1.0.0", type: "source", - props: { - ...base.props, - tableId: { - propDefinition: [ - base.props.airtable, - "tableId", - ({ baseId }) => ({ - baseId, - }), - ], - description: "The table ID to watch for changes.", + dedupe: "unique", + methods: { + ...common.methods, + getChangeTypes() { + return [ + "add", + "update", + ]; }, - fieldIds: { + }, + props: { + ...common.props, + watchDataInFieldIds: { propDefinition: [ - base.props.airtable, + airtable, "sortFieldId", - ({ - baseId, tableId, - }) => ({ - baseId, - tableId, + (c) => ({ + baseId: c.baseId, + tableId: c.tableId, }), ], type: "string[]", - label: "Field IDs", - description: "Identifiers of spedific fields/columns to watch for updates", - withLabel: true, - }, - returnFieldsByFieldId: { - propDefinition: [ - base.props.airtable, - "returnFieldsByFieldId", - ], - }, - }, - methods: { - ...base.methods, - _getFieldValues() { - return this.db.get("fieldValues") || {}; + label: "Watch Data In Field Ids", + description: + "Only emit events for updates that modify values in cells in these fields. If omitted, all fields within the table/view/base are watched", }, - _setFieldValues(fieldValues) { - this.db.set("fieldValues", fieldValues); - }, - updateFieldValues(newFieldValues, record) { - const fieldKey = this.returnFieldsByFieldId - ? "value" - : "label"; - for (const fieldId of this.fieldIds) { - newFieldValues[record.id] = { - ...newFieldValues[record.id], - [fieldId.value]: record.fields[fieldId[fieldKey]] || null, - }; - } - return newFieldValues; - }, - isUpdated(fieldValues, fieldIds, record) { - const fieldKey = this.returnFieldsByFieldId - ? "value" - : "label"; - for (const fieldId of fieldIds) { - if (!record.fields[fieldId[fieldKey]]) { - record.fields[fieldId[fieldKey]] = null; - } - if (fieldValues[record.id] - && fieldValues[record.id][fieldId.value] !== undefined - && record.fields[fieldId[fieldKey]] !== fieldValues[record.id][fieldId.value] - ) { - return true; - } - } - return false; - }, - }, - async run(event) { - const { - baseId, - tableId, - viewId, - } = this; - - const lastTimestamp = this._getLastTimestamp(); - const fieldValues = this._getFieldValues(); - const isFirstRunWithFields = this.fieldIds && Object.keys(fieldValues).length === 0; - const params = { - returnFieldsByFieldId: this.returnFieldsByFieldId, - }; - if (!isFirstRunWithFields) { - params.filterByFormula = `LAST_MODIFIED_TIME() > "${lastTimestamp}"`; - } - - const data = await this.airtable.listRecords({ - baseId, - tableId, - params, - }); - - if (!data.records.length) { - console.log("No new or modified records."); - return; - } - - const metadata = { - baseId, - tableId, - viewId, - }; - - let newRecords = 0, modifiedRecords = 0; - let newFieldValues = { - ...fieldValues, - }; - for (const record of data.records) { - if (this.fieldIds) { - newFieldValues = this.updateFieldValues(newFieldValues, record); - } - if (!lastTimestamp || moment(record.createdTime) > moment(lastTimestamp)) { - record.type = "new_record"; - newRecords++; - } else { - if (this.fieldIds - && (!this.isUpdated(fieldValues, this.fieldIds, record) || isFirstRunWithFields) - ) { - continue; - } - record.type = "record_modified"; - modifiedRecords++; - } - - record.metadata = metadata; - - this.$emit(record, { - summary: `${record.type}: ${JSON.stringify(record.fields)}`, - id: record.id, - ts: Date.now(), - }); - } - this._setFieldValues(newFieldValues); - console.log(`Emitted ${newRecords} new records(s) and ${modifiedRecords} modified record(s).`); - - // We keep track of the timestamp of the current invocation - this.updateLastTimestamp(event); }, - sampleEmit, }; diff --git a/components/airtable_oauth/sources/new-or-modified-records/test-event.mjs b/components/airtable_oauth/sources/new-or-modified-records/test-event.mjs deleted file mode 100644 index d33e5d1a470a7..0000000000000 --- a/components/airtable_oauth/sources/new-or-modified-records/test-event.mjs +++ /dev/null @@ -1,27 +0,0 @@ -export default { - "id": "recslHzC7AUjXDw", - "createdTime": "2023-07-31T04:29:26.000Z", - "fields": { - "Status": "In progress", - "ID": 27, - "Concat Calculation": "In progress-Medium", - "Record ID": "recslHzC7AUjXDw", - "Last Modified By": { - "id": "usrBLNP11LGUDJNTJ", - "email": "test@test.com", - "name": "Test User" - }, - "Created By": { - "id": "usrBLNP11LGUDJNTJ", - "email": "test@test.com", - "name": "Test User" - }, - "Name": "Pipedream 1129 Kanban", - "Priority": "Medium" - }, - "type": "record_modified", - "metadata": { - "baseId": "appeSZQLVSAD2cz", - "tableId": "tblPay0M5s9" - } - } \ No newline at end of file diff --git a/components/airtable_oauth/sources/new-records-in-view/new-records-in-view.mjs b/components/airtable_oauth/sources/new-records-in-view/new-records-in-view.mjs index 052de713e34f7..2e5cc68d869bc 100644 --- a/components/airtable_oauth/sources/new-records-in-view/new-records-in-view.mjs +++ b/components/airtable_oauth/sources/new-records-in-view/new-records-in-view.mjs @@ -6,8 +6,9 @@ export default { name: "New Records in View", description: "Emit new event for each new record in a view", key: "airtable_oauth-new-records-in-view", - version: "0.0.5", + version: "0.0.8", type: "source", + dedupe: "unique", props: { ...base.props, tableId: { @@ -51,16 +52,16 @@ export default { const params = { view: viewId, filterByFormula: `CREATED_TIME() > "${lastTimestamp}"`, - returnFieldsByFieldId: this.returnFieldsByFieldId, + returnFieldsByFieldId: this.returnFieldsByFieldId || false, }; - const data = await this.airtable.listRecords({ + const records = await this.airtable.listRecords({ baseId, tableId, params, }); - if (!data.records.length) { + if (!records.length) { console.log("No new records."); return; } @@ -73,12 +74,12 @@ export default { let maxTimestamp; let recordCount = 0; - for (const record of data.records) { + for (const record of records) { record.metadata = metadata; this.$emit(record, { ts: moment(record.createdTime).valueOf(), - summary: JSON.stringify(record.fields), + summary: `New record: ${record.id}`, id: record.id, }); if (!maxTimestamp || moment(record.createdTime).valueOf() > moment(maxTimestamp).valueOf()) { diff --git a/components/airtable_oauth/sources/new-records/new-records.mjs b/components/airtable_oauth/sources/new-records/new-records.mjs index 72f14c32fdd30..3cf18cc89e3c9 100644 --- a/components/airtable_oauth/sources/new-records/new-records.mjs +++ b/components/airtable_oauth/sources/new-records/new-records.mjs @@ -1,78 +1,19 @@ -import base from "../common/common.mjs"; -import moment from "moment"; +import common from "../common/common-webhook-record.mjs"; export default { - ...base, - name: "New Records", + ...common, + name: "New Record(s) Created (Instant)", description: "Emit new event for each new record in a table", key: "airtable_oauth-new-records", - version: "0.0.5", + version: "1.0.0", type: "source", - props: { - ...base.props, - tableId: { - propDefinition: [ - base.props.airtable, - "tableId", - ({ baseId }) => ({ - baseId, - }), - ], - description: "The table ID to watch for changes.", + dedupe: "unique", + methods: { + ...common.methods, + getChangeTypes() { + return [ + "add", + ]; }, - returnFieldsByFieldId: { - propDefinition: [ - base.props.airtable, - "returnFieldsByFieldId", - ], - }, - }, - async run() { - const { - baseId, - tableId, - viewId, - } = this; - - const lastTimestamp = this._getLastTimestamp(); - const params = { - filterByFormula: `CREATED_TIME() > "${lastTimestamp}"`, - returnFieldsByFieldId: this.returnFieldsByFieldId, - }; - - const data = await this.airtable.listRecords({ - baseId, - tableId, - params, - }); - - if (!data.records.length) { - console.log("No new records."); - return; - } - - const metadata = { - baseId, - tableId, - viewId, - }; - - let maxTimestamp; - let recordCount = 0; - for (const record of data.records) { - record.metadata = metadata; - - this.$emit(record, { - ts: moment(record.createdTime).valueOf(), - summary: JSON.stringify(record.fields), - id: record.id, - }); - if (!maxTimestamp || moment(record.createdTime).valueOf() > moment(maxTimestamp).valueOf()) { - maxTimestamp = record.createdTime; - } - recordCount++; - } - console.log(`Emitted ${recordCount} new records(s).`); - this._setLastTimestamp(maxTimestamp); }, }; diff --git a/components/aitable_ai/actions/create-datasheet/create-datasheet.mjs b/components/aitable_ai/actions/create-datasheet/create-datasheet.mjs new file mode 100644 index 0000000000000..f89e7136903d1 --- /dev/null +++ b/components/aitable_ai/actions/create-datasheet/create-datasheet.mjs @@ -0,0 +1,50 @@ +import app from "../../aitable_ai.app.mjs"; + +export default { + key: "aitable_ai-create-datasheet", + name: "Create Datasheet", + description: "Create a datasheet in the specified space. [See the documentation](https://developers.aitable.ai/api/reference#tag/Datasheet/operation/create-datasheets)", + version: "0.0.1", + type: "action", + props: { + app, + spaceId: { + propDefinition: [ + app, + "spaceId", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + description: { + propDefinition: [ + app, + "description", + ], + }, + folderId: { + propDefinition: [ + app, + "folderId", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.createDatasheet({ + $, + spaceId: this.spaceId, + data: { + name: this.name, + description: this.description, + folderId: this.folderId, + }, + }); + $.export("$summary", `Successfully created Datasheet with ID '${response.data.id}'`); + return response; + }, +}; diff --git a/components/aitable_ai/actions/create-field/create-field.mjs b/components/aitable_ai/actions/create-field/create-field.mjs new file mode 100644 index 0000000000000..eadc1e7e58b40 --- /dev/null +++ b/components/aitable_ai/actions/create-field/create-field.mjs @@ -0,0 +1,44 @@ +import app from "../../aitable_ai.app.mjs"; + +export default { + key: "aitable_ai-create-field", + name: "Create Field", + description: "Create a new field in the specified datasheet. [See the documentation](https://developers.aitable.ai/api/reference#tag/Field/operation/create-fields)", + version: "0.0.1", + type: "action", + props: { + app, + spaceId: { + propDefinition: [ + app, + "spaceId", + ], + }, + type: { + propDefinition: [ + app, + "type", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + description: "Name of the Field", + }, + }, + + async run({ $ }) { + const response = await this.app.createField({ + $, + spaceId: this.spaceId, + data: { + type: this.type, + name: this.name, + }, + }); + $.export("$summary", `Successfully sent request to create field. Result: '${response.message}'`); + return response; + }, +}; diff --git a/components/aitable_ai/actions/delete-field/delete-field.mjs b/components/aitable_ai/actions/delete-field/delete-field.mjs new file mode 100644 index 0000000000000..04153ea8d7912 --- /dev/null +++ b/components/aitable_ai/actions/delete-field/delete-field.mjs @@ -0,0 +1,34 @@ +import app from "../../aitable_ai.app.mjs"; + +export default { + key: "aitable_ai-delete-field", + name: "Delete Field", + description: "Delete a field in the specified datasheet. [See the documentation](https://developers.aitable.ai/api/reference/#tag/Field/operation/delete-fields)", + version: "0.0.1", + type: "action", + props: { + app, + spaceId: { + propDefinition: [ + app, + "spaceId", + ], + }, + fieldId: { + propDefinition: [ + app, + "fieldId", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.deleteField({ + $, + spaceId: this.spaceId, + fieldId: this.fieldId, + }); + $.export("$summary", `Successfully deleted the field with ID '${this.fieldId}'`); + return response; + }, +}; diff --git a/components/aitable_ai/aitable_ai.app.mjs b/components/aitable_ai/aitable_ai.app.mjs new file mode 100644 index 0000000000000..598c292151ff3 --- /dev/null +++ b/components/aitable_ai/aitable_ai.app.mjs @@ -0,0 +1,121 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "aitable_ai", + propDefinitions: { + spaceId: { + type: "string", + label: "Space ID", + description: "ID of the Space", + async options() { + const response = await this.getSpaces({}); + const spaceIds = response.data.spaces; + return spaceIds.map(({ + id, name, + }) => ({ + value: id, + label: name, + })); + }, + }, + fieldId: { + type: "string", + label: "Field ID", + description: "ID of the Field", + async options() { + const response = await this.getFields({}); + const fieldIds = response.data.fields; + return fieldIds.map(({ + id, name, + }) => ({ + value: id, + label: name, + })); + }, + }, + name: { + type: "string", + label: "Name", + description: "Name of the Datasheet", + }, + description: { + type: "string", + label: "Description", + description: "Description of the Datasheet", + }, + folderId: { + type: "string", + label: "Folder ID", + description: "The Folder ID is located in the `URL` when the folder is selected on the `Workbench page`, i.e.: if the URL is `https://aitable.ai/workbench/123456`, the `Folder ID` is 123456", + optional: true, + }, + type: { + type: "string", + label: "Type", + description: "Type of the Field", + options: constants.FIELD_TYPES, + }, + }, + methods: { + _baseUrl() { + return "https://aitable.ai/fusion/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_token}`, + }, + }); + }, + async createDatasheet({ + spaceId, ...args + }) { + return this._makeRequest({ + path: `/spaces/${spaceId}/datasheets`, + method: "post", + ...args, + }); + }, + async createField({ + spaceId, ...args + }) { + return this._makeRequest({ + path: `/spaces/${spaceId}/datasheets/${this.$auth.datasheet_id}/fields`, + method: "post", + ...args, + }); + }, + async deleteField({ + spaceId, fieldId, ...args + }) { + return this._makeRequest({ + path: `/spaces/${spaceId}/datasheets/${this.$auth.datasheet_id}/fields/${fieldId}`, + method: "delete", + ...args, + }); + }, + async getSpaces(args = {}) { + return this._makeRequest({ + path: "/spaces", + ...args, + }); + }, + async getFields(args = {}) { + return this._makeRequest({ + path: `/datasheets/${this.$auth.datasheet_id}/fields`, + ...args, + }); + }, + }, +}; diff --git a/components/aitable_ai/common/constants.mjs b/components/aitable_ai/common/constants.mjs new file mode 100644 index 0000000000000..c4e7a5d13877e --- /dev/null +++ b/components/aitable_ai/common/constants.mjs @@ -0,0 +1,11 @@ +export default { + FIELD_TYPES: [ + "Text", + "URL", + "Phone", + "Email", + "WorkDoc", + "AutoNumber", + "CreatedBy", + ], +}; diff --git a/components/aitable_ai/package.json b/components/aitable_ai/package.json new file mode 100644 index 0000000000000..78fde63930a62 --- /dev/null +++ b/components/aitable_ai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/aitable_ai", + "version": "0.1.0", + "description": "Pipedream AITable.ai Components", + "main": "aitable_ai.app.mjs", + "keywords": [ + "pipedream", + "aitable_ai" + ], + "homepage": "https://pipedream.com/apps/aitable_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/aivoov/README.md b/components/aivoov/README.md new file mode 100644 index 0000000000000..286f219b15585 --- /dev/null +++ b/components/aivoov/README.md @@ -0,0 +1,11 @@ +# Overview + +AiVOOV API offers speech recognition capabilities that enable you to convert audio into text, supporting various languages. This can be particularly useful for transcription services, content generation, and multilingual support systems. On Pipedream, you can leverage this API to create powerful serverless workflows that trigger upon receiving audio data, automatically transcribing it and connecting the output to countless other services for processing, analysis, or storage. + +# Example Use Cases + +- **Automatic Podcast Transcription and Posting**: Upon uploading a new podcast episode to a cloud storage service like Google Drive or Dropbox, Pipedream can trigger a workflow that sends the audio file to AiVOOV for transcription. The resulting text can then be formatted and posted to a CMS like WordPress, creating an SEO-friendly version of the content. + +- **Voicemail to Email Conversion**: Voicemails left on a virtual phone system like Twilio can be automatically routed to AiVOOV for transcription. Once transcribed, the text can be embedded into an email and sent to the intended recipient via an email service like SendGrid or Gmail, thus providing a readable version of the message. + +- **Customer Support Call Analysis**: Recordings from customer support calls, possibly stored in a service like Amazon S3, can be transcribed using AiVOOV. The transcription can then be analyzed for keywords or sentiment using a text analysis tool like MonkeyLearn. Insights gained can help improve customer service and track common issues. diff --git a/components/akeneo/README.md b/components/akeneo/README.md index e07cc883398cb..a94f23f467d71 100644 --- a/components/akeneo/README.md +++ b/components/akeneo/README.md @@ -1,10 +1,11 @@ # Overview -With the Akeneo API, you can build a variety of products and solutions that -help companies manage their product data. Here are some examples: - -- A product data management system -- A product data sync tool -- A product data visualization tool -- A product data cleansing tool -- A product data enrichment tool +The Akeneo API empowers users to streamline product information management by automating data exchange between Akeneo and other systems. By leveraging Pipedream, you can construct workflows that trigger actions based on events in Akeneo, sync product data across platforms, enhance product information with data from external sources, and keep sales channels up-to-date with the latest catalog details. + +# Example Use Cases + +- **Product Data Sync Across E-commerce Platforms**: Automate the synchronization of product information from Akeneo to various e-commerce platforms like Shopify or WooCommerce. When a product is updated in Akeneo, the workflow triggers an update across the linked e-commerce sites, ensuring consistent product details, pricing, and stock levels. + +- **Multi-channel Marketing Automation**: When launching a new product in Akeneo, automatically generate and distribute marketing materials across different channels. The workflow could create social media posts, email campaigns, and promotional banners on platforms like Twitter, Mailchimp, or Google Ads, leveraging the rich product data within Akeneo to craft engaging content. + +- **Supply Chain Update Notifications**: Set up a workflow that monitors stock levels and supplier data in Akeneo and sends automatic notifications to relevant stakeholders via Slack, email, or SMS using Twilio when supplies fall below a certain threshold or when a supplier updates delivery times, helping to maintain seamless inventory management. diff --git a/components/akismet/README.md b/components/akismet/README.md new file mode 100644 index 0000000000000..18ba54283d40e --- /dev/null +++ b/components/akismet/README.md @@ -0,0 +1,11 @@ +# Overview + +The Akismet API is a powerful ally in the fight against spam. This API allows you to check comments and contact form submissions against Akismet's database of known spam. It can also provide valuable insights into the nature of the content being analyzed, helping you determine whether it's a legitimate message or an unwanted interruption. By integrating Akismet with Pipedream, you can streamline the spam filtering process across various platforms, ensure the integrity of user-generated content, and maintain a cleaner digital environment. + +# Example Use Cases + +- **Blog Comment Moderation Workflow**: Automatically filter comments posted on your blog by connecting Akismet to a CMS like WordPress on Pipedream. When a new comment is posted, the workflow triggers, sends the content to Akismet for analysis, and depending on the result, it either approves the comment, marks it as spam, or even notifies the moderator via email for borderline cases. + +- **Customer Feedback Protection**: Ensure your customer feedback forms stay spam-free. This workflow integrates Akismet with form services like Typeform or Google Forms. When a new submission is received, it's passed to Akismet for vetting. Clean responses are stored in a Google Sheet, while suspicious ones are logged for review, keeping your feedback process pristine. + +- **User Registration Shield**: Protect your user registration process from fraudulent sign-ups. When a new user registers, pass their details through Akismet using Pipedream. If Akismet flags the registration as spam, the workflow can block the account creation or flag it for manual review, helping you maintain the integrity of your user base. diff --git a/components/akismet/akismet.app.mjs b/components/akismet/akismet.app.mjs new file mode 100644 index 0000000000000..ef3e3f71f6f20 --- /dev/null +++ b/components/akismet/akismet.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "akismet", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/akismet/package.json b/components/akismet/package.json new file mode 100644 index 0000000000000..41876d2725dd0 --- /dev/null +++ b/components/akismet/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/akismet", + "version": "0.0.1", + "description": "Pipedream Akismet Components", + "main": "akismet.app.mjs", + "keywords": [ + "pipedream", + "akismet" + ], + "homepage": "https://pipedream.com/apps/akismet", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/akkio/actions/make-prediction/make-prediction.mjs b/components/akkio/actions/make-prediction/make-prediction.mjs new file mode 100644 index 0000000000000..74553d6457aa2 --- /dev/null +++ b/components/akkio/actions/make-prediction/make-prediction.mjs @@ -0,0 +1,46 @@ +import akkio from "../../akkio.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "akkio-make-prediction", + name: "Make Prediction", + description: "Makes a prediction based on the input props. [See the documentation](https://docs.akkio.com/akkio-docs/rest-api/api-options/curl-commands)", + version: "0.0.1", + type: "action", + props: { + akkio, + data: { + type: "string[]", + label: "Data", + description: "Data in the format of: [{'field name 1': 'value 1', 'field name 2': 0}, {...}, ...]", + }, + modelId: { + type: "string", + label: "Model ID", + description: "The ID of the model to make the prediction with", + async options() { + const { models } = await this.akkio.getAllModels(); + + return models.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + + }, + async run({ $ }) { + const response = await this.akkio.makePrediction({ + $, + data: { + data: parseObject(this.data), + id: this.modelId, + }, + }); + + $.export("$summary", `Successfully made prediction with model ID ${this.modelId}`); + return response; + }, +}; diff --git a/components/akkio/akkio.app.mjs b/components/akkio/akkio.app.mjs new file mode 100644 index 0000000000000..5fea11bc1ce3d --- /dev/null +++ b/components/akkio/akkio.app.mjs @@ -0,0 +1,39 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "akkio", + methods: { + _baseUrl() { + return "https://api.akkio.com/v1"; + }, + _headers() { + return { + api_key: `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + getAllModels(opts = {}) { + return this._makeRequest({ + ...opts, + method: "GET", + path: "/models", + }); + }, + makePrediction(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/models", + ...opts, + }); + }, + }, +}; diff --git a/components/akkio/common/utils.mjs b/components/akkio/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/akkio/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/akkio/package.json b/components/akkio/package.json new file mode 100644 index 0000000000000..5471328795747 --- /dev/null +++ b/components/akkio/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/akkio", + "version": "0.1.0", + "description": "Pipedream Akkio Components", + "main": "akkio.app.mjs", + "keywords": [ + "pipedream", + "akkio" + ], + "homepage": "https://pipedream.com/apps/akkio", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} + diff --git a/components/albus/actions/ask-question/ask-question.mjs b/components/albus/actions/ask-question/ask-question.mjs new file mode 100644 index 0000000000000..ae0d8e166a7ae --- /dev/null +++ b/components/albus/actions/ask-question/ask-question.mjs @@ -0,0 +1,54 @@ +import albus from "../../albus.app.mjs"; + +export default { + key: "albus-ask-question", + name: "Ask Question", + description: "Ask a question to Albus and receive a response.", + version: "0.0.1", + type: "action", + props: { + albus, + prompt: { + propDefinition: [ + albus, + "prompt", + ], + }, + waitForCompletion: { + propDefinition: [ + albus, + "waitForCompletion", + ], + }, + }, + async run({ $ }) { + let response = await this.albus.chatCompletion({ + $, + data: { + prompt: this.prompt, + }, + }); + let summary = "Successfully submitted question."; + + if (this.waitForCompletion) { + const timer = (ms) => new Promise((res) => setTimeout(res, ms)); + let canPoll = true; + const url = response.pollEndpoint; + while (canPoll) { + response = await this.albus.makeRequest({ + $, + url, + }); + if (!response.ok) { + throw new Error(`Error retrieving response - ${JSON.stringify(response)}`); + } + canPoll = response.data.canPoll; + await timer(3000); + } + summary = "Successfully submitted question and received response"; + } + + $.export("$summary", summary); + return response; + }, +}; diff --git a/components/albus/albus.app.mjs b/components/albus/albus.app.mjs new file mode 100644 index 0000000000000..8334d4ba0fb79 --- /dev/null +++ b/components/albus/albus.app.mjs @@ -0,0 +1,45 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "albus", + propDefinitions: { + prompt: { + type: "string", + label: "Prompt", + description: "The question to ask", + }, + waitForCompletion: { + type: "boolean", + label: "Wait for Completion", + description: "Set to `true` to poll the API in 3-second intervals until the answer is ready", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api-albus.springworks.in"; + }, + makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `${this.$auth.api_key}`, + }, + ...otherOpts, + }); + }, + chatCompletion(opts = {}) { + return this.makeRequest({ + method: "POST", + path: "/chat/completions/custom", + ...opts, + }); + }, + }, +}; diff --git a/components/albus/package.json b/components/albus/package.json new file mode 100644 index 0000000000000..ba2007f099d96 --- /dev/null +++ b/components/albus/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/albus", + "version": "0.1.0", + "description": "Pipedream Albus Components", + "main": "albus.app.mjs", + "keywords": [ + "pipedream", + "albus" + ], + "homepage": "https://pipedream.com/apps/albus", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} diff --git a/components/alchemer/README.md b/components/alchemer/README.md index c1ca483d7fe15..0c7f7abca5f44 100644 --- a/components/alchemer/README.md +++ b/components/alchemer/README.md @@ -1,14 +1,11 @@ # Overview -With the Alchemer API, you can build a wide variety of applications and -integrations. Below are just a few examples of what you can do with the -Alchemer API: - -- Build a custom report or dashboard to display your survey data in the way - that you want -- Create a survey-taking application that allows your users to take your - surveys on the go -- Embed your surveys into your website or blog to collect data from your - visitors -- Create an integration with your CRM or marketing automation platform to push - survey data into those systems +The Alchemer API facilitates the creation and management of surveys, collection and analysis of responses, and automation of survey-related workflows. With Pipedream, you can harness this API to trigger actions in other applications based on survey events, sync survey data with databases or CRM systems, and even automate follow-up communications based on participant responses. Think of it as a bridge between Alchemer's rich survey feature set and a universe of other apps to streamline your data collection and analysis processes. + +# Example Use Cases + +- **Sync Survey Responses with Google Sheets**: After a survey completion, you can automatically push each new response to a Google Sheet. This allows for real-time data analysis and sharing with team members who prefer working within the Google ecosystem. + +- **Trigger Email Campaigns Based on Survey Results**: Integrate Alchemer with an email marketing service like Mailchimp. Use survey response data to segment your audience and trigger targeted email campaigns, sending personalized content to respondents based on their answers. + +- **Automate Customer Support Tickets from Survey Feedback**: Connect Alchemer to a customer support platform like Zendesk. Automatically create support tickets when a survey response indicates a customer issue, ensuring that no feedback is missed and all issues are addressed promptly. diff --git a/components/alchemer/package.json b/components/alchemer/package.json new file mode 100644 index 0000000000000..c12e89771dbcf --- /dev/null +++ b/components/alchemer/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/alchemer", + "version": "0.6.0", + "description": "Pipedream alchemer Components", + "main": "alchemer.app.mjs", + "keywords": [ + "pipedream", + "alchemer" + ], + "homepage": "https://pipedream.com/apps/alchemer", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/alchemy/.gitignore b/components/alchemy/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/alchemy/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/alchemy/README.md b/components/alchemy/README.md index 448ccaa77dcd4..006fa97fb36fd 100644 --- a/components/alchemy/README.md +++ b/components/alchemy/README.md @@ -1,11 +1,11 @@ # Overview -With the Alchemy API, you can build many different kinds of applications and -services that can analyze and extract data from texts. Here are some examples: - -- A text analysis tool that can analyze texts and extract information from them -- A service that can automatically summarize texts -- A tool that can extract named entities from texts -- A service that can generate sentiment scores for texts -- A service that can analyze texts for topic keywords -- A tool that can automatically tag texts with relevant keywords +The Alchemy API provides a robust platform for sending and managing faxes programmatically, giving you the power to integrate fax capabilities into automated workflows. With Pipedream, you can tap into these capabilities to build event-driven serverless workflows. Automate the sending of faxes when certain triggers occur, receive status updates, and connect with countless other services to streamline document management, notifications, and data synchronization tasks. + +# Example Use Cases + +- **Automated Invoice Delivery**: When a new invoice is generated in your accounting software, Pipedream can trigger an Alchemy API action to send the invoice as a fax to the customer. This ensures that billing communication is delivered promptly without manual intervention. + +- **Order Confirmation Receipts**: Integrate Alchemy with an e-commerce platform via Pipedream. Every time an order is placed, automatically fax the order confirmation to the supplier or manufacturer, speeding up the fulfillment process. + +- **Healthcare Appointment Reminders**: Connect Alchemy with a healthcare app to fax appointment reminders to patients. Use Pipedream to schedule and send personalized faxes a day before their appointment, improving attendance rates and patient engagement. diff --git a/components/alchemy/alchemy.app.mjs b/components/alchemy/alchemy.app.mjs new file mode 100644 index 0000000000000..584340384182d --- /dev/null +++ b/components/alchemy/alchemy.app.mjs @@ -0,0 +1,58 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "alchemy", + propDefinitions: { + network: { + type: "string", + label: "Network", + description: "Network of the webhook", + options: constants.NETWORKS, + }, + query: { + type: "string", + label: "GraphQL Query", + description: "Create a custom GraphQL query or select `Full Block Receipts` to get all log events for every new block", + options: [ + { + label: "Full Block Receipts", + value: constants.FULL_BLOCK_RECEIPTS, + }, + ], + }, + }, + methods: { + _baseUrl() { + return "https://dashboard.alchemy.com/api"; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "X-Alchemy-Token": this.$auth.auth_token, + }, + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/create-webhook", + ...opts, + }); + }, + deleteWebhook(opts = {}) { + return this._makeRequest({ + method: "DELETE", + path: "/delete-webhook", + ...opts, + }); + }, + }, +}; diff --git a/components/alchemy/app/alchemy.app.ts b/components/alchemy/app/alchemy.app.ts deleted file mode 100644 index 5d8ef75487b4a..0000000000000 --- a/components/alchemy/app/alchemy.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "alchemy", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); diff --git a/components/alchemy/common/constants.mjs b/components/alchemy/common/constants.mjs new file mode 100644 index 0000000000000..9d64361282e57 --- /dev/null +++ b/components/alchemy/common/constants.mjs @@ -0,0 +1,82 @@ +const NETWORKS = [ + "ETH_MAINNET", + "ETH_SEPOLIA", + "ETH_HOLESKY", + "ARBMAINNET", + "ARBSEPOLIA", + "ARBNOVA_MAINNET", + "MATICMAINNET", + "MATICMUMBAI", + "OPTMAINNET", + "OPTGOERLI", + "BASE_MAINNET", + "BASE_SEPOLIA", + "ZKSYNC_MAINNET", + "ZKSYNC_SEPOLIA", + "LINEA_MAINNET", + "LINEA_SEPOLIA", + "GNOSIS_MAINNET", + "GNOSIS_CHIADO", + "FANTOM_MAINNET", + "FANTOM_TESTNET", + "METIS_MAINNET", + "BLAST_MAINNET", + "BLAST_SEPOLIA", + "SHAPE_SEPOLIA", + "ZETACHAIN_MAINNET", + "ZETACHAIN_TESTNET", + "WORLDCHAIN_MAINNET", + "WORLDCHAIN_SEPOLIA", + "BNB_MAINNET", + "BNB_TESTNET", + "AVAX_MAINNET", + "AVAX_FUJI", + "SONEIUM_MINATO", + "GEIST_POLTER", +]; + +const FULL_BLOCK_RECEIPTS = ` +{ + block { + hash, + number, + timestamp, + logs(filter: {addresses: [], topics: []}) { + data, + topics, + index, + account { + address + }, + transaction { + hash, + nonce, + index, + from { + address + }, + to { + address + }, + value, + gasPrice, + maxFeePerGas, + maxPriorityFeePerGas, + gas, + status, + gasUsed, + cumulativeGasUsed, + effectiveGasPrice, + createdContract { + address + } + } + } + } +} +`; + +export default { + NETWORKS, + FULL_BLOCK_RECEIPTS, +}; diff --git a/components/alchemy/package.json b/components/alchemy/package.json index d85cd23208478..747a6d1d2aca0 100644 --- a/components/alchemy/package.json +++ b/components/alchemy/package.json @@ -1,18 +1,18 @@ { "name": "@pipedream/alchemy", - "version": "0.0.3", + "version": "0.1.0", "description": "Pipedream Alchemy Components", - "main": "dist/app/alchemy.app.mjs", + "main": "alchemy.app.mjs", "keywords": [ "pipedream", "alchemy" ], - "files": [ - "dist" - ], "homepage": "https://pipedream.com/apps/alchemy", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/alchemy/sources/new-graphql-query-instant/new-graphql-query-instant.mjs b/components/alchemy/sources/new-graphql-query-instant/new-graphql-query-instant.mjs new file mode 100644 index 0000000000000..ae34fa5ee29ff --- /dev/null +++ b/components/alchemy/sources/new-graphql-query-instant/new-graphql-query-instant.mjs @@ -0,0 +1,77 @@ +import alchemy from "../../alchemy.app.mjs"; + +export default { + key: "alchemy-new-graphql-query-instant", + name: "New GraphQL Query (Instant)", + description: "Emit new event when a new GraphQL query is uploaded to Alchemy's Custom Webhook service. [See the documentation](https://docs.alchemy.com/reference/create-webhook)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + alchemy, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + network: { + propDefinition: [ + alchemy, + "network", + ], + }, + query: { + propDefinition: [ + alchemy, + "query", + ], + }, + }, + hooks: { + async activate() { + const { data: { id } } = await this.alchemy.createWebhook({ + data: { + network: this.network, + webhook_type: "GRAPHQL", + webhook_url: this.http.endpoint, + graphql_query: this.query, + }, + }); + this._setHookId(id); + }, + async deactivate() { + const hookId = this._getHookId(); + if (hookId) { + await this.alchemy.deleteWebhook({ + params: { + webhook_id: hookId, + }, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + generateMeta(body) { + return { + id: body.id, + summary: `New Event ID: ${body.id}`, + ts: Date.parse(body.createdAt), + }; + }, + }, + async run(event) { + this.http.respond({ + status: 200, + }); + + const { body } = event; + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, +}; diff --git a/components/alegra/README.md b/components/alegra/README.md index 2eac4de7e39c8..57e2ecc3ec0b4 100644 --- a/components/alegra/README.md +++ b/components/alegra/README.md @@ -1,10 +1,11 @@ # Overview -With the Alegra API, you can build a variety of applications and integrations. -Here are some examples: - -- A contact management system -- An invoicing system -- A project management system -- A CRM system -- An accounting system +Alegra API allows for the automation and integration of accounting and financial tasks within Alegra's software platform. Through the API, developers can create, read, update, and delete information related to invoices, clients, products, services, and more. Essentially, it's a way to streamline financial operations, ensure data accuracy, and free up time from manual data entry by connecting Alegra to other business tools and services. + +# Example Use Cases + +- **Automated Invoice Creation and Email Notification**: When a new sale is recorded in your e-commerce platform, Pipedream triggers a workflow that generates an invoice in Alegra and then sends an email notification to the customer with the invoice attached. This could be paired with Shopify or WooCommerce on Pipedream. + +- **Sync Contacts Between CRM and Alegra**: Upon adding a new contact in your CRM, like Salesforce, a Pipedream workflow is triggered to create the same contact in Alegra, ensuring your customer data is consistent across all business systems. This avoids duplicate entries and keeps your accounting records up to date. + +- **Expense Tracking and Reporting**: Connect Alegra to a receipt scanning app like Expensify using Pipedream. Each time an employee uploads a receipt, the workflow triggers an entry in Alegra's expense ledger, streamlining reimbursement processes and financial reporting. diff --git a/components/alegra/actions/create-contact/create-contact.mjs b/components/alegra/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..7dedc816f1e1c --- /dev/null +++ b/components/alegra/actions/create-contact/create-contact.mjs @@ -0,0 +1,172 @@ +import alegra from "../../alegra.app.mjs"; +import { + STATUS_OPTIONS, + TYPE_OPTIONS, +} from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "alegra-create-contact", + name: "Create Contact", + description: "Adds a new contact to Alegra. [See the documentation](https://developer.alegra.com/reference/post_contacts).", + version: "0.0.1", + type: "action", + props: { + alegra, + name: { + type: "string", + label: "Name", + description: "Name of the contact", + }, + identification: { + type: "string", + label: "Identification", + description: "Identification of the contact", + optional: true, + }, + address: { + type: "string", + label: "Address", + description: "Address of the contact", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "City of the contact", + optional: true, + }, + phonePrimary: { + type: "string", + label: "Primary Phone", + description: "Primary phone number of the contact", + optional: true, + }, + phoneSecondary: { + type: "string", + label: "Secondary Phone", + description: "Secondary phone number of the contact", + optional: true, + }, + mobile: { + type: "string", + label: "Mobile", + description: "Mobile phone number of the contact", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "Email of the contact", + optional: true, + }, + type: { + type: "string", + label: "Type", + description: "Type of the contact", + options: TYPE_OPTIONS, + optional: true, + }, + status: { + type: "string", + label: "Status", + description: "Status of the contact", + options: STATUS_OPTIONS, + optional: true, + }, + fax: { + type: "string", + label: "Fax", + description: "Fax number of the contact", + optional: true, + }, + debtToPay: { + propDefinition: [ + alegra, + "debtToPay", + ], + optional: true, + }, + accountReceivable: { + propDefinition: [ + alegra, + "debtToPay", + ], + label: "Account Receivable", + description: "Id of the account receivable associated with the contact", + optional: true, + }, + internalContacts: { + type: "string[]", + label: "Internal Contacts", + description: "A list of objects of internal contacts related to the contact. **Example: [ { name: \"John Doe\", email: \"john@email.com\"}]**. [See the documentation](https://developer.alegra.com/reference/post_contacts) for further information.", + optional: true, + }, + ignoreRepeated: { + type: "boolean", + label: "Ignore Repeated", + description: "Ignore repeated contacts", + optional: true, + }, + statementAttached: { + type: "boolean", + label: "Statement Attached", + description: "Indicates whether to include a statement for the contact", + optional: true, + }, + seller: { + propDefinition: [ + alegra, + "seller", + ], + optional: true, + }, + priceList: { + propDefinition: [ + alegra, + "priceList", + ], + optional: true, + }, + term: { + propDefinition: [ + alegra, + "term", + ], + optional: true, + }, + }, + async run({ $ }) { + const { + alegra, + city, + address, + debtToPay, + accountReceivable, + internalContacts, + statementAttached, + ...data + } = this; + + const response = await alegra.createContact({ + $, + data: { + ...data, + address: { + city, + address, + }, + accounting: { + debtToPay, + accountReceivable, + }, + internalContacts: parseObject(internalContacts), + statementAttached: statementAttached + ? "yes" + : "no", + }, + }); + $.export("$summary", `Created contact with ID ${response.id}`); + return response; + }, +}; diff --git a/components/alegra/actions/create-invoice/create-invoice.mjs b/components/alegra/actions/create-invoice/create-invoice.mjs new file mode 100644 index 0000000000000..909d4294b1f9b --- /dev/null +++ b/components/alegra/actions/create-invoice/create-invoice.mjs @@ -0,0 +1,193 @@ +import { ConfigurationError } from "@pipedream/platform"; +import alegra from "../../alegra.app.mjs"; +import { + INVOICE_STATUS_OPTIONS, PERIODICITY_OPTIONS, +} from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "alegra-create-invoice", + name: "Create Invoice", + description: "Creates a new invoice in Alegra. [See the documentation](https://developer.alegra.com/reference/post_invoices)", + version: "0.0.1", + type: "action", + props: { + alegra, + status: { + type: "string", + label: "Status", + description: "Status of the invoice. If this attribute is not sent and no associated payments are sent, the invoice is created in \"draft\". If payments are sent to the invoice, the invoice is created in \"open\".", + options: INVOICE_STATUS_OPTIONS, + optional: true, + }, + numberTemplateId: { + type: "string", + label: "Number Template ID", + description: "Number template ID for the invoice. You can use this to automatically numbering.", + optional: true, + }, + numberTemplatePrefix: { + type: "string", + label: "Number Template Prefix", + description: "Number template prefix for the invoice. Send in case the numbering is manual. (Optional)", + optional: true, + }, + numberTemplateNumber: { + type: "string", + label: "Number Template Number", + description: "Number template number for the invoice. Send in case the numbering is manual. (Required)", + optional: true, + }, + items: { + type: "string[]", + label: "Items", + description: "Array of item objects (products/services) associated with the invoice. **Example: [{\"id\": \"123\", \"name\": \"Name\", \"price\": \"12.00\", \"quantity\": \"2\"}]**. [See the documentation](https://developer.alegra.com/reference/post_invoices) for further information.", + }, + payments: { + type: "string[]", + label: "Payments", + description: "Array of objects indicating the payments made to the invoice. **Example: [{\"date\": \"YYYY-MM-DD\", \"account\": \"123123\", \"amount\": \"10,00\"}]**. [See the documentation](https://developer.alegra.com/reference/post_invoices) for further information.", + optional: true, + }, + estimate: { + type: "string", + label: "Estimate", + description: "Specifies the identifier of the quote you want to associate with the sales invoice, in this way, the quote is invoiced and the items specified in the items parameter are associated, not those in the quote.", + optional: true, + }, + termsConditions: { + type: "string", + label: "Terms and Conditions", + description: "Terms and conditions of the invoice. Maximum allowed length: 500.", + optional: true, + }, + annotation: { + type: "string", + label: "Annotation", + description: "Invoice notes, visible in the PDF or printed document. Maximum allowed length: 500.", + optional: true, + }, + dueDate: { + type: "string", + label: "Due Date", + description: "Invoice due date. Format yyyy-MM-dd.", + }, + date: { + type: "string", + label: "Date", + description: "Invoice date. Format yyyy-MM-dd.", + }, + observations: { + type: "string", + label: "Observations", + description: "Invoice observations (not visible in the PDF or printed document). Maximum allowed length: 500.", + optional: true, + }, + client: { + propDefinition: [ + alegra, + "client", + ], + }, + seller: { + propDefinition: [ + alegra, + "seller", + ], + description: "Seller associated with the invoice.", + optional: true, + }, + pricelist: { + propDefinition: [ + alegra, + "priceList", + ], + description: "Price list associated with the invoice", + optional: true, + }, + currency: { + type: "object", + label: "Currency", + description: "Object that includes the information of the currency and exchange rate associated with the invoice. It should only be included if the company has the multi-currency functionality active and has configured the selected currency. It must include the currency code (three letters according to ISO) and the exchange rate.", + optional: true, + }, + retentions: { + type: "string[]", + label: "Retentions", + description: "Array of retention objects indicating the retentions of the sales invoice. **Example: [{\"id\": \"123123\", \"amount\": 10}]**. [See the documentation](https://developer.alegra.com/reference/post_invoices) for further information.", + optional: true, + }, + warehouse: { + propDefinition: [ + alegra, + "warehouse", + ], + optional: true, + }, + remissions: { + type: "string[]", + label: "Remissions", + description: "Array of identifiers of the remissions to be invoiced, you can associate one or more remissions by simply indicating the id of each one in an array. The client of the remissions and the sales invoice must be the same. Only open remissions can be invoiced. In this way, the items of each remission will be invoiced, and you can also specify other items with the items parameter. **Example: [{\"id\": 123, \"items\": [{\"id\": 123}], }]**. [See the documentation](https://developer.alegra.com/reference/post_invoices) for further information.", + optional: true, + }, + costCenter: { + propDefinition: [ + alegra, + "costCenter", + ], + optional: true, + }, + comments: { + type: "string[]", + label: "Comments", + description: "Array of strings with each of the comments to be associated. Comments can be updated even if the sales invoice cannot be edited.", + optional: true, + }, + periodicity: { + type: "string", + label: "Periodicity", + description: "Indicates the periodicity of the payments of the invoice installments. If you want to issue the invoice, the payment method is on credit this attribute becomes mandatory.", + options: PERIODICITY_OPTIONS, + optional: true, + }, + }, + async run({ $ }) { + try { + const invoice = await this.alegra.generateInvoice({ + $, + data: { + status: this.status, + numberTemplate: { + id: this.numberTemplateId, + prefix: this.numberTemplatePrefix, + number: this.numberTemplateNumber, + }, + items: parseObject(this.items), + payments: parseObject(this.payments), + estimate: this.estimate, + termsConditions: this.termsConditions, + annotation: this.annotation, + dueDate: this.dueDate, + date: this.date, + observations: this.observations, + client: { + id: this.client, + }, + seller: this.seller, + pricelist: this.pricelist, + currency: parseObject(this.currency), + retentions: parseObject(this.retentions), + warehouse: this.warehouse, + remissions: parseObject(this.remissions), + costCenter: this.costCenter, + comments: parseObject(this.comments), + periodicity: this.periodicity, + }, + }); + $.export("$summary", `Created invoice with ID ${invoice.id}`); + return invoice; + } catch (e) { + throw new ConfigurationError(e.response.data.message); + } + }, +}; diff --git a/components/alegra/actions/find-contact/find-contact.mjs b/components/alegra/actions/find-contact/find-contact.mjs new file mode 100644 index 0000000000000..ec0e2dee1bf75 --- /dev/null +++ b/components/alegra/actions/find-contact/find-contact.mjs @@ -0,0 +1,29 @@ +import alegra from "../../alegra.app.mjs"; + +export default { + key: "alegra-find-contact", + name: "Find Contact", + description: "Search for an existing contact in Alegra based on name or identification. [See the documentation](https://developer.alegra.com/reference/listcontacts-1)", + version: "0.0.1", + type: "action", + props: { + alegra, + query: { + propDefinition: [ + alegra, + "query", + ], + }, + }, + async run({ $ }) { + const response = await this.alegra.searchContact({ + $, + params: { + query: this.query, + }, + }); + + $.export("$summary", `Found ${response.length} contact(s) matching your query`); + return response; + }, +}; diff --git a/components/alegra/alegra.app.mjs b/components/alegra/alegra.app.mjs index 94de318ca6c35..77631f7fb376c 100644 --- a/components/alegra/alegra.app.mjs +++ b/components/alegra/alegra.app.mjs @@ -1,11 +1,245 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + export default { type: "app", app: "alegra", - propDefinitions: {}, + propDefinitions: { + seller: { + type: "string", + label: "Seller", + description: "Seller associated with the contact", + async options({ page }) { + const data = await this.getSellers({ + params: { + start: LIMIT * page, + limit: LIMIT, + status: "active", + }, + }); + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + priceList: { + type: "string", + label: "Price List", + description: "Price list associated with the contact", + async options({ page }) { + const data = await this.getPriceLists({ + params: { + start: LIMIT * page, + limit: LIMIT, + }, + }); + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + term: { + type: "string", + label: "Term", + description: "Payment terms associated with the contact", + async options({ page }) { + const data = await this.getTerms({ + params: { + start: LIMIT * page, + limit: LIMIT, + }, + }); + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + debtToPay: { + type: "integer", + label: "Debt to Pay", + description: "The Id of the debt to pay for the contact", + async options({ page }) { + const data = await this.getAccountingAccounts({ + params: { + start: LIMIT * page, + limit: LIMIT, + }, + }); + return data.map(({ + id, name: label, + }) => ({ + label, + value: parseInt(id), + })); + }, + }, + query: { + type: "string", + label: "Query", + description: "Search query for contacting (email, phone, or name)", + }, + client: { + type: "integer", + label: "Client", + description: "Client associated with the invoice", + async options({ page }) { + const data = await this.searchContact({ + params: { + start: LIMIT * page, + limit: LIMIT, + type: "client", + }, + }); + + return data.map(({ + id, name: label, + }) => ({ + label, + value: parseInt(id), + })); + }, + }, + warehouse: { + type: "string", + label: "Warehouse", + description: "Warehouse associated with the invoice", + async options({ page }) { + const data = await this.getWarehouses({ + params: { + start: LIMIT * page, + limit: LIMIT, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + costCenter: { + type: "string", + label: "Cost Center", + description: "Cost center associated with the invoice", + async options({ page }) { + const data = await this.getCostCenters({ + params: { + start: LIMIT * page, + limit: LIMIT, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.alegra.com/api/v1"; + }, + _auth() { + return { + username: this.$auth.user_email, + password: this.$auth.access_token, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + auth: this._auth(), + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + }, + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contacts", + ...opts, + }); + }, + generateInvoice(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/invoices", + ...opts, + }); + }, + searchContact(opts = {}) { + return this._makeRequest({ + path: "/contacts", + ...opts, + }); + }, + getSellers(opts = {}) { + return this._makeRequest({ + path: "/sellers", + ...opts, + }); + }, + getPriceLists(opts = {}) { + return this._makeRequest({ + path: "/price-lists", + ...opts, + }); + }, + getTerms(opts = {}) { + return this._makeRequest({ + path: "/terms", + ...opts, + }); + }, + getAccountingAccounts(opts = {}) { + return this._makeRequest({ + path: "/categories", + ...opts, + }); + }, + getWarehouses(opts = {}) { + return this._makeRequest({ + path: "/warehouses", + ...opts, + }); + }, + getCostCenters(opts = {}) { + return this._makeRequest({ + path: "/cost-centers", + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks/subscriptions", + ...opts, + }); + }, + deleteWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/subscriptions/${webhookId}`, + }); }, }, }; diff --git a/components/alegra/common/constants.mjs b/components/alegra/common/constants.mjs new file mode 100644 index 0000000000000..fd6248d1442d4 --- /dev/null +++ b/components/alegra/common/constants.mjs @@ -0,0 +1,25 @@ +export const LIMIT = 30; + +export const TYPE_OPTIONS = [ + "client", + "provider", +]; + +export const STATUS_OPTIONS = [ + "active", + "inactive", +]; + +export const INVOICE_STATUS_OPTIONS = [ + "draft", + "open", +]; + +export const PERIODICITY_OPTIONS = [ + "BIWEEKLY", + "MONTHLY", + "BIMONTHLY", + "QUARTERLY", + "SEMIANNUALLY", + "MANUAL", +]; diff --git a/components/alegra/common/utils.mjs b/components/alegra/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/alegra/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/alegra/package.json b/components/alegra/package.json new file mode 100644 index 0000000000000..b60563d76c3bf --- /dev/null +++ b/components/alegra/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/alegra", + "version": "0.1.0", + "description": "Pipedream Alegra Components", + "main": "alegra.app.mjs", + "keywords": [ + "pipedream", + "alegra" + ], + "homepage": "https://pipedream.com/apps/alegra", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/alegra/sources/common/base.mjs b/components/alegra/sources/common/base.mjs new file mode 100644 index 0000000000000..694f1d364c895 --- /dev/null +++ b/components/alegra/sources/common/base.mjs @@ -0,0 +1,48 @@ +import alegra from "../../alegra.app.mjs"; + +export default { + props: { + alegra, + http: "$.interface.http", + db: "$.service.db", + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getExtraData() { + return {}; + }, + }, + hooks: { + async activate() { + const { subscription } = await this.alegra.createWebhook({ + data: { + url: this.http.endpoint.split("https://")[1], + event: this.getEventType(), + }, + }); + this._setHookId(subscription.id); + }, + async deactivate() { + const webhookId = this._getHookId(); + await this.alegra.deleteWebhook(webhookId); + }, + }, + async run({ body }) { + if (!body.message) return; + + const ts = Date.parse(new Date()); + const message = body.message; + const model = message.client || message.item || message.invoice; + + this.$emit(body, { + id: model.id, + summary: this.getSummary(message), + ts: ts, + }); + }, +}; diff --git a/components/alegra/sources/new-client-instant/new-client-instant.mjs b/components/alegra/sources/new-client-instant/new-client-instant.mjs new file mode 100644 index 0000000000000..80430f82ad44a --- /dev/null +++ b/components/alegra/sources/new-client-instant/new-client-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "alegra-new-client-instant", + name: "New Client Created (Instant)", + description: "Emit new event when a new client is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return "new-client"; + }, + getSummary({ client }) { + return `New client created: ${client.email} - (${client.email})`; + }, + }, + sampleEmit, +}; diff --git a/components/alegra/sources/new-client-instant/test-event.mjs b/components/alegra/sources/new-client-instant/test-event.mjs new file mode 100644 index 0000000000000..00230042d7306 --- /dev/null +++ b/components/alegra/sources/new-client-instant/test-event.mjs @@ -0,0 +1,31 @@ +export default { + "subject": "new-client", + "message": { + "client": { + "id": "774", + "name": { + "firstName": "Primer Nombre", + "secondName": "Segundo Nombre", + "lastName": "Primer Apellido", + "secondLastName": "Segundo Apellido" + }, + "phonePrimary": "+432432234", + "phoneSecondary": "+534234523", + "mobile": "+443242323123", + "email": "uncorreo@correo.com", + "type": [ + "client", + "provider" + ], + "fax": "unFax", + "identification": "3211233", + "address": { + "zipCode": "050013", + "department": "Antioquia", + "country": "Colombia", + "address": "Una Dirección", + "city": "Abriaquí" + } + } + } +} diff --git a/components/alegra/sources/new-invoice-instant/new-invoice-instant.mjs b/components/alegra/sources/new-invoice-instant/new-invoice-instant.mjs new file mode 100644 index 0000000000000..0dd0fac77c14f --- /dev/null +++ b/components/alegra/sources/new-invoice-instant/new-invoice-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "alegra-new-invoice-instant", + name: "New Invoice Created (Instant)", + description: "Emit new event when a new invoice is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return "new-invoice"; + }, + getSummary({ invoice }) { + return `New invoice created: ${invoice.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/alegra/sources/new-invoice-instant/test-event.mjs b/components/alegra/sources/new-invoice-instant/test-event.mjs new file mode 100644 index 0000000000000..144db0d0b0f84 --- /dev/null +++ b/components/alegra/sources/new-invoice-instant/test-event.mjs @@ -0,0 +1,49 @@ +export default { + "subject": "new-invoice", + "message": { + "invoice": { + "id": "123", + "date": "2024-06-18", + "dueDate": "2024-06-18", + "observations": "Notas de facturas de capacitación", + "anotation": "Notas de facturas de capacitación", + "status": "open", + "client": { + "id": "123" + }, + "numberTemplate": { + "id": "123" + }, + "seller": { + "id": "123" + }, + "total": 1190, + "totalPaid": 0, + "balance": 1190, + "decimalPrecision": "0", + "estimate": { + "id": "123" + }, + "costCenter": { + "id": "123" + }, + "items": [ + { + "name": "cédula digital / Option 2", + "description": "Descripción del Item", + "reference": "123321", + "price": 1000, + "quantity": 1, + "tax": [ + { + "id": "123" + } + ], + "remission": { + "id": "123" + } + } + ] + } + } +} diff --git a/components/alegra/sources/new-item-instant/new-item-instant.mjs b/components/alegra/sources/new-item-instant/new-item-instant.mjs new file mode 100644 index 0000000000000..6e3fdc2d89f7a --- /dev/null +++ b/components/alegra/sources/new-item-instant/new-item-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "alegra-new-item-instant", + name: "New Item Added (Instant)", + description: "Emit new event each time a new item is added.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return "new-item"; + }, + getSummary({ item }) { + return `New item added: ${item.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/alegra/sources/new-item-instant/test-event.mjs b/components/alegra/sources/new-item-instant/test-event.mjs new file mode 100644 index 0000000000000..45fdcfacf010d --- /dev/null +++ b/components/alegra/sources/new-item-instant/test-event.mjs @@ -0,0 +1,55 @@ +export default { + "subject": "new-item", + "message": { + "item": { + "id": "865", + "name": "Un Ítem / S", + "description": "Descripción del Item", + "reference": "423424134213", + "itemCategory": { + "id": "19" + }, + "price": [ + { + "id": "123", + "price": 1 + }, + { + "id": "124", + "price": 2 + } + ], + "inventory": { + "unit": "unit", + "availableQuantity": 1, + "unitCost": 1, + "initialQuantity": 1, + "warehouses": [ + { + "id": "10" + } + ] + }, + "category": { + "id": "234" + }, + "tax": [], + "status": "active", + "customFields": [ + { + "id": "6" + }, + { + "id": "5" + }, + { + "id": "1" + } + ], + "type": "variant", + "variantAttributes": null, + "itemVariants": null, + "subitems": null + } + } +} diff --git a/components/alerty/actions/notify-devices/notify-devices.mjs b/components/alerty/actions/notify-devices/notify-devices.mjs new file mode 100644 index 0000000000000..7d1f1da5ef030 --- /dev/null +++ b/components/alerty/actions/notify-devices/notify-devices.mjs @@ -0,0 +1,73 @@ +import alerty from "../../alerty.app.mjs"; +import { URGENCY_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "alerty-notify-devices", + name: "Notify Devices", + description: "Sends a notification to active devices. [See the documentation](https://alerty.dev/api/notify)", + version: "0.0.1", + type: "action", + props: { + alerty, + message: { + type: "string", + label: "Message", + description: "The message to be included in the push notification.", + }, + title: { + type: "string", + label: "Title", + description: "Title for your notification message.", + optional: true, + }, + image: { + type: "string", + label: "Image URL", + description: "URL of an image to be displayed in the notification.", + optional: true, + }, + icon: { + type: "string", + label: "Icon URL", + description: "URL of an image to be used as an icon by the notification. Default: Alerty Logo", + optional: true, + }, + deviceId: { + type: "string[]", + label: "Device ID", + description: "Specific device IDs to send the notification to. If no Device Id is included, the push message will be sent to all active devices on your account.", + optional: true, + }, + urgency: { + type: "string", + label: "Urgency", + description: "Urgency of the notification. Default: very-low", + options: URGENCY_OPTIONS, + optional: true, + }, + actions: { + type: "string[]", + label: "Actions", + description: "Actions for the notification, each item should be a JSON string. Example: { \"action\": \"https://example.com\", \"title\": \"Open Site\", \"icon\": \"https://example.com/icon.png\" }", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.alerty.makeRequest({ + $, + data: { + message: this.message, + title: this.title, + image: this.image, + icon: this.icon, + device_id: parseObject(this.deviceId), + urgency: this.urgency, + actions: parseObject(this.actions), + }, + }); + + $.export("$summary", `Notification sent with message: "${this.message}"`); + return response; + }, +}; diff --git a/components/alerty/alerty.app.mjs b/components/alerty/alerty.app.mjs new file mode 100644 index 0000000000000..f478429ee343b --- /dev/null +++ b/components/alerty/alerty.app.mjs @@ -0,0 +1,26 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "alerty", + methods: { + _baseUrl() { + return `${this.$auth.notification_url}`; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.api_key}`, + }; + }, + makeRequest({ + $ = this, ...opts + }) { + return axios($, { + method: "POST", + url: this._baseUrl(), + headers: this._headers(), + ...opts, + }); + }, + }, +}; diff --git a/components/alerty/common/constants.mjs b/components/alerty/common/constants.mjs new file mode 100644 index 0000000000000..3d30edf5f831a --- /dev/null +++ b/components/alerty/common/constants.mjs @@ -0,0 +1,6 @@ +export const URGENCY_OPTIONS = [ + "very-low", + "low", + "normal", + "high", +]; diff --git a/components/alerty/common/utils.mjs b/components/alerty/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/alerty/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/alerty/package.json b/components/alerty/package.json new file mode 100644 index 0000000000000..eaadf692bfa3a --- /dev/null +++ b/components/alerty/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/alerty", + "version": "0.1.0", + "description": "Pipedream Alerty Components", + "main": "alerty.app.mjs", + "keywords": [ + "pipedream", + "alerty" + ], + "homepage": "https://pipedream.com/apps/alerty", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} + diff --git a/components/algodocs/algodocs.app.mjs b/components/algodocs/algodocs.app.mjs new file mode 100644 index 0000000000000..0911410d0f8ad --- /dev/null +++ b/components/algodocs/algodocs.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "algodocs", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/algodocs/package.json b/components/algodocs/package.json new file mode 100644 index 0000000000000..821c36f24870a --- /dev/null +++ b/components/algodocs/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/algodocs", + "version": "0.0.1", + "description": "Pipedream AlgoDocs Components", + "main": "algodocs.app.mjs", + "keywords": [ + "pipedream", + "algodocs" + ], + "homepage": "https://pipedream.com/apps/algodocs", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/algolia/README.md b/components/algolia/README.md index 6a1cc31cbab1b..166b848eb3dbc 100644 --- a/components/algolia/README.md +++ b/components/algolia/README.md @@ -1,9 +1,11 @@ # Overview -Algolia provides a powerful API that lets you build scalable, fast search -applications. Here are some examples of what you can build with Algolia: +Algolia is a powerful search-as-a-service platform that makes it easy to deliver fast, relevant search experiences to users. The Algolia API allows developers to programmatically manage search indices, perform searches, and capture insights about search activity. With Pipedream, you can harness this API to automate and integrate Algolia's capabilities with other apps, creating efficient workflows that enhance search functionalities and user engagement. You can sync data across platforms, automate content indexing, and trigger actions based on search patterns or user behaviors. -- A fast, relevant search engine for your website or application -- A autocomplete feature for your search bar -- A searchable database for your products or inventory -- A searchable customer directory for your CRM +# Example Use Cases + +- **Sync User Data from CRM to Algolia**: Automatically update your Algolia indices whenever a new user is added or updated in your CRM system, like Salesforce. This ensures that your search results always include the latest user information. + +- **Automate Content Indexing from a CMS**: Whenever new content is published in your Content Management System (CMS), like WordPress, trigger a workflow that adds or updates records in an Algolia index, keeping your search results fresh and relevant. + +- **Search Analytics to Slack**: Capture Algolia search analytics, such as popular search terms or zero-result searches, and send a summary report to a Slack channel. This can be used for real-time insights, helping your team to optimize content and search strategies. diff --git a/components/algolia/actions/browse-records/browse-records.mjs b/components/algolia/actions/browse-records/browse-records.mjs new file mode 100644 index 0000000000000..34354ad817032 --- /dev/null +++ b/components/algolia/actions/browse-records/browse-records.mjs @@ -0,0 +1,37 @@ +import app from "../../algolia.app.mjs"; + +export default { + key: "algolia-browse-records", + name: "Browse Records", + description: "Browse for records in the given index. [See the documentation](https://www.algolia.com/doc/libraries/javascript/v5/methods/search/browse/?client=javascript).", + version: "0.0.1", + type: "action", + props: { + app, + indexName: { + propDefinition: [ + app, + "indexName", + ], + }, + }, + methods: { + browse(args) { + return this.app._client().browse(args); + }, + }, + async run({ $ }) { + const { + browse, + indexName, + } = this; + + const response = await browse({ + indexName, + }); + + $.export("$summary", `Successfully fetched records from ${indexName} index.`); + + return response; + }, +}; diff --git a/components/algolia/actions/create-objects/create-objects.mjs b/components/algolia/actions/create-objects/create-objects.mjs deleted file mode 100644 index 64952c2c9b564..0000000000000 --- a/components/algolia/actions/create-objects/create-objects.mjs +++ /dev/null @@ -1,54 +0,0 @@ -import algolia from "../../algolia.app.mjs"; - -export default { - key: "algolia-create-objects", - name: "Create Objects", - description: "Adds an array of JavaScript objects to the given index. [See docs here](https://www.algolia.com/doc/api-reference/api-methods/save-objects/)", - version: "0.0.2", - type: "action", - props: { - algolia, - indexName: { - propDefinition: [ - algolia, - "indexName", - ], - }, - objects: { - label: "Objects", - description: "An array of JavaScript objects, each corresponding to a new object in your search index. It's recommended you create this array of objects in a prior code step, then select expression mode and enter a variable reference to that array here", - type: "string[]", - }, - autoGenerateObjectIDIfNotExist: { - type: "boolean", - label: "Auto Generate Object ID If Not Present", - description: "If an objectID property is not present on objects, automatically assign on. Defaults to true", - optional: true, - default: true, - }, - }, - methods: { - parseObject(object) { - return (typeof object === "object") - ? object - : JSON.parse(object); - }, - }, - async run({ $ }) { - const parsedObjects = this.objects.length - ? this.objects.map((object) => this.parseObject(object)) - : this.parseObject(this.objects); - - const response = await this.algolia.createObjects({ - indexName: this.indexName, - objects: parsedObjects, - options: { - autoGenerateObjectIDIfNotExist: this.autoGenerateObjectIDIfNotExist, - }, - }); - - $.export("$summary", "Successfully created objects"); - - return response; - }, -}; diff --git a/components/algolia/actions/delete-objects/delete-objects.mjs b/components/algolia/actions/delete-objects/delete-objects.mjs deleted file mode 100644 index bd5142f1c3c9f..0000000000000 --- a/components/algolia/actions/delete-objects/delete-objects.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import algolia from "../../algolia.app.mjs"; - -export default { - key: "algolia-delete-objects", - name: "Delete Objects", - description: "Delete objects from the given index. [See docs here](https://www.algolia.com/doc/api-reference/api-methods/delete-objects/)", - version: "0.0.1", - type: "action", - props: { - algolia, - indexName: { - propDefinition: [ - algolia, - "indexName", - ], - }, - objectIds: { - label: "Object Ids", - description: "An array of JavaScript object IDs, each corresponding to a existing object in your search index", - type: "string[]", - }, - }, - async run({ $ }) { - const response = await this.algolia.deleteObjects({ - indexName: this.indexName, - objectIds: typeof this.objectIds === "string" - ? JSON.parse(this.objectIds) - : this.objectIds, - }); - - $.export("$summary", "Successfully deleted objects"); - - return response; - }, -}; diff --git a/components/algolia/actions/delete-records/delete-records.mjs b/components/algolia/actions/delete-records/delete-records.mjs new file mode 100644 index 0000000000000..2e5868f085341 --- /dev/null +++ b/components/algolia/actions/delete-records/delete-records.mjs @@ -0,0 +1,46 @@ +import app from "../../algolia.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "algolia-delete-records", + name: "Delete Records", + description: "Delete records from the given index. [See the documentation](https://www.algolia.com/doc/libraries/javascript/v5/helpers/#delete-records)", + version: "0.0.1", + type: "action", + props: { + app, + indexName: { + propDefinition: [ + app, + "indexName", + ], + }, + recordIds: { + type: "string[]", + label: "Record IDs", + description: "IDs of the records to delete also known as `objectIDs`. Eg. `[\"1\", \"2\"]`. If you don't know the IDs, you can use the **Browse Records** action to find them, then map them and then use them here as a custom expression. Eg. `{{steps.map_records_to_objectids.$return_value}}`.", + }, + }, + methods: { + deleteRecords(args = {}) { + return this.app._client().deleteObjects(args); + }, + }, + async run({ $ }) { + const { + deleteRecords, + indexName, + recordIds, + } = this; + + const response = await deleteRecords({ + indexName, + objectIDs: utils.parseArray(recordIds), + waitForTasks: true, + }); + + $.export("$summary", "Successfully deleted records."); + + return response; + }, +}; diff --git a/components/algolia/actions/save-records/save-records.mjs b/components/algolia/actions/save-records/save-records.mjs new file mode 100644 index 0000000000000..00209544870e5 --- /dev/null +++ b/components/algolia/actions/save-records/save-records.mjs @@ -0,0 +1,45 @@ +import app from "../../algolia.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "algolia-save-records", + name: "Save Records", + description: "Adds records to an index. [See the documentation](https://www.algolia.com/doc/libraries/javascript/v5/helpers/#save-records).", + version: "0.0.1", + type: "action", + props: { + app, + indexName: { + propDefinition: [ + app, + "indexName", + ], + }, + records: { + type: "string[]", + label: "Records From JSON Objects", + description: "The records to add to the index. Each record should be a JSON object. Eg. `{\"objectID\": \"1\", \"name\": \"Jane Doe\"}`. For a better user experience, you can use the [**CSV File To Objects**](https://pipedream.com/apps/helper-functions/actions/csv-file-to-objects) **Helper Function** action to convert a CSV file to an array of objects and then map the objects to the records field here as a **Custom Expression**. Eg. `{{steps.csv_file_to_objects.$return_value}}`.", + }, + }, + methods: { + saveRecords(args = {}) { + return this.app._client().saveObjects(args); + }, + }, + async run({ $ }) { + const { + saveRecords, + indexName, + records, + } = this; + + const response = await saveRecords({ + indexName, + objects: utils.parseArrayAndMap(records), + waitForTasks: true, + }); + + $.export("$summary", "Successfully created records."); + return response; + }, +}; diff --git a/components/algolia/algolia.app.mjs b/components/algolia/algolia.app.mjs index d7dfef51197f2..d0d220f79cffc 100644 --- a/components/algolia/algolia.app.mjs +++ b/components/algolia/algolia.app.mjs @@ -1,4 +1,4 @@ -import algoliasearch from "algoliasearch"; +import { algoliasearch } from "algoliasearch"; export default { type: "app", @@ -9,8 +9,7 @@ export default { description: "The name of the index", type: "string", async options() { - const indexes = await this.getIndexes(); - + const { items: indexes } = await this.listIndices(); return indexes.map((index) => index.name); }, }, @@ -25,23 +24,8 @@ export default { _client() { return algoliasearch(this._applicationId(), this._apiKey()); }, - _index(indexName) { - return this._client().initIndex(indexName); - }, - async getIndexes() { - const response = await this._client().listIndices(); - - return response.items; - }, - async createObjects({ - indexName, objects, options, - }) { - return this._index(indexName).saveObjects(objects, options); - }, - async deleteObjects({ - indexName, objectIds, - }) { - return this._index(indexName).deleteObjects(objectIds); + listIndices() { + return this._client().listIndices(); }, }, }; diff --git a/components/algolia/common/utils.mjs b/components/algolia/common/utils.mjs new file mode 100644 index 0000000000000..7748d9d025164 --- /dev/null +++ b/components/algolia/common/utils.mjs @@ -0,0 +1,52 @@ +import { ConfigurationError } from "@pipedream/platform"; + +const parseJson = (input) => { + const parse = (value) => { + if (typeof(value) === "string") { + try { + return parseJson(JSON.parse(value)); + } catch (e) { + return value; + } + } else if (typeof(value) === "object" && value !== null) { + return Object.entries(value) + .reduce((acc, [ + key, + val, + ]) => Object.assign(acc, { + [key]: parse(val), + }), {}); + } + return value; + }; + + return parse(input); +}; + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid JSON array object"); + } +} + +export default { + parseArray, + parseArrayAndMap: (value) => parseArray(value)?.map(parseJson), +}; diff --git a/components/algolia/package.json b/components/algolia/package.json index 97a92b1020b2a..23c566106e5bc 100644 --- a/components/algolia/package.json +++ b/components/algolia/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/algolia", - "version": "0.0.4", + "version": "0.1.0", "description": "Pipedream Algolia Components", "main": "algolia.app.js", "keywords": [ @@ -14,6 +14,6 @@ "access": "public" }, "dependencies": { - "algoliasearch": "^4.13.1" + "algoliasearch": "^5.17.1" } } diff --git a/components/algomo/README.md b/components/algomo/README.md new file mode 100644 index 0000000000000..636a3dbc86e26 --- /dev/null +++ b/components/algomo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Algomo API empowers developers to integrate multilingual customer support automation into their services. It offers the ability to understand and respond to customer queries in various languages, making it a powerful tool for global customer engagement. Within Pipedream, you can harness this API to automate customer interactions, analyze sentiment and feedback, and streamline support workflows, among other possibilities, without managing infrastructure. + +# Example Use Cases + +- **Automated Multilingual Support Tickets**: Leverage the Algomo API in Pipedream to auto-generate support tickets from customer queries in different languages. By integrating with a CRM like Salesforce, you can capture, translate, and route inquiries to the appropriate support agents based on language or content. + +- **Customer Sentiment Analysis**: Create a workflow that utilizes Algomo's natural language processing to gauge customer sentiment from support messages or social media posts. Connect to a data visualization tool like Google Sheets or Tableau on Pipedream to track sentiment trends over time, helping inform product improvements or marketing strategies. + +- **Real-Time Support Chat Translation**: Build a real-time translation system for live chat applications by combining Algomo with a messaging platform like Slack or Intercom on Pipedream. This allows seamless communication between support staff and customers, breaking down language barriers and improving the customer experience. diff --git a/components/algorand_developer_portal/README.md b/components/algorand_developer_portal/README.md new file mode 100644 index 0000000000000..b61c150e3021f --- /dev/null +++ b/components/algorand_developer_portal/README.md @@ -0,0 +1,11 @@ +# Overview + +The Algorand Developer Portal API allows you to interact with the Algorand blockchain, performing operations like checking account balances, creating transactions, and reading the current state of the blockchain. When used within Pipedream, you can automate workflows that respond to real-time events on the blockchain, integrate with other APIs for cross-platform actions, or analyze blockchain data to inform business decisions. + +# Example Use Cases + +- **Automated Asset Tracking**: Create a workflow that triggers on a schedule to fetch the balance and asset details for specified Algorand accounts. Use this data to update a Google Sheet, sending an email alert if certain assets exceed a threshold value, integrating with the Google Sheets and Gmail apps on Pipedream. + +- **Transaction Alerting System**: Build a Pipedream workflow triggered by webhooks that listens for specific transaction events on the Algorand blockchain. When a transaction meets your criteria, send a Slack message to a designated channel using the Slack app on Pipedream, keeping your team immediately informed about high-priority transactions. + +- **Blockchain Analytics Dashboard**: Set up a workflow that periodically calls the Algorand Developer Portal API to retrieve recent block data. Process this data to extract insights and trends, then push the results to a service like Datadog or a custom dashboard, enabling you to visualize blockchain operations and performance metrics over time. \ No newline at end of file diff --git a/components/algorand_developer_portal/package.json b/components/algorand_developer_portal/package.json index b8393fd707268..bb13549d9107e 100644 --- a/components/algorand_developer_portal/package.json +++ b/components/algorand_developer_portal/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/algorithmia/README.md b/components/algorithmia/README.md index dbead378dab26..beebe8fc49778 100644 --- a/components/algorithmia/README.md +++ b/components/algorithmia/README.md @@ -1,12 +1,11 @@ # Overview -You can use the Algorithmia API to build programs that perform a variety of -tasks, including: - -- Machine learning algorithms -- Data analysis algorithms -- Natural language processing algorithms -- Computer vision algorithms -- Robotics algorithms -- Optimization algorithms -- And more! +The Algorithmia API provides access to a myriad of algorithms in various categories like machine learning, data analysis, and natural language processing. By utilizing this API on Pipedream, you can automate complex tasks that require advanced computations, such as sentiment analysis, image processing, or predictive modeling, without the need to develop and maintain the underlying algorithms yourself. Pipedream's serverless platform allows you to seamlessly integrate these capabilities with other services to create powerful data processing pipelines. + +# Example Use Cases + +- **Content Categorization Workflow**: Extract and categorize text data from various sources such as RSS feeds, customer feedback forms, or social media via the Algorithmia API. Use Pipedream to orchestrate this flow: listen for new data, pass it to Algorithmia for categorization, and then store the results in a Google Sheets document for easy review and analysis. + +- **Image Recognition and Alerting System**: Build a system that uses Algorithmia's image recognition algorithms to analyze images uploaded to an S3 bucket. With Pipedream, you can detect when a new image is uploaded, send it to Algorithmia for processing, and if certain criteria are met (like recognizing a specific object), trigger an alert via email or messaging apps like Slack to notify the relevant parties. + +- **Language Translation Bot**: Implement a language translation bot for real-time communication platforms like Slack. Employ Pipedream to receive messages that need translation, use Algorithmia's language translation algorithms to translate the text, and then post the translated message back into the conversation. This could facilitate seamless multi-lingual interactions within global teams or customer support channels. diff --git a/components/algorithmia/package.json b/components/algorithmia/package.json new file mode 100644 index 0000000000000..ded82b9c019e2 --- /dev/null +++ b/components/algorithmia/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/algorithmia", + "version": "0.6.0", + "description": "Pipedream algorithmia Components", + "main": "algorithmia.app.mjs", + "keywords": [ + "pipedream", + "algorithmia" + ], + "homepage": "https://pipedream.com/apps/algorithmia", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/all_images_ai/README.md b/components/all_images_ai/README.md new file mode 100644 index 0000000000000..415fcf2edc4b7 --- /dev/null +++ b/components/all_images_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +The All-Images.ai API serves as a tool for processing images through AI-driven methods. With this API, you can enhance images, deblur them, remove backgrounds, and much more. All-Images.ai becomes more powerful when integrated into Pipedream’s serverless platform, allowing you to automate image processing in workflows that connect with other apps. You can trigger workflows with HTTP requests, email, and over 800 Pipedream-supported apps. These automations can save time and enhance productivity, especially for tasks involving bulk image processing or real-time image manipulation. + +# Example Use Cases + +- **Automated Image Enhancement for Social Media Posts**: Use All-Images.ai to automatically enhance images uploaded to a cloud storage platform like Google Drive. Once an image is uploaded, Pipedream triggers a workflow that sends the image to All-Images.ai for processing. The enhanced image is then posted to various social media platforms using their respective APIs. + +- **Real-Time Deblurring for User-Submitted Images**: Implement a system where users submit images via a web form, and those images are automatically deblurred using All-Images.ai. In Pipedream, trigger a workflow on form submission, process the images, and then store the deblurred versions in a service like AWS S3, sending a notification to the user with a link to the processed image. + +- **Background Removal for E-commerce Product Listings**: Integrate All-Images.ai with e-commerce platforms like Shopify. When a new product is listed, automatically trigger a Pipedream workflow to remove the background from the product images using All-Images.ai. Once processed, update the product listing with the new images to create a clean and professional appearance. diff --git a/components/all_images_ai/actions/buy-image/buy-image.mjs b/components/all_images_ai/actions/buy-image/buy-image.mjs new file mode 100644 index 0000000000000..ac996c6ca3bd8 --- /dev/null +++ b/components/all_images_ai/actions/buy-image/buy-image.mjs @@ -0,0 +1,29 @@ +import allImagesAi from "../../all_images_ai.app.mjs"; + +export default { + key: "all_images_ai-buy-image", + name: "Buy Image", + description: "Allows user to purchase an image and receive a direct public link. User must have sufficient credit balance. [See the documentation](https://developer.all-images.ai/all-images.ai-api/api-reference/images#buy-image-return-direct-url)", + version: "0.0.1", + type: "action", + props: { + allImagesAi, + imageId: { + propDefinition: [ + allImagesAi, + "imageId", + ], + }, + }, + async run({ $ }) { + const response = await this.allImagesAi.purchaseImage({ + $, + data: { + id: this.imageId, + }, + }); + + $.export("$summary", `Successfully purchased image. Public link: ${response.url}`); + return response; + }, +}; diff --git a/components/all_images_ai/actions/generate-images-advanced/generate-images-advanced.mjs b/components/all_images_ai/actions/generate-images-advanced/generate-images-advanced.mjs new file mode 100644 index 0000000000000..f9ca5aa4d35ae --- /dev/null +++ b/components/all_images_ai/actions/generate-images-advanced/generate-images-advanced.mjs @@ -0,0 +1,49 @@ +import allImagesAi from "../../all_images_ai.app.mjs"; + +export default { + key: "all_images_ai-generate-images-advanced", + name: "Generate Advanced Customized Images", + description: "Generates advanced customized images using a prompt from the user. [See the documentation](https://developer.all-images.ai/all-images.ai-api/api-reference/images-generation#create-image-generation)", + version: "0.0.1", + type: "action", + props: { + allImagesAi, + name: { + type: "string", + label: "Name", + description: "Enter the name for the image generation.", + }, + prompt: { + type: "string", + label: "Prompt", + description: "Enter the prompt for the image generation.", + }, + tags: { + type: "string[]", + label: "Tags", + description: "Tags make it easy to find generations.", + optional: true, + }, + metaData: { + type: "object", + label: "Meta Data", + description: "Pass on metadata to a ImageGeneration.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.allImagesAi.generateImage({ + $, + data: { + name: this.name, + mode: "advanced", + prompt: this.prompt, + tags: this.tags, + metaData: this.metaData, + }, + }); + $.export("$summary", `Successfully generated images with the name "${this.name}"`); + return response; + }, +}; + diff --git a/components/all_images_ai/actions/get-image-generation/get-image-generation.mjs b/components/all_images_ai/actions/get-image-generation/get-image-generation.mjs new file mode 100644 index 0000000000000..cd01ab7ef204e --- /dev/null +++ b/components/all_images_ai/actions/get-image-generation/get-image-generation.mjs @@ -0,0 +1,26 @@ +import allImagesAi from "../../all_images_ai.app.mjs"; + +export default { + key: "all_images_ai-get-image-generation", + name: "Get Image Generation", + description: "Retrieves a previously generated image using its unique ID. [See the documentation](https://developer.all-images.ai/all-images.ai-api/api-reference/images-generation#get-image-generation)", + version: "0.0.1", + type: "action", + props: { + allImagesAi, + imageGenerationId: { + propDefinition: [ + allImagesAi, + "imageGenerationId", + ], + }, + }, + async run({ $ }) { + const response = await this.allImagesAi.getImage({ + $, + imageGenerationId: this.imageGenerationId, + }); + $.export("$summary", `Successfully retrieved image generation with ID: ${this.imageGenerationId}`); + return response; + }, +}; diff --git a/components/all_images_ai/all_images_ai.app.mjs b/components/all_images_ai/all_images_ai.app.mjs index 3245b43215d5c..afb3489a34a86 100644 --- a/components/all_images_ai/all_images_ai.app.mjs +++ b/components/all_images_ai/all_images_ai.app.mjs @@ -1,11 +1,111 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + export default { type: "app", app: "all_images_ai", - propDefinitions: {}, + propDefinitions: { + imageId: { + type: "string", + label: "Image Id", + description: "Enter the unique ID of the image.", + async options({ page }) { + const { images } = await this.listImages({ + params: { + limit: LIMIT, + offset: page * LIMIT, + }, + }); + + return images.map(({ id }) => id); + }, + }, + imageGenerationId: { + type: "string", + label: "Image Generation Id", + description: "Enter the unique ID of the image.", + async options({ page }) { + const { prints } = await this.listGenerationImages({ + params: { + limit: LIMIT, + offset: page * LIMIT, + }, + }); + + return prints.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.all-images.ai/v1"; + }, + _headers() { + return { + "api-key": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + generateImage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/image-generations", + ...opts, + }); + }, + getImage({ + imageGenerationId, ...opts + }) { + return this._makeRequest({ + path: `/image-generations/${imageGenerationId}`, + ...opts, + }); + }, + listImages(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/images/search", + ...opts, + }); + }, + listGenerationImages(opts = {}) { + return this._makeRequest({ + path: "/image-generations", + ...opts, + }); + }, + purchaseImage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/images/buy", + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/api-keys/webhook/subscribe", + ...opts, + }); + }, + deleteWebhook(hookId) { + return this._makeRequest({ + method: "DELETE", + path: `/api-keys/webhook/unsubscribe/${hookId}`, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/all_images_ai/common/constants.mjs b/components/all_images_ai/common/constants.mjs new file mode 100644 index 0000000000000..ea830c15a04cb --- /dev/null +++ b/components/all_images_ai/common/constants.mjs @@ -0,0 +1 @@ +export const LIMIT = 100; diff --git a/components/all_images_ai/package.json b/components/all_images_ai/package.json index 37b4b3bba1767..75178cf7088ec 100644 --- a/components/all_images_ai/package.json +++ b/components/all_images_ai/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/all_images_ai", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream All-Images.ai Components", "main": "all_images_ai.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" } -} \ No newline at end of file +} diff --git a/components/all_images_ai/sources/new-generation-image-update-instant/new-generation-image-update-instant.mjs b/components/all_images_ai/sources/new-generation-image-update-instant/new-generation-image-update-instant.mjs new file mode 100644 index 0000000000000..4f4f52b256676 --- /dev/null +++ b/components/all_images_ai/sources/new-generation-image-update-instant/new-generation-image-update-instant.mjs @@ -0,0 +1,53 @@ +import allImagesAi from "../../all_images_ai.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "all_images_ai-new-generation-image-update-instant", + name: "New Generation Image Update (Instant)", + description: "Emit new event when the generation status of an image gets updated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + allImagesAi, + db: "$.service.db", + http: "$.interface.http", + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + }, + hooks: { + async activate() { + const { webhookId } = await this.allImagesAi.createWebhook({ + data: { + url: this.http.endpoint, + events: [ + "print.created", + "print.active", + "print.progress", + "print.failed", + "print.completed", + ], + }, + }); + this._setHookId(webhookId); + }, + async deactivate() { + const hookId = this._getHookId(); + await this.allImagesAi.deleteWebhook(hookId); + }, + }, + async run({ body }) { + this.$emit(body, { + id: body.id, + summary: `Image generation status updated for image ID: ${body.type}`, + ts: Date.parse(body.created), + }); + }, + sampleEmit, +}; diff --git a/components/all_images_ai/sources/new-generation-image-update-instant/test-event.mjs b/components/all_images_ai/sources/new-generation-image-update-instant/test-event.mjs new file mode 100644 index 0000000000000..f7f96b3c51544 --- /dev/null +++ b/components/all_images_ai/sources/new-generation-image-update-instant/test-event.mjs @@ -0,0 +1,33 @@ +export default { + "id": "string", + "api_version": "number", + "created": "number", + "type": "string", + "data": { // Image Generation + "id": "string", + "name": "string", + "prompt": "string", + "status": 0, + "params": [ + { + "name": "sujetMode", + "value": "string" + } + ], + "images": [ + { + "id": "string", + "url": "string", + "validate": true, + "free": true, + "titles": {} + } + ], + "nbImages": 0, + "tags": [ + "string" + ], + "metaData": {}, + "createdAt": "2023-10-30T14:10:34.002Z" + } +} \ No newline at end of file diff --git a/components/allocadence/actions/create-customer-order/create-customer-order.mjs b/components/allocadence/actions/create-customer-order/create-customer-order.mjs new file mode 100644 index 0000000000000..8da0e725f2fae --- /dev/null +++ b/components/allocadence/actions/create-customer-order/create-customer-order.mjs @@ -0,0 +1,343 @@ +import { ConfigurationError } from "@pipedream/platform"; +import allocadence from "../../allocadence.app.mjs"; +import { POSTAGE_ACCOUNT_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "allocadence-create-customer-order", + name: "Create Customer Order", + description: "Creates a new customer order. [See the documentation](https://docs.allocadence.com/#tag/customer_order/paths/~1customer-orders/post)", + version: "0.0.1", + type: "action", + props: { + allocadence, + orderNumber: { + type: "string", + label: "Order Number", + description: "Order number for the customer order.", + optional: true, + }, + clientId: { + type: "integer", + label: "Client Id", + description: "Id of the client that the customer order is for.", + optional: true, + }, + clientName: { + type: "string", + label: "Client Name", + description: "Name of the client that the customer order is for. **Ignored if clientId is provided and is nonzero.**", + optional: true, + }, + customerId: { + type: "integer", + label: "Customer Id", + description: "Id of the customer. If none is provided, will attempt to find an existing customer based on the other customer fields and shipping address. Other fields not used if id is provided.", + optional: true, + }, + title: { + type: "string", + label: "Customer Title", + description: "The title of the customer.", + optional: true, + }, + name: { + type: "string", + label: "Customer Name", + description: "A combination of name and surname is required if the customer is new.", + optional: true, + }, + surname: { + type: "string", + label: "Customer Surname", + description: "A combination of name and surname is required if the customer is new.", + optional: true, + }, + email: { + type: "string", + label: "Customer Email", + description: "The email of the customer.", + optional: true, + }, + customerCompany: { + type: "string", + label: "Customer Company", + description: "The company of the customer.", + optional: true, + }, + accountNumber: { + type: "string", + label: "Customer Account Number", + description: "Will only match a customer on this field if provided.", + optional: true, + }, + shippingAddressId: { + type: "integer", + label: "Shipping Address Id", + description: "Id of the shipping address. **Other Shipping fields will not be used if provided.**", + optional: true, + }, + shippingAddressCode: { + type: "string", + label: "Shipping Address Code", + description: "Used to find an existing address. Other fields not used if an address is successfully found.", + optional: true, + }, + shippingAddressCompany: { + type: "string", + label: "Shipping Address Company", + description: "The company of the shipping address.", + optional: true, + }, + shippingAddressName: { + type: "string", + label: "Shipping Address Name", + description: "The name of the shipping address.", + optional: true, + }, + shippingAddressline1: { + type: "string", + label: "Shipping Address Line 1", + description: "The shipping address line 1.", + optional: true, + }, + shippingAddressline2: { + type: "string", + label: "Shipping Address Line 2", + description: "The shipping address line 2.", + optional: true, + }, + shippingAddressline3: { + type: "string", + label: "Shipping Address Line 3", + description: "The shipping address line 3.", + optional: true, + }, + shippingAddressCity: { + type: "string", + label: "Shipping Address City", + description: "The city of the shipping address.", + optional: true, + }, + shippingAddressState: { + type: "string", + label: "Shipping Address State", + description: "The state of the shipping address.", + optional: true, + }, + shippingAddressZip: { + type: "string", + label: "Shipping Address Zip", + description: "The zip of the shipping address.", + optional: true, + }, + shippingAddressCountryCode: { + type: "string", + label: "Shipping Address Country Code", + description: "The country code of the shipping address. [See the ISO 3166 codes](https://www.iban.com/country-codes).", + optional: true, + }, + shippingAddressPhone: { + type: "string", + label: "Shipping Address Phone", + description: "The phone of the shipping address.", + optional: true, + }, + sameAsShipping: { + type: "boolean", + label: "Same As Shipping", + description: "True if the billing address is the same as the shipping address.", + reloadProps: true, + optional: true, + }, + shipFromWarehouseId: { + type: "string", + label: "Ship From Warehouse Id", + description: "Id of the warehouse the ordered items will be allocated from. If no warehouse parameters are given, then the user's current warehouse will be used.", + optional: true, + }, + shipFromWarehouseName: { + type: "string", + label: "Ship From Warehouse Name", + description: "Name of the warehouse the ordered items will be allocated from. Ignored if warehouseId is provided.", + optional: true, + }, + shipVia: { + type: "string", + label: "Ship Via", + description: "Code of the carrier or service to use for shipping.", + optional: true, + }, + postageAccount: { + type: "string", + label: "Postage Account", + description: "Who to bill for shipping.", + options: POSTAGE_ACCOUNT_OPTIONS, + optional: true, + }, + items: { + type: "string[]", + label: "Items", + description: "An array of objects of ordered items. **Example: {\"itemId\": \"123\", \"sku\": \"SKU123\", \"quantity\": 1}** [See the documentation](https://docs.allocadence.com/#tag/customer_order/paths/~1customer-orders/post) fro further information.", + optional: true, + }, + }, + async additionalProps() { + const props = {}; + + if (!this.sameAsShipping) { + props.billingAddressId = { + type: "integer", + label: "Billing Address Id", + description: "Id of the billing address. **Other Billing fields will not be used if provided.**", + optional: true, + }; + props.billingAddressCode = { + type: "string", + label: "Billing Address Code", + description: "Used to find an existing address. Other fields not used if an address is successfully found.", + optional: true, + }; + props.billingAddressCompany = { + type: "string", + label: "Billing Address Company", + description: "The company of the billing address.", + optional: true, + }; + props.billingAddressName = { + type: "string", + label: "Billing Address Name", + description: "The name of the billing address.", + optional: true, + }; + props.billingAddressline1 = { + type: "string", + label: "Billing Address Line 1", + description: "The billing address line 1.", + optional: true, + }; + props.billingAddressline2 = { + type: "string", + label: "Billing Address Line 2", + description: "The billing address line 2.", + optional: true, + }; + props.billingAddressline3 = { + type: "string", + label: "Billing Address Line 3", + description: "The billing address line 3.", + optional: true, + }; + props.billingAddressCity = { + type: "string", + label: "Billing Address City", + description: "The city of the billing address.", + optional: true, + }; + props.billingAddressState = { + type: "string", + label: "Billing Address State", + description: "The state of the billing address.", + optional: true, + }; + props.billingAddressZip = { + type: "string", + label: "Billing Address Zip", + description: "The zip of the billing address.", + optional: true, + }; + props.billingAddressCountryCode = { + type: "string", + label: "Billing Address Country Code", + description: "The country code of the billing address. [See the ISO 3166 codes](https://www.iban.com/country-codes).", + optional: true, + }; + props.billingAddressPhone = { + type: "string", + label: "Billing Address Phone", + description: "The phone of the billing address.", + optional: true, + }; + } + return props; + }, + async run({ $ }) { + if (!this.customerId && + !this.title && + !this.name && + !this.surname && + !this.email && + !this.customerCompany && + !this.accountNumber) { + throw new ConfigurationError("You must provide at least 'Customer Id', 'Customer Title', 'Customer Name', 'Customer Surname', 'Customer Email', 'Customer Company' or 'Customer Account Number'."); + } + + if (!this.shippingAddressId && + !this.shippingAddressline1 && + !this.shippingAddressCountryCode) { + throw new ConfigurationError("You must provide at least 'Shipping Address Id' or 'Shipping Address Line1' and 'Shipping Address Country Code'."); + } + + if (!this.sameAsShipping && + !this.billingAddressId && + !this.billingAddressline1 && + !this.billingAddressCountryCode) { + throw new ConfigurationError("When 'Same As Shipping' is set **False** you must provide at least 'Billing Address Id' or 'Billing Address Line1' and 'Billing Address Country Code'."); + } + + const response = await this.allocadence.createCustomerOrder({ + $, + data: { + orderNumber: this.orderNumber, + clientId: this.clientId, + clientName: this.clientName, + customer: { + id: this.customerId, + title: this.title, + name: this.name, + surname: this.surname, + email: this.email, + company: this.customerCompany, + accountNumber: this.accountNumber, + }, + shippingAddress: { + id: this.shippingAddressId, + code: this.shippingAddressCode, + company: this.shippingAddressCompany, + name: this.shippingAddressName, + line1: this.shippingAddressline1, + line2: this.shippingAddressline2, + line3: this.shippingAddressline3, + city: this.shippingAddressCity, + state: this.shippingAddressState, + zip: this.shippingAddressZip, + countryCode: this.shippingAddressCountryCode, + phone: this.shippingAddressPhone, + }, + billingAddress: { + sameAsShipping: this.sameAsShipping, + id: this.billingAddressId, + code: this.billingAddressCode, + company: this.billingAddressCompany, + name: this.billingAddressName, + line1: this.billingAddressline1, + line2: this.billingAddressline2, + line3: this.billingAddressline3, + city: this.billingAddressCity, + state: this.billingAddressState, + zip: this.billingAddressZip, + countryCode: this.billingAddressCountryCode, + phone: this.billingAddressPhone, + }, + shipFromWarehouseId: this.shipFromWarehouseId, + shipFromWarehouseName: this.shipFromWarehouseName, + shipVia: this.shipVia, + postageAccount: this.postageAccount, + items: parseObject(this.items), + }, + }); + + $.export("$summary", `Successfully created customer order with ID ${response.id}`); + return response; + }, +}; diff --git a/components/allocadence/actions/create-purchase-order/create-purchase-order.mjs b/components/allocadence/actions/create-purchase-order/create-purchase-order.mjs new file mode 100644 index 0000000000000..4d4ee3d41601c --- /dev/null +++ b/components/allocadence/actions/create-purchase-order/create-purchase-order.mjs @@ -0,0 +1,112 @@ +import { ConfigurationError } from "@pipedream/platform"; +import allocadence from "../../allocadence.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "allocadence-create-purchase-order", + name: "Create Purchase Order", + description: "Generates a new purchase order. [See the documentation](https://docs.allocadence.com/)", + version: "0.0.1", + type: "action", + props: { + allocadence, + supplierId: { + type: "integer", + label: "Supplier Id", + description: "Id of the supplier that is being ordered from.", + optional: true, + }, + supplierName: { + type: "string", + label: "Supplier Name", + description: "Name of the supplier that is being ordered from. Ignored if supplierId is provided.", + optional: true, + }, + warehouseId: { + type: "integer", + label: "Warehouse Id", + description: "Id of the warehouse the items will be delivered to. If no warehouse parameters are given, then the user's current warehouse will be used.", + optional: true, + }, + warehouseName: { + type: "string", + label: "Warehouse Name", + description: "Name of the warehouse the items will be delivered to. Ignored if warehouseId is provided.", + optional: true, + }, + clientId: { + type: "integer", + label: "Client Id", + description: "Id of the client that the purchase order is for. Defaults to the user's client id.", + optional: true, + }, + clientName: { + type: "string", + label: "Client Name", + description: "Name of the client that the purchase order is for. Ignored if clientId is provided and is nonzero.", + optional: true, + }, + orderNumber: { + type: "string", + label: "Order Number", + description: "Order number for the purchase order. If blank, one will automatically be generated.", + optional: true, + }, + draft: { + type: "boolean", + label: "Draft", + description: "True if the purchase order should be created as a draft to allow future editing.", + optional: true, + }, + requiredByDate: { + type: "string", + label: "Required By.", + description: "The date of the purchase. **Format: YYYY-MM-DD**", + optional: true, + }, + projectNumber: { + type: "string", + label: "Project Number", + description: "The number of the project.", + optional: true, + }, + notes: { + type: "string", + label: "Notes", + description: "A note of the purchase.", + optional: true, + }, + items: { + type: "string[]", + label: "Items", + description: "A list of object of ordered items. **Example: {\"itemId\": 123, \"sku\": \"SKU123\", \"description\": \"description\", \"quantity\": 1}**. [See the documentation](https://docs.allocadence.com/#tag/purchase_order/paths/~1purchase-orders/post) for further information.", + optional: true, + }, + }, + async run({ $ }) { + if (!this.supplierId && !this.supplierName) { + throw new ConfigurationError("You must provide at least 'Supplier Id' or 'Supplier Name'."); + } + + const response = await this.allocadence.createPurchaseOrder({ + $, + data: { + supplierId: this.supplierId, + supplierName: this.supplierName, + warehouseId: this.warehouseId, + warehouseName: this.warehouseName, + clientId: this.clientId, + clientName: this.clientName, + orderNumber: this.orderNumber, + draft: this.draft, + requiredByDate: this.requiredByDate, + projectNumber: this.projectNumber, + notes: this.notes, + items: parseObject(this.items), + }, + }); + + $.export("$summary", `Successfully created purchase order with ID ${response.id}`); + return response; + }, +}; diff --git a/components/allocadence/allocadence.app.mjs b/components/allocadence/allocadence.app.mjs new file mode 100644 index 0000000000000..9648b1b0cb310 --- /dev/null +++ b/components/allocadence/allocadence.app.mjs @@ -0,0 +1,78 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "allocadence", + methods: { + _baseUrl() { + return "https://app.allocadence.com/rest"; + }, + _auth() { + return { + username: `${this.$auth.api_key}`, + password: `${this.$auth.api_secret}`, + }; + }, + _makeRequest({ + $ = this, method = "GET", path, ...opts + }) { + return axios($, { + method, + url: this._baseUrl() + path, + auth: this._auth(), + ...opts, + }); + }, + createCustomerOrder(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/customer-orders", + ...opts, + }); + }, + createPurchaseOrder(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/purchase-orders", + ...opts, + }); + }, + listCustomerOrders(opts = {}) { + return this._makeRequest({ + path: "/customer-orders", + ...opts, + }); + }, + listPurchaseOrders(opts = {}) { + return this._makeRequest({ + path: "/purchase-orders", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, dataField, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const response = await fn({ + params, + ...opts, + }); + for (const d of response[dataField]) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = page < response.meta.totalPages; + + } while (hasMore); + }, + }, +}; diff --git a/components/allocadence/common/constants.mjs b/components/allocadence/common/constants.mjs new file mode 100644 index 0000000000000..a3deffb7fea4d --- /dev/null +++ b/components/allocadence/common/constants.mjs @@ -0,0 +1,5 @@ +export const POSTAGE_ACCOUNT_OPTIONS = [ + "sender", + "client", + "recipient", +]; diff --git a/components/allocadence/common/utils.mjs b/components/allocadence/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/allocadence/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/allocadence/package.json b/components/allocadence/package.json new file mode 100644 index 0000000000000..792f7575ea61a --- /dev/null +++ b/components/allocadence/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/allocadence", + "version": "0.1.0", + "description": "Pipedream Allocadence Components", + "main": "allocadence.app.mjs", + "keywords": [ + "pipedream", + "allocadence" + ], + "homepage": "https://pipedream.com/apps/allocadence", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/allocadence/sources/common/base.mjs b/components/allocadence/sources/common/base.mjs new file mode 100644 index 0000000000000..1ba07e91f4d2c --- /dev/null +++ b/components/allocadence/sources/common/base.mjs @@ -0,0 +1,69 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import allocadence from "../../allocadence.app.mjs"; + +export default { + props: { + allocadence, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01T00:00:01"; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + prepareData({ + responseArray, fieldDate, + }) { + if (responseArray.length) { + this._setLastDate(responseArray[0][fieldDate]); + } + return responseArray; + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const fieldDate = this.getFieldDate(); + + const response = this.allocadence.paginate({ + fn: this.getFunction(), + params: this.getParams(lastDate), + dataField: this.getDataField(), + maxResults, + }); + + let responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + const preparedData = this.prepareData({ + responseArray, + fieldDate, + maxResults, + }); + + for (const item of preparedData.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(new Date()), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/allocadence/sources/new-customer-order/new-customer-order.mjs b/components/allocadence/sources/new-customer-order/new-customer-order.mjs new file mode 100644 index 0000000000000..64bfe62c53899 --- /dev/null +++ b/components/allocadence/sources/new-customer-order/new-customer-order.mjs @@ -0,0 +1,46 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "allocadence-new-customer-order", + name: "New Customer Order Created", + description: "Emit new event when a new customer order is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + prepareData({ + responseArray, fieldDate, maxResults, + }) { + responseArray.reverse(); + if (responseArray.length) { + if (maxResults && responseArray.length > maxResults) { + responseArray.length = maxResults; + } + this._setLastDate(responseArray[0][fieldDate]); + } + return responseArray; + }, + getParams(orderedDate) { + return { + orderedDate, + orderedDateConditional: "on_or_after", + }; + }, + getFunction() { + return this.allocadence.listCustomerOrder; + }, + getDataField() { + return "customerOrders"; + }, + getFieldDate() { + return "orderedDate"; + }, + getSummary(item) { + return `New Customer Order: ${item.orderNumber}`; + }, + }, + sampleEmit, +}; diff --git a/components/allocadence/sources/new-customer-order/test-event.mjs b/components/allocadence/sources/new-customer-order/test-event.mjs new file mode 100644 index 0000000000000..aa1d145c03e70 --- /dev/null +++ b/components/allocadence/sources/new-customer-order/test-event.mjs @@ -0,0 +1,109 @@ +export default { + "id": 103717, + "orderNumber": "Test-Order-001", + "orderReference": "", + "customer": { + "id": 7089, + "title": "", + "name": "Test Customer 1", + "surname": "", + "email": "testcustomer1@testserver.com", + "company": "Test Company", + "accountNumber": "" + }, + "client": null, + "orderedDate": "2021-12-13T00:00:00-07:00", + "createdDate": "2024-09-13T13:02:58-07:00", + "modifiedDate": "2024-09-13T13:02:58-07:00", + "orderPlaced": true, + "createdBy": { + "id": 520, + "name": "Pipedream Support" + }, + "completed": false, + "completedDate": "", + "cancelled": false, + "cancelledDate": "", + "cancelledReason": "", + "cancelledBy": null, + "onHold": false, + "onHoldUntil": "", + "postageAccount": "sender", + "buyerPaidShipping": 0, + "shippingAddress": { + "id": 12517, + "company": "", + "name": "", + "line1": "515 E Grant St", + "line2": "", + "line3": "", + "city": "Phoenix", + "state": "AZ", + "zip": "85004", + "country": "United States", + "countryCode": "US", + "phone": "", + "code": "", + "verifiedStatus": "awaiting", + "verifiedDate": "", + "verifiedMessage": "" + }, + "billingAddress": { + "id": 12518, + "company": "", + "name": "", + "line1": "N/A", + "line2": "", + "line3": "", + "city": "", + "state": "", + "zip": "", + "country": "United States", + "countryCode": "US", + "phone": "", + "code": "", + "verifiedStatus": "awaiting", + "verifiedDate": "", + "verifiedMessage": "" + }, + "shipVia": "Standard", + "shipViaPackaging": "", + "shipViaConfirmation": "", + "dryIceWeight": 0, + "packageSku": "", + "shipFromWarehouse": { + "id": 187, + "name": "Main Warehouse" + }, + "projectNumber": "", + "discountPercentage": 0, + "orderSource": "CSV Import", + "internalNote": "", + "noteFromCustomer": "", + "noteToCustomer": "", + "notificationEmail": "", + "userField1": "", + "userField2": "", + "userField3": "", + "myList1": "", + "myList2": "", + "tags": [], + "items": [ + { + "id": 214405, + "itemId": 12063, + "sku": "TEST-SKU-101", + "description": "Baseball Cap", + "orderQuantity": 1, + "uom": "Each", + "quantity": 1, + "allocatedQuantity": 1, + "pickedQuantity": 0, + "price": 49, + "discountPercentage": 0, + "partOfKit": false, + "componentOf": 0, + "additionalFields": null + } + ] +} \ No newline at end of file diff --git a/components/allocadence/sources/new-purchase-order/new-purchase-order.mjs b/components/allocadence/sources/new-purchase-order/new-purchase-order.mjs new file mode 100644 index 0000000000000..072982dfd2877 --- /dev/null +++ b/components/allocadence/sources/new-purchase-order/new-purchase-order.mjs @@ -0,0 +1,35 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "allocadence-new-purchase-order", + name: "New Purchase Order Created", + description: "Emit new event when a new purchase order is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getParams(lastDate) { + return { + createdFrom: lastDate, + orderBy: "createdDate", + orderDir: "desc", + }; + }, + getFunction() { + return this.allocadence.listPurchaseOrder; + }, + getDataField() { + return "purchaseOrders"; + }, + getFieldDate() { + return "createdDate"; + }, + getSummary(item) { + return `New Purchase Order: ${item.orderNumber}`; + }, + }, + sampleEmit, +}; diff --git a/components/allocadence/sources/new-purchase-order/test-event.mjs b/components/allocadence/sources/new-purchase-order/test-event.mjs new file mode 100644 index 0000000000000..5e0b55e3aed9e --- /dev/null +++ b/components/allocadence/sources/new-purchase-order/test-event.mjs @@ -0,0 +1,32 @@ +export default { + "id": 3858, + "orderNumber": "000000", + "supplier": { + "id": 569, + "name": "Default Supplier" + }, + "warehouse": { + "id": 187, + "name": "Main Warehouse" + }, + "client": null, + "user": { + "id": 123, + "name": "User Name" + }, + "draft": true, + "completed": false, + "deleted": false, + "createdDate": "2024-10-07T11:00:31-07:00", + "preparedDate": "", + "requiredByDate": "2024-10-07", + "completedDate": "", + "projectNumber": "", + "notes": "", + "shipMethod": "", + "terms": "", + "userField1": "", + "userField2": "", + "userField3": "", + "items": [] +} \ No newline at end of file diff --git a/components/alpaca/README.md b/components/alpaca/README.md index 6d37d8ced0a24..c0fbf50403b1c 100644 --- a/components/alpaca/README.md +++ b/components/alpaca/README.md @@ -1,13 +1,11 @@ # Overview -Alpaca is a financial technology company that offers a commission-free API for -trading and investing. With Alpaca, you can build trading and investing -applications for the web, mobile, and desktop. +Alpaca API allows you to harness the power of automated trading by providing an interface to manage your stock and equity portfolio through simple API calls. It's particularly suited for building trading bots, algorithmic trading strategies, or just automating interactions with your investment portfolio. Whether you're looking to place trades based on specific market conditions, automatically adjust your portfolio in response to certain triggers, or simply streamline your investment activities, Alpaca's trading API offers a robust solution. -Here are some examples of what you can build with Alpaca: +# Example Use Cases -- A trading platform that lets users buy and sell stocks and other assets -- An investing app that helps users manage their portfolios -- A financial newsfeed that provides real-time market data -- A stock market simulator that lets users practice trading without risk -- A financial planning tool that helps users save for their future +- **Automated Trading Bot**: Create a trading bot on Pipedream that reacts to real-time market data from a separate financial data app (like Alpha Vantage). When certain stock prices hit predefined thresholds, the bot can execute trades on Alpaca, enabling responsive and dynamic trading strategies. + +- **Portfolio Rebalancing**: Set up a Pipedream workflow that periodically checks your portfolio performance on Alpaca and compares it with desired asset allocations. If deviations are detected, automatically generate and execute orders to buy or sell assets to maintain the balance, ensuring your investments stay aligned with your strategy. + +- **Market Alerts to Slack**: Get instant notifications in a Slack channel when certain stocks reach trigger points. A workflow on Pipedream listens for market data changes, and when specific conditions are met, it sends an alert to Slack using the Slack app, so you can make timely investment decisions or just stay informed. diff --git a/components/alpha_vantage/README.md b/components/alpha_vantage/README.md index 05249f6046521..0be37bcb32296 100644 --- a/components/alpha_vantage/README.md +++ b/components/alpha_vantage/README.md @@ -1,8 +1,14 @@ # Overview -Some examples of what you can build with the Alpha Vantage API: +The Alpha Vantage API offers access to a wealth of financial data, including real-time and historical stock prices, forex rates, cryptocurrency data, and more. With Pipedream’s serverless platform, you can easily connect Alpha Vantage to a variety of other services and apps to create powerful financial data workflows. Automate the tracking of stock performance, set up alerts for forex rate changes, digest cryptocurrency trends, or integrate financial data into custom dashboards or databases without the need to manage complex infrastructure. -- Stock Market Data -- Technical Indicators -- Sector Performance -- Currency Exchange Rates +# Example Use Cases + +- **Stock Price Alert** + Create a workflow that triggers daily, checking specific stock prices via Alpha Vantage. If the price exceeds a certain threshold, send a notification email through Gmail or SMS via Twilio, allowing for timely investment decisions. + +- **Forex Rate Tracker** + Set up a workflow that polls currency exchange rates at regular intervals. Store this data in a Google Sheet for analysis or use Pipedream's built-in data store for historical tracking. Use this data to inform purchasing decisions or trigger alerts when rates are favorable. + +- **Cryptocurrency Portfolio Dashboard** + Construct a workflow that aggregates your cryptocurrency holdings data from Alpha Vantage. Pipe this data into a custom dashboard on platforms like Geckoboard or a visualization tool like Tableau for a real-time overview of your portfolio’s performance. diff --git a/components/alpha_vantage/package.json b/components/alpha_vantage/package.json new file mode 100644 index 0000000000000..b98785e1a605e --- /dev/null +++ b/components/alpha_vantage/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/alpha_vantage", + "version": "0.6.0", + "description": "Pipedream alpha_vantage Components", + "main": "alpha_vantage.app.mjs", + "keywords": [ + "pipedream", + "alpha_vantage" + ], + "homepage": "https://pipedream.com/apps/alpha_vantage", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/alphamoon/README.md b/components/alphamoon/README.md new file mode 100644 index 0000000000000..a79144061e0c5 --- /dev/null +++ b/components/alphamoon/README.md @@ -0,0 +1,11 @@ +# Overview + +The Alphamoon API taps into advanced AI-driven text processing, offering services like document understanding, information extraction, and automated data entry. With this API, you can transform unstructured text into actionable data within your Pipedream workflows. It's a potent tool for automating document-heavy processes, making it invaluable for tasks ranging from data analysis to integrating extracted info into databases or other apps. + +# Example Use Cases + +- **Automated Invoice Processing**: Extract key information from scanned invoices using Alphamoon's OCR capabilities. Validate and record extracted data into a Google Sheets spreadsheet, creating an efficient, serverless bookkeeping workflow. + +- **Contract Analysis for Legal Tech**: Feed contract documents into Alphamoon to identify and extract specific clauses. Use this extracted data to flag potential issues and integrate with Slack to notify your legal team to review the contracts. + +- **Customer Feedback Analysis**: Analyze text from customer feedback forms submitted via a website. Use Alphamoon to detect sentiment and key topics, then categorize and store insights in a CRM tool like Salesforce for marketing and product development teams to act upon. diff --git a/components/alphamoon/package.json b/components/alphamoon/package.json index f2757a2535cc1..df02e4466b146 100644 --- a/components/alphamoon/package.json +++ b/components/alphamoon/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/alteryx_analytics_cloud/alteryx_analytics_cloud.app.mjs b/components/alteryx_analytics_cloud/alteryx_analytics_cloud.app.mjs new file mode 100644 index 0000000000000..a247dc6a589d8 --- /dev/null +++ b/components/alteryx_analytics_cloud/alteryx_analytics_cloud.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "alteryx_analytics_cloud", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/alteryx_analytics_cloud/package.json b/components/alteryx_analytics_cloud/package.json new file mode 100644 index 0000000000000..94d7601bb3849 --- /dev/null +++ b/components/alteryx_analytics_cloud/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/alteryx_analytics_cloud", + "version": "0.0.1", + "description": "Pipedream Alteryx Analytics Cloud Components", + "main": "alteryx_analytics_cloud.app.mjs", + "keywords": [ + "pipedream", + "alteryx_analytics_cloud" + ], + "homepage": "https://pipedream.com/apps/alteryx_analytics_cloud", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/altiria/actions/send-sms/send-sms.mjs b/components/altiria/actions/send-sms/send-sms.mjs new file mode 100644 index 0000000000000..7141a52baeb8a --- /dev/null +++ b/components/altiria/actions/send-sms/send-sms.mjs @@ -0,0 +1,51 @@ +import app from "../../altiria.app.mjs"; + +export default { + key: "altiria-send-sms", + name: "Send SMS", + description: "Send an SMS message. The message will be sent to the phone numbers you specify. [See the documentation](https://static.altiria.com/especificaciones/altiria_push_rest.pdf).", + type: "action", + version: "0.0.1", + props: { + app, + destination: { + type: "string[]", + label: "Destination Phone Numbers", + description: "The phone numbers to which the message will be sent. Each number will be specified in international numbering format without the prefix `00` or the sign `+`. Eg: `34645852126`. It is essential to include the country prefix (`34` for Spain) so that the message reaches the expected destination. It must not exceed 16 digits. In any case, it is recommended not to exceed **100** phone numbers per request.", + }, + msg: { + type: "string", + label: "Message", + description: "Message to send. The list of valid characters and the maximum allowed length is detailed in section 2.4 of the [documentation](https://static.altiria.com/especificaciones/altiria_push_rest.pdf). It cannot be empty (empty string). Mobile web identifiers can be added to generate unique shortened links in the message body. See section 2.5 for more details on mobile webs.", + }, + }, + methods: { + sendSms(args = {}) { + return this.app.post({ + path: "/sendSms", + ...args, + }); + }, + }, + async run({ $ }) { + const { + sendSms, + destination, + msg, + } = this; + + const response = await sendSms({ + $, + data: { + destination, + message: { + msg, + }, + }, + }); + + $.export("$summary", "Successfully sent SMS message."); + + return response; + }, +}; diff --git a/components/altiria/altiria.app.mjs b/components/altiria/altiria.app.mjs new file mode 100644 index 0000000000000..deb5f70a942d3 --- /dev/null +++ b/components/altiria/altiria.app.mjs @@ -0,0 +1,48 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "altiria", + propDefinitions: {}, + methods: { + getUrl(path) { + return `https://www.altiria.net:8443/apirest/ws${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Content-Type": "application/json;charset=UTF-8", + "Accept": "application/json", + }; + }, + getDataAuth(data) { + const { + api_key: apiKey, + api_secret: apiSecret, + } = this.$auth; + return { + ...data, + credentials: { + apiKey, + apiSecret, + }, + }; + }, + makeRequest({ + $ = this, path, headers, data, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + data: this.getDataAuth(data), + }); + }, + post(args = {}) { + return this.makeRequest({ + method: "POST", + ...args, + }); + }, + }, +}; diff --git a/components/altiria/package.json b/components/altiria/package.json new file mode 100644 index 0000000000000..06bbf031e565e --- /dev/null +++ b/components/altiria/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/altiria", + "version": "0.1.0", + "description": "Pipedream Altiria Components", + "main": "altiria.app.mjs", + "keywords": [ + "pipedream", + "altiria" + ], + "homepage": "https://pipedream.com/apps/altiria", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.1" + } +} diff --git a/components/altoviz/README.md b/components/altoviz/README.md new file mode 100644 index 0000000000000..378c5c9695970 --- /dev/null +++ b/components/altoviz/README.md @@ -0,0 +1,11 @@ +# Overview + +The Altoviz API offers access to a range of data visualization tools that allow users to create, manage, and embed interactive charts and maps into their applications. With Pipedream, you can leverage this functionality to automate the generation of custom visualizations based on various data sources and events. You can trigger workflows using webhooks, schedule them, or even react to emails and messages, and then use the Altoviz API to dynamically create data visualizations that can be shared with your audience or team. + +# Example Use Cases + +- **Automated Reporting**: Set up a workflow that triggers at regular intervals (e.g., daily, weekly) to fetch data from a database or API, such as Google Sheets or SQL, and use the Altoviz API to generate a visualization. This visualization can then be emailed to stakeholders or posted to a team channel in Slack, keeping everyone updated with the latest metrics. + +- **Real-time Dashboard Updates**: Create a real-time dashboard by deploying a webhook-triggered Pipedream workflow that listens for data updates from your app. Each time new data comes in, use Altoviz API to refresh the visualization on your dashboard, providing up-to-the-minute insights for users or customers. + +- **Integrating User Feedback into Visuals**: Implement a workflow that captures user feedback via a form submission or a support ticket system like Zendesk. Analyze the feedback data, categorize it, and use the Altoviz API to update a public-facing visualization that tracks customer satisfaction or product improvement over time. diff --git a/components/alttext_ai/README.md b/components/alttext_ai/README.md new file mode 100644 index 0000000000000..f896b8b2e6a9e --- /dev/null +++ b/components/alttext_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +AltText.ai provides an API that leverages artificial intelligence to generate alternative text (alt text) for images, which is crucial for web accessibility and SEO. With AltText.ai, you can automate the process of creating descriptive, keyword-rich, and meaningful alt text for images on websites, blogs, or online stores. In Pipedream, you can create workflows that trigger on various events to send images to the AltText.ai API and receive alt text, which you can then store or apply to your content management system automatically. + +# Example Use Cases + +- **Automated Alt Text for E-commerce Products**: Automatically generate and update alt text for new product images uploaded to your online store. When an image is added to a specific folder in Dropbox, Pipedream can trigger a workflow to send that image to AltText.ai, receive the generated alt text, and then patch the product's details with the new alt text in a platform like Shopify. + +- **Blog Post Accessibility Enhancement**: Improve the accessibility of your blog by ensuring all images have proper alt text. Each time a WordPress post is drafted, use Pipedream to send any embedded images to AltText.ai, then update the post with the received alt text before it's published. + +- **SEO Optimization for Static Sites**: Optimize your static website hosted on GitHub Pages by adding alt text to images. When a new commit is pushed to a designated GitHub repository, Pipedream can trigger a workflow that checks for added or updated images, sends them to AltText.ai for alt text generation, and then commits a change to the image tags with the new alt text. diff --git a/components/amara/README.md b/components/amara/README.md index 500e6520a2b2a..ad29802b780ef 100644 --- a/components/amara/README.md +++ b/components/amara/README.md @@ -1,11 +1,11 @@ # Overview -The Amara API enables you to build a wide variety of applications and tools -that work with subtitles and captions. Here are just a few examples: - -- Subtitle translation -- Transcribing audio or video files -- Creating subtitles or captions for videos -- Synchronizing subtitles with videos -- Editing subtitles or captions -- Converting subtitles or captions between different formats +The Amara API provides programmatic access to Amara’s community-driven subtitle creation and translation services. With this API, you can automate subtitle workflows, streamline video content translation, and integrate with other services to create efficient localization processes. The API allows you to manage videos, subtitles, and teams, offering a doorway to effortlessly scaling your video content to reach global audiences. + +# Example Use Cases + +- **Automated Subtitle Generation Workflow**: By integrating the Amara API with transcription services like Google Speech-to-Text on Pipedream, you can create a workflow that automatically transcribes spoken language in videos, submits the transcript to Amara for subtitling, and then retrieves and stores the subtitles once they’re ready. + +- **Subtitle Sync and Translation Pipeline**: Utilize the Amara API in a Pipedream workflow that monitors your content management system (CMS) for new video uploads. Once a new video is detected, the workflow can automatically upload the video to Amara, trigger the subtitle creation process, and, upon completion, push those subtitles through a translation service like DeepL or Google Translate, then update the CMS with the translated subtitles for various language versions of the video. + +- **Team Collaboration Booster**: Create a Pipedream workflow that taps into the Amara API to oversee your subtitle translation projects. Automatically assign tasks to team members based on availability and expertise, notify translators when a new video is ready for their language skills, and track progress through to completion, all while keeping everyone in sync with notifications through communication platforms like Slack or Microsoft Teams. diff --git a/components/amazing_marvin/README.md b/components/amazing_marvin/README.md new file mode 100644 index 0000000000000..81d6a27b19908 --- /dev/null +++ b/components/amazing_marvin/README.md @@ -0,0 +1,11 @@ +# Overview + +The Amazing Marvin API lets you tap into your task management in a powerful way, enabling you to automate actions, fetch data for analysis, and sync with other apps. With Pipedream, these capabilities are amplified. You can create serverless workflows that interact with the Marvin tasks and projects, respond to events, and connect with countless other services to streamline your productivity systems. Whether you're looking to automatically schedule tasks, export your day's activities to other platforms, or trigger notifications based on task updates, Pipedream's integration makes it all possible without writing extensive code. + +# Example Use Cases + +- **Daily Task Digest Email**: Send a summary email each morning with the day's tasks fetched from Amazing Marvin. Use Pipedream's built-in email service or integrate with apps like SendGrid or Gmail to deliver this digest. + +- **Task Completion Webhook Trigger**: Set up a webhook in Pipedream that listens for task completion events from Amazing Marvin. Upon completion, trigger an action like logging time in a timesheet app like Toggl or sending a celebratory GIF via Slack. + +- **Sync with Calendar**: Whenever a new task with a due date is added in Amazing Marvin, use Pipedream to create an event in Google Calendar or Microsoft Outlook Calendar, keeping all deadlines visible and synchronized across platforms. diff --git a/components/amazon/README.md b/components/amazon/README.md index 33fd821e73126..24b7c52280b5d 100644 --- a/components/amazon/README.md +++ b/components/amazon/README.md @@ -1,11 +1,11 @@ # Overview -The Amazon API can be used to build a variety of applications, including: - -- E-commerce applications -- Shopping applications -- Price comparison applications -- Product review applications -- Wishlist applications -- Amazon Affiliate applications -- And more! +The Amazon API allows for robust interaction with Amazon's vast e-commerce platform. With Pipedream, you can harness this API to automate a variety of tasks, such as tracking price changes, managing inventory, or automating the buying process. By integrating the Amazon API with Pipedream's capabilities, you can set up custom workflows that respond to specific triggers or events. This could range from sending notifications when a product's price drops to automatically placing orders for items that are low in stock. + +# Example Use Cases + +- **Price Change Alerts**: Monitor specific products for price changes. When a price falls below a certain threshold or changes by a set percentage, trigger an alert. This alert could be sent via email, SMS, or directly to a Slack channel, keeping you or your team informed in real time. + +- **Inventory Management**: Set up a workflow that checks the inventory levels of your products on Amazon. If inventory drops below a set point, automatically reorder products or send a notification to your supplier through an app like Email or a project management tool like Asana to prompt a restock. + +- **Order Fulfillment Automation**: When a new order comes in through Amazon, use Pipedream to automate the fulfillment process. This could involve creating a task in a tool like Trello for the shipping department, generating a shipping label via an app like Shippo, and updating the order status on Amazon once the item is on its way to the customer. diff --git a/components/amazon/package.json b/components/amazon/package.json new file mode 100644 index 0000000000000..f2c53dbb0cabb --- /dev/null +++ b/components/amazon/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/amazon", + "version": "0.6.0", + "description": "Pipedream amazon Components", + "main": "amazon.app.mjs", + "keywords": [ + "pipedream", + "amazon" + ], + "homepage": "https://pipedream.com/apps/amazon", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/amazon_advertising/README.md b/components/amazon_advertising/README.md index c204a78404f5a..0e40f84f61a5e 100644 --- a/components/amazon_advertising/README.md +++ b/components/amazon_advertising/README.md @@ -1,13 +1,11 @@ # Overview -The Amazon Advertising API allows developers to programmatically access Amazon -advertising data and metrics. With the API, developers can build tools and -applications that enable advertisers to manage their Amazon advertising -campaigns more effectively. +The Amazon Advertising API provides programmatic access to advertising data such as campaign performance, keyword insights, and budget management for marketers and developers. Leveraging it with Pipedream, you can automate complex advertising tasks, sync campaign data with your database, and integrate with other marketing tools to enhance your advertising strategies. -Some examples of what you can build with the Amazon Advertising API include: +# Example Use Cases -- A tool for managing Amazon advertising campaigns -- A tool for analyzing Amazon advertising data -- An application for reporting on Amazon advertising performance -- A tool for optimizing Amazon advertising campaigns +- **Campaign Performance Dashboard**: Create a real-time dashboard by piping campaign performance data from Amazon Advertising into Google Sheets using Pipedream. Trigger a workflow that fetches the latest campaign metrics and appends them to a Google Sheet, allowing for easy monitoring and analysis. + +- **Automated Ad Spend Adjustment**: Set up a workflow that monitors your campaign performance metrics and automatically adjusts bids or pauses campaigns based on predefined criteria. For example, if the ACoS (Advertising Cost of Sale) surpasses a certain threshold, reduce the bid or pause the campaign to control spend and maintain profitability. + +- **Cross-Platform Ad Performance Reporting**: Generate comprehensive ad performance reports by aggregating data from Amazon Advertising and other platforms like Facebook Ads or Google Ads. Use Pipedream to orchestrate a workflow that collects data from multiple sources, consolidates it, and then pushes it to a BI tool like Tableau for in-depth cross-channel analysis. diff --git a/components/amazon_advertising/package.json b/components/amazon_advertising/package.json new file mode 100644 index 0000000000000..f784679b3b116 --- /dev/null +++ b/components/amazon_advertising/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/amazon_advertising", + "version": "0.6.0", + "description": "Pipedream amazon_advertising Components", + "main": "amazon_advertising.app.mjs", + "keywords": [ + "pipedream", + "amazon_advertising" + ], + "homepage": "https://pipedream.com/apps/amazon_advertising", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/amazon_alexa/README.md b/components/amazon_alexa/README.md new file mode 100644 index 0000000000000..40eb80152ec16 --- /dev/null +++ b/components/amazon_alexa/README.md @@ -0,0 +1,11 @@ +# Overview + +The Amazon Alexa API connects your Pipedream workflows to Alexa's smart capabilities, enabling you to build voice-driven interactions and extend the functionality of Alexa Skills. Interact with users through their Alexa devices, manage and update Skills, or harness Alexa's smart home control. With Pipedream's serverless platform, you can create complex workflows that respond to Alexa events, trigger actions in other services, or process voice commands for custom outcomes. + +# Example Use Cases + +- **Post Tweet with Voice Command**: Trigger a workflow with a custom Alexa skill that captures a voice command and posts a tweet to your Twitter account. Use Alexa to dictate the tweet, then the Pipedream workflow processes the text and interacts with the Twitter API to share your message. + +- **Voice-Controlled Smart Home Events**: Create a Pipedream workflow that listens for specific Alexa voice commands to control smart home devices. Alexa could change the color of your Philips Hue lights or adjust your Nest thermostat based on a spoken phrase, with Pipedream acting as the bridge between Alexa and the device APIs. + +- **Daily Briefing Email Digest**: Use Alexa to trigger a Pipedream workflow that compiles your essential daily information, like calendar events, weather updates, and news headlines, then sends it to your email. Start your day with a custom briefing delivered to your inbox, simply by asking Alexa for your daily summary. diff --git a/components/amazon_polly/README.md b/components/amazon_polly/README.md new file mode 100644 index 0000000000000..249b7a2d87095 --- /dev/null +++ b/components/amazon_polly/README.md @@ -0,0 +1,11 @@ +# Overview + +The Amazon Polly API lets you convert text into lifelike speech using deep learning. With Polly, you can create applications that talk and build entirely new categories of speech-enabled products. Pipedream's platform enables you to integrate Polly's capabilities into workflows that can automate tasks, like generating audio files from blog posts or alert messages, and piping them to various services or storage solutions. + +# Example Use Cases + +- **Blog Post to Podcast**: Automatically convert new blog posts to audio. When a new post is published to your CMS (like WordPress), Pipedream can trigger a workflow that sends the text to Amazon Polly, which then returns an audio stream. The audio file is then uploaded to a podcast hosting platform or cloud storage for distribution. + +- **Real-time Alerts to Speech Notifications**: Turn alerts from monitoring tools like Datadog into spoken notifications. When an alert is triggered, it's sent to Pipedream, where Polly generates an audio message. This message could be played over a PA system or sent as a voice notification to a team's phones. + +- **Language Learning App**: Enhance a language-learning platform by providing users with pronunciation examples. As users submit text, it goes through Pipedream to Polly, which translates the text into spoken words in the chosen language. The audio can then be embedded back into the app for the users to listen to. diff --git a/components/amazon_polly/amazon_polly.app.mjs b/components/amazon_polly/amazon_polly.app.mjs new file mode 100644 index 0000000000000..53c57b9f9dbdd --- /dev/null +++ b/components/amazon_polly/amazon_polly.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "amazon_polly", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/amazon_polly/package.json b/components/amazon_polly/package.json new file mode 100644 index 0000000000000..7c3b22d577a58 --- /dev/null +++ b/components/amazon_polly/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/amazon_polly", + "version": "0.0.1", + "description": "Pipedream Amazon Polly Components", + "main": "amazon_polly.app.mjs", + "keywords": [ + "pipedream", + "amazon_polly" + ], + "homepage": "https://pipedream.com/apps/amazon_polly", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/amazon_selling_partner/amazon_selling_partner.app.mjs b/components/amazon_selling_partner/amazon_selling_partner.app.mjs new file mode 100644 index 0000000000000..d0cea946e286b --- /dev/null +++ b/components/amazon_selling_partner/amazon_selling_partner.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "amazon_selling_partner", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/amazon_selling_partner/package.json b/components/amazon_selling_partner/package.json new file mode 100644 index 0000000000000..ddab104783ce3 --- /dev/null +++ b/components/amazon_selling_partner/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/amazon_selling_partner", + "version": "0.0.1", + "description": "Pipedream Amazon Selling Partner Components", + "main": "amazon_selling_partner.app.mjs", + "keywords": [ + "pipedream", + "amazon_selling_partner" + ], + "homepage": "https://pipedream.com/apps/amazon_selling_partner", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/amazon_ses/README.md b/components/amazon_ses/README.md index 5e6680db2ff15..52ae3ad955466 100644 --- a/components/amazon_ses/README.md +++ b/components/amazon_ses/README.md @@ -1,15 +1,11 @@ # Overview -With Amazon SES, you can easily send email from your applications. Amazon SES -eliminates the complexity and headaches of managing outgoing email -infrastructure by providing a scalable, pay-as-you-go email sending service. -Amazon SES makes it easy to get started sending email from your applications, -with no upfront costs or long-term commitments. - -Here are some examples of what you can build with the Amazon SES API: - -- A system that automatically sends emails to customers when their orders are - ready -- A system that notifies customers when their bills are due -- A system that sends out weekly newsletters to subscribers -- A system that sends out targeted marketing emails to potential customers +Amazon Simple Email Service (SES) is a powerful cloud-based email sending service designed to help digital marketers and application developers send marketing, notification, and transactional emails. With the SES API, you can reliably send emails at scale, manage sender reputations, view email sending statistics, and maintain a high deliverability rate. Leveraging Pipedream's capabilities, you can integrate SES seamlessly into serverless workflows, automate email responses based on triggers from other apps, and analyze the effectiveness of your email campaigns by connecting to data analytics platforms. + +# Example Use Cases + +- **Automated Customer Onboarding Emails**: Trigger an SES email sequence when a new user signs up via a web app (like Shopify). The workflow could send a welcome email, followed by onboarding steps, and eventually gather feedback, all automated based on user activity. + +- **Real-time Email Analytics Dashboard**: Connect SES with a data analytics app (such as Google Sheets) on Pipedream to track email open rates, click-through rates, and bounces. Each time SES sends an email, Pipedream can log the relevant data to a Google Sheet to create a real-time analytics dashboard. + +- **Support Ticketing System Integration**: When a new support ticket is created in a tool like Zendesk, use SES through Pipedream to automatically send an acknowledgment email to the customer, and set up subsequent follow-up reminders, escalating unresolved tickets to senior staff. diff --git a/components/ambee/ambee.app.mjs b/components/ambee/ambee.app.mjs new file mode 100644 index 0000000000000..6d294f1c7e090 --- /dev/null +++ b/components/ambee/ambee.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "ambee", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/ambee/package.json b/components/ambee/package.json new file mode 100644 index 0000000000000..2d4d322f0fed6 --- /dev/null +++ b/components/ambee/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/ambee", + "version": "0.0.1", + "description": "Pipedream Ambee Components", + "main": "ambee.app.mjs", + "keywords": [ + "pipedream", + "ambee" + ], + "homepage": "https://pipedream.com/apps/ambee", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/ambient_weather/README.md b/components/ambient_weather/README.md index 9f0a71b1aa609..1386aa4080c1a 100644 --- a/components/ambient_weather/README.md +++ b/components/ambient_weather/README.md @@ -1,12 +1,11 @@ # Overview -With the Ambient Weather API, you can build applications that track and display -current weather conditions, as well as forecast data. Here are some examples of -what you can build: - -- A weather tracker that shows current conditions and forecast data for your - location -- A weather widget that can be embedded on other websites -- A mobile app that shows the current weather and forecast for your location -- A desktop application that shows the current weather and forecast for your - location +The Ambient Weather API provides real-time access to personal weather station data, enabling you to monitor and analyze environmental conditions. Through Pipedream's integration, you can automate actions based on this data, such as triggering alerts when specific weather thresholds are reached, logging climate trends for analysis, or controlling smart home devices to adapt to changing conditions. + +# Example Use Cases + +- **Weather-Triggered Notifications**: Set up a workflow that monitors temperature and humidity levels from your Ambient Weather station. When certain weather conditions are met, like a sudden drop in temperature, automatically send a notification via email, SMS, or messaging platforms like Slack to alert you or a group of people. + +- **Smart Home Climate Control**: Integrate your weather station data with smart home apps like Nest or Philips Hue on Pipedream. Create a workflow that adjusts your home's heating or lighting based on real-time outdoor conditions, such as dimming lights as the sun sets or increasing the temperature when the outdoor temperature falls below a comfortable level. + +- **Agricultural Monitoring**: For agricultural applications, use the Ambient Weather API to keep tabs on weather conditions affecting crop health. Automate the collection of data into a Google Sheets spreadsheet for trend analysis and predictive insights. Additionally, set up a system to automatically water crops when the weather station reports insufficient rainfall. diff --git a/components/ambivo/README.md b/components/ambivo/README.md new file mode 100644 index 0000000000000..88124e930a1f5 --- /dev/null +++ b/components/ambivo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Ambivo API provides access to a range of services for web scraping, image processing, and data extraction. On Pipedream, you can leverage these capabilities to automate workflows that require the transformation and analysis of content from various sources. Whether you're looking to extract text from images, scrape web data without getting blocked, or perform content-based image retrieval, Ambivo can be integrated with Pipedream's serverless platform to build robust and scalable automations. + +# Example Use Cases + +- **Content Change Detection and Notification**: Set up a Pipedream workflow that periodically checks a website using Ambivo's web scraping capabilities. If new content is detected, trigger an alert or notification through email, Slack, or another communication platform connected to Pipedream. + +- **Image-Based Search Engine**: Create a workflow that allows users to upload an image, then use Ambivo's content-based image retrieval to find similar images from a dataset or across the web. The results can be stored in a Pipedream data store or sent to a database of your choice for further processing or indexing. + +- **Automated Image Processing Pipeline**: Develop a pipeline in Pipedream that takes images uploaded to a cloud storage platform, processes them with Ambivo's image processing services to extract text or analyze content, and then routes the processed data to other services or apps for additional workflows like translation, sentiment analysis, or database entry. diff --git a/components/ambivo/actions/get-contacts/get-contacts.mjs b/components/ambivo/actions/get-contacts/get-contacts.mjs new file mode 100644 index 0000000000000..c59926dbf6275 --- /dev/null +++ b/components/ambivo/actions/get-contacts/get-contacts.mjs @@ -0,0 +1,21 @@ +import ambivo from "../../ambivo.app.mjs"; + +export default { + key: "ambivo-get-contacts", + name: "Get Contacts", + description: "Retrieves a list of contacts in Ambivo. [See the documentation](https://fapi.ambivo.com/docs#/CRM%20Service%20Calls/get_contacts_created_crm_contacts_created_get)", + version: "0.0.1", + type: "action", + props: { + ambivo, + }, + async run({ $ }) { + const contacts = await this.ambivo.listContacts({ + $, + }); + $.export("$summary", `Successfully retrieved ${contacts.length} contact${contacts.length === 1 + ? "" + : "s"}`); + return contacts; + }, +}; diff --git a/components/ambivo/actions/get-leads/get-leads.mjs b/components/ambivo/actions/get-leads/get-leads.mjs new file mode 100644 index 0000000000000..56215d173951e --- /dev/null +++ b/components/ambivo/actions/get-leads/get-leads.mjs @@ -0,0 +1,21 @@ +import ambivo from "../../ambivo.app.mjs"; + +export default { + key: "ambivo-get-leads", + name: "Get Leads", + description: "Retrieves a list of leads in Ambivo. [See the documentation](https://fapi.ambivo.com/docs#/CRM%20Service%20Calls/get_leads_created_crm_leads_created_get)", + version: "0.0.1", + type: "action", + props: { + ambivo, + }, + async run({ $ }) { + const leads = await this.ambivo.listLeads({ + $, + }); + $.export("$summary", `Successfully retrieved ${leads.length} lead${leads.length === 1 + ? "" + : "s"}`); + return leads; + }, +}; diff --git a/components/ambivo/ambivo.app.mjs b/components/ambivo/ambivo.app.mjs index 3ac6f2f80402f..31acdf785a167 100644 --- a/components/ambivo/ambivo.app.mjs +++ b/components/ambivo/ambivo.app.mjs @@ -1,11 +1,62 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "ambivo", - propDefinitions: {}, + propDefinitions: { + contactIds: { + type: "string[]", + label: "Contact IDs", + description: "The IDs of the contacts whose status updates you want to monitor", + optional: true, + async options() { + const contacts = await this.listContacts(); + return contacts?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://fapi.ambivo.com/crm"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `Bearer ${this.$auth.access_token}`, + "Content-Type": "application/json", + "Accept": "application/json", + }, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts/created", + ...opts, + }); + }, + listContactStatusUpdated(opts = {}) { + return this._makeRequest({ + path: "/contacts/status_updated", + ...opts, + }); + }, + listLeads(opts = {}) { + return this._makeRequest({ + path: "/leads/created", + ...opts, + }); }, }, }; diff --git a/components/ambivo/package.json b/components/ambivo/package.json index fd0691d1ce426..f83137ae4c7fc 100644 --- a/components/ambivo/package.json +++ b/components/ambivo/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/ambivo", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Ambivo Components", "main": "ambivo.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" } -} \ No newline at end of file +} diff --git a/components/ambivo/sources/common/base.mjs b/components/ambivo/sources/common/base.mjs new file mode 100644 index 0000000000000..ca48149c036b9 --- /dev/null +++ b/components/ambivo/sources/common/base.mjs @@ -0,0 +1,71 @@ +import ambivo from "../../ambivo.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + ambivo, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + async processEvent(limit) { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + const resourceFn = this.getResourceFn(); + const tsField = this.getTsField(); + const results = []; + let count = 0; + + const items = await resourceFn(); console.log(items); + for (const item of items) { + const ts = Date.parse(item[tsField]); + if (ts >= lastTs && this.isRelevant(item)) { + maxTs = Math.max(ts, maxTs); + results.push(item); + count++; + if (limit && count >= limit) { + break; + } + } + } + + results.reverse().forEach((item) => this.emitEvent(item)); + this._setLastTs(maxTs); + }, + isRelevant() { + return true; + }, + emitEvent(item) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + getTsField() { + throw new Error("getTsField is not implemented"); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/ambivo/sources/contact-status-updated/contact-status-updated.mjs b/components/ambivo/sources/contact-status-updated/contact-status-updated.mjs new file mode 100644 index 0000000000000..5d36a16dbd945 --- /dev/null +++ b/components/ambivo/sources/contact-status-updated/contact-status-updated.mjs @@ -0,0 +1,42 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ambivo-contact-status-updated", + name: "Contact Status Updated", + description: "Emit new event when a contact's status has been updated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + contactIds: { + propDefinition: [ + common.props.ambivo, + "contactIds", + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.ambivo.listContactStatusUpdated; + }, + getTsField() { + return "status_updated_date"; + }, + isRelevant(contact) { + return !this.contactactIds || this.contactIds.includes(contact.id); + }, + generateMeta(contact) { + const ts = Date.parse(contact.status_updated_date); + return { + id: `${contact.id}-${ts}`, + summary: `Status Updated for Contact: ${contact.name}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/ambivo/sources/contact-status-updated/test-event.mjs b/components/ambivo/sources/contact-status-updated/test-event.mjs new file mode 100644 index 0000000000000..181fc8fc15d22 --- /dev/null +++ b/components/ambivo/sources/contact-status-updated/test-event.mjs @@ -0,0 +1,48 @@ +export default { + "id": "661eb24f44e3aebc6d23d59e", + "phone_list": [ + { + "type": "Main", + "phone": "15555555555", + "country": "US", + "phone_validation_dict": { + "result": 1, + "phone": "15555555555", + "response": { + "gender": "", + "first_name": "", + "last_name": "", + "line_provider": "N/A", + "mms_email": "", + "sms_email": "N/A", + "address": "", + "city": "N/A", + "state": "N/A", + "zip": "N/A", + "nuisance_score": 0, + "status": false, + "line_type": "Unknown", + "type": "Unknown", + "cnam": "N/A" + } + }, + "formatted_phone": "(555) 555-5555" + } + ], + "email_list": [ + { + "email": "1713287758@invalid.ambivo.com", + "type": "other", + "email_validation_dict": { + "result": 2 + } + } + ], + "first_name": "Test", + "last_name": "User", + "name": "Test User", + "status": "open", + "created_date": "2024-04-16T17:15:59Z", + "updated_date": "2024-04-16T17:15:59Z", + "status_updated_date": "2024-04-16T17:15:59Z" +} \ No newline at end of file diff --git a/components/ambivo/sources/new-contact-created/new-contact-created.mjs b/components/ambivo/sources/new-contact-created/new-contact-created.mjs new file mode 100644 index 0000000000000..0fadd6f74212c --- /dev/null +++ b/components/ambivo/sources/new-contact-created/new-contact-created.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ambivo-new-contact-created", + name: "New Contact Created", + description: "Emit new event when a new contact is created in Ambivo.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.ambivo.listContacts; + }, + getTsField() { + return "created_date"; + }, + generateMeta(contact) { + return { + id: contact.id, + summary: `New Contact: ${contact.name}`, + ts: Date.parse(contact.created_date), + }; + }, + }, + sampleEmit, +}; diff --git a/components/ambivo/sources/new-contact-created/test-event.mjs b/components/ambivo/sources/new-contact-created/test-event.mjs new file mode 100644 index 0000000000000..58927876d547a --- /dev/null +++ b/components/ambivo/sources/new-contact-created/test-event.mjs @@ -0,0 +1,23 @@ +export default { + "id": "661ea7ed6657b0528f313f6c", + "phone_list": "", + "email_list": [ + { + "type": "Main", + "email": "test@gmail.com", + "email_validation_dict": { + "result": 1, + "email_provider_list": [ + "google.com" + ] + } + } + ], + "first_name": "Test", + "last_name": "User", + "name": "Test User", + "status": "meeting-setup", + "created_date": "2024-04-16T16:31:41Z", + "updated_date": "2024-04-16T16:33:04Z", + "status_updated_date": "2024-04-16T16:33:04Z" +} \ No newline at end of file diff --git a/components/ambivo/sources/new-lead-created/new-lead-created.mjs b/components/ambivo/sources/new-lead-created/new-lead-created.mjs new file mode 100644 index 0000000000000..81242f90bcce7 --- /dev/null +++ b/components/ambivo/sources/new-lead-created/new-lead-created.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ambivo-new-lead-created", + name: "New Lead Created", + description: "Emit new event when a new lead is created in Ambivo CRM.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.ambivo.listLeads; + }, + getTsField() { + return "created_date"; + }, + generateMeta(lead) { + return { + id: lead.id, + summary: `New Lead: ${lead.name}`, + ts: Date.parse(lead.created_date), + }; + }, + }, + sampleEmit, +}; diff --git a/components/ambivo/sources/new-lead-created/test-event.mjs b/components/ambivo/sources/new-lead-created/test-event.mjs new file mode 100644 index 0000000000000..8b97ab60afd1c --- /dev/null +++ b/components/ambivo/sources/new-lead-created/test-event.mjs @@ -0,0 +1,48 @@ +export default { + "id": "661eb9de2484e03d8a934181", + "phone_list": [ + { + "type": "Main", + "phone": "15555555555", + "country": "US", + "phone_validation_dict": { + "result": 1, + "phone": "15555555555", + "response": { + "gender": "", + "first_name": "", + "last_name": "", + "line_provider": "N/A", + "mms_email": "", + "sms_email": "N/A", + "address": "", + "city": "N/A", + "state": "N/A", + "zip": "N/A", + "nuisance_score": 0, + "status": false, + "line_type": "Unknown", + "type": "Unknown", + "cnam": "N/A" + } + }, + "formatted_phone": "(555) 555-5555" + } + ], + "email_list": [ + { + "email": "1713289694@invalid.ambivo.com", + "type": "other", + "email_validation_dict": { + "result": 2 + } + } + ], + "first_name": "Test", + "last_name": "User", + "name": "Test User", + "lead_status": "open", + "created_date": "2024-04-16T17:48:14Z", + "updated_date": "2024-04-16T17:48:14Z", + "status_updated_date": "2024-04-16T17:48:14Z" +} \ No newline at end of file diff --git a/components/amcards/README.md b/components/amcards/README.md index a0472e61d8e4d..89b2ddc8eaac2 100644 --- a/components/amcards/README.md +++ b/components/amcards/README.md @@ -1,14 +1,11 @@ # Overview -I can build a Markdown document with a few paragraphs, followed by a list of -examples, telling me what I can build using the [AMcards -API](https://amcards.com/>). - -Some things I can build using the AMcards API include: - -- A card that displays a user's name, location, and website -- A card that links to a user's social media profiles -- A card that displays a user's email address and phone number -- A card that links to a user's resume or CV -- A card that displays a user's skills and experience -- A card that links to a user's portfolio +AMcards API allows you to automate the process of sending physical greeting cards. This can be a powerful tool for businesses looking to add a personal touch to their customer relationship management. Through Pipedream, you can harness this API to create cards based on triggers from a myriad of apps, streamlining the process of connecting with customers, employees, or stakeholders on special occasions, milestones, or as a token of appreciation. + +# Example Use Cases + +- **Customer Birthday Celebration Workflow**: When a customer's birthday is approaching, as noted in your CRM system, Pipedream can automatically trigger an AMcards API call to send a personalized birthday card. This gesture can help boost customer loyalty and engagement. + +- **Employee Anniversary Acknowledgement Workflow**: Using employee data from an HR platform, Pipedream can initiate the sending of work anniversary cards. It listens for work anniversary dates and triggers AMcards to mail out customized anniversary cards to employees, enhancing company culture. + +- **E-commerce Order Follow-up Workflow**: After a customer places an order on your e-commerce platform, Pipedream can orchestrate a thank-you card dispatch via AMcards. This post-purchase engagement can increase the likelihood of repeat business and positive word-of-mouth. diff --git a/components/amentum_aerospace/README.md b/components/amentum_aerospace/README.md index 3665959c09c5b..2262f5beb0009 100644 --- a/components/amentum_aerospace/README.md +++ b/components/amentum_aerospace/README.md @@ -1,10 +1,11 @@ # Overview -The Amentum Aerospace API gives you access to a wide range of data and -information about the aerospace industry. With this API you can: - -- Retrieve data about aircraft, airports, airlines, and more -- Search for aircraft by manufacturer, model, or other criteria -- Get information about flight routes and schedules -- Access weather data for specific locations -- And much more! +The Amentum Aerospace API provides programmatic access to a suite of aerospace functionalities, such as satellite tracking, pass predictions, and atmospheric conditions. Leveraging this API, one can automate data collection for space-related projects, enhance satellite monitoring applications, or enrich educational platforms with real-time space data. Pipedream's serverless integration platform allows developers to connect the Amentum API with countless other services, creating custom workflows that can trigger actions, send notifications, or store data based on space events or conditions. + +# Example Use Cases + +- **Satellite Pass Alert System**: Integrate Amentum Aerospace with communication apps like Twilio or Slack on Pipedream. Set up a workflow that monitors the predicted passes of satellites over a specific location, triggering an alert to a subscribed user's phone or a designated Slack channel when a satellite is about to pass overhead. + +- **Educational Dashboard Update**: Link Amentum Aerospace with a CMS like WordPress or a database like Airtable using Pipedream. Automatically update an educational website or a database with the latest satellite positions and atmospheric data, providing students and researchers with updated information for their projects or studies. + +- **Weather Impact Analysis**: Combine Amentum Aerospace with weather services like OpenWeatherMap on Pipedream. Create a workflow that correlates atmospheric conditions provided by Amentum with local weather data, analyzing the potential impact on satellite operations or ground-based observatories and sending detailed reports to stakeholders. diff --git a/components/americommerce/actions/change-order-status/change-order-status.mjs b/components/americommerce/actions/change-order-status/change-order-status.mjs new file mode 100644 index 0000000000000..23acbf3b93314 --- /dev/null +++ b/components/americommerce/actions/change-order-status/change-order-status.mjs @@ -0,0 +1,132 @@ +import app from "../../americommerce.app.mjs"; + +export default { + key: "americommerce-change-order-status", + name: "Change Order Status", + description: "Changes the status of an existing order. [See the documentation](https://developers.cart.com/docs/rest-api/6898d9f254dfb-update-an-order-status).", + version: "0.0.1", + type: "action", + props: { + app, + orderStatusId: { + optional: false, + propDefinition: [ + app, + "orderStatusId", + ], + }, + name: { + type: "string", + label: "Name", + description: "The name of the order status.", + optional: true, + }, + isOpen: { + type: "boolean", + label: "Is Open", + description: "Indicates whether the order status is open.", + optional: true, + }, + isDeclined: { + type: "boolean", + label: "Is Declined", + description: "Indicates whether the order status is declined.", + optional: true, + }, + isCancelled: { + type: "boolean", + label: "Is Cancelled", + description: "Indicates whether the order status is cancelled.", + optional: true, + }, + isShipped: { + type: "boolean", + label: "Is Shipped", + description: "Indicates whether the order status is shipped.", + optional: true, + }, + color: { + type: "string", + label: "Color", + description: "The color of the order status.", + optional: true, + }, + emailTemplateId: { + propDefinition: [ + app, + "emailTemplateId", + ], + }, + isFullyRefunded: { + type: "boolean", + label: "Is Fully Refunded", + description: "Indicates whether the order status is fully refunded.", + optional: true, + }, + isPartiallyRefunded: { + type: "boolean", + label: "Is Partially Refunded", + description: "Indicates whether the order status is partially refunded.", + optional: true, + }, + isQuoteStatus: { + type: "boolean", + label: "Is Quote Status", + description: "Indicates whether the order status is a quote status.", + optional: true, + }, + isPartiallyShipped: { + type: "boolean", + label: "Is Partially Shipped", + description: "Indicates whether the order status is partially shipped.", + optional: true, + }, + }, + methods: { + changeOrderStatus({ + orderStatusId, ...args + } = {}) { + return this.app.put({ + path: `/order_statuses/${orderStatusId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + changeOrderStatus, + orderStatusId, + name, + isOpen, + isDeclined, + isCancelled, + isShipped, + color, + emailTemplateId, + isFullyRefunded, + isPartiallyRefunded, + isQuoteStatus, + isPartiallyShipped, + } = this; + + const response = await changeOrderStatus({ + $, + orderStatusId, + data: { + name, + is_open: isOpen, + is_declined: isDeclined, + is_cancelled: isCancelled, + is_shipped: isShipped, + color, + email_template_id: emailTemplateId, + is_fully_refunded: isFullyRefunded, + is_partially_refunded: isPartiallyRefunded, + is_quote_status: isQuoteStatus, + is_partially_shipped: isPartiallyShipped, + }, + }); + $.export("$summary", `Successfully changed the order status with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/americommerce/actions/create-update-order/create-update-order.mjs b/components/americommerce/actions/create-update-order/create-update-order.mjs new file mode 100644 index 0000000000000..461773224d914 --- /dev/null +++ b/components/americommerce/actions/create-update-order/create-update-order.mjs @@ -0,0 +1,431 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../americommerce.app.mjs"; + +export default { + key: "americommerce-create-update-order", + name: "Create Or Update Order", + description: "Creates a new order or updates an existing one. [See the documentation here](https://developers.cart.com/docs/rest-api/3f2ab73188031-create-an-order) and [here](https://developers.cart.com/docs/rest-api/e2649bb3adba9-update-an-order).", + version: "0.0.1", + type: "action", + props: { + app, + orderId: { + propDefinition: [ + app, + "orderId", + ], + }, + customerId: { + optional: true, + propDefinition: [ + app, + "customerId", + ], + }, + orderStatusId: { + propDefinition: [ + app, + "orderStatusId", + ], + }, + orderShippingAddressId: { + label: "Order Shipping Address ID", + description: "The ID of the order's shipping address.", + propDefinition: [ + app, + "orderAddressId", + ], + }, + orderBillingAddressId: { + label: "Order Billing Address ID", + description: "The ID of the order's billing address.", + propDefinition: [ + app, + "orderAddressId", + ], + }, + customerTypeId: { + propDefinition: [ + app, + "customerTypeId", + ], + }, + specialInstructions: { + type: "string", + label: "Special Instructions", + description: "Special instructions for the order.", + optional: true, + }, + subtotal: { + type: "string", + label: "Subtotal", + description: "The subtotal of the order.", + optional: true, + }, + taxTotal: { + type: "string", + label: "Tax Total", + description: "The total tax for the order or tax added to the order.", + optional: true, + }, + shippingTotal: { + type: "string", + label: "Shipping Total", + description: "The total shipping cost for the order or shipping added to the order.", + optional: true, + }, + discountTotal: { + type: "string", + label: "Discount Total", + description: "The total discount for the order.", + optional: true, + }, + grandTotal: { + type: "string", + label: "Grand Total", + description: "The grand total of the order.", + optional: true, + }, + costTotal: { + type: "string", + label: "Cost Total", + description: "The total cost of the order.", + optional: true, + }, + selectedShippingMethod: { + label: "Selected Shipping Method", + description: "The selected shipping method for the order.", + propDefinition: [ + app, + "shippingMethodId", + () => ({ + mapper: ({ + shipping_method_id: value, + shipping_method: label, + }) => ({ + label, + value: String(value), + }), + }), + ], + }, + referrer: { + type: "string", + label: "Referrer", + description: "The referrer for the order.", + optional: true, + }, + adminComments: { + type: "string", + label: "Admin Comments", + description: "Comments from the admin about the order.", + optional: true, + }, + source: { + type: "string", + label: "Source", + description: "The source of the order.", + optional: true, + }, + searchPhrase: { + type: "string", + label: "Search Phrase", + description: "The search phrase for the order.", + optional: true, + }, + storeId: { + propDefinition: [ + app, + "storeId", + ], + }, + isPpc: { + type: "boolean", + label: "Is PPC", + description: "Whether the order is a PPC order.", + optional: true, + }, + ppcKeyword: { + type: "string", + label: "PPC Keyword", + description: "The PPC keyword for the order.", + optional: true, + }, + handlingTotal: { + type: "string", + label: "Handling Total", + description: "The handling total for the order.", + optional: true, + }, + isPaymentOrderOnly: { + type: "boolean", + label: "Is Payment Order Only", + description: "Whether the order is a payment order only.", + optional: true, + }, + selectedShippingProviderService: { + type: "string", + label: "Selected Shipping Provider Service", + description: "The selected shipping provider service for the order.", + optional: true, + }, + additionalFees: { + type: "string", + label: "Additional Fees", + description: "Additional fees for the order.", + optional: true, + }, + adcodeId: { + propDefinition: [ + app, + "adcodeId", + ], + }, + isGift: { + type: "boolean", + label: "Is Gift", + description: "Whether the order is a gift.", + optional: true, + }, + giftMessage: { + type: "string", + label: "Gift Message", + description: "The gift message for the order.", + optional: true, + }, + publicComments: { + type: "string", + label: "Public Comments", + description: "Public comments about the order.", + optional: true, + }, + instructions: { + type: "string", + label: "Instructions", + description: "Instructions for the order.", + optional: true, + }, + sourceGroup: { + type: "string", + label: "Source Group", + description: "The source group for the order.", + optional: true, + }, + fromSubscriptionId: { + label: "From Subscription ID", + description: "The ID of the subscription.", + propDefinition: [ + app, + "subscriptionId", + ], + }, + discountedShippingTotal: { + type: "string", + label: "Discounted Shipping Total", + description: "The discounted shipping total for the order.", + optional: true, + }, + orderNumber: { + type: "string", + label: "Order Number", + description: "The order number.", + optional: true, + }, + couponCode: { + type: "string", + label: "Coupon Code", + description: "The coupon code for the order.", + optional: true, + }, + orderType: { + type: "string", + label: "Order Type", + description: "The type of order.", + optional: true, + }, + expires: { + type: "boolean", + label: "Expires", + description: "Whether the order expires.", + optional: true, + }, + campaignCode: { + type: "string", + label: "Campaign Code", + description: "The campaign code for the order.", + optional: true, + }, + rewardPointsCredited: { + type: "boolean", + label: "Reward Points Credited", + description: "Whether the reward points are credited.", + optional: true, + }, + channel: { + type: "string", + label: "Channel", + description: "The channel for the order.", + optional: true, + options: [ + "Online", + "InStore", + "Chat", + "Phone", + "Email", + ], + }, + device: { + type: "string", + label: "Device", + description: "The device for the order.", + optional: true, + }, + dueDate: { + type: "string", + label: "Due Date", + description: "The due date for the order. Eg. `2021-05-14`", + optional: true, + }, + }, + methods: { + createOrder(args = {}) { + return this.app.post({ + path: "/orders", + ...args, + }); + }, + updateOrder({ + orderId, ...args + } = {}) { + return this.app.put({ + path: `/orders/${orderId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + createOrder, + updateOrder, + orderId, + customerId, + orderStatusId, + orderShippingAddressId, + orderBillingAddressId, + customerTypeId, + specialInstructions, + subtotal, + taxTotal, + shippingTotal, + discountTotal, + grandTotal, + costTotal, + selectedShippingMethod, + referrer, + adminComments, + source, + searchPhrase, + storeId, + isPpc, + ppcKeyword, + handlingTotal, + isPaymentOrderOnly, + selectedShippingProviderService, + additionalFees, + adcodeId, + isGift, + giftMessage, + publicComments, + instructions, + sourceGroup, + fromSubscriptionId, + discountedShippingTotal, + orderNumber, + couponCode, + orderType, + expires, + campaignCode, + rewardPointsCredited, + channel, + device, + dueDate, + } = this; + + const isCreate = !orderId; + + if ( + isCreate + && ( + !customerId + || !orderStatusId + || !orderShippingAddressId + || !orderBillingAddressId + || !taxTotal + || !shippingTotal + ) + ) { + throw new ConfigurationError("The **Customer ID**, **Order Status ID**, **Order Shipping Address ID**, **Order Billing Address ID**, **Tax Total**, and **Shipping Total** are required to create a new order."); + } + + const data = { + customer_id: customerId, + order_status_id: orderStatusId, + order_shipping_address_id: orderShippingAddressId, + order_billing_address_id: orderBillingAddressId, + customer_type_id: customerTypeId, + special_instructions: specialInstructions, + subtotal, + tax_total: taxTotal, + shipping_total: shippingTotal, + discount_total: discountTotal, + grand_total: grandTotal, + cost_total: costTotal, + selected_shipping_method: selectedShippingMethod, + referrer, + admin_comments: adminComments, + source, + search_phrase: searchPhrase, + store_id: storeId, + is_ppc: isPpc, + ppc_keyword: ppcKeyword, + handling_total: handlingTotal, + is_payment_order_only: isPaymentOrderOnly, + selected_shipping_provider_service: selectedShippingProviderService, + additional_fees: additionalFees, + adcode_id: adcodeId, + is_gift: isGift, + gift_message: giftMessage, + public_comments: publicComments, + instructions, + source_group: sourceGroup, + from_subscription_id: fromSubscriptionId, + discounted_shipping_total: discountedShippingTotal, + order_number: orderNumber, + coupon_code: couponCode, + order_type: orderType, + expires, + campaign_code: campaignCode, + reward_points_credited: rewardPointsCredited, + channel, + device, + due_date: dueDate, + }; + + if (isCreate) { + const response = await createOrder({ + $, + data, + }); + $.export("$summary", `Successfully created the order with ID \`${response.id}\`.`); + return response; + } + + const response = await updateOrder({ + $, + orderId, + data, + }); + $.export("$summary", `Successfully updated the order with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/americommerce/actions/update-customer/update-customer.mjs b/components/americommerce/actions/update-customer/update-customer.mjs new file mode 100644 index 0000000000000..9157ccab10b85 --- /dev/null +++ b/components/americommerce/actions/update-customer/update-customer.mjs @@ -0,0 +1,252 @@ +import app from "../../americommerce.app.mjs"; + +export default { + key: "americommerce-update-customer", + name: "Update Customer", + description: "Updates the details of a registered customer. [See the documentation](https://developers.cart.com/docs/rest-api/5da2f1ddbe199-update-a-customer).", + version: "0.0.1", + type: "action", + props: { + app, + customerId: { + propDefinition: [ + app, + "customerId", + ], + }, + customerNumber: { + type: "string", + label: "Customer Number", + description: "The customer number for the customer.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the customer.", + optional: true, + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the customer.", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "The email address of the customer.", + optional: true, + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number of the customer.", + optional: true, + }, + adcodeId: { + propDefinition: [ + app, + "adcodeId", + ], + }, + customerTypeId: { + propDefinition: [ + app, + "customerTypeId", + ], + }, + isNoTaxCustomer: { + type: "boolean", + label: "Is No Tax Customer", + description: "Whether the customer is a no-tax customer.", + optional: true, + }, + comments: { + type: "string", + label: "Comments", + description: "Comments about the customer.", + optional: true, + }, + storeId: { + propDefinition: [ + app, + "storeId", + ], + }, + source: { + type: "string", + label: "Source", + description: "The source of the customer.", + optional: true, + }, + searchString: { + type: "string", + label: "Search String", + description: "The search string for the customer.", + optional: true, + }, + noAccount: { + type: "boolean", + label: "No Account", + description: "Indicates whether the customer does not have an account.", + optional: true, + }, + alternatePhoneNumber: { + type: "string", + label: "Alternate Phone Number", + description: "The alternate phone number for the customer.", + optional: true, + }, + isAffiliateCustomer: { + type: "boolean", + label: "Is Affiliate Customer", + description: "Whether the customer is an affiliate customer.", + optional: true, + }, + username: { + type: "string", + label: "Username", + description: "The username for the customer.", + optional: true, + }, + isContactInformationOnly: { + type: "boolean", + label: "Is Contact Information Only", + description: "Whether the customer is contact information only.", + optional: true, + }, + taxExemptionNumber: { + type: "string", + label: "Tax Exemption Number", + description: "The tax exemption number for the customer.", + optional: true, + }, + company: { + type: "string", + label: "Company", + description: "The company for the customer.", + optional: true, + }, + taxRate: { + type: "string", + label: "Tax Rate", + description: "The tax rate for the customer.", + optional: true, + }, + lockDefaultAddress: { + type: "boolean", + label: "Lock Default Address", + description: "Whether the default address is locked.", + optional: true, + }, + paymentNetTerm: { + type: "integer", + label: "Payment Net Term", + description: "The payment net term for the customer.", + optional: true, + }, + creditLimit: { + type: "string", + label: "Credit Limit", + description: "The credit limit for the customer.", + optional: true, + }, + isInactive: { + type: "boolean", + label: "Is Inactive", + description: "Whether the customer is inactive.", + optional: true, + }, + useSharedCreditLimit: { + type: "boolean", + label: "Use Shared Credit Limit", + description: "Whether the shared credit limit is used.", + optional: true, + }, + overrideSharedCreditLimit: { + type: "boolean", + label: "Override Shared Credit Limit", + description: "Whether the shared credit limit is overridden.", + optional: true, + }, + }, + methods: { + updateCustomer({ + customerId, ...args + } = {}) { + return this.app.put({ + path: `/customers/${customerId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + updateCustomer, + customerId, + customerNumber, + lastName, + firstName, + email, + phoneNumber, + adcodeId, + customerTypeId, + isNoTaxCustomer, + comments, + storeId, + source, + searchString, + noAccount, + alternatePhoneNumber, + isAffiliateCustomer, + username, + isContactInformationOnly, + taxExemptionNumber, + company, + taxRate, + lockDefaultAddress, + paymentNetTerm, + creditLimit, + isInactive, + useSharedCreditLimit, + overrideSharedCreditLimit, + } = this; + + const response = await updateCustomer({ + $, + customerId, + data: { + customer_number: customerNumber, + last_name: lastName, + first_name: firstName, + email, + phone_number: phoneNumber, + adcode_id: adcodeId, + customer_type_id: customerTypeId, + is_no_tax_customer: isNoTaxCustomer, + comments, + store_id: storeId, + source, + search_string: searchString, + no_account: noAccount, + alternate_phone_number: alternatePhoneNumber, + is_affiliate_customer: isAffiliateCustomer, + username, + is_contact_information_only: isContactInformationOnly, + tax_exemption_number: taxExemptionNumber, + company, + tax_rate: taxRate, + lock_default_address: lockDefaultAddress, + payment_net_term: paymentNetTerm, + credit_limit: creditLimit, + is_inactive: isInactive, + use_shared_credit_limit: useSharedCreditLimit, + override_shared_credit_limit: overrideSharedCreditLimit, + }, + }); + + $.export("$summary", `Successfully updated customer with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/americommerce/americommerce.app.mjs b/components/americommerce/americommerce.app.mjs new file mode 100644 index 0000000000000..b10102186aeb9 --- /dev/null +++ b/components/americommerce/americommerce.app.mjs @@ -0,0 +1,253 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "americommerce", + propDefinitions: { + customerId: { + type: "string", + label: "Customer ID", + description: "The unique identifier for the customer. This ID is optional.", + async options({ filter = () => true }) { + const { customers } = await this.getCustomers(); + return customers + .filter(filter) + .map(({ + first_name: firstName, last_name: lastName, id: value, + }) => ({ + label: [ + firstName, + lastName, + ].filter(Boolean).join(" "), + value, + })); + }, + }, + adcodeId: { + type: "string", + label: "Adcode ID", + description: "The adcode ID for the customer.", + optional: true, + async options() { + const { adcodes } = await this.getAdCodes(); + return adcodes.map(({ + name: label, id: value, + }) => ({ + label, + value: String(value), + })); + }, + }, + customerTypeId: { + type: "string", + label: "Customer Type ID", + description: "The customer type ID for the customer.", + optional: true, + async options() { + const { customer_types: customerTypes } = await this.getCustomerTypes(); + return customerTypes.map(({ + name: label, id: value, + }) => ({ + label, + value: String(value), + })); + }, + }, + storeId: { + type: "string", + label: "Store ID", + description: "The ID of the store.", + optional: true, + async options() { + const { stores } = await this.getStores(); + return stores.map(({ + name: label, id: value, + }) => ({ + label, + value: String(value), + })); + }, + }, + orderId: { + type: "string", + label: "Order ID", + description: "The unique identifier for the order. Required for updating or changing the status of an order.", + optional: true, + async options() { + const { orders } = await this.getOrders(); + return orders.map(({ + id, order_number: orderNumber, + }) => ({ + label: `Order #${orderNumber}`, + value: String(id), + })); + }, + }, + orderStatusId: { + type: "string", + label: "Order Status ID", + description: "The ID of the order status.", + optional: true, + async options() { + const { order_statuses: orderStatuses } = await this.getOrderStatuses(); + return orderStatuses.map(({ + name: label, id: value, + }) => ({ + label, + value: String(value), + })); + }, + }, + orderAddressId: { + type: "string", + label: "Order Address ID", + description: "The ID of the order's address.", + optional: true, + async options() { + const { addresses } = await this.getOrderAddresses(); + return addresses.map(({ + id, address_line_1: label, + }) => ({ + label, + value: String(id), + })); + }, + }, + shippingMethodId: { + type: "string", + label: "Shipping Method ID", + description: "The ID of the shipping method.", + optional: true, + async options({ mapper = ({ id }) => String(id) }) { + const { shipments } = await this.getOrderShipments(); + return shipments.map(mapper); + }, + }, + subscriptionId: { + type: "string", + label: "Subscription ID", + description: "The ID of the subscription.", + optional: true, + async options() { + const { subscriptions } = await this.getSubscriptions(); + return subscriptions.map(({ id }) => String(id)); + }, + }, + emailTemplateId: { + type: "string", + label: "Email Template ID", + description: "The ID of the email template.", + optional: true, + async options() { + const { email_templates: emailTemplates } = await this.getEmailTemplates(); + return emailTemplates.map(({ + type: label, id: value, + }) => ({ + label, + value: String(value), + })); + }, + }, + }, + methods: { + getUrl(path) { + const baseUrl = constants.BASE_URL + .replace(constants.SUBDOMAIN_PLACEHOLDER, this.$auth.subdomain); + return `${baseUrl}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "X-AC-Auth-Token": this.$auth.api_token, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + ...args, + method: "POST", + }); + }, + put(args = {}) { + return this._makeRequest({ + ...args, + method: "PUT", + }); + }, + delete(args = {}) { + return this._makeRequest({ + ...args, + method: "DELETE", + }); + }, + getCustomers(args = {}) { + return this._makeRequest({ + ...args, + path: "/customers", + }); + }, + getAdCodes(args = {}) { + return this._makeRequest({ + ...args, + path: "/adcodes", + }); + }, + getCustomerTypes(args = {}) { + return this._makeRequest({ + ...args, + path: "/customer_types", + }); + }, + getStores(args = {}) { + return this._makeRequest({ + ...args, + path: "/stores", + }); + }, + getOrders(args = {}) { + return this._makeRequest({ + ...args, + path: "/orders", + }); + }, + getOrderStatuses(args = {}) { + return this._makeRequest({ + ...args, + path: "/order_statuses", + }); + }, + getOrderAddresses(args = {}) { + return this._makeRequest({ + ...args, + path: "/order_addresses", + }); + }, + getOrderShipments(args = {}) { + return this._makeRequest({ + ...args, + path: "/order_shipments", + }); + }, + getSubscriptions(args = {}) { + return this._makeRequest({ + ...args, + path: "/subscriptions", + }); + }, + getEmailTemplates(args = {}) { + return this._makeRequest({ + ...args, + path: "/email_templates", + }); + }, + }, +}; diff --git a/components/americommerce/common/constants.mjs b/components/americommerce/common/constants.mjs new file mode 100644 index 0000000000000..c00ef6f39b068 --- /dev/null +++ b/components/americommerce/common/constants.mjs @@ -0,0 +1,11 @@ +const SUBDOMAIN_PLACEHOLDER = "{subdomain}"; +const BASE_URL = `https://${SUBDOMAIN_PLACEHOLDER}.americommerce.com`; +const VERSION_PATH = "/api/v1"; +const WEBHOOK_ID = "webhookId"; + +export default { + SUBDOMAIN_PLACEHOLDER, + BASE_URL, + VERSION_PATH, + WEBHOOK_ID, +}; diff --git a/components/americommerce/package.json b/components/americommerce/package.json new file mode 100644 index 0000000000000..ed22f1a8a124a --- /dev/null +++ b/components/americommerce/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/americommerce", + "version": "0.1.0", + "description": "Pipedream AmeriCommerce Components", + "main": "americommerce.app.mjs", + "keywords": [ + "pipedream", + "americommerce" + ], + "homepage": "https://pipedream.com/apps/americommerce", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" + } +} diff --git a/components/americommerce/sources/common/events.mjs b/components/americommerce/sources/common/events.mjs new file mode 100644 index 0000000000000..f56051158ea2f --- /dev/null +++ b/components/americommerce/sources/common/events.mjs @@ -0,0 +1,32 @@ +export default { + AUTHORIZING_PAYMENT: "AuthorizingPayment", + CAPTURING_PAYMENT: "CapturingPayment", + CUSTOMER_CREATED: "CustomerCreated", + CUSTOMER_EMAIL_UPDATED: "CustomerEmailUpdated", + CUSTOMER_UPDATED: "CustomerUpdated", + CUSTOMER_REGISTERED: "CustomerRegistered", + GET_DISCOUNT: "GetDiscount", + GET_PRICE: "GetPrice", + GET_PRODUCT_STATUS: "GetProductStatus", + GET_TAX: "GetTax", + NEW_ORDER: "NewOrder", + NEW_QUOTE: "NewQuote", + NEW_VALID_ORDER: "NewValidOrder", + ORDER_APPROVED: "OrderApproved", + ORDER_CANCELED: "OrderCanceled", + ORDER_DECLINED: "OrderDeclined", + ORDER_PLACED: "OrderPlaced", + ORDER_SHIPPED: "OrderShipped", + ORDER_STATUS_CHANGED: "OrderStatusChanged", + ORDER_UPDATED: "OrderUpdated", + PAYMENT_PROCESSED: "PaymentProcessed", + PRODUCT_CREATED: "ProductCreated", + PRODUCT_STATUS_CHANGED: "ProductStatusChanged", + PRODUCT_UPDATED: "ProductUpdated", + QUOTE_STATUS_CHANGED: "QuoteStatusChanged", + QUOTE_UPDATED: "QuoteUpdated", + VALIDATE_CART: "ValidateCart", + VALIDATE_CHECKOUT: "ValidateCheckout", + VALIDATE_CUSTOMER: "ValidateCustomer", + VALIDATE_PRODUCT: "ValidateProduct", +}; diff --git a/components/americommerce/sources/common/webhook.mjs b/components/americommerce/sources/common/webhook.mjs new file mode 100644 index 0000000000000..1eb548fc8dc00 --- /dev/null +++ b/components/americommerce/sources/common/webhook.mjs @@ -0,0 +1,90 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../americommerce.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + storeId: { + optional: false, + propDefinition: [ + app, + "storeId", + ], + }, + }, + hooks: { + async activate() { + const { + createWebhook, + setWebhookId, + http: { endpoint: url }, + getEventType, + storeId, + } = this; + const response = + await createWebhook({ + data: { + url, + event_type: getEventType(), + store_id: storeId, + }, + }); + + setWebhookId(response.id); + }, + async deactivate() { + const webhookId = this.getWebhookId(); + if (webhookId) { + await this.deleteWebhook({ + webhookId, + }); + } + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + setWebhookId(value) { + this.db.set(constants.WEBHOOK_ID, value); + }, + getWebhookId() { + return this.db.get(constants.WEBHOOK_ID); + }, + getEventType() { + throw new ConfigurationError("getEventType is not implemented"); + }, + processResource(resource) { + this.$emit(resource, this.generateMeta(resource)); + }, + createWebhook(args = {}) { + return this.app.post({ + debug: true, + path: "/webhooks", + ...args, + }); + }, + deleteWebhook({ + webhookId, ...args + } = {}) { + return this.app.delete({ + debug: true, + path: `/webhooks/${webhookId}`, + ...args, + }); + }, + }, + async run({ body }) { + this.http.respond({ + status: 200, + }); + + this.processResource(body); + }, +}; diff --git a/components/americommerce/sources/new-customer-instant/new-customer-instant.mjs b/components/americommerce/sources/new-customer-instant/new-customer-instant.mjs new file mode 100644 index 0000000000000..93def75236dc7 --- /dev/null +++ b/components/americommerce/sources/new-customer-instant/new-customer-instant.mjs @@ -0,0 +1,28 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "americommerce-new-customer-instant", + name: "New Customer Created (Instant)", + description: "Emit new event when a customer is created in your Americommerce store. [See the documentation](https://developers.cart.com/docs/rest-api/ZG9jOjM1MDU4Nw-webhooks#subscribing-to-a-webhook).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return events.CUSTOMER_CREATED; + }, + generateMeta(resource) { + const { customer } = resource; + return { + id: customer.id, + summary: `New Customer: ${customer.first_name}`, + ts: Date.parse(customer.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/americommerce/sources/new-customer-instant/test-event.mjs b/components/americommerce/sources/new-customer-instant/test-event.mjs new file mode 100644 index 0000000000000..4e9d27c46ea19 --- /dev/null +++ b/components/americommerce/sources/new-customer-instant/test-event.mjs @@ -0,0 +1,48 @@ +export default { + "customer": { + "id": 0, + "customer_number": "string", + "last_name": "string", + "first_name": "string", + "email": "string", + "phone_number": "string", + "registered_at": "string", + "last_visit_at": "string", + "adcode": "string", + "adcode_id": 0, + "affiliate_id": 0, + "customer_type_id": 0, + "is_no_tax_customer": true, + "comments": "string", + "store_id": 0, + "source": "string", + "search_string": "string", + "no_account": true, + "sales_person": "string", + "alternate_phone_number": "string", + "is_affiliate_customer": true, + "updated_at": "string", + "created_at": "string", + "username": "string", + "is_contact_information_only": true, + "tax_exemption_number": "string", + "company": "string", + "source_group": "string", + "sales_person_user_id": 0, + "store_payment_methods_enabled": [ + "string" + ], + "tax_rate": 0, + "lock_default_address": true, + "reward_tier_id": 0, + "payment_net_term": 0, + "credit_limit": 0, + "is_inactive": true, + "use_shared_credit_limit": true, + "override_shared_credit_limit": true, + "customer_payment_methods_availability": [ + {} + ], + "default_payment_type_name": "string" + } +}; diff --git a/components/americommerce/sources/new-product-instant/new-product-instant.mjs b/components/americommerce/sources/new-product-instant/new-product-instant.mjs new file mode 100644 index 0000000000000..149892507386c --- /dev/null +++ b/components/americommerce/sources/new-product-instant/new-product-instant.mjs @@ -0,0 +1,28 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "americommerce-new-product-instant", + name: "New Product Instant", + description: "Emit new event when a product is added to your Americommerce store. [See the documentation](https://developers.cart.com/docs/rest-api/ZG9jOjM1MDU4Nw-webhooks#subscribing-to-a-webhook).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return events.PRODUCT_CREATED; + }, + generateMeta(resource) { + const { product } = resource; + return { + id: product.id, + summary: `New Product: ${product.id}`, + ts: Date.parse(product.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/americommerce/sources/new-product-instant/test-event.mjs b/components/americommerce/sources/new-product-instant/test-event.mjs new file mode 100644 index 0000000000000..fcb3539767e5c --- /dev/null +++ b/components/americommerce/sources/new-product-instant/test-event.mjs @@ -0,0 +1,169 @@ +export default { + "product": { + "id": 0, + "item_number": "string", + "manufacturer_id": 0, + "manufacturer_part_number": "string", + "primary_category_id": 0, + "product_status_id": 0, + "item_name": "string", + "bullets": "string", + "short_description": "string", + "long_description_2": "string", + "long_description_3": "string", + "long_description_4": "string", + "long_description_5": "string", + "height": "string", + "length": "string", + "width": "string", + "size_unit": "string", + "weight": 0, + "weight_unit": "string", + "cost": 0, + "price": 0, + "retail": 0, + "minimum_quantity": 0, + "maximum_quantity": 0, + "is_spotlight_item": 0, + "quantity_on_hand": 0, + "keywords": "string", + "is_non_taxable": 0, + "is_shipped_individually": 0, + "is_hidden": 0, + "sort_order": 0, + "e_product_type": "string", + "e_product_url": "string", + "e_product_password": "string", + "e_product_verification_link_expiration": 0, + "e_product_email": "string", + "e_product_allow_multiple_deliveries": 0, + "warehouse_id": 0, + "call_for_shipping": 0, + "quickbooks_item_id": "string", + "call_for_pricing": 0, + "rate_adjustment_type": "string", + "meta_description": "string", + "page_title": "string", + "use_tabs": true, + "related_name": "string", + "override_theme_use_tabs": true, + "long_description_tab_name_1": "string", + "long_description_tab_name_2": "string", + "long_description_tab_name_3": "string", + "long_description_tab_name_4": "string", + "long_description_tab_name_5": "string", + "long_description_1": "string", + "is_non_shipping_item": true, + "e_product_delivery_action": "string", + "use_variant_inventory": true, + "is_featured_item": true, + "long_description_external_url_1": "string", + "long_description_external_url_2": "string", + "long_description_external_url_3": "string", + "long_description_external_url_4": "string", + "long_description_external_url_5": "string", + "bullets_external_url": "string", + "custom_flag_1": true, + "custom_flag_2": true, + "custom_flag_3": true, + "custom_flag_4": true, + "custom_flag_5": true, + "created_at": "string", + "updated_at": "string", + "url_rewrite": "string", + "is_kit": true, + "is_child_product": true, + "is_non_inventory": true, + "is_discontinued": true, + "eta_date": "string", + "quantity_on_order": 0, + "available_region_id": 0, + "call_for_shipping_on_whole_order": true, + "break_out_shipping": true, + "shipping_classification_code": "string", + "exclude_parent_from_display": true, + "drop_ship": true, + "no_price_mask": "string", + "starting_quantity": 0, + "tax_code": "string", + "use_map_pricing": true, + "last_item_number": "string", + "has_visible_variants": true, + "product_rating_dimension_group_override_id": 0, + "average_review_rating": 0, + "review_count": 0, + "exclude_children_from_display": true, + "use_pricing_from_parent": true, + "low_stock_warning_threshold": 0, + "enable_low_stock_warning": true, + "do_not_discount": true, + "head_tags": "string", + "handling_fee": 0, + "custom_upsell_url": "string", + "e_product_serial_number_file_path": "string", + "hide_variant_surcharges": true, + "quantity_increment": 0, + "gtin": "string", + "add_to_cart_message": "string", + "is_subscription_product": true, + "subscription_frequency": 0, + "subscription_frequency_type": "string", + "e_product_generic_username": "string", + "e_product_generic_password": "string", + "shipping_override": 0, + "insurance_cost": 0, + "exclude_from_commissions": true, + "days_until_reorder_allowed": 0, + "force_separate_order": true, + "approval_required": true, + "in_stock_notification_email_template_id": 0, + "inelligible_for_purchase_by_points": true, + "earns_points": true, + "additional_points_earned": 0, + "allowed_variable_subscription_types": "string", + "profile_id": 0, + "is_linked_product": true, + "master_product_id": 0, + "do_not_send_review_request": true, + "pack_slip_sort_order": 0, + "is_enable_variants_on_parent": true, + "is_hide_children_on_parent": true, + "cart_product_id": null, + "enabled_configurator": "string", + "material_code": "string", + "product_line_code": "string", + "ext_update_date": "string", + "additional_attributes": "string", + "shipper_hq_shipping_groups": "string", + "shipper_hq_dimensional_rule_groups": "string", + "shipper_hq_packing_boxes": "string", + "freight_class": "string", + "hs_code": "string", + "coo": "string", + "hts": "string", + "is_exempt_from_min_order_amount": true, + "category_list": "string", + "variants": [ + { + "id": 0, + "product_id": 0, + "variant_group_id": 0, + "description": "string", + "price_adjustment": 0, + "price_adjustment_type": "string", + "sort_order": 0, + "item_number_extension": "string", + "item_number_sort_order": 0, + "is_hidden": true, + "weight": 0, + "weight_type": "string", + "updated_at": "string", + "created_at": "string", + "is_default_selection": true, + "swatch_file": "string", + "swatch_thumbnail": "string", + "swatch_thumbnail_color": "string" + } + ] + } +}; diff --git a/components/americommerce/sources/new-valid-order-instant/new-valid-order-instant.mjs b/components/americommerce/sources/new-valid-order-instant/new-valid-order-instant.mjs new file mode 100644 index 0000000000000..188f3d0d110d7 --- /dev/null +++ b/components/americommerce/sources/new-valid-order-instant/new-valid-order-instant.mjs @@ -0,0 +1,28 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "americommerce-new-valid-order-instant", + name: "New Valid Order (Instant)", + description: "Emit new event when a new valid order (not declined or cancelled) is created in your Americommerce store. [See the documentation](https://developers.cart.com/docs/rest-api/ZG9jOjM1MDU4Nw-webhooks#subscribing-to-a-webhook).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return events.NEW_VALID_ORDER; + }, + generateMeta(resource) { + const { order } = resource; + return { + id: order.id, + summary: `New Valid Order: ${order.id}`, + ts: Date.parse(order.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/americommerce/sources/new-valid-order-instant/test-event.mjs b/components/americommerce/sources/new-valid-order-instant/test-event.mjs new file mode 100644 index 0000000000000..3cfbadc1a063e --- /dev/null +++ b/components/americommerce/sources/new-valid-order-instant/test-event.mjs @@ -0,0 +1,65 @@ +export default { + "order": { + "id": 0, + "customer_id": 0, + "customer_type_id": 0, + "adcode": "string", + "ordered_at": "string", + "order_status_id": 0, + "special_instructions": "string", + "subtotal": 0, + "tax_total": 0, + "shipping_total": 0, + "discount_total": 0, + "grand_total": 0, + "cost_total": 0, + "selected_shipping_method": "string", + "ip_address": "string", + "exported_to_accounting_system": 0, + "referrer": "string", + "order_shipping_address_id": 0, + "order_billing_address_id": 0, + "admin_comments": "string", + "source": "string", + "search_phrase": "string", + "is_ppc": true, + "ppc_keyword": "string", + "store_id": 0, + "session_id": 0, + "handling_total": 0, + "is_payment_order_only": true, + "selected_shipping_provider_service": "string", + "additional_fees": 0, + "adcode_id": 0, + "updated_at": "string", + "created_at": "string", + "is_gift": true, + "gift_message": "string", + "public_comments": "string", + "instructions": "string", + "source_group": "string", + "from_subscription_id": 0, + "previous_order_status_id": 0, + "order_status_last_changed_at": "string", + "discounted_shipping_total": 0, + "order_number": "string", + "coupon_code": "string", + "order_type": "string", + "expires_at": "string", + "expires": true, + "from_quote_id": 0, + "campaign_code": "string", + "reward_points_credited": true, + "sales_agent_user_id": 0, + "channel": "string", + "device": "string", + "manufacturer_invoice_number": "string", + "manufacturer_invoice_amount": 0, + "manufacturer_invoice_paid": true, + "due_date": "string", + "delivery_tax": 0, + "location_id": 0, + "entered_by": "string", + "entered_by_type": "string" + } +}; diff --git a/components/amilia/README.md b/components/amilia/README.md index 8820c14f20bfe..7fe33be045a71 100644 --- a/components/amilia/README.md +++ b/components/amilia/README.md @@ -1,9 +1,11 @@ # Overview -You can use the Amilia API to build a variety of applications, including: +Amilia API serves as a gateway to streamline recreation and community-based activities management. It allows developers to integrate a variety of functionalities such as account management, program registrations, and scheduling into their applications. With Pipedream, you can harness this API to automate processes, sync data across platforms, and enhance customer engagement by tying into email, SMS services, or other business tools. -- A shopping cart application -- A registration system for events or courses -- A membership management system -- A booking system for appointments or rentals -- A payment processing system +# Example Use Cases + +- **Automated Registration Confirmation**: Trigger an email or SMS through SendGrid or Twilio whenever a new user signs up for a program on Amilia. This workflow ensures participants receive immediate confirmation and relevant information, improving communication and user experience. + +- **Synchronization of Participant Data**: Connect Amilia to Google Sheets to maintain a real-time updated list of program participants. Whenever a new registration occurs, their details are added to a Google Sheet, streamlining data management and accessibility for non-technical staff. + +- **Event Reminder and Feedback Loop**: Utilize Amilia's scheduling features by setting up a workflow that automatically sends a reminder to participants before an event starts using a messaging platform like Slack. After the event, trigger a feedback survey through Typeform to gather participant insights. diff --git a/components/amplenote/README.md b/components/amplenote/README.md index 5187bd95d42a2..2dab33b05a1a2 100644 --- a/components/amplenote/README.md +++ b/components/amplenote/README.md @@ -1,10 +1,11 @@ # Overview -The Amplenote API allows you to build several different types of applications. +The Amplenote API provides a way to interact programmatically with Amplenote's note-taking and task management capabilities. By using Amplenote's API on Pipedream, you can automate note creation, update tasks, and sync content across other apps and services. It's a powerful tool for those looking to streamline their workflow, enhance productivity, and integrate with other digital tools seamlessly. -Here are some examples: +# Example Use Cases -- A note taking app -- A task manager -- A to-do list -- A project management tool +- **Automated Note Creation from Emails**: Set up a workflow where incoming emails with a specific label in Gmail automatically create new notes in Amplenote. Utilize Pipedream's ability to trigger on new emails and use the Amplenote API to create notes containing the email's content. This can be useful for saving important project updates or customer feedback without manual copying and pasting. + +- **Task Management Sync with Google Calendar**: Create a flow that syncs Amplenote tasks with Google Calendar events. When a new task is added to a certain tag in Amplenote, a corresponding Google Calendar event is created. Conversely, when an event is marked as 'completed' on Google Calendar, the linked task in Amplenote gets updated. This ensures your scheduling and task tracking are always in harmony. + +- **Daily Note Digest to Slack**: Build a Pipedream workflow that fetches all new notes or tasks created on the current day in Amplenote and posts a digest to a designated Slack channel each evening. This could be a way for teams to stay updated on collective progress, or for individuals to keep track of their daily accomplishments. diff --git a/components/amplenote/package-lock.json b/components/amplenote/package-lock.json index 5283c8d853638..c46b0bda247cd 100644 --- a/components/amplenote/package-lock.json +++ b/components/amplenote/package-lock.json @@ -28,11 +28,11 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", - "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "dependencies": { - "follow-redirects": "^1.15.4", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -57,9 +57,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -153,11 +153,11 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "axios": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", - "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "requires": { - "follow-redirects": "^1.15.4", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -176,9 +176,9 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" }, "form-data": { "version": "4.0.0", diff --git a/components/amplifier/README.md b/components/amplifier/README.md new file mode 100644 index 0000000000000..e4f6dc0ecbfb3 --- /dev/null +++ b/components/amplifier/README.md @@ -0,0 +1,11 @@ +# Overview + +The Amplifier API enables customized fulfillment solutions, including on-demand production and drop-shipping for a variety of products. With this API, you can seamlessly integrate Amplifier's capabilities into your e-commerce platforms, manage inventory, track orders, and handle customer service inquiries with ease. Automating these processes with Pipedream can dramatically increase efficiency, reduce errors, and save time. + +# Example Use Cases + +- **Order Fulfillment Automation**: When a new order is placed on your Shopify store, trigger a Pipedream workflow that automatically sends the order details to Amplifier for fulfillment. This eliminates the need for manual entry, speeds up the fulfillment process, and ensures that customers receive their orders promptly. + +- **Inventory Sync Across Platforms**: Set up a Pipedream workflow that regularly checks your inventory levels in Amplifier and updates your listings on marketplaces like eBay or Amazon. This keeps your listings accurate, prevents overselling, and can automate re-listing of items as they become available. + +- **Customer Service Integration**: Implement a workflow that listens for order status updates from Amplifier and then uses the Twilio API to notify customers via SMS. This proactive approach keeps customers informed about their orders and enhances the overall customer experience. diff --git a/components/amplitude/README.md b/components/amplitude/README.md new file mode 100644 index 0000000000000..f527bdc2276cc --- /dev/null +++ b/components/amplitude/README.md @@ -0,0 +1,11 @@ +# Overview + +The Amplitude API empowers you to sync and analyze product data within Amplitude's analytics platform. With Pipedream, you can automate interactions with the Amplitude API to create complex workflows. Send event and user data, query metrics, and automate cohort analysis, which can streamline your data science and product development processes. Integrating Amplitude with Pipedream allows you to connect this data to hundreds of other services for enhanced analytics, marketing automation, and personalized customer experiences. + +# Example Use Cases + +- **Real-time Event Tracking**: Capture and send custom events to Amplitude in real-time as users interact with your product. Trigger a workflow on Pipedream when a user completes a key action, sending this event to Amplitude for immediate analysis. + +- **Cohort Analysis Automation**: Schedule daily or weekly Pipedream workflows to pull cohort data from Amplitude. Analyze user behavior over time, and automatically send this data to tools like Google Sheets or Data Studio for reporting and deeper insights. + +- **Personalized User Engagement**: Listen for particular user events in Amplitude, and respond with personalized messages or offers via email, SMS, or push notifications. For example, integrate with the Mailgun app on Pipedream to automatically send a targeted email when a user achieves a milestone within your product. diff --git a/components/amqp/README.md b/components/amqp/README.md index 8a95069a3063e..730c5d9b61c08 100644 --- a/components/amqp/README.md +++ b/components/amqp/README.md @@ -1,12 +1,11 @@ # Overview -With the AMQP API, you can build a variety of applications that perform -message-oriented middleware tasks, such as reliable messaging, queuing, and -publish/subscribe routing. Here are a few examples: - -- A messaging system that allows applications to communicate with each other - using message queues -- A system that allows applications to communicate with each other using - publish/subscribe topics -- A queuing system that allows applications to put messages into queues and - process them later +AMQP (Advanced Message Queuing Protocol) is a flexible protocol designed for high-performance messaging. Integrating the AMQP API within Pipedream workflows allows for robust messaging capabilities between various systems and services. You can use it to queue tasks, run asynchronous job processing, and facilitate communication between different parts of your application or different applications altogether. AMQP's reliability and standardization make it a go-to choice for enterprise-level messaging patterns. + +# Example Use Cases + +- **Real-time Order Processing**: Automate a real-time order processing system where AMQP queues receive new orders. Once an order message hits the queue, Pipedream triggers a workflow that checks the product availability, calculates shipping, completes the transaction, and finally, posts the order details to a Slack channel for the fulfillment team. + +- **IoT Device Message Handling**: Tackle the flow of messages from IoT devices. As your devices publish data to AMQP queues (like temperature or GPS location), Pipedream workflows can be triggered to process this data, perhaps logging it into a Google Sheets spreadsheet for analysis or alerting maintenance staff via email or SMS through Twilio if certain thresholds are exceeded. + +- **Distributed Job Scheduler**: Build a distributed job scheduler where tasks are sent to an AMQP queue. Workflows in Pipedream can then be set up to consume these messages, process the jobs — such as image rendering or data crunching — and then update a project management tool like Trello or Asana with the job status, ensuring that team members are always informed of the progress. diff --git a/components/anchor_browser/anchor_browser.app.mjs b/components/anchor_browser/anchor_browser.app.mjs new file mode 100644 index 0000000000000..0f7c1724e365a --- /dev/null +++ b/components/anchor_browser/anchor_browser.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "anchor_browser", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/anchor_browser/package.json b/components/anchor_browser/package.json new file mode 100644 index 0000000000000..1f3ec194f0af0 --- /dev/null +++ b/components/anchor_browser/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/anchor_browser", + "version": "0.0.1", + "description": "Pipedream Anchor Browser Components", + "main": "anchor_browser.app.mjs", + "keywords": [ + "pipedream", + "anchor_browser" + ], + "homepage": "https://pipedream.com/apps/anchor_browser", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/components/annature/README.md b/components/annature/README.md new file mode 100644 index 0000000000000..970b72b2d1637 --- /dev/null +++ b/components/annature/README.md @@ -0,0 +1,11 @@ +# Overview + +The Annature API provides functionality for electronic signatures, allowing documents to be signed digitally with ease and security. Integrating this API with Pipedream, users can automate document workflows, such as sending contracts for signature, tracking the status of documents, and storing signed agreements. By leveraging serverless workflows on Pipedream, these processes can be connected to hundreds of other apps to create efficient automation solutions. + +# Example Use Cases + +- **Automated Contract Deployment**: Set up a workflow that triggers when a new customer is added to your CRM (like Salesforce). The workflow automatically sends a pre-filled contract for electronic signature via Annature, streamlining the onboarding process. + +- **Document Status Tracking**: Implement a Pipedream workflow that polls the Annature API for updates on document status. When a document is fully signed, trigger an action to update your project management tool (like Asana) and notify the team through a messaging platform (like Slack). + +- **Secure Document Archiving**: Create a workflow that reacts to a webhook from Annature indicating a completed signature. The workflow could then store the signed document in a secure cloud storage service (like Google Drive) and log the transaction in a database for reference. diff --git a/components/annature/annature.app.mjs b/components/annature/annature.app.mjs new file mode 100644 index 0000000000000..df2524b90ac42 --- /dev/null +++ b/components/annature/annature.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "annature", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/annature/package.json b/components/annature/package.json new file mode 100644 index 0000000000000..5d9778bcfbe34 --- /dev/null +++ b/components/annature/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/annature", + "version": "0.0.1", + "description": "Pipedream Annature Components", + "main": "annature.app.mjs", + "keywords": [ + "pipedream", + "annature" + ], + "homepage": "https://pipedream.com/apps/annature", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/announcekit/README.md b/components/announcekit/README.md new file mode 100644 index 0000000000000..2bfe099243f58 --- /dev/null +++ b/components/announcekit/README.md @@ -0,0 +1,11 @@ +# Overview + +AnnounceKit is a tool for creating, managing, and publishing announcements to keep your users informed about product updates and news. Using the AnnounceKit API within Pipedream, you can automate the delivery of these updates across different platforms, sync release notes with your product's lifecycle events, or control the flow of communication based on user behavior or preferences. This level of automation can enhance user engagement and ensure timely updates without manual intervention. + +# Example Use Cases + +- **Sync New Blog Posts with Announcements**: Whenever a new blog post is published in WordPress, the AnnounceKit API can automatically create a corresponding announcement in Pipedream. This keeps your users informed about the latest news as part of your regular content strategy. + +- **Release Note Updates On Product Changes**: Tie in your CI/CD pipeline (e.g., GitHub Actions) with AnnounceKit through Pipedream. When a new version of your product is released, you can automatically generate and publish detailed release notes to your user base. + +- **User Feedback Triggered Announcements**: Connect AnnounceKit to a customer support tool like Zendesk using Pipedream. When multiple users report a similar issue or request, trigger a new announcement that informs users about the upcoming fix or feature, enhancing transparency and trust. diff --git a/components/anonyflow/README.md b/components/anonyflow/README.md new file mode 100644 index 0000000000000..faeb33dd7cb9e --- /dev/null +++ b/components/anonyflow/README.md @@ -0,0 +1,11 @@ +# Overview + +The AnonyFlow API provides a way to manage and automate data privacy operations. Use it on Pipedream to orchestrate data anonymization workflows for compliance with privacy regulations like GDPR. This API can handle personal data extraction, anonymization, and deletion requests programmatically. With Pipedream's serverless platform, you can integrate these privacy functions into your existing systems, triggering actions based on webhooks, schedules, or other apps' events. + +# Example Use Cases + +- **User Data Anonymization Workflow**: When a user requests their data to be anonymized, a Pipedream workflow can catch this event, send the user's data to AnonyFlow for anonymization, and then update the user's records in your database via a direct database integration or through an app like Airtable. + +- **Scheduled Data Privacy Audit**: Automate regular privacy audits by scheduling a Pipedream workflow that retrieves user data from your app, sends it to AnonyFlow for a privacy check, and receives a report. Integrate with Slack to notify your team if any issues arise, maintaining ongoing compliance. + +- **Right to be Forgotten Compliance**: On receiving a deletion request, trigger a Pipedream workflow that uses AnonyFlow to delete the user’s personal data. Follow up by logging the action in a compliance app like Smartsheet or posting a confirmation message to a private admin channel in Discord. diff --git a/components/anonyflow/actions/protect-sensitive-data/protect-sensitive-data.mjs b/components/anonyflow/actions/protect-sensitive-data/protect-sensitive-data.mjs new file mode 100644 index 0000000000000..304cc66f45b54 --- /dev/null +++ b/components/anonyflow/actions/protect-sensitive-data/protect-sensitive-data.mjs @@ -0,0 +1,50 @@ +import app from "../../anonyflow.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "anonyflow-protect-sensitive-data", + name: "Protect Sensitive Data", + description: "Encrypts sensitive data using AnonyFlow encryption service with a unique private key managed by AnonyFlow. [See the documentation](https://anonyflow.com/api)", + version: "0.0.1", + type: "action", + props: { + app, + data: { + propDefinition: [ + app, + "data", + ], + }, + keys: { + propDefinition: [ + app, + "keys", + ], + }, + }, + methods: { + anonyPacket(args = {}) { + return this.app.post({ + path: "/anony-packet", + ...args, + }); + }, + }, + async run({ $ }) { + const { + anonyPacket, + data, + keys, + } = this; + + const response = await anonyPacket({ + $, + data: { + data: utils.valueToObject(data), + keys, + }, + }); + $.export("$summary", `Successfully protected the data with status \`${response.status}\``); + return response; + }, +}; diff --git a/components/anonyflow/actions/unprotect-sensitive-data/unprotect-sensitive-data.mjs b/components/anonyflow/actions/unprotect-sensitive-data/unprotect-sensitive-data.mjs new file mode 100644 index 0000000000000..2c5bd050d564f --- /dev/null +++ b/components/anonyflow/actions/unprotect-sensitive-data/unprotect-sensitive-data.mjs @@ -0,0 +1,50 @@ +import app from "../../anonyflow.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "anonyflow-unprotect-sensitive-data", + name: "Unprotect Sensitive Data", + description: "Decrypts protected data using AnonyFlow decryption service with a unique private key, managed by AnonyFlow. [See the documentation](https://anonyflow.com/api)", + version: "0.0.1", + type: "action", + props: { + app, + data: { + propDefinition: [ + app, + "data", + ], + }, + keys: { + propDefinition: [ + app, + "keys", + ], + }, + }, + methods: { + deanonyPacket(args = {}) { + return this.app.post({ + path: "/deanony-packet", + ...args, + }); + }, + }, + async run({ $ }) { + const { + deanonyPacket, + data, + keys, + } = this; + + const response = await deanonyPacket({ + $, + data: { + data: utils.valueToObject(data), + keys, + }, + }); + $.export("$summary", `Successfully unprotected the data with status \`${response.status}\``); + return response; + }, +}; diff --git a/components/anonyflow/anonyflow.app.mjs b/components/anonyflow/anonyflow.app.mjs index 96802b2ad5bda..33a61bdbb59b1 100644 --- a/components/anonyflow/anonyflow.app.mjs +++ b/components/anonyflow/anonyflow.app.mjs @@ -1,11 +1,47 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "anonyflow", - propDefinitions: {}, + propDefinitions: { + data: { + type: "object", + label: "Data", + description: "Data packet to be Anonymized or Deanonymized. Note that **only JSON Object is accepted**", + }, + keys: { + type: "string[]", + label: "Keys", + description: "Provided keys to be used for Anonymization or Deanonymization", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `https://api.anonyflow.com${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Accept": "application/json", + "Content-Type": "application/json", + "x-api-key": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + const config = { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }; + return axios($, config); + }, + post(args = {}) { + return this._makeRequest({ + method: "post", + ...args, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/anonyflow/common/utils.mjs b/components/anonyflow/common/utils.mjs new file mode 100644 index 0000000000000..1c285647cbdda --- /dev/null +++ b/components/anonyflow/common/utils.mjs @@ -0,0 +1,25 @@ +function isJson(value) { + if (typeof(value) !== "string") { + return false; + } + + let parsedValue; + try { + parsedValue = JSON.parse(value); + } catch (e) { + return false; + } + + return typeof(parsedValue) === "object" && parsedValue !== null; +} + +function valueToObject(value) { + if (!isJson(value)) { + return value; + } + return JSON.parse(value); +} + +export default { + valueToObject, +}; diff --git a/components/anonyflow/package.json b/components/anonyflow/package.json index 917055117d608..de1e56f6a201f 100644 --- a/components/anonyflow/package.json +++ b/components/anonyflow/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/anonyflow", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream AnonyFlow Components", "main": "anonyflow.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" } -} \ No newline at end of file +} diff --git a/components/anthropic/README.md b/components/anthropic/README.md new file mode 100644 index 0000000000000..a54d06b61c0a1 --- /dev/null +++ b/components/anthropic/README.md @@ -0,0 +1,11 @@ +# Overview + +The Anthropic (Claude) API offers a conversational AI that can be leveraged for a variety of applications, ranging from answering questions to generating content or providing customer support. In Pipedream, you can create powerful workflows that utilize the capabilities of Claude to automate responses, analyze text, or enhance data with AI-generated insights. Pipedream's serverless platform simplifies integrating Claude into your processes with other apps and services for seamless automation. + +# Example Use Cases + +- **Content Generation Workflow**: Use Claude to automatically generate blog post ideas or outlines. Set up a trigger in Pipedream that listens for a specific event (like a new row in a Google Sheets spreadsheet) and then sends a prompt to Claude. The AI-generated content is then posted to a CMS like WordPress or returned for further editing. + +- **Customer Support Automation**: Connect Claude with a customer support platform like Zendesk within Pipedream. When a new support ticket is created, the workflow triggers Claude to draft a response based on the query. The suggested response is sent to a support agent for review, then forwarded to the customer, speeding up resolution times. + +- **Sentiment Analysis Engine**: Analyze user feedback by sending survey responses to Claude via Pipedream. Set up a webhook to capture responses, pass them to Claude for sentiment analysis, and store the results in a database like Airtable or send them to a Slack channel for real-time team review. diff --git a/components/anthropic/actions/chat/chat.mjs b/components/anthropic/actions/chat/chat.mjs index 8f248e3babed1..bce1c267a3108 100644 --- a/components/anthropic/actions/chat/chat.mjs +++ b/components/anthropic/actions/chat/chat.mjs @@ -1,20 +1,20 @@ -import anthropic from "../../app/anthropic.app.mjs"; +import anthropic from "../../anthropic.app.mjs"; import constants from "../common/constants.mjs"; export default { name: "Chat", - version: "0.0.4", + version: "0.0.9", key: "anthropic-chat", - description: "The Chat API. [See the documentation](https://docs.anthropic.com/claude/reference/complete_post)", + description: "The Chat API. [See the documentation](https://docs.anthropic.com/claude/reference/messages_post)", type: "action", props: { anthropic, model: { label: "Model", - description: "Select the model to use for your query. Defaults to the latest stable `claude-2` model, which Anthropic describes as having \"superior performance on tasks that require complex reasoning\".", + description: "Select the model to use for your query. Defaults to the `claude-3-opus-20240229` model, which Anthropic describes as the \"Most powerful model for highly complex tasks\".", type: "string", - options: constants.COMPLETION_MODELS, - default: constants.COMPLETION_MODELS[0], + options: constants.MESSAGE_MODELS, + default: constants.MESSAGE_MODELS[0], }, userMessage: { label: "User Message", @@ -52,35 +52,38 @@ export default { }, }, async run({ $ }) { - let prompt = ""; + const messages = []; - this.messages = typeof this.messages === "string" + const priorMessages = typeof this.messages === "string" ? JSON.parse(this.messages) : this.messages; - if (this.messages && this.messages.length) { + if (priorMessages?.length) { let isUserMessage = true; - for (const message of this.messages) { - prompt += `\n\n${isUserMessage - ? "Human" - : "Assistant"}: ${message}`; + for (const message of priorMessages) { + messages.push({ + role: isUserMessage + ? "user" + : "assistant", + content: message, + }); isUserMessage = !isUserMessage; } - } else { - this.messages = []; } - prompt = `\n\nHuman: ${this.userMessage}\n\nAssistant:`; - this.messages.push(this.userMessage); + messages.push({ + role: "user", + content: this.userMessage, + }); - const response = await this.anthropic.createChatCompletion({ + const response = await this.anthropic.createMessage({ $, data: { - prompt, + messages, model: this.model, - max_tokens_to_sample: this.maxTokensToSample, + max_tokens: this.maxTokensToSample, temperature: this.temperature ? parseFloat(this.temperature) : undefined, @@ -92,12 +95,13 @@ export default { }); if (response) { - $.export("$summary", `Successfully sent chat with ID ${response.log_id}`); + $.export("$summary", `Successfully sent message with ID ${response.id}`); } + const originalMessages = messages.map(({ content }) => content); return { - original_messages: this.messages, - original_messages_with_assistant_response: this.messages.concat(response.completion), + original_messages: originalMessages, + original_messages_with_assistant_response: originalMessages.concat(response.content[0].text), ...response, }; }, diff --git a/components/anthropic/actions/common/constants.mjs b/components/anthropic/actions/common/constants.mjs index 1caa044596981..8d5bf78792d1c 100644 --- a/components/anthropic/actions/common/constants.mjs +++ b/components/anthropic/actions/common/constants.mjs @@ -1,6 +1,12 @@ export default { - COMPLETION_MODELS: [ - "claude-2", - "claude-instant-1", + MESSAGE_MODELS: [ + "claude-3-5-sonnet-20241022", + "claude-3-5-sonnet-20240620", + "claude-3-opus-20240229", + "claude-3-sonnet-20240229", + "claude-3-haiku-20240307", + "claude-2.1", + "claude-2.0", + "claude-instant-1.2", ], }; diff --git a/components/anthropic/app/anthropic.app.mjs b/components/anthropic/anthropic.app.mjs similarity index 86% rename from components/anthropic/app/anthropic.app.mjs rename to components/anthropic/anthropic.app.mjs index 33099ccaad269..4624117312eb2 100644 --- a/components/anthropic/app/anthropic.app.mjs +++ b/components/anthropic/anthropic.app.mjs @@ -11,7 +11,7 @@ export default { _apiUrl() { return "https://api.anthropic.com/v1"; }, - async _makeRequest({ + _makeRequest({ $ = this, path, ...args }) { return axios($, { @@ -23,9 +23,9 @@ export default { ...args, }); }, - async createChatCompletion(args = {}) { + createMessage(args = {}) { return this._makeRequest({ - path: "/complete", + path: "/messages", method: "post", ...args, }); diff --git a/components/anthropic/package.json b/components/anthropic/package.json index 6b3602b7e6cbe..9dbbf034b806b 100644 --- a/components/anthropic/package.json +++ b/components/anthropic/package.json @@ -1,8 +1,8 @@ { "name": "@pipedream/anthropic", - "version": "0.0.6", + "version": "0.0.11", "description": "Pipedream Anthropic (Claude) Components", - "main": "app/anthropic.app.mjs", + "main": "anthropic.app.mjs", "keywords": [ "pipedream", "anthropic" @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" } } diff --git a/components/anyflow/package.json b/components/anyflow/package.json index d6429fc6ecdfc..d7195fd270309 100644 --- a/components/anyflow/package.json +++ b/components/anyflow/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/anymail_finder/README.md b/components/anymail_finder/README.md new file mode 100644 index 0000000000000..99fea643ebe34 --- /dev/null +++ b/components/anymail_finder/README.md @@ -0,0 +1,11 @@ +# Overview + +The Anymail Finder API lets you find and verify email addresses. Using Anymail Finder, you can look up emails by providing a person's name and their company's domain, or simply validate if an existing email is deliverable. When used in Pipedream, this API unlocks possibilities for automating outreach, enriching customer data, or cleaning your email lists. You can set up workflows that respond to various triggers (like a new lead in your CRM) and perform email lookups or verifications, streamlining tasks that would otherwise require manual intervention. + +# Example Use Cases + +- **Lead Enrichment**: When a new lead is added to your CRM (e.g., Salesforce), automatically find and verify the lead's email address using Anymail Finder. This ensures that your sales team always has accurate contact information. + +- **Email Verification for Newsletter Sign-ups**: Once someone signs up for your newsletter, use Anymail Finder to verify the email before adding it to your mailing list. This can cut down on bounce rates and maintain the health of your email campaigns. + +- **Automated Outreach**: For a list of prospects stored in Google Sheets, set up a workflow that iterates through each row, uses Anymail Finder to find the email address associated with the prospect's name and domain, and then sends a personalized outreach email using a service like SendGrid. diff --git a/components/anymail_finder/actions/search-all-emails-on-company/search-all-emails-on-company.mjs b/components/anymail_finder/actions/search-all-emails-on-company/search-all-emails-on-company.mjs new file mode 100644 index 0000000000000..d8aab21e1f7b0 --- /dev/null +++ b/components/anymail_finder/actions/search-all-emails-on-company/search-all-emails-on-company.mjs @@ -0,0 +1,35 @@ +import anymailFinder from "../../anymail_finder.app.mjs"; + +export default { + key: "anymail_finder-search-all-emails-on-company", + name: "Search All Emails on Company", + description: "Searches for most popular emails based on company information. [See the documentation](https://anymailfinder.com/email-finder-api/docs#email_search_domain)", + version: "0.0.1", + type: "action", + props: { + anymailFinder, + domain: { + propDefinition: [ + anymailFinder, + "domain", + ], + }, + companyName: { + propDefinition: [ + anymailFinder, + "companyName", + ], + }, + }, + async run({ $ }) { + const response = await this.anymailFinder.searchPopularEmails({ + $, + data: { + domain: this.domain, + company_name: this.companyName, + }, + }); + $.export("$summary", `Successfully found ${response.results.total_count} popular emails on ${this.domain}`); + return response; + }, +}; diff --git a/components/anymail_finder/actions/search-email/search-email.mjs b/components/anymail_finder/actions/search-email/search-email.mjs new file mode 100644 index 0000000000000..7ba727b3202b4 --- /dev/null +++ b/components/anymail_finder/actions/search-email/search-email.mjs @@ -0,0 +1,57 @@ +import { ConfigurationError } from "@pipedream/platform"; +import anymailFinder from "../../anymail_finder.app.mjs"; + +export default { + key: "anymail_finder-search-email", + name: "Search Email", + description: "Searches for emails based on company information and, optionally, a person's name.", + version: "0.0.1", + type: "action", + props: { + anymailFinder, + domain: { + propDefinition: [ + anymailFinder, + "domain", + ], + optional: true, + }, + companyName: { + propDefinition: [ + anymailFinder, + "companyName", + ], + optional: true, + }, + fullName: { + type: "string", + label: "Full Name", + description: "The full name of the person to search.", + }, + countryCode: { + type: "string", + label: "Country Code", + description: "The country code (e.g. US) to target the company name conversion in.", + optional: true, + }, + }, + async run({ $ }) { + + if (!this.domain && !this.companyName) { + throw new ConfigurationError("You must provide either a `domain` or a `company_name`"); + } + + const response = await this.anymailFinder.searchPersonEmail({ + $, + data: { + full_name: this.fullName, + domain: this.domain, + company_name: this.companyName, + country_code: this.countryCode, + }, + }); + + $.export("$summary", `Successfully searched for emails related to ${this.domain || this.companyName} for ${this.fullName}`); + return response; + }, +}; diff --git a/components/anymail_finder/anymail_finder.app.mjs b/components/anymail_finder/anymail_finder.app.mjs index 3c0cd1ce68faa..d2c00c8df9abe 100644 --- a/components/anymail_finder/anymail_finder.app.mjs +++ b/components/anymail_finder/anymail_finder.app.mjs @@ -1,11 +1,51 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "anymail_finder", - propDefinitions: {}, + propDefinitions: { + domain: { + type: "string", + label: "Domain", + description: "The domain to search the emails at.", + }, + companyName: { + type: "string", + label: "Company Name", + description: "The company name to search the emails at.", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.anymailfinder.com/v5.0"; + }, + _headers() { + return { + "Authorization": `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...otherOpts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...otherOpts, + }); + }, + searchPopularEmails(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/search/company.json", + ...opts, + }); + }, + searchPersonEmail(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/search/person.json", + ...opts, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/anymail_finder/package.json b/components/anymail_finder/package.json index f474e7ee51b24..25bb480a5cc7d 100644 --- a/components/anymail_finder/package.json +++ b/components/anymail_finder/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/anymail_finder", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Anymail Finder Components", "main": "anymail_finder.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" } -} \ No newline at end of file +} + diff --git a/components/apaleo/apaleo.app.mjs b/components/apaleo/apaleo.app.mjs new file mode 100644 index 0000000000000..6b13599c37438 --- /dev/null +++ b/components/apaleo/apaleo.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "apaleo", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/apaleo/package.json b/components/apaleo/package.json new file mode 100644 index 0000000000000..2f066369feae6 --- /dev/null +++ b/components/apaleo/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/apaleo", + "version": "0.0.1", + "description": "Pipedream Apaleo Components", + "main": "apaleo.app.mjs", + "keywords": [ + "pipedream", + "apaleo" + ], + "homepage": "https://pipedream.com/apps/apaleo", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/apex_27/README.md b/components/apex_27/README.md index 331f59e2d6821..62d3a469fb354 100644 --- a/components/apex_27/README.md +++ b/components/apex_27/README.md @@ -1,7 +1,11 @@ # Overview -Using the Apex 27 API, you can build a variety of applications, including: +The Apex 27 API provides real estate management tools that can streamline property listings, client interactions, and office administration. With Pipedream's serverless integration platform, this API can be a powerhouse for automations, connecting with a multitude of apps to enhance real estate business processes. By harnessing the power of Apex 27, you can automate listing updates, synchronize client information across platforms, and generate real-time notifications for key activities, ensuring that agents stay ahead of the market and deliver top-notch service. -- A web application to search for and display a list of Apex 27 products. -- A mobile application to view Apex 27 product information and pricing. -- A desktop application to manage Apex 27 inventory and orders. +# Example Use Cases + +- **Automated Property Listing Updates**: Trigger a workflow on Pipedream when a new property is listed in Apex 27 to automatically post the listing details to multiple platforms such as social media (Twitter, Facebook), real estate portals, and email newsletters. This ensures maximum exposure with minimal effort. + +- **Client Relationship Management Sync**: Whenever a new client is added to Apex 27, use Pipedream to sync their information to a CRM like Salesforce or HubSpot. This could include setting up automatic follow-up reminders, tasks for agents, and keeping all client communication and documents in one place for easy access. + +- **Lead Notification and Distribution System**: Capture leads from various sources (website inquiries, email, social media) and pipe them into Apex 27 using Pipedream. Set up a distribution system that assigns leads to agents based on predefined rules (such as location or property type) and sends instant notifications via SMS or Slack, ensuring quick response times and fair distribution of opportunities. diff --git a/components/api2pdf/README.md b/components/api2pdf/README.md index 37e55632e2c02..db4ea5a12b73a 100644 --- a/components/api2pdf/README.md +++ b/components/api2pdf/README.md @@ -1,5 +1,11 @@ # Overview -Using the Api2pdf API, you can build applications that can generate PDF -documents from a variety of input formats. For example, you could use the API -to generate PDFs from HTML, Word documents, images, and so on. +Api2pdf is a powerful API service that enables the conversion of HTML, URLs, and office documents to PDF, as well as merging of PDFs. It's an ideal tool for automating document workflows, creating on-the-fly reporting, or generating invoices without the need for an extensive backend infrastructure. On Pipedream, you can harness Api2pdf to build automated, serverless workflows that respond to events across various apps to create, modify, and distribute PDFs seamlessly. + +# Example Use Cases + +- **Generate Invoices from New Orders**: Trigger a workflow when a new order is placed in an e-commerce platform like Shopify. Use Api2pdf to convert the order details from HTML or a predefined template into a PDF invoice, then email it directly to the customer or save it to cloud storage like Google Drive for record-keeping. + +- **Create PDF Reports from Spreadsheet Data**: Set up a Pipedream scheduled task to periodically pull data from a Google Sheets spreadsheet. Format the data into an HTML template and use Api2pdf to convert it into a polished PDF report. Automate the distribution of this report by emailing it to stakeholders or uploading it to a Slack channel for team review. + +- **Merge PDFs for Contract Assembly**: Whenever contract components are updated in a document management system like Dropbox, trigger a workflow to fetch the relevant PDF files. Use Api2pdf to merge these files into a single contract document and then upload the finalized contract back to Dropbox, or send it for signing via an app like DocuSign. diff --git a/components/api4ai/README.md b/components/api4ai/README.md new file mode 100644 index 0000000000000..d088b99522db5 --- /dev/null +++ b/components/api4ai/README.md @@ -0,0 +1,43 @@ +# Overview + +API4AI offers a range of artificial intelligence solutions and APIs for diverse applications, including content moderation, image recognition, and text analysis. Built on a robust cloud technology stack, our APIs guarantee seamless operability, scalability, and reliable uptime. Our aim is to provide standalone AI solutions that effortlessly integrate into any application with minimal setup required. + +All [API4AI](https://api4.ai) APIs are subscription-based and managed through [RapidAPI](https://rapidapi.com/api4ai-api4ai-default). + + +👉️️ Website: https://api4.ai +📩 Email: hello@api4.ai +💬 Chat: https://t.me/a4a_support_bot + + + +# Getting Started + +## 🚀 Register and Subscribe to API4AI APIs on RapidAPI + +To access our APIs, begin at RapidAPI, a renowned API marketplace. Here, you'll find solutions by [API4AI](https://rapidapi.com/api4ai-api4ai-default) and other vendors, available through a subscription model, including free plans. Simply register on RapidAPI and subscribe to preferred API4AI API to start. + + +## 📖 Detailed documentation for specific actions + +- [Alcohol Label Recognition](actions/alcohol-label-recognition) +- [Brand Recognition](actions/brand-recognition) +- [Background Removal](actions/background-removal) +- [Car Image Background Removal](actions/car-image-background-removal) +- [Furniture & Household Item Recognition](actions/furniture-and-household-item-recognition) +- [Image Anonymization](actions/image-anonymization) +- [NSFW Image Recognition](actions/nsfw-image-recognition) + + + +# Example Use Cases + +- **Content Moderation Automation**: Integrate api4ai with a social media platform like Twitter on Pipedream to automatically detect and flag inappropriate or sensitive content in images or videos. When media is uploaded, api4ai can analyze it and trigger an alert or an automatic removal process if unwanted content is detected. + +- **Car dealerships**: Utilize api4ai background removal algorithm to efficiently remove backgrounds from car images and enhance them with natural shadow effects. Then, simply apply any background of your choice, and your car ad placement is ready for deployment. + +- **Automating Commercial Offer Drafts**: By integrating furniture and household items recognition solution from api4ai, the Moving companies can automate the estimation process by using photos provided by the client, leveraging AI to identify and list items quickly. This rapid response can prevent customers from turning to competitors and improve operational efficiency by reducing the workload on staff. Read step-by-step guide ["Low-Code Automation for Image Workflows with Pipedream"](http://api4.ai/blog/low-code-automation-for-image-workflows-with-pipedream-the-step-by-step-guide) to learn more about this use case. + +- **Product identification app**: Enhance your application for product identification by integrating an api4ai alcohol label recognition with a vast database of beverages and labels. + +- **Marketing campaigns**: Use api4ai to estimate if the appearance of the needed brand is sufficient for your marketing purposes and is in alignment with your request. diff --git a/components/api4ai/actions/alcohol-label-recognition/README.md b/components/api4ai/actions/alcohol-label-recognition/README.md new file mode 100644 index 0000000000000..04e57633dcde5 --- /dev/null +++ b/components/api4ai/actions/alcohol-label-recognition/README.md @@ -0,0 +1,149 @@ +# Overview + +⭐️ **Alcohol Label Recognition** – is a advanced label scanning API, powered by cutting-edge computer vision and neural network technology, designed to accurately identify various alcoholic beverages from their labels. + +This solution analyzes an image containing one or several alcohol labels and provides detailed information about the type of each beverage, along with its unique properties. + +![alco-rec](https://storage.googleapis.com/api4ai-static/rapidapi/alco-rec/alco_rec_1.jpg) + +✅ **All-in-One.** This API is more than just a wine recognition tool! It supports a diverse range of beverages, including beer, wine, vodka, whiskey, bourbon, brandy, cognac, rum, tequila, and liqueur. +✅ **Multiple labels.** The advanced alcohol label recognition algorithm can accurately analyze multiple labels, individually corresponding to different drinks, within a single image. +✅ **Characteristics.** The algorithm provides unique attributes for each drink type, such as winery, country, variety, vintage, and region for wines, and brand, country, and malt for whiskey, improving searchability. +✅ **Databaseless.** The solution operates efficiently without the need for pre-populating a label database, offering an immediate, out-of-the-box solution that works seamlessly for users. + + + + + + + + + + + + + + + + + + +
+ +

Vodka

+
+ +

Brandy

+
+ +

Beer

+
+ +

Tekila

+
+ +

Bourbon

+
+

10 drinks

+
+ +

Rum

+
+ +

Wine

+
+ +

Liqueur

+
+ +

Whiskey

+
+ +

Cognac

+
+ + +## 🤖 Demo + +Explore the Alcohol Label Recognition Web demo for free before delving into the details (no registration is required): https://api4.ai/apis/household-stuff#demo-wrapper + + + +# Getting started + +## 🚀 Subscribe and get API key + +To use Alcohol Label Recognition, start at [RapidAPI](https://rapidapi.com/), a well-known API hub. Register, subscribe to begin, and obtain an API key: + +1. Register on RapidAPI and subscribe to API4AI [Alcohol Label Recognition API](https://rapidapi.com/api4ai-api4ai-default/api/alcohol-label-recognition/pricing) to start. +2. Navigate to the [Alcohol Label Recognition API endpoints](https://rapidapi.com/api4ai-api4ai-default/api/alcohol-label-recognition) list. +3. In the "Header Parameters" section, your API Key will be shown in the `X-RapidAPI-Key` field. + + +## 🛠 Parameters + +### API Key + +An API Key is required. Register and subscribe on RapidAPI to receive one. + +### Image + +Input image. Various types are accepted: + * a **path** to a file + ``` + /tmp/myfile.jpg + ``` + * a **URL** to a file + ``` + https://storage.googleapis.com/api4ai-static/samples/nsfw-1.jpg + ``` + * a file's content encoded as a **base64** string + ``` + iVBORw0KGgoAAAANSUhEUgAABdwAAAPoCAYAAADEDjzlAAEAAElEQVR4nO... + ``` + * a file's content as a **Buffer** encoded in JSON + ```json + { + "type": "Buffer", + "data": [255,216,255,224,0,16,74,70,73,70,0,1,1,0,...] + } + ``` + * a file's content as an **Array** of bytes + ```json + [255,216,255,224,0,16,74,70,73,70,0,1,1,0,0,1,0,1,0,0,,...] + ``` + + +## ↩️ Returned values + +The "Alcohol Label Recognition" action returns the following value: + +* `labels` (array) – An array of labels identified in an input image. + +Labels are objects. Each label always has property drink which defines the kind of the drink. Possible values: `beer`, `wine`, `vodka`, `whiskey`, `bourbon`, `brandy`, `cognac`, `rum`, `tekila`, `liqueur`. + +The other properties of a label object depends on kind of the drink. For example a `wine` label will contain the following properties (besides `drink`): `country`, `variety`, `vintage`, `region`. While a `beer` label will contain the following properties (besides `drink`, again): `brewery`, `country`, `abv`, `type`. + +`N/A` – is special value for properties which is used in cases when the algorithm can not identify proper value. + +**Example** + +```json +[ + { + "drink": "wine", + "winery": "Tierra de Almas", + "country": "Spain", + "variety": "Tempranillo", + "vintage": "2020", + "region": "Rioja" + }, + { + "drink": "beer", + "brewery": "Löwenbräu", + "country": "Germany", + "abv": "0.0", + "type": "Wheat Beer" + } +] +``` diff --git a/components/api4ai/actions/alcohol-label-recognition/alcohol-label-recognition.mjs b/components/api4ai/actions/alcohol-label-recognition/alcohol-label-recognition.mjs new file mode 100644 index 0000000000000..a5eeb2c6088e3 --- /dev/null +++ b/components/api4ai/actions/alcohol-label-recognition/alcohol-label-recognition.mjs @@ -0,0 +1,49 @@ +import app from "../../api4ai.app.mjs"; +import { retryWithExponentialBackoff } from "../../common/utils.mjs"; + +export default { + name: "Alcohol Label Recognition", + description: "Accurately identifies alcohol labels using advanced intelligent technologies. Powered by API4AI.", + key: "api4ai-alcohol-label-recognition", + version: "0.0.1", + type: "action", + props: { + app, + alert: { + type: "alert", + alertType: "info", + content: "Subscribe to [API4AI Alcohol Label Recognition](https://rapidapi.com/api4ai-api4ai-default/api/alcohol-label-recognition/pricing) on the RapidAPI hub before you start using it.", + }, + image: { + propDefinition: [ + app, + "image", + ], + }, + }, + async run({ $ }) { + // Initialize output. + let summary = ""; + let labels = ""; + + // Perform request and parse results. + const cb = () => + this.app.makeRequest( + $, + "https://alcohol-label-recognition.p.rapidapi.com/v1/results", + this.image, + ); + const response = await retryWithExponentialBackoff(cb); + const isOk = response?.results?.[0]?.status?.code === "ok"; + summary = response?.results?.[0]?.status?.message || JSON.stringify(response); + if (isOk) { + labels = response.results[0].entities[0].array; + } + else { + throw new Error(summary); + } + + $.export("$summary", summary); + $.export("labels", labels); + }, +}; diff --git a/components/api4ai/actions/background-removal/README.md b/components/api4ai/actions/background-removal/README.md new file mode 100644 index 0000000000000..45af5aea24899 --- /dev/null +++ b/components/api4ai/actions/background-removal/README.md @@ -0,0 +1,114 @@ +# Overview + +⭐️ **Background Removal** offers advanced image analysis for foreground segmentation and effortless background removal. + +![bg-removal](https://storage.googleapis.com/api4ai-static/rapidapi/background-removal/bg-removal.png) + + +## 🤖 Demo + +Discover the Background Removal Web demo for free and get a feel for its capabilities before diving deeper. No registration is required: https://api4.ai/apis/bg-removal#demo-wrapper + + + +# Getting started + +## 🚀 Subscribe and get API key + +To use Background Removal, start at [RapidAPI](https://rapidapi.com/), a well-known API hub. Register, subscribe to begin, and obtain an API key: + +1. Register on RapidAPI and subscribe to API4AI [Background Removal API](https://rapidapi.com/api4ai-api4ai-default/api/background-removal4/pricing) to start. +2. Navigate to the [Background Removal API endpoints](https://rapidapi.com/api4ai-api4ai-default/api/background-removal4) list. +3. In the "Header Parameters" section, your API Key will be shown in the `X-RapidAPI-Key` field. + + +## 🛠 Parameters + +### API Key + +An API Key is required. Register and subscribe on RapidAPI to receive one. + +### Image + +Input image. Various types are accepted: + * a **path** to a file + ``` + /tmp/myfile.jpg + ``` + * a **URL** to a file + ``` + https://storage.googleapis.com/api4ai-static/samples/img-bg-removal-3.jpg + ``` + * a file's content encoded as a **base64** string + ``` + iVBORw0KGgoAAAANSUhEUgAABdwAAAPoCAYAAADEDjzlAAEAAElEQVR4nO... + ``` + * a file's content as a **Buffer** encoded in JSON + ```json + { + "type": "Buffer", + "data": [255,216,255,224,0,16,74,70,73,70,0,1,1,0,...] + } + ``` + * a file's content as an **Array** of bytes + ```json + [255,216,255,224,0,16,74,70,73,70,0,1,1,0,0,1,0,1,0,0,,...] + ``` + +### Result image representation + +By default, Background Removal returns a PNG image saved to a local file storage in the `/tmp` directory with a random filename. However, in some cases, it may be more appropriate to have an alternative representation of the result image. + +Here is a list of possible result image representations: + * **path** to a file (default) + * **URL** to a file hosted by api4ai (valid for 1 day) + * **Base64** string with file's content + * **Buffer** encoded in JSON with a file's content + * **Array** of bytes with a file's content + +In general, the result image representations are same as those for the `Image` option. Please refer to the `Image` section above for examples of each representation. + +ℹ️ **Note**: If you choose the `URL to file` option, Background Removal will return a direct URL to the resulting image file hosted by API4AI. Please be informed that API4AI hosts resulting images for **one day**. + +💡️️️️️️ **Hint**: If you wish to use a base64 encoded image as the `src` attribute value of an `` HTML element somewhere in your pipeline, remember to add `data:image/png;base64,` (don’t forget the comma!) before the actual base64 content. This informs the web browser that the `src` contains a PNG image encoded as base64 rather than a URL. For more information on displaying base64 images in HTML, visit: https://www.w3docs.com/snippets/html/how-to-display-base64-images-in-html.html + + +### Mask mode + +By default, the Background Removal returns a PNG image with the background removed. However, in some use cases, it is preferred to receive a mask of the foreground object. Technically, the mask is also a PNG image, but instead ofcontaining the original image content with the background removed, it consists of pixels ranging from black to white. White pixels correspond to the foreground area, while black pixels correspond to the background area. Grayscale pixels are transitional. + +![image-vs-mask](https://storage.googleapis.com/api4ai-static/rapidapi/background-removal/image-vs-mask.png) + + +## ↩️ Returned values + +The "Background removal" action returns a set of values which can be used to obtain processing results: + +* `result` (string) – The resulting PNG image. See the documentation of the `Result image representation` option for information on available representations. +* `width` (number) – The width of the result image (same as the input image). +* `height` (number) – The height of the result image (same as the input image). +* `objects` (array) – An array of objects in the image. + +### About "objects" + +`objects` is an array of objects in format: + +```json +{ + "class": "CLASS", + "box": { + "x": X, + "y": Y, + "w": W, + "h": H + } +} +``` + +Where: +* `CLASS` represents the object class. +* `X`, `Y`, `W`, `H` denote the bounding box dimensions. + +Bounding box coordinates are normalized, meaning they range from `0.0` to `1.0`. Multiply `X` and `W` by the image's width and `Y` and `H` by the image's height to convert to pixels. + +ℹ️ **Note**: Currently, the "Background Removal" always returns only one object – `opaque-content`, corresponding to the area of opaque content in the result image. The content outside this area in the result image is fully transparent. diff --git a/components/api4ai/actions/background-removal/background-removal.mjs b/components/api4ai/actions/background-removal/background-removal.mjs new file mode 100644 index 0000000000000..dcc98d4e94659 --- /dev/null +++ b/components/api4ai/actions/background-removal/background-removal.mjs @@ -0,0 +1,111 @@ +import app from "../../api4ai.app.mjs"; +import { + retryWithExponentialBackoff, representFile, +} from "../../common/utils.mjs"; + +export default { + name: "Background Removal", + description: + "Automatically and quickly remove image background with high accuracy. Powered by API4AI.", + key: "api4ai-background-removal", + version: "0.0.1", + type: "action", + props: { + app, + alert: { + type: "alert", + alertType: "info", + content: "Subscribe to [API4AI Background Removal](https://rapidapi.com/api4ai-api4ai-default/api/background-removal4/pricing) on the RapidAPI hub before you start using it.", + }, + image: { + propDefinition: [ + app, + "image", + ], + }, + mask: { + type: "boolean", + label: "Mask mode", + description: + "Return the foreground object mask instead of the image with the background removed.", + default: false, + optional: true, + }, + representation: { + type: "string", + label: "Result image representation", + description: + "Return the result image as:\n * **path** to a file (default)\n * **URL** to a file hosted by api4ai (valid for 1 day)\n * **Base64** string with file's content\n * **Buffer** encoded in JSON with a file's content\n * **Array** of bytes with a file's content", + default: "Path to file", + optional: true, + options: [ + "Path to file", + "URL to file", + "Base64 string", + "JSON Buffer", + "Array", + ], + }, + }, + async run({ $ }) { + // Initialize output. + let summary = ""; + let result = ""; + const objects = []; + let width = 0; + let height = 0; + + // Prepare query params. + const mode = this.mask + ? "fg-mask" + : "fg-image"; + const representation = this.representation == "URL to file" + ? "url" + : "base64"; + + // Perform request and parse results. + const cb = () => + this.app.makeRequest( + $, + "https://background-removal4.p.rapidapi.com/v1/results", + this.image, + { + mode, + representation, + }, + ); + const response = await retryWithExponentialBackoff(cb); + const isOk = response?.results?.[0]?.status?.code === "ok"; + summary = response?.results?.[0]?.status?.message || JSON.stringify(response); + if (isOk) { + result = response.results[0].entities[0].image; + const rawObjects = response.results[0].entities[1].objects; + rawObjects.forEach(function (o) { + const box = { + x: o.box[0], + y: o.box[1], + w: o.box[2], + h: o.box[3], + }; + const cls = Object.keys(o.entities[0].classes)[0]; + objects.push({ + class: cls, + box: box, + }); + }); + const format = response.results[0].entities[0].format; + result = representFile(result, format, this.representation); + width = response.results[0].width; + height = response.results[0].height; + } + else { + throw new Error(summary); + } + + $.export("$summary", summary); + $.export("result", result); + $.export("objects", objects); + $.export("width", width); + $.export("height", height); + }, +}; diff --git a/components/api4ai/actions/brand-recognition/README.md b/components/api4ai/actions/brand-recognition/README.md new file mode 100644 index 0000000000000..195810f528fd4 --- /dev/null +++ b/components/api4ai/actions/brand-recognition/README.md @@ -0,0 +1,68 @@ +# Overview + +⭐️ **Brand Recognition** – is a ready-to-Use solution for the identification of thousands of brand marks and logos within images. + +Brand Recognition provides AI-powered image processing designed for analyzing the presence of brands in the pictures. + +![brands](https://storage.googleapis.com/api4ai-static/rapidapi/brand_recognition_1.png) + +✅ The algorithm recognizes brand marks and logos, returning a JSON with the elements found in the image. One of the core features of our technology is that it usually does not require any additional actions to begin supporting a new logo, unlike most other solutions for logo detection. +✅ This solution provides out-of-the-box support for an extensive range of brands, encompassing a vast array of logos and trademarks. Furthermore, it incorporates advanced, sophisticated logic designed to automatically identify unknown ones. + + +## 🤖 Demo + +Explore the Brand Recognition Web demo for free before delving into the details (no registration is required): https://api4.ai/apis/brand-recognition#demo-wrapper + + + +# Getting started + +## 🚀 Subscribe and get API key + +To use Brand Recognition, start at [RapidAPI](https://rapidapi.com/), a well-known API hub. Register, subscribe to begin, and obtain an API key: + +1. Register on RapidAPI and subscribe to API4AI [Brand Recognition API](https://rapidapi.com/api4ai-api4ai-default/api/brand-recognition/pricing) to start. +2. Navigate to the [Brand Recognition API endpoints](https://rapidapi.com/api4ai-api4ai-default/api/brand-recognition) list. +3. In the "Header Parameters" section, your API Key will be shown in the `X-RapidAPI-Key` field. + + +## 🛠 Parameters + +### API Key + +An API Key is required. Register and subscribe on RapidAPI to receive one. + +### Image + +Input image. Various types are accepted: + * a **path** to a file + ``` + /tmp/myfile.jpg + ``` + * a **URL** to a file + ``` + https://storage.googleapis.com/api4ai-static/samples/nsfw-1.jpg + ``` + * a file's content encoded as a **base64** string + ``` + iVBORw0KGgoAAAANSUhEUgAABdwAAAPoCAYAAADEDjzlAAEAAElEQVR4nO... + ``` + * a file's content as a **Buffer** encoded in JSON + ```json + { + "type": "Buffer", + "data": [255,216,255,224,0,16,74,70,73,70,0,1,1,0,...] + } + ``` + * a file's content as an **Array** of bytes + ```json + [255,216,255,224,0,16,74,70,73,70,0,1,1,0,0,1,0,1,0,0,,...] + ``` + + +## ↩️ Returned values + +The "Brand Recognition" action returns the following value: + +* `brands` (array) – An array of brands (represented as strings) identified in an input image. diff --git a/components/api4ai/actions/brand-recognition/brand-recognition.mjs b/components/api4ai/actions/brand-recognition/brand-recognition.mjs new file mode 100644 index 0000000000000..efbd1c34edbcd --- /dev/null +++ b/components/api4ai/actions/brand-recognition/brand-recognition.mjs @@ -0,0 +1,49 @@ +import app from "../../api4ai.app.mjs"; +import { retryWithExponentialBackoff } from "../../common/utils.mjs"; + +export default { + name: "Brand Recognition", + description: "The service processes input image and responds with a list of found brand logos. Powered by API4AI.", + key: "api4ai-brand-recognition", + version: "0.0.1", + type: "action", + props: { + app, + alert: { + type: "alert", + alertType: "info", + content: "Subscribe to [API4AI Brand Recognition](https://rapidapi.com/api4ai-api4ai-default/api/brand-recognition/pricing) on the RapidAPI hub before you start using it.", + }, + image: { + propDefinition: [ + app, + "image", + ], + }, + }, + async run({ $ }) { + // Initialize output. + let summary = ""; + let brands = ""; + + // Perform request and parse results. + const cb = () => + this.app.makeRequest( + $, + "https://brand-recognition.p.rapidapi.com/v2/results", + this.image, + ); + const response = await retryWithExponentialBackoff(cb); + const isOk = response?.results?.[0]?.status?.code === "ok"; + summary = response?.results?.[0]?.status?.message || JSON.stringify(response); + if (isOk) { + brands = response.results[0].entities[0].strings; + } + else { + throw new Error(summary); + } + + $.export("$summary", summary); + $.export("brands", brands); + }, +}; diff --git a/components/api4ai/actions/car-image-background-removal/README.md b/components/api4ai/actions/car-image-background-removal/README.md new file mode 100644 index 0000000000000..809db1ea5beb4 --- /dev/null +++ b/components/api4ai/actions/car-image-background-removal/README.md @@ -0,0 +1,134 @@ +# Overview + +⭐️ **Cars Image Background Removal** designed to enhance automotive imagery, featuring high-accuracy segmentation, background removal, shadow effects, license plate blurring, and custom background replacement. + +![car-bg-removal](https://storage.googleapis.com/api4ai-static/rapidapi/cars-image-background-removal/car-bg-removal.png) + + +## 🤖 Demo + +Discover the Cars Image Background Removal Web demo for free and get a feel for its capabilities before diving deeper. No registration is required: https://api4.ai/apis/car-bg-removal#demo-wrapper + + + +# Getting started + +## 🚀 Subscribe and get API key + +To use Cars Image Background Removal, start at [RapidAPI](https://rapidapi.com/), a well-known API hub. Register, subscribe to begin, and obtain an API key: + +1. Register on RapidAPI and subscribe to API4AI [Cars Image Background Removal API](https://rapidapi.com/api4ai-api4ai-default/api/cars-image-background-removal/pricing) to start. +2. Navigate to the [Cars Image Background Removal API endpoints](https://rapidapi.com/api4ai-api4ai-default/api/cars-image-background-removal) list. +3. In the "Header Parameters" section, your API Key will be shown in the `X-RapidAPI-Key` field. + + +## 🛠 Parameters + +### API Key + +An API Key is required. Register and subscribe on RapidAPI to receive one. + +### Image + +Input image. Various types are accepted: + * a **path** to a file + ``` + /tmp/myfile.jpg + ``` + * a **URL** to a file + ``` + https://storage.googleapis.com/api4ai-static/samples/img-bg-removal-cars-1.jpg + ``` + * a file's content encoded as a **base64** string + ``` + iVBORw0KGgoAAAANSUhEUgAABdwAAAPoCAYAAADEDjzlAAEAAElEQVR4nO... + ``` + * a file's content as a **Buffer** encoded in JSON + ```json + { + "type": "Buffer", + "data": [255,216,255,224,0,16,74,70,73,70,0,1,1,0,...] + } + ``` + * a file's content as an **Array** of bytes + ```json + [255,216,255,224,0,16,74,70,73,70,0,1,1,0,0,1,0,1,0,0,,...] + ``` + +### Result image representation + +By default, Car Image Background Removal returns a PNG image saved to a local file storage in the `/tmp` directory with a random filename. However, in some cases, it may be more appropriate to have an alternative representation of the result image. + +Here is a list of possible result image representations: + * **path** to a file (default) + * **URL** to a file hosted by api4ai (valid for 1 day) + * **Base64** string with file's content + * **Buffer** encoded in JSON with a file's content + * **Array** of bytes with a file's content + +In general, the result image representations are same as those for the `Image` option. Please refer to the `Image` section above for examples of each representation. + +ℹ️ **Note**: If you choose the `URL to file` option, Car Image Background Removal will return a direct URL to the resulting image file hosted by API4AI. Please be informed that API4AI hosts resulting images for **one day**. + +💡️️️️️️ **Hint**: If you wish to use a base64 encoded image as the `src` attribute value of an `` HTML element somewhere in your pipeline, remember to add `data:image/png;base64,` (don’t forget the comma!) before the actual base64 content. This informs the web browser that the `src` contains a PNG image encoded as base64 rather than a URL. For more information on displaying base64 images in HTML, visit: https://www.w3docs.com/snippets/html/how-to-display-base64-images-in-html.html + + +### Mask mode + +By default, the Cars Image Background Removal returns a PNG image with the background removed. However, in some use cases, it is preferred to receive a mask of the car. Technically, the mask is also a PNG image, but instead ofcontaining the original image content with the background removed, it consists of pixels ranging from black to white. White pixels correspond to the car, while black pixels correspond to the background area. Grayscale pixels are transitional. + +![image-vs-mask](https://storage.googleapis.com/api4ai-static/rapidapi/cars-image-background-removal/image-vs-mask.png) + + +### Draw a shadow + +To enhance visual appeal, the Car Background Removal may add realistic shadows to the segmented car images. This optional enhancement significantly improves the overall presentation of car images, making them more attractive for online listings and promotional materials. You can enable or disable this feature using the "Draw shadow" option. + +![noshadow-vs-shadow](https://storage.googleapis.com/api4ai-static/rapidapi/cars-image-background-removal/noshadow-vs-shadow.png) + + +### Hide car license plates + +The "hide car license plate" feature may be needed for privacy and security or other reasons. This feature may be particularly important when sharing car images online or in public settings where the license plate may be visible to others. + +The Car Image Background Removal offer an advanced blurring feature for license plates of any country or shape ensures global privacy and compliance, ideal for car dealerships, automotive photographers, and online marketplaces aiming to improve digital images while adhering to privacy laws. + +![nohideclp-vs-hideclp](https://storage.googleapis.com/api4ai-static/rapidapi/cars-image-background-removal/nohideclp-vs-hideclp.png) + +ℹ️ **Note**: Enabling this feature may slightly degrade performance. + + +## ↩️ Returned values + +The "Car Image Background Removal" action returns a set of values which can be used to obtain processing results: + +* `result` (string) – The resulting PNG image, represented as base64 string or a URL to a file hosted by api4ai (valid for 1 day). +* `width` (number) – The width of the result image (same as the input image). +* `height` (number) – The height of the result image (same as the input image). +* `objects` (array) – An array of objects in the image. + +### About "objects" + +`objects` is an array of objects in format: + +```json +{ + "class": "CLASS", + "box": { + "x": X, + "y": Y, + "w": W, + "h": H + } +} +``` + +Where: +* `CLASS` represents the object class. +* `X`, `Y`, `W`, `H` denote the bounding box dimensions. + +Bounding box coordinates are normalized, meaning they range from `0.0` to `1.0`. Multiply `X` and `W` by the image's width and `Y` and `H` by the image's height to convert to pixels. + +Possible classes: +* `opaque-content`. The "Cars Image Background Removal" always returns this object, corresponding to the area of opaque content in the result image. The content outside this area in the result image is fully transparent. +* `license plate`. Returned when "Hide car license plates" is enabled and car license plate(s) are detected. diff --git a/components/api4ai/actions/car-image-background-removal/car-image-background-removal.mjs b/components/api4ai/actions/car-image-background-removal/car-image-background-removal.mjs new file mode 100644 index 0000000000000..34cc8d47b2d79 --- /dev/null +++ b/components/api4ai/actions/car-image-background-removal/car-image-background-removal.mjs @@ -0,0 +1,132 @@ +import app from "../../api4ai.app.mjs"; +import { + retryWithExponentialBackoff, representFile, +} from "../../common/utils.mjs"; + +export default { + name: "Car Image Background Removal", + description: + "Remove Background for car images. Powered by API4AI.", + key: "api4ai-car-image-background-removal", + version: "0.0.1", + type: "action", + props: { + app, + alert: { + type: "alert", + alertType: "info", + content: "Subscribe to [API4AI Cars Image Background Removal](https://rapidapi.com/api4ai-api4ai-default/api/cars-image-background-removal/pricing) on the RapidAPI hub before you start using it.", + }, + image: { + propDefinition: [ + app, + "image", + ], + }, + mask: { + type: "boolean", + label: "Mask mode", + description: + "Return the foreground object mask instead of the image with the background removed.", + default: false, + optional: true, + }, + representation: { + type: "string", + label: "Result image representation", + description: + "Return the result image as:\n * **path** to a file (default)\n * **URL** to a file hosted by api4ai (valid for 1 day)\n * **Base64** string with file's content\n * **Buffer** encoded in JSON with a file's content\n * **Array** of bytes with a file's content", + default: "Path to file", + optional: true, + options: [ + "Path to file", + "URL to file", + "Base64 string", + "JSON Buffer", + "Array", + ], + }, + shadow: { + type: "boolean", + label: "Draw a shadow", + description: "Draw a shadow underneath the car.", + default: true, + optional: true, + }, + hideclp: { + type: "boolean", + label: "Hide car license plates", + description: "Hide car license plates.", + default: false, + optional: true, + }, + }, + async run({ $ }) { + // Initialize output. + let summary = ""; + let result = ""; + const objects = []; + let width = 0; + let height = 0; + + // Prepare query params. + let mode = "fg-mask"; + if (!this.mask) { + mode = "fg-image"; + if (this.shadow) { + mode += "-shadow"; + } + if (this.hideclp) { + mode += "-hideclp"; + } + } + const representation = this.representation == "URL to file" + ? "url" + : "base64"; + + // Perform request and parse results. + const cb = () => + this.app.makeRequest( + $, + "https://cars-image-background-removal.p.rapidapi.com/v1/results", + this.image, + { + mode, + representation, + }, + ); + const response = await retryWithExponentialBackoff(cb); + const isOk = response?.results?.[0]?.status?.code === "ok"; + summary = response?.results?.[0]?.status?.message || JSON.stringify(response); + if (isOk) { + result = response.results[0].entities[0].image; + const rawObjects = response.results[0].entities[1].objects; + rawObjects.forEach(function (o) { + const box = { + x: o.box[0], + y: o.box[1], + w: o.box[2], + h: o.box[3], + }; + const cls = Object.keys(o.entities[0].classes)[0]; + objects.push({ + class: cls, + box: box, + }); + }); + const format = response.results[0].entities[0].format; + result = representFile(result, format, this.representation); + width = response.results[0].width; + height = response.results[0].height; + } + else { + throw new Error(summary); + } + + $.export("$summary", summary); + $.export("result", result); + $.export("objects", objects); + $.export("width", width); + $.export("height", height); + }, +}; diff --git a/components/api4ai/actions/furniture-and-household-item-recognition/README.md b/components/api4ai/actions/furniture-and-household-item-recognition/README.md new file mode 100644 index 0000000000000..c9ee07e4ec8f5 --- /dev/null +++ b/components/api4ai/actions/furniture-and-household-item-recognition/README.md @@ -0,0 +1,81 @@ +# Overview + +⭐️ **Furniture & Household Items Recognition** – is an accurate identification of furniture and household items with advanced intelligent detection, categorization, and counting technologies. + +The Furniture & Household Items Recognition offers AI-driven image analysis, ideal for identifying household items in photos. It streamlines inventory management for interior design, real estate, retail, and enhances service efficiency for moving companies. + +![household-stuff](https://storage.googleapis.com/api4ai-static/rapidapi/household-stuff/household_stuff_1.jpg) + +✅ Our solution provides immediate, out-of-the-box support for a wide array of furniture and household items, encompassing more than 200 distinct categories. Designed to meet diverse needs, it simplifies inventory management and enhances cataloging processes for businesses and developers. +✅ The algorithm employs advanced logic to automatically count items, seamlessly integrating this capability. Consequently, our API meticulously generates a comprehensive JSON output, detailing each identified item along with its precise corresponding quantities. + + +## 🤖 Demo + +Explore the Furniture & Household Items Recognition Web demo for free before delving into the details (no registration is required): https://api4.ai/apis/household-stuff#demo-wrapper + + + +# Getting started + +## 🚀 Subscribe and get API key + +To use Furniture & Household Items Recognition, start at [RapidAPI](https://rapidapi.com/), a well-known API hub. Register, subscribe to begin, and obtain an API key: + +1. Register on RapidAPI and subscribe to API4AI [Furniture & Household Items Recognition API](https://rapidapi.com/api4ai-api4ai-default/api/furniture-and-household-items/pricing) to start. +2. Navigate to the [Furniture & Household Items Recognition API endpoints](https://rapidapi.com/api4ai-api4ai-default/api/furniture-and-household-items) list. +3. In the "Header Parameters" section, your API Key will be shown in the `X-RapidAPI-Key` field. + + +## 🛠 Parameters + +### API Key + +An API Key is required. Register and subscribe on RapidAPI to receive one. + +### Image + +Input image. Various types are accepted: + * a **path** to a file + ``` + /tmp/myfile.jpg + ``` + * a **URL** to a file + ``` + https://storage.googleapis.com/api4ai-static/samples/nsfw-1.jpg + ``` + * a file's content encoded as a **base64** string + ``` + iVBORw0KGgoAAAANSUhEUgAABdwAAAPoCAYAAADEDjzlAAEAAElEQVR4nO... + ``` + * a file's content as a **Buffer** encoded in JSON + ```json + { + "type": "Buffer", + "data": [255,216,255,224,0,16,74,70,73,70,0,1,1,0,...] + } + ``` + * a file's content as an **Array** of bytes + ```json + [255,216,255,224,0,16,74,70,73,70,0,1,1,0,0,1,0,1,0,0,,...] + ``` + + +## ↩️ Returned values + +The "Furniture & Household Items Recognition" action returns the following value: + +* `items` (object) – An object containing the items found (keys) and their respective counts (values). + +**Example** + +```json +{ + "3-seater sofa": 1, + "Carpet": 1, + "Chair": 2, + "Chandelier": 1, + "Painting": 1, + "Table": 1 +} +``` diff --git a/components/api4ai/actions/furniture-and-household-item-recognition/furniture-and-household-item-recognition.mjs b/components/api4ai/actions/furniture-and-household-item-recognition/furniture-and-household-item-recognition.mjs new file mode 100644 index 0000000000000..317da1d924c38 --- /dev/null +++ b/components/api4ai/actions/furniture-and-household-item-recognition/furniture-and-household-item-recognition.mjs @@ -0,0 +1,49 @@ +import app from "../../api4ai.app.mjs"; +import { retryWithExponentialBackoff } from "../../common/utils.mjs"; + +export default { + name: "Furniture & Household Item Recognition", + description: "This API provides identification of furniture & household items with advanced intelligent detection, categorization, and counting technologies. Powered by API4AI.", + key: "api4ai-furniture-and-household-item-recognition", + version: "0.0.1", + type: "action", + props: { + app, + alert: { + type: "alert", + alertType: "info", + content: "Subscribe to [API4AI Furniture and Household Items Recognition](https://rapidapi.com/api4ai-api4ai-default/api/furniture-and-household-items/pricing) on the RapidAPI hub before you start using it.", + }, + image: { + propDefinition: [ + app, + "image", + ], + }, + }, + async run({ $ }) { + // Initialize output. + let summary = ""; + let items = ""; + + // Perform request and parse results. + const cb = () => + this.app.makeRequest( + $, + "https://furniture-and-household-items.p.rapidapi.com/v1/results", + this.image, + ); + const response = await retryWithExponentialBackoff(cb); + const isOk = response?.results?.[0]?.status?.code === "ok"; + summary = response?.results?.[0]?.status?.message || JSON.stringify(response); + if (isOk) { + items = response.results[0].entities[0].mapping; + } + else { + throw new Error(summary); + } + + $.export("$summary", summary); + $.export("items", items); + }, +}; diff --git a/components/api4ai/actions/image-anonymization/README.md b/components/api4ai/actions/image-anonymization/README.md new file mode 100644 index 0000000000000..d13116c41079e --- /dev/null +++ b/components/api4ai/actions/image-anonymization/README.md @@ -0,0 +1,122 @@ +# Overview + +⭐️ **Image Anonymization** – is a high-Accuracy solution for automatic detection and blurring of sensitive areas in images. Cloud-based Image Anonymization API detects and blurs faces and license plates in photos, ensuring sensitive information remains unrecognizable for secure privacy protection. + +![image-anonymization](https://storage.googleapis.com/api4ai-static/rapidapi/img_anonymization_0.gif) + + +## 🤖 Demo + +Discover the Image Anonymization Web demo for free and get a feel for its capabilities before diving deeper. No registration is required: https://api4.ai/apis/image-anonymization/#demo-wrapper + + + +# Getting started + +## 🚀 Subscribe and get API key + +To use Image Anonymization, start at [RapidAPI](https://rapidapi.com/), a well-known API hub. Register, subscribe to begin, and obtain an API key: + +1. Register on RapidAPI and subscribe to API4AI [Image Anonymization](https://rapidapi.com/api4ai-api4ai-default/api/image-anonymization/pricing) to start. +2. Navigate to the [Image Anonymization endpoints](https://rapidapi.com/api4ai-api4ai-default/api/image-anonymization) list. +3. In the "Header Parameters" section, your API Key will be shown in the `X-RapidAPI-Key` field. + + +## 🛠 Parameters + +### API Key + +An API Key is required. Register and subscribe on RapidAPI to receive one. + +### Image + +Input image. Various types are accepted: + * a **path** to a file + ``` + /tmp/myfile.jpg + ``` + * a **URL** to a file + ``` + https://storage.googleapis.com/api4ai-static/samples/img-anonymization-1.jpg + ``` + * a file's content encoded as a **base64** string + ``` + iVBORw0KGgoAAAANSUhEUgAABdwAAAPoCAYAAADEDjzlAAEAAElEQVR4nO... + ``` + * a file's content as a **Buffer** encoded in JSON + ```json + { + "type": "Buffer", + "data": [255,216,255,224,0,16,74,70,73,70,0,1,1,0,...] + } + ``` + * a file's content as an **Array** of bytes + ```json + [255,216,255,224,0,16,74,70,73,70,0,1,1,0,0,1,0,1,0,0,,...] + ``` + +### Result image representation + +By default, Image Anonymization returns a JPEG or PNG image saved to a local file storage in the `/tmp` directory with a random filename. However, in some cases, it may be more appropriate to have an alternative representation of the result image. + +Here is a list of possible result image representations: + * **path** to a file (default) + * **URL** to a file hosted by api4ai (valid for 1 day) + * **Base64** string with file's content + * **Buffer** encoded in JSON with a file's content + * **Array** of bytes with a file's content + +In general, the result image representations are same as those for the `Image` option. Please refer to the `Image` section above for examples of each representation. + +ℹ️ **Note**: If you choose the `URL to file` option, Image Anonymization will return a direct URL to the resulting image file hosted by API4AI. Please be informed that API4AI hosts resulting images for **one day**. + +💡 **Hint**: If you wish to use a base64 encoded image as the `src` attribute value of an `` HTML element somewhere in your pipeline, remember to add `data:image/jpeg;base64,` or `data:image/png;base64,` (don’t forget the comma!). before the actual base64 content. This informs the web browser that the `src` contains a JPEG or PNG image encoded as base64 rather than a URL. For more information on displaying base64 images in HTML, visit: https://www.w3docs.com/snippets/html/how-to-display-base64-images-in-html.html + + +### Objects to hide + +By default, Image Anonymization detects and hides both faces and license plates. However, for some reasons, you may wish to hide only one type. For this purpose, you can set the `Objects to hide` option to one of the following values: + * `Everything` – hides everything (default) + * `Faces only` – hides faces only + * `Licence plates only` – hides licence plates only + +![verything-vs-fonly-vs-lponly](https://storage.googleapis.com/api4ai-static/rapidapi/image-anonymization/verything-vs-fonly-vs-lponly.png) + +💡 **Hint**: Enabling the `Faces only` or `Licence plates only` mode may also improve overall performance. + + +## ↩️ Returned values + +The "Image Anonymization" action returns a set of values which can be used to obtain processing results: + +* `result` (string) – The resulting JPEG or PNG image, represented as base64 string or a URL to a file hosted by api4ai (valid for 1 day). +* `format` (string) – Resulting image format: `JPEG` or `PNG`. Same as input image format. +* `width` (number) – The width of the result image (same as the input image). +* `height` (number) – The height of the result image (same as the input image). +* `objects` (array) – An array of objects in the image. + +### About "objects" + +`objects` is an array of objects in format: + +```json +{ + "class": "CLASS", + "box": { + "x": X, + "y": Y, + "w": W, + "h": H + } +} +``` + +Where: +* `CLASS` represents the object class. +* `X`, `Y`, `W`, `H` denote the bounding box dimensions. + +Bounding box coordinates are normalized, meaning they range from `0.0` to `1.0`. Multiply `X` and `W` by the image's width and `Y` and `H` by the image's height to convert to pixels. + +Possible classes: +* `Face` +* `License plate` diff --git a/components/api4ai/actions/image-anonymization/image-anonymization.mjs b/components/api4ai/actions/image-anonymization/image-anonymization.mjs new file mode 100644 index 0000000000000..f5d27a1dcb366 --- /dev/null +++ b/components/api4ai/actions/image-anonymization/image-anonymization.mjs @@ -0,0 +1,117 @@ +import app from "../../api4ai.app.mjs"; +import { + retryWithExponentialBackoff, representFile, +} from "../../common/utils.mjs"; + +export default { + name: "Image Anonymization", + description: "Performs actual image anonymization. Powered by API4AI.", + key: "api4ai-image-anonymization", + version: "0.0.1", + type: "action", + props: { + app, + alert: { + type: "alert", + alertType: "info", + content: "Subscribe to [API4AI Image Anonymization](https://rapidapi.com/api4ai-api4ai-default/api/image-anonymization/pricing) on the RapidAPI hub before you start using it.", + }, + image: { + propDefinition: [ + app, + "image", + ], + }, + representation: { + type: "string", + label: "Result image representation", + description: + "Return the result image as:\n * **path** to a file (default)\n * **URL** to a file hosted by api4ai (valid for 1 day)\n * **Base64** string with file's content\n * **Buffer** encoded in JSON with a file's content\n * **Array** of bytes with a file's content", + default: "Path to file", + optional: true, + options: [ + "Path to file", + "URL to file", + "Base64 string", + "JSON Buffer", + "Array", + ], + }, + mode: { + type: "string", + label: "Objects to hide", + description: "Select objects which you would like to hide.", + default: "Everything", + optional: true, + options: [ + "Everything", + "Faces only", + "Licence plates only", + ], + }, + }, + async run({ $ }) { + // Initialize output. + let summary = ""; + let result = ""; + let format = ""; + const objects = []; + let width = 0; + let height = 0; + + // Prepare query params. + const params = {}; + if (this.mode == "Faces only") { + params.mode = "hide-face"; + } + else if (this.mode == "Licence plates only") { + params.mode = "hide-clp"; + } + params.representation = this.representation == "URL to file" + ? "url" + : "base64"; + + // Perform request and parse results. + const cb = () => + this.app.makeRequest( + $, + "https://image-anonymization.p.rapidapi.com/v1/results", + this.image, + params, + ); + const response = await retryWithExponentialBackoff(cb); + const isOk = response?.results?.[0]?.status?.code === "ok"; + summary = response?.results?.[0]?.status?.message || JSON.stringify(response); + if (isOk) { + result = response.results[0].entities[0].image; + const rawObjects = response.results[0].entities[1].objects; + rawObjects.forEach(function (o) { + const box = { + x: o.box[0], + y: o.box[1], + w: o.box[2], + h: o.box[3], + }; + const cls = Object.keys(o.entities[0].classes)[0]; + objects.push({ + class: cls, + box: box, + }); + }); + format = response.results[0].entities[0].format; + result = representFile(result, format, this.representation); + width = response.results[0].width; + height = response.results[0].height; + } + else { + throw new Error(summary); + } + + $.export("$summary", summary); + $.export("result", result); + $.export("format", format); + $.export("objects", objects); + $.export("width", width); + $.export("height", height); + }, +}; diff --git a/components/api4ai/actions/nsfw-image-recognition/README.md b/components/api4ai/actions/nsfw-image-recognition/README.md new file mode 100644 index 0000000000000..17d8a2aa0b476 --- /dev/null +++ b/components/api4ai/actions/nsfw-image-recognition/README.md @@ -0,0 +1,73 @@ +# Overview + +⭐️ **NSFW Image Recognition** – is a content moderation solution for NSFW (Not Safe For Work) sexual images identification. + +Leveraging AI-powered technology this solution recognizes potentially offensive content in the image that may be inappropriate for public places or workspace viewing. It provides a confidence level to indicate how certain it is that the content is NSFW. + +![sfw](https://storage.googleapis.com/api4ai-static/rapidapi/nsfw/sfw.png) + + +## 🤖 Demo + +Explore the NSFW Image Recognition Web demo for free before delving into the details (no registration is required): https://api4.ai/apis/nsfw#demo-wrapper + + + +# Getting started + +## 🚀 Subscribe and get API key + +To use NSFW Image Recognition, start at [RapidAPI](https://rapidapi.com/), a well-known API hub. Register, subscribe to begin, and obtain an API key: + +1. Register on RapidAPI and subscribe to API4AI [NSFW API](https://rapidapi.com/api4ai-api4ai-default/api/nsfw3/pricing) to start. +2. Navigate to the [NSFW API endpoints](https://rapidapi.com/api4ai-api4ai-default/api/nsfw3) list. +3. In the "Header Parameters" section, your API Key will be shown in the `X-RapidAPI-Key` field. + + +## 🛠 Parameters + +### API Key + +An API Key is required. Register and subscribe on RapidAPI to receive one. + +### Image + +Input image. Various types are accepted: + * a **path** to a file + ``` + /tmp/myfile.jpg + ``` + * a **URL** to a file + ``` + https://storage.googleapis.com/api4ai-static/samples/nsfw-1.jpg + ``` + * a file's content encoded as a **base64** string + ``` + iVBORw0KGgoAAAANSUhEUgAABdwAAAPoCAYAAADEDjzlAAEAAElEQVR4nO... + ``` + * a file's content as a **Buffer** encoded in JSON + ```json + { + "type": "Buffer", + "data": [255,216,255,224,0,16,74,70,73,70,0,1,1,0,...] + } + ``` + * a file's content as an **Array** of bytes + ```json + [255,216,255,224,0,16,74,70,73,70,0,1,1,0,0,1,0,1,0,0,,...] + ``` + +### Strictness + +Term NSFW is not well defined. The same appearance may be appropriate or not depending on context. E.g. photo of woman in bikini may considered both as SFW and NSFW. In order to satisfy needs in various scenarios we introduces strictness query parameter in order to control how strict algorithm should be. + +By default algorithms is as strict as possible (strictness `1.0`) and even photo of woman in bikini is considered as NSFW. But you may reduces strictness if it suites better your needs (up to `0.0` value). + +![strictness](https://storage.googleapis.com/api4ai-static/rapidapi/nsfw/strictness.png) + + +## ↩️ Returned values + +The "NSFW Image Recognition" action returns the following value: + +* `nsfw` (number) – Represents the NSFW probability. Is a number, typically ranging from `0.0` (safe) to `1.0` (not safe). A special negative value `-1.0` used to indicate processing error. diff --git a/components/api4ai/actions/nsfw-image-recognition/nsfw-image-recognition.mjs b/components/api4ai/actions/nsfw-image-recognition/nsfw-image-recognition.mjs new file mode 100644 index 0000000000000..eb2c2fa205d07 --- /dev/null +++ b/components/api4ai/actions/nsfw-image-recognition/nsfw-image-recognition.mjs @@ -0,0 +1,61 @@ +import app from "../../api4ai.app.mjs"; +import { retryWithExponentialBackoff } from "../../common/utils.mjs"; + +export default { + name: "NSFW Image Recognition", + description: + "Content moderation solution for NSFW (Not Safe For Work) sexual images identification. Powered by API4AI.", + key: "api4ai-nsfw-image-recognition", + version: "0.0.1", + type: "action", + props: { + app, + alert: { + type: "alert", + alertType: "info", + content: "Subscribe to [API4AI NSFW](https://rapidapi.com/api4ai-api4ai-default/api/nsfw3/pricing) on the RapidAPI hub before you start using it.", + }, + image: { + propDefinition: [ + app, + "image", + ], + }, + strictness: { + type: "string", + label: "Strictness", + description: + "Algorithm strictness. Use float values in range from 0.0 (less strict) to 1.0 (strict).", + optional: true, + default: "1.0", + }, + }, + async run({ $ }) { + // Initialize output. + let summary = ""; + let nsfw = -1; + + // Perform request and parse results. + const cb = () => + this.app.makeRequest( + $, + "https://nsfw3.p.rapidapi.com/v1/results", + this.image, + { + strictness: this.strictness, + }, + ); + const response = await retryWithExponentialBackoff(cb); + const isOk = response?.results?.[0]?.status?.code === "ok"; + summary = response?.results?.[0]?.status?.message || JSON.stringify(response); + if (isOk) { + nsfw = response.results[0].entities[0].classes.nsfw; + } + else { + throw new Error(summary); + } + + $.export("$summary", summary); + $.export("nsfw", nsfw); + }, +}; diff --git a/components/api4ai/api4ai.app.mjs b/components/api4ai/api4ai.app.mjs new file mode 100644 index 0000000000000..a91436b79c894 --- /dev/null +++ b/components/api4ai/api4ai.app.mjs @@ -0,0 +1,90 @@ +import { axios } from "@pipedream/platform"; +import FormData from "form-data"; +import fs from "node:fs"; + +export default { + type: "app", + app: "api4ai", + propDefinitions: { + image: { + type: "string", + label: "Image", + description: "Input image. Various types are accepted:\n * a **path** to a file\n * a **URL** to a file\n * a file's content encoded as a **base64** string\n * a file's content as a **Buffer** encoded in JSON\n * a file's content as an **Array** of bytes", + }, + }, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + + /** + * Make HTTP request. + * + * @param {Object} $ - Pipedream. + * @param {string} url - API Endpoint URL. + * @param {*} image - Input image. + * Various types are accepted: + * - a path to a file + * - a URL to a file + * - a file's content encoded as a base64 string + * - a file's content as a Buffer encoded in JSON + * - a file's content as an Array of bytes + * @param {Object} params - Query parameters. + * @returns Axios response. + */ + async makeRequest($, url, image, params = {}) { + // Prepare form data. + const base64regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; + const urlregex = /^(http|ftp)s?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/; + const form = new FormData(); + if (urlregex.test(image)) { + console.log("Input image considered as URL"); + form.append("url", image); + } + else { + let buf; + if (image instanceof Buffer) { + console.log("Input image considered as Buffer"); + buf = image; + } + else if ((image instanceof Object) && image.type === "Buffer") { + console.log("Input image considered as JSON object with Buffer"); + buf = Buffer.from(image); + } + else if (image instanceof Array) { + console.log("Input image considered as Array"); + buf = Buffer.from(image); + } + else if (base64regex.test(image)) { + console.log("Input image considered as base64 string"); + buf = Buffer.from(image, "base64"); + } + else { + console.log("Input image considered as file path"); + buf = fs.readFileSync(image); + } + form.append("image", buf); + } + + // Prepare headers. + const headers = { + "Content-Type": "multipart/form-data", + "X-RapidAPI-Key": this.$auth.api_key, + "A4A-CLIENT-USER-ID": "pipedream.com", + }; + + // Perfrom POST reqest. + const response = await axios($, { + url, + method: "post", + data: form, + headers, + params, + }); + + // Return response. + return response; + }, + }, +}; diff --git a/components/api4ai/common/utils.mjs b/components/api4ai/common/utils.mjs new file mode 100644 index 0000000000000..e508074808e73 --- /dev/null +++ b/components/api4ai/common/utils.mjs @@ -0,0 +1,72 @@ +import fs from "node:fs"; + +/** + * HTTP request wrapper with retry logic. + * + * @param {Function} func - Function which perform request that should be retried. + * @param {number} [maxAttempts=3] - Max retry attempts before throw error. + * @param {number} [baseDelayS=2] - Base delay in seconds. + * @returns {Promise} + */ +const retryWithExponentialBackoff = (func, maxAttempts = 3, baseDelayS = 2) => { + let attempt = 0; + + const execute = async () => { + try { + console.log(`Attempt ${attempt + 1}`); + return await func(); + } catch (error) { + if (attempt >= maxAttempts) { + throw error; + } + + const delayMs = Math.pow(baseDelayS, attempt) * 1000; + await new Promise((resolve) => setTimeout(resolve, delayMs)); + + attempt++; + return execute(); + } + }; + + return execute(); +}; + +/** + * Represent file. + * + * @param {*} file - File represent as url or base64. + * @param {string} representation - New file representation. + * @param {string} format - file format (PNG or JPEG). + * @returns {*} Represented file. + */ +const representFile = (file, format, representation) => { + if (representation != "Base64 string" && representation != "URL to file") { + const buf = Buffer.from(file, "base64"); + if (representation == "Path to file") { + const name = Math.random() + .toString(36) + .substring(2, 10); + file = `/tmp/${name}`; + if (format !== "" && format !== undefined) { + file = `${file}.${format.toLowerCase()}`; + } + console.log(`Writing file to ${file}`); + fs.writeFileSync(file, buf); + } + else if (representation == "JSON Buffer") { + file = buf.toJSON(); + } + else if (representation == "Array") { + file = buf.toJSON().data; + } + else { + throw Error("Unsupported representation"); + } + } + return file; +}; + +export { + retryWithExponentialBackoff, + representFile, +}; diff --git a/components/api4ai/package.json b/components/api4ai/package.json new file mode 100644 index 0000000000000..1b1f6e4a2c363 --- /dev/null +++ b/components/api4ai/package.json @@ -0,0 +1,36 @@ +{ + "name": "@pipedream/api4ai", + "version": "0.1.0", + "description": "Pipedream API4AI Components", + "main": "api4ai.app.mjs", + "keywords": [ + "pipedream", + "api4ai", + "AI", + "computer vision", + "API", + "image", + "analysis", + "background", + "removal", + "car", + "nsfw", + "sfw", + "face", + "anonymization", + "brand", + "wine", + "alcohol", + "furniture", + "household" + ], + "homepage": "https://pipedream.com/apps/api4ai", + "author": "API4AI Team (https://api4.ai/)", + "dependencies": { + "@pipedream/platform": "^1.6.2", + "form-data": "^4.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/components/api_bible/README.md b/components/api_bible/README.md new file mode 100644 index 0000000000000..155de0b70c669 --- /dev/null +++ b/components/api_bible/README.md @@ -0,0 +1,11 @@ +# Overview + +The API.Bible service allows you to integrate a vast collection of Bible translations into your applications, providing access to verses, chapters, and even audio Bibles. With API.Bible on Pipedream, you can build serverless workflows that automate tasks such as daily verse delivery, content search and retrieval, scripture comparison across translations, or the integration of scripture into educational or devotional apps. Pipedream's capability to connect with various other apps and services means you can seamlessly integrate scripture content into chatbots, social media posts, email campaigns, and more. + +# Example Use Cases + +- **Daily Inspirational Verse Workflow**: Triggered daily, this workflow fetches a verse of the day from API.Bible, formats it, and sends it to a predetermined list of email subscribers using the SendGrid app. + +- **Social Media Scripture Sharing**: When a new post is tagged with a specific hashtag (e.g., #VerseOfTheDay) on platforms like Twitter or Instagram, the workflow searches for a relevant verse using API.Bible and replies to the post with the scripture text. + +- **Bible Study Resource Aggregator**: On a weekly trigger, this workflow compiles a list of scriptures from a given reading plan, fetches commentary and related resources using API.Bible, and posts the compiled study material to a Slack channel or Google Drive folder for easy access by study group members. diff --git a/components/api_bible/package.json b/components/api_bible/package.json index 7207bb162b01b..87226f1af8b0e 100644 --- a/components/api_bible/package.json +++ b/components/api_bible/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/api_ninjas/README.md b/components/api_ninjas/README.md new file mode 100644 index 0000000000000..115816cbdb132 --- /dev/null +++ b/components/api_ninjas/README.md @@ -0,0 +1,11 @@ +# Overview + +API Ninjas offers a wide-ranging collection of APIs that allow you to tap into various functionalities, like data retrieval, calculations, and text processing. On Pipedream, you can harness these capabilities to automate workflows, enrich data, and integrate with other services. The platform provides a serverless environment to trigger actions based on events, schedule tasks, and execute complex logic with minimal overhead. + +# Example Use Cases + +- **Content Enrichment for SEO**: Use the API Ninjas API to fetch keyword-rich content and enhance your SEO strategy. In Pipedream, create a workflow that triggers on new content pieces, enriches them with SEO-friendly terms using the API, and then updates your CMS or notifies your marketing team. + +- **Data Aggregation for Market Research**: Build a workflow that periodically invokes the API Ninjas API to gather data on market trends, competitor prices, or other relevant market research data. Integrate this workflow with Google Sheets on Pipedream to store and analyze data, providing actionable insights for your business. + +- **Multilingual Support for Customer Service**: Create a Pipedream workflow that listens to incoming customer support tickets, uses the API Ninjas API to translate messages into different languages, and automates responses. This workflow can be connected to your customer service platform to streamline operations and improve customer experiences. diff --git a/components/api_ninjas/api_ninjas.app.mjs b/components/api_ninjas/api_ninjas.app.mjs new file mode 100644 index 0000000000000..4e00ae37b8003 --- /dev/null +++ b/components/api_ninjas/api_ninjas.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "api_ninjas", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/api_ninjas/package.json b/components/api_ninjas/package.json new file mode 100644 index 0000000000000..79ea1b16a4df0 --- /dev/null +++ b/components/api_ninjas/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/api_ninjas", + "version": "0.0.1", + "description": "Pipedream API Ninjas Components", + "main": "api_ninjas.app.mjs", + "keywords": [ + "pipedream", + "api_ninjas" + ], + "homepage": "https://pipedream.com/apps/api_ninjas", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/api_sports/README.md b/components/api_sports/README.md new file mode 100644 index 0000000000000..4c53f5010deab --- /dev/null +++ b/components/api_sports/README.md @@ -0,0 +1,11 @@ +# Overview + +The API Sports API is a robust data source offering detailed information on numerous sports, leagues, teams, and players. It provides real-time scores, stats, standings, and historical data, making it a treasure trove for sports enthusiasts, fantasy league managers, and sports analysts. On Pipedream, you can leverage this API to create serverless workflows that respond to various sports events and data updates. Combine it with other apps to craft automated processes, notifications, and data analysis pipelines that enrich your application or feed your sports data obsession. + +# Example Use Cases + +- **Live Score Updates to Slack**: Send instant updates to a Slack channel whenever a goal is scored in a soccer match. This workflow can utilize the Pipedream's built-in Slack app to post messages directly to your workspace, keeping you and your team informed in real-time. + +- **Fantasy Football Alerts**: Create a workflow that tracks player statistics and injuries, triggering email alerts using Pipedream's integrated email service. This is perfect for fantasy football managers needing up-to-date info for making critical roster decisions. + +- **Sports Data Aggregation**: Build a workflow that collects daily sports statistics and stores them in a Google Sheets document. Pipedream's Google Sheets app can insert rows of fresh data, providing you with a continuously updated dataset for analysis or sharing. diff --git a/components/api_void/README.md b/components/api_void/README.md deleted file mode 100644 index 9e3b06e47dab0..0000000000000 --- a/components/api_void/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Overview - -With the Api Void API, you can build apps that: - -- Search for and analyze public APIs -- Get detailed information about an API, including its methods, parameters, and - response data -- Monitor an API for changes, and receive notifications when an API changes -- Generate mock data for an API -- Generate code samples for an API diff --git a/components/apiary/README.md b/components/apiary/README.md index 6557a39e01ad6..e636e33a92ae3 100644 --- a/components/apiary/README.md +++ b/components/apiary/README.md @@ -1,11 +1,17 @@ # Overview -Apiary is an API service that allows you to create and host APIs. You can use -Apiary to create APIs for your web applications, mobile apps, or any other type -of app. +The Apiary API, part of the Foursquare suite of applications, opens a treasure trove of location-based data and functionality. It's a powerful tool for developers aiming to enrich their apps with detailed information about venues, user check-ins, and location-based recommendations. From creating personalized travel guides to enhancing event management systems with nearby point of interest (POI) suggestions, Apiary's robust set of endpoints can be leveraged to tailor user experiences based on their real-world surroundings. -Some examples include: +# Example Use Cases -- Create APIs for your web applications -- Create APIs for your mobile apps -- Create APIs for any other type of app +**Local Event Recommendations** + +- Sync the Apiary API with a calendar app on Pipedream to recommend nearby events or venues to users based on their current location and interests logged in their calendar events. + +**Smart Travel Itineraries** + +- Use Apiary's data to automatically generate travel itineraries with local attractions, eateries, and hidden gems by connecting it to a travel planning app. These itineraries can be personalized based on user preferences stored in a Pipedream data store. + +**Location-Based Marketing Automation** + +- Integrate Apiary API with an email marketing platform on Pipedream to send targeted offers or messages to subscribers when they check in at specific locations or venues relevant to your business. diff --git a/components/apiary/package.json b/components/apiary/package.json new file mode 100644 index 0000000000000..10ef237db78c2 --- /dev/null +++ b/components/apiary/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/apiary", + "version": "0.6.0", + "description": "Pipedream apiary Components", + "main": "apiary.app.mjs", + "keywords": [ + "pipedream", + "apiary" + ], + "homepage": "https://pipedream.com/apps/apiary", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/apiflash/README.md b/components/apiflash/README.md index 0a0c115ba702e..5a280937af675 100644 --- a/components/apiflash/README.md +++ b/components/apiflash/README.md @@ -1,9 +1,11 @@ # Overview -Using the ApiFlash API, you can build: +The ApiFlash API lets you capture website screenshots programmatically. It's a Chrome-based screenshot API for developers, ideal for automating the process of taking snapshots of web pages for archiving, performance monitoring, or visual verification. With Pipedream, you can harness this functionality to create event-driven workflows, tapping into vast integration possibilities. -- A simple scraper to extract data from websites -- A tool to automate web tasks -- A bot to interact with websites on your behalf -- A system to monitor website changes -- And much more! +# Example Use Cases + +- **Content Archiving Automation**: Build a workflow that triggers daily, capturing screenshots of key web pages and storing them in cloud storage like AWS S3 or Google Cloud Storage. Useful for compliance and record-keeping. + +- **Visual Monitoring of Web Performance**: Set up a Pipedream workflow that periodically takes screenshots of your web application's critical paths. Compare these snapshots over time or against a baseline image to detect visual regressions or performance issues. + +- **Social Media Campaign Tracking**: Create a workflow that captures screenshots of social media posts or ad campaigns using ApiFlash. Have Pipedream send the images to your marketing team on Slack or via email for quick reviews and approvals. diff --git a/components/apiflash/package.json b/components/apiflash/package.json new file mode 100644 index 0000000000000..eb216e7292155 --- /dev/null +++ b/components/apiflash/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/apiflash", + "version": "0.6.0", + "description": "Pipedream apiflash Components", + "main": "apiflash.app.mjs", + "keywords": [ + "pipedream", + "apiflash" + ], + "homepage": "https://pipedream.com/apps/apiflash", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/apify/README.md b/components/apify/README.md index d8b77b6add5a4..504e7ad8f26b1 100644 --- a/components/apify/README.md +++ b/components/apify/README.md @@ -1,11 +1,17 @@ # Overview -Using the Apify API, here are some things that you can build: - -- A web scraper that can extract data from websites and store it in a format of - your choice -- An automated bot that can carry out tasks on your behalf, such as order - products or copy data from one application to another -- A tool to monitor websites for changes and notify you when they occur -- A platform to run computationally intensive tasks, such as image and video - processing, machine learning or scientific simulations +The Apify API unleashes the power to automate web scraping, process data, and orchestrate web automation workflows. By utilizing Apify on Pipedream, you can create dynamic serverless workflows to manage tasks like extracting data from websites, running browser automation, and scheduling these jobs to run autonomously. It integrates smoothly with Pipedream's capabilities to trigger actions on various other apps, store the results, and manage complex data flow with minimal setup. + +# Example Use Cases + +**E-commerce Price Monitoring** + +- Set up a Pipedream workflow that triggers an Apify actor to scrape product prices from multiple e-commerce sites daily. Store this data in Pipedream's built-in data store and use the Pipedream Email API to send a daily digest of the best deals to subscribers. + +**Social Media Sentiment Analysis** + +- Use Apify to scrape the latest tweets or social media posts about a specific topic or brand. Analyze the sentiment of these posts using a natural language processing service like Google Cloud Natural Language API. Then, use Pipedream to post the sentiment analysis results to a Slack channel for real-time brand monitoring. + +**Real Estate Listings Aggregator** + +- Create a workflow where Apify actors periodically fetch the latest real estate listings from multiple websites. Process and normalize the data with Pipedream's code steps, then automatically update a Google Sheet that serves as a central repository for all listings, keeping potential buyers informed with the latest options. diff --git a/components/apify/actions/run-actor/run-actor.mjs b/components/apify/actions/run-actor/run-actor.mjs new file mode 100644 index 0000000000000..c154add225786 --- /dev/null +++ b/components/apify/actions/run-actor/run-actor.mjs @@ -0,0 +1,130 @@ +/* eslint-disable no-unused-vars */ +import apify from "../../apify.app.mjs"; + +export default { + key: "apify-run-actor", + name: "Run Actor", + description: "Performs an execution of a selected actor in Apify. [See the documentation](https://docs.apify.com/api/v2#/reference/actors/run-collection/run-actor)", + version: "0.0.1", + type: "action", + props: { + apify, + actorId: { + propDefinition: [ + apify, + "actorId", + ], + reloadProps: true, + }, + }, + methods: { + getType(type) { + return [ + "string", + "object", + "integer", + "boolean", + ].includes(type) + ? type + : "string[]"; + }, + async getSchema() { + const { data: { items: builds } } = await this.apify.listBuilds(this.actorId); + const buildId = builds.at(-1).id; + const { data: { inputSchema } } = await this.apify.getBuild(buildId); + return JSON.parse(inputSchema); + }, + async prepareData(data) { + const newData = {}; + + const { properties } = await this.getSchema(); + for (const [ + key, + value, + ] of Object.entries(data)) { + const editor = properties[key].editor; + newData[key] = (Array.isArray(value)) + ? value.map((item) => this.setValue(editor, item)) + : value; + } + return newData; + }, + prepareOptions(value) { + let options = []; + if (value.enum && value.enumTitles) { + for (const [ + index, + val, + ] of value.enum.entries()) { + if (val) { + options.push({ + value: val, + label: value.enumTitles[index], + }); + } + } + } + return options.length + ? options + : undefined; + }, + setValue(editor, item) { + switch (editor) { + case "requestListSources" : return { + url: item, + }; + case "pseudoUrls" : return { + purl: item, + }; + case "globs" : return { + glob: item, + }; + default: return item; + } + }, + }, + async additionalProps() { + const props = {}; + if (this.actorId) { + const { + properties, required: requiredProps = [], + } = await this.getSchema(); + + for (const [ + key, + value, + ] of Object.entries(properties)) { + if (value.editor === "hidden") continue; + + props[key] = { + type: this.getType(value.type), + label: value.title, + description: value.description, + optional: !requiredProps.includes(key), + }; + const options = this.prepareOptions(value); + if (options) props[key].options = options; + } + } + return props; + }, + async run({ $ }) { + const { + getType, + getSchema, + prepareOptions, + setValue, + prepareData, + apify, + actorId, + ...data + } = this; + + const response = await apify.runActor({ + actorId, + data: await prepareData(data), + }); + $.export("$summary", `Successfully started actor run with ID: ${response.data.id}`); + return response; + }, +}; diff --git a/components/apify/actions/scrape-single-url/scrape-single-url.mjs b/components/apify/actions/scrape-single-url/scrape-single-url.mjs new file mode 100644 index 0000000000000..24c4f92c5b7c0 --- /dev/null +++ b/components/apify/actions/scrape-single-url/scrape-single-url.mjs @@ -0,0 +1,57 @@ +import apify from "../../apify.app.mjs"; +import { ACTOR_ID } from "../../common/constants.mjs"; + +export default { + key: "apify-scrape-single-url", + name: "Scrape Single URL", + description: "Executes a scraper on a specific website and returns its content as text. This action is perfect for extracting content from a single page.", + version: "0.0.1", + type: "action", + props: { + apify, + url: { + type: "string", + label: "URL", + description: "The URL of the web page to scrape.", + optional: false, + }, + crawlerType: { + type: "string", + label: "Crawler Type", + description: "Select the crawling engine:\n- **Headless web browser** - Useful for modern websites with anti-scraping protections and JavaScript rendering. It recognizes common blocking patterns like CAPTCHAs and automatically retries blocked requests through new sessions. However, running web browsers is more expensive as it requires more computing resources and is slower. It is recommended to use at least 8 GB of RAM.\n- **Stealthy web browser** (default) - Another headless web browser with anti-blocking measures enabled. Try this if you encounter bot protection while scraping. For best performance, use with Apify Proxy residential IPs. \n- **Raw HTTP client** - High-performance crawling mode that uses raw HTTP requests to fetch the pages. It is faster and cheaper, but it might not work on all websites.", + options: [ + { + label: "Headless browser (stealthy Firefox+Playwright) - Very reliable, best in avoiding blocking, but might be slow", + value: "playwright:firefox", + }, + { + label: "Headless browser (Chrome+Playwright) - Reliable, but might be slow", + value: "playwright:chrome", + }, + { + label: "Raw HTTP client (Cheerio) - Extremely fast, but cannot handle dynamic content", + value: "cheerio", + }, + ], + }, + }, + async run({ $ }) { + const response = await this.apify.runActor({ + $, + actorId: ACTOR_ID, + data: { + crawlerType: this.crawlerType, + maxCrawlDepth: 0, + maxCrawlPages: 1, + maxResults: 1, + startUrls: [ + { + url: this.url, + }, + ], + }, + }); + $.export("$summary", `Successfully scraped content from ${this.url}`); + return response; + }, +}; diff --git a/components/apify/actions/set-key-value-store-record/set-key-value-store-record.mjs b/components/apify/actions/set-key-value-store-record/set-key-value-store-record.mjs new file mode 100644 index 0000000000000..a9318261b1927 --- /dev/null +++ b/components/apify/actions/set-key-value-store-record/set-key-value-store-record.mjs @@ -0,0 +1,39 @@ +import apify from "../../apify.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "apify-set-key-value-store-record", + name: "Set Key-Value Store Record", + description: "Create or update a record in the key-value store of Apify. [See the documentation](https://docs.apify.com/api/v2#/reference/key-value-stores/record-collection/put-record)", + version: "0.0.1", + type: "action", + props: { + apify, + keyValueStoreId: { + propDefinition: [ + apify, + "keyValueStoreId", + ], + }, + key: { + type: "string", + label: "Key", + description: "The key of the record to create or update in the key-value store.", + }, + value: { + type: "object", + label: "Value", + description: "The value of the record to create or update in the key-value store.", + }, + }, + async run({ $ }) { + const response = await this.apify.setKeyValueStoreRecord({ + $, + storeId: this.keyValueStoreId, + recordKey: this.key, + data: parseObject(this.value), + }); + $.export("$summary", `Successfully set the record with key '${this.key}'`); + return response; + }, +}; diff --git a/components/apify/apify.app.mjs b/components/apify/apify.app.mjs index b7dcb2508b8b2..82ffa8830bd48 100644 --- a/components/apify/apify.app.mjs +++ b/components/apify/apify.app.mjs @@ -1,11 +1,181 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + export default { type: "app", app: "apify", - propDefinitions: {}, + propDefinitions: { + keyValueStoreId: { + type: "string", + label: "Key-Value Store Id", + description: "The Id of the key-value store.", + async options({ page }) { + const { data: { items } } = await this.listKeyValueStores({ + params: { + offset: LIMIT * page, + limit: LIMIT, + unnamed: true, + }, + }); + + return items.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + actorId: { + type: "string", + label: "Actor ID", + description: "The ID of the actor to run.", + async options({ page }) { + const { data: { items } } = await this.listActors({ + params: { + offset: LIMIT * page, + limit: LIMIT, + }, + }); + + return items.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + userActorId: { + type: "string", + label: "Actor ID", + description: "The ID of the actor to monitor.", + async options({ page }) { + const { data: { items } } = await this.listUserActors({ + params: { + offset: LIMIT * page, + limit: LIMIT, + }, + }); + + return items.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + taskId: { + type: "string", + label: "Task ID", + description: "The ID of the task to monitor.", + async options({ page }) { + const { data: { items } } = await this.listTasks({ + params: { + offset: LIMIT * page, + limit: LIMIT, + }, + }); + + return items.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.apify.com/v2"; + }, + _headers() { + return { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.$auth.api_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createHook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteHook(hookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${hookId}`, + }); + }, + runActor({ + actorId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/acts/${actorId}/runs`, + ...opts, + }); + }, + runTask({ taskId }) { + return this._makeRequest({ + method: "POST", + path: `/actor-tasks/${taskId}/runs`, + }); + }, + getBuild(build) { + return this._makeRequest({ + path: `/actor-builds/${build}`, + }); + }, + listActors(opts = {}) { + return this._makeRequest({ + path: "/store", + ...opts, + }); + }, + listUserActors(opts = {}) { + return this._makeRequest({ + path: "/acts", + ...opts, + }); + }, + listTasks(opts = {}) { + return this._makeRequest({ + path: "/actor-tasks", + ...opts, + }); + }, + listBuilds(actorId) { + return this._makeRequest({ + path: `/acts/${actorId}/builds`, + }); + }, + listKeyValueStores(opts = {}) { + return this._makeRequest({ + path: "/key-value-stores", + ...opts, + }); + }, + setKeyValueStoreRecord({ + storeId, recordKey, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/key-value-stores/${storeId}/records/${recordKey}`, + ...opts, + }); }, }, }; diff --git a/components/apify/common/constants.mjs b/components/apify/common/constants.mjs new file mode 100644 index 0000000000000..a16d51a32af1a --- /dev/null +++ b/components/apify/common/constants.mjs @@ -0,0 +1,2 @@ +export const ACTOR_ID = "aYG0l9s7dbB7j3gbS"; +export const LIMIT = 100; diff --git a/components/apify/common/utils.mjs b/components/apify/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/apify/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/apify/package.json b/components/apify/package.json new file mode 100644 index 0000000000000..7e5ca56745aa6 --- /dev/null +++ b/components/apify/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/apify", + "version": "0.1.0", + "description": "Pipedream Apify Components", + "main": "apify.app.mjs", + "keywords": [ + "pipedream", + "apify" + ], + "homepage": "https://pipedream.com/apps/apify", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} + diff --git a/components/apify/sources/common/base.mjs b/components/apify/sources/common/base.mjs new file mode 100644 index 0000000000000..b246221f724d8 --- /dev/null +++ b/components/apify/sources/common/base.mjs @@ -0,0 +1,46 @@ +import apify from "../../apify.app.mjs"; + +export default { + props: { + apify, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + }, + hooks: { + async activate() { + const response = await this.apify.createHook({ + data: { + requestUrl: this.http.endpoint, + eventTypes: [ + "ACTOR.RUN.SUCCEEDED", + "ACTOR.RUN.FAILED", + "ACTOR.RUN.TIMED_OUT", + "ACTOR.RUN.ABORTED", + ], + condition: this.getCondition(), + }, + }); + this.db.set("webhookId", response.data.id); + }, + async deactivate() { + const webhookId = this.db.get("webhookId"); + await this.apify.deleteHook(webhookId); + }, + }, + async run({ body }) { + this.http.respond({ + status: 200, + }); + + this.$emit(body, { + summary: body.eventType === "TEST" + ? "Webhook test has successfully triggered!" + : this.getSummary(body), + id: body.eventData.actorRunId || `${body.userId}-${body.createAt}`, + ts: Date.parse(body.createdAt), + }); + }, +}; diff --git a/components/apify/sources/new-finished-actor-run-instant/new-finished-actor-run-instant.mjs b/components/apify/sources/new-finished-actor-run-instant/new-finished-actor-run-instant.mjs new file mode 100644 index 0000000000000..00a355a91045e --- /dev/null +++ b/components/apify/sources/new-finished-actor-run-instant/new-finished-actor-run-instant.mjs @@ -0,0 +1,34 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "apify-new-finished-actor-run-instant", + name: "New Finished Actor Run (Instant)", + description: "Emit new event when a selected actor is run and finishes.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + db: "$.service.db", + actorId: { + propDefinition: [ + common.props.apify, + "userActorId", + ], + }, + }, + methods: { + ...common.methods, + getCondition() { + return { + actorId: this.actorId, + }; + }, + getSummary(body) { + return `A new actor run ${body.eventData.actorRunId} has finished`; + }, + }, + sampleEmit, +}; diff --git a/components/apify/sources/new-finished-actor-run-instant/test-event.mjs b/components/apify/sources/new-finished-actor-run-instant/test-event.mjs new file mode 100644 index 0000000000000..ee63026ae2aeb --- /dev/null +++ b/components/apify/sources/new-finished-actor-run-instant/test-event.mjs @@ -0,0 +1,83 @@ +export default { + "userId": "e64364562TfQasd", + "createdAt": "2024-05-20T18:17:44.960Z", + "eventType": "ACTOR.RUN.SUCCEEDED", + "eventData": { + "actorId": "e64364562TfQasd", + "actorTaskId": "e64364562TfQasd", + "actorRunId": "e64364562TfQasd", + }, + "resource": { + "id": "e64364562TfQasd", + "actId": "e64364562TfQasd", + "userId": "e64364562TfQasd", + "actorTaskId": "e64364562TfQasd", + "startedAt": "2024-05-20T18:17:16.231Z", + "finishedAt": "2024-05-20T18:17:38.736Z", + "status": "SUCCEEDED", + "meta": { + "origin": "WEB", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", + }, + "stats": { + "inputBodyLen": 98, + "rebootCount": 0, + "restartCount": 0, + "durationMillis": 22359, + "resurrectCount": 0, + "runTimeSecs": 22.359, + "metamorph": 0, + "computeUnits": 0.00234, + "memAvgBytes": 123456.123456, + "memMaxBytes": 123456, + "memCurrentBytes": 123456, + "cpuAvgUsage": 4.123456, + "cpuMaxUsage": 116.123456, + "cpuCurrentUsage": 0.12, + "netRxBytes": 123456, + "netTxBytes": 123456, + }, + "options": { + "build": "latest", + "timeoutSecs": 0, + "memoryMbytes": 1024, + "diskMbytes": 2048, + }, + "buildId": "e64364562TfQasd", + "exitCode": 0, + "defaultKeyValueStoreId": "e64364562TfQasd", + "defaultDatasetId": "e64364562TfQasd", + "defaultRequestQueueId": "e64364562TfQasd", + "buildNumber": "0.3.6", + "containerUrl": "https://e64364562TfQasd.runs.apify.net", + "usage": { + "ACTOR_COMPUTE_UNITS": 0.00063456, + "DATASET_READS": 0, + "DATASET_WRITES": 0, + "KEY_VALUE_STORE_READS": 1, + "KEY_VALUE_STORE_WRITES": 1, + "KEY_VALUE_STORE_LISTS": 0, + "REQUEST_QUEUE_READS": 0, + "REQUEST_QUEUE_WRITES": 0, + "DATA_TRANSFER_INTERNAL_GBYTES": 0.000008449, + "DATA_TRANSFER_EXTERNAL_GBYTES": 0.000008449, + "PROXY_RESIDENTIAL_TRANSFER_GBYTES": 0, + "PROXY_SERPS": 0, + }, + "usageTotalUsd": 0.00254, + "usageUsd": { + "ACTOR_COMPUTE_UNITS": 0.00254, + "DATASET_READS": 0, + "DATASET_WRITES": 0, + "KEY_VALUE_STORE_READS": 0.000005, + "KEY_VALUE_STORE_WRITES": 0.00005, + "KEY_VALUE_STORE_LISTS": 0, + "REQUEST_QUEUE_READS": 0, + "REQUEST_QUEUE_WRITES": 0, + "DATA_TRANSFER_INTERNAL_GBYTES": 4.00254, + "DATA_TRANSFER_EXTERNAL_GBYTES": 0.00254, + "PROXY_RESIDENTIAL_TRANSFER_GBYTES": 0, + "PROXY_SERPS": 0, + }, + }, +}; diff --git a/components/apify/sources/new-finished-task-run-instant/new-finished-task-run-instant.mjs b/components/apify/sources/new-finished-task-run-instant/new-finished-task-run-instant.mjs new file mode 100644 index 0000000000000..0dc18ed997399 --- /dev/null +++ b/components/apify/sources/new-finished-task-run-instant/new-finished-task-run-instant.mjs @@ -0,0 +1,34 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "apify-new-finished-task-run-instant", + name: "New Finished Task Run (Instant)", + description: "Emit new event when a selected task is run and finishes.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + db: "$.service.db", + taskId: { + propDefinition: [ + common.props.apify, + "taskId", + ], + }, + }, + methods: { + ...common.methods, + getCondition() { + return { + actorTaskId: this.taskId, + }; + }, + getSummary(body) { + return `A new task run ${body.eventData.actorRunId} has finished`; + }, + }, + sampleEmit, +}; diff --git a/components/apify/sources/new-finished-task-run-instant/test-event.mjs b/components/apify/sources/new-finished-task-run-instant/test-event.mjs new file mode 100644 index 0000000000000..ee63026ae2aeb --- /dev/null +++ b/components/apify/sources/new-finished-task-run-instant/test-event.mjs @@ -0,0 +1,83 @@ +export default { + "userId": "e64364562TfQasd", + "createdAt": "2024-05-20T18:17:44.960Z", + "eventType": "ACTOR.RUN.SUCCEEDED", + "eventData": { + "actorId": "e64364562TfQasd", + "actorTaskId": "e64364562TfQasd", + "actorRunId": "e64364562TfQasd", + }, + "resource": { + "id": "e64364562TfQasd", + "actId": "e64364562TfQasd", + "userId": "e64364562TfQasd", + "actorTaskId": "e64364562TfQasd", + "startedAt": "2024-05-20T18:17:16.231Z", + "finishedAt": "2024-05-20T18:17:38.736Z", + "status": "SUCCEEDED", + "meta": { + "origin": "WEB", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", + }, + "stats": { + "inputBodyLen": 98, + "rebootCount": 0, + "restartCount": 0, + "durationMillis": 22359, + "resurrectCount": 0, + "runTimeSecs": 22.359, + "metamorph": 0, + "computeUnits": 0.00234, + "memAvgBytes": 123456.123456, + "memMaxBytes": 123456, + "memCurrentBytes": 123456, + "cpuAvgUsage": 4.123456, + "cpuMaxUsage": 116.123456, + "cpuCurrentUsage": 0.12, + "netRxBytes": 123456, + "netTxBytes": 123456, + }, + "options": { + "build": "latest", + "timeoutSecs": 0, + "memoryMbytes": 1024, + "diskMbytes": 2048, + }, + "buildId": "e64364562TfQasd", + "exitCode": 0, + "defaultKeyValueStoreId": "e64364562TfQasd", + "defaultDatasetId": "e64364562TfQasd", + "defaultRequestQueueId": "e64364562TfQasd", + "buildNumber": "0.3.6", + "containerUrl": "https://e64364562TfQasd.runs.apify.net", + "usage": { + "ACTOR_COMPUTE_UNITS": 0.00063456, + "DATASET_READS": 0, + "DATASET_WRITES": 0, + "KEY_VALUE_STORE_READS": 1, + "KEY_VALUE_STORE_WRITES": 1, + "KEY_VALUE_STORE_LISTS": 0, + "REQUEST_QUEUE_READS": 0, + "REQUEST_QUEUE_WRITES": 0, + "DATA_TRANSFER_INTERNAL_GBYTES": 0.000008449, + "DATA_TRANSFER_EXTERNAL_GBYTES": 0.000008449, + "PROXY_RESIDENTIAL_TRANSFER_GBYTES": 0, + "PROXY_SERPS": 0, + }, + "usageTotalUsd": 0.00254, + "usageUsd": { + "ACTOR_COMPUTE_UNITS": 0.00254, + "DATASET_READS": 0, + "DATASET_WRITES": 0, + "KEY_VALUE_STORE_READS": 0.000005, + "KEY_VALUE_STORE_WRITES": 0.00005, + "KEY_VALUE_STORE_LISTS": 0, + "REQUEST_QUEUE_READS": 0, + "REQUEST_QUEUE_WRITES": 0, + "DATA_TRANSFER_INTERNAL_GBYTES": 4.00254, + "DATA_TRANSFER_EXTERNAL_GBYTES": 0.00254, + "PROXY_RESIDENTIAL_TRANSFER_GBYTES": 0, + "PROXY_SERPS": 0, + }, + }, +}; diff --git a/components/apilio/README.md b/components/apilio/README.md index 740d6ac5c6914..2a4ec3edc79c7 100644 --- a/components/apilio/README.md +++ b/components/apilio/README.md @@ -1,13 +1,11 @@ # Overview -With Apilio, you can build custom integrations to automate your home, office, -or car. +Apilio is an intelligent automation platform that enables the creation of complex logic for smart home and IoT devices. By leveraging the Apilio API on Pipedream, users can stitch together actions from various services and devices, making contextual decisions based on real-time data. This can involve using variables, conditions, and logicblocks within Apilio to execute actions that would otherwise require manual intervention or complex scripting. -Some examples of what you can build with Apilio include: +# Example Use Cases -- A custom integration to automatically turn off your lights when you leave - your house -- A custom integration to automatically adjust your thermostat based on the - weather forecast -- A custom integration to automatically open your garage door when you pull - into your driveway +- **Temperature-Controlled Smart Home**: Trigger climate control devices like smart thermostats or fans when Apilio receives temperature data from IoT sensors via HTTP requests. For example, when the living room temperature exceeds 25°C, a Pipedream workflow could automatically instruct a smart thermostat to begin cooling the house. + +- **Cost-Saving Energy Automation**: Automate your home's electricity usage by connecting Apilio to smart plugs and utility rate APIs. Create a workflow on Pipedream that turns off non-essential devices when electricity prices peak, or when your solar panels aren't producing enough power, optimizing for both cost and energy efficiency. + +- **Smart Security Response**: Integrate motion sensors with smart lights and security cameras. When motion is detected in a specific area after dark, Apilio can prompt a Pipedream workflow to turn on the lights, begin recording on a camera, and send a notification to your phone, ensuring enhanced security and peace of mind. diff --git a/components/apipie_ai/apipie_ai.app.mjs b/components/apipie_ai/apipie_ai.app.mjs new file mode 100644 index 0000000000000..dc830e6a1a949 --- /dev/null +++ b/components/apipie_ai/apipie_ai.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "apipie_ai", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/apipie_ai/package.json b/components/apipie_ai/package.json new file mode 100644 index 0000000000000..0fc068179e4ae --- /dev/null +++ b/components/apipie_ai/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/apipie_ai", + "version": "0.0.1", + "description": "Pipedream APIpie.ai Components", + "main": "apipie_ai.app.mjs", + "keywords": [ + "pipedream", + "apipie_ai" + ], + "homepage": "https://pipedream.com/apps/apipie_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/apitemplate_io/README.md b/components/apitemplate_io/README.md index e1c4619750c56..b6b7428d9c49b 100644 --- a/components/apitemplate_io/README.md +++ b/components/apitemplate_io/README.md @@ -1,5 +1,14 @@ # Overview -With the APITemplate.io API, you can easily create and manage APIs for your web -applications. You can use the API to create, update, and delete resources, as -well as to manage users and permissions. +APITemplate.io's API unleashes creative automation for generating images or PDFs from predefined templates. It's a powerful tool for creating custom visual content on-the-fly, ideal for marketers, developers, or content creators who need to personalize images or documents at scale. With Pipedream's integration capabilities, you can construct workflows that trigger custom media creation based on specific triggers or actions from a wide range of apps and services. + +# Example Use Cases + +- **Dynamic Social Media Content Creation** + Automate personalized social media post generation by using APITemplate.io with Twitter's API on Pipedream. When a new product is added to your e-commerce platform, trigger the APITemplate.io API to create a custom image with product details, then post it automatically to your Twitter account to engage followers. + +- **Personalized Email Campaigns** + Combine APITemplate.io with an email service like SendGrid on Pipedream. Set up a workflow that, upon the conclusion of a webinar, generates personalized certificates using APITemplate.io and emails them to attendees using SendGrid, providing a memorable touch to your online events. + +- **Automated Real Estate Listings** + Use APITemplate.io with the Google Sheets API on Pipedream to streamline real estate listings. Whenever a new property is added to a Google Sheet, trigger a workflow that creates a professional-looking PDF brochure using APITemplate.io and send it to potential clients or upload it to your website. diff --git a/components/apollo_io/README.md b/components/apollo_io/README.md index 7f2ced03e43f4..1528c55c4f9a6 100644 --- a/components/apollo_io/README.md +++ b/components/apollo_io/README.md @@ -1,11 +1,11 @@ # Overview -Some things you can build using the Apollo.io API include: - -- A platform for connecting with doctors and medical professionals -- A social network for connecting with like-minded people -- A messaging app for sending messages and getting advice from experts -- A marketplace for finding and buying products and services -- An online community for discussing various topics -- A news site for getting the latest news and updates -- A blog for sharing your thoughts and ideas with others +The Apollo.io API on Pipedream enables you to automate sales processes by giving you programmatic access to your Apollo.io data. Through this API, you can manage leads, contacts, and opportunities, or sync data with your CRM. Pipedream's serverless platform allows you to connect Apollo.io with hundreds of other apps to automate workflows like lead enrichment, data syncing between apps, and triggering personalized communication based on prospect actions. + +# Example Use Cases + +- **Lead Enrichment and Outreach Automation**: When a new lead is added to Apollo.io, automatically enrich the lead's data with additional information and then trigger an outreach sequence in an email marketing platform like Mailchimp or SendGrid. This ensures timely and personalized engagement with potential customers. + +- **CRM Synchronization**: Sync contact and opportunity data between Apollo.io and your CRM, such as Salesforce or HubSpot. Whenever a new contact is added or an opportunity is updated in Apollo.io, the relevant data is automatically updated in your CRM, keeping all sales information aligned and up-to-date. + +- **Event-Driven Follow-up Campaigns**: Set up a Pipedream workflow that listens for specific events in Apollo.io, like email opens or link clicks. Once an event is detected, trigger personalized follow-up actions, such as sending a customized message via Slack to the sales team or creating a follow-up task in a project management tool like Asana. diff --git a/components/apollo_io/actions/add-contacts-to-sequence/add-contacts-to-sequence.mjs b/components/apollo_io/actions/add-contacts-to-sequence/add-contacts-to-sequence.mjs index caa6d38a9f35a..84b7a910efe7d 100644 --- a/components/apollo_io/actions/add-contacts-to-sequence/add-contacts-to-sequence.mjs +++ b/components/apollo_io/actions/add-contacts-to-sequence/add-contacts-to-sequence.mjs @@ -5,7 +5,7 @@ export default { name: "Add Contacts to Sequence", description: "Adds one or more contacts to a sequence in Apollo.io. [See the documentation](https://apolloio.github.io/apollo-api-docs/?shell#add-contacts-to-sequence)", type: "action", - version: "0.0.1", + version: "0.0.3", props: { app, sequenceId: { diff --git a/components/apollo_io/actions/create-account/create-account.mjs b/components/apollo_io/actions/create-account/create-account.mjs index bdb3c26aaaac2..144709ebe3600 100644 --- a/components/apollo_io/actions/create-account/create-account.mjs +++ b/components/apollo_io/actions/create-account/create-account.mjs @@ -5,7 +5,7 @@ export default { name: "Create Account", description: "Creates a new account in Apollo.io. [See the documentation](https://apolloio.github.io/apollo-api-docs/?shell#create-an-account)", type: "action", - version: "0.0.1", + version: "0.0.3", props: { app, name: { diff --git a/components/apollo_io/actions/create-contact/create-contact.mjs b/components/apollo_io/actions/create-contact/create-contact.mjs index 80109785ecaf9..a5467c95557f8 100644 --- a/components/apollo_io/actions/create-contact/create-contact.mjs +++ b/components/apollo_io/actions/create-contact/create-contact.mjs @@ -5,7 +5,7 @@ export default { name: "Create Contact", description: "Creates a new contact in Apollo.io. [See the documentation](https://apolloio.github.io/apollo-api-docs/?shell#create-a-contact)", type: "action", - version: "0.0.2", + version: "0.0.4", props: { app, email: { diff --git a/components/apollo_io/actions/create-opportunity/create-opportunity.mjs b/components/apollo_io/actions/create-opportunity/create-opportunity.mjs new file mode 100644 index 0000000000000..25c414cdb8df3 --- /dev/null +++ b/components/apollo_io/actions/create-opportunity/create-opportunity.mjs @@ -0,0 +1,66 @@ +import app from "../../apollo_io.app.mjs"; + +export default { + key: "apollo_io-create-opportunity", + name: "Create Opportunity", + description: "Creates a new opportunity in Apollo.io. [See the documentation](https://apolloio.github.io/apollo-api-docs/?shell#create-opportunity)", + type: "action", + version: "0.0.2", + props: { + app, + ownerId: { + propDefinition: [ + app, + "ownerId", + ], + optional: true, + }, + name: { + propDefinition: [ + app, + "name", + ], + description: "The name of the opportunity.", + }, + amount: { + type: "integer", + label: "Amount", + description: "The amount of money involved in the opportunity/deal.", + optional: true, + }, + opportunityStageId: { + propDefinition: [ + app, + "opportunityStageId", + ], + }, + closedDate: { + type: "string", + label: "Closed Date", + description: "The date the opportunity was closed.", + }, + accountId: { + propDefinition: [ + app, + "accountId", + ], + }, + }, + async run({ $ }) { + const { opportunity } = await this.app.createOpportunity({ + $, + data: { + owner_id: this.ownerId, + name: this.name, + amount: this.amount, + opportunity_stage_id: this.opportunityStageId, + closed_date: this.closedDate, + account_id: this.accountId, + }, + }); + + $.export("$summary", `Successfully created opportunity with ID ${opportunity.id}`); + + return opportunity; + }, +}; diff --git a/components/apollo_io/actions/create-update-contact/create-update-contact.mjs b/components/apollo_io/actions/create-update-contact/create-update-contact.mjs new file mode 100644 index 0000000000000..3c23feed64a0e --- /dev/null +++ b/components/apollo_io/actions/create-update-contact/create-update-contact.mjs @@ -0,0 +1,121 @@ +import app from "../../apollo_io.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "apollo_io-create-update-contact", + name: "Create Or Update Contact", + description: "Creates or updates a specific contact. If the contact email already exists, it's updated. Otherwise, a new contact is created. [See the documentation](https://apolloio.github.io/apollo-api-docs/?shell#create-a-contact)", + type: "action", + version: "0.0.1", + props: { + app, + email: { + propDefinition: [ + app, + "email", + ], + }, + firstName: { + propDefinition: [ + app, + "firstName", + ], + }, + lastName: { + propDefinition: [ + app, + "lastName", + ], + }, + title: { + propDefinition: [ + app, + "title", + ], + }, + accountId: { + propDefinition: [ + app, + "accountId", + ], + optional: true, + }, + websiteUrl: { + propDefinition: [ + app, + "websiteUrl", + ], + }, + labelNames: { + propDefinition: [ + app, + "labelNames", + ], + }, + contactStageId: { + propDefinition: [ + app, + "contactStageId", + ], + optional: true, + }, + address: { + propDefinition: [ + app, + "address", + ], + }, + phone: { + propDefinition: [ + app, + "phone", + ], + }, + }, + async run({ $: step }) { + let contact = {}; + let action = "created"; + let data = utils.cleanObject({ + email: this.email, + first_name: this.firstName, + last_name: this.lastName, + title: this.title, + account_id: this.accountId, + website_url: this.websiteUrl, + label_names: this.labelNames, + contact_stage_id: this.contactStageId, + present_raw_address: this.address, + direct_phone: this.phone, + }); + + const { contacts } = await this.app.listContacts({ + params: { + q_keywords: this.email, + }, + }); + + if (contacts.length) { + action = "updated"; + contact = contacts[0]; + + await this.app.updateContact({ + step, + contactId: contact.id, + data: { + ...contact, + ...data, + }, + }); + } else { + const response = await this.app.createContact({ + step, + data, + }); + contact = response.contact; + } + + step.export("$summary", `Successfully ${action} contact with ID ${contact.id}`); + + return contact; + }, +}; diff --git a/components/apollo_io/actions/get-opportunity/get-opportunity.mjs b/components/apollo_io/actions/get-opportunity/get-opportunity.mjs new file mode 100644 index 0000000000000..3ecb429ed1480 --- /dev/null +++ b/components/apollo_io/actions/get-opportunity/get-opportunity.mjs @@ -0,0 +1,29 @@ +import app from "../../apollo_io.app.mjs"; + +export default { + key: "apollo_io-get-opportunity", + name: "Get Opportunity", + description: "Gets a specific opportunity in Apollo.io. [See the documentation](https://apolloio.github.io/apollo-api-docs/?shell#view-opportunity)", + type: "action", + version: "0.0.2", + props: { + app, + opportunityId: { + propDefinition: [ + app, + "opportunityId", + ], + }, + }, + async run({ $ }) { + const { opportunity } = await this.app.getOpportunity({ + $, + opportunityId: this.opportunityId, + }); + + $.export("$summary", `Successfully fetched the opportunity with Id ${this.opportunityId}.`); + + return opportunity; + + }, +}; diff --git a/components/apollo_io/actions/people-enrichment/people-enrichment.mjs b/components/apollo_io/actions/people-enrichment/people-enrichment.mjs index b6e15ba3a33ff..9b2d671bcd12b 100644 --- a/components/apollo_io/actions/people-enrichment/people-enrichment.mjs +++ b/components/apollo_io/actions/people-enrichment/people-enrichment.mjs @@ -5,7 +5,7 @@ export default { name: "People Enrichment", description: "Enriches a person's information, the more information you pass in, the more likely we can find a match. [See the documentation](https://apolloio.github.io/apollo-api-docs/?shell#people-enrichment)", type: "action", - version: "0.0.2", + version: "0.0.5", props: { app, firstName: { @@ -20,20 +20,93 @@ export default { description: "The person's last name", optional: true, }, + name: { + type: "string", + label: "Name", + description: "The full name of the person", + optional: true, + }, email: { type: "string", label: "Email", description: "The person's email", optional: true, }, + hashedEmail: { + type: "string", + label: "Hashed Email", + description: "The hashed email of the person. The email should adhere to either the MD5 or SHA-256 hash format. Example: `8d935115b9ff4489f2d1f9249503cadf` (MD5) or `97817c0c49994eb500ad0a5e7e2d8aed51977b26424d508f66e4e8887746a152` (SHA-256)", + optional: true, + }, organizationName: { type: "string", label: "Organization Name", description: "The person's company name", optional: true, }, + domain: { + type: "string", + label: "Domain", + description: "The domain name for the person's employer. This can be the current employer or a previous employer. Do not include `www.`, the `@` symbol, or similar. Example: `apollo.io` or `microsoft.com`", + optional: true, + }, + personId: { + type: "string", + label: "Person ID", + description: "The Apollo ID for the person", + optional: true, + async options({ page }) { + const { people } = await this.peopleSearch({ + params: { + page: page + 1, + }, + }); + return people?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + linkedinUrl: { + type: "string", + label: "LinkedIn URL", + description: "The URL for the person's LinkedIn profile", + optional: true, + }, + revealPersonalEmails: { + type: "boolean", + label: "Reveal Personal Emails", + description: "Set to `true` if you want to enrich the person's data with personal emails", + optional: true, + }, + revealPhoneNumber: { + type: "boolean", + label: "Reveal Phone Number", + description: "Set to `true` if you want to enrich the person's data with all available phone numbers, including mobile phone numbers", + optional: true, + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.revealPhoneNumber) { + props.webhookUrl = { + type: "string", + label: "Webhook URL", + description: "Enter the webhook URL that specifies where Apollo should send a JSON response that includes the phone number you requested. Required if \"Reveal Phone Number\" is set to `true`", + }; + } + return props; }, methods: { + peopleSearch(args = {}) { + return this.app.post({ + path: "/people/search", + ...args, + }); + }, peopleEnrichment(args = {}) { return this.app.post({ path: "/people/match", @@ -46,8 +119,16 @@ export default { peopleEnrichment, firstName, lastName, + name, email, + hashedEmail, organizationName, + domain, + personId, + linkedinUrl, + revealPersonalEmails, + revealPhoneNumber, + webhookUrl, } = this; const response = await peopleEnrichment({ @@ -55,8 +136,16 @@ export default { data: { first_name: firstName, last_name: lastName, + name, email, + hashed_email: hashedEmail, organization_name: organizationName, + domain, + id: personId, + linkedin_url: linkedinUrl, + reveal_personal_emails: revealPersonalEmails, + reveal_phone_number: revealPhoneNumber, + webhook_url: webhookUrl, }, }); diff --git a/components/apollo_io/actions/search-accounts/search-accounts.mjs b/components/apollo_io/actions/search-accounts/search-accounts.mjs new file mode 100644 index 0000000000000..3ab3e7ec188cd --- /dev/null +++ b/components/apollo_io/actions/search-accounts/search-accounts.mjs @@ -0,0 +1,63 @@ +import app from "../../apollo_io.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "apollo_io-search-accounts", + name: "Search For Accounts", + description: "Search for accounts in Apollo.io. [See the documentation](https://apolloio.github.io/apollo-api-docs/?shell#search-for-accounts)", + type: "action", + version: "0.0.2", + props: { + app, + search: { + type: "string", + label: "Search", + description: "The account's name", + }, + accountStageId: { + propDefinition: [ + app, + "accountStageId", + ], + type: "string[]", + optional: true, + }, + sortByField: { + type: "string", + label: "Sort By Field", + description: "The field to sort the response.", + options: [ + "account_last_activity_date", + "account_created_at", + ], + optional: true, + }, + sortAscending: { + type: "boolean", + label: "Sort Ascending", + description: "The order to be applied to the sort.", + optional: true, + }, + }, + async run({ $ }) { + const resourcesStream = this.app.getResourcesStream({ + resourceFn: this.app.searchAccounts, + resourceFnArgs: { + params: { + q_organization_name: this.search, + account_stage_ids: this.accountStageId, + sort_by_field: this.sortByField, + sort_ascending: this.sortAscending, + }, + }, + resourceName: "accounts", + }); + + const accounts = await utils.streamIterator(resourcesStream); + + $.export("$summary", `Successfully fetched ${accounts.length} accounts.`); + + return accounts; + + }, +}; diff --git a/components/apollo_io/actions/search-contacts/search-contacts.mjs b/components/apollo_io/actions/search-contacts/search-contacts.mjs new file mode 100644 index 0000000000000..64cdf64094156 --- /dev/null +++ b/components/apollo_io/actions/search-contacts/search-contacts.mjs @@ -0,0 +1,66 @@ +import app from "../../apollo_io.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "apollo_io-search-contacts", + name: "Search For Contacts", + description: "Search for contacts in Apollo.io. [See the documentation](https://apolloio.github.io/apollo-api-docs/?shell#search-for-contacts)", + type: "action", + version: "0.0.2", + props: { + app, + search: { + type: "string", + label: "Search", + description: "The contact's name, title, company, or email", + }, + contactStageId: { + propDefinition: [ + app, + "contactStageId", + ], + type: "string[]", + optional: true, + }, + sortByField: { + type: "string", + label: "Sort By Field", + description: "The field to sort the response.", + options: [ + "contact_last_activity_date", + "contact_email_last_opened_at", + "contact_email_last_clicked_at", + "contact_created_at", + "contact_updated_at", + ], + optional: true, + }, + sortAscending: { + type: "boolean", + label: "Sort Ascending", + description: "The order to be applied to the sort.", + optional: true, + }, + }, + async run({ $ }) { + const resourcesStream = this.app.getResourcesStream({ + resourceFn: this.app.searchContacts, + resourceFnArgs: { + params: { + q_keywords: this.search, + contact_stage_ids: this.contactStageId, + sort_by_field: this.sortByField, + sort_ascending: this.sortAscending, + }, + }, + resourceName: "contacts", + }); + + const contacts = await utils.streamIterator(resourcesStream); + + $.export("$summary", `Successfully fetched ${contacts.length} contacts.`); + + return contacts; + + }, +}; diff --git a/components/apollo_io/actions/search-sequences/search-sequences.mjs b/components/apollo_io/actions/search-sequences/search-sequences.mjs new file mode 100644 index 0000000000000..efe150e808fa6 --- /dev/null +++ b/components/apollo_io/actions/search-sequences/search-sequences.mjs @@ -0,0 +1,36 @@ +import app from "../../apollo_io.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "apollo_io-search-sequences", + name: "Search For Sequences", + description: "Search for sequences in Apollo.io. [See the documentation](https://apolloio.github.io/apollo-api-docs/?shell#search-for-sequences)", + type: "action", + version: "0.0.2", + props: { + app, + search: { + type: "string", + label: "Search", + description: "The sequence's name", + }, + }, + async run({ $ }) { + const resourcesStream = this.app.getResourcesStream({ + resourceFn: this.app.listSequences, + resourceFnArgs: { + params: { + q_name: this.search, + }, + }, + resourceName: "emailer_campaigns", + }); + + const sequences = await utils.streamIterator(resourcesStream); + + $.export("$summary", `Successfully fetched ${sequences.length} sequences.`); + + return sequences; + + }, +}; diff --git a/components/apollo_io/actions/update-account-stage/update-account-stage.mjs b/components/apollo_io/actions/update-account-stage/update-account-stage.mjs index 73b4328f38dac..41ddcb560a606 100644 --- a/components/apollo_io/actions/update-account-stage/update-account-stage.mjs +++ b/components/apollo_io/actions/update-account-stage/update-account-stage.mjs @@ -5,7 +5,7 @@ export default { name: "Update Account Stage", description: "Updates the stage of one or more accounts in Apollo.io. [See the documentation](https://apolloio.github.io/apollo-api-docs/?shell#update-account-stage)", type: "action", - version: "0.0.1", + version: "0.0.3", props: { app, accountIds: { diff --git a/components/apollo_io/actions/update-account/update-account.mjs b/components/apollo_io/actions/update-account/update-account.mjs index adaa5fee8ae88..cc4e7f46de2f2 100644 --- a/components/apollo_io/actions/update-account/update-account.mjs +++ b/components/apollo_io/actions/update-account/update-account.mjs @@ -6,7 +6,7 @@ export default { name: "Update Account", description: "Updates an existing account in Apollo.io. [See the documentation](https://apolloio.github.io/apollo-api-docs/?shell#update-an-account)", type: "action", - version: "0.0.1", + version: "0.0.3", props: { app, accountId: { diff --git a/components/apollo_io/actions/update-contact-stage/update-contact-stage.mjs b/components/apollo_io/actions/update-contact-stage/update-contact-stage.mjs index 97c58a09355c7..3ece01c6e686d 100644 --- a/components/apollo_io/actions/update-contact-stage/update-contact-stage.mjs +++ b/components/apollo_io/actions/update-contact-stage/update-contact-stage.mjs @@ -5,7 +5,7 @@ export default { name: "Update Contact Stage", description: "Updates the stage of one or more contacts in Apollo.io. [See the documentation](https://apolloio.github.io/apollo-api-docs/?shell#update-contact-stage)", type: "action", - version: "0.0.1", + version: "0.0.3", props: { app, contactIds: { diff --git a/components/apollo_io/actions/update-contact/update-contact.mjs b/components/apollo_io/actions/update-contact/update-contact.mjs index d500d397877c9..248c6b35d79e7 100644 --- a/components/apollo_io/actions/update-contact/update-contact.mjs +++ b/components/apollo_io/actions/update-contact/update-contact.mjs @@ -6,7 +6,7 @@ export default { name: "Update Contact", description: "Updates an existing contact in Apollo.io. [See the documentation](https://apolloio.github.io/apollo-api-docs/?shell#update-a-contact)", type: "action", - version: "0.0.2", + version: "0.0.4", props: { app, contactId: { diff --git a/components/apollo_io/actions/update-opportunity/update-opportunity.mjs b/components/apollo_io/actions/update-opportunity/update-opportunity.mjs new file mode 100644 index 0000000000000..28ef5aabc6fa2 --- /dev/null +++ b/components/apollo_io/actions/update-opportunity/update-opportunity.mjs @@ -0,0 +1,77 @@ +import app from "../../apollo_io.app.mjs"; + +export default { + key: "apollo_io-update-opportunity", + name: "Update Opportunity", + description: "Updates a new opportunity in Apollo.io. [See the documentation](https://apolloio.github.io/apollo-api-docs/?shell#update-opportunity)", + type: "action", + version: "0.0.2", + props: { + app, + opportunityId: { + propDefinition: [ + app, + "opportunityId", + ], + }, + ownerId: { + propDefinition: [ + app, + "ownerId", + ], + optional: true, + }, + name: { + propDefinition: [ + app, + "name", + ], + description: "The name of the opportunity.", + optional: true, + }, + amount: { + type: "integer", + label: "Amount", + description: "The amount of money involved in the opportunity/deal.", + optional: true, + }, + opportunityStageId: { + propDefinition: [ + app, + "opportunityStageId", + ], + optional: true, + }, + closedDate: { + type: "string", + label: "Closed Date", + description: "The date the opportunity was closed.", + optional: true, + }, + accountId: { + propDefinition: [ + app, + "accountId", + ], + optional: true, + }, + }, + async run({ $ }) { + const { opportunity } = await this.app.updateOpportunity({ + $, + opportunityId: this.opportunityId, + data: { + owner_id: this.ownerId, + name: this.name, + amount: this.amount, + opportunity_stage_id: this.opportunityStageId, + closed_date: this.closedDate, + account_id: this.accountId, + }, + }); + + $.export("$summary", `Successfully updated opportunity with ID ${this.opportunityId}`); + + return opportunity; + }, +}; diff --git a/components/apollo_io/apollo_io.app.mjs b/components/apollo_io/apollo_io.app.mjs index f7e831a764b6d..d604819a9043a 100644 --- a/components/apollo_io/apollo_io.app.mjs +++ b/components/apollo_io/apollo_io.app.mjs @@ -97,6 +97,50 @@ export default { })) || []; }, }, + ownerId: { + type: "string", + label: "Owner ID", + description: "Identifier of the user to associate as owner.", + async options() { + const { users } = await this.listUsers(); + return users?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + opportunityId: { + type: "string", + label: "Opportunity ID", + description: "The ID of the opportunity.", + async options({ page }) { + const { opportunities } = await this.listOpportunities({ + page: page + 1, + }); + return opportunities?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + opportunityStageId: { + type: "string", + label: "Opportunity Stage ID", + description: "The ID of the current stage.", + async options() { + const { opportunity_stages: stages } = await this.listOpportunityStages(); + return stages?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, email: { type: "string", label: "Email", @@ -231,6 +275,24 @@ export default { ...args, }); }, + listUsers(args = {}) { + return this.makeRequest({ + path: "/users/search", + ...args, + }); + }, + listOpportunities(args = {}) { + return this.makeRequest({ + path: "/opportunities/search", + ...args, + }); + }, + listOpportunityStages(args = {}) { + return this.makeRequest({ + path: "/opportunity_stages", + ...args, + }); + }, createContact(args = {}) { return this.makeRequest({ method: "POST", @@ -261,6 +323,30 @@ export default { ...args, }); }, + createOpportunity(args = {}) { + return this.makeRequest({ + method: "POST", + path: "/opportunities", + ...args, + }); + }, + updateOpportunity({ + opportunityId, ...args + }) { + return this.makeRequest({ + method: "PATCH", + path: `/opportunities/${opportunityId}`, + ...args, + }); + }, + getOpportunity({ + opportunityId, ...args + }) { + return this.makeRequest({ + path: `/opportunities/${opportunityId}`, + ...args, + }); + }, updateAccount({ accountId, ...args }) { @@ -286,6 +372,12 @@ export default { ...args, }); }, + searchAccounts(args = {}) { + return this.makeRequest({ + path: "/accounts/search", + ...args, + }); + }, searchContacts(args = {}) { return this.makeRequest({ path: "/contacts/search", diff --git a/components/apollo_io/package.json b/components/apollo_io/package.json index 6884419cc2d1e..ccd044bfd04f2 100644 --- a/components/apollo_io/package.json +++ b/components/apollo_io/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/apollo_io", - "version": "0.2.0", + "version": "0.4.1", "description": "Pipedream Apollo.io Components", "main": "apollo_io.app.mjs", "keywords": [ @@ -13,7 +13,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.6.0", + "@pipedream/platform": "^3.0.3", "md5": "^2.3.0" } } diff --git a/components/apollo_io/sources/account-created/account-created.mjs b/components/apollo_io/sources/account-created/account-created.mjs index eec349c45fc8a..ad39bc2776f78 100644 --- a/components/apollo_io/sources/account-created/account-created.mjs +++ b/components/apollo_io/sources/account-created/account-created.mjs @@ -6,7 +6,7 @@ export default { name: "Account Created", description: "Triggers when an account is created. [See the documentation](https://apolloio.github.io/apollo-api-docs/?shell#search-for-accounts)", type: "source", - version: "0.0.1", + version: "0.0.3", dedupe: "unique", props: { ...common.props, diff --git a/components/apollo_io/sources/account-updated/account-updated.mjs b/components/apollo_io/sources/account-updated/account-updated.mjs index 4746ebf23a3c7..f2b1afe8d22a5 100644 --- a/components/apollo_io/sources/account-updated/account-updated.mjs +++ b/components/apollo_io/sources/account-updated/account-updated.mjs @@ -1,5 +1,5 @@ -import common from "../common/polling.mjs"; import md5 from "md5"; +import common from "../common/polling.mjs"; export default { ...common, @@ -7,7 +7,7 @@ export default { name: "Account Updated", description: "Triggers when an account is updated. [See the documentation](https://apolloio.github.io/apollo-api-docs/?shell#search-for-contacts)", type: "source", - version: "0.0.1", + version: "0.0.3", dedupe: "unique", props: { ...common.props, diff --git a/components/apollo_io/sources/contact-created/contact-created.mjs b/components/apollo_io/sources/contact-created/contact-created.mjs index 1d86efcf4b212..a8978e5c9a349 100644 --- a/components/apollo_io/sources/contact-created/contact-created.mjs +++ b/components/apollo_io/sources/contact-created/contact-created.mjs @@ -6,7 +6,7 @@ export default { name: "Contact Created", description: "Triggers when a contact is created. [See the documentation](https://apolloio.github.io/apollo-api-docs/?shell#search-for-contacts)", type: "source", - version: "0.0.3", + version: "0.0.5", dedupe: "unique", methods: { ...common.methods, diff --git a/components/apollo_io/sources/contact-updated/contact-updated.mjs b/components/apollo_io/sources/contact-updated/contact-updated.mjs index 5c81ead29338e..7b4b7619dee54 100644 --- a/components/apollo_io/sources/contact-updated/contact-updated.mjs +++ b/components/apollo_io/sources/contact-updated/contact-updated.mjs @@ -6,7 +6,7 @@ export default { name: "Contact Updated", description: "Triggers when a contact is updated. [See the documentation](https://apolloio.github.io/apollo-api-docs/?shell#search-for-contacts)", type: "source", - version: "0.0.3", + version: "0.0.5", dedupe: "unique", methods: { ...common.methods, diff --git a/components/appcircle/README.md b/components/appcircle/README.md new file mode 100644 index 0000000000000..320caabe37d38 --- /dev/null +++ b/components/appcircle/README.md @@ -0,0 +1,11 @@ +# Overview + +The Appcircle API provides programmatic access to Appcircle's mobile CI/CD features, allowing you to automate build, test, and deployment processes for mobile apps. With it, you can trigger builds, fetch build outputs, manage distribution profiles, and more directly via HTTP requests. Integrating the Appcircle API with Pipedream unlocks a world of automation possibilities, where you can seamlessly connect your mobile app development workflow with other services and internal systems, streamlining your CI/CD pipeline. + +# Example Use Cases + +- **Automate Mobile App Builds**: Trigger a new build for your mobile app when changes are pushed to a specific branch on GitHub. Use Pipedream's GitHub integration to listen for `push` events and invoke the Appcircle API to start a new build process. + +- **Distribute Builds to Testers**: After a successful build, use the Appcircle API to distribute the app to testers automatically. Set up a Pipedream workflow to watch for successful build notifications and connect with the Slack API to send download links directly to your testing team's Slack channel. + +- **Sync Build Status with Project Management Tools**: Keep your project management tool in sync with the build status of your mobile app. Create a workflow on Pipedream that, upon a build completion, uses the Appcircle API to fetch the status and updates a task in Trello with the results. diff --git a/components/appcircle/package.json b/components/appcircle/package.json index 512e77fdd5dce..888c46048e332 100644 --- a/components/appcircle/package.json +++ b/components/appcircle/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/appcues/README.md b/components/appcues/README.md index 2317af40075bd..44cf6f6ba29f8 100644 --- a/components/appcues/README.md +++ b/components/appcues/README.md @@ -1,17 +1,11 @@ # Overview -Appcues lets you easily add targeted content, onboarding flows, and in-app -messages to improve your app's engagement and retention. +The Appcues API enables developers to enrich and automate the user onboarding and engagement processes. By interfacing with Appcues, you can programmatically manage flows, segments, and events, and integrate user experience data into other systems for analysis or action. This supports creating customized, scalable user journeys and leveraging user interactions for informed decision-making across your business applications. -With Appcues, you can: +# Example Use Cases -- Build targeted content, such as messages, images, and videos, and target it - to specific users based on things like: - - User properties, such as signup date, account type, or number of logins - - Event properties, such as whether a user has completed a task or viewed a - page -- Create onboarding flows to guide new users through your app -- Add in-app messages to highlight features, drive conversions, or announce - changes +- **Sync User Events with a CRM**: Automate the synchronization of user engagement data from Appcues to your CRM platform. For instance, when a user completes an onboarding flow in Appcues, trigger a workflow in Pipedream that updates the user's profile in Salesforce with the new engagement data. -All without having to write a single line of code! +- **Personalized Email Campaigns Based on User Behavior**: Use Appcues events to trigger personalized email campaigns. When a user completes a specific action or flow within your app, use Pipedream to send this data to an email marketing service like Mailchimp to enroll the user in a targeted email sequence. + +- **Slack Notifications for User Milestones**: Enhance team collaboration by sending Slack notifications when users hit key milestones. Set up a Pipedream workflow that listens for `flow_completed` events in Appcues and posts a message to a designated Slack channel, keeping your team informed about user progress in real time. diff --git a/components/appdrag/README.md b/components/appdrag/README.md new file mode 100644 index 0000000000000..0a186cda04ba0 --- /dev/null +++ b/components/appdrag/README.md @@ -0,0 +1,14 @@ +# Overview + +The AppDrag API lets you manage cloud functions, databases, and mail sending features within your AppDrag projects. With Pipedream, you can harness these capabilities to automate workflows such as syncing data between different platforms, processing form submissions, or triggering communication sequences. By connecting AppDrag’s API to Pipedream, you gain the ability to create serverless workflows that can respond to various triggers like webhooks, schedules, or even actions from other apps. + +# Example Use Cases + +- **Automate Database Backups** + Sync your AppDrag database to a cloud storage service like Google Drive. Use Pipedream to schedule daily exports of your database and automatically upload the backup files, ensuring you always have a recent copy of your data stored safely. + +- **Process Contact Form Submissions** + Capture submissions from a web contact form built with AppDrag and use Pipedream to parse and insert the data into a customer relationship management (CRM) system like Salesforce. This workflow can notify your sales team or trigger follow-up actions within the CRM. + +- **Email Campaign Trigger** + Initiate an email sequence in a marketing platform such as Mailchimp whenever a new user signs up through your AppDrag site. Pipedream can listen for new sign-ups via AppDrag’s API and push the contact information to Mailchimp, automating your email marketing efforts. diff --git a/components/applicantstack/applicantstack.app.mjs b/components/applicantstack/applicantstack.app.mjs new file mode 100644 index 0000000000000..b2252ac746914 --- /dev/null +++ b/components/applicantstack/applicantstack.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "applicantstack", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/applicantstack/package.json b/components/applicantstack/package.json new file mode 100644 index 0000000000000..ef27db018bc5d --- /dev/null +++ b/components/applicantstack/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/applicantstack", + "version": "0.0.1", + "description": "Pipedream ApplicantStack Components", + "main": "applicantstack.app.mjs", + "keywords": [ + "pipedream", + "applicantstack" + ], + "homepage": "https://pipedream.com/apps/applicantstack", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/appointedd/README.md b/components/appointedd/README.md new file mode 100644 index 0000000000000..875a0957187c2 --- /dev/null +++ b/components/appointedd/README.md @@ -0,0 +1,11 @@ +# Overview + +The Appointedd API enables you to manage bookings, resources, services, and customers within the Appointedd platform programmatically. Integrating with Pipedream allows you to automate these tasks, connect with multiple apps, and streamline your scheduling and business workflows. With Pipedream's serverless platform, you can harness Appointedd's capabilities to trigger workflows on specific events, sync data across platforms, or handle complex scheduling logic without writing extensive code. + +# Example Use Cases + +- **Automate Booking Confirmations**: Send personalized confirmation emails via SendGrid or Gmail when a new booking is made through Appointedd. Use Pipedream to listen for new booking events and automatically trigger an email confirmation with details. + +- **Sync Appointments to Google Calendar**: Keep your Google Calendar in sync with Appointedd bookings by creating a Pipedream workflow that adds new Appointedd appointments to your Google Calendar and updates them if any changes occur in Appointedd. + +- **Customer Follow-Up Surveys**: After an appointment is completed, trigger a workflow to send a follow-up survey to the customer using Typeform or Google Forms. Use Pipedream to detect when an appointment's status changes to completed and then send a survey link to gather feedback. diff --git a/components/appointo/appointo.app.mjs b/components/appointo/appointo.app.mjs new file mode 100644 index 0000000000000..ffb88733b6acc --- /dev/null +++ b/components/appointo/appointo.app.mjs @@ -0,0 +1,78 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; + +export default { + type: "app", + app: "appointo", + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "APPOINTO-TOKEN": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + listBookings(args = {}) { + return this._makeRequest({ + path: "/bookings", + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, + max = constants.DEFAULT_MAX, + }) { + let offset = 0; + let resourcesCount = 0; + + while (true) { + const nextResources = + await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + limit: constants.DEFAULT_LIMIT, + offset, + }, + }); + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + yield resource; + resourcesCount += 1; + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + if (nextResources.length < constants.DEFAULT_LIMIT) { + console.log("No next page found"); + return; + } + + offset += constants.DEFAULT_LIMIT; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); + }, + }, +}; diff --git a/components/appointo/common/constants.mjs b/components/appointo/common/constants.mjs new file mode 100644 index 0000000000000..4fe0801a6e413 --- /dev/null +++ b/components/appointo/common/constants.mjs @@ -0,0 +1,11 @@ +const BASE_URL = "https://app.appointo.me"; +const VERSION_PATH = "/api"; +const DEFAULT_LIMIT = 100; +const DEFAULT_MAX = 600; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_LIMIT, + DEFAULT_MAX, +}; diff --git a/components/appointo/common/utils.mjs b/components/appointo/common/utils.mjs new file mode 100644 index 0000000000000..903b2593ed3c2 --- /dev/null +++ b/components/appointo/common/utils.mjs @@ -0,0 +1,11 @@ +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +export default { + iterate, +}; diff --git a/components/appointo/package.json b/components/appointo/package.json new file mode 100644 index 0000000000000..cc2e50bf752f0 --- /dev/null +++ b/components/appointo/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/appointo", + "version": "0.1.0", + "description": "Pipedream Appointo Components", + "main": "appointo.app.mjs", + "keywords": [ + "pipedream", + "appointo" + ], + "homepage": "https://pipedream.com/apps/appointo", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/appointo/sources/common/polling.mjs b/components/appointo/sources/common/polling.mjs new file mode 100644 index 0000000000000..1fd214ac2babb --- /dev/null +++ b/components/appointo/sources/common/polling.mjs @@ -0,0 +1,62 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../appointo.app.mjs"; + +export default { + props: { + app, + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + isResourceRelevant() { + return true; + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + async processResources(resources) { + const { + isResourceRelevant, + processResource, + } = this; + + Array.from(resources) + .filter(isResourceRelevant) + .forEach(processResource); + }, + }, + async run() { + const { + app, + getResourcesFn, + getResourcesFnArgs, + processResources, + } = this; + + const resources = await app.paginate({ + resourcesFn: getResourcesFn(), + resourcesFnArgs: getResourcesFnArgs(), + }); + + processResources(resources); + }, +}; diff --git a/components/appointo/sources/new-booking-created/new-booking-created.mjs b/components/appointo/sources/new-booking-created/new-booking-created.mjs new file mode 100644 index 0000000000000..ec997c90a1bef --- /dev/null +++ b/components/appointo/sources/new-booking-created/new-booking-created.mjs @@ -0,0 +1,32 @@ +import common from "../common/polling.mjs"; + +export default { + ...common, + key: "appointo-new-booking-created", + name: "New Booking Created", + description: "Emit new event when a booking is created in Appointo. [See the documentation](https://api-docs.appointo.me/reference/api-reference/bookings)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + isResourceRelevant(resource) { + return Array.isArray(resource.customers) && resource.customers.length > 0; + }, + getResourcesFn() { + return this.app.listBookings; + }, + getResourcesFnArgs() { + return { + debug: true, + }; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Booking: ${resource.id}`, + ts: Date.parse(resource.created_at), + }; + }, + }, +}; diff --git a/components/appveyor/README.md b/components/appveyor/README.md index 0d1c18cfca5bc..3c0cfedaa5271 100644 --- a/components/appveyor/README.md +++ b/components/appveyor/README.md @@ -1,13 +1,11 @@ # Overview -The AppVeyor API enables you to build, test and deploy your applications on the -AppVeyor platform. +The AppVeyor API grants programmatic access to AppVeyor's continuous integration and deployment services, empowering developers to trigger builds, fetch build history, deploy applications, and manage projects and account settings. With Pipedream's serverless platform, you can craft custom workflows that react to AppVeyor events or manipulate AppVeyor's pipeline dynamically, streamlining your CI/CD process by interfacing with other tools in the software development lifecycle. -You can use the API to: +# Example Use Cases -- Create and manage projects -- Build and test your applications -- Deploy your applications -- View and manage your builds -- View and manage your tests -- View and manage your deployments +- **Automated Deployment Triggering:** When a new tag is pushed to your GitHub repository, a Pipedream workflow can automatically trigger a new build in AppVeyor, ensuring that your latest code is always being deployed without manual intervention. + +- **Dynamic Build Notifications:** Configure a workflow on Pipedream to listen for build status webhook events from AppVeyor. Upon build completion, whether successful or failed, automatically send detailed notifications through Slack, Discord, or even SMS, keeping your team informed in real-time. + +- **Project Synchronization and Backups:** Use Pipedream to periodically call AppVeyor's API to fetch the configuration of all projects. Store this data in a Google Sheet or sync it to a GitHub repository to maintain a backup of your CI/CD configuration, making disaster recovery smooth and quick. diff --git a/components/appveyor/package.json b/components/appveyor/package.json new file mode 100644 index 0000000000000..e3f85ca30de7a --- /dev/null +++ b/components/appveyor/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/appveyor", + "version": "0.6.0", + "description": "Pipedream appveyor Components", + "main": "appveyor.app.mjs", + "keywords": [ + "pipedream", + "appveyor" + ], + "homepage": "https://pipedream.com/apps/appveyor", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/appwrite/README.md b/components/appwrite/README.md new file mode 100644 index 0000000000000..1e16217e10a7d --- /dev/null +++ b/components/appwrite/README.md @@ -0,0 +1,11 @@ +# Overview + +Appwrite is a secure backend server geared towards providing developers with a set of easy-to-use APIs to manage core backend needs such as user authentication, databases, file storage, and more. With Appwrite's API, you can streamline backend development processes, ensuring quick and secure application development. Integrating Appwrite with Pipedream allows for the automation of processes like user management, data manipulation, and real-time updates cross-platform. + +# Example Use Cases + +- **Automated User Onboarding**: - When a new user signs up in an application, trigger a workflow in Pipedream that utilizes the Appwrite API to create the user in Appwrite's database, send a welcome email via SendGrid, and log this event in a Google Sheets for record-keeping. + +- **Real-time Data Backup**: - Set up a workflow on Pipedream that triggers whenever changes are made to a database in Appwrite. This workflow can automatically backup the updated data to an AWS S3 bucket, ensuring that there's always a recent backup available without manual intervention. + +- **Issue Tracker Integration**: - Connect Appwrite with GitHub via Pipedream to automate issue tracking. Whenever a new issue is reported in your Appwrite application, automatically create a corresponding issue in a GitHub repository, and notify the project manager via Slack to take immediate action. diff --git a/components/appwrite/actions/create-account/create-account.mjs b/components/appwrite/actions/create-account/create-account.mjs new file mode 100644 index 0000000000000..8094e81738680 --- /dev/null +++ b/components/appwrite/actions/create-account/create-account.mjs @@ -0,0 +1,51 @@ +import app from "../../appwrite.app.mjs"; + +export default { + key: "appwrite-create-account", + name: "Create Account", + description: "Register a new account in your project. [See the documentation](https://appwrite.io/docs/references/cloud/client-web/account#create)", + version: "0.0.1", + type: "action", + props: { + app, + email: { + propDefinition: [ + app, + "email", + ], + }, + password: { + propDefinition: [ + app, + "password", + ], + }, + userId: { + propDefinition: [ + app, + "userId", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createAccount({ + $, + data: { + email: this.email, + password: this.password, + userId: this.userId, + name: this.name, + }, + }); + + $.export("$summary", `Successfully created account '${response.name}'`); + + return response; + }, +}; diff --git a/components/appwrite/actions/create-team/create-team.mjs b/components/appwrite/actions/create-team/create-team.mjs new file mode 100644 index 0000000000000..5a6ed1ddf555b --- /dev/null +++ b/components/appwrite/actions/create-team/create-team.mjs @@ -0,0 +1,44 @@ +import app from "../../appwrite.app.mjs"; + +export default { + key: "appwrite-create-team", + name: "Create Team", + description: "Create a new team. [See the documentation](https://appwrite.io/docs/references/cloud/client-rest/teams#create)", + version: "0.0.1", + type: "action", + props: { + app, + teamId: { + type: "string", + label: "Team ID", + description: "Choose a custom ID or generate a random ID with ID.unique(). Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore.", + }, + name: { + propDefinition: [ + app, + "name", + ], + description: "Name of the team", + }, + roles: { + propDefinition: [ + app, + "roles", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createTeam({ + $, + data: { + teamId: this.teamId, + name: this.name, + roles: this.roles, + }, + }); + + $.export("$summary", `Successfully created team '${response.name}'`); + + return response; + }, +}; diff --git a/components/appwrite/actions/get-members/get-members.mjs b/components/appwrite/actions/get-members/get-members.mjs new file mode 100644 index 0000000000000..50932846551be --- /dev/null +++ b/components/appwrite/actions/get-members/get-members.mjs @@ -0,0 +1,28 @@ +import app from "../../appwrite.app.mjs"; + +export default { + key: "appwrite-get-members", + name: "Get Members", + description: "Get accounts setup information. [See the documentation](https://appwrite.io/docs/references/cloud/client-rest/teams#listMemberships)", + version: "0.0.1", + type: "action", + props: { + app, + teamId: { + propDefinition: [ + app, + "teamId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getMembers({ + $, + teamId: this.teamId, + }); + + $.export("$summary", `Successfully retrieved ${response.total} members`); + + return response; + }, +}; diff --git a/components/appwrite/appwrite.app.mjs b/components/appwrite/appwrite.app.mjs new file mode 100644 index 0000000000000..71e070a7adccf --- /dev/null +++ b/components/appwrite/appwrite.app.mjs @@ -0,0 +1,99 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "appwrite", + propDefinitions: { + teamId: { + type: "string", + label: "Team ID", + description: "Team ID. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore", + async options() { + const response = await this.getTeams({}); + const teamsIds = response.teams; + return teamsIds.map(({ + $id, name, + }) => ({ + value: $id, + label: name, + })); + }, + }, + email: { + type: "string", + label: "Email", + description: "User email", + }, + password: { + type: "string", + label: "Password", + description: "User password. Must be at least 8 characters", + }, + userId: { + type: "string", + label: "User ID", + description: "Choose a custom ID. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore", + }, + name: { + type: "string", + label: "Name", + description: "User name", + }, + roles: { + type: "string[]", + label: "Roles", + description: "Use this prop to set the roles in the team for the user who created it.", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://cloud.appwrite.io/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "X-Appwrite-Project": `${this.$auth.project_id}`, + "X-Appwrite-Key": `${this.$auth.api_key}`, + }, + }); + }, + async createAccount(args = {}) { + return this._makeRequest({ + method: "post", + path: "/account", + ...args, + }); + }, + async createTeam(args = {}) { + return this._makeRequest({ + method: "post", + path: "/teams", + ...args, + }); + }, + async getMembers({ + teamId, ...args + }) { + return this._makeRequest({ + path: `/teams/${teamId}/memberships`, + ...args, + }); + }, + async getTeams(args = {}) { + return this._makeRequest({ + path: "/teams", + ...args, + }); + }, + }, +}; diff --git a/components/appwrite/package.json b/components/appwrite/package.json new file mode 100644 index 0000000000000..258670feeb159 --- /dev/null +++ b/components/appwrite/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/appwrite", + "version": "0.1.0", + "description": "Pipedream Appwrite Components", + "main": "appwrite.app.mjs", + "keywords": [ + "pipedream", + "appwrite" + ], + "homepage": "https://pipedream.com/apps/appwrite", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} diff --git a/components/arcgis_online/README.md b/components/arcgis_online/README.md new file mode 100644 index 0000000000000..254c9f3228a96 --- /dev/null +++ b/components/arcgis_online/README.md @@ -0,0 +1,11 @@ +# Overview + +The ArcGIS Online API lets you work with ESRI's platform for mapping and spatial analysis. In Pipedream, harness this API to craft workflows combining GIS data management, location-based analytics, and automated mapping tasks. Pipedream's serverless architecture means you can trigger these workflows via HTTP requests, schedule them, or even fire them in response to emails or form submissions. + +# Example Use Cases + +- **Spatial Data Updates on Schedule**: Automate the process of updating GIS layers in ArcGIS Online by scheduling workflows in Pipedream. Fetch the latest data from an external database or API, transform it as needed, and push updates to keep maps current. + +- **Incident Reporting and Mapping**: Create a workflow triggered by a form submission or email that contains location data about incidents. Pipedream can parse this data, create a new feature in an ArcGIS layer, and send notifications to relevant stakeholders with the updated map link. + +- **Geo-Triggered Alerts with Twilio**: For real-time alerts based on geographic triggers, use Pipedream to monitor a dataset in ArcGIS Online for changes. If a feature enters a specified zone, automatically send an SMS via Twilio to notify personnel or customers in the affected area. diff --git a/components/arcgis_online/arcgis_online.app.mjs b/components/arcgis_online/arcgis_online.app.mjs new file mode 100644 index 0000000000000..c973b9d9b4b69 --- /dev/null +++ b/components/arcgis_online/arcgis_online.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "arcgis_online", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/arcgis_online/package.json b/components/arcgis_online/package.json new file mode 100644 index 0000000000000..a823f5e67b70e --- /dev/null +++ b/components/arcgis_online/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/arcgis_online", + "version": "0.0.1", + "description": "Pipedream ArcGIS Online Components", + "main": "arcgis_online.app.mjs", + "keywords": [ + "pipedream", + "arcgis_online" + ], + "homepage": "https://pipedream.com/apps/arcgis_online", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/are_na/actions/create-channel/create-channel.mjs b/components/are_na/actions/create-channel/create-channel.mjs new file mode 100644 index 0000000000000..a532851db5940 --- /dev/null +++ b/components/are_na/actions/create-channel/create-channel.mjs @@ -0,0 +1,35 @@ +import app from "../../are_na.app.mjs"; + +export default { + key: "are_na-create-channel", + name: "Create Channel", + description: "Create a new channel in Are.na. [See the documentation](https://dev.are.na/documentation/channels#Block45041)", + version: "0.0.1", + type: "action", + props: { + app, + channelTitle: { + propDefinition: [ + app, + "channelTitle", + ], + }, + channelStatus: { + propDefinition: [ + app, + "channelStatus", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createChannel({ + $, + params: { + title: this.channelTitle, + status: this.channelStatus, + }, + }); + $.export("$summary", `Successfully created channel '${this.channelTitle}' with slug '${response.slug}'`); + return response; + }, +}; diff --git a/components/are_na/actions/delete-channel/delete-channel.mjs b/components/are_na/actions/delete-channel/delete-channel.mjs new file mode 100644 index 0000000000000..3b7c992615237 --- /dev/null +++ b/components/are_na/actions/delete-channel/delete-channel.mjs @@ -0,0 +1,26 @@ +import app from "../../are_na.app.mjs"; + +export default { + key: "are_na-delete-channel", + name: "Delete Channel", + description: "Delete a Channel with the specified slug. [See the documentation](https://dev.are.na/documentation/channels#Block45049)", + version: "0.0.1", + type: "action", + props: { + app, + channelSlug: { + propDefinition: [ + app, + "channelSlug", + ], + }, + }, + async run({ $ }) { + const response = await this.app.deleteChannel({ + $, + slug: this.channelSlug, + }); + $.export("$summary", `Successfully deleted channel with slug: '${this.channelSlug}'`); + return response; + }, +}; diff --git a/components/are_na/actions/get-authenticated-user/get-authenticated-user.mjs b/components/are_na/actions/get-authenticated-user/get-authenticated-user.mjs new file mode 100644 index 0000000000000..95143461cb429 --- /dev/null +++ b/components/are_na/actions/get-authenticated-user/get-authenticated-user.mjs @@ -0,0 +1,20 @@ +import are_na from "../../are_na.app.mjs"; + +export default { + key: "are_na-get-authenticated-user", + name: "Get Authenticated User", + description: "Returns the currently authenticated user", + version: "0.0.2", + type: "action", + props: { + are_na, + }, + async run({ $ }) { + const response = await this.are_na.getAuthenticatedUser({ + $, + }); + + $.export("$summary", "Successfully retrieved user"); + return response; + }, +}; diff --git a/components/are_na/actions/search/search.mjs b/components/are_na/actions/search/search.mjs new file mode 100644 index 0000000000000..6f0ecdaa9147a --- /dev/null +++ b/components/are_na/actions/search/search.mjs @@ -0,0 +1,27 @@ +import are_na from "../../are_na.app.mjs"; + +export default { + key: "are_na-search", + name: "Perform a Search", + description: "Search across all of Are.na (returns channels, blocks and users that match the query)", + version: "0.0.2", + type: "action", + props: { + are_na, + query: { + propDefinition: [ + are_na, + "query", + ], + }, + }, + async run({ $ }) { + const response = await this.are_na.search({ + $, + query: this.query, + }); + + $.export("$summary", `Search returned ${response.length} results`); + return response; + }, +}; diff --git a/components/are_na/actions/update-channel/update-channel.mjs b/components/are_na/actions/update-channel/update-channel.mjs new file mode 100644 index 0000000000000..754163c814eac --- /dev/null +++ b/components/are_na/actions/update-channel/update-channel.mjs @@ -0,0 +1,42 @@ +import app from "../../are_na.app.mjs"; + +export default { + key: "are_na-update-channel", + name: "Update Channel", + description: "Update a Channel with the specified slug. [See the documentation](https://dev.are.na/documentation/channels#Block45048)", + version: "0.0.1", + type: "action", + props: { + app, + channelSlug: { + propDefinition: [ + app, + "channelSlug", + ], + }, + channelTitle: { + propDefinition: [ + app, + "channelTitle", + ], + }, + channelStatus: { + propDefinition: [ + app, + "channelStatus", + ], + }, + }, + async run({ $ }) { + const response = await this.app.updateChannel({ + $, + slug: this.channelSlug, + params: { + title: this.channelTitle, + status: this.channelStatus, + }, + }); + $.export("$summary", `Successfully updated channel with slug: '${this.channelSlug}'`); + return response; + }, +}; diff --git a/components/are_na/are_na.app.mjs b/components/are_na/are_na.app.mjs new file mode 100644 index 0000000000000..3e2a6c4a01134 --- /dev/null +++ b/components/are_na/are_na.app.mjs @@ -0,0 +1,106 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "are_na", + propDefinitions: { + channelSlug: { + type: "string", + label: "Channel Slug", + description: "Slug of the Channel", + async options() { + const response = await this.getChannels(); + const channelsSlugs = response.channels; + return channelsSlugs.map(({ + slug, title, + }) => ({ + value: slug, + label: title, + })); + }, + }, + channelTitle: { + type: "string", + label: "Channel Tittle", + description: "Tittle of the Channel", + }, + channelStatus: { + type: "string", + label: "Channel Status", + description: "Status of the Channel", + options: constants.CHANNEL_STATUS, + }, + query: { + type: "string", + label: "Query", + description: "The search query", + }, + }, + methods: { + _baseUrl() { + return "http://api.are.na/v2"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + getAuthenticatedUser(args = {}) { + return this._makeRequest({ + path: "/me", + args, + }); + }, + search({ + query, ...args + }) { + return this._makeRequest({ + path: `/search?q=${query}`, + args, + }); + }, + async createChannel(args = {}) { + return this._makeRequest({ + method: "post", + path: "/channels", + ...args, + }); + }, + async deleteChannel({ + slug, ...args + }) { + return this._makeRequest({ + method: "delete", + path: `/channels/${slug}`, + ...args, + }); + }, + async updateChannel({ + slug, ...args + }) { + return this._makeRequest({ + method: "put", + path: `/channels/${slug}`, + ...args, + }); + }, + async getChannels(args = {}) { + return this._makeRequest({ + path: "/search/channels", + ...args, + }); + }, + }, +}; diff --git a/components/are_na/common/constants.mjs b/components/are_na/common/constants.mjs new file mode 100644 index 0000000000000..8ec074acf68ef --- /dev/null +++ b/components/are_na/common/constants.mjs @@ -0,0 +1,7 @@ +export default { + CHANNEL_STATUS: [ + "public", + "closed", + "private", + ], +}; diff --git a/components/are_na/package.json b/components/are_na/package.json new file mode 100644 index 0000000000000..ca42f1037a4cc --- /dev/null +++ b/components/are_na/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/are_na", + "version": "0.1.0", + "description": "Pipedream Are.na Components", + "main": "are_na.app.mjs", + "keywords": [ + "pipedream", + "are_na" + ], + "homepage": "https://pipedream.com/apps/are_na", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/aroflo/README.md b/components/aroflo/README.md index 50148d362c831..af30309367468 100644 --- a/components/aroflo/README.md +++ b/components/aroflo/README.md @@ -1,11 +1,11 @@ # Overview -Aroflo provides a set of APIs that lets you build various applications that can -be used to manage your business processes. Here are some examples of what you -can build using the Aroflo API: - -- An application to manage your customer data -- An application to track your sales and leads -- An application to manage your inventory -- An application to track your project progress -- An application to manage your financial data +Aroflo's API offers the power to streamline operations by automating tasks and integrating with a wide variety of systems. Through Pipedream, you can leverage this to synchronize data across platforms, trigger workflows based on specific events, and manipulate Aroflo data in real-time. Whether updating project statuses, managing assets, or automating invoicing and reporting, Pipedream's serverless platform can facilitate a seamless link between Aroflo and other services. + +# Example Use Cases + +- **Task Automation Upon Project Completion**: When a project is marked as complete in Aroflo, a Pipedream workflow could automatically generate an invoice in accounting software like QuickBooks, send a summary report email via SendGrid, and update a project management dashboard on Google Sheets. + +- **Inventory Management**: A workflow might monitor stock levels in Aroflo; when supplies dip below a certain threshold, it could place an order with a supplier's API, notify the purchasing team via a Slack message, and log the event in a Microsoft Excel spreadsheet for future tracking. + +- **Client Communication and Scheduling**: After a new work order is created in Aroflo, a Pipedream workflow could schedule the job in the staff's Google Calendar, send an appointment confirmation to the client using Twilio SMS, and create a follow-up reminder in a CRM like Salesforce for post-service feedback. diff --git a/components/aroflo/package.json b/components/aroflo/package.json new file mode 100644 index 0000000000000..8985b4c22a4fc --- /dev/null +++ b/components/aroflo/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/aroflo", + "version": "0.6.0", + "description": "Pipedream aroflo Components", + "main": "aroflo.app.mjs", + "keywords": [ + "pipedream", + "aroflo" + ], + "homepage": "https://pipedream.com/apps/aroflo", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/arxiv/README.md b/components/arxiv/README.md new file mode 100644 index 0000000000000..368c532e5f060 --- /dev/null +++ b/components/arxiv/README.md @@ -0,0 +1,11 @@ +# Overview + +The arXiv API provides programmatic access to the vast repository of electronic preprints and publications in various fields of science and academia. Leveraging this API within Pipedream allows you to automate workflows involving scholarly communication, such as tracking new publications in specific domains, analyzing research trends, or notifying collaborators about recent papers. Pipedream's serverless platform can trigger actions based on new submissions to arXiv, process metadata, and connect with other apps to create powerful integrations for researchers and data enthusiasts. + +# Example Use Cases + +- **Daily Research Digest**: Build a workflow that fetches the latest papers from a selected arXiv category every day and sends a digest to your email. This keeps you updated on recent publications without manual searching. + +- **New Paper Alert**: Set up a real-time notification system that monitors arXiv for new papers from specific authors or keywords. When a new paper matches your criteria, receive an alert through Slack or another messaging platform. + +- **Literature Review Automation**: Create a workflow that compiles new papers related to your research into a Google Sheets document, including titles, authors, and abstracts. This can serve as an auto-updating literature review to aid in research projects. diff --git a/components/arxiv/package.json b/components/arxiv/package.json index 016818d2a5c51..40b892640b977 100644 --- a/components/arxiv/package.json +++ b/components/arxiv/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/aryn/aryn.app.mjs b/components/aryn/aryn.app.mjs new file mode 100644 index 0000000000000..78792b388e6aa --- /dev/null +++ b/components/aryn/aryn.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "aryn", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/aryn/package.json b/components/aryn/package.json new file mode 100644 index 0000000000000..c21b0da01e26b --- /dev/null +++ b/components/aryn/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/aryn", + "version": "0.0.1", + "description": "Pipedream Aryn Components", + "main": "aryn.app.mjs", + "keywords": [ + "pipedream", + "aryn" + ], + "homepage": "https://pipedream.com/apps/aryn", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/asana/README.md b/components/asana/README.md index 11299bc6fb25c..5313e9d815ff3 100644 --- a/components/asana/README.md +++ b/components/asana/README.md @@ -1,6 +1,11 @@ # Overview -Using the Asana API, you can build software that integrates with Asana to help -your team track their work. For example, you could build a tool to help your -team plan their work for the week, or a reporting tool to help you track -progress on a project. +The Asana API taps into the feature-rich project management platform, allowing you to automate tasks, sync data across apps, and enhance team collaboration. With the Asana API on Pipedream, you can create tasks, update project timelines, and trigger workflows from events within Asana—like new tasks or comments—streamlining your project management processes. Pipedream’s serverless platform empowers you to build complex workflows with minimal overhead, integrating Asana with numerous other services to maximize productivity and reduce manual workload. + +# Example Use Cases + +- **Task Creation from Emails**: Automatically create Asana tasks from incoming emails. When you receive an email tagged with a specific label or keyword, a Pipedream workflow can parse the email content and create a new task in Asana with appropriate details, ensuring that action items from emails are never missed. + +- **GitHub Commit Linked Project Updates**: Keep your development work in sync with project management by triggering Asana task updates when new commits are pushed to a GitHub repository. This Pipedream workflow can be set to update a task's status, add comments, or create subtasks, providing real-time project tracking for software teams. + +- **Slack Notifications for Task Completion**: Improve team communication by sending a notification to a designated Slack channel whenever a task in Asana is completed. This ensures all team members are immediately updated about project progress and can celebrate milestones or quickly move on to the next task. diff --git a/components/asana/actions/add-task-to-section/add-task-to-section.mjs b/components/asana/actions/add-task-to-section/add-task-to-section.mjs index c734c4cc648f9..ded1148bfe383 100644 --- a/components/asana/actions/add-task-to-section/add-task-to-section.mjs +++ b/components/asana/actions/add-task-to-section/add-task-to-section.mjs @@ -5,7 +5,7 @@ export default { name: "Add Task To Section", description: "Add a task to a specific, existing section. This will remove the task from other sections of the project. [See the documentation](https://developers.asana.com/docs/add-task-to-section)", key: "asana-add-task-to-section", - version: "0.2.5", + version: "0.2.7", type: "action", props: { ...common.props, diff --git a/components/asana/actions/create-project/create-project.mjs b/components/asana/actions/create-project/create-project.mjs index ccd5cd888311b..c62e9cb8b6e70 100644 --- a/components/asana/actions/create-project/create-project.mjs +++ b/components/asana/actions/create-project/create-project.mjs @@ -5,7 +5,7 @@ export default { key: "asana-create-project", name: "Create Project", description: "Create a new project in a workspace or team. [See the documentation](https://developers.asana.com/docs/create-a-project)", - version: "0.9.5", + version: "0.10.0", type: "action", props: { asana, @@ -93,6 +93,9 @@ export default { propDefinition: [ asana, "users", + ({ workspace }) => ({ + workspace, + }), ], }, html_notes: { @@ -109,6 +112,9 @@ export default { propDefinition: [ asana, "users", + ({ workspace }) => ({ + workspace, + }), ], }, }, diff --git a/components/asana/actions/create-subtask/create-subtask.mjs b/components/asana/actions/create-subtask/create-subtask.mjs index f3784333db6ae..c1f63b3a0ef08 100644 --- a/components/asana/actions/create-subtask/create-subtask.mjs +++ b/components/asana/actions/create-subtask/create-subtask.mjs @@ -5,7 +5,7 @@ export default { key: "asana-create-subtask", name: "Create Subtask", description: "Creates a new subtask and adds it to the parent task. [See the documentation](https://developers.asana.com/docs/create-a-subtask)", - version: "0.3.5", + version: "0.4.0", type: "action", props: { ...common.props, @@ -34,6 +34,9 @@ export default { propDefinition: [ asana, "users", + ({ workspace }) => ({ + workspace, + }), ], }, assignee_section: { @@ -75,6 +78,9 @@ export default { propDefinition: [ asana, "users", + ({ workspace }) => ({ + workspace, + }), ], }, html_notes: { diff --git a/components/asana/actions/create-task-comment/create-task-comment.mjs b/components/asana/actions/create-task-comment/create-task-comment.mjs index 6a6dd432b0fa7..cfb5f5f42f7a3 100644 --- a/components/asana/actions/create-task-comment/create-task-comment.mjs +++ b/components/asana/actions/create-task-comment/create-task-comment.mjs @@ -5,7 +5,7 @@ export default { key: "asana-create-task-comment", name: "Create Task Comment", description: "Adds a comment to a task. [See the documentation](https://developers.asana.com/docs/create-a-story-on-a-task)", - version: "0.2.5", + version: "0.2.7", type: "action", props: { ...common.props, diff --git a/components/asana/actions/create-task-from-template/create-task-from-template.mjs b/components/asana/actions/create-task-from-template/create-task-from-template.mjs new file mode 100644 index 0000000000000..a2962e3fdf97c --- /dev/null +++ b/components/asana/actions/create-task-from-template/create-task-from-template.mjs @@ -0,0 +1,40 @@ +import common from "../common/common.mjs"; + +export default { + name: "Create Task from Template", + key: "asana-create-task-from-template", + description: "Creates a new task from a task template. [See the documentation](https://developers.asana.com/reference/instantiatetask)", + version: "0.0.2", + type: "action", + props: { + ...common.props, + taskTemplateId: { + propDefinition: [ + common.props.asana, + "taskTemplate", + (c) => ({ + project: c.project, + }), + ], + }, + name: { + type: "string", + label: "Name", + description: "The name of the new task. If not provided, the name of the task template will be used.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.asana.createTaskFromTemplate({ + $, + taskTemplateId: this.taskTemplateId, + data: { + data: { + name: this.name, + }, + }, + }); + $.export("$summary", `Successfully created task with ID ${response.data.new_task.gid}`); + return response; + }, +}; diff --git a/components/asana/actions/create-task/create-task.mjs b/components/asana/actions/create-task/create-task.mjs index d372fb6df0f03..d9f782979045b 100644 --- a/components/asana/actions/create-task/create-task.mjs +++ b/components/asana/actions/create-task/create-task.mjs @@ -5,7 +5,7 @@ export default { key: "asana-create-task", name: "Create Task", description: "Creates a new task. [See the documentation](https://developers.asana.com/docs/create-a-task)", - version: "0.3.5", + version: "0.4.0", type: "action", props: { ...common.props, @@ -29,6 +29,9 @@ export default { propDefinition: [ asana, "users", + ({ workspace }) => ({ + workspace, + }), ], }, assignee_section: { @@ -70,6 +73,9 @@ export default { propDefinition: [ asana, "users", + ({ workspace }) => ({ + workspace, + }), ], }, html_notes: { diff --git a/components/asana/actions/delete-task/delete-task.mjs b/components/asana/actions/delete-task/delete-task.mjs index bd2291a99272f..239ae416485ea 100644 --- a/components/asana/actions/delete-task/delete-task.mjs +++ b/components/asana/actions/delete-task/delete-task.mjs @@ -5,7 +5,7 @@ export default { key: "asana-delete-task", name: "Delete Task", description: "Deletes a specific and existing task. [See the documentation](https://developers.asana.com/docs/delete-a-task)", - version: "0.0.6", + version: "0.0.8", type: "action", props: { ...common.props, diff --git a/components/asana/actions/find-task-by-id/find-task-by-id.mjs b/components/asana/actions/find-task-by-id/find-task-by-id.mjs index 972c734f6d982..c8f80c8d2cbdf 100644 --- a/components/asana/actions/find-task-by-id/find-task-by-id.mjs +++ b/components/asana/actions/find-task-by-id/find-task-by-id.mjs @@ -5,7 +5,7 @@ export default { key: "asana-find-task-by-id", name: "Find Task by ID", description: "Searches for a task by id. Returns the complete task record for a single task. [See the documentation](https://developers.asana.com/docs/get-a-task)", - version: "0.2.5", + version: "0.2.7", type: "action", props: { ...common.props, diff --git a/components/asana/actions/get-tasks-from-task-list/get-tasks-from-task-list.mjs b/components/asana/actions/get-tasks-from-task-list/get-tasks-from-task-list.mjs index 28b5f40bcefe7..6cfef06b80977 100644 --- a/components/asana/actions/get-tasks-from-task-list/get-tasks-from-task-list.mjs +++ b/components/asana/actions/get-tasks-from-task-list/get-tasks-from-task-list.mjs @@ -4,7 +4,7 @@ export default { key: "asana-get-tasks-from-task-list", name: "Get Tasks From Task List", description: "Returns the compact list of tasks in a user’s My Tasks list. [See the documentation](https://developers.asana.com/reference/gettasksforusertasklist)", - version: "0.0.3", + version: "0.0.5", type: "action", props: { ...common.props, diff --git a/components/asana/actions/search-projects/search-projects.mjs b/components/asana/actions/search-projects/search-projects.mjs index ed377b20ed243..9e05045d037ab 100644 --- a/components/asana/actions/search-projects/search-projects.mjs +++ b/components/asana/actions/search-projects/search-projects.mjs @@ -3,7 +3,7 @@ import asana from "../../asana.app.mjs"; export default { type: "action", key: "asana-search-projects", - version: "0.2.5", + version: "0.2.7", name: "Search Projects", description: "Finds an existing project. [See the documentation](https://developers.asana.com/docs/get-multiple-projects)", props: { diff --git a/components/asana/actions/search-sections/search-sections.mjs b/components/asana/actions/search-sections/search-sections.mjs index ead87a5287a08..6e434e50b3493 100644 --- a/components/asana/actions/search-sections/search-sections.mjs +++ b/components/asana/actions/search-sections/search-sections.mjs @@ -4,7 +4,7 @@ export default { key: "asana-search-sections", name: "Search Sections", description: "Searches for a section by name within a particular project. [See the documentation](https://developers.asana.com/docs/get-sections-in-a-project)", - version: "0.2.5", + version: "0.2.7", type: "action", props: { ...common.props, diff --git a/components/asana/actions/search-tasks/search-tasks.mjs b/components/asana/actions/search-tasks/search-tasks.mjs index 960e07a457b6b..8ee8ef631d252 100644 --- a/components/asana/actions/search-tasks/search-tasks.mjs +++ b/components/asana/actions/search-tasks/search-tasks.mjs @@ -5,7 +5,7 @@ export default { key: "asana-search-tasks", name: "Search Tasks", description: "Searches for a Task by name within a Project. [See the documentation](https://developers.asana.com/docs/get-multiple-tasks)", - version: "0.2.5", + version: "0.3.0", type: "action", props: { ...common.props, @@ -22,6 +22,9 @@ export default { propDefinition: [ asana, "users", + ({ workspace }) => ({ + workspace, + }), ], }, section: { diff --git a/components/asana/actions/search-user-projects/search-user-projects.mjs b/components/asana/actions/search-user-projects/search-user-projects.mjs index 9e656739b96c1..b7a36770308b7 100644 --- a/components/asana/actions/search-user-projects/search-user-projects.mjs +++ b/components/asana/actions/search-user-projects/search-user-projects.mjs @@ -1,11 +1,11 @@ -import asana from "../../asana.app.mjs"; import _ from "lodash"; +import asana from "../../asana.app.mjs"; export default { key: "asana-search-user-projects", name: "Get list of user projects", description: "Return list of projects given the user and workspace gid. [See the documentation](https://developers.asana.com/docs/get-multiple-projects)", - version: "0.4.5", + version: "0.5.0", type: "action", props: { asana, @@ -17,7 +17,6 @@ export default { asana, "workspaces", ], - optional: true, }, user: { label: "User", @@ -26,6 +25,9 @@ export default { propDefinition: [ asana, "users", + ({ workspace }) => ({ + workspace, + }), ], }, }, diff --git a/components/asana/actions/update-task/update-task.mjs b/components/asana/actions/update-task/update-task.mjs index 631ed9b342f56..ddfe7c482bd90 100644 --- a/components/asana/actions/update-task/update-task.mjs +++ b/components/asana/actions/update-task/update-task.mjs @@ -5,7 +5,7 @@ export default { key: "asana-update-task", name: "Update Task", description: "Updates a specific and existing task. [See the documentation](https://developers.asana.com/docs/update-a-task)", - version: "0.3.5", + version: "0.4.0", type: "action", props: { ...common.props, @@ -34,6 +34,9 @@ export default { propDefinition: [ asana, "users", + ({ workspace }) => ({ + workspace, + }), ], }, assignee_section: { diff --git a/components/asana/asana.app.mjs b/components/asana/asana.app.mjs index 82cd63a035fca..40e10c5e4f613 100644 --- a/components/asana/asana.app.mjs +++ b/components/asana/asana.app.mjs @@ -120,9 +120,12 @@ export default { label: "Users", description: "List of users. This field uses the user GID.", type: "string[]", - async options({ prevContext }) { + async options({ + prevContext, workspace, + }) { const params = { limit: DEFAULT_LIMIT, + workspace, }; if (prevContext?.offset) { params.offset = prevContext.offset; @@ -232,6 +235,38 @@ export default { return Object.keys(task); }, }, + taskTemplate: { + type: "string", + label: "Task Template", + description: "The identifier of a task template", + async options({ + project, prevContext, + }) { + const params = { + project, + limit: DEFAULT_LIMIT, + }; + if (prevContext?.offset) { + params.offset = prevContext.offset; + } + const { + data, next_page: next, + } = await this.listTaskTemplates({ + params, + }); + return { + options: data?.map(({ + gid: value, name: label, + }) => ({ + value, + label, + })) || [], + context: { + offset: next?.offset, + }, + }; + }, + }, }, methods: { /** @@ -561,5 +596,20 @@ export default { $, }); }, + listTaskTemplates(opts = {}) { + return this._makeRequest({ + path: "task_templates", + ...opts, + }); + }, + createTaskFromTemplate({ + taskTemplateId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `task_templates/${taskTemplateId}/instantiateTask`, + ...opts, + }); + }, }, }; diff --git a/components/asana/package.json b/components/asana/package.json index 783f980195c07..b275598bd3841 100644 --- a/components/asana/package.json +++ b/components/asana/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/asana", - "version": "0.6.2", + "version": "0.7.0", "description": "Pipedream Asana Components", "main": "asana.app.mjs", "keywords": [ @@ -10,8 +10,7 @@ "homepage": "https://pipedream.com/apps/asana", "author": "Pipedream (https://pipedream.com/)", "dependencies": { - "@pipedream/platform": "^1.2.0", - "axios": "^0.21.1", + "@pipedream/platform": "^3.0.3", "lodash": "^4.17.21" }, "gitHead": "e12480b94cc03bed4808ebc6b13e7fdb3a1ba535", diff --git a/components/asana/sources/new-completed-task/new-completed-task.mjs b/components/asana/sources/new-completed-task/new-completed-task.mjs index fcb36605d62dd..eb46aeac1ecef 100644 --- a/components/asana/sources/new-completed-task/new-completed-task.mjs +++ b/components/asana/sources/new-completed-task/new-completed-task.mjs @@ -7,7 +7,7 @@ export default { type: "source", name: "New Completed Task (Instant)", description: "Emit new event for each task completed in a project.", - version: "0.1.5", + version: "0.1.7", dedupe: "unique", props: { ...common.props, diff --git a/components/asana/sources/new-project/new-project.mjs b/components/asana/sources/new-project/new-project.mjs index fef57d5eb20f9..6b21e8a3a9ecc 100644 --- a/components/asana/sources/new-project/new-project.mjs +++ b/components/asana/sources/new-project/new-project.mjs @@ -6,7 +6,7 @@ export default { key: "asana-new-project", name: "New Project Added To Workspace (Instant)", description: "Emit new event for each new project added to a workspace.", - version: "0.1.5", + version: "0.1.7", dedupe: "unique", props: { ...common.props, diff --git a/components/asana/sources/new-story/new-story.mjs b/components/asana/sources/new-story/new-story.mjs index c98732c7f3fcd..99a959ba61ae3 100644 --- a/components/asana/sources/new-story/new-story.mjs +++ b/components/asana/sources/new-story/new-story.mjs @@ -7,7 +7,7 @@ export default { type: "source", name: "New Story Added To Project (Instant)", description: "Emit new event for each story added to a project.", - version: "0.1.5", + version: "0.1.7", dedupe: "unique", props: { ...common.props, diff --git a/components/asana/sources/new-subtask/new-subtask.mjs b/components/asana/sources/new-subtask/new-subtask.mjs index f6eb22300c5a3..c96cc6ffd3003 100644 --- a/components/asana/sources/new-subtask/new-subtask.mjs +++ b/components/asana/sources/new-subtask/new-subtask.mjs @@ -7,7 +7,7 @@ export default { type: "source", name: "New Subtask (Instant)", description: "Emit new event for each subtask added to a project.", - version: "1.0.5", + version: "1.0.7", dedupe: "unique", props: { ...common.props, diff --git a/components/asana/sources/new-tag/new-tag.mjs b/components/asana/sources/new-tag/new-tag.mjs index dc9b34ab58f8e..42211e8c32dce 100644 --- a/components/asana/sources/new-tag/new-tag.mjs +++ b/components/asana/sources/new-tag/new-tag.mjs @@ -1,12 +1,12 @@ -import asana from "../../asana.app.mjs"; import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import asana from "../../asana.app.mjs"; export default { key: "asana-new-tag", type: "source", name: "New Tag", description: "Emit new event for each tag created in a workspace.", - version: "0.0.7", + version: "0.0.9", dedupe: "unique", props: { asana, diff --git a/components/asana/sources/new-task/new-task.mjs b/components/asana/sources/new-task/new-task.mjs index 48d867ab0beb0..b1373c6272fcc 100644 --- a/components/asana/sources/new-task/new-task.mjs +++ b/components/asana/sources/new-task/new-task.mjs @@ -7,7 +7,7 @@ export default { type: "source", name: "New Task (Instant)", description: "Emit new event for each task added to a project. [See docs here](https://developers.asana.com/docs/establish-a-webhook)", - version: "0.1.5", + version: "0.1.7", dedupe: "unique", props: { ...common.props, diff --git a/components/asana/sources/new-team/new-team.mjs b/components/asana/sources/new-team/new-team.mjs index 3ddf7acffff4f..68110bd605088 100644 --- a/components/asana/sources/new-team/new-team.mjs +++ b/components/asana/sources/new-team/new-team.mjs @@ -1,12 +1,12 @@ -import asana from "../../asana.app.mjs"; import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import asana from "../../asana.app.mjs"; export default { key: "asana-new-team", type: "source", name: "New Team", - description: "Emit new event for each task added to an organization.", - version: "0.1.6", + description: "Emit new event for each team added to an organization.", + version: "0.1.8", dedupe: "unique", props: { asana, diff --git a/components/asana/sources/new-user/new-user.mjs b/components/asana/sources/new-user/new-user.mjs index 6795a2b6dc2ce..a3ac891dc3df8 100644 --- a/components/asana/sources/new-user/new-user.mjs +++ b/components/asana/sources/new-user/new-user.mjs @@ -6,7 +6,7 @@ export default { type: "source", name: "New User (Instant)", description: "Emit new event for each user added to a workspace.", - version: "0.1.5", + version: "0.1.7", dedupe: "unique", props: { ...common.props, diff --git a/components/asana/sources/new-workspace/new-workspace.mjs b/components/asana/sources/new-workspace/new-workspace.mjs index 09c28711a72aa..d367483d28d51 100644 --- a/components/asana/sources/new-workspace/new-workspace.mjs +++ b/components/asana/sources/new-workspace/new-workspace.mjs @@ -1,12 +1,12 @@ -import asana from "../../asana.app.mjs"; import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import asana from "../../asana.app.mjs"; export default { type: "source", key: "asana-new-workspace", name: "New Workspace Added", description: "Emit new event each time you add a new workspace/organization.", - version: "0.1.6", + version: "0.1.8", dedupe: "unique", props: { asana, diff --git a/components/asana/sources/tag-added-to-task/tag-added-to-task.mjs b/components/asana/sources/tag-added-to-task/tag-added-to-task.mjs index 23f1c9831db0f..a7d70c741ec47 100644 --- a/components/asana/sources/tag-added-to-task/tag-added-to-task.mjs +++ b/components/asana/sources/tag-added-to-task/tag-added-to-task.mjs @@ -7,7 +7,7 @@ export default { type: "source", name: "New Tag Added To Task (Instant)", description: "Emit new event for each new tag added to a task.", - version: "0.1.5", + version: "0.1.7", dedupe: "unique", props: { ...common.props, diff --git a/components/asana/sources/tags-added-to-any-task/tags-added-to-any-task.mjs b/components/asana/sources/tags-added-to-any-task/tags-added-to-any-task.mjs index 50fde642a6db8..b14f843c397f1 100644 --- a/components/asana/sources/tags-added-to-any-task/tags-added-to-any-task.mjs +++ b/components/asana/sources/tags-added-to-any-task/tags-added-to-any-task.mjs @@ -5,9 +5,9 @@ export default { ...common, key: "asana-tags-added-to-any-task", type: "source", - name: "Tags added to any task (Instant)", + name: "New Tags added to any task (Instant)", description: "Emit new event each time a tag is added to any task, optionally filtering by a given set of tags.", - version: "0.0.4", + version: "0.0.6", dedupe: "unique", props: { ...common.props, diff --git a/components/asana/sources/task-assigned-in-project/task-assigned-in-project.mjs b/components/asana/sources/task-assigned-in-project/task-assigned-in-project.mjs index df73562994b34..7e13f9e98dc0e 100644 --- a/components/asana/sources/task-assigned-in-project/task-assigned-in-project.mjs +++ b/components/asana/sources/task-assigned-in-project/task-assigned-in-project.mjs @@ -5,9 +5,9 @@ export default { ...common, key: "asana-task-assigned-in-project", type: "source", - name: "Task Assigned in Project (Instant)", + name: "New Task Assigned in Project (Instant)", description: "Emit new event each time a task is assigned, reassigned or unassigned.", - version: "0.0.4", + version: "0.1.0", dedupe: "unique", props: { ...common.props, @@ -30,6 +30,9 @@ export default { propDefinition: [ asana, "users", + ({ workspace }) => ({ + workspace, + }), ], }, }, diff --git a/components/asana/sources/task-field-updated-in-project/task-field-updated-in-project.mjs b/components/asana/sources/task-field-updated-in-project/task-field-updated-in-project.mjs index 1d67f62f5fc40..7d057a6c246e3 100644 --- a/components/asana/sources/task-field-updated-in-project/task-field-updated-in-project.mjs +++ b/components/asana/sources/task-field-updated-in-project/task-field-updated-in-project.mjs @@ -5,9 +5,9 @@ export default { ...common, key: "asana-task-field-updated-in-project", type: "source", - name: "Task Field Updated In Project (Instant)", + name: "New Task Field Updated In Project (Instant)", description: "Emit new event whenever given task fields are updated.", - version: "0.0.4", + version: "0.0.6", dedupe: "unique", props: { ...common.props, diff --git a/components/asana/sources/task-updated-in-project/task-updated-in-project.mjs b/components/asana/sources/task-updated-in-project/task-updated-in-project.mjs index 6c668fc44330b..8d3e5527dc289 100644 --- a/components/asana/sources/task-updated-in-project/task-updated-in-project.mjs +++ b/components/asana/sources/task-updated-in-project/task-updated-in-project.mjs @@ -5,9 +5,9 @@ export default { ...common, key: "asana-task-updated-in-project", type: "source", - name: "Task Updated In Project (Instant)", + name: "New Task Updated In Project (Instant)", description: "Emit new event for each update to a task.", - version: "1.1.3", + version: "1.1.6", dedupe: "unique", props: { ...common.props, @@ -41,11 +41,38 @@ export default { propDefinition: [ asana, "users", + ({ workspace }) => ({ + workspace, + }), ], }, + delay: { + type: "integer", + label: "Delay In Seconds", + description: "How long to wait before emitting events.", + default: 15, + optional: true, + }, }, methods: { ...common.methods, + getLastEventDate() { + return this.db.get("lastEventDate"); + }, + setLastEventDate(value) { + this.db.set("lastEventDate", value); + }, + debounce(fn, delay = 15000) { + const lastEventDate = this.getLastEventDate(); + if (lastEventDate) { + const diff = Date.now() - new Date(lastEventDate).getTime(); + if (diff < delay) { + return; + } + } + fn(); + this.setLastEventDate(new Date()); + }, getWebhookFilter() { return { filters: [ @@ -59,7 +86,7 @@ export default { }, async emitEvent(event) { const { - tasks, user, + tasks, user, delay, } = this; const { events = [] } = event.body || {}; @@ -84,11 +111,11 @@ export default { event, task, }) => { const ts = Date.parse(event.created_at); - this.$emit(task, { + this.debounce(() => this.$emit(task, { id: `${task.gid}-${ts}`, summary: task.name, ts, - }); + }), delay * 1000); }); }, }, diff --git a/components/ascora/README.md b/components/ascora/README.md new file mode 100644 index 0000000000000..13ee33352314f --- /dev/null +++ b/components/ascora/README.md @@ -0,0 +1,11 @@ +# Overview + +The Ascora API enables interaction with Ascora's business and job management system, allowing for streamlined operations including job scheduling, invoicing, and client management. Through Pipedream, you can craft serverless workflows that leverage the Ascora API to automate tasks, sync data across apps, and react to events in real-time. This can increase efficiency, reduce manual entry, and provide up-to-date information across platforms. + +# Example Use Cases + +- **Automated Job Dispatch**: When a new job is created in your CRM, use Pipedream to trigger a workflow that automatically creates a corresponding job in Ascora, assigns it to the appropriate team, and notifies them via Slack or email. + +- **Invoicing Sync**: After an invoice is marked as paid in Ascora, a Pipedream workflow can capture this event and update the invoice status in your accounting software, such as QuickBooks or Xero, ensuring financial records are consistent. + +- **Customer Feedback Collection**: Post-service, send a survey link to customers through Pipedream. When a survey is completed, trigger a workflow that updates the customer record in Ascora with feedback data, and if necessary, creates a follow-up task. diff --git a/components/ascora/ascora.app.mjs b/components/ascora/ascora.app.mjs new file mode 100644 index 0000000000000..2f6556d81497f --- /dev/null +++ b/components/ascora/ascora.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "ascora", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/ascora/package.json b/components/ascora/package.json new file mode 100644 index 0000000000000..c7787a61ee677 --- /dev/null +++ b/components/ascora/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/ascora", + "version": "0.0.1", + "description": "Pipedream Ascora Components", + "main": "ascora.app.mjs", + "keywords": [ + "pipedream", + "ascora" + ], + "homepage": "https://pipedream.com/apps/ascora", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/asin_data_api/README.md b/components/asin_data_api/README.md new file mode 100644 index 0000000000000..7dbd308e39594 --- /dev/null +++ b/components/asin_data_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The ASIN Data API lets you tap into detailed product data from Amazon, including price, rank, reviews, and more. It's a treasure trove for e-commerce pros, marketers, or data analysts looking to enrich their datasets with real-time insights from the world’s largest online retailer. On Pipedream, you can automate workflows using this API to track product changes, analyze market trends, or even alert you when specific conditions are met. + +# Example Use Cases + +- **Product Price Alert**: Trigger a workflow when a product's price drops below a certain threshold. You could use this to inform your purchasing decisions, or to update your own pricing if you're retailing. + +- **Market Trend Analysis**: Schedule a daily or weekly workflow to fetch data for a set of ASINs. Store this in a Pipedream data store, then push to Google Sheets or a BI tool for trends analysis, helping you make informed stock decisions. + +- **Review Monitoring & Sentiment Analysis**: Keep an eye on new reviews for products. Fetch reviews via the API, then use a sentiment analysis tool to gauge customer satisfaction and react swiftly to any negative feedback. diff --git a/components/asin_data_api/asin_data_api.app.mjs b/components/asin_data_api/asin_data_api.app.mjs new file mode 100644 index 0000000000000..1506c2e2db530 --- /dev/null +++ b/components/asin_data_api/asin_data_api.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "asin_data_api", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/asin_data_api/package.json b/components/asin_data_api/package.json new file mode 100644 index 0000000000000..111be775b66ef --- /dev/null +++ b/components/asin_data_api/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/asin_data_api", + "version": "0.0.1", + "description": "Pipedream ASIN Data API Components", + "main": "asin_data_api.app.mjs", + "keywords": [ + "pipedream", + "asin_data_api" + ], + "homepage": "https://pipedream.com/apps/asin_data_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/asknicely/README.md b/components/asknicely/README.md new file mode 100644 index 0000000000000..797071d572d8e --- /dev/null +++ b/components/asknicely/README.md @@ -0,0 +1,11 @@ +# Overview + +The AskNicely API enables you to tap into customer feedback processes by automating the collection and analysis of customer satisfaction data. With Pipedream, you can leverage this API to create custom workflows that trigger on specific events, interact with other apps, and perform actions based on customer feedback scores and comments. Whether you're syncing data to a CRM, triggering email campaigns based on scores, or aggregating responses for analysis, the API's integration into Pipedream opens a world of possibilities for enhancing customer experience and operational efficiency. + +# Example Use Cases + +- **Sync Feedback to CRM:** Automatically update customer records in Salesforce with the latest Net Promoter Score (NPS) and feedback collected via AskNicely. This ensures that customer-facing teams have the most current sentiment data at their fingertips. + +- **Trigger Support Tickets:** Create a workflow that monitors for low NPS scores and automatically generates a support ticket in Zendesk. This allows customer service teams to swiftly address concerns and potentially turn detractors into promoters. + +- **Aggregate Feedback for Analysis:** Collect all AskNicely feedback responses and feed them into a Google Sheets document. Apply further analysis or visualizations to track trends and make data-driven decisions to improve customer satisfaction. diff --git a/components/askyourpdf/README.md b/components/askyourpdf/README.md new file mode 100644 index 0000000000000..5835b324cacd1 --- /dev/null +++ b/components/askyourpdf/README.md @@ -0,0 +1,11 @@ +# Overview + +AskYourPDF API allows you to extract text and data from PDF files programmatically. On Pipedream, leverage this API to automate workflows involving PDF data extraction, such as parsing invoices, extracting data for reports, or importing information into a database. With Pipedream's ability to integrate with various apps, you can create workflows that trigger on specific events and handle extracted PDF data with custom logic and actions. + +# Example Use Cases + +- **Automated Invoice Processing**: Use AskYourPDF to extract invoice data from PDFs and send it to an accounting app like QuickBooks for record-keeping and financial analysis. + +- **Data Migration to Spreadsheets**: Extract structured data from PDFs and append rows to a Google Sheet for easy data visualization and manipulation. + +- **Email Attachments to CRM**: On receiving a PDF attachment in Gmail, use AskYourPDF to parse the content and update relevant fields in a CRM like Salesforce, keeping customer records up-to-date. diff --git a/components/askyourpdf/actions/add-document-via-file-upload/add-document-via-file-upload.mjs b/components/askyourpdf/actions/add-document-via-file-upload/add-document-via-file-upload.mjs index 9a5fac157d6c2..4b4b50d7a0631 100644 --- a/components/askyourpdf/actions/add-document-via-file-upload/add-document-via-file-upload.mjs +++ b/components/askyourpdf/actions/add-document-via-file-upload/add-document-via-file-upload.mjs @@ -6,7 +6,7 @@ export default { name: "Add Document Via File Upload", description: "Add a document via file upload. [See the documentation](https://docs.askyourpdf.com/askyourpdf-docs/#2.-adding-document-via-file-upload)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { app, file: { diff --git a/components/askyourpdf/actions/add-document-via-url/add-document-via-url.mjs b/components/askyourpdf/actions/add-document-via-url/add-document-via-url.mjs index 7bf37bbe7bcd2..ddde3ced98403 100644 --- a/components/askyourpdf/actions/add-document-via-url/add-document-via-url.mjs +++ b/components/askyourpdf/actions/add-document-via-url/add-document-via-url.mjs @@ -5,7 +5,7 @@ export default { name: "Add Document Via URL", description: "Add a document via URL. [See the documentation](https://docs.askyourpdf.com/askyourpdf-docs/#1.-adding-document-via-url)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { app, url: { diff --git a/components/askyourpdf/actions/chat-with-document/chat-with-document.mjs b/components/askyourpdf/actions/chat-with-document/chat-with-document.mjs index 138f0e0baca02..03c3050afc6bd 100644 --- a/components/askyourpdf/actions/chat-with-document/chat-with-document.mjs +++ b/components/askyourpdf/actions/chat-with-document/chat-with-document.mjs @@ -5,7 +5,7 @@ export default { name: "Chat With Document", description: "Chat with a document. [See the documentation](https://docs.askyourpdf.com/askyourpdf-docs/#3.-chat-endpoint)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { app, docId: { diff --git a/components/askyourpdf/common/utils.mjs b/components/askyourpdf/common/utils.mjs index c75d0469ab1b8..fb5909561429a 100644 --- a/components/askyourpdf/common/utils.mjs +++ b/components/askyourpdf/common/utils.mjs @@ -9,7 +9,7 @@ function buildFormData(formData, data, parentKey) { buildFormData(formData, data[key], parentKey && `${parentKey}[${key}]` || key); }); - } else if (data && constants.FILE_PROP_NAMES.some((prop) => prop.includes(parentKey))) { + } else if (data && constants.FILE_PROP_NAMES.some((prop) => parentKey.includes(prop))) { formData.append(parentKey, createReadStream(data)); } else if (data) { diff --git a/components/askyourpdf/package.json b/components/askyourpdf/package.json index a3caca73396f1..afdaf84cde945 100644 --- a/components/askyourpdf/package.json +++ b/components/askyourpdf/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/askyourpdf", - "version": "0.1.0", + "version": "0.1.1", "description": "Pipedream AskYourPDF Components", "main": "askyourpdf.app.mjs", "keywords": [ @@ -10,7 +10,9 @@ "homepage": "https://pipedream.com/apps/askyourpdf", "author": "Pipedream (https://pipedream.com/)", "dependencies": { - "@pipedream/platform": "^1.5.1" + "@pipedream/platform": "^1.6.2", + "form-data": "^4.0.0", + "fs": "^0.0.1-security" }, "publishConfig": { "access": "public" diff --git a/components/assembla/README.md b/components/assembla/README.md index 7c6b7b5afcb92..f3d0187cbac0d 100644 --- a/components/assembla/README.md +++ b/components/assembla/README.md @@ -1,11 +1,11 @@ # Overview -Assembla is a powerful collaboration and development platform that helps -companies accelerate their development process. The Assembla API allows you to -programmatically access and modify your Assembla account. With the Assembla -API, you can build applications that: - -- Automate your development process -- Integrate Assembla with other tools -- Build custom reports -- And much more! +The Assembla API lets you tap into a robust project management platform, offering seamless automation opportunities for developers and teams. With it, you can automate ticket creation, track commits and merge requests, manage users, and synchronize project activities with other tools. It's a boon for boosting productivity and ensuring that your project's moving parts stay well-oiled and in sync. + +# Example Use Cases + +- **Ticket Management Automation**: Automatically create Assembla tickets from customer emails or support requests captured in apps like Zendesk or Intercom. This ensures that user-reported issues are promptly converted into actionable tasks for your dev team. + +- **Commit Tracking for Compliance**: Set up a workflow that monitors every push to your repositories, logs them in a Google Sheet or a data store, and creates an Assembla ticket for review. A perfect way to maintain change logs for auditing and compliance purposes. + +- **User Management Sync**: Whenever a new team member is added to your GitHub organization, use Pipedream to add them as a user in Assembla too, and assign them to the correct team space. Keep your user management in sync across platforms without lifting a finger. diff --git a/components/assembla/package.json b/components/assembla/package.json new file mode 100644 index 0000000000000..f86ec7b0e3079 --- /dev/null +++ b/components/assembla/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/assembla", + "version": "0.6.0", + "description": "Pipedream assembla Components", + "main": "assembla.app.mjs", + "keywords": [ + "pipedream", + "assembla" + ], + "homepage": "https://pipedream.com/apps/assembla", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/assemblyai/README.md b/components/assemblyai/README.md new file mode 100644 index 0000000000000..98b19d1190e88 --- /dev/null +++ b/components/assemblyai/README.md @@ -0,0 +1,12 @@ +# Overview + +The AssemblyAI API provides powerful speech recognition and natural language processing capabilities. It allows users to transcribe audio, analyze sentiment, detect topics, and more. In Pipedream, you can leverage these features to create automated workflows that process audio and text data. Connect AssemblyAI to various apps and services, trigger actions based on the API's output, and build robust, serverless data pipelines. + +# Example Use Cases + +- **Automated Content Moderation**: Use AssemblyAI's API to transcribe user-generated audio content and automatically flag inappropriate or sensitive material. Integrate with a messaging app like Slack to alert moderators when content needs review. + +- **Sentiment Analysis for Customer Feedback**: Connect AssemblyAI with a customer support platform like Zendesk. Transcribe support calls, analyze the sentiment, and categorize tickets based on customer emotions. This can help prioritize urgent cases and improve service quality. + +- **Meeting Transcription and Summarization**: After a Zoom meeting, use Pipedream to send the recording to AssemblyAI for transcription. Process the text to extract key points and action items, then post them to a project management tool like Asana or Trello for easy follow-up. +. diff --git a/components/astica_ai/README.md b/components/astica_ai/README.md new file mode 100644 index 0000000000000..659fda853e13f --- /dev/null +++ b/components/astica_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +The Astica.ai API affords developers the ability to tap into advanced natural language processing (NLP) capabilities. By integrating this API into Pipedream workflows, you can automate text analysis, summarization, sentiment analysis, and more, leveraging the AI-powered insights to enrich applications or streamline processes. Pipedream’s serverless platform makes it painless to set up event-driven workflows that respond to various triggers and manipulate or move data between apps with no need for infrastructure management. + +# Example Use Cases + +- **Content Summarization for Social Media**: Automate the generation of concise summaries for long-form content. Whenever a new article is published on your CMS, trigger a Pipedream workflow to send the content to Astica.ai for summarization. Use the summary to auto-post updates to social platforms like Twitter or LinkedIn, saving time and ensuring your audience stays engaged with bite-sized content. + +- **Customer Feedback Analysis**: Collect and analyze customer feedback from various sources, such as support tickets or product reviews. With Pipedream, you can trigger a workflow when new feedback arrives, send it to Astica.ai for sentiment analysis, and then route the results to a Google Sheet or a Slack channel for real-time team review and response prioritization. + +- **Automated Research Assistant**: Create a workflow that helps with gathering and synthesizing research material. When a set of documents is uploaded to a cloud storage service like Dropbox, Pipedream can kick off a workflow to pass the documents to Astica.ai for key phrase extraction and summarization. The extracted insights could then be sent to Notion or Evernote, providing a synthesized research reference point. diff --git a/components/astrology_api/README.md b/components/astrology_api/README.md new file mode 100644 index 0000000000000..2114beeda44f2 --- /dev/null +++ b/components/astrology_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Astrology API allows you to tap into the intricate world of astrological insights directly from your Pipedream workflows. With this API, you can generate horoscopes, match-making reports, and various astrological calculations such as planetary positions and birth charts. This API is a valuable tool for creating personalized content for users, enhancing apps with astrological features, or even for building niche astrology-based services. + +# Example Use Cases + +- **Daily Horoscope Email Service**: Create a workflow that triggers daily, generating personalized horoscopes for a list of users' zodiac signs using the Astrology API. Then, using the built-in Email app, send these horoscopes directly to the users' inboxes. + +- **Astrological Matchmaking App Integration**: Incorporate astrological matchmaking into a dating app by creating a workflow that uses user data to request compatibility scores from the Astrology API. Connect this to a Database app to store scores and present matches to users based on astrological compatibility. + +- **Event Timing Based on Astrology**: Develop a workflow for event planners that uses the Astrology API to select auspicious dates and times. Combine this with a Calendar app to help schedule events in alignment with astrological recommendations. diff --git a/components/async_interview/README.md b/components/async_interview/README.md new file mode 100644 index 0000000000000..ffcaa78af86f1 --- /dev/null +++ b/components/async_interview/README.md @@ -0,0 +1,11 @@ +# Overview + +The Async Interview API enables automation around video interview processes. With this API, you can craft workflows on Pipedream that manipulate interview data, initiate new interviews, and respond to events within the Async Interview platform. Pipedream's serverless architecture allows you to connect with hundreds of apps to streamline recruitment workflows, analyze interview data, or enhance the candidate experience with personalized interactions. + +# Example Use Cases + +- **Automated Candidate Follow-Up**: After an interview is completed, trigger an automated email to the candidate with personalized feedback or next steps using the Gmail app. + +- **Interview Data Analysis**: Gather interview responses and feed them into a Google Sheets document for further analysis, allowing HR teams to make data-driven decisions. + +- **Scheduling Interviews**: Integrate with a calendar service like Google Calendar to automatically schedule interviews once a candidate passes a certain application stage, and send calendar invites to both interviewers and candidates. diff --git a/components/attio/actions/create-note/create-note.mjs b/components/attio/actions/create-note/create-note.mjs new file mode 100644 index 0000000000000..923c548133fde --- /dev/null +++ b/components/attio/actions/create-note/create-note.mjs @@ -0,0 +1,57 @@ +import attio from "../../attio.app.mjs"; + +export default { + key: "attio-create-note", + name: "Create Note", + description: "Creates a new note for a given record. The note will be linked to the specified record. [See the documentation](https://developers.attio.com/reference/post_v2-notes)", + version: "0.0.2", + type: "action", + props: { + attio, + parentObject: { + propDefinition: [ + attio, + "objectId", + ], + label: "Parent Object ID", + description: "The ID of the parent object the note belongs to", + }, + parentRecordId: { + propDefinition: [ + attio, + "recordId", + (c) => ({ + objectId: c.parentObject, + }), + ], + label: "Parent Record ID", + description: "The ID of the parent record the note belongs to", + }, + title: { + type: "string", + label: "Title", + description: "The note title", + }, + content: { + type: "string", + label: "Content", + description: "The content of the note", + }, + }, + async run({ $ }) { + const response = await this.attio.createNote({ + $, + data: { + data: { + parent_object: this.parentObject, + parent_record_id: this.parentRecordId, + title: this.title, + format: "plaintext", + content: this.content, + }, + }, + }); + $.export("$summary", `Successfully created note with ID: ${response.data.id.note_id}`); + return response; + }, +}; diff --git a/components/attio/actions/create-update-record/create-update-record.mjs b/components/attio/actions/create-update-record/create-update-record.mjs new file mode 100644 index 0000000000000..e0abe3c586fd1 --- /dev/null +++ b/components/attio/actions/create-update-record/create-update-record.mjs @@ -0,0 +1,84 @@ +import attio from "../../attio.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "attio-create-update-record", + name: "Create or Update Record", + description: "Creates or updates a specific record such as a person or a deal. If the record already exists, it's updated. Otherwise, a new record is created. [See the documentation](https://developers.attio.com/reference/put_v2-objects-object-records)", + version: "0.0.2", + type: "action", + props: { + attio, + objectId: { + propDefinition: [ + attio, + "objectId", + ], + }, + attributeId: { + propDefinition: [ + attio, + "attributeId", + (c) => ({ + objectId: c.objectId, + }), + ], + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (!this.attributeId) { + return props; + } + const attributes = await this.getRelevantAttributes(); + for (const attribute of attributes) { + props[attribute.id.attribute_id] = { + type: attribute.is_multiselect + ? "string[]" + : "string", + label: attribute.title, + optional: attribute.id.attribute_id !== this.attributeId && !attribute.is_required, + }; + } + return props; + }, + methods: { + async getRelevantAttributes() { + const stream = utils.paginate({ + fn: this.attio.listAttributes, + args: { + objectId: this.objectId, + }, + }); + const attributes = await utils.streamIterator(stream); + return attributes.filter((a) => a.is_writable || a.id.attribute_id === this.attributeId); + }, + }, + async run({ $ }) { + const { + attio, + getRelevantAttributes, + objectId, + attributeId, + ...values + } = this; + + const attributes = await getRelevantAttributes(); + + const response = await attio.upsertRecord({ + $, + objectId, + params: { + matching_attribute: attributeId, + }, + data: { + data: { + values: utils.parseValues(attributes, values), + }, + }, + }); + $.export("$summary", "Successfully created or updated record"); + return response; + }, +}; diff --git a/components/attio/actions/delete-list-entry/delete-list-entry.mjs b/components/attio/actions/delete-list-entry/delete-list-entry.mjs new file mode 100644 index 0000000000000..3b98149d16d05 --- /dev/null +++ b/components/attio/actions/delete-list-entry/delete-list-entry.mjs @@ -0,0 +1,36 @@ +import attio from "../../attio.app.mjs"; + +export default { + key: "attio-delete-list-entry", + name: "Delete List Entry", + description: "Deletes an existing entry from a specific list. [See the documentation](https://developers.attio.com/reference/delete_v2-lists-list-entries-entry-id)", + version: "0.0.2", + type: "action", + props: { + attio, + listId: { + propDefinition: [ + attio, + "listId", + ], + }, + entryId: { + propDefinition: [ + attio, + "entryId", + (c) => ({ + listId: c.listId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.attio.deleteListEntry({ + $, + listId: this.listId, + entryId: this.entryId, + }); + $.export("$summary", `Successfully deleted list entry with ID: ${this.entryId}`); + return response; + }, +}; diff --git a/components/attio/attio.app.mjs b/components/attio/attio.app.mjs new file mode 100644 index 0000000000000..27f2fb1027f24 --- /dev/null +++ b/components/attio/attio.app.mjs @@ -0,0 +1,200 @@ +import { axios } from "@pipedream/platform"; +const DEFAULT_LIMIT = 20; + +export default { + type: "app", + app: "attio", + propDefinitions: { + listId: { + type: "string", + label: "List ID", + description: "The identifier of a list", + async options() { + const { data } = await this.listLists(); + return data?.map(({ + id, name: label, + }) => ({ + value: id.list_id, + label, + })) || []; + }, + }, + entryId: { + type: "string", + label: "Entry ID", + description: "The identifier of a list entry", + async options({ + listId, page, + }) { + const { data } = await this.listEntries({ + listId, + params: { + limit: DEFAULT_LIMIT, + offset: page * DEFAULT_LIMIT, + }, + }); + return data?.map(({ id }) => id.entry_id) || []; + }, + }, + objectId: { + type: "string", + label: "Object ID", + description: "The identifier of an object", + async options() { + const { data } = await this.listObjects(); + return data?.map(({ + id, singular_noun: label, + }) => ({ + value: id.object_id, + label, + })) || []; + }, + }, + recordId: { + type: "string", + label: "Record ID", + description: "Identifier of a record", + async options({ + objectId, page, + }) { + const { data } = await this.listRecords({ + objectId, + params: { + limit: DEFAULT_LIMIT, + offset: page * DEFAULT_LIMIT, + }, + }); + return data?.map(({ + id, values, + }) => ({ + value: id.record_id, + label: (values?.name?.length && (values.name[0].value || values.name[0].full_name)) + ?? (values?.domains?.length && values.domains[0].domain) + ?? (values?.email_addresses?.length && values.email_addresses[0].email_address) + ?? values?.id?.record_id, + })) || []; + }, + }, + attributeId: { + type: "string", + label: "Attribute ID", + description: "The ID or slug of the attribute to use to check if a record already exists. The attribute must be unique.", + async options({ + objectId, page, + }) { + const { data } = await this.listAttributes({ + objectId, + params: { + limit: DEFAULT_LIMIT, + offset: page * DEFAULT_LIMIT, + }, + }); + return data + ?.filter((attribute) => attribute.is_unique) + ?.map(({ + id, title: label, + }) => ({ + value: id.attribute_id, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.attio.com/v2"; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteWebhook({ + hookId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${hookId}`, + ...opts, + }); + }, + listLists(opts = {}) { + return this._makeRequest({ + path: "/lists", + ...opts, + }); + }, + listEntries({ + listId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/lists/${listId}/entries/query`, + ...opts, + }); + }, + listObjects(opts = {}) { + return this._makeRequest({ + path: "/objects", + ...opts, + }); + }, + listRecords({ + objectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/objects/${objectId}/records/query`, + ...opts, + }); + }, + listAttributes({ + objectId, ...opts + }) { + return this._makeRequest({ + path: `/objects/${objectId}/attributes`, + ...opts, + }); + }, + createNote(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/notes", + ...opts, + }); + }, + upsertRecord({ + objectId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/objects/${objectId}/records`, + ...opts, + }); + }, + deleteListEntry({ + listId, entryId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/lists/${listId}/entries/${entryId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/attio/common/utils.mjs b/components/attio/common/utils.mjs new file mode 100644 index 0000000000000..240f1978403cd --- /dev/null +++ b/components/attio/common/utils.mjs @@ -0,0 +1,62 @@ +async function streamIterator(stream) { + let resources = []; + for await (const resource of stream) { + resources.push(resource); + } + return resources; +} + +async function *paginate({ + fn, + args, + max, +}) { + args = { + ...args, + params: { + ...args.params ?? {}, + limit: 500, + offset: 0, + }, + }; + let total, count = 0; + do { + const { data } = await fn(args); + for (const item of data) { + yield item; + if (max && ++count >= max) { + return; + } + } + total = data?.length; + args.params.offset += args.params.limit; + } while (total === args.params.limit); +} + +function parseValues(attributes, values) { + for (const [ + key, + value, + ] of Object.entries(values)) { + const { + type, is_multiselect: isMultiselect, + } = attributes.find(({ id }) => id.attribute_id === key); + if (type === "checkbox") { + values[key] = isMultiselect + ? value.map((v) => !(v === "false" || v === "0")) + : !(value === "false" || value === "0"); + } + if (type === "number" || type === "rating") { + values[key] = isMultiselect + ? value.map((v) => +v) + : +value; + } + } + return values; +} + +export default { + streamIterator, + paginate, + parseValues, +}; diff --git a/components/attio/package.json b/components/attio/package.json new file mode 100644 index 0000000000000..e74624a9a7544 --- /dev/null +++ b/components/attio/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/attio", + "version": "0.2.0", + "description": "Pipedream Attio Components", + "main": "attio.app.mjs", + "keywords": [ + "pipedream", + "attio" + ], + "homepage": "https://pipedream.com/apps/attio", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/attio/sources/common/base.mjs b/components/attio/sources/common/base.mjs new file mode 100644 index 0000000000000..967d7b8428bce --- /dev/null +++ b/components/attio/sources/common/base.mjs @@ -0,0 +1,62 @@ +import attio from "../../attio.app.mjs"; + +export default { + props: { + attio, + db: "$.service.db", + http: "$.interface.http", + }, + hooks: { + async activate() { + const { data: { id: { webhook_id: hookId } } } = await this.attio.createWebhook({ + data: { + data: { + target_url: this.http.endpoint, + subscriptions: [ + { + event_type: this.getEventType(), + filter: this.getFilter(), + }, + ], + }, + }, + }); + this._setHookId(hookId); + }, + async deactivate() { + const hookId = this._getHookId(); + if (hookId) { + await this.attio.deleteWebhook({ + hookId, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getFilter() { + return null; + }, + getEventType() { + throw new Error("getEventType is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run(event) { + const { body: { events } } = event; + if (!events?.length) { + return; + } + events.forEach((item) => { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }); + }, +}; diff --git a/components/attio/sources/list-entry-deleted-instant/list-entry-deleted-instant.mjs b/components/attio/sources/list-entry-deleted-instant/list-entry-deleted-instant.mjs new file mode 100644 index 0000000000000..121a90a04d387 --- /dev/null +++ b/components/attio/sources/list-entry-deleted-instant/list-entry-deleted-instant.mjs @@ -0,0 +1,46 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "attio-list-entry-deleted-instant", + name: "List Entry Deleted (Instant)", + description: "Emit new event when a list entry is deleted (i.e. when a record is removed from a list).", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + listId: { + propDefinition: [ + common.props.attio, + "listId", + ], + }, + }, + methods: { + ...common.methods, + getEventType() { + return "list-entry.deleted"; + }, + getFilter() { + return { + "$and": [ + { + field: "id.list_id", + operator: "equals", + value: this.listId, + }, + ], + }; + }, + generateMeta(entry) { + return { + id: entry.id.entry_id, + summary: `Deleted Entry with ID: ${entry.id.entry_id}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/attio/sources/list-entry-deleted-instant/test-event.mjs b/components/attio/sources/list-entry-deleted-instant/test-event.mjs new file mode 100644 index 0000000000000..3751928712e76 --- /dev/null +++ b/components/attio/sources/list-entry-deleted-instant/test-event.mjs @@ -0,0 +1,14 @@ +export default { + "event_type": "list-entry.deleted", + "id": { + "workspace_id": "21e6dc8b-aa9f-4970-bdce-fa207cf3a7d9", + "list_id": "dd41a385-c7cd-4832-ad57-c9a824b32ba4", + "entry_id": "7ce64af0-9d4a-4899-88c8-6d74258ca8ef" + }, + "parent_object_id": "6a3d5e81-0de0-4e96-a535-957b28ba8211", + "parent_record_id": "64561036-ee99-47f7-926c-66bf43808dcf", + "actor": { + "type": "api-token", + "id": "76602e62-6767-4975-9e3f-751d5ce80a05" + } +} \ No newline at end of file diff --git a/components/attio/sources/list-entry-updated-instant/list-entry-updated-instant.mjs b/components/attio/sources/list-entry-updated-instant/list-entry-updated-instant.mjs new file mode 100644 index 0000000000000..97970cdab82ef --- /dev/null +++ b/components/attio/sources/list-entry-updated-instant/list-entry-updated-instant.mjs @@ -0,0 +1,47 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "attio-list-entry-updated-instant", + name: "List Entry Updated (Instant)", + description: "Emit new event when an existing list entry is updated (i.e. when a list attribute is changed for a specific list entry, e.g. when setting \"Owner\")", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + listId: { + propDefinition: [ + common.props.attio, + "listId", + ], + }, + }, + methods: { + ...common.methods, + getEventType() { + return "list-entry.updated"; + }, + getFilter() { + return { + "$and": [ + { + field: "id.list_id", + operator: "equals", + value: this.listId, + }, + ], + }; + }, + generateMeta(entry) { + const ts = Date.now(); + return { + id: `${entry.id.entry_id}-${ts}`, + summary: `Updated Entry with ID: ${entry.id.entry_id}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/attio/sources/list-entry-updated-instant/test-event.mjs b/components/attio/sources/list-entry-updated-instant/test-event.mjs new file mode 100644 index 0000000000000..1287e0fb841db --- /dev/null +++ b/components/attio/sources/list-entry-updated-instant/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "event_type": "list-entry.updated", + "id": { + "workspace_id": "21e6dc8b-aa9f-4970-bdce-fa207cf3a7d9", + "list_id": "dd41a385-c7cd-4832-ad57-c9a824b32ba4", + "entry_id": "d7b73c9c-5b91-45df-bf3f-677b9bd563eb", + "attribute_id": "82b07502-9c9d-4996-a46f-4a5b687015d5" + }, + "parent_object_id": "6a3d5e81-0de0-4e96-a535-957b28ba8211", + "parent_record_id": "6a3cce50-9589-4f94-be16-8aa0a80c46e1", + "actor": { + "type": "workspace-member", + "id": "3ccf848d-58e5-4ccb-88d7-d85944eda037" + } +} \ No newline at end of file diff --git a/components/attio/sources/new-list-entry-instant/new-list-entry-instant.mjs b/components/attio/sources/new-list-entry-instant/new-list-entry-instant.mjs new file mode 100644 index 0000000000000..886af1869c311 --- /dev/null +++ b/components/attio/sources/new-list-entry-instant/new-list-entry-instant.mjs @@ -0,0 +1,46 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "attio-new-list-entry-instant", + name: "New List Entry (Instant)", + description: "Emit new event when a record, such as person, company, or deal, is added to a list", + version: "0.0.2", + type: "source", + dedupe: "unique", + props: { + ...common.props, + listId: { + propDefinition: [ + common.props.attio, + "listId", + ], + }, + }, + methods: { + ...common.methods, + getEventType() { + return "list-entry.created"; + }, + getFilter() { + return { + "$and": [ + { + field: "id.list_id", + operator: "equals", + value: this.listId, + }, + ], + }; + }, + generateMeta(entry) { + return { + id: entry.id.entry_id, + summary: `New Entry with ID: ${entry.id.entry_id}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/attio/sources/new-list-entry-instant/test-event.mjs b/components/attio/sources/new-list-entry-instant/test-event.mjs new file mode 100644 index 0000000000000..8a57ba4e4f789 --- /dev/null +++ b/components/attio/sources/new-list-entry-instant/test-event.mjs @@ -0,0 +1,14 @@ +export default { + "event_type": "list-entry.created", + "id": { + "workspace_id": "14beef7a-99f7-4534-a87e-70b564330a4c", + "list_id": "33ebdbe9-e529-47c9-b894-0ba25e9c15c0", + "entry_id": "2e6e29ea-c4e0-4f44-842d-78a891f8c156" + }, + "parent_object_id": "97052eb9-e65e-443f-a297-f2d9a4a7f795", + "parent_record_id": "bf071e1f-6035-429d-b874-d83ea64ea13b", + "actor": { + "type": "workspace-member", + "id": "50cf242c-7fa3-4cad-87d0-75b1af71c57b" + } +} \ No newline at end of file diff --git a/components/attio/sources/new-note-instant/new-note-instant.mjs b/components/attio/sources/new-note-instant/new-note-instant.mjs new file mode 100644 index 0000000000000..96beab68b8b21 --- /dev/null +++ b/components/attio/sources/new-note-instant/new-note-instant.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "attio-new-note-instant", + name: "New Note (Instant)", + description: "Emit new event when a new note is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return "note.created"; + }, + generateMeta(note) { + return { + id: note.id.note_id, + summary: `New Note with ID: ${note.id.note_id}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/attio/sources/new-note-instant/test-event.mjs b/components/attio/sources/new-note-instant/test-event.mjs new file mode 100644 index 0000000000000..72901adeff289 --- /dev/null +++ b/components/attio/sources/new-note-instant/test-event.mjs @@ -0,0 +1,13 @@ +export default { + "event_type": "note.created", + "id": { + "workspace_id": "21e6dc8b-aa9f-4970-bdce-fa207cf3a7d9", + "note_id": "cc2a06ee-61ae-4a9b-bf1b-4e9774270061" + }, + "parent_object_id": "6a3d5e81-0de0-4e96-a535-957b28ba8211", + "parent_record_id": "64561036-ee99-47f7-926c-66bf43808dcf", + "actor": { + "type": "workspace-member", + "id": "3ccf848d-58e5-4ccb-88d7-d85944eda037" + } +} \ No newline at end of file diff --git a/components/attio/sources/new-object-attribute-instant/new-object-attribute-instant.mjs b/components/attio/sources/new-object-attribute-instant/new-object-attribute-instant.mjs new file mode 100644 index 0000000000000..f76c29e6268e4 --- /dev/null +++ b/components/attio/sources/new-object-attribute-instant/new-object-attribute-instant.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "attio-new-object-attribute-instant", + name: "New Object Attribute (Instant)", + description: "Emit new event when an object attribute is created (e.g. when defining a new attribute \"Rating\" on the company object)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return "object-attribute.created"; + }, + generateMeta(attribute) { + return { + id: attribute.id.attribute_id, + summary: `New Object Attribute with ID: ${attribute.id.attribute_id}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/attio/sources/new-object-attribute-instant/test-event.mjs b/components/attio/sources/new-object-attribute-instant/test-event.mjs new file mode 100644 index 0000000000000..a240821b2e2c1 --- /dev/null +++ b/components/attio/sources/new-object-attribute-instant/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "event_type": "object-attribute.created", + "id": { + "workspace_id": "21e6dc8b-aa9f-4970-bdce-fa207cf3a7d9", + "object_id": "6a3d5e81-0de0-4e96-a535-957b28ba8211", + "attribute_id": "44a18da2-582e-4e65-9349-9b49b9bb7a14" + }, + "actor": { + "type": "workspace-member", + "id": "3ccf848d-58e5-4ccb-88d7-d85944eda037" + } +} \ No newline at end of file diff --git a/components/attio/sources/new-record-created-instant/new-record-created-instant.mjs b/components/attio/sources/new-record-created-instant/new-record-created-instant.mjs new file mode 100644 index 0000000000000..6bf50c00fb8eb --- /dev/null +++ b/components/attio/sources/new-record-created-instant/new-record-created-instant.mjs @@ -0,0 +1,46 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "attio-new-record-created-instant", + name: "New Record Created (Instant)", + description: "Emit new event when new record, such as person, company or deal gets created", + version: "0.0.2", + type: "source", + dedupe: "unique", + props: { + ...common.props, + objectId: { + propDefinition: [ + common.props.attio, + "objectId", + ], + }, + }, + methods: { + ...common.methods, + getEventType() { + return "record.created"; + }, + getFilter() { + return { + "$and": [ + { + field: "id.object_id", + operator: "equals", + value: this.objectId, + }, + ], + }; + }, + generateMeta(record) { + return { + id: record.id.record_id, + summary: `New Record with ID: ${record.id.record_id}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/attio/sources/new-record-created-instant/test-event.mjs b/components/attio/sources/new-record-created-instant/test-event.mjs new file mode 100644 index 0000000000000..14e2dac6d638d --- /dev/null +++ b/components/attio/sources/new-record-created-instant/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "event_type": "record.created", + "id": { + "workspace_id": "14beef7a-99f7-4534-a87e-70b564330a4c", + "object_id": "97052eb9-e65e-443f-a297-f2d9a4a7f795", + "record_id": "bf071e1f-6035-429d-b874-d83ea64ea13b" + }, + "actor": { + "type": "workspace-member", + "id": "50cf242c-7fa3-4cad-87d0-75b1af71c57b" + } +} \ No newline at end of file diff --git a/components/attio/sources/note-updated-instant/note-updated-instant.mjs b/components/attio/sources/note-updated-instant/note-updated-instant.mjs new file mode 100644 index 0000000000000..64aaa4dc8fa5b --- /dev/null +++ b/components/attio/sources/note-updated-instant/note-updated-instant.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "attio-note-updated-instant", + name: "Note Updated (Instant)", + description: "Emit new event when the title of a note is modified. Body updates do not currently trigger webhooks.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return "note.updated"; + }, + generateMeta(note) { + return { + id: note.id.note_id, + summary: `Updated Note with ID: ${note.id.note_id}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/attio/sources/note-updated-instant/test-event.mjs b/components/attio/sources/note-updated-instant/test-event.mjs new file mode 100644 index 0000000000000..64531ddcb1e7b --- /dev/null +++ b/components/attio/sources/note-updated-instant/test-event.mjs @@ -0,0 +1,13 @@ +export default { + "event_type": "note.updated", + "id": { + "workspace_id": "21e6dc8b-aa9f-4970-bdce-fa207cf3a7d9", + "note_id": "cc2a06ee-61ae-4a9b-bf1b-4e9774270061" + }, + "parent_object_id": "6a3d5e81-0de0-4e96-a535-957b28ba8211", + "parent_record_id": "64561036-ee99-47f7-926c-66bf43808dcf", + "actor": { + "type": "workspace-member", + "id": "3ccf848d-58e5-4ccb-88d7-d85944eda037" + } +} \ No newline at end of file diff --git a/components/attio/sources/object-attribute-updated-instant/object-attribute-updated-instant.mjs b/components/attio/sources/object-attribute-updated-instant/object-attribute-updated-instant.mjs new file mode 100644 index 0000000000000..4ccb8c0112607 --- /dev/null +++ b/components/attio/sources/object-attribute-updated-instant/object-attribute-updated-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "attio-object-attribute-updated-instant", + name: "Object Attribute Updated (Instant)", + description: "Emit new event when an object attribute is updated (e.g. when renaming the \"Rating\" attribute to \"Score\" on the company object)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return "object-attribute.updated"; + }, + generateMeta(attribute) { + const ts = Date.now(); + return { + id: `${attribute.id.attribute_id}-${ts}`, + summary: `New Object Attribute with ID: ${attribute.id.attribute_id}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/attio/sources/object-attribute-updated-instant/test-event.mjs b/components/attio/sources/object-attribute-updated-instant/test-event.mjs new file mode 100644 index 0000000000000..c65e340708f1a --- /dev/null +++ b/components/attio/sources/object-attribute-updated-instant/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "event_type": "object-attribute.updated", + "id": { + "workspace_id": "21e6dc8b-aa9f-4970-bdce-fa207cf3a7d9", + "object_id": "6a3d5e81-0de0-4e96-a535-957b28ba8211", + "attribute_id": "44a18da2-582e-4e65-9349-9b49b9bb7a14" + }, + "actor": { + "type": "workspace-member", + "id": "3ccf848d-58e5-4ccb-88d7-d85944eda037" + } +} \ No newline at end of file diff --git a/components/attio/sources/record-updated-instant/record-updated-instant.mjs b/components/attio/sources/record-updated-instant/record-updated-instant.mjs new file mode 100644 index 0000000000000..cfe60ad7bdeef --- /dev/null +++ b/components/attio/sources/record-updated-instant/record-updated-instant.mjs @@ -0,0 +1,47 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "attio-record-updated-instant", + name: "Record Updated (Instant)", + description: "Emit new event when values on a record, such as person, company or deal, are updated", + version: "0.0.2", + type: "source", + dedupe: "unique", + props: { + ...common.props, + objectId: { + propDefinition: [ + common.props.attio, + "objectId", + ], + }, + }, + methods: { + ...common.methods, + getEventType() { + return "record.updated"; + }, + getFilter() { + return { + "$and": [ + { + field: "id.object_id", + operator: "equals", + value: this.objectId, + }, + ], + }; + }, + generateMeta(record) { + const ts = Date.now(); + return { + id: `${record.id.record_id}-${ts}`, + summary: `New Record with ID: ${record.id.record_id}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/attio/sources/record-updated-instant/test-event.mjs b/components/attio/sources/record-updated-instant/test-event.mjs new file mode 100644 index 0000000000000..ec4bff3c11864 --- /dev/null +++ b/components/attio/sources/record-updated-instant/test-event.mjs @@ -0,0 +1,13 @@ +export default { + "event_type": "record.updated", + "id": { + "workspace_id": "14beef7a-99f7-4534-a87e-70b564330a4c", + "object_id": "97052eb9-e65e-443f-a297-f2d9a4a7f795", + "record_id": "bf071e1f-6035-429d-b874-d83ea64ea13b", + "attribute_id": "41252299-f8c7-4b5e-99c9-4ff8321d2f96" + }, + "actor": { + "type": "workspace-member", + "id": "50cf242c-7fa3-4cad-87d0-75b1af71c57b" + } +} \ No newline at end of file diff --git a/components/attractwell/actions/create-update-contact/create-update-contact.mjs b/components/attractwell/actions/create-update-contact/create-update-contact.mjs new file mode 100644 index 0000000000000..9c5301430cbc7 --- /dev/null +++ b/components/attractwell/actions/create-update-contact/create-update-contact.mjs @@ -0,0 +1,353 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../attractwell.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "attractwell-create-update-contact", + name: "Create or Update Contact", + description: "Creates or updates a contact with the provided identification and contact details.", + version: "0.0.1", + type: "action", + props: { + app, + email: { + type: "string", + label: "Email", + description: "The email address of the contact.", + optional: true, + }, + mobilePhone: { + type: "string", + label: "Mobile Phone", + description: "The mobile phone number of the contact.", + optional: true, + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the contact.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the contact.", + optional: true, + }, + contactType: { + type: "string", + label: "Contact Type", + description: "The type of the contact.", + optional: true, + }, + rating: { + type: "string", + label: "Rating", + description: "The rating of the contact. From `0` (coldest) to `5` (hottest). You'll get periodic reminders of which contacts to reach out to more often if you choose a higher rating, or not at all if you pick `0`.", + default: "0", + options: [ + { + value: "0", + label: "No reminders", + }, + { + value: "1", + label: "Annual reminders", + }, + { + value: "2", + label: "Quarterly reminders", + }, + { + value: "3", + label: "Monthly reminders", + }, + { + value: "4", + label: "Weekly reminders", + }, + { + value: "5", + label: "Reminders every 3 days", + }, + ], + }, + workPhone: { + type: "string", + label: "Work Phone", + description: "The work phone number of the contact.", + optional: true, + }, + homePhone: { + type: "string", + label: "Home Phone", + description: "The home phone number of the contact.", + optional: true, + }, + address1: { + type: "string", + label: "Street Address", + description: "The street address of the contact.", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "The city of the contact.", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "The state of the contact.", + optional: true, + }, + postalCode: { + type: "string", + label: "Postal Code", + description: "The postal code of the contact.", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "The country of the contact.", + optional: true, + }, + companyName: { + type: "string", + label: "Company Name", + description: "The company name of the contact.", + optional: true, + }, + title: { + type: "string", + label: "Title", + description: "The title of the contact.", + optional: true, + }, + campaignContactEmail: { + type: "boolean", + label: "Send Campaigns By Email", + description: "The campaign contact email setting.", + default: true, + }, + campaignContactText: { + type: "boolean", + label: "Send Campaigns By Text", + description: "The campaign contact text setting.", + default: false, + }, + receiveMarketingEmail: { + type: "boolean", + label: "Opted Into Email", + description: "The receive marketing email setting.", + default: true, + }, + receiveMarketingText: { + type: "boolean", + label: "Opted Into Text", + description: "The receive marketing text setting.", + default: true, + }, + tagsToAdd: { + type: "string[]", + label: "Tags to Add", + description: "Tags to add to the contact.", + propDefinition: [ + app, + "tag", + ], + }, + tagsToRemove: { + type: "string[]", + label: "Tags to Remove", + description: "Tags to remove from the contact.", + propDefinition: [ + app, + "tag", + ], + }, + campaignsToAdd: { + type: "string[]", + label: "Campaigns to Add", + description: "If a contact isn't already receiving a campaign, start sending these campaigns to them.", + propDefinition: [ + app, + "campaignId", + ], + }, + campaignsToAddOrRestart: { + type: "string[]", + label: "Campaigns to Add or Restart", + description: "If a contact is already receiving a campaign, restart these campaigns. If a contact is not receiving a campaign, start sending these campaigns to them.", + propDefinition: [ + app, + "campaignId", + ], + }, + campaignsToRemove: { + type: "string[]", + label: "Campaigns to Remove", + description: "Campaigns to remove from the contact.", + propDefinition: [ + app, + "campaignId", + ], + }, + offlineCampaignsToAdd: { + type: "string[]", + label: "Offline Campaigns To Add", + description: "Offline campaigns to add to the contact.", + propDefinition: [ + app, + "campaignId", + ], + }, + offlineCampaignsToRemove: { + type: "string[]", + label: "Offline Campaigns To Remove", + description: "Offline campaigns to remove from the contact.", + propDefinition: [ + app, + "campaignId", + ], + }, + addToVaults: { + type: "string[]", + label: "Add To Vaults", + description: "Give Access To Vault (Contact Still Must Pay For Paid Vaults).", + propDefinition: [ + app, + "vaultId", + ], + }, + addToVaultsForFree: { + type: "string[]", + label: "Add To Vaults For Free", + description: "Give Access To Vault For Free (Contact Gets Free Access To Paid Vaults).", + propDefinition: [ + app, + "vaultId", + ], + }, + removeFromVaults: { + type: "string[]", + label: "Remove from Vaults", + description: "Vaults to remove the contact from.", + propDefinition: [ + app, + "vaultId", + ], + }, + automationsToRun: { + type: "string[]", + label: "Automations To Run", + description: "Automations to run for the contact.", + propDefinition: [ + app, + "automationId", + ], + }, + mayAccessMemberArea: { + type: "boolean", + label: "May Access Member Area", + description: "Whether the user may access or is banned from the member area. If this is set to `true`, they only are able to access the member area if they are also assigned to one or more vaults.", + default: true, + }, + }, + methods: { + fromBooleanToInt(value) { + return value === true + ? 1 + : 0; + }, + createOrUpdateContact(args = {}) { + return this.app.post({ + path: "/contacts", + ...args, + }); + }, + }, + async run({ $ }) { + const { + fromBooleanToInt, + createOrUpdateContact, + email, + mobilePhone, + firstName, + lastName, + contactType, + rating, + workPhone, + homePhone, + address1, + city, + state, + postalCode, + country, + companyName, + title, + campaignContactEmail, + campaignContactText, + receiveMarketingEmail, + receiveMarketingText, + tagsToAdd, + tagsToRemove, + campaignsToAdd, + campaignsToRemove, + offlineCampaignsToAdd, + offlineCampaignsToRemove, + addToVaults, + addToVaultsForFree, + removeFromVaults, + automationsToRun, + campaignsToAddOrRestart, + mayAccessMemberArea, + } = this; + + if (!email && !mobilePhone) { + throw new ConfigurationError("Either **Email** or **Mobile Phone** is required."); + } + + const response = await createOrUpdateContact({ + $, + data: { + contact_source: "API", + email, + mobile_phone: mobilePhone, + first_name: firstName, + last_name: lastName, + contact_type: contactType, + rating: parseInt(rating, 10), + work_phone: workPhone, + home_phone: homePhone, + address1, + city, + state, + postal_code: postalCode, + country, + company_name: companyName, + title, + campaign_contact_email: fromBooleanToInt(campaignContactEmail), + campaign_contact_text: fromBooleanToInt(campaignContactText), + receive_marketing_email: fromBooleanToInt(receiveMarketingEmail), + receive_marketing_text: fromBooleanToInt(receiveMarketingText), + tags_to_add: utils.parseArray(tagsToAdd), + tags_to_remove: utils.parseArray(tagsToRemove), + campaigns_to_add: campaignsToAdd, + campaigns_to_remove: campaignsToRemove, + offline_campaigns_to_add: offlineCampaignsToAdd, + offline_campaigns_to_remove: offlineCampaignsToRemove, + add_to_vaults: addToVaults, + add_to_vaults_for_free: addToVaultsForFree, + remove_from_vaults: removeFromVaults, + automations_to_run: automationsToRun, + campaigns_to_add_or_restart: campaignsToAddOrRestart, + may_access_member_area: fromBooleanToInt(mayAccessMemberArea), + }, + }); + $.export("$summary", `Successfully created or updated contact with ID \`${response.results.contact_id}\`.`); + return response; + }, +}; diff --git a/components/attractwell/actions/lesson-approval/lesson-approval.mjs b/components/attractwell/actions/lesson-approval/lesson-approval.mjs new file mode 100644 index 0000000000000..cc812fdc61428 --- /dev/null +++ b/components/attractwell/actions/lesson-approval/lesson-approval.mjs @@ -0,0 +1,81 @@ +import app from "../../attractwell.app.mjs"; + +export default { + key: "attractwell-lesson-approval", + name: "Lesson Approval", + description: "Approves, rejects, or unapproves a lesson in the AttractWell system based on the selected status.", + version: "0.0.1", + type: "action", + props: { + app, + approvalStatus: { + type: "string", + label: "Approval Status", + description: "The approval status to set for the lesson.", + options: [ + "approved", + "rejected", + "unapproved", + ], + }, + contactEmail: { + type: "string", + label: "Contact Email", + description: "The email address of the contact.", + }, + vaultId: { + optional: false, + propDefinition: [ + app, + "vaultId", + ], + }, + onlineClassLessonId: { + label: "Online Class Lesson ID", + description: "The ID of the online class lesson.", + optional: false, + propDefinition: [ + app, + "lessonId", + ], + }, + instructorComment: { + type: "string", + label: "Instructor Comment", + description: "The comment from the instructor.", + optional: true, + }, + }, + methods: { + lessonApproval(args = {}) { + return this.app.post({ + path: "/classes/lessons/approve", + ...args, + }); + }, + }, + async run({ $ }) { + const { + lessonApproval, + approvalStatus, + contactEmail, + vaultId, + onlineClassLessonId, + instructorComment, + } = this; + + const response = await lessonApproval({ + $, + data: { + approval_status: approvalStatus, + contact_email: contactEmail, + vault_id: vaultId, + online_class_lesson_id: onlineClassLessonId, + instructor_comment: instructorComment, + }, + }); + + $.export("$summary", "Successfully updated lesson approval status."); + return response; + }, +}; diff --git a/components/attractwell/attractwell.app.mjs b/components/attractwell/attractwell.app.mjs new file mode 100644 index 0000000000000..4fb53f72678d4 --- /dev/null +++ b/components/attractwell/attractwell.app.mjs @@ -0,0 +1,193 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "attractwell", + propDefinitions: { + tag: { + type: "string", + label: "Tag", + description: "The name of the tag.", + optional: true, + async options() { + const tags = await this.listContactTags(); + return tags.map(({ tag: value }) => value); + }, + }, + campaignId: { + type: "string", + label: "Campaign ID", + description: "The id of the campaign.", + optional: true, + async options() { + const campaigns = await this.listCampaigns(); + return campaigns.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + vaultId: { + type: "string", + label: "Vault ID", + description: "The id of the vault.", + optional: true, + async options() { + const vaults = await this.listVaults(); + return vaults.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + followUpPlanId: { + type: "string", + label: "Follow-Up Plan ID", + description: "The id of the follow-up plan.", + optional: true, + async options() { + const followUpPlans = await this.listFollowUpPlans(); + return followUpPlans.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + automationId: { + type: "string", + label: "Automation ID", + description: "The id of the automation.", + optional: true, + async options() { + const automations = await this.listAutomations(); + return automations.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + classId: { + type: "string", + label: "Class ID", + description: "The id of the class.", + optional: true, + async options({ page }) { + const classes = await this.listClasses({ + params: { + page, + }, + }); + return classes.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + lessonId: { + type: "string", + label: "Lesson ID", + description: "The id of the lesson.", + optional: true, + async options({ page }) { + const classLessons = await this.listClassLessons({ + params: { + page, + }, + }); + return classLessons.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "Content-Type": "application/json", + "Accept": "application/json", + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + listContactTags(args = {}) { + return this._makeRequest({ + path: "/contacts/tags", + ...args, + }); + }, + listCampaigns(args = {}) { + return this._makeRequest({ + path: "/campaigns", + ...args, + }); + }, + listVaults(args = {}) { + return this._makeRequest({ + path: "/vaults", + ...args, + }); + }, + listFollowUpPlans(args = {}) { + return this._makeRequest({ + path: "/follow-up-plans", + ...args, + }); + }, + listAutomations(args = {}) { + return this._makeRequest({ + path: "/automations", + ...args, + }); + }, + listClasses(args = {}) { + return this._makeRequest({ + path: "/classes", + ...args, + }); + }, + listClassLessons(args = {}) { + return this._makeRequest({ + path: "/classes/lessons", + ...args, + }); + }, + }, +}; diff --git a/components/attractwell/common/constants.mjs b/components/attractwell/common/constants.mjs new file mode 100644 index 0000000000000..447f450389fc9 --- /dev/null +++ b/components/attractwell/common/constants.mjs @@ -0,0 +1,9 @@ +const BASE_URL = "https://api.attractwell.com"; +const VERSION_PATH = "/api/v1"; +const TRIGGER_CONTEXT_ID = "triggerContextId"; + +export default { + BASE_URL, + VERSION_PATH, + TRIGGER_CONTEXT_ID, +}; diff --git a/components/attractwell/common/utils.mjs b/components/attractwell/common/utils.mjs new file mode 100644 index 0000000000000..2e10f7e6444f5 --- /dev/null +++ b/components/attractwell/common/utils.mjs @@ -0,0 +1,28 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function parseArray(value) { + try { + if (!value) { + return; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +export default { + parseArray, +}; diff --git a/components/attractwell/package.json b/components/attractwell/package.json new file mode 100644 index 0000000000000..5a7eb02944c7d --- /dev/null +++ b/components/attractwell/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/attractwell", + "version": "0.1.1", + "description": "Pipedream AttractWell Components", + "main": "attractwell.app.mjs", + "keywords": [ + "pipedream", + "attractwell" + ], + "homepage": "https://pipedream.com/apps/attractwell", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" + } +} diff --git a/components/attractwell/sources/common/trigger-contexts.mjs b/components/attractwell/sources/common/trigger-contexts.mjs new file mode 100644 index 0000000000000..d86c6f0dcdf33 --- /dev/null +++ b/components/attractwell/sources/common/trigger-contexts.mjs @@ -0,0 +1,9 @@ +export default { + PAGE: "page", + LANDING_PAGE: "landing_page", + CONTACT_ME: "contact_me", + BLOG_COMMENT: "blog_comment", + EVENT_REGISTRATION: "event_registration", + STORE: "store", + VAULT: "vault", +}; diff --git a/components/attractwell/sources/common/trigger-names.mjs b/components/attractwell/sources/common/trigger-names.mjs new file mode 100644 index 0000000000000..a20c6183434fb --- /dev/null +++ b/components/attractwell/sources/common/trigger-names.mjs @@ -0,0 +1,7 @@ +export default { + NEW_LEAD_OR_SALE: "new_lead_or_sale", + PAYMENT_FAILURE: "payment_failure", + JOIN: "join", + LEAVE: "leave", + PAYMENT_SUCCESS: "payment_success", +}; diff --git a/components/attractwell/sources/common/webhook.mjs b/components/attractwell/sources/common/webhook.mjs new file mode 100644 index 0000000000000..3be9be13c5db2 --- /dev/null +++ b/components/attractwell/sources/common/webhook.mjs @@ -0,0 +1,99 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../attractwell.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + http: "$.interface.http", + vaultId: { + optional: false, + propDefinition: [ + app, + "vaultId", + ], + }, + }, + hooks: { + async activate() { + const { + createWebhook, + http: { endpoint: hookUrl }, + vaultId, + getTriggerContext, + getTriggerName, + setTriggerContextId, + } = this; + + const triggerContextId = Date.now(); + + await createWebhook({ + data: { + hookUrl, + vault_id: vaultId, + trigger_name: getTriggerName(), + trigger_context: getTriggerContext(), + trigger_context_id: triggerContextId, + }, + }); + + setTriggerContextId(triggerContextId); + }, + async deactivate() { + const { + deleteWebhook, + vaultId, + getTriggerContextId, + getTriggerName, + getTriggerContext, + } = this; + + await deleteWebhook({ + data: { + vault_id: vaultId, + trigger_name: getTriggerName(), + trigger_context: getTriggerContext(), + trigger_context_id: getTriggerContextId(), + }, + }); + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getTriggerName() { + throw new ConfigurationError("getTriggerName is not implemented"); + }, + getTriggerContext() { + throw new ConfigurationError("getTriggerContext is not implemented"); + }, + setTriggerContextId(value) { + this.db.set(constants.TRIGGER_CONTEXT_ID, value); + }, + getTriggerContextId() { + return this.db.get(constants.TRIGGER_CONTEXT_ID); + }, + processResource(resource) { + this.$emit(resource, this.generateMeta(resource)); + }, + createWebhook(args = {}) { + return this.app.post({ + debug: true, + path: "/zapier/trigger", + ...args, + }); + }, + deleteWebhook(args = {}) { + return this.app.delete({ + debug: true, + path: "/zapier/trigger", + ...args, + }); + }, + }, + async run({ body }) { + this.processResource(body); + }, +}; diff --git a/components/attractwell/sources/contact-joins-vault-instant/contact-joins-vault-instant.mjs b/components/attractwell/sources/contact-joins-vault-instant/contact-joins-vault-instant.mjs new file mode 100644 index 0000000000000..fee6fda784361 --- /dev/null +++ b/components/attractwell/sources/contact-joins-vault-instant/contact-joins-vault-instant.mjs @@ -0,0 +1,29 @@ +import common from "../common/webhook.mjs"; +import triggerNames from "../common/trigger-names.mjs"; +import triggerContexts from "../common/trigger-contexts.mjs"; + +export default { + ...common, + key: "attractwell-contact-joins-vault-instant", + name: "Contact Joins Vault (Instant)", + description: "Emit new event when a contact becomes a new member of a vault.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTriggerName() { + return triggerNames.JOIN; + }, + getTriggerContext() { + return triggerContexts.VAULT; + }, + generateMeta(resource) { + return { + id: resource.contact_id, + summary: `New Contact: ${resource.contact_name}`, + ts: Date.now(), + }; + }, + }, +}; diff --git a/components/attractwell/sources/new-event-registration-instant/new-event-registration-instant.mjs b/components/attractwell/sources/new-event-registration-instant/new-event-registration-instant.mjs new file mode 100644 index 0000000000000..371cc84e7c88b --- /dev/null +++ b/components/attractwell/sources/new-event-registration-instant/new-event-registration-instant.mjs @@ -0,0 +1,30 @@ +import common from "../common/webhook.mjs"; +import triggerNames from "../common/trigger-names.mjs"; +import triggerContexts from "../common/trigger-contexts.mjs"; + +export default { + ...common, + key: "attractwell-new-event-registration-instant", + name: "New Event Registration (Instant)", + description: "Emit new event when a new registration for an event takes place.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTriggerName() { + return triggerNames.NEW_LEAD_OR_SALE; + }, + getTriggerContext() { + return triggerContexts.EVENT_REGISTRATION; + }, + generateMeta(resource) { + const ts = Date.now(); + return { + id: ts, + summary: `New Event: ${resource.name}`, + ts, + }; + }, + }, +}; diff --git a/components/attractwell/sources/new-lead-from-landing-page-instant/new-lead-from-landing-page-instant.mjs b/components/attractwell/sources/new-lead-from-landing-page-instant/new-lead-from-landing-page-instant.mjs new file mode 100644 index 0000000000000..d25edb0abf4f5 --- /dev/null +++ b/components/attractwell/sources/new-lead-from-landing-page-instant/new-lead-from-landing-page-instant.mjs @@ -0,0 +1,30 @@ +import common from "../common/webhook.mjs"; +import triggerNames from "../common/trigger-names.mjs"; +import triggerContexts from "../common/trigger-contexts.mjs"; + +export default { + ...common, + key: "attractwell-new-lead-from-landing-page-instant", + name: "New Lead from Landing Page (Instant)", + description: "Emit new event when a lead is gained from a landing page.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTriggerName() { + return triggerNames.NEW_LEAD_OR_SALE; + }, + getTriggerContext() { + return triggerContexts.LANDING_PAGE; + }, + generateMeta(resource) { + const ts = Date.now(); + return { + id: ts, + summary: `New Lead: ${resource.lead_source_id}`, + ts, + }; + }, + }, +}; diff --git a/components/auth0_management_api/README.md b/components/auth0_management_api/README.md index b7cc4d20cf96d..fe3c1b48f7bf2 100644 --- a/components/auth0_management_api/README.md +++ b/components/auth0_management_api/README.md @@ -30,4 +30,12 @@ This application enables you to use your own Auth0 machine-to-machine applicatio ![Copy API Audience URL](https://res.cloudinary.com/dpenc2lit/image/upload/v1702077306/Screenshot_2023-12-08_at_3.14.54_PM_blz4pg.png) -7. You should now have the credentials you need to connect your Auth0 application to Pipedream! \ No newline at end of file +7. You should now have the credentials you need to connect your Auth0 application to Pipedream! + +# Example Use Cases + +- **User Provisioning and Deprovisioning**: Automate the user lifecycle by syncing new user accounts from a HR management platform like BambooHR to Auth0. Conversely, when employees leave, ensure their Auth0 account is deactivated. This can help maintain security and reduce manual overhead. + +- **Role and Permission Updates**: When a user's role changes internally, a workflow can listen for the event from a service like Slack or a database update, and subsequently adjust their roles and permissions in Auth0. This ensures that users have the right access at the right time. + +- **Custom Alerts and Monitoring**: Set up a workflow to monitor Auth0 logs for specific events, such as failed logins or configuration changes. When an event of interest occurs, trigger notifications through channels like email, SMS via Twilio, or messaging platforms like Microsoft Teams to keep your team informed and responsive to potential issues. \ No newline at end of file diff --git a/components/auth0_management_api/package.json b/components/auth0_management_api/package.json new file mode 100644 index 0000000000000..8b3b6407ed486 --- /dev/null +++ b/components/auth0_management_api/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/auth0_management_api", + "version": "0.6.0", + "description": "Pipedream auth0_management_api Components", + "main": "auth0_management_api.app.mjs", + "keywords": [ + "pipedream", + "auth0_management_api" + ], + "homepage": "https://pipedream.com/apps/auth0_management_api", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/autoblogger/actions/get-blogposts/get-blogposts.mjs b/components/autoblogger/actions/get-blogposts/get-blogposts.mjs new file mode 100644 index 0000000000000..646f687bd6295 --- /dev/null +++ b/components/autoblogger/actions/get-blogposts/get-blogposts.mjs @@ -0,0 +1,22 @@ +import app from "../../autoblogger.app.mjs"; + +export default { + key: "autoblogger-get-blogposts", + name: "Get Blogposts", + description: "Retrieves blogposts using the API key. [See the documentation](https://u.pcloud.link/publink/show?code=XZdjuv0ZtabS8BN58thUiE8FGjznajoMc6Qy)", + version: "0.0.1", + type: "action", + props: { + app, + }, + + async run({ $ }) { + const response = await this.app.getBlogposts({ + $, + }); + + $.export("$summary", `Successfully retrieved ${response.length} blogposts`); + + return response; + }, +}; diff --git a/components/autoblogger/actions/validate-api-key/validate-api-key.mjs b/components/autoblogger/actions/validate-api-key/validate-api-key.mjs new file mode 100644 index 0000000000000..9fca04302f918 --- /dev/null +++ b/components/autoblogger/actions/validate-api-key/validate-api-key.mjs @@ -0,0 +1,23 @@ +import app from "../../autoblogger.app.mjs"; + +export default { + key: "autoblogger-validate-api-key", + name: "Validate API Key", + description: "Validates the provided API key. [See the documentation](https://u.pcloud.link/publink/show?code=XZdjuv0ZtabS8BN58thUiE8FGjznajoMc6Qy)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.validateKey({ + $, + }); + + $.export("$summary", `API Key ${response.is_valid + ? "is" + : "isn't"} valid`); + + return response; + }, +}; diff --git a/components/autoblogger/autoblogger.app.mjs b/components/autoblogger/autoblogger.app.mjs new file mode 100644 index 0000000000000..408803f971739 --- /dev/null +++ b/components/autoblogger/autoblogger.app.mjs @@ -0,0 +1,41 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "autoblogger", + propDefinitions: {}, + methods: { + _baseUrl() { + return "https://autoblogger-api.otherweb.com"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "x-api-key": `${this.$auth.api_key}`, + "Accept": "application/json", + }, + }); + }, + async validateKey(args = {}) { + return this._makeRequest({ + path: "/api/v1/site/validate/apikey", + ...args, + }); + }, + async getBlogposts(args = {}) { + return this._makeRequest({ + path: "/api/v1/blogposts", + ...args, + }); + }, + }, +}; diff --git a/components/autoblogger/package.json b/components/autoblogger/package.json new file mode 100644 index 0000000000000..3e5ddee0c3e8e --- /dev/null +++ b/components/autoblogger/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/autoblogger", + "version": "0.1.0", + "description": "Pipedream AutoBlogger Components", + "main": "autoblogger.app.mjs", + "keywords": [ + "pipedream", + "autoblogger" + ], + "homepage": "https://pipedream.com/apps/autoblogger", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/autobound/README.md b/components/autobound/README.md new file mode 100644 index 0000000000000..e0c862bda403f --- /dev/null +++ b/components/autobound/README.md @@ -0,0 +1,11 @@ +# Overview + +The Autobound API enables sales teams to enhance outreach by providing targeted sales communications. By leveraging data like industry news, hiring changes, and company growth, Autobound helps to personalize sales pitches and increase the relevance of your messages. With Pipedream, you can automate workflows using the Autobound API, connecting sales insights with other apps to create powerful sales automation systems, track engagement, and streamline communications. + +# Example Use Cases + +- **Automated Lead Prioritization**: Connect Autobound with your CRM platform on Pipedream to automatically update lead scores based on fresh Autobound insights. For example, if Autobound flags a company as experiencing significant growth, increase that lead's score in your CRM. + +- **Personalized Outreach Sequences**: Use Autobound's insights to trigger personalized email sequences from your email platform. When new data indicates a potential lead's increased readiness to buy, Pipedream can push this trigger to your email tool to start an outreach sequence tailored to that lead's specific context. + +- **Sales Alerts and Reports**: Combine Autobound with messaging apps like Slack to send instant alerts or daily summaries about key sales opportunities to your team. When Autobound identifies a high-value sales opportunity, Pipedream can automate an alert to the appropriate sales rep or channel. diff --git a/components/autobound/autobound.app.mjs b/components/autobound/autobound.app.mjs new file mode 100644 index 0000000000000..4c9acaccef9dd --- /dev/null +++ b/components/autobound/autobound.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "autobound", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/autobound/package.json b/components/autobound/package.json new file mode 100644 index 0000000000000..38c4dedaf7f62 --- /dev/null +++ b/components/autobound/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/autobound", + "version": "0.0.1", + "description": "Pipedream Autobound Components", + "main": "autobound.app.mjs", + "keywords": [ + "pipedream", + "autobound" + ], + "homepage": "https://pipedream.com/apps/autobound", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/autodesk/actions/create-folder/create-folder.mjs b/components/autodesk/actions/create-folder/create-folder.mjs new file mode 100644 index 0000000000000..e4b5d589a199c --- /dev/null +++ b/components/autodesk/actions/create-folder/create-folder.mjs @@ -0,0 +1,92 @@ +import autodesk from "../../autodesk.app.mjs"; + +export default { + key: "autodesk-create-folder", + name: "Create Folder", + description: "Creates a new folder in a project in Autodesk. [See the documentation](https://aps.autodesk.com/en/docs/data/v2/reference/http/projects-project_id-folders-POST/)", + version: "0.0.1", + type: "action", + props: { + autodesk, + hubId: { + propDefinition: [ + autodesk, + "hubId", + ], + }, + projectId: { + propDefinition: [ + autodesk, + "projectId", + (c) => ({ + hubId: c.hubId, + }), + ], + }, + name: { + type: "string", + label: "Name", + description: "The name of the new folder", + }, + parent: { + propDefinition: [ + autodesk, + "folderId", + (c) => ({ + hubId: c.hubId, + projectId: c.projectId, + }), + ], + label: "Parent Folder ID", + description: "The identifier of the parent folder", + }, + type: { + type: "string", + label: "Extension Type", + description: "The type of folder extension. For BIM 360 Docs folders, use `folders:autodesk.bim360:Folder`. For all other services, use `folders:autodesk.core:Folder`.", + options: [ + { + label: "BIM 360 Docs folders", + value: "folders:autodesk.core:Folder", + }, + { + label: "Other folders", + value: "folders:autodesk.bim360:Folder", + }, + ], + default: "folders:autodesk.core:Folder", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.autodesk.createFolder({ + $, + projectId: this.projectId, + data: { + jsonapi: { + version: "1.0", + }, + data: { + type: "folders", + attributes: { + name: this.name, + extension: { + type: this.type, + version: "1.0", + }, + }, + relationships: { + parent: { + data: { + type: "folders", + id: this.parent, + }, + }, + }, + }, + }, + }); + $.export("$summary", `Successfully created new folder: ${this.name}`); + return response; + }, +}; diff --git a/components/autodesk/actions/upload-file/upload-file.mjs b/components/autodesk/actions/upload-file/upload-file.mjs new file mode 100644 index 0000000000000..f04a1718e6a08 --- /dev/null +++ b/components/autodesk/actions/upload-file/upload-file.mjs @@ -0,0 +1,197 @@ +import autodesk from "../../autodesk.app.mjs"; +import { axios } from "@pipedream/platform"; +import fs from "fs"; + +export default { + key: "autodesk-upload-file", + name: "Upload File", + description: "Uploads a new file to a specified folder in Autodesk. [See the documentation](https://aps.autodesk.com/en/docs/data/v2/tutorials/upload-file/).", + version: "0.0.1", + type: "action", + props: { + autodesk, + hubId: { + propDefinition: [ + autodesk, + "hubId", + ], + }, + projectId: { + propDefinition: [ + autodesk, + "projectId", + (c) => ({ + hubId: c.hubId, + }), + ], + }, + folderId: { + propDefinition: [ + autodesk, + "folderId", + (c) => ({ + hubId: c.hubId, + projectId: c.projectId, + }), + ], + }, + fileName: { + type: "string", + label: "File Name", + description: "The name of the file to upload", + }, + filePath: { + type: "string", + label: "File Path", + description: "The path to a file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + }, + type: { + type: "string", + label: "Extension Type", + description: "The type of file extension. For BIM 360 Docs files, use `items:autodesk.bim360:File`. For all other services, use `items:autodesk.core:File`.", + options: [ + { + label: "BIM 360 Docs files", + value: "items:autodesk.core:File", + }, + { + label: "Other files", + value: "items:autodesk.bim360:File", + }, + ], + default: "items:autodesk.core:File", + optional: true, + }, + }, + async run({ $ }) { + // Create storage location + const { data } = await this.autodesk.createStorageLocation({ + $, + projectId: this.projectId, + data: { + jsonapi: { + version: "1.0", + }, + data: { + type: "objects", + attributes: { + name: this.fileName, + }, + relationships: { + target: { + data: { + type: "folders", + id: this.folderId, + }, + }, + }, + }, + }, + }); + + const objectId = data.id; + const [ + bucketKey, + objectKey, + ] = objectId.split("os.object:")[1]?.split("/") || []; + + // Generate signed URL + const { + urls, uploadKey, + } = await this.autodesk.generateSignedUrl({ + $, + bucketKey, + objectKey, + }); + + const signedUrl = urls[0]; + + // Upload to signed URL + const filePath = this.filePath.includes("tmp/") + ? this.filePath + : `/tmp/${this.filePath}`; + const fileStream = fs.createReadStream(filePath); + const fileSize = fs.statSync(filePath).size; + + await axios($, { + url: signedUrl, + data: fileStream, + method: "PUT", + headers: { + "Content-Type": "application/octet-stream", + "Content-Length": fileSize, + }, + maxContentLength: Infinity, + maxBodyLength: Infinity, + }); + + // Complete the upload + await this.autodesk.completeUpload({ + $, + bucketKey, + objectKey, + data: { + uploadKey, + }, + }); + + // Create version 1.0 of uploaded file + const response = await this.autodesk.createFirstVersionOfFile({ + $, + projectId: this.projectId, + data: { + jsonapi: { + version: "1.0", + }, + data: { + type: "items", + attributes: { + displayName: this.fileName, + extension: { + type: this.type, + version: "1.0", + }, + }, + relationships: { + tip: { + data: { + type: "versions", + id: "1", + }, + }, + parent: { + data: { + type: "folders", + id: this.folderId, + }, + }, + }, + }, + included: [ + { + type: "versions", + id: "1", + attributes: { + name: this.fileName, + extension: { + type: this.type.replace("items", "versions"), + version: "1.0", + }, + }, + relationships: { + storage: { + data: { + type: "objects", + id: objectId, + }, + }, + }, + }, + ], + }, + }); + + $.export("$summary", `Successfully uploaded file ${this.fileName}`); + return response; + }, +}; diff --git a/components/autodesk/autodesk.app.mjs b/components/autodesk/autodesk.app.mjs new file mode 100644 index 0000000000000..8c9d9f84b2ef2 --- /dev/null +++ b/components/autodesk/autodesk.app.mjs @@ -0,0 +1,246 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "autodesk", + propDefinitions: { + hubId: { + type: "string", + label: "Hub ID", + description: "The identifier of a hub", + async options() { + const { data } = await this.listHubs(); + return data?.map(({ + id, attributes, + }) => ({ + label: attributes.name, + value: id, + })) || []; + }, + }, + projectId: { + type: "string", + label: "Project", + description: "The identifier of a project", + async options({ + hubId, page, + }) { + if (!hubId) { + return []; + } + const { data } = await this.listProjects({ + hubId, + params: { + "page[number]": page, + }, + }); + return data?.map(({ + id, attributes, + }) => ({ + label: attributes.name, + value: id, + })) || []; + }, + }, + folderId: { + type: "string", + label: "Folder ID", + description: "The identifier of a folder", + async options({ + hubId, projectId, + }) { + if (!hubId || !projectId) { + return []; + } + const rootFolderId = await this.getProjectTopFolderId({ + hubId, + projectId, + }); + const options = [ + { + label: "Root Folder", + value: rootFolderId, + }, + ]; + + const fetchFoldersRecursively = async (folderId, depth = 0, maxDepth = 10) => { + if (depth > maxDepth) { + return; + } + const { data } = await this.getFolderContent({ + projectId, + folderId, + params: { + "filter[type]": "folders", + }, + }); + const folders = data?.map(({ + id, attributes, + }) => ({ + label: attributes.name, + value: id, + })) || []; + + options.push(...folders); + + for (const folder of folders) { + await fetchFoldersRecursively(folder.value, depth + 1, maxDepth); + } + }; + + await fetchFoldersRecursively(rootFolderId); + + return options; + }, + }, + }, + methods: { + _baseUrl() { + return "https://developer.api.autodesk.com"; + }, + _makeRequest({ + $ = this, + path, + headers, + ...otherOpts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "Content-Type": "application/json", + ...headers, + }, + ...otherOpts, + }); + }, + createWebhook({ + system, event, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/webhooks/v1/systems/${system}/events/${event}/hooks`, + ...opts, + }); + }, + deleteWebhook({ + system, event, hookId, + }) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/v1/systems/${system}/events/${event}/hooks/${hookId}`, + }); + }, + listHubs(opts = {}) { + return this._makeRequest({ + path: "/project/v1/hubs", + ...opts, + }); + }, + listProjects({ + hubId, ...opts + }) { + return this._makeRequest({ + path: `/project/v1/hubs/${hubId}/projects`, + ...opts, + }); + }, + async getProjectTopFolderId({ + hubId, projectId, ...opts + }) { + const { data } = await this._makeRequest({ + path: `/project/v1/hubs/${hubId}/projects/${projectId}/topFolders`, + ...opts, + }); + return data[0].id; + }, + getFolderContent({ + projectId, folderId, ...opts + }) { + return this._makeRequest({ + path: `/data/v1/projects/${projectId}/folders/${folderId}/contents`, + ...opts, + }); + }, + createFolder({ + projectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/data/v1/projects/${projectId}/folders`, + headers: { + "Content-Type": "application/vnd.api+json", + }, + ...opts, + }); + }, + createStorageLocation({ + projectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/data/v1/projects/${projectId}/storage`, + headers: { + "Content-Type": "application/vnd.api+json", + }, + ...opts, + }); + }, + generateSignedUrl({ + bucketKey, objectKey, ...opts + }) { + return this._makeRequest({ + path: `/oss/v2/buckets/${bucketKey}/objects/${objectKey}/signeds3upload`, + ...opts, + }); + }, + completeUpload({ + bucketKey, objectKey, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/oss/v2/buckets/${bucketKey}/objects/${objectKey}/signeds3upload`, + ...opts, + }); + }, + createFirstVersionOfFile({ + projectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/data/v1/projects/${projectId}/items`, + headers: { + "Content-Type": "application/vnd.api+json", + }, + ...opts, + }); + }, + async *paginate({ + fn, + args, + max, + }) { + let hasMore = true; + let count = 0; + args = { + ...args, + params: { + ...args?.params, + "page[number]": 0, + "page[limit]": 200, + }, + }; + while (hasMore) { + const { data } = await fn(args); + for (const item of data) { + yield item; + if (max && ++count >= max) { + return; + } + } + hasMore = data?.length === args.params["page[limit]"]; + args.params["page[number]"] += 1; + } + }, + }, +}; diff --git a/components/autodesk/package.json b/components/autodesk/package.json new file mode 100644 index 0000000000000..edab34b5a08b0 --- /dev/null +++ b/components/autodesk/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/autodesk", + "version": "0.1.0", + "description": "Pipedream Autodesk Components", + "main": "autodesk.app.mjs", + "keywords": [ + "pipedream", + "autodesk" + ], + "homepage": "https://pipedream.com/apps/autodesk", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/autodesk/sources/common/base-polling.mjs b/components/autodesk/sources/common/base-polling.mjs new file mode 100644 index 0000000000000..ca28c59971ddd --- /dev/null +++ b/components/autodesk/sources/common/base-polling.mjs @@ -0,0 +1,51 @@ +import autodesk from "../../autodesk.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + autodesk, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + async processEvent(max) { + const items = this.autodesk.paginate({ + fn: this.getFn(), + args: this.getArgs(), + max, + }); + + const results = []; + for await (const item of items) { + results.push(item); + } + + results.forEach((result) => { + const meta = this.generateMeta(result); + this.$emit(result, meta); + }); + }, + getFn() { + throw new Error("getFn is not implemented"); + }, + getArgs() { + return {}; + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/autodesk/sources/common/base-webhook.mjs b/components/autodesk/sources/common/base-webhook.mjs new file mode 100644 index 0000000000000..dd2cb1423ef53 --- /dev/null +++ b/components/autodesk/sources/common/base-webhook.mjs @@ -0,0 +1,95 @@ +import autodesk from "../../autodesk.app.mjs"; + +export default { + props: { + autodesk, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + hubId: { + propDefinition: [ + autodesk, + "hubId", + ], + }, + projectId: { + propDefinition: [ + autodesk, + "projectId", + (c) => ({ + hubId: c.hubId, + }), + ], + }, + folderId: { + propDefinition: [ + autodesk, + "folderId", + (c) => ({ + hubId: c.hubId, + projectId: c.projectId, + }), + ], + }, + }, + hooks: { + async activate() { + const { headers: { location } } = await this.autodesk.createWebhook({ + system: "data", + event: this.getEvent(), + data: { + callbackUrl: this.http.endpoint, + scope: { + folder: this.folderId, + }, + hubId: this.hubId, + projectId: this.projectId, + autoReactivateHook: true, + }, + returnFullResponse: true, + }); + if (!location) { + throw new Error("Could not create webhook"); + } + const hookId = location.split("/").pop(); + this._setHookId(hookId); + }, + async deactivate() { + const hookId = this._getHookId(); + if (hookId) { + await this.autodesk.deleteWebhook({ + hookId, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getEvent() { + throw new Error("getEvent is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run(event) { + this.http.respond({ + status: 200, + }); + + const { body: { payload } } = event; + if (!payload) { + return; + } + + const meta = this.generateMeta(payload); + this.$emit(payload, meta); + }, +}; diff --git a/components/autodesk/sources/new-folder-instant/new-folder-instant.mjs b/components/autodesk/sources/new-folder-instant/new-folder-instant.mjs new file mode 100644 index 0000000000000..288c8459908cd --- /dev/null +++ b/components/autodesk/sources/new-folder-instant/new-folder-instant.mjs @@ -0,0 +1,26 @@ +import common from "../common/base-webhook.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "autodesk-new-folder-instant", + name: "New Folder Created (Instant)", + description: "Emit new event when a folder is added to a specified folder in Autodesk. [See the documentation](https://aps.autodesk.com/en/docs/webhooks/v1/reference/http/webhooks/systems-system-events-event-hooks-POST/)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return "dm.folder.added"; + }, + generateMeta(payload) { + return { + id: payload.source, + summary: `New Folder Created: ${payload.name}`, + ts: Date.parse(payload.createdTime), + }; + }, + }, + sampleEmit, +}; diff --git a/components/autodesk/sources/new-folder-instant/test-event.mjs b/components/autodesk/sources/new-folder-instant/test-event.mjs new file mode 100644 index 0000000000000..1af723c45d371 --- /dev/null +++ b/components/autodesk/sources/new-folder-instant/test-event.mjs @@ -0,0 +1,37 @@ +export default { + "modifiedTime": "2025-01-20T21:24:41+0000", + "creator": "7QNJ9NNCKNKRJ8ZU", + "hidden": false, + "folderAggregatePath": "/tenant-57405840/group-470016059/folder/subfolder", + "indexable": false, + "source": "urn:adsk.wipprod:fs.folder:co.NDQS1d9xSZWYNKPIXn1WBQ", + "user_info": { + "id": "7QNJ9NNCKNKRJ8ZU" + }, + "name": "subfolder", + "context": { + "operation": "PostFolders" + }, + "createdTime": "2025-01-20T21:24:41+0000", + "modifiedBy": "7QNJ9NNCKNKRJ8ZU", + "parentFolderUrn": "urn:adsk.wipprod:fs.folder:co.g9HYzAN-QbKWxdHTtLDfog", + "ancestors": [ + { + "name": "pipedream", + "urn": "urn:adsk.wipprod:fs.folder:co.jAXxcF7IQQSUHIhZ1CPHGA" + }, + { + "name": "Default Project", + "urn": "urn:adsk.wipprod:fs.folder:co.6Nb9sJXVRNKRVbgmZyS4oQ" + }, + { + "name": "folder", + "urn": "urn:adsk.wipprod:fs.folder:co.g9HYzAN-QbKWxdHTtLDfog" + } + ], + "project": "470016059", + "tenant": "57405840", + "custom-metadata": { + "fileName": "subfolder" + } +} \ No newline at end of file diff --git a/components/autodesk/sources/new-project-created/new-project-created.mjs b/components/autodesk/sources/new-project-created/new-project-created.mjs new file mode 100644 index 0000000000000..2451451d4b8c2 --- /dev/null +++ b/components/autodesk/sources/new-project-created/new-project-created.mjs @@ -0,0 +1,40 @@ +import common from "../common/base-polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "autodesk-new-project-created", + name: "New Project Created", + description: "Emit new event when a new project is created in Autodesk. [See the documentation](https://aps.autodesk.com/en/docs/data/v2/reference/http/hubs-hub_id-projects-GET/)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + hubId: { + propDefinition: [ + common.props.autodesk, + "hubId", + ], + }, + }, + methods: { + ...common.methods, + getFn() { + return this.autodesk.listProjects; + }, + getArgs() { + return { + hubId: this.hubId, + }; + }, + generateMeta(project) { + return { + id: project.id, + summary: `New Project: ${project.attributes.name}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/autodesk/sources/new-project-created/test-event.mjs b/components/autodesk/sources/new-project-created/test-event.mjs new file mode 100644 index 0000000000000..1f8ca0b083c01 --- /dev/null +++ b/components/autodesk/sources/new-project-created/test-event.mjs @@ -0,0 +1,54 @@ +export default { + "type": "projects", + "id": "a.YnVzaW5lc3M6cGlwZWRyZWFtI0QyMDI1MDEyMDg2Mjg5MDYwNg", + "attributes": { + "name": "Default Project", + "scopes": [ + "global" + ], + "extension": { + "type": "projects:autodesk.core:Project", + "version": "1.0", + "schema": { + "href": "https://developer.api.autodesk.com/schema/v1/versions/projects:autodesk.core:Project-1.0" + }, + "data": {} + } + }, + "links": { + "self": { + "href": "https://developer.api.autodesk.com/project/v1/hubs/a.YnVzaW5lc3M6cGlwZWRyZWFt/projects/a.YnVzaW5lc3M6cGlwZWRyZWFtI0QyMDI1MDEyMDg2Mjg5MDYwNg" + } + }, + "relationships": { + "hub": { + "data": { + "type": "hubs", + "id": "a.YnVzaW5lc3M6cGlwZWRyZWFt" + }, + "links": { + "related": { + "href": "https://developer.api.autodesk.com/project/v1/hubs/a.YnVzaW5lc3M6cGlwZWRyZWFt" + } + } + }, + "rootFolder": { + "data": { + "type": "folders", + "id": "urn:adsk.wipprod:fs.folder:co.6Nb9sJXVRNKRVbgmZyS4oQ" + }, + "meta": { + "link": { + "href": "https://developer.api.autodesk.com/data/v1/projects/a.YnVzaW5lc3M6cGlwZWRyZWFtI0QyMDI1MDEyMDg2Mjg5MDYwNg/folders/urn:adsk.wipprod:fs.folder:co.6Nb9sJXVRNKRVbgmZyS4oQ" + } + } + }, + "topFolders": { + "links": { + "related": { + "href": "https://developer.api.autodesk.com/project/v1/hubs/a.YnVzaW5lc3M6cGlwZWRyZWFt/projects/a.YnVzaW5lc3M6cGlwZWRyZWFtI0QyMDI1MDEyMDg2Mjg5MDYwNg/topFolders" + } + } + } + } +} \ No newline at end of file diff --git a/components/autodesk/sources/new-version-instant/new-version-instant.mjs b/components/autodesk/sources/new-version-instant/new-version-instant.mjs new file mode 100644 index 0000000000000..fcfd2c64807ce --- /dev/null +++ b/components/autodesk/sources/new-version-instant/new-version-instant.mjs @@ -0,0 +1,26 @@ +import common from "../common/base-webhook.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "autodesk-new-version-instant", + name: "New File Version Created (Instant)", + description: "Emit new event when a new version of a file is created in Autodesk. [See the documentation](https://aps.autodesk.com/en/docs/webhooks/v1/reference/http/webhooks/systems-system-events-event-hooks-POST/)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return "dm.version.added"; + }, + generateMeta(payload) { + return { + id: payload.source, + summary: `New File Version for File: ${payload.name}`, + ts: Date.parse(payload.createdTime), + }; + }, + }, + sampleEmit, +}; diff --git a/components/autodesk/sources/new-version-instant/test-event.mjs b/components/autodesk/sources/new-version-instant/test-event.mjs new file mode 100644 index 0000000000000..106b0776d727a --- /dev/null +++ b/components/autodesk/sources/new-version-instant/test-event.mjs @@ -0,0 +1,62 @@ +export default { + "ext": "jpg", + "modifiedTime": "2025-01-20T21:33:05+0000", + "creator": "7QNJ9NNCKNKRJ8ZU", + "lineageUrn": "urn:adsk.wipprod:dm.lineage:ghXmW_6LQNKrcMoezksabw", + "sizeInBytes": 463713, + "hidden": false, + "indexable": true, + "source": "urn:adsk.wipprod:fs.file:vf.ghXmW_6LQNKrcMoezksabw?version=1", + "mimeType": "application/image", + "version": "1", + "user_info": { + "id": "7QNJ9NNCKNKRJ8ZU" + }, + "name": "file.jpg", + "context": { + "lineage": { + "reserved": false, + "reservedUserName": null, + "reservedUserId": null, + "reservedTime": null, + "unreservedUserName": null, + "unreservedUserId": null, + "unreservedTime": null, + "createUserId": "7QNJ9NNCKNKRJ8ZU", + "createTime": "2025-01-20T21:33:05+0000", + "createUserName": "", + "lastModifiedUserId": "7QNJ9NNCKNKRJ8ZU", + "lastModifiedTime": "2025-01-20T21:33:05+0000", + "lastModifiedUserName": "" + }, + "operation": "PostVersionedFiles" + }, + "createdTime": "2025-01-20T21:33:05+0000", + "modifiedBy": "7QNJ9NNCKNKRJ8ZU", + "state": "CONTENT_AVAILABLE", + "parentFolderUrn": "urn:adsk.wipprod:fs.folder:co.NDQS1d9xSZWYNKPIXn1WBQ", + "ancestors": [ + { + "name": "pipedream", + "urn": "urn:adsk.wipprod:fs.folder:co.jAXxcF7IQQSUHIhZ1CPHGA" + }, + { + "name": "Default Project", + "urn": "urn:adsk.wipprod:fs.folder:co.6Nb9sJXVRNKRVbgmZyS4oQ" + }, + { + "name": "folder", + "urn": "urn:adsk.wipprod:fs.folder:co.g9HYzAN-QbKWxdHTtLDfog" + }, + { + "name": "subfolder", + "urn": "urn:adsk.wipprod:fs.folder:co.NDQS1d9xSZWYNKPIXn1WBQ" + } + ], + "project": "470016059", + "tenant": "57405840", + "custom-metadata": { + "lineageTitle": "file.jpg", + "fileName": "file.jpg" + } +} \ No newline at end of file diff --git a/components/autoklose/README.md b/components/autoklose/README.md index cb54a2fd93e36..ecf510d537d8d 100644 --- a/components/autoklose/README.md +++ b/components/autoklose/README.md @@ -1,15 +1,11 @@ # Overview -You can use the Autoklose API to build apps that connect to your Autoklose -account and automate your email marketing tasks. - -Here are some examples of what you can build with the Autoklose API: - -- A lead capture form that automatically adds new contacts to your Autoklose - account -- An email template builder that lets you create and edit email templates - directly in your Autoklose account -- A campaign scheduling tool that lets you automate your email marketing - campaigns -- A reporting tool that gives you insights into your email marketing - performance +The Autoklose API taps into the power of sales automation, enabling users to streamline email campaigns, manage contacts, and track performance metrics without manual effort. By leveraging the API with Pipedream, you can craft serverless workflows that react to various triggers, such as new subscribers or campaign events, and automate actions like updating CRM records or initiating follow-up tasks. This orchestration is key to nurturing leads efficiently and can dramatically improve a sales team's productivity. + +# Example Use Cases + +- **Sync New Leads to CRM**: When a new lead is captured in Autoklose, use Pipedream to automatically add or update the lead's details in a CRM like Salesforce or HubSpot. This ensures that your sales team always has up-to-date information and can follow up promptly. + +- **Automated Campaign Follow-ups**: After an email campaign is sent via Autoklose, set up a workflow on Pipedream to monitor open and click-through rates. If a recipient engages with the email, trigger a personalized follow-up message or task creation for further engagement, thus increasing the chances of conversion. + +- **Real-time Performance Dashboards**: Create a workflow that fetches campaign metrics from Autoklose and sends the data to a BI tool like Google Data Studio or Tableau. This allows for real-time visualization of campaign performance and can inform timely strategic decisions. diff --git a/components/autom/README.md b/components/autom/README.md new file mode 100644 index 0000000000000..16d5978472d32 --- /dev/null +++ b/components/autom/README.md @@ -0,0 +1,11 @@ +# Overview + +Autom API offers a rich set of features for creating, managing, and deploying automations. With its capabilities, you can streamline tasks by setting up triggers and actions based on certain conditions. When used within Pipedream, you can harness these features to create robust, serverless workflows that integrate with numerous other services and APIs. Autom API's interaction with Pipedream allows for building scalable automation solutions that respond to real-time events and connect with a multitude of apps to perform complex tasks seamlessly. + +# Example Use Cases + +- **Automate Content Publishing**: Trigger a workflow in Pipedream that uses the Autom API to publish content across multiple platforms. When a new blog post is ready in a CMS like WordPress, Autom can be programmed to distribute the content to various social media channels and email lists. + +- **Dynamic Inventory Management**: Set up a Pipedream workflow that employs Autom API to update inventory levels across e-commerce platforms. Whenever a sale occurs on Shopify, Autom can adjust the stock count in real-time on other marketplaces like eBay or Amazon. + +- **Customer Feedback Analysis**: Implement a workflow that captures customer feedback from a tool like Typeform, sends it to Autom for processing, and uses sentiment analysis to categorize the responses. The results could then be sent to a Slack channel or logged in a Google Sheet for easy review and action. diff --git a/components/automatic_data_extraction/README.md b/components/automatic_data_extraction/README.md new file mode 100644 index 0000000000000..029e45df54c08 --- /dev/null +++ b/components/automatic_data_extraction/README.md @@ -0,0 +1,11 @@ +# Overview + +The Automatic Data Extraction API by Zyte specializes in extracting structured data from web pages. When incorporated into Pipedream workflows, this API allows you to automate the process of gathering web data, which can feed into various tasks such as market research, price monitoring, or even lead generation. By triggering workflows with new data inputs, processing and storing the extracted data, and connecting to other apps, Pipedream amplifies the API's utility. + +# Example Use Cases + +- **Content Change Detection and Alerting**: Use the Automatic Data Extraction API to monitor changes in content on target websites. When a change is detected, like a price drop or a new article publication, trigger a Pipedream workflow to send an alert via email (using the SendGrid app) or post a notification in a Slack channel. + +- **Competitor Price Tracking**: Set up a scheduled Pipedream workflow to extract pricing data from competitor websites daily. Process and compare this data within Pipedream, then update a Google Sheets document with the latest pricing information for easy analysis and decision-making. + +- **Lead Generation from Online Directories**: Utilize the API to pull contact information from online business directories. With Pipedream, parse the extracted data for quality leads and enrich it using a CRM app like Salesforce. Automatically create new leads in Salesforce with the enriched data for your sales team to act upon. diff --git a/components/automizy/README.md b/components/automizy/README.md index 621414ced5398..dbf95ab39c58e 100644 --- a/components/automizy/README.md +++ b/components/automizy/README.md @@ -1,11 +1,11 @@ # Overview -With the Automizy API, you can build a variety of applications and -integrations. Some examples include: - -- A web application that allows users to sign up for Automizy emails -- An integration that adds Automizy contacts to a CRM system -- A Zapier integration that triggers an Automizy campaign when a new lead is - generated -- A bot that helps Automizy users segment their lists -- And much more! +The Automizy API provides a platform for automating email marketing campaigns, managing contacts, and analyzing the effectiveness of your email strategies. By leveraging this API within Pipedream, you can create dynamic, serverless workflows that respond to various triggers, such as user behavior or external events, to send personalized emails, segment contacts based on interactions, and synchronize subscriber data across various platforms. + +# Example Use Cases + +- **Dynamic Email Campaigns Based on User Activity**: Trigger an email campaign in Automizy when a user signs up on your platform, completes a purchase, or reaches a specific milestone. Using Pipedream, you can watch for these events in your app and set up conditions to send targeted follow-up emails through Automizy. + +- **Automated Contact Segmentation for Personalized Marketing**: Use Pipedream to listen for customer interactions across different touchpoints, such as support tickets, feedback forms, or activity logs. Based on the data collected, automatically update contact segments in Automizy to tailor email content to the customer's interests and behavior. + +- **Synchronization with CRM Systems**: Keep your CRM and email marketing lists in sync by using Pipedream to monitor changes in your CRM, such as HubSpot or Salesforce. When a contact is updated, added, or removed from your CRM, reflect those changes in Automizy to ensure consistent messaging and avoid communication gaps. diff --git a/components/automizy/package.json b/components/automizy/package.json new file mode 100644 index 0000000000000..8304f85036e3c --- /dev/null +++ b/components/automizy/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/automizy", + "version": "0.6.0", + "description": "Pipedream automizy Components", + "main": "automizy.app.mjs", + "keywords": [ + "pipedream", + "automizy" + ], + "homepage": "https://pipedream.com/apps/automizy", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/autopilot/package.json b/components/autopilot/package.json new file mode 100644 index 0000000000000..3490b14f2dbcb --- /dev/null +++ b/components/autopilot/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/autopilot", + "version": "0.6.0", + "description": "Pipedream autopilot Components", + "main": "autopilot.app.mjs", + "keywords": [ + "pipedream", + "autopilot" + ], + "homepage": "https://pipedream.com/apps/autopilot", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/avaza/README.md b/components/avaza/README.md index 647852fe5fcc2..3fdd28b24a84b 100644 --- a/components/avaza/README.md +++ b/components/avaza/README.md @@ -1,9 +1,11 @@ # Overview -You can build a variety of applications using the Avaza API, including: +The Avaza API enables integration of project management, time tracking, and invoicing features into custom workflows. With Pipedream, automation between Avaza and other apps becomes seamless, allowing for the streamlining of project updates, financial processes, and team collaboration. Pipedream's serverless platform offers a powerful way to link Avaza's capabilities with numerous other services, triggering actions based on events, and automating repetitive tasks. -- API-powered websites -- Web-based applications -- Mobile apps -- Desktop apps -- Command-line applications. +# Example Use Cases + +- **Project Status Updates to Slack**: When a project's status changes in Avaza, trigger a workflow in Pipedream that posts an update to a designated Slack channel. This keeps the team informed in real-time about project progress without manual updates. + +- **Time Tracking to Google Sheets**: Automate the process of transferring time tracking data from Avaza to a Google Sheet for further analysis and reporting. Once a time entry is added in Avaza, it triggers a Pipedream workflow that appends the entry to a spreadsheet, enabling easier tracking and management of billable hours. + +- **Invoice Creation to Email Notification**: When a new invoice is created in Avaza, use a Pipedream workflow to automatically send an email notification to the finance team or the client. This ensures prompt communication and can help speed up the payment process. diff --git a/components/avaza/package.json b/components/avaza/package.json new file mode 100644 index 0000000000000..83bd08bc643d0 --- /dev/null +++ b/components/avaza/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/avaza", + "version": "0.6.0", + "description": "Pipedream avaza Components", + "main": "avaza.app.mjs", + "keywords": [ + "pipedream", + "avaza" + ], + "homepage": "https://pipedream.com/apps/avaza", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/avochato/README.md b/components/avochato/README.md index 459ae5b6f1a83..2029c6cb6a915 100644 --- a/components/avochato/README.md +++ b/components/avochato/README.md @@ -1,10 +1,11 @@ # Overview -With the Avochato API, you can: - -- Send SMS messages -- Create and manage contacts -- Create and manage groups -- Schedule messages -- Send and receive MMS messages -- And more! +The Avochato API provides a communication toolkit that enables automated text messaging and phone call workflows. With Pipedream, you can leverage this API to craft custom automations that respond to incoming messages, send alerts, or synchronize communication with your CRM, among other functionalities. Imagine streamlining customer support by auto-responding to common queries, or improving sales engagement by triggering timely follow-up messages—all this without manual intervention, directly harnessing the power of Avochato's communication capabilities within Pipedream's serverless platform. + +# Example Use Cases + +- **Customer Support Auto-Responses**: Trigger an Avochato workflow on Pipedream when a message containing specific keywords is received. Use conditional logic to send tailored responses, resolve common issues, or escalate to human agents when necessary. + +- **Appointment Reminders and Confirmation**: Connect Avochato with Google Calendar using Pipedream. Automatically send reminders for upcoming appointments and process confirmations or rescheduling requests via text, updating the calendar events accordingly. + +- **Real-time Alerts and Notifications**: Integrate Avochato with monitoring tools like Datadog on Pipedream. Set up a workflow that sends real-time SMS alerts to your team when critical systems exhibit issues, ensuring prompt attention and response. diff --git a/components/aweber/README.md b/components/aweber/README.md index 4b7cdd6ab3f87..364ca1bc19891 100644 --- a/components/aweber/README.md +++ b/components/aweber/README.md @@ -1,7 +1,11 @@ # Overview -The AWeber API enables you to interact with your AWeber account data and -perform actions, such as creating and updating subscribers, retrieving campaign -statistics, and managing account preferences. You can use the API to build -applications that integrate with AWeber, such as a custom subscriber management -system or a campaign reporting tool. +AWeber's API provides a suite of tools to automate email marketing efforts, enabling developers to manage subscribers, send emails, and track results programmatically. With the AWeber API on Pipedream, you can create custom workflows that react to various triggers and connect with other services to streamline your email campaigns, analyze performance, and enhance subscriber engagement. + +# Example Use Cases + +- **Automated Welcome Email Sequence**: Set up a workflow that triggers when a new user subscribes to your list on AWeber. Use Pipedream to send a series of personalized welcome emails over the first few days or weeks to onboard your new subscriber. + +- **Subscriber Segmentation and Tagging**: Create a workflow that monitors subscriber interactions, such as link clicks or email opens. Based on these interactions, use the AWeber API to tag and segment users on Pipedream, ensuring they receive more targeted and relevant email content. + +- **E-commerce Purchase Follow-up**: Integrate AWeber with Shopify (or another e-commerce platform) on Pipedream. Whenever a purchase is made, trigger an email sequence in AWeber that provides product usage tips, asks for reviews, and offers related products to increase customer retention and sales. diff --git a/components/awork/README.md b/components/awork/README.md index 4e96483297f94..29619bf3e17ad 100644 --- a/components/awork/README.md +++ b/components/awork/README.md @@ -1,8 +1,11 @@ # Overview -The awork API allows developers to access awork.io content and build -applications on top of the awork platform. With the awork API, you can: +The awork API allows for automation and integration of project management features within awork, such as tasks, projects, time tracking, and team collaborations. By leveraging the awork API on Pipedream, you can create seamless workflows that connect project activities with other apps and services, streamline processes, and enhance productivity. With Pipedream, you can trigger actions based on specific events in awork or schedule periodic tasks that synchronize data across multiple platforms, all with minimal coding. -- Access awork.io content, including articles, photos, and videos -- Build applications on top of the awork platform -- Integrate awork.io content into your own website or application +# Example Use Cases + +- **Project Task Sync with Google Calendar**: Automatically create or update Google Calendar events when new tasks are assigned in awork. This keeps your scheduling in sync and ensures that deadlines are reflected in your calendar. + +- **Slack Notifications for Project Updates**: Set up notifications to a Slack channel when a project's status is updated in awork. This allows teams to stay informed in real-time about project progress without having to constantly check the awork platform. + +- **Time Tracking Integration with QuickBooks**: Connect awork time tracking entries with QuickBooks to automate the invoicing process for completed tasks or projects. This streamlines billing and ensures accurate and timely invoicing for work done. diff --git a/components/aws/README.md b/components/aws/README.md index 3f64b03160248..d67c26dadb3ac 100644 --- a/components/aws/README.md +++ b/components/aws/README.md @@ -1,10 +1,56 @@ # Overview -You can use the AWS API to build a variety of applications and services. Here -are some examples: - -- A mobile application that allows users to access data stored in the cloud -- A web application that allows users to upload and download files -- A web service that allows developers to access Amazon's vast product catalog -- A desktop application that allows users to access their Amazon account - information +The AWS API unlocks endless possibilities for automation with Pipedream. With this powerful combo, you can manage your AWS services and resources, automate deployment workflows, process data, and react to events across your AWS infrastructure. Pipedream offers a serverless platform for creating workflows triggered by various events that can execute AWS SDK functions, making it an efficient tool to integrate, automate, and orchestrate tasks across AWS services and other apps. + +# Getting Started + +To get started, first [log in to the AWS console](https://signin.aws.amazon.com/signin). + +Once you've logged in, navigate to the Identity and Access Management (IAM) service. Then click the **Users** section: + +![Open the users management area in the AWS IAM service](https://res.cloudinary.com/pipedreamin/image/upload/v1715097590/marketplace/apps/aws/CleanShot_2024-05-07_at_11.59.24_mgqvr5.png) + +From within the users management section, create a new user by clicking **Create User** in the top right: + +![Creating a new user from within the AWS IAM console](https://res.cloudinary.com/pipedreamin/image/upload/v1715097847/marketplace/apps/aws/CleanShot_2024-05-07_at_12.03.52_rm4kae.png) + +On the next page, you'll be prompted to name the user. We recommend naming the user `pipedream` so you can easily remember which service this user is tied to: + +![Naming the new IAM user](https://res.cloudinary.com/pipedreamin/image/upload/v1715097913/marketplace/apps/aws/CleanShot_2024-05-07_at_12.04.30_acgthh.png) + +Next, you'll be prompted to define this user's *permissions*. You have three options: +1. Attach the user to a group - the new user will inherit the group's permission policies. +2. Copy permissions - copy the permission policies from another existing IAM user. +3. Attach policies directly - attach a policy directly to the new user. + +If you're unfamiliar with defining permissions in AWS, consider using a pre-made permission policy. For example, if you need Pipedream to integrate with S3, you can choose the `S3FullAccessPolicy` by searching for "s3" in the search bar: + +![Searching for s3 in the permissions search bar within IAM to attach the S3FullAccessPolicy directly to the pipedream user](https://res.cloudinary.com/pipedreamin/image/upload/v1715098770/marketplace/apps/aws/CleanShot_2024-05-07_at_12.19.01_zwgldj.png) + +Alternatively, you can craft specific policies within IAM that only grant specific access to specific AWS resources to this new `pipedream` user. + +Click **Create Policy** to create a new custom policy, and from within this view, you can use either JSON or the UI to include permissions to specific services and resources. + +After you’ve created your IAM user, it will display a **Client Key** and **Secret**. Copy these fields into Pipedream to connect your AWS account. + +Please note, the AWS Client Secret will not be shown again after closing the window. So make sure that your credentials are properly copied into Pipedream before closing the IAM window. + +# Troubleshooting + +## Permissions issues + +The most common issue when integrating Pipedream with AWS is permissions issues. + +The IAM user you create for Pipedream must have access to the AWS resources it’s attempting to use within your triggers, actions, Node.js, or Python code steps. + +You can use the AWS IAM console to attach additional policies to your IAM user associated with Pipedream. + + + +# Example Use Cases + +- **Automated Backup to S3**: Trigger a workflow when a new row is added to a Google Sheets document, process the data within Pipedream, and automatically back it up to an AWS S3 bucket. This ensures important data is stored safely without manual intervention. + +- **CloudWatch Alerts Handler**: Receive AWS CloudWatch alerts in Pipedream, process the data to determine the severity, and send notifications to a Slack channel. Additionally, create Jira tickets for high-severity alerts to streamline incident management. + +- **Serverless Image Processing**: On image upload to S3, trigger a Pipedream workflow that uses AWS Lambda to resize the image, then store the processed image back in another S3 bucket, and finally update a database record through AWS RDS to reflect the change. This creates a seamless media management pipeline. diff --git a/components/aws/actions/s3-download-file-to-tmp/s3-download-file-to-tmp.mjs b/components/aws/actions/s3-download-file-to-tmp/s3-download-file-to-tmp.mjs index aaae7a7519b77..1d1fc7725700c 100644 --- a/components/aws/actions/s3-download-file-to-tmp/s3-download-file-to-tmp.mjs +++ b/components/aws/actions/s3-download-file-to-tmp/s3-download-file-to-tmp.mjs @@ -2,6 +2,7 @@ import { pipeline } from "stream/promises"; import fs from "fs"; import common from "../../common/common-s3.mjs"; import { toSingleLineString } from "../../common/utils.mjs"; +import "@aws-sdk/signature-v4-crt"; export default { ...common, @@ -9,9 +10,9 @@ export default { name: "S3 - Download File to /tmp", description: toSingleLineString(` Downloads a file from S3 to the /tmp directory. - [See the docs](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html) + [See the documentation](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html) `), - version: "0.0.1", + version: "0.0.3", type: "action", props: { aws: common.props.aws, diff --git a/components/aws/actions/s3-generate-presigned-url/s3-generate-presigned-url.mjs b/components/aws/actions/s3-generate-presigned-url/s3-generate-presigned-url.mjs new file mode 100644 index 0000000000000..0775a050348cb --- /dev/null +++ b/components/aws/actions/s3-generate-presigned-url/s3-generate-presigned-url.mjs @@ -0,0 +1,33 @@ +import common from "../../common/common-s3.mjs"; +import { GetObjectCommand } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; + +export default { + ...common, + key: "aws-s3-generate-presigned-url", + name: "S3 - Generate Presigned URL", + description: "Creates a presigned URL to download from a bucket. [See the documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/example_s3_Scenario_PresignedUrl_section.html)", + version: "0.0.2", + type: "action", + props: { + aws: common.props.aws, + region: common.props.region, + bucket: common.props.bucket, + key: { + ...common.props.key, + description: "The name of the S3 key with extension you'd like to download", + }, + }, + async run({ $ }) { + const client = this._clientS3(); + const command = new GetObjectCommand({ + Bucket: this.bucket, + Key: this.key, + }); + const response = getSignedUrl(client, command, { + expiresIn: 3600, + }); + $.export("$summary", "Successfully generated presigned URL"); + return response; + }, +}; diff --git a/components/aws/actions/s3-stream-file/s3-stream-file.mjs b/components/aws/actions/s3-stream-file/s3-stream-file.mjs index e0ae82f220bb9..e705f0c593199 100644 --- a/components/aws/actions/s3-stream-file/s3-stream-file.mjs +++ b/components/aws/actions/s3-stream-file/s3-stream-file.mjs @@ -10,7 +10,7 @@ export default { Accepts a file URL, and streams the file to the provided S3 bucket/key. [See the docs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/upload-objects.html) `), - version: "0.4.3", + version: "0.4.4", type: "action", props: { aws: common.props.aws, diff --git a/components/aws/actions/s3-upload-file-tmp/s3-upload-file-tmp.mjs b/components/aws/actions/s3-upload-file-tmp/s3-upload-file-tmp.mjs index 008d0e4977e7e..a5a1a314b305b 100644 --- a/components/aws/actions/s3-upload-file-tmp/s3-upload-file-tmp.mjs +++ b/components/aws/actions/s3-upload-file-tmp/s3-upload-file-tmp.mjs @@ -12,7 +12,7 @@ export default { Accepts a file path or folder path starting from /tmp, then uploads the contents to S3. [See the docs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/upload-objects.html) `), - version: "1.0.1", + version: "1.0.2", type: "action", props: { aws: common.props.aws, diff --git a/components/aws/actions/s3-upload-file-url/s3-upload-file-url.mjs b/components/aws/actions/s3-upload-file-url/s3-upload-file-url.mjs index f482bbab9c26f..f0181e439d183 100644 --- a/components/aws/actions/s3-upload-file-url/s3-upload-file-url.mjs +++ b/components/aws/actions/s3-upload-file-url/s3-upload-file-url.mjs @@ -15,7 +15,7 @@ export default { Accepts a download link and a filename, downloads it, then uploads to S3. [See the docs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/upload-objects.html) `), - version: "0.1.2", + version: "0.1.3", type: "action", props: { aws: common.props.aws, diff --git a/components/aws/actions/s3-upload-file/s3-upload-file.mjs b/components/aws/actions/s3-upload-file/s3-upload-file.mjs index 6d4c13b9e3b97..9ae3dbeb57cfc 100644 --- a/components/aws/actions/s3-upload-file/s3-upload-file.mjs +++ b/components/aws/actions/s3-upload-file/s3-upload-file.mjs @@ -9,7 +9,7 @@ export default { Accepts a base64-encoded string and a filename, then uploads as a file to S3. [See the docs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/upload-objects.html) `), - version: "0.3.1", + version: "0.3.2", type: "action", props: { aws: common.props.aws, diff --git a/components/aws/common/common-s3.mjs b/components/aws/common/common-s3.mjs index 1eb331472e0a0..43d2eddc015ad 100644 --- a/components/aws/common/common-s3.mjs +++ b/components/aws/common/common-s3.mjs @@ -4,11 +4,11 @@ import { CreateBucketCommand, ListBucketsCommand, GetObjectCommand, - PutObjectCommand, PutBucketPolicyCommand, GetBucketNotificationConfigurationCommand, PutBucketNotificationConfigurationCommand, } from "@aws-sdk/client-s3"; +import { Upload } from "@aws-sdk/lib-storage"; import { axios } from "@pipedream/platform"; export default { @@ -81,7 +81,11 @@ export default { return this._clientS3().send(new GetObjectCommand(params)); }, async uploadFile(params) { - return this._clientS3().send(new PutObjectCommand(params)); + const parallelUploads3 = new Upload({ + client: await this._clientS3(), + params, + }); + await parallelUploads3.done(); }, async streamFile(fileUrl) { return axios(this, { diff --git a/components/aws/package.json b/components/aws/package.json index fa36d8d21e197..c5a5cee3e3468 100644 --- a/components/aws/package.json +++ b/components/aws/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/aws", - "version": "0.5.0", + "version": "0.7.4", "description": "Pipedream Aws Components", "main": "aws.app.mjs", "keywords": [ @@ -17,6 +17,7 @@ "@aws-sdk/client-eventbridge": "^3.66.0", "@aws-sdk/client-iam": "^3.58.0", "@aws-sdk/client-lambda": "^3.65.0", + "@aws-sdk/client-rds": "^3.556.0", "@aws-sdk/client-s3": "^3.66.0", "@aws-sdk/client-ses": "^3.58.0", "@aws-sdk/client-sfn": "^3.58.0", @@ -24,8 +25,11 @@ "@aws-sdk/client-sqs": "^3.58.0", "@aws-sdk/client-ssm": "^3.58.0", "@aws-sdk/client-sts": "^3.58.0", + "@aws-sdk/lib-storage": "^3.734.0", + "@aws-sdk/s3-request-presigner": "^3.609.0", + "@aws-sdk/signature-v4-crt": "^3.731.0", "@pipedream/helper_functions": "^0.3.6", - "@pipedream/platform": "^1.4.1", + "@pipedream/platform": "^1.6.3", "adm-zip": "^0.5.10", "dedent": "^1.5.1", "mailparser": "^3.6.6", diff --git a/components/aws/sources/new-emails-sent-to-ses-catch-all-domain/new-emails-sent-to-ses-catch-all-domain.mjs b/components/aws/sources/new-emails-sent-to-ses-catch-all-domain/new-emails-sent-to-ses-catch-all-domain.mjs index 58c82107eceee..fcb682022b0b1 100644 --- a/components/aws/sources/new-emails-sent-to-ses-catch-all-domain/new-emails-sent-to-ses-catch-all-domain.mjs +++ b/components/aws/sources/new-emails-sent-to-ses-catch-all-domain/new-emails-sent-to-ses-catch-all-domain.mjs @@ -16,7 +16,7 @@ export default { These events can trigger a Pipedream workflow and can be consumed via SSE or REST API. `), type: "source", - version: "1.2.3", + version: "1.2.5", props: { ...base.props, domain: { diff --git a/components/aws/sources/rds-new-event/rds-new-event.mjs b/components/aws/sources/rds-new-event/rds-new-event.mjs new file mode 100644 index 0000000000000..2193b1ec69b13 --- /dev/null +++ b/components/aws/sources/rds-new-event/rds-new-event.mjs @@ -0,0 +1,163 @@ +import aws from "../../aws.app.mjs"; +import { axios } from "@pipedream/platform"; +import { + RDSClient, + DescribeEventCategoriesCommand, + CreateEventSubscriptionCommand, + DeleteEventSubscriptionCommand, +} from "@aws-sdk/client-rds"; +import { + SNSClient, + ListTopicsCommand, + SubscribeCommand, + UnsubscribeCommand, +} from "@aws-sdk/client-sns"; + +export default { + key: "aws-rds-new-event", + name: "New Update to AWS RDS Database (Instant)", + description: "Emit new event when there is an update to an AWS RDS Database.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + aws, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + region: { + propDefinition: [ + aws, + "region", + ], + }, + topic: { + type: "string", + label: "SNS Topic", + description: "The ARN of the SNS Topic", + async options({ prevContext }) { + const response = await this._clientSns().send(new ListTopicsCommand({ + NextToken: prevContext.nextToken, + })); + return { + options: response.Topics.map((topic) => topic.TopicArn), + context: { + nextToken: response.NextToken, + }, + }; + }, + }, + name: { + type: "string", + label: "Subscription Name", + description: "The name of the subscription", + }, + sourceType: { + type: "string", + label: "Source Type", + description: "The type of source that is generating the events. If this value isn't specified, all events are returned.", + async options() { + const eventCategoriesList = await this._describeEventCategories(); + return eventCategoriesList.map(({ SourceType: type }) => type); + }, + }, + eventCategories: { + type: "string[]", + label: "Event Categories", + description: "A list of event categories that you want to subscribe to", + async options() { + if (!this.sourceType) { + return []; + } + const eventCategoriesList = await this._describeEventCategories(); + const sourceType = eventCategoriesList + .find(({ SourceType: type }) => type === this.sourceType); + return sourceType.EventCategories; + }, + }, + }, + hooks: { + async activate() { + await this._clientRds().send(new CreateEventSubscriptionCommand({ + SnsTopicArn: this.topic, + SubscriptionName: this.name, + Enabled: true, + SourceType: this.sourceType, + EventCategories: this.eventCategories, + })); + await this._clientSns().send(new SubscribeCommand({ + TopicArn: this.topic, + Protocol: "https", + Endpoint: this.http.endpoint, + })); + }, + async deactivate() { + await this._clientRds().send(new DeleteEventSubscriptionCommand({ + SubscriptionName: this.name, + })); + const subscriptionArn = this._getSubscriptionArn(); + await this._clientSns().send(new UnsubscribeCommand({ + SubscriptionArn: subscriptionArn, + })); + }, + }, + methods: { + _getSubscriptionArn() { + return this.db.get("subscriptionArn"); + }, + _setSubscriptionArn(subscriptionArn) { + this.db.set("subscriptionArn", subscriptionArn); + }, + _clientRds() { + return this.aws.getAWSClient(RDSClient, this.region); + }, + _clientSns() { + return this.aws.getAWSClient(SNSClient, this.region); + }, + async _describeEventCategories() { + const { EventCategoriesMapList: list } = await this._clientRds() + .send(new DescribeEventCategoriesCommand()); + return list; + }, + _isSubscriptionConfirmationEvent(body = {}) { + const { Type: type } = body; + return type === "SubscriptionConfirmation"; + }, + async _confirmSubscription({ + SubscribeURL: callbackUrl, + TopicArn: topicArn, + }) { + console.log(`Confirming subscription to SNS topic '${topicArn}'`); + const data = await axios(this, { + url: callbackUrl, + }); + const subscriptionArn = data + .ConfirmSubscriptionResponse + .ConfirmSubscriptionResult + .SubscriptionArn; + + console.log(`Subscribed to SNS topic '${topicArn}'`); + return subscriptionArn; + }, + generateMeta(body) { + const message = JSON.parse(body.Message); + return { + id: body.MessageId, + summary: message["Event Message"], + ts: Date.parse(body.Timestamp), + }; + }, + }, + async run(event) { + const { body } = event; + if (this._isSubscriptionConfirmationEvent(body)) { + const subscriptionArn = await this._confirmSubscription(body); + this._setSubscriptionArn(subscriptionArn); + return; + } + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, +}; diff --git a/components/aws/sources/s3-deleted-file/s3-deleted-file.mjs b/components/aws/sources/s3-deleted-file/s3-deleted-file.mjs index 61f0e8c6af0a5..773be37ee7175 100644 --- a/components/aws/sources/s3-deleted-file/s3-deleted-file.mjs +++ b/components/aws/sources/s3-deleted-file/s3-deleted-file.mjs @@ -6,7 +6,7 @@ export default { key: "aws-s3-deleted-file", name: "New Deleted S3 File", description: "Emit new event when a file is deleted from a S3 bucket", - version: "0.1.2", + version: "0.1.3", dedupe: "unique", props: { ...base.props, diff --git a/components/aws/sources/s3-new-event/s3-new-event.mjs b/components/aws/sources/s3-new-event/s3-new-event.mjs index 05c10107f2f6e..aabfad2159dfc 100644 --- a/components/aws/sources/s3-new-event/s3-new-event.mjs +++ b/components/aws/sources/s3-new-event/s3-new-event.mjs @@ -6,7 +6,7 @@ export default { key: "aws-s3-new-event", name: "New S3 Event", description: "Emit new S3 events for a given bucket", - version: "0.1.2", + version: "0.1.3", dedupe: "unique", props: { ...base.props, diff --git a/components/aws/sources/s3-new-file/s3-new-file.mjs b/components/aws/sources/s3-new-file/s3-new-file.mjs index 9f7a846a6b19c..bcd088202d326 100644 --- a/components/aws/sources/s3-new-file/s3-new-file.mjs +++ b/components/aws/sources/s3-new-file/s3-new-file.mjs @@ -6,7 +6,7 @@ export default { key: "aws-s3-new-file", name: "New S3 File", description: "Emit new event when a file is added to an S3 bucket", - version: "0.1.2", + version: "0.1.3", dedupe: "unique", methods: { ...base.methods, diff --git a/components/aws/sources/s3-restored-file/s3-restored-file.mjs b/components/aws/sources/s3-restored-file/s3-restored-file.mjs index d598e8aac2204..39dbef62b859c 100644 --- a/components/aws/sources/s3-restored-file/s3-restored-file.mjs +++ b/components/aws/sources/s3-restored-file/s3-restored-file.mjs @@ -6,7 +6,7 @@ export default { key: "aws-s3-restored-file", name: "New Restored S3 File", description: "Emit new event when an file is restored into a S3 bucket", - version: "0.1.2", + version: "0.1.3", dedupe: "unique", props: { ...base.props, diff --git a/components/axesso_data_service/README.md b/components/axesso_data_service/README.md new file mode 100644 index 0000000000000..ec78465a86b81 --- /dev/null +++ b/components/axesso_data_service/README.md @@ -0,0 +1,11 @@ +# Overview + +The Axesso Data Service - Amazon API lets you tap into a wealth of data from Amazon, including product searches, price trends, and reviews. In Pipedream, you can harness this data to drive insights, track products, and create custom notifications or reports. Pipedream’s serverless platform simplifies the process of setting up automated workflows that use the Axesso API to respond to events and perform actions in other apps. + +# Example Use Cases + +- **Product Availability Tracker:** Set up a workflow on Pipedream that triggers daily to check the availability of specific products using the Axesso Data Service - Amazon API. If the product is in stock, automatically send a Slack message or email to notify your team or customers. + +- **Price Change Alerts:** Create a Pipedream workflow that monitors price changes for products on Amazon. Use the Axesso API to fetch the latest prices and compare them with the previous day's prices stored in a Pipedream data store. If there’s a significant change, the workflow can post a message to a Discord channel or Twitter to alert followers. + +- **Competitive Analysis Dashboard:** Build a Pipedream workflow that collects product data from Amazon on a weekly basis via the Axesso API. Aggregate and analyze this data to gain competitive insights. Then, use Google Sheets to create and update a dashboard that can be shared with your business team. diff --git a/components/axesso_data_service/actions/get-product-details/get-product-details.mjs b/components/axesso_data_service/actions/get-product-details/get-product-details.mjs new file mode 100644 index 0000000000000..0c988aeb613f5 --- /dev/null +++ b/components/axesso_data_service/actions/get-product-details/get-product-details.mjs @@ -0,0 +1,29 @@ +import axesso from "../../axesso_data_service.app.mjs"; + +export default { + key: "axesso_data_service-get-product-details", + name: "Get Product Details", + description: "Requests product detail information using Axesso Data Service. [See the documentation](https://axesso.developer.azure-api.net/api-details#api=axesso-amazon-data-service&operation=product-details)", + version: "0.0.1", + type: "action", + props: { + axesso, + url: { + propDefinition: [ + axesso, + "url", + ], + }, + }, + async run({ $ }) { + const response = await this.axesso.getProductDetails({ + $, + params: { + url: this.url, + psc: 1, + }, + }); + $.export("$summary", `Retrieved product details for URL: ${this.url}`); + return response; + }, +}; diff --git a/components/axesso_data_service/actions/list-reviews/list-reviews.mjs b/components/axesso_data_service/actions/list-reviews/list-reviews.mjs new file mode 100644 index 0000000000000..820ff2160d2cf --- /dev/null +++ b/components/axesso_data_service/actions/list-reviews/list-reviews.mjs @@ -0,0 +1,67 @@ +import axesso from "../../axesso_data_service.app.mjs"; + +export default { + key: "axesso_data_service-list-reviews", + name: "List Reviews", + description: "Lists reviews for an Amazon product using Axesso Data Service. [See the documentation](https://axesso.developer.azure-api.net/api-details#api=axesso-amazon-data-service&operation=reviews)", + version: "0.0.1", + type: "action", + props: { + axesso, + url: { + propDefinition: [ + axesso, + "url", + ], + }, + domainCode: { + propDefinition: [ + axesso, + "domainCode", + ], + }, + sortBy: { + propDefinition: [ + axesso, + "sortBy", + ], + }, + maxResults: { + propDefinition: [ + axesso, + "maxResults", + ], + }, + }, + async run({ $ }) { + const { asin } = await this.axesso.getProductDetails({ + $, + params: { + url: this.url, + psc: 1, + }, + }); + + const results = await this.axesso.paginate({ + fn: this.axesso.lookupReviews, + args: { + $, + params: { + asin, + domainCode: this.domainCode, + sortBy: this.sortBy, + }, + }, + resourceKey: "reviews", + max: this.maxResults, + }); + + const reviews = []; + for await (const review of results) { + reviews.push(review); + } + + $.export("$summary", `Fetched reviews for product with ASIN: ${asin}`); + return reviews; + }, +}; diff --git a/components/axesso_data_service/actions/search-products/search-products.mjs b/components/axesso_data_service/actions/search-products/search-products.mjs new file mode 100644 index 0000000000000..e4198f97a6067 --- /dev/null +++ b/components/axesso_data_service/actions/search-products/search-products.mjs @@ -0,0 +1,67 @@ +import axesso from "../../axesso_data_service.app.mjs"; + +export default { + key: "axesso_data_service-search-products", + name: "Search Products", + description: "Search Amazon products by keyword using Axesso Data Service. [See the documentation](https://axesso.developer.azure-api.net/api-details#api=axesso-amazon-data-service&operation=search-products)", + version: "0.0.1", + type: "action", + props: { + axesso, + keyword: { + type: "string", + label: "Keyword", + description: "The keyword to search for products", + }, + domainCode: { + propDefinition: [ + axesso, + "domainCode", + ], + }, + sortBy: { + propDefinition: [ + axesso, + "sortBy", + ], + }, + category: { + type: "string", + label: "Category", + description: "Valid category list can found on the amazon website on the search selection box. Important: If the passed category is not a valid amazon category, the response will be empty", + optional: true, + }, + maxResults: { + propDefinition: [ + axesso, + "maxResults", + ], + }, + }, + async run({ $ }) { + const results = this.axesso.paginate({ + fn: this.axesso.searchProducts, + args: { + $, + params: { + keyword: this.keyword, + domainCode: this.domainCode, + sortBy: this.sortBy, + category: this.category, + }, + }, + resourceKey: "searchProductDetails", + max: this.maxResults, + }); + + const products = []; + for await (const product of results) { + products.push(product); + } + + $.export("$summary", `Found ${products.length} product${products.length === 1 + ? "" + : "s"} for keyword '${this.keyword}'`); + return products; + }, +}; diff --git a/components/axesso_data_service/axesso_data_service.app.mjs b/components/axesso_data_service/axesso_data_service.app.mjs new file mode 100644 index 0000000000000..a10a897a7493b --- /dev/null +++ b/components/axesso_data_service/axesso_data_service.app.mjs @@ -0,0 +1,103 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import Bottleneck from "bottleneck"; +const limiter = new Bottleneck({ + minTime: 6000, // 10 requests per minute (1 request every 6000ms) + maxConcurrent: 1, +}); +const axiosRateLimiter = limiter.wrap(axios); + +export default { + type: "app", + app: "axesso_data_service", + propDefinitions: { + url: { + type: "string", + label: "Product URL", + description: "The URL of the Amazon product to lookup", + }, + domainCode: { + type: "string", + label: "Domain Code", + description: "The amazon marketplace domain code", + options: constants.DOMAIN_CODES, + }, + sortBy: { + type: "string", + label: "Sort By", + description: "The sort option for the search or reviews", + options: constants.SORT_OPTIONS, + optional: true, + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "The maximum number of results to return. Defaults to `25`", + default: 25, + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.axesso.de/amz"; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axiosRateLimiter($, { + url: `${this._baseUrl()}${path}`, + headers: { + "axesso-api-key": this.$auth.api_key, + }, + ...opts, + }); + }, + getProductDetails(opts = {}) { + return this._makeRequest({ + path: "/amazon-lookup-product", + ...opts, + }); + }, + searchProducts(opts = {}) { + return this._makeRequest({ + path: "/amazon-search-by-keyword-asin", + ...opts, + }); + }, + lookupReviews(opts = {}) { + return this._makeRequest({ + path: "/amazon-lookup-reviews", + ...opts, + }); + }, + async *paginate({ + fn, args, resourceKey, max, + }) { + args = { + ...args, + params: { + ...args?.params, + page: 1, + }, + }; + let hasMore = true; + let count = 0; + while (hasMore) { + const response = await fn(args); + const items = response[resourceKey]; + for (const item of items) { + yield item; + if (max && ++count >= max) { + return; + } + } + if (!items?.length) { + hasMore = false; + } + args.params.page++; + } + }, + }, +}; diff --git a/components/axesso_data_service/common/constants.mjs b/components/axesso_data_service/common/constants.mjs new file mode 100644 index 0000000000000..c1ae0e1210d9a --- /dev/null +++ b/components/axesso_data_service/common/constants.mjs @@ -0,0 +1,47 @@ +const DOMAIN_CODES = [ + { + label: "US", + value: "com", + }, + { + label: "Canada", + value: "ca", + }, + { + label: "Mexico", + value: "com.mx", + }, + { + label: "UK", + value: "co.uk", + }, + { + label: "France", + value: "fr", + }, + { + label: "Germany", + value: "de", + }, + { + label: "Italy", + value: "it", + }, + { + label: "Spain", + value: "es", + }, +]; + +const SORT_OPTIONS = [ + "relevanceblender", + "price-asc-rank", + "price-desc-rank", + "review-rank", + "date-desc-rank", +]; + +export default { + DOMAIN_CODES, + SORT_OPTIONS, +}; diff --git a/components/axesso_data_service/package.json b/components/axesso_data_service/package.json new file mode 100644 index 0000000000000..19472fa1a5a7e --- /dev/null +++ b/components/axesso_data_service/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/axesso_data_service", + "version": "0.1.0", + "description": "Pipedream Axesso Data Service - Amazon Components", + "main": "axesso_data_service.app.mjs", + "keywords": [ + "pipedream", + "axesso_data_service" + ], + "homepage": "https://pipedream.com/apps/axesso_data_service", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "bottleneck": "^2.19.5" + } +} diff --git a/components/axis_lms/README.md b/components/axis_lms/README.md new file mode 100644 index 0000000000000..7c8716f445a75 --- /dev/null +++ b/components/axis_lms/README.md @@ -0,0 +1,11 @@ +# Overview + +The Axis LMS API provides a wide array of functionalities for integrating your Learning Management System with various services and automating tasks. In Pipedream, you can leverage this API to create workflows that handle data synchronization, user management, course enrollment, and reporting, among others. The API's flexibility allows you to trigger actions based on events, schedule tasks, and interact with other apps to expand your LMS capabilities. + +# Example Use Cases + +- **Automated Course Enrollment**: Trigger a workflow in Pipedream when a new user is added to your CRM. The workflow would use the Axis LMS API to automatically enroll the user in the appropriate course or series of courses based on the data from the CRM. + +- **Progress Tracking and Reporting**: Set up a scheduled Pipedream workflow to fetch progress reports from Axis LMS. This data can then be sent to a Google Sheet for analysis or emailed to instructors and students using Gmail, helping to keep all stakeholders informed about learner progress. + +- **Real-time Alerts for Course Completion**: Create a Pipedream workflow that listens for course completion events from Axis LMS. Upon completion, trigger a congratulatory email via SendGrid and update a Slack channel with the user's achievement, encouraging team recognition and motivation. diff --git a/components/axis_lms/package.json b/components/axis_lms/package.json index fd76fdcc2d26c..6bc2897c1a546 100644 --- a/components/axis_lms/package.json +++ b/components/axis_lms/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/axonaut/README.md b/components/axonaut/README.md new file mode 100644 index 0000000000000..6afb42a8ead4c --- /dev/null +++ b/components/axonaut/README.md @@ -0,0 +1,11 @@ +# Overview + +The Axonaut API enables interaction with Axonaut's CRM and ERP features, allowing you to manage customers, invoices, quotes, and more. By building workflows on Pipedream with the Axonaut API, you can automate repetitive tasks, sync data between different systems, and create custom, scalable solutions for your business needs. Connect Axonaut to numerous other services via Pipedream to streamline your sales, accounting, and operational processes. + +# Example Use Cases + +- **Automate Invoice Creation and Notification**: Trigger a workflow when a new deal is marked as won in your sales pipeline on Axonaut, automatically creating an invoice and sending a notification via Slack or email to the finance department. + +- **Sync Contacts Between Axonaut and Email Marketing Tools**: Build a workflow that runs on a schedule to sync new and updated contacts from Axonaut to an email marketing tool like Mailchimp, ensuring your marketing campaigns always target the latest contacts. + +- **Lead Qualification and Distribution**: On receiving a new lead form submission on your website, use Pipedream to add the lead to Axonaut, then, based on predefined criteria such as location or industry, distribute the lead details to the appropriate sales rep via SMS or a messaging app like Microsoft Teams. diff --git a/components/aylien_news_api/README.md b/components/aylien_news_api/README.md new file mode 100644 index 0000000000000..ba247737d31d7 --- /dev/null +++ b/components/aylien_news_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Aylien News API provides a robust dataset of news content from across the globe, enabling users to search, analyze, and collect insights from news articles and blogs in real-time. With this API, one can access a wealth of information categorized by source, sentiment, keywords, entities, and more. In Pipedream, Aylien News API can be used to trigger automations, collect data for analysis, or integrate with other apps to enhance media monitoring, market research, and content curation workflows. + +# Example Use Cases + +- **Real-Time Media Monitoring**: Create a workflow that triggers whenever the Aylien News API detects news articles matching specified keywords or topics. Collect and analyze these articles for sentiment, and send a summary report to Slack or email for quick team updates. + +- **Market Trends Analysis**: Set up a scheduled Pipedream job that queries the Aylien News API daily for articles on market-specific trends. Process the results through a data analysis tool like Google Sheets or Data Studio to visualize changes and insights over time. + +- **Content Curation and Sharing**: Build a workflow that searches for the most popular or trending articles in a specific category using the Aylien News API. Curate this content and automatically post it to your company's social media channels like Twitter or LinkedIn to engage with your audience. diff --git a/components/aylien_news_api/aylien_news_api.app.mjs b/components/aylien_news_api/aylien_news_api.app.mjs new file mode 100644 index 0000000000000..e7131f4f5c920 --- /dev/null +++ b/components/aylien_news_api/aylien_news_api.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "aylien_news_api", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/aylien_news_api/package.json b/components/aylien_news_api/package.json new file mode 100644 index 0000000000000..37506fe3ed8d0 --- /dev/null +++ b/components/aylien_news_api/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/aylien_news_api", + "version": "0.0.1", + "description": "Pipedream Aylien News API Components", + "main": "aylien_news_api.app.mjs", + "keywords": [ + "pipedream", + "aylien_news_api" + ], + "homepage": "https://pipedream.com/apps/aylien_news_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/ayrshare/ayrshare.app.mjs b/components/ayrshare/ayrshare.app.mjs new file mode 100644 index 0000000000000..b6b40607573be --- /dev/null +++ b/components/ayrshare/ayrshare.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "ayrshare", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/ayrshare/package.json b/components/ayrshare/package.json new file mode 100644 index 0000000000000..657b6bcf7bb50 --- /dev/null +++ b/components/ayrshare/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/ayrshare", + "version": "0.0.1", + "description": "Pipedream Ayrshare Components", + "main": "ayrshare.app.mjs", + "keywords": [ + "pipedream", + "ayrshare" + ], + "homepage": "https://pipedream.com/apps/ayrshare", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/azure_ai_vision/README.md b/components/azure_ai_vision/README.md new file mode 100644 index 0000000000000..2188ab79d9c9f --- /dev/null +++ b/components/azure_ai_vision/README.md @@ -0,0 +1,11 @@ +# Overview + +The Azure AI Vision API provides powerful image analysis capabilities, enabling you to extract information and insights from your visual data. With this API, you can perform tasks like image classification, object detection, and OCR (Optical Character Recognition) to recognize text within images. Leveraging Pipedream, you can integrate these AI-powered insights into your workflows to create dynamic and automated processes. + +# Example Use Cases + +- **Content Moderation Pipeline**: Implement an automated system that scans user-uploaded images for inappropriate content using the Azure AI Vision API. Once processed, images can be tagged, and if necessary, alerts can be sent or content removed automatically, ensuring a safe environment for your community. + +- **Digital Asset Management Enhancement**: Augment your digital asset management by automatically tagging images with descriptive labels. Connect Azure AI Vision API to your asset database on Pipedream, so every time a new image is uploaded, it gets analyzed and tagged, making it easier to organize and search for. + +- **Real-time Text Extraction for Receipts**: Build a workflow that captures text from uploaded receipt images in real-time. Use the Azure AI Vision API for OCR to extract and parse information, then pipe the data to a spreadsheet app like Google Sheets for expense tracking and reporting. diff --git a/components/azure_ai_vision/actions/extract-text-from-image/extract-text-from-image.mjs b/components/azure_ai_vision/actions/extract-text-from-image/extract-text-from-image.mjs new file mode 100644 index 0000000000000..6a41f71708082 --- /dev/null +++ b/components/azure_ai_vision/actions/extract-text-from-image/extract-text-from-image.mjs @@ -0,0 +1,40 @@ +import azureAiVision from "../../azure_ai_vision.app.mjs"; + +export default { + key: "azure_ai_vision-extract-text-from-image", + name: "Extract Text from Image", + description: "Extracts text from the provided image using Azure AI Vision OCR. [See the documentation](https://centraluseuap.dev.cognitive.microsoft.com/docs/services/unified-vision-apis-public-preview-2023-02-01-preview/operations/61d65934cd35050c20f73ab6)", + version: "0.0.1", + type: "action", + props: { + azureAiVision, + image: { + propDefinition: [ + azureAiVision, + "image", + ], + }, + language: { + propDefinition: [ + azureAiVision, + "language", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.azureAiVision.analyzeImage({ + $, + data: { + url: this.image, + }, + params: { + "features": "read", + "language": this.language, + }, + }); + + $.export("$summary", "Successfully extracted text from image"); + return response; + }, +}; diff --git a/components/azure_ai_vision/azure_ai_vision.app.mjs b/components/azure_ai_vision/azure_ai_vision.app.mjs new file mode 100644 index 0000000000000..fe78e895217d7 --- /dev/null +++ b/components/azure_ai_vision/azure_ai_vision.app.mjs @@ -0,0 +1,52 @@ +import { axios } from "@pipedream/platform"; +import { LANGUAGE_OPTIONS } from "./common/constants.mjs"; + +export default { + type: "app", + app: "azure_ai_vision", + propDefinitions: { + image: { + type: "string", + label: "Image URL", + description: "The URL of the image from which you want to extract text.", + }, + language: { + type: "string", + label: "Language", + description: "The language code of the text to be detected in the image.", + options: LANGUAGE_OPTIONS, + optional: true, + }, + }, + methods: { + _baseUrl() { + return this.$auth.endpoint; + }, + _headers() { + return { + "Ocp-Apim-Subscription-Key": this.$auth.subscription_key, + "Content-Type": "application/json", + }; + }, + _makeRequest({ + $ = this, path, params, ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._headers(), + params: { + ...params, + "api-version": "2023-02-01-preview", + }, + ...opts, + }); + }, + analyzeImage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "computervision/imageanalysis:analyze", + ...opts, + }); + }, + }, +}; diff --git a/components/azure_ai_vision/common/constants.mjs b/components/azure_ai_vision/common/constants.mjs new file mode 100644 index 0000000000000..dc3b563bdba97 --- /dev/null +++ b/components/azure_ai_vision/common/constants.mjs @@ -0,0 +1,38 @@ +export const LANGUAGE_OPTIONS = [ + { + label: "English", + value: "en", + }, + { + label: "Chinese Simplified", + value: "zh-Hans", + }, + { + label: "French", + value: "fr", + }, + { + label: "German", + value: "de", + }, + { + label: "Italian", + value: "it", + }, + { + label: "Japanese", + value: "ja", + }, + { + label: "Korean", + value: "ko", + }, + { + label: "Portuguese", + value: "pt", + }, + { + label: "Spanish", + value: "es", + }, +]; diff --git a/components/azure_ai_vision/package.json b/components/azure_ai_vision/package.json new file mode 100644 index 0000000000000..b7fe7ab6cb251 --- /dev/null +++ b/components/azure_ai_vision/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/azure_ai_vision", + "version": "0.1.0", + "description": "Pipedream Azure AI Vision Components", + "main": "azure_ai_vision.app.mjs", + "keywords": [ + "pipedream", + "azure_ai_vision" + ], + "homepage": "https://pipedream.com/apps/azure_ai_vision", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" + } +} diff --git a/components/azure_api_for_fhir/azure_api_for_fhir.app.mjs b/components/azure_api_for_fhir/azure_api_for_fhir.app.mjs new file mode 100644 index 0000000000000..00ae145ee0848 --- /dev/null +++ b/components/azure_api_for_fhir/azure_api_for_fhir.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "azure_api_for_fhir", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/azure_api_for_fhir/package.json b/components/azure_api_for_fhir/package.json new file mode 100644 index 0000000000000..44b01ca4fd9ea --- /dev/null +++ b/components/azure_api_for_fhir/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/azure_api_for_fhir", + "version": "0.0.1", + "description": "Pipedream Azure API for FHIR Components", + "main": "azure_api_for_fhir.app.mjs", + "keywords": [ + "pipedream", + "azure_api_for_fhir" + ], + "homepage": "https://pipedream.com/apps/azure_api_for_fhir", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/azure_devops/README.md b/components/azure_devops/README.md new file mode 100644 index 0000000000000..11edf9323278b --- /dev/null +++ b/components/azure_devops/README.md @@ -0,0 +1,11 @@ +# Overview + +The Azure DevOps API opens up a world of possibilities for automating and enhancing your software development workflows. Through Pipedream, you can leverage this API to create custom integrations and serverless workflows that interact with your Azure DevOps projects. Automate tasks such as triggering builds, managing work items, updating repositories, and orchestrating multi-step processes that connect Azure DevOps with other services. + +# Example Use Cases + +- **Automated Build Triggers**: Create a workflow on Pipedream that listens for code commits or pull requests on GitHub. When a change is detected, it triggers a new build in Azure DevOps. This ensures your project is always up to date with the latest code changes. + +- **Work Item Management**: Set up a Pipedream workflow that syncs Azure DevOps work items with Trello cards. Every time a work item is created or updated in Azure DevOps, a corresponding card is created or updated in Trello, keeping non-technical team members in the loop. + +- **Release Notifications**: Use Pipedream to monitor the status of Azure DevOps deployments. Whenever a deployment to a particular environment is successful, send a notification via Slack to inform your team, allowing for immediate awareness and quick response to any issues that may arise. diff --git a/components/azure_devops/azure_devops.app.mjs b/components/azure_devops/azure_devops.app.mjs index ec4be8c5e1c17..f985359983e52 100644 --- a/components/azure_devops/azure_devops.app.mjs +++ b/components/azure_devops/azure_devops.app.mjs @@ -1,5 +1,5 @@ import { axios } from "@pipedream/platform"; -const API_VERSION = "7.1-preview.1"; +const API_VERSION = "5.0"; export default { type: "app", @@ -60,7 +60,7 @@ export default { }, async listAccounts(args = {}) { const { value } = await this._makeRequest({ - url: `https://app.vssps.visualstudio.com/_apis/accounts?ownerId=${this._oauthUid()}`, + url: `https://app.vssps.visualstudio.com/_apis/accounts?memberId=${this._oauthUid()}`, ...args, }); return value; diff --git a/components/azure_devops/package.json b/components/azure_devops/package.json index f483925311f95..499bb348c4530 100644 --- a/components/azure_devops/package.json +++ b/components/azure_devops/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/azure_devops", - "version": "0.0.3", + "version": "0.0.4", "description": "Pipedream Azure DevOps Components", "main": "azure_devops.app.mjs", "keywords": [ diff --git a/components/azure_devops/sources/new-event/new-event.mjs b/components/azure_devops/sources/new-event/new-event.mjs index 55246b4db91db..0cea4c6143aad 100644 --- a/components/azure_devops/sources/new-event/new-event.mjs +++ b/components/azure_devops/sources/new-event/new-event.mjs @@ -2,7 +2,7 @@ import azureDevops from "../../azure_devops.app.mjs"; export default { name: "New Event (Instant)", - version: "0.0.1", + version: "0.0.2", key: "azure_devops-new-event", description: "Emit new event for the specified event type.", type: "source", diff --git a/components/azure_openai_service/README.md b/components/azure_openai_service/README.md new file mode 100644 index 0000000000000..9711ec0b8659a --- /dev/null +++ b/components/azure_openai_service/README.md @@ -0,0 +1,11 @@ +# Overview + +The Azure OpenAI Service API provides access to powerful AI models that can understand and generate human-like text. With Pipedream, you can harness this capability to create a variety of serverless workflows, automating tasks like content creation, code generation, and language translation. By integrating the API with other apps on Pipedream, you can streamline processes, analyze sentiment, and even automate customer support. + +# Example Use Cases + +- **Content Generation Pipeline**: Ingest data from a webhook, feed it into the Azure OpenAI Service API to generate articles, summaries, or reports, and then directly post the content to a CMS like WordPress or a platform like Medium. + +- **Real-time Customer Support**: Connect the Azure OpenAI Service API to your CRM, like Salesforce, to generate automatic responses to customer inquiries. Analyze incoming messages for intent, and use the API to craft replies or route the customer to the appropriate service channel. + +- **Language Translation App**: Create an app that takes input from a Google Sheet, translates the text into multiple languages using the Azure OpenAI Service API, and stores the translations back in the sheet or another database, streamlining the process of multilingual content creation. diff --git a/components/azure_speech_service/README.md b/components/azure_speech_service/README.md new file mode 100644 index 0000000000000..ea7e15715fee6 --- /dev/null +++ b/components/azure_speech_service/README.md @@ -0,0 +1,11 @@ +# Overview + +The Azure Speech Service API offers a suite of speech-to-text, text-to-speech, and speech translation capabilities, allowing you to integrate advanced speech processing into your applications. With the API, you can transcribe audio into text, convert text into natural-sounding speech, and even translate spoken languages in real-time. Leveraging these features within Pipedream, you can automate workflows that respond to voice commands, generate audio content from textual data, or provide real-time translation services for global communication. + +# Example Use Cases + +- **Transcribe Customer Support Calls**: Automatically transcribe audio files from customer support calls into text. Use this text for sentiment analysis, searchable call logs, or compliance checks by connecting Azure Speech Service with apps like Google Sheets or Slack for notifications. + +- **Voice-Driven Content Creation**: Convert blog posts or written articles into audio files. This can be useful for creating podcasts or audio versions of content for accessibility. Hook in the Azure Speech Service with a CMS like WordPress to post the audio directly to your website. + +- **Real-Time Translation for Global Webinars**: During a live webinar, use Azure Speech Service to provide real-time translation. Integrate with webinar platforms like Zoom and live chat tools to display translated subtitles, making the content accessible to a non-English speaking audience. diff --git a/components/azure_speech_service/package.json b/components/azure_speech_service/package.json index c1f7e2277439d..e621092b86089 100644 --- a/components/azure_speech_service/package.json +++ b/components/azure_speech_service/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/azure_sql/README.md b/components/azure_sql/README.md new file mode 100644 index 0000000000000..66d6a9a9bd7c1 --- /dev/null +++ b/components/azure_sql/README.md @@ -0,0 +1,11 @@ +# Overview + +The Microsoft Azure SQL Database API allows you to manage and interact with your SQL databases hosted on Azure directly from Pipedream. It provides a serverless way to run SQL queries, manage tables, and handle database management tasks. You can create, read, update, and delete database records, execute stored procedures, and perform a variety of other SQL operations. Leveraging this API on Pipedream enables you to automate workflows, respond to database events in real-time, and integrate with countless other apps and services. + +# Example Use Cases + +- **Automated Data Backups**: Create a workflow that triggers at regular intervals to back up specific tables or the entire database. Store the backups in Azure Blob Storage or integrate with another storage solution like Amazon S3. + +- **Real-Time Data Synchronization**: Sync data between Azure SQL Database and third-party services like Salesforce, Shopify, or QuickBooks. Whenever a new record is inserted or updated in the Azure SQL Database, the corresponding data is automatically updated in the other service. + +- **Event-Driven Notifications**: Set up a workflow that sends notifications via email, Slack, or SMS when specific database events occur, such as a new row being added to a table, or a particular threshold being reached on a key performance metric. diff --git a/components/azure_sql/actions/execute-query/execute-query.mjs b/components/azure_sql/actions/execute-query/execute-query.mjs index ec66b60b8e8b6..42bbdc31adea4 100644 --- a/components/azure_sql/actions/execute-query/execute-query.mjs +++ b/components/azure_sql/actions/execute-query/execute-query.mjs @@ -5,7 +5,7 @@ export default { name: "Execute Query", description: "Executes a SQL query and returns the results. [See the documentation](https://learn.microsoft.com/en-us/sql/t-sql/queries/select-transact-sql?view=azuresqldb-current)", type: "action", - version: "0.0.1", + version: "0.0.6", props: { app, query: { @@ -20,18 +20,26 @@ export default { description: "The inputs to the query. These will be available as @input_parameter in the query. For example, if you provide an input named 'id', you can use @id in the query.", optional: true, }, + requestTimeout: { + propDefinition: [ + app, + "requestTimeout", + ], + }, }, run({ $ }) { const { app, inputs, query, + requestTimeout, } = this; return app.executeQuery({ $, query, inputs, + requestTimeout, summary: () => "Successfully executed query.", }); }, diff --git a/components/azure_sql/actions/execute-raw-query/execute-raw-query.mjs b/components/azure_sql/actions/execute-raw-query/execute-raw-query.mjs new file mode 100644 index 0000000000000..04f303c1c4e7b --- /dev/null +++ b/components/azure_sql/actions/execute-raw-query/execute-raw-query.mjs @@ -0,0 +1,32 @@ +import app from "../../azure_sql.app.mjs"; + +export default { + key: "azure_sql-execute-raw-query", + name: "Execute SQL Query", + description: "Execute a custom SQL query. See [our docs](https://pipedream.com/docs/databases/working-with-sql) to learn more about working with SQL in Pipedream.", + type: "action", + version: "0.0.5", + props: { + app, + sql: { + type: "sql", + auth: { + app: "app", + }, + label: "SQL Query", + }, + requestTimeout: { + propDefinition: [ + app, + "requestTimeout", + ], + }, + }, + async run({ $ }) { + const args = this.app.executeQueryAdapter(this.sql); + args.requestTimeout = this.requestTimeout; + const response = await this.app.executeQuery(args); + $.export("$summary", "Successfully executed query."); + return response; + }, +}; diff --git a/components/azure_sql/actions/insert-row/insert-row.mjs b/components/azure_sql/actions/insert-row/insert-row.mjs index ff0b07136377c..78bcaf1f88acc 100644 --- a/components/azure_sql/actions/insert-row/insert-row.mjs +++ b/components/azure_sql/actions/insert-row/insert-row.mjs @@ -4,7 +4,7 @@ export default { key: "azure_sql-insert-row", name: "Insert Row", description: "Inserts a new row in a table. [See the documentation](https://learn.microsoft.com/en-us/sql/t-sql/statements/insert-transact-sql?view=azuresqldb-current)", - version: "0.0.1", + version: "0.0.6", type: "action", props: { app, @@ -16,6 +16,12 @@ export default { ], reloadProps: true, }, + requestTimeout: { + propDefinition: [ + app, + "requestTimeout", + ], + }, }, async additionalProps() { const { @@ -41,6 +47,7 @@ export default { const { app, table, + requestTimeout, ...inputs } = this; @@ -48,6 +55,7 @@ export default { $, table, inputs, + requestTimeout, summary: () => "Successfully inserted row.", }); }, diff --git a/components/azure_sql/azure_sql.app.mjs b/components/azure_sql/azure_sql.app.mjs index 921a1b4cf19f3..16b48386e4d47 100644 --- a/components/azure_sql/azure_sql.app.mjs +++ b/components/azure_sql/azure_sql.app.mjs @@ -1,4 +1,8 @@ import sql from "mssql"; +import { + sqlProxy, + sqlProp, +} from "@pipedream/platform"; export default { type: "app", @@ -24,14 +28,23 @@ export default { return recordset.map(({ COLUMN_NAME: columnName }) => columnName); }, }, + requestTimeout: { + type: "integer", + label: "Request Timeout", + description: "The timeout for query execution in seconds. Default is 60 seconds if not specified.", + optional: true, + default: 60, + }, }, methods: { + ...sqlProxy.methods, + ...sqlProp.methods, exportSummary($) { return (msg = "") => $.export("$summary", msg); }, getConfig() { const { - host, username, password, port, database, encrypt, + host, username, password, port, database, requestTimeout, } = this.$auth; return { user: username, @@ -43,32 +56,121 @@ export default { type: "default", }, options: { - encrypt: String(encrypt).toLowerCase() === "true", + encrypt: true, + port: Number(port), + requestTimeout: (requestTimeout || 60) * 1000, // Convert to milliseconds }, }; }, getConnection() { return sql.connect(this.getConfig()); }, - async executeQuery({ - $ = this, summary, query = "", inputs = {}, - } = {}) { + /** + * A helper method to get the schema of the database. Used by other features + * (like the `sql` prop) to enrich the code editor and provide the user with + * auto-complete and fields suggestion. + * + * @returns {DbInfo} The schema of the database, which is a + * JSON-serializable object. + */ + async getSchema() { + const sql = ` + SELECT t.TABLE_SCHEMA AS tableSchema, + t.TABLE_NAME AS tableName, + c.COLUMN_NAME AS columnName, + c.DATA_TYPE AS dataType, + c.IS_NULLABLE AS isNullable, + c.COLUMN_DEFAULT AS columnDefault + FROM INFORMATION_SCHEMA.TABLES AS t + JOIN INFORMATION_SCHEMA.COLUMNS AS c ON t.TABLE_NAME = c.TABLE_NAME + AND t.TABLE_SCHEMA = c.TABLE_SCHEMA + WHERE t.TABLE_TYPE = 'BASE TABLE' + ORDER BY t.TABLE_NAME, + c.ORDINAL_POSITION + `; + const { recordset: rows } = await this.executeQuery({ + query: sql, + }); + return rows.reduce((acc, row) => { + const key = `${row.tableSchema}.${row.tableName}`; + acc[key] ??= { + metadata: { + rowCount: row.rowCount, + }, + schema: {}, + }; + acc[key].schema[row.columnName] = { + ...row, + }; + return acc; + }, {}); + }, + /** + * Adapts the arguments to `executeQuery` so that they can be consumed by + * the SQL proxy (when applicable). Note that this method is not intended to + * be used by the component directly. + * @param {object} preparedStatement The prepared statement to be sent to the DB. + * @param {string} preparedStatement.sql The prepared SQL query to be executed. + * @param {string[]} preparedStatement.values The values to replace in the SQL query. + * @returns {object} - The adapted query and parameters. + */ + proxyAdapter(preparedStatement = {}) { + const { query } = preparedStatement; + const inputs = preparedStatement?.inputs || {}; + for (const [ + key, + value, + ] of Object.entries(inputs)) { + query.replaceAll(`@${key}`, value); + } + return { + query, + params: [], + }; + }, + /** + * A method that performs the inverse transformation of `proxyAdapter`. + * + * @param {object} proxyArgs - The output of `proxyAdapter`. + * @param {string} proxyArgs.query - The SQL query to be executed. + * @param {string[]} proxyArgs.params - The values to replace in the SQL + * query. + * @returns {object} - The adapted query and parameters, compatible with + * `executeQuery`. + */ + executeQueryAdapter(proxyArgs = {}) { + let { query } = proxyArgs; + const params = proxyArgs?.params || []; + for (const param of params) { + query = query.replace("?", param); + } + query = query.replaceAll("\n", " "); + return { + query, + inputs: {}, + }; + }, + getClientConfiguration() { + return this.getConfig(); + }, + async executeQuery(preparedStatement = {}) { let connection; - + const { + query, requestTimeout, + } = preparedStatement; + const inputs = preparedStatement?.inputs || {}; try { - connection = await this.getConnection(); - + const config = this.getConfig(); + if (requestTimeout) { + config.options.requestTimeout = requestTimeout * 1000; // Convert to milliseconds + } + connection = await sql.connect(config); const input = Object.entries(inputs) .reduce((req, inputArgs) => req.input(...inputArgs), connection.request()); - const response = await input.query(query); - if (typeof summary === "function") { - this.exportSummary($)(summary(response)); - } - return response; } catch (error) { diff --git a/components/azure_sql/package.json b/components/azure_sql/package.json index 010110930efb9..2eb548a18e497 100644 --- a/components/azure_sql/package.json +++ b/components/azure_sql/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/azure_sql", - "version": "0.1.0", + "version": "0.1.5", "description": "Pipedream Microsoft Azure SQL Database Components", "main": "azure_sql.app.mjs", "keywords": [ @@ -13,7 +13,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.5.1", + "@pipedream/platform": "^3.0.0", "mssql": "^10.0.1", "uuid": "^9.0.1" } diff --git a/components/azure_sql/sources/new-column/new-column.mjs b/components/azure_sql/sources/new-column/new-column.mjs index eb84dc3f158e9..cd9286c62076c 100644 --- a/components/azure_sql/sources/new-column/new-column.mjs +++ b/components/azure_sql/sources/new-column/new-column.mjs @@ -7,7 +7,7 @@ export default { name: "New Column", description: "Triggers when a new column is added to a table.", type: "source", - version: "0.0.1", + version: "0.0.6", dedupe: "unique", props: { ...common.props, diff --git a/components/azure_sql/sources/new-or-updated-row/new-or-updated-row.mjs b/components/azure_sql/sources/new-or-updated-row/new-or-updated-row.mjs index 291884e81ec90..bef6d6b1ebdea 100644 --- a/components/azure_sql/sources/new-or-updated-row/new-or-updated-row.mjs +++ b/components/azure_sql/sources/new-or-updated-row/new-or-updated-row.mjs @@ -7,7 +7,7 @@ export default { name: "New or Updated Row", description: "Triggers when a new row is added or an existing row is updated.", type: "source", - version: "0.0.1", + version: "0.0.6", dedupe: "unique", props: { ...common.props, diff --git a/components/azure_storage/actions/create-container/create-container.mjs b/components/azure_storage/actions/create-container/create-container.mjs new file mode 100644 index 0000000000000..5dcb4b21d4199 --- /dev/null +++ b/components/azure_storage/actions/create-container/create-container.mjs @@ -0,0 +1,54 @@ +import app from "../../azure_storage.app.mjs"; + +export default { + key: "azure_storage-create-container", + name: "Create Container", + description: "Creates a new container under the specified account. If a container with the same name already exists, the operation fails. [See the documentation](https://learn.microsoft.com/en-us/rest/api/storageservices/create-container?tabs=microsoft-entra-id).", + version: "0.0.1", + type: "action", + props: { + app, + // eslint-disable-next-line pipedream/props-label, pipedream/props-description + info: { + type: "alert", + alertType: "info", + content: "The name of your container can include only lowercase characters and needs to follow [these naming rules](https://learn.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names).", + }, + containerName: { + type: "string", + label: "Container Name", + description: "The name of the container within the specified storage account.", + }, + }, + methods: { + createContainer({ + containerName, params, ...args + } = {}) { + return this.app.put({ + ...args, + path: `/${containerName}`, + params: { + ...params, + restype: "container", + }, + }); + }, + }, + async run({ $ }) { + const { + createContainer, + containerName, + } = this; + + await createContainer({ + $, + containerName, + }); + + $.export("$summary", "Successfully created container."); + + return { + success: true, + }; + }, +}; diff --git a/components/azure_storage/actions/delete-blob/delete-blob.mjs b/components/azure_storage/actions/delete-blob/delete-blob.mjs new file mode 100644 index 0000000000000..abbc295833db7 --- /dev/null +++ b/components/azure_storage/actions/delete-blob/delete-blob.mjs @@ -0,0 +1,60 @@ +import app from "../../azure_storage.app.mjs"; + +export default { + key: "azure_storage-delete-blob", + name: "Delete Blob", + description: "Deletes a specific blob from a container in Azure Storage. [See the documentation](https://learn.microsoft.com/en-us/rest/api/storageservices/delete-blob?tabs=microsoft-entra-id).", + version: "0.0.1", + type: "action", + props: { + app, + // eslint-disable-next-line pipedream/props-label, pipedream/props-description + info: { + type: "alert", + alertType: "info", + content: "In order to have the right permissions to use this feature you need to go to the Azure Console in `Storage Account > IAM > Add role assignment`, and add the special permissions for this type of request:\n - Storage Blob Data Contributor\n - Storage Queue Data Contributor\n [See the documentation](https://learn.microsoft.com/en-us/rest/api/storageservices/delete-blob?tabs=microsoft-entra-id#permissions).", + }, + containerName: { + propDefinition: [ + app, + "containerName", + ], + }, + blobName: { + propDefinition: [ + app, + "blobName", + ({ containerName }) => ({ + containerName, + }), + ], + }, + }, + methods: { + deleteBlob({ + containerName, blobName, + } = {}) { + return this.app.delete({ + path: `/${containerName}/${blobName}`, + }); + }, + }, + async run({ $ }) { + const { + deleteBlob, + containerName, + blobName, + } = this; + + await deleteBlob({ + $, + containerName, + blobName, + }); + + $.export("$summary", "Successfully deleted blob."); + return { + success: true, + }; + }, +}; diff --git a/components/azure_storage/actions/upload-blob/upload-blob.mjs b/components/azure_storage/actions/upload-blob/upload-blob.mjs new file mode 100644 index 0000000000000..8b7b4c1dd9342 --- /dev/null +++ b/components/azure_storage/actions/upload-blob/upload-blob.mjs @@ -0,0 +1,79 @@ +import mime from "mime-types"; +import app from "../../azure_storage.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "azure_storage-upload-blob", + name: "Upload Blob", + description: "Uploads a new blob to a specified container in Azure Storage. [See the documentation](https://learn.microsoft.com/en-us/rest/api/storageservices/put-blob?tabs=microsoft-entra-id).", + version: "0.0.1", + type: "action", + props: { + app, + // eslint-disable-next-line pipedream/props-label, pipedream/props-description + info: { + type: "alert", + alertType: "info", + content: "In order to have the right permissions to use this feature you need to go to the Azure Console in `Storage Account > IAM > Add role assignment`, and add the special permissions for this type of request:\n - Storage Blob Data Contributor\n - Storage Queue Data Contributor\n [See the documentation](https://learn.microsoft.com/en-us/rest/api/storageservices/put-blob?tabs=microsoft-entra-id#permissions).", + }, + containerName: { + propDefinition: [ + app, + "containerName", + ], + }, + blobName: { + type: "string", + label: "Blob Name", + description: "The name of the blob within the specified container.", + }, + filePath: { + type: "string", + label: "File", + description: "The file to be uploaded, please provide a file from `/tmp` Eg. `/tmp/my-file.txt`. To upload a file to `/tmp` folder, please follow the doc [here](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + }, + }, + methods: { + uploadBlob({ + containerName, blobName, ...args + } = {}) { + return this.app.put({ + path: `/${containerName}/${blobName}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + uploadBlob, + containerName, + blobName, + filePath, + } = this; + + const data = utils.getDataFromFile(filePath); + const fileName = utils.getFilenameFromPath(filePath); + const contentType = mime.lookup(fileName) || "application/octet-stream"; + + await uploadBlob({ + $, + containerName, + blobName, + data, + headers: { + "x-ms-blob-type": "BlockBlob", + // "Content-Type": "text/plain; charset=UTF-8", + "Content-Type": contentType, + "x-ms-blob-content-disposition": `attachment; filename=${fileName}`, + "x-ms-meta-m1": "v1", + "x-ms-meta-m2": "v2", + "Content-Length": data?.length, + }, + }); + + $.export("$summary", "Successfully uploaded the blob."); + return { + success: true, + }; + }, +}; diff --git a/components/azure_storage/azure_storage.app.mjs b/components/azure_storage/azure_storage.app.mjs new file mode 100644 index 0000000000000..b7ffcb7ea870b --- /dev/null +++ b/components/azure_storage/azure_storage.app.mjs @@ -0,0 +1,125 @@ +import { XMLParser } from "fast-xml-parser"; +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +const parser = new XMLParser({ + ignoreAttributes: false, + arrayMode: true, + textNodeName: "value", +}); + +export default { + type: "app", + app: "azure_storage", + propDefinitions: { + containerName: { + type: "string", + label: "Container Name", + description: "The name of the container within the specified storage account.", + async options() { + const { EnumerationResults: { Containers: { Container: containers } } } = + await this.listContainers(); + if (!containers) { + return []; + } + return Array.isArray(containers) + ? containers.map(({ Name: value }) => value) + : [ + containers.Name, + ]; + }, + }, + blobName: { + type: "string", + label: "Blob Name", + description: "The name of the blob within the specified container.", + async options({ containerName }) { + const { EnumerationResults: { Blobs: { Blob: blobs } } } = await this.listBlobs({ + containerName, + }); + if (!blobs) { + return []; + } + return Array.isArray(blobs) + ? blobs.map(({ Name: value }) => value) + : [ + blobs.Name, + ]; + }, + }, + }, + methods: { + getUrl(path) { + const { storage_account_name: storageAccount } = this.$auth; + const baseUrl = constants.BASE_URL + .replace(constants.STORAGE_ACCOUNT_PLACEHOLDER, storageAccount); + return `${baseUrl}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "x-ms-date": new Date().toUTCString(), + "x-ms-version": constants.API_VERSION, + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + async _makeRequest({ + $ = this, path, headers, jsonOutput = true, ...args + } = {}) { + let response; + try { + response = await axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + + } catch (error) { + const errorResponse = parser.parse(error.response.data); + if (errorResponse.Error) { + throw new Error(JSON.stringify(errorResponse.Error, null, 2)); + } + throw error; + } + + return jsonOutput + ? parser.parse(response) + : response; + }, + put(args = {}) { + return this._makeRequest({ + method: "PUT", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + listContainers(args = {}) { + return this._makeRequest({ + ...args, + path: "/", + params: { + ...args.params, + comp: "list", + }, + }); + }, + listBlobs({ + containerName, ...args + } = {}) { + return this._makeRequest({ + ...args, + path: `/${containerName}`, + params: { + ...args.params, + restype: "container", + comp: "list", + }, + }); + }, + }, +}; diff --git a/components/azure_storage/common/constants.mjs b/components/azure_storage/common/constants.mjs new file mode 100644 index 0000000000000..506338caf7e02 --- /dev/null +++ b/components/azure_storage/common/constants.mjs @@ -0,0 +1,9 @@ +const STORAGE_ACCOUNT_PLACEHOLDER = "{storageAccount}"; +const BASE_URL = `https://${STORAGE_ACCOUNT_PLACEHOLDER}.blob.core.windows.net`; +const API_VERSION = "2025-01-05"; + +export default { + STORAGE_ACCOUNT_PLACEHOLDER, + BASE_URL, + API_VERSION, +}; diff --git a/components/azure_storage/common/utils.mjs b/components/azure_storage/common/utils.mjs new file mode 100644 index 0000000000000..d769a6ea15ea5 --- /dev/null +++ b/components/azure_storage/common/utils.mjs @@ -0,0 +1,30 @@ +import fs from "fs"; +import { ConfigurationError } from "@pipedream/platform"; + +function checkTmp(filePath) { + const adjustedPath = filePath.startsWith("/tmp") + ? filePath + : `/tmp/${filePath}`; + + if (!fs.existsSync(adjustedPath)) { + throw new ConfigurationError("File does not exist!"); + } + + return adjustedPath; +} + +function getDataFromFile(filePath) { + const path = checkTmp(filePath); + const file = fs.readFileSync(path); + return file; +} + +function getFilenameFromPath(filePath) { + const pathParts = filePath.split("/"); + return pathParts[pathParts.length - 1]; +} + +export default { + getDataFromFile, + getFilenameFromPath, +}; diff --git a/components/azure_storage/package.json b/components/azure_storage/package.json new file mode 100644 index 0000000000000..d605ccdadfa5d --- /dev/null +++ b/components/azure_storage/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/azure_storage", + "version": "0.1.0", + "description": "Pipedream Azure Storage Components", + "main": "azure_storage.app.mjs", + "keywords": [ + "pipedream", + "azure_storage" + ], + "homepage": "https://pipedream.com/apps/azure_storage", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "mime-types": "^2.1.35" + } +} diff --git a/components/azure_storage/sources/blob-deleted/blob-deleted.mjs b/components/azure_storage/sources/blob-deleted/blob-deleted.mjs new file mode 100644 index 0000000000000..a4b587309ac51 --- /dev/null +++ b/components/azure_storage/sources/blob-deleted/blob-deleted.mjs @@ -0,0 +1,58 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "azure_storage-blob-deleted", + name: "Blob Deleted", + description: "Emit new event when a blob is deleted from a specified container in Azure Storage. [See the documentation](https://learn.microsoft.com/en-us/rest/api/storageservices/list-blobs?tabs=microsoft-entra-id).", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + containerName: { + propDefinition: [ + common.props.app, + "containerName", + ], + }, + }, + methods: { + ...common.methods, + getResourcesFromResponse(response) { + const resources = response.EnumerationResults.Blobs.Blob; + if (!resources) { + return []; + } + return Array.isArray(resources) + ? resources + : [ + resources, + ]; + }, + isResourceRelevant(resource) { + return resource?.Deleted === true; + }, + getResourcesFn() { + return this.app.listBlobs; + }, + getResourcesFnArgs() { + return { + debug: true, + containerName: this.containerName, + params: { + include: "deleted", + }, + }; + }, + generateMeta(resource) { + return { + id: resource.Name, + summary: `Blob Deleted: ${resource.Name}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/azure_storage/sources/blob-deleted/test-event.mjs b/components/azure_storage/sources/blob-deleted/test-event.mjs new file mode 100644 index 0000000000000..adce0639c8b6b --- /dev/null +++ b/components/azure_storage/sources/blob-deleted/test-event.mjs @@ -0,0 +1,24 @@ +export default { + "Name": "test1", + "Deleted": true, + "Properties": { + "Creation-Time": "Tue, 11 Feb 2025 23:05:34 GMT", + "Last-Modified": "Tue, 11 Feb 2025 23:05:34 GMT", + "Etag": 638749119346776200, + "Content-Length": 60222, + "Content-Type": "text/plain; charset=UTF-8", + "Content-Encoding": "", + "Content-Language": "", + "Content-CRC64": "", + "Content-MD5": "3PVKemYZDbECunKqLKSRLw==", + "Cache-Control": "", + "Content-Disposition": "attachment; filename=test1.jpg", + "BlobType": "BlockBlob", + "AccessTier": "Hot", + "AccessTierInferred": true, + "ServerEncrypted": true, + "DeletedTime": "Tue, 11 Feb 2025 23:12:16 GMT", + "RemainingRetentionDays": 6 + }, + "OrMetadata": "" +}; diff --git a/components/azure_storage/sources/common/polling.mjs b/components/azure_storage/sources/common/polling.mjs new file mode 100644 index 0000000000000..547901f05031e --- /dev/null +++ b/components/azure_storage/sources/common/polling.mjs @@ -0,0 +1,58 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../azure_storage.app.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + isResourceRelevant() { + return true; + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + getResourcesFromResponse() { + throw new ConfigurationError("getResourcesFromResponse is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + }, + async run() { + const { + getResourcesFn, + getResourcesFnArgs, + getResourcesFromResponse, + isResourceRelevant, + processResource, + } = this; + + const resourcesFn = getResourcesFn(); + const response = await resourcesFn(getResourcesFnArgs()); + const resources = getResourcesFromResponse(response); + + Array.from(resources) + .filter(isResourceRelevant) + .forEach(processResource); + }, +}; diff --git a/components/azure_storage/sources/new-blob-created/new-blob-created.mjs b/components/azure_storage/sources/new-blob-created/new-blob-created.mjs new file mode 100644 index 0000000000000..383072960c7f4 --- /dev/null +++ b/components/azure_storage/sources/new-blob-created/new-blob-created.mjs @@ -0,0 +1,52 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "azure_storage-new-blob-created", + name: "New Blob Created", + description: "Emit new event when a new blob is created to a specified container in Azure Storage. [See the documentation](https://learn.microsoft.com/en-us/rest/api/storageservices/list-blobs?tabs=microsoft-entra-id).", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + containerName: { + propDefinition: [ + common.props.app, + "containerName", + ], + }, + }, + methods: { + ...common.methods, + getResourcesFromResponse(response) { + const resources = response.EnumerationResults.Blobs.Blob; + if (!resources) { + return []; + } + return Array.isArray(resources) + ? resources + : [ + resources, + ]; + }, + getResourcesFn() { + return this.app.listBlobs; + }, + getResourcesFnArgs() { + return { + debug: true, + containerName: this.containerName, + }; + }, + generateMeta(resource) { + return { + id: resource.Name, + summary: `New Blob: ${resource.Name}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/azure_storage/sources/new-blob-created/test-event.mjs b/components/azure_storage/sources/new-blob-created/test-event.mjs new file mode 100644 index 0000000000000..7e169d3ddb88c --- /dev/null +++ b/components/azure_storage/sources/new-blob-created/test-event.mjs @@ -0,0 +1,23 @@ +export default { + "Name": "test1", + "Properties": { + "Creation-Time": "Tue, 11 Feb 2025 23:05:08 GMT", + "Last-Modified": "Tue, 11 Feb 2025 23:05:08 GMT", + "Etag": 638749119084727400, + "Content-Length": 60222, + "Content-Type": "text/plain; charset=UTF-8", + "Content-Encoding": "", + "Content-Language": "", + "Content-CRC64": "", + "Content-MD5": "3PVKemYZDbECunKqLKSRLw==", + "Cache-Control": "", + "Content-Disposition": "attachment; filename=test1.jpg", + "BlobType": "BlockBlob", + "AccessTier": "Hot", + "AccessTierInferred": true, + "LeaseStatus": "unlocked", + "LeaseState": "available", + "ServerEncrypted": true + }, + "OrMetadata": "" +}; diff --git a/components/azure_storage/sources/new-container-created/new-container-created.mjs b/components/azure_storage/sources/new-container-created/new-container-created.mjs new file mode 100644 index 0000000000000..0ea2398234bdb --- /dev/null +++ b/components/azure_storage/sources/new-container-created/new-container-created.mjs @@ -0,0 +1,42 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "azure_storage-new-container-created", + name: "New Container Created", + description: "Emit new event when a new container is created in the specified Azure Storage account. [See the documentation](https://learn.microsoft.com/en-us/rest/api/storageservices/list-containers2?tabs=microsoft-entra-id#Request).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourcesFromResponse(response) { + const resources = response.EnumerationResults.Containers.Container; + if (!resources) { + return []; + } + return Array.isArray(resources) + ? resources + : [ + resources, + ]; + }, + getResourcesFn() { + return this.app.listContainers; + }, + getResourcesFnArgs() { + return { + debug: true, + }; + }, + generateMeta(resource) { + return { + id: resource.Name, + summary: `New Container: ${resource.Name}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/azure_storage/sources/new-container-created/test-event.mjs b/components/azure_storage/sources/new-container-created/test-event.mjs new file mode 100644 index 0000000000000..30c191a015fba --- /dev/null +++ b/components/azure_storage/sources/new-container-created/test-event.mjs @@ -0,0 +1,14 @@ +export default { + "Name": "test1", + "Properties": { + "Last-Modified": "Mon, 10 Feb 2025 21:39:59 GMT", + "Etag": "\"0x8DD4A1B77C220F2\"", + "LeaseStatus": "unlocked", + "LeaseState": "available", + "DefaultEncryptionScope": "$account-encryption-key", + "DenyEncryptionScopeOverride": false, + "HasImmutabilityPolicy": false, + "HasLegalHold": false, + "ImmutableStorageWithVersioningEnabled": false + } +}; diff --git a/components/backblaze/README.md b/components/backblaze/README.md new file mode 100644 index 0000000000000..c4a65b0e6bc0b --- /dev/null +++ b/components/backblaze/README.md @@ -0,0 +1,11 @@ +# Overview + +The Backblaze API offers robust capabilities for managing data storage and backup solutions on the cloud. Via Pipedream, you can tap into this API to automate tasks like file uploads, data backups, and management of storage buckets. Utilizing Pipedream’s serverless platform, developers can create event-driven workflows to connect Backblaze with other apps, trigger actions based on specific conditions, and streamline operations without manual intervention. + +# Example Use Cases + +- **Automated Backup for New Shopify Orders**: Automatically back up new order details from Shopify to a secure Backblaze bucket. Each time a new order is placed in Shopify, a Pipedream workflow triggers, pulling the order data and storing it as a JSON file in Backblaze. This ensures that important sales data is securely backed up and readily available for analytics or archival purposes. + +- **Sync New Files from Dropbox to Backblaze**: Set up a workflow where new files added to a specified Dropbox folder are automatically backed up to a Backblaze bucket. The workflow listens for new files in Dropbox using Pipedream's Dropbox trigger, then uploads these files to Backblaze, providing a secondary, secure backup location for important documents or media files. + +- **Photo Backup from Instagram to Backblaze**: Create a Pipedream workflow that automatically saves new photos you post on Instagram to a Backblaze bucket. This can be especially useful for photographers or social media managers who want to ensure that a high-quality backup of their digital assets is always available off-platform. diff --git a/components/backblaze/backblaze.app.mjs b/components/backblaze/backblaze.app.mjs new file mode 100644 index 0000000000000..264d033c9dea9 --- /dev/null +++ b/components/backblaze/backblaze.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "backblaze", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/backblaze/package.json b/components/backblaze/package.json new file mode 100644 index 0000000000000..3f1b935faf942 --- /dev/null +++ b/components/backblaze/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/backblaze", + "version": "0.0.1", + "description": "Pipedream Backblaze Components", + "main": "backblaze.app.mjs", + "keywords": [ + "pipedream", + "backblaze" + ], + "homepage": "https://pipedream.com/apps/backblaze", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/backendless/README.md b/components/backendless/README.md index 8d4aacee3d0f5..2569317c8b6d0 100644 --- a/components/backendless/README.md +++ b/components/backendless/README.md @@ -1,19 +1,11 @@ # Overview -Backendless provides a number of services that you can use to build -applications, including: +Backendless is a visual app development platform that allows developers to create mobile and web apps without needing to manage the backend infrastructure. It offers features like database management, user authentication, real-time data, and serverless hosting. With the Backendless API on Pipedream, you can automate workflows, sync data across apps, process data triggers, and handle user actions in real-time. It's perfect for extending app capabilities, integrating with third-party services, and automating repetitive tasks. -- User management -- Data storage -- Push notifications -- Geolocation -- Analytics -- Files +# Example Use Cases -Here are a few examples of applications you could build using Backendless: +- **User Registration Automation**: When a new user registers in your Backendless app, trigger a Pipedream workflow that sends a welcome email using SendGrid, creates a new customer record in Stripe for future billing, and logs the action in a Google Sheets spreadsheet for tracking. -- A social network -- A task manager -- A dating app -- A news aggregator -- A weather app +- **Real-Time Order Processing**: Set up a Pipedream workflow that listens for new purchase events from your Backendless app. When an order is placed, the workflow can verify the order details, create an invoice in QuickBooks, send a notification to a Slack channel for your fulfillment team, and update the order status back in Backendless. + +- **Scheduled Data Sync**: Implement a Pipedream workflow that runs on a schedule to sync data between Backendless and Airtable. It can fetch new records from a Backendless database, transform the data as needed, and upsert records into an Airtable base, ensuring that your team always has the most up-to-date information at their fingertips. diff --git a/components/backendless/package.json b/components/backendless/package.json new file mode 100644 index 0000000000000..7a09af34cd998 --- /dev/null +++ b/components/backendless/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/backendless", + "version": "0.6.0", + "description": "Pipedream backendless Components", + "main": "backendless.app.mjs", + "keywords": [ + "pipedream", + "backendless" + ], + "homepage": "https://pipedream.com/apps/backendless", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/backlog_api/README.md b/components/backlog_api/README.md index 748bea29c6eff..7855fb74577fc 100644 --- a/components/backlog_api/README.md +++ b/components/backlog_api/README.md @@ -1,15 +1,11 @@ # Overview -Backlog is a project management tool that enables developers to track and -manage their work. The Backlog API allows developers to programmatically access -and manipulate their Backlog data. With the Backlog API, developers can build -tools and applications that automate and streamline their Backlog workflow. - -Some examples of what you can build with the Backlog API: - -- A tool to automatically add new Backlog tasks to your project when code is - committed to your version control system -- A dashboard that displays your team's Backlog progress -- A tool to help you ensure that all your Backlog tasks are properly assigned -- An integration that automatically creates Backlog tickets when new issues are - filed in your bug tracker +The Backlog API provides a suite of functions for developers to integrate with and automate their project management needs. Through Pipedream, you can harness this API to create workflows that respond to events in Backlog, manipulate issues, track progress, and sync with other tools. This seamless integration allows you to streamline project management tasks, keep your team informed in real-time, and connect Backlog with other apps to enhance your productivity. + +# Example Use Cases + +- **Issue Syncing Between Backlog and GitHub**: When an issue is reported in Backlog, automatically create a corresponding issue in GitHub to track code-related changes. Conversely, when a GitHub issue is closed, update the status in Backlog. This keeps project management and development in sync. + +- **Slack Notifications for Backlog Updates**: Set up a workflow that sends Slack messages to a designated channel whenever a new issue is created or an existing issue is updated in Backlog. This ensures that your team stays informed about project updates without leaving their communication platform. + +- **Time Tracking and Reporting Automation**: Whenever an issue's status in Backlog changes to 'In Progress', start a timer. When the status changes to 'Resolved' or 'Closed', stop the timer and log the time spent. Compile time tracking data weekly and send a report via email to project managers for resource allocation analysis. diff --git a/components/backlog_api/package.json b/components/backlog_api/package.json new file mode 100644 index 0000000000000..83effabf09c3a --- /dev/null +++ b/components/backlog_api/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/backlog_api", + "version": "0.6.0", + "description": "Pipedream backlog_api Components", + "main": "backlog_api.app.mjs", + "keywords": [ + "pipedream", + "backlog_api" + ], + "homepage": "https://pipedream.com/apps/backlog_api", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/badger_maps/README.md b/components/badger_maps/README.md new file mode 100644 index 0000000000000..75ac98d3e67f1 --- /dev/null +++ b/components/badger_maps/README.md @@ -0,0 +1,11 @@ +# Overview + +The Badger Maps API offers interaction with a platform designed for field sales; it allows you to optimize routes, manage territories, and explore customer locations. Combining the Badger Maps API with Pipedream's capabilities enables the automation of sales processes, integration with CRM systems, and enhancement of data analytics. You can build workflows that respond to events in real-time, sync data across multiple platforms, and create custom triggers and actions that cater specifically to your sales strategies. + +# Example Use Cases + +- **Territory Data Sync with CRM**: Automatically sync new and updated customer information from Badger Maps to a CRM like Salesforce or HubSpot. This ensures that the sales team has the latest data at hand without manual updates. + +- **Optimized Routing for Calendar Events**: Before a salesperson's day begins, trigger a workflow that optimizes their route based on their Google Calendar events. The workflow fetches the day's appointments, sends the locations to Badger Maps for optimization, and then updates the calendar with the best route. + +- **Lead Qualification and Prioritization**: Integrate Badger Maps with a lead generation tool like LinkedIn Lead Gen Forms. When a new lead comes in, use Badger Maps to analyze the lead's location data and prioritize them based on their proximity to existing customers or territories. diff --git a/components/badger_maps/actions/delete-account/delete-account.mjs b/components/badger_maps/actions/delete-account/delete-account.mjs new file mode 100644 index 0000000000000..d1266a92e12fb --- /dev/null +++ b/components/badger_maps/actions/delete-account/delete-account.mjs @@ -0,0 +1,45 @@ +import app from "../../badger_maps.app.mjs"; + +export default { + key: "badger_maps-delete-account", + name: "Delete Account", + description: "Deletes an account. [See the docs](https://badgerupdatedapi.docs.apiary.io/#reference/accounts/retrieve-and-update-account-details/delete-customer).", + type: "action", + version: "0.0.1", + props: { + app, + accountId: { + propDefinition: [ + app, + "accountId", + ], + }, + }, + methods: { + deleteAccount({ + accountId, ...args + } = {}) { + return this.app.delete({ + path: `/customers/${accountId}/`, + ...args, + }); + }, + }, + async run({ $: step }) { + const { + deleteAccount, + accountId, + } = this; + + const response = await deleteAccount({ + step, + accountId, + }); + + step.export("$summary", `Successfully deleted account with ID ${accountId}.`); + + return response || { + success: true, + }; + }, +}; diff --git a/components/badger_maps/package.json b/components/badger_maps/package.json index f9581bc406520..8b5c8442052ee 100644 --- a/components/badger_maps/package.json +++ b/components/badger_maps/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/badger_maps", - "version": "0.0.3", + "version": "0.1.0", "description": "Pipedream Badger Maps Components", "main": "badger_maps.app.mjs", "keywords": [ diff --git a/components/bamboohr/README.md b/components/bamboohr/README.md index cf6c14d759125..bb6794ef29c19 100644 --- a/components/bamboohr/README.md +++ b/components/bamboohr/README.md @@ -1,9 +1,11 @@ # Overview -The BambooHR API enables developers to create amazing tools and applications -that help organizations manage their employee data. With the BambooHR API, you -can: +BambooHR is a comprehensive human resources information system (HRIS) that allows businesses to manage employee data, track time off, run reports, and integrate with other services. With its API, users on Pipedream can automate various HR tasks, sync employee data across platforms, and trigger workflows based on events like new hires, time-off requests, or updates to employee details. -- Build applications that help organizations manage their employee data -- Create tools that help employees self-service their HR data -- Build integrations that connect BambooHR with other systems +# Example Use Cases + +- **Employee Onboarding Automation**: When a new employee is added to BambooHR, trigger a Pipedream workflow to create accounts for them in apps like Slack, G Suite, or Office 365. This could also involve sending a welcome email and adding the new hire to relevant mailing lists and project management tools. + +- **Time-off Request Management**: Automatically sync time-off requests from BambooHR to other calendar apps like Google Calendar or Outlook. When an employee requests time off, a workflow could be triggered to inform their manager, update team calendars, and adjust task deadlines in project management apps like Asana or Trello. + +- **Employee Data Sync and Reporting**: Any update to an employee's profile in BambooHR can trigger a workflow that updates their information in other systems such as Salesforce, Zendesk, or custom databases. This helps maintain data integrity across platforms. Additionally, generate regular reports on employee data or time-off usage and send them to management through email or chat apps like Slack. diff --git a/components/bamboohr/package.json b/components/bamboohr/package.json new file mode 100644 index 0000000000000..a59711aa7bfd2 --- /dev/null +++ b/components/bamboohr/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/bamboohr", + "version": "0.6.0", + "description": "Pipedream bamboohr Components", + "main": "bamboohr.app.mjs", + "keywords": [ + "pipedream", + "bamboohr" + ], + "homepage": "https://pipedream.com/apps/bamboohr", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/bandwidth/README.md b/components/bandwidth/README.md index bb15725ed7ee4..5be044d0db3e4 100644 --- a/components/bandwidth/README.md +++ b/components/bandwidth/README.md @@ -1,11 +1,11 @@ # Overview -With the Bandwidth API, you can build a variety of applications and tools for -managing your communication needs. Here are a few examples of what you can -build: - -- A tool for monitoring your bandwidth usage -- A tool for managing your contact lists -- A tool for sending and receiving text messages -- A tool for making and receiving phone calls -- A tool for recording and storing call data +The Bandwidth API opens up a world of possibilities for integrating telecom services into your applications. Bandwidth specializes in voice, messaging, and 911 access, making it possible to programmatically send and receive text messages, orchestrate calls, and implement emergency call routing. Linking the Bandwidth API with Pipedream allows you to automate these telecom features with various triggers and actions from other apps, creating seamless and powerful workflows. + +# Example Use Cases + +- **Automated Customer Support System**: Build a system that automatically responds to customer support queries via SMS. When an SMS is received, it triggers a workflow that classifies the query using a machine learning model. Based on the classification, it either responds with a predefined message or escalates the issue to a human agent by creating a ticket in a service like Zendesk. + +- **Real-time Alerts and Notifications**: Set up a workflow where Bandwidth sends out voice or SMS alerts based on triggers from monitoring tools like Datadog. For instance, if a monitoring tool detects server downtime, a high priority alert is sent to the on-call engineer via Bandwidth, ensuring quick response times to critical incidents. + +- **Event-Driven Survey Campaigns**: After a customer interaction with your service, use a workflow to send a follow-up survey via SMS. When a customer completes a transaction or interaction, Pipedream triggers Bandwidth to send an SMS with a link to a feedback form. The responses can then be collected and stored in a database like Airtable for analysis and follow-up. diff --git a/components/bannerbear/README.md b/components/bannerbear/README.md index 6df66cf66cb75..816eb17bfcb0a 100644 --- a/components/bannerbear/README.md +++ b/components/bannerbear/README.md @@ -1,9 +1,11 @@ # Overview -Bannerbear's API enables easy creation of images and videos for social media, marketing, and more. With Bannerbear, you can: +Bannerbear is an automated image and video generation service that allows you to create custom visuals with dynamic data. Using the Bannerbear API, you can programmatically generate images and videos for social media posts, e-commerce product images, personalized email marketing visuals, and more, all tailored to your brand's aesthetic and updated in real time with the latest information. -- Create images with text, logos, and other graphical elements -- Place images and videos on top of each other -- Adjust image colors, brightness, and other settings -- Create animations and transitions -- And much more! +# Example Use Cases + +- **Automated Social Media Content Creation**: Set up a Pipedream workflow that listens for new blog posts on your CMS (like WordPress or Contentful). When a new post is detected, the workflow uses Bannerbear to generate a tailored social media image including the post's title and featured image. The image is then automatically posted to your social media accounts like Twitter or Instagram. + +- **Personalized Email Marketing Campaigns**: Create a workflow that triggers when a user signs up for your newsletter or service. The Pipedream workflow calls the Bannerbear API to create a personalized welcome image with the user's name or other details. This image is then embedded in an email sent to the user, using an email service like SendGrid or Mailgun. + +- **Dynamic Product Images for E-commerce**: Integrate Bannerbear with your e-commerce platform (such as Shopify) through Pipedream. When a product's price changes or a new item is added, the workflow generates updated product images with the current price, discount information, or new product label, and uploads these images back to the product's listing on the e-commerce site. diff --git a/components/barcode_lookup/README.md b/components/barcode_lookup/README.md new file mode 100644 index 0000000000000..619919cd0dd96 --- /dev/null +++ b/components/barcode_lookup/README.md @@ -0,0 +1,11 @@ +# Overview + +The Barcode Lookup API allows you to get product data by scanning or inputting the barcode number. With this API on Pipedream, you can automate tasks involving product information retrieval, inventory management, price comparison, and more. Since Pipedream seamlessly integrates with numerous apps, you can connect the Barcode Lookup with other services to streamline various workflows, saving time and reducing manual effort. + +# Example Use Cases + +- **Product Information Enrichment:**: Trigger a Pipedream workflow whenever a new product is added to your database. Use the Barcode Lookup API to fetch detailed information about the product using its barcode and then update your database with the enriched data. + +- **Inventory Tracking:**: Scan barcodes to trigger a Pipedream workflow that checks the current stock levels in your inventory management system. If stock is low, automatically reorder the product or alert the appropriate team members. + +- **Price Monitoring and Comparison:**: Combine the Barcode Lookup API with a scheduling trigger to periodically check the prices of products in various online stores. Use this data to adjust pricing in your own store to stay competitive. diff --git a/components/barcode_lookup/barcode_lookup.app.mjs b/components/barcode_lookup/barcode_lookup.app.mjs new file mode 100644 index 0000000000000..00bf75d5e208d --- /dev/null +++ b/components/barcode_lookup/barcode_lookup.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "barcode_lookup", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/barcode_lookup/package.json b/components/barcode_lookup/package.json new file mode 100644 index 0000000000000..1029a92512a3a --- /dev/null +++ b/components/barcode_lookup/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/barcode_lookup", + "version": "0.0.1", + "description": "Pipedream Barcode Lookup Components", + "main": "barcode_lookup.app.mjs", + "keywords": [ + "pipedream", + "barcode_lookup" + ], + "homepage": "https://pipedream.com/apps/barcode_lookup", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/baremetrics/README.md b/components/baremetrics/README.md index e9b6861e05d0f..cf47830b92bbc 100644 --- a/components/baremetrics/README.md +++ b/components/baremetrics/README.md @@ -1,9 +1,11 @@ # Overview -Baremetrics provides an API that allows developers to interact with the Baremetrics platform in order to build integrations, automate tasks, and track data. Here are some examples of what you can build with the Baremetrics API: +The Baremetrics API offers granular data on your SaaS metrics, including MRR, ARR, LTV, and churn rates, directly accessible for analytics, reporting, and enhancing business intelligence. With Pipedream's integration capabilities, you can automate workflows that react to this data in real-time, syncing with other services for actions like customer engagement, financial forecasting, and trigger-based alerting. -- Integrations with other business platforms (e.g. CRM, accounting, ecommerce) +# Example Use Cases -- Automated tasks such as fetching data or generating reports +- **Customer Milestone Engagement**: Set up a workflow that listens for updates to customer lifetime value (LTV) via the Baremetrics API. When a customer's LTV hits a predefined milestone, trigger an automated congratulatory email or special offer via SendGrid or another email service, bolstering customer relations and encouraging brand loyalty. -- Tracking data such as customer acquisition or churn +- **Churn Alerting and Follow-Up**: Monitor churn events with the Baremetrics API on Pipedream. When a churn is detected, automatically send the event to a Slack channel to alert the team. Following that, trigger a feedback survey from Typeform to the churned customer to understand their reasons for leaving and gather data for service improvement. + +- **Real-Time Reporting Dashboard**: Build a workflow where Pipedream pulls the latest SaaS metrics from Baremetrics and pushes them to a Google Sheet or a data visualization tool like Google Data Studio. Automate daily or weekly updates to maintain a real-time dashboard for stakeholders to make more informed decisions based on the most current business performance indicators. diff --git a/components/baremetrics/actions/create-customer/create-customer.mjs b/components/baremetrics/actions/create-customer/create-customer.mjs new file mode 100644 index 0000000000000..a75def489653c --- /dev/null +++ b/components/baremetrics/actions/create-customer/create-customer.mjs @@ -0,0 +1,58 @@ +import app from "../../baremetrics.app.mjs"; + +export default { + key: "baremetrics-create-customer", + name: "Create Customer", + description: "Create a customer. [See the documentation](https://developers.baremetrics.com/reference/create-customer)", + version: "0.0.1", + type: "action", + props: { + app, + sourceId: { + propDefinition: [ + app, + "sourceId", + ], + }, + oid: { + propDefinition: [ + app, + "oid", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + notes: { + propDefinition: [ + app, + "notes", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createCustomer({ + $, + source_id: this.sourceId, + data: { + name: this.name, + notes: this.notes, + email: this.email, + oid: this.oid, + }, + }); + + $.export("$summary", `Successfully created the customer record with the ID '${this.oid}'`); + + return response; + }, +}; diff --git a/components/baremetrics/actions/create-plan/create-plan.mjs b/components/baremetrics/actions/create-plan/create-plan.mjs new file mode 100644 index 0000000000000..5da71fc5def4a --- /dev/null +++ b/components/baremetrics/actions/create-plan/create-plan.mjs @@ -0,0 +1,74 @@ +import app from "../../baremetrics.app.mjs"; + +export default { + key: "baremetrics-create-plan", + name: "Create Plan", + description: "Create a plan. [See the documentation](https://developers.baremetrics.com/reference/create-plan)", + version: "0.0.1", + type: "action", + props: { + app, + sourceId: { + propDefinition: [ + app, + "sourceId", + ], + }, + oid: { + propDefinition: [ + app, + "oid", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + optional: false, + description: "Name of the Plan", + }, + currency: { + propDefinition: [ + app, + "currency", + ], + }, + amount: { + propDefinition: [ + app, + "amount", + ], + }, + interval: { + propDefinition: [ + app, + "interval", + ], + }, + intervalCount: { + propDefinition: [ + app, + "intervalCount", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createPlan({ + $, + source_id: this.sourceId, + data: { + name: this.name, + currency: this.currency, + amount: this.amount, + interval: this.interval, + interval_count: this.intervalCount, + oid: this.oid, + }, + }); + + $.export("$summary", `Successfully created the plan with the ID '${this.oid}'`); + + return response; + }, +}; diff --git a/components/baremetrics/actions/create-subscription/create-subscription.mjs b/components/baremetrics/actions/create-subscription/create-subscription.mjs new file mode 100644 index 0000000000000..601e7e044949e --- /dev/null +++ b/components/baremetrics/actions/create-subscription/create-subscription.mjs @@ -0,0 +1,64 @@ +import app from "../../baremetrics.app.mjs"; + +export default { + key: "baremetrics-create-subscription", + name: "Create Subscription", + description: "Subscribe a client to a plan. [See the documentation](https://developers.baremetrics.com/reference/create-subscription)", + version: "0.0.1", + type: "action", + props: { + app, + sourceId: { + propDefinition: [ + app, + "sourceId", + ], + }, + oid: { + propDefinition: [ + app, + "oid", + ], + }, + customerOid: { + propDefinition: [ + app, + "customerOid", + (c) => ({ + sourceId: c.sourceId, + }), + ], + }, + planOid: { + propDefinition: [ + app, + "planOid", + (c) => ({ + sourceId: c.sourceId, + }), + ], + }, + startedAt: { + propDefinition: [ + app, + "startedAt", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createSubscription({ + $, + source_id: this.sourceId, + data: { + started_at: this.startedAt, + oid: this.oid, + customer_oid: this.customerOid, + plan_oid: this.planOid, + }, + }); + + $.export("$summary", `Successfully subscribed customer with ID '${this.customerOid}' to the plan with ID '${this.planOid}'`); + + return response; + }, +}; diff --git a/components/baremetrics/actions/update-customer/update-customer.mjs b/components/baremetrics/actions/update-customer/update-customer.mjs new file mode 100644 index 0000000000000..0ad4e53d34aaa --- /dev/null +++ b/components/baremetrics/actions/update-customer/update-customer.mjs @@ -0,0 +1,61 @@ +import app from "../../baremetrics.app.mjs"; + +export default { + key: "baremetrics-update-customer", + name: "Update Customer", + description: "Update a customer. [See the documentation](https://developers.baremetrics.com/reference/update-customer)", + version: "0.0.1", + type: "action", + props: { + app, + sourceId: { + propDefinition: [ + app, + "sourceId", + ], + }, + customerOid: { + propDefinition: [ + app, + "customerOid", + (c) => ({ + sourceId: c.sourceId, + }), + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + notes: { + propDefinition: [ + app, + "notes", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + }, + async run({ $ }) { + const response = await this.app.updateCustomer({ + $, + source_id: this.sourceId, + customer_oid: this.customerOid, + data: { + name: this.name, + notes: this.notes, + email: this.email, + }, + }); + + $.export("$summary", `Successfully updated the customer record with the ID '${this.customerOid}'`); + + return response; + }, +}; diff --git a/components/baremetrics/baremetrics.app.mjs b/components/baremetrics/baremetrics.app.mjs index 1346884531fce..9af7021c6eb44 100644 --- a/components/baremetrics/baremetrics.app.mjs +++ b/components/baremetrics/baremetrics.app.mjs @@ -1,11 +1,186 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "baremetrics", - propDefinitions: {}, + propDefinitions: { + name: { + type: "string", + label: "Name", + description: "Name of the customer", + optional: true, + }, + notes: { + type: "string", + label: "Notes", + description: "Your own notes for this customer. These will be displayed in the profile", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "An email address for this customer", + optional: true, + }, + startedAt: { + type: "string", + label: "Started At", + description: "A unix timestamp in seconds of when this subscription started", + }, + oid: { + type: "string", + label: "OID", + description: "The unique OID", + }, + currency: { + type: "string", + label: "Currency", + description: "The ISO code of the currency of this plan", + }, + amount: { + type: "string", + label: "Amount", + description: "How much is this plan? (In cents)", + }, + interval: { + type: "string", + label: "Interval", + description: "The time interval at which the plan is charged", + options: constants.INTERVALS, + }, + intervalCount: { + type: "string", + label: "intervalCount", + description: "intervalCount", + }, + sourceId: { + type: "string", + label: "Source ID", + description: "ID of the source of the customer", + async options() { + const response = await this.getSources({}); + const sourcesIDs = response.sources; + return sourcesIDs.map(({ + id, provider, + }) => ({ + value: id, + label: provider, + })); + }, + }, + customerOid: { + type: "string", + label: "Customer OID", + description: "Your unique OID for the customer", + async options({ sourceId }) { + const response = await this.getCustomers({ + sourceId, + }); + const customersOids = response.customers; + return customersOids.map(({ + oid, name, + }) => ({ + value: oid, + label: name, + })); + }, + }, + planOid: { + type: "string", + label: "Plan OID", + description: "Your unique OID for the plan", + async options({ sourceId }) { + const response = await this.getPlans({ + sourceId, + }); + const plansOids = response.plans; + return plansOids.map(({ + oid, name, + }) => ({ + value: oid, + label: name, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.baremetrics.com/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + async createCustomer({ + source_id, ...args + }) { + return this._makeRequest({ + method: "post", + path: `/${source_id}/customers`, + ...args, + }); + }, + async createPlan({ + source_id, ...args + }) { + return this._makeRequest({ + method: "post", + path: `/${source_id}/plans`, + ...args, + }); + }, + async createSubscription({ + source_id, ...args + }) { + return this._makeRequest({ + method: "post", + path: `/${source_id}/subscriptions`, + ...args, + }); + }, + async updateCustomer({ + source_id, customer_oid, ...args + }) { + return this._makeRequest({ + method: "put", + path: `/${source_id}/customers/${customer_oid}`, + ...args, + }); + }, + async getCustomers({ + sourceId: source_id, ...args + }) { + return this._makeRequest({ + path: `/${source_id}/customers`, + ...args, + }); + }, + async getPlans({ + sourceId: source_id, ...args + }) { + return this._makeRequest({ + path: `/${source_id}/plans`, + ...args, + }); + }, + async getSources(args = {}) { + return this._makeRequest({ + path: "/sources", + ...args, + }); }, }, }; diff --git a/components/baremetrics/common/constants.mjs b/components/baremetrics/common/constants.mjs new file mode 100644 index 0000000000000..faa988a32d12c --- /dev/null +++ b/components/baremetrics/common/constants.mjs @@ -0,0 +1,7 @@ +export default { + INTERVALS: [ + "day", + "month", + "year", + ], +}; diff --git a/components/baremetrics/package.json b/components/baremetrics/package.json new file mode 100644 index 0000000000000..e95cd528a6482 --- /dev/null +++ b/components/baremetrics/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/baremetrics", + "version": "0.1.0", + "description": "Pipedream baremetrics Components", + "main": "baremetrics.app.mjs", + "keywords": [ + "pipedream", + "baremetrics" + ], + "homepage": "https://pipedream.com/apps/baremetrics", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} diff --git a/components/bart/README.md b/components/bart/README.md index 52cb5e020217c..e903cfa93e1f2 100644 --- a/components/bart/README.md +++ b/components/bart/README.md @@ -1,3 +1,14 @@ # Overview -With the BART API, you can access real-time arrival information for BART trains, as well as schedule information. This makes it possible to build applications that can help people plan their trips on BART, or track the status of a train in real-time. +The BART API provides real-time data about the Bay Area Rapid Transit system, including schedules, advisories, and station information. Using Pipedream, developers can craft serverless workflows that react to BART data and integrate with countless apps to automate transit-related tasks, enrich apps with transit data, and enhance user experiences with up-to-the-minute BART information. + +# Example Use Cases + +- **Commuter Alert System** + Create a workflow that triggers at specific times (like rush hours) to check BART schedules and delays. If a delay is detected on a commuter's usual route, the workflow could automatically send a notification via SMS (using Twilio) or email (using SendGrid) to alert them ahead of time. + +- **Dynamic Transit Dashboard** + Set up a workflow that periodically fetches the latest BART station statuses and advisories. The gathered data could then be sent to a Google Sheets document, creating a live dashboard that organizations can use for managing employee travel or informing visitors about the best times to travel. + +- **Event Planning Coordination** + Integrate the BART API with calendar apps like Google Calendar. When an event is added to a calendar, a workflow could check the BART schedule for that day and time, then email participants advice on the best departure times or alert them to any expected service disruptions. diff --git a/components/basecamp/README.md b/components/basecamp/README.md index 575b783c2b8ae..43274cd0bdfa0 100644 --- a/components/basecamp/README.md +++ b/components/basecamp/README.md @@ -1,8 +1,11 @@ # Overview -Using the Basecamp API, you can build applications that: +The Basecamp API enables the automation of project management tasks, facilitating seamless interaction with Basecamp's project management tools. Through Pipedream, you can leverage the API to create custom workflows that trigger actions within Basecamp, sync data across various platforms, and enhance team collaboration with automated notifications and task management. -- Access and manage your Basecamp account -- Create, edit, and delete projects -- Manage project members -- Manage to-dos +# Example Use Cases + +- **Automated Project Creation**: Upon closing deals in a CRM like Salesforce, trigger a workflow to create a new project in Basecamp with predefined to-do lists and document templates, ensuring that project kickoff is standardized and immediate. + +- **Task Tracking and Notifications**: Connect the Basecamp API to a time tracking tool like Toggl. Whenever a task is marked as completed in Basecamp, log the time automatically in Toggl and send a Slack notification to the team, keeping everyone informed and aligned. + +- **Daily Digest Email**: Compile a daily digest of updates across all Basecamp projects, like new messages, to-dos, or documents added, and send this summary via email using a service like SendGrid. This keeps stakeholders informed without them needing to manually check Basecamp for progress. diff --git a/components/basecamp/actions/create-campfire-chatbot-message/create-campfire-chatbot-message.mjs b/components/basecamp/actions/create-campfire-chatbot-message/create-campfire-chatbot-message.mjs new file mode 100644 index 0000000000000..a6d2602132b18 --- /dev/null +++ b/components/basecamp/actions/create-campfire-chatbot-message/create-campfire-chatbot-message.mjs @@ -0,0 +1,78 @@ +import app from "../../basecamp.app.mjs"; +import common from "../../common/common.mjs"; + +export default { + key: "basecamp-create-campfire-chatbot-message", + name: "Create Campfire Chatbot Message", + description: "Creates a message in a Campfire for a Basecamp Chatbot. [See the documentation](https://github.com/basecamp/bc3-api/blob/master/sections/chatbots.md#create-a-line)", + type: "action", + version: "0.0.4", + props: { + ...common.props, + campfireId: { + propDefinition: [ + app, + "campfireId", + ({ + accountId, + projectId, + }) => ({ + accountId, + projectId, + }), + ], + }, + botId: { + propDefinition: [ + app, + "botId", + ({ + accountId, + projectId, + campfireId, + }) => ({ + accountId, + projectId, + campfireId, + }), + ], + }, + content: { + type: "string", + label: "Content", + description: "The plain text body for the Campfire message.", + }, + }, + async run({ $ }) { + const { + accountId, + projectId, + campfireId, + botId, + content, + } = this; + + const { lines_url: url } = await this.app.getChatbot({ + $, + accountId, + projectId, + campfireId, + botId, + }); + + const message = await this.app.makeRequest({ + $, + accountId, + url, + method: "POST", + data: { + content, + }, + }); + + $.export("$summary", "Successfully posted campfire chatbot message"); + return message ?? { + content, + }; + }, +}; diff --git a/components/basecamp/actions/create-campfire-message/create-campfire-message.mjs b/components/basecamp/actions/create-campfire-message/create-campfire-message.mjs index ddbe9bbfca7fe..7eaba8634f5a5 100644 --- a/components/basecamp/actions/create-campfire-message/create-campfire-message.mjs +++ b/components/basecamp/actions/create-campfire-message/create-campfire-message.mjs @@ -1,28 +1,14 @@ import app from "../../basecamp.app.mjs"; +import common from "../../common/common.mjs"; export default { key: "basecamp-create-campfire-message", name: "Create Campfire Message", - description: "Creates a line in the Campfire for the selected project. [See the docs here](https://github.com/basecamp/bc3-api/blob/master/sections/campfires.md#create-a-campfire-line)", + description: "Creates a message in a selected Campfire. [See the documentation](https://github.com/basecamp/bc3-api/blob/master/sections/campfires.md#create-a-campfire-line)", type: "action", - version: "0.0.4", + version: "0.0.8", props: { - app, - accountId: { - propDefinition: [ - app, - "accountId", - ], - }, - projectId: { - propDefinition: [ - app, - "projectId", - ({ accountId }) => ({ - accountId, - }), - ], - }, + ...common.props, campfireId: { propDefinition: [ app, @@ -39,7 +25,7 @@ export default { content: { type: "string", label: "Content", - description: "The plain text body for the Campfire line.", + description: "The plain text body for the Campfire message.", }, }, async run({ $ }) { @@ -60,7 +46,7 @@ export default { }, }); - $.export("$summary", `Successfully posted campfire message with ID ${message.id}`); + $.export("$summary", `Successfully created campfire message (ID: ${message.id})`); return message; }, }; diff --git a/components/basecamp/actions/create-card/create-card.mjs b/components/basecamp/actions/create-card/create-card.mjs index 2011e4b3433a1..353e06e7a138d 100644 --- a/components/basecamp/actions/create-card/create-card.mjs +++ b/components/basecamp/actions/create-card/create-card.mjs @@ -1,25 +1,23 @@ import app from "../../basecamp.app.mjs"; +import common from "../../common/common.mjs"; export default { key: "basecamp-create-card", name: "Create a Card", - description: "Creates a card in the select column. [See the documentation](https://github.com/basecamp/bc3-api/blob/master/sections/card_table_cards.md#create-a-card)", + description: "Creates a card in a selected column. [See the documentation](https://github.com/basecamp/bc3-api/blob/master/sections/card_table_cards.md#create-a-card)", type: "action", - version: "0.0.3", + version: "0.1.1", props: { - app, - accountId: { + ...common.props, + cardTableId: { propDefinition: [ app, - "accountId", - ], - }, - projectId: { - propDefinition: [ - app, - "projectId", - ({ accountId }) => ({ + "cardTableId", + ({ + accountId, projectId, + }) => ({ accountId, + projectId, }), ], }, @@ -28,35 +26,37 @@ export default { app, "columnId", ({ - accountId, projectId, + accountId, projectId, cardTableId, }) => ({ accountId, projectId, + cardTableId, }), ], }, title: { type: "string", label: "Title", - description: "Title of the card", + description: "The title of the new card.", }, content: { type: "string", label: "Content", - description: "Content of the card", + description: "The content of the card. [See the documentation](https://github.com/basecamp/bc3-api/blob/master/sections/card_table_cards.md#create-a-card) for information on using HTML tags.", optional: true, }, dueOn: { type: "string", - label: "Due on", - description: "Due date (ISO 8601) of the card, e.g.: `2023-12-12`", + label: "Due On", + description: "The due date of the card, in `YYYY-MM-DD` format.", optional: true, }, notify: { type: "boolean", - label: "Notify", - description: "Whether to notify assignees. Defaults to false", + label: "Notify Assignees", + description: "Whether to notify assignees.", optional: true, + default: false, }, }, async run({ $ }) { @@ -83,7 +83,7 @@ export default { }, }); - $.export("$summary", `Successfully created card with ID ${card.id}`); + $.export("$summary", `Successfully created card (ID: ${card.id})`); return card; }, }; diff --git a/components/basecamp/actions/create-comment/create-comment.mjs b/components/basecamp/actions/create-comment/create-comment.mjs index 99402aed6d794..721ed21a1c789 100644 --- a/components/basecamp/actions/create-comment/create-comment.mjs +++ b/components/basecamp/actions/create-comment/create-comment.mjs @@ -1,60 +1,96 @@ import app from "../../basecamp.app.mjs"; +import common from "../../common/common.mjs"; export default { key: "basecamp-create-comment", name: "Create a Comment", - description: "Publishes a comment to the select recording. [See the docs here](https://github.com/basecamp/bc3-api/blob/master/sections/comments.md#create-a-comment)", + description: "Creates a comment in a selected recording. [See the documentation](https://github.com/basecamp/bc3-api/blob/master/sections/comments.md#create-a-comment)", type: "action", - version: "0.0.4", + version: "0.1.0", props: { - app, - accountId: { + ...common.props, + recordingTypeAlert: { + type: "alert", + alertType: "info", + content: `You can select either a **Recording** (by selecting the recording type) or a **Card** (by selecting the card table and column) to create the comment on. +\\ +If both are specified, the **Card** will take precedence.`, + }, + recordingType: { propDefinition: [ app, - "accountId", + "recordingType", ], + optional: true, }, - projectId: { + recordingId: { propDefinition: [ app, - "projectId", - ({ accountId }) => ({ + "recordingId", + ({ accountId, + projectId, + recordingType, + }) => ({ + accountId, + projectId, + recordingType, }), ], + optional: true, }, - recordingType: { + cardTableId: { propDefinition: [ app, - "recordingType", + "cardTableId", + ({ + accountId, projectId, + }) => ({ + accountId, + projectId, + }), ], + optional: true, }, - recordingId: { + columnId: { propDefinition: [ app, - "recordingId", + "columnId", ({ + accountId, projectId, cardTableId, + }) => ({ accountId, projectId, - recordingType, + cardTableId, + }), + ], + optional: true, + }, + cardId: { + propDefinition: [ + app, + "cardId", + ({ + accountId, projectId, cardTableId, columnId, }) => ({ accountId, projectId, - recordingType, + cardTableId, + columnId, }), ], + optional: true, }, content: { type: "string", label: "Content", - description: "The body of the comment. See [Rich text guide](https://github.com/basecamp/bc3-api/blob/master/sections/rich_text.md) for what HTML tags are allowed.", + description: "The body of the comment. [See the documentation](https://github.com/basecamp/bc3-api/blob/master/sections/comments.md#create-a-comment) for information on using HTML tags.", }, }, async run({ $ }) { const { accountId, projectId, - recordingId, content, } = this; @@ -62,13 +98,13 @@ export default { $, accountId, projectId, - recordingId, + recordingId: this.cardId ?? this.recordingId, data: { content, }, }); - $.export("$summary", `Successfully posted comment with ID ${comment.id}`); + $.export("$summary", `Successfully posted comment (ID: ${comment.id})`); return comment; }, }; diff --git a/components/basecamp/actions/create-message/create-message.mjs b/components/basecamp/actions/create-message/create-message.mjs index 0bfb6d42276f3..8032fec26a95c 100644 --- a/components/basecamp/actions/create-message/create-message.mjs +++ b/components/basecamp/actions/create-message/create-message.mjs @@ -1,28 +1,14 @@ import app from "../../basecamp.app.mjs"; +import common from "../../common/common.mjs"; export default { key: "basecamp-create-message", name: "Create Message", - description: "Publishes a message in the project and message board selected. [See the docs here](https://github.com/basecamp/bc3-api/blob/master/sections/messages.md#create-a-message)", + description: "Creates a message in a selected message board. [See the documentation](https://github.com/basecamp/bc3-api/blob/master/sections/messages.md#create-a-message)", type: "action", - version: "0.0.4", + version: "0.0.8", props: { - app, - accountId: { - propDefinition: [ - app, - "accountId", - ], - }, - projectId: { - propDefinition: [ - app, - "projectId", - ({ accountId }) => ({ - accountId, - }), - ], - }, + ...common.props, messageBoardId: { propDefinition: [ app, @@ -44,7 +30,7 @@ export default { content: { type: "string", label: "Content", - description: "The body of the message. See [Rich text guide](https://github.com/basecamp/bc3-api/blob/master/sections/rich_text.md) for what HTML tags are allowed.", + description: "The body of the message. [See the documentation](https://github.com/basecamp/bc3-api/blob/master/sections/messages.md#create-a-message) for information on using HTML tags.", optional: true, }, messageTypeId: { @@ -85,7 +71,7 @@ export default { }, }); - $.export("$summary", `Successfully posted message with ID ${message.id}`); + $.export("$summary", `Successfully posted message (ID: ${message.id})`); return message; }, }; diff --git a/components/basecamp/actions/create-todo-item/create-todo-item.mjs b/components/basecamp/actions/create-todo-item/create-todo-item.mjs index 83488d16dc8d5..823298c52f24b 100644 --- a/components/basecamp/actions/create-todo-item/create-todo-item.mjs +++ b/components/basecamp/actions/create-todo-item/create-todo-item.mjs @@ -1,28 +1,14 @@ import app from "../../basecamp.app.mjs"; +import common from "../../common/common.mjs"; export default { key: "basecamp-create-todo-item", - name: "Create Todo Item", - description: "Creates a todo in the project and message board selected. [See the docs here](https://github.com/basecamp/bc3-api/blob/master/sections/todos.md#create-a-to-do)", + name: "Create To-do Item", + description: "Creates a to-do item in a selected to-do list. [See the documentation](https://github.com/basecamp/bc3-api/blob/master/sections/todos.md#create-a-to-do)", type: "action", - version: "0.0.4", + version: "0.0.8", props: { - app, - accountId: { - propDefinition: [ - app, - "accountId", - ], - }, - projectId: { - propDefinition: [ - app, - "projectId", - ({ accountId }) => ({ - accountId, - }), - ], - }, + ...common.props, todoSetId: { propDefinition: [ app, @@ -53,13 +39,13 @@ export default { }, content: { type: "string", - label: "Content", - description: "The title of the todo item.", + label: "Title (Content)", + description: "The title of the to-do item.", }, description: { type: "string", label: "Description", - description: "Containing information about the to-do. See [Rich text guide](https://github.com/basecamp/bc3-api/blob/master/sections/rich_text.md) for what HTML tags are allowed.", + description: "Description of the item. [See the documentation](https://github.com/basecamp/bc3-api/blob/master/sections/todos.md#create-a-to-do) for information on using HTML tags.", optional: true, }, assigneeIds: { @@ -75,19 +61,19 @@ export default { }), ], label: "Assignee", - description: "An array of people that will be assigned to this to-do.", + description: "One or more people to be assigned to this item.", optional: true, }, dueOn: { type: "string", - label: "Due on", - description: "A date when the to-do should be completed. format: `yyyy-mm-dd`.", + label: "Due On", + description: "The due date when the item should be completed, in `YYYY-MM-DD` format.`", optional: true, }, startsOn: { type: "string", - label: "Starts on", - description: "Allows the to-do to run from this date to the due date. format: `yyyy-mm-dd`.", + label: "Starts On", + description: "The start date, in `YYYY-MM-DD` format. Allows the item to run from this date to the `Due Date`.", optional: true, }, }, @@ -117,7 +103,7 @@ export default { }, }); - $.export("$summary", `Successfully created todo with ID ${todo.id}`); + $.export("$summary", `Successfully created to-do (ID: ${todo.id})`); return todo; }, }; diff --git a/components/basecamp/basecamp.app.mjs b/components/basecamp/basecamp.app.mjs index b03e95eb217d8..28b8a48392746 100644 --- a/components/basecamp/basecamp.app.mjs +++ b/components/basecamp/basecamp.app.mjs @@ -7,8 +7,8 @@ export default { propDefinitions: { accountId: { type: "string", - label: "Account Id", - description: "The ID of the account.", + label: "Account ID", + description: "Select an account or provide an account ID.", async options() { const accounts = await this.getAccounts({}); return accounts.map(({ @@ -22,11 +22,16 @@ export default { }, projectId: { type: "string", - label: "Project Id", - description: "The ID of the project.", - async options({ accountId }) { + label: "Project ID", + description: "Select a project or provide a project ID.", + async options({ + accountId, page, + }) { const projects = await this.getProjects({ accountId, + params: { + page: page + 1, + }, }); return projects.map(({ id, @@ -39,8 +44,8 @@ export default { }, messageBoardId: { type: "string", - label: "Message Board Id", - description: "The ID of the message board.", + label: "Message Board ID", + description: "Select a message board or provide a message board ID.", async options({ accountId, projectId, @@ -61,20 +66,24 @@ export default { }, recordingId: { type: "string", - label: "Recording Id", - description: "The ID of the recording.", + label: "Recording ID", + description: "Select a recording or provide a recording ID.", async options({ accountId, projectId, recordingType, + page, }) { if (!recordingType) { return []; } const recordings = await this.getRecordings({ accountId, - projectId, - recordingType, + params: { + type: recordingType, + bucket: projectId, + page: page + 1, + }, }); return recordings .map(({ @@ -95,14 +104,18 @@ export default { peopleIds: { type: "string[]", label: "People", - description: "An array of all people visible to the current user.", + description: "One or more people visible to the current user.", async options({ accountId, projectId, + page, }) { const people = await this.getPeople({ accountId, projectId, + params: { + page: page + 1, + }, }); return people. map(({ @@ -116,8 +129,8 @@ export default { }, todoSetId: { type: "string", - label: "Todo Set Id", - description: "The ID of the todo set.", + label: "To-do Set Id", + description: "Select a to-do set or provide a to-do set ID.", async options({ accountId, projectId, @@ -138,8 +151,8 @@ export default { }, campfireId: { type: "string", - label: "Campfire Id", - description: "The ID of the campfire.", + label: "Campfire ID", + description: "Select a campfire or provide a campfire ID.", async options({ accountId, projectId, @@ -160,17 +173,21 @@ export default { }, todoListId: { type: "string", - label: "Todo List Id", - description: "The ID of the todo list.", + label: "To-do List Id", + description: "The ID of the to-do list.", async options({ accountId, projectId, todoSetId, + page, }) { const todoslists = await this.getTodoLists({ accountId, projectId, todoSetId, + params: { + page: page + 1, + }, }); return todoslists .map(({ @@ -184,7 +201,7 @@ export default { }, messageTypeId: { type: "string", - label: "Message Types", + label: "Message Type", description: "Select a type for the message.", async options({ accountId, @@ -203,16 +220,36 @@ export default { })); }, }, - columnId: { + cardTableId: { type: "string", - label: "Column ID", - description: "The column ID", + label: "Card Table ID", + description: "Select a card table or provide a card table ID.", async options({ accountId, projectId, + }) { + const columns = await this.getCardTables({ + accountId, + projectId, + }); + return columns.map(({ + id: value, title: label, + }) => ({ + value, + label, + })); + }, + }, + columnId: { + type: "string", + label: "Card Column ID", + description: "Select a card column or provide a column ID.", + async options({ + accountId, projectId, cardTableId, }) { const columns = await this.getColumns({ accountId, projectId, + cardTableId, }); return columns.map(({ id: value, title: label, @@ -222,6 +259,46 @@ export default { })); }, }, + cardId: { + type: "string", + label: "Card ID", + description: "Select a card or provide a card ID.", + async options({ + accountId, projectId, columnId, + }) { + const cards = await this.getCards({ + accountId, + projectId, + columnId, + }); + return cards.map(({ + id: value, title: label, + }) => ({ + value, + label, + })); + }, + }, + botId: { + type: "string", + label: "Chatbot ID", + description: "Select a chatbot to send the message from.", + async options({ + accountId, projectId, campfireId, + }) { + const bots = await this.listChatbots({ + accountId, + projectId, + campfireId, + }); + return bots?.map(({ + id: value, service_name: label, + }) => ({ + value, + label, + })) || []; + }, + }, }, methods: { async getAccounts({ $ = this }) { @@ -262,54 +339,52 @@ export default { async getProjects(args = {}) { return this.makeRequest({ path: "/projects.json", - accountId: args.accountId, ...args, }); }, - async getPeople(args = {}) { + async getPeople({ + projectId, ...args + } = {}) { return this.makeRequest({ - path: `/projects/${args.projectId}/people.json`, - accountId: args.accountId, + path: `/projects/${projectId}/people.json`, ...args, }); }, - async getMessageTypes(args = {}) { + async getMessageTypes({ + projectId, ...args + } = {}) { return this.makeRequest({ - path: `/buckets/${args.projectId}/categories.json`, - accountId: args.accountId, + path: `/buckets/${projectId}/categories.json`, ...args, }); }, - async getProject(args = {}) { + async getProject({ + projectId, ...args + } = {}) { return this.makeRequest({ - path: `/projects/${args.projectId}.json`, - accountId: args.accountId, + path: `/projects/${projectId}.json`, ...args, }); }, async getRecordings(args = {}) { return this.makeRequest({ path: "/projects/recordings.json", - accountId: args.accountId, - params: { - type: args.recordingType, - bucket: args.projectId, - }, ...args, }); }, - async getTodoLists(args = {}) { + async getTodoLists({ + projectId, todoSetId, ...args + } = {}) { return this.makeRequest({ - path: `/buckets/${args.projectId}/todosets/${args.todoSetId}/todolists.json`, - accountId: args.accountId, + path: `/buckets/${projectId}/todosets/${todoSetId}/todolists.json`, ...args, }); }, - async createMessage(args = {}) { + async createMessage({ + projectId, messageBoardId, ...args + } = {}) { return this.makeRequest({ - $: args.$, - accountId: args.accountId, - path: `/buckets/${args.projectId}/message_boards/${args.messageBoardId}/messages.json`, + path: `/buckets/${projectId}/message_boards/${messageBoardId}/messages.json`, method: "post", ...args, }); @@ -323,72 +398,96 @@ export default { ...args, }); }, - async createCampfireMessage(args = {}) { + async createCampfireMessage({ + projectId, campfireId, ...args + } = {}) { return this.makeRequest({ - $: args.$, - accountId: args.accountId, - path: `/buckets/${args.projectId}/chats/${args.campfireId}/lines.json`, + path: `/buckets/${projectId}/chats/${campfireId}/lines.json`, method: "post", ...args, }); }, - async createComment(args = {}) { + async createComment({ + projectId, recordingId, ...args + } = {}) { return this.makeRequest({ - $: args.$, - accountId: args.accountId, - path: `/buckets/${args.projectId}/recordings/${args.recordingId}/comments.json`, + path: `/buckets/${projectId}/recordings/${recordingId}/comments.json`, method: "post", ...args, }); }, - async createTodoItem(args = {}) { + async createTodoItem({ + projectId, todoListId, ...args + } = {}) { return this.makeRequest({ - $: args.$, - accountId: args.accountId, - path: `/buckets/${args.projectId}/todolists/${args.todoListId}/todos.json`, + path: `/buckets/${projectId}/todolists/${todoListId}/todos.json`, method: "post", ...args, }); }, - async createWebhook(args = {}) { + async createWebhook({ + projectId, ...args + } = {}) { return this.makeRequest({ - path: `/buckets/${args.projectId}/webhooks.json`, - accountId: args.accountId, + path: `/buckets/${projectId}/webhooks.json`, method: "post", ...args, }); }, - async deleteWebhook(args = {}) { + async deleteWebhook({ + projectId, webhookId, ...args + } = {}) { return this.makeRequest({ - path: `/buckets/${args.projectId}/webhooks/${args.webhookId}.json`, - accountId: args.accountId, + path: `/buckets/${projectId}/webhooks/${webhookId}.json`, method: "delete", ...args, }); }, getCardTable({ - accountId, projectId, cardTableId, ...args + projectId, cardTableId, ...args }) { return this.makeRequest({ path: `/buckets/${projectId}/card_tables/${cardTableId}.json`, - accountId, ...args, }); }, - async getColumns({ + async getCardTables({ accountId, projectId, }) { const { dock } = await this.getProject({ accountId, projectId, }); - const cardTable = dock.find(({ name }) => name === "kanban_board"); - const { lists } = await this.getCardTable({ - accountId, - projectId, - cardTableId: cardTable.id, - }); + const cardTables = dock.filter(({ name }) => name === "kanban_board"); + return cardTables; + }, + async getColumns(args) { + const { lists } = await this.getCardTable(args); return lists; }, + async getCards({ + projectId, columnId, ...args + }) { + return this.makeRequest({ + path: `/buckets/${projectId}/card_tables/lists/${columnId}/cards.json`, + ...args, + }); + }, + getChatbot({ + projectId, campfireId, botId, ...args + }) { + return this.makeRequest({ + path: `/buckets/${projectId}/chats/${campfireId}/integrations/${botId}.json`, + ...args, + }); + }, + listChatbots({ + projectId, campfireId, ...args + }) { + return this.makeRequest({ + path: `/buckets/${projectId}/chats/${campfireId}/integrations.json`, + ...args, + }); + }, }, }; diff --git a/components/basecamp/common/common.mjs b/components/basecamp/common/common.mjs new file mode 100644 index 0000000000000..9f085539225bd --- /dev/null +++ b/components/basecamp/common/common.mjs @@ -0,0 +1,22 @@ +import app from "../basecamp.app.mjs"; + +export default { + props: { + app, + accountId: { + propDefinition: [ + app, + "accountId", + ], + }, + projectId: { + propDefinition: [ + app, + "projectId", + ({ accountId }) => ({ + accountId, + }), + ], + }, + }, +}; diff --git a/components/basecamp/package.json b/components/basecamp/package.json index 5fec3cc3ae406..8a26422c19622 100644 --- a/components/basecamp/package.json +++ b/components/basecamp/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/basecamp", - "version": "0.1.2", + "version": "0.2.0", "description": "Pipedream Basecamp Components", "main": "basecamp.app.js", "keywords": [ @@ -14,6 +14,6 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.2.1" + "@pipedream/platform": "^3.0.3" } } diff --git a/components/basecamp/sources/common/base.mjs b/components/basecamp/sources/common/base.mjs index 32198290098a8..d918b070a45f1 100644 --- a/components/basecamp/sources/common/base.mjs +++ b/components/basecamp/sources/common/base.mjs @@ -1,24 +1,9 @@ -import app from "../../basecamp.app.mjs"; +import common from "../../common/common.mjs"; export default { props: { - app, + ...common.props, db: "$.service.db", - accountId: { - propDefinition: [ - app, - "accountId", - ], - }, - projectId: { - propDefinition: [ - app, - "projectId", - ({ accountId }) => ({ - accountId, - }), - ], - }, }, methods: { getWebhookTypes() { diff --git a/components/basecamp/sources/new-comment-created/new-comment-created.mjs b/components/basecamp/sources/new-comment-created/new-comment-created.mjs index cfaf741eba285..f08766258c687 100644 --- a/components/basecamp/sources/new-comment-created/new-comment-created.mjs +++ b/components/basecamp/sources/new-comment-created/new-comment-created.mjs @@ -4,8 +4,8 @@ export default { ...common, key: "basecamp-new-comment-created", name: "New Comment Created (Instant)", - description: "Emit new event when a comment is created. [See the docs here](https://github.com/basecamp/bc3-api/blob/master/sections/webhooks.md#create-a-webhook)", - version: "0.0.4", + description: "Emit new event when a comment is created. [See the documentation](https://github.com/basecamp/bc3-api/blob/master/sections/webhooks.md#webhooks)", + version: "0.0.8", dedupe: "unique", type: "source", methods: { diff --git a/components/basecamp/sources/new-event-by-webhook-type/new-event-by-webhook-type.mjs b/components/basecamp/sources/new-event-by-webhook-type/new-event-by-webhook-type.mjs index 4c717feccacba..051152ac4862f 100644 --- a/components/basecamp/sources/new-event-by-webhook-type/new-event-by-webhook-type.mjs +++ b/components/basecamp/sources/new-event-by-webhook-type/new-event-by-webhook-type.mjs @@ -1,20 +1,20 @@ -import common from "../common/webhook.mjs"; import constants from "../common/constants.mjs"; +import common from "../common/webhook.mjs"; export default { ...common, key: "basecamp-new-event-by-webhook-type", - name: "New Event By Webhook Type (Instant)", - description: "Emit new event based on the selected webhook type. [See the docs here](https://github.com/basecamp/bc3-api/blob/master/sections/webhooks.md#create-a-webhook)", - version: "0.0.4", + name: "New Webhook Event (Instant)", + description: "Emit events of one or more selected types. [See the documentation](https://github.com/basecamp/bc3-api/blob/master/sections/webhooks.md#webhooks)", + version: "0.0.8", dedupe: "unique", type: "source", props: { ...common.props, webhookTypes: { type: "string[]", - label: "Webhook Types", - description: "Select types of webhook for listening.", + label: "Event Types", + description: "Select the types of events to be emitted. [See the documentation](https://github.com/basecamp/bc3-api/blob/master/sections/webhooks.md#webhooks) for more information.", options: constants.WEBHOOK_TYPE_OPTS, }, }, diff --git a/components/basecamp/sources/new-message-created/new-message-created.mjs b/components/basecamp/sources/new-message-created/new-message-created.mjs index c80bc18ce2ab2..d3dbab19d20f3 100644 --- a/components/basecamp/sources/new-message-created/new-message-created.mjs +++ b/components/basecamp/sources/new-message-created/new-message-created.mjs @@ -4,8 +4,8 @@ export default { ...common, key: "basecamp-new-message-created", name: "New Message Created (Instant)", - description: "Emit new event when a message is created. [See the docs here](https://github.com/basecamp/bc3-api/blob/master/sections/webhooks.md#create-a-webhook)", - version: "0.0.4", + description: "Emit new event when a message is created. [See the documentation](https://github.com/basecamp/bc3-api/blob/master/sections/webhooks.md#webhooks)", + version: "0.0.8", dedupe: "unique", type: "source", methods: { diff --git a/components/basecamp/sources/new-to-do-item-created/new-to-do-item-created.mjs b/components/basecamp/sources/new-to-do-item-created/new-to-do-item-created.mjs index 42ab6ed15727b..ad7d4c1754d99 100644 --- a/components/basecamp/sources/new-to-do-item-created/new-to-do-item-created.mjs +++ b/components/basecamp/sources/new-to-do-item-created/new-to-do-item-created.mjs @@ -4,8 +4,8 @@ export default { ...common, key: "basecamp-new-to-do-item-created", name: "New To-Do Item Created (Instant)", - description: "Emit new event when a to-do item is created. [See the docs here](https://github.com/basecamp/bc3-api/blob/master/sections/webhooks.md#create-a-webhook)", - version: "0.0.4", + description: "Emit new event when a to-do item is created. [See the documentation](https://github.com/basecamp/bc3-api/blob/master/sections/webhooks.md#webhooks)", + version: "0.0.8", dedupe: "unique", type: "source", methods: { diff --git a/components/basecamp/sources/new-to-do-item-status/new-to-do-item-status.mjs b/components/basecamp/sources/new-to-do-item-status/new-to-do-item-status.mjs index 1599f724d102b..390bb291f0b75 100644 --- a/components/basecamp/sources/new-to-do-item-status/new-to-do-item-status.mjs +++ b/components/basecamp/sources/new-to-do-item-status/new-to-do-item-status.mjs @@ -3,9 +3,9 @@ import common from "../common/webhook.mjs"; export default { ...common, key: "basecamp-new-to-do-item-status", - name: "New To-Do Item Status (Instant)", - description: "Emit new event when a to-do item status changes. [See the docs here](https://github.com/basecamp/bc3-api/blob/master/sections/webhooks.md#create-a-webhook)", - version: "0.0.4", + name: "To-Do Item Completed Or Uncompleted (Instant)", + description: "Emit new event when a to-do item's status changes. [See the documentation](https://github.com/basecamp/bc3-api/blob/master/sections/webhooks.md#webhooks)", + version: "0.0.8", dedupe: "unique", type: "source", methods: { diff --git a/components/basecamp/sources/new-to-do-list-created/new-to-do-list-created.mjs b/components/basecamp/sources/new-to-do-list-created/new-to-do-list-created.mjs index b09708f01aea7..2658c79e8d54a 100644 --- a/components/basecamp/sources/new-to-do-list-created/new-to-do-list-created.mjs +++ b/components/basecamp/sources/new-to-do-list-created/new-to-do-list-created.mjs @@ -4,8 +4,8 @@ export default { ...common, key: "basecamp-new-to-do-list-created", name: "New To-Do List Created (Instant)", - description: "Emit new event when a to-do list is created. [See the docs here](https://github.com/basecamp/bc3-api/blob/master/sections/webhooks.md#create-a-webhook)", - version: "0.0.4", + description: "Emit new event when a to-do list is created. [See the documentation](https://github.com/basecamp/bc3-api/blob/master/sections/webhooks.md#webhooks)", + version: "0.0.8", dedupe: "unique", type: "source", methods: { diff --git a/components/baselinker/README.md b/components/baselinker/README.md new file mode 100644 index 0000000000000..1f39cca52d997 --- /dev/null +++ b/components/baselinker/README.md @@ -0,0 +1,11 @@ +# Overview + +The BaseLinker API offers access to a suite of e-commerce management tools, enabling seamless integration of orders, products, and inventory across various online sales channels. With Pipedream's ability to connect APIs, you can automate tasks between BaseLinker and other apps to streamline your e-commerce operations, from syncing inventory to processing orders. + +# Example Use Cases + +- **Automated Order Processing**: When a new order comes in via BaseLinker, automatically notify your team on Slack, then generate an invoice in your accounting software like QuickBooks. + +- **Multi-channel Inventory Sync**: After a sale is made on one platform, use Pipedream to update stock levels across other platforms like eBay, Amazon, or Shopify to prevent overselling. + +- **Customer Follow-Up Sequences**: Trigger an email sequence in Mailchimp or SendGrid when a new order is marked as shipped in BaseLinker, providing customers with tracking info and asking for feedback. diff --git a/components/baserow/README.md b/components/baserow/README.md index ddbf335ae0870..cabf9d851fe89 100644 --- a/components/baserow/README.md +++ b/components/baserow/README.md @@ -1,11 +1,11 @@ # Overview -With Baserow, you can easily build custom software for managing data. Examples include: - -- Custom CRM systems -- Sales pipelines -- Project management tools -- Inventory management systems -- HR management systems -- Time tracking tools -- And more! +The Baserow API allows you to interact programmatically with Baserow, an open-source no-code database tool. With its API, you can automate database operations such as creating, reading, updating, and deleting records. Moreover, it's possible to manage databases, tables, fields, and rows, or integrate with external services to enrich or move data in real-time. Leveraging Pipedream’s capabilities, you can trigger workflows from a variety of events and connect Baserow to numerous apps for seamless data processes. + +# Example Use Cases + +- **Real-time Lead Capture to CRM**: Capture leads in real-time from a landing page form and feed them directly into a Baserow database. Use Pipedream to trigger a workflow that enriches these leads with additional data points from external APIs (like Clearbit for business intelligence) and then syncs them to a CRM like Salesforce, ensuring your sales team always has the most up-to-date lead information. + +- **Automated Support Ticket Triage**: Automatically create support tickets in Baserow based on incoming customer support emails or chat messages from platforms like Zendesk. Use Pipedream to analyze the sentiment of the inquiry with a tool like the IBM Watson API and prioritize or categorize the ticket accordingly, streamlining your customer service operations and response times. + +- **Inventory Management Sync**: Synchronize your inventory management by updating Baserow records each time a sale is made on an e-commerce platform like Shopify. Set up a Pipedream workflow that listens for new order webhooks from Shopify, updates the inventory count in Baserow, and if the stock falls below a threshold, triggers reorder notifications to suppliers via email or SMS, keeping inventory levels optimal with minimal manual intervention. diff --git a/components/baserow/actions/create-row/create-row.ts b/components/baserow/actions/create-row/create-row.ts index a4ae950ee2ced..a974e0a54e976 100644 --- a/components/baserow/actions/create-row/create-row.ts +++ b/components/baserow/actions/create-row/create-row.ts @@ -11,7 +11,7 @@ export default defineAction({ name: "Create Row", description: `Create a row [See docs here](${DOCS_LINK})`, key: "baserow-create-row", - version: "0.0.2", + version: "0.0.3", type: "action", props: { ...common.props, diff --git a/components/baserow/actions/delete-row/delete-row.ts b/components/baserow/actions/delete-row/delete-row.ts index 5ccfe68a284d9..ae1f6987f44c8 100644 --- a/components/baserow/actions/delete-row/delete-row.ts +++ b/components/baserow/actions/delete-row/delete-row.ts @@ -10,7 +10,7 @@ export default defineAction({ description: `Delete a row [See docs here](${DOCS_LINK})`, key: "baserow-delete-row", - version: "0.0.2", + version: "0.0.3", type: "action", props: { ...common.props, diff --git a/components/baserow/actions/get-row/get-row.ts b/components/baserow/actions/get-row/get-row.ts index 2677fd1308ebf..86430c19eb29c 100644 --- a/components/baserow/actions/get-row/get-row.ts +++ b/components/baserow/actions/get-row/get-row.ts @@ -12,7 +12,7 @@ export default defineAction({ description: `Get a single row [See docs here](${DOCS_LINK})`, key: "baserow-get-row", - version: "0.0.2", + version: "0.0.3", type: "action", props: { ...common.props, diff --git a/components/baserow/actions/list-rows/list-rows.ts b/components/baserow/actions/list-rows/list-rows.ts index 9f4b984a5b695..63fc29d9d70f7 100644 --- a/components/baserow/actions/list-rows/list-rows.ts +++ b/components/baserow/actions/list-rows/list-rows.ts @@ -11,7 +11,7 @@ export default defineAction({ description: `List a table's rows [See docs here](${DOCS_LINK})`, key: "baserow-list-rows", - version: "0.0.2", + version: "0.0.3", type: "action", async run({ $ }) { const { tableId } = this; diff --git a/components/baserow/actions/update-row/update-row.ts b/components/baserow/actions/update-row/update-row.ts index 23e524909cb20..93377ed4e49c1 100644 --- a/components/baserow/actions/update-row/update-row.ts +++ b/components/baserow/actions/update-row/update-row.ts @@ -11,7 +11,7 @@ export default defineAction({ name: "Update Row", description: `Update a row [See docs here](${DOCS_LINK})`, key: "baserow-update-row", - version: "0.0.2", + version: "0.0.3", type: "action", props: { ...common.props, diff --git a/components/baserow/common/constants.ts b/components/baserow/common/constants.ts index e1485c816cfd5..bc1005f6a5159 100644 --- a/components/baserow/common/constants.ts +++ b/components/baserow/common/constants.ts @@ -2,3 +2,9 @@ const DOCS_LINK = "https://baserow.io/api-docs"; export { DOCS_LINK, }; + +export enum EventType { + Create = "New Row Created", + Update = "Row Updated", + Delete = "Row Deleted", +} diff --git a/components/baserow/package.json b/components/baserow/package.json index 027ae7ae6560a..9d6ac906e2226 100644 --- a/components/baserow/package.json +++ b/components/baserow/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/baserow", - "version": "0.0.4", + "version": "0.1.0", "description": "Pipedream Baserow Components", "main": "dist/app/baserow.app.mjs", "keywords": [ diff --git a/components/baserow/sources/new-or-updated-row/new-or-updated-row.ts b/components/baserow/sources/new-or-updated-row/new-or-updated-row.ts new file mode 100644 index 0000000000000..81120bc6c4197 --- /dev/null +++ b/components/baserow/sources/new-or-updated-row/new-or-updated-row.ts @@ -0,0 +1,135 @@ +import { defineSource } from "@pipedream/types"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import common from "../../actions/common"; +import { Row } from "../../common/types"; +import { EventType } from "../../common/constants"; + +export default defineSource({ + ...common, + key: "baserow-new-or-updated-row", + name: "New or Updated Row", + description: + "Emit new event when a table row is created, updated or deleted, according to the selected event types", + version: "0.0.1", + type: "source", + props: { + ...common.props, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + eventTypes: { + type: "string[]", + label: "Event Types", + description: + "Which events to emit. If not specified, all events will be emitted.", + optional: true, + options: [ + EventType.Create, + EventType.Update, + EventType.Delete, + ], + }, + }, + hooks: { + async deploy() { + await this.getAndProcessData(); + }, + }, + methods: { + _getTableId(): string { + return this.db.get("tableId"); + }, + _setTableId(value: string) { + this.db.set("tableId", value); + }, + _getSavedData(): string[] { + return JSON.parse(this.db.get("savedData") ?? "{}"); + }, + _setSavedData(data: string[]) { + this.db.set("savedData", JSON.stringify(data)); + }, + async getAndProcessData() { + const { + baserow, tableId, eventTypes, + } = this; + const savedTableId = this._getTableId(); + const data: Row[] = await baserow.listRows({ + tableId, + }); + if (!data) { + console.log("No data received: ", data); + return; + } + + const firstRun = savedTableId !== tableId; + if (firstRun) { + this._setTableId(tableId); + } else { + const events: EventType[] = eventTypes?.length + ? eventTypes + : Object.values(EventType); + + const savedData = this._getSavedData(); + + const emitCreate = events.includes(EventType.Create); + const emitUpdate = events.includes(EventType.Update); + const emitDelete = events.includes(EventType.Delete); + + if (emitCreate || emitUpdate) { + data.forEach((row) => { + const savedRow = savedData[row.id]; + if (emitCreate && !savedRow) { + this.emitEvent(EventType.Create, row); + } else if ( + emitUpdate && + JSON.stringify(savedRow) !== JSON.stringify(row) + ) { + this.emitEvent(EventType.Update, row); + } + }); + } + + if (emitDelete) { + Object.entries(savedData) + .filter(([ + id, + ]) => { + const numId = Number(id); + return !data.some((row) => row.id === numId); + }) + .forEach(([ + , savedRow, + ]) => { + this.emitEvent(EventType.Delete, savedRow); + }); + } + } + + this._setSavedData( + data.reduce((acc, row) => { + acc[row.id] = row; + return acc; + }, {}), + ); + }, + emitEvent(eventType: EventType, rowData: Row) { + const ts = Date.now(); + const { id } = rowData; + this.$emit({ + eventType, + rowData, + }, { + id, + summary: `${eventType}: ${id}`, + ts, + }); + }, + }, + async run() { + await this.getAndProcessData(); + }, +}); diff --git a/components/bash/package.json b/components/bash/package.json new file mode 100644 index 0000000000000..d1c9c1f796cd7 --- /dev/null +++ b/components/bash/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/bash", + "version": "0.6.0", + "description": "Pipedream bash Components", + "main": "bash.app.mjs", + "keywords": [ + "pipedream", + "bash" + ], + "homepage": "https://pipedream.com/apps/bash", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/basin/README.md b/components/basin/README.md index 50df5abec5363..007a9bbf938f5 100644 --- a/components/basin/README.md +++ b/components/basin/README.md @@ -1,10 +1,11 @@ # Overview -Basin helps you build custom forms for your web projects remarkably fast, without the massive engineering lift. It works out of the box with all your favorite tools and frameworks with no dependencies, special libraries, or extra configuration needed. Simply point your forms to Basin and it will: +The Basin API provides a powerful platform for automating form submissions, gathering feedback, and conducting surveys with ease. By leveraging the Pipedream's serverless execution model, you can trigger workflows in response to new form submissions, process and analyze the data, and take action in countless other apps. Whether you're routing form data to a CRM, generating support tickets, or synthesizing customer insights, Basin on Pipedream simplifies these tasks with its event-driven architecture and seamless integrations. -- Collect and store submission records -- Block spam -- Send email notifications -- Trigger downstream integrations with connected apps (like Pipedream) +# Example Use Cases -Basin saves you the time and hassle of building, monitoring, and maintaining your own form backend or API. +- **Automated Customer Feedback Analysis**: When a new form submission occurs in Basin, trigger a Pipedream workflow that sends the data to a sentiment analysis service like MonkeyLearn. The analyzed sentiment can then be logged to a Google Sheet for aggregate reporting, providing real-time insights into customer satisfaction. + +- **Support Ticket Creation**: Use Basin to capture support requests and auto-generate tickets in a tool like Zendesk or Jira using Pipedream workflows. Each form submission can create a new ticket, with the details from the form mapped to the corresponding fields in the ticketing system, streamlining the support process. + +- **Email Campaign Enrollment**: For each new sign-up through a Basin form, trigger a Pipedream workflow to add the contact to an email marketing list in Mailchimp. The workflow can further segment the contacts based on the responses, enabling targeted email campaigns that resonate with specific audience segments. diff --git a/components/battle_net/battle_net.app.mjs b/components/battle_net/battle_net.app.mjs new file mode 100644 index 0000000000000..f995831a19096 --- /dev/null +++ b/components/battle_net/battle_net.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "battle_net", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/battle_net/package.json b/components/battle_net/package.json new file mode 100644 index 0000000000000..b11502806205d --- /dev/null +++ b/components/battle_net/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/battle_net", + "version": "0.0.1", + "description": "Pipedream Blizzard Battle.net Components", + "main": "battle_net.app.mjs", + "keywords": [ + "pipedream", + "battle_net" + ], + "homepage": "https://pipedream.com/apps/battle_net", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/bc_gov_news/README.md b/components/bc_gov_news/README.md index 5f0f32b11d460..ddeab88b2c9fd 100644 --- a/components/bc_gov_news/README.md +++ b/components/bc_gov_news/README.md @@ -1,10 +1,11 @@ # Overview -The BC Gov News API is a powerful tool that allows developers to access a wealth of information about the news and events happening in British Columbia. With this API, developers can create a variety of applications and websites that provide users with up-to-date information on what's happening in the province. Here are a few examples of what you can build using the BC Gov News API: - -- A news website that aggregates stories from across the province -- A calendar of events happening in BC -- A map of government offices and facilities in the province -- A directory of government services and information -- A searchable database of government news and events -- A mobile app that provides users with push notifications about breaking news in BC +The BC Gov News API provides programmatic access to government news releases, stories, and resources in British Columbia. Leveraging this API on Pipedream allows for the automation of monitoring, distributing, and analyzing news content from the BC government. Developers can create workflows to keep track of the latest updates in various sectors, filter news based on specific interests or keywords, and integrate with other services to disseminate information efficiently. + +# Example Use Cases + +- **Content Distribution Workflow**: Trigger a Pipedream workflow with new BC Gov News releases. Use filters to select news within certain categories, then automatically format and send these updates via email to subscribers, post to company Slack channels, or distribute through social media platforms like Twitter or LinkedIn. + +- **Keyword Alert System**: Set up a workflow that scans new BC Gov News articles for specified keywords or phrases. When a match is found, the workflow can trigger an alert, sending notifications to interested parties through SMS (using Twilio), or pushing a message to a Discord server, ensuring rapid dissemination of critical information. + +- **Data Analysis and Reporting**: Collect data from the BC Gov News API and feed it into a data analytics platform like Google Sheets or a database. Use Pipedream's scheduled tasks to process this data regularly, generating reports or visualizations that can provide insights into government news trends, sentiment analysis, or the frequency of topic mentions for research and strategic planning purposes. diff --git a/components/beaconchain/actions/get-epoch/get-epoch.mjs b/components/beaconchain/actions/get-epoch/get-epoch.mjs new file mode 100644 index 0000000000000..fb75c4fe52e13 --- /dev/null +++ b/components/beaconchain/actions/get-epoch/get-epoch.mjs @@ -0,0 +1,42 @@ +import app from "../../beaconchain.app.mjs"; + +export default { + key: "beaconchain-get-epoch", + name: "Get Epoch", + description: "Returns information for a specified epoch by the epoch number or an epoch tag (can be latest or finalized). [See the documentation](https://beaconcha.in/api/v1/docs/index.html#/Epoch/get_api_v1_epoch__epoch_)", + version: "0.0.1", + type: "action", + props: { + app, + epoch: { + propDefinition: [ + app, + "epoch", + ], + }, + }, + methods: { + getEpoch({ + epoch, ...args + } = {}) { + return this.app._makeRequest({ + path: `/epoch/${epoch}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + getEpoch, + epoch, + } = this; + + const response = await getEpoch({ + $, + epoch, + }); + + $.export("$summary", `Successfully retrieved epoch \`${response.data.epoch}\`.`); + return response; + }, +}; diff --git a/components/beaconchain/actions/get-execution-blocks/get-execution-blocks.mjs b/components/beaconchain/actions/get-execution-blocks/get-execution-blocks.mjs new file mode 100644 index 0000000000000..849ab5104a46e --- /dev/null +++ b/components/beaconchain/actions/get-execution-blocks/get-execution-blocks.mjs @@ -0,0 +1,43 @@ +import app from "../../beaconchain.app.mjs"; + +export default { + key: "beaconchain-get-execution-blocks", + name: "Get Execution Blocks", + description: "Retrieve execution blocks by execution block number. [See the documentation](https://beaconcha.in/api/v1/docs/index.html#/Execution/get_api_v1_execution_block__blockNumber_)", + version: "0.0.1", + type: "action", + props: { + app, + blockNumbers: { + type: "string[]", + label: "Block Number", + description: "Enter one or more execution block numbers, up to a maximum of 100.", + }, + }, + methods: { + getExecutionBlocks({ + blockNumber, ...args + } = {}) { + return this.app._makeRequest({ + path: `/execution/block/${blockNumber}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + getExecutionBlocks, + blockNumbers, + } = this; + + const response = await getExecutionBlocks({ + $, + blockNumber: Array.isArray(blockNumbers) + ? blockNumbers.map((value) => value.trim()).join(",") + : blockNumbers, + }); + + $.export("$summary", "Successfully retrieved execution blocks."); + return response; + }, +}; diff --git a/components/beaconchain/actions/get-slots/get-slots.mjs b/components/beaconchain/actions/get-slots/get-slots.mjs new file mode 100644 index 0000000000000..59eb8a851554d --- /dev/null +++ b/components/beaconchain/actions/get-slots/get-slots.mjs @@ -0,0 +1,43 @@ +import app from "../../beaconchain.app.mjs"; + +export default { + key: "beaconchain-get-slots", + name: "Get Slots", + description: "Returns all slots for a specified epoch. [See the documentation](https://beaconcha.in/api/v1/docs/index.html#/Epoch/get_api_v1_epoch__epoch__slots)", + version: "0.0.1", + type: "action", + props: { + app, + epoch: { + description: "Returns all slots for a specified epoch.", + propDefinition: [ + app, + "epoch", + ], + }, + }, + methods: { + getEpochSlots({ + epoch, ...args + } = {}) { + return this.app._makeRequest({ + path: `/epoch/${epoch}/slots`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + getEpochSlots, + epoch, + } = this; + + const response = await getEpochSlots({ + $, + epoch, + }); + + $.export("$summary", `Successfully retrieved \`${response.data.length}\` slot(s).`); + return response; + }, +}; diff --git a/components/beaconchain/actions/get-validators/get-validators.mjs b/components/beaconchain/actions/get-validators/get-validators.mjs new file mode 100644 index 0000000000000..a9eb627e7bcc0 --- /dev/null +++ b/components/beaconchain/actions/get-validators/get-validators.mjs @@ -0,0 +1,43 @@ +import app from "../../beaconchain.app.mjs"; + +export default { + key: "beaconchain-get-validators", + name: "Get Validators", + description: "Returns information for all validators up to 100 by index or public key. [See the documentation](https://beaconcha.in/api/v1/docs/index.html#/Validator/get_api_v1_validator__indexOrPubkey_).", + version: "0.0.1", + type: "action", + props: { + app, + indexesOrPubkeys: { + type: "string[]", + label: "Validator Indexes Or Public Keys", + description: "Enter up to 100 validator indices or public keys.", + }, + }, + methods: { + getValidators({ + indexOrPubkey, ...args + } = {}) { + return this.app._makeRequest({ + path: `/validator/${indexOrPubkey}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + getValidators, + indexesOrPubkeys, + } = this; + + const response = await getValidators({ + $, + indexOrPubkey: Array.isArray(indexesOrPubkeys) + ? indexesOrPubkeys.map((value) => value.trim()).join(",") + : indexesOrPubkeys, + }); + + $.export("$summary", "Successfully retrieved validators."); + return response; + }, +}; diff --git a/components/beaconchain/beaconchain.app.mjs b/components/beaconchain/beaconchain.app.mjs new file mode 100644 index 0000000000000..887f2e4ade733 --- /dev/null +++ b/components/beaconchain/beaconchain.app.mjs @@ -0,0 +1,40 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "beaconchain", + propDefinitions: { + epoch: { + type: "string", + label: "Epoch", + description: "Epoch number, the string `latest` or the string `finalized`.", + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "apikey": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + }, +}; diff --git a/components/beaconchain/common/constants.mjs b/components/beaconchain/common/constants.mjs new file mode 100644 index 0000000000000..3fc051957d671 --- /dev/null +++ b/components/beaconchain/common/constants.mjs @@ -0,0 +1,7 @@ +const BASE_URL = "https://beaconcha.in"; +const VERSION_PATH = "/api/v1"; + +export default { + BASE_URL, + VERSION_PATH, +}; diff --git a/components/beaconchain/package.json b/components/beaconchain/package.json new file mode 100644 index 0000000000000..b15f4a742e4ca --- /dev/null +++ b/components/beaconchain/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/beaconchain", + "version": "0.1.0", + "description": "Pipedream Beaconchain Components", + "main": "beaconchain.app.mjs", + "keywords": [ + "pipedream", + "beaconchain" + ], + "homepage": "https://pipedream.com/apps/beaconchain", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" + } +} diff --git a/components/beaconstac/README.md b/components/beaconstac/README.md index b51a7030eac2e..8f402b3b0f302 100644 --- a/components/beaconstac/README.md +++ b/components/beaconstac/README.md @@ -1,10 +1,11 @@ # Overview -Beaconstac API can be used to develop a range of BLE-enabled applications for Android and iOS. The API can be used to build apps for beacon management, indoor navigation, targeted content delivery, and proximity-based marketing. +The Beaconstac API allows you to leverage proximity marketing through beacon and QR code technology. By integrating with Beaconstac, you can automate the creation and management of QR codes and beacons, track analytics, and engage users based on location. This aligns perfectly with Pipedream's capabilities, where you can create complex workflows to connect Beaconstac with other apps for efficient, real-time automation. -Some examples of applications that can be built using the Beaconstac API include: +# Example Use Cases -- A beacon management app that can be used to configure and manage beacons -- An indoor navigation app that can be used to provide turn-by-turn directions within a building -- A targeted content delivery app that can be used to deliver content to specific users based on their location -- A proximity-based marketing app that can be used to send targeted marketing messages to users based on their proximity to a beacon +- **Lead Capture at Events**: Automate the process of capturing leads at events by setting up beacons that integrate with a CRM like Salesforce. When attendees scan a QR code or interact with a beacon, Pipedream can add their information directly to Salesforce and trigger a personalized follow-up email via SendGrid. + +- **Location-Based Notifications**: Create workflows that send personalized notifications or offers to users when they are near a beacon. Connect Beaconstac to a messaging app like Twilio on Pipedream. When a user enters a beacon's range, trigger an SMS with a special discount or information about nearby products. + +- **Analytics Dashboard Synching**: Synch beacon and QR code interaction data to a dashboard application like Google Sheets or Data Studio. Set up a Pipedream workflow that listens for Beaconstac webhook events and processes the data to maintain real-time analytics on user interactions, foot traffic, or campaign performance. diff --git a/components/beamer/README.md b/components/beamer/README.md index ebe57a559c73b..41252e4f50642 100644 --- a/components/beamer/README.md +++ b/components/beamer/README.md @@ -1,12 +1,11 @@ # Overview -What is Beamer? +The Beamer API enables you to automate client communication and project management tasks. With Beamer, you can create notifications, announcements, and alerts that keep your team and clients updated on project status, milestones, and deadlines. When integrated with Pipedream, Beamer unleashes the power of serverless workflows to connect with myriad apps, allowing for sophisticated automations that can streamline communications, synchronize updates across platforms, and trigger actions based on specific events or conditions. -Beamer is a powerful and flexible LaTeX class for creating great looking presentations. With Beamer, you can create slideshows, presentations, and even handouts for your lectures. Beamer makes it easy to create beautiful slides, with or without frames, lists, and tables. Beamer is also very easy to use with other LaTeX packages, such as graphics and color. +# Example Use Cases -Here are some examples of what you can build using the Beamer API: +- **Automated Project Updates Broadcast**: When a project status changes in your project management tool, such as Asana or Trello, a workflow can automatically trigger Beamer to send an update to a designated channel. This keeps all stakeholders in the loop without manual intervention. -- A presentation for your next conference -- A handout for your next lecture -- A slideshow for your next family vacation -- A table for your next research paper +- **Client Feedback Collection and Notification**: After completing a milestone, a Pipedream workflow can prompt Beamer to request client feedback. When feedback is received, it's automatically posted to a Slack channel or emailed to the project manager, ensuring real-time responses and client engagement. + +- **Scheduled Announcement Digests**: Compile weekly or monthly digests of all announcements and updates from various platforms like GitHub or JIRA, and distribute them through Beamer. This Pipedream workflow ensures that your team and clients receive a comprehensive, scheduled report of all relevant updates, keeping everyone aligned and informed. diff --git a/components/beamer/package.json b/components/beamer/package.json new file mode 100644 index 0000000000000..20c14b49e5ae8 --- /dev/null +++ b/components/beamer/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/beamer", + "version": "0.6.0", + "description": "Pipedream beamer Components", + "main": "beamer.app.mjs", + "keywords": [ + "pipedream", + "beamer" + ], + "homepage": "https://pipedream.com/apps/beamer", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/beanstalkapp/README.md b/components/beanstalkapp/README.md new file mode 100644 index 0000000000000..f9f6b8105647f --- /dev/null +++ b/components/beanstalkapp/README.md @@ -0,0 +1,11 @@ +# Overview + +The Beanstalk API allows for streamlined version control and release management within your development workflow. By leveraging the API with Pipedream, you can automate interactions with your repositories, changesets, and deployment environments. You can create workflows that react to code commits, manage deploy environments, and integrate with other services for a more cohesive development lifecycle. + +# Example Use Cases + +- **Automated Code Review Notifications**: Set up a workflow where Pipedream listens for Beanstalk's webhook events on new commits. On detecting new code pushed to a repository, Pipedream can send a notification to a Slack channel or a Discord server, prompting team members to review the latest changes. + +- **Continuous Deployment Trigger**: Create a Pipedream workflow that triggers a deployment on Beanstalk when specific criteria are met, such as successful CI tests. Connect Beanstalk with a CI tool like CircleCI within Pipedream, and automatically deploy code to staging or production environments when the CI process passes. + +- **Sync Issues with Project Management Tools**: Implement a workflow in Pipedream that creates or updates tasks in project management tools such as Trello or Jira when issues are reported in Beanstalk commits or messages. This helps to keep track of bugs and feature requests directly from the commit messages or code reviews. diff --git a/components/beebole/beebole.app.mjs b/components/beebole/beebole.app.mjs index 9c771566743ca..db8a15bd52693 100644 --- a/components/beebole/beebole.app.mjs +++ b/components/beebole/beebole.app.mjs @@ -1,6 +1,7 @@ export default { type: "app", app: "beebole", + version: "0.0.1", propDefinitions: {}, methods: { // this.$auth contains connected account data @@ -8,4 +9,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/beebole/package.json b/components/beebole/package.json index 9396d01d56675..c16f96e7208f7 100644 --- a/components/beebole/package.json +++ b/components/beebole/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/beebole_app/README.md b/components/beebole_app/README.md new file mode 100644 index 0000000000000..528bf50c75323 --- /dev/null +++ b/components/beebole_app/README.md @@ -0,0 +1,11 @@ +# Overview + +The Beebole API provides a way to automate time tracking, manage projects, and handle employee data efficiently. With Pipedream, you can harness this functionality to create custom workflows that trigger on specific events, manipulate and analyze time tracking data, and integrate with other services for enhanced automation. For instance, you could sync time entries with project management tools, create reports for payroll, or set up alerts based on your team's time tracking patterns. + +# Example Use Cases + +- **Sync Time Entries with Project Management Tools**: Integrate Beebole with project management apps like Trello or Asana on Pipedream. When a new time entry is logged in Beebole, trigger a workflow to create a corresponding task or log time on an existing task in your project management tool. This ensures project time tracking is up-to-date across all platforms. + +- **Automated Reporting for Payroll**: Set up a workflow where Beebole time entries are collected periodically, summarized, and then sent to a payroll system like Gusto or a spreadsheet in Google Sheets. This can be done on a schedule (e.g., every payday) or triggered by a specific event, ensuring payroll reflects the most recent time tracking data. + +- **Alerts for Unusual Time Tracking Patterns**: Monitor time tracking entries in Beebole for patterns that might indicate overwork, such as entries exceeding a certain number of hours in a day. Trigger alerts using the Pipedream workflow which can notify a manager or the HR team through email or messaging platforms like Slack, helping to maintain healthy work-life balance and labor compliance. diff --git a/components/beebole_app/actions/list-companies/list-companies.mjs b/components/beebole_app/actions/list-companies/list-companies.mjs new file mode 100644 index 0000000000000..07d1b7c265af7 --- /dev/null +++ b/components/beebole_app/actions/list-companies/list-companies.mjs @@ -0,0 +1,24 @@ +import app from "../../beebole_app.app.mjs"; + +export default { + key: "beebole_app-list-companies", + name: "List Companies", + description: "List all companies in your Beebole account. [See the documentation](https://beebole.com/help/api/#list-companies)", + version: "0.0.3", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.apiRequest({ + $, + data: { + service: "company.list", + }, + }); + + $.export("$summary", `Successfully listed ${response.companies.length} companies`); + + return response; + }, +}; diff --git a/components/beebole_app/actions/list-untimed-employees/list-untimed-employees.mjs b/components/beebole_app/actions/list-untimed-employees/list-untimed-employees.mjs new file mode 100644 index 0000000000000..41a6ce05a6598 --- /dev/null +++ b/components/beebole_app/actions/list-untimed-employees/list-untimed-employees.mjs @@ -0,0 +1,59 @@ +import app from "../../beebole_app.app.mjs"; + +export default { + name: "List untimed employees", + description: "Get a list of employees without any time entry for a given period of time", + key: "beebole_app-list-untimed-employees", + version: "0.0.1", + type: "action", + props: { + app, + begda: { + propDefinition: [ + app, + "begda", + ], + }, + endda: { + propDefinition: [ + app, + "endda", + ], + }, + }, + async run({ $ }) { + let response, + people = []; + + try { + response = await this.app.apiRequest({ + $, + data: { + service: "time.get_people_timesheets", + undoc: true, + from: this.begda.split("-"), + to: this.endda.split("-"), + show: "all", + statusFilters: [ + "d", + ], + showAll: true, + details: [], + }, + }); + + $.export("$summary", `Successfully generated list for ${response.items[0].people.length} people`); + + response.items[0].people.forEach((element) => { + people.push({ + name: element.doc.name, + eid: element.eid, + draft: element.hours.sum, + }); + }); + return people; + } catch (errors) { + return errors; + } + }, +}; diff --git a/components/beebole_app/actions/run-report/run-report.mjs b/components/beebole_app/actions/run-report/run-report.mjs new file mode 100644 index 0000000000000..9b77738c9f0b1 --- /dev/null +++ b/components/beebole_app/actions/run-report/run-report.mjs @@ -0,0 +1,348 @@ +import app from "../../beebole_app.app.mjs"; + +export default { + name: "Run report", + description: "Run a report from Beebole", + key: "beebole_app-run-report", + version: "0.0.1", + type: "action", + props: { + app, + begda: { + propDefinition: [ + app, + "begda", + ], + }, + endda: { + propDefinition: [ + app, + "endda", + ], + }, + keys: { + type: "string[]", + label: "Columns", + description: "Select the columns to display in the report.", + options: [ + { + label: "Absence", + value: "absence", + }, + { + label: "Absence ID", + value: "absenceId", + }, + { + label: "Activity", + value: "activity", + }, + { + label: "Billing", + value: "billing", + }, + { + label: "Billing - %", + value: "billingPercent", + }, + { + label: "Billable - ratio", + value: "billingRatio", + }, + { + label: "Billable/Non-billable", + value: "billableSplit", + }, + { + label: "Billable Utilization %", + value: "billability", + }, + { + label: "Budget", + value: "budget", + }, + { + label: "Business days", + value: "workDays", + }, + { + label: "Business days remaining", + value: "remWorkDays", + }, + { + label: "Business hours", + value: "workHours", + }, + { + label: "Business hours remaining", + value: "remWorkHours", + }, + { + label: "Comment", + value: "comment", + }, + { + label: "Company", + value: "company", + }, + { + label: "Company ID", + value: "companyId", + }, + { + label: "Costs", + value: "cost", + }, + { + label: "Costs - %", + value: "costsPercent", + }, + { + label: "Date", + value: "date", + }, + { + label: "Daily Cost", + value: "dailyCost", + }, + { + label: "Daily Rate", + value: "dailyRate", + }, + { + label: "Days", + value: "inDays", + }, + { + label: "Event ID", + value: "eventId", + }, + { + label: "Hourly Cost", + value: "hourlyCost", + }, + { + label: "Hourly Rate", + value: "hourlyRate", + }, + { + label: "Hours", + value: "hours", + }, + { + label: "Hours - %", + value: "hoursPercent", + }, + { + label: "Is working", + value: "isAtWork", + }, + { + label: "Month", + value: "month", + }, + { + label: "Overtime / day", + value: "overtime", + }, + { + label: "Overtime / week", + value: "weeklyOvertime", + }, + { + label: "Person", + value: "person", + }, + { + label: "Person ID", + value: "personId", + }, + { + label: "Profit", + value: "profit", + }, + { + label: "Profit - %", + value: "profitPercent", + }, + { + label: "Profit - ratio", + value: "profitRatio", + }, + { + label: "Project", + value: "project", + }, + { + label: "Project ID", + value: "projectId", + }, + { + label: "Status", + value: "status", + }, + { + label: "Task", + value: "task", + }, + { + label: "Task ID", + value: "taskId", + }, + { + label: "Time End", + value: "etime", + }, + { + label: "Time Start", + value: "stime", + }, + { + label: "Sub Project", + value: "subproject", + }, + { + label: "Sub Project ID", + value: "subprojectId", + }, + { + label: "Week", + value: "week", + }, + ], + default: [ + "company", + "project", + "task", + "person", + "hours", + "comment", + ], + }, + statusFilters: { + type: "string[]", + label: "Status", + description: "Choose approval statuses", + options: [ + { + label: "Draft", + value: "d", + }, + { + label: "Submitted", + value: "s", + }, + { + label: "Rejected", + value: "r", + }, + { + label: "Approved", + value: "a", + }, + { + label: "Locked", + value: "l", + }, + ], + default: [ + "d", + "s", + "a", + "r", + "l", + ], + }, + groups: { + type: "string[]", + label: "Groups/Custom fields (optional)", + description: "Groups and Custom fields as columns", + default: [], + async options() { + const response = await this.app.apiRequest({ + data: { + services: [ + { + service: "group.tree", + }, + { + service: "custom_field.list", + }, + ], + }, + }); + let opts = [], + pushGroups = function(v, c, t) { + v.forEach((i) => { + let text = t + i.name; + if (i.groups) { + c.push({ + "value": "gid" + i.id, + "label": text, + }); + pushGroups(i.groups, c, text + ": "); + } + }); + }; + pushGroups(response[0].groups, opts, ""); + response[1].customFields.forEach((cf) => opts.push({ + "value": "gid" + cf.id, + "label": cf.name, + })); + + return opts; + }, + optional: true, + }, + groupFilters: { + type: "integer[]", + label: "Groups to filter results by (optional)", + description: "List of groups to filter results. Array of internal IDs i.e. [66]", + optional: true, + }, + }, + async run({ $ }) { + let jobResp, + response, + maxWait = 10, + srvData = { + service: "time_entry.export", + from: this.begda, + to: this.endda, + show: "all", + statusFilters: this.statusFilters, + outputFormat: "array", + }; + + srvData.keys = this.keys.concat(...this.groups); + if (this.groupFilters && this.groupFilters.length) + srvData.gids = JSON.parse(this.groupFilters); + + try { + jobResp = await this.app.apiRequest({ + $, + data: srvData, + }); + while (maxWait) { + response = await this.app.apiRequest({ + $, + data: { + service: "time_entry.get_job_info", + id: jobResp.job.id, + outputFormat: "array", + }, + }); + if (response.job.status.indexOf("running") > -1) + maxWait -= 1; + else { + $.export("$summary", `Successfully run report with ${response.job.result.length - 1} results`); + return response; + } + } + + return "Report is taking too long. Stopped. Define additional filters"; + } catch (errors) { + return errors; + } + }, +}; diff --git a/components/beebole_app/beebole_app.app.mjs b/components/beebole_app/beebole_app.app.mjs new file mode 100644 index 0000000000000..8e1975158e533 --- /dev/null +++ b/components/beebole_app/beebole_app.app.mjs @@ -0,0 +1,52 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "beebole_app", + propDefinitions: { + begda: { + type: "string", + label: "Start date", + description: "Start date in ISO format (YYYY-MM-DD)", + default: (new Date).toISOString() + .split("T")[0], + }, + endda: { + type: "string", + label: "End date", + description: "End date in ISO format (YYYY-MM-DD)", + default: (new Date).toISOString() + .split("T")[0], + }, + }, + methods: { + _baseUrl() { + return "https://beebole-apps.com/api/v2"; + }, + + async _makeRequest(opts = {}) { + const { + $ = this, + auth, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl(), + auth: { + ...auth, + username: `${this.$auth.api_token}`, + password: !otherOpts.data.undoc + ? "x" + : "true", + }, + }); + }, + async apiRequest(args = {}) { + return this._makeRequest({ + method: "post", + ...args, + }); + }, + }, +}; diff --git a/components/beebole_app/package.json b/components/beebole_app/package.json new file mode 100644 index 0000000000000..8f5bcbec544f9 --- /dev/null +++ b/components/beebole_app/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/beebole_app", + "version": "0.2.1", + "description": "Pipedream Beebole Components", + "main": "beebole_app.app.mjs", + "keywords": [ + "pipedream", + "beebole_app" + ], + "homepage": "https://pipedream.com/apps/beebole_app", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/beehiiv/README.md b/components/beehiiv/README.md index 50f782ae3dbf4..8eac0ae468703 100644 --- a/components/beehiiv/README.md +++ b/components/beehiiv/README.md @@ -1,10 +1,11 @@ # Overview -Using the Beehiiv API, you can build a variety of applications, including: - -- Social networking applications -- Messaging applications -- Location-based applications -- Gaming applications -- ECommerce applications -- And much more! +The Beehiiv API lets you seamlessly manage your email newsletter platform, enabling tasks such as subscriber management, campaign analytics, and content automation. With Beehiiv on Pipedream, you can connect your newsletter operations with other apps and services, automate routine tasks, and enhance your email marketing workflows with real-time data processing and event-driven triggers. + +# Example Use Cases + +- **Subscriber Sync Workflow**: Automatically sync new subscribers from a signup form on your website (handled by apps like Typeform or Wufoo) to your Beehiiv subscriber list. When a new form submission occurs, Pipedream captures the event and updates your Beehiiv subscriber list accordingly. + +- **Campaign Performance Monitor**: Get real-time alerts in Slack or Discord when your newsletter campaigns hit specific metrics, like open rates or click-through rates. Use Pipedream to watch for these events and then send a notification to your preferred messaging platform to keep your team informed on performance milestones. + +- **Content Automation for Newsletters**: Integrate RSS feeds or content from services like WordPress to automate the inclusion of latest blog posts or news items in your Beehiiv newsletters. When a new post goes live, Pipedream triggers a workflow that fetches the content and updates your upcoming campaign draft with the new material. diff --git a/components/beekeeper/README.md b/components/beekeeper/README.md index 2bc72a1de7bc3..8e464464f0d76 100644 --- a/components/beekeeper/README.md +++ b/components/beekeeper/README.md @@ -1,10 +1,11 @@ # Overview - Beekeeper's API enables developers to access Beekeeper's features and data, including users, posts, and comments. With the API, developers can create applications that display Beekeeper data or allow Beekeeper users to interact with their data. +The Beekeeper API enables seamless integration of its workplace communications platform with other business tools and systems, enhancing team collaboration and operational efficiency. By leveraging the Beekeeper API on Pipedream, you can automate the flow of information between Beekeeper and various applications, trigger actions based on messages or alerts, and synchronize data across your organization's tools. Pipedream's serverless architecture simplifies the process of setting up these automations without the need for dedicated infrastructure. -Some example applications that could be built using the Beekeeper API include: +# Example Use Cases -- A desktop application for viewing and interacting with Beekeeper data -- A mobile application for accessing Beekeeper on the go -- A web application for displaying Beekeeper data in a custom interface -- A plugin for another application that allows Beekeeper data to be used within that application +- **Broadcast Urgent Updates from Incident Management Tools:**: Automatically post critical alerts from incident management platforms like PagerDuty directly into Beekeeper streams. This keeps frontline workers informed in real-time about incidents that may impact their workflow or safety. + +- **Synchronize HR Information:**: Create a workflow that syncs new employee details from an HR system like BambooHR to Beekeeper, automatically creating accounts or updating user profiles. This ensures a smooth onboarding experience and keeps employee information consistent across platforms. + +- **Aggregate Feedback to Product Teams:**: Consolidate employee feedback or suggestions from Beekeeper posts and direct them to a tool like Jira. This helps product teams track and prioritize user feedback, fostering a culture of continuous improvement. diff --git a/components/beekeeper/package.json b/components/beekeeper/package.json new file mode 100644 index 0000000000000..00c5d06a9a1c4 --- /dev/null +++ b/components/beekeeper/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/beekeeper", + "version": "0.6.0", + "description": "Pipedream beekeeper Components", + "main": "beekeeper.app.mjs", + "keywords": [ + "pipedream", + "beekeeper" + ], + "homepage": "https://pipedream.com/apps/beekeeper", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/benchmark_email/README.md b/components/benchmark_email/README.md index 79a184e498bfa..d6e63f3f9f07c 100644 --- a/components/benchmark_email/README.md +++ b/components/benchmark_email/README.md @@ -1,10 +1,11 @@ # Overview -With the Benchmark Email API, you can: - -- Send one or more messages -- Get and update contact lists -- Add, update, and delete contacts -- Get message details -- Get message stats -- Add, update, and delete webhooks +Benchmark Email API lets you automate your email marketing efforts. You can manage subscribers, send emails, and track campaign results. With Pipedream's serverless platform, tapping into this API means you can create custom, automated workflows that respond to events in real time across various apps and services. + +# Example Use Cases + +- **Automated Welcome Email Series**: Trigger a series of welcome emails in Benchmark Email when a new user signs up on your platform. Use Pipedream to listen to signup webhooks from your app, then add the new user to a specific Benchmark Email list and kick off a welcome email sequence. + +- **Subscriber List Syncing**: Keep your CRM contacts and Benchmark Email subscriber lists in sync. Set up a Pipedream workflow to monitor changes in your CRM (like Salesforce or HubSpot), and automatically update subscriber lists in Benchmark Email whenever new contacts are added or existing contacts are updated. + +- **Survey Response Campaigns**: After a user completes a survey through a platform like Typeform, use Pipedream to capture the submission, analyze responses, and trigger a follow-up email campaign in Benchmark Email tailored to the user's feedback. diff --git a/components/benchmark_email/package.json b/components/benchmark_email/package.json new file mode 100644 index 0000000000000..42cf50895183a --- /dev/null +++ b/components/benchmark_email/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/benchmark_email", + "version": "0.6.0", + "description": "Pipedream benchmark_email Components", + "main": "benchmark_email.app.mjs", + "keywords": [ + "pipedream", + "benchmark_email" + ], + "homepage": "https://pipedream.com/apps/benchmark_email", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/benchmarkone/benchmarkone.app.mjs b/components/benchmarkone/benchmarkone.app.mjs new file mode 100644 index 0000000000000..6e9058b2fe7af --- /dev/null +++ b/components/benchmarkone/benchmarkone.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "benchmarkone", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/benchmarkone/package.json b/components/benchmarkone/package.json new file mode 100644 index 0000000000000..3c09a92341b72 --- /dev/null +++ b/components/benchmarkone/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/benchmarkone", + "version": "0.0.1", + "description": "Pipedream BenchmarkONE Components", + "main": "benchmarkone.app.mjs", + "keywords": [ + "pipedream", + "benchmarkone" + ], + "homepage": "https://pipedream.com/apps/benchmarkone", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/benzinga/README.md b/components/benzinga/README.md index 577fbdb32ceb6..a8c421f363943 100644 --- a/components/benzinga/README.md +++ b/components/benzinga/README.md @@ -1,9 +1,11 @@ # Overview -Using the Benzinga API, you can build applications that: +The Benzinga API provides access to a suite of financial market data including stock quotes, news, and analysis that could be potent for traders, financial analysts, and apps needing real-time information. With Pipedream’s serverless platform, you can build dynamic automations around this data. You might set up workflows that react to market changes, enrich CRM leads with financial insights, or automate news alerts for specific stock movements. -- Retrieve breaking news stories -- Get real-time stock quotes -- Get market summaries -- Look up company profiles -- And more! +# Example Use Cases + +- **Real-Time Stock Alert System**: Create a workflow that monitors stock prices via Benzinga API and sends SMS alerts using the Twilio app when certain thresholds are crossed. This could help traders act quickly on price movements. + +- **Automated News Digest for Portfolio Stocks**: Use Benzinga to fetch the latest news for a list of stocks in your portfolio and compile a daily digest email, automating this with Pipedream's built-in email service. Investors stay informed without manual news tracking. + +- **Dynamic Financial Data Enrichment for CRM**: Connect Benzinga API with a CRM platform like Salesforce. Automatically update contact records with the latest financial data for companies associated with each lead, giving sales teams an edge with up-to-date information. diff --git a/components/benzinga/package.json b/components/benzinga/package.json new file mode 100644 index 0000000000000..abfa619b1f5a5 --- /dev/null +++ b/components/benzinga/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/benzinga", + "version": "0.6.0", + "description": "Pipedream benzinga Components", + "main": "benzinga.app.mjs", + "keywords": [ + "pipedream", + "benzinga" + ], + "homepage": "https://pipedream.com/apps/benzinga", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/better_proposals/README.md b/components/better_proposals/README.md index fc6305419a8ed..e554a712f5bb1 100644 --- a/components/better_proposals/README.md +++ b/components/better_proposals/README.md @@ -1,11 +1,11 @@ # Overview -With the Better Proposals API, you can easily generate PDF proposals with your own branding, automatically calculate prices and taxes based on input from your customer, fetch data from your CRM, and much more. +The Better Proposals API offers a powerful way to automate proposal creation and management tasks. With it, you can programmatically generate, send, and track business proposals, streamlining the sales process and integrating seamlessly with CRM systems. By leveraging Pipedream’s capabilities, you can connect Better Proposals to a multitude of other apps to trigger actions based on proposal activities, sync data, or even automate follow-ups, thereby enhancing the efficiency of your sales workflow. -Here are some examples of what you can build with the Better Proposals API: +# Example Use Cases -- A proposal generation tool for your sales team -- An estimate calculator for your customers -- A document management system for your proposal process -- An integration with your CRM to automatically generate proposals -- A workflow tool to help you manage your proposal process +- **Automated Proposal Follow-ups**: After a proposal is sent using Better Proposals, you can trigger a workflow on Pipedream that waits for a specified period (e.g., two days) and then checks the proposal status. If it remains unviewed, the workflow could automatically send a reminder email to the prospect via an email platform like SendGrid or directly through your company's email server. + +- **CRM Integration for Proposal Status Updates**: Once a proposal's status changes (e.g., from sent to viewed or accepted), trigger a workflow that updates the deal stage in your CRM system, such as Salesforce or HubSpot. This ensures that sales teams have the latest information without manual data entry, maintaining an up-to-date sales pipeline automatically. + +- **Slack Notifications for Proposal Activities**: Configure a workflow that notifies a sales team in a specific Slack channel whenever a new proposal is sent or when there is a change in the proposal status. This real-time update system keeps everyone in the loop and can prompt immediate action when a proposal is accepted, speeding up the sales cycle. diff --git a/components/better_proposals/package.json b/components/better_proposals/package.json new file mode 100644 index 0000000000000..a4322e4b3047a --- /dev/null +++ b/components/better_proposals/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/better_proposals", + "version": "0.6.0", + "description": "Pipedream better_proposals Components", + "main": "better_proposals.app.mjs", + "keywords": [ + "pipedream", + "better_proposals" + ], + "homepage": "https://pipedream.com/apps/better_proposals", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/better_stack/README.md b/components/better_stack/README.md new file mode 100644 index 0000000000000..a9349488db34c --- /dev/null +++ b/components/better_stack/README.md @@ -0,0 +1,11 @@ +# Overview + +The Better Stack API allows you to enhance your app's capabilities with powerful monitoring, logging, and analytics tools. By connecting to the API through Pipedream, you can create custom, serverless workflows that automate the ingestion, analysis, and response to your app's operational data. Leverage real-time insights, set up alerts, and integrate with various other services to maintain optimal performance and swiftly troubleshoot issues. + +# Example Use Cases + +- **Incident Alerting to Communication Platforms**: When Better Stack detects an incident, you could use Pipedream to automatically post a detailed alert to Slack, Discord, or Microsoft Teams to notify your team instantly. This rapid communication can help in reducing the time to resolution. + +- **Automated Issue Tracking**: On encountering a specified error or threshold breach, trigger a workflow that creates a new issue in project management tools like Jira, Asana, or GitHub Issues. This ensures that your development team can track and address problems in a systematic manner. + +- **Performance Metrics Dashboard Update**: Set up a workflow that periodically fetches performance metrics from Better Stack and sends the data to a Google Sheets spreadsheet or a Grafana dashboard. This enables consistent monitoring and a historical view of your app's performance trends. diff --git a/components/better_stack/actions/acknowledge-incident/acknowledge-incident.mjs b/components/better_stack/actions/acknowledge-incident/acknowledge-incident.mjs new file mode 100644 index 0000000000000..92f39d54bb732 --- /dev/null +++ b/components/better_stack/actions/acknowledge-incident/acknowledge-incident.mjs @@ -0,0 +1,52 @@ +import app from "../../better_stack.app.mjs"; + +export default { + key: "better_stack-acknowledge-incident", + name: "Acknowledge Incident", + description: "Acknowledges an incident, marking it as acknowledged in Better Stack. [See the documentation](https://betterstack.com/docs/uptime/api/acknowledge-an-ongoing-incident/)", + version: "0.0.1", + type: "action", + props: { + app, + incidentId: { + propDefinition: [ + app, + "incidentId", + ], + }, + acknowledgedBy: { + type: "string", + label: "Acknowledged By", + description: "User e-mail or a custom identifier of the entity that acknowledged the incident", + optional: true, + }, + }, + methods: { + acknowledgeIncident({ + incidentId, ...args + } = {}) { + return this.app.post({ + path: `/incidents/${incidentId}/acknowledge`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + acknowledgeIncident, + incidentId, + acknowledgedBy, + } = this; + + const response = await acknowledgeIncident({ + $, + incidentId, + data: { + acknowledged_by: acknowledgedBy, + }, + }); + + $.export("$summary", `Successfully acknowledged incident with ID \`${response.data.id}\``); + return response; + }, +}; diff --git a/components/better_stack/actions/create-incident/create-incident.mjs b/components/better_stack/actions/create-incident/create-incident.mjs new file mode 100644 index 0000000000000..a096236146098 --- /dev/null +++ b/components/better_stack/actions/create-incident/create-incident.mjs @@ -0,0 +1,112 @@ +import app from "../../better_stack.app.mjs"; + +export default { + key: "better_stack-create-incident", + name: "Create Incident", + description: "Initiates an incident that signals the team. [See the documentation](https://betterstack.com/docs/uptime/api/create-a-new-incident/)", + version: "0.0.1", + type: "action", + props: { + app, + requesterEmail: { + type: "string", + label: "Requester Email", + description: "E-mail of the user who requested the incident", + }, + summary: { + type: "string", + label: "Summary", + description: "Brief summary of the incident", + }, + name: { + type: "string", + label: "Name", + description: "Short name of the incident", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "Full description of the incident", + optional: true, + }, + call: { + type: "boolean", + label: "Call", + description: "Should we call the on-call person?", + optional: true, + }, + sms: { + type: "boolean", + label: "SMS", + description: "Should we send an SMS to the on-call person?", + optional: true, + }, + email: { + type: "boolean", + label: "Email", + description: "Should we send an email to the on-call person?", + optional: true, + }, + push: { + type: "boolean", + label: "Push", + description: "Should we send a push notification to the on-call person?", + optional: true, + }, + teamWait: { + type: "integer", + label: "Team Wait", + description: "How long to wait before escalating the incident alert to the team. Leave blank to disable escalating to the entire team. In seconds.", + optional: true, + }, + policyId: { + propDefinition: [ + app, + "policyId", + ], + }, + }, + methods: { + createIncident(args = {}) { + return this.app.post({ + path: "/incidents", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createIncident, + requesterEmail, + name, + summary, + description, + call, + sms, + email, + push, + teamWait, + policyId, + } = this; + + const response = await createIncident({ + $, + data: { + requester_email: requesterEmail, + name, + summary, + description, + call, + sms, + email, + push, + team_wait: teamWait, + policy_id: policyId, + }, + }); + + $.export("$summary", `Successfully created incident with ID \`${response.data.id}\``); + return response; + }, +}; diff --git a/components/better_stack/actions/resolve-incident/resolve-incident.mjs b/components/better_stack/actions/resolve-incident/resolve-incident.mjs new file mode 100644 index 0000000000000..660ffc84414c6 --- /dev/null +++ b/components/better_stack/actions/resolve-incident/resolve-incident.mjs @@ -0,0 +1,52 @@ +import app from "../../better_stack.app.mjs"; + +export default { + key: "better_stack-resolve-incident", + name: "Resolve Incident", + description: "Brings a closure to an incident by resolving it with optional resolution details. [See the documentation](https://betterstack.com/docs/uptime/api/resolve-an-ongoing-incident/)", + version: "0.0.1", + type: "action", + props: { + app, + incidentId: { + propDefinition: [ + app, + "incidentId", + ], + }, + resolvedBy: { + type: "string", + label: "Resolved By", + description: "User e-mail or a custom identifier of the entity that resolved the incident", + optional: true, + }, + }, + methods: { + resolveIncident({ + incidentId, ...args + } = {}) { + return this.app.post({ + path: `/incidents/${incidentId}/resolve`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + resolveIncident, + incidentId, + resolvedBy, + } = this; + + const response = await resolveIncident({ + $, + incidentId, + data: { + resolved_by: resolvedBy, + }, + }); + + $.export("$summary", `Successfully resolved incident with ID \`${response.data.id}\``); + return response; + }, +}; diff --git a/components/better_stack/better_stack.app.mjs b/components/better_stack/better_stack.app.mjs new file mode 100644 index 0000000000000..7ceb83a0f5462 --- /dev/null +++ b/components/better_stack/better_stack.app.mjs @@ -0,0 +1,111 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "better_stack", + propDefinitions: { + policyId: { + type: "string", + label: "Policy ID", + description: "The ID of the escalation policy with which you'd like to escalate this incident", + optional: true, + async options({ + page, prevContext: { hasNext }, + }) { + if (hasNext === false) { + return []; + } + const { + data, + pagination: { next }, + } = await this.listPolicies({ + params: { + page: page + 1, + }, + }); + return { + options: data.map(({ + id: value, attributes: { name: label }, + }) => ({ + label, + value, + })), + context: { + hasNext: !!next, + }, + }; + }, + }, + incidentId: { + type: "string", + label: "Incident ID", + description: "The unique identifier for the incident.", + async options({ + page, prevContext: { hasNext }, + }) { + if (hasNext === false) { + return []; + } + const { + data, + pagination: { next }, + } = await this.listIncidents({ + params: { + page: page + 1, + }, + }); + return { + options: data.map(({ + id: value, attributes: { name: label }, + }) => ({ + label, + value, + })), + context: { + hasNext: !!next, + }, + }; + }, + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Authorization": `Bearer ${this.$auth.uptime_token}`, + "Content-Type": "application/json", + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + listPolicies(args = {}) { + return this._makeRequest({ + path: "/policies", + ...args, + }); + }, + listIncidents(args = {}) { + return this._makeRequest({ + path: "/incidents", + ...args, + }); + }, + }, +}; diff --git a/components/better_stack/common/constants.mjs b/components/better_stack/common/constants.mjs new file mode 100644 index 0000000000000..747fecdb9989b --- /dev/null +++ b/components/better_stack/common/constants.mjs @@ -0,0 +1,7 @@ +const BASE_URL = "https://uptime.betterstack.com"; +const VERSION_PATH = "/api/v2"; + +export default { + BASE_URL, + VERSION_PATH, +}; diff --git a/components/better_stack/package.json b/components/better_stack/package.json new file mode 100644 index 0000000000000..00382cda087a4 --- /dev/null +++ b/components/better_stack/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/better_stack", + "version": "0.1.0", + "description": "Pipedream Better Stack Components", + "main": "better_stack.app.mjs", + "keywords": [ + "pipedream", + "better_stack" + ], + "homepage": "https://pipedream.com/apps/better_stack", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/better_uptime/README.md b/components/better_uptime/README.md new file mode 100644 index 0000000000000..effe46c7d87c9 --- /dev/null +++ b/components/better_uptime/README.md @@ -0,0 +1,11 @@ +# Overview + +The Better Uptime API allows users to automate incident management and monitor the status of various systems. With Pipedream, you can harness this API to create custom workflows that trigger actions based on the status of your monitored endpoints. You can receive alerts, automate incident resolution tasks, or sync incident data with other tools, enhancing your operational efficiency and reducing downtime. + +# Example Use Cases + +- **Incident Alert to Slack**: When Better Uptime detects an incident, trigger a Pipedream workflow to send a notification to a designated Slack channel. This ensures that your team is alerted immediately and can take quick action. + +- **Auto-Create Incident Tickets in Jira**: On a new incident, automatically create a ticket in Jira through Pipedream. This workflow can include details such as the incident's time, nature, and affected system, keeping your issue tracking synchronized and up to date without manual entry. + +- **Sync Incidents to Google Sheets**: Maintain an audit trail by pushing incident reports into a Google Sheets spreadsheet using Pipedream. Each incident can be logged with timestamps, status updates, and resolution notes, allowing for easy tracking and analysis over time. diff --git a/components/bettervoice/README.md b/components/bettervoice/README.md index 3baa858a1c3f1..e02e7e86fc380 100644 --- a/components/bettervoice/README.md +++ b/components/bettervoice/README.md @@ -1,10 +1,11 @@ # Overview -The BetterVoice API enables you to build a wide variety of voice-enabled applications. Below are just a few examples of what you can build: - -- A voice-enabled call center application -- A voice-enabled customer service application -- A voice-enabled ordering system -- A voice-enabled directory assistance application -- A voice-enabled voicemail application -- A voice-enabled messaging application +The BetterVoice API allows you to automate interactions with phone systems, such as sending and receiving calls, managing voicemails, and creating custom interactive voice response (IVR) systems. Utilizing Pipedream, you can tap into BetterVoice functionalities to craft workflows that trigger actions based on phone events, analyze call data, and integrate with other services for a seamless communication experience. + +# Example Use Cases + +- **Sales Lead Response Automation**: When a new voicemail is received on BetterVoice, Pipedream can trigger a workflow that transcribes the voicemail using a speech-to-text service, then logs the lead's details and the transcription into a CRM like Salesforce. An automated email or SMS follow-up is triggered instantly, ensuring that potential leads are engaged promptly. + +- **Customer Support Ticketing**: Integrate BetterVoice with a helpdesk app like Zendesk on Pipedream. Every time a call ends, a workflow is triggered that creates a new support ticket with the call details, tags it based on the IVR options the caller selected, and assigns it to the appropriate support team. + +- **Event-Driven Surveys**: After a call concludes, trigger a workflow that sends a follow-up SMS to the caller using Twilio, inviting them to participate in a customer satisfaction survey. Based on the survey results, which are processed and stored, take further action such as sending a thank-you note, a discount code, or flagging the feedback for review. diff --git a/components/bettervoice/package.json b/components/bettervoice/package.json new file mode 100644 index 0000000000000..8eee613dc671e --- /dev/null +++ b/components/bettervoice/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/bettervoice", + "version": "0.6.0", + "description": "Pipedream bettervoice Components", + "main": "bettervoice.app.mjs", + "keywords": [ + "pipedream", + "bettervoice" + ], + "homepage": "https://pipedream.com/apps/bettervoice", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/bexio/README.md b/components/bexio/README.md index 2c92d4138ba30..3699e9a07de8f 100644 --- a/components/bexio/README.md +++ b/components/bexio/README.md @@ -1,8 +1,11 @@ # Overview -You can use the bexio API to build a wide variety of applications and integrations. Some examples include: +The bexio API offers a gateway to streamline administrative tasks linked with small business management. With this API, you can automate processes, sync data across platforms, and enhance efficiency by reducing manual input. Pipedream, with its serverless execution environment, lets you create complex workflows around bexio. You can automate invoicing, manage contacts, monitor projects, and integrate with other services like CRMs, email, and payment gateways. -- An accounting application that automatically pulls in invoices and bills from bexio -- A CRM integration that lets you keep track of your customers' bexio activity -- A project management tool that gives you visibility into your team's bexio activity -- An e-commerce platform that integrates with bexio's inventory and order management features +# Example Use Cases + +- **Automated Invoice Creation and Delivery**: Trigger a workflow in Pipedream when a new sale is made in your e-commerce platform. Create an invoice in bexio, email it to the customer, and record the transaction in your accounting software. + +- **Dynamic Contact Synchronization**: Sync new contacts between bexio and your email marketing tool. When a new contact is added in bexio, automatically add them to your Mailchimp list, ensuring your marketing campaigns reach all your business contacts. + +- **Project Time Tracking Integration**: Link bexio with time tracking apps like Toggl. When a project is created in bexio, set up a corresponding project in Toggl, and when time is logged, reflect this in bexio for accurate and up-to-date project management. diff --git a/components/bexio/package.json b/components/bexio/package.json new file mode 100644 index 0000000000000..da41ba9070f4c --- /dev/null +++ b/components/bexio/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/bexio", + "version": "0.6.0", + "description": "Pipedream bexio Components", + "main": "bexio.app.mjs", + "keywords": [ + "pipedream", + "bexio" + ], + "homepage": "https://pipedream.com/apps/bexio", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/bidsketch/README.md b/components/bidsketch/README.md index f7705f13fa780..066b21fb44bae 100644 --- a/components/bidsketch/README.md +++ b/components/bidsketch/README.md @@ -1,5 +1,11 @@ # Overview -The Bidsketch API allows developers to access and integrate the functionality of Bidsketch with other applications. +The Bidsketch API offers a gateway to streamline your proposal creation and management process. It empowers users to automate the drafting of proposals, maintain client data, and track proposal statuses without the manual hassle. With Pipedream, you can connect Bidsketch to a multitude of other apps, crafting workflows that trigger actions based on events such as proposal acceptance or comment additions, thus ensuring timely follow-ups and efficient client interaction. -Some example API methods include managing users, retrieving proposals, and creating proposals. +# Example Use Cases + +- **Automated Proposal Follow-Ups**: When a proposal is viewed by a client, trigger an email or SMS to remind them to provide feedback or ask if they have any questions. This can be done by integrating Bidsketch with email and SMS services like SendGrid or Twilio on Pipedream. + +- **Instant Notification on Proposal Approval**: Set up an instant alert via Slack or another messaging platform when a proposal is approved. This can keep your sales team in the loop and initiate the next steps in the onboarding process swiftly. + +- **Sync Proposal Status with CRM**: Whenever a proposal status changes, the workflow can update the deal stage in a CRM like Salesforce, keeping sales pipelines up-to-date without manual data entry. diff --git a/components/bidsketch/package.json b/components/bidsketch/package.json new file mode 100644 index 0000000000000..62fecf58b6d62 --- /dev/null +++ b/components/bidsketch/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/bidsketch", + "version": "0.6.0", + "description": "Pipedream bidsketch Components", + "main": "bidsketch.app.mjs", + "keywords": [ + "pipedream", + "bidsketch" + ], + "homepage": "https://pipedream.com/apps/bidsketch", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/big_cartel/README.md b/components/big_cartel/README.md index 269289c010032..478b00af28ec4 100644 --- a/components/big_cartel/README.md +++ b/components/big_cartel/README.md @@ -1,11 +1,11 @@ # Overview -With the Big Cartel API, you can build applications that: - -- Display information about products, collections, and orders -- Allow customers to purchase products -- Create and manage customer accounts -- Offer discounts and promotional codes -- Process payments -- Send abandoned cart emails -- Analyze sales data +The Big Cartel API unlocks the power to automate and integrate your e-commerce operations, enabling you to seamlessly manage products, orders, and store settings. With Pipedream, you can harness this functionality within serverless workflows to trigger actions based on store events, sync data across platforms, or personalize customer interactions—all without the need for dedicated infrastructure. + +# Example Use Cases + +- **Order Fulfillment Automation**: When a new order comes in, Pipedream can trigger a workflow that automatically sends the order details to a fulfillment service like ShipStation. You could also set up notifications to be sent to a Slack channel, keeping your team updated in real time. + +- **Customer Relationship Management (CRM) Integration**: Sync new customer data from Big Cartel to a CRM platform such as Salesforce or HubSpot. Whenever a new order is placed, the workflow can enrich customer profiles with their latest purchases, or add new contacts, aiding in personalized marketing and sales follow-ups. + +- **Inventory Management**: Keep your inventory up-to-date by connecting Big Cartel with inventory tracking tools like Airtable. Set up a workflow that adjusts stock levels in your Airtable base when sales are made or when new products are added on Big Cartel, ensuring your data stays synchronized across platforms. diff --git a/components/big_data_cloud/README.md b/components/big_data_cloud/README.md new file mode 100644 index 0000000000000..bc24d7f16bbaf --- /dev/null +++ b/components/big_data_cloud/README.md @@ -0,0 +1,11 @@ +# Overview + +The Big Data Cloud API provides a suite of RESTful APIs for IP geolocation and various other data services that can enrich your applications with valuable insights. On Pipedream, you can integrate these APIs into serverless workflows to automate data collection, analysis, and decision-making processes. Whether you're looking to enhance user experience with location-based content, validate user data, or perform any number of data-enrichment tasks, Big Data Cloud can be a go-to resource within your Pipedream workflows. + +# Example Use Cases + +- **IP Geolocation to Localize User Experience**: Trigger a Pipedream workflow with HTTP requests containing user IPs, use Big Data Cloud API to fetch geolocation data, and adjust content or services provided based on the user's location. For instance, tailor language preferences or currency formatting in a web app dynamically. + +- **Security Alerts Based on Geographical Anomalies**: Integrate user login data from a database or authentication service with Big Data Cloud API in Pipedream to detect unusual login locations. If an account logs in from a new country, you can automate an alert via email or Slack to notify the user or your security team. + +- **Data Enrichment for Lead Scoring**: Combine CRM data with Big Data Cloud's geolocation API in Pipedream to enhance lead profiles. Use geolocation insights to score leads more accurately, improving sales targeting and personalized marketing efforts. diff --git a/components/bigbox/README.md b/components/bigbox/README.md new file mode 100644 index 0000000000000..affd3076a61eb --- /dev/null +++ b/components/bigbox/README.md @@ -0,0 +1,11 @@ +# Overview + +The BigBox API serves up data on products, allowing for searches by various parameters like brand, category, or specifics like vegan or gluten-free options. With this tool, you can automate e-commerce tasks, sync inventory with your database, or enrich product listings with detailed information. Pipedream's platform is ideal for harnessing this API, offering serverless connectors that trigger workflows, run code, and interact with countless other APIs and services. + +# Example Use Cases + +- **Automated Product Information Updates**: Track changes in product details or prices by setting up a Pipedream workflow that regularly hits the BigBox API. Sync this data directly to your store's database, ensuring your product listings are always current. + +- **Inventory Alert System**: Create a workflow that monitors specific products for stock levels using the BigBox API. When inventory drops below a threshold, trigger an email alert or a Slack message to prompt restocking, keeping your supply chain responsive. + +- **Cross-Platform Product Syncing**: Integrate BigBox with platforms like Shopify or WooCommerce. When a new product is added to BigBox, use a Pipedream workflow to automatically add that product to your online store, complete with details and images, keeping your offerings consistent across channels. diff --git a/components/bigbox/bigbox.app.mjs b/components/bigbox/bigbox.app.mjs new file mode 100644 index 0000000000000..0601d5d509d26 --- /dev/null +++ b/components/bigbox/bigbox.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "bigbox", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/bigbox/package.json b/components/bigbox/package.json new file mode 100644 index 0000000000000..7280d276ce084 --- /dev/null +++ b/components/bigbox/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/bigbox", + "version": "0.0.1", + "description": "Pipedream BigBox Components", + "main": "bigbox.app.mjs", + "keywords": [ + "pipedream", + "bigbox" + ], + "homepage": "https://pipedream.com/apps/bigbox", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/bigcommerce/README.md b/components/bigcommerce/README.md index cb3e1b1ea2ae6..5c14a428b2b4a 100644 --- a/components/bigcommerce/README.md +++ b/components/bigcommerce/README.md @@ -1,11 +1,11 @@ # Overview -The BigCommerce API lets you build eCommerce applications and integrations on top of the BigCommerce platform. With the API, you can: - -- Create and manage products -- Create and manage customers -- Create and manage orders -- Create and manage coupons -- Create and manage shipping methods -- Create and manage payment methods -- And much more! +The BigCommerce API enables merchants to seamlessly manage their e-commerce operations by automating tasks, syncing data, and integrating with a plethora of other services. With Pipedream, you can tap into the BigCommerce API to create custom workflows that handle everything from order processing to customer relationship management. The result is a more efficient, personalized shopping experience for customers and less manual work for store owners. + +# Example Use Cases + +- **Automated Order Processing**: When a new order is placed in BigCommerce, trigger a workflow in Pipedream that captures the order details, then generates a packing slip, and emails it to your fulfillment center. Connect to a shipping service API, like Shippo or EasyPost, to automatically create shipping labels and track the order until delivery. + +- **Inventory Management**: Set up a Pipedream workflow that monitors changes in inventory levels on BigCommerce. When stock for a popular item runs low, the workflow can trigger reordering from suppliers, update inventory quantities across multiple sales channels, or send restock notifications to relevant team members via Slack or email. + +- **Customer Retention Campaigns**: Use Pipedream to listen for events such as completed orders or customer anniversaries. Trigger a sequence that integrates BigCommerce with an email marketing service like Mailchimp or SendGrid to send personalized promotions, gather customer feedback with surveys, or offer loyalty discounts to encourage repeat business. diff --git a/components/bigcommerce/actions/common/product.mjs b/components/bigcommerce/actions/common/product.mjs new file mode 100644 index 0000000000000..c9214220efbf5 --- /dev/null +++ b/components/bigcommerce/actions/common/product.mjs @@ -0,0 +1,565 @@ +/* eslint-disable no-unused-vars */ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../bigcommerce.app.mjs"; +import constants from "../../common/constants.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + props: { + app, + name: { + type: "string", + label: "Name", + description: + "The product name. >= 1 characters <= 250 characters", + }, + type: { + type: "string", + label: "Type", + description: + "The product type. One of: physical - a physical stock unit, digital - a digital download. Allowed values: physical digital Example: physical", + options: [ + "physical", + "digital", + ], + }, + sku: { + type: "string", + label: "SKU", + description: + "User defined product code/stock keeping unit (SKU). >= 0 characters <= 255 characters Example: SM-13", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "The product description, which can include HTML formatting.", + optional: true, + }, + weight: { + type: "any", + label: "Weight", + description: + "Weight of the product, which can be used when calculating shipping costs. This is based on the unit set on the store >= 0 and <= 9999999999", + }, + width: { + type: "any", + label: "Width", + description: + "Width of the product, which can be used when calculating shipping costs. >= 0 and <= 9999999999", + optional: true, + }, + depth: { + type: "any", + label: "Depth", + description: + "Depth of the product, which can be used when calculating shipping costs. >= 0 and <= 9999999999", + optional: true, + }, + height: { + type: "any", + label: "Height", + description: + "Height of the product, which can be used when calculating shipping costs. >= 0 and <= 9999999999", + optional: true, + }, + price: { + type: "any", + label: "Price", + description: + "The price of the product. The price should include or exclude tax, based on the store settings. >= 0", + }, + cost_price: { + type: "any", + label: "Cost Price", + description: + "The cost price of the product. Stored for reference only; it is not used or displayed anywhere on the store. >=0", + optional: true, + }, + retail_price: { + type: "any", + label: "Retail Price", + description: + "The retail cost of the product. If entered, the retail cost price will be shown on the product page. >=0", + optional: true, + }, + sale_price: { + type: "any", + label: "Sale Price", + description: + "If entered, the sale price will be used instead of value in the price field when calculating the product's cost. >=0", + optional: true, + }, + map_price: { + type: "integer", + label: "Map Price", + description: "Minimum Advertised Price", + optional: true, + }, + tax_class_id: { + type: "integer", + label: "Tax class id", + description: + "The ID of the tax class applied to the product. (NOTE: Value ignored if automatic tax is enabled.) >= 0 and <= 1000000000", + optional: true, + async options({ page }) { + const taxes = await this.app.getAllTaxClasses({ + page: page + 1, + }); + return taxes.map((item) => ({ + label: item.name, + value: parseInt(item.id), + })); + }, + }, + product_tax_code: { + type: "string", + label: "Product tax code", + description: + "Accepts AvaTax System Tax Codes, which identify products and services that fall into special sales-tax categories. By using these codes, merchants who subscribe to BigCommerce's Avalara Premium integration can calculate sales taxes more accurately. Stores without Avalara Premium will ignore the code when calculating sales tax. Do not pass more than one code. The codes are case-sensitive. For details, please see Avalara's documentation. >= 0 characters <= 255 characters", + optional: true, + }, + categories: { + type: "integer[]", + label: "Categories", + description: + "An array of IDs for the categories to which this product belongs. When updating a product, if an array of categories is supplied, all product categories will be overwritten. Does not accept more than 1,000 ID values.", + optional: true, + async options({ page }) { + const categories = await this.app.getAllCategories({ + page: page + 1, + }); + return categories.map((item) => ({ + label: item.name, + value: item.id, + })); + }, + }, + brand_id: { + type: "integer", + label: "Brand Id", + description: + "A product can be added to an existing brand during a product /PUT or /POST. >= 0 and <= 1000000000", + optional: true, + async options({ page }) { + const { data } = await this.app.getAllBrands({ + page: page + 1, + }); + + return data.map((item) => ({ + label: item.name, + value: item.id, + })); + }, + }, + inventory_level: { + type: "integer", + label: "Inventory Level", + description: + "Current inventory level of the product. Simple inventory tracking must be enabled (See the inventory_tracking field) for this to take any effect. >= 0 and <= 1000000000", + optional: true, + }, + inventory_warning_level: { + type: "integer", + label: "Inventory Warning Level", + description: + "Inventory warning level for the product. When the product's inventory level drops below the warning level, the store owner will be informed. Simple inventory tracking must be enabled (see the inventory_tracking field) for this to take any effect. >= 0 and <= 1000000000", + optional: true, + }, + inventory_tracking: { + type: "string", + label: "Inventory Tracking", + description: + "The type of inventory tracking for the product. Values are: none - inventory levels will not be tracked; product - inventory levels will be tracked using the inventory_level and inventory_warning_level fields; variant - inventory levels will be tracked based on variants, which maintain their own warning levels and inventory levels. Allowed values: none, product, variant", + optional: true, + options: [ + "none", + "product", + "variant", + ], + }, + fixed_cost_shipping_price: { + type: "any", + label: "Fixed cost shipping price", + description: + "A fixed shipping cost for the product. If defined, this value will be used during checkout instead of normal shipping-cost calculation. >= 0", + optional: true, + }, + is_free_shipping: { + type: "boolean", + label: "Is free shipping", + description: + "Flag used to indicate whether the product has free shipping. If true, the shipping cost for the product will be zero.", + optional: true, + }, + is_visible: { + type: "boolean", + label: "Is visible", + description: + "Flag to determine whether the product should be displayed to customers browsing the store. If true, the product will be displayed. If false, the product will be hidden from view.", + optional: true, + }, + is_featured: { + type: "boolean", + label: "Is Featured", + description: + "Flag to determine whether the product should be included in the featured products panel when viewing the store.", + optional: true, + }, + related_products: { + type: "integer[]", + label: "Related Products", + description: "An array of IDs for the related products.", + optional: true, + async options({ page }) { + const { data } = await this.app.getAllProducts({ + page: page + 1, + }); + return data.map((item) => ({ + label: item.name, + value: item.id, + })); + }, + }, + warranty: { + type: "string", + label: "Warranty", + description: + "Warranty information displayed on the product page. Can include HTML formatting. >= 0 characters and <= 65535 characters", + optional: true, + }, + bin_picking_number: { + type: "string", + label: "Bin picking number", + description: + "The BIN picking number for the product. >= 0 characters and <= 255 characters", + optional: true, + }, + layout_file: { + type: "string", + label: "Layout File", + description: + "The layout template file used to render this product category. This field is writable only for stores with a Blueprint theme applied. >= 0 characters and <= 500 characters", + optional: true, + }, + upc: { + type: "string", + label: "UPC", + description: + "The product UPC code, which is used in feeds for shopping comparison sites and external channel integrations. >= 0 characters and <= 255 characters", + optional: true, + }, + search_keywords: { + type: "string", + label: "Seach keywords", + description: + "A comma-separated list of keywords that can be used to locate the product when searching the store. >= 0 characters and <= 65535 characters", + optional: true, + }, + availability: { + type: "string", + label: "Availability", + description: + "Availability of the product. (Corresponds to the product's Purchasability section in the control panel.) Supported values: available - the product is available for purchase; disabled - the product is listed on the storefront, but cannot be purchased; preorder - the product is listed for pre-orders. Allowed values: available, disabled or preorder", + optional: true, + options: [ + "available", + "disabled", + "preorder", + ], + }, + availability_description: { + type: "string", + label: "Availability description", + description: + "Availability text displayed on the checkout page, under the product title. Tells the customer how long it will normally take to ship this product, such as: 'Usually ships in 24 hours.' >= 0 characters and <= 255 characters", + optional: true, + }, + gift_wrapping_options_type: { + type: "string", + label: "Gift wrapping options type", + description: + "Type of gift-wrapping options. Values: any - allow any gift-wrapping options in the store; none - disallow gift-wrapping on the product; list – provide a list of IDs in the gift_wrapping_options_list field. Allowed values: any, none or list", + optional: true, + options: [ + "any", + "none", + "list", + ], + }, + gift_wrapping_options_list: { + type: "string[]", + label: "Gift wrapping options list", + description: "A list of gift-wrapping option IDs.", + optional: true, + }, + sort_order: { + type: "integer", + label: "Sort order", + description: + "Priority to give this product when included in product lists on category pages and in search results. Lower integers will place the product closer to the top of the results. >= -2147483648 and <= 2147483647", + optional: true, + }, + condition: { + type: "string", + label: "Condition", + description: + "The product condition. Will be shown on the product page if the is_condition_shown field's value is true. Possible values: New, Used, Refurbished. Allowed values: New, Used and Refurbished", + optional: true, + options: [ + "New", + "Used", + "Refurbished", + ], + }, + is_condition_shown: { + type: "boolean", + label: "Is condition shown", + description: + "Flag used to determine whether the product condition is shown to the customer on the product page.", + optional: true, + }, + order_quantity_minimum: { + type: "integer", + label: "Order quantity minimum", + description: + "The minimum quantity an order must contain, to be eligible to purchase this product. >= 0 and <= 1000000000", + optional: true, + }, + order_quantity_maximum: { + type: "integer", + label: "Order quantity maximum", + description: + "The maximum quantity an order can contain when purchasing the product. >= 0 and <= 1000000000", + optional: true, + }, + page_title: { + type: "string", + label: "Page title", + description: + "Custom title for the product page. If not defined, the product name will be used as the meta title. >= 0 characters and <= 255 characters", + optional: true, + }, + meta_keywords: { + type: "string[]", + label: "Meta keywords", + description: + "Custom meta keywords for the product page. If not defined, the store's default keywords will be used.", + optional: true, + }, + meta_description: { + type: "string", + label: "Meta description", + description: + "Custom meta description for the product page. If not defined, the store's default meta description will be used. >= 0 characters and <= 65535 characters", + optional: true, + }, + view_count: { + type: "integer", + label: "View count", + description: + "The number of times the product has been viewed. >= 0 and <= 1000000000", + optional: true, + }, + preorder_release_date: { + type: "string", + label: "Preorder release date", + description: + "Pre-order release date. See the availability field for details on setting a product's availability to accept pre-orders.", + optional: true, + }, + preorder_message: { + type: "string", + label: "Preorder message", + description: + "Custom expected-date message to display on the product page. If undefined, the message defaults to the storewide setting. Can contain the %%DATE%% placeholder, which will be substituted for the release date. >= 0 characters and <= 255 characters", + optional: true, + }, + is_preorder_only: { + type: "boolean", + label: "Is preorder only", + description: + "If set to true then on the preorder release date the preorder status will automatically be removed. If set to false, then on the release date the preorder status will not be removed. It will need to be changed manually either in the control panel or using the API. Using the API set availability to available.", + optional: true, + }, + is_price_hidden: { + type: "boolean", + label: "Is preorder hidden", + description: + "False by default, indicating that this product's price should be shown on the product page. If set to true, the price is hidden. (NOTE: To successfully set is_price_hidden to true, the availability value must be disabled.)", + optional: true, + }, + price_hidden_label: { + type: "string", + label: "Price hidden label", + description: + "By default, an empty string. If is_price_hidden is true, the value of price_hidden_label is displayed instead of the price. (NOTE: To successfully set a non-empty string value with is_price_hidden set to true, the availability value must be disabled.) >= 0 characters and <= 200 characters", + optional: true, + }, + open_graph_type: { + type: "string", + label: "Open graph type", + description: + "Type of product, defaults to product. Allowed values: product, album, book, drink, food, game, movie, song OR tv_show", + optional: true, + options: [ + "product", + "album", + "book", + "drink", + "food", + "game", + "movie", + "song", + "tv_show", + ], + }, + open_graph_title: { + type: "string", + label: "Open graph title", + description: + "Title of the product, if not specified the product name will be used instead.", + optional: true, + }, + open_graph_description: { + type: "string", + label: "Open graph description", + description: + "Description to use for the product, if not specified then the meta_description will be used instead.", + optional: true, + }, + open_graph_use_meta_description: { + type: "boolean", + label: "Open graph use meta description", + description: + "Flag to determine if product description or open graph description is used.", + optional: true, + }, + open_graph_use_product_name: { + type: "boolean", + label: "Open graph use product name", + description: + "Flag to determine if product name or open graph name is used.", + optional: true, + }, + open_graph_use_image: { + type: "boolean", + label: "Open graph use image", + description: + "Flag to determine if product image or open graph image is used.", + optional: true, + }, + brand_name: { + type: "string", + label: "Brand name", + description: + "It performs a fuzzy match and adds the brand. eg. \"Common Good\" and \"Common good\" are the same. Brand name does not return as part of a product response. Only the brand_id.", + optional: true, + }, + gtin: { + type: "string", + label: "GTIN", + description: "Global Trade Item Number", + optional: true, + }, + mpn: { + type: "string", + label: "MPN", + description: "Manufacturer Part Number", + optional: true, + }, + reviews_rating_sum: { + type: "any", + label: "Reviews rating sum", + description: "The total rating for the product.", + optional: true, + }, + reviews_count: { + type: "integer", + label: "Reviews count", + description: "The number of times the product has been rated.", + optional: true, + }, + total_sold: { + type: "integer", + label: "Total sold", + description: "The total quantity of this product sold.", + optional: true, + }, + images: { + type: "string[]", + label: "Images", + description: "Array of image strings. They can be either local paths or URLs. Limit of 8MB per file. If it is a URL it has a 255 character limit. Supported image file types are BMP, GIF, JPEG, PNG, WBMP, XBM, and WEBP. If it is a local path it should be previously downloaded to Pipedream E.g. (`/tmp/my-image.png`). [Download a file to the `/tmp` directory](https://pipedream.com/docs/code/nodejs/http-requests/#download-a-file-to-the-tmp-directory)", + optional: true, + }, + }, + methods: { + getRequestFn() { + throw new ConfigurationError("getRequestFn not implemented"); + }, + getRequestFnArgs() { + throw new ConfigurationError("getRequestFnArgs not implemented"); + }, + getSummary() { + throw new ConfigurationError("getSummary not implemented"); + }, + }, + async run({ $ }) { + const { + productId, + createProduct, + updateProduct, + app, + getSummary, + getRequestFn, + getRequestFnArgs, + images, + ...data + } = this; + + const imagesArray = utils.parseArray(images); + const imageFiles = imagesArray?.filter((image) => image?.startsWith("/")); + const imageUrls = imagesArray?.filter((image) => image?.startsWith("http")); + + const requestFn = getRequestFn(); + const args = getRequestFnArgs({ + $, + data: { + ...data, + images: imageUrls + .map((imageUrl, idx) => ({ + image_url: imageUrl, + is_thumbnail: idx === 0, + })), + }, + }); + const productResponse = await requestFn(args); + + if (!imageFiles.length) { + $.export("$summary", getSummary(productResponse)); + return productResponse; + } + + const productImages = imageFiles.map((imageFile) => app.createProductImage({ + $, + productId: productResponse.data.id, + headers: constants.MULTIPART_FORM_DATA_HEADERS, + data: { + image_file: imageFile, + }, + })); + + await Promise.all(productImages); + + const response = await app.getProduct({ + $, + productId: productResponse.data.id, + params: { + include: "images", + }, + }); + + $.export("$summary", getSummary(productResponse)); + return response; + }, +}; diff --git a/components/bigcommerce/actions/create-product-image/create-product-image.mjs b/components/bigcommerce/actions/create-product-image/create-product-image.mjs new file mode 100644 index 0000000000000..8afb45d50121e --- /dev/null +++ b/components/bigcommerce/actions/create-product-image/create-product-image.mjs @@ -0,0 +1,55 @@ +import app from "../../bigcommerce.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "bigcommerce-create-product-image", + name: "Create Product Image", + description: + "Create a product image. [See the docs here](https://developer.bigcommerce.com/docs/rest-catalog/products/images#create-a-product-image)", + version: "0.0.1", + type: "action", + props: { + app, + productId: { + propDefinition: [ + app, + "productId", + ], + }, + imageFile: { + propDefinition: [ + app, + "imageFile", + ], + }, + imageUrl: { + propDefinition: [ + app, + "imageUrl", + ], + }, + }, + async run({ $ }) { + const { + app, + productId, + imageFile, + imageUrl, + } = this; + + const response = await app.createProductImage({ + $, + productId, + ...(imageFile?.startsWith("/") && { + headers: constants.MULTIPART_FORM_DATA_HEADERS, + }), + data: { + image_file: imageFile, + image_url: imageUrl, + }, + }); + + $.export("$summary", `Successfully created product image with ID \`${response.data.id}\``); + return response; + }, +}; diff --git a/components/bigcommerce/actions/create-product/create-product.mjs b/components/bigcommerce/actions/create-product/create-product.mjs index 46e9fae736a42..32db30a926225 100644 --- a/components/bigcommerce/actions/create-product/create-product.mjs +++ b/components/bigcommerce/actions/create-product/create-product.mjs @@ -1,27 +1,30 @@ -import bigcommerce from "../../bigcommerce.app.mjs"; -import productProps from "../../common/product-props.mjs"; +import common from "../common/product.mjs"; export default { + ...common, key: "bigcommerce-create-product", name: "Create Product", description: "Create a product. [See the docs here](https://developer.bigcommerce.com/api-reference/366928572e59e-create-a-product)", - version: "0.0.2", + version: "0.0.3", type: "action", - props: { - bigcommerce, - ...productProps, - }, - async run({ $ }) { - const { - bigcommerce, ...props - } = this; - const response = await bigcommerce.createProduct({ - $, - props, - }); - - $.export("$summary", `Successfully created product ${this.name}`); - return response.data; + methods: { + ...common.methods, + createProduct(args = {}) { + return this.app._makeRequest({ + method: "POST", + path: "/catalog/products", + ...args, + }); + }, + getRequestFn() { + return this.createProduct; + }, + getRequestFnArgs(args = {}) { + return args; + }, + getSummary(response) { + return `Successfully created product with ID \`${response.data.id}\``; + }, }, }; diff --git a/components/bigcommerce/actions/delete-product/delete-product.mjs b/components/bigcommerce/actions/delete-product/delete-product.mjs index cd7760b274f53..488ed818a1edc 100644 --- a/components/bigcommerce/actions/delete-product/delete-product.mjs +++ b/components/bigcommerce/actions/delete-product/delete-product.mjs @@ -5,7 +5,7 @@ export default { name: "Delete Product", description: "Delete a product by Id. [See the docs here](https://developer.bigcommerce.com/api-reference/76f5ebcdab695-delete-a-product)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { bigcommerce, diff --git a/components/bigcommerce/actions/get-all-products-sort-order/get-all-products-sort-order.mjs b/components/bigcommerce/actions/get-all-products-sort-order/get-all-products-sort-order.mjs index 36819dba652a0..5b7450844d40a 100644 --- a/components/bigcommerce/actions/get-all-products-sort-order/get-all-products-sort-order.mjs +++ b/components/bigcommerce/actions/get-all-products-sort-order/get-all-products-sort-order.mjs @@ -5,7 +5,7 @@ export default { name: "Get All Products Sort Order", description: "Get all your products. [See the docs here](https://developer.bigcommerce.com/api-reference/90ab7265480e2-get-product-sort-order)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { bigcommerce, diff --git a/components/bigcommerce/actions/get-all-products/get-all-products.mjs b/components/bigcommerce/actions/get-all-products/get-all-products.mjs index c657fb953cae3..a743c2f96c219 100644 --- a/components/bigcommerce/actions/get-all-products/get-all-products.mjs +++ b/components/bigcommerce/actions/get-all-products/get-all-products.mjs @@ -5,7 +5,7 @@ export default { name: "Get All Products", description: "Get all your products. [See the docs here](https://developer.bigcommerce.com/api-reference/4101d472a814d-get-all-products)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { bigcommerce, diff --git a/components/bigcommerce/actions/get-product-variants/get-product-variants.mjs b/components/bigcommerce/actions/get-product-variants/get-product-variants.mjs index f65675e65c068..a6aecf589fc87 100644 --- a/components/bigcommerce/actions/get-product-variants/get-product-variants.mjs +++ b/components/bigcommerce/actions/get-product-variants/get-product-variants.mjs @@ -5,7 +5,7 @@ export default { name: "Get Product Variants", description: "Get all product variants. [See the docs here](https://developer.bigcommerce.com/api-reference/02db3ddfc6be7-get-all-product-variants)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { bigcommerce, diff --git a/components/bigcommerce/actions/get-product/get-product.mjs b/components/bigcommerce/actions/get-product/get-product.mjs index 0ade4a08838df..859771fdc64f2 100644 --- a/components/bigcommerce/actions/get-product/get-product.mjs +++ b/components/bigcommerce/actions/get-product/get-product.mjs @@ -5,7 +5,7 @@ export default { name: "Get Product By Id", description: "Get a specific product by id. [See the docs here](https://developer.bigcommerce.com/api-reference/6fe995bba597e-get-a-product)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { bigcommerce, diff --git a/components/bigcommerce/actions/update-product/update-product.mjs b/components/bigcommerce/actions/update-product/update-product.mjs index 252f8931a7021..0c671f12d781f 100644 --- a/components/bigcommerce/actions/update-product/update-product.mjs +++ b/components/bigcommerce/actions/update-product/update-product.mjs @@ -1,33 +1,47 @@ -import bigcommerce from "../../bigcommerce.app.mjs"; -import productProps from "../../common/product-props.mjs"; +import common from "../common/product.mjs"; + +const { app } = common.props; export default { + ...common, key: "bigcommerce-update-product", name: "Update Product", description: "Update a product by Id. [See the docs here](https://developer.bigcommerce.com/api-reference/6f05c1244d972-update-a-product)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { - bigcommerce, + app, productId: { propDefinition: [ - bigcommerce, + app, "productId", ], }, - ...productProps, + ...common.props, }, - async run({ $ }) { - const { - bigcommerce, ...props - } = this; - const response = await bigcommerce.updateProduct({ - $, - props, - }); - - $.export("$summary", `Successfully updated product ${this.productId}`); - return response.data; + methods: { + ...common.methods, + updateProduct({ + productId, ...args + }) { + return this.app._makeRequest({ + method: "PUT", + path: `/catalog/products/${productId}`, + ...args, + }); + }, + getRequestFn() { + return this.updateProduct; + }, + getRequestFnArgs(args = {}) { + return { + ...args, + productId: this.productId, + }; + }, + getSummary(response) { + return `Successfully updated product with ID \`${response.data.id}\``; + }, }, }; diff --git a/components/bigcommerce/bigcommerce.app.mjs b/components/bigcommerce/bigcommerce.app.mjs index 21d0a4ee5c574..33803001b7707 100644 --- a/components/bigcommerce/bigcommerce.app.mjs +++ b/components/bigcommerce/bigcommerce.app.mjs @@ -1,5 +1,6 @@ import { axios } from "@pipedream/platform"; import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; export default { type: "app", @@ -20,6 +21,40 @@ export default { type: "integer", label: "Product Id", description: "The id of the product", + async options({ + page, prevContext: { hasMore }, + mapper = ({ + id: value, name: label, + }) => ({ + label, + value, + }), + }) { + if (hasMore === false) { + return []; + } + const { + data, + meta: { + pagination: { + total, + count, + }, + }, + } = await this.getAllProducts({ + params: { + page, + limit: constants.DEFAULT_LIMIT, + }, + }); + const options = data.map(mapper); + return { + options, + context: { + hasMore: count < total, + }, + }; + }, }, categoryId: { type: "integer", @@ -35,31 +70,56 @@ export default { })); }, }, + imageUrl: { + type: "string", + label: "Image URL", + description: "The image URL to upload. 255 character limit. Supported image file types are BMP, GIF, JPEG, PNG, WBMP, XBM, and WEBP. Each image uploaded by URL can be up to 8 MB.", + optional: true, + }, + imageFile: { + type: "string", + label: "Product Image File", + description: "The local path to the original image file previously downloaded to Pipedream E.g. (`/tmp/my-image.png`). [Download a file to the `/tmp` directory](https://pipedream.com/docs/code/nodejs/http-requests/#download-a-file-to-the-tmp-directory). Supported image file types are BMP, GIF, JPEG, PNG, WBMP, XBM, and WEBP. Each image file can be up to 8 MB.", + optional: true, + }, }, methods: { - _getHeaders() { + getHeaders() { return { "X-Auth-Token": `${this.$auth.access_token}`, }; }, - _defaultConfig({ - path, version = "v3", ...otherConfig - }) { - const config = { - url: `${constants.BASE_URL}/${this.$auth.store_hash}/${version}${path}`, - headers: this._getHeaders(), - ...otherConfig, + getUrl(path, version = "v3") { + return `${constants.BASE_URL}/${this.$auth.store_hash}/${version}${path}`; + }, + getFormDataConfig({ + headers, data: preData, ...args + } = {}) { + const contentType = constants.CONTENT_TYPE_KEY_HEADER; + const hasMultipartHeader = utils.hasMultipartHeader(headers); + const data = hasMultipartHeader && utils.getFormData(preData) || preData; + const currentHeaders = this.getHeaders(headers); + + return { + headers: hasMultipartHeader + ? { + ...currentHeaders, + [contentType]: data.getHeaders()[contentType.toLowerCase()], + } + : currentHeaders, + data, + ...args, }; - return config; }, - async _makeRequest({ - $, ...otherConfig + _makeRequest({ + $ = this, path, version, ...args }) { - const config = this._defaultConfig({ - ...otherConfig, + const config = this.getFormDataConfig({ + debug: true, + url: this.getUrl(path, version), + ...args, }); - - return axios($ || this, config); + return axios($, config); }, async *paginate({ $, fn, params = {}, cursor, @@ -142,13 +202,12 @@ export default { params: qparams, }); }, - async getProduct({ - $, productId, - }) { - return await this._makeRequest({ - $, - method: "GET", + getProduct({ + productId, ...args + } = {}) { + return this._makeRequest({ path: `/catalog/products/${productId}`, + ...args, }); }, async getProductVariants({ @@ -160,29 +219,6 @@ export default { path: `/catalog/products/${productId}/variants`, }); }, - async createProduct({ - $, props, - }) { - return await this._makeRequest({ - $, - method: "POST", - path: "/catalog/products", - data: props, - }); - }, - async updateProduct({ - $, props, - }) { - const { - productId, ...data - } = props; - return await this._makeRequest({ - $, - method: "PUT", - path: `/catalog/products/${productId}`, - data, - }); - }, async deleteProduct({ $, productId, }) { @@ -221,5 +257,14 @@ export default { path: `/catalog/brands?page=${page}&limit=${limit}`, }); }, + createProductImage({ + productId, ...args + } = {}) { + return this._makeRequest({ + method: "POST", + path: `/catalog/products/${productId}/images`, + ...args, + }); + }, }, }; diff --git a/components/bigcommerce/common/constants.mjs b/components/bigcommerce/common/constants.mjs index b7c67266aa4c3..f55f5ee902712 100644 --- a/components/bigcommerce/common/constants.mjs +++ b/components/bigcommerce/common/constants.mjs @@ -190,7 +190,24 @@ const WEBHOOK_SCOPES = { }, }; +const FILE_PROP_NAMES = [ + "image_file", +]; + +const CONTENT_TYPE_KEY_HEADER = "Content-Type"; +const MULTIPART_FORM_DATA_VALUE_HEADER = "multipart/form-data"; +const MULTIPART_FORM_DATA_HEADERS = { + [CONTENT_TYPE_KEY_HEADER]: MULTIPART_FORM_DATA_VALUE_HEADER, +}; + +const DEFAULT_LIMIT = 100; + export default { BASE_URL, WEBHOOK_SCOPES, + FILE_PROP_NAMES, + CONTENT_TYPE_KEY_HEADER, + MULTIPART_FORM_DATA_VALUE_HEADER, + MULTIPART_FORM_DATA_HEADERS, + DEFAULT_LIMIT, }; diff --git a/components/bigcommerce/common/product-props.mjs b/components/bigcommerce/common/product-props.mjs deleted file mode 100644 index 04d9b0767ad0a..0000000000000 --- a/components/bigcommerce/common/product-props.mjs +++ /dev/null @@ -1,484 +0,0 @@ -const PROPS = { - name: { - type: "string", - label: "Name", - description: - "The product name. >= 1 characters <= 250 characters", - }, - type: { - type: "string", - label: "Type", - description: - "The product type. One of: physical - a physical stock unit, digital - a digital download. Allowed values: physical digital Example: physical", - options: [ - "physical", - "digital", - ], - }, - sku: { - type: "string", - label: "SKU", - description: - "User defined product code/stock keeping unit (SKU). >= 0 characters <= 255 characters Example: SM-13", - optional: true, - }, - description: { - type: "string", - label: "Description", - description: "The product description, which can include HTML formatting.", - optional: true, - }, - weight: { - type: "any", - label: "Weight", - description: - "Weight of the product, which can be used when calculating shipping costs. This is based on the unit set on the store >= 0 and <= 9999999999", - }, - width: { - type: "any", - label: "Width", - description: - "Width of the product, which can be used when calculating shipping costs. >= 0 and <= 9999999999", - optional: true, - }, - depth: { - type: "any", - label: "Depth", - description: - "Depth of the product, which can be used when calculating shipping costs. >= 0 and <= 9999999999", - optional: true, - }, - height: { - type: "any", - label: "Height", - description: - "Height of the product, which can be used when calculating shipping costs. >= 0 and <= 9999999999", - optional: true, - }, - price: { - type: "any", - label: "Price", - description: - "The price of the product. The price should include or exclude tax, based on the store settings. >= 0", - }, - cost_price: { - type: "any", - label: "Cost Price", - description: - "The cost price of the product. Stored for reference only; it is not used or displayed anywhere on the store. >=0", - optional: true, - }, - retail_price: { - type: "any", - label: "Retail Price", - description: - "The retail cost of the product. If entered, the retail cost price will be shown on the product page. >=0", - optional: true, - }, - sale_price: { - type: "any", - label: "Sale Price", - description: - "If entered, the sale price will be used instead of value in the price field when calculating the product's cost. >=0", - optional: true, - }, - map_price: { - type: "integer", - label: "Map Price", - description: "Minimum Advertised Price", - optional: true, - }, - tax_class_id: { - type: "integer", - label: "Tax class id", - description: - "The ID of the tax class applied to the product. (NOTE: Value ignored if automatic tax is enabled.) >= 0 and <= 1000000000", - optional: true, - async options({ page }) { - const taxes = await this.bigcommerce.getAllTaxClasses({ - page: page + 1, - }); - return taxes.map((item) => ({ - label: item.name, - value: parseInt(item.id), - })); - }, - }, - product_tax_code: { - type: "string", - label: "Product tax code", - description: - "Accepts AvaTax System Tax Codes, which identify products and services that fall into special sales-tax categories. By using these codes, merchants who subscribe to BigCommerce's Avalara Premium integration can calculate sales taxes more accurately. Stores without Avalara Premium will ignore the code when calculating sales tax. Do not pass more than one code. The codes are case-sensitive. For details, please see Avalara's documentation. >= 0 characters <= 255 characters", - optional: true, - }, - categories: { - type: "integer[]", - label: "Categories", - description: - "An array of IDs for the categories to which this product belongs. When updating a product, if an array of categories is supplied, all product categories will be overwritten. Does not accept more than 1,000 ID values.", - optional: true, - async options({ page }) { - const categories = await this.bigcommerce.getAllCategories({ - page: page + 1, - }); - return categories.map((item) => ({ - label: item.name, - value: item.id, - })); - }, - }, - brand_id: { - type: "integer", - label: "Brand Id", - description: - "A product can be added to an existing brand during a product /PUT or /POST. >= 0 and <= 1000000000", - optional: true, - async options({ page }) { - const { data } = await this.bigcommerce.getAllBrands({ - page: page + 1, - }); - - return data.map((item) => ({ - label: item.name, - value: item.id, - })); - }, - }, - inventory_level: { - type: "integer", - label: "Inventory Level", - description: - "Current inventory level of the product. Simple inventory tracking must be enabled (See the inventory_tracking field) for this to take any effect. >= 0 and <= 1000000000", - optional: true, - }, - inventory_warning_level: { - type: "integer", - label: "Inventory Warning Level", - description: - "Inventory warning level for the product. When the product's inventory level drops below the warning level, the store owner will be informed. Simple inventory tracking must be enabled (see the inventory_tracking field) for this to take any effect. >= 0 and <= 1000000000", - optional: true, - }, - inventory_tracking: { - type: "string", - label: "Inventory Tracking", - description: - "The type of inventory tracking for the product. Values are: none - inventory levels will not be tracked; product - inventory levels will be tracked using the inventory_level and inventory_warning_level fields; variant - inventory levels will be tracked based on variants, which maintain their own warning levels and inventory levels. Allowed values: none, product, variant", - optional: true, - options: [ - "none", - "product", - "variant", - ], - }, - fixed_cost_shipping_price: { - type: "any", - label: "Fixed cost shipping price", - description: - "A fixed shipping cost for the product. If defined, this value will be used during checkout instead of normal shipping-cost calculation. >= 0", - optional: true, - }, - is_free_shipping: { - type: "boolean", - label: "Is free shipping", - description: - "Flag used to indicate whether the product has free shipping. If true, the shipping cost for the product will be zero.", - optional: true, - }, - is_visible: { - type: "boolean", - label: "Is visible", - description: - "Flag to determine whether the product should be displayed to customers browsing the store. If true, the product will be displayed. If false, the product will be hidden from view.", - optional: true, - }, - is_featured: { - type: "boolean", - label: "Is Featured", - description: - "Flag to determine whether the product should be included in the featured products panel when viewing the store.", - optional: true, - }, - related_products: { - type: "integer[]", - label: "Related Products", - description: "An array of IDs for the related products.", - optional: true, - async options({ page }) { - const { data } = await this.bigcommerce.getAllProducts({ - page: page + 1, - }); - - return data.map((item) => ({ - label: item.name, - value: item.id, - })); - }, - }, - warranty: { - type: "string", - label: "Warranty", - description: - "Warranty information displayed on the product page. Can include HTML formatting. >= 0 characters and <= 65535 characters", - optional: true, - }, - bin_picking_number: { - type: "string", - label: "Bin picking number", - description: - "The BIN picking number for the product. >= 0 characters and <= 255 characters", - optional: true, - }, - layout_file: { - type: "string", - label: "Layout File", - description: - "The layout template file used to render this product category. This field is writable only for stores with a Blueprint theme applied. >= 0 characters and <= 500 characters", - optional: true, - }, - upc: { - type: "string", - label: "UPC", - description: - "The product UPC code, which is used in feeds for shopping comparison sites and external channel integrations. >= 0 characters and <= 255 characters", - optional: true, - }, - search_keywords: { - type: "string", - label: "Seach keywords", - description: - "A comma-separated list of keywords that can be used to locate the product when searching the store. >= 0 characters and <= 65535 characters", - optional: true, - }, - availability: { - type: "string", - label: "Availability", - description: - "Availability of the product. (Corresponds to the product's Purchasability section in the control panel.) Supported values: available - the product is available for purchase; disabled - the product is listed on the storefront, but cannot be purchased; preorder - the product is listed for pre-orders. Allowed values: available, disabled or preorder", - optional: true, - options: [ - "available", - "disabled", - "preorder", - ], - }, - availability_description: { - type: "string", - label: "Availability description", - description: - "Availability text displayed on the checkout page, under the product title. Tells the customer how long it will normally take to ship this product, such as: 'Usually ships in 24 hours.' >= 0 characters and <= 255 characters", - optional: true, - }, - gift_wrapping_options_type: { - type: "string", - label: "Gift wrapping options type", - description: - "Type of gift-wrapping options. Values: any - allow any gift-wrapping options in the store; none - disallow gift-wrapping on the product; list – provide a list of IDs in the gift_wrapping_options_list field. Allowed values: any, none or list", - optional: true, - options: [ - "any", - "none", - "list", - ], - }, - gift_wrapping_options_list: { - type: "string[]", - label: "Gift wrapping options list", - description: "A list of gift-wrapping option IDs.", - optional: true, - }, - sort_order: { - type: "integer", - label: "Sort order", - description: - "Priority to give this product when included in product lists on category pages and in search results. Lower integers will place the product closer to the top of the results. >= -2147483648 and <= 2147483647", - optional: true, - }, - condition: { - type: "string", - label: "Condition", - description: - "The product condition. Will be shown on the product page if the is_condition_shown field's value is true. Possible values: New, Used, Refurbished. Allowed values: New, Used and Refurbished", - optional: true, - options: [ - "New", - "Used", - "Refurbished", - ], - }, - is_condition_shown: { - type: "boolean", - label: "Is condition shown", - description: - "Flag used to determine whether the product condition is shown to the customer on the product page.", - optional: true, - }, - order_quantity_minimum: { - type: "integer", - label: "Order quantity minimum", - description: - "The minimum quantity an order must contain, to be eligible to purchase this product. >= 0 and <= 1000000000", - optional: true, - }, - order_quantity_maximum: { - type: "integer", - label: "Order quantity maximum", - description: - "The maximum quantity an order can contain when purchasing the product. >= 0 and <= 1000000000", - optional: true, - }, - page_title: { - type: "string", - label: "Page title", - description: - "Custom title for the product page. If not defined, the product name will be used as the meta title. >= 0 characters and <= 255 characters", - optional: true, - }, - meta_keywords: { - type: "string[]", - label: "Meta keywords", - description: - "Custom meta keywords for the product page. If not defined, the store's default keywords will be used.", - optional: true, - }, - meta_description: { - type: "string", - label: "Meta description", - description: - "Custom meta description for the product page. If not defined, the store's default meta description will be used. >= 0 characters and <= 65535 characters", - optional: true, - }, - view_count: { - type: "integer", - label: "View count", - description: - "The number of times the product has been viewed. >= 0 and <= 1000000000", - optional: true, - }, - preorder_release_date: { - type: "string", - label: "Preorder release date", - description: - "Pre-order release date. See the availability field for details on setting a product's availability to accept pre-orders.", - optional: true, - }, - preorder_message: { - type: "string", - label: "Preorder message", - description: - "Custom expected-date message to display on the product page. If undefined, the message defaults to the storewide setting. Can contain the %%DATE%% placeholder, which will be substituted for the release date. >= 0 characters and <= 255 characters", - optional: true, - }, - is_preorder_only: { - type: "boolean", - label: "Is preorder only", - description: - "If set to true then on the preorder release date the preorder status will automatically be removed. If set to false, then on the release date the preorder status will not be removed. It will need to be changed manually either in the control panel or using the API. Using the API set availability to available.", - optional: true, - }, - is_price_hidden: { - type: "boolean", - label: "Is preorder hidden", - description: - "False by default, indicating that this product's price should be shown on the product page. If set to true, the price is hidden. (NOTE: To successfully set is_price_hidden to true, the availability value must be disabled.)", - optional: true, - }, - price_hidden_label: { - type: "string", - label: "Price hidden label", - description: - "By default, an empty string. If is_price_hidden is true, the value of price_hidden_label is displayed instead of the price. (NOTE: To successfully set a non-empty string value with is_price_hidden set to true, the availability value must be disabled.) >= 0 characters and <= 200 characters", - optional: true, - }, - open_graph_type: { - type: "string", - label: "Open graph type", - description: - "Type of product, defaults to product. Allowed values: product, album, book, drink, food, game, movie, song OR tv_show", - optional: true, - options: [ - "product", - "album", - "book", - "drink", - "food", - "game", - "movie", - "song", - "tv_show", - ], - }, - open_graph_title: { - type: "string", - label: "Open graph title", - description: - "Title of the product, if not specified the product name will be used instead.", - optional: true, - }, - open_graph_description: { - type: "string", - label: "Open graph description", - description: - "Description to use for the product, if not specified then the meta_description will be used instead.", - optional: true, - }, - open_graph_use_meta_description: { - type: "boolean", - label: "Open graph use meta description", - description: - "Flag to determine if product description or open graph description is used.", - optional: true, - }, - open_graph_use_product_name: { - type: "boolean", - label: "Open graph use product name", - description: - "Flag to determine if product name or open graph name is used.", - optional: true, - }, - open_graph_use_image: { - type: "boolean", - label: "Open graph use image", - description: - "Flag to determine if product image or open graph image is used.", - optional: true, - }, - brand_name: { - type: "string", - label: "Brand name", - description: - "It performs a fuzzy match and adds the brand. eg. \"Common Good\" and \"Common good\" are the same. Brand name does not return as part of a product response. Only the brand_id.", - optional: true, - }, - gtin: { - type: "string", - label: "GTIN", - description: "Global Trade Item Number", - optional: true, - }, - mpn: { - type: "string", - label: "MPN", - description: "Manufacturer Part Number", - optional: true, - }, - reviews_rating_sum: { - type: "any", - label: "Reviews rating sum", - description: "The total rating for the product.", - optional: true, - }, - reviews_count: { - type: "integer", - label: "Reviews count", - description: "The number of times the product has been rated.", - optional: true, - }, - total_sold: { - type: "integer", - label: "Total sold", - description: "The total quantity of this product sold.", - optional: true, - }, -}; - -export default PROPS; diff --git a/components/bigcommerce/common/utils.mjs b/components/bigcommerce/common/utils.mjs new file mode 100644 index 0000000000000..554048822bac0 --- /dev/null +++ b/components/bigcommerce/common/utils.mjs @@ -0,0 +1,66 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { createReadStream } from "fs"; +import FormData from "form-data"; +import constants from "./constants.mjs"; + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid JSON array object"); + } +} + +function buildFormData(formData, data, parentKey) { + if (data && typeof(data) === "object") { + Object.keys(data) + .forEach((key) => { + buildFormData(formData, data[key], parentKey && `${parentKey}[${key}]` || key); + }); + + } else if (data && constants.FILE_PROP_NAMES.some((propName) => parentKey.includes(propName))) { + formData.append(parentKey, createReadStream(data)); + + } else if (data) { + formData.append(parentKey, (data).toString()); + } +} + +function getFormData(data) { + try { + const formData = new FormData(); + buildFormData(formData, data); + return formData; + } catch (error) { + console.log("FormData Error", error); + throw error; + } +} + +function hasMultipartHeader(headers) { + return headers + && headers[constants.CONTENT_TYPE_KEY_HEADER]?. + includes(constants.MULTIPART_FORM_DATA_VALUE_HEADER); +} + +export default { + parseArray, + buildFormData, + getFormData, + hasMultipartHeader, +}; diff --git a/components/bigcommerce/package.json b/components/bigcommerce/package.json new file mode 100644 index 0000000000000..878e2f539037f --- /dev/null +++ b/components/bigcommerce/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/bigcommerce", + "version": "0.0.1", + "description": "Pipedream BigCommerce Components", + "main": "bigcommerce.app.mjs", + "keywords": [ + "pipedream", + "bigcommerce" + ], + "homepage": "https://pipedream.com/apps/bigcommerce", + "author": "Pipedream (https://pipedream.com/)", + "dependencies": { + "@pipedream/platform": "^1.6.2", + "form-data": "^4.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/components/bigcommerce/sources/custom-events/custom-events.mjs b/components/bigcommerce/sources/custom-events/custom-events.mjs index 10ffb03f099c3..68b8935bc9622 100644 --- a/components/bigcommerce/sources/custom-events/custom-events.mjs +++ b/components/bigcommerce/sources/custom-events/custom-events.mjs @@ -6,7 +6,7 @@ import common from "../common/base.mjs"; export default { ...common, name: "New Custom Events", - version: "0.0.1", + version: "0.0.2", type: "source", key: "bigcommerce-custom-events", description: "Emit new custom webhook event", diff --git a/components/bigcommerce/sources/new-customer/new-customer.mjs b/components/bigcommerce/sources/new-customer/new-customer.mjs index 250ce88e946a8..dc22ac7ae396b 100644 --- a/components/bigcommerce/sources/new-customer/new-customer.mjs +++ b/components/bigcommerce/sources/new-customer/new-customer.mjs @@ -3,7 +3,7 @@ import common from "../common/base.mjs"; export default { ...common, name: "New Customer", - version: "0.0.1", + version: "0.0.2", type: "source", key: "bigcommerce-new-customer", description: "Emit new created customer", diff --git a/components/bigcommerce/sources/new-order/new-order.mjs b/components/bigcommerce/sources/new-order/new-order.mjs index f10c8cb6a0916..e7fc0b7be07fa 100644 --- a/components/bigcommerce/sources/new-order/new-order.mjs +++ b/components/bigcommerce/sources/new-order/new-order.mjs @@ -3,7 +3,7 @@ import common from "../common/base.mjs"; export default { ...common, name: "New Order", - version: "0.0.1", + version: "0.0.2", type: "source", key: "bigcommerce-new-order", description: "Emit new created order", diff --git a/components/bigdatacorp/actions/get-address-data/get-address-data.mjs b/components/bigdatacorp/actions/get-address-data/get-address-data.mjs new file mode 100644 index 0000000000000..e22bfe896b7c6 --- /dev/null +++ b/components/bigdatacorp/actions/get-address-data/get-address-data.mjs @@ -0,0 +1,47 @@ +import app from "../../bigdatacorp.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "bigdatacorp-get-address-data", + name: "Get Address Data", + description: "Returns the available data for a Zipcode number according to the selected dataset. [See the documentation](https://docs.bigdatacorp.com.br/plataforma/reference/enderecos_legal_amazon)", + version: "0.0.2", + type: "action", + props: { + app, + doc: { + propDefinition: [ + app, + "doc", + ], + description: "Zipcode of the address you want to search for, i.e.: `88048-656`", + }, + dataset: { + propDefinition: [ + app, + "dataset", + ], + options: constants.ADDRESS_DATASETS, + }, + }, + + async run({ $ }) { + const response = await this.app.getAddressData({ + $, + data: { + Datasets: this.dataset, + q: `zipcode[${this.doc}]`, + }, + }); + + const status = response.Status[this.dataset][0].Message; + + if (status === "OK") { + $.export("$summary", `Successfully sent the request for the '${this.dataset}' dataset. Status: ${status}`); + } else { + throw new Error(status); + } + + return response; + }, +}; diff --git a/components/bigdatacorp/actions/get-company-data/get-company-data.mjs b/components/bigdatacorp/actions/get-company-data/get-company-data.mjs new file mode 100644 index 0000000000000..38b3aa41fa880 --- /dev/null +++ b/components/bigdatacorp/actions/get-company-data/get-company-data.mjs @@ -0,0 +1,47 @@ +import app from "../../bigdatacorp.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "bigdatacorp-get-company-data", + name: "Get Company Data", + description: "Returns the available data for a CNPJ number according to the selected dataset. [See the documentation](https://docs.bigdatacorp.com.br/plataforma/reference/empresas_emails_extended)", + version: "0.0.2", + type: "action", + props: { + app, + doc: { + propDefinition: [ + app, + "doc", + ], + description: "Document Number of the entity you want to search for, i.e.: `27.823.957/0001-94`", + }, + dataset: { + propDefinition: [ + app, + "dataset", + ], + options: constants.COMPANY_DATASETS, + }, + }, + + async run({ $ }) { + const response = await this.app.getCompanyData({ + $, + data: { + Datasets: this.dataset, + q: `doc{${this.doc}}`, + }, + }); + + const status = response.Status[this.dataset][0].Message; + + if (status === "OK") { + $.export("$summary", `Successfully sent the request for the '${this.dataset}' dataset. Status: ${status}`); + } else { + throw new Error(status); + } + + return response; + }, +}; diff --git a/components/bigdatacorp/actions/get-person-data/get-person-data.mjs b/components/bigdatacorp/actions/get-person-data/get-person-data.mjs new file mode 100644 index 0000000000000..15a72f86aa2d0 --- /dev/null +++ b/components/bigdatacorp/actions/get-person-data/get-person-data.mjs @@ -0,0 +1,47 @@ +import app from "../../bigdatacorp.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "bigdatacorp-get-person-data", + name: "Get Person Data", + description: "Returns the available data for a CPF number according to the selected dataset. [See the documentation](https://docs.bigdatacorp.com.br/plataforma/reference/pessoas_registration_data)", + version: "0.0.2", + type: "action", + props: { + app, + doc: { + propDefinition: [ + app, + "doc", + ], + description: "Document Number of the entity you want to search for, i.e.: `128.982.560-21`", + }, + dataset: { + propDefinition: [ + app, + "dataset", + ], + options: constants.PERSON_DATASETS, + }, + }, + + async run({ $ }) { + const response = await this.app.getPersonData({ + $, + data: { + Datasets: this.dataset, + q: `doc{${this.doc}}`, + }, + }); + + const status = response.Status[this.dataset][0].Message; + + if (status === "OK") { + $.export("$summary", `Successfully sent the request for the '${this.dataset}' dataset. Status: ${status}`); + } else { + throw new Error(status); + } + + return response; + }, +}; diff --git a/components/bigdatacorp/bigdatacorp.app.mjs b/components/bigdatacorp/bigdatacorp.app.mjs new file mode 100644 index 0000000000000..57131a5ba0ff9 --- /dev/null +++ b/components/bigdatacorp/bigdatacorp.app.mjs @@ -0,0 +1,62 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "bigdatacorp", + propDefinitions: { + doc: { + type: "string", + label: "Document Number", + description: "Document Number of the entity you want to search for", + }, + dataset: { + type: "string", + label: "Dataset", + description: "The target dataset to which the query will be sent", + }, + }, + methods: { + _baseUrl() { + return "https://plataforma.bigdatacorp.com.br"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "Accept": "application/json", + "AccessToken": `${this.$auth.access_token}`, + "TokenId": `${this.$auth.token_id}`, + }, + }); + }, + async getPersonData(args = {}) { + return this._makeRequest({ + path: "/pessoas", + method: "post", + ...args, + }); + }, + async getCompanyData(args = {}) { + return this._makeRequest({ + path: "/empresas", + method: "post", + ...args, + }); + }, + async getAddressData(args = {}) { + return this._makeRequest({ + path: "/enderecos", + method: "post", + ...args, + }); + }, + }, +}; diff --git a/components/bigdatacorp/common/constants.mjs b/components/bigdatacorp/common/constants.mjs new file mode 100644 index 0000000000000..1fd6dac89cfff --- /dev/null +++ b/components/bigdatacorp/common/constants.mjs @@ -0,0 +1,92 @@ +export default { + PERSON_DATASETS: [ + { + "value": "emails_extended", + "label": "Emails", + }, + { + "value": "phones_extended", + "label": "Phones", + }, + { + "value": "registration_data", + "label": "Registration Data", + }, + { + "value": "related_people_emails", + "label": "Related People Emails", + }, + { + "value": "related_people_phones", + "label": "Related People Phones", + }, + { + "value": "related_people_addresses", + "label": "Related People Addresses", + }, + { + "value": "vehicles", + "label": "Vehicles", + }, + ], + COMPANY_DATASETS: [ + { + "value": "emails_extended", + "label": "Emails", + }, + { + "value": "phones_extended", + "label": "Phones", + }, + { + "value": "registration_data", + "label": "Registration Data", + }, + { + "value": "related_people_emails", + "label": "Related People Emails", + }, + { + "value": "related_people_phones", + "label": "Related People Phones", + }, + { + "value": "related_people_addresses", + "label": "Related People Addresses", + }, + { + "value": "political_involvement", + "label": "Political Involvement", + }, + { + "value": "online_ads", + "label": "Online Ads", + }, + ], + ADDRESS_DATASETS: [ + { + "value": "legal_amazon", + "label": "Legal Amazon", + }, + { + "value": "environmental_preservation_areas", + "label": "Enviromental Preservation Areas", + }, + { + "value": "biomes_data", + "label": "Biomes Data", + }, + { + "value": "embargoed_areas", + "label": "Embargoed Areas", + }, + { + "value": "legal_reserve", + "label": "Legal Reserve", + }, + { + "value": "basic_data", + "label": "Basic Data", + }, + ], +}; diff --git a/components/bigdatacorp/package.json b/components/bigdatacorp/package.json new file mode 100644 index 0000000000000..a6bc72a1f5e4d --- /dev/null +++ b/components/bigdatacorp/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/bigdatacorp", + "version": "0.1.1", + "description": "Pipedream BigDataCorp Components", + "main": "bigdatacorp.app.mjs", + "keywords": [ + "pipedream", + "bigdatacorp" + ], + "homepage": "https://pipedream.com/apps/bigdatacorp", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/bigmailer/README.md b/components/bigmailer/README.md new file mode 100644 index 0000000000000..0d01113210a4d --- /dev/null +++ b/components/bigmailer/README.md @@ -0,0 +1,11 @@ +# Overview + +The BigMailer API enables you to automate your email marketing by interacting with your BigMailer account programmatically. On Pipedream, you can leverage this API to create workflows that respond to events, manage contacts, campaigns, and analyze the effectiveness of your email strategies. By connecting BigMailer to other apps on Pipedream, you can orchestrate complex automations and streamline your email operations. + +# Example Use Cases + +- **Sync New Users to BigMailer Contact List**: When a new user signs up on your platform, trigger a workflow on Pipedream that adds their details to a specified BigMailer contact list. This keeps your email lists up-to-date without manual intervention. + +- **Automate Campaign Analytics Reporting**: After an email campaign ends, use Pipedream to fetch analytics from the BigMailer API and send a detailed report to your Slack channel or Google Sheets. Here, you're combining BigMailer's analytical capabilities with Pipedream's integrations to facilitate instant reporting. + +- **Transactional Email Triggers Based on User Activity**: Set up a workflow where certain user actions in your app (like completing a purchase) trigger Pipedream to send a transactional email through BigMailer. This method ensures timely communication with customers, fostering a more engaged user experience. diff --git a/components/bigml/README.md b/components/bigml/README.md new file mode 100644 index 0000000000000..ee7e82585a92d --- /dev/null +++ b/components/bigml/README.md @@ -0,0 +1,11 @@ +# Overview + +The BigML API offers a suite of machine learning tools that enable the creation and management of datasets, models, predictions, and more. It's a powerful resource for developers looking to incorporate machine learning into their applications. Within Pipedream, you can leverage the BigML API to automate workflows, process data, and apply predictive analytics. By connecting BigML to other apps in Pipedream, you can orchestrate sophisticated data pipelines that react to events, perform analyses, and take action based on machine learning insights. + +# Example Use Cases + +- **Automated Data Analysis Pipeline**: Set up an event-driven workflow where a new data file uploaded to Google Drive triggers a Pipedream workflow. The workflow fetches the file, processes the data, and sends it to BigML to update a dataset or train a new model. Gain insights or predictions immediately without manual intervention. + +- **Real-time Prediction Service**: Create a real-time prediction service by setting up an HTTP endpoint in Pipedream. When data is posted to this endpoint, it's forwarded to BigML to generate a prediction using a pre-trained model. The prediction is then sent back as the HTTP response, allowing for real-time integration into applications or services. + +- **IoT Data Monitoring and Response**: Integrate IoT device data streams with BigML via Pipedream. Monitor sensor data in real-time, pass it to BigML for analysis, and use the results to trigger notifications or actions in other services like Slack or email. This can be used for predictive maintenance, anomaly detection, or environmental monitoring. diff --git a/components/bigpicture_io/README.md b/components/bigpicture_io/README.md new file mode 100644 index 0000000000000..c93039f875d13 --- /dev/null +++ b/components/bigpicture_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The Bigpicture.io API allows you to enrich your customer data by collecting, unifying, and streaming it to different platforms. With Pipedream, you can automate workflows that trigger on events, manipulate and route data, and integrate with numerous other services. Leveraging Bigpicture.io's API within Pipedream's serverless platform can help you create real-time, data-driven workflows for personalized marketing, analytics, customer support, and more. + +# Example Use Cases + +- **Customer Data Enrichment and Segmentation**: Automatically enrich incoming customer data from webhooks or forms with Bigpicture.io. Use this enriched data to segment customers based on their behavior or attributes and store the segments in a data warehouse like Snowflake or Google BigQuery for further analysis. + +- **Real-time Personalized Marketing Campaigns**: Trigger a workflow whenever a user performs a specific action on your website, like viewing a high-value product. Use Bigpicture.io to fetch additional user details and send this data to a marketing platform like Mailchimp or HubSpot to personalize follow-up emails or ad campaigns. + +- **Enhanced Customer Support Interactions**: When a customer submits a support ticket, use Bigpicture.io to enrich their profile with recent activity or purchase history. Integrate this workflow with a CRM like Salesforce or Zendesk to provide support agents with richer context, leading to more efficient and personalized customer service. diff --git a/components/bigpicture_io/bigpicture_io.app.mjs b/components/bigpicture_io/bigpicture_io.app.mjs new file mode 100644 index 0000000000000..66d5aff4106d3 --- /dev/null +++ b/components/bigpicture_io/bigpicture_io.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "bigpicture_io", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/bigpicture_io/package.json b/components/bigpicture_io/package.json new file mode 100644 index 0000000000000..cc9fc0b2fcc8b --- /dev/null +++ b/components/bigpicture_io/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/bigpicture_io", + "version": "0.0.1", + "description": "Pipedream Bigpicture.io Components", + "main": "bigpicture_io.app.mjs", + "keywords": [ + "pipedream", + "bigpicture_io" + ], + "homepage": "https://pipedream.com/apps/bigpicture_io", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/bilflo/README.md b/components/bilflo/README.md new file mode 100644 index 0000000000000..77f6d0bdfb46a --- /dev/null +++ b/components/bilflo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Bilflo API allows users to automate and integrate staffing and workforce management processes. By connecting Bilflo with Pipedream, users can streamline data flows between Bilflo and other business applications, enabling automated reporting, payroll processing, and seamless data synchronization across platforms. This integration can significantly enhance operational efficiencies, reduce manual data entry, and provide real-time analytics for better decision-making. + +# Example Use Cases + +- **Automated Payroll Processing**: Set up a workflow on Pipedream where hours logged in Bilflo are automatically pushed to a payroll system like ADP or Paychex. When an employee submits their timesheet, the workflow triggers, validates the data, and sends it to the payroll app, ensuring timely and accurate payroll execution. + +- **Real-time Staffing Updates to CRM**: Create a workflow that syncs new employee details or updates from Bilflo to a CRM platform such as Salesforce. Whenever there is a new hire or an update in employee status in Bilflo, the workflow automatically updates the corresponding records in Salesforce, keeping sales and service teams informed about staffing changes. + +- **Automated Compliance Reporting**: Develop a workflow where employee data from Bilflo is used to automatically generate compliance reports. This workflow could connect Bilflo to a document generation tool like Google Docs or an analytics tool like Tableau, compiling necessary compliance information and creating reports at regular intervals or on demand. diff --git a/components/bilflo/actions/assign-contract-job-to-invoice/assign-contract-job-to-invoice.mjs b/components/bilflo/actions/assign-contract-job-to-invoice/assign-contract-job-to-invoice.mjs new file mode 100644 index 0000000000000..c6ecb84ad0a8b --- /dev/null +++ b/components/bilflo/actions/assign-contract-job-to-invoice/assign-contract-job-to-invoice.mjs @@ -0,0 +1,33 @@ +import bilflo from "../../bilflo.app.mjs"; + +export default { + key: "bilflo-assign-contract-job-to-invoice", + name: "Assign Contract Job to Invoice Group", + description: "Assigns a contract job to a specified invoice group for a client. [See the documentation](https://developer.bilflo.com/documentation#operations-tag-Clients)", + version: "0.0.1", + type: "action", + props: { + bilflo, + jobId: { + type: "integer", + label: "Contract Job Identifier", + description: "The unique identifier for the contract job.", + }, + invoiceGroupId: { + type: "integer", + label: "Invoice Group Identifier", + description: "The unique identifier for the invoice group.", + }, + }, + async run({ $ }) { + const response = await this.bilflo.assignContractJobToInvoiceGroup({ + $, + data: { + jobId: this.jobId, + invoiceGroupId: this.invoiceGroupId, + }, + }); + $.export("$summary", `Successfully assigned contract job ${this.jobId} to invoice group ${this.invoiceGroupId}`); + return response; + }, +}; diff --git a/components/bilflo/actions/create-client/create-client.mjs b/components/bilflo/actions/create-client/create-client.mjs new file mode 100644 index 0000000000000..eeb120268d1bd --- /dev/null +++ b/components/bilflo/actions/create-client/create-client.mjs @@ -0,0 +1,27 @@ +import bilflo from "../../bilflo.app.mjs"; + +export default { + key: "bilflo-create-client", + name: "Create Client", + description: "Creates a new client account in Bilflo. [See the documentation](https://developer.bilflo.com/documentation#operations-tag-Clients)", + version: "0.0.1", + type: "action", + props: { + bilflo, + businessName: { + type: "string", + label: "Business Name", + description: "The name of the business for the new client account.", + }, + }, + async run({ $ }) { + const response = await this.bilflo.createClient({ + $, + data: { + businessName: this.businessName, + }, + }); + $.export("$summary", `Successfully created new client account with Id: ${response.data.clientId}`); + return response; + }, +}; diff --git a/components/bilflo/actions/create-contract-job/create-contract-job.mjs b/components/bilflo/actions/create-contract-job/create-contract-job.mjs new file mode 100644 index 0000000000000..d92ed39753e83 --- /dev/null +++ b/components/bilflo/actions/create-contract-job/create-contract-job.mjs @@ -0,0 +1,93 @@ +import bilflo from "../../bilflo.app.mjs"; + +export default { + key: "bilflo-create-contract-job", + name: "Create Contract Job", + description: "Creates a new contract job in Bilflo. [See the documentation](https://developer.bilflo.com/documentation)", + version: "0.0.1", + type: "action", + props: { + bilflo, + clientId: { + propDefinition: [ + bilflo, + "clientId", + ], + }, + contractorId: { + type: "integer", + label: "Contractor ID", + description: "The unique identifier for the contractor.", + }, + contractorTypeId: { + type: "integer", + label: "Contractor Type ID", + description: "The unique identifier for the contractor type.", + options: [ + { + label: "W2", + value: 1, + }, + { + label: "1099", + value: 2, + }, + ], + }, + timeCardMethodId: { + type: "integer", + label: "Time Card Method ID", + description: "The unique identifier for the time card method.", + }, + overtimeRuleId: { + type: "integer", + label: "Overtime Rule ID", + description: "The unique identifier for the overtime rule.", + }, + jobTitle: { + type: "string", + label: "Job Title", + description: "The title of the job.", + }, + startDate: { + type: "string", + label: "Start Date", + description: "The start date of the contract job. **Format YYYY-MM-DDTHH:MM:SSZ**", + }, + endDate: { + type: "string", + label: "End Date", + description: "The end date of the contract job. **Format YYYY-MM-DDTHH:MM:SSZ**", + }, + firstWeekEndingDate: { + type: "string", + label: "First Week Ending Date", + description: "The first week ending date of the contract job. **Format YYYY-MM-DDTHH:MM:SSZ**", + }, + burdenTypeId: { + type: "integer", + label: "Burden Type ID", + description: "The unique identifier for the burden type.", + }, + }, + async run({ $ }) { + const response = await this.bilflo.createContractJob({ + $, + data: { + clientId: this.clientId, + contractorId: this.contractorId, + contractorTypeId: this.contractorTypeId, + timeCardMethodId: this.timeCardMethodId, + overtimeRuleId: this.overtimeRuleId, + jobTitle: this.jobTitle, + startDate: this.startDate, + endDate: this.endDate, + firstWeekEndingDate: this.firstWeekEndingDate, + burdenTypeId: this.burdenTypeId, + }, + }); + + $.export("$summary", `Successfully created contract job Id: ${response.data.jobId}`); + return response; + }, +}; diff --git a/components/bilflo/bilflo.app.mjs b/components/bilflo/bilflo.app.mjs new file mode 100644 index 0000000000000..116dd928b5d6a --- /dev/null +++ b/components/bilflo/bilflo.app.mjs @@ -0,0 +1,69 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "bilflo", + propDefinitions: { + clientId: { + type: "string", + label: "Client ID", + description: "The unique identifier for the client.", + async options() { + const { data } = await this.listClients(); + + return data.map(({ + clientId: value, businessName: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.bilflo.com/v1"; + }, + _headers() { + return { + "company-id": this.$auth.company_id, + "Authorization": `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createClient(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/Clients", + ...opts, + }); + }, + listClients() { + return this._makeRequest({ + path: "/Clients", + }); + }, + assignContractJobToInvoiceGroup(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/Clients/contractInvoiceGroups/assignContractJob", + ...opts, + }); + }, + createContractJob(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/ContractJobs", + ...opts, + }); + }, + }, +}; diff --git a/components/bilflo/package.json b/components/bilflo/package.json new file mode 100644 index 0000000000000..def44d80c96e4 --- /dev/null +++ b/components/bilflo/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/bilflo", + "version": "0.1.0", + "description": "Pipedream Bilflo Components", + "main": "bilflo.app.mjs", + "keywords": [ + "pipedream", + "bilflo" + ], + "homepage": "https://pipedream.com/apps/bilflo", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} + diff --git a/components/bilionis/README.md b/components/bilionis/README.md new file mode 100644 index 0000000000000..53755b7429b08 --- /dev/null +++ b/components/bilionis/README.md @@ -0,0 +1,11 @@ +# Overview + +The Bilionis API offers a suite of functionalities for managing and analyzing customer interactions and sales data. Within Pipedream, you can leverage this API to automate workflows involving customer relationship management and data analysis. By connecting Bilionis with other apps, you streamline operations such as syncing customer data across platforms, triggering personalized marketing campaigns, and generating data-driven insights to inform business strategies. + +# Example Use Cases + +- **Customer Data Synchronization**: Sync customer data from Bilionis to a CRM like Salesforce. Each time a new customer is added or updated in Bilionis, the workflow triggers an update or creates a new record in Salesforce, ensuring your sales team has the latest information at their fingertips. + +- **Automated Marketing Campaigns**: Trigger email campaigns in Mailchimp based on customer behavior tracked by Bilionis. For instance, when a customer completes a purchase, Pipedream can automate a "Thank You" email from Mailchimp, or start a new nurture sequence if they browse a product category. + +- **Sales Reporting and Insights**: Compile sales reports by aggregating data from Bilionis and visualizing it with Google Sheets. Set up a workflow on Pipedream that regularly fetches sales data from Bilionis, processes it, and updates a Google Sheet, giving you real-time access to sales performance metrics. diff --git a/components/bill/README.md b/components/bill/README.md new file mode 100644 index 0000000000000..066aa005445f7 --- /dev/null +++ b/components/bill/README.md @@ -0,0 +1,11 @@ +# Overview + +The BILL API enables automation of accounts payable and receivable processes. With this API on Pipedream, you can create workflows that streamline financial operations, sync invoices, and manage transactions. Pipedream's serverless platform allows you to connect BILL with various other apps to trigger actions based on events, ensuring seamless financial data handling. + +# Example Use Cases + +- **Automate Invoice Processing**: Trigger a workflow in Pipedream when a new invoice is created in BILL, and automatically send an email notification to relevant parties using the Gmail integration. This keeps everyone informed about new invoices without manual effort. + +- **Vendor Payment Confirmation**: Set up a Pipedream workflow that listens for a payment status update in BILL, and upon successful payment, notifies the vendor via Slack and logs the transaction in a Google Sheets document for record-keeping. + +- **Expense Monitoring**: Create a Pipedream workflow that triggers on a regular schedule to fetch recent transactions from BILL, analyze expenses, and post a summary to a designated Slack channel to help maintain budget oversight. diff --git a/components/billplz/billplz.app.mjs b/components/billplz/billplz.app.mjs new file mode 100644 index 0000000000000..9edd507d0b6b9 --- /dev/null +++ b/components/billplz/billplz.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "billplz", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/billplz/package.json b/components/billplz/package.json new file mode 100644 index 0000000000000..a29214302d43e --- /dev/null +++ b/components/billplz/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/billplz", + "version": "0.0.1", + "description": "Pipedream Billplz Components", + "main": "billplz.app.mjs", + "keywords": [ + "pipedream", + "billplz" + ], + "homepage": "https://pipedream.com/apps/billplz", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/billsby/README.md b/components/billsby/README.md new file mode 100644 index 0000000000000..9cb3d87f67f1f --- /dev/null +++ b/components/billsby/README.md @@ -0,0 +1,11 @@ +# Overview + +The Billsby API allows for automated subscription management, providing endpoints to create and manage customer accounts, subscriptions, invoices, and payments. With Pipedream, you can connect Billsby to hundreds of other services, triggering workflows based on subscription events, syncing customer data, or automating billing processes. This can lead to more efficient operations, better customer experiences, and timely data-driven decisions. + +# Example Use Cases + +- **Automate Customer Onboarding**: When a new customer signs up via Billsby, trigger a Pipedream workflow to add the customer to a CRM like Salesforce, send a personalized welcome email through SendGrid, and create a Slack notification for the sales team. + +- **Sync Subscription Status with a Database**: Keep a real-time sync between Billsby subscription statuses and a database like Google Sheets or Airtable. When a subscription changes in Billsby (like an upgrade or cancellation), the corresponding record in your database updates automatically. + +- **Handle Failed Payments**: Set up a workflow that triggers when a payment fails in Billsby. The workflow could email the customer with updated payment instructions via a service like Mailgun, alert your support team in a tool like Zendesk, and even pause the customer's subscription until the payment issue is resolved. diff --git a/components/bingx/README.md b/components/bingx/README.md index dd56ebedb61de..be9ae5315c940 100644 --- a/components/bingx/README.md +++ b/components/bingx/README.md @@ -1,3 +1,11 @@ # Overview -The Bingx API enables developers to build powerful applications and custom solutions using the Bingx platform. With the Bingx API, developers can access the full range of Bingx features and functionality, including search, ads, and social. +The BingX API offers a digital asset trading platform that enables users to perform crypto trades, access market data, and manage their portfolio. By leveraging this API on Pipedream, you can automate your cryptocurrency trading strategies, synchronize your trading data with other financial tools, and create real-time alerts based on market conditions. + +# Example Use Cases + +- **Automated Trading Execution**: Set up a Pipedream workflow that monitors market data from BingX and automatically executes trades based on predefined criteria such as price thresholds or technical indicators. Integrate with messaging platforms like Slack to receive immediate notifications upon trade execution. + +- **Portfolio Synchronization**: Create a workflow that periodically retrieves your trading history and portfolio data from BingX and syncs it with Google Sheets. Use this to maintain an up-to-date record of all transactions and current positions for analysis or reporting purposes. + +- **Real-Time Market Alerts**: Implement a Pipedream workflow that subscribes to market data updates from BingX and uses conditions to trigger alerts. Connect with Twilio to send SMS messages or with email services like SendGrid to notify you when certain market conditions are met, such as drastic price changes or high trading volume. diff --git a/components/bippybox/actions/activate-box/activate-box.mjs b/components/bippybox/actions/activate-box/activate-box.mjs new file mode 100644 index 0000000000000..7c8859fc44a33 --- /dev/null +++ b/components/bippybox/actions/activate-box/activate-box.mjs @@ -0,0 +1,39 @@ +import app from "../../bippybox.app.mjs"; + +export default { + key: "bippybox-activate-box", + name: "Activate Box", + description: "Triggers the BippyBox to play an audio file. [See the documentation](https://bippybox.io/docs/).", + version: "0.0.1", + type: "action", + props: { + app, + device: { + type: "string", + label: "Device", + description: "The device identifier. Eg. `DEVICE123`.", + }, + url: { + type: "string", + label: "URL", + description: "The URL of the audio file to play. Eg. `https://storage.example.com/users/exampleUserUID67890/audio/SampleAudioFile.wav?alt=media&token=exampleToken123456`.", + }, + }, + async run({ $ }) { + const { + app, + device, + url, + } = this; + + const response = await app.activateBox({ + $, + data: { + device, + URL: url, + }, + }); + $.export("$summary", "Successfully activated BippyBox."); + return response; + }, +}; diff --git a/components/bippybox/bippybox.app.mjs b/components/bippybox/bippybox.app.mjs new file mode 100644 index 0000000000000..52c5cd0c53e20 --- /dev/null +++ b/components/bippybox/bippybox.app.mjs @@ -0,0 +1,46 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "bippybox", + methods: { + getUrl(path) { + return `https://websocket.bippybox.io${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Content-Type": "application/json", + "x-api-key": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + activateBox({ + data, ...args + } = {}) { + const { uid } = this.$auth; + return this.post({ + path: "/send", + data: { + ...data, + uid, + }, + ...args, + }); + }, + }, +}; diff --git a/components/bippybox/package.json b/components/bippybox/package.json new file mode 100644 index 0000000000000..c69829f985d38 --- /dev/null +++ b/components/bippybox/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/bippybox", + "version": "0.1.0", + "description": "Pipedream BippyBox Components", + "main": "bippybox.app.mjs", + "keywords": [ + "pipedream", + "bippybox" + ], + "homepage": "https://pipedream.com/apps/bippybox", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" + } +} diff --git a/components/bit_io/README.md b/components/bit_io/README.md index 911901c5aea08..7d12b1537c058 100644 --- a/components/bit_io/README.md +++ b/components/bit_io/README.md @@ -1,9 +1,11 @@ # Overview -The Bit.io API lets you build applications that interact with the Bit.io platform. Using the API, you can do things like: +bit.io is a modern database as a service platform that provides a straightforward way to share, manage, and collaborate on data sets. By leveraging the bit.io API on Pipedream, you can automate interactions with your databases, such as triggering queries in response to external events, syncing data between various applications, or maintaining your datasets with scheduled tasks. -- Query information about Bit.io accounts and transactions -- Create and manage Bit.io addresses -- Send and receive Bit.io payments -- Generate Bit.io invoices -- Integrate Bit.io into your own applications +# Example Use Cases + +- **Automated Reporting and Data Aggregation**: Integrate bit.io with Google Sheets on Pipedream to periodically run complex SQL queries and populate the results in a spreadsheet for easy reporting and analysis. This can be used to amalgamate data from various sources for business intelligence purposes. + +- **Dynamic Data Syncing Between Services**: Utilize webhooks from services like Shopify to trigger workflows that update inventory statistics in your bit.io database. This ensures real-time data accuracy across your e-commerce platform and backend databases, streamlining inventory management. + +- **Event-Driven Database Backup**: Set up a Pipedream workflow that reacts to a GitHub webhook. Whenever you push code to a particular branch, the workflow can dump a snapshot of your database to cloud storage like Google Drive or Dropbox, providing an on-demand backup service. diff --git a/components/bit_io/package.json b/components/bit_io/package.json new file mode 100644 index 0000000000000..bb667a2602faf --- /dev/null +++ b/components/bit_io/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/bit_io", + "version": "0.6.0", + "description": "Pipedream bit_io Components", + "main": "bit_io.app.mjs", + "keywords": [ + "pipedream", + "bit_io" + ], + "homepage": "https://pipedream.com/apps/bit_io", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/bitbadges/bitbadges.app.mjs b/components/bitbadges/bitbadges.app.mjs new file mode 100644 index 0000000000000..f8306fe27c091 --- /dev/null +++ b/components/bitbadges/bitbadges.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "bitbadges", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/bitbadges/package.json b/components/bitbadges/package.json new file mode 100644 index 0000000000000..871087529482d --- /dev/null +++ b/components/bitbadges/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/bitbadges", + "version": "0.0.1", + "description": "Pipedream BitBadges Components", + "main": "bitbadges.app.mjs", + "keywords": [ + "pipedream", + "bitbadges" + ], + "homepage": "https://pipedream.com/apps/bitbadges", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/bitbucket/README.md b/components/bitbucket/README.md index bc7b86e80a6c3..174466ae77d4b 100644 --- a/components/bitbucket/README.md +++ b/components/bitbucket/README.md @@ -1,14 +1,11 @@ # Overview -With the BitBucket API, you can programmatically interact with BitBucket, via -REST calls. This allows you to build apps and integrations on top of BitBucket, -customize and automate your workflows, and much more. - -Here are some examples of what you can build with the BitBucket API: - -- Build integrations with 3rd party applications, such as CI/CD tools, chat - applications, etc. -- Create custom workflows and automate tasks -- Build applications that interact with BitBucket (for example, a tool that - helps you find repositories or users) -- and much more! +The BitBucket API taps the potential of BitBucket's Git-based version control system, enabling you to automate workflows around code commits, pull requests, and overall repository management. With this API, you can streamline the collaboration process, enforce coding standards, or integrate with other tools to create a cohesive development ecosystem. Pipedream, as a serverless integration and compute platform, provides a seamless environment to connect BitBucket with various apps and services, enabling you to harness its API for efficient, customized automations. + +# Example Use Cases + +- **Automated Code Quality Checks**: Trigger a workflow on Pipedream whenever a new commit is pushed to a BitBucket repository. The workflow could then run code quality checks using tools like ESLint or SonarQube, and report the results back as comments on the commit or pull request in BitBucket. + +- **Deployment Automation**: Set up a Pipedream workflow to deploy the latest code pushed to a specific BitBucket branch. On push events, the workflow can trigger deployment scripts on platforms like AWS, Google Cloud, or Azure, and notify your team via Slack or email upon successful deployment. + +- **Syncing Issues Across Platforms**: Create a workflow that synchronizes BitBucket issues with an external project management tool like Jira or Trello. Whenever an issue is created or updated in BitBucket, it reflects on the connected platform in real-time, ensuring seamless tracking and management of development tasks. diff --git a/components/bitbucket_data_center/bitbucket_data_center.app.mjs b/components/bitbucket_data_center/bitbucket_data_center.app.mjs new file mode 100644 index 0000000000000..a193358cf0c4a --- /dev/null +++ b/components/bitbucket_data_center/bitbucket_data_center.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "bitbucket_data_center", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/bitbucket_data_center/package.json b/components/bitbucket_data_center/package.json new file mode 100644 index 0000000000000..452bfdfc8b348 --- /dev/null +++ b/components/bitbucket_data_center/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/bitbucket_data_center", + "version": "0.0.1", + "description": "Pipedream Bitbucket Data Center Components", + "main": "bitbucket_data_center.app.mjs", + "keywords": [ + "pipedream", + "bitbucket_data_center" + ], + "homepage": "https://pipedream.com/apps/bitbucket_data_center", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/components/bitdefender_gravityzone/bitdefender_gravityzone.app.mjs b/components/bitdefender_gravityzone/bitdefender_gravityzone.app.mjs new file mode 100644 index 0000000000000..711b49f8a7886 --- /dev/null +++ b/components/bitdefender_gravityzone/bitdefender_gravityzone.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "bitdefender_gravityzone", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/bitdefender_gravityzone/package.json b/components/bitdefender_gravityzone/package.json new file mode 100644 index 0000000000000..1fc42aaeb4516 --- /dev/null +++ b/components/bitdefender_gravityzone/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/bitdefender_gravityzone", + "version": "0.0.1", + "description": "Pipedream Bitdefender GravityZone Components", + "main": "bitdefender_gravityzone.app.mjs", + "keywords": [ + "pipedream", + "bitdefender_gravityzone" + ], + "homepage": "https://pipedream.com/apps/bitdefender_gravityzone", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/bitly/README.md b/components/bitly/README.md index 23f58745630c5..49889765ac58f 100644 --- a/components/bitly/README.md +++ b/components/bitly/README.md @@ -1,3 +1,11 @@ # Overview -The Bitly API allows developers to build apps that interact with the Bitly URL shortening service. With the API, developers can create new short links, expand short links, and retrieve the clicks data for short links. +The Bitly API enables you to programmatically shorten URLs, track click rates, and manage link performance metrics. By integrating Bitly with Pipedream, you can automate various tasks around link management and amalgamate Bitly’s capabilities with other services, thereby streamlining digital marketing efforts, enhancing social media strategies, and monitoring engagement through data-driven insights. + +# Example Use Cases + +- **Social Media Post Scheduler**: Automate the scheduling of social media posts with shortened URLs on platforms like Twitter or Facebook. When a new blog post is published, use Pipedream to trigger Bitly to shorten the URL, and then schedule the post with the shortened link to go live at peak engagement times. + +- **Marketing Campaign Tracker**: Create a workflow that automatically shortens URLs for different marketing campaigns and channels. Tag the links for analytics, then feed data into a Google Sheet or a dashboard app like Geckoboard. Monitor campaign performance in real-time, and adjust strategies based on click-through rates and user engagement. + +- **Customer Support Automation**: Set up a system where support team can generate personalized Bitly links within a CRM platform like Salesforce or HubSpot when sending support materials or knowledge base articles to customers. Track which resources are most used and improve resource allocation and support documentation based on usage patterns. diff --git a/components/bitly/package.json b/components/bitly/package.json new file mode 100644 index 0000000000000..8442975541828 --- /dev/null +++ b/components/bitly/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/bitly", + "version": "0.6.0", + "description": "Pipedream bitly Components", + "main": "bitly.app.mjs", + "keywords": [ + "pipedream", + "bitly" + ], + "homepage": "https://pipedream.com/apps/bitly", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/bitport/README.md b/components/bitport/README.md new file mode 100644 index 0000000000000..616a9364e3474 --- /dev/null +++ b/components/bitport/README.md @@ -0,0 +1,11 @@ +# Overview + +The Bitport API allows you to integrate cloud torrent downloads into your apps and workflows. With Bitport, you can fetch torrents to a secure cloud location and stream them online. This opens up possibilities for automating downloads, orchestrating media collection, or even building custom alerts for completed torrent downloads. On Pipedream, you can connect the Bitport API to various other services to create powerful automation workflows. For instance, you could trigger downloads with webhooks, organize files with cloud storage services, or send notifications through communication platforms. + +# Example Use Cases + +- **Automated Torrent Downloading on New RSS Feed Item**: When an RSS feed updates with new content that you're tracking (say, a weekly podcast or video series), you could automatically initiate a torrent download to your Bitport cloud storage. This way, you ensure you never miss an episode and can access it securely from anywhere. + +- **Organizing Downloaded Media with Cloud Storage**: After a torrent download is complete, you can move the file to specific folders in Google Drive or Dropbox based on the media type or other characteristics. This helps keep your cloud storage organized without manual intervention. + +- **Notification System for Completed Downloads**: Set up a workflow that sends a notification via email, SMS, or a platform like Slack whenever a Bitport download finishes. This alert can help you stay informed about your downloads without constantly checking the status manually. diff --git a/components/bitport/bitport.app.mjs b/components/bitport/bitport.app.mjs new file mode 100644 index 0000000000000..d67f960bdc012 --- /dev/null +++ b/components/bitport/bitport.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "bitport", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/bitport/package.json b/components/bitport/package.json new file mode 100644 index 0000000000000..ff54ba4d24b36 --- /dev/null +++ b/components/bitport/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/bitport", + "version": "0.0.1", + "description": "Pipedream Bitport Components", + "main": "bitport.app.mjs", + "keywords": [ + "pipedream", + "bitport" + ], + "homepage": "https://pipedream.com/apps/bitport", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/bitquery/README.md b/components/bitquery/README.md new file mode 100644 index 0000000000000..fcb074cf6a622 --- /dev/null +++ b/components/bitquery/README.md @@ -0,0 +1,11 @@ +# Overview + +The Bitquery API offers deep insights into blockchain data across multiple chains. With it, you can extract transaction details, smart contract interactions, and wallet addresses' behavior. When paired with Pipedream, you can automate blockchain-related tasks, create triggers based on on-chain events, or integrate blockchain data into other applications or services. Analyzing trends, monitoring wallets, and real-time alerting for transactions become seamless with Bitquery on Pipedream. + +# Example Use Cases + +- **Crypto Market Analytics Dashboard**: Construct a serverless dashboard that aggregates and displays cryptocurrency market trends by leveraging Bitquery's API to fetch data, such as token price movements, volume, and liquidity information across various exchanges. Refresh the data periodically with Pipedream's scheduled workflows to keep the dashboard updated. + +- **Wallet Transaction Monitoring**: Set up a Pipedream workflow that triggers on a schedule to check a list of wallet addresses using Bitquery's API. When a new transaction is detected, process and send a notification via email, SMS, or a messaging app like Slack. This enables real-time monitoring of wallets for unusual activities or significant transfers. + +- **Automated Trading Signals**: Develop a system that uses Bitquery's API to scan blockchain networks for specific trading indicators or patterns. Connect this with a trading platform's API on Pipedream to execute trades automatically whenever the defined conditions are met, such as a sudden increase in token transfers, which may indicate market movement. diff --git a/components/bitquery/bitquery.app.mjs b/components/bitquery/bitquery.app.mjs new file mode 100644 index 0000000000000..142b490cf1760 --- /dev/null +++ b/components/bitquery/bitquery.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "bitquery", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/bitquery/package.json b/components/bitquery/package.json new file mode 100644 index 0000000000000..f17005f551ef8 --- /dev/null +++ b/components/bitquery/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/bitquery", + "version": "0.0.1", + "description": "Pipedream Bitquery Components", + "main": "bitquery.app.mjs", + "keywords": [ + "pipedream", + "bitquery" + ], + "homepage": "https://pipedream.com/apps/bitquery", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/bitwarden/README.md b/components/bitwarden/README.md index 9617083a11523..93d878fa9389a 100644 --- a/components/bitwarden/README.md +++ b/components/bitwarden/README.md @@ -1,9 +1,11 @@ # Overview -Bitwarden is a secure and free password manager that can be used to store passwords, credit card information, and other sensitive data. With the Bitwarden API, developers can create applications that allow users to securely store and access their data. +The Bitwarden API provides a gateway to interact with your secure vault programmatically, enabling the automation of credential management and security operations. Through Pipedream, you can leverage this API to create workflows that enhance your security practices by integrating with other services, triggering actions based on vault events, or even conducting periodic security audits. By harnessing the Bitwarden API, you can streamline password rotation, audit access, and synchronize secrets across your applications automatically. -Some examples of what can be built using the Bitwarden API include: +# Example Use Cases -- A password manager that allows users to securely store and access their passwords -- A credit card manager that allows users to securely store and access their credit card information -- A secure notes manager that allows users to securely store and access their notes +- **Automated Password Rotation**: Rotate passwords stored in Bitwarden automatically by connecting to services like AWS or GitHub. Pipedream can schedule a workflow that updates credentials at regular intervals, ensuring compliance with security policies without manual intervention. + +- **Security Alerts Integration**: Trigger notifications or actions in response to Bitwarden events. For instance, set up a workflow that listens for failed login attempts and automatically pushes alerts to Slack, informing your security team immediately of potential threats. + +- **Secrets Synchronization Across Environments**: Keep application secrets in sync across different environments like production, staging, or development. Use Pipedream to detect changes in Bitwarden and propagate updates to services like Kubernetes, automatically updating environment variables or secrets in your infrastructure. diff --git a/components/bitwarden/package.json b/components/bitwarden/package.json new file mode 100644 index 0000000000000..c404e0e6e08cf --- /dev/null +++ b/components/bitwarden/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/bitwarden", + "version": "0.6.0", + "description": "Pipedream bitwarden Components", + "main": "bitwarden.app.mjs", + "keywords": [ + "pipedream", + "bitwarden" + ], + "homepage": "https://pipedream.com/apps/bitwarden", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/biztera/README.md b/components/biztera/README.md index abf07807397f7..383c322695977 100644 --- a/components/biztera/README.md +++ b/components/biztera/README.md @@ -1,3 +1,11 @@ # Overview -Biztera is a powerful API that enables developers to create sophisticated applications for business purposes. With Biztera, developers can access data from a variety of sources, including financial data, customer data, and product data. In addition, developers can use Biztera to create applications that can be used to automate business processes. +The Biztera API allows users to automate their decision-making processes and approval workflows, offering features like requesting approvals, tracking decision status, and integrating with other business tools. Leveraging the Biztera API on Pipedream, you can craft serverless workflows that streamline how approvals are managed within your organization, trigger notifications based on decision changes, and synchronize Biztera data with other systems such as CRMs, project management tools, or custom databases. + +# Example Use Cases + +- **Automated Approval Notifications**: Create a workflow on Pipedream that triggers when a new approval request is submitted in Biztera. Use this trigger to send a notification via email, Slack, or SMS to the relevant stakeholders, ensuring that they are promptly informed and can take action quickly. + +- **Syncing Decisions with Project Management**: Whenever a decision is made within Biztera, a Pipedream workflow can automatically update the status of a corresponding task or project in a tool like Asana, Trello, or Jira. This keeps project management tightly aligned with internal approvals and ensures that team members are aware of the latest decisions impacting their work. + +- **Conditional Approval Escalations**: Construct a Pipedream workflow that monitors decision status in Biztera and applies conditional logic to escalate unaddressed or overdue approvals. If an approval is pending beyond a certain timeframe, the workflow could alert a manager or re-route the request to an alternative approver, helping to prevent bottlenecks in the approval process. diff --git a/components/biztera/package.json b/components/biztera/package.json new file mode 100644 index 0000000000000..24a379ca8f821 --- /dev/null +++ b/components/biztera/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/biztera", + "version": "0.6.0", + "description": "Pipedream biztera Components", + "main": "biztera.app.mjs", + "keywords": [ + "pipedream", + "biztera" + ], + "homepage": "https://pipedream.com/apps/biztera", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/blackbaud/README.md b/components/blackbaud/README.md index 93af315e08f7c..b18f04627f3c2 100644 --- a/components/blackbaud/README.md +++ b/components/blackbaud/README.md @@ -1,9 +1,11 @@ # Overview -The Blackbaud API lets you build a variety of applications that work with Blackbaud data. Here are some examples: +The Blackbaud API offers programmable access to a suite of services for educational institutions, nonprofits, and foundations, focusing on fundraising, relationship management, marketing, and financial functions. Leveraging this API on Pipedream allows for automated interactions with donor records, campaign metrics, and financial data, simplifying tasks like synchronizing donor information, triggering communications based on donations, or reporting financial insights to stakeholders. -- An application that displays a list of your organization's donors -- A donor management tool that allows you to add, update, and delete donors -- A tool that allows you to import donor data from a CSV file -- An application that generates reports on your donor data -- A donor portal that allows donors to login and view their giving history +# Example Use Cases + +- **Automated Donor Thank-You Emails**: After a new donation is recorded via Blackbaud, trigger an automated, personalized thank-you email through a service like SendGrid. This maintains donor engagement and appreciation with minimal manual effort. + +- **Real-Time Donation Analytics Dashboard**: Use Pipedream to send new donation data from Blackbaud to a Google Sheets document as it comes in. Create a live dashboard that provides an up-to-date analysis of fundraising campaigns, helping to inform strategy decisions on the fly. + +- **Monthly Financial Reporting Automation**: Set up a monthly workflow that consolidates financial data from Blackbaud, formats it, and then sends a comprehensive report to a designated Slack channel or via email to key stakeholders, ensuring timely and consistent financial updates. diff --git a/components/blackbaud/package.json b/components/blackbaud/package.json new file mode 100644 index 0000000000000..9b15e0e90e7ad --- /dev/null +++ b/components/blackbaud/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/blackbaud", + "version": "0.6.0", + "description": "Pipedream blackbaud Components", + "main": "blackbaud.app.mjs", + "keywords": [ + "pipedream", + "blackbaud" + ], + "homepage": "https://pipedream.com/apps/blackbaud", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/bland_ai/README.md b/components/bland_ai/README.md new file mode 100644 index 0000000000000..4f151107f2702 --- /dev/null +++ b/components/bland_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +The Bland AI API offers a suite of artificial intelligence services, which, when integrated with Pipedream, can automate tasks, analyze data, and enhance applications with machine learning capabilities. Through Pipedream's serverless platform, you can connect Bland AI with various apps to create custom workflows, process data, and respond to events in real time without managing infrastructure. + +# Example Use Cases + +- **Automated Sentiment Analysis on Social Media Posts**: Set up a workflow where Bland AI analyzes the sentiment of incoming social media posts from platforms like Twitter or Facebook. Use the sentiment data to trigger actions like sending alerts, categorizing customer feedback, or flagging posts for follow-up. + +- **Enhanced Customer Support with AI-powered Chatbots**: Integrate Bland AI with customer support platforms such as Zendesk or Intercom. Automate initial customer interactions using AI chatbots, routing complex issues to human agents, and tagging conversations based on their content for improved service analytics. + +- **Content Recommendation Engine**: Build a content recommendation system by connecting Bland AI with a CMS like WordPress. Analyze user behavior and content metadata to provide personalized content suggestions, increasing engagement and time spent on your site. diff --git a/components/blazemeter/README.md b/components/blazemeter/README.md new file mode 100644 index 0000000000000..c13e546ec0305 --- /dev/null +++ b/components/blazemeter/README.md @@ -0,0 +1,11 @@ +# Overview + +The BlazeMeter API allows you to automate performance testing by integrating with Pipedream's serverless platform. You can trigger tests, fetch test results, and manage your testing environment programmatically. With Pipedream, connecting BlazeMeter with other apps and services streamlines performance data analysis and alerts, enhancing continuous integration and deployment (CI/CD) pipelines. + +# Example Use Cases + +- **Automated Performance Test Triggering**: Trigger BlazeMeter performance tests automatically after code commits using a webhook from GitHub. This ensures your application undergoes necessary testing with each update, maintaining performance standards. + +- **Performance Test Result Analysis and Reporting**: After a test run, use Pipedream to send the results to Google Sheets for easy analysis and visualization. Set up alerts through Slack or email if performance metrics fall below a certain threshold, keeping your team informed. + +- **Dynamic Test Configuration Based on Real-time Data**: Adjust test parameters dynamically by using incoming data from a monitoring tool like Datadog. This allows stress testing to reflect real-world scenarios, ensuring your application can handle unexpected traffic spikes. diff --git a/components/blazemeter/actions/create-project/create-project.mjs b/components/blazemeter/actions/create-project/create-project.mjs new file mode 100644 index 0000000000000..39d9f5ff91ab9 --- /dev/null +++ b/components/blazemeter/actions/create-project/create-project.mjs @@ -0,0 +1,53 @@ +import app from "../../blazemeter.app.mjs"; + +export default { + key: "blazemeter-create-project", + name: "Create Project", + description: "Creates a new project in a specific workspace. [See the documentation](https://api.blazemeter.com/functional/#create-a-project)", + version: "0.0.1", + type: "action", + props: { + app, + accountId: { + propDefinition: [ + app, + "accountId", + ], + }, + workspaceId: { + propDefinition: [ + app, + "workspaceId", + (c) => ({ + accountId: c.accountId, + }), + ], + }, + projectName: { + propDefinition: [ + app, + "projectName", + ], + }, + projectDescription: { + propDefinition: [ + app, + "projectDescription", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createProject({ + $, + data: { + name: this.projectName, + description: this.projectDescription, + workspaceId: this.workspaceId, + }, + }); + + $.export("$summary", `Successfully created project '${this.projectName}' in workspace ${this.workspaceId}`); + + return response; + }, +}; diff --git a/components/blazemeter/actions/list-projects/list-projects.mjs b/components/blazemeter/actions/list-projects/list-projects.mjs new file mode 100644 index 0000000000000..6a79151aeff52 --- /dev/null +++ b/components/blazemeter/actions/list-projects/list-projects.mjs @@ -0,0 +1,39 @@ +import app from "../../blazemeter.app.mjs"; + +export default { + key: "blazemeter-list-projects", + name: "List Projects", + description: "List projects from a specified workspace in BlazeMeter. [See the documentation](https://api.blazemeter.com/functional/#projects-list)", + version: "0.0.1", + type: "action", + props: { + app, + accountId: { + propDefinition: [ + app, + "accountId", + ], + }, + workspaceId: { + propDefinition: [ + app, + "workspaceId", + (c) => ({ + accountId: c.accountId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.app.listProjects({ + $, + params: { + workspaceId: this.workspaceId, + }, + }); + + $.export("$summary", `Successfully listed ${response.result.length} projects in the workspace`); + + return response; + }, +}; diff --git a/components/blazemeter/actions/list-workspaces/list-workspaces.mjs b/components/blazemeter/actions/list-workspaces/list-workspaces.mjs new file mode 100644 index 0000000000000..4048780348bec --- /dev/null +++ b/components/blazemeter/actions/list-workspaces/list-workspaces.mjs @@ -0,0 +1,30 @@ +import app from "../../blazemeter.app.mjs"; + +export default { + key: "blazemeter-list-workspaces", + name: "List Workspaces", + description: "List all workspaces associated with the specified account. [See the documentation](https://api.blazemeter.com/functional/#workspaces-list)", + version: "0.0.1", + type: "action", + props: { + app, + accountId: { + propDefinition: [ + app, + "accountId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.listWorkspaces({ + $, + params: { + accountId: this.accountId, + }, + }); + + $.export("$summary", `Successfully listed ${response.result.length} workspace(s)`); + + return response; + }, +}; diff --git a/components/blazemeter/blazemeter.app.mjs b/components/blazemeter/blazemeter.app.mjs new file mode 100644 index 0000000000000..d6e1b37268c9e --- /dev/null +++ b/components/blazemeter/blazemeter.app.mjs @@ -0,0 +1,101 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "blazemeter", + propDefinitions: { + accountId: { + type: "string", + label: "Account ID", + description: "The ID of the account", + async options() { + const response = await this.listAccounts({}); + const accountIDs = response.result; + return accountIDs.map(({ + id, name, + }) => ({ + value: id, + label: name, + })); + }, + }, + workspaceId: { + type: "string", + label: "Workspace ID", + description: "The ID of the workspace to retrieve projects from.", + async options({ accountId }) { + const { result: resources } = await this.listWorkspaces({ + params: { + accountId, + }, + }); + return resources.map(({ + id, name, + }) => ({ + value: id, + label: name, + })); + }, + }, + projectName: { + type: "string", + label: "Project Name", + description: "The name of the project.", + }, + projectDescription: { + type: "string", + label: "Project Description", + description: "A description for the project.", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://a.blazemeter.com/api/v4"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + auth, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + params, + auth: { + ...auth, + username: `${this.$auth.api_key}`, + password: `${this.$auth.api_secret}`, + }, + }); + }, + async listProjects(args = {}) { + return this._makeRequest({ + path: "/projects", + ...args, + }); + }, + async listWorkspaces(args = {}) { + return this._makeRequest({ + path: "/workspaces", + ...args, + }); + }, + async createProject(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/projects", + ...args, + }); + }, + async listAccounts(args = {}) { + return this._makeRequest({ + path: "/accounts", + ...args, + }); + }, + }, +}; diff --git a/components/blazemeter/package.json b/components/blazemeter/package.json new file mode 100644 index 0000000000000..bafa55e9479e6 --- /dev/null +++ b/components/blazemeter/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/blazemeter", + "version": "0.1.0", + "description": "Pipedream Blazemeter Components", + "main": "blazemeter.app.mjs", + "keywords": [ + "pipedream", + "blazemeter" + ], + "homepage": "https://pipedream.com/apps/blazemeter", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/blink/README.md b/components/blink/README.md new file mode 100644 index 0000000000000..634c5f9898b7b --- /dev/null +++ b/components/blink/README.md @@ -0,0 +1,11 @@ +# Overview + +The Blink API allows you to access and manage data within the Blink platform, a hub for team communication and content sharing. Through Pipedream, you can automate workflows that leverage this API to streamline notifications, content distribution, and team interactions. You can create actions that respond to specific triggers, like new messages or updates to content, and connect Blink with other apps to enrich the functionality of your workflows. + +# Example Use Cases + +- **Automated Team Notifications**: Set up a workflow that listens for updates from a project management tool like Trello or Asana. When a task is marked as complete, automatically post a notification in the relevant Blink feed to keep the team informed. + +- **Content Sharing Pipeline**: Create a workflow that monitors a cloud storage service like Dropbox or Google Drive for new files. When a new file is added, share a link to the file automatically in a Blink group or 1:1 chat, ensuring that team members have immediate access to the latest resources. + +- **Event-Driven Reminders**: Use the Blink API to send reminders or follow-ups in Blink chats. Trigger this workflow by scheduling events in Google Calendar; when an event is coming up or a deadline is near, the workflow can post a reminder message in the appropriate Blink feed. diff --git a/components/blockchain_exchange/README.md b/components/blockchain_exchange/README.md new file mode 100644 index 0000000000000..c12ef9dc38c62 --- /dev/null +++ b/components/blockchain_exchange/README.md @@ -0,0 +1,11 @@ +# Overview + +With the Blockchain Exchange API, you can interact with the Blockchain Exchange platform programmatically to automate trading strategies, monitor market data, and manage accounts. This robust API allows you to execute trades, retrieve real-time and historical market data, and access account information within Pipedream's serverless platform. Pipedream provides a no-code, low-code environment where you can connect the Blockchain Exchange API with hundreds of other apps to create custom, automated workflows. + +# Example Use Cases + +- **Real-Time Trade Execution**: Trigger a workflow that executes a trade on Blockchain Exchange when a specific cryptocurrency reaches a certain price. Combine the API with the Pipedream Cron Scheduler to periodically check the price and execute trades at optimal moments. + +- **Market Data Alerts**: Use the Blockchain Exchange API to monitor market data and send alerts. Set up a Pipedream workflow to watch for significant market changes and automatically notify you via Slack or send an email through SendGrid, ensuring you never miss an important market move. + +- **Automated Portfolio Management**: Create a workflow that connects to Google Sheets and the Blockchain Exchange API to maintain an up-to-date portfolio. Automatically pull in balances and recent transactions, record them in a spreadsheet, and even calculate portfolio performance over time. diff --git a/components/blocknative/README.md b/components/blocknative/README.md new file mode 100644 index 0000000000000..22ad3c6cedb1d --- /dev/null +++ b/components/blocknative/README.md @@ -0,0 +1,11 @@ +# Overview + +The Blocknative API is a powerful tool for real-time Ethereum blockchain monitoring. It allows developers to track transactions with precision, getting notified of state changes, from mempool to confirmation. By leveraging this with Pipedream's capabilities, you can create automated workflows that respond to events like transaction status updates, address activity, and gas price changes. This can be particularly useful for applications that need to react instantly to on-chain activities, such as trading bots, wallet services, or notification systems. + +# Example Use Cases + +- **Transaction Status Monitoring**: Set up a workflow that listens for transaction events from Blocknative API and sends updates via Slack or email when a transaction involving a watched Ethereum address is confirmed or fails. This can provide real-time alerts for users tracking their transactions. + +- **Smart Contract Interaction Tracker**: Create a workflow to monitor interactions with a specific smart contract. When the Blocknative API detects a new transaction to the contract, use Pipedream to process the data and store transaction details in a Google Sheets document for analysis. This is ideal for developers who want to audit interactions with their deployed contracts. + +- **Dynamic Gas Price Adjustment**: Leverage Blocknative API to watch for changes in the Ethereum gas price. Use a Pipedream workflow to automatically adjust the gas price settings in a cryptocurrency trading app, ensuring that your transactions are timely and cost-efficient. Connect this with a database like Airtable to log gas price trends over time. diff --git a/components/blocknative/blocknative.app.mjs b/components/blocknative/blocknative.app.mjs new file mode 100644 index 0000000000000..0688869ad205e --- /dev/null +++ b/components/blocknative/blocknative.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "blocknative", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/blocknative/package.json b/components/blocknative/package.json new file mode 100644 index 0000000000000..2759d0483151e --- /dev/null +++ b/components/blocknative/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/blocknative", + "version": "0.0.1", + "description": "Pipedream Blocknative Components", + "main": "blocknative.app.mjs", + "keywords": [ + "pipedream", + "blocknative" + ], + "homepage": "https://pipedream.com/apps/blocknative", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/blogger/README.md b/components/blogger/README.md index 77c4c939d5918..cd5d9f32777c7 100644 --- a/components/blogger/README.md +++ b/components/blogger/README.md @@ -1,3 +1,11 @@ # Overview -With the Blogger API, you can create or manage your own blog on Blogger. You can also use the API to manage comments on your blog. +The Blogger API grants the power to manage your blog's content, comments, and settings programmatically. With Pipedream, you can automate content creation, streamline publishing workflows, and integrate your blog with a multitude of other services to expand its capabilities. Think real-time content updates from social media inputs, analytics-driven publishing, or automated content syndication. Harness the Blogger API on Pipedream to craft dynamic blogging ecosystems that respond to your audience's engagement and your creative inputs, keeping your content fresh and your operations efficient. + +# Example Use Cases + +- **Automated Blog Post from Social Media**: Whenever you post on platforms like Twitter or Instagram, Pipedream triggers a workflow that creates a new blog post on Blogger with the content of your social media post. Ideal for cross-posting and extending reach. + +- **Scheduled Post Publishing**: Use Google Calendar events to schedule blog posts. When an event starts, a Pipedream workflow publishes a pre-drafted post in Blogger. It's perfect for maintaining a consistent content calendar. + +- **Comment Moderation Alerts**: Set up a workflow that monitors new comments on your Blogger posts. When a comment meets certain criteria (e.g., contains specific keywords or surpasses a sentiment score threshold), Pipedream sends an alert via email, Slack, or another communication platform, allowing for prompt moderation. diff --git a/components/blogify/blogify.app.mjs b/components/blogify/blogify.app.mjs new file mode 100644 index 0000000000000..2c0e77469cd68 --- /dev/null +++ b/components/blogify/blogify.app.mjs @@ -0,0 +1,40 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "blogify", + propDefinitions: {}, + methods: { + _baseUrl() { + return "https://api.blogify.ai/public-api/v1"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/subscribe", + ...opts, + }); + }, + deleteWebhook(opts = {}) { + return this._makeRequest({ + method: "DELETE", + path: "/subscribe", + ...opts, + }); + }, + }, +}; diff --git a/components/blogify/package.json b/components/blogify/package.json new file mode 100644 index 0000000000000..f09666ae32d95 --- /dev/null +++ b/components/blogify/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/blogify", + "version": "0.1.0", + "description": "Pipedream Blogify Components", + "main": "blogify.app.mjs", + "keywords": [ + "pipedream", + "blogify" + ], + "homepage": "https://pipedream.com/apps/blogify", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/blogify/sources/new-blog-instant/new-blog-instant.mjs b/components/blogify/sources/new-blog-instant/new-blog-instant.mjs new file mode 100644 index 0000000000000..62c94bb7ae772 --- /dev/null +++ b/components/blogify/sources/new-blog-instant/new-blog-instant.mjs @@ -0,0 +1,48 @@ +import blogify from "../../blogify.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "blogify-new-blog-instant", + name: "New Blog (Instant)", + description: "Emit new events when a blog is created. [See the documentation](https://blogify.ai/developer/docs)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + blogify, + http: { + type: "$.interface.http", + customResponse: false, + }, + }, + methods: { + filterEvent() { + return true; + }, + }, + hooks: { + async activate() { + await this.blogify.createWebhook({ + data: { + hookUrl: this.http.endpoint, + }, + }); + }, + async deactivate() { + await this.blogify.deleteWebhook({ + data: { + hookUrl: this.http.endpoint, + }, + }); + }, + }, + async run({ body }) { + const ts = Date.parse(new Date()); + this.$emit(body, { + id: `${body.title}- ${ts}`, + summary: `New blog: ${body.title}`, + ts: ts, + }); + }, + sampleEmit, +}; diff --git a/components/blogify/sources/new-blog-instant/test-event.mjs b/components/blogify/sources/new-blog-instant/test-event.mjs new file mode 100644 index 0000000000000..bc25b6fa63661 --- /dev/null +++ b/components/blogify/sources/new-blog-instant/test-event.mjs @@ -0,0 +1,13 @@ +export default { + "title": "Blog Title", + "content": "

Blog content

", + "blogKeywords": [ + "PDF files", + "downloading PDFs", + "viewing PDFs", + "printing PDFs", + "Adobe Acrobat" + ], + "draft": true, + "image": "" +} \ No newline at end of file diff --git a/components/bloom_growth/README.md b/components/bloom_growth/README.md new file mode 100644 index 0000000000000..53360865c7807 --- /dev/null +++ b/components/bloom_growth/README.md @@ -0,0 +1,11 @@ +# Overview + +The Bloom Growth API provides a suite of tools for business management and growth, focusing on customer relationships, project management, and data analysis. With Pipedream, you can leverage this API to create automated workflows that integrate Bloom Growth's functionalities with other apps, streamline processes, and enhance data-driven decision-making. + +# Example Use Cases + +- **Automated Lead Capture to CRM**: Capture leads from various sources like web forms, emails, and social media, and automatically add them to the Bloom Growth CRM. This ensures that potential customer information is organized and accessible for follow-up. + +- **Project Management Automation**: Trigger workflows in Pipedream when new projects are created in Bloom Growth. For example, you could automate the creation of related tasks in a project management app like Trello or Asana, assigning team members and setting deadlines based on project details. + +- **Data Sync and Analytics**: Keep your business data in sync by connecting Bloom Growth with data analytics platforms like Google Sheets or Tableau. Automatically export new sales or customer data to these platforms for real-time analysis and reporting, helping you make informed decisions quickly. diff --git a/components/blue/README.md b/components/blue/README.md new file mode 100644 index 0000000000000..c49d9a40b6200 --- /dev/null +++ b/components/blue/README.md @@ -0,0 +1,11 @@ +# Overview + +The Blue API enables developers to access a range of services provided by Blue, such as managing customer accounts, processing payments, and handling other financial transactions. Leveraging this API within Pipedream allows you to create powerful, serverless workflows that automate these financial operations, integrate with other apps, and react to events all in real-time. This can lead to significant time savings and efficiency improvements in financial processes. + +# Example Use Cases + +- **Automated Payment Processing**: Automatically process payments when new orders are received in your e-commerce platform. Upon order creation in Shopify, trigger a workflow that uses the Blue API to charge the customer's payment method, then update the order status. + +- **Fraud Detection Alerting**: Build a workflow that monitors transactions via the Blue API. Use conditional logic to check for irregular patterns or amounts, and if a suspicious transaction is detected, send an alert through Slack or email to your team for immediate review. + +- **Customer Onboarding Automation**: When a new user signs up on your platform, use a Pipedream workflow to create a customer account in Blue. Then, integrate with a CRM like Salesforce to log the new customer details, and kick off an onboarding email sequence using a service like SendGrid. diff --git a/components/bluecart_api/README.md b/components/bluecart_api/README.md new file mode 100644 index 0000000000000..ac5217671d10d --- /dev/null +++ b/components/bluecart_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The BlueCart API offers a suite of features for streamlining the wholesale ordering process between buyers and sellers. By leveraging this API, you can automate order placements, track shipments, manage inventory, and analyze sales data to drive business decisions. In Pipedream, you can create serverless workflows that interact with the BlueCart API to integrate real-time order data with other apps, trigger custom notifications, or synchronize inventory across platforms. + +# Example Use Cases + +- **Order Processing Automation**: Automatically process new orders from BlueCart by integrating with the BlueCart API on Pipedream. Whenever a new order is placed, a Pipedream workflow can be triggered, which could, for example, update an inventory management system and send a confirmation email to the customer. + +- **Shipment Tracking Updates**: Use Pipedream to set up a workflow that listens for shipment status updates from the BlueCart API. With this workflow, you could notify a Slack channel or update a record in a Google Sheet whenever an order's shipping status changes, ensuring that all team members stay informed. + +- **Sales Data Analysis and Reporting**: Implement a scheduled Pipedream workflow that fetches sales data from the BlueCart API at regular intervals. Connect this data to a Google BigQuery dataset for advanced analysis, and generate automated reports that are sent to stakeholders via email to support data-driven decision-making. diff --git a/components/bluecart_api/bluecart_api.app.mjs b/components/bluecart_api/bluecart_api.app.mjs new file mode 100644 index 0000000000000..230ce209ee7f2 --- /dev/null +++ b/components/bluecart_api/bluecart_api.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "bluecart_api", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/bluecart_api/package.json b/components/bluecart_api/package.json new file mode 100644 index 0000000000000..c4776206a4b8d --- /dev/null +++ b/components/bluecart_api/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/bluecart_api", + "version": "0.0.1", + "description": "Pipedream BlueCart API Components", + "main": "bluecart_api.app.mjs", + "keywords": [ + "pipedream", + "bluecart_api" + ], + "homepage": "https://pipedream.com/apps/bluecart_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/bluesky/actions/create-post/create-post.mjs b/components/bluesky/actions/create-post/create-post.mjs new file mode 100644 index 0000000000000..62fe277d45cd6 --- /dev/null +++ b/components/bluesky/actions/create-post/create-post.mjs @@ -0,0 +1,74 @@ +import app from "../../bluesky.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "bluesky-create-post", + name: "Create Post", + description: "Creates a new post on Bluesky. [See the documentation](https://docs.bsky.app/docs/api/com-atproto-repo-create-record).", + version: "0.0.2", + type: "action", + props: { + app, + text: { + type: "string", + label: "Text", + description: "The text content of the post.", + }, + }, + methods: { + parseUrls(text) { + const spans = []; + const urlRegex = /(?:[$|\W])(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*[-a-zA-Z0-9@%_+~#//=])?)/g; + + let match; + while ((match = urlRegex.exec(text)) !== null) { + spans.push({ + start: match.index + 1, + end: urlRegex.lastIndex, + url: match[1], + }); + } + return spans; + }, + parseFacets(text) { + const facets = []; + for (const link of this.parseUrls(text)) { + facets.push({ + index: { + byteStart: link["start"], + byteEnd: link["end"], + }, + features: [ + { + ["$type"]: "app.bsky.richtext.facet#link", + uri: link["url"], + }, + ], + }); + } + return facets; + }, + }, + async run({ $ }) { + const { + app, + text, + } = this; + + const response = await app.createRecord({ + $, + data: { + collection: constants.RESOURCE_TYPE.POST, + record: { + ["$type"]: constants.RESOURCE_TYPE.POST, + text, + facets: this.parseFacets(text), + createdAt: new Date().toISOString(), + }, + }, + }); + + $.export("$summary", `Successfully created a new post with uri \`${response.uri}\`.`); + return response; + }, +}; diff --git a/components/bluesky/actions/like-post/like-post.mjs b/components/bluesky/actions/like-post/like-post.mjs new file mode 100644 index 0000000000000..cdaae7eb8d3b3 --- /dev/null +++ b/components/bluesky/actions/like-post/like-post.mjs @@ -0,0 +1,61 @@ +import app from "../../bluesky.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "bluesky-like-post", + name: "Like Post", + description: "Like a specific post on Bluesky. [See the documentation](https://docs.bsky.app/docs/api/com-atproto-repo-create-record).", + version: "0.0.1", + type: "action", + props: { + app, + postUrl: { + propDefinition: [ + app, + "postUrl", + ], + }, + }, + async run({ $ }) { + const { + app, + postUrl, + } = this; + + const { + handle, + postId, + } = app.getHandleAndPostIdFromUrl(postUrl); + + const { + uri, + cid, + } = await app.getRecord({ + $, + params: { + repo: handle, + collection: constants.RESOURCE_TYPE.POST, + rkey: postId, + }, + }); + + const response = await app.createRecord({ + $, + data: { + collection: constants.RESOURCE_TYPE.LIKE, + record: { + ["$type"]: constants.RESOURCE_TYPE.LIKE, + createdAt: new Date().toISOString(), + subject: { + uri, + cid, + py_type: "com.atproto.repo.strongRef", + }, + }, + }, + }); + + $.export("$summary", "Successfully liked post."); + return response; + }, +}; diff --git a/components/bluesky/actions/retrieve-thread/retrieve-thread.mjs b/components/bluesky/actions/retrieve-thread/retrieve-thread.mjs new file mode 100644 index 0000000000000..ed0a4ee9e300a --- /dev/null +++ b/components/bluesky/actions/retrieve-thread/retrieve-thread.mjs @@ -0,0 +1,73 @@ +import app from "../../bluesky.app.mjs"; + +export default { + key: "bluesky-retrieve-thread", + name: "Retrieve Thread", + description: "Retrieve a full thread of posts. [See the documentation](https://docs.bsky.app/docs/api/app-bsky-feed-get-post-thread).", + version: "0.0.1", + type: "action", + props: { + app, + postUrl: { + propDefinition: [ + app, + "postUrl", + ], + }, + depth: { + type: "integer", + label: "Depth", + description: "How many levels of reply depth should be included in response. Default is `6`.", + optional: true, + max: 100, + }, + parentHeight: { + type: "integer", + label: "Parent Height", + description: "How many levels of parent (and grandparent, etc) post to include. Default is `80`.", + optional: true, + max: 100, + }, + }, + methods: { + getPostThread(args = {}) { + return this.app._makeRequest({ + path: "/app.bsky.feed.getPostThread", + ...args, + }); + }, + }, + async run({ $ }) { + const { + app, + getPostThread, + postUrl, + depth, + parentHeight, + } = this; + + const { + handle, + postId, + } = app.getHandleAndPostIdFromUrl(postUrl); + + const { did } = await app.resolveHandle({ + $, + params: { + handle, + }, + }); + + const response = await getPostThread({ + $, + params: { + uri: app.getPostUri(postId, did), + depth, + parentHeight, + }, + }); + + $.export("$summary", "Successfully retrieved thread."); + return response; + }, +}; diff --git a/components/bluesky/bluesky.app.mjs b/components/bluesky/bluesky.app.mjs new file mode 100644 index 0000000000000..4f74ce123e284 --- /dev/null +++ b/components/bluesky/bluesky.app.mjs @@ -0,0 +1,174 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; + +export default { + type: "app", + app: "bluesky", + propDefinitions: { + postUrl: { + type: "string", + label: "Post URL", + description: "The URL will look like `https://bsky.app/profile/myhandle.bsky.social/post/3le7x3qgmaw23`.", + }, + authorId: { + type: "string", + label: "Author ID", + description: "The ID of the author to track posts.", + }, + accountId: { + type: "string", + label: "Account ID", + description: "The ID of the account to monitor for new followers.", + }, + }, + methods: { + getHandleAndPostIdFromUrl(postUrl) { + const match = postUrl?.match(constants.HANDLE_AND_POST_ID_REGEX); + if (!match) { + throw new Error("Invalid post URL"); + } + const { + handle, + postId, + } = match.groups; + + return { + handle, + postId, + }; + }, + getPostUri(postId, did = this.getDID()) { + return `at://${did}/${constants.RESOURCE_TYPE.POST}/${postId}`; + }, + getDID() { + return this.$auth.did; + }, + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + createRecord(args = {}) { + return this.post({ + path: "/com.atproto.repo.createRecord", + ...args, + data: { + ...args.data, + repo: this.getDID(), + }, + }); + }, + getRecord(args = {}) { + return this._makeRequest({ + path: "/com.atproto.repo.getRecord", + ...args, + }); + }, + resolveHandle(args = {}) { + return this._makeRequest({ + path: "/com.atproto.identity.resolveHandle", + ...args, + }); + }, + getAuthorFeed(args = {}) { + return this._makeRequest({ + path: "/app.bsky.feed.getAuthorFeed", + ...args, + }); + }, + getTimeline(args = {}) { + return this._makeRequest({ + path: "/app.bsky.feed.getTimeline", + ...args, + }); + }, + getFollowers(args = {}) { + return this._makeRequest({ + path: "/app.bsky.graph.getFollowers", + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, resourceName, + lastDateAt, dateField, + max = constants.DEFAULT_MAX, + }) { + let cursor; + let resourcesCount = 0; + const firstRun = !lastDateAt; + + while (true) { + const response = await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + cursor, + limit: constants.DEFAULT_LIMIT, + }, + }); + + const nextResources = utils.getNestedProperty(response, resourceName); + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + const isLastDateGreater = lastDateAt + && Date.parse(lastDateAt) > Date.parse(utils.getNestedProperty(resource, dateField)); + + if (isLastDateGreater) { + console.log(`Last date is greater than the current resource date in ${dateField}`); + return; + } + + if (!isLastDateGreater) { + yield resource; + resourcesCount += 1; + } + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + if (firstRun) { + console.log("First run: only one request processed"); + return; + } + + if (nextResources.length < constants.DEFAULT_LIMIT) { + console.log("No next page found"); + return; + } + + cursor = response.cursor; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); + }, + }, +}; diff --git a/components/bluesky/common/constants.mjs b/components/bluesky/common/constants.mjs new file mode 100644 index 0000000000000..578245b441bc0 --- /dev/null +++ b/components/bluesky/common/constants.mjs @@ -0,0 +1,41 @@ +const BASE_URL = "https://bsky.social"; +const VERSION_PATH = "/xrpc"; + +const INTERACTION_EVENT = { + REQUES_TLESS: "app.bsky.feed.defs#requestLess", + REQUEST_MORE: "app.bsky.feed.defs#requestMore", + CLICK_THROUGH_ITEM: "app.bsky.feed.defs#clickthroughItem", + CLICK_THROUGH_AUTHOR: "app.bsky.feed.defs#clickthroughAuthor", + CLICK_THROUGH_REPOSTER: "app.bsky.feed.defs#clickthroughReposter", + CLICK_THROUGH_EMBED: "app.bsky.feed.defs#clickthroughEmbed", + INTERACTION_SEEN: "app.bsky.feed.defs#interactionSeen", + INTERACTION_LIKE: "app.bsky.feed.defs#interactionLike", + INTERACTION_REPOST: "app.bsky.feed.defs#interactionRepost", + INTERACTION_REPLY: "app.bsky.feed.defs#interactionReply", + INTERACTION_QUOTE: "app.bsky.feed.defs#interactionQuote", + INTERACTION_SHARE: "app.bsky.feed.defs#interactionShare", +}; + +const RESOURCE_TYPE = { + POST: "app.bsky.feed.post", + LIKE: "app.bsky.feed.like", +}; + +const HANDLE_AND_POST_ID_REGEX = /(?:https?:\/\/)?(?:www\.)?(?:[^/]+)\/profile\/(?[^/]+)\/post\/(?[^/]+)/; + +const DEFAULT_LIMIT = 3; +const DEFAULT_MAX = 600; +const IS_FIRST_RUN = "isFirstRun"; +const LAST_DATE_AT = "lastDateAt"; + +export default { + BASE_URL, + VERSION_PATH, + INTERACTION_EVENT, + RESOURCE_TYPE, + HANDLE_AND_POST_ID_REGEX, + DEFAULT_LIMIT, + DEFAULT_MAX, + IS_FIRST_RUN, + LAST_DATE_AT, +}; diff --git a/components/bluesky/common/utils.mjs b/components/bluesky/common/utils.mjs new file mode 100644 index 0000000000000..de7ee6c4a4692 --- /dev/null +++ b/components/bluesky/common/utils.mjs @@ -0,0 +1,17 @@ +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +function getNestedProperty(obj, propertyString) { + const properties = propertyString.split("."); + return properties.reduce((prev, curr) => prev?.[curr], obj); +} + +export default { + iterate, + getNestedProperty, +}; diff --git a/components/bluesky/package.json b/components/bluesky/package.json new file mode 100644 index 0000000000000..4b510b117d1fd --- /dev/null +++ b/components/bluesky/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/bluesky", + "version": "0.1.1", + "description": "Pipedream Bluesky Components", + "main": "bluesky.app.mjs", + "keywords": [ + "pipedream", + "bluesky" + ], + "homepage": "https://pipedream.com/apps/bluesky", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" + } +} diff --git a/components/bluesky/sources/common/polling.mjs b/components/bluesky/sources/common/polling.mjs new file mode 100644 index 0000000000000..0a24e4b463dcf --- /dev/null +++ b/components/bluesky/sources/common/polling.mjs @@ -0,0 +1,110 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../bluesky.app.mjs"; +import constants from "../../common/constants.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + deploy() { + this.setIsFirstRun(true); + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + setIsFirstRun(value) { + this.db.set(constants.IS_FIRST_RUN, value); + }, + getIsFirstRun() { + return this.db.get(constants.IS_FIRST_RUN); + }, + setLastDateAt(value) { + this.db.set(constants.LAST_DATE_AT, value); + }, + getLastDateAt() { + return this.db.get(constants.LAST_DATE_AT); + }, + getDateField() { + throw new ConfigurationError("getDateField is not implemented"); + }, + getResourceName() { + throw new ConfigurationError("getResourceName is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + }, + async run() { + const { + app, + getDateField, + getLastDateAt, + getResourcesFn, + getResourcesFnArgs, + getResourceName, + processResource, + getIsFirstRun, + setIsFirstRun, + setLastDateAt, + } = this; + + const isFirstRun = getIsFirstRun(); + const dateField = getDateField(); + const lastDateAt = getLastDateAt(); + + const otherArgs = isFirstRun + ? { + max: constants.DEFAULT_LIMIT, + } + : { + dateField, + lastDateAt, + }; + + const resources = await app.paginate({ + resourcesFn: getResourcesFn(), + resourcesFnArgs: getResourcesFnArgs(), + resourceName: getResourceName(), + ...otherArgs, + }); + + if (resources.length) { + const [ + firstResource, + ] = Array.from(resources); + if (firstResource) { + setLastDateAt(utils.getNestedProperty(firstResource, dateField)); + } + } + + Array.from(resources) + .forEach(processResource); + + if (isFirstRun) { + setIsFirstRun(false); + } + }, +}; diff --git a/components/bluesky/sources/new-follower-on-account/new-follower-on-account.mjs b/components/bluesky/sources/new-follower-on-account/new-follower-on-account.mjs new file mode 100644 index 0000000000000..a679a42795575 --- /dev/null +++ b/components/bluesky/sources/new-follower-on-account/new-follower-on-account.mjs @@ -0,0 +1,49 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "bluesky-new-follower-on-account", + name: "New Follower On Account", + description: "Emit new event when someone follows the specified account. Requires the account ID as a prop to monitor followers for that account. [See the documentation](https://docs.bsky.app/docs/api/app-bsky-graph-get-followers).", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + accountId: { + propDefinition: [ + common.props.app, + "accountId", + ], + }, + }, + methods: { + ...common.methods, + getDateField() { + return "createdAt"; + }, + getResourceName() { + return "followers"; + }, + getResourcesFn() { + return this.app.getFollowers; + }, + getResourcesFnArgs() { + return { + debug: true, + params: { + actor: this.accountId, + }, + }; + }, + generateMeta(resource) { + return { + id: resource.did, + summary: `New Follower ${resource.handle}`, + ts: Date.parse(resource.createdAt), + }; + }, + }, + sampleEmit, +}; diff --git a/components/bluesky/sources/new-follower-on-account/test-event.mjs b/components/bluesky/sources/new-follower-on-account/test-event.mjs new file mode 100644 index 0000000000000..26e8dd25184ec --- /dev/null +++ b/components/bluesky/sources/new-follower-on-account/test-event.mjs @@ -0,0 +1,13 @@ +export default { + "did": "did:plc:rwanjd2ci6wnzxmxbjqibfdsdl", + "handle": "test.bsky.social", + "displayName": "", + "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:rwdfasdfa@jpeg", + "viewer": { + "muted": false, + "blockedBy": false + }, + "labels": [], + "createdAt": "2024-12-25T17:22:17.844Z", + "indexedAt": "2024-12-25T17:22:17.844Z" +}; diff --git a/components/bluesky/sources/new-posts-by-author/new-posts-by-author.mjs b/components/bluesky/sources/new-posts-by-author/new-posts-by-author.mjs new file mode 100644 index 0000000000000..e1c706e048c0a --- /dev/null +++ b/components/bluesky/sources/new-posts-by-author/new-posts-by-author.mjs @@ -0,0 +1,50 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "bluesky-new-posts-by-author", + name: "New Posts By Author", + description: "Emit new event when an author creates a post. Requires the author id as a prop to track posts from a specific author. [See the documentation](https://docs.bsky.app/docs/api/app-bsky-feed-search-posts).", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + authorId: { + propDefinition: [ + common.props.app, + "authorId", + ], + }, + }, + methods: { + ...common.methods, + getDateField() { + return "post.record.createdAt"; + }, + getResourceName() { + return "feed"; + }, + getResourcesFn() { + return this.app.getAuthorFeed; + }, + getResourcesFnArgs() { + return { + debug: true, + params: { + actor: this.authorId, + }, + }; + }, + generateMeta(resource) { + const { post } = resource; + return { + id: post.cid, + summary: `New Post at ${post.record.createdAt}`, + ts: Date.parse(post.record.createdAt), + }; + }, + }, + sampleEmit, +}; diff --git a/components/bluesky/sources/new-posts-by-author/test-event.mjs b/components/bluesky/sources/new-posts-by-author/test-event.mjs new file mode 100644 index 0000000000000..0ca423d0a1a09 --- /dev/null +++ b/components/bluesky/sources/new-posts-by-author/test-event.mjs @@ -0,0 +1,137 @@ +export default { + post: { + uri: "at://did:plc:fakeOne/app.bsky.feed.post/mockPostId", + cid: "bafyreimachangedxxxyyyzzz", + author: { + did: "did:plc:fakeOne", + handle: "john_doe.bsky.social", + displayName: "John Doe", + avatar: "https://mock.cdn/avatar/plain/did:plc:fakeOne/bafkremockavatar@jpeg", + viewer: { + muted: false, + blockedBy: false, + following: "at://did:plc:someOtherDid/app.bsky.graph.follow/mockFollow" + }, + labels: [], + createdAt: "2025-01-01T08:00:00.000Z", + }, + record: { + $type: "app.bsky.feed.post", + createdAt: "2025-01-01T12:00:00.000Z", + langs: ["en"], + reply: { + parent: { + cid: "bafyreimockparentxyz", + uri: "at://did:plc:someoneElseDid/app.bsky.feed.post/mockParentPost" + }, + root: { + cid: "bafyreimockrootabc", + uri: "at://did:plc:fakeOne/app.bsky.feed.post/mockRootPost" + } + }, + text: "Mock text for demonstration of the post content structure.", + }, + replyCount: 0, + repostCount: 0, + likeCount: 1, + quoteCount: 0, + indexedAt: "2025-01-01T12:00:10.000Z", + viewer: { + threadMuted: false, + embeddingDisabled: false, + }, + labels: [], + }, + reply: { + root: { + $type: "app.bsky.feed.defs#postView", + uri: "at://did:plc:fakeOne/app.bsky.feed.post/mockRootPost", + cid: "bafyreimockrootabc", + author: { + did: "did:plc:fakeOne", + handle: "john_doe.bsky.social", + displayName: "John Doe", + avatar: "https://mock.cdn/avatar/plain/did:plc:fakeOne/bafkremockavatar@jpeg", + viewer: { + muted: false, + blockedBy: false, + following: "at://did:plc:someOtherDid/app.bsky.graph.follow/mockFollow" + }, + labels: [], + createdAt: "2025-01-01T08:00:00.000Z" + }, + record: { + $type: "app.bsky.feed.post", + createdAt: "2025-01-01T09:00:00.000Z", + langs: ["en"], + text: "Mock text to show how the root reply might look.", + }, + replyCount: 5, + repostCount: 2, + likeCount: 10, + quoteCount: 0, + indexedAt: "2025-01-01T09:00:05.000Z", + viewer: { + threadMuted: false, + embeddingDisabled: false, + }, + labels: [], + }, + parent: { + $type: "app.bsky.feed.defs#postView", + uri: "at://did:plc:someoneElseDid/app.bsky.feed.post/mockParentPost", + cid: "bafyreimockparentxyz", + author: { + did: "did:plc:someoneElseDid", + handle: "alice_example.bsky.social", + displayName: "Alice Example", + avatar: "https://mock.cdn/avatar/plain/did:plc:someoneElseDid/bafkremockavatar@jpeg", + viewer: { + muted: false, + blockedBy: false, + }, + labels: [], + createdAt: "2025-01-01T07:00:00.000Z" + }, + record: { + $type: "app.bsky.feed.post", + createdAt: "2025-01-01T10:00:00.000Z", + langs: ["en"], + reply: { + parent: { + cid: "bafyreimockrootabc", + uri: "at://did:plc:fakeOne/app.bsky.feed.post/mockRootPost" + }, + root: { + cid: "bafyreimockrootabc", + uri: "at://did:plc:fakeOne/app.bsky.feed.post/mockRootPost" + } + }, + text: "Mock text showing how a parent comment might appear in the structure." + }, + replyCount: 1, + repostCount: 0, + likeCount: 0, + quoteCount: 0, + indexedAt: "2025-01-01T10:00:05.000Z", + viewer: { + threadMuted: false, + embeddingDisabled: false, + }, + labels: [], + }, + grandparentAuthor: { + did: "did:plc:fakeOne", + handle: "john_doe.bsky.social", + displayName: "John Doe", + avatar: "https://mock.cdn/avatar/plain/did:plc:fakeOne/bafkremockavatar@jpeg", + viewer: { + muted: false, + blockedBy: false, + following: "at://did:plc:someOtherDid/app.bsky.graph.follow/mockFollow" + }, + labels: [], + createdAt: "2025-01-01T08:00:00.000Z", + }, + }, +}; diff --git a/components/bluesky/sources/new-timeline-posts/new-timeline-posts.mjs b/components/bluesky/sources/new-timeline-posts/new-timeline-posts.mjs new file mode 100644 index 0000000000000..f233c6452de45 --- /dev/null +++ b/components/bluesky/sources/new-timeline-posts/new-timeline-posts.mjs @@ -0,0 +1,38 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "bluesky-new-timeline-posts", + name: "New Timeline Posts", + description: "Emit new event when posts appear in the `following` feed. [See the documentation](https://docs.bsky.app/docs/api/app-bsky-feed-get-timeline).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getDateField() { + return "post.record.createdAt"; + }, + getResourceName() { + return "feed"; + }, + getResourcesFn() { + return this.app.getTimeline; + }, + getResourcesFnArgs() { + return { + debug: true, + }; + }, + generateMeta(resource) { + const { post } = resource; + return { + id: post.cid, + summary: `New Post at ${post.record.createdAt}`, + ts: Date.parse(post.record.createdAt), + }; + }, + }, + sampleEmit, +}; diff --git a/components/bluesky/sources/new-timeline-posts/test-event.mjs b/components/bluesky/sources/new-timeline-posts/test-event.mjs new file mode 100644 index 0000000000000..560f897f9aa54 --- /dev/null +++ b/components/bluesky/sources/new-timeline-posts/test-event.mjs @@ -0,0 +1,137 @@ +export default { + "post": { + "uri": "at://did:plc:mockDid/app.bsky.feed.post/mockUri", + "cid": "mockCid", + "author": { + "did": "did:plc:mockDid", + "handle": "mockUser.bsky.social", + "displayName": "Mock User", + "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:mockDid/mockAvatar@jpeg", + "viewer": { + "muted": false, + "blockedBy": false, + "following": "at://did:plc:mockFollow/app.bsky.graph.follow/mockId" + }, + "labels": [], + "createdAt": "2025-01-01T00:00:00.000Z" + }, + "record": { + "$type": "app.bsky.feed.post", + "createdAt": "2025-01-01T00:00:00.000Z", + "langs": ["en"], + "reply": { + "parent": { + "cid": "mockParentCid", + "uri": "at://did:plc:mockParentDid/app.bsky.feed.post/mockParentUri" + }, + "root": { + "cid": "mockRootCid", + "uri": "at://did:plc:mockRootDid/app.bsky.feed.post/mockRootUri" + } + }, + "text": "This is some mock post content." + }, + "replyCount": 0, + "repostCount": 0, + "likeCount": 0, + "quoteCount": 0, + "indexedAt": "2025-01-01T00:00:00.000Z", + "viewer": { + "threadMuted": false, + "embeddingDisabled": false + }, + "labels": [] + }, + "reply": { + "root": { + "$type": "app.bsky.feed.defs#postView", + "uri": "at://did:plc:mockRootDid/app.bsky.feed.post/mockRootUri", + "cid": "mockRootCid", + "author": { + "did": "did:plc:mockRootDid", + "handle": "mockRootUser.bsky.social", + "displayName": "Mock Root User", + "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:mockRootDid/mockRootAvatar@jpeg", + "viewer": { + "muted": false, + "blockedBy": false, + "following": "at://did:plc:mockOtherFollow/app.bsky.graph.follow/mockRootFollowId" + }, + "labels": [], + "createdAt": "2025-01-01T01:11:00.000Z" + }, + "record": { + "$type": "app.bsky.feed.post", + "createdAt": "2025-01-01T01:12:00.000Z", + "langs": ["en"], + "text": "Mock text for root post." + }, + "replyCount": 9, + "repostCount": 8, + "likeCount": 36, + "quoteCount": 0, + "indexedAt": "2025-01-01T01:13:00.000Z", + "viewer": { + "threadMuted": false, + "embeddingDisabled": false + }, + "labels": [] + }, + "parent": { + "$type": "app.bsky.feed.defs#postView", + "uri": "at://did:plc:mockParentDid/app.bsky.feed.post/mockParentUri", + "cid": "mockParentCid", + "author": { + "did": "did:plc:mockParentDid", + "handle": "mockParentUser.bsky.social", + "displayName": "Mock Parent User", + "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:mockParentDid/mockParentAvatar@jpeg", + "viewer": { + "muted": false, + "blockedBy": false + }, + "labels": [], + "createdAt": "2025-01-01T02:15:00.000Z" + }, + "record": { + "$type": "app.bsky.feed.post", + "createdAt": "2025-01-01T02:16:00.000Z", + "langs": ["en"], + "reply": { + "parent": { + "cid": "mockRootCid", + "uri": "at://did:plc:mockRootDid/app.bsky.feed.post/mockRootUri" + }, + "root": { + "cid": "mockRootCid", + "uri": "at://did:plc:mockRootDid/app.bsky.feed.post/mockRootUri" + } + }, + "text": "Mock text for parent post." + }, + "replyCount": 1, + "repostCount": 0, + "likeCount": 1, + "quoteCount": 0, + "indexedAt": "2025-01-01T02:17:00.000Z", + "viewer": { + "threadMuted": false, + "embeddingDisabled": false + }, + "labels": [] + }, + "grandparentAuthor": { + "did": "did:plc:mockDid", + "handle": "mockUser.bsky.social", + "displayName": "Mock User", + "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:mockDid/mockAvatar@jpeg", + "viewer": { + "muted": false, + "blockedBy": false, + "following": "at://did:plc:anotherMockDid/app.bsky.graph.follow/mockGrandparentFollowId" + }, + "labels": [], + "createdAt": "2025-01-01T03:00:00.000Z" + } + } +}; diff --git a/components/bluesky_by_unshape/README.md b/components/bluesky_by_unshape/README.md new file mode 100644 index 0000000000000..53190f8881d78 --- /dev/null +++ b/components/bluesky_by_unshape/README.md @@ -0,0 +1,11 @@ +# Overview + +The Bluesky by Unshape API lets you create, manage, and interact with your Bluesky social media account programmatically. With the API, you can post new statuses, follow or unfollow accounts, fetch a user's statuses, and more. In Pipedream, this can be leveraged to automate social media content sharing, track account interactions, and integrate with other services for enhanced social media management. + +# Example Use Cases + +- **Automated Content Sharing**: Create a workflow that automatically posts your latest blog posts or news articles to Bluesky. Use RSS or webhook triggers to initiate the workflow when new content is available, and the Bluesky by Unshape API to post the updates. + +- **Follow-Back Automation**: Build a workflow that monitors your Bluesky account for new followers using a schedule or webhook and uses the Bluesky by Unshape API to follow back these accounts automatically. Enhance engagement by integrating a CRM tool to track these interactions. + +- **Sentiment Analysis and Response**: Set up a workflow that fetches recent mentions of your Bluesky account, analyzes the sentiment using a natural language processing service like Google's Cloud Natural Language API, and then uses the Bluesky API to respond accordingly or flag for manual review. diff --git a/components/bluesnap/bluesnap.app.mjs b/components/bluesnap/bluesnap.app.mjs new file mode 100644 index 0000000000000..67f5cf5ac3f7a --- /dev/null +++ b/components/bluesnap/bluesnap.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "bluesnap", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/bluesnap/package.json b/components/bluesnap/package.json new file mode 100644 index 0000000000000..4566cf28a5e32 --- /dev/null +++ b/components/bluesnap/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/bluesnap", + "version": "0.0.1", + "description": "Pipedream BlueSnap Components", + "main": "bluesnap.app.mjs", + "keywords": [ + "pipedream", + "bluesnap" + ], + "homepage": "https://pipedream.com/apps/bluesnap", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/boldsign/actions/send-document-template/send-document-template.mjs b/components/boldsign/actions/send-document-template/send-document-template.mjs new file mode 100644 index 0000000000000..6aae2961da60f --- /dev/null +++ b/components/boldsign/actions/send-document-template/send-document-template.mjs @@ -0,0 +1,270 @@ +import { ConfigurationError } from "@pipedream/platform"; +import fs from "fs"; +import boldsign from "../../boldsign.app.mjs"; +import { DOCUMENT_DOWNLOAD_OPTIONS } from "../../common/constants.mjs"; +import { + checkTmp, + parseObject, +} from "../../common/utils.mjs"; + +export default { + key: "boldsign-send-document-template", + name: "Send Document Using Template", + description: "Send documents for e-signature using a BoldSign template. [See the documentation](https://developers.boldsign.com/documents/send-document-from-template/?region=us)", + version: "0.0.1", + type: "action", + props: { + boldsign, + templateId: { + propDefinition: [ + boldsign, + "templateId", + ], + }, + title: { + type: "string", + label: "Title", + description: "The title of the document.", + optional: true, + }, + message: { + type: "string", + label: "Message", + description: "A message to include with the document.", + optional: true, + }, + roles: { + type: "string[]", + label: "Roles", + description: "A role is simply a placeholder for a real person. For example, if we have a purchase order that will always be signed by two people, one from the company and one from the customer, we can create a template with two roles Customer and Representative. Example: **[{\"roleIndex\": 50,\"signerName\": \"Richard\",\"signerOrder\": 1,\"signerEmail\": \"richard@cubeflakes.com\",\"privateMessage\": \"Please check and sign the document.\",\"authenticationCode\": \"281028\",\"enableEmailOTP\": false,\"signerType\": \"Signer\",\"signerRole\": \"Manager\",\"formFields\": [{\"id\": \"SignField\",\"fieldType\": \"Signature\",\"pageNumber\": 1,\"bounds\": {\"x\": 100,\"y\": 100,\"width\": 100,\"height\": 50},\"isRequired\": true}]**.", + }, + brandId: { + propDefinition: [ + boldsign, + "brandId", + ], + optional: true, + }, + labels: { + propDefinition: [ + boldsign, + "labels", + ], + optional: true, + }, + disableEmails: { + type: "boolean", + label: "Disable Emails", + description: "Disable sending emails to recipients.", + optional: true, + }, + disableSMS: { + type: "boolean", + label: "Disable SMS", + description: "Disable sending SMS to recipients.", + optional: true, + }, + hideDocumentId: { + type: "boolean", + label: "Hide Document ID", + description: "Decides whether the document ID should be hidden or not.", + optional: true, + }, + enableAutoReminder: { + type: "boolean", + label: "Enable Auto Reminder", + description: "Enable automatic reminders.", + reloadProps: true, + optional: true, + }, + reminderDays: { + type: "integer", + label: "Reminder Days", + description: "Number of days between reminders.", + hidden: true, + optional: true, + }, + reminderCount: { + type: "integer", + label: "Reminder Count", + description: "Number of reminder attempts.", + hidden: true, + optional: true, + }, + cc: { + propDefinition: [ + boldsign, + "cc", + ], + optional: true, + }, + expiryDays: { + type: "integer", + label: "Expiry Days", + description: "Number of days before document expires.", + optional: true, + }, + enablePrintAndSign: { + type: "boolean", + label: "Enable Print and Sign", + description: "Allow signers to print and sign document.", + optional: true, + }, + enableReassign: { + type: "boolean", + label: "Enable Reassign", + description: "Allow signers to reassign the document.", + optional: true, + }, + enableSigningOrder: { + type: "boolean", + label: "Enable Signing Order", + description: "Enable signing order for the document.", + optional: true, + }, + disableExpiryAlert: { + type: "boolean", + label: "Disable Expiry Alert", + description: "Disable alerts before document expiry.", + optional: true, + }, + documentInfo: { + type: "string[]", + label: "Document Info", + description: "Custom information fields for the document. [See the documentation](https://developers.boldsign.com/documents/send-document-from-template) for further information.", + optional: true, + }, + roleRemovalIndices: { + type: "integer[]", + label: "Role Removal Indices", + description: "Removes the roles present in the template with their indices given in this property.", + optional: true, + }, + documentDownloadOption: { + type: "string", + label: "Document Download Option", + description: "Option for document download configuration.", + options: DOCUMENT_DOWNLOAD_OPTIONS, + optional: true, + }, + formGroups: { + type: "string[]", + label: "Form Groups", + description: "Manages the rules and configuration of grouped form fields. [See the documentation](https://developers.boldsign.com/documents/send-document-from-template) for further information.", + optional: true, + }, + files: { + type: "string[]", + label: "Files", + description: "A list of paths to files in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp).", + optional: true, + }, + fileUrls: { + type: "string[]", + label: "File URLs", + description: "URLs of files to include in the document.", + optional: true, + }, + recipientNotificationSettings: { + type: "object", + label: "Recipient Notification Settings", + description: "Settings for recipient notifications. [See the documentation](https://developers.boldsign.com/documents/send-document-from-template) for further information.", + optional: true, + }, + removeFormfields: { + type: "string[]", + label: "Remove Formfields", + description: "The removeFormFields property in API allows you to exclude specific form fields from a document before sending it. You provide a string array with the IDs of the existing form fields you want to remove. One or more values can be specified.", + optional: true, + }, + enableAuditTrailLocalization: { + type: "boolean", + label: "Enable Audit Trail Localization", + description: "Enable localization for audit trail based on the signer's language. If null is provided, the value will be inherited from the Business Profile settings. Only one additional language can be specified in the signer's languages besides English.", + optional: true, + }, + }, + async additionalProps(props) { + props.reminderDays.hidden = !this.enableAutoReminder; + props.reminderCount.hidden = !this.enableAutoReminder; + return {}; + }, + async run({ $ }) { + try { + const files = []; + if (this.files) { + for (const file of parseObject(this.files)) { + const filePath = fs.readFileSync(checkTmp(file), "base64"); + files.push(`data:application/${file.substr(file.length - 3)};base64,${filePath}`); + } + } + + const additionalData = {}; + if (this.enableAutoReminder) { + additionalData.reminderSettings = { + enableAutoReminder: this.enableAutoReminder, + reminderDays: this.reminderDays, + reminderCount: this.reminderCount, + }; + } + + const response = await this.boldsign.sendDocument({ + $, + headers: { + "Content-Type": "application/json;odata.metadata=minimal;odata.streaming=true", + }, + params: { + templateId: this.templateId, + }, + data: { + title: this.title, + message: this.message, + roles: parseObject(this.roles), + brandId: this.brandId, + labels: parseObject(this.labels), + disableEmails: this.disableEmails, + disableSMS: this.disableSMS, + hideDocumentId: this.hideDocumentId, + reminderSettings: { + enableAutoReminder: this.enableAutoReminder, + reminderDays: this.reminderDays, + reminderCount: this.reminderCount, + }, + cc: parseObject(this.cc)?.map((item) => ({ + emailAddress: item, + })), + expiryDays: this.expiryDays, + enablePrintAndSign: this.enablePrintAndSign, + enableReassign: this.enableReassign, + enableSigningOrder: this.enableSigningOrder, + disableExpiryAlert: this.disableExpiryAlert, + documentInfo: parseObject(this.documentInfo), + roleRemovalIndices: parseObject(this.roleRemovalIndices), + documentDownloadOption: this.documentDownloadOption, + formGroups: parseObject(this.formGroups), + files, + fileUrls: parseObject(this.fileUrls), + recipientNotificationSettings: parseObject(this.recipientNotificationSettings), + removeFormfields: parseObject(this.removeFormfields), + enableAuditTrailLocalization: this.enableAuditTrailLocalization, + ...additionalData, + }, + }); + $.export("$summary", `Document sent successfully using template ${this.templateId}`); + return response; + } catch ({ message }) { + const parsedMessage = JSON.parse(message); + let errorMessage = ""; + if (parsedMessage.error) errorMessage = parsedMessage.error; + if (parsedMessage.errors) { + Object.entries(parsedMessage.errors).map(([ + , + value, + ]) => { + errorMessage += `- ${value[0]}\n`; + }); + } + throw new ConfigurationError(errorMessage); + } + }, +}; diff --git a/components/boldsign/boldsign.app.mjs b/components/boldsign/boldsign.app.mjs new file mode 100644 index 0000000000000..cd0328e46d266 --- /dev/null +++ b/components/boldsign/boldsign.app.mjs @@ -0,0 +1,162 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "boldsign", + propDefinitions: { + templateId: { + type: "string", + label: "Template ID", + description: "The ID of the BoldSign template to use.", + async options({ page }) { + const { result } = await this.listTemplates({ + params: { + page: page + 1, + }, + }); + + return result.map(({ + documentId: value, templateName: label, + }) => ({ + label, + value, + })); + }, + }, + brandId: { + type: "string", + label: "Brand ID", + description: "The brand ID for customizing the document.", + async options() { + const { result } = await this.listBrands(); + return result.map(({ + brandId: value, brandName: label, + }) => ({ + label, + value, + })); + }, + }, + labels: { + type: "string[]", + label: "Labels", + description: "Labels for categorizing documents.", + }, + cc: { + type: "string[]", + label: "CC", + description: "A list of CC recipients for the document.", + async options({ page }) { + const { result } = await this.listContacts({ + params: { + page: page + 1, + }, + }); + + return result.map(({ email }) => email); + }, + }, + sentBy: { + type: "string", + label: "Sent By", + description: "The sender of the document.", + async options({ page }) { + const { result } = await this.listSenderIdentities({ + params: { + page: page + 1, + }, + }); + + return result.map(({ email }) => email); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.boldsign.com/v1"; + }, + _headers(headers = {}) { + return { + ...headers, + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "accept": "application/json", + }; + }, + _makeRequest({ + $ = this, path, headers, ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._headers(headers), + ...opts, + }); + }, + listTemplates(opts = {}) { + return this._makeRequest({ + path: "/template/list", + ...opts, + }); + }, + listBrands(opts = {}) { + return this._makeRequest({ + path: "/brand/list", + ...opts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts/list", + ...opts, + }); + }, + listDocuments(opts = {}) { + return this._makeRequest({ + path: "/document/list", + ...opts, + }); + }, + listSenderIdentities(opts = {}) { + return this._makeRequest({ + path: "/senderIdentities/list", + ...opts, + }); + }, + sendDocument(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/template/send", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const { + result, + pageDetails: { + page: currentPage, totalPages, + }, + } = await fn({ + params, + ...opts, + }); + for (const d of result) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = currentPage < totalPages; + + } while (hasMore); + }, + }, +}; diff --git a/components/boldsign/common/constants.mjs b/components/boldsign/common/constants.mjs new file mode 100644 index 0000000000000..412c03323c14c --- /dev/null +++ b/components/boldsign/common/constants.mjs @@ -0,0 +1,4 @@ +export const DOCUMENT_DOWNLOAD_OPTIONS = [ + "Combined", + "Individually", +]; diff --git a/components/boldsign/common/utils.mjs b/components/boldsign/common/utils.mjs new file mode 100644 index 0000000000000..0cd1a12b6a4ba --- /dev/null +++ b/components/boldsign/common/utils.mjs @@ -0,0 +1,31 @@ +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; + +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/boldsign/package.json b/components/boldsign/package.json new file mode 100644 index 0000000000000..214d46128b487 --- /dev/null +++ b/components/boldsign/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/boldsign", + "version": "0.1.0", + "description": "Pipedream BoldSign Components", + "main": "boldsign.app.mjs", + "keywords": [ + "pipedream", + "boldsign" + ], + "homepage": "https://pipedream.com/apps/boldsign", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/boldsign/sources/common/base.mjs b/components/boldsign/sources/common/base.mjs new file mode 100644 index 0000000000000..112e52108f74a --- /dev/null +++ b/components/boldsign/sources/common/base.mjs @@ -0,0 +1,106 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import boldsign from "../../boldsign.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + props: { + boldsign, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + sentBy: { + propDefinition: [ + boldsign, + "sentBy", + ], + optional: true, + }, + recipients: { + type: "string[]", + label: "Recipients", + description: "Recipients of the document.", + optional: true, + }, + searchKey: { + type: "string", + label: "Search Key", + description: "Search key for documents.", + optional: true, + }, + labels: { + propDefinition: [ + boldsign, + "labels", + ], + optional: true, + }, + brandIds: { + propDefinition: [ + boldsign, + "brandId", + ], + type: "string[]", + label: "Brand IDs", + description: "Brand IDs associated with the document.", + optional: true, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const params = { + ...this.getParams(), + }; + + if (this.sentBy) params.sentBy = this.sentBy; + if (this.recipients) params.recipients = parseObject(this.recipients); + if (this.searchKey) params.searchKey = this.searchKey; + if (this.labels) params.labels = parseObject(this.labels); + if (this.brandIds) params.brandIds = parseObject(this.brandIds); + + const response = this.boldsign.paginate({ + fn: this.getFunction(), + params, + }); + + let responseArray = []; + for await (const item of response) { + if (item.activityDate <= lastDate) break; + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastDate(responseArray[0].activityDate); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.documentId, + summary: this.getSummary(item), + ts: Date.parse(item.activityDate || new Date()), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/boldsign/sources/new-document-completed/new-document-completed.mjs b/components/boldsign/sources/new-document-completed/new-document-completed.mjs new file mode 100644 index 0000000000000..96b6916bb6ca8 --- /dev/null +++ b/components/boldsign/sources/new-document-completed/new-document-completed.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "boldsign-new-document-completed", + name: "New Document Completed", + description: "Emit new event when a document is completed by all the signers.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.boldsign.listDocuments; + }, + getSummary(item) { + return `Document Completed: ${item.documentId}`; + }, + getParams() { + return { + status: "Completed", + }; + }, + }, + sampleEmit, +}; diff --git a/components/boldsign/sources/new-document-completed/test-event.mjs b/components/boldsign/sources/new-document-completed/test-event.mjs new file mode 100644 index 0000000000000..e2ab71be6dac3 --- /dev/null +++ b/components/boldsign/sources/new-document-completed/test-event.mjs @@ -0,0 +1,47 @@ +export default { + "documentId": "755195d8-xxxx-xxxx-xxxx-88ff77d35419", + "senderDetail": { + "name": "Richard", + "privateMessage": null, + "emailAddress": "richard@cubeflakes.com", + "isViewed": false + }, + "ccDetails": [ + { + "emailAddress": "alexgayle@cubeflakes.com", + "isViewed": false + } + ], + "createdDate": 1664961706, + "activityDate": 1665989290, + "activityBy": "alexgayle@cubeflakes.com", + "messageTitle": "565", + "status": "Completed", + "signerDetails": [ + { + "signerName": "Richard", + "signerRole": "", + "signerEmail": "", + "status": "NotCompleted", + "enableAccessCode": false, + "isAuthenticationFailed": null, + "enableEmailOTP": false, + "authenticationType": "None", + "isDeliveryFailed": false, + "isViewed": false, + "order": 1, + "signerType": "Signer", + "hostEmail": "", + "hostName": "", + "isReassigned": true, + "privateMessage": "", + "formFields": [], + "language": 0 + } + ], + "expiryDate": 1670178599, + "enableSigningOrder": false, + "isDeleted": false, + "labels": [], + "nextCursor": 1665989290 +} \ No newline at end of file diff --git a/components/boldsign/sources/new-document-declined/new-document-declined.mjs b/components/boldsign/sources/new-document-declined/new-document-declined.mjs new file mode 100644 index 0000000000000..d190ea58ab1eb --- /dev/null +++ b/components/boldsign/sources/new-document-declined/new-document-declined.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "boldsign-new-document-declined", + name: "New Document Declined", + description: "Emit new event when a document is declined by a signer.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.boldsign.listDocuments; + }, + getSummary(item) { + return `Document Declined: ${item.documentId}`; + }, + getParams() { + return { + status: "Declined", + }; + }, + }, + sampleEmit, +}; diff --git a/components/boldsign/sources/new-document-declined/test-event.mjs b/components/boldsign/sources/new-document-declined/test-event.mjs new file mode 100644 index 0000000000000..5bb5b3269b125 --- /dev/null +++ b/components/boldsign/sources/new-document-declined/test-event.mjs @@ -0,0 +1,47 @@ +export default { + "documentId": "755195d8-xxxx-xxxx-xxxx-88ff77d35419", + "senderDetail": { + "name": "Richard", + "privateMessage": null, + "emailAddress": "richard@cubeflakes.com", + "isViewed": false + }, + "ccDetails": [ + { + "emailAddress": "alexgayle@cubeflakes.com", + "isViewed": false + } + ], + "createdDate": 1664961706, + "activityDate": 1665989290, + "activityBy": "alexgayle@cubeflakes.com", + "messageTitle": "565", + "status": "Declined", + "signerDetails": [ + { + "signerName": "Richard", + "signerRole": "", + "signerEmail": "", + "status": "NotCompleted", + "enableAccessCode": false, + "isAuthenticationFailed": null, + "enableEmailOTP": false, + "authenticationType": "None", + "isDeliveryFailed": false, + "isViewed": false, + "order": 1, + "signerType": "Signer", + "hostEmail": "", + "hostName": "", + "isReassigned": true, + "privateMessage": "", + "formFields": [], + "language": 0 + } + ], + "expiryDate": 1670178599, + "enableSigningOrder": false, + "isDeleted": false, + "labels": [], + "nextCursor": 1665989290 +} \ No newline at end of file diff --git a/components/boldsign/sources/new-document-sent/new-document-sent.mjs b/components/boldsign/sources/new-document-sent/new-document-sent.mjs new file mode 100644 index 0000000000000..30259ca9aa4b2 --- /dev/null +++ b/components/boldsign/sources/new-document-sent/new-document-sent.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "boldsign-new-document-sent", + name: "New Document Sent", + description: "Emit new event when a document is sent to a signer.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.boldsign.listDocuments; + }, + getSummary(item) { + return `New Document Sent: ${item.documentId}`; + }, + getParams() { + return { + status: "None", + transmitType: "Sent", + }; + }, + }, + sampleEmit, +}; diff --git a/components/boldsign/sources/new-document-sent/test-event.mjs b/components/boldsign/sources/new-document-sent/test-event.mjs new file mode 100644 index 0000000000000..7cbc93bc10a7f --- /dev/null +++ b/components/boldsign/sources/new-document-sent/test-event.mjs @@ -0,0 +1,47 @@ +export default { + "documentId": "755195d8-xxxx-xxxx-xxxx-88ff77d35419", + "senderDetail": { + "name": "Richard", + "privateMessage": null, + "emailAddress": "richard@cubeflakes.com", + "isViewed": false + }, + "ccDetails": [ + { + "emailAddress": "alexgayle@cubeflakes.com", + "isViewed": false + } + ], + "createdDate": 1664961706, + "activityDate": 1665989290, + "activityBy": "alexgayle@cubeflakes.com", + "messageTitle": "565", + "status": "WaitingForOthers", + "signerDetails": [ + { + "signerName": "Richard", + "signerRole": "", + "signerEmail": "", + "status": "NotCompleted", + "enableAccessCode": false, + "isAuthenticationFailed": null, + "enableEmailOTP": false, + "authenticationType": "None", + "isDeliveryFailed": false, + "isViewed": false, + "order": 1, + "signerType": "Signer", + "hostEmail": "", + "hostName": "", + "isReassigned": true, + "privateMessage": "", + "formFields": [], + "language": 0 + } + ], + "expiryDate": 1670178599, + "enableSigningOrder": false, + "isDeleted": false, + "labels": [], + "nextCursor": 1665989290 +} \ No newline at end of file diff --git a/components/boloforms/README.md b/components/boloforms/README.md new file mode 100644 index 0000000000000..30bd9d9b96706 --- /dev/null +++ b/components/boloforms/README.md @@ -0,0 +1,11 @@ +# Overview + +Boloforms is a powerful tool designed to handle form submissions without the need for a server. With the Boloforms API, you can dynamically process, store, and manage form submissions. By integrating Boloforms with Pipedream, you can automate workflows, trigger actions in other apps, and streamline data management, making it an excellent choice for businesses and developers looking to enhance their form handling capabilities. Pipedream's capabilities allow you to connect Boloforms data with numerous other services like email platforms, databases, and CRM systems to enhance functionality and automate routine tasks. + +# Example Use Cases + +- **Lead Collection to CRM Integration**: Capture form submissions on Boloforms and automatically add new leads or contacts to a CRM system like Salesforce. This workflow can enrich lead data, trigger follow-up tasks, or initiate drip email campaigns, ensuring timely engagement with potential customers. + +- **Automated Support Ticket Creation**: Use Boloforms submissions to create support tickets in tools like Zendesk or Jira. Each form submission related to support queries can trigger a ticket creation, automatically populating fields like description, priority, and customer details, streamlining the support process. + +- **Event Registration and Calendar Synchronization**: Manage event registrations through Boloforms and use Pipedream to sync new registration data to Google Calendar or Microsoft Outlook. Automate email confirmations and reminders to registrants, ensuring efficient event attendance management. diff --git a/components/boloforms/actions/send-form/send-form.mjs b/components/boloforms/actions/send-form/send-form.mjs new file mode 100644 index 0000000000000..911b80fe7e355 --- /dev/null +++ b/components/boloforms/actions/send-form/send-form.mjs @@ -0,0 +1,57 @@ +import { ConfigurationError } from "@pipedream/platform"; +import boloforms from "../../boloforms.app.mjs"; + +export default { + key: "boloforms-send-form", + name: "Send Form", + description: "Enables form dispatching to a specific recipient. [See the documentation](https://help.boloforms.com/en/articles/8557660-sending-for-signing)", + version: "0.0.1", + type: "action", + props: { + boloforms, + formId: { + propDefinition: [ + boloforms, + "formId", + ], + type: "string", + }, + email: { + type: "string", + label: "Email", + description: "The email of the receiver.", + }, + subject: { + propDefinition: [ + boloforms, + "subject", + ], + }, + message: { + propDefinition: [ + boloforms, + "message", + ], + }, + }, + async run({ $ }) { + const response = await this.boloforms.dispatchForm({ + $, + data: { + formId: this.formId, + email: { + to: this.email, + subject: this.subject, + message: this.message, + }, + }, + }); + + if (response.error) { + throw new ConfigurationError(response.error); + } + + $.export("$summary", `Form dispatched successfully to ${this.email}`); + return response; + }, +}; diff --git a/components/boloforms/actions/send-template-signature/send-template-signature.mjs b/components/boloforms/actions/send-template-signature/send-template-signature.mjs new file mode 100644 index 0000000000000..850b22502297b --- /dev/null +++ b/components/boloforms/actions/send-template-signature/send-template-signature.mjs @@ -0,0 +1,68 @@ +import { ConfigurationError } from "@pipedream/platform"; +import boloforms from "../../boloforms.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "boloforms-send-template-signature", + name: "Send Template Signature", + description: "Dispatch a predefined template to obtain a signature. [See the documentation](https://help.boloforms.com/en/articles/8557564-sending-for-signing)", + version: "0.0.1", + type: "action", + props: { + boloforms, + documentId: { + propDefinition: [ + boloforms, + "documentId", + () => ({ + isStandAloneTemplate: true, + }), + ], + type: "string", + }, + subject: { + propDefinition: [ + boloforms, + "subject", + ], + }, + message: { + propDefinition: [ + boloforms, + "message", + ], + }, + receiversList: { + type: "string[]", + label: "Receivers List", + description: "A list of receiver objects. **Format: {\"name\": \"Chirag Gupta\", \"email\": \"support@boloforms.com\", \"roleTitle\": \"Junior Doctor\", \"roleColour\": \"#8FB1C8\"}** `RoleTitle has to exactly same as the role you added otherwise it will not work properly`. `Give color to your role you can pass any hex code but it's necessary`.", + }, + customVariables: { + type: "string[]", + label: "Custom Variables", + description: "A list of custom variable objects. **Format: {\"varName\": \"[v1]\", \"varValue\": \"v1 value from api\" }**. `Variable name has to be in square barckets`. `If you don't pass customVariables then also the call would work, Boloforms will pass each variable as empty.`.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.boloforms.dispatchPDFTemplate({ + $, + data: { + documentId: this.documentId, + mailData: { + subject: this.subject, + message: this.message, + }, + receiversList: parseObject(this.receiversList), + customVariables: parseObject(this.customVariables), + }, + }); + + if (response.error) { + throw new ConfigurationError(response.error); + } + + $.export("$summary", `Template dispatched successfully to ${parseObject(this.receiversList).length} receivers`); + return response; + }, +}; diff --git a/components/boloforms/boloforms.app.mjs b/components/boloforms/boloforms.app.mjs new file mode 100644 index 0000000000000..efdc7899358c6 --- /dev/null +++ b/components/boloforms/boloforms.app.mjs @@ -0,0 +1,120 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "boloforms", + propDefinitions: { + formId: { + type: "string[]", + label: "Form ID", + description: "The ID of the form.", + async options({ page }) { + const { forms } = await this.getForms({ + params: { + page: page + 1, + filter: "ALL", + }, + }); + return forms.map(({ + formId: value, formJson: { title: label }, + }) => ({ + label, + value, + })); + }, + }, + documentId: { + type: "string[]", + label: "Document ID", + description: "The ID of the document.", + async options({ + page, isStandAloneTemplate = false, + }) { + const { documents } = await this.getDocuments({ + params: { + page: page + 1, + filter: "ALL", + isStandAloneTemplate, + }, + }); + return documents.map(({ + documentId: value, documentName: label, + }) => ({ + label, + value, + })); + }, + }, + subject: { + type: "string", + label: "Subject", + description: "The subject will be sent.", + }, + message: { + type: "string", + label: "Message", + description: "The message will be sent.", + }, + }, + methods: { + _baseUrl() { + return "https://signature-backend.boloforms.com/api/v1/signature"; + }, + _headers() { + return { + "x-api-key": this.$auth.api_key, + "Workspaceid": this.$auth.workspace_id, + "Content-Type": "application/json", + }; + }, + _data(data) { + return { + email: this.$auth.email, + ...data, + }; + }, + _makeRequest({ + $ = this, data, path, ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._headers(), + data: this._data(data), + ...opts, + }); + }, + dispatchPDFTemplate(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/add-respondent-pdfTemplate", + ...opts, + }); + }, + dispatchForm(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/forms/send-form-email", + ...opts, + }); + }, + getDocuments(opts = {}) { + return this._makeRequest({ + path: "/get-all-documents/v1", + ...opts, + }); + }, + getForms(opts = {}) { + return this._makeRequest({ + path: "/get-all-forms/v1", + ...opts, + }); + }, + updateHooks(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/workspace/webhooks", + ...opts, + }); + }, + }, +}; diff --git a/components/boloforms/common/utils.mjs b/components/boloforms/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/boloforms/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/boloforms/package.json b/components/boloforms/package.json new file mode 100644 index 0000000000000..6cd9f0d344c31 --- /dev/null +++ b/components/boloforms/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/boloforms", + "version": "0.1.0", + "description": "Pipedream Boloforms Components", + "main": "boloforms.app.mjs", + "keywords": [ + "pipedream", + "boloforms" + ], + "homepage": "https://pipedream.com/apps/boloforms", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5", + "uuid": "^9.0.1" + } +} diff --git a/components/boloforms/sources/common/base.mjs b/components/boloforms/sources/common/base.mjs new file mode 100644 index 0000000000000..5dbc32ae90839 --- /dev/null +++ b/components/boloforms/sources/common/base.mjs @@ -0,0 +1,83 @@ +import { v4 as uuidv4 } from "uuid"; +import boloforms from "../../boloforms.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + props: { + boloforms, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + webhookName: { + type: "string", + label: "Webhook Name", + description: "The name of the webhook to identify on the BoloForms platform.", + }, + }, + methods: { + _setHookId(hookId) { + this.db.set("webhookId", hookId); + }, + _getHookId() { + return this.db.get("webhookId"); + }, + getDocs() { + return this.documentId; + }, + generateMeta(body) { + const ts = Date.now(); + return { + id: `${body.documentId}-${ts}`, + summary: this.getSummary(body), + ts: ts, + }; + }, + }, + hooks: { + async activate() { + const uuid = uuidv4(); + const response = await this.boloforms.updateHooks({ + data: { + "task": "ADD", + "webhookObj": { + "webhookId": uuid, + "webhookName": this.webhookName, + "webhookUrl": this.http.endpoint, + "webhookEvent": this.getWebhookEvent(), + "selectedDocs": parseObject(this.getDocs()), + "webhookStatus": "ACTIVE", + }, + }, + }); + + const newHook = response.owner.webhooks.filter((hook) => hook.webhookId === uuid); + + this._setHookId({ + webhookId: uuid, + _id: newHook[0]._id, + }); + }, + async deactivate() { + await this.boloforms.updateHooks({ + data: { + "task": "DELETE", + "webhookObj": this._getHookId(), + }, + }); + }, + }, + async run({ body }) { + this.http.respond({ + status: 200, + body: "Success", + }); + if (!Array.isArray(body)) { + body = [ + body, + ]; + } + body.forEach((item) => this.$emit(item, this.generateMeta(item))); + }, +}; diff --git a/components/boloforms/sources/new-response-instant/new-response-instant.mjs b/components/boloforms/sources/new-response-instant/new-response-instant.mjs new file mode 100644 index 0000000000000..b40b2af3960fd --- /dev/null +++ b/components/boloforms/sources/new-response-instant/new-response-instant.mjs @@ -0,0 +1,39 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "boloforms-new-response-instant", + name: "New Response (Instant)", + description: "Emit new event when a filled form response is received.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + formId: { + propDefinition: [ + common.props.boloforms, + "formId", + ], + withLabel: true, + }, + }, + methods: { + ...common.methods, + getWebhookEvent() { + return "new_form_response"; + }, + getDocs() { + return this.formId; + }, + generateMeta(body) { + return { + id: body.formResponseId, + summary: `New response for form ID: ${body.formId}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/boloforms/sources/new-response-instant/test-event.mjs b/components/boloforms/sources/new-response-instant/test-event.mjs new file mode 100644 index 0000000000000..659e3c37459ce --- /dev/null +++ b/components/boloforms/sources/new-response-instant/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "formId": "12345678-1234-1234-1234-123456789012", + "formResponseId": "12345678-1234-1234-1234-123456789012", + "formTitle": "Title", + "documentUrl": "https://boloforms-files-ap-south-1.s3.ap-south-1.amazonaws.com/documents/12345678-1234-1234-1234-123456789012.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=12343565678%12313123%2Fap-south-1%2Fs3%2Faws4_request&X-Amz-Date=123123123123&X-Amz-Expires=604800&X-Amz-Signature=1233452667864fdgu54323weq65432rwe&X-Amz-SignedHeaders=host&x-id=GetObject", + "respondentEmail": "test@email.com", + "response": [ + { + "type": "Text", + "question": "Email", + "answer": "test@email.com" + } + ], + "response[Email]": "test@email.com" +} \ No newline at end of file diff --git a/components/boloforms/sources/new-signature-completed-instant/new-signature-completed-instant.mjs b/components/boloforms/sources/new-signature-completed-instant/new-signature-completed-instant.mjs new file mode 100644 index 0000000000000..a09a677cc93dd --- /dev/null +++ b/components/boloforms/sources/new-signature-completed-instant/new-signature-completed-instant.mjs @@ -0,0 +1,32 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "boloforms-new-signature-completed-instant", + name: "New Signature Completed (Instant)", + description: "Emit new event when a PDF document is fully signed. [See the documentation](https://help.boloforms.com/en/collections/8024362-webhooks)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + documentId: { + propDefinition: [ + common.props.boloforms, + "documentId", + ], + withLabel: true, + }, + }, + methods: { + ...common.methods, + getWebhookEvent() { + return "new_signature_completed"; + }, + getSummary(body) { + return `Document ${body.documentId} fully signed`; + }, + }, + sampleEmit, +}; diff --git a/components/boloforms/sources/new-signature-completed-instant/test-event.mjs b/components/boloforms/sources/new-signature-completed-instant/test-event.mjs new file mode 100644 index 0000000000000..94b3be7e84248 --- /dev/null +++ b/components/boloforms/sources/new-signature-completed-instant/test-event.mjs @@ -0,0 +1,20 @@ +export default { + "documentName": "Sample.pdf", + "documentId": "12345678-1234-1234-1234-123456789012", + "documentUrl": "https://boloforms-files-ap-south-1.s3.ap-south-1.amazonaws.com/documents/12345678-1234-1234-1234-123456789012.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=12343565678%12313123%2Fap-south-1%2Fs3%2Faws4_request&X-Amz-Date=123123123123&X-Amz-Expires=604800&X-Amz-Signature=1233452667864fdgu54323weq65432rwe&X-Amz-SignedHeaders=host&x-id=GetObject", + "authorEmail": "author@email.com", + "finishedPdfUrl": "https://sample-pdf-url.pdf", + "Signer 1 Email": "email@example.com", + "Signer 1 Name": "Sample User", + "signers": [ + { + "email": "email@example.com", + "name": "Signer Name", + "status": "DRAFT", + "signerIp": "", + "hasDeclined": false + } + ], + "text(page4)[Signer Name]": "Sample Response", + "date(page4)[Signer Name]": "Sample Response" +} \ No newline at end of file diff --git a/components/boloforms/sources/new-template-response-instant/new-template-response-instant.mjs b/components/boloforms/sources/new-template-response-instant/new-template-response-instant.mjs new file mode 100644 index 0000000000000..4ba20b25e9ca6 --- /dev/null +++ b/components/boloforms/sources/new-template-response-instant/new-template-response-instant.mjs @@ -0,0 +1,35 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "boloforms-new-template-response-instant", + name: "New Template Response (Instant)", + description: "Emit new event when a new form template response is filled.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + documentId: { + propDefinition: [ + common.props.boloforms, + "documentId", + () => ({ + isStandAloneTemplate: true, + }), + ], + withLabel: true, + }, + }, + methods: { + ...common.methods, + getWebhookEvent() { + return "new_template_response"; + }, + getSummary(body) { + return `New response for template ID ${body.documentId}`; + }, + }, + sampleEmit, +}; diff --git a/components/boloforms/sources/new-template-response-instant/test-event.mjs b/components/boloforms/sources/new-template-response-instant/test-event.mjs new file mode 100644 index 0000000000000..cc9559a1c2f64 --- /dev/null +++ b/components/boloforms/sources/new-template-response-instant/test-event.mjs @@ -0,0 +1,20 @@ +export default { + "documentName": "invoice.pdf", + "documentId": "12345678-1234-1234-1234-123456789012", + "documentUrl": "https://boloforms-files-ap-south-1.s3.ap-south-1.amazonaws.com/documents/12345678-1234-1234-1234-123456789012.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=12343565678%12313123%2Fap-south-1%2Fs3%2Faws4_request&X-Amz-Date=123123123123&X-Amz-Expires=604800&X-Amz-Signature=1233452667864fdgu54323weq65432rwe&X-Amz-SignedHeaders=host&x-id=GetObject", + "authorEmail": "author@email.com", + "finishedPdfUrl": "https://sample-pdf-url.pdf", + "Signer 1 Email": "Developer@example.com", + "Signer 1 Name": "Sample User", + "Signer 1 Role title": "Developer", + "signers": [ + { + "roleTitle": "Developer", + "email": "Developer@example.com", + "name": "Sample User", + "status": "", + "signerIp": "" + } + ], + "name(page1)[Developer]": "Developer Name" +} \ No newline at end of file diff --git a/components/bolt_iot/README.md b/components/bolt_iot/README.md new file mode 100644 index 0000000000000..631f3f6dda420 --- /dev/null +++ b/components/bolt_iot/README.md @@ -0,0 +1,11 @@ +# Overview + +The Bolt IoT API lets you harness the power of your Bolt IoT devices, enabling you to control and monitor them remotely through Pipedream. With Pipedream's serverless platform, you can create automated workflows that trigger on specific events, process data, and integrate with countless other services. Whether you're aiming to automate your smart home, set up alerts, or manage device data, Pipedream makes it straightforward to connect Bolt IoT with a world of apps and services for a seamless automation experience. + +# Example Use Cases + +- **Temperature Alert System:** Set up a Bolt IoT device to monitor temperature. Use Pipedream to listen for temperature readings that exceed a set threshold and automate sending an alert via email or SMS through integrations with SendGrid or Twilio. + +- **Smart Irrigation Scheduler:** Create a workflow where the Bolt IoT API reports soil moisture levels to Pipedream, which then triggers a smart irrigation system via IFTTT or directly controls a smart plug connected to a water pump, ensuring plants are watered optimally. + +- **Energy Consumption Monitor:** Leverage Pipedream to collect energy usage data from your Bolt IoT equipped devices. Analyze the data and generate reports or visualizations with Google Sheets or Tableau. Use Pipedream to schedule regular updates and share insights with stakeholders automatically. diff --git a/components/bolt_iot/package.json b/components/bolt_iot/package.json index ed51a73affb76..3b5e7216fc5d2 100644 --- a/components/bolt_iot/package.json +++ b/components/bolt_iot/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/bookingmood/README.md b/components/bookingmood/README.md new file mode 100644 index 0000000000000..410cebf1cf104 --- /dev/null +++ b/components/bookingmood/README.md @@ -0,0 +1,11 @@ +# Overview + +The Bookingmood API offers a suite of functionalities centered around booking and reservation management, allowing users to integrate real-time booking capabilities into their applications. With this API, you can manage reservations, check availability, and handle customer data, making it incredibly beneficial for businesses in hospitality, travel, event management, and other industries that require booking solutions. Using Pipedream, you can leverage these capabilities to create automated workflows that connect Bookingmood with other apps, streamlining complex processes and enhancing customer interaction. + +# Example Use Cases + +- **Automate Reservation Confirmation Emails**: Once a reservation is made through Bookingmood, set up a Pipedream workflow to automatically send a confirmation email to the customer using the SendGrid app. This ensures immediate communication and improves customer service. + +- **Sync New Bookings to Google Sheets for Record-Keeping**: Every time a booking is made via Bookingmood, use Pipedream to add the booking details to a Google Sheets spreadsheet. This can be used for centralized record-keeping, helping businesses maintain organized records of all bookings without manual data entry. + +- **Trigger SMS Notifications for Booking Reminders**: Set up a workflow in Pipedream where, 24 hours before a booking date, an SMS reminder is automatically sent to the customer using the Twilio app. This helps in reducing no-shows and enhancing customer experience by reminding them of their upcoming reservations. diff --git a/components/bookingmood/bookingmood.app.mjs b/components/bookingmood/bookingmood.app.mjs new file mode 100644 index 0000000000000..22740b29a7ea5 --- /dev/null +++ b/components/bookingmood/bookingmood.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "bookingmood", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/bookingmood/package.json b/components/bookingmood/package.json new file mode 100644 index 0000000000000..4bcf3f27232fc --- /dev/null +++ b/components/bookingmood/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/bookingmood", + "version": "0.0.1", + "description": "Pipedream Bookingmood Components", + "main": "bookingmood.app.mjs", + "keywords": [ + "pipedream", + "bookingmood" + ], + "homepage": "https://pipedream.com/apps/bookingmood", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/boomtrain/README.md b/components/boomtrain/README.md index 8ccd2db493354..f01d02cd98619 100644 --- a/components/boomtrain/README.md +++ b/components/boomtrain/README.md @@ -1,8 +1,11 @@ # Overview -With the Zeta API, you can build a range of applications that can help you marketing efforts. Here are some examples: +The Zeta (Boomtrain) API offers a suite of tools designed for predictive marketing and analytics. By leveraging machine learning and data science, it can help personalize user experiences and optimize marketing efforts. With this API on Pipedream, you can automate customer segmentation, execute targeted campaigns, and analyze customer behavior. The API's capabilities can be harnessed to create dynamic, responsive marketing workflows that react to user interactions in real-time. -- A marketing automation tool that can help you automate your marketing tasks -- A customer segmentation tool that can help you segment your customers based on their behavior -- A tool that can help you personalize your marketing messages based on customer data -- A tool that can help you track the performance of your marketing campaigns +# Example Use Cases + +- **Personalized Email Campaigns**: Automate the sending of personalized emails by triggering a workflow on Pipedream whenever a user performs a specific action on your app. Use the Zeta API to segment users based on this behavior and other collected data points, then connect with an email service like SendGrid to dispatch tailored messages. + +- **User Engagement Analysis**: Capture events from your web or mobile platforms, send them to Zeta via Pipedream, and use Zeta’s analytics to score user engagement. Craft a workflow to process this data and connect with tools like Google Sheets or Airtable to visualize engagement levels and identify key patterns or segments for targeted follow-ups. + +- **Real-time Recommendations**: Integrate user actions from your e-commerce site to trigger workflows that communicate with the Zeta API, generating real-time product or content recommendations. These recommendations can then be sent to the user through a chatbot platform like Twilio or integrated directly back into the user's session on your site, enhancing the shopping experience. diff --git a/components/boomtrain/package.json b/components/boomtrain/package.json new file mode 100644 index 0000000000000..cb9e924d1307c --- /dev/null +++ b/components/boomtrain/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/boomtrain", + "version": "0.6.0", + "description": "Pipedream boomtrain Components", + "main": "boomtrain.app.mjs", + "keywords": [ + "pipedream", + "boomtrain" + ], + "homepage": "https://pipedream.com/apps/boomtrain", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/booqable/README.md b/components/booqable/README.md new file mode 100644 index 0000000000000..f466d43028304 --- /dev/null +++ b/components/booqable/README.md @@ -0,0 +1,12 @@ +# Overview + +The Booqable API provides programmatic access to rental shop functions such as inventory management, orders, and customers. Within Pipedream, you can leverage this API to automate tasks, sync data across platforms, and create custom workflows. The API's endpoints allow you to programmatically check item availability, manage bookings, and update customer information, making it suitable for integrating with a wide range of other apps and services to streamline operations. + +# Example Use Cases + +- **Automated Inventory Sync with Shopify**: When new products are added or updated in Booqable, trigger a workflow in Pipedream to reflect those changes in your Shopify store. Keep inventory levels in sync across both platforms, ensuring accurate stock data and avoiding overbooking. + +- **Scheduled Order Reporting**: Use Pipedream's scheduled triggers to fetch daily rental orders from Booqable and compile a report. Send this report via email with SendGrid or save it to Google Sheets for easy sharing and analysis, helping you keep track of rental performance. + +- **New Customer Onboarding**: When a new customer is created in Booqable, kick off a Pipedream workflow to add them to a Mailchimp list for email marketing, create a contact in HubSpot for CRM tracking, and send a personalized welcome message via Twilio SMS, enhancing the customer experience right from the start. +. diff --git a/components/bot9/README.md b/components/bot9/README.md new file mode 100644 index 0000000000000..366fd41bc3135 --- /dev/null +++ b/components/bot9/README.md @@ -0,0 +1,11 @@ +# Overview + +The Bot9 API enables automated interactions with trading systems, allowing users to execute, manage, and analyze trades through a programmatic interface. In Pipedream, you can leverage this API to craft serverless workflows that handle trading tasks, notifications, and analyses without needing to build a full backend system. This can speed up trade execution, improve response times to market changes, and enable complex trading strategies that adjust to live market data. + +# Example Use Cases + +- **Real-Time Trading Alerts to Slack**: Listen for specific trading signals from the Bot9 API and send alerts to a Slack channel using Pipedream's Slack integration. This allows your team to stay updated on potential trade opportunities or market movements as they happen. + +- **Automated Trade Execution Based on Custom Logic**: Use Pipedream to set up a workflow that processes market data from other sources, applies your custom trading logic, and uses the Bot9 API to execute trades when certain conditions are met. This can help automate your trading strategy, making it faster and more efficient. + +- **Scheduled Portfolio Analysis**: Schedule a daily or weekly Pipedream workflow to retrieve portfolio data from the Bot9 API, perform an analysis using built-in code steps, and compile a report. You can then email this report to yourself or a list of subscribers with Pipedream's built-in email action. diff --git a/components/botbaba/README.md b/components/botbaba/README.md index 89dab35abcff9..46eb25e733cb7 100644 --- a/components/botbaba/README.md +++ b/components/botbaba/README.md @@ -1,9 +1,11 @@ # Overview -Botbaba offers a wide range of features to help you build bots for your business. Here are some examples of what you can build with Botbaba: +Botbaba is a powerful chatbot platform that enables the creation of interactive chatbots for various purposes, from customer support to lead generation. With the Botbaba API, you can extend the capabilities of your bots by integrating them with a myriad of services and automations via Pipedream. This connection allows you to trigger workflows from chatbot interactions, synchronize data with CRMs, send notifications, process orders, and more, effectively turning chat conversations into actionable business insights and tasks. -- A customer support chatbot to help your customers resolve issues -- A sales chatbot to help customers find the products they need -- A marketing chatbot to engage with your customers and promote your brand -- An FAQ chatbot to answer common questions about your business -- A survey chatbot to gather feedback from your customers +# Example Use Cases + +- **Customer Support Ticket Creation**: When a user interacts with your Botbaba chatbot and requests assistance, you can set up a Pipedream workflow to automatically create a ticket in a customer support platform like Zendesk. This ensures that no customer query goes unnoticed and helps your support team track and manage requests efficiently. + +- **Lead Qualification and CRM Integration**: Upon capturing a lead through Botbaba, use Pipedream to qualify this lead based on predefined criteria. If the lead is qualified, automatically add their details to a CRM like Salesforce, and even assign them to the appropriate sales rep, streamlining the lead management process. + +- **E-commerce Order Processing**: For e-commerce chatbots built with Botbaba, integrate a Pipedream workflow that processes orders right from the chat. Connect with payment gateways, update inventory systems like Shopify, and send order confirmations to customers, all in real-time, creating a seamless buying experience. diff --git a/components/botcake/actions/create-keyword/create-keyword.mjs b/components/botcake/actions/create-keyword/create-keyword.mjs new file mode 100644 index 0000000000000..0d16ec7da99fe --- /dev/null +++ b/components/botcake/actions/create-keyword/create-keyword.mjs @@ -0,0 +1,20 @@ +import app from "../../botcake.app.mjs"; + +export default { + key: "botcake-create-keyword", + name: "Create Keyword", + description: "Create a new Keyword. [See the documentation](https://docs.botcake.io/english/api-reference#create-keyword)", + version: "0.0.1", + type: "action", + props: { + app, + }, + + async run({ $ }) { + const response = await this.app.createKeyword({ + $, + }); + $.export("$summary", "Successfully created new Keyword"); + return response; + }, +}; diff --git a/components/botcake/actions/get-tools/get-tools.mjs b/components/botcake/actions/get-tools/get-tools.mjs new file mode 100644 index 0000000000000..fe2138dea9645 --- /dev/null +++ b/components/botcake/actions/get-tools/get-tools.mjs @@ -0,0 +1,20 @@ +import app from "../../botcake.app.mjs"; + +export default { + key: "botcake-get-tools", + name: "Get Tools", + description: "Get a list of tools associated with the specified page. [See the documentation](https://docs.botcake.io/english/api-reference#get-tools)", + version: "0.0.1", + type: "action", + props: { + app, + }, + + async run({ $ }) { + const response = await this.app.getTools({ + $, + }); + $.export("$summary", `Successfully retrieved ${response.data.length} tools`); + return response; + }, +}; diff --git a/components/botcake/actions/update-keyword/update-keyword.mjs b/components/botcake/actions/update-keyword/update-keyword.mjs new file mode 100644 index 0000000000000..7318c9f0236ae --- /dev/null +++ b/components/botcake/actions/update-keyword/update-keyword.mjs @@ -0,0 +1,38 @@ +import app from "../../botcake.app.mjs"; + +export default { + key: "botcake-update-keyword", + name: "Update Keyword", + description: "Update the Keyword with the specified ID. [See the documentation](https://docs.botcake.io/english/api-reference#update-keyword)", + version: "0.0.1", + type: "action", + props: { + app, + keywordId: { + propDefinition: [ + app, + "keywordId", + ], + }, + flowId: { + propDefinition: [ + app, + "flowId", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.updateKeyword({ + $, + data: { + keyword_id: this.keywordId, + update: { + flow_id: this.flowId, + }, + }, + }); + $.export("$summary", `Successfully updated Keyword with ID: '${this.keywordId}'`); + return response; + }, +}; diff --git a/components/botcake/botcake.app.mjs b/components/botcake/botcake.app.mjs new file mode 100644 index 0000000000000..5c64d3338c85f --- /dev/null +++ b/components/botcake/botcake.app.mjs @@ -0,0 +1,88 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "botcake", + propDefinitions: { + flowId: { + type: "integer", + label: "Flow ID", + description: "ID of the Flow", + async options() { + const response = await this.getFlow(); + const flowsIds = response.data.flows; + return flowsIds.map(({ + id, name, + }) => ({ + label: name, + value: id, + })); + }, + }, + keywordId: { + type: "integer", + label: "Keyword ID", + description: "ID of the Keyword", + async options() { + const response = await this.getKeyword(); + const keywordIds = response.data; + return keywordIds.map(({ id }) => ({ + value: id, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://botcake.io/api/public_api/v1/"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "access-token": `${this.$auth.api_key}`, + }, + }); + }, + async createKeyword(args = {}) { + return this._makeRequest({ + path: `/pages/${this.$auth.page_id}/keywords/create`, + method: "post", + ...args, + }); + }, + async updateKeyword(args = {}) { + return this._makeRequest({ + path: `/pages/${this.$auth.page_id}/keywords/update`, + method: "post", + ...args, + }); + }, + async getTools(args = {}) { + return this._makeRequest({ + path: `/pages/${this.$auth.page_id}/tools`, + ...args, + }); + }, + async getKeyword(args = {}) { + return this._makeRequest({ + path: `/pages/${this.$auth.page_id}/keywords`, + ...args, + }); + }, + async getFlow(args = {}) { + return this._makeRequest({ + path: `/pages/${this.$auth.page_id}/flows`, + ...args, + }); + }, + }, +}; diff --git a/components/botcake/package.json b/components/botcake/package.json new file mode 100644 index 0000000000000..6f582da0df783 --- /dev/null +++ b/components/botcake/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/botcake", + "version": "0.1.0", + "description": "Pipedream Botcake Components", + "main": "botcake.app.mjs", + "keywords": [ + "pipedream", + "botcake" + ], + "homepage": "https://pipedream.com/apps/botcake", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/botconversa/README.md b/components/botconversa/README.md new file mode 100644 index 0000000000000..42815f80e3079 --- /dev/null +++ b/components/botconversa/README.md @@ -0,0 +1,11 @@ +# Overview + +The BotConversa API allows for the automation and integration of chatbot services into various platforms, fostering seamless interactions with users. With this API, you can create, manage, and deploy conversational bots that can engage with users in real-time, providing support, gathering feedback, or even conducting surveys. On Pipedream, you can concoct workflows that trigger actions within BotConversa or respond to events, such as new messages or user queries. This opens the door to a world where chatbots become a part of larger automation schemes, interacting with other apps and services to streamline communication processes. + +# Example Use Cases + +- **Customer Support Automation**: Automatically create a ticket in a service like Zendesk or Help Scout when a user reports an issue to a BotConversa chatbot. This bridges the gap between chatbot interactions and customer service management, ensuring that no issue goes unattended. + +- **Real-time Notifications**: Set up a workflow where a BotConversa chatbot message triggers a notification in Slack or Microsoft Teams. This keeps teams instantly informed about important user interactions, feedback, or queries received through the chatbot. + +- **Data Collection and Analysis**: After a chatbot session, have BotConversa send the conversation data to a Google Sheet or a database. You can then run analytics or generate reports, creating a feedback loop that helps to refine the chatbot's performance and user experience. diff --git a/components/botmaker/botmaker.app.mjs b/components/botmaker/botmaker.app.mjs new file mode 100644 index 0000000000000..5d4f823d75036 --- /dev/null +++ b/components/botmaker/botmaker.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "botmaker", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/botmaker/package.json b/components/botmaker/package.json new file mode 100644 index 0000000000000..65cd0c9b32d3e --- /dev/null +++ b/components/botmaker/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/botmaker", + "version": "0.0.1", + "description": "Pipedream Botmaker Components", + "main": "botmaker.app.mjs", + "keywords": [ + "pipedream", + "botmaker" + ], + "homepage": "https://pipedream.com/apps/botmaker", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/botpenguin/actions/add-whatsapp-contact/add-whatsapp-contact.mjs b/components/botpenguin/actions/add-whatsapp-contact/add-whatsapp-contact.mjs new file mode 100644 index 0000000000000..5759e15656306 --- /dev/null +++ b/components/botpenguin/actions/add-whatsapp-contact/add-whatsapp-contact.mjs @@ -0,0 +1,70 @@ +import { ConfigurationError } from "@pipedream/platform"; +import botpenguin from "../../botpenguin.app.mjs"; + +export default { + key: "botpenguin-add-whatsapp-contact", + name: "Add WhatsApp Contact", + description: "Adds a new WhatsApp contact to your BotPenguin account. [See the documentation](https://help.botpenguin.com/api-references/contacts-and-chats-apis/add-whatsapp-contact)", + version: "0.0.1", + type: "action", + props: { + botpenguin, + name: { + propDefinition: [ + botpenguin, + "name", + ], + optional: true, + }, + email: { + propDefinition: [ + botpenguin, + "email", + ], + optional: true, + }, + phone: { + propDefinition: [ + botpenguin, + "phone", + ], + }, + prefix: { + propDefinition: [ + botpenguin, + "prefix", + ], + }, + }, + async run({ $ }) { + let response; + + try { + response = await this.botpenguin.addContact({ + $, + data: [ + { + profile: { + userDetails: { + userProvidedName: this.name, + contact: { + email: this.email, + phone: { + number: this.phone, + prefix: this.prefix, + }, + }, + }, + }, + }, + ], + }); + } catch (e) { + const message = JSON.parse(e.message); + throw new ConfigurationError(message.data.join(" ")); + } + + $.export("$summary", "Successfully added contact!"); + return response; + }, +}; diff --git a/components/botpenguin/actions/send-whatsapp-message/send-whatsapp-message.mjs b/components/botpenguin/actions/send-whatsapp-message/send-whatsapp-message.mjs new file mode 100644 index 0000000000000..aa1cecb26947a --- /dev/null +++ b/components/botpenguin/actions/send-whatsapp-message/send-whatsapp-message.mjs @@ -0,0 +1,56 @@ +import botpenguin from "../../botpenguin.app.mjs"; + +export default { + key: "botpenguin-send-whatsapp-message", + name: "Send WhatsApp Message", + description: "Sends a WhatsApp message. [See the documentation](https://help.botpenguin.com/api-references/whatsapp-cloud-api/post-send-message-api)", + version: "0.0.1", + type: "action", + props: { + botpenguin, + apiKey: { + type: "string", + label: "Api Key", + description: "The bot's api key [Click here for further information](https://help.botpenguin.com/api-references/whatsapp-cloud-api#id-2.-api-key).", + secret: true, + }, + userName: { + type: "string", + label: "Username", + description: "The name of the user to whom the message is being sent.", + optional: true, + }, + waId: { + type: "string", + label: "Wa Id", + description: "The WhatsApp number of the user to whom the message is being sent. The number must contain the country code without the plus sign.", + }, + message: { + type: "string", + label: "Message", + description: "The message that needs to sent.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.botpenguin.sendMessage({ + $, + data: { + userName: this.userName, + wa_id: this.waId, + type: "text", + message: { + text: this.message, + }, + }, + params: { + apiKey: this.apiKey, + }, + headers: { + apiKey: this.apiKey, + }, + }); + $.export("$summary", "Message successfully sent!"); + return response; + }, +}; diff --git a/components/botpenguin/actions/update-contact-attributes/update-contact-attributes.mjs b/components/botpenguin/actions/update-contact-attributes/update-contact-attributes.mjs new file mode 100644 index 0000000000000..1656b872c66a5 --- /dev/null +++ b/components/botpenguin/actions/update-contact-attributes/update-contact-attributes.mjs @@ -0,0 +1,96 @@ +import { ConfigurationError } from "@pipedream/platform"; +import botpenguin from "../../botpenguin.app.mjs"; + +export default { + key: "botpenguin-update-contact-attributes", + name: "Update Contact Attributes", + description: "Updates custom attributes for a specific contact in your BotPenguin account.", + version: "0.0.1", + type: "action", + props: { + botpenguin, + contactId: { + propDefinition: [ + botpenguin, + "contactId", + ], + }, + name: { + propDefinition: [ + botpenguin, + "name", + ], + optional: true, + }, + email: { + propDefinition: [ + botpenguin, + "email", + ], + optional: true, + }, + phone: { + propDefinition: [ + botpenguin, + "phone", + ], + optional: true, + }, + prefix: { + propDefinition: [ + botpenguin, + "prefix", + ], + optional: true, + }, + telegram: { + type: "string", + label: "Telegram", + description: "The telegram username.", + optional: true, + }, + instagram: { + type: "string", + label: "Instagram", + description: "The Instagram profile Id.", + optional: true, + }, + facebook: { + type: "string", + label: "Facebook", + description: "The Facebook profile Id.", + optional: true, + }, + }, + async run({ $ }) { + let response; + const contact = await this.botpenguin.getContact({ + contactId: this.contactId, + }); + + if (!contact.profile.userDetails.contact) contact.profile.userDetails.contact = {}; + if (this.name) contact.profile.userDetails.userProvidedName = this.name; + if (this.email) contact.profile.userDetails.contact.email = this.email; + if (this.phone || this.prefix) contact.profile.userDetails.contact.phone = {}; + if (this.phone) contact.profile.userDetails.contact.phone.number = this.phone; + if (this.prefix) contact.profile.userDetails.contact.phone.prefix = this.prefix; + if (this.telegram) contact.profile.userDetails.telegramUserName = this.telegram; + if (this.instagram) contact.profile.userDetails.instagramProfileId = this.instagram; + if (this.facebook) contact.profile.userDetails.facebookProfileId = this.facebook; + if (this.phone && this.prefix) contact.profile.userDetails.whatsAppNumber = `${this.prefix} ${this.phone}`; + + try { + response = await this.botpenguin.updateContact({ + $, + contactId: this.contactId, + data: contact, + }); + } catch (e) { + const message = JSON.parse(e.message); + throw new ConfigurationError(message.data.join(" ")); + } + + $.export("$summary", `Successfully updated attributes for contact ID ${this.contactId}`); + return response; + }, +}; diff --git a/components/botpenguin/botpenguin.app.mjs b/components/botpenguin/botpenguin.app.mjs new file mode 100644 index 0000000000000..f23d9d3b2e811 --- /dev/null +++ b/components/botpenguin/botpenguin.app.mjs @@ -0,0 +1,167 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "botpenguin", + propDefinitions: { + contactId: { + type: "string", + label: "Contact ID", + description: "The identifier for the contact.", + async options({ page }) { + const { data } = await this.listContacts({ + data: { + page: page + 1, + }, + }); + + return data.map(({ + _id: value, profile: { + userDetails: { + name, contact: { + email, phone, + }, + }, + }, + }) => ({ + label: `${name || email || `${phone.prefix} ${phone.number}`}`, + value, + })); + }, + }, + email: { + type: "string", + label: "Email", + description: "The email address of the contact.", + }, + name: { + type: "string", + label: "Name", + description: "The full name of the contact.", + }, + phone: { + type: "string", + label: "Phone", + description: "The phone number of the contact.", + }, + prefix: { + type: "string", + label: "Prefix", + description: "The prefix of the phone number.", + }, + }, + methods: { + _baseUrl() { + return "https://api.v7.botpenguin.com"; + }, + _headers(headers) { + return { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.$auth.access_token}`, + "authtype": "Key", + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers = {}, ...opts + }) { + const config = { + url: this._baseUrl() + path, + headers: this._headers(headers), + ...opts, + }; + + return axios($, config); + }, + addContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/inbox/users/import", + ...opts, + }); + }, + async getContact({ contactId }) { + const responseArray = this.paginate({ + fn: this.listContacts, + + }); + for await (const item of responseArray) { + if (item._id === contactId) return item; + } + }, + listContacts({ + opts, data, + }) { + return this._makeRequest({ + method: "POST", + path: "/inbox/users", + data: { + searchText: "", + applicableFilters: [], + createdAt: { + startAt: "", + endsAt: "", + }, + lastSeenAt: { + startAt: "", + endsAt: "", + }, + _botWebsite: [], + _botWhatsapp: [], + _botTelegram: [], + _botFacebook: [], + status: [], + lastMessageBy: [], + tags: [], + _agentAssigned: [], + isExport: "none", + ...data, + }, + ...opts, + + }); + }, + updateContact({ + contactId, ...data + }) { + return this._makeRequest({ + method: "PUT", + path: `/inbox/users/${contactId}`, + ...data, + }); + }, + sendMessage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/whatsapp-automation/wa/send-message", + ...opts, + }); + }, + async *paginate({ + fn, data = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + data.page = ++page; + const { data: response } = await fn({ + data, + ...opts, + }); + + for (const d of response) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = response.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/botpenguin/package.json b/components/botpenguin/package.json new file mode 100644 index 0000000000000..525c50996b58c --- /dev/null +++ b/components/botpenguin/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/botpenguin", + "version": "0.1.0", + "description": "Pipedream BotPenguin Components", + "main": "botpenguin.app.mjs", + "keywords": [ + "pipedream", + "botpenguin" + ], + "homepage": "https://pipedream.com/apps/botpenguin", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} + diff --git a/components/botpenguin/sources/common/base.mjs b/components/botpenguin/sources/common/base.mjs new file mode 100644 index 0000000000000..f501a828dec27 --- /dev/null +++ b/components/botpenguin/sources/common/base.mjs @@ -0,0 +1,68 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import botpenguin from "../../botpenguin.app.mjs"; + +export default { + props: { + botpenguin, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + getWebhookProps() { + return {}; + }, + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01T00:00:01Z"; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async startEvent(maxResults = 0) { + const lastDate = this._getLastDate(); + const response = this.botpenguin.paginate({ + fn: this.botpenguin.listContacts, + maxResults, + data: { + createdAt: { + startAt: lastDate, + endsAt: "", + }, + isContact: this.isContact(), + }, + }); + + let responseArray = []; + + for await (const item of response) { + if (item.createdAt <= lastDate) break; + responseArray.push(item); + } + + if (responseArray.length) this._setLastDate(responseArray[0].createdAt); + + for (const item of responseArray.reverse()) { + this.$emit(item, this.generateMeta(item)); + } + }, + generateMeta(data) { + return { + id: this.getId(data), + summary: this.getSummary(data), + ts: data.createdAt, + }; + }, + }, + hooks: { + async deploy() { + await this.startEvent(25); + }, + }, + async run() { + await this.startEvent(); + }, +}; diff --git a/components/botpenguin/sources/new-contact-created/new-contact-created.mjs b/components/botpenguin/sources/new-contact-created/new-contact-created.mjs new file mode 100644 index 0000000000000..5dc9d925969a7 --- /dev/null +++ b/components/botpenguin/sources/new-contact-created/new-contact-created.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "../new-contact-created/test-event.mjs"; + +export default { + ...common, + key: "botpenguin-new-contact-created", + name: "New Contact Created", + description: "Emit new event when a user interacts with the bot and a new contact or lead is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + isContact() { + return true; + }, + getId(item) { + return item._id; + }, + getSummary(item) { + return `New contact with Id: ${item._id} was successfully created!`; + }, + }, + sampleEmit, +}; diff --git a/components/botpenguin/sources/new-contact-created/test-event.mjs b/components/botpenguin/sources/new-contact-created/test-event.mjs new file mode 100644 index 0000000000000..d250aa68d48cb --- /dev/null +++ b/components/botpenguin/sources/new-contact-created/test-event.mjs @@ -0,0 +1,55 @@ +export default { + "_id": "66e07fd940c43d176eaf0b71", + "_agency": "66e07fd940c43d176eaf0b71", + "_customer": "66e07fd940c43d176eaf0b71", + "platform": [ + "WHATSAPP" + ], + "secondaryPlatform": [ + "website" + ], + "isSubscriber": true, + "websiteVisits": 0, + "isOnline": true, + "userInteracted": false, + "profile": { + "userDetails": { + "contact": { + "email": "", + "phone": { + "number": "123456789", + "prefix": "1" + } + }, + "userProvidedName": "Contact Name", + "tags": [], + "attributes": [], + "country": "", + "whatsAppNumber": "1123456789" + }, + "marketing": { + "urlsVisited": [] + }, + "notes": [] + }, + "status": "OPEN", + "lastMessage": { + "by": "user", + "at": "2024-06-05T19:05:50.875Z" + }, + "isContact": true, + "isImported": true, + "isMigrated": false, + "segments": [], + "isLiveChatActive": false, + "isPaid": true, + "hasOrdered": { + "status": false + }, + "unreadMessages": 0, + "lastSeenAt": "2024-06-05T19:05:50.875Z", + "unreadMessage": [], + "__v": 0, + "createdAt": "2024-06-05T19:05:50.876Z", + "updatedAt": "2024-06-05T19:05:50.876Z" +} \ No newline at end of file diff --git a/components/botpenguin/sources/new-incoming-message/new-incoming-message.mjs b/components/botpenguin/sources/new-incoming-message/new-incoming-message.mjs new file mode 100644 index 0000000000000..ee83cbd40f7da --- /dev/null +++ b/components/botpenguin/sources/new-incoming-message/new-incoming-message.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "../new-contact-created/test-event.mjs"; + +export default { + ...common, + key: "botpenguin-new-incoming-message", + name: "New Incoming Message", + description: "Emit new event for each new incoming message from a user.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + isContact() { + return false; + }, + getId(item) { + return `${item._id}-${item.updatedAt}`; + }, + getSummary(item) { + return `New incoming message with Id: ${item._id}`; + }, + }, + sampleEmit, +}; diff --git a/components/botpenguin/sources/new-incoming-message/test-event.mjs b/components/botpenguin/sources/new-incoming-message/test-event.mjs new file mode 100644 index 0000000000000..d250aa68d48cb --- /dev/null +++ b/components/botpenguin/sources/new-incoming-message/test-event.mjs @@ -0,0 +1,55 @@ +export default { + "_id": "66e07fd940c43d176eaf0b71", + "_agency": "66e07fd940c43d176eaf0b71", + "_customer": "66e07fd940c43d176eaf0b71", + "platform": [ + "WHATSAPP" + ], + "secondaryPlatform": [ + "website" + ], + "isSubscriber": true, + "websiteVisits": 0, + "isOnline": true, + "userInteracted": false, + "profile": { + "userDetails": { + "contact": { + "email": "", + "phone": { + "number": "123456789", + "prefix": "1" + } + }, + "userProvidedName": "Contact Name", + "tags": [], + "attributes": [], + "country": "", + "whatsAppNumber": "1123456789" + }, + "marketing": { + "urlsVisited": [] + }, + "notes": [] + }, + "status": "OPEN", + "lastMessage": { + "by": "user", + "at": "2024-06-05T19:05:50.875Z" + }, + "isContact": true, + "isImported": true, + "isMigrated": false, + "segments": [], + "isLiveChatActive": false, + "isPaid": true, + "hasOrdered": { + "status": false + }, + "unreadMessages": 0, + "lastSeenAt": "2024-06-05T19:05:50.875Z", + "unreadMessage": [], + "__v": 0, + "createdAt": "2024-06-05T19:05:50.876Z", + "updatedAt": "2024-06-05T19:05:50.876Z" +} \ No newline at end of file diff --git a/components/botpress/README.md b/components/botpress/README.md new file mode 100644 index 0000000000000..99094e3f40f00 --- /dev/null +++ b/components/botpress/README.md @@ -0,0 +1,11 @@ +# Overview + +Botpress is a powerful, open-source chatbot platform that enables developers to create intelligent, conversational bots. With its natural language understanding (NLU) capabilities, users can design bots that can interpret human language and respond appropriately. Utilizing the Botpress API on Pipedream allows you to automate interactions, analyze chat data, and integrate with various other services to enhance the functionality of your bots. + +# Example Use Cases + +- **Customer Support Automation**: Automatically handle common customer inquiries by connecting Botpress with a CRM like Salesforce on Pipedream. When a Botpress bot identifies a user query about order status, it triggers a workflow that retrieves order details from Salesforce and provides real-time updates to the customer through the chat interface. + +- **Event Registration via Chatbot**: Use Botpress to create a chatbot on your website that handles event registrations. Connect the bot with Google Sheets on Pipedream. When a user expresses interest in an event and provides details, the bot captures this data and automatically populates a row in Google Sheets, registering the user for the event without manual data entry. + +- **Feedback Collection and Analysis**: Deploy a Botpress chatbot to collect feedback on a product or service. Integrate this bot with Twilio on Pipedream to send a follow-up SMS to thank the customer for their feedback. Additionally, connect to a tool like Airtable or Google Sheets to store and analyze feedback data systematically. diff --git a/components/botpress/actions/add-participant/add-participant.mjs b/components/botpress/actions/add-participant/add-participant.mjs new file mode 100644 index 0000000000000..287649bd7c4f6 --- /dev/null +++ b/components/botpress/actions/add-participant/add-participant.mjs @@ -0,0 +1,53 @@ +import app from "../../botpress.app.mjs"; + +export default { + key: "botpress-add-participant", + name: "Add Participant To Conversation", + description: "Adds a participant to a conversation. [See the documentation](https://botpress.com/docs/api-documentation/#add-participant)", + version: "0.0.1", + type: "action", + props: { + app, + conversationId: { + propDefinition: [ + app, + "conversationId", + ], + }, + userId: { + propDefinition: [ + app, + "userId", + ], + }, + }, + methods: { + addParticipant({ + conversationId, ...args + } = {}) { + return this.app.post({ + path: `/chat/conversations/${conversationId}/participants`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + addParticipant, + conversationId, + userId, + } = this; + + const response = await addParticipant({ + $, + conversationId, + data: { + userId, + }, + }); + + $.export("$summary", `Successfully added participant with ID \`${response.participant?.id}\` to the conversation.`); + + return response; + }, +}; diff --git a/components/botpress/actions/list-conversations/list-conversations.mjs b/components/botpress/actions/list-conversations/list-conversations.mjs new file mode 100644 index 0000000000000..3bdb895df7bbc --- /dev/null +++ b/components/botpress/actions/list-conversations/list-conversations.mjs @@ -0,0 +1,52 @@ +import app from "../../botpress.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "botpress-list-conversations", + name: "List Conversations", + description: "List conversations. [See the documentation](https://botpress.com/docs/api-documentation/#list-conversations)", + version: "0.0.1", + type: "action", + props: { + app, + participanIds: { + type: "string[]", + label: "Participant IDs", + description: "IDs of the participants.", + optional: true, + propDefinition: [ + app, + "userId", + ], + }, + integrationName: { + type: "string", + label: "Integration Name", + description: "Name of the integration. Eg. `webchat`.", + optional: true, + }, + }, + async run({ $ }) { + const { + app, + participantIds, + integrationName, + } = this; + + const resources = await app.paginate({ + resourcesFn: app.listConversations, + resourcesFnArgs: { + $, + params: { + participantIds: utils.parseArray(participantIds), + integrationName, + }, + }, + resourceName: "conversations", + }); + + $.export("$summary", `Successfully listed \`${resources.length}\` conversation(s).`); + + return resources; + }, +}; diff --git a/components/botpress/botpress.app.mjs b/components/botpress/botpress.app.mjs new file mode 100644 index 0000000000000..03cd0c3ec3431 --- /dev/null +++ b/components/botpress/botpress.app.mjs @@ -0,0 +1,170 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; + +export default { + type: "app", + app: "botpress", + propDefinitions: { + conversationId: { + type: "string", + label: "Conversation ID", + description: "ID of the conversation.", + async options({ prevContext: { nextToken } }) { + if (nextToken === false) { + return []; + } + const { + conversations, + meta, + } = await this.listConversations({ + ...(nextToken && { + params: { + nextToken: encodeURIComponent(nextToken), + }, + }), + }); + const options = conversations.map(({ + id: value, channel: label, + }) => ({ + label, + value, + })); + return { + options, + context: { + nextToken: meta?.nextToken || false, + }, + }; + }, + }, + userId: { + type: "string", + label: "User ID", + description: "ID of the user.", + async options({ prevContext: { nextToken } }) { + if (nextToken === false) { + return []; + } + const { + users, + meta, + } = await this.listUsers({ + ...(nextToken && { + params: { + nextToken: encodeURIComponent(nextToken), + }, + }), + }); + const options = users.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + return { + options, + context: { + nextToken: meta?.nextToken || false, + }, + }; + }, + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + const { + identity_token: identityToken, + bot_id: botId, + } = this.$auth; + return { + "Content-Type": "application/json", + "Authorization": `Bearer ${identityToken}`, + "x-bot-id": botId, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + listUsers(args = {}) { + return this._makeRequest({ + path: "/chat/users", + ...args, + }); + }, + listEvents(args = {}) { + return this._makeRequest({ + debug: true, + path: "/chat/events", + ...args, + }); + }, + listConversations(args = {}) { + return this._makeRequest({ + path: "/chat/conversations", + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, resourceName, + max = constants.DEFAULT_MAX, + }) { + let nextToken; + let resourcesCount = 0; + + while (true) { + if (nextToken === false) { + console.log("No more resources to paginate"); + return; + } + + const response = + await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + nextToken, + }, + }); + + const nextResources = resourceName && response[resourceName] || response; + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + yield resource; + resourcesCount += 1; + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + nextToken = response.meta?.nextToken || false; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); + }, + }, +}; diff --git a/components/botpress/common/constants.mjs b/components/botpress/common/constants.mjs new file mode 100644 index 0000000000000..499fccef31d49 --- /dev/null +++ b/components/botpress/common/constants.mjs @@ -0,0 +1,9 @@ +const BASE_URL = "https://api.botpress.cloud"; +const VERSION_PATH = "/v1"; +const DEFAULT_MAX = 600; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_MAX, +}; diff --git a/components/botpress/common/utils.mjs b/components/botpress/common/utils.mjs new file mode 100644 index 0000000000000..7cd99800e82ad --- /dev/null +++ b/components/botpress/common/utils.mjs @@ -0,0 +1,37 @@ +import { ConfigurationError } from "@pipedream/platform"; + +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +export default { + iterate, + parseArray, +}; diff --git a/components/botpress/package.json b/components/botpress/package.json new file mode 100644 index 0000000000000..b0e3e90e357df --- /dev/null +++ b/components/botpress/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/botpress", + "version": "0.1.0", + "description": "Pipedream Botpress Components", + "main": "botpress.app.mjs", + "keywords": [ + "pipedream", + "botpress" + ], + "homepage": "https://pipedream.com/apps/botpress", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} diff --git a/components/botpress/sources/common/polling.mjs b/components/botpress/sources/common/polling.mjs new file mode 100644 index 0000000000000..c8b8973649f4f --- /dev/null +++ b/components/botpress/sources/common/polling.mjs @@ -0,0 +1,59 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../botpress.app.mjs"; + +export default { + props: { + app, + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getResourceName() { + throw new ConfigurationError("getResourceName is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + async processResources(resources) { + Array.from(resources) + .reverse() + .forEach(this.processResource); + }, + }, + async run() { + const { + app, + getResourcesFn, + getResourcesFnArgs, + getResourceName, + processResources, + } = this; + + const resources = await app.paginate({ + resourcesFn: getResourcesFn(), + resourcesFnArgs: getResourcesFnArgs(), + resourceName: getResourceName(), + }); + + processResources(resources); + }, +}; diff --git a/components/botpress/sources/new-bot-event-created/new-bot-event-created.mjs b/components/botpress/sources/new-bot-event-created/new-bot-event-created.mjs new file mode 100644 index 0000000000000..e6345f5529bb6 --- /dev/null +++ b/components/botpress/sources/new-bot-event-created/new-bot-event-created.mjs @@ -0,0 +1,54 @@ +import common from "../common/polling.mjs"; + +export default { + ...common, + key: "botpress-new-bot-event-created", + name: "New Bot Event Created", + description: "Emit new event from bot is created. [See the documentation](https://botpress.com/docs/api-documentation/#list-events)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + type: { + type: "string", + label: "Event Type", + description: "The type of event to poll for", + options: [ + "state_expired", + "message_created", + "task_update", + "schedulev1", + "botpublished", + "botready", + "resumeprocessing", + "hitlAgent", + "webchat:conversationStarted", + "webchat:trigger", + ], + }, + }, + methods: { + ...common.methods, + getResourceName() { + return "events"; + }, + getResourcesFn() { + return this.app.listEvents; + }, + getResourcesFnArgs() { + return { + params: { + type: this.type, + }, + }; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Event: ${resource.type}`, + ts: Date.parse(resource.createdAt), + }; + }, + }, +}; diff --git a/components/botstar/README.md b/components/botstar/README.md index be736902fd318..6e139d5719a90 100644 --- a/components/botstar/README.md +++ b/components/botstar/README.md @@ -1,10 +1,11 @@ # Overview -Botstar allows you to build bots for a variety of purposes, including: - -- Providing customer support -- Taking orders -- Providing information -- Scheduling appointments -- Managing tasks -- And more! +Botstar API lets you extend the capabilities of Botstar's chatbot platform. It empowers you to programmatically manage and interact with your chatbots, access chat and user data, and integrate with external services. With Pipedream, you can leverage these API functions to automate workflows between Botstar and other apps, trigger actions based on chat events, or synchronize chatbot data with CRM platforms, databases, or marketing tools. + +# Example Use Cases + +- **Customer Support Automation**: Automate ticket creation in a helpdesk app like Zendesk whenever a Botstar chatbot identifies a support request. Use Pipedream to listen for specific keywords or intents in Botstar conversations, then create tickets in Zendesk with the relevant chat details, enhancing support response times. + +- **E-commerce Order Alerts**: Whenever a new order is placed via a Botstar e-commerce chatbot, trigger a workflow that sends real-time notifications to a Slack channel. This keeps your sales team instantly informed about new orders, allows them to quickly respond to customer inquiries, and streamlines the order management process. + +- **Feedback Collection and Analysis**: Integrate Botstar with Google Sheets or Airtable to collect user feedback from chatbot interactions. After a conversation ends, Pipedream can capture the feedback and store it in a spreadsheet or database. Combine this data with sentiment analysis tools to gain insights and improve chatbot performance. diff --git a/components/botstar/package.json b/components/botstar/package.json new file mode 100644 index 0000000000000..afa47077c6bfb --- /dev/null +++ b/components/botstar/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/botstar", + "version": "0.6.0", + "description": "Pipedream botstar Components", + "main": "botstar.app.mjs", + "keywords": [ + "pipedream", + "botstar" + ], + "homepage": "https://pipedream.com/apps/botstar", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/bouncer/README.md b/components/bouncer/README.md new file mode 100644 index 0000000000000..1004f9931a098 --- /dev/null +++ b/components/bouncer/README.md @@ -0,0 +1,11 @@ +# Overview + +The Bouncer API provides robust email verification services. Using Pipedream, you can harness this capability to clean your mailing list, verify emails on sign-up in real-time, or even integrate it into a multi-step workflow involving other apps and services. Pipedream simplifies connecting Bouncer with hundreds of other services for automating tasks, such as sending a welcome email via SendGrid to verified addresses or adding them to a Google Sheet. + +# Example Use Cases + +- **Email Verification for New Users**: When a new user signs up on your platform, trigger a Pipedream workflow to verify their email address with Bouncer. Based on the verification result, conditionally proceed to create their account and send a welcome email. + +- **Mailing List Cleanup**: Set up a scheduled Pipedream workflow that iterates through your mailing list stored in a platform like Airtable. Use Bouncer to check each email and update the Airtable record with the verification status, helping maintain a high deliverability rate. + +- **Real-time Signup Form Validation**: Integrate Bouncer API with your website's signup form using Pipedream. When a form is submitted, instantly verify the email address. If it's valid, proceed with the signup process and add the subscriber to your Mailchimp list. diff --git a/components/box/README.md b/components/box/README.md index ec8caec3372f2..1dc55c3dd33db 100644 --- a/components/box/README.md +++ b/components/box/README.md @@ -1,11 +1,11 @@ # Overview -With the Box API, you can build applications that: - -- Access files stored in Box -- Upload or download files -- Search for files and folders -- Modify files and folders -- Manage comments, tasks, and notifications -- Work with content collaboration -- Integrate withBox View to display PDFs, Office documents, and more +The Box API offers a playground for enhancing content management and collaboration within your cloud storage. With Pipedream, you can orchestrate Box's functionality to automate document handling, streamline approval flows, sync files across apps, and trigger actions based on file events. Think of Pipedream as the glue that allows you to connect Box with your tech stack, triggering workflows with new file uploads, comments, or when sharing settings change, to enhance productivity and minimize manual labor. + +# Example Use Cases + +- **Automated Document Backup**: When a new file is uploaded to a specific Box folder, a Pipedream workflow can automatically back it up to a secondary cloud storage like Google Drive or AWS S3, ensuring redundancy and compliance with data backup policies. + +- **Content Approval Pipeline**: Kick off a content approval process whenever a document is added to a designated Box folder. Use Pipedream to notify a Slack channel, await approvals or rejections through Slack interactions, and move the file to an "Approved" or "Rejected" folder based on the response. + +- **CRM Attachment Sync**: Integrate Box with a CRM platform like Salesforce. When a sales contract is uploaded to Box, Pipedream can automatically attach the file to the relevant opportunity in Salesforce, streamlining record-keeping and ensuring all team members have access to the latest documents. diff --git a/components/box/actions/download-file/download-file.mjs b/components/box/actions/download-file/download-file.mjs index 472d6d35d5074..d64e6060b0a75 100644 --- a/components/box/actions/download-file/download-file.mjs +++ b/components/box/actions/download-file/download-file.mjs @@ -7,7 +7,7 @@ export default { name: "Download File", description: "Downloads a file from Box to your workflow's `/tmp` directory. [See the documentation](https://developer.box.com/reference/get-files-id-content/)", key: "box-download-file", - version: "0.0.2", + version: "0.0.3", type: "action", props: { app, diff --git a/components/box/actions/get-comments/get-comments.mjs b/components/box/actions/get-comments/get-comments.mjs new file mode 100644 index 0000000000000..fa1264013fae0 --- /dev/null +++ b/components/box/actions/get-comments/get-comments.mjs @@ -0,0 +1,48 @@ +import app from "../../box.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + name: "Get Comments", + description: "Fetches comments for a file. [See the documentation](https://developer.box.com/reference/get-files-id-comments/).", + key: "box-get-comments", + version: "0.0.1", + type: "action", + props: { + app, + folderId: { + propDefinition: [ + app, + "parentId", + ], + label: "Parent Folder", + description: "Use this option to select your File ID from a dropdown list.", + }, + fileId: { + propDefinition: [ + app, + "fileId", + (c) => ({ + folderId: c.folderId, + }), + ], + label: "File ID", + description: "The file ID to get comments from. Use a custom expression to reference a file from your workflow or select it from the dropdown list.", + }, + }, + async run({ $ }) { + const results = []; + const resourcesStream = utils.getResourcesStream({ + resourceFn: this.app.getComments, + resourceFnArgs: { + $, + fileId: this.fileId, + }, + }); + for await (const resource of resourcesStream) { + results.push(resource); + } + // eslint-disable-next-line multiline-ternary + $.export("$summary", results.length ? `Successfully fetched ${results.length} comment${results.length === 1 ? "" : "s"}.` : "No comments found."); + return results; + }, +}; diff --git a/components/box/actions/search-content/search-content.mjs b/components/box/actions/search-content/search-content.mjs index 47cf6ac962ed9..78ade1dc6d1a4 100644 --- a/components/box/actions/search-content/search-content.mjs +++ b/components/box/actions/search-content/search-content.mjs @@ -6,7 +6,7 @@ export default { name: "Search Content", description: "Searches for files, folders, web links, and shared files across the users content or across the entire enterprise. [See the documentation](https://developer.box.com/reference/get-search/).", key: "box-search-content", - version: "0.0.3", + version: "0.0.4", type: "action", props: { app, diff --git a/components/box/actions/upload-file-version/upload-file-version.mjs b/components/box/actions/upload-file-version/upload-file-version.mjs index 102b1cd5eacfd..48a9ed40212a6 100644 --- a/components/box/actions/upload-file-version/upload-file-version.mjs +++ b/components/box/actions/upload-file-version/upload-file-version.mjs @@ -5,7 +5,7 @@ export default { name: "Upload File Version", description: "Update a file's content. [See the documentation](https://developer.box.com/reference/post-files-id-content/).", key: "box-upload-file-version", - version: "0.0.1", + version: "0.0.2", type: "action", props: { app, diff --git a/components/box/actions/upload-file/upload-file.mjs b/components/box/actions/upload-file/upload-file.mjs index ca3e90ca3e38b..6e9f42e3ec6b8 100644 --- a/components/box/actions/upload-file/upload-file.mjs +++ b/components/box/actions/upload-file/upload-file.mjs @@ -5,7 +5,7 @@ export default { name: "Upload a File", description: "Uploads a small file to Box. [See the documentation](https://developer.box.com/reference/post-files-content/).", key: "box-upload-file", - version: "0.0.3", + version: "0.0.4", type: "action", props: { app, diff --git a/components/box/box.app.mjs b/components/box/box.app.mjs index 49ac49a68acdb..f7a2cbd47cb44 100644 --- a/components/box/box.app.mjs +++ b/components/box/box.app.mjs @@ -310,5 +310,14 @@ export default { ...args, }); }, + async getComments({ + fileId, ...args + } = {}) { + return this._makeRequest({ + method: "GET", + path: `/files/${fileId}/comments`, + ...args, + }); + }, }, }; diff --git a/components/box/package.json b/components/box/package.json index e07cef5ece0b8..40752ed28373d 100644 --- a/components/box/package.json +++ b/components/box/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/box", - "version": "0.2.0", + "version": "0.3.0", "description": "Pipedream Box Components", "main": "box.app.mjs", "keywords": [ diff --git a/components/box/sources/new-event/new-event.mjs b/components/box/sources/new-event/new-event.mjs index dab2a5271eed5..58f449c354429 100644 --- a/components/box/sources/new-event/new-event.mjs +++ b/components/box/sources/new-event/new-event.mjs @@ -5,7 +5,7 @@ export default { key: "box-new-event", name: "New Event", description: "Emit new event when an event with subscribed event source triggered on a target. [See the documentation](https://developer.box.com/reference/post-webhooks)", - version: "0.0.3", + version: "0.0.4", type: "source", dedupe: "unique", ...common, diff --git a/components/box/sources/new-file/new-file.mjs b/components/box/sources/new-file/new-file.mjs index c2acd333ae37a..99e6778201c3c 100644 --- a/components/box/sources/new-file/new-file.mjs +++ b/components/box/sources/new-file/new-file.mjs @@ -4,7 +4,7 @@ export default { key: "box-new-file", name: "New File Event", description: "Emit new event when a new file uploaded on a target. [See the documentation](https://developer.box.com/reference/post-webhooks)", - version: "0.0.3", + version: "0.0.4", type: "source", dedupe: "unique", ...common, diff --git a/components/box/sources/new-folder/new-folder.mjs b/components/box/sources/new-folder/new-folder.mjs index 694a49539aec7..f66753c6047b2 100644 --- a/components/box/sources/new-folder/new-folder.mjs +++ b/components/box/sources/new-folder/new-folder.mjs @@ -4,7 +4,7 @@ export default { key: "box-new-folder", name: "New Folder Event", description: "Emit new event when a new folder created on a target. [See the documentation](https://developer.box.com/reference/post-webhooks)", - version: "0.0.3", + version: "0.0.4", type: "source", dedupe: "unique", ...common, diff --git a/components/boxhero/README.md b/components/boxhero/README.md new file mode 100644 index 0000000000000..4619ae8d673ac --- /dev/null +++ b/components/boxhero/README.md @@ -0,0 +1,11 @@ +# Overview + +The BoxHero API provides tools to manage inventory in a streamlined and efficient way. By integrating with Pipedream, you can automate inventory tracking, update stock levels in real-time, and synchronize data across platforms. Create powerful workflows that trigger actions based on inventory changes, connect to other apps for a seamless operation, or analyze inventory data for smarter decision-making. + +# Example Use Cases + +- **Inventory Low Stock Alert**: Set up a workflow that monitors inventory levels and sends alerts via email or Slack when stock for a particular item falls below a threshold. This ensures timely reordering and helps prevent stockouts. + +- **Order Fulfillment Automation**: Automate order processing by integrating BoxHero with an e-commerce platform like Shopify. When a new order is placed, the workflow can deduct the purchased items from inventory in BoxHero and update the order status accordingly. + +- **Inventory Data Sync and Analysis**: Keep your inventory data in sync across platforms by connecting BoxHero to a database like Google Sheets or Airtable. Use this data to run analyses, generate reports, or visualize trends to inform business strategy. diff --git a/components/brainshop/README.md b/components/brainshop/README.md new file mode 100644 index 0000000000000..60d2b0f312851 --- /dev/null +++ b/components/brainshop/README.md @@ -0,0 +1,11 @@ +# Overview + +The BrainShop API offers a conversational AI interface, allowing you to integrate a chatbot into various applications or services. On Pipedream, you can connect BrainShop API with numerous other apps to create dynamic workflows that respond to events or triggers. Leveraging Pipedream's serverless platform, you can automate interactions, analyze sentiment, and engage users with minimal effort. + +# Example Use Cases + +- **Customer Support Auto-Responder**: Automate customer support by triggering a Pipedream workflow when a support ticket is created in an app like Zendesk. The workflow can use BrainShop API to generate a response and post it back to the ticket, providing immediate engagement. + +- **Social Media Interaction Bot**: Create a bot that interacts with users on social media platforms such as Twitter. With Pipedream, you can listen for specific keywords or phrases on tweets, pass them to BrainShop API to come up with witty or helpful responses, and automatically reply to the tweets. + +- **Feedback Analysis Tool**: Collect feedback from various sources like email or web forms and send it to BrainShop API to analyze sentiment. Integrate this with a data visualization tool like Google Sheets or Tableau on Pipedream to create reports and dashboards that track customer satisfaction over time. diff --git a/components/brainshop/brainshop.app.mjs b/components/brainshop/brainshop.app.mjs new file mode 100644 index 0000000000000..bf5346a278f99 --- /dev/null +++ b/components/brainshop/brainshop.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "brainshop", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/brainshop/package.json b/components/brainshop/package.json new file mode 100644 index 0000000000000..5ed864a485abe --- /dev/null +++ b/components/brainshop/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/brainshop", + "version": "0.0.1", + "description": "Pipedream BrainShop Components", + "main": "brainshop.app.mjs", + "keywords": [ + "pipedream", + "brainshop" + ], + "homepage": "https://pipedream.com/apps/brainshop", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/braintree/README.md b/components/braintree/README.md new file mode 100644 index 0000000000000..f46f9a7d768a7 --- /dev/null +++ b/components/braintree/README.md @@ -0,0 +1,11 @@ +# Overview + +The Braintree API provides a robust platform for handling online payments, including payment processing, merchant account management, fraud prevention, and data analysis. With Pipedream, you can automate workflows that trigger on a variety of Braintree events, such as new transactions or customer records. These automations can integrate seamlessly with other services, facilitating custom notifications, data synchronization, and more, streamlining ecommerce operations and enhancing customer experiences. + +# Example Use Cases + +- **Automate Invoice Generation**: When a new transaction is processed in Braintree, use Pipedream to trigger a workflow that creates an invoice using a service like QuickBooks or sends a formatted invoice directly to the customer's email using SendGrid. + +- **Sync New Customers to CRM**: After a customer makes their first purchase through Braintree, automatically add their details to a CRM like Salesforce or HubSpot in Pipedream. This ensures your sales team has up-to-date information and can follow up for future sales opportunities. + +- **Fraud Alert System**: Configure a Pipedream workflow to monitor transactions in Braintree for signs of fraud, such as irregular transaction amounts or volumes. If suspected fraud is detected, send an alert to a Slack channel or an email to the security team for immediate review. diff --git a/components/brandmentions/README.md b/components/brandmentions/README.md new file mode 100644 index 0000000000000..2d58952b39f17 --- /dev/null +++ b/components/brandmentions/README.md @@ -0,0 +1,11 @@ +# Overview + +The BrandMentions API lets you monitor and engage with online conversations about your brand in real-time. It provides insights into brand mentions across various channels, including social media, blogs, forums, and news sites. Pipedream serves as a powerful platform, enabling you to build automated workflows that respond to brand mentions, giving you the ability to track sentiment, identify influencers, and manage your brand’s online reputation efficiently. + +# Example Use Cases + +- **Brand Reputation Management**: Set up a Pipedream workflow that triggers whenever your brand is mentioned. You can filter mentions by sentiment, reach, or specific keywords, and then automatically log this data into a Google Sheet for analysis or send it to Slack for your team to review and act upon immediately. + +- **Influencer Identification and Outreach**: Detect mentions from influential voices in your industry with the BrandMentions API on Pipedream. Once an influencer is identified, automatically send their details to a CRM like HubSpot or Salesforce, and create tasks for your marketing team to reach out for partnerships or collaborations. + +- **Customer Support Enhancement**: Improve your customer support by integrating BrandMentions with a ticketing system like Zendesk through Pipedream. Automatically create support tickets when a negative mention is detected, ensuring that your support team can swiftly address customer concerns and manage potential crises before they escalate. diff --git a/components/braze/README.md b/components/braze/README.md new file mode 100644 index 0000000000000..8fc3e0ccf179e --- /dev/null +++ b/components/braze/README.md @@ -0,0 +1,11 @@ +# Overview + +The Braze API allows you to automate your customer relationship management by engaging with users through various channels like email, push notifications, and in-app messages. By leveraging the Braze API on Pipedream, you can create customized, scalable workflows to streamline your marketing campaigns, user segmentation, and event tracking. With real-time data processing and the ability to connect with multiple services, Pipedream enhances the power of Braze, allowing for more dynamic and responsive user engagement strategies. + +# Example Use Cases + +- **Sync New Sign-ups to Braze**: Automatically add new users from your app to Braze as soon as they sign up. Pair this with a welcome email campaign in Braze to engage users from day one. + +- **User Segmentation Based on Activity**: Send user behavior data from your app to Braze to segment users based on their in-app activity. Use these segments to trigger personalized marketing campaigns. + +- **Real-time Event Response**: Trigger messages or actions in Braze in response to real-time events from your app, like a purchase or a high-score achievement, to create timely and relevant user engagement. diff --git a/components/breathe/actions/approve-or-reject-leave-request/approve-or-reject-leave-request.mjs b/components/breathe/actions/approve-or-reject-leave-request/approve-or-reject-leave-request.mjs new file mode 100644 index 0000000000000..d6f7ab71a7763 --- /dev/null +++ b/components/breathe/actions/approve-or-reject-leave-request/approve-or-reject-leave-request.mjs @@ -0,0 +1,72 @@ +import breathe from "../../breathe.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "breathe-approve-or-reject-leave-request", + name: "Approve or Reject Leave Request", + description: "Approve or reject an employee leave request in Breathe. [See the documentation](https://developer.breathehr.com/examples#!/leave_requests)", + version: "0.0.1", + type: "action", + props: { + breathe, + employeeId: { + propDefinition: [ + breathe, + "employeeId", + ], + }, + leaveRequestId: { + propDefinition: [ + breathe, + "leaveRequestId", + (c) => ({ + employeeId: c.employeeId, + }), + ], + }, + approveOrReject: { + type: "string", + label: "Approve or Reject?", + description: "Whether to approve or reject the leave request", + options: [ + "approve", + "reject", + ], + }, + rejectionReason: { + type: "string", + label: "Rejection Reason", + description: "The reason for rejecting the leave request", + optional: true, + }, + }, + async run({ $ }) { + if (this.approveOrReject === "reject" && !this.rejectionReason) { + throw new ConfigurationError("Rejection Reason is required if rejecting the leave request"); + } + + let response; + if (this.approveOrReject === "reject") { + response = await this.breathe.rejectLeaveRequest({ + $, + leaveRequestId: this.leaveRequestId, + data: { + leave_request: { + rejection_reason: this.rejectionReason, + }, + }, + }); + } + else { + response = await this.breathe.approveLeaveRequest({ + $, + leaveRequestId: this.leaveRequestId, + }); + } + + $.export("$summary", `Successfully ${this.approveOrReject === "reject" + ? "rejected" + : "approved"} leave request with ID: ${this.leaveRequestId}`); + return response; + }, +}; diff --git a/components/breathe/actions/create-employee/create-employee.mjs b/components/breathe/actions/create-employee/create-employee.mjs new file mode 100644 index 0000000000000..47d61cc617e52 --- /dev/null +++ b/components/breathe/actions/create-employee/create-employee.mjs @@ -0,0 +1,118 @@ +import breathe from "../../breathe.app.mjs"; + +export default { + key: "breathe-create-employee", + name: "Create Employee", + description: "Creates a new employee in Breathe. [See the documentation](https://developer.breathehr.com/examples#!/employees/POST_version_employees_json)", + version: "0.0.1", + type: "action", + props: { + breathe, + firstName: { + type: "string", + label: "First Name", + description: "First name of the employee", + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the employee", + }, + email: { + type: "string", + label: "Email", + description: "Email address of the employee", + }, + companyJoinDate: { + type: "string", + label: "Company Join Date", + description: "The date that the employee joined the company. Example: `2023-12-25`", + }, + dob: { + type: "string", + label: "Date of Birth", + description: "The date of birth of the employee", + optional: true, + }, + jobTitle: { + type: "string", + label: "Job Title", + description: "The job title of the employee", + optional: true, + }, + departmentId: { + propDefinition: [ + breathe, + "departmentId", + ], + }, + divisionId: { + propDefinition: [ + breathe, + "divisionId", + ], + }, + locationId: { + propDefinition: [ + breathe, + "locationId", + ], + }, + workingPatternId: { + propDefinition: [ + breathe, + "workingPatternId", + ], + }, + holidayAllowanceId: { + propDefinition: [ + breathe, + "holidayAllowanceId", + ], + }, + workMobile: { + type: "string", + label: "Work Mobile", + description: "The work moblie phone number of the employee", + optional: true, + }, + personalMobile: { + type: "string", + label: "Personal Mobile", + description: "The personal mobile phone number of the employee", + optional: true, + }, + homeTelephone: { + type: "string", + label: "Home Telephone", + description: "The home telephone number of the employee", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.breathe.createEmployee({ + $, + data: { + employee: { + person_type: "Employee", + first_name: this.firstName, + last_name: this.lastName, + email: this.email, + company_join_date: this.companyJoinDate, + dob: this.dob, + job_title: this.jobTitle, + work_mobile: this.workMobile, + personal_mobile: this.personalMobile, + home_telephone: this.homeTelephone, + department: this.departmentId, + division: this.divisionId, + location: this.locationId, + working_pattern: this.workingPatternId, + holiday_allowance: this.holidayAllowanceId, + }, + }, + }); + $.export("$summary", `Successfully created employee with ID: ${response.employees[0].id}`); + return response; + }, +}; diff --git a/components/breathe/actions/create-leave-request/create-leave-request.mjs b/components/breathe/actions/create-leave-request/create-leave-request.mjs new file mode 100644 index 0000000000000..3520598756e8d --- /dev/null +++ b/components/breathe/actions/create-leave-request/create-leave-request.mjs @@ -0,0 +1,57 @@ +import breathe from "../../breathe.app.mjs"; + +export default { + key: "breathe-create-leave-request", + name: "Create Leave Request", + description: "Creates a new leave request for an employee in Breathe. [See the documentation](https://developer.breathehr.com/examples#!/employees/POST_version_employees_id_leave_requests_json)", + version: "0.0.1", + type: "action", + props: { + breathe, + employeeId: { + propDefinition: [ + breathe, + "employeeId", + ], + }, + startDate: { + type: "string", + label: "Start Date", + description: "The start date of the leave request. Example: `2023-12-25`", + }, + endDate: { + type: "string", + label: "End Date", + description: "The end date of the leave request. Example: `2023-12-27`", + }, + halfStart: { + type: "boolean", + label: "Half Start", + description: "Set to `true` if requesting a half-day", + default: false, + optional: true, + }, + notes: { + type: "string", + label: "Notes", + description: "Notes about the leave request", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.breathe.createLeaveRequest({ + $, + employeeId: this.employeeId, + data: { + leave_request: { + start_date: this.startDate, + end_date: this.endDate, + half_start: this.halfStart, + notes: this.notes, + }, + }, + }); + $.export("$summary", `Successfully created leave for employee with ID: ${this.employeeId}`); + return response; + }, +}; diff --git a/components/breathe/breathe.app.mjs b/components/breathe/breathe.app.mjs new file mode 100644 index 0000000000000..aaedd80aa5521 --- /dev/null +++ b/components/breathe/breathe.app.mjs @@ -0,0 +1,244 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "breathe", + propDefinitions: { + employeeId: { + type: "string", + label: "Employee ID", + description: "The identifier of an employee", + async options({ page }) { + const { employees } = await this.listEmployees({ + params: { + page: page + 1, + }, + }); + return employees?.map(({ + id: value, first_name: firstName, last_name: lastName, + }) => ({ + value, + label: `${firstName} ${lastName}`, + })) || []; + }, + }, + leaveRequestId: { + type: "string", + label: "Leave Request ID", + description: "The identifier of a leave request", + async options({ + page, employeeId, + }) { + const { leave_requests: leaveRequests } = await this.listLeaveRequests({ + params: { + page: page + 1, + employee_id: employeeId, + exclude_cancelled_requests: true, + }, + }); + return leaveRequests?.map(({ + id: value, start_date: startDate, end_date: endDate, + }) => ({ + value, + label: `${startDate} - ${endDate}`, + })) || []; + }, + }, + departmentId: { + type: "string", + label: "Department ID", + description: "The identifier of a department", + optional: true, + async options({ page }) { + const { departments } = await this.listDepartments({ + params: { + page: page + 1, + }, + }); + return departments?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + divisionId: { + type: "string", + label: "Division ID", + description: "The identifier of a division", + optional: true, + async options() { + const { divisions } = await this.listDivisions(); + return divisions?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + locationId: { + type: "string", + label: "Location ID", + description: "The identifier of a location", + optional: true, + async options() { + const { locations } = await this.listLocations(); + return locations?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + workingPatternId: { + type: "string", + label: "Working Pattern ID", + description: "The identifier of a working pattern", + optional: true, + async options() { + const { working_patterns: workingPatterns } = await this.listWorkingPatterns(); + return workingPatterns?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + holidayAllowanceId: { + type: "string", + label: "Holiday Allowance ID", + description: "The identifier of a holiday allowance", + optional: true, + async options() { + const { holiday_allowances: holidayAllowances } = await this.listHolidayAllowances(); + return holidayAllowances?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return `${this.$auth.api_url}/v1`; + }, + _makeRequest({ + $ = this, + path, + ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "x-api-key": `${this.$auth.api_key}`, + }, + }); + }, + listEmployees(opts = {}) { + return this._makeRequest({ + path: "/employees", + ...opts, + }); + }, + listDepartments(opts = {}) { + return this._makeRequest({ + path: "/departments", + ...opts, + }); + }, + listDivisions(opts = {}) { + return this._makeRequest({ + path: "/divisions", + ...opts, + }); + }, + listLocations(opts = {}) { + return this._makeRequest({ + path: "/locations", + ...opts, + }); + }, + listWorkingPatterns(opts = {}) { + return this._makeRequest({ + path: "/working_patterns", + ...opts, + }); + }, + listHolidayAllowances(opts = {}) { + return this._makeRequest({ + path: "/holiday_allowances", + ...opts, + }); + }, + listLeaveRequests(opts = {}) { + return this._makeRequest({ + path: "/leave_requests", + ...opts, + }); + }, + createEmployee(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/employees", + ...opts, + }); + }, + createLeaveRequest({ + employeeId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/employees/${employeeId}/leave_requests`, + ...opts, + }); + }, + approveLeaveRequest({ + leaveRequestId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/leave_requests/${leaveRequestId}/approve`, + ...opts, + }); + }, + rejectLeaveRequest({ + leaveRequestId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/leave_requests/${leaveRequestId}/reject`, + ...opts, + }); + }, + async *paginate({ + resourceFn, + args, + resourceKey, + }) { + args = { + ...args, + params: { + ...args?.params, + page: 1, + }, + }; + let total; + do { + const response = await resourceFn(args); + const items = response[resourceKey]; + for (const item of items) { + yield item; + } + total = items?.length; + args.params.page++; + } while (total); + }, + }, +}; diff --git a/components/breathe/package.json b/components/breathe/package.json new file mode 100644 index 0000000000000..352d266e38dd3 --- /dev/null +++ b/components/breathe/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/breathe", + "version": "0.1.0", + "description": "Pipedream Breathe Components", + "main": "breathe.app.mjs", + "keywords": [ + "pipedream", + "breathe" + ], + "homepage": "https://pipedream.com/apps/breathe", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/breathe/sources/common/base.mjs b/components/breathe/sources/common/base.mjs new file mode 100644 index 0000000000000..629e8a6e07109 --- /dev/null +++ b/components/breathe/sources/common/base.mjs @@ -0,0 +1,69 @@ +import breathe from "../../breathe.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + breathe, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || "1970-01-01"; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + getArgs() { + return {}; + }, + isRelevant() { + return true; + }, + getTsField() { + throw new Error("getTsField is not implemented"); + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + getResourceKey() { + throw new Error("getResourceKey is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run() { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + + const tsField = this.getTsField(); + + const results = this.breathe.paginate({ + resourceFn: this.getResourceFn(), + args: this.getArgs(lastTs), + resourceKey: this.getResourceKey(), + }); + + for await (const item of results) { + const ts = Date.parse(item[tsField]); + if (ts >= Date.parse(lastTs)) { + if (this.isRelevant(item)) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + } + + if (ts > Date.parse(maxTs)) { + maxTs = item[tsField]; + } + } + } + + this._setLastTs(maxTs); + }, +}; diff --git a/components/breathe/sources/employee-updated/employee-updated.mjs b/components/breathe/sources/employee-updated/employee-updated.mjs new file mode 100644 index 0000000000000..2cf02beca7b65 --- /dev/null +++ b/components/breathe/sources/employee-updated/employee-updated.mjs @@ -0,0 +1,34 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "breathe-employee-updated", + name: "Employee Updated", + description: "Emit new event when an existing employee is updated in Breathe. [See the documentation](https://developer.breathehr.com/examples#!/employees/GET_version_employees_json)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTsField() { + return "updated_at"; + }, + getResourceFn() { + return this.breathe.listEmployees; + }, + getResourceKey() { + return "employees"; + }, + isRelevant(employee) { + return !(employee.created_at === employee.updated_at); + }, + generateMeta(employee) { + const ts = Date.parse(employee.updated_at); + return { + id: `${employee.id}-${ts}`, + summary: `Employee Updated: ${employee.first_name} ${employee.last_name}`, + ts, + }; + }, + }, +}; diff --git a/components/breathe/sources/new-employee-created/new-employee-created.mjs b/components/breathe/sources/new-employee-created/new-employee-created.mjs new file mode 100644 index 0000000000000..60dbe5c57812e --- /dev/null +++ b/components/breathe/sources/new-employee-created/new-employee-created.mjs @@ -0,0 +1,30 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "breathe-new-employee-created", + name: "New Employee Created", + description: "Emit new event when a new employee is created in Breathe. [See the documentation](https://developer.breathehr.com/examples#!/employees/GET_version_employees_json).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTsField() { + return "created_at"; + }, + getResourceFn() { + return this.breathe.listEmployees; + }, + getResourceKey() { + return "employees"; + }, + generateMeta(employee) { + return { + id: employee.id, + summary: `New Employee: ${employee.first_name} ${employee.last_name}`, + ts: Date.parse(employee.created_at), + }; + }, + }, +}; diff --git a/components/breathe/sources/new-leave-request-created/new-leave-request-created.mjs b/components/breathe/sources/new-leave-request-created/new-leave-request-created.mjs new file mode 100644 index 0000000000000..41e83fa1f43c8 --- /dev/null +++ b/components/breathe/sources/new-leave-request-created/new-leave-request-created.mjs @@ -0,0 +1,57 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "breathe-new-leave-request-created", + name: "New Leave Request Created", + description: "Emit new event when a new employee leave request is created in Breathe. [See the documentation](https://developer.breathehr.com/examples#!/employees/GET_version_employees_id_leave_requests_json)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + employeeIds: { + propDefinition: [ + common.props.breathe, + "employeeId", + ], + type: "string[]", + label: "Employee IDs", + description: "Return leave requests for the selected employees only. If no employees are selected, leave requests for all employees will be returned.", + optional: true, + }, + }, + methods: { + ...common.methods, + getTsField() { + return "created_at"; + }, + getResourceFn() { + return this.breathe.listLeaveRequests; + }, + getArgs(lastTs) { + const args = { + params: { + startDate: lastTs, + }, + }; + if (this.employeeIds?.length === 1) { + args.params.employee_id = this.employeeIds[0]; + } + return args; + }, + getResourceKey() { + return "leave_requests"; + }, + isRelevant(leaveRequest) { + return !this.employeeIds?.length || this.employeeIds.includes(leaveRequest.employee.id); + }, + generateMeta(leaveRequest) { + return { + id: leaveRequest.id, + summary: `New Leave Request ID: ${leaveRequest.id}`, + ts: Date.parse(leaveRequest.created_at), + }; + }, + }, +}; diff --git a/components/breeze/README.md b/components/breeze/README.md index 90433275331ae..d467603b2333e 100644 --- a/components/breeze/README.md +++ b/components/breeze/README.md @@ -1,9 +1,11 @@ # Overview -Breeze API is a powerful tool that lets you create beautiful and responsive websites. With Breeze API, you can create websites that look amazing on any device. Breeze API is also extremely user-friendly, so you can create websites that are both beautiful and user-friendly. +Breeze is a project management tool designed to streamline task planning, collaboration, and tracking for teams. With the Breeze API, you can automate routine project management tasks, sync data with other tools, and create custom notifications to keep your team aligned. By leveraging Pipedream's serverless platform, you can connect Breeze to hundreds of apps, set up complex workflows, and manipulate data in real-time—no server required. -Some examples of what you can build with Breeze API include: +# Example Use Cases -- A website that looks amazing on any device -- A user-friendly website -- A website that is both beautiful and user-friendly +- **Project Dashboard Sync**: Integrate Breeze with Google Sheets on Pipedream to create a live project dashboard. Automatically export task statuses, team workloads, and time tracking data to a shared Google Sheet, ensuring that stakeholders always have access to the latest project insights. + +- **Slack Project Updates**: Use Pipedream to connect Breeze with Slack. Set up an automation that posts daily project updates to a dedicated Slack channel, including new tasks, completed items, and upcoming deadlines, to keep the entire team informed without manual reporting. + +- **Time-Tracking Analysis**: Combine Breeze with a data visualization tool like Power BI on Pipedream. Extract time-tracking data from Breeze to analyze project efficiency, team performance, and to identify bottlenecks. Share insights through automatically generated and distributed reports. diff --git a/components/breeze/package.json b/components/breeze/package.json new file mode 100644 index 0000000000000..98dbceadda1bf --- /dev/null +++ b/components/breeze/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/breeze", + "version": "0.6.0", + "description": "Pipedream breeze Components", + "main": "breeze.app.mjs", + "keywords": [ + "pipedream", + "breeze" + ], + "homepage": "https://pipedream.com/apps/breeze", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/breezy_hr/README.md b/components/breezy_hr/README.md index 8b0bcbb09e297..94832981b017c 100644 --- a/components/breezy_hr/README.md +++ b/components/breezy_hr/README.md @@ -1,12 +1,11 @@ # Overview -The Breezy HR API allows developers to interact with Breezy HR data in order to -build applications that can perform various Human Resources-related tasks. Some -examples of what you can build using the Breezy HR API include: - -- An application that can help job seekers create and manage their resumes -- A tool that helps managers post job openings and track applications -- A tool that helps businesses keep track of employee vacation days -- An application that helps employees find internal job openings -- A tool that helps businesses track the progress of job applicants through the - hiring process +Breezy HR's API unlocks the power to streamline recruitment processes by automating routine tasks and integrating with a multitude of other apps. With this API on Pipedream, you can craft custom workflows to manage candidates, coordinate interview schedules, trigger communications, and track the hiring pipeline effortlessly. By harnessing data from Breezy HR, you can enhance efficiency, reduce manual errors, and keep the entire hiring team on the same page. + +# Example Use Cases + +- **Candidate Screening Automation**: When new candidates apply, automatically screen their applications based on predefined criteria such as keywords in their resume, years of experience, or required skills. Qualified candidates can be tagged and advanced to the next stage in Breezy HR, while others receive a personalized email informing them of their status. + +- **Interview Coordination**: Sync candidate interview schedules with team calendars using Google Calendar or Office 365. When an interview is arranged in Breezy HR, Pipedream can book corresponding calendar slots and send out invites, ensuring no double bookings occur. It can also send reminders to both the interviewers and the candidate a day before the scheduled interview. + +- **New Hire Onboarding**: Kick off onboarding workflows once a candidate accepts an offer. This could involve creating user accounts in apps like Slack or Office 365, sending out welcome emails with necessary documentation, and adding new hires to relevant mailing lists and project management tools such as Trello or Asana. diff --git a/components/breezy_hr/package.json b/components/breezy_hr/package.json new file mode 100644 index 0000000000000..5c6930a243941 --- /dev/null +++ b/components/breezy_hr/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/breezy_hr", + "version": "0.6.0", + "description": "Pipedream breezy_hr Components", + "main": "breezy_hr.app.mjs", + "keywords": [ + "pipedream", + "breezy_hr" + ], + "homepage": "https://pipedream.com/apps/breezy_hr", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/brevo/README.md b/components/brevo/README.md new file mode 100644 index 0000000000000..3fe5288d515b7 --- /dev/null +++ b/components/brevo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Brevo API lets you automate and integrate your user and access management tasks right within Pipedream. With Brevo's API, you can manage users, groups, permissions, and more, streamlining how you control access to your resources. By crafting workflows on Pipedream, you can connect Brevo with other apps to create custom, automated processes that suit your specific business needs. + +# Example Use Cases + +- **Automated User Provisioning**: Use the Brevo API to create a workflow that automatically adds new employees to specific groups and grants permissions when they're added to your HR system, ensuring a smooth onboarding experience. + +- **Dynamic Access Control**: Build a Pipedream workflow to adjust user permissions in real-time based on activity logs or events from security systems. This may involve revoking access when suspicious behavior is detected, or granting temporary access when required. + +- **User Audit and Compliance Reports**: Leverage the Brevo API on Pipedream to generate regular user access reports. Combine data from Brevo with other services, like a database or logging tool, to create comprehensive compliance documentation. diff --git a/components/brevo/actions/add-or-update-contact/add-or-update-contact.mjs b/components/brevo/actions/add-or-update-contact/add-or-update-contact.mjs index 1b5dd64beeb6b..c0d0209084f0e 100644 --- a/components/brevo/actions/add-or-update-contact/add-or-update-contact.mjs +++ b/components/brevo/actions/add-or-update-contact/add-or-update-contact.mjs @@ -4,7 +4,7 @@ export default { key: "brevo-add-or-update-contact", name: "Add or Update a contact", description: "Add or Update a contact", - version: "0.0.1", + version: "0.0.4", type: "action", props: { brevo, @@ -51,14 +51,16 @@ export default { return dynamicProps; }, async run({ $ }) { - let identifier = this.providedIdentifier; + let identifier = this.providedIdentifier || this.email; const listIds = Object.keys(this.listIds).map((key) => parseInt(this.listIds[key], 10)); let contact = null; if (identifier) { - contact = await this.brevo.existingContactByIdentifier( - $, - encodeURIComponent(identifier), - ); + try { + contact = await this.brevo.existingContactByIdentifier($, + identifier); + } catch (e) { + contact = null; + } } const attributes = { @@ -73,16 +75,25 @@ export default { $.export("Defined attributes", attributes); if (contact) { - identifier = contact.id; - const unlinkListIds = contact.listIds.filter((el) => !listIds.includes(el)); - await this.brevo.updateContact( - $, - identifier, - attributes, - listIds, - unlinkListIds, - ); - $.export("$summary", `Successfully updated contact "${identifier}"`); + try { + identifier = contact.id; + const unlinkListIds = contact.listIds.filter((el) => !listIds.includes(el)); + await this.brevo.updateContact( + $, + identifier, + attributes, + listIds, + unlinkListIds, + ); + + $.export("$summary", `Successfully updated contact "${identifier}"`); + } catch ({ response: { data } }) { + let errorMessage = data.message; + if (data.message === "Contact already exist") { + errorMessage = `A contact with email ${this.email} already exists!`; + } + throw new Error(errorMessage); + } } else { const inserted = await this.brevo.addContact( $, diff --git a/components/brevo/package.json b/components/brevo/package.json index 09eece9d1bbee..8b5670a32012e 100644 --- a/components/brevo/package.json +++ b/components/brevo/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/brevo", - "version": "0.1.0", + "version": "0.1.3", "description": "Pipedream Brevo Components", "main": "brevo.app.mjs", "keywords": [ @@ -13,6 +13,6 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.5.1" + "@pipedream/platform": "^3.0.0" } } diff --git a/components/brex/README.md b/components/brex/README.md index 8799a39b91223..214515b2fa89e 100644 --- a/components/brex/README.md +++ b/components/brex/README.md @@ -1,11 +1,11 @@ # Overview -The Brex API enables developers to programmatically manage their Brex account -and cardholders. With the API, developers can create and manage cardholders, -retrieve transactions, and more. +The Brex API offers a powerful avenue for automating financial operations, providing programmatic access to a company's Brex account. Through the API, you can manage cards, view transactions, and handle rewards programmatically. When combined with Pipedream, the API's capabilities expand, allowing users to integrate Brex data with other apps, trigger workflows based on financial events, and streamline financial reporting and reconciliation processes. -Here are some examples of what you can build with the Brex API: +# Example Use Cases -- A tool to help manage your Brex account and cardholders -- A tool to help retrieve transactions -- A tool to help create and manage cardholders +- **Expense Management Automation**: Trigger a Pipedream workflow whenever a new transaction is posted to a Brex account. Automatically categorize the transaction, send a Slack message to the relevant department head for review, and update a Google Sheet used for expense tracking. + +- **Receipt Collection Workflow**: Send an email or a Slack prompt to employees using a Pipedream workflow when a new Brex card transaction lacks a receipt. This can be set up to run periodically and check for transactions that are missing documentation, ensuring compliance with company expense policies. + +- **Real-Time Financial Dashboard**: Use Pipedream to feed Brex transaction data into a BI tool like Tableau or Looker. Set up a webhook that triggers a workflow on each new transaction, which then processes the data and sends it to the BI tool, providing up-to-date financial insights. diff --git a/components/brex_staging/README.md b/components/brex_staging/README.md index 3200e2c12211b..7dd27932dffa0 100644 --- a/components/brex_staging/README.md +++ b/components/brex_staging/README.md @@ -1,19 +1,11 @@ # Overview -The Brex (Staging) API allows developers to access Brex data and build -applications on top of Brex. +The Brex (Staging) API allows you to automate financial operations and integrate with Brex's suite of financial services. This API can be leveraged to synchronize transactions, manage cards, and automate accounting processes, providing real-time insights into your business finances. Using Pipedream, these capabilities can be harnessed to create custom workflows that trigger actions based on financial events, reconcile expenses, and streamline financial reporting. -With the Brex (Staging) API, you can: +# Example Use Cases -- Build apps that integrate with Brex data -- Build custom reports and dashboards -- Automate Brex data flows -- And more! +- **Automated Expense Reporting**: Track expenses by triggering a Pipedream workflow that listens for new transactions on a Brex card. Once a transaction is detected, categorize the expense and automatically send a report to a Google Sheet for accounting reconciliation. -Here are some examples of what you can build with the Brex (Staging) API: +- **Real-Time Budget Alerts**: Create a workflow on Pipedream that monitors your Brex account balances and transaction activity. When spending approaches a set budget limit, automatically send an alert via email or Slack to the finance team to prevent budget overruns. -- A reporting tool that pulls data from Brex and generates custom reports -- A dashboard that visualizes Brex data in real-time -- A tool that helps customers manage their Brex data -- An app that allows customers to access their Brex data on the go -- And more! +- **Streamlined Employee Onboarding**: Automate the process of issuing Brex cards for new employees. When a new employee is added to your HR system (like BambooHR), trigger a Pipedream workflow that sends a request to the Brex API to issue a new card and set spending limits according to their role. diff --git a/components/brex_staging/actions/create-card/create-card.mjs b/components/brex_staging/actions/create-card/create-card.mjs index 4e8205def2fe9..77e02b9637fdb 100644 --- a/components/brex_staging/actions/create-card/create-card.mjs +++ b/components/brex_staging/actions/create-card/create-card.mjs @@ -1,4 +1,4 @@ -import common from "../../../brex/actions/create-card/common.mjs"; +import common from "@pipedream/brex/actions/create-card/common.mjs"; import brexApp from "../../brex_staging.app.mjs"; export default { @@ -6,7 +6,7 @@ export default { name: "Create Card", description: "Creates a new card. [See the docs here](https://developer.brex.com/openapi/team_api/#operation/createCard).", key: "brex_staging-create-card", - version: "0.0.1", + version: "0.0.2", type: "action", props: { brexApp, diff --git a/components/brex_staging/actions/invite-user/invite-user.mjs b/components/brex_staging/actions/invite-user/invite-user.mjs index 4a47c00222afc..d4cae0ca3b6cf 100644 --- a/components/brex_staging/actions/invite-user/invite-user.mjs +++ b/components/brex_staging/actions/invite-user/invite-user.mjs @@ -1,12 +1,12 @@ import brexApp from "../../brex_staging.app.mjs"; -import common from "../../../brex/actions/invite-user/common.mjs"; +import common from "@pipedream/brex/actions/invite-user/common.mjs"; export default { ...common, name: "Invite User", description: "Invites a new user as an employee. [See the docs here](https://developer.brex.com/openapi/team_api/#operation/createUser).", key: "brex_staging-invite-user", - version: "0.0.1", + version: "0.0.2", type: "action", props: { brexApp, diff --git a/components/brex_staging/actions/list-transactions-for-primary-card-account/list-transactions-for-primary-card-account.mjs b/components/brex_staging/actions/list-transactions-for-primary-card-account/list-transactions-for-primary-card-account.mjs index 1a835d4c74b72..f68e66c475cc5 100644 --- a/components/brex_staging/actions/list-transactions-for-primary-card-account/list-transactions-for-primary-card-account.mjs +++ b/components/brex_staging/actions/list-transactions-for-primary-card-account/list-transactions-for-primary-card-account.mjs @@ -1,12 +1,12 @@ import brexApp from "../../brex_staging.app.mjs"; -import common from "../../../brex/actions/list-transactions-for-primary-card-account/common.mjs"; +import common from "@pipedream/brex/actions/list-transactions-for-primary-card-account/common.mjs"; export default { ...common, name: "List Transactions for Primary Card Account", description: "Lists all settled transactions for the primary card account. [See the docs here](https://developer.brex.com/openapi/transactions_api/#operation/listPrimaryCardTransactions).", key: "brex_staging-list-transactions-for-primary-card-account", - version: "0.0.1", + version: "0.0.2", type: "action", props: { brexApp, diff --git a/components/brex_staging/actions/list-transactions-for-selected-cash-account/list-transactions-for-selected-cash-account.mjs b/components/brex_staging/actions/list-transactions-for-selected-cash-account/list-transactions-for-selected-cash-account.mjs index ec0b9acf3f1d1..6adf4d9b203ee 100644 --- a/components/brex_staging/actions/list-transactions-for-selected-cash-account/list-transactions-for-selected-cash-account.mjs +++ b/components/brex_staging/actions/list-transactions-for-selected-cash-account/list-transactions-for-selected-cash-account.mjs @@ -1,12 +1,12 @@ import brexApp from "../../brex_staging.app.mjs"; -import common from "../../../brex/actions/list-transactions-for-selected-cash-account/common.mjs"; +import common from "@pipedream/brex/actions/list-transactions-for-selected-cash-account/common.mjs"; export default { ...common, name: "List Transactions for Selected Cash Account", description: "Lists all transactions for the specified cash account. [See the docs here](https://developer.brex.com/openapi/transactions_api/#operation/listCashTransactions).", key: "brex_staging-list-transactions-for-selected-cash-account", - version: "0.0.1", + version: "0.0.2", type: "action", props: { brexApp, diff --git a/components/brex_staging/actions/set-limit-for-user/set-limit-for-user.mjs b/components/brex_staging/actions/set-limit-for-user/set-limit-for-user.mjs index b37cb4cfbdfee..45d5c271071cb 100644 --- a/components/brex_staging/actions/set-limit-for-user/set-limit-for-user.mjs +++ b/components/brex_staging/actions/set-limit-for-user/set-limit-for-user.mjs @@ -1,12 +1,12 @@ import brexApp from "../../brex_staging.app.mjs"; -import common from "../../../brex/actions/set-limit-for-user/common.mjs"; +import common from "@pipedream/brex/actions/set-limit-for-user/common.mjs"; export default { ...common, name: "Set Limit for User", description: "Sets the monthly limit for a user. [See the docs here](https://developer.brex.com/openapi/team_api/#operation/setUserLimit).", key: "brex_staging-set-limit-for-user", - version: "0.0.1", + version: "0.0.2", type: "action", props: { brexApp, diff --git a/components/brex_staging/brex_staging.app.mjs b/components/brex_staging/brex_staging.app.mjs index 6197099c44982..e8bbc5e79ba1c 100644 --- a/components/brex_staging/brex_staging.app.mjs +++ b/components/brex_staging/brex_staging.app.mjs @@ -1,4 +1,4 @@ -import commonApp from "../brex/common-app.mjs"; +import commonApp from "@pipedream/brex/common-app.mjs"; export default { ...commonApp, diff --git a/components/brex_staging/package.json b/components/brex_staging/package.json index d74f8a5397cfe..e4914c5114469 100644 --- a/components/brex_staging/package.json +++ b/components/brex_staging/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/brex_staging", - "version": "0.0.3", + "version": "0.0.4", "description": "Pipedream Brex Staging Components", "main": "brex_staging.app.js", "keywords": [ @@ -13,6 +13,7 @@ "access": "public" }, "dependencies": { + "@pipedream/brex": "^0.1.0", "axios": "^0.25.0", "crypto": "^1.0.1", "uuid": "^8.3.2" diff --git a/components/brex_staging/sources/new-transfer-event/new-transfer-event.mjs b/components/brex_staging/sources/new-transfer-event/new-transfer-event.mjs index 6718281bff6e3..6bf2214baff60 100644 --- a/components/brex_staging/sources/new-transfer-event/new-transfer-event.mjs +++ b/components/brex_staging/sources/new-transfer-event/new-transfer-event.mjs @@ -1,4 +1,4 @@ -import common from "../../../brex/sources/new-transfer-event/common.mjs"; +import common from "@pipedream/brex/sources/new-transfer-event/common.mjs"; import brexApp from "../../brex_staging.app.mjs"; export default { @@ -8,7 +8,7 @@ export default { key: "brex_staging-new-transfer-event", name: "New Transfer Event (Instant)", description: "Emit new event for new failed or processed events", - version: "0.0.1", + version: "0.0.2", props: { brexApp, ...common.props, diff --git a/components/bridge/README.md b/components/bridge/README.md index e22e72dbd4d89..e75aaa4e172af 100644 --- a/components/bridge/README.md +++ b/components/bridge/README.md @@ -1,7 +1,11 @@ # Overview -- A social media platform -- A discussion forum -- A messaging app -- A voting system -- A performance review system +The Bridge API by Dandelion provides text analysis through natural language processing, enabling you to extract meaning, categorize content, and understand sentiments in large volumes of text. Integrating Bridge with Pipedream opens a realm of possibilities for automating content analysis across various apps, aiding in decision making, content organization, and customer feedback analysis. + +# Example Use Cases + +- **Content Categorization Workflow**: Automatically process incoming support tickets from Zendesk in Pipedream, using the Bridge API to categorize them based on sentiment and content. This allows for prioritizing urgent tickets and routing them to the appropriate department or support agent. + +- **Social Media Sentiment Analysis**: Stream tweets from Twitter using a Pipedream trigger, analyze them with the Bridge API to gauge public sentiment about your brand or products. This real-time feedback can inform marketing strategies and product development decisions. + +- **Customer Feedback Aggregator**: Collect feedback from multiple sources like emails, chatbots, or online forms into a Pipedream workflow. Use Bridge API to understand the sentiment and main topics of the feedback for better insights into customer satisfaction and product improvement. diff --git a/components/bridge/package.json b/components/bridge/package.json new file mode 100644 index 0000000000000..8cac5c61c9e5f --- /dev/null +++ b/components/bridge/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/bridge", + "version": "0.6.0", + "description": "Pipedream bridge Components", + "main": "bridge.app.mjs", + "keywords": [ + "pipedream", + "bridge" + ], + "homepage": "https://pipedream.com/apps/bridge", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/brilliant_directories/README.md b/components/brilliant_directories/README.md new file mode 100644 index 0000000000000..1a09eb0c90bce --- /dev/null +++ b/components/brilliant_directories/README.md @@ -0,0 +1,11 @@ +# Overview + +The Brilliant Directories API lets you tap into the functionality of your directory software, enabling you to automate tasks, manage listings, users, and more. With Pipedream, you can build serverless workflows integrating the Brilliant Directories API to streamline operations, sync data across platforms, and react to events in real-time. + +# Example Use Cases + +- **Automate Member Onboarding**: Set up a workflow that triggers when a new member signs up on Brilliant Directories. The workflow can automatically add the member to a Mailchimp list for email marketing, send a welcome message via Twilio, and create a task in Trello for your team to follow up. + +- **Directory Listing Sync**: Create a workflow that watches for updates to listings on Brilliant Directories and reflects those changes in real-time on other platforms like Salesforce or Airtable. This ensures that your sales team always has the latest information at their fingertips. + +- **User Engagement Tracking**: Develop a workflow that captures user interactions on your directory, such as reviews or messages sent to listings. Feed this data into a Google Sheet for analysis or trigger an automated response via Slack to your customer engagement team to take action. diff --git a/components/brillium/brillium.app.mjs b/components/brillium/brillium.app.mjs new file mode 100644 index 0000000000000..318c547227b06 --- /dev/null +++ b/components/brillium/brillium.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "brillium", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/brillium/package.json b/components/brillium/package.json new file mode 100644 index 0000000000000..ccfe2c6bfaa58 --- /dev/null +++ b/components/brillium/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/brillium", + "version": "0.0.1", + "description": "Pipedream Brillium Components", + "main": "brillium.app.mjs", + "keywords": [ + "pipedream", + "brillium" + ], + "homepage": "https://pipedream.com/apps/brillium", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/brosix/README.md b/components/brosix/README.md index eeefcae284b98..39679296172bb 100644 --- a/components/brosix/README.md +++ b/components/brosix/README.md @@ -1,16 +1,11 @@ # Overview -With the Brosix API, you can build instant messaging and collaboration -applications for a variety of purposes, including: +Brosix is a secure instant messaging platform designed for enterprise communication. With the Brosix API on Pipedream, you can automate various messaging tasks, synchronize communication data across platforms, and trigger workflows from specific events within Brosix. Pipedream's serverless execution model and event-driven architecture enable you to integrate Brosix with numerous other services, enhancing collaboration and productivity without manual intervention. -- Customer service and support -- Internal company communication -- Educational platforms -- And more! +# Example Use Cases -Here are some examples of what you can build with the Brosix API: +- **Automated Welcome Messages for New Team Members**: When a new member is added to a Brosix network, a Pipedream workflow can be triggered to send a personalized welcome message or important company information directly to that new member via Brosix. -- An instant messaging application for customer service and support -- An internal company communication platform -- An educational platform for students and teachers -- And more! +- **Synchronize Chat Logs to a Database for Compliance**: Brosix chat logs can be captured and stored in a database like MySQL or MongoDB using Pipedream to ensure compliance with internal policies or regulations. This workflow can be set to run at regular intervals or triggered by specific messaging events. + +- **Instant Notifications for Project Management Updates**: Connect Brosix with a project management tool like Trello on Pipedream. Whenever a task is updated or a new comment is added in Trello, a notification can be sent to a designated Brosix group or user to keep the team informed in real-time. diff --git a/components/browse_ai/README.md b/components/browse_ai/README.md new file mode 100644 index 0000000000000..9923b2470e0fa --- /dev/null +++ b/components/browse_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +The Browse AI API enables the automation of data extraction from websites, turning web pages into organized data. It's built for non-coders and coders alike, allowing for custom web scraping and monitoring tasks. Within Pipedream, you can harness Browse AI to create intricate workflows that trigger actions in other apps based on the data you extract. Think of automated competitive analysis, price tracking, or content changes detection, all streamlined through Pipedream's serverless platform. + +# Example Use Cases + +- **Track Competitor Price Changes**: Set up a Browse AI task to monitor a competitor's product pricing page. When a price change is detected, trigger a Pipedream workflow that updates a shared Google Sheet and sends a Slack notification to your sales team. + +- **Monitor Website Content Changes**: Employ Browse AI to watch for content updates on industry news sites. Combine it with Pipedream to parse the extracted data and send summaries via Gmail to your marketing team for content strategy alignment. + +- **Alert for Restocked Products**: Create a Browse AI task to check for stock updates on e-commerce sites. Use Pipedream to link this data with Twilio, sending an SMS alert to interested customers when their desired product is back in stock. diff --git a/components/browserbase/browserbase.app.mjs b/components/browserbase/browserbase.app.mjs new file mode 100644 index 0000000000000..741071514a090 --- /dev/null +++ b/components/browserbase/browserbase.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "browserbase", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/browserbase/package.json b/components/browserbase/package.json new file mode 100644 index 0000000000000..97bd71a53c20c --- /dev/null +++ b/components/browserbase/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/browserbase", + "version": "0.0.1", + "description": "Pipedream Browserbase Components", + "main": "browserbase.app.mjs", + "keywords": [ + "pipedream", + "browserbase" + ], + "homepage": "https://pipedream.com/apps/browserbase", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/browserhub/README.md b/components/browserhub/README.md new file mode 100644 index 0000000000000..8f2c3c057b87f --- /dev/null +++ b/components/browserhub/README.md @@ -0,0 +1,11 @@ +# Overview + +The Browserhub API offers automation and control over browser sessions, enabling users to create, manipulate, and extract data from web pages programmatically. Integrating Browserhub with Pipedream opens a world of possibilities for automating web interaction workflows, monitoring website changes, scraping data, and testing web applications. Pipedream's serverless platform allows for easy orchestration of API calls and data handling, making it a powerful tool for developers to harness the capabilities of Browserhub in a scalable and efficient manner. + +# Example Use Cases + +- **Automated Data Scraping and Aggregation**: Use Browserhub to navigate to specific web pages, extract relevant data, and then use Pipedream to process and store that data in a Google Sheet. This could be useful for market research, competitive analysis, or aggregating content from multiple sources. + +- **Continuous Website Monitoring for Changes**: Set up a Pipedream workflow that triggers Browserhub to periodically check a website for changes or updates. When changes are detected, Pipedream can send alerts via email or Slack, ensuring that stakeholders are promptly informed about critical updates. + +- **Automated Browser-Based Testing**: Leverage Browserhub's API to run automated tests on your web application. Pipedream can orchestrate different test scenarios, collect results, and report any issues via GitHub issues or Jira, facilitating a seamless CI/CD pipeline. diff --git a/components/browserless/README.md b/components/browserless/README.md index d741e3043864e..c36a7c5f91d2e 100644 --- a/components/browserless/README.md +++ b/components/browserless/README.md @@ -1,12 +1,11 @@ # Overview -Browserless allows you to build websites and applications that can be used -without a web browser. This means that you can create apps that can be used -offline, or that can be used on devices that don't have a web browser -installed. +The Browserless API on Pipedream allows you to automate browser actions without the overhead of managing your own browser infrastructure. This service provides a way to run Chrome browser sessions programmatically, making it ideal for web scraping, automated testing, and screenshot capture. Leveraging this on Pipedream, you can create serverless workflows that interact with web pages, extract data, and perform actions as a human would, all in a scalable and efficient manner. -Some examples of things that you can build using Browserless are: +# Example Use Cases -- A website that can be used without a web browser -- An app that can be used offline -- A app that can be used on devices that don't have a web browser installed +- **Automated Website Testing**: Employ Browserless to run end-to-end tests on your web application after deploying updates. Trigger these tests through Pipedream workflows whenever you push new code to your repository using a service like GitHub. Pipedream can process the results, and if tests fail, automatically notify your team via Slack or email. + +- **Scheduled Website Screenshots**: Set up a regular snapshot regime of your website or competitor websites by using Browserless to capture screenshots. Pipedream can schedule these actions and save the images to cloud storage platforms such as Google Drive or Dropbox. Use these snapshots to monitor visual changes or keep an archive of web page history. + +- **Web Scraping for Data Analysis**: Leverage Browserless to scrape data from websites that require JavaScript rendering. Pipedream can orchestrate this process, aggregate the data, and push it to data analysis tools such as Google Sheets or a database. This can be particularly useful for compiling market research or tracking price changes over time. diff --git a/components/browserstack/README.md b/components/browserstack/README.md index 28ea3f4239231..8d8479ac50e76 100644 --- a/components/browserstack/README.md +++ b/components/browserstack/README.md @@ -1,7 +1,11 @@ # Overview -With BrowserStack, you can build applications that are: +The BrowserStack API allows you to automate web and mobile application testing by providing instant access to multiple desktop and mobile browsers. With this API, you can manage your tests, integrate with CI/CD pipelines, and get real-time results from your testing environment. Whether you're looking to streamline your development workflow, or automate repetitive testing tasks, BrowserStack's API via Pipedream provides the flexibility to tailor your testing processes and feedback loops for efficiency and thorough coverage. -- Compatible with a wide range of browsers and devices -- Automated and scalable -- Easily testable +# Example Use Cases + +- **Automated Cross-Browser Testing Workflow**: Trigger an automated testing workflow whenever code is pushed to your repository. Use the BrowserStack API to run your suite of tests across various browsers and devices, then receive the test results in Slack or via email. This saves time and ensures your application performs well across all user platforms before deployment. + +- **Scheduled Regression Testing**: Set a Pipedream cron job to periodically initiate regression tests on BrowserStack. After the tests are complete, the results can be logged into a Google Sheet for tracking and trend analysis. This regular health check can catch issues early and maintain the integrity of your application over time. + +- **Dynamic Resource Allocation for Testing**: Use the BrowserStack API in a Pipedream workflow to dynamically allocate testing resources based on the development cycle. For instance, ramp up testing environments before a major release and scale down during off-peak periods. Integrate with Jira to create or update issues based on test outcomes, optimizing your resource usage and bug tracking. diff --git a/components/browserstack/package.json b/components/browserstack/package.json new file mode 100644 index 0000000000000..f32e70d901ac1 --- /dev/null +++ b/components/browserstack/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/browserstack", + "version": "0.6.0", + "description": "Pipedream browserstack Components", + "main": "browserstack.app.mjs", + "keywords": [ + "pipedream", + "browserstack" + ], + "homepage": "https://pipedream.com/apps/browserstack", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/bruzu/README.md b/components/bruzu/README.md index 07ae15563475b..9fc947d480ecc 100644 --- a/components/bruzu/README.md +++ b/components/bruzu/README.md @@ -1,6 +1,11 @@ # Overview -With the Bruzu API, you can build applications that allow users to find and -book appointments with local businesses. You can also use the API to search for -businesses and get detailed information about them, including ratings and -reviews. +The Bruzu API enables dynamic image generation, allowing for the creation of personalized images with text overlay, custom graphics, and various design elements. These capabilities can be harnessed in serverless workflows within Pipedream to automate marketing campaigns, personalize user engagement, and streamline content creation. By leveraging Pipedream's integration platform, Bruzu's API can be connected to a myriad of services to enrich CRM data, augment social media posts, or create on-the-fly images for e-commerce products. + +# Example Use Cases + +- **Dynamic Social Media Content Creation**: Automate the generation of personalized social media images for each new blog post or product release. When a new post is published in your CMS or a new product is added to your inventory, trigger a Pipedream workflow that uses Bruzu to create an image with the post title or product details, and then post it directly to platforms like Twitter or Instagram. + +- **Custom Email Campaign Graphics**: Enhance your email marketing by injecting custom images into each email. Use Bruzu within a Pipedream workflow to generate images that include recipient-specific information, such as names or account details from your CRM like Salesforce. These images can then be included in automated emails sent through email platforms like SendGrid, adding a personal touch to your outreach. + +- **Personalized E-commerce Experiences**: Create a more engaging shopping experience by providing customers with personalized product recommendations. Set up a Pipedream workflow that triggers when a user views a product, uses Bruzu to generate images that incorporate other recommended products based on user behavior, and displays these images on the product page or in a follow-up email to entice further engagement. diff --git a/components/bruzu/package.json b/components/bruzu/package.json new file mode 100644 index 0000000000000..4702562cefdaf --- /dev/null +++ b/components/bruzu/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/bruzu", + "version": "0.6.0", + "description": "Pipedream bruzu Components", + "main": "bruzu.app.mjs", + "keywords": [ + "pipedream", + "bruzu" + ], + "homepage": "https://pipedream.com/apps/bruzu", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/btcpay_server/README.md b/components/btcpay_server/README.md new file mode 100644 index 0000000000000..229b8fa5d661d --- /dev/null +++ b/components/btcpay_server/README.md @@ -0,0 +1,11 @@ +# Overview + +The BTCPay Server API provides a robust interface for automating payment processing, managing invoices, and overseeing stores within a BTCPay Server instance. Leveraging Pipedream, you can build powerful serverless workflows that react to events in BTCPay Server, such as new payments or invoice statuses, and integrate with countless other services through Pipedream's platform. + +# Example Use Cases + +- **Invoice Status Change Notifications**: When an invoice status changes in BTCPay Server, you can set up a workflow in Pipedream to send a notification to a Slack channel, inform your team, or update a record in a Google Sheet, keeping all stakeholders in the loop about payment statuses in real-time. + +- **Automate Refund Processing**: If a payment is made over the expected amount, create a Pipedream workflow that triggers a refund process. This could connect to BTCPay Server to calculate the excess and then issue a refund via the original payment method or notify an admin to handle the refund manually. + +- **Sync Payments to Accounting Software**: After receiving a payment, a Pipedream workflow can automatically log this transaction in your accounting software, such as QuickBooks or Xero. This automation ensures that your financial records are always up-to-date without manual data entry. diff --git a/components/bubble/README.md b/components/bubble/README.md index 5a0e35d2099a1..f9b55f255db68 100644 --- a/components/bubble/README.md +++ b/components/bubble/README.md @@ -1,15 +1,11 @@ # Overview -With Bubble, you can build all sorts of web applications without having to -write any code. - -Here are some examples of what you can build with Bubble: - -- A to-do list app -- A chat app -- A social networking app -- A task management app -- A customer relationship management (CRM) app -- A real estate listing app -- A food delivery app -- An e-commerce app +The Bubble API empowers you to automate and extend the functionality of your Bubble applications. Using Pipedream, engage your Bubble app with dynamic workflows that can handle tasks like syncing data between platforms, processing events in real-time, and orchestrating complex actions without writing extensive code. Leverage Pipedream’s capabilities to trigger on events, perform API operations, and connect with countless other services to enrich your Bubble app with minimal fuss. + +# Example Use Cases + +- **Data Synchronization between Bubble and Google Sheets**: Listen for new entries in your Bubble app's database and sync them to a Google Sheet in real time. This workflow can be used for sharing updates with a team that prefers working within spreadsheets or for archiving data externally. + +- **User Onboarding Sequence with SendGrid**: Kick off a user onboarding sequence via email using SendGrid whenever a new user signs up in your Bubble app. This can include a welcome email followed by a sequence of educational content or usage tips, helping to engage users right from the start. + +- **Support Ticket Escalation using Slack**: Monitor your Bubble app's support ticket system and post messages to a designated Slack channel when tickets meet certain criteria, such as high priority or staying unresolved for a specific time frame. This workflow ensures critical issues gain immediate attention. diff --git a/components/bubble/package.json b/components/bubble/package.json new file mode 100644 index 0000000000000..e82bd5f36a8be --- /dev/null +++ b/components/bubble/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/bubble", + "version": "0.6.0", + "description": "Pipedream bubble Components", + "main": "bubble.app.mjs", + "keywords": [ + "pipedream", + "bubble" + ], + "homepage": "https://pipedream.com/apps/bubble", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/budgets_ai/README.md b/components/budgets_ai/README.md new file mode 100644 index 0000000000000..5751b77ccf889 --- /dev/null +++ b/components/budgets_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +The Budgets.ai API offers robust tools for managing budgets, tracking expenses, and monitoring financial goals. By leveraging this API on Pipedream, you can automate your financial data processes, sync with other financial tools, and generate real-time analytics and notifications. Pipedream's serverless platform facilitates the integration of Budgets.ai with hundreds of other apps, enabling you to create custom, automated workflows that fit your financial management needs. + +# Example Use Cases + +- **Synchronize Financial Data**: Automate the import of transactions from your business bank account into Budgets.ai by connecting it with a banking app via Pipedream. Every time a new transaction is posted, it triggers a Pipedream workflow that adds the transaction to your Budgets.ai ledger. + +- **Budget Threshold Alerts**: Set up a workflow where Pipedream monitors your Budgets.ai categories. When spending approaches the budget limit, trigger an SMS or email alert via Twilio or SendGrid. Stay proactive with budget management without having to manually check figures. + +- **Expense Approval Automation**: Connect Budgets.ai to Slack using Pipedream. When an expense is submitted for approval in Budgets.ai, trigger a Slack message to the relevant manager for review. Once approved in Slack, the workflow updates the expense status in Budgets.ai, streamlining the approval process. diff --git a/components/bugbug/README.md b/components/bugbug/README.md new file mode 100644 index 0000000000000..266721baabf74 --- /dev/null +++ b/components/bugbug/README.md @@ -0,0 +1,11 @@ +# Overview + +The BugBug API lets you automate and integrate your browser testing workflows. With it, you can manage tests, run them programmatically, and receive test results. By connecting the BugBug API with Pipedream, you can craft serverless workflows that trigger on various events, enabling a seamless CI/CD integration, or alerting you when your automated tests detect issues. + +# Example Use Cases + +- **Automated Test Execution After Deployment:** Trigger a suite of browser tests on BugBug whenever a new version of your app is deployed. Connect this to your CI/CD pipeline using a platform like GitHub Actions, and post the test results back to your GitHub commit or PR for a streamlined review process. + +- **Scheduled Browser Test Runs:** Set up a Pipedream cron job to run your BugBug tests at regular intervals. This ensures your website remains in top shape by catching bugs early. If a test fails, automatically send alerts through Slack or email to notify your team instantly. + +- **Test Management Upon Issue Tracking:** Integrate BugBug with an issue tracking system like Jira. When a new issue is created that requires browser testing, trigger a specific test in BugBug and upon completion, update the issue with the test results, ensuring all relevant data is captured in your issue tracking system. diff --git a/components/bugherd/README.md b/components/bugherd/README.md index ad43f92ad7cb2..88a1ad61dc145 100644 --- a/components/bugherd/README.md +++ b/components/bugherd/README.md @@ -1,10 +1,11 @@ # Overview -With the BugHerd API, you can build a variety of applications and integrations -to help streamline your workflow. Here are some examples of what you can build: - -- A tool to automatically generate bug reports based on user feedback -- A plugin to integrate BugHerd with your existing bug tracking system -- A custom dashboard to track your team's progress on fixing bugs -- A script to automatically assign tasks to team members based on their skills - and availability +The BugHerd API provides a streamlined interface for managing and tracking bugs and issues directly within your web projects. By leveraging this API on Pipedream, you can automate the bug tracking process, sync data with project management tools, and create custom notifications based on event triggers. Automations can range from simple task creation to complex workflows involving multiple systems, cutting down the manual effort required to keep your development projects bug-free. + +# Example Use Cases + +- **Sync Bug Reports to Project Management Tools**: Automatically create tasks in project management apps like Trello, Asana, or JIRA whenever a new bug is reported in BugHerd. This keeps your project boards up-to-date with the latest issues needing attention. + +- **Notify Team Members in Chat Apps**: Send immediate notifications to Slack, Microsoft Teams, or Discord when bugs are reported or updated. This can include bug details and links to the issue, ensuring the right people are informed instantly for quicker resolution. + +- **Aggregate Bug Data for Reporting**: Collect and send bug reports from BugHerd to Google Sheets or Airtable. Use this data for generating weekly summaries, tracking bug resolution times, or maintaining a central bug knowledge base for the team. diff --git a/components/bugherd/package.json b/components/bugherd/package.json new file mode 100644 index 0000000000000..09473c9a7fad9 --- /dev/null +++ b/components/bugherd/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/bugherd", + "version": "0.6.0", + "description": "Pipedream bugherd Components", + "main": "bugherd.app.mjs", + "keywords": [ + "pipedream", + "bugherd" + ], + "homepage": "https://pipedream.com/apps/bugherd", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/bugsnag/README.md b/components/bugsnag/README.md new file mode 100644 index 0000000000000..27263a6ca7417 --- /dev/null +++ b/components/bugsnag/README.md @@ -0,0 +1,11 @@ +# Overview + +The Bugsnag API enables you to interact programmatically with Bugsnag data, letting you manage and access error reports, project settings, and release tracking for your applications. With Pipedream's integration, you can automate workflows that respond to new errors, aggregate error data for analysis, or synchronize error information with other tools in your development stack. + +# Example Use Cases + +- **Automated Error Notification to Slack**: Trigger a Pipedream workflow whenever a new error is reported to Bugsnag. The workflow can format the error data and send a notification to a Slack channel, ensuring your team is immediately aware of issues. + +- **Error Triage with GitHub Issues**: When Bugsnag captures a critical error, use Pipedream to create a GitHub issue for it. This workflow can include error details and link back to Bugsnag, helping developers start the debugging process right from their issue tracker. + +- **Daily Error Summary Email**: Set up a scheduled workflow on Pipedream that fetches the day's errors from Bugsnag, summarizes them, and then sends an email report. This daily digest can help your team stay informed about the app's health without having to check Bugsnag manually. diff --git a/components/buildchatbot/README.md b/components/buildchatbot/README.md new file mode 100644 index 0000000000000..4a34c9540d440 --- /dev/null +++ b/components/buildchatbot/README.md @@ -0,0 +1,11 @@ +# Overview + +The BuildChatbot API allows you to create and manage chatbots with ease. This API provides a suite of tools to build conversational AI that can engage with users across various platforms. On Pipedream, you can leverage these capabilities to automate interactions, analyze sentiments, and connect with other services. With Pipedream's serverless platform, you can trigger workflows based on chatbot events, send data to and from your chatbot, and integrate with countless apps to extend functionality. + +# Example Use Cases + +- **Customer Support Automation**: Automate your customer support by integrating the BuildChatbot API with a CRM platform on Pipedream. When a new support request comes in, trigger a workflow that creates a ticket in your CRM and simultaneously deploys your chatbot to interact with the customer, providing instant responses and collecting information until a human agent takes over. + +- **Feedback Collection and Analysis**: Use BuildChatbot to gather feedback from users after interactions with your service. Then, with Pipedream, process that feedback, run sentiment analysis, and push the results into a data visualization tool like Google Sheets or a dashboard app for real-time insights. + +- **Multi-Platform Engagement**: Create a workflow on Pipedream that triggers your BuildChatbot API to send tailored messages or promotions across different platforms, such as SMS, Slack, or email. Connect this to your marketing platform to personalize the content based on user behavior or preferences, ensuring engagement is relevant and effective. diff --git a/components/buildchatbot/package.json b/components/buildchatbot/package.json index c52a558354d3d..9e6b51e4e95da 100644 --- a/components/buildchatbot/package.json +++ b/components/buildchatbot/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/builder_io/README.md b/components/builder_io/README.md new file mode 100644 index 0000000000000..9f766027f3202 --- /dev/null +++ b/components/builder_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The Builder.io API offers a powerful way to manage and deliver content across any platform with structured data. Using Pipedream, you can automate content creation, update workflows, and sync content across various applications. Pipedream's serverless platform allows you to connect Builder.io to hundreds of other services without writing complex code, triggering actions on events or on a schedule. + +# Example Use Cases + +- **Automate Content Publishing**: When a piece of content reaches a 'ready' state in Builder.io, a Pipedream workflow can automatically publish it to your website. This ensures that content goes live as soon as it's approved without manual intervention. + +- **Sync Builder.io and CMS**: Integrate Builder.io with a content management system (CMS) like WordPress. When a new entry is added in Builder.io, the workflow can create a corresponding post in WordPress, keeping both platforms updated seamlessly. + +- **Cross-Platform Content Distribution**: Use a Pipedream workflow to distribute content from Builder.io to multiple platforms such as social media (Twitter, LinkedIn) or email marketing services (Mailchimp, SendGrid) whenever new content is published, thus expanding the reach of your content without extra manual work. diff --git a/components/builderall_mailingboss/README.md b/components/builderall_mailingboss/README.md new file mode 100644 index 0000000000000..4a3d4ed93645c --- /dev/null +++ b/components/builderall_mailingboss/README.md @@ -0,0 +1,11 @@ +# Overview + +The Builderall Mailingboss API empowers you to automate your email marketing operations via Pipedream. You can create, update, and manage contacts and email lists, trigger email campaigns, and analyze campaign performance directly through API calls. Integrating these capabilities within Pipedream’s serverless platform enables you to connect Mailingboss with hundreds of other apps, streamlining your marketing workflows and data management. + +# Example Use Cases + +- **Sync New Subscribers From Web Forms to Mailingboss**: Capture new leads from a web form (like Typeform), then use Pipedream to automatically add those contacts to a specified email list in Mailingboss. This keeps your email list current without manual input. + +- **Trigger Email Campaigns Based on User Activity**: Set up a workflow where user actions on your website or app (monitored via Segment) trigger targeted email campaigns. For instance, if a user abandons their shopping cart, Pipedream can trigger a Mailingboss campaign to re-engage them. + +- **Analyze Campaign Performance in Google Sheets**: After sending a campaign, use Pipedream to pull campaign performance data from Mailingboss and append it to a Google Sheet. This allows for easy tracking and analysis of metrics like open rates and click-through rates over time. diff --git a/components/builderall_mailingboss/package.json b/components/builderall_mailingboss/package.json index d61567396c335..85c23695ec4f3 100644 --- a/components/builderall_mailingboss/package.json +++ b/components/builderall_mailingboss/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/buildkite/README.md b/components/buildkite/README.md index 9f5d9d049611a..09112024493ef 100644 --- a/components/buildkite/README.md +++ b/components/buildkite/README.md @@ -1,14 +1,11 @@ # Overview -With the Buildkite API, you can: - -- Trigger new builds -- Get the status of builds -- Cancel builds -- Rerun builds -- Delete builds -- List agents -- Get agent details -- Delete agents -- Disable agents -- Enable agents +The BuildKite API is a powerhouse for automating your continuous integration and delivery (CI/CD) pipelines. With it, you can trigger builds, get information on agents and artifacts, and manage your organization's setup programmatically. Pipedream leverages this API to connect BuildKite with a myriad of other services, allowing for customized workflows that go beyond the standard CI/CD process. You can automate notifications, synchronize with project management tools, or even gather analytics to optimize your build processes. + +# Example Use Cases + +- **Automated Deployment Notifications**: Create a workflow where Pipedream listens for BuildKite build completion events. Once a build succeeds, use the Slack app to send a notification to your team's channel with the build details and deployment status. + +- **Issue Tracking Integration**: Automate the process of updating issues or tickets in tools like Jira whenever a build fails. Set up a Pipedream workflow that watches for failed build events, extracts the relevant error information, and uses the Jira API to create or update an issue, linking back to the failed build for quick access. + +- **Performance Metrics Aggregation**: Gather build performance metrics over time by setting up a Pipedream workflow that triggers on every build completion. Use the workflow to extract timing and success rate data, feeding it into a Google Sheets document or a database connected via Pipedream, to track and analyze your CI/CD process efficiency. diff --git a/components/buildkite/package.json b/components/buildkite/package.json new file mode 100644 index 0000000000000..a64a27df196d5 --- /dev/null +++ b/components/buildkite/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/buildkite", + "version": "0.6.0", + "description": "Pipedream buildkite Components", + "main": "buildkite.app.mjs", + "keywords": [ + "pipedream", + "buildkite" + ], + "homepage": "https://pipedream.com/apps/buildkite", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/builtwith/README.md b/components/builtwith/README.md new file mode 100644 index 0000000000000..781c51b59625d --- /dev/null +++ b/components/builtwith/README.md @@ -0,0 +1,11 @@ +# Overview + +The BuiltWith API on Pipedream lets you tap into the details of the technologies used by websites across the internet. With this API, you can find out what tools and frameworks are powering different sites, uncover technology trends, or enrich your CRM with tech stack data. When you combine this with Pipedream's ability to connect to hundreds of other apps, you can automate workflows that react to this technology intelligence in real-time. + +# Example Use Cases + +- **Lead Scoring based on Tech Stack**: Enrich leads in a CRM like HubSpot with the technology data from BuiltWith. Score these leads based on the tech they use, which could indicate how qualified they are or how ready they might be for your product. + +- **Competitive Analysis Alerts**: Set up a workflow that checks the tech stack of your competitors' websites regularly. When there's a change, like the addition of a new marketing tool or e-commerce platform, send alerts via Slack or email to keep your team informed. + +- **Market Research and Reporting**: Use BuiltWith to conduct market research on technology adoption rates. Pull data on the usage of specific technologies and send this data to Google Sheets or Data Studio for reporting and visualization. diff --git a/components/builtwith/actions/get-domain-relationships/get-domain-relationships.mjs b/components/builtwith/actions/get-domain-relationships/get-domain-relationships.mjs new file mode 100644 index 0000000000000..dabd7492ed345 --- /dev/null +++ b/components/builtwith/actions/get-domain-relationships/get-domain-relationships.mjs @@ -0,0 +1,36 @@ +import app from "../../builtwith.app.mjs"; + +export default { + key: "builtwith-get-domain-relationships", + name: "Get Domain Relationships", + description: "Get the relationships of a domain with other websites. [See the documentation](https://api.builtwith.com/relationships-api)", + version: "0.0.1", + type: "action", + props: { + app, + domain: { + propDefinition: [ + app, + "domain", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getWebsiteRelationships({ + $, + params: { + LOOKUP: this.domain, + }, + }); + + if (response.Errors.length) { + throw new Error(response.Errors[0].Message); + } + + if (response.Relationships.length) { + $.export("$summary", `Retrieved relationships for domain ${this.domain}`); + } + + return response; + }, +}; diff --git a/components/builtwith/actions/get-domain-technologies/get-domain-technologies.mjs b/components/builtwith/actions/get-domain-technologies/get-domain-technologies.mjs new file mode 100644 index 0000000000000..c42643d7b6317 --- /dev/null +++ b/components/builtwith/actions/get-domain-technologies/get-domain-technologies.mjs @@ -0,0 +1,34 @@ +import app from "../../builtwith.app.mjs"; + +export default { + key: "builtwith-get-domain-technologies", + name: "Get Domain Technologies", + description: "Retrieve the technology information of a website. [See the documentation](https://api.builtwith.com/domain-api)", + version: "0.0.1", + type: "action", + props: { + app, + domain: { + propDefinition: [ + app, + "domain", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getWebsiteTechnologies({ + $, + params: { + LOOKUP: this.domain, + }, + }); + + if (response.Errors.length) { + throw new Error(response.Errors[0].Message); + } + + $.export("$summary", `Retrieved technology information for domain ${this.domain}`); + + return response; + }, +}; diff --git a/components/builtwith/actions/get-profile-websites/get-profile-websites.mjs b/components/builtwith/actions/get-profile-websites/get-profile-websites.mjs new file mode 100644 index 0000000000000..17abddfd8e615 --- /dev/null +++ b/components/builtwith/actions/get-profile-websites/get-profile-websites.mjs @@ -0,0 +1,34 @@ +import app from "../../builtwith.app.mjs"; + +export default { + key: "builtwith-get-profile-websites", + name: "Get Profile Websites", + description: "Get websites associated with a social media URL. [See the documentation](https://api.builtwith.com/social-api)", + version: "0.0.1", + type: "action", + props: { + app, + domain: { + propDefinition: [ + app, + "domain", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getSocialMediaWebsites({ + $, + params: { + LOOKUP: this.domain, + }, + }); + + if (response.Errors.length) { + throw new Error(response.Errors[0].Message); + } + + $.export("$summary", `Successfully retrieved websites associated with the social media URL: ${this.domain}`); + + return response; + }, +}; diff --git a/components/builtwith/builtwith.app.mjs b/components/builtwith/builtwith.app.mjs index 25ae0ea761211..7e66beb552166 100644 --- a/components/builtwith/builtwith.app.mjs +++ b/components/builtwith/builtwith.app.mjs @@ -1,11 +1,52 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "builtwith", - propDefinitions: {}, + propDefinitions: { + domain: { + type: "string", + label: "Domain", + description: "The domain of the website to look up", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.builtwith.com"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + params: { + ...params, + KEY: `${this.$auth.api_key}`, + }, + }); + }, + async getWebsiteTechnologies(args = {}) { + return this._makeRequest({ + path: "/v21/api.json", + ...args, + }); + }, + async getWebsiteRelationships(args = {}) { + return this._makeRequest({ + path: "/rv2/api.json", + ...args, + }); + }, + async getSocialMediaWebsites(args = {}) { + return this._makeRequest({ + path: "/social1/api.json", + ...args, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/builtwith/package.json b/components/builtwith/package.json index ac9aeb80a3b28..5a204521ba30a 100644 --- a/components/builtwith/package.json +++ b/components/builtwith/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/builtwith", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream BuiltWith Components", "main": "builtwith.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.1" } -} \ No newline at end of file +} diff --git a/components/bulkgate/README.md b/components/bulkgate/README.md new file mode 100644 index 0000000000000..da67ee30dcd18 --- /dev/null +++ b/components/bulkgate/README.md @@ -0,0 +1,11 @@ +# Overview + +The BulkGate API enables automated sending of SMS messages, offering a powerful way to communicate with customers or users directly through their mobile devices. With this API on Pipedream, you can craft workflows that trigger SMS notifications based on specific events, such as new sign-ups, transaction alerts, or appointment reminders. It's a tool for enhancing customer engagement, support, and conversion through timely and personalized messaging. + +# Example Use Cases + +- **Customer Order Alerts**: Automate SMS notifications to customers when their order status changes. Combine BulkGate with an e-commerce app like Shopify on Pipedream, where an order status update in Shopify triggers an SMS alert via BulkGate, keeping the customer informed every step of the way. + +- **Appointment Reminders**: Set up a workflow that sends SMS reminders for upcoming appointments. This can be tied to a calendar app such as Google Calendar. When an appointment is nearing, Pipedream triggers BulkGate to send a reminder, reducing the chance of no-shows and enhancing customer relations. + +- **Real-time Event Notifications**: Implement real-time alerts for critical system events. Connect BulkGate with a monitoring tool like Datadog on Pipedream. When Datadog detects an issue, an SMS alert is immediately sent through BulkGate, allowing for swift action and management of the situation. diff --git a/components/bunnycdn/README.md b/components/bunnycdn/README.md index 88f7bf7cd5fc6..aa50db414b80e 100644 --- a/components/bunnycdn/README.md +++ b/components/bunnycdn/README.md @@ -1,9 +1,11 @@ # Overview -BunnyCDN provides a powerful API that you can use to create CDN integrations -for your website or application. With the BunnyCDN API, you can: +BunnyCDN is a powerful content delivery network (CDN) offering an API to manage and deliver content globally with ease. Through its API, you can programmatically purge cache, manage storage zones, and get detailed statistics about your content distribution. The BunnyCDN API facilitates building workflows on Pipedream that automate content delivery processes, leverage CDN data for analytics, and optimize digital asset management. -- Create and manage your BunnyCDN account -- Add and remove BunnyCDN zones -- Upload and manage your content on BunnyCDN -- Get detailed statistics about your content's performance on BunnyCDN +# Example Use Cases + +- **Automated Cache Purge on Content Update**: When a website hosted on a platform like WordPress updates content, trigger a workflow on Pipedream to automatically purge the BunnyCDN cache. This ensures visitors always see the most recent version of the site. + +- **Dynamic Content Distribution Management**: Create a workflow that monitors your server for new or updated files. When changes are detected, automatically upload these files to a BunnyCDN storage zone and update your website's links to the new CDN URLs, ensuring efficient content distribution. + +- **Analytics and Monitoring**: Set up a Pipedream workflow to regularly fetch CDN usage statistics from BunnyCDN. Integrate with tools like Google Sheets or Data Studio, allowing you to build live dashboards or send daily or weekly performance reports to stakeholders. diff --git a/components/bunnycdn/package.json b/components/bunnycdn/package.json new file mode 100644 index 0000000000000..19f1f7d124d73 --- /dev/null +++ b/components/bunnycdn/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/bunnycdn", + "version": "0.6.0", + "description": "Pipedream bunnycdn Components", + "main": "bunnycdn.app.mjs", + "keywords": [ + "pipedream", + "bunnycdn" + ], + "homepage": "https://pipedream.com/apps/bunnycdn", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/bunnydoc/actions/send-signature-request-from-template/send-signature-request-from-template.mjs b/components/bunnydoc/actions/send-signature-request-from-template/send-signature-request-from-template.mjs new file mode 100644 index 0000000000000..f01f42ce43023 --- /dev/null +++ b/components/bunnydoc/actions/send-signature-request-from-template/send-signature-request-from-template.mjs @@ -0,0 +1,63 @@ +import bunnydoc from "../../bunnydoc.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "bunnydoc-send-signature-request-from-template", + name: "Send Signature Request from Template", + description: "Sends a signature request using a pre-designed bunnydoc template. [See the documentation](https://support.bunnydoc.com/doc/api/#create-signature-request)", + version: "0.0.1", + type: "action", + props: { + bunnydoc, + templateId: { + type: "string", + label: "Template ID", + description: "The ID of the bunnydoc template to be used for the signature request.", + }, + title: { + type: "string", + label: "Title", + description: "The title of the signature resquest.", + optional: true, + }, + emailMessage: { + type: "string", + label: "Email Message", + description: "The message of the signature resquest.", + optional: true, + }, + signingOrder: { + type: "boolean", + label: "Signing Order", + description: "Set the signing order.", + default: false, + }, + recipients: { + type: "string", + label: "Recipients", + description: "A stringified array of objects of recipients. E.g. [{\"role\" : \"role1\", \"name\" : \"Signer1\", \"email\" : \"signer1@example.com\", \"accessCode\" : \"\"}].", + }, + fields: { + type: "string", + label: "Fields", + description: "A stringified array of objects of fields. E.g. [{ \"apiLabel\": \"textFieldPatientHistory\", \"value\": \"My test value\", \"readOnly\" : 1 }]", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.bunnydoc.createSignatureRequestFromTemplate({ + $, + data: { + templateId: this.templateId, + title: this.title, + emailMessage: this.emailMessage, + signingOrder: this.signingOrder, + recipients: parseObject(this.recipients), + fields: parseObject(this.fields), + }, + }); + + $.export("$summary", "Successfully sent signature request."); + return response; + }, +}; diff --git a/components/bunnydoc/bunnydoc.app.mjs b/components/bunnydoc/bunnydoc.app.mjs new file mode 100644 index 0000000000000..a0e50b1b10c6e --- /dev/null +++ b/components/bunnydoc/bunnydoc.app.mjs @@ -0,0 +1,45 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "bunnydoc", + methods: { + _baseUrl() { + return "https://api.bunnydoc.com/v1"; + }, + _headers() { + return { + "Authorization": `API-KEY ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path = "/", ...opts + }) { + return axios($, { + ...opts, + url: this._baseUrl() + path, + headers: this._headers(), + }); + }, + createSignatureRequestFromTemplate(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/createSignatureRequestFromTemplate", + ...opts, + }); + }, + createHook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/subscribeWebhook", + ...opts, + }); + }, + deleteHook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/unsubscribeWebhook/${webhookId}`, + }); + }, + }, +}; diff --git a/components/bunnydoc/common/utils.mjs b/components/bunnydoc/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/bunnydoc/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/bunnydoc/package.json b/components/bunnydoc/package.json new file mode 100644 index 0000000000000..151a7e127e130 --- /dev/null +++ b/components/bunnydoc/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/bunnydoc", + "version": "0.1.0", + "description": "Pipedream BunnyDoc Components", + "main": "bunnydoc.app.mjs", + "keywords": [ + "pipedream", + "bunnydoc" + ], + "homepage": "https://pipedream.com/apps/bunnydoc", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} diff --git a/components/bunnydoc/sources/common/base.mjs b/components/bunnydoc/sources/common/base.mjs new file mode 100644 index 0000000000000..a8adea0e1fa1c --- /dev/null +++ b/components/bunnydoc/sources/common/base.mjs @@ -0,0 +1,41 @@ +import bunnydoc from "../../bunnydoc.app.mjs"; + +export default { + props: { + bunnydoc, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + }, + hooks: { + async activate() { + const response = await this.bunnydoc.createHook({ + data: { + hookUrl: this.http.endpoint, + webhookEvents: this.getEvents(), + }, + }); + this.db.set("webhookId", response.id); + }, + async deactivate() { + const webhookId = this.db.get("webhookId"); + await this.bunnydoc.deleteHook(webhookId); + }, + }, + async run({ body }) { + if (body.event_type === "callback_test") { + return this.http.respond({ + status: 200, + body: "BUNNYDOC API EVENT RECEIVED", + }); + } + + this.$emit(body, { + id: body.event_metadata.envelope_id, + summary: body.event_metadata.event_message, + ts: Date.parse(body.event_time), + }); + }, +}; diff --git a/components/bunnydoc/sources/new-completed-request-instant/new-completed-request-instant.mjs b/components/bunnydoc/sources/new-completed-request-instant/new-completed-request-instant.mjs new file mode 100644 index 0000000000000..0fd2db268334c --- /dev/null +++ b/components/bunnydoc/sources/new-completed-request-instant/new-completed-request-instant.mjs @@ -0,0 +1,21 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "bunnydoc-new-completed-request-instant", + name: "New Completed Signature Request (Instant)", + description: "Emit new event each time a signature request is completed.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "signatureRequestCompleted", + ]; + }, + }, + sampleEmit, +}; diff --git a/components/bunnydoc/sources/new-completed-request-instant/test-event.mjs b/components/bunnydoc/sources/new-completed-request-instant/test-event.mjs new file mode 100644 index 0000000000000..28593676b275d --- /dev/null +++ b/components/bunnydoc/sources/new-completed-request-instant/test-event.mjs @@ -0,0 +1,11 @@ +export default { + "event_type": "signatureRequestViewed", + "event_time": 1716990853, + "event_metadata": { + "envelope_id": "12345678-1234-1234-1234-123456789012", + "envelope_title": "Invoice", + "reported_for_user_id": "1234567890abcdefghij", + "event_message": "Envelope was Completed." + }, + "recipients": [] +}; diff --git a/components/bunnydoc/sources/new-signed-request-instant/new-signed-request-instant.mjs b/components/bunnydoc/sources/new-signed-request-instant/new-signed-request-instant.mjs new file mode 100644 index 0000000000000..6225ec3268a82 --- /dev/null +++ b/components/bunnydoc/sources/new-signed-request-instant/new-signed-request-instant.mjs @@ -0,0 +1,21 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "bunnydoc-new-signed-request-instant", + name: "New Signed Request (Instant)", + description: "Emit new event each time a signature request is signed.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "signatureRequestSigned", + ]; + }, + }, + sampleEmit, +}; diff --git a/components/bunnydoc/sources/new-signed-request-instant/test-event.mjs b/components/bunnydoc/sources/new-signed-request-instant/test-event.mjs new file mode 100644 index 0000000000000..a4be69fd6639f --- /dev/null +++ b/components/bunnydoc/sources/new-signed-request-instant/test-event.mjs @@ -0,0 +1,16 @@ +export default { + "event_type": "signatureRequestViewed", + "event_time": 1716990853, + "event_metadata": { + "envelope_id": "12345678-1234-1234-1234-123456789012", + "envelope_title": "Invoice", + "reported_for_user_id": "1234567890abcdefghij", + "event_message": "Envelope was signed by Signer (signer@email.com)" + }, + "recipients": [ + { + "name": "Signer", + "email": "signer@email.com" + } + ] +}; diff --git a/components/bunnydoc/sources/new-viewed-request-instant/new-viewed-request-instant.mjs b/components/bunnydoc/sources/new-viewed-request-instant/new-viewed-request-instant.mjs new file mode 100644 index 0000000000000..2f8c02c0d510d --- /dev/null +++ b/components/bunnydoc/sources/new-viewed-request-instant/new-viewed-request-instant.mjs @@ -0,0 +1,21 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "bunnydoc-new-viewed-request-instant", + name: "New Viewed Signature Request (Instant)", + description: "Emit new event when a signature request is viewed.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "signatureRequestViewed", + ]; + }, + }, + sampleEmit, +}; diff --git a/components/bunnydoc/sources/new-viewed-request-instant/test-event.mjs b/components/bunnydoc/sources/new-viewed-request-instant/test-event.mjs new file mode 100644 index 0000000000000..ee5aab8a21246 --- /dev/null +++ b/components/bunnydoc/sources/new-viewed-request-instant/test-event.mjs @@ -0,0 +1,16 @@ +export default { + "event_type": "signatureRequestViewed", + "event_time": 1716990853, + "event_metadata": { + "envelope_id": "12345678-1234-1234-1234-123456789012", + "envelope_title": "Invoice", + "reported_for_user_id": "1234567890abcdefghij", + "event_message": "Envelope has been viewed by Signer (signer@email.com)" + }, + "recipients": [ + { + "name": "Signer", + "email": "signer@email.com" + } + ] +}; diff --git a/components/burst_sms/README.md b/components/burst_sms/README.md new file mode 100644 index 0000000000000..9825ff46d3f74 --- /dev/null +++ b/components/burst_sms/README.md @@ -0,0 +1,11 @@ +# Overview + +The Burst SMS API on Pipedream allows you to send SMS messages, manage contacts, and automate responses based on incoming messages. By leveraging Pipedream's serverless platform, you can craft workflows that integrate Burst SMS with other apps to create powerful communication automations. From marketing campaigns to alert systems, the API's capabilities are a gateway to enhanced engagement through text messaging. + +# Example Use Cases + +- **Automated Customer Support**: Trigger a workflow on Pipedream with an incoming SMS to Burst SMS. Use this to create a ticket in a customer support platform like Zendesk, and send an automated response back to the customer acknowledging receipt. + +- **SMS Marketing Campaigns**: Schedule and send SMS messages to a list of subscribers whenever a new product is added to your e-commerce platform like Shopify. Track responses and engagement, and update your CRM tool with the campaign's results. + +- **Event Reminders and RSVPs**: Connect Burst SMS with Google Calendar to send reminders for upcoming events. When recipients respond with an RSVP, update the event details on the calendar and send a confirmation SMS back. diff --git a/components/burstyai/actions/run-workflow/run-workflow.mjs b/components/burstyai/actions/run-workflow/run-workflow.mjs new file mode 100644 index 0000000000000..1632db78c0b19 --- /dev/null +++ b/components/burstyai/actions/run-workflow/run-workflow.mjs @@ -0,0 +1,57 @@ +import burstyai from "../../burstyai.app.mjs"; + +export default { + key: "burstyai-run-workflow", + name: "Run Workflow", + description: "Triggers an AI workflow on BurstyAI. [See the documentation](https://burstyai.readme.io/reference)", + version: "0.0.1", + type: "action", + props: { + burstyai, + workflow: { + type: "string", + label: "Workflow ID", + description: "The ID of the specific workflow to run. When viewing the workflow in BurstyAI, the ID is the last part of the URL. Example: `65e1cd00141fdc000195cb88`", + }, + params: { + type: "object", + label: "Parameters", + description: "Optional parameters for the workflow", + optional: true, + }, + waitForCompletion: { + type: "boolean", + label: "Wait For Completion", + description: "Set to `true` to poll the API in 3-second intervals until the workflow is completed", + optional: true, + }, + }, + async run({ $ }) { + let response = await this.burstyai.triggerWorkflow({ + $, + workflow: this.workflow, + data: { + params: this.params || {}, + }, + }); + + const jobId = response; + + if (this.waitForCompletion) { + const timer = (ms) => new Promise((res) => setTimeout(res, ms)); + while (response?.jobStatus !== "END" && response?.jobStatus !== "ERROR") { + response = await this.burstyai.getWorkflowExecutionResult({ + $, + jobId, + }); + await timer(3000); + } + } + + if (response?.status === "END") { + $.export("$summary", `Successfully triggered workflow ${this.workflow}`); + } + + return response; + }, +}; diff --git a/components/burstyai/burstyai.app.mjs b/components/burstyai/burstyai.app.mjs new file mode 100644 index 0000000000000..41870776056b3 --- /dev/null +++ b/components/burstyai/burstyai.app.mjs @@ -0,0 +1,42 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "burstyai", + methods: { + _baseUrl() { + return "https://app.burstyai.com/burstyai"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + triggerWorkflow({ + workflow, ...args + }) { + return this._makeRequest({ + method: "POST", + path: `/aiflows/${workflow}/async_run`, + ...args, + }); + }, + getWorkflowExecutionResult({ + jobId, ...args + }) { + return this._makeRequest({ + path: `/aiflowjobs/${jobId}/result`, + ...args, + }); + }, + }, +}; diff --git a/components/burstyai/package.json b/components/burstyai/package.json new file mode 100644 index 0000000000000..3772fac44d492 --- /dev/null +++ b/components/burstyai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/burstyai", + "version": "0.1.0", + "description": "Pipedream BurstyAI Components", + "main": "burstyai.app.mjs", + "keywords": [ + "pipedream", + "burstyai" + ], + "homepage": "https://pipedream.com/apps/burstyai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/businesslogic/README.md b/components/businesslogic/README.md new file mode 100644 index 0000000000000..9af0d7cc18f7d --- /dev/null +++ b/components/businesslogic/README.md @@ -0,0 +1,11 @@ +# Overview + +The BusinessLogic API offers a platform for automating complex business processes and logic. It provides endpoints to create, retrieve, update, and manage business rules and components programmatically. With Pipedream, you can integrate this API into workflows to trigger actions based on business events, manage data across multiple systems, and streamline decision-making processes. By leveraging Pipedream's serverless execution model, you can set up event-driven pipelines that react in real-time to changes in BusinessLogic without managing infrastructure. + +# Example Use Cases + +- **Automated Order Processing**: Trigger a Pipedream workflow when a new order is placed in your e-commerce system. Use the BusinessLogic API to check the order against your business rules, calculate discounts, taxes, and shipping costs, then update the order details accordingly. Finally, notify your fulfillment team via email or messaging app like Slack. + +- **Dynamic Pricing Adjustments**: Create a workflow that periodically polls your inventory database. When stock levels for products fall below a certain threshold, use the BusinessLogic API to adjust pricing based on predefined rules to optimize sales and revenue. This updated pricing can then be pushed to your online store platform automatically. + +- **Customer Segmentation and Targeting**: When a new user signs up on your platform, trigger a Pipedream workflow that employs the BusinessLogic API to segment the user based on the data provided. Assign them to marketing campaigns or personalize their experience according to the determined segment. Integrate with a CRM like HubSpot to track and manage these user segments effectively. diff --git a/components/buy_me_a_coffee/README.md b/components/buy_me_a_coffee/README.md new file mode 100644 index 0000000000000..75b96d96e5ded --- /dev/null +++ b/components/buy_me_a_coffee/README.md @@ -0,0 +1,11 @@ +# Overview + +The Buy Me a Coffee API allows creators to interact programmatically with their Buy Me a Coffee account. Through Pipedream, you can automate actions such as thanking supporters, tracking donations, and posting updates. By integrating with other apps, you can extend functionalities, streamline your supporter engagement, and analyze your growth. + +# Example Use Cases + +- **Automated Thank-You Messages**: Trigger a workflow on Pipedream whenever you receive a new support on Buy Me a Coffee. Use this trigger to send personalized thank-you emails or direct messages to your supporters via platforms like Gmail or Slack. + +- **Supporter Engagement Analytics**: Connect Buy Me a Coffee to Google Sheets on Pipedream. Whenever a new coffee is bought, log the details in a sheet to analyze trends, track supporter engagement, and manage financial records more efficiently. + +- **Social Media Shout-Outs**: Integrate Buy Me a Coffee with Twitter using Pipedream. Set up a workflow to post a public thank-you tweet tagging the supporter (with their consent) each time a new coffee is purchased, amplifying your gratitude and engagement. diff --git a/components/buysellads/buysellads.app.mjs b/components/buysellads/buysellads.app.mjs new file mode 100644 index 0000000000000..c55456bf7ad31 --- /dev/null +++ b/components/buysellads/buysellads.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "buysellads", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/buysellads/package.json b/components/buysellads/package.json new file mode 100644 index 0000000000000..f27c255bd9a8e --- /dev/null +++ b/components/buysellads/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/buysellads", + "version": "0.0.1", + "description": "Pipedream BuySellAds Components", + "main": "buysellads.app.mjs", + "keywords": [ + "pipedream", + "buysellads" + ], + "homepage": "https://pipedream.com/apps/buysellads", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/bybit/README.md b/components/bybit/README.md index dc5e76e3085e2..0f91b6f56705c 100644 --- a/components/bybit/README.md +++ b/components/bybit/README.md @@ -1,13 +1,11 @@ # Overview -With ByBit's cryptocurrency trading API, you can: - -- Create a new ByBit account -- Retrieve your ByBit account details -- Get a list of all available currency pairs -- Get real-time market data for a currency pair -- Place a new buy or sell order -- Cancel an existing order -- Get a list of your open orders -- Get a list of your recent trades -- Withdraw funds from your ByBit account +The ByBit API offers programmatic access to ByBit's crypto trading platform, enabling you to automate trades, manage accounts, and retrieve market data. With Pipedream, you can harness this API to create custom workflows that react to market changes, automate trading strategies, or synchronize your ByBit data with other services for analysis or record-keeping. + +# Example Use Cases + +- **Automated Trading Bot**: Create a workflow that listens for specific market signals—like a rapid increase in the price of Bitcoin—and automatically places a trade on ByBit. This could be paired with a technical analysis service to trigger trades based on complex indicators. + +- **Portfolio Synchronization**: Set up a workflow that periodically fetches your current holdings from ByBit and updates a Google Sheet or a database, keeping a real-time record of your cryptocurrency portfolio across multiple platforms. + +- **Price Alert System**: Develop a system that sends you real-time alerts—via SMS or email—when a cryptocurrency hits a target price or experiences high volatility. You could integrate Twilio for SMS or SendGrid for email notifications to keep you informed on the go. diff --git a/components/byteforms/byteforms.app.mjs b/components/byteforms/byteforms.app.mjs new file mode 100644 index 0000000000000..481d665831288 --- /dev/null +++ b/components/byteforms/byteforms.app.mjs @@ -0,0 +1,55 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "byteforms", + propDefinitions: { + formId: { + type: "string", + label: "Form ID", + description: "The ID of the form to monitor for new submissions", + async options() { + const { data } = await this.listForms(); + return data?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.forms.bytesuite.io/api"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: this.$auth.api_key, + }, + }); + }, + listForms(opts = {}) { + return this._makeRequest({ + path: "/form", + ...opts, + }); + }, + listFormResponses({ + formId, ...opts + }) { + return this._makeRequest({ + path: `/form/responses/${formId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/byteforms/package.json b/components/byteforms/package.json new file mode 100644 index 0000000000000..2b25c8620f940 --- /dev/null +++ b/components/byteforms/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/byteforms", + "version": "0.1.0", + "description": "Pipedream ByteForms Components", + "main": "byteforms.app.mjs", + "keywords": [ + "pipedream", + "byteforms" + ], + "homepage": "https://pipedream.com/apps/byteforms", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/byteforms/sources/new-submission/new-submission.mjs b/components/byteforms/sources/new-submission/new-submission.mjs new file mode 100644 index 0000000000000..dedfadb5d8992 --- /dev/null +++ b/components/byteforms/sources/new-submission/new-submission.mjs @@ -0,0 +1,94 @@ +import byteforms from "../../byteforms.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "byteforms-new-submission", + name: "New Submission", + description: "Emit new event when a user submission to a form occurs. [See the documentation](https://forms.bytesuite.io/docs/api#endpoints)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + byteforms, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + formId: { + propDefinition: [ + byteforms, + "formId", + ], + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastCreated() { + return this.db.get("lastCreated") || 0; + }, + _setLastCreated(lastCreated) { + this.db.set("lastCreated", lastCreated); + }, + generateMeta(submission) { + return { + id: submission.id, + summary: `New Form Submission: ${submission.id}`, + ts: Date.parse(submission.created_at), + }; + }, + async getSubmissions(max, lastCreated, params) { + const submissions = []; + do { + const { + cursor, data = [], + } = await this.byteforms.listFormResponses({ + formId: this.formId, + params, + }); + if (!data?.length) { + return submissions; + } + for (const item of data) { + const ts = Date.parse(item.created_at); + if (ts >= lastCreated) { + submissions.push(item); + if (max && submissions.length >= max) { + return submissions; + } + } else { + return submissions; + } + } + params.after = cursor?.after; + } while (params.after); + return submissions; + }, + async processEvent(max) { + const lastCreated = this._getLastCreated(); + const params = { + order: "desc", + }; + const submissions = await this.getSubmissions(max, lastCreated, params); + if (!submissions?.length) { + return; + } + this._setLastCreated(Date.parse(submissions[0].created_at)); + submissions.forEach((submission) => { + const meta = this.generateMeta(submission); + this.$emit(submission, meta); + }); + }, + }, + async run() { + await this.processEvent(); + }, + sampleEmit, +}; diff --git a/components/byteforms/sources/new-submission/test-event.mjs b/components/byteforms/sources/new-submission/test-event.mjs new file mode 100644 index 0000000000000..35aebe93d7326 --- /dev/null +++ b/components/byteforms/sources/new-submission/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "id": 100, + "form_id": 172, + "response": { + "text": "Test" + }, + "score": 0, + "total_score": 0, + "options": { + "ip": "47.225.32.185" + }, + "created_at": "2024-08-08T18:21:54.259224Z", + "updated_at": "2024-08-08T18:21:54.259224Z", + "deleted_at": null +} \ No newline at end of file diff --git a/components/bytenite/actions/create-job/create-job.mjs b/components/bytenite/actions/create-job/create-job.mjs new file mode 100644 index 0000000000000..10339aba24f47 --- /dev/null +++ b/components/bytenite/actions/create-job/create-job.mjs @@ -0,0 +1,60 @@ +import bytenite from "../../bytenite.app.mjs"; + +export default { + key: "bytenite-create-job", + name: "Create Video Encoding Task", + description: "Creates a new video encoding task with ByteNite. [See the documentation](https://docs.bytenite.com/reference/customer_createjob)", + version: "0.0.1", + type: "action", + props: { + bytenite, + name: { + type: "string", + label: "Name", + description: "Mnemonic name for the job.", + }, + templateId: { + propDefinition: [ + bytenite, + "templateId", + ], + }, + url: { + type: "string", + label: "URL", + description: "URL of the video to encode", + }, + jobId: { + type: "string", + label: "Job ID", + description: "Unique identifier for the job, automatically generated if left blank.", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "Textual description of the job.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.bytenite.createJob({ + $, + data: { + name: this.name, + templateId: this.templateId, + jobId: this.jobId, + description: this.description, + dataSource: { + dataSourceDescriptor: "url", + params: { + "@type": "type.googleapis.com/bytenite.data_source.HttpDataSource", + "url": this.url, + }, + }, + }, + }); + $.export("$summary", `Successfully created video encoding task with ID: ${response.job.id}`); + return response; + }, +}; diff --git a/components/bytenite/actions/get-job-results/get-job-results.mjs b/components/bytenite/actions/get-job-results/get-job-results.mjs new file mode 100644 index 0000000000000..01c84b985733f --- /dev/null +++ b/components/bytenite/actions/get-job-results/get-job-results.mjs @@ -0,0 +1,29 @@ +import bytenite from "../../bytenite.app.mjs"; + +export default { + key: "bytenite-get-job-results", + name: "Get Job Results", + description: "Secures the link of the output from a finished encoding job. [See the documentation](https://docs.bytenite.com/reference/customer_getjobresults)", + version: "0.0.1", + type: "action", + props: { + bytenite, + jobId: { + propDefinition: [ + bytenite, + "jobId", + () => ({ + completedOnly: true, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.bytenite.getResults({ + $, + jobId: this.jobId, + }); + $.export("$summary", `Successfully fetched results for job ${this.jobId}`); + return response; + }, +}; diff --git a/components/bytenite/actions/start-job/start-job.mjs b/components/bytenite/actions/start-job/start-job.mjs new file mode 100644 index 0000000000000..92843953a408c --- /dev/null +++ b/components/bytenite/actions/start-job/start-job.mjs @@ -0,0 +1,26 @@ +import bytenite from "../../bytenite.app.mjs"; + +export default { + key: "bytenite-start-job", + name: "Start Job", + description: "Initiates a previously created video encoding job. [See the documentation](https://docs.bytenite.com/reference/customer_runjob)", + version: "0.0.1", + type: "action", + props: { + bytenite, + jobId: { + propDefinition: [ + bytenite, + "jobId", + ], + }, + }, + async run({ $ }) { + const response = await this.bytenite.startJob({ + $, + jobId: this.jobId, + }); + $.export("$summary", `Successfully initiated job with ID: ${this.jobId}`); + return response; + }, +}; diff --git a/components/bytenite/bytenite.app.mjs b/components/bytenite/bytenite.app.mjs new file mode 100644 index 0000000000000..576cc56e92073 --- /dev/null +++ b/components/bytenite/bytenite.app.mjs @@ -0,0 +1,109 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "bytenite", + propDefinitions: { + templateId: { + type: "string", + label: "Template ID", + description: "The ID of the template to use", + async options({ page }) { + const limit = constants.DEFAULT_LIMIT; + const { data } = await this.listTemplates({ + params: { + limit, + offset: page * limit, + }, + }); + return data?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + jobId: { + type: "string", + label: "Job ID", + description: "The ID of the video encoding job", + async options({ + page, completedOnly = false, + }) { + const limit = constants.DEFAULT_LIMIT; + let { data } = await this.listJobs({ + params: { + limit, + offset: page * limit, + }, + }); + if (completedOnly) { + data = data.filter(({ state }) => state === "JOB_STATE_COMPLETE"); + } + return data?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.bytenite.com/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + listTemplates(opts = {}) { + return this._makeRequest({ + path: "/customer/jobs/templates", + ...opts, + }); + }, + listJobs(opts = {}) { + return this._makeRequest({ + path: "/customer/jobs", + ...opts, + }); + }, + getResults({ + jobId, ...opts + }) { + return this._makeRequest({ + path: `/customer/jobs/${jobId}/results`, + ...opts, + }); + }, + createJob(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/customer/jobs", + ...opts, + }); + }, + startJob({ + jobId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/customer/jobs/run/${jobId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/bytenite/common/constants.mjs b/components/bytenite/common/constants.mjs new file mode 100644 index 0000000000000..7f4081138acc1 --- /dev/null +++ b/components/bytenite/common/constants.mjs @@ -0,0 +1,5 @@ +const DEFAULT_LIMIT = 20; + +export default { + DEFAULT_LIMIT, +}; diff --git a/components/bytenite/package.json b/components/bytenite/package.json new file mode 100644 index 0000000000000..ff7156906bdc1 --- /dev/null +++ b/components/bytenite/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/bytenite", + "version": "0.1.0", + "description": "Pipedream ByteNite Components", + "main": "bytenite.app.mjs", + "keywords": [ + "pipedream", + "bytenite" + ], + "homepage": "https://pipedream.com/apps/bytenite", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.4" + } +} diff --git a/components/cabinpanda/README.md b/components/cabinpanda/README.md new file mode 100644 index 0000000000000..64d06dd597c58 --- /dev/null +++ b/components/cabinpanda/README.md @@ -0,0 +1,11 @@ +# Overview + +CabinPanda offers a suite of API tools to help you collect and send data from forms or surveys to other services. With it, you can streamline data collection processes, synchronize information across platforms, and create automated workflows to enhance data management. Pipedream's serverless platform can leverage CabinPanda's capabilities to craft custom workflows, such as handling form submissions, updating databases, or triggering events in other apps based on the data received. + +# Example Use Cases + +- **Form Submission to Email Alert**: When a new form submission is detected by CabinPanda, a Pipedream workflow can automatically send an email notification. This is useful for teams needing instant updates on survey or form entries. + +- **Database Update upon Form Entry**: Use CabinPanda to trigger a Pipedream workflow that updates a SQL or NoSQL database whenever a new form submission comes in. This keeps your records up-to-date without manual data entry. + +- **Slack Notification for New Responses**: Connect CabinPanda to Slack via Pipedream. Each time a form response is received, post a message to a designated Slack channel, keeping your team informed in real-time. diff --git a/components/cal_com/README.md b/components/cal_com/README.md index fd429011de74c..0267542f76e18 100644 --- a/components/cal_com/README.md +++ b/components/cal_com/README.md @@ -1,11 +1,11 @@ # Overview -Cal.com provides an API that allows developers to access and integration the -functionality of Cal.com with other applications. +The Cal.com API on Pipedream lets you seamlessly integrate your scheduling infrastructure with other services to automate appointment setting, calendar syncing, and notifications. Using Pipedream, you can craft workflows that trigger on new event bookings, cancellations, or reschedules, and connect these events to countless apps to streamline business processes, enhance customer engagement, and maintain organized schedules. -Some examples of what you can build using the Cal.com API include: +# Example Use Cases -- A web application that allows users to search for and view calendar events. -- A mobile app that allows users to add calendar events to their device - calendar. -- A tool that allows businesses to track and manage employee vacation days. +- **Automated Follow-up Emails**: Upon a new booking via Cal.com, trigger an email sequence in Mailchimp to provide the attendee with additional information, a thank you note, or a reminder, enhancing communication and engagement. + +- **Slack Notifications for New Bookings**: Set up a workflow that sends a message to a designated Slack channel when a new appointment is scheduled on Cal.com, keeping the team instantly informed about new meetings or client bookings. + +- **Google Sheets Logging**: Create a workflow that logs new and updated appointments from Cal.com into a Google Sheets spreadsheet. This can serve as a simple CRM or a way to track and analyze meeting patterns over time. diff --git a/components/calendarhero/README.md b/components/calendarhero/README.md index e2a6e138cba74..3f3304d0f4c6b 100644 --- a/components/calendarhero/README.md +++ b/components/calendarhero/README.md @@ -1,14 +1,11 @@ # Overview -You can use the CalendarHero API to build applications that manage events and -calendars. With CalendarHero, you can: - -- Create and manage events -- Search for events -- Get event details -- Edit events -- Delete events -- Manage calendar settings -- Add and remove event participants -- Send event invitations -- And more! +CalendarHero's API on Pipedream enables you to craft tailored scheduling experiences by automating meeting setups, reminders, and follow-ups. Imagine the convenience of automating meeting coordination among various stakeholders, synchronizing schedules across platforms, and triggering actions based on meeting events. With CalendarHero and Pipedream, you can create powerful workflows that respond dynamically to your calendar activities, ensuring you spend less time managing meetings and more time engaging in them. + +# Example Use Cases + +- **Automated Meeting Coordination with Slack**: When a new meeting is scheduled in CalendarHero, trigger a Pipedream workflow to post a message with meeting details to a designated Slack channel. This keeps your team informed and allows for quick follow-up actions or questions related to the meeting. + +- **Dynamic CRM Updates**: After a meeting's completion, use CalendarHero to notify a Pipedream workflow which then logs the meeting details to a CRM like Salesforce or HubSpot. This ensures your sales and customer success teams have the latest information for each contact or account. + +- **Email Follow-Up Automation**: Set up a workflow on Pipedream that listens for an event indicating a meeting has ended in CalendarHero. It then crafts and sends a customized follow-up email via SendGrid to all meeting participants, providing summaries, action items, or requests for feedback. diff --git a/components/calendly_v2/README.md b/components/calendly_v2/README.md index a1d81dc1fd1e8..5159cfb2dd069 100644 --- a/components/calendly_v2/README.md +++ b/components/calendly_v2/README.md @@ -1,17 +1,11 @@ # Overview -The Calendly (OAuth) API allows developers to integrate Calendly with their -applications, allowing their users to schedule appointments and events directly -from the app. With the API, developers can create, read, update, and delete -Calendly event types, invitees, and schedule events on behalf of their users. +The Calendly API lets you craft bespoke scheduling experiences within your apps or automate repetitive tasks involving your calendar. With this API, you can read event types, set up webhooks for new events, list invitees, or create and cancel invitation links programmatically. Pipedream's serverless platform takes the heavy lifting out of integrating Calendly with hundreds of other apps, enabling you to focus on creating workflows that save time, enhance productivity, and eliminate scheduling errors. -Example applications that could be built using the Calendly (OAuth) API -include: +# Example Use Cases -- A scheduling app that allows users to schedule appointments and events - directly from the app -- An online booking system that allows users to book appointments and events - directly from the app -- A calendar app that allows users to view and manage their Calendly events and - appointments -- A to-do list app that allows users to track and manage their Calendly events and appointments +- **Automated Follow-Up Emails**: When a meeting is scheduled on Calendly, trigger an email via SendGrid to send personalized follow-up details and preparation materials to the invitee. + +- **Zoom Meeting Creation**: Automatically create a Zoom meeting and send the invite link to participants as soon as they book a time slot on Calendly, ensuring seamless video conference scheduling. + +- **Slack Notifications for New Bookings**: For every new appointment scheduled, send a notification to a designated Slack channel to keep your team informed about upcoming meetings and availability changes. diff --git a/components/calendly_v2/actions/create-invitee-no-show/create-invitee-no-show.mjs b/components/calendly_v2/actions/create-invitee-no-show/create-invitee-no-show.mjs new file mode 100644 index 0000000000000..0283a307b3a6e --- /dev/null +++ b/components/calendly_v2/actions/create-invitee-no-show/create-invitee-no-show.mjs @@ -0,0 +1,67 @@ +import calendly from "../../calendly_v2.app.mjs"; +import { axios } from "@pipedream/platform"; + +export default { + key: "calendly_v2-create-invitee-no-show", + name: "Create Invitee No Show", + description: "Marks an Invitee as a No Show in Calendly. [See the documentation](https://calendly.stoplight.io/docs/api-docs/cebd8c3170790-create-invitee-no-show).", + version: "0.0.2", + type: "action", + props: { + calendly, + eventId: { + propDefinition: [ + calendly, + "eventId", + ], + }, + inviteeUri: { + type: "string", + label: "Invitee URI", + description: "The invitee to mark as a no show", + async options({ prevContext }) { + const params = prevContext.pageToken + ? { + page_token: prevContext.pageToken, + } + : {}; + const { + collection, pagination, + } = await this.calendly.listEventInvitees(params, this.eventId); + const options = collection?.map(({ + uri: value, name: label, + }) => ({ + value, + label, + })) || []; + return { + options, + context: { + pageToken: pagination?.next_page_token, + }, + }; + }, + }, + }, + methods: { + createInviteeNoShow(opts = {}, $) { + return axios( + $, + this.calendly._makeRequestOpts({ + method: "POST", + path: "/invitee_no_shows", + ...opts, + }), + ); + }, + }, + async run({ $ }) { + const response = await this.createInviteeNoShow({ + data: { + invitee: this.inviteeUri, + }, + }, $); + $.export("$summary", `Successfully marked invitee ${this.inviteeUri} as a no show`); + return response; + }, +}; diff --git a/components/calendly_v2/actions/create-scheduling-link/create-scheduling-link.mjs b/components/calendly_v2/actions/create-scheduling-link/create-scheduling-link.mjs index 80ecc685f56f5..0479916b9a10b 100644 --- a/components/calendly_v2/actions/create-scheduling-link/create-scheduling-link.mjs +++ b/components/calendly_v2/actions/create-scheduling-link/create-scheduling-link.mjs @@ -3,8 +3,8 @@ import calendly from "../../calendly_v2.app.mjs"; export default { key: "calendly_v2-create-scheduling-link", name: "Create a Scheduling Link", - description: "Creates a single-use scheduling link. [See the docs](https://calendly.stoplight.io/docs/api-docs/b3A6MzQyNTM0OQ-create-single-use-scheduling-link)", - version: "0.0.3", + description: "Creates a single-use scheduling link. [See the documentation](https://calendly.stoplight.io/docs/api-docs/b3A6MzQyNTM0OQ-create-single-use-scheduling-link)", + version: "0.0.4", type: "action", props: { calendly, diff --git a/components/calendly_v2/actions/get-event/get-event.mjs b/components/calendly_v2/actions/get-event/get-event.mjs index 3e4024ddbadfe..9947ee3c86bed 100644 --- a/components/calendly_v2/actions/get-event/get-event.mjs +++ b/components/calendly_v2/actions/get-event/get-event.mjs @@ -5,8 +5,8 @@ import calendly from "../../calendly_v2.app.mjs"; export default { key: "calendly_v2-get-event", name: "Get Event", - description: "Gets information about an Event associated with a URI. [See docs here](https://developer.calendly.com/api-docs/e2f95ebd44914-get-event).", - version: "0.1.3", + description: "Gets information about an Event associated with a URI. [See the documentation](https://developer.calendly.com/api-docs/e2f95ebd44914-get-event).", + version: "0.1.4", type: "action", props: { calendly, diff --git a/components/calendly_v2/actions/list-event-invitees/list-event-invitees.mjs b/components/calendly_v2/actions/list-event-invitees/list-event-invitees.mjs index 6a0a5ca0a3b78..43df12321d313 100644 --- a/components/calendly_v2/actions/list-event-invitees/list-event-invitees.mjs +++ b/components/calendly_v2/actions/list-event-invitees/list-event-invitees.mjs @@ -3,8 +3,8 @@ import calendly from "../../calendly_v2.app.mjs"; export default { key: "calendly_v2-list-event-invitees", name: "List Event Invitees", - description: "List invitees for an event. [See the docs](https://calendly.stoplight.io/docs/api-docs/b3A6NTkxNDEx-list-event-invitees)", - version: "0.0.3", + description: "List invitees for an event. [See the documentation](https://calendly.stoplight.io/docs/api-docs/b3A6NTkxNDEx-list-event-invitees)", + version: "0.0.4", type: "action", props: { calendly, diff --git a/components/calendly_v2/actions/list-events/list-events.mjs b/components/calendly_v2/actions/list-events/list-events.mjs index 9c576fd7964f9..264409eebadae 100644 --- a/components/calendly_v2/actions/list-events/list-events.mjs +++ b/components/calendly_v2/actions/list-events/list-events.mjs @@ -3,8 +3,8 @@ import calendly from "../../calendly_v2.app.mjs"; export default { key: "calendly_v2-list-events", name: "List Events", - description: "List events for an user. [See the docs](https://calendly.stoplight.io/docs/api-docs/b3A6NTkxNDEy-list-events)", - version: "0.0.3", + description: "List events for an user. [See the documentation](https://calendly.stoplight.io/docs/api-docs/b3A6NTkxNDEy-list-events)", + version: "0.0.4", type: "action", props: { calendly, @@ -22,7 +22,7 @@ export default { organization: c.organization, }), ], - description: "Returns events for a specified user, or leave blank for your own events", + description: "Returns events for a specified user", optional: true, }, inviteeEmail: { @@ -52,7 +52,9 @@ export default { }, }, async run({ $ }) { - const params = {}; + const params = { + organization: this.organization, + }; if (this.inviteeEmail) params.invitee_email = this.inviteeEmail; if (this.status) params.status = this.status; if (this.paginate) params.paginate = this.paginate; diff --git a/components/calendly_v2/actions/list-webhook-subscriptions/list-webhook-subscriptions.mjs b/components/calendly_v2/actions/list-webhook-subscriptions/list-webhook-subscriptions.mjs index 6bf20b63937d2..cedb18466768f 100644 --- a/components/calendly_v2/actions/list-webhook-subscriptions/list-webhook-subscriptions.mjs +++ b/components/calendly_v2/actions/list-webhook-subscriptions/list-webhook-subscriptions.mjs @@ -5,7 +5,7 @@ export default { key: "calendly_v2-list-webhook-subscriptions", name: "List Webhook Subscriptions", description: "Get a list of Webhook Subscriptions for an Organization or User with a UUID.", - version: "0.1.3", + version: "0.1.4", type: "action", props: { calendly_v2: { diff --git a/components/calendly_v2/calendly_v2.app.mjs b/components/calendly_v2/calendly_v2.app.mjs index 709324635fbae..3b365f1d64609 100644 --- a/components/calendly_v2/calendly_v2.app.mjs +++ b/components/calendly_v2/calendly_v2.app.mjs @@ -241,7 +241,9 @@ export default { async listEvents(params, uuid, $) { const user = uuid ? this._buildUserUri(uuid) - : await this.defaultUser($); + : !params?.organization + ? await this.defaultUser($) + : undefined; const opts = { path: "/scheduled_events", diff --git a/components/calendly_v2/package.json b/components/calendly_v2/package.json index fac1ddbda3186..0f1fff7b0563c 100644 --- a/components/calendly_v2/package.json +++ b/components/calendly_v2/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/calendly_v2", - "version": "1.1.4", + "version": "1.2.1", "description": "Pipedream Calendly V2 Components", "main": "calendly_v2.app.mjs", "keywords": [ @@ -13,7 +13,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.5.1", + "@pipedream/platform": "^3.0.3", "url": "^0.11.0" } } diff --git a/components/calendly_v2/sources/invitee-canceled/invitee-canceled.mjs b/components/calendly_v2/sources/invitee-canceled/invitee-canceled.mjs index 9c4742cd2ed83..f7b7c61fafc23 100644 --- a/components/calendly_v2/sources/invitee-canceled/invitee-canceled.mjs +++ b/components/calendly_v2/sources/invitee-canceled/invitee-canceled.mjs @@ -5,7 +5,7 @@ export default { key: "calendly_v2-invitee-canceled", name: "New Invitee Canceled", description: "Emit new event when an event is canceled.", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "unique", props: { diff --git a/components/calendly_v2/sources/invitee-created/invitee-created.mjs b/components/calendly_v2/sources/invitee-created/invitee-created.mjs index 9fab64a731e6e..7c8d21c06a360 100644 --- a/components/calendly_v2/sources/invitee-created/invitee-created.mjs +++ b/components/calendly_v2/sources/invitee-created/invitee-created.mjs @@ -5,7 +5,7 @@ export default { key: "calendly_v2-invitee-created", name: "New Invitee Created", description: "Emit new event when a new event is scheduled.", - version: "0.0.2", + version: "0.0.3", type: "source", dedupe: "unique", props: { diff --git a/components/calendly_v2/sources/new-event-scheduled/new-event-scheduled.mjs b/components/calendly_v2/sources/new-event-scheduled/new-event-scheduled.mjs index 41be042024c2f..f90312dae39d7 100644 --- a/components/calendly_v2/sources/new-event-scheduled/new-event-scheduled.mjs +++ b/components/calendly_v2/sources/new-event-scheduled/new-event-scheduled.mjs @@ -6,7 +6,7 @@ export default { key: "calendly_v2-new-event-scheduled", name: "New Event Scheduled", description: "Emit new event when a event is scheduled.", - version: "0.0.5", + version: "0.0.6", type: "source", dedupe: "unique", props: { diff --git a/components/calendly_v2/sources/routing-form-submission-created/routing-form-submission-created.mjs b/components/calendly_v2/sources/routing-form-submission-created/routing-form-submission-created.mjs index cfeaa6b2b3bea..59266cce30900 100644 --- a/components/calendly_v2/sources/routing-form-submission-created/routing-form-submission-created.mjs +++ b/components/calendly_v2/sources/routing-form-submission-created/routing-form-submission-created.mjs @@ -5,7 +5,7 @@ export default { key: "calendly_v2-routing-form-submission-created", name: "New Routing Form Submission Created", description: "Emit new event when a new routing form submission is created.", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "unique", methods: { diff --git a/components/call_fire/README.md b/components/call_fire/README.md new file mode 100644 index 0000000000000..8ba9ec4851adb --- /dev/null +++ b/components/call_fire/README.md @@ -0,0 +1,11 @@ +# Overview + +The CallFire API offers a suite of telephony functions that allows users to send voice messages, text messages, and make calls to lists of contacts for various purposes like marketing campaigns, alerts, reminders, and polls. When integrated with Pipedream, you can automate interactions with your CallFire account, programmatically managing calls, texts, contacts, and creating custom, serverless workflows that respond to events in real-time without the need to manage infrastructure. + +# Example Use Cases + +- **Automated Customer Outreach Workflow**: Create a Pipedream workflow that triggers based on customer activity in your CRM (like Salesforce). When a new lead is added, Pipedream can automate a CallFire SMS or voice message to the lead, introducing your services or providing contact information. + +- **Feedback Collection System**: Build a Pipedream workflow that sends out a CallFire voice or text survey after a customer interaction, such as a support call or product delivery. Collect responses and save them to a Google Sheet or database for easy analysis and follow-up. + +- **Real-Time Alerting Mechanism**: Configure a Pipedream workflow that monitors a specific condition, such as a server's health status using Datadog or a similar service. When an anomaly is detected, trigger a CallFire call to the IT support team to ensure immediate attention. diff --git a/components/callerapi/actions/get-phone-number-information/get-phone-number-information.mjs b/components/callerapi/actions/get-phone-number-information/get-phone-number-information.mjs new file mode 100644 index 0000000000000..d508482bc5d59 --- /dev/null +++ b/components/callerapi/actions/get-phone-number-information/get-phone-number-information.mjs @@ -0,0 +1,26 @@ +import callerapi from "../../callerapi.app.mjs"; + +export default { + key: "callerapi-get-phone-number-information", + name: "Get Phone Number Information", + description: "Retrieve detailed information about a specific phone number, including name, location, and carrier. [See the documentation](https://callerapi.com/documentation)", + version: "0.0.1", + type: "action", + props: { + callerapi, + phoneNumber: { + propDefinition: [ + callerapi, + "phoneNumber", + ], + }, + }, + async run({ $ }) { + const response = await this.callerapi.getPhoneInfo({ + $, + phoneNumber: this.phoneNumber, + }); + $.export("$summary", `Retrieved information for phone number ${this.phoneNumber}`); + return response; + }, +}; diff --git a/components/callerapi/actions/get-phone-number-picture/get-phone-number-picture.mjs b/components/callerapi/actions/get-phone-number-picture/get-phone-number-picture.mjs new file mode 100644 index 0000000000000..d46ec5b7c1d9d --- /dev/null +++ b/components/callerapi/actions/get-phone-number-picture/get-phone-number-picture.mjs @@ -0,0 +1,39 @@ +import { ConfigurationError } from "@pipedream/platform"; +import fs from "fs"; +import callerapi from "../../callerapi.app.mjs"; + +export default { + key: "callerapi-get-phone-number-picture", + name: "Get Phone Number Picture", + description: "Retrieve the profile picture associated with a phone number. [See the documentation](https://callerapi.com/documentation)", + version: "0.0.1", + type: "action", + props: { + callerapi, + phoneNumber: { + propDefinition: [ + callerapi, + "phoneNumber", + ], + description: "The phone number to retrieve the profile picture for, in E.164 format (e.g., +18006927753)", + }, + }, + async run({ $ }) { + try { + const response = await this.callerapi.getPhonePicture({ + $, + phoneNumber: this.phoneNumber, + }); + const fileName = `CallerAPI-Pictgure-${Date.parse(new Date())}.png`; + const buf = Buffer.from(response, "base64"); + fs.writeFileSync(`/tmp/${fileName}`, buf); + + $.export("$summary", `The profile picture for ${this.phoneNumber} has been successfully retrieved and saved to the /tmp directory.`); + return { + path: `/tmp/${fileName}`, + }; + } catch (e) { + throw new ConfigurationError(e?.response?.data || e); + } + }, +}; diff --git a/components/callerapi/callerapi.app.mjs b/components/callerapi/callerapi.app.mjs new file mode 100644 index 0000000000000..4820ef3912edc --- /dev/null +++ b/components/callerapi/callerapi.app.mjs @@ -0,0 +1,49 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "callerapi", + propDefinitions: { + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number to retrieve information for (E.164 format, e.g., +18006927753)", + }, + }, + methods: { + _baseUrl() { + return "https://callerapi.com/api/phone"; + }, + _headers() { + return { + "x-auth": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, method, path = "/", ...opts + }) { + return axios($, { + ...opts, + method, + url: this._baseUrl() + path, + headers: this._headers(), + }); + }, + getPhoneInfo({ + $, phoneNumber, + }) { + return this._makeRequest({ + $, + path: `/info/${phoneNumber}`, + }); + }, + getPhonePicture({ + $, phoneNumber, + }) { + return this._makeRequest({ + $, + path: `/pic/${phoneNumber}`, + }); + }, + }, +}; diff --git a/components/callerapi/package.json b/components/callerapi/package.json new file mode 100644 index 0000000000000..8b4dcc7f8f74c --- /dev/null +++ b/components/callerapi/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/callerapi", + "version": "0.1.0", + "description": "Pipedream CallerAPI Components", + "main": "callerapi.app.mjs", + "keywords": [ + "pipedream", + "callerapi" + ], + "homepage": "https://pipedream.com/apps/callerapi", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/callhub/README.md b/components/callhub/README.md new file mode 100644 index 0000000000000..19fa09bb64830 --- /dev/null +++ b/components/callhub/README.md @@ -0,0 +1,11 @@ +# Overview + +The CallHub API offers programmatic access to a range of telecommunication features, such as phone banking, text messaging, and voice broadcasting. Integrating CallHub with Pipedream allows users to automate these communication services within custom workflows. With Pipedream’s serverless platform, you can react to events across various apps, store and manipulate data, and trigger communications via CallHub based on user-defined criteria. This seamless integration expands the automation possibilities for campaign management, engagement tracking, and proactive notifications. + +# Example Use Cases + +- **Automated Campaign Feedback Collection**: Gather feedback from CallHub phone campaigns automatically. After a call campaign ends, trigger a workflow that collects call data, analyzes responses, and stores results in a Google Sheet for easy review. + +- **SMS Subscriber Engagement**: Increase engagement with SMS subscribers. Set up a workflow to monitor a CRM like HubSpot for updated subscriber lists, and use CallHub to send personalized text messages to new or updated contacts. + +- **Proactive Support Notifications**: Enhance customer support with proactive notifications. Create a workflow where a ticket creation in Zendesk triggers an informative voice message via CallHub to the customer, providing immediate acknowledgment and estimated wait times. diff --git a/components/callingly/README.md b/components/callingly/README.md index 44cb1917e1254..dd5d413754909 100644 --- a/components/callingly/README.md +++ b/components/callingly/README.md @@ -1,11 +1,11 @@ # Overview -The Callingly API allows developers to programmatically place and track phone -calls. This can be used to create a variety of applications, including: +The Callingly API on Pipedream empowers you to automate call initiation and follow-ups, sync lead information, and manage user interactions in real-time. By leveraging Pipedream's serverless platform, you can seamlessly integrate Callingly with numerous other apps to streamline communication workflows, trigger calls based on specific actions, and analyze call data for insights. -- Call center automation -- Lead generation -- Appointment reminders -- Customer support hotlines +# Example Use Cases -And more! +- **Lead Qualification Automation**: Trigger Callingly to initiate a call to a new lead as soon as they complete a form on your website. Use Pipedream to capture the form submission, perhaps from a service like Typeform, and automatically send the lead info to Callingly to start the call, ensuring immediate contact and increasing the chances of conversion. + +- **Support Ticket Escalation**: When a support ticket in a system like Zendesk is marked as urgent, automatically trigger a Callingly call to the assigned support agent. This ensures that critical issues are communicated instantly, reducing response time and improving customer satisfaction. + +- **CRM Integration for Sales Follow-up**: After a deal stage is updated in your CRM, such as HubSpot, use Pipedream to trigger a follow-up call via Callingly. This workflow can help sales teams stay on top of their pipelines by automating routine follow-ups, thus allowing more time to focus on closing deals. diff --git a/components/calllerapi/calllerapi.app.mjs b/components/calllerapi/calllerapi.app.mjs new file mode 100644 index 0000000000000..cf89b869907a9 --- /dev/null +++ b/components/calllerapi/calllerapi.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "calllerapi", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/calllerapi/package.json b/components/calllerapi/package.json new file mode 100644 index 0000000000000..6110646927d9c --- /dev/null +++ b/components/calllerapi/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/calllerapi", + "version": "0.0.1", + "description": "Pipedream CalllerAPI Components", + "main": "calllerapi.app.mjs", + "keywords": [ + "pipedream", + "calllerapi" + ], + "homepage": "https://pipedream.com/apps/calllerapi", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/callminer/callminer.app.mjs b/components/callminer/callminer.app.mjs new file mode 100644 index 0000000000000..5c908a53a4abc --- /dev/null +++ b/components/callminer/callminer.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "callminer", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/callminer/package.json b/components/callminer/package.json new file mode 100644 index 0000000000000..d9c7ee9414833 --- /dev/null +++ b/components/callminer/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/callminer", + "version": "0.0.1", + "description": "Pipedream CallMiner Components", + "main": "callminer.app.mjs", + "keywords": [ + "pipedream", + "callminer" + ], + "homepage": "https://pipedream.com/apps/callminer", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/callpage/README.md b/components/callpage/README.md new file mode 100644 index 0000000000000..9116f1c78f1ea --- /dev/null +++ b/components/callpage/README.md @@ -0,0 +1,11 @@ +# Overview + +The CallPage API enables automations centered around phone calls and lead management. Integrating this API with Pipedream allows you to create, update, and track calls, as well as manage users and leads. With Pipedream's serverless architecture, you can easily connect CallPage to other apps, triggering complex workflows from CallPage events or performing actions in CallPage in response to external triggers. + +# Example Use Cases + +- **Lead Qualification Automation**: Automate the process of qualifying leads by connecting CallPage to a CRM like HubSpot. When a call ends, send the call details and outcome to HubSpot to create or update a contact, enriching lead data for better follow-up and conversion. + +- **Real-time SMS Notifications**: Get immediate notifications via Twilio SMS when a new call is requested through CallPage. Use this workflow to alert sales reps so they can connect with potential clients faster, reducing response times and increasing the likelihood of closing deals. + +- **Customer Support Ticket Creation**: Create a helpdesk ticket in a platform like Zendesk when a call on CallPage indicates a customer support issue. By capturing call details and client requests, you can ensure that support teams are informed and can provide timely assistance. diff --git a/components/callrail/README.md b/components/callrail/README.md new file mode 100644 index 0000000000000..ed8d56f1b09b3 --- /dev/null +++ b/components/callrail/README.md @@ -0,0 +1,11 @@ +# Overview + +The CallRail API provides access to data about calls, texts, and form submissions that flow through the CallRail system. Leveraging Pipedream's serverless platform, you can automate workflows triggered by these events. For instance, you can log calls to a Google Sheet, create real-time alerts, or sync call data with a CRM like Salesforce. + +# Example Use Cases + +- **Call Logging to Google Sheets**: When new calls are tracked in CallRail, use Pipedream to append the call details to a Google Sheet. This can help you maintain records, analyze call data, or share insights with your team. + +- **Real-time SMS Alerts for Missed Calls**: Set up a Pipedream workflow that monitors your CallRail account for missed calls and instantly sends an SMS alert via Twilio, ensuring you can follow up promptly. + +- **CRM Integration for Call Tracking**: Automatically add CallRail call details to Salesforce as new leads or activities. This streamlines your sales process by keeping your CRM updated with the latest call interactions. diff --git a/components/callrail/package.json b/components/callrail/package.json index 1d17ee181e7f4..d95408c4a4e11 100644 --- a/components/callrail/package.json +++ b/components/callrail/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/campaign_cleaner/README.md b/components/campaign_cleaner/README.md new file mode 100644 index 0000000000000..81cc8cf0ea63b --- /dev/null +++ b/components/campaign_cleaner/README.md @@ -0,0 +1,11 @@ +# Overview + +The Campaign Cleaner API offers tools to clean and verify email lists, improving email campaign performance and sender reputation. By integrating with Pipedream, you can automate workflows that enhance your email marketing efforts. Use the API to check the validity of email addresses, remove duplicates, and classify email types. Pipedream's serverless platform allows you to trigger these processes from various events, schedule them, or manually invoke them as needed. + +# Example Use Cases + +- **Email Verification on User Signup**: When a new user signs up on your website, automatically verify their email address using the Campaign Cleaner API. Connect this with your authentication system to ensure only valid emails are added to your database. + +- **Scheduled Email List Cleaning**: Set up a recurring Pipedream workflow that periodically cleans your entire email list. Use Campaign Cleaner to remove invalid, disposable, or duplicate email addresses, maintaining a high-quality list for better engagement rates. + +- **Subscriber Classification and Segmentation**: Classify and segment your subscribers based on the analysis from Campaign Cleaner. Integrate with email marketing apps like Mailchimp or SendGrid to create targeted campaigns for different types of email addresses, such as personal, business or role-based. diff --git a/components/campaign_monitor/actions/add-subscriber/add-subscriber.mjs b/components/campaign_monitor/actions/add-subscriber/add-subscriber.mjs new file mode 100644 index 0000000000000..4d5e440692e1d --- /dev/null +++ b/components/campaign_monitor/actions/add-subscriber/add-subscriber.mjs @@ -0,0 +1,84 @@ +import campaignMonitor from "../../campaign_monitor.app.mjs"; + +export default { + key: "campaign_monitor-add-subscriber", + name: "Add Subscriber", + description: "Creates a new subscriber on a specific list. [See the documentation](https://www.campaignmonitor.com/api/v3-3/subscribers/)", + version: "0.0.1", + type: "action", + props: { + campaignMonitor, + clientId: { + propDefinition: [ + campaignMonitor, + "clientId", + ], + }, + listId: { + propDefinition: [ + campaignMonitor, + "listId", + (c) => ({ + clientId: c.clientId, + }), + ], + }, + email: { + type: "string", + label: "Email", + description: "The email address of the subscriber", + }, + name: { + type: "string", + label: "Name", + description: "The name of the subscriber", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The phone number of the subscriber. Example: `+5012398752`", + optional: true, + }, + consentToTrack: { + propDefinition: [ + campaignMonitor, + "consentToTrack", + ], + }, + consentToSendSMS: { + type: "string", + label: "Consent to Send SMS", + description: "Indicates if consent has been granted by the subscriber to receive Sms", + options: [ + "Yes", + "No", + "Unchanged", + ], + default: "Unchanged", + optional: true, + }, + resubscribe: { + type: "boolean", + label: "Resubscribe", + description: "Resubscribe if the email address has previously been unsubscribed", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.campaignMonitor.createSubscriber({ + $, + listId: this.listId, + data: { + EmailAddress: this.email, + Name: this.name, + MobileNumber: this.phone, + ConsentToTrack: this.consentToTrack, + ConsentToSendSms: this.consentToSendSMS, + Resubscribe: this.resubscribe, + }, + }); + $.export("$summary", `Successfully added subscriber ${this.email}`); + return response; + }, +}; diff --git a/components/campaign_monitor/actions/send-smart-transactional-email/send-smart-transactional-email.mjs b/components/campaign_monitor/actions/send-smart-transactional-email/send-smart-transactional-email.mjs new file mode 100644 index 0000000000000..3507386a189f6 --- /dev/null +++ b/components/campaign_monitor/actions/send-smart-transactional-email/send-smart-transactional-email.mjs @@ -0,0 +1,64 @@ +import campaignMonitor from "../../campaign_monitor.app.mjs"; + +export default { + key: "campaign_monitor-send-smart-transactional-email", + name: "Send Smart Transactional Email", + description: "Sends an intelligent transactional email to a specified recipient. [See the documentation](https://www.campaignmonitor.com/api/v3-3/transactional/#send-smart-email)", + version: "0.0.1", + type: "action", + props: { + campaignMonitor, + clientId: { + propDefinition: [ + campaignMonitor, + "clientId", + ], + }, + smartEmailId: { + propDefinition: [ + campaignMonitor, + "smartEmailId", + (c) => ({ + clientId: c.clientId, + }), + ], + }, + to: { + type: "string", + label: "To", + description: "An array of email addresses to send the email to", + }, + cc: { + type: "string", + label: "CC", + description: "An array of email address to carbon copy the email to", + optional: true, + }, + bcc: { + type: "string", + label: "BCC", + description: "An array of email address to blind carbon copy the email to", + optional: true, + }, + consentToTrack: { + propDefinition: [ + campaignMonitor, + "consentToTrack", + ], + }, + }, + async run({ $ }) { + const response = await this.campaignMonitor.sendSmartEmail({ + $, + smartEmailId: this.smartEmailId, + data: { + To: this.to, + CC: this.cc, + BCC: this.bcc, + ConsentToTrack: this.consentToTrack, + }, + }); + $.export("$summary", "Successfully sent smart transactional email"); + return response; + }, +}; diff --git a/components/campaign_monitor/actions/unsubscribe/unsubscribe.mjs b/components/campaign_monitor/actions/unsubscribe/unsubscribe.mjs new file mode 100644 index 0000000000000..fb5d562932f90 --- /dev/null +++ b/components/campaign_monitor/actions/unsubscribe/unsubscribe.mjs @@ -0,0 +1,47 @@ +import campaignMonitor from "../../campaign_monitor.app.mjs"; + +export default { + key: "campaign_monitor-unsubscribe", + name: "Unsubscribe", + description: "Removes a subscriber from a mailing list given their email address. [See the documentation](https://www.campaignmonitor.com/api/v3-3/subscribers/#unsubscribing-a-subscriber)", + version: "0.0.1", + type: "action", + props: { + campaignMonitor, + clientId: { + propDefinition: [ + campaignMonitor, + "clientId", + ], + }, + listId: { + propDefinition: [ + campaignMonitor, + "listId", + (c) => ({ + clientId: c.clientId, + }), + ], + }, + subscriber: { + propDefinition: [ + campaignMonitor, + "subscriber", + (c) => ({ + listId: c.listId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.campaignMonitor.unsubscribeSubscriber({ + $, + listId: this.listId, + data: { + EmailAddress: this.subscriber, + }, + }); + $.export("$summary", `Successfully unsubscribed ${this.subscriber}`); + return response; + }, +}; diff --git a/components/campaign_monitor/campaign_monitor.app.mjs b/components/campaign_monitor/campaign_monitor.app.mjs new file mode 100644 index 0000000000000..ead348f6733b7 --- /dev/null +++ b/components/campaign_monitor/campaign_monitor.app.mjs @@ -0,0 +1,270 @@ +import { axios } from "@pipedream/platform"; +const DEFAULT_PAGE_SIZE = 1000; + +export default { + type: "app", + app: "campaign_monitor", + propDefinitions: { + clientId: { + type: "string", + label: "Client ID", + description: "The ID of the client", + async options() { + const clients = await this.listClients(); + return clients?.map(({ + ClientID: value, Name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + campaignId: { + type: "string", + label: "Campaign ID", + description: "The ID of a sent or scheduled campaign", + async options({ clientId }) { + const campaigns = await this.listCampaigns(clientId); + return campaigns?.map(({ + CampaignID: value, Name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + subscriber: { + type: "string", + label: "Subscriber", + description: "The email address of the subscriber", + async options({ + listId, page, + }) { + const { Results: subscribers } = await this.listSubscribers({ + listId, + params: { + page: page + 1, + }, + }); + return subscribers?.map(({ + EmailAddress: value, Name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + listId: { + type: "string", + label: "List ID", + description: "The ID of the list", + async options({ clientId }) { + const lists = await this.listLists({ + clientId, + }); + return lists?.map(({ + ListID: value, Name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + smartEmailId: { + type: "string", + label: "Smart Email ID", + description: "The ID of the smart email to send", + async options({ clientId }) { + const emails = await this.listSmartEmails({ + params: { + clientId, + }, + }); + return emails?.map(({ + ID: value, Name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + consentToTrack: { + type: "string", + label: "Consent to Track", + description: "Whether the subscriber has given permission to have their email opens and clicks tracked", + options: [ + "Yes", + "No", + "Unchanged", + ], + default: "Unchanged", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.createsend.com/api/v3.3"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + const requestFn = async () => { + return await axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }; + return await this.retryWithExponentialBackoff(requestFn); + }, + // The API has been observed to occasionally return - + // {"Code":120,"Message":"Invalid OAuth Token"} + // Retry if a 401 Unauthorized or 429 (Rate limit exceeded) + // status is returned + async retryWithExponentialBackoff(requestFn, retries = 3, backoff = 500) { + try { + return await requestFn(); + } catch (error) { + if (retries > 0 && (error.response?.status === 401 || error.response?.status === 429)) { + await new Promise((resolve) => setTimeout(resolve, backoff)); + return this.retryWithExponentialBackoff(requestFn, retries - 1, backoff * 2); + } + throw error; + } + }, + listClients(opts = {}) { + return this._makeRequest({ + path: "/clients.json", + ...opts, + }); + }, + async listCampaigns(clientId) { + const scheduledCampaigns = await this.listScheduledCampaigns({ + clientId, + }); + const { Results: sentCampaigns } = await this.listSentCampaigns({ + clientId, + params: { + pagesize: DEFAULT_PAGE_SIZE, + }, + }); + return [ + ...scheduledCampaigns, + ...sentCampaigns, + ]; + }, + listSentCampaigns({ + clientId, ...opts + }) { + return this._makeRequest({ + path: `/clients/${clientId}/campaigns.json`, + ...opts, + }); + }, + listScheduledCampaigns({ + clientId, ...opts + }) { + return this._makeRequest({ + path: `/clients/${clientId}/scheduled.json`, + ...opts, + }); + }, + listBounces({ + campaignId, ...opts + }) { + return this._makeRequest({ + path: `/campaigns/${campaignId}/bounces.json`, + ...opts, + }); + }, + listOpens({ + campaignId, ...opts + }) { + return this._makeRequest({ + path: `/campaigns/${campaignId}/opens.json`, + ...opts, + }); + }, + listSubscribers({ + listId, ...opts + }) { + return this._makeRequest({ + path: `/lists/${listId}/active.json`, + ...opts, + }); + }, + listLists({ + clientId, ...opts + }) { + return this._makeRequest({ + path: `/clients/${clientId}/lists.json`, + ...opts, + }); + }, + listSmartEmails(opts = {}) { + return this._makeRequest({ + path: "/transactional/smartEmail", + ...opts, + }); + }, + sendSmartEmail({ + smartEmailId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/transactional/smartEmail/${smartEmailId}/send`, + ...opts, + }); + }, + unsubscribeSubscriber({ + listId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/subscribers/${listId}/unsubscribe.json`, + ...opts, + }); + }, + createSubscriber({ + listId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/subscribers/${listId}.json`, + ...opts, + }); + }, + async *paginate({ + fn, + args, + max, + }) { + args = { + ...args, + params: { + ...args?.params, + pagesize: DEFAULT_PAGE_SIZE, + }, + }; + let hasMore, count = 0; + do { + const { + Results: results, NumberOfPages: numPages, + } = await fn(args); + for (const item of results) { + yield item; + if (max && ++count >= max) { + return; + } + } + hasMore = args.params.page < numPages; + args.params.page++; + } while (hasMore); + }, + }, +}; diff --git a/components/campaign_monitor/package.json b/components/campaign_monitor/package.json new file mode 100644 index 0000000000000..2a5c8328cfbcc --- /dev/null +++ b/components/campaign_monitor/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/campaign_monitor", + "version": "0.1.0", + "description": "Pipedream Campaign Monitor Components", + "main": "campaign_monitor.app.mjs", + "keywords": [ + "pipedream", + "campaign_monitor" + ], + "homepage": "https://pipedream.com/apps/campaign_monitor", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/campaign_monitor/sources/common/base.mjs b/components/campaign_monitor/sources/common/base.mjs new file mode 100644 index 0000000000000..df120d4c3fe96 --- /dev/null +++ b/components/campaign_monitor/sources/common/base.mjs @@ -0,0 +1,82 @@ +import campaignMonitor from "../../campaign_monitor.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + campaignMonitor, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + clientId: { + propDefinition: [ + campaignMonitor, + "clientId", + ], + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + getArgs() { + return {}; + }, + getTsField() { + return "Date"; + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + const fn = this.getResourceFn(); + const args = this.getArgs(); + const tsField = this.getTsField(); + + const results = this.campaignMonitor.paginate({ + fn, + args, + max, + }); + + const items = []; + for await (const item of results) { + const ts = Date.parse(item[tsField]); + if (ts >= lastTs) { + items.push(item); + } else { + break; + } + } + + if (!items?.length) { + return; + } + + this._setLastTs(Date.parse(items[0][tsField])); + + items.forEach((item) => { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/campaign_monitor/sources/new-bounce/new-bounce.mjs b/components/campaign_monitor/sources/new-bounce/new-bounce.mjs new file mode 100644 index 0000000000000..868af3f4d7f31 --- /dev/null +++ b/components/campaign_monitor/sources/new-bounce/new-bounce.mjs @@ -0,0 +1,46 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "campaign_monitor-new-bounce", + name: "New Bounce", + description: "Emit new event when a campaign email bounces", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + campaignId: { + propDefinition: [ + common.props.campaignMonitor, + "campaignId", + (c) => ({ + clientId: c.clientId, + }), + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.campaignMonitor.listBounces; + }, + getArgs() { + return { + campaignId: this.campaignId, + params: { + orderfield: "date", + orderdirection: "desc", + }, + }; + }, + generateMeta(bounce) { + const ts = Date.parse(bounce[this.getTsField()]); + return { + id: `${bounce.EmailAddress}-${ts}`, + summary: `New Bounce: ${bounce.EmailAddress}`, + ts, + }; + }, + }, +}; diff --git a/components/campaign_monitor/sources/new-email-open/new-email-open.mjs b/components/campaign_monitor/sources/new-email-open/new-email-open.mjs new file mode 100644 index 0000000000000..199bf6ed132ee --- /dev/null +++ b/components/campaign_monitor/sources/new-email-open/new-email-open.mjs @@ -0,0 +1,46 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "campaign_monitor-new-email-open", + name: "New Email Open", + description: "Emit new event when an email from a campaign is opened", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + campaignId: { + propDefinition: [ + common.props.campaignMonitor, + "campaignId", + (c) => ({ + clientId: c.clientId, + }), + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.campaignMonitor.listOpens; + }, + getArgs() { + return { + campaignId: this.campaignId, + params: { + orderfield: "date", + orderdirection: "desc", + }, + }; + }, + generateMeta(open) { + const ts = Date.parse(open[this.getTsField()]); + return { + id: `${open.EmailAddress}-${ts}`, + summary: `New Email Open: ${open.EmailAddress}`, + ts, + }; + }, + }, +}; diff --git a/components/campaign_monitor/sources/new-subscriber/new-subscriber.mjs b/components/campaign_monitor/sources/new-subscriber/new-subscriber.mjs new file mode 100644 index 0000000000000..5149782b83839 --- /dev/null +++ b/components/campaign_monitor/sources/new-subscriber/new-subscriber.mjs @@ -0,0 +1,45 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "campaign_monitor-new-subscriber", + name: "New Subscriber Added", + description: "Emit new event when a new subscriber is added to a specific list", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + listId: { + propDefinition: [ + common.props.campaignMonitor, + "listId", + (c) => ({ + clientId: c.clientId, + }), + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.campaignMonitor.listSubscribers; + }, + getArgs() { + return { + listId: this.listId, + params: { + orderfield: "date", + orderdirection: "desc", + }, + }; + }, + generateMeta(subscriber) { + return { + id: subscriber.EmailAddress, + summary: `New Subscriber: ${subscriber.EmailAddress}`, + ts: Date.parse(subscriber[this.getTsField()]), + }; + }, + }, +}; diff --git a/components/campaignhq/README.md b/components/campaignhq/README.md new file mode 100644 index 0000000000000..5af9dfa191464 --- /dev/null +++ b/components/campaignhq/README.md @@ -0,0 +1,11 @@ +# Overview + +The CampaignHQ API offers tools to engage in political campaigns, supporting services like phone calls, texts, and surveys. With Pipedream, you can link CampaignHQ's capabilities with other apps to automate your political campaign's engagement and data management. Using Pipedream's serverless platform, you can trigger workflows on events, process and analyze incoming data, and respond in real-time, all without managing infrastructure. + +# Example Use Cases + +- **Automate Volunteer Recruitment Notifications**: When new volunteers sign up via a form on your website, trigger a Pipedream workflow that automatically sends their details to CampaignHQ to queue a welcome phone call or text, ensuring a personal touch is added to your campaign outreach instantaneously. + +- **Sync Survey Responses to a CRM**: Collect survey responses through CampaignHQ's API, then use a Pipedream workflow to parse the data and sync it with your CRM platform, like Salesforce. This can help you to maintain an up-to-date record of constituent opinions and streamline follow-up communication strategies. + +- **Coordinate Event Invitations**: Use CampaignHQ to send out mass invites to a political rally or fundraiser. With Pipedream, set up a workflow that listens for RSVPs, automatically updates your event management app (such as Eventbrite), and sends a personalized confirmation with event details using a service like SendGrid. diff --git a/components/campayn/README.md b/components/campayn/README.md new file mode 100644 index 0000000000000..0fe9da9da84ce --- /dev/null +++ b/components/campayn/README.md @@ -0,0 +1,11 @@ +# Overview + +The Campayn API lets you automate email marketing tasks directly within Pipedream. It provides programmatic access to manage contacts, lists, and email campaigns. With Pipedream, you can build serverless workflows to integrate Campayn with other apps and services, triggering actions based on events, scheduling regular email campaigns, or syncing data across platforms. + +# Example Use Cases + +- **Automate Contact Syncing**: Create a workflow that synchronizes contacts between Campayn and your CRM system. Whenever a new contact is added in your CRM, the workflow can automatically add or update the contact in your designated Campayn list. + +- **Email Campaign Triggered by E-Commerce Events**: Set up a workflow that sends targeted email campaigns through Campayn when a customer makes a purchase, abandons a cart, or performs a significant action on your e-commerce platform. + +- **Periodic Campaign Performance Reports**: Develop a workflow that fetches campaign performance data from Campayn on a regular basis and compiles it into a report using Google Sheets or a data visualization tool, then sends the report via email or Slack to your marketing team. diff --git a/components/canny/README.md b/components/canny/README.md index 180dd9844edb7..8a666e6ca2805 100644 --- a/components/canny/README.md +++ b/components/canny/README.md @@ -1,9 +1,11 @@ # Overview -With Canny, you can build a variety of things, including: +The Canny API provides programmatic access to Canny's customer feedback management platform. With it, you can automate the collection, analysis, and response to user feedback. This includes creating posts, voting on behalf of users, fetching boards, and managing comments and tags. Such integrations can streamline how you gather insights and act on user suggestions, making sure the voice of the customer is heard and addressed efficiently. -- Customizable feedback forms -- Single sign-on (SSO) capabilities -- User management tools -- Integration with third-party applications -- And more! +# Example Use Cases + +- **Feedback Loop Automation**: Trigger a workflow on Pipedream when a new feedback post is created on Canny. This can be further integrated to notify a Slack channel, create a task in a project management tool like Trello, or log the feedback in a Google Sheet for review during product meetings. + +- **Customer Support Enhancement**: When a customer support ticket in Zendesk or another service mentions a feature request or bug, automatically check Canny for related posts and link them. If none exist, create a new feedback post in Canny, ensuring the team is aware of customer needs and can prioritize them accordingly. + +- **Product Roadmap Planning**: On a schedule, compile votes and comments from Canny and feed them into a BI tool like Google Data Studio or Tableau. Analyze trends to inform product roadmap decisions, ensuring that the most requested features are given priority. diff --git a/components/canny/package.json b/components/canny/package.json new file mode 100644 index 0000000000000..cff4df7d5dea0 --- /dev/null +++ b/components/canny/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/canny", + "version": "0.6.0", + "description": "Pipedream canny Components", + "main": "canny.app.mjs", + "keywords": [ + "pipedream", + "canny" + ], + "homepage": "https://pipedream.com/apps/canny", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/canva/README.md b/components/canva/README.md new file mode 100644 index 0000000000000..ba976be642dd2 --- /dev/null +++ b/components/canva/README.md @@ -0,0 +1,11 @@ +# Overview + +The Canva API enables automated interaction with the Canva platform, allowing users to create, edit, and manage designs programmatically. By using this API on Pipedream, you can streamline design workflows, integrate rich media generation into your apps, or even automate content creation across various marketing channels. The core power lies in its ability to seamlessly integrate with other services, making it a vital tool for dynamic content creation that can adapt to data inputs from various sources. + +# Example Use Cases + +- **Automated Social Media Content Creation**: Set up a workflow on Pipedream where, based on calendar events (like holidays or company events from Google Calendar), Canva is used to automatically generate and update social media graphics tailored to these events. These can then be pushed directly to platforms like Twitter or Facebook. + +- **Dynamic Email Marketing Graphics**: Integrate Canva with email platforms such as SendGrid on Pipedream. Automatically create customized email headers or promotional banners based on subscriber data or segmentations. For instance, generate unique birthday offers or event-specific banners, enhancing the personalization of marketing campaigns. + +- **Real-Time Merchandising Updates**: Connect Canva to an e-commerce platform like Shopify via Pipedream. Automatically update product images based on inventory changes, seasonal promotions, or trending colors and designs. This keeps the visual content fresh and aligned with marketing strategies without manual intervention. diff --git a/components/canva/actions/create-design-import-job/create-design-import-job.mjs b/components/canva/actions/create-design-import-job/create-design-import-job.mjs new file mode 100644 index 0000000000000..42c2beb4200f1 --- /dev/null +++ b/components/canva/actions/create-design-import-job/create-design-import-job.mjs @@ -0,0 +1,68 @@ +import canva from "../../canva.app.mjs"; +import fs from "fs"; + +export default { + key: "canva-create-design-import-job", + name: "Create Design Import Job", + description: "Starts a new job to import an external file as a new design in Canva. [See the documentation](https://www.canva.dev/docs/connect/api-reference/design-imports/create-design-import-job/)", + version: "0.0.5", + type: "action", + props: { + canva, + title: { + propDefinition: [ + canva, + "title", + ], + }, + filePath: { + propDefinition: [ + canva, + "filePath", + ], + }, + waitForCompletion: { + propDefinition: [ + canva, + "waitForCompletion", + ], + }, + }, + async run({ $ }) { + const titleBase64 = Buffer.from(this.title).toString("base64"); + const filePath = this.filePath.includes("tmp/") + ? this.filePath + : `/tmp/${this.filePath}`; + let response = await this.canva.importDesign({ + $, + headers: { + "Import-Metadata": JSON.stringify({ + "title_base64": titleBase64, + }), + "Content-Length": fs.statSync(filePath).size, + "Content-Type": "application/octet-stream", + }, + data: fs.createReadStream(filePath), + }); + + if (this.waitForCompletion) { + const timer = (ms) => new Promise((res) => setTimeout(res, ms)); + const importId = response.job_id; + while (!response?.status || response.status?.state === "importing") { + response = await this.canva.getDesignImportJob({ + $, + importId, + }); + if (response.status.error) { + throw new Error(response.status.error.message); + } + await timer(3000); + } + } + + $.export("$summary", `Successfully ${this.waitForCompletion + ? "imported" + : "started import job for"} design "${this.title}"`); + return response; + }, +}; diff --git a/components/canva/actions/create-design/create-design.mjs b/components/canva/actions/create-design/create-design.mjs new file mode 100644 index 0000000000000..7eb7d7d49d44f --- /dev/null +++ b/components/canva/actions/create-design/create-design.mjs @@ -0,0 +1,77 @@ +import canva from "../../canva.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "canva-create-design", + name: "Create Design", + description: "Creates a new Canva design. [See the documentation](https://www.canva.dev/docs/connect/api-reference/designs/create-design/)", + version: "0.0.5", + type: "action", + props: { + canva, + designType: { + type: "string", + label: "Design Type", + description: "The desired design type", + reloadProps: true, + options: constants.DESIGN_TYPE_OPTIONS, + }, + title: { + propDefinition: [ + canva, + "title", + ], + optional: true, + }, + assetId: { + type: "string", + label: "Asset ID", + description: "The ID of the asset to add to the new design", + optional: true, + }, + }, + async additionalProps() { + const props = {}; + if (!this.designType) { + return props; + } + if (this.designType === "preset") { + props.name = { + type: "string", + label: "Design Type Name", + description: "The name of the design type", + options: constants.DESIGN_TYPE_NAME_OPTIONS, + }; + } + if (this.designType === "custom") { + props.width = { + type: "integer", + label: "Width", + description: "The width of the design (in pixels). Minimum 40px, maximum 8000px", + }; + props.height = { + type: "integer", + label: "Height", + description: "The height of the design (in pixels). Minimum 40px, maximum 8000px", + }; + } + return props; + }, + async run({ $ }) { + const response = await this.canva.createDesign({ + $, + data: { + design_type: { + type: this.designType, + name: this.name, + width: this.width, + height: this.height, + }, + title: this.title, + asset_id: this.assetId, + }, + }); + $.export("$summary", `Created design with ID: ${response.design.id}`); + return response; + }, +}; diff --git a/components/canva/actions/export-design/export-design.mjs b/components/canva/actions/export-design/export-design.mjs new file mode 100644 index 0000000000000..efc7dd51a919d --- /dev/null +++ b/components/canva/actions/export-design/export-design.mjs @@ -0,0 +1,145 @@ +import canva from "../../canva.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "canva-export-design", + name: "Export Design", + description: "Starts a new job to export a file from Canva. [See the documentation](https://www.canva.dev/docs/connect/api-reference/exports/create-design-export-job/)", + version: "0.0.5", + type: "action", + props: { + canva, + designId: { + propDefinition: [ + canva, + "designId", + ], + }, + type: { + type: "string", + label: "Format Type", + description: "The desired export format", + reloadProps: true, + options: constants.EXPORT_TYPES, + }, + pages: { + type: "integer[]", + label: "Pages", + description: "To specify which pages to export in a multi-page design, provide the page numbers as an array. The first page in a design is page `1`. If pages isn't specified, all the pages are exported.", + optional: true, + }, + waitForCompletion: { + propDefinition: [ + canva, + "waitForCompletion", + ], + }, + }, + async additionalProps() { + const props = {}; + if (!this.type) { + return props; + } + if (this.type === "jpg") { + props.quality = { + type: "integer", + label: "Quality", + description: "Determines how compressed the exported file should be. A low `quality` value (minimum `1`) will create a file with a smaller file size, but the resulting file will have pixelated artifacts when compared to a file created with a high `quality` value (maximum `100`).", + }; + } + if (this.type === "mp4") { + props.quality = { + type: "string", + label: "Quality", + description: "The orientation and resolution of the exported video", + options: constants.MP4_QUALITY, + }; + } + if (this.type === "pdf") { + props.size = { + type: "string", + label: "Paper Size", + description: "The paper size of the export PDF file", + options: constants.PAPER_SIZE, + optional: true, + }; + } + if (this.type === "png") { + props.lossless = { + type: "boolean", + label: "Lossless", + description: "When `true`, the PNG is compressed with a lossless compression algorithm (`false` by default)", + optional: true, + }; + props.asSingleImage = { + type: "boolean", + label: "As Single Image", + description: "When `true`, multi-page designs are merged into a single image. When `false` (default), each page is exported as a separate image", + optional: true, + }; + } + if (this.type === "pdf" || this.type === "jpg" || this.type === "png" || this.type === "gif" || this.type === "mp4") { + props.exportQuality = { + type: "string", + label: "Export Quality", + description: "Specifies the export quality of the design. A `pro` export might fail if the design contains premium elements and the calling user either hasn't purchased the elements or isn't on a Canva plan (such as Canva Pro) that has premium features.", + options: constants.EXPORT_QUALITY, + optional: true, + }; + if (this.type === "jpg" || this.type === "png" || this.type === "gif") { + props.height = { + type: "integer", + label: "Height", + description: "The height in pixels of the exported image", + optional: true, + }; + props.width = { + type: "integer", + label: "Width", + description: "The width in pixels of the exported image", + optional: true, + }; + } + } + return props; + }, + async run({ $ }) { + let response = await this.canva.exportDesign({ + $, + data: { + design_id: this.designId, + format: { + type: this.type, + pages: this.pages, + quality: this.quality, + export_quality: this.exportQuality, + size: this.size, + height: this.height, + width: this.width, + lossless: this.lossless, + as_single_image: this.asSingleImage, + }, + }, + }); + + if (this.waitForCompletion) { + const timer = (ms) => new Promise((res) => setTimeout(res, ms)); + const exportId = response.job.id; + while (response.job.status === "in_progress") { + response = await this.canva.getDesignExportJob({ + $, + exportId, + }); + if (response.job.error) { + throw new Error(response.job.error.message); + } + await timer(3000); + } + } + + $.export("$summary", `Successfully ${this.waitForCompletion + ? "exported" + : "started export job for"} design with ID "${this.designId}"`); + return response; + }, +}; diff --git a/components/canva/actions/upload-asset/upload-asset.mjs b/components/canva/actions/upload-asset/upload-asset.mjs new file mode 100644 index 0000000000000..a7b5d9f448fec --- /dev/null +++ b/components/canva/actions/upload-asset/upload-asset.mjs @@ -0,0 +1,67 @@ +import canva from "../../canva.app.mjs"; +import fs from "fs"; + +export default { + key: "canva-upload-asset", + name: "Upload Asset", + description: "Uploads an asset to Canva. [See the documentation](https://www.canva.dev/docs/connect/api-reference/assets/create-asset-upload-job/)", + version: "0.0.5", + type: "action", + props: { + canva, + name: { + type: "string", + label: "Asset Name", + description: "The asset's name", + }, + filePath: { + propDefinition: [ + canva, + "filePath", + ], + }, + waitForCompletion: { + propDefinition: [ + canva, + "waitForCompletion", + ], + }, + }, + async run({ $ }) { + const nameBase64 = Buffer.from(this.name).toString("base64"); + const filePath = this.filePath.includes("tmp/") + ? this.filePath + : `/tmp/${this.filePath}`; + let response = await this.canva.uploadAsset({ + $, + headers: { + "Asset-Upload-Metadata": JSON.stringify({ + "name_base64": nameBase64, + }), + "Content-Length": fs.statSync(filePath).size, + "Content-Type": "application/octet-stream", + }, + data: fs.createReadStream(filePath), + }); + + if (this.waitForCompletion) { + const timer = (ms) => new Promise((res) => setTimeout(res, ms)); + const jobId = response.job.id; + while (response.job.status === "in_progress") { + response = await this.canva.getUploadJob({ + $, + jobId, + }); + if (response.job.error) { + throw new Error(response.job.error.message); + } + await timer(3000); + } + } + + $.export("$summary", `Successfully ${this.waitForCompletion + ? "uploaded" + : "started upload job for"} asset "${this.name}"`); + return response; + }, +}; diff --git a/components/canva/canva.app.mjs b/components/canva/canva.app.mjs new file mode 100644 index 0000000000000..00c7d6df5c3ea --- /dev/null +++ b/components/canva/canva.app.mjs @@ -0,0 +1,134 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "canva", + propDefinitions: { + designId: { + type: "string", + label: "Design ID", + description: "The ID of the design", + async options({ prevContext }) { + const params = prevContext?.continuation + ? { + continuation: prevContext.continuation, + } + : {}; + const { + items, continuation, + } = await this.listDesigns({ + params, + }); + return { + options: items?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })) || [], + context: { + continuation, + }, + }; + }, + }, + title: { + type: "string", + label: "Title", + description: "The name of the design", + }, + filePath: { + type: "string", + label: "File Path", + description: "The path to a file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + }, + waitForCompletion: { + type: "boolean", + label: "Wait for Completion", + description: "Set to `true` to poll the API in 3-second intervals until the job is completed", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.canva.com/rest/v1"; + }, + _auth() { + return this.$auth.oauth_access_token; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + ...headers, + Authorization: `Bearer ${this._auth()}`, + }, + }); + }, + listDesigns(opts = {}) { + return this._makeRequest({ + path: "/designs", + ...opts, + }); + }, + getUploadJob({ + jobId, ...opts + }) { + return this._makeRequest({ + path: `/asset-uploads/${jobId}`, + ...opts, + }); + }, + uploadAsset(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/asset-uploads", + ...opts, + }); + }, + createDesign(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/designs", + ...opts, + }); + }, + importDesign(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/imports", + ...opts, + }); + }, + getDesignImportJob({ + importId, ...opts + }) { + return this._makeRequest({ + path: `/imports/${importId}`, + ...opts, + }); + }, + exportDesign(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/exports", + ...opts, + }); + }, + getDesignExportJob({ + exportId, ...opts + }) { + return this._makeRequest({ + path: `/exports/${exportId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/canva/common/constants.mjs b/components/canva/common/constants.mjs new file mode 100644 index 0000000000000..270ff5e055d20 --- /dev/null +++ b/components/canva/common/constants.mjs @@ -0,0 +1,66 @@ +const DESIGN_TYPE_OPTIONS = [ + { + label: "Provide the common design type", + value: "preset", + }, + { + label: "Provide the width and height to define a custom design type", + value: "custom", + }, +]; + +const DESIGN_TYPE_NAME_OPTIONS = [ + { + label: "Doc: A document for Canva's online text editor", + value: "doc", + }, + { + label: "Whiteboard: A design which gives you infinite space to collaborate", + value: "whiteboard", + }, + { + label: "Presentation: Lets you create and collaborate for presenting to an audience", + value: "presentation", + }, +]; + +const EXPORT_TYPES = [ + "pdf", + "jpg", + "png", + "pptx", + "gif", + "mp4", +]; + +const MP4_QUALITY = [ + "horizontal_480p", + "horizontal_720p", + "horizontal_1080p", + "horizontal_4k", + "vertical_480p", + "vertical_720p", + "vertical_1080p", + "vertical_4k", +]; + +const EXPORT_QUALITY = [ + "regular", + "pro", +]; + +const PAPER_SIZE = [ + "a4", + "a3", + "letter", + "legal", +]; + +export default { + DESIGN_TYPE_OPTIONS, + DESIGN_TYPE_NAME_OPTIONS, + EXPORT_TYPES, + MP4_QUALITY, + EXPORT_QUALITY, + PAPER_SIZE, +}; diff --git a/components/canva/package.json b/components/canva/package.json new file mode 100644 index 0000000000000..2fe7e79402317 --- /dev/null +++ b/components/canva/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/canva", + "version": "0.2.3", + "description": "Pipedream Canva Components", + "main": "canva.app.mjs", + "keywords": [ + "pipedream", + "canva" + ], + "homepage": "https://pipedream.com/apps/canva", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/canva_enterprise/actions/create-design-from-brand-template/create-design-from-brand-template.mjs b/components/canva_enterprise/actions/create-design-from-brand-template/create-design-from-brand-template.mjs new file mode 100644 index 0000000000000..0c226eff76d7e --- /dev/null +++ b/components/canva_enterprise/actions/create-design-from-brand-template/create-design-from-brand-template.mjs @@ -0,0 +1,112 @@ +import canva from "../../canva_enterprise.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "canva_enterprise-create-design-from-brand-template", + name: "Create Design from Brand Template", + description: "Creates an asynchronous job to autofill a design from a brand template with your input information. [See the documentation](https://www.canva.dev/docs/connect/api-reference/autofills/create-design-autofill-job/)", + version: "0.0.2", + type: "action", + props: { + canva, + brandTemplateId: { + propDefinition: [ + canva, + "brandTemplateId", + ], + reloadProps: true, + }, + alert: { + type: "alert", + alertType: "error", + content: "Design not fillable. Design does not have any autofill capable elements.", + hidden: true, + }, + title: { + type: "string", + label: "Title", + description: "Title to use for the autofilled design. Must be less than 256 characters. If no design title is provided, the autofilled design will have the same title as the brand template.", + optional: true, + }, + waitForCompletion: { + propDefinition: [ + canva, + "waitForCompletion", + ], + }, + }, + async additionalProps(existingProps) { + const props = {}; + if (!this.brandTemplateId) { + return props; + } + const { dataset } = await this.canva.getBrandTemplateDataset({ + brandTemplateId: this.brandTemplateId, + }); + if (!dataset) { + existingProps.alert.hidden = false; + return props; + } + for (const [ + key, + value, + ] of Object.entries(dataset)) { + props[key] = { + type: "string", + label: `${key}`, + description: value.type === "image" + ? "The asset ID of an image" + : "Please enter a text string", + }; + } + return props; + }, + async run({ $ }) { + const { dataset } = await this.canva.getBrandTemplateDataset({ + $, + brandTemplateId: this.brandTemplateId, + }); + if (!dataset) { + throw new ConfigurationError("Design not fillable. Design does not have any autofill capable elements."); + } + for (const [ + key, + value, + ] of Object.entries(dataset)) { + if (value.type === "image") { + dataset[key].asset_id = this[key]; + } else { + dataset[key].text = this[key]; + } + } + let response = await this.canva.createDesignAutofillJob({ + $, + data: { + brand_template_id: this.brandTemplateId, + data: dataset || {}, + title: this.title, + }, + }); + + if (this.waitForCompletion) { + const timer = (ms) => new Promise((res) => setTimeout(res, ms)); + const jobId = response.job.id; + while (response.job.status === "in_progress") { + response = await this.canva.getDesignAutofillJob({ + $, + jobId, + }); + if (response.job.error) { + throw new Error(response.job.error.message); + } + await timer(3000); + } + } + + $.export("$summary", `Successfully ${this.waitForCompletion + ? "created" + : "started autofil job for"} design`); + + return response; + }, +}; diff --git a/components/canva_enterprise/actions/create-design/create-design.mjs b/components/canva_enterprise/actions/create-design/create-design.mjs new file mode 100644 index 0000000000000..8a2ee44d35a18 --- /dev/null +++ b/components/canva_enterprise/actions/create-design/create-design.mjs @@ -0,0 +1,35 @@ +import canva from "../../canva_enterprise.app.mjs"; +import common from "@pipedream/canva/actions/create-design/create-design.mjs"; +import constants from "@pipedream/canva/common/constants.mjs"; + +export default { + ...common, + key: "canva_enterprise-create-design", + name: "Create Design", + description: "Creates a new Canva design. [See the documentation](https://www.canva.dev/docs/connect/api-reference/designs/create-design/)", + version: "0.0.2", + type: "action", + props: { + canva, + designType: { + type: "string", + label: "Design Type", + description: "The desired design type", + reloadProps: true, + options: constants.DESIGN_TYPE_OPTIONS, + }, + title: { + propDefinition: [ + canva, + "title", + ], + optional: true, + }, + assetId: { + type: "string", + label: "Asset ID", + description: "The ID of the asset to add to the new design", + optional: true, + }, + }, +}; diff --git a/components/canva_enterprise/actions/export-design/export-design.mjs b/components/canva_enterprise/actions/export-design/export-design.mjs new file mode 100644 index 0000000000000..6ef64d2a4ebe1 --- /dev/null +++ b/components/canva_enterprise/actions/export-design/export-design.mjs @@ -0,0 +1,40 @@ +import canva from "../../canva_enterprise.app.mjs"; +import common from "@pipedream/canva/actions/export-design/export-design.mjs"; +import constants from "@pipedream/canva/common/constants.mjs"; + +export default { + ...common, + key: "canva_enterprise-export-design", + name: "Export Design", + description: "Starts a new job to export a file from Canva. [See the documentation](https://www.canva.dev/docs/connect/api-reference/exports/create-design-export-job/)", + version: "0.0.2", + type: "action", + props: { + canva, + designId: { + propDefinition: [ + canva, + "designId", + ], + }, + type: { + type: "string", + label: "Format Type", + description: "The desired export format", + reloadProps: true, + options: constants.EXPORT_TYPES, + }, + pages: { + type: "integer[]", + label: "Pages", + description: "To specify which pages to export in a multi-page design, provide the page numbers as an array. The first page in a design is page `1`. If pages isn't specified, all the pages are exported.", + optional: true, + }, + waitForCompletion: { + propDefinition: [ + canva, + "waitForCompletion", + ], + }, + }, +}; diff --git a/components/canva_enterprise/actions/upload-asset/upload-asset.mjs b/components/canva_enterprise/actions/upload-asset/upload-asset.mjs new file mode 100644 index 0000000000000..78d7bff3e8b0d --- /dev/null +++ b/components/canva_enterprise/actions/upload-asset/upload-asset.mjs @@ -0,0 +1,31 @@ +import canva from "../../canva_enterprise.app.mjs"; +import common from "@pipedream/canva/actions/upload-asset/upload-asset.mjs"; + +export default { + ...common, + key: "canva_enterprise-upload-asset", + name: "Upload Asset", + description: "Uploads an asset to Canva. [See the documentation](https://www.canva.dev/docs/connect/api-reference/assets/create-asset-upload-job/)", + version: "0.0.2", + type: "action", + props: { + canva, + name: { + type: "string", + label: "Asset Name", + description: "The asset's name", + }, + filePath: { + propDefinition: [ + canva, + "filePath", + ], + }, + waitForCompletion: { + propDefinition: [ + canva, + "waitForCompletion", + ], + }, + }, +}; diff --git a/components/canva_enterprise/canva_enterprise.app.mjs b/components/canva_enterprise/canva_enterprise.app.mjs new file mode 100644 index 0000000000000..6b41bd8b1e305 --- /dev/null +++ b/components/canva_enterprise/canva_enterprise.app.mjs @@ -0,0 +1,73 @@ +import common from "@pipedream/canva"; + +export default { + ...common, + type: "app", + app: "canva_enterprise", + propDefinitions: { + ...common.propDefinitions, + brandTemplateId: { + type: "string", + label: "Brand Template ID", + description: "The ID of a brand template", + async options({ prevContext }) { + const params = prevContext?.continuation + ? { + continuation: prevContext.continuation, + } + : {}; + const { + items, continuation, + } = await this.listBrandTemplates({ + params, + }); + return { + options: items?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })) || [], + context: { + continuation, + }, + }; + }, + }, + }, + methods: { + ...common.methods, + _auth() { + return this.$auth.oauth_access_token; + }, + listBrandTemplates(opts = {}) { + return this._makeRequest({ + path: "/brand-templates", + ...opts, + }); + }, + getBrandTemplateDataset({ + brandTemplateId, ...opts + }) { + return this._makeRequest({ + path: `/brand-templates/${brandTemplateId}/dataset`, + ...opts, + }); + }, + createDesignAutofillJob(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/autofills", + ...opts, + }); + }, + getDesignAutofillJob({ + jobId, ...opts + }) { + return this._makeRequest({ + path: `/autofills/${jobId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/canva_enterprise/package.json b/components/canva_enterprise/package.json new file mode 100644 index 0000000000000..834d23f1c6a79 --- /dev/null +++ b/components/canva_enterprise/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/canva_enterprise", + "version": "0.1.1", + "description": "Pipedream Canva Enterprise Components", + "main": "canva_enterprise.app.mjs", + "keywords": [ + "pipedream", + "canva_enterprise" + ], + "homepage": "https://pipedream.com/apps/canva_enterprise", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/canva": "^0.2.3", + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/canvas/README.md b/components/canvas/README.md index 2b39582dbebde..865b8ec510f4c 100644 --- a/components/canvas/README.md +++ b/components/canvas/README.md @@ -1,10 +1,11 @@ # Overview -With the Canvas API, you can build applications that: - -- Create and manage courses -- Add and remove users from courses -- Submit and grade assignments -- Create and manage quiz questions -- View analytics about how students are using the course -- And much more! +The Canvas API allows you to tap into the power of the Canvas Learning Management System, enabling educators and administrators to automate tasks, synchronize educational tools, and manage courses and users programmatically. It provides endpoints for accessing course information, user details, assignments, and more, which can be incredibly useful for creating customized educational experiences and streamlining administrative workflows. With Pipedream, you can harness these capabilities to trigger actions, connect with other apps and services, and create complex automations without writing extensive code. + +# Example Use Cases + +- **Automated Course Enrollment**: Trigger an automation that adds students to courses in Canvas based on their enrollment in a separate student information system. When a new student record is created in the SIS, the workflow on Pipedream can add that student to the appropriate Canvas course. + +- **Assignment Submission Tracking**: Monitor assignment submissions in real-time by setting up a workflow that listens for new submissions on Canvas. When a submission is detected, send a notification to the instructor via email, Slack, or another messaging service connected through Pipedream. + +- **Grading and Feedback Sync**: Simplify the grading process by creating a workflow that automatically retrieves new grades entered in Canvas and syncs them with an external gradebook. Additionally, feedback entered in the external system can be pushed back to Canvas, keeping all records consistent and up-to-date. diff --git a/components/canvas/package.json b/components/canvas/package.json new file mode 100644 index 0000000000000..bec0fb966084d --- /dev/null +++ b/components/canvas/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/canvas", + "version": "0.6.0", + "description": "Pipedream canvas Components", + "main": "canvas.app.mjs", + "keywords": [ + "pipedream", + "canvas" + ], + "homepage": "https://pipedream.com/apps/canvas", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/capsule/README.md b/components/capsule/README.md index 9fef07c1434b6..73fb08b9b1751 100644 --- a/components/capsule/README.md +++ b/components/capsule/README.md @@ -1,8 +1,11 @@ # Overview -With the Capsule API, you can build applications that: +The Capsule API provides access to a treasure trove of customer relationship management (CRM) functions. Through Pipedream's serverless platform, it becomes fantastically simple to automate interactions with your Capsule CRM data. Envision syncing contacts, managing cases, tracking sales opportunities, or even automating follow-up emails based on customer behavior. The workflows created can be both time-saving and insightful, providing not just automation but also analytics on customer engagements. -- Integrate Capsule with your other systems -- Build custom tools and reports -- Automate tasks -- Extend Capsule's functionality +# Example Use Cases + +- **Sync New Sign-ups to Capsule CRM**: Automatically add new users from your sign-up form or app to Capsule as contacts. When someone signs up on your website, a Pipedream workflow can trigger, and with the Capsule API, it can directly add a new contact into your CRM, ensuring your sales team always has the latest leads to work with. + +- **Follow-up Automation for Support Tickets**: Create a workflow where new support tickets from your helpdesk software trigger follow-up tasks in Capsule. If a ticket goes unresolved for a certain period, Pipedream can use the Capsule API to create a follow-up task, ensuring high-priority customer issues never fall through the cracks. + +- **Sales Pipeline Update Notifications**: Connect Capsule to communication platforms like Slack. Set up a Pipedream workflow where any update to a sales opportunity in Capsule sends a notification to a designated Slack channel, keeping the team instantly informed about pipeline progress and enabling quick action on hot leads. diff --git a/components/capsule/package.json b/components/capsule/package.json new file mode 100644 index 0000000000000..0d944878c594d --- /dev/null +++ b/components/capsule/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/capsule", + "version": "0.6.0", + "description": "Pipedream capsule Components", + "main": "capsule.app.mjs", + "keywords": [ + "pipedream", + "capsule" + ], + "homepage": "https://pipedream.com/apps/capsule", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/captaindata/README.md b/components/captaindata/README.md index b76b364bbdb77..d0ae767336d85 100644 --- a/components/captaindata/README.md +++ b/components/captaindata/README.md @@ -1,11 +1,11 @@ # Overview -With the Captain Data API you can easily access data stored in your CaptainData -account and use it in your own applications. +Captain Data is a powerful automation tool that helps to extract and automate data processes. With its API, you can programmatically trigger tasks, extract data from various sources, and manage the data workflow. On Pipedream, integrating Captain Data's API expands its capabilities, enabling sophisticated serverless workflows that connect with numerous other apps and services. You can automate data extraction, enrichment, and dissemination tasks, setting up complex data pipelines without a single line of server maintenance. -Some examples of what you can build include: +# Example Use Cases -- A tool to track your energy usage -- A web app to help you plan your weekly grocery list -- A mobile app to track your fitness goals -- A desktop app to keep track of your tasks and to-dos +- **Sales Lead Generation and Enrichment Workflow**: Trigger a Captain Data task to scrape leads from specified sources, enrich these leads with additional contact information, and push the data to a CRM like Salesforce. In Pipedream, set up subsequent actions to segment the leads based on their profile data and schedule personalized follow-up emails via an email platform like SendGrid. + +- **Competitor Price Monitoring**: Configure a Captain Data workflow to regularly check competitor websites for product pricing. Use Pipedream to process the extracted data, compare it with your own pricing, and adjust your prices in your e-commerce store through platforms like Shopify. Add alerts through services like Slack to notify your team when significant price changes are detected. + +- **Social Media Sentiment Analysis**: Use Captain Data to extract recent comments and posts about your brand from social media platforms. Then, employ Pipedream to pass this data to a sentiment analysis service like MonkeyLearn. Based on the sentiment results, create automated responses or escalate issues to customer service representatives, integrating with a ticketing system like Zendesk. diff --git a/components/carbone/README.md b/components/carbone/README.md new file mode 100644 index 0000000000000..fa4f6f8030f75 --- /dev/null +++ b/components/carbone/README.md @@ -0,0 +1,11 @@ +# Overview + +The Carbone API provides a powerful solution for automating the generation and transformation of documents. With Carbone, you can dynamically create PDFs, Excel files, and more by injecting data into templates. On Pipedream, you can integrate Carbone into serverless workflows, tapping into its capabilities to automate document customization and distribution. Integrating Carbone into Pipedream workflows allows you to leverage data from various sources, transform it on-the-fly, and produce documents tailored for clients, reports, or any personalized content. + +# Example Use Cases + +- **Generate Monthly Reports**: Automatically create and send customized monthly financial reports. Connect Carbone to a Google Sheets data source, generate a PDF using a predefined template, and email the report using the Gmail app. + +- **Dynamic Invoicing**: Streamline the invoicing process by triggering an invoice generation whenever a new order is received in an eCommerce platform like Shopify. The Carbone API can populate an invoice template with order data and then save the invoice to Google Drive. + +- **Customized Welcome Letters**: For a membership site, use Carbone to create a personalized welcome letter each time a new member signs up. The letter can be generated from a template and sent via SendGrid or another email service directly to the new member's email address. diff --git a/components/carbone/package.json b/components/carbone/package.json index ab0d798bdab4b..9a52782744570 100644 --- a/components/carbone/package.json +++ b/components/carbone/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/cardinal/README.md b/components/cardinal/README.md new file mode 100644 index 0000000000000..28fae4a1087c6 --- /dev/null +++ b/components/cardinal/README.md @@ -0,0 +1,11 @@ +# Overview + +The Cardinal API is designed to streamline and automate interactions within project management tasks, providing a robust platform for managing tasks, projects, and team communication efficiently. By integrating the Cardinal API on Pipedream, users can automate complex workflows, synchronize data across various platforms, and enhance team productivity by automating routine actions and notifications. This seamless integration allows for real-time updates and interactions with tasks and projects, making it easier to keep track of progress and important details without manual intervention. + +# Example Use Cases + +- **Automated Task Sync with Google Calendar**: Create a workflow on Pipedream that triggers whenever a new task is added or modified in Cardinal. The workflow would automatically update or create an event in Google Calendar, ensuring that all task deadlines and details are synchronized across both platforms. This is particularly useful for maintaining a unified calendar view of all project deadlines and tasks. + +- **Slack Notifications for Task Updates**: Set up a Pipedream workflow that listens for changes in tasks on Cardinal (such as status updates or new comments) and sends a customized notification to a designated Slack channel. This keeps the whole team informed in real-time about important task developments, fostering better communication and quicker response times. + +- **Daily Task Digest Email**: Configure a daily Pipedream workflow that aggregates all new tasks and any updates made to existing tasks within the past 24 hours from Cardinal. The workflow would then compile this information into a structured email format and send it to all project stakeholders. This daily digest ensures that everyone is up-to-date with the latest project activities without having to manually check multiple platforms. diff --git a/components/cardinal/cardinal.app.mjs b/components/cardinal/cardinal.app.mjs new file mode 100644 index 0000000000000..9e098a0649482 --- /dev/null +++ b/components/cardinal/cardinal.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "cardinal", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/cardinal/package.json b/components/cardinal/package.json new file mode 100644 index 0000000000000..cbece94332c9a --- /dev/null +++ b/components/cardinal/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/cardinal", + "version": "0.0.1", + "description": "Pipedream Cardinal Components", + "main": "cardinal.app.mjs", + "keywords": [ + "pipedream", + "cardinal" + ], + "homepage": "https://pipedream.com/apps/cardinal", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/cardly/README.md b/components/cardly/README.md new file mode 100644 index 0000000000000..afbcd9d1bf1c7 --- /dev/null +++ b/components/cardly/README.md @@ -0,0 +1,11 @@ +# Overview + +The Cardly API empowers you to automate sending personalized cards and gifts to clients, employees, or any recipient with ease. Using Pipedream's serverless platform, you can create workflows that trigger on specific events or conditions, connecting Cardly with other apps to send out physical cards. Automate birthday greetings, thank you notes, holiday cards, or customer follow-ups with handwritten messages, leveraging the power of direct mail in a digital world. + +# Example Use Cases + +- **Automated Birthday Cards**: Set up a workflow on Pipedream where a daily trigger checks a Google Sheets spreadsheet for birthdays. When a match is found, it uses the Cardly API to send a personalized birthday card to the employee or customer, ensuring no special day goes unnoticed. + +- **CRM-triggered Thank You Cards**: After closing a deal or receiving a donation, trigger a workflow from a CRM like Salesforce. The workflow will send a thank you card via the Cardly API to the client or donor, adding a personal touch to your professional relationships. + +- **Feedback Follow-ups**: Post-event or after a purchase, trigger a workflow using a webhook when a customer provides feedback. Use the Cardly API to send a personalized appreciation card or a discount for their next purchase, fostering customer loyalty and engagement. diff --git a/components/carmd/README.md b/components/carmd/README.md new file mode 100644 index 0000000000000..583f0de5603df --- /dev/null +++ b/components/carmd/README.md @@ -0,0 +1,11 @@ +# Overview + +The CarMD API taps into a vast database of car diagnostics and maintenance information. With it, you can fetch detailed reports on a vehicle's health, decode check engine lights, predict upcoming maintenance issues, and get fair estimates for repair costs. By integrating CarMD with Pipedream, you can automate workflows for vehicle management, create alerts for vehicle diagnostics, or build apps that help users maintain their cars better. + +# Example Use Cases + +- **Vehicle Health Monitoring System**: Trigger a Pipedream workflow whenever a fleet vehicle completes a trip, using the CarMD API to conduct a quick diagnostic check. If any issues are detected, the workflow can automatically notify the fleet manager or schedule a maintenance appointment with a connected service like Google Calendar. + +- **Automated Cost Estimation for Repairs**: Create a workflow that listens for new customer inquiries via a form on your website (using a service like Typeform). Use the CarMD API to estimate repair costs based on the vehicle's information provided by the customer. Then, email the estimate to the customer and add a new deal to a CRM like Salesforce. + +- **Maintenance Alert System**: Set up a Pipedream workflow that checks the health of registered vehicles at regular intervals using the CarMD API. When it's time for routine maintenance or if an issue is found, send a reminder to the vehicle owner via SMS using Twilio, and log the alert in a data store for record-keeping. diff --git a/components/carmd/carmd.app.mjs b/components/carmd/carmd.app.mjs new file mode 100644 index 0000000000000..c2872eb233d7d --- /dev/null +++ b/components/carmd/carmd.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "carmd", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/carmd/package.json b/components/carmd/package.json new file mode 100644 index 0000000000000..f82ca0cb04d87 --- /dev/null +++ b/components/carmd/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/carmd", + "version": "0.0.1", + "description": "Pipedream CarMD Components", + "main": "carmd.app.mjs", + "keywords": [ + "pipedream", + "carmd" + ], + "homepage": "https://pipedream.com/apps/carmd", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/cartes/README.md b/components/cartes/README.md index b4ffb35e93eef..4ca99ad4f85ef 100644 --- a/components/cartes/README.md +++ b/components/cartes/README.md @@ -1,9 +1,11 @@ # Overview -With the Cartes API, you can build a variety of things, including: +Cartes API allows users to create, update, and share maps with custom markers, making it useful for location-based data visualization. With Pipedream, you can automate interactions with the Cartes API to integrate real-time mapping capabilities into your workflows. For instance, you could track assets, manage event locations, or update a map with user-generated content from various sources. Pipedream's power lies in its ability to connect the Cartes API with hundreds of other apps to create dynamic, automated workflows. -- Mapping and location-based applications -- Tracking and logistics applications -- Applications that need to geocode data -- Apps that work with real-time location data -- And more! +# Example Use Cases + +- **Real-Time Event Mapping**: Automatically update a Cartes map with event locations as they are scheduled in a Google Calendar. Each time a new event is added, a Pipedream workflow triggers and places a marker on the map, providing a visual overview of event hotspots. + +- **Customer Feedback Visualization**: When a customer submits feedback through a Typeform survey indicating a location-based issue, a Pipedream workflow adds a marker to a Cartes map. This aids businesses in pinpointing areas with high volumes of customer concerns for targeted service improvements. + +- **Asset Tracking System**: Combine Cartes with GPS tracking hardware data. As assets move and report their coordinates through a webhook, Pipedream processes the data and updates their positions on a Cartes map in near real-time, useful for fleet management or logistics. diff --git a/components/cascade_strategy/README.md b/components/cascade_strategy/README.md new file mode 100644 index 0000000000000..9cd5c5225d38b --- /dev/null +++ b/components/cascade_strategy/README.md @@ -0,0 +1,11 @@ +# Overview + +The Cascade Strategy API unlocks the potential to automate and integrate strategic planning processes directly within Pipedream. By accessing Cascade’s features through the API, you can seamlessly connect strategic goals, track progress, manage tasks, and align your organization’s efforts with its vision. This enables creating dynamic workflows that react to changes in strategy execution, report on key performance indicators, or synchronize organizational goals across multiple platforms. + +# Example Use Cases + +- **Automated Strategy Reporting**: Set up a workflow that triggers at regular intervals, pulling data from Cascade to create a comprehensive report of strategic goal progress, which is then sent to stakeholders via email or a messaging app like Slack. + +- **Task Synchronization with Project Management Tools**: Whenever a new task is created or updated in Cascade, trigger a workflow to create or update corresponding tasks in a project management tool like Trello or Asana, ensuring cross-platform alignment of strategic tasks. + +- **Real-time Alerts for Goal Achievement**: Build a workflow that monitors specific strategic goals within Cascade, and when a goal is achieved or its status changes, send real-time notifications to a team’s communication platform like Microsoft Teams, celebrating the wins and keeping everyone informed. diff --git a/components/caspio/README.md b/components/caspio/README.md new file mode 100644 index 0000000000000..4f92c8d4b986e --- /dev/null +++ b/components/caspio/README.md @@ -0,0 +1,11 @@ +# Overview + +The Caspio API provides a way to programmatically interact with your Caspio applications, making data management and integration tasks automated and efficient. With Pipedream, you can harness this API to build custom workflows that trigger actions in Caspio or use Caspio data to kick off processes in other services. From syncing data with external databases to processing form submissions and automating notifications, Pipedream makes it easy to connect Caspio with other apps for a seamless data flow. + +# Example Use Cases + +- **Data Sync Between Caspio and Google Sheets**: Automatically sync data from a Caspio table to a Google Sheet for reporting and analysis. Whenever a new record is added or updated in Caspio, a Pipedream workflow would detect the change and append or update the corresponding row in Google Sheets. + +- **Automated Email Notifications on New Caspio Records**: Set up a workflow where new entries to a Caspio form trigger an email notification. Pipedream listens for new records and uses an email service like SendGrid or the built-in email action to send a customized email to the appropriate recipient. + +- **Slack Alerts for Caspio Data Changes**: Keep your team updated with Slack messages when specific data changes in Caspio. A Pipedream workflow can monitor modifications to a Caspio table, then filter and format the data, sending a Slack message to a channel or DM when important updates occur. diff --git a/components/caspio/package.json b/components/caspio/package.json index 57fba78cba4a0..54097ee931087 100644 --- a/components/caspio/package.json +++ b/components/caspio/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/castingwords/README.md b/components/castingwords/README.md index 0246ce97c7611..5192d94ad4b09 100644 --- a/components/castingwords/README.md +++ b/components/castingwords/README.md @@ -1,15 +1,11 @@ # Overview -The CastingWords API can be used to generate transcriptions of audio files. -This can be useful for a variety of applications, including: +The CastingWords API provides programmatic access to a professional transcription service, allowing you to upload audio or video files for transcription, check the status of jobs, and retrieve completed transcripts. This interface streamlines the process of converting spoken content into written text, which can be a boon for content creators, journalists, researchers, and anyone who needs to transform audiovisual media into a textual format. With Pipedream, you can automate interactions with the CastingWords API, integrating transcription tasks seamlessly into your workflows. -- Creating written transcripts of audio files -- Generating subtitles for videos -- Creating text-based summaries of audio files +# Example Use Cases -Some example applications that could be built using the CastingWords API -include: +- **Automated Podcast Transcription Workflow**: Trigger a Pipedream workflow with a new podcast episode from an RSS feed. The workflow uploads the audio file to CastingWords for transcription. Once the transcription is complete, it fetches the text and posts it to your blog or website CMS, enhancing podcast accessibility and SEO. -- A transcription service that generates written transcripts of audio files -- A subtitle generation tool that creates subtitles for videos -- A summary generation tool that creates text-based summaries of audio files +- **Transcription-Driven Content Syndication**: When a new video is uploaded to your YouTube channel, a Pipedream workflow initiates a transcription request to CastingWords. After the transcription, the workflow formats the text into a newsletter and distributes it via SendGrid or another email service, repurposing your video content and engaging your subscriber base. + +- **Customer Support Call Analysis**: Record customer support calls and use Pipedream to send them to CastingWords for transcription. The resulting text files are then analyzed for sentiment and keywords using a service like Google Natural Language API, providing insights into customer satisfaction and support effectiveness. diff --git a/components/castingwords/package.json b/components/castingwords/package.json new file mode 100644 index 0000000000000..8bb6ed6785519 --- /dev/null +++ b/components/castingwords/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/castingwords", + "version": "0.6.0", + "description": "Pipedream castingwords Components", + "main": "castingwords.app.mjs", + "keywords": [ + "pipedream", + "castingwords" + ], + "homepage": "https://pipedream.com/apps/castingwords", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/castmagic/README.md b/components/castmagic/README.md new file mode 100644 index 0000000000000..6a69ae9b48bfd --- /dev/null +++ b/components/castmagic/README.md @@ -0,0 +1,11 @@ +# Overview + +The Castmagic API allows you to control and automate the casting of media to various devices, such as Chromecast and smart TVs. It's designed for developers who want to build custom casting solutions without the hassle of dealing with low-level protocol details. Through Pipedream, you can integrate Castmagic with a myriad of services and triggers, creating workflows that respond to events like new content releases, user interactions, or scheduled routines. You can leverage Pipedream's serverless platform to create, run, and maintain these automations with ease. + +# Example Use Cases + +- **Automated Media Casting for New Content**: When a new video is uploaded to a cloud storage platform like Dropbox, trigger a Pipedream workflow that uses the Castmagic API to cast the video to a designated device, ensuring you never miss the latest uploads. + +- **Scheduled Morning Routine**: Set up a daily morning routine that casts the latest news, weather updates, and your favorite playlist to your TV and speakers as you start your day. This can be done by triggering a Pipedream workflow at a scheduled time, which then calls the Castmagic API to manage the media playback. + +- **Voice-Controlled Media Playback**: Integrate Castmagic with a voice assistant like Google Assistant through Pipedream. Use voice commands to trigger a workflow that instructs the Castmagic API to play, pause, or stop media content on your connected devices, bringing hands-free convenience to your media experience. diff --git a/components/cats/actions/add-candidate-pipeline/add-candidate-pipeline.mjs b/components/cats/actions/add-candidate-pipeline/add-candidate-pipeline.mjs new file mode 100644 index 0000000000000..92e7914068faf --- /dev/null +++ b/components/cats/actions/add-candidate-pipeline/add-candidate-pipeline.mjs @@ -0,0 +1,58 @@ +import cats from "../../cats.app.mjs"; + +export default { + key: "cats-add-candidate-pipeline", + name: "Add Candidate to Job Pipeline", + description: "Adds a specific candidate to a job pipeline in CATS. [See the documentation](https://docs.catsone.com/api/v3/#jobs-create-a-job)", + version: "0.0.1", + type: "action", + props: { + cats, + createActivity: { + type: "boolean", + label: "Create Activity", + description: "Whether a corresponding activity should be created automatically. This mimics what happens when a pipeline is created from the CATS UI.", + optional: true, + }, + candidateId: { + propDefinition: [ + cats, + "candidateId", + ], + }, + jobId: { + propDefinition: [ + cats, + "jobId", + ], + }, + rating: { + type: "integer", + label: "Rating", + description: "The record's rating for the job (0-5).", + optional: true, + }, + }, + async run({ $ }) { + const { headers } = await this.cats.addCandidateToJobPipeline({ + $, + returnFullResponse: true, + params: { + create_activity: this.createActivity, + }, + data: { + candidate_id: this.candidateId, + job_id: this.jobId, + rating: this.rating, + }, + }); + + const location = headers.location.split("/"); + const pipelineId = location[location.length - 1]; + + $.export("$summary", `Successfully added candidate ID ${this.candidateId} to job ID ${this.jobId}`); + return { + pipelineId, + }; + }, +}; diff --git a/components/cats/actions/create-candidate/create-candidate.mjs b/components/cats/actions/create-candidate/create-candidate.mjs new file mode 100644 index 0000000000000..6dd807516ebfe --- /dev/null +++ b/components/cats/actions/create-candidate/create-candidate.mjs @@ -0,0 +1,308 @@ +import cats from "../../cats.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "cats-create-candidate", + name: "Create Candidate", + description: "Create a new candidate in your CATS database. [See the documentation](https://docs.catsone.com/api/v3/#candidates-create-a-candidate)", + version: "0.0.1", + type: "action", + props: { + cats, + checkDuplicate: { + propDefinition: [ + cats, + "checkDuplicate", + ], + }, + firstName: { + propDefinition: [ + cats, + "firstName", + ], + }, + middleName: { + propDefinition: [ + cats, + "middleName", + ], + optional: true, + }, + lastName: { + propDefinition: [ + cats, + "lastName", + ], + }, + title: { + propDefinition: [ + cats, + "title", + ], + optional: true, + }, + emails: { + propDefinition: [ + cats, + "emails", + ], + optional: true, + }, + phones: { + propDefinition: [ + cats, + "phones", + ], + optional: true, + }, + addressStreet: { + propDefinition: [ + cats, + "addressStreet", + ], + optional: true, + }, + addressCity: { + propDefinition: [ + cats, + "addressCity", + ], + optional: true, + }, + addressState: { + propDefinition: [ + cats, + "addressState", + ], + optional: true, + }, + addressPostalCode: { + propDefinition: [ + cats, + "addressPostalCode", + ], + optional: true, + }, + countryCode: { + propDefinition: [ + cats, + "countryCode", + ], + optional: true, + }, + socialMediaUrls: { + propDefinition: [ + cats, + "socialMediaUrls", + ], + optional: true, + }, + website: { + type: "string", + label: "Website", + description: "The website of the record.", + optional: true, + }, + bestTimeToCall: { + propDefinition: [ + cats, + "bestTimeToCall", + ], + optional: true, + }, + currentEmployer: { + propDefinition: [ + cats, + "currentEmployer", + ], + optional: true, + }, + dateAvailable: { + type: "string", + label: "Date Available", + description: "The date the record is available for an opening. **Format: YYYY-MM-DD**.", + optional: true, + }, + currentPay: { + type: "string", + label: "Current Pay", + description: "The current pay of the record.", + optional: true, + }, + desiredPay: { + propDefinition: [ + cats, + "desiredPay", + ], + optional: true, + }, + isWillingToRelocate: { + propDefinition: [ + cats, + "isWillingToRelocate", + ], + optional: true, + }, + keySkills: { + propDefinition: [ + cats, + "keySkills", + ], + optional: true, + }, + notes: { + propDefinition: [ + cats, + "notes", + ], + optional: true, + }, + source: { + propDefinition: [ + cats, + "source", + ], + optional: true, + }, + ownerId: { + propDefinition: [ + cats, + "ownerId", + ], + optional: true, + }, + isActive: { + type: "boolean", + label: "Is Active", + description: "A flag indicating if the candidate is active.", + optional: true, + }, + isHot: { + propDefinition: [ + cats, + "isHot", + ], + optional: true, + }, + password: { + type: "string", + label: "password", + description: "The candidate's password if they are \"registering\".", + secret: true, + optional: true, + }, + customFields: { + propDefinition: [ + cats, + "customFields", + ], + withLabel: true, + reloadProps: true, + optional: true, + }, + workHistory: { + propDefinition: [ + cats, + "workHistory", + ], + optional: true, + }, + }, + async additionalProps() { + const props = {}; + (this.customFields || []).map(({ + label, value, + }) => { + props[value] = { + type: "string", + label: `Custom Field: ${label}`, + optional: true, + }; + }, {}); + + return props; + }, + async run({ $ }) { + const { + cats, // eslint-disable-next-line no-unused-vars + customFields, + firstName, + lastName, + ownerId, + middleName, + checkDuplicate, + bestTimeToCall, + currentEmployer, + emails, + phones, + addressStreet, + addressCity, + addressState, + addressPostalCode, + countryCode, + socialMediaUrls, + dateAvailable, + currentPay, + desiredPay, + isWillingToRelocate, + keySkills, + isActive, + isHot, + workHistory, + ...data + } = this; + + const customFieldsObject = customFields + ? customFields.map(({ value }) => { + return { + id: value, + value: data[value], + }; + }) + : {}; + + const { headers } = await cats.createCandidate({ + $, + returnFullResponse: true, + params: { + check_duplicate: checkDuplicate, + }, + data: { + first_name: firstName, + middle_name: middleName, + last_name: lastName, + emails: parseObject(emails), + phones: parseObject(phones), + address: { + street: addressStreet, + city: addressCity, + state: addressState, + postal_code: addressPostalCode, + }, + country_code: countryCode, + social_media_urls: parseObject(socialMediaUrls), + best_time_to_call: bestTimeToCall, + current_employer: currentEmployer, + date_available: dateAvailable, + current_pay: currentPay, + desired_pay: desiredPay, + is_willing_to_relocate: isWillingToRelocate, + key_skills: keySkills, + owner_id: ownerId, + is_active: isActive, + is_hot: isHot, + work_history: parseObject(workHistory), + custom_fields: customFieldsObject, + ...data, + }, + }); + + const location = headers.location.split("/"); + const candidateId = location[location.length - 1]; + + $.export("$summary", `Created candidate with ID ${candidateId}`); + return { + candidateId, + }; + }, +}; diff --git a/components/cats/actions/create-contact/create-contact.mjs b/components/cats/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..e1ffe64da8b38 --- /dev/null +++ b/components/cats/actions/create-contact/create-contact.mjs @@ -0,0 +1,226 @@ +import cats from "../../cats.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "cats-create-contact", + name: "Create Contact", + description: "Adds a new contact to the CATS platform. [See the documentation](https://docs.catsone.com/api/v3/#contacts-create-a-contact)", + version: "0.0.1", + type: "action", + props: { + cats, + firstName: { + propDefinition: [ + cats, + "firstName", + ], + }, + lastName: { + propDefinition: [ + cats, + "lastName", + ], + }, + ownerId: { + propDefinition: [ + cats, + "ownerId", + ], + }, + companyId: { + propDefinition: [ + cats, + "companyId", + ], + }, + checkDuplicate: { + propDefinition: [ + cats, + "checkDuplicate", + ], + optional: true, + }, + title: { + propDefinition: [ + cats, + "title", + ], + optional: true, + }, + reportsToId: { + propDefinition: [ + cats, + "contactId", + ], + label: "Reports To Id", + description: "The ID of the contact that this contact reports to.", + optional: true, + }, + hasLeftCompany: { + type: "boolean", + label: "Has Left Company", + description: "Whether the contact has left the company or not.", + optional: true, + }, + emails: { + propDefinition: [ + cats, + "emails", + ], + optional: true, + }, + phones: { + propDefinition: [ + cats, + "phones", + ], + optional: true, + }, + addressStreet: { + propDefinition: [ + cats, + "addressStreet", + ], + optional: true, + }, + addressCity: { + propDefinition: [ + cats, + "addressCity", + ], + optional: true, + }, + addressState: { + propDefinition: [ + cats, + "addressState", + ], + optional: true, + }, + addressPostalCode: { + propDefinition: [ + cats, + "addressPostalCode", + ], + optional: true, + }, + countryCode: { + propDefinition: [ + cats, + "countryCode", + ], + optional: true, + }, + socialMediaUrls: { + propDefinition: [ + cats, + "socialMediaUrls", + ], + optional: true, + }, + isHot: { + propDefinition: [ + cats, + "isHot", + ], + optional: true, + }, + notes: { + propDefinition: [ + cats, + "notes", + ], + optional: true, + }, + customFields: { + propDefinition: [ + cats, + "customFields", + ], + withLabel: true, + reloadProps: true, + optional: true, + }, + }, + async additionalProps() { + const props = {}; + (this.customFields || []).map(({ + label, value, + }) => { + props[value] = { + type: "string", + label: `Custom Field: ${label}`, + optional: true, + }; + }, {}); + + return props; + }, + async run({ $ }) { + const { + cats, // eslint-disable-next-line no-unused-vars + customFields, + firstName, + lastName, + ownerId, + companyId, + checkDuplicate, + reportsToId, + hasLeftCompany, + emails, + phones, + addressStreet, + addressCity, + addressState, + addressPostalCode, + countryCode, + socialMediaUrls, + ...data + } = this; + + const customFieldsObject = customFields + ? customFields.map(({ value }) => { + return { + id: value, + value: data[value], + }; + }) + : {}; + + const { headers } = await cats.createContact({ + $, + returnFullResponse: true, + params: { + check_duplicate: checkDuplicate, + }, + data: { + first_name: firstName, + last_name: lastName, + owner_id: ownerId, + company_id: companyId, + reports_to_id: reportsToId, + has_left_company: hasLeftCompany, + emails: parseObject(emails), + phones: parseObject(phones), + address: { + street: addressStreet, + city: addressCity, + state: addressState, + postal_code: addressPostalCode, + }, + country_code: countryCode, + social_media_urls: parseObject(socialMediaUrls), + custom_fields: customFieldsObject, + ...data, + }, + }); + + const location = headers.location.split("/"); + const contactId = location[location.length - 1]; + + $.export("$summary", `Contact successfully created with Id: ${contactId}!`); + return { + contactId, + }; + }, +}; diff --git a/components/cats/cats.app.mjs b/components/cats/cats.app.mjs new file mode 100644 index 0000000000000..74694a4ba14ba --- /dev/null +++ b/components/cats/cats.app.mjs @@ -0,0 +1,342 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "cats", + propDefinitions: { + candidateId: { + type: "integer", + label: "Candidate ID", + description: "The ID of the candidate.", + async options({ page }) { + const { _embedded } = await this.listCandidates({ + params: { + page: page + 1, + }, + }); + + return _embedded + ? _embedded.candidates.map(({ + id, first_name: fName, last_name: lName, emails: { + primary, secondary, + }, + }) => ({ + label: `${fName} ${lName} ${primary || secondary + ? `(${primary || secondary})` + : ""}`, + value: parseInt(id), + })) + : []; + }, + }, + companyId: { + type: "integer", + label: "Company ID", + description: "The company ID related to the contact.", + async options({ page }) { + const { _embedded } = await this.listCompanies({ + params: { + page: page + 1, + }, + }); + + return _embedded + ? _embedded.companies.map(({ + id: value, name: label, + }) => ({ + label, + value, + })) + : []; + }, + }, + contactId: { + type: "integer", + label: "Contact Id", + description: "The ID of the contact.", + async options({ page }) { + const { _embedded } = await this.listContacts({ + params: { + page: page + 1, + }, + }); + + return _embedded + ? _embedded.contacts.map(({ + id, first_name: fName, last_name: lName, emails: { primary }, + }) => ({ + label: `${fName} ${lName} ${primary + ? `(${primary})` + : ""}`, + value: parseInt(id), + })) + : []; + }, + }, + ownerId: { + type: "integer", + label: "Owner ID", + description: "The user id of the record owner.", + async options({ page }) { + const { _embedded } = await this.listUsers({ + params: { + page: page + 1, + }, + }); + + return _embedded + ? _embedded.users.map(({ + id: value, username: label, + }) => ({ + label, + value, + })) + : []; + }, + }, + customFields: { + type: "string[]", + label: "Custom Fields", + description: "An array of custom field objects. ", + async options({ page }) { + const { _embedded } = await this.listCustomFields({ + params: { + page: page + 1, + }, + }); + + return _embedded + ? _embedded.custom_fields.map(({ + id: value, name: label, + }) => ({ + label, + value, + })) + : []; + }, + }, + jobId: { + type: "integer", + label: "Job ID", + description: "The ID of the job to which the record is added.", + async options({ page }) { + const { _embedded } = await this.listJobs({ + params: { + page: page + 1, + }, + }); + + return _embedded + ? _embedded.jobs.map(({ + id: value, title: label, + }) => ({ + label, + value, + })) + : []; + }, + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the record.", + }, + middleName: { + type: "string", + label: "Middle Name", + description: "The middle name of the record.", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the record.", + }, + checkDuplicate: { + type: "boolean", + label: "Check Duplicate", + description: "When this flag is set to true, if a duplicate record is found to the one being created, an error will be thrown instead of creating a duplicate record.", + default: false, + }, + title: { + type: "string", + label: "Title", + description: "The record's job title.", + }, + emails: { + type: "string[]", + label: "Emails", + description: "An array of email objects. Each email object should contain two keys: `email` and `is_primary`, as described [here](https://docs.catsone.com/api/v3/#contacts-create-a-contact-email). **Format: {\"email\":\"example@email.com\", \"is_primary\":\"true\"}**", + }, + isHot: { + type: "boolean", + label: "Is Hot", + description: "Whether the record is marked as hot.", + }, + phones: { + type: "string[]", + label: "Phones", + description: "An array of phone objects. Each phone object should contain three keys: `number`, `extension`, and `type`, as described [here](https://docs.catsone.com/api/v3/#contacts-create-a-contact-phone). **Format: {\"number\":\"+16124063451\", \"extension\":\"8371\", \"type\":\"mobile\"}**. Type can be `mobile`, `home`, `work`, `fax`, `main` or `other`", + }, + addressStreet: { + type: "string", + label: "Street Address", + description: "The street of the record's address.", + }, + addressCity: { + type: "string", + label: "City Address", + description: "The city of the record's address.", + }, + addressState: { + type: "string", + label: "State Address", + description: "The state of the record's address.", + }, + addressPostalCode: { + type: "string", + label: "Address Postal Code", + description: "The postal code of the record's address.", + }, + countryCode: { + type: "string", + label: "Country Code", + description: "The country code of the record.", + }, + socialMediaUrls: { + type: "string[]", + label: "Social Media URLs", + description: "The social media URLs of the record.", + }, + notes: { + type: "string", + label: "Notes", + description: "Any notes about the record.", + }, + bestTimeToCall: { + type: "string", + label: "Best Time to Call", + description: "The best time to call the record. **Format: HH:MM**.", + }, + currentEmployer: { + type: "string", + label: "Current Employer", + description: "The current employer of the record.", + }, + desiredPay: { + type: "string", + label: "Desired Pay", + description: "The desired pay for the record.", + }, + isWillingToRelocate: { + type: "boolean", + label: "Willing to Relocate", + description: "Whether the record is willing to relocate.", + }, + keySkills: { + type: "string", + label: "Key Skills", + description: "The key skills of the record.", + }, + source: { + type: "string", + label: "Source", + description: "The source of the record.", + }, + workHistory: { + type: "string[]", + label: "Work History", + description: "An array of work history objects. **Example: {\"title\": \"Engineer\", \"employer\": { \"linked\": false, \"name\": \"\", \"location\": { \"city\": \"\", \"state\": \"\" }}, \"supervisor\": { \"linked\": false, \"name\": \"\", \"phone\": \"\" }, \"is_verified\": true, \"is_current\": false, \"start_date\": \"YYYY-MM-DD\", \"end_date\": \"YYYY-MM-DD\", \"reason_for_leaving\": \"foo\"}**", + }, + }, + methods: { + _baseUrl() { + return "https://api.catsone.com/v3"; + }, + _headers() { + return { + Authorization: `Token ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + const config = { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }; + console.log("config: ", config); + return axios($, config); + }, + listCandidates(opts = {}) { + return this._makeRequest({ + path: "/candidates", + ...opts, + }); + }, + listCompanies(opts = {}) { + return this._makeRequest({ + path: "/companies", + ...opts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts", + ...opts, + }); + }, + listCustomFields(opts = {}) { + return this._makeRequest({ + path: "/contacts/custom_fields", + ...opts, + }); + }, + listJobs(opts = {}) { + return this._makeRequest({ + path: "/jobs", + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users", + ...opts, + }); + }, + createCandidate(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/candidates", + ...opts, + }); + }, + addCandidateToJobPipeline(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/pipelines", + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contacts", + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + }); + }, + }, +}; diff --git a/components/cats/common/utils.mjs b/components/cats/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/cats/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/cats/package.json b/components/cats/package.json new file mode 100644 index 0000000000000..52701eb2cf6a7 --- /dev/null +++ b/components/cats/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/cats", + "version": "0.1.0", + "description": "Pipedream CATS Components", + "main": "cats.app.mjs", + "keywords": [ + "pipedream", + "cats" + ], + "homepage": "https://pipedream.com/apps/cats", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "crypto": "^1.0.1" + } +} diff --git a/components/cats/sources/common/base.mjs b/components/cats/sources/common/base.mjs new file mode 100644 index 0000000000000..76c33f18ae210 --- /dev/null +++ b/components/cats/sources/common/base.mjs @@ -0,0 +1,80 @@ +import { + createHmac, randomUUID, +} from "crypto"; +import cats from "../../cats.app.mjs"; + +export default { + props: { + cats, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + }, + methods: { + _getUUID() { + return this.db.get("UUID"); + }, + _setUUID(UUID) { + this.db.set("UUID", UUID); + }, + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getExtraData() { + return {}; + }, + checkSignature({ + bodyRaw, headers, + }) { + const uuid = this._getUUID(); + const hash = createHmac("sha256", uuid).update(`${bodyRaw}${headers["x-request-id"]}`) + .digest() + .toString("hex"); + + console.log("hash: ", hash); + + return headers["x-signature"] === `HMAC-SHA256 ${hash}`; + }, + }, + hooks: { + async activate() { + const uuid = randomUUID(); + const { headers } = await this.cats.createWebhook({ + returnFullResponse: true, + data: { + target_url: this.http.endpoint, + events: this.getEventType(), + secret: uuid, + }, + }); + + const location = headers.location.split("/"); + const webhookId = location[location.length - 1]; + + this._setUUID(uuid); + this._setHookId(webhookId); + }, + async deactivate() { + const webhookId = this._getHookId(); + await this.cats.deleteWebhook(webhookId); + }, + }, + async run({ + body, ...event + }) { + if (!this.checkSignature(event)) { + return this.http.respond({ + status: 400, + }); + } + + console.log("this.generateMeta(body): ", this.generateMeta(body)); + + this.$emit(body, this.generateMeta(body)); + }, +}; diff --git a/components/cats/sources/new-activity-instant/new-activity-instant.mjs b/components/cats/sources/new-activity-instant/new-activity-instant.mjs new file mode 100644 index 0000000000000..0b4d4d010dd5a --- /dev/null +++ b/components/cats/sources/new-activity-instant/new-activity-instant.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "cats-new-activity-instant", + name: "New Activity (Instant)", + description: "Emit new event when an activity related to a cat is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return [ + "activity.created", + ]; + }, + generateMeta(body) { + return { + id: body.activity_id, + summary: `New activity: ${body.activity_id}`, + ts: Date.parse(body.date || new Date()), + }; + }, + }, + sampleEmit, +}; diff --git a/components/cats/sources/new-activity-instant/test-event.mjs b/components/cats/sources/new-activity-instant/test-event.mjs new file mode 100644 index 0000000000000..f1fe0026dd622 --- /dev/null +++ b/components/cats/sources/new-activity-instant/test-event.mjs @@ -0,0 +1,41 @@ +export default { + "event": "activity.created", + "activity_id": 123456789, + "date": "2024-11-19T19:57:54+00:00", + "_links": { + "activity": { + "href": "/activities/123456789" + } + }, + "_embedded": { + "activity": { + "id": 123456789, + "data_item": { + "id": 123456789, + "type": "candidate" + }, + "date": "2024-11-19T19:57:54+00:00", + "regarding_id": 123456789, + "type": "other", + "notes": null, + "annotation": "Added candidate to pipeline: No Contact", + "entered_by_id": 123456789, + "date_created": "2024-11-19T19:57:54+00:00", + "date_modified": "2024-11-19T19:57:54+00:00", + "_links": { + "self": { + "href": "/activities/123456789" + }, + "regarding": { + "href": "/jobs/123456789" + }, + "data_item": { + "href": "/candidates/123456789" + }, + "entered_by": { + "href": "/users/123456789" + } + } + } + } +} \ No newline at end of file diff --git a/components/cats/sources/new-candidate-instant/new-candidate-instant.mjs b/components/cats/sources/new-candidate-instant/new-candidate-instant.mjs new file mode 100644 index 0000000000000..5a2418396bbe0 --- /dev/null +++ b/components/cats/sources/new-candidate-instant/new-candidate-instant.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "cats-new-candidate-instant", + name: "New Candidate (Instant)", + description: "Emit new event when a new candidate is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return [ + "candidate.created", + ]; + }, + generateMeta(body) { + const candidate = body._embedded.candidate; + return { + id: body.candidate_id, + summary: `New candidate created: ${candidate.first_name} ${candidate.last_name} (${candidate.emails.primary || candidate.emails.second})`, + ts: Date.parse(body.date || new Date()), + }; + }, + }, + sampleEmit, +}; diff --git a/components/cats/sources/new-candidate-instant/test-event.mjs b/components/cats/sources/new-candidate-instant/test-event.mjs new file mode 100644 index 0000000000000..34e1779782171 --- /dev/null +++ b/components/cats/sources/new-candidate-instant/test-event.mjs @@ -0,0 +1,112 @@ +export default { + "event": "candidate.created", + "candidate_id": 123456789, + "date": "2024-11-19T20:13:12+00:00", + "_links": { + "candidate": { + "href": "/candidates/123456789" + } + }, + "_embedded": { + "candidate": { + "id": 123456789, + "first_name": "Candidate Name", + "middle_name": "Middle Name", + "last_name": "Last Name", + "title": "Candidate Tittle", + "emails": { + "primary": "candidate@email.com", + "secondary": null + }, + "address": { + "street": "street", + "city": "city", + "state": "CA", + "postal_code": "18234" + }, + "country_code": "US", + "social_media_urls": [], + "website": "https://website.com", + "phones": { + "home": null, + "cell": "1234567890", + "work": null + }, + "best_time_to_call": "14:00", + "current_employer": "CurrentEmployer", + "date_available": "2024-12-12", + "current_pay": "400", + "desired_pay": "8000", + "is_willing_to_relocate": true, + "key_skills": "", + "notes": "", + "is_hot": true, + "is_active": true, + "contact_id": null, + "owner_id": 123456, + "entered_by_id": 123456, + "source": "", + "is_registered": true, + "consent_status": null, + "date_created": "2024-11-19T20:13:12+00:00", + "date_modified": "2024-11-19T20:13:12+00:00", + "_links": { + "self": { + "href": "/candidates/123456789" + }, + "custom_fields": { + "href": "/candidates/123456789/custom_fields" + }, + "attachments": { + "href": "/candidates/123456789/attachments" + }, + "activities": { + "href": "/candidates/123456789/activities" + }, + "work_history": { + "href": "/candidates/123456789/work_history" + }, + "pipelines": { + "href": "/candidates/123456789/pipelines" + }, + "tags": { + "href": "/candidates/123456789/tags" + }, + "thumbnail": { + "href": "/candidates/123456789/thumbnail" + }, + "phones": { + "href": "/candidates/123456789/phones" + }, + "emails": { + "href": "/candidates/123456789/emails" + }, + "owner": { + "href": "/users/123456" + }, + "entered_by": { + "href": "/users/123456" + } + }, + "_embedded": { + "custom_fields": [], + "work_history": [], + "thumbnail": [ + { + "id": 92680, + "source": "gravatar", + "attachment_id": null, + "url": "https://pipedream.catsone.com/candidates/123456789/thumbnail?_s=e1221ae117fecea3c20ec9075dfb36ac05e876d1be10725e27b4eeb42289bd65", + "_links": { + "self": { + "href": "/candidates/123456789/thumbnail" + } + } + } + ], + "phones": [], + "emails": [] + } + } + } +} \ No newline at end of file diff --git a/components/cats/sources/new-contact-instant/new-contact-instant.mjs b/components/cats/sources/new-contact-instant/new-contact-instant.mjs new file mode 100644 index 0000000000000..a3ae27151b3de --- /dev/null +++ b/components/cats/sources/new-contact-instant/new-contact-instant.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "cats-new-contact-instant", + name: "New Contact Created (Instant)", + description: "Emit new event when a contact related to a cat is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return [ + "contact.created", + ]; + }, + generateMeta(body) { + const contact = body._embedded.contact; + return { + id: body.contact_id, + summary: `New contact: ${contact.first_name} ${contact.last_name}`, + ts: Date.parse(body.date || new Date()), + }; + }, + }, + sampleEmit, +}; diff --git a/components/cats/sources/new-contact-instant/test-event.mjs b/components/cats/sources/new-contact-instant/test-event.mjs new file mode 100644 index 0000000000000..3fd9ac647f591 --- /dev/null +++ b/components/cats/sources/new-contact-instant/test-event.mjs @@ -0,0 +1,103 @@ +export default { + "event": "contact.created", + "contact_id": 123456789, + "date": "2024-11-19T20:21:10+00:00", + "_links": { + "contact": { + "href": "/contacts/123456789" + } + }, + "_embedded": { + "contact": { + "id": "123456789", + "first_name": "Contact Name", + "last_name": "Last Name", + "title": "Contact title", + "reports_to_id": 1234567, + "owner_id": 123456, + "company_id": 22978541, + "emails": { + "primary": "contact@email.com", + "secondary": null + }, + "phones": { + "work": null, + "cell": "12345678", + "other": null + }, + "address": { + "street": "street", + "city": "city", + "state": "CA", + "postal_code": "92132" + }, + "country_code": "US", + "social_media_urls": [], + "is_hot": false, + "has_left_company": false, + "notes": "", + "entered_by_id": 123456, + "consent_status": null, + "date_created": "2024-11-19T20:21:10+00:00", + "date_modified": "2024-11-19T20:21:10+00:00", + "status_id": 123456, + "_links": { + "self": { + "href": "/contacts/123456789" + }, + "reports_to": { + "href": "/users/1234567" + }, + "custom_fields": { + "href": "/contacts/123456789/custom_fields" + }, + "activities": { + "href": "/contacts/123456789/activities" + }, + "status": { + "href": "/contacts/statuses/123456" + }, + "entered_by": { + "href": "/users/123456" + }, + "owner": { + "href": "/users/123456" + }, + "attachments": { + "href": "/contacts/123456789/attachments" + }, + "tags": { + "href": "/contacts/123456789/tags" + }, + "thumbnail": { + "href": "/contacts/123456789/thumbnail" + }, + "phones": { + "href": "/contacts/123456789/phones" + }, + "emails": { + "href": "/contacts/123456789/emails" + } + }, + "_embedded": { + "custom_fields": [], + "status": { + "id": 123456, + "workflow_id": 5696671, + "title": "No Status", + "mapping": "", + "prerequisites": [], + "triggers": [], + "_links": { + "self": { + "href": "contacts/statuses/123456" + } + } + }, + "thumbnail": [], + "phones": [], + "emails": [] + } + } + } +} \ No newline at end of file diff --git a/components/cdc_national_environmental_public_health_tracking/README.md b/components/cdc_national_environmental_public_health_tracking/README.md index 5ef0e76d307aa..feeca570f1443 100644 --- a/components/cdc_national_environmental_public_health_tracking/README.md +++ b/components/cdc_national_environmental_public_health_tracking/README.md @@ -1,13 +1,11 @@ # Overview -The CDC - National Environmental Public Health Tracking API can be used to -build applications that track environmental and public health data. This data -can be used to track the health of communities, monitor environmental health -threats, and more. +The CDC - National Environmental Public Health Tracking API provides access to a treasure trove of environmental and public health data. Harness this API to monitor trends, perform analyses, and spark proactive interventions. It's a goldmine for public health officials, researchers, and policy makers, offering data on air quality, water contaminants, climate change, and disease statistics. Leverage Pipedream's platform to automate data retrieval, gain insights, and connect with other services for a seamless data-driven decision-making process. -Some examples of what you can build using the CDC - National Environmental -Public Health Tracking API include: +# Example Use Cases -- An application that tracks environmental health data for a community -- An application that monitors environmental health threats in a community -- An application that tracks the health of a community over time +- **Automated Health Alerts System**: Use the API to monitor environmental health hazard data like air pollution levels. When certain thresholds are exceeded, trigger an automated alert through services like Twilio or SendGrid to inform local authorities and public health officials, enabling swift action to protect communities. + +- **Environmental Data Dashboards**: Fetch environmental data periodically and pipe it into data visualization tools such as Google Sheets or Tableau. Create live dashboards that policymakers and the public can use to understand current environmental conditions and long-term trends, supporting informed decision-making and awareness. + +- **Chronic Disease Correlation Analysis**: Combine CDC environmental data with health data from other APIs to analyze correlations between environmental conditions and chronic disease prevalence. Schedule regular data retrieval and use a tool like Python or R within Pipedream to conduct statistical analyses, which can then inform public health strategies. diff --git a/components/cdr_platform/README.md b/components/cdr_platform/README.md new file mode 100644 index 0000000000000..2f1219a0f0748 --- /dev/null +++ b/components/cdr_platform/README.md @@ -0,0 +1,11 @@ +# Overview + +The CDR Platform API provides tools for automating and integrating processes related to cyber defense and threat intelligence. With this API, you can access and manage threat data, analyze security incidents, and enhance your cybersecurity measures. Pipedream serves as a powerful platform to create workflows leveraging the CDR Platform API, enabling you to connect with numerous other services, orchestrate complex automations, and react to events in real-time. + +# Example Use Cases + +- **Automated Threat Intelligence Gathering**: Trigger a Pipedream workflow with a scheduled timer to fetch the latest threat intelligence from the CDR Platform. Process this data and pipe it into a Slack channel, alerting your team to new threats immediately. + +- **Incident Response Coordination**: Use a webhook to start a Pipedream workflow when a new incident is reported to the CDR Platform. The workflow could then create a ticket in Jira for your response team, ensuring swift action and tracking. + +- **Security Dashboard Updates**: Combine the CDR Platform API with a data visualization tool like Google Sheets. Set up a Pipedream workflow to periodically pull the latest security analysis and metrics, then update a Google Sheet that powers a live security dashboard. diff --git a/components/celonis_ems/README.md b/components/celonis_ems/README.md new file mode 100644 index 0000000000000..f7753ceef68c4 --- /dev/null +++ b/components/celonis_ems/README.md @@ -0,0 +1,11 @@ +# Overview + +The Celonis EMS API allows you to harness the power of process mining and execution management within your workflows. Integrated within Pipedream, this API enables you to automate actions based on process insights, such as identifying bottlenecks and initiating corrective measures. You can trigger workflows from Celonis data, send data back into Celonis for deeper analysis, or even mix data from different sources for rich, actionable insights. + +# Example Use Cases + +- **Automate Ticket Creation in Jira based on Process Findings**: When Celonis identifies a deviation or inefficiency in a process, like longer lead times in procurement, a Pipedream workflow can automatically create a ticket in Jira to address the issue, ensuring timely resolution and continuous process optimization. + +- **Sync Process Analytics with Google Sheets**: Regularly export key process metrics from Celonis to a Google Sheet for reporting and analysis. This could include data like process cycle times or compliance rates, allowing for a more accessible view for stakeholders who prefer quick looks over spreadsheets. + +- **Slack Alerts for Critical Process Changes**: Set up a workflow that listens for critical event changes within Celonis, such as a drop in supplier performance or an increase in operational risk, and instantaneously sends a message to a designated Slack channel to alert the relevant team members for immediate action. diff --git a/components/celonis_ems/package.json b/components/celonis_ems/package.json index 69cc6d0b76556..21cbc448ad57c 100644 --- a/components/celonis_ems/package.json +++ b/components/celonis_ems/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/celoxis/README.md b/components/celoxis/README.md index 973529943294f..f8e9bd97b7f6e 100644 --- a/components/celoxis/README.md +++ b/components/celoxis/README.md @@ -1,12 +1,11 @@ # Overview -Celoxis API allows you to build a variety of web-based applications. Some -examples include: - -- A project management tool that can help you keep track of your projects, - tasks, and resources -- A timesheet application that can help you track your time and bill your - clients accordingly -- An expense tracking application that can help you track your expenses and - reimbursements -- A document management tool that can help you manage your documents and files +Celoxis is a comprehensive project management tool that offers an API to automate and integrate your project management activities. Using its API with Pipedream, you can streamline processes, sync data across multiple platforms, and trigger actions based on project updates or milestones. For instance, you can automate task assignments, track time, update project statuses, and generate custom reports, all by connecting Celoxis with other tools and services. + +# Example Use Cases + +- **Project Status Updates to Slack**: Trigger a Pipedream workflow whenever a project status changes in Celoxis. The workflow would fetch the updated project details and send a notification to a designated Slack channel, keeping your team informed in real-time. + +- **Task Assignment Emails via SendGrid**: Automate the dispatch of task assignment emails when new tasks are created in Celoxis. Use Pipedream to integrate with SendGrid, sending personalized email notifications to team members, so they're immediately aware of their new responsibilities. + +- **Sync Projects with Google Sheets**: Set up a Pipedream workflow to synchronize Celoxis project data with a Google Sheet. Whenever a project is updated in Celoxis, the relevant Google Sheet is automatically updated. This is ideal for maintaining an easily accessible, always up-to-date project overview for stakeholders not using Celoxis. diff --git a/components/celoxis/package.json b/components/celoxis/package.json new file mode 100644 index 0000000000000..35d5e1005ac86 --- /dev/null +++ b/components/celoxis/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/celoxis", + "version": "0.6.0", + "description": "Pipedream celoxis Components", + "main": "celoxis.app.mjs", + "keywords": [ + "pipedream", + "celoxis" + ], + "homepage": "https://pipedream.com/apps/celoxis", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/census_bureau/README.md b/components/census_bureau/README.md new file mode 100644 index 0000000000000..0aa8b2c42dc35 --- /dev/null +++ b/components/census_bureau/README.md @@ -0,0 +1,11 @@ +# Overview + +The Census Bureau API provides access to a wealth of demographic and economic data collected by the U.S. Census Bureau. In Pipedream, this powerful API can be harnessed to create custom workflows that automate data retrieval, process statistics, and integrate with other services for enhanced analysis and reporting. By leveraging Pipedream's serverless platform, you can set up scheduled tasks or event-driven triggers to interact with the Census Bureau's data, making it easier to incorporate this information into your applications, research, or business intelligence tools. + +# Example Use Cases + +- **Demographic Analysis on Demand**: Trigger a Pipedream workflow that fetches the latest demographic data for a specified region when a custom event occurs (e.g., a form submission on your website). This data can then be sent to Google Sheets for real-time analysis and visualization. + +- **Economic Trends Alert System**: Use Pipedream to set up a scheduled workflow that pulls economic data from the Census Bureau API at regular intervals. Analyze the data for specific trends or thresholds and configure alerts via email or messaging platforms like Slack if certain conditions are met. + +- **Enhanced Market Research**: Combine the Census Bureau API with location-based services like Google Maps API in a Pipedream workflow. Retrieve demographic and economic information for a specific area to gain insights on market characteristics, which can inform business strategy or targeted marketing campaigns. diff --git a/components/centralstationcrm/README.md b/components/centralstationcrm/README.md new file mode 100644 index 0000000000000..ea8a0e865a8bf --- /dev/null +++ b/components/centralstationcrm/README.md @@ -0,0 +1,11 @@ +# Overview + +The CentralStationCRM API enables you to manage customer relationships by automating tasks, syncing data, and creating custom integrations within Pipedream. With this API, you can access functions such as retrieving customer details, logging activities, and managing sales opportunities. Integrating CentralStationCRM with Pipedream allows you to connect to other apps and services, creating efficient workflows that can save time and enhance customer engagement. + +# Example Use Cases + +- **Automated Lead Capture to CRM**: When a new lead is captured through a form on your website, Pipedream can automate the process of adding that lead to CentralStationCRM. Combine this with email parsing or webhooks to add leads in real-time. + +- **Synchronize Contacts with Email Marketing Tools**: Keep your email marketing lists up to date by syncing new contacts from CentralStationCRM to platforms like Mailchimp. Whenever a contact is added or updated in the CRM, Pipedream can trigger an update in Mailchimp. + +- **Task and Event Scheduling Notification**: Use Pipedream to monitor tasks and events scheduled in CentralStationCRM. When a new task is due, trigger notifications through channels like Slack or SMS to keep your team informed and responsive. diff --git a/components/certifier/README.md b/components/certifier/README.md new file mode 100644 index 0000000000000..d87a176095897 --- /dev/null +++ b/components/certifier/README.md @@ -0,0 +1,11 @@ +# Overview + +Certifier is a comprehensive SaaS platform that streamlines and automates the process of issuing and managing digital credentials. With its intuitive API, Certifier allows organizations to effortlessly generate, distribute, and monitor credentials, enhancing efficiency, security, and compliance in a variety of industries. + +# Example Use Cases + +- **Online Course Completion Certificates**: Automatically generate and distribute certificates to students upon course completion, enhancing the learning experience and recognition. + +- **Event Participation Badges**: Issue personalized digital badges to attendees of webinars, conferences, or workshops, making it easy for them to share their achievements on social media. + +- **Employee Recognition**: Streamline the process of awarding certificates or badges for employee achievements, such as "Employee of the Month" or training program completions. diff --git a/components/certifier/actions/create-group/create-group.mjs b/components/certifier/actions/create-group/create-group.mjs new file mode 100644 index 0000000000000..98d3d9991d8c5 --- /dev/null +++ b/components/certifier/actions/create-group/create-group.mjs @@ -0,0 +1,71 @@ +import certifier from "../../certifier.app.mjs"; + +export default { + name: "Create Group", + version: "0.0.1", + key: "certifier-create-group", + description: + "Create a group. [See the documentation](https://developers.certifier.io/reference/create-a-group)", + props: { + certifier, + name: { + propDefinition: [ + certifier, + "groupName", + ], + }, + certificateDesignId: { + propDefinition: [ + certifier, + "certificateDesignId", + ], + }, + badgeDesignId: { + propDefinition: [ + certifier, + "badgeDesignId", + ], + }, + learningEventUrl: { + propDefinition: [ + certifier, + "learningEventUrl", + ], + }, + // eslint-disable-next-line pipedream/props-label, pipedream/props-description + alert: { + type: "alert", + alertType: "warning", + content: + "At least one of `Certificate Design` and `Badge Design` fields is required.", + }, + }, + type: "action", + methods: {}, + async run({ $ }) { + const { + certifier, + name, + certificateDesignId, + badgeDesignId, + learningEventUrl, + } = this; + + const response = await certifier.createGroup({ + $, + data: { + name, + certificateDesignId, + badgeDesignId, + learningEventUrl, + }, + }); + + $.export( + "$summary", + `Successfully created group ${response.name}`, + ); + + return response; + }, +}; diff --git a/components/certifier/actions/issue-credential/issue-credential.mjs b/components/certifier/actions/issue-credential/issue-credential.mjs new file mode 100644 index 0000000000000..4241c86d4433b --- /dev/null +++ b/components/certifier/actions/issue-credential/issue-credential.mjs @@ -0,0 +1,140 @@ +import certifier from "../../certifier.app.mjs"; + +export default { + name: "Issue Credential", + version: "0.0.2", + key: "certifier-issue-credential", + description: + "Create, issue and send a credential to a recipient. [See the documentation](https://developers.certifier.io/reference/create-issue-send-a-credential)", + props: { + certifier, + groupId: { + propDefinition: [ + certifier, + "groupId", + ], + // reloadProps is used so that customAttributes can be loaded + // However, note that in the Certifier app custom attributes + // are defined on a workspace level, not group + reloadProps: true, + }, + recipientName: { + propDefinition: [ + certifier, + "recipientName", + ], + }, + recipientEmail: { + propDefinition: [ + certifier, + "recipientEmail", + ], + }, + issueCredential: { + propDefinition: [ + certifier, + "issueCredential", + ], + }, + sendCredential: { + propDefinition: [ + certifier, + "sendCredential", + ], + }, + issueDate: { + propDefinition: [ + certifier, + "issueDate", + ], + }, + expiryDate: { + propDefinition: [ + certifier, + "expiryDate", + ], + }, + }, + async additionalProps() { + const attributes = await this.certifier.searchAttributes(); + return attributes + .filter((attribute) => !attribute.isDefault) + .reduce( + (acc, attribute) => ({ + ...acc, + [attribute.tag]: { + type: "string", + label: `Custom Attribute: ${attribute.name}`, + optional: true, + }, + }), + {}, + ); + }, + type: "action", + methods: {}, + async run({ $ }) { + const { + certifier, + groupId, + recipientName, + recipientEmail, + issueCredential, + sendCredential, + issueDate, + expiryDate, + } = this; + + const customAttributes = Object.fromEntries( + Object.entries(this).filter(([ + key, + ]) => key.startsWith("custom.")), + ); + + let response = await certifier.createCredential({ + $, + data: { + groupId: groupId, + recipient: { + email: recipientEmail, + name: recipientName, + }, + customAttributes, + ...(issueDate && { + issueDate, + }), + ...(expiryDate && { + expiryDate, + }), + }, + }); + + console.log(`Successfully created credential with ID \`${response.id}\`.`); + + if (issueCredential) { + response = await certifier.issueCredential(response.id, { + $, + }); + + console.log(`Successfully issued credential with ID \`${response.id}\`.`); + + if (sendCredential) { + response = await certifier.sendCredential(response.id, { + $, + data: { + deliveryMethod: "email", + }, + }); + + console.log(`Successfully sent credential with ID \`${response.id}\`.`); + } + } + + $.export( + "$summary", + `Successfully created credential for ${response.recipient.name}`, + ); + + return response; + }, +}; diff --git a/components/certifier/certifier.app.mjs b/components/certifier/certifier.app.mjs new file mode 100644 index 0000000000000..2e3bc29e01b2b --- /dev/null +++ b/components/certifier/certifier.app.mjs @@ -0,0 +1,215 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "certifier", + methods: { + getUrl(path, apiVersion = "v1") { + return `https://api.certifier.io/${apiVersion}${path}`; + }, + getHeaders(headers = {}) { + return { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.$auth.access_token}`, + "Certifier-Version": "2022-10-26", + ...headers, + }; + }, + callApi({ + $ = this, path, headers, apiVersion, ...args + } = {}) { + return axios($, { + url: this.getUrl(path, apiVersion), + headers: this.getHeaders(headers), + ...args, + }); + }, + searchGroups(args = {}) { + return this.callApi({ + path: "/groups", + ...args, + }); + }, + searchAttributes(args = {}) { + return this.callApi({ + path: "/attributes", + ...args, + }); + }, + searchDesigns(args = {}) { + return this.callApi({ + path: "/designs", + ...args, + }); + }, + createCredential(args = {}) { + return this.callApi({ + method: "POST", + path: "/credentials", + ...args, + }); + }, + issueCredential(id, args = {}) { + return this.callApi({ + method: "POST", + path: `/credentials/${id}/issue`, + ...args, + }); + }, + sendCredential(id, args = {}) { + return this.callApi({ + method: "POST", + path: `/credentials/${id}/send`, + ...args, + }); + }, + createGroup(args = {}) { + return this.callApi({ + method: "POST", + path: "/groups", + ...args, + }); + }, + }, + propDefinitions: { + groupId: { + type: "string", + label: "Group ID", + description: "The ID of the group", + async options({ prevContext } = {}) { + const response = await this.searchGroups({ + params: { + cursor: prevContext.cursor, + }, + }); + const groups = response.data; + const cursor = response.pagination.next; + + return { + options: groups.map(({ + id, name, + }) => ({ + label: name, + value: id, + })), + context: { + cursor, + }, + }; + }, + }, + recipientName: { + type: "string", + label: "Recipient Name", + description: "The name of the credential’s recipient.", + }, + recipientEmail: { + type: "string", + label: "Recipient Email", + description: "The email of the credential’s recipient.", + }, + issueCredential: { + type: "boolean", + label: "Issue Credential", + description: + "Whether to issue a credential (`true`) or create a draft (`false`) when the workflow is triggered (default `true`).", + }, + sendCredential: { + type: "boolean", + label: "Send Credential", + description: + "Whether to send a credential to a recipient via email (`true`) or not (`false`) when the workflow is triggered (default is `true`). This step is only applicable if \"Issue Credential\" is set to `true`.", + }, + issueDate: { + type: "string", + label: "Issue Date", + description: + "The date (in `YYYY-MM-DD` format) of your credential's issuance (by default this field is set to the day when the workflow is triggered).", + optional: true, + }, + expiryDate: { + type: "string", + label: "Expiry Date", + description: + "The date (in `YYYY-MM-DD` format) of your credential's expiration. If not provided, expiry date from the group settings will be used (by default this field is empty).", + optional: true, + }, + groupName: { + type: "string", + label: "Group Name", + description: + "The group's name that is used as [group.name] attribute later on.", + }, + certificateDesignId: { + type: "string", + label: "Certificate Design", + description: "The unique certificate design's name.", + optional: true, + async options({ prevContext } = {}) { + const response = await this.searchDesigns({ + params: { + cursor: prevContext.cursor, + }, + }); + const designs = response.data; + const cursor = response.pagination.next; + + return { + options: designs + .filter((design) => design.type === "certificate") + .map(({ + id, name, + }) => ({ + label: name, + value: id, + })), + context: { + cursor, + }, + }; + }, + }, + badgeDesignId: { + type: "string", + label: "Badge Design", + description: "The unique badge design's name.", + optional: true, + async options({ prevContext } = {}) { + const response = await this.searchDesigns({ + params: { + cursor: prevContext.cursor, + }, + }); + const designs = response.data; + const cursor = response.pagination.next; + + return { + options: designs + .filter((design) => design.type === "badge") + .map(({ + id, name, + }) => ({ + label: name, + value: id, + })), + context: { + cursor, + }, + }; + }, + }, + learningEventUrl: { + type: "string", + label: "Learning Event URL", + description: + "The learning event's URL that is shown in the digital wallet.", + optional: true, + }, + credentialId: { + type: "string", + label: "Credential ID", + description: + "The credential's unique ID.", + }, + }, +}; diff --git a/components/certifier/package.json b/components/certifier/package.json new file mode 100644 index 0000000000000..587da8d1ed5de --- /dev/null +++ b/components/certifier/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/certifier", + "version": "0.2.0", + "description": "Pipedream Certifier Components", + "main": "certifier.app.mjs", + "keywords": [ + "pipedream", + "certifier" + ], + "homepage": "https://pipedream.com/apps/certifier", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/cflow/README.md b/components/cflow/README.md new file mode 100644 index 0000000000000..0c96fcc7687ca --- /dev/null +++ b/components/cflow/README.md @@ -0,0 +1,11 @@ +# Overview + +The Cflow API lets you automate and integrate your workflow management, enhancing your business processes with custom automations. With Pipedream, you can harness this API to create, update, and track workflows without manual interference. This enables you to connect Cflow with various other apps and services, streamlining your processes from initiation to completion. + +# Example Use Cases + +- **Automate Document Approval Workflows**: Trigger a Cflow workflow for document approvals when a new file is added to Google Drive. Use Pipedream to listen for new files on Drive, then automatically start an approval process in Cflow, ensuring no document goes unchecked. + +- **Sync HR Requests with Slack**: Integrate Cflow with Slack to notify team channels when new HR requests are submitted. When an employee submits a request in Cflow, Pipedream can post a message with details to a designated Slack channel, keeping everyone in the loop. + +- **Consolidate Sales Leads**: Combine Cflow with a CRM like Salesforce. Whenever a new sales lead is captured in Cflow, use Pipedream to automatically create or update the lead in Salesforce, ensuring your sales team has the most up-to-date information at their fingertips. diff --git a/components/chainaware_ai/actions/fraud-check/fraud-check.mjs b/components/chainaware_ai/actions/fraud-check/fraud-check.mjs new file mode 100644 index 0000000000000..7fb0f495e08e6 --- /dev/null +++ b/components/chainaware_ai/actions/fraud-check/fraud-check.mjs @@ -0,0 +1,51 @@ +import app from "../../chainaware_ai.app.mjs"; + +export default { + key: "chainaware_ai-fraud-check", + name: "Comprehensive Fraud Check", + description: "Conducts a comprehensive fraud check. [See the documentation](https://learn.chainaware.ai/api/fraud-detection-api).", + version: "0.0.1", + type: "action", + props: { + app, + network: { + type: "string", + label: "Network", + description: "The network to which the transaction belongs. Eg. `ETH`.", + default: "ETH", + }, + walletAddress: { + type: "string", + label: "Wallet Address", + description: "The wallet address associated with the transaction.", + }, + }, + methods: { + fraudCheck(args = {}) { + return this.app.post({ + path: "/fraud/check", + ...args, + }); + }, + }, + async run({ $ }) { + const { + fraudCheck, + network, + walletAddress, + } = this; + + const response = await fraudCheck({ + $, + data: { + network, + walletAddress, + onlyFraud: [ + true, + ], + }, + }); + $.export("$summary", `Conducted fraud check with message \`${response.message}\`.`); + return response; + }, +}; diff --git a/components/chainaware_ai/chainaware_ai.app.mjs b/components/chainaware_ai/chainaware_ai.app.mjs new file mode 100644 index 0000000000000..7430bf7f519ec --- /dev/null +++ b/components/chainaware_ai/chainaware_ai.app.mjs @@ -0,0 +1,33 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "chainaware_ai", + methods: { + getUrl(path) { + return `https://enterprise.api.chainaware.ai${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Content-Type": "application/json", + "x-api-key": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + }, +}; diff --git a/components/chainaware_ai/package.json b/components/chainaware_ai/package.json new file mode 100644 index 0000000000000..1bced4e83a5cf --- /dev/null +++ b/components/chainaware_ai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/chainaware_ai", + "version": "0.1.0", + "description": "Pipedream ChainAware.ai Components", + "main": "chainaware_ai.app.mjs", + "keywords": [ + "pipedream", + "chainaware_ai" + ], + "homepage": "https://pipedream.com/apps/chainaware_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.1" + } +} diff --git a/components/chaindesk/README.md b/components/chaindesk/README.md new file mode 100644 index 0000000000000..f315619dc204e --- /dev/null +++ b/components/chaindesk/README.md @@ -0,0 +1,11 @@ +# Overview + +Chaindesk API offers a platform to create, deploy, and manage AI chatbots. It includes features such as natural language processing, integration capabilities, and a conversational interface to engage with users. Using Pipedream, you can leverage the Chaindesk API to build serverless workflows that trigger on various events and interact with other services to automate tasks, analyze conversations, and enhance user experiences. + +# Example Use Cases + +- **Customer Support Automation**: Deploy a chatbot that automatically handles common customer inquiries on your website. Use Pipedream to integrate Chaindesk with your CRM system, such as Salesforce, to log interactions and follow-ups, streamlining customer support. + +- **Real-time Chat Analysis**: Build a workflow where the chatbot conversations are analyzed in real-time for sentiment using Chaindesk API. Connect it with a sentiment analysis tool, like MonkeyLearn, on Pipedream to gauge customer mood and gather insights. + +- **Multi-Channel Messaging Bot**: Create a bot that can operate across multiple messaging platforms like Slack, SMS, or email. With Pipedream, you can orchestrate a workflow where Chaindesk chatbot can be hooked into these platforms, providing a consistent experience no matter where your users interact. diff --git a/components/change_photos/actions/transform-image/transform-image.mjs b/components/change_photos/actions/transform-image/transform-image.mjs new file mode 100644 index 0000000000000..f360e42988275 --- /dev/null +++ b/components/change_photos/actions/transform-image/transform-image.mjs @@ -0,0 +1,155 @@ +import changePhotos from "../../change_photos.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "change_photos-transform-image", + name: "Transform Image", + description: "Transforms an image with various effects and optimizations. [See the documentation](https://www.change.photos/api-docs)", + version: "0.0.1", + type: "action", + props: { + changePhotos, + imageUrl: { + type: "string", + label: "Image URL", + description: "URL of the image to transform", + }, + width: { + type: "integer", + label: "Width", + description: "Desired width in pixels of the output image", + optional: true, + }, + height: { + type: "integer", + label: "Height", + description: "Desired height in pixels of the output image", + optional: true, + }, + format: { + type: "string", + label: "Format", + description: "Output image format. Default: `jpeg`", + options: [ + "jpeg", + "png", + "webp", + ], + optional: true, + }, + quality: { + type: "integer", + label: "Quality", + description: "Output image quality. Must be between `1` and `100`. Default: `80`", + optional: true, + }, + fit: { + type: "string", + label: "Fit", + description: "How the image should fit within the dimensions. Default: `contain`", + options: [ + "contain", + "cover", + "fill", + "inside", + "outside", + ], + optional: true, + }, + flip: { + type: "boolean", + label: "Flip", + description: "Flip the image vertically", + optional: true, + }, + flop: { + type: "boolean", + label: "Flop", + description: "Flip the image horizontally", + optional: true, + }, + rotate: { + type: "integer", + label: "Rotate", + description: "Rotation angle in degrees. Must be between `-360` and `360`. Default: `0`", + optional: true, + }, + grayscale: { + type: "boolean", + label: "Grayscale", + description: "Convert image to grayscale", + optional: true, + }, + blur: { + type: "string", + label: "Blur", + description: "Gaussian blur sigma value. Must be between `0.3` and `1000`.", + optional: true, + }, + sharpen: { + type: "boolean", + label: "Sharpen", + description: "Apply sharpening effect", + optional: true, + }, + compress: { + type: "boolean", + label: "Compress", + description: "Apply additional compression", + optional: true, + }, + tintRedComponent: { + type: "string", + label: "Tint - Red Component", + description: "Red Component of the RGB tint to apply to the image. Must be between `0` and `255`.", + optional: true, + }, + tintGreenComponent: { + type: "string", + label: "Tint - Green Component", + description: "Green Component of the RGB tint to apply to the image. Must be between `0` and `255`.", + optional: true, + }, + tintBlueComponent: { + type: "string", + label: "Tint - Blue Component", + description: "Blue Component of the RGB tint to apply to the image. Must be between `0` and `255`.", + optional: true, + }, + }, + async run({ $ }) { + if ( + (this.tintRedComponent || this.tintGreenComponent || this.tintBlueComponent) + && !(this.tintRedComponent && this.tintGreenComponent && this.tintBlueComponent) + ) { + throw new ConfigurationError("Must specify Red, Green, and Blue RGB components to apply tint"); + } + + const response = await this.changePhotos.transformImage({ + $, + data: { + url: this.imageUrl, + width: this.width, + height: this.height, + format: this.format, + quality: this.quality, + fit: this.fit, + flip: this.flip, + flop: this.flop, + rotate: this.rotate, + grayscale: this.grayscale, + blur: this.blur && +this.blur, + sharpen: this.sharpen, + compress: this.compress, + tint: this.tintRedComponent && { + r: +this.tintRedComponent, + g: +this.tintGreenComponent, + b: +this.tintBlueComponent, + }, + }, + }); + + $.export("$summary", "Image transformed successfully"); + return response; + }, +}; diff --git a/components/change_photos/change_photos.app.mjs b/components/change_photos/change_photos.app.mjs new file mode 100644 index 0000000000000..d550a3e2b0c7e --- /dev/null +++ b/components/change_photos/change_photos.app.mjs @@ -0,0 +1,32 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "change_photos", + propDefinitions: {}, + methods: { + _baseUrl() { + return "https://www.change.photos/api"; + }, + _makeRequest({ + $ = this, + path, + ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + transformImage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/change", + ...opts, + }); + }, + }, +}; diff --git a/components/change_photos/package.json b/components/change_photos/package.json new file mode 100644 index 0000000000000..5f4ec894585b8 --- /dev/null +++ b/components/change_photos/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/change_photos", + "version": "0.1.0", + "description": "Pipedream change.photos Components", + "main": "change_photos.app.mjs", + "keywords": [ + "pipedream", + "change_photos" + ], + "homepage": "https://pipedream.com/apps/change_photos", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/changenow/README.md b/components/changenow/README.md new file mode 100644 index 0000000000000..b5d2c7b54ebf8 --- /dev/null +++ b/components/changenow/README.md @@ -0,0 +1,11 @@ +# Overview + +The ChangeNOW API provides a platform for instant cryptocurrency exchange services, enabling users to swap cryptocurrencies without the need for registration or creating an account. It offers functionalities such as getting currency exchange rates, transaction statuses, and creating new transactions. Within Pipedream, this API can be used to automate workflows involving cryptocurrency trades, monitor exchange rates, and even trigger actions based on these rates or transaction statuses. + +# Example Use Cases + +- **Automated Crypto Trading**: Create a workflow that monitors the exchange rates for a specific cryptocurrency pair. Once the rate hits your target, trigger a transaction automatically. + +- **Transaction Status Notifications**: Set up a workflow to track the status of your ChangeNOW transactions. Whenever there's an update, receive notifications via email, SMS, or a messaging app like Slack. + +- **Exchange Rate Alerts**: Build a workflow that checks for favorable exchange rates at predetermined intervals. When a beneficial rate is detected, the workflow can send you an alert, allowing you to make timely trades. diff --git a/components/changenow/changenow.app.mjs b/components/changenow/changenow.app.mjs new file mode 100644 index 0000000000000..bc97866caad95 --- /dev/null +++ b/components/changenow/changenow.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "changenow", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/changenow/package.json b/components/changenow/package.json new file mode 100644 index 0000000000000..2c759ea2c6a48 --- /dev/null +++ b/components/changenow/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/changenow", + "version": "0.0.1", + "description": "Pipedream ChangeNOW Components", + "main": "changenow.app.mjs", + "keywords": [ + "pipedream", + "changenow" + ], + "homepage": "https://pipedream.com/apps/changenow", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/channeladvisor/README.md b/components/channeladvisor/README.md index 341df9a858853..559027cc98dcd 100644 --- a/components/channeladvisor/README.md +++ b/components/channeladvisor/README.md @@ -1,11 +1,11 @@ # Overview -The ChannelAdvisor API can be used to build a variety of applications, -including: - -- A application to help manage your ChannelAdvisor account -- An application to pull data from ChannelAdvisor and display it in a custom - Dashboard -- A tool to help automate the creation of new listings on ChannelAdvisor -- A tool to help monitor your ChannelAdvisor account for changes or errors -- And much more! +ChannelAdvisor is a comprehensive e-commerce platform that enables retailers and brands to manage sales, inventory, and marketing efforts across multiple online channels. Using the ChannelAdvisor API on Pipedream, users can automate listing updates, order management, and performance analytics. This opens opportunities for streamlining e-commerce operations, such as synchronizing inventory levels, dynamically adjusting prices based on market trends, and collating sales data for actionable insights. + +# Example Use Cases + +- **Order Fulfillment Automation**: Once an order is placed through ChannelAdvisor, trigger a Pipedream workflow to create shipping labels, send tracking information to customers, and update order status. This could be connected to shipping services like Shippo or EasyPost for seamless label generation. + +- **Inventory Sync Across Platforms**: Use Pipedream to monitor inventory changes in ChannelAdvisor. When a change is detected, automatically update inventory levels across other sales channels like Shopify or eBay to maintain consistency and prevent overselling. + +- **Dynamic Repricing Strategy**: Implement a workflow that adjusts product prices on ChannelAdvisor based on predefined rules or competitor pricing. This could involve using Pipedream to fetch competitor prices from APIs such as Amazon MWS or integrating with a price optimization tool that recommends new pricing. diff --git a/components/channeladvisor/package.json b/components/channeladvisor/package.json new file mode 100644 index 0000000000000..2c6733f25ad87 --- /dev/null +++ b/components/channeladvisor/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/channeladvisor", + "version": "0.6.0", + "description": "Pipedream channeladvisor Components", + "main": "channeladvisor.app.mjs", + "keywords": [ + "pipedream", + "channeladvisor" + ], + "homepage": "https://pipedream.com/apps/channeladvisor", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/chaport/README.md b/components/chaport/README.md index 157808b560e5a..f9c7dd5dce6e7 100644 --- a/components/chaport/README.md +++ b/components/chaport/README.md @@ -1,14 +1,11 @@ # Overview -Chaport is a powerful customer communication platform that enables businesses -to easily connect with their customers. Chaport provides a robust set of APIs -that allow businesses to build a wide range of customer communication -experiences, from simple chatbots to complex customer support systems. - -Some examples of what you can build with the Chaport API include: - -- A customer support chatbot to help customers resolve issues -- A live chat system to provide customer support in real-time -- A customer engagement platform to keep customers informed and engaged -- A customer service system to track and respond to customer inquiries -- And much more! +The Chaport API lets you harness the power of live chat automation, enabling seamless communication between customers and support agents. With Pipedream, you can connect the Chaport API to a vast array of services to automate notifications, sync chat data, and streamline customer interactions. Whether you're triggering workflows from new messages or updating CRM records, Chaport via Pipedream makes it possible to create custom automation that extends the utility of live chat across your business ecosystem. + +# Example Use Cases + +- **Customer Support Ticket Creation**: When a new message arrives in Chaport, automatically create a ticket in a helpdesk system like Zendesk or Freshdesk. This ensures that every customer query is logged and tracked, facilitating efficient support workflow management. + +- **CRM Integration for Lead Capture**: Integrate Chaport with a CRM like Salesforce or HubSpot. When a chat ends with a potential lead, automatically create or update a CRM record with the chat details. This can help in nurturing leads and maintaining a centralized database of interactions. + +- **Real-time Slack Notifications for VIP Customers**: Set up a workflow that identifies messages from VIP customers in Chaport and instantly sends a notification to a designated Slack channel. This ensures immediate attention from your team, enhancing customer experience for your most valuable clients. diff --git a/components/chargebee/README.md b/components/chargebee/README.md index 6f653ede689aa..93fe3cda8c1ac 100644 --- a/components/chargebee/README.md +++ b/components/chargebee/README.md @@ -1,11 +1,11 @@ # Overview -The Chargebee API enables you to do the following: - -- Create and manage customers -- Create and manage subscription plans -- Create and manage coupons -- Create and manage invoices -- Set up and manage webhooks -- Generate 3D Secure tokens -- And much more! +The Chargebee API provides a suite of powerful endpoints that facilitate automation around subscription billing, invoicing, and customer management. By leveraging this API on Pipedream, you can build complex, event-driven workflows that react to subscription changes, automate billing operations, sync customer data across platforms, and trigger personalized communication, all without managing servers. + +# Example Use Cases + +- **Automated Dunning Management**: Set up a workflow to monitor for failed payments. When a payment fails, trigger a sequence of emails to the customer with updated payment links, escalating to account suspension if the user does not update their payment method within a given timeframe. + +- **Subscription Lifecycle Events**: Create a workflow that listens for subscription lifecycle events such as sign-ups, upgrades, and renewals. As events occur, sync this data with a CRM like Salesforce, log it to Google Sheets for reporting, or kick off a custom onboarding process using Slack notifications. + +- **Real-Time Analytics Sync**: Configure a workflow to capture real-time data on successful transactions. Aggregate this payment data and push it to a data warehouse such as Snowflake or BigQuery, where you can join it with other datasets and perform in-depth analytics. diff --git a/components/chargebee/actions/create-customer/create-customer.mjs b/components/chargebee/actions/create-customer/create-customer.mjs new file mode 100644 index 0000000000000..666a96f36da4b --- /dev/null +++ b/components/chargebee/actions/create-customer/create-customer.mjs @@ -0,0 +1,69 @@ +import chargebee from "../../chargebee.app.mjs"; +import { clearObject } from "../../common/utils.mjs"; + +export default { + key: "chargebee-create-customer", + name: "Create Customer", + description: "Create a customer in Chargebee. [See the documentation](https://apidocs.chargebee.com/docs/api/customers?lang=node-v3#create_a_customer)", + version: "0.0.1", + type: "action", + props: { + chargebee, + id: { + type: "string", + label: "ID", + description: "ID for the new customer. If not given, this will be auto-generated.", + optional: true, + }, + firstName: { + type: "string", + label: "First Name", + description: "First name of the customer.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the customer.", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "Email of the customer.", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Phone number of the customer.", + optional: true, + }, + company: { + type: "string", + label: "Company", + description: "Company name of the customer.", + optional: true, + }, + additionalFields: { + type: "object", + label: "Additional Fields", + description: "Additional fields and values to set for the customer. [See the documentation](https://apidocs.chargebee.com/docs/api/customers?lang=curl#create_a_customer) for all available fields.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.chargebee.createCustomer(clearObject({ + id: this.id, + first_name: this.firstName, + last_name: this.lastName, + email: this.email, + phone: this.phone, + company: this.company, + ...this.additionalFields, + })); + + $.export("$summary", `Successfully created customer (ID: ${response?.customer?.id})`); + return response?.customer ?? response; + }, +}; diff --git a/components/chargebee/actions/create-subscription/create-subscription.mjs b/components/chargebee/actions/create-subscription/create-subscription.mjs new file mode 100644 index 0000000000000..f7d3f9a8fcd0a --- /dev/null +++ b/components/chargebee/actions/create-subscription/create-subscription.mjs @@ -0,0 +1,83 @@ +import chargebee from "../../chargebee.app.mjs"; +import { clearObject } from "../../common/utils.mjs"; + +export default { + key: "chargebee-create-subscription", + name: "Create Subscription", + description: "Create a new subscription for an existing customer. [See the documentation](https://apidocs.chargebee.com/docs/api/subscriptions?lang=curl#create_subscription_for_items)", + version: "0.0.1", + type: "action", + props: { + chargebee, + customerId: { + propDefinition: [ + chargebee, + "customerId", + ], + }, + itemPriceId: { + propDefinition: [ + chargebee, + "itemPriceId", + ], + }, + unitPrice: { + type: "integer", + label: "Unit Price", + description: "The unit price of the plan item.", + }, + quantity: { + type: "integer", + label: "Quantity", + description: "The quantity of the plan item.", + }, + id: { + type: "string", + label: "ID", + description: "A unique and immutable identifier for the subscription. If not provided, it is autogenerated.", + optional: true, + }, + netTermDays: { + type: "integer", + label: "Net Term Days", + description: "Defines [Net D](https://www.chargebee.com/docs/net_d.html?_gl=1*1w075xz*_gcl_au*MTU4NzU2NDYzOC4xNzMzODA0OTYw) for the subscription. Net D is the number of days within which any invoice raised for the subscription must be paid.", + optional: true, + }, + startDate: { + type: "string", + label: "Start Date", + description: "The date/time at which the subscription is to start, e.g. `2024-08-15T09:30:00Z`. If not provided, the subscription starts immediately.", + optional: true, + }, + additionalFields: { + type: "object", + label: "Additional Fields", + description: "Additional fields and values to set for the subscription. [See the documentation](https://apidocs.chargebee.com/docs/api/subscriptions?lang=curl#create_subscription_for_items) for all available fields.", + optional: true, + }, + }, + async run({ $ }) { + try { + const response = await this.chargebee.createSubscription(this.customerId, clearObject({ + id: this.id, + net_term_days: this.netTermDays, + start_date: this.startDate && (Date.parse(this.startDate) / 1000), + subscription_items: [ + { + item_price_id: this.itemPriceId, + item_type: "plan", + unit_price: this.unitPrice, + quantity: this.quantity, + }, + ], + ...this.additionalFields, + })); + + $.export("$summary", `Successfully created subscription (ID: ${response?.subscription?.id})`); + return response; + } catch (error) { + $.export("debug", error); + throw new Error(`Error creating subscription: ${error.message}`); + } + }, +}; diff --git a/components/chargebee/chargebee.app.mjs b/components/chargebee/chargebee.app.mjs index df798cf319d40..0978e19dbf037 100644 --- a/components/chargebee/chargebee.app.mjs +++ b/components/chargebee/chargebee.app.mjs @@ -1,33 +1,73 @@ -import chargebee from "chargebee"; +import Chargebee from "chargebee"; export default { type: "app", app: "chargebee", + propDefinitions: { + customerId: { + type: "string", + label: "Customer ID", + description: "The ID of the customer to create the subscription for.", + async options() { + const customers = await this.getCustomers(); + return customers.list.map(({ customer }) => ({ + label: `${customer.first_name ?? ""} ${customer.last_name ?? ""} (${customer.email ?? customer.id})`, + value: customer.id, + })); + }, + }, + itemPriceId: { + type: "string", + label: "Plan Item Price ID", + description: "The unique identifier of the plan item price.", + async options() { + const itemPrices = await this.getItemPrices(); + return itemPrices.list + .filter(({ item_price: { item_type } }) => item_type === "plan") + .map(({ + item_price: { + name, id, + }, + }) => ({ + label: name, + value: id, + })); + }, + }, + }, methods: { instance() { - chargebee.configure({ + return new Chargebee({ site: this.$auth.sub_url, - api_key: this.$auth.api_key, + apiKey: this.$auth.api_key, }); - return chargebee; }, getSubscriptions(args = {}) { - return this.instance().subscription.list(args).request(); + return this.instance().subscription.list(args); }, getTransactions(args = {}) { - return this.instance().transaction.list(args).request(); + return this.instance().transaction.list(args); }, getCustomers(args = {}) { - return this.instance().customer.list(args).request(); + return this.instance().customer.list(args); }, getInvoices(args = {}) { - return this.instance().invoice.list(args).request(); + return this.instance().invoice.list(args); }, getPaymentSources(args = {}) { - return this.instance().payment_source.list(args).request(); + return this.instance().paymentSource.list(args); }, getEvents(args = {}) { - return this.instance().event.list(args).request(); + return this.instance().event.list(args); + }, + createCustomer(args = {}) { + return this.instance().customer.create(args); + }, + createSubscription(customerId, args = {}) { + return this.instance().subscription.createWithItems(customerId, args); + }, + getItemPrices(args = {}) { + return this.instance().itemPrice.list(args); }, }, }; diff --git a/components/chargebee/common/utils.mjs b/components/chargebee/common/utils.mjs new file mode 100644 index 0000000000000..5756f20689d80 --- /dev/null +++ b/components/chargebee/common/utils.mjs @@ -0,0 +1,6 @@ +export function clearObject(obj) { + return Object.fromEntries(Object.entries(obj).filter(([ + , + v, + ]) => v !== undefined)); +} diff --git a/components/chargebee/package.json b/components/chargebee/package.json index a67b8c2e839e9..ea00c712495e8 100644 --- a/components/chargebee/package.json +++ b/components/chargebee/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/chargebee", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream Chargebee Components", "main": "chargebee.app.mjs", "keywords": [ @@ -11,8 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "license": "MIT", "dependencies": { - "@pipedream/platform": "^1.4.1", - "chargebee": "^2.22.3" + "@pipedream/platform": "^3.0.3", + "chargebee": "^3.2.1" }, "publishConfig": { "access": "public" diff --git a/components/chargebee/sources/customer-card-expired-instant/customer-card-expired-instant.mjs b/components/chargebee/sources/customer-card-expired-instant/customer-card-expired-instant.mjs index 3a74d3b10d86d..28ca224f21b94 100644 --- a/components/chargebee/sources/customer-card-expired-instant/customer-card-expired-instant.mjs +++ b/components/chargebee/sources/customer-card-expired-instant/customer-card-expired-instant.mjs @@ -8,7 +8,7 @@ export default { name: "Customer Card Expired (Instant)", description: "Emit new event when a customer card has expired. [See the Documentation](https://apidocs.chargebee.com/docs/api/events#card_expired). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", methods: { ...common.methods, diff --git a/components/chargebee/sources/customer-changed-instant/customer-changed-instant.mjs b/components/chargebee/sources/customer-changed-instant/customer-changed-instant.mjs index ef6fcffe01b22..b591ff2b858ac 100644 --- a/components/chargebee/sources/customer-changed-instant/customer-changed-instant.mjs +++ b/components/chargebee/sources/customer-changed-instant/customer-changed-instant.mjs @@ -8,7 +8,7 @@ export default { name: "Customer Changed (Instant)", description: "Emit new event when a customer is changed. [See the Documentation](https://apidocs.chargebee.com/docs/api/events#customer_changed). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", methods: { ...common.methods, diff --git a/components/chargebee/sources/new-customer-created-instant/new-customer-created-instant.mjs b/components/chargebee/sources/new-customer-created-instant/new-customer-created-instant.mjs index 215c024ce5172..50af5d9f75a08 100644 --- a/components/chargebee/sources/new-customer-created-instant/new-customer-created-instant.mjs +++ b/components/chargebee/sources/new-customer-created-instant/new-customer-created-instant.mjs @@ -8,7 +8,7 @@ export default { name: "New Customer Created (Instant)", description: "Emit new event when a new customer is created. [See the Documentation](https://apidocs.chargebee.com/docs/api/events#customer_created). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", methods: { ...common.methods, diff --git a/components/chargebee/sources/new-event/new-event.mjs b/components/chargebee/sources/new-event/new-event.mjs index 7dca499f23b2b..0079e25251cca 100644 --- a/components/chargebee/sources/new-event/new-event.mjs +++ b/components/chargebee/sources/new-event/new-event.mjs @@ -8,7 +8,7 @@ export default { name: "New Event", description: "Emit new event when the selected event is triggered. [See the Documentation](https://apidocs.chargebee.com/docs/api/events). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.1", + version: "0.0.2", dedupe: "unique", props: { ...common.props, diff --git a/components/chargebee/sources/new-invoice-created-instant/new-invoice-created-instant.mjs b/components/chargebee/sources/new-invoice-created-instant/new-invoice-created-instant.mjs index 3a92be609fff1..23baf9e2c8682 100644 --- a/components/chargebee/sources/new-invoice-created-instant/new-invoice-created-instant.mjs +++ b/components/chargebee/sources/new-invoice-created-instant/new-invoice-created-instant.mjs @@ -8,7 +8,7 @@ export default { name: "New Invoice Created (Instant)", description: "Emit new event when a new invoice is created. [See the Documentation](https://apidocs.chargebee.com/docs/api/events#invoice_generated). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", methods: { ...common.methods, diff --git a/components/chargebee/sources/new-invoice-updated-instant/new-invoice-updated-instant.mjs b/components/chargebee/sources/new-invoice-updated-instant/new-invoice-updated-instant.mjs index 33f8c2787c53e..8ec3706f1d92f 100644 --- a/components/chargebee/sources/new-invoice-updated-instant/new-invoice-updated-instant.mjs +++ b/components/chargebee/sources/new-invoice-updated-instant/new-invoice-updated-instant.mjs @@ -8,7 +8,7 @@ export default { name: "New Invoice Updated (Instant)", description: "Emit new event when a new invoice is updated. [See the Documentation](https://apidocs.chargebee.com/docs/api/events#invoice_updated). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.1", + version: "0.0.2", dedupe: "unique", methods: { ...common.methods, diff --git a/components/chargebee/sources/new-payment-source-added-instant/new-payment-source-added-instant.mjs b/components/chargebee/sources/new-payment-source-added-instant/new-payment-source-added-instant.mjs index afb7c3bcee224..354559d9f5544 100644 --- a/components/chargebee/sources/new-payment-source-added-instant/new-payment-source-added-instant.mjs +++ b/components/chargebee/sources/new-payment-source-added-instant/new-payment-source-added-instant.mjs @@ -8,7 +8,7 @@ export default { name: "New Payment Source Added (Instant)", description: "Emit new event when a new payment source is added. [See the Documentation](https://apidocs.chargebee.com/docs/api/events#payment_source_added). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", methods: { ...common.methods, diff --git a/components/chargebee/sources/payment-failed-instant/payment-failed-instant.mjs b/components/chargebee/sources/payment-failed-instant/payment-failed-instant.mjs index 0e985226d49a8..37009f7c4cdb4 100644 --- a/components/chargebee/sources/payment-failed-instant/payment-failed-instant.mjs +++ b/components/chargebee/sources/payment-failed-instant/payment-failed-instant.mjs @@ -8,7 +8,7 @@ export default { name: "Payment Failed (Instant)", description: "Emit new event when a payment is failed. [See the Documentation](https://apidocs.chargebee.com/docs/api/events#payment_failed). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", methods: { ...common.methods, diff --git a/components/chargebee/sources/payment-refunded-instant/payment-refunded-instant.mjs b/components/chargebee/sources/payment-refunded-instant/payment-refunded-instant.mjs index 467bc8fd00c7f..c44e9fb54cf6e 100644 --- a/components/chargebee/sources/payment-refunded-instant/payment-refunded-instant.mjs +++ b/components/chargebee/sources/payment-refunded-instant/payment-refunded-instant.mjs @@ -8,7 +8,7 @@ export default { name: "New Payment Refunded (Instant)", description: "Emit new event when a payment is refunded. [See the Documentation](https://apidocs.chargebee.com/docs/api/events#payment_refunded). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.1", + version: "0.0.2", dedupe: "unique", methods: { ...common.methods, diff --git a/components/chargebee/sources/payment-source-updated-instant/payment-source-updated-instant.mjs b/components/chargebee/sources/payment-source-updated-instant/payment-source-updated-instant.mjs index 8c88970b904fb..5d44d8e4b66b3 100644 --- a/components/chargebee/sources/payment-source-updated-instant/payment-source-updated-instant.mjs +++ b/components/chargebee/sources/payment-source-updated-instant/payment-source-updated-instant.mjs @@ -8,7 +8,7 @@ export default { name: "Payment Source Updated (Instant)", description: "Emit new event when a payment source is updated. [See the Documentation](https://apidocs.chargebee.com/docs/api/events#payment_source_updated). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", methods: { ...common.methods, diff --git a/components/chargebee/sources/payment-succeeded-instant/payment-succeeded-instant.mjs b/components/chargebee/sources/payment-succeeded-instant/payment-succeeded-instant.mjs index da7134771dcc9..7dee368aadebe 100644 --- a/components/chargebee/sources/payment-succeeded-instant/payment-succeeded-instant.mjs +++ b/components/chargebee/sources/payment-succeeded-instant/payment-succeeded-instant.mjs @@ -8,7 +8,7 @@ export default { name: "Payment Succeeded (Instant)", description: "Emit new event when a payment is successful. [See the Documentation](https://apidocs.chargebee.com/docs/api/events#payment_succeeded). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", methods: { ...common.methods, diff --git a/components/chargebee/sources/subscription-activated-instant/subscription-activated-instant.mjs b/components/chargebee/sources/subscription-activated-instant/subscription-activated-instant.mjs index 131b6681065ff..401e4c181f8df 100644 --- a/components/chargebee/sources/subscription-activated-instant/subscription-activated-instant.mjs +++ b/components/chargebee/sources/subscription-activated-instant/subscription-activated-instant.mjs @@ -8,7 +8,7 @@ export default { name: "New Subscription Activated (Instant)", description: "Emit new event when a subscription is activated. [See the Documentation](https://apidocs.chargebee.com/docs/api/events#subscription_activated). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.1", + version: "0.0.2", dedupe: "unique", methods: { ...common.methods, diff --git a/components/chargebee/sources/subscription-cancellation-scheduled-instant/subscription-cancellation-scheduled-instant.mjs b/components/chargebee/sources/subscription-cancellation-scheduled-instant/subscription-cancellation-scheduled-instant.mjs index 2707cbb587b43..c739e94554126 100644 --- a/components/chargebee/sources/subscription-cancellation-scheduled-instant/subscription-cancellation-scheduled-instant.mjs +++ b/components/chargebee/sources/subscription-cancellation-scheduled-instant/subscription-cancellation-scheduled-instant.mjs @@ -7,7 +7,7 @@ export default { name: "Subscription Cancellation Scheduled (Instant)", description: "Emit new event when a subscription cancellation is scheduled. [See the Documentation](https://apidocs.chargebee.com/docs/api/events#subscription_cancellation_scheduled). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", methods: { ...common.methods, diff --git a/components/chargebee/sources/subscription-cancelled-instant/subscription-cancelled-instant.mjs b/components/chargebee/sources/subscription-cancelled-instant/subscription-cancelled-instant.mjs index 6443b6f498694..18159b7a8b441 100644 --- a/components/chargebee/sources/subscription-cancelled-instant/subscription-cancelled-instant.mjs +++ b/components/chargebee/sources/subscription-cancelled-instant/subscription-cancelled-instant.mjs @@ -8,7 +8,7 @@ export default { name: "Subscription Cancelled (Instant)", description: "Emit new event when a subscription is cancelled. [See the Documentation](https://apidocs.chargebee.com/docs/api/events#subscription_cancelled). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", methods: { ...common.methods, diff --git a/components/chargebee/sources/subscription-changed-instant/subscription-changed-instant.mjs b/components/chargebee/sources/subscription-changed-instant/subscription-changed-instant.mjs index 3e9aa3505a7a4..72f0074abee32 100644 --- a/components/chargebee/sources/subscription-changed-instant/subscription-changed-instant.mjs +++ b/components/chargebee/sources/subscription-changed-instant/subscription-changed-instant.mjs @@ -8,7 +8,7 @@ export default { name: "Subscription Changed (Instant)", description: "Emit new event when a subscription is changed. [See the Documentation](https://apidocs.chargebee.com/docs/api/events#subscription_changed). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", methods: { ...common.methods, diff --git a/components/chargebee/sources/subscription-created-instant/subscription-created-instant.mjs b/components/chargebee/sources/subscription-created-instant/subscription-created-instant.mjs index acb82218a0a5a..26f98b7c726f9 100644 --- a/components/chargebee/sources/subscription-created-instant/subscription-created-instant.mjs +++ b/components/chargebee/sources/subscription-created-instant/subscription-created-instant.mjs @@ -8,7 +8,7 @@ export default { name: "New Subscription Created (Instant)", description: "Emit new event when a new subscription is created. [See the Documentation](https://apidocs.chargebee.com/docs/api/events#subscription_created). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", methods: { ...common.methods, diff --git a/components/chargebee/sources/subscription-paused-instant/subscription-paused-instant.mjs b/components/chargebee/sources/subscription-paused-instant/subscription-paused-instant.mjs index 0a31e1e25ad5a..83e28f1d55468 100644 --- a/components/chargebee/sources/subscription-paused-instant/subscription-paused-instant.mjs +++ b/components/chargebee/sources/subscription-paused-instant/subscription-paused-instant.mjs @@ -8,7 +8,7 @@ export default { name: "Subscription Paused (Instant)", description: "Emit new event when a subscription is paused. [See the Documentation](https://apidocs.chargebee.com/docs/api/events#subscription_paused). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", methods: { ...common.methods, diff --git a/components/chargebee/sources/subscription-reactivated-instant/subscription-reactivated-instant.mjs b/components/chargebee/sources/subscription-reactivated-instant/subscription-reactivated-instant.mjs index e123be272e3ec..d502af20efc80 100644 --- a/components/chargebee/sources/subscription-reactivated-instant/subscription-reactivated-instant.mjs +++ b/components/chargebee/sources/subscription-reactivated-instant/subscription-reactivated-instant.mjs @@ -8,7 +8,7 @@ export default { name: "Subscription Reactivated (Instant)", description: "Emit new event when a subscription is reactivated. [See the Documentation](https://apidocs.chargebee.com/docs/api/events#subscription_reactivated). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", methods: { ...common.methods, diff --git a/components/chargebee/sources/subscription-renewed-instant/subscription-renewed-instant.mjs b/components/chargebee/sources/subscription-renewed-instant/subscription-renewed-instant.mjs index e3ad3cfe3601f..16d6e9289b646 100644 --- a/components/chargebee/sources/subscription-renewed-instant/subscription-renewed-instant.mjs +++ b/components/chargebee/sources/subscription-renewed-instant/subscription-renewed-instant.mjs @@ -8,7 +8,7 @@ export default { name: "New Subscription Renewed (Instant)", description: "Emit new event when a subscription is renewed. [See the Documentation](https://apidocs.chargebee.com/docs/api/events#subscription_renewed). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.1", + version: "0.0.2", dedupe: "unique", methods: { ...common.methods, diff --git a/components/chargebee/sources/subscription-resumed-instant/subscription-resumed-instant.mjs b/components/chargebee/sources/subscription-resumed-instant/subscription-resumed-instant.mjs index ec0e7391e776c..c442a040d1bb6 100644 --- a/components/chargebee/sources/subscription-resumed-instant/subscription-resumed-instant.mjs +++ b/components/chargebee/sources/subscription-resumed-instant/subscription-resumed-instant.mjs @@ -7,7 +7,7 @@ export default { name: "Subscription Resumed (Instant)", description: "Emit new event when a subscription is resumed. [See the Documentation](https://apidocs.chargebee.com/docs/api/events#subscription_resumed). Please make sure once you deploy this source, you copy/paste the webhook URL to create it in your [Chargebee Webhook settings](https://www.chargebee.com/docs/2.0/webhook_settings.html#configure-webhooks).", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", methods: { ...common.methods, diff --git a/components/chargeblast/README.md b/components/chargeblast/README.md new file mode 100644 index 0000000000000..fd9c77307ef47 --- /dev/null +++ b/components/chargeblast/README.md @@ -0,0 +1,11 @@ +# Overview + +The Chargeblast API allows users to manage their SMS and email marketing campaigns directly through API calls. By integrating with the Chargeblast API on Pipedream, you can automate the process of creating, updating, and monitoring your marketing campaigns. Pipedream's serverless platform facilitates the creation of workflows that can respond in real-time to various triggers, such as incoming webhook data, scheduled times, or actions from other apps. With these capabilities, you can streamline your marketing operations, personalize your customer interactions, and execute complex sequences of tasks with ease. + +# Example Workflows + +- **Automate Campaign Creation**: Trigger a workflow on Pipedream when a new product is added to your eCommerce platform, such as Shopify. Use this event to create a new marketing campaign on Chargeblast, targeting customers who have shown interest in similar products. + +- **Sync Subscriber Lists**: Maintain an updated subscriber list by creating a Pipedream workflow that listens for new sign-ups on your website. When a new subscriber is detected, automatically add their contact details to a Chargeblast subscriber list to keep your marketing campaigns well-targeted and up-to-date. + +- **Campaign Metrics Dashboard**: Configure a workflow on Pipedream that periodically retrieves campaign performance data from Chargeblast. Feed this data into a dashboard application like Google Sheets or Data Studio, giving you real-time insights into the effectiveness of your marketing efforts. diff --git a/components/chargeover/README.md b/components/chargeover/README.md index 7bfb0fcae3224..803e7048c2fea 100644 --- a/components/chargeover/README.md +++ b/components/chargeover/README.md @@ -1,15 +1,11 @@ # Overview -ChargeOver is an API that allows for the management of recurring billing, -one-time payments, and invoicing. With ChargeOver, you can create and manage -subscriptions, process payments, and generate invoices. Additionally, -ChargeOver can be used to automate billing processes and integrate with your -existing accounting software. - -Some example integrations that can be built using the ChargeOver API include: - -- Automating the billing process -- Generating invoices -- Processing payments -- Creating and managing subscriptions -- Integrating with accounting software +ChargeOver is an API that simplifies the billing and invoicing process for SaaS and subscription-based services. With ChargeOver, you can automate billing workflows, handle various payment methods, set up recurring billing schedules, and manage customer information seamlessly. By integrating ChargeOver with Pipedream, you unlock the ability to craft custom automation flows that link billing events to countless other applications, enhance data management, and streamline communication with customers. It's ideal for businesses looking to reduce manual overhead and ensure their billing operations are as efficient as possible. + +# Example Use Cases + +- **Automated Dunning Management Workflow**: Trigger a Pipedream workflow when a payment fails in ChargeOver. The workflow can automatically send a tailored email to the customer with a link to update their payment method, log the failed payment in a Google Sheet for tracking, and alert your support team in Slack. + +- **New Customer Onboarding Sequence**: When a new customer is added in ChargeOver, initiate a Pipedream workflow that creates a new customer record in your CRM (like Salesforce), enrolls the customer in an onboarding email sequence in a marketing tool like Mailchimp, and posts a welcome message with customer details in a private Slack channel for the sales team. + +- **Subscription Upgrade Analytics**: Configure a workflow to be triggered by subscription changes in ChargeOver. When a customer upgrades their plan, the workflow can update the subscription details in a database, increment a metric in a business analytics tool like Google Analytics, and send a congratulatory email to the customer, leveraging SendGrid or a similar email delivery service. diff --git a/components/chargeover/package.json b/components/chargeover/package.json new file mode 100644 index 0000000000000..ebccf0e5faefa --- /dev/null +++ b/components/chargeover/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/chargeover", + "version": "0.6.0", + "description": "Pipedream chargeover Components", + "main": "chargeover.app.mjs", + "keywords": [ + "pipedream", + "chargeover" + ], + "homepage": "https://pipedream.com/apps/chargeover", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/chargify/README.md b/components/chargify/README.md index 3b437b3bbeac4..ed9b0e11375a2 100644 --- a/components/chargify/README.md +++ b/components/chargify/README.md @@ -1,11 +1,11 @@ # Overview -With the Chargify API, you can build applications that: - -- Create and manage Chargify subscriptions -- Process one-time and recurring charges -- Handle account management tasks, such as updating account details and - managing payment methods -- Generate invoices and account statements -- Trigger webhooks to get real-time notifications of events in your Chargify - account +The Chargify API enables seamless integration of subscription billing, management, and reporting functionalities. With Chargify, you can automate the creation and management of customer subscriptions, handle invoicing, apply taxes, and track analytics related to your billing processes. It's a powerful tool for businesses with recurring revenue models to keep their billing systems in sync with other business operations, reducing manual workload and increasing efficiency. + +# Example Use Cases + +- **Automated New Customer Onboarding**: Trigger a workflow when a new customer subscribes via Chargify. Capture their details, create an account in your CRM (like Salesforce), send a personalized welcome email using an email platform (like SendGrid), and notify your team on Slack. + +- **Dunning Management Automation**: Set up a workflow that listens to payment failure events from Chargify. When a payment fails, automate outreach with a series of emails through an email service like Mailgun to remind customers of their payment obligations. Simultaneously, log the incident in a customer service platform like Zendesk for follow-up. + +- **Subscription Analytics Reporting**: Create a daily scheduled workflow that fetches subscription data from Chargify and compiles key metrics into a report. Use this data to update a live dashboard in Google Sheets or Data Studio, providing your team with up-to-date insights on revenue and customer churn. diff --git a/components/chargify/actions/create-customer/create-customer.mjs b/components/chargify/actions/create-customer/create-customer.mjs new file mode 100644 index 0000000000000..016411119d257 --- /dev/null +++ b/components/chargify/actions/create-customer/create-customer.mjs @@ -0,0 +1,104 @@ +import chargify from "../../chargify.app.mjs"; + +export default { + key: "chargify-create-customer", + name: "Create Customer", + description: "Creates a new customer in Chargify. [See the documentation](https://developers.maxio.com/http/advanced-billing-api/api-endpoints/customers/create-customer)", + version: "0.0.1", + type: "action", + props: { + chargify, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the customer", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the customer", + }, + email: { + type: "string", + label: "Email", + description: "The email of the customer", + }, + organization: { + type: "string", + label: "Organization", + description: "The organization of the customer", + optional: true, + }, + address: { + type: "string", + label: "Street Address", + description: "Street address of the customer (line 1)", + optional: true, + }, + address2: { + type: "string", + label: "Street Address (line 2)", + description: "Street address of the customer (line 2)", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "City of the customer", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "State/Region of the customer", + optional: true, + }, + zip: { + type: "string", + label: "Zip", + description: "Zip code of the customer", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "The country code of the customer. Example: `US`", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Phone number of the customer", + optional: true, + }, + taxExempt: { + type: "boolean", + label: "Tax Exempt", + description: "Set to `true` if the customer is tax exempt.", + optional: true, + }, + }, + async run({ $ }) { + const { customer } = await this.chargify.createCustomer({ + $, + data: { + customer: { + first_name: this.firstName, + last_name: this.lastName, + email: this.email, + organization: this.organization, + address: this.address, + address_2: this.address2, + city: this.city, + state: this.state, + zip: this.zip, + country: this.country, + phone: this.phone, + tax_exempt: this.taxExempt, + }, + }, + }); + $.export("$summary", `Successfully created customer with ID ${customer.id}`); + return customer; + }, +}; diff --git a/components/chargify/actions/create-subscription/create-subscription.mjs b/components/chargify/actions/create-subscription/create-subscription.mjs new file mode 100644 index 0000000000000..d2eadaf55f3ea --- /dev/null +++ b/components/chargify/actions/create-subscription/create-subscription.mjs @@ -0,0 +1,58 @@ +import chargify from "../../chargify.app.mjs"; + +export default { + key: "chargify-create-subscription", + name: "Create Subscription", + description: "Establishes a new subscription for a given customer in Chargify. [See the documentation](https://developers.maxio.com/http/advanced-billing-api/api-endpoints/subscriptions/create-subscription)", + version: "0.0.1", + type: "action", + props: { + chargify, + customerId: { + propDefinition: [ + chargify, + "customerId", + ], + }, + productId: { + propDefinition: [ + chargify, + "productId", + ], + }, + couponCode: { + propDefinition: [ + chargify, + "couponCode", + ], + }, + nextBillingAt: { + propDefinition: [ + chargify, + "nextBillingAt", + ], + }, + paymentCollectionMethod: { + propDefinition: [ + chargify, + "paymentCollectionMethod", + ], + }, + }, + async run({ $ }) { + const response = await this.chargify.createSubscription({ + $, + data: { + subscription: { + customer_id: this.customerId, + product_id: this.productId, + coupon_code: this.couponCode, + next_billing_at: this.nextBillingAt, + payment_collection_method: this.paymentCollectionMethod, + }, + }, + }); + $.export("$summary", `Successfully created subscription with ID: ${response.subscription.id}`); + return response; + }, +}; diff --git a/components/chargify/actions/update-subscription/update-subscription.mjs b/components/chargify/actions/update-subscription/update-subscription.mjs new file mode 100644 index 0000000000000..01e1a6421ed2f --- /dev/null +++ b/components/chargify/actions/update-subscription/update-subscription.mjs @@ -0,0 +1,59 @@ +import chargify from "../../chargify.app.mjs"; + +export default { + key: "chargify-update-subscription", + name: "Update Subscription", + description: "Modifies an existing subscription in Chargify. [See the documentation](https://developers.maxio.com/http/advanced-billing-api/api-endpoints/subscriptions/update-subscription)", + version: "0.0.1", + type: "action", + props: { + chargify, + subscriptionId: { + propDefinition: [ + chargify, + "subscriptionId", + ], + }, + productId: { + propDefinition: [ + chargify, + "productId", + ], + optional: true, + }, + couponCode: { + propDefinition: [ + chargify, + "couponCode", + ], + }, + nextBillingAt: { + propDefinition: [ + chargify, + "nextBillingAt", + ], + }, + paymentCollectionMethod: { + propDefinition: [ + chargify, + "paymentCollectionMethod", + ], + }, + }, + async run({ $ }) { + const response = await this.chargify.updateSubscription({ + $, + subscriptionId: this.subscriptionId, + data: { + subscription: { + product_id: this.productId, + coupon_code: this.couponCode, + next_billing_at: this.nextBillingAt, + payment_collection_method: this.paymentCollectionMethod, + }, + }, + }); + $.export("$summary", `Successfully updated subscription ${this.subscriptionId}`); + return response; + }, +}; diff --git a/components/chargify/chargify.app.mjs b/components/chargify/chargify.app.mjs index fb19cd17138b5..499ef2d70d568 100644 --- a/components/chargify/chargify.app.mjs +++ b/components/chargify/chargify.app.mjs @@ -1,11 +1,187 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "chargify", - propDefinitions: {}, + propDefinitions: { + customerId: { + type: "string", + label: "Customer ID", + description: "The ID of the customer", + async options({ page }) { + const customers = await this.listCustomers({ + params: { + page: page + 1, + }, + }); + return customers?.map(({ customer }) => ({ + value: customer.id, + label: `${customer.first_name} ${customer.last_name}`, + })) || []; + }, + }, + subscriptionId: { + type: "string", + label: "Subscription ID", + description: "The ID of the subscription", + async options({ page }) { + const subscriptions = await this.listSubscriptions({ + params: { + page: page + 1, + }, + }); + return subscriptions?.map(({ subscription }) => ({ + value: subscription.id, + label: `${subscription.customer.first_name} ${subscription.customer.last_name} - ${subscription.product.name}`, + })) || []; + }, + }, + productId: { + type: "string", + label: "Product ID", + description: "The ID of the product", + async options({ page }) { + const products = await this.listProducts({ + params: { + page: page + 1, + }, + }); + return products?.map(({ product }) => ({ + value: product.id, + label: product.name, + })) || []; + }, + }, + couponCode: { + type: "string", + label: "Coupon Code", + description: "The coupon code", + optional: true, + async options({ page }) { + const coupons = await this.listCoupons({ + params: { + page: page + 1, + }, + }); + return coupons?.map(({ coupon }) => ({ + value: coupon.code, + label: coupon.name, + })) || []; + }, + }, + paymentCollectionMethod: { + type: "string", + label: "Payment Collection Method", + description: "The type of payment collection to be used in the subscription", + optional: true, + options: [ + "automatic", + "remittance", + "prepaid", + ], + }, + nextBillingAt: { + type: "string", + label: "Next Billing At", + description: "The next billing date in ISO-8601 format. Example: `2024-10-31T00:00:00Z`", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `https://${this.$auth.subdomain}.chargify.com`; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "Content-type": "application/json", + "Accept": "application/json", + }, + auth: { + username: `${this.$auth.api_key}`, + password: "", + }, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/endpoints.json", + ...opts, + }); + }, + deleteWebhook({ + hookId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/endpoints/${hookId}.json`, + ...opts, + }); + }, + enableWebhooks(opts = {}) { + return this._makeRequest({ + method: "PUT", + path: "/webhooks/settings.json", + data: { + webhooks_enabled: true, + }, + ...opts, + }); + }, + listCustomers(opts = {}) { + return this._makeRequest({ + path: "/customers.json", + ...opts, + }); + }, + listSubscriptions(opts = {}) { + return this._makeRequest({ + path: "/subscriptions.json", + ...opts, + }); + }, + listProducts(opts = {}) { + return this._makeRequest({ + path: "/products.json", + ...opts, + }); + }, + listCoupons(opts = {}) { + return this._makeRequest({ + path: "/coupons.json", + ...opts, + }); + }, + createCustomer(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/customers.json", + ...opts, + }); + }, + createSubscription(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/subscriptions.json", + ...opts, + }); + }, + updateSubscription({ + subscriptionId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/subscriptions/${subscriptionId}.json`, + ...opts, + }); }, }, }; diff --git a/components/chargify/package.json b/components/chargify/package.json new file mode 100644 index 0000000000000..89314a086ed2a --- /dev/null +++ b/components/chargify/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/chargify", + "version": "0.0.1", + "description": "Pipedream Chargify Components", + "main": "chargify.app.mjs", + "keywords": [ + "pipedream", + "chargify" + ], + "homepage": "https://pipedream.com/apps/chargify", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/chargify/sources/common/base.mjs b/components/chargify/sources/common/base.mjs new file mode 100644 index 0000000000000..79d16a72599e5 --- /dev/null +++ b/components/chargify/sources/common/base.mjs @@ -0,0 +1,71 @@ +import chargify from "../../chargify.app.mjs"; + +export default { + props: { + chargify, + db: "$.service.db", + http: "$.interface.http", + }, + hooks: { + async deploy() { + // ensure webhooks are enabled in user's account + await this.chargify.enableWebhooks(); + }, + async activate() { + const { endpoint: { id } } = await this.chargify.createWebhook({ + data: { + endpoint: { + url: this.http.endpoint, + webhook_subscriptions: [ + this.getEventType(), + ], + }, + }, + }); + this._setHookId(id); + }, + async deactivate() { + const hookId = this._getHookId(); + if (hookId) { + await this.chargify.deleteWebhook({ + hookId, + data: { + endpoint: { + url: this.http.endpoint, + webhook_subscriptions: [], + }, + }, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + generateMeta(item) { + return { + id: item.id, + summary: this.getSummary(item), + ts: Date.now(), + }; + }, + getEventType() { + throw new Error("getEventType is not implemented"); + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + }, + async run(event) { + const { body } = event; + if (!body) { + return; + } + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, +}; diff --git a/components/chargify/sources/new-customer-instant/new-customer-instant.mjs b/components/chargify/sources/new-customer-instant/new-customer-instant.mjs new file mode 100644 index 0000000000000..9dcb36e880802 --- /dev/null +++ b/components/chargify/sources/new-customer-instant/new-customer-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "chargify-new-customer-instant", + name: "New Customer (Instant)", + description: "Emit new event when a new customer is added in Chargify", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return "customer_create"; + }, + getSummary(item) { + return `New Customer with ID: ${item["payload[customer][id]"]}`; + }, + }, + sampleEmit, +}; diff --git a/components/chargify/sources/new-customer-instant/test-event.mjs b/components/chargify/sources/new-customer-instant/test-event.mjs new file mode 100644 index 0000000000000..69bfbae5755b3 --- /dev/null +++ b/components/chargify/sources/new-customer-instant/test-event.mjs @@ -0,0 +1,35 @@ +export default { + "id": "2566241561", + "event": "customer_create", + "payload[customer][id]": "83698432", + "payload[customer][first_name]": "FirstName", + "payload[customer][last_name]": "LastName", + "payload[customer][organization]": "", + "payload[customer][email]": "test@email.com", + "payload[customer][created_at]": "2024-10-11 15:12:51 -0400", + "payload[customer][updated_at]": "2024-10-11 15:12:51 -0400", + "payload[customer][reference]": "", + "payload[customer][address]": "", + "payload[customer][address_2]": "", + "payload[customer][address_3]": "", + "payload[customer][city]": "", + "payload[customer][state]": "", + "payload[customer][zip]": "", + "payload[customer][country]": "", + "payload[customer][phone]": "", + "payload[customer][portal_invite_last_sent_at]": "", + "payload[customer][portal_invite_last_accepted_at]": "", + "payload[customer][verified]": "false", + "payload[customer][portal_customer_created_at]": "", + "payload[customer][vat_number]": "", + "payload[customer][cc_emails]": "", + "payload[customer][tax_exempt]": "false", + "payload[customer][tax_exempt_reason]": "", + "payload[customer][parent_id]": "", + "payload[customer][locale]": "", + "payload[customer][salesforce_id]": "", + "payload[customer][default_auto_renewal_profile_id]": "", + "payload[site][id]": "87942", + "payload[site][subdomain]": "testing", + "payload[event_id]": "4833962613" +} \ No newline at end of file diff --git a/components/chargify/sources/new-subscription-instant/new-subscription-instant.mjs b/components/chargify/sources/new-subscription-instant/new-subscription-instant.mjs new file mode 100644 index 0000000000000..9551da104e7cd --- /dev/null +++ b/components/chargify/sources/new-subscription-instant/new-subscription-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "chargify-new-subscription-instant", + name: "New Subscription (Instant)", + description: "Emit new event when a new subscription is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return "signup_success"; + }, + getSummary(item) { + return `New Subscription with ID: ${item["payload[subscription][id]"]}`; + }, + }, + sampleEmit, +}; diff --git a/components/chargify/sources/new-subscription-instant/test-event.mjs b/components/chargify/sources/new-subscription-instant/test-event.mjs new file mode 100644 index 0000000000000..9e265b8021903 --- /dev/null +++ b/components/chargify/sources/new-subscription-instant/test-event.mjs @@ -0,0 +1,132 @@ +export default { + "id": "2566271773", + "event": "signup_success", + "payload[subscription][id]": "79431158", + "payload[subscription][state]": "active", + "payload[subscription][trial_started_at]": "", + "payload[subscription][trial_ended_at]": "", + "payload[subscription][activated_at]": "2024-10-11 15:22:07 -0400", + "payload[subscription][created_at]": "2024-10-11 15:22:06 -0400", + "payload[subscription][updated_at]": "2024-10-11 15:22:08 -0400", + "payload[subscription][expires_at]": "", + "payload[subscription][balance_in_cents]": "0", + "payload[subscription][current_period_ends_at]": "2024-11-11 14:22:06 -0500", + "payload[subscription][next_assessment_at]": "2024-11-11 14:22:06 -0500", + "payload[subscription][canceled_at]": "", + "payload[subscription][cancellation_message]": "", + "payload[subscription][next_product_id]": "", + "payload[subscription][next_product_handle]": "", + "payload[subscription][cancel_at_end_of_period]": "false", + "payload[subscription][payment_collection_method]": "automatic", + "payload[subscription][snap_day]": "", + "payload[subscription][cancellation_method]": "", + "payload[subscription][current_period_started_at]": "2024-10-11 15:22:06 -0400", + "payload[subscription][previous_state]": "active", + "payload[subscription][signup_payment_id]": "1170015322", + "payload[subscription][signup_revenue]": "0.00", + "payload[subscription][delayed_cancel_at]": "", + "payload[subscription][coupon_code]": "", + "payload[subscription][total_revenue_in_cents]": "0", + "payload[subscription][product_price_in_cents]": "0", + "payload[subscription][product_version_number]": "1", + "payload[subscription][payment_type]": "", + "payload[subscription][referral_code]": "", + "payload[subscription][coupon_use_count]": "", + "payload[subscription][coupon_uses_allowed]": "", + "payload[subscription][reason_code]": "", + "payload[subscription][automatically_resume_at]": "", + "payload[subscription][offer_id]": "", + "payload[subscription][payer_id]": "", + "payload[subscription][receives_invoice_emails]": "", + "payload[subscription][product_price_point_id]": "3322919", + "payload[subscription][next_product_price_point_id]": "", + "payload[subscription][credit_balance_in_cents]": "0", + "payload[subscription][prepayment_balance_in_cents]": "0", + "payload[subscription][net_terms]": "", + "payload[subscription][stored_credential_transaction_id]": "", + "payload[subscription][locale]": "", + "payload[subscription][reference]": "", + "payload[subscription][currency]": "USD", + "payload[subscription][on_hold_at]": "", + "payload[subscription][scheduled_cancellation_at]": "", + "payload[subscription][product_price_point_type]": "default", + "payload[subscription][dunning_communication_delay_enabled]": "false", + "payload[subscription][dunning_communication_delay_time_zone]": "", + "payload[subscription][customer][id]": "83698432", + "payload[subscription][customer][first_name]": "FirstName", + "payload[subscription][customer][last_name]": "LastName", + "payload[subscription][customer][organization]": "", + "payload[subscription][customer][email]": "firstname@lastname.com", + "payload[subscription][customer][created_at]": "2024-10-11 15:12:51 -0400", + "payload[subscription][customer][updated_at]": "2024-10-11 15:12:51 -0400", + "payload[subscription][customer][reference]": "", + "payload[subscription][customer][address]": "", + "payload[subscription][customer][address_2]": "", + "payload[subscription][customer][address_3]": "", + "payload[subscription][customer][city]": "", + "payload[subscription][customer][state]": "", + "payload[subscription][customer][state_name]": "", + "payload[subscription][customer][zip]": "", + "payload[subscription][customer][country]": "", + "payload[subscription][customer][country_name]": "", + "payload[subscription][customer][phone]": "", + "payload[subscription][customer][portal_invite_last_sent_at]": "", + "payload[subscription][customer][portal_invite_last_accepted_at]": "", + "payload[subscription][customer][verified]": "false", + "payload[subscription][customer][portal_customer_created_at]": "", + "payload[subscription][customer][vat_number]": "", + "payload[subscription][customer][cc_emails]": "", + "payload[subscription][customer][tax_exempt]": "false", + "payload[subscription][customer][tax_exempt_reason]": "", + "payload[subscription][customer][parent_id]": "", + "payload[subscription][customer][locale]": "", + "payload[subscription][customer][salesforce_id]": "", + "payload[subscription][product][id]": "6748126", + "payload[subscription][product][name]": "Pro Plan", + "payload[subscription][product][handle]": "", + "payload[subscription][product][description]": "", + "payload[subscription][product][accounting_code]": "", + "payload[subscription][product][request_credit_card]": "true", + "payload[subscription][product][expiration_interval]": "", + "payload[subscription][product][expiration_interval_unit]": "never", + "payload[subscription][product][created_at]": "2024-10-11 12:56:32 -0400", + "payload[subscription][product][updated_at]": "2024-10-11 12:56:32 -0400", + "payload[subscription][product][price_in_cents]": "0", + "payload[subscription][product][interval]": "1", + "payload[subscription][product][interval_unit]": "month", + "payload[subscription][product][initial_charge_in_cents]": "", + "payload[subscription][product][trial_price_in_cents]": "", + "payload[subscription][product][trial_interval]": "", + "payload[subscription][product][trial_interval_unit]": "month", + "payload[subscription][product][archived_at]": "", + "payload[subscription][product][require_credit_card]": "false", + "payload[subscription][product][return_params]": "", + "payload[subscription][product][taxable]": "false", + "payload[subscription][product][update_return_url]": "", + "payload[subscription][product][tax_code]": "", + "payload[subscription][product][initial_charge_after_trial]": "false", + "payload[subscription][product][version_number]": "1", + "payload[subscription][product][update_return_params]": "", + "payload[subscription][product][default_product_price_point_id]": "3322919", + "payload[subscription][product][request_billing_address]": "false", + "payload[subscription][product][require_billing_address]": "false", + "payload[subscription][product][require_shipping_address]": "false", + "payload[subscription][product][use_site_exchange_rate]": "true", + "payload[subscription][product][item_category]": "", + "payload[subscription][product][product_price_point_id]": "3322919", + "payload[subscription][product][product_price_point_name]": "Original", + "payload[subscription][product][product_price_point_handle]": "uuid:b0c26c80-6a1f-013d-c9f9-063702f7df93", + "payload[subscription][product][product_family][id]": "2699089", + "payload[subscription][product][product_family][name]": "Billing Plans", + "payload[subscription][product][product_family][description]": "", + "payload[subscription][product][product_family][handle]": "testing-billing-plans", + "payload[subscription][product][product_family][accounting_code]": "", + "payload[subscription][product][product_family][created_at]": "2024-10-11 11:23:44 -0400", + "payload[subscription][product][product_family][updated_at]": "2024-10-11 11:23:44 -0400", + "payload[subscription][group]": "", + "payload[subscription][initial_billing_at]": "", + "payload[subscription][referred_by]": "", + "payload[site][id]": "87942", + "payload[site][subdomain]": "testing", + "payload[event_id]": "4833997313" +} \ No newline at end of file diff --git a/components/chargify/sources/new-subscription-state-instant/new-subscription-state-instant.mjs b/components/chargify/sources/new-subscription-state-instant/new-subscription-state-instant.mjs new file mode 100644 index 0000000000000..98bd4044abce8 --- /dev/null +++ b/components/chargify/sources/new-subscription-state-instant/new-subscription-state-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "chargify-new-subscription-state-instant", + name: "New Subscription State (Instant)", + description: "Emit new event when the state of a subscription changes", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return "subscription_state_change"; + }, + getSummary(item) { + return `State updated for subscription ${item["payload[subscription][id]"]}`; + }, + }, + sampleEmit, +}; diff --git a/components/chargify/sources/new-subscription-state-instant/test-event.mjs b/components/chargify/sources/new-subscription-state-instant/test-event.mjs new file mode 100644 index 0000000000000..066982231c74a --- /dev/null +++ b/components/chargify/sources/new-subscription-state-instant/test-event.mjs @@ -0,0 +1,130 @@ +export default { + "id": "2566279204", + "event": "subscription_state_change", + "payload[subscription][id]": "79431128", + "payload[subscription][state]": "on_hold", + "payload[subscription][trial_started_at]": "", + "payload[subscription][trial_ended_at]": "", + "payload[subscription][activated_at]": "2024-10-11 15:20:08 -0400", + "payload[subscription][created_at]": "2024-10-11 15:20:06 -0400", + "payload[subscription][updated_at]": "2024-10-11 15:28:31 -0400", + "payload[subscription][expires_at]": "", + "payload[subscription][balance_in_cents]": "0", + "payload[subscription][current_period_ends_at]": "2024-11-11 14:20:06 -0500", + "payload[subscription][next_assessment_at]": "2024-11-11 14:20:06 -0500", + "payload[subscription][canceled_at]": "", + "payload[subscription][cancellation_message]": "", + "payload[subscription][next_product_id]": "", + "payload[subscription][next_product_handle]": "", + "payload[subscription][cancel_at_end_of_period]": "false", + "payload[subscription][payment_collection_method]": "automatic", + "payload[subscription][snap_day]": "", + "payload[subscription][cancellation_method]": "", + "payload[subscription][current_period_started_at]": "2024-10-11 15:20:06 -0400", + "payload[subscription][previous_state]": "active", + "payload[subscription][signup_payment_id]": "1170014104", + "payload[subscription][signup_revenue]": "0.00", + "payload[subscription][delayed_cancel_at]": "", + "payload[subscription][coupon_code]": "", + "payload[subscription][total_revenue_in_cents]": "0", + "payload[subscription][product_price_in_cents]": "0", + "payload[subscription][product_version_number]": "1", + "payload[subscription][payment_type]": "", + "payload[subscription][referral_code]": "", + "payload[subscription][coupon_use_count]": "", + "payload[subscription][coupon_uses_allowed]": "", + "payload[subscription][reason_code]": "", + "payload[subscription][automatically_resume_at]": "", + "payload[subscription][offer_id]": "", + "payload[subscription][payer_id]": "", + "payload[subscription][receives_invoice_emails]": "", + "payload[subscription][product_price_point_id]": "3322919", + "payload[subscription][next_product_price_point_id]": "", + "payload[subscription][credit_balance_in_cents]": "0", + "payload[subscription][prepayment_balance_in_cents]": "0", + "payload[subscription][net_terms]": "", + "payload[subscription][stored_credential_transaction_id]": "", + "payload[subscription][locale]": "", + "payload[subscription][reference]": "", + "payload[subscription][currency]": "USD", + "payload[subscription][on_hold_at]": "2024-10-11 15:28:31 -0400", + "payload[subscription][scheduled_cancellation_at]": "", + "payload[subscription][product_price_point_type]": "default", + "payload[subscription][dunning_communication_delay_enabled]": "false", + "payload[subscription][dunning_communication_delay_time_zone]": "", + "payload[subscription][customer][id]": "83697711", + "payload[subscription][customer][first_name]": "FirstName", + "payload[subscription][customer][last_name]": "LastName", + "payload[subscription][customer][organization]": "", + "payload[subscription][customer][email]": "test@email.com", + "payload[subscription][customer][created_at]": "2024-10-11 15:08:35 -0400", + "payload[subscription][customer][updated_at]": "2024-10-11 15:08:35 -0400", + "payload[subscription][customer][reference]": "", + "payload[subscription][customer][address]": "", + "payload[subscription][customer][address_2]": "", + "payload[subscription][customer][address_3]": "", + "payload[subscription][customer][city]": "", + "payload[subscription][customer][state]": "", + "payload[subscription][customer][state_name]": "", + "payload[subscription][customer][zip]": "", + "payload[subscription][customer][country]": "", + "payload[subscription][customer][country_name]": "", + "payload[subscription][customer][phone]": "", + "payload[subscription][customer][portal_invite_last_sent_at]": "", + "payload[subscription][customer][portal_invite_last_accepted_at]": "", + "payload[subscription][customer][verified]": "false", + "payload[subscription][customer][portal_customer_created_at]": "", + "payload[subscription][customer][vat_number]": "", + "payload[subscription][customer][cc_emails]": "", + "payload[subscription][customer][tax_exempt]": "false", + "payload[subscription][customer][tax_exempt_reason]": "", + "payload[subscription][customer][parent_id]": "", + "payload[subscription][customer][locale]": "", + "payload[subscription][customer][salesforce_id]": "", + "payload[subscription][product][id]": "6748126", + "payload[subscription][product][name]": "Pro Plan", + "payload[subscription][product][handle]": "", + "payload[subscription][product][description]": "", + "payload[subscription][product][accounting_code]": "", + "payload[subscription][product][request_credit_card]": "true", + "payload[subscription][product][expiration_interval]": "", + "payload[subscription][product][expiration_interval_unit]": "never", + "payload[subscription][product][created_at]": "2024-10-11 12:56:32 -0400", + "payload[subscription][product][updated_at]": "2024-10-11 12:56:32 -0400", + "payload[subscription][product][price_in_cents]": "0", + "payload[subscription][product][interval]": "1", + "payload[subscription][product][interval_unit]": "month", + "payload[subscription][product][initial_charge_in_cents]": "", + "payload[subscription][product][trial_price_in_cents]": "", + "payload[subscription][product][trial_interval]": "", + "payload[subscription][product][trial_interval_unit]": "month", + "payload[subscription][product][archived_at]": "", + "payload[subscription][product][require_credit_card]": "false", + "payload[subscription][product][return_params]": "", + "payload[subscription][product][taxable]": "false", + "payload[subscription][product][update_return_url]": "", + "payload[subscription][product][tax_code]": "", + "payload[subscription][product][initial_charge_after_trial]": "false", + "payload[subscription][product][version_number]": "1", + "payload[subscription][product][update_return_params]": "", + "payload[subscription][product][default_product_price_point_id]": "3322919", + "payload[subscription][product][request_billing_address]": "false", + "payload[subscription][product][require_billing_address]": "false", + "payload[subscription][product][require_shipping_address]": "false", + "payload[subscription][product][use_site_exchange_rate]": "true", + "payload[subscription][product][item_category]": "", + "payload[subscription][product][product_price_point_id]": "3322919", + "payload[subscription][product][product_price_point_name]": "Original", + "payload[subscription][product][product_price_point_handle]": "uuid:b0c26c80-6a1f-013d-c9f9-063702f7df93", + "payload[subscription][product][product_family][id]": "2699089", + "payload[subscription][product][product_family][name]": "Billing Plans", + "payload[subscription][product][product_family][description]": "", + "payload[subscription][product][product_family][handle]": "testing-billing-plans", + "payload[subscription][product][product_family][accounting_code]": "", + "payload[subscription][product][product_family][created_at]": "2024-10-11 11:23:44 -0400", + "payload[subscription][product][product_family][updated_at]": "2024-10-11 11:23:44 -0400", + "payload[subscription][group]": "", + "payload[site][id]": "87942", + "payload[site][subdomain]": "testing", + "payload[event_id]": "4834010965" +} \ No newline at end of file diff --git a/components/charthop/charthop.app.mjs b/components/charthop/charthop.app.mjs new file mode 100644 index 0000000000000..746957318d73f --- /dev/null +++ b/components/charthop/charthop.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "charthop", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/charthop/package.json b/components/charthop/package.json new file mode 100644 index 0000000000000..24879cd5541dd --- /dev/null +++ b/components/charthop/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/charthop", + "version": "0.0.1", + "description": "Pipedream ChartHop Components", + "main": "charthop.app.mjs", + "keywords": [ + "pipedream", + "charthop" + ], + "homepage": "https://pipedream.com/apps/charthop", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/chartmogul/README.md b/components/chartmogul/README.md index 1902b0d854688..b4d0d66c8a974 100644 --- a/components/chartmogul/README.md +++ b/components/chartmogul/README.md @@ -1,11 +1,11 @@ # Overview -With the ChartMogul API, you can build a variety of applications and -integrations to help you better understand and manage your customer data. Here -are some examples of what you can build: - -- A dashboard to visualize your customer data -- An integration with your billing system to automatically import customer data -- A tool to help you segment your customers based on their data -- A customer churn prediction tool -- A customer lifetime value calculation tool +ChartMogul is an analytics platform designed to help subscription businesses analyze their revenue, churn, and other key performance indicators (KPIs). By leveraging the ChartMogul API, you can import, manipulate, and retrieve customer subscription data to gain insights into the financial health of your business. When used with Pipedream, you can automate complex workflows, combine subscription data with external tools, and react in real-time to changes in your subscription metrics. + +# Example Use Cases + +- **Customer Lifecycle Event Tracking**: Sync customer subscription events from ChartMogul to a CRM like Salesforce or HubSpot. Trigger workflows in Pipedream when a new subscription is created, a customer upgrades or downgrades their plan, or cancels their subscription. This data can be used to automate follow-up tasks, customer support tickets, and personalized marketing campaigns. + +- **Real-Time Revenue Dashboards**: Create a real-time dashboard by sending ChartMogul revenue data to a Google Sheet using Pipedream's Google Sheets integration. Whenever there's a new invoice, payment, or refund, update the sheet to reflect changes in MRR (Monthly Recurring Revenue), ARR (Annual Recurring Revenue), or other custom metrics. Use this dashboard for immediate insights during team meetings or decision-making processes. + +- **Automated Financial Reporting**: Combine ChartMogul metrics with additional financial data from tools like QuickBooks or Xero for comprehensive reporting. Set up a Pipedream workflow to run at the end of each accounting period, pull the necessary data from these services and ChartMogul, and generate a consolidated financial report. This can greatly reduce the manual effort required during the financial close process. diff --git a/components/chartmogul/actions/update-customer/update-customer.mjs b/components/chartmogul/actions/update-customer/update-customer.mjs index d8ef49e512f19..e1388864a4a3b 100644 --- a/components/chartmogul/actions/update-customer/update-customer.mjs +++ b/components/chartmogul/actions/update-customer/update-customer.mjs @@ -3,7 +3,7 @@ import chartmogul from "../../chartmogul.app.mjs"; export default { key: "chartmogul-update-customer", name: "Update Customer", - version: "0.0.1", + version: "0.0.2", description: "Updates certain modifiable attributes of a `customer` object in your ChartMogul account. [See the docs here](https://dev.chartmogul.com/reference/update-a-customer)", type: "action", props: { @@ -28,6 +28,21 @@ export default { ], optional: true, }, + status: { + type: "string", + label: "Status", + description: "The new status of the customer.", + options: [ + "New Lead", + "Working Lead", + "Qualified Lead", + "Unqualified Lead", + "Active", + "Past Due", + "Cancelled", + ], + optional: true, + }, company: { propDefinition: [ chartmogul, @@ -97,6 +112,7 @@ export default { customerId, name, email, + status, company, country, state, @@ -113,6 +129,7 @@ export default { customerId, name, email, + status, company, country, state, diff --git a/components/chartmogul/package.json b/components/chartmogul/package.json index e39e7e7ce2aca..b4a0265bbfcdc 100644 --- a/components/chartmogul/package.json +++ b/components/chartmogul/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/chartmogul", - "version": "0.0.2", + "version": "0.0.3", "description": "Pipedream ChartMogul Components", "main": "chartmogul.app.js", "keywords": [ diff --git a/components/chaser/README.md b/components/chaser/README.md new file mode 100644 index 0000000000000..6ee9e1ee2b749 --- /dev/null +++ b/components/chaser/README.md @@ -0,0 +1,11 @@ +# Overview + +The Chaser API enables users to automate their accounts receivable process efficiently, making it possible to manage and follow up on invoices programmatically. By integrating Chaser with Pipedream, users can automate invoice reminders, synchronize financial data with other business applications, and generate detailed accounts receivable reports. These capabilities help businesses improve their cash flow and minimize the time spent on manual follow-ups. + +# Example Use Cases + +- **Automated Invoice Reminders**: Set up a workflow on Pipedream that triggers weekly, based on the payment due date of invoices. Use the Chaser API to send customized reminder emails to clients whose payments are overdue. This reduces the manual effort required in managing follow-ups and helps maintain consistent cash flow. + +- **Sync Invoices with Accounting Software**: Create a workflow that triggers whenever a new invoice is added in Chaser. Use this trigger to automatically copy the invoice details to your accounting software (like QuickBooks or Xero), ensuring that your financial records are always up-to-date without manual data entry. + +- **Generate and Send Financial Reports**: Implement a monthly workflow on Pipedream that uses the Chaser API to gather all receivables data and compile it into a comprehensive financial report. This report can then be automatically emailed to stakeholders using an email service like SendGrid or directly through SMTP steps available in Pipedream. This automation ensures that all pertinent parties are regularly updated with the latest financial statuses without any manual intervention. diff --git a/components/chaser/actions/create-customer/create-customer.mjs b/components/chaser/actions/create-customer/create-customer.mjs new file mode 100644 index 0000000000000..24fb601f29e06 --- /dev/null +++ b/components/chaser/actions/create-customer/create-customer.mjs @@ -0,0 +1,89 @@ +import chaser from "../../chaser.app.mjs"; + +export default { + key: "chaser-create-customer", + name: "Create Customer", + description: "Creates a new customer in Chaser. [See the documentation](https://openapi.chaserhq.com/docs/static/index.html)", + version: "0.0.1", + type: "action", + props: { + chaser, + externalId: { + type: "string", + label: "External ID", + description: "External ID of the customer", + }, + companyName: { + type: "string", + label: "Company Name", + description: "Company name of the customer", + }, + firstName: { + type: "string", + label: "First Name", + description: "First name of the customer", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the customer", + optional: true, + }, + emailAddress: { + type: "string", + label: "Email Address", + description: "Email address of the customer", + optional: true, + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "Phone number of the customer", + optional: true, + }, + additionalOptions: { + type: "object", + label: "Additional Options", + description: + "Additional parameters to send in the request. [See the documentation](https://openapi.chaserhq.com/docs/static/index.html) for available parameters. Values will be parsed as JSON where applicable.", + optional: true, + }, + }, + async run({ $ }) { + let additionalOptions = Object.fromEntries( + Object.entries(this.additionalOptions ?? {}).map(([ + key, + value, + ]) => { + // optional JSON parsing + try { + return [ + key, + JSON.parse(value), + ]; + } catch (e) { + return [ + key, + value, + ]; + } + }), + ); + + const response = await this.chaser.createCustomer({ + $, + data: { + external_id: this.externalId, + company_name: this.companyName, + contact_first_name: this.firstName, + contact_last_name: this.lastName, + contact_email_address: this.emailAddress, + phone_number: this.phoneNumber, + ...additionalOptions, + }, + }); + $.export("$summary", `Successfully created customer (ID: ${response.data.id})`); + return response; + }, +}; diff --git a/components/chaser/actions/create-invoice/create-invoice.mjs b/components/chaser/actions/create-invoice/create-invoice.mjs new file mode 100644 index 0000000000000..3ab3dc3893e3b --- /dev/null +++ b/components/chaser/actions/create-invoice/create-invoice.mjs @@ -0,0 +1,98 @@ +import chaser from "../../chaser.app.mjs"; + +export default { + key: "chaser-create-invoice", + name: "Create Invoice", + description: "Creates a new invoice in Chaser. [See the documentation](https://openapi.chaserhq.com/docs/static/index.html)", + version: "0.0.1", + type: "action", + props: { + chaser, + invoiceId: { + type: "string", + label: "Invoice ID", + description: "Invoice ID", + }, + invoiceNumber: { + type: "string", + label: "Invoice Number", + description: "Invoice Number", + }, + customerExternalId: { + propDefinition: [ + chaser, + "customerExternalId", + ], + }, + status: { + type: "string", + label: "Status", + description: "Invoice Status", + options: [ + "DRAFT", + "SUBMITTED", + "AUTHORISED", + "PAID", + "VOIDED", + "DELETED", + ], + }, + currencyCode: { + type: "string", + label: "Currency Code", + description: "A 3-letter currency code such as `USD`, `EUR` or `GBP`", + }, + amountDue: { + type: "string", + label: "Amount Due", + description: "Amount Due (number)", + }, + amountPaid: { + type: "string", + label: "Amount Paid", + description: "Amount Paid (number)", + }, + total: { + type: "string", + label: "Total", + description: "Total (number)", + }, + date: { + type: "string", + label: "Date", + description: "Invoice date (date-time string such as `2024-04-23T11:00:00Z`)", + }, + dueDate: { + type: "string", + label: "Due Date", + description: "Invoice due date (date-time string such as `2024-04-23T11:00:00Z`)", + }, + fullyPaidDate: { + type: "string", + label: "Fully Paid Date", + description: "Invoice fully paid date (date-time string such as `2024-04-23T11:00:00Z`)", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.chaser.createInvoice({ + $, + data: { + invoice_id: this.invoiceId, + invoice_number: this.invoiceNumber, + status: this.status, + currency_code: this.currencyCode, + amount_due: Number(this.amountDue), + amount_paid: Number(this.amountPaid), + total: Number(this.total), + date: this.date, + due_date: this.dueDate, + fully_paid_date: this.fullyPaidDate, + customer_external_id: this.customerExternalId, + }, + }); + + $.export("$summary", `Successfully created invoice (ID: ${response?.data?.id})`); + return response; + }, +}; diff --git a/components/chaser/chaser.app.mjs b/components/chaser/chaser.app.mjs new file mode 100644 index 0000000000000..0b07368e80089 --- /dev/null +++ b/components/chaser/chaser.app.mjs @@ -0,0 +1,65 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "chaser", + propDefinitions: { + customerExternalId: { + type: "string", + label: "Customer External ID", + description: "Select a customer, or provide a customer external ID.", + async options({ page }) { + const { data } = await this.listCustomers({ + page, + limit: 100, + }); + return data?.map((customer) => ({ + label: + `${customer.contact_first_name ?? ""} ${ + customer.contact_last_name ?? "" + }`.trim() || + customer.contact_email_address || + customer.external_id, + value: customer.external_id, + })); + }, + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.host}/v1`; + }, + async _makeRequest({ + $ = this, ...otherOpts + }) { + return axios($, { + ...otherOpts, + baseURL: this._baseUrl(), + auth: { + username: `${this.$auth.key}`, + password: `${this.$auth.secret}`, + }, + }); + }, + async listCustomers(args) { + return this._makeRequest({ + url: "/customers", + ...args, + }); + }, + async createCustomer(args) { + return this._makeRequest({ + method: "POST", + url: "/customers", + ...args, + }); + }, + async createInvoice(args) { + return this._makeRequest({ + method: "POST", + url: "/invoices", + ...args, + }); + }, + }, +}; diff --git a/components/chaser/package.json b/components/chaser/package.json new file mode 100644 index 0000000000000..1a36e145ce857 --- /dev/null +++ b/components/chaser/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/chaser", + "version": "0.1.0", + "description": "Pipedream Chaser Components", + "main": "chaser.app.mjs", + "keywords": [ + "pipedream", + "chaser" + ], + "homepage": "https://pipedream.com/apps/chaser", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.4" + } +} diff --git a/components/chat_api_for_whatsapp/package.json b/components/chat_api_for_whatsapp/package.json new file mode 100644 index 0000000000000..5e60ccc46ed0e --- /dev/null +++ b/components/chat_api_for_whatsapp/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/chat_api_for_whatsapp", + "version": "0.6.0", + "description": "Pipedream chat_api_for_whatsapp Components", + "main": "chat_api_for_whatsapp.app.mjs", + "keywords": [ + "pipedream", + "chat_api_for_whatsapp" + ], + "homepage": "https://pipedream.com/apps/chat_api_for_whatsapp", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/chat_data/actions/create-chatbot/create-chatbot.mjs b/components/chat_data/actions/create-chatbot/create-chatbot.mjs new file mode 100644 index 0000000000000..97eb82251709f --- /dev/null +++ b/components/chat_data/actions/create-chatbot/create-chatbot.mjs @@ -0,0 +1,57 @@ +import app from "../../chat_data.app.mjs"; + +export default { + key: "chat_data-create-chatbot", + name: "Create Chatbot", + description: "Create a chatbot with the specified properties. [See the documentation](https://www.chat-data.com/api-reference#tag/Chatbot-Operations/operation/chatbotCreate)", + version: "0.0.1", + type: "action", + props: { + app, + chatbotName: { + propDefinition: [ + app, + "chatbotName", + ], + }, + sourceText: { + propDefinition: [ + app, + "sourceText", + ], + }, + urlsToScrape: { + propDefinition: [ + app, + "urlsToScrape", + ], + }, + customBackend: { + propDefinition: [ + app, + "customBackend", + ], + }, + model: { + propDefinition: [ + app, + "model", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.createChatbot({ + $, + data: { + chatbotName: this.chatbotName, + sourceText: this.sourceText, + urlsToScrape: this.urlsToScrape, + customBackend: this.customBackend, + model: this.model, + }, + }); + $.export("$summary", `Successfully created Chatbot with ID '${response.chatbotId}'`); + return response; + }, +}; diff --git a/components/chat_data/actions/delete-chatbot/delete-chatbot.mjs b/components/chat_data/actions/delete-chatbot/delete-chatbot.mjs new file mode 100644 index 0000000000000..138dbc1ee6787 --- /dev/null +++ b/components/chat_data/actions/delete-chatbot/delete-chatbot.mjs @@ -0,0 +1,29 @@ +import app from "../../chat_data.app.mjs"; + +export default { + key: "chat_data-delete-chatbot", + name: "Delete Chatbot", + description: "Delete a chatbot with the specified ID. [See the documentation](https://www.chat-data.com/api-reference#tag/Chatbot-Operations/operation/chatbotDelete)", + version: "0.0.1", + type: "action", + props: { + app, + chatbotId: { + propDefinition: [ + app, + "chatbotId", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.deleteChatbot({ + $, + chatbotId: this.chatbotId, + }); + + $.export("$summary", `Successfully deleted Chatbot with ID '${this.chatbotId}'`); + + return response; + }, +}; diff --git a/components/chat_data/actions/get-chatbot-details/get-chatbot-details.mjs b/components/chat_data/actions/get-chatbot-details/get-chatbot-details.mjs new file mode 100644 index 0000000000000..8bbf8be34cc2d --- /dev/null +++ b/components/chat_data/actions/get-chatbot-details/get-chatbot-details.mjs @@ -0,0 +1,30 @@ +import app from "../../chat_data.app.mjs"; + +export default { + key: "chat_data-get-chatbot-details", + + name: "Get Chatbot Status", + description: "Get status of the Chatbot with the specified ID. [See the documentation](https://www.chat-data.com/api-reference#tag/Chatbot-Operations/operation/GetChatbotStatus)", + version: "0.0.1", + type: "action", + props: { + app, + chatbotId: { + propDefinition: [ + app, + "chatbotId", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.getChatbotStatus({ + $, + chatbotId: this.chatbotId, + }); + + $.export("$summary", `Successfully retrieved status of the Chatbot with ID '${this.chatbotId}'`); + + return response; + }, +}; diff --git a/components/chat_data/chat_data.app.mjs b/components/chat_data/chat_data.app.mjs new file mode 100644 index 0000000000000..5a9c02998f2c2 --- /dev/null +++ b/components/chat_data/chat_data.app.mjs @@ -0,0 +1,106 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "chat_data", + propDefinitions: { + chatbotId: { + type: "string", + label: "Employee ID", + description: "The employee ID", + async options() { + const response = await this.getChatbots(); + + const chatbotIds = response.chatbots; + + return chatbotIds.map(({ + chatbotId, chatbotName, + }) => ({ + value: chatbotId, + label: chatbotName, + })); + }, + }, + chatbotName: { + type: "string", + label: "Chatbot Name", + description: "Name of the Chatbot", + }, + sourceText: { + type: "string", + label: "Source Text", + description: "Text data for the chatbot, subject to character limits based on your plan. Relevant only if the model is custom-data-upload", + optional: true, + }, + urlsToScrape: { + type: "string[]", + label: "URLs to Scrape", + description: "A list of URLs is for text content extraction by Chat Data, i.e.: `https://www.chat-data.com`. Relevant only if the model is custom-data-upload", + optional: true, + }, + customBackend: { + type: "string", + label: "Custom Backend", + description: "The URL of a customized backend for the chatbot", + optional: true, + }, + model: { + type: "string", + label: "Model", + description: "The chatbot defaults to `custom-data-upload` if the model parameter is not provided", + options: constants.CHATBOT_MODELS, + }, + }, + methods: { + _baseUrl() { + return "https://api.chat-data.com/api/v2"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + async createChatbot(args = {}) { + return this._makeRequest({ + path: "/create-chatbot", + method: "post", + ...args, + }); + }, + async getChatbotStatus({ + chatbotId, ...args + }) { + return this._makeRequest({ + path: `/chatbot/status/${chatbotId}/`, + ...args, + }); + }, + async deleteChatbot({ + chatbotId, ...args + }) { + return this._makeRequest({ + path: `/delete-chatbot/${chatbotId}/`, + method: "delete", + ...args, + }); + }, + async getChatbots(args = {}) { + return this._makeRequest({ + path: "/get-chatbots", + ...args, + }); + }, + }, +}; diff --git a/components/chat_data/common/constants.mjs b/components/chat_data/common/constants.mjs new file mode 100644 index 0000000000000..87f8af5c966a2 --- /dev/null +++ b/components/chat_data/common/constants.mjs @@ -0,0 +1,8 @@ +export default { + CHATBOT_MODELS: [ + "custom-data-upload", + "medical-chat-human", + "medical-chat-vet", + "custom-model", + ], +}; diff --git a/components/chat_data/package.json b/components/chat_data/package.json new file mode 100644 index 0000000000000..624df3b499197 --- /dev/null +++ b/components/chat_data/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/chat_data", + "version": "0.1.0", + "description": "Pipedream Chat Data Components", + "main": "chat_data.app.mjs", + "keywords": [ + "pipedream", + "chat_data" + ], + "homepage": "https://pipedream.com/apps/chat_data", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/chatbot/README.md b/components/chatbot/README.md index 9f3af5d4189c2..2cf0e1d48da5f 100644 --- a/components/chatbot/README.md +++ b/components/chatbot/README.md @@ -1,13 +1,11 @@ # Overview -With the Chatbot API, you can build a variety of chatbots to improve customer -service or automate business processes. For example, you could build a chatbot -to: - -- Help customers find the right product -- Answer questions about products or services -- Schedule appointments -- Give directions or advice -- Check prices or inventory -- Provide customer support -- And much more! +Leverage the ChatBot API on Pipedream to automate conversations, streamline customer service, and connect chat functionality with various apps for rich, responsive interaction. With this API, you can programmatically send messages, manage chat histories, and implement chatbots that react to user input in real-time. By integrating with Pipedream, these capabilities can be augmented with thousands of apps, enabling seamless data flow and complex automations. + +# Example Use Cases + +- **Customer Support Ticketing Workflow**: Integrate ChatBot with a CRM like Salesforce or Zendesk on Pipedream. When a user raises an issue through the chatbot, automatically create a new support ticket in the CRM, assign it to the appropriate team, and send a confirmation message back through the chatbot, ensuring a punctual follow-up. + +- **E-commerce Order Update Notifications**: Connect ChatBot to an e-commerce platform such as Shopify. Set up a workflow where customers can inquire about their order status through the chatbot, triggering a lookup in Shopify. The chatbot then responds with the latest shipping details or order status, providing instant updates directly through the messaging interface. + +- **Event Registration and Reminder System**: Use ChatBot in tandem with Google Calendar on Pipedream. Design a bot that allows users to register for upcoming events via chat. Once registered, automate the creation of a calendar event and send reminders through the chatbot prior to the event, keeping attendees informed and engaged. diff --git a/components/chatbot_builder/README.md b/components/chatbot_builder/README.md new file mode 100644 index 0000000000000..f7c1f7d9363f2 --- /dev/null +++ b/components/chatbot_builder/README.md @@ -0,0 +1,11 @@ +# Overview + +The Chatbot Builder API allows you to create and manage customized chatbots that can interact with users in real-time. Within Pipedream's serverless platform, this API can be harnessed to build powerful workflows that react to messages, automate responses, and integrate with a plethora of other services. With Pipedream, you can set up event-driven processes, making it easy to manage chatbot activities, analyze conversations, and trigger actions in other apps based on chatbot interactions. + +# Example Use Cases + +- **Customer Support Automation**: Use the Chatbot Builder API to handle common customer inquiries on your site. When a customer message is recognized, Pipedream can trigger a workflow to fetch relevant account information from a CRM like Salesforce, and then send personalized assistance directly through the chatbot. + +- **Survey Distribution and Analysis**: Create a workflow where the chatbot distributes surveys to users. Pipedream can capture survey responses, push this data to a Google Sheet for analysis, and even trigger follow-up emails via SendGrid to thank participants or offer incentives. + +- **Real-time Order Updates**: Implement a system where customers can inquire about their order status through the chatbot. Pipedream can listen for these specific queries, pull order details from an e-commerce platform like Shopify, and deliver real-time updates back to the customer through the chatbot. diff --git a/components/chatbot_builder/actions/create-tag/create-tag.mjs b/components/chatbot_builder/actions/create-tag/create-tag.mjs new file mode 100644 index 0000000000000..0efd3026e5ca4 --- /dev/null +++ b/components/chatbot_builder/actions/create-tag/create-tag.mjs @@ -0,0 +1,32 @@ +import app from "../../chatbot_builder.app.mjs"; +import qs from "qs"; + +export default { + key: "chatbot_builder-create-tag", + name: "Create Tag", + description: "Creates a new tag in Chatbot Builder. [See the documentation](https://app.chatgptbuilder.io/api/swagger/#/accounts/createtag)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "name" + ] + }, + }, + async run({ $ }) { + const response = await this.app.createTag({ + $, + data: qs.stringify({ + name: this.name, + }), + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }); + $.export("$summary", `Successfully created a new tag named ${this.name}`); + return response; + }, +}; \ No newline at end of file diff --git a/components/chatbot_builder/actions/delete-tag/delete-tag.mjs b/components/chatbot_builder/actions/delete-tag/delete-tag.mjs new file mode 100644 index 0000000000000..89320a79850f4 --- /dev/null +++ b/components/chatbot_builder/actions/delete-tag/delete-tag.mjs @@ -0,0 +1,26 @@ +import app from "../../chatbot_builder.app.mjs"; + +export default { + key: "chatbot_builder-delete-tag", + name: "Delete Tag", + description: "Deletes a tag from Chatbot Builder. [See the documentation](https://app.chatgptbuilder.io/api/swagger/#/accounts/deletetag)", + version: "0.0.1", + type: "action", + props: { + app, + tagId: { + propDefinition: [ + app, + "tagId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.deleteTag({ + $, + tagId: this.tagId, + }); + $.export("$summary", `Successfully deleted tag with ID: ${this.tagId}`); + return response; + }, +}; diff --git a/components/chatbot_builder/actions/list-tags/list-tags.mjs b/components/chatbot_builder/actions/list-tags/list-tags.mjs new file mode 100644 index 0000000000000..d2aa050772a55 --- /dev/null +++ b/components/chatbot_builder/actions/list-tags/list-tags.mjs @@ -0,0 +1,19 @@ +import app from "../../chatbot_builder.app.mjs"; + +export default { + key: "chatbot_builder-list-tags", + name: "List Tags", + description: "Lists all tags in Chatbot Builder. [See the documentation](https://app.chatgptbuilder.io/api/swagger/#/accounts/getpagetags)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const tags = await this.app.getTags({ + $, + }); + $.export("$summary", `Retrieved ${tags.length} tags from page ${this.pageId}`); + return tags; + }, +}; diff --git a/components/chatbot_builder/chatbot_builder.app.mjs b/components/chatbot_builder/chatbot_builder.app.mjs index 879881354b714..f15c644d97868 100644 --- a/components/chatbot_builder/chatbot_builder.app.mjs +++ b/components/chatbot_builder/chatbot_builder.app.mjs @@ -1,11 +1,76 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "chatbot_builder", - propDefinitions: {}, + propDefinitions: { + name: { + type: "string", + label: "Tag Name", + description: "The name of the new tag", + }, + pageId: { + type: "string", + label: "Page ID", + description: "The ID of the page to get or delete tags from", + }, + tagId: { + type: "string", + label: "Tag ID", + description: "The ID of the tag", + async options() { + const tags = await this.getTags(); + + return tags.map(({ + id, name, + }) => ({ + value: id, + label: name, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://app.chatgptbuilder.io/api"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "X-ACCESS-TOKEN": `${this.$auth.api_access_token}`, + }, + }); + }, + async createTag(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/accounts/tags", + ...args, + }); + }, + async getTags(args = {}) { + return this._makeRequest({ + path: "/accounts/tags", + ...args, + }); + }, + async deleteTag({ + tagId, ...args + }) { + return this._makeRequest({ + method: "DELETE", + path: `/accounts/tags/${tagId}`, + ...args, + }); }, }, }; diff --git a/components/chatbot_builder/package.json b/components/chatbot_builder/package.json index 2db95b570325a..5afcf1080fc2c 100644 --- a/components/chatbot_builder/package.json +++ b/components/chatbot_builder/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/chatbot_builder", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Chatbot Builder Components", "main": "chatbot_builder.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0", + "qs": "^6.12.0" } -} \ No newline at end of file +} diff --git a/components/chatbotic/README.md b/components/chatbotic/README.md new file mode 100644 index 0000000000000..df7d0c1574c00 --- /dev/null +++ b/components/chatbotic/README.md @@ -0,0 +1,11 @@ +# Overview + +The Chatbotic API enables the creation and integration of chatbots with various platforms. With this API, you can automate conversations, craft responses based on user input, and manage bot interactions efficiently. When combined with Pipedream's capabilities, you can set up serverless workflows to trigger actions in other apps based on conversations, analyze chat data, and connect your chatbot with a vast array of services to enhance its functionality. + +# Example Use Cases + +- **Automated Customer Support**: Use Chatbotic to handle initial customer support queries. When a chatbot encounters a complex issue, use Pipedream to escalate the conversation to a human agent on a platform like Slack or Zendesk. + +- **E-commerce Bot Transactions**: Integrate Chatbotic with an e-commerce platform like Shopify. When a customer places an order via chat, Pipedream can capture the order details, process the transaction, and update inventory in real-time. + +- **Event Registration via Chat**: Set up a Chatbotic bot to handle event registrations. Pipedream can then add the registrant's details to a Google Sheet and send a confirmation email through an email service like SendGrid. diff --git a/components/chatbotic/package.json b/components/chatbotic/package.json index 0478be7cc66bc..5358edff30f8a 100644 --- a/components/chatbotic/package.json +++ b/components/chatbotic/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/chatbotkit/README.md b/components/chatbotkit/README.md new file mode 100644 index 0000000000000..3874a591c966e --- /dev/null +++ b/components/chatbotkit/README.md @@ -0,0 +1,11 @@ +# Overview + +ChatBotKit API empowers you to create and manage conversational experiences with ease. Within Pipedream, you can leverage this API to automate interactions, analyze message content, and enhance customer engagement by integrating with other apps. Think of ChatBotKit as the backbone of your chatbot logic, while Pipedream serves as the orchestrator, connecting your bot to a vast array of services, databases, and communication platforms. + +# Example Use Cases + +- **Customer Support Automation**: Automate customer inquiries by triggering a Pipedream workflow whenever a new message is received. Use ChatBotKit to analyze the content and provide automated responses or escalate to human agents. Integrate with a CRM like HubSpot to log conversations or follow-up tasks. + +- **Feedback Collection Bot**: Build a feedback loop into your service by triggering a workflow when a chat session ends. Use ChatBotKit to summarize the conversation, extract sentiment, and key points. Push this data into Google Sheets or Airtable for analysis and future improvements. + +- **Event-Based Notifications Bot**: Deploy a bot that sends notifications to users about upcoming events. Trigger a Pipedream workflow on a schedule to fetch event data from a source like Google Calendar, use ChatBotKit to craft personalized messages, and distribute them over your preferred messaging platform. diff --git a/components/chatfai/README.md b/components/chatfai/README.md new file mode 100644 index 0000000000000..bfa7f8323b59d --- /dev/null +++ b/components/chatfai/README.md @@ -0,0 +1,11 @@ +# Overview + +The ChatFAI API enables the creation of chatbots with capabilities ranging from answering FAQs to providing recommendations and conducting conversations. Leveraging this API within Pipedream allows for the automation of these conversational features across various platforms and services. With Pipedream's serverless approach, you can integrate the ChatFAI API to trigger actions, respond to events, and connect with numerous apps to enhance user engagement and streamline communication processes. + +# Example Use Cases + +- **Customer Support Automation**: Automate customer support by integrating the ChatFAI API with a CRM like Salesforce. When a new support ticket is created, use Pipedream to trigger a ChatFAI-driven conversation, providing instant, AI-generated responses to common queries, before escalating complex issues to human agents. + +- **Real-time Chat Translation**: Pair the ChatFAI API with a translation service like Google Translate in Pipedream. Use the API to understand the intent of a message in a chat application, translate it to a desired language, and deliver an appropriate response in the user's native language, facilitating seamless international communication. + +- **Social Media Engagement**: Enhance social media interaction by connecting the ChatFAI API with Twitter on Pipedream. Monitor tweets mentioning your brand, analyze sentiment, and automatically craft and post intelligent and personalized responses to user queries or comments, thereby increasing brand presence and user satisfaction. diff --git a/components/chatfly/README.md b/components/chatfly/README.md new file mode 100644 index 0000000000000..8050608483730 --- /dev/null +++ b/components/chatfly/README.md @@ -0,0 +1,11 @@ +# Overview + +The ChatFly API allows developers to integrate chat functionalities into their applications, making it easier to build custom chat solutions or enhance existing communication systems. On Pipedream, leveraging the ChatFly API can automate interactions, analyze chat data, or sync with other services to create a more dynamic user experience. + +# Example Use Cases + +- **Customer Support Automation**: Automatically create and manage customer support tickets from chat messages using the ChatFly API on Pipedream. Connect ChatFly with a ticketing system like Zendesk, where each chat message tagged as a support request triggers the creation of a new ticket, streamlining customer support workflows. + +- **Chat Analytics Pipeline**: Analyze chat data by connecting ChatFly to Google Sheets or BigQuery on Pipedream. Every chat message can trigger a workflow that processes and logs data into Google Sheets for real-time analysis or into BigQuery for more complex data analysis, helping in understanding user interactions and improving service delivery. + +- **Real-Time Notifications System**: Enhance communication by setting up real-time alerts using the ChatFly API on Pipedream. Connect ChatFly with Slack to send customized alerts to specific channels or users whenever key phrases or words are detected in a chat, enabling immediate response to important messages. diff --git a/components/chatfly/actions/send-message/send-message.mjs b/components/chatfly/actions/send-message/send-message.mjs new file mode 100644 index 0000000000000..67f43f90398bd --- /dev/null +++ b/components/chatfly/actions/send-message/send-message.mjs @@ -0,0 +1,45 @@ +import chatfly from "../../chatfly.app.mjs"; + +export default { + key: "chatfly-send-message", + name: "Send Message", + description: "Dispatches a text message to a specified group or individual in Chatfly.", + version: "0.0.1", + type: "action", + props: { + chatfly, + botId: { + propDefinition: [ + chatfly, + "botId", + ], + }, + sessionId: { + propDefinition: [ + chatfly, + "sessionId", + ({ botId }) => ({ + botId, + }), + ], + }, + message: { + propDefinition: [ + chatfly, + "message", + ], + }, + }, + async run({ $ }) { + const response = await this.chatfly.dispatchMessage({ + $, + data: { + bot_id: this.botId, + message: this.message, + session_id: this.sessionId, + }, + }); + $.export("$summary", "Message dispatched successfully"); + return response; + }, +}; diff --git a/components/chatfly/chatfly.app.mjs b/components/chatfly/chatfly.app.mjs new file mode 100644 index 0000000000000..25b33229e5824 --- /dev/null +++ b/components/chatfly/chatfly.app.mjs @@ -0,0 +1,86 @@ +import { axios } from "@pipedream/platform"; +import { prepareSessionLabel } from "./common/utils.mjs"; + +export default { + type: "app", + app: "chatfly", + propDefinitions: { + botId: { + type: "string", + label: "Bot ID", + description: "The ID of the bot to send the message to.", + async options() { + const data = await this.listBots(); + + return data.map(({ + id: value, bot_name: label, + }) => ({ + label, + value, + })); + }, + }, + message: { + type: "string", + label: "Message", + description: "The message to send.", + }, + sessionId: { + type: "string", + label: "Session ID", + description: "The session ID for the message. To initiate a new session, use a unique string in a Custom Expression, for example: `8g12h`", + async options({ botId }) { + const data = await this.listSessions({ + params: { + bot_id: botId, + }, + }); + + return data.map(({ + session_id: value, chat_history_response, + }) => ({ + label: prepareSessionLabel(chat_history_response), + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://backend.chatfly.co/api"; + }, + _headers() { + return { + "CHATFLY-API-KEY": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path = "/", ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + dispatchMessage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/chat/get-streaming-response", + ...opts, + }); + }, + listBots(opts = {}) { + return this._makeRequest({ + path: "/bot", + ...opts, + }); + }, + listSessions(opts = {}) { + return this._makeRequest({ + path: "/chat/history", + ...opts, + }); + }, + }, +}; diff --git a/components/chatfly/common/utils.mjs b/components/chatfly/common/utils.mjs new file mode 100644 index 0000000000000..646098290a789 --- /dev/null +++ b/components/chatfly/common/utils.mjs @@ -0,0 +1,5 @@ +export const prepareSessionLabel = (messages) => { + return `${messages[messages.length - 1].content.substring(0, 60)} ${(messages[messages.length - 1].content.length > 60) + ? "..." + : ""}`; +}; diff --git a/components/chatfly/package.json b/components/chatfly/package.json new file mode 100644 index 0000000000000..453c9d3179c95 --- /dev/null +++ b/components/chatfly/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/chatfly", + "version": "0.1.0", + "description": "Pipedream ChatFly Components", + "main": "chatfly.app.mjs", + "keywords": [ + "pipedream", + "chatfly" + ], + "homepage": "https://pipedream.com/apps/chatfly", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} diff --git a/components/chatforma/README.md b/components/chatforma/README.md index 35739e7649d6b..3c2b9e93ecaef 100644 --- a/components/chatforma/README.md +++ b/components/chatforma/README.md @@ -1,5 +1,11 @@ # Overview -With the Chatforma API, you can easily build chatbots for your business or -personal use. You can also use the API to build other applications such as a -customer support system or a personal assistant. +Chatforma is an API that opens up a world of possibilities in chatbot creation and management, allowing businesses to automate interactions with customers across various platforms. With Chatforma, you can design, deploy, and track chatbots that handle customer service, feedback collection, and more, streamlining your communication processes and providing valuable analytical insights. Leveraging Pipedream’s integration capabilities, you can efficiently tie Chatforma into a multitude of systems and services, creating custom workflows that enhance your chatbot's utility and extend its reach. + +# Example Use Cases + +- **Customer Support Ticket Creation**: Automate the creation of support tickets in a system like Zendesk or Jira whenever a customer raises an issue via a Chatforma-powered chatbot. This ensures that customer inquiries are logged correctly and can be tracked for timely resolution. + +- **Feedback Collection and Analysis**: After a chatbot interaction, use Chatforma to prompt for customer feedback and push the responses to a Google Sheet or Airtable base. Combine this with sentiment analysis tools like Google’s Natural Language API to gauge customer satisfaction and identify areas for improvement. + +- **E-Commerce Order Updates**: Integrate Chatforma with Shopify or WooCommerce to notify customers about order updates. When an order status changes, trigger a chatbot message with the updated status, providing timely and personalized updates directly through the chat interface. diff --git a/components/chatforma/package.json b/components/chatforma/package.json new file mode 100644 index 0000000000000..8af0ff9ad485b --- /dev/null +++ b/components/chatforma/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/chatforma", + "version": "0.6.0", + "description": "Pipedream chatforma Components", + "main": "chatforma.app.mjs", + "keywords": [ + "pipedream", + "chatforma" + ], + "homepage": "https://pipedream.com/apps/chatforma", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/chatfuel_dashboard_api_/README.md b/components/chatfuel_dashboard_api_/README.md index 626e2bb185534..854746329b206 100644 --- a/components/chatfuel_dashboard_api_/README.md +++ b/components/chatfuel_dashboard_api_/README.md @@ -1,12 +1,11 @@ # Overview -With the Chatfuel API, you can build a wide variety of applications and -chatbots. Here are some examples: - -- A chatbot that helps customers order food from a restaurant -- A chatbot that helps users book hotels -- A chatbot that helps patients schedule appointments with doctors -- A chatbot that helps people buy tickets to events -- A chatbot that helps people find local businesses -- A chatbot that helps people get news and weather updates -- A chatbot that helps people find information about products +Chatfuel’s Dashboard API opens a realm of possibilities for automating and streamlining chatbot interactions and management. With this API, you can programmatically update content, retrieve analytics, manage users, and automate messaging. This empowers you to dynamically adjust chat flows based on user behavior or external triggers, analyze user interactions for insights, and personalize the chat experience at scale. + +# Example Use Cases + +- **Automated Content Updates**: Use Pipedream to schedule regular content updates in Chatfuel. Connect to a CMS to fetch fresh content and automatically push it to your Chatfuel bot, keeping your users engaged with the latest information. + +- **Dynamic User Segmentation**: Implement a workflow where user responses from Chatfuel are sent to a CRM system. Based on the data, use Pipedream to segment users into different Chatfuel attributes for targeted messaging campaigns, ensuring personalized communication. + +- **Real-time Analytics Reporting**: Create a Pipedream workflow that fetches chat metrics from Chatfuel and sends this data to Google Sheets or a BI tool. Schedule this as a daily summary, providing stakeholders with up-to-date insights on user engagement and bot performance. diff --git a/components/chatlayer/README.md b/components/chatlayer/README.md new file mode 100644 index 0000000000000..3649e35f2124b --- /dev/null +++ b/components/chatlayer/README.md @@ -0,0 +1,11 @@ +# Overview + +The Chatlayer API by Sinch allows developers to integrate advanced chatbot functionalities into their applications, enabling AI-driven conversations across various channels like web, mobile, and social media platforms. With Pipedream, you can automate workflows involving the Chatlayer API to facilitate seamless interactions, gather user insights, and streamline communications. This integration can help in automating responses, analyzing sentiment, and managing user data in real-time, thus enhancing user engagement through personalized and context-aware dialogues. + +# Example Use Cases + +- **Customer Support Automation**: Automate the initial stages of customer support by integrating Chatlayer with Slack using Pipedream. When a customer initiates a conversation on your platform, use Chatlayer to interpret the query and provide an automated response. If the query is too complex, escalate it to a human agent in a specific Slack channel, ensuring that high-priority issues are addressed swiftly. + +- **Feedback Collection and Analysis**: Connect Chatlayer to Google Sheets via Pipedream to automate the collection and analysis of customer feedback. Use Chatlayer's conversational AI to solicit feedback through your app or website. Automatically store responses in Google Sheets, where you can analyze the data to derive actionable insights and identify areas for improvement in your services or products. + +- **Event Registration Bot**: Use Chatlayer integrated with Eventbrite via Pipedream to manage event registrations through automated chats. Set up a Chatlayer bot to handle inquiries about event details, register participants directly through the conversation, and send event tickets or confirmation emails automatically. This workflow simplifies event management by reducing manual tasks and improving attendee experience through immediate communication. diff --git a/components/chatlayer/chatlayer.app.mjs b/components/chatlayer/chatlayer.app.mjs new file mode 100644 index 0000000000000..d3754cd914f9e --- /dev/null +++ b/components/chatlayer/chatlayer.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "chatlayer", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/chatlayer/package.json b/components/chatlayer/package.json new file mode 100644 index 0000000000000..2bf0d011c9a60 --- /dev/null +++ b/components/chatlayer/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/chatlayer", + "version": "0.0.1", + "description": "Pipedream Chatlayer Components", + "main": "chatlayer.app.mjs", + "keywords": [ + "pipedream", + "chatlayer" + ], + "homepage": "https://pipedream.com/apps/chatlayer", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/chatpdf/README.md b/components/chatpdf/README.md new file mode 100644 index 0000000000000..0f9d7681be22e --- /dev/null +++ b/components/chatpdf/README.md @@ -0,0 +1,11 @@ +# Overview + +The ChatPDF API enables you to convert chat histories from various messaging platforms into PDFs. This utility can be a game-changer for archiving conversations, legal documentation, or creating backups of critical chats. With Pipedream, you can automate the conversion process and integrate it with countless apps to streamline chat management tasks. Think of pulling chat data from messaging apps, converting them to PDF, and then saving or sharing them—all set up to run on autopilot. + +# Example Use Cases + +- **Automated Chat Backups**: Set up a workflow that listens for new messages in a Slack channel or WhatsApp chat, forwards them to the ChatPDF API, and saves the resulting PDF to Google Drive or Dropbox. This ensures you always have up-to-date backups of important conversations. + +- **Customer Support Ticket Archiving**: Convert completed customer support chats into PDFs for archiving. After a support session on Intercom or Zendesk, a Pipedream workflow can trigger the PDF conversion and attach the file to the corresponding support ticket or email it to the customer for their records. + +- **Event Conversation Compilation**: After an event, you might want to compile all the chat history from a Discord server or a Slack channel into a single PDF document. With Pipedream, you can create a workflow that triggers at a specific time, grabs the chat history, sends it to ChatPDF for conversion, and then shares the PDF with attendees via email or a shared workspace. diff --git a/components/chatpdf/actions/add-pdf-via-url/add-pdf-via-url.mjs b/components/chatpdf/actions/add-pdf-via-url/add-pdf-via-url.mjs new file mode 100644 index 0000000000000..4022ab01adc4d --- /dev/null +++ b/components/chatpdf/actions/add-pdf-via-url/add-pdf-via-url.mjs @@ -0,0 +1,41 @@ +import app from "../../chatpdf.app.mjs"; + +export default { + key: "chatpdf-add-pdf-via-url", + name: "Add PDF Via URL", + description: "Adds a PDF from a publicly accessible URL to ChatPDF, returning a source ID for interactions. [See the documentation](https://www.chatpdf.com/docs/api/backend)", + version: "0.0.1", + type: "action", + props: { + app, + url: { + type: "string", + label: "PDF URL", + description: "The URL of the publicly accessible PDF", + }, + }, + methods: { + addPdfByUrl(args = {}) { + return this.app.post({ + path: "/sources/add-url", + ...args, + }); + }, + }, + async run({ $ }) { + const { + addPdfByUrl, + url, + } = this; + + const response = await addPdfByUrl({ + $, + data: { + url, + }, + }); + + $.export("$summary", `Successfully added PDF with source ID \`${response.sourceId}\``); + return response; + }, +}; diff --git a/components/chatpdf/actions/chat-with-pdf/chat-with-pdf.mjs b/components/chatpdf/actions/chat-with-pdf/chat-with-pdf.mjs new file mode 100644 index 0000000000000..0561275e1111a --- /dev/null +++ b/components/chatpdf/actions/chat-with-pdf/chat-with-pdf.mjs @@ -0,0 +1,57 @@ +import app from "../../chatpdf.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "chatpdf-chat-with-pdf", + name: "Chat With PDF", + description: "Sends messages to interact with a specific PDF using its source ID. Can handle single or multiple messages for complex queries. [See the documentation](https://www.chatpdf.com/docs/api/backend)", + version: "0.0.1", + type: "action", + props: { + app, + sourceId: { + type: "string", + label: "Source ID", + description: "The source ID of the PDF in chatpdf", + }, + messages: { + type: "string[]", + label: "Messages", + description: "An array of messages to interact with the PDF. Each row should be set as a JSON string. Eg: `{\"role\": \"user\", \"content\": \"How much is the world?\"}`, `{\"role\": \"assistant\", \"content\": \"The world is 10 dollars.\"}` and `{\"role\": \"user\", \"content\": \"Where can I buy it?\"}`", + }, + referenceSources: { + type: "boolean", + label: "Reference Sources", + description: "Includes references to the PDF pages that were used to generate the response.", + optional: true, + }, + }, + methods: { + chatWithPdf(args = {}) { + return this.app.post({ + path: "/chats/message", + ...args, + }); + }, + }, + async run({ $ }) { + const { + chatWithPdf, + sourceId, + messages, + referenceSources, + } = this; + + const response = await chatWithPdf({ + $, + data: { + sourceId, + messages: utils.parseArray(messages), + referenceSources, + }, + }); + + $.export("$summary", "Successfully sent messages to interact with PDF"); + return response; + }, +}; diff --git a/components/chatpdf/actions/delete-pdf/delete-pdf.mjs b/components/chatpdf/actions/delete-pdf/delete-pdf.mjs new file mode 100644 index 0000000000000..f36076ab8b78f --- /dev/null +++ b/components/chatpdf/actions/delete-pdf/delete-pdf.mjs @@ -0,0 +1,44 @@ +import app from "../../chatpdf.app.mjs"; + +export default { + key: "chatpdf-delete-pdf", + name: "Delete PDF", + description: "Deletes one or more PDFs from ChatPDF using their source IDs. [See the documentation](https://www.chatpdf.com/docs/api/backend)", + version: "0.0.1", + type: "action", + props: { + app, + sources: { + type: "string[]", + label: "Source IDs", + description: "An array of source IDs of the PDFs to delete", + }, + }, + methods: { + deletePdfs(args = {}) { + return this.app.post({ + path: "/sources/delete", + ...args, + }); + }, + }, + async run({ $ }) { + const { + deletePdfs, + sources, + } = this; + + const response = await deletePdfs({ + $, + data: { + sources, + }, + }); + + $.export("$summary", "Successfully deleted PDFs"); + + return response || { + success: true, + }; + }, +}; diff --git a/components/chatpdf/chatpdf.app.mjs b/components/chatpdf/chatpdf.app.mjs index bec4521d1664b..b82c99e19e3f4 100644 --- a/components/chatpdf/chatpdf.app.mjs +++ b/components/chatpdf/chatpdf.app.mjs @@ -1,11 +1,34 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "chatpdf", - propDefinitions: {}, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `https://api.chatpdf.com/v1${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Content-Type": "application/json", + "x-api-key": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + const config = { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }; + return axios($, config); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/chatpdf/common/utils.mjs b/components/chatpdf/common/utils.mjs new file mode 100644 index 0000000000000..eceb6a9167739 --- /dev/null +++ b/components/chatpdf/common/utils.mjs @@ -0,0 +1,50 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function isJson(value) { + value = + typeof(value) !== "string" + ? JSON.stringify(value) + : value; + + try { + value = JSON.parse(value); + } catch (e) { + return false; + } + + return typeof(value) === "object" && value !== null; +} + +function valueToObject(value) { + if (!isJson(value)) { + return value; + } + return JSON.parse(value); +} + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid JSON array object"); + } +} + +export default { + parseArray: (value) => parseArray(value).map(valueToObject), +}; diff --git a/components/chatpdf/package.json b/components/chatpdf/package.json index ed0808d25f49f..5e9b39e404d54 100644 --- a/components/chatpdf/package.json +++ b/components/chatpdf/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/chatpdf", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream ChatPDF Components", "main": "chatpdf.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" } -} \ No newline at end of file +} diff --git a/components/chatra/README.md b/components/chatra/README.md index faf1f6bc64e17..c52cd70f46172 100644 --- a/components/chatra/README.md +++ b/components/chatra/README.md @@ -1,8 +1,11 @@ # Overview -With Chatra, you can build a variety of applications, including: +The Chatra API enables the automation of customer support processes by allowing the programmatic sending of messages, retrieval of chat history, and management of ongoing conversations. With Pipedream's serverless platform, you can harness this API to integrate real-time chat capabilities with other applications, set up automated responses based on specific triggers, and analyze customer interactions to enhance support strategies. -- A chatbot that interacts with your users -- A customer service chat platform -- A live chat platform for your website -- A messaging app for your team or organization +# Example Use Cases + +- **Automated Customer Support Workflow**: Trigger a Pipedream workflow when a new message arrives in Chatra, analyze the content using sentiment analysis (with an app like MonkeyLearn), and respond automatically with predefined responses or escalate to a human agent based on the sentiment score. + +- **E-commerce Order Support**: Integrate Chatra with Shopify using Pipedream. When a customer inquires about an order, automatically fetch order details from Shopify and provide real-time updates within the chat, enhancing customer experience and offloading work from support agents. + +- **Support Ticket Integration**: Connect Chatra to a ticketing system like Zendesk on Pipedream. Whenever a chat in Chatra is tagged with 'support', automatically create a support ticket in Zendesk with chat details, streamlining the issue resolution process. diff --git a/components/chatra/package.json b/components/chatra/package.json new file mode 100644 index 0000000000000..90b49fb090166 --- /dev/null +++ b/components/chatra/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/chatra", + "version": "0.6.0", + "description": "Pipedream chatra Components", + "main": "chatra.app.mjs", + "keywords": [ + "pipedream", + "chatra" + ], + "homepage": "https://pipedream.com/apps/chatra", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/chatrace/README.md b/components/chatrace/README.md index b940e5af9541a..75c251e966bf8 100644 --- a/components/chatrace/README.md +++ b/components/chatrace/README.md @@ -1,12 +1,11 @@ # Overview -With the Chatrace API you can build a wide variety of applications that can -utilise the power of chatrooms. Below are some examples of what you can build: - -- A chatroom application that can be used by people to communicate with each - other in real time. -- A Location sharing application that can be used to share your location with - others in a chatroom. -- An application that can be used to find new friends and chat with them. -- A gaming application that can be used to play games with other people in a - chatroom. +The Chatrace API is a toolkit for building advanced chatbot solutions. With this API, you can streamline customer interactions, automate responses, and integrate your chat systems with various platforms and data sources. Whether you're enhancing customer support, leading marketing campaigns, or gathering insights from user conversations, the Chatrace API, when married with Pipedream's capabilities, allows for extensive automation and integration possibilities. + +# Example Use Cases + +- **Customer Support Automation**: Automate responses to common queries using Chatrace. Set up a workflow on Pipedream that listens for specific keywords or questions and triggers Chatrace to deliver the appropriate response, reducing the workload on human agents. + +- **E-commerce Shopping Assistant**: Create a chatbot that aids users in making purchase decisions. Integrate Chatrace with a product database on Pipedream to provide real-time stock updates and recommendations based on user preferences, streamlining the shopping experience. + +- **Feedback Collection and Analysis**: Implement a workflow where Chatrace gathers user feedback and sends it to Pipedream. From there, connect to a data analysis tool like Google Sheets or a CRM like HubSpot to categorize and analyze customer sentiment, helping improve products and services. diff --git a/components/chatsonic/README.md b/components/chatsonic/README.md new file mode 100644 index 0000000000000..231058b9ad3ea --- /dev/null +++ b/components/chatsonic/README.md @@ -0,0 +1,11 @@ +# Overview + +The Chatsonic API by Writesonic unleashes the power of conversational AI, enabling you to craft chatbots, automate customer service, and generate natural language responses for a variety of use cases. With Chatsonic, you can design workflows on Pipedream that respond to events, messages, or prompts with human-like text, leverage contextually aware conversations, and even tap into real-time data like weather or news for enriched interactions. + +# Example Use Cases + +- **Automate Customer Support**: Connect Chatsonic to a customer support ticketing system like Zendesk. When a new ticket is created, trigger a workflow that uses Chatsonic to generate an initial response, aiming to resolve common queries without human intervention. + +- **Content Generation for Social Media**: Pair Chatsonic with Twitter's API. Automate content creation by triggering a workflow whenever you need to post a new tweet. Chatsonic can generate creative and context-relevant posts, which Pipedream can then automatically publish to your Twitter account. + +- **Event-Driven Email Responses**: Integrate Chatsonic with an email service like SendGrid. Set up a Pipedream workflow that listens for incoming emails and uses Chatsonic to draft replies based on the email content. This can provide quick, personalized responses to common inquiries, feedback, or even out-of-office messages. diff --git a/components/chatwork/README.md b/components/chatwork/README.md new file mode 100644 index 0000000000000..013ab02fd5fce --- /dev/null +++ b/components/chatwork/README.md @@ -0,0 +1,11 @@ +# Overview + +The Chatwork API on Pipedream allows you to automate conversations, manage contacts, and streamline notifications within Chatwork. It’s a great way to integrate Chatwork with other apps and services, creating efficient workflows that save time and reduce manual tasks. With Pipedream, you can trigger actions based on messages, send alerts to a Chatwork room, or even sync Chatwork data with CRM systems or databases. + +# Example Use Cases + +- **Automated Project Updates**: Send automatic updates to a Chatwork room when a new task is added or completed in a project management tool like Trello or Asana. This keeps the team in sync and reduces the need for manual updates. + +- **Customer Support Alerts**: Create a workflow where new customer support tickets in Zendesk or Help Scout trigger a message to a dedicated Chatwork room, ensuring that support issues are communicated instantly to your team. + +- **Scheduled Reports Delivery**: Set up a daily or weekly report from data sources like Google Sheets or Airtable to be formatted and sent into a Chatwork room, providing teams with regular insights without manual intervention. diff --git a/components/checkout_com/checkout_com.app.mjs b/components/checkout_com/checkout_com.app.mjs new file mode 100644 index 0000000000000..8816d80d56296 --- /dev/null +++ b/components/checkout_com/checkout_com.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "checkout_com", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/checkout_com/package.json b/components/checkout_com/package.json new file mode 100644 index 0000000000000..c04047736f979 --- /dev/null +++ b/components/checkout_com/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/checkout_com", + "version": "0.0.1", + "description": "Pipedream Checkout.com Components", + "main": "checkout_com.app.mjs", + "keywords": [ + "pipedream", + "checkout_com" + ], + "homepage": "https://pipedream.com/apps/checkout_com", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/checkvist/README.md b/components/checkvist/README.md index 97460c5a2f019..47e030cc1a0bb 100644 --- a/components/checkvist/README.md +++ b/components/checkvist/README.md @@ -1,14 +1,11 @@ # Overview -Checkvist is a to-do list and task management app that lets you organize and -track your work. With the Checkvist API, you can build apps that integrate with -Checkvist and add new features or extend existing ones. - -Some examples of what you can build with the Checkvist API: - -- A task management app that lets users create and manage their tasks in - Checkvist -- A to-do list app that lets users add and track their to-dos in Checkvist -- An integration that lets users add tasks from Checkvist to another task - management app -- A plugin that adds new features to Checkvist +The Checkvist API allows for the creation and management of tasks and lists in a way that streamlines productivity and enhances organizational workflows. With the API, you can automate task updates, synchronize lists with other platforms, and trigger actions based on task completion. Utilizing Pipedream, these capabilities can be expanded by orchestrating complex workflows that connect Checkvist with a multitude of other apps and services, enabling seamless data flow and event-driven automation. + +# Example Use Cases + +- **Task Completion to Communication Alert**: Automate notifications to a Slack channel when a task in Checkvist is marked as complete. This keeps team members informed in real-time and celebrates progress without manual updates. + +- **List Synchronization with Google Sheets**: Sync Checkvist lists to a Google Sheet for advanced reporting and analysis. Whenever a new list is created or updated in Checkvist, append the relevant data to a Google Sheet, allowing for historical data tracking and shareability with stakeholders not using Checkvist. + +- **Email Digest of Daily Tasks**: Set up a daily workflow that compiles tasks due today from Checkvist, formats them into an email, and sends a digest to your inbox using SendGrid. This ensures that you start the day with a clear agenda, directly in your email, without needing to log into Checkvist. diff --git a/components/checkvist/actions/create-list-item/create-list-item.mjs b/components/checkvist/actions/create-list-item/create-list-item.mjs new file mode 100644 index 0000000000000..766898d83d170 --- /dev/null +++ b/components/checkvist/actions/create-list-item/create-list-item.mjs @@ -0,0 +1,79 @@ +import checkvist from "../../checkvist.app.mjs"; +import { STATUS_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "checkvist-create-list-item", + name: "Create List Item", + description: "Creates a new list item within a specified list. [See the documentation](https://checkvist.com/auth/api)", + version: "0.0.1", + type: "action", + props: { + checkvist, + listId: { + propDefinition: [ + checkvist, + "listId", + ], + }, + content: { + type: "string", + label: "Content", + description: "Block of text containing items to add. Indentations indicate nested list items.", + }, + parentId: { + propDefinition: [ + checkvist, + "parentId", + ({ listId }) => ({ + listId, + }), + ], + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "An array of tags.", + optional: true, + }, + dueDate: { + type: "string", + label: "Due Date", + description: "Due for the task, in Checkvist's smart syntax format.", + optional: true, + }, + position: { + type: "integer", + label: "Position", + description: "1-based position of the task (omit to add to the end of the list).", + optional: true, + }, + status: { + type: "string", + label: "Status", + description: "Task status", + options: STATUS_OPTIONS, + optional: true, + }, + }, + async run({ $ }) { + const response = await this.checkvist.createListItem({ + $, + listId: this.listId, + data: { + task: { + content: this.content, + parent_id: this.parentId || 0, + tags: parseObject(this.tags)?.join(","), + due_date: this.dueDate, + position: this.position, + status: this.status, + }, + }, + }); + + $.export("$summary", `Successfully created a new list item in list with ID ${this.listId}`); + return response; + }, +}; diff --git a/components/checkvist/actions/create-multiple-list-items/create-multiple-list-items.mjs b/components/checkvist/actions/create-multiple-list-items/create-multiple-list-items.mjs new file mode 100644 index 0000000000000..68d94932e3a69 --- /dev/null +++ b/components/checkvist/actions/create-multiple-list-items/create-multiple-list-items.mjs @@ -0,0 +1,77 @@ +import checkvist from "../../checkvist.app.mjs"; +import { + SEPARATE_LINE_OPTIONS, STATUS_OPTIONS, +} from "../../common/constants.mjs"; + +export default { + key: "checkvist-create-multiple-list-items", + name: "Create Multiple List Items", + description: "Enables creation of several list items at once from a block of text. Indentations in the text indicate nested list items. [See the documentation](https://checkvist.com/auth/api)", + version: "0.0.1", + type: "action", + props: { + checkvist, + listId: { + propDefinition: [ + checkvist, + "listId", + ], + }, + itemsContent: { + type: "string", + label: "Content Items", + description: "list items in the same format, as supported by [Checkvist's import function](https://checkvist.com/help#import).", + }, + parentId: { + propDefinition: [ + checkvist, + "parentId", + ({ listId }) => ({ + listId, + }), + ], + optional: true, + }, + position: { + type: "integer", + label: "Position", + description: "1-based position of the task (omit to add to the end of the list).", + optional: true, + }, + parseTasks: { + type: "boolean", + label: "Parse Tasks", + description: "If true, recognize **^due** and **#tags** syntax in imported list items", + }, + separateWithEmptyLine: { + type: "string", + label: "Separate With Empty Line", + description: "Select value for List items separator.", + options: SEPARATE_LINE_OPTIONS, + }, + status: { + type: "string", + label: "Status", + description: "Task status", + options: STATUS_OPTIONS, + optional: true, + }, + }, + async run({ $ }) { + const response = await this.checkvist.createMultipleListItems({ + $, + listId: this.listId, + data: { + import_content: this.itemsContent, + parent_id: this.parentId, + position: this.position, + parse_tasks: this.parseTasks, + separate_with_empty_line: this.separateWithEmptyLine, + status: this.status, + }, + }); + + $.export("$summary", "Successfully created multiple list items"); + return response; + }, +}; diff --git a/components/checkvist/actions/create-new-list/create-new-list.mjs b/components/checkvist/actions/create-new-list/create-new-list.mjs new file mode 100644 index 0000000000000..bf2cb176a63d1 --- /dev/null +++ b/components/checkvist/actions/create-new-list/create-new-list.mjs @@ -0,0 +1,35 @@ +import checkvist from "../../checkvist.app.mjs"; + +export default { + key: "checkvist-create-new-list", + name: "Create New List", + description: "Creates a new list in Checkvist. [See the documentation](https://checkvist.com/auth/api)", + version: "0.0.1", + type: "action", + props: { + checkvist, + name: { + type: "string", + label: "List Name", + description: "Name of the new list to be created", + }, + public: { + type: "boolean", + label: "Public", + description: "true for checklist which can be accessed in read-only mode by anyone. Access to such checklists doesn't require authentication.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.checkvist.createList({ + $, + data: { + name: this.name, + public: this.public, + }, + }); + + $.export("$summary", `Successfully created a new list: ${this.name}`); + return response; + }, +}; diff --git a/components/checkvist/checkvist.app.mjs b/components/checkvist/checkvist.app.mjs index 58dca2a8a09c7..ec6fd50f67e55 100644 --- a/components/checkvist/checkvist.app.mjs +++ b/components/checkvist/checkvist.app.mjs @@ -1,11 +1,101 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "checkvist", - propDefinitions: {}, + propDefinitions: { + listId: { + type: "string", + label: "List ID", + description: "Select a list to monitor for new items", + async options() { + const lists = await this.getLists({ + params: { + skip_stats: true, + }, + }); + return lists.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + parentId: { + type: "string", + label: "Parent task ID", + description: "Empty for root-level tasks", + async options({ listId }) { + const items = await this.getListItems({ + listId, + }); + return items.map(({ + id: value, content: label, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://checkvist.com"; + }, + _auth() { + return { + username: `${this.$auth.username}`, + password: `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + auth: this._auth(), + ...opts, + }); + }, + getLists(opts = {}) { + return this._makeRequest({ + path: "/checklists.json", + ...opts, + }); + }, + getListItems({ + listId, ...opts + }) { + return this._makeRequest({ + path: `/checklists/${listId}/tasks.json`, + ...opts, + }); + }, + createList(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/checklists.json", + ...opts, + }); + }, + createListItem({ + listId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/checklists/${listId}/tasks.json`, + ...opts, + }); + }, + createMultipleListItems({ + listId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/checklists/${listId}/import.json`, + ...opts, + }); }, }, }; diff --git a/components/checkvist/common/constants.mjs b/components/checkvist/common/constants.mjs new file mode 100644 index 0000000000000..406c51135bcf9 --- /dev/null +++ b/components/checkvist/common/constants.mjs @@ -0,0 +1,25 @@ +export const STATUS_OPTIONS = [ + { + label: "Open", + value: "0", + }, + { + label: "Closed", + value: "1", + }, + { + label: "Invalidated", + value: "2", + }, +]; + +export const SEPARATE_LINE_OPTIONS = [ + { + label: "Separate with empty line", + value: "singleItem", + }, + { + label: "One item per line", + value: "multipleItems", + }, +]; diff --git a/components/checkvist/common/utils.mjs b/components/checkvist/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/checkvist/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/checkvist/package.json b/components/checkvist/package.json new file mode 100644 index 0000000000000..11c99b63033b3 --- /dev/null +++ b/components/checkvist/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/checkvist", + "version": "0.1.0", + "description": "Pipedream Checkvist Components", + "main": "checkvist.app.mjs", + "keywords": [ + "pipedream", + "checkvist" + ], + "homepage": "https://pipedream.com/apps/checkvist", + "author": "Pipedream (https://pipedream.com/)", + "gitHead": "e12480b94cc03bed4808ebc6b13e7fdb3a1ba535", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/checkvist/sources/common/base.mjs b/components/checkvist/sources/common/base.mjs new file mode 100644 index 0000000000000..70fe935687f2a --- /dev/null +++ b/components/checkvist/sources/common/base.mjs @@ -0,0 +1,69 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import checkvist from "../../checkvist.app.mjs"; + +export default { + props: { + checkvist, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + getArgs() { + return {}; + }, + async emitEvent(maxResults = false) { + const lastId = this._getLastId(); + const fn = this.getFunction(); + + const response = await fn({ + ...this.getArgs(), + params: { + order: "id:desc", + }, + }); + + let responseArray = []; + for (const item of response) { + if (item.id <= lastId) break; + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastId(responseArray[0].id); + } + + for (const item of responseArray.reverse()) { + const summary = this.getSummary(item); + this.$emit(item, { + id: item.id, + summary: summary.length > 40 + ? `${summary.slice(0, 39)}...` + : summary, + ts: Date.parse(item.created_at), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/checkvist/sources/new-list-item/new-list-item.mjs b/components/checkvist/sources/new-list-item/new-list-item.mjs new file mode 100644 index 0000000000000..479fd1087ed01 --- /dev/null +++ b/components/checkvist/sources/new-list-item/new-list-item.mjs @@ -0,0 +1,36 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "checkvist-new-list-item", + name: "New List Item Added", + description: "Emit new event when a new list item is added in a selected list.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + listId: { + propDefinition: [ + common.props.checkvist, + "listId", + ], + }, + }, + methods: { + ...common.methods, + getFunction() { + return this.checkvist.getListItems; + }, + getArgs() { + return { + listId: this.listId, + }; + }, + getSummary(item) { + return `New Item: ${item.content}`; + }, + }, + sampleEmit, +}; diff --git a/components/checkvist/sources/new-list-item/test-event.mjs b/components/checkvist/sources/new-list-item/test-event.mjs new file mode 100644 index 0000000000000..f9704b6259bcd --- /dev/null +++ b/components/checkvist/sources/new-list-item/test-event.mjs @@ -0,0 +1,21 @@ +export default { + "id": 66743402, + "parent_id": 0, + "checklist_id": 910790, + "status": 0, + "position": 9, + "tasks": [], + "update_line": "created by Username", + "updated_at": "2024/11/01 15:37:54 +0000", + "created_at": "2024/11/01 15:37:54 +0000", + "due": null, + "content": "Root task", + "collapsed": false, + "comments_count": 0, + "assignee_ids": [], + "details": {}, + "link_ids": [], + "backlink_ids": [], + "tags": {}, + "tags_as_text": "" +} \ No newline at end of file diff --git a/components/checkvist/sources/new-list/new-list.mjs b/components/checkvist/sources/new-list/new-list.mjs new file mode 100644 index 0000000000000..04141a62cdc3a --- /dev/null +++ b/components/checkvist/sources/new-list/new-list.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "checkvist-new-list", + name: "New List Created", + description: "Emit new event when a new list is created in your Checkvist account.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.checkvist.getLists; + }, + getSummary(list) { + return `New list: ${list.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/checkvist/sources/new-list/test-event.mjs b/components/checkvist/sources/new-list/test-event.mjs new file mode 100644 index 0000000000000..53ab1208d8ab0 --- /dev/null +++ b/components/checkvist/sources/new-list/test-event.mjs @@ -0,0 +1,20 @@ +export default { + "id": 910780, + "name": "Introduction to Checkvist", + "updated_at": "2024/10/31 18:49:45 +0000", + "public": false, + "options": 2, + "created_at": "2024/10/31 18:49:45 +0000", + "markdown?": true, + "archived": false, + "read_only": false, + "user_count": 1, + "user_updated_at": "2024/10/31 18:49:45 +0000", + "related_task_ids": null, + "percent_completed": 0, + "task_count": 44, + "task_completed": 0, + "item_count": 68, + "tags": {}, + "tags_as_text": "" +} \ No newline at end of file diff --git a/components/cheddar/README.md b/components/cheddar/README.md new file mode 100644 index 0000000000000..68c6c53550d81 --- /dev/null +++ b/components/cheddar/README.md @@ -0,0 +1,11 @@ +# Overview + +The Cheddar API provides developers with the ability to manage subscription billing and track customer usage data. In Pipedream, you can leverage the Cheddar API to automate billing operations, synchronize customer data across platforms, and respond to events like payment successes or failures. Use HTTP requests to integrate Cheddar's features into your Pipedream workflows, seamlessly connecting with other apps available on Pipedream's platform. + +# Example Use Cases + +- **Automate Customer Invoicing**: Set up a workflow that triggers monthly to generate invoices for customers with active subscriptions. Integrate with the Cheddar API to fetch subscription details and usage, create invoices, and send them out via email using an email service like SendGrid in Pipedream. + +- **Monitor Payment Failures**: Build a workflow that listens for payment failure webhooks from Cheddar. When a payment fails, you can automatically reach out to the customer with a reminder via Twilio's SMS service, or update your CRM with the payment issue for follow-up. + +- **Sync New Subscribers to a Mailing List**: When a new customer subscribes, trigger a workflow that captures the event via Cheddar's webhook, extracts the subscriber's information, and adds them to a mailing list in Mailchimp for future marketing campaigns. diff --git a/components/chimp_rewriter/README.md b/components/chimp_rewriter/README.md new file mode 100644 index 0000000000000..90eebdb0dd704 --- /dev/null +++ b/components/chimp_rewriter/README.md @@ -0,0 +1,11 @@ +# Overview + +The Chimp Rewriter API allows you to leverage advanced article spinning and rewriting capabilities within your Pipedream workflows. With this API, you can automatically generate unique versions of text for content creation, SEO strategies, or any application where textual uniqueness is valuable. By integrating the Chimp Rewriter API in Pipedream, you can create automated processes that include content analysis, translation, and rephrasing, augmenting your content management and distribution systems. + +# Example Use Cases + +- **Content Refresh Automation**: Trigger a Pipedream workflow when blog posts on a WordPress site get old. Use Chimp Rewriter API to rewrite the content, ensuring freshness and SEO relevance, then automatically update the WordPress post with the revised text. + +- **SEO Optimization Pipeline**: Combine Chimp Rewriter with a keyword analysis tool like Google Search Console. Extract underperforming content from your site, spin it for better keyword optimization with Chimp Rewriter, and republish to improve search rankings. Monitor performance with the Search Console app in Pipedream. + +- **Social Media Content Generation**: Spin multiple unique descriptions for a product from a single source description. Then, use Pipedream to distribute these variations across social media platforms like Twitter, LinkedIn, or Facebook to test which version garners more engagement and conversions. diff --git a/components/chimp_rewriter/chimp_rewriter.app.mjs b/components/chimp_rewriter/chimp_rewriter.app.mjs new file mode 100644 index 0000000000000..3238335cfad6a --- /dev/null +++ b/components/chimp_rewriter/chimp_rewriter.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "chimp_rewriter", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/chimp_rewriter/package.json b/components/chimp_rewriter/package.json new file mode 100644 index 0000000000000..883d38b1fd13b --- /dev/null +++ b/components/chimp_rewriter/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/chimp_rewriter", + "version": "0.0.1", + "description": "Pipedream Chimp Rewriter Components", + "main": "chimp_rewriter.app.mjs", + "keywords": [ + "pipedream", + "chimp_rewriter" + ], + "homepage": "https://pipedream.com/apps/chimp_rewriter", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/chmeetings/README.md b/components/chmeetings/README.md new file mode 100644 index 0000000000000..58c366fcb5ece --- /dev/null +++ b/components/chmeetings/README.md @@ -0,0 +1,11 @@ +# Overview + +The ChMeetings API allows for the management of church management tasks such as the coordination of events, member communication, and reporting. By integrating it with Pipedream, you can automate these processes, syncing data across platforms, and triggering actions based on event updates or member activities. Pipedream's serverless platform provides a potent way to react to ChMeetings events, connect to other services, and streamline church management workflows. + +# Example Use Cases + +- **Automated Event Reminder Emails**: Using the ChMeetings API, set up a workflow on Pipedream that triggers a sequence of reminder emails through an email service such as SendGrid whenever a new church event is approaching. Leverage Pipedream's scheduled tasks to time these reminders strategically. + +- **New Member Welcome Process**: Kick off an automated welcome sequence when a new member is added to ChMeetings. This could involve adding the member to a mailing list in Mailchimp, sending out a personalized welcome message, and creating a task in a project management tool like Trello to follow up with the new member. + +- **Attendance Reporting to Google Sheets**: After each event, use Pipedream to capture attendance data from ChMeetings and log it in a Google Sheet for future analysis and reporting. This workflow could also include conditional logic to flag members who have been absent for a certain number of events, triggering a follow-up action. diff --git a/components/chmeetings/package.json b/components/chmeetings/package.json index 9777b846770fd..951cd4a7968b1 100644 --- a/components/chmeetings/package.json +++ b/components/chmeetings/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/chucknorris/README.md b/components/chucknorris/README.md index 2b7f04d26d875..d16b3f94fb788 100644 --- a/components/chucknorris/README.md +++ b/components/chucknorris/README.md @@ -1,11 +1,11 @@ # Overview -With the ChuckNorris.io API, you can build a wide range of applications and -integrations. Here are just a few examples: - -- A joke generator that fetches random Chuck Norris jokes and displays them on - your website or app -- A Slack bot that posts a Chuck Norris joke in your channel every day -- A Twitter bot that tweets a random Chuck Norris joke every hour -- An Alexa skill that tells a Chuck Norris joke when you ask it -- A web or mobile app that lets users search for Chuck Norris jokes by keyword +The ChuckNorris.io API offers a humorous touch by providing a wealth of Chuck Norris jokes categorized by themes such as `animal`, `dev`, or `food`. It's a fun API to integrate when you want to lighten the mood or give your audience a chuckle. Using Pipedream's serverless platform, you can seamlessly invoke this API and incorporate jokes into notifications, social media posts, or even as a response within chat applications. Pipedream's capacity to connect multiple apps and create complex workflows makes it a great playground for automating the distribution of these jokes across various platforms. + +# Example Use Cases + +- **Slack Chuckle Break**: Set up a recurring workflow that pushes a random Chuck Norris joke to a designated Slack channel every Friday at 3 pm. This can act as a digital watercooler, boosting team morale at the end of the week. + +- **Email Fun Facts**: Create an email campaign where subscribers receive a daily Chuck Norris joke. Use Pipedream's ability to trigger workflows on a schedule, fetch a joke from ChuckNorris.io, and integrate with an email service like SendGrid to distribute the daily chuckle. + +- **Twitter Chuckle Bot**: Deploy a Twitter bot that posts a Chuck Norris joke whenever a user mentions your bot in a tweet. Listen for events using Pipedream's Twitter integration, retrieve a joke from ChuckNorris.io, and post it as a reply, all in real-time. diff --git a/components/chucknorris/package.json b/components/chucknorris/package.json new file mode 100644 index 0000000000000..c1bc06916e836 --- /dev/null +++ b/components/chucknorris/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/chucknorris", + "version": "0.6.0", + "description": "Pipedream chucknorris Components", + "main": "chucknorris.app.mjs", + "keywords": [ + "pipedream", + "chucknorris" + ], + "homepage": "https://pipedream.com/apps/chucknorris", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/cinc/README.md b/components/cinc/README.md new file mode 100644 index 0000000000000..0e8bb6fc77c37 --- /dev/null +++ b/components/cinc/README.md @@ -0,0 +1,11 @@ +# Overview + +The CINC (Commissions Inc) API provides a suite of tools tailored for real estate professionals seeking to enhance lead generation, manage clients, and streamline operations. By leveraging the CINC API on Pipedream, users can create automated workflows to sync leads, manage contacts, and trigger custom real-time actions based on client interactions and data changes. Pipedream's serverless platform allows for seamless integration between CINC and various other services, enabling agents and agencies to improve efficiency and stay ahead in the competitive real estate market. + +# Example Use Cases + +- **Automated Lead Syncing with Google Sheets**: Sync new leads from CINC to a Google Sheet in real-time. Whenever a lead is created in CINC, Pipedream triggers a workflow that appends the lead's details to a designated Google Sheet. This can help in maintaining an organized list for further analysis and follow-ups. + +- **Real-Time SMS Notifications for New Leads**: Send SMS notifications via Twilio when new leads register on CINC. Set up a Pipedream workflow to detect new leads in CINC and use Twilio to send a tailored message to an agent's phone, ensuring immediate engagement with potential clients. + +- **Dynamic Email Campaigns with SendGrid**: Trigger email campaigns from SendGrid in response to lead status updates in CINC. When a lead's status changes, Pipedream can initiate an email sequence through SendGrid, providing timely and relevant content to nurture leads through the sales funnel. diff --git a/components/cinc/actions/create-lead/create-lead.mjs b/components/cinc/actions/create-lead/create-lead.mjs new file mode 100644 index 0000000000000..d9afb6371845d --- /dev/null +++ b/components/cinc/actions/create-lead/create-lead.mjs @@ -0,0 +1,101 @@ +import cinc from "../../cinc.app.mjs"; + +export default { + key: "cinc-create-lead", + name: "Create New Lead", + description: "This component creates a new lead in Cinc. [See the documentation](https://public.cincapi.com/v2/docs/#post-site-leads)", + version: "0.0.1", + type: "action", + props: { + cinc, + firstName: { + propDefinition: [ + cinc, + "firstName", + ], + }, + lastName: { + propDefinition: [ + cinc, + "lastName", + ], + }, + email: { + propDefinition: [ + cinc, + "email", + ], + }, + cellphone: { + propDefinition: [ + cinc, + "cellphone", + ], + }, + status: { + propDefinition: [ + cinc, + "status", + ], + }, + source: { + propDefinition: [ + cinc, + "source", + ], + }, + medianListingPrice: { + propDefinition: [ + cinc, + "medianListingPrice", + ], + }, + averageListingPrice: { + propDefinition: [ + cinc, + "averageListingPrice", + ], + }, + isBuyerLead: { + propDefinition: [ + cinc, + "isBuyerLead", + ], + }, + isSellerLead: { + propDefinition: [ + cinc, + "isSellerLead", + ], + }, + }, + async run({ $ }) { + const response = await this.cinc.createLead({ + $, + data: { + info: { + contact: { + first_name: this.firstName, + last_name: this.lastName, + email: this.email, + phone_numbers: { + cell_phone: this.cellphone, + }, + }, + status: this.status, + source: this.source, + buyer: this.medianListingPrice || this.averageListingPrice + ? { + median_price: this.medianListingPrice && +this.medianListingPrice, + average_price: this.averageListingPrice && +this.averageListingPrice, + } + : undefined, + is_buyer: this.isBuyerLead, + is_seller: this.isSellerLead, + }, + }, + }); + $.export("$summary", `New lead added with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/cinc/actions/get-lead/get-lead.mjs b/components/cinc/actions/get-lead/get-lead.mjs new file mode 100644 index 0000000000000..f852a6f008f7e --- /dev/null +++ b/components/cinc/actions/get-lead/get-lead.mjs @@ -0,0 +1,26 @@ +import cinc from "../../cinc.app.mjs"; + +export default { + key: "cinc-get-lead", + name: "Get Lead", + description: "Retrieves a lead by ID in CINC. [See the documentation](https://public.cincapi.com/v2/docs/#get-site-leads-lead_id-2)", + version: "0.0.1", + type: "action", + props: { + cinc, + leadId: { + propDefinition: [ + cinc, + "leadId", + ], + }, + }, + async run({ $ }) { + const { lead } = await this.cinc.getLead({ + $, + leadId: this.leadId, + }); + $.export("$summary", `Successfully retrieved lead with ID ${this.leadId}`); + return lead; + }, +}; diff --git a/components/cinc/cinc.app.mjs b/components/cinc/cinc.app.mjs index cbd2ef277c7c2..8c696a81ebf22 100644 --- a/components/cinc/cinc.app.mjs +++ b/components/cinc/cinc.app.mjs @@ -1,11 +1,125 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "cinc", - propDefinitions: {}, + propDefinitions: { + leadId: { + type: "string", + label: "Lead Identifier", + description: "The identifier for the lead", + async options({ page }) { + const limit = constants.DEFAULT_LIMIT; + const { leads } = await this.listLeads({ + params: { + limit, + offset: page * limit, + }, + }); + return leads?.map(({ + id: value, info, + }) => ({ + value, + label: `${info.contact.first_name} ${info.contact.last_name}`, + })) || []; + }, + }, + firstName: { + type: "string", + label: "First Name", + description: "First name of the lead", + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the lead", + }, + email: { + type: "string", + label: "Email", + description: "Email address of the lead", + }, + cellphone: { + type: "string", + label: "Cellphone", + description: "Cellphone number of the lead", + }, + status: { + type: "string", + label: "Status", + description: "Status of the lead", + optional: true, + }, + source: { + type: "string", + label: "Source", + description: "Source of the lead", + optional: true, + }, + medianListingPrice: { + type: "string", + label: "Median Listing Price", + description: "Median listing price of the lead", + optional: true, + }, + averageListingPrice: { + type: "string", + label: "Average Listing Price", + description: "Average listing price of the lead", + optional: true, + }, + isBuyerLead: { + type: "boolean", + label: "Is Buyer Lead", + description: "Whether the lead is a buyer lead or not", + optional: true, + }, + isSellerLead: { + type: "boolean", + label: "Is Seller Lead", + description: "Whether the lead is a seller lead or not", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://public.cincapi.com/v2/site"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + getLead({ + leadId, ...opts + }) { + return this._makeRequest({ + path: `/leads/${leadId}`, + ...opts, + }); + }, + listLeads(opts = {}) { + return this._makeRequest({ + path: "/leads", + ...opts, + }); + }, + createLead(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/leads", + ...opts, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/cinc/common/constants.mjs b/components/cinc/common/constants.mjs new file mode 100644 index 0000000000000..6414d992bb568 --- /dev/null +++ b/components/cinc/common/constants.mjs @@ -0,0 +1,5 @@ +const DEFAULT_LIMIT = 50; + +export default { + DEFAULT_LIMIT, +}; diff --git a/components/cinc/package.json b/components/cinc/package.json index bcc9e06ca30e6..02ef87d2b5864 100644 --- a/components/cinc/package.json +++ b/components/cinc/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/cinc", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream CINC Components", "main": "cinc.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" } -} \ No newline at end of file +} diff --git a/components/cinc/sources/common/base.mjs b/components/cinc/sources/common/base.mjs new file mode 100644 index 0000000000000..66616036f0090 --- /dev/null +++ b/components/cinc/sources/common/base.mjs @@ -0,0 +1,54 @@ +import cinc from "../../cinc.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + cinc, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run() { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + const limit = constants.DEFAULT_LIMIT; + const params = { + limit, + offset: 0, + }; + let total = 0; + do { + const { leads } = await this.cinc.listLeads({ + params, + }); + for (const lead of leads) { + const ts = Date.parse(lead.info.updated_date); + if (ts > lastTs) { + maxTs = Math.max(ts, maxTs); + const meta = this.generateMeta(lead); + this.$emit(lead, meta); + } + } + total = leads?.length; + params.offset += limit; + } while (total === limit); + + this._setLastTs(maxTs); + }, +}; diff --git a/components/cinc/sources/lead-details-updated/lead-details-updated.mjs b/components/cinc/sources/lead-details-updated/lead-details-updated.mjs new file mode 100644 index 0000000000000..5eaac747e6f3a --- /dev/null +++ b/components/cinc/sources/lead-details-updated/lead-details-updated.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "cinc-lead-details-updated", + name: "Lead Details Updated", + description: "Emit new event when a lead's information is updated in Cinc", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + generateMeta(lead) { + const ts = Date.parse(lead.info.updated_date); + return { + id: `${lead.id}-${ts}`, + summary: `Lead Updated ${lead.id}`, + ts, + }; + }, + }, +}; diff --git a/components/cinc/sources/new-lead-created/new-lead-created.mjs b/components/cinc/sources/new-lead-created/new-lead-created.mjs new file mode 100644 index 0000000000000..187c290b78e3b --- /dev/null +++ b/components/cinc/sources/new-lead-created/new-lead-created.mjs @@ -0,0 +1,21 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "cinc-new-lead-created", + name: "New Lead Created", + description: "Emit new event when a new lead is added in Cinc.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + generateMeta(lead) { + return { + id: lead.id, + summary: `New Lead ${lead.id}`, + ts: Date.parse(lead.info.updated_date), + }; + }, + }, +}; diff --git a/components/cincopa/README.md b/components/cincopa/README.md index a34dfd0c59640..aa8475f012eea 100644 --- a/components/cincopa/README.md +++ b/components/cincopa/README.md @@ -1,8 +1,11 @@ # Overview -Here are just a few examples of things you can build using the Cincopa API: +The Cincopa API enables users to elevate their media content management by automating tasks and integrating with other services. With Cincopa, you can manipulate galleries, upload media, extract analytics, and manage your media assets in a programmatic way. When paired with Pipedream, this API becomes a powerhouse for creating seamless workflows that boost productivity and enhance content delivery, offering a rich set of triggers and actions to automate media operations. -1. A website with a video gallery -2. A website with an image gallery -3. A website with a podcast -4. A website with a blog +# Example Use Cases + +- **Automated Media Gallery Creation for Events**: When a new event is scheduled in your Google Calendar, Pipedream can trigger a workflow that automatically creates a new media gallery in Cincopa, populating it with default images and videos relevant to the event. + +- **Dynamic Content Updates on CMS**: Sync updates from Cincopa to your Content Management System (CMS) like WordPress. Whenever a new media file is uploaded to Cincopa, Pipedream can push that content to your CMS, keeping your website fresh and up-to-date without manual intervention. + +- **Marketing Analytics Reporting**: Compile and send regular media engagement reports by connecting Cincopa to a data visualization tool like Google Sheets. Each time Cincopa updates media analytics, Pipedream can append this data to a sheet, enabling easy tracking and analysis of viewer interactions over time. diff --git a/components/cincopa/actions/add-assets-to-gallery/add-assets-to-gallery.mjs b/components/cincopa/actions/add-assets-to-gallery/add-assets-to-gallery.mjs new file mode 100644 index 0000000000000..c267056af23e0 --- /dev/null +++ b/components/cincopa/actions/add-assets-to-gallery/add-assets-to-gallery.mjs @@ -0,0 +1,63 @@ +import app from "../../cincopa.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "cincopa-add-assets-to-gallery", + name: "Add Assets to Gallery", + description: "Adds an asset or a list of assets to an existing gallery. [See the documentation](https://www.cincopa.com/media-platform/api-documentation-v2#gallery.add_item)", + version: "0.0.1", + type: "action", + props: { + app, + fid: { + propDefinition: [ + app, + "fid", + ], + }, + rid: { + propDefinition: [ + app, + "rid", + ], + }, + insertPosition: { + type: "string", + label: "Insert Position", + description: "Add the assets at the top or the bottom of the gallery. Options: top or bottom (default is `bottom`)", + optional: true, + options: [ + "top", + "bottom", + ], + }, + }, + methods: { + addAssetToGallery(args = {}) { + return this.app._makeRequest({ + path: "/gallery.add_item.json", + ...args, + }); + }, + }, + async run({ $ }) { + const { + addAssetToGallery, + fid, + rid, + insertPosition, + } = this; + + const response = await addAssetToGallery({ + $, + params: { + fid, + rid: utils.parseArray(rid).join(","), + insert_position: insertPosition, + }, + }); + + $.export("$summary", "Successfully added assets to gallery"); + return response; + }, +}; diff --git a/components/cincopa/actions/create-gallery/create-gallery.mjs b/components/cincopa/actions/create-gallery/create-gallery.mjs new file mode 100644 index 0000000000000..8330e9f608d2f --- /dev/null +++ b/components/cincopa/actions/create-gallery/create-gallery.mjs @@ -0,0 +1,57 @@ +import app from "../../cincopa.app.mjs"; + +export default { + key: "cincopa-create-gallery", + name: "Create Gallery", + description: "Creates a new gallery, returning the new gallery FID (unique ID). [See the documentation](https://www.cincopa.com/media-platform/api-documentation-v2#gallery.create)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + type: "string", + label: "Name", + description: "Name of the gallery", + }, + description: { + type: "string", + label: "Description", + description: "Description of the gallery", + optional: true, + }, + template: { + type: "string", + label: "Template", + description: "Set the gallery skin to this template", + optional: true, + }, + }, + methods: { + createGallery(args = {}) { + return this.app._makeRequest({ + path: "/gallery.create.json", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createGallery, + name, + description, + template, + } = this; + + const response = await createGallery({ + $, + params: { + name, + description, + template, + }, + }); + + $.export("$summary", `Successfully created gallery with FID \`${response.fid}\``); + return response; + }, +}; diff --git a/components/cincopa/actions/upload-asset-from-url/upload-asset-from-url.mjs b/components/cincopa/actions/upload-asset-from-url/upload-asset-from-url.mjs new file mode 100644 index 0000000000000..51edb7d9069ae --- /dev/null +++ b/components/cincopa/actions/upload-asset-from-url/upload-asset-from-url.mjs @@ -0,0 +1,57 @@ +import app from "../../cincopa.app.mjs"; + +export default { + key: "cincopa-upload-asset-from-url", + name: "Upload Asset From URL", + description: "Upload an asset from an input URL to a Cincopa gallery. [See the documentation](https://www.cincopa.com/media-platform/api-documentation-v2#asset_upload_from_url)", + version: "0.0.1", + type: "action", + props: { + app, + input: { + type: "string", + label: "Input URL", + description: "Input URL for the source asset to upload", + }, + fid: { + propDefinition: [ + app, + "fid", + ], + }, + type: { + type: "string", + label: "Type", + description: "Type of the attached asset", + optional: true, + }, + }, + methods: { + uploadAssetFromUrl(args = {}) { + return this.app._makeRequest({ + path: "/asset.upload_from_url.json", + ...args, + }); + }, + }, + async run({ $ }) { + const { + uploadAssetFromUrl, + input, + fid, + type, + } = this; + + const response = await uploadAssetFromUrl({ + $, + params: { + input, + fid, + type, + }, + }); + + $.export("$summary", `Successfully uploaded asset from URL with status ID \`${response.status_id}\``); + return response; + }, +}; diff --git a/components/cincopa/cincopa.app.mjs b/components/cincopa/cincopa.app.mjs index 1dfaebdd98c49..1b58ceeb2b666 100644 --- a/components/cincopa/cincopa.app.mjs +++ b/components/cincopa/cincopa.app.mjs @@ -1,11 +1,82 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "cincopa", - propDefinitions: {}, + propDefinitions: { + fid: { + type: "string", + label: "Gallery FID", + description: "Gallery FID to add the assets", + async options({ page }) { + const { galleries } = await this.listGalleries({ + params: { + page: page + 1, + }, + }); + return galleries.map(({ + fid: value, name: label, + }) => ({ + value, + label, + })); + }, + }, + rid: { + type: "string[]", + label: "Asset RIDs", + description: "List of RIDs (assets id) to be added", + optional: true, + useQuery: true, + async options({ + query: search, page, + }) { + const { items } = await this.listAssets({ + params: { + search, + page: page + 1, + }, + }); + return items.map(({ + rid: value, filename: label, + }) => ({ + value, + label, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getAuthParams(params) { + return { + ...params, + api_token: this.$auth.api_token, + }; + }, + _makeRequest({ + $ = this, path, params, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + params: this.getAuthParams(params), + }); + }, + listGalleries(args = {}) { + return this._makeRequest({ + path: "/gallery.list.json", + ...args, + }); + }, + listAssets(args = {}) { + return this._makeRequest({ + path: "/asset.list.json", + ...args, + }); }, }, }; diff --git a/components/cincopa/common/constants.mjs b/components/cincopa/common/constants.mjs new file mode 100644 index 0000000000000..3a9e619bdc79c --- /dev/null +++ b/components/cincopa/common/constants.mjs @@ -0,0 +1,11 @@ +const BASE_URL = "https://api.cincopa.com"; +const VERSION_PATH = "/v2"; +const SECURITY_KEY = "securityKey"; +const DEFAULT_MAX = 600; + +export default { + BASE_URL, + VERSION_PATH, + SECURITY_KEY, + DEFAULT_MAX, +}; diff --git a/components/cincopa/common/utils.mjs b/components/cincopa/common/utils.mjs new file mode 100644 index 0000000000000..d9df0aa935aef --- /dev/null +++ b/components/cincopa/common/utils.mjs @@ -0,0 +1,28 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +export default { + parseArray, +}; diff --git a/components/cincopa/package.json b/components/cincopa/package.json new file mode 100644 index 0000000000000..3b7209b16efd0 --- /dev/null +++ b/components/cincopa/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/cincopa", + "version": "0.0.1", + "description": "Pipedream Cincopa Components", + "main": "cincopa.app.mjs", + "keywords": [ + "pipedream", + "cincopa" + ], + "homepage": "https://pipedream.com/apps/cincopa", + "author": "Pipedream (https://pipedream.com/)", + "dependencies": { + "@pipedream/platform": "^1.6.5", + "uuid": "^8.3.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/components/cincopa/sources/asset-uploaded-instant/asset-uploaded-instant.mjs b/components/cincopa/sources/asset-uploaded-instant/asset-uploaded-instant.mjs new file mode 100644 index 0000000000000..208636c9c4ed2 --- /dev/null +++ b/components/cincopa/sources/asset-uploaded-instant/asset-uploaded-instant.mjs @@ -0,0 +1,25 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; + +export default { + ...common, + key: "cincopa-asset-uploaded-instant", + name: "New Asset Uploaded (Instant)", + description: "Emit new event when a new asset is uploaded. [See the documentation](https://www.cincopa.com/media-platform/api-documentation-v2#webhook.set)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return events.ASSET_UPLOADED; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Asset: ${resource.filename}`, + ts: Date.parse(resource.modified), + }; + }, + }, +}; diff --git a/components/cincopa/sources/common/events.mjs b/components/cincopa/sources/common/events.mjs new file mode 100644 index 0000000000000..7d040342a67f0 --- /dev/null +++ b/components/cincopa/sources/common/events.mjs @@ -0,0 +1,15 @@ +export default { + GALLERY_ALL: "gallery.*", + GALLERY_CREATED: "gallery.created", + GALLERY_UPDATED: "gallery.updated", + GALLERY_DELETED: "gallery.deleted", + GALLERY_CLAIM: "gallery.claim", + GALLERY_SYNC: "gallery.sync", + GALLERY_SYNCED: "gallery.synced", + ASSET_ALL: "asset.*", + ASSET_UPLOADED: "asset.uploaded", + ASSET_SYNCED: "asset.synced", + ACCOUNT_ALL: "account.*", + ANALYTICS_ALL: "analytics.*", + LEADS_ALL: "leads.*", +}; diff --git a/components/cincopa/sources/common/webhook.mjs b/components/cincopa/sources/common/webhook.mjs new file mode 100755 index 0000000000000..22b924eac30f6 --- /dev/null +++ b/components/cincopa/sources/common/webhook.mjs @@ -0,0 +1,73 @@ +import { v4 as uuid } from "uuid"; +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../cincopa.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + }, + hooks: { + async activate() { + const securityKey = uuid(); + await this.createWebhook({ + params: { + hook_url: this.http.endpoint, + security_key: securityKey, + events: this.getEvents(), + }, + }); + this.setSecurityKey(securityKey); + }, + async deactivate() { + await this.deleteWebhook({ + params: { + hook_url: this.http.endpoint, + }, + }); + }, + }, + methods: { + setSecurityKey(value) { + this.db.set(constants.SECURITY_KEY, value); + }, + getSecurityKey() { + return this.db.get(constants.SECURITY_KEY); + }, + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getEvents() { + throw new ConfigurationError("getEvents is not implemented"); + }, + processResource(resource) { + this.$emit(resource, this.generateMeta(resource)); + }, + createWebhook(args = {}) { + return this.app._makeRequest({ + debug: true, + path: "/webhook.set.json", + ...args, + }); + }, + deleteWebhook(args = {}) { + return this.app._makeRequest({ + debug: true, + path: "/webhook.delete.json", + ...args, + }); + }, + }, + async run({ body }) { + this.http.respond({ + status: 200, + }); + + this.processResource(body); + }, +}; diff --git a/components/cincopa/sources/gallery-created-instant/gallery-created-instant.mjs b/components/cincopa/sources/gallery-created-instant/gallery-created-instant.mjs new file mode 100644 index 0000000000000..a4cd0a01b0523 --- /dev/null +++ b/components/cincopa/sources/gallery-created-instant/gallery-created-instant.mjs @@ -0,0 +1,25 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; + +export default { + ...common, + key: "cincopa-gallery-created-instant", + name: "New Gallery Created (Instant)", + description: "Emit new event when a new gallery is created. [See the documentation](https://www.cincopa.com/media-platform/api-documentation-v2#webhook.set)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return events.GALLERY_CREATED; + }, + generateMeta(resource) { + return { + id: resource.fid, + summary: `New Gallery: ${resource.name}`, + ts: Date.parse(resource.modified), + }; + }, + }, +}; diff --git a/components/circl_hash_lookup/README.md b/components/circl_hash_lookup/README.md new file mode 100644 index 0000000000000..8528506eed746 --- /dev/null +++ b/components/circl_hash_lookup/README.md @@ -0,0 +1,11 @@ +# Overview + +The CIRCL Hash Lookup API offers a way to check files against known hashes to detect potential threats or verify file integrity. Within Pipedream's serverless platform, you can leverage this API to automate security checks, integrate with your existing cloud storage solutions, or enhance your incident response workflows by cross-referencing file hashes against CIRCL's extensive database. Pipedream's ability to connect to hundreds of apps allows you to create multifaceted workflows that trigger on various events, process data, and invoke the Hash Lookup API to provide real-time security analysis. + +# Example Use Cases + +- **Automated Security Alerts**: Trigger a Pipedream workflow whenever new files are uploaded to your cloud storage (e.g., Dropbox, Google Drive). Extract the hash of each file and use the CIRCL Hash Lookup API to check if the file is potentially harmful. Send an alert using apps like Slack, PagerDuty, or email if a threat is detected. + +- **Continuous Monitoring for File Integrity**: Schedule a Pipedream workflow to regularly scan file hashes in your database or a specified directory. Use the Hash Lookup API to ensure none of the files have been tampered with or replaced by known malicious hashes. Record the results in a Google Sheet or send them to a logging service like Datadog. + +- **Incident Response Enrichment**: When your monitoring tools detect a potential security incident, trigger a Pipedream workflow to collect related file hashes. Run these hashes through the CIRCL Hash Lookup API to enrich incident reports with information on whether these files are listed as known malware. Integrate with ticketing systems like Jira or Zendesk to automatically update incident tickets with the lookup results. diff --git a/components/circle/README.md b/components/circle/README.md index e47c5ccc732d2..eebec4abf82c6 100644 --- a/components/circle/README.md +++ b/components/circle/README.md @@ -1,7 +1,11 @@ # Overview -The Circle API allows developers to access and integrate the functionality of -Circle with other applications. +The Circle API enables developers to tap into the heart of their Circle community platform, allowing them to read and write various types of community data programmatically. With this API, you can automate community management tasks, extract insights, manage members, and create a more dynamic and integrated community experience. By leveraging the Circle API within Pipedream's ecosystem, you can stitch together powerful workflows that react to events in Circle, push data to other services, and streamline community operations. -Some example API methods include retrieving circles, retrieving users, and -managing notifications. +# Example Use Cases + +- **Member Onboarding Automation**: When a new member joins your Circle community, automatically send them a welcome email via SendGrid, enroll them in an onboarding course in Teachable, and tag them in your CRM for follow-up. This creates a warm, personalized introduction to your community and ensures they're set up for success. + +- **Content Digest Creation**: Compile a weekly content digest from your Circle community, including popular posts and active discussions, using the Circle API to fetch the data. Then, format this content and distribute it via Mailchimp to keep your members engaged and informed about the latest community highlights. + +- **Event RSVP and Notification System**: When a member RSVPs to an event within Circle, use the Circle API to capture that event, add the member to an event list in a Google Sheet, and set up a reminder using Google Calendar. A few hours before the event, send a reminder via Twilio SMS to ensure a great turnout. diff --git a/components/circleci/README.md b/components/circleci/README.md index 570b75fd27dfe..d03cab0d6ef94 100644 --- a/components/circleci/README.md +++ b/components/circleci/README.md @@ -1,10 +1,11 @@ # Overview -With CircleCI's API, you can: - -- Automate your CircleCI workflows -- Trigger builds remotely -- Get information about builds, jobs, and artifacts -- Download artifacts -- Delete builds and artifacts -- And much more! +The CircleCI API allows for seamless integration and automation of your CI/CD pipeline. By leveraging the API through Pipedream, you can trigger builds, monitor the status of pipelines, fetch build artifacts, and interact with various aspects of your CircleCI projects. This facilitates the creation of dynamic workflows that can streamline development practices, enhance deployment strategies, and ensure continuous integration processes are more efficient and less error-prone. + +# Example Use Cases + +- **Automated Deployment Notifications**: When a deployment succeeds or fails in CircleCI, use Pipedream to send customized notifications through Slack or email. This keeps your team promptly informed about the status of deployment processes, fostering immediate feedback and action. + +- **Dynamic Resource Allocation Based on Workload**: Analyze ongoing builds and resource usage in CircleCI via Pipedream and automatically scale up or down your infrastructure on AWS or Google Cloud depending on the demand. This optimizes cost and performance without manual oversight. + +- **Enhanced Project Management**: Integrate CircleCI with project management tools like Jira or Asana on Pipedream. Whenever a build passes or fails, automatically create or update tasks, attach build logs, and set assignments for debugging or follow-up actions, ensuring your project’s development cycle stays on track. diff --git a/components/circleci/actions/get-job-artifacts/get-job-artifacts.mjs b/components/circleci/actions/get-job-artifacts/get-job-artifacts.mjs new file mode 100644 index 0000000000000..434cba3175dd3 --- /dev/null +++ b/components/circleci/actions/get-job-artifacts/get-job-artifacts.mjs @@ -0,0 +1,54 @@ +import circleci from "../../circleci.app.mjs"; + +export default { + key: "circleci-get-job-artifacts", + name: "Get Job Artifacts", + description: "Retrieves a job's artifacts. [See the documentation](https://circleci.com/docs/api/v2/index.html#tag/Job/operation/getJobArtifacts).", + version: "0.0.1", + type: "action", + props: { + circleci, + projectSlug: { + propDefinition: [ + circleci, + "projectSlug", + ], + }, + pipelineId: { + propDefinition: [ + circleci, + "pipelineId", + (c) => ({ + projectSlug: c.projectSlug, + }), + ], + }, + workflowId: { + propDefinition: [ + circleci, + "workflowId", + (c) => ({ + pipelineId: c.pipelineId, + }), + ], + }, + jobNumber: { + propDefinition: [ + circleci, + "jobNumber", + (c) => ({ + workflowId: c.workflowId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.circleci.getJobArtifacts({ + $, + projectSlug: this.projectSlug, + jobNumber: this.jobNumber, + }); + $.export("$summary", `Successfully retrieved artifacts for job number: ${this.jobNumber}`); + return response; + }, +}; diff --git a/components/circleci/actions/rerun-workflow/rerun-workflow.mjs b/components/circleci/actions/rerun-workflow/rerun-workflow.mjs new file mode 100644 index 0000000000000..a997d763cd744 --- /dev/null +++ b/components/circleci/actions/rerun-workflow/rerun-workflow.mjs @@ -0,0 +1,79 @@ +import circleci from "../../circleci.app.mjs"; + +export default { + key: "circleci-rerun-workflow", + name: "Rerun Workflow", + description: "Reruns the specified workflow. [See the documentation](https://circleci.com/docs/api/v2/index.html#tag/Workflow/operation/rerunWorkflow)", + version: "0.0.1", + type: "action", + props: { + circleci, + projectSlug: { + propDefinition: [ + circleci, + "projectSlug", + ], + }, + pipelineId: { + propDefinition: [ + circleci, + "pipelineId", + (c) => ({ + projectSlug: c.projectSlug, + }), + ], + }, + workflowId: { + propDefinition: [ + circleci, + "workflowId", + (c) => ({ + pipelineId: c.pipelineId, + }), + ], + }, + enableSsh: { + type: "boolean", + label: "Enable SSH", + description: "Whether to enable SSH access for the triggering user on the newly-rerun job. Requires the jobs parameter to be used and so is mutually exclusive with the from_failed parameter.", + optional: true, + }, + fromFailed: { + type: "boolean", + label: "From Failed", + description: "Whether to rerun the workflow from the failed job. Mutually exclusive with the jobs parameter.", + optional: true, + }, + jobIds: { + propDefinition: [ + circleci, + "jobIds", + (c) => ({ + workflowId: c.workflowId, + }), + ], + }, + sparseTree: { + type: "boolean", + label: "Sparse Tree", + description: "", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.circleci.rerunWorkflow({ + $, + workflowId: this.workflowId, + data: { + enable_ssh: this.enableSsh, + from_failed: this.fromFailed, + jobs: typeof this.jobIds === "string" + ? JSON.parse(this.jobIds) + : this.jobIds, + sparse_tree: this.sparseTree, + }, + }); + $.export("$summary", `Successfully started a rerun of workflow with ID: ${this.workflowId}`); + return response; + }, +}; diff --git a/components/circleci/actions/trigger-pipeline/trigger-pipeline.mjs b/components/circleci/actions/trigger-pipeline/trigger-pipeline.mjs new file mode 100644 index 0000000000000..2d531baeaacfd --- /dev/null +++ b/components/circleci/actions/trigger-pipeline/trigger-pipeline.mjs @@ -0,0 +1,87 @@ +import circleci from "../../circleci.app.mjs"; + +export default { + key: "circleci-trigger-pipeline", + name: "Trigger a Pipeline", + description: "Trigger a pipeline given a pipeline definition ID. Supports all integrations except GitLab. [See the documentation](https://circleci.com/docs/api/v2/index.html#tag/Pipeline/operation/triggerPipelineRun)", + version: "0.0.1", + type: "action", + props: { + circleci, + alert: { + type: "alert", + alertType: "info", + content: "Supports all integrations except GitLab.", + }, + projectSlug: { + propDefinition: [ + circleci, + "projectSlug", + ], + }, + definitionId: { + type: "string", + label: "Definition ID", + description: "The unique ID for the pipeline definition. This can be found in the page Project Settings > Pipelines.", + }, + configBranch: { + type: "string", + label: "Config Branch", + description: "The branch that should be used to fetch the config file. Note that branch and tag are mutually exclusive. To trigger a pipeline for a PR by number use pull//head for the PR ref or pull//merge for the merge ref (GitHub only)", + optional: true, + }, + configTag: { + type: "string", + label: "Config Tag", + description: "The tag that should be used to fetch the config file. The commit that this tag points to is used for the pipeline. Note that branch and tag are mutually exclusive.", + optional: true, + }, + checkoutBranch: { + type: "string", + label: "Checkout Branch", + description: "The branch that should be used to check out code on a checkout step. Note that branch and tag are mutually exclusive. To trigger a pipeline for a PR by number use pull//head for the PR ref or pull//merge for the merge ref (GitHub only)", + optional: true, + }, + checkoutTag: { + type: "string", + label: "Checkout Tag", + description: "The tag that should be used to check out code on a checkout step. The commit that this tag points to is used for the pipeline. Note that branch and tag are mutually exclusive.", + optional: true, + }, + parameters: { + type: "object", + label: "Parameters", + description: "An object containing pipeline parameters and their values. Pipeline parameters have the following size limits: 100 max entries, 128 maximum key length, 512 maximum value length.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.circleci.triggerPipeline({ + $, + projectSlug: this.projectSlug, + data: { + definition_id: this.definitionId, + config: this.configBranch || this.configTag + ? { + branch: this.configBranch, + tag: this.configTag, + } + : undefined, + checkout: this.checkoutBranch || this.checkoutTag + ? { + branch: this.checkoutBranch, + tag: this.checkoutTag, + } + : undefined, + parameters: typeof this.parameters === "string" + ? JSON.parse(this.parameters) + : this.parameters, + }, + }); + + if (response?.id) { + $.export("$summary", `Successfully triggered pipeline with ID: ${response.id}`); + } + return response; + }, +}; diff --git a/components/circleci/circleci.app.mjs b/components/circleci/circleci.app.mjs index 83f7f387fef73..b44cefa1746ad 100644 --- a/components/circleci/circleci.app.mjs +++ b/components/circleci/circleci.app.mjs @@ -1,11 +1,211 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "circleci", - propDefinitions: {}, + propDefinitions: { + pipelineId: { + type: "string", + label: "Pipeline ID", + description: "The identifier of a pipeline", + async options({ + projectSlug, prevContext, + }) { + const { + items, nextPageToken, + } = await this.listPipelines({ + projectSlug, + params: prevContext?.next + ? { + "page-token": prevContext.next, + } + : {}, + }); + return { + options: items?.map(({ id }) => id) || [], + context: { + next: nextPageToken, + }, + }; + }, + }, + workflowId: { + type: "string", + label: "Workflow ID", + description: "The identifier of a workflow", + async options({ + pipelineId, prevContext, + }) { + const { + items, nextPageToken, + } = await this.listPipelineWorkflows({ + pipelineId, + params: prevContext?.next + ? { + "page-token": prevContext.next, + } + : {}, + }); + return { + options: items?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || [], + context: { + next: nextPageToken, + }, + }; + }, + }, + jobNumber: { + type: "string", + label: "Job Number", + description: "The job number of a job", + async options({ + workflowId, prevContext, + }) { + const { + items, nextPageToken, + } = await this.listWorkflowJobs({ + workflowId, + params: prevContext?.next + ? { + "page-token": prevContext.next, + } + : {}, + }); + return { + options: items?.map(({ + job_number: value, name: label, + }) => ({ + value, + label, + })) || [], + context: { + next: nextPageToken, + }, + }; + }, + }, + jobIds: { + type: "string[]", + label: "Jobs", + description: "A list of job IDs to rerun", + optional: true, + async options({ + workflowId, prevContext, + }) { + const { + items, nextPageToken, + } = await this.listWorkflowJobs({ + workflowId, + params: prevContext?.next + ? { + "page-token": prevContext.next, + } + : {}, + }); + return { + options: items?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || [], + context: { + next: nextPageToken, + }, + }; + }, + }, + projectSlug: { + type: "string", + label: "Project Slug", + description: "Project slug in the form `vcs-slug/org-name/repo-name` (found in Project Settings)", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://circleci.com/api/v2"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "Circle-Token": this.$auth.token, + }, + ...otherOpts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhook", + ...opts, + }); + }, + deleteWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhook/${webhookId}`, + }); + }, + listPipelines({ + projectSlug, ...opts + }) { + return this._makeRequest({ + path: `/project/${projectSlug}/pipeline/mine`, + ...opts, + }); + }, + listPipelineWorkflows({ + pipelineId, ...opts + }) { + return this._makeRequest({ + path: `/pipeline/${pipelineId}/workflow`, + ...opts, + }); + }, + listWorkflowJobs({ + workflowId, ...opts + }) { + return this._makeRequest({ + path: `/workflow/${workflowId}/job`, + ...opts, + }); + }, + triggerPipeline({ + projectSlug, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/project/${projectSlug}/pipeline/run`, + ...opts, + }); + }, + getJobArtifacts({ + projectSlug, jobNumber, ...opts + }) { + return this._makeRequest({ + path: `/project/${projectSlug}/${jobNumber}/artifacts`, + ...opts, + }); + }, + rerunWorkflow({ + workflowId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/workflow/${workflowId}/rerun`, + ...opts, + }); }, }, }; diff --git a/components/circleci/package.json b/components/circleci/package.json new file mode 100644 index 0000000000000..7066275ff6b20 --- /dev/null +++ b/components/circleci/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/circleci", + "version": "0.0.1", + "description": "Pipedream CircleCI Components", + "main": "circleci.app.mjs", + "keywords": [ + "pipedream", + "circleci" + ], + "homepage": "https://pipedream.com/apps/circleci", + "author": "Pipedream Msupport@pipedream.com> (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "uuid": "^11.0.3" + } +} diff --git a/components/circleci/sources/common/base.mjs b/components/circleci/sources/common/base.mjs new file mode 100644 index 0000000000000..031b792f7be4e --- /dev/null +++ b/components/circleci/sources/common/base.mjs @@ -0,0 +1,119 @@ +import circleci from "../../circleci.app.mjs"; +import { v4 as uuidv4 } from "uuid"; +import crypto from "crypto"; + +export default { + props: { + circleci, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + name: { + type: "string", + label: "Webhook Name", + description: "Name of the webhook", + }, + projectId: { + type: "string", + label: "Project ID", + description: "The identifier of a project. Can be found in Project Settings -> Overview", + }, + }, + hooks: { + async activate() { + const secret = uuidv4(); + const { id } = await this.circleci.createWebhook({ + data: { + "name": this.name, + "events": this.getEvents(), + "url": this.http.endpoint, + "verify-tls": true, + "signing-secret": secret, + "scope": { + "id": this.projectId, + "type": "project", + }, + }, + }); + this._setHookId(id); + this._setSigningSecret(secret); + }, + async deactivate() { + const webhookId = this._getHookId(); + if (webhookId) { + await this.circleci.deleteWebhook(webhookId); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + _getSigningSecret() { + return this.db.get("signingSecret"); + }, + _setSigningSecret(secret) { + this.db.set("signingSecret", secret); + }, + verifySignature({ + headers, body, + }) { + if (!headers["circleci-signature"]) { + return false; + } + const secret = this._getSigningSecret(); + const signatureFromHeader = headers["circleci-signature"] + .split(",") + .reduce((acc, pair) => { + const [ + key, + value, + ] = pair.split("="); + acc[key] = value; + return acc; + }, {}).v1; + + const validSignature = crypto + .createHmac("sha256", secret) + .update(JSON.stringify(body), "utf8") + .digest("hex"); + + return crypto.timingSafeEqual( + Buffer.from(validSignature, "hex"), + Buffer.from(signatureFromHeader, "hex"), + ); + }, + generateMeta(event) { + return { + id: event.id, + summary: this.getSummary(event), + ts: Date.parse(event.happened_at), + }; + }, + getEvents() { + throw new Error("getEvents is not implemented"); + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + }, + async run(event) { + this.http.respond({ + status: 200, + }); + + if (!this.verifySignature(event)) { + console.log("Invalid webhook signature"); + return; + } + + const { body } = event; + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, +}; diff --git a/components/circleci/sources/new-job-completed-instant/new-job-completed-instant.mjs b/components/circleci/sources/new-job-completed-instant/new-job-completed-instant.mjs new file mode 100644 index 0000000000000..cd907c4b068c4 --- /dev/null +++ b/components/circleci/sources/new-job-completed-instant/new-job-completed-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "circleci-new-job-completed-instant", + name: "New Job Completed (Instant)", + description: "Emit new event when a job is completed in CircleCI.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "job-completed", + ]; + }, + getSummary(event) { + return `Job Completed: ${event.job.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/circleci/sources/new-job-completed-instant/test-event.mjs b/components/circleci/sources/new-job-completed-instant/test-event.mjs new file mode 100644 index 0000000000000..46d16fff09a71 --- /dev/null +++ b/components/circleci/sources/new-job-completed-instant/test-event.mjs @@ -0,0 +1,82 @@ +export default { + "happened_at": "2024-12-18T18:51:31.379158Z", + "pipeline": { + "id": "5c8229b0-194a-4942-bddd-2b6a40c5ab35", + "number": 5, + "created_at": "2024-12-18T17:23:29.629Z", + "trigger": { + "type": "api" + }, + "trigger_parameters": { + "github_app": { + "commit_author_name": "", + "owner": "", + "full_ref": "refs/heads/circleci-project-setup", + "forced": "false", + "user_username": "", + "branch": "circleci-project-setup", + "commit_title": "CircleCI Commit", + "repo_url": "", + "ref": "circleci-project-setup", + "repo_name": "", + "commit_author_email": "", + "commit_sha": "9e2e4bed59d2e6c51d00e0e0b49f1b79ff146ab1" + }, + "git": { + "commit_author_name": "", + "repo_owner": "", + "branch": "circleci-project-setup", + "commit_message": "CircleCI Commit", + "repo_url": "", + "ref": "refs/heads/circleci-project-setup", + "author_avatar_url": "", + "checkout_url": "", + "author_login": "", + "repo_name": "", + "commit_author_email": "", + "checkout_sha": "9e2e4bed59d2e6c51d00e0e0b49f1b79ff146ab1", + "default_branch": "master" + }, + "circleci": { + "event_time": "2024-12-18 17:23:29.361740256 +0000 UTC", + "provider_actor_id": "864c50f3-d0b6-4929-a524-036d3cd4e23f", + "actor_id": "864c50f3-d0b6-4929-a524-036d3cd4e23f", + "event_type": "create pipeline run api", + "trigger_type": "github_app" + }, + "webhook": { + "body": "{}" + } + } + }, + "webhook": { + "id": "6973c364-58bc-43f4-9074-1f3ea720fb6c", + "name": "" + }, + "type": "job-completed", + "organization": { + "id": "48c30eda-389d-48a8-ac64-7091f80c69df", + "name": "" + }, + "workflow": { + "id": "c5aa9edf-4de9-43fd-b50e-2bfd75e32cb1", + "name": "say-hello-workflow", + "created_at": "2024-12-18T18:51:06.137Z", + "stopped_at": "2024-12-18T18:51:31.300Z", + "url": "https://app.circleci.com/pipelines/circleci/9z8PamRuKaKWrW91o7Kqvn/AHWysFUuUfBgs6VeUqm6tg/5/workflows/c5aa9edf-4de9-43fd-b50e-2bfd75e32cb1" + }, + "project": { + "id": "4b309fd6-d103-401a-bee5-1de19651d969", + "name": "", + "slug": "circleci/9z8PamRuKaKWrW91o7Kqvn/AHWysFUuUfBgs6VeUqm6tg" + }, + "id": "1c457f7f-5b15-341b-9b86-6370d1357f61", + "job": { + "status": "success", + "id": "00f06c85-7476-4a75-ba64-7daa608dc61e", + "name": "say-hello", + "number": 12, + "started_at": "2024-12-18T18:51:09.357Z", + "stopped_at": "2024-12-18T18:51:31.300Z" + } +} \ No newline at end of file diff --git a/components/circleci/sources/new-workflow-completed-instant/new-workflow-completed-instant.mjs b/components/circleci/sources/new-workflow-completed-instant/new-workflow-completed-instant.mjs new file mode 100644 index 0000000000000..f4296e6964d06 --- /dev/null +++ b/components/circleci/sources/new-workflow-completed-instant/new-workflow-completed-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "circleci-new-workflow-completed-instant", + name: "New Workflow Completed (Instant)", + description: "Emit new event when a workflow is completed in CircleCI.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "workflow-completed", + ]; + }, + getSummary(event) { + return `Workflow Completed: ${event.workflow.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/circleci/sources/new-workflow-completed-instant/test-event.mjs b/components/circleci/sources/new-workflow-completed-instant/test-event.mjs new file mode 100644 index 0000000000000..6f57ea3d3fbd0 --- /dev/null +++ b/components/circleci/sources/new-workflow-completed-instant/test-event.mjs @@ -0,0 +1,75 @@ +export default { + "type": "workflow-completed", + "id": "ab1491fb-c4ae-3496-a958-c248b4732020", + "happened_at": "2024-12-18T18:44:38.000100Z", + "webhook": { + "id": "7d0a502e-0880-414a-8951-97ba4c361aea", + "name": "" + }, + "workflow": { + "id": "7acc6aa9-aac0-40ac-8cf1-48fca8d8455d", + "name": "say-hello-workflow", + "created_at": "2024-12-18T18:44:20.682Z", + "stopped_at": "2024-12-18T18:44:37.867Z", + "url": "", + "status": "success" + }, + "pipeline": { + "id": "5c8229b0-194a-4942-bddd-2b6a40c5ab35", + "number": 5, + "created_at": "2024-12-18T17:23:29.629Z", + "trigger": { + "type": "api" + }, + "trigger_parameters": { + "github_app": { + "commit_author_name": "", + "owner": "", + "full_ref": "refs/heads/circleci-project-setup", + "forced": "false", + "user_username": "", + "branch": "circleci-project-setup", + "commit_title": "CircleCI Commit", + "repo_url": "", + "ref": "circleci-project-setup", + "repo_name": "", + "commit_author_email": "", + "commit_sha": "9e2e4bed59d2e6c51d00e0e0b49f1b79ff146ab1" + }, + "git": { + "commit_author_name": "", + "repo_owner": "", + "branch": "circleci-project-setup", + "commit_message": "CircleCI Commit", + "repo_url": "", + "ref": "refs/heads/circleci-project-setup", + "author_avatar_url": "", + "checkout_url": "", + "author_login": "", + "repo_name": "", + "commit_author_email": "", + "checkout_sha": "9e2e4bed59d2e6c51d00e0e0b49f1b79ff146ab1", + "default_branch": "master" + }, + "circleci": { + "event_time": "2024-12-18 17:23:29.361740256 +0000 UTC", + "provider_actor_id": "864c50f3-d0b6-4929-a524-036d3cd4e23f", + "actor_id": "864c50f3-d0b6-4929-a524-036d3cd4e23f", + "event_type": "create pipeline run api", + "trigger_type": "github_app" + }, + "webhook": { + "body": "{}" + } + } + }, + "project": { + "id": "4b309fd6-d103-401a-bee5-1de19651d969", + "name": "", + "slug": "circleci/9z8PamRuKaKWrW91o7Kqvn/AHWysFUuUfBgs6VeUqm6tg" + }, + "organization": { + "id": "48c30eda-389d-48a8-ac64-7091f80c69df", + "name": "" + } +} \ No newline at end of file diff --git a/components/cisco_meraki/README.md b/components/cisco_meraki/README.md new file mode 100644 index 0000000000000..eb3d847afa193 --- /dev/null +++ b/components/cisco_meraki/README.md @@ -0,0 +1,11 @@ +# Overview + +The Cisco Meraki API on Pipedream allows developers to interact with Meraki's cloud-managed network infrastructure. Through the API, you can automate tasks, gather network insights, and control various aspects of your Meraki environment like network settings, clients, devices, and more. This seamless integration can elevate your network management to be more efficient and responsive to real-time data and events. + +# Example Use Cases + +- **Automated Network Alerts to Slack**: Use the Cisco Meraki API to monitor network traffic or device statuses and automatically send alerts to a Slack channel when anomalies or specific events are detected. This keeps your IT team promptly informed about network issues. + +- **Sync Meraki Data with Google Sheets**: Collect data from your Meraki network devices and clients periodically and sync it to a Google Sheet for easy reporting and analysis. This can be useful for inventory management, usage tracking, or monitoring network performance trends over time. + +- **Device Provisioning Workflow**: Create a workflow that triggers on a webhook from your internal system (like a ticketing system). When a new employee is onboarded, the workflow can provision a new device in the Meraki network, associate it with the user, and set up the appropriate policies, all done automatically. diff --git a/components/cisco_webex/README.md b/components/cisco_webex/README.md index 022ff58572292..d354b9503b641 100644 --- a/components/cisco_webex/README.md +++ b/components/cisco_webex/README.md @@ -1,9 +1,11 @@ # Overview -Some things you can build using the Cisco Webex API include: +The Cisco Webex API allows developers to integrate their applications with Cisco's robust collaboration tools, creating a workflow of communication within teams and automating various aspects of the meeting lifecycle, from scheduling to follow-up actions. With this API, you can streamline meeting setups, fetch detailed information about participants and meetings, send messages to spaces (rooms), and manage your Webex resources programmatically. Leveraging Pipedream's capabilities, you can connect these features with other apps to automate complex tasks, analyze meeting data, enhance productivity, and maintain a well-organized communication ecosystem. -- A bot that can perform various tasks such as scheduling meeting, sending - reminders, and taking minutes -- An application that can help manage your team's tasks and to-do lists -- A tool that can help transcribe meeting notes into text format -- A system that can automatically record and archive your team's Webex meetings +# Example Use Cases + +- **Automate Meeting Scheduling with Google Calendar**: When a new event is added to a Google Calendar, automatically schedule a corresponding Webex meeting and send invites to the participants. This ensures that all meetings have a virtual space without manual setup. + +- **Post-Meeting Summary to Slack**: After a Webex meeting concludes, trigger a workflow to compile meeting notes, attendance, and action items, then post a summary to a designated Slack channel. This keeps the team informed and accountable for follow-up tasks. + +- **Sync Meeting Recordings to Google Drive**: Once a Webex meeting is recorded, trigger a workflow to upload the recording to Google Drive. Set permissions for sharing within your organization, ensuring that team members can access the meeting content at any time. diff --git a/components/cisco_webex_custom_app/README.md b/components/cisco_webex_custom_app/README.md index 47ebc3eb895d6..fb7146cb6a6ad 100644 --- a/components/cisco_webex_custom_app/README.md +++ b/components/cisco_webex_custom_app/README.md @@ -1,14 +1,8 @@ # Overview -With the Cisco Webex Custom App API, you can build a range of applications that -take advantage of the power of Webex Teams. Here are just a few examples of -what you can build: +Cisco Webex (Custom App) API on Pipedream allows users to automate actions within the Webex platform and connect them with other services to streamline communication and collaboration. You can leverage the API to create participants, send messages in spaces, and manage meetings programmatically. Pipedream's serverless platform enables triggering workflows from various events, processing data, and executing complex actions in response. -1. A message bot that responds to certain keywords or phrases -2. An app that automatically schedules meeting times for team members -3. A app that allows team members to vote on decisions -4. An app that provides real-time translations of team conversations -5. A app that allows team members to share files and documents with each other +# Getting Started ## Creating a custom Webex application To use your own custom Cisco Webex app, you will need to sign up for a free [Webex developer account](https://developer.webex.com/signup) if you don't already have one, and [create a new app](https://developer.webex.com/my-apps/new). @@ -45,4 +39,12 @@ you'll need to create a space-separated list of scopes: 9. Copy and paste your **Client ID**, **Client Secret**, and **Space Separated Scopes** on the Cisco Webex (Custom App) account connection page on Pipedream. - \ No newline at end of file + + +# Example Use Cases + +- **Automated Meeting Scheduling and Notifications**: Schedule Webex meetings automatically based on calendar events from Google Calendar. When a new event is added to a specific Google Calendar, a Pipedream workflow can trigger to create a Webex meeting and send the meeting details to participants via email or a preferred messaging app. + +- **Customer Support Ticket Escalation**: Integrate Cisco Webex with a customer support ticketing system like Zendesk. When a high-priority ticket is received, a workflow can automatically create a Webex space and add the support team for real-time collaboration on resolving the issue. This ensures urgent matters are addressed swiftly. + +- **Project Management Updates**: Connect Cisco Webex to a project management tool like Trello or Asana. Whenever a task is updated or completed, a Pipedream workflow can post an update in a dedicated Webex team space, keeping everyone aligned and informed of project progress without manual status reports. diff --git a/components/cisco_webex_custom_app/package.json b/components/cisco_webex_custom_app/package.json new file mode 100644 index 0000000000000..e5841f7ec247c --- /dev/null +++ b/components/cisco_webex_custom_app/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/cisco_webex_custom_app", + "version": "0.6.0", + "description": "Pipedream cisco_webex_custom_app Components", + "main": "cisco_webex_custom_app.app.mjs", + "keywords": [ + "pipedream", + "cisco_webex_custom_app" + ], + "homepage": "https://pipedream.com/apps/cisco_webex_custom_app", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/civicrm/README.md b/components/civicrm/README.md new file mode 100644 index 0000000000000..4a84753e68d81 --- /dev/null +++ b/components/civicrm/README.md @@ -0,0 +1,14 @@ +# Overview + +The CiviCRM API allows interaction with the CiviCRM ecosystem, enabling you to manage contacts, contributions, events, memberships, and more directly via Pipedream. With Pipedream's serverless platform, you can create workflows that automate tasks, sync data across apps, and respond to webhooks. Using Pipedream, you can harness the CiviCRM API to craft custom integrations that trigger actions within CiviCRM or sync data with other tools, without managing infrastructure. + +# Example Use Cases + +- **Sync CiviCRM Contacts to a Google Sheet** + Automatically add new contacts from CiviCRM to a Google Sheet. This workflow can help maintain backup lists or prepare mailing lists for campaigns. Each new contact in CiviCRM could trigger a workflow that appends their information to a specific Google Sheet. + +- **Send Welcome Emails via SendGrid** + Craft a workflow that triggers when a new member is added to CiviCRM. It can send personalized welcome emails through SendGrid, ensuring new members receive timely, professional communication without manual intervention. + +- **Create Slack Notifications for New Donations** + Set up a Pipedream workflow that listens for new contributions in CiviCRM. Whenever a new donation is recorded, it can trigger a notification in a designated Slack channel, keeping your team instantly informed about fundraising progress. diff --git a/components/civicrm/package.json b/components/civicrm/package.json index cee44eeee1720..30f6414857305 100644 --- a/components/civicrm/package.json +++ b/components/civicrm/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/claid_ai/README.md b/components/claid_ai/README.md new file mode 100644 index 0000000000000..d7579dd2690f4 --- /dev/null +++ b/components/claid_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +Claid AI API provides tools for image enhancement, including resizing, cropping, increasing resolution, and optimizing for web use. It's suitable for automating image processing tasks in applications like e-commerce, real estate, and content management. On Pipedream, you can create serverless workflows that use the Claid AI API to process images in response to various triggers, such as new file uploads or webhooks, and then pass the processed images to other services or store them. + +# Example Use Cases + +- **Automated Image Enhancement for E-commerce Listings**: When a new product image is uploaded to a cloud storage platform like AWS S3, trigger a Pipedream workflow that uses the Claid AI API to enhance the image, then update the product listing with the improved image on platforms like Shopify or WooCommerce. + +- **Real Estate Image Optimization**: For real estate listings, integrate Claid AI with a CRM like Salesforce. Each time a new property image is added to the CRM, a Pipedream workflow is triggered to optimize and resize images for different listing platforms, ensuring fast loading and high-quality visuals. + +- **Content Management System Image Processing**: Connect Claid AI with a CMS like WordPress. Use Pipedream to automate the workflow where every image uploaded by content creators is automatically processed through Claid AI for optimization, then re-uploaded to the CMS, maintaining visual quality while reducing file size. diff --git a/components/clarify/clarify.app.mjs b/components/clarify/clarify.app.mjs new file mode 100644 index 0000000000000..d5f1c88b2aa63 --- /dev/null +++ b/components/clarify/clarify.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "clarify", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/clarify/package.json b/components/clarify/package.json new file mode 100644 index 0000000000000..a867959d7dc95 --- /dev/null +++ b/components/clarify/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/clarify", + "version": "0.0.1", + "description": "Pipedream Clarify Components", + "main": "clarify.app.mjs", + "keywords": [ + "pipedream", + "clarify" + ], + "homepage": "https://pipedream.com/apps/clarify", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/claris_filemaker_server_admin_api/claris_filemaker_server_admin_api.app.mjs b/components/claris_filemaker_server_admin_api/claris_filemaker_server_admin_api.app.mjs new file mode 100644 index 0000000000000..0c050cfef1e05 --- /dev/null +++ b/components/claris_filemaker_server_admin_api/claris_filemaker_server_admin_api.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "claris_filemaker_server_admin_api", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/claris_filemaker_server_admin_api/package.json b/components/claris_filemaker_server_admin_api/package.json new file mode 100644 index 0000000000000..5f1a588837a94 --- /dev/null +++ b/components/claris_filemaker_server_admin_api/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/claris_filemaker_server_admin_api", + "version": "0.0.1", + "description": "Pipedream Claris FileMaker Server - Admin API Components", + "main": "claris_filemaker_server_admin_api.app.mjs", + "keywords": [ + "pipedream", + "claris_filemaker_server_admin_api" + ], + "homepage": "https://pipedream.com/apps/claris_filemaker_server_admin_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/claris_filemaker_server_data_api/claris_filemaker_server_data_api.app.mjs b/components/claris_filemaker_server_data_api/claris_filemaker_server_data_api.app.mjs new file mode 100644 index 0000000000000..d85185a399eb1 --- /dev/null +++ b/components/claris_filemaker_server_data_api/claris_filemaker_server_data_api.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "claris_filemaker_server_data_api", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/claris_filemaker_server_data_api/package.json b/components/claris_filemaker_server_data_api/package.json new file mode 100644 index 0000000000000..99938dee27c07 --- /dev/null +++ b/components/claris_filemaker_server_data_api/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/claris_filemaker_server_data_api", + "version": "0.0.1", + "description": "Pipedream Claris FileMaker Server Data API Components", + "main": "claris_filemaker_server_data_api.app.mjs", + "keywords": [ + "pipedream", + "claris_filemaker_server_data_api" + ], + "homepage": "https://pipedream.com/apps/claris_filemaker_server_data_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/claris_filemaker_server_odata_api/claris_filemaker_server_odata_api.app.mjs b/components/claris_filemaker_server_odata_api/claris_filemaker_server_odata_api.app.mjs new file mode 100644 index 0000000000000..fe1592dfbf14d --- /dev/null +++ b/components/claris_filemaker_server_odata_api/claris_filemaker_server_odata_api.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "claris_filemaker_server_odata_api", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/claris_filemaker_server_odata_api/package.json b/components/claris_filemaker_server_odata_api/package.json new file mode 100644 index 0000000000000..830d5d78760d0 --- /dev/null +++ b/components/claris_filemaker_server_odata_api/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/claris_filemaker_server_odata_api", + "version": "0.0.2", + "description": "Pipedream Claris Filemaker Server OData API Components", + "main": "claris_filemaker_server_odata_api.app.mjs", + "keywords": [ + "pipedream", + "claris_filemaker_server_odata_api" + ], + "homepage": "https://pipedream.com/apps/claris_filemaker_server_odata_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/clear_books/actions/create-customer/create-customer.mjs b/components/clear_books/actions/create-customer/create-customer.mjs new file mode 100644 index 0000000000000..f5770b8771a03 --- /dev/null +++ b/components/clear_books/actions/create-customer/create-customer.mjs @@ -0,0 +1,84 @@ +import clearBooks from "../../clear_books.app.mjs"; + +export default { + key: "clear_books-create-customer", + name: "Create Customer", + description: "Creates a new customer in Clear Books. [See the documentation](https://u.pcloud.link/publink/show?code=XZkThJ5Z4zKewgCL6VBpfxlPeHPDdXXj0Cc7)", + version: "0.0.1", + type: "action", + props: { + clearBooks, + name: { + type: "string", + label: "Name", + description: "Name of the customer", + }, + email: { + type: "string", + label: "Email", + description: "Email address of the customer", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Phone number of the customer", + optional: true, + }, + streetAddress: { + type: "string", + label: "Street Address", + description: "Street address of the customer", + optional: true, + }, + town: { + type: "string", + label: "Town", + description: "Town of the customer", + optional: true, + }, + county: { + type: "string", + label: "County", + description: "County of the customer", + optional: true, + }, + postcode: { + type: "string", + label: "Postcode", + description: "Postcode of the customer", + optional: true, + }, + countryCode: { + type: "string", + label: "Country Code", + description: "Country code of the customer", + optional: true, + }, + }, + async run({ $ }) { + const hasAddress = this.streetAddress + || this.town + || this.county + || this.postcode + || this.countryCode; + + const response = await this.clearBooks.createCustomer({ + $, + data: { + name: this.name, + email: this.email, + phone: this.phone, + address: hasAddress && { + line1: this.streetAddress, + town: this.town, + county: this.county, + postcode: this.postcode, + countryCode: this.countryCode, + }, + }, + }); + $.export("$summary", `Successfully created customer with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/clear_books/actions/create-purchase-document/create-purchase-document.mjs b/components/clear_books/actions/create-purchase-document/create-purchase-document.mjs new file mode 100644 index 0000000000000..8c742b374415c --- /dev/null +++ b/components/clear_books/actions/create-purchase-document/create-purchase-document.mjs @@ -0,0 +1,124 @@ +import clearBooks from "../../clear_books.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "clear_books-create-purchase-document", + name: "Create Purchase Document", + description: "Creates a new Purchase Document in Clear Books. [See the documentation](https://u.pcloud.link/publink/show?code=XZkThJ5Z4zKewgCL6VBpfxlPeHPDdXXj0Cc7)", + version: "0.0.1", + type: "action", + props: { + clearBooks, + purchaseType: { + propDefinition: [ + clearBooks, + "purchaseType", + ], + }, + date: { + type: "string", + label: "Date", + description: "The date of the purchase. Format: YYYY-MM-DD", + }, + supplierId: { + propDefinition: [ + clearBooks, + "supplierId", + ], + }, + description: { + type: "string", + label: "Description", + description: "Description of the purchase document", + optional: true, + }, + numLineItems: { + type: "integer", + label: "Number of Line Items", + description: "The number of line items. Use this to manually enter Unit Price, Quantity, and Description of each line item.", + optional: true, + reloadProps: true, + }, + lineItemsJson: { + type: "string", + label: "Line Items JSON", + description: "JSON value containing an array of Line Items. For example: `[{\"description\":\"Line Item 1 Description\",\"unitPrice\":1022,\"quantity\":1,\"accountCode\":\"2001001\"},{\"description\":\"Line Item 2 Description\",\"unitPrice\":1023,\"quantity\":2,\"accountCode\":\"2001001\"}]`. [See documentation](https://u.pcloud.link/publink/show?code=XZkThJ5Z4zKewgCL6VBpfxlPeHPDdXXj0Cc7)", + optional: true, + }, + }, + additionalProps() { + const props = {}; + if (!this.numLineItems) { + return props; + } + for (let i = 1; i <= this.numLineItems; i++) { + props[`line_item_${i}_unit_price`] = { + type: "string", + label: `Line Item ${i} - Unit Price`, + }; + props[`line_item_${i}_quantity`] = { + type: "integer", + label: `Line Item ${i} - Quantity`, + }; + props[`line_item_${i}_description`] = { + type: "string", + label: `Line Item ${i} - Description`, + }; + } + return props; + }, + methods: { + buildLineItems() { + const lineItems = []; + for (let i = 1; i <= this.numLineItems; i++) { + lineItems.push({ + unitPrice: this[`line_item_${i}_unit_price`], + quantity: this[`line_item_${i}_quantity`], + description: this[`line_item_${i}_description`], + }); + } + return lineItems; + }, + parseLineItemsJson() { + try { + return Array.isArray(this.lineItemsJson) + ? this.lineItemsJson.map((item) => typeof item === "string" + ? JSON.parse(item) + : item) + : typeof this.lineItemsJson === "string" + ? JSON.parse(this.lineItemsJson) + : this.lineItemsJson; + } catch { + throw new ConfigurationError("Could not parse Line Items JSON"); + } + }, + }, + async run({ $ }) { + if (!this.numLineItems && !this.lineItemsJson) { + throw new ConfigurationError("Please enter at least one line item"); + } + + const lineItems = []; + if (this.numLineItems) { + const lineItemsManual = this.buildLineItems(); + lineItems.push(...lineItemsManual); + } + if (this.lineItemsJson) { + const lineItemsJson = this.parseLineItemsJson(); + lineItems.push(...lineItemsJson); + } + + const response = await this.clearBooks.createPurchaseDocument({ + $, + type: this.purchaseType, + data: { + date: this.date, + supplierId: this.supplierId, + description: this.description, + lineItems, + }, + }); + $.export("$summary", `Successfully created purchase document with ID ${response.id}`); + return response; + }, +}; diff --git a/components/clear_books/actions/create-supplier/create-supplier.mjs b/components/clear_books/actions/create-supplier/create-supplier.mjs new file mode 100644 index 0000000000000..f728e96d9384b --- /dev/null +++ b/components/clear_books/actions/create-supplier/create-supplier.mjs @@ -0,0 +1,84 @@ +import clearBooks from "../../clear_books.app.mjs"; + +export default { + key: "clear_books-create-supplier", + name: "Create Supplier", + description: "Creates a new supplier in Clear Books. [See the documentation](https://u.pcloud.link/publink/show?code=XZkThJ5Z4zKewgCL6VBpfxlPeHPDdXXj0Cc7)", + version: "0.0.1", + type: "action", + props: { + clearBooks, + name: { + type: "string", + label: "Name", + description: "Name of the supplier", + }, + email: { + type: "string", + label: "Email", + description: "Email address of the supplier", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Phone number of the supplier", + optional: true, + }, + streetAddress: { + type: "string", + label: "Street Address", + description: "Street address of the supplier", + optional: true, + }, + town: { + type: "string", + label: "Town", + description: "Town of the supplier", + optional: true, + }, + county: { + type: "string", + label: "County", + description: "County of the supplier", + optional: true, + }, + postcode: { + type: "string", + label: "Postcode", + description: "Postcode of the supplier", + optional: true, + }, + countryCode: { + type: "string", + label: "Country Code", + description: "Country code of the supplier", + optional: true, + }, + }, + async run({ $ }) { + const hasAddress = this.streetAddress + || this.town + || this.county + || this.postcode + || this.countryCode; + + const response = await this.clearBooks.createSupplier({ + $, + data: { + name: this.name, + email: this.email, + phone: this.phone, + address: hasAddress && { + line1: this.streetAddress, + town: this.town, + county: this.county, + postcode: this.postcode, + countryCode: this.countryCode, + }, + }, + }); + $.export("$summary", `Successfully created supplier with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/clear_books/clear_books.app.mjs b/components/clear_books/clear_books.app.mjs new file mode 100644 index 0000000000000..e183d7ad9e886 --- /dev/null +++ b/components/clear_books/clear_books.app.mjs @@ -0,0 +1,132 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "clear_books", + propDefinitions: { + supplierId: { + type: "string", + label: "Supplier ID", + description: "The identifier of a supplier", + async options({ page }) { + const suppliers = await this.listSuppliers({ + params: { + page: page + 1, + }, + }); + return suppliers?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + purchaseType: { + type: "string", + label: "Purchase Type", + description: "Type of purchase document", + options: [ + "bills", + "creditNotes", + ], + }, + }, + methods: { + _baseUrl() { + return "https://api.clearbooks.co.uk/v1"; + }, + _makeRequest({ + $ = this, + path, + ...otherOpts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + ...otherOpts, + }); + }, + listCustomers(opts = {}) { + return this._makeRequest({ + path: "/accounting/customers", + ...opts, + }); + }, + listPurchaseDocuments({ + type, ...opts + }) { + return this._makeRequest({ + path: `/accounting/purchases/${type}`, + ...opts, + }); + }, + listTransactions(opts = {}) { + return this._makeRequest({ + path: "/accounting/transactions", + ...opts, + }); + }, + listSuppliers(opts = {}) { + return this._makeRequest({ + path: "/accounting/suppliers", + ...opts, + }); + }, + createCustomer(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/accounting/customers", + ...opts, + }); + }, + createPurchaseDocument({ + type, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/accounting/purchases/${type}`, + ...opts, + }); + }, + createSupplier(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/accounting/suppliers", + ...opts, + }); + }, + async *paginate({ + fn, + args, + max, + }) { + args = { + ...args, + params: { + ...args?.params, + limit: 100, + page: 1, + }, + }; + let total, count = 0; + try { + do { + const results = await fn(args); + for (const item of results) { + yield item; + if (max && ++count >= max) { + return; + } + } + total = results?.length; + args.params.page++; + } while (total === args.params.limit); + } catch (e) { + console.log(`Error: ${e}`); + } + }, + }, +}; diff --git a/components/clear_books/package.json b/components/clear_books/package.json new file mode 100644 index 0000000000000..55fb4e395f0d0 --- /dev/null +++ b/components/clear_books/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/clear_books", + "version": "0.1.0", + "description": "Pipedream Clear Books Components", + "main": "clear_books.app.mjs", + "keywords": [ + "pipedream", + "clear_books" + ], + "homepage": "https://pipedream.com/apps/clear_books", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/clear_books/sources/common/base.mjs b/components/clear_books/sources/common/base.mjs new file mode 100644 index 0000000000000..52baefa2e2714 --- /dev/null +++ b/components/clear_books/sources/common/base.mjs @@ -0,0 +1,79 @@ +import clearBooks from "../../clear_books.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + clearBooks, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + getBaseArgs() { + return { + params: { + sortBy: "id", + sortDirection: "descending", + }, + }; + }, + getArgs() { + return {}; + }, + getFn() { + throw new Error("getFn is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + async processEvent(max) { + const lastId = this._getLastId(); + const results = []; + + const items = this.clearBooks.paginate({ + fn: this.getFn(), + args: { + ...this.getBaseArgs(), + ...this.getArgs(), + }, + max, + }); + + for await (const item of items) { + if (item.id > lastId) { + results.push(item); + } else { + break; + } + } + + if (!results.length) { + return; + } + + this._setLastId(results[0].id); + results.reverse().forEach((item) => { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }); + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/clear_books/sources/new-customer-created/new-customer-created.mjs b/components/clear_books/sources/new-customer-created/new-customer-created.mjs new file mode 100644 index 0000000000000..21a0a05497f4f --- /dev/null +++ b/components/clear_books/sources/new-customer-created/new-customer-created.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "clear_books-new-customer-created", + name: "New Customer Created", + description: "Emit new event when a new customer is added to the system.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFn() { + return this.clearBooks.listCustomers; + }, + generateMeta(customer) { + return { + id: customer.id, + summary: `New Customer with ID: ${customer.id}`, + ts: Date.now(), + }; + }, + }, +}; diff --git a/components/clear_books/sources/new-purchase-document-created/new-purchase-document-created.mjs b/components/clear_books/sources/new-purchase-document-created/new-purchase-document-created.mjs new file mode 100644 index 0000000000000..330f1c8b6b3a7 --- /dev/null +++ b/components/clear_books/sources/new-purchase-document-created/new-purchase-document-created.mjs @@ -0,0 +1,38 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "clear_books-new-purchase-document-created", + name: "New Purchase Document Created", + description: "Emit new event when a new purchase document is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + purchaseType: { + propDefinition: [ + common.props.clearBooks, + "purchaseType", + ], + }, + }, + methods: { + ...common.methods, + getFn() { + return this.clearBooks.listPurchaseDocuments; + }, + getArgs() { + return { + type: this.purchaseType, + }; + }, + generateMeta(purchaseDocument) { + return { + id: purchaseDocument.id, + summary: `New Purchase Document with ID: ${purchaseDocument.id}`, + ts: Date.now(), + }; + }, + }, +}; diff --git a/components/clear_books/sources/new-transaction-created/new-transaction-created.mjs b/components/clear_books/sources/new-transaction-created/new-transaction-created.mjs new file mode 100644 index 0000000000000..3cdaaf0f73dd6 --- /dev/null +++ b/components/clear_books/sources/new-transaction-created/new-transaction-created.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "clear_books-new-transaction-created", + name: "New Transaction Created", + description: "Emit new event when a new transaction is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFn() { + return this.clearBooks.listTransactions; + }, + generateMeta(transaction) { + return { + id: transaction.id, + summary: `New Transaction with ID: ${transaction.id}`, + ts: Date.now(), + }; + }, + }, +}; diff --git a/components/clearbit/README.md b/components/clearbit/README.md index cdaeb680e1e25..2c351cbb1d6cd 100644 --- a/components/clearbit/README.md +++ b/components/clearbit/README.md @@ -1,6 +1,11 @@ # Overview -Clearbit provides APIs that allow you to retrieve information about companies -and people. This information can be used to build applications that perform -functions such as identifying potential customers, conducting due diligence, or -fraud prevention. +Clearbit API offers robust data enrichment capabilities, allowing you to seamlessly integrate comprehensive business intelligence into your applications. By leveraging Clearbit, you can append real-time insights to your leads, automate personalized outreach, and gain a deeper understanding of your customers. With insights ranging from company profiles, contact details, to employment information, the possibilities to enhance your marketing, sales, and customer support efforts are vast. + +# Example Use Cases + +- **Lead Scoring Automation**: Streamline lead qualification by using Clearbit to enrich lead data collected via forms or webhooks. Integrate with CRM apps like Salesforce on Pipedream to automatically update lead scores based on the enriched data, ensuring your sales team prioritizes high-value prospects. + +- **Personalized Email Campaigns**: Dynamically segment your email lists using enriched user profiles from Clearbit. Connect to email marketing platforms like Mailchimp on Pipedream to trigger personalized campaigns based on job role, company size, or industry, increasing the relevance and effectiveness of your outreach. + +- **Enhanced Customer Support**: Improve customer interactions by providing support teams with up-to-date customer info. When a support ticket is created in helpdesk tools like Zendesk, use Clearbit via Pipedream to fetch the latest customer details and surface this data directly within the ticket, allowing for more informed and personalized support. diff --git a/components/clearly_defined/README.md b/components/clearly_defined/README.md new file mode 100644 index 0000000000000..7c10617837c7f --- /dev/null +++ b/components/clearly_defined/README.md @@ -0,0 +1,11 @@ +# Overview + +The Clearly Defined API offers a way to interact with the Clearly Defined service, which curates and shares data about the clarity of licenses and security of open source components. You can retrieve detailed information about software components' licenses, scores for clarity, and metadata. On Pipedream, you can harness this API to automate checking the compliance and security of software dependencies, notify pertinent stakeholders about the status of components, and integrate with other tools for a seamless open source governance process. + +# Example Use Cases + +- **Automate License Compliance Checks**: Set up a Pipedream workflow that triggers on a schedule to check the licenses of dependencies used in your project. If it finds dependencies with unclear licenses, it can send alerts to Slack or another communication platform used by your team. + +- **Enrich Continuous Integration/Deployment (CI/CD) Processes**: Use Pipedream to integrate the Clearly Defined API into your CI/CD pipeline. Before deployment, trigger a workflow to verify that all open source components are up to the defined standards and block the deployment if necessary. + +- **Maintain a Database of Component Licenses**: Create a Pipedream workflow to query the Clearly Defined API periodically for your projects' dependencies. Store the results in a database, such as Google Sheets or Airtable, to maintain a live catalog of licenses and security statuses for auditing or reporting purposes. diff --git a/components/clearly_defined/actions/create-definition/create-definition.mjs b/components/clearly_defined/actions/create-definition/create-definition.mjs new file mode 100644 index 0000000000000..223ee50651aee --- /dev/null +++ b/components/clearly_defined/actions/create-definition/create-definition.mjs @@ -0,0 +1,60 @@ +import clearlyDefined from "../../clearly_defined.app.mjs"; + +export default { + key: "clearly_defined-create-definition", + name: "Create Definition", + description: "Request the creation of a resource. [See the documentation](https://api.clearlydefined.io/api-docs/#/definitions/post_definitions).", + version: "0.0.1", + type: "action", + props: { + clearlyDefined, + type: { + propDefinition: [ + clearlyDefined, + "type", + ], + }, + provider: { + propDefinition: [ + clearlyDefined, + "provider", + ], + }, + namespace: { + propDefinition: [ + clearlyDefined, + "namespace", + ], + }, + name: { + propDefinition: [ + clearlyDefined, + "name", + ], + }, + revision: { + propDefinition: [ + clearlyDefined, + "revision", + ], + }, + }, + async run({ $ }) { + const component = `${this.type}/${this.provider}/${this.namespace}/${this.name}${this.revision + ? "/" + this.revision + : ""}`; + + const response = await this.clearlyDefined.createDefinition({ + $, + data: [ + component, + ], + }); + + if (response && Object.keys(response).length > 0) { + $.export("$summary", "Successfully created definition"); + } + + return response; + }, +}; diff --git a/components/clearly_defined/actions/get-definitions/get-definitions.mjs b/components/clearly_defined/actions/get-definitions/get-definitions.mjs new file mode 100644 index 0000000000000..8bd812c73f9bc --- /dev/null +++ b/components/clearly_defined/actions/get-definitions/get-definitions.mjs @@ -0,0 +1,140 @@ +import clearlyDefined from "../../clearly_defined.app.mjs"; + +export default { + key: "clearly_defined-get-definitions", + name: "Get Definitions", + description: "Gets the coordinates for all definitions that match the given pattern in the specified part of the definition. [See the documentation](https://api.clearlydefined.io/api-docs/#/definitions/get_definitions).", + version: "0.0.1", + type: "action", + props: { + clearlyDefined, + pattern: { + type: "string", + label: "Pattern", + description: "The string to search for in definition coordinates to get coordinate suggestions", + optional: true, + }, + type: { + propDefinition: [ + clearlyDefined, + "type", + ], + optional: true, + }, + provider: { + propDefinition: [ + clearlyDefined, + "provider", + ], + optional: true, + }, + namespace: { + propDefinition: [ + clearlyDefined, + "namespace", + ], + optional: true, + }, + name: { + propDefinition: [ + clearlyDefined, + "name", + ], + optional: true, + }, + license: { + type: "string", + label: "License", + description: "The SPDX license identifier", + optional: true, + }, + releasedAfter: { + type: "string", + label: "Released After", + description: "The minimum release date for the component. E.g. `2025-01-01`", + optional: true, + }, + releasedBefore: { + type: "string", + label: "Released Before", + description: "The maximum release date for the component. E.g. `2025-01-01`", + optional: true, + }, + minLicensedScore: { + type: "integer", + label: "Min Licensed Score", + description: "The minimum effective licensed score for the component", + optional: true, + }, + maxLicensedScore: { + type: "integer", + label: "Max Licensed Score", + description: "The maximum effective licensed score for the component", + optional: true, + }, + minDescribedScore: { + type: "integer", + label: "Min Described Score", + description: "The minimum effective described score for the component", + optional: true, + }, + maxDescribedScore: { + type: "integer", + label: "Max Described Score", + description: "The maximum effective described score for the component", + optional: true, + }, + sort: { + propDefinition: [ + clearlyDefined, + "sort", + ], + }, + sortDirection: { + propDefinition: [ + clearlyDefined, + "sortDirection", + ], + }, + continuationToken: { + type: "string", + label: "Continuation Token", + description: "Used for pagination. Seeded from the results of the previous query", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.clearlyDefined.getDefinitions({ + $, + params: { + pattern: this.pattern, + type: this.type, + provider: this.provider, + name: this.name, + namespace: this.namespace, + license: this.license, + releasedAfter: this.releasedAfter, + releasedBefore: this.releasedBefore, + minLicensedScore: this.minLicensedScore, + maxLicensedScore: this.maxLicensedScore, + minDescribedScore: this.minDescribedScore, + maxDescribedScore: this.maxDescribedScore, + sort: this.sort, + sortDesc: this.sortDirection === "descending", + continuationToken: this.continuationToken, + }, + }); + + const length = response.data + ? response.data.length + : response.length + ? response.length + : 0; + + $.export("$summary", `Successfully retrieved ${length} definition${length === 1 + ? "" + : "s"}`); + + return response; + }, +}; diff --git a/components/clearly_defined/actions/get-harvests/get-harvests.mjs b/components/clearly_defined/actions/get-harvests/get-harvests.mjs new file mode 100644 index 0000000000000..2baa75e556c34 --- /dev/null +++ b/components/clearly_defined/actions/get-harvests/get-harvests.mjs @@ -0,0 +1,67 @@ +import clearlyDefined from "../../clearly_defined.app.mjs"; + +export default { + key: "clearly_defined-get-harvests", + name: "Get Harvests", + description: "Get all the harvested data for a component revision. [See the documentation](https://api.clearlydefined.io/api-docs/#/harvest/get_harvest__type___provider___namespace___name___revision_).", + version: "0.0.1", + type: "action", + props: { + clearlyDefined, + type: { + propDefinition: [ + clearlyDefined, + "type", + ], + }, + provider: { + propDefinition: [ + clearlyDefined, + "provider", + ], + }, + namespace: { + propDefinition: [ + clearlyDefined, + "namespace", + ], + }, + name: { + propDefinition: [ + clearlyDefined, + "name", + ], + }, + revision: { + propDefinition: [ + clearlyDefined, + "revision", + ], + }, + form: { + propDefinition: [ + clearlyDefined, + "form", + ], + }, + }, + async run({ $ }) { + const response = await this.clearlyDefined.getHarvests({ + $, + type: this.type, + provider: this.provider, + name: this.name, + namespace: this.namespace, + revision: this.revision, + params: { + form: this.form, + }, + }); + + if (response && Object.keys(response).length > 0) { + $.export("$summary", "Successfully retrieved harvest details"); + } + + return response; + }, +}; diff --git a/components/clearly_defined/clearly_defined.app.mjs b/components/clearly_defined/clearly_defined.app.mjs index 2298be03d05a0..13707c76a651e 100644 --- a/components/clearly_defined/clearly_defined.app.mjs +++ b/components/clearly_defined/clearly_defined.app.mjs @@ -1,11 +1,94 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "clearly_defined", - propDefinitions: {}, + propDefinitions: { + type: { + type: "string", + label: "Type", + description: "The type of component", + options: constants.COMPONENT_TYPES, + }, + provider: { + type: "string", + label: "Provider", + description: "Where the component can be found", + options: constants.COMPONENT_PROVIDERS, + }, + namespace: { + type: "string", + label: "Namespace", + description: "Many component systems have namespaces. GitHub orgs, NPM namespace, Maven group id, Conda Subdir/Architecture. If your component does not have a namespace, use '-' (ASCII hyphen).", + }, + name: { + type: "string", + label: "Name", + description: "The name of the component you want", + }, + revision: { + type: "string", + label: "Revision", + description: "Components typically have some differentiator like a version or commit id. Use that here. If this segment is omitted, the latest revision is used (if that makes sense for the provider).", + optional: true, + }, + sort: { + type: "string", + label: "Sort", + description: "The field to sort the results by", + options: constants.SORT_FIELDS, + optional: true, + }, + sortDirection: { + type: "string", + label: "Sort Direction", + description: "The direction to sort the results by", + options: constants.SORT_DIRECTIONS, + optional: true, + }, + form: { + type: "string", + label: "Form", + description: "Form of the response", + options: constants.RESPONSE_FORMS, + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `https://${this.$auth.environment}.clearlydefined.io`; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + ...opts, + }); + }, + getDefinitions(opts = {}) { + return this._makeRequest({ + path: "/definitions", + ...opts, + }); + }, + getHarvests({ + type, provider, namespace, name, revision, ...opts + }) { + return this._makeRequest({ + path: `/harvest/${type}/${provider}/${namespace}/${name}/${revision}`, + ...opts, + }); + }, + createDefinition(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/definitions", + ...opts, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/clearly_defined/common/constants.mjs b/components/clearly_defined/common/constants.mjs new file mode 100644 index 0000000000000..aaea9be6da0fa --- /dev/null +++ b/components/clearly_defined/common/constants.mjs @@ -0,0 +1,82 @@ +const COMPONENT_TYPES = [ + "composer", + "conda", + "condasrc", + "crate", + "deb", + "debsrc", + "gem", + "git", + "go", + "maven", + "npm", + "nuget", + "pod", + "pypi", + "sourcearchive", +]; + +const COMPONENT_PROVIDERS = [ + "anaconda-main", + "anaconda-r", + "cocoapods", + "conda-forge", + "cratesio", + "debian", + "github", + "gitlab", + "mavencentral", + "mavengoogle", + "gradleplugin", + "npmjs", + "nuget", + "packagist", + "pypi", + "rubygems", +]; + +const SORT_FIELDS = [ + "type", + "provider", + "namespace", + "name", + "revision", + "license", + "releaseDate", + "licensedScore", + "describedScore", + "effectiveScore", + "toolScore", +]; + +const SORT_DIRECTIONS = [ + "ascending", + "descending", +]; + +const RESPONSE_FORMS = [ + { + label: "summarize harvested file", + value: "summary", + }, + { + label: "raw content of the harvested file", + value: "raw", + }, + { + label: "streamed content of the harvested file", + value: "streamed", + }, + { + label: "List of matching harvested files", + value: "list", + }, +]; + +export default { + COMPONENT_TYPES, + COMPONENT_PROVIDERS, + SORT_FIELDS, + SORT_DIRECTIONS, + RESPONSE_FORMS, +}; diff --git a/components/clearly_defined/package.json b/components/clearly_defined/package.json index a08648ded33dd..bb3d3a6e8c609 100644 --- a/components/clearly_defined/package.json +++ b/components/clearly_defined/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/clearly_defined", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Clearly Defined Components", "main": "clearly_defined.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } -} \ No newline at end of file +} diff --git a/components/clearout/README.md b/components/clearout/README.md new file mode 100644 index 0000000000000..537a7ccb72607 --- /dev/null +++ b/components/clearout/README.md @@ -0,0 +1,11 @@ +# Overview + +The Clearout API enables real-time email verification and cleaning to boost the deliverability of your emails. Integrated within Pipedream, it offers a serverless workflow for automating email list maintenance, validation processes, and enhances email marketing efficiency. With it, you can programmatically verify individual addresses or bulk lists, identify disposable emails, and detect invalid domains, creating a reliable foundation for your communication strategies. + +# Example Use Cases + +- **Validate Emails on User Sign-up**: Automate the verification of email addresses during user registration. When a new user signs up through your app's form, trigger a Pipedream workflow to use Clearout's API to verify the email address. If the email is valid, proceed with the registration; otherwise, prompt the user to provide a valid email. + +- **Clean Email Lists Before Campaigns**: Use Pipedream's scheduled workflows to regularly clean your email lists with the Clearout API. Before sending out a marketing campaign, verify and clean your email list to minimize bounces and improve open rates. Once the list is verified, use a service like Mailchimp or SendGrid within Pipedream to send emails to the cleaned list. + +- **Detect and Remove Disposable Emails**: Protect your database from being cluttered with temporary emails. Set up a Pipedream workflow that checks for disposable email addresses using the Clearout API each time a new email is added to your database. If a disposable email is detected, automatically remove it from your list or flag it for review. diff --git a/components/clerk/README.md b/components/clerk/README.md new file mode 100644 index 0000000000000..fc614b030e207 --- /dev/null +++ b/components/clerk/README.md @@ -0,0 +1,11 @@ +# Overview + +The Clerk API lets you manage user authentication and create secure, delightful user experiences in your apps. Within Pipedream's serverless platform, you can harness this API to automate workflows that trigger on user events, sync user data across apps, and maintain robust user management without the heavy lifting of building authentication infrastructure from scratch. + +# Example Use Cases + +- **Automate User Onboarding**: When a new user signs up via Clerk, trigger a Pipedream workflow that sends a personalized welcome email using the SendGrid app, adds the user to a CRM like HubSpot for future outreach, and logs the activity in a Google Sheets spreadsheet. + +- **Sync Profile Updates Across Platforms**: Establish a workflow that listens for profile updates in Clerk. When a user changes their info, Pipedream can propagate these changes to other platforms, such as updating their Slack profile, sending a notification to a Discord channel, and updating their record in an Airtable base. + +- **Manage Access with Webhooks**: Use Clerk's webhooks to trigger a Pipedream workflow that revokes or grants access to features or content in your app. For instance, when a subscription status changes in Stripe, use Pipedream to call Clerk's API to update the user's session or permissions accordingly, ensuring consistent access control. diff --git a/components/cleverreach/README.md b/components/cleverreach/README.md new file mode 100644 index 0000000000000..f0173c66d34cc --- /dev/null +++ b/components/cleverreach/README.md @@ -0,0 +1,11 @@ +# Overview + +The CleverReach API lets you automate email marketing operations and integrate with your data sources for personalized campaigns. On Pipedream, use this API to craft and manage subscriber lists, send targeted emails, and track campaign performance. With an event-driven platform like Pipedream, you can trigger workflows from numerous sources, manipulate data, and connect CleverReach to other apps to automate complex tasks. + +# Example Use Cases + +- **Sync New Users to CleverReach List**: When a user signs up on your platform, trigger a workflow that adds them to a specific CleverReach subscriber list. Pair this with a database like MySQL on Pipedream to log the addition or update subscriber details. + +- **Send Personalized Welcome Emails**: Use the CleverReach API to send a welcome email when someone joins a list. Enhance this with conditional logic on Pipedream to personalize the message based on subscriber data, such as location or interests. + +- **Automate Campaign Performance Reporting**: After sending a campaign, automate the retrieval of performance data via the CleverReach API. Combine this with Google Sheets on Pipedream to update a spreadsheet with open rates, click-through rates, and other metrics for easy analysis. diff --git a/components/clevertap/README.md b/components/clevertap/README.md new file mode 100644 index 0000000000000..3776d32a2d38a --- /dev/null +++ b/components/clevertap/README.md @@ -0,0 +1,11 @@ +# Overview + +CleverTap offers a powerful suite of tools for user engagement and analytics that can help you track user activities, segment users, and run targeted campaigns. Integrating Clevertap with Pipedream allows you to automate interactions with users, synchronize data across platforms, and create personalized marketing strategies. You can trigger workflows with real-time events, analyze user behavior, and leverage Pipedream's ability to connect to hundreds of other apps to enrich and act on your CleverTap data. + +# Example Use Cases + +- **User Segmentation and Email Campaigns**: Synchronize CleverTap segments to an email marketing service like Mailchimp using Pipedream. When a new segment is created in CleverTap, automatically add these users to Mailchimp and trigger a tailored email campaign. + +- **Real-Time User Activity Alerts**: Send instant notifications to Slack when a high-value user performs a specific action in your app. Pipedream can listen for CleverTap webhook events and post messages to a Slack channel, keeping your team updated on critical user engagements. + +- **Automated User Onboarding**: Create a workflow where new users added to CleverTap trigger a series of welcome messages via Twilio. Pipedream can capture the event, and use Twilio to send SMS or WhatsApp messages, making the onboarding process smooth and personal. diff --git a/components/click2mail2/README.md b/components/click2mail2/README.md new file mode 100644 index 0000000000000..8c25bed576cab --- /dev/null +++ b/components/click2mail2/README.md @@ -0,0 +1,11 @@ +# Overview + +The Click2Mail API enables automated mailing solutions, letting you integrate direct mail processes within your digital workflows. On Pipedream, you can harness this API to craft serverless workflows that interact with other apps and services. Create, manage, and send postal mail without leaving your digital ecosystem. Automate mail for marketing, invoicing, or any correspondence that requires a physical mail presence. + +# Example Use Cases + +- **Automated Invoice Mailing**: When a new invoice is created in your accounting software, such as QuickBooks, trigger a Pipedream workflow that sends the invoice details to Click2Mail. Click2Mail then prints and mails the invoice to the customer, streamlining the billing process. + +- **Postcard Marketing Campaign Trigger**: Use a CRM platform like HubSpot to trigger a Pipedream workflow when a new lead is added to a specific stage in your sales pipeline. The workflow then sends a custom postcard via Click2Mail, providing a personal touch to potential customers. + +- **Customer Thank You Notes**: After an order is marked as delivered in an e-commerce platform like Shopify, trigger a Pipedream workflow that sends a thank you note through Click2Mail. This gesture can enhance customer satisfaction and encourage repeat business. diff --git a/components/clickfunnels/README.md b/components/clickfunnels/README.md new file mode 100644 index 0000000000000..fb88cd9eec704 --- /dev/null +++ b/components/clickfunnels/README.md @@ -0,0 +1,11 @@ +# Overview + +The ClickFunnels API lets you interact programmatically with your ClickFunnels account, giving you control over your funnels, contacts, purchases, and more. With Pipedream, you can automate workflows that respond to a variety of triggers, such as new contacts or purchases. By connecting ClickFunnels with other apps available on Pipedream, you can streamline your sales process, sync data across platforms, and create powerful marketing automation. + +# Example Use Cases + +- **Sync New Contacts to a CRM**: Automatically add new ClickFunnels contacts to a CRM like Salesforce or HubSpot. Each time a contact is created in ClickFunnels, the workflow triggers and adds the contact to your CRM, keeping your sales team up-to-date. + +- **Send Custom Email Campaigns for New Purchases**: When a new purchase occurs in ClickFunnels, trigger a workflow that sends a personalized email via SendGrid or Mailgun. Tailor the content based on the purchased item to enhance customer experience and encourage repeat business. + +- **Update Google Sheets with Funnel Performance Data**: Collect performance data from your ClickFunnels account and log it into Google Sheets. This workflow can be set to run on a schedule, providing you with a regularly updated overview of how your funnels are performing. diff --git a/components/clickfunnels/actions/apply-tag-contact/apply-tag-contact.mjs b/components/clickfunnels/actions/apply-tag-contact/apply-tag-contact.mjs new file mode 100644 index 0000000000000..78edc066845f8 --- /dev/null +++ b/components/clickfunnels/actions/apply-tag-contact/apply-tag-contact.mjs @@ -0,0 +1,58 @@ +import clickfunnels from "../../clickfunnels.app.mjs"; + +export default { + key: "clickfunnels-apply-tag-contact", + name: "Apply Tag to Contact", + description: "Applies a tag to a contact. [See the documentation](https://developers.myclickfunnels.com/reference/createcontactsappliedtags)", + version: "0.0.1", + type: "action", + props: { + clickfunnels, + teamId: { + propDefinition: [ + clickfunnels, + "teamId", + ], + }, + workspaceId: { + propDefinition: [ + clickfunnels, + "workspaceId", + ({ teamId }) => ({ + teamId, + }), + ], + }, + contactId: { + propDefinition: [ + clickfunnels, + "contactId", + ({ workspaceId }) => ({ + workspaceId, + }), + ], + }, + tagId: { + propDefinition: [ + clickfunnels, + "tagId", + ({ workspaceId }) => ({ + workspaceId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.clickfunnels.applyTagToContact({ + $, + contactId: this.contactId, + data: { + contacts_applied_tag: { + tag_id: this.tagId, + }, + }, + }); + $.export("$summary", `Successfully applied tag with Id: ${this.tagId} to contact with Id: ${this.contactId}`); + return response; + }, +}; diff --git a/components/clickfunnels/actions/remove-tag-contact/remove-tag-contact.mjs b/components/clickfunnels/actions/remove-tag-contact/remove-tag-contact.mjs new file mode 100644 index 0000000000000..fe57f172d8c5c --- /dev/null +++ b/components/clickfunnels/actions/remove-tag-contact/remove-tag-contact.mjs @@ -0,0 +1,63 @@ +import clickfunnels from "../../clickfunnels.app.mjs"; + +export default { + key: "clickfunnels-remove-tag-contact", + name: "Remove Tag from Contact", + description: "Removes a specified tag from a contact. This action will take no effect if the specified tag doesn't exist on the contact. [See the documentation](https://developers.myclickfunnels.com/reference/removecontactsappliedtags)", + version: "0.0.1", + type: "action", + props: { + clickfunnels, + teamId: { + propDefinition: [ + clickfunnels, + "teamId", + ], + }, + workspaceId: { + propDefinition: [ + clickfunnels, + "workspaceId", + ({ teamId }) => ({ + teamId, + }), + ], + }, + contactId: { + propDefinition: [ + clickfunnels, + "contactId", + ({ workspaceId }) => ({ + workspaceId, + }), + ], + }, + tagId: { + propDefinition: [ + clickfunnels, + "tagId", + ({ workspaceId }) => ({ + workspaceId, + }), + ], + }, + }, + async run({ $ }) { + const { tags } = await this.clickfunnels.getContact({ + contactId: this.contactId, + }); + + const filteredTags = tags.filter((tag) => tag.id != this.tagId).map((tag) => tag.id); + const response = await this.clickfunnels.updateContact({ + contactId: this.contactId, + data: { + contact: { + tag_ids: filteredTags, + }, + }, + }); + + $.export("$summary", `Successfully removed tag ${this.tagId} from contact ${this.contactId}`); + return response; + }, +}; diff --git a/components/clickfunnels/actions/update-create-contact/update-create-contact.mjs b/components/clickfunnels/actions/update-create-contact/update-create-contact.mjs new file mode 100644 index 0000000000000..58ff5d1e9956a --- /dev/null +++ b/components/clickfunnels/actions/update-create-contact/update-create-contact.mjs @@ -0,0 +1,147 @@ +import clickfunnels from "../../clickfunnels.app.mjs"; +import { + clearObj, parseObject, +} from "../../common/utils.mjs"; + +export default { + key: "clickfunnels-update-create-contact", + name: "Update or Create Contact", + description: "Searches for a contact by email and updates it, or creates a new one if not found. [See the documentation](https://developers.myclickfunnels.com/reference/upsertcontact)", + version: "0.0.1", + type: "action", + props: { + clickfunnels, + teamId: { + propDefinition: [ + clickfunnels, + "teamId", + ], + }, + workspaceId: { + propDefinition: [ + clickfunnels, + "workspaceId", + ({ teamId }) => ({ + teamId, + }), + ], + }, + email: { + propDefinition: [ + clickfunnels, + "email", + ], + }, + firstName: { + type: "string", + label: "First Name", + description: "The contact's first name.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "The contact's last name.", + optional: true, + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The contact's phone number.", + optional: true, + }, + timeZone: { + type: "string", + label: "Time Zone", + description: "The contact's time zone.", + optional: true, + }, + fbUrl: { + type: "string", + label: "FB URL", + description: "The contact's Facebook URL.", + optional: true, + }, + twitterUrl: { + type: "string", + label: "Twitter URL", + description: "The contact's twitter URL.", + optional: true, + }, + instagramUrl: { + type: "string", + label: "Instagram URL", + description: "The contact's instagram URL.", + optional: true, + }, + linkedinUrl: { + type: "string", + label: "Linkedin URL", + description: "The contact's linkedin URL.", + optional: true, + }, + websiteUrl: { + type: "string", + label: "Website URL", + description: "The contact's website URL.", + optional: true, + }, + tagIds: { + propDefinition: [ + clickfunnels, + "tagId", + ({ workspaceId }) => ({ + workspaceId, + }), + ], + optional: true, + type: "string[]", + label: "Tag Ids", + description: "An empty array is ignored here. If you wish to remove all tags for a Contact, use the Update Contact endpoint. A non-empty array overwrites the existing array.", + }, + customAttributes: { + type: "object", + label: "Custom Attributes", + description: `Custom attributes are usually added in ClickFunnels to a contact when they submit forms that contain custom contact attributes. Here you can directly create them during contact modification. + Custom attributes are provided as key-value pairs: + A key that does not exist, will create a new custom contact attribute. + A key that already exists, will update the value of an existing custom contact attribute. + Empty or null values, will set the custom attribute values to empty strings. A key that has special characters or spaces will be automatically converted to snake_case (For example, 'Favorite Food! 🥑' will be converted to 'favorite_food'). + Empty keys will trigger a bad request response. Also, non-object inputs for custom_attributes(e.g. an array or string), it will be ignored. + Keys that are default properties on the Contact resource or variations of it will result in an error. E.g., 'first_name', 'First Name', etc. are not valid inputs. + `, + optional: true, + }, + }, + async run({ $ }) { + const { + data, status, + } = await this.clickfunnels.upsertContact({ + $, + returnFullResponse: true, + workspaceId: this.workspaceId, + data: { + contact: clearObj({ + email_address: this.email, + first_name: this.firstName, + last_name: this.lastName, + phone_number: this.phoneNumber, + time_zone: this.timeZone, + fb_url: this.fbUrl, + twitter_url: this.twitterUrl, + instagram_url: this.instagramUrl, + linkedin_url: this.linkedinUrl, + website_url: this.websiteUrl, + tag_ids: parseObject(this.tagIds), + custom_attributes: parseObject(this.customAttributes), + }), + }, + }); + + $.export("$summary", `Contact with Id: ${data.id} has been ${status === 200 + ? "updated" + : "created"} successfully.`); + return data; + }, + +}; diff --git a/components/clickfunnels/clickfunnels.app.mjs b/components/clickfunnels/clickfunnels.app.mjs index f56d8915ab69e..85613dee913b2 100644 --- a/components/clickfunnels/clickfunnels.app.mjs +++ b/components/clickfunnels/clickfunnels.app.mjs @@ -1,11 +1,230 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "clickfunnels", - propDefinitions: {}, + propDefinitions: { + tagId: { + type: "string", + label: "Tag Id", + description: "The unique identification of the tag.", + async options({ + workspaceId, prevContext: { after }, + }) { + const data = await this.listTags({ + workspaceId, + params: { + after, + }, + }); + + return { + options: data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })), + context: { + after: data.length + ? data[data.length - 1].id + : null, + }, + }; + }, + }, + teamId: { + type: "string", + label: "Team Id", + description: "The unique identification of the team.", + async options({ prevContext: { after } }) { + const data = await this.listTeams({ + params: { + after, + }, + }); + + return { + options: data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })), + context: { + after: data.length + ? data[data.length - 1].id + : null, + }, + }; + }, + }, + workspaceId: { + type: "string", + label: "Workspace Id", + description: "The unique identification of the workspace.", + async options({ + teamId, prevContext: { after }, + }) { + const data = await this.listWorkspaces({ + teamId, + params: { + after, + }, + }); + + return { + options: data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })), + context: { + after: data.length + ? data[data.length - 1].id + : null, + }, + }; + }, + }, + contactId: { + type: "string", + label: "Contact ID", + description: "The ID of the contact", + async options({ + workspaceId, prevContext: { after }, + }) { + const data = await this.listContacts({ + workspaceId, + params: { + after, + }, + }); + + return { + options: data.map(({ + id: value, email_address: label, + }) => ({ + label, + value, + })), + context: { + after: data.length + ? data[data.length - 1].id + : null, + }, + }; + }, + }, + email: { + type: "string", + label: "Email", + description: "The email address of the contact", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `https://${this.$auth.subdomain}.myclickfunnels.com/api/v2`; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.api_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + ...opts, + url: this._baseUrl() + path, + headers: this._headers(), + }); + }, + applyTagToContact({ + contactId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/contacts/${contactId}/applied_tags`, + ...opts, + }); + }, + getContact({ contactId }) { + return this._makeRequest({ + path: `/contacts/${contactId}`, + }); + }, + listContacts({ + workspaceId, ...opts + }) { + return this._makeRequest({ + path: `/workspaces/${workspaceId}/contacts`, + ...opts, + }); + }, + listTags({ + workspaceId, ...opts + }) { + return this._makeRequest({ + path: `/workspaces/${workspaceId}/contacts/tags`, + ...opts, + }); + }, + listTeams(opts = {}) { + return this._makeRequest({ + path: "/teams", + ...opts, + }); + }, + listWorkspaces({ + teamId, ...opts + }) { + return this._makeRequest({ + path: `/teams/${teamId}/workspaces`, + ...opts, + }); + }, + removeTagFromContact({ + contactId, tagId, + }) { + return this._makeRequest({ + method: "DELETE", + path: `/contacts/${contactId}/applied_tags/${tagId}`, + }); + }, + updateContact({ + contactId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/contacts/${contactId}`, + ...opts, + }); + }, + upsertContact({ + workspaceId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/workspaces/${workspaceId}/contacts/upsert`, + ...opts, + }); + }, + createWebhook({ + workspaceId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/workspaces/${workspaceId}/webhooks/outgoing/endpoints`, + ...opts, + }); + }, + deleteWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/outgoing/endpoints/${webhookId}`, + }); }, }, }; diff --git a/components/clickfunnels/common/utils.mjs b/components/clickfunnels/common/utils.mjs new file mode 100644 index 0000000000000..adf012089ff85 --- /dev/null +++ b/components/clickfunnels/common/utils.mjs @@ -0,0 +1,41 @@ +export const clearObj = (obj) => { + return Object.entries(obj) + .filter(([ + , + v, + ]) => (v != null && v != "" && JSON.stringify(v) != "{}")) + .reduce((acc, [ + k, + v, + ]) => ({ + ...acc, + [k]: (!Array.isArray(v) && v === Object(v)) + ? clearObj(v) + : v, + }), {}); +}; + +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/clickfunnels/package.json b/components/clickfunnels/package.json index d8d02efa7b384..a5f8930cd1443 100644 --- a/components/clickfunnels/package.json +++ b/components/clickfunnels/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/clickfunnels", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream ClickFunnels Components", "main": "clickfunnels.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" } -} \ No newline at end of file +} diff --git a/components/clickfunnels/sources/common/base.mjs b/components/clickfunnels/sources/common/base.mjs new file mode 100644 index 0000000000000..29b1879fb41d8 --- /dev/null +++ b/components/clickfunnels/sources/common/base.mjs @@ -0,0 +1,75 @@ +import clickfunnels from "../../clickfunnels.app.mjs"; + +export default { + props: { + clickfunnels, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + teamId: { + propDefinition: [ + clickfunnels, + "teamId", + ], + }, + workspaceId: { + propDefinition: [ + clickfunnels, + "workspaceId", + ({ teamId }) => ({ + teamId, + }), + ], + }, + name: { + type: "string", + label: "Webhook Name", + description: "The name of the webhook to identify in Clickfunnels.", + }, + }, + methods: { + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + _getHookId() { + return this.db.get("hookId"); + }, + generateMeta(body) { + return { + id: body.event_id, + summary: this.getSummary(body), + ts: Date.parse(body.created_at), + }; + }, + }, + hooks: { + async activate() { + const webhook = await this.clickfunnels.createWebhook({ + workspaceId: this.workspaceId, + data: { + webhooks_outgoing_endpoint: { + url: this.http.endpoint, + name: this.name, + event_type_ids: this.getEventTypes(), + }, + }, + }); + + this._setHookId(webhook.id); + }, + async deactivate() { + const hookId = this._getHookId(); + return await this.clickfunnels.deleteWebhook(hookId); + }, + }, + async run({ body }) { + this.http.respond({ + status: 200, + }); + + this.$emit(body, this.generateMeta(body)); + + }, +}; diff --git a/components/clickfunnels/sources/new-contact-identified-instant/new-contact-identified-instant.mjs b/components/clickfunnels/sources/new-contact-identified-instant/new-contact-identified-instant.mjs new file mode 100644 index 0000000000000..c0b59ba7da6a9 --- /dev/null +++ b/components/clickfunnels/sources/new-contact-identified-instant/new-contact-identified-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "clickfunnels-new-contact-identified-instant", + name: "New Contact Identified (Instant)", + description: "Emit new event when a fresh or formerly anonymous contact is identified via email address or contact number.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventTypes() { + return [ + "contact.identified", + ]; + }, + getSummary(body) { + return `New contact identified: ${body.data.email_address || body.data.phone_number}`; + }, + }, + sampleEmit, +}; diff --git a/components/clickfunnels/sources/new-contact-identified-instant/test-event.mjs b/components/clickfunnels/sources/new-contact-identified-instant/test-event.mjs new file mode 100644 index 0000000000000..d4d6398391b52 --- /dev/null +++ b/components/clickfunnels/sources/new-contact-identified-instant/test-event.mjs @@ -0,0 +1,39 @@ +export default { + "data": { + "id": 42, + "public_id": "rKVqwU", + "workspace_id": 42, + "anonymous": null, + "email_address": "test-523ccfac54e4f5c0d515@example.com", + "first_name": "Rolando", + "last_name": "Keeling", + "phone_number": "798-864-5411 x3310", + "time_zone": "Pacific Time (US & Canada)", + "uuid": "98d3ccf3-e2a4-4747-bec6-65931dc8c591", + "unsubscribed_at": null, + "last_notification_email_sent_at": null, + "fb_url": "https://www.facebook.com/example", + "twitter_url": "https://twitter.com/example", + "instagram_url": null, + "linkedin_url": "https://www.linkedin.com/in/example", + "website_url": "https://example.com", + "created_at": "2024-05-20T19:13:00.940Z", + "updated_at": "2024-05-20T19:13:00.940Z", + "tags": [ + { + "id": 44, + "public_id": "dOsEHJ", + "name": "Example Tag", + "color": "#54d728" + } + ], + "custom_attributes": { + "CustomKey": "MyText" + } + }, + "event_id": "61a4c32b-2bdf-4087-a137-00fe36534cb3", + "created_at": "2024-05-20T19:13:00.940Z", + "event_type": "contact.identified", + "subject_id": 1234567, + "subject_type": "Contact" +} \ No newline at end of file diff --git a/components/clickfunnels/sources/new-one-time-order-paid-instant/new-one-time-order-paid-instant.mjs b/components/clickfunnels/sources/new-one-time-order-paid-instant/new-one-time-order-paid-instant.mjs new file mode 100644 index 0000000000000..f9726d7ff8254 --- /dev/null +++ b/components/clickfunnels/sources/new-one-time-order-paid-instant/new-one-time-order-paid-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "clickfunnels-new-one-time-order-paid-instant", + name: "New One-Time Order Paid (Instant)", + description: "Emit new event when a one-time order gets paid by a customer.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventTypes() { + return [ + "one-time-order.invoice.paid", + ]; + }, + getSummary(body) { + return `New one time order with Id: ${body.data.id} paid successfully!`; + }, + }, + sampleEmit, +}; diff --git a/components/clickfunnels/sources/new-one-time-order-paid-instant/test-event.mjs b/components/clickfunnels/sources/new-one-time-order-paid-instant/test-event.mjs new file mode 100644 index 0000000000000..1836e1398a905 --- /dev/null +++ b/components/clickfunnels/sources/new-one-time-order-paid-instant/test-event.mjs @@ -0,0 +1,177 @@ +export default { + "id": 17, + "public_id": "EKUqVT", + "order_number": "542907", + "workspace_id": 42, + "contact_id": 114, + "total_amount": "123.00", + "currency": "USD", + "origination_channel_id": 23, + "origination_channel_type": "User", + "shipping_address_first_name": "Yolande", + "shipping_address_last_name": "Bogisich", + "shipping_address_organization_name": "Testing, Inc.", + "shipping_address_phone_number": "+19053871234", + "shipping_address_street_one": "3039 Buckridge Ridge", + "shipping_address_street_two": "Suite 139", + "shipping_address_city": "East Russshire", + "shipping_address_region": "WA", + "shipping_address_country": "US", + "shipping_address_postal_code": "63107-7452", + "billing_address_street_one": "249 Schiller Landing", + "billing_address_street_two": "Apt. 652", + "billing_address_city": "Lueilwitzton", + "billing_address_region": "WA", + "billing_address_country": "US", + "billing_address_postal_code": "42515-7383", + "page_id": 28, + "notes": "Id autem cumque laborum.", + "in_trial": false, + "billing_status": "pending", + "service_status": "pending", + "order_type": "one-time-order", + "next_charge_at": null, + "tax_amount": "0.00", + "trial_end_at": null, + "billing_payment_method_id": null, + "funnel_name": null, + "tag_ids": [ + 19 + ], + "discount_ids": [], + "activated_at": null, + "workspace_sharing_id": null, + "canceled_at": null, + "canceled_by": null, + "cancel_reason": null, + "cancel_description": null, + "churned_at": null, + "created_at": "2024-05-20T19:13:21.285Z", + "updated_at": "2024-05-20T19:13:21.285Z", + "phone_number": "194.668.1342 x8947", + "page_name": "Great Page", + "origination_channel_name": "", + "order_page": { + "id": 28, + "public_id": "equyXm", + "name": "Great Page" + }, + "contact": { + "id": 114, + "public_id": "vlbyJp", + "workspace_id": 42, + "anonymous": null, + "email_address": "test-c16faf7671680ea45d82@example.com", + "first_name": "Trinidad", + "last_name": "Schmeler", + "phone_number": "194.668.1342 x8947", + "time_zone": "Pacific Time (US & Canada)", + "uuid": "7a99f796-1300-4235-8d8c-c3592fcebff2", + "unsubscribed_at": null, + "last_notification_email_sent_at": null, + "fb_url": "https://www.facebook.com/example", + "twitter_url": "https://twitter.com/example", + "instagram_url": null, + "linkedin_url": "https://www.linkedin.com/in/example", + "website_url": "https://example.com", + "created_at": "2024-05-20T19:13:20.632Z", + "updated_at": "2024-05-20T19:13:20.632Z", + "tags": [ + { + "id": 134, + "public_id": "fsymYi", + "name": "Example Tag", + "color": "#141481" + } + ], + "custom_attributes": { + "CustomKey": "MyText" + } + }, + "contact_groups": [ + { + "id": 17, + "public_id": "jeuzZl", + "name": "Example Group" + } + ], + "workspace": { + "id": 42, + "public_id": "eQrklX", + "team_id": 42, + "name": "Example Workspace", + "subdomain": "example", + "created_at": "2024-05-20T19:13:20.612Z", + "updated_at": "2024-05-20T19:13:20.612Z" + }, + "segments": [ + { + "id": 17, + "public_id": "QYlSHq", + "name": "Example Segment" + } + ], + "line_items": [ + { + "id": 17, + "public_id": "QNOXex", + "order_id": 17, + "quantity": 1, + "prorated": null, + "created_at": "2024-02-17T13:33:28.947Z", + "updated_at": "2024-04-04T19:27:04.752Z", + "original_product": { + "id": 6503, + "public_id": "Ykznlj", + "name": "Cool Shirt" + }, + "products_price": { + "id": 27, + "public_id": "KNXRjo", + "amount": "29.0", + "currency": "usd", + "duration": null, + "interval": null, + "interval_count": null + }, + "products_variant": { + "id": 11, + "public_id": "OjzDND", + "name": "Cool Shirt", + "description": "", + "sku": "sku_5487923746" + } + }, + { + "id": 18, + "public_id": "IyriAS", + "order_id": 17, + "quantity": 1, + "prorated": null, + "created_at": null, + "updated_at": null, + "original_product": { + "id": 61, + "public_id": "PJodeN", + "name": "Lightweight Paper Lamp 80ed6b" + }, + "products_price": { + "id": 42, + "public_id": "GvnCsK", + "amount": "100.0", + "currency": "USD", + "duration": null, + "interval": null, + "interval_count": null + }, + "products_variant": { + "id": 43, + "public_id": "tXznVd", + "name": "Ergonomic Rubber Coat variant 8dbd", + "description": "Non ducimus nesciunt id.", + "sku": "ZIg6wOS0IOu4IBGj" + } + } + ], + "previous_line_item": null +} \ No newline at end of file diff --git a/components/clickfunnels/sources/new-subscription-invoice-paid-instant/new-subscription-invoice-paid-instant.mjs b/components/clickfunnels/sources/new-subscription-invoice-paid-instant/new-subscription-invoice-paid-instant.mjs new file mode 100644 index 0000000000000..226ace4a51954 --- /dev/null +++ b/components/clickfunnels/sources/new-subscription-invoice-paid-instant/new-subscription-invoice-paid-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "clickfunnels-new-subscription-invoice-paid-instant", + name: "New Subscription Invoice Paid (Instant)", + description: "Emit new event when a subscription fee is paid by a customer.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventTypes() { + return [ + "subscription.invoice.paid", + ]; + }, + getSummary(body) { + return `New subscription invoice with Id: ${body.data.id} paid successfully!`; + }, + }, + sampleEmit, +}; diff --git a/components/clickfunnels/sources/new-subscription-invoice-paid-instant/test-event.mjs b/components/clickfunnels/sources/new-subscription-invoice-paid-instant/test-event.mjs new file mode 100644 index 0000000000000..1836e1398a905 --- /dev/null +++ b/components/clickfunnels/sources/new-subscription-invoice-paid-instant/test-event.mjs @@ -0,0 +1,177 @@ +export default { + "id": 17, + "public_id": "EKUqVT", + "order_number": "542907", + "workspace_id": 42, + "contact_id": 114, + "total_amount": "123.00", + "currency": "USD", + "origination_channel_id": 23, + "origination_channel_type": "User", + "shipping_address_first_name": "Yolande", + "shipping_address_last_name": "Bogisich", + "shipping_address_organization_name": "Testing, Inc.", + "shipping_address_phone_number": "+19053871234", + "shipping_address_street_one": "3039 Buckridge Ridge", + "shipping_address_street_two": "Suite 139", + "shipping_address_city": "East Russshire", + "shipping_address_region": "WA", + "shipping_address_country": "US", + "shipping_address_postal_code": "63107-7452", + "billing_address_street_one": "249 Schiller Landing", + "billing_address_street_two": "Apt. 652", + "billing_address_city": "Lueilwitzton", + "billing_address_region": "WA", + "billing_address_country": "US", + "billing_address_postal_code": "42515-7383", + "page_id": 28, + "notes": "Id autem cumque laborum.", + "in_trial": false, + "billing_status": "pending", + "service_status": "pending", + "order_type": "one-time-order", + "next_charge_at": null, + "tax_amount": "0.00", + "trial_end_at": null, + "billing_payment_method_id": null, + "funnel_name": null, + "tag_ids": [ + 19 + ], + "discount_ids": [], + "activated_at": null, + "workspace_sharing_id": null, + "canceled_at": null, + "canceled_by": null, + "cancel_reason": null, + "cancel_description": null, + "churned_at": null, + "created_at": "2024-05-20T19:13:21.285Z", + "updated_at": "2024-05-20T19:13:21.285Z", + "phone_number": "194.668.1342 x8947", + "page_name": "Great Page", + "origination_channel_name": "", + "order_page": { + "id": 28, + "public_id": "equyXm", + "name": "Great Page" + }, + "contact": { + "id": 114, + "public_id": "vlbyJp", + "workspace_id": 42, + "anonymous": null, + "email_address": "test-c16faf7671680ea45d82@example.com", + "first_name": "Trinidad", + "last_name": "Schmeler", + "phone_number": "194.668.1342 x8947", + "time_zone": "Pacific Time (US & Canada)", + "uuid": "7a99f796-1300-4235-8d8c-c3592fcebff2", + "unsubscribed_at": null, + "last_notification_email_sent_at": null, + "fb_url": "https://www.facebook.com/example", + "twitter_url": "https://twitter.com/example", + "instagram_url": null, + "linkedin_url": "https://www.linkedin.com/in/example", + "website_url": "https://example.com", + "created_at": "2024-05-20T19:13:20.632Z", + "updated_at": "2024-05-20T19:13:20.632Z", + "tags": [ + { + "id": 134, + "public_id": "fsymYi", + "name": "Example Tag", + "color": "#141481" + } + ], + "custom_attributes": { + "CustomKey": "MyText" + } + }, + "contact_groups": [ + { + "id": 17, + "public_id": "jeuzZl", + "name": "Example Group" + } + ], + "workspace": { + "id": 42, + "public_id": "eQrklX", + "team_id": 42, + "name": "Example Workspace", + "subdomain": "example", + "created_at": "2024-05-20T19:13:20.612Z", + "updated_at": "2024-05-20T19:13:20.612Z" + }, + "segments": [ + { + "id": 17, + "public_id": "QYlSHq", + "name": "Example Segment" + } + ], + "line_items": [ + { + "id": 17, + "public_id": "QNOXex", + "order_id": 17, + "quantity": 1, + "prorated": null, + "created_at": "2024-02-17T13:33:28.947Z", + "updated_at": "2024-04-04T19:27:04.752Z", + "original_product": { + "id": 6503, + "public_id": "Ykznlj", + "name": "Cool Shirt" + }, + "products_price": { + "id": 27, + "public_id": "KNXRjo", + "amount": "29.0", + "currency": "usd", + "duration": null, + "interval": null, + "interval_count": null + }, + "products_variant": { + "id": 11, + "public_id": "OjzDND", + "name": "Cool Shirt", + "description": "", + "sku": "sku_5487923746" + } + }, + { + "id": 18, + "public_id": "IyriAS", + "order_id": 17, + "quantity": 1, + "prorated": null, + "created_at": null, + "updated_at": null, + "original_product": { + "id": 61, + "public_id": "PJodeN", + "name": "Lightweight Paper Lamp 80ed6b" + }, + "products_price": { + "id": 42, + "public_id": "GvnCsK", + "amount": "100.0", + "currency": "USD", + "duration": null, + "interval": null, + "interval_count": null + }, + "products_variant": { + "id": 43, + "public_id": "tXznVd", + "name": "Ergonomic Rubber Coat variant 8dbd", + "description": "Non ducimus nesciunt id.", + "sku": "ZIg6wOS0IOu4IBGj" + } + } + ], + "previous_line_item": null +} \ No newline at end of file diff --git a/components/clickhelp/README.md b/components/clickhelp/README.md new file mode 100644 index 0000000000000..1697da96acbcc --- /dev/null +++ b/components/clickhelp/README.md @@ -0,0 +1,14 @@ +# Overview + +The ClickHelp API allows technical writers to automate and interact with the ClickHelp documentation platform programmatically. With this API, Pipedream users can create, update, retrieve, and manage documentation projects, topics, and users within ClickHelp. It opens up opportunities to streamline documentation workflows, integrate with content management systems, and trigger notifications based on documentation changes or reviews. + +# Example Use Cases + +- **Automated Documentation Backup** + Build a workflow in Pipedream to automatically back up ClickHelp projects to a cloud storage service like Google Drive or Dropbox at regular intervals. This ensures that you always have an up-to-date copy of your documentation, providing an extra layer of data protection. + +- **Documentation Review and Approval Process** + Create a Pipedream workflow that triggers when a new topic is added or updated in ClickHelp. It can notify stakeholders through Slack or email to review the content. Once reviewed, the workflow can update the topic status in ClickHelp based on the feedback received, streamlining the review process. + +- **Syncing Help Content with a CRM** + Develop a workflow on Pipedream that synchronizes ClickHelp documentation with a CRM like Salesforce. When new documentation is published or existing documents are updated in ClickHelp, the workflow can update the corresponding records or files in Salesforce, ensuring the sales team has access to the latest help content. diff --git a/components/clickhelp/actions/create-project-backup/create-project-backup.mjs b/components/clickhelp/actions/create-project-backup/create-project-backup.mjs new file mode 100644 index 0000000000000..7d0caac36d3bd --- /dev/null +++ b/components/clickhelp/actions/create-project-backup/create-project-backup.mjs @@ -0,0 +1,50 @@ +import clickhelp from "../../clickhelp.app.mjs"; +import { pollTaskStatus } from "../../common/utils.mjs"; + +export default { + key: "clickhelp-create-project-backup", + name: "Create Project Backup", + description: "Generates a backup of the specified project. This action ensures you have a safe copy of your project in case of any unpredicted data loss. [See the documentation](https://clickhelp.com/software-documentation-tool/user-manual/api-create-project-backup.html)", + version: "0.0.1", + type: "action", + props: { + clickhelp, + projectId: { + propDefinition: [ + clickhelp, + "projectId", + ], + }, + outputFileName: { + type: "string", + label: "Output File Name", + description: "The full file name of the output file, including the file path that starts with `Storage/`. Example: `Storage/Backups/Project-backup_2023-03-13_03-46-07.zip`", + }, + waitForCompletion: { + propDefinition: [ + clickhelp, + "waitForCompletion", + ], + }, + }, + async run({ $ }) { + let response = await this.clickhelp.createProjectBackup({ + $, + projectId: this.projectId, + params: { + action: "download", + }, + data: { + outputFileName: this.outputFileName, + }, + }); + + const { taskKey } = response; + if (this.waitForCompletion) { + response = await pollTaskStatus($, this, taskKey); + } + + $.export("$summary", "Successfully created project backup."); + return response; + }, +}; diff --git a/components/clickhelp/actions/create-publication/create-publication.mjs b/components/clickhelp/actions/create-publication/create-publication.mjs new file mode 100644 index 0000000000000..1b9333e49b749 --- /dev/null +++ b/components/clickhelp/actions/create-publication/create-publication.mjs @@ -0,0 +1,83 @@ +import clickhelp from "../../clickhelp.app.mjs"; +import { pollTaskStatus } from "../../common/utils.mjs"; + +export default { + key: "clickhelp-create-publication", + name: "Create Publication", + description: "Creates a new publication from the designated project. This action allows you to share your content with others in various formats. [See the documentation](https://clickhelp.com/software-documentation-tool/user-manual/api-publish-project.html)", + version: "0.0.1", + type: "action", + props: { + clickhelp, + projectId: { + propDefinition: [ + clickhelp, + "projectId", + ], + }, + pubId: { + type: "string", + label: "Pub ID", + description: "The ID of the publication to create", + }, + pubName: { + type: "string", + label: "Pub Name", + description: "The name of the publication to create", + }, + isPublishOnlyReadyTopics: { + type: "boolean", + label: "Publish Only Ready Topics?", + description: "Whether to publish only topics in the Ready status", + optional: true, + }, + pubVisibility: { + type: "string", + label: "Pub Visibility", + description: "The target publication visibility. Defalts to `Public`.", + optional: true, + options: [ + "Public", + "Private", + "Restricted", + ], + default: "Public", + }, + nodeIds: { + propDefinition: [ + clickhelp, + "nodeIds", + (c) => ({ + projectId: c.projectId, + }), + ], + }, + waitForCompletion: { + propDefinition: [ + clickhelp, + "waitForCompletion", + ], + }, + }, + async run({ $ }) { + let response = await this.clickhelp.createPublication({ + $, + projectId: this.projectId, + data: { + pubId: this.pubId, + pubName: this.pubName, + isPublishOnlyReadyTopics: this.isPublishOnlyReadyTopics, + pubVisibility: this.pubVisibility, + publishedTocNodeIds: this.nodeIds, + }, + }); + + const { taskKey } = response; + if (this.waitForCompletion) { + response = await pollTaskStatus($, this, taskKey); + } + + $.export("$summary", "Successfully created publication."); + return response; + }, +}; diff --git a/components/clickhelp/actions/create-topic/create-topic.mjs b/components/clickhelp/actions/create-topic/create-topic.mjs new file mode 100644 index 0000000000000..2dcf3a7dfcc55 --- /dev/null +++ b/components/clickhelp/actions/create-topic/create-topic.mjs @@ -0,0 +1,124 @@ +import clickhelp from "../../clickhelp.app.mjs"; + +export default { + key: "clickhelp-create-topic", + name: "Create Topic", + description: "Produces a new topic in the existing project. A useful action for starting a new chapter or section within your project. [See the documentation](https://clickhelp.com/software-documentation-tool/user-manual/api-create-topic.html)", + version: "0.0.1", + type: "action", + props: { + clickhelp, + projectId: { + propDefinition: [ + clickhelp, + "projectId", + ], + }, + assigneeUserName: { + propDefinition: [ + clickhelp, + "userName", + (c) => ({ + projectId: c.projectId, + }), + ], + label: "Assignee Username", + description: "Topic assignee's login", + }, + id: { + type: "string", + label: "ID", + description: "The ID of the topic", + }, + ownerUserName: { + propDefinition: [ + clickhelp, + "userName", + (c) => ({ + projectId: c.projectId, + }), + ], + label: "Owner Username", + description: "Topic owner's login", + }, + statusName: { + type: "string", + label: "Status Name", + description: "Topic's workflow status", + options: [ + "Draft", + "Ready", + ], + }, + body: { + type: "string", + label: "Body", + description: "The HTML content of the topic", + optional: true, + }, + isShowInToc: { + type: "boolean", + label: "Show in TOC?", + description: "Whether the topic's TOC node is shown in the table of contents in publications. Sets the corresponding option in the topic's properties. `false` by default", + optional: true, + }, + parentTocNodeId: { + propDefinition: [ + clickhelp, + "nodeIds", + (c) => ({ + projectId: c.projectId, + }), + ], + type: "string", + label: "Parent Node ID", + description: "The unique identifier of the parent TOC node. Specifying `null` will put the topic on the root level.", + optional: true, + }, + title: { + type: "string", + label: "Title", + description: "The topic title", + optional: true, + }, + tocNodeCaption: { + type: "string", + label: "TOC Node Caption", + description: "Custom TOC node caption. If not specified, the topic title is used instead", + optional: true, + }, + tocNodeOrdinalNo: { + type: "integer", + label: "TOC Node Ordinal Number", + description: "The number indicating the position of the topic's TOC node in the table of contents. If not specified, will create the topic on the last position on the respective level.", + optional: true, + }, + indexKeywords: { + type: "string[]", + label: "Index Keywords", + description: "An array of strings containing Index Keywords to set when creating a topic", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.clickhelp.createTopic({ + $, + projectId: this.projectId, + data: { + assigneeUserName: this.assigneeUserName, + id: this.id, + ownerUserName: this.ownerUserName, + statusName: this.statusName, + body: this.body, + isShownInToc: this.isShownInToc, + parentToNodeId: this.parentToNodeId, + title: this.title, + tocNodeCaption: this.tocNodeCaption, + tocNodeOrdinalNo: this.tocNodeOrdinalNo, + indexKeywords: this.indexKeywords, + }, + }); + $.export("$summary", `Successfully created topic with taskKey ${response.id}`); + return response; + }, +}; diff --git a/components/clickhelp/clickhelp.app.mjs b/components/clickhelp/clickhelp.app.mjs new file mode 100644 index 0000000000000..a65a149838e47 --- /dev/null +++ b/components/clickhelp/clickhelp.app.mjs @@ -0,0 +1,147 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "clickhelp", + propDefinitions: { + projectId: { + type: "string", + label: "Project ID", + description: "Identifier of a project", + async options() { + const projects = await this.listProjects(); + return projects?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })) || []; + }, + }, + nodeIds: { + type: "string[]", + label: "Node IDs", + description: "An array of strings containing IDs of the TOC nodes to publish. If not specified, the entire project is published.", + optional: true, + async options({ projectId }) { + const nodes = await this.listNodes({ + projectId, + }); + return nodes?.map(({ + id: value, topicId: label, + }) => ({ + value, + label, + })) || []; + }, + }, + userName: { + type: "string", + label: "User Name", + description: "The username of a user", + async options() { + const users = await this.listUsers(); + return users?.map(({ userName }) => userName) || []; + }, + }, + waitForCompletion: { + type: "boolean", + label: "Wait for Completion", + description: "Set to `true` to poll the API in 3-second intervals until the task is completed", + optional: true, + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.portal_domain}/api/v1`; + }, + _auth() { + return { + username: `${this.$auth.email}`, + password: `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + auth: this._auth(), + ...opts, + }); + }, + listProjects(opts = {}) { + return this._makeRequest({ + path: "/projects", + ...opts, + }); + }, + listNodes({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/projects/${projectId}/toc/nodes`, + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users", + ...opts, + }); + }, + listTopics({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/projects/${projectId}/articles`, + ...opts, + }); + }, + getTaskStatus({ + taskKey, ...opts + }) { + return this._makeRequest({ + path: `/tasks/${taskKey}`, + ...opts, + }); + }, + createProjectBackup({ + projectId, params, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/projects/${projectId}`, + params: { + ...params, + action: "download", + }, + ...opts, + }); + }, + createPublication({ + projectId, params, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/projects/${projectId}`, + params: { + ...params, + action: "publish", + }, + ...opts, + }); + }, + createTopic({ + projectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/projects/${projectId}/articles`, + ...opts, + }); + }, + }, +}; diff --git a/components/clickhelp/common/utils.mjs b/components/clickhelp/common/utils.mjs new file mode 100644 index 0000000000000..0fdb59b983376 --- /dev/null +++ b/components/clickhelp/common/utils.mjs @@ -0,0 +1,12 @@ +export async function pollTaskStatus($, ctx, taskKey) { + let response = {}; + const timer = (ms) => new Promise((res) => setTimeout(res, ms)); + while (!response.isSucceeded) { + response = await ctx.clickhelp.getTaskStatus({ + $, + taskKey, + }); + await timer(3000); + } + return response; +} diff --git a/components/clickhelp/package.json b/components/clickhelp/package.json new file mode 100644 index 0000000000000..9bdfcbfad4808 --- /dev/null +++ b/components/clickhelp/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/clickhelp", + "version": "0.1.0", + "description": "Pipedream ClickHelp Components", + "main": "clickhelp.app.mjs", + "keywords": [ + "pipedream", + "clickhelp" + ], + "homepage": "https://pipedream.com/apps/clickhelp", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.1" + } +} diff --git a/components/clickhelp/sources/common/base.mjs b/components/clickhelp/sources/common/base.mjs new file mode 100644 index 0000000000000..51a54310120e8 --- /dev/null +++ b/components/clickhelp/sources/common/base.mjs @@ -0,0 +1,83 @@ +import clickhelp from "../../clickhelp.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + clickhelp, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + projectId: { + propDefinition: [ + clickhelp, + "projectId", + ], + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + getArgs() { + return { + projectId: this.projectId, + }; + }, + isRelevant() { + return true; + }, + emitEvent(item) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }, + getTsField() { + throw new Error("getTsField is not implemented"); + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + const resourceFn = this.getResourceFn(); + const args = this.getArgs(); + const items = await resourceFn(args); + if (!items.length) { + return; + } + let results = []; + for (const item of items) { + const ts = Date.parse(item[this.getTsField()]); + if (ts >= lastTs) { + maxTs = Math.max(ts, maxTs); + if (this.isRelevant(item)) { + results.push(item); + } + } + } + if (max) { + results = results.slice(max * -1); + } + results.forEach((item) => this.emitEvent(item)); + this._setLastTs(maxTs); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/clickhelp/sources/new-topic-created/new-topic-created.mjs b/components/clickhelp/sources/new-topic-created/new-topic-created.mjs new file mode 100644 index 0000000000000..2d217903a9d58 --- /dev/null +++ b/components/clickhelp/sources/new-topic-created/new-topic-created.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "clickhelp-new-topic-created", + name: "New Topic Created", + description: "Emit new event when a topic is created. [See the documentation](https://clickhelp.com/software-documentation-tool/user-manual/api-get-all-topics-from-project.html)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTsField() { + return "createdOn"; + }, + getResourceFn() { + return this.clickhelp.listTopics; + }, + generateMeta(topic) { + return { + id: topic.id, + summary: `New Topic with ID ${topic.id}`, + ts: Date.parse(topic.createdOn), + }; + }, + }, + sampleEmit, +}; diff --git a/components/clickhelp/sources/new-topic-created/test-event.mjs b/components/clickhelp/sources/new-topic-created/test-event.mjs new file mode 100644 index 0000000000000..db84c01a0eced --- /dev/null +++ b/components/clickhelp/sources/new-topic-created/test-event.mjs @@ -0,0 +1,21 @@ +export default { + "assigneeUserName": "admin", + "body": null, + "createdOn": "2024-03-27T19:28:30", + "ftsSnippetHtml": null, + "ftsTitleHtml": null, + "fullUrl": "https://docs.hedron.org.try.clickhelp.co/articles/project-sample-project/1", + "html": null, + "id": "1", + "indexKeywords": [], + "modifiedOn": "2024-03-27T19:28:30", + "ownerUserName": "admin", + "projectId": "project-sample-project", + "projectTitle": "Sample Project", + "projectUrl": "project-sample-project", + "smartLink": "https://docs.hedron.org.try.clickhelp.co/smart/project-sample-project/1", + "statusName": "Draft", + "title": "New Topic", + "tocNodeId": "9dd629e1-7083-4f16-a7e0-b9d3dd5ed759", + "url": "1" +} \ No newline at end of file diff --git a/components/clickhelp/sources/topic-updated/test-event.mjs b/components/clickhelp/sources/topic-updated/test-event.mjs new file mode 100644 index 0000000000000..73547a7f68b8f --- /dev/null +++ b/components/clickhelp/sources/topic-updated/test-event.mjs @@ -0,0 +1,21 @@ +export default { + "assigneeUserName": "admin", + "body": null, + "createdOn": "2024-03-26T15:20:30", + "ftsSnippetHtml": null, + "ftsTitleHtml": null, + "fullUrl": "https://docs.hedron.org.try.clickhelp.co/articles/project-sample-project/1", + "html": null, + "id": "1", + "indexKeywords": [], + "modifiedOn": "2024-03-27T19:28:30", + "ownerUserName": "admin", + "projectId": "project-sample-project", + "projectTitle": "Sample Project", + "projectUrl": "project-sample-project", + "smartLink": "https://docs.hedron.org.try.clickhelp.co/smart/project-sample-project/1", + "statusName": "Draft", + "title": "New Topic", + "tocNodeId": "9dd629e1-7083-4f16-a7e0-b9d3dd5ed759", + "url": "1" +} \ No newline at end of file diff --git a/components/clickhelp/sources/topic-updated/topic-updated.mjs b/components/clickhelp/sources/topic-updated/topic-updated.mjs new file mode 100644 index 0000000000000..0a48ef4d71bae --- /dev/null +++ b/components/clickhelp/sources/topic-updated/topic-updated.mjs @@ -0,0 +1,33 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "clickhelp-topic-updated", + name: "Topic Updated", + description: "Emit new event when a topic is updated. [See the documentation](https://clickhelp.com/software-documentation-tool/user-manual/api-get-all-topics-from-project.html)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + isRelevant(item) { + return item.createdOn !== item.modifiedOn; + }, + getTsField() { + return "modifiedOn"; + }, + getResourceFn() { + return this.clickhelp.listTopics; + }, + generateMeta(topic) { + const ts = Date.parse(topic.modifiedOn); + return { + id: `${topic.id}-${ts}`, + summary: `Topic Updated with ID ${topic.id}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/clickmeeting/README.md b/components/clickmeeting/README.md index 89d77c1300de5..179f7e2b06892 100644 --- a/components/clickmeeting/README.md +++ b/components/clickmeeting/README.md @@ -1,12 +1,11 @@ # Overview -With ClickMeeting, you can easily build video conferencing and webinar -solutions. The ClickMeeting API allows you to easily integrate the power of -video conferencing into your own website or application. +The ClickMeeting API taps into the functionality of the ClickMeeting platform, allowing you to streamline your webinar and online meeting management directly through Pipedream. Automate webinar creation, manage attendees, and extract analytics. By leveraging Pipedream's capabilities, you can create workflows that trigger on specific events, orchestrate data flow between apps, and automate repetitive tasks – all this without the hassle of managing server infrastructure. -Here are some examples of what you can build with the ClickMeeting API: +# Example Use Cases -- A video conferencing solution for your website or application -- A webinar solution for your website or application -- A solution for easily integrating video conferencing into your existing - website or application +- **Automated Webinar Setup and Follow-Up**: Automatically schedule webinars in ClickMeeting whenever a new event is added to your Google Calendar. Post-event, send personalized follow-up emails to attendees via SendGrid, including links to recorded sessions and additional resources. + +- **Dynamic Attendee Management**: Sync new sign-ups from your Eventbrite events to ClickMeeting registrants. Use Pipedream's SQL service to maintain a database of attendees, enabling advanced queries for insights and targeted post-webinar campaigns. + +- **Real-Time Analytics Reporting**: Trigger a Pipedream workflow whenever a webinar concludes to fetch detailed analytics from ClickMeeting. Compile a report and upload it to Google Sheets, then send a notification with key metrics to your team's Slack channel. diff --git a/components/clicksend/README.md b/components/clicksend/README.md index 257be348aacc9..40157e7b311c6 100644 --- a/components/clicksend/README.md +++ b/components/clicksend/README.md @@ -1,15 +1,11 @@ # Overview -With ClickSend's SMS API, you can: +The ClickSend SMS API unlocks the potential to integrate robust messaging capabilities into your workflows. With Pipedream, you can not only send SMS messages programmatically but also automate notifications, streamline communication based on events, and much more. Whether you're confirming orders, alerting staff, or engaging with customers, ClickSend and Pipedream make these tasks seamless. -- Send SMS messages -- Check SMS delivery status -- Receive inbound SMS messages -- Set up SMS webhooks +# Example Use Cases -Here are some examples of what you can build with the SMS API: +- **E-commerce Order Confirmation**: Automate order confirmation messages by connecting ClickSend with a Shopify trigger. When a new order is placed, Pipedream listens for the event and triggers the ClickSend action to send an SMS to the customer, confirming their purchase and providing order details. -- A service that sends SMS messages to users -- A system that tracks the delivery status of SMS messages -- A service that receioves inbound SMS messages -- A system that sets up SMS webhooks +- **CRM Lead Follow-up**: Pair ClickSend with Salesforce or HubSpot. Each time a new lead is added to your CRM, a workflow is triggered on Pipedream, sending a personalized welcome SMS via ClickSend, ensuring a timely touchpoint that could increase conversion rates. + +- **Downtime Alert System**: Integrate ClickSend with monitoring tools like Datadog or Uptime Robot. If your service experiences downtime, Pipedream triggers ClickSend to send an SMS to the IT support team, enabling swift action to resolve the issue and minimize impact. diff --git a/components/clicksend/actions/create-contact/create-contact.mjs b/components/clicksend/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..a5582d20bdfbd --- /dev/null +++ b/components/clicksend/actions/create-contact/create-contact.mjs @@ -0,0 +1,78 @@ +import app from "../../clicksend.app.mjs"; + +export default { + key: "clicksend-create-contact", + name: "Create Contact", + description: "Creates a new contact in a specific list. [See the documentation](https://developers.clicksend.com/docs/rest/v3/#create-new-contact)", + version: "0.0.1", + type: "action", + props: { + app, + listId: { + propDefinition: [ + app, + "listId", + ], + }, + phoneNumber: { + propDefinition: [ + app, + "phoneNumber", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + faxNumber: { + propDefinition: [ + app, + "faxNumber", + ], + }, + firstName: { + propDefinition: [ + app, + "firstName", + ], + }, + }, + methods: { + createContact({ + listId, ...args + } = {}) { + return this.app.post({ + path: `/lists/${listId}/contacts`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + createContact, + listId, + name, + phoneNumber, + email, + faxNumber, + firstName, + } = this; + + const response = await createContact({ + $, + listId, + data: { + name, + phone_number: phoneNumber, + email, + fax_number: faxNumber, + first_name: firstName, + }, + }); + + $.export("$summary", `Successfully created new contact with ID \`${response.data.contact_id}\``); + return response; + }, +}; diff --git a/components/clicksend/actions/send-mms/send-mms.mjs b/components/clicksend/actions/send-mms/send-mms.mjs new file mode 100644 index 0000000000000..9d36c992c4690 --- /dev/null +++ b/components/clicksend/actions/send-mms/send-mms.mjs @@ -0,0 +1,77 @@ +import app from "../../clicksend.app.mjs"; + +export default { + key: "clicksend-send-mms", + name: "Send MMS", + description: "Sends a new MMS to one or multiple recipients. [See the documentation](https://developers.clicksend.com/docs/rest/v3/#send-mms)", + version: "0.0.1", + type: "action", + props: { + app, + from: { + propDefinition: [ + app, + "from", + ], + }, + to: { + optional: false, + propDefinition: [ + app, + "to", + ], + }, + subject: { + type: "string", + label: "Subject", + description: "The subject of the MMS.", + }, + body: { + propDefinition: [ + app, + "body", + ], + }, + mediaFile: { + type: "string", + label: "Media File URL", + description: "The URL to the media file you want to send via MMS. Eg `http://yourdomain.com/tpLaX6A.gif`", + }, + }, + methods: { + sendMms(args = {}) { + return this.app.post({ + path: "/mms/send", + ...args, + }); + }, + }, + async run({ $ }) { + const { + sendMms, + from, + body, + to, + mediaFile, + subject, + } = this; + + const response = await sendMms({ + $, + data: { + media_file: mediaFile, + messages: [ + { + from, + to, + subject, + body, + }, + ], + }, + }); + + $.export("$summary", `Successfully sent MMS with ID \`${response.data.messages[0].message_id}\``); + return response; + }, +}; diff --git a/components/clicksend/actions/send-sms/send-sms.mjs b/components/clicksend/actions/send-sms/send-sms.mjs new file mode 100644 index 0000000000000..cbc77273bd40c --- /dev/null +++ b/components/clicksend/actions/send-sms/send-sms.mjs @@ -0,0 +1,71 @@ +import app from "../../clicksend.app.mjs"; + +export default { + key: "clicksend-send-sms", + name: "Send SMS", + description: "Sends a new SMS to one or several recipients. [See the documentation](https://developers.clicksend.com/docs/rest/v3/#send-sms-message-s)", + version: "0.0.1", + type: "action", + props: { + app, + from: { + propDefinition: [ + app, + "from", + ], + }, + to: { + propDefinition: [ + app, + "to", + ], + }, + listId: { + optional: true, + propDefinition: [ + app, + "listId", + ], + }, + body: { + propDefinition: [ + app, + "body", + ], + }, + }, + methods: { + sendSms(args = {}) { + return this.app.post({ + path: "/sms/send", + ...args, + }); + }, + }, + async run({ $ }) { + const { + sendSms, + from, + body, + to, + listId, + } = this; + + const response = await sendSms({ + $, + data: { + messages: [ + { + from, + body, + to, + list_id: listId, + }, + ], + }, + }); + + $.export("$summary", `Successfully sent SMS with ID \`${response.data.messages[0].message_id}\``); + return response; + }, +}; diff --git a/components/clicksend/clicksend.app.mjs b/components/clicksend/clicksend.app.mjs index fb3962db0db09..b5061385003e7 100644 --- a/components/clicksend/clicksend.app.mjs +++ b/components/clicksend/clicksend.app.mjs @@ -1,11 +1,185 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "clicksend", - propDefinitions: {}, + propDefinitions: { + listId: { + type: "integer", + label: "List ID", + description: "The ID of the list to add the contact to.", + async options({ + page, prevContext: { hasMore }, + }) { + if (hasMore === false) { + return []; + } + const { + data: { data }, + next_page_url: nextPageUrl, + } = await this.getLists({ + params: { + page: page + 1, + limit: constants.DEFAULT_LIMIT, + }, + }); + const options = data?.map(({ + list_id: value, list_name: label, + }) => ({ + label, + value, + })) || []; + return { + options, + context: { + hasMore: !!nextPageUrl, + }, + }; + }, + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number of the contact, in [E.164](https://en.wikipedia.org/wiki/E.164) format. Must be provided if no **Fax Number** or **Email**.", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "The email address of the contact. Must be provided if no **Phone Number** or **Fax Number**.", + optional: true, + }, + faxNumber: { + type: "string", + label: "Fax Number", + description: "The fax number of the contact. Must be provided if no **Phone Number** or **Email**.", + optional: true, + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the contact.", + optional: true, + }, + from: { + type: "string", + label: "Sender ID", + description: "The sender ID to use when sending the SMS or MMS message.", + async options() { + const { data } = await this.getAccount(); + return [ + { + label: data.username, + value: data.user_id, + }, + ]; + }, + }, + body: { + type: "string", + label: "Message", + description: "The SMS or MMS message to send.", + }, + to: { + type: "string", + label: "Recipient Phone Number", + description: "The phone number of the recipient, in [E.164](https://en.wikipedia.org/wiki/E.164) format.", + optional: true, + }, + dedicatedNumber: { + type: "string", + label: "Dedicated Number", + description: "", + async options({ + page, prevContext: { hasMore }, + }) { + if (hasMore === false) { + return [ + { + label: "All Numbers", + value: "*", + }, + ]; + } + const { + data: { data }, + next_page_url: nextPageUrl, + } = await this.getNumbers({ + params: { + page: page + 1, + limit: constants.DEFAULT_LIMIT, + }, + }); + const options = data?.map(({ dedicated_number: value }) => value) || []; + return { + options, + context: { + hasMore: !!nextPageUrl, + }, + }; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Content-Type": "application/json", + }; + }, + getAuth() { + const { + username, + api_key: password, + } = this.$auth; + return { + username, + password, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + auth: this.getAuth(), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + getAccount(args = {}) { + return this._makeRequest({ + path: "/account", + ...args, + }); + }, + getLists(args = {}) { + return this._makeRequest({ + path: "/lists", + ...args, + }); + }, + getNumbers(args = {}) { + return this._makeRequest({ + path: "/numbers", + ...args, + }); }, }, }; diff --git a/components/clicksend/common/constants.mjs b/components/clicksend/common/constants.mjs new file mode 100644 index 0000000000000..a0c6f058dd524 --- /dev/null +++ b/components/clicksend/common/constants.mjs @@ -0,0 +1,54 @@ +const BASE_URL = "https://rest.clicksend.com"; +const VERSION_PATH = "/v3"; +const WEBHOOK_ID = "webhookId"; +const DEFAULT_LIMIT = 50; + +const DISABLED = 0; +const ENABLED = 1; + +const MSG_SEARCH_TYPE = { + ANY_MSG: 0, + STARTS_WITH: 1, + CONTAINS: 2, + DOES_NOT_CONTAIN: 3, +}; + +const ACTION = { + AUTO_REPLY: "AUTO_REPLY", + EMAIL_USER: "EMAIL_USER", + EMAIL_FIXED: "EMAIL_FIXED", + URL: "URL", + SMS: "SMS", + POLL: "POLL", + GROUP_SMS: "GROUP_SMS", + MOVE_CONTACT: "MOVE_CONTACT", + CREATE_CONTACT: "CREATE_CONTACT", + CREATE_CONTACT_PLUS_EMAIL: "CREATE_CONTACT_PLUS_EMAIL", + CREATE_CONTACT_PLUS_NAME_EMAIL: "CREATE_CONTACT_PLUS_NAME_EMAIL", + CREATE_CONTACT_PLUS_NAME: "CREATE_CONTACT_PLUS_NAME", + SMPP: "SMPP", + NONE: "NONE", +}; + +const WEBHOOK_TYPE = { + POST: "post", + GET: "get", + JSON: "json", +}; + +const MATCH_TYPE = { + ALL_REPORTS: 0, +}; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_LIMIT, + WEBHOOK_ID, + DISABLED, + ENABLED, + MSG_SEARCH_TYPE, + ACTION, + WEBHOOK_TYPE, + MATCH_TYPE, +}; diff --git a/components/clicksend/package.json b/components/clicksend/package.json new file mode 100644 index 0000000000000..3114554526919 --- /dev/null +++ b/components/clicksend/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/clicksend", + "version": "0.0.1", + "description": "Pipedream ClickSend Components", + "main": "clicksend.app.mjs", + "keywords": [ + "pipedream", + "clicksend" + ], + "homepage": "https://pipedream.com/apps/clicksend", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/clicksend/sources/common/webhook.mjs b/components/clicksend/sources/common/webhook.mjs new file mode 100644 index 0000000000000..180237a9dbd11 --- /dev/null +++ b/components/clicksend/sources/common/webhook.mjs @@ -0,0 +1,34 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../clicksend.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + setWebhookId(value) { + this.db.set(constants.WEBHOOK_ID, value); + }, + getWebhookId() { + return this.db.get(constants.WEBHOOK_ID); + }, + processResource(resource) { + this.$emit(resource, this.generateMeta(resource)); + }, + }, + async run({ body }) { + this.http.respond({ + status: 200, + }); + this.processResource(body); + }, +}; diff --git a/components/clicksend/sources/new-incoming-sms-instant/new-incoming-sms-instant.mjs b/components/clicksend/sources/new-incoming-sms-instant/new-incoming-sms-instant.mjs new file mode 100644 index 0000000000000..d0310732e382d --- /dev/null +++ b/components/clicksend/sources/new-incoming-sms-instant/new-incoming-sms-instant.mjs @@ -0,0 +1,94 @@ +import constants from "../../common/constants.mjs"; +import common from "../common/webhook.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "clicksend-new-incoming-sms-instant", + name: "New Incoming SMS (Instant)", + description: "Emit new event for each new incoming SMS message received. [See the documentation](https://developers.clicksend.com/docs/rest/v3/#view-inbound-sms)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + dedicatedNumber: { + propDefinition: [ + common.props.app, + "dedicatedNumber", + ], + }, + messageSearchTerm: { + type: "string", + label: "Message Search Term", + description: "Search for a specific message. Eg `hello world`", + }, + }, + hooks: { + async activate() { + const { + http: { endpoint: actionAddress }, + dedicatedNumber, + messageSearchTerm, + createSMSInboundAutomation, + setWebhookId, + } = this; + + const response = + await createSMSInboundAutomation({ + data: { + dedicated_number: dedicatedNumber, + rule_name: "PD Inbound SMS", + message_search_type: constants.MSG_SEARCH_TYPE.ANY_MSG, + message_search_term: messageSearchTerm, + action: constants.ACTION.URL, + action_address: actionAddress, + enabled: constants.ENABLED, + webhook_type: constants.WEBHOOK_TYPE.JSON, + }, + }); + + setWebhookId(response.data.inbound_rule_id); + }, + async deactivate() { + const { + getWebhookId, + deleteSMSInboundAutomation, + } = this; + + const inboundRuleId = getWebhookId(); + if (inboundRuleId) { + await deleteSMSInboundAutomation({ + inboundRuleId, + }); + } + }, + }, + methods: { + ...common.methods, + createSMSInboundAutomation(args = {}) { + return this.app.post({ + debug: true, + path: "/automations/sms/inbound", + ...args, + }); + }, + deleteSMSInboundAutomation({ + inboundRuleId, ...args + } = {}) { + return this.app.delete({ + debug: true, + path: `/automations/sms/inbound/${inboundRuleId}`, + ...args, + }); + }, + generateMeta(resource) { + return { + id: resource.message_id, + summary: `New SMS: ${resource.message_id}`, + ts: Date.parse(resource.timestamp), + }; + }, + }, + sampleEmit, +}; diff --git a/components/clicksend/sources/new-incoming-sms-instant/test-event.mjs b/components/clicksend/sources/new-incoming-sms-instant/test-event.mjs new file mode 100644 index 0000000000000..77cda2c6a5c28 --- /dev/null +++ b/components/clicksend/sources/new-incoming-sms-instant/test-event.mjs @@ -0,0 +1,18 @@ +export default { + "originalsenderid": "+61411111111", + "body": "This is a test incoming SMS. LFLLSIH5RS.", + "message": "This is a test incoming SMS. LFLLSIH5RS.", + "sms": "+61477127762", + "custom_string": "Test custom string ", + "to": "+61411111111", + "original_message_id": "1430DA56-200B-452E-978C-4F1830D6A68F", + "originalmessageid": "1430DA56-200B-452E-978C-4F1830D6A68F", + "customstring": "Test custom string ", + "from": "+61477127762", + "originalmessage": "This is the original message. G555CRYS0H.", + "user_id": 497931, + "subaccount_id": 565379, + "original_body": "This is the original message. G555CRYS0H.", + "timestamp": "1710886316", + "message_id": "2CC1F1D8-0B50-418A-A66E-3980CF02DEE9" +}; diff --git a/components/clicksend/sources/watch-voice-messages-instant/test-event.mjs b/components/clicksend/sources/watch-voice-messages-instant/test-event.mjs new file mode 100644 index 0000000000000..51796afe8ec42 --- /dev/null +++ b/components/clicksend/sources/watch-voice-messages-instant/test-event.mjs @@ -0,0 +1,13 @@ +export default { + "timestamp_send": "1710887250", + "message_id": "89BC0510-166E-462A-B4A3-27D5157DBEB3", + "user_id": "497931", + "timestamp": "1710887250", + "messageid": "89BC0510-166E-462A-B4A3-27D5157DBEB3", + "status_text": "Success: Message received on handset.", + "subaccount_id": "565379", + "message_type": "voice", + "status": "Delivered", + "status_code": "201", + "digits": "8" +}; diff --git a/components/clicksend/sources/watch-voice-messages-instant/watch-voice-messages-instant.mjs b/components/clicksend/sources/watch-voice-messages-instant/watch-voice-messages-instant.mjs new file mode 100644 index 0000000000000..6a46c9482b1e6 --- /dev/null +++ b/components/clicksend/sources/watch-voice-messages-instant/watch-voice-messages-instant.mjs @@ -0,0 +1,75 @@ +import constants from "../../common/constants.mjs"; +import common from "../common/webhook.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "clicksend-watch-voice-messages-instant", + name: "Watch Voice Messages (Instant)", + description: "Emit new event when a new voice message is received or sent. [See the documentation](https://developers.clicksend.com/docs/rest/v3/#view-voice-receipts)", + version: "0.0.1", + type: "source", + dedupe: "unique", + hooks: { + async activate() { + const { + http: { endpoint: actionAddress }, + createVoiceDeliveryReceiptRule, + setWebhookId, + } = this; + + const response = + await createVoiceDeliveryReceiptRule({ + data: { + rule_name: "PD Voice Receipts", + match_type: constants.MATCH_TYPE.ALL_REPORTS, + action: constants.ACTION.URL, + action_address: actionAddress, + enabled: constants.ENABLED, + }, + }); + + setWebhookId(response.data.receipt_rule_id); + }, + async deactivate() { + const { + getWebhookId, + deleteVoiceDeliveryReceiptRule, + } = this; + + const receiptRuleId = getWebhookId(); + if (receiptRuleId) { + await deleteVoiceDeliveryReceiptRule({ + receiptRuleId, + }); + } + }, + }, + methods: { + ...common.methods, + createVoiceDeliveryReceiptRule(args = {}) { + return this.app.post({ + debug: true, + path: "/automations/voice/receipts", + ...args, + }); + }, + deleteVoiceDeliveryReceiptRule({ + receiptRuleId, ...args + } = {}) { + return this.app.delete({ + debug: true, + path: `/automations/voice/receipts/${receiptRuleId}`, + ...args, + }); + }, + generateMeta(resource) { + return { + id: resource.message_id, + summary: `New Voice MSG ${resource.message_id}`, + ts: Date.parse(resource.timestamp), + }; + }, + }, + sampleEmit, +}; diff --git a/components/clicktime/README.md b/components/clicktime/README.md new file mode 100644 index 0000000000000..66fa982a5a008 --- /dev/null +++ b/components/clicktime/README.md @@ -0,0 +1,11 @@ +# Overview + +The ClickTime API provides programmatic access to the ClickTime platform, which specializes in time tracking and project management. Leveraging this API within Pipedream allows you to automate various aspects of time tracking, employee management, and reporting. For example, you can create workflows that sync time entry data with other systems, automate notifications based on time tracking metrics, or even manage projects and tasks dynamically. Pipedream's serverless platform facilitates these automations by triggering workflows with HTTP requests, schedules, or app-based events, and offers the capability to connect to numerous other services and APIs to extend functionality further. + +# Example Use Cases + +- **Time Entry Syncing Workflow**: Create a workflow that listens for new time entries in ClickTime and automatically syncs them to a Google Sheet for reporting and archival purposes. This can help with consolidating time tracking data for analysis or sharing with team members who prefer working with spreadsheets. + +- **Slack Notification for Approaching Project Limits**: Set up a workflow that checks ClickTime project hours daily and sends a Slack message to the project manager if the total hours are close to the predefined project limit. This real-time alert can assist in managing project budgets and timelines effectively. + +- **Automated Invoicing Integration**: Build a workflow that generates invoices in an accounting platform like QuickBooks whenever a project phase or task reaches completion, based on the time tracked in ClickTime. This automation streamlines the billing process, reducing the manual effort to transfer time tracking data into invoices. diff --git a/components/clickup/README.md b/components/clickup/README.md index cc187fc0c5337..d4def2ad80d65 100644 --- a/components/clickup/README.md +++ b/components/clickup/README.md @@ -1,12 +1,11 @@ # Overview -With the ClickUp API, you can build a variety of applications and integrations -to customize your ClickUp experience. Here are a few examples of what you can -build: - -- A custom app to display your ClickUp tasks in a kanban board view -- An integration with your favorite task management app to allow you to send - tasks to ClickUp -- A tool to help you track your productivity and goal progress over time -- A bot that can answer questions about your ClickUp tasks and workflow -- A script to automatically create tasks based on certain events or triggers +The ClickUp API on Pipedream allows you to automate tasks, sync data across various platforms, and construct custom workflows to streamline project management. By leveraging the API, you can create tasks, update statuses, and manipulate lists or spaces programmatically. Whether you're looking to integrate ClickUp with your CRM, coordinate cross-functional teams, or just manage notifications, Pipedream's serverless platform lets you build scalable, event-driven processes with minimal fuss. + +# Example Use Cases + +- **Task Synchronization Across Platforms**: Sync tasks between ClickUp and other project management tools, such as Trello or Asana. When a task is updated in ClickUp, the corresponding card on Trello or project in Asana can be updated automatically, keeping all project views consistent. + +- **CRM Integration for Sales Teams**: Link ClickUp with a CRM like Salesforce. When a new lead is added to Salesforce, a task is automatically created in ClickUp for the sales team to follow up. This ensures no lead gets missed and the sales process is tightly managed. + +- **Support Ticket Triaging**: Connect ClickUp to a support platform like Zendesk. When a new support ticket is filed, a ClickUp task is generated and assigned to the correct team. Use status, priority, and tags to sort and triage support issues efficiently. diff --git a/components/clickup/actions/common/checklist-item-props.mjs b/components/clickup/actions/common/checklist-item-props.mjs deleted file mode 100644 index f4d3f8f8df367..0000000000000 --- a/components/clickup/actions/common/checklist-item-props.mjs +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable pipedream/props-description */ -/* eslint-disable pipedream/props-label */ -import common from "./checklist-props.mjs"; - -export default { - props: { - ...common.props, - checklistId: { - ...common.props.checklistId, - optional: true, - }, - checklistItemId: { - propDefinition: [ - common.props.clickup, - "checklistItems", - (c) => ({ - taskId: c.taskId, - checklistId: c.checklistId, - useCustomTaskIds: c.useCustomTaskIds, - authorizedTeamId: c.authorizedTeamId, - }), - ], - description: "To show options please select a **Task and Checklist** first", - }, - }, -}; diff --git a/components/clickup/actions/common/checklist-props.mjs b/components/clickup/actions/common/checklist-props.mjs deleted file mode 100644 index c33022b4d87e3..0000000000000 --- a/components/clickup/actions/common/checklist-props.mjs +++ /dev/null @@ -1,25 +0,0 @@ -/* eslint-disable pipedream/props-description */ -/* eslint-disable pipedream/props-label */ -import common from "./task-props.mjs"; - -export default { - props: { - ...common.props, - taskId: { - ...common.props.taskId, - optional: true, - }, - checklistId: { - propDefinition: [ - common.props.clickup, - "checklists", - (c) => ({ - taskId: c.taskId, - useCustomTaskIds: c.useCustomTaskIds, - authorizedTeamId: c.authorizedTeamId, - }), - ], - description: "To show options please select a **Task** first", - }, - }, -}; diff --git a/components/clickup/actions/common/comment-props.mjs b/components/clickup/actions/common/comment-props.mjs index 519be622847e7..d663d86659bed 100644 --- a/components/clickup/actions/common/comment-props.mjs +++ b/components/clickup/actions/common/comment-props.mjs @@ -1,4 +1,6 @@ import common from "./workspace-prop.mjs"; +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; export default { props: { @@ -11,65 +13,13 @@ export default { workspaceId: c.workspaceId, }), ], - optional: true, - }, - folderId: { - propDefinition: [ - common.props.clickup, - "folders", - (c) => ({ - spaceId: c.spaceId, - }), - ], - optional: true, - }, - listId: { - propDefinition: [ - common.props.clickup, - "lists", - (c) => ({ - folderId: c.folderId, - spaceId: c.spaceId, - }), - ], - optional: true, - }, - taskId: { - propDefinition: [ - common.props.clickup, - "tasks", - (c) => ({ - listId: c.listId, - useCustomTaskIds: c.useCustomTaskIds, - }), - ], - optional: true, - }, - viewId: { - propDefinition: [ - common.props.clickup, - "views", - (c) => ({ - workspaceId: c.workspaceId, - spaceId: c.spaceId, - listId: c.listId, - folderId: c.folderId, - }), - ], - optional: true, - }, - commentId: { - propDefinition: [ - common.props.clickup, - "comments", - (c) => ({ - listId: c.listId, - taskId: c.taskId, - viewId: c.viewId, - useCustomTaskIds: c.useCustomTaskIds, - authorizedTeamId: c.authorizedTeamId, - }), - ], }, }, + additionalProps: builder.buildListProps({ + tailProps: { + taskId: propsFragments.taskId, + viewId: propsFragments.viewId, + commentId: propsFragments.commentId, + }, + }), }; diff --git a/components/clickup/actions/common/list-props.mjs b/components/clickup/actions/common/list-props.mjs index c0e582169958a..48127111d5980 100644 --- a/components/clickup/actions/common/list-props.mjs +++ b/components/clickup/actions/common/list-props.mjs @@ -12,29 +12,6 @@ export default { }), ], description: "If selected, the **Lists** will be filtered by this Space ID", - optional: true, - }, - folderId: { - propDefinition: [ - common.props.clickup, - "folders", - (c) => ({ - spaceId: c.spaceId, - }), - ], - description: "If selected, the **Lists** will be filtered by this Folder ID", - optional: true, - }, - listId: { - propDefinition: [ - common.props.clickup, - "lists", - (c) => ({ - workspaceId: c.workspaceId, - spaceId: c.spaceId, - folderId: c.folderId, - }), - ], }, }, }; diff --git a/components/clickup/actions/common/task-props.mjs b/components/clickup/actions/common/task-props.mjs index 64bce54116be6..1b3aeb678425e 100644 --- a/components/clickup/actions/common/task-props.mjs +++ b/components/clickup/actions/common/task-props.mjs @@ -1,14 +1,8 @@ -/* eslint-disable pipedream/props-description */ -/* eslint-disable pipedream/props-label */ import common from "./list-props.mjs"; export default { props: { ...common.props, - listId: { - ...common.props.listId, - optional: true, - }, useCustomTaskIds: { propDefinition: [ common.props.clickup, @@ -21,16 +15,5 @@ export default { "authorizedTeamId", ], }, - taskId: { - propDefinition: [ - common.props.clickup, - "tasks", - (c) => ({ - listId: c.listId, - useCustomTaskIds: c.useCustomTaskIds, - }), - ], - description: "To show options please select a **List** first", - }, }, }; diff --git a/components/clickup/actions/common/thread-comment-props.mjs b/components/clickup/actions/common/thread-comment-props.mjs new file mode 100644 index 0000000000000..9b21bc41f39eb --- /dev/null +++ b/components/clickup/actions/common/thread-comment-props.mjs @@ -0,0 +1,24 @@ +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; +import common from "./workspace-prop.mjs"; + +export default { + props: { + ...common.props, + spaceId: { + propDefinition: [ + common.props.clickup, + "spaces", + (c) => ({ + workspaceId: c.workspaceId, + }), + ], + }, + }, + additionalProps: builder.buildListProps({ + tailProps: { + taskId: propsFragments.taskId, + commentId: propsFragments.commentId, + }, + }), +}; diff --git a/components/clickup/actions/common/view-props.mjs b/components/clickup/actions/common/view-props.mjs deleted file mode 100644 index 4fa3bf11dea07..0000000000000 --- a/components/clickup/actions/common/view-props.mjs +++ /dev/null @@ -1,51 +0,0 @@ -import common from "./workspace-prop.mjs"; - -export default { - props: { - ...common.props, - spaceId: { - propDefinition: [ - common.props.clickup, - "spaces", - (c) => ({ - workspaceId: c.workspaceId, - }), - ], - optional: true, - }, - folderId: { - propDefinition: [ - common.props.clickup, - "folders", - (c) => ({ - spaceId: c.spaceId, - }), - ], - optional: true, - }, - listId: { - propDefinition: [ - common.props.clickup, - "lists", - (c) => ({ - workspaceId: c.workspaceId, - spaceId: c.spaceId, - folderId: c.folderId, - }), - ], - optional: true, - }, - viewId: { - propDefinition: [ - common.props.clickup, - "views", - (c) => ({ - workspaceId: c.workspaceId, - spaceId: c.spaceId, - listId: c.listId, - folderId: c.folderId, - }), - ], - }, - }, -}; diff --git a/components/clickup/actions/create-chat-view-comment/create-chat-view-comment.mjs b/components/clickup/actions/create-chat-view-comment/create-chat-view-comment.mjs index 66183fed9ff68..2eb5598e2cc7f 100644 --- a/components/clickup/actions/create-chat-view-comment/create-chat-view-comment.mjs +++ b/components/clickup/actions/create-chat-view-comment/create-chat-view-comment.mjs @@ -1,62 +1,16 @@ -import clickup from "../../clickup.app.mjs"; +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; +import common from "../common/list-props.mjs"; export default { + ...common, key: "clickup-create-chat-view-comment", name: "Create Chat View Comment", description: "Creates a chat view comment. See the docs [here](https://clickup.com/api) in **Comments / Create Chat View Comment** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { - clickup, - workspaceId: { - propDefinition: [ - clickup, - "workspaces", - ], - }, - spaceId: { - propDefinition: [ - clickup, - "spaces", - (c) => ({ - workspaceId: c.workspaceId, - }), - ], - optional: true, - }, - folderId: { - propDefinition: [ - clickup, - "folders", - (c) => ({ - spaceId: c.spaceId, - }), - ], - optional: true, - }, - listId: { - propDefinition: [ - clickup, - "lists", - (c) => ({ - spaceId: c.spaceId, - folderId: c.folderId, - }), - ], - optional: true, - }, - viewId: { - propDefinition: [ - clickup, - "views", - (c) => ({ - workspaceId: c.workspaceId, - spaceId: c.spaceId, - listId: c.listId, - folderId: c.folderId, - }), - ], - }, + ...common.props, commentText: { label: "Comment Text", description: "The text of the comment", @@ -71,7 +25,7 @@ export default { }, assignees: { propDefinition: [ - clickup, + common.props.clickup, "assignees", (c) => ({ workspaceId: c.workspaceId, @@ -79,7 +33,20 @@ export default { ], optional: true, }, + listWithFolder: { + optional: true, + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, }, + additionalProps: builder.buildListProps({ + listPropsOptional: true, + tailProps: { + viewId: propsFragments.viewId, + }, + }), async run({ $ }) { const { viewId, diff --git a/components/clickup/actions/create-checklist-item/create-checklist-item.mjs b/components/clickup/actions/create-checklist-item/create-checklist-item.mjs index fd617160d23d0..e774804b22de7 100644 --- a/components/clickup/actions/create-checklist-item/create-checklist-item.mjs +++ b/components/clickup/actions/create-checklist-item/create-checklist-item.mjs @@ -1,10 +1,13 @@ -import common from "../common/checklist-props.mjs"; +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; +import common from "../common/task-props.mjs"; export default { + ...common, key: "clickup-create-checklist-item", name: "Create Checklist Item", description: "Creates a new item in a checklist. See the docs [here](https://clickup.com/api) in **Checklists / Create Checklist Item** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, @@ -25,7 +28,19 @@ export default { ], optional: true, }, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, }, + additionalProps: builder.buildListProps({ + tailProps: { + taskId: propsFragments.taskId, + checklistId: propsFragments.checklistId, + }, + }), async run({ $ }) { const { taskId, diff --git a/components/clickup/actions/create-checklist/create-checklist.mjs b/components/clickup/actions/create-checklist/create-checklist.mjs index e670cc8053265..84063ce76edc4 100644 --- a/components/clickup/actions/create-checklist/create-checklist.mjs +++ b/components/clickup/actions/create-checklist/create-checklist.mjs @@ -1,10 +1,13 @@ +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; import common from "../common/task-props.mjs"; export default { + ...common, key: "clickup-create-checklist", name: "Create Checklist", description: "Creates a new checklist in a task. See the docs [here](https://clickup.com/api) in **Checklists / Create Checklist** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, @@ -13,7 +16,18 @@ export default { type: "string", description: "The name of checklist", }, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, }, + additionalProps: builder.buildListProps({ + tailProps: { + taskId: propsFragments.taskId, + }, + }), async run({ $ }) { const { taskId, diff --git a/components/clickup/actions/create-folder/create-folder.mjs b/components/clickup/actions/create-folder/create-folder.mjs index 07991eda9a402..1f43c2a08f0ca 100644 --- a/components/clickup/actions/create-folder/create-folder.mjs +++ b/components/clickup/actions/create-folder/create-folder.mjs @@ -1,10 +1,11 @@ import common from "../common/space-props.mjs"; export default { + ...common, key: "clickup-create-folder", name: "Create Folder", description: "Creates a new folder. See the docs [here](https://clickup.com/api) in **Folders / Create Folder** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, diff --git a/components/clickup/actions/create-list-comment/create-list-comment.mjs b/components/clickup/actions/create-list-comment/create-list-comment.mjs index 167e72964b67b..67d24a89acd9e 100644 --- a/components/clickup/actions/create-list-comment/create-list-comment.mjs +++ b/components/clickup/actions/create-list-comment/create-list-comment.mjs @@ -1,11 +1,13 @@ import clickup from "../../clickup.app.mjs"; +import builder from "../../common/builder.mjs"; import common from "../common/list-props.mjs"; export default { + ...common, key: "clickup-create-list-comment", name: "Create List Comment", description: "Creates a list comment. See the docs [here](https://clickup.com/api) in **Comments / Create List Comment** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, @@ -31,8 +33,14 @@ export default { ], optional: true, }, - + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, }, + additionalProps: builder.buildListProps(), async run({ $ }) { const { listId, diff --git a/components/clickup/actions/create-list/create-list.mjs b/components/clickup/actions/create-list/create-list.mjs index 1963d2c36f6fc..fd17698fa1c09 100644 --- a/components/clickup/actions/create-list/create-list.mjs +++ b/components/clickup/actions/create-list/create-list.mjs @@ -5,7 +5,7 @@ export default { key: "clickup-create-list", name: "Create List", description: "Creates a new list. See the docs [here](https://clickup.com/api) in **Lists / Create List** section.", - version: "0.0.12", + version: "0.0.14", type: "action", props: { clickup, diff --git a/components/clickup/actions/create-space/create-space.mjs b/components/clickup/actions/create-space/create-space.mjs index c4f84cca222b2..d26eed5f2445c 100644 --- a/components/clickup/actions/create-space/create-space.mjs +++ b/components/clickup/actions/create-space/create-space.mjs @@ -4,7 +4,7 @@ export default { key: "clickup-create-space", name: "Create Space", description: "Creates a new space. See the docs [here](https://clickup.com/api) in **Spaces / Create Space** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, diff --git a/components/clickup/actions/create-task-comment/create-task-comment.mjs b/components/clickup/actions/create-task-comment/create-task-comment.mjs index 6faf56e7a918c..dd01f6bcd1be3 100644 --- a/components/clickup/actions/create-task-comment/create-task-comment.mjs +++ b/components/clickup/actions/create-task-comment/create-task-comment.mjs @@ -1,10 +1,13 @@ +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; import common from "../common/task-props.mjs"; export default { + ...common, key: "clickup-create-task-comment", name: "Create Task Comment", description: "Creates a task comment. See the docs [here](https://clickup.com/api) in **Comments / Create Task Comment** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, @@ -30,8 +33,18 @@ export default { ], optional: true, }, - + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, }, + additionalProps: builder.buildListProps({ + tailProps: { + taskId: propsFragments.taskId, + }, + }), async run({ $ }) { const { taskId, diff --git a/components/clickup/actions/create-task-from-template/create-task-from-template.mjs b/components/clickup/actions/create-task-from-template/create-task-from-template.mjs index a2bee79b6f288..7a3607c0923d9 100644 --- a/components/clickup/actions/create-task-from-template/create-task-from-template.mjs +++ b/components/clickup/actions/create-task-from-template/create-task-from-template.mjs @@ -1,11 +1,13 @@ import clickup from "../../clickup.app.mjs"; +import builder from "../../common/builder.mjs"; import common from "../common/list-props.mjs"; export default { + ...common, key: "clickup-create-task-from-template", name: "Create Task From Template", description: "Creates a new task from a template. See the docs [here](https://clickup.com/api) in **Task Templates / Create Task From Template** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, @@ -23,7 +25,14 @@ export default { type: "string", description: "The name of task", }, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, }, + additionalProps: builder.buildListProps(), async run({ $ }) { const { listId, diff --git a/components/clickup/actions/create-task/create-task.mjs b/components/clickup/actions/create-task/create-task.mjs index cbda718d80aaf..f313682aa392d 100644 --- a/components/clickup/actions/create-task/create-task.mjs +++ b/components/clickup/actions/create-task/create-task.mjs @@ -1,12 +1,15 @@ import clickup from "../../clickup.app.mjs"; -import common from "../common/list-props.mjs"; +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; import constants from "../common/constants.mjs"; +import common from "../common/list-props.mjs"; export default { + ...common, key: "clickup-create-task", name: "Create Task", description: "Creates a new task. See the docs [here](https://clickup.com/api) in **Tasks / Create Task** section.", - version: "0.0.12", + version: "0.0.14", type: "action", props: { ...common.props, @@ -54,27 +57,6 @@ export default { ], optional: true, }, - status: { - propDefinition: [ - clickup, - "statuses", - (c) => ({ - listId: c.listId, - }), - ], - optional: true, - }, - parent: { - label: "Parent Task", - propDefinition: [ - clickup, - "tasks", - (c) => ({ - listId: c.listId, - }), - ], - optional: true, - }, dueDate: { type: "string", label: "Due Date", @@ -87,7 +69,23 @@ export default { description: "If set `true`, due date will be given with time. If not it will only be the closest date", optional: true, }, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, }, + additionalProps: builder.buildListProps({ + tailProps: { + status: propsFragments.status, + parent: { + ...propsFragments.taskId, + label: "Parent Task", + optional: true, + }, + }, + }), async run({ $ }) { const { listId, diff --git a/components/clickup/actions/create-threaded-comment/create-threaded-comment.mjs b/components/clickup/actions/create-threaded-comment/create-threaded-comment.mjs new file mode 100644 index 0000000000000..2915b5a4a3cb4 --- /dev/null +++ b/components/clickup/actions/create-threaded-comment/create-threaded-comment.mjs @@ -0,0 +1,65 @@ +import common from "../common/thread-comment-props.mjs"; + +export default { + ...common, + key: "clickup-create-threaded-comment", + name: "Create Threaded Comment", + description: "Creates a threaded comment. See the docs [here](https://clickup.com/api) in **Comments / Create Threaded Comment** section.", + version: "0.0.1", + type: "action", + props: { + ...common.props, + commentText: { + label: "Comment Text", + description: "The text of the comment", + type: "string", + }, + notifyAll: { + label: "Notify All", + description: "Will notify all", + type: "boolean", + default: false, + }, + assignee: { + propDefinition: [ + common.props.clickup, + "assignees", + (c) => ({ + workspaceId: c.workspaceId, + }), + ], + optional: true, + }, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, + }, + async run({ $ }) { + const { + commentId, + commentText, + notifyAll, + assignee, + } = this; + + const data = { + comment_text: commentText, + notify_all: notifyAll, + }; + + if (assignee) data.assignee = assignee; + + const response = await this.clickup.createThreadedComment({ + $, + commentId, + data, + }); + + $.export("$summary", "Successfully created threaded comment"); + + return response; + }, +}; diff --git a/components/clickup/actions/create-view-comment/create-view-comment.mjs b/components/clickup/actions/create-view-comment/create-view-comment.mjs index 9d48257e50d54..6a23f057ac6c9 100644 --- a/components/clickup/actions/create-view-comment/create-view-comment.mjs +++ b/components/clickup/actions/create-view-comment/create-view-comment.mjs @@ -1,62 +1,15 @@ -import clickup from "../../clickup.app.mjs"; +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; +import common from "../common/list-props.mjs"; export default { key: "clickup-create-view-comment", name: "Create View Comment", description: "Creates a view comment. See the docs [here](https://clickup.com/api) in **Comments / Create Chat View Comment** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { - clickup, - workspaceId: { - propDefinition: [ - clickup, - "workspaces", - ], - }, - spaceId: { - propDefinition: [ - clickup, - "spaces", - (c) => ({ - workspaceId: c.workspaceId, - }), - ], - optional: true, - }, - folderId: { - propDefinition: [ - clickup, - "folders", - (c) => ({ - spaceId: c.spaceId, - }), - ], - optional: true, - }, - listId: { - propDefinition: [ - clickup, - "lists", - (c) => ({ - spaceId: c.spaceId, - folderId: c.folderId, - }), - ], - optional: true, - }, - viewId: { - propDefinition: [ - clickup, - "views", - (c) => ({ - workspaceId: c.workspaceId, - spaceId: c.spaceId, - listId: c.listId, - folderId: c.folderId, - }), - ], - }, + ...common.props, commentText: { label: "Comment Text", description: "The text of the comment", @@ -71,7 +24,7 @@ export default { }, assignees: { propDefinition: [ - clickup, + common.props.clickup, "assignees", (c) => ({ workspaceId: c.workspaceId, @@ -79,8 +32,20 @@ export default { ], optional: true, }, - + listWithFolder: { + optional: true, + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, }, + additionalProps: builder.buildListProps({ + listPropsOptional: true, + tailProps: { + viewId: propsFragments.viewId, + }, + }), async run({ $ }) { const { viewId, diff --git a/components/clickup/actions/delete-checklist-item/delete-checklist-item.mjs b/components/clickup/actions/delete-checklist-item/delete-checklist-item.mjs index 96e3247d0fbe1..c5ba739388b39 100644 --- a/components/clickup/actions/delete-checklist-item/delete-checklist-item.mjs +++ b/components/clickup/actions/delete-checklist-item/delete-checklist-item.mjs @@ -1,12 +1,33 @@ -import common from "../common/checklist-item-props.mjs"; +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; +import common from "../common/task-props.mjs"; export default { + ...common, key: "clickup-delete-checklist-item", name: "Delete Checklist Item", description: "Deletes item in a checklist. See the docs [here](https://clickup.com/api) in **Checklists / Delete Checklist Item** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, + props: { + ...common.props, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, + }, + additionalProps: builder.buildListProps({ + tailProps: { + taskId: { + ...propsFragments.taskId, + description: "To show options please select a **List** first", + }, + checklistId: propsFragments.checklistId, + checklistItemId: propsFragments.checklistItemId, + }, + }), async run({ $ }) { const { checklistId, diff --git a/components/clickup/actions/delete-checklist/delete-checklist.mjs b/components/clickup/actions/delete-checklist/delete-checklist.mjs index 4471bc4e2b01e..d3db6c1568340 100644 --- a/components/clickup/actions/delete-checklist/delete-checklist.mjs +++ b/components/clickup/actions/delete-checklist/delete-checklist.mjs @@ -1,12 +1,29 @@ -import common from "../common/checklist-props.mjs"; +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; +import common from "../common/task-props.mjs"; export default { + ...common, key: "clickup-delete-checklist", name: "Delete Checklist", description: "Deletes a checklist in a task. See the docs [here](https://clickup.com/api) in **Checklists / Delete Checklist** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, + props: { + ...common.props, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, + }, + additionalProps: builder.buildListProps({ + tailProps: { + taskId: propsFragments.taskId, + checklistId: propsFragments.checklistId, + }, + }), async run({ $ }) { const { checklistId } = this; diff --git a/components/clickup/actions/delete-comment/delete-comment.mjs b/components/clickup/actions/delete-comment/delete-comment.mjs index 681a0f3dcb231..d44f0d1afd210 100644 --- a/components/clickup/actions/delete-comment/delete-comment.mjs +++ b/components/clickup/actions/delete-comment/delete-comment.mjs @@ -1,12 +1,21 @@ import common from "../common/comment-props.mjs"; export default { + ...common, key: "clickup-delete-comment", name: "Delete Comment", description: "Deletes a comment. See the docs [here](https://clickup.com/api) in **Comments / Deleet Comment** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, + props: { + ...common.props, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, + }, async run({ $ }) { const { commentId } = this; diff --git a/components/clickup/actions/delete-folder/delete-folder.mjs b/components/clickup/actions/delete-folder/delete-folder.mjs index 282048e1d2aae..7d1f2858f1ade 100644 --- a/components/clickup/actions/delete-folder/delete-folder.mjs +++ b/components/clickup/actions/delete-folder/delete-folder.mjs @@ -1,12 +1,12 @@ import common from "../common/folder-props.mjs"; export default { + ...common, key: "clickup-delete-folder", name: "Delete Folder", description: "Delete a folder. See the docs [here](https://clickup.com/api) in **Folders / Delete Folder** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, async run({ $ }) { const { folderId } = this; diff --git a/components/clickup/actions/delete-list/delete-list.mjs b/components/clickup/actions/delete-list/delete-list.mjs index 9416bee26b2b2..a22bc6b82a459 100644 --- a/components/clickup/actions/delete-list/delete-list.mjs +++ b/components/clickup/actions/delete-list/delete-list.mjs @@ -1,12 +1,23 @@ +import builder from "../../common/builder.mjs"; import common from "../common/list-props.mjs"; export default { + ...common, key: "clickup-delete-list", name: "Delete List", description: "Delete a list. See the docs [here](https://clickup.com/api) in **Lists / Delete List** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, + props: { + ...common.props, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, + }, + additionalProps: builder.buildListProps(), async run({ $ }) { const { listId } = this; diff --git a/components/clickup/actions/delete-space/delete-space.mjs b/components/clickup/actions/delete-space/delete-space.mjs index 3868faa1c6875..6d74890b9cfcf 100644 --- a/components/clickup/actions/delete-space/delete-space.mjs +++ b/components/clickup/actions/delete-space/delete-space.mjs @@ -1,12 +1,12 @@ import common from "../common/space-props.mjs"; export default { + ...common, key: "clickup-delete-space", name: "Delete Space", description: "Delete a space. See the docs [here](https://clickup.com/api) in **Spaces / Delete Space** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, async run({ $ }) { const { spaceId } = this; diff --git a/components/clickup/actions/delete-task/delete-task.mjs b/components/clickup/actions/delete-task/delete-task.mjs index 703c7cdc4086d..53cdf7872cc2d 100644 --- a/components/clickup/actions/delete-task/delete-task.mjs +++ b/components/clickup/actions/delete-task/delete-task.mjs @@ -1,12 +1,31 @@ +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; import common from "../common/task-props.mjs"; export default { + ...common, key: "clickup-delete-task", name: "Delete Task", description: "Delete a task. See the docs [here](https://clickup.com/api) in **Tasks / Delete Task** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, + props: { + ...common.props, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, + }, + additionalProps: builder.buildListProps({ + tailProps: { + taskId: { + ...propsFragments.taskId, + description: "To show options please select a **List** first", + }, + }, + }), async run({ $ }) { const { taskId } = this; diff --git a/components/clickup/actions/get-custom-fields/get-custom-fields.mjs b/components/clickup/actions/get-custom-fields/get-custom-fields.mjs index f50ffbeecbab4..963f1c764b65f 100644 --- a/components/clickup/actions/get-custom-fields/get-custom-fields.mjs +++ b/components/clickup/actions/get-custom-fields/get-custom-fields.mjs @@ -1,12 +1,23 @@ +import builder from "../../common/builder.mjs"; import common from "../common/list-props.mjs"; export default { + ...common, key: "clickup-get-custom-fields", name: "Get Custom Fields", description: "Get a list of custom fields. See the docs [here](https://clickup.com/api) in **Custom Fields / Get Accessible Custom Fields** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, + props: { + ...common.props, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, + }, + additionalProps: builder.buildListProps(), async run({ $ }) { const { listId } = this; diff --git a/components/clickup/actions/get-folder-views/get-folder-views.mjs b/components/clickup/actions/get-folder-views/get-folder-views.mjs index 06e485be6e0f3..e6d0ffcc14cd3 100644 --- a/components/clickup/actions/get-folder-views/get-folder-views.mjs +++ b/components/clickup/actions/get-folder-views/get-folder-views.mjs @@ -1,12 +1,12 @@ import common from "../common/folder-props.mjs"; export default { + ...common, key: "clickup-get-folder-views", name: "Get Folder Views", description: "Get all views of a folder. See the docs [here](https://clickup.com/api) in **Views / Get Folder Views** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, async run({ $ }) { const { folderId } = this; diff --git a/components/clickup/actions/get-folder/get-folder.mjs b/components/clickup/actions/get-folder/get-folder.mjs index 9e9c05224765f..11d34ed766681 100644 --- a/components/clickup/actions/get-folder/get-folder.mjs +++ b/components/clickup/actions/get-folder/get-folder.mjs @@ -1,12 +1,12 @@ import common from "../common/folder-props.mjs"; export default { + ...common, key: "clickup-get-folder", name: "Get Folder", description: "Get a folder in a workplace. See the docs [here](https://clickup.com/api) in **Folders / Get Folder** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, async run({ $ }) { const { folderId } = this; diff --git a/components/clickup/actions/get-folders/get-folders.mjs b/components/clickup/actions/get-folders/get-folders.mjs index bf0bc4e22be7d..f131ecb39374a 100644 --- a/components/clickup/actions/get-folders/get-folders.mjs +++ b/components/clickup/actions/get-folders/get-folders.mjs @@ -1,10 +1,11 @@ import common from "../common/space-props.mjs"; export default { + ...common, key: "clickup-get-folders", name: "Get Folders", description: "Get a list of folders in a workplace. See the docs [here](https://clickup.com/api) in **Folders / Get Folders** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, diff --git a/components/clickup/actions/get-list-comments/get-list-comments.mjs b/components/clickup/actions/get-list-comments/get-list-comments.mjs index cba6c65c23aee..b43efa0a27268 100644 --- a/components/clickup/actions/get-list-comments/get-list-comments.mjs +++ b/components/clickup/actions/get-list-comments/get-list-comments.mjs @@ -1,12 +1,23 @@ +import builder from "../../common/builder.mjs"; import common from "../common/list-props.mjs"; export default { + ...common, key: "clickup-get-list-comments", name: "Get List Comments", description: "Get a list comments. See the docs [here](https://clickup.com/api) in **Comments / Get List Comments** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, + props: { + ...common.props, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, + }, + additionalProps: builder.buildListProps(), async run({ $ }) { const { listId } = this; diff --git a/components/clickup/actions/get-list-views/get-list-views.mjs b/components/clickup/actions/get-list-views/get-list-views.mjs index f1a76f293a298..dde6c264ff90e 100644 --- a/components/clickup/actions/get-list-views/get-list-views.mjs +++ b/components/clickup/actions/get-list-views/get-list-views.mjs @@ -1,12 +1,23 @@ +import builder from "../../common/builder.mjs"; import common from "../common/list-props.mjs"; export default { + ...common, key: "clickup-get-list-views", name: "Get List Views", description: "Get all views of a list. See the docs [here](https://clickup.com/api) in **Views / Get List Views** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, + props: { + ...common.props, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, + }, + additionalProps: builder.buildListProps(), async run({ $ }) { const { listId } = this; diff --git a/components/clickup/actions/get-list/get-list.mjs b/components/clickup/actions/get-list/get-list.mjs index e836b4178d49b..792f99eacda2d 100644 --- a/components/clickup/actions/get-list/get-list.mjs +++ b/components/clickup/actions/get-list/get-list.mjs @@ -1,12 +1,23 @@ +import builder from "../../common/builder.mjs"; import common from "../common/list-props.mjs"; export default { + ...common, key: "clickup-get-list", name: "Get List", description: "Get a list. See the docs [here](https://clickup.com/api) in **Lists / Get List** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, + props: { + ...common.props, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, + }, + additionalProps: builder.buildListProps(), async run({ $ }) { const { listId } = this; diff --git a/components/clickup/actions/get-lists/get-lists.mjs b/components/clickup/actions/get-lists/get-lists.mjs index e720cc6fed2b4..664b3d3476a08 100644 --- a/components/clickup/actions/get-lists/get-lists.mjs +++ b/components/clickup/actions/get-lists/get-lists.mjs @@ -1,10 +1,11 @@ import common from "../common/folder-props.mjs"; export default { + ...common, key: "clickup-get-lists", name: "Get Lists", description: "Get a list of lists. See the docs [here](https://clickup.com/api) in **Lists / Get Lists** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, diff --git a/components/clickup/actions/get-space-views/get-space-views.mjs b/components/clickup/actions/get-space-views/get-space-views.mjs index 26f46d024e0f3..7149f43368ae0 100644 --- a/components/clickup/actions/get-space-views/get-space-views.mjs +++ b/components/clickup/actions/get-space-views/get-space-views.mjs @@ -1,12 +1,12 @@ import common from "../common/space-props.mjs"; export default { + ...common, key: "clickup-get-space-views", name: "Get Space Views", description: "Get all views of a space. See the docs [here](https://clickup.com/api) in **Views / Get Space Views** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, async run({ $ }) { const { spaceId } = this; diff --git a/components/clickup/actions/get-space/get-space.mjs b/components/clickup/actions/get-space/get-space.mjs index 99d647dad7385..42cbc4bf7bc52 100644 --- a/components/clickup/actions/get-space/get-space.mjs +++ b/components/clickup/actions/get-space/get-space.mjs @@ -1,12 +1,12 @@ import common from "../common/space-props.mjs"; export default { + ...common, key: "clickup-get-space", name: "Get Space", description: "Get a space in a workplace. See the docs [here](https://clickup.com/api) in **Spaces / Get Space** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, async run({ $ }) { const { spaceId } = this; diff --git a/components/clickup/actions/get-spaces/get-spaces.mjs b/components/clickup/actions/get-spaces/get-spaces.mjs index fed4e41adc2f1..e194934fb2715 100644 --- a/components/clickup/actions/get-spaces/get-spaces.mjs +++ b/components/clickup/actions/get-spaces/get-spaces.mjs @@ -4,7 +4,7 @@ export default { key: "clickup-get-spaces", name: "Get Spaces", description: "Get a list of spaces in a workplace. See the docs [here](https://clickup.com/api) in **Spaces / Get Spaces** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, diff --git a/components/clickup/actions/get-task-comments/get-task-comments.mjs b/components/clickup/actions/get-task-comments/get-task-comments.mjs index 5c74ae533413f..2b3832bc7cbaa 100644 --- a/components/clickup/actions/get-task-comments/get-task-comments.mjs +++ b/components/clickup/actions/get-task-comments/get-task-comments.mjs @@ -1,12 +1,33 @@ +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; import common from "../common/task-props.mjs"; export default { + ...common, key: "clickup-get-task-comments", name: "Get Task Comments", description: "Get a task comments. See the docs [here](https://clickup.com/api) in **Comments / Get Task Comments** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, + props: { + ...common.props, + listWithFolder: { + optional: true, + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, + }, + additionalProps: builder.buildListProps({ + listPropsOptional: true, + tailProps: { + taskId: { + ...propsFragments.taskId, + description: "To show options please select a **List** first", + }, + }, + }), async run({ $ }) { const { taskId } = this; diff --git a/components/clickup/actions/get-task-templates/get-task-templates.mjs b/components/clickup/actions/get-task-templates/get-task-templates.mjs index a6b052ef649e5..5e1972a96a635 100644 --- a/components/clickup/actions/get-task-templates/get-task-templates.mjs +++ b/components/clickup/actions/get-task-templates/get-task-templates.mjs @@ -4,7 +4,7 @@ export default { key: "clickup-get-task-templates", name: "Get Task Templates", description: "Get a list of templates. See the docs [here](https://clickup.com/api) in **Task Templates / Get Task Templates** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, diff --git a/components/clickup/actions/get-task/get-task.mjs b/components/clickup/actions/get-task/get-task.mjs index a1b956d4423e3..b1c7d45658abd 100644 --- a/components/clickup/actions/get-task/get-task.mjs +++ b/components/clickup/actions/get-task/get-task.mjs @@ -1,12 +1,31 @@ +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; import common from "../common/task-props.mjs"; export default { + ...common, key: "clickup-get-task", name: "Get Task", description: "Get a task. See the docs [here](https://clickup.com/api) in **Tasks / Get Task** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, + props: { + ...common.props, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, + }, + additionalProps: builder.buildListProps({ + tailProps: { + taskId: { + ...propsFragments.taskId, + description: "To show options please select a **List** first", + }, + }, + }), async run({ $ }) { const { taskId } = this; diff --git a/components/clickup/actions/get-tasks/get-tasks.mjs b/components/clickup/actions/get-tasks/get-tasks.mjs index 5dcba8b72f1ac..9842496855870 100644 --- a/components/clickup/actions/get-tasks/get-tasks.mjs +++ b/components/clickup/actions/get-tasks/get-tasks.mjs @@ -1,11 +1,13 @@ import clickup from "../../clickup.app.mjs"; +import builder from "../../common/builder.mjs"; import common from "../common/list-props.mjs"; export default { + ...common, key: "clickup-get-tasks", name: "Get Tasks", description: "Get a list of tasks. See the docs [here](https://clickup.com/api) in **Tasks / Get Tasks** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, @@ -47,7 +49,14 @@ export default { ], optional: true, }, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, }, + additionalProps: builder.buildListProps(), async run({ $ }) { const { listId, diff --git a/components/clickup/actions/get-team-views/get-team-views.mjs b/components/clickup/actions/get-team-views/get-team-views.mjs index 956166290729c..30d99c799bf6e 100644 --- a/components/clickup/actions/get-team-views/get-team-views.mjs +++ b/components/clickup/actions/get-team-views/get-team-views.mjs @@ -1,12 +1,12 @@ import common from "../common/workspace-prop.mjs"; export default { + ...common, key: "clickup-get-team-views", name: "Get Team Views", description: "Get all views of a team. See the docs [here](https://clickup.com/api) in **Views / Get Team Views** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, async run({ $ }) { const { workspaceId } = this; diff --git a/components/clickup/actions/get-view-comments/get-view-comments.mjs b/components/clickup/actions/get-view-comments/get-view-comments.mjs index f2ef2b8351999..476a42b563131 100644 --- a/components/clickup/actions/get-view-comments/get-view-comments.mjs +++ b/components/clickup/actions/get-view-comments/get-view-comments.mjs @@ -1,12 +1,30 @@ -import common from "../common/view-props.mjs"; +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; +import common from "../common/list-props.mjs"; export default { + ...common, key: "clickup-get-view-comments", name: "Get View Comments", description: "Get a view comments. See the docs [here](https://clickup.com/api) in **Comments / Get Chat View Comments** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, + props: { + ...common.props, + listWithFolder: { + optional: true, + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, + }, + additionalProps: builder.buildListProps({ + listPropsOptional: true, + tailProps: { + viewId: propsFragments.viewId, + }, + }), async run({ $ }) { const { viewId } = this; diff --git a/components/clickup/actions/get-view-tasks/get-view-tasks.mjs b/components/clickup/actions/get-view-tasks/get-view-tasks.mjs index 0211ab0d63e8a..c1efcd4bcd785 100644 --- a/components/clickup/actions/get-view-tasks/get-view-tasks.mjs +++ b/components/clickup/actions/get-view-tasks/get-view-tasks.mjs @@ -1,10 +1,13 @@ -import common from "../common/view-props.mjs"; +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; +import common from "../common/list-props.mjs"; export default { + ...common, key: "clickup-get-view-tasks", name: "Get View Tasks", description: "Get all tasks of a view. See the docs [here](https://clickup.com/api) in **Views / Get View Tasks** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, @@ -16,7 +19,20 @@ export default { default: 0, optional: true, }, + listWithFolder: { + optional: true, + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, }, + additionalProps: builder.buildListProps({ + listPropsOptional: true, + tailProps: { + viewId: propsFragments.viewId, + }, + }), async run({ $ }) { const { viewId, diff --git a/components/clickup/actions/get-view/get-view.mjs b/components/clickup/actions/get-view/get-view.mjs index 3353ca233d13a..a0ede32d6f5c7 100644 --- a/components/clickup/actions/get-view/get-view.mjs +++ b/components/clickup/actions/get-view/get-view.mjs @@ -1,12 +1,30 @@ -import common from "../common/view-props.mjs"; +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; +import common from "../common/list-props.mjs"; export default { + ...common, key: "clickup-get-view", name: "Get View", description: "Get a view. See the docs [here](https://clickup.com/api) in **Views / Get View** section.", - version: "0.0.7", + version: "0.0.9", type: "action", - props: common.props, + props: { + ...common.props, + listWithFolder: { + optional: true, + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, + }, + additionalProps: builder.buildListProps({ + listPropsOptional: true, + tailProps: { + viewId: propsFragments.viewId, + }, + }), async run({ $ }) { const { viewId } = this; diff --git a/components/clickup/actions/remove-task-custom-field/remove-task-custom-field.mjs b/components/clickup/actions/remove-task-custom-field/remove-task-custom-field.mjs index b68522da1da78..e9ff3fd00e786 100644 --- a/components/clickup/actions/remove-task-custom-field/remove-task-custom-field.mjs +++ b/components/clickup/actions/remove-task-custom-field/remove-task-custom-field.mjs @@ -1,23 +1,32 @@ +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; import common from "../common/task-props.mjs"; export default { + ...common, key: "clickup-remove-task-custom-field", name: "Remove Task Custom Field", description: "Remove custom field from a task. See the docs [here](https://clickup.com/api) in **Custom Fields / Remove Custom Field Value** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, - customFieldId: { + listWithFolder: { propDefinition: [ common.props.clickup, - "customFields", - (c) => ({ - listId: c.listId, - }), + "listWithFolder", ], }, }, + additionalProps: builder.buildListProps({ + tailProps: { + taskId: { + ...propsFragments.taskId, + description: "To show options please select a **List** first", + }, + customFieldId: propsFragments.customFieldId, + }, + }), async run({ $ }) { const { taskId, diff --git a/components/clickup/actions/start-time-entry/start-time-entry.mjs b/components/clickup/actions/start-time-entry/start-time-entry.mjs index 1d5e2f08086f2..e23d304561617 100644 --- a/components/clickup/actions/start-time-entry/start-time-entry.mjs +++ b/components/clickup/actions/start-time-entry/start-time-entry.mjs @@ -1,10 +1,13 @@ +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; import common from "../common/task-props.mjs"; export default { + ...common, key: "clickup-start-time-entry", name: "Start Time Entry", description: "Start time entry. [See documentation here](https://clickup.com/api/clickupreference/operation/StartatimeEntry)", - version: "0.0.1", + version: "0.0.3", type: "action", props: { ...common.props, @@ -13,7 +16,23 @@ export default { description: "Description of the time entry", type: "string", }, + listWithFolder: { + optional: true, + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, }, + additionalProps: builder.buildListProps({ + listPropsOptional: true, + tailProps: { + taskId: { + ...propsFragments.taskId, + description: "To show options please select a **List** first", + }, + }, + }), async run({ $ }) { const response = await this.clickup.startTimeEntry({ $, diff --git a/components/clickup/actions/stop-time-entry/stop-time-entry.mjs b/components/clickup/actions/stop-time-entry/stop-time-entry.mjs index c6edb2b00d655..734988b46fe4c 100644 --- a/components/clickup/actions/stop-time-entry/stop-time-entry.mjs +++ b/components/clickup/actions/stop-time-entry/stop-time-entry.mjs @@ -1,12 +1,12 @@ import common from "../common/workspace-prop.mjs"; export default { + ...common, key: "clickup-stop-time-entry", name: "Stop Time Entry", description: "Stop time entry. [See documentation here](https://clickup.com/api/clickupreference/operation/StopatimeEntry)", - version: "0.0.2", + version: "0.0.4", type: "action", - props: common.props, async run({ $ }) { const response = await this.clickup.stopTimeEntry({ $, diff --git a/components/clickup/actions/update-checklist-item/update-checklist-item.mjs b/components/clickup/actions/update-checklist-item/update-checklist-item.mjs index 03ac7ad983b2f..cb5f9a32046aa 100644 --- a/components/clickup/actions/update-checklist-item/update-checklist-item.mjs +++ b/components/clickup/actions/update-checklist-item/update-checklist-item.mjs @@ -1,10 +1,13 @@ -import common from "../common/checklist-item-props.mjs"; +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; +import common from "../common/task-props.mjs"; export default { + ...common, key: "clickup-update-checklist-item", name: "Update Checklist Item", description: "Updates item in a checklist. See the docs [here](https://clickup.com/api) in **Checklists / Edit Checklist Item** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, @@ -31,22 +34,29 @@ export default { type: "boolean", optional: true, }, - parent: { - label: "Checklist Parent", - description: "Set another checklist item as parent", + listWithFolder: { propDefinition: [ common.props.clickup, - "checklistItems", - (c) => ({ - taskId: c.taskId, - checklistId: c.checklistId, - useCustomTaskIds: c.useCustomTaskIds, - authorizedTeamId: c.authorizedTeamId, - }), + "listWithFolder", ], - optional: true, }, }, + additionalProps: builder.buildListProps({ + tailProps: { + taskId: { + ...propsFragments.taskId, + description: "To show options please select a **List** first", + }, + checklistId: propsFragments.checklistId, + checklistItemId: propsFragments.checklistItemId, + parent: { + ...propsFragments.checklistItemId, + label: "Checklist Parent", + description: "Set another checklist item as parent", + optional: true, + }, + }, + }), async run({ $ }) { const { checklistId, diff --git a/components/clickup/actions/update-checklist/update-checklist.mjs b/components/clickup/actions/update-checklist/update-checklist.mjs index 7f1ac192cdb6c..b91b412ec05d6 100644 --- a/components/clickup/actions/update-checklist/update-checklist.mjs +++ b/components/clickup/actions/update-checklist/update-checklist.mjs @@ -1,10 +1,13 @@ -import common from "../common/checklist-props.mjs"; +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; +import common from "../common/task-props.mjs"; export default { + ...common, key: "clickup-update-checklist", name: "Update Checklist", description: "Updates a checklist in a task. See the docs [here](https://clickup.com/api) in **Checklists / Edit Checklist** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, @@ -20,7 +23,22 @@ export default { min: 0, optional: true, }, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, }, + additionalProps: builder.buildListProps({ + tailProps: { + taskId: { + ...propsFragments.taskId, + description: "To show options please select a **List** first", + }, + checklistId: propsFragments.checklistId, + }, + }), async run({ $ }) { const { checklistId, diff --git a/components/clickup/actions/update-comment/update-comment.mjs b/components/clickup/actions/update-comment/update-comment.mjs index ee5e8d7f31746..33cc776c97601 100644 --- a/components/clickup/actions/update-comment/update-comment.mjs +++ b/components/clickup/actions/update-comment/update-comment.mjs @@ -1,10 +1,11 @@ import common from "../common/comment-props.mjs"; export default { + ...common, key: "clickup-update-comment", name: "Update Comment", description: "Updates a comment. See the docs [here](https://clickup.com/api) in **Comments / Update Comment** section.", - version: "0.0.9", + version: "0.0.11", type: "action", props: { ...common.props, @@ -32,6 +33,12 @@ export default { type: "boolean", optional: true, }, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, }, async run({ $ }) { const { diff --git a/components/clickup/actions/update-folder/update-folder.mjs b/components/clickup/actions/update-folder/update-folder.mjs index 0667181df22e7..133e8ff5d37c4 100644 --- a/components/clickup/actions/update-folder/update-folder.mjs +++ b/components/clickup/actions/update-folder/update-folder.mjs @@ -1,10 +1,11 @@ import common from "../common/folder-props.mjs"; export default { + ...common, key: "clickup-update-folder", name: "Update Folder", description: "Update a folder. See the docs [here](https://clickup.com/api) in **Folders / Update Folder** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, diff --git a/components/clickup/actions/update-list/update-list.mjs b/components/clickup/actions/update-list/update-list.mjs index bda2a55516503..67d75cc9759ff 100644 --- a/components/clickup/actions/update-list/update-list.mjs +++ b/components/clickup/actions/update-list/update-list.mjs @@ -1,12 +1,14 @@ import clickup from "../../clickup.app.mjs"; -import common from "../common/list-props.mjs"; +import builder from "../../common/builder.mjs"; import constants from "../common/constants.mjs"; +import common from "../common/list-props.mjs"; export default { + ...common, key: "clickup-update-list", name: "Update List", description: "Update a list. See the docs [here](https://clickup.com/api) in **Lists / Update List** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, @@ -38,7 +40,14 @@ export default { ], optional: true, }, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, }, + additionalProps: builder.buildListProps(), async run({ $ }) { const { listId, diff --git a/components/clickup/actions/update-space/update-space.mjs b/components/clickup/actions/update-space/update-space.mjs index ebf5a9a5769c4..3628044b235e2 100644 --- a/components/clickup/actions/update-space/update-space.mjs +++ b/components/clickup/actions/update-space/update-space.mjs @@ -1,10 +1,11 @@ import common from "../common/space-props.mjs"; export default { + ...common, key: "clickup-update-space", name: "Update Space", description: "Update a space. See the docs [here](https://clickup.com/api) in **Spaces / Update Space** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, diff --git a/components/clickup/actions/update-task-custom-field/update-task-custom-field.mjs b/components/clickup/actions/update-task-custom-field/update-task-custom-field.mjs index f3535a95590e3..1b855167fdffe 100644 --- a/components/clickup/actions/update-task-custom-field/update-task-custom-field.mjs +++ b/components/clickup/actions/update-task-custom-field/update-task-custom-field.mjs @@ -1,28 +1,37 @@ +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; import common from "../common/task-props.mjs"; export default { + ...common, key: "clickup-update-task-custom-field", name: "Update Task Custom Field", description: "Update custom field value of a task. See the docs [here](https://clickup.com/api) in **Custom Fields / Set Custom Field Value** section.", - version: "0.0.7", + version: "0.0.9", type: "action", props: { ...common.props, - customFieldId: { - propDefinition: [ - common.props.clickup, - "customFields", - (c) => ({ - listId: c.listId, - }), - ], - }, value: { label: "Value", type: "any", description: "The value of custom field", }, + listWithFolder: { + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, }, + additionalProps: builder.buildListProps({ + tailProps: { + taskId: { + ...propsFragments.taskId, + description: "To show options please select a **List** first", + }, + customFieldId: propsFragments.customFieldId, + }, + }), async run({ $ }) { const { taskId, diff --git a/components/clickup/actions/update-task/update-task.mjs b/components/clickup/actions/update-task/update-task.mjs index 1bee08da87110..e01c8496a48b9 100644 --- a/components/clickup/actions/update-task/update-task.mjs +++ b/components/clickup/actions/update-task/update-task.mjs @@ -1,12 +1,15 @@ -import common from "../common/task-props.mjs"; -import constants from "../common/constants.mjs"; import { ConfigurationError } from "@pipedream/platform"; +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; +import constants from "../common/constants.mjs"; +import common from "../common/task-props.mjs"; export default { + ...common, key: "clickup-update-task", name: "Update Task", description: "Update a task. See the docs [here](https://clickup.com/api) in **Tasks / Update Task** section.", - version: "0.0.9", + version: "0.0.11", type: "action", props: { ...common.props, @@ -39,29 +42,6 @@ export default { ], optional: true, }, - status: { - propDefinition: [ - common.props.clickup, - "statuses", - (c) => ({ - listId: c.listId, - }), - ], - optional: true, - }, - parent: { - label: "Parent Task", - propDefinition: [ - common.props.clickup, - "tasks", - (c) => ({ - listId: c.listId, - useCustomTaskIds: c.useCustomTaskIds, - authorizedTeamId: c.authorizedTeamId, - }), - ], - optional: true, - }, dueDate: { label: "Due Date", type: "string", @@ -74,7 +54,29 @@ export default { description: "The start date of task, please use `YYYY-MM-DD` format", optional: true, }, + listWithFolder: { + optional: true, + propDefinition: [ + common.props.clickup, + "listWithFolder", + ], + }, }, + additionalProps: builder.buildListProps({ + listPropsOptional: true, + tailProps: { + taskId: { + ...propsFragments.taskId, + description: "To show options please select a **List** first", + }, + status: propsFragments.status, + parent: { + ...propsFragments.taskId, + label: "Parent Task", + optional: true, + }, + }, + }), async run({ $ }) { const { taskId, diff --git a/components/clickup/clickup.app.mjs b/components/clickup/clickup.app.mjs index 89e5dfe812e87..0183778e7bfe9 100644 --- a/components/clickup/clickup.app.mjs +++ b/components/clickup/clickup.app.mjs @@ -1,7 +1,8 @@ -import { axios } from "@pipedream/platform"; -import constants from "./actions/common/constants.mjs"; +import { + axios, ConfigurationError, +} from "@pipedream/platform"; import _ from "lodash"; -import { ConfigurationError } from "@pipedream/platform"; +import constants from "./actions/common/constants.mjs"; export default { type: "app", @@ -52,35 +53,6 @@ export default { })); }, }, - lists: { - type: "string", - label: "List", - description: "The id of a list", - async options({ - workspaceId, folderId, spaceId, - }) { - const lists = []; - - if (!folderId && !spaceId) { - lists.push(...await this.getAllLists({ - workspaceId, - })); - } else if (folderId) { - lists.push(...await this.getLists({ - folderId, - })); - } else if (spaceId) { - lists.push(...await this.getFolderlessLists({ - spaceId, - })); - } - - return lists.map((list) => ({ - label: list.name, - value: list.id, - })); - }, - }, useCustomTaskIds: { type: "boolean", label: "Use custom task ids", @@ -101,35 +73,6 @@ export default { })); }, }, - tasks: { - type: "string", - label: "Task", - description: "The id of a task", - async options({ - listId, page, useCustomTaskIds, - }) { - const tasks = await this.getTasks({ - listId, - params: { - page, - }, - }); - - const tasksHasCustomId = tasks.some((task) => task.custom_id); - if (useCustomTaskIds && !tasksHasCustomId) { - throw new ConfigurationError("Custom task id is a ClickApp, and it must to be enabled on ClickUp settings."); - } - - console.log(tasks); - - return tasks.map((task) => ({ - label: task.name, - value: useCustomTaskIds ? - task.custom_id : - task.id, - })); - }, - }, assignees: { type: "string[]", label: "Assignees", @@ -157,122 +100,6 @@ export default { return tags.map((tag) => tag.name); }, }, - checklists: { - type: "string", - label: "Checklist", - description: "The id of a checklist", - async options({ - taskId, useCustomTaskIds, authorizedTeamId, - }) { - if (!taskId) return []; - - const params = this.getParamsForCustomTaskIdCall(useCustomTaskIds, authorizedTeamId); - - const checklists = await this.getChecklists({ - taskId, - params, - }); - - return checklists.map((checklist) => ({ - label: checklist.name, - value: checklist.id, - })); - }, - }, - checklistItems: { - type: "string", - label: "Checklist Item", - description: "The id of a checklist item", - async options({ - taskId, checklistId, useCustomTaskIds, authorizedTeamId, - }) { - if (!taskId || !checklistId) return []; - - const params = this.getParamsForCustomTaskIdCall(useCustomTaskIds, authorizedTeamId); - - const items = await this.getChecklistItems({ - taskId, - checklistId, - params, - }); - - return items.map((item) => ({ - label: item.name, - value: item.id, - })); - }, - }, - comments: { - type: "string", - label: "Comment", - description: "The id of a comment", - async options({ - taskId, listId, viewId, useCustomTaskIds, authorizedTeamId, - }) { - if (!taskId && !listId && !viewId) { - throw new ConfigurationError("Please enter the List, View, or Task to retrieve Comments from"); - } - let comments = []; - - const params = this.getParamsForCustomTaskIdCall(useCustomTaskIds, authorizedTeamId); - - if (taskId) comments = comments.concat(await this.getTaskComments({ - taskId, - params, - })); - if (listId) comments = comments.concat(await this.getListComments({ - listId, - })); - if (viewId) comments = comments.concat(await this.getViewComments({ - viewId, - })); - - return comments.map((comment) => ({ - label: comment.comment_text, - value: comment.id, - })); - }, - }, - views: { - type: "string", - label: "Views", - description: "The id of a view", - async options({ - workspaceId, spaceId, folderId, listId, - }) { - let views = []; - - if (workspaceId) views = views.concat(await this.getTeamViews({ - workspaceId, - })); - if (spaceId) views = views.concat(await this.getSpaceViews({ - spaceId, - })); - if (folderId) views = views.concat(await this.getFolderViews({ - folderId, - })); - if (listId) views = views.concat(await this.getListViews({ - listId, - })); - - return views.map((view) => ({ - label: view.name, - value: view.id, - })); - }, - }, - statuses: { - type: "string", - label: "Status", - description: "Select a status", - async options({ listId }) { - const { statuses } = await this.getList({ - listId, - }); - - return statuses.map((status) => status.status); - }, - }, taskTemplates: { type: "string", label: "Task Templates", @@ -293,23 +120,6 @@ export default { })); }, }, - customFields: { - type: "string", - label: "Custom Field", - description: "Select a custom field", - async options({ listId }) { - if (!listId) return []; - - const fields = await this.getCustomFields({ - listId, - }); - - return fields.map((field) => ({ - label: field.name, - value: field.id, - })); - }, - }, priorities: { type: "string", label: "Priority", @@ -317,6 +127,12 @@ export default { options: Object.keys(constants.PRIORITIES), default: "Normal", }, + listWithFolder: { + type: "boolean", + label: "Filter List ID By Folder?", + description: "If `TRUE`, the **List ID** field will be filtered by the selected **Folder ID**", + reloadProps: true, + }, }, methods: { /** @@ -473,36 +289,6 @@ export default { method: "DELETE", }, $); }, - async getAllLists({ - workspaceId, params, $, - }) { - const lists = []; - const foldersPromises = []; - - const spaces = await this.getSpaces({ - workspaceId, - params, - $, - }); - - for (const { id: spaceId } of spaces) { - foldersPromises.push(this.getFolders({ - spaceId, - })); - lists.push(...await this.getFolderlessLists({ - spaceId, - })); - } - - const folders = (await Promise.all(foldersPromises)).flat(); - for (const { id: folderId } of folders) { - lists.push(...await this.getLists({ - folderId, - })); - } - - return lists; - }, async getLists({ folderId, params, $, }) { @@ -776,6 +562,14 @@ export default { params, }, $); }, + async createThreadedComment({ + commentId, data, $, + }) { + return this._makeRequest(`comment/${commentId}/reply`, { + method: "POST", + data, + }, $); + }, async createListComment({ listId, data, $, }) { diff --git a/components/clickup/common/builder.mjs b/components/clickup/common/builder.mjs new file mode 100644 index 0000000000000..c33efd4f56b01 --- /dev/null +++ b/components/clickup/common/builder.mjs @@ -0,0 +1,84 @@ +const buildListProps = ({ + listPropsOptional: optional = false, + tailProps, +} = {}) => function additionalProps() { + if (!this.listWithFolder) { + return { + listId: { + type: "string", + label: "List ID", + description: "The id of a list", + optional, + options: async () => { + const { + app, + clickup, + spaceId, + } = this; + const lists = await (clickup || app).getFolderlessLists({ + spaceId, + }); + return lists.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + ...tailProps, + }; + } + + return { + folderId: { + type: "string", + label: "Folder ID", + description: "The id of a folder", + optional, + options: async () => { + const { + app, + clickup, + spaceId, + } = this; + const folders = await (clickup || app).getFolders({ + spaceId, + }); + return folders.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + listId: { + type: "string", + label: "List ID", + description: "The id of a list", + optional, + options: async () => { + const { + app, + clickup, + folderId, + } = this; + const lists = await (clickup || app).getLists({ + folderId, + }); + return lists.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + ...tailProps, + }; +}; + +export default { + buildListProps, +}; diff --git a/components/clickup/common/props-fragments.mjs b/components/clickup/common/props-fragments.mjs new file mode 100644 index 0000000000000..508359ab9b2a0 --- /dev/null +++ b/components/clickup/common/props-fragments.mjs @@ -0,0 +1,223 @@ +import { ConfigurationError } from "@pipedream/platform"; + +export default { + viewId: { + type: "string", + label: "View Id", + description: "The id of a view", + options: async () => { + const { + app, + clickup, + workspaceId, + spaceId, + folderId, + listId, + } = this; + + let views = []; + if (workspaceId) { + views = views.concat(await (clickup || app).getTeamViews({ + workspaceId, + })); + } + if (spaceId) { + views = views.concat(await (clickup || app).getSpaceViews({ + spaceId, + })); + } + if (folderId) { + views = views.concat(await (clickup || app).getFolderViews({ + folderId, + })); + } + if (listId) { + views = views.concat(await (clickup || app).getListViews({ + listId, + })); + } + return views.map((view) => ({ + label: view.name, + value: view.id, + })); + }, + }, + taskId: { + type: "string", + label: "Task", + description: "The id of a task", + options: async ({ page }) => { + const { + app, + clickup, + listId, + useCustomTaskIds, + } = this; + + const tasks = await (clickup || app).getTasks({ + listId, + params: { + page, + }, + }); + + const tasksHasCustomId = tasks.some((task) => task.custom_id); + if (useCustomTaskIds && !tasksHasCustomId) { + throw new ConfigurationError("Custom task id is a ClickApp, and it must to be enabled on ClickUp settings."); + } + + return tasks.map((task) => ({ + label: task.name, + value: useCustomTaskIds ? + task.custom_id : + task.id, + })); + }, + }, + status: { + type: "string", + label: "Status", + description: "Select a status", + optional: true, + options: async () => { + const { + app, + clickup, + listId, + } = this; + + const { statuses } = await (clickup || app).getList({ + listId, + }); + return statuses.map(({ status }) => status); + }, + }, + checklistId: { + type: "string", + label: "Checklist", + description: "To show options please select a **Task** first", + options: async () => { + const { + app, + clickup, + taskId, + useCustomTaskIds, + authorizedTeamId, + } = this; + + if (!taskId) { + return []; + } + + const params = + (clickup || app).getParamsForCustomTaskIdCall(useCustomTaskIds, authorizedTeamId); + + const checklists = await (clickup || app).getChecklists({ + taskId, + params, + }); + + return checklists.map((checklist) => ({ + label: checklist.name, + value: checklist.id, + })); + }, + }, + checklistItemId: { + type: "string", + label: "Checklist Item", + description: "To show options please select a **Task and Checklist** first", + options: async () => { + const { + app, + clickup, + taskId, + checklistId, + useCustomTaskIds, + authorizedTeamId, + } = this; + + if (!taskId || !checklistId) { + return []; + } + + const params = + (clickup || app).getParamsForCustomTaskIdCall(useCustomTaskIds, authorizedTeamId); + + const items = await (clickup || app).getChecklistItems({ + taskId, + checklistId, + params, + }); + + return items.map((item) => ({ + label: item.name, + value: item.id, + })); + }, + }, + customFieldId: { + type: "string", + label: "Custom Field", + description: "Select a custom field", + options: async () => { + const { + app, + clickup, + listId, + } = this; + if (!listId) { + return []; + } + const fields = await (clickup || app).getCustomFields({ + listId, + }); + + return fields.map((field) => ({ + label: field.name, + value: field.id, + })); + }, + }, + commentId: { + type: "string", + label: "Comment", + description: "The id of a comment", + options: async () => { + const { + app, + clickup, + taskId, + listId, + viewId, + } = this; + + if (!taskId && !listId && !viewId) { + throw new ConfigurationError("Please enter the List, View, or Task to retrieve Comments from"); + } + let comments = []; + + if (taskId) { + comments = comments.concat(await (clickup || app).getTaskComments({ + taskId, + params: {}, + })); + } + if (listId) { + comments = comments.concat(await (clickup || app).getListComments({ + listId, + })); + } + if (viewId) { + comments = comments.concat(await (clickup || app).getViewComments({ + viewId, + })); + } + + return comments.map((comment) => ({ + label: comment.comment_text, + value: comment.id, + })); + }, + }, +}; diff --git a/components/clickup/package.json b/components/clickup/package.json index dd67e03676d45..42a72bed2f7ec 100644 --- a/components/clickup/package.json +++ b/components/clickup/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/clickup", - "version": "0.2.1", + "version": "0.3.0", "description": "Pipedream Clickup Components", "main": "clickup.app.mjs", "keywords": [ @@ -14,7 +14,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^0.10.0", + "@pipedream/platform": "^3.0.0", "crypto": "^1.0.1", "lodash": "^4.17.21" } diff --git a/components/clickup/sources/new-folder/new-folder.mjs b/components/clickup/sources/new-folder/new-folder.mjs index f1129269090cf..ec5eec8fa83ce 100644 --- a/components/clickup/sources/new-folder/new-folder.mjs +++ b/components/clickup/sources/new-folder/new-folder.mjs @@ -1,11 +1,12 @@ import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "clickup-new-folder", name: "New Folder (Instant)", description: "Emit new event when a new folder is created", - version: "0.0.6", + version: "0.0.8", dedupe: "unique", type: "source", methods: { @@ -28,4 +29,5 @@ export default { this.checkSignature(httpRequest); this.$emit(httpRequest.body, this._getMeta(httpRequest.body)); }, + sampleEmit, }; diff --git a/components/clickup/sources/new-folder/test-event.mjs b/components/clickup/sources/new-folder/test-event.mjs new file mode 100644 index 0000000000000..6d99d8cdb3e22 --- /dev/null +++ b/components/clickup/sources/new-folder/test-event.mjs @@ -0,0 +1,5 @@ +export default { + "event": "folderCreated", + "folder_id": "96772212", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" +} \ No newline at end of file diff --git a/components/clickup/sources/new-list/new-list.mjs b/components/clickup/sources/new-list/new-list.mjs index e2ed36c6eb4c7..ce91399c4edc8 100644 --- a/components/clickup/sources/new-list/new-list.mjs +++ b/components/clickup/sources/new-list/new-list.mjs @@ -1,11 +1,12 @@ import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "clickup-new-list", name: "New List (Instant)", description: "Emit new event when a new list is created", - version: "0.0.6", + version: "0.0.8", dedupe: "unique", type: "source", methods: { @@ -28,4 +29,5 @@ export default { this.checkSignature(httpRequest); this.$emit(httpRequest.body, this._getMeta(httpRequest.body)); }, + sampleEmit, }; diff --git a/components/clickup/sources/new-list/test-event.mjs b/components/clickup/sources/new-list/test-event.mjs new file mode 100644 index 0000000000000..a90c53eab9984 --- /dev/null +++ b/components/clickup/sources/new-list/test-event.mjs @@ -0,0 +1,5 @@ +export default { + "event": "listCreated", + "list_id": "162641234", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" +} \ No newline at end of file diff --git a/components/clickup/sources/new-task-advanced/new-task-advanced.mjs b/components/clickup/sources/new-task-advanced/new-task-advanced.mjs index 258cae7b27c68..e311b83349508 100644 --- a/components/clickup/sources/new-task-advanced/new-task-advanced.mjs +++ b/components/clickup/sources/new-task-advanced/new-task-advanced.mjs @@ -1,11 +1,13 @@ +import builder from "../../common/builder.mjs"; import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "clickup-new-task-advanced", name: "New Task Advanced (Instant)", description: "Emit new event when a new task is created matching the filter", - version: "0.0.3", + version: "0.0.6", dedupe: "unique", type: "source", props: { @@ -18,33 +20,18 @@ export default { workspaceId: c.workspaceId, }), ], - description: "If selected, the **Lists** will be filtered by this Space ID", - optional: true, }, - folderId: { - propDefinition: [ - common.props.app, - "folders", - (c) => ({ - spaceId: c.spaceId, - }), - ], - description: "If selected, the **Lists** will be filtered by this Folder ID", + listWithFolder: { optional: true, - }, - listId: { propDefinition: [ common.props.app, - "lists", - (c) => ({ - workspaceId: c.workspaceId, - spaceId: c.spaceId, - folderId: c.folderId, - }), + "listWithFolder", ], - optional: true, }, }, + additionalProps: builder.buildListProps({ + listPropsOptional: true, + }), methods: { ...common.methods, _getMeta({ task_id: taskId }) { @@ -89,4 +76,5 @@ export default { }, this._getMeta(httpRequest.body)); } }, + sampleEmit, }; diff --git a/components/clickup/sources/new-task-advanced/test-event.mjs b/components/clickup/sources/new-task-advanced/test-event.mjs new file mode 100644 index 0000000000000..2329eb6a7792f --- /dev/null +++ b/components/clickup/sources/new-task-advanced/test-event.mjs @@ -0,0 +1,57 @@ +export default { + "event": "taskCreated", + "history_items": [ + { + "id": "2800763136717140857", + "type": 1, + "date": "1642734631523", + "field": "status", + "parent_id": "162641062", + "data": { + "status_type": "open" + }, + "source": null, + "user": { + "id": 183, + "username": "John", + "email": "john@company.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + }, + "before": { + "status": null, + "color": "#000000", + "type": "removed", + "orderindex": -1 + }, + "after": { + "status": "to do", + "color": "#f9d900", + "orderindex": 0, + "type": "open" + } + }, + { + "id": "2800763136700363640", + "type": 1, + "date": "1642734631523", + "field": "task_creation", + "parent_id": "162641062", + "data": {}, + "source": null, + "user": { + "id": 183, + "username": "John", + "email": "john@company.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + }, + "before": null, + "after": null + } + ], + "task_id": "1vj37mc", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" +} \ No newline at end of file diff --git a/components/clickup/sources/new-task-comment-updated/new-task-comment-updated.mjs b/components/clickup/sources/new-task-comment-updated/new-task-comment-updated.mjs new file mode 100644 index 0000000000000..61f21dfe69d92 --- /dev/null +++ b/components/clickup/sources/new-task-comment-updated/new-task-comment-updated.mjs @@ -0,0 +1,72 @@ +import builder from "../../common/builder.mjs"; +import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "clickup-new-task-comment-updated", + name: "New Task Comment Updated (Instant)", + description: "Emit new event when a new task comment is updated", + version: "0.0.1", + dedupe: "unique", + type: "source", + props: { + ...common.props, + spaceId: { + propDefinition: [ + common.props.app, + "spaces", + (c) => ({ + workspaceId: c.workspaceId, + }), + ], + }, + listWithFolder: { + optional: true, + propDefinition: [ + common.props.app, + "listWithFolder", + ], + }, + }, + additionalProps: builder.buildListProps({ + listPropsOptional: true, + }), + methods: { + ...common.methods, + _getMeta({ + task_id: taskId, history_items: [ + { id }, + ], + }) { + return { + id, + summary: String(taskId), + ts: Date.now(), + }; + }, + _getEventsList() { + return [ + "taskCommentUpdated", + ]; + }, + }, + async run(httpRequest) { + console.log("Event received"); + this.checkSignature(httpRequest); + + const { body } = httpRequest; + const { listId } = this; + if (listId) { + const { task_id: taskId } = body; + const { list: { id } } = await this.app.getTask({ + taskId, + }); + + if (id !== listId) return; + } + + this.$emit(body, this._getMeta(body)); + }, + sampleEmit, +}; diff --git a/components/clickup/sources/new-task-comment-updated/test-event.mjs b/components/clickup/sources/new-task-comment-updated/test-event.mjs new file mode 100644 index 0000000000000..9a0e22c402b85 --- /dev/null +++ b/components/clickup/sources/new-task-comment-updated/test-event.mjs @@ -0,0 +1,85 @@ +export default { + "event": "taskCommentUpdated", + "history_items": [ + { + "id": "2800803631413624919", + "type": 1, + "date": "1642737045116", + "field": "comment", + "parent_id": "162641285", + "data": {}, + "source": null, + "user": { + "id": 183, + "username": "John", + "email": "john@company.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + }, + "before": null, + "after": "648893191", + "comment": { + "id": "648893191", + "date": "1642737045116", + "parent": "1vj38vv", + "type": 1, + "comment": [ + { + "text": "comment abc1234 56789", + "attributes": {} + }, + { + "text": "\n", + "attributes": { + "block-id": "block-4c8fe54f-7bff-4b7b-92a2-9142068983ea" + } + } + ], + "text_content": "comment abc1234 56789\n", + "x": null, + "y": null, + "image_y": null, + "image_x": null, + "page": null, + "comment_number": null, + "page_id": null, + "page_name": null, + "view_id": null, + "view_name": null, + "team": null, + "user": { + "id": 183, + "username": "John", + "email": "john@company.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + }, + "new_thread_count": 0, + "new_mentioned_thread_count": 0, + "email_attachments": [], + "threaded_users": [], + "threaded_replies": 0, + "threaded_assignees": 0, + "threaded_assignees_members": [], + "threaded_unresolved_count": 0, + "thread_followers": [ + { + "id": 183, + "username": "John", + "email": "john@company.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + } + ], + "group_thread_followers": [], + "reactions": [], + "emails": [] + } + } + ], + "task_id": "1vj38vv", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" +} \ No newline at end of file diff --git a/components/clickup/sources/new-task-comment/new-task-comment.mjs b/components/clickup/sources/new-task-comment/new-task-comment.mjs new file mode 100644 index 0000000000000..c945cf8fc359f --- /dev/null +++ b/components/clickup/sources/new-task-comment/new-task-comment.mjs @@ -0,0 +1,72 @@ +import builder from "../../common/builder.mjs"; +import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "clickup-new-task-comment", + name: "New Task Comment (Instant)", + description: "Emit new event when a new task comment is posted", + version: "0.0.1", + dedupe: "unique", + type: "source", + props: { + ...common.props, + spaceId: { + propDefinition: [ + common.props.app, + "spaces", + (c) => ({ + workspaceId: c.workspaceId, + }), + ], + }, + listWithFolder: { + optional: true, + propDefinition: [ + common.props.app, + "listWithFolder", + ], + }, + }, + additionalProps: builder.buildListProps({ + listPropsOptional: true, + }), + methods: { + ...common.methods, + _getMeta({ + task_id: taskId, history_items: [ + { id }, + ], + }) { + return { + id, + summary: String(taskId), + ts: Date.now(), + }; + }, + _getEventsList() { + return [ + "taskCommentPosted", + ]; + }, + }, + async run(httpRequest) { + console.log("Event received"); + this.checkSignature(httpRequest); + + const { body } = httpRequest; + const { listId } = this; + if (listId) { + const { task_id: taskId } = body; + const { list: { id } } = await this.app.getTask({ + taskId, + }); + + if (id !== listId) return; + } + + this.$emit(body, this._getMeta(body)); + }, + sampleEmit, +}; diff --git a/components/clickup/sources/new-task-comment/test-event.mjs b/components/clickup/sources/new-task-comment/test-event.mjs new file mode 100644 index 0000000000000..080af97caaf07 --- /dev/null +++ b/components/clickup/sources/new-task-comment/test-event.mjs @@ -0,0 +1,85 @@ +export default { + "event": "taskCommentPosted", + "history_items": [ + { + "id": "2800803631413624919", + "type": 1, + "date": "1642737045116", + "field": "comment", + "parent_id": "162641285", + "data": {}, + "source": null, + "user": { + "id": 183, + "username": "John", + "email": "john@company.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + }, + "before": null, + "after": "648893191", + "comment": { + "id": "648893191", + "date": "1642737045116", + "parent": "1vj38vv", + "type": 1, + "comment": [ + { + "text": "comment abc1234", + "attributes": {} + }, + { + "text": "\n", + "attributes": { + "block-id": "block-4c8fe54f-7bff-4b7b-92a2-9142068983ea" + } + } + ], + "text_content": "comment abc1234\n", + "x": null, + "y": null, + "image_y": null, + "image_x": null, + "page": null, + "comment_number": null, + "page_id": null, + "page_name": null, + "view_id": null, + "view_name": null, + "team": null, + "user": { + "id": 183, + "username": "John", + "email": "john@company.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + }, + "new_thread_count": 0, + "new_mentioned_thread_count": 0, + "email_attachments": [], + "threaded_users": [], + "threaded_replies": 0, + "threaded_assignees": 0, + "threaded_assignees_members": [], + "threaded_unresolved_count": 0, + "thread_followers": [ + { + "id": 183, + "username": "John", + "email": "john@company.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + } + ], + "group_thread_followers": [], + "reactions": [], + "emails": [] + } + } + ], + "task_id": "1vj38vv", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" +} \ No newline at end of file diff --git a/components/clickup/sources/new-task/new-task.mjs b/components/clickup/sources/new-task/new-task.mjs index 670daf5fe0307..ac421e120060a 100644 --- a/components/clickup/sources/new-task/new-task.mjs +++ b/components/clickup/sources/new-task/new-task.mjs @@ -1,28 +1,37 @@ +import builder from "../../common/builder.mjs"; import common from "../common/common.mjs"; -import app from "../../clickup.app.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "clickup-new-task", name: "New Task (Instant)", description: "Emit new event when a new task is created", - version: "0.1.2", + version: "0.1.5", dedupe: "unique", type: "source", props: { ...common.props, - listId: { + spaceId: { propDefinition: [ - app, - "lists", - ({ workspaceId }) => ({ - workspaceId, + common.props.app, + "spaces", + (c) => ({ + workspaceId: c.workspaceId, }), ], - description: "If a list is selected, only tasks created in this list will emit an event", + }, + listWithFolder: { optional: true, + propDefinition: [ + common.props.app, + "listWithFolder", + ], }, }, + additionalProps: builder.buildListProps({ + listPropsOptional: true, + }), methods: { ...common.methods, _getMeta({ task_id: taskId }) { @@ -55,4 +64,5 @@ export default { this.$emit(body, this._getMeta(body)); }, + sampleEmit, }; diff --git a/components/clickup/sources/new-task/test-event.mjs b/components/clickup/sources/new-task/test-event.mjs new file mode 100644 index 0000000000000..2329eb6a7792f --- /dev/null +++ b/components/clickup/sources/new-task/test-event.mjs @@ -0,0 +1,57 @@ +export default { + "event": "taskCreated", + "history_items": [ + { + "id": "2800763136717140857", + "type": 1, + "date": "1642734631523", + "field": "status", + "parent_id": "162641062", + "data": { + "status_type": "open" + }, + "source": null, + "user": { + "id": 183, + "username": "John", + "email": "john@company.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + }, + "before": { + "status": null, + "color": "#000000", + "type": "removed", + "orderindex": -1 + }, + "after": { + "status": "to do", + "color": "#f9d900", + "orderindex": 0, + "type": "open" + } + }, + { + "id": "2800763136700363640", + "type": 1, + "date": "1642734631523", + "field": "task_creation", + "parent_id": "162641062", + "data": {}, + "source": null, + "user": { + "id": 183, + "username": "John", + "email": "john@company.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + }, + "before": null, + "after": null + } + ], + "task_id": "1vj37mc", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" +} \ No newline at end of file diff --git a/components/clickup/sources/updated-task/test-event.mjs b/components/clickup/sources/updated-task/test-event.mjs new file mode 100644 index 0000000000000..fba82f76a3fc9 --- /dev/null +++ b/components/clickup/sources/updated-task/test-event.mjs @@ -0,0 +1,26 @@ +export default { + "event": "taskUpdated", + "history_items": [ + { + "id": "2800768061568222238", + "type": 1, + "date": "1642734925064", + "field": "content", + "parent_id": "162641062", + "data": {}, + "source": null, + "user": { + "id": 183, + "username": "John", + "email": "john@company.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + }, + "before": null, + "after": "{\"ops\":[{\"insert\":\"This is a task description update to trigger the \"},{\"insert\":\"\\n\",\"attributes\":{\"block-id\":\"block-24d0457c-908f-412c-8267-da08f8dc93e4\"}}]}" + } + ], + "task_id": "1vj37mc", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" +} \ No newline at end of file diff --git a/components/clickup/sources/updated-task/updated-task.mjs b/components/clickup/sources/updated-task/updated-task.mjs index a1a6d05d5ee4c..e4502079f3885 100644 --- a/components/clickup/sources/updated-task/updated-task.mjs +++ b/components/clickup/sources/updated-task/updated-task.mjs @@ -1,26 +1,25 @@ +import builder from "../../common/builder.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; import common from "../common/common.mjs"; -import app from "../../clickup.app.mjs"; import constants from "../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "clickup-updated-task", name: "New Updated Task (Instant)", description: "Emit new event when a new task is updated", - version: "0.0.8", + version: "0.0.11", dedupe: "unique", type: "source", props: { ...common.props, - listId: { - description: "If a list is selected, only tasks updated in this list will emit an event", - optional: true, - reloadProps: true, + spaceId: { propDefinition: [ - app, - "lists", - ({ workspaceId }) => ({ - workspaceId, + common.props.app, + "spaces", + (c) => ({ + workspaceId: c.workspaceId, }), ], }, @@ -31,20 +30,26 @@ export default { options: constants.TASK_FIELDS, optional: true, }, - customFieldIds: { - label: "Custom Fields", - type: "string[]", - description: "Select a custom field to filter", + listWithFolder: { optional: true, propDefinition: [ - app, - "customFields", - ({ listId }) => ({ - listId, - }), + common.props.app, + "listWithFolder", ], }, }, + additionalProps: builder.buildListProps({ + listPropsOptional: true, + tailProps: { + customFieldIds: { + ...propsFragments.customFieldId, + label: "Custom Fields", + type: "string[]", + description: "Select a custom field to filter", + optional: true, + }, + }, + }), methods: { ...common.methods, _getMeta(historyItems) { @@ -93,6 +98,6 @@ export default { if (isValidated) { this.$emit(body, this._getMeta(body.history_items)); } - }, + sampleEmit, }; diff --git a/components/cliengo/README.md b/components/cliengo/README.md index bfd6870c45b25..ab9f7c2735e13 100644 --- a/components/cliengo/README.md +++ b/components/cliengo/README.md @@ -1,31 +1,11 @@ # Overview -With the Cliengo API, you can easily build a chatbot for your business. Cliengo -is a powerful chatbot platform that makes it easy to create and manage bots for -your business. +Cliengo is a chatbot platform that lets you automate conversations with visitors on your website, aiding in lead capture and customer service. The Cliengo API offers capabilities to programmatically manage these conversations, extract chat data, and integrate with CRM systems. By leveraging Pipedream, you can connect Cliengo to a variety of different apps and services to streamline workflows, such as syncing chat data to your CRM, triggering emails based on chat events, or analyzing customer interaction patterns. -Here are some examples of what you can build with the Cliengo API: +# Example Use Cases -- A chatbot to help customers with their orders -- A chatbot to answer customer questions -- A chatbot to provide customer support -- A chatbot to schedule appointments -- A chatbot to take surveys -- A chatbot to collect leads -- A chatbot to provide recommendations -- A chatbot to give directions -- A chatbot to play games -- A chatbot to provide weather updates -- A chatbot to news stories -- A chatbot to find local businesses -- A chatbot to book hotels -- A chatbot to book flights -- A chatbot to find restaurants -- A chatbot to find movie times -- A chatbot to find out the score of a game -- A chatbot to track a package -- A chatbot to give traffic updates -- A chatbot to show the latest trending topics -- A chatbot to tell a joke +- **Lead Capture to CRM**: When a visitor engages with the Cliengo chatbot and provides contact information, use Pipedream to automatically add or update this lead in your CRM software, such as Salesforce or HubSpot. This ensures that potential customers are quickly and efficiently integrated into your sales pipeline. -The possibilities are endless! +- **Support Ticket Creation**: If a visitor reports an issue via the Cliengo chatbot, set up a workflow using Pipedream to create a support ticket in your helpdesk system like Zendesk. The workflow can include relevant chat data to provide context for the support team, streamlining the resolution process. + +- **Chat Analysis and Reporting**: Implement a workflow that regularly collects chat logs through the Cliengo API and sends them to a data analysis tool like Google BigQuery. Here, you can run queries to gain insights into common inquiries or issues, which can then be used to inform business decisions or to optimize the chatbot's responses. diff --git a/components/cliengo/package.json b/components/cliengo/package.json new file mode 100644 index 0000000000000..2b26bf20951ad --- /dev/null +++ b/components/cliengo/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/cliengo", + "version": "0.6.0", + "description": "Pipedream cliengo Components", + "main": "cliengo.app.mjs", + "keywords": [ + "pipedream", + "cliengo" + ], + "homepage": "https://pipedream.com/apps/cliengo", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/clientary/README.md b/components/clientary/README.md new file mode 100644 index 0000000000000..3ad06fa66b005 --- /dev/null +++ b/components/clientary/README.md @@ -0,0 +1,11 @@ +# Overview + +The Clientary API allows you to automate and integrate Clientary's project management, time tracking, and invoicing functionalities within Pipedream. By leveraging this API, you can create custom workflows to streamline operations, such as syncing new invoices with accounting software, triggering notifications based on project updates, or automating time tracking entries. Pipedream acts as the glue, enabling you to connect Clientary with hundreds of other apps to create sophisticated, serverless workflows tailored to your business needs. + +# Example Use Cases + +- **Invoice Synchronization Workflow**: Automate the process of synchronizing newly created invoices in Clientary with your accounting software like QuickBooks. Every time an invoice is generated in Clientary, the workflow triggers, extracts the invoice details, and posts them to QuickBooks, keeping your accounts up to date without manual entry. + +- **Project Update Notifications Workflow**: Set up a workflow that sends real-time notifications via Slack or email when there's an update on any project within Clientary. Whether it's a status change or a new comment, you'll get an instant alert, ensuring that your team stays informed and can react swiftly to project developments. + +- **Automated Time Entry Workflow**: Integrate Clientary with time-tracking tools like Toggl. When a time entry is stopped in Toggl, a Pipedream workflow can capture this event and automatically create a corresponding time entry in Clientary. This removes the redundancy of logging time across multiple platforms and ensures that billing is always up-to-date. diff --git a/components/clientify/README.md b/components/clientify/README.md new file mode 100644 index 0000000000000..54f35653d4fa2 --- /dev/null +++ b/components/clientify/README.md @@ -0,0 +1,11 @@ +# Overview + +The Clientify API enables automation and integration of Clientify's CRM functions within Pipedream. This API allows you to manage contacts, accounts, deals, and tasks, streamlining your customer relationship management. With Pipedream, you can harness these capabilities to create custom workflows that trigger actions within Clientify, respond to events, or connect with other apps to enhance productivity and data utilization. + +# Example Use Cases + +- **Sync New Shopify Orders to Clientify Deals**: When a new order is placed in Shopify, Pipedream captures the event and creates a corresponding deal in Clientify, ensuring your sales pipeline is always up-to-date with e-commerce activity. + +- **Schedule Follow-up Tasks After Zoom Meetings**: Post-meeting automation where Pipedream listens for Zoom meeting ends, then creates follow-up tasks in Clientify for the sales team to reach out to participants, maintaining engagement and opportunity tracking. + +- **Automate LinkedIn Lead Generation to Clientify Contacts**: Combine LinkedIn's lead gen forms with Clientify by using Pipedream to automatically add new leads as contacts in Clientify, streamlining the process of capturing and managing potential clients from social media campaigns. diff --git a/components/cliento/actions/get-ref-data/get-ref-data.mjs b/components/cliento/actions/get-ref-data/get-ref-data.mjs new file mode 100644 index 0000000000000..393af01511ff5 --- /dev/null +++ b/components/cliento/actions/get-ref-data/get-ref-data.mjs @@ -0,0 +1,19 @@ +import cliento from "../../cliento.app.mjs"; + +export default { + key: "cliento-get-ref-data", + name: "Get Reference Data", + description: "Fetch services, resources and mappings for the booking widget. [See the documentation](https://developers.cliento.com/docs/rest-api#fetch-ref-data)", + version: "0.0.1", + type: "action", + props: { + cliento, + }, + async run({ $ }) { + const response = await this.cliento.fetchRefData({ + $, + }); + $.export("$summary", "Successfully fetched reference data"); + return response; + }, +}; diff --git a/components/cliento/actions/get-settings/get-settings.mjs b/components/cliento/actions/get-settings/get-settings.mjs new file mode 100644 index 0000000000000..9032bb3be361b --- /dev/null +++ b/components/cliento/actions/get-settings/get-settings.mjs @@ -0,0 +1,19 @@ +import cliento from "../../cliento.app.mjs"; + +export default { + key: "cliento-get-settings", + name: "Fetch Settings", + description: "Fetch settings and features for the booking widget. [See the documentation](https://developers.cliento.com/docs/rest-api#fetch-settings)", + version: "0.0.1", + type: "action", + props: { + cliento, + }, + async run({ $ }) { + const response = await this.cliento.fetchSettings({ + $, + }); + $.export("$summary", "Successfully fetched settings"); + return response; + }, +}; diff --git a/components/cliento/actions/get-slots/get-slots.mjs b/components/cliento/actions/get-slots/get-slots.mjs new file mode 100644 index 0000000000000..88fbc1845b48c --- /dev/null +++ b/components/cliento/actions/get-slots/get-slots.mjs @@ -0,0 +1,53 @@ +import cliento from "../../cliento.app.mjs"; + +export default { + key: "cliento-get-slots", + name: "Get Slots", + description: "Fetch available slots for the given service, resource and dates. [See the documentation](https://developers.cliento.com/docs/rest-api#fetch-slots)", + version: "0.0.1", + type: "action", + props: { + cliento, + fromDate: { + propDefinition: [ + cliento, + "fromDate", + ], + }, + toDate: { + propDefinition: [ + cliento, + "toDate", + ], + }, + resourceIds: { + propDefinition: [ + cliento, + "resourceIds", + ], + }, + serviceIds: { + propDefinition: [ + cliento, + "serviceIds", + ], + }, + }, + async run({ $ }) { + const response = await this.cliento.fetchSlots({ + $, + params: { + fromDate: this.fromDate, + toDate: this.toDate, + resIds: this.resourceIds.join(), + srvIds: this.serviceIds.join(), + }, + }); + if (response?.length) { + $.export("$summary", `Successfully fetched ${response.length} slot${response.length === 1 + ? "" + : "s"}`); + } + return response; + }, +}; diff --git a/components/cliento/cliento.app.mjs b/components/cliento/cliento.app.mjs new file mode 100644 index 0000000000000..7f31a9503ac62 --- /dev/null +++ b/components/cliento/cliento.app.mjs @@ -0,0 +1,83 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "cliento", + propDefinitions: { + fromDate: { + type: "string", + label: "From Date", + description: "The start date for the booking period (format: YYYY-MM-DD)", + }, + toDate: { + type: "string", + label: "To Date", + description: "The end date for the booking period (format: YYYY-MM-DD)", + }, + resourceIds: { + type: "string[]", + label: "Resource IDs", + description: "The IDs of the resources for the booking", + async options() { + const { resources } = await this.fetchRefData(); + return resources?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + serviceIds: { + type: "string[]", + label: "Service IDs", + description: "The IDs of the services for the booking", + async options() { + const { services } = await this.fetchRefData(); + return services?.map(({ + serviceId: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return `https://cliento.com/api/v2/partner/cliento/${this.$auth.account_id}`; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Accept: "application/json", + }, + }); + }, + fetchSettings(opts = {}) { + return this._makeRequest({ + path: "/settings/", + ...opts, + }); + }, + fetchRefData(opts = {}) { + return this._makeRequest({ + path: "/ref-data/", + ...opts, + }); + }, + fetchSlots(opts = {}) { + return this._makeRequest({ + path: "/resources/slots", + ...opts, + }); + }, + }, +}; diff --git a/components/cliento/package.json b/components/cliento/package.json new file mode 100644 index 0000000000000..0242739d685b9 --- /dev/null +++ b/components/cliento/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/cliento", + "version": "0.1.0", + "description": "Pipedream Cliento Components", + "main": "cliento.app.mjs", + "keywords": [ + "pipedream", + "cliento" + ], + "homepage": "https://pipedream.com/apps/cliento", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/clinchpad/README.md b/components/clinchpad/README.md index be33efd523299..72504239e5ceb 100644 --- a/components/clinchpad/README.md +++ b/components/clinchpad/README.md @@ -1,10 +1,11 @@ # Overview -With the ClinchPad API, you can build applications that: - -- Create and manage ClinchPad users -- Access and update information about ClinchPad leads -- Add, remove, and update lead notes -- Create and manage tasks for ClinchPad leads -- Set and update lead stages -- Create and manage deals in ClinchPad +ClinchPad is a modern CRM tailored for small teams to manage sales pipelines with minimal fuss. The ClinchPad API integrates with Pipedream, allowing you to automate interactions with your sales pipeline. For instance, you can automatically create leads, update their status, track communications, or gather analytics for reporting purposes. Through Pipedream, you can connect ClinchPad with hundreds of other apps to streamline your sales process, maintain customer relations, and close deals faster. + +# Example Use Cases + +- **Sales Pipeline Automation**: Automatically create a new lead in ClinchPad when a form is submitted on your website. Using Pipedream’s HTTP requests trigger, you can capture form data and feed it into ClinchPad, ensuring no lead falls through the cracks. + +- **Lead Status Update Notifications**: Set up a Slack notification whenever a lead status changes in ClinchPad. By linking the ClinchPad API with Slack on Pipedream, you can keep your sales team instantly informed about lead progress, fostering swift action and follow-ups. + +- **Email Campaign Follow-Up**: Trigger an automated email sequence from Mailchimp when a lead reaches a certain stage in ClinchPad. This ensures consistent engagement with your prospects, and by using Pipedream to connect ClinchPad with Mailchimp, you can nurture leads effectively without manual intervention. diff --git a/components/cliniko/README.md b/components/cliniko/README.md index 9cba2726de64d..3cdfde76c22b2 100644 --- a/components/cliniko/README.md +++ b/components/cliniko/README.md @@ -1,12 +1,11 @@ # Overview -The Cliniko API allows developers to access content and features from the -Cliniko platform. Using the API, developers can build applications that can do -the following: - -- View and search for patient records -- View and search for appointments -- Manage patient files -- Send SMS and email messages -- Generate clinical reports -- And more! +The Cliniko API opens a gateway to healthcare practice management by allowing you to interact with patient data, appointments, treatments, and more. With Pipedream, you can automate mundane tasks, ensuring that patient care remains front and center. Think of syncing appointment data with other calendars, sending personalized follow-up emails, or even managing inventory based on treatment records, all streamlined on a serverless platform. + +# Example Use Cases + +- **Automated Patient Follow-up**: After an appointment is completed in Cliniko, trigger a Pipedream workflow that sends a follow-up email through SendGrid, thanking the patient and providing care instructions or a feedback survey link. + +- **Appointment Syncing with Google Calendar**: Use Pipedream to watch for new or updated appointments in Cliniko and automatically mirror these changes in a specified Google Calendar, ensuring seamless scheduling visibility across different platforms. + +- **Inventory Management Post-Treatment**: Configure a Pipedream workflow to update inventory levels in a tool like Airtable or Google Sheets when a treatment is recorded in Cliniko, which helps in maintaining adequate stock levels for medical supplies. diff --git a/components/cliniko/actions/get-patient/get-patient.js b/components/cliniko/actions/get-patient/get-patient.js deleted file mode 100644 index f3d309e8747d4..0000000000000 --- a/components/cliniko/actions/get-patient/get-patient.js +++ /dev/null @@ -1,21 +0,0 @@ -const cliniko = require("../../cliniko.app.js"); - -module.exports = { - name: "Get Patient", - key: "cliniko-get-patient", - description: "Get the details of a patient by patient ID.", - version: "0.0.1", - type: "action", - props: { - cliniko, - patientId: { - propDefinition: [ - cliniko, - "patientId", - ], - }, - }, - async run() { - return await this.cliniko.getPatient(this.patientId); - }, -}; diff --git a/components/cliniko/actions/get-patient/get-patient.mjs b/components/cliniko/actions/get-patient/get-patient.mjs new file mode 100644 index 0000000000000..28ea6b8198064 --- /dev/null +++ b/components/cliniko/actions/get-patient/get-patient.mjs @@ -0,0 +1,48 @@ +import app from "../../cliniko.app.mjs"; + +export default { + name: "Get Patient", + key: "cliniko-get-patient", + description: "Get the details of a patient by patient ID.", + version: "0.1.0", + type: "action", + props: { + app, + patientId: { + propDefinition: [ + app, + "patientId", + ], + }, + }, + methods: { + /** + * Get the details of a specified patient. + * @params {Integer} patientId - The unique identifier of the patient + * @returns {Object} The details of the specified patient. + */ + getPatient({ + patientId, ...args + } = {}) { + return this.app.makeRequest({ + path: `/patients/${patientId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + getPatient, + patientId, + } = this; + + const response = await getPatient({ + $, + patientId, + }); + + $.export("$summary", `Suceesfully retrieved patient with ID: \`${response.id}\``); + + return response; + }, +}; diff --git a/components/cliniko/cliniko.app.js b/components/cliniko/cliniko.app.js deleted file mode 100644 index 82c0f43c1bce4..0000000000000 --- a/components/cliniko/cliniko.app.js +++ /dev/null @@ -1,63 +0,0 @@ -const { axios } = require("@pipedreamhq/platform"); - -module.exports = { - type: "app", - app: "cliniko", - propDefinitions: { - patientId: { - type: "integer", - label: "Patient ID", - description: "Enter a unique patient ID.", - }, - }, - methods: { - /** - * Get the Cliniko API key for the authenticated user. - * @returns {String} The Cliniko API key. - */ - _apiKey() { - return this.$auth.api_key; - }, - _shard() { - return this.$auth.shard; - }, - _baseApiUrl() { - return `https://api.${this._shard()}.cliniko.com/v1`; - }, - _makeRequestConfig(url) { - const auth = { - username: this._apiKey(), - password: "", // No password needed. - }; - // Cliniko requires contact email address in User-Agent header. - // See https://github.com/redguava/cliniko-api#identifying-your-application. - const headers = { - "Accept": "application/json", - "User-Agent": "Pipedream (support@pipedream.com)", - }; - return { - url, - headers, - auth, - }; - }, - _patientsApiUrl(id = undefined) { - const baseUrl = this._baseApiUrl(); - const basePath = "/patients"; - const path = id - ? `${basePath}/${id}` - : basePath; - return `${baseUrl}${path}`; - }, - /** - * Get the details of a specified patient. - * @params {Integer} patientId - The unique identifier of the patient - * @returns {Object} The details of the specified patient. - */ - async getPatient(patientId) { - const apiUrl = this._patientsApiUrl(patientId); - const requestConfig = this._makeRequestConfig(apiUrl); - return await axios(this, requestConfig); - }, - }, -}; diff --git a/components/cliniko/cliniko.app.mjs b/components/cliniko/cliniko.app.mjs new file mode 100644 index 0000000000000..485c728f960ed --- /dev/null +++ b/components/cliniko/cliniko.app.mjs @@ -0,0 +1,106 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "cliniko", + propDefinitions: { + patientId: { + type: "string", + label: "Patient ID", + description: "Enter a unique patient ID.", + async options({ + page, prevContext: { hasNext }, + }) { + if (hasNext === false) { + return []; + } + const { + links: { next }, + patients, + } = await this.listPatients({ + params: { + page: page + 1, + sort: "created_at:desc", + }, + }); + const options = patients.map(({ + id: value, first_name: firstName, last_name: lastName, email, + }) => ({ + label: [ + firstName, + lastName, + email, + ].join(" ").trim(), + value, + })); + return { + options, + context: { + hasNext: !!next, + }, + }; + }, + }, + }, + methods: { + /** + * Get the Cliniko API key for the authenticated user. + * @returns {String} The Cliniko API key. + */ + _apiKey() { + return this.$auth.api_key; + }, + _shard() { + return this.$auth.shard; + }, + getUrl(path) { + const baseUrl = constants.BASE_URL.replace(constants.SHARD_PLACEHOLDER, this._shard()); + return `${baseUrl}${constants.VERSION_PATH}${path}`; + }, + getAuth() { + return { + username: this._apiKey(), + password: "", + }; + }, + getHeaders(headers) { + // Cliniko requires contact email address in User-Agent header. + // See https://github.com/redguava/cliniko-api#identifying-your-application. + return { + ...headers, + "Accept": "application/json", + "User-Agent": "Pipedream (support@pipedream.com)", + }; + }, + makeRequest({ + $ = this, path, headers, ...args + }) { + const config = { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + auth: this.getAuth(), + }; + return axios($, config); + }, + listPatients(args = {}) { + return this.makeRequest({ + path: "/patients", + ...args, + }); + }, + listContacts(args = {}) { + return this.makeRequest({ + path: "/contacts", + ...args, + }); + }, + listBookings(args = {}) { + return this.makeRequest({ + path: "/bookings", + ...args, + }); + }, + }, +}; diff --git a/components/cliniko/common/constants.mjs b/components/cliniko/common/constants.mjs new file mode 100644 index 0000000000000..26b68d19d7d9c --- /dev/null +++ b/components/cliniko/common/constants.mjs @@ -0,0 +1,11 @@ +const SHARD_PLACEHOLDER = "{shard}"; +const BASE_URL = `https://api.${SHARD_PLACEHOLDER}.cliniko.com`; +const VERSION_PATH = "/v1"; +const DEFAULT_LIMIT = 100; + +export default { + BASE_URL, + VERSION_PATH, + SHARD_PLACEHOLDER, + DEFAULT_LIMIT, +}; diff --git a/components/cliniko/package.json b/components/cliniko/package.json index c7c23776a7040..b00086ad1df45 100644 --- a/components/cliniko/package.json +++ b/components/cliniko/package.json @@ -1,8 +1,8 @@ { "name": "@pipedream/cliniko", - "version": "0.0.3", + "version": "0.1.0", "description": "Pipedream Cliniko Components", - "main": "cliniko.app.js", + "main": "cliniko.app.mjs", "keywords": [ "pipedream", "cliniko" @@ -10,7 +10,7 @@ "homepage": "https://pipedream.com/apps/cliniko", "author": "Adrian Edwards (https://ageuphealth.com.au/)", "dependencies": { - "@pipedreamhq/platform": "^0.8.1" + "@pipedream/platform": "^1.6.2" }, "publishConfig": { "access": "public" diff --git a/components/cliniko/sources/common/polling.mjs b/components/cliniko/sources/common/polling.mjs new file mode 100644 index 0000000000000..cbe4da531c3ba --- /dev/null +++ b/components/cliniko/sources/common/polling.mjs @@ -0,0 +1,64 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../cliniko.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getResourceName() { + throw new ConfigurationError("getResourceName is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + return { + debug: true, + params: { + per_page: constants.DEFAULT_LIMIT, + sort: "created_at:desc", + }, + }; + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + processResources(resources) { + Array.from(resources) + .reverse() + .forEach(this.processResource); + }, + }, + async run() { + const { + getResourcesFn, + getResourcesFnArgs, + getResourceName, + processResources, + } = this; + const resourcesFn = getResourcesFn(); + + const { [getResourceName()]: resources } = + await resourcesFn(getResourcesFnArgs()); + + processResources(resources); + }, +}; diff --git a/components/cliniko/sources/new-booking-created/new-booking-created.mjs b/components/cliniko/sources/new-booking-created/new-booking-created.mjs new file mode 100644 index 0000000000000..4a29d7f54b16b --- /dev/null +++ b/components/cliniko/sources/new-booking-created/new-booking-created.mjs @@ -0,0 +1,29 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "cliniko-new-booking-created", + name: "New Booking Created", + description: "Emit new event when a booking is created in Cliniko. [See the documentation]()", + type: "source", + version: "0.0.1", + dedupe: "unique", + methods: { + ...common.methods, + getResourceName() { + return "bookings"; + }, + getResourcesFn() { + return this.app.listBookings; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Booking: ${resource.id}`, + ts: Date.parse(resource.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/cliniko/sources/new-booking-created/test-event.mjs b/components/cliniko/sources/new-booking-created/test-event.mjs new file mode 100644 index 0000000000000..5094db04114ba --- /dev/null +++ b/components/cliniko/sources/new-booking-created/test-event.mjs @@ -0,0 +1,60 @@ +export default { + "id": "1390726237298631770", + "created_at": "2024-04-02T20:04:46Z", + "updated_at": "2024-04-02T20:04:46Z", + "booking_ip_address": null, + "cancellation_note": null, + "cancellation_reason": null, + "cancellation_reason_description": "", + "conflicts": { + "links": { + "self": "https://api.au4.cliniko.com/v1/individual_appointments/1390726237298631770/conflicts" + } + }, + "did_not_arrive": false, + "email_reminder_sent": false, + "notes": null, + "online_booking_policy_accepted": null, + "patient_arrived": false, + "has_patient_appointment_notes": false, + "patient_name": "Test 1 Test 1", + "repeat_rule": {}, + "repeats": null, + "sms_reminder_sent": false, + "telehealth_url": "https://my-user.au4.cliniko.com/appointments/1390726237298631770/connect", + "treatment_note_status": null, + "archived_at": null, + "cancelled_at": null, + "deleted_at": null, + "ends_at": "2024-04-02T15:30:00Z", + "starts_at": "2024-04-02T14:45:00Z", + "invoice_status": null, + "appointment_type": { + "links": { + "self": "https://api.au4.cliniko.com/v1/appointment_types/1390053865100944805" + } + }, + "business": { + "links": { + "self": "https://api.au4.cliniko.com/v1/businesses/1390053865344214699" + } + }, + "practitioner": { + "links": { + "self": "https://api.au4.cliniko.com/v1/practitioners/1390053863163176768" + } + }, + "patient": { + "links": { + "self": "https://api.au4.cliniko.com/v1/patients/1390710863102485699" + } + }, + "attendees": { + "links": { + "self": "https://api.au4.cliniko.com/v1/individual_appointments/1390726237298631770/attendees" + } + }, + "links": { + "self": "https://api.au4.cliniko.com/v1/bookings/1390726237298631770" + } +}; diff --git a/components/cliniko/sources/new-contact-created/new-contact-created.mjs b/components/cliniko/sources/new-contact-created/new-contact-created.mjs new file mode 100644 index 0000000000000..ab411d71635cb --- /dev/null +++ b/components/cliniko/sources/new-contact-created/new-contact-created.mjs @@ -0,0 +1,29 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "cliniko-new-contact-created", + name: "New Contact Created", + description: "Emit new event when a contact is created in Cliniko. [See the documentation]()", + type: "source", + version: "0.0.1", + dedupe: "unique", + methods: { + ...common.methods, + getResourceName() { + return "contacts"; + }, + getResourcesFn() { + return this.app.listContacts; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Contact: ${resource.id}`, + ts: Date.parse(resource.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/cliniko/sources/new-contact-created/test-event.mjs b/components/cliniko/sources/new-contact-created/test-event.mjs new file mode 100644 index 0000000000000..bdb00a6c5f5d6 --- /dev/null +++ b/components/cliniko/sources/new-contact-created/test-event.mjs @@ -0,0 +1,31 @@ +export default { + "id": "1390712996921091568", + "created_at": "2024-04-02T19:38:28Z", + "updated_at": "2024-04-02T19:38:28Z", + "address_1": "", + "address_2": "", + "address_3": "", + "city": "", + "company_name": "", + "country_code": "US", + "doctor_type": "", + "email": "", + "first_name": "Test 1", + "last_name": "", + "notes": "", + "occupation": "", + "phone_numbers": [], + "post_code": "", + "preferred_name": "", + "provider_number": "", + "state": "", + "title": "", + "archived_at": null, + "country": "United States", + "deleted_at": null, + "type": "Standard", + "type_code": 0, + "links": { + "self": "https://api.au4.cliniko.com/v1/contacts/1390712996921091568" + } +}; diff --git a/components/clio/README.md b/components/clio/README.md index da9a0a1ab763c..d34f81317f394 100644 --- a/components/clio/README.md +++ b/components/clio/README.md @@ -1,12 +1,11 @@ # Overview -The Clio API allows developers to access and integrate the functionality of -Clio with other applications. +The Clio API offers a powerful way for law firms and legal professionals to automate and streamline their operations. By integrating with Pipedream, users can create custom workflows that leverage Clio's capabilities such as managing cases, tracking time, billing, and client communications. With Pipedream’s serverless platform, these automations can be triggered by a vast array of events and can connect Clio with other apps and services to enhance productivity and data consistency. -Some example applications that can be built using the Clio API include: +# Example Use Cases -- A tool for generating custom reports from Clio data -- A data Migration tool for importing data into Clio from other systems -- A custom interface for managing Clio data -- An application for managing Clio billing and invoicing -- A mobile app for accessing Clio data on the go +- **Time Tracking Automation**: Automatically log billable hours in Clio when a timer is stopped in a time tracking app like Toggl. This workflow could check for new time entries in Toggl and create corresponding time entries in Clio, ensuring accurate and up-to-date billing records. + +- **New Client Onboarding**: Streamline the process of adding new clients to Clio by triggering an automation when a new client form is submitted through a service like Typeform. The workflow can create a new matter in Clio, generate a welcome email via Gmail or SendGrid, and schedule an initial consultation in Google Calendar. + +- **Document Management Sync**: Keep all legal documents in sync between Clio and cloud storage services like Dropbox or Google Drive. Whenever a document is uploaded to a specific case folder in the cloud service, the workflow would create a link in the corresponding matter in Clio, keeping case-related documents organized and accessible. diff --git a/components/clio/actions/create-matter/create-matter.mjs b/components/clio/actions/create-matter/create-matter.mjs new file mode 100644 index 0000000000000..8f4cb5e1aa405 --- /dev/null +++ b/components/clio/actions/create-matter/create-matter.mjs @@ -0,0 +1,70 @@ +import app from "../../clio.app.mjs"; + +export default { + key: "clio-create-matter", + name: "Create New Matter", + description: "Creates a new matter in Clio. [See the documentation](https://docs.developers.clio.com/api-reference/#tag/Matters/operation/Matter#index)", + version: "0.0.1", + type: "action", + props: { + app, + clientId: { + propDefinition: [ + app, + "clientId", + ], + }, + description: { + propDefinition: [ + app, + "description", + ], + }, + closeDate: { + propDefinition: [ + app, + "closeDate", + ], + }, + clientReference: { + propDefinition: [ + app, + "clientReference", + ], + }, + }, + methods: { + createMatter(args = {}) { + return this.app.post({ + path: "/matters.json", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createMatter, + clientId, + description, + closeDate, + clientReference, + } = this; + + const response = await createMatter({ + $, + data: { + data: { + client: { + id: clientId, + }, + description, + close_date: closeDate, + client_reference: clientReference, + }, + }, + }); + + $.export("$summary", `Successfully created a new matter with ID \`${response.data.id}\``); + return response; + }, +}; diff --git a/components/clio/actions/create-task/create-task.mjs b/components/clio/actions/create-task/create-task.mjs new file mode 100644 index 0000000000000..201857727bfb5 --- /dev/null +++ b/components/clio/actions/create-task/create-task.mjs @@ -0,0 +1,128 @@ +import app from "../../clio.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "clio-create-task", + name: "Create New Task", + description: "Creates a new task in Clio. [See the documentation](https://docs.developers.clio.com/api-reference/#tag/Tasks/operation/Task#index)", + version: "0.0.1", + type: "action", + props: { + app, + description: { + propDefinition: [ + app, + "description", + ], + }, + name: { + type: "string", + label: "Task Name", + description: "The name of the task.", + }, + priority: { + type: "string", + label: "Priority", + description: "The priority of the task.", + optional: true, + options: [ + "High", + "Normal", + "Low", + ], + }, + assigneeType: { + type: "string", + label: "Assignee Type", + description: "Model type of the assignee.", + options: Object.values(constants.ASSIGNEE_TYPE), + reloadProps: true, + }, + }, + methods: { + getListResourcesFn() { + const { + app, + assigneeType, + } = this; + return assigneeType === constants.ASSIGNEE_TYPE.CONTACT + ? app.listContacts + : app.listUsers; + }, + createTask(args = {}) { + return this.app.post({ + path: "/tasks.json", + ...args, + }); + }, + }, + additionalProps() { + return { + assigneeId: { + type: "string", + label: "Assignee ID", + description: "ID of the assignee.", + options: async ({ + prevContext: { params }, + mapper = ({ + id: value, name: label, + }) => ({ + value, + label, + }), + }) => { + const listResourcesFn = this.getListResourcesFn(); + if (params === null) { + return []; + } + const { + data, + meta: { paging }, + } = await listResourcesFn({ + params, + }); + return { + options: data.map(mapper), + context: { + params: paging?.next?.includes("?") + ? Object.fromEntries( + new URLSearchParams(paging.next.split("?")[1]), + ) + : null, + }, + }; + }, + }, + }; + }, + async run({ $ }) { + const { + createTask, + assigneeType, + assigneeId, + description, + name, + priority, + } = this; + + const response = await createTask({ + $, + data: { + data: { + name, + description, + priority, + ...(assigneeType && assigneeId && { + assignee: { + id: assigneeId, + type: assigneeType, + }, + }), + }, + }, + }); + + $.export("$summary", `Successfully created a new task with ID \`${response.data.id}\``); + return response; + }, +}; diff --git a/components/clio/actions/update-matter/update-matter.mjs b/components/clio/actions/update-matter/update-matter.mjs new file mode 100644 index 0000000000000..b3456d423f10e --- /dev/null +++ b/components/clio/actions/update-matter/update-matter.mjs @@ -0,0 +1,85 @@ +import app from "../../clio.app.mjs"; + +export default { + key: "clio-update-matter", + name: "Update Matter", + description: "Updates an existing matter in Clio. [See the documentation](https://docs.developers.clio.com/api-reference/#tag/Matters/operation/Matter#update)", + version: "0.0.1", + type: "action", + props: { + app, + matterId: { + propDefinition: [ + app, + "matterId", + ], + }, + clientId: { + optional: true, + propDefinition: [ + app, + "clientId", + ], + }, + description: { + optional: true, + propDefinition: [ + app, + "description", + ], + }, + closeDate: { + optional: true, + propDefinition: [ + app, + "closeDate", + ], + }, + clientReference: { + propDefinition: [ + app, + "clientReference", + ], + }, + }, + methods: { + updateMatter({ + matterId, ...args + } = {}) { + return this.app.patch({ + path: `/matters/${matterId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + updateMatter, + matterId, + clientId, + description, + closeDate, + clientReference, + } = this; + + const response = await updateMatter({ + $, + matterId, + data: { + data: { + ...(clientId && { + client: { + id: clientId, + }, + }), + description, + close_date: closeDate, + client_reference: clientReference, + }, + }, + }); + + $.export("$summary", `Successfully updated matter with ID \`${response.data.id}\``); + return response; + }, +}; diff --git a/components/clio/clio.app.mjs b/components/clio/clio.app.mjs index 8881cd50ed72d..171cc171b111b 100644 --- a/components/clio/clio.app.mjs +++ b/components/clio/clio.app.mjs @@ -1,11 +1,162 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "clio", - propDefinitions: {}, + propDefinitions: { + clientId: { + type: "string", + label: "Client ID", + description: "The unique identifier for a single Contact associated with the Matter. ", + async options({ + prevContext: { nextParams }, + mapper = ({ + id: value, name: label, + }) => ({ + value, + label, + }), + }) { + if (nextParams === null) { + return []; + } + const { + data, + meta: { paging }, + } = await this.listContacts({ + params: { + ...nextParams, + limit: constants.DEFAULT_LIMIT, + }, + }); + return { + options: data.map(mapper), + context: { + nextParams: paging?.next?.includes("?") + ? Object.fromEntries( + new URLSearchParams(paging.next.split("?")[1]), + ) + : null, + }, + }; + }, + }, + description: { + type: "string", + label: "Description", + description: "A description of the matter or task.", + }, + closeDate: { + type: "string", + label: "Close Date", + description: "Date the Matter was set to closed. (Expects an ISO-8601 date). Eg. `2021-01-01`", + optional: true, + }, + clientReference: { + type: "string", + label: "Client Reference", + description: "Client Reference string for external uses.", + optional: true, + }, + matterId: { + type: "string", + label: "Matter ID", + description: "The unique identifier for the matter.", + async options({ + prevContext: { nextParams }, + mapper = ({ + id: value, display_number: label, + }) => ({ + value, + label, + }), + }) { + if (nextParams === null) { + return []; + } + const { + data, + meta: { paging }, + } = await this.listMatters({ + params: { + ...nextParams, + limit: constants.DEFAULT_LIMIT, + }, + }); + return { + options: data.map(mapper), + context: { + nextParams: paging?.next?.includes("?") + ? Object.fromEntries( + new URLSearchParams(paging.next.split("?")[1]), + ) + : null, + }, + }; + }, + }, + fields: { + type: "string[]", + label: "Fields", + description: "Fields to be included in the Webhook request.", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Content-Type": "application/json", + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "post", + ...args, + }); + }, + patch(args = {}) { + return this._makeRequest({ + method: "patch", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "delete", + ...args, + }); + }, + listContacts(args = {}) { + return this._makeRequest({ + path: "/contacts.json", + ...args, + }); + }, + listMatters(args = {}) { + return this._makeRequest({ + path: "/matters.json", + ...args, + }); + }, + listUsers(args = {}) { + return this._makeRequest({ + path: "/users.json", + ...args, + }); }, }, }; diff --git a/components/clio/common/constants.mjs b/components/clio/common/constants.mjs new file mode 100644 index 0000000000000..339b933238264 --- /dev/null +++ b/components/clio/common/constants.mjs @@ -0,0 +1,29 @@ +const BASE_URL = "https://app.clio.com"; +const VERSION_PATH = "/api/v4"; +const LAST_CREATED_AT = "lastCreatedAt"; +const DEFAULT_MAX = 600; +const DEFAULT_LIMIT = 100; +const WEBHOOK_ID = "webhookId"; +const SECRET = "secret"; +const SECRET_HEADER = "x-hook-secret"; +const SIGNATURE_HEADER = "x-hook-signature"; +const BILLS = "bills"; + +const ASSIGNEE_TYPE = { + USER: "User", + CONTACT: "Contact", +}; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_MAX, + LAST_CREATED_AT, + DEFAULT_LIMIT, + ASSIGNEE_TYPE, + WEBHOOK_ID, + SECRET, + SECRET_HEADER, + SIGNATURE_HEADER, + BILLS, +}; diff --git a/components/clio/package.json b/components/clio/package.json new file mode 100644 index 0000000000000..a2e08b6a62e39 --- /dev/null +++ b/components/clio/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/clio", + "version": "0.1.1", + "description": "Pipedream Clio Components", + "main": "clio.app.mjs", + "keywords": [ + "pipedream", + "clio" + ], + "homepage": "https://pipedream.com/apps/clio", + "author": "Pipedream (https://pipedream.com/)", + "dependencies": { + "@pipedream/platform": "^1.6.4", + "crypto": "^1.0.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/components/clio/sources/bill-state-updated-instant/bill-state-updated-instant.mjs b/components/clio/sources/bill-state-updated-instant/bill-state-updated-instant.mjs new file mode 100644 index 0000000000000..1296e7e72af9f --- /dev/null +++ b/components/clio/sources/bill-state-updated-instant/bill-state-updated-instant.mjs @@ -0,0 +1,71 @@ +import common from "../common/webhook.mjs"; +import event from "../common/event.mjs"; +import model from "../common/model.mjs"; +import fields from "../common/bill-fields.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + ...common, + key: "clio-bill-state-updated-instant", + name: "Bill State Updated (Instant)", + description: "Emit new event when the state of a bill has changed in Clio. [See the documentation](https://docs.developers.clio.com/api-reference/#tag/Webhooks/operation/Webhook#index)", + version: "0.0.3", + type: "source", + dedupe: "unique", + props: { + ...common.props, + fields: { + propDefinition: [ + common.props.app, + "fields", + ], + options: fields, + }, + }, + methods: { + ...common.methods, + setBills(value) { + this.db.set(constants.BILLS, value); + }, + getBills() { + return this.db.get(constants.BILLS) || {}; + }, + getRelevantFields() { + return [ + "state", + ]; + }, + getModel() { + return model.BILL; + }, + getEvents() { + return [ + event.UPDATED, + ]; + }, + isRelevant(body) { + const resource = body.data; + const bills = this.getBills(); + + if (bills[resource.id] === resource.state) { + console.log("Bill state is the same, not relevant!"); + return false; + } + + this.setBills({ + ...bills, + [resource.id]: resource.state, + }); + return true; + }, + generateMeta(body) { + const resource = body.data; + const ts = Date.parse(resource.updated_at); + return { + id: `${resource.id}-${ts}`, + summary: `Bill State Updated: ${resource.id}`, + ts, + }; + }, + }, +}; diff --git a/components/clio/sources/common/activity-fields.mjs b/components/clio/sources/common/activity-fields.mjs new file mode 100644 index 0000000000000..8f435d0297522 --- /dev/null +++ b/components/clio/sources/common/activity-fields.mjs @@ -0,0 +1,41 @@ +export default [ + "id", + "etag", + "type", + "date", + "quantity_in_hours", + "rounded_quantity_in_hours", + "quantity", + "rounded_quantity", + "quantity_redacted", + "price", + "note", + "flat_rate", + "billed", + "on_bill", + "total", + "contingency_fee", + "reference", + "non_billable", + "non_billable_total", + "no_charge", + "tax_setting", + "activity_description", + "expense_category", + "bill", + "communication", + "client_portal", + "matter", + "matter_note", + "contact_note", + "subject", + "timer", + "user", + "utbms_expense", + "vendor", + "calendar_entry", + "task", + "text_message_conversation", + "document_version", + "legal_aid_uk_activity", +]; diff --git a/components/clio/sources/common/bill-fields.mjs b/components/clio/sources/common/bill-fields.mjs new file mode 100644 index 0000000000000..40b2bd2fb9af2 --- /dev/null +++ b/components/clio/sources/common/bill-fields.mjs @@ -0,0 +1,55 @@ +export default [ + "id", + "etag", + "number", + "issued_at", + "due_at", + "tax_rate", + "secondary_tax_rate", + "subject", + "purchase_order", + "type", + "memo", + "start_at", + "end_at", + "balance", + "state", + "kind", + "total", + "paid", + "paid_at", + "pending", + "due", + "discount_services_only", + "can_update", + "credits_issued", + "shared", + "last_sent_at", + "services_secondary_tax", + "services_sub_total", + "services_tax", + "services_taxable_sub_total", + "services_secondary_taxable_sub_total", + "taxable_sub_total", + "secondary_taxable_sub_total", + "sub_total", + "tax_sum", + "secondary_tax_sum", + "total_tax", + "available_state_transitions", + "user", + "client", + "discount", + "interest", + "matters", + "group", + "bill_theme", + "original_bill", + "destination_account", + "balances", + "matter_totals", + "currency", + "billing_setting", + "client_addresses", + "legal_aid_uk_bill", +]; diff --git a/components/clio/sources/common/document-fields.mjs b/components/clio/sources/common/document-fields.mjs new file mode 100644 index 0000000000000..11dea549a391c --- /dev/null +++ b/components/clio/sources/common/document-fields.mjs @@ -0,0 +1,21 @@ +export default [ + "id", + "etag", + "deleted_at", + "type", + "locked", + "name", + "received_at", + "filename", + "size", + "content_type", + "parent", + "matter", + "contact", + "document_category", + "creator", + "latest_document_version", + "group", + "external_properties", + "document_versions", +]; diff --git a/components/clio/sources/common/event.mjs b/components/clio/sources/common/event.mjs new file mode 100644 index 0000000000000..de7b3d4479a81 --- /dev/null +++ b/components/clio/sources/common/event.mjs @@ -0,0 +1,8 @@ +export default { + CREATED: "created", + UPDATED: "updated", + DELETED: "deleted", + MATTER_OPENED: "matter_opened", + MATTER_PENDED: "matter_pended", + MATTER_CLOSED: "matter_closed", +}; diff --git a/components/clio/sources/common/model.mjs b/components/clio/sources/common/model.mjs new file mode 100644 index 0000000000000..505ae96730357 --- /dev/null +++ b/components/clio/sources/common/model.mjs @@ -0,0 +1,11 @@ +export default { + MATTER: "matter", + ACTIVITY: "activity", + BILL: "bill", + CALENDAR_ENTRY: "calendar_entry", + COMMUNICATION: "communication", + CONTACT: "contact", + TASK: "task", + DOCUMENT: "document", + FOLDER: "folder", +}; diff --git a/components/clio/sources/common/webhook.mjs b/components/clio/sources/common/webhook.mjs new file mode 100644 index 0000000000000..2b2c2e21a15ec --- /dev/null +++ b/components/clio/sources/common/webhook.mjs @@ -0,0 +1,159 @@ +import crypto from "crypto"; +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../clio.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + }, + hooks: { + async activate() { + const { + http: { endpoint: url }, + createWebhook, + setWebhookId, + getEvents, + getModel, + fields, + getRelevantFields, + } = this; + + const fieldsSet = new Set( + [ + "created_at", + "updated_at", + ].concat(fields || []) + .concat(getRelevantFields()), + ); + + const response = + await createWebhook({ + data: { + data: { + url, + events: getEvents(), + model: getModel(), + fields: Array.from(fieldsSet).join(","), + }, + }, + }); + + setWebhookId(response.data.id); + }, + async deactivate() { + const { + getWebhookId, + deleteWebhook, + } = this; + + const webhookId = getWebhookId(); + if (webhookId) { + await deleteWebhook({ + webhookId, + }); + } + }, + }, + methods: { + setWebhookId(value) { + this.db.set(constants.WEBHOOK_ID, value); + }, + getWebhookId() { + return this.db.get(constants.WEBHOOK_ID); + }, + setSecret(value) { + this.db.set(constants.SECRET, value); + }, + getSecret() { + return this.db.get(constants.SECRET); + }, + getRelevantFields() { + return []; + }, + isRelevant() { + return true; + }, + isSignatureValid(signature, rawBody) { + const secret = this.getSecret(); + const digest = + crypto + .createHmac("sha256", secret) + .update(rawBody) + .digest("hex"); + return digest === signature; + }, + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getEvents() { + throw new ConfigurationError("getEvents is not implemented"); + }, + getModel() { + throw new ConfigurationError("getModel is not implemented"); + }, + processResource(resource) { + if (!this.isRelevant(resource)) { + return console.log("Ignoring resource!", resource); + } + this.$emit(resource, this.generateMeta(resource)); + }, + createWebhook(args = {}) { + return this.app.post({ + debug: true, + path: "/webhooks.json", + ...args, + }); + }, + deleteWebhook({ + webhookId, ...args + } = {}) { + return this.app.delete({ + debug: true, + path: `/webhooks/${webhookId}.json`, + ...args, + }); + }, + }, + async run({ + body, bodyRaw, headers: { + [constants.SECRET_HEADER]: secret, + [constants.SIGNATURE_HEADER]: signature, + }, + }) { + const { + http: { respond }, + getSecret, + setSecret, + processResource, + isSignatureValid, + } = this; + + const currentSecret = getSecret(); + if (!currentSecret) { + console.log("First handshake. Storing secret!"); + setSecret(secret); + return respond({ + status: 200, + body: "OK", + headers: { + [constants.SECRET_HEADER]: secret, + }, + }); + } + + if (signature && !isSignatureValid(signature, bodyRaw)) { + console.log("Invalid signature!"); + return respond({ + status: 401, + }); + } + + processResource(body); + }, +}; diff --git a/components/clio/sources/new-activity-instant/new-activity-instant.mjs b/components/clio/sources/new-activity-instant/new-activity-instant.mjs new file mode 100644 index 0000000000000..18faacc756895 --- /dev/null +++ b/components/clio/sources/new-activity-instant/new-activity-instant.mjs @@ -0,0 +1,43 @@ +import common from "../common/webhook.mjs"; +import event from "../common/event.mjs"; +import model from "../common/model.mjs"; +import fields from "../common/activity-fields.mjs"; + +export default { + ...common, + key: "clio-new-activity-instant", + name: "New Activity (Instant)", + description: "Emit new event when a new activity is created in Clio. [See the documentation](https://docs.developers.clio.com/api-reference/#tag/Webhooks/operation/Webhook#index)", + version: "0.0.2", + type: "source", + dedupe: "unique", + props: { + ...common.props, + fields: { + propDefinition: [ + common.props.app, + "fields", + ], + options: fields, + }, + }, + methods: { + ...common.methods, + getModel() { + return model.ACTIVITY; + }, + getEvents() { + return [ + event.CREATED, + ]; + }, + generateMeta(body) { + const resource = body.data; + return { + id: resource.id, + summary: `New Activity: ${resource.id}`, + ts: Date.parse(resource.created_at), + }; + }, + }, +}; diff --git a/components/clio/sources/new-document-instant/new-document-instant.mjs b/components/clio/sources/new-document-instant/new-document-instant.mjs new file mode 100644 index 0000000000000..6396d1f1c4ea4 --- /dev/null +++ b/components/clio/sources/new-document-instant/new-document-instant.mjs @@ -0,0 +1,43 @@ +import common from "../common/webhook.mjs"; +import event from "../common/event.mjs"; +import model from "../common/model.mjs"; +import fields from "../common/document-fields.mjs"; + +export default { + ...common, + key: "clio-new-document-instant", + name: "New Document (Instant)", + description: "Emit new event when a new document is created. [See the documentation](https://docs.developers.clio.com/api-reference/#tag/Webhooks/operation/Webhook#index)", + version: "0.0.3", + type: "source", + dedupe: "unique", + props: { + ...common.props, + fields: { + propDefinition: [ + common.props.app, + "fields", + ], + options: fields, + }, + }, + methods: { + ...common.methods, + getModel() { + return model.DOCUMENT; + }, + getEvents() { + return [ + event.CREATED, + ]; + }, + generateMeta(body) { + const resource = body.data; + return { + id: resource.id, + summary: `New Document: ${resource.id}`, + ts: Date.parse(resource.created_at), + }; + }, + }, +}; diff --git a/components/clockify/README.md b/components/clockify/README.md index 2da6e2f339001..180d79cdfae34 100644 --- a/components/clockify/README.md +++ b/components/clockify/README.md @@ -1,14 +1,11 @@ # Overview -With the Clockify API, you can build applications that: +Clockify is a time tracking and timesheet app that provides a straightforward way to track work hours across projects. With the Clockify API on Pipedream, you can automate time tracking, create detailed reports, manage projects, and sync timesheet data with other apps. Using Pipedream, you can create workflows that trigger on events in Clockify or other apps, process the data, and perform actions like logging time automatically, sending notifications, or creating invoices based on tracked time. -- Display data from your Clockify account -- Update data in your Clockify account -- Perform actions in your Clockify account (such as starting and stopping - timers) +# Example Use Cases -Here are some examples of what you can build with the Clockify API: +- **Automatic Time Tracking for Meetings**: Trigger a workflow whenever a Google Calendar event starts, automatically starting a time entry in Clockify. When the event ends, stop the time entry. This ensures precise tracking of time spent in meetings without manual input. -- A dashboard that displays yourClockify data -- An application that starts and stops timers in your Clockify account -- A tool that helps you track your time spent on different projects +- **Project Management Integration**: On task completion in Trello, Jira, or Asana, log a time entry in Clockify corresponding to the task. Additionally, when a new project is created in your project management tool, create a matching project in Clockify to keep both systems in sync. + +- **Invoice Generation and Notifications**: After a time entry is completed in Clockify, calculate the billable amount based on the hourly rate and generate an invoice using a service like Stripe or PayPal. Then, send a notification with invoice details to Slack or email to keep the team or client updated. diff --git a/components/clockwork_recruiting/README.md b/components/clockwork_recruiting/README.md new file mode 100644 index 0000000000000..7fbfff4ae82e4 --- /dev/null +++ b/components/clockwork_recruiting/README.md @@ -0,0 +1,11 @@ +# Overview + +The Clockwork Recruiting API offers a suite of features tailored for executive search firms and in-house recruiting teams, enabling users to manage candidates, clients, and search projects with ease. By integrating this API with Pipedream, you can automate the recruitment process, sync data between various platforms, and streamline candidate engagement and tracking. Pipedream's serverless platform facilitates the creation of custom workflows using the Clockwork Recruiting API to optimize recruitment operations without manual intervention. + +# Example Use Cases + +- **Automate Candidate Sourcing**: Automatically pull candidate data from Clockwork into a Google Sheets spreadsheet or an HR management system for further analysis and processing. Each new candidate could trigger a workflow that appends their details to a sheet or database for easy tracking and management. + +- **Synchronize Candidate Status Updates**: Keep a CRM like Salesforce in sync with Clockwork by updating candidate statuses in real-time. When a candidate's status changes in Clockwork, a Pipedream workflow can be triggered to update the corresponding record in Salesforce, ensuring all teams have the latest information. + +- **Streamline Interview Scheduling**: Integrate Clockwork with a calendar service like Google Calendar to automate interview scheduling. When a candidate reaches a certain stage in Clockwork, a Pipedream workflow can send calendar invites to the candidate and the interviewers, reducing the time spent on manual scheduling. diff --git a/components/close/README.md b/components/close/README.md index 130ba206c1667..837302a62f2be 100644 --- a/components/close/README.md +++ b/components/close/README.md @@ -1,14 +1,11 @@ # Overview -With the Close API, you can build a number of applications and integrations -that can automate your sales process and make your life easier. Here are some -examples: - -- An integration that automatically updates your CRM with information from - Close -- A custom sales dashboard that displays your team's sales pipeline and - performance -- A lead capture form thatcollects information from potential customers and - adds them to your Close account -- An automated email campaign that sends follow-up emails to your customers - after they purchase a product from you +The Close API provides access to a robust CRM platform, enabling users to automate sales processes, manage leads, and track communication history. Harnessing the Close API within Pipedream's ecosystem allows for the creation of custom, serverless workflows that can react in real-time to events, synchronize data across apps, and streamline sales operations. By leveraging Pipedream's capabilities, you can trigger actions based on Close events, manipulate Close data, and integrate with countless other services to amplify your CRM's functionality. + +# Example Use Cases + +- **Lead Scoring Automation**: Automatically score leads in Close based on interaction data from multiple touchpoints. For instance, increase a lead's score when they open an email, attend a webinar, or visit your pricing page, pushing high-scoring leads to your sales team for immediate action. + +- **Email Campaign Follow-up**: Create a workflow that listens for a status change on a lead in Close, triggering a personalized follow-up email sequence via apps like SendGrid or Mailchimp when a lead enters a specific status, ensuring timely engagement and nurturing of potential customers. + +- **Support Ticket Creation**: When a Close lead sends a distress signal, like a negative email reply or a support request, trigger a workflow that creates a support ticket in a tool like Zendesk or Jira Service Management, helping your support team prioritize and respond rapidly to critical issues. diff --git a/components/cloud_convert/README.md b/components/cloud_convert/README.md index ab049d26882a4..691b38583fe3c 100644 --- a/components/cloud_convert/README.md +++ b/components/cloud_convert/README.md @@ -1,11 +1,11 @@ # Overview -With Cloud Convert, you can easily convert between a wide range of file -formats. Here are just a few examples of what you can do: +The Cloud Convert API offers a robust solution for file conversion, supporting a vast array of file formats. With Pipedream, you can harness this versatility to create automated workflows that trigger file conversions, process the resulting files, and integrate with other services. By combining Cloud Convert with Pipedream's connectivity to hundreds of apps, you can craft custom automation that saves time and removes the friction from multi-format file management tasks. -- Convert an image from one format to another (e.g. PNG to JPG) -- Convert a video from one format to another (e.g. MP4 to AVI) -- Convert a document from one format to another (e.g. DOC to PDF) +# Example Use Cases -There are literally hundreds of different conversions that you can do with -Cloud Convert - The possibilities are endless! +- **Bulk Document Conversion**: Automatically convert a batch of documents from Google Drive to PDF whenever a new file is added to a specific folder. Utilize Pipedream's Google Drive integration to monitor the folder and trigger the Cloud Convert API to perform the conversion, then save the PDFs back to Google Drive. + +- **Image Optimization for Web**: Create a workflow where high-resolution images uploaded to Dropbox are automatically optimized for web use by converting them to a compressed format with Cloud Convert. After conversion, use Pipedream to upload the optimized images to a content delivery network like Amazon S3 for fast and efficient web serving. + +- **Audio/Video Transcoding for Social Media**: Set up a Pipedream workflow that takes newly recorded podcasts or videos from a platform like Zoom, transcodes them into formats suitable for social media platforms using Cloud Convert, and then posts the media to YouTube or SoundCloud. This streamlines the process of sharing content across various channels. diff --git a/components/cloud_convert/actions/convert-file/convert-file.mjs b/components/cloud_convert/actions/convert-file/convert-file.mjs new file mode 100644 index 0000000000000..6eb37eca59901 --- /dev/null +++ b/components/cloud_convert/actions/convert-file/convert-file.mjs @@ -0,0 +1,64 @@ +import app from "../../cloud_convert.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "cloud_convert-convert-file", + name: "Convert File", + description: "Converts an input file to a specified output format using CloudConvert. [See the documentation](https://cloudconvert.com/api/v2/convert#convert-tasks)", + version: "0.0.1", + type: "action", + props: { + app, + input: { + propDefinition: [ + app, + "taskId", + () => ({ + filters: { + ["filter[status]"]: constants.TASK_STATUS.FINISHED, + }, + }), + ], + }, + outputFormat: { + label: "Output Format", + description: "The output format of the file to be converted", + propDefinition: [ + app, + "conversionType", + () => ({ + mapper: ({ output_format: value }) => value, + filters: { + ["filter[operation]"]: constants.TASK_OPERATION.CONVERT, + }, + }), + ], + }, + }, + methods: { + convertFile(args = {}) { + return this.app.post({ + path: "/convert", + ...args, + }); + }, + }, + async run({ $ }) { + const { + convertFile, + input, + outputFormat, + } = this; + + const response = await convertFile({ + $, + data: { + input, + output_format: outputFormat, + }, + }); + + $.export("$summary", `Successfully converted file with ID \`${response.data.id}\``); + return response; + }, +}; diff --git a/components/cloud_convert/actions/create-archive/create-archive.mjs b/components/cloud_convert/actions/create-archive/create-archive.mjs new file mode 100644 index 0000000000000..e1e53337f2cd7 --- /dev/null +++ b/components/cloud_convert/actions/create-archive/create-archive.mjs @@ -0,0 +1,67 @@ +import app from "../../cloud_convert.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "cloud_convert-create-archive", + name: "Create Archive", + description: "Creates an archive in a specified format. [See the documentation](https://cloudconvert.com/api/v2/archive#archive-tasks)", + version: "0.0.1", + type: "action", + props: { + app, + input: { + type: "string[]", + label: "Files To Archive", + description: "The files to be included in the archive.", + propDefinition: [ + app, + "taskId", + () => ({ + filters: { + ["filter[status]"]: constants.TASK_STATUS.FINISHED, + }, + }), + ], + }, + outputFormat: { + label: "Output Format", + description: "The archive format.", + propDefinition: [ + app, + "conversionType", + () => ({ + mapper: ({ output_format: value }) => value, + filters: { + ["filter[operation]"]: constants.TASK_OPERATION.ARCHIVE, + }, + }), + ], + }, + }, + methods: { + createArchive(args = {}) { + return this.app.post({ + path: "/archive", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createArchive, + input, + outputFormat, + } = this; + + const response = await createArchive({ + $, + data: { + input, + output_format: outputFormat, + }, + }); + + $.export("$summary", `Successfully created archive with ID \`${response.data.id}\``); + return response; + }, +}; diff --git a/components/cloud_convert/actions/create-import-url-task/create-import-url-task.mjs b/components/cloud_convert/actions/create-import-url-task/create-import-url-task.mjs new file mode 100644 index 0000000000000..72b745663e041 --- /dev/null +++ b/components/cloud_convert/actions/create-import-url-task/create-import-url-task.mjs @@ -0,0 +1,57 @@ +import app from "../../cloud_convert.app.mjs"; + +export default { + key: "cloud_convert-create-import-url-task", + name: "Create Import URL Task", + description: "Creates a task to import a file from a URL. [See the documentation](https://cloudconvert.com/api/v2/import#import-url-tasks)", + version: "0.0.1", + type: "action", + props: { + app, + url: { + type: "string", + label: "URL", + description: "The URL of the file to import", + }, + filename: { + propDefinition: [ + app, + "filename", + ], + }, + headers: { + type: "object", + label: "Headers", + description: "Object of additional headers to send with the download request. Can be used to access URLs that require authorization.", + optional: true, + }, + }, + methods: { + createImportUrlTask(args = {}) { + return this.app.post({ + path: "/import/url", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createImportUrlTask, + url, + filename, + headers, + } = this; + + const response = await createImportUrlTask({ + $, + data: { + url, + filename, + headers, + }, + }); + + $.export("$summary", `Successfully created import URL task with ID \`${response.data.id}\``); + return response; + }, +}; diff --git a/components/cloud_convert/actions/create-merge-files-to-pdf-job/create-merge-files-to-pdf-job.mjs b/components/cloud_convert/actions/create-merge-files-to-pdf-job/create-merge-files-to-pdf-job.mjs new file mode 100644 index 0000000000000..4dd29ebe6643a --- /dev/null +++ b/components/cloud_convert/actions/create-merge-files-to-pdf-job/create-merge-files-to-pdf-job.mjs @@ -0,0 +1,97 @@ +import app from "../../cloud_convert.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "cloud_convert-create-merge-files-to-pdf-job", + name: "Create Merge Files To PDF Job", + description: "Combines multiple input files into a single PDF file and create an export URL with a job. [See the documentation](https://cloudconvert.com/api/v2/merge#merge-tasks)", + version: "0.0.1", + type: "action", + props: { + app, + urls: { + type: "string[]", + label: "PDF URLs", + description: "The URLs of the PDF files to merge.", + optional: true, + }, + filename: { + description: "Choose a filename (including extension) for the output file.", + propDefinition: [ + app, + "filename", + ], + }, + tag: { + type: "string", + label: "Tag", + description: "An arbitrary string to identify the job. Does not have any effect and can be used to associate the job with an ID in your application.", + optional: true, + }, + webhookUrl: { + type: "string", + label: "Webhook URL", + description: "A [webhook](https://cloudconvert.com/api/v2/webhooks) that is used for this single job only. The url will receive a job.finished or job.failed event. We do recommend using account wide webhooks which can be created via the dashboard.", + optional: true, + }, + }, + methods: { + getFileTasksByUrls(urls) { + return urls.reduce((tasks, url, idx) => { + const key = `file${idx + 1}`; + return { + ...tasks, + [key]: { + operation: constants.TASK_OPERATION.IMPORT_URL, + url, + filename: `${key}.pdf`, + }, + }; + }, {}); + }, + getMergeFilesToPdfTasks({ + urls, filename, + } = {}) { + const fileTasks = this.getFileTasksByUrls(urls); + return { + ...fileTasks, + merged: { + operation: constants.TASK_OPERATION.MERGE, + output_format: "pdf", + input: Object.keys(fileTasks), + filename, + }, + output: { + operation: constants.TASK_OPERATION.EXPORT_URL, + input: [ + "merged", + ], + inline: false, + archive_multiple_files: false, + }, + }; + }, + }, + async run({ $ }) { + const { + getMergeFilesToPdfTasks, + app, + tag, + webhookUrl, + ...props + } = this; + + const response = await app.createJob({ + $, + data: { + tasks: getMergeFilesToPdfTasks(props), + tag, + webhook_url: webhookUrl, + }, + }); + + $.export("$summary", `Successfully created job with ID \`${response.data.id}\``); + + return response; + }, +}; diff --git a/components/cloud_convert/actions/get-task/get-task.mjs b/components/cloud_convert/actions/get-task/get-task.mjs new file mode 100644 index 0000000000000..100da28130a51 --- /dev/null +++ b/components/cloud_convert/actions/get-task/get-task.mjs @@ -0,0 +1,55 @@ +import app from "../../cloud_convert.app.mjs"; + +export default { + key: "cloud_convert-get-task", + name: "Get Task", + description: "Retrieves a task by ID. [See the documentation](https://cloudconvert.com/api/v2/tasks#tasks-show)", + version: "0.0.1", + type: "action", + props: { + app, + taskId: { + description: "The ID of the task", + propDefinition: [ + app, + "taskId", + ], + }, + include: { + propDefinition: [ + app, + "include", + ], + }, + }, + methods: { + getTask({ + taskId, ...args + } = {}) { + return this.app._makeRequest({ + path: `/tasks/${taskId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + getTask, + taskId, + include, + } = this; + + const response = await getTask({ + $, + taskId, + params: { + include: Array.isArray(include) + ? include?.join(",") + : include, + }, + }); + + $.export("$summary", `Successfully retrieved task with ID \`${response.data.id}\``); + return response; + }, +}; diff --git a/components/cloud_convert/cloud_convert.app.mjs b/components/cloud_convert/cloud_convert.app.mjs index 899c271319bfd..df71f31613e08 100644 --- a/components/cloud_convert/cloud_convert.app.mjs +++ b/components/cloud_convert/cloud_convert.app.mjs @@ -1,11 +1,119 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "cloud_convert", - propDefinitions: {}, + propDefinitions: { + taskId: { + type: "string", + label: "Task ID", + description: "The ID of the input task for the conversion, normally the import task.", + async options({ + page, filters, + filter = () => true, + mapper = ({ + id: value, + result, + }) => ({ + value, + label: result?.files?.map(({ filename }) => filename).join(", ") || value, + }), + }) { + const { data } = await this.listTasks({ + params: { + ...filters, + page: page + 1, + }, + }); + return data.filter(filter).map(mapper); + }, + }, + conversionType: { + type: "string", + label: "Converstion Type", + description: "The ID of the operation to perform.", + async options({ + page, filters, + mapper = ({ id: value }) => value, + }) { + const { data } = await this.listOperations({ + params: { + ...filters, + page: page + 1, + }, + }); + return data.map(mapper); + }, + }, + include: { + type: "string[]", + label: "Include", + description: "The properties to include in the response", + optional: true, + options: [ + "retries", + "depends_on_tasks", + "payload", + "job", + ], + }, + filename: { + type: "string", + label: "Filename", + description: "The filename of the input file, including extension. If none provided we will try to detect the filename from the URL.", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Content-Type": "application/json", + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + createJob(args = {}) { + return this.post({ + path: "/jobs", + ...args, + }); + }, + listTasks(args = {}) { + return this._makeRequest({ + path: "/tasks", + ...args, + }); + }, + listOperations(args = {}) { + return this._makeRequest({ + path: "/operations", + ...args, + }); }, }, }; diff --git a/components/cloud_convert/common/constants.mjs b/components/cloud_convert/common/constants.mjs new file mode 100644 index 0000000000000..80751281f41a9 --- /dev/null +++ b/components/cloud_convert/common/constants.mjs @@ -0,0 +1,35 @@ +const BASE_URL = "https://api.cloudconvert.com"; +const VERSION_PATH = "/v2"; +const LAST_CREATED_AT = "lastCreatedAt"; +const DEFAULT_MAX = 600; +const WEBHOOK_ID = "webhookId"; +const SECRET = "secret"; + +const TASK_STATUS = { + WAITING: "waiting", + PROCESSING: "processing", + FINISHED: "finished", + ERROR: "error", +}; + +const TASK_OPERATION = { + CONVERT: "convert", + IMPORT_S3: "import/s3", + OPTIMIZE: "optimize", + CAPTURE_WEB: "capture-website", + EXPORT_URL: "export/url", + ARCHIVE: "archive", + IMPORT_URL: "import/url", + MERGE: "merge", +}; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_MAX, + LAST_CREATED_AT, + WEBHOOK_ID, + SECRET, + TASK_STATUS, + TASK_OPERATION, +}; diff --git a/components/cloud_convert/package.json b/components/cloud_convert/package.json new file mode 100644 index 0000000000000..e502290251841 --- /dev/null +++ b/components/cloud_convert/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/cloud_convert", + "version": "0.1.0", + "description": "Pipedream Cloud Convert Components", + "main": "cloud_convert.app.mjs", + "keywords": [ + "pipedream", + "cloud_convert" + ], + "homepage": "https://pipedream.com/apps/cloud_convert", + "author": "Pipedream (https://pipedream.com/)", + "dependencies": { + "@pipedream/platform": "^1.6.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/components/cloud_convert/sources/common/events.mjs b/components/cloud_convert/sources/common/events.mjs new file mode 100644 index 0000000000000..2b8e6ced6ade9 --- /dev/null +++ b/components/cloud_convert/sources/common/events.mjs @@ -0,0 +1,5 @@ +export default { + JOB_CREATED: "job.created", + JOB_FINISHED: "job.finished", + JOB_FAILED: "job.failed", +}; diff --git a/components/cloud_convert/sources/common/webhook.mjs b/components/cloud_convert/sources/common/webhook.mjs new file mode 100644 index 0000000000000..a3fba3c2016ed --- /dev/null +++ b/components/cloud_convert/sources/common/webhook.mjs @@ -0,0 +1,106 @@ +import { createHmac } from "crypto"; +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../cloud_convert.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + }, + hooks: { + async activate() { + const { + data: { + id: webhookId, + signing_secret: secret, + }, + } = + await this.createWebhook({ + data: { + url: this.http.endpoint, + events: this.getEvents(), + }, + }); + + this.setWebhookId(webhookId); + this.setSecret(secret); + }, + async deactivate() { + const webhookId = this.getWebhookId(); + if (webhookId) { + await this.deleteWebhook({ + webhookId, + }); + } + }, + }, + methods: { + setWebhookId(value) { + this.db.set(constants.WEBHOOK_ID, value); + }, + getWebhookId() { + return this.db.get(constants.WEBHOOK_ID); + }, + setSecret(value) { + this.db.set(constants.SECRET, value); + }, + getSecret() { + return this.db.get(constants.SECRET); + }, + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getEvents() { + throw new ConfigurationError("getEvents is not implemented"); + }, + processResource(resource) { + this.$emit(resource, this.generateMeta(resource)); + }, + createWebhook(args = {}) { + return this.app.post({ + debug: true, + path: "/webhooks", + ...args, + }); + }, + deleteWebhook({ + webhookId, ...args + }) { + return this.app.delete({ + debug: true, + path: `/webhooks/${webhookId}`, + ...args, + }); + }, + isSignatureValid(body, signature) { + const secret = this.getSecret(); + + const computedSignature = + createHmac("sha256", secret) + .update(body) + .digest("hex"); + + return signature === computedSignature; + }, + }, + async run({ + body, bodyRaw, headers: { ["cloudconvert-signature"]: signature }, + }) { + const isValid = this.isSignatureValid(bodyRaw, signature); + + if (!isValid) { + return console.log("Invalid signature"); + } + + this.http.respond({ + status: 200, + }); + + this.processResource(body); + }, +}; diff --git a/components/cloud_convert/sources/job-failed-instant/job-failed-instant.mjs b/components/cloud_convert/sources/job-failed-instant/job-failed-instant.mjs new file mode 100644 index 0000000000000..b5ab2ce38ef8a --- /dev/null +++ b/components/cloud_convert/sources/job-failed-instant/job-failed-instant.mjs @@ -0,0 +1,29 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; + +export default { + ...common, + key: "cloud_convert-job-failed-instant", + name: "Job Failed (Instant)", + description: "Emit new event when a CloudConvert job has failed. [See the documentation](https://cloudconvert.com/api/v2/webhooks#webhooks-events)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + events.JOB_FAILED, + ]; + }, + generateMeta(resource) { + const { job } = resource; + const ts = Date.parse(job.created_at); + return { + id: `${job.id}-${ts}`, + summary: `Job Failed: ${job.id}`, + ts, + }; + }, + }, +}; diff --git a/components/cloud_convert/sources/job-finished-instant/job-finished-instant.mjs b/components/cloud_convert/sources/job-finished-instant/job-finished-instant.mjs new file mode 100644 index 0000000000000..aaad1aa21a4c1 --- /dev/null +++ b/components/cloud_convert/sources/job-finished-instant/job-finished-instant.mjs @@ -0,0 +1,29 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; + +export default { + ...common, + key: "cloud_convert-job-finished-instant", + name: "Job Finished (Instant)", + description: "Emit new event when a CloudConvert job has been completed. [See the documentation](https://cloudconvert.com/api/v2/webhooks#webhooks-events)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + events.JOB_FINISHED, + ]; + }, + generateMeta(resource) { + const { job } = resource; + const ts = Date.parse(job.created_at); + return { + id: `${job.id}-${ts}`, + summary: `Job Finished: ${job.id}`, + ts, + }; + }, + }, +}; diff --git a/components/cloud_convert/sources/new-job-instant/new-job-instant.mjs b/components/cloud_convert/sources/new-job-instant/new-job-instant.mjs new file mode 100644 index 0000000000000..19e2c628b967e --- /dev/null +++ b/components/cloud_convert/sources/new-job-instant/new-job-instant.mjs @@ -0,0 +1,29 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; + +export default { + ...common, + key: "cloud_convert-new-job-instant", + name: "New Job (Instant)", + description: "Emit new event when a new job has been created. [See the documentation](https://cloudconvert.com/api/v2/webhooks#webhooks-events)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + events.JOB_CREATED, + ]; + }, + generateMeta(resource) { + const { job } = resource; + const ts = Date.parse(job.created_at); + return { + id: job.id, + summary: `New Job: ${job.id}`, + ts, + }; + }, + }, +}; diff --git a/components/cloudbees/README.md b/components/cloudbees/README.md index 9b46bc2491476..194f1f691d1ad 100644 --- a/components/cloudbees/README.md +++ b/components/cloudbees/README.md @@ -1,9 +1,11 @@ # Overview -The CloudBees API allows you to build a range of applications and tools. Here -are some examples: +The CloudBees API interfaces with CloudBees CI and CD solutions to automate and manage build, test, and deployment processes for software projects. Through integration with Pipedream, you can harness the potential of the CloudBees API to create custom workflows. This might include triggering deployments, managing build statuses, or responding to events within your CI/CD pipeline. Automating these tasks can expedite the development cycle, reduce errors, and enhance productivity. -- A tool to manage your CloudBees account -- A tool to monitor your CloudBees account -- A tool to track your CloudBees usage -- A tool to generate reports on your CloudBees usage +# Example Use Cases + +- **Automated Deployment Trigger**: Set up a Pipedream workflow that listens for a successful build event from your source control platform (e.g., GitHub). Upon detecting this event, it triggers a deployment using the CloudBees API, ensuring that code transitions from repository to production smoothly and without manual intervention. + +- **Build Status Notifications**: Configure a Pipedream workflow to monitor build statuses via the CloudBees API. When a build fails, the workflow can send real-time notifications to a Slack channel, alerting the team to investigate and resolve the issue promptly. + +- **Dynamic Resource Allocation**: Create a workflow on Pipedream that uses the CloudBees API to adjust resources dynamically based on the pipeline load. For instance, if the API reports high usage, the workflow can spin up additional build executors or scale down when activity is low to optimize costs. diff --git a/components/cloudbees/package.json b/components/cloudbees/package.json new file mode 100644 index 0000000000000..0d1f671660145 --- /dev/null +++ b/components/cloudbees/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/cloudbees", + "version": "0.6.0", + "description": "Pipedream cloudbees Components", + "main": "cloudbees.app.mjs", + "keywords": [ + "pipedream", + "cloudbees" + ], + "homepage": "https://pipedream.com/apps/cloudbees", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/cloudcart/README.md b/components/cloudcart/README.md new file mode 100644 index 0000000000000..b7c93300f2f21 --- /dev/null +++ b/components/cloudcart/README.md @@ -0,0 +1,11 @@ +# Overview + +The CloudCart API enables you to interact programmatically with your CloudCart store, offering endpoints to manage products, orders, customers, and more. By leveraging this API within Pipedream, you can create automated workflows that streamline e-commerce operations, sync data with other services, and react to events in real-time. Whether it's syncing order details to a CRM, updating inventory levels, or sending transactional emails, Pipedream's serverless platform provides a powerful and flexible toolkit to enhance your e-commerce automation. + +# Example Use Cases + +- **Order Processing Automation**: Automatically process new orders from CloudCart by capturing the order details and forwarding them to fulfillment services or inventory management systems like ShipStation or QuickBooks. Use Pipedream to listen for new orders and trigger subsequent actions effortlessly. + +- **Customer Relationship Management**: When a new customer registers or places an order on CloudCart, use Pipedream to add or update their details in a CRM like Salesforce or HubSpot. This ensures that customer data is consistent across platforms and that marketing efforts are tailored to customer behavior. + +- **Real-Time Inventory Sync**: Keep your inventory levels in check by syncing them between CloudCart and your point-of-sale (POS) system whenever a sale is made or new stock is added. Pipedream can help automate these updates, reducing the risk of overselling and ensuring a smooth operation. diff --git a/components/cloudentity/cloudentity.app.mjs b/components/cloudentity/cloudentity.app.mjs new file mode 100644 index 0000000000000..8b5d7da0371df --- /dev/null +++ b/components/cloudentity/cloudentity.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "cloudentity", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/cloudentity/package.json b/components/cloudentity/package.json new file mode 100644 index 0000000000000..89ba6746bdb14 --- /dev/null +++ b/components/cloudentity/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/cloudentity", + "version": "0.0.1", + "description": "Pipedream Cloudentity Components", + "main": "cloudentity.app.mjs", + "keywords": [ + "pipedream", + "cloudentity" + ], + "homepage": "https://pipedream.com/apps/cloudentity", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/components/cloudfill/README.md b/components/cloudfill/README.md index d59ea5fce4ca0..c2240387fa571 100644 --- a/components/cloudfill/README.md +++ b/components/cloudfill/README.md @@ -1,11 +1,11 @@ # Overview -The CloudFill API enables developers to create applications that can -automatically fill out web forms and sign documents. +The CloudFill API specializes in automatic form filling, allowing you to pre-fill web forms with accurate and up-to-date information programmatically. With CloudFill on Pipedream, you can create serverless workflows that trigger form filling operations, handle data extracted from forms, and connect to other apps to streamline complex tasks like data entry, customer onboarding, and lead generation. -Some examples of what you can build using the CloudFill API include: +# Example Use Cases -- A web form completion tool that can automatically fill out online forms -- A document signing tool that can electronically sign documents -- An integration with an existing application or website that allows users to - fill out forms or sign documents using their existing account +- **Automated Customer Onboarding**: On receiving a new customer signup via a CRM like Salesforce, trigger a Pipedream workflow that uses CloudFill to auto-populate registration and onboarding forms with the customer's details. The workflow can then email the pre-filled forms to the customer for review, improving the onboarding experience and reducing manual data entry. + +- **Dynamic Lead Generation Forms**: Combine CloudFill with a marketing tool like Mailchimp on Pipedream. When a potential lead downloads a resource or signs up for a webinar, have CloudFill pre-fill a personalized follow-up form based on the information they provided. Then, use Mailchimp to send the form to the lead, increasing engagement with a customized approach. + +- **Streamlined Invoice Processing**: In an e-commerce scenario, when a new order is placed and captured by a Pipedream workflow, employ CloudFill to automatically fill out an invoice template with the order details. This invoice can then be sent to an accounting app like QuickBooks for payment processing, ensuring accuracy and saving valuable time. diff --git a/components/cloudflare_api_key/README.md b/components/cloudflare_api_key/README.md index 2c9508c498caa..abfc33d030d06 100644 --- a/components/cloudflare_api_key/README.md +++ b/components/cloudflare_api_key/README.md @@ -1,13 +1,11 @@ # Overview -Cloudflare provides a powerful API that you can use to manage your DNS -settings,Crypto settings, and more.Here are some examples of what you can do -with the Cloudflare API: - -- Manage your DNS settings: You can use the Cloudflare API to manage your DNS - settings, including creating and updating DNS records. -- Crypto settings: You can use the Cloudflare API to manage your Crypto - settings, including enabling and disabling SSL/TLS encryption for your - website. -- Firewall rules: You can use the Cloudflare API to manage your firewall rules, - including creating and updating firewall rules. +Harness the power of Cloudflare within Pipedream's scalable platform to automate and optimize your web operations. The Cloudflare API enables you to programmatically control countless aspects of your web presence, from security settings and firewall rules to traffic and DNS management. By integrating this with Pipedream, you can create custom workflows that react to specific triggers, manipulate Cloudflare configurations on-the-fly, and connect to countless other services for a seamless automation experience. + +# Example Use Cases + +- **Automate DNS Updates**: When a new domain is added to your system via a webhook from your domain registration service, Pipedream can catch this event and automatically create or update DNS records in Cloudflare, keeping your domain configurations in sync without manual intervention. + +- **Dynamic Content Caching Rules**: Connect Cloudflare with a CMS like WordPress. Whenever a new post is published or updated, trigger a Pipedream workflow that purges the Cloudflare cache for the specific URL or path, ensuring that visitors get the most up-to-date content. + +- **Security Alerts and Mitigation**: Integrate Cloudflare with Slack using Pipedream to monitor security events. When Cloudflare identifies a potential security threat, a Pipedream workflow can send an alert to a designated Slack channel and automatically apply pre-defined firewall rules or rate-limiting to mitigate the issue. diff --git a/components/cloudflare_r2/README.md b/components/cloudflare_r2/README.md new file mode 100644 index 0000000000000..34dea47e6f4cb --- /dev/null +++ b/components/cloudflare_r2/README.md @@ -0,0 +1,11 @@ +# Overview + +The Cloudflare R2 API lets you interact with Cloudflare's object storage service, providing a cost-effective way to store large amounts of data with no egress fees. On Pipedream, you can harness this API to build automated workflows that can store, retrieve, and manage data within your R2 buckets. By combining Cloudflare R2 with Pipedream’s capabilities, you can create serverless workflows that trigger on various events, process data in-flight, and integrate with over 800+ apps available on the platform. + +# Example Use Cases + +- **Automated Backup to R2**: Create a workflow on Pipedream that listens for new files in your Dropbox or Google Drive and automatically backs them up to an R2 bucket for secure and cost-effective long-term storage. + +- **Website Asset Delivery**: Build a pipeline that optimizes images or other static assets uploaded to Cloudflare R2 and serves them efficiently to your website, leveraging Pipedream’s ability to process the data and Cloudflare’s global delivery network. + +- **Event-Driven Data Archival**: Set up a system where logs or data from your application, received through webhooks or other triggers on Pipedream, are automatically archived to an R2 bucket for compliance, analysis, or backup purposes, potentially integrating with database services like Airtable for metadata management. diff --git a/components/cloudflare_r2/package.json b/components/cloudflare_r2/package.json index 52cf0759c587d..2e53b018351eb 100644 --- a/components/cloudflare_r2/package.json +++ b/components/cloudflare_r2/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/cloudinary/README.md b/components/cloudinary/README.md index d31f699cc5dc1..169265ba9d9a0 100644 --- a/components/cloudinary/README.md +++ b/components/cloudinary/README.md @@ -1,20 +1,11 @@ # Overview -Cloudinary is a powerful image and video management API that can be used to -deliver images and videos for a number of different use cases, including: +The Cloudinary API empowers developers to manage media assets in the cloud with ease. It allows for uploading, storing, optimizing, and delivering images and videos with automated transformations to ensure the content is tailored for any device or platform. This API's versatility is key for automating workflows that require dynamic media handling, such as resizing images on-the-fly, converting video formats, or even extracting metadata for asset management. -- Social media applications -- E-commerce websites -- News websites -- Blogs -- And more! +# Example Use Cases -Here are some examples of what you can build with the Cloudinary API: +- **Automated Social Media Content Creation**: When a new product image is uploaded to your ecommerce platform, trigger a Pipedream workflow that uses Cloudinary to resize and optimize the image for various social media channels, then automatically post it to Instagram, Facebook, and Twitter using their respective APIs. -- An image gallery that displays images from a Cloudinary account -- A social media application that allows users to upload and share images and - videos -- An e-commerce website that allows users to manage their product images and - videos -- A news website that allows journalists to upload and share images and videos -- A blog that allows bloggers to manage their blog images and videos +- **User-Generated Content Moderation**: Integrate a system where user-uploaded images are sent to Cloudinary via Pipedream, which then uses Cloudinary's AI-powered content moderation to flag inappropriate content. Once moderated, the images could be stored in a secure bucket on Amazon S3 for approved content, while sending alerts to administrators for review if content is flagged. + +- **Dynamic Asset Delivery for Web Applications**: Create a Pipedream workflow that listens for new entries in a CMS like Contentful. Upon a new post creation, automatically trigger Cloudinary to generate multiple versions of the associated images (thumbnails, different resolutions, watermarked versions) and update the CMS entry with the optimized image URLs for faster page loads and improved SEO. diff --git a/components/cloudinary/actions/get-account-usage-details/get-account-usage-details.mjs b/components/cloudinary/actions/get-account-usage-details/get-account-usage-details.mjs index 51ab42c2a2759..bd26519352103 100644 --- a/components/cloudinary/actions/get-account-usage-details/get-account-usage-details.mjs +++ b/components/cloudinary/actions/get-account-usage-details/get-account-usage-details.mjs @@ -3,15 +3,20 @@ import cloudinary from "../../cloudinary.app.mjs"; export default { key: "cloudinary-get-account-usage-details", name: "Get Account Usage Details", - description: "Enables you to get a report on the status of your Cloudinary account usage details, including storage, credits, bandwidth, requests, number of resources, and add-on usage. [See the documentation](https://cloudinary.com/documentation/admin_api#usage)", - version: "0.1.2", + description: "Gets a report of your Cloudinary account usage details, including storage, credits, bandwidth, requests, number of resources, and add-on usage. [See the documentation](https://cloudinary.com/documentation/admin_api#usage)", + version: "0.2.0", type: "action", props: { cloudinary, + dateInfo: { + type: "alert", + alertType: "info", + content: "If `Date` is not specified, it defaults to the current date.", + }, date: { type: "string", label: "Date", - description: "The date for the usage report. Must be within the last 3 months and given in the format: `dd-mm-yyyy`. Default: the current date", + description: "The date for the usage report, in the `yyyy-mm-dd` format, e.g. `2019-07-21`. Must be between yesterday and the last 3 months.", optional: true, }, }, @@ -20,12 +25,17 @@ export default { date: this.date, }; - const response = await this.cloudinary.getUsage(options); + try { + const response = await this.cloudinary.getUsage(options); - if (response) { - $.export("$summary", "Successfully retrieved usage details."); - } + if (response) { + $.export("$summary", "Successfully retrieved usage details"); + } - return response; + return response; + } + catch (err) { + throw new Error(`Cloudinary error response: ${err.error?.message ?? JSON.stringify(err)}`); + } }, }; diff --git a/components/cloudinary/actions/get-resources/get-resources.mjs b/components/cloudinary/actions/get-resources/get-resources.mjs index 4ef4f0c8eb191..fb348fbd58116 100644 --- a/components/cloudinary/actions/get-resources/get-resources.mjs +++ b/components/cloudinary/actions/get-resources/get-resources.mjs @@ -4,7 +4,7 @@ export default { key: "cloudinary-get-resources", name: "Get Resources", description: "Lists resources (assets) uploaded to your product environment. [See the documentation](https://cloudinary.com/documentation/admin_api#get_resources)", - version: "0.0.1", + version: "0.1.0", type: "action", props: { cloudinary, @@ -22,28 +22,35 @@ export default { }, prefix: { type: "string", - label: "Prefix", - description: "Find all assets with a public ID that starts with the specified prefix", + label: "Filter by Prefix", + description: "Find all assets with a public ID that starts with the specified prefix.", optional: true, }, tags: { type: "boolean", - label: "Tags", - description: "Whether to include the list of tag names assigned to each asset", + label: "Include Tags", + description: "Whether to include the list of tag names assigned to each asset.", default: false, optional: true, }, context: { type: "boolean", - label: "Context", - description: "Whether to include key-value pairs of contextual metadata associated with each asset", + label: "Include Context", + description: "Whether to include key-value pairs of contextual metadata associated with each asset.", + default: false, + optional: true, + }, + metadata: { + type: "boolean", + label: "Include Metadata", + description: "Whether to include the structured metadata fields and values assigned to each asset.", default: false, optional: true, }, moderation: { type: "boolean", - label: "Moderation", - description: "Whether to include the image moderation status of each asset", + label: "Include Moderation", + description: "Whether to include the image moderation status of each asset.", default: false, optional: true, }, @@ -66,22 +73,27 @@ export default { }; const resources = []; - let next; - do { - const response = await this.cloudinary.getResources(options); - resources.push(...response.resources); - next = response.next_cursor; - options.next_cursor = next; - } while (next && resources.length < this.maxResults); + try { + let next; + do { + const response = await this.cloudinary.getResources(options); + resources.push(...response.resources); + next = response.next_cursor; + options.next_cursor = next; + } while (next && resources.length < this.maxResults); - if (resources.length > this.maxResults) { - resources.length = this.maxResults; - } + if (resources.length > this.maxResults) { + resources.length = this.maxResults; + } - $.export("$summary", `Found ${resources.length} resource${resources.length === 1 - ? "" - : "s"}.`); + $.export("$summary", `Retrieved ${resources.length} resource${resources.length === 1 + ? "" + : "s"}`); - return resources; + return resources; + } + catch (err) { + throw new Error(`Cloudinary error response: ${err.error?.message ?? JSON.stringify(err)}`); + } }, }; diff --git a/components/cloudinary/actions/image-transformation/image-transformation.mjs b/components/cloudinary/actions/image-transformation/image-transformation.mjs deleted file mode 100644 index d9df4a4d59760..0000000000000 --- a/components/cloudinary/actions/image-transformation/image-transformation.mjs +++ /dev/null @@ -1,31 +0,0 @@ -import cloudinary from "../../cloudinary.app.mjs"; - -export default { - key: "cloudinary-image-transformation", - name: "Image Transformation", - description: "Transforms images on-the-fly. It modifies them to any required format, style and dimension, resize and crop the images, etc. [See the documentation](https://cloudinary.com/documentation/image_transformations)", - version: "0.1.2", - type: "action", - props: { - cloudinary, - imageSource: { - type: "string", - label: "Public ID", - description: "The [public ID](https://cloudinary.com/documentation/upload_images#public_id) that references a file you've previously uploaded to Cloudinary, e.g. `folder/filename`.", - }, - options: { - type: "object", - label: "Options", - description: "The image transformation options to apply and/or the URL parameters supported by Cloudinary API. For all transformation options, please check [Image transformation API reference](https://cloudinary.com/documentation/image_transformation_reference), for URL parameters, please check [Transforming media assets using dynamic URLs](https://cloudinary.com/documentation/image_transformations#transforming_media_assets_using_dynamic_urls)", - }, - }, - async run({ $ }) { - const response = await this.cloudinary.transformImage(this.imageSource, this.options); - - if (response) { - $.export("$summary", "Successfully transformed image."); - } - - return response; - }, -}; diff --git a/components/cloudinary/actions/resource-transformation/resource-transformation.mjs b/components/cloudinary/actions/resource-transformation/resource-transformation.mjs deleted file mode 100644 index 827f71e88d8c0..0000000000000 --- a/components/cloudinary/actions/resource-transformation/resource-transformation.mjs +++ /dev/null @@ -1,46 +0,0 @@ -import cloudinary from "../../cloudinary.app.mjs"; - -export default { - key: "cloudinary-resource-transformation", - name: "Resource Transformation", - description: "Transforms image or video resources on-the-fly. It allows transformation options for resource optimization (i.e. web viewing), resize and crop the resources, etc. [Image transformation documentation](https://cloudinary.com/documentation/image_transformations). [Video transformation documentation](https://cloudinary.com/documentation/video_transformation_reference)", - version: "0.2.2", - type: "action", - props: { - cloudinary, - resourceType: { - propDefinition: [ - cloudinary, - "transformationResourceType", - ], - }, - options: { - type: "string", - label: "Options", - description: "For an image `resourceType`, use this parameter to set the image transformation options to apply and/or the URL parameters supported by Cloudinary API. For all transformation options, please check [Image transformation API reference](https://cloudinary.com/documentation/image_transformation_reference), for URL parameters, please check [Transforming media assets using dynamic URLs](https://cloudinary.com/documentation/image_transformations#transforming_media_assets_using_dynamic_urls).\nFor a video `resourceType`, use this parameter to set video transformation options to apply and/or the URL parameters supported by Cloudinary API. For all transformation options, please check [Video transformation API reference](https://cloudinary.com/documentation/video_transformation_reference), for URL parameters, please check [Transforming media assets using dynamic URLs](https://cloudinary.com/documentation/image_transformations#transforming_media_assets_using_dynamic_urls).", - }, - imageSource: { - type: "string", - label: "Image Source", - description: "If `resourceType` is an image, use this parameter to point to the source of the image to apply transformations on. It can be a local file, the actual image data, a remote FTP, HTTP or HTTPS URL address of an existing image. For details and examples, see: [file source options](https://cloudinary.com/documentation/upload_images#file_source_options).", - optional: true, - }, - videoPublicId: { - type: "string", - label: "Video Public Id", - description: "If `resourceType` is a video, use this parameter to set the public id of the video to apply transformations on. The public id is the unique identifier of the video, and is either specified when uploading the video to your Cloudinary account, or automatically assigned by Cloudinary. For more details on the options for specifying the public id, see [Public ID - the image identifier](https://cloudinary.com/documentation/upload_images#public_id).", - optional: true, - }, - }, - async run({ $ }) { - const response = this.resourceType === "image" - ? await this.cloudinary.transformImage(this.imageSource, this.options) - : await this.cloudinary.transformVideo(this.videoPublicId, this.options); - - if (response) { - $.export("$summary", `Successfully transformed ${this.resourceType}`); - } - - return response; - }, -}; diff --git a/components/cloudinary/actions/transform-resource/transform-resource.mjs b/components/cloudinary/actions/transform-resource/transform-resource.mjs new file mode 100644 index 0000000000000..5925e59dc41ee --- /dev/null +++ b/components/cloudinary/actions/transform-resource/transform-resource.mjs @@ -0,0 +1,65 @@ +import { ConfigurationError } from "@pipedream/platform"; +import cloudinary from "../../cloudinary.app.mjs"; + +export default { + key: "cloudinary-transform-resource", + name: "Transform Resource", + description: "Transform an image, video or audio asset on-the-fly with several options. [See the documentation](https://cloudinary.com/documentation/video_manipulation_and_delivery)", + version: "0.0.1", + type: "action", + props: { + cloudinary, + assetId: { + propDefinition: [ + cloudinary, + "assetId", + ], + }, + info: { + type: "alert", + alertType: "info", + content: `You can either select a pre-configured transformation or pass a transformation string. Both can be managed in the [Cloudinary Transformation Builder](https://tx.cloudinary.com/). +\\ +If both are specified, the transformation string will be ignored.`, + }, + namedTransformation: { + propDefinition: [ + cloudinary, + "namedTransformation", + ], + }, + transformationString: { + propDefinition: [ + cloudinary, + "transformationString", + ], + }, + }, + async run({ $ }) { + const { + cloudinary, assetId, namedTransformation, transformationString, + } = this; + + if (!namedTransformation && !transformationString) { + throw new ConfigurationError("Either `Named Transformation` or `Transformation String` are required"); + } + + try { + const response = await cloudinary.transformAsset(assetId, namedTransformation + ? { + transformation: namedTransformation, + } + : { + raw_transformation: transformationString, + }); + + if (response) { + $.export("$summary", "Successfully transformed resource"); + } + + return response; + } catch (err) { + throw new Error(`Cloudinary error response: ${err.error?.message ?? JSON.stringify(err)}`); + } + }, +}; diff --git a/components/cloudinary/actions/upload-media-asset/upload-media-asset.mjs b/components/cloudinary/actions/upload-media-asset/upload-media-asset.mjs index 34ff52233582e..3129625e29da7 100644 --- a/components/cloudinary/actions/upload-media-asset/upload-media-asset.mjs +++ b/components/cloudinary/actions/upload-media-asset/upload-media-asset.mjs @@ -3,20 +3,25 @@ import cloudinary from "../../cloudinary.app.mjs"; export default { key: "cloudinary-upload-media-asset", name: "Upload Media Asset", - description: "Uploads media assets in the cloud such as images or videos, and allows configuration options to be set on the upload. [See the documentation](https://cloudinary.com/documentation/image_upload_api_reference#upload_method)", - version: "0.5.2", + description: "Upload media assets such as images or videos. [See the documentation](https://cloudinary.com/documentation/image_upload_api_reference#upload_method)", + version: "1.0.0", type: "action", props: { cloudinary, + infoAlert: { + type: "alert", + alertType: "info", + content: "Cloudinary offers a large amount of options to customize your asset upload. You can set any available options in the `Additional Options` prop. [See the Cloudinary documentation for more information.](https://cloudinary.com/documentation/image_upload_api_reference#upload_method)", + }, file: { type: "string", - label: "File", - description: "The file to upload. It can be:\n* a local file path\n* the actual data (byte array buffer).\nFor example, this could be an IO input stream of the data (e.g., File.open(file, \"rb\")).\n* the Data URI (Base64 encoded), max ~60 MB (62,910,000 chars)\n* the remote FTP, HTTP or HTTPS URL address of an existing file\n* a private storage bucket (S3 or Google Storage) URL of a **whitelisted** bucket\nFor details and examples, see: [file source options](https://cloudinary.com/documentation/upload_images#file_source_options).", + label: "File Path or URL", + description: "The file to upload. You can provide a file path from the `/tmp` folder (e.g. `/tmp/myFile.jpg`) or a public file URL, among other options supported by Cloudinary ([see the documentation](https://cloudinary.com/documentation/upload_images#file_source_options) for available options).", }, publicId: { type: "string", label: "Public Id", - description: "The identifier that is used for accessing the uploaded asset. The Public ID may contain a full path including folders separated by a slash (`/`).\nIf not specified, then the Public ID of the asset will either be comprised of random characters or will use the original file's filename, depending whether `use_filename` was set to true.\n\n**Note**: The Public ID value for images and videos should not include a file extension. Include the file extension for `raw` files only.", + description: "The identifier that is used for accessing the uploaded asset. [See the documentation](https://cloudinary.com/documentation/image_upload_api_reference#upload_method) for more information.", optional: true, }, folder: { @@ -25,18 +30,6 @@ export default { description: "An optional folder name where the uploaded asset will be stored. The public ID contains the full path of the uploaded asset, including the folder name.", optional: true, }, - useFilename: { - type: "boolean", - label: "Use Filename", - description: "Whether to use the original file name of the uploaded asset. Relevant only if the `public_id` parameter isn't set.\nWhen false and the `public_id` parameter is also not defined, the Public ID will be comprised of random characters.\n\nWhen true and the `public_id` parameter is not defined, the uploaded file's original filename becomes the Public ID. Random characters are appended to the filename value to ensure Public ID uniqueness if `unique_filename` is true.\n\nDefault: `false`.", - optional: true, - }, - uniqueFilename: { - type: "boolean", - label: "Unique Filename", - description: "When set to false, does not add random characters at the end of the filename that guarantee its uniqueness. In this case, if the `overwrite` parameter is also false, the upload returns an error. This parameter is relevant only if `use_filename` is also set to true. Default: `true`.", - optional: true, - }, resourceType: { propDefinition: [ cloudinary, @@ -49,178 +42,22 @@ export default { "uploadDeliveryType", ], }, - accessControl: { - type: "boolean", - label: "Access Control", - description: "An array of access types for the asset. The asset is accessible as long as one of the access types is valid.\nPossible values for each access type:\n\n- `token` requires either [Token-based authentication](https://cloudinary.com/documentation/control_access_to_media#token_based_authentication_premium_feature) or [Cookie-based authentication](https://cloudinary.com/documentation/control_access_to_media#cookie_based_authentication_premium_feature) for accessing the asset.\nFor example: `access_type: \"token\"`\n- `anonymous` allows public access to the asset. The anonymous access type can optionally include `start` and/or `end` dates (in ISO 8601 format) that define when the asset is publically available. Note that you can only include a single 'anonymous' access type. For example:\n`access_type: \"anonymous\", start: \"2017-12-15T12:00Z\", end: \"2018-01-20T12:00Z\"`", - optional: true, - }, accessMode: { propDefinition: [ cloudinary, "accessMode", ], }, - discardOriginalFilename: { - type: "boolean", - label: "Discard Original Filename", - description: "Whether to discard the name of the original uploaded file. Relevant when delivering assets as attachments (setting the `flag` transformation parameter to `attachment`). Default: `false`.", - optional: true, - }, - overwrite: { - type: "boolean", - label: "Overwrite", - description: "Whether to overwrite existing assets with the same public ID. When set to false, return immediately if an asset with the same Public ID was found. Default: `true` (when using unsigned upload, the default is false and cannot be changed to true).\n**Important**: Depending on the settings for your account, overwriting an asset may clear the tags, contextual, and structured metadata values for that asset. If you prefer these values to always be preserved on overwrite (unless other values are specifically set when uploading the new version), you can [submit a request](https://support.cloudinary.com/hc/en-us/requests/new) to change this behavior for your account.", - optional: true, - }, tags: { - type: "any", + type: "string[]", label: "Tags", description: "An array of tag names to assign to the uploaded asset for later group reference.", optional: true, }, - context: { - type: "object", - label: "Context", - description: "A map of the key-value pairs of general textual context metadata to attach to an uploaded asset. The context values of uploaded files can be retrieved using the Admin API. For example: `alt=My image?caption=Profile image` (the `=` and `?` characters can be supported as values when escaped with a prepended backslash (`\\`)). Note that key values are limited to 1024 characters and an asset can have a maximum of 1000 context key-value pairs.", - optional: true, - }, - colors: { - type: "boolean", - label: "Colors", - description: "Whether to retrieve predominant colors & color histogram of the uploaded image.\n**Note:** If all returned colors are opaque, then 6-digit RGB hex values are returned. If one or more colors contain an alpha channel, then 8-digit RGBA hex quadruplet values are returned.\nDefault: `false`. Relevant for images only.", - optional: true, - }, - faces: { - type: "boolean", - label: "Faces", - description: "Whether to return the coordinates of faces contained in an uploaded image (automatically detected or manually defined). Each face is specified by the X & Y coordinates of the top left corner and the width & height of the face. The coordinates for each face are returned as an array (using the SDKs), and individual faces are separated with a pipe (`?`). For example: `10,20,150,130?213,345,82,61`.\nDefault: `false`. Relevant for images only.", - optional: true, - }, - qualityAnalysis: { - type: "boolean", - label: "Quality Analysis", - description: "Whether to return a quality analysis value for the image between 0 and 1, where 0 means the image is blurry and out of focus and 1 means the image is sharp and in focus. Default: `false`. Relevant for images only.\nPaid customers can [request to take part](https://support.cloudinary.com/hc/en-us/requests/new) in the extended quality analysis Beta trial. When activated, this parameter returns quality scores for various other factors in addition to `focus`, such as `jpeg_quality`, `noise`, `exposure`, `lighting` and `resolution`, together with an overall weighted `quality_score`. The `quality_score`, `color_quality_score` and `pixel_quality_score` fields can be used in the Search API.", - optional: true, - }, - accessibilityAnalysis: { - type: "boolean", - label: "Accessibility Analysis", - description: "Currently available only to paid customers [requesting to take part](https://support.cloudinary.com/hc/en-us/requests/new) in the [accessibility analysis](https://cloudinary.com/documentation/analysis_on_upload#accessibility_analysis) Beta trial. Set to `true` to return accessibility analysis values for the image and to enable the `colorblind_accessibility_score` field to be used in the Search API.\nDefault: `false`. Relevant for images only.", - optional: true, - }, - cinemagraphAnalysis: { - type: "boolean", - label: "Cinemagraph Analysis", - description: "Whether to return a cinemagraph analysis value for the media asset between 0 and 1, where 0 means the asset is **not** a cinemagraph and 1 means the asset **is** a cinemagraph. Default: `false`. Relevant for animated images and video only. A static image will return 0.", - optional: true, - }, - imageMetadata: { - type: "string", - label: "Image Metadata", - description: "Whether to return IPTC, XMP, and detailed Exif metadata of the uploaded asset in the response.\nDefault: `false`. Supported for images, video, and audio.\nReturned metadata for images includes: `PixelsPerUnitX`, `PixelsPerUnitY`, `PixelUnits`, `Colorspace`, and `DPI`.\nReturned metadata for audio and video includes: `audio_codec`, `audio_bit_rate`, `audio_frequency`, `channels`, `channel_layout`.\nAdditional metadata for video includes: `pix_format`, `codec`, `level`, `profile`, `video_bit_rate`, `dar`.", - optional: true, - }, - phash: { - type: "boolean", - label: "pHash", - description: "Whether to return the perceptual hash (pHash) on the uploaded image. The pHash acts as a fingerprint that allows checking image similarity.\nDefault: `false`. Relevant for images only.", - optional: true, - }, - responsiveBreakpoints: { - type: "object", - label: "Responsive Breakpoints", - description: "Requests that Cloudinary automatically find the best breakpoints. The parameter value is an array of breakpoint request settings, where each request setting can include the following parameters:\n* `create_derived`(Boolean - Required) If true, create and keep the derived images of the selected breakpoints during the API call. If false, images * generated during the analysis process are thrown away.\n* `format` (String - Optional) Sets the file extension of the derived resources to the format indicated (as opposed to changing the format as part of a transformation - which would be included as part of the transformation component (e.g., f_jpg)).\n* `transformation` (String - Optional) The base transformation to first apply to the image before finding the best breakpoints. The API accepts a string representation of a chained transformation (same as the regular transformation parameter of the upload API).\n* `max_width` (Integer - Optional) The maximum width needed for this image. If specifying a width bigger than the original image, the width of the original image is used instead. Default: `1000`.\n* `min_width` (Integer - Optional) The minimum width needed for this image. Default: `50`.\n* `bytes_step` (Integer - Optional) The minimum number of bytes between two consecutive breakpoints (images). Default: `20000`.\n* `max_images` (Integer - Optional) The maximum number of breakpoints to find, between 3 and 200. This means that there might be size differences bigger than the given bytes_step value between consecutive images. Default: `20`.\nThe return response will include an array of the selected breakpoints for each breakpoint request, where the following information is given for each breakpoint: `transformation`, `width`, `height`, `bytes`, `url` and `secure_url`.\nRelevant for images only.", - optional: true, - }, - autoTagging: { - type: "integer", - label: "Auto Tagging", - description: "Whether to assign tags to an asset according to detected scene categories with a confidence score higher than the given value (between 0.0 and 1.0). See the [Google Automatic Video Tagging](https://cloudinary.com/documentation/google_automatic_video_tagging_addon), [Google Auto Tagging](https://cloudinary.com/documentation/google_auto_tagging_addon), [Imagga Auto Tagging](https://cloudinary.com/documentation/imagga_auto_tagging_addon), [Amazon Rekognition Auto Tagging](https://cloudinary.com/documentation/aws_rekognition_auto_tagging_addon), and [Amazon Rekognition Celebrity Detection](https://cloudinary.com/documentation/aws_rekognition_celebrity_and_face_detection_addon) add-ons for more details.", - optional: true, - }, - categorization: { - type: "string", - label: "Categorization", - description: "A comma-separated list of the categorization add-ons to run on the asset. Set to `google_tagging`, `google_video_tagging`, `imagga_tagging` and/or `aws_rek_tagging` to automatically classify the scenes of the uploaded asset. See the [Google Automatic Video Tagging](https://cloudinary.com/documentation/google_automatic_video_tagging_addon), [Google Auto Tagging](https://cloudinary.com/documentation/google_auto_tagging_addon), [Imagga Auto Tagging](https://cloudinary.com/documentation/imagga_auto_tagging_addon), and [Amazon Rekognition Auto Tagging](https://cloudinary.com/documentation/aws_rekognition_auto_tagging_addon) add-ons for more details.", - optional: true, - }, - detection: { - type: "string", - label: "Detection", - description: "Set to `adv_face` or `aws_rek_face` to extract an extensive list of face attributes from an image using the [Advanced Facial Attribute Detection](https://cloudinary.com/documentation/advanced_facial_attributes_detection_addon) or [Amazon Rekognition Celebrity Detection](https://cloudinary.com/documentation/aws_rekognition_celebrity_and_face_detection_addon) add-ons.\nRelevant for images only.", - optional: true, - }, - ocr: { - type: "string", - label: "OCR", - description: "Set to `adv_ocr` to extract all text elements in an image as well as the bounding box coordinates of each detected element using the [OCR text detection and extraction add-on](https://cloudinary.com/documentation/ocr_text_detection_and_extraction_addon). Relevant for images only.", - optional: true, - }, - eager: { - type: "any", - label: "Eager", - description: "An array of transformation representations. This generates derived resources in advance, instead of lazily creating each of the derived resources when first accessed by your site's visitors.", - optional: true, - }, - eagerAsync: { - type: "boolean", - label: "Eager Async", - description: "Whether to generate the eager transformations asynchronously in the background after the upload request is completed rather than online as part of the upload call. Default: `false`", - optional: true, - }, - eagerNotificationUrl: { - type: "string", - label: "Eager Notification URL", - description: "An HTTP or HTTPS URL to send a notification to (a webhook) when the generation of eager transformations is completed.", - optional: true, - }, - transformation: { - type: "string", - label: "Transformation", - description: "An incoming transformation to run on the uploaded asset before saving it in the cloud. T his parameter is given as a hash of transformation parameters (or an array of hashes for chained transformations).", - optional: true, - }, format: { type: "string", - label: "Format", - description: "An optional format to convert the uploaded asset to before saving in the cloud. For example: `jpg`.", - optional: true, - }, - customCoordinates: { - type: "any", - label: "Custom Coordinates", - description: "Sets the coordinates of a single region contained in an uploaded image that is subsequently used for cropping uploaded images using the `custom` gravity mode. The region is specified by the X & Y coordinates of the top left corner and the width & height of the region, as an array. For example: `85,120,220,310.`\nRelevant for images only.", - optional: true, - }, - faceCoordinates: { - type: "any", - label: "Face Coordinates", - description: "Sets the coordinates of faces contained in an uploaded image and overrides the automatically detected faces. Each face is specified by the X & Y coordinates of the top left corner and the width & height of the face. The coordinates for each face are given as an array.\nRelevant for images only.", - optional: true, - }, - backgroundRemoval: { - type: "string", - label: "Background Removal", - description: "Automatically remove the background of an image using an add-on.\nSet to `cloudinary_ai` to use the deep-learning based [Cloudinary AI Background Removal](https://cloudinary.com/documentation/cloudinary_ai_background_removal_addon) add-on.\nSet to `pixelz` to use the human-powered [Pizelz Remove-The-Background Editing](https://cloudinary.com/documentation/remove_the_background_image_editing_addon) add-on service.\nRelevant for images only.", - optional: true, - }, - rawConvert: { - type: "string", - label: "Raw Convert", - description: "Asynchronously generates a related file based on the uploaded file.\n* Set to `aspose` to automatically create a PDF or other image format from a `raw` Office document using the [Aspose Document Conversion](https://cloudinary.com/documentation/aspose_document_conversion_addon) add-on.\n* Set to `google_speech` to instruct the [Google AI Video Transcription](https://cloudinary.com/documentation/google_ai_video_transcription_addon) add-on to generate an automatic transcript `raw` file from an uploaded video.\n* Set to `extract_text` to extract all the text from a PDF file and store it in a raw file. The public ID of the generated `raw` file will be in the format: **[pdf_public_id].extract_text.json.**\nSee also: [Converting raw files](https://cloudinary.com/documentation/upload_images#converting_raw_files).", - optional: true, - }, - allowedFormats: { - type: "any", - label: "Allowed Formats", - description: "An array of file formats that are allowed for uploading. Files of other types will be rejected. The formats can be any combination of image types, video formats or raw file extensions. For example: `mp4,ogv,jpg,png,pdf`. Default: any supported format for images and videos, and any kind of raw file (i.e. no restrictions by default).", - optional: true, - }, - async: { - type: "boolean", - label: "Async", - description: "Whether to perform the request in the background (asynchronously). Default: `false`.", + label: "Convert to Format", + description: "An optional format to convert the uploaded asset to before saving in the cloud, e.g. `jpg`.", optional: true, }, backup: { @@ -229,46 +66,10 @@ export default { description: "Tell Cloudinary whether to [back up](https://cloudinary.com/documentation/backups_and_version_management) the uploaded asset. Overrides the default backup settings of your account.", optional: true, }, - eval: { - type: "string", - label: "Eval", - description: "Allows you to modify upload parameters by specifying custom logic with JavaScript. This can be useful for conditionally adding tags, context, metadata or eager transformations depending on specific criteria of the uploaded file. For more details see [Evaluating and modifying upload parameters](https://cloudinary.com/documentation/analysis_on_upload#evaluating_and_modifying_upload_parameters).", - optional: true, - }, - headers: { - type: "string", - label: "Headers", - description: "An HTTP header or a list of headers lines for adding as response HTTP headers when delivering the asset to your users. Supported headers: `Link`, `Authorization`, `X-Robots-Tag`. For example: `X-Robots-Tag: noindex`.", - optional: true, - }, - invalidate: { - type: "boolean", - label: "Invalidate", - description: "Whether to invalidate CDN cached copies of a previously uploaded asset (and all transformed versions that share the same Public ID). Default: `false`.\nIt usually takes between a few seconds and a few minutes for the invalidation to fully propagate through the CDN. There are also a number of other [important considerations](https://cloudinary.com/documentation/managing_assets#invalidating_cached_media_assets_on_the_cdn) when using the invalidate functionality.", - optional: true, - }, - moderation: { - type: "string", - label: "Moderation", - description: "**For all asset types**: Set to `manual` to add the uploaded asset to a queue of pending assets that can be moderated using the Admin API or the [Cloudinary Management Console](https://cloudinary.com/console/media_library), or set to `metascan` to automatically moderate the uploaded asset using the [MetaDefender Anti-Malware Protection](https://cloudinary.com/documentation/metadefender_anti_malware_protection_addon) add-on.\n**For images only**: Set to `webpurify` or `aws_rek` to automatically moderate the uploaded image using the [WebPurify Image Moderation](https://cloudinary.com/documentation/webpurify_image_moderation_addon) add-on or the [Amazon Rekognition AI Moderation](https://cloudinary.com/documentation/aws_rekognition_ai_moderation_addon) add-on respectively.", - optional: true, - }, - notificationUrl: { - type: "string", - label: "Notification URL", - description: "An HTTP or HTTPS URL to receive the upload response (a webhook) when the upload or any requested asynchronous action is completed. If not specified, the response is sent to the global **Notification URL** (if defined) in the **Upload** settings of your account console.", - optional: true, - }, - proxy: { - type: "string", - label: "Proxy", - description: "Tells Cloudinary to upload assets from remote URLs through the given proxy. Format: `https://hostname:port.`", - optional: true, - }, - returnDeleteToken: { - type: "boolean", - label: "Return Deleted Token", - description: "Whether to return a deletion token in the upload response. The token can be used to delete the uploaded asset within 10 minutes using an unauthenticated API request. Default: `false`.", + additionalOptions: { + type: "object", + label: "Additional Options", + description: "Additional parameters and their values to use in the upload. [See the documentation](https://cloudinary.com/documentation/image_upload_api_reference#upload_method) for all available options. Values will be parsed as JSON where applicable. Example: `{ \"use_filename\": true }`", optional: true, }, }, @@ -276,55 +77,38 @@ export default { const options = { public_id: this.publicId, folder: this.folder, - use_filename: this.useFilename, - unique_filename: this.uniqueFilename, resource_type: this.resourceType, type: this.type, - access_control: this.accessControl, access_mode: this.accessMode, - discard_original_filename: this.discardOriginalFilename, - overwrite: this.overwrite, tags: this.tags, - context: this.context, - colors: this.colors, - faces: this.faces, - quality_analysis: this.qualityAnalysis, - accessibility_analysis: this.accessibilityAnalysis, - cinemagraph_analysis: this.cinemagraphAnalysis, - image_metadata: this.imageMetadata, - phash: this.phash, - responsive_breakpoints: this.responsiveBreakpoints, - auto_tagging: this.autoTagging, - categorization: this.categorization, - detection: this.detection, - ocr: this.ocr, - eager: this.eager, - eager_async: this.eagerAsync, - eager_notification_url: this.eagerNotificationUrl, - transformation: this.transformation, format: this.format, - custom_coordinates: this.customCoordinates, - face_coordinates: this.faceCoordinates, - background_removal: this.backgroundRemoval, - raw_convert: this.rawConvert, - allowed_formats: this.allowedFormats, - async: this.async, backup: this.backup, - eval: this.eval, - headers: this.headers, - invalidate: this.invalidate, - moderation: this.moderation, - notification_url: this.notification_url, - proxy: this.proxy, - return_delete_token: this.returnDeleteToken, + ...Object.fromEntries(Object.entries(this.additionalOptions ?? {}).map(([ + key, + value, + ]) => { + try { + return [ + key, + JSON.parse(value), + ]; + } catch (err) { + return [ + key, + value, + ]; + } + })), }; - const response = await this.cloudinary.uploadMedia(this.file, options); - - if (response) { - $.export("$summary", "Successfully uploaded media asset"); + try { + const response = await this.cloudinary.uploadMedia(this.file, options); + if (response) { + $.export("$summary", "Successfully uploaded media asset"); + } + return response; + } catch (err) { + throw new Error(`Cloudinary error response: ${err.error?.message ?? JSON.stringify(err)}`); } - - return response; }, }; diff --git a/components/cloudinary/cloudinary.app.mjs b/components/cloudinary/cloudinary.app.mjs index ce1be88cb1fb9..9b22ba9b49d17 100644 --- a/components/cloudinary/cloudinary.app.mjs +++ b/components/cloudinary/cloudinary.app.mjs @@ -8,7 +8,7 @@ export default { resourceType: { type: "string", label: "Resource Type", - description: "The type of asset. Defaults to `image` if left blank", + description: "The type of asset. Defaults to `image` if not specified. `Note:` use video for all video and audio assets, such as `.mp3`. ", options: constants.RESOURCE_TYPE_OPTIONS, default: "image", optional: true, @@ -31,8 +31,8 @@ export default { }, deliveryType: { type: "string", - label: "Type", - description: "The delivery type. Defaults to `upload` if left blank", + label: "Filter by Type", + description: "Find assets with the specified delivery type (defaults to `upload`).", options: constants.DELIVERY_TYPE_OPTIONS, default: "upload", optional: true, @@ -40,7 +40,7 @@ export default { uploadDeliveryType: { type: "string", label: "Type", - description: "The delivery type. Allows uploading assets as `private` or `authenticated` instead of the default `upload` mode. Valid values: `upload`, `private` and `authenticated`. Default: `upload`.", + description: "The delivery type. Allows uploading assets as `private` or `authenticated` instead of the default `upload` mode.", options: constants.UPLOAD_DELIVERY_TYPE_OPTIONS, default: "upload", optional: true, @@ -48,10 +48,45 @@ export default { accessMode: { type: "string", label: "Access Mode", - description: "Allows the asset to behave as if it's of the authenticated 'type' (see above) while still using the default 'upload' type in delivery URLs. The asset can later be made public by changing its access_mode via the [Admin API](https://cloudinary.com/documentation/admin_api#update_access_mode), without having to update any delivery URLs. Valid values: `public`, and `authenticated`. Default: `public`.", + description: "Allows the asset to behave as if it's of the authenticated 'type'. Default is `public`. The asset can later be made public by changing its Access Mode via the [Admin API](https://cloudinary.com/documentation/admin_api#update_access_mode), without having to update any delivery URLs.", optional: true, options: constants.ACCESS_MODE_OPTIONS, }, + assetId: { + type: "string", + label: "Public ID", + description: "The [public ID](https://cloudinary.com/documentation/upload_images#public_id) of the asset , e.g. `folder/filename`.", + }, + transformations: { + type: "object", + label: "Additional Transformations", + description: "Additional transformations to apply to the resource. [See the documentation](https://cloudinary.com/documentation/transformation_reference) for all available transformations. Example: `{ \"angle\": 90, \"color_space\": \"srgb\"}`", + }, + transformationString: { + type: "string", + label: "Transformation String", + description: "A string representing the transformation to apply to the resource. You can use the [Cloudinary Transformation Builder](https://tx.cloudinary.com/) to create and preview the transformation, then copy the string here. Example: `c_fill,h_500,w_500` is the transformation string in the URL `https://res.cloudinary.com/demo/video/upload/c_fill,h_500,w_500/samples/cld-sample-video.mp4`", + optional: true, + }, + namedTransformation: { + type: "string", + label: "Named Transformation", + description: "Select a pre-configured named transformation to apply to the resource. You can create and manage transformations in the [Cloudinary Transformation Builder](https://tx.cloudinary.com).", + optional: true, + async options({ prevContext: { cursor } }) { + const { + transformations, next_cursor, + } = await this.getTransformations({ + next_cursor: cursor, + }); + return { + options: transformations?.filter?.((t) => t.named).map((t) => t.name.replace(/^t_/, "")), + context: { + cursor: next_cursor, + }, + }; + }, + }, }, methods: { _client() { @@ -69,14 +104,14 @@ export default { async getUsage(options) { return this._client().api.usage(options); }, - async transformImage(imageSource, options) { - return this._client().image(imageSource, options); - }, - async transformVideo(videoPublicId, options) { - return this._client().video(videoPublicId, options); + async transformAsset(imageSource, options) { + return this._client().url(imageSource, options); }, async uploadMedia(file, options) { return this._client().uploader.upload(file, options); }, + async getTransformations(args) { + return this._client().api.transformations(args); + }, }, }; diff --git a/components/cloudinary/package.json b/components/cloudinary/package.json index e8cda21d4af49..42cacde9d0114 100644 --- a/components/cloudinary/package.json +++ b/components/cloudinary/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/cloudinary", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Cloudinary Components", "main": "cloudinary.app.mjs", "keywords": [ @@ -13,6 +13,6 @@ "access": "public" }, "dependencies": { - "cloudinary": "^1.36.1" + "cloudinary": "^2.5.1" } } diff --git a/components/cloudlayer/README.md b/components/cloudlayer/README.md index b5a8b7f8a3d32..1d6d3d4333613 100644 --- a/components/cloudlayer/README.md +++ b/components/cloudlayer/README.md @@ -1,9 +1,11 @@ # Overview -You can use Cloudlayer to build a variety of applications, including: +The Cloudlayer API transforms HTML or URLs into PDFs or images, enabling seamless creation of documents and screenshots programmatically. When integrated with Pipedream, you can automate document generation and distribution workflows, trigger PDF conversions based on specific actions within your apps, or archive web content as images for compliance or monitoring purposes. It's a powerful tool for generating reports, invoices, or snapshots of digital content without manual intervention. -- Web applications -- Mobile applications -- Desktop applications -- IoT applications -- AI/ML applications +# Example Use Cases + +- **Automated Invoice Generation and Distribution**: Create invoices as PDFs using HTML templates on Cloudlayer whenever a new sale is recorded in your e-commerce platform (like Shopify). Then, use Pipedream to email the invoice directly to the customer or save it to a cloud storage service such as Dropbox for record-keeping. + +- **Dynamic Report Creation for Analytics Data**: On a schedule, fetch analytics data from your preferred platform (e.g., Google Analytics), generate a PDF report via Cloudlayer, and share it with stakeholders via a Slack message or email. This ensures your team stays updated with the latest performance metrics without manual report generation. + +- **Website Monitoring and Archiving**: Check your website periodically for uptime or content changes using a Pipedream cron workflow. Use Cloudlayer to capture screenshots and store them in Google Drive or another storage solution. This can serve as a historical archive or a tool for monitoring visual changes on your site over time. diff --git a/components/cloudmersive/README.md b/components/cloudmersive/README.md index 1c19aa9b1eba0..8845458f77695 100644 --- a/components/cloudmersive/README.md +++ b/components/cloudmersive/README.md @@ -1,20 +1,11 @@ # Overview -Cloudmersive provides many different APIs that can be used to build all sorts -of applications and integrations. Here are some examples: +The Cloudmersive API provides powerful tools for handling tasks like document conversion, data validation, virus scanning, and natural language processing. Integrating Cloudmersive with Pipedream allows you to automate complex processes with these abilities as triggers or steps in multi-stage workflows. This integration empowers you to streamline operations like sanitizing uploads before they hit your storage, parsing and converting user-submitted documents on-the-fly, or enhancing user engagement by leveraging language utilities. -- [Cloudmersive Image Recognition - API](https://cloudmersive.com/image-recognition-api): The Cloudmersive Image - Recognition API offers a wide range of image recognition and classification - capabilities, including face detection, object identification, and text - recognition. -- [Cloudmersive Converter API](https://cloudmersive.com/converter-api): The - Cloudmersive Converter API provides a simple way to convert between different - file formats, including images, documents, and videos. -- [Cloudmersive OCR API](https://cloudmersive.com/ocr-api): The Cloudmersive - OCR API provides optical character recognition (OCR) capabilities, making it - easy to extract text from images and documents. -- [Cloudmersive Text Analysis API](https://cloudmersive.com/text-analysis-api): - The Cloudmersive Text Analysis API provides a wide range of text analysis and - natural language processing capabilities, including sentiment analysis, topic - classification, and named entity recognition. +# Example Use Cases + +- **Automated Document Conversion for Cloud Storage:** When a document is uploaded to a cloud storage service like Dropbox, trigger a workflow that uses Cloudmersive to convert the file into a different format (e.g., DOCX to PDF). Once converted, the new file can be automatically saved back to Dropbox or another preferred storage solution. + +- **Email Attachment Sanitization and Virus Scanning:** Upon receiving an email with attachments through a service like Gmail, trigger a workflow that passes the attachments to Cloudmersive for virus scanning and sanitization. Clean files can then be safely stored in a database or sent to a Slack channel for immediate team access. + +- **Real-time Language Translation for Customer Support:** Integrate Cloudmersive's language API within a customer support platform workflow, such as Zendesk. When a non-English support ticket is submitted, use Cloudmersive to translate the content and then immediately provide the translation to the support team, enabling them to respond quickly without language barriers. diff --git a/components/cloudpresenter/actions/create-contact/create-contact.mjs b/components/cloudpresenter/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..2d206a3d19a33 --- /dev/null +++ b/components/cloudpresenter/actions/create-contact/create-contact.mjs @@ -0,0 +1,114 @@ +import cloudpresenter from "../../cloudpresenter.app.mjs"; +import { + getCustomFieldProps, parseCustomFields, +} from "../../common/utils.mjs"; + +export default { + key: "cloudpresenter-create-contact", + name: "Create Contact", + description: "Creates a new contact within the Cloudpresenter application. [See the documentation](https://cloudpresenter.stoplight.io/docs/cloudpresenter-public-apis/gnglqnrsy7k38-create-contact)", + version: "0.0.1", + type: "action", + props: { + cloudpresenter, + firstName: { + propDefinition: [ + cloudpresenter, + "firstName", + ], + }, + lastName: { + propDefinition: [ + cloudpresenter, + "lastName", + ], + }, + email: { + propDefinition: [ + cloudpresenter, + "email", + ], + }, + company: { + propDefinition: [ + cloudpresenter, + "company", + ], + }, + jobTitle: { + propDefinition: [ + cloudpresenter, + "jobTitle", + ], + }, + streetAddress: { + propDefinition: [ + cloudpresenter, + "streetAddress", + ], + }, + city: { + propDefinition: [ + cloudpresenter, + "city", + ], + }, + state: { + propDefinition: [ + cloudpresenter, + "state", + ], + }, + country: { + propDefinition: [ + cloudpresenter, + "country", + ], + }, + phone: { + propDefinition: [ + cloudpresenter, + "phone", + ], + }, + tagIds: { + propDefinition: [ + cloudpresenter, + "tagIds", + ], + }, + customFieldIds: { + propDefinition: [ + cloudpresenter, + "customFieldIds", + ], + reloadProps: true, + }, + }, + async additionalProps() { + return getCustomFieldProps(this); + }, + async run({ $ }) { + const response = await this.cloudpresenter.createContact({ + $, + data: { + contact: { + first_name: this.firstName, + last_name: this.lastName, + email: this.email, + company: this.company || null, + job_title: this.jobTitle || null, + address: this.streetAddress || null, + city: this.city || null, + state: this.state || null, + country: this.country || null, + phone_number: this.phone || null, + tags: this.tagIds || [], + custom_fields: parseCustomFields(this), + }, + }, + }); + $.export("$summary", `Successfully created contact: ${this.firstName} ${this.lastName}`); + return response; + }, +}; diff --git a/components/cloudpresenter/actions/update-contact/update-contact.mjs b/components/cloudpresenter/actions/update-contact/update-contact.mjs new file mode 100644 index 0000000000000..c077cc420e750 --- /dev/null +++ b/components/cloudpresenter/actions/update-contact/update-contact.mjs @@ -0,0 +1,141 @@ +import cloudpresenter from "../../cloudpresenter.app.mjs"; +import { + getCustomFieldProps, getPaginatedResources, parseCustomFields, +} from "../../common/utils.mjs"; + +export default { + key: "cloudpresenter-update-contact", + name: "Update Contact", + description: "Updates an existing contact within the Cloudpresenter application. [See the documentation](https://cloudpresenter.stoplight.io/docs/cloudpresenter-public-apis/tjbk1nm3qvbg2-update-contact)", + version: "0.0.1", + type: "action", + props: { + cloudpresenter, + contactId: { + propDefinition: [ + cloudpresenter, + "contactId", + ], + }, + firstName: { + propDefinition: [ + cloudpresenter, + "firstName", + ], + optional: true, + }, + lastName: { + propDefinition: [ + cloudpresenter, + "lastName", + ], + optional: true, + }, + email: { + propDefinition: [ + cloudpresenter, + "email", + ], + optional: true, + }, + company: { + propDefinition: [ + cloudpresenter, + "company", + ], + }, + jobTitle: { + propDefinition: [ + cloudpresenter, + "jobTitle", + ], + }, + streetAddress: { + propDefinition: [ + cloudpresenter, + "streetAddress", + ], + }, + city: { + propDefinition: [ + cloudpresenter, + "city", + ], + }, + state: { + propDefinition: [ + cloudpresenter, + "state", + ], + }, + country: { + propDefinition: [ + cloudpresenter, + "country", + ], + }, + phone: { + propDefinition: [ + cloudpresenter, + "phone", + ], + }, + tagIds: { + propDefinition: [ + cloudpresenter, + "tagIds", + ], + }, + customFieldIds: { + propDefinition: [ + cloudpresenter, + "customFieldIds", + ], + reloadProps: true, + }, + }, + async additionalProps() { + return getCustomFieldProps(this); + }, + async run({ $ }) { + const contacts = await getPaginatedResources({ + resourceFn: this.cloudpresenter.listContacts, + resourceType: "contacts", + }); + const contact = contacts.find(({ uuid }) => uuid === this.contactId); + contact.tags = contact?.tags + ? contact.tags.map(({ id }) => id) + : []; + contact.custom_fields = contact?.custom_fields + ? contact.custom_fields.filter((field) => field.custom_field).map((field) => ({ + id: field.custom_field.id, + value: field.value, + })) + : []; + + const response = await this.cloudpresenter.updateContact({ + $, + contactId: this.contactId, + data: { + contact: { + first_name: this.firstName || contact.first_name, + last_name: this.lastName || contact.last_name, + email: this.email || contact.email, + company: this.company || contact.company, + job_title: this.jobTitle || contact.job_title, + address: this.streetAddress || contact.address, + city: this.city || contact.city, + state: this.state || contact.state, + country: this.country || contact.country, + phone_number: this.phone || contact.phone, + tags: this.tagIds || contact.tags, + custom_fields: this.customFieldIds + ? parseCustomFields(this) + : contact.custom_fields, + }, + }, + }); + $.export("$summary", `Successfully updated contact with ID: ${this.contactId}`); + return response; + }, +}; diff --git a/components/cloudpresenter/cloudpresenter.app.mjs b/components/cloudpresenter/cloudpresenter.app.mjs new file mode 100644 index 0000000000000..51a186b50322c --- /dev/null +++ b/components/cloudpresenter/cloudpresenter.app.mjs @@ -0,0 +1,175 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "cloudpresenter", + propDefinitions: { + contactId: { + type: "string", + label: "Contact UUID", + description: "Unique identifier for the contact", + async options({ page }) { + const { data } = await this.listContacts({ + params: { + page: page + 1, + }, + }); + if (!data?.contacts) { + return []; + } + return data.contacts.data?.map(({ + uuid: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + tagIds: { + type: "string[]", + label: "Tag IDs", + description: "Unique identifiers for the tags to associate with the contact", + optional: true, + async options({ page }) { + const { data } = await this.listTags({ + params: { + page: page + 1, + }, + }); + return data?.map(({ + id: value, tag_name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + customFieldIds: { + type: "integer[]", + label: "Custom Field IDs", + description: "The IDs of custom fields to add to the contact", + optional: true, + async options() { + const { data } = await this.listCustomFields(); + return data?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the contact", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the contact", + }, + email: { + type: "string", + label: "Email", + description: "The email address of the contact", + }, + company: { + type: "string", + label: "Company", + description: "The company of the contact", + optional: true, + }, + jobTitle: { + type: "string", + label: "Job Title", + description: "The job title of the contact", + optional: true, + }, + streetAddress: { + type: "string", + label: "Street Address", + description: "The street address of the contact", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "The city of the contact", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "The state of the contact", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "The country of the contact", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The phone number of the contact", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api2.cloudpresenter.com/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "origin": `${this.$auth.api_base_url}`, + "workspace": `${this.$auth.workspace_id}`, + }, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts", + ...opts, + }); + }, + listTags(opts = {}) { + return this._makeRequest({ + path: "/tags", + ...opts, + }); + }, + listCustomFields(opts = {}) { + return this._makeRequest({ + path: "/custom-fields/all", + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contact", + ...opts, + }); + }, + updateContact({ + contactId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/contact/${contactId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/cloudpresenter/common/utils.mjs b/components/cloudpresenter/common/utils.mjs new file mode 100644 index 0000000000000..aa6438a1af939 --- /dev/null +++ b/components/cloudpresenter/common/utils.mjs @@ -0,0 +1,77 @@ +async function getPaginatedResources(args) { + const items = paginate(args); + const results = []; + for await (const item of items) { + results.push(item); + } + return results; +} + +async function *paginate({ + resourceFn, + params = {}, + resourceType, + max, +}) { + params = { + ...params, + page: 1, + }; + let lastPage = 1; + let count = 0; + do { + const { data } = await resourceFn({ + params, + }); + const items = data[resourceType].data; + for (const item of items) { + yield item; + count++; + if (max && count >= max) { + return; + } + } + lastPage = data[resourceType].last_page; + params.page++; + } while (params.page <= lastPage); +} + +async function getCustomFieldProps(ctx) { + const props = {}; + if (!ctx.customFieldIds?.length) { + return props; + } + const { data } = await ctx.cloudpresenter.listCustomFields(); + const customFieldLabels = {}; + for (const field of data) { + customFieldLabels[field.id] = field.name; + } + for (const id of ctx.customFieldIds) { console.log(id); + const fieldLabel = customFieldLabels[`${id}`]; console.log(fieldLabel); + props[`customField-${id}`] = { + type: "string", + label: `Value of ${fieldLabel}`, + }; + } + return props; +} + +function parseCustomFields(ctx) { + const customFields = []; + if (!ctx.customFieldIds?.length) { + return customFields; + } + for (const id of ctx.customFieldIds) { + customFields.push({ + id, + value: ctx[`customField-${id}`], + }); + } + return customFields; +} + +export { + getPaginatedResources, + getCustomFieldProps, + parseCustomFields, +}; diff --git a/components/cloudpresenter/package.json b/components/cloudpresenter/package.json new file mode 100644 index 0000000000000..56fb29a1c2dad --- /dev/null +++ b/components/cloudpresenter/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/cloudpresenter", + "version": "0.1.0", + "description": "Pipedream Cloudpresenter Components", + "main": "cloudpresenter.app.mjs", + "keywords": [ + "pipedream", + "cloudpresenter" + ], + "homepage": "https://pipedream.com/apps/cloudpresenter", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/cloudpresenter/sources/new-contact-created/new-contact-created.mjs b/components/cloudpresenter/sources/new-contact-created/new-contact-created.mjs new file mode 100644 index 0000000000000..1f83d5d941198 --- /dev/null +++ b/components/cloudpresenter/sources/new-contact-created/new-contact-created.mjs @@ -0,0 +1,53 @@ +import cloudpresenter from "../../cloudpresenter.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import { getPaginatedResources } from "../../common/utils.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "cloudpresenter-new-contact-created", + name: "New Contact Created", + description: "Emit new event when a user adds a new contact in Cloudpresenter.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + cloudpresenter, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + generateMeta(contact) { + return { + id: contact.id, + summary: `New Contact: ${contact.name}`, + ts: Date.now(), + }; + }, + async processEvent(max) { + const items = await getPaginatedResources({ + resourceFn: this.cloudpresenter.listContacts, + resourceType: "contacts", + max, + }); + + for (const item of items) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + } + }, + }, + async run() { + await this.processEvent(); + }, + sampleEmit, +}; diff --git a/components/cloudpresenter/sources/new-contact-created/test-event.mjs b/components/cloudpresenter/sources/new-contact-created/test-event.mjs new file mode 100644 index 0000000000000..90449de23411b --- /dev/null +++ b/components/cloudpresenter/sources/new-contact-created/test-event.mjs @@ -0,0 +1,42 @@ +export default { + "workspace_id": 1922, + "first_name": "John", + "last_name": "Smith", + "email": "john@smith.co", + "company": null, + "job_title": null, + "address": null, + "city": null, + "state": null, + "country": null, + "phone_number": null, + "id": 128964, + "uuid": "bf8515a8-a582-40d8-9865-76d274d92b56", + "name": "John Smith", + "tags": [], + "custom_fields": [], + "owner": { + "first_name": null, + "last_name": null, + "email": "john@smith.co", + "phone_number": null, + "signup_type": "GENERAL", + "id": 2038, + "uuid": "3495df3c-d3e4-4c95-8cb8-de52033dfc47", + "created_at": "2024-07-23T17:48:26.000000Z", + "updated_at": "2024-07-23T18:14:29.000000Z", + "created_by": null, + "updated_by": null, + "deleted_at": null, + "deleted_by": null, + "is_locked_sidebar": false, + "birthday": null, + "phone_code": null, + "stripe_customer_id": null, + "name": " ", + "is_password_set": true, + "profile_completed": 50, + "media": null + }, + "media": null +} \ No newline at end of file diff --git a/components/cloudpress/README.md b/components/cloudpress/README.md new file mode 100644 index 0000000000000..0abcf98ad2c43 --- /dev/null +++ b/components/cloudpress/README.md @@ -0,0 +1,11 @@ +# Overview + +The Cloudpress API lets you convert rich text content into various formats. With Pipedream, you can use this API to automate the process of converting and distributing content across different platforms. Think of it as your bridge between content creation and content publication, streamlining workflows that involve any sort of text formatting, conversion, or distribution. + +# Example Use Cases + +- **Automated Blog Publishing**: Use Cloudpress to convert documents into HTML and then publish them to your blog. Trigger a workflow when a new document is added to a cloud storage service like Google Drive, convert it to HTML using Cloudpress, and post it on your CMS, like WordPress. + +- **Content Syndication**: Spread your content across multiple platforms. When you publish a new article, have Cloudpress convert the content, and then use Pipedream to share it on social media platforms like LinkedIn, Twitter, or Facebook. + +- **Newsletter Creation**: Take the pain out of newsletter formatting. When your content is ready, convert it to the desired format with Cloudpress, and then send it out with an email service like SendGrid, all within a Pipedream workflow. diff --git a/components/cloudtables/README.md b/components/cloudtables/README.md index 4c5d0182916eb..36abfff76bc62 100644 --- a/components/cloudtables/README.md +++ b/components/cloudtables/README.md @@ -1,10 +1,11 @@ # Overview -CloudTables is a great API for building cloud-based applications. With it, you -can easily store, query, and update data in the cloud. Here are some examples -of what you can build with CloudTables: - -- A cloud-based task manager -- A cloud-based to-do list -- A cloud-based notes app -- A cloud-based contact manager +CloudTables API allows the creation and manipulation of sophisticated and dynamic data tables in the cloud. With this API, you can manage table schemas, insert, update, and fetch data in real time, and control access with fine-grained permissions. Leveraging CloudTables on Pipedream, you can automate data flows, sync with other services, and respond to events—think of it as supercharging your data tables with the power of integration and automation. + +# Example Use Cases + +- **Sales Pipeline Management**: Automate the update of a sales pipeline in CloudTables by triggering workflows on Pipedream when new leads are captured in a CRM like Salesforce. Sync new lead details to a specific table, ensuring sales reps have the latest information at their fingertips. + +- **Inventory Tracking System**: Connect CloudTables to an eCommerce platform like Shopify. When a product's stock level changes, use Pipedream to update the inventory table in CloudTables automatically. This keeps inventory data accurate across systems without manual intervention. + +- **Event Registration Coordination**: Tie in a CloudTables table with an event management app like Eventbrite using Pipedream. When attendees register for an event, add their information to a table. Then, use this data to trigger follow-up emails or create personalized itineraries. diff --git a/components/cloudtables/package.json b/components/cloudtables/package.json new file mode 100644 index 0000000000000..a8f9068299d42 --- /dev/null +++ b/components/cloudtables/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/cloudtables", + "version": "0.6.0", + "description": "Pipedream cloudtables Components", + "main": "cloudtables.app.mjs", + "keywords": [ + "pipedream", + "cloudtables" + ], + "homepage": "https://pipedream.com/apps/cloudtables", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/cloudtalk/README.md b/components/cloudtalk/README.md index 36ca1eea0f318..09c3c3cabe261 100644 --- a/components/cloudtalk/README.md +++ b/components/cloudtalk/README.md @@ -1,12 +1,11 @@ # Overview -CloudTalk provides a reliable and simple API for building VoIP applications. -With CloudTalk you can: - -- Make and receive phone calls -- Play audio and video files -- Record phone calls -- Transcribe phone calls -- Control call flow -- Handle call events -- And much more! +The CloudTalk API facilitates advanced telephony capabilities, letting you streamline communication workflows, from call tracking and analytics to automating customer support processes. With Pipedream's integration, you can effortlessly connect CloudTalk to a myriad of other apps and services, enabling you to automate actions based on call events, synchronize customer data across platforms, and trigger communication sequences without manual intervention. + +# Example Use Cases + +- **Synchronize Call Data with CRM**: Automatically log CloudTalk call details in your CRM system like Salesforce or HubSpot. After every call, a Pipedream workflow can be triggered to create or update a contact’s record with the call summary, duration, and any custom metadata, ensuring your sales team has the latest information at their fingertips. + +- **Real-time Customer Support Ticket Creation**: Implement a workflow that creates a support ticket in Zendesk or another ticketing system whenever a support call ends on CloudTalk. This workflow can attach call transcripts, customer details, and prioritize the ticket based on keywords mentioned during the call, streamlining your support team’s follow-up process. + +- **Automated SMS Follow-Ups**: Use Twilio's SMS service to send follow-up messages to customers after a call concludes. A Pipedream workflow can use the call outcome or duration to tailor the message, whether it's a thank-you note, a survey for feedback, or a reminder for the next steps, fostering better customer relationships and service. diff --git a/components/cloze/.gitignore b/components/cloze/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/cloze/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/cloze/README.md b/components/cloze/README.md new file mode 100644 index 0000000000000..6521e33c0d5ca --- /dev/null +++ b/components/cloze/README.md @@ -0,0 +1,11 @@ +# Overview + +The Cloze API enables you to access and manage your Cloze CRM data programmatically. In Pipedream, you can create powerful, serverless workflows that react to events or run on schedules to automate tasks involving Cloze data. You could synchronize contacts, track communication history, or trigger actions based on updates in Cloze. By leveraging Pipedream's capacity to connect to a myriad of services, you can create multi-step workflows that involve other apps to streamline your business processes and harness the full potential of CRM automation. + +# Example Use Cases + +- **Sync New Contacts to a Google Sheet**: Whenever you add a new contact in Cloze, this workflow triggers and appends their details to a Google Sheet. It's useful for maintaining a backup or for sharing contact information with team members who prefer working directly from spreadsheets. + +- **Create Cloze Tasks from Incoming Emails**: This workflow monitors an email inbox (like Gmail) for specific keywords or senders using Pipedream's built-in email trigger. When a matching email arrives, it automatically creates a task in Cloze, ensuring that you follow up on important messages promptly without manual intervention. + +- **Notify Team on Slack for Cloze Pipeline Updates**: Stay updated on your sales pipeline without constantly checking Cloze. This workflow listens for updates to deals in Cloze and sends a notification with the deal details to a designated Slack channel. This keeps your team informed in real time about potential opportunities or actions required. diff --git a/components/cloze/actions/create-note/create-note.mjs b/components/cloze/actions/create-note/create-note.mjs new file mode 100644 index 0000000000000..71e6b7059366e --- /dev/null +++ b/components/cloze/actions/create-note/create-note.mjs @@ -0,0 +1,81 @@ +import app from "../../cloze.app.mjs"; + +export default { + key: "cloze-create-note", + name: "Create Note", + description: "Creates a note in Cloze. [See the documentation](https://api.cloze.com/api-docs/#!/Content/post_v1_createcontent).", + version: "0.0.1", + type: "action", + props: { + app, + uniqueId: { + type: "string", + label: "Unique ID", + description: "A unique identifier for this content record. This will often be the unique Id in an external system so that updates can be matched up with the record in Cloze.", + }, + source: { + type: "string", + label: "Source", + description: "The source that this content record originally came from (Eg. `todoist.com`). Must be a valid domain.", + }, + date: { + type: "string", + label: "Date", + description: "When the content should show up in the timeline. Can be a string or a UTC timestamp in ms since the epoch. Eg. `2021-01-01` or `1609459200000`.", + optional: true, + }, + from: { + type: "string", + label: "From", + description: "From address for this content record (the address of the person created the record). This can be an email address, phone number, social handle or app link (Eg. `na16.salesforce.com:006j000000Pkp1d`)", + optional: true, + }, + subject: { + type: "string", + label: "Subject", + description: "Subject of the communication record.", + optional: true, + }, + body: { + type: "string", + label: "Body", + description: "Body text of the communication record.", + }, + additionalData: { + type: "object", + label: "Additional Data", + description: "Additional details for the note in JSON format. [See the documentation](https://api.cloze.com/api-docs/#!/Content/post_v1_createcontent).", + optional: true, + }, + }, + async run({ $ }) { + const { + app, + uniqueId, + date, + from, + source, + subject, + body, + additionalData, + } = this; + + const response = await app.addContentRecord({ + $, + data: { + uniqueid: uniqueId, + date, + style: "note", + from, + source, + subject, + body, + ...additionalData, + }, + }); + + $.export("$summary", "Successfully created note."); + + return response; + }, +}; diff --git a/components/cloze/actions/create-update-company/create-update-company.mjs b/components/cloze/actions/create-update-company/create-update-company.mjs new file mode 100644 index 0000000000000..06ed206a4bdfd --- /dev/null +++ b/components/cloze/actions/create-update-company/create-update-company.mjs @@ -0,0 +1,148 @@ +import app from "../../cloze.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "cloze-create-update-company", + name: "Create Or Update Company", + description: "Create a new company or enhance an existing company within Cloze. Companies can be created with just a domain name or both a name and another unique identifier such as a phone number and email address. [See the documentation](https://api.cloze.com/api-docs/#!/Relations_-_Companies/post_v1_companies_create).", + version: "0.0.1", + type: "action", + props: { + app, + name: { + type: "string", + label: "Company Name", + description: "The name of the company.", + optional: true, + }, + emails: { + type: "string[]", + label: "Emails", + description: "The emails of the company. Each email should be a JSON object with `value` key. [See the documentation](https://api.cloze.com/api-docs/#!/Relations_-_Companies/post_v1_companies_create).", + optional: true, + }, + phones: { + type: "string[]", + label: "Phones", + description: "The phones of the company. Each phone should be a JSON object with `value` key. [See the documentation](https://api.cloze.com/api-docs/#!/Relations_-_Companies/post_v1_companies_create).", + optional: true, + }, + domains: { + type: "string[]", + label: "Domains", + description: "The domains of the company.", + optional: true, + }, + segment: { + type: "string", + label: "Segment", + description: "The segment of the company.", + optional: true, + options: [ + "customer", + "partner", + "supplier", + "investor", + "advisor", + "competitor", + "custom1", + "custom2", + "custom3", + "custom4", + "custom5", + "coworker", + "family", + "friend", + "network", + "personal1", + "personal2", + ], + }, + step: { + type: "string", + label: "Step", + description: "Unique Id of Next Step", + optional: true, + }, + stage: { + type: "string", + label: "Stage", + description: "The stage of the company.", + optional: true, + options: [ + { + label: "Lead Stage", + value: "lead", + }, + { + label: "Potential Stage", + value: "future", + }, + { + label: "Active Stage", + value: "current", + }, + { + label: "Inactive Stage", + value: "past", + }, + { + label: "Lost Stage", + value: "out", + }, + ], + }, + assignTo: { + type: "string", + label: "Assign To", + description: "Assign this company to this team member.", + optional: true, + }, + additionalData: { + type: "object", + label: "Additional Data", + description: "Additional details for the company in JSON format. [See the documentation](https://api.cloze.com/api-docs/#!/Relations_-_Companies/post_v1_companies_create).", + optional: true, + }, + }, + methods: { + createCompany(args = {}) { + return this.app.post({ + path: "/companies/create", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createCompany, + name, + emails, + phones, + domains, + segment, + step, + stage, + assignTo, + additionalData, + } = this; + + const response = await createCompany({ + $, + data: { + name, + emails: utils.parseArray(emails), + phones: utils.parseArray(phones), + domains, + segment, + step, + stage, + assignTo, + ...additionalData, + }, + }); + + $.export("$summary", "Successfully created/updated company."); + return response; + }, +}; diff --git a/components/cloze/actions/create-update-project/create-update-project.mjs b/components/cloze/actions/create-update-project/create-update-project.mjs new file mode 100644 index 0000000000000..ab25a8ea2cc7c --- /dev/null +++ b/components/cloze/actions/create-update-project/create-update-project.mjs @@ -0,0 +1,115 @@ +import app from "../../cloze.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "cloze-create-update-project", + name: "Create Or Update Project", + description: "Create a new project or merge updates into an existing one. [See the documentation](https://api.cloze.com/api-docs/#!/Relations_-_Projects/post_v1_projects_create).", + version: "0.0.1", + type: "action", + props: { + app, + name: { + type: "string", + label: "Project Name", + description: "The name of the project.", + }, + appLinks: { + type: "string[]", + label: "App Links", + description: "The app links of the project. Each app link should be a JSON object with at least `source` and `uniqueid` keys. [See the documentation](https://api.cloze.com/api-docs/#!/Relations_-_Projects/post_v1_projects_create).", + optional: true, + default: [ + JSON.stringify({ + source: "na16.salesforce.com", + uniqueid: "sdf234v", + }), + ], + }, + summary: { + type: "string", + label: "Project Summary", + description: "The summary of the project.", + optional: true, + }, + stage: { + type: "string", + label: "Stage", + description: "The stage of the project.", + optional: true, + options: [ + { + label: "Potential Stage", + value: "future", + }, + { + label: "Active Stage", + value: "current", + }, + { + label: "Won or Done stage", + value: "won", + }, + { + label: "Lost Stage", + value: "lost", + }, + ], + }, + segment: { + type: "string", + label: "Segment", + description: "The segment of the project.", + optional: true, + options: [ + "project", + "project1", + "project2", + "project3", + "project4", + "project5", + ], + }, + additionalData: { + type: "object", + label: "Additional Data", + description: "Additional details for the project in JSON format. [See the documentation](https://api.cloze.com/api-docs/#!/Relations_-_Projects/post_v1_projects_create).", + optional: true, + }, + }, + methods: { + createProject(args = {}) { + return this.app.post({ + path: "/projects/create", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createProject, + name, + appLinks, + summary, + stage, + segment, + additionalData, + } = this; + + const response = await createProject({ + $, + data: { + name, + appLinks: utils.parseArray(appLinks), + summary, + stage, + segment, + ...additionalData, + }, + }); + + $.export("$summary", "Successfully created/updated project."); + + return response; + }, +}; diff --git a/components/cloze/app/cloze.app.ts b/components/cloze/app/cloze.app.ts deleted file mode 100644 index d510120638d18..0000000000000 --- a/components/cloze/app/cloze.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "cloze", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/cloze/cloze.app.mjs b/components/cloze/cloze.app.mjs new file mode 100644 index 0000000000000..cb6a97dfde6d8 --- /dev/null +++ b/components/cloze/cloze.app.mjs @@ -0,0 +1,45 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "cloze", + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + ...headers, + }; + }, + async _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + const response = await axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + + if (response.errorcode) { + throw new Error(JSON.stringify(response, null, 2)); + } + + return response; + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + addContentRecord(args = {}) { + return this.post({ + path: "/createcontent", + ...args, + }); + }, + }, +}; diff --git a/components/cloze/common/constants.mjs b/components/cloze/common/constants.mjs new file mode 100644 index 0000000000000..24ae1690ba99f --- /dev/null +++ b/components/cloze/common/constants.mjs @@ -0,0 +1,9 @@ +const BASE_URL = "https://api.cloze.com"; +const VERSION_PATH = "/v1"; +const WEBHOOK_ID = "webhookId"; + +export default { + BASE_URL, + VERSION_PATH, + WEBHOOK_ID, +}; diff --git a/components/cloze/common/utils.mjs b/components/cloze/common/utils.mjs new file mode 100644 index 0000000000000..650af25ed3a8e --- /dev/null +++ b/components/cloze/common/utils.mjs @@ -0,0 +1,51 @@ +import { ConfigurationError } from "@pipedream/platform"; + +const parseJson = (input) => { + const parse = (value) => { + if (typeof(value) === "string") { + try { + return parseJson(JSON.parse(value)); + } catch (e) { + return value; + } + } else if (typeof(value) === "object" && value !== null) { + return Object.entries(value) + .reduce((acc, [ + key, + val, + ]) => Object.assign(acc, { + [key]: parse(val), + }), {}); + } + return value; + }; + + return parse(input); +}; + +function parseArray(value) { + try { + if (!value) { + return; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +export default { + parseArray: (value) => parseArray(value)?.map(parseJson), +}; diff --git a/components/cloze/package.json b/components/cloze/package.json index 82ec92999670b..6c94bb0443f38 100644 --- a/components/cloze/package.json +++ b/components/cloze/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/cloze", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Cloze Components", - "main": "dist/app/cloze.app.mjs", + "main": "cloze.app.mjs", "keywords": [ "pipedream", "cloze" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/cloze", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" } -} \ No newline at end of file +} diff --git a/components/cloze/sources/common/events.mjs b/components/cloze/sources/common/events.mjs new file mode 100644 index 0000000000000..4b7a4d3f4a0c8 --- /dev/null +++ b/components/cloze/sources/common/events.mjs @@ -0,0 +1,8 @@ +export default { + PERSON_CHANGE: "person.change", + PROJECT_CHANGE: "project.change", + COMPANY_CHANGE: "company.change", + PERSON_AUDIT_CHANGE: "person.audit.change", + PROJECT_AUDIT_CHANGE: "project.audit.change", + COMPANY_AUDIT_CHANGE: "company.audit.change", +}; diff --git a/components/cloze/sources/common/webhook.mjs b/components/cloze/sources/common/webhook.mjs new file mode 100644 index 0000000000000..5ff0146bcff6f --- /dev/null +++ b/components/cloze/sources/common/webhook.mjs @@ -0,0 +1,118 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../cloze.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + scope: { + type: "string", + label: "Scope", + description: "Scope of subscription, changes to the user's local person, project, and company may be monitored, or team relations may be monitored, or team hierarchies can be monitored. Can be `local`, `team`, `hierarchy:/X/Y/Z` or `hierarchy:/X/Y/Z/*`", + options: [ + "local", + "team", + ], + default: "local", + }, + }, + hooks: { + async activate() { + const { + createWebhook, + setWebhookId, + http: { endpoint: targetUrl }, + getEventName, + scope, + } = this; + + const response = + await createWebhook({ + data: { + event: getEventName(), + target_url: targetUrl, + scope, + }, + }); + + setWebhookId(response.uniqueid); + }, + async deactivate() { + const { + getWebhookId, + deleteWebhook, + getEventName, + } = this; + + const webhookId = getWebhookId(); + if (webhookId) { + await deleteWebhook({ + data: { + uniqueid: webhookId, + event: getEventName(), + }, + }); + } + }, + }, + methods: { + setWebhookId(value) { + this.db.set(constants.WEBHOOK_ID, value); + }, + getWebhookId() { + return this.db.get(constants.WEBHOOK_ID); + }, + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getEventName() { + throw new ConfigurationError("getEventName is not implemented"); + }, + processResource(events) { + events.forEach((event) => { + this.$emit(event, this.generateMeta(event)); + }); + }, + createWebhook(args = {}) { + return this.app.post({ + debug: true, + path: "/subscribe", + ...args, + }); + }, + deleteWebhook(args = {}) { + return this.app.post({ + debug: true, + path: "/unsubscribe", + ...args, + }); + }, + }, + async run({ + body, headers, + }) { + const { + getWebhookId, + http, + } = this; + + if (headers["x-cloze-subscription-id"] !== getWebhookId()) { + return console.log("Webhook ID does not match with Cloze subscription ID"); + } + + http.respond({ + status: 200, + body: "OK", + headers: { + "content-type": "text/plain", + }, + }); + + this.processResource(body); + }, +}; diff --git a/components/cloze/sources/company-change-instant/company-change-instant.mjs b/components/cloze/sources/company-change-instant/company-change-instant.mjs new file mode 100644 index 0000000000000..416e3fa5c8f4b --- /dev/null +++ b/components/cloze/sources/company-change-instant/company-change-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "cloze-company-change-instant", + name: "Company Change (Instant)", + description: "Emit new event when significant changes regarding a company are detected. [See the documentation](https://api.cloze.com/api-docs/#!/Webhooks/post_v1_subscribe).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventName() { + return events.COMPANY_CHANGE; + }, + generateMeta(event) { + return { + id: event?.company.syncKey, + summary: "New Company Change", + ts: event?.company.lastChanged, + }; + }, + }, + sampleEmit, +}; diff --git a/components/cloze/sources/company-change-instant/test-event.mjs b/components/cloze/sources/company-change-instant/test-event.mjs new file mode 100644 index 0000000000000..1342a328c51bd --- /dev/null +++ b/components/cloze/sources/company-change-instant/test-event.mjs @@ -0,0 +1,22 @@ +export default { + "company": { + "syncKey": "nL8WlFFbFuVvlZfLWFEH3tSsTYZqUQ50qNyfMUi8zFA", + "name": "test 1 upd 3", + "visibility": "visible", + "views": [ + "my", + "defined" + ], + "firstSeen": 1731443475576, + "lastChanged": 1731453402556, + "domains": [ + "test.com" + ], + "segment": "none", + "stage": "none", + "step": "none", + "assignee": "test@test.com" + }, + "changes": {}, + "event": "company.change" +}; diff --git a/components/cloze/sources/person-change-instant/person-change-instant.mjs b/components/cloze/sources/person-change-instant/person-change-instant.mjs new file mode 100644 index 0000000000000..7f7d88d0f0f05 --- /dev/null +++ b/components/cloze/sources/person-change-instant/person-change-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "cloze-person-change-instant", + name: "Person Change (Instant)", + description: "Emit new event when significant changes happen to a person. [See the documentation](https://api.cloze.com/api-docs/#!/Webhooks/post_v1_subscribe).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventName() { + return events.PERSON_CHANGE; + }, + generateMeta(event) { + return { + id: event?.person.syncKey, + summary: "New Person Change", + ts: event?.person.lastChanged, + }; + }, + }, + sampleEmit, +}; diff --git a/components/cloze/sources/person-change-instant/test-event.mjs b/components/cloze/sources/person-change-instant/test-event.mjs new file mode 100644 index 0000000000000..97b22de9bee08 --- /dev/null +++ b/components/cloze/sources/person-change-instant/test-event.mjs @@ -0,0 +1,22 @@ +export default { + "person": { + "syncKey": "nL8WlasdFbFuVvlZfLWFEH3tSsTYZqUQ50qNyfMUi8zFA", + "name": "test 1 upd 3", + "visibility": "visible", + "views": [ + "my", + "defined" + ], + "firstSeen": 1731443475576, + "lastChanged": 1731453402556, + "domains": [ + "test.com" + ], + "segment": "none", + "stage": "none", + "step": "none", + "assignee": "test@test.com", + }, + "changes": {}, + "event": "person.change" +}; diff --git a/components/cloze/sources/project-change-instant/project-change-instant.mjs b/components/cloze/sources/project-change-instant/project-change-instant.mjs new file mode 100644 index 0000000000000..d787091b2e2b9 --- /dev/null +++ b/components/cloze/sources/project-change-instant/project-change-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "cloze-project-change-instant", + name: "Project Change (Instant)", + description: "Emit new event when a significant change occurs in a project. [See the documentation](https://api.cloze.com/api-docs/#!/Webhooks/post_v1_subscribe).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventName() { + return events.PROJECT_CHANGE; + }, + generateMeta(event) { + return { + id: event?.project.syncKey, + summary: "New Project Change", + ts: event?.project.lastChanged, + }; + }, + }, + sampleEmit, +}; diff --git a/components/cloze/sources/project-change-instant/test-event.mjs b/components/cloze/sources/project-change-instant/test-event.mjs new file mode 100644 index 0000000000000..579c6f86eedde --- /dev/null +++ b/components/cloze/sources/project-change-instant/test-event.mjs @@ -0,0 +1,22 @@ +export default { + "project": { + "syncKey": "nL8WlFFbFuVvlZfLWFEH3tSsTYZqUQ50qNyfMUi8zFA", + "name": "test 1 upd 3", + "visibility": "visible", + "views": [ + "my", + "defined" + ], + "firstSeen": 1731443475576, + "lastChanged": 1731453402556, + "domains": [ + "test.com" + ], + "segment": "none", + "stage": "none", + "step": "none", + "assignee": "test@test.com", + }, + "changes": {}, + "event": "project.change" +}; diff --git a/components/clubworx/README.md b/components/clubworx/README.md new file mode 100644 index 0000000000000..544bf6676c232 --- /dev/null +++ b/components/clubworx/README.md @@ -0,0 +1,11 @@ +# Overview + +The Clubworx API provides programmatic access to Clubworx, a gym and martial arts school management platform. With this API, you can automate various tasks such as managing members, tracking attendance, handling billing, and scheduling classes. Utilizing the Clubworx API in Pipedream allows for automated workflows that can save time by integrating with other services, triggering actions based on events, and syncing data across platforms. + +# Example Use Cases + +- **Sync New Members to a Mailchimp List:** Automatically add new Clubworx members to a Mailchimp list to streamline email marketing campaigns. When a new member is added in Clubworx, trigger a Pipedream workflow to subscribe that member to a Mailchimp audience, ensuring timely communication with all members. + +- **Class Attendance to Google Sheets:** Record member attendance from Clubworx classes directly into a Google Sheet. Set up a Pipedream workflow that triggers after a class, pulls attendance data from Clubworx, and appends it to a Google Sheet for easy tracking and analytics. + +- **Slack Notifications for Failed Payments:** Improve payment collection by getting instant Slack notifications for failed member payments in Clubworx. Create a Pipedream workflow that listens for payment failures and then sends a detailed alert to a designated Slack channel, allowing rapid follow-up and resolution. diff --git a/components/coassemble/README.md b/components/coassemble/README.md new file mode 100644 index 0000000000000..249f0accf40a9 --- /dev/null +++ b/components/coassemble/README.md @@ -0,0 +1,11 @@ +# Overview + +Coassemble is a user-friendly training and development platform that lets you create and deliver online courses. The Coassemble API enables the automation of various eLearning tasks, such as user management, course enrollment, and tracking course progress. By leveraging this API within Pipedream, you can streamline educational operations, synchronize educational data with other systems, and create dynamic learning experiences by triggering actions based on course activities. + +# Example Use Cases + +- **Automated User Onboarding Workflow**: When a new employee is added to your HR system, use Pipedream to automatically create a user account in Coassemble and enroll them in relevant training courses. + +- **Course Completion Certification**: Set up a workflow that listens for a course completion event in Coassemble and then generates a personalized certificate using a document generation service like DocuSign or PDF.co. + +- **Synchronize Course Data with a CRM**: Keep your CRM, like Salesforce, updated by using Pipedream to monitor course progress and completion in Coassemble and then reflect these updates in the user's CRM record. diff --git a/components/cobalt/README.md b/components/cobalt/README.md index 0457f342c7088..71b0f94fe5205 100644 --- a/components/cobalt/README.md +++ b/components/cobalt/README.md @@ -1,11 +1,11 @@ # Overview -Cobalt is a powerful, yet easy to use, API that allows you to build all sorts -of applications. +The Cobalt API offers the power to interact with a robust penetration testing platform that assists in identifying vulnerabilities and security loopholes within your digital assets. By integrating this API with Pipedream, you can create automated workflows that trigger actions based on the findings of security tests, streamline the communication of these results within your team, and connect them with other tools to manage remediation processes efficiently. -Here are some examples of what you can build with Cobalt: +# Example Use Cases -- Web applications -- Mobile applications -- Desktop applications -- Games +- **Automated Vulnerability Alerting in Slack**: When Cobalt flags a new vulnerability, use Pipedream to automatically post a detailed alert in a designated Slack channel. This ensures that your security team is instantly informed and can act quickly to assess and remediate the issue. + +- **Issue Tracking Integration**: As vulnerabilities are detected by Cobalt, automatically create issues in project management tools like Jira. Each issue would contain the vulnerability details, allowing your development team to prioritize and track the remediation process seamlessly from start to finish. + +- **Daily Security Report Email**: Configure a Pipedream workflow to fetch daily reports from Cobalt and send a formatted summary via email to stakeholders. This workflow could include filtering to highlight critical vulnerabilities, ensuring that leadership stays informed about the security posture without the need to log into multiple systems. diff --git a/components/cobalt/package.json b/components/cobalt/package.json new file mode 100644 index 0000000000000..6574220f9af6e --- /dev/null +++ b/components/cobalt/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/cobalt", + "version": "0.6.0", + "description": "Pipedream cobalt Components", + "main": "cobalt.app.mjs", + "keywords": [ + "pipedream", + "cobalt" + ], + "homepage": "https://pipedream.com/apps/cobalt", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/coda/README.md b/components/coda/README.md index 812dafbe057cc..fbe5254b7a2c6 100644 --- a/components/coda/README.md +++ b/components/coda/README.md @@ -1,11 +1,11 @@ # Overview -With the Coda API, you can build: - -- A list of all the docs you have access to -- A table of contents for a doc -- A specific page from a doc -- A list of all the action items in a doc -- A list of all the people who have access to a doc -- A list of all the docs a specific person has access to -- A list of all the changes made to a doc +The Coda API enables you to interact with your Coda docs for both data retrieval and manipulation. By leveraging this API on Pipedream, you can automate document updates, synchronize data across different platforms, orchestrate complex workflows, and react to changes in real-time. Coda’s tables can act as dynamic databases that interconnect with various services, allowing you to streamline operations that depend on the timely and accurate exchange of information. + +# Example Use Cases + +- **Sync Coda Tables with Google Sheets**: Automatically update a Google Sheet whenever a row in a Coda table is modified. This workflow can be essential for teams that use Google Sheets for reporting but manage their data within Coda. + +- **GitHub Issue Tracker Integration**: When a new issue is created in GitHub, automatically add a corresponding row to a Coda table set up as an issue tracker. This can help engineering teams to stay on top of new issues without constantly monitoring GitHub. + +- **Slack Notifications for New Coda Rows**: Set up a workflow where a new row added to a Coda table triggers a notification in a designated Slack channel. This can keep teams instantly informed about new entries, like support tickets or sales leads. diff --git a/components/coda/actions/copy-doc/copy-doc.mjs b/components/coda/actions/copy-doc/copy-doc.mjs index 34f9573a18798..221a2df1cd4b1 100644 --- a/components/coda/actions/copy-doc/copy-doc.mjs +++ b/components/coda/actions/copy-doc/copy-doc.mjs @@ -4,7 +4,7 @@ export default { key: "coda-copy-doc", name: "Copy Doc", description: "Creates a copy of the specified doc. [See docs](https://coda.io/developers/apis/v1#operation/createDoc)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { coda, diff --git a/components/coda/actions/create-doc/create-doc.mjs b/components/coda/actions/create-doc/create-doc.mjs index 44fc3304fd341..74cd9e00ebc70 100644 --- a/components/coda/actions/create-doc/create-doc.mjs +++ b/components/coda/actions/create-doc/create-doc.mjs @@ -4,7 +4,7 @@ export default { key: "coda-create-doc", name: "Create Doc", description: "Creates a new doc. [See docs](https://coda.io/developers/apis/v1#operation/createDoc)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { coda, diff --git a/components/coda/actions/create-rows/create-rows.mjs b/components/coda/actions/create-rows/create-rows.mjs index c7b50e545a041..c83dd0208f2cd 100644 --- a/components/coda/actions/create-rows/create-rows.mjs +++ b/components/coda/actions/create-rows/create-rows.mjs @@ -4,7 +4,7 @@ export default { key: "coda-create-rows", name: "Create Rows", description: "Insert a row in a selected table. [See docs](https://coda.io/developers/apis/v1#operation/upsertRows)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { coda, diff --git a/components/coda/actions/delete-row/delete-row.mjs b/components/coda/actions/delete-row/delete-row.mjs index f176164d0792b..eb6c8939ebd60 100644 --- a/components/coda/actions/delete-row/delete-row.mjs +++ b/components/coda/actions/delete-row/delete-row.mjs @@ -4,7 +4,7 @@ export default { key: "coda-delete-row", name: "Delete Row", description: "Delete a single row by name or ID. [See docs](https://coda.io/developers/apis/v1#tag/Rows/operation/deleteRow)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { coda, diff --git a/components/coda/actions/find-row/find-row.mjs b/components/coda/actions/find-row/find-row.mjs index 2a514ea701fb3..9682a91c64976 100644 --- a/components/coda/actions/find-row/find-row.mjs +++ b/components/coda/actions/find-row/find-row.mjs @@ -4,7 +4,7 @@ export default { key: "coda-find-row", name: "Find Row", description: "Searches for a row in the selected table using a column match search. [See docs](https://coda.io/developers/apis/v1#operation/listRows)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { coda, diff --git a/components/coda/actions/get-row/get-row.mjs b/components/coda/actions/get-row/get-row.mjs index a5941ffc977a8..a380bf3b00ad0 100644 --- a/components/coda/actions/get-row/get-row.mjs +++ b/components/coda/actions/get-row/get-row.mjs @@ -4,7 +4,7 @@ export default { key: "coda-get-row", name: "Get Row", description: "Fetch a single row by name or ID. [See docs](https://coda.io/developers/apis/v1#tag/Rows/operation/getRow)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { coda, diff --git a/components/coda/actions/list-columns/list-columns.mjs b/components/coda/actions/list-columns/list-columns.mjs index 1375bd49a8ba9..3184862cbb443 100644 --- a/components/coda/actions/list-columns/list-columns.mjs +++ b/components/coda/actions/list-columns/list-columns.mjs @@ -4,7 +4,7 @@ export default { key: "coda-list-columns", name: "List Columns", description: "Lists columns in a table. [See docs](https://coda.io/developers/apis/v1#operation/listColumns)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { coda, diff --git a/components/coda/actions/list-docs/list-docs.mjs b/components/coda/actions/list-docs/list-docs.mjs index c4e7a220994e0..b59cc0bec5a10 100644 --- a/components/coda/actions/list-docs/list-docs.mjs +++ b/components/coda/actions/list-docs/list-docs.mjs @@ -4,7 +4,7 @@ export default { key: "coda-list-docs", name: "List Docs", description: "Returns a list of docs accessible by the user. These are returned in the same order as on the docs page: reverse chronological by the latest event relevant to the user (last viewed, edited, or shared). [See docs](https://coda.io/developers/apis/v1#operation/listDocs)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { coda, diff --git a/components/coda/actions/list-formulas/list-formulas.mjs b/components/coda/actions/list-formulas/list-formulas.mjs new file mode 100644 index 0000000000000..2845794bdeccd --- /dev/null +++ b/components/coda/actions/list-formulas/list-formulas.mjs @@ -0,0 +1,45 @@ +import coda from "../../coda.app.mjs"; + +export default { + key: "coda-list-formulas", + name: "List Formulas", + description: "Lists formulas in a doc. [See docs](https://coda.io/developers/apis/v1#tag/Formulas/operation/listFormulas)", + version: "0.0.1", + type: "action", + props: { + coda, + docId: { + propDefinition: [ + coda, + "docId", + ], + }, + sortBy: { + propDefinition: [ + coda, + "sortBy", + ], + }, + }, + async run({ $ }) { + let params = { + sortBy: this.sortBy, + }; + + let items = []; + let response; + do { + response = await this.coda.listFormulas( + $, + this.docId, + params, + ); + + items.push(...response.items); + params.pageToken = response.nextPageToken; + } while (params.pageToken); + + $.export("$summary", `Retrieved ${items.length} formula(s)`); + return items; + }, +}; diff --git a/components/coda/actions/list-tables/list-tables.mjs b/components/coda/actions/list-tables/list-tables.mjs index ebb4a6558b7ba..b5927dff018a2 100644 --- a/components/coda/actions/list-tables/list-tables.mjs +++ b/components/coda/actions/list-tables/list-tables.mjs @@ -4,7 +4,7 @@ export default { key: "coda-list-tables", name: "List Tables", description: "Lists tables in a doc. [See docs](https://coda.io/developers/apis/v1#operation/listTables)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { coda, diff --git a/components/coda/actions/update-row/update-row.mjs b/components/coda/actions/update-row/update-row.mjs index 6350d66be6a6c..9efcc6318d5fc 100644 --- a/components/coda/actions/update-row/update-row.mjs +++ b/components/coda/actions/update-row/update-row.mjs @@ -4,7 +4,7 @@ export default { key: "coda-update-row", name: "Update a Row", description: "Updates the specified row in the table. [See docs](https://coda.io/developers/apis/v1#operation/updateRow)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { coda, diff --git a/components/coda/actions/upsert-rows/upsert-rows.mjs b/components/coda/actions/upsert-rows/upsert-rows.mjs index dad69bc49e296..b6763d32a4c4a 100644 --- a/components/coda/actions/upsert-rows/upsert-rows.mjs +++ b/components/coda/actions/upsert-rows/upsert-rows.mjs @@ -4,7 +4,7 @@ export default { key: "coda-upsert-rows", name: "Upsert Rows", description: "Creates a new row or updates existing rows if any upsert key columns are provided. When upserting, if multiple rows match the specified key column(s), they will all be updated with the specified value. [See docs](https://coda.io/developers/apis/v1#operation/upsertRows)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { coda, diff --git a/components/coda/coda.app.mjs b/components/coda/coda.app.mjs index 8cf6157cc0118..e60c4bd0862b4 100644 --- a/components/coda/coda.app.mjs +++ b/components/coda/coda.app.mjs @@ -228,6 +228,21 @@ export default { }; return await this._makeRequest($, opts); }, + /** + * Lists formulas in a doc according to parameters + * @param {object} $ + * @param {string} docId + * @param {object} [params] + * @param {string} [params.sortBy] + * @return {object[]} List of formulas + */ + async listFormulas($, docId, params = {}) { + let opts = { + path: `/docs/${docId}/formulas`, + params, + }; + return await this._makeRequest($, opts); + }, /** * Fetch a single row by name or ID * @param {object} $ diff --git a/components/coda/package.json b/components/coda/package.json index 285efaf4a40a0..ad0b5f8ae41f7 100644 --- a/components/coda/package.json +++ b/components/coda/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/coda", - "version": "0.1.1", + "version": "0.1.2", "description": "Pipedream Coda Components", "main": "coda.app.mjs", "keywords": [ diff --git a/components/coda/sources/row-created/row-created.mjs b/components/coda/sources/row-created/row-created.mjs index 1646aa50ff52f..c19d20a3e17eb 100644 --- a/components/coda/sources/row-created/row-created.mjs +++ b/components/coda/sources/row-created/row-created.mjs @@ -1,12 +1,12 @@ -import coda from "../../coda.app.mjs"; import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import coda from "../../coda.app.mjs"; export default { key: "coda-row-created", name: "New Row Created", description: "Emit new event for every created / updated row in a table. [See the documentation.](https://coda.io/developers/apis/v1#tag/Rows/operation/listRows)", type: "source", - version: "0.0.3", + version: "0.0.4", dedupe: "unique", props: { coda, diff --git a/components/codacy/README.md b/components/codacy/README.md new file mode 100644 index 0000000000000..eeae1bb6ba5f6 --- /dev/null +++ b/components/codacy/README.md @@ -0,0 +1,11 @@ +# Overview + +The Codacy API lends itself to automated code review and analysis within Pipedream's serverless platform. By wielding this API, you can craft workflows that trigger on code pushes, scrutinize code quality, enforce coding standards, and seamlessly integrate with other tools for broader development process automation. It's a gateway to embed code quality checks into your CI/CD pipeline, get real-time alerts, and keep your codebase healthy, all within the Pipedream ecosystem. + +# Example Use Cases + +- **Automated Code Quality Report on Push**: Trigger a workflow in Pipedream whenever a new commit is pushed to your repository, using it to call the Codacy API to perform an automated code review. Once the analysis is complete, collect and send a report detailing code quality metrics to your team's Slack channel. + +- **Enforce Coding Standards Before Merging Pull Requests**: Set up a Pipedream workflow that runs every time a new pull request is opened. The workflow communicates with the Codacy API to check if the changes adhere to your predefined coding standards. If the pull request fails the check, automatically add a comment on the pull request with the issues that need to be resolved. + +- **Daily Code Quality Snapshot to Email**: Schedule a daily workflow in Pipedream that uses the Codacy API to fetch the latest code quality data for your main branches. Compile this data into a snapshot and dispatch a digest email to your development team, highlighting any new issues or improvements in the code quality over the past day. diff --git a/components/codacy/actions/delete-integration/delete-integration.mjs b/components/codacy/actions/delete-integration/delete-integration.mjs new file mode 100644 index 0000000000000..3f977bbccbe46 --- /dev/null +++ b/components/codacy/actions/delete-integration/delete-integration.mjs @@ -0,0 +1,28 @@ +import app from "../../codacy.app.mjs"; + +export default { + key: "codacy-delete-integration", + name: "Delete Integration", + description: "Delete integration for the authenticated user. [See the documentation](https://api.codacy.com/api/api-docs?http#deleteintegration)", + version: "0.0.1", + type: "action", + props: { + app, + accountProvider: { + propDefinition: [ + app, + "accountProvider", + ], + }, + }, + async run({ $ }) { + const response = await this.app.deleteIntegration({ + $, + accountProvider: this.accountProvider, + }); + + $.export("$summary", `Successfully deleted integration with ${this.accountProvider}`); + + return response; + }, +}; diff --git a/components/codacy/actions/get-user/get-user.mjs b/components/codacy/actions/get-user/get-user.mjs new file mode 100644 index 0000000000000..bd863077bb352 --- /dev/null +++ b/components/codacy/actions/get-user/get-user.mjs @@ -0,0 +1,21 @@ +import app from "../../codacy.app.mjs"; + +export default { + key: "codacy-get-user", + name: "Get Authenticated User", + description: "Get the authenticated user on Codacy. [See the documentation](https://api.codacy.com/api/api-docs#getuser)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.getUser({ + $, + }); + + $.export("$summary", "Retrieved authenticated user information"); + + return response; + }, +}; diff --git a/components/codacy/actions/list-integrations/list-integrations.mjs b/components/codacy/actions/list-integrations/list-integrations.mjs new file mode 100644 index 0000000000000..b2b4fc4867d69 --- /dev/null +++ b/components/codacy/actions/list-integrations/list-integrations.mjs @@ -0,0 +1,21 @@ +import app from "../../codacy.app.mjs"; + +export default { + key: "codacy-list-integrations", + name: "List Integrations", + description: "List integrations on Codacy. [See the documentation](https://api.codacy.com/api/api-docs?http#listuserintegrations)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.listIntegrations({ + $, + }); + + $.export("$summary", `Successfully listed ${response.data.length} integration(s).`); + + return response; + }, +}; diff --git a/components/codacy/actions/list-organizations/list-organizations.mjs b/components/codacy/actions/list-organizations/list-organizations.mjs new file mode 100644 index 0000000000000..f1b854bc02ad4 --- /dev/null +++ b/components/codacy/actions/list-organizations/list-organizations.mjs @@ -0,0 +1,29 @@ +import app from "../../codacy.app.mjs"; + +export default { + key: "codacy-list-organizations", + name: "List Organizations", + description: "List organizations for the authenticated user. [See the documentation](https://api.codacy.com/api/api-docs#listorganizations)", + version: "0.0.1", + type: "action", + props: { + app, + provider: { + propDefinition: [ + app, + "provider", + ], + optional: false, + }, + }, + async run({ $ }) { + const response = await this.app.listOrganizations({ + $, + provider: this.provider, + }); + + $.export("$summary", "Successfully listed organizations"); + + return response; + }, +}; diff --git a/components/codacy/codacy.app.mjs b/components/codacy/codacy.app.mjs index 47f3b301b1e8f..90e8c1fb55047 100644 --- a/components/codacy/codacy.app.mjs +++ b/components/codacy/codacy.app.mjs @@ -1,11 +1,86 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "codacy", - propDefinitions: {}, + propDefinitions: { + provider: { + type: "string", + label: "Provider", + description: "The Git provider", + optional: true, + options: constants.GIT_PROVIDERS, + }, + accountProvider: { + type: "string", + label: "Account Provider", + description: "The Account Provider", + options: constants.ACCOUNT_PROVIDERS, + }, + limit: { + type: "string", + label: "Limit", + description: "Maximum number of items to return", + optional: true, + }, + remoteOrganizationName: { + type: "string", + label: "Organization Name", + description: "Name of the organization", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://app.codacy.com/api/v3"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + params, + headers: { + ...headers, + "Accept": "application/json", + "api-token": this.$auth.api_token, + }, + }); + }, + async getUser(args = {}) { + return this._makeRequest({ + path: "/user", + ...args, + }); + }, + async listOrganizations({ + provider, ...args + }) { + return this._makeRequest({ + path: `/user/organizations/${provider}`, + ...args, + }); + }, + async listIntegrations(args = {}) { + return this._makeRequest({ + path: "/user/integrations", + ...args, + }); + }, + async deleteIntegration({ + accountProvider, ...args + }) { + return this._makeRequest({ + method: "DELETE", + path: `/user/integrations/${accountProvider}`, + ...args, + }); }, }, }; diff --git a/components/codacy/common/constants.mjs b/components/codacy/common/constants.mjs new file mode 100644 index 0000000000000..0ec57cec63b96 --- /dev/null +++ b/components/codacy/common/constants.mjs @@ -0,0 +1,34 @@ +export default { + GIT_PROVIDERS: [ + { + label: "bb", + value: "bb", + }, + { + label: "gh", + value: "gh", + }, + { + label: "gl", + value: "gl", + }, + ], + ACCOUNT_PROVIDERS: [ + { + label: "GitHub", + value: "github", + }, + { + label: "Google", + value: "google", + }, + { + label: "Bitbucket", + value: "bitbucket", + }, + { + label: "GitLab", + value: "gitlab", + }, + ], +}; diff --git a/components/codacy/package.json b/components/codacy/package.json index 4697dd2ba7f07..e4fbd74aa572f 100644 --- a/components/codacy/package.json +++ b/components/codacy/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/codacy", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Codacy Components", "main": "codacy.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" } -} \ No newline at end of file +} diff --git a/components/codat/README.md b/components/codat/README.md new file mode 100644 index 0000000000000..05b0798235cca --- /dev/null +++ b/components/codat/README.md @@ -0,0 +1,11 @@ +# Overview + +The Codat API provides a way to seamlessly integrate and automate financial data processes for businesses. With Codat, you can connect to accounting software, extract company financials, and reconcile transactions in real-time. Whether it's for pulling recent invoices, syncing payment information, or consolidating financial reports, Codat's API acts as a bridge between various accounting platforms and your applications, enabling more efficient financial operations and decision-making. + +# Example Use Cases + +- **Real-time Financial Dashboard**: Sync financial data from multiple sources using Codat into a centralized dashboard on Pipedream. Combine this with Pipedream’s data stores to cache and display real-time financial KPIs, providing businesses with up-to-date insights into their financial health. + +- **Automated Bookkeeping Workflows**: Use Codat to fetch new transactions from different accounting services, then process and categorize them on Pipedream. Connect with Google Sheets or Airtable to log these transactions, creating an automated bookkeeping system that reduces manual entry and potential human error. + +- **Invoice Payment Tracking**: Build a workflow that monitors invoice statuses via Codat. Whenever an invoice is marked as paid in the accounting software, trigger a Pipedream workflow that updates your CRM, such as Salesforce, and sends a thank you email to the client using a service like SendGrid or Mailgun. diff --git a/components/codat/codat.app.mjs b/components/codat/codat.app.mjs new file mode 100644 index 0000000000000..761877065f517 --- /dev/null +++ b/components/codat/codat.app.mjs @@ -0,0 +1,75 @@ +import { axios } from "@pipedream/platform"; +import eventTypes from "./common/event-types.mjs"; + +export default { + type: "app", + app: "codat", + propDefinitions: { + eventTypes: { + type: "string[]", + label: "Event Type", + description: "The type of event to emit when it is produced by Codat", + options: eventTypes, + }, + companyId: { + type: "string", + label: "Company ID", + description: "Unique identifier of the company to indicate company-specific events. The associated webhook consumer will receive events only for the specified ID.", + async options({ page }) { + const { results } = await this.listCompanies({ + params: { + page: page + 1, + }, + }); + return results?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.codat.io"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `${this.$auth.authorization_header}`, + Accept: "application/json", + }, + }); + }, + listCompanies(opts = {}) { + return this._makeRequest({ + path: "/companies", + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteWebhook({ + hookId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${hookId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/codat/common/event-types.mjs b/components/codat/common/event-types.mjs new file mode 100644 index 0000000000000..0c7e22abecd7a --- /dev/null +++ b/components/codat/common/event-types.mjs @@ -0,0 +1,62 @@ +export default [ + { + value: "AccountCategoriesUpdated", + label: "Triggered anytime a company's accounts are categorized. This can be when Codat updates the suggested category fields or a user updates the confirmed category fields.", + }, + { + value: "bankFeeds.sourceAccount.connected", + label: "Indicates a bank feed source account has changed to a status of connected.", + }, + { + value: "bankFeeds.sourceAccount.disconnected", + label: "Indicates a bank feed source account has changed to a status of disconnected.", + }, + { + value: "ClientRateLimitReached", + label: "Triggered when the number of requests to Codat's API from the client exceeds the current quota.", + }, + { + value: "ClientRateLimitReset", + label: "Triggered when the rate limit quota has reset for the client, and more requests to the API are available.", + }, + { + value: "DataConnectionStatusChanged", + label: "Triggered when a data connection status of a specific company changes.", + }, + { + value: "DataSyncCompleted", + label: " Generated for each dataType to indicate that data synchronization is successfully completed for that specific data type.", + }, + { + value: "DataSyncStatusChangedToError", + label: "Triggered when the synchronization of a dataset fails.", + }, + { + value: "DatasetDataChanged", + label: " Generated for each dataType to indicate that dataset synchronization has completed and updated Codat's data cache through the creation of new records or a change to existing records.", + }, + { + value: "NewCompanySynchronized", + label: "Triggered when initial syncs are complete for all data types queued for a newly connected company, and at least one of those syncs is successful.", + }, + { + value: "PushOperationStatusChanged", + label: "Indicates that a create, update, or delete operation's status has changed. You can learn more about push operations at Codat.", + }, + { + value: "PushOperationTimedOut", + label: "Indicates that a create, update, or delete operation has timed out. You can learn more about timeouts for push operations at Codat.", + }, + { + value: "SyncCompleted", + label: "Triggered anytime an expense sync completes. Used for Sync for Expenses only.", + }, + { + value: "SyncConnectionDeleted", + label: "Indicates a Sync for Commerce connection has been deleted. Used for Sync for Commerce only.", + }, + { + value: "SyncFailed", + label: "Indicates a failure occurred during an expense sync. Used for Sync for Expenses only.", + }, +]; diff --git a/components/codat/package.json b/components/codat/package.json new file mode 100644 index 0000000000000..b1db72e0b7f66 --- /dev/null +++ b/components/codat/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/codat", + "version": "0.1.0", + "description": "Pipedream Codat Components", + "main": "codat.app.mjs", + "keywords": [ + "pipedream", + "codat" + ], + "homepage": "https://pipedream.com/apps/codat", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/codat/sources/new-webhook-message-instant/new-webhook-message-instant.mjs b/components/codat/sources/new-webhook-message-instant/new-webhook-message-instant.mjs new file mode 100644 index 0000000000000..f500a2f7aaec3 --- /dev/null +++ b/components/codat/sources/new-webhook-message-instant/new-webhook-message-instant.mjs @@ -0,0 +1,77 @@ +import codat from "../../codat.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "codat-new-webhook-message-instant", + name: "New Webhook Message (Instant)", + description: "Emit new event when a specified event type is produced by Codat. [See the documentation](https://docs.codat.io/platform-api#/operations/create-webhook-consumer)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + codat, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + eventTypes: { + propDefinition: [ + codat, + "eventTypes", + ], + }, + companyId: { + propDefinition: [ + codat, + "companyId", + ], + }, + }, + hooks: { + async activate() { + const data = { + url: this.http.endpoint, + eventTypes: this.eventTypes, + }; + if (this.companyId) { + data.companyId = this.companyId; + } + const { id } = await this.codat.createWebhook({ + data, + }); + this._setHookId(id); + }, + async deactivate() { + const hookId = this._getHookId(); + if (hookId) { + await this.codat.deleteWebhook({ + hookId, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + generateMeta(body) { + return { + id: body.AlertId, + summary: body.Message, + ts: Date.now(), + }; + }, + }, + async run({ body }) { + this.http.respond({ + status: 200, + }); + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, + sampleEmit, +}; diff --git a/components/codat/sources/new-webhook-message-instant/test-event.mjs b/components/codat/sources/new-webhook-message-instant/test-event.mjs new file mode 100644 index 0000000000000..c4770a9e6e92c --- /dev/null +++ b/components/codat/sources/new-webhook-message-instant/test-event.mjs @@ -0,0 +1,13 @@ +export default { + "AlertId": "a9367074-b5c3-42c4-9be4-be129f43577e", + "ClientId": "bae71d36-ff47-420a-b4a6-f8c9ddf41140", + "ClientName": "Bank of Dave", + "CompanyId": "8a210b68-6988-11ed-a1eb-0242ac120002", + "Data": { + "modifiedDate": "2019-08-24T14:15:22Z" + }, + "DataConnectionId": "2e9d2c44-f675-40ba-8049-353bfcb5e171", + "Message": "Account categories updated for company f1c35bdc-1546-41b9-baf4-3f31135af968.", + "RuleId": "70af3071-65d9-4ec3-b3cb-5283e8d55dac", + "RuleType": "Account Categories Updated" +} \ No newline at end of file diff --git a/components/code_climate/README.md b/components/code_climate/README.md new file mode 100644 index 0000000000000..ac64fed90a65e --- /dev/null +++ b/components/code_climate/README.md @@ -0,0 +1,11 @@ +# Overview + +The Code Climate API lets you tap into your software's code quality metrics, test coverage, and technical debt, enabling you to monitor and improve your development process. By integrating Code Climate with Pipedream, you can create automated workflows that respond to changes in your codebase, streamline quality assurance, and synchronize data with other tools in your development stack. + +# Example Use Cases + +- **Automated Quality Reports**: Send weekly code quality reports to your team's Slack channel. When Code Climate detects changes in code quality metrics, Pipedream can format these metrics into a readable report and post it automatically to a designated Slack channel, keeping your team informed. + +- **Merge Request Analysis Trigger**: Run custom scripts or notify team members when a new merge request might introduce code issues. Once Code Climate updates the analysis for a merge request, Pipedream can trigger workflows that execute custom logic, such as running additional tests, or send notifications through apps like Microsoft Teams or Email. + +- **Issue Tracking Integration**: Create issues in your project management tool, like JIRA or GitHub Issues, when Code Climate identifies new code smells or security vulnerabilities. Pipedream can listen for such events and automatically create detailed issues to track the resolution of each finding right where your team coordinates work. diff --git a/components/code_climate/actions/create-organization/create-organization.mjs b/components/code_climate/actions/create-organization/create-organization.mjs new file mode 100644 index 0000000000000..467839a4fc767 --- /dev/null +++ b/components/code_climate/actions/create-organization/create-organization.mjs @@ -0,0 +1,35 @@ +import app from "../../code_climate.app.mjs"; + +export default { + key: "code_climate-create-organization", + name: "Create Organization", + description: "Creates a new organization on code_climate. [See the documentation](https://developer.codeclimate.com/#create-organization)", + version: "0.0.1", + type: "action", + props: { + app, + orgName: { + propDefinition: [ + app, + "orgName", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createOrganization({ + $, + data: { + data: { + type: "orgs", + attributes: { + name: this.orgName, + }, + }, + }, + }); + + $.export("$summary", `Successfully created organization '${this.orgName}'`); + + return response; + }, +}; diff --git a/components/code_climate/actions/get-members/get-members.mjs b/components/code_climate/actions/get-members/get-members.mjs new file mode 100644 index 0000000000000..54724b4b6b7e1 --- /dev/null +++ b/components/code_climate/actions/get-members/get-members.mjs @@ -0,0 +1,28 @@ +import app from "../../code_climate.app.mjs"; + +export default { + key: "code_climate-get-members", + name: "Get Members", + description: "Returns a list of active members for the specified organization. [See the documentation](https://developer.codeclimate.com/#get-members)", + version: "0.0.1", + type: "action", + props: { + app, + orgId: { + propDefinition: [ + app, + "orgId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getMembers({ + $, + orgId: this.orgId, + }); + + $.export("$summary", `Successfully retrieved ${response.data.length} user(s) in the specified organization`); + + return response; + }, +}; diff --git a/components/code_climate/actions/get-organizations/get-organizations.mjs b/components/code_climate/actions/get-organizations/get-organizations.mjs new file mode 100644 index 0000000000000..66deab870a5f2 --- /dev/null +++ b/components/code_climate/actions/get-organizations/get-organizations.mjs @@ -0,0 +1,21 @@ +import app from "../../code_climate.app.mjs"; + +export default { + key: "code_climate-get-organizations", + name: "Get Organizations", + description: "Returns collection of organizations for the current user. [See the documentation](https://developer.codeclimate.com/#get-organizations)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.getOrganizations({ + $, + }); + + $.export("$summary", `Successfully retrieved ${response.data.length} organization(s)`); + + return response; + }, +}; diff --git a/components/code_climate/code_climate.app.mjs b/components/code_climate/code_climate.app.mjs index bd94180e3ca6f..3de92b961118b 100644 --- a/components/code_climate/code_climate.app.mjs +++ b/components/code_climate/code_climate.app.mjs @@ -1,11 +1,71 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "code_climate", - propDefinitions: {}, + propDefinitions: { + orgName: { + type: "string", + label: "Organization Name", + description: "The name of the organization in Code Climate", + }, + orgId: { + type: "string", + label: "Organization ID", + description: "The ID of the organization", + async options() { + const { data } = await this.getOrganizations(); + + return data.map(({ + id, attributes, + }) => ({ + value: id, + label: attributes.name, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.codeclimate.com/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "Accept": "application/vnd.api+json", + "Authorization": `Token token=${this.$auth.personal_access_token}`, + }, + }); + }, + async createOrganization(args = {}) { + return this._makeRequest({ + method: "post", + path: "/orgs", + ...args, + }); + }, + async getMembers({ + orgId, ...args + }) { + return this._makeRequest({ + path: `/orgs/${orgId}/members`, + ...args, + }); + }, + async getOrganizations(args = {}) { + return this._makeRequest({ + path: "/orgs", + ...args, + }); }, }, }; diff --git a/components/code_climate/package.json b/components/code_climate/package.json index e3962a44a138b..2eedae37b8ced 100644 --- a/components/code_climate/package.json +++ b/components/code_climate/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/code_climate", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Code Climate Components", "main": "code_climate.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" } -} \ No newline at end of file +} diff --git a/components/codegpt/README.md b/components/codegpt/README.md new file mode 100644 index 0000000000000..30526640c2469 --- /dev/null +++ b/components/codegpt/README.md @@ -0,0 +1,11 @@ +# Overview + +The CodeGPT API allows you to integrate advanced AI-driven code generation and completion capabilities within Pipedream workflows. This API can bolster your coding tasks by suggesting code snippets, completing blocks of code, and even writing functions based on your descriptions. With this power, you can automate coding tasks, analyze and improve code quality, or build tools that aid in educational environments. The API's flexibility opens up numerous possibilities when connected to various triggers or actions in other apps within Pipedream's ecosystem. + +# Example Use Cases + +- **Automated Code Snippet Generation**: Trigger this workflow with a new GitHub issue label, such as "snippet-request". Use the CodeGPT API to generate a code snippet based on the issue's description, then post the result back to the issue or send it via Slack to the development team. + +- **Dynamic Code Analysis and Improvement Suggestions**: Set up a workflow that listens for new pull requests on GitHub. Use the CodeGPT API to review the submitted code and suggest improvements or optimizations. Automatically comment on the pull request with the API's feedback, aiding in code review processes. + +- **Code-Based Educational Content Creation**: Connect the CodeGPT API to a schedule trigger to regularly create educational content, like coding challenges or examples. Generate code snippets and explanations, then post them to a Medium blog or educational platform to engage learners and provide valuable resources. diff --git a/components/codegpt/codegpt.app.mjs b/components/codegpt/codegpt.app.mjs new file mode 100644 index 0000000000000..8db4beaf917ae --- /dev/null +++ b/components/codegpt/codegpt.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "codegpt", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/codegpt/package.json b/components/codegpt/package.json new file mode 100644 index 0000000000000..7f6b2a09f4376 --- /dev/null +++ b/components/codegpt/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/codegpt", + "version": "0.0.1", + "description": "Pipedream CodeGPT Components", + "main": "codegpt.app.mjs", + "keywords": [ + "pipedream", + "codegpt" + ], + "homepage": "https://pipedream.com/apps/codegpt", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/codemagic/README.md b/components/codemagic/README.md new file mode 100644 index 0000000000000..82491e1927693 --- /dev/null +++ b/components/codemagic/README.md @@ -0,0 +1,11 @@ +# Overview + +Codemagic API offers a way to automate your mobile app development and delivery processes. By integrating with Pipedream, you can invoke workflows that react to events in your Git repository, automate build tasks, deploy applications, send notifications, and more. It's a powerful ally for developers looking to streamline the CI/CD pipeline for their mobile applications. + +# Example Use Cases + +- **Automate App Builds on Git Push**: Trigger a Codemagic build whenever changes are pushed to the main branch of your app's repository. Use Pipedream's Git platform triggers to start the build process on Codemagic, ensuring your app is always up to date with the latest code. + +- **Deploy Updates after Successful Builds**: After a successful build on Codemagic, use Pipedream to deploy the app automatically to app stores or distribution services. Connect to stores like Google Play or Apple App Store via their respective APIs to streamline your delivery process. + +- **Slack Notifications for Build Status**: Set up a workflow that listens for build status updates from Codemagic and sends a message to a Slack channel with the results. This keeps your team informed about the build process, fostering quick feedback and action on build success or failure. diff --git a/components/codemagic/actions/add-application/add-application.mjs b/components/codemagic/actions/add-application/add-application.mjs new file mode 100644 index 0000000000000..e01eb83c79554 --- /dev/null +++ b/components/codemagic/actions/add-application/add-application.mjs @@ -0,0 +1,37 @@ +import app from "../../codemagic.app.mjs"; + +export default { + key: "codemagic-add-application", + name: "Create Application", + description: "Creates a new application codemagic. [See the documentation](https://docs.codemagic.io/rest-api/applications/#add-a-new-application)", + version: "0.0.1", + type: "action", + props: { + app, + repositoryUrl: { + propDefinition: [ + app, + "repositoryUrl", + ], + }, + teamId: { + propDefinition: [ + app, + "teamId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.addApp({ + $, + data: { + repositoryUrl: this.repositoryUrl, + teamId: this.teamId, + }, + }); + + $.export("$summary", `Successfully created application '${response.appName}'`); + + return response; + }, +}; diff --git a/components/codemagic/actions/create-variable/create-variable.mjs b/components/codemagic/actions/create-variable/create-variable.mjs new file mode 100644 index 0000000000000..4617be05d07a9 --- /dev/null +++ b/components/codemagic/actions/create-variable/create-variable.mjs @@ -0,0 +1,58 @@ +import app from "../../codemagic.app.mjs"; + +export default { + key: "codemagic-create-variable", + name: "Create Variable", + description: "Create a variable in the specified workflow. [See the documentation](https://docs.codemagic.io/rest-api/applications/#add-new-variable)", + version: "0.0.1", + type: "action", + props: { + app, + appId: { + propDefinition: [ + app, + "appId", + ], + }, + workflowID: { + propDefinition: [ + app, + "workflowID", + ], + }, + key: { + propDefinition: [ + app, + "key", + ], + }, + value: { + propDefinition: [ + app, + "value", + ], + }, + group: { + propDefinition: [ + app, + "group", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createVariable({ + $, + appId: this.appId, + data: { + key: this.key, + value: this.value, + workflowId: this.workflowID, + group: this.group, + }, + }); + + $.export("$summary", `Successfully created variable '${this.key}'`); + + return response; + }, +}; diff --git a/components/codemagic/actions/list-application/list-application.mjs b/components/codemagic/actions/list-application/list-application.mjs new file mode 100644 index 0000000000000..427b6225d5a3c --- /dev/null +++ b/components/codemagic/actions/list-application/list-application.mjs @@ -0,0 +1,21 @@ +import app from "../../codemagic.app.mjs"; + +export default { + key: "codemagic-list-application", + name: "List Applications", + description: "List applications in codemagic. [See the documentation](https://docs.codemagic.io/rest-api/applications/#retrieve-all-applications)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.listApps({ + $, + }); + + $.export("$summary", `Successfully retrieved ${response.applications.length} application(s)`); + + return response; + }, +}; diff --git a/components/codemagic/codemagic.app.mjs b/components/codemagic/codemagic.app.mjs index 2539ac3a380fb..1ca120d0eb12d 100644 --- a/components/codemagic/codemagic.app.mjs +++ b/components/codemagic/codemagic.app.mjs @@ -1,11 +1,99 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "codemagic", - propDefinitions: {}, + propDefinitions: { + appId: { + type: "string", + label: "Application ID", + description: "The ID of the application", + async options() { + const { applications } = await this.listApps(); + + return applications.map(({ + _id, appName, + }) => ({ + value: _id, + label: appName, + })); + }, + }, + workflowID: { + type: "string", + label: "Workflow ID", + description: "The ID of the workflow", + }, + repositoryUrl: { + type: "string", + label: "Repository URL", + description: "SSH or HTTPS URL for cloning the repository", + }, + key: { + type: "string", + label: "Variable Key", + description: "Name of the variable", + }, + group: { + type: "string", + label: "Group Name", + description: "Required for applications using yaml configuration. Name of the group that the variable should be added to. If the group does not exist, it will be created.", + optional: true, + }, + value: { + type: "string", + label: "Variable Value", + description: "Value of the variable", + }, + teamId: { + type: "string", + label: "Team ID", + description: "Team ID, if you wish to add an app directly to one of your teams. You must be an admin in the team specified", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.codemagic.io"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "Content-Type": "application/json", + "x-auth-token": `${this.$auth.api_token}`, + }, + }); + }, + async listApps(args = {}) { + return this._makeRequest({ + path: "/apps", + ...args, + }); + }, + async createVariable({ + appId, ...args + }) { + return this._makeRequest({ + method: "post", + path: `/apps/${appId}/variables`, + ...args, + }); + }, + async addApp(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/apps", + ...args, + }); }, }, }; diff --git a/components/codemagic/package.json b/components/codemagic/package.json index 391888016923d..8ff748d05c9eb 100644 --- a/components/codemagic/package.json +++ b/components/codemagic/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/codemagic", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Codemagic Components", "main": "codemagic.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" } -} \ No newline at end of file +} diff --git a/components/codeq_natural_language_processing_api/README.md b/components/codeq_natural_language_processing_api/README.md new file mode 100644 index 0000000000000..452ff3d1fe37a --- /dev/null +++ b/components/codeq_natural_language_processing_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Codeq Natural Language Processing API provides powerful text analysis capabilities. It parses and understands complex structures in text, extracting meaningful insights. On Pipedream, you can harness this API to analyze text data from various sources, automate content categorization, sentiment analysis, or even construct rich profiles of user feedback. With Pipedream's serverless platform, these processes can be automated, triggered by events, and integrated with numerous other apps to create robust, data-driven workflows. + +# Example Use Cases + +- **Sentiment Analysis on Customer Feedback**: Use the Codeq API to evaluate customer feedback from a support ticket system like Zendesk. When a new ticket arrives, trigger a Pipedream workflow that analyzes sentiment, and conditionally route positive feedback to marketing or negative feedback to customer service teams. + +- **Categorizing Social Media Posts**: Process tweets or posts grabbed from social media platforms like Twitter or Facebook. Analyze the content for topics and sentiment, then use the results to tag and organize posts in a database or send a summary to Slack for team review, enabling quick response to market trends. + +- **Content Summarization for Articles**: Automatically summarize articles posted in RSS feeds or content platforms. Use the Codeq API to extract key points and then save these summaries to a Google Sheet or Notion database, providing a curated list of content for further reading or sharing with team members. diff --git a/components/codereadr/README.md b/components/codereadr/README.md new file mode 100644 index 0000000000000..a4204b5d75420 --- /dev/null +++ b/components/codereadr/README.md @@ -0,0 +1,11 @@ +# Overview + +The CodeREADr API allows you to integrate barcode scanning and data collection into your workflows on Pipedream. With this API, you can automate barcode validation, track event attendance, manage inventory, and more by creating, updating, and retrieving scan records. Pipedream's serverless platform enables you to connect CodeREADr with other apps, triggering actions based on scan data, or enriching CodeREADr data with information from other sources. + +# Example Use Cases + +- **Automated Check-in System**: Use the CodeREADr API on Pipedream to scan tickets or badges for events, automatically checking in attendees by validating barcodes against a database. Upon successful validation, trigger an email or SMS confirmation using SendGrid or Twilio. + +- **Inventory Management**: Sync barcode scans with an inventory database to update stock levels in real-time. When a product's stock falls below a threshold, trigger a restock process by creating a purchase order in QuickBooks or sending a notification to Slack. + +- **Access Control Workflow**: Connect CodeREADr with a security system to manage building access. Scan employee badges and use the API to check if they have access permissions. If access is granted, send a webhook to the security system to unlock the door and log the entry in a Google Sheet. diff --git a/components/coderpad/README.md b/components/coderpad/README.md index c39688ea1f1c9..fc93d1a351426 100644 --- a/components/coderpad/README.md +++ b/components/coderpad/README.md @@ -1,13 +1,11 @@ # Overview -CoderPad is a powerful online code editor that lets you easily collaborate with -others and test your code in real time. With CoderPad, you can easily write and -edit code online, share your code with others, and even run your code in real -time. +The CoderPad API on Pipedream allows developers to streamline technical interviews and coding assessments by automating the creation, management, and evaluation of coding pads. By integrating CoderPad with Pipedream, you can set up custom workflows that respond to events in your hiring pipeline, synchronize interview data with HR systems, evaluate coding performance, and much more. This seamless automation can save time, reduce manual error, and enhance the overall efficiency of technical assessments. -Here are some examples of what you can build with the CoderPad API: +# Example Use Cases -- A code editor for collaborative coding -- A real-time code testing tool -- A code sharing platform -- A code execution tool +- **Automated Pad Setup for Interviews**: When a new interview is scheduled in your calendar app (e.g., Google Calendar), Pipedream triggers a workflow that automatically creates a new CoderPad pad, sets it up with the necessary programming language or template, and sends an invite link to the candidate and interviewer. + +- **Interview Results Aggregation**: After an interview, the candidate's code submission can be automatically fetched from CoderPad, assessed using a code quality tool (like CodeClimate), and the results can be compiled and pushed to a database or sent via email to the hiring team for review. + +- **Real-Time Notifications for Coding Tests**: Set up a workflow that listens for when a candidate starts or completes a coding test in CoderPad. Upon this trigger, real-time notifications can be sent to a Slack channel or via SMS to the hiring team, keeping everyone informed about the candidate's progress. diff --git a/components/codescene/README.md b/components/codescene/README.md new file mode 100644 index 0000000000000..646c8f64fd1a6 --- /dev/null +++ b/components/codescene/README.md @@ -0,0 +1,11 @@ +# Overview + +The CodeScene API lets you dive into the technical and social aspects of your codebase. It offers insights into code health, identifies hotspots, and provides actionable information for technical debt management. Integrating CodeScene with Pipedream allows you to automate these insights, tying them into your workflow to ensure code quality and team efficiency. + +# Example Use Cases + +- **Automated Code Health Reports**: Schedule a Pipedream workflow to fetch code health metrics from CodeScene on a regular basis, then send a formatted report to your team's Slack channel or email, keeping everyone updated on potential problem areas. + +- **Pull Request Analysis Trigger**: Set up a workflow where each new pull request on GitHub triggers an analysis in CodeScene. The results can then be posted back as a comment on the PR, ensuring that code reviews are informed by CodeScene's findings before a merge. + +- **Project Health Dashboard Update**: Use Pipedream to pull project health data from CodeScene at regular intervals and update a project management tool like Jira or Trello with the latest metrics, providing a live dashboard of codebase health for project managers. diff --git a/components/cohere_platform/README.md b/components/cohere_platform/README.md new file mode 100644 index 0000000000000..2fca123cb4070 --- /dev/null +++ b/components/cohere_platform/README.md @@ -0,0 +1,11 @@ +# Overview + +The Cohere API enables the development of apps with advanced natural language understanding capabilities. Utilizing machine learning, it can help with tasks like text generation, summarization, sentiment analysis, and more. On Pipedream, you can seamlessly integrate Cohere's features into serverless workflows, triggering actions based on text input, processing large volumes of data, or even enhancing chatbots with more human-like responses. + +# Example Use Cases + +- **Automated Support Ticket Summarization**: When new support tickets arrive, trigger a workflow to summarize the content using Cohere's summarization feature. Save the summary in a data store, then post a message to a Slack channel to alert your team. + +- **Real-Time Sentiment Analysis for Customer Feedback**: Analyze customer feedback messages for sentiment with Cohere's sentiment analysis. Use this data to create a sentiment score, update a customer's profile in a CRM like Salesforce, or route positive/negative feedback to the appropriate team. + +- **Content Generation for Social Media Posts**: Generate initial draft content for social media posts based on input topics using Cohere's text generation. Review and edit the drafts within a CMS or directly post to platforms like Twitter or Facebook after approval. diff --git a/components/cohere_platform/actions/chat/chat.mjs b/components/cohere_platform/actions/chat/chat.mjs new file mode 100644 index 0000000000000..b30bddb4d25b7 --- /dev/null +++ b/components/cohere_platform/actions/chat/chat.mjs @@ -0,0 +1,99 @@ +import app from "../../cohere_platform.app.mjs"; +import { clearObj } from "../../common/utils.mjs"; + +export default { + key: "cohere_platform-chat", + name: "Chat", + version: "0.0.1", + description: "Generates a text response to a user message. [See the documentation](https://docs.cohere.com/reference/chat)", + type: "action", + props: { + app, + message: { + propDefinition: [ + app, + "message", + ], + }, + model: { + propDefinition: [ + app, + "model", + ], + }, + temperature: { + propDefinition: [ + app, + "temperature", + ], + }, + maxTokens: { + propDefinition: [ + app, + "maxTokens", + ], + }, + k: { + propDefinition: [ + app, + "k", + ], + }, + p: { + propDefinition: [ + app, + "p", + ], + }, + stopSequences: { + propDefinition: [ + app, + "stopSequences", + ], + }, + frequencyPenalty: { + propDefinition: [ + app, + "frequencyPenalty", + ], + }, + }, + methods: { + chat(data) { + return this.app.client().chat(data); + }, + }, + async run({ $ }) { + const { + chat, + message, + model, + temperature, + maxTokens, + k, + p, + stopSequences, + frequencyPenalty, + + } = this; + const response = await chat(clearObj({ + message, + model, + ...(temperature && { + temperature: parseFloat(temperature), + }), + maxTokens, + k, + ...(p && { + p: parseFloat(p), + }), + stopSequences, + ...(frequencyPenalty && { + frequencyPenalty: parseFloat(frequencyPenalty), + }), + })); + + $.export("$summary", `The message was successfully responded with ID \`${response.response_id}\``); + return response; + }, +}; diff --git a/components/cohere_platform/actions/choose-best-completion/choose-best-completion.mjs b/components/cohere_platform/actions/choose-best-completion/choose-best-completion.mjs deleted file mode 100644 index 3fa5939e43ae9..0000000000000 --- a/components/cohere_platform/actions/choose-best-completion/choose-best-completion.mjs +++ /dev/null @@ -1,28 +0,0 @@ -import common from "../common/base.mjs"; - -export default { - ...common, - key: "cohere_platform-choose-best-completion", - name: "Choose Best Completion", - version: "0.0.2", - description: "This action chooses the best completion conditioned on a given examples. [See the docs here](https://docs.cohere.ai/reference/generate)", - type: "action", - props: { - ...common.props, - prompt: { - propDefinition: [ - common.props.coherePlatform, - "prompt", - ], - type: "string[]", - }, - }, - methods: { - ...common.methods, - prepareAdditionalData({ prompt }) { - return { - prompt: prompt.toString(), - }; - }, - }, -}; diff --git a/components/cohere_platform/actions/classify-text/classify-text.mjs b/components/cohere_platform/actions/classify-text/classify-text.mjs index 643d3b5077765..5dfb46c84b30d 100644 --- a/components/cohere_platform/actions/classify-text/classify-text.mjs +++ b/components/cohere_platform/actions/classify-text/classify-text.mjs @@ -1,44 +1,46 @@ -import coherePlatform from "../../cohere_platform.app.mjs"; +import app from "../../cohere_platform.app.mjs"; +import { clearObj } from "../../common/utils.mjs"; export default { key: "cohere_platform-classify-text", name: "Classify Text", - version: "0.0.1", - description: "This action makes a prediction about which label fits the specified text inputs best. [See the docs here](https://docs.cohere.com/reference/classify)", + version: "0.1.0", + description: "This action makes a prediction about which label fits the specified text inputs best. [See the documentation](https://docs.cohere.com/reference/classify-1)", type: "action", props: { - coherePlatform, + app, inputs: { type: "string[]", label: "Inputs", description: "Represents a list of queries to be classified, each entry must not be empty. The maximum is 96 inputs.", }, - classifyModel: { + model: { propDefinition: [ - coherePlatform, + app, "classifyModel", ], }, preset: { propDefinition: [ - coherePlatform, + app, "preset", ], }, truncate: { propDefinition: [ - coherePlatform, + app, "truncate", ], }, numExamples: { type: "integer", label: "Number of Examples", - description: "To make a prediction, Classify uses provided examples of text + label pairs. Specify the number of examples to provide. Each example is a text string and its associated label/class. At least 2 labels must be provided.", + description: "To make a prediction, Classify uses provided examples of text + label pairs. Specify the number of examples to provide. Each example is a text string and its associated label/class. At least 2 unique labels must be provided and each label should have at least 2 different examples.", + default: 4, reloadProps: true, }, }, - async additionalProps() { + additionalProps() { const props = {}; for (let i = 0; i < this.numExamples; i++) { @@ -56,28 +58,40 @@ export default { return props; }, + methods: { + getExamples() { + const examples = []; + for (let i = 0; i < this.numExamples; i++) { + examples.push({ + text: this[`text_${i}`], + label: this[`label_${i}`], + }); + } + return examples; + }, + classifyText(data) { + return this.app.client().classify(data); + }, + }, async run({ $ }) { - const examples = []; - for (let i = 0; i < this.numExamples; i++) { - examples.push({ - text: this[`text_${i}`], - label: this[`label_${i}`], - }); - } - - const response = await this.coherePlatform.classifyText({ - inputs: this.inputs, - model: this.model, - preset: this.preset, - truncate: this.truncate, - examples, - }); + const { + classifyText, + inputs, + model, + preset, + truncate, + getExamples, + } = this; - if (response.statusCode != "200") { - throw new Error(response.body.message); - } + const response = await classifyText(clearObj({ + inputs, + model, + preset, + truncate, + examples: getExamples(), + })); - $.export("$summary", "The text was successfully classified."); + $.export("$summary", `The text was successfully classified with ID \`${response.id}\``); return response; }, }; diff --git a/components/cohere_platform/actions/common/base.mjs b/components/cohere_platform/actions/common/base.mjs deleted file mode 100644 index ae427a8f63d99..0000000000000 --- a/components/cohere_platform/actions/common/base.mjs +++ /dev/null @@ -1,147 +0,0 @@ -import { ConfigurationError } from "@pipedream/platform"; -import coherePlatform from "../../cohere_platform.app.mjs"; -import { clearObj } from "../../common/utils.mjs"; - -export default { - props: { - coherePlatform, - prompt: { - propDefinition: [ - coherePlatform, - "prompt", - ], - }, - model: { - propDefinition: [ - coherePlatform, - "model", - ], - }, - numGenerations: { - propDefinition: [ - coherePlatform, - "numGenerations", - ], - }, - maxTokens: { - propDefinition: [ - coherePlatform, - "maxTokens", - ], - }, - preset: { - propDefinition: [ - coherePlatform, - "preset", - ], - }, - temperature: { - propDefinition: [ - coherePlatform, - "temperature", - ], - }, - k: { - propDefinition: [ - coherePlatform, - "k", - ], - }, - p: { - propDefinition: [ - coherePlatform, - "p", - ], - }, - frequencyPenalty: { - propDefinition: [ - coherePlatform, - "frequencyPenalty", - ], - }, - endSequences: { - propDefinition: [ - coherePlatform, - "endSequences", - ], - }, - stopSequences: { - propDefinition: [ - coherePlatform, - "stopSequences", - ], - }, - returnLikelihoods: { - propDefinition: [ - coherePlatform, - "returnLikelihoods", - ], - }, - logitBias: { - propDefinition: [ - coherePlatform, - "logitBias", - ], - }, - truncate: { - propDefinition: [ - coherePlatform, - "truncate", - ], - }, - }, - methods: { - prepareData({ - numGenerations, - maxTokens, - temperature, - p, - frequencyPenalty, - endSequences, - stopSequences, - returnLikelihoods, - logitBias, - ...data - }) { - return { - num_generations: numGenerations, - max_tokens: maxTokens, - temperature: temperature && parseFloat(temperature), - p: p && parseFloat(p), - frequency_penalty: frequencyPenalty && parseFloat(frequencyPenalty), - end_sequences: endSequences, - stop_sequences: stopSequences, - retur_likelihoods: returnLikelihoods, - logit_bias: logitBias && Object.entries(logitBias).reduce((p, [ - k, - v, - ]) => ({ - ...p, - [k]: parseInt(v), - }), {}), - ...data, - }; - }, - prepareAdditionalData() { - return {}; - }, - }, - async run({ $ }) { - const { - coherePlatform, - prepareData, - prepareAdditionalData, - ...data - } = this; - - const response = await coherePlatform.generateText(clearObj({ - ...prepareData(data), - ...prepareAdditionalData(data), - })); - - if (response.statusCode != "200") throw new ConfigurationError(response.body.message); - - $.export("$summary", "The text was successfully generated!"); - return response; - }, -}; diff --git a/components/cohere_platform/actions/generate-text/generate-text.mjs b/components/cohere_platform/actions/generate-text/generate-text.mjs deleted file mode 100644 index 60bb6102ce74e..0000000000000 --- a/components/cohere_platform/actions/generate-text/generate-text.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import common from "../common/base.mjs"; - -export default { - ...common, - key: "cohere_platform-generate-text", - name: "Generate Text", - version: "0.0.2", - description: "This action generates realistic text conditioned on a given input. [See the docs here](https://docs.cohere.ai/reference/generate)", - type: "action", -}; diff --git a/components/cohere_platform/actions/summarize-text/summarize-text.mjs b/components/cohere_platform/actions/summarize-text/summarize-text.mjs deleted file mode 100644 index 6f38fb2984843..0000000000000 --- a/components/cohere_platform/actions/summarize-text/summarize-text.mjs +++ /dev/null @@ -1,57 +0,0 @@ -import coherePlatform from "../../cohere_platform.app.mjs"; - -export default { - key: "cohere_platform-summarize-text", - name: "Summarize Text", - version: "0.0.1", - description: "This action generates a summary in English for the given text. [See the docs here](https://docs.cohere.com/reference/summarize-2)", - type: "action", - props: { - coherePlatform, - text: { - type: "string", - label: "Text", - description: "The text to generate a summary for. Can be up to 100,000 characters long. Currently the only supported language is English.", - }, - temperature: { - propDefinition: [ - coherePlatform, - "temperature", - ], - }, - length: { - propDefinition: [ - coherePlatform, - "summaryLength", - ], - }, - format: { - propDefinition: [ - coherePlatform, - "summaryFormat", - ], - }, - model: { - propDefinition: [ - coherePlatform, - "summaryModel", - ], - }, - }, - async run({ $ }) { - const response = await this.coherePlatform.summarizeText({ - text: this.text, - temperature: this.temperature && parseFloat(this.temperature), - length: this.length, - format: this.format, - model: this.model, - }); - - if (response.statusCode != "200") { - throw new Error(response.body.message); - } - - $.export("$summary", "The text was successfully summarized."); - return response; - }, -}; diff --git a/components/cohere_platform/cohere_platform.app.mjs b/components/cohere_platform/cohere_platform.app.mjs index e020f18c085ce..7dbf06ea8a7c0 100644 --- a/components/cohere_platform/cohere_platform.app.mjs +++ b/components/cohere_platform/cohere_platform.app.mjs @@ -1,142 +1,86 @@ -import cohere from "cohere-ai"; +import { CohereClient } from "cohere-ai"; import constants from "./common/constants.mjs"; export default { type: "app", app: "cohere_platform", propDefinitions: { - endSequences: { - type: "string[]", - label: "End Sequences", - description: "The generated text will be cut at the beginning of the earliest occurence of an end sequence. The sequence will be excluded from the text.", - optional: true, - }, - frequencyPenalty: { + message: { type: "string", - label: "Frequency Penalty", - description: "Defaults to `0.0`, min value of `0.0`, max value of `1.0`. Can be used to reduce repetitiveness of generated tokens. Similar to `frequency_penalty`, except that this penalty is applied equally to all tokens that have already appeared, regardless of their exact frequencies.", - optional: true, + label: "Message", + description: "Text input for the model to respond to. Compatible Deployments: Cohere Platform, Azure, AWS Sagemaker, Private Deployments", }, - k: { - type: "integer", - label: "K", - description: "Defaults to `0`(disabled), which is the minimum. Maximum value is `500`. Ensures only the top `k` most likely tokens are considered for generation at each step.", - min: 1, - max: 500, + model: { + type: "string", + label: "Model", + description: "Defaults to `command-r-plus`. The name of a compatible [Cohere model](https://docs.cohere.com/docs/models) or the ID of a [fine-tuned](https://docs.cohere.com/docs/chat-fine-tuning) model. Compatible Deployments: Cohere Platform, Private Deployments.", + options: constants.MODEL_OPTIONS, optional: true, }, - logitBias: { - type: "object", - label: "Logit Bias", - description: "Used to prevent the model from generating unwanted tokens or to incentivize it to include desired tokens. The format is `{token_id: bias}` where bias is a float between `-10` and `10`. Tokens can be obtained from text using [Tokenize](https://docs.cohere.ai/reference/tokenize). For example, if the value `{'11': -10}` is provided, the model will be very unlikely to include the token 11 (`\"\n\"`, the newline character) anywhere in the generated text. In contrast `{'11': 10}` will result in generations that nearly only contain that token. Values between -10 and 10 will proportionally affect the likelihood of the token appearing in the generated text. Note: logit bias may not be supported for all custom models.", + temperature: { + type: "string", + label: "Temperature", + description: "Must be between 0 and 1.0 inclusive that tunes the degree of randomness in generation. Lower temperatures mean less random generations, and higher temperatures mean more random generations.\nRandomness can be further maximized by increasing the value of the **P** parameter. Compatible Deployments: Cohere Platform, Azure, AWS Sagemaker, Private Deployments See [Temperature](https://docs.cohere.ai/docs/temperature) for more details.", optional: true, }, maxTokens: { type: "integer", label: "Max Tokens", - description: "Denotes the number of tokens to predict per generation, defaults to 20. See [BPE Tokens](https://docs.cohere.ai/docs/tokens) for more details. Can only be set to `0` if `return_likelihoods` is set to `ALL` to get the likelihood of the prompt.", - optional: true, - }, - model: { - type: "string", - label: "Model", - description: "The size of the model. Currently available models are `medium` and `xlarge` (default). Smaller models are faster, while larger models will perform better. [Custom models](https://docs.cohere.ai/docs/training-custom-models) can also be supplied with their full ID.", - options: constants.MODEL_OPTIONS, + description: "The maximum number of tokens the model will generate as part of the response. Note: Setting a low value may result in incomplete generations. Compatible Deployments: Cohere Platform, Azure, AWS Sagemaker, Private Deployments. See [BPE Tokens](https://docs.cohere.ai/docs/tokens) for more details.", optional: true, }, - numGenerations: { + k: { type: "integer", - label: "Number of generations", - description: "Defaults to 1, min value of 1, max value of 5. Denotes the maximum number of generations that will be returned.", - min: 1, - max: 5, + label: "K", + description: "Ensures only the top **K** most likely tokens are considered for generation at each step. Defaults to `0`, min value of `0`, max value of `500`. Compatible Deployments: Cohere Platform, Azure, AWS Sagemaker, Private Deployments", + min: 0, + max: 500, optional: true, }, p: { type: "string", label: "P", - description: "Defaults to `0.75`. Set to `1.0` or `0` to disable. If set to a probability `0.0 < p < 1.0`, it ensures that only the most likely tokens, with total probability mass of `p`, are considered for generation at each step. If both `k` and `p` are enabled, `p` acts after `k`.", - optional: true, - }, - preset: { - type: "string", - label: "Preset", - description: "The ID of a custom playground preset. You can create presets in the [playground](https://dashboard.cohere.ai/playground/generate). If you use a preset, all other parameters become optional, and any included parameters will override the preset's parameters.", - optional: true, - }, - prompt: { - type: "string", - label: "Prompt", - description: "Represents the prompt or text to be completed. Trailing whitespaces will be trimmed. If your use case requires trailing whitespaces, please contact ivan@cohere.ai.", - }, - returnLikelihoods: { - type: "string", - label: "Return Likelihoods", - description: "It specifies how and if the token likelihoods are returned with the response. Defaults to `NONE`. If `GENERATION` is selected, the token likelihoods will only be provided for generated text. If `ALL` is selected, the token likelihoods will be provided both for the prompt and the generated text.", - options: constants.RETURN_LIKELIHOODS_OPTIONS, + description: "Ensures that only the most likely tokens, with total probability mass of **P**, are considered for generation at each step. If both **K** and **P** are enabled, **P** acts after **K**. Defaults to `0.75`. min value of `0.01`, max value of `0.99`. Compatible Deployments: Cohere Platform, Azure, AWS Sagemaker, Private Deployments", optional: true, }, stopSequences: { type: "string[]", label: "Stop Sequences", - description: "The generated text will be cut at the end of the earliest occurence of a stop sequence. The sequence will be included the text.", + description: "A list of up to 5 strings that the model will use to stop generation. If the model generates a string that matches any of the strings in the list, it will stop generating tokens and return the generated text up to that point not including the stop sequence. Compatible Deployments: Cohere Platform, Azure, AWS Sagemaker, Private Deployments", optional: true, }, - temperature: { - type: "string", - label: "Temperature", - description: "Defaults to `0.75`, min value of `0.0`, max value of `5.0`. A non-negative float that tunes the degree of randomness in generation. Lower temperatures mean less random generations. See [Temperature](https://docs.cohere.ai/docs/temperature) for more details.", - optional: true, - }, - truncate: { + frequencyPenalty: { type: "string", - label: "Truncate", - description: "It specifies how the API will handle inputs longer than the maximum token length. Passing `START` will discard the start of the input. `END` will discard the end of the input. In both cases, input is discarded until the remaining input is exactly the maximum input token length for the model. If `NONE` is selected, when the input exceeds the maximum input token length an error will be returned.", - options: constants.TRUNCATE_OPTIONS, + label: "Frequency Penalty", + description: "Defaults to `0.0`, min value of `0.0`, max value of `1.0`. Used to reduce repetitiveness of generated tokens. The higher the value, the stronger a penalty is applied to previously present tokens, proportional to how many times they have already appeared in the prompt or prior generation. Compatible Deployments: Cohere Platform, Azure, AWS Sagemaker, Private Deployments", optional: true, }, classifyModel: { type: "string", label: "Model", - description: "The size of the model.", + description: "The identifier of the model. Currently available models are `embed-multilingual-v2.0`, `embed-english-light-v2.0`, and `embed-english-v2.0` (default). Smaller light models are faster, while larger models will perform better. [Fine-tuned models](https://docs.cohere.com/docs/fine-tuning) can also be supplied with their full ID.", options: constants.CLASSIFY_MODEL_OPTIONS, optional: true, }, - summaryLength: { - type: "string", - label: "Length", - description: "Indicates the approximate length of the summary.", - options: constants.SUMMARY_LENGTH_OPTIONS, - optional: true, - }, - summaryFormat: { + preset: { type: "string", - label: "Format", - description: "Indicates the style in which the summary will be delivered - in a free form paragraph or in bullet points.", - options: constants.SUMMARY_FORMAT_OPTIONS, + label: "Preset", + description: "The ID of a custom playground preset. You can create presets in the [playground](https://dashboard.cohere.com/playground/classify?model=large). If you use a preset, all other parameters become optional, and any included parameters will override the preset's parameters.", optional: true, }, - summaryModel: { + truncate: { type: "string", - label: "Model", - description: "The ID of the model to generate the summary with. Smaller models are faster, while larger models will perform better.", - options: constants.SUMMARY_MODEL_OPTIONS, + label: "Truncate", + description: "One of `NONE|START|END` to specify how the API will handle inputs longer than the maximum token length. Passing `START` will discard the start of the input. `END` will discard the end of the input. In both cases, input is discarded until the remaining input is exactly the maximum input token length for the model. If `NONE` is selected, when the input exceeds the maximum input token length an error will be returned.", optional: true, + options: constants.TRUNCATE_OPTIONS, }, }, methods: { - api() { - cohere.init(this.$auth.api_key); - return cohere; - }, - generateText(data) { - return this.api().generate(data); - }, - classifyText(data) { - return this.api().classify(data); - }, - summarizeText(data) { - return this.api().summarize(data); + client() { + return new CohereClient({ + token: this.$auth.api_key, + }); }, }, }; diff --git a/components/cohere_platform/common/constants.mjs b/components/cohere_platform/common/constants.mjs index 224a867190c3c..66c6c9283c118 100644 --- a/components/cohere_platform/common/constants.mjs +++ b/components/cohere_platform/common/constants.mjs @@ -1,12 +1,21 @@ const MODEL_OPTIONS = [ - "medium", - "xlarge", -]; - -const RETURN_LIKELIHOODS_OPTIONS = [ - "GENERATION", - "ALL", - "NONE", + "command-r-plus", + "command-r", + "command", + "command-nightly", + "command-light", + "command-light-nightly", + "embed-english-v3.0", + "embed-english-light-v3.0", + "embed-multilingual-v3.0", + "embed-multilingual-light-v3.0", + "embed-english-v2.0", + "embed-english-light-v2.0", + "embed-multilingual-v2.0", + "rerank-english-v3.0", + "rerank-multilingual-v3.0", + "rerank-english-v2.0", + "rerank-multilingual-v2.0", ]; const TRUNCATE_OPTIONS = [ @@ -21,30 +30,8 @@ const CLASSIFY_MODEL_OPTIONS = [ "embed-english-v2.0", ]; -const SUMMARY_LENGTH_OPTIONS = [ - "short", - "medium", - "long", - "auto", -]; - -const SUMMARY_FORMAT_OPTIONS = [ - "paragraph", - "bullets", - "auto", -]; - -const SUMMARY_MODEL_OPTIONS = [ - "summarize-medium", - "summarize-xlarge", -]; - export default { MODEL_OPTIONS, - RETURN_LIKELIHOODS_OPTIONS, TRUNCATE_OPTIONS, CLASSIFY_MODEL_OPTIONS, - SUMMARY_LENGTH_OPTIONS, - SUMMARY_FORMAT_OPTIONS, - SUMMARY_MODEL_OPTIONS, }; diff --git a/components/cohere_platform/package.json b/components/cohere_platform/package.json index d918f4b6156f2..884bc6e63fff5 100644 --- a/components/cohere_platform/package.json +++ b/components/cohere_platform/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/cohere_platform", - "version": "0.1.0", + "version": "0.2.0", "description": "Pipedream Cohere Components", "main": "cohere_platform.app.mjs", "keywords": [ @@ -13,6 +13,6 @@ "access": "public" }, "dependencies": { - "cohere-ai": "^6.1.0" + "cohere-ai": "^7.10.6" } } diff --git a/components/coinbase/README.md b/components/coinbase/README.md index 53a4cb69f72fe..a65d66ca08276 100644 --- a/components/coinbase/README.md +++ b/components/coinbase/README.md @@ -1,11 +1,11 @@ # Overview -The Coinbase API allows you to build a variety of different applications, -including: - -- A cryptocurrency exchange -- A digital wallet -- A payment processor -- A way to track cryptocurrency prices -- A way to buy and sell cryptocurrency -- A way to accept cryptocurrency payments +The Coinbase API on Pipedream allows you to craft automated workflows that can manage your cryptocurrency assets efficiently. Through the API, you can get real-time information about exchange rates, your transaction history, and account balances. Automate buying and selling decisions based on set criteria, receive notifications for price changes, and synchronize your Coinbase data with other financial tracking tools. + +# Example Use Cases + +- **Automated Trading Bot**: Create a workflow that triggers a buy or sell order based on specific market conditions. For example, if Bitcoin drops to a certain price, the workflow could automatically purchase a predefined amount of Bitcoin. + +- **Real-time Alerts**: Set up a workflow that monitors the prices of your favorite cryptocurrencies and sends you real-time alerts through email, SMS, or Slack when certain price thresholds are crossed. + +- **Portfolio Synchronization**: Build a workflow that periodically fetches your transaction history and balance from Coinbase and updates a Google Sheet, allowing you to maintain an up-to-date view of your portfolio alongside other investments. diff --git a/components/coinbase_commerce/README.md b/components/coinbase_commerce/README.md index ebcf389c60eb9..5bea686ed6774 100644 --- a/components/coinbase_commerce/README.md +++ b/components/coinbase_commerce/README.md @@ -1,14 +1,11 @@ # Overview -The Coinbase Commerce API enables developers to easily integrate Coinbase -Commerce into their websites or apps. With the API, businesses can accept -crypto payments from customers around the world with just a few lines of code. - -Coinbase Commerce can be used to build a variety of different applications, -including: - -- E-commerce websites or apps -- Point-of-sale systems -- Invoicing and billing platforms -- Donation platforms -- Cryptocurrency wallets +The Coinbase Commerce API enables developers to seamlessly integrate cryptocurrency payments into their websites or applications. With this API, you can automate the process of creating charges, managing invoices, and tracking payments in various cryptocurrencies. Using Pipedream's serverless platform, you can connect Coinbase Commerce to hundreds of other apps to streamline payment processing, trigger actions based on payment status, and track financial data across your business tools. + +# Example Use Cases + +- **Automate Invoice Generation & Payment Confirmation Emails**: Create a workflow that triggers whenever a new charge is created in Coinbase Commerce. Once the charge is detected, automatically generate an invoice using a service like QuickBooks, and send a payment confirmation email to the customer using SendGrid or Gmail once the payment is confirmed. + +- **Sync Payment Data with Accounting Software**: Set up a Pipedream workflow that listens for payment events from Coinbase Commerce. When a payment is successful, the workflow can log this transaction in your accounting software, such as Xero or FreshBooks, ensuring your financial records are always up-to-date without manual data entry. + +- **Manage Inventory Based on Crypto Payments**: Connect Coinbase Commerce with an inventory management system like Shopify or WooCommerce. Use Pipedream to create a workflow that reduces stock levels once a cryptocurrency payment is successful, and if the stock hits a low threshold, automatically send a replenishment order to your supplier or notify your logistics team. diff --git a/components/coinlore/README.md b/components/coinlore/README.md index b614d516ef382..21af862f54495 100644 --- a/components/coinlore/README.md +++ b/components/coinlore/README.md @@ -1,10 +1,11 @@ # Overview -The CoinLore API allows you to access the latest cryptocurrency prices, as well -as information on individual coins. You can use this data to build a variety of -applications, such as: - -- A cryptocurrency price tracker -- A cryptocurrency portfolio manager -- A cryptocurrency news aggregator -- A cryptocurrency price comparison website +The CoinLore API provides real-time access to cryptocurrency market data, allowing you to fetch current prices, historical statistics, ticker information, and global market metrics. Leveraging Pipedream's serverless platform enables you to create automated workflows that can react to changes in cryptocurrency data, integrate with other services, and perform actions based on crypto market conditions. + +# Example Use Cases + +- **Crypto Alert System**: Build a workflow that triggers at regular intervals, fetching the latest price of a specific cryptocurrency from CoinLore. If the price hits a predefined threshold, send an alert via email or SMS using the integrated SendGrid or Twilio service on Pipedream. This keeps you informed about significant market movements without having to constantly monitor prices. + +- **Portfolio Value Tracker**: Create a workflow that periodically retrieves your portfolio data from CoinLore, calculates the total value based on current market prices, and logs this information to a Google Sheet. Use this to track the value of your holdings over time, drawing on Pipedream's ability to seamlessly connect to Google's services. + +- **Automated Crypto Trading**: Construct a Pipedream workflow that uses CoinLore API to monitor the market conditions for a set of cryptocurrencies. When specific trading signals are met, such as a rapid price increase or volume surge, the workflow could execute trades on a connected exchange like Binance or Coinbase through their respective APIs, enabling automated trading strategies. diff --git a/components/coinlore/package.json b/components/coinlore/package.json new file mode 100644 index 0000000000000..6afed4c5318a4 --- /dev/null +++ b/components/coinlore/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/coinlore", + "version": "0.6.0", + "description": "Pipedream coinlore Components", + "main": "coinlore.app.mjs", + "keywords": [ + "pipedream", + "coinlore" + ], + "homepage": "https://pipedream.com/apps/coinlore", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/coinmarketcal/README.md b/components/coinmarketcal/README.md new file mode 100644 index 0000000000000..577e1e5c9f22f --- /dev/null +++ b/components/coinmarketcal/README.md @@ -0,0 +1,11 @@ +# Overview + +CoinMarketCal API delivers information on cryptocurrency events. It can serve as a critical data source for market sentiment analysis, trading, and investment strategies. With Pipedream, you can trigger workflows based on this data, process and analyze it, or connect it to other services to create comprehensive crypto market tools. + +# Example Use Cases + +- **Automated Trading Alerts**: Create a Pipedream workflow that monitors CoinMarketCal for key events like coin listings or hard forks, then sends these alerts to your messaging platform of choice, such as Slack or Discord, enabling you to make timely trading decisions. + +- **Sentiment Analysis Pipeline**: Leverage the CoinMarketCal API to feed event data into a sentiment analysis model hosted on services like Google Cloud's Natural Language API. Combine the sentiment scores with other market data to inform your crypto investment strategy. + +- **Calendar Sync for Events**: Sync cryptocurrency events from CoinMarketCal to your Google Calendar using Pipedream's built-in Google Calendar integration. This keeps you up-to-date on important market events without manually checking multiple sources. diff --git a/components/coinmarketcal/actions/search-events/search-events.mjs b/components/coinmarketcal/actions/search-events/search-events.mjs index 24ad3bae5c222..2650566cd981d 100644 --- a/components/coinmarketcal/actions/search-events/search-events.mjs +++ b/components/coinmarketcal/actions/search-events/search-events.mjs @@ -8,7 +8,7 @@ import { export default { key: "coinmarketcal-search-events", name: "Search Events", - version: "0.0.1", + version: "0.0.2", description: "Retrieve a list of events based on specified filters. [See the docs here](https://coinmarketcal.com/en/doc/redoc#/paths/~1events/get)", type: "action", props: { @@ -59,12 +59,6 @@ export default { description: "Show the number of views of the event.", optional: true, }, - showVotes: { - type: "boolean", - label: "Show Votes", - description: "Show the number of votes of the event.", - optional: true, - }, translations: { type: "string", label: "Translations", diff --git a/components/coinmarketcal/package.json b/components/coinmarketcal/package.json index 7c0480a372d92..93a7f6777ac19 100644 --- a/components/coinmarketcal/package.json +++ b/components/coinmarketcal/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/coinmarketcal", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream CoinMarketCal Components", "main": "coinmarketcal.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.4" } } diff --git a/components/coinmarketcap/README.md b/components/coinmarketcap/README.md index 1470adf8eaf78..dcbb12dab9c5d 100644 --- a/components/coinmarketcap/README.md +++ b/components/coinmarketcap/README.md @@ -1,15 +1,11 @@ # Overview -With the CoinMarketCap API, you can: +The CoinMarketCap API delivers real-time and historical cryptocurrency market data, including price, volume, market cap, and much more, for over 9,000 cryptocurrencies. With this data, you can track crypto trends, compare currency performance, or integrate up-to-date information into apps, widgets, or websites. Pipedream's platform enables developers to create automated workflows that can harness the vast array of data from CoinMarketCap to trigger actions, notify stakeholders, or power analytics dashboards. -- Get the latest prices for all cryptocurrencies -- Get the 24-hour volume for all cryptocurrencies -- Get the market capitalization for all cryptocurrencies -- Get the list of all cryptocurrencies +# Example Use Cases -Here are some examples of what you can build with the CoinMarketCap API: +- **Crypto Market Alert System**: Create a workflow that monitors the price of specific cryptocurrencies and sends alerts via email or SMS when the price drops or rises past certain thresholds. This can be connected to Twilio for SMS alerts or to email services like SendGrid. -- A cryptocurrency price tracker -- A cryptocurrency trading bot -- A cryptocurrency portfolio manager -- A cryptocurrency news site +- **Portfolio Value Tracker**: Construct a workflow to calculate the total value of a cryptocurrency portfolio by fetching current prices and multiplying by the quantity of each asset held. This data can be sent periodically to a Google Sheet for tracking and historical comparison using Pipedream's Google Sheets integration. + +- **Automated Trading Signals**: Develop a system that analyzes the market data from CoinMarketCap, such as price changes and trading volumes, to generate trading signals. These signals can then be used to trigger trades on a connected trading platform or to notify a Discord channel using Pipedream's Discord app integration. diff --git a/components/coinranking/README.md b/components/coinranking/README.md index 52ea9a0d60023..1b85d3ee44658 100644 --- a/components/coinranking/README.md +++ b/components/coinranking/README.md @@ -1,9 +1,11 @@ # Overview -What can I build using the Coinranking API? +The Coinranking API provides real-time access to cryptocurrency data such as prices, market caps, and coin information. This powerful data stream, when harnessed through Pipedream, can automate tasks, generate alerts, and integrate with a plethora of other apps to analyze trends, sync data across platforms, or kick off workflows based on crypto market movements. With Pipedream's serverless platform, you can build complex automations without managing infrastructure, allowing you to focus on strategy and analysis. -- A crypto currency price tracker -- A way to compare crypto currencies -- A portfolio manager for crypto currencies -- A news aggregator for crypto currencies -- A tool to convert between fiat currencies and crypto currencies +# Example Use Cases + +- **Price Alert System**: Create a Pipedream workflow that monitors cryptocurrency prices using Coinranking API. Trigger actions, like sending an email via SendGrid or a direct message on Slack, when your target crypto hits a certain price, ensuring you never miss an investment opportunity. + +- **Portfolio Tracker**: Build a serverless function on Pipedream that periodically checks your cryptocurrency portfolio's value by fetching current prices from the Coinranking API. You could log this data to a Google Sheet to track historical performance or use a chart API to visualize your portfolio growth over time. + +- **Automated Trading**: Leverage Pipedream's capabilities with the Coinranking API to implement a simple trading bot. Set criteria for buying or selling cryptocurrencies and use integrations with exchange APIs to execute trades automatically when certain conditions, like specific price points or trends, are met. diff --git a/components/coinranking/package.json b/components/coinranking/package.json new file mode 100644 index 0000000000000..96db4cea97c7c --- /dev/null +++ b/components/coinranking/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/coinranking", + "version": "0.6.0", + "description": "Pipedream coinranking Components", + "main": "coinranking.app.mjs", + "keywords": [ + "pipedream", + "coinranking" + ], + "homepage": "https://pipedream.com/apps/coinranking", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/college_football_data/README.md b/components/college_football_data/README.md index 6836a9a90d03b..a478fd9d6c6cd 100644 --- a/components/college_football_data/README.md +++ b/components/college_football_data/README.md @@ -1,12 +1,11 @@ # Overview -With the College Football Data API, you can access data for every college -football team, including game scores, statistics, recruits, and more. This data -can be used to build a variety of applications, such as: - -- A college football news app, featuring the latest headlines and scores -- A college football stats app, featuring live game statistics and player stats -- A college football recruiting app, featuring the latest news and information - on recruits -- A college football game simulator, allowing users to simulation matchups and - predict outcomes +The College Football Data API is a treasure trove for developers and sports analysts wanting to tap into detailed college football statistics and scores. With this API, you can fetch a wide array of data, from team stats to player metrics and game results. Harness this information to fuel apps, dashboards, and automated reporting systems, or to enrich your sports-related datasets. + +# Example Use Cases + +- **Automated Game Summary Reports**: Generate post-game summary reports by fetching recent game data and statistics. Use Pipedream to trigger this workflow at the end of scheduled games, format the data into a readable report, and send it via email or post it to a Slack channel for immediate team review. + +- **Fantasy Football Insights**: Create a fantasy football assistant that pulls player performance data, injury reports, and team stats. It can run weekly on Pipedream, process the data to give insights on player picks, and push notifications via Twilio or a Telegram bot to inform fantasy league participants. + +- **Historical Data Analysis for Recruiting**: For college football recruiters, compile historical performance data on potential recruits. With Pipedream, schedule regular data pulls for specified prospects, store data in a cloud service like Google Sheets, and use it to power data visualizations that help in making informed recruiting decisions. diff --git a/components/columns_ai/actions/build-graph-from-scratch/build-graph-from-scratch.mjs b/components/columns_ai/actions/build-graph-from-scratch/build-graph-from-scratch.mjs new file mode 100644 index 0000000000000..cc9adec34625f --- /dev/null +++ b/components/columns_ai/actions/build-graph-from-scratch/build-graph-from-scratch.mjs @@ -0,0 +1,152 @@ +import { ChartType } from "columns-graph-model"; +import app from "../../columns_ai.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "columns_ai-build-graph-from-scratch", + name: "Build Graph From Scratch", + description: "Builds a graph object from scratch and publishes it. [See the documentation](https://github.com/varchar-io/vaas?tab=readme-ov-file#basic-usage)", + version: "0.0.2", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "name", + ], + }, + chartType: { + type: "string", + label: "Chart Type", + description: "The type of chart to construct", + async options() { + return [ + { + label: "Bar", + value: ChartType.BAR, + }, + { + label: "Pie", + value: ChartType.PIE, + }, + { + label: "Doughnut", + value: ChartType.DOUGHNUT, + }, + { + label: "Line", + value: ChartType.LINE, + }, + { + label: "Area", + value: ChartType.AREA, + }, + { + label: "Scatter", + value: ChartType.SCATTER, + }, + { + label: "Bar Race", + value: ChartType.BAR_RACE, + }, + { + label: "Boxplot", + value: ChartType.BOXPLOT, + }, + { + label: "Column", + value: ChartType.COLUMN, + }, + { + label: "Dot", + value: ChartType.DOT, + }, + { + label: "Gauge", + value: ChartType.GAUGE, + }, + { + label: "Map", + value: ChartType.MAP, + }, + { + label: "Radar", + value: ChartType.RADAR, + }, + { + label: "Summary", + value: ChartType.SUMMARY, + }, + { + label: "Table", + value: ChartType.TABLE, + }, + { + label: "Timeline", + value: ChartType.TIMELINE, + }, + { + label: "Timeline Area", + value: ChartType.TIMELINE_AREA, + }, + { + label: "Timeline Bar", + value: ChartType.TIMELINE_BAR, + }, + { + label: "Tree", + value: ChartType.TREE, + }, + { + label: "Wordcloud", + value: ChartType.WORDCLOUD, + }, + ]; + }, + }, + keys: { + propDefinition: [ + app, + "keys", + ], + }, + metrics: { + propDefinition: [ + app, + "metrics", + ], + }, + rows: { + propDefinition: [ + app, + "rows", + ], + }, + }, + async run({ $ }) { + const { + app, + name, + chartType, + keys, + metrics, + rows, + } = this; + + const graph = await app.createGraphFromScratch({ + chartType, + keys, + metrics, + rows: utils.parseArray(rows), + }); + + const response = await app.publishGraph({ + name, + graph, + }); + + $.export("$summary", "Successfully built and published the graph from scratch."); + return response; + }, +}; diff --git a/components/columns_ai/actions/build-graph-from-template/build-graph-from-template.mjs b/components/columns_ai/actions/build-graph-from-template/build-graph-from-template.mjs new file mode 100644 index 0000000000000..62fa048820218 --- /dev/null +++ b/components/columns_ai/actions/build-graph-from-template/build-graph-from-template.mjs @@ -0,0 +1,67 @@ +import app from "../../columns_ai.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "columns_ai-build-graph-from-template", + name: "Build Graph From Template", + description: "Builds a graph object from a template and publishes it. [See the documentation](https://github.com/varchar-io/vaas?tab=readme-ov-file#basic-usage).", + version: "0.0.2", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "name", + ], + }, + visualId: { + type: "string", + label: "Visual ID", + description: "The ID of an existing graph template on Columns. Eg. `U6tALuJ3cTdPFw` wher it can be taken from the URL `https://columns.ai/visual/view/U6tALuJ3cTdPFw`.", + }, + keys: { + propDefinition: [ + app, + "keys", + ], + }, + metrics: { + propDefinition: [ + app, + "metrics", + ], + }, + rows: { + propDefinition: [ + app, + "rows", + ], + }, + }, + async run({ $ }) { + const { + app, + name, + visualId, + keys, + metrics, + rows, + } = this; + + const graph = await app.createGraphFromTemplate({ + visualId, + keys, + metrics, + rows: utils.parseArray(rows), + }); + + const response = await app.publishGraph({ + name, + graph, + }); + + $.export("$summary", "Successfully built and published the graph from template."); + return response; + }, +}; diff --git a/components/columns_ai/columns_ai.app.mjs b/components/columns_ai/columns_ai.app.mjs new file mode 100644 index 0000000000000..b656937fdf7cb --- /dev/null +++ b/components/columns_ai/columns_ai.app.mjs @@ -0,0 +1,65 @@ +import { ChartType } from "columns-graph-model"; +import { Columns } from "columns-sdk"; + +export default { + type: "app", + app: "columns_ai", + propDefinitions: { + name: { + type: "string", + label: "Graph Name", + description: "The name of the graph", + }, + keys: { + type: "string[]", + label: "Keys", + description: "An array of keys for the data rows.", + }, + metrics: { + type: "string[]", + label: "Metrics", + description: "An array of metrics for the data rows.", + }, + rows: { + type: "string[]", + label: "Rows", + description: "An array of data objects, where each object should be a JSON string. Eg. `{ \"metric\": 4000, \"key\": \"US\", \"parent\": null }`.", + }, + }, + methods: { + getColumns() { + return new Columns(this.$auth.api_key); + }, + async createGraphFromScratch({ + keys = [], metrics = [], rows = [], chartType = ChartType.COLUMN, + } = {}) { + const columns = this.getColumns(); + const data = columns.data(keys, metrics, rows); + const graph = columns.graph(data); + + graph.type = chartType; + + return graph; + }, + async createGraphFromTemplate({ + visualId, keys = [], metrics = [], rows = [], + } = {}) { + const columns = this.getColumns(); + const graph = await columns.template(visualId); + + if (!graph) { + throw new Error(`Failed to load template from Columns: ${visualId}`); + } + + graph.data = columns.data(keys, metrics, rows); + + return graph; + }, + publishGraph({ + name, graph, + } = {}) { + const columns = this.getColumns(); + return columns.publish(name, graph); + }, + }, +}; diff --git a/components/columns_ai/common/utils.mjs b/components/columns_ai/common/utils.mjs new file mode 100644 index 0000000000000..f48102438db77 --- /dev/null +++ b/components/columns_ai/common/utils.mjs @@ -0,0 +1,51 @@ +import { ConfigurationError } from "@pipedream/platform"; + +const parseJson = (input) => { + const parse = (value) => { + if (typeof(value) === "string") { + try { + return parseJson(JSON.parse(value)); + } catch (e) { + return value; + } + } else if (typeof(value) === "object" && value !== null) { + return Object.entries(value) + .reduce((acc, [ + key, + val, + ]) => Object.assign(acc, { + [key]: parse(val), + }), {}); + } + return value; + }; + + return parse(input); +}; + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +export default { + parseArray: (value) => parseArray(value).map(parseJson), +}; diff --git a/components/columns_ai/package.json b/components/columns_ai/package.json new file mode 100644 index 0000000000000..a23a70d680d7b --- /dev/null +++ b/components/columns_ai/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/columns_ai", + "version": "0.1.2", + "description": "Pipedream Columns Ai Components", + "main": "columns_ai.app.mjs", + "keywords": [ + "pipedream", + "columns_ai" + ], + "homepage": "https://pipedream.com/apps/columns_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "columns-graph-model": "^1.1.4", + "columns-sdk": "^0.0.6" + } +} diff --git a/components/cometly/README.md b/components/cometly/README.md new file mode 100644 index 0000000000000..c0fb1e9a4dcb6 --- /dev/null +++ b/components/cometly/README.md @@ -0,0 +1,11 @@ +# Overview + +The Cometly API allows you to track ad performance and automate the analysis and optimization of advertising campaigns. With Pipedream, you can create serverless workflows that interact with the Cometly API to streamline campaign management tasks, such as retrieving campaign metrics, updating ad spend, and generating performance reports. Using Cometly in conjunction with Pipedream’s robust integration capabilities, you can connect to numerous other services to enrich your ads data, automate actions based on performance thresholds, and synchronize your marketing efforts across platforms. + +# Example Use Cases + +- **Campaign Metrics to Google Sheets**: Extract daily ad performance metrics from Cometly and append them to a Google Sheets spreadsheet for easy tracking and visualization. This workflow enables you to maintain an up-to-date record of your campaign's progress and share it with your team or clients. + +- **Slack Alerts for Performance Thresholds**: Set up a Pipedream workflow that monitors your Cometly campaign metrics and sends alerts to a Slack channel when performance dips below a certain threshold. This immediate notification allows for quick reaction to optimize campaigns and control ad spend. + +- **Email Reports with Mailgun**: Generate weekly performance reports by aggregating Cometly ad data and using Mailgun to send customized email summaries to stakeholders. This workflow ensures regular updates on campaign status and ROI, helping with informed decision-making. diff --git a/components/commcare/README.md b/components/commcare/README.md index ae939f5448a68..f64676702f283 100644 --- a/components/commcare/README.md +++ b/components/commcare/README.md @@ -1,6 +1,11 @@ # Overview -The CommCare API allows developers to access and manipulate data within the -CommCare platform. This includes data related to CommCare applications, users, -cases, and more. With the CommCare API, developers can build tools and -applications that work with CommCare data. +The CommCare API provides programmatic access to CommCareHQ data, allowing developers to build powerful automations and integrations. With this API, you can retrieve and update data such as forms, cases, and users. These capabilities enable you to streamline workflows, synchronize datasets across platforms, and extend CommCare's functionality. + +# Example Use Cases + +- **Automated Case Management Alerts**: Trigger notifications in Slack or via email using Pipedream when a case in CommCare reaches a specific status, ensuring prompt action from case workers or supervisors. For instance, set an alert when a case is marked 'urgent' to accelerate response times. + +- **Data Synchronization Between CommCare and a Database**: Create a workflow on Pipedream that regularly syncs case data from CommCare to a cloud database like AWS RDS or Google Cloud SQL. This can be used for advanced analytics, reporting, or to maintain a backup of critical case information. + +- **Dynamic Form Submission Processing**: When a new form is submitted in CommCare, use Pipedream to parse the data and take different actions based on the content. For example, if a form indicates a health facility is out of stock on a medication, automatically reorder supplies from a connected inventory management system. diff --git a/components/commercehq/README.md b/components/commercehq/README.md index 18d83a3773830..de6b9c31ee541 100644 --- a/components/commercehq/README.md +++ b/components/commercehq/README.md @@ -1,11 +1,11 @@ # Overview -CommerceHQ provides a powerful and flexible API that enables you to easily -create, manage, and sell online courses and products. With the CommerceHQ API, -you can: - -- Create and manage courses -- Create and manage products -- Sell courses and products online -- Integrate with your existing website or blog -- Access course and product data via the API +The CommerceHQ API enables merchants to craft seamless e-commerce experiences by automating various aspects of their online store. This API offers endpoints to manage products, orders, customers, and cart operations. With Pipedream, you can harness these capabilities to create custom automation workflows that connect CommerceHQ with a multitude of other services, streamlining operations, enhancing customer engagement, and driving sales. + +# Example Use Cases + +- **Order Fulfillment Automation**: When a new order comes in on CommerceHQ, trigger a workflow on Pipedream to automatically send order details to a fulfillment service like Shipstation. This speeds up the delivery process, ensuring customers receive their purchases promptly. + +- **Inventory Management**: Set up a Pipedream workflow that monitors CommerceHQ product stock levels. When inventory dips below a specified threshold, automatically generate purchase orders and send them to suppliers via email or a service like QuickBooks for reordering, keeping stock levels optimal. + +- **Customer Retention Activities**: Use Pipedream to track customer order history from CommerceHQ. If a customer hasn't made a purchase within a specific time frame, trigger an email marketing campaign through Mailchimp, offering a special discount to re-engage them and encourage repeat purchases. diff --git a/components/commercetools/README.md b/components/commercetools/README.md index 6fbe107fd1d8a..66183957c7273 100644 --- a/components/commercetools/README.md +++ b/components/commercetools/README.md @@ -1,15 +1,11 @@ # Overview -You can use the commercetools API to build a variety of applications, -including: +Commercetools is a flexible, cloud-based e-commerce platform offering APIs that allow you to build a wide array of custom shopping experiences. Using Pipedream, you can harness these APIs to automate various e-commerce operations, such as inventory management, order processing, and customer engagement. Its headless architecture means that you can decouple the backend from the frontend, giving you the freedom to create unique user interfaces while Pipedream manages the heavy lifting on the backend. -- ECommerce applications -- Mobile applications -- Web applications +# Example Use Cases -Some examples of what you can build using the commercetools API include: +- **Automate Order Fulfillment**: When a new order is placed in commercetools, trigger a Pipedream workflow that automatically sends the order details to a fulfillment service like ShipStation. Once the order is shipped, update the order status in commercetools and notify the customer via email or SMS. -- A mobile app that allows users to browse and purchase products from a store -- A web app that allows customers to view their order history and status -- An eCommerce platform that allows merchants to manage their products and - orders +- **Sync Inventory Across Platforms**: Maintain a consistent inventory count by triggering a workflow on Pipedream whenever a product's stock level changes in commercetools. The workflow could then update inventory counts on other platforms like Shopify or Amazon, ensuring that your listings are always up to date and preventing overselling. + +- **Personalized Customer Outreach**: Leverage customer data in commercetools to create targeted marketing campaigns. Use Pipedream to analyze purchase history and preferences, then trigger personalized emails or ads through platforms like Mailchimp or Google Ads when new products are available that match their interests. diff --git a/components/commercetools/package.json b/components/commercetools/package.json new file mode 100644 index 0000000000000..7c4051ce772d1 --- /dev/null +++ b/components/commercetools/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/commercetools", + "version": "0.6.0", + "description": "Pipedream commercetools Components", + "main": "commercetools.app.mjs", + "keywords": [ + "pipedream", + "commercetools" + ], + "homepage": "https://pipedream.com/apps/commercetools", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/common_paper/README.md b/components/common_paper/README.md new file mode 100644 index 0000000000000..e747a8cc04181 --- /dev/null +++ b/components/common_paper/README.md @@ -0,0 +1,11 @@ +# Overview + +The Common Paper API lets you automate contract management functions, such as creating, sending, and managing contracts programmatically. Integrating Common Paper API with Pipedream allows you to trigger workflows from contract events, sync contract data with other platforms, or dynamically generate contracts based on actions in other apps. + +# Example Use Cases + +- **Auto-Generate Contracts from CRM Deals**: When a deal reaches a certain stage in a CRM like Salesforce, trigger a Pipedream workflow to create a contract in Common Paper with the deal details. Then, send the contract to the customer for signature. + +- **Sync Contract Status with Project Management Tools**: Keep your project management tool, like Asana, up-to-date by using Pipedream to monitor contract status changes in Common Paper. When a contract is signed or updated, automatically update the corresponding project or task in Asana. + +- **Automate Contract Approvals via Slack**: Streamline the approval process by sending contract drafts from Common Paper to a designated Slack channel for review. Pipedream can listen for approvals directly in Slack, then update the contract status or push it to the next stage in the workflow. diff --git a/components/common_paper/common_paper.app.mjs b/components/common_paper/common_paper.app.mjs new file mode 100644 index 0000000000000..76aa67ea2709e --- /dev/null +++ b/components/common_paper/common_paper.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "common_paper", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/common_paper/package.json b/components/common_paper/package.json new file mode 100644 index 0000000000000..56e2d6c01729d --- /dev/null +++ b/components/common_paper/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/common_paper", + "version": "0.0.1", + "description": "Pipedream Common Paper Components", + "main": "common_paper.app.mjs", + "keywords": [ + "pipedream", + "common_paper" + ], + "homepage": "https://pipedream.com/apps/common_paper", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/commpeak/README.md b/components/commpeak/README.md new file mode 100644 index 0000000000000..ef69682328807 --- /dev/null +++ b/components/commpeak/README.md @@ -0,0 +1,11 @@ +# Overview + +The CommPeak API offers a suite of telecommunication services, such as sending SMS messages, making voice calls, and managing phone numbers. In Pipedream, harnessing this API lets you automate communication processes, integrate telephony features into your workflows, and connect them with other apps for enhanced functionality. Think of orchestrating SMS notifications based on certain triggers or automating call logs syncing to a database. CommPeak's capabilities, when combined with Pipedream's serverless platform, can streamline complex communication tasks and create powerful, event-driven automations. + +# Example Use Cases + +- **Automated Customer Support Notifications**: Trigger an SMS or voice message via CommPeak whenever a new support ticket is created in your helpdesk software. This could ensure immediate engagement with customers, showing them their concerns are being addressed promptly. + +- **Lead Qualification and Outreach**: When a new lead is added to your CRM, use CommPeak within Pipedream to place a call or send an SMS. This can help in quickly touching base with potential customers, thus improving the chances of conversion. + +- **Event-Driven Alerts for Systems Monitoring**: Set up a workflow that monitors your system's health (using an app like Datadog). If an issue is detected, automatically trigger a CommPeak voice call to the on-call engineer to ensure rapid response to critical incidents. diff --git a/components/companycam/README.md b/components/companycam/README.md index 05b55041a3a1c..499d6568ce559 100644 --- a/components/companycam/README.md +++ b/components/companycam/README.md @@ -1,10 +1,11 @@ # Overview -CompanyCam provides a REST API that developers can use to interact with -CompanyCam data and images. The CompanyCam API allows developers to create apps -that can: - -- Upload images to CompanyCam -- Get images from CompanyCam -- Edit images in CompanyCam -- Delete images from CompanyCam +The CompanyCam API provides programmatic access to CompanyCam’s project-based photo solution, allowing for the management of photos, users, and projects within their ecosystem. With Pipedream, you can harness this API to create powerful automations and integrations that streamline photo management workflows, sync project details, and enhance reporting capabilities for teams in construction, roofing, and similar industries. + +# Example Use Cases + +- **Photo Backup to Cloud Storage**: Automatically upload new photos from CompanyCam projects to cloud storage platforms like Google Drive or Dropbox. This not only creates a backup for security but also enables easy sharing and access across devices. + +- **Project Updates via Messaging Apps**: Trigger notifications on messaging platforms like Slack or Microsoft Teams when new photos are added to a project. This keeps the team instantly informed about the latest visual updates and progress without manual intervention. + +- **Integrate Photo Data with CRMs**: Sync photo details and associated project information from CompanyCam to Customer Relationship Management (CRM) systems like Salesforce or HubSpot. This can help in tracking customer interactions and project stages, providing a visual timeline of work completed. diff --git a/components/companyhub/README.md b/components/companyhub/README.md index aea1207afec1c..af772830cb620 100644 --- a/components/companyhub/README.md +++ b/components/companyhub/README.md @@ -1,12 +1,11 @@ # Overview -CompanyHub provides an API that lets you access your account data and build -custom applications on top of it. This API documentation will show you how to -get started. +The CompanyHub API offers a platform for seamless CRM automation, enabling you to streamline sales processes, manage customer interactions, and analyze data efficiently. With it, you can automate tasks like data entry, lead tracking, and follow-ups, saving time and eliminating manual errors. Pipedream's integration capabilities allow you to connect CompanyHub with other apps to create custom, automated workflows that trigger actions based on events, sync data across platforms, or generate real-time notifications. -Some examples of what you can build include: +# Example Use Cases -- A custom CRM application -- A sales management tool -- An invoicing system -- A project management tool +- **Lead Scoring and Prioritization**: Automatically score leads based on interactions and engagement captured in CompanyHub. Use Pipedream to integrate with an external scoring service or algorithm, update the lead scores in real-time, and push priority leads to the top of your sales team's list in CompanyHub. + +- **Email Campaign Trigger Based on CRM Updates**: Set up a workflow where changes in CompanyHub, such as a lead status update, automatically trigger a personalized email campaign in a marketing platform like Mailchimp. This ensures timely follow-up emails that are tailored to the lead's journey stage. + +- **Customer Support Ticket Creation**: Create a support ticket in a helpdesk tool like Zendesk whenever a specific event, such as a deal closing or a customer feedback submission, occurs in CompanyHub. This ensures an immediate support response, contributing to a better customer experience. diff --git a/components/companyhub/package.json b/components/companyhub/package.json new file mode 100644 index 0000000000000..0e86fc5c7b619 --- /dev/null +++ b/components/companyhub/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/companyhub", + "version": "0.6.0", + "description": "Pipedream companyhub Components", + "main": "companyhub.app.mjs", + "keywords": [ + "pipedream", + "companyhub" + ], + "homepage": "https://pipedream.com/apps/companyhub", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/concord/README.md b/components/concord/README.md new file mode 100644 index 0000000000000..89cb46aa36b8a --- /dev/null +++ b/components/concord/README.md @@ -0,0 +1,11 @@ +# Overview + +The Concord API provides access to a contract management platform that lets users automate the creation, negotiation, signing, and management of contracts. With the API, you can leverage Concord's features in a programmatic way, allowing for integration with external systems and automation of workflows. Using Pipedream, you can connect Concord's capabilities to a multitude of other services, trigger workflows based on events within Concord, or perform operations in Concord in response to triggers from other apps. + +# Example Use Cases + +- **Automate Contract Creation**: Create a workflow on Pipedream that listens for new deal records in a CRM like Salesforce. When a new deal is confirmed, use the Concord API to automatically generate a contract based on pre-defined templates and send it to the relevant parties for signing. + +- **Contract Approval Workflow**: Set up a Pipedream workflow that triggers whenever a contract is updated in Concord. The workflow will check if a contract has reached a certain stage, then notify a Slack channel or a specific approver via email, requesting their review or approval before the next stage. + +- **Sync Contracts with Cloud Storage**: Develop a Pipedream workflow that triggers on the completion of a contract's signing process in Concord. The signed contract can be automatically uploaded to a cloud storage service like Google Drive or Dropbox, ensuring that all contracts are securely stored and easily accessible. diff --git a/components/confection/README.md b/components/confection/README.md index 746174daaf635..844d5cb1d239f 100644 --- a/components/confection/README.md +++ b/components/confection/README.md @@ -1,7 +1,11 @@ # Overview -Confection is an API that enables you to easily create and manage serverless -functions. Using Confection, you can deploy your functions to any number of -providers, including AWS Lambda, Google Cloud Functions, and Azure Functions. -Confection also provides a convenient command-line interface, making it easy to -manage your functions from your terminal. +Confection API provides a robust solution for collecting, managing, and utilizing user data in compliance with privacy regulations. It helps businesses capture data lost due to ad blockers and privacy tech, ensuring you don't miss out on valuable insights. With Pipedream, you can harness this data in real-time, triggering actions, analyzing trends, or integrating with other services for a comprehensive data strategy. + +# Example Use Cases + +- **Sales Lead Enrichment Workflow**: Trigger a workflow whenever new data is captured by Confection. Use the Confection API to enrich this data and then send it to a CRM like Salesforce or HubSpot. This ensures your sales team always has the latest, most comprehensive view of potential leads. + +- **Marketing Attribution Reporting**: Combine Confection data with marketing platforms such as Google Ads or Facebook Ads via Pipedream. By doing this, you can attribute conversions accurately and optimize ad spend based on the enriched data from Confection, improving ROI on your marketing campaigns. + +- **Privacy Compliance Automation**: Use Confection API to audit data collection practices and trigger workflows in Pipedream to alert your compliance team or automatically rectify data handling to ensure alignment with GDPR, CCPA, or other privacy regulations. This proactive approach to compliance can save your business from hefty fines and maintain customer trust. diff --git a/components/confluence/README.md b/components/confluence/README.md index 3e2cdb5a59353..2ccf2fdd6e5b1 100644 --- a/components/confluence/README.md +++ b/components/confluence/README.md @@ -1,16 +1,11 @@ # Overview -The Confluence API allows developers to interact with Confluence remotely, from -within another application. This means that you can create, update, delete, and -retrieve information from Confluence, without needing to use the Confluence -user interface. +The Confluence API allows you to harness the collaborative power of Confluence programmatically. With it, you can automate content creation, update pages, manage spaces, and extract data for reporting or integration with other tools. Leveraging this API within Pipedream enables streamlined workflows that can react to events or schedule tasks, interacting with Confluence data and connecting to an array of other apps and services. -Some examples of what you can build using the Confluence API include: +# Example Use Cases -- A process manager that updates Confluence pages as processes are completed -- A tool that automatically generates meeting minutes based on information - stored in Confluence -- A customer support system that creates and updates Confluence pages based on - customer queries -- A project management tool that updates Confluence pages as project milestones - are reached +- **Automated Content Management**: Trigger a workflow when a new Jira ticket is created to generate a Confluence page pre-populated with the ticket details. This page can then be linked back to the Jira ticket, ensuring that documentation is initiated from the outset of a project. + +- **Team Onboarding**: Onboard new team members by creating a personalized Confluence space with essential reading and tasks when a new user is added to your HR system, like BambooHR. You could also set up reminders to check in on their progress by sending follow-up emails after a set period. + +- **Meeting Minute Distribution**: After a meeting is concluded in a tool like Zoom, automatically create a Confluence page using the meeting notes and distribute it to attendees via Slack or email, ensuring everyone has access to the discussed points and action items. diff --git a/components/confluence/actions/create-post/create-post.mjs b/components/confluence/actions/create-post/create-post.mjs new file mode 100644 index 0000000000000..6491a01a3a43e --- /dev/null +++ b/components/confluence/actions/create-post/create-post.mjs @@ -0,0 +1,55 @@ +import confluence from "../../confluence.app.mjs"; + +export default { + key: "confluence-create-post", + name: "Create Post", + description: "Creates a new page or blog post on Confluence. [See the documentation](https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-blog-post/#api-blogposts-post)", + version: "0.0.1", + type: "action", + props: { + confluence, + spaceId: { + propDefinition: [ + confluence, + "spaceId", + ], + }, + title: { + propDefinition: [ + confluence, + "title", + ], + }, + body: { + propDefinition: [ + confluence, + "body", + ], + }, + status: { + propDefinition: [ + confluence, + "status", + ], + }, + }, + async run({ $ }) { + const response = await this.confluence.createPost({ + $, + cloudId: await this.confluence.getCloudId({ + $, + }), + data: { + spaceId: this.spaceId, + status: this.status, + title: this.title, + body: { + representation: "storage", + value: this.body, + }, + }, + }); + $.export("$summary", `Successfully created post with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/confluence/actions/delete-post/delete-post.mjs b/components/confluence/actions/delete-post/delete-post.mjs new file mode 100644 index 0000000000000..7457a32c7a78a --- /dev/null +++ b/components/confluence/actions/delete-post/delete-post.mjs @@ -0,0 +1,29 @@ +import confluence from "../../confluence.app.mjs"; + +export default { + key: "confluence-delete-post", + name: "Delete Post", + description: "Removes a blog post from Confluence by its ID. Use with caution, the action is irreversible. [See the documentation](https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-blog-post/#api-blogposts-id-delete)", + version: "0.0.1", + type: "action", + props: { + confluence, + postId: { + propDefinition: [ + confluence, + "postId", + ], + }, + }, + async run({ $ }) { + const response = await this.confluence.deletePost({ + $, + cloudId: await this.confluence.getCloudId({ + $, + }), + postId: this.postId, + }); + $.export("$summary", `Post with ID ${this.postId} was successfully deleted.`); + return response; + }, +}; diff --git a/components/confluence/actions/update-post/update-post.mjs b/components/confluence/actions/update-post/update-post.mjs new file mode 100644 index 0000000000000..c5755b252a331 --- /dev/null +++ b/components/confluence/actions/update-post/update-post.mjs @@ -0,0 +1,86 @@ +import confluence from "../../confluence.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "confluence-update-post", + name: "Update a Post", + description: "Updates a page or blog post on Confluence by its ID. [See the documentation](https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-blog-post/#api-blogposts-id-put)", + version: "0.0.1", + type: "action", + props: { + confluence, + postId: { + propDefinition: [ + confluence, + "postId", + ], + }, + spaceId: { + propDefinition: [ + confluence, + "spaceId", + ], + optional: true, + }, + status: { + propDefinition: [ + confluence, + "status", + ], + }, + title: { + propDefinition: [ + confluence, + "title", + ], + optional: true, + }, + body: { + propDefinition: [ + confluence, + "body", + ], + optional: true, + }, + }, + async run({ $ }) { + const cloudId = await this.confluence.getCloudId({ + $, + }); + const post = await this.confluence.getPost({ + $, + cloudId, + postId: this.postId, + params: { + "body-format": "storage", + }, + }); + if (!Object.keys(post.body).length && !this.body) { + throw new ConfigurationError("Must contain Body"); + } + const representation = Object.keys(post.body)[0]; + const value = post.body[representation].value; + const version = post.version.number; + const response = await this.confluence.updatePost({ + $, + cloudId, + postId: this.postId, + data: { + id: this.postId, + status: this.status || post.status, + title: this.title || post.title, + body: { + representation: this.body + ? "storage" + : representation, + value: this.body || value, + }, + version: { + number: version + 1, + }, + }, + }); + $.export("$summary", `Successfully updated post with ID: ${this.postId}`); + return response; + }, +}; diff --git a/components/confluence/confluence.app.mjs b/components/confluence/confluence.app.mjs index 88410719fcd29..1078c411a700a 100644 --- a/components/confluence/confluence.app.mjs +++ b/components/confluence/confluence.app.mjs @@ -1,11 +1,211 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "confluence", - propDefinitions: {}, + propDefinitions: { + postId: { + type: "string", + label: "Post ID", + description: "The ID of the post", + async options({ prevContext }) { + const params = prevContext?.cursor + ? { + cursor: prevContext.cursor, + } + : {}; + const cloudId = await this.getCloudId(); + const { + results, _links: links, + } = await this.listPosts({ + cloudId, + params, + }); + const options = results?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })) || []; + return { + options, + context: { + cursor: links?.next, + }, + }; + }, + }, + spaceId: { + type: "string", + label: "Space Id", + description: "The Id of the space", + async options({ prevContext }) { + const params = prevContext?.cursor + ? { + cursor: prevContext.cursor, + } + : {}; + const cloudId = await this.getCloudId(); + const { + results, _links: links, + } = await this.listSpaces({ + cloudId, + params, + }); + const options = results?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + return { + options, + context: { + cursor: links?.next, + }, + }; + }, + }, + status: { + type: "string", + label: "Status", + description: "The status of the blog post, specifies if the blog post will be created as a new blog post or a draft.", + optional: true, + options: [ + "current", + "draft", + ], + }, + title: { + type: "string", + label: "Title", + description: "Title of the blog post, required if creating non-draft.", + }, + body: { + type: "string", + label: "Body", + description: "Body of the blog post", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl(cloudId) { + return `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/`; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + cloudId, + ...otherOpts + } = opts; + return axios($, { + url: `${this._baseUrl(cloudId)}${path}`, + headers: { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }, + ...otherOpts, + }); + }, + async getCloudId(opts = {}) { + const response = await this._makeRequest({ + url: "https://api.atlassian.com/oauth/token/accessible-resources", + ...opts, + }); + if (!response?.length) { + throw new Error("Unable to locate Confluence project site. Please verify that your site is setup correctly. https://www.atlassian.com/software/confluence/resources/guides/get-started/set-up"); + } + return response[0].id; + }, + getPost({ + postId, ...opts + }) { + return this._makeRequest({ + path: `/blogposts/${postId}`, + ...opts, + }); + }, + listSpaces(opts = {}) { + return this._makeRequest({ + path: "/spaces", + ...opts, + }); + }, + listPosts(opts = {}) { + return this._makeRequest({ + path: "/blogposts", + ...opts, + }); + }, + listPostsInSpace({ + spaceId, ...opts + }) { + return this._makeRequest({ + path: `/spaces/${spaceId}/blogposts`, + ...opts, + }); + }, + listPages(opts = {}) { + return this._makeRequest({ + path: "/pages", + ...opts, + }); + }, + listPagesInSpace({ + spaceId, ...opts + }) { + return this._makeRequest({ + path: `/spaces/${spaceId}/pages`, + ...opts, + }); + }, + createPost(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/blogposts", + ...opts, + }); + }, + deletePost({ + postId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/blogposts/${postId}`, + ...opts, + }); + }, + updatePost({ + postId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/blogposts/${postId}`, + ...opts, + }); + }, + async *paginate({ + resourceFn, + args, + max, + }) { + args.params = { + ...args.params, + }; + let hasMore, count = 0; + do { + const { + results, _links: links, + } = await resourceFn(args); + for (const item of results) { + yield item; + count++; + if (max && count >= max) { + return; + } + } + hasMore = links?.next; + args.params.cursor = hasMore; + } while (hasMore); }, }, }; diff --git a/components/confluence/package.json b/components/confluence/package.json new file mode 100644 index 0000000000000..e9f023fb9c80b --- /dev/null +++ b/components/confluence/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/confluence", + "version": "0.0.1", + "description": "Pipedream Confluence Components", + "main": "confluence.app.mjs", + "keywords": [ + "pipedream", + "confluence" + ], + "homepage": "https://pipedream.com/apps/confluence", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/confluence/sources/common/base.mjs b/components/confluence/sources/common/base.mjs new file mode 100644 index 0000000000000..4b59b318e7fca --- /dev/null +++ b/components/confluence/sources/common/base.mjs @@ -0,0 +1,78 @@ +import confluence from "../../confluence.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + confluence, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + getArgs() { + return {}; + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + getTs() { + throw new Error("getTsField is not implemented"); + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + emitEvent(event) { + const meta = this.generateMeta(event); + this.$emit(event, meta); + }, + generateMeta(post) { + const ts = this.getTs(post); + return { + id: `${post.id}-${ts}`, + summary: this.getSummary(post), + ts, + }; + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + const resourceFn = this.getResourceFn(); + const args = await this.getArgs(); + const items = this.confluence.paginate({ + resourceFn, + args, + max, + }); + const results = []; + for await (const item of items) { + if (this.getTs(item) >= lastTs) { + results.push(item); + } else { + break; + } + } + if (!results.length) { + return; + } + this._setLastTs(this.getTs(results[0])); + results.forEach((result) => this.emitEvent(result)); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/confluence/sources/new-page-or-blog-post/new-page-or-blog-post.mjs b/components/confluence/sources/new-page-or-blog-post/new-page-or-blog-post.mjs new file mode 100644 index 0000000000000..19e56518235ce --- /dev/null +++ b/components/confluence/sources/new-page-or-blog-post/new-page-or-blog-post.mjs @@ -0,0 +1,57 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "confluence-new-page-or-blog-post", + name: "New Page or Blog Post", + description: "Emit new event whenever a new page or blog post is created in a specified space", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + spaceId: { + propDefinition: [ + common.props.confluence, + "spaceId", + ], + }, + type: { + type: "string", + label: "Type", + description: "Whether to watch for new pages or blog posts", + options: [ + "pages", + "blogposts", + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + if (this.type === "pages") { + return this.confluence.listPagesInSpace; + } + if (this.type === "blogposts") { + return this.confluence.listPostsInSpace; + } + }, + async getArgs() { + return { + cloudId: await this.confluence.getCloudId(), + spaceId: this.spaceId, + params: { + sort: "-created-date", + }, + }; + }, + getTs(post) { + return Date.parse(post.createdAt); + }, + getSummary(post) { + return `New ${this.type} with ID ${post.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/confluence/sources/new-page-or-blog-post/test-event.mjs b/components/confluence/sources/new-page-or-blog-post/test-event.mjs new file mode 100644 index 0000000000000..562b4c3539d68 --- /dev/null +++ b/components/confluence/sources/new-page-or-blog-post/test-event.mjs @@ -0,0 +1,24 @@ +export default { + "id": "", + "status": "current", + "title": "", + "spaceId": "", + "authorId": "", + "createdAt": "", + "version": { + "createdAt": "", + "message": "", + "number": 19, + "minorEdit": true, + "authorId": "" + }, + "body": { + "storage": {}, + "atlas_doc_format": {} + }, + "_links": { + "webui": "", + "editui": "", + "tinyui": "" + } +} \ No newline at end of file diff --git a/components/confluence/sources/watch-blog-posts/test-event.mjs b/components/confluence/sources/watch-blog-posts/test-event.mjs new file mode 100644 index 0000000000000..562b4c3539d68 --- /dev/null +++ b/components/confluence/sources/watch-blog-posts/test-event.mjs @@ -0,0 +1,24 @@ +export default { + "id": "", + "status": "current", + "title": "", + "spaceId": "", + "authorId": "", + "createdAt": "", + "version": { + "createdAt": "", + "message": "", + "number": 19, + "minorEdit": true, + "authorId": "" + }, + "body": { + "storage": {}, + "atlas_doc_format": {} + }, + "_links": { + "webui": "", + "editui": "", + "tinyui": "" + } +} \ No newline at end of file diff --git a/components/confluence/sources/watch-blog-posts/watch-blog-posts.mjs b/components/confluence/sources/watch-blog-posts/watch-blog-posts.mjs new file mode 100644 index 0000000000000..e8266cd88c688 --- /dev/null +++ b/components/confluence/sources/watch-blog-posts/watch-blog-posts.mjs @@ -0,0 +1,33 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "confluence-watch-blog-posts", + name: "Watch Blog Posts", + description: "Emit new event when a blog post is created or updated", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.confluence.listPosts; + }, + async getArgs() { + return { + cloudId: await this.confluence.getCloudId(), + params: { + sort: "-modified-date", + }, + }; + }, + getTs(post) { + return Date.parse(post.version.createdAt); + }, + getSummary(post) { + return `New or updated blogpost with ID ${post.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/confluence/sources/watch-pages/test-event.mjs b/components/confluence/sources/watch-pages/test-event.mjs new file mode 100644 index 0000000000000..314c5eb0dc060 --- /dev/null +++ b/components/confluence/sources/watch-pages/test-event.mjs @@ -0,0 +1,29 @@ +export default { + "id": "", + "status": "current", + "title": "", + "spaceId": "", + "parentId": "", + "parentType": "page", + "position": 57, + "authorId": "", + "ownerId": "", + "lastOwnerId": "", + "createdAt": "", + "version": { + "createdAt": "", + "message": "", + "number": 19, + "minorEdit": true, + "authorId": "" + }, + "body": { + "storage": {}, + "atlas_doc_format": {} + }, + "_links": { + "webui": "", + "editui": "", + "tinyui": "" + } +} \ No newline at end of file diff --git a/components/confluence/sources/watch-pages/watch-pages.mjs b/components/confluence/sources/watch-pages/watch-pages.mjs new file mode 100644 index 0000000000000..5b60e9d932645 --- /dev/null +++ b/components/confluence/sources/watch-pages/watch-pages.mjs @@ -0,0 +1,33 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "confluence-watch-pages", + name: "Watch Pages", + description: "Emit new event when a page is created or updated in Confluence", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.confluence.listPages; + }, + async getArgs() { + return { + cloudId: await this.confluence.getCloudId(), + params: { + sort: "-modified-date", + }, + }; + }, + getTs(post) { + return Date.parse(post.version.createdAt); + }, + getSummary(post) { + return `New or updated page with ID ${post.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/confluent/README.md b/components/confluent/README.md new file mode 100644 index 0000000000000..c2549362ccae5 --- /dev/null +++ b/components/confluent/README.md @@ -0,0 +1,11 @@ +# Overview + +The Confluent API provides programmatic interaction with Confluent Cloud, a fully managed Kafka service. It lets you manage Kafka clusters, topics, users, and configurations, enabling seamless integration and data streaming capabilities. With Pipedream, you can create workflows that automate interactions with your Kafka infrastructure, such as triggering events on message arrival, managing topics, or integrating Kafka data with other services. + +# Example Use Cases + +- **Automatically Manage Kafka Topics:** Set up a workflow that listens for webhooks from your apps, triggering the creation or deletion of topics in your Confluent Cloud based on specific events or conditions met within your application ecosystem. + +- **Stream Data to Analytics Platforms:** Ingest messages from a Kafka topic and send them to an analytics platform like Google BigQuery. This workflow can transform and batch messages before loading, providing real-time analytics capabilities to your data. + +- **Kafka User Management:** Automate user creation, deletion, and access control list updates in Confluent Cloud in response to changes in your identity provider service, such as Okta or Active Directory, ensuring your Kafka user permissions stay in sync with your organization's user management policies. diff --git a/components/congress_gov/README.md b/components/congress_gov/README.md index 4854df903396d..f248558fe1c72 100644 --- a/components/congress_gov/README.md +++ b/components/congress_gov/README.md @@ -1,10 +1,11 @@ # Overview -The Congress.gov API can be used to build applications that retrieve data about -the U.S. Congress, including: - -- Legislation -- Members of Congress -- Congressional votes -- Hearings -- And more! +The Congress.gov API opens the door to a repository of legislative information from the United States Congress. With this API, you can access details on bills, amendments, congressional records, and members' profiles. Integrating this API with Pipedream allows you to automatically track legislative developments, analyze policymaking trends, and stay informed on the activities of specific members of Congress. By creating workflows on Pipedream, you can connect Congress.gov data with numerous apps to streamline political data analysis, advocacy efforts, or journalistic research. + +# Example Use Cases + +- **Track Specific Legislation**: Set up a workflow that monitors Congress.gov for updates on specific bills. Once a new action is detected, the workflow can automatically send notifications via email, Slack, or other communication platforms to keep stakeholders informed in real time. + +- **Legislative Trend Analysis**: Create a workflow that aggregates legislative data over time to identify trends. Connect Congress.gov with a data visualization tool like Google Sheets or Tableau, automatically updating dashboards with new data as it becomes available, providing insights into legislative patterns. + +- **Congressional Voting Record Notifications**: Develop a workflow that watches for new voting records of particular members of Congress. Whenever a vote is cast, the workflow can post the update to a Twitter account or a news digest platform, offering immediate public insight into congressional decision-making. diff --git a/components/connecteam/actions/create-shift/create-shift.mjs b/components/connecteam/actions/create-shift/create-shift.mjs new file mode 100644 index 0000000000000..3cbffcbc73425 --- /dev/null +++ b/components/connecteam/actions/create-shift/create-shift.mjs @@ -0,0 +1,154 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import connecteam from "../../connecteam.app.mjs"; + +export default { + key: "connecteam-create-shift", + name: "Create Shift", + description: "Creates a new shift in the scheduler. [See the documentation](https://developer.connecteam.com/reference/create_shifts_scheduler_v1_schedulers__schedulerid__shifts_post)", + version: "0.0.1", + type: "action", + props: { + connecteam, + schedulerId: { + propDefinition: [ + connecteam, + "schedulerId", + ], + }, + startTime: { + type: "string", + label: "Start Time", + description: "The start time of the shift in ISO8601 format (YYYY-MM-DDTHH:MM:SS.SSSZ).", + }, + endTime: { + type: "string", + label: "End Time", + description: "The end time of the shift in ISO8601 format (YYYY-MM-DDTHH:MM:SS.SSSZ).", + }, + title: { + type: "string", + label: "Title", + description: "The title of the shift.", + }, + timezone: { + type: "string", + label: "Timezone", + description: "The timezone of the shift in Tz format (e.g. America/New_York). If not specified, it uses the timezone configured in the app settings.", + optional: true, + }, + isPublished: { + type: "boolean", + label: "Is Published", + description: "Whether the shift is published.", + optional: true, + }, + jobId: { + propDefinition: [ + connecteam, + "jobId", + ], + optional: true, + }, + locationData: { + type: "object", + label: "Location Data", + description: "The location data of the shift. Example `{\"gps\":{\"address\": \"Address Example 123\",\"longitude\":\"-12.345678\",\"latitude\":\"-12.345678\"},\"isReferencedToJob\":false}`. [See the documentation](https://developer.connecteam.com/reference/create_shifts_scheduler_v1_schedulers__schedulerid__shifts_post).", + optional: true, + }, + assignedUserId: { + propDefinition: [ + connecteam, + "assignedUserId", + ], + optional: true, + }, + notes: { + type: "string[]", + label: "Notes", + description: "Additional notes for the shift. HTML is supported. Example `

Note example

` [See the documentation](https://developer.connecteam.com/reference/create_shifts_scheduler_v1_schedulers__schedulerid__shifts_post).", + optional: true, + }, + breaks: { + type: "string[]", + label: "Breaks", + description: "A list of stringified objects of breaks to create for the shift. Example `{\"name\":\"Break name example\",\"type\":\"paid\",\"startTime\":123456789,\"duration\":123}` [See the documentation](https://developer.connecteam.com/reference/create_shifts_scheduler_v1_schedulers__schedulerid__shifts_post).", + optional: true, + }, + isOpenShift: { + type: "boolean", + label: "Is Open Shift", + description: "Whether the shift is an open shift.", + optional: true, + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.isOpenShift) { + props.isRequireAdminApproval = { + type: "boolean", + label: "Requires Admin Approval", + description: "Whether the shift requires admin approval.", + optional: true, + }; + } + return props; + }, + methods: { + checkDatetime(dateTime) { + try { + new Date(dateTime).toISOString(); + return Date.parse(new Date(dateTime)) / 1000; + } catch (e) { + throw new Error(JSON.stringify({ + "error": { + "errorDate": { + message: "Invalid datetime format.", + }, + }, + })); + } + }, + }, + async run({ $ }) { + try { + const { + connecteam, + schedulerId, + startTime, + endTime, + ...data + } = this; + + const response = await connecteam.createShift({ + $, + schedulerId, + data: [ + { + ...data, + locationData: parseObject(data.locationData), + assignedUserIds: data.assignedUserId + ? [ + data.assignedUserId, + ] + : undefined, + startTime: this.checkDatetime(startTime), + endTime: this.checkDatetime(endTime), + notes: parseObject(data.notes)?.map((note) => ({ + html: note, + })), + breaks: parseObject(data.breaks), + }, + ], + }); + + $.export("$summary", `Successfully created shift with ID ${response.data.shifts[0].id}`); + return response; + } catch ({ message }) { + const errors = JSON.parse(message); + const keys = Object.keys(errors.error); + throw new ConfigurationError(errors.error[keys[0]].message); + } + }, +}; diff --git a/components/connecteam/actions/create-user/create-user.mjs b/components/connecteam/actions/create-user/create-user.mjs new file mode 100644 index 0000000000000..7b8c8617e3c4f --- /dev/null +++ b/components/connecteam/actions/create-user/create-user.mjs @@ -0,0 +1,74 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import connecteam from "../../connecteam.app.mjs"; + +export default { + key: "connecteam-create-user", + name: "Create User", + description: "Creates a new user profile in Connecteam. [See the documentation](https://developer.connecteam.com/reference/create_users_users_v1_users_post)", + version: "0.0.1", + type: "action", + props: { + connecteam, + firstName: { + type: "string", + label: "First Name", + description: "The user's first name.", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The user's last name.", + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The user's phone number. **Format : +(countrycode)(phone number)**.", + }, + userType: { + propDefinition: [ + connecteam, + "userType", + ], + }, + email: { + type: "string", + label: "Email", + description: "The user's email (mandatory for managers and owners).", + optional: true, + }, + customFields: { + type: "string[]", + label: "Custom Fields", + description: "An array of objects representing the user's custom fields. [See the documentation](https://developer.connecteam.com/reference/create_users_users_v1_users_post)", + optional: true, + }, + isArchived: { + type: "boolean", + label: "Is Archived", + description: "The user's archived status. Default is **False**.", + optional: true, + }, + }, + async run({ $ }) { + const { + connecteam, + ...data + } = this; + + if (data.customFields) data.customFields = parseObject(data.customFields); + + if ((data.userType === "manager" || data.userType === "owner") && !data.email) { + throw new ConfigurationError("Email is mandatory for managers and owners."); + } + + const response = await connecteam.createUser({ + $, + data: [ + data, + ], + }); + $.export("$summary", `Successfully created user ${data.firstName} ${data.lastName}`); + return response; + }, +}; diff --git a/components/connecteam/actions/delete-shift/delete-shift.mjs b/components/connecteam/actions/delete-shift/delete-shift.mjs new file mode 100644 index 0000000000000..94235dd7ff210 --- /dev/null +++ b/components/connecteam/actions/delete-shift/delete-shift.mjs @@ -0,0 +1,37 @@ +import connecteam from "../../connecteam.app.mjs"; + +export default { + key: "connecteam-delete-shift", + name: "Delete Shift", + description: "Removes a specific shift based on shift ID. [See the documentation](https://developer.connecteam.com/reference/delete_shift_by_id_scheduler_v1_schedulers__schedulerid__shifts__shiftid__delete)", + version: "0.0.1", + type: "action", + props: { + connecteam, + schedulerId: { + propDefinition: [ + connecteam, + "schedulerId", + ], + }, + shiftId: { + propDefinition: [ + connecteam, + "shiftId", + ({ schedulerId }) => ({ + schedulerId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.connecteam.deleteShift({ + $, + schedulerId: this.schedulerId, + shiftId: this.shiftId, + }); + + $.export("$summary", `Successfully deleted shift with ID ${this.shiftId}`); + return response; + }, +}; diff --git a/components/connecteam/common/constants.mjs b/components/connecteam/common/constants.mjs new file mode 100644 index 0000000000000..ea830c15a04cb --- /dev/null +++ b/components/connecteam/common/constants.mjs @@ -0,0 +1 @@ +export const LIMIT = 100; diff --git a/components/connecteam/common/utils.mjs b/components/connecteam/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/connecteam/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/connecteam/connecteam.app.mjs b/components/connecteam/connecteam.app.mjs new file mode 100644 index 0000000000000..821cfb6fdffad --- /dev/null +++ b/components/connecteam/connecteam.app.mjs @@ -0,0 +1,226 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "connecteam", + propDefinitions: { + assignedUserId: { + type: "string", + label: "Assigned User ID", + description: "The ID of the assigned user.", + async options({ page }) { + const { data: { users } } = await this.listUsers({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + return users.map(({ + email: label, userId: value, + }) => ({ + label, + value, + })); + }, + }, + formId: { + type: "string", + label: "Form ID", + description: "The ID of the form.", + async options({ page }) { + const { data: { forms } } = await this.listForms({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + return forms.map(({ + formId: value, formName: label, + }) => ({ + label, + value, + })); + }, + }, + jobId: { + type: "string", + label: "Job ID", + description: "The ID of the job.", + async options({ page }) { + const { data: { jobs } } = await this.listJobs({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + return jobs.map(({ + jobId: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + schedulerId: { + type: "string", + label: "Scheduler ID", + description: "The ID of the scheduler.", + async options() { + const { data: { schedulers } } = await this.listSchedulers(); + return schedulers.map(({ + schedulerId: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + shiftId: { + type: "string", + label: "Shift ID", + description: "The ID of the shift.", + async options({ + schedulerId, page, + }) { + const date = new Date(); + const { data: { shifts } } = await this.listShifts({ + schedulerId, + params: { + limit: LIMIT, + offset: LIMIT * page, + startTime: 1, + endTime: date.setFullYear(date.getFullYear() + 1000), + }, + }); + return shifts.map(({ + id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + userType: { + type: "string", + label: "User Type", + description: "The type of the user.", + options: [ + "user", + "manager", + "owner", + ], + }, + }, + methods: { + _baseUrl() { + return "https://api.connecteam.com"; + }, + _headers() { + return { + "X-API-KEY": `${this.$auth.api_key}`, + "accept": "application/json", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listForms(opts = {}) { + return this._makeRequest({ + path: "/forms/v1/forms", + ...opts, + }); + }, + listFormSubmissions({ + formId, ...opts + }) { + return this._makeRequest({ + path: `/forms/v1/forms/${formId}/form-submissions`, + ...opts, + }); + }, + listJobs(opts = {}) { + return this._makeRequest({ + path: "/jobs/v1/jobs", + ...opts, + }); + }, + listSchedulers() { + return this._makeRequest({ + path: "/scheduler/v1/schedulers", + }); + }, + listShifts({ + schedulerId, ...opts + }) { + return this._makeRequest({ + path: `/scheduler/v1/schedulers/${schedulerId}/shifts`, + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users/v1/users", + ...opts, + }); + }, + createShift({ + schedulerId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/scheduler/v1/schedulers/${schedulerId}/shifts`, + ...opts, + }); + }, + deleteShift({ + schedulerId, shiftId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/scheduler/v1/schedulers/${schedulerId}/shifts/${shiftId}`, + ...opts, + }); + }, + createUser(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/users/v1/users", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, modelField, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.limit = LIMIT; + params.offset = LIMIT * page; + page++; + const { data } = await fn({ + params, + ...opts, + }); + for (const d of data[modelField]) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = data[modelField].length; + + } while (hasMore); + }, + }, +}; diff --git a/components/connecteam/package.json b/components/connecteam/package.json new file mode 100644 index 0000000000000..48aff919bd3f4 --- /dev/null +++ b/components/connecteam/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/connecteam", + "version": "0.1.0", + "description": "Pipedream Connecteam Components", + "main": "connecteam.app.mjs", + "keywords": [ + "pipedream", + "connecteam" + ], + "homepage": "https://pipedream.com/apps/connecteam", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/connecteam/sources/common/base.mjs b/components/connecteam/sources/common/base.mjs new file mode 100644 index 0000000000000..f8bbaac2f4b81 --- /dev/null +++ b/components/connecteam/sources/common/base.mjs @@ -0,0 +1,69 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import connecteam from "../../connecteam.app.mjs"; + +export default { + props: { + connecteam, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 1; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + getProps() { + return {}; + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const modelField = this.getModelField(); + const modelFieldId = this.getModelFieldId(); + const modelDateField = this.getModelDateField(); + + const response = this.connecteam.paginate({ + fn: this.getFunction(), + maxResults, + modelField, + ...this.getProps(), + params: { + sort: "created_at", + order: "desc", + ...this.getParams(lastDate), + }, + }); + + let responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + if (responseArray.length) { + this._setLastDate(responseArray[0][modelDateField]); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item[modelFieldId], + summary: this.getSummary(item), + ts: modelDateField, + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/connecteam/sources/new-form-submission/new-form-submission.mjs b/components/connecteam/sources/new-form-submission/new-form-submission.mjs new file mode 100644 index 0000000000000..c542bac88407a --- /dev/null +++ b/components/connecteam/sources/new-form-submission/new-form-submission.mjs @@ -0,0 +1,50 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "connecteam-new-form-submission", + name: "New Form Submission", + description: "Emit new event when a new form submission is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + formId: { + propDefinition: [ + common.props.connecteam, + "formId", + ], + }, + }, + methods: { + ...common.methods, + getModelField() { + return "formSubmissions"; + }, + getModelFieldId() { + return "formSubmissionId"; + }, + getModelDateField() { + return "submissionTimestamp"; + }, + getParams(lastDate) { + return { + submittingStartTimestamp: lastDate, + }; + }, + getProps() { + return { + formId: this.formId, + }; + }, + getFunction() { + return this.connecteam.listFormSubmissions; + }, + getSummary(item) { + return `New form submission ${item.formSubmissionId}`; + }, + }, + sampleEmit, +}; diff --git a/components/connecteam/sources/new-form-submission/test-event.mjs b/components/connecteam/sources/new-form-submission/test-event.mjs new file mode 100644 index 0000000000000..33783dae55a57 --- /dev/null +++ b/components/connecteam/sources/new-form-submission/test-event.mjs @@ -0,0 +1,330 @@ +export default { + "formSubmissionId": "string", + "formId": 0, + "submissionTimestamp": 0, + "submissionTimezone": "string", + "entryNum": 0, + "submittingUserId": 0, + "isAnonymous": true, + "managerFields": [ + { + "managerFieldId": "string", + "managerFieldType": "date", + "date": "string" + }, + { + "managerFieldId": "string", + "managerFieldType": "file", + "files": [ + { + "fileUrl": "string", + "filename": "string" + } + ] + }, + { + "managerFieldId": "string", + "managerFieldType": "signature", + "image": "string", + "signingUserId": 0, + "signingTimestamp": 0 + }, + { + "managerFieldId": "string", + "managerFieldType": "owner", + "userId": 0 + }, + { + "managerFieldId": "string", + "managerFieldType": "status", + "status": { + "statusOptionId": "string", + "name": "string", + "color": "string" + } + }, + { + "managerFieldId": "string", + "managerFieldType": "note", + "note": "string" + } + ], + "answers": [ + { + "questionId": "string", + "submissionTimestamp": 0, + "location": { + "address": "string", + "longitude": 0, + "latitude": 0 + }, + "updateTimestamp": 0, + "updateUserId": 0, + "wasSubmittedEmpty": true, + "wasHidden": true, + "questionType": "description" + }, + { + "questionId": "string", + "submissionTimestamp": 0, + "location": { + "address": "string", + "longitude": 0, + "latitude": 0 + }, + "updateTimestamp": 0, + "updateUserId": 0, + "wasSubmittedEmpty": true, + "wasHidden": true, + "questionType": "multipleChoice", + "selectedAnswers": [ + { + "multipleChoiceOptionId": "string", + "text": "string" + } + ] + }, + { + "questionId": "string", + "submissionTimestamp": 0, + "location": { + "address": "string", + "longitude": 0, + "latitude": 0 + }, + "updateTimestamp": 0, + "updateUserId": 0, + "wasSubmittedEmpty": true, + "wasHidden": true, + "questionType": "number", + "inputValue": 0 + }, + { + "questionId": "string", + "submissionTimestamp": 0, + "location": { + "address": "string", + "longitude": 0, + "latitude": 0 + }, + "updateTimestamp": 0, + "updateUserId": 0, + "wasSubmittedEmpty": true, + "wasHidden": true, + "questionType": "openEnded", + "value": "string" + }, + { + "questionId": "string", + "submissionTimestamp": 0, + "location": { + "address": "string", + "longitude": 0, + "latitude": 0 + }, + "updateTimestamp": 0, + "updateUserId": 0, + "wasSubmittedEmpty": true, + "wasHidden": true, + "questionType": "yesNo", + "selectedIndex": 0 + }, + { + "questionId": "string", + "submissionTimestamp": 0, + "location": { + "address": "string", + "longitude": 0, + "latitude": 0 + }, + "updateTimestamp": 0, + "updateUserId": 0, + "wasSubmittedEmpty": true, + "wasHidden": true, + "questionType": "scanDocument", + "images": [ + { + "url": "string" + } + ] + }, + { + "questionId": "string", + "submissionTimestamp": 0, + "location": { + "address": "string", + "longitude": 0, + "latitude": 0 + }, + "updateTimestamp": 0, + "updateUserId": 0, + "wasSubmittedEmpty": true, + "wasHidden": true, + "questionType": "imageSelection", + "selectedAnswers": [ + { + "imageSelectionId": "string", + "text": "string", + "image": "string" + } + ] + }, + { + "questionId": "string", + "submissionTimestamp": 0, + "location": { + "address": "string", + "longitude": 0, + "latitude": 0 + }, + "updateTimestamp": 0, + "updateUserId": 0, + "wasSubmittedEmpty": true, + "wasHidden": true, + "questionType": "location", + "locationInput": { + "address": "string", + "longitude": 0, + "latitude": 0 + } + }, + { + "questionId": "string", + "submissionTimestamp": 0, + "location": { + "address": "string", + "longitude": 0, + "latitude": 0 + }, + "updateTimestamp": 0, + "updateUserId": 0, + "wasSubmittedEmpty": true, + "wasHidden": true, + "questionType": "audioRecording", + "audioUrl": "string", + "audioLength": 0 + }, + { + "questionId": "string", + "submissionTimestamp": 0, + "location": { + "address": "string", + "longitude": 0, + "latitude": 0 + }, + "updateTimestamp": 0, + "updateUserId": 0, + "wasSubmittedEmpty": true, + "wasHidden": true, + "questionType": "task", + "isChecked": true + }, + { + "questionId": "string", + "submissionTimestamp": 0, + "location": { + "address": "string", + "longitude": 0, + "latitude": 0 + }, + "updateTimestamp": 0, + "updateUserId": 0, + "wasSubmittedEmpty": true, + "wasHidden": true, + "questionType": "datetime", + "timestamp": 0, + "timezone": "string", + "isTimeSubmitted": true, + "isDateSubmitted": true + }, + { + "questionId": "string", + "submissionTimestamp": 0, + "location": { + "address": "string", + "longitude": 0, + "latitude": 0 + }, + "updateTimestamp": 0, + "updateUserId": 0, + "wasSubmittedEmpty": true, + "wasHidden": true, + "questionType": "rating", + "ratingValue": 0 + }, + { + "questionId": "string", + "submissionTimestamp": 0, + "location": { + "address": "string", + "longitude": 0, + "latitude": 0 + }, + "updateTimestamp": 0, + "updateUserId": 0, + "wasSubmittedEmpty": true, + "wasHidden": true, + "questionType": "image", + "images": [ + { + "url": "string" + } + ] + }, + { + "questionId": "string", + "submissionTimestamp": 0, + "location": { + "address": "string", + "longitude": 0, + "latitude": 0 + }, + "updateTimestamp": 0, + "updateUserId": 0, + "wasSubmittedEmpty": true, + "wasHidden": true, + "questionType": "signature", + "images": [ + { + "url": "string" + } + ] + }, + { + "questionId": "string", + "submissionTimestamp": 0, + "location": { + "address": "string", + "longitude": 0, + "latitude": 0 + }, + "updateTimestamp": 0, + "updateUserId": 0, + "wasSubmittedEmpty": true, + "wasHidden": true, + "questionType": "files", + "files": [ + { + "url": "string", + "size": 0, + "fileName": "string" + } + ] + }, + { + "questionId": "string", + "submissionTimestamp": 0, + "location": { + "address": "string", + "longitude": 0, + "latitude": 0 + }, + "updateTimestamp": 0, + "updateUserId": 0, + "wasSubmittedEmpty": true, + "wasHidden": true, + "questionType": "slider", + "value": 0 + } + ] +} \ No newline at end of file diff --git a/components/connecteam/sources/new-shift-schedule/new-shift-schedule.mjs b/components/connecteam/sources/new-shift-schedule/new-shift-schedule.mjs new file mode 100644 index 0000000000000..5ff7de24b0825 --- /dev/null +++ b/components/connecteam/sources/new-shift-schedule/new-shift-schedule.mjs @@ -0,0 +1,52 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "connecteam-new-shift-schedule", + name: "New Shift Schedule", + description: "Emit new event when new shifts are created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + schedulerId: { + propDefinition: [ + common.props.connecteam, + "schedulerId", + ], + }, + }, + methods: { + ...common.methods, + getModelField() { + return "shifts"; + }, + getModelFieldId() { + return "id"; + }, + getModelDateField() { + return "creationTime"; + }, + getParams() { + const date = new Date(); + return { + startTime: 1, + endTime: date.setFullYear(date.getFullYear() + 1000), + }; + }, + getProps() { + return { + schedulerId: this.schedulerId, + }; + }, + getFunction() { + return this.connecteam.listShifts; + }, + getSummary(item) { + return `New Shift: ${item.title}`; + }, + }, + sampleEmit, +}; diff --git a/components/connecteam/sources/new-shift-schedule/test-event.mjs b/components/connecteam/sources/new-shift-schedule/test-event.mjs new file mode 100644 index 0000000000000..601964f3ec483 --- /dev/null +++ b/components/connecteam/sources/new-shift-schedule/test-event.mjs @@ -0,0 +1,94 @@ +export default { + "requestId": "string", + "data": { + "shifts": [ + { + "id": "string", + "color": "string", + "assignedUserIds": [ + 0 + ], + "startTime": 0, + "endTime": 0, + "timezone": "string", + "isOpenShift": true, + "title": "string", + "jobId": "string", + "locationData": { + "isReferencedToJob": true, + "gps": { + "address": "string", + "longitude": 0, + "latitude": 0 + } + }, + "isPublished": true, + "isRequireAdminApproval": true, + "updateTime": 0, + "creationTime": 0, + "openSpots": 0, + "notes": [ + { + "type": "html", + "html": "string" + }, + { + "type": "album", + "album": [ + { + "url": "string" + } + ] + }, + { + "url": "string", + "type": "file", + "name": "string" + } + ], + "statuses": [ + { + "statusId": "string", + "note": "string", + "attachments": [ + "string" + ], + "creationTime": 0, + "updateTime": 0, + "creatingUserId": 0, + "modifyingUserId": 0, + "gps": { + "address": "string", + "longitude": 0, + "latitude": 0 + } + } + ], + "breaks": [ + { + "id": "string", + "name": "string", + "type": "paid", + "startTime": 0, + "duration": 0 + } + ], + "shiftDetails": { + "shiftLayers": [ + { + "id": "string", + "title": "string", + "value": { + "id": "string", + "displayName": "string" + } + } + ] + } + } + ] + }, + "paging": { + "offset": 0 + } +} \ No newline at end of file diff --git a/components/connecteam/sources/new-user/new-user.mjs b/components/connecteam/sources/new-user/new-user.mjs new file mode 100644 index 0000000000000..e7582f0b3b1b9 --- /dev/null +++ b/components/connecteam/sources/new-user/new-user.mjs @@ -0,0 +1,36 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "connecteam-new-user", + name: "New User Added", + description: "Emit new event when a new user is added.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getModelField() { + return "users"; + }, + getModelFieldId() { + return "userId"; + }, + getModelDateField() { + return "createdAt"; + }, + getParams(lastDate) { + return { + createdAt: lastDate, + }; + }, + getFunction() { + return this.connecteam.listUsers; + }, + getSummary(item) { + return `New User: ${item.firstName} ${item.lastName}`; + }, + }, + sampleEmit, +}; diff --git a/components/connecteam/sources/new-user/test-event.mjs b/components/connecteam/sources/new-user/test-event.mjs new file mode 100644 index 0000000000000..28c5f2ed28c86 --- /dev/null +++ b/components/connecteam/sources/new-user/test-event.mjs @@ -0,0 +1,23 @@ +export default { + "firstName": "string", + "lastName": "string", + "phoneNumber": "string", + "userType": "user", + "email": "string", + "customFields": [ + { + "customFieldId": 0, + "type": "email", + "name": "string" + } + ], + "isArchived": false, + "userId": 0, + "createdAt": 0, + "modifiedAt": 0, + "archivedAt": 0, + "lastLogin": 0, + "smartGroupsIds": [ + 0 + ] +} \ No newline at end of file diff --git a/components/connectwise_psa/README.md b/components/connectwise_psa/README.md new file mode 100644 index 0000000000000..22737308aaf46 --- /dev/null +++ b/components/connectwise_psa/README.md @@ -0,0 +1,11 @@ +# Overview + +ConnectWise PSA (Professional Services Automation) API offers a powerful avenue for managing business processes related to technology services. By integrating with ConnectWise PSA via Pipedream, developers can automate complex workflows, synchronize data across various platforms, and enhance operational efficiencies. This API allows for control over modules like service tickets, project management, and account management, essentially streamlining operations and making data management more effective. + +# Example Use Cases + +- **Ticket Management Automation**: Automatically create or update tickets in ConnectWise PSA whenever specific triggers occur in other apps, such as receiving a high-priority email in Gmail or a new form entry in Typeform. This workflow can help in ensuring rapid response times and better issue tracking. + +- **Client Onboarding**: Streamline the onboarding process for new clients by using ConnectWise PSA to manage project setups, configurations, and initial assessments whenever a new client is added in CRM platforms like Salesforce. Automate task creation and assignment to ensure every client setup is thorough and consistent. + +- **Invoice and Payment Sync**: Sync invoices and payments between ConnectWise PSA and accounting software like QuickBooks. Automate the process of updating financial records whenever a new invoice is created or a payment is received in ConnectWise, ensuring that your financial data remains accurate and up to date. diff --git a/components/connectwise_psa/actions/create-company/create-company.mjs b/components/connectwise_psa/actions/create-company/create-company.mjs new file mode 100644 index 0000000000000..a39f2cac87a18 --- /dev/null +++ b/components/connectwise_psa/actions/create-company/create-company.mjs @@ -0,0 +1,142 @@ +import connectwise from "../../connectwise_psa.app.mjs"; + +export default { + key: "connectwise_psa-create-company", + name: "Create Company", + description: "Creates a new company in Connectwise. [See the documentation](https://developer.connectwise.com/Products/ConnectWise_PSA/REST#/Companies/postCompanyCompanies)", + version: "0.0.1", + type: "action", + props: { + connectwise, + name: { + type: "string", + label: "Company Name", + description: "The name of the new company", + }, + identifier: { + type: "string", + label: "Identifier", + description: "A unique identifier for the company", + }, + types: { + propDefinition: [ + connectwise, + "companyTypes", + ], + }, + status: { + propDefinition: [ + connectwise, + "status", + ], + }, + site: { + type: "string", + label: "Site Name", + description: "The site name for the company", + }, + address1: { + type: "string", + label: "Address Line 1", + description: "First line of the company's address", + optional: true, + }, + address2: { + type: "string", + label: "Address Line 2", + description: "Second line of the company's address", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "City of the company", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "State of the company", + optional: true, + }, + zip: { + type: "string", + label: "Zip", + description: "Zip code of the company", + optional: true, + }, + country: { + propDefinition: [ + connectwise, + "country", + ], + }, + phone: { + type: "string", + label: "Phone", + description: "Phone number of the company", + optional: true, + }, + website: { + type: "string", + label: "Website", + description: "Website of the company", + optional: true, + }, + market: { + propDefinition: [ + connectwise, + "market", + ], + }, + territory: { + propDefinition: [ + connectwise, + "territory", + ], + }, + }, + async run({ $ }) { + const types = this.types.map((type) => ({ + id: type, + })); + const response = await this.connectwise.createCompany({ + $, + data: { + name: this.name, + identifier: this.identifier, + types, + status: { + id: this.status, + }, + site: { + name: this.site, + }, + addressLine1: this.address1, + addressLine2: this.address2, + city: this.city, + state: this.state, + zip: this.zip, + country: this.country + ? { + id: this.country, + } + : undefined, + phoneNumber: this.phone, + website: this.website, + market: this.market + ? { + id: this.market, + } + : undefined, + territory: this.territory + ? { + id: this.territory, + } + : undefined, + }, + }); + $.export("$summary", `Successfully created company with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/connectwise_psa/actions/create-contact/create-contact.mjs b/components/connectwise_psa/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..4ac0d16d6d05b --- /dev/null +++ b/components/connectwise_psa/actions/create-contact/create-contact.mjs @@ -0,0 +1,153 @@ +import connectwise from "../../connectwise_psa.app.mjs"; + +export default { + key: "connectwise_psa-create-contact", + name: "Create Contact", + description: "Creates a new contact in Connectwise. [See the documentation](https://developer.connectwise.com/Products/ConnectWise_PSA/REST#/Contacts/postCompanyContacts)", + version: "0.0.1", + type: "action", + props: { + connectwise, + firstName: { + type: "string", + label: "First Name", + description: "First name of the contact", + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the contact", + }, + email: { + type: "string", + label: "Email", + description: "Email address of the contact", + optional: true, + }, + types: { + propDefinition: [ + connectwise, + "contactTypes", + ], + }, + company: { + propDefinition: [ + connectwise, + "company", + ], + optional: true, + }, + relationship: { + propDefinition: [ + connectwise, + "relationship", + ], + }, + department: { + propDefinition: [ + connectwise, + "department", + ], + }, + phone: { + type: "string", + label: "Phone", + description: "Phone number of the contact", + optional: true, + }, + address1: { + type: "string", + label: "Address Line 1", + description: "First line of the contact's address", + optional: true, + }, + address2: { + type: "string", + label: "Address Line 2", + description: "Second line of the contact's address", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "City of the contact", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "State of the contact", + optional: true, + }, + zip: { + type: "string", + label: "Zip", + description: "Zip code of the contact", + optional: true, + }, + country: { + propDefinition: [ + connectwise, + "country", + ], + }, + }, + async run({ $ }) { + const communicationItems = []; + if (this.email) { + communicationItems.push({ + value: this.email, + type: { + id: 1, // email + }, + }); + } + if (this.phone) { + communicationItems.push({ + value: this.phone, + type: { + id: 2, // phone + }, + }); + } + const types = this.types.map((type) => ({ + id: type, + })); + const response = await this.connectwise.createContact({ + $, + data: { + firstName: this.firstName, + lastName: this.lastName, + addressLine1: this.address1, + addressLine2: this.address2, + city: this.city, + state: this.state, + zip: this.zip, + country: this.country + ? { + id: this.country, + } + : undefined, + communicationItems, + types, + company: this.company + ? { + id: this.company, + } + : undefined, + relationship: this.relationship + ? { + id: this.relationship, + } + : undefined, + department: this.department + ? { + id: this.department, + } + : undefined, + }, + }); + $.export("$summary", `Successfully created contact with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/connectwise_psa/actions/create-ticket/create-ticket.mjs b/components/connectwise_psa/actions/create-ticket/create-ticket.mjs new file mode 100644 index 0000000000000..850a2d9d1f910 --- /dev/null +++ b/components/connectwise_psa/actions/create-ticket/create-ticket.mjs @@ -0,0 +1,58 @@ +import connectwise from "../../connectwise_psa.app.mjs"; + +export default { + key: "connectwise_psa-create-ticket", + name: "Create Ticket", + description: "Creates a new ticket in Connectwise. [See the documentation](https://developer.connectwise.com/Products/ConnectWise_PSA/REST#/Tickets/postServiceTickets)", + version: "0.0.1", + type: "action", + props: { + connectwise, + summary: { + type: "string", + label: "Summary", + description: "The subject line or description line for the ticket", + }, + company: { + propDefinition: [ + connectwise, + "company", + ], + }, + contact: { + propDefinition: [ + connectwise, + "contact", + ], + }, + priority: { + propDefinition: [ + connectwise, + "priority", + ], + }, + }, + async run({ $ }) { + const response = await this.connectwise.createTicket({ + $, + data: { + summary: this.summary, + company: { + id: this.company, + }, + contact: this.contact + ? { + id: this.contact, + } + : undefined, + priority: this.priority + ? { + id: this.priority, + } + : undefined, + }, + }); + $.export("$summary", `Successfully created ticket with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/connectwise_psa/connectwise_psa.app.mjs b/components/connectwise_psa/connectwise_psa.app.mjs new file mode 100644 index 0000000000000..95104a7a06031 --- /dev/null +++ b/components/connectwise_psa/connectwise_psa.app.mjs @@ -0,0 +1,364 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "connectwise_psa", + propDefinitions: { + company: { + type: "integer", + label: "Company", + description: "The identifier of a company", + async options({ page }) { + const companies = await this.listCompanies({ + params: { + page: page + 1, + }, + }); + return companies?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + contact: { + type: "integer", + label: "Contact", + description: "The identifier of a contact", + async options({ page }) { + const contacts = await this.listContacts({ + params: { + page: page + 1, + }, + }); + return contacts?.map(({ + id: value, firstName, lastName, + }) => ({ + value, + label: `${firstName} ${lastName}`, + })) || []; + }, + }, + companyTypes: { + type: "integer[]", + label: "Types", + description: "Select one or more company types", + async options({ page }) { + const types = await this.listCompanyTypes({ + params: { + page: page + 1, + }, + }); + return types?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + contactTypes: { + type: "integer[]", + label: "Types", + description: "Select one or more contact types", + async options({ page }) { + const types = await this.listContactTypes({ + params: { + page: page + 1, + }, + }); + return types?.map(({ + id: value, description: label, + }) => ({ + value, + label, + })) || []; + }, + }, + status: { + type: "integer", + label: "Status", + description: "The status of the company", + async options({ page }) { + const statuses = await this.listCompanyStatuses({ + params: { + page: page + 1, + }, + }); + return statuses?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + territory: { + type: "integer", + label: "Territory", + description: "The territory location the company", + optional: true, + async options({ page }) { + const territories = await this.listLocations({ + params: { + page: page + 1, + }, + }); + return territories?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + market: { + type: "integer", + label: "Market", + description: "The market of the company", + optional: true, + async options({ page }) { + const markets = await this.listMarkets({ + params: { + page: page + 1, + }, + }); + return markets?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + relationship: { + type: "integer", + label: "Relationship", + description: "The relationship of the contact", + optional: true, + async options({ page }) { + const relationships = await this.listRelationships({ + params: { + page: page + 1, + }, + }); + return relationships?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + department: { + type: "integer", + label: "Department", + description: "The department of the contact", + optional: true, + async options({ page }) { + const departments = await this.listDepartments({ + params: { + page: page + 1, + }, + }); + return departments?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + country: { + type: "integer", + label: "Country", + description: "The identifier of a country", + optional: true, + async options({ page }) { + const countries = await this.listCountries({ + params: { + page: page + 1, + }, + }); + return countries?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + priority: { + type: "integer", + label: "Priority", + description: "The priority of the ticket", + optional: true, + async options({ page }) { + const priorities = await this.listPriorities({ + params: { + page: page + 1, + }, + }); + return priorities?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.environment}/v4_6_release/apis/3.0`; + }, + _headers() { + return { + clientId: this.$auth.client_id, + }; + }, + _auth() { + return { + username: `${this.$auth.company_id}+${this.$auth.public_key}`, + password: `${this.$auth.private_key}`, + }; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._headers(), + auth: this._auth(), + ...opts, + }); + }, + listCompanies(opts = {}) { + return this._makeRequest({ + path: "/company/companies", + ...opts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/company/contacts", + ...opts, + }); + }, + listProjects(opts = {}) { + return this._makeRequest({ + path: "/project/projects", + ...opts, + }); + }, + listTickets(opts = {}) { + return this._makeRequest({ + path: "/service/tickets", + ...opts, + }); + }, + listCompanyTypes(opts = {}) { + return this._makeRequest({ + path: "/company/companies/types", + ...opts, + }); + }, + listContactTypes(opts = {}) { + return this._makeRequest({ + path: "/company/contacts/types", + ...opts, + }); + }, + listCompanyStatuses(opts = {}) { + return this._makeRequest({ + path: "/company/companies/statuses", + ...opts, + }); + }, + listLocations(opts = {}) { + return this._makeRequest({ + path: "/system/locations", + ...opts, + }); + }, + listMarkets(opts = {}) { + return this._makeRequest({ + path: "/company/marketDescriptions", + ...opts, + }); + }, + listRelationships(opts = {}) { + return this._makeRequest({ + path: "/company/contacts/relationships", + ...opts, + }); + }, + listDepartments(opts = {}) { + return this._makeRequest({ + path: "/company/contacts/departments", + ...opts, + }); + }, + listPriorities(opts = {}) { + return this._makeRequest({ + path: "/service/priorities", + ...opts, + }); + }, + listCountries(opts = {}) { + return this._makeRequest({ + path: "/company/countries", + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/company/contacts", + ...opts, + }); + }, + createCompany(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/company/companies", + ...opts, + }); + }, + createTicket(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/service/tickets", + ...opts, + }); + }, + async *paginate({ + resourceFn, + params, + max, + }) { + params = { + ...params, + page: 1, + }; + let hasMore, count = 0; + do { + const items = await resourceFn({ + params, + }); + for (const item of items) { + yield item; + count++; + if (max && count >= max) { + return; + } + } + hasMore = items.length; + params.page++; + } while (hasMore); + }, + }, +}; diff --git a/components/connectwise_psa/package.json b/components/connectwise_psa/package.json new file mode 100644 index 0000000000000..12cb51c301656 --- /dev/null +++ b/components/connectwise_psa/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/connectwise_psa", + "version": "0.1.1", + "description": "Pipedream Connectwise PSA Components", + "main": "connectwise_psa.app.mjs", + "keywords": [ + "pipedream", + "connectwise_psa" + ], + "homepage": "https://pipedream.com/apps/connectwise_psa", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} diff --git a/components/connectwise_psa/sources/common/base.mjs b/components/connectwise_psa/sources/common/base.mjs new file mode 100644 index 0000000000000..f31b81aba1551 --- /dev/null +++ b/components/connectwise_psa/sources/common/base.mjs @@ -0,0 +1,70 @@ +import connectwise from "../../connectwise_psa.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + connectwise, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + getParams() { + return {}; + }, + generateMeta(item) { + return { + id: item.id, + summary: this.getSummary(item), + ts: Date.now(), + }; + }, + async processEvent(max) { + const lastId = this._getLastId(); + const resourceFn = this.getResourceFn(); + const params = this.getParams(); + const results = this.connectwise.paginate({ + resourceFn, + params, + }); + let items = []; + for await (const item of results) { + if (item.id > lastId) { + items.push(item); + } + } + if (!items.length) { + return; + } + if (max) { + items = items.slice(-1 * max); + } + this._setLastId(items[items.length - 1].id); + items.forEach((item) => this.$emit(item, this.generateMeta(item))); + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/connectwise_psa/sources/new-contact-created/new-contact-created.mjs b/components/connectwise_psa/sources/new-contact-created/new-contact-created.mjs new file mode 100644 index 0000000000000..630191f34e289 --- /dev/null +++ b/components/connectwise_psa/sources/new-contact-created/new-contact-created.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "connectwise_psa-new-contact-created", + name: "New Contact Created", + description: "Emit new event when a new contact is created in Connectwise.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.connectwise.listContacts; + }, + getSummary(item) { + return `New Contact Created: ${item.firstName} ${item.lastName}`; + }, + }, + sampleEmit, +}; diff --git a/components/connectwise_psa/sources/new-contact-created/test-event.mjs b/components/connectwise_psa/sources/new-contact-created/test-event.mjs new file mode 100644 index 0000000000000..5fd2b0a999e61 --- /dev/null +++ b/components/connectwise_psa/sources/new-contact-created/test-event.mjs @@ -0,0 +1,63 @@ +export default { + "id": 179, + "firstName": "Mike", + "lastName": "Evans", + "company": { + "id": 132, + "identifier": "EarthTrisolarisOrganization", + "name": "Earth-Trisolaris Organization", + "_info": { + "company_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//company/companies/132", + "mobileGuid": "74e4f32b-ebda-40d4-b25f-231d97e9dd28" + } + }, + "site": { + "id": 136, + "name": "Earth", + "_info": { + "site_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//company/companies/132/sites/136", + "mobileGuid": "77d95c91-bc33-4931-940a-22295abf42e3" + } + }, + "inactiveFlag": false, + "marriedFlag": false, + "childrenFlag": false, + "portalSecurityLevel": 1, + "disablePortalLoginFlag": true, + "unsubscribeFlag": false, + "mobileGuid": "8d6a547e-6a2e-43aa-9670-9907376e4f4f", + "defaultBillingFlag": false, + "defaultFlag": false, + "companyLocation": { + "id": 39, + "name": "My Accounts", + "_info": { + "location_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//system/locations/39" + } + }, + "types": [ + { + "id": 11, + "name": "No Longer Employed", + "_info": { + "type_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//company/contacts/types/11" + } + } + ], + "ignoreDuplicates": false, + "_info": { + "lastUpdated": "2024-05-31T18:18:16Z", + "updatedBy": "Admin1", + "communications_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//company/contacts/179/communications", + "notes_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//company/contacts/179/notes", + "tracks_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//company/contacts/179/tracks", + "portalSecurity_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//company/contacts/179/portalSecurity", + "activities_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//sales/activities?conditions=contact/id=179", + "documents_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//system/documents?recordType=Contact&recordId=179", + "configurations_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//company/configurations?conditions=contact/id=179", + "tickets_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//service/tickets?conditions=contact/id=179", + "opportunities_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//sales/opportunities?conditions=contact/id=179", + "projects_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//project/projects?conditions=contact/id=179", + "image_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//company/contacts/179/image?lastModified=2024-05-31T18:18:16Z" + } +} \ No newline at end of file diff --git a/components/connectwise_psa/sources/new-project-created/new-project-created.mjs b/components/connectwise_psa/sources/new-project-created/new-project-created.mjs new file mode 100644 index 0000000000000..ddd86b0c76b22 --- /dev/null +++ b/components/connectwise_psa/sources/new-project-created/new-project-created.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "connectwise_psa-new-project-created", + name: "New Project Created", + description: "Emit new event when a new project is created in Connectwise.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.connectwise.listProjects; + }, + getSummary(item) { + return `New Project Created: ${item.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/connectwise_psa/sources/new-project-created/test-event.mjs b/components/connectwise_psa/sources/new-project-created/test-event.mjs new file mode 100644 index 0000000000000..84899a49b7b15 --- /dev/null +++ b/components/connectwise_psa/sources/new-project-created/test-event.mjs @@ -0,0 +1,122 @@ +export default { + "id": 11, + "actualHours": 0, + "billExpenses": "NoDefault", + "billingAmount": 0, + "billingAttention": "", + "billingMethod": "ActualRates", + "billingRateType": "WorkRole", + "billProducts": "Billable", + "billProjectAfterClosedFlag": true, + "billTime": "NoDefault", + "billUnapprovedTimeAndExpense": false, + "board": { + "id": 3, + "name": "Projects-2-10", + "_info": { + "board_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//service/boards/3" + } + }, + "budgetAnalysis": "ActualHours", + "budgetFlag": false, + "company": { + "id": 2, + "identifier": "YourCompany", + "name": "Your Company", + "_info": { + "company_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//company/companies/2" + } + }, + "contact": { + "id": 77, + "name": "Arnie Bellini", + "_info": { + "contact_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//company/contacts/77" + } + }, + "customerPO": "", + "description": "", + "currency": { + "id": 7, + "symbol": "$", + "currencyCode": "USD", + "decimalSeparator": ".", + "numberOfDecimals": 2, + "thousandsSeparator": ",", + "negativeParenthesesFlag": false, + "displaySymbolFlag": false, + "currencyIdentifier": "Z-US$", + "displayIdFlag": false, + "rightAlign": false, + "name": "US Dollars", + "_info": { + "currency_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//finance/currencies/7" + } + }, + "downpayment": 0, + "estimatedEnd": "2007-07-27T00:00:00Z", + "estimatedExpenseRevenue": 0, + "estimatedHours": 0, + "estimatedProductRevenue": 0, + "estimatedStart": "2007-07-02T00:00:00Z", + "estimatedTimeRevenue": 0, + "includeDependenciesFlag": false, + "includeEstimatesFlag": false, + "location": { + "id": 2, + "name": "Tampa Office", + "_info": { + "location_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//system/locations/2" + } + }, + "department": { + "id": 10, + "identifier": "Services", + "name": "Professional Services", + "_info": { + "department_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//system/departments/10" + } + }, + "manager": { + "id": 150, + "identifier": "zAdmin", + "name": "zSys Admin", + "_info": { + "member_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//system/members/150" + } + }, + "name": "ConnectWise Implementation", + "restrictDownPaymentFlag": false, + "scheduledHours": 0, + "status": { + "id": 1, + "name": "Open", + "_info": { + "status_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//project/statuses/1" + } + }, + "closedFlag": false, + "type": { + "id": 8, + "name": "Implementations", + "_info": { + "projectType_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//project/projectTypes/8" + } + }, + "doNotDisplayInPortalFlag": false, + "poAmount": 0, + "estimatedTimeCost": 0, + "estimatedExpenseCost": 0, + "estimatedProductCost": 0, + "companyLocation": { + "id": 38, + "name": "Corporate", + "_info": { + "location_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//system/locations/38" + } + }, + "_info": { + "lastUpdated": "2007-07-17T17:26:02Z", + "updatedBy": "zadmin" + } +} \ No newline at end of file diff --git a/components/connectwise_psa/sources/new-ticket-created/new-ticket-created.mjs b/components/connectwise_psa/sources/new-ticket-created/new-ticket-created.mjs new file mode 100644 index 0000000000000..e9dec3f20ca97 --- /dev/null +++ b/components/connectwise_psa/sources/new-ticket-created/new-ticket-created.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "connectwise_psa-new-ticket-created", + name: "New Ticket Created", + description: "Emit new event when a new ticket is created in Connectwise.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.connectwise.listTickets; + }, + getSummary(item) { + return `New Ticket Created: ${item.summary}`; + }, + }, + sampleEmit, +}; diff --git a/components/connectwise_psa/sources/new-ticket-created/test-event.mjs b/components/connectwise_psa/sources/new-ticket-created/test-event.mjs new file mode 100644 index 0000000000000..296bc6520be1f --- /dev/null +++ b/components/connectwise_psa/sources/new-ticket-created/test-event.mjs @@ -0,0 +1,183 @@ +export default { + "id": 493, + "summary": "Check Remote Backup", + "recordType": "ServiceTicket", + "board": { + "id": 1, + "name": "Professional Services", + "_info": { + "board_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//service/boards/1" + } + }, + "status": { + "id": 16, + "name": "New (not responded)", + "Sort": 0, + "_info": { + "status_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//service/boards/1/statuses/16" + } + }, + "company": { + "id": 16, + "identifier": "IndigoStrawberryCo", + "name": "IndigoStrawberry, Co.", + "_info": { + "company_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//company/companies/16", + "mobileGuid": "102830a1-479e-4a5f-bec1-fbc532e43c30" + } + }, + "site": { + "id": 20, + "name": "Main", + "_info": { + "site_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//company/companies/16/sites/20", + "mobileGuid": "caf531b0-1e63-4159-b429-642b6057a242" + } + }, + "siteName": "Main", + "addressLine1": "2106 SHADYHILL TER", + "city": "Harrells", + "stateIdentifier": "FL", + "zip": "34667", + "contact": { + "id": 48, + "name": "Ramon Stawiarz", + "_info": { + "mobileGuid": "57a19455-a9d7-4587-ac45-0f80f5387c31", + "contact_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//company/contacts/48" + } + }, + "contactName": "Ramon Stawiarz", + "contactPhoneNumber": "8133936413", + "contactEmailAddress": "ramon.stawiarz@indigostrawberry.com", + "type": { + "id": 14, + "name": "Proactive", + "_info": { + "type_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//service/boards/1/types/14" + } + }, + "team": { + "id": 25, + "name": "Service Team", + "_info": { + "team_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//service/boards/1/teams/25" + } + }, + "priority": { + "id": 4, + "name": "Priority 3 - Normal Response", + "sort": 6, + "_info": { + "priority_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//service/priorities/4", + "image_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//service/priorities/4/image?lm=2005-05-27T14:58:21Z" + } + }, + "serviceLocation": { + "id": 2, + "name": "In-house", + "_info": { + "location_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//service/locations/2" + } + }, + "source": { + "id": 2, + "name": "Phone", + "_info": { + "source_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//service/sources/2" + } + }, + "severity": "Medium", + "impact": "Medium", + "allowAllClientsPortalView": false, + "customerUpdatedFlag": false, + "automaticEmailContactFlag": false, + "automaticEmailResourceFlag": false, + "automaticEmailCcFlag": false, + "automaticEmailCc": "", + "closedFlag": false, + "approved": true, + "estimatedExpenseCost": 0, + "estimatedExpenseRevenue": 0, + "estimatedProductCost": 0, + "estimatedProductRevenue": 0, + "estimatedTimeCost": 0, + "estimatedTimeRevenue": 0, + "billingMethod": "ActualRates", + "subBillingMethod": "ActualRates", + "resolveMinutes": 0, + "resPlanMinutes": 0, + "respondMinutes": 0, + "isInSla": false, + "hasChildTicket": false, + "hasMergedChildTicketFlag": false, + "billTime": "NoDefault", + "billExpenses": "NoDefault", + "billProducts": "Billable", + "location": { + "id": 2, + "name": "Tampa Office", + "_info": { + "location_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//system/locations/2" + } + }, + "department": { + "id": 10, + "identifier": "Services", + "name": "Professional Services", + "_info": { + "department_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//system/departments/10" + } + }, + "mobileGuid": "25bdf3ce-b284-470b-a13d-0ffcea6aa922", + "sla": { + "id": 1, + "name": "Standard SLA", + "_info": { + "sla_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//service/SLAs/1" + } + }, + "slaStatus": "Respond by Mon 07/04 1:00 PM UTC-04", + "requestForChangeFlag": false, + "currency": { + "id": 7, + "symbol": "$", + "currencyCode": "USD", + "decimalSeparator": ".", + "numberOfDecimals": 2, + "thousandsSeparator": ",", + "negativeParenthesesFlag": false, + "displaySymbolFlag": false, + "currencyIdentifier": "Z-US$", + "displayIdFlag": false, + "rightAlign": false, + "name": "US Dollars", + "_info": { + "currency_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//finance/currencies/7" + } + }, + "_info": { + "lastUpdated": "2022-07-02T04:20:41Z", + "updatedBy": "template3", + "dateEntered": "2022-07-02T04:20:41Z", + "enteredBy": "template3", + "activities_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//sales/activities?conditions=ticket/id=493", + "scheduleentries_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//schedule/entries?conditions=type/id=4 AND objectId=493", + "documents_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//system/documents?recordType=Ticket&recordId=493", + "configurations_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//service/tickets/493/configurations", + "tasks_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//service/tickets/493/tasks", + "notes_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//service/tickets/493/notes", + "products_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//procurement/products?conditions=chargeToType='Ticket' AND chargeToId=493", + "timeentries_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//time/entries?conditions=(chargeToType='ServiceTicket' OR chargeToType='ProjectTicket') AND chargeToId=493", + "expenseEntries_href": "https://api-staging.connectwisedev.com/v4_6_release/apis/3.0//expense/entries?conditions=(chargeToType='ServiceTicket' OR chargeToType='ProjectTicket') AND chargeToId=493" + }, + "escalationStartDateUTC": "2022-07-04T13:00:00Z", + "escalationLevel": 0, + "minutesBeforeWaiting": 0, + "respondedSkippedMinutes": 0, + "resplanSkippedMinutes": 0, + "respondedHours": 4, + "resplanHours": 24, + "resolutionHours": 48, + "minutesWaiting": 0 +} \ No newline at end of file diff --git a/components/constant_contact/README.md b/components/constant_contact/README.md new file mode 100644 index 0000000000000..3eb60cce54f9a --- /dev/null +++ b/components/constant_contact/README.md @@ -0,0 +1,11 @@ +# Overview + +The Constant Contact API allows users to automate email marketing efforts by managing contacts, sending emails, and tracking results. With Pipedream, you can connect Constant Contact to other apps to create powerful workflows. You can trigger events, sync contact lists, automate email campaigns, and analyze email performance with ease. Pipedream's serverless platform offers a streamlined approach to integrating Constant Contact's functionalities into diverse, automated sequences. + +# Example Use Cases + +- **Sync New Shopify Customers to Constant Contact**: When a new customer is added in Shopify, automatically add them to a specified contact list in Constant Contact. This keeps your email list updated with minimal effort. + +- **Automate Email Campaigns Based on Survey Responses**: Link Constant Contact with a survey app like Typeform. Trigger an email sequence in Constant Contact for users who score a certain threshold on a feedback survey, allowing for targeted follow-ups. + +- **Track Email Campaign Performance in Google Sheets**: After sending an email campaign through Constant Contact, use Pipedream to capture the campaign's performance data and log it to a Google Sheet for easy tracking and analysis. diff --git a/components/contact_enhance/README.md b/components/contact_enhance/README.md new file mode 100644 index 0000000000000..5efed3a71f931 --- /dev/null +++ b/components/contact_enhance/README.md @@ -0,0 +1,11 @@ +# Overview + +The Contact Enhance API allows you to enrich contact data by providing detailed information on individuals or companies based on email, domain, or other identifying data. With this API integrated into Pipedream, it's possible to automate enriching contact lists, validate and score leads for marketing, and augment customer profiles for better personalization. Pipedream's serverless platform enables these tasks to be set up with minimal coding, leveraging a vast array of triggers and actions from various apps to create powerful workflows. + +# Example Use Cases + +- **Lead Qualification Automation**: Trigger a workflow with a new lead submission from a form or CRM like Salesforce. Pass the lead's email to Contact Enhance API to retrieve detailed information, then use conditional logic in Pipedream to score the lead and add it to the appropriate pipeline or nurturing sequence in the CRM. + +- **Enriched Contact Segmentation**: When a new email is added to a mailing list in an email marketing platform like Mailchimp, use Contact Enhance API to get additional details about the contact. Based on the enriched data, create segments in Mailchimp for targeted campaigns, improving engagement and conversion rates. + +- **Customer Data Platform Enhancement**: On a regular schedule, pull records from a Customer Data Platform (CDP) like Segment. Use Contact Enhance API to update each record with additional data. The enriched profiles can then be synchronized back to the CDP or used to tailor customer experiences across various touchpoints. diff --git a/components/contact_enhance/actions/find-contact/find-contact.mjs b/components/contact_enhance/actions/find-contact/find-contact.mjs new file mode 100644 index 0000000000000..20a1358b9d392 --- /dev/null +++ b/components/contact_enhance/actions/find-contact/find-contact.mjs @@ -0,0 +1,46 @@ +import app from "../../contact_enhance.app.mjs"; + +export default { + key: "contact_enhance-find-contact", + name: "Find Contact", + description: "Locates a specific contact in the database using the email. [See the documentation](https://u.pcloud.link/publink/show?code=XZ8tzp0ZjaO5gqh55FuTmEEbwt0GOJPtqqgX)", + version: "0.0.1", + type: "action", + props: { + app, + email: { + type: "string", + label: "Email", + description: "The email address of the contact to find", + }, + }, + methods: { + getContacts(args = {}) { + return this.app.post({ + path: "/contacts", + ...args, + }); + }, + }, + async run({ $ }) { + const { + getContacts, + email, + } = this; + + const response = await getContacts({ + $, + params: { + email, + }, + }); + + if (response.message) { + $.export("$summary", `Failed to find contact with email \`${email}\``); + return response; + } + + $.export("$summary", `Successfully found contact with email \`${email}\``); + return response; + }, +}; diff --git a/components/contact_enhance/contact_enhance.app.mjs b/components/contact_enhance/contact_enhance.app.mjs new file mode 100644 index 0000000000000..b50e3f5717565 --- /dev/null +++ b/components/contact_enhance/contact_enhance.app.mjs @@ -0,0 +1,37 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "contact_enhance", + methods: { + getHost() { + return "contactenhance-91b9c0ef8a71.herokuapp.com"; + }, + getUrl(path) { + return `https://${this.getHost()}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Host": this.getHost(), + "Content-Type": "application/json", + "Authorization": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "post", + ...args, + }); + }, + }, +}; diff --git a/components/contact_enhance/package.json b/components/contact_enhance/package.json new file mode 100644 index 0000000000000..de9ffaf6f249e --- /dev/null +++ b/components/contact_enhance/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/contact_enhance", + "version": "0.1.0", + "description": "Pipedream Contact Enhance Components", + "main": "contact_enhance.app.mjs", + "keywords": [ + "pipedream", + "contact_enhance" + ], + "homepage": "https://pipedream.com/apps/contact_enhance", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/contacts/README.md b/components/contacts/README.md index 00eb0681869ca..f77b6ff84ccdf 100644 --- a/components/contacts/README.md +++ b/components/contacts/README.md @@ -1,7 +1,11 @@ # Overview -- A mobile app that displays a contact's information when you tap on their name -- A messaging app that lets you easily find and message your contacts -- A social media app that lets you see which of your contacts are also using - the app -- An app that lets you easily access your contacts' information offline +The Contacts+ API on Pipedream opens doors to intelligent automation around contact management. By leveraging Pipedream's serverless platform, you can sync contact information, manage updates, track engagement, and create custom workflows to streamline communication processes. Integration with other apps enables seamless data flow between platforms, enriching CRM systems, marketing campaigns, or customer support services with minimal manual intervention. + +# Example Use Cases + +- **Sync Contacts with a CRM**: Automatically upload new contacts from Contacts+ to your preferred CRM platform, ensuring your sales team always has the latest information at their fingertips. Use Pipedream to detect new contacts and then trigger an action to add them to Salesforce or HubSpot. + +- **Automated Contact Cleanup**: Schedule regular clean-up tasks to keep your Contacts+ database tidy. Workflows can be set up to identify duplicates, update outdated information, and remove inactive contacts, maintaining a high-quality database. + +- **Event-Driven Email Campaigns**: Trigger personalized email campaigns in Mailchimp when specific updates occur in Contacts+, like a change in job title or company. This allows for targeted communication, keeping your marketing efforts sharp and relevant. diff --git a/components/contacts/package.json b/components/contacts/package.json new file mode 100644 index 0000000000000..f5d3d320eba97 --- /dev/null +++ b/components/contacts/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/contacts", + "version": "0.6.0", + "description": "Pipedream contacts Components", + "main": "contacts.app.mjs", + "keywords": [ + "pipedream", + "contacts" + ], + "homepage": "https://pipedream.com/apps/contacts", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/contactually/package.json b/components/contactually/package.json new file mode 100644 index 0000000000000..c28095033b01e --- /dev/null +++ b/components/contactually/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/contactually", + "version": "0.6.0", + "description": "Pipedream contactually Components", + "main": "contactually.app.mjs", + "keywords": [ + "pipedream", + "contactually" + ], + "homepage": "https://pipedream.com/apps/contactually", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/content_snare/README.md b/components/content_snare/README.md new file mode 100644 index 0000000000000..f32e0f472094b --- /dev/null +++ b/components/content_snare/README.md @@ -0,0 +1,11 @@ +# Overview + +The Content Snare API allows you to automate the process of gathering content from clients and stakeholders. With this API, you can create, update, and manage requests for content, as well as track submissions and automate follow-ups. On Pipedream, you can integrate Content Snare with hundreds of other apps to create seamless workflows that save time and reduce manual tasks. From triggering actions when new content is submitted to syncing data across multiple platforms, the possibilities are vast. + +# Example Use Cases + +- **Automated Content Collection Workflow**: Trigger a Pipedream workflow when new content is submitted to Content Snare. Validate the content using built-in code steps or a third-party API, then update the related Content Snare request status. Finally, notify your team via Slack or email that new content is ready for review. + +- **Client Onboarding and Content Scheduling**: Use a Pipedream scheduled workflow to periodically check for pending content requests in Content Snare. If a deadline is approaching, automatically send reminders to clients via SMS using the Twilio app or by email. This helps ensure that content is submitted on time and clients are adequately prompted. + +- **Content Approval and Publishing Integration**: When content is approved within Content Snare, kick off a Pipedream workflow that sends the content to a CMS like WordPress for publishing. After publishing, update the status in Content Snare and send a confirmation to the client along with the live URL using a service like SendGrid or Mailgun. diff --git a/components/content_snare/actions/create-client/create-client.mjs b/components/content_snare/actions/create-client/create-client.mjs new file mode 100644 index 0000000000000..0c2cd972f1413 --- /dev/null +++ b/components/content_snare/actions/create-client/create-client.mjs @@ -0,0 +1,70 @@ +import app from "../../content_snare.app.mjs"; + +export default { + key: "content_snare-create-client", + name: "Create Client", + description: "Creates a new client on Content Snare. [See the documentation](https://api.contentsnare.com/partner_api/v1/documentation#post-/partner_api/v1/clients)", + version: "0.0.1", + type: "action", + props: { + app, + companyName: { + propDefinition: [ + app, + "companyName", + ], + }, + email: { + propDefinition: [ + app, + "clientEmail", + ], + }, + fullName: { + propDefinition: [ + app, + "clientFullName", + ], + }, + phone: { + propDefinition: [ + app, + "clientPhone", + ], + }, + }, + methods: { + createClient(args = {}) { + return this.app.post({ + path: "/clients", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createClient, + companyName, + email, + fullName, + phone, + } = this; + + const response = await createClient({ + $, + data: { + client_companies: (companyName && [ + { + name: companyName, + }, + ]), + email, + full_name: fullName, + phone, + }, + }); + + $.export("$summary", `Successfully created client with ID \`${response.id}\``); + return response; + }, +}; diff --git a/components/content_snare/actions/create-request/create-request.mjs b/components/content_snare/actions/create-request/create-request.mjs new file mode 100644 index 0000000000000..a428b2313a303 --- /dev/null +++ b/components/content_snare/actions/create-request/create-request.mjs @@ -0,0 +1,91 @@ +import app from "../../content_snare.app.mjs"; + +export default { + key: "content_snare-create-request", + name: "Create Request", + description: "Initiates a novel request on Content Snare. [See the documentation](https://api.contentsnare.com/partner_api/v1/documentation#post-/partner_api/v1/requests)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + type: "string", + label: "Request Name", + description: "The name of the request to initiate on Content Snare.", + }, + clientEmail: { + propDefinition: [ + app, + "clientEmail", + ], + }, + clientFullName: { + propDefinition: [ + app, + "clientFullName", + ], + }, + clientPhone: { + propDefinition: [ + app, + "clientPhone", + ], + }, + companyName: { + propDefinition: [ + app, + "companyName", + ], + }, + requestTemplateName: { + type: "string", + label: "Request Template Name", + description: "The name of the request template to use for this request. Either this field or **Request Template ID** must be provided.", + optional: true, + }, + requestTemplateId: { + propDefinition: [ + app, + "requestTemplateId", + ], + optional: true, + }, + }, + methods: { + createRequest(args = {}) { + return this.app.post({ + path: "/requests", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createRequest, + name, + clientEmail, + clientFullName, + clientPhone, + companyName, + requestTemplateName, + requestTemplateId, + } = this; + + const response = await createRequest({ + $, + data: { + name, + client_email: clientEmail, + client_full_name: clientFullName, + client_phone: clientPhone, + company_name: companyName, + request_template_name: requestTemplateName, + request_template_id: requestTemplateId, + }, + }); + + $.export("$summary", `Successfully created request with ID \`${response.id}\``); + + return response; + }, +}; diff --git a/components/content_snare/common/constants.mjs b/components/content_snare/common/constants.mjs new file mode 100644 index 0000000000000..26088aef41240 --- /dev/null +++ b/components/content_snare/common/constants.mjs @@ -0,0 +1,11 @@ +const BASE_URL = "https://api.contentsnare.com"; +const VERSION_PATH = "/partner_api/v1"; +const WEBHOOK_ID = "webhookId"; +const DEFAULT_LIMIT = 20; + +export default { + BASE_URL, + VERSION_PATH, + WEBHOOK_ID, + DEFAULT_LIMIT, +}; diff --git a/components/content_snare/content_snare.app.mjs b/components/content_snare/content_snare.app.mjs index b596dbac6e473..a50f1b04d64ff 100644 --- a/components/content_snare/content_snare.app.mjs +++ b/components/content_snare/content_snare.app.mjs @@ -1,11 +1,105 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "content_snare", - propDefinitions: {}, + propDefinitions: { + companyName: { + type: "string", + label: "Company Name", + description: "The name of the client's company.", + optional: true, + }, + clientEmail: { + type: "string", + label: "Email", + description: "The email address of the client.", + }, + clientFullName: { + type: "string", + label: "Full Name", + description: "The full name of the client.", + }, + clientPhone: { + type: "string", + label: "Client Phone", + description: "The phone number of the client.", + optional: true, + }, + requestTemplateId: { + type: "string", + label: "Request Template ID", + description: "The ID of the request template to use for this request.", + async options({ prevContext }) { + const { nextOffset } = prevContext; + if (nextOffset === null) { + return []; + } + const { + pagination: { offset }, + results, + } = await this.listRequestTemplates({ + params: { + limit: constants.DEFAULT_LIMIT, + offset: nextOffset, + }, + }); + return { + options: results.map(({ + id: value, name: label, + }) => ({ + label, + value, + })), + context: { + nextOffset: results.length < constants.DEFAULT_LIMIT + ? null + : offset + results.length, + }, + }; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + const config = { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }; + return axios($, config); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + listRequestTemplates(args = {}) { + return this._makeRequest({ + path: "/request_templates", + ...args, + }); }, }, }; diff --git a/components/content_snare/package.json b/components/content_snare/package.json index ec757e55e3071..b95de7e312d3b 100644 --- a/components/content_snare/package.json +++ b/components/content_snare/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/content_snare", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Content Snare Components", "main": "content_snare.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" } -} \ No newline at end of file +} diff --git a/components/content_snare/sources/common/subscription.mjs b/components/content_snare/sources/common/subscription.mjs new file mode 100644 index 0000000000000..f3185729ed45a --- /dev/null +++ b/components/content_snare/sources/common/subscription.mjs @@ -0,0 +1,40 @@ +export default { + ALL_FIELDS_COMPLETED: "all_fields_completed", + CLIENT_CREATE_FAILED: "client_create_failed", + CLIENT_CREATED: "client_created", + CLIENT_DESTROY_FAILED: "client_destroy_failed", + CLIENT_DESTROYED: "client_destroyed", + CLIENT_UPDATE_FAILED: "client_update_failed", + CLIENT_UPDATED: "client_updated", + COMMENT_CREATE_FAILED: "comment_create_failed", + COMMENT_CREATED: "comment_created", + EMAIL_BOUNCED: "email_bounced", + EMAIL_DELIVERED: "email_delivered", + EMAIL_SPAM_COMPLAINED: "email_spam_complained", + FIELD_APPROVED: "field_approved", + FIELD_COMPLETED: "field_completed", + FIELD_REJECTED: "field_rejected", + REQUEST_ARCHIVED: "request_archived", + REQUEST_ARCHIVED_FAILED: "request_archived_failed", + REQUEST_BOARD_COLUMN_CHANGE_FAILED: "request_board_column_change_failed", + REQUEST_BOARD_COLUMN_CHANGED: "request_board_column_changed", + REQUEST_COMPLETED: "request_completed", + REQUEST_COMPLETED_FAILED: "request_completed_failed", + REQUEST_CREATE_FAILED: "request_create_failed", + REQUEST_CREATED: "request_created", + REQUEST_DESTROY_FAILED: "request_destroy_failed", + REQUEST_DESTROYED: "request_destroyed", + REQUEST_PUBLISHED: "request_published", + REQUEST_PUBLISHED_FAILED: "request_published_failed", + REQUEST_UPDATE_FAILED: "request_update_failed", + REQUEST_UPDATED: "request_updated", + REQUEST_VIEWED: "request_viewed", + SMS_DELIVERED: "sms_delivered", + SMS_FAILED: "sms_failed", + TEAM_MEMBER_CREATE_FAILED: "team_member_create_failed", + TEAM_MEMBER_CREATED: "team_member_created", + TEAM_MEMBER_DESTROY_FAILED: "team_member_destroy_failed", + TEAM_MEMBER_DESTROYED: "team_member_destroyed", + TEAM_MEMBER_UPDATE_FAILED: "team_member_update_failed", + TEAM_MEMBER_UPDATED: "team_member_updated", +}; diff --git a/components/content_snare/sources/common/webhook.mjs b/components/content_snare/sources/common/webhook.mjs new file mode 100644 index 0000000000000..9f8e767860b44 --- /dev/null +++ b/components/content_snare/sources/common/webhook.mjs @@ -0,0 +1,80 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../content_snare.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + http: "$.interface.http", + }, + hooks: { + async activate() { + const { + http, + createWebhook, + getSubscriptions, + } = this; + + const response = + await createWebhook({ + data: { + url: http.endpoint, + subscriptions: getSubscriptions(), + enabled: true, + }, + }); + + this.setWebhookId(response.id); + }, + async deactivate() { + const { + getWebhookId, + deleteWebhook, + } = this; + + const webhookId = getWebhookId(); + if (webhookId) { + await deleteWebhook({ + webhookId, + }); + } + }, + }, + methods: { + setWebhookId(value) { + this.db.set(constants.WEBHOOK_ID, value); + }, + getWebhookId() { + return this.db.get(constants.WEBHOOK_ID); + }, + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getSubscriptions() { + throw new ConfigurationError("getSubscriptions is not implemented"); + }, + processResource(resource) { + this.$emit(resource, this.generateMeta(resource)); + }, + createWebhook(args = {}) { + return this.app.post({ + debug: true, + path: "/webhooks", + ...args, + }); + }, + deleteWebhook({ + webhookId, ...args + } = {}) { + return this.app.delete({ + debug: true, + path: `/webhooks/${webhookId}`, + ...args, + }); + }, + }, + run({ body }) { + this.processResource(body); + }, +}; diff --git a/components/content_snare/sources/new-client-updated-instant/new-client-updated-instant.mjs b/components/content_snare/sources/new-client-updated-instant/new-client-updated-instant.mjs new file mode 100644 index 0000000000000..5cad0cf0ba9b8 --- /dev/null +++ b/components/content_snare/sources/new-client-updated-instant/new-client-updated-instant.mjs @@ -0,0 +1,31 @@ +import common from "../common/webhook.mjs"; +import subscription from "../common/subscription.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "content_snare-new-client-updated-instant", + name: "New Client Updated (Instant)", + description: "Emit new event when a client is updated in Content Snare. [See the documentation](https://contentsnare.com/help/knowledge-base/content-snare-webhooks/#client-webhooks)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSubscriptions() { + return [ + subscription.CLIENT_UPDATED, + ]; + }, + generateMeta(resource) { + const { id } = resource; + const ts = Date.now(); + return { + id: `${id}-${ts}`, + summary: `Client Updated: ${id}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/content_snare/sources/new-client-updated-instant/test-event.mjs b/components/content_snare/sources/new-client-updated-instant/test-event.mjs new file mode 100644 index 0000000000000..7ed124675cf4a --- /dev/null +++ b/components/content_snare/sources/new-client-updated-instant/test-event.mjs @@ -0,0 +1,9 @@ +export default { + "id": "acc_MORVw9Cyq97AyN", + "event_name": "client_updated", + "company_name": "Example Company", + "email": "example@example.com", + "full_name": "Example Client", + "phone": "123-456-7890", + "avatar": "https://www.gravatar.com/avatar/93942e96f5acd83e2e047ad8fe03114d?s=100&d=robohash", +}; diff --git a/components/content_snare/sources/new-field-approved-instant/new-field-approved-instant.mjs b/components/content_snare/sources/new-field-approved-instant/new-field-approved-instant.mjs new file mode 100644 index 0000000000000..0205b0b614728 --- /dev/null +++ b/components/content_snare/sources/new-field-approved-instant/new-field-approved-instant.mjs @@ -0,0 +1,31 @@ +import common from "../common/webhook.mjs"; +import subscription from "../common/subscription.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "content_snare-new-field-approved-instant", + name: "New Field Approved (Instant)", + description: "Emit new event when a field is approved in Content Snare. [See the documentation](https://contentsnare.com/help/knowledge-base/content-snare-webhooks/#field-webhooks)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSubscriptions() { + return [ + subscription.FIELD_APPROVED, + ]; + }, + generateMeta(resource) { + const { id } = resource; + const ts = Date.now(); + return { + id: `${id}-${ts}`, + summary: `Field Approved: ${id}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/content_snare/sources/new-field-approved-instant/test-event.mjs b/components/content_snare/sources/new-field-approved-instant/test-event.mjs new file mode 100644 index 0000000000000..327ac376b6285 --- /dev/null +++ b/components/content_snare/sources/new-field-approved-instant/test-event.mjs @@ -0,0 +1,38 @@ +export default { + "id": "fld_1lqbwY6TaMPmAo", + "event_name": "field_approved", + "section_id": "0", + "section_name": "Example section name", + "page_id": "0", + "page_name": "Example page name", + "name": 'Example approved field name', + "instruction_text": "Example approved field instructions", + "status": "approved", + "type": "text", + "values": ["Example approved field answer"], + "values_flat": "Example approved field answer", + "request": { + "id": "req_1k7zv4aUlpN8PB", + "name": "Example Request", + "status": "published", + "due_date": "2020-10-20", + "folder_name": "Default Folder", + "url": "https://app.contentsnare.com/requests/EXAMPLE_ONLY", + "pin_code_enabled": "true", + "pin_code": "1234", + "request_template_name": "Example Template", + "communications_template_name": "Default", + "completion_percentage": "0", + "share_link": "https://app.contentsnare.com/requests/EXAMPLE_ONLY", + "owner_name": "Example Owner", + "author_name": "Example Author", + "client": { + "id": "acc_MORVw9Cyq97AyN", + "company_name": "Example Company", + "email": "example@example.com", + "full_name": "Example Client", + "phone": "123-456-7890", + "avatar": "https://www.gravatar.com/avatar/93942e96f5acd83e2e047ad8fe03114d?s=100&d=robohash", + } + } +}; diff --git a/components/content_snare/sources/new-request-published-instant/new-request-published-instant.mjs b/components/content_snare/sources/new-request-published-instant/new-request-published-instant.mjs new file mode 100644 index 0000000000000..37ee154313f82 --- /dev/null +++ b/components/content_snare/sources/new-request-published-instant/new-request-published-instant.mjs @@ -0,0 +1,31 @@ +import common from "../common/webhook.mjs"; +import subscription from "../common/subscription.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "content_snare-new-request-published-instant", + name: "New Request Published (Instant)", + description: "Emit new event when a request is published on Content Snare. [See the documentation](https://contentsnare.com/help/knowledge-base/content-snare-webhooks/)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSubscriptions() { + return [ + subscription.REQUEST_PUBLISHED, + ]; + }, + generateMeta(resource) { + const { id } = resource; + const ts = Date.now(); + return { + id: `${id}-${ts}`, + summary: `Request Published: ${id}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/content_snare/sources/new-request-published-instant/test-event.mjs b/components/content_snare/sources/new-request-published-instant/test-event.mjs new file mode 100644 index 0000000000000..84894dbde8452 --- /dev/null +++ b/components/content_snare/sources/new-request-published-instant/test-event.mjs @@ -0,0 +1,25 @@ +export default { + "id": "req_1k7zv4aUlpN8PB", + "event_name": "request_published", + "name": "Example Request", + "status": "published", + "due_date": "2020-10-20", + "folder_name": "Default Folder", + "url": "https://app.contentsnare.com/requests/EXAMPLE_ONLY", + "pin_code_enabled": "true", + "pin_code": "1234", + "request_template_name": "Example Template", + "communications_template_name": "Default", + "completion_percentage": "0", + "share_link": "https://app.contentsnare.com/requests/EXAMPLE_ONLY", + "owner_name": "Example Owner", + "author_name": "Example Author", + "client": { + "id": "acc_MORVw9Cyq97AyN", + "company_name": "Example Company", + "email": "example@example.com", + "full_name": "Example Client", + "phone": "123-456-7890", + "avatar": "https://www.gravatar.com/avatar/93942e96f5acd83e2e047ad8fe03114d?s=100&d=robohash", + } +}; diff --git a/components/contentful/README.md b/components/contentful/README.md index 7f42e3cbf38c3..238cc1738948d 100644 --- a/components/contentful/README.md +++ b/components/contentful/README.md @@ -1,15 +1,11 @@ # Overview -Contentful is a content management API that enables developers to build -applications that can be used to manage content. Contentful provides a set of -tools that can be used to manage content, including: +Contentful's Content Management API lets you manage content as data and integrate it seamlessly with any tech stack. On Pipedream, you can wield this power to automate content operations, sync data across platforms, and trigger workflows based on content events. For example, you could update an external database with new content entries, notify team members of content changes, or generate static sites when content updates. -- A content management system -- A set of APIs for managing content -- A set of tools for managing content +# Example Use Cases -Contentful can be used to build a variety of applications, including: +- **Automatic Content Backup**: Create a workflow that triggers every time a new entry is published in Contentful. Use this event to save a copy of the content to an AWS S3 bucket for backup purposes. -- Content management systems -- Content management APIs -- Content management tools +- **Content Update Notifications**: Set up a workflow that sends a Slack message to a specified channel whenever a piece of content is updated or published. This keeps your team informed in real-time about content changes, facilitating immediate action or review. + +- **Multi-Channel Content Publishing**: Develop a workflow that, upon publishing content in Contentful, automatically posts a formatted version of that content to multiple platforms such as WordPress, Medium, or social media channels. This expands the reach of your content without additional manual effort. diff --git a/components/contentful/package.json b/components/contentful/package.json new file mode 100644 index 0000000000000..c5874f6399d58 --- /dev/null +++ b/components/contentful/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/contentful", + "version": "0.6.0", + "description": "Pipedream contentful Components", + "main": "contentful.app.mjs", + "keywords": [ + "pipedream", + "contentful" + ], + "homepage": "https://pipedream.com/apps/contentful", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/contentful_graphql/README.md b/components/contentful_graphql/README.md index 7fc9df77e5d36..a2cbf76242981 100644 --- a/components/contentful_graphql/README.md +++ b/components/contentful_graphql/README.md @@ -1,15 +1,11 @@ # Overview -Contentful is a powerful API that can be used to build a variety of -content-rich applications. Here are just a few examples of what you can build -using the Contentful API: - -- A content management system (CMS) -- A blog -- A e-commerce store -- A news site -- A portfolio site - -All of these applications would benefit from being able to easily manage and -publish content, and the Contentful API makes this possible. So if you're -looking to build a content-rich application, consider using the Contentful API. +The Contentful GraphQL Content API opens up a world of possibilities for creating, managing, and delivering content across multiple platforms. With this API, you can query your Contentful content model using GraphQL, allowing for more efficient data retrieval with fewer requests. Integrate this with Pipedream's serverless capabilities, and you've got a powerful tool to automate content workflows, sync content across applications, trigger notifications based on content changes, and more. + +# Example Use Cases + +- **Content Sync Across Platforms**: Automatically sync new blog posts from Contentful to your website, social media, and email newsletter platforms. When Contentful signals a new post via a webhook, Pipedream can handle the distribution, ensuring content consistency and timely updates across all channels. + +- **Automated Content Backup**: Set up a workflow to periodically query your Contentful data using the GraphQL Content API and back up the results to a cloud storage provider like Google Drive or Dropbox. By leveraging Pipedream's scheduled triggers, you can ensure your content is safely archived on a regular basis. + +- **Dynamic Content Update Notifications**: Create a workflow where updates to Contentful entries trigger notifications. For instance, when an author updates a content piece, Pipedream can use the API to fetch the changes and send an alert through Slack or email to the relevant stakeholders, maintaining an up-to-date collaborative environment. diff --git a/components/contentful_graphql/package.json b/components/contentful_graphql/package.json new file mode 100644 index 0000000000000..fafc2215b94d1 --- /dev/null +++ b/components/contentful_graphql/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/contentful_graphql", + "version": "0.6.0", + "description": "Pipedream contentful_graphql Components", + "main": "contentful_graphql.app.mjs", + "keywords": [ + "pipedream", + "contentful_graphql" + ], + "homepage": "https://pipedream.com/apps/contentful_graphql", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/contentgroove/README.md b/components/contentgroove/README.md new file mode 100644 index 0000000000000..2a25b15fca02a --- /dev/null +++ b/components/contentgroove/README.md @@ -0,0 +1,11 @@ +# Overview + +The ContentGroove API offers functionality for managing and automating content-related tasks. Within Pipedream, you can harness this API to create workflows that trigger on various events, process data, and connect to other services. Think about automating content dissemination, analyzing audience engagement, or syncing content across platforms. With Pipedream's serverless architecture, these workflows can run on demand, on a schedule, or in response to specific events, providing a powerful way to streamline your content management processes. + +# Example Use Cases + +- **Content Distribution Automation**: Automate the distribution of new content. When ContentGroove publishes a new article, use Pipedream to post a summary and link to multiple social media platforms like Twitter, LinkedIn, and Facebook using their respective APIs. + +- **Engagement Data Aggregation**: Collect engagement data on your content. Set up a workflow on Pipedream that, upon receiving a webhook from ContentGroove about new engagement stats, aggregates this data and stores it in a Google Sheet for analysis and reporting. + +- **Content Sync Across Platforms**: Keep your content aligned across different platforms. Create a Pipedream workflow that triggers when ContentGroove updates content. The workflow can then update that same content on other CMS platforms like WordPress or Drupal using their APIs. diff --git a/components/contentstack/actions/common/entries.mjs b/components/contentstack/actions/common/entries.mjs new file mode 100644 index 0000000000000..275315575ffa1 --- /dev/null +++ b/components/contentstack/actions/common/entries.mjs @@ -0,0 +1,127 @@ +import contentstack from "../../contentstack.app.mjs"; +import { + parseArray, parseEntry, +} from "../../common/utils.mjs"; + +const createDocLink = "https://www.contentstack.com/docs/developers/apis/content-management-api#create-an-entry"; +const updateDocLink = "https://www.contentstack.com/docs/developers/apis/content-management-api#update-an-entry"; + +export default { + props: { + contentstack, + contentType: { + propDefinition: [ + contentstack, + "contentType", + ], + reloadProps: true, + }, + }, + async additionalProps() { + if (!this.contentType) { + return {}; + } + try { + return await this.buildFieldProps(this.contentType); + } catch { + return { + entryObj: { + type: "object", + label: "Entry", + description: `Enter the entry object as JSON. [See the documentation](${this.isUpdate() + ? updateDocLink + : createDocLink}) for more information.`, + }, + }; + } + }, + methods: { + getType(field) { + if (field.data_type === "boolean") { + return "boolean"; + } + if (field.data_type === "json") { + return "object"; + } + return field.multiple + ? "string[]" + : "string"; + }, + isUpdate() { + return false; + }, + async getOptions(field) { + if (field.data_type === "reference") { + const referenceContentType = field.reference_to[0]; + const { entries } = await this.contentstack.listEntries({ + contentType: referenceContentType, + }); + return entries?.map(({ + uid: value, title: label, + }) => ({ + value, + label: label ?? value, + })) || []; + } + if (field.data_type === "file") { + const { assets } = await this.contentstack.listAssets(); + return assets?.map(({ + uid: value, title: label, + }) => ({ + value, + label: label ?? value, + })) || []; + } + return undefined; + }, + async buildFieldProps(contentType) { + const props = {}; + const { content_type: { schema } } = await this.contentstack.getContentType({ + contentType, + }); + for (const field of schema) { + props[field.uid] = { + type: this.getType(field), + label: field.display_name ?? field.uid, + description: `Value of field ${field.display_name}. Field type: \`${field.data_type}\``, + optional: this.isUpdate() + ? true + : !field.mandatory, + options: await this.getOptions(field), + }; + } + return props; + }, + async buildEntry() { + if (this.entryObj) { + return parseEntry(this.entryObj); + } + const { content_type: { schema } } = await this.contentstack.getContentType({ + contentType: this.contentType, + }); + const entry = {}; + for (const field of schema) { + if (!this[field.uid]) { + continue; + } + if (field.data_type === "reference") { + if (field.multiple) { + const referenceField = parseArray(this[field.uid]); + entry[field.uid] = referenceField?.map((value) => ({ + uid: value, + _content_type_uid: field.reference_to[0], + })); + } else { + entry[field.uid] = { + uid: this[field.uid], + _content_type_uid: field.reference_to[0], + }; + } + continue; + } + entry[field.uid] = this[field.uid]; + } + return parseEntry(entry); + }, + }, +}; diff --git a/components/contentstack/actions/create-entry/create-entry.mjs b/components/contentstack/actions/create-entry/create-entry.mjs new file mode 100644 index 0000000000000..3baa5263bc97c --- /dev/null +++ b/components/contentstack/actions/create-entry/create-entry.mjs @@ -0,0 +1,33 @@ +import common from "../common/entries.mjs"; + +export default { + ...common, + key: "contentstack-create-entry", + name: "Create Entry", + description: "Creates a new entry in Contentstack. [See the documentation](https://www.contentstack.com/docs/developers/apis/content-management-api#create-an-entry).", + version: "0.0.1", + type: "action", + props: { + ...common.props, + locale: { + propDefinition: [ + common.props.contentstack, + "locale", + ], + }, + }, + async run({ $ }) { + const response = await this.contentstack.createEntry({ + $, + contentType: this.contentType, + params: { + locale: this.locale, + }, + data: { + entry: await this.buildEntry(), + }, + }); + $.export("$summary", `Created entry "${response.entry.title}" with UID ${response.entry.uid}`); + return response; + }, +}; diff --git a/components/contentstack/actions/publish-entry/publish-entry.mjs b/components/contentstack/actions/publish-entry/publish-entry.mjs new file mode 100644 index 0000000000000..cb4d954e8e100 --- /dev/null +++ b/components/contentstack/actions/publish-entry/publish-entry.mjs @@ -0,0 +1,73 @@ +import contentstack from "../../contentstack.app.mjs"; +import { parseArray } from "../../common/utils.mjs"; + +export default { + key: "contentstack-publish-entry", + name: "Publish Entry", + description: "Publishes a specific entry using its UID. [See the documentation](https://www.contentstack.com/docs/developers/apis/content-management-api#publish-entry)", + version: "0.0.1", + type: "action", + props: { + contentstack, + contentType: { + propDefinition: [ + contentstack, + "contentType", + ], + }, + entryId: { + propDefinition: [ + contentstack, + "entryId", + (c) => ({ + contentType: c.contentType, + }), + ], + }, + environments: { + propDefinition: [ + contentstack, + "environments", + ], + }, + locales: { + propDefinition: [ + contentstack, + "locale", + ], + type: "string[]", + label: "Locale", + description: "The code of the language in which you want your entry to be localized in", + }, + scheduledAt: { + type: "string", + label: "Scheduled At", + description: "The date/time in the ISO format to publish the entry. Example: `2016-10-07T12:34:36.000Z`", + optional: true, + }, + }, + async run({ $ }) { + const { entry } = await this.contentstack.getEntry({ + $, + contentType: this.contentType, + entryId: this.entryId, + }); + + const response = await this.contentstack.publishEntry({ + $, + contentType: this.contentType, + entryId: this.entryId, + data: { + entry: { + environments: parseArray(this.environments), + locales: parseArray(this.locales), + }, + locale: entry.locale, + version: entry._version, + scheduled_at: this.scheduledAt, + }, + }); + $.export("$summary", `Successfully published entry with UID ${this.entryId}`); + return response; + }, +}; diff --git a/components/contentstack/actions/update-entry/update-entry.mjs b/components/contentstack/actions/update-entry/update-entry.mjs new file mode 100644 index 0000000000000..66c3e50450e9d --- /dev/null +++ b/components/contentstack/actions/update-entry/update-entry.mjs @@ -0,0 +1,50 @@ +import common from "../common/entries.mjs"; + +export default { + ...common, + key: "contentstack-update-entry", + name: "Update Entry", + description: "Updates an existing Contentstack entry. [See the documentation](https://www.contentstack.com/docs/developers/apis/content-management-api#update-an-entry).", + version: "0.0.1", + type: "action", + props: { + ...common.props, + entryId: { + propDefinition: [ + common.props.contentstack, + "entryId", + (c) => ({ + contentType: c.contentType, + }), + ], + }, + locale: { + propDefinition: [ + common.props.contentstack, + "locale", + ], + optional: true, + }, + }, + methods: { + ...common.methods, + isUpdate() { + return true; + }, + }, + async run({ $ }) { + const response = await this.contentstack.updateEntry({ + $, + contentType: this.contentType, + entryId: this.entryId, + params: { + locale: this.locale, + }, + data: { + entry: await this.buildEntry(), + }, + }); + $.export("$summary", `Entry ${this.entryId} updated successfully`); + return response; + }, +}; diff --git a/components/contentstack/common/utils.mjs b/components/contentstack/common/utils.mjs new file mode 100644 index 0000000000000..079fd29439720 --- /dev/null +++ b/components/contentstack/common/utils.mjs @@ -0,0 +1,42 @@ +export function parseArray(value) { + if (!value) { + return undefined; + } + if (typeof value === "string") { + try { + return JSON.parse(value); + } catch { + throw new Error(`Could not parse as array: ${value}`); + } + } + return value; +} + +export function parseEntry(entry) { + if (!entry) { + return undefined; + } + if (typeof entry === "string") { + try { + return JSON.parse(entry); + } catch { + throw new Error("Could not parse entry as JSON"); + } + } + const parsedEntry = {}; + for (const [ + key, + value, + ] of Object.entries(entry)) { + if (typeof value === "string") { + try { + parsedEntry[key] = JSON.parse(value); + } catch { + parsedEntry[key] = value; + } + } else { + parsedEntry[key] = value; + } + } + return parsedEntry; +} diff --git a/components/contentstack/contentstack.app.mjs b/components/contentstack/contentstack.app.mjs new file mode 100644 index 0000000000000..cda7523cc87ae --- /dev/null +++ b/components/contentstack/contentstack.app.mjs @@ -0,0 +1,201 @@ +import { axios } from "@pipedream/platform"; +const DEFAULT_LIMIT = 20; + +export default { + type: "app", + app: "contentstack", + propDefinitions: { + branchIds: { + type: "string[]", + label: "Branches", + description: "An array of branch identifiers", + async options({ page }) { + const { branches } = await this.listBranches({ + params: { + limit: DEFAULT_LIMIT, + skip: page * DEFAULT_LIMIT, + }, + }); + return branches?.map(({ uid }) => uid) || []; + }, + }, + contentType: { + type: "string", + label: "Content Type", + description: "The UID of the content type for creating and listing entries", + async options() { + const { content_types: contentTypes } = await this.listContentTypes(); + return contentTypes?.map(({ + uid: value, title: label, + }) => ({ + value, + label, + })) || []; + }, + }, + entryId: { + type: "string", + label: "Entry ID", + description: "The UID of the entry to publish or update", + async options({ contentType }) { + const { entries } = await this.listEntries({ + contentType, + }); + return entries?.map(({ + uid: value, title: label, + }) => ({ + value, + label, + })) || []; + }, + }, + environments: { + type: "string[]", + label: "Environments", + description: "The UIDs of the environments to which you want to publish the entry", + async options() { + const { environments } = await this.listEnvironments(); + return environments?.map(({ name }) => name ) || []; + }, + }, + locale: { + type: "string", + label: "Locale", + description: "The code of the language in which you want your entry to be localized in", + async options() { + const { locales } = await this.listLocales(); + return locales?.map(({ + code: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return `${this.$auth.region}api.contentstack.${this.$auth.region === "https://" + ? "io" + : "com"}/v3`; + }, + _makeRequest({ + $ = this, + path, + ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "api_key": `${this.$auth.stack_api_key}`, + "authorization": `${this.$auth.management_token}`, + "content-type": "application/json", + }, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteWebhook({ + webhookId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + ...opts, + }); + }, + listBranches(opts = {}) { + return this._makeRequest({ + path: "/stacks/branches", + ...opts, + }); + }, + listContentTypes(opts = {}) { + return this._makeRequest({ + path: "/content_types", + ...opts, + }); + }, + listEntries({ + contentType, ...opts + }) { + return this._makeRequest({ + path: `/content_types/${contentType}/entries`, + ...opts, + }); + }, + listEnvironments(opts = {}) { + return this._makeRequest({ + path: "/environments", + ...opts, + }); + }, + listLocales(opts = {}) { + return this._makeRequest({ + path: "/locales", + ...opts, + }); + }, + getEntry({ + contentType, entryId, ...opts + }) { + return this._makeRequest({ + path: `/content_types/${contentType}/entries/${entryId}`, + ...opts, + }); + }, + getContentType({ + contentType, ...opts + }) { + return this._makeRequest({ + path: `/content_types/${contentType}`, + ...opts, + }); + }, + createEntry({ + contentType, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/content_types/${contentType}/entries`, + ...opts, + }); + }, + updateEntry({ + contentType, entryId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/content_types/${contentType}/entries/${entryId}`, + ...opts, + }); + }, + publishEntry({ + contentType, entryId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/content_types/${contentType}/entries/${entryId}/publish`, + ...opts, + }); + }, + listAssets(opts = {}) { + return this._makeRequest({ + path: "/assets", + ...opts, + }); + }, + async getAsset(opts = {}) { + return this._makeRequest({ + path: `/stacks/${this.stackId}/assets/${this.assetId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/contentstack/package.json b/components/contentstack/package.json new file mode 100644 index 0000000000000..38810b88c51d5 --- /dev/null +++ b/components/contentstack/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/contentstack", + "version": "0.1.0", + "description": "Pipedream Contentstack Components", + "main": "contentstack.app.mjs", + "keywords": [ + "pipedream", + "contentstack" + ], + "homepage": "https://pipedream.com/apps/contentstack", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/contentstack/sources/common/base.mjs b/components/contentstack/sources/common/base.mjs new file mode 100644 index 0000000000000..a683ebae1c8c7 --- /dev/null +++ b/components/contentstack/sources/common/base.mjs @@ -0,0 +1,76 @@ +import contentstack from "../../contentstack.app.mjs"; + +export default { + props: { + contentstack, + db: "$.service.db", + http: "$.interface.http", + name: { + type: "string", + label: "Webhook Name", + description: "Name of the webhook", + }, + branchIds: { + propDefinition: [ + contentstack, + "branchIds", + ], + }, + }, + hooks: { + async activate() { + const { + webhook, notice, + } = await this.contentstack.createWebhook({ + data: { + webhook: { + name: this.name, + destinations: [ + { + target_url: this.http.endpoint, + authentication_type: "None", + }, + ], + channels: this.getChannels(), + branches: this.branchIds, + disabled: false, + concise_payload: false, + retry_policy: "automatic", + }, + }, + }); + console.log(notice); + this._setHookId(webhook.uid); + }, + async deactivate() { + const webhookId = this._getHookId(); + if (webhookId) { + await this.contentstack.deleteWebhook({ + webhookId, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getChannels() { + throw new Error("getChannels is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run(event) { + const { body } = event; + if (!body) { + return; + } + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, +}; diff --git a/components/contentstack/sources/new-asset-instant/new-asset-instant.mjs b/components/contentstack/sources/new-asset-instant/new-asset-instant.mjs new file mode 100644 index 0000000000000..e95f8e0063fc3 --- /dev/null +++ b/components/contentstack/sources/new-asset-instant/new-asset-instant.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "contentstack-new-asset-instant", + name: "New Asset Created (Instant)", + description: "Emit new event when a new asset is created in ContentStack.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getChannels() { + return [ + "assets.create", + ]; + }, + generateMeta(event) { + const id = event.data.asset.uid; + return { + id, + summary: `New asset created with ID: ${id}`, + ts: Date.parse(event.triggered_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/contentstack/sources/new-asset-instant/test-event.mjs b/components/contentstack/sources/new-asset-instant/test-event.mjs new file mode 100644 index 0000000000000..e69a7a20a3f15 --- /dev/null +++ b/components/contentstack/sources/new-asset-instant/test-event.mjs @@ -0,0 +1,30 @@ +export default { + "module": "asset", + "api_key": "blt6e39cdfaac74824c", + "data": { + "asset": { + "uid": "blt81f45c9d5c3103de", + "created_at": "2024-12-19T20:55:53.701Z", + "updated_at": "2024-12-19T20:55:53.701Z", + "created_by": "cs52122cc4ac22f20e", + "updated_by": "cs52122cc4ac22f20e", + "content_type": "image/jpeg", + "file_size": "463713", + "tags": [], + "filename": "george.jpg", + "url": "https://images.contentstack.io/v3/assets/blt6e39cdfaac74824c/blt81f45c9d5c3103de/67648859989c5367eed4ffdb/george.jpg", + "ACL": {}, + "is_dir": false, + "parent_uid": null, + "_version": 1, + "title": "george.jpg" + }, + "branch": { + "uid": "main", + "source": "", + "alias": [] + } + }, + "event": "create", + "triggered_at": "2024-12-19T20:55:53.977Z" +} \ No newline at end of file diff --git a/components/contentstack/sources/new-entry-instant/new-entry-instant.mjs b/components/contentstack/sources/new-entry-instant/new-entry-instant.mjs new file mode 100644 index 0000000000000..64cda26665589 --- /dev/null +++ b/components/contentstack/sources/new-entry-instant/new-entry-instant.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "contentstack-new-entry-instant", + name: "New Entry Created (Instant)", + description: "Emit new event when a new entry is created in ContentStack.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getChannels() { + return [ + "content_types.entries.create", + ]; + }, + generateMeta(event) { + const id = event.data.entry.uid; + return { + id, + summary: `New entity created with ID: ${id}`, + ts: Date.parse(event.triggered_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/contentstack/sources/new-entry-instant/test-event.mjs b/components/contentstack/sources/new-entry-instant/test-event.mjs new file mode 100644 index 0000000000000..4db3414817b50 --- /dev/null +++ b/components/contentstack/sources/new-entry-instant/test-event.mjs @@ -0,0 +1,496 @@ +export default { + "module": "entry", + "api_key": "blt6e39cdfaac74824c", + "data": { + "entry": { + "title": "test article", + "url": "/article/test-article", + "cover_image": { + "uid": "blt81f45c9d5c3103de", + "created_at": "2024-12-19T20:55:53.701Z", + "updated_at": "2024-12-19T20:55:53.701Z", + "created_by": "cs52122cc4ac22f20e", + "updated_by": "cs52122cc4ac22f20e", + "content_type": "image/jpeg", + "file_size": "463713", + "tags": [], + "filename": "george.jpg", + "url": "https://images.contentstack.io/v3/assets/blt6e39cdfaac74824c/blt81f45c9d5c3103de/67648859989c5367eed4ffdb/george.jpg", + "ACL": [], + "is_dir": false, + "parent_uid": null, + "_version": 1, + "title": "george.jpg" + }, + "summary": "Summary", + "taxonomies": [], + "content": { + "type": "doc", + "attrs": {}, + "uid": "d05b26fe64214bca883e5e31eb11090c", + "children": [ + { + "type": "p", + "attrs": {}, + "uid": "5a4f78e3731748d284c83d93ecd794a3", + "children": [ + { + "text": "" + } + ] + } + ], + "_version": 1 + }, + "show_related_links": false, + "related_links": { + "text": "Related Links" + }, + "show_related_articles": false, + "related_articles": { + "heading": "Related Headline", + "sub_heading": "Related Subhead", + "number_of_articles": 6 + }, + "seo": { + "title": "Title", + "description": "Description", + "canonical_url": "/", + "no_index": true, + "no_follow": true + }, + "tags": [], + "locale": "en", + "uid": "blt6b0da76c39f34851", + "created_by": "cs52122cc4ac22f20e", + "updated_by": "cs52122cc4ac22f20e", + "created_at": "2024-12-19T21:29:49.303Z", + "updated_at": "2024-12-19T21:29:49.303Z", + "ACL": {}, + "_version": 1, + "_in_progress": false + }, + "content_type": { + "created_at": "2024-11-30T03:52:14.855Z", + "created_by": "bltce9401ba486d3c23", + "updated_at": "2024-11-30T03:54:29.605Z", + "updated_by": "bltce9401ba486d3c23", + "title": "Article", + "uid": "article", + "description": "", + "schema": [ + { + "data_type": "text", + "display_name": "Title", + "field_metadata": { + "_default": true, + "placeholder": "Title", + "version": 3 + }, + "mandatory": true, + "uid": "title", + "unique": true, + "multiple": false, + "non_localizable": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "text", + "display_name": "URL", + "uid": "url", + "field_metadata": { + "_default": true, + "version": 3 + }, + "multiple": false, + "unique": false, + "mandatory": false, + "non_localizable": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "file", + "display_name": "Cover image", + "uid": "cover_image", + "field_metadata": { + "description": "", + "rich_text_type": "standard", + "image": true + }, + "mandatory": true, + "multiple": false, + "non_localizable": false, + "unique": false, + "dimension": { + "width": { + "min": null, + "max": null + }, + "height": { + "min": null, + "max": null + } + }, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "text", + "display_name": "Summary", + "uid": "summary", + "field_metadata": { + "description": "", + "default_value": "Summary", + "multiline": true, + "placeholder": "Summary", + "version": 3 + }, + "format": "", + "error_messages": { + "format": "" + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "taxonomy", + "display_name": "Taxonomy", + "uid": "taxonomies", + "taxonomies": [ + { + "taxonomy_uid": "region", + "mandatory": false, + "multiple": true, + "non_localizable": false + }, + { + "taxonomy_uid": "topic", + "mandatory": false, + "multiple": true, + "non_localizable": false + } + ], + "field_metadata": { + "description": "", + "default_value": "" + }, + "format": "", + "error_messages": { + "format": "" + }, + "mandatory": false, + "multiple": true, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "json", + "display_name": "Content", + "uid": "content", + "field_metadata": { + "allow_json_rte": true, + "embed_entry": false, + "description": "", + "default_value": "", + "multiline": false, + "rich_text_type": "advanced", + "options": [] + }, + "format": "", + "error_messages": { + "format": "" + }, + "reference_to": [ + "sys_assets" + ], + "multiple": false, + "non_localizable": false, + "unique": false, + "mandatory": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "boolean", + "display_name": "Show Related Links", + "uid": "show_related_links", + "field_metadata": { + "description": "", + "default_value": false + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "group", + "display_name": "Related Links", + "field_metadata": { + "description": "", + "instruction": "" + }, + "schema": [ + { + "data_type": "text", + "display_name": "Text", + "uid": "text", + "field_metadata": { + "description": "", + "default_value": "Related Links", + "placeholder": "Text", + "version": 3 + }, + "format": "", + "error_messages": { + "format": "" + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + } + ], + "uid": "related_links", + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "boolean", + "display_name": "Show related articles", + "uid": "show_related_articles", + "field_metadata": { + "description": "", + "default_value": false + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "group", + "display_name": "Related articles", + "field_metadata": { + "description": "", + "instruction": "" + }, + "schema": [ + { + "data_type": "text", + "display_name": "Heading", + "uid": "heading", + "field_metadata": { + "description": "", + "default_value": "Related Headline", + "placeholder": "Related Articles", + "version": 3 + }, + "format": "", + "error_messages": { + "format": "" + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "text", + "display_name": "Sub heading", + "uid": "sub_heading", + "field_metadata": { + "description": "", + "default_value": "Related Subhead", + "multiline": true, + "placeholder": "Related Subheading", + "version": 3 + }, + "format": "", + "error_messages": { + "format": "" + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "number", + "display_name": "Number of articles", + "uid": "number_of_articles", + "field_metadata": { + "description": "", + "default_value": 6, + "placeholder": "Number of articles" + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "min": 1, + "max": 6, + "indexed": false, + "inbuilt_model": false + } + ], + "uid": "related_articles", + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "global_field", + "display_name": "SEO", + "reference_to": "seo", + "field_metadata": { + "description": "" + }, + "uid": "seo", + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false, + "schema": [ + { + "data_type": "text", + "display_name": "Title", + "uid": "title", + "field_metadata": { + "description": "Please add the SEO title of the page.", + "default_value": "Title", + "placeholder": "Title", + "version": 3 + }, + "format": "", + "error_messages": { + "format": "" + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "text", + "display_name": "Description", + "uid": "description", + "field_metadata": { + "description": "Please enter the SEO description of the page.", + "default_value": "Description", + "multiline": true, + "placeholder": "Description", + "version": 3 + }, + "format": "", + "error_messages": { + "format": "" + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "text", + "display_name": "Canonical URL", + "uid": "canonical_url", + "field_metadata": { + "description": "Please add the canonical url of the page.", + "default_value": "/", + "placeholder": "Canonical URL", + "version": 3 + }, + "format": "", + "error_messages": { + "format": "" + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "boolean", + "display_name": "No index", + "uid": "no_index", + "field_metadata": { + "description": "Please check if the value is no index", + "default_value": true + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "boolean", + "display_name": "No follow", + "uid": "no_follow", + "field_metadata": { + "description": "Please check if the value is no follow.", + "default_value": true + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + } + ] + } + ], + "options": { + "is_page": true, + "singleton": false, + "sub_title": [], + "title": "title", + "url_pattern": "/:title", + "url_prefix": "/article/" + } + }, + "branch": { + "uid": "main", + "source": "", + "alias": [] + } + }, + "event": "create", + "triggered_at": "2024-12-19T21:29:49.441Z" +} \ No newline at end of file diff --git a/components/contentstack/sources/publish-entry-instant/publish-entry-instant.mjs b/components/contentstack/sources/publish-entry-instant/publish-entry-instant.mjs new file mode 100644 index 0000000000000..16529f65edeb1 --- /dev/null +++ b/components/contentstack/sources/publish-entry-instant/publish-entry-instant.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "contentstack-publish-entry-instant", + name: "New Entry Published (Instant)", + description: "Emit new event when an entry is published in ContentStack.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getChannels() { + return [ + "content_types.entries.publish", + ]; + }, + generateMeta(event) { + const id = event.data.entry.uid; + return { + id, + summary: `New entry published with ID: ${id}`, + ts: Date.parse(event.triggered_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/contentstack/sources/publish-entry-instant/test-event.mjs b/components/contentstack/sources/publish-entry-instant/test-event.mjs new file mode 100644 index 0000000000000..ad7581313d029 --- /dev/null +++ b/components/contentstack/sources/publish-entry-instant/test-event.mjs @@ -0,0 +1,452 @@ +export default { + "module": "entry", + "api_key": "blt6e39cdfaac74824c", + "event": "publish", + "bulk": true, + "data": { + "locale": "en", + "status": "success", + "action": "publish", + "entry": { + "_version": 1, + "deleted_at": false, + "locale": "en", + "uid": "blt6b0da76c39f34851", + "_in_progress": false, + "content": { + "type": "doc", + "attrs": {}, + "uid": "d05b26fe64214bca883e5e31eb11090c", + "children": [ + { + "type": "p", + "attrs": {}, + "uid": "5a4f78e3731748d284c83d93ecd794a3", + "children": [ + { + "text": "" + } + ] + } + ], + "_version": 1 + }, + "cover_image": { + "parent_uid": null, + "title": "george.jpg", + "uid": "blt81f45c9d5c3103de", + "created_by": "cs52122cc4ac22f20e", + "updated_by": "cs52122cc4ac22f20e", + "created_at": "2024-12-19T20:55:53.701Z", + "updated_at": "2024-12-19T20:55:53.701Z", + "deleted_at": false, + "content_type": "image/jpeg", + "file_size": "463713", + "filename": "george.jpg", + "dimension": { + "height": 1548, + "width": 1353 + }, + "_version": 1, + "is_dir": false, + "tags": [], + "url": "https://images.contentstack.io/v3/assets/blt6e39cdfaac74824c/blt81f45c9d5c3103de/67648859989c5367eed4ffdb/george.jpg" + }, + "created_at": "2024-12-19T21:29:49.303Z", + "created_by": "cs52122cc4ac22f20e", + "related_articles": { + "heading": "Related Headline", + "sub_heading": "Related Subhead", + "number_of_articles": 6 + }, + "related_links": { + "text": "Related Links" + }, + "seo": { + "title": "Title", + "description": "Description", + "canonical_url": "/", + "no_index": true, + "no_follow": true + }, + "show_related_articles": false, + "show_related_links": false, + "summary": "Summary", + "tags": [], + "taxonomies": [], + "title": "test article", + "updated_at": "2024-12-19T21:29:49.303Z", + "updated_by": "cs52122cc4ac22f20e", + "url": "/article/test-article", + "publish_details": { + "environment": "blt78bb9fab298c6744", + "locale": "en", + "time": "2024-12-19T21:30:01.252Z", + "user": "cs52122cc4ac22f20e" + } + }, + "content_type": { + "title": "Article", + "uid": "article", + "schema": [ + { + "data_type": "text", + "display_name": "Title", + "field_metadata": { + "_default": true, + "placeholder": "Title", + "version": 3 + }, + "mandatory": true, + "uid": "title", + "unique": true, + "multiple": false, + "non_localizable": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "text", + "display_name": "URL", + "uid": "url", + "field_metadata": { + "_default": true, + "version": 3 + }, + "multiple": false, + "unique": false, + "mandatory": false, + "non_localizable": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "file", + "display_name": "Cover image", + "uid": "cover_image", + "field_metadata": { + "description": "", + "rich_text_type": "standard", + "image": true + }, + "mandatory": true, + "multiple": false, + "non_localizable": false, + "unique": false, + "dimension": { + "width": { + "min": null, + "max": null + }, + "height": { + "min": null, + "max": null + } + }, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "text", + "display_name": "Summary", + "uid": "summary", + "field_metadata": { + "description": "", + "default_value": "Summary", + "multiline": true, + "placeholder": "Summary", + "version": 3 + }, + "format": "", + "error_messages": { + "format": "" + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "taxonomy", + "display_name": "Taxonomy", + "uid": "taxonomies", + "taxonomies": [ + { + "taxonomy_uid": "region", + "mandatory": false, + "multiple": true, + "non_localizable": false + }, + { + "taxonomy_uid": "topic", + "mandatory": false, + "multiple": true, + "non_localizable": false + } + ], + "field_metadata": { + "description": "", + "default_value": "" + }, + "format": "", + "error_messages": { + "format": "" + }, + "mandatory": false, + "multiple": true, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "json", + "display_name": "Content", + "uid": "content", + "field_metadata": { + "allow_json_rte": true, + "embed_entry": false, + "description": "", + "default_value": "", + "multiline": false, + "rich_text_type": "advanced", + "options": [] + }, + "format": "", + "error_messages": { + "format": "" + }, + "reference_to": [ + "sys_assets" + ], + "multiple": false, + "non_localizable": false, + "unique": false, + "mandatory": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "boolean", + "display_name": "Show Related Links", + "uid": "show_related_links", + "field_metadata": { + "description": "", + "default_value": false + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "group", + "display_name": "Related Links", + "field_metadata": { + "description": "", + "instruction": "" + }, + "schema": [ + { + "data_type": "text", + "display_name": "Text", + "uid": "text", + "field_metadata": { + "description": "", + "default_value": "Related Links", + "placeholder": "Text", + "version": 3 + }, + "format": "", + "error_messages": { + "format": "" + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + } + ], + "uid": "related_links", + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "boolean", + "display_name": "Show related articles", + "uid": "show_related_articles", + "field_metadata": { + "description": "", + "default_value": false + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "group", + "display_name": "Related articles", + "field_metadata": { + "description": "", + "instruction": "" + }, + "schema": [ + { + "data_type": "text", + "display_name": "Heading", + "uid": "heading", + "field_metadata": { + "description": "", + "default_value": "Related Headline", + "placeholder": "Related Articles", + "version": 3 + }, + "format": "", + "error_messages": { + "format": "" + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "text", + "display_name": "Sub heading", + "uid": "sub_heading", + "field_metadata": { + "description": "", + "default_value": "Related Subhead", + "multiline": true, + "placeholder": "Related Subheading", + "version": 3 + }, + "format": "", + "error_messages": { + "format": "" + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "number", + "display_name": "Number of articles", + "uid": "number_of_articles", + "field_metadata": { + "description": "", + "default_value": 6, + "placeholder": "Number of articles" + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "min": 1, + "max": 6, + "indexed": false, + "inbuilt_model": false + } + ], + "uid": "related_articles", + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + }, + { + "data_type": "global_field", + "display_name": "SEO", + "reference_to": "seo", + "field_metadata": { + "description": "" + }, + "uid": "seo", + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + } + ], + "options": { + "is_page": true, + "singleton": false, + "sub_title": [], + "title": "title", + "url_pattern": "/:title", + "url_prefix": "/article/" + }, + "created_by": "bltce9401ba486d3c23", + "updated_by": "bltce9401ba486d3c23", + "created_at": "2024-11-30T03:52:14.855Z", + "updated_at": "2024-11-30T03:54:29.605Z", + "deleted_at": false, + "description": "", + "_version": 5, + "field_rules": [] + }, + "environment": { + "urls": [ + { + "locale": "en", + "url": "https://compass-starter---blt6e39cdfaac-production.contentstackapps.com/en" + }, + { + "locale": "fr", + "url": "https://compass-starter---blt6e39cdfaac-production.contentstackapps.com/fr" + }, + { + "locale": "de", + "url": "https://compass-starter---blt6e39cdfaac-production.contentstackapps.com/de" + }, + { + "locale": "es", + "url": "https://compass-starter---blt6e39cdfaac-production.contentstackapps.com/es" + } + ], + "name": "production", + "_version": 2, + "api_key": "blt6e39cdfaac74824c", + "org_uid": "blte5beeb2edad1d61a", + "uid": "blt78bb9fab298c6744", + "created_by": "bltce9401ba486d3c23", + "updated_by": "bltce9401ba486d3c23", + "created_at": "2024-11-30T03:50:51.656Z", + "updated_at": "2024-11-30T03:58:41.754Z" + }, + "branch": { + "api_key": "blt6e39cdfaac74824c", + "org_uid": "blte5beeb2edad1d61a", + "uid": "main", + "source": "", + "created_by": "bltce9401ba486d3c23", + "updated_by": "bltce9401ba486d3c23", + "created_at": "2024-11-30T03:50:41.195Z", + "updated_at": "2024-11-30T03:50:41.195Z", + "alias": [] + } + }, + "triggered_at": "2024-12-19T21:30:01.521Z" +} \ No newline at end of file diff --git a/components/control_d/README.md b/components/control_d/README.md new file mode 100644 index 0000000000000..78fc49676604f --- /dev/null +++ b/components/control_d/README.md @@ -0,0 +1,14 @@ +# Overview + +The Control D API allows you to manage and monitor internet access across different devices and networks. With it, you can automate the control of browsing data, enforce internet access policies, and generate detailed reports on internet usage. When integrated with Pipedream, these capabilities can be extended to automate workflows involving data from other apps, trigger actions based on internet usage patterns, and much more, leveraging Pipedream's capability to connect with hundreds of other apps and services. + +# Example Use Cases + +- **Automate Content Filtering Based on Time of Day** + Use the Control D API on Pipedream to dynamically change filtering rules based on the time of day. For instance, more restrictive rules could be applied during work hours and relaxed during off-hours, automatically. Connect with Google Calendar API to adjust internet access policies based on company-wide events or meetings. + +- **Alerts for Unusual Activity** + Set up a workflow where the Control D API monitors internet traffic and uses Pipedream's integrated email service to send alerts when there is unusual activity, such as access to risky domains. This can be crucial for IT security in monitoring potential cyber threats in realtime. + +- **Sync Internet Usage Stats with Analytics Tools** + Integrate Control D with analytics tools like Google Sheets or Microsoft Power BI through Pipedream. Automatically push detailed logs and usage statistics from Control D to these platforms for advanced analysis and visualization, enabling data-driven decisions regarding network management and policy adjustments. diff --git a/components/control_d/actions/create-device/create-device.mjs b/components/control_d/actions/create-device/create-device.mjs new file mode 100644 index 0000000000000..fcc7e1008cf74 --- /dev/null +++ b/components/control_d/actions/create-device/create-device.mjs @@ -0,0 +1,44 @@ +import app from "../../control_d.app.mjs"; + +export default { + key: "control_d-create-device", + name: "Create Device", + description: "Create a new device. [See the documentation](https://docs.controld.com/reference/post_devices)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "name", + ], + }, + profileId: { + propDefinition: [ + app, + "profileId", + ], + }, + icon: { + propDefinition: [ + app, + "icon", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createDevice({ + $, + data: { + name: this.name, + profile_id: this.profileId, + icon: this.icon, + }, + }); + + $.export("$summary", `Successfully created the device with the ID ${response.body.device_id}`); + + return response; + }, +}; diff --git a/components/control_d/actions/create-profile/create-profile.mjs b/components/control_d/actions/create-profile/create-profile.mjs new file mode 100644 index 0000000000000..777c3230b002d --- /dev/null +++ b/components/control_d/actions/create-profile/create-profile.mjs @@ -0,0 +1,31 @@ +import app from "../../control_d.app.mjs"; + +export default { + key: "control_d-create-profile", + name: "Create Profile", + description: "Create a profile. [See the documentation](https://docs.controld.com/reference/post_profiles)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "name", + ], + description: "Profile name", + }, + }, + async run({ $ }) { + const response = await this.app.createProfile({ + $, + data: { + name: this.name, + }, + }); + + $.export("$summary", `Successfully created the profile '${this.name}'`); + + return response; + }, +}; diff --git a/components/control_d/actions/delete-device/delete-device.mjs b/components/control_d/actions/delete-device/delete-device.mjs new file mode 100644 index 0000000000000..8d93728c86a51 --- /dev/null +++ b/components/control_d/actions/delete-device/delete-device.mjs @@ -0,0 +1,28 @@ +import app from "../../control_d.app.mjs"; + +export default { + key: "control_d-delete-device", + name: "Delete Device", + description: "Delete a device with the specified ID. [See the documentation](https://docs.controld.com/reference/delete_devices-device-id)", + version: "0.0.1", + type: "action", + props: { + app, + deviceId: { + propDefinition: [ + app, + "deviceId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.deleteDevice({ + $, + device_id: this.deviceId, + }); + + $.export("$summary", `Successfully deleted the device with the ID ${this.deviceId}`); + + return response; + }, +}; diff --git a/components/control_d/control_d.app.mjs b/components/control_d/control_d.app.mjs new file mode 100644 index 0000000000000..27fdd45ad832f --- /dev/null +++ b/components/control_d/control_d.app.mjs @@ -0,0 +1,126 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "control_d", + propDefinitions: { + name: { + type: "string", + label: "Name", + description: "Device name", + }, + profileId: { + type: "string", + label: "Profile ID", + description: "Primary key of main profile to enforce on this device", + async options() { + const { body: { profiles } } = await this.getProfiles(); + + return profiles.map(({ + PK, name, + }) => ({ + value: PK, + label: name, + })); + }, + }, + deviceId: { + type: "string", + label: "Device ID", + description: "ID of the device", + async options() { + const { body: { devices: devicesIds } } = await this.getDevices(); + + return devicesIds.map(({ + PK, name, + }) => ({ + value: PK, + label: name, + })); + }, + }, + icon: { + type: "string", + label: "Icon", + description: "Device icon/type", + async options() { + const { body: { types: iconsData } } = await this.getTypes(); + + return [ + ...Object.keys(iconsData.os.icons).map((icon) => ({ + value: icon, + })), + ...Object.keys(iconsData.browser.icons).map((icon) => ({ + value: icon, + })), + ...Object.keys(iconsData.router.icons).map((icon) => ({ + value: icon, + })), + ]; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.controld.com"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "Authorization": `Bearer ${this.$auth.api_token}`, + "Accept": "application/json", + }, + }); + }, + async createDevice(args = {}) { + return this._makeRequest({ + method: "post", + path: "/devices", + ...args, + }); + }, + async createProfile(args = {}) { + return this._makeRequest({ + method: "post", + path: "/profiles", + ...args, + }); + }, + async deleteDevice({ + device_id, ...args + }) { + return this._makeRequest({ + method: "delete", + path: `/devices/${device_id}`, + ...args, + }); + }, + async getDevices(args = {}) { + return this._makeRequest({ + path: "/devices", + ...args, + }); + }, + async getProfiles(args = {}) { + return this._makeRequest({ + path: "/profiles", + ...args, + }); + }, + async getTypes(args = {}) { + return this._makeRequest({ + path: "/devices/types", + ...args, + }); + }, + }, +}; diff --git a/components/control_d/package.json b/components/control_d/package.json new file mode 100644 index 0000000000000..1a8913b07080d --- /dev/null +++ b/components/control_d/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/control_d", + "version": "0.1.0", + "description": "Pipedream Control D Components", + "main": "control_d.app.mjs", + "keywords": [ + "pipedream", + "control_d" + ], + "homepage": "https://pipedream.com/apps/control_d", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/convenia/README.md b/components/convenia/README.md new file mode 100644 index 0000000000000..1c2481cd944cf --- /dev/null +++ b/components/convenia/README.md @@ -0,0 +1,11 @@ +# Overview + +The Convenia API offers a suite of HR management functionalities, allowing for the automation of employee onboarding, payroll management, and other HR tasks. By integrating with Convenia via Pipedream, you can streamline HR operations, sync employee data across applications, and trigger actions based on various HR events. Pipedream's serverless platform facilitates the creation of complex workflows to interact with the Convenia API, enabling developers and HR professionals to build custom automations without managing infrastructure. + +# Example Use Cases + +- **Automated Employee Onboarding**: Trigger a Pipedream workflow whenever a new employee is added in Convenia. The workflow can send a welcome email, create accounts in tools like Slack or Microsoft 365, and add the employee to relevant HR and project management platforms. + +- **Payroll Change Notifications**: Set up a workflow to listen for payroll updates from Convenia. Once a change is detected, the workflow can notify the finance team via email or a messaging app like Slack, ensuring they are always aware of updates in real-time. + +- **Time-Off Management**: Implement a workflow triggered by time-off requests in Convenia. It can automatically update shared calendars and inform team members in applications like Google Calendar or Outlook, facilitating better team planning and resource allocation. diff --git a/components/conversion_tools/README.md b/components/conversion_tools/README.md new file mode 100644 index 0000000000000..6369a7ff43ebc --- /dev/null +++ b/components/conversion_tools/README.md @@ -0,0 +1,11 @@ +# Overview + +The Conversion Tools API allows for seamless file conversions between various formats, including document, image, audio, and video transformation. In Pipedream, you can use the API to automate such conversions within workflows, and handle file manipulation tasks efficiently, integrating them with hundreds of other services to create a smooth data processing pipeline. + +# Example Use Cases + +- **Automated Image Format Conversion**: Convert uploaded images from PNG to JPEG automatically whenever a file is dropped into a Dropbox folder. Use Dropbox's trigger to start the workflow, and then pass the file through Conversion Tools before saving the converted image back to Dropbox or another storage service like Google Drive. + +- **Document Translation Workflow**: Create a workflow where a PDF document is first converted to a Word file using Conversion Tools, then use a language translation API (like Google Translate) to convert the text to another language, and finally convert the translated Word document back to PDF. + +- **Audio Transcription Pipeline**: Transcribe audio files uploaded to a service like Box by first converting them from various formats to a uniform format (e.g., WAV to MP3) with Conversion Tools. Then, send the converted audio file to a transcription service like Google Speech-to-Text for processing, and save the transcribed text to a database or send it via email. diff --git a/components/conversion_tools/package.json b/components/conversion_tools/package.json index a162c715a5e16..d39310b1f5d6d 100644 --- a/components/conversion_tools/package.json +++ b/components/conversion_tools/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/convertapi/README.md b/components/convertapi/README.md new file mode 100644 index 0000000000000..04b140ad33b83 --- /dev/null +++ b/components/convertapi/README.md @@ -0,0 +1,11 @@ +# Overview + +ConvertAPI is a powerhouse for online file conversion, enabling you to transform files from one format to another effortlessly. It supports a plethora of file types, from common ones like PDFs and DOCs to more obscure formats. With ConvertAPI on Pipedream, you can automate file conversion tasks, seamlessly integrating them into workflows that trigger on events from other apps or schedules. Imagine converting incoming email attachments, processing uploaded documents, or archiving files in a different format—all happening automatically, in the background. + +# Example Use Cases + +- **Automated Document Conversion for Email Attachments**: Whenever you receive an email with an attachment in Gmail, Pipedream can catch this event, send the file to ConvertAPI to change it to your preferred format (e.g., DOCX to PDF), and then save the converted file to Google Drive. + +- **Batch Image Conversion for Cloud Storage**: Set up a Pipedream workflow that monitors a Dropbox folder for new images. When new images are detected, the workflow uses ConvertAPI to convert them to a different format (say, PNG to JPG) and then stores the converted images back in Dropbox or another storage service like AWS S3. + +- **Website Screenshot to PDF Archival**: Trigger a Pipedream workflow with a cron schedule to take a screenshot of a webpage using ConvertAPI's 'Web to PDF' function. The PDF can then be timestamped and saved to a data store or cloud service for record-keeping or later reference. diff --git a/components/convertkit/README.md b/components/convertkit/README.md index 19e85ac6aedec..4fe8edc6d4695 100644 --- a/components/convertkit/README.md +++ b/components/convertkit/README.md @@ -1,10 +1,11 @@ # Overview -With the ConvertKit API, you can build a wide variety of apps and integrations -to help you grow your business. Here are just a few examples: - -- An app that allows you to sign up for ConvertKit courses and products -- An integration that allows you to sell ConvertKit products on your website -- A tool that allows you to automatically add ConvertKit subscribers to your - CRM -- An app that allows you to send ConvertKit broadcasts to your customers +ConvertKit's API offers granular control over email marketing campaigns, allowing users to automate subscriber management, broadcast sending, and sequence setup. Utilize Pipedream's power to react to events, sync data across platforms, or create personalized, timely campaigns. Pipedream's serverless platform connects ConvertKit with hundreds of other apps, enabling automated workflows that save time and improve engagement. + +# Example Use Cases + +- **Automated Welcome Sequence for New Subscribers**: Trigger an automated email sequence in ConvertKit when a new user signs up on your platform. This could be set up using Pipedream's trigger for new database entries or sign-ups from another app like Shopify. + +- **Subscriber Segmentation Based on Behavior**: Use webhooks in Pipedream to monitor user interactions on your website or app. Based on the actions they take, add tags to subscribers in ConvertKit to segment them for more targeted follow-up emails. + +- **Sync ConvertKit Subscribers to CRM**: Keep your CRM updated by syncing new subscribers or changes in subscriber info from ConvertKit to your CRM of choice, such as Salesforce. Pipedream can automate this process, ensuring your sales team has the latest information at their fingertips. diff --git a/components/conveyor/README.md b/components/conveyor/README.md index c26df9f36c260..0f0606098b786 100644 --- a/components/conveyor/README.md +++ b/components/conveyor/README.md @@ -1,8 +1,11 @@ # Overview -With the Conveyor API, you can build a variety of applications, including: +The Conveyor API facilitates automated management of continuous delivery for your applications, allowing you to build, test, and deploy code with ease. By leveraging Pipedream's integration capabilities, you can connect the Conveyor API with a multitude of apps to streamline your CI/CD pipeline, automate deployment tasks, and synchronize your development workflow with other services such as version control, project management, and notification systems. -- A system that allows users to order and track their packages in real-time -- A tool that helps businesses optimize their shipping routes -- A mobile app that provides turn-by-turn directions for delivery drivers -- A web app that helps customers compare shipping rates from different carriers +# Example Use Cases + +- **Automate Deployment Notifications**: Use Pipedream to trigger a workflow every time Conveyor successfully deploys a new version of your application. This workflow could send a message to a Slack channel or Discord server, informing your team about the deployment details and status. + +- **Sync Deployment Status with Project Management Tools**: Create a workflow on Pipedream that updates a ticket in Jira, Trello, or Asana with the latest deployment status from Conveyor. This provides real-time visibility into which features or bug fixes have been deployed to your staging or production environments. + +- **Automated Rollback on Failure**: Design a workflow that listens for failed deployment events from Conveyor. On detecting a failure, the workflow could automatically trigger a rollback to the previous stable version of the application and post an incident report to an incident management tool like PagerDuty or Opsgenie. diff --git a/components/convolo_ai/README.md b/components/convolo_ai/README.md new file mode 100644 index 0000000000000..33be64ebc5b68 --- /dev/null +++ b/components/convolo_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +The Convolo.ai API enables automation of lead capturing and conversation management. With this API, you can programmatically access and manipulate your Convolo leads, manage call schedules, and track conversation outcomes. Integrating Convolo.ai with Pipedream allows you to connect your lead management process with other apps and services to create a seamless workflow, enhancing your ability to respond quickly to potential customers and organize your sales funnel efficiently. + +# Example Use Cases + +- **Automate Lead Syncing to CRM**: Use the Convolo.ai API on Pipedream to automatically capture new leads and sync them with your Customer Relationship Management (CRM) software, ensuring that your sales team always has the latest information without manual entry. + +- **Schedule Follow-Up Reminders**: After a lead is captured, set up a workflow that schedules follow-up tasks in your task management tool, like Trello or Asana, reminding your team to reach out to the lead at the optimal time. + +- **Analyze Lead Sources**: Track where your most valuable leads are coming from by sending lead data from Convolo.ai to a Google Sheet. Utilize this data to understand which marketing channels are most effective and optimize your spending. diff --git a/components/copper/README.md b/components/copper/README.md index 573aa644514dd..2631033266868 100644 --- a/components/copper/README.md +++ b/components/copper/README.md @@ -1,6 +1,11 @@ # Overview -Copper's API allows you to access and manipulate data in your Copper account -programmatically. With the API, you can create new records, update existing -records, delete records, and search for records. You can also perform bulk -operations, such as importing or exporting data. +Copper CRM's API unlocks the potential to streamline customer relationship management through automation and integration. On Pipedream, this means you can leverage event-driven workflows to sync contacts, manage deals, and automate tasks. Think of it as connecting Copper's insights directly to other apps or databases, triggering events in real-time based on CRM updates or capturing fresh leads instantaneously. + +# Example Use Cases + +- **Lead Entry Automation**: Automatically add new leads captured from a Google Form into Copper as contacts. Use Pipedream to listen for form submissions, then create or update contacts in Copper accordingly. This removes manual data entry and ensures your CRM is always up-to-date with the latest leads. + +- **Deal Won Notification**: Set up a notification system that alerts your team via Slack whenever a deal is marked as won in Copper. This workflow would use Pipedream to monitor changes in deal status and post a celebratory message to a designated Slack channel, keeping everyone in the loop and fostering a culture of success. + +- **Support Ticket Integration**: Integrate Copper with a helpdesk tool like Zendesk. When a support ticket is resolved, use Pipedream to check the customer's details in Copper and update the opportunity or contact record with the latest interaction details. This ensures your sales team has context on any support issues that have been addressed, potentially aiding in future sales or retention efforts. diff --git a/components/copper/actions/associate-to-project/associate-to-project.mjs b/components/copper/actions/associate-to-project/associate-to-project.mjs new file mode 100644 index 0000000000000..1f9417ffb322b --- /dev/null +++ b/components/copper/actions/associate-to-project/associate-to-project.mjs @@ -0,0 +1,53 @@ +import copper from "../../copper.app.mjs"; + +export default { + key: "copper-associate-to-project", + name: "Associate to Project", + description: "Relates an existing project with an existing CRM object. [See the documentation](https://developer.copper.com/related-items/relate-an-existing-record-to-an-entity.html)", + version: "0.0.1", + type: "action", + props: { + copper, + projectId: { + propDefinition: [ + copper, + "objectId", + () => ({ + objectType: "projects", + }), + ], + label: "Project ID", + description: "The ID of the project you wish to relate", + }, + objectType: { + propDefinition: [ + copper, + "objectType", + ], + }, + objectId: { + propDefinition: [ + copper, + "objectId", + (c) => ({ + objectType: c.objectType, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.copper.relateProjectToCrmObject({ + $, + objectType: this.objectType, + objectId: this.objectId, + data: { + resource: { + id: this.projectId, + type: "project", + }, + }, + }); + $.export("$summary", `Successfully associated Project ID ${this.projectId} with CRM ID ${this.objectId}`); + return response; + }, +}; diff --git a/components/copper/actions/create-update-person/create-update-person.mjs b/components/copper/actions/create-update-person/create-update-person.mjs new file mode 100644 index 0000000000000..b57c0c21ada11 --- /dev/null +++ b/components/copper/actions/create-update-person/create-update-person.mjs @@ -0,0 +1,126 @@ +import copper from "../../copper.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "copper-create-update-person", + name: "Create or Update Person", + description: "Creates a new person or updates an existing one based on email address. [See the documentation](https://developer.copper.com/people/create-a-new-person.html)", + version: "0.0.1", + type: "action", + props: { + copper, + email: { + type: "string", + label: "Email", + description: "Email address of the person. If email already exists, the person will be updated", + }, + name: { + type: "string", + label: "Name", + description: "Name of the contact. Required if creating a new person.", + optional: true, + }, + streetAddress: { + type: "string", + label: "Street Address", + description: "Street address of the person", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "City address of the person", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "State address of the person", + optional: true, + }, + postalCode: { + type: "string", + label: "Postal Code", + description: "Postal code of the person", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "Country of the person", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Phone number of the person", + optional: true, + }, + }, + async run({ $ }) { + let response; + const hasAddress = this.streetAddress + || this.city + || this.state + || this.postalCode + || this.country; + const data = { + emails: [ + { + email: this.email, + category: "work", + }, + ], + name: this.name, + address: hasAddress && { + street: this.streetAddress, + city: this.city, + state: this.state, + postal_code: this.postalCode, + country: this.country, + }, + phone_numbers: this.phone && [ + { + number: this.phone, + category: "work", + }, + ], + }; + + // search for the person + const person = await this.copper.listObjects({ + $, + objectType: "people", + data: { + emails: [ + this.email, + ], + }, + }); + + if (!person?.length && !this.name) { + throw new ConfigurationError(`Person with email ${this.email} not found. Name is required for creating a new person.`); + } + + // create person if not found + if (!person?.length) { + response = await this.copper.createPerson({ + $, + data, + }); + } + // update person if found + else { + response = await this.copper.updatePerson({ + $, + personId: person[0].id, + data, + }); + } + + $.export("$summary", `Successfully ${person?.length + ? "updated" + : "created"} person with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/copper/actions/create-update-project/create-update-project.mjs b/components/copper/actions/create-update-project/create-update-project.mjs new file mode 100644 index 0000000000000..4c2c75d0da895 --- /dev/null +++ b/components/copper/actions/create-update-project/create-update-project.mjs @@ -0,0 +1,86 @@ +import copper from "../../copper.app.mjs"; + +export default { + key: "copper-create-update-project", + name: "Create or Update Project", + description: "Creates a new project or updates an existing one based on the project name. [See the documentation](https://developer.copper.com/projects/create-a-new-project.html)", + version: "0.0.1", + type: "action", + props: { + copper, + name: { + type: "string", + label: "Name", + description: "Name of the project. If a project with the specified name already exists, it will be updated", + }, + details: { + type: "string", + label: "Details", + description: "Description of the Project", + optional: true, + }, + assigneeId: { + propDefinition: [ + copper, + "objectId", + () => ({ + objectType: "users", + }), + ], + label: "Assignee ID", + description: "The ID of the User that will be the owner of the Project", + optional: true, + }, + status: { + propDefinition: [ + copper, + "status", + ], + }, + tags: { + propDefinition: [ + copper, + "tags", + ], + }, + }, + async run({ $ }) { + let response; + const data = { + name: this.name, + assignee_id: this.assigneeId, + status: this.status, + tags: this.tags, + }; + + // search for the project + const project = await this.copper.listObjects({ + $, + objectType: "projects", + data: { + name: this.name, + }, + }); + + // create project if not found + if (!project?.length) { + response = await this.copper.createProject({ + $, + data, + }); + } + // update project if found + else { + response = await this.copper.updateProject({ + $, + projectId: project[0].id, + data, + }); + } + + $.export("$summary", `Successfully ${project?.length + ? "updated" + : "created"} project with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/copper/actions/get-object/get-object.mjs b/components/copper/actions/get-object/get-object.mjs new file mode 100644 index 0000000000000..bc709b8d39b74 --- /dev/null +++ b/components/copper/actions/get-object/get-object.mjs @@ -0,0 +1,36 @@ +import copper from "../../copper.app.mjs"; + +export default { + key: "copper-get-object", + name: "Get Object", + description: "Retrieves an existing CRM object. [See the documentation](https://developer.copper.com/account-and-users/fetch-user-by-id.html)", + version: "0.0.1", + type: "action", + props: { + copper, + objectType: { + propDefinition: [ + copper, + "objectType", + ], + }, + objectId: { + propDefinition: [ + copper, + "objectId", + (c) => ({ + objectType: c.objectType, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.copper.getObject({ + objectType: this.objectType, + objectId: this.objectId, + $, + }); + $.export("$summary", `Successfully retrieved CRM object with ID ${this.objectId}`); + return response; + }, +}; diff --git a/components/copper/copper.app.mjs b/components/copper/copper.app.mjs index 844ee8264d5f5..58bcaf83985bb 100644 --- a/components/copper/copper.app.mjs +++ b/components/copper/copper.app.mjs @@ -1,11 +1,184 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "copper", - propDefinitions: {}, + propDefinitions: { + objectId: { + type: "string", + label: "Object ID", + description: "The ID of the CRM object", + async options({ + objectType, page, + }) { + const objects = await this.listObjects({ + objectType, + params: { + page_number: page + 1, + }, + }); + return objects?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + tags: { + type: "string[]", + label: "Tags", + description: "An array of the tags associated with the Project", + optional: true, + async options() { + const tags = await this.listTags(); + return tags?.map(({ name }) => name) || []; + }, + }, + status: { + type: "string", + label: "Status", + description: "The status of the Project", + optional: true, + options: [ + "Open", + "Completed", + ], + }, + objectType: { + type: "string", + label: "Object Type", + description: "The type of CRM object", + async options() { + return [ + { + label: "Lead", + value: "leads", + }, + { + label: "Person", + value: "people", + }, + { + label: "Company", + value: "companies", + }, + { + label: "Opportunity", + value: "opportunities", + }, + { + label: "Project", + value: "projects", + }, + { + label: "Task", + value: "tasks", + }, + ]; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.copper.com/developer_api/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "X-PW-AccessToken": this.$auth.api_key, + "X-PW-Application": "developer_api", + "X-PW-UserEmail": this.$auth.email, + "Content-Type": "application/json", + }, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteWebhook({ + hookId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${hookId}`, + ...opts, + }); + }, + listObjects({ + objectType, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/${objectType}/search`, + ...opts, + }); + }, + listTags(opts = {}) { + return this._makeRequest({ + path: "/tags", + ...opts, + }); + }, + getObject({ + objectType, objectId, ...opts + }) { + return this._makeRequest({ + path: `/${objectType}/${objectId}`, + ...opts, + }); + }, + createPerson(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/people", + ...opts, + }); + }, + updatePerson({ + personId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/people/${personId}`, + ...opts, + }); + }, + createProject(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/projects", + ...opts, + }); + }, + updateProject({ + projectId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/projects/${projectId}`, + ...opts, + }); + }, + relateProjectToCrmObject({ + objectType, objectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/${objectType}/${objectId}/related`, + ...opts, + }); }, }, }; diff --git a/components/copper/package.json b/components/copper/package.json new file mode 100644 index 0000000000000..b0eb582b196cb --- /dev/null +++ b/components/copper/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/copper", + "version": "0.0.1", + "description": "Pipedream Copper Components", + "main": "copper.app.mjs", + "keywords": [ + "pipedream", + "copper" + ], + "homepage": "https://pipedream.com/apps/copper", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/copper/sources/common/base.mjs b/components/copper/sources/common/base.mjs new file mode 100644 index 0000000000000..6a69e9bac4873 --- /dev/null +++ b/components/copper/sources/common/base.mjs @@ -0,0 +1,58 @@ +import copper from "../../copper.app.mjs"; + +export default { + props: { + copper, + db: "$.service.db", + http: "$.interface.http", + }, + hooks: { + async activate() { + const { id } = await this.copper.createWebhook({ + data: { + target: this.http.endpoint, + type: this.getObjectType(), + event: this.getEventType(), + }, + }); + this._setHookId(id); + }, + async deactivate() { + const hookId = this._getHookId(); + if (hookId) { + await this.copper.deleteWebhook({ + hookId, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getEventType() { + return "new"; + }, + generateMeta(item) { + return { + id: item.ids[0], + summary: this.getSummary(item), + ts: Date.parse(item.timestamp), + }; + }, + getObjectType() { + throw new Error("getObjectType is not implemented"); + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + }, + async run(event) { + const { body } = event; + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, +}; diff --git a/components/copper/sources/new-lead-instant/new-lead-instant.mjs b/components/copper/sources/new-lead-instant/new-lead-instant.mjs new file mode 100644 index 0000000000000..9b3a3aee48cff --- /dev/null +++ b/components/copper/sources/new-lead-instant/new-lead-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "copper-new-lead-instant", + name: "New Lead (Instant)", + description: "Emit new event when a new lead is created in Copper", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getObjectType() { + return "lead"; + }, + getSummary(item) { + return `New lead created with ID ${item.ids[0]}`; + }, + }, + sampleEmit, +}; diff --git a/components/copper/sources/new-lead-instant/test-event.mjs b/components/copper/sources/new-lead-instant/test-event.mjs new file mode 100644 index 0000000000000..ea50c83f1924f --- /dev/null +++ b/components/copper/sources/new-lead-instant/test-event.mjs @@ -0,0 +1,10 @@ +export default { + "subscription_id": 395049, + "event": "new", + "type": "lead", + "ids": [ + 89011972 + ], + "updated_attributes": {}, + "timestamp": "2024-10-14T16:15:54.893Z" +} \ No newline at end of file diff --git a/components/copper/sources/new-opportunity-instant/new-opportunity-instant.mjs b/components/copper/sources/new-opportunity-instant/new-opportunity-instant.mjs new file mode 100644 index 0000000000000..f1a78821c4dfa --- /dev/null +++ b/components/copper/sources/new-opportunity-instant/new-opportunity-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "copper-new-opportunity-instant", + name: "New Opportunity (Instant)", + description: "Emit new event when a new opportunity is created in Copper", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getObjectType() { + return "opportunity"; + }, + getSummary(item) { + return `New opportunity created with ID ${item.ids[0]}`; + }, + }, + sampleEmit, +}; diff --git a/components/copper/sources/new-opportunity-instant/test-event.mjs b/components/copper/sources/new-opportunity-instant/test-event.mjs new file mode 100644 index 0000000000000..43a76ff5c6c4f --- /dev/null +++ b/components/copper/sources/new-opportunity-instant/test-event.mjs @@ -0,0 +1,10 @@ +export default { + "subscription_id": 395050, + "event": "new", + "type": "opportunity", + "ids": [ + 32978697 + ], + "updated_attributes": {}, + "timestamp": "2024-10-14T16:22:15.070Z" +} \ No newline at end of file diff --git a/components/copper/sources/new-person-instant/new-person-instant.mjs b/components/copper/sources/new-person-instant/new-person-instant.mjs new file mode 100644 index 0000000000000..0bd763c87bfab --- /dev/null +++ b/components/copper/sources/new-person-instant/new-person-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "copper-new-person-instant", + name: "New Person (Instant)", + description: "Emit new event when a person object is newly created in Copper", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getObjectType() { + return "person"; + }, + getSummary(item) { + return `New person created with ID ${item.ids[0]}`; + }, + }, + sampleEmit, +}; diff --git a/components/copper/sources/new-person-instant/test-event.mjs b/components/copper/sources/new-person-instant/test-event.mjs new file mode 100644 index 0000000000000..ff2c0c257af8d --- /dev/null +++ b/components/copper/sources/new-person-instant/test-event.mjs @@ -0,0 +1,10 @@ +export default { + "subscription_id": 395034, + "event": "new", + "type": "person", + "ids": [ + 167487595 + ], + "updated_attributes": {}, + "timestamp": "2024-10-14T16:07:45.004Z" +} \ No newline at end of file diff --git a/components/copperx/README.md b/components/copperx/README.md new file mode 100644 index 0000000000000..d63f16ebb84bd --- /dev/null +++ b/components/copperx/README.md @@ -0,0 +1,11 @@ +# Overview + +The Copperx API offers a suite of tools for cryptocurrency trading, providing users with real-time access to market data, order execution, and account management. Integrating this API into Pipedream allows for the creation of custom, serverless workflows that can automate trading strategies, alert users to market changes, and manage portfolios efficiently without manual intervention. By leveraging Pipedream's ability to connect to hundreds of apps, users can develop robust, event-driven automations that respond to market conditions or other external signals. + +# Example Use Cases + +- **Automated Trading System**: Build a workflow that triggers a trade on Copperx based on specific market conditions. For example, if a cryptocurrency hits a certain price, the workflow can execute a buy or sell order, then log the transaction in a Google Sheet. + +- **Real-time Alerts**: Use the Copperx API to monitor the market for certain events, such as drastic price changes or volume spikes. When such an event occurs, Pipedream can send an immediate alert through services like Slack, SMS, or email, allowing you to make timely decisions. + +- **Portfolio Rebalancing**: Create a recurring workflow in Pipedream that checks your cryptocurrency portfolio on Copperx at regular intervals. If the portfolio drifts from your desired asset allocation, the workflow can execute trades to rebalance the portfolio back to the target allocations. diff --git a/components/corrently/README.md b/components/corrently/README.md index 6aa67c5f47ec3..5267e526850a7 100644 --- a/components/corrently/README.md +++ b/components/corrently/README.md @@ -1,14 +1,11 @@ # Overview -What is Corrently? +The Corrently API offers a suite of services related to green energy, including insights on electricity consumption, cost forecasting, and localized energy generation data. With Corrently, you can pull real-time data to drive sustainability initiatives, create cost-optimization strategies for energy usage, and foster eco-friendly practices. Leveraging this API within Pipedream allows for automated workflows that can turn this data into actionable insights, integrate with smart home devices, manage energy budgets, and even shift usage to times when the grid is greener and electricity cheaper. -Corrently is a German startup that operates a crowd-sourced P2P smart energy -grid. The company was founded in Berlin in early 2017. The current network -comprises around 1,200 rooftop PV systems with a total capacity of around 2.8 -MW. +# Example Use Cases -What can I build using the Corrently API? +- **Smart Home Energy Optimization**: Trigger a Pipedream workflow using Corrently API to monitor real-time energy prices. When the price drops below a certain threshold, send a signal to smart devices (via SmartThings or similar apps) to activate energy-intensive tasks like charging electric vehicles or running the dishwasher. -- A P2P smart energy grid -- A crowd-sourced P2P smart energy grid -- A Berlin-based P2P smart energy grid +- **Green Energy Reporting**: Set up a Pipedream workflow that periodically retrieves data from the Corrently API to track the carbon footprint of your household or business. Use the data to compile detailed reports or visualizations with Google Sheets or Data Studio, which can be shared automatically through email or Slack to stakeholders interested in sustainability metrics. + +- **Demand Response Automation**: Create a Pipedream workflow that uses the Corrently API to monitor grid load and renewable energy availability. When renewable energy supply is high, the workflow can send notifications via Twilio SMS to encourage users to consume more power during these greener periods, contributing to a more balanced energy grid. diff --git a/components/corsizio/README.md b/components/corsizio/README.md index 686189e42bd68..12d5f8229c029 100644 --- a/components/corsizio/README.md +++ b/components/corsizio/README.md @@ -1,5 +1,11 @@ # Overview -Corsizio is an online tool that allows you to create and sell courses and -events. You can use the Corsizio API to create and manage your courses and -events, as well as to sell tickets to your courses and events. +Corsizio is a platform for event registration, payment processing, and attendee management. With the Corsizio API, you can automate your event management workflows on Pipedream, such as synchronizing attendee data with a CRM, sending custom emails based on event registration status, or processing payments and refunding attendees. By utilizing Pipedream's serverless platform, you can create intricate automations that respond to various Corsizio events, handle data transformation, and interact with countless other APIs for a seamless integration experience. + +# Example Use Cases + +- **Automated Attendee Follow-up**: After an attendee registers for an event on Corsizio, trigger a Pipedream workflow to send a personalized welcome email via SendGrid. If the attendee doesn't complete payment within a certain timeframe, follow up with a reminder email or SMS via Twilio. + +- **CRM Synchronization**: Keep your CRM up-to-date by creating a Pipedream workflow that triggers whenever a new registration occurs on Corsizio. Use this trigger to add or update the attendee's information in Salesforce, ensuring your sales team has the latest data without manual entry. + +- **Feedback Collection Post-Event**: After an event concludes, automate the process of gathering feedback. Use a Pipedream workflow to send a survey link from Typeform to all attendees. Collect responses and aggregate the data in Google Sheets for easy analysis and future event improvement. diff --git a/components/corsizio/package.json b/components/corsizio/package.json new file mode 100644 index 0000000000000..bf81599b15f9a --- /dev/null +++ b/components/corsizio/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/corsizio", + "version": "0.6.0", + "description": "Pipedream corsizio Components", + "main": "corsizio.app.mjs", + "keywords": [ + "pipedream", + "corsizio" + ], + "homepage": "https://pipedream.com/apps/corsizio", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/countdown_api/README.md b/components/countdown_api/README.md new file mode 100644 index 0000000000000..64a9aa779d84d --- /dev/null +++ b/components/countdown_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Countdown API provides a simple way to create and manage countdowns to specific events or deadlines. Integrating this API with Pipedream allows for the creation of dynamic serverless workflows that can react to time-sensitive triggers. Whether it's sending reminders, updating statuses, or triggering events as deadlines approach, Pipedream's ability to connect to hundreds of apps gives you the flexibility to craft custom solutions around your countdowns. + +# Example Use Cases + +- **Countdown to Product Launch**: Automate social media posts using the Twitter API or another social app on Pipedream to share daily countdown updates leading up to a product launch, building hype and keeping your audience engaged. + +- **Subscription Renewal Reminders**: Connect the Countdown API with an email service like SendGrid on Pipedream. Set up a workflow to send personalized email reminders to customers a set number of days before their subscription renewal date. + +- **Project Deadline Alerts**: Use the Countdown API to monitor project deadlines and integrate with Slack on Pipedream. Create a workflow that sends alerts to a Slack channel when a project is a week away from its deadline, keeping the team informed and on-task. diff --git a/components/countdown_api/actions/get-product-data/get-product-data.mjs b/components/countdown_api/actions/get-product-data/get-product-data.mjs new file mode 100644 index 0000000000000..e0b0259e0ba0c --- /dev/null +++ b/components/countdown_api/actions/get-product-data/get-product-data.mjs @@ -0,0 +1,37 @@ +import app from "../../countdown_api.app.mjs"; + +export default { + key: "countdown_api-get-product-data", + name: "Get Product Data", + description: "Retrieves data for a specific product on eBay using the Countdown API. [See the documentation](https://www.countdownapi.com/docs/ebay-product-data-api/parameters/product)", + version: "0.0.1", + type: "action", + props: { + app, + epid: { + propDefinition: [ + app, + "epid", + ], + }, + ebayDomain: { + propDefinition: [ + app, + "ebayDomain", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getData({ + $, + params: { + epid: this.epid, + ebay_domain: this.ebayDomain, + type: "product", + }, + }); + + $.export("$summary", `Retrieved product data for EPID ${this.epid}`); + return response; + }, +}; diff --git a/components/countdown_api/actions/get-reviews/get-reviews.mjs b/components/countdown_api/actions/get-reviews/get-reviews.mjs new file mode 100644 index 0000000000000..56144a1002e1b --- /dev/null +++ b/components/countdown_api/actions/get-reviews/get-reviews.mjs @@ -0,0 +1,37 @@ +import app from "../../countdown_api.app.mjs"; + +export default { + key: "countdown_api-get-reviews", + name: "Get Product Reviews", + description: "Retrieves customer reviews for a specific product on eBay. [See the documentation](https://www.countdownapi.com/docs/ebay-product-data-api/parameters/reviews)", + version: "0.0.1", + type: "action", + props: { + app, + epid: { + propDefinition: [ + app, + "epid", + ], + }, + ebayDomain: { + propDefinition: [ + app, + "ebayDomain", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getData({ + $, + params: { + epid: this.epid, + ebay_domain: this.ebayDomain, + type: "reviews", + }, + }); + + $.export("$summary", `Retrieved reviews for product with EPID ${this.epid}`); + return response; + }, +}; diff --git a/components/countdown_api/actions/get-search-results/get-search-results.mjs b/components/countdown_api/actions/get-search-results/get-search-results.mjs new file mode 100644 index 0000000000000..2c25c763111d6 --- /dev/null +++ b/components/countdown_api/actions/get-search-results/get-search-results.mjs @@ -0,0 +1,37 @@ +import app from "../../countdown_api.app.mjs"; + +export default { + key: "countdown_api-get-search-results", + name: "Get Search Results from eBay", + description: "Retrieves search results from eBay. [See the documentation](https://www.countdownapi.com/docs/ebay-product-data-api/parameters/search)", + version: "0.0.1", + type: "action", + props: { + app, + searchTerm: { + propDefinition: [ + app, + "searchTerm", + ], + }, + ebayDomain: { + propDefinition: [ + app, + "ebayDomain", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getData({ + $, + params: { + search_term: this.searchTerm, + ebay_domain: this.ebayDomain, + type: "search", + }, + }); + + $.export("$summary", `Successfully retrieved search results for ${this.searchTerm}`); + return response; + }, +}; diff --git a/components/countdown_api/common/constants.mjs b/components/countdown_api/common/constants.mjs new file mode 100644 index 0000000000000..500dc171ff9ac --- /dev/null +++ b/components/countdown_api/common/constants.mjs @@ -0,0 +1,84 @@ +export default { + EBAY_DOMAINS: [ + { + "value": "ebay.com.au", + "label": "Australia", + }, + { + "value": "ebay.at", + "label": "Austria", + }, + { + "value": "ebay.be", + "label": "Belgium", + }, + { + "value": "befr.ebay.be", + "label": "Belgium (FR)", + }, + { + "value": "benl.ebay.be", + "label": "Belgium (NL)", + }, + { + "value": "ebay.ca", + "label": "Canada", + }, + { + "value": "ebay.fr", + "label": "France", + }, + { + "value": "ebay.de", + "label": "Germany", + }, + { + "value": "ebay.com.hk", + "label": "Hong Kong", + }, + { + "value": "ebay.ie", + "label": "Ireland", + }, + { + "value": "ebay.it", + "label": "Italy", + }, + { + "value": "ebay.com.my", + "label": "Malaysia", + }, + { + "value": "ebay.nl", + "label": "Netherlands", + }, + { + "value": "ebay.ph", + "label": "Philippines", + }, + { + "value": "ebay.pl", + "label": "Poland", + }, + { + "value": "ebay.com.sg", + "label": "Singapore", + }, + { + "value": "ebay.es", + "label": "Spain", + }, + { + "value": "ebay.ch", + "label": "Switzerland", + }, + { + "value": "ebay.co.uk", + "label": "United Kingdom", + }, + { + "value": "ebay.com", + "label": "United States", + }, + ], +}; diff --git a/components/countdown_api/countdown_api.app.mjs b/components/countdown_api/countdown_api.app.mjs new file mode 100644 index 0000000000000..5f1fe7adc075e --- /dev/null +++ b/components/countdown_api/countdown_api.app.mjs @@ -0,0 +1,52 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "countdown_api", + propDefinitions: { + searchTerm: { + type: "string", + label: "Search Term", + description: "The search term to use when querying eBay search results", + }, + ebayDomain: { + type: "string", + label: "eBay Domain", + description: "The eBay domain to use for the search (e.g., ebay.com, ebay.co.uk).", + options: constants.EBAY_DOMAINS, + }, + epid: { + type: "string", + label: "EPID", + description: "The eBay Product Identifier", + }, + }, + methods: { + _baseUrl() { + return "https://api.countdownapi.com"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + ...otherOpts + } = opts; + return axios($, { + url: this._baseUrl() + path, + params: { + api_key: this.$auth.api_key, + ...params, + }, + ...otherOpts, + }); + }, + async getData(args = {}) { + return this._makeRequest({ + path: "/request", + ...args, + }); + }, + }, +}; diff --git a/components/countdown_api/package.json b/components/countdown_api/package.json new file mode 100644 index 0000000000000..b8d41340092b5 --- /dev/null +++ b/components/countdown_api/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/countdown_api", + "version": "0.1.0", + "description": "Pipedream Countdown API Components", + "main": "countdown_api.app.mjs", + "keywords": [ + "pipedream", + "countdown_api" + ], + "homepage": "https://pipedream.com/apps/countdown_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/coupontools/README.md b/components/coupontools/README.md new file mode 100644 index 0000000000000..ca52c8650c441 --- /dev/null +++ b/components/coupontools/README.md @@ -0,0 +1,11 @@ +# Overview + +The Coupontools API offers a robust set of functions for creating, managing, and tracking digital coupons and deals. With Pipedream, you can automate workflows that integrate with Coupontools to enhance customer engagement, streamline promotions, and gather insightful data on coupon usage. By leveraging Pipedream's capability to connect with various apps and services, you can create multi-step workflows that react to events, process data, and perform actions across your marketing stack. + +# Example Use Cases + +- **Automated Coupon Distribution via Email**: Trigger a workflow in Pipedream when a new subscriber is added to a Mailchimp audience. Automatically generate a unique coupon code through the Coupontools API and send it to the new subscriber via a personalized email. + +- **Real-Time Coupon Validation for In-Store Purchases**: Connect a point-of-sale system to Pipedream and validate coupon codes in real-time by hitting the Coupontools API. Once validated, process the discount and update inventory or CRM records accordingly. + +- **Coupon Campaign Performance Tracking**: Schedule a regular workflow in Pipedream to query the Coupontools API for coupon redemption data. Aggregate and analyze these metrics, then push the insights to a Google Sheets document for easy reporting and visualization. diff --git a/components/covalent/README.md b/components/covalent/README.md new file mode 100644 index 0000000000000..63ca53e3bd9b9 --- /dev/null +++ b/components/covalent/README.md @@ -0,0 +1,11 @@ +# Overview + +The Covalent API provides comprehensive blockchain data from multiple chains, enabling the retrieval of balances, transactions, and contract details with a simple API call. On Pipedream, you can harness this API to create powerful automations and workflows that respond to blockchain events, aggregate data for analysis, and integrate with other apps for a wide array of use cases, such as notifying users of transaction events or updating databases with the latest blockchain states. + +# Example Use Cases + +- **Crypto Portfolio Tracker**: Build a workflow that periodically fetches the balance and transaction history of wallet addresses across various blockchains using the Covalent API. Send this data to a Google Sheet using the Google Sheets app on Pipedream for an easy-to-use portfolio tracking solution. + +- **Real-time Transaction Alerts**: Set up a workflow that listens for new transactions to specific wallet addresses using Covalent API. When a new transaction is detected, trigger an alert via Twilio's SMS service or send a Discord notification to keep stakeholders informed in real time. + +- **Decentralized Finance (DeFi) Opportunity Monitor**: Create a workflow that uses the Covalent API to scan for profitable opportunities in DeFi protocols. Aggregate current rates and positions, compare against historical data stored in a Pipedream data store, and use this information to email a curated list of opportunities via the SendGrid app, helping users make timely investment decisions. diff --git a/components/cradl_ai/README.md b/components/cradl_ai/README.md new file mode 100644 index 0000000000000..29aaadbb0a8fd --- /dev/null +++ b/components/cradl_ai/README.md @@ -0,0 +1,14 @@ +# Overview + +Cradl AI is a powerful tool for creating customized machine learning models without extensive coding knowledge. It specializes in processing and understanding various forms of data, including text and images. Utilizing the Cradl AI API on Pipedream, developers can automate workflows involving data analysis, enhance data-driven decision-making, and integrate AI capabilities into applications seamlessly. This opens up opportunities for dynamic interactions with other services, streamlining operations and enhancing user experiences. + +# Example Use Cases + +- **Automated Document Processing Workflow**: + Extract text from uploaded documents on cloud storage platforms like Dropbox or Google Drive using Cradl AI, then analyze and store extracted data in a database such as Google Sheets or Airtable. This workflow can be used for automated invoice processing, extracting data from standardized forms, or updating databases with information from physical documents. + +- **Image Content Analysis and Notification System**: + Set up a workflow where images uploaded to an AWS S3 bucket are automatically sent to Cradl AI for content analysis. Based on the analysis, if certain criteria are met (like detecting specific objects or text within the image), trigger notifications via email or messaging platforms such as Slack. This can be particularly useful for security systems, quality control in manufacturing, or content moderation in social media. + +- **Real-Time Feedback Collection and Analysis**: + Implement a system where feedback submitted through a form (e.g., Google Forms or Typeform) is automatically sent to Cradl AI for sentiment analysis. Aggregate and visualize this data in tools like Tableau or Google Data Studio, providing real-time insights into customer satisfaction and helping businesses quickly adjust to feedback trends. diff --git a/components/cradl_ai/actions/common/common.mjs b/components/cradl_ai/actions/common/common.mjs new file mode 100644 index 0000000000000..fafa7eb27e763 --- /dev/null +++ b/components/cradl_ai/actions/common/common.mjs @@ -0,0 +1,63 @@ +import cradlAi from "../../cradl_ai.app.mjs"; +import constants from "../../common/constants.mjs"; +import fs from "fs"; + +export default { + props: { + cradlAi, + filePath: { + type: "string", + label: "File Path", + description: "The path to the document file saved to the `/tmp` directory (e.g. `/tmp/example.pdf`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + }, + contentType: { + type: "string", + label: "Content Type", + description: "The content type of the document", + options: constants.CONTENT_TYPES, + }, + name: { + type: "string", + label: "Name", + description: "Name of the document", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "Description of the document", + optional: true, + }, + datasetId: { + propDefinition: [ + cradlAi, + "datasetId", + ], + }, + }, + methods: { + createDocumentHandle($) { + return this.cradlAi.createDocumentHandle({ + $, + data: { + name: this.name, + description: this.description, + datasetId: this.datasetId, + }, + }); + }, + uploadFile($, fileUrl) { + const fileData = fs.readFileSync(this.filePath.includes("/tmp") + ? this.filePath + : `/tmp/${this.filePath}`); + return this.cradlAi.uploadDocument({ + $, + fileUrl, + data: fileData, + headers: { + "Content-Type": this.contentType, + }, + }); + }, + }, +}; diff --git a/components/cradl_ai/actions/parse-document-human-in-loop/parse-document-human-in-loop.mjs b/components/cradl_ai/actions/parse-document-human-in-loop/parse-document-human-in-loop.mjs new file mode 100644 index 0000000000000..c4a16d6eff9c1 --- /dev/null +++ b/components/cradl_ai/actions/parse-document-human-in-loop/parse-document-human-in-loop.mjs @@ -0,0 +1,41 @@ +import common from "../common/common.mjs"; + +export default { + ...common, + key: "cradl_ai-parse-document-human-in-loop", + name: "Parse Document with Human in the Loop", + description: "Sends a document to an existing flow for human-in-the-loop processing. [See the documentation](https://docs.cradl.ai/integrations/rest-api)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + workflowId: { + propDefinition: [ + common.props.cradlAi, + "workflowId", + ], + }, + }, + async run({ $ }) { + const { + documentId, fileUrl, + } = await this.createDocumentHandle($); + await this.uploadFile($, fileUrl); + const { executionId } = await this.cradlAi.runWorkflow({ + $, + workflowId: this.workflowId, + data: { + input: { + documentId, + }, + }, + }); + const result = await this.cradlAi.getRunResult({ + $, + workflowId: this.workflowId, + executionId, + }); + $.export("$summary", `Successfully parsed document with ID: ${documentId}`); + return result; + }, +}; diff --git a/components/cradl_ai/actions/parse-document/parse-document.mjs b/components/cradl_ai/actions/parse-document/parse-document.mjs new file mode 100644 index 0000000000000..5b320fb1af25b --- /dev/null +++ b/components/cradl_ai/actions/parse-document/parse-document.mjs @@ -0,0 +1,34 @@ +import common from "../common/common.mjs"; + +export default { + ...common, + key: "cradl_ai-parse-document", + name: "Parse Document", + description: "Parses data from a document using a custom selected model. [See the documentation](https://docs.cradl.ai/integrations/rest-api)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + modelId: { + propDefinition: [ + common.props.cradlAi, + "modelId", + ], + }, + }, + async run({ $ }) { + const { + documentId, fileUrl, + } = await this.createDocumentHandle($); + await this.uploadFile($, fileUrl); + const result = await this.cradlAi.createPrediction({ + $, + data: { + documentId, + modelId: this.modelId, + }, + }); + $.export("$summary", `Successfully parsed document with ID: ${documentId}`); + return result; + }, +}; diff --git a/components/cradl_ai/common/constants.mjs b/components/cradl_ai/common/constants.mjs new file mode 100644 index 0000000000000..6749916005b10 --- /dev/null +++ b/components/cradl_ai/common/constants.mjs @@ -0,0 +1,10 @@ +const CONTENT_TYPES = [ + "application/pdf", + "image/jpeg", + "image/png", + "image/tiff", +]; + +export default { + CONTENT_TYPES, +}; diff --git a/components/cradl_ai/cradl_ai.app.mjs b/components/cradl_ai/cradl_ai.app.mjs new file mode 100644 index 0000000000000..3683f3997cc93 --- /dev/null +++ b/components/cradl_ai/cradl_ai.app.mjs @@ -0,0 +1,206 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "cradl_ai", + propDefinitions: { + modelId: { + type: "string", + label: "Model ID", + description: "The ID of the model to be used for parsing", + async options({ prevContext }) { + const params = prevContext?.nextToken + ? { + nextToken: prevContext.nextToken, + } + : {}; + const { + models, nextToken, + } = await this.listModels({ + params, + }); + const options = models?.map(({ + modelId: value, name: label, + }) => ({ + value, + label, + })) || []; + return { + options, + context: { + nextToken, + }, + }; + }, + }, + workflowId: { + type: "string", + label: "Workflow ID", + description: "The ID of the existing flow where the document will be sent", + async options({ prevContext }) { + const params = prevContext?.nextToken + ? { + nextToken: prevContext.nextToken, + } + : {}; + const { + workflows, nextToken, + } = await this.listWorkflows({ + params, + }); + const options = workflows?.map(({ + workflowId: value, name: label, + }) => ({ + value, + label, + })) || []; + return { + options, + context: { + nextToken, + }, + }; + }, + }, + datasetId: { + type: "string", + label: "Dataset ID", + description: "The ID of the dataset to use", + optional: true, + async options({ prevContext }) { + const params = prevContext?.nextToken + ? { + nextToken: prevContext.nextToken, + } + : {}; + const { + datasets, nextToken, + } = await this.listDatasets({ + params, + }); + const options = datasets?.map(({ + datasetId: value, name: label, + }) => ({ + value, + label, + })) || []; + return { + options, + context: { + nextToken, + }, + }; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.lucidtech.ai/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + ...otherOpts, + }); + }, + listModels(opts = {}) { + return this._makeRequest({ + path: "/models", + ...opts, + }); + }, + listWorkflows(opts = {}) { + return this._makeRequest({ + path: "/workflows", + ...opts, + }); + }, + listDatasets(opts = {}) { + return this._makeRequest({ + path: "/datasets", + ...opts, + }); + }, + listExecutions({ + workflowId, ...opts + }) { + return this._makeRequest({ + path: `/workflows/${workflowId}/executions`, + ...opts, + }); + }, + createDocumentHandle(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/documents", + ...opts, + }); + }, + uploadDocument({ + fileUrl, ...opts + }) { + return this._makeRequest({ + method: "PUT", + url: fileUrl, + ...opts, + }); + }, + runWorkflow({ + workflowId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/workflows/${workflowId}/executions`, + ...opts, + }); + }, + getRunResult({ + workflowId, executionId, ...opts + }) { + return this._makeRequest({ + path: `/workflows/${workflowId}/executions/${executionId}`, + ...opts, + }); + }, + createPrediction(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/predictions", + ...opts, + }); + }, + async *paginate({ + resourceFn, + args, + resourceType, + max, + }) { + args.params = { + ...args.params, + }; + let count = 0; + do { + const response = await resourceFn(args); + const results = response[resourceType]; + for (const item of results) { + yield item; + count++; + if (max && count >= max) { + return; + } + } + args.params.nextToken = response?.nextToken; + } while (args.params.nextToken); + }, + }, +}; diff --git a/components/cradl_ai/package.json b/components/cradl_ai/package.json new file mode 100644 index 0000000000000..5e42698c0847f --- /dev/null +++ b/components/cradl_ai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/cradl_ai", + "version": "0.1.0", + "description": "Pipedream Cradl AI Components", + "main": "cradl_ai.app.mjs", + "keywords": [ + "pipedream", + "cradl_ai" + ], + "homepage": "https://pipedream.com/apps/cradl_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} diff --git a/components/cradl_ai/sources/new-document-parsing-completed-instant/new-document-parsing-completed-instant.mjs b/components/cradl_ai/sources/new-document-parsing-completed-instant/new-document-parsing-completed-instant.mjs new file mode 100644 index 0000000000000..a5e1dd3d05139 --- /dev/null +++ b/components/cradl_ai/sources/new-document-parsing-completed-instant/new-document-parsing-completed-instant.mjs @@ -0,0 +1,86 @@ +import cradlAi from "../../cradl_ai.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + key: "cradl_ai-new-document-parsing-completed-instant", + name: "New Document Parsing Completed (Instant)", + description: "Emit new event when a document processing flow has completed.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + cradlAi, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + workflowId: { + propDefinition: [ + cradlAi, + "workflowId", + ], + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + emitEvent(item) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }, + generateMeta(item) { + return { + id: item.executionId, + summary: `Execution completed with ID ${item.executionId}`, + ts: Date.parse(item.endTime), + }; + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + const results = this.cradlAi.paginate({ + resourceFn: this.cradlAi.listExecutions, + args: { + workflowId: this.workflowId, + params: { + order: "descending", + sortBy: "endTime", + status: [ + "completed", + ], + }, + }, + resourceType: "executions", + max, + }); + const executions = []; + for await (const item of results) { + const ts = Date.parse(item.endTime); + if (ts >= lastTs) { + executions.push(item); + } else { + break; + } + } + if (!executions.length) { + return; + } + this._setLastTs(Date.parse(executions[0].endTime)); + executions.reverse().forEach((item) => this.emitEvent(item)); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/craftboxx/README.md b/components/craftboxx/README.md new file mode 100644 index 0000000000000..c66ef511cf32c --- /dev/null +++ b/components/craftboxx/README.md @@ -0,0 +1,11 @@ +# Overview + +The Craftboxx API allows users to integrate various features of the Craftboxx project management solution into their workflows. With this API, you can automate tasks related to project planning, resource allocation, time tracking, and document management. On Pipedream, you can tap into the Craftboxx API to create customized, serverless workflows that respond to events in real time. This enables businesses to streamline operations, reduce manual data entry, and improve coordination between teams. + +# Example Use Cases + +- **Craftboxx Project Sync with Google Sheets**: Automate the process of syncing project details and updates from Craftboxx to a Google Sheet. Anytime a project is updated or a new project is created in Craftboxx, the workflow triggers and updates the corresponding Google Sheet. + +- **Time Tracking Alerts via Slack**: Set up a workflow that monitors time tracking entries in Craftboxx. When a certain threshold of hours is logged on a project, the workflow sends an alert to a designated Slack channel. This ensures project managers are informed and can keep projects on track. + +- **Invoice Generation with QuickBooks**: Create an automated workflow that generates invoices in QuickBooks whenever a project reaches completion in Craftboxx. The workflow would pull necessary details from Craftboxx and populate them in a new QuickBooks invoice, streamlining the billing process. diff --git a/components/craftmypdf/README.md b/components/craftmypdf/README.md index 6c423a2c717cc..cf8f2d4c5dacc 100644 --- a/components/craftmypdf/README.md +++ b/components/craftmypdf/README.md @@ -1,11 +1,11 @@ # Overview -With the CraftMyPDF API, you can easily build PDF documents from scratch using -plain old HTML and CSS. Here are a few examples of what you can do: - -- Generate a simple PDF document with basic formatting -- Add headers and footers to your PDF document -- Insert images, charts, and other graphics into your PDF document -- Add hyperlinks and bookmarks to your PDF document -- Protect your PDF document with password security -- And much more! +CraftMyPDF is a powerful API that allows you to create dynamic and customizable PDF documents. By leveraging this API on Pipedream, you can automate the generation of invoices, reports, tickets, and any other document that requires a polished and professional look. Pipedream's serverless platform enables you to connect CraftMyPDF with numerous apps to create workflows that trigger PDF generation from events like form submissions, new database entries, or scheduled jobs. + +# Example Use Cases + +- **Automated Invoice Generation**: When a new order is placed through an e-commerce platform like Shopify, trigger a workflow on Pipedream that captures the order details and sends them to CraftMyPDF. The API generates a custom invoice PDF which is then automatically emailed to the customer and saved to a cloud storage service like Dropbox. + +- **Monthly Reporting for Analytics**: Set up a Pipedream workflow that runs at the end of each month, pulling analytics data from a tool like Google Analytics. The data is formatted and sent to CraftMyPDF to create a comprehensive visual report. The final PDF can be distributed via email to stakeholders or uploaded to a shared workspace such as Google Drive. + +- **Event Ticketing Upon Registration**: When a user registers for an event using a form on platforms like Typeform or Google Forms, trigger a Pipedream workflow that collects the registration data. CraftMyPDF creates a personalized event ticket in PDF format, which is then sent to the registrant's email and added to a database for event management purposes. diff --git a/components/crawlbase/README.md b/components/crawlbase/README.md new file mode 100644 index 0000000000000..cb09e5df5598d --- /dev/null +++ b/components/crawlbase/README.md @@ -0,0 +1,11 @@ +# Overview + +The Crawlbase API provides powerful tools for web scraping and data extraction from any webpage. It handles large-scale data collection tasks, bypassing bot protection and CAPTCHAs, and returning structured data. Within Pipedream, you can leverage Crawlbase to automate the harvesting of web data, integrate scraped content with other services, and process it for analysis, reporting, or triggering other workflows. + +# Example Use Cases + +- **Automated Content Aggregation**: Extract content from various online sources and aggregate it for a news digest or market research. You could use Crawlbase to scrape articles, blog posts, or product listings, then send the results to Google Sheets using Pipedream's Google Sheets integration for easy organization and analysis. + +- **E-commerce Price Monitoring**: Set up a workflow that regularly scrapes competitor websites for product pricing using Crawlbase. Process the scraped data in Pipedream to find pricing patterns or changes, and automatically adjust your own pricing on Shopify or alert your sales team via Slack if competitors change their prices. + +- **SEO Monitoring**: Monitor changes in content and meta tags on your own or competitors' web pages to inform SEO strategies. Crawlbase can capture relevant data points, and Pipedream can forward this information to an SEO tool like Moz or Ahrefs for further analysis, or log it in a data store for tracking over time. diff --git a/components/crawlbase/crawlbase.app.mjs b/components/crawlbase/crawlbase.app.mjs new file mode 100644 index 0000000000000..c15a9e54f4f5d --- /dev/null +++ b/components/crawlbase/crawlbase.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "crawlbase", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/crawlbase/package.json b/components/crawlbase/package.json new file mode 100644 index 0000000000000..c8f60455b48ce --- /dev/null +++ b/components/crawlbase/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/crawlbase", + "version": "0.0.1", + "description": "Pipedream Crawlbase Components", + "main": "crawlbase.app.mjs", + "keywords": [ + "pipedream", + "crawlbase" + ], + "homepage": "https://pipedream.com/apps/crawlbase", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/credit_repair_cloud/README.md b/components/credit_repair_cloud/README.md new file mode 100644 index 0000000000000..b50b92a8050df --- /dev/null +++ b/components/credit_repair_cloud/README.md @@ -0,0 +1,11 @@ +# Overview + +Credit Repair Cloud API provides a suite of tools to manage and automate tasks for credit repair businesses. With this API, you can create and manage clients, leads, affiliates, and credit reports, as well as automate the credit repair process. Integrating this API with Pipedream allows you to build serverless workflows that can harness data from Credit Repair Cloud and connect it with other services to enhance your credit repair operations. + +# Example Use Cases + +- **Automated New Client Onboarding**: When a new client signs up using a web form, trigger a Pipedream workflow that automatically creates a new client profile in Credit Repair Cloud. This workflow can also send a welcome email to the client and alert your team via Slack. + +- **Lead Tracking and Follow-Up**: Use Pipedream to monitor new leads in Credit Repair Cloud. When a lead is flagged as ‘hot’, trigger a sequence of follow-up emails and tasks, and log the activity in a CRM like Salesforce for further action by the sales team. + +- **Monthly Credit Report Generation**: Schedule a Pipedream workflow to generate and email credit reports to clients on a monthly basis. This workflow can also update the client’s record in Credit Repair Cloud with the latest report details and perform data analysis by connecting to Google Sheets. diff --git a/components/crimeometer/README.md b/components/crimeometer/README.md new file mode 100644 index 0000000000000..07cce8db12e0c --- /dev/null +++ b/components/crimeometer/README.md @@ -0,0 +1,11 @@ +# Overview + +The CrimeoMeter API provides detailed crime data, allowing users to access crime statistics and safety information across various locations. Utilize this API within Pipedream to integrate real-time crime data into apps, send alerts, or enhance analytics. Pipedream makes it simple to set up event-driven workflows that react to CrimeoMeter's data, enabling both individual users and organizations to act upon crime insights swiftly and automate their safety processes. + +# Example Use Cases + +- **Real-time Crime Alerts**: Create a workflow that triggers whenever the CrimeoMeter API reports a new crime in a specific area. Use Pipedream's built-in HTTP / Webhook trigger to initiate the flow and send alerts through email, SMS, or messaging apps like Slack to keep community members informed. + +- **Daily Crime Summary Reports**: Set up a daily scheduled workflow that pulls the previous day's crime data for a particular location, compiles a summary, and sends it to local authorities or neighborhood watch groups. This could be delivered via email or stored in Google Sheets for easy sharing and analysis. + +- **Dynamic Safety Maps Integration**: Use the CrimeoMeter API to feed data into a mapping tool such as Google Maps. Have a workflow that periodically updates a map with recent crime incidents, allowing users to visualize crime hotspots and plan safer routes. This could be particularly useful for travel planning apps or real estate platforms analyzing neighborhood safety. diff --git a/components/crisp/README.md b/components/crisp/README.md index 0dbc7085965f8..8b666175c7f65 100644 --- a/components/crisp/README.md +++ b/components/crisp/README.md @@ -1,12 +1,11 @@ # Overview -Crisp API lets you buildchatbots and live chat features into your app or -website. With Crisp, you can also manage your customer relationships, helpdesk, -and support team. +The Crisp API allows you to tap into the capabilities of the Crisp Chat platform, which includes managing conversations, updating user information, and handling various customer support interactions programmatically. With Pipedream's integration, you can create bespoke workflows that automate actions based on events in Crisp or connect Crisp with other services to streamline customer engagement and support processes. These automations can help in managing leads, providing real-time support notifications, syncing customer data across platforms, and more. -Here are some things you can build with the Crisp API: +# Example Use Cases -- A chatbot to help customers with their purchase -- A live chat feature to answer customer questions -- A customer support management system -- A helpdesk ticketing system +- **Sync Support Tickets to a CRM**: Automate the creation of tickets in your CRM system like Salesforce or HubSpot whenever a new conversation is started in Crisp. This ensures that every customer interaction is logged and can be followed up on by your sales or support team, keeping all customer-related data in one place. + +- **Real-Time Slack Notifications for High-Priority Issues**: Set up a Pipedream workflow that listens for specific keywords or tags in Crisp conversations and sends an immediate alert to a dedicated Slack channel. This enables your support team to respond quickly to urgent customer issues. + +- **Automate Customer Follow-ups with Email Marketing Tools**: After resolving a conversation in Crisp, trigger a workflow that adds the customer to a follow-up campaign in email marketing platforms like Mailchimp or SendGrid. This can help in nurturing customer relationships and gathering feedback on the support experience. diff --git a/components/cronfree/README.md b/components/cronfree/README.md new file mode 100644 index 0000000000000..7bb68f5fd7ec5 --- /dev/null +++ b/components/cronfree/README.md @@ -0,0 +1,11 @@ +# Overview + +The Cronfree API allows you to schedule tasks to run at specific intervals, similar to a traditional cron job, but with the flexibility of web-based management. On Pipedream, you can harness this API to trigger workflows on a schedule, offload periodic tasks from your apps, and integrate with other services to perform actions like sending emails, updating databases, or even automating social media posts. + +# Example Use Cases + +- **Scheduled Data Backup**: Automate the process of backing up your database to a cloud storage service like Google Drive or Dropbox. Set up a Pipedream workflow that triggers on the schedule you set with Cronfree, executes a database dump, and uploads the resulting file to your chosen storage provider. + +- **Automated Reporting**: Create a Pipedream workflow that generates and sends daily, weekly, or monthly reports. Use the Cronfree API to schedule the workflow to pull data from your analytics service, compile it into a report format, and email it to the necessary stakeholders or save it to a shared location. + +- **Social Media Automation**: Schedule regular social media posts across platforms like Twitter, Facebook, or Instagram. With Cronfree and Pipedream, you can build workflows that post updates or promotional content at optimal times, helping to increase engagement without manual intervention. diff --git a/components/crove_app/README.md b/components/crove_app/README.md index 490a5adda2b91..1dc1c403d8d7c 100644 --- a/components/crove_app/README.md +++ b/components/crove_app/README.md @@ -1,7 +1,11 @@ # Overview -The Crove API allows developers to access and integrate the functionality of -Crove with other applications. +The Crove API offers a suite of tools for automating document creation, customization, and management. Using Pipedream's integration capabilities, you can leverage Crove to generate tailored documents based on dynamic data inputs and trigger actions within other applications. Imagine auto-generating contracts when a deal is marked 'won' in a CRM, or creating personalized onboarding paperwork as soon as a new employee is added to your HR system. With Pipedream, you can connect Crove to a multitude of apps to streamline document workflows and enhance productivity. -Some example API methods include retrieving objects, retrieving and managing -object properties, and managing objects. +# Example Use Cases + +- **Automated Contract Generation on Deal Closure**: When a sales deal is marked as 'won' in a CRM like Salesforce or HubSpot, Pipedream listens to the event via a webhook or an API poll, captures the relevant deal information, and triggers the Crove API to generate a customized contract with the deal details. The generated document is then automatically emailed to the client for signing, cutting down the time to kickoff project execution. + +- **Dynamic Welcome Packets for New Employees**: Once a new employee record is created in an HR platform like BambooHR, Pipedream detects this addition and triggers the Crove API to create a personalized welcome packet. This packet includes essential documents like company policies, benefits summaries, and onboarding schedules, all custom-tailored with the new hire's details. The welcome packet is then sent via email or uploaded to a shared workspace such as Google Drive for easy access. + +- **Custom Invoices Post-Payment Confirmation**: After a payment is confirmed through an e-commerce platform like Shopify, Pipedream captures this payment confirmation and instructs the Crove API to generate a detailed invoice containing itemized purchases, customer information, and branding elements. This invoice can be automatically sent to the customer and stored in accounting software like QuickBooks for financial tracking and reconciliation. diff --git a/components/crowd_dev/README.md b/components/crowd_dev/README.md index 80f66e71ab260..f191d3f901fff 100644 --- a/components/crowd_dev/README.md +++ b/components/crowd_dev/README.md @@ -1,11 +1,11 @@ # Overview - Crowd.dev is a powerful API that enables you to easily build applications that - work with the Crowd.dev platform. With the Crowd.dev API you can: +Crowd.dev is an API that enriches your understanding of your developer community by aggregating data from various platforms like GitHub, GitLab, and more. By connecting to Crowd.dev, you can track community activity, identify top contributors, and gauge the health of your open-source projects. Pipedream simplifies and amplifies the potential of the Crowd.dev API by allowing you to create automated workflows that respond to events in real-time, like new contributions, issues, or discussions. -- Build applications that allow users to post and manage projects -- Build applications that allow users to browse and search for projects -- Build applications that allow users to pledge money to projects -- Build applications that allow users to manage their Crowd.dev account +# Example Use Cases -The possibilities are endless! So what are you waiting for? Get started today! +- **Community Engagement Tracker**: Build a workflow that monitors community contributions across platforms. Whenever a new issue is created or a pull request is merged, trigger an automated thank-you message via Discord or Slack to the contributor, fostering a positive and engaging community. + +- **Weekly Digest Generator**: Construct a workflow that compiles a weekly digest of community activity, including new issues, pull requests, and active discussions. Use the data gathered from Crowd.dev to generate a markdown report and automatically email it to the development team using a service like SendGrid or Mailgun. + +- **Contributor Recognition System**: Develop a system that uses Crowd.dev to track contributions and identify top contributors each month. Set up a workflow to send personalized rewards such as digital gift cards or swag through an integration with fulfillment services or via a custom webhook to your merchandising system. diff --git a/components/crowd_dev/package.json b/components/crowd_dev/package.json new file mode 100644 index 0000000000000..1a4625d2f91a3 --- /dev/null +++ b/components/crowd_dev/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/crowd_dev", + "version": "0.6.0", + "description": "Pipedream crowd_dev Components", + "main": "crowd_dev.app.mjs", + "keywords": [ + "pipedream", + "crowd_dev" + ], + "homepage": "https://pipedream.com/apps/crowd_dev", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/crowdin/.gitignore b/components/crowdin/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/crowdin/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/crowdin/README.md b/components/crowdin/README.md new file mode 100644 index 0000000000000..57b3ec58dbd30 --- /dev/null +++ b/components/crowdin/README.md @@ -0,0 +1,11 @@ +# Overview + +The Crowdin API offers a way to streamline localization processes by enabling automated interactions with Crowdin projects. With this API on Pipedream, you can create workflows that manage translations, coordinate with translators, and sync localized content across your apps. It becomes easy to upload new source files, download translations, manage projects, and crowdsource translations, all within the Pipedream ecosystem. + +# Example Use Cases + +- **Automate Source File Updates**: Automatically upload new source files to your Crowdin project when you push to a GitHub repository. Combine GitHub and Crowdin on Pipedream to keep your localization files in sync with your codebase. + +- **Translation Status Updates**: Set up a workflow to notify your team via Slack whenever a translation reaches a certain completion percentage. This can help in keeping track of project progress and planning release schedules accordingly. + +- **Dynamic Content Localization**: Use the Crowdin API with a CMS like Contentful to fetch new content and push it to Crowdin for translation. Once translated, the workflow can update the CMS with the localized versions, ensuring your website or app content stays current in all languages. diff --git a/components/crowdin/actions/add-file/add-file.mjs b/components/crowdin/actions/add-file/add-file.mjs new file mode 100644 index 0000000000000..b5e7703dc6cd0 --- /dev/null +++ b/components/crowdin/actions/add-file/add-file.mjs @@ -0,0 +1,120 @@ +import fs from "fs"; +import { TYPE_OPTIONS } from "../../common/constants.mjs"; +import { + checkTmp, + parseObject, +} from "../../common/utils.mjs"; +import crowdin from "../../crowdin.app.mjs"; + +export default { + key: "crowdin-add-file", + name: "Add File to Project", + description: "Adds a file into the created project. [See the documentation](https://developer.crowdin.com/api/v2/#tag/source-files/operation/api.projects.files.post)", + version: "0.0.1", + type: "action", + props: { + crowdin, + projectId: { + propDefinition: [ + crowdin, + "projectId", + ], + }, + file: { + type: "string", + label: "File", + description: "The path to the file saved to the `/tmp` directory (e.g. `/tmp/example.jpg`) to process. [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + }, + name: { + type: "string", + label: "Name", + description: "The name of the file in Crowdin. **Note:** Can't contain `\\ / : * ? \" < > |` symbols. `ZIP` files are not allowed.", + }, + branchId: { + propDefinition: [ + crowdin, + "branchId", + (c) => ({ + projectId: c.projectId, + }), + ], + }, + directoryId: { + propDefinition: [ + crowdin, + "directoryId", + (c) => ({ + projectId: c.projectId, + }), + ], + }, + title: { + type: "string", + label: "Title", + description: "Use to provide more details for translators. Title is available in UI only", + optional: true, + }, + context: { + type: "string", + label: "Context", + description: "Use to provide context about whole file", + optional: true, + }, + type: { + type: "string", + label: "File Type", + description: "The type of the file. **Note:** Use `docx` type to import each cell as a separate source string for XLSX file. Default is `auto`", + options: TYPE_OPTIONS, + optional: true, + }, + parserVersion: { + type: "integer", + label: "Parser Version", + description: "Using latest parser version by default. **Note:** Must be used together with `type`.", + optional: true, + }, + attachLabelIds: { + propDefinition: [ + crowdin, + "attachLabelIds", + (c) => ({ + projectId: c.projectId, + }), + ], + }, + }, + async run({ $ }) { + const { + crowdin, + attachLabelIds, + projectId, + file, + ...data + } = this; + + const fileBinary = fs.readFileSync(checkTmp(file)); + const crowdinFilename = file.startsWith("/tmp/") + ? file.slice(5) + : file; + + const fileResponse = await crowdin.createStorage({ + data: Buffer.from(fileBinary, "binary"), + headers: { + "Crowdin-API-FileName": encodeURI(crowdinFilename), + "Content-Type": "application/octet-stream", + }, + }); + + const response = await crowdin.uploadFileToProject({ + $, + projectId, + data: { + ...data, + storageId: fileResponse.data.id, + attachLabelIds: parseObject(attachLabelIds), + }, + }); + $.export("$summary", `Successfully uploaded file: ${this.name}`); + return response; + }, +}; diff --git a/components/crowdin/actions/create-project/create-project.mjs b/components/crowdin/actions/create-project/create-project.mjs new file mode 100644 index 0000000000000..004cd4499cb7f --- /dev/null +++ b/components/crowdin/actions/create-project/create-project.mjs @@ -0,0 +1,184 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { + LANGUAGE_ACCESS_POLICY_OPTIONS, + TAGS_DETECTION_OPTIONS, + VISIBILITY_OPTIONS, +} from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import crowdin from "../../crowdin.app.mjs"; + +export default { + key: "crowdin-create-project", + name: "Create Project", + description: "Creates a new project within Crowdin. [See the documentation](https://support.crowdin.com/developer/api/v2/#/projects-api/create-project)", + version: "0.0.1", + type: "action", + props: { + crowdin, + name: { + type: "string", + label: "Project Name", + description: "The name of the project to be created", + }, + sourceLanguageId: { + propDefinition: [ + crowdin, + "sourceLanguageId", + ], + }, + targetLanguageIds: { + propDefinition: [ + crowdin, + "sourceLanguageId", + ], + type: "string[]", + label: "Target Language IDs", + description: "Array of target language IDs", + optional: true, + }, + identifier: { + type: "string", + label: "Identifier", + description: "A custom identifier for the project", + optional: true, + }, + visibility: { + type: "string", + label: "Visibility", + description: "Defines how users can join the project.", + options: VISIBILITY_OPTIONS, + optional: true, + }, + languageAccessPolicy: { + type: "string", + label: "Language Access Policy", + description: "Defines access to project languages.", + optional: true, + options: LANGUAGE_ACCESS_POLICY_OPTIONS, + }, + cname: { + type: "string", + label: "Custom Domain Name", + description: "Custom domain name for the project.", + optional: true, + }, + description: { + type: "string", + label: "Project Description", + description: "The description of the project.", + optional: true, + }, + tagsDetection: { + type: "string", + label: "Tags Detection", + description: "The type of the tags detection.", + options: TAGS_DETECTION_OPTIONS, + optional: true, + }, + isMtAllowed: { + type: "boolean", + label: "Allow Machine Translation", + description: "Allows machine translations to be visible for translators. Default is **true**.", + optional: true, + }, + taskBasedAccessControl: { + type: "boolean", + label: "Task Based Access Control", + description: "Allow project members to work with tasks they're assigned to. Default is **false**.", + optional: true, + default: false, + }, + autoSubstitution: { + type: "boolean", + label: "Auto Substitution", + description: "Allows auto-substitution. Default is **true**.", + optional: true, + default: true, + }, + autoTranslateDialects: { + type: "boolean", + label: "Auto Translate Dialects", + description: "Automatically fill in regional dialects. Default is **false**.", + optional: true, + }, + publicDownloads: { + type: "boolean", + label: "Public Downloads", + description: "Allows translators to download source files. Default is **true**.", + optional: true, + }, + hiddenStringsProofreadersAccess: { + type: "boolean", + label: "Hidden Strings Proofreaders Access", + description: "Allows proofreaders to work with hidden strings. Default is **true**.", + optional: true, + default: true, + }, + useGlobalTm: { + type: "boolean", + label: "Use Global Translation Memory", + description: "If true, machine translations from connected MT engines will appear as suggestions. Default is **true**.", + optional: true, + }, + showTmSuggestionsDialects: { + type: "boolean", + label: "Show TM Suggestions for Dialects", + description: "Show primary language TM suggestions for dialects if there are no dialect-specific ones. Default is **true**.", + optional: true, + default: true, + }, + skipUntranslatedStrings: { + type: "boolean", + label: "Skip Untranslated Strings", + description: "Defines whether to skip untranslated strings.", + optional: true, + }, + exportApprovedOnly: { + type: "boolean", + label: "Export Approved Only", + description: "Defines whether to export only approved strings.", + optional: true, + }, + qaCheckIsActive: { + type: "boolean", + label: "QA Check Is Active", + description: "If true, QA checks are active. Default is **true**.", + optional: true, + }, + type: { + type: "string", + label: "Type", + description: "Defines the project type. To create a file-based project, use 0.", + options: [ + "0", + "1", + ], + optional: true, + }, + }, + async run({ $ }) { + try { + const { + crowdin, + type, + targetLanguageIds, + tagsDetection, + ...data + } = this; + + const response = await crowdin.createProject({ + $, + data: { + ...data, + type: parseInt(type), + targetLanguageIds: parseObject(targetLanguageIds), + tagsDetection: parseInt(tagsDetection), + }, + }); + $.export("$summary", `Project created successfully with Id: ${response.data.id}`); + return response; + } catch ({ response }) { + throw new ConfigurationError(response.data.errors[0]?.error?.errors[0]?.message); + } + }, +}; diff --git a/components/crowdin/actions/translate-via-machine-translation/translate-via-machine-translation.mjs b/components/crowdin/actions/translate-via-machine-translation/translate-via-machine-translation.mjs new file mode 100644 index 0000000000000..e6f46ed2157bc --- /dev/null +++ b/components/crowdin/actions/translate-via-machine-translation/translate-via-machine-translation.mjs @@ -0,0 +1,60 @@ +import { LANGUAGE_R_PROVIDER_OPTIONS } from "../../common/constants.mjs"; +import crowdin from "../../crowdin.app.mjs"; + +export default { + key: "crowdin-translate-via-machine-translation", + name: "Translate via Machine Translation", + description: "Performs machine translation of the uploaded files. [See the documentation](https://support.crowdin.com/developer/api/v2/)", + version: "0.0.1", + type: "action", + props: { + crowdin, + mtId: { + propDefinition: [ + crowdin, + "mtId", + ], + }, + targetLanguageId: { + propDefinition: [ + crowdin, + "sourceLanguageId", + ], + type: "string", + label: "Target Language ID", + description: "The language ID for the target translation language", + }, + languageRecognitionProvider: { + type: "string", + label: "Language Recognition Provider", + description: "Select a provider for language recognition **Note:** Is required if the source language is not selected", + options: LANGUAGE_R_PROVIDER_OPTIONS, + }, + sourceLanguageId: { + propDefinition: [ + crowdin, + "sourceLanguageId", + ], + }, + strings: { + type: "string[]", + label: "Strings", + description: "Array of strings to be translated. **Note:** You can translate up to 100 strings at a time.", + }, + }, + async run({ $ }) { + const response = await this.crowdin.performMachineTranslation({ + $, + mtId: this.mtId, + data: { + targetLanguageId: this.targetLanguageId, + strings: this.strings, + languageRecognitionProvider: this.languageRecognitionProvider, + sourceLanguageId: this.sourceLanguageId, + }, + }); + + $.export("$summary", "Successfully performed machine translation"); + return response; + }, +}; diff --git a/components/crowdin/app/crowdin.app.ts b/components/crowdin/app/crowdin.app.ts deleted file mode 100644 index 680d55b717589..0000000000000 --- a/components/crowdin/app/crowdin.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "crowdin", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/crowdin/common/constants.mjs b/components/crowdin/common/constants.mjs new file mode 100644 index 0000000000000..21964b4eb113b --- /dev/null +++ b/components/crowdin/common/constants.mjs @@ -0,0 +1,266 @@ +export const LIMIT = 500; + +export const VISIBILITY_OPTIONS = [ + { + label: "Open - anyone can join the project", + value: "open", + }, + { + label: "Private - only invited users can join the project", + value: "private", + }, +]; + +export const LANGUAGE_ACCESS_POLICY_OPTIONS = [ + { + label: "Open - each project user can access all project languages", + value: "open", + }, + { + label: "Moderate - users should join each project language separately", + value: "moderate", + }, +]; + +export const TAGS_DETECTION_OPTIONS = [ + { + label: "Auto", + value: "0", + }, + { + label: "Count Tags", + value: "1", + }, + { + label: "Skip Tags", + value: "2", + }, +]; + +export const LANGUAGE_R_PROVIDER_OPTIONS = [ + "crowdin", + "engine", +]; + +export const TYPE_OPTIONS = [ + { + label: "Try to detect file type by extension or MIME type", + value: "auto", + }, + { + label: "Android (*.xml)", + value: "android", + }, + { + label: "Mac OS X / iOS (*.strings)", + value: "macosx", + }, + { + label: ".NET, Windows Phone (*.resx)", + value: "resx", + }, + { + label: "Java (*.properties)", + value: "properties", + }, + { + label: "GNU GetText (*.po, *.pot)", + value: "gettext", + }, + { + label: "Ruby On Rails (*.yaml, *.yml)", + value: "yaml", + }, + { + label: "Hypertext Preprocessor (*.php)", + value: "php", + }, + { + label: "Generic JSON (*.json)", + value: "json", + }, + { + label: "Generic XML (*.xml)", + value: "xml", + }, + { + label: "Generic INI (*.ini)", + value: "ini", + }, + { + label: "Windows Resources (*.rc)", + value: "rc", + }, + { + label: "Windows 8 Metro (*.resw)", + value: "resw", + }, + { + label: "Windows 8 Metro (*.resjson)", + value: "resjson", + }, + { + label: "Nokia Qt (*.ts)", + value: "qtts", + }, + { + label: "Joomla localizable resources (*.ini)", + value: "joomla", + }, + { + label: "Google Chrome Extension (*.json)", + value: "chrome", + }, + { + label: "Mozilla DTD (*.dtd)", + value: "dtd", + }, + { + label: "Delphi DKLang (*.dklang)", + value: "dklang", + }, + { + label: "Flex (*.properties)", + value: "flex", + }, + { + label: "NSIS Installer Resources (*.nsh)", + value: "nsh", + }, + { + label: "WiX Installer (*.wxl)", + value: "wxl", + }, + { + label: "XLIFF (*.xliff, *.xlf)", + value: "xliff", + }, + { + label: "XLIFF 2.0 (*.xliff, *.xlf)", + value: "xliff_two", + }, + { + label: "HTML (*.html, *.htm, *.xhtml, *.xhtm, *.xht, *.hbs, *.liquid)", + value: "html", + }, + { + label: "Haml (*.haml)", + value: "haml", + }, + { + label: "Plain Text (*.txt)", + value: "txt", + }, + { + label: "Comma Separated Values (*.csv)", + value: "csv", + }, + { + label: "Markdown (*.md, *.text, *.markdown...)", + value: "md", + }, + { + label: "MadCap Flare (*.flnsp, .flpgpl .fltoc)", + value: "flsnp", + }, + { + label: "Jekyll HTML (*.html)", + value: "fm_html", + }, + { + label: "Jekyll Markdown (*.md)", + value: "fm_md", + }, + { + label: "MediaWiki (*.wiki, *.wikitext, *.mediawiki)", + value: "mediawiki", + }, + { + label: "Microsoft Office, OpenOffice.org Documents, Adobe InDesign, Adobe FrameMaker(*.docx, *.dotx, *.docm, *.dotm, *.xlsx, *.xltx, *.xlsm, *.xltm, *.pptx, *.potx, *.ppsx, *.pptm, *.potm, *.ppsm, *.odt, *.ods, *.odg, *.odp, *.imdl, *.mif)", + value: "docx", + }, + { + label: "Microsoft Excel (*.xlsx)", + value: "xlsx", + }, + { + label: "Youtube .sbv (*.sbv)", + value: "sbv", + }, + { + label: "Play Framework", + value: "properties_play", + }, + { + label: "Java Application (*.xml)", + value: "properties_xml", + }, + { + label: "Maxthon Browser (*.ini)", + value: "maxthon", + }, + { + label: "Go (*.gotext.json)", + value: "go_json", + }, + { + label: "DITA Document (*.dita, *.ditamap)", + value: "dita", + }, + { + label: "Adobe FrameMaker (*.mif)", + value: "mif", + }, + { + label: "Adobe InDesign (*.idml)", + value: "idml", + }, + { + label: "iOS (*.stringsdict)", + value: "stringsdict", + }, + { + label: "Mac OS property list (*.plist)", + value: "plist", + }, + { + label: "Video Subtitling and WebVTT (*.vtt)", + value: "vtt", + }, + { + label: "Steamworks Localization Valve Data File (*.vdf)", + value: "vdf", + }, + { + label: "SubRip .srt (*.srt)", + value: "srt", + }, + { + label: "Salesforce (*.stf)", + value: "stf", + }, + { + label: "Toml (*.toml)", + value: "toml", + }, + { + label: "Contentful (*.json)", + value: "contentful_rt", + }, + { + label: "SVG (*.svg)", + value: "svg", + }, + { + label: "JavaScript (*.js)", + value: "js", + }, + { + label: "CoffeeScript (*.coffee)", + value: "coffee", + }, + { + label: "NestJS i18n", + value: "nestjs_i18n", + }, +]; diff --git a/components/crowdin/common/utils.mjs b/components/crowdin/common/utils.mjs new file mode 100644 index 0000000000000..32b6f5a875df7 --- /dev/null +++ b/components/crowdin/common/utils.mjs @@ -0,0 +1,31 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; + +export const checkTmp = (filename) => { + if (filename.indexOf("/tmp") === -1) { + return `/tmp/${filename}`; + } + return filename; +}; diff --git a/components/crowdin/crowdin.app.mjs b/components/crowdin/crowdin.app.mjs new file mode 100644 index 0000000000000..2829d52aa45d9 --- /dev/null +++ b/components/crowdin/crowdin.app.mjs @@ -0,0 +1,306 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "crowdin", + propDefinitions: { + projectId: { + type: "string", + label: "Project ID", + description: "The ID of the project", + async options({ page }) { + const { data } = await this.listProjects({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + return data.map(({ + data: { + id: value, name: label, + }, + }) => ({ + label, + value, + })); + }, + }, + directoryId: { + type: "string", + label: "Directory ID", + description: "The ID of the directory. **Note:** Can't be used with `Branch Id` in same request", + optional: true, + async options({ + page, projectId, + }) { + const { data } = await this.listDirectories({ + projectId, + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + return data.map(({ + data: { + id: value, name: label, + }, + }) => ({ + label, + value, + })); + }, + }, + sourceLanguageId: { + type: "string", + label: "Source Language ID", + description: "The language ID of the source language", + async options({ page }) { + const { data } = await this.listSupportedLanguages({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + return data.map(({ + data: { + id: value, name: label, + }, + }) => ({ + label, + value, + })); + }, + }, + storageId: { + type: "integer", + label: "Storage ID", + description: "The ID of the storage", + async options({ page }) { + const { data } = await this.listStorages({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + return data.map(({ + data: { + id: value, fileName: label, + }, + }) => ({ + label, + value, + })); + }, + }, + branchId: { + type: "string", + label: "Branch ID", + description: "Defines branch to which file will be added. **Note:** Can't be used with `Directory Id` in same request", + optional: true, + async options({ + page, projectId, + }) { + const { data } = await this.listBranches({ + projectId, + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + return data.map(({ + data: { + id: value, name, title, + }, + }) => ({ + label: `${title} - ${name}`, + value, + })); + }, + }, + attachLabelIds: { + type: "string[]", + label: "Attach Label IDs", + description: "The IDs of the labels to attach", + optional: true, + async options({ + page, projectId, + }) { + const { data } = await this.listLabels({ + projectId, + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + return data.map(({ + data: { + id: value, title: label, + }, + }) => ({ + label, + value, + })); + }, + }, + mtId: { + type: "string", + label: "Machine Translation ID", + description: "The ID of the machine translation engine", + async options({ page }) { + const { data } = await this.listMTs({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + return data.map(({ + data: { + id: value, name: label, + }, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.crowdin.com/api/v2"; + }, + _headers(headers = {}) { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(headers), + ...opts, + }); + }, + listProjects(opts = {}) { + return this._makeRequest({ + path: "/projects", + ...opts, + }); + }, + listDirectories({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/projects/${projectId}/directories`, + ...opts, + }); + }, + listSupportedLanguages(opts = {}) { + return this._makeRequest({ + path: "/languages", + ...opts, + }); + }, + listStorages(opts = {}) { + return this._makeRequest({ + path: "/storages", + ...opts, + }); + }, + listBranches({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/projects/${projectId}/branches`, + ...opts, + }); + }, + listLabels({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/projects/${projectId}/labels`, + ...opts, + }); + }, + listMTs(opts = {}) { + return this._makeRequest({ + path: "/mts", + ...opts, + }); + }, + createProject(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/projects", + ...opts, + }); + }, + createStorage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/storages", + ...opts, + }); + }, + uploadFileToProject({ + projectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/projects/${projectId}/files`, + ...opts, + }); + }, + performMachineTranslation({ + mtId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/mts/${mtId}/translations`, + ...opts, + }); + }, + createWebhook({ + projectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/projects/${projectId}/webhooks`, + ...opts, + }); + }, + deleteWebhook({ + projectId, webhookId, + }) { + return this._makeRequest({ + method: "DELETE", + path: `/projects/${projectId}/webhooks/${webhookId}`, + }); + }, + async *paginate({ + fn, params = {}, ...opts + }) { + let hasMore = false; + let page = 0; + + do { + params.limit = LIMIT; + params.offset = LIMIT * page++; + const { data } = await fn({ + params, + ...opts, + }); + for (const d of data) { + yield d; + } + + hasMore = data.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/crowdin/package.json b/components/crowdin/package.json index 33034ea5d16a9..2b917337bf538 100644 --- a/components/crowdin/package.json +++ b/components/crowdin/package.json @@ -1,16 +1,19 @@ { "name": "@pipedream/crowdin", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream Crowdin Components", - "main": "dist/app/crowdin.app.mjs", + "main": "crowdin.app.mjs", "keywords": [ "pipedream", "crowdin" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/crowdin", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "fs": "^0.0.1-security" } } diff --git a/components/crowdin/sources/common/base-instant.mjs b/components/crowdin/sources/common/base-instant.mjs new file mode 100644 index 0000000000000..13341ebf7cc50 --- /dev/null +++ b/components/crowdin/sources/common/base-instant.mjs @@ -0,0 +1,68 @@ +import crowdin from "../../crowdin.app.mjs"; + +export default { + props: { + crowdin, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + name: { + type: "string", + label: "Name", + description: "The webhook name.", + }, + projectId: { + propDefinition: [ + crowdin, + "projectId", + ], + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getExtraData() { + return {}; + }, + }, + hooks: { + async activate() { + const response = await this.crowdin.createWebhook({ + projectId: this.projectId, + data: { + name: this.name, + url: this.http.endpoint, + events: this.getEvents(), + requestType: "POST", + }, + }); + this._setHookId(response.data.id); + }, + async deactivate() { + const webhookId = this._getHookId(); + await this.crowdin.deleteWebhook({ + projectId: this.projectId, + webhookId, + }); + }, + }, + async run({ body }) { + + this.http.respond({ + status: 200, + }); + + const ts = Date.parse(new Date()); + this.$emit(body, { + id: `${body.comment?.id || ts}`, + summary: this.getSummary(body), + ts: ts, + }); + }, +}; diff --git a/components/crowdin/sources/common/base.mjs b/components/crowdin/sources/common/base.mjs new file mode 100644 index 0000000000000..9709d575c5d06 --- /dev/null +++ b/components/crowdin/sources/common/base.mjs @@ -0,0 +1,66 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import crowdin from "../../crowdin.app.mjs"; + +export default { + props: { + crowdin, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + projectId: { + propDefinition: [ + crowdin, + "projectId", + ], + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + async emitEvent(maxResults = false) { + const lastId = this._getLastId(); + + const response = this.crowdin.paginate({ + projectId: this.projectId, + ...this.getArgs(), + }); + + let responseArray = []; + for await (const item of response) { + if (item.data.id <= lastId) break; + responseArray.push(item.data); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastId(responseArray[0].id); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item.createdAt), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/crowdin/sources/file-approved-instant/file-approved-instant.mjs b/components/crowdin/sources/file-approved-instant/file-approved-instant.mjs new file mode 100644 index 0000000000000..1672800d09e5a --- /dev/null +++ b/components/crowdin/sources/file-approved-instant/file-approved-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base-instant.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "crowdin-file-approved-instant", + name: "New File Approved (Instant)", + description: "Emit new event when a file is fully translated and approved.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "project.approved", + ]; + }, + getSummary(body) { + return `File approved for project ID: ${body.project.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/crowdin/sources/file-approved-instant/test-event.mjs b/components/crowdin/sources/file-approved-instant/test-event.mjs new file mode 100644 index 0000000000000..bc81fc2ff5f3e --- /dev/null +++ b/components/crowdin/sources/file-approved-instant/test-event.mjs @@ -0,0 +1,36 @@ +export default { + "event": "project.approved", + "project": { + "id": "123123", + "userId": "123123", + "sourceLanguageId": "en", + "targetLanguageIds": [ + "ru", + "pt-BR" + ], + "identifier": "project-01", + "name": "Project 01", + "createdAt": "2024-10-28T13:54:03+00:00", + "updatedAt": "2024-10-30T14:59:43+00:00", + "lastActivity": "2024-10-30T16:35:02+00:00", + "description": "", + "url": "https://crowdin.com/project/project-01", + "cname": null, + "languageAccessPolicy": "open", + "visibility": "private", + "publicDownloads": true + }, + "targetLanguage": { + "id": "en", + "name": "English", + "editorCode": "en", + "twoLettersCode": "en", + "threeLettersCode": "eng", + "locale": "en", + "androidCode": "en", + "osxCode": "en.lproj", + "osxLocale": "en", + "textDirection": "ltr", + "dialectOf": null + } +} \ No newline at end of file diff --git a/components/crowdin/sources/new-comment-issue-instant/new-comment-issue-instant.mjs b/components/crowdin/sources/new-comment-issue-instant/new-comment-issue-instant.mjs new file mode 100644 index 0000000000000..17098ecb6a80d --- /dev/null +++ b/components/crowdin/sources/new-comment-issue-instant/new-comment-issue-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base-instant.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "crowdin-new-comment-issue-instant", + name: "New Comment or Issue Added (Instant)", + description: "Emit new event when a user adds a comment or an issue in Crowdin.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "stringComment.created", + ]; + }, + getSummary(body) { + return `New comment or issue in project: ${body.comment.string.project.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/crowdin/sources/new-comment-issue-instant/test-event.mjs b/components/crowdin/sources/new-comment-issue-instant/test-event.mjs new file mode 100644 index 0000000000000..18737adfb28e4 --- /dev/null +++ b/components/crowdin/sources/new-comment-issue-instant/test-event.mjs @@ -0,0 +1,91 @@ +export default { + "event": "stringComment.created", + "comment": { + "id": "4", + "text": "text 01", + "type": "issue", + "issueType": "translation_mistake", + "issueStatus": "unresolved", + "resolvedAt": null, + "createdAt": "2024-10-30T17:06:06+00:00", + "string": { + "id": "2", + "identifier": "5ad6c6099c2bc7211eb8151483826e80", + "key": "NFDBB2FA9", + "text": "Text file", + "type": "text", + "context": "NFDBB2FA9", + "maxLength": "0", + "isHidden": false, + "isDuplicate": false, + "masterStringId": null, + "revision": "1", + "hasPlurals": false, + "labelIds": [], + "url": "https://crowdin.com/editor/project-01/14/en-ptbr#2", + "createdAt": "2024-10-28T14:18:30+00:00", + "updatedAt": null, + "file": { + "id": "14", + "name": "file.docx", + "title": null, + "type": "docx13", + "path": "/Folder 01/dummy.docx", + "status": "active", + "revision": "1", + "branchId": null, + "directoryId": "2" + }, + "project": { + "id": "123123", + "userId": "123123", + "sourceLanguageId": "en", + "targetLanguageIds": [ + "ru", + "pt-BR", + "yi", + "am", + "es-PR", + "es-NI", + "ar-YE" + ], + "identifier": "project-01", + "name": "Project 01", + "createdAt": "2024-10-28T13:54:03+00:00", + "updatedAt": "2024-10-30T16:40:17+00:00", + "lastActivity": "2024-10-30T17:03:02+00:00", + "description": "", + "url": "https://crowdin.com/project/project-01", + "cname": null, + "languageAccessPolicy": "open", + "visibility": "private", + "publicDownloads": true + } + }, + "targetLanguage": { + "id": "pt-BR", + "name": "Portuguese, Brazilian", + "editorCode": "ptbr", + "twoLettersCode": "pt", + "threeLettersCode": "por", + "locale": "pt-BR", + "androidCode": "pt-rBR", + "osxCode": "pt-BR.lproj", + "osxLocale": "pt_BR", + "textDirection": "ltr", + "dialectOf": null + }, + "user": { + "id": "16645627", + "username": "username", + "fullName": "username", + "avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/123123/small/f1fb36bf9de3ded05b455e88ac9c77cf_default.png" + }, + "commentResolver": { + "id": null, + "username": null, + "fullName": null, + "avatarUrl": null + } + } +} \ No newline at end of file diff --git a/components/crowdin/sources/new-directory/new-directory.mjs b/components/crowdin/sources/new-directory/new-directory.mjs new file mode 100644 index 0000000000000..63a5600c94051 --- /dev/null +++ b/components/crowdin/sources/new-directory/new-directory.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "crowdin-new-directory", + name: "New Directory Created", + description: "Emit new event when a new directory is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getArgs() { + return { + fn: this.crowdin.listDirectories, + params: { + orderBy: "createdAt desc", + }, + }; + }, + getSummary(item) { + return `New Directory: ${item.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/crowdin/sources/new-directory/test-event.mjs b/components/crowdin/sources/new-directory/test-event.mjs new file mode 100644 index 0000000000000..9b19e5716e62c --- /dev/null +++ b/components/crowdin/sources/new-directory/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "data": { + "id": 4, + "projectId": 2, + "branchId": 34, + "directoryId": null, + "name": "main", + "title": "", + "exportPattern": "/localization/%locale%/%file_name%", + "path": "/main", + "priority": "normal", + "createdAt": "2019-09-19T14:14:00+00:00", + "updatedAt": "2019-09-19T14:14:00+00:00" + } +} \ No newline at end of file diff --git a/components/crowdpower/README.md b/components/crowdpower/README.md new file mode 100644 index 0000000000000..776791bd5546c --- /dev/null +++ b/components/crowdpower/README.md @@ -0,0 +1,11 @@ +# Overview + +The CrowdPower API allows you to automate and streamline customer engagement by creating personalized campaigns and actions based on user behavior. By integrating with Pipedream, you can leverage its serverless platform to connect CrowdPower with hundreds of other apps and services, building workflows that respond to events in real-time. This can optimize how you target customers, manage campaigns, and analyze customer data without manual intervention. + +# Example Use Cases + +- **Sync Customer Data Across Platforms**: Automatically update customer profiles in your CRM like Salesforce or HubSpot when a user completes an action in CrowdPower, ensuring your sales team has the most current information. + +- **Dynamic Email Campaigns**: Trigger email sequences in email marketing platforms such as Mailchimp or SendGrid when specific conditions are met in CrowdPower, like a user reaching a particular milestone or making a purchase, to keep your engagement relevant and timely. + +- **Real-time Analytics Notifications**: Send custom Slack or Discord notifications to your team whenever a new campaign is launched or when key performance indicators within CrowdPower reach certain thresholds, enabling prompt analysis and decision making. diff --git a/components/crunchbase/README.md b/components/crunchbase/README.md index c5714917b9982..0b8b106d6bcea 100644 --- a/components/crunchbase/README.md +++ b/components/crunchbase/README.md @@ -1,11 +1,11 @@ # Overview -You can use the Crunchbase API to - -- Get information on startups, including recent news and funding rounds -- Get information on VC firms and investors, including recent news and - investments -- Get information on people in the startup ecosystem, including their - background and current roles -- Get information on acquisition and exits -- Search for companies, people, and news +The Crunchbase API grants access to a trove of information on companies, investors, and the key players steering them. With Pipedream, you can harness this data to enrich CRM leads, track investments, or automate market research. By marrying the Crunchbase API with Pipedream's serverless platform, you unlock a realm of possibilities, creating workflows that respond dynamically to events like funding rounds or acquisitions, or that periodically aggregate data for analysis. + +# Example Use Cases + +- **Lead Enrichment in Salesforce**: Sync company data from Crunchbase to Salesforce leads. When a company in your lead database gets new funding, use Pipedream to update the lead's profile with the latest financial data and trigger a follow-up task in Salesforce. + +- **Investment Alerts via Email**: Monitor specific sectors or companies for new funding rounds. Set up a Pipedream workflow that checks Crunchbase for updates and sends an email alert via SendGrid to your investment team with curated insights and details about the latest funding events. + +- **Market Trends Dashboard**: Populate a Google Sheets dashboard with market trends data. Use Pipedream to periodically pull the latest industry-specific data from Crunchbase and push it to Google Sheets, keeping your dashboard updated with fresh insights for strategic decision-making. diff --git a/components/crunchbase/actions/get-organization/get-organization.mjs b/components/crunchbase/actions/get-organization/get-organization.mjs new file mode 100644 index 0000000000000..cc5cfd29ca300 --- /dev/null +++ b/components/crunchbase/actions/get-organization/get-organization.mjs @@ -0,0 +1,27 @@ +import crunchbase from "../../crunchbase.app.mjs"; + +export default { + key: "crunchbase-get-organization", + name: "Get Organization Details", + description: "Retrieve details about an organization. [See the documentation](https://data.crunchbase.com/reference/get_data-entities-organizations-entity-id)", + version: "0.0.1", + type: "action", + props: { + crunchbase, + entityId: { + propDefinition: [ + crunchbase, + "entityId", + ], + }, + }, + async run({ $ }) { + const response = await this.crunchbase.getOrganizationDetails({ + $, + entityId: this.entityId, + }); + + $.export("$summary", `Successfully retrieved details for organization with ID ${this.entityId}`); + return response; + }, +}; diff --git a/components/crunchbase/actions/search-organizations/search-organizations.mjs b/components/crunchbase/actions/search-organizations/search-organizations.mjs new file mode 100644 index 0000000000000..e6400c11154f7 --- /dev/null +++ b/components/crunchbase/actions/search-organizations/search-organizations.mjs @@ -0,0 +1,37 @@ +import { FIELDS_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import crunchbase from "../../crunchbase.app.mjs"; + +export default { + key: "crunchbase-search-organizations", + name: "Search Organizations", + description: "Search for organizations based on specified criteria. [See the documentation](https://data.crunchbase.com/reference/post_data-searches-organizations)", + version: "0.0.1", + type: "action", + props: { + crunchbase, + fieldIds: { + type: "string[]", + label: "Field IDs", + description: "Fields to include on the resulting entity", + options: FIELDS_OPTIONS, + }, + query: { + type: "string[]", + label: "Query", + description: "Array of objects for searching organizations. `Format example: { \"operator_id\": \"eq\", \"type\": \"predicate\", \"values\": [ \"Citi\" ], \"field_id\": \"name\"}` [See the documentation](https://data.crunchbase.com/reference/post_data-searches-organizations) for further information.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.crunchbase.searchOrganizations({ + $, + data: { + field_ids: this.fieldIds, + query: parseObject(this.query), + }, + }); + $.export("$summary", "Successfully searched organizations"); + return response; + }, +}; diff --git a/components/crunchbase/common/constants.mjs b/components/crunchbase/common/constants.mjs new file mode 100644 index 0000000000000..00e004bbc322b --- /dev/null +++ b/components/crunchbase/common/constants.mjs @@ -0,0 +1,7 @@ +export const LIMIT = 100; +export const FIELDS_OPTIONS = [ + "identifier", + "short_description", + "name", + "website_url", +]; diff --git a/components/crunchbase/common/utils.mjs b/components/crunchbase/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/crunchbase/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/crunchbase/crunchbase.app.mjs b/components/crunchbase/crunchbase.app.mjs index 16bfdbec8a348..247848bdb6521 100644 --- a/components/crunchbase/crunchbase.app.mjs +++ b/components/crunchbase/crunchbase.app.mjs @@ -1,11 +1,71 @@ +import { axios } from "@pipedream/platform"; +import { + FIELDS_OPTIONS, LIMIT, +} from "./common/constants.mjs"; + export default { type: "app", app: "crunchbase", - propDefinitions: {}, + propDefinitions: { + entityId: { + type: "string", + label: "Entity ID", + description: "UUID or permalink of the desired entity", + async options({ prevContext }) { + const { entities } = await this.searchOrganizations({ + data: { + field_ids: FIELDS_OPTIONS, + limit: LIMIT, + after_id: prevContext.lastId, + }, + }); + + return { + options: entities.map(({ + uuid, properties: { name }, + }) => ({ + label: name, + value: uuid, + })), + context: { + lastId: entities[entities.length - 1].uuid, + }, + }; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.crunchbase.com/v4/data"; + }, + _headers() { + return { + "X-cb-user-key": this.$auth.user_key, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + getOrganizationDetails({ + entityId, ...opts + }) { + return this._makeRequest({ + path: `/entities/organizations/${entityId}`, + ...opts, + }); + }, + searchOrganizations(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/searches/organizations", + ...opts, + }); }, }, }; diff --git a/components/crunchbase/package.json b/components/crunchbase/package.json new file mode 100644 index 0000000000000..359c6859e8775 --- /dev/null +++ b/components/crunchbase/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/crunchbase", + "version": "0.1.0", + "description": "Pipedream Crunchbase Components", + "main": "crunchbase.app.mjs", + "keywords": [ + "pipedream", + "crunchbase" + ], + "homepage": "https://pipedream.com/apps/crunchbase", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} + diff --git a/components/cryptowatch/README.md b/components/cryptowatch/README.md index 1c9eb69ec6f72..83fa3763a9c52 100644 --- a/components/cryptowatch/README.md +++ b/components/cryptowatch/README.md @@ -1,8 +1,11 @@ # Overview -You can use the Cryptowatch API to: +The Cryptowatch API offers real-time cryptocurrency market data across multiple exchanges, providing an extensive dataset for traders and developers. With Pipedream, you can harness this data to create automated workflows that react to market changes, analyze trends, or synchronize data across platforms. Whether you're automating trade strategies, alerting on price movements, or consolidating market analysis, Pipedream's serverless platform facilitates rapid development and execution of these tasks without managing infrastructure. -- Get real-time market data for various cryptocurrencies -- Get OHLCV (open, high, low, close, volume) data for various cryptocurrencies -- Get order book data for various cryptocurrencies -- Get trade data for various cryptocurrencies +# Example Use Cases + +- **Real-Time Price Alerts**: Trigger a notification via email, SMS, or communication platforms like Slack when a cryptocurrency reaches a certain price. Using Pipedream's workflow, one can monitor price changes in real-time with the Cryptowatch API and send alerts through integrated services like Twilio or SendGrid. + +- **Automated Trading Strategy**: Execute trades on a connected exchange when certain market conditions are met. A Pipedream workflow could poll Cryptowatch for specific market indicators or price thresholds and use the exchange's API (e.g., Binance, Coinbase) to place trades automatically, ensuring timely market entry or exit. + +- **Portfolio Valuation Sync**: Update a Google Sheet or Airtable with the latest portfolio values by fetching current prices from Cryptowatch. A scheduled Pipedream workflow can regularly pull data for your assets and update the cells in your spreadsheet, keeping your portfolio valuation up to date with minimal effort. diff --git a/components/cryptowatch/package.json b/components/cryptowatch/package.json new file mode 100644 index 0000000000000..b9684ea3ab8b4 --- /dev/null +++ b/components/cryptowatch/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/cryptowatch", + "version": "0.6.0", + "description": "Pipedream cryptowatch Components", + "main": "cryptowatch.app.mjs", + "keywords": [ + "pipedream", + "cryptowatch" + ], + "homepage": "https://pipedream.com/apps/cryptowatch", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/cufinder/README.md b/components/cufinder/README.md new file mode 100644 index 0000000000000..2541603c80248 --- /dev/null +++ b/components/cufinder/README.md @@ -0,0 +1,11 @@ +# Overview + +The CUFinder API offers a way to tap into company data and insights, enabling users to enrich their databases with valuable information on businesses. In Pipedream, you can leverage this API to automate tasks that require company data, such as lead generation, market analysis, or enhancing customer profiles. Pipedream's serverless platform allows you to integrate CUFinder with other apps to create seamless workflows without the hassle of managing infrastructure. + +# Example Use Cases + +- **Enrich CRM Contacts**: Automatically enrich contact information in your CRM whenever new contacts are added. Use the CUFinder API to fetch detailed company data and update each contact with this info in apps like Salesforce or HubSpot. + +- **Lead Qualification Automation**: Streamline your lead qualification process by setting up a Pipedream workflow that uses CUFinder to score and prioritize leads based on company size, industry, or other criteria, ensuring your sales team focuses on high-value prospects. + +- **Market Research Digest**: Build a daily or weekly digest of market trends by pulling data from CUFinder. Combine it with news feeds or other market indicators and send a consolidated report to Slack or via email to keep your team informed. diff --git a/components/cults/README.md b/components/cults/README.md new file mode 100644 index 0000000000000..733aab676809a --- /dev/null +++ b/components/cults/README.md @@ -0,0 +1,11 @@ +# Overview + +Cults is a digital marketplace for 3D printing models. The Cults API allows you to interact with their platform programmatically. With the API, you can retrieve model data, search for designs, or get information about designers and makes. By leveraging Pipedream, you can automate workflows that react to new model uploads, filter searches, or integrate with other services like social media or notification systems. Pipedream's serverless platform provides an event-driven approach to execute code in response to Cults API events and connect to hundreds of other services with minimal setup. + +# Example Use Cases + +- **Automated Tweet of New 3D Models**: When a new model is uploaded to Cults that matches certain criteria (e.g., category, tags, or popularity), trigger a workflow that automatically posts a tweet about the model with a link to drive traffic or awareness. + +- **Daily Digest of New Designs**: Compile a daily digest of new 3D designs uploaded to Cults in a specific category and send the list via email, Slack, or Discord. Use this to keep a community or customer base informed about the latest models available for 3D printing. + +- **Sync New Models to a Database**: Automatically add new 3D model listings from Cults to a database or a Google Sheet for inventory tracking, price monitoring, or creating a curated repository. This can be useful for retailers or enthusiasts who want to maintain a database of models. diff --git a/components/cults/package.json b/components/cults/package.json index 2270dffb54507..c0f24cedc1565 100644 --- a/components/cults/package.json +++ b/components/cults/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/curated/README.md b/components/curated/README.md index 8fe8052a3461b..b2aa5a47d8f45 100644 --- a/components/curated/README.md +++ b/components/curated/README.md @@ -1,10 +1,11 @@ # Overview -With the Curated API, you can build a variety of applications that can help you -curate content from the web. Here are some examples of what you can build: - -- A content curation tool that allows you to collect and organize content from - the web -- A social news aggregator that allows you to see what's trending on the web -- A content discovery engine that helps you find new and interesting content - from around the web +The Curated API enables you to automate the management of your newsletters. With it, you can create issues, manage subscribers, and curate content, all programmatically. This allows for rich integrations with other tools and systems, offering opportunities to streamline your newsletter operations. Using Pipedream, you can harness this functionality to set up custom workflows, trigger actions based on various events, and connect Curated with a plethora of other services to enhance your newsletter productivity. + +# Example Use Cases + +- **Automated Newsletter Issue Creation**: Trigger a workflow on Pipedream that automatically creates a new newsletter issue in Curated whenever a new blog post is published on your website, using the RSS by Zapier app to monitor the blog’s RSS feed. + +- **Subscriber Management Automation**: Keep your Curated subscriber list in sync with your CRM platform. Set up a Pipedream workflow that listens for new contacts in Salesforce (or another CRM) and automatically adds them as subscribers to your selected Curated list. + +- **Content Curation from Social Media**: Curate content directly from your social media activity. Build a Pipedream workflow that collects your liked tweets or favorited items on other platforms (using Twitter app for likes) and compiles them in Curated, ready to be included in your next newsletter. diff --git a/components/currencyapi/README.md b/components/currencyapi/README.md new file mode 100644 index 0000000000000..a72fa808677c4 --- /dev/null +++ b/components/currencyapi/README.md @@ -0,0 +1,11 @@ +# Overview + +The CurrencyAPI on Pipedream allows you to access real-time, historical, and fluctuation data for various world currencies. By leveraging this API, you can automate currency conversion tasks, monitor exchange rates, and integrate financial data into your applications or workflows. With Pipedream's serverless platform, these tasks can run on schedules, be triggered by events, and connect seamlessly with hundreds of other services. + +# Example Use Cases + +- **Automated Currency Conversion for E-Commerce:** Streamline your international sales by creating a workflow that automatically updates product prices in your e-commerce platform based on the latest exchange rates. Connect CurrencyAPI with Shopify or WooCommerce to ensure your pricing reflects current currency market trends. + +- **Financial Reporting:** Generate financial reports by building a workflow that fetches historical exchange rates from CurrencyAPI. Combine this with Google Sheets or Excel to aggregate and visualize financial data, making it easier to understand currency impacts on your business's financial health. + +- **Exchange Rate Alerts:** Create a system that monitors currency fluctuations and sends alerts via email, SMS, or messaging platforms such as Slack when certain thresholds are crossed. By connecting CurrencyAPI with Twilio or SendGrid, you can keep yourself or your team informed about significant market movements. diff --git a/components/currencyscoop/README.md b/components/currencyscoop/README.md new file mode 100644 index 0000000000000..0950e4806baca --- /dev/null +++ b/components/currencyscoop/README.md @@ -0,0 +1,11 @@ +# Overview + +The CurrencyScoop API offers real-time and historical exchange rate data, as well as currency conversion and time-series data. By integrating with Pipedream, users can create automated workflows that react to currency fluctuations, update pricing in their apps, or generate financial reports. Pipedream's serverless architecture enables running these workflows on a schedule or in response to certain triggers without managing any infrastructure. + +# Example Use Cases + +- **Automated Currency Alert System**: Set up a Pipedream workflow that monitors specific currency pairs and sends alerts via email or Slack when exchange rates hit certain thresholds. This can help businesses and individuals make timely financial decisions. + +- **Dynamic Pricing Model**: Implement a workflow that adjusts the prices of products or services in an eCommerce platform by pulling the latest exchange rates from CurrencyScoop. This can ensure international prices remain competitive and accurate. + +- **Financial Reporting Automation**: Create a workflow that periodically retrieves historical exchange rates to analyze currency performance over time. The data can be formatted into reports and automatically sent to stakeholders or saved to Google Sheets for further analysis. diff --git a/components/current_rms/current_rms.app.mjs b/components/current_rms/current_rms.app.mjs new file mode 100644 index 0000000000000..8618e5ab2d478 --- /dev/null +++ b/components/current_rms/current_rms.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "current_rms", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/current_rms/package.json b/components/current_rms/package.json new file mode 100644 index 0000000000000..2331e70a7c80c --- /dev/null +++ b/components/current_rms/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/current_rms", + "version": "0.0.1", + "description": "Pipedream Current RMS Components", + "main": "current_rms.app.mjs", + "keywords": [ + "pipedream", + "current_rms" + ], + "homepage": "https://pipedream.com/apps/current_rms", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/customer_fields/README.md b/components/customer_fields/README.md new file mode 100644 index 0000000000000..ddf72d13a66c6 --- /dev/null +++ b/components/customer_fields/README.md @@ -0,0 +1,11 @@ +# Overview + +The Customer Fields API provides a suite of tools for managing customer data in e-commerce platforms, including custom form fields and data automation. With it, you can efficiently tailor customer interactions, streamline data collection, and enhance the shopping experience. Integrating Customer Fields with Pipedream allows for seamless automation of tasks like syncing data to other platforms, triggering personalized communications, or managing customer segments based on custom field data. + +# Example Use Cases + +- **Customer Data Sync to CRM**: Automate the transfer of customer information from Customer Fields to a Customer Relationship Management (CRM) system like Salesforce or HubSpot. Each time a new customer is added or existing customer details are updated in Customer Fields, trigger a Pipedream workflow to update the corresponding entry in your CRM. + +- **Dynamic Email Campaign Trigger**: Use Customer Fields data to personalize marketing efforts. Create a Pipedream workflow that listens for updates in customer preferences or behaviors and use this data to trigger targeted email campaigns through an email marketing service such as Mailchimp or SendGrid. + +- **Customer Support Ticket Automation**: Improve customer support by creating a Pipedream workflow that creates a support ticket in a tool like Zendesk or Help Scout whenever a customer submits specific requests or feedback through custom forms in Customer Fields. diff --git a/components/customer_guru/README.md b/components/customer_guru/README.md new file mode 100644 index 0000000000000..a2267b3b3bf65 --- /dev/null +++ b/components/customer_guru/README.md @@ -0,0 +1,11 @@ +# Overview + +The Customer.guru API is a potent tool for gauging customer satisfaction and Net Promoter Score (NPS). By integrating with Pipedream, you can automate the collection and analysis of customer feedback, trigger targeted actions based on NPS results, and meld those insights with other data sources. Pipedream's serverless platform lets you create workflows that react to Customer.guru events in real-time, enrich customer data, and drive personalized engagements. + +# Example Use Cases + +- **Automated Feedback Analysis Workflow**: When Customer.guru sends a new NPS score, the workflow is triggered on Pipedream, automatically analyzing the feedback. The sentiment is parsed and categorized, and if the score falls below a certain threshold, a follow-up task is created in a project management tool like Trello or Asana, prompting timely customer outreach. + +- **Real-Time Alerts for High NPS Scores**: Set up a Pipedream process that listens for high NPS score submissions via Customer.guru. When a score over a certain threshold is detected, an immediate alert is sent to a Slack channel, celebrating the customer's positive experience and encouraging the team to maintain high standards. + +- **NPS-Triggered Email Campaigns**: A Pipedream workflow leverages high NPS scores by triggering personalized thank-you emails from a marketing platform such as Mailchimp. For detractors, it can initiate a different campaign aimed at collecting detailed feedback, helping you to understand and address their concerns. diff --git a/components/customer_io/README.md b/components/customer_io/README.md index 849fd2c02c045..c3663d90718ad 100644 --- a/components/customer_io/README.md +++ b/components/customer_io/README.md @@ -1,12 +1,11 @@ # Overview -With the Customer.io API, you can build a variety of applications and -integrations to help you better engage with your customers. Some examples of -what you can build include: - -- A customer facing application that displays customer information and account - activity -- An integration with your CRM system to keep customer data up-to-date -- A system to segment your customers and send them targeted messages -- An application to trigger events based on customer behavior -- A tool to track customer churn and identify at-risk customers +Customer.io is a versatile automation tool that allows you to harness the power of email, SMS, and push notifications to create personalized customer experiences. With its comprehensive API, Customer.io enables you to track customer interactions, segment audiences, and trigger targeted communications based on user behavior and data. By leveraging Pipedream, you can easily connect Customer.io to various other services and APIs to automate complex workflows, synthesize data, and respond to events in real-time. + +# Example Use Cases + +- **Sync Customer.io Segments with a CRM**: Automatically update your CRM contacts based on Customer.io segment changes. When a user enters or exits a segment in Customer.io, Pipedream can capture this event to add or update the contact in your CRM, ensuring your sales team always has the latest information. + +- **Trigger Email Campaigns Based on Product Usage**: Utilize Pipedream to monitor user interactions with your product (e.g., feature usage or inactivity) from your app's backend or tracking service. Once a specific condition is met, Pipedream triggers a Customer.io workflow that sends out tailored email campaigns to engage or re-engage users. + +- **Automate Support Ticket Creation**: When a user expresses dissatisfaction through a survey or a low NPS score in Customer.io, Pipedream can automatically create a support ticket in a tool like Zendesk or Jira. This ensures that your customer support team promptly addresses potential issues and improves customer satisfaction. diff --git a/components/customgpt/README.md b/components/customgpt/README.md new file mode 100644 index 0000000000000..32d68c31f6e80 --- /dev/null +++ b/components/customgpt/README.md @@ -0,0 +1,11 @@ +# Overview + +CustomGPT API harnesses the power of generative AI to create custom chatbots tailored to specific needs or data sets. With Pipedream's serverless platform, you can integrate CustomGPT into complex workflows, triggering custom AI responses based on events from over 800+ apps. Automate tasks like customer support, personalized content creation, or data analysis by tapping into the rich capabilities of CustomGPT and Pipedream's seamless orchestration. + +# Example Use Cases + +- **Automated Customer Support**: Use CustomGPT to handle initial customer support queries received through a service like Zendesk. When a new ticket is created, trigger a Pipedream workflow to fetch the appropriate response from CustomGPT and automatically reply, reducing response times and offloading work from human agents. + +- **Content Personalization for Email Campaigns**: Incorporate CustomGPT into an email marketing platform like Mailchimp via Pipedream. Analyze subscriber data to generate personalized content for email campaigns. Each subscriber interaction can trigger a workflow that uses CustomGPT to tailor content, improving engagement and conversion rates. + +- **Insightful Data Analysis Reports**: Connect CustomGPT with a data visualization tool like Google Sheets. Use Pipedream to pull data from various sources, process it, and then use CustomGPT to generate a human-like analysis or summary. Automatically update a Google Sheet with the insights, streamlining the reporting process for teams. diff --git a/components/customgpt/package.json b/components/customgpt/package.json index 22e7dcd190025..e01ba06eb812e 100644 --- a/components/customgpt/package.json +++ b/components/customgpt/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/customjs/actions/common/utils.mjs b/components/customjs/actions/common/utils.mjs new file mode 100644 index 0000000000000..4c9858fbb0c89 --- /dev/null +++ b/components/customjs/actions/common/utils.mjs @@ -0,0 +1,13 @@ +const normalizeFilepath = (filename, ext = "pdf") => { + let filepath = filename.includes("/tmp") + ? filename + : `/tmp/${filename}`; + filepath = filepath.includes(`.${ext}`) + ? filepath + : `${filepath}.${ext}`; + return filepath; +}; + +export { + normalizeFilepath, +}; diff --git a/components/customjs/actions/convert-html-to-pdf/convert-html-to-pdf.mjs b/components/customjs/actions/convert-html-to-pdf/convert-html-to-pdf.mjs new file mode 100644 index 0000000000000..3efbfb3c3357a --- /dev/null +++ b/components/customjs/actions/convert-html-to-pdf/convert-html-to-pdf.mjs @@ -0,0 +1,44 @@ +import customjs from "../../customjs.app.mjs"; +import fs from "fs"; +import { normalizeFilepath } from "../common/utils.mjs"; + +export default { + key: "customjs-convert-html-to-pdf", + name: "Convert HTML to PDF", + description: "Converts an HTML string to a PDF document. [See the documentation](https://www.customjs.space/api/docs#_1-html-to-pdf)", + version: "0.0.2", + type: "action", + props: { + customjs, + html: { + type: "string", + label: "HTML", + description: "The HTML string to be converted to a PDF", + }, + filename: { + propDefinition: [ + customjs, + "filename", + ], + }, + }, + async run({ $ }) { + const fileContent = await this.customjs.convertHtmlToPdf({ + $, + data: { + input: this.html, + code: "const { HTML2PDF } = require(\"./utils\"); return HTML2PDF(input);", + returnBinary: "true", + }, + }); + + const filepath = normalizeFilepath(this.filename); + fs.writeFileSync(filepath, Buffer.from(fileContent)); + + $.export("$summary", "Successfully converted HTML to PDF"); + return { + filename: this.filename, + filepath, + }; + }, +}; diff --git a/components/customjs/actions/convert-html-to-png/convert-html-to-png.mjs b/components/customjs/actions/convert-html-to-png/convert-html-to-png.mjs new file mode 100644 index 0000000000000..48c7a499f558b --- /dev/null +++ b/components/customjs/actions/convert-html-to-png/convert-html-to-png.mjs @@ -0,0 +1,44 @@ +import customjs from "../../customjs.app.mjs"; +import fs from "fs"; +import { normalizeFilepath } from "../common/utils.mjs"; + +export default { + key: "customjs-convert-html-to-png", + name: "Convert HTML to PNG", + description: "Converts an HTML string to a PNG image. [See the documentation](https://www.customjs.space/api/docs#_4-html-to-png)", + version: "0.0.1", + type: "action", + props: { + customjs, + html: { + type: "string", + label: "HTML", + description: "The HTML string to be converted to a PNG", + }, + filename: { + propDefinition: [ + customjs, + "filename", + ], + }, + }, + async run({ $ }) { + const fileContent = await this.customjs.convertHtmlToPng({ + $, + data: { + input: this.html, + code: "const { HTML2PNG } = require('./utils'); return HTML2PNG(input);", + returnBinary: "true", + }, + }); + + const filepath = normalizeFilepath(this.filename, "png"); + fs.writeFileSync(filepath, Buffer.from(fileContent)); + + $.export("$summary", "Successfully converted HTML to PNG"); + return { + filename: this.filename, + filepath, + }; + }, +}; diff --git a/components/customjs/actions/create-screenshot/create-screenshot.mjs b/components/customjs/actions/create-screenshot/create-screenshot.mjs new file mode 100644 index 0000000000000..284a696b6ccc7 --- /dev/null +++ b/components/customjs/actions/create-screenshot/create-screenshot.mjs @@ -0,0 +1,45 @@ +import customjs from "../../customjs.app.mjs"; +import fs from "fs"; +import { normalizeFilepath } from "../common/utils.mjs"; + +export default { + key: "customjs-create-screenshot", + name: "Create Screenshot", + description: "Create a screenshot of a website. [See the documentation](https://www.customjs.space/api/docs#_3-create-screenshot)", + version: "0.0.2", + type: "action", + props: { + customjs, + url: { + type: "string", + label: "URL", + description: "The URL of the website to take a screenshot of", + }, + filename: { + propDefinition: [ + customjs, + "filename", + ], + description: "Download the PNG file to the `/tmp` directory with the specified filename.", + }, + }, + async run({ $ }) { + const fileContent = await this.customjs.createScreenshot({ + $, + data: { + input: this.url, + code: "const { SCREENSHOT } = require(\"./utils\"); return SCREENSHOT(input);", + returnBinary: "true", + }, + }); + + const filepath = normalizeFilepath(this.filename, "png"); + fs.writeFileSync(filepath, Buffer.from(fileContent)); + + $.export("$summary", `Successfully created screenshot of ${this.url}`); + return { + filename: this.filename, + filepath, + }; + }, +}; diff --git a/components/customjs/actions/merge-pdfs/merge-pdfs.mjs b/components/customjs/actions/merge-pdfs/merge-pdfs.mjs new file mode 100644 index 0000000000000..7f38cb6fce82a --- /dev/null +++ b/components/customjs/actions/merge-pdfs/merge-pdfs.mjs @@ -0,0 +1,48 @@ +import customjs from "../../customjs.app.mjs"; +import fs from "fs"; +import { normalizeFilepath } from "../common/utils.mjs"; + +export default { + key: "customjs-merge-pdfs", + name: "Merge PDFs", + description: "Merges multiple PDF documents into one. [See the documentation](https://www.customjs.space/api/docs#_2-merge-pdfs)", + version: "0.0.2", + type: "action", + props: { + customjs, + pdfs: { + type: "string[]", + label: "PDFs", + description: "The array of URLs to the PDF documents to merge", + }, + filename: { + propDefinition: [ + customjs, + "filename", + ], + }, + }, + async run({ $ }) { + const pdfs = typeof this.pdfs === "string" + ? JSON.parse(this.pdfs) + : this.pdfs; + + const fileContent = await this.customjs.mergePdfs({ + $, + data: { + input: pdfs, + code: "const { PDF_MERGE } = require(\"./utils\"); const axios = require(\"axios\"); const pdfBuffers = await Promise.all(input.map(async file => { const res = await axios.get(file, { responseType: \"arraybuffer\" }); return Buffer.from(res.data).toString(\"base64\"); })); return PDF_MERGE(pdfBuffers);", + returnBinary: "true", + }, + }); + + const filepath = normalizeFilepath(this.filename); + fs.writeFileSync(filepath, Buffer.from(fileContent)); + + $.export("$summary", "Successfully merged PDFs"); + return { + filename: this.filename, + filepath, + }; + }, +}; diff --git a/components/customjs/actions/run-puppeteer/run-puppeteer.mjs b/components/customjs/actions/run-puppeteer/run-puppeteer.mjs new file mode 100644 index 0000000000000..1eb69bad1411b --- /dev/null +++ b/components/customjs/actions/run-puppeteer/run-puppeteer.mjs @@ -0,0 +1,45 @@ +import customjs from "../../customjs.app.mjs"; +import fs from "fs"; +import { normalizeFilepath } from "../common/utils.mjs"; + +export default { + key: "customjs-run-puppeteer", + name: "Run Puppeteer", + description: "Run Puppeteer. [See the documentation](https://www.customjs.space/api/docs#_5-run-puppeteer)", + version: "0.0.1", + type: "action", + props: { + customjs, + code: { + type: "string", + label: "Code", + description: "Enter the code you want to run on puppeteer. For example: `await page.goto(\"https://example.com\");` will return a screenshot of https://example.com.", + }, + filename: { + propDefinition: [ + customjs, + "filename", + ], + description: "Download the PNG file to the `/tmp` directory with the specified filename.", + }, + }, + async run({ $ }) { + const fileContent = await this.customjs.runPuppeteer({ + $, + data: { + input: this.code, + code: "const { PUPPETEER } = require('./utils'); return PUPPETEER(input);", + returnBinary: "true", + }, + }); + + const filepath = normalizeFilepath(this.filename, "png"); + fs.writeFileSync(filepath, Buffer.from(fileContent)); + + $.export("$summary", "Successfully ran the puppeteer code."); + return { + filename: this.filename, + filepath, + }; + }, +}; diff --git a/components/customjs/customjs.app.mjs b/components/customjs/customjs.app.mjs new file mode 100644 index 0000000000000..40afd8fe0b0c2 --- /dev/null +++ b/components/customjs/customjs.app.mjs @@ -0,0 +1,72 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "customjs", + propDefinitions: { + filename: { + type: "string", + label: "Filename", + description: "Download the file to the `/tmp` directory with the specified filename.", + }, + }, + methods: { + _makeRequest(opts = {}) { + const { + $ = this, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + method: "POST", + url: `https://e.customjs.io/__js1-${this.$auth.api_key}`, + headers: { + ...headers, + "Content-Type": "application/json", + }, + responseType: "arraybuffer", + }); + }, + convertHtmlToPdf(opts = {}) { + return this._makeRequest({ + headers: { + "customjs-origin": "pipedream/generatePDF", + }, + ...opts, + }); + }, + convertHtmlToPng(opts = {}) { + return this._makeRequest({ + headers: { + "customjs-origin": "pipedream/generatePNG", + }, + ...opts, + }); + }, + createScreenshot(opts = {}) { + return this._makeRequest({ + headers: { + "customjs-origin": "pipedream/screenshot", + }, + ...opts, + }); + }, + mergePdfs(opts = {}) { + return this._makeRequest({ + headers: { + "customjs-origin": "pipedream/mergePDFs", + }, + ...opts, + }); + }, + runPuppeteer(opts = {}) { + return this._makeRequest({ + headers: { + "customjs-origin": "pipedream/puppeteer", + }, + ...opts, + }); + }, + }, +}; diff --git a/components/customjs/package.json b/components/customjs/package.json new file mode 100644 index 0000000000000..7e6c726641592 --- /dev/null +++ b/components/customjs/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/customjs", + "version": "0.2.0", + "description": "Pipedream CustomJS Components", + "main": "customjs.app.mjs", + "keywords": [ + "pipedream", + "customjs" + ], + "homepage": "https://pipedream.com/apps/customjs", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/cutt_ly/README.md b/components/cutt_ly/README.md new file mode 100644 index 0000000000000..dc57bd6bb7c1b --- /dev/null +++ b/components/cutt_ly/README.md @@ -0,0 +1,11 @@ +# Overview + +The Cutt.ly API allows users to shorten URLs, track their performance, and manage link settings. With Pipedream, you can automate these tasks and create workflows that respond to various triggers, like new form submissions, emails, or scheduled timers. Utilize Pipedream's capabilities to connect Cutt.ly with other apps, store and analyze click data, and streamline digital marketing processes. + +# Example Use Cases + +- **Automated Link Shortening for Social Media Posts**: When you schedule a new post in Buffer or Hootsuite, Pipedream can automatically shorten the post's URLs using Cutt.ly and update the post with the shortened links before publishing. + +- **URL Shortening and Performance Tracking for Email Campaigns**: Integrate Cutt.ly with Mailchimp or SendGrid in Pipedream. Whenever you send out a new email campaign, use Cutt.ly to shorten links and track click data, enabling you to monitor campaign performance directly within your Pipedream workflows. + +- **Shortened URL Management for Event Registrations**: Combine Cutt.ly with Google Forms or Typeform on Pipedream. Each time someone registers for an event, generate a personalized, shortened URL for the event details page, and send it to the registrant via an automated email. diff --git a/components/cyberimpact/README.md b/components/cyberimpact/README.md index cc2ad45a7f720..c1494e7d1bea7 100644 --- a/components/cyberimpact/README.md +++ b/components/cyberimpact/README.md @@ -1,13 +1,11 @@ # Overview -With the Cyberimpact API, you can build a variety of programs and applications -to automate your email marketing. Here are some examples of what you can build: - -- An email marketing program that automatically sends out weekly or monthly - newsletters to your subscribers -- A program that allows you to send out automated emails based on certain - triggers, such as if a user abandon's their shopping cart -- An email template builder that makes it easy to create and customize email - templates -- A tool that analyzes your email marketing campaigns and provides insights on - what's working and what's not +The Cyberimpact API allows for the seamless integration of email marketing tools into your custom workflows, enabling automation of contact management, campaign tracking, and subscriber interaction. Using this API with Pipedream, you can automate the sync of subscriber lists, trigger email campaigns based on specific actions, and analyze campaign performances, all within a serverless environment. This can significantly cut the time spent on routine marketing tasks and enhance the efficiency and effectiveness of your marketing strategies. + +# Example Use Cases + +- **Automated Subscriber Syncing**: Create a workflow that listens for new sign-ups on your website (using a webhook or your preferred sign-up tool). Once a new user registers, automatically add them to a specified Cyberimpact mailing list. This keeps your subscriber list up-to-date without manual intervention. + +- **Triggered Email Campaigns from E-Commerce Events**: Set up a workflow that catches webhook notifications from your e-commerce platform (like Shopify) for events like a completed purchase. Use this trigger to send a tailored thank you email from Cyberimpact, or enroll the customer in a post-purchase education sequence. + +- **Campaign Performance Reporting to Google Sheets**: After sending out a campaign, use Cyberimpact's API to collect campaign performance data. Then, employ Pipedream's built-in Google Sheets actions to push this data into a spreadsheet. Here, it can automatically update dashboards or reports, making performance analysis accessible and real-time. diff --git a/components/cyberimpact/package.json b/components/cyberimpact/package.json new file mode 100644 index 0000000000000..7a4127528ef72 --- /dev/null +++ b/components/cyberimpact/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/cyberimpact", + "version": "0.6.0", + "description": "Pipedream cyberimpact Components", + "main": "cyberimpact.app.mjs", + "keywords": [ + "pipedream", + "cyberimpact" + ], + "homepage": "https://pipedream.com/apps/cyberimpact", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/cyfe/README.md b/components/cyfe/README.md new file mode 100644 index 0000000000000..c12ac374e9cac --- /dev/null +++ b/components/cyfe/README.md @@ -0,0 +1,11 @@ +# Overview + +The Cyfe API lets you automate and enhance your data visualization and business intelligence tasks. With Cyfe, you can programmatically create dashboards, add or update widgets, and manage data pushed to these widgets. By leveraging the Cyfe API on Pipedream, you gain serverless execution of workflows, allowing you to connect Cyfe with numerous other services and automate tasks without managing infrastructure. + +# Example Use Cases + +- **Automated Reporting Pipeline**: Use Pipedream to schedule and execute a workflow that fetches data from various sources (like Google Analytics or Salesforce), processes it, and then pushes it to a Cyfe dashboard widget. This turns manual reporting tasks into automated updates, providing real-time insights. + +- **Cyfe as a Data Hub**: Build a workflow on Pipedream that collects data from different apps (like social media statistics, support tickets, and server health metrics), and then uses the Cyfe API to populate a centralized business operations dashboard, offering a cohesive view of various business aspects. + +- **Event-Driven Alerts**: Configure a Pipedream workflow that listens for specific events, such as a spike in website traffic or a drop in sales. Once an event is detected, it could trigger an update to a relevant Cyfe dashboard widget and simultaneously send a notification via Slack or email, keeping the team informed instantly. diff --git a/components/d2l_brightspace/d2l_brightspace.app.mjs b/components/d2l_brightspace/d2l_brightspace.app.mjs new file mode 100644 index 0000000000000..0d2718091ed74 --- /dev/null +++ b/components/d2l_brightspace/d2l_brightspace.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "d2l_brightspace", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/d2l_brightspace/package.json b/components/d2l_brightspace/package.json new file mode 100644 index 0000000000000..01145376def66 --- /dev/null +++ b/components/d2l_brightspace/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/d2l_brightspace", + "version": "0.0.1", + "description": "Pipedream D2L Brightspace Components", + "main": "d2l_brightspace.app.mjs", + "keywords": [ + "pipedream", + "d2l_brightspace" + ], + "homepage": "https://pipedream.com/apps/d2l_brightspace", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/d7_darwin/d7_darwin.app.mjs b/components/d7_darwin/d7_darwin.app.mjs new file mode 100644 index 0000000000000..06ecfc1d84767 --- /dev/null +++ b/components/d7_darwin/d7_darwin.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "d7_darwin", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/d7_darwin/package.json b/components/d7_darwin/package.json new file mode 100644 index 0000000000000..34f9b43da4606 --- /dev/null +++ b/components/d7_darwin/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/d7_darwin", + "version": "0.0.1", + "description": "Pipedream D7 Darwin Components", + "main": "d7_darwin.app.mjs", + "keywords": [ + "pipedream", + "d7_darwin" + ], + "homepage": "https://pipedream.com/apps/d7_darwin", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/d7_networks/README.md b/components/d7_networks/README.md new file mode 100644 index 0000000000000..436db8ff4f53f --- /dev/null +++ b/components/d7_networks/README.md @@ -0,0 +1,11 @@ +# Overview + +The D7 SMS API on Pipedream allows you to send SMS messages programmatically to users worldwide from a serverless platform. This API can enable instant communication with customers or team members directly from automated workflows. With Pipedream, you can trigger SMS messages based on events from countless apps, handle incoming messages, and integrate with other services for a seamless messaging experience. + +# Example Use Cases + +- **Customer Order Notifications**: Automate sending SMS updates to customers when their order status changes in an e-commerce platform like Shopify. Connect Shopify triggers to the D7 SMS API on Pipedream to keep customers informed in real time. + +- **Two-Factor Authentication (2FA)**: Enhance security by implementing 2FA in your app. Use the D7 SMS API to send one-time passcodes to user's mobile phones when they attempt to log in or perform sensitive actions. + +- **Appointment Reminders**: Send SMS reminders for appointments by linking calendar apps like Google Calendar with the D7 SMS API. Set up a Pipedream workflow that triggers an SMS reminder to clients a day before their scheduled appointment. diff --git a/components/dacast/README.md b/components/dacast/README.md index d6c8fe99d7a84..c0e13a22868a8 100644 --- a/components/dacast/README.md +++ b/components/dacast/README.md @@ -1,11 +1,11 @@ # Overview -The Dacast API allows developers to access and integrate the functionality of -Dacast with other applications and websites. +Dacast's API gives you the power to manage live and on-demand video content. With this API, you can automate content creation, update settings, and pull analytics, all of which can be seamlessly integrated with Pipedream workflows. Use it to synchronize your video content with other databases, notify subscribers about new videos, or generate real-time alerts based on streaming performance metrics. -Some example applications that can be built using the Dacast API include: +# Example Use Cases -- A video streaming website or application -- A video on demand website or application -- A live streaming website or application -- A video content management system +- **Automated Video Content Syncing with CMS**: Integrate Dacast with a Content Management System (CMS) like WordPress. Whenever a new video is uploaded to Dacast, trigger a workflow that creates or updates a post in WordPress with the video details and embed code, keeping your site's content fresh and in sync with your streaming service. + +- **Subscriber Notifications via Email or Messaging Apps**: Connect Dacast to an email service like SendGrid or a messaging app like Slack. Set up a workflow to detect when a new live stream starts or a new video is uploaded to Dacast and instantly send out a notification with viewing details to a list of subscribers or a Slack channel, keeping your audience engaged and informed. + +- **Real-Time Streaming Analytics with Dashboard Integration**: Pair Dacast with a dashboard platform like Google Data Studio. Create a workflow that fetches streaming performance data from Dacast and sends it to Google Sheets, which feeds into Data Studio, providing live analytics visualization for monitoring and decision-making. diff --git a/components/dacast/package.json b/components/dacast/package.json new file mode 100644 index 0000000000000..cb8b5f3ddf640 --- /dev/null +++ b/components/dacast/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/dacast", + "version": "0.6.0", + "description": "Pipedream dacast Components", + "main": "dacast.app.mjs", + "keywords": [ + "pipedream", + "dacast" + ], + "homepage": "https://pipedream.com/apps/dacast", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/dadata_ru/README.md b/components/dadata_ru/README.md index 89a4f384e1a81..a2e556552dc93 100644 --- a/components/dadata_ru/README.md +++ b/components/dadata_ru/README.md @@ -1,14 +1,11 @@ # Overview -Using the DaData.ru API, you can build a variety of applications and -integrations that make use of the data the API provides. Here are some -examples: - -- A contact management system that uses DaData.ru to verify and enrich user - contact information -- A CRM system that uses DaData.ru to automatically fill in missing data fields - for new contacts -- An e-commerce platform that uses DaData.ru to verify customer addresses and - prevent fraud -- A marketing automation tool that uses DaData.ru to segment customers by - location +With the DaData.ru API, you can enrich, clean, and autocomplete various types of data, including addresses, names, and company details. This powerful tool can be used to enhance the quality of user input, automate data normalization, and conduct insightful analysis on datasets. Specifically, it can help verify and format addresses to ensure delivery accuracy, deduplicate and correct database entries, and provide auto-suggestions for form fields, improving user experience and backend data consistency. + +# Example Use Cases + +- **Address Validation for E-commerce Checkouts**: Integrate DaData.ru with an e-commerce platform to validate and correct shipping addresses as customers input their details. This ensures accurate deliveries and reduces the risk of returns due to incorrect address information. + +- **Lead Enrichment for CRM**: Use DaData.ru to automatically enrich incoming leads with additional data before they are entered into a Customer Relationship Management (CRM) system like Salesforce. This could include verifying company names, adding industry information, and standardizing phone numbers, thus saving time and improving the quality of lead data for sales teams. + +- **Automated Contacts Deduplication**: Connect DaData.ru with an email marketing tool, such as Mailchimp, to clean and deduplicate contact lists. This process could involve normalizing phone numbers and addresses, merging duplicate records, and validating email addresses to improve campaign quality and engagement rates. diff --git a/components/dadata_ru/package.json b/components/dadata_ru/package.json new file mode 100644 index 0000000000000..140b514e66ebf --- /dev/null +++ b/components/dadata_ru/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/dadata_ru", + "version": "0.6.0", + "description": "Pipedream dadata_ru Components", + "main": "dadata_ru.app.mjs", + "keywords": [ + "pipedream", + "dadata_ru" + ], + "homepage": "https://pipedream.com/apps/dadata_ru", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/daffy/README.md b/components/daffy/README.md new file mode 100644 index 0000000000000..a733c4a3400b8 --- /dev/null +++ b/components/daffy/README.md @@ -0,0 +1,11 @@ +# Overview + +The Daffy API lets you drive charitable giving through its platform. With Pipedream, you can automate donations, manage donor data, and integrate charitable actions within your existing workflows. You can trigger actions based on diverse events, like donations or new user registrations, and connect with other apps to streamline and scale your impact. + +# Example Use Cases + +- **Automate Acknowledgements for Donations**: Create a workflow that detects new donations through the Daffy API, then triggers an email or a message via Twilio or SendGrid to thank the donor automatically. This builds strong relationships with donors by ensuring they're appreciated promptly. + +- **Sync Donation Data with CRM**: When a new donation is recorded on Daffy, use Pipedream to send that data to a CRM system like Salesforce or HubSpot. This keeps your donor records up-to-date and enables personalized outreach or future fundraising campaigns. + +- **Trigger Corporate Gift Matching**: Set up a workflow where you monitor for new donations on Daffy, then check with a service like Double the Donation to see if the donor’s employer matches gifts. If they do, automatically initiate the matching gift process, maximizing the donation's impact. diff --git a/components/daily/README.md b/components/daily/README.md index fc3ac5f4dd2b7..e342367701bec 100644 --- a/components/daily/README.md +++ b/components/daily/README.md @@ -1,9 +1,11 @@ # Overview -With the Daily API, you can build a variety of applications and features -including: +Daily API offers a suite of powerful video and audio capabilities, enabling the creation, management, and customization of real-time video and audio calls directly within your applications. Pipedream, as a serverless integration and automation platform, empowers users to connect Daily with various other apps to automate workflows. Whether you're looking to synchronize meeting data with project management tools, kick off post-meeting workflows, or manage recordings at scale, Pipedream provides the infrastructure to streamline these processes. -- A video meeting and chat application -- A video streaming service -- A video calling and messaging service -- A virtual reality application +# Example Use Cases + +- **Automated Meeting Summaries:** After a Daily call ends, trigger a workflow on Pipedream that captures call metadata, transcribes audio recordings, and sends a summary email to all participants using the Gmail app. + +- **Meeting Scheduler with Calendar Integration:** Set up a Pipedream workflow that listens for new events in a Google Calendar, automatically creates corresponding Daily meeting rooms, and then emails the meeting links to attendees using the Gmail app. + +- **Post-Call Feedback Collection:** Initiate a Pipedream workflow once a Daily meeting concludes to send a feedback form via Typeform to all participants, then collect and store responses in a Google Sheet for analysis. diff --git a/components/daily/package.json b/components/daily/package.json new file mode 100644 index 0000000000000..f2fb0bfd44fb8 --- /dev/null +++ b/components/daily/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/daily", + "version": "0.6.0", + "description": "Pipedream daily Components", + "main": "daily.app.mjs", + "keywords": [ + "pipedream", + "daily" + ], + "homepage": "https://pipedream.com/apps/daily", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/dailybot/README.md b/components/dailybot/README.md new file mode 100644 index 0000000000000..9bc59feada887 --- /dev/null +++ b/components/dailybot/README.md @@ -0,0 +1,11 @@ +# Overview + +The DailyBot API on Pipedream opens up a world of possibilities for automating team interactions and enhancing productivity. With DailyBot, you can create custom workflows to automate stand-ups, collect feedback, run polls, and send reminders. By leveraging Pipedream's seamless connections with other apps and services, you can integrate DailyBot into your existing tools and streamline your team's communication processes without writing extensive code. + +# Example Use Cases + +- **Automated Stand-up Reports to Task Management**: Collect daily stand-up reports from your team via DailyBot and automatically create tasks in a project management app like Trello or Asana. This workflow ensures that updates are not just collected but acted upon, keeping everyone on the same page with current tasks and progress. + +- **Feedback Aggregation and Analysis**: Send out periodic feedback requests using DailyBot and funnel responses to a data analysis tool like Google Sheets. Use Pipedream to process the data and generate insights or reports, making it easier to measure team satisfaction and identify areas for improvement. + +- **Event-Based Reminder System**: Set up DailyBot to send custom reminders for upcoming events or deadlines by integrating with Google Calendar. When a new event is added to the calendar, Pipedream triggers DailyBot to schedule reminders, ensuring the team stays informed about important dates. diff --git a/components/daktela/daktela.app.mjs b/components/daktela/daktela.app.mjs new file mode 100644 index 0000000000000..210e26efe7add --- /dev/null +++ b/components/daktela/daktela.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "daktela", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/daktela/package.json b/components/daktela/package.json new file mode 100644 index 0000000000000..fb7d8a484827e --- /dev/null +++ b/components/daktela/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/daktela", + "version": "0.0.1", + "description": "Pipedream Daktela Components", + "main": "daktela.app.mjs", + "keywords": [ + "pipedream", + "daktela" + ], + "homepage": "https://pipedream.com/apps/daktela", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/damstra_forms/README.md b/components/damstra_forms/README.md new file mode 100644 index 0000000000000..0508df0fe944b --- /dev/null +++ b/components/damstra_forms/README.md @@ -0,0 +1,11 @@ +# Overview + +The Damstra Forms API provides a digital solution for creating, managing, and submitting forms vital to workplace safety and compliance. By integrating this API with Pipedream, you can automate form-related workflows to efficiently handle data collection, processing, and analysis. Use cases include automatically triggering actions upon form submission, syncing form data to other business systems, and monitoring submissions for compliance. + +# Example Use Cases + +- **Automated Form Submission Notifications**: Trigger an email or Slack message using Pipedream's built-in actions when a new form is submitted via Damstra Forms API. This keeps teams immediately informed of new data entries or issues that require attention. + +- **Data Synchronization to Cloud Storage**: On new form submission via Damstra Forms API, automatically store the data in Google Sheets or upload to AWS S3. This ensures that form data is backed up, easy to access, and ready for further analysis or reporting. + +- **Compliance Monitoring Dashboard**: Use Pipedream to monitor form submissions for compliance-related keywords or missing fields. If a submission doesn't meet criteria, create a task in project management tools like Trello or Asana to address the issue, ensuring your organization stays on top of regulatory requirements. diff --git a/components/damstra_forms/damstra_forms.app.mjs b/components/damstra_forms/damstra_forms.app.mjs new file mode 100644 index 0000000000000..e161add9077b8 --- /dev/null +++ b/components/damstra_forms/damstra_forms.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "damstra_forms", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/damstra_forms/package.json b/components/damstra_forms/package.json new file mode 100644 index 0000000000000..4e2833bb5b1e8 --- /dev/null +++ b/components/damstra_forms/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/damstra_forms", + "version": "0.0.1", + "description": "Pipedream Damstra Forms Components", + "main": "damstra_forms.app.mjs", + "keywords": [ + "pipedream", + "damstra_forms" + ], + "homepage": "https://pipedream.com/apps/damstra_forms", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/dandelion/README.md b/components/dandelion/README.md index bd97bc39fd86c..c4470896dd8ae 100644 --- a/components/dandelion/README.md +++ b/components/dandelion/README.md @@ -1,10 +1,11 @@ # Overview -Dandelion is a powerful tool that can be used to build a variety of -applications. Here are just a few examples: - -- A tool for extracting information from text -- A tool for automatically generating summaries of text -- A tool for automatically categorizing text -- A tool for identifying the sentiment of text -- A tool for translating text +The Dandelion API is a text analysis toolkit that allows for understanding and extracting information from texts in various languages. On Pipedream, you can leverage this API to automate workflows that involve natural language processing tasks like sentiment analysis, entity recognition, and language detection. These capabilities enable developers to create applications that can interpret user input, analyze social media sentiment, categorize content, and more, all within a serverless platform. + +# Example Use Cases + +- **Sentiment Analysis on Support Tickets**: Automatically scan incoming customer support tickets for sentiment using the Dandelion API. This workflow could flag negative sentiments and escalate them to higher-priority queues or alert managers for immediate intervention. Connect to a CRM like Salesforce to log and track these communications effectively. + +- **Content Categorization for CMS**: Before publishing articles in a Content Management System (CMS), use the Dandelion API to categorize content based on the topics detected in the text. This can be used to automate tagging and improve content discoverability. Integrate with WordPress or another CMS on Pipedream to streamline the publishing process. + +- **Social Media Monitoring for Brand Management**: Combine Dandelion's entity recognition with social media APIs to monitor mentions of your brand or products. When a mention is detected, analyze the post's sentiment and extract key entities. These insights can trigger follow-up actions like sending a thank you response to positive mentions or alerting PR teams to potential issues on platforms like Slack or via email. diff --git a/components/dandelion/package.json b/components/dandelion/package.json new file mode 100644 index 0000000000000..c34c454cbfd94 --- /dev/null +++ b/components/dandelion/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/dandelion", + "version": "0.6.0", + "description": "Pipedream dandelion Components", + "main": "dandelion.app.mjs", + "keywords": [ + "pipedream", + "dandelion" + ], + "homepage": "https://pipedream.com/apps/dandelion", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/danny_test_app/danny_test_app.app.mjs b/components/danny_test_app/danny_test_app.app.mjs new file mode 100644 index 0000000000000..18c3cebfa93a2 --- /dev/null +++ b/components/danny_test_app/danny_test_app.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "danny_test_app", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/danny_test_app/package.json b/components/danny_test_app/package.json index 872ee3cba8c4b..2c1b12139656a 100644 --- a/components/danny_test_app/package.json +++ b/components/danny_test_app/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/darksky_api/README.md b/components/darksky_api/README.md index 278199be07225..a01dd77861b16 100644 --- a/components/darksky_api/README.md +++ b/components/darksky_api/README.md @@ -1,8 +1,11 @@ # Overview -With the DarkSky API, you can build applications that: +The DarkSky API offers detailed weather information that includes minute-by-minute forecasts, hourly forecasts, and daily summaries. By harnessing this API on Pipedream, you can automate tasks like sending alerts, integrating weather data into event planning platforms, or tailoring content based on weather conditions. With Pipedream’s serverless platform, these use cases become straightforward to implement, and you can trigger workflows with new weather data, schedule checks at regular intervals, or even react to changes in forecasted conditions. -- Retrieve detailed weather information for a given location -- Get hourly, daily, and weekly forecasts -- Monitor weather conditions in near-real-time -- Get historical weather data for a given location +# Example Use Cases + +- **Weather-Based Content Personalization**: Tailor user experiences by integrating DarkSky with a content management system (CMS). For instance, trigger a workflow that updates your website's featured products based on the current weather, promoting umbrellas on rainy days or sunglasses when it's sunny. + +- **Event Weather Preparedness**: Connect DarkSky to a calendar or event management app to monitor forecasted conditions for upcoming events. Automate notifications to event attendees, suggesting they bring a coat if it's going to be cold, or reschedule outdoor activities if a storm is on the horizon. + +- **Personal Weather Notifications**: Create a personalized weather notification system. Use DarkSky's data to send daily weather summaries or severe weather alerts to your phone via SMS or messaging apps like Slack, keeping you informed and ready for the day's conditions. diff --git a/components/darksky_api/package.json b/components/darksky_api/package.json new file mode 100644 index 0000000000000..53fc9735f0427 --- /dev/null +++ b/components/darksky_api/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/darksky_api", + "version": "0.6.0", + "description": "Pipedream darksky_api Components", + "main": "darksky_api.app.mjs", + "keywords": [ + "pipedream", + "darksky_api" + ], + "homepage": "https://pipedream.com/apps/darksky_api", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/dart/actions/create-task/create-task.mjs b/components/dart/actions/create-task/create-task.mjs new file mode 100644 index 0000000000000..99142908e1917 --- /dev/null +++ b/components/dart/actions/create-task/create-task.mjs @@ -0,0 +1,84 @@ +import dart from "../../dart.app.mjs"; + +export default { + key: "dart-create-task", + name: "Create Task", + description: "Creates a new task within a dartboard. [See the documentation](https://app.itsdart.com/api/v0/docs/)", + version: "0.0.1", + type: "action", + props: { + dart, + dartboardId: { + propDefinition: [ + dart, + "dartboardId", + ], + }, + duid: { + type: "string", + label: "Task DUID", + description: "A unique identifier for the task. Must contain at least 12 characters.", + }, + title: { + propDefinition: [ + dart, + "taskName", + ], + }, + description: { + propDefinition: [ + dart, + "taskDescription", + ], + }, + dueAt: { + propDefinition: [ + dart, + "dueAt", + ], + }, + assigneeIds: { + propDefinition: [ + dart, + "assigneeIds", + ], + }, + priority: { + propDefinition: [ + dart, + "priority", + ], + }, + }, + async run({ $ }) { + const response = await this.dart.createTransaction({ + $, + data: { + clientDuid: this.duid, + items: [ + { + duid: this.duid, + operations: [ + { + model: "task", + kind: "create", + data: { + duid: this.duid, + dartboardDuid: this.dartboardId, + title: this.title, + description: this.description, + dueAt: this.dueAt, + assigneeDuids: this.assigneeIds, + priority: this.priority, + }, + }, + ], + kind: "task_create", + }, + ], + }, + }); + $.export("$summary", `Created task "${this.title}"`); + return response; + }, +}; diff --git a/components/dart/actions/find-or-create-task/find-or-create-task.mjs b/components/dart/actions/find-or-create-task/find-or-create-task.mjs new file mode 100644 index 0000000000000..babda26ed5d4c --- /dev/null +++ b/components/dart/actions/find-or-create-task/find-or-create-task.mjs @@ -0,0 +1,103 @@ +import dart from "../../dart.app.mjs"; + +export default { + key: "dart-find-or-create-task", + name: "Find or Create Task", + description: "Checks for an existing task within a dartboard using the 'task-name'. If it doesn't exist, a new task is created. [See the documentation](https://app.itsdart.com/api/v0/docs/)", + version: "0.0.1", + type: "action", + props: { + dart, + dartboardId: { + propDefinition: [ + dart, + "dartboardId", + ], + }, + taskName: { + propDefinition: [ + dart, + "taskName", + ], + }, + duid: { + type: "string", + label: "Task DUID", + description: "If the task is not found, a unique identifier to assign to the newly created task. Must contain at least 12 characters.", + }, + description: { + propDefinition: [ + dart, + "taskDescription", + ], + }, + dueAt: { + propDefinition: [ + dart, + "dueAt", + ], + }, + assigneeIds: { + propDefinition: [ + dart, + "assigneeIds", + ], + }, + priority: { + propDefinition: [ + dart, + "priority", + ], + }, + }, + async run({ $ }) { + const { results } = await this.dart.listTasks({ + $, + params: { + title: this.taskName, + }, + }); + + const matchingTasks = results.filter(({ dartboardDuid }) => dartboardDuid === this.dartboardId); + + if (matchingTasks?.length) { + $.export("$summary", `Successfully found task "${this.taskName}"`); + return matchingTasks; + } + + const response = await this.dart.createTransaction({ + $, + data: { + clientDuid: this.duid, + items: [ + { + duid: this.duid, + operations: [ + { + model: "task", + kind: "create", + data: { + duid: this.duid, + dartboardDuid: this.dartboardId, + title: this.taskName, + description: this.description, + dueAt: this.dueAt, + assigneeDuids: this.assigneeIds, + priority: this.priority, + }, + }, + ], + kind: "task_create", + }, + ], + }, + }); + + if (!response.results[0].success) { + throw new Error(response.results[0].message); + } + + $.export("$summary", `Created task: "${this.taskName}"`); + return response; + }, +}; diff --git a/components/dart/actions/update-task/update-task.mjs b/components/dart/actions/update-task/update-task.mjs new file mode 100644 index 0000000000000..0e3d3ccbbd000 --- /dev/null +++ b/components/dart/actions/update-task/update-task.mjs @@ -0,0 +1,80 @@ +import dart from "../../dart.app.mjs"; + +export default { + key: "dart-update-task", + name: "Update Task", + description: "Updates an existing task within a dartboard. [See the documentation](https://app.itsdart.com/api/v0/docs/)", + version: "0.0.1", + type: "action", + props: { + dart, + taskId: { + propDefinition: [ + dart, + "taskId", + ], + }, + title: { + propDefinition: [ + dart, + "taskName", + ], + optional: true, + }, + description: { + propDefinition: [ + dart, + "taskDescription", + ], + }, + dueAt: { + propDefinition: [ + dart, + "dueAt", + ], + }, + assigneeIds: { + propDefinition: [ + dart, + "assigneeIds", + ], + }, + priority: { + propDefinition: [ + dart, + "priority", + ], + }, + }, + async run({ $ }) { + const response = await this.dart.createTransaction({ + $, + data: { + clientDuid: this.taskId, + items: [ + { + duid: this.taskId, + operations: [ + { + model: "task", + kind: "update", + data: { + duid: this.taskId, + dartboardDuid: this.dartboardId, + title: this.title, + description: this.description, + dueAt: this.dueAt, + assigneeDuids: this.assigneeIds, + priority: this.priority, + }, + }, + ], + kind: "task_update", + }, + ], + }, + }); + $.export("$summary", `Updated task with ID: "${this.taskId}"`); + return response; + }, +}; diff --git a/components/dart/common/constants.mjs b/components/dart/common/constants.mjs new file mode 100644 index 0000000000000..7535f7ede1bac --- /dev/null +++ b/components/dart/common/constants.mjs @@ -0,0 +1,13 @@ +const DEFAULT_LIMIT = 50; + +const TASK_PRIORITIES = [ + "Critical", + "High", + "Medium", + "Low", +]; + +export default { + DEFAULT_LIMIT, + TASK_PRIORITIES, +}; diff --git a/components/dart/dart.app.mjs b/components/dart/dart.app.mjs new file mode 100644 index 0000000000000..29a16be4fcddc --- /dev/null +++ b/components/dart/dart.app.mjs @@ -0,0 +1,165 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "dart", + propDefinitions: { + dartboardId: { + type: "string", + label: "Dartboard ID", + description: "The dartboard where the task is or will be located", + async options({ page }) { + const { results } = await this.listDartboards({ + params: { + limit: constants.DEFAULT_LIMIT, + offset: page * constants.DEFAULT_LIMIT, + }, + }); + return results?.map(({ + duid: value, title: label, + }) => ({ + value, + label, + })) || []; + }, + }, + assigneeIds: { + type: "string[]", + label: "Assigned To", + description: "The user(s) the task is assigned to", + optional: true, + async options({ page }) { + const { results } = await this.listUsers({ + params: { + limit: constants.DEFAULT_LIMIT, + offset: page * constants.DEFAULT_LIMIT, + }, + }); + return results?.map(({ + duid: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + taskId: { + type: "string", + label: "Task ID", + description: "The ID of the task", + async options({ page }) { + const { results } = await this.listTasks({ + params: { + limit: constants.DEFAULT_LIMIT, + offset: page * constants.DEFAULT_LIMIT, + }, + }); + return results?.map(({ + duid: value, title: label, + }) => ({ + value, + label, + })) || []; + }, + }, + taskName: { + type: "string", + label: "Task Name", + description: "The name of the task", + }, + taskDescription: { + type: "string", + label: "Description", + description: "The description of the task", + optional: true, + }, + dueAt: { + type: "string", + label: "Due Date", + description: "The due date for the task in ISO-8601 format. Example: `2024-06-25T15:43:49.214Z`", + optional: true, + }, + priority: { + type: "string", + label: "Priority", + description: "The priority of the task", + optional: true, + options: constants.TASK_PRIORITIES, + }, + }, + methods: { + _baseUrl() { + return "https://app.itsdart.com/api/v0"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + listDocs(opts = {}) { + return this._makeRequest({ + path: "/docs", + ...opts, + }); + }, + listTasks(opts = {}) { + return this._makeRequest({ + path: "/tasks", + ...opts, + }); + }, + listDartboards(opts = {}) { + return this._makeRequest({ + path: "/dartboards", + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users", + ...opts, + }); + }, + createTransaction(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/transactions/create", + ...opts, + }); + }, + async *paginate({ + resourceFn, params, max, + }) { + let total, count = 0; + params = { + ...params, + limit: constants.DEFAULT_LIMIT, + offset: 0, + }; + do { + const { results } = await resourceFn({ + params, + }); + total = results?.length; + for (const item of results) { + yield item; + count++; + if (max && count >= max) { + return; + } + params.offset += params.limit; + } + } while (total); + }, + }, +}; diff --git a/components/dart/package.json b/components/dart/package.json new file mode 100644 index 0000000000000..b02d530bfb443 --- /dev/null +++ b/components/dart/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/dart", + "version": "0.1.0", + "description": "Pipedream Dart Components", + "main": "dart.app.mjs", + "keywords": [ + "pipedream", + "dart" + ], + "homepage": "https://pipedream.com/apps/dart", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/dart/sources/common/base.mjs b/components/dart/sources/common/base.mjs new file mode 100644 index 0000000000000..636faaefc8fda --- /dev/null +++ b/components/dart/sources/common/base.mjs @@ -0,0 +1,62 @@ +import dart from "../../dart.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + dart, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + const resourceFn = this.getResourceFn(); + const tsField = this.getTsField(); + + const items = this.dart.paginate({ + resourceFn, + max, + }); + for await (const item of items) { + const ts = Date.parse(item[tsField]); + if (ts > lastTs) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + if (ts > maxTs) { + maxTs = ts; + } + } + } + this._setLastTs(maxTs); + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + getTsField() { + throw new Error("getTsField is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/dart/sources/new-doc-created/new-doc-created.mjs b/components/dart/sources/new-doc-created/new-doc-created.mjs new file mode 100644 index 0000000000000..bcacba8bf790d --- /dev/null +++ b/components/dart/sources/new-doc-created/new-doc-created.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "dart-new-doc-created", + name: "New Document Created", + description: "Emit new event when a new document is created in Dart.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.dart.listDocs; + }, + getTsField() { + return "createdAt"; + }, + generateMeta(doc) { + return { + id: doc.duid, + summary: `New Document Created: ${doc.duid}`, + ts: Date.parse(doc.createdAt), + }; + }, + }, + sampleEmit, +}; diff --git a/components/dart/sources/new-doc-created/test-event.mjs b/components/dart/sources/new-doc-created/test-event.mjs new file mode 100644 index 0000000000000..3572f424fd0ae --- /dev/null +++ b/components/dart/sources/new-doc-created/test-event.mjs @@ -0,0 +1,44 @@ +export default { + "duid": "7VApYuFA8cDv", + "updatedByClientDuid": "", + "createdAt": "2024-08-05T21:30:57.040460Z", + "updatedAt": "2024-08-05T21:31:03.369680Z", + "drafterDuid": null, + "inTrash": false, + "folderDuid": "BrpNiqHz2Lio", + "reportKind": null, + "order": "˜ûš&", + "title": "test", + "text": { + "root": { + "type": "root", + "format": "", + "indent": 0, + "version": 1, + "children": [ + { + "type": "paragraph", + "format": "", + "indent": 0, + "version": 1, + "children": [], + "direction": null, + "textFormat": 0 + } + ], + "direction": null + } + }, + "editedByAi": false, + "recommendationDuid": null, + "editorDuids": [ + "CAZ1ew7TlAj6" + ], + "subscriberDuids": [ + "CAZ1ew7TlAj6" + ], + "iconKind": "None", + "iconNameOrEmoji": "GallerySlashIcon", + "colorHex": "#ee7d48", + "colorName": "Dark Orange" +} \ No newline at end of file diff --git a/components/dart/sources/new-doc-updated/new-doc-updated.mjs b/components/dart/sources/new-doc-updated/new-doc-updated.mjs new file mode 100644 index 0000000000000..2dc323e68d576 --- /dev/null +++ b/components/dart/sources/new-doc-updated/new-doc-updated.mjs @@ -0,0 +1,30 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + type: "source", + key: "dart-new-doc-updated", + name: "New Document Updated", + description: "Emit new event when any document is updated.", + version: "0.0.1", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.dart.listDocs; + }, + getTsField() { + return "updatedAt"; + }, + generateMeta(doc) { + const ts = Date.parse(doc.updatedAt); + return { + id: `${doc.duid}-${ts}`, + summary: `Document Updated ${doc.duid}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/dart/sources/new-doc-updated/test-event.mjs b/components/dart/sources/new-doc-updated/test-event.mjs new file mode 100644 index 0000000000000..855f977439abb --- /dev/null +++ b/components/dart/sources/new-doc-updated/test-event.mjs @@ -0,0 +1,44 @@ +export default { + "duid": "7VApYuFA8cDv", + "updatedByClientDuid": "", + "createdAt": "2024-08-05T21:30:57.040460Z", + "updatedAt": "2024-08-05T21:33:26.149020Z", + "drafterDuid": null, + "inTrash": false, + "folderDuid": "BrpNiqHz2Lio", + "reportKind": null, + "order": "˜ûš&", + "title": "test123", + "text": { + "root": { + "type": "root", + "format": "", + "indent": 0, + "version": 1, + "children": [ + { + "type": "paragraph", + "format": "", + "indent": 0, + "version": 1, + "children": [], + "direction": null, + "textFormat": 0 + } + ], + "direction": null + } + }, + "editedByAi": false, + "recommendationDuid": null, + "editorDuids": [ + "CAZ1ew7TlAj6" + ], + "subscriberDuids": [ + "CAZ1ew7TlAj6" + ], + "iconKind": "Emoji", + "iconNameOrEmoji": "🧪", + "colorHex": "#ee7d48", + "colorName": "Dark Orange" +} \ No newline at end of file diff --git a/components/dart/sources/new-task-created/new-task-created.mjs b/components/dart/sources/new-task-created/new-task-created.mjs new file mode 100644 index 0000000000000..16341d15be391 --- /dev/null +++ b/components/dart/sources/new-task-created/new-task-created.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "dart-new-task-created", + name: "New Task Created", + description: "Emit new event when a task is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.dart.listTasks; + }, + getTsField() { + return "createdAt"; + }, + generateMeta(task) { + return { + id: task.duid, + summary: `New Task: ${task.duid}`, + ts: Date.parse(task.createdAt), + }; + }, + }, + sampleEmit, +}; diff --git a/components/dart/sources/new-task-created/test-event.mjs b/components/dart/sources/new-task-created/test-event.mjs new file mode 100644 index 0000000000000..69ed72cc8e088 --- /dev/null +++ b/components/dart/sources/new-task-created/test-event.mjs @@ -0,0 +1,59 @@ +export default { + "duid": "VEAMCZK4zj6z", + "sourceType": "AppTcm", + "updatedByClientDuid": "", + "createdAt": "2024-08-05T21:38:40.916526Z", + "createdByDuid": "CAZ1ew7TlAj6", + "updatedAt": "2024-08-05T21:38:54.224427Z", + "updatedByDuid": "CAZ1ew7TlAj6", + "drafterDuid": null, + "inTrash": false, + "dartboardDuid": "EPGErx7XXiBy", + "order": "\u0001,ÕSë", + "expanded": true, + "kind": "Task", + "title": "new task", + "description": { + "root": { + "type": "root", + "format": "", + "indent": 0, + "version": 1, + "children": [ + { + "type": "paragraph", + "format": "", + "indent": 0, + "version": 1, + "children": [], + "direction": null, + "textFormat": 0 + } + ], + "direction": null + } + }, + "notionDocument": null, + "statusDuid": "iohpkppCuwdB", + "assignedToAi": false, + "recommendationDuid": null, + "assigneeDuids": [ + "CAZ1ew7TlAj6" + ], + "subscriberDuids": [ + "CAZ1ew7TlAj6" + ], + "tagDuids": [], + "links": [], + "attachmentDuids": [], + "relationships": [], + "priority": null, + "size": null, + "startAt": null, + "dueAt": null, + "timeTracking": [], + "remindAt": null, + "recurrence": null, + "recursNextAt": null, + "properties": {} +} \ No newline at end of file diff --git a/components/darwinbox/README.md b/components/darwinbox/README.md index 85e60919670ac..581559d2e0620 100644 --- a/components/darwinbox/README.md +++ b/components/darwinbox/README.md @@ -1,13 +1,11 @@ # Overview -Darwinbox is a powerful API that allows you to build a variety of applications. -Here are some examples of what you can build with Darwinbox: - -- A social networking application that allows users to connect with each other - and share information. -- A messaging application that allows users to communicate with each other in - real time. -- A newsfeed application that allows users to stay up to date with the latest - news and information. -- A gaming application that allows users to play games with each other and - compete for high scores. +Darwinbox is a cloud-based Human Resources Management System (HRMS) that streamulates HR processes from recruitment to retirement. With the Darwinbox API, you can automate various HR tasks such as employee onboarding, performance appraisals, and leave management. The API allows you to synchronize employee data across different systems, automate HR workflows, and build custom integrations to extend the capabilities of your HR tech stack. + +# Example Use Cases + +- **Automated Employee Onboarding**: Trigger a workflow in Pipedream when a new employee is added in Darwinbox. This workflow can send a welcome email, create accounts in third-party services like Slack or G Suite, and add the new hire to relevant Asana projects for onboarding tasks. + +- **Leave Request Approval**: Set up a workflow that listens for leave requests in Darwinbox. When a request is detected, the workflow can automatically post a message in a manager's Slack channel for approval. Once approved, the workflow updates the leave status in Darwinbox and syncs the information with a Google Calendar. + +- **Performance Review Reminders**: Create a Pipedream workflow that runs on a schedule to check for upcoming performance reviews in Darwinbox. If a review is due, the workflow can send reminder emails to the employees and their managers, and post a reminder in the team's project management tool, like Trello, to ensure the review is completed in time. diff --git a/components/data247/README.md b/components/data247/README.md index 8a1a4dbe743f7..9867fb1f3f241 100644 --- a/components/data247/README.md +++ b/components/data247/README.md @@ -1,14 +1,14 @@ # Overview -The Data247 API allows developers to access data and build various types of -applications. Here are some examples of what you can build using the Data247 -API: - -- A data visualization application that shows real-time data from the Data247 - API -- An application that allows users to search for data sets and visualize the - results -- A data analysis application that uses the Data247 API to perform statistical - analysis on data sets -- A machine learning application that uses the Data247 API to train and test - models on data sets +Data247 is a powerful service that provides real-time data such as phone carrier lookup, email to SMS gateway, text/SMS verification, and append information (e.g., appending carrier data to phone numbers). By leveraging Data247 with Pipedream, you can automate various data enrichment processes, validate customer contact details, or integrate enhanced data insights into your business workflows. This empowers you to maintain data accuracy, streamline communication strategies, and customize user experiences based on rich data sets. + +# Example Use Cases + +- **Lead Enrichment for CRM** + Automate the enrichment of new leads by appending carrier information to phone numbers as they enter your CRM. This can help in segmenting leads based on carrier or geolocation, optimizing communication strategies. + +- **User Verification Workflow** + Implement an automated system that uses Data247's text/SMS verification to confirm user identities. Once a user signs up, trigger a Pipedream workflow to send a verification code and verify the user’s phone number, enhancing security. + +- **Marketing Automation Customization** + Tailor marketing campaigns by using Data247's email to SMS gateway service within a Pipedream workflow. Convert email addresses to SMS gateways, send targeted messages, and track engagement metrics for campaign optimization. diff --git a/components/data_axle_platform/README.md b/components/data_axle_platform/README.md new file mode 100644 index 0000000000000..0c9dde7631e4f --- /dev/null +++ b/components/data_axle_platform/README.md @@ -0,0 +1,11 @@ +# Overview + +Data Axle Platform API gives you access to rich datasets encompassing business, consumer, and donor information. It can empower your Pipedream workflows to execute potent data-driven strategies, such as enriching leads, verifying business details, and tailoring marketing campaigns based on demographic insights. You can tap into this well of information, integrate it with other apps, and automate actions based on the data you query. + +# Example Use Cases + +- **Lead Enrichment for CRM Systems**: Integrate Data Axle with your CRM platform on Pipedream, such as Salesforce or HubSpot. Whenever a new lead is added to your CRM, trigger a workflow that uses Data Axle to enrich the lead's information. This can provide your sales team with more context and improve the chances of conversion. + +- **Automated Data Verification for E-commerce Platforms**: Combine Data Axle with your e-commerce app like Shopify. Set up a Pipedream workflow that validates and augments business information of vendors or partners automatically upon their registration on your platform. This ensures you're working with credible entities and maintains the integrity of your marketplace. + +- **Demographic-Based Marketing Automation**: Use Data Axle to fetch demographic data and connect it with a marketing automation tool like Mailchimp on Pipedream. Create workflows that segment your audience and send personalized marketing campaigns based on demographic insights obtained from Data Axle, enhancing customer engagement and campaign performance. diff --git a/components/data_police_uk/README.md b/components/data_police_uk/README.md new file mode 100644 index 0000000000000..2dd0a480e23e0 --- /dev/null +++ b/components/data_police_uk/README.md @@ -0,0 +1,11 @@ +# Overview + +The Data.Police.UK API provides access to a wide array of UK crime data, allowing users to query information about street-level crimes, outcomes, and the location of police stations. You can leverage this data within Pipedream to build automated workflows that react to new data, analyze crime trends, or integrate with other services to enhance public awareness and safety. With Pipedream, you can create serverless workflows that connect Data.Police.UK API with other apps to streamline tasks, generate reports, or trigger alerts based on crime data. + +# Example Use Cases + +- **Crime Alert System**: Use the Data.Police.UK API to monitor crime reports in a specific area. Set up a Pipedream workflow that triggers daily, fetching the latest crime data. Connect with the Twilio app to send SMS alerts to subscribed local residents or businesses with details about recent incidents, promoting community awareness and safety. + +- **Automated Crime Analysis Report**: Create a weekly digest of crime statistics around multiple locations. The workflow can aggregate data from the Data.Police.UK API, analyze patterns or hotspots, and compile a report using Google Sheets or Data Studio. Then, automatically email the report to local authorities or community groups through the Gmail app on Pipedream, providing valuable insights for decision-making. + +- **Interactive Crime Map Integration**: Build an interactive map on your website that displays up-to-date crime data. A Pipedream workflow can regularly pull data from the Data.Police.UK API and push it to a database connected to the map. Incorporate Google Maps API to visualize the crime data geographically, offering your website visitors a real-time view of crime in different areas. diff --git a/components/data_stores/README.md b/components/data_stores/README.md index 52d980a1b38b6..6828ca04aa671 100644 --- a/components/data_stores/README.md +++ b/components/data_stores/README.md @@ -1,8 +1,11 @@ # Overview -With the Data Stores API, you can build applications that: +Data Stores are a key-value store that allow you to persist state and share data across workflows. You can perform CRUD operations, enabling dynamic data management within your serverless architecture. Use it to save results from API calls, user inputs, or interim data; then read, update, or enrich this data in subsequent steps or workflows. Data Stores simplify stateful logic and cross-workflow communication, making them ideal for tracking process statuses, aggregating metrics, or serving as a simple configuration store. -- Store data for later retrieval -- Retrieve data from a store -- Update data in a store -- Delete data from a store +# Example Use Cases + +- **User Activity Dashboard**: Aggregate event data from various sources like webhooks, timestamp them, and store in a Data Store. Then, use this data to power a real-time dashboard that displays user activities across your app. Connect with Google Sheets to periodically sync and create historical reports. + +- **Inventory Management System**: Monitor stock levels by writing inventory changes to a Data Store whenever sales or new stock events occur. Create workflows that trigger alerts when items are low in stock or auto-generate purchase orders to suppliers by integrating with an email service like SendGrid. + +- **Feature Flag Toggle**: Use a Data Store to manage feature flags for your application. Update the flags in real-time through Pipedream's API and have your app query the Data Store to toggle features on or off without deploying new code. Enhance it by connecting with Slack to notify your team when features are toggled. diff --git a/components/data_stores/actions/get-all-records/get-all-records.mjs b/components/data_stores/actions/get-all-records/get-all-records.mjs index 9b9396f96793b..fd6c4932633d8 100644 --- a/components/data_stores/actions/get-all-records/get-all-records.mjs +++ b/components/data_stores/actions/get-all-records/get-all-records.mjs @@ -4,7 +4,7 @@ export default { key: "data_stores-get-all-records", name: "Get all records", description: "Get all records in your [Pipedream Data Store](https://pipedream.com/data-stores/). The memory consumption of the workflow can be affected, since this action will be exposing, to the workflow, the entire data from the selected datastore.", - version: "0.0.2", + version: "0.0.3", type: "action", props: { app, @@ -16,18 +16,16 @@ export default { }, }, async run({ $ }) { - const { dataStore } = this; - const keys = await dataStore.keys(); - const promises = []; - for (const key of keys) { - promises.push(dataStore.get(key)); - } - const arrData = await Promise.all(promises); const objData = {}; - for (let i = 0; i < keys.length; i++) { - objData[keys[i]] = arrData[i]; + let count = 0; + for await (const [ + k, + v, + ] of this.dataStore) { + objData[k] = v; + count++; } - $.export("$summary", `Successfully returned ${keys.length} keys.`); + $.export("$summary", `Successfully returned ${count} keys.`); return objData; }, }; diff --git a/components/data_stores/package.json b/components/data_stores/package.json index d514b285fab8b..b77dd6d34fa05 100644 --- a/components/data_stores/package.json +++ b/components/data_stores/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/data_stores", - "version": "0.1.4", + "version": "0.1.5", "description": "Pipedream Data Stores Components", "main": "data_stores.app.js", "keywords": [ diff --git a/components/database/README.md b/components/database/README.md new file mode 100644 index 0000000000000..16bbe560dbcb7 --- /dev/null +++ b/components/database/README.md @@ -0,0 +1,11 @@ +# Overview + +The Database API on Pipedream allows users to execute SQL commands directly within workflows, enabling rich and dynamic data manipulation and storage. It supports PostgreSQL, MySQL, and SQLite, making it a versatile option for managing data across various database systems. With this API, users can perform tasks such as data insertion, querying, updates, and deletions, directly within their automations, facilitating real-time data processing and integration across multiple platforms. + +# Example Use Cases + +- **Customer Data Sync Across Platforms**: Automatically sync customer data between a CRM platform like Salesforce and a PostgreSQL database on Pipedream whenever new data is entered or updated in Salesforce. This workflow ensures that all customer information remains consistent and up-to-date across enterprise systems. + +- **Real-time Analytics Dashboard Update**: Set up a workflow that pulls new transaction data from an e-commerce platform (e.g., Shopify) into a MySQL database every hour and processes this data to update a real-time analytics dashboard. This can help in monitoring sales trends, customer behavior, and inventory levels, providing actionable insights almost instantly. + +- **Automated Backup of Critical Data**: Create a nightly workflow that backs up critical data from a production database to a SQLite database on Pipedream. This can include important transaction data, user information, or any other data deemed critical for business operations, ensuring data safety and compliance with data governance standards. diff --git a/components/database/actions/query-sql-database/query-sql-database.mjs b/components/database/actions/query-sql-database/query-sql-database.mjs new file mode 100644 index 0000000000000..39d5c21a73e78 --- /dev/null +++ b/components/database/actions/query-sql-database/query-sql-database.mjs @@ -0,0 +1,132 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable pipedream/props-label */ +/* eslint-disable pipedream/props-description */ +import postgresql from "../../../postgresql/postgresql.app.mjs"; +import mysql from "../../../mysql/mysql.app.mjs"; +import snowflake from "../../../snowflake/snowflake.app.mjs"; +import azure_sql from "../../../azure_sql/azure_sql.app.mjs"; +import microsoft_sql_server from "../../../microsoft_sql_server/microsoft_sql_server.app.mjs"; +import turso from "../../../turso/turso.app.mjs"; + +export default { + name: "Query SQL Database", + key: "database-query-sql-database", + description: + "Execute a SQL Query. See [our docs](https://pipedream.com/docs/databases/working-with-sql) to learn more about working with SQL in Pipedream.", + version: "0.0.4", + type: "action", + props: { + database: { + type: "app", + app: "database", + }, + db_type: { + label: "Database Type", + type: "string", + description: "Select the database type you need to query", + options: [ + { + label: "PostgreSQL", + value: "postgresql", + }, + { + label: "MySQL", + value: "mysql", + }, + { + label: "Snowflake", + value: "snowflake", + }, + { + label: "Azure SQL Server", + value: "azure_sql", + }, + { + label: "Microsoft SQL Server", + value: "microsoft_sql_server", + }, + { + label: "Turso", + value: "turso", + }, + ], + reloadProps: true, + }, + postgresql: { + ...postgresql, + hidden: true, + }, + mysql: { + ...mysql, + hidden: true, + }, + snowflake: { + ...snowflake, + hidden: true, + }, + azure_sql: { + ...azure_sql, + hidden: true, + }, + microsoft_sql_server: { + ...microsoft_sql_server, + hidden: true, + }, + turso: { + ...turso, + hidden: true, + }, + sql: { + type: "sql", + auth: { + app: "postgresql", + }, + hidden: true, + }, + }, + async additionalProps(prev) { + const db_type = this.db_type?.value || this.db_type; + + prev.snowflake.hidden = db_type !== "snowflake"; + prev.mysql.hidden = db_type !== "mysql"; + prev.postgresql.hidden = db_type !== "postgresql"; + prev.azure_sql.hidden = db_type !== "azure_sql"; + prev.microsoft_sql_server.hidden = db_type !== "microsoft_sql_server"; + prev.turso.hidden = db_type !== "turso"; + prev.sql.hidden = !db_type; + + prev.snowflake.disabled = db_type !== "snowflake"; + prev.mysql.disabled = db_type !== "mysql"; + prev.postgresql.disabled = db_type !== "postgresql"; + prev.azure_sql.disabled = db_type !== "azure_sql"; + prev.microsoft_sql_server.disabled = db_type !== "microsoft_sql_server"; + prev.turso.disabled = db_type !== "turso"; + + prev.sql.disabled = !db_type; + prev.sql.auth.app = db_type; + + return { + sql: { + type: "sql", + auth: { + app: db_type, + }, + label: "SQL Query", + }, + }; + }, + async run({ + steps, $, + }) { + const db_type = this.db_type.value || this.db_type; + const args = this[db_type].executeQueryAdapter(this.sql); + const data = await this[db_type].executeQuery(args); + $.export( + "$summary", + `Returned ${data.length} ${data.length === 1 + ? "row" + : "rows"}`, + ); + return data; + }, +}; diff --git a/components/database/database.app.mjs b/components/database/database.app.mjs new file mode 100644 index 0000000000000..c0da581e40e91 --- /dev/null +++ b/components/database/database.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "database", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/database/package.json b/components/database/package.json new file mode 100644 index 0000000000000..4de5eec816cbe --- /dev/null +++ b/components/database/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/database", + "version": "0.1.5", + "description": "Pipedream Database Components", + "main": "database.app.mjs", + "keywords": [ + "pipedream", + "database" + ], + "homepage": "https://pipedream.com/apps/database", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/databox/README.md b/components/databox/README.md index 78aa826b08c3e..ca4e398c61b39 100644 --- a/components/databox/README.md +++ b/components/databox/README.md @@ -1,9 +1,11 @@ # Overview -You can use the Databox API to build a variety of applications, including: +The Databox API enables you to create custom dashboards, integrating various data sources to track performance metrics in real-time. With Pipedream, you can leverage this API to automate the flow of data between Databox and other applications, creating bespoke monitoring solutions and data-driven workflows that save time and enhance insights. -- A dashboard for tracking your business performance -- A data visualization tool for exploring your data -- A report builder for creating custom reports -- An alerts system for monitoring your data in real-time -- A data management tool for organizing your data +# Example Use Cases + +- **Sales Performance Tracking**: Automatically push daily sales data from a CRM like Salesforce into Databox. Create triggers in Pipedream that capture new sales records and post the relevant metrics to Databox, providing a real-time view of sales performance. + +- **Marketing Campaign Analysis**: Connect Google Analytics with Databox via Pipedream to monitor the success of marketing campaigns. Set up a workflow that periodically fetches campaign data from Google Analytics and sends it to Databox to track website traffic, conversion rates, and customer behavior trends. + +- **Financial Reporting Automation**: Streamline financial reporting by integrating accounting software, such as QuickBooks, with Databox through Pipedream. Construct a workflow that extracts key financial metrics at the end of each week and populates them into Databox dashboards for up-to-date financial health monitoring. diff --git a/components/databricks/README.md b/components/databricks/README.md new file mode 100644 index 0000000000000..bc09bf6b45c12 --- /dev/null +++ b/components/databricks/README.md @@ -0,0 +1,11 @@ +# Overview + +The Databricks API allows you to interact programmatically with Databricks services, enabling you to manage clusters, jobs, notebooks, and other resources within Databricks environments. Through Pipedream, you can leverage these APIs to create powerful automations and integrate with other apps for enhanced data processing, transformation, and analytics workflows. This unlocks possibilities like automating cluster management, dynamically running jobs based on external triggers, and orchestrating complex data pipelines with ease. + +# Example Use Cases + +- **Automated Cluster Management**: Set up workflows on Pipedream to monitor cluster performance metrics and automatically scale clusters up or down based on predefined rules. This can help optimize costs and ensure performance without manual intervention. + +- **Dynamic Job Triggering with GitHub**: Create a workflow that triggers a Databricks job whenever a new commit is pushed to a specific GitHub repository. This can be used for continuous integration and deployment (CI/CD) of data processing tasks, such as ETL jobs or machine learning model training. + +- **Event-Driven Data Pipelines with Amazon S3**: Construct a serverless data pipeline on Pipedream that kicks off a Databricks job when a new file is uploaded to an Amazon S3 bucket. Use this workflow to process and analyze data in near-real-time, enabling quicker insights and decision-making. diff --git a/components/datadog/README.md b/components/datadog/README.md index 39cc009ef493c..5c044bf05698e 100644 --- a/components/datadog/README.md +++ b/components/datadog/README.md @@ -1,9 +1,11 @@ # Overview -With the Datadog API, you can build a variety of applications and tools to help -you monitor and optimize your Datadog account. Here are a few examples: +The Datadog API, accessible through Pipedream, empowers you to programmatically interact with Datadog's monitoring and analytics platform. This enables developers to automate the retrieval of monitoring data, manage alert configurations, and synchronize service health information across systems. With Pipedream's serverless execution model, you can create intricate workflows that react to Datadog events or metrics, manipulate the data, and pass it on to other services or even Datadog itself for a cohesive operational ecosystem. -- A tool to help you visualize your Datadog account data -- A tool to help you manage your Datadog account settings -- A tool to help you monitor and troubleshoot your Datadog account -- A tool to help you migrate your data to Datadog +# Example Use Cases + +- **Incident Management Integration**: Whenever Datadog detects an anomaly or a threshold breach, you can automate the creation of an incident ticket in a system like Jira or PagerDuty. This workflow ensures that your team promptly addresses service disruptions, and maintains a record of incidents for post-mortems. + +- **Slack Alert Summarization**: Configure a workflow that listens to Datadog alerts and processes them to extract key details. It can then send a well-formatted and concise summary directly to a designated Slack channel. This keeps your team informed without the noise of raw alerts, enabling quicker response times. + +- **Automated Metric Reporting**: Build a workflow that periodically fetches key performance metrics from Datadog and compiles them into a report. This report can be sent to Google Sheets, emailed to stakeholders, or even pushed back into Datadog as an event for record-keeping. It streamlines reporting tasks and ensures consistent visibility into system performance. diff --git a/components/dataforseo/README.md b/components/dataforseo/README.md new file mode 100644 index 0000000000000..10129b7d68589 --- /dev/null +++ b/components/dataforseo/README.md @@ -0,0 +1,11 @@ +# Overview + +The DataForSEO API offers a robust suite of tools for SEO analytics and research, providing data on rankings, keywords, competitor analysis, and SERP features. Integrating DataForSEO with Pipedream enables you to automate the collection of SEO data, monitor keyword positions, trigger actions based on SEO insights, and enrich your marketing or analytics platforms with up-to-date search data. + +# Example Use Cases + +- **SEO Dashboard Reporting**: Automate the extraction of keyword rankings and search volume data daily. Use this data to populate a Google Sheets SEO dashboard, offering real-time insights for decision-making. + +- **Content Alert System**: Monitor your website's content performance by setting up an alert system with Slack or email. When specific keywords achieve or lose targeted rankings, send notifications to keep your content team informed. + +- **Competitor Analysis Workflow**: Schedule regular retrieval of competitor SEO data and integrate it with a CRM like HubSpot. This enables you to track competitor movements, update sales strategies, and identify potential leads based on market changes. diff --git a/components/dataforseo/actions/get-business-listings/get-business-listings.mjs b/components/dataforseo/actions/get-business-listings/get-business-listings.mjs new file mode 100644 index 0000000000000..e8ea7197156c6 --- /dev/null +++ b/components/dataforseo/actions/get-business-listings/get-business-listings.mjs @@ -0,0 +1,65 @@ +import app from "../../dataforseo.app.mjs"; + +export default { + key: "dataforseo-get-business-listings", + name: "Get Business Listings", + description: "Get Business Listings. [See the documentation](https://docs.dataforseo.com/v3/business_data/business_listings/search/live/?bash)", + version: "0.0.1", + type: "action", + props: { + app, + locationCoordinate: { + propDefinition: [ + app, + "locationCoordinate", + ], + }, + categories: { + propDefinition: [ + app, + "categories", + ], + }, + title: { + propDefinition: [ + app, + "title", + ], + }, + description: { + propDefinition: [ + app, + "description", + ], + }, + isClaimed: { + propDefinition: [ + app, + "isClaimed", + ], + }, + limit: { + propDefinition: [ + app, + "limit", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getBusinessListings({ + $, + data: [ + { + location_coordinate: this.locationCoordinate, + categories: this.categories, + description: this.description, + title: this.title, + is_claimed: this.isClaimed, + limit: this.limit, + }, + ], + }); + $.export("$summary", `Successfully sent the request. Status: ${response.tasks[0].status_message}`); + return response; + }, +}; diff --git a/components/dataforseo/actions/get-keyword-difficulty/get-keyword-difficulty.mjs b/components/dataforseo/actions/get-keyword-difficulty/get-keyword-difficulty.mjs new file mode 100644 index 0000000000000..ec0f079888410 --- /dev/null +++ b/components/dataforseo/actions/get-keyword-difficulty/get-keyword-difficulty.mjs @@ -0,0 +1,44 @@ +import app from "../../dataforseo.app.mjs"; + +export default { + key: "dataforseo-get-keyword-difficulty", + name: "Get Keyword Difficulty", + description: "Get Keyword Difficulty. [See the documentation](https://docs.dataforseo.com/v3/dataforseo_labs/google/bulk_keyword_difficulty/live/?bash)", + version: "0.0.1", + type: "action", + props: { + app, + languageCode: { + propDefinition: [ + app, + "languageCode", + ], + }, + locationCode: { + propDefinition: [ + app, + "locationCode", + ], + }, + keywords: { + propDefinition: [ + app, + "keywords", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getKeywordDifficulty({ + $, + data: [ + { + language_code: this.languageCode, + location_code: this.locationCode, + keywords: this.keywords, + }, + ], + }); + $.export("$summary", `Successfully sent the request. Status: ${response.tasks[0].status_message}`); + return response; + }, +}; diff --git a/components/dataforseo/actions/get-ranked-keywords/get-ranked-keywords.mjs b/components/dataforseo/actions/get-ranked-keywords/get-ranked-keywords.mjs new file mode 100644 index 0000000000000..f8d8ef8eb7ffe --- /dev/null +++ b/components/dataforseo/actions/get-ranked-keywords/get-ranked-keywords.mjs @@ -0,0 +1,44 @@ +import app from "../../dataforseo.app.mjs"; + +export default { + key: "dataforseo-get-ranked-keywords", + name: "Get Ranked Keywords", + description: "Description for get-ranked-keywords. [See the documentation](https://docs.dataforseo.com/v3/keywords_data/google_ads/keywords_for_site/task_post/?bash)", + version: "0.0.1", + type: "action", + props: { + app, + locationCode: { + propDefinition: [ + app, + "locationCode", + ], + }, + targetType: { + propDefinition: [ + app, + "targetType", + ], + }, + target: { + propDefinition: [ + app, + "target", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getRankedKeywords({ + $, + data: [ + { + location_code: this.locationCode, + target_type: this.targetType, + target: this.target, + }, + ], + }); + $.export("$summary", `Successfully sent the request. Status: ${response.tasks[0].status_message}`); + return response; + }, +}; diff --git a/components/dataforseo/common/constants.mjs b/components/dataforseo/common/constants.mjs new file mode 100644 index 0000000000000..004a1c4b1e0db --- /dev/null +++ b/components/dataforseo/common/constants.mjs @@ -0,0 +1,6 @@ +export default { + TARGET_TYPES: [ + "site", + "page", + ], +}; diff --git a/components/dataforseo/dataforseo.app.mjs b/components/dataforseo/dataforseo.app.mjs new file mode 100644 index 0000000000000..97c91b754813c --- /dev/null +++ b/components/dataforseo/dataforseo.app.mjs @@ -0,0 +1,143 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "dataforseo", + propDefinitions: { + locationCode: { + type: "string", + label: "Location Code", + description: "The code of the target location", + async options() { + const response = await this.getLocations(); + const languageCodes = response.tasks[0].result; + return languageCodes.map(({ + location_name, location_code, + }) => ({ + value: location_code, + label: location_name, + })); + }, + }, + locationCoordinate: { + type: "string", + label: "Location Coordinate", + description: "The coordinate of the target location. It should be specified in the “latitude,longitude,radius” format, i.e.: `53.476225,-2.243572,200`", + }, + targetType: { + type: "string", + label: "Target Type", + description: "The type of the target", + options: constants.TARGET_TYPES, + }, + target: { + type: "string", + label: "Target", + description: "The domain name or the url of the target website or page", + }, + categories: { + type: "string[]", + label: "Categories", + description: "The categories you specify are used to search for business listings", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "The description of the business entity for which the results are collected", + optional: true, + }, + title: { + type: "string", + label: "Title", + description: "The name of the business entity for which the results are collected", + optional: true, + }, + isClaimed: { + type: "boolean", + label: "Is Claimed", + description: "Indicates whether the business is verified by its owner on Google Maps", + optional: true, + }, + limit: { + type: "string", + label: "Limit", + description: "The maximum number of returned businesses", + optional: true, + }, + languageCode: { + type: "string", + label: "Language Code", + description: "The language code for the request", + async options() { + const response = await this.getLanguageCode(); + const languageCodes = response.tasks[0].result; + return languageCodes.map(({ + language_name, language_code, + }) => ({ + value: language_code, + label: language_name, + })); + }, + }, + keywords: { + type: "string[]", + label: "Keywords", + description: "Target Keywords. The maximum number of keywords is 1000", + }, + }, + methods: { + _baseUrl() { + return "https://api.dataforseo.com/v3"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + auth: { + username: `${this.$auth.api_login}`, + password: `${this.$auth.api_password}`, + }, + }); + }, + async getKeywordDifficulty(args = {}) { + return this._makeRequest({ + path: "/dataforseo_labs/google/bulk_keyword_difficulty/live", + method: "post", + ...args, + }); + }, + async getBusinessListings(args = {}) { + return this._makeRequest({ + path: "/business_data/business_listings/search/live", + method: "post", + ...args, + }); + }, + async getRankedKeywords(args = {}) { + return this._makeRequest({ + path: "/keywords_data/google_ads/keywords_for_site/live", + method: "post", + ...args, + }); + }, + async getLanguageCode(args = {}) { + return this._makeRequest({ + path: "/keywords_data/google_ads/languages", + ...args, + }); + }, + async getLocations(args = {}) { + return this._makeRequest({ + path: "/serp/google/ads_search/locations", + ...args, + }); + }, + }, +}; diff --git a/components/dataforseo/package.json b/components/dataforseo/package.json new file mode 100644 index 0000000000000..31d0b5466fb61 --- /dev/null +++ b/components/dataforseo/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/dataforseo", + "version": "0.1.0", + "description": "Pipedream DataForSEO Components", + "main": "dataforseo.app.mjs", + "keywords": [ + "pipedream", + "dataforseo" + ], + "homepage": "https://pipedream.com/apps/dataforseo", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/datagma/README.md b/components/datagma/README.md new file mode 100644 index 0000000000000..d2eaf901a200f --- /dev/null +++ b/components/datagma/README.md @@ -0,0 +1,11 @@ +# Overview + +The Datagma API allows for the extraction and analysis of valuable data from emails. By using this API, you can automate the process of parsing emails to retrieve information like signatures, contact details, and other pertinent data. In Pipedream, this functionality becomes a powerful tool for creating workflows that can help to streamline business processes, enhance customer relationship management, or fuel data enrichment strategies. + +# Example Use Cases + +- **Automated Contact Information Extraction**: Use the Datagma API to parse incoming emails for new contacts. When a new email arrives, extract the sender's signature and details, and save them in a Google Sheets document for easy access and management. + +- **Enhanced Customer Support Ticketing**: Integrate the Datagma API to scan support emails for relevant information. Automatically create tickets in a service like Zendesk or HubSpot, populating them with the extracted sender details and any highlighted issues. + +- **Email-based Event Registration**: Set up a workflow where Datagma parses registration emails for an event. Extract attendee information and automatically add it to an event management app like Eventbrite or to your own database, ensuring a seamless sign-up process. diff --git a/components/datagma/package.json b/components/datagma/package.json index 676ddc8d261f9..79277052c329b 100644 --- a/components/datagma/package.json +++ b/components/datagma/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/datanews/package.json b/components/datanews/package.json new file mode 100644 index 0000000000000..bf82a68966c43 --- /dev/null +++ b/components/datanews/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/datanews", + "version": "0.6.0", + "description": "Pipedream datanews Components", + "main": "datanews.app.mjs", + "keywords": [ + "pipedream", + "datanews" + ], + "homepage": "https://pipedream.com/apps/datanews", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/datarobot/README.md b/components/datarobot/README.md index 04f8bff05eaa4..d35cf4dcf7caa 100644 --- a/components/datarobot/README.md +++ b/components/datarobot/README.md @@ -1,8 +1,11 @@ # Overview -With the Datarobot API, you can: +DataRobot offers an AI platform that empowers users to build and deploy predictive models. Through its API, you can manage datasets, kick off model training, retrieve predictive scores, and integrate machine learning insights into your business processes. With Pipedream, you can create serverless workflows that leverage DataRobot's capabilities, enabling a powerful synergy between machine learning and everyday business automation. -- Build predictive models -- Extract predictions from models -- Get insights into how models work -- Manage models and model versions +# Example Use Cases + +- **Automated Lead Scoring**: Integrate DataRobot with a CRM like Salesforce in Pipedream. As new leads come in, trigger a workflow that sends the lead data to DataRobot, runs a prediction to score the lead's likelihood to convert, and then updates the lead score in Salesforce. This ensures that sales teams focus on the most promising leads. + +- **Real-time Fraud Detection**: Link DataRobot with transactional systems such as Stripe. When a new transaction occurs, a Pipedream workflow can invoke DataRobot to analyze the transaction for potential fraud. If a high probability of fraud is detected, the workflow can automatically flag the transaction and alert your risk management team. + +- **Dynamic Pricing Adjustments**: Combine DataRobot with an eCommerce platform like Shopify. Set up a Pipedream workflow that triggers whenever a new order is placed. The workflow sends the order data to DataRobot to predict demand and suggests price adjustments. This can help optimize revenue by dynamically pricing products based on changing market conditions. diff --git a/components/datarobot/package.json b/components/datarobot/package.json new file mode 100644 index 0000000000000..13c10d016cca3 --- /dev/null +++ b/components/datarobot/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/datarobot", + "version": "0.6.0", + "description": "Pipedream datarobot Components", + "main": "datarobot.app.mjs", + "keywords": [ + "pipedream", + "datarobot" + ], + "homepage": "https://pipedream.com/apps/datarobot", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/datascope/README.md b/components/datascope/README.md index a550642cdda42..310c8cb833c67 100644 --- a/components/datascope/README.md +++ b/components/datascope/README.md @@ -1,8 +1,11 @@ # Overview -The DataScope API provides data that can be used to build a variety of applications. Here are some examples of what you can build using the DataScope API: +DataScope is a versatile API for data collection and management, ideal for streamlining workflows that involve forms, surveys, or data entry. With DataScope, you can automate the consolidation of data gathered from various sources, analyze submissions in real-time, and trigger actions based on incoming data. It’s a powerhouse for anyone looking to digitize and automate paper-based processes, conduct field research, or collect structured data on the go. -- A social network for connecting with different people based on interests and data -- A data-driven marketing tool that lets you segment your audience and target them with relevant content -- A data analytics tool that helps you understand your customers and make better business decisions -- A customer support tool that uses data to help you resolve customer issues faster +# Example Use Cases + +- **Automated Survey Data Aggregation**: Use DataScope to collect responses from surveys or forms. With Pipedream, you can automatically transfer these responses to a Google Sheets document for easy analysis and sharing. This workflow can be set to run every time a new response is received, ensuring your data is always up-to-date. + +- **Customer Feedback to CRM Integration**: Gather customer feedback through DataScope forms. Pipedream can funnel this information into your Customer Relationship Management (CRM) system, like Salesforce or HubSpot. This helps keep customer data centralized, and allows for automated follow-up actions, such as triggering customer service processes or updating contact records with new insights. + +- **Field Research Data Processing**: For field research data collected via DataScope, set up a Pipedream workflow to parse, validate, and store this data in a cloud database like AWS RDS or MongoDB. Additionally, Pipedream can send notifications to team members on Slack or via email when new data meets certain criteria, enabling prompt analysis and decision-making. diff --git a/components/datascope/package.json b/components/datascope/package.json new file mode 100644 index 0000000000000..fa0ab64d51b23 --- /dev/null +++ b/components/datascope/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/datascope", + "version": "0.6.0", + "description": "Pipedream datascope Components", + "main": "datascope.app.mjs", + "keywords": [ + "pipedream", + "datascope" + ], + "homepage": "https://pipedream.com/apps/datascope", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/dataset/README.md b/components/dataset/README.md new file mode 100644 index 0000000000000..10cd3db65fc18 --- /dev/null +++ b/components/dataset/README.md @@ -0,0 +1,11 @@ +# Overview + +The DataSet API enables you to manage and analyze large datasets. With Pipedream's integration, you can automate data ingestion, transformation, and analysis, create serverless workflows that respond to data events, and connect with other apps to enrich and utilize your datasets. Utilize Pipedream's serverless platform to trigger workflows, perform actions based on DataSet events, and orchestrate complex data processes with minimal setup. + +# Example Workflows + +- **Data Enrichment and Analysis Workflow**: Ingest raw data from various sources into DataSet using Pipedream's HTTP / Webhook triggers. Transform and enrich the data by integrating with apps like Clearbit for adding company information. Analyze enriched data within DataSet and send insights to a Slack channel for real-time decision-making. + +- **Automated Data Backup Workflow**: Schedule regular backups of your DataSet data to cloud storage platforms like Google Drive or Dropbox using Pipedream’s cron job feature. This can ensure data redundancy and safety, creating a historical archive of your dataset changes over time. + +- **Real-time Alerts and Monitoring Workflow**: Monitor your DataSet for specific changes or thresholds using Pipedream's scheduled tasks. When a particular condition is met, trigger notifications through apps like Twilio to send SMS alerts or use SendGrid to dispatch email notifications, keeping relevant stakeholders informed instantly. diff --git a/components/datawaves/README.md b/components/datawaves/README.md index a351b81612511..5b9121c443fb3 100644 --- a/components/datawaves/README.md +++ b/components/datawaves/README.md @@ -1,18 +1,11 @@ # Overview -Datawaves is a powerful API that enables developers to easily and efficiently -access data from a variety of sources. With Datawaves, you can easily connect -to and query data from popular data sources such as Amazon Redshift, Google -BigQuery, and Microsoft SQL Server. +The Datawaves API offers rich functionality for tracking and analyzing user interactions on websites and applications. With this API, you can capture events, analyze user behavior, and gain insights that drive decision-making for product enhancements and personalized user experiences. On Pipedream, you can harness these capabilities to create automated workflows that respond to data collected by Datawaves in real-time, integrate with other services, and streamline analytics processes. -In addition, Datawaves provides a number of features that make it easy to work -with data, including: +# Example Use Cases -- Query Builder: A powerful query builder that makes it easy to construct - complex queries. -- Data Explorer: A visual interface that makes it easy to explore and analyze - data. -- Support for popular data formats: Datawaves supports CSV, JSON, and XML. -- Integration with popular programming languages: Datawaves offers native - integrations with popular programming languages such as Java, Python, and - Node.js. +- **User Engagement Reporting**: Automate the generation of user engagement reports by triggering a Pipedream workflow every time a new event is captured by Datawaves. The workflow could aggregate event data and filter based on specific user actions, then format this data into a report and send it through email or post it on a Slack channel for immediate review by your team. + +- **Real-time User Behavior Alerts**: Set up a workflow that listens for specific high-value events captured by Datawaves, such as a user completing a purchase or reaching a particular milestone in your app. When such an event occurs, the workflow can trigger personalized notifications, send out discount codes via SMS using Twilio, or even trigger additional in-app actions to enhance user satisfaction. + +- **A/B Test Analysis**: When running A/B tests on your website, utilize Datawaves with Pipedream to track the performance of different test variations. Create workflows that automatically collect test data, then use this data to determine the winning variation. Integrate with Google Sheets to log this data for further analysis, or connect with a visualization tool like Google Data Studio to create dynamic, real-time dashboards. diff --git a/components/datawaves/package.json b/components/datawaves/package.json new file mode 100644 index 0000000000000..f9f99885841e9 --- /dev/null +++ b/components/datawaves/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/datawaves", + "version": "0.6.0", + "description": "Pipedream datawaves Components", + "main": "datawaves.app.mjs", + "keywords": [ + "pipedream", + "datawaves" + ], + "homepage": "https://pipedream.com/apps/datawaves", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/datumbox/README.md b/components/datumbox/README.md index 4b8ae2a05801b..a73e94551dd85 100644 --- a/components/datumbox/README.md +++ b/components/datumbox/README.md @@ -1,4 +1,11 @@ # Overview -Go to the [Datumbox API docs](https://www.datumbox.com/machine-learning-api/) -to find out what you can build using their API! +The Datumbox API offers a robust suite of Machine Learning services that can enrich your applications with advanced text analysis, sentiment analysis, classification, and more. Leveraging this API on Pipedream allows you to create powerful serverless workflows that connect Datumbox's capabilities with numerous other apps and services. Automate content categorization, extract insights from user feedback, or monitor social media sentiment in real-time. + +# Example Use Cases + +- **Content Classification Automation**: Automatically categorize articles, blog posts, or web content as they are published. Set up a Pipedream workflow where a webhook triggers the process whenever new content is detected. The text is sent to Datumbox for classification, and the results are posted to a Slack channel or logged into a Google Sheet for editorial review. + +- **Customer Feedback Analysis**: Integrate Datumbox with a customer support tool like Zendesk. When a customer submits feedback or a support ticket, the text is analyzed by Datumbox to determine sentiment and main topics. This analysis can help prioritize urgent tickets and gather insights, which are then stored in a CRM like Salesforce or HubSpot. + +- **Social Media Sentiment Monitoring**: Connect Datumbox to Twitter via Pipedream. Create a workflow that monitors tweets mentioning your brand, uses the Datumbox API to assess the sentiment, and triggers different actions based on the result. Positive tweets might be retweeted or added to a testimonial database, while negative ones could trigger alerts to your customer service team. diff --git a/components/dayschedule/README.md b/components/dayschedule/README.md new file mode 100644 index 0000000000000..249044df7c279 --- /dev/null +++ b/components/dayschedule/README.md @@ -0,0 +1,11 @@ +# Overview + +The DaySchedule API enables the automation of appointment scheduling tasks, allowing users to create, update, and manage appointments efficiently. With Pipedream, you can harness this API to integrate with various services, triggering workflows based on events like new bookings or cancellations. You can orchestrate data flows between DaySchedule and other apps you use for CRM, email marketing, customer support, or internal communication, streamlining the process of managing your calendar and client interactions. + +# Example Use Cases + +- **Automate Customer Follow-ups**: After a client books an appointment through DaySchedule, trigger a Pipedream workflow that sends a personalized follow-up email via SendGrid to confirm the details and express gratitude for their booking. + +- **Sync Appointments with Google Calendar**: Create a workflow that listens for new appointments on DaySchedule and automatically adds them to a Google Calendar, ensuring your schedule is always up-to-date across all platforms. + +- **Manage Cancellations and Rescheduling**: When an appointment is canceled or rescheduled in DaySchedule, trigger a workflow that updates the status in your CRM tool, like Salesforce, and sends a real-time notification to a Slack channel to inform the team. diff --git a/components/dbt/README.md b/components/dbt/README.md new file mode 100644 index 0000000000000..028e3e6d474d0 --- /dev/null +++ b/components/dbt/README.md @@ -0,0 +1,11 @@ +# Overview + +The dbt Cloud API allows users to initiate jobs, check on their status, and interact with dbt Cloud programmatically. On Pipedream, you can harness this functionality to automate workflows, such as triggering dbt runs, monitoring your data transformation jobs, and integrating dbt Cloud with other data services. By leveraging Pipedream's serverless platform, you can create custom workflows that act on dbt Cloud events or use the dbt Cloud API to manage your data transformation processes seamlessly. + +# Example Use Cases + +- **Trigger dbt Cloud Jobs from GitHub Pushes**: Automatically start a dbt Cloud job when changes are pushed to specific branches in your GitHub repository. This ensures that your data transformations are always in sync with your latest codebase. + +- **Monitor dbt Cloud Job Statuses and Alert via Slack**: Set up a workflow on Pipedream that polls dbt Cloud job statuses at regular intervals. If a job fails or completes, send an alert with job details to a designated Slack channel, keeping your team informed in real time. + +- **Sync dbt Cloud Metadata with Google Sheets**: Extract metadata from completed dbt Cloud jobs and append it to a Google Sheet. This workflow can help you build a custom log or report, providing insights into your data transformation jobs without needing to access dbt Cloud's interface. diff --git a/components/dbt/actions/get-environment/get-environment.mjs b/components/dbt/actions/get-environment/get-environment.mjs index 8b226e7005d89..2d8617c688fd9 100644 --- a/components/dbt/actions/get-environment/get-environment.mjs +++ b/components/dbt/actions/get-environment/get-environment.mjs @@ -4,7 +4,7 @@ export default { key: "dbt-get-environment", name: "Get Environment", description: "Retrieve information about an environment. [See the documentation](https://docs.getdbt.com/dbt-cloud/api-v3#/operations/Retrieve%20Projects%20Environment)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { dbt, diff --git a/components/dbt/actions/get-run-artifact/get-run-artifact.mjs b/components/dbt/actions/get-run-artifact/get-run-artifact.mjs index 8fd442c3e0f73..c275866e955f0 100644 --- a/components/dbt/actions/get-run-artifact/get-run-artifact.mjs +++ b/components/dbt/actions/get-run-artifact/get-run-artifact.mjs @@ -4,7 +4,7 @@ export default { key: "dbt-get-run-artifact", name: "Get Run Artifact", description: "Retrieve information about a run artifact. [See the documentation](https://docs.getdbt.com/dbt-cloud/api-v2#/operations/Retrieve%20Run%20Artifact)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { dbt, diff --git a/components/dbt/actions/get-run/get-run.mjs b/components/dbt/actions/get-run/get-run.mjs index d39178da38190..7e3ce2a52db50 100644 --- a/components/dbt/actions/get-run/get-run.mjs +++ b/components/dbt/actions/get-run/get-run.mjs @@ -4,7 +4,7 @@ export default { key: "dbt-get-run", name: "Get Run", description: "Retrieve information about a run. [See the documentation](https://docs.getdbt.com/dbt-cloud/api-v2#/operations/Retrieve%20Run)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { dbt, diff --git a/components/dbt/actions/trigger-job-run/trigger-job-run.mjs b/components/dbt/actions/trigger-job-run/trigger-job-run.mjs index c6f770340ac34..0c504b76a79d5 100644 --- a/components/dbt/actions/trigger-job-run/trigger-job-run.mjs +++ b/components/dbt/actions/trigger-job-run/trigger-job-run.mjs @@ -4,7 +4,7 @@ export default { key: "dbt-trigger-job-run", name: "Trigger Job Run", description: "Trigger a specified job to begin running. [See the documentation](https://docs.getdbt.com/dbt-cloud/api-v2#/operations/Trigger%20Job%20Run)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { dbt, diff --git a/components/dbt/dbt.app.mjs b/components/dbt/dbt.app.mjs index c896fa127fa50..a999f34ddd509 100644 --- a/components/dbt/dbt.app.mjs +++ b/components/dbt/dbt.app.mjs @@ -151,7 +151,9 @@ export default { }, methods: { _baseUrl() { - return "https://cloud.getdbt.com/api"; + return this.$auth.access_url + ? `${this.$auth.access_url}api` + : `https://${this.$auth.region}.com/api`; }, _headers() { return { diff --git a/components/dbt/package.json b/components/dbt/package.json index 95939d2774d24..ce828f0a6dc0a 100644 --- a/components/dbt/package.json +++ b/components/dbt/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/dbt", - "version": "0.2.0", + "version": "0.2.1", "description": "Pipedream dbt Cloud Components", "main": "dbt.app.mjs", "keywords": [ @@ -13,6 +13,6 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.5.1" + "@pipedream/platform": "^3.0.3" } } diff --git a/components/dbt/sources/job-run-completed/job-run-completed.mjs b/components/dbt/sources/job-run-completed/job-run-completed.mjs index 7f6ef4bccf188..3f0d6acec5519 100644 --- a/components/dbt/sources/job-run-completed/job-run-completed.mjs +++ b/components/dbt/sources/job-run-completed/job-run-completed.mjs @@ -6,7 +6,7 @@ export default { key: "dbt-job-run-completed", name: "Job Run Completed (Instant)", description: "Emit new event when a job run has completed.", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "unique", methods: { diff --git a/components/dbt/sources/job-run-errored/job-run-errored.mjs b/components/dbt/sources/job-run-errored/job-run-errored.mjs index 06b3750dee6e0..4a18f0f238c27 100644 --- a/components/dbt/sources/job-run-errored/job-run-errored.mjs +++ b/components/dbt/sources/job-run-errored/job-run-errored.mjs @@ -6,7 +6,7 @@ export default { key: "dbt-job-run-errored", name: "Job Run Errored (Instant)", description: "Emit new event when a job run has errored.", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "unique", methods: { diff --git a/components/dbt/sources/job-run-started/job-run-started.mjs b/components/dbt/sources/job-run-started/job-run-started.mjs index 7e217aac1e447..2d937b65a3e14 100644 --- a/components/dbt/sources/job-run-started/job-run-started.mjs +++ b/components/dbt/sources/job-run-started/job-run-started.mjs @@ -6,7 +6,7 @@ export default { key: "dbt-job-run-started", name: "Job Run Started (Instant)", description: "Emit new event when a job run has started.", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "unique", methods: { diff --git a/components/dbt/sources/new-job-run-event/new-job-run-event.mjs b/components/dbt/sources/new-job-run-event/new-job-run-event.mjs index 0ce1c6ae5a587..b669e9252c5a6 100644 --- a/components/dbt/sources/new-job-run-event/new-job-run-event.mjs +++ b/components/dbt/sources/new-job-run-event/new-job-run-event.mjs @@ -6,7 +6,7 @@ export default { key: "dbt-new-job-run-event", name: "New Job Run Event (Instant)", description: "Emit new event when a job run has started, errored, or completed.", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "unique", props: { diff --git a/components/dealmachine/README.md b/components/dealmachine/README.md new file mode 100644 index 0000000000000..ff739320ed08e --- /dev/null +++ b/components/dealmachine/README.md @@ -0,0 +1,11 @@ +# Overview + +The DealMachine API allows for automation of real estate investment tasks such as property research, direct mail marketing, and lead management. By connecting the DealMachine API to Pipedream, users can automate workflows, combine real estate data with other apps, and trigger custom actions based on specific criteria. Pipedream's serverless platform provides a powerful way to interact with the DealMachine API, process data, and integrate with countless other services to optimize real estate investment strategies. + +# Example Use Cases + +- **Automated Property Outreach**: When a new property is added to DealMachine, automatically send a personalized postcard or email to the property owner using Pipedream's integration with postal or email services, initiating contact and expressing interest in their property. + +- **Lead Qualification Pipeline**: Connect DealMachine to a CRM like Salesforce on Pipedream. When a new lead is created in DealMachine, check the property details, score the lead based on predefined criteria, and add qualified leads to the CRM for further follow-up. This can streamline the process of managing potential investment opportunities. + +- **Real-Time Notifications for Deal Updates**: Set up a workflow on Pipedream to listen for status updates on deals in DealMachine. When a deal's status changes, trigger an instant notification to a Slack channel or send an SMS via Twilio, keeping the investment team up-to-date on the latest developments. diff --git a/components/dear/README.md b/components/dear/README.md index e2c5ed757e651..d193cffc51cfb 100644 --- a/components/dear/README.md +++ b/components/dear/README.md @@ -1,11 +1,11 @@ # Overview -With the DEAR Systems API, you can build a range of applications that can -automate your business processes. Below are some examples of what you can -build: - -- An application to automatically generate invoices and send them to customers -- A system to track inventory levels and send alerts when levels are low -- An app to manage sales orders and send shipping notifications -- A tool to streamline your purchasing process by integrating with supplier - systems +DEAR Systems offers an API that provides programmatic access to its inventory management platform, allowing for seamless integration with other apps for automating various business processes. Leveraging this API within Pipedream, you can create custom workflows to synchronize inventory levels, manage sales and purchase orders, and automate financial reporting among other tasks. By connecting DEAR Systems to Pipedream’s vast array of supported apps, you can achieve a high degree of automation, reducing manual entry and data errors, and gaining insights from real-time data processing. + +# Example Use Cases + +- **Inventory Level Sync with Shopify**: When stock levels change in DEAR Systems, trigger a workflow that updates the corresponding inventory quantities in Shopify. This ensures that your online storefront reflects accurate stock information, preventing overselling and enhancing customer trust. + +- **Automated Purchase Order Processing**: Automatically create purchase orders in DEAR Systems when your stock falls below a certain threshold. This workflow can be triggered by low inventory alerts and can also notify suppliers or internal teams via email or messaging apps like Slack. + +- **Financial Reporting to Google Sheets**: Consolidate financial data from DEAR Systems into a Google Sheets spreadsheet at regular intervals. This can help maintain up-to-date financial records for review and analysis, enabling businesses to keep an eye on cash flow, profit margins, and other critical financial metrics. diff --git a/components/decision_journal/README.md b/components/decision_journal/README.md new file mode 100644 index 0000000000000..fa38652e0a880 --- /dev/null +++ b/components/decision_journal/README.md @@ -0,0 +1,11 @@ +# Overview + +The Decision Journal API allows for the systematic tracking of decisions over time, providing a structured way to log and evaluate past choices. By integrating with Pipedream, users can automate workflows to capture decisions from various sources, set reminders for review, analyze decision patterns, and sync with other productivity tools. This supports improved decision-making processes and personal growth. + +# Example Use Cases + +- **Automated Decision Capture from Emails**: Set up a workflow that monitors your email inbox for messages with a specific subject line or keyword that indicates a decision. Use Pipedream's built-in email trigger to parse the content and automatically log it as a new decision in your Decision Journal. + +- **Scheduled Decision Reviews**: Create a recurring workflow that sends you a reminder to review past decisions. The trigger can be a scheduled event in Pipedream, which then fetches decisions from the Decision Journal based on the date and sends a summary via Slack, email, or another preferred communication app. + +- **Decision Analysis and Reporting**: Design a workflow that periodically collects decision data from Decision Journal, analyzes trends or patterns using a Python or JavaScript code step, and generates a report. The report can be sent to Google Sheets for further analysis or visualization, or pushed to a dashboard app like Geckoboard. diff --git a/components/deel/README.md b/components/deel/README.md new file mode 100644 index 0000000000000..fa3d3c8e2b9d3 --- /dev/null +++ b/components/deel/README.md @@ -0,0 +1,11 @@ +# Overview + +The Deel API offers a set of endpoints to automate and integrate contractor payments and compliance management within your business processes. Using Pipedream, you can connect the Deel API with hundreds of other services to streamline HR operations, financial reporting, and more. Pipedream's serverless platform allows you to trigger workflows from various events, facilitating real-time data synchronization and task automation without maintaining infrastructure. + +# Example Use Cases + +- **Automated Contractor Onboarding and Payments**: When a new contractor is added to your HR platform, use Pipedream to automatically create a contract in Deel. Set up a payment schedule and track invoices without manual intervention by connecting Deel with your accounting software. + +- **Compliance Alerting**: Monitor changes in compliance status for your contracts in Deel, and use Pipedream to send alerts through Slack or email. This helps you stay on top of any compliance issues and take immediate action. + +- **Expense Tracking Integration**: Integrate Deel with your expense tracking system. Whenever a contractor submits an expense on Deel, trigger a workflow in Pipedream to log the expense in your bookkeeping software, like QuickBooks, ensuring accurate and up-to-date financial records. diff --git a/components/deel/package.json b/components/deel/package.json index dc24c37b457ab..c232f69b8d6d5 100644 --- a/components/deel/package.json +++ b/components/deel/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/deepgram/README.md b/components/deepgram/README.md index 98e26c0007a30..90ebfa552e30b 100644 --- a/components/deepgram/README.md +++ b/components/deepgram/README.md @@ -1,7 +1,11 @@ # Overview -Deepgram’s API can be used to transcribe audio and video files, with support -for various file formats and languages. Transcriptions can be stored and -retrieved, and various text processing capabilities are available to enhance -the transcripts. Additionally, the API can be used to search through -transcripts for specific terms. +The Deepgram API offers powerful speech recognition capabilities, enabling you to transcribe, search, and analyze audio data with ease. With its cutting-edge machine learning models, you can uncover insights from voice conversations, automate content moderation, and enhance user experience by converting spoken words into text. Pipedream, as a serverless integration platform, empowers you to craft custom workflows that harness the potential of Deepgram. By connecting to various apps, you can automate complex tasks, analyze audio data on-the-fly, and respond to audio-driven events. + +# Example Use Cases + +- **Voice-Driven Content Creation**: Tap into podcast episodes or webinars by transcribing the audio with Deepgram, then use Pipedream to push the text to a content management system like WordPress. This workflow can simplify content repurposing, making it searchable and accessible. + +- **Customer Support Analysis**: Automatically transcribe customer support calls with Deepgram, then analyze sentiment and keywords using the Pipedream workflow to integrate with apps like Google Sheets or Airtable. You can track trends, monitor service quality, and identify areas for improvement. + +- **Real-Time Translation and Subtitling**: Use Deepgram to transcribe live audio streams and leverage Pipedream to connect with a translation service like Google Translate. You can then output real-time subtitles to a streaming platform or broadcast service, breaking down language barriers. diff --git a/components/deepgram/actions/transcribe-audio/transcribe-audio.mjs b/components/deepgram/actions/transcribe-audio/transcribe-audio.mjs index bd99e284bf339..29ab7f9b11199 100644 --- a/components/deepgram/actions/transcribe-audio/transcribe-audio.mjs +++ b/components/deepgram/actions/transcribe-audio/transcribe-audio.mjs @@ -6,7 +6,7 @@ export default { key: "deepgram-transcribe-audio", name: "Transcribe Audio", description: "Transcribes the specified audio file. [See the documentation](https://developers.deepgram.com/api-reference/transcription/#transcribe-pre-recorded-audio)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { deepgram, @@ -51,7 +51,7 @@ export default { type: "boolean", label: "Detect Language", description: "Indicates whether to detect the language of the provided audio", - optional: true, + default: true, }, punctuate: { type: "boolean", @@ -86,8 +86,8 @@ export default { smartFormat: { type: "boolean", label: "Smart Format", - description: "Indicates whether to apply formatting to transcript output", - optional: true, + description: "Applies additional formatting to transcripts to optimize them for human readability.", + default: true, }, multiChannel: { type: "boolean", diff --git a/components/deepgram/package.json b/components/deepgram/package.json index 6da9fdfc3fc1a..839212b942eff 100644 --- a/components/deepgram/package.json +++ b/components/deepgram/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/deepgram", - "version": "0.0.6", + "version": "0.0.7", "description": "Pipedream Deepgram Components", "main": "deepgram.app.mjs", "keywords": [ diff --git a/components/deepimage/actions/auto-enhance/auto-enhance.mjs b/components/deepimage/actions/auto-enhance/auto-enhance.mjs new file mode 100644 index 0000000000000..22f933424d039 --- /dev/null +++ b/components/deepimage/actions/auto-enhance/auto-enhance.mjs @@ -0,0 +1,30 @@ +import { getUrlOrFile } from "../../common/utils.mjs"; +import deepimage from "../../deepimage.app.mjs"; + +export default { + key: "deepimage-auto-enhance", + name: "Auto Enhance Image", + description: "Improves the provided image. [See the documentation](https://documentation.deep-image.ai/image-processing/auto-enhance)", + version: "0.0.1", + type: "action", + props: { + deepimage, + image: { + propDefinition: [ + deepimage, + "image", + ], + }, + }, + async run({ $ }) { + const response = await this.deepimage.makeRequest({ + data: { + url: getUrlOrFile(this.image), + preset: "auto_enhance", + }, + }); + + $.export("$summary", "Successfully enhanced the image."); + return response; + }, +}; diff --git a/components/deepimage/actions/remove-background/remove-background.mjs b/components/deepimage/actions/remove-background/remove-background.mjs new file mode 100644 index 0000000000000..4fff3c45ced44 --- /dev/null +++ b/components/deepimage/actions/remove-background/remove-background.mjs @@ -0,0 +1,55 @@ +import { + BACKGROUND_COLOR_OPTIONS, CROP_TYPE_OPTIONS, +} from "../../common/constants.mjs"; +import { getUrlOrFile } from "../../common/utils.mjs"; +import deepimage from "../../deepimage.app.mjs"; + +export default { + key: "deepimage-remove-background", + name: "Remove Background", + description: "Removes the background from the provided image using DeepImage. [See the documentation](https://documentation.deep-image.ai/image-processing/background-processing)", + version: "0.0.1", + type: "action", + props: { + deepimage, + image: { + propDefinition: [ + deepimage, + "image", + ], + }, + backgroundColor: { + type: "string", + label: "Background Color", + description: "The background color for the image.", + options: BACKGROUND_COLOR_OPTIONS, + }, + cropType: { + type: "string", + label: "Crop Type", + description: "The crop type for background removal.", + optional: true, + options: CROP_TYPE_OPTIONS, + }, + }, + async run({ $ }) { + const response = await this.deepimage.makeRequest({ + $, + data: { + url: getUrlOrFile(this.image), + background: { + remove: "auto", + color: this.backgroundColor, + }, + fit: this.cropType + ? { + crop: this.cropType, + } + : {}, + }, + }); + + $.export("$summary", "Background removal successful"); + return response; + }, +}; diff --git a/components/deepimage/actions/upscale/upscale.mjs b/components/deepimage/actions/upscale/upscale.mjs new file mode 100644 index 0000000000000..8043b35473bdd --- /dev/null +++ b/components/deepimage/actions/upscale/upscale.mjs @@ -0,0 +1,44 @@ +import { getUrlOrFile } from "../../common/utils.mjs"; +import deepimage from "../../deepimage.app.mjs"; + +export default { + key: "deepimage-upscale", + name: "Upscale Image", + description: "Upscales the provided image using Deep Image. [See the documentation](https://documentation.deep-image.ai/image-processing/resize-and-padding)", + version: "0.0.1", + type: "action", + props: { + deepimage, + image: { + propDefinition: [ + deepimage, + "image", + ], + }, + upscaleMultiplier: { + type: "integer", + label: "Upscale Multiplier", + description: "The factor by which to upscale the image in %.", + }, + generativeUpscale: { + type: "boolean", + label: "Generative Upscale", + description: "Whether to use generative upscale.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.deepimage.makeRequest({ + $, + data: { + url: getUrlOrFile(this.image), + width: `${this.upscaleMultiplier}%`, + height: `${this.upscaleMultiplier}%`, + generative_upscale: this.generativeUpscale, + }, + }); + + $.export("$summary", "Successfully upscaled the image"); + return response; + }, +}; diff --git a/components/deepimage/common/constants.mjs b/components/deepimage/common/constants.mjs new file mode 100644 index 0000000000000..83057231e4f19 --- /dev/null +++ b/components/deepimage/common/constants.mjs @@ -0,0 +1,37 @@ +export const BACKGROUND_COLOR_OPTIONS = [ + { + label: "White", + value: "#FFFFFF", + }, + { + label: "Transparent", + value: "transparent", + }, +]; + +export const CROP_TYPE_OPTIONS = [ + { + label: "Crop center", + value: "center", + }, + { + label: "Crop item", + value: "item", + }, + { + label: "Crop content", + value: "content", + }, + { + label: "Cover", + value: "cover", + }, + { + label: "Canvas", + value: "canvas", + }, + { + label: "Bounds", + value: "bounds", + }, +]; diff --git a/components/deepimage/common/utils.mjs b/components/deepimage/common/utils.mjs new file mode 100644 index 0000000000000..bde896bb93a73 --- /dev/null +++ b/components/deepimage/common/utils.mjs @@ -0,0 +1,27 @@ +import fs from "fs"; + +export const isValidUrl = (urlString) => { + var urlPattern = new RegExp("^(https?:\\/\\/)?" + // validate protocol +"((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // validate domain name +"((\\d{1,3}\\.){3}\\d{1,3}))" + // validate OR ip (v4) address +"(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // validate port and path +"(\\?[;&a-z\\d%_.~+=-]*)?" + // validate query string +"(\\#[-a-z\\d_]*)?$", "i"); // validate fragment locator + return !!urlPattern.test(urlString); +}; + +export const checkTmp = (filename) => { + if (filename.indexOf("/tmp") === -1) { + return `/tmp/${filename}`; + } + return filename; +}; + +export const getUrlOrFile = (url) => { + if (!isValidUrl(url)) { + const data = fs.readFileSync(checkTmp(url)); + const base64Image = Buffer.from(data, "binary").toString("base64"); + return `base64,${base64Image}`; + } + return url; +}; diff --git a/components/deepimage/deepimage.app.mjs b/components/deepimage/deepimage.app.mjs new file mode 100644 index 0000000000000..9d68f6fe63f94 --- /dev/null +++ b/components/deepimage/deepimage.app.mjs @@ -0,0 +1,34 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "deepimage", + propDefinitions: { + image: { + type: "string", + label: "Image", + description: "The URL of the image or the path to the file saved to the `/tmp` directory (e.g. `/tmp/example.jpg`) to process. [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + }, + }, + methods: { + _baseUrl() { + return "https://deep-image.ai/rest_api/process_result"; + }, + _headers() { + return { + "content-type": "application/json", + "x-api-key": `${this.$auth.api_key}`, + }; + }, + makeRequest({ + $ = this, ...opts + }) { + return axios($, { + method: "POST", + url: this._baseUrl(), + headers: this._headers(), + ...opts, + }); + }, + }, +}; diff --git a/components/deepimage/package.json b/components/deepimage/package.json new file mode 100644 index 0000000000000..f22362bab2104 --- /dev/null +++ b/components/deepimage/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/deepimage", + "version": "0.1.0", + "description": "Pipedream DeepImage Components", + "main": "deepimage.app.mjs", + "keywords": [ + "pipedream", + "deepimage" + ], + "homepage": "https://pipedream.com/apps/deepimage", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "fs": "^0.0.1-security" + } +} diff --git a/components/deepl/README.md b/components/deepl/README.md index 16ca5d5db5f64..8071682688fda 100644 --- a/components/deepl/README.md +++ b/components/deepl/README.md @@ -1,12 +1,11 @@ # Overview -DeepL is a powerful AI-based translation tool that offers users a wide range of -features and options. With the DeepL API, developers can access these features -and integrate them into their own applications. Some examples of what can be -built with the DeepL API include: - -- A translation tool that can be used to translate documents or text from one - language to another. -- A chatbot that can communicate with users in multiple languages. -- A language learning tool that can help users learn new languages. -- A tool that can be used to create multilingual websites or applications. +The DeepL API provides a powerful machine translation service, allowing you to translate text between languages with a high degree of accuracy and nuance. Leveraging the DeepL API on Pipedream, you can automate multilingual content creation, streamline communication in different languages, and enrich apps with instant translation features. The integration possibilities are vast, enabling you to weave translation capabilities into various workflows, data processing, and content management systems. + +# Example Use Cases + +- **Content Localization Workflow**: Automatically translate blog posts or articles into multiple languages as soon as they are published. A Pipedream workflow can be triggered by a new post event from a CMS like WordPress, which then sends the content to DeepL for translation and posts the translated versions to the respective language-specific sections of the site. + +- **Customer Support Automation**: Integrate DeepL with a customer support ticketing system such as Zendesk. When a non-English support ticket is received, use Pipedream to trigger a translation workflow that translates the ticket to English, allowing support staff to respond effectively. Once the response is crafted, translate it back to the customer's language and update the ticket. + +- **Real-time Chat Translation**: Connect DeepL to a messaging platform like Slack to create a real-time chat translation bot. When messages are posted in a multilingual Slack channel, Pipedream can catch these messages, translate them through DeepL, and post the translated text back into the channel, enabling seamless cross-language team communication. diff --git a/components/deepseek/actions/create-chat-completion/create-chat-completion.mjs b/components/deepseek/actions/create-chat-completion/create-chat-completion.mjs new file mode 100644 index 0000000000000..8cdbd0f1b8981 --- /dev/null +++ b/components/deepseek/actions/create-chat-completion/create-chat-completion.mjs @@ -0,0 +1,136 @@ +import { RESPONSE_FORMAT_TYPE_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/util.mjs"; +import deepseek from "../../deepseek.app.mjs"; + +export default { + key: "deepseek-create-chat-completion", + name: "Create Chat Completion", + description: "Creates a chat completion using the DeepSeek API. [See the documentation](https://api-docs.deepseek.com/api/create-chat-completion)", + version: "0.0.1", + type: "action", + props: { + deepseek, + messages: { + type: "string[]", + label: "Messages", + description: "The messages for the chat conversation as JSON strings. Each message should be a JSON string like '{\"role\": \"user\", \"content\": \"Hello!\"}'. [See the documentation](https://api-docs.deepseek.com/api/create-chat-completion) for further details.", + }, + frequencyPenalty: { + type: "string", + label: "Frequency Penalty", + 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.", + optional: true, + }, + maxTokens: { + type: "integer", + label: "Max Tokens", + description: "Integer between 1 and 8192. The maximum number of tokens that can be generated in the chat completion. The total length of input tokens and generated tokens is limited by the model's context length. If `max_tokens` is not specified, the default value 4096 is used.", + optional: true, + }, + presencePenalty: { + type: "string", + label: "Presence Penalty", + 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 model's likelihood to talk about new topics.", + optional: true, + }, + responseFormatType: { + type: "string", + label: "Response Format Type", + description: "The format that the model must output. Setting to JSON Object enables JSON Output, which guarantees the message the model generates is valid JSON.", + options: RESPONSE_FORMAT_TYPE_OPTIONS, + optional: true, + }, + stop: { + type: "string[]", + label: "Stop Sequences", + description: "Up to 16 sequences where the API will stop generating further tokens.", + optional: true, + }, + stream: { + type: "boolean", + label: "Stream", + description: "If set, partial message deltas will be sent. Tokens will be sent as data-only server-sent events (SSE) as they become available, with the stream terminated by a `data: [DONE]` message.", + optional: true, + reloadProps: true, + }, + streamIncludeUsage: { + type: "string", + label: "Stream Include Usage", + description: "If set, an additional chunk will be streamed before the `data: [DONE]` message. The `usage` field on this chunk shows the token usage statistics for the entire request, and the `choices` field will always be an empty array. All other chunks will also include a `usage` field, but with a null value.", + optional: true, + hidden: true, + }, + temperature: { + type: "string", + label: "Temperature", + description: "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. We generally recommend altering this or Top P but not both.", + optional: true, + }, + topP: { + type: "string", + label: "Top P", + 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. We generally recommend altering this or Temperature but not both.", + optional: true, + }, + tools: { + type: "string[]", + label: "Tools", + description: "A list of tools the model may call. Currently, only functions are supported as a tool. Use this to provide a list of functions the model may generate JSON inputs for. A max of 128 functions are supported.", + optional: true, + }, + toolChoice: { + type: "string", + label: "Tool Choice", + description: "Controls which (if any) tool is called by the model. [See the documentation](https://api-docs.deepseek.com/api/create-chat-completion) for further details.", + optional: true, + }, + logprobs: { + type: "boolean", + label: "Log Probs", + description: "Whether to return log probabilities of the output tokens or not. If true, returns the log probabilities of each output token returned in the `content` of `message`.", + optional: true, + }, + topLogprobs: { + type: "string", + label: "Top Log Probabilities", + description: "An integer between 0 and 20 specifying the number of most likely tokens to return at each token position, each with an associated log probability. logprobs must be set to true if this parameter is used.", + optional: true, + }, + }, + async additionalProps(props) { + props.streamIncludeUsage.hidden = !this.stream; + return {}; + }, + async run({ $ }) { + const response = await this.deepseek.createModelResponse({ + $, + data: { + messages: parseObject(this.messages), + model: "deepseek-chat", + frequency_penalty: this.frequencyPenalty && parseInt(this.frequencyPenalty), + max_tokens: this.maxTokens, + presence_penalty: this.presencePenalty && parseInt(this.presencePenalty), + response_format: this.responseFormatType + ? { + type: this.responseFormatType, + } + : null, + stop: parseObject(this.stop), + stream: this.stream, + stream_options: this.stream + ? { + include_usage: this.streamIncludeUsage, + } + : null, + temperature: this.temperature && parseInt(this.temperature), + top_p: this.topP && parseInt(this.topP), + tools: parseObject(this.tools), + tool_choice: parseObject(this.toolChoice), + logprobs: this.logprobs, + top_logprobs: this.topLogprobs && parseInt(this.topLogprobs), + }, + }); + $.export("$summary", "Chat completion created"); + return response; + }, +}; diff --git a/components/deepseek/actions/get-balance/get-balance.mjs b/components/deepseek/actions/get-balance/get-balance.mjs new file mode 100644 index 0000000000000..c621fc32bfed9 --- /dev/null +++ b/components/deepseek/actions/get-balance/get-balance.mjs @@ -0,0 +1,19 @@ +import deepseek from "../../deepseek.app.mjs"; + +export default { + key: "deepseek-get-balance", + name: "Get User Balance", + description: "Retrieves the user's current balance. [See the documentation](https://api-docs.deepseek.com/api/get-user-balance)", + version: "0.0.1", + type: "action", + props: { + deepseek, + }, + async run({ $ }) { + const response = await this.deepseek.getUserBalance({ + $, + }); + $.export("$summary", "Successfully retrieved user balance"); + return response; + }, +}; diff --git a/components/deepseek/actions/list-models/list-models.mjs b/components/deepseek/actions/list-models/list-models.mjs new file mode 100644 index 0000000000000..b867caba2e520 --- /dev/null +++ b/components/deepseek/actions/list-models/list-models.mjs @@ -0,0 +1,19 @@ +import deepseek from "../../deepseek.app.mjs"; + +export default { + key: "deepseek-list-models", + name: "List Models", + description: "Lists the currently available models, and provides basic information about each one such as the owner and availability. [See the documentation](https://api-docs.deepseek.com/api/list-models)", + version: "0.0.1", + type: "action", + props: { + deepseek, + }, + async run({ $ }) { + const models = await this.deepseek.listModels({ + $, + }); + $.export("$summary", "Successfully listed models"); + return models; + }, +}; diff --git a/components/deepseek/common/constants.mjs b/components/deepseek/common/constants.mjs new file mode 100644 index 0000000000000..f5de38062a1d9 --- /dev/null +++ b/components/deepseek/common/constants.mjs @@ -0,0 +1,10 @@ +export const RESPONSE_FORMAT_TYPE_OPTIONS = [ + { + label: "Text", + value: "text", + }, + { + label: "JSON Object", + value: "json_object", + }, +]; diff --git a/components/deepseek/common/util.mjs b/components/deepseek/common/util.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/deepseek/common/util.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/deepseek/deepseek.app.mjs b/components/deepseek/deepseek.app.mjs new file mode 100644 index 0000000000000..7ecf9ef719a83 --- /dev/null +++ b/components/deepseek/deepseek.app.mjs @@ -0,0 +1,44 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "deepseek", + methods: { + _baseUrl() { + return "https://api.deepseek.com"; + }, + _headers() { + return { + "Authorization": `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createModelResponse(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/chat/completions", + ...opts, + }); + }, + getUserBalance() { + return this._makeRequest({ + method: "GET", + path: "/user/balance", + }); + }, + listModels() { + return this._makeRequest({ + method: "GET", + path: "/models", + }); + }, + }, +}; diff --git a/components/deepseek/package.json b/components/deepseek/package.json new file mode 100644 index 0000000000000..f520a8520fe49 --- /dev/null +++ b/components/deepseek/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/deepseek", + "version": "0.1.0", + "description": "Pipedream DeepSeek Components", + "main": "deepseek.app.mjs", + "keywords": [ + "pipedream", + "deepseek" + ], + "homepage": "https://pipedream.com/apps/deepseek", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/defastra/README.md b/components/defastra/README.md new file mode 100644 index 0000000000000..5c698c363543e --- /dev/null +++ b/components/defastra/README.md @@ -0,0 +1,11 @@ +# Overview + +The Defastra API is a tool designed for automating and integrating your cybersecurity efforts. It offers features such as vulnerability scans, monitoring of dark web activities, and risk assessments. When combined with Pipedream’s capabilities, you can create powerful, automated workflows to enhance your security posture. You can trigger events in real-time, process data, and connect to numerous other services, all of which help in proactively managing cybersecurity threats. + +# Example Use Cases + +- **Automated Vulnerability Alerts**: Set up a workflow that triggers a Defastra vulnerability scan on your assets periodically. Once the scan is complete, automatically parse the results and send alerts with details of any new vulnerabilities via Slack to your security team. + +- **Dark Web Monitoring Notifications**: Use the Defastra API to monitor specific keywords or data related to your organization on the dark web. Create a Pipedream workflow that sends notifications through email or a messaging platform like Microsoft Teams whenever your monitoring uncovers potentially malicious activities. + +- **Risk Assessment Reports**: Implement a workflow that periodically triggers a risk assessment for your projects or digital assets using Defastra. After the assessment is finished, automatically generate a report and save it to Google Drive, and then notify the relevant stakeholders through a Pipedream-supported communication app like Twilio SMS. diff --git a/components/defastra/actions/common/base.mjs b/components/defastra/actions/common/base.mjs new file mode 100644 index 0000000000000..a58f0185c41ee --- /dev/null +++ b/components/defastra/actions/common/base.mjs @@ -0,0 +1,43 @@ +import { ConfigurationError } from "@pipedream/platform"; +import defastra from "../../defastra.app.mjs"; + +export default { + props: { + defastra, + timeout: { + type: "string", + label: "Timeout", + description: "The timeout parameters allows you to control the response time of the check, allowing you to choose between more detailed results and faster response times. [Please read this guide](https://docs.defastra.com/docs/response-time-timeout-settings).", + optional: true, + options: [ + "minimal", + "normal", + "extensive", + ], + }, + label: { + type: "string", + label: "Label", + description: "A label is any string of text (max. 100 char) that can be attributed to any lookup. They are useful for organizing your data and can be used to search for data in the [Observer](https://docs.defastra.com/docs/what-is-the-observer), or visualize them on the dashboard.", + optional: true, + }, + }, + async run({ $ }) { + try { + const fn = this.getFn(); + const response = await fn({ + data: { + ...this.getData(), + timeout: this.timeout, + label: this.label, + }, + }); + + $.export("$summary", `Successfully performed risk analysis with Id: ${response.request_id}.`); + + return response; + } catch ({ message }) { + throw new ConfigurationError(JSON.parse(message).error_message); + } + }, +}; diff --git a/components/defastra/actions/deep-email-check/deep-email-check.mjs b/components/defastra/actions/deep-email-check/deep-email-check.mjs new file mode 100644 index 0000000000000..8b48c6804516b --- /dev/null +++ b/components/defastra/actions/deep-email-check/deep-email-check.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "defastra-deep-email-check", + name: "Deep Email Check", + description: "Performs a risk analysis on a given email address and provides a risk score indicating if the email is disposable, risky, or safe. [See the documentation](https://docs.defastra.com/reference/deep-email-check)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + email: { + propDefinition: [ + common.props.defastra, + "email", + ], + }, + }, + methods: { + getFn() { + return this.defastra.performEmailRiskAnalysis; + }, + getData() { + return { + email: this.email, + }; + }, + }, +}; diff --git a/components/defastra/actions/deep-phone-check/deep-phone-check.mjs b/components/defastra/actions/deep-phone-check/deep-phone-check.mjs new file mode 100644 index 0000000000000..bc3e1b74db13c --- /dev/null +++ b/components/defastra/actions/deep-phone-check/deep-phone-check.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "defastra-deep-phone-check", + name: "Deep Phone Check", + description: "Conducts a risk assessment and digital lookup for a provided phone number. Returns a risk score indicating if the number is disposable, risky, or safe, along with carrier details, location, and potential social profiles. [See the documentation](https://docs.defastra.com/reference/deep-phone-check)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + phoneNumber: { + propDefinition: [ + common.props.defastra, + "phoneNumber", + ], + }, + }, + methods: { + getFn() { + return this.defastra.performPhoneRiskAnalysis; + }, + getData() { + return { + phone: this.phoneNumber, + }; + }, + }, +}; diff --git a/components/defastra/defastra.app.mjs b/components/defastra/defastra.app.mjs index 4f3cbeea14110..bab32f7bd6597 100644 --- a/components/defastra/defastra.app.mjs +++ b/components/defastra/defastra.app.mjs @@ -1,11 +1,52 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "defastra", - propDefinitions: {}, + propDefinitions: { + email: { + type: "string", + label: "Email Address", + description: "The email address to perform the risk analysis on.", + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number to conduct the risk assessment and digital lookup for, in international format.", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.defastra.com"; + }, + _headers() { + return { + "X-API-KEY": this.$auth.api_key, + "Content-Type": "application/x-www-form-urlencoded", + }; + }, + _makeRequest({ + $ = this, path, ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: this._headers(), + }); + }, + performEmailRiskAnalysis(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/deep_email_check", + ...opts, + }); + }, + performPhoneRiskAnalysis(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/deep_phone_check", + ...opts, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/defastra/package.json b/components/defastra/package.json index cc731804338f2..00799e213e0a6 100644 --- a/components/defastra/package.json +++ b/components/defastra/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/defastra", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Defastra Components", "main": "defastra.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" } -} \ No newline at end of file +} + diff --git a/components/deftship/actions/create-freight-order/create-freight-order.mjs b/components/deftship/actions/create-freight-order/create-freight-order.mjs new file mode 100644 index 0000000000000..0910e938a58fc --- /dev/null +++ b/components/deftship/actions/create-freight-order/create-freight-order.mjs @@ -0,0 +1,230 @@ +import deftship from "../../deftship.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "deftship-create-freight-order", + name: "Create Freight Order", + description: "Initializes a new parcel order within Deftship. [See the documentation](https://developer.deftship.com/freight/create-order)", + version: "0.0.1", + type: "action", + props: { + deftship, + pickupDate: { + type: "string", + label: "Pickup Date", + description: "Proposed pickup date, in `Y-m-d`", + }, + fromOpeningTime: { + type: "string", + label: "From Opening Time", + description: "Opening time of pickup address, in `H:i`", + }, + fromClosingTime: { + type: "string", + label: "From Closing Time", + description: "Closing time of pickup address, in `H:i`", + }, + fromName: { + propDefinition: [ + deftship, + "fromName", + ], + }, + fromStreet: { + propDefinition: [ + deftship, + "fromStreet", + ], + }, + fromCity: { + propDefinition: [ + deftship, + "fromCity", + ], + }, + fromState: { + propDefinition: [ + deftship, + "fromState", + ], + }, + fromZip: { + propDefinition: [ + deftship, + "fromZip", + ], + }, + fromCountry: { + propDefinition: [ + deftship, + "fromCountry", + ], + }, + fromTelephone: { + propDefinition: [ + deftship, + "fromTelephone", + ], + description: "The telephone number of the sender. (must be a number 10-15 digits)", + }, + toOpeningTime: { + type: "string", + label: "To Opening Time", + description: "Opening time of delivery address, in `H:i`", + }, + toClosingTime: { + type: "string", + label: "To Closing Time", + description: "Closing time of delivery address, in `H:i`", + }, + toName: { + propDefinition: [ + deftship, + "toName", + ], + }, + toStreet: { + propDefinition: [ + deftship, + "toStreet", + ], + }, + toCity: { + propDefinition: [ + deftship, + "toCity", + ], + }, + toState: { + propDefinition: [ + deftship, + "toState", + ], + }, + toZip: { + propDefinition: [ + deftship, + "toZip", + ], + }, + toCountry: { + propDefinition: [ + deftship, + "toCountry", + ], + }, + toTelephone: { + propDefinition: [ + deftship, + "fromTelephone", + ], + description: "The telephone number of the sender. (must be a number 10-15 digits)", + }, + packageType: { + type: "string", + label: "Package Type", + description: "The type of package", + options: constants.PACKAGE_TYPES, + }, + itemCount: { + propDefinition: [ + deftship, + "itemCount", + ], + }, + length: { + type: "string", + label: "Length", + description: "Length (IN)", + }, + width: { + type: "string", + label: "Width", + description: "Width (IN)", + }, + height: { + type: "string", + label: "Height", + description: "Height (IN)", + }, + weight: { + type: "string", + label: "Weight", + description: "Weight (LBS)", + }, + description: { + type: "string", + label: "Description", + description: "Description of the item", + }, + price: { + type: "string", + label: "Price", + description: "Price (Commodity Value) of the item", + optional: true, + }, + sku: { + type: "string", + label: "Sku", + description: "PO number of the shipment", + optional: true, + }, + additionalFields: { + propDefinition: [ + deftship, + "additionalFields", + ], + }, + }, + async run({ $ }) { + const additionalFields = !this.additionalFields + ? {} + : typeof this.additionalFields === "string" + ? JSON.parse(this.additionalFields) + : this.additionalFields; + const response = await this.deftship.createFreightOrder({ + $, + data: { + pickup_date: this.pickupDate, + from_opening_time: this.fromOpeningTime, + from_closing_time: this.fromClosingTime, + from_address: { + name: this.fromName, + street_1: this.fromStreet, + city: this.fromCity, + state: this.fromState, + zip: this.fromZip, + country: this.fromCountry, + telephone: +this.fromTelephone, + }, + to_opening_time: this.toOpeningTime, + to_closing_time: this.toClosingTime, + to_address: { + name: this.toName, + street_1: this.toStreet, + city: this.toCity, + state: this.toState, + zip: this.toZip, + country: this.toCountry, + telephone: +this.toTelephone, + }, + items: [ + { + package_type: this.packageType, + count: this.itemCount, + length: +this.length, + width: +this.width, + height: +this.height, + weight: +this.weight, + description: this.description, + price: this.price && +this.price, + sku: this.sku, + }, + ], + ...additionalFields, + }, + }); + $.export("$summary", `New freight order with ID: ${response.data.shipment_order_id}`); + return response; + }, +}; diff --git a/components/deftship/actions/create-parcel-order/create-parcel-order.mjs b/components/deftship/actions/create-parcel-order/create-parcel-order.mjs new file mode 100644 index 0000000000000..efef1c398b94a --- /dev/null +++ b/components/deftship/actions/create-parcel-order/create-parcel-order.mjs @@ -0,0 +1,192 @@ +import deftship from "../../deftship.app.mjs"; + +export default { + key: "deftship-create-parcel-order", + name: "Create Parcel Order", + description: "Initializes a new parcel order within Deftship. [See the documentation](https://developer.deftship.com/parcel/create-parcel)", + version: "0.0.1", + type: "action", + props: { + deftship, + fromName: { + propDefinition: [ + deftship, + "fromName", + ], + }, + fromStreet: { + propDefinition: [ + deftship, + "fromStreet", + ], + }, + fromCity: { + propDefinition: [ + deftship, + "fromCity", + ], + }, + fromState: { + propDefinition: [ + deftship, + "fromState", + ], + }, + fromZip: { + propDefinition: [ + deftship, + "fromZip", + ], + }, + fromCountry: { + propDefinition: [ + deftship, + "fromCountry", + ], + }, + fromTelephone: { + propDefinition: [ + deftship, + "fromTelephone", + ], + }, + toName: { + propDefinition: [ + deftship, + "toName", + ], + }, + toStreet: { + propDefinition: [ + deftship, + "toStreet", + ], + }, + toCity: { + propDefinition: [ + deftship, + "toCity", + ], + }, + toState: { + propDefinition: [ + deftship, + "toState", + ], + }, + toZip: { + propDefinition: [ + deftship, + "toZip", + ], + }, + toCountry: { + propDefinition: [ + deftship, + "toCountry", + ], + }, + toTelephone: { + propDefinition: [ + deftship, + "fromTelephone", + ], + }, + itemCount: { + propDefinition: [ + deftship, + "itemCount", + ], + }, + length: { + type: "string", + label: "Length", + description: "Length of the parcel", + }, + width: { + type: "string", + label: "Width", + description: "Width of the parcel", + }, + height: { + type: "string", + label: "Height", + description: "Height of the parcel", + }, + lengthUnit: { + type: "string", + label: "Length Unit", + description: "By default, length unit is taken from your preferences. However, it can be overwritten using this field.", + optional: true, + options: [ + "IN", + "CM", + ], + }, + weight: { + type: "string", + label: "Weight", + description: "Weight of the parcel", + }, + weightUnit: { + type: "string", + label: "Weight Unit", + description: "By default, weight unit is taken from your preferences. However, it can be overwritten using this field.", + optional: true, + options: [ + "LBS", + "KG", + ], + }, + additionalFields: { + propDefinition: [ + deftship, + "additionalFields", + ], + }, + }, + async run({ $ }) { + const additionalFields = !this.additionalFields + ? {} + : typeof this.additionalFields === "string" + ? JSON.parse(this.additionalFields) + : this.additionalFields; + const response = await this.deftship.createParcelOrder({ + $, + data: { + from_address: { + name: this.fromName, + street_1: this.fromStreet, + city: this.fromCity, + state: this.fromState, + zip: this.fromZip, + country: this.fromCountry, + telephone: this.fromTelephone, + }, + to_address: { + name: this.toName, + street_1: this.toStreet, + city: this.toCity, + state: this.toState, + zip: this.toZip, + country: this.toCountry, + telephone: this.toTelephone, + }, + weight_unit: this.weightUnit, + length_unit: this.lengthUnit, + package_list: [ + { + count: this.itemCount, + length: +this.length, + width: +this.width, + height: +this.height, + weight: +this.weight, + }, + ], + ...additionalFields, + }, + }); + $.export("$summary", `Successfully created parcel order with ID: ${response.data.shipment_order_id}`); + return response; + }, +}; diff --git a/components/deftship/actions/get-insurance-rate/get-insurance-rate.mjs b/components/deftship/actions/get-insurance-rate/get-insurance-rate.mjs new file mode 100644 index 0000000000000..ad0f2085afd27 --- /dev/null +++ b/components/deftship/actions/get-insurance-rate/get-insurance-rate.mjs @@ -0,0 +1,211 @@ +import deftship from "../../deftship.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "deftship-get-insurance-rate", + name: "Get Insurance Rate", + description: "Checks pricing for Insurance based on the supplied information. Also automatically creates an insurance and returns the pricing. [See the documentation](https://developer.deftship.com/insurance/get-rates)", + version: "0.0.1", + type: "action", + props: { + deftship, + carrier: { + type: "string", + label: "Insurance Carrier", + description: "Carrier of the order going to be insured", + async options() { + return constants.INSURANCE_CARRIERS; + }, + }, + serviceCode: { + propDefinition: [ + deftship, + "serviceCode", + (c) => ({ + carrier: c.carrier, + }), + ], + }, + shipmentDate: { + type: "string", + label: "Shipment Date", + description: "Shipment date, in `Y-m-d`", + }, + fromName: { + propDefinition: [ + deftship, + "fromName", + ], + }, + fromAttention: { + propDefinition: [ + deftship, + "fromAttention", + ], + }, + fromStreet: { + propDefinition: [ + deftship, + "fromStreet", + ], + }, + fromCity: { + propDefinition: [ + deftship, + "fromCity", + ], + }, + fromState: { + propDefinition: [ + deftship, + "fromState", + ], + }, + fromZip: { + propDefinition: [ + deftship, + "fromZip", + ], + }, + fromCountry: { + propDefinition: [ + deftship, + "fromCountry", + ], + }, + fromTelephone: { + propDefinition: [ + deftship, + "fromTelephone", + ], + }, + toName: { + propDefinition: [ + deftship, + "toName", + ], + }, + toAttention: { + propDefinition: [ + deftship, + "toAttention", + ], + }, + toStreet: { + propDefinition: [ + deftship, + "toStreet", + ], + }, + toCity: { + propDefinition: [ + deftship, + "toCity", + ], + }, + toState: { + propDefinition: [ + deftship, + "toState", + ], + }, + toZip: { + propDefinition: [ + deftship, + "toZip", + ], + }, + toCountry: { + propDefinition: [ + deftship, + "toCountry", + ], + }, + toTelephone: { + propDefinition: [ + deftship, + "toTelephone", + ], + }, + trackingNumber: { + type: "string", + label: "Tracking Number", + description: "Tracking number of the package going to be insured", + }, + itemDescription: { + type: "string", + label: "Item", + description: "Description of the item being shipped", + }, + itemQuantity: { + type: "integer", + label: "Quantity", + description: "Item piece count", + }, + itemPrice: { + type: "string", + label: "Price", + description: "Item piece price", + }, + isInternational: { + type: "boolean", + label: "Is International", + description: "Set to `true` if the shipment is international", + default: false, + optional: true, + }, + isFreight: { + type: "boolean", + label: "Is Freight", + description: "Set to `true` if the shipment is freight", + default: false, + optional: true, + }, + }, + async run({ $ }) { + const response = await this.deftship.getInsuranceRates({ + $, + data: { + carrier: this.carrier, + service_code: this.serviceCode, + shipment_date: this.shipmentDate, + from_address: { + name: this.fromName, + attention: this.fromAttention, + street_1: this.fromStreet, + city: this.fromCity, + state: this.fromState, + zip: this.fromZip, + country: this.fromCountry, + telephone: this.fromTelephone, + }, + to_address: { + name: this.toName, + attention: this.toAttention, + street_1: this.toStreet, + city: this.toCity, + state: this.toState, + zip: this.toZip, + country: this.toCountry, + telephone: this.toTelephone, + }, + package_list: [ + { + tracking_number: this.trackingNumber, + items: [ + { + description: this.itemDescription, + quantity: this.itemQuantity, + price: +this.itemPrice, + }, + ], + }, + ], + is_international: this.isInternational, + is_freight: this.isFreight, + }, + }); + $.export("$summary", `Successfully retrieved insurance rate with ID: ${response.data.id}`); + return response; + }, +}; diff --git a/components/deftship/common/constants.mjs b/components/deftship/common/constants.mjs new file mode 100644 index 0000000000000..259ff0127082b --- /dev/null +++ b/components/deftship/common/constants.mjs @@ -0,0 +1,1420 @@ +const INSURANCE_CARRIERS = [ + "dhl", + "fedex", + "usps", + "ups", + "canadapost", + "xpo_logistics", + "reddaway", + "central_transport", + "frontline", + "daylight_transport", + "duie_pyle", + "saia", + "unis", + "fedex_freight", + "abf_freight", + "xpress_global", + "ddp", + "old_dominion", + "roadrunner", + "estes_express_lines", + "yrc_worldwide", + "oak_harbor", + "dependable_highway_express", + "t_force", + "forward_air", + "amazon_freight", + "new_penn", + "r_and_l_carriers", + "ward", + "dayton", + "land_air", + "ross_express", + "pitt_ohio", + "southeastern_freight_lines", + "best_yet_express", + "aaa_cooper", + "averitt_express", + "total_transportation", + "uber", + "gls", + "diamond_line_delivery_systems", + "midwest_motor_express", + "dugan_truck_line", + "southwestern_motor_transport", + "aci_motor_freight", + "dohrn", + "holland", + "moto_transportation", + "magnum_ltl", + "pace_motor_lines", + "ch_robinson", + "sutton_transport", + "jp_express", + "tax_airfreight", + "performance_freight", + "fort_transportation", + "double_d_express", + "crosscountry_freight_solutions", + "go2_logistics", + "warp", + "hercules_freight", + "standard_forwarding", +]; + +const COUNTRY_CODES = [ + { + label: "Afghanistan", + value: "AF", + }, + { + label: "Åland Islands", + value: "AX", + }, + { + label: "Albania", + value: "AL", + }, + { + label: "Algeria", + value: "DZ", + }, + { + label: "American Samoa", + value: "AS", + }, + { + label: "Andorra", + value: "AD", + }, + { + label: "Angola", + value: "AO", + }, + { + label: "Anguilla", + value: "AI", + }, + { + label: "Antarctica", + value: "AQ", + }, + { + label: "Antigua and Barbuda", + value: "AG", + }, + { + label: "Argentina", + value: "AR", + }, + { + label: "Armenia", + value: "AM", + }, + { + label: "Aruba", + value: "AW", + }, + { + label: "Australia", + value: "AU", + }, + { + label: "Austria", + value: "AT", + }, + { + label: "Azerbaijan", + value: "AZ", + }, + { + label: "Bahamas", + value: "BS", + }, + { + label: "Bahrain", + value: "BH", + }, + { + label: "Bangladesh", + value: "BD", + }, + { + label: "Barbados", + value: "BB", + }, + { + label: "Belarus", + value: "BY", + }, + { + label: "Belgium", + value: "BE", + }, + { + label: "Belize", + value: "BZ", + }, + { + label: "Benin", + value: "BJ", + }, + { + label: "Bermuda", + value: "BM", + }, + { + label: "Bhutan", + value: "BT", + }, + { + label: "Bolivia, Plurinational State of", + value: "BO", + }, + { + label: "Bonaire, Sint Eustatius and Saba", + value: "BQ", + }, + { + label: "Bosnia and Herzegovina", + value: "BA", + }, + { + label: "Botswana", + value: "BW", + }, + { + label: "Bouvet Island", + value: "BV", + }, + { + label: "Brazil", + value: "BR", + }, + { + label: "British Indian Ocean Territory", + value: "IO", + }, + { + label: "Brunei Darussalam", + value: "BN", + }, + { + label: "Bulgaria", + value: "BG", + }, + { + label: "Burkina Faso", + value: "BF", + }, + { + label: "Burundi", + value: "BI", + }, + { + label: "Cambodia", + value: "KH", + }, + { + label: "Cameroon", + value: "CM", + }, + { + label: "Canada", + value: "CA", + }, + { + label: "Cape Verde", + value: "CV", + }, + { + label: "Cayman Islands", + value: "KY", + }, + { + label: "Central African Republic", + value: "CF", + }, + { + label: "Chad", + value: "TD", + }, + { + label: "Chile", + value: "CL", + }, + { + label: "China", + value: "CN", + }, + { + label: "Christmas Island", + value: "CX", + }, + { + label: "Cocos (Keeling) Islands", + value: "CC", + }, + { + label: "Colombia", + value: "CO", + }, + { + label: "Comoros", + value: "KM", + }, + { + label: "Congo", + value: "CG", + }, + { + label: "Congo, the Democratic Republic of the", + value: "CD", + }, + { + label: "Cook Islands", + value: "CK", + }, + { + label: "Costa Rica", + value: "CR", + }, + { + label: "Côte d'Ivoire", + value: "CI", + }, + { + label: "Croatia", + value: "HR", + }, + { + label: "Cuba", + value: "CU", + }, + { + label: "Curaçao", + value: "CW", + }, + { + label: "Cyprus", + value: "CY", + }, + { + label: "Czech Republic", + value: "CZ", + }, + { + label: "Denmark", + value: "DK", + }, + { + label: "Djibouti", + value: "DJ", + }, + { + label: "Dominica", + value: "DM", + }, + { + label: "Dominican Republic", + value: "DO", + }, + { + label: "Ecuador", + value: "EC", + }, + { + label: "Egypt", + value: "EG", + }, + { + label: "El Salvador", + value: "SV", + }, + { + label: "Equatorial Guinea", + value: "GQ", + }, + { + label: "Eritrea", + value: "ER", + }, + { + label: "Estonia", + value: "EE", + }, + { + label: "Ethiopia", + value: "ET", + }, + { + label: "Falkland Islands (Malvinas)", + value: "FK", + }, + { + label: "Faroe Islands", + value: "FO", + }, + { + label: "Fiji", + value: "FJ", + }, + { + label: "Finland", + value: "FI", + }, + { + label: "France", + value: "FR", + }, + { + label: "French Guiana", + value: "GF", + }, + { + label: "French Polynesia", + value: "PF", + }, + { + label: "French Southern Territories", + value: "TF", + }, + { + label: "Gabon", + value: "GA", + }, + { + label: "Gambia", + value: "GM", + }, + { + label: "Georgia", + value: "GE", + }, + { + label: "Germany", + value: "DE", + }, + { + label: "Ghana", + value: "GH", + }, + { + label: "Gibraltar", + value: "GI", + }, + { + label: "Greece", + value: "GR", + }, + { + label: "Greenland", + value: "GL", + }, + { + label: "Grenada", + value: "GD", + }, + { + label: "Guadeloupe", + value: "GP", + }, + { + label: "Guam", + value: "GU", + }, + { + label: "Guatemala", + value: "GT", + }, + { + label: "Guernsey", + value: "GG", + }, + { + label: "Guinea", + value: "GN", + }, + { + label: "Guinea-Bissau", + value: "GW", + }, + { + label: "Guyana", + value: "GY", + }, + { + label: "Haiti", + value: "HT", + }, + { + label: "Heard Island and McDonald Islands", + value: "HM", + }, + { + label: "Holy See (Vatican City State)", + value: "VA", + }, + { + label: "Honduras", + value: "HN", + }, + { + label: "Hong Kong", + value: "HK", + }, + { + label: "Hungary", + value: "HU", + }, + { + label: "Iceland", + value: "IS", + }, + { + label: "India", + value: "IN", + }, + { + label: "Indonesia", + value: "ID", + }, + { + label: "Iraq", + value: "IQ", + }, + { + label: "Ireland", + value: "IE", + }, + { + label: "Isle of Man", + value: "IM", + }, + { + label: "Israel", + value: "IL", + }, + { + label: "Italy", + value: "IT", + }, + { + label: "Jamaica", + value: "JM", + }, + { + label: "Japan", + value: "JP", + }, + { + label: "Jersey", + value: "JE", + }, + { + label: "Jordan", + value: "JO", + }, + { + label: "Kazakhstan", + value: "KZ", + }, + { + label: "Kenya", + value: "KE", + }, + { + label: "Kiribati", + value: "KI", + }, + { + label: "Korea, Democratic People's Republic of", + value: "KP", + }, + { + label: "Korea, Republic of", + value: "KR", + }, + { + label: "Kuwait", + value: "KW", + }, + { + label: "Kyrgyzstan", + value: "KG", + }, + { + label: "Lao People's Democratic Republic", + value: "LA", + }, + { + label: "Latvia", + value: "LV", + }, + { + label: "Lebanon", + value: "LB", + }, + { + label: "Lesotho", + value: "LS", + }, + { + label: "Liberia", + value: "LR", + }, + { + label: "Libya", + value: "LY", + }, + { + label: "Liechtenstein", + value: "LI", + }, + { + label: "Lithuania", + value: "LT", + }, + { + label: "Luxembourg", + value: "LU", + }, + { + label: "Macao", + value: "MO", + }, + { + label: "Macedonia, the Former Yugoslav Republic of", + value: "MK", + }, + { + label: "Madagascar", + value: "MG", + }, + { + label: "Malawi", + value: "MW", + }, + { + label: "Malaysia", + value: "MY", + }, + { + label: "Maldives", + value: "MV", + }, + { + label: "Mali", + value: "ML", + }, + { + label: "Malta", + value: "MT", + }, + { + label: "Marshall Islands", + value: "MH", + }, + { + label: "Martinique", + value: "MQ", + }, + { + label: "Mauritania", + value: "MR", + }, + { + label: "Mauritius", + value: "MU", + }, + { + label: "Mayotte", + value: "YT", + }, + { + label: "Mexico", + value: "MX", + }, + { + label: "Micronesia, Federated States of", + value: "FM", + }, + { + label: "Moldova, Republic of", + value: "MD", + }, + { + label: "Monaco", + value: "MC", + }, + { + label: "Mongolia", + value: "MN", + }, + { + label: "Montenegro", + value: "ME", + }, + { + label: "Montserrat", + value: "MS", + }, + { + label: "Morocco", + value: "MA", + }, + { + label: "Mozambique", + value: "MZ", + }, + { + label: "Myanmar", + value: "MM", + }, + { + label: "Namibia", + value: "NA", + }, + { + label: "Nauru", + value: "NR", + }, + { + label: "Nepal", + value: "NP", + }, + { + label: "Netherlands", + value: "NL", + }, + { + label: "New Caledonia", + value: "NC", + }, + { + label: "New Zealand", + value: "NZ", + }, + { + label: "Nicaragua", + value: "NI", + }, + { + label: "Niger", + value: "NE", + }, + { + label: "Nigeria", + value: "NG", + }, + { + label: "Niue", + value: "NU", + }, + { + label: "Norfolk Island", + value: "NF", + }, + { + label: "Northern Mariana Islands", + value: "MP", + }, + { + label: "Norway", + value: "NO", + }, + { + label: "Oman", + value: "OM", + }, + { + label: "Pakistan", + value: "PK", + }, + { + label: "Palau", + value: "PW", + }, + { + label: "Palestine, State of", + value: "PS", + }, + { + label: "Panama", + value: "PA", + }, + { + label: "Papua New Guinea", + value: "PG", + }, + { + label: "Paraguay", + value: "PY", + }, + { + label: "Peru", + value: "PE", + }, + { + label: "Philippines", + value: "PH", + }, + { + label: "Pitcairn", + value: "PN", + }, + { + label: "Poland", + value: "PL", + }, + { + label: "Portugal", + value: "PT", + }, + { + label: "Puerto Rico", + value: "PR", + }, + { + label: "Qatar", + value: "QA", + }, + { + label: "Réunion", + value: "RE", + }, + { + label: "Romania", + value: "RO", + }, + { + label: "Russian Federation", + value: "RU", + }, + { + label: "Rwanda", + value: "RW", + }, + { + label: "Saint Barthélemy", + value: "BL", + }, + { + label: "Saint Helena, Ascension and Tristan da Cunha", + value: "SH", + }, + { + label: "Saint Kitts and Nevis", + value: "KN", + }, + { + label: "Saint Lucia", + value: "LC", + }, + { + label: "Saint Martin (French part)", + value: "MF", + }, + { + label: "Saint Pierre and Miquelon", + value: "PM", + }, + { + label: "Saint Vincent and the Grenadines", + value: "VC", + }, + { + label: "Samoa", + value: "WS", + }, + { + label: "San Marino", + value: "SM", + }, + { + label: "Sao Tome and Principe", + value: "ST", + }, + { + label: "Saudi Arabia", + value: "SA", + }, + { + label: "Senegal", + value: "SN", + }, + { + label: "Serbia", + value: "RS", + }, + { + label: "Seychelles", + value: "SC", + }, + { + label: "Sierra Leone", + value: "SL", + }, + { + label: "Singapore", + value: "SG", + }, + { + label: "Sint Maarten (Dutch part)", + value: "SX", + }, + { + label: "Slovakia", + value: "SK", + }, + { + label: "Slovenia", + value: "SI", + }, + { + label: "Solomon Islands", + value: "SB", + }, + { + label: "Somalia", + value: "SO", + }, + { + label: "South Africa", + value: "ZA", + }, + { + label: "South Georgia and the South Sandwich Islands", + value: "GS", + }, + { + label: "South Sudan", + value: "SS", + }, + { + label: "Spain", + value: "ES", + }, + { + label: "Sri Lanka", + value: "LK", + }, + { + label: "Sudan", + value: "SD", + }, + { + label: "Suriname", + value: "SR", + }, + { + label: "Svalbard and Jan Mayen", + value: "SJ", + }, + { + label: "Swaziland", + value: "SZ", + }, + { + label: "Sweden", + value: "SE", + }, + { + label: "Switzerland", + value: "CH", + }, + { + label: "Taiwan, Province of China", + value: "TW", + }, + { + label: "Tajikistan", + value: "TJ", + }, + { + label: "Tanzania, United Republic of", + value: "TZ", + }, + { + label: "Thailand", + value: "TH", + }, + { + label: "Timor-Leste", + value: "TL", + }, + { + label: "Togo", + value: "TG", + }, + { + label: "Tokelau", + value: "TK", + }, + { + label: "Tonga", + value: "TO", + }, + { + label: "Trinidad and Tobago", + value: "TT", + }, + { + label: "Tunisia", + value: "TN", + }, + { + label: "Turkey", + value: "TR", + }, + { + label: "Turkmenistan", + value: "TM", + }, + { + label: "Turks and Caicos Islands", + value: "TC", + }, + { + label: "Tuvalu", + value: "TV", + }, + { + label: "Uganda", + value: "UG", + }, + { + label: "Ukraine", + value: "UA", + }, + { + label: "United Arab Emirates", + value: "AE", + }, + { + label: "United Kingdom", + value: "GB", + }, + { + label: "United States", + value: "US", + }, + { + label: "United States Minor Outlying Islands", + value: "UM", + }, + { + label: "Uruguay", + value: "UY", + }, + { + label: "Uzbekistan", + value: "UZ", + }, + { + label: "Vanuatu", + value: "VU", + }, + { + label: "Venezuela, Bolivarian Republic of", + value: "VE", + }, + { + label: "Viet Nam", + value: "VN", + }, + { + label: "Virgin Islands, British", + value: "VG", + }, + { + label: "Virgin Islands, U.S.", + value: "VI", + }, + { + label: "Wallis and Futuna", + value: "WF", + }, + { + label: "Western Sahara", + value: "EH", + }, + { + label: "Yemen", + value: "YE", + }, + { + label: "Zambia", + value: "ZM", + }, + { + label: "Zimbabwe", + value: "ZW", + }, +]; + +const STATE_CODES = [ + { + label: "Alabama", + value: "AL", + }, + { + label: "Alaska", + value: "AK", + }, + { + label: "American Samoa", + value: "AS", + }, + { + label: "Arizona", + value: "AZ", + }, + { + label: "Arkansas", + value: "AR", + }, + { + label: "California", + value: "CA", + }, + { + label: "Colorado", + value: "CO", + }, + { + label: "Connecticut", + value: "CT", + }, + { + label: "Delaware", + value: "DE", + }, + { + label: "District Of Columbia", + value: "DC", + }, + { + label: "Federated States Of Micronesia", + value: "FM", + }, + { + label: "Florida", + value: "FL", + }, + { + label: "Georgia", + value: "GA", + }, + { + label: "Guam", + value: "GU", + }, + { + label: "Hawaii", + value: "HI", + }, + { + label: "Idaho", + value: "ID", + }, + { + label: "Illinois", + value: "IL", + }, + { + label: "Indiana", + value: "IN", + }, + { + label: "Iowa", + value: "IA", + }, + { + label: "Kansas", + value: "KS", + }, + { + label: "Kentucky", + value: "KY", + }, + { + label: "Louisiana", + value: "LA", + }, + { + label: "Maine", + value: "ME", + }, + { + label: "Marshall Islands", + value: "MH", + }, + { + label: "Maryland", + value: "MD", + }, + { + label: "Massachusetts", + value: "MA", + }, + { + label: "Michigan", + value: "MI", + }, + { + label: "Minnesota", + value: "MN", + }, + { + label: "Mississippi", + value: "MS", + }, + { + label: "Missouri", + value: "MO", + }, + { + label: "Montana", + value: "MT", + }, + { + label: "Nebraska", + value: "NE", + }, + { + label: "Nevada", + value: "NV", + }, + { + label: "New Hampshire", + value: "NH", + }, + { + label: "New Jersey", + value: "NJ", + }, + { + label: "New Mexico", + value: "NM", + }, + { + label: "New York", + value: "NY", + }, + { + label: "North Carolina", + value: "NC", + }, + { + label: "North Dakota", + value: "ND", + }, + { + label: "Northern Mariana Islands", + value: "MP", + }, + { + label: "Ohio", + value: "OH", + }, + { + label: "Oklahoma", + value: "OK", + }, + { + label: "Oregon", + value: "OR", + }, + { + label: "Palau", + value: "PW", + }, + { + label: "Pennsylvania", + value: "PA", + }, + { + label: "Puerto Rico", + value: "PR", + }, + { + label: "Rhode Island", + value: "RI", + }, + { + label: "South Carolina", + value: "SC", + }, + { + label: "South Dakota", + value: "SD", + }, + { + label: "Tennessee", + value: "TN", + }, + { + label: "Texas", + value: "TX", + }, + { + label: "Utah", + value: "UT", + }, + { + label: "Vermont", + value: "VT", + }, + { + label: "Virgin Islands", + value: "VI", + }, + { + label: "Virginia", + value: "VA", + }, + { + label: "Washington", + value: "WA", + }, + { + label: "West Virginia", + value: "WV", + }, + { + label: "Wisconsin", + value: "WI", + }, + { + label: "Wyoming", + value: "WY", + }, + { + label: "AA", + value: "AA", + }, + { + label: "AE", + value: "AE", + }, + { + label: "AP", + value: "AP", + }, + { + label: "Alberta", + value: "AB", + }, + { + label: "British Colombia", + value: "BC", + }, + { + label: "Manitoba", + value: "MB", + }, + { + label: "New Brunswick", + value: "NB", + }, + { + label: "Newfoundland and Labrador", + value: "NL", + }, + { + label: "Nova Scotia", + value: "NS", + }, + { + label: "Northwest Territories", + value: "NT", + }, + { + label: "Nunavut", + value: "NU", + }, + { + label: "Ontario", + value: "ON", + }, + { + label: "Prince Edward Island", + value: "PE", + }, + { + label: "Québec", + value: "QC", + }, + { + label: "Saskatchewan", + value: "SK", + }, + { + label: "Yukon", + value: "YT", + }, +]; + +const PACKAGE_TYPES = [ + { + label: "Pallet (48\" x 40\")", + value: "pallet_48x40", + }, + { + label: "Pallet (48\" x 48\")", + value: "pallet_48x48", + }, + { + label: "Pallet (Custom Dimensions)", + value: "pallet_custom", + }, + { + label: "Bag", + value: "bag", + }, + { + label: "Bale", + value: "bale", + }, + { + label: "Box", + value: "box", + }, + { + label: "Crate", + value: "crate", + }, + { + label: "Cylinder", + value: "cylinder", + }, + { + label: "Drum", + value: "drum", + }, + { + label: "Pail", + value: "pail", + }, + { + label: "Reel", + value: "reel", + }, + { + label: "Roll", + value: "roll", + }, + { + label: "Loose", + value: "loose", + }, +]; + +export default { + INSURANCE_CARRIERS, + COUNTRY_CODES, + STATE_CODES, + PACKAGE_TYPES, +}; diff --git a/components/deftship/deftship.app.mjs b/components/deftship/deftship.app.mjs new file mode 100644 index 0000000000000..301e7cc3885f3 --- /dev/null +++ b/components/deftship/deftship.app.mjs @@ -0,0 +1,165 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "deftship", + propDefinitions: { + serviceCode: { + type: "string", + label: "Service Code", + description: "Service code of this order", + async options({ carrier }) { + const { data } = await this.getServiceNames(); + const relevant = data.filter((item) => item.carrier === carrier); + return relevant.map(({ + code: value, service: label, + }) => ({ + value, + label, + })); + }, + }, + fromName: { + type: "string", + label: "From Address: Name", + description: "The name in the sender address", + }, + fromAttention: { + type: "string", + label: "From Address: Attention", + description: "Attention of the sender", + }, + fromStreet: { + type: "string", + label: "From Address: Street", + description: "The street address of the sender", + }, + fromCity: { + type: "string", + label: "From Address: City", + description: "The city address of the sender", + }, + fromState: { + type: "string", + label: "From Address: State", + description: "The state code of the sender", + options: constants.STATE_CODES, + }, + fromZip: { + type: "string", + label: "From Address: Zip Code", + description: "The zip code of the sender", + }, + fromCountry: { + type: "string", + label: "From Address: Country", + description: "The country code of the sender", + options: constants.COUNTRY_CODES, + }, + fromTelephone: { + type: "string", + label: "From Address: Telephone", + description: "The telephone number of the sender", + }, + toName: { + type: "string", + label: "To Address: Name", + description: "The name in the receiver address", + }, + toAttention: { + type: "string", + label: "To Address: Attention", + description: "Attention of the receiver", + }, + toStreet: { + type: "string", + label: "To Address: Street", + description: "The street address of the receiver", + }, + toCity: { + type: "string", + label: "To Address: City", + description: "The city address of the receiver", + }, + toState: { + type: "string", + label: "To Address: State", + description: "The state code of the receiver", + options: constants.STATE_CODES, + }, + toZip: { + type: "string", + label: "To Address: Zip Code", + description: "The zip code of the receiver", + }, + toCountry: { + type: "string", + label: "To Address: Country", + description: "The country code of the receiver", + options: constants.COUNTRY_CODES, + }, + toTelephone: { + type: "string", + label: "To Address: Telephone", + description: "The telephone number of the receiver", + }, + itemCount: { + type: "integer", + label: "Count", + description: "Pieces count", + }, + additionalFields: { + type: "object", + label: "Additional Fields", + description: "An object of key/value pairs containing additional fields to add to the order", + optional: true, + }, + }, + methods: { + _baseUrl() { + return this.$auth.environment; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.api_token}`, + }, + }); + }, + getInsuranceRates(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/insurances/get-rates", + ...opts, + }); + }, + getServiceNames(opts = {}) { + return this._makeRequest({ + path: "/shipment/service-names", + ...opts, + }); + }, + createFreightOrder(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/freight-orders", + ...opts, + }); + }, + createParcelOrder(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/parcel-orders", + ...opts, + }); + }, + }, +}; diff --git a/components/deftship/package.json b/components/deftship/package.json new file mode 100644 index 0000000000000..96e001cb88451 --- /dev/null +++ b/components/deftship/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/deftship", + "version": "0.1.0", + "description": "Pipedream Deftship Components", + "main": "deftship.app.mjs", + "keywords": [ + "pipedream", + "deftship" + ], + "homepage": "https://pipedream.com/apps/deftship", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/degreed/README.md b/components/degreed/README.md index d5a3148aff605..4fb4fdd0cb96b 100644 --- a/components/degreed/README.md +++ b/components/degreed/README.md @@ -1,16 +1,11 @@ # Overview -Degreed provides an API that lets you access Degreed data and content in order -to create new applications and services. Here are some examples of what you can -build using the Degreed API: +The Degreed API enables automated interactions with the Degreed platform, a hub for professional growth and learning. Through this API, you can programmatically access user data, learning content, pathways, and more. By leveraging Pipedream, users can design custom workflows that integrate Degreed's learning management capabilities with other services to streamline processes, report on progress, sync data across platforms, and even curate learning content based on specific triggers or criteria. -- An application that lets users search for and find Degreed courses and - content -- A service that provides recommendations for Degreed courses and content based - on user interests -- A service that lets users track their progress in completing Degreed courses - and content -- An application that provides Degreed course and content updates and - notifications to users -- A service that helps users plan their learning by suggesting Degreed courses - and content based on their goals +# Example Use Cases + +- **Automated Learning Progress Reports**: Send weekly learning progress reports from Degreed to Slack. Utilize the Degreed API to gather user progress and then create a formatted message sent to a designated Slack channel to keep teams updated on their learning milestones. + +- **Content Completion Trigger for Certificates**: When a user completes a learning path in Degreed, trigger a workflow in Pipedream that generates a certificate in Canva and emails it to the user via SendGrid. This workflow enables automatic recognition of user achievements, enhancing motivation and engagement. + +- **Cross-Platform Learning Path Synchronization**: Sync learning paths between Degreed and an internal company wiki or LMS like Moodle. When a new learning path is created in Degreed, the Pipedream workflow detects the change and updates the company's internal platforms, ensuring that all learning resources are consistently available across all systems. diff --git a/components/delay/README.md b/components/delay/README.md index 3c96d0bd053d6..e8ecfdf077b6b 100644 --- a/components/delay/README.md +++ b/components/delay/README.md @@ -1,11 +1,11 @@ # Overview -The Delay API allows you to build workflows that wait a specified amount of -time before continuing. This can be useful for rate-limiting actions, or -waiting for an external event to occur. +The Delay API in Pipedream is a built-in function that allows you to pause a workflow for a specified amount of time. This can be incredibly useful when you need to stagger API calls to avoid rate limits, wait for an external process to complete, or simply introduce a delay between actions in a sequence. With precision up to milliseconds, the Delay API provides a simple yet powerful tool for managing timing in automation workflows. -Here are some examples of what you can build using the Delay API: +# Example Use Cases -- A workflow that sends a Slack message after a specified amount of time -- A workflow that checks for new data from an external API every hour -- A workflow that backs up a file to Dropbox every day +- **Scheduled Content Release**: Automate a content publication process where blog posts or social media updates are staggered over time. After an initial post is published, use the Delay API to wait for a few hours before the next piece of content goes live. + +- **Rate Limit Management**: When working with APIs that have strict rate limits, you can insert a delay between requests to stay within the rate limit. For example, after making an API call to fetch data from an app like Twitter, add a delay before making another call. + +- **Drip Email Campaigns**: For marketing automation, you can use the Delay API to send out a series of emails over a period of days or weeks. After sending an initial welcome email using an email service like SendGrid, apply a delay before sending a follow-up email offering a discount or additional information. diff --git a/components/delay/package.json b/components/delay/package.json new file mode 100644 index 0000000000000..cd34e8c7e91ef --- /dev/null +++ b/components/delay/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/delay", + "version": "0.6.0", + "description": "Pipedream delay Components", + "main": "delay.app.mjs", + "keywords": [ + "pipedream", + "delay" + ], + "homepage": "https://pipedream.com/apps/delay", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/delighted/README.md b/components/delighted/README.md index 46e583de68e2d..8b071bd563388 100644 --- a/components/delighted/README.md +++ b/components/delighted/README.md @@ -1,7 +1,11 @@ # Overview -With the Delighted API, you can build a variety of things, including: +Delighted API on Pipedream lets you automate and integrate customer feedback into various aspects of your business processes. Utilize real-time survey responses to trigger actions, manage contacts, and streamline follow-ups. With a mix of webhooks and API calls, you can react promptly to customer sentiment, categorize feedback, and enhance customer experience by connecting insights to your CRM, support, and product management tools. -- A way to automate your customer surveys -- A platform to track your customer satisfaction levels -- A tool to help you identify areas of improvement for your business +# Example Use Cases + +- **Auto-tagging feedback for Support Teams**: Automatically tag and sort feedback based on sentiment score into your helpdesk software, such as Zendesk. Use webhook triggers for new feedback in Delighted, apply conditional logic in Pipedream to assess sentiment, and create/update tickets in Zendesk with insights for targeted follow-ups. + +- **Feedback-Driven Email Campaigns**: Enrich your email marketing tool, like Mailchimp, with customer feedback to segment and personalize campaigns. When a customer completes a Delighted survey, Pipedream can capture this event, process the feedback, and update Mailchimp subscriber lists to trigger tailored email sequences. + +- **Product Feature Request Tracking**: Streamline the process of collecting product feedback by channeling Delighted survey responses into a project management tool like Trello. Set up a Pipedream workflow to filter responses for feature requests and automatically create Trello cards, prioritizing product enhancements based on customer input. diff --git a/components/demandbase/demandbase.app.mjs b/components/demandbase/demandbase.app.mjs new file mode 100644 index 0000000000000..de53a87b7d83f --- /dev/null +++ b/components/demandbase/demandbase.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "demandbase", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/demandbase/package.json b/components/demandbase/package.json new file mode 100644 index 0000000000000..5459462ed8613 --- /dev/null +++ b/components/demandbase/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/demandbase", + "version": "0.0.1", + "description": "Pipedream Demandbase Components", + "main": "demandbase.app.mjs", + "keywords": [ + "pipedream", + "demandbase" + ], + "homepage": "https://pipedream.com/apps/demandbase", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/demio/README.md b/components/demio/README.md index f98d589f2dcfe..ceb2df4adf53b 100644 --- a/components/demio/README.md +++ b/components/demio/README.md @@ -1,9 +1,11 @@ # Overview -With the Demio API, you can build applications that: +Demio, a webinar platform, offers an API that lets you automate various aspects of your webinar experience. With the Demio API on Pipedream, you can seamlessly integrate webinar registration, attendance tracking, and follow-up actions into your business workflows. Pipedream's serverless platform facilitates the creation of complex automations that can trigger actions based on events in Demio, connecting it with hundreds of other apps for a cohesive ecosystem. -- Schedule and launch video meetings -- Embed a meeting launch page on your website -- Manage participants and registrants -- Stream live video and recordings -- And more! +# Example Use Cases + +- **Webinar Registration Sync:** Automatically sync new webinar registrations from Demio to your CRM platform, like Salesforce. Each time a new attendee registers for a webinar in Demio, a corresponding lead or contact can be created or updated in Salesforce, keeping all customer touchpoints in sync. + +- **Post-Webinar Engagement:** Trigger an email or SMS campaign using SendGrid or Twilio for attendees who participated in a webinar. Using the attendee data from Demio, craft personalized follow-up messages thanking attendees, providing additional resources, or offering special promotions. + +- **Attendance-Based Project Management:** Connect Demio to a project management tool like Trello or Asana. After a webinar ends, automatically create tasks for sales or support teams to follow up with attendees based on their participation level, ensuring that no potential lead falls through the cracks. diff --git a/components/deployhq/README.md b/components/deployhq/README.md index d5f5fd89eea8d..5b06e9b7cfdf3 100644 --- a/components/deployhq/README.md +++ b/components/deployhq/README.md @@ -1,11 +1,11 @@ -# Getting Started +# Overview -DeployHQ doesn't provide a way to automatically setup webhooks so sources will need to be registered with DeployHQ manually. The source will need to be registered as an [HTTP POST Integration](https://www.deployhq.com/support/integrations/http-post). +DeployHQ is a service that automates the deployment of your web applications. By integrating with version control systems, DeployHQ can automatically deploy code changes to various environments. Using the DeployHQ API on Pipedream, you can orchestrate deployments, manage your projects, and synchronize deployment activities with other tools in your toolchain. It opens up possibilities for custom deployment workflows, notifications, monitoring, and more, all connected within the Pipedream ecosystem. -1. Create a source -2. In DeployHQ go to "Integrations" > "New Integration" > "HTTP POST" -3. Copy the endpoint from the source to the "Endpoint" field in DeployHQ -4. Select the option that matches your source from the "Trigger integration when..." list. For the source to work you must select at least the type of event that matches the source, e.g. "A deployment starts" for the "Deploy Started" source. -5. Select the servers and groups you would like your source to emit events for. -6. Click "Create integration" +# Example Use Cases +- **Automated Deployment Triggering**: Trigger deployments in DeployHQ when a new commit is pushed to the main branch of a GitHub repository. Use Pipedream's GitHub trigger to initiate the workflow, and then call the DeployHQ API to start the deployment, ensuring that your production environment is always up-to-date with the latest stable code. + +- **Deployment Status Notifications**: Set up a workflow that listens for deployment status updates from DeployHQ and sends notifications through Slack or email. This workflow can be configured to alert your team when a deployment starts, succeeds, or fails, aiding in fast communication and response times. + +- **Syncing Deployments with Project Management Tools**: Create a workflow that updates a task in a project management app like Trello or Asana whenever a deployment is completed. This can help keep track of what has been deployed and when, aligning deployment activities with your project's progress and tasks. \ No newline at end of file diff --git a/components/deputy/README.md b/components/deputy/README.md index c4d0427cd685c..2553ddccc1051 100644 --- a/components/deputy/README.md +++ b/components/deputy/README.md @@ -1,8 +1,11 @@ # Overview -With Deputy, you can create amazing things like: +Deputy is a robust workforce management tool, catering to scheduling, timesheet tracking, and other employee coordination tasks. Leveraging the Deputy API on Pipedream allows you to automate mundane tasks, sync data across platforms, and create custom notifications based on employee actions. You could build workflows to manage shifts, sync employee details with HR systems, or trigger payroll processes, harnessing Pipedream's capability to integrate with various services without the need for manual coding. -- A to-do list -- A notes app -- A task manager -- A project management tool +# Example Use Cases + +- **Automate Timesheet Reporting**: Pull timesheets from Deputy as they're submitted and feed them into a Google Sheets document. This is great for streamlining payroll processes or providing regular reports to management on employee hours. + +- **Scheduling Sync-Up**: When a new shift is published or updated in Deputy, trigger a workflow that syncs this info with a shared company calendar on Office 365 or Google Calendar, keeping everyone informed and up-to-date. + +- **Employee Onboarding**: When a new employee is added to Deputy, automatically send their details to an HR system like BambooHR or Workday, and enroll them in necessary training courses via an LMS like Lessonly or TalentLMS. diff --git a/components/deputy/actions/create-employee/create-employee.mjs b/components/deputy/actions/create-employee/create-employee.mjs new file mode 100644 index 0000000000000..4e758c68022a4 --- /dev/null +++ b/components/deputy/actions/create-employee/create-employee.mjs @@ -0,0 +1,68 @@ +import deputy from "../../deputy.app.mjs"; + +export default { + key: "deputy-create-employee", + name: "Create Employee", + description: "Adds a new employee or staff member to the organization in Deputy. [See the documentation](https://developer.deputy.com/deputy-docs/reference/addanemployee)", + version: "0.0.1", + type: "action", + props: { + deputy, + firstName: { + type: "string", + label: "First Name", + description: "First name of the employee", + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the employee", + }, + locationId: { + propDefinition: [ + deputy, + "locationId", + ], + }, + dob: { + type: "string", + label: "Date of Birth", + description: "The date of birth of the employee. Example: `1971-01-01`", + optional: true, + }, + startDate: { + type: "string", + label: "Start Date", + description: "The date that the employee started employment. Example: `1971-01-01`", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Phone number of the employee", + optional: true, + }, + rate: { + type: "string", + label: "Rate", + description: "The hourly rate of the employee", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.deputy.createEmployee({ + $, + data: { + strFirstName: this.firstName, + strLastName: this.lastName, + intCompanyId: this.locationId, + strDob: this.dob, + strStartDate: this.startDate, + strMobilePhone: this.phone, + fltWeekDayRate: this.rate, + }, + }); + $.export("$summary", `Successfully created employee with ID: ${response.Id}`); + return response; + }, +}; diff --git a/components/deputy/actions/create-location/create-location.mjs b/components/deputy/actions/create-location/create-location.mjs new file mode 100644 index 0000000000000..cdd793d589fd5 --- /dev/null +++ b/components/deputy/actions/create-location/create-location.mjs @@ -0,0 +1,57 @@ +import deputy from "../../deputy.app.mjs"; + +export default { + key: "deputy-create-location", + name: "Create Location", + description: "Creates a new location in Deputy. [See the documentation](https://developer.deputy.com/deputy-docs/reference/addalocation)", + version: "0.0.1", + type: "action", + props: { + deputy, + name: { + type: "string", + label: "Name", + description: "The name of the workplace", + }, + address: { + type: "string", + label: "Address", + description: "The address of the workplace", + }, + parentCompanyId: { + propDefinition: [ + deputy, + "locationId", + ], + label: "Parent Company ID", + description: "The identifier of the parent company", + optional: true, + }, + timezone: { + type: "string", + label: "Timezone", + description: "The timezone of the workplace. Example: `Australia/Sydney`", + optional: true, + }, + notes: { + type: "string", + label: "Notes", + description: "Notes about the location", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.deputy.createLocation({ + $, + data: { + strWorkplaceName: this.name, + strAddress: this.address, + intParentCompany: this.parentCompanyId, + strTimezone: this.timezone, + strAddressNotes: this.notes, + }, + }); + $.export("$summary", `Successfully created location with ID: ${response.Id}`); + return response; + }, +}; diff --git a/components/deputy/actions/start-shift/start-shift.mjs b/components/deputy/actions/start-shift/start-shift.mjs new file mode 100644 index 0000000000000..1723936cca535 --- /dev/null +++ b/components/deputy/actions/start-shift/start-shift.mjs @@ -0,0 +1,36 @@ +import deputy from "../../deputy.app.mjs"; + +export default { + key: "deputy-start-shift", + name: "Start Shift", + description: "Starts a work shift for a specified employee in Deputy. [See the documentation](https://developer.deputy.com/deputy-docs/reference/startanemployeestimesheetclockon)", + version: "0.0.1", + type: "action", + props: { + deputy, + locationId: { + propDefinition: [ + deputy, + "locationId", + ], + }, + employeeId: { + propDefinition: [ + deputy, + "employeeId", + ], + }, + }, + async run({ $ }) { + const response = await this.deputy.startTimesheet({ + $, + data: { + intOpunitId: this.locationId, + intEmployeeId: this.employeeId, + }, + }); + + $.export("$summary", `Successfully started work shift for employee ${this.employeeId}`); + return response; + }, +}; diff --git a/components/deputy/deputy.app.mjs b/components/deputy/deputy.app.mjs index c7c10b320fc2e..a67375d0a486b 100644 --- a/components/deputy/deputy.app.mjs +++ b/components/deputy/deputy.app.mjs @@ -1,11 +1,95 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "deputy", - propDefinitions: {}, + propDefinitions: { + locationId: { + type: "string", + label: "Location ID", + description: "The identifier of a location", + async options() { + const locations = await this.listLocations(); + return locations?.map(({ + Id: value, CompanyName: label, + }) => ({ + value, + label, + })) || []; + }, + }, + employeeId: { + type: "string", + label: "Employee ID", + description: "The identifier of an employee", + async options() { + const employees = await this.listEmployees(); + return employees?.map(({ + Id: value, DisplayName: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `https://${this.$auth.endpoint}/api/v1`; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/resource/Webhook", + ...opts, + }); + }, + listEmployees(opts = {}) { + return this._makeRequest({ + path: "/supervise/employee", + ...opts, + }); + }, + listLocations(opts = {}) { + return this._makeRequest({ + path: "/resource/Company", + ...opts, + }); + }, + createLocation(opts = {}) { + return this._makeRequest({ + method: "PUT", + path: "/supervise/company", + ...opts, + }); + }, + startTimesheet(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/supervise/timesheet/start", + ...opts, + }); + }, + createEmployee(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/supervise/employee", + ...opts, + }); }, }, }; diff --git a/components/deputy/package.json b/components/deputy/package.json new file mode 100644 index 0000000000000..ca84b395e44ab --- /dev/null +++ b/components/deputy/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/deputy", + "version": "0.0.1", + "description": "Pipedream Deputy Components", + "main": "deputy.app.mjs", + "keywords": [ + "pipedream", + "deputy" + ], + "homepage": "https://pipedream.com/apps/deputy", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} diff --git a/components/deputy/sources/common/base.mjs b/components/deputy/sources/common/base.mjs new file mode 100644 index 0000000000000..3db21bd183fb0 --- /dev/null +++ b/components/deputy/sources/common/base.mjs @@ -0,0 +1,37 @@ +import deputy from "../../deputy.app.mjs"; + +export default { + props: { + deputy, + db: "$.service.db", + http: "$.interface.http", + }, + hooks: { + async deploy() { + await this.deputy.createWebhook({ + data: { + Topic: this.getTopic(), + Enabled: 1, + Type: "URL", + Address: this.http.endpoint, + }, + }); + }, + }, + methods: { + getTopic() { + throw new Error("getTopic is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run(event) { + const { body: { data } } = event; + if (!data) { + return; + } + const meta = this.generateMeta(data); + this.$emit(data, meta); + }, +}; diff --git a/components/deputy/sources/new-colleague-created/new-colleague-created.mjs b/components/deputy/sources/new-colleague-created/new-colleague-created.mjs new file mode 100644 index 0000000000000..e570f3c173ff9 --- /dev/null +++ b/components/deputy/sources/new-colleague-created/new-colleague-created.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "deputy-new-colleague-created", + name: "New Colleague Created (Instant)", + description: "Emit new event when a new individual is added to the workplace", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTopic() { + return "Employee.Insert"; + }, + generateMeta(employee) { + return { + id: employee.Id, + summary: `New Employee: ${employee.Id}`, + ts: Date.parse(employee.Created), + }; + }, + }, + sampleEmit, +}; diff --git a/components/deputy/sources/new-colleague-created/test-event.mjs b/components/deputy/sources/new-colleague-created/test-event.mjs new file mode 100644 index 0000000000000..04646534b6fcb --- /dev/null +++ b/components/deputy/sources/new-colleague-created/test-event.mjs @@ -0,0 +1,92 @@ +export default { + "Id": 4, + "Company": 1, + "FirstName": "Wendla", + "LastName": "Bergman", + "DisplayName": "Wendla Bergman", + "OtherName": null, + "Salutation": null, + "MainAddress": null, + "PostalAddress": null, + "Contact": 0, + "EmergencyAddress": null, + "DateOfBirth": null, + "Gender": null, + "Pronouns": 0, + "CustomPronouns": null, + "Photo": null, + "UserId": null, + "JobAppId": null, + "Active": true, + "StartDate": "2024-09-03T15:38:27-04:00", + "TerminationDate": null, + "EmploymentEndDate": null, + "EmploymentEndReason": null, + "StressProfile": 8, + "Position": null, + "HigherDuty": null, + "Role": 50, + "AllowAppraisal": true, + "HistoryId": 2571, + "CustomFieldData": null, + "OnboardingId": null, + "ExternalLinkId": null, + "Creator": 1, + "Created": "2024-09-03T15:38:28-04:00", + "Modified": "2024-09-03T15:38:28-04:00", + "CompanyObject": { + "Id": 1, + "Portfolio": null, + "Code": "223", + "Active": true, + "ParentCompany": 0, + "CompanyName": "Testing", + "TradingName": "", + "BusinessNumber": "", + "CompanyNumber": null, + "IsWorkplace": true, + "IsPayrollEntity": true, + "PayrollExportCode": "", + "Address": 157, + "Contact": 14, + "Creator": 1, + "Created": "2009-04-08T00:53:33-04:00", + "Modified": "2024-09-03T13:23:43-04:00" + }, + "StressProfileObject": { + "Id": 8, + "Name": "Standard 40 hours, All day", + "MaxHoursPerShift": 8, + "MaxHoursPerPeriod": 40, + "MaxDaysPerPeriod": 5, + "MaxHoursPerDay": 24, + "GapHoursBetweenShifts": 12, + "CustomRules": null, + "Creator": 1, + "Created": "2020-10-25T17:26:48-04:00", + "Modified": "2020-10-25T17:26:48-04:00" + }, + "RoleObject": { + "Id": 50, + "Role": "Employee", + "Ranking": 97, + "ReportTo": 3, + "Permissions": "Time Sheets-> Access:Yes\nMobile Bump-> Access:Yes\nJournal-> Access:No\nAnnouncements-> Access:Yes\nRoster Manager-> Access:No\nCan View Cost-> Access:No\nCan Roster All Departments-> Access:No\nCan Roster Non-Recommended Employees-> Access:No\nCan Add Shift or Edit Shift Times-> Access:No\nApprove Time Sheets-> Access:No\nCan Approve Pay Conditions-> Access:No\nCan Approve for All Departments-> Access:No\nCan Approve for All Employee Roles-> Access:No\nCan Approve Outside the Current Period-> Access:No\nExport Time Sheets-> Access:No\nApprove Date Range of Leave Request-> Access:No\nApprove Leave Type for a Leave Request-> Access:No\nView Employee Details-> Access:No\nManage Employee Details-> Access:No\nManage Employee Pay Details-> Access:No\nManage Workplaces and Pay Centres-> Access:Yes Add/Edit:No\nManage Departments-> Access:Yes Add/Edit:No\nManage Employment Terms-> Access:No Add/Edit:No\nManage Training Requirements-> Access:No Add/Edit:No\nManage Events-> Access:No Add/Edit:No\nManage Employee Roles-> Access:No Add/Edit:No\nManage KPI Reports-> Access:No Add/Edit:No\nManage Procedures-> Access:No Add/Edit:No\nCan Select Employees and Departments from All Workplaces-> Access:No\nCan Manage Employees from All Roles-> Access:No\nGeneral Settings-> Access:No\nDeveloper-> Access:No\nSetup Kiosks-> Access:No\nPublic Holidays-> Access:No Add/Edit:No\nComment Categories-> Access:No Add/Edit:No\n-> Access:No", + "Require2fa": false, + "Creator": 1, + "Created": "2009-11-30T17:37:03-05:00", + "Modified": "2012-09-25T00:54:40-04:00" + }, + "_DPMetaData": { + "System": "Employee", + "CreatorInfo": { + "Id": 1, + "DisplayName": "Test User", + "EmployeeProfile": 1, + "Employee": 1, + "Photo": "https://d11hmzhsuwuq9f.cloudfront.net/my/avatar?name=M+B&width=135&height=135", + "Pronouns": 0, + "CustomPronouns": null + } + } +} \ No newline at end of file diff --git a/components/deputy/sources/new-post-created/new-post-created.mjs b/components/deputy/sources/new-post-created/new-post-created.mjs new file mode 100644 index 0000000000000..2310f20fa69ac --- /dev/null +++ b/components/deputy/sources/new-post-created/new-post-created.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "deputy-new-post-created", + name: "New Post Created (Instant)", + description: "Emit new event when a new newsfeed post arrives", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTopic() { + return "Memo.Insert"; + }, + generateMeta(post) { + return { + id: post.Id, + summary: `New Post: ${post.Id}`, + ts: Date.parse(post.Created), + }; + }, + }, + sampleEmit, +}; diff --git a/components/deputy/sources/new-post-created/test-event.mjs b/components/deputy/sources/new-post-created/test-event.mjs new file mode 100644 index 0000000000000..a2a60e4494b41 --- /dev/null +++ b/components/deputy/sources/new-post-created/test-event.mjs @@ -0,0 +1,208 @@ +export default { + "Id": 1, + "ShowFrom": "2024-09-03T00:00:00-04:00", + "Active": true, + "ShowTill": null, + "Title": null, + "Content": "Hello world", + "Type": 1, + "File": null, + "Url": null, + "ConfirmText": "", + "Keyword": "Hello world", + "DisableComment": false, + "Creator": 1, + "Created": "2024-09-03T15:25:14-04:00", + "Modified": "2024-09-03T15:25:14-04:00", + "_DPMetaData": { + "System": "Memo", + "CreatorInfo": { + "Id": 1, + "DisplayName": "Test User", + "EmployeeProfile": 1, + "Employee": 1, + "Photo": "https://d11hmzhsuwuq9f.cloudfront.net/my/avatar?name=M+B&width=135&height=135", + "Pronouns": 0, + "CustomPronouns": null + }, + "Companies": [ + { + "Id": 1, + "Portfolio": null, + "Code": "223", + "Active": true, + "ParentCompany": 0, + "CompanyName": "Testing", + "TradingName": "", + "BusinessNumber": "", + "CompanyNumber": null, + "IsWorkplace": true, + "IsPayrollEntity": true, + "PayrollExportCode": "", + "Address": 157, + "Contact": 14, + "Creator": 1, + "Created": "2009-04-08T00:53:33-04:00", + "Modified": "2024-09-03T13:23:43-04:00", + "_DPMetaData": { + "System": "Company", + "CreatorInfo": { + "Id": 1, + "DisplayName": "Test User", + "EmployeeProfile": 1, + "Employee": 1, + "Photo": "https://d11hmzhsuwuq9f.cloudfront.net/my/avatar?name=M+B&width=135&height=135", + "Pronouns": 0, + "CustomPronouns": null + }, + "AddressObject": { + "Id": 157, + "ContactName": null, + "UnitNo": null, + "StreetNo": null, + "SuiteNo": null, + "PoBox": null, + "Street1": "San Francisco\n", + "Street2": null, + "City": null, + "State": "34", + "Postcode": null, + "Country": 224, + "Phone": null, + "Notes": null, + "Format": null, + "Saved": null, + "Creator": 1, + "Created": "2024-09-03T13:23:43-04:00", + "Modified": "2024-09-03T13:23:43-04:00", + "Verified": null, + "Print": "San Francisco\n\n" + }, + "Geo": { + "Id": 1, + "Orm": "DeputecCompany", + "RecId": 1, + "Longitude": -85.771997, + "Latitude": 43.5502974, + "Accuracy": null, + "No": null, + "Street": null, + "Suburb": null, + "State": null, + "Postcode": null, + "Country": null, + "Action": null, + "Creator": 1, + "Created": "2024-09-03T13:22:56-04:00", + "Modified": "2024-09-03T13:23:43-04:00" + }, + "CustomData": { + "GooglePlaceId": null + } + } + }, + { + "Id": 2, + "Portfolio": null, + "Code": "Pip", + "Active": true, + "ParentCompany": 1, + "CompanyName": "Pipedream", + "TradingName": null, + "BusinessNumber": null, + "CompanyNumber": null, + "IsWorkplace": true, + "IsPayrollEntity": true, + "PayrollExportCode": null, + "Address": 158, + "Contact": null, + "Creator": 1, + "Created": "2024-09-03T14:45:43-04:00", + "Modified": "2024-09-03T14:45:43-04:00", + "_DPMetaData": { + "System": "Company", + "CreatorInfo": { + "Id": 1, + "DisplayName": "Test User", + "EmployeeProfile": 1, + "Employee": 1, + "Photo": "https://d11hmzhsuwuq9f.cloudfront.net/my/avatar?name=M+B&width=135&height=135", + "Pronouns": 0, + "CustomPronouns": null + }, + "AddressObject": { + "Id": 158, + "ContactName": null, + "UnitNo": null, + "StreetNo": null, + "SuiteNo": null, + "PoBox": null, + "Street1": "44 Montgomery St., San Francisco, CA 94104", + "Street2": null, + "City": null, + "State": "15", + "Postcode": null, + "Country": 224, + "Phone": null, + "Notes": null, + "Format": null, + "Saved": null, + "Creator": 1, + "Created": "2024-09-03T14:45:43-04:00", + "Modified": "2024-09-03T14:45:43-04:00", + "Verified": null, + "Print": "44 Montgomery St., San Francisco, CA 94104\n" + }, + "Geo": { + "Id": 2, + "Orm": "DeputecCompany", + "RecId": 2, + "Longitude": -122.401906, + "Latitude": 37.7897312, + "Accuracy": null, + "No": null, + "Street": null, + "Suburb": null, + "State": null, + "Postcode": null, + "Country": null, + "Action": null, + "Creator": 1, + "Created": "2024-09-03T14:45:43-04:00", + "Modified": "2024-09-03T14:45:43-04:00" + }, + "CustomData": { + "GooglePlaceId": null + } + } + } + ], + "Teams": [], + "CanDelete": true, + "Files": [], + "AssignedUsers": [ + { + "Id": 1, + "DisplayName": "Test User", + "EmployeeProfile": 1, + "Employee": 1, + "Photo": "https://d11hmzhsuwuq9f.cloudfront.net/my/avatar?name=M+B&width=135&height=135", + "Pronouns": 0, + "CustomPronouns": null + } + ], + "Reactions": { + "1": 0, + "2": 0, + "3": 0, + "4": 0, + "5": 0, + "6": 0 + }, + "UserReaction": [], + "Comments": [], + "RequireConfirmation": false, + "RequireMyConfirm": false, + "MemoLogs": [] + } +} \ No newline at end of file diff --git a/components/deputy/sources/new-timesheet-saved/new-timesheet-saved.mjs b/components/deputy/sources/new-timesheet-saved/new-timesheet-saved.mjs new file mode 100644 index 0000000000000..40db8e1a144c6 --- /dev/null +++ b/components/deputy/sources/new-timesheet-saved/new-timesheet-saved.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "deputy-new-timesheet-saved", + name: "New Timesheet Saved (Instant)", + description: "Emit new event when a new timesheet has been saved", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTopic() { + return "Timesheet.Save"; + }, + generateMeta(timesheet) { + return { + id: timesheet.Id, + summary: `New Timesheet: ${timesheet.Id}`, + ts: Date.parse(timesheet.Created), + }; + }, + }, + sampleEmit, +}; diff --git a/components/deputy/sources/new-timesheet-saved/test-event.mjs b/components/deputy/sources/new-timesheet-saved/test-event.mjs new file mode 100644 index 0000000000000..66d3f07eb1eea --- /dev/null +++ b/components/deputy/sources/new-timesheet-saved/test-event.mjs @@ -0,0 +1,211 @@ +export default { + "Id": 4, + "Employee": 1, + "EmployeeHistory": 2566, + "EmployeeAgreement": 3, + "Date": "2024-09-03T00:00:00-04:00", + "StartTime": 1725391020, + "EndTime": 1725394620, + "Mealbreak": "2024-09-03T00:00:00-04:00", + "MealbreakSlots": "", + "Slots": [], + "TotalTime": 1, + "TotalTimeInv": 1, + "Cost": null, + "Roster": null, + "EmployeeComment": null, + "SupervisorComment": null, + "Supervisor": null, + "Disputed": false, + "TimeApproved": false, + "TimeApprover": null, + "Discarded": null, + "ValidationFlag": 0, + "OperationalUnit": 2, + "IsInProgress": true, + "IsLeave": false, + "LeaveId": null, + "LeaveRule": null, + "Invoiced": false, + "InvoiceComment": null, + "PayRuleApproved": false, + "Exported": null, + "StagingId": null, + "PayStaged": false, + "PaycycleId": 2, + "AccrualState": null, + "AccrualStateChangedAt": null, + "AccrualAttempts": null, + "MarkedPaidUnpaidAt": null, + "File": null, + "CustomFieldData": null, + "RealTime": true, + "AutoProcessed": 0, + "AutoRounded": 0, + "AutoTimeApproved": 0, + "AutoPayRuleApproved": 0, + "Metadata": "{\"external_location\":false,\"forecast_cost\":0}", + "ParentId": null, + "Creator": 1, + "Created": "2024-09-03T15:17:56-04:00", + "Modified": "2024-09-03T15:17:56-04:00", + "ReviewState": 0, + "EmployeeObject": { + "Id": 1, + "Company": 1, + "FirstName": "Test", + "LastName": "User", + "DisplayName": "Test User", + "OtherName": null, + "Salutation": null, + "MainAddress": null, + "PostalAddress": null, + "Contact": 500, + "EmergencyAddress": null, + "DateOfBirth": null, + "Gender": null, + "Pronouns": null, + "CustomPronouns": null, + "Photo": null, + "UserId": 1, + "JobAppId": null, + "Active": true, + "StartDate": "2024-09-03T00:00:00-04:00", + "TerminationDate": null, + "EmploymentEndDate": null, + "EmploymentEndReason": null, + "StressProfile": 8, + "Position": null, + "HigherDuty": null, + "Role": 1, + "AllowAppraisal": true, + "HistoryId": 2566, + "CustomFieldData": null, + "OnboardingId": null, + "ExternalLinkId": null, + "Creator": 1, + "Created": "2024-09-03T13:22:48-04:00", + "Modified": "2024-09-03T13:22:48-04:00" + }, + "EmployeeAgreementObject": { + "Id": 3, + "EmployeeId": 1, + "PayPoint": 1, + "EmpType": 1, + "CompanyName": "", + "Active": true, + "StartDate": "2012-10-14T00:00:00-04:00", + "EndDate": null, + "Contract": 133, + "SalaryPayRule": 0, + "ContractFile": 0, + "PayrollId": "", + "PayPeriod": 1, + "HistoryId": 2954, + "Creator": 1, + "Created": "2012-10-13T20:00:00-04:00", + "Modified": "2016-01-19T09:36:49-05:00", + "BaseRate": null, + "Config": "[]" + }, + "OperationalUnitObject": { + "Id": 2, + "Creator": 1, + "Created": "2024-09-03T13:22:56-04:00", + "Modified": "2024-09-03T13:22:56-04:00", + "Company": 1, + "WorkType": null, + "ParentOperationalUnit": 0, + "OperationalUnitName": "Production", + "Active": true, + "PayrollExportName": "", + "Address": 155, + "Contact": null, + "RosterSortOrder": 1, + "ShowOnRoster": true, + "Colour": "#bc51ff", + "RosterActiveHoursSchedule": null, + "DailyRosterBudget": null, + "OperationalUnitType": null, + "AddressObject": { + "Id": 155, + "ContactName": null, + "UnitNo": null, + "StreetNo": null, + "SuiteNo": null, + "PoBox": null, + "Street1": "San Francisco", + "Street2": null, + "City": "San Francisco", + "State": "34", + "Postcode": "94104", + "Country": 224, + "Phone": null, + "Notes": null, + "Format": null, + "Saved": null, + "Creator": 361, + "Created": "2015-12-07T13:23:59-05:00", + "Modified": "2024-09-03T13:22:56-04:00", + "Verified": null, + "Print": "San Francisco\n" + }, + "CompanyCode": "223", + "CompanyName": "Testing", + "CompanyAddress": 157 + }, + "Paycycle": { + "Id": 2, + "EmployeeId": 1, + "EmployeeAgreementId": 3, + "PeriodId": 42, + "RecommendedLoadings": true, + "Timesheets": 2, + "TimesheetsTimeApproved": 0, + "TimesheetsPayApproved": 0, + "PaycycleRules": 0, + "PaycycleRulesApproved": 0, + "Exported": false, + "ExportId": 0, + "Paid": false, + "TimeTotal": 0.71, + "CostTotal": 0, + "EmployeeAgreementHistoryId": 2954, + "Creator": 1, + "Created": "2024-09-03T14:19:04-04:00", + "Modified": "2024-09-03T15:14:07-04:00" + }, + "StartTimeLocalized": "2024-09-03T15:17:00-04:00", + "EndTimeLocalized": "2024-09-03T16:17:00-04:00", + "OnCost": 0, + "ExternalId": null, + "_DPMetaData": { + "System": "Timesheet", + "CreatorInfo": { + "Id": 1, + "DisplayName": "Test User", + "EmployeeProfile": 1, + "Employee": 1, + "Photo": "https://d11hmzhsuwuq9f.cloudfront.net/my/avatar?name=M+B&width=135&height=135", + "Pronouns": 0, + "CustomPronouns": null + }, + "OperationalUnitInfo": { + "Id": 2, + "OperationalUnitName": "Production", + "Company": 1, + "CompanyName": "Testing", + "LabelWithCompany": "[223] Production" + }, + "EmployeeInfo": { + "Id": 1, + "DisplayName": "Test User", + "EmployeeProfile": 1, + "Employee": 1, + "Photo": "https://d11hmzhsuwuq9f.cloudfront.net/my/avatar?name=M+B&width=135&height=135", + "Pronouns": 0, + "CustomPronouns": null + }, + "CustomFieldData": null + } +} \ No newline at end of file diff --git a/components/desktime/README.md b/components/desktime/README.md new file mode 100644 index 0000000000000..320f47c15c234 --- /dev/null +++ b/components/desktime/README.md @@ -0,0 +1,11 @@ +# Overview + +The DeskTime API enables you to hook into vital data concerning productivity tracking, employee attendance, and project management. Integrating this API with Pipedream allows you to automate workflows, synchronize productivity data with other tools, and trigger actions based on team performance metrics. For instance, you can set up a system that notifies a project manager when an employee logs overtime or integrates time tracking data with payroll software. + +# Example Use Cases + +- **Time-Tracking Alerts**: Automatically send a Slack message to a project manager when an employee's tracked time exceeds a certain threshold for the day, indicating potential overtime or excessive workload. + +- **Attendance Reports**: Generate and send daily attendance reports to HR by compiling employee check-in and check-out times from DeskTime, then emailing the report using a service like SendGrid or directly through Gmail. + +- **Project Hours and Invoicing**: Connect DeskTime with a billing platform like QuickBooks to create invoices based on the number of hours tracked on specific projects, ensuring accurate and timely billing for clients. diff --git a/components/desktime/actions/create-project/create-project.mjs b/components/desktime/actions/create-project/create-project.mjs new file mode 100644 index 0000000000000..53081bcc5868f --- /dev/null +++ b/components/desktime/actions/create-project/create-project.mjs @@ -0,0 +1,37 @@ +import app from "../../desktime.app.mjs"; + +export default { + key: "desktime-create-project", + name: "Create a New Project with an optional task", + description: "Create a new project with an optional task in DeskTime. [See the documentation](https://desktime.com/app/settings/api?tab=project)", + version: "0.0.1", + type: "action", + props: { + app, + project: { + propDefinition: [ + app, + "project", + ], + }, + task: { + propDefinition: [ + app, + "task", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createProject({ + $, + params: { + project: this.project, + task: this.task, + }, + }); + + $.export("$summary", `Successfully created project '${this.project}'`); + + return response; + }, +}; diff --git a/components/desktime/actions/start-project/start-project.mjs b/components/desktime/actions/start-project/start-project.mjs new file mode 100644 index 0000000000000..29c5fd0053e82 --- /dev/null +++ b/components/desktime/actions/start-project/start-project.mjs @@ -0,0 +1,37 @@ +import app from "../../desktime.app.mjs"; + +export default { + key: "desktime-start-project", + name: "Start Project", + description: "Starts tracking time for a given project and optionally a task. [See the documentation](https://desktime.com/app/settings/api?tab=project)", + version: "0.0.1", + type: "action", + props: { + app, + project: { + propDefinition: [ + app, + "project", + ], + }, + task: { + propDefinition: [ + app, + "task", + ], + }, + }, + async run({ $ }) { + const response = await this.app.startTracking({ + $, + params: { + project: this.project, + task: this.task, + }, + }); + + $.export("$summary", `Tracking of project '${this.project}' started successfully`); + + return response; + }, +}; diff --git a/components/desktime/actions/stop-project/stop-project.mjs b/components/desktime/actions/stop-project/stop-project.mjs new file mode 100644 index 0000000000000..c7447f8390669 --- /dev/null +++ b/components/desktime/actions/stop-project/stop-project.mjs @@ -0,0 +1,37 @@ +import app from "../../desktime.app.mjs"; + +export default { + key: "desktime-stop-project", + name: "Stop Project", + description: "Stop tracking time for a given project and optionally a task. [See the documentation](https://desktime.com/app/settings/api?tab=project)", + version: "0.0.1", + type: "action", + props: { + app, + project: { + propDefinition: [ + app, + "project", + ], + }, + task: { + propDefinition: [ + app, + "task", + ], + }, + }, + async run({ $ }) { + const response = await this.app.stopTracking({ + $, + params: { + project: this.project, + task: this.task, + }, + }); + + $.export("$summary", `Tracking of project \`${this.project}\` stopped successfully`); + + return response; + }, +}; diff --git a/components/desktime/desktime.app.mjs b/components/desktime/desktime.app.mjs index cdd488e224592..f3399d017c692 100644 --- a/components/desktime/desktime.app.mjs +++ b/components/desktime/desktime.app.mjs @@ -1,11 +1,70 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "desktime", - propDefinitions: {}, + propDefinitions: { + projectId: { + type: "string", + label: "Project ID", + description: "The ID of the project to track time for", + }, + taskId: { + type: "string", + label: "Task ID", + description: "The ID of the task to track time for (optional)", + optional: true, + }, + project: { + type: "string", + label: "Project Name", + description: "The name of the project", + }, + task: { + type: "string", + label: "Task Name", + description: "The name of the task", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://desktime.com/api/v2/json"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + params: { + ...params, + apiKey: this.$auth.api_key, + }, + }); + }, + async createProject(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/create-project", + ...args, + }); + }, + async startTracking(args = {}) { + return this._makeRequest({ + path: "/start-project", + ...args, + }); + }, + async stopTracking(args = {}) { + return this._makeRequest({ + path: "/stop-project", + ...args, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/desktime/package.json b/components/desktime/package.json index e28d4c5619400..73711e861eb0a 100644 --- a/components/desktime/package.json +++ b/components/desktime/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/desktime", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream DeskTime Components", "main": "desktime.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" } -} \ No newline at end of file +} diff --git a/components/detectify/README.md b/components/detectify/README.md new file mode 100644 index 0000000000000..66b3681024a34 --- /dev/null +++ b/components/detectify/README.md @@ -0,0 +1,11 @@ +# Overview + +The Detectify API provides a powerful interface for automating and integrating continuous web application security testing into your development cycle. With this API, you can programmatically start scans, fetch scan profiles, retrieve findings, and manage your Detectify domain assets. When leveraged through Pipedream, you can create serverless workflows that trigger actions in other apps based on the results from Detectify, ensuring timely responses to security threats and streamlining your DevSecOps processes. + +# Example Use Cases + +- **Automate Scanning After Deployment**: Trigger a Detectify scan automatically post-deployment from your CI/CD pipeline. Use a webhook to signal a Pipedream workflow that deploys code, and then start a security scan on the updated application. + +- **Slack Alerts for Critical Findings**: Set up a workflow to check for new findings from Detectify on a schedule. Filter for critical security issues and send a summary directly to your security team's Slack channel, facilitating immediate action. + +- **Triage Issues with Jira**: Automatically create Jira tickets for new security findings classified as high risk. This workflow can parse the output from Detectify, create a ticket in Jira with relevant details, and assign it to the appropriate team for resolution. diff --git a/components/detectify/detectify.app.mjs b/components/detectify/detectify.app.mjs index 8462de318957f..5e55d98b6c91c 100644 --- a/components/detectify/detectify.app.mjs +++ b/components/detectify/detectify.app.mjs @@ -1,11 +1,73 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "detectify", - propDefinitions: {}, + propDefinitions: { + domainToken: { + type: "string", + label: "Domain Token", + description: "The asset token for the target domain", + async options({ prevContext }) { + const { + assets, next_marker: marker, + } = await this.listAssets({ + include_subdomains: true, + marker: prevContext?.marker + ? prevContext.marker + : undefined, + }); + return { + options: assets?.map(({ + name, token, + }) => ({ + label: name, + value: token, + })), + context: { + marker, + }, + }; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.detectify.com/rest/v2"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "X-Detectify-Key": `${this.$auth.api_key}`, + }, + }); + }, + listAssets(opts = {}) { + return this._makeRequest({ + path: "/assets/", + ...opts, + }); + }, + listScanProfilesForAsset({ + token, ...opts + }) { + return this._makeRequest({ + path: `/profiles/${token}/`, + ...opts, + }); + }, + listVulnerabilities(opts = {}) { + return this._makeRequest({ + path: "/vulnerabilities/", + ...opts, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/detectify/package.json b/components/detectify/package.json index f8e868f986057..c44e98371523c 100644 --- a/components/detectify/package.json +++ b/components/detectify/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/detectify", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Detectify Components", "main": "detectify.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" } -} \ No newline at end of file +} diff --git a/components/detectify/sources/common/base.mjs b/components/detectify/sources/common/base.mjs new file mode 100644 index 0000000000000..c6111f6083f4c --- /dev/null +++ b/components/detectify/sources/common/base.mjs @@ -0,0 +1,68 @@ +import detectify from "../../detectify.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + detectify, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + domainToken: { + propDefinition: [ + detectify, + "domainToken", + ], + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs"); + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + getTs(item) { + return item.created_at; + }, + getArgs() { + return {}; + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + getResourceType() { + return null; + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run() { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + const resourceFn = this.getResourceFn(); + const args = this.getArgs(lastTs); + const resourceType = this.getResourceType(); + + let results = await resourceFn(args); + if (resourceType) { + results = results[resourceType]; + } + + for (const item of results) { + const ts = this.getTs(item); + if (!lastTs || Date.parse(ts) >= Date.parse(lastTs)) { + maxTs = !maxTs || Date.parse(ts) > Date.parse(maxTs) + ? ts + : maxTs; + this.$emit(item, this.generateMeta(item)); + } + } + + this._setLastTs(maxTs); + }, +}; diff --git a/components/detectify/sources/new-high-risk-finding/new-high-risk-finding.mjs b/components/detectify/sources/new-high-risk-finding/new-high-risk-finding.mjs new file mode 100644 index 0000000000000..a0c8ff7af5c14 --- /dev/null +++ b/components/detectify/sources/new-high-risk-finding/new-high-risk-finding.mjs @@ -0,0 +1,38 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "detectify-new-high-risk-finding", + name: "New High Risk Finding", + description: "Emit new event when a severe security finding at high risk level is detected.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.detectify.listVulnerabilities; + }, + getArgs(lastTs) { + return { + params: { + "severity[]": "high", + "asset_token[]": this.domainToken, + "created_after": lastTs, + }, + }; + }, + getResourceType() { + return "vulnerabilities"; + }, + generateMeta(vulnerability) { + return { + id: vulnerability.uuid, + summary: `New high risk finding: ${vulnerability.title}`, + ts: Date.parse(this.getTs(vulnerability)), + }; + }, + }, + sampleEmit, +}; diff --git a/components/detectify/sources/new-high-risk-finding/test-event.mjs b/components/detectify/sources/new-high-risk-finding/test-event.mjs new file mode 100644 index 0000000000000..11cf7ae9fd677 --- /dev/null +++ b/components/detectify/sources/new-high-risk-finding/test-event.mjs @@ -0,0 +1,219 @@ +export default { + "asset": { + "name": "string", + "token": "string" + }, + "asset_token": "string", + "cookie": { + "domain": "string", + "expires": "2019-08-24T14:15:22Z", + "httponly": true, + "name": "string", + "path": "string", + "secure": true, + "value": "string" + }, + "created_at": "2020-01-01T01:01:00Z", + "cvss_scores": { + "cvss_2_0": { + "score": 0, + "severity": "high", + "vector": "string" + }, + "cvss_3_0": { + "score": 0, + "severity": "high", + "vector": "string" + }, + "cvss_3_1": { + "score": 0, + "severity": "high", + "vector": "string" + } + }, + "cwe": 0, + "definition": { + "description": "string", + "is_crowdsourced": true, + "module_release": "2019-08-24T14:15:22Z", + "module_version": "string", + "risk": "string", + "title": "string" + }, + "details": { + "geography": [ + { + "city": "string", + "country_code": "string", + "country_name": "string", + "highlights": [ + { + "field": "string", + "length": 0, + "offset": 0, + "value": "string" + } + ], + "latitude": "string", + "longitude": "string", + "region": "string", + "topic": "string", + "zip": "string" + } + ], + "graph": [ + { + "data": { + "property1": [ + 0 + ], + "property2": [ + 0 + ] + }, + "highlights": [ + { + "field": "string", + "length": 0, + "offset": 0, + "value": "string" + } + ], + "topic": "string", + "unit": "string" + } + ], + "html": [ + { + "highlights": [ + { + "field": "string", + "length": 0, + "offset": 0, + "value": "string" + } + ], + "topic": "string", + "value": "string" + } + ], + "image": [ + { + "height": 0, + "highlights": [ + { + "field": "string", + "length": 0, + "offset": 0, + "value": "string" + } + ], + "link": "string", + "topic": "string", + "width": 0 + } + ], + "markdown": [ + { + "fallback": "string", + "highlights": [ + { + "field": "string", + "length": 0, + "offset": 0, + "value": "string" + } + ], + "topic": "string", + "value": "string" + } + ], + "text": [ + { + "highlights": [ + { + "field": "string", + "length": 0, + "offset": 0, + "value": "string" + } + ], + "topic": "string", + "value": "string" + } + ], + "video": [ + { + "highlights": [ + { + "field": "string", + "length": 0, + "offset": 0, + "value": "string" + } + ], + "link": "string", + "topic": "string" + } + ] + }, + "host": "admin.example.com", + "links": { + "details_page": "string" + }, + "location": "https://example.com/login.php", + "owasp": [ + { + "classification": "string", + "year": 0 + } + ], + "references": [ + { + "link": "string", + "name": "string", + "source": "string", + "uuid": "string" + } + ], + "request": { + "body": "string", + "headers": [ + { + "name": "string", + "uuid": "string", + "value": "string" + } + ], + "method": "string", + "url": "string" + }, + "response": { + "body": "string", + "headers": [ + { + "name": "string", + "uuid": "string", + "value": "string" + } + ], + "status_code": 0 + }, + "scan_profile_token": "string", + "scan_source": "string", + "severity": "high", + "source": { + "value": "string" + }, + "status": "string", + "tags": [ + { + "name": "string", + "uuid": "string" + } + ], + "title": "string", + "updated_at": "2020-01-01T01:01:00Z", + "uuid": "string", + "version": "string" +} \ No newline at end of file diff --git a/components/detectify/sources/new-medium-risk-finding/new-medium-risk-finding.mjs b/components/detectify/sources/new-medium-risk-finding/new-medium-risk-finding.mjs new file mode 100644 index 0000000000000..94fce4d59fe87 --- /dev/null +++ b/components/detectify/sources/new-medium-risk-finding/new-medium-risk-finding.mjs @@ -0,0 +1,38 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "detectify-new-medium-risk-finding", + name: "New Medium Risk Finding", + description: "Emit new event when a moderate security finding at a medium risk level is recognized.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.detectify.listVulnerabilities; + }, + getArgs(lastTs) { + return { + params: { + "severity[]": "medium", + "asset_token[]": this.domainToken, + "created_after": lastTs, + }, + }; + }, + getResourceType() { + return "vulnerabilities"; + }, + generateMeta(vulnerability) { + return { + id: vulnerability.uuid, + summary: `New medium risk finding: ${vulnerability.title}`, + ts: Date.parse(this.getTs(vulnerability)), + }; + }, + }, + sampleEmit, +}; diff --git a/components/detectify/sources/new-medium-risk-finding/test-event.mjs b/components/detectify/sources/new-medium-risk-finding/test-event.mjs new file mode 100644 index 0000000000000..836af729b5cd8 --- /dev/null +++ b/components/detectify/sources/new-medium-risk-finding/test-event.mjs @@ -0,0 +1,219 @@ +export default { + "asset": { + "name": "string", + "token": "string" + }, + "asset_token": "string", + "cookie": { + "domain": "string", + "expires": "2019-08-24T14:15:22Z", + "httponly": true, + "name": "string", + "path": "string", + "secure": true, + "value": "string" + }, + "created_at": "2020-01-01T01:01:00Z", + "cvss_scores": { + "cvss_2_0": { + "score": 0, + "severity": "medium", + "vector": "string" + }, + "cvss_3_0": { + "score": 0, + "severity": "medium", + "vector": "string" + }, + "cvss_3_1": { + "score": 0, + "severity": "medium", + "vector": "string" + } + }, + "cwe": 0, + "definition": { + "description": "string", + "is_crowdsourced": true, + "module_release": "2019-08-24T14:15:22Z", + "module_version": "string", + "risk": "string", + "title": "string" + }, + "details": { + "geography": [ + { + "city": "string", + "country_code": "string", + "country_name": "string", + "highlights": [ + { + "field": "string", + "length": 0, + "offset": 0, + "value": "string" + } + ], + "latitude": "string", + "longitude": "string", + "region": "string", + "topic": "string", + "zip": "string" + } + ], + "graph": [ + { + "data": { + "property1": [ + 0 + ], + "property2": [ + 0 + ] + }, + "highlights": [ + { + "field": "string", + "length": 0, + "offset": 0, + "value": "string" + } + ], + "topic": "string", + "unit": "string" + } + ], + "html": [ + { + "highlights": [ + { + "field": "string", + "length": 0, + "offset": 0, + "value": "string" + } + ], + "topic": "string", + "value": "string" + } + ], + "image": [ + { + "height": 0, + "highlights": [ + { + "field": "string", + "length": 0, + "offset": 0, + "value": "string" + } + ], + "link": "string", + "topic": "string", + "width": 0 + } + ], + "markdown": [ + { + "fallback": "string", + "highlights": [ + { + "field": "string", + "length": 0, + "offset": 0, + "value": "string" + } + ], + "topic": "string", + "value": "string" + } + ], + "text": [ + { + "highlights": [ + { + "field": "string", + "length": 0, + "offset": 0, + "value": "string" + } + ], + "topic": "string", + "value": "string" + } + ], + "video": [ + { + "highlights": [ + { + "field": "string", + "length": 0, + "offset": 0, + "value": "string" + } + ], + "link": "string", + "topic": "string" + } + ] + }, + "host": "admin.example.com", + "links": { + "details_page": "string" + }, + "location": "https://example.com/login.php", + "owasp": [ + { + "classification": "string", + "year": 0 + } + ], + "references": [ + { + "link": "string", + "name": "string", + "source": "string", + "uuid": "string" + } + ], + "request": { + "body": "string", + "headers": [ + { + "name": "string", + "uuid": "string", + "value": "string" + } + ], + "method": "string", + "url": "string" + }, + "response": { + "body": "string", + "headers": [ + { + "name": "string", + "uuid": "string", + "value": "string" + } + ], + "status_code": 0 + }, + "scan_profile_token": "string", + "scan_source": "string", + "severity": "medium", + "source": { + "value": "string" + }, + "status": "string", + "tags": [ + { + "name": "string", + "uuid": "string" + } + ], + "title": "string", + "updated_at": "2020-01-01T01:01:00Z", + "uuid": "string", + "version": "string" +} \ No newline at end of file diff --git a/components/detectify/sources/new-scan-started/new-scan-started.mjs b/components/detectify/sources/new-scan-started/new-scan-started.mjs new file mode 100644 index 0000000000000..47d61d416cfe6 --- /dev/null +++ b/components/detectify/sources/new-scan-started/new-scan-started.mjs @@ -0,0 +1,35 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "detectify-new-scan-started", + name: "New Scan Started", + description: "Emit new event as soon as a new security scan on the entered domain commences", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.detectify.listScanProfilesForAsset; + }, + getArgs() { + return { + token: this.domainToken, + }; + }, + getTs(profile) { + return profile.latest_scan.started; + }, + generateMeta(profile) { + const started = this.getTs(profile); + return { + id: Date.parse(started), + summary: `New scan started at ${started}`, + ts: Date.parse(started), + }; + }, + }, + sampleEmit, +}; diff --git a/components/detectify/sources/new-scan-started/test-event.mjs b/components/detectify/sources/new-scan-started/test-event.mjs new file mode 100644 index 0000000000000..c7a427a6e6028 --- /dev/null +++ b/components/detectify/sources/new-scan-started/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "name": "scan profile", + "endpoint": "detectify.com", + "created": "2024-05-17T15:11:22Z", + "token": "6ee09e11a9baa95ba34d71400dce36c4", + "latest_scan": { + "started": "2024-05-17T16:24:14Z", + "ended": "2024-05-17T16:58:17Z", + "status": "completed" + }, + "status": "verified" +} \ No newline at end of file diff --git a/components/detrack/README.md b/components/detrack/README.md index 853038372ecc5..8f042a5b2bf98 100644 --- a/components/detrack/README.md +++ b/components/detrack/README.md @@ -1,11 +1,11 @@ # Overview -The DeTrack API allows developers to access data from the DeTrack system, -including GPS tracking data, vehicle and driver information, and much more. -Using the DeTrack API, developers can build a wide range of applications and -integrations, such as: - -- GPS tracking applications -- Vehicle and driver management systems -- Logistics and fleet management applications -- Integration with other business systems +The DeTrack API offers the functionality to track deliveries and vehicles, manage delivery-related data, and automate communication between logistics stakeholders. Leveraging the DeTrack API on Pipedream, you can create a seamless flow of delivery information, reduce manual work, and keep all parties up-to-date with real-time notifications. This API’s capabilities when integrated into workflows can streamline inventory management, enhance customer service, and optimize delivery operations. + +# Example Use Cases + +- **Automated Delivery Status Updates to Customers**: Track delivery status through DeTrack and automatically send SMS or email notifications to customers via Twilio or SendGrid when their order status changes. This keeps the customer informed at every step of the delivery process without manual intervention. + +- **Synchronize Delivery Data with Inventory Management Systems**: As deliveries are completed, use the DeTrack API within a Pipedream workflow to update inventory levels in an inventory management system like QuickBooks or Airtable. This real-time synchronization helps maintain accurate stock levels and can trigger restocking workflows if needed. + +- **Real-Time Dashboard for Delivery Operations**: Collect live data from the DeTrack API and feed it into a BI tool such as Google Data Studio or Tableau on Pipedream. This creates a real-time operational dashboard that visualizes delivery metrics, helping decision-makers to identify bottlenecks and improve logistics efficiency. diff --git a/components/dev_to/README.md b/components/dev_to/README.md index b55e1ba0e5295..8c3b63dbdb1db 100644 --- a/components/dev_to/README.md +++ b/components/dev_to/README.md @@ -1,9 +1,11 @@ # Overview -With the Dev.to API, you can build applications that: +The Dev.to API enables programmatic interaction with Dev.to, a community for software developers to share articles and engage with content. Through the API, you can automate content creation, management, and analysis. Accessing user articles, managing comments, and triggering actions based on Dev.to events are just a few capabilities at your disposal. By leveraging these with Pipedream's serverless platform, you can craft powerful automations that respond in real-time to activities on Dev.to, streamlining your content strategy and community engagement. -- Stand out from the crowd on Dev.to by customizing your profile page -- Build a Dev.to extension that helps other developers be more productive -- Develop a bot that helps moderation on Dev.to -- Get notified whenever a new article is published on a certain topic -- And much more! +# Example Use Cases + +- **Content Synchronization**: Sync new Dev.to articles to other platforms like Medium or your personal blog. When a new article is published on Dev.to, Pipedream can automatically format and post it to other platforms, keeping your content consistent across the web. + +- **Social Media Sharing**: Amplify your content reach by auto-sharing published Dev.to articles to social networks such as Twitter or LinkedIn. Once your article goes live, trigger a Pipedream workflow that posts a tailored message with the article link to your social profiles. + +- **Comment Moderation Alert System**: Maintain a healthy community by setting up a comment moderation alert system. Use Pipedream to monitor new comments on your Dev.to articles, applying sentiment analysis via a machine learning API. Get instant notifications for negative sentiments allowing you to respond or moderate quickly. diff --git a/components/device_magic/README.md b/components/device_magic/README.md new file mode 100644 index 0000000000000..d43d3ef43d639 --- /dev/null +++ b/components/device_magic/README.md @@ -0,0 +1,11 @@ +# Overview + +Device Magic is a mobile forms and data collection tool that allows field teams to capture and submit form data using smartphones and tablets. With the Device Magic API on Pipedream, you can automate the ingestion, processing, and distribution of this data. Harnessing Pipedream's capabilities, you can trigger workflows upon form submission, manipulate the data, and integrate it with countless other services, all in real-time. This enables seamless data flow from field to office, triggering notifications, analytics, and even actions in other business applications. + +# Example Use Cases + +- **Automated Data Processing and Storage**: Once a form is submitted via Device Magic, trigger a Pipedream workflow to parse the data, apply any necessary transformations, and store it in a cloud database like AWS DynamoDB. This can be instrumental for maintaining up-to-date records and ensuring data quality. + +- **Dynamic Reporting and Notification**: Use Pipedream to create a workflow where new Device Magic submissions trigger a report generation. The workflow could format the data into a report within Google Sheets, and then send a notification with a summary or link to the report via email through apps like SendGrid or direct messaging platforms like Slack. + +- **Streamlined Task Assignment**: On submission of a Device Magic form, trigger a Pipedream workflow to analyze the data and create tasks in a project management tool such as Trello or Asana. For instance, a maintenance request form could result in the automatic creation of a new card in Trello, assigned to the appropriate team with all the relevant details included. diff --git a/components/device_magic/device_magic.app.mjs b/components/device_magic/device_magic.app.mjs new file mode 100644 index 0000000000000..68ea1c0cb7cc8 --- /dev/null +++ b/components/device_magic/device_magic.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "device_magic", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/device_magic/package.json b/components/device_magic/package.json new file mode 100644 index 0000000000000..c5f922e24eac5 --- /dev/null +++ b/components/device_magic/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/device_magic", + "version": "0.0.1", + "description": "Pipedream Device Magic Components", + "main": "device_magic.app.mjs", + "keywords": [ + "pipedream", + "device_magic" + ], + "homepage": "https://pipedream.com/apps/device_magic", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/devrev/README.md b/components/devrev/README.md new file mode 100644 index 0000000000000..42100e975a512 --- /dev/null +++ b/components/devrev/README.md @@ -0,0 +1,11 @@ +# Overview + +The DevRev API bridges the gap between developers and customers, enabling teams to interact with customer data, handle support issues, and integrate with various development workflows. Within Pipedream, you can wield this API to automate tasks like syncing customer feedback with your issue trackers, updating CRM records, or triggering custom notifications based on customer interactions. + +# Example Use Cases + +- **Customer Feedback to Issue Tracker**: When a new customer feedback comes in via DevRev, automatically create an issue in your project management tool like Jira or GitHub, ensuring that the feedback is prioritized and addressed in your development cycle. + +- **CRM Sync**: On updating a customer’s status or adding a new customer in DevRev, trigger a workflow to update those details in a CRM like Salesforce, keeping all your customer records in sync across different platforms. + +- **Custom Notifications for Support Tickets**: Set up a workflow that listens for new support tickets in DevRev, then sends a formatted message to a Slack channel or sends an email to the support team, ensuring fast response times to customer issues. diff --git a/components/dex/README.md b/components/dex/README.md new file mode 100644 index 0000000000000..2539c5618cfcb --- /dev/null +++ b/components/dex/README.md @@ -0,0 +1,11 @@ +# Overview + +The Dex API offers a path to streamline and automate your relationship management by allowing you to sync, update, and interact with your personal Dex CRM data programmatically. On Pipedream, you can leverage this API to create powerful automations that keep your network vibrant and your connections strong, without manual effort. By integrating Dex with other apps on Pipedream, you can tailor workflows that sync contact info, set reminders for follow-ups, or trigger personalized communications based on various criteria. + +# Example Use Cases + +- **Automated Contact Syncing Workflow**: Use the Dex API to automatically sync new LinkedIn connections to your Dex account. Trigger a Pipedream workflow when someone accepts your LinkedIn connection request, and add them as a contact in Dex with their details, such as name, company, and title. + +- **Follow-Up Reminder Setup**: Create a workflow that adds follow-up reminders in Dex for new contacts added from a networking event. Whenever a new contact is added to your Google Sheets spreadsheet from a conference, use that trigger to create a corresponding reminder in Dex, ensuring you never miss an opportunity to nurture a connection. + +- **Personalized Email Campaigns**: Launch personalized email campaigns by integrating Dex with a mailing service like SendGrid on Pipedream. When you tag a contact in Dex for a specific interest, automatically enroll them in a tailored email sequence in SendGrid, providing valuable content that aligns with their interests. diff --git a/components/dex/actions/create-note/create-note.mjs b/components/dex/actions/create-note/create-note.mjs new file mode 100644 index 0000000000000..4737c91758fcf --- /dev/null +++ b/components/dex/actions/create-note/create-note.mjs @@ -0,0 +1,49 @@ +import dex from "../../dex.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "dex-create-note", + name: "Create Note", + description: "Establishes a brand new note within dex. [See the documentation](https://guide.getdex.com/dex-user-api/post-a-note)", + version: "0.0.1", + type: "action", + props: { + dex, + note: { + type: "string", + label: "Note", + description: "Content of the note", + }, + contactIds: { + propDefinition: [ + dex, + "contactIds", + ], + }, + eventTime: { + type: "string", + label: "Event Time", + description: "Time of the note in ISO 8601 format (`2023-05-19T01:03:27.083Z`)", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.dex.createNote({ + $, + data: { + timeline_event: { + note: this.note, + event_time: this.eventTime || utils.getCurrentDatetime(), + meeting_type: "note", + timeline_items_contacts: this.contactIds?.length + ? { + data: utils.buildContactData(this.contactIds), + } + : undefined, + }, + }, + }); + $.export("$summary", `Successfully created note with ID: ${response.insert_timeline_items_one.id}`); + return response; + }, +}; diff --git a/components/dex/actions/create-reminder/create-reminder.mjs b/components/dex/actions/create-reminder/create-reminder.mjs new file mode 100644 index 0000000000000..92167d9b630be --- /dev/null +++ b/components/dex/actions/create-reminder/create-reminder.mjs @@ -0,0 +1,55 @@ +import dex from "../../dex.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "dex-create-reminder", + name: "Create Reminder", + description: "Generates a new reminder within the Dex system. [See the documentation](https://guide.getdex.com/dex-user-api/post-reminder)", + version: "0.0.1", + type: "action", + props: { + dex, + text: { + type: "string", + label: "Text", + description: "Text of the reminder", + }, + isComplete: { + type: "boolean", + label: "IsComplete", + description: "Whether the reminder is completed", + optional: true, + }, + dueAtDate: { + type: "string", + label: "Due at Date", + description: "The date of the reminder in ISO 8601 format (`2023-05-19T01:03:27.083Z`)", + optional: true, + }, + contactIds: { + propDefinition: [ + dex, + "contactIds", + ], + }, + }, + async run({ $ }) { + const response = await this.dex.createReminder({ + $, + data: { + reminder: { + text: this.text, + is_complete: this.isComplete, + due_at_date: this.dueAtDate || utils.getCurrentDatetime(), + reminders_contacts: this.contactIds?.length + ? { + data: utils.buildContactData(this.contactIds), + } + : undefined, + }, + }, + }); + $.export("$summary", `Successfully created reminder with ID: ${response.insert_reminders_one.id}`); + return response; + }, +}; diff --git a/components/dex/actions/create-update-contact/create-update-contact.mjs b/components/dex/actions/create-update-contact/create-update-contact.mjs new file mode 100644 index 0000000000000..50237da04cab7 --- /dev/null +++ b/components/dex/actions/create-update-contact/create-update-contact.mjs @@ -0,0 +1,160 @@ +import dex from "../../dex.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "dex-create-update-contact", + name: "Create or Update Contact", + description: "Adds a new contact or updates an existing one if the email address already exists in the Dex system. [See the documentation](https://guide.getdex.com/dex-user-api/post-contact)", + version: "0.0.1", + type: "action", + props: { + dex, + email: { + type: "string", + label: "Email", + description: "The email of the contact", + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the contact", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the contact", + optional: true, + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number of the contact", + optional: true, + }, + jobTitle: { + type: "string", + label: "Job Title", + description: "Job title of the contact", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "Description of the contact", + optional: true, + }, + education: { + type: "string", + label: "Education", + description: "Education of the contact", + optional: true, + }, + imageUrl: { + type: "string", + label: "Image URL", + description: "Image URL of the contact", + optional: true, + }, + linkedin: { + type: "string", + label: "LinkedIn", + description: "LinkedIn URL of the contact", + optional: true, + }, + twitter: { + type: "string", + label: "Twitter", + description: "Twitter URL of the contact", + optional: true, + }, + instagram: { + type: "string", + label: "Instagram", + description: "Instagram URL of the contact", + optional: true, + }, + telegram: { + type: "string", + label: "Telegram", + description: "Telegram URL of the contact", + optional: true, + }, + birthdayYear: { + type: "string", + label: "Birthday Year", + description: "Birthday year of the contact", + optional: true, + }, + }, + async run({ $ }) { + const { search_contacts_by_exact_email: match } = await this.dex.getContact({ + $, + params: { + email: this.email, + }, + }); + const data = utils.cleanObject({ + first_name: this.firstName, + last_name: this.lastName, + job_title: this.jobTitle, + description: this.description, + education: this.education, + image_url: this.imageUrl, + linkedin: this.linkedin, + twitter: this.twitter, + instagram: this.instagram, + telegram: this.telegram, + birthday_year: this.birthdayYear, + }); + let response; + if (match?.length) { + const contact = match[0]; + response = await this.dex.updateContact({ + $, + contactId: contact.id, + data: utils.cleanObject({ + contactId: contact.id, + changes: data, + update_contact_phone_numbers: this.phoneNumber + ? true + : undefined, + contact_phone_numbers: this.phoneNumber + ? [ + { + contact_id: contact.id, + phone_number: this.phoneNumber, + }, + ] + : undefined, + }), + }); + } else { + response = await this.dex.createContact({ + $, + data: { + contact: utils.cleanObject({ + ...data, + contact_emails: { + data: { + email: this.email, + }, + }, + contact_phone_numbers: this.phoneNumber + ? { + data: { + phone_number: this.phoneNumber, + }, + } + : undefined, + }), + }, + }); + } + + $.export("$summary", `Successfully ${match?.length + ? "updated" + : "created"} contact ${this.email}`); + return response; + }, +}; diff --git a/components/dex/common/constants.mjs b/components/dex/common/constants.mjs new file mode 100644 index 0000000000000..7f4081138acc1 --- /dev/null +++ b/components/dex/common/constants.mjs @@ -0,0 +1,5 @@ +const DEFAULT_LIMIT = 20; + +export default { + DEFAULT_LIMIT, +}; diff --git a/components/dex/common/utils.mjs b/components/dex/common/utils.mjs new file mode 100644 index 0000000000000..22eec297d285f --- /dev/null +++ b/components/dex/common/utils.mjs @@ -0,0 +1,20 @@ +export default { + getCurrentDatetime() { + return new Date().toISOString() + .slice(0, 19) + .replace("T", " ") + "Z"; + }, + buildContactData(contactIds) { + return contactIds?.map((contactId) => ({ + contact_id: contactId, + })) || []; + }, + cleanObject(o) { + for (var k in o || {}) { + if (typeof o[k] === "undefined") { + delete o[k]; + } + } + return o; + }, +}; diff --git a/components/dex/dex.app.mjs b/components/dex/dex.app.mjs new file mode 100644 index 0000000000000..b99a771a28f9b --- /dev/null +++ b/components/dex/dex.app.mjs @@ -0,0 +1,91 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "dex", + propDefinitions: { + contactIds: { + type: "string[]", + label: "Contact IDs", + description: "Array of contact IDs", + optional: true, + async options({ page }) { + const limit = constants.DEFAULT_LIMIT; + const { contacts } = await this.listContacts({ + params: { + limit, + offset: page * limit, + }, + }); + return contacts?.map(({ + id: value, first_name: firstname, last_name: lastname, + }) => ({ + value, + label: `${firstname} ${lastname}`, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.getdex.com/api/rest"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "x-hasura-dex-api-key": `${this.$auth.api_key}`, + }, + }); + }, + getContact(opts = {}) { + return this._makeRequest({ + path: "/search/contacts", + ...opts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts", + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contacts", + ...opts, + }); + }, + updateContact({ + contactId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/contacts/${contactId}`, + ...opts, + }); + }, + createReminder(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/reminders", + ...opts, + }); + }, + createNote(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/timeline_items", + ...opts, + }); + }, + }, +}; diff --git a/components/dex/package.json b/components/dex/package.json new file mode 100644 index 0000000000000..42a2ff042950c --- /dev/null +++ b/components/dex/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/dex", + "version": "0.1.0", + "description": "Pipedream Dex Components", + "main": "dex.app.mjs", + "keywords": [ + "pipedream", + "dex" + ], + "homepage": "https://pipedream.com/apps/dex", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/dexatel/README.md b/components/dexatel/README.md new file mode 100644 index 0000000000000..baec27ad6cbd8 --- /dev/null +++ b/components/dexatel/README.md @@ -0,0 +1,11 @@ +# Overview + +The Dexatel API provides capabilities for sending and receiving SMS and voice messages globally, enabling businesses to enhance communication and marketing efforts. Leveraging the Dexatel API on Pipedream allows you to automate messaging flows, integrate with CRM platforms, trigger alerts or notifications based on certain events, and more. By connecting Dexatel to other apps available on Pipedream, you can create powerful, event-driven workflows that save time and increase efficiency. + +# Example Use Cases + +- **Automated Customer Support Messages**: Send SMS updates or alerts to customers based on their interactions with your service. For instance, trigger a message when a customer signs up, places an order, or their order is out for delivery. + +- **Event-Driven Notifications**: Combine Dexatel with scheduling apps on Pipedream to send SMS reminders before appointments or events. You can even automate follow-up messages asking for feedback after the event has concluded. + +- **Integrating with CRM Systems**: Sync Dexatel with a CRM app like Salesforce or HubSpot. Automatically send personalized SMS messages to leads or customers based on CRM data such as deal stage changes or service renewals. diff --git a/components/dext/README.md b/components/dext/README.md new file mode 100644 index 0000000000000..5a3541dc4bd2d --- /dev/null +++ b/components/dext/README.md @@ -0,0 +1,11 @@ +# Overview + +The Dext API allows you to automate the management of expenses, receipts, and invoices within your Dext account. With Pipedream, you can harness this API to create workflows that streamline your finance operations, such as extracting data from receipts, categorizing expenses, and syncing financial documents across various platforms. By connecting Dext with other apps through Pipedream, you can craft efficient, serverless workflows that save time and reduce manual data entry. + +# Example Use Cases + +- **Automated Receipt Processing**: Trigger a Pipedream workflow with new receipts uploaded to Dext. Extract key data with Dext's API and insert it into a Google Sheets spreadsheet for real-time expense tracking. + +- **Expense Approval Notifications**: Use Dext to review expenses, then set up a Pipedream workflow to send approval notifications through Slack or email, streamlining the communication process involved in expense management. + +- **Monthly Financial Reports**: Build a workflow on Pipedream that gathers all approved expenses and invoices from Dext at month-end, compiles them into a report, and saves it to Dropbox or sends it via email for easy access and archiving. diff --git a/components/diabatix_coldstream/README.md b/components/diabatix_coldstream/README.md new file mode 100644 index 0000000000000..025aaa218f61c --- /dev/null +++ b/components/diabatix_coldstream/README.md @@ -0,0 +1,11 @@ +# Overview + +The Diabatix ColdStream API provides automated thermal analysis capabilities, allowing users to streamline the cooling design process for various components and devices. With this API, you can automate the design of thermal systems, optimize existing cooling solutions, and simulate different scenarios to find the most effective thermal management strategy. In Pipedream, you can leverage this API to build automated workflows that integrate thermal analysis into your engineering cycles, ensuring your designs meet the necessary thermal specifications before physical prototypes are ever built. + +# Example Use Cases + +- **Automated Thermal Analysis Reporting**: Create a workflow in Pipedream that triggers every time a new design is uploaded to your cloud storage (e.g., Google Drive). The workflow sends the design to the Diabatix ColdStream API to perform thermal analysis and then emails the report to your engineering team or saves it to a dedicated report directory. + +- **Design Optimization Loop**: Set up a continuous optimization process where initial thermal design parameters are sent to the ColdStream API. The results are then evaluated, and if they don't meet specified performance criteria, the parameters are adjusted and the design is resubmitted. This loop can continue until the thermal performance is optimized, all automated within Pipedream. + +- **Thermal Analysis Dashboard Integration**: Build a Pipedream workflow that integrates with a dashboard app like Geckoboard. Use Diabatix ColdStream API to run thermal analyses at regular intervals or upon specific events, and then push the summarized data to Geckoboard. This allows real-time monitoring of thermal performance metrics for ongoing projects. diff --git a/components/dialmycalls/README.md b/components/dialmycalls/README.md index 10f8356698c09..3c338aa0c57dc 100644 --- a/components/dialmycalls/README.md +++ b/components/dialmycalls/README.md @@ -1,11 +1,11 @@ # Overview -With the DialMyCalls API you can easily integrate voice and text broadcasting -into your application. For example, you could use the API to: - -- Create a system to mass notify your customers or employees of important news - or updates -- Build a service to send out appointment reminders or other time-sensitive - notifications -- Integrate DialMyCalls into your CRM or other application to automatically - place calls or send texts to your customers or leads +The DialMyCalls API lets you harness the power of voice and text messaging for mass communication. With this API, you can automate the creation and dispatch of messages to lists of recipients, schedule calls, manage contacts, and track the delivery status of your messages. This becomes particularly useful in scenarios like emergency alerts, appointment reminders, and promotional campaigns where reaching out to a broad audience swiftly and reliably is key. + +# Example Use Cases + +- **Emergency Alert System**: Automatically trigger voice or SMS alerts to a predefined list of contacts when Pipedream detects a specific keyword in an email or a tweet, ensuring rapid dissemination of critical information during emergencies. + +- **Appointment Reminder Service**: Integrate Pipedream with a calendar app like Google Calendar to schedule and send reminder calls or texts via DialMyCalls API to clients about their upcoming appointments, reducing no-shows. + +- **Marketing Campaign Automation**: Use Pipedream to listen for new subscribers on platforms like Mailchimp, then automatically enroll them in a promotional campaign via DialMyCalls, sending personalized messages or special offers to engage them right away. diff --git a/components/dialpad/README.md b/components/dialpad/README.md index 386dd63bba3fc..c3e0cfeeabf30 100644 --- a/components/dialpad/README.md +++ b/components/dialpad/README.md @@ -1,14 +1,11 @@ # Overview -With the Dialpad API, you can build a wide range of voice and video -communication applications. - -Here are some examples of what you can build with the Dialpad API: - -- A softphone application that lets users make and receive calls from their - computer -- A business phone system that lets companies make and receive calls using - Dialpad's infrastructure -- A call center application that lets customer service representatives make and - receive calls -- A video conferencing application that lets users make and receive video calls +The Dialpad API taps into the core of Dialpad's communication platform, allowing for the automation of voice and messaging workflows. By leveraging this API through Pipedream, you can interact with call data, manage users, and automate sending of SMS messages, among other tasks. This enables the creation of intricate, automated processes that can enhance business communication efficiency, customer support, and team collaboration within your organization. + +# Example Use Cases + +- **Automated Customer Support Ticket Creation**: When a call ends in Dialpad, trigger a workflow on Pipedream that creates a support ticket in a tool like Zendesk. Include details like caller ID, call duration, and any notes taken during the call to provide context for the support team. + +- **SMS Outreach Campaigns**: Use Pipedream to listen for a specific trigger, such as a new email subscriber or a form submission. Then, send a personalized SMS via Dialpad to the new contact, providing a warm welcome or timely information about your services. + +- **User Provisioning Automation**: Automate the process of setting up new employees in Dialpad when they are added to your HR system, like BambooHR. Trigger a Pipedream workflow that creates a new user in Dialpad, assigns a number, and sends an introductory email with setup instructions to the new hire. diff --git a/components/dictionary_api/README.md b/components/dictionary_api/README.md index dbd54ea2e70c8..bbf43a0ad6b14 100644 --- a/components/dictionary_api/README.md +++ b/components/dictionary_api/README.md @@ -1,14 +1,11 @@ # Overview -The Free Dictionary API is a powerful tool that allows you to access the vast -array of dictionary data from the comfort of your own web site or application. - -With the Free Dictionary API you can: - -- Look up the definition of a word -- Find synonyms and antonyms for a word -- Get pronunciation information for a word -- Search for words by their prefix, suffix, or root -- Find words that rhyme with another word -- Get etymological information for a word -- And much more! +The Free Dictionary API provides a straightforward way to access dictionary definitions, synonyms, antonyms, and translations for words in multiple languages. With it, you can fetch detailed word meanings, usage examples, and phonetic transcriptions, enabling a variety of educational and linguistic automation opportunities on Pipedream. This API is valuable for developers looking to create language learning tools, content enrichment services, or to simply integrate rich lexical data into their applications. + +# Example Use Cases + +- **Content Enrichment Bot**: Craft a bot that monitors chat platforms like Slack or Discord, using Pipedream's triggers and actions. When a user asks for the definition or synonyms of a word, the bot can call the Free Dictionary API and respond with the relevant information, enhancing communication with real-time language support. + +- **Vocabulary Builder App**: Set up a daily routine that fetches a "Word of the Day" from the Free Dictionary API, and pushes it to an email list or a social media account. This workflow can help language learners expand their vocabulary consistently and can be enhanced by integrating translation services for multilingual support. + +- **Interactive Educational Content**: Develop an interactive quiz or flashcard application that pulls definitions, synonyms, and antonyms from the Free Dictionary API. Trigger these workflows through webhooks when learners request a challenge, providing a dynamic way to reinforce language comprehension and retention. diff --git a/components/diffbot/README.md b/components/diffbot/README.md new file mode 100644 index 0000000000000..00485b1d47f12 --- /dev/null +++ b/components/diffbot/README.md @@ -0,0 +1,14 @@ +# Overview + +The Diffbot API enables you to extract structured data from web pages automatically. It transforms the chaos of the web into usable information through web scraping and natural language processing. On Pipedream, you can use Diffbot to monitor changes on websites, extract article data, or process web pages for specific information. By tapping into Pipedream’s ability to integrate with hundreds of other services, you can create powerful workflows that automate data extraction and act on the data in real-time. + +# Example Use Cases + +- **Content Change Detection** + Use Diffbot to monitor a product page for changes and trigger a Pipedream workflow whenever the price or availability changes. This can be connected to a notification service like Slack or email to alert your team immediately. + +- **Article Extraction and Analysis** + Extract articles from a set of URLs using Diffbot and send the extracted text to a sentiment analysis API like IBM Watson. Store the sentiment scores in a Google Sheet for easy tracking and visualization of the sentiment of news articles or blog posts over time. + +- **Lead Generation Automation** + Scrape contact information from a list of company websites using Diffbot. Combine this with the Clearbit API to enrich the data and add it to a CRM like Salesforce or HubSpot, automating the lead generation process. diff --git a/components/diffbot/actions/enhance-entity/enhance-entity.mjs b/components/diffbot/actions/enhance-entity/enhance-entity.mjs new file mode 100644 index 0000000000000..bcbffb1be4aa3 --- /dev/null +++ b/components/diffbot/actions/enhance-entity/enhance-entity.mjs @@ -0,0 +1,49 @@ +import app from "../../diffbot.app.mjs"; + +export default { + key: "diffbot-enhance-entity", + name: "Enhance Entity", + description: "Enrich a person or organization record with partial data input [See the documentation] (https://docs.diffbot.com/reference/enhancepost)", + version: "0.0.2", + type: "action", + props: { + app, + type: { + propDefinition: [ + app, + "type", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + location: { + propDefinition: [ + app, + "location", + ], + }, + url: { + propDefinition: [ + app, + "url", + ], + }, + }, + async run({ $ }) { + const { + app, ...data + } = this; + const response = await app.enhanceEntity({ + $, + data, + }); + + $.export("$summary", "Successfully enhanced entity"); + + return response; + }, +}; diff --git a/components/diffbot/actions/extract-page/extract-page.mjs b/components/diffbot/actions/extract-page/extract-page.mjs new file mode 100644 index 0000000000000..1dce051fa0ec1 --- /dev/null +++ b/components/diffbot/actions/extract-page/extract-page.mjs @@ -0,0 +1,30 @@ +import app from "../../diffbot.app.mjs"; + +export default { + key: "diffbot-extract-page", + name: "Extract Page", + description: "Automatically classify a page and extract data according to its type. [See the documentation](https://docs.diffbot.com/reference/extract-analyze)", + version: "0.0.1", + type: "action", + props: { + app, + url: { + propDefinition: [ + app, + "url", + ], + }, + }, + async run({ $ }) { + const response = await this.app.extractPage({ + $, + params: { + url: this.url, + }, + }); + + $.export("$summary", `Successfully extracted data from ${response.title}`); + + return response; + }, +}; diff --git a/components/diffbot/diffbot.app.mjs b/components/diffbot/diffbot.app.mjs index f5f3d050febdf..08513e5c036b7 100644 --- a/components/diffbot/diffbot.app.mjs +++ b/components/diffbot/diffbot.app.mjs @@ -1,11 +1,72 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "diffbot", - propDefinitions: {}, + propDefinitions: { + name: { + type: "string", + label: "Entity Name", + description: "The name of the entity to enhance", + optional: true, + }, + url: { + type: "string", + label: "URL", + description: "URL to extract or enhance", + optional: true, + }, + location: { + type: "string", + label: "Entity location", + description: "Location of the entity to enhance", + optional: true, + }, + type: { + type: "string", + label: "Entity type", + description: "Diffbot entity type", + options: [ + "person", + "organization", + ], + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl(base) { + return `https://${base}.diffbot.com`; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + base, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl(base) + path, + params: { + ...params, + token: `${this.$auth.api_token}`, + }, + }); + }, + async enhanceEntity(args = {}) { + return this._makeRequest({ + method: "post", + base: "kg", + path: "/kg/v3/enhance", + ...args, + }); + }, + async extractPage(args = {}) { + return this._makeRequest({ + base: "api", + path: "/v3/analyze", + ...args, + }); }, }, }; diff --git a/components/diffbot/package.json b/components/diffbot/package.json index f37ffae3ec0fc..417705addbabe 100644 --- a/components/diffbot/package.json +++ b/components/diffbot/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/diffbot", - "version": "0.0.1", + "version": "0.2.0", "description": "Pipedream Diffbot Components", "main": "diffbot.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" } -} \ No newline at end of file +} diff --git a/components/diffchecker/README.md b/components/diffchecker/README.md new file mode 100644 index 0000000000000..84f7ca4bd7b02 --- /dev/null +++ b/components/diffchecker/README.md @@ -0,0 +1,11 @@ +# Overview + +The Diffchecker API allows you to compare differences between texts, images, PDFs, and other files programmatically, providing an automated way to spot changes or inconsistencies. Leveraging this API within Pipedream workflows can automate diff tasks, alerting you to changes, syncing content across platforms, or maintaining consistency in code or document versions. Pipedream's ability to connect to various services and trigger workflows based on numerous events makes it an excellent tool for integrating Diffchecker in a variety of scenarios. + +# Example Use Cases + +- **Automated Content Change Detection**: When content on a website changes, the Diffchecker API can compare the new content with a stored version. If differences are detected, the Pipedream workflow can send notifications via email, Slack, or other messaging platforms to relevant stakeholders. + +- **Code Review Automation**: After a push to a Git repository, a Pipedream workflow can trigger a Diffchecker API call to compare the new commit against the main branch. The output can be used for automated code reviews, sending summary diff reports to project managers, or flagging potential issues to developers via GitHub issues or JIRA. + +- **Periodic Backup Integrity Checks**: Regularly compare backups with their previous versions using the Diffchecker API within a Pipedream schedule. This can help verify the integrity of the backups and provide peace of mind that critical data hasn't been altered or corrupted. Notifications or logs can be managed via Pipedream's built-in services or sent to external databases or logging services. diff --git a/components/diffy/README.md b/components/diffy/README.md new file mode 100644 index 0000000000000..8b9087708fe3f --- /dev/null +++ b/components/diffy/README.md @@ -0,0 +1,11 @@ +# Overview + +The Diffy API offers visual regression testing, allowing you to compare visuals of web pages over time or across different environments. With Pipedream's serverless architecture, you can integrate Diffy into various workflows to automate visual testing, get notified of changes, or even trigger deployments in a CI/CD pipeline when a visual test passes. + +# Example Use Cases + +- **Scheduled Visual Regression Tests**: Set up a Pipedream workflow that triggers on a schedule to run visual comparisons of your production site against a staging environment using the Diffy API. If differences exceed a certain threshold, it can alert your team through Slack or email for review. + +- **Monitor Competitor Websites**: Create a Pipedream workflow that periodically checks your competitors' websites using the Diffy API. Store the snapshots in a data store, and if significant visual changes are detected, notify your marketing team so they can analyze the competitor's strategy. + +- **Integration with Deployment Pipelines**: Design a workflow in Pipedream to act as a gatekeeper in your deployment pipeline. After a deployment to a test environment, trigger a visual test using Diffy API. If the test passes, automatically promote the build to the next stage in your pipeline; if not, halt the deployment and notify your development team. diff --git a/components/digicert/README.md b/components/digicert/README.md index 31499c00cc904..396de29962702 100644 --- a/components/digicert/README.md +++ b/components/digicert/README.md @@ -1,15 +1,11 @@ # Overview -With the DigiCert API, you can programmatically request and renew SSL/TLS -certificates, automate certificate provisioning for your systems and web -servers, and monitor certificate inventory and expiration dates. - -Here are a few examples of what you can build with the DigiCert API: - -- A tool to automatically request and renew SSL/TLS certificates for your web - servers -- A system to track your organization's SSL/TLS certificate inventory and - expiration dates -- A dashboard to monitor the status of your SSL/TLS certificates -- A tool to automate the provisioning of SSL/TLS certificates for your systems - and web servers +The DigiCert API provides a programmatic interface to manage the lifecycle of SSL/TLS certificates, offering automation for tasks such as ordering, reissuing, and renewing certificates. Using Pipedream, you can harness this API to create powerful workflows that enhance your certificate management process, ensuring security compliance, automating routine tasks, and integrating with other services, like monitoring systems or deployment pipelines. + +# Example Use Cases + +- **Automated Certificate Renewal Notifications**: Create a workflow on Pipedream that periodically checks the expiration status of your SSL/TLS certificates through the DigiCert API. If a certificate is nearing its expiry date, automatically send renewal reminders via email or push notifications to your team with apps like Gmail or Slack integrated into the workflow. + +- **Provisioning Certificates for New Domains**: Set up a Pipedream workflow that listens for events from a domain management system or triggers when a new domain is registered. Use DigiCert API to automatically issue certificates for these new domains and, optionally, deploy them to your servers via SSH or integrate with infrastructure as code tools like Terraform to update your configurations. + +- **Incident Response for Certificate Errors**: Construct a Pipedream workflow that monitors your site’s certificate health by integrating with uptime monitoring services like UptimeRobot. On detecting SSL errors, trigger the workflow to automatically reissue or renew certificates with the DigiCert API, and then alert your DevOps team through channels like PagerDuty or Opsgenie. diff --git a/components/digistore24/README.md b/components/digistore24/README.md new file mode 100644 index 0000000000000..ea2c2064bedd8 --- /dev/null +++ b/components/digistore24/README.md @@ -0,0 +1,11 @@ +# Overview + +Digistore24 API allows for automation of tasks related to digital sales, such as product management, order processing, and affiliate tracking. Using Pipedream, you can craft workflows that automate repetitive tasks, integrating with other services to streamline your digital sales pipeline. Pipedream's serverless platform facilitates the creation of these workflows without the need for dedicated infrastructure, making it easier to connect Digistore24 to a multitude of other apps and services. + +# Example Use Cases + +- **Sync New Purchases to a Google Sheet**: When a new purchase is made on Digistore24, automatically capture the order details and log them into a Google Sheet. This workflow enables real-time monitoring and reporting of sales data without manual entry. + +- **Send Custom Thank-You Emails via SendGrid**: Customize and send thank-you emails through SendGrid for each completed purchase. Use customer data from Digistore24 to personalize the message, enhancing customer engagement post-purchase. + +- **Create Support Tickets from Refund Requests**: Trigger the creation of a support ticket in a tool like Zendesk whenever a refund is requested on Digistore24. This ensures timely follow-up and maintains a high level of customer service. diff --git a/components/digital_ocean/README.md b/components/digital_ocean/README.md index 3db2bf8442935..5c51f011d182b 100644 --- a/components/digital_ocean/README.md +++ b/components/digital_ocean/README.md @@ -1,13 +1,11 @@ # Overview -You can use the Digital Ocean API to manage Droplets (virtual private servers), -resources within those Droplets, and other account-wide resources like domains -and images. +The Digital Ocean API provides programmatic access to manage Digital Ocean resources such as Droplets, Spaces, and Databases. With Pipedream, you can harness this API to automate a variety of tasks, like spinning up new servers, scaling resources, or integrating cloud infrastructure management into your workflow. It's a powerful tool for DevOps automation, allowing for the dynamic management of infrastructure in response to events or schedules. -Examples of what you can build using the Digital Ocean API include: +# Example Use Cases -- A script to create and destroy Droplets on demand -- A script to snapshot (back up) your Droplets on a regular basis -- A script to add and remove SSH keys from your account -- A script to manage domain records (DNS) -- A script to add and remove Floating IPs from your account +- **Automated Environment Setup**: Trigger a workflow on Pipedream to create a new Droplet when a new feature branch is created in your GitHub repository. This can include cloning the repo to the server, installing necessary dependencies, and setting up a staging URL. + +- **Resource Scaling Based on Load**: Monitor your application's performance metrics and use the Digital Ocean API to scale your Droplet sizes up or down based on the CPU or memory usage. This workflow could be triggered by a monitoring tool like Datadog, which sends an alert to Pipedream when resource usage hits certain thresholds. + +- **Backup Automation**: Schedule daily or weekly backups of your Digital Ocean volumes or databases to Spaces. Use Pipedream's cron job feature to trigger backups and notify your team via Slack or email upon completion or if any errors occur. diff --git a/components/digitalocean_spaces/README.md b/components/digitalocean_spaces/README.md new file mode 100644 index 0000000000000..6816d28af8901 --- /dev/null +++ b/components/digitalocean_spaces/README.md @@ -0,0 +1,11 @@ +# Overview + +DigitalOcean Spaces API permits you to manage object storage, allowing for the storage and serving of massive amounts of data. This API is great for backing up, archiving, and providing public access to data or assets. On Pipedream, you can use this API to automate file operations like uploads, downloads, and deletions, as well as manage permissions and metadata. You can integrate it with other services for end-to-end workflow automation. + +# Example Use Cases + +- **Automated Backup to DigitalOcean Spaces**: Trigger a workflow on a schedule to back up important files from your server to a Space, ensuring data is always safe and versioned. + +- **Media File Processing and Storage**: When a new image or video is uploaded to a Space, trigger a workflow that utilizes a service like AWS Lambda to process the media then update the metadata in the Space. + +- **Static Website Content Deployment**: Upon pushing new content to a GitHub repository, automatically deploy the changes to your DigitalOcean Space where your static site content is hosted for seamless updates. diff --git a/components/digitalocean_spaces/actions/delete-files/delete-files.mjs b/components/digitalocean_spaces/actions/delete-files/delete-files.mjs index 28fb083d8a8d2..0f43d05bb6433 100644 --- a/components/digitalocean_spaces/actions/delete-files/delete-files.mjs +++ b/components/digitalocean_spaces/actions/delete-files/delete-files.mjs @@ -6,7 +6,7 @@ export default { key: "digitalocean_spaces-delete-files", name: "Delete Files", description: "Delete files in a bucket. [See the docs](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/classes/deleteobjectscommand.html).", - version: "0.0.1", + version: "0.0.2", props: { ...common.props, files: { diff --git a/components/digitalocean_spaces/actions/list-files/list-files.mjs b/components/digitalocean_spaces/actions/list-files/list-files.mjs index fd0aa2aef64fe..a0537ae696d07 100644 --- a/components/digitalocean_spaces/actions/list-files/list-files.mjs +++ b/components/digitalocean_spaces/actions/list-files/list-files.mjs @@ -6,7 +6,7 @@ export default { key: "digitalocean_spaces-list-files", name: "List Files", description: "List files in a bucket. [See the docs](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/classes/listobjectsv2command.html).", - version: "0.0.1", + version: "0.0.2", props: { ...common.props, prefix: { diff --git a/components/digitalocean_spaces/actions/upload-file-base64/upload-file-base64.mjs b/components/digitalocean_spaces/actions/upload-file-base64/upload-file-base64.mjs index 9196961a3792c..826a76c81b70d 100644 --- a/components/digitalocean_spaces/actions/upload-file-base64/upload-file-base64.mjs +++ b/components/digitalocean_spaces/actions/upload-file-base64/upload-file-base64.mjs @@ -1,5 +1,5 @@ import common from "../../common/common-s3.mjs"; -import base from "../../../aws/actions/s3-upload-file/s3-upload-file.mjs"; +import base from "@pipedream/aws/actions/s3-upload-file/s3-upload-file.mjs"; export default { ...common, @@ -8,10 +8,38 @@ export default { key: "digitalocean_spaces-upload-file-base64", name: "Upload File Base64", description: "Accepts a base64-encoded string and a filename, then uploads as a file to DigitalOcean Spaces. [See the docs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/upload-objects.html).", - version: "0.0.1", + version: "0.0.2", props: { ...common.props, filename: base.props.filename, data: base.props.data, }, + async run({ $ }) { + const { + uploadFile, + bucket, + filename, + data, + acl, + contentType, + metadata, + } = this; + + const response = await uploadFile({ + Bucket: bucket, + Key: filename, + Body: Buffer.from(data, "base64"), + ...(acl && { + ACL: acl, + }), + ...(contentType && { + ContentType: contentType, + }), + ...(metadata && { + Metadata: metadata, + }), + }); + $.export("$summary", `Uploaded file ${filename} to S3`); + return response; + }, }; diff --git a/components/digitalocean_spaces/actions/upload-file-tmp/upload-file-tmp.mjs b/components/digitalocean_spaces/actions/upload-file-tmp/upload-file-tmp.mjs index 9336e37c3cfce..4510201ca6d8d 100644 --- a/components/digitalocean_spaces/actions/upload-file-tmp/upload-file-tmp.mjs +++ b/components/digitalocean_spaces/actions/upload-file-tmp/upload-file-tmp.mjs @@ -1,5 +1,7 @@ +import fs from "fs"; +import { ConfigurationError } from "@pipedream/platform"; +import base from "@pipedream/aws/actions/s3-upload-file-tmp/s3-upload-file-tmp.mjs"; import common from "../../common/common-s3.mjs"; -import base from "../../../aws/actions/s3-upload-file-tmp/s3-upload-file-tmp.mjs"; export default { ...common, @@ -8,10 +10,48 @@ export default { key: "digitalocean_spaces-upload-file-tmp", name: "Upload File /tmp", description: "Accepts a file path starting from /tmp, then uploads as a file to DigitalOcean Spaces. [See the docs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/upload-objects.html).", - version: "0.0.1", + version: "0.0.2", props: { ...common.props, - filename: base.props.filename, + customFilename: base.props.customFilename, path: base.props.path, }, + methods: { + ...base.methods, + async uploadSingleFile($) { + const { + uploadFile, + bucket, + path, + customFilename, + acl, + contentType, + metadata, + } = this; + + if (!path) { + throw new ConfigurationError("File Path is required"); + } + const file = fs.readFileSync(path, { + encoding: "base64", + }); + const filename = customFilename || path.split("/").pop(); + const response = await uploadFile({ + Bucket: bucket, + Key: filename, + Body: Buffer.from(file, "base64"), + ...(acl && { + ACL: acl, + }), + ...(contentType && { + ContentType: contentType, + }), + ...(metadata && { + Metadata: metadata, + }), + }); + $.export("$summary", `Uploaded file ${filename} to S3`); + return response; + }, + }, }; diff --git a/components/digitalocean_spaces/actions/upload-file-url/upload-file-url.mjs b/components/digitalocean_spaces/actions/upload-file-url/upload-file-url.mjs index d57f2a9996509..f0eb0a4aaa636 100644 --- a/components/digitalocean_spaces/actions/upload-file-url/upload-file-url.mjs +++ b/components/digitalocean_spaces/actions/upload-file-url/upload-file-url.mjs @@ -1,5 +1,7 @@ +import fs from "fs"; +import base from "@pipedream/aws/actions/s3-upload-file-url/s3-upload-file-url.mjs"; +import downloadFileToTmp from "@pipedream/helper_functions/actions/download-file-to-tmp/download-file-to-tmp.mjs"; import common from "../../common/common-s3.mjs"; -import base from "../../../aws/actions/s3-upload-file-url/s3-upload-file-url.mjs"; export default { ...common, @@ -8,15 +10,46 @@ export default { key: "digitalocean_spaces-upload-file-url", name: "Upload File URL", description: "Accepts a download link and a filename, downloads it, then uploads to DigitalOcean Spaces. [See the docs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/upload-objects.html).", - version: "0.0.1", + version: "0.0.2", props: { ...common.props, url: base.props.url, filename: base.props.filename, }, async run({ $ }) { - return base.run.bind(this)({ + const { + uploadFile, + bucket, + acl, + contentType, + metadata, + } = this; + + const filedata = await downloadFileToTmp.run.bind(this)({ $, }); + + const filename = filedata[0]; + const filepath = filedata[1]; + const file = fs.readFileSync(filepath, { + encoding: "base64", + }); + + const response = await uploadFile({ + Bucket: bucket, + Key: filename, + Body: Buffer.from(file, "base64"), + ...(acl && { + ACL: acl, + }), + ...(contentType && { + ContentType: contentType, + }), + ...(metadata && { + Metadata: metadata, + }), + }); + $.export("$summary", `Uploaded file ${filename} to S3`); + return response; }, }; diff --git a/components/digitalocean_spaces/common/common-s3.mjs b/components/digitalocean_spaces/common/common-s3.mjs index 9a1fa0e89529a..cdd262d151264 100644 --- a/components/digitalocean_spaces/common/common-s3.mjs +++ b/components/digitalocean_spaces/common/common-s3.mjs @@ -1,10 +1,28 @@ import app from "../digitalocean_spaces.app.mjs"; -import pipedreamS3 from "../../aws/common/common-s3.mjs"; +import pipedreamS3 from "@pipedream/aws/common/common-s3.mjs"; export default { ...pipedreamS3, props: { aws: app, bucket: pipedreamS3.props.bucket, + acl: { + propDefinition: [ + app, + "acl", + ], + }, + contentType: { + propDefinition: [ + app, + "contentType", + ], + }, + metadata: { + propDefinition: [ + app, + "metadata", + ], + }, }, }; diff --git a/components/digitalocean_spaces/digitalocean_spaces.app.mjs b/components/digitalocean_spaces/digitalocean_spaces.app.mjs index 4d3747e2a5891..cc00ffe571140 100644 --- a/components/digitalocean_spaces/digitalocean_spaces.app.mjs +++ b/components/digitalocean_spaces/digitalocean_spaces.app.mjs @@ -25,6 +25,33 @@ export default { description: "Limits the response to keys that begin with the specified prefix", optional: true, }, + acl: { + type: "string", + label: "ACL", + description: "The canned ACL to apply to the object", + options: [ + "private", + "public-read", + "public-read-write", + "authenticated-read", + "aws-exec-read", + "bucket-owner-read", + "bucket-owner-full-control", + ], + optional: true, + }, + contentType: { + type: "string", + label: "Content Type", + description: "A standard MIME type describing the format of the object data. Eg. `text/plain`", + optional: true, + }, + metadata: { + type: "object", + label: "Metadata", + description: "A map of metadata to store with the object in S3.", + optional: true, + }, }, methods: { getAWSClient(clientType) { diff --git a/components/digitalocean_spaces/package.json b/components/digitalocean_spaces/package.json index 23690c6783624..26ad550e4c1a7 100644 --- a/components/digitalocean_spaces/package.json +++ b/components/digitalocean_spaces/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/digitalocean_spaces", - "version": "0.0.3", + "version": "0.0.4", "description": "Pipedream DigitalOcean Spaces Components", "main": "digitalocean_spaces.app.mjs", "keywords": [ @@ -14,7 +14,9 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.231.0", + "@pipedream/aws": "^0.7.0", "@pipedream/helper_functions": "^0.3.8", - "@pipedream/platform": "^1.2.1" + "@pipedream/platform": "^1.2.1", + "fs": "^0.0.1-security" } } diff --git a/components/digitalocean_spaces/sources/file-deleted/file-deleted.mjs b/components/digitalocean_spaces/sources/file-deleted/file-deleted.mjs index 1f41f347a0b55..c99c0e0529809 100644 --- a/components/digitalocean_spaces/sources/file-deleted/file-deleted.mjs +++ b/components/digitalocean_spaces/sources/file-deleted/file-deleted.mjs @@ -5,7 +5,7 @@ export default { key: "digitalocean_spaces-file-deleted", name: "File Deleted", description: "Emit new event when a file is deleted from a DigitalOcean Spaces bucket", - version: "0.0.1", + version: "0.0.2", type: "source", hooks: { async deploy() { diff --git a/components/digitalocean_spaces/sources/file-uploaded/file-uploaded.mjs b/components/digitalocean_spaces/sources/file-uploaded/file-uploaded.mjs index 9239ee84f639b..73c4a4aa28a1d 100644 --- a/components/digitalocean_spaces/sources/file-uploaded/file-uploaded.mjs +++ b/components/digitalocean_spaces/sources/file-uploaded/file-uploaded.mjs @@ -5,7 +5,7 @@ export default { key: "digitalocean_spaces-file-uploaded", name: "New File Uploaded", description: "Emit new event when a file is uploaded to a DigitalOcean Spaces bucket", - version: "0.0.1", + version: "0.0.2", type: "source", methods: { ...base.methods, diff --git a/components/digitalriver/README.md b/components/digitalriver/README.md new file mode 100644 index 0000000000000..6929a4fb0a484 --- /dev/null +++ b/components/digitalriver/README.md @@ -0,0 +1,11 @@ +# Overview + +The DigitalRiver API lets you manage e-commerce activities like orders, payments, and customer information. On Pipedream, you can harness this API to create automated workflows that integrate with other apps, react to events, process transactions, and handle global e-commerce complexities. Pipedream's serverless platform enables you to build and execute these workflows quickly, without setting up infrastructure, and to connect DigitalRiver with countless other services for a seamless e-commerce ecosystem. + +# Example Use Cases + +- **Automate Order Fulfillment**: When a new order is placed in DigitalRiver, trigger a workflow in Pipedream to send order details to a fulfillment service like ShipStation or a custom inventory management system to ensure timely delivery. + +- **Sync Customer Data**: After a transaction is completed on DigitalRiver, use Pipedream to sync customer information with a CRM tool like Salesforce or HubSpot, keeping all customer interactions and history up to date for enhanced customer service and targeted marketing. + +- **Handle Refunds and Returns**: Set up a Pipedream workflow that listens for DigitalRiver refund events and then processes them automatically, updating accounting records in QuickBooks and notifying customer service teams to handle any follow-up actions required. diff --git a/components/dingconnect/README.md b/components/dingconnect/README.md new file mode 100644 index 0000000000000..2742c5dbb3b70 --- /dev/null +++ b/components/dingconnect/README.md @@ -0,0 +1,11 @@ +# Overview + +DingConnect API provides a platform for connecting with mobile operators globally, allowing you to recharge phones or send airtime to anyone, anywhere. Within Pipedream, you can harness this API to automate mobile top-ups, manage your DingConnect account, and integrate with other services for comprehensive workflows. The API can be particularly powerful when combined with platforms that require notifications, confirmations, or financial transactions. + +# Example Use Cases + +- **Automated Customer Rewards**: Trigger a workflow that sends mobile top-ups through DingConnect as rewards when customers reach certain milestones on your platform. This can help increase customer loyalty and engagement. + +- **E-commerce Order Confirmation**: Set up a Pipedream workflow that sends a small mobile credit via DingConnect as a thank-you gift every time a customer completes a purchase. This could be tied in with Shopify or WooCommerce for an e-commerce store. + +- **Alert System for Service Uptime**: Integrate DingConnect with monitoring tools like UptimeRobot. Whenever your service goes down, you could use Pipedream to automatically send a notification via mobile top-up with a predefined message to the responsible personnel to take immediate action. diff --git a/components/dingconnect/actions/estimate-prices/estimate-prices.mjs b/components/dingconnect/actions/estimate-prices/estimate-prices.mjs new file mode 100644 index 0000000000000..db749c8e7e9ed --- /dev/null +++ b/components/dingconnect/actions/estimate-prices/estimate-prices.mjs @@ -0,0 +1,46 @@ +import app from "../../dingconnect.app.mjs"; + +export default { + key: "dingconnect-estimate-prices", + name: "Estimate Prices", + description: "Estimates prices send values using the DingConnect API. [See the documentation](https://www.dingconnect.com/api#operation/estimateprices)", + version: "0.0.1", + type: "action", + props: { + app, + skuCode: { + propDefinition: [ + app, + "skuCode", + ], + }, + sendValue: { + propDefinition: [ + app, + "sendValue", + ], + }, + batchItemRef: { + propDefinition: [ + app, + "batchItemRef", + ], + }, + }, + async run({ $ }) { + const response = await this.app.estimatePrices({ + $, + data: [ + { + sendValue: this.sendValue, + skuCode: this.skuCode, + BatchItemRef: this.batchItemRef, + }, + ], + }); + + $.export("$summary", "Successfully retrieved the estimated price"); + + return response; + }, +}; diff --git a/components/dingconnect/actions/get-balance/get-balance.mjs b/components/dingconnect/actions/get-balance/get-balance.mjs new file mode 100644 index 0000000000000..d16cf0f3d8a42 --- /dev/null +++ b/components/dingconnect/actions/get-balance/get-balance.mjs @@ -0,0 +1,21 @@ +import app from "../../dingconnect.app.mjs"; + +export default { + key: "dingconnect-get-balance", + name: "Get Balance", + description: "Get the current agent balance from DingConnect. [See the documentation](https://www.dingconnect.com/api#operation/getbalance)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.getBalance({ + $, + }); + + $.export("$summary", "Successfully retrieved the current agent balance"); + + return response; + }, +}; diff --git a/components/dingconnect/actions/get-products/get-products.mjs b/components/dingconnect/actions/get-products/get-products.mjs new file mode 100644 index 0000000000000..bb141e4d13d08 --- /dev/null +++ b/components/dingconnect/actions/get-products/get-products.mjs @@ -0,0 +1,21 @@ +import app from "../../dingconnect.app.mjs"; + +export default { + key: "dingconnect-get-products", + name: "Get Products", + description: "Retrieves a list of products from DingConnect. [See the documentation](https://www.dingconnect.com/api#operation/getproducts)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.getProducts({ + $, + }); + + $.export("$summary", `Successfully retrieved ${response.Items.length} products`); + + return response; + }, +}; diff --git a/components/dingconnect/dingconnect.app.mjs b/components/dingconnect/dingconnect.app.mjs index 14a8f83b0399f..967d3b01456a8 100644 --- a/components/dingconnect/dingconnect.app.mjs +++ b/components/dingconnect/dingconnect.app.mjs @@ -1,11 +1,71 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "dingconnect", - propDefinitions: {}, + propDefinitions: { + skuCode: { + type: "string", + label: "SKU Code", + description: "The unique identifier for the product", + async options() { + const response = await this.getProducts({}); + const products = response.Items; + return products.map(({ SkuCode }) => ({ + value: SkuCode, + })); + }, + }, + sendValue: { + type: "string", + label: "Send Value", + description: "The monetary value to send, i.e `10.5`", + }, + batchItemRef: { + type: "string", + label: "Item Reference", + description: "A unique reference for an item", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://www.dingconnect.com/api/V1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + async getProducts(args = {}) { + return this._makeRequest({ + path: "/GetProducts", + ...args, + }); + }, + async estimatePrices(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/EstimatePrices", + ...args, + }); + }, + async getBalance(args = {}) { + return this._makeRequest({ + path: "/GetBalance", + ...args, + }); }, }, }; diff --git a/components/dingconnect/package.json b/components/dingconnect/package.json index 3397eb89f574d..d386bafd5e63f 100644 --- a/components/dingconnect/package.json +++ b/components/dingconnect/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/dingconnect", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream DingConnect Components", "main": "dingconnect.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" } -} \ No newline at end of file +} diff --git a/components/directus/README.md b/components/directus/README.md index bd1229bbdc31f..335433f785fa2 100644 --- a/components/directus/README.md +++ b/components/directus/README.md @@ -1,13 +1,11 @@ # Overview -Directus is a headless CMS that gives you all the power of a database without -any of the complexities. With Directus, you can easily build custom -applications without having to worry about over-complicating your database. +Directus is an open-source headless CMS with a rich API that lets you manage content dynamically and distribute it across various channels. Harness Directus API on Pipedream to automate content operations, streamline workflows, and integrate with countless other apps. With real-time capabilities, you can trigger actions on content changes, automate publishing, sync with external databases, and more. Pipedream's serverless platform provides a powerful, flexible way to build custom automations using Directus, without writing complex infrastructural code. -Here are a few examples of what you can build with the Directus API: +# Example Use Cases -- A custom content management system (CMS) -- A custom eCommerce platform -- A custom social media platform -- A custom CRM system -- A custom data management system +- **Content Sync Across Platforms**: Automate content synchronization between Directus and other Content Management Systems like WordPress. When a new item is added or updated in Directus, use Pipedream to push those changes to your WordPress site in real-time, keeping all your content platforms up to date. + +- **Automated Backup Workflow**: Set up a workflow that periodically backs up your Directus database to cloud storage services like Google Drive or Dropbox. This ensures you have regular snapshots of your content, safeguarding against data loss and giving you peace of mind. + +- **Dynamic Email Campaigns**: Leverage Directus' content updates to trigger personalized email campaigns with an email platform like SendGrid. When a piece of content is published in Directus, Pipedream can kick off an email blast to subscribers with the new content, keeping your audience engaged and informed. diff --git a/components/directus/package.json b/components/directus/package.json new file mode 100644 index 0000000000000..49a798b1ff0ee --- /dev/null +++ b/components/directus/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/directus", + "version": "0.6.0", + "description": "Pipedream directus Components", + "main": "directus.app.mjs", + "keywords": [ + "pipedream", + "directus" + ], + "homepage": "https://pipedream.com/apps/directus", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/discogs/README.md b/components/discogs/README.md new file mode 100644 index 0000000000000..fb582e7cd9c7e --- /dev/null +++ b/components/discogs/README.md @@ -0,0 +1,14 @@ +# Overview + +The Discogs API offers a gateway to rich data on music artists, labels, and releases. With Pipedream, you can harness this data to create automated workflows that can tap into a vast repository of music information. Whether you're looking to sync your music collection with a personal database, track marketplace activity, or curate personalized music recommendations, the Discogs API paired with Pipedream provides a streamlined platform for integrating and automating these processes. + +# Example Use Cases + +- **Sync Discogs Marketplace Listings to Google Sheets** + Track new listings on the Discogs Marketplace in real-time by creating a workflow that triggers when new items are listed. Automatically add these details to a Google Sheet for easy monitoring and analysis. + +- **Notify When a Rare Record is Listed** + Set up a Pipedream workflow to alert you via email or SMS when a record on your wishlist or a rare vinyl is listed for sale. Use Pipedream's built-in connectivity to filter for condition, price, or seller rating to make sure you only get notifications for listings that meet your criteria. + +- **Automate Collection Curation** + Build a workflow that automatically adds new music recommendations to your Discogs wantlist based on your listening habits on Spotify. Trigger the workflow when a new track is added to a playlist or your favorite songs, then use the Discogs API to search for the release and update your wantlist. diff --git a/components/discord/README.md b/components/discord/README.md index a7673828b16c8..a8881ea419228 100644 --- a/components/discord/README.md +++ b/components/discord/README.md @@ -1,6 +1,6 @@ # Overview -The Pipedream Discord app enables you to build event-driven workflows that interact with the Discord API. When you authorize the Pipedream app's access to your guilds, you can use [Pipedream workflows](/workflows/) to perform common Discord [actions](#workflow-actions), or [write your own code](/code/) against the Discord API. +The Discord API interacts seamlessly with Pipedream, empowering you to craft customized automations and workflows for your Discord server. With this powerful integration, you can automate tasks like message posting, user management, and notifications, based on a myriad of triggers and actions from different apps. These automations can enhance the Discord experience for community moderators and members, by synchronizing with external tools, organizing community engagement, and streamlining notifications. # Getting Started @@ -31,11 +31,20 @@ Using the Discord Bot integration will use a custom bot instead, with it's own n 1. [Create a new workflow](https://pipedream.com/new). 2. Select your trigger (HTTP, Cron, etc.). -3. Click on the **+** button below the trigger step, and search for "Slack". +3. Click on the **+** button below the trigger step, and search for "Discord". 4. Select the **Send a Message** action. 5. Click the **Connect Account** button near the top of the step. This will prompt you to select any existing Discord accounts you've previously authenticated with Pipedream, or you can select a **New** account. Clicking **New** opens a new window asking you to allow Pipedream access to your Discord account. Choose the right guilde where you'd like to install the app, then click **Allow**. 6. That's it! You can now connect to the Discord API using any of the Discord actions within a Pipedream workflow. +# Example Use Cases + +- **Automate Welcome Messages**: Trigger a workflow on Pipedream when a new user joins your Discord server to send them a personalized welcome message. Enhance this by adding a step to tag the user in a welcome channel or send them a DM with server rules and helpful resources. + +- **Sync Discord Roles with External Databases**: Manage Discord roles by syncing with user data from an external database or Google Sheets. When a user's status changes in the sheet—like upgrading to a paid tier of your service—use Pipedream to automatically update their role in Discord to reflect their new privileges. + +- **Content Alerts from Social Media**: Monitor social media platforms like Twitter or YouTube using their respective APIs on Pipedream. When a new tweet or video is posted, trigger a workflow that shares this content in a dedicated Discord channel, keeping your community engaged and informed about the latest updates. + + # Troubleshooting If you have issues with this integration, please join our public Slack and ask for help. diff --git a/components/discord/actions/common/common.mjs b/components/discord/actions/common/common.mjs index 2dd86ab2a749e..edf36b8e7b75a 100644 --- a/components/discord/actions/common/common.mjs +++ b/components/discord/actions/common/common.mjs @@ -1,4 +1,5 @@ import discord from "../../discord.app.mjs"; +import constants from "./constants.mjs"; export default { props: { @@ -37,6 +38,12 @@ export default { "includeSentViaPipedream", ], }, + suppressNotifications: { + propDefinition: [ + discord, + "suppressNotifications", + ], + }, }, methods: { getUserInputProps(omit = [ @@ -61,5 +68,10 @@ export default { const workflowId = process.env.PIPEDREAM_WORKFLOW_ID; return `Sent via [Pipedream]()`; }, + getMessageFlags(suppressNotifications) { + let flags = 0; + if (suppressNotifications) flags += constants.SUPPRESS_NOTIFICATIONS; + return flags; + }, }, }; diff --git a/components/discord/actions/common/constants.mjs b/components/discord/actions/common/constants.mjs new file mode 100644 index 0000000000000..fd82bfbaf7811 --- /dev/null +++ b/components/discord/actions/common/constants.mjs @@ -0,0 +1,6 @@ +// sendMessage flags constants +const SUPPRESS_NOTIFICATIONS = 4096; + +export default { + SUPPRESS_NOTIFICATIONS, +}; diff --git a/components/discord/actions/send-message-advanced/send-message-advanced.mjs b/components/discord/actions/send-message-advanced/send-message-advanced.mjs index 4c553983c7bca..75cda2917d440 100644 --- a/components/discord/actions/send-message-advanced/send-message-advanced.mjs +++ b/components/discord/actions/send-message-advanced/send-message-advanced.mjs @@ -6,7 +6,7 @@ export default { key: "discord-send-message-advanced", name: "Send Message (Advanced)", description: "Send a simple or structured message (using embeds) to a Discord channel", - version: "1.0.1", + version: "1.0.2", type: "action", props: { ...common.props, @@ -31,6 +31,7 @@ export default { threadID, username, includeSentViaPipedream, + suppressNotifications, embeds: embedsProp, } = this; const embeds = embedsProp; @@ -55,6 +56,7 @@ export default { const resp = await this.discord.sendMessage(this.channel, { avatar_url: avatarURL, username, + flags: this.getMessageFlags(suppressNotifications), embeds, content, }, { diff --git a/components/discord/actions/send-message-with-file/send-message-with-file.mjs b/components/discord/actions/send-message-with-file/send-message-with-file.mjs index 3b1ec679f847b..65f6ce82fac05 100644 --- a/components/discord/actions/send-message-with-file/send-message-with-file.mjs +++ b/components/discord/actions/send-message-with-file/send-message-with-file.mjs @@ -8,7 +8,7 @@ export default { key: "discord-send-message-with-file", name: "Send Message With File", description: "Post a message with an attached file", - version: "1.1.0", + version: "1.1.1", type: "action", props: { ...common.props, @@ -41,6 +41,7 @@ export default { fileUrl, filePath, includeSentViaPipedream, + suppressNotifications, } = this; if (!fileUrl && !filePath) { @@ -60,6 +61,7 @@ export default { avatar_url: avatarURL, username, file, + flags: this.getMessageFlags(suppressNotifications), content: includeSentViaPipedream ? this.appendPipedreamText(message ?? "") : message, diff --git a/components/discord/actions/send-message/send-message.mjs b/components/discord/actions/send-message/send-message.mjs index edceb9dea05e6..f30742084ecf7 100644 --- a/components/discord/actions/send-message/send-message.mjs +++ b/components/discord/actions/send-message/send-message.mjs @@ -5,7 +5,7 @@ export default { key: "discord-send-message", name: "Send Message", description: "Send a simple message to a Discord channel", - version: "1.0.1", + version: "1.0.2", type: "action", async run({ $ }) { const { @@ -14,12 +14,13 @@ export default { threadID, username, includeSentViaPipedream, + suppressNotifications, } = this; - try { const resp = await this.discord.sendMessage(this.channel, { avatar_url: avatarURL, username, + flags: this.getMessageFlags(suppressNotifications), content: includeSentViaPipedream ? this.appendPipedreamText(message) : message, diff --git a/components/discord/discord.app.mjs b/components/discord/discord.app.mjs index 40a0770db6e77..eeda121d54bde 100644 --- a/components/discord/discord.app.mjs +++ b/components/discord/discord.app.mjs @@ -50,5 +50,12 @@ export default { description: "The path to the file, e.g. `/tmp/myFile.csv`. Must specify either **File URL** or **File Path**.", optional: true, }, + suppressNotifications: { + type: "boolean", + optional: true, + default: false, + label: "Suppress notifications", + description: "Defaults to `false`, sends the message as silent, triggers no notifications to clients.", + }, }, }; diff --git a/components/discord/package.json b/components/discord/package.json index e7380d942c1eb..8e8ec3371b708 100644 --- a/components/discord/package.json +++ b/components/discord/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/discord", - "version": "1.2.3", + "version": "1.2.4", "description": "Pipedream Discord Components", "main": "discord.app.mjs", "keywords": [ diff --git a/components/discord/sources/message-deleted/message-deleted.mjs b/components/discord/sources/message-deleted/message-deleted.mjs index 5a8885bc44471..ace0d9e941f61 100644 --- a/components/discord/sources/message-deleted/message-deleted.mjs +++ b/components/discord/sources/message-deleted/message-deleted.mjs @@ -4,7 +4,7 @@ export default { key: "discord-message-deleted", name: "Message Deleted (Instant)", description: "Emit new event for each message deleted", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", type: "source", props: { diff --git a/components/discord/sources/new-command-received/new-command-received.mjs b/components/discord/sources/new-command-received/new-command-received.mjs index 3ac09982ffafa..caa85d199c823 100644 --- a/components/discord/sources/new-command-received/new-command-received.mjs +++ b/components/discord/sources/new-command-received/new-command-received.mjs @@ -6,7 +6,7 @@ export default { key: "discord-new-command-received", name: "New Command Received (Instant)", description: "Emit new event for each command posted to one or more channels in a Discord server", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", props: { discord, diff --git a/components/discord/sources/new-guild-member/new-guild-member.mjs b/components/discord/sources/new-guild-member/new-guild-member.mjs index ba5f20df27b8a..91fccc1707377 100644 --- a/components/discord/sources/new-guild-member/new-guild-member.mjs +++ b/components/discord/sources/new-guild-member/new-guild-member.mjs @@ -1,10 +1,10 @@ import discord from "../../discord.app.mjs"; export default { - key: "discord-guild-member", + key: "discord-new-guild-member", name: "New Guild Member (Instant)", description: "Emit new event for each new member added to a guild", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", type: "source", props: { diff --git a/components/discord/sources/new-message/new-message.mjs b/components/discord/sources/new-message/new-message.mjs index 9e30121155067..4110ece097d74 100644 --- a/components/discord/sources/new-message/new-message.mjs +++ b/components/discord/sources/new-message/new-message.mjs @@ -6,7 +6,7 @@ export default { key: "discord-new-message", name: "New Message (Instant)", description: "Emit new event for each message posted to one or more channels in a Discord server", - version: "1.0.2", + version: "1.0.3", dedupe: "unique", props: { diff --git a/components/discord/sources/reaction-added/reaction-added.mjs b/components/discord/sources/reaction-added/reaction-added.mjs index ef832457dc3f8..494e5f9db97c0 100644 --- a/components/discord/sources/reaction-added/reaction-added.mjs +++ b/components/discord/sources/reaction-added/reaction-added.mjs @@ -4,7 +4,7 @@ export default { key: "discord-reaction-added", name: "Reaction Added (Instant)", description: "Emit new event for each reaction added to a message", - version: "0.0.2", + version: "0.0.3", type: "source", props: { discord, diff --git a/components/discord_bot/README.md b/components/discord_bot/README.md index 11c64b6882ad0..7fd043a583e3e 100644 --- a/components/discord_bot/README.md +++ b/components/discord_bot/README.md @@ -1,6 +1,6 @@ # Overview -The Pipedream Discord app enables you to build event-driven workflows that interact with the Discord API. When you authorize the Pipedream app's access to your guilds, you can use [Pipedream workflows](/workflows/) to perform common Discord [actions](#workflow-actions), or [write your own code](/code/) against the Discord API. +The Discord Bot API unlocks the power to interact with Discord users and channels programmatically, making it possible to automate messages, manage servers, and integrate with other services. With Pipedream's serverless platform, you can create complex workflows that respond to events in Discord, process data, and trigger actions in other apps. This opens up opportunities for community engagement, content moderation, analytics, and more, without the overhead of managing infrastructure. # Getting Started @@ -57,6 +57,14 @@ At the time of writing, Discord only requires bots in 100 or more servers to app You can request for this approval in the **Discord Developer Portal**. +# Example Use Cases + +- **Automated Moderation Workflow**: Trigger a Pipedream workflow whenever a message is posted in a Discord channel. Use sentiment analysis to detect negative content and automatically remove messages or alert moderators. Integrate with Slack to send real-time notifications to a moderation team. + +- **Streamlined Community Engagement**: Send automated welcome messages to new Discord server members using Pipedream. Sync these new member details to a CRM like Salesforce or Airtable to maintain an organized community member database, and trigger an email sequence with SendGrid to onboard them. + +- **Event-Driven Notifications**: Configure a workflow that listens for specific keywords or commands in a Discord channel. When detected, use Pipedream to fetch data from an external API, such as Jira or GitHub, and post updates back to the channel to keep your community informed on project statuses or issue resolutions. + # Troubleshooting Make sure your bot has the required privileges: diff --git a/components/discord_bot/actions/add-role/add-role.mjs b/components/discord_bot/actions/add-role/add-role.mjs index 7c86e13562a8f..db2598dfc26e9 100644 --- a/components/discord_bot/actions/add-role/add-role.mjs +++ b/components/discord_bot/actions/add-role/add-role.mjs @@ -8,7 +8,7 @@ export default { name: "Add Role", description: "Assign a role to a user. Remember that your bot requires the `MANAGE_ROLES` permission. [See the docs here](https://discord.com/developers/docs/resources/guild#add-guild-member-role)", type: "action", - version: "0.0.11", + version: "0.0.12", props: { ...common.props, userId: { diff --git a/components/discord_bot/actions/change-nickname/change-nickname.mjs b/components/discord_bot/actions/change-nickname/change-nickname.mjs index 3e56ea260d1fe..39a6786df6bbd 100644 --- a/components/discord_bot/actions/change-nickname/change-nickname.mjs +++ b/components/discord_bot/actions/change-nickname/change-nickname.mjs @@ -8,7 +8,7 @@ export default { name: "Change Nickname", description: "Modifies the nickname of the current user in a guild.", type: "action", - version: "0.0.8", + version: "0.0.9", props: { ...common.props, nick: { diff --git a/components/discord_bot/actions/create-channel-invite/create-channel-invite.mjs b/components/discord_bot/actions/create-channel-invite/create-channel-invite.mjs index 37fdb7a8e6919..854f825ae5e15 100644 --- a/components/discord_bot/actions/create-channel-invite/create-channel-invite.mjs +++ b/components/discord_bot/actions/create-channel-invite/create-channel-invite.mjs @@ -7,7 +7,7 @@ export default { name: "Create Channel Invite", description: "Create a new invite for the channel. [See the docs here](https://discord.com/developers/docs/resources/channel#create-channel-invite)", type: "action", - version: "0.0.11", + version: "0.0.12", props: { ...common.props, maxAge: { diff --git a/components/discord_bot/actions/create-guild-channel/create-guild-channel.mjs b/components/discord_bot/actions/create-guild-channel/create-guild-channel.mjs index 0231043615b61..2fef98f9065d8 100644 --- a/components/discord_bot/actions/create-guild-channel/create-guild-channel.mjs +++ b/components/discord_bot/actions/create-guild-channel/create-guild-channel.mjs @@ -9,7 +9,7 @@ export default { name: "Create Guild Channel", description: "Create a new channel for the guild. [See the docs here](https://discord.com/developers/docs/resources/guild#create-guild-channel)", type: "action", - version: "0.0.13", + version: "0.0.14", props: { ...common.props, name: { diff --git a/components/discord_bot/actions/delete-channel/delete-channel.mjs b/components/discord_bot/actions/delete-channel/delete-channel.mjs index 54499575ac8a9..5a648e3d8b8d3 100644 --- a/components/discord_bot/actions/delete-channel/delete-channel.mjs +++ b/components/discord_bot/actions/delete-channel/delete-channel.mjs @@ -6,7 +6,7 @@ export default { name: "Delete Channel", description: "Delete a Channel.", type: "action", - version: "0.0.11", + version: "0.0.12", async run({ $ }) { return this.discord.deleteChannel({ $, diff --git a/components/discord_bot/actions/delete-message/delete-message.mjs b/components/discord_bot/actions/delete-message/delete-message.mjs index 9ead8b861d0f0..63747def95b98 100644 --- a/components/discord_bot/actions/delete-message/delete-message.mjs +++ b/components/discord_bot/actions/delete-message/delete-message.mjs @@ -8,7 +8,7 @@ export default { name: "Delete message", description: "Delete a message. [See the docs here](https://discord.com/developers/docs/resources/channel#delete-message)", type: "action", - version: "0.0.11", + version: "0.0.12", props: { ...common.props, messageId: { diff --git a/components/discord_bot/actions/find-channel/find-channel.mjs b/components/discord_bot/actions/find-channel/find-channel.mjs index 2c12d413370ad..b4b9e65845832 100644 --- a/components/discord_bot/actions/find-channel/find-channel.mjs +++ b/components/discord_bot/actions/find-channel/find-channel.mjs @@ -8,7 +8,7 @@ export default { name: "Find Channel", description: "Find an existing channel by name. [See the docs here](https://discord.com/developers/docs/resources/guild#get-guild-channels)", type: "action", - version: "0.0.11", + version: "0.0.12", props: { ...common.props, channelName: { diff --git a/components/discord_bot/actions/find-user/find-user.mjs b/components/discord_bot/actions/find-user/find-user.mjs index 309fa2059f408..0386ccb6a438f 100644 --- a/components/discord_bot/actions/find-user/find-user.mjs +++ b/components/discord_bot/actions/find-user/find-user.mjs @@ -6,7 +6,7 @@ export default { name: "Find User", description: "Find an existing user by name. [See the docs here](https://discord.com/developers/docs/resources/guild#search-guild-members)", type: "action", - version: "0.0.12", + version: "0.0.13", props: { ...common.props, query: { diff --git a/components/discord_bot/actions/get-message/get-message.mjs b/components/discord_bot/actions/get-message/get-message.mjs index d6ef5893b4882..dd395d6b15025 100644 --- a/components/discord_bot/actions/get-message/get-message.mjs +++ b/components/discord_bot/actions/get-message/get-message.mjs @@ -8,7 +8,7 @@ export default { name: "Get message", description: "Return a specific message in a channel. [See the docs here](https://discord.com/developers/docs/resources/channel#get-channel-message)", type: "action", - version: "0.0.11", + version: "0.0.12", props: { ...common.props, messageId: { diff --git a/components/discord_bot/actions/list-channel-invites/list-channel-invites.mjs b/components/discord_bot/actions/list-channel-invites/list-channel-invites.mjs index 4240d786ae083..49f3f3d9e5edb 100644 --- a/components/discord_bot/actions/list-channel-invites/list-channel-invites.mjs +++ b/components/discord_bot/actions/list-channel-invites/list-channel-invites.mjs @@ -9,7 +9,7 @@ export default { name: "List Channel Invites", description: "Return a list of invitees for the channel. Only usable for guild channels.", type: "action", - version: "0.0.11", + version: "0.0.12", props: { ...common.props, channelId: { diff --git a/components/discord_bot/actions/list-channel-messages/list-channel-messages.mjs b/components/discord_bot/actions/list-channel-messages/list-channel-messages.mjs index 07e6c2f54c20b..8f51b7df92889 100644 --- a/components/discord_bot/actions/list-channel-messages/list-channel-messages.mjs +++ b/components/discord_bot/actions/list-channel-messages/list-channel-messages.mjs @@ -10,7 +10,7 @@ export default { name: "List Channel Messages", description: "Return the messages for a channel. [See the docs here](https://discord.com/developers/docs/resources/channel#get-channel-messages)", type: "action", - version: "0.0.12", + version: "0.0.13", props: { ...common.props, max: { diff --git a/components/discord_bot/actions/list-channels/list-channels.mjs b/components/discord_bot/actions/list-channels/list-channels.mjs index fe52c236ddb4f..33d3082313172 100644 --- a/components/discord_bot/actions/list-channels/list-channels.mjs +++ b/components/discord_bot/actions/list-channels/list-channels.mjs @@ -6,7 +6,7 @@ export default { name: "List Channels", description: "Return a list of channels. [See the docs here](https://discord.com/developers/docs/resources/guild#get-guild-channels)", type: "action", - version: "0.0.11", + version: "0.0.12", async run({ $ }) { return this.discord.getGuildChannels({ $, diff --git a/components/discord_bot/actions/list-guild-members/list-guild-members.mjs b/components/discord_bot/actions/list-guild-members/list-guild-members.mjs index 889f52de7f079..ec9ca6d4aa3e1 100644 --- a/components/discord_bot/actions/list-guild-members/list-guild-members.mjs +++ b/components/discord_bot/actions/list-guild-members/list-guild-members.mjs @@ -11,7 +11,7 @@ export default { name: "List Guild Members", description: "Return a list of guild members. [See the docs here](https://discord.com/developers/docs/resources/guild#list-guild-members)", type: "action", - version: "0.0.12", + version: "0.0.13", props: { discord, guildId: { diff --git a/components/discord_bot/actions/list-users-with-emoji-reactions/list-users-with-emoji-reactions.mjs b/components/discord_bot/actions/list-users-with-emoji-reactions/list-users-with-emoji-reactions.mjs index fcad0b2cd64b7..940dd4dc033a4 100644 --- a/components/discord_bot/actions/list-users-with-emoji-reactions/list-users-with-emoji-reactions.mjs +++ b/components/discord_bot/actions/list-users-with-emoji-reactions/list-users-with-emoji-reactions.mjs @@ -11,7 +11,7 @@ export default { name: "List Users that Reacted with Emoji", description: "Return a list of users that reacted with a specified emoji.", type: "action", - version: "0.0.12", + version: "0.0.13", props: { ...common.props, messageId: { diff --git a/components/discord_bot/actions/modify-channel/modify-channel.mjs b/components/discord_bot/actions/modify-channel/modify-channel.mjs index 6b928ac0a1c7b..513a7330dcffd 100644 --- a/components/discord_bot/actions/modify-channel/modify-channel.mjs +++ b/components/discord_bot/actions/modify-channel/modify-channel.mjs @@ -18,7 +18,7 @@ export default { name: "Modify Channel", description: "Update a channel's settings. [See the docs here](https://discord.com/developers/docs/resources/channel#modify-channel)", type: "action", - version: "0.0.13", + version: "0.0.14", props: { ...common.props, channelId: { diff --git a/components/discord_bot/actions/modify-guild-member/modify-guild-member.mjs b/components/discord_bot/actions/modify-guild-member/modify-guild-member.mjs index effe89191a2d4..26beb8b8cc8d1 100644 --- a/components/discord_bot/actions/modify-guild-member/modify-guild-member.mjs +++ b/components/discord_bot/actions/modify-guild-member/modify-guild-member.mjs @@ -19,7 +19,7 @@ export default { name: "Modify Guild Member", description: "Update attributes of a guild member. [See the docs here](https://discord.com/developers/docs/resources/guild#modify-guild-member)", type: "action", - version: "0.0.9", + version: "0.0.10", props: { ...common.props, userId: { diff --git a/components/discord_bot/actions/post-reaction-with-emoji/post-reaction-with-emoji.mjs b/components/discord_bot/actions/post-reaction-with-emoji/post-reaction-with-emoji.mjs index ace83e5452c36..348cf0adc6279 100644 --- a/components/discord_bot/actions/post-reaction-with-emoji/post-reaction-with-emoji.mjs +++ b/components/discord_bot/actions/post-reaction-with-emoji/post-reaction-with-emoji.mjs @@ -8,7 +8,7 @@ export default { name: "Post Reaction with Emoji", description: "Post a reaction for a message with an emoji. [See the docs here](https://discord.com/developers/docs/resources/channel#create-reaction)", type: "action", - version: "0.0.11", + version: "0.0.12", props: { ...common.props, messageId: { diff --git a/components/discord_bot/actions/remove-user-role/remove-user-role.mjs b/components/discord_bot/actions/remove-user-role/remove-user-role.mjs index f888f97b5a993..3e9fb216e441a 100644 --- a/components/discord_bot/actions/remove-user-role/remove-user-role.mjs +++ b/components/discord_bot/actions/remove-user-role/remove-user-role.mjs @@ -8,7 +8,7 @@ export default { name: "Remove User Role", description: "Remove a selected role from the specified user. [See the docs here](https://discord.com/developers/docs/resources/guild#remove-guild-member-role)", type: "action", - version: "0.0.11", + version: "0.0.12", props: { ...common.props, userId: { diff --git a/components/discord_bot/actions/rename-channel/rename-channel.mjs b/components/discord_bot/actions/rename-channel/rename-channel.mjs index 8307135aa37b8..784a33f2a2ba3 100644 --- a/components/discord_bot/actions/rename-channel/rename-channel.mjs +++ b/components/discord_bot/actions/rename-channel/rename-channel.mjs @@ -8,7 +8,7 @@ export default { name: "Rename Channel", description: "Rename a channel to a specified name you choose", type: "action", - version: "0.0.12", + version: "0.0.13", props: { ...common.props, channelId: { diff --git a/components/discord_bot/actions/send-message-to-forum-post/send-message-to-forum-post.mjs b/components/discord_bot/actions/send-message-to-forum-post/send-message-to-forum-post.mjs new file mode 100644 index 0000000000000..896f4a51adfcf --- /dev/null +++ b/components/discord_bot/actions/send-message-to-forum-post/send-message-to-forum-post.mjs @@ -0,0 +1,119 @@ +import discord from "../../discord_bot.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "discord_bot-send-message-to-forum-post", + name: "Send Message to Forum Post", + description: "Send a message to a Discord forum. [See the documentation](https://discord.com/developers/docs/resources/channel#create-message)", + version: "0.0.1", + type: "action", + props: { + discord, + info: { + type: "alert", + alertType: "info", + content: ` + \nTo get the Post ID: + \n1. **Manually on Web** + - Select the Forum thread, them copy the **second** number from the URL. For example, if the URL is \`https://discord.com/channels/123/456\`, the Post ID is \`456\`. + \n2. **Pipedream Source** + - If you're using **Discord Bot - New Forum Thread Message** source, use the \`channel_id\` field from the event. + `, + }, + postId: { + type: "string", + label: "Post ID", + description: "The ID of a forum post", + }, + message: { + propDefinition: [ + discord, + "message", + ], + }, + embeds: { + propDefinition: [ + discord, + "embeds", + ], + }, + username: { + propDefinition: [ + discord, + "username", + ], + }, + avatarURL: { + propDefinition: [ + discord, + "avatarURL", + ], + }, + includeSentViaPipedream: { + type: "boolean", + optional: true, + default: true, + label: "Include link to workflow", + description: "Defaults to `true`, includes a link to this workflow at the end of your Discord message.", + }, + }, + methods: { + getUserInputProps(omit = [ + "discord", + ]) { + return Object.keys(this) + .filter((key) => typeof this[key] !== "function" && !omit.includes(key)) + .reduce((props, key) => { + props[key] = this[key]; + return props; + }, {}); + }, + appendPipedreamText(message) { + let content = message; + if (typeof content !== "string") { + content = JSON.stringify(content); + } + content += `\n\n${this.getSentViaPipedreamText()}`; + return content; + }, + getSentViaPipedreamText() { + const workflowId = process.env.PIPEDREAM_WORKFLOW_ID; + // The link text is a URL without a protocol for consistency with the "Send via link" text in + // Slack messages + const linkText = `pipedream.com/@/${workflowId}?o=a&a=discord_webhook`; + const link = `https://${linkText}`; + return `Sent via [${linkText}](<${link}>)`; + }, + }, + async run({ $ }) { + const { + postId, + message, + embeds, + username, + avatarURL, + includeSentViaPipedream, + } = this; + try { + const createMessageResponse = await this.discord.createMessage({ + $, + channelId: postId, + data: { + embeds: utils.parseObject(embeds), + avatarURL, + username: username, + content: includeSentViaPipedream + ? this.appendPipedreamText(message) + : message, + }, + }); + $.export("$summary", "Message has been sent successfully"); + return createMessageResponse; + } catch (err) { + console.log(err); + const unsentMessage = this.getUserInputProps(); + $.export("unsent", unsentMessage); + throw err; + } + }, +}; diff --git a/components/discord_bot/actions/send-message-with-file/send-message-with-file.mjs b/components/discord_bot/actions/send-message-with-file/send-message-with-file.mjs index ebfa7b668b6fe..5fc963eb8a121 100644 --- a/components/discord_bot/actions/send-message-with-file/send-message-with-file.mjs +++ b/components/discord_bot/actions/send-message-with-file/send-message-with-file.mjs @@ -9,7 +9,7 @@ export default { key: "discord_bot-send-message-with-file", name: "Send Message With File", description: "Post a message with an attached file. [See the docs here](https://discord.com/developers/docs/reference#uploading-files)", - version: "0.0.5", + version: "0.0.6", type: "action", props: { discord, diff --git a/components/discord_bot/actions/send-message/send-message.mjs b/components/discord_bot/actions/send-message/send-message.mjs index 6ea9d2ac9e6e1..52b0e5fb7a0fc 100644 --- a/components/discord_bot/actions/send-message/send-message.mjs +++ b/components/discord_bot/actions/send-message/send-message.mjs @@ -9,7 +9,7 @@ export default { key: "discord_bot-send-message", name: "Send message", description: "Send message to a user or a channel. [See the docs here](https://discord.com/developers/docs/resources/user#create-dm) and [here](https://discord.com/developers/docs/resources/channel#create-message)", - version: "0.0.11", + version: "0.0.12", type: "action", props: { discord, diff --git a/components/discord_bot/common/utils.mjs b/components/discord_bot/common/utils.mjs index 327e013440d5b..6b2023f4f9cf7 100644 --- a/components/discord_bot/common/utils.mjs +++ b/components/discord_bot/common/utils.mjs @@ -1,4 +1,3 @@ -import { ConfigurationError } from "@pipedream/platform"; import constants from "./constants.mjs"; export default { @@ -157,14 +156,4 @@ export default { return obj; } }, - getUploadContentType(filename) { - const fileExt = filename.split(".").pop(); - switch (fileExt.toLowerCase()) { - case "png": return "image/png"; - case "jpg": return "image/jpeg"; - case "jpeg": return "image/jpeg"; - case "gif": return "image/gif"; - default: throw ConfigurationError("Only `.jpg`, `.jpeg`, `.png`, and `.gif` may be used at this time. Other file types are not supported."); - } - }, }; diff --git a/components/discord_bot/discord_bot.app.mjs b/components/discord_bot/discord_bot.app.mjs index 8ccaebc1efc07..4fb78a0d8a2bc 100644 --- a/components/discord_bot/discord_bot.app.mjs +++ b/components/discord_bot/discord_bot.app.mjs @@ -669,7 +669,6 @@ export default { data.append("files[0]", file, { header: [ `Content-Disposition: form-data; name="files[0]"; filename="${filename}"`, - `Content-Type: ${utils.getUploadContentType(filename)}`, ], }); return await this._makeRequest({ diff --git a/components/discord_bot/package.json b/components/discord_bot/package.json index 0bfa83ff9900e..357cc70699011 100644 --- a/components/discord_bot/package.json +++ b/components/discord_bot/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/discord_bot", - "version": "0.5.3", + "version": "0.6.0", "description": "Pipedream Discord_bot Components", "main": "discord_bot.app.js", "keywords": [ @@ -10,8 +10,9 @@ "homepage": "https://pipedream.com/apps/discord_bot", "author": "Pipedream (https://pipedream.com/)", "dependencies": { - "@pipedream/platform": "^1.4.0", - "@pipedreamhq/platform": "^0.8.1" + "@pipedream/platform": "^3.0.3", + "form-data": "^4.0.0", + "lodash.maxby": "^4.6.0" }, "gitHead": "e12480b94cc03bed4808ebc6b13e7fdb3a1ba535", "publishConfig": { diff --git a/components/discord_bot/sources/new-forum-thread-message/new-forum-thread-message.mjs b/components/discord_bot/sources/new-forum-thread-message/new-forum-thread-message.mjs index dbc59b524caaa..5466e5abfe46d 100644 --- a/components/discord_bot/sources/new-forum-thread-message/new-forum-thread-message.mjs +++ b/components/discord_bot/sources/new-forum-thread-message/new-forum-thread-message.mjs @@ -8,9 +8,9 @@ export default { ...common, key: "discord_bot-new-forum-thread-message", name: "New Forum Thread Message", - description: "Emit new event for each forum thread message posted. Note that your bot must have the `MESSAGE_CONTENT` privilege intent to see the message content, [see the docs here](https://discord.com/developers/docs/topics/gateway#message-content-intent).", + description: "Emit new event for each forum thread message posted. Note that your bot must have the `MESSAGE_CONTENT` privilege intent to see the message content. [See the documentation](https://discord.com/developers/docs/topics/gateway#message-content-intent).", type: "source", - version: "0.0.2", + version: "0.0.4", dedupe: "unique", // Dedupe events based on the Discord message ID props: { ...common.props, @@ -36,6 +36,14 @@ export default { description: "Select the forum you want to watch.", }, }, + methods: { + ...common.methods, + getChannel(id) { + return this.discord._makeRequest({ + path: `/channels/${id}`, + }); + }, + }, async run({ $ }) { // We store a cursor to the last message ID let lastMessageIDs = this._getLastMessageIDs(); @@ -107,6 +115,21 @@ export default { console.log(`${messages.length} new messages in thread ${channelId}`); + messages = await Promise.all(messages.map(async (message) => ({ + ...message, + thread: await this.getChannel(message.channel_id), + }))); + + const { available_tags: availableTags = [] } = await this.getChannel(this.forumId); + for (const message of messages) { + if (!message.thread.applied_tags) { + message.thread.applied_tags = []; + } + message.thread.applied_tags = message.thread.applied_tags.map((tagId) => ({ + ...availableTags.find(({ id }) => id === tagId), + })); + } + messages.reverse().forEach((message) => { this.$emit(message, { id: message.id, // dedupes events based on this ID diff --git a/components/discord_bot/sources/new-forum-thread-message/test-event.mjs b/components/discord_bot/sources/new-forum-thread-message/test-event.mjs index 71fae6e2a5ef5..1a8687e5f2fae 100644 --- a/components/discord_bot/sources/new-forum-thread-message/test-event.mjs +++ b/components/discord_bot/sources/new-forum-thread-message/test-event.mjs @@ -28,5 +28,45 @@ export default { "edited_timestamp": null, "flags": 0, "components": [], - "position": 13 -} \ No newline at end of file + "position": 13, + "thread": { + "id": "1301256410990116917", + "type": 11, + "last_message_id": "1301256410990116917", + "flags": 0, + "guild_id": "901259362205589565", + "name": "hello world", + "parent_id": "1301256016934994024", + "rate_limit_per_user": 0, + "bitrate": 64000, + "user_limit": 0, + "rtc_region": null, + "owner_id": "867892178135023656", + "thread_metadata": { + "archived": false, + "archive_timestamp": "2024-10-30T18:48:24.555000+00:00", + "auto_archive_duration": 4320, + "locked": false, + "create_timestamp": "2024-10-30T18:48:24.555000+00:00", + }, + "message_count": 0, + "member_count": 1, + "total_message_sent": 0, + "applied_tags": [ + { + "id": "1301256232052457563", + "name": "tag", + "moderated": false, + "emoji_id": null, + "emoji_name": null, + }, + { + "id": "1301281978968178759", + "name": "tag2", + "moderated": false, + "emoji_id": null, + "emoji_name": null, + }, + ], + }, +}; diff --git a/components/discord_bot/sources/new-guild-member/new-guild-member.mjs b/components/discord_bot/sources/new-guild-member/new-guild-member.mjs index 795b878b4bbe8..5e94fd29b1ec3 100644 --- a/components/discord_bot/sources/new-guild-member/new-guild-member.mjs +++ b/components/discord_bot/sources/new-guild-member/new-guild-member.mjs @@ -8,7 +8,7 @@ export default { description: "Emit new event for every member added to a guild. [See docs here](https://discord.com/developers/docs/resources/guild#list-guild-members)", type: "source", dedupe: "unique", - version: "0.1.3", + version: "0.1.4", props: { ...common.props, db: "$.service.db", diff --git a/components/discord_bot/sources/new-message-in-channel/new-message-in-channel.mjs b/components/discord_bot/sources/new-message-in-channel/new-message-in-channel.mjs index b305df85d2f25..ffa263725d0a9 100644 --- a/components/discord_bot/sources/new-message-in-channel/new-message-in-channel.mjs +++ b/components/discord_bot/sources/new-message-in-channel/new-message-in-channel.mjs @@ -11,7 +11,7 @@ export default { name: "New Message in Channel", description: "Emit new event for each message posted to one or more channels", type: "source", - version: "0.0.17", + version: "0.0.18", dedupe: "unique", // Dedupe events based on the Discord message ID props: { diff --git a/components/discord_bot/sources/new-tag-added-to-thread/new-tag-added-to-thread.mjs b/components/discord_bot/sources/new-tag-added-to-thread/new-tag-added-to-thread.mjs new file mode 100644 index 0000000000000..cb5b8dc58303f --- /dev/null +++ b/components/discord_bot/sources/new-tag-added-to-thread/new-tag-added-to-thread.mjs @@ -0,0 +1,87 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import common from "../common.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "discord_bot-new-tag-added-to-thread", + name: "New Tag Added to Forum Thread", + description: "Emit new event when a new tag is added to a thread", + type: "source", + version: "0.0.1", + dedupe: "unique", + props: { + ...common.props, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + const tags = {}; + const { threads } = await this.discord.listThreads({ + guildId: this.guildId, + }); + threads.forEach((thread) => { + if (thread?.applied_tags) { + tags[thread.id] = thread?.applied_tags; + } + }); + this._setTags(tags); + }, + }, + methods: { + ...common.methods, + _getTags() { + return this.db.get("tags") || {}; + }, + _setTags(tags) { + this.db.set("tags", tags); + }, + generateMeta(thread) { + return { + id: thread.id, + summary: `New tag in thread ${thread.id}`, + ts: Date.now(), + }; + }, + getChannel(id) { + return this.discord._makeRequest({ + path: `/channels/${id}`, + }); + }, + }, + async run() { + let tags = this._getTags(); + + const { threads } = await this.discord.listThreads({ + guildId: this.guildId, + }); + + for (const thread of threads) { + if (!thread.applied_tags) { + continue; + } + if (thread.applied_tags.some((tag) => !tags[thread.id] || !tags[thread.id].includes(tag))) { + tags[thread.id] = thread.applied_tags; + + const { available_tags: availableTags = [] } = await this.getChannel(thread.parent_id); + + thread.applied_tags = thread.applied_tags.map((tagId) => ({ + ...availableTags.find(({ id }) => id === tagId), + })); + + const meta = this.generateMeta(thread); + this.$emit(thread, meta); + + } + } + + this._setTags(tags); + }, + sampleEmit, +}; diff --git a/components/discord_bot/sources/new-tag-added-to-thread/test-event.mjs b/components/discord_bot/sources/new-tag-added-to-thread/test-event.mjs new file mode 100644 index 0000000000000..f6d7bcabf1448 --- /dev/null +++ b/components/discord_bot/sources/new-tag-added-to-thread/test-event.mjs @@ -0,0 +1,47 @@ +export default { + "id": "1301256410990116917", + "type": 11, + "last_message_id": "1301256410990116917", + "flags": 0, + "guild_id": "901259362205589565", + "name": "hello world", + "parent_id": "1301256016934994024", + "rate_limit_per_user": 0, + "bitrate": 64000, + "user_limit": 0, + "rtc_region": null, + "owner_id": "867892178135023656", + "thread_metadata": { + "archived": false, + "archive_timestamp": "2024-10-30T18:48:24.555000+00:00", + "auto_archive_duration": 4320, + "locked": false, + "create_timestamp": "2024-10-30T18:48:24.555000+00:00" + }, + "message_count": 0, + "member_count": 1, + "total_message_sent": 0, + "applied_tags": [ + { + "id": "1301256232052457563", + "name": "tag", + "moderated": false, + "emoji_id": null, + "emoji_name": null, + }, + { + "id": "1301282004998033428", + "name": "tag3", + "moderated": false, + "emoji_id": null, + "emoji_name": null, + }, + { + "id": "1301281978968178759", + "name": "tag2", + "moderated": false, + "emoji_id": null, + "emoji_name": null, + }, + ], +}; diff --git a/components/discord_bot/sources/new-thread-message/new-thread-message.mjs b/components/discord_bot/sources/new-thread-message/new-thread-message.mjs index 1da47934bf271..de5ee3bf09763 100644 --- a/components/discord_bot/sources/new-thread-message/new-thread-message.mjs +++ b/components/discord_bot/sources/new-thread-message/new-thread-message.mjs @@ -9,7 +9,7 @@ export default { name: "New Thread Message", description: "Emit new event for each thread message posted.", type: "source", - version: "0.0.4", + version: "0.0.5", dedupe: "unique", // Dedupe events based on the Discord message ID props: { ...common.props, diff --git a/components/discord_webhook/README.md b/components/discord_webhook/README.md deleted file mode 100644 index 2f7189b17ad63..0000000000000 --- a/components/discord_webhook/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Overview - -With Discord's Webhook API, you can create applications that send messages to -Discord channels automatically. For example, you could create a bot that sends -a message to a channel every time a new blog post is published, or a message to -a channel when someone joins your Discord server. - -Here are some ideas for what you could build using the Discord Webhook API: - -- A bot that sends a message to a channel when a new blog post is published -- A bot that sends a message to a channel when someone joins your Discord - server -- A bot that sends a message to a channel when a new product is added to your - online store diff --git a/components/discord_webhook/actions/send-message-advanced/send-message-advanced.mjs b/components/discord_webhook/actions/send-message-advanced/send-message-advanced.mjs deleted file mode 100644 index 0b396613a63a4..0000000000000 --- a/components/discord_webhook/actions/send-message-advanced/send-message-advanced.mjs +++ /dev/null @@ -1,58 +0,0 @@ -import common from "../send-message-common.mjs"; - -export default { - ...common, - key: "discord_webhook-send-message-advanced", - name: "Send Message (Advanced)", - description: "Send a simple or structured message (using embeds) to a Discord channel", - version: "0.4.0", - type: "action", - props: { - ...common.props, - message: { - propDefinition: [ - common.props.discordWebhook, - "message", - ], - optional: true, - }, - embeds: { - propDefinition: [ - common.props.discordWebhook, - "embeds", - ], - }, - }, - async run({ $ }) { - const { - message, - avatarURL, - threadID, - username, - includeSentViaPipedream, - embeds, - } = this; - - if (!message && !embeds) { - throw new Error("This action requires at least 1 message OR embeds object. Please enter one or the other above."); - } - - try { - // No interesting data is returned from Discord - await this.discordWebhook.sendMessage({ - avatarURL, - threadID, - username, - embeds, - content: includeSentViaPipedream - ? this.appendPipedreamText(message ?? "") - : message, - }); - $.export("$summary", "Message sent successfully"); - } catch (err) { - const unsentMessage = this.getUserInputProps(); - $.export("unsent", unsentMessage); - throw err; - } - }, -}; diff --git a/components/discord_webhook/actions/send-message-common.mjs b/components/discord_webhook/actions/send-message-common.mjs deleted file mode 100644 index f336eafff7952..0000000000000 --- a/components/discord_webhook/actions/send-message-common.mjs +++ /dev/null @@ -1,69 +0,0 @@ -import discordWebhook from "../discord_webhook.app.mjs"; - -/* eslint-disable pipedream/required-properties-key, pipedream/required-properties-name, - pipedream/required-properties-version, pipedream/required-properties-description */ -export default { - type: "action", - props: { - discordWebhook, - message: { - propDefinition: [ - discordWebhook, - "message", - ], - }, - threadID: { - propDefinition: [ - discordWebhook, - "threadID", - ], - }, - username: { - propDefinition: [ - discordWebhook, - "username", - ], - }, - avatarURL: { - propDefinition: [ - discordWebhook, - "avatarURL", - ], - }, - includeSentViaPipedream: { - type: "boolean", - optional: true, - default: true, - label: "Include link to workflow", - description: "Defaults to `true`, includes a link to this workflow at the end of your Discord message.", - }, - }, - methods: { - getUserInputProps(omit = [ - "discordWebhook", - ]) { - return Object.keys(this) - .filter((k) => typeof this[k] !== "function" && !omit.includes(k)) - .reduce((props, key) => { - props[key] = this[key]; - return props; - }, {}); - }, - appendPipedreamText(message) { - let content = message; - if (typeof content !== "string") { - content = JSON.stringify(content); - } - content += `\n\n${this.getSentViaPipedreamText()}`; - return content; - }, - getSentViaPipedreamText() { - const workflowId = process.env.PIPEDREAM_WORKFLOW_ID; - // The link text is a URL without a protocol for consistency with the "Send via link" text in - // Slack messages - const linkText = `pipedream.com/@/${workflowId}?o=a&a=discord_webhook`; - const link = `https://${linkText}`; - return `Sent via [${linkText}](<${link}>)`; - }, - }, -}; diff --git a/components/discord_webhook/actions/send-message-with-file/send-message-with-file.mjs b/components/discord_webhook/actions/send-message-with-file/send-message-with-file.mjs deleted file mode 100644 index c6a537ab20ad9..0000000000000 --- a/components/discord_webhook/actions/send-message-with-file/send-message-with-file.mjs +++ /dev/null @@ -1,77 +0,0 @@ -import common from "../send-message-common.mjs"; -import { axios } from "@pipedream/platform"; -import fs from "fs"; - -export default { - ...common, - key: "discord_webhook-send-message-with-file", - name: "Send Message With File", - description: "Post a message with an attached file", - version: "0.3.0", - type: "action", - props: { - ...common.props, - message: { - propDefinition: [ - common.props.discordWebhook, - "message", - ], - optional: true, - }, - fileUrl: { - type: "string", - label: "File URL", - description: - "The URL of the file to attach. Must specify either **File URL** or **File Path**.", - optional: true, - }, - filePath: { - type: "string", - label: "File Path", - description: - "The path to the file, e.g. `/tmp/myFile.csv`. Must specify either **File URL** or **File Path**.", - optional: true, - }, - }, - async run({ $ }) { - const { - message, - avatarURL, - threadID, - username, - fileUrl, - filePath, - includeSentViaPipedream, - } = this; - - if (!fileUrl && !filePath) { - throw new Error("This action requires either File URL or File Path. Please enter one or the other above."); - } - - const file = fileUrl - ? await axios($, { - method: "get", - url: fileUrl, - responseType: "stream", - }) - : fs.createReadStream(filePath); - - try { - // No interesting data is returned from Discord - await this.discordWebhook.sendMessageWithFile({ - avatarURL, - threadID, - username, - file, - content: includeSentViaPipedream - ? this.appendPipedreamText(message ?? "") - : message, - }); - $.export("$summary", "Message sent successfully"); - } catch (err) { - const unsentMessage = this.getUserInputProps(); - $.export("unsent", unsentMessage); - throw err; - } - }, -}; diff --git a/components/discord_webhook/actions/send-message/send-message.mjs b/components/discord_webhook/actions/send-message/send-message.mjs deleted file mode 100644 index 42b9faad57b5d..0000000000000 --- a/components/discord_webhook/actions/send-message/send-message.mjs +++ /dev/null @@ -1,39 +0,0 @@ -import common from "../send-message-common.mjs"; - -export default { - ...common, - key: "discord_webhook-send-message", - name: "Send Message", - description: "Send a simple message to a Discord channel", - version: "0.4.0", - type: "action", - props: { - ...common.props, - }, - async run({ $ }) { - const { - message, - avatarURL, - threadID, - username, - includeSentViaPipedream, - } = this; - - try { - // No interesting data is returned from Discord - await this.discordWebhook.sendMessage({ - avatarURL, - threadID, - username, - content: includeSentViaPipedream - ? this.appendPipedreamText(message) - : message, - }); - $.export("$summary", "Message sent successfully"); - } catch (err) { - const unsentMessage = this.getUserInputProps(); - $.export("unsent", unsentMessage); - throw err; - } - }, -}; diff --git a/components/discord_webhook/discord_webhook.app.mjs b/components/discord_webhook/discord_webhook.app.mjs deleted file mode 100644 index e5d984c9a18ae..0000000000000 --- a/components/discord_webhook/discord_webhook.app.mjs +++ /dev/null @@ -1,107 +0,0 @@ -import { axios } from "@pipedream/platform"; -import FormData from "form-data"; - -export default { - type: "app", - app: "discord_webhook", - propDefinitions: { - message: { - type: "string", - label: "Message", - description: "Enter a simple message up to 2000 characters. This is the most commonly used field. However, it's optional if you pass embed content.", - }, - embeds: { - type: "any", - label: "Embeds", - description: "Optionally pass an [array of embed objects](https://birdie0.github.io/discord-webhooks-guide/discord_webhook.html). E.g., ``{{ [{\"description\":\"Use markdown including *Italic* **bold** __underline__ ~~strikeout~~ [hyperlink](https://google.com) `code`\"}] }}``. To pass data from another step, enter a reference using double curly brackets (e.g., `{{steps.mydata.$return_value}}`).\nTip: Construct the `embeds` array in a Node.js code step, return it, and then pass the return value to this step.", - optional: true, - }, - username: { - type: "string", - label: "Username", - description: "Overrides the current username of the webhook", - optional: true, - }, - avatarURL: { - type: "string", - label: "Avatar URL", - description: "If used, it overrides the default avatar of the webhook. Note: Consecutive posts by the same username within 10 minutes of each other will not display updated avatar.", - optional: true, - }, - threadID: { - type: "string", - label: "Thread ID", - description: "If provided, the message will be posted to this thread", - optional: true, - }, - }, - methods: { - url() { - return this.$auth.oauth_uid; - }, - async sendMessage({ - $ = this, content, embeds, username, avatarURL, threadID, - }) { - const serializedContent = (typeof content !== "string") - ? JSON.stringify(content) - : content; - if (!threadID) threadID = undefined; - const resp = await axios($, { - method: "POST", - url: this.url(), - headers: { - "Content-Type": "application/json", - }, - validateStatus: () => true, - params: { - thread_id: threadID, - }, - data: { - content: serializedContent, - embeds, - username, - avatar_url: avatarURL, - }, - returnFullResponse: true, - }); - if (resp.status >= 400) { - throw new Error(JSON.stringify(resp.data)); - } - return resp.data; - }, - async sendMessageWithFile({ - $ = this, content, username, avatarURL, embeds, threadID, file, - }) { - const data = new FormData(); - const serializedContent = (typeof content !== "string") - ? JSON.stringify(content) - : content; - data.append("payload_json", JSON.stringify({ - content: serializedContent, - username, - avatar_url: avatarURL, - embeds, - })); - if (file) data.append("file", file); - if (!threadID) threadID = undefined; - const resp = await axios($, { - method: "POST", - url: this.url(), - headers: { - "Content-Type": "multipart/form-data; boundary=" + data._boundary, - }, - validateStatus: () => true, - params: { - thread_id: threadID, - }, - data, - file, - returnFullResponse: true, - }); - if (resp.status >= 400) { - throw new Error(JSON.stringify(resp.data)); - } - return resp.data; - }, - }, -}; diff --git a/components/discord_webhook/package.json b/components/discord_webhook/package.json deleted file mode 100644 index 8596c7d43e250..0000000000000 --- a/components/discord_webhook/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@pipedream/discord_webhook", - "version": "1.1.0", - "description": "Pipedream Discord_Webook Components", - "main": "discord_webhook.app.mjs", - "keywords": [ - "pipedream", - "discord_webhook" - ], - "homepage": "https://pipedream.com/apps/discord_webhook", - "author": "Pipedream (https://pipedream.com/)", - "dependencies": { - "@pipedream/platform": "^1.4.0", - "form-data": "^4.0.0" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/components/discourse/README.md b/components/discourse/README.md index 70585f383bb94..4377262092672 100644 --- a/components/discourse/README.md +++ b/components/discourse/README.md @@ -1,13 +1,11 @@ # Overview -Discourse is a powerful platform for building online communities. With the -Discourse API, you can extend Discourse's functionality to build all sorts of -custom applications and integrations. +The Discourse API empowers developers to interact programmatically with Discourse forums, allowing for a myriad of automations and integrations. Using Pipedream, you can harness this API for tasks such as syncing forum data with other platforms, automating user management, or triggering workflows based on forum activity. The key is in creatively coupling Discourse events with Pipedream's capabilities to streamline community interactions and administration. -Here are some examples of what you can build with the Discourse API: +# Example Use Cases -- A custom Discourse plugin -- A Discourse-powered website or forum -- An application that interacts with Discourse data -- ADiscourse-based chatbot -- ADiscourse integration for another application +- **User Onboarding Automation**: When a new member joins your Discourse forum, trigger a Pipedream workflow that sends a personalized welcome email via SendGrid, adds the user to a Mailchimp list for community updates, and posts an introductory message in a designated forum category using the Discourse API. + +- **Content Digest Creator**: Compile a weekly digest of the most popular topics and posts by using the Discourse API to fetch forum activity data. The workflow could then format this information into a newsletter with HTML, and send it to all subscribers through an email service like SendGrid or Mailchimp, keeping your community engaged and informed. + +- **Issue Tracking Integration**: For communities using Discourse for support and feature requests, you can set up a workflow that watches for new posts with specific tags or in certain categories. Once detected, it can create an issue in GitHub, Jira, or Trello, providing seamless integration between community feedback on Discourse and your team's project management tools. diff --git a/components/dispatch/README.md b/components/dispatch/README.md index 720993b3dfa62..7ebe2c151e85d 100644 --- a/components/dispatch/README.md +++ b/components/dispatch/README.md @@ -1,9 +1,11 @@ # Overview -With the Dispatch API you can: +The Dispatch API provides a powerful mechanism to streamline logistics and delivery operations. By leveraging it through Pipedream, businesses can automate the scheduling and tracking of dispatches, facilitating real-time coordination between dispatchers, drivers, and customers. This can lead to more efficient route planning, faster delivery times, and improved communication, all of which contribute to a better overall experience for both the provider and the end-user. -- Create a new Dispatch -- Get a list of all Dispatches -- Get a specific Dispatch -- Update a Dispatch -- Delete a Dispatch +# Example Use Cases + +- **Automated Delivery Tracking**: Integrate Dispatch with GPS and mapping services on Pipedream to create a workflow that automatically updates customers with real-time tracking information. As drivers progress on their routes, Pipedream can trigger updates that are sent via SMS or email, keeping customers informed. + +- **Dynamic Route Optimization**: Use Dispatch alongside weather and traffic APIs to adjust delivery routes in real-time. If a traffic jam or poor weather conditions occur, Pipedream can trigger Dispatch to recalculate and communicate the most efficient detour to drivers, reducing delays and maintaining delivery schedules. + +- **Streamlined Order Processing**: Connect Dispatch with an e-commerce platform like Shopify. When a new order is placed, Pipedream can automatically create a dispatch job, assign it to a driver, and communicate pickup and delivery details, ensuring a smooth handoff from order placement to delivery completion. diff --git a/components/dixa/actions/add-message/add-message.mjs b/components/dixa/actions/add-message/add-message.mjs new file mode 100644 index 0000000000000..8e9daaff33cd3 --- /dev/null +++ b/components/dixa/actions/add-message/add-message.mjs @@ -0,0 +1,67 @@ +import dixa from "../../dixa.app.mjs"; + +export default { + key: "dixa-add-message", + name: "Add Message to Conversation", + description: "Adds a message to an existing conversation. [See the documentation](https://docs.dixa.io/openapi/dixa-api/v1/tag/Conversations/#tag/Conversations/operation/postConversationsConversationidMessages).", + version: "0.0.1", + type: "action", + props: { + dixa, + endUserId: { + propDefinition: [ + dixa, + "endUserId", + ], + }, + conversationId: { + propDefinition: [ + dixa, + "conversationId", + ({ endUserId }) => ({ + endUserId, + }), + ], + }, + content: { + type: "string", + label: "Content", + description: "Content of the message", + }, + direction: { + propDefinition: [ + dixa, + "direction", + ], + reloadProps: true, + }, + agentId: { + propDefinition: [ + dixa, + "agentId", + ], + }, + }, + async additionalProps(props) { + props.agentId.hidden = this.direction !== "Outbound"; + return {}; + }, + async run({ $ }) { + const response = await this.dixa.addMessage({ + $, + conversationId: this.conversationId, + data: { + agentId: this.direction === "Outbound" + ? this.agentId + : undefined, + content: { + value: this.content, + _type: "Text", + }, + _type: this.direction, + }, + }); + $.export("$summary", `Added message to conversation ${this.conversationId}`); + return response; + }, +}; diff --git a/components/dixa/actions/create-conversation/create-conversation.mjs b/components/dixa/actions/create-conversation/create-conversation.mjs new file mode 100644 index 0000000000000..6afa9624db276 --- /dev/null +++ b/components/dixa/actions/create-conversation/create-conversation.mjs @@ -0,0 +1,102 @@ +import dixa from "../../dixa.app.mjs"; + +export default { + key: "dixa-create-conversation", + name: "Create Conversation", + description: "Creates a new email or contact form-based conversation. [See the documentation](https://docs.dixa.io/openapi/dixa-api/v1/tag/Conversations/#tag/Conversations/operation/postConversations).", + version: "0.0.1", + type: "action", + props: { + dixa, + requesterId: { + propDefinition: [ + dixa, + "endUserId", + ], + label: "Requester Id", + }, + direction: { + propDefinition: [ + dixa, + "direction", + ], + reloadProps: true, + }, + channel: { + type: "string", + label: "Channel", + description: "For outbound, only Email is supported. Inbound also supports ContactForm.", + options: [ + "Email", + "ContactForm", + ], + }, + emailIntegrationId: { + propDefinition: [ + dixa, + "emailIntegrationId", + ], + }, + subject: { + propDefinition: [ + dixa, + "subject", + ], + }, + message: { + type: "string", + label: "Message", + description: "The content message.", + }, + language: { + propDefinition: [ + dixa, + "language", + ], + optional: true, + }, + agentId: { + propDefinition: [ + dixa, + "agentId", + ], + optional: true, + }, + }, + async additionalProps(props) { + props.agentId.hidden = !(this.direction === "Outbound"); + props.channel.options = this.direction === "Outbound" + ? [ + "Email", + ] + : [ + "ContactForm", + "Email", + ]; + return {}; + }, + async run({ $ }) { + const response = await this.dixa.createConversation({ + $, + data: { + subject: this.subject, + emailIntegrationId: this.emailIntegrationId, + language: this.language, + requesterId: this.requesterId, + message: { + agentId: this.direction === "Outbound" + ? this.agentId + : undefined, + content: { + _type: "Text", + value: this.message, + }, + _type: this.direction, + }, + _type: this.channel, + }, + }); + $.export("$summary", `Created conversation with Id: ${response.data.id}`); + return response; + }, +}; diff --git a/components/dixa/actions/set-custom-contact-attributes/set-custom-contact-attributes.mjs b/components/dixa/actions/set-custom-contact-attributes/set-custom-contact-attributes.mjs new file mode 100644 index 0000000000000..d4c39c1657915 --- /dev/null +++ b/components/dixa/actions/set-custom-contact-attributes/set-custom-contact-attributes.mjs @@ -0,0 +1,98 @@ +import dixa from "../../dixa.app.mjs"; + +export default { + key: "dixa-set-custom-contact-attributes", + name: "Set Custom Contact Attributes", + description: "Updates custom attributes for a specified user. [See the documentation](https://docs.dixa.io/openapi/dixa-api/v1/tag/Custom-Attributes/#tag/Custom-Attributes/operation/patchEndusersUseridCustom-attributes)", + version: "0.0.1", + type: "action", + props: { + dixa, + userId: { + propDefinition: [ + dixa, + "endUserId", + ], + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + const { data } = await this.dixa.listCustomAttributes(); + + for (const item of data) { + if (item.isDeactivated || item.isArchived || item.entityType != "Contact") continue; + + props[item.id] = { + type: "string", + label: item.label, + description: item.description, + optional: !item.isRequired, + default: item.inputDefinition.placeholder, + }; + + if (item.inputDefinition._type === "Select") { + props[item.id].options = this.prepareOptions(item.inputDefinition.options); + } + } + return props; + }, + methods: { + prepareOptions(options, parentVal = "", parentLabel = "") { + const newOptions = []; + + for (const opt of options) { + const newLabel = parentLabel + ? `${parentLabel} - ${opt.label}` + : opt.label; + + const newVal = parentVal + ? `${parentVal}/${opt.value}` + : opt.value; + + if (opt.nestedOptions.length) { + newOptions.push(...this.prepareOptions(opt.nestedOptions, newVal, newLabel)); + } else { + newOptions.push({ + label: newLabel, + value: newVal, + }); + } + } + return newOptions; + }, + async prepareData(data) { + const response = {}; + const { data: customAttributes } = await this.dixa.listCustomAttributes(); + Object.entries(data).map(([ + key, + val, + ]) => { + const customAttribute = customAttributes.find((attr) => attr.id === key); + + response[key] = customAttribute.inputDefinition._type != "Text" + ? val.split("/") + : val; + }); + return response; + }, + }, + async run({ $ }) { + const { + dixa, + // eslint-disable-next-line no-unused-vars + prepareOptions, + prepareData, + userId, + ...data + } = this; + + const response = await dixa.updateCustomAttributes({ + $, + userId, + data: await prepareData(data), + }); + $.export("$summary", `Updated custom attributes for user ${this.userId}`); + return response; + }, +}; diff --git a/components/dixa/actions/tag-conversation/tag-conversation.mjs b/components/dixa/actions/tag-conversation/tag-conversation.mjs new file mode 100644 index 0000000000000..5427ff70441a3 --- /dev/null +++ b/components/dixa/actions/tag-conversation/tag-conversation.mjs @@ -0,0 +1,42 @@ +import dixa from "../../dixa.app.mjs"; + +export default { + key: "dixa-tag-conversation", + name: "Add Tag to Conversation", + description: "Adds a tag to a conversation. [See the documentation](https://docs.dixa.io/openapi/dixa-api/v1/tag/Tags/#tag/Tags/operation/putConversationsConversationidTagsTagid)", + version: "0.0.1", + type: "action", + props: { + dixa, + endUserId: { + propDefinition: [ + dixa, + "endUserId", + ], + }, + conversationId: { + propDefinition: [ + dixa, + "conversationId", + ({ endUserId }) => ({ + endUserId, + }), + ], + }, + tagId: { + propDefinition: [ + dixa, + "tagId", + ], + }, + }, + async run({ $ }) { + const response = await this.dixa.addTag({ + $, + conversationId: this.conversationId, + tagId: this.tagId, + }); + $.export("$summary", `Added tag ${this.tagId} to conversation ${this.conversationId}`); + return response; + }, +}; diff --git a/components/dixa/dixa.app.mjs b/components/dixa/dixa.app.mjs new file mode 100644 index 0000000000000..d637977931ddf --- /dev/null +++ b/components/dixa/dixa.app.mjs @@ -0,0 +1,212 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "dixa", + propDefinitions: { + endUserId: { + type: "string", + label: "End User Id", + description: "The id of the end user.", + async options({ page }) { + const { data } = await this.listEndUsers({ + params: { + page: page + 1, + }, + }); + return data.map(({ + id: value, displayName: label, + }) => ({ + label, + value, + })); + }, + }, + direction: { + type: "string", + label: "Direction", + description: "The direction of the message", + options: [ + "Inbound", + "Outbound", + ], + }, + emailIntegrationId: { + type: "string", + label: "Email Integration ID", + description: "The contact endpoint in the organization.", + async options() { + const { data } = await this.listContactEndpoints(); + return data.filter(({ _type }) => _type === "EmailEndpoint").map(({ address }) => address); + }, + }, + subject: { + type: "string", + label: "Subject", + description: "The subject of the conversation", + }, + language: { + type: "string", + label: "Language", + description: "The [2-letter ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) language of the conversation", + }, + agentId: { + type: "string", + label: "Agent Id", + description: "The id of the agent.", + hidden: true, + async options({ page }) { + const { data } = await this.listAgents({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + id: value, displayName: label, + }) => ({ + label, + value, + })); + }, + }, + conversationId: { + type: "string", + label: "Conversation ID", + description: "The ID of the conversation", + async options({ endUserId }) { + const { data } = await this.listConversations({ + endUserId, + }); + return data.map(({ + id: value, direction, toEmail, fromEmail, + }) => { + return { + label: `${direction} - ${direction === "Inbound" + ? fromEmail + : toEmail}`, + value, + }; + }); + }, + }, + tagId: { + type: "string", + label: "Tag ID", + description: "The ID of the tag to add", + async options() { + const { data } = await this.listTags(); + return data + .filter(({ state }) => state === "Active") + .map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://dev.dixa.io/v1"; + }, + _headers() { + return { + "authorization": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listEndUsers(opts = {}) { + return this._makeRequest({ + path: "/endusers", + ...opts, + }); + }, + listAgents(opts = {}) { + return this._makeRequest({ + path: "/agents", + ...opts, + }); + }, + listContactEndpoints(opts = {}) { + return this._makeRequest({ + path: "/contact-endpoints", + ...opts, + }); + }, + listConversations({ + endUserId, ...opts + }) { + return this._makeRequest({ + path: `/endusers/${endUserId}/conversations`, + ...opts, + }); + }, + listCustomAttributes() { + return this._makeRequest({ + path: "/custom-attributes", + }); + }, + addTag({ + conversationId, tagId, + }) { + return this._makeRequest({ + method: "PUT", + path: `/conversations/${conversationId}/tags/${tagId}`, + }); + }, + updateCustomAttributes({ + userId, ...opts + }) { + return this._makeRequest({ + method: "PATCH", + path: `/endusers/${userId}/custom-attributes`, + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + }); + }, + listTags(opts = {}) { + return this._makeRequest({ + path: "/tags", + ...opts, + }); + }, + createConversation(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/conversations", + ...opts, + }); + }, + addMessage({ + conversationId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/conversations/${conversationId}/messages`, + ...opts, + }); + }, + }, +}; diff --git a/components/dixa/package.json b/components/dixa/package.json new file mode 100644 index 0000000000000..f7012f0fe3697 --- /dev/null +++ b/components/dixa/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/dixa", + "version": "0.1.0", + "description": "Pipedream Dixa Components", + "main": "dixa.app.mjs", + "keywords": [ + "pipedream", + "dixa" + ], + "homepage": "https://pipedream.com/apps/dixa", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/dixa/sources/common/base.mjs b/components/dixa/sources/common/base.mjs new file mode 100644 index 0000000000000..bbe2c47f7c7f5 --- /dev/null +++ b/components/dixa/sources/common/base.mjs @@ -0,0 +1,49 @@ +import dixa from "../../dixa.app.mjs"; + +export default { + props: { + dixa, + http: "$.interface.http", + db: "$.service.db", + name: { + type: "string", + label: "Name", + description: "The Webhook Subscription name", + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + }, + hooks: { + async activate() { + const response = await this.dixa.createWebhook({ + data: { + name: this.name, + url: this.http.endpoint, + events: this.getEventType(), + authorization: { + _type: "NoAuth", + }, + }, + }); + this._setHookId(response.data.id); + }, + async deactivate() { + const webhookId = this._getHookId(); + await this.dixa.deleteWebhook(webhookId); + }, + }, + async run({ body }) { + const ts = Date.now(); + this.$emit(body, { + id: body.event_id, + summary: this.getSummary(body), + ts: ts, + }); + }, +}; diff --git a/components/dixa/sources/conversation-status-changed-instant/conversation-status-changed-instant.mjs b/components/dixa/sources/conversation-status-changed-instant/conversation-status-changed-instant.mjs new file mode 100644 index 0000000000000..9d4fc452f49d6 --- /dev/null +++ b/components/dixa/sources/conversation-status-changed-instant/conversation-status-changed-instant.mjs @@ -0,0 +1,41 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "dixa-conversation-status-changed-instant", + name: "New Conversation Status Changed (Instant)", + description: "Emit new events when the status of a conversation changes (e.g., open, closed, or abandoned). [See the documentation](https://docs.dixa.io/openapi/dixa-api/v1/tag/Webhooks/).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return [ + "ConversationPending", + "ConversationMessageAdded", + "ConversationTagAdded", + "ConversationAssigned", + "ConversationPendingExpired", + "ConversationTransferred", + "ConversationEnqueued", + "ConversationCreated", + "ConversationUnassigned", + "ConversationOpen", + "ConversationAbandoned", + "ConversationClosed", + "ConversationNoteAdded", + "ConversationEndUserReplaced", + "ConversationTagRemoved", + "ConversationRated", + ]; + }, + getSummary({ + data, event_fqn: eventType, + }) { + return `Conversation ${data.conversation.csid} status changed to ${eventType}`; + }, + }, + sampleEmit, +}; diff --git a/components/dixa/sources/conversation-status-changed-instant/test-event.mjs b/components/dixa/sources/conversation-status-changed-instant/test-event.mjs new file mode 100644 index 0000000000000..f853fc35bbcd3 --- /dev/null +++ b/components/dixa/sources/conversation-status-changed-instant/test-event.mjs @@ -0,0 +1,42 @@ +export default { + "event_fqn": "CONVERSATION_PENDING", + "event_id": "12345678-1234-1234-1234-1234567890", + "event_timestamp": "2025-01-14T22:01:05.429Z", + "event_version": "1", + "organization": { + "id": "12345678-1234-1234-1234-1234567890", + "name": "Org Name" + }, + "data": { + "conversation": { + "assignee": { + "email": "email@dixa.com", + "id": "12345678-1234-1234-1234-1234567890", + "name": "Agent Name", + "phone": "+123456789", + "roles": [], + "user_type": "Member" + }, + "channel": "EMAIL", + "contact_point": "contact@email.dixa.io", + "created_at": "2025-01-13T19:48:33.178Z", + "csid": 2, + "direction": "OUTBOUND", + "queue": { + "id": "12345678-1234-1234-1234-1234567890", + "name": "default" + }, + "requester": { + "email": "contact@email.com", + "id": "12345678-1234-1234-1234-1234567890", + "name": "Contact Name", + "phone": null, + "roles": [], + "user_type": "Contact" + }, + "status": "PENDING", + "subject": "Subject Text", + "tags": [] + } + } +} \ No newline at end of file diff --git a/components/dixa/sources/new-conversation-created-instant/new-conversation-created-instant.mjs b/components/dixa/sources/new-conversation-created-instant/new-conversation-created-instant.mjs new file mode 100644 index 0000000000000..fa6e3bd3da5c2 --- /dev/null +++ b/components/dixa/sources/new-conversation-created-instant/new-conversation-created-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "dixa-new-conversation-created-instant", + name: "New Conversation Created (Instant)", + description: "Emit new event when a conversation is created in Dixa. [See the documentation](https://docs.dixa.io/openapi/dixa-api/v1/tag/Webhooks/).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return [ + "ConversationCreated", + ]; + }, + getSummary({ data }) { + return `New conversation created with Id: ${data.conversation.csid}`; + }, + }, + sampleEmit, +}; diff --git a/components/dixa/sources/new-conversation-created-instant/test-event.mjs b/components/dixa/sources/new-conversation-created-instant/test-event.mjs new file mode 100644 index 0000000000000..c5a3311016db8 --- /dev/null +++ b/components/dixa/sources/new-conversation-created-instant/test-event.mjs @@ -0,0 +1,35 @@ +export default { + "event_fqn": "CONVERSATION_CREATED", + "event_id": "12345678-1234-1234-1234-1234567890", + "event_timestamp": "2025-01-14T22:01:05.429Z", + "event_version": "1", + "organization": { + "id": "12345678-1234-1234-1234-1234567890", + "name": "Org Name" + }, + "data": { + "conversation": { + "assignee": null, + "channel": "EMAIL", + "contact_point": "contact@email.dixa.io", + "created_at": "2025-01-13T19:48:33.178Z", + "csid": 2, + "direction": "INBOUND", + "queue": { + "id": "12345678-1234-1234-1234-1234567890", + "name": "default" + }, + "requester": { + "email": "contact@email.com", + "id": "12345678-1234-1234-1234-1234567890", + "name": "Contact Name", + "phone": null, + "roles": [], + "user_type": "Contact" + }, + "status": "OPEN", + "subject": "Subject Text", + "tags": [] + } + } +} \ No newline at end of file diff --git a/components/dixa/sources/new-customer-satisfaction-rating-instant/new-customer-satisfaction-rating-instant.mjs b/components/dixa/sources/new-customer-satisfaction-rating-instant/new-customer-satisfaction-rating-instant.mjs new file mode 100644 index 0000000000000..8cc259dbc8f03 --- /dev/null +++ b/components/dixa/sources/new-customer-satisfaction-rating-instant/new-customer-satisfaction-rating-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "dixa-new-customer-satisfaction-rating-instant", + name: "New Customer Satisfaction Rating (Instant)", + description: "Emit new event when a customer submits a satisfaction rating for a conversation. [See the documentation](https://docs.dixa.io/openapi/dixa-api/v1/tag/Webhooks/).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return [ + "ConversationRated", + ]; + }, + getSummary({ data }) { + return `New satisfaction rating for conversation ${data.conversation.csid}`; + }, + }, + sampleEmit, +}; diff --git a/components/dixa/sources/new-customer-satisfaction-rating-instant/test-event.mjs b/components/dixa/sources/new-customer-satisfaction-rating-instant/test-event.mjs new file mode 100644 index 0000000000000..77287d17ba3c2 --- /dev/null +++ b/components/dixa/sources/new-customer-satisfaction-rating-instant/test-event.mjs @@ -0,0 +1,53 @@ +export default { + "event_fqn": "CONVERSATION_RATED", + "event_id": "12345678-1234-1234-1234-1234567890", + "event_timestamp": "2025-01-14T22:01:05.429Z", + "event_version": "1", + "organization": { + "id": "12345678-1234-1234-1234-1234567890", + "name": "Org Name" + }, + "data": { + "agent": { + "email": "email@dixa.com", + "id": "12345678-1234-1234-1234-1234567890", + "name": "Agent Name", + "phone": "+123456789", + "roles": [], + "user_type": "Member" + }, + "comment": "Best customer service ever!", + "conversation": { + "assignee": { + "email": "email@dixa.com", + "id": "12345678-1234-1234-1234-1234567890", + "name": "Agent Name", + "phone": "+123456789", + "roles": [], + "user_type": "Member" + }, + "channel": "EMAIL", + "contact_point": "contact@email.dixa.io", + "created_at": "2025-01-13T19:48:33.178Z", + "csid": 2, + "direction": "OUTBOUND", + "queue": { + "id": "12345678-1234-1234-1234-1234567890", + "name": "default" + }, + "requester": { + "email": "contact@email.com", + "id": "12345678-1234-1234-1234-1234567890", + "name": "Contact Name", + "phone": null, + "roles": [], + "user_type": "Contact" + }, + "status": "CLOSED", + "subject": "Subject Text", + "tags": [], + "score": 4, + "type": "Csat" + } + } +} \ No newline at end of file diff --git a/components/dixa/sources/new-message-added-instant/new-message-added-instant.mjs b/components/dixa/sources/new-message-added-instant/new-message-added-instant.mjs new file mode 100644 index 0000000000000..f499673b4ab3e --- /dev/null +++ b/components/dixa/sources/new-message-added-instant/new-message-added-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "dixa-new-message-added-instant", + name: "New Message Added to Conversation (Instant)", + description: "Emit new event when a new message is added to a conversation. [See the documentation](https://docs.dixa.io/openapi/dixa-api/v1/tag/Webhooks/).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return [ + "ConversationMessageAdded", + ]; + }, + getSummary({ data }) { + return `New message in conversation ${data.conversation.csid}`; + }, + }, + sampleEmit, +}; diff --git a/components/dixa/sources/new-message-added-instant/test-event.mjs b/components/dixa/sources/new-message-added-instant/test-event.mjs new file mode 100644 index 0000000000000..18d41dbe56419 --- /dev/null +++ b/components/dixa/sources/new-message-added-instant/test-event.mjs @@ -0,0 +1,62 @@ +export default { + "event_fqn": "CONVERSATION_MESSAGE_ADDED", + "event_id": "12345678-1234-1234-1234-1234567890", + "event_timestamp": "2025-01-14T22:01:05.429Z", + "event_version": "1", + "organization": { + "id": "12345678-1234-1234-1234-1234567890", + "name": "Org Name" + }, + "data": { + "author": { + "email": "email@dixa.com", + "id": "12345678-1234-1234-1234-1234567890", + "name": "Agent Name", + "phone": "+123456789", + "roles": [], + "user_type": "Member" + }, + "channel": "EMAIL", + "content": { + "text":"Message Text", + "content_type":"Text", + "original_content_url":null, + "processed_content_url":null + }, + "conversation": { + "assignee": { + "email": "email@dixa.com", + "id": "12345678-1234-1234-1234-1234567890", + "name": "Agent Name", + "phone": "+123456789", + "roles": [], + "user_type": "Member" + }, + "channel": "EMAIL", + "contact_point": "contact@email.dixa.io", + "created_at": "2025-01-13T19:48:33.178Z", + "csid": 2, + "direction": "OUTBOUND", + "queue": { + "id": "12345678-1234-1234-1234-1234567890", + "name": "default" + }, + "requester": { + "email": "contact@email.com", + "id": "12345678-1234-1234-1234-1234567890", + "name": "Contact Name", + "phone": null, + "roles": [], + "user_type": "Contact" + }, + "status": "PENDING", + "subject": "Subject Text", + "tags": [] + }, + "created_at":"2025-01-14T23:08:43.187Z", + "direction":"outbound", + "external_id":"null", + "message_id":"12345678-1234-1234-1234-1234567890", + "text":"Message Text", + } +} \ No newline at end of file diff --git a/components/dixa/sources/new-tag-added-instant/new-tag-added-instant.mjs b/components/dixa/sources/new-tag-added-instant/new-tag-added-instant.mjs new file mode 100644 index 0000000000000..4b2c6109c4ef9 --- /dev/null +++ b/components/dixa/sources/new-tag-added-instant/new-tag-added-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "dixa-new-tag-added-instant", + name: "New Tag Added in Conversation (Instant)", + description: "Emit new event when a tag is added to a conversation. [See the documentation](https://docs.dixa.io/openapi/dixa-api/v1/tag/Webhooks/).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return [ + "ConversationTagAdded", + ]; + }, + getSummary({ data }) { + return `Tag "${data.tag}" added to conversation ${data.conversation.csid}`; + }, + }, + sampleEmit, +}; diff --git a/components/dixa/sources/new-tag-added-instant/test-event.mjs b/components/dixa/sources/new-tag-added-instant/test-event.mjs new file mode 100644 index 0000000000000..1bc7dac26796f --- /dev/null +++ b/components/dixa/sources/new-tag-added-instant/test-event.mjs @@ -0,0 +1,57 @@ +export default { + "event_fqn": "CONVERSATION_TAG_ADDED", + "event_id": "12345678-1234-1234-1234-1234567890", + "event_timestamp": "2025-01-14T22:01:05.429Z", + "event_version": "1", + "organization": { + "id": "12345678-1234-1234-1234-1234567890", + "name": "Org Name" + }, + "data": { + "author": { + "email": "email@dixa.com", + "id": "12345678-1234-1234-1234-1234567890", + "name": "Agent Name", + "phone": "+123456789", + "roles": [], + "user_type": "Member" + }, + "tag": "Tag Name", + "conversation": { + "assignee": { + "email": "email@dixa.com", + "id": "12345678-1234-1234-1234-1234567890", + "name": "Agent Name", + "phone": "+123456789", + "roles": [], + "user_type": "Member" + }, + "channel": "EMAIL", + "contact_point": "contact@email.dixa.io", + "created_at": "2025-01-13T19:48:33.178Z", + "csid": 2, + "direction": "OUTBOUND", + "queue": { + "id": "12345678-1234-1234-1234-1234567890", + "name": "default" + }, + "requester": { + "email": "contact@email.com", + "id": "12345678-1234-1234-1234-1234567890", + "name": "Contact Name", + "phone": null, + "roles": [], + "user_type": "Contact" + }, + "status": "PENDING", + "subject": "Subject Text", + "tags": [ + { + "id":"12345678-1234-1234-1234-1234567890", + "name":"Tag Name", + "is_deactivated":false + } + ] + } + } +} \ No newline at end of file diff --git a/components/dnsfilter/README.md b/components/dnsfilter/README.md new file mode 100644 index 0000000000000..d8b9f66041c8c --- /dev/null +++ b/components/dnsfilter/README.md @@ -0,0 +1,11 @@ +# Overview + +The DNSFilter API enables you to automate interactions with your DNSFilter account, offering capabilities like managing filtered domains, accessing reports, updating settings, and more. Within Pipedream, you can wield this API for seamless and serverless integrations, crafting workflows that trigger on specified conditions, process DNSFilter data, and interact with other apps and services, all without managing infrastructure. + +# Example Use Cases + +- **Automate Domain Management**: Sync a list of blocked or allowed domains from a corporate database to DNSFilter, ensuring real-time updates of filtering rules across your network. + +- **Security Alerts Integration**: Create a workflow that listens for threat alerts from DNSFilter. Use this to trigger notifications in messaging platforms like Slack, or log the incidents in a SIEM system like Splunk for further analysis. + +- **Scheduled Reporting**: Set up a Pipedream workflow to generate and retrieve DNSFilter reports on a schedule. These can be sent to Google Sheets for data analysis or emailed to stakeholders for regular updates on network activity. diff --git a/components/dock_certs/README.md b/components/dock_certs/README.md new file mode 100644 index 0000000000000..bfe527a1730e3 --- /dev/null +++ b/components/dock_certs/README.md @@ -0,0 +1,11 @@ +# Overview + +The Dock Certs API provides a means for issuing verifiable credentials and decentralized identities, which are essential for managing digital proofs of various qualifications, memberships, or certifications. Using Pipedream's integration capabilities, you can automate the process of creating, revoking, and verifying these credentials. With Pipedream's serverless architecture, you can set up triggers and actions that respond to events in real-time, orchestrate data flow between Dock Certs and other services, and manage credentials with minimal manual intervention. + +# Example Use Cases + +- **Automate Credential Issuance on User Registration**: When a user completes registration on your platform, automatically issue a verifiable credential using Dock Certs API. This can be integrated with a user management app like Auth0 or a form service like Typeform to trigger the workflow. + +- **Credential Verification on Login**: Verify user credentials during the login process by integrating Dock Certs API with your authentication system. This ensures only users with valid, unrevoked credentials can access certain services or platforms. + +- **Revoke Credentials upon Subscription Cancellation**: When a user cancels their subscription, use the Dock Certs API to revoke their credentials. This workflow could be connected to a payment platform like Stripe to trigger the revocation. diff --git a/components/docker_hub/README.md b/components/docker_hub/README.md new file mode 100644 index 0000000000000..2081d962b5936 --- /dev/null +++ b/components/docker_hub/README.md @@ -0,0 +1,11 @@ +# Overview + +The Docker Hub API allows for programmatic interaction with Docker Hub, enabling you to manage repositories, automate image builds, and work with webhooks and user accounts. On Pipedream, you can harness this API to create workflows that simplify and automate tasks like monitoring repository changes, triggering actions on image pushes, and orchestrating multi-service deployments. + +# Example Use Cases + +- **Automate Image Builds on Code Commit**: Set up a workflow that watches for new commits on your GitHub repository and triggers a new image build on Docker Hub. This ensures your Docker images are always up-to-date with your latest code. + +- **Monitor Docker Hub Repo for New Tags**: Create a workflow that polls your Docker Hub repository for new tags and sends notifications via Slack or email. This can keep your team informed whenever a new version of your image is available. + +- **Sync Docker Hub Repos with Cloud Storage**: Develop a workflow that backs up new or updated images from your Docker Hub repository to cloud storage services like Google Drive or AWS S3, providing an extra layer of redundancy for your container images. diff --git a/components/docker_hub/docker_hub.app.mjs b/components/docker_hub/docker_hub.app.mjs new file mode 100644 index 0000000000000..2a04c10f91ba3 --- /dev/null +++ b/components/docker_hub/docker_hub.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "docker_hub", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/docker_hub/package.json b/components/docker_hub/package.json new file mode 100644 index 0000000000000..174b36caa84ad --- /dev/null +++ b/components/docker_hub/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/docker_hub", + "version": "0.0.1", + "description": "Pipedream Docker Hub Components", + "main": "docker_hub.app.mjs", + "keywords": [ + "pipedream", + "docker_hub" + ], + "homepage": "https://pipedream.com/apps/docker_hub", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/docmosis/README.md b/components/docmosis/README.md new file mode 100644 index 0000000000000..f113c0e3ea12a --- /dev/null +++ b/components/docmosis/README.md @@ -0,0 +1,11 @@ +# Overview + +The Docmosis API lets you generate custom documents based on templates. You can populate these templates with dynamic content and convert them into various formats such as PDF, Word, and HTML. On Pipedream, leveraging the Docmosis API in your workflows allows automated document generation, making it ideal for reports, contracts, invoices, and more. You can trigger workflows with various events and use data from other apps to fill templates. + +# Example Use Cases + +- **Automated Invoice Generation**: When a new order is received in Shopify, you can trigger a Pipedream workflow to collect order data and send it to Docmosis to generate an invoice. The workflow could then email the invoice to the customer and update the order status in Shopify. + +- **Dynamic Contract Creation**: Upon a new lead in Salesforce, you can automatically create a custom contract using Docmosis. The workflow could fill a template with lead data, generate the contract, and upload the signed version back to Salesforce once completed. + +- **Monthly Report Compilation**: At the end of each month, gather data from Google Sheets, process the data in Pipedream, and pass the relevant information to Docmosis to generate a comprehensive report. You might then save this report to Google Drive and notify your team via Slack. diff --git a/components/docmosis/actions/generate-document/generate-document.mjs b/components/docmosis/actions/generate-document/generate-document.mjs new file mode 100644 index 0000000000000..aab992aacc85e --- /dev/null +++ b/components/docmosis/actions/generate-document/generate-document.mjs @@ -0,0 +1,76 @@ +import utils from "../../common/utils.mjs"; +import app from "../../docmosis.app.mjs"; +import { writeFileSync } from "fs"; + +export default { + key: "docmosis-generate-document", + name: "Generate Document", + description: "Generates a document by merging data with a Docmosis template. [See the documentation](https://resources.docmosis.com/Documentation/Cloud/DWS4/Cloud-Web-Services-Guide-DWS4.pdf)", + version: "0.0.1", + type: "action", + props: { + app, + templateName: { + propDefinition: [ + app, + "templateName", + ], + }, + data: { + propDefinition: [ + app, + "data", + ], + }, + outputName: { + propDefinition: [ + app, + "outputName", + ], + }, + outputFormat: { + propDefinition: [ + app, + "outputFormat", + ], + }, + }, + methods: { + render(args = {}) { + return this.app.post({ + path: "/render", + ...args, + }); + }, + }, + async run({ $ }) { + const { + render, + templateName, + outputName, + data, + outputFormat, + } = this; + const downloadedFilepath = `/tmp/${outputName}`; + + const response = await render({ + $, + responseType: "arraybuffer", + data: { + templateName, + data: utils.parseProp(data), + outputName, + outputFormat, + }, + }); + + writeFileSync(downloadedFilepath, response); + + $.export("$summary", `Successfully generated document with name \`${outputName}\``); + + return [ + outputName, + downloadedFilepath, + ]; + }, +}; diff --git a/components/docmosis/common/utils.mjs b/components/docmosis/common/utils.mjs new file mode 100644 index 0000000000000..528b89c0ddaf8 --- /dev/null +++ b/components/docmosis/common/utils.mjs @@ -0,0 +1,36 @@ +function isJson(value) { + if (typeof(value) !== "string") { + return false; + } + + try { + JSON.parse(value); + } catch (e) { + return false; + } + return true; +} + +function parse(value) { + return isJson(value) + ? JSON.parse(value) + : value; +} + +function parseProp(prop) { + if (!prop) { + return; + } + return Object.entries(parse(prop)) + .reduce((acc, [ + key, + value, + ]) => ({ + ...acc, + [key]: parse(value), + }), {}); +} + +export default { + parseProp, +}; diff --git a/components/docmosis/docmosis.app.mjs b/components/docmosis/docmosis.app.mjs index c590c355f67d6..ae6864e834c8f 100644 --- a/components/docmosis/docmosis.app.mjs +++ b/components/docmosis/docmosis.app.mjs @@ -1,11 +1,92 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "docmosis", - propDefinitions: {}, + propDefinitions: { + templateName: { + type: "string", + label: "Template Name", + description: "The name of the template to merge data with", + async options() { + const { templateList } = await this.listTemplates(); + return templateList + .filter(({ name }) => !name.endsWith("/")) + .map(({ name: value }) => value); + }, + }, + data: { + type: "object", + label: "Data To Merge", + description: "The data to be merged with the template", + }, + outputName: { + type: "string", + label: "Output Name", + description: "The name of the generated document. Eg `result.pdf`", + }, + outputFormat: { + type: "string", + label: "Output Format", + description: "The output format of the generated document", + optional: true, + options: [ + "pdf", + "docx", + "odt", + "rtf", + "html", + "txt", + ], + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `${this.$auth.location_base_url}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Content-Type": "application/json", + }; + }, + getAuthData(data) { + return { + ...data, + accessKey: this.$auth.access_key, + }; + }, + async _makeRequest({ + $ = this, path = "", headers, data, ...args + } = {}) { + try { + return await axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + data: this.getAuthData(data), + }); + + } catch (error) { + if (Buffer.isBuffer(error?.response?.data)) { + const decoder = new TextDecoder(); + const msg = decoder.decode(error.response.data); + throw new Error(msg); + } + throw error; + } + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + listTemplates(args = {}) { + return this.post({ + path: "/listTemplates", + ...args, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/docmosis/package.json b/components/docmosis/package.json index b956c41def7e8..720d903dcf780 100644 --- a/components/docmosis/package.json +++ b/components/docmosis/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/docmosis", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Docmosis Components", "main": "docmosis.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "1.6.2" } -} \ No newline at end of file +} diff --git a/components/docnify/actions/add-recipient-to-document/add-recipient-to-document.mjs b/components/docnify/actions/add-recipient-to-document/add-recipient-to-document.mjs new file mode 100644 index 0000000000000..f472fb6b632a1 --- /dev/null +++ b/components/docnify/actions/add-recipient-to-document/add-recipient-to-document.mjs @@ -0,0 +1,42 @@ +import docnify from "../../docnify.app.mjs"; + +export default { + key: "docnify-add-recipient-to-document", + name: "Add Recipient To Document", + description: "Add a recipient to an existing Docnify document. [See the documentation]([See the documentation](https://app.docnify.io/api/v1/openapi))", + version: "0.0.1", + type: "action", + props: { + docnify, + documentId: { + propDefinition: [ + docnify, + "documentId", + ], + }, + name: { + type: "string", + label: "Name", + description: "Name of the recipient", + }, + email: { + type: "string", + label: "Email", + description: "Email address of the recipient", + }, + }, + async run({ $ }) { + const response = await this.docnify.addRecipientToDocument({ + $, + documentId: this.documentId, + data: { + name: this.name, + email: this.email, + role: "SIGNER", + }, + }); + + $.export("$summary", `Successfully added recipient to document ${this.documentId}`); + return response; + }, +}; diff --git a/components/docnify/actions/create-document-from-template/create-document-from-template.mjs b/components/docnify/actions/create-document-from-template/create-document-from-template.mjs new file mode 100644 index 0000000000000..d8d9e16a32a9b --- /dev/null +++ b/components/docnify/actions/create-document-from-template/create-document-from-template.mjs @@ -0,0 +1,54 @@ +import docnify from "../../docnify.app.mjs"; + +export default { + key: "docnify-create-document-from-template", + name: "Create Document From Template", + description: "Create a new document in Docnify from a pre-existing template. [See the documentation](https://app.docnify.io/api/v1/openapi)", + version: "0.0.1", + type: "action", + props: { + docnify, + templateId: { + propDefinition: [ + docnify, + "templateId", + ], + }, + title: { + type: "string", + label: "Title", + description: "Document title. Will override the original title defined in the template.", + optional: true, + }, + subject: { + type: "string", + label: "Subject", + description: "Document subject. Will override the original subject defined in the template.", + optional: true, + }, + message: { + type: "string", + label: "Message", + description: "Document message. Will override the original message defined in the template.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.docnify.createDocumentFromTemplate({ + $, + templateId: this.templateId, + data: { + title: this.title, + recipients: [], + meta: this.subject || this.message + ? { + subject: this.subject, + message: this.message, + } + : undefined, + }, + }); + $.export("$summary", `Successfully created document with ID: ${response.documentId}`); + return response; + }, +}; diff --git a/components/docnify/actions/send-document/send-document.mjs b/components/docnify/actions/send-document/send-document.mjs new file mode 100644 index 0000000000000..61f899d275355 --- /dev/null +++ b/components/docnify/actions/send-document/send-document.mjs @@ -0,0 +1,29 @@ +import docnify from "../../docnify.app.mjs"; + +export default { + key: "docnify-send-document", + name: "Send Document", + description: "Send a document within Docnify for signing. [See the documentation](https://app.docnify.io/api/v1/openapi)", + version: "0.0.1", + type: "action", + props: { + docnify, + documentId: { + propDefinition: [ + docnify, + "documentId", + ], + }, + }, + async run({ $ }) { + const response = await this.docnify.sendDocumentForSigning({ + $, + documentId: this.documentId, + data: { + sendEmail: true, + }, + }); + $.export("$summary", `Document with ID ${this.documentId} sent successfully.`); + return response; + }, +}; diff --git a/components/docnify/docnify.app.mjs b/components/docnify/docnify.app.mjs new file mode 100644 index 0000000000000..e3e9546f24592 --- /dev/null +++ b/components/docnify/docnify.app.mjs @@ -0,0 +1,110 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "docnify", + propDefinitions: { + templateId: { + type: "string", + label: "Template ID", + description: "The ID of the pre-existing template", + async options({ page }) { + const { templates } = await this.listTemplates({ + params: { + page: page + 1, + }, + }); + return templates?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })) || []; + }, + }, + documentId: { + type: "string", + label: "Document ID", + description: "The ID of the document", + async options({ page }) { + const { documents } = await this.listDocuments({ + params: { + page: page + 1, + }, + }); + return documents?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return `${this.$auth.url}/api/v1`; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `${this.$auth.api_token}`, + }, + }); + }, + listTemplates(opts = {}) { + return this._makeRequest({ + path: "/templates", + ...opts, + }); + }, + listDocuments(opts = {}) { + return this._makeRequest({ + path: "/documents", + ...opts, + }); + }, + getDocument({ + documentId, ...opts + }) { + return this._makeRequest({ + path: `/documents/${documentId}`, + ...opts, + }); + }, + createDocumentFromTemplate({ + templateId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/templates/${templateId}/generate-document`, + ...opts, + }); + }, + sendDocumentForSigning({ + documentId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/documents/${documentId}/send`, + ...opts, + }); + }, + addRecipientToDocument({ + documentId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/documents/${documentId}/recipients`, + ...opts, + }); + }, + }, +}; diff --git a/components/docnify/package.json b/components/docnify/package.json new file mode 100644 index 0000000000000..4bc137a96cb13 --- /dev/null +++ b/components/docnify/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/docnify", + "version": "0.1.0", + "description": "Pipedream Docnify Components", + "main": "docnify.app.mjs", + "keywords": [ + "pipedream", + "docnify" + ], + "homepage": "https://pipedream.com/apps/docnify", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} diff --git a/components/docnify/sources/common/base.mjs b/components/docnify/sources/common/base.mjs new file mode 100644 index 0000000000000..be7a1abbefa61 --- /dev/null +++ b/components/docnify/sources/common/base.mjs @@ -0,0 +1,63 @@ +import docnify from "../../docnify.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + docnify, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + isRelevant() { + return true; + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run() { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + const tsField = this.getTsField(); + + const docs = []; + let total; + const params = { + page: 1, + }; + do { + const { documents } = await this.docnify.listDocuments({ + params, + }); + for (const doc of documents) { + const ts = Date.parse(doc[tsField]); + if (ts >= lastTs) { + if (await this.isRelevant(doc, lastTs)) { + docs.push(doc); + } + maxTs = Math.max(ts, maxTs); + } + } + total = documents?.length; + params.page++; + } while (total > 0); + + for (const doc of docs) { + const meta = await this.generateMeta(doc); + this.$emit(doc, meta); + } + + this._setLastTs(maxTs); + }, +}; diff --git a/components/docnify/sources/new-document-completed/new-document-completed.mjs b/components/docnify/sources/new-document-completed/new-document-completed.mjs new file mode 100644 index 0000000000000..203e833f06f1d --- /dev/null +++ b/components/docnify/sources/new-document-completed/new-document-completed.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "docnify-new-document-completed", + name: "New Document Completed", + description: "Emit new event when a document is signed by all recipients.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTsField() { + return "updatedAt"; + }, + isRelevant(doc) { + return doc.status === "COMPLETED"; + }, + generateMeta(doc) { + return { + id: doc.id, + summary: `New Document Completed: ${doc.id}`, + ts: Date.parse(doc[this.getTsField()]), + }; + }, + }, + sampleEmit, +}; diff --git a/components/docnify/sources/new-document-completed/test-event.mjs b/components/docnify/sources/new-document-completed/test-event.mjs new file mode 100644 index 0000000000000..887d4c07c0ca0 --- /dev/null +++ b/components/docnify/sources/new-document-completed/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "id": 21983, + "externalId": null, + "userId": 6780, + "teamId": null, + "title": "file-sample_150kB.pdf", + "status": "COMPLETED", + "documentDataId": "cm0eaghs2002nl70cza7g41z1", + "createdAt": "2024-08-28T20:08:22.289Z", + "updatedAt": "2024-08-28T20:24:03.342Z", + "completedAt": "2024-08-28T20:24:03.342Z" +} \ No newline at end of file diff --git a/components/docnify/sources/new-document-created/new-document-created.mjs b/components/docnify/sources/new-document-created/new-document-created.mjs new file mode 100644 index 0000000000000..d47a60833c6d9 --- /dev/null +++ b/components/docnify/sources/new-document-created/new-document-created.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "docnify-new-document-created", + name: "New Document Created", + description: "Emit new event when a new document is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTsField() { + return "createdAt"; + }, + generateMeta(doc) { + return { + id: doc.id, + summary: `New Document Created: ${doc.id}`, + ts: Date.parse(doc[this.getTsField()]), + }; + }, + }, + sampleEmit, +}; diff --git a/components/docnify/sources/new-document-created/test-event.mjs b/components/docnify/sources/new-document-created/test-event.mjs new file mode 100644 index 0000000000000..db4b18198f30b --- /dev/null +++ b/components/docnify/sources/new-document-created/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "id": 21983, + "externalId": null, + "userId": 6780, + "teamId": null, + "title": "file-sample_150kB.pdf", + "status": "PENDING", + "documentDataId": "cm0eaghs2002nl70cza7g41z1", + "createdAt": "2024-08-28T20:08:22.289Z", + "updatedAt": "2024-08-28T20:24:03.342Z", + "completedAt": null +} \ No newline at end of file diff --git a/components/docnify/sources/new-document-signed/new-document-signed.mjs b/components/docnify/sources/new-document-signed/new-document-signed.mjs new file mode 100644 index 0000000000000..f1e49a145df28 --- /dev/null +++ b/components/docnify/sources/new-document-signed/new-document-signed.mjs @@ -0,0 +1,39 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "docnify-new-document-signed", + name: "New Document Signed", + description: "Emit new event when a document is signed by a recipient", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTsField() { + return "updatedAt"; + }, + async isRelevant(doc, lastTs) { + const { recipients } = await this.docnify.getDocument({ + documentId: doc.id, + }); + const recentlySigned = recipients + ?.filter(({ signedAt }) => signedAt && Date.parse(signedAt) >= lastTs); + return !!recentlySigned?.length; + }, + async generateMeta(doc) { + const { recipients } = await this.docnify.getDocument({ + documentId: doc.id, + }); + const signedAts = recipients?.map(({ signedAt }) => Date.parse(signedAt)); + const ts = Math.max(...signedAts); + return { + id: `${doc.id}-${ts}`, + summary: `New Document Signed: ${doc.id}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/docnify/sources/new-document-signed/test-event.mjs b/components/docnify/sources/new-document-signed/test-event.mjs new file mode 100644 index 0000000000000..db4b18198f30b --- /dev/null +++ b/components/docnify/sources/new-document-signed/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "id": 21983, + "externalId": null, + "userId": 6780, + "teamId": null, + "title": "file-sample_150kB.pdf", + "status": "PENDING", + "documentDataId": "cm0eaghs2002nl70cza7g41z1", + "createdAt": "2024-08-28T20:08:22.289Z", + "updatedAt": "2024-08-28T20:24:03.342Z", + "completedAt": null +} \ No newline at end of file diff --git a/components/docparser/README.md b/components/docparser/README.md index abd967febd63b..b51af916542e3 100644 --- a/components/docparser/README.md +++ b/components/docparser/README.md @@ -1,12 +1,9 @@ # Overview -With Docparser, you can easily extract data from PDF documents and convert them -into usable formats such as JSON, CSV or XML. This way you can easily get the -data you need from PDF documents without having to manually copy and paste it. +Docparser is a tool for extracting data from documents, such as PDFs, Word, and images. With the Docparser API, you can automate the process of capturing data without manual entry, transforming documents into actionable information. It shines in scenarios where structured information needs to be pulled from files that typically require manual data entry, such as invoices, forms, and reports. -Here are some examples of what you can build using the Docparser API: +# Example Use Cases -- A tool to extract data from PDF documents and convert it into JSON, CSV or - XML -- A tool to automate data entry by extracting data from PDF documents -- A tool to track data changes over time by extracting data from PDF documents +- **Invoice Processing Workflow**: Automate invoice data extraction by sending PDF invoices from an email to Docparser via Pipedream. Then, use Pipedream to pass the structured data to an accounting app like QuickBooks for invoice creation and archiving. +- **Employee Onboarding Documents**: Streamline the onboarding process by extracting details from new hire documents. When a document is uploaded to a cloud storage service, trigger a Pipedream workflow that uses Docparser to extract personal information and populate it in an HR management system like BambooHR. +- **Monthly Report Analysis**: Set up a Pipedream workflow to process monthly sales reports. Docparser can extract sales data from reports, which Pipedream then sends to a data visualization tool like Google Sheets or Tableau for tracking trends and generating insights. diff --git a/components/docparser/package.json b/components/docparser/package.json new file mode 100644 index 0000000000000..fd4c035efdd74 --- /dev/null +++ b/components/docparser/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/docparser", + "version": "0.6.0", + "description": "Pipedream docparser Components", + "main": "docparser.app.mjs", + "keywords": [ + "pipedream", + "docparser" + ], + "homepage": "https://pipedream.com/apps/docparser", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/docraptor/README.md b/components/docraptor/README.md new file mode 100644 index 0000000000000..a1da2afd3652a --- /dev/null +++ b/components/docraptor/README.md @@ -0,0 +1,11 @@ +# Overview + +DocRaptor is an API that converts HTML to PDF or XLS(X). Using DocRaptor with Pipedream allows you to automate document generation within your custom workflows. With Pipedream's serverless platform, you can trigger document creation from a multitude of events, process the data, and integrate it with countless apps to create dynamic, on-demand documents. Whether you need to generate invoices, reports, or any other documents, you can set up a pipeline that does the heavy lifting for you. + +# Example Use Cases + +- **Generate Monthly Reports**: Automatically create a PDF report from data in a Google Sheets document. Each month, trigger the workflow on Pipedream that fetches the latest data from Google Sheets, uses DocRaptor to generate a PDF, and then emails the report to stakeholders using a service like SendGrid. + +- **Invoice Creation on New Orders**: When a new order is placed in an eCommerce platform like Shopify, trigger a workflow on Pipedream that creates a custom invoice using HTML/CSS templates. DocRaptor then converts this template into a PDF, which can be automatically sent to the customer or stored in cloud services like Dropbox. + +- **Event-driven Financial Statements**: On the closing of a financial period, a webhook can trigger a Pipedream workflow that collates financial data, possibly from a database or accounting software like QuickBooks. With this data, generate an HTML template and use DocRaptor to transform it into a formatted XLSX financial statement ready for distribution or further analysis. diff --git a/components/docsautomator/actions/create-document/create-document.mjs b/components/docsautomator/actions/create-document/create-document.mjs new file mode 100644 index 0000000000000..7c844d7dd4717 --- /dev/null +++ b/components/docsautomator/actions/create-document/create-document.mjs @@ -0,0 +1,58 @@ +import { parseObject } from "../../common/utils.mjs"; +import docsautomator from "../../docsautomator.app.mjs"; + +export default { + key: "docsautomator-create-document", + name: "Create Document", + description: "Generate a new document from a pre-existing template. [See the documentation](https://docs.docsautomator.co/integrations-api/docsautomator-api)", + version: "0.0.1", + type: "action", + props: { + docsautomator, + automationId: { + propDefinition: [ + docsautomator, + "automationId", + ], + }, + documentName: { + propDefinition: [ + docsautomator, + "documentName", + ], + }, + recId: { + propDefinition: [ + docsautomator, + "recId", + ], + }, + taskId: { + propDefinition: [ + docsautomator, + "taskId", + ], + }, + data: { + propDefinition: [ + docsautomator, + "data", + ], + }, + }, + async run({ $ }) { + const response = await this.docsautomator.createDocument({ + $, + data: { + docId: this.automationId, + documentName: this.documentName, + recId: this.recId, + taskId: this.taskId, + data: parseObject(this.data), + }, + }); + + $.export("$summary", `Successfully created document with automation ID ${this.automationId}`); + return response; + }, +}; diff --git a/components/docsautomator/common/utils.mjs b/components/docsautomator/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/docsautomator/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/docsautomator/docsautomator.app.mjs b/components/docsautomator/docsautomator.app.mjs new file mode 100644 index 0000000000000..797c95f461d3e --- /dev/null +++ b/components/docsautomator/docsautomator.app.mjs @@ -0,0 +1,79 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "docsautomator", + propDefinitions: { + automationId: { + type: "string", + label: "Automation ID", + description: "The ID of the automation to use for generating the document", + async options() { + const { automations } = await this.listAutomations(); + + return automations.map(({ + title: label, _id: value, + }) => ({ + label, + value, + })); + }, + }, + documentName: { + type: "string", + label: "Document Name", + description: "The name of the generated document", + optional: true, + }, + recId: { + type: "string", + label: "Record ID", + description: "Record ID for Airtable (Airtable only — data not required)", + optional: true, + }, + taskId: { + type: "string", + label: "Task ID", + description: "Task ID for ClickUp (ClickUp only — data not required)", + optional: true, + }, + data: { + type: "object", + label: "Data", + description: "Placeholders data for the template. [See the documentation](https://docs.docsautomator.co/integrations-api/docsautomator-api) for further information.", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.docsautomator.co"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listAutomations(opts = {}) { + return this._makeRequest({ + ...opts, + path: "/automations", + }); + }, + createDocument(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/createDocument", + ...opts, + }); + }, + }, +}; diff --git a/components/docsautomator/package.json b/components/docsautomator/package.json new file mode 100644 index 0000000000000..b64b439ead41e --- /dev/null +++ b/components/docsautomator/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/docsautomator", + "version": "0.1.0", + "description": "Pipedream DocsAutomator Components", + "main": "docsautomator.app.mjs", + "keywords": [ + "pipedream", + "docsautomator" + ], + "homepage": "https://pipedream.com/apps/docsautomator", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} + diff --git a/components/docsbot_ai/README.md b/components/docsbot_ai/README.md new file mode 100644 index 0000000000000..4f90f10dfe90a --- /dev/null +++ b/components/docsbot_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +DocsBot AI API transforms natural language into structured data, enabling the extraction and analysis of information from unstructured text. In Pipedream, you can leverage this API to automate workflows that require processing of text for sentiment analysis, summarization, or data extraction. With Pipedream's serverless platform, these capabilities can be integrated into diverse systems without writing complex infrastructure code. + +# Example Use Cases + +- **Customer Feedback Analysis**: Automatically process customer feedback by connecting DocsBot AI with a CRM like Salesforce. Extract sentiments and categorize feedback to help improve products and services. + +- **Content Summarization**: Streamline content curation by summarizing articles or documents fetched from sources like RSS feeds or cloud storage platforms. Use DocsBot AI to create concise summaries and store them in a database like Airtable for easy access and reference. + +- **Data Extraction from Support Tickets**: Integrate DocsBot AI with a support ticketing system like Zendesk. Extract key information from tickets and route it to project management tools such as Jira or Trello, helping teams to quickly address customer issues. diff --git a/components/docsgenflow/README.md b/components/docsgenflow/README.md new file mode 100644 index 0000000000000..12a89b39a9293 --- /dev/null +++ b/components/docsgenflow/README.md @@ -0,0 +1,11 @@ +# Overview + +The DocsGenFlow API enables the automation of document generation and management. Leveraging this API on Pipedream allows users to create dynamic workflows that connect various apps to generate, update, retrieve, and manage documents based on specific triggers and conditions. This could be particularly useful in scenarios involving contract management, report generation, or automated document distribution where document templates can be dynamically populated with data from other sources. + +# Example Use Cases + +- **Automated Contract Generation**: Trigger a workflow in Pipedream when a new client is added to a CRM like Salesforce. Utilize the DocsGenFlow API to populate a contract template with client details from Salesforce and then send the generated contract to the client via email using SendGrid. + +- **Monthly Report Automation**: Set up a scheduled workflow in Pipedream to pull data monthly from a database or a platform like Google Sheets. Use DocsGenFlow to generate a monthly performance report from this data, and distribute it through Slack to specified channels or team members. + +- **Dynamic Invoice Creation**: Integrate DocsGenFlow with an e-commerce platform like Shopify. Whenever a new order is placed, trigger a workflow in Pipedream that uses DocsGenFlow to generate an invoice based on the order details. This invoice can then be automatically emailed to the customer and stored in a cloud service like Google Drive for record-keeping. diff --git a/components/docsgenflow/docsgenflow.app.mjs b/components/docsgenflow/docsgenflow.app.mjs new file mode 100644 index 0000000000000..32e7c7fd9f421 --- /dev/null +++ b/components/docsgenflow/docsgenflow.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "docsgenflow", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/docsgenflow/package.json b/components/docsgenflow/package.json new file mode 100644 index 0000000000000..b9dcfeb744e6f --- /dev/null +++ b/components/docsgenflow/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/docsgenflow", + "version": "0.0.1", + "description": "Pipedream DocsGenFlow Components", + "main": "docsgenflow.app.mjs", + "keywords": [ + "pipedream", + "docsgenflow" + ], + "homepage": "https://pipedream.com/apps/docsgenflow", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/docsumo/README.md b/components/docsumo/README.md new file mode 100644 index 0000000000000..ee62b753a3f54 --- /dev/null +++ b/components/docsumo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Docsumo API offers robust capabilities for extracting and processing data from various types of documents, like invoices, receipts, and bank statements. It applies intelligent OCR (Optical Character Recognition) to transform unstructured documents into structured data. With Pipedream, you can harness this power to automate workflows, such as digitizing financial records, validating document data, or integrating extracted information into databases or other apps, all with minimal code. + +# Example Use Cases + +- **Automated Invoice Processing**: Extract data from uploaded invoices using Docsumo, then use Pipedream to send the structured data to accounting software like QuickBooks for streamlined bookkeeping. + +- **Expense Management Automation**: Capture receipt data with the Docsumo API and automatically create expense records in a platform such as Expensify or a Google Sheet, facilitating quick expense reporting and reimbursement. + +- **Bank Statement Reconciliation**: Use Docsumo to parse bank statements and match transactions with records in your accounting system. This can be done by integrating the Docsumo API with Pipedream workflows that reconcile transactions in apps like Xero. diff --git a/components/docugenerate/README.md b/components/docugenerate/README.md new file mode 100644 index 0000000000000..edbdb54fec2df --- /dev/null +++ b/components/docugenerate/README.md @@ -0,0 +1,11 @@ +# Overview + +The DocuGenerate API lets you automate document creation and management tasks within Pipedream workflows. With this API, you can create custom documents, populate them with dynamic data, and perform various actions like retrieving, updating, or deleting documents programmatically. Integrating DocuGenerate with Pipedream enables you to connect your document workflows with hundreds of other apps, simplifying processes like contract generation from CRM data, automating report distribution, or even pushing notifications based on document status changes. + +# Example Use Cases + +- **CRM to Contract Generation**: Trigger a workflow whenever a new deal is closed in your CRM (like Salesforce). Fetch the deal details and use the DocuGenerate API to create a personalized contract, which is then sent to the client for e-signature. + +- **Automated Report Creation and Distribution**: Schedule a workflow to run weekly, gathering data from tools such as Google Sheets or a database. Use this data to create a tailored report via DocuGenerate and subsequently email the report to a list of stakeholders using an email service like SendGrid. + +- **Dynamic Invoice Creation from E-Commerce Platforms**: When a new order is placed on an e-commerce platform (like Shopify), trigger a Pipedream workflow that creates an invoice through the DocuGenerate API. Then, archive the invoice in cloud storage like Google Drive and update the order status within the e-commerce platform. diff --git a/components/docugenerate/package.json b/components/docugenerate/package.json index 3967ab8a0ea98..58227e2d13194 100644 --- a/components/docugenerate/package.json +++ b/components/docugenerate/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/documenso/actions/add-recipient-to-document/add-recipient-to-document.mjs b/components/documenso/actions/add-recipient-to-document/add-recipient-to-document.mjs new file mode 100644 index 0000000000000..76b1493d54f32 --- /dev/null +++ b/components/documenso/actions/add-recipient-to-document/add-recipient-to-document.mjs @@ -0,0 +1,42 @@ +import documenso from "../../documenso.app.mjs"; + +export default { + key: "documenso-add-recipient-to-document", + name: "Add Recipient To Document", + description: "Add a recipient to an existing Documenso document. [See the documentation]([See the documentation](https://app.documenso.com/api/v1/openapi))", + version: "0.0.1", + type: "action", + props: { + documenso, + documentId: { + propDefinition: [ + documenso, + "documentId", + ], + }, + name: { + type: "string", + label: "Name", + description: "Name of the recipient", + }, + email: { + type: "string", + label: "Email", + description: "Email address of the recipient", + }, + }, + async run({ $ }) { + const response = await this.documenso.addRecipientToDocument({ + $, + documentId: this.documentId, + data: { + name: this.name, + email: this.email, + role: "SIGNER", + }, + }); + + $.export("$summary", `Successfully added recipient to document ${this.documentId}`); + return response; + }, +}; diff --git a/components/documenso/actions/create-document-from-template/create-document-from-template.mjs b/components/documenso/actions/create-document-from-template/create-document-from-template.mjs new file mode 100644 index 0000000000000..acc96aef48fec --- /dev/null +++ b/components/documenso/actions/create-document-from-template/create-document-from-template.mjs @@ -0,0 +1,54 @@ +import documenso from "../../documenso.app.mjs"; + +export default { + key: "documenso-create-document-from-template", + name: "Create Document From Template", + description: "Create a new document in Documenso from a pre-existing template. [See the documentation](https://app.documenso.com/api/v1/openapi)", + version: "0.0.1", + type: "action", + props: { + documenso, + templateId: { + propDefinition: [ + documenso, + "templateId", + ], + }, + title: { + type: "string", + label: "Title", + description: "Document title. Will override the original title defined in the template.", + optional: true, + }, + subject: { + type: "string", + label: "Subject", + description: "Document subject. Will override the original subject defined in the template.", + optional: true, + }, + message: { + type: "string", + label: "Message", + description: "Document message. Will override the original message defined in the template.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.documenso.createDocumentFromTemplate({ + $, + templateId: this.templateId, + data: { + title: this.title, + recipients: [], + meta: this.subject || this.message + ? { + subject: this.subject, + message: this.message, + } + : undefined, + }, + }); + $.export("$summary", `Successfully created document with ID: ${response.documentId}`); + return response; + }, +}; diff --git a/components/documenso/actions/send-document/send-document.mjs b/components/documenso/actions/send-document/send-document.mjs new file mode 100644 index 0000000000000..32aad3ef4e6de --- /dev/null +++ b/components/documenso/actions/send-document/send-document.mjs @@ -0,0 +1,29 @@ +import documenso from "../../documenso.app.mjs"; + +export default { + key: "documenso-send-document", + name: "Send Document", + description: "Send a document within Documenso for signing. [See the documentation](https://app.documenso.com/api/v1/openapi)", + version: "0.0.1", + type: "action", + props: { + documenso, + documentId: { + propDefinition: [ + documenso, + "documentId", + ], + }, + }, + async run({ $ }) { + const response = await this.documenso.sendDocumentForSigning({ + $, + documentId: this.documentId, + data: { + sendEmail: true, + }, + }); + $.export("$summary", `Document with ID ${this.documentId} sent successfully.`); + return response; + }, +}; diff --git a/components/documenso/documenso.app.mjs b/components/documenso/documenso.app.mjs new file mode 100644 index 0000000000000..dfcceb75127ed --- /dev/null +++ b/components/documenso/documenso.app.mjs @@ -0,0 +1,110 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "documenso", + propDefinitions: { + templateId: { + type: "string", + label: "Template ID", + description: "The ID of the pre-existing template", + async options({ page }) { + const { templates } = await this.listTemplates({ + params: { + page: page + 1, + }, + }); + return templates?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })) || []; + }, + }, + documentId: { + type: "string", + label: "Document ID", + description: "The ID of the document", + async options({ page }) { + const { documents } = await this.listDocuments({ + params: { + page: page + 1, + }, + }); + return documents?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return `${this.$auth.url}/api/v1`; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `${this.$auth.api_token}`, + }, + }); + }, + listTemplates(opts = {}) { + return this._makeRequest({ + path: "/templates", + ...opts, + }); + }, + listDocuments(opts = {}) { + return this._makeRequest({ + path: "/documents", + ...opts, + }); + }, + getDocument({ + documentId, ...opts + }) { + return this._makeRequest({ + path: `/documents/${documentId}`, + ...opts, + }); + }, + createDocumentFromTemplate({ + templateId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/templates/${templateId}/generate-document`, + ...opts, + }); + }, + sendDocumentForSigning({ + documentId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/documents/${documentId}/send`, + ...opts, + }); + }, + addRecipientToDocument({ + documentId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/documents/${documentId}/recipients`, + ...opts, + }); + }, + }, +}; diff --git a/components/documenso/package.json b/components/documenso/package.json new file mode 100644 index 0000000000000..6abc48575dbfd --- /dev/null +++ b/components/documenso/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/documenso", + "version": "0.1.0", + "description": "Pipedream Documenso Components", + "main": "documenso.app.mjs", + "keywords": [ + "pipedream", + "documenso" + ], + "homepage": "https://pipedream.com/apps/documenso", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} diff --git a/components/documenso/sources/common/base.mjs b/components/documenso/sources/common/base.mjs new file mode 100644 index 0000000000000..249b35ab5f4a9 --- /dev/null +++ b/components/documenso/sources/common/base.mjs @@ -0,0 +1,63 @@ +import documenso from "../../documenso.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + documenso, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + isRelevant() { + return true; + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run() { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + const tsField = this.getTsField(); + + const docs = []; + let total; + const params = { + page: 1, + }; + do { + const { documents } = await this.documenso.listDocuments({ + params, + }); + for (const doc of documents) { + const ts = Date.parse(doc[tsField]); + if (ts >= lastTs) { + if (await this.isRelevant(doc, lastTs)) { + docs.push(doc); + } + maxTs = Math.max(ts, maxTs); + } + } + total = documents?.length; + params.page++; + } while (total > 0); + + for (const doc of docs) { + const meta = await this.generateMeta(doc); + this.$emit(doc, meta); + } + + this._setLastTs(maxTs); + }, +}; diff --git a/components/documenso/sources/new-document-completed/new-document-completed.mjs b/components/documenso/sources/new-document-completed/new-document-completed.mjs new file mode 100644 index 0000000000000..4e1645e919ffe --- /dev/null +++ b/components/documenso/sources/new-document-completed/new-document-completed.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "documenso-new-document-completed", + name: "New Document Completed", + description: "Emit new event when a document is signed by all recipients.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTsField() { + return "updatedAt"; + }, + isRelevant(doc) { + return doc.status === "COMPLETED"; + }, + generateMeta(doc) { + return { + id: doc.id, + summary: `New Document Completed: ${doc.id}`, + ts: Date.parse(doc[this.getTsField()]), + }; + }, + }, + sampleEmit, +}; diff --git a/components/documenso/sources/new-document-completed/test-event.mjs b/components/documenso/sources/new-document-completed/test-event.mjs new file mode 100644 index 0000000000000..887d4c07c0ca0 --- /dev/null +++ b/components/documenso/sources/new-document-completed/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "id": 21983, + "externalId": null, + "userId": 6780, + "teamId": null, + "title": "file-sample_150kB.pdf", + "status": "COMPLETED", + "documentDataId": "cm0eaghs2002nl70cza7g41z1", + "createdAt": "2024-08-28T20:08:22.289Z", + "updatedAt": "2024-08-28T20:24:03.342Z", + "completedAt": "2024-08-28T20:24:03.342Z" +} \ No newline at end of file diff --git a/components/documenso/sources/new-document-created/new-document-created.mjs b/components/documenso/sources/new-document-created/new-document-created.mjs new file mode 100644 index 0000000000000..ae9456957a4e2 --- /dev/null +++ b/components/documenso/sources/new-document-created/new-document-created.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "documenso-new-document-created", + name: "New Document Created", + description: "Emit new event when a new document is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTsField() { + return "createdAt"; + }, + generateMeta(doc) { + return { + id: doc.id, + summary: `New Document Created: ${doc.id}`, + ts: Date.parse(doc[this.getTsField()]), + }; + }, + }, + sampleEmit, +}; diff --git a/components/documenso/sources/new-document-created/test-event.mjs b/components/documenso/sources/new-document-created/test-event.mjs new file mode 100644 index 0000000000000..db4b18198f30b --- /dev/null +++ b/components/documenso/sources/new-document-created/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "id": 21983, + "externalId": null, + "userId": 6780, + "teamId": null, + "title": "file-sample_150kB.pdf", + "status": "PENDING", + "documentDataId": "cm0eaghs2002nl70cza7g41z1", + "createdAt": "2024-08-28T20:08:22.289Z", + "updatedAt": "2024-08-28T20:24:03.342Z", + "completedAt": null +} \ No newline at end of file diff --git a/components/documenso/sources/new-document-signed/new-document-signed.mjs b/components/documenso/sources/new-document-signed/new-document-signed.mjs new file mode 100644 index 0000000000000..87c7ddc151b71 --- /dev/null +++ b/components/documenso/sources/new-document-signed/new-document-signed.mjs @@ -0,0 +1,39 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "documenso-new-document-signed", + name: "New Document Signed", + description: "Emit new event when a document is signed by a recipient", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTsField() { + return "updatedAt"; + }, + async isRelevant(doc, lastTs) { + const { recipients } = await this.documenso.getDocument({ + documentId: doc.id, + }); + const recentlySigned = recipients + ?.filter(({ signedAt }) => signedAt && Date.parse(signedAt) >= lastTs); + return !!recentlySigned?.length; + }, + async generateMeta(doc) { + const { recipients } = await this.documenso.getDocument({ + documentId: doc.id, + }); + const signedAts = recipients?.map(({ signedAt }) => Date.parse(signedAt)); + const ts = Math.max(...signedAts); + return { + id: `${doc.id}-${ts}`, + summary: `New Document Signed: ${doc.id}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/documenso/sources/new-document-signed/test-event.mjs b/components/documenso/sources/new-document-signed/test-event.mjs new file mode 100644 index 0000000000000..db4b18198f30b --- /dev/null +++ b/components/documenso/sources/new-document-signed/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "id": 21983, + "externalId": null, + "userId": 6780, + "teamId": null, + "title": "file-sample_150kB.pdf", + "status": "PENDING", + "documentDataId": "cm0eaghs2002nl70cza7g41z1", + "createdAt": "2024-08-28T20:08:22.289Z", + "updatedAt": "2024-08-28T20:24:03.342Z", + "completedAt": null +} \ No newline at end of file diff --git a/components/document360/README.md b/components/document360/README.md new file mode 100644 index 0000000000000..565976a53493d --- /dev/null +++ b/components/document360/README.md @@ -0,0 +1,11 @@ +# Overview + +The Document360 API enables programmatic interaction with your knowledge base, allowing you to automate content creation, updates, and management. With this API, you can integrate Document360 into your existing content pipelines, support workflows, or any system that needs to push or pull knowledge base content. On Pipedream, you can harness this API to craft serverless workflows that seamlessly integrate with other apps to keep documentation up-to-date, trigger notifications based on content changes, or analyze knowledge base usage. + +# Example Use Cases + +- **Automated Knowledge Base Article Updates**: Keep your documentation fresh by automating the update process. When a relevant ticket is resolved in a connected system, such as Zendesk or Jira, use a Pipedream workflow to update related articles in Document360 with the latest information. + +- **Content Publishing from Git Pushes**: Automatically pull the latest documentation from your Git repository and publish to Document360 whenever a new push to the repo is made. This ensures that your knowledge base reflects the most recent changes without manual intervention. + +- **Knowledge Base Analytics Reporting**: Channel knowledge base analytics into a data visualization tool like Google Data Studio. A Pipedream workflow can trigger on a schedule to fetch analytics from Document360 and send them to Data Studio, offering regular insights into how your content is performing. diff --git a/components/documenterra/actions/create-page/create-page.mjs b/components/documenterra/actions/create-page/create-page.mjs new file mode 100644 index 0000000000000..12e17221ec184 --- /dev/null +++ b/components/documenterra/actions/create-page/create-page.mjs @@ -0,0 +1,101 @@ +import app from "../../documenterra.app.mjs"; + +export default { + key: "documenterra-create-page", + name: "Create Page", + description: "Creates a new page. [See the documentation](https://documenterra.ru/docs/user-manual/api-sozdaniye-stranitsy.html)", + version: "0.0.1", + type: "action", + props: { + app, + projectId: { + propDefinition: [ + app, + "projectId", + ], + }, + id: { + type: "string", + label: "Page ID", + description: "The unique identifier for the page.", + }, + assigneeUserName: { + type: "string", + label: "Assignee User Name", + description: "Login of the artist to whom the page is assigned.", + }, + ownerUserName: { + type: "string", + label: "Owner User Name", + description: "The name of the user who is the owner of the page.", + }, + statusName: { + type: "string", + label: "Status Name", + description: "Page workflow status.", + options: [ + "Ready", + "Draft", + "Under Review", + ], + }, + title: { + type: "string", + label: "Page Title", + description: "The title of the page.", + optional: true, + }, + bodyHtml: { + type: "string", + label: "Body HTML", + description: "HTML content of the page.", + optional: true, + }, + indexKeywords: { + type: "string[]", + label: "Index Keywords", + description: "An array of strings containing keywords to be used when creating the page.", + optional: true, + }, + }, + methods: { + createPage({ + projectId, ...args + } = {}) { + return this.app.post({ + path: `/projects/${projectId}/articles`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + createPage, + projectId, + assigneeUserName, + id, + ownerUserName, + statusName, + title, + bodyHtml, + indexKeywords, + } = this; + + const response = await createPage({ + $, + projectId, + data: { + assigneeUserName, + id, + ownerUserName, + statusName, + title, + bodyHtml, + indexKeywords, + }, + }); + + $.export("$summary", `Successfully created a new page with ID: \`${response?.id}\`.`); + return response; + }, +}; diff --git a/components/documenterra/actions/create-project-backup/create-project-backup.mjs b/components/documenterra/actions/create-project-backup/create-project-backup.mjs new file mode 100644 index 0000000000000..612a8564b1d4d --- /dev/null +++ b/components/documenterra/actions/create-project-backup/create-project-backup.mjs @@ -0,0 +1,45 @@ +import app from "../../documenterra.app.mjs"; + +export default { + key: "documenterra-create-project-backup", + name: "Create Project Backup", + description: "Creates a backup of a project. [See the documentation](https://documenterra.ru/docs/user-manual/api-sozdaniye-rezervnoy-kopii-proyekta.html)", + version: "0.0.1", + type: "action", + props: { + app, + projectId: { + propDefinition: [ + app, + "projectId", + ], + }, + outputFileName: { + propDefinition: [ + app, + "outputFileName", + ], + }, + }, + async run({ $ }) { + const { + app, + projectId, + outputFileName, + } = this; + + const response = await app.createProject({ + $, + projectId, + params: { + action: "download", + }, + data: { + outputFileName, + }, + }); + + $.export("$summary", `Successfully created project backup with taskKey: \`${response?.taskKey}\`.`); + return response; + }, +}; diff --git a/components/documenterra/actions/export-publication/export-publication.mjs b/components/documenterra/actions/export-publication/export-publication.mjs new file mode 100644 index 0000000000000..7d3edc0ddca70 --- /dev/null +++ b/components/documenterra/actions/export-publication/export-publication.mjs @@ -0,0 +1,83 @@ +import app from "../../documenterra.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "documenterra-export-publication", + name: "Export Publication", + description: "Exports an existing publication to a user-selected format. [See the documentation](https://documenterra.ru/docs/user-manual/api-eksport-publikatsii.html)", + version: "0.0.1", + type: "action", + props: { + app, + projectId: { + label: "Publication ID", + description: "The unique identifier for the publication.", + propDefinition: [ + app, + "projectId", + () => ({ + filter: ({ parentId }) => !!parentId, + }), + ], + }, + outputFileName: { + description: "The full name of the output file. If the output file is saved to Documenterra's file storage (the default), the full file name must be specified, including the file path starting with `Storage/` Eg. `Storage/export-files/deep-space-1.0-docs.zip`.", + propDefinition: [ + app, + "outputFileName", + ], + }, + format: { + type: "string", + label: "Format", + description: "The format of the publication to be exported.", + options: constants.PRINTED_FORMAT_OPTIONS.concat(constants.OTHER_FORMAT_OPTIONS), + reloadProps: true, + }, + }, + additionalProps() { + const { + app, + format, + } = this; + + const isPrintedFormat = constants.PRINTED_FORMAT_OPTIONS.some(({ value }) => value === format); + + if (!isPrintedFormat) { + return {}; + } + return { + exportPresetName: { + type: "string", + label: "Export Preset Name", + description: `Full name of the Export Preset used. [See the settings page here](${app.getBaseUrl()}/settings/export-presets)`, + default: "Default", + }, + }; + }, + async run({ $ }) { + const { + app, + projectId, + format, + outputFileName, + exportPresetName, + } = this; + + const response = await app.createProject({ + $, + projectId, + params: { + action: "export", + }, + data: { + format, + outputFileName, + exportPresetName, + }, + }); + + $.export("$summary", `Successfully exported publication with task key \`${response.taskKey}\`.`); + return response; + }, +}; diff --git a/components/documenterra/common/constants.mjs b/components/documenterra/common/constants.mjs new file mode 100644 index 0000000000000..9e350b2bd4b19 --- /dev/null +++ b/components/documenterra/common/constants.mjs @@ -0,0 +1,50 @@ +const PRINTED_FORMAT_OPTIONS = [ + { + value: "Pdf", + label: "Portable Document Format (.pdf)", + }, + { + value: "Docx", + label: "OpenXML, Microsoft Word (.docx)", + }, + { + value: "Doc", + label: "Microsoft Word 97-2003 (.doc)", + }, + { + value: "Rtf", + label: "Rich Text Format (.rtf)", + }, + { + value: "Epub", + label: "E-books and other digital publications (.epub)", + }, + { + value: "Odt", + label: "OpenDocument, OpenOffice.org file format (.odt)", + }, + { + value: "Mht", + label: "Web archive in one file (.mht)", + }, +]; + +const OTHER_FORMAT_OPTIONS = [ + { + value: "WebHelp", + label: "HTML5 Web Help (no border, built-in search, page tree and index, one .zip archive)", + }, + { + value: "PureHtml", + label: "HTML files (page content without user interface in one .zip archive)", + }, + { + value: "Chm", + label: "Microsoft Compiled HTML Help (.chm)", + }, +]; + +export default { + PRINTED_FORMAT_OPTIONS, + OTHER_FORMAT_OPTIONS, +}; diff --git a/components/documenterra/documenterra.app.mjs b/components/documenterra/documenterra.app.mjs new file mode 100644 index 0000000000000..c80ae5e80f007 --- /dev/null +++ b/components/documenterra/documenterra.app.mjs @@ -0,0 +1,106 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "documenterra", + propDefinitions: { + projectId: { + type: "string", + label: "Project Identity", + description: "The unique identifier for the project.", + async options({ filter = () => true }) { + const response = await this.listProjects(); + return response + ?.filter(filter) + .map(({ + id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + pageId: { + type: "string", + label: "Page ID", + description: "The unique identifier for the page.", + async options({ projectId }) { + if (!projectId) { + return []; + } + const response = await this.listPages({ + projectId, + }); + return response?.map(({ + id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + outputFileName: { + type: "string", + label: "Output File Name", + description: "The full file name, including the file path starting with `Storage/` Eg. `Storage/Backups/Project-backup_2023-03-13_03-46-07.zip`", + }, + }, + methods: { + getBaseUrl() { + const { portal_url: baseUrl } = this.$auth; + return baseUrl.endsWith("/") + ? baseUrl.slice(0, -1) + : baseUrl; + }, + getUrl(path) { + return `${this.getBaseUrl()}/api/v1${path}`; + }, + getAuth() { + const { + email: username, + api_key: password, + } = this.$auth; + return { + username, + password, + }; + }, + _makeRequest({ + $ = this, path, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + auth: this.getAuth(), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + createProject({ + projectId, ...args + } = {}) { + return this.post({ + path: `/projects/${projectId}`, + ...args, + }); + }, + listProjects(args = {}) { + return this._makeRequest({ + path: "/projects", + ...args, + }); + }, + listPages({ + projectId, ...args + } = {}) { + return this._makeRequest({ + path: `/projects/${projectId}/articles`, + ...args, + }); + }, + }, +}; diff --git a/components/documenterra/package.json b/components/documenterra/package.json new file mode 100644 index 0000000000000..5a10a537adadc --- /dev/null +++ b/components/documenterra/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/documenterra", + "version": "0.2.0", + "description": "Pipedream Documenterra Components", + "main": "documenterra.app.mjs", + "keywords": [ + "pipedream", + "documenterra" + ], + "homepage": "https://pipedream.com/apps/documenterra", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "2.0.0" + } +} diff --git a/components/documentpro/actions/new-document/new-document.mjs b/components/documentpro/actions/new-document/new-document.mjs new file mode 100644 index 0000000000000..349d3fa25c9af --- /dev/null +++ b/components/documentpro/actions/new-document/new-document.mjs @@ -0,0 +1,41 @@ +import FormData from "form-data"; +import fs from "fs"; +import { checkTmp } from "../../common/utils.mjs"; +import documentpro from "../../documentpro.app.mjs"; + +export default { + key: "documentpro-new-document", + name: "Upload New Document", + description: "Uploads a document to DocumentPro's parser. [See the documentation](https://docs.documentpro.ai/docs/using-api/manage-documents/import-files)", + version: "0.0.1", + type: "action", + props: { + documentpro, + parserId: { + propDefinition: [ + documentpro, + "parserId", + ], + }, + document: { + propDefinition: [ + documentpro, + "document", + ], + }, + }, + async run({ $ }) { + const formData = new FormData(); + const file = fs.createReadStream(checkTmp(this.document)); + formData.append("file", file); + + const response = await this.documentpro.uploadDocument({ + parserId: this.parserId, + data: formData, + headers: formData.getHeaders(), + }); + + $.export("$summary", `Successfully uploaded document with Id: ${response.request_id}`); + return response; + }, +}; diff --git a/components/documentpro/common/utils.mjs b/components/documentpro/common/utils.mjs new file mode 100644 index 0000000000000..dc2ed5a5be34d --- /dev/null +++ b/components/documentpro/common/utils.mjs @@ -0,0 +1,6 @@ +export const checkTmp = (filename) => { + if (filename.indexOf("/tmp") === -1) { + return `/tmp/${filename}`; + } + return filename; +}; diff --git a/components/documentpro/documentpro.app.mjs b/components/documentpro/documentpro.app.mjs new file mode 100644 index 0000000000000..04c5cba1ca5ec --- /dev/null +++ b/components/documentpro/documentpro.app.mjs @@ -0,0 +1,71 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "documentpro", + propDefinitions: { + document: { + type: "string", + label: "Document", + description: "The path to the file saved to the `/tmp` directory (e.g. `/tmp/example.pdf`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + }, + parserId: { + type: "string", + label: "Parser ID", + description: "The ID of the parser to use for uploading the document", + async options() { + const { items } = await this.getParsers(); + return items.map(({ + template_title: label, template_id: value, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.documentpro.ai"; + }, + _headers(headers = {}) { + return { + ...headers, + "x-api-key": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path = "", headers, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(headers), + ...opts, + }); + }, + getParsers(opts = {}) { + return this._makeRequest({ + path: "/v1/templates", + ...opts, + }); + }, + updateParser({ + parserId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/v1/templates/${parserId}`, + ...opts, + }); + }, + uploadDocument({ + parserId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/files/upload/${parserId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/documentpro/package.json b/components/documentpro/package.json new file mode 100644 index 0000000000000..134058103819c --- /dev/null +++ b/components/documentpro/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/documentpro", + "version": "0.1.0", + "description": "Pipedream DocumentPro Components", + "main": "documentpro.app.mjs", + "keywords": [ + "pipedream", + "documentpro" + ], + "homepage": "https://pipedream.com/apps/documentpro", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0", + "form-data": "^4.0.0" + } +} diff --git a/components/documentpro/sources/new-document-updated-instant/new-document-updated-instant.mjs b/components/documentpro/sources/new-document-updated-instant/new-document-updated-instant.mjs new file mode 100644 index 0000000000000..56bf3bbc6cff9 --- /dev/null +++ b/components/documentpro/sources/new-document-updated-instant/new-document-updated-instant.mjs @@ -0,0 +1,58 @@ +import documentpro from "../../documentpro.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "documentpro-new-document-updated-instant", + name: "New Document Updated (Instant)", + description: "Emit new event when a file request status changes. You can only create one webhook in a parser at a time.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + documentpro, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + parserId: { + propDefinition: [ + documentpro, + "parserId", + ], + }, + }, + hooks: { + async activate() { + await this.documentpro.updateParser({ + parserId: this.parserId, + data: { + webhook_url: this.http.endpoint, + }, + }); + }, + async deactivate() { + await this.documentpro.updateParser({ + parserId: this.parserId, + data: { + webhook_url: null, + }, + }); + }, + }, + async run({ body }) { + this.$emit(body, { + id: `${body.data.request_id}-${body.timestamp}`, + summary: `New document (${body.data.response_body.file_name}) status updated: ${body.event} - ${body.data.request_status}`, + ts: Date.parse(body.timestamp), + }); + + this.http.respond({ + status: 200, + body: { + message: "Received", + }, + }); + }, + sampleEmit, +}; diff --git a/components/documentpro/sources/new-document-updated-instant/test-event.mjs b/components/documentpro/sources/new-document-updated-instant/test-event.mjs new file mode 100644 index 0000000000000..0639a9b0d5ef9 --- /dev/null +++ b/components/documentpro/sources/new-document-updated-instant/test-event.mjs @@ -0,0 +1,20 @@ +export default { + "event": "file_request_status_change", + "timestamp": "2023-07-30T19:05:29.565249", + "data": { + "request_id": "your-request-id", + "request_status": "completed", + "response_body": { + "file_name": "your-file-name", + "file_presigned_url": "temporary-url-to-your-file", + "user_error_msg": null, + "template_id": "your-template-id", + "template_type": "receipt", + "template_title": "Receipt", + "num_pages": 2, + "result_json_data": {} + }, + "created_at": "2023-07-30T19:05:10.696893", + "updated_at": "2023-07-30T19:05:29.565249" + } +} \ No newline at end of file diff --git a/components/documerge/actions/combine-files/combine-files.mjs b/components/documerge/actions/combine-files/combine-files.mjs new file mode 100644 index 0000000000000..944177fd8af30 --- /dev/null +++ b/components/documerge/actions/combine-files/combine-files.mjs @@ -0,0 +1,53 @@ +import documerge from "../../documerge.app.mjs"; +import fs from "fs"; + +export default { + key: "documerge-combine-files", + name: "Combine Files", + description: "Merges multiple user-specified files into a single PDF or DOCX. [See the documentation](https://app.documerge.ai/api-docs/#tools-POSTapi-tools-combine)", + version: "0.0.1", + type: "action", + props: { + documerge, + name: { + type: "string", + label: "Name", + description: "Name of the new file", + }, + output: { + type: "string", + label: "Output", + description: "The output file type", + options: [ + "pdf", + "docx", + ], + }, + urls: { + type: "string[]", + label: "URLs", + description: "Array of URLs to combine", + }, + }, + async run({ $ }) { + const fileContent = await this.documerge.combineFiles({ + $, + data: { + output: this.output, + files: this.urls.map((url) => ({ + name: this.name, + url, + })), + }, + }); + + const filename = this.name.includes(".pdf") || this.name.includes(".docx") + ? this.name + : `${this.name}.${this.output}`; + const path = `/tmp/${filename}`; + await fs.writeFileSync(path, Buffer.from(fileContent)); + + $.export("$summary", "Successfully combined files"); + return path; + }, +}; diff --git a/components/documerge/actions/convert-file-to-pdf/convert-file-to-pdf.mjs b/components/documerge/actions/convert-file-to-pdf/convert-file-to-pdf.mjs new file mode 100644 index 0000000000000..c5751f6fc93ba --- /dev/null +++ b/components/documerge/actions/convert-file-to-pdf/convert-file-to-pdf.mjs @@ -0,0 +1,44 @@ +import documerge from "../../documerge.app.mjs"; +import fs from "fs"; + +export default { + key: "documerge-convert-file-to-pdf", + name: "Convert File to PDF", + description: "Converts a specified file into a PDF. [See the documentation](https://app.documerge.ai/api-docs/#tools-POSTapi-tools-pdf-convert)", + version: "0.0.1", + type: "action", + props: { + documerge, + name: { + type: "string", + label: "Name", + description: "Name of the new file", + }, + url: { + type: "string", + label: "URL", + description: "The URL of the file to convert", + optional: true, + }, + }, + async run({ $ }) { + const fileContent = await this.documerge.convertToPdf({ + $, + data: { + file: { + name: this.name, + url: this.url, + }, + }, + }); + + const filename = this.name.includes(".pdf") + ? this.name + : `${this.name}.pdf`; + const path = `/tmp/${filename}`; + await fs.writeFileSync(path, Buffer.from(fileContent)); + + $.export("$summary", "Successfully converted file to PDF"); + return path; + }, +}; diff --git a/components/documerge/actions/get-document-fields/get-document-fields.mjs b/components/documerge/actions/get-document-fields/get-document-fields.mjs new file mode 100644 index 0000000000000..2cde3e238e5a9 --- /dev/null +++ b/components/documerge/actions/get-document-fields/get-document-fields.mjs @@ -0,0 +1,26 @@ +import documerge from "../../documerge.app.mjs"; + +export default { + key: "documerge-get-document-fields", + name: "Get Document Fields", + description: "Extracts and returns data from fields in a given document. [See the documentation](https://app.documerge.ai/api-docs/#documents-GETapi-documents-fields--document_id-)", + version: "0.0.1", + type: "action", + props: { + documerge, + documentId: { + propDefinition: [ + documerge, + "documentId", + ], + }, + }, + async run({ $ }) { + const response = await this.documerge.getDocumentFields({ + $, + documentId: this.documentId, + }); + $.export("$summary", `Successfully extracted field values from document with ID: ${this.documentId}`); + return response; + }, +}; diff --git a/components/documerge/documerge.app.mjs b/components/documerge/documerge.app.mjs new file mode 100644 index 0000000000000..7ce530e34725f --- /dev/null +++ b/components/documerge/documerge.app.mjs @@ -0,0 +1,129 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "documerge", + propDefinitions: { + documentId: { + type: "string", + label: "Document ID", + description: "Identifier of a document", + async options() { + const { data } = await this.listDocuments(); + return data?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + routeId: { + type: "string", + label: "Routet ID", + description: "Identifier of a route", + async options() { + const { data } = await this.listRoutes(); + return data?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://app.documerge.ai/api"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `Bearer ${this.$auth.api_token}`, + "Content-Type": "application/json", + "Accept": "application/json", + }, + }); + }, + listDocuments(opts = {}) { + return this._makeRequest({ + path: "/documents", + ...opts, + }); + }, + listRoutes(opts = {}) { + return this._makeRequest({ + path: "/routes", + ...opts, + }); + }, + getDocumentFields({ + documentId, ...opts + }) { + return this._makeRequest({ + path: `/documents/fields/${documentId}`, + ...opts, + }); + }, + combineFiles(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/tools/combine", + responseType: "arraybuffer", + ...opts, + }); + }, + convertToPdf(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/tools/pdf/convert", + responseType: "arraybuffer", + ...opts, + }); + }, + createDocumentDeliveryMethod({ + documentId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/documents/delivery-methods/${documentId}`, + ...opts, + }); + }, + deleteDocumentDeliveryMethod({ + documentId, deliveryMethodId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/documents/delivery-methods/${documentId}/${deliveryMethodId}`, + ...opts, + }); + }, + createRouteDeliveryMethod({ + routeId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/routes/delivery-methods/${routeId}`, + ...opts, + }); + }, + deleteRouteDeliveryMethod({ + routeId, deliveryMethodId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/routes/delivery-methods/${routeId}/${deliveryMethodId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/documerge/package.json b/components/documerge/package.json new file mode 100644 index 0000000000000..70633a3ac5936 --- /dev/null +++ b/components/documerge/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/documerge", + "version": "0.1.0", + "description": "Pipedream DocuMerge Components", + "main": "documerge.app.mjs", + "keywords": [ + "pipedream", + "documerge" + ], + "homepage": "https://pipedream.com/apps/documerge", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/documerge/sources/common/base.mjs b/components/documerge/sources/common/base.mjs new file mode 100644 index 0000000000000..4a785fb20d30d --- /dev/null +++ b/components/documerge/sources/common/base.mjs @@ -0,0 +1,44 @@ +import documerge from "../../documerge.app.mjs"; + +export default { + props: { + documerge, + http: "$.interface.http", + db: "$.service.db", + }, + methods: { + _getDeliveryMethodIds() { + return this.db.get("deliveryMethodIds") || {}; + }, + _setDeliveryMethodIds(deliveryMethodIds) { + this.db.set("deliveryMethodIds", deliveryMethodIds); + }, + getWebhookSettings() { + return { + type: "webhook", + settings: { + url: this.http.endpoint, + always_send: true, + send_temporary_download_url: true, + send_data_using_json: true, + send_merge_data: true, + }, + }; + }, + generateMeta(body) { + return { + id: body.merge_id, + summary: this.getSummary(body), + ts: Date.now(), + }; + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + }, + async run(event) { + const { body } = event; + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, +}; diff --git a/components/documerge/sources/new-merged-document-instant/new-merged-document-instant.mjs b/components/documerge/sources/new-merged-document-instant/new-merged-document-instant.mjs new file mode 100644 index 0000000000000..41acdc38923cb --- /dev/null +++ b/components/documerge/sources/new-merged-document-instant/new-merged-document-instant.mjs @@ -0,0 +1,54 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "documerge-new-merged-document-instant", + name: "New Merged Document (Instant)", + description: "Emit new event when a merged document is created in documerge.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + documentIds: { + propDefinition: [ + common.props.documerge, + "documentId", + ], + type: "string[]", + label: "Document IDs", + description: "An array of document identifiers of the documents to watch", + }, + }, + hooks: { + async activate() { + const deliveryMethodIds = {}; + for (const documentId of this.documentIds) { + const { data: { id } } = await this.documerge.createDocumentDeliveryMethod({ + documentId, + data: this.getWebhookSettings(), + }); + deliveryMethodIds[documentId] = id; + } + this._setDeliveryMethodIds(deliveryMethodIds); + }, + async deactivate() { + const deliveryMethodIds = this._getDeliveryMethodIds(); + for (const documentId of this.documentIds) { + await this.documerge.deleteDocumentDeliveryMethod({ + documentId, + deliveryMethodId: deliveryMethodIds[documentId], + }); + } + this._setDeliveryMethodIds({}); + }, + }, + methods: { + ...common.methods, + getSummary(body) { + return `Merged Document: ${body.file_name}`; + }, + }, + sampleEmit, +}; diff --git a/components/documerge/sources/new-merged-document-instant/test-event.mjs b/components/documerge/sources/new-merged-document-instant/test-event.mjs new file mode 100644 index 0000000000000..95c5d8f8ee835 --- /dev/null +++ b/components/documerge/sources/new-merged-document-instant/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "file_url": "https://documerge.blob.core.windows.net/documerge/documentMerges/408/294186/doc1_-_test.pdf?sv=2017-11-09&sr=b&se=2024-07-18T17:16:51Z&sp=r&spr=https&sig=%2F02yOVeuTEB66AihSptlTg7a7GYfxz3JGkxKE2kt94Y%3D", + "file_name": "doc1 - test.pdf", + "merge_id": 320214, + "fields": { + "field1": "test", + "_date": "2024-07-18", + "_datetime": "2024-07-18 12:16 pm", + "_ip": "10.1.0.4", + "_auto_number": null + } +} \ No newline at end of file diff --git a/components/documerge/sources/new-merged-route-instant/new-merged-route-instant.mjs b/components/documerge/sources/new-merged-route-instant/new-merged-route-instant.mjs new file mode 100644 index 0000000000000..5da9b0a2c7041 --- /dev/null +++ b/components/documerge/sources/new-merged-route-instant/new-merged-route-instant.mjs @@ -0,0 +1,54 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "documerge-new-merged-route-instant", + name: "New Merged Route (Instant)", + description: "Emit new event when a merged route is created in documerge.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + routeIds: { + propDefinition: [ + common.props.documerge, + "routeId", + ], + type: "string[]", + label: "Route IDs", + description: "An array of route identifiers of the routes to watch", + }, + }, + hooks: { + async activate() { + const deliveryMethodIds = {}; + for (const routeId of this.routeIds) { + const { data: { id } } = await this.documerge.createRouteDeliveryMethod({ + routeId, + data: this.getWebhookSettings(), + }); + deliveryMethodIds[routeId] = id; + } + this._setDeliveryMethodIds(deliveryMethodIds); + }, + async deactivate() { + const deliveryMethodIds = this.getDeliveryMethodIds(); + for (const routeId of this.routeIds) { + await this.documerge.deleteRoutetDeliveryMethod({ + routeId, + deliveryMethodId: deliveryMethodIds[routeId], + }); + } + this._setDeliveryMethodIds({}); + }, + }, + methods: { + ...common.methods, + getSummary(body) { + return `Merged Route: ${body.file_name}`; + }, + }, + sampleEmit, +}; diff --git a/components/documerge/sources/new-merged-route-instant/test-event.mjs b/components/documerge/sources/new-merged-route-instant/test-event.mjs new file mode 100644 index 0000000000000..75e75ebe5b59b --- /dev/null +++ b/components/documerge/sources/new-merged-route-instant/test-event.mjs @@ -0,0 +1,13 @@ +export default { + "file_url": "https://documerge.blob.core.windows.net/documerge/combinedFiles/294339/route1_-_test.pdf?sv=2017-11-09&sr=b&se=2024-07-18T17:42:40Z&sp=r&spr=https&sig=6jsL2yYs3URH3g193%2Fvc1AK66vAdx7G8q5hOn1gmPVg%3D", + "file_name": "route1 - test.pdf", + "merge_id": 320379, + "fields": { + "field1": "abc", + "_date": "2024-07-18", + "_datetime": "2024-07-18 12:42 pm", + "_ip": "10.1.0.4", + "_auto_number": null, + "_route_auto_number": null + } +} \ No newline at end of file diff --git a/components/documint/README.md b/components/documint/README.md new file mode 100644 index 0000000000000..7531608486f18 --- /dev/null +++ b/components/documint/README.md @@ -0,0 +1,14 @@ +# Overview + +The Documint API enables automated document generation with dynamic content, offering a robust solution for creating tailored PDFs or other document types from templates. With this API, you can insert custom data into predefined fields, generate documents on the fly, and streamline the creation of invoices, contracts, reports, and more. Integrating Documint with Pipedream allows for the orchestration of serverless workflows that react to various triggers, like webhooks, emails, or schedule timings, to produce documents as part of a larger automated process. + +# Example Use Cases + +- **Generate Invoices on New Stripe Charges** + Automatically create and send customized invoices when new charges are detected in Stripe. Use Stripe's trigger to start the Pipedream workflow, fetch charge details, and pass them to Documint to generate a PDF invoice, which can then be emailed to the customer. + +- **Create Reports from Google Sheets Data** + Set up a workflow that gets triggered on a schedule to fetch data from a specific Google Sheets document. This data is used to populate a report template in Documint, generating a daily, weekly, or monthly report, which can be stored in Google Drive or sent out via email directly to stakeholders. + +- **Contract Generation from Salesforce Opportunities** + Kick off document creation when a new opportunity reaches a certain stage in Salesforce. The workflow grabs the relevant data from the opportunity, uses it to fill out a contract template in Documint, and then uploads the finalized contract to the Salesforce record to be available for all parties involved. diff --git a/components/docupilot/README.md b/components/docupilot/README.md index 9bab9d76d7435..f6019218cb221 100644 --- a/components/docupilot/README.md +++ b/components/docupilot/README.md @@ -1,14 +1,11 @@ # Overview -Docupilot offers a simple, yet powerful API that enables you to generate -documents on the fly. With just a few lines of code, you can easily create -documents such as PDFs, Word documents, Excel Spreadsheets, and more. - -Here are just a few examples of what you can build using the Docupilot API: - -- PDF documents -- Word documents -- Excel spreadsheets -- EBooks -- Presentations -- Forms +Docupilot offers powerful document automation capabilities, allowing you to create dynamic documents based on templates and data inputs. With the Docupilot API, you can integrate this functionality directly into Pipedream workflows, triggering document generation from a myriad of events and data sources. Think of automating contract creation when a new deal is won in your CRM, or sending personalized letters en masse with the click of a button. The API empowers you to craft, distribute, and manage documents efficiently and programmatically. + +# Example Use Cases + +- **Automated Contract Generation for CRM Deals**: When a new deal is marked as won in a CRM like Salesforce, trigger a Pipedream workflow that collects deal details and customer data, then uses Docupilot to generate a personalized contract, which can be sent out for e-signature via an app like HelloSign. + +- **Custom Reports on Schedule**: Merge data from a SQL database or a data analytics platform like Google Analytics into a Docupilot template to generate custom reports. Set up a Pipedream cron job to run this workflow weekly, sending the freshly minted report to stakeholders via email or Slack. + +- **On-Demand HR Document Creation**: With a trigger from an HR platform like BambooHR or from a Pipedream HTTP endpoint, you can kick off creation of employee-related documents such as offer letters or performance reviews, which are then stored in a cloud service like Google Drive and shared with the relevant parties. diff --git a/components/docupost/README.md b/components/docupost/README.md new file mode 100644 index 0000000000000..94a486e873f02 --- /dev/null +++ b/components/docupost/README.md @@ -0,0 +1,14 @@ +# Overview + +The DocuPost API enables automated mail handling, offering services like sending letters, postcards, and checks via a programmatic interface. Within Pipedream, you can harness this API to create dynamic, serverless workflows that trigger postal mail actions based on various events from a plethora of supported apps. Think of automating thank-you notes, payment disbursements, or direct mail marketing campaigns, all reacting in real-time to user behavior, payment events, or custom triggers. + +# Example Use Cases + +- **Automate Thank-You Postcards for E-commerce Orders** + When a new order is placed via Shopify, use the DocuPost API on Pipedream to send a personalized postcard thanking your customer. This can help increase customer loyalty and encourage repeat business. + +- **Send Payment Checks on Invoice Completion** + Combine QuickBooks and DocuPost by setting up a workflow in Pipedream where a check is mailed out automatically whenever an invoice is marked as paid. This streamlines the payment process for both the business and the client. + +- **Direct Mail Campaigns Triggered by CRM Events** + Integrate Salesforce events with DocuPost API on Pipedream. For instance, when a prospect reaches a certain stage in your sales pipeline, automatically send them a tailored brochure or offer letter to enhance engagement and conversion. diff --git a/components/docupost/actions/mail-letter/mail-letter.mjs b/components/docupost/actions/mail-letter/mail-letter.mjs new file mode 100644 index 0000000000000..bf2a923049164 --- /dev/null +++ b/components/docupost/actions/mail-letter/mail-letter.mjs @@ -0,0 +1,187 @@ +import app from "../../docupost.app.mjs"; + +export default { + key: "docupost-mail-letter", + name: "Mail Letter", + description: "Sends a physical letter via USPS first class mail. [See the documentation](https://help.docupost.com/help/send-letter-api)", + version: "0.0.1", + type: "action", + props: { + app, + pdf: { + type: "string", + label: "PDF URL", + description: "A valid PDF URL for the letter content. `10mb` max. 8.5x11 recommended.", + }, + color: { + type: "boolean", + label: "Color", + description: "Whether the document should be in color. `true` or `false`. Defaults to `false`.", + optional: true, + }, + doublesided: { + type: "string", + label: "Double-Sided", + description: "Whether the document should be printed double-sided. `true` or `false`. Defaults to `true`.", + optional: true, + }, + mailingClass: { + type: "string", + label: "Class", + description: "Must be `usps_first_class` or `usps_standard`. Defaults to `usps_first_class`.", + options: [ + "usps_first_class", + "usps_standard", + ], + optional: true, + }, + servicelevel: { + type: "string", + label: "Service Level", + description: "Only available for `usps_first_class` mailings. Should be blank for non-certified, or `certified` or `certified_return_receipt`.", + options: [ + { + value: "", + label: "Non-Certified", + }, + { + value: "certified", + label: "Certified", + }, + { + value: "certified_return_receipt", + label: "Certified Return Receipt", + }, + ], + optional: true, + }, + toName: { + propDefinition: [ + app, + "toName", + ], + }, + toAddress1: { + propDefinition: [ + app, + "toAddress1", + ], + }, + toAddress2: { + propDefinition: [ + app, + "toAddress2", + ], + }, + toCity: { + propDefinition: [ + app, + "toCity", + ], + }, + toState: { + propDefinition: [ + app, + "toState", + ], + }, + toZip: { + propDefinition: [ + app, + "toZip", + ], + }, + fromName: { + propDefinition: [ + app, + "fromName", + ], + }, + fromAddress1: { + propDefinition: [ + app, + "fromAddress1", + ], + }, + fromAddress2: { + propDefinition: [ + app, + "fromAddress2", + ], + }, + fromCity: { + propDefinition: [ + app, + "fromCity", + ], + }, + fromState: { + propDefinition: [ + app, + "fromState", + ], + }, + fromZip: { + propDefinition: [ + app, + "fromZip", + ], + }, + }, + methods: { + sendLetter(args = {}) { + return this.app.post({ + path: "/sendletter", + ...args, + }); + }, + }, + async run({ $ }) { + const { + sendLetter, + pdf, + color, + doublesided, + mailingClass, + servicelevel, + toName, + toAddress1, + toAddress2, + toCity, + toState, + toZip, + fromName, + fromAddress1, + fromAddress2, + fromCity, + fromState, + fromZip, + } = this; + + const response = await sendLetter({ + $, + params: { + pdf, + color, + doublesided, + class: mailingClass, + servicelevel, + to_name: toName, + to_address1: toAddress1, + to_address2: toAddress2, + to_city: toCity, + to_state: toState, + to_zip: toZip, + from_name: fromName, + from_address1: fromAddress1, + from_address2: fromAddress2, + from_city: fromCity, + from_state: fromState, + from_zip: fromZip, + }, + }); + + $.export("$summary", "Successfully sent letter"); + return response; + }, +}; diff --git a/components/docupost/actions/mail-postcard/mail-postcard.mjs b/components/docupost/actions/mail-postcard/mail-postcard.mjs new file mode 100644 index 0000000000000..0c219eb361847 --- /dev/null +++ b/components/docupost/actions/mail-postcard/mail-postcard.mjs @@ -0,0 +1,144 @@ +import app from "../../docupost.app.mjs"; + +export default { + key: "docupost-mail-postcard", + name: "Mail Postcard", + description: "Dispatches a glossy, color 4x6 postcard via the US Postal Service. [See the documentation](https://help.docupost.com/help/send-postcard-api)", + version: "0.0.1", + type: "action", + props: { + app, + frontImage: { + type: "string", + label: "Front Image URL", + description: "A valid image URL for the front of your postcard. Image should be 1875 x 1275. PNG recommended. Your API request will succeed but your mailing will later fail if the image is the incorrect size.", + }, + backImage: { + type: "string", + label: "Back Image URL", + description: "A valid image URL for the back of your postcard. Image should be 1875 x 1275. PNG recommended. Your API request will succeed but your mailing will later fail if the image is the incorrect size. Important: Your recipient/sender and postage information will be automatically overlaid onto your image. See [this template](https://docupost.s3.us-east-2.amazonaws.com/postcardtemplateback_ip0jws.png) as an example. ", + }, + toName: { + propDefinition: [ + app, + "toName", + ], + }, + toAddress1: { + propDefinition: [ + app, + "toAddress1", + ], + }, + toAddress2: { + propDefinition: [ + app, + "toAddress2", + ], + }, + toCity: { + propDefinition: [ + app, + "toCity", + ], + }, + toState: { + propDefinition: [ + app, + "toState", + ], + }, + toZip: { + propDefinition: [ + app, + "toZip", + ], + }, + fromName: { + propDefinition: [ + app, + "fromName", + ], + }, + fromAddress1: { + propDefinition: [ + app, + "fromAddress1", + ], + }, + fromAddress2: { + propDefinition: [ + app, + "fromAddress2", + ], + }, + fromCity: { + propDefinition: [ + app, + "fromCity", + ], + }, + fromState: { + propDefinition: [ + app, + "fromState", + ], + }, + fromZip: { + propDefinition: [ + app, + "fromZip", + ], + }, + }, + methods: { + sendPostcard(args = {}) { + return this.app.post({ + path: "/sendpostcard", + ...args, + }); + }, + }, + async run({ $ }) { + const { + sendPostcard, + frontImage, + backImage, + toName, + toAddress1, + toAddress2, + toCity, + toState, + toZip, + fromName, + fromAddress1, + fromAddress2, + fromCity, + fromState, + fromZip, + } = this; + + const response = await sendPostcard({ + $, + params: { + front_image: frontImage, + back_image: backImage, + to_name: toName, + to_address1: toAddress1, + to_address2: toAddress2, + to_city: toCity, + to_state: toState, + to_zip: toZip, + from_name: fromName, + from_address1: fromAddress1, + from_address2: fromAddress2, + from_city: fromCity, + from_state: fromState, + from_zip: fromZip, + }, + }); + + $.export("$summary", "Successfully sent postcard"); + return response; + }, +}; diff --git a/components/docupost/common/constants.mjs b/components/docupost/common/constants.mjs new file mode 100644 index 0000000000000..7539fd780262b --- /dev/null +++ b/components/docupost/common/constants.mjs @@ -0,0 +1,13 @@ +import states from "./states.mjs"; +import militaryStates from "./military-states.mjs"; +import directions from "./directions.mjs"; + +const BASE_URL = "https://app.docupost.com"; +const VERSION_PATH = "/api/1.1/wf"; +const TWO_LETTER_ABBREVIATION_CODES = states.concat(militaryStates).concat(directions); + +export default { + BASE_URL, + VERSION_PATH, + TWO_LETTER_ABBREVIATION_CODES, +}; diff --git a/components/docupost/common/directions.mjs b/components/docupost/common/directions.mjs new file mode 100644 index 0000000000000..1b1a02ac566d7 --- /dev/null +++ b/components/docupost/common/directions.mjs @@ -0,0 +1,34 @@ +export default [ + { + label: "North", + value: "N", + }, + { + label: "East", + value: "E", + }, + { + label: "South", + value: "S", + }, + { + label: "West", + value: "W", + }, + { + label: "Northeast", + value: "NE", + }, + { + label: "Southeast", + value: "SE", + }, + { + label: "Northwest", + value: "NW", + }, + { + label: "Southwest", + value: "SW", + }, +]; diff --git a/components/docupost/common/military-states.mjs b/components/docupost/common/military-states.mjs new file mode 100644 index 0000000000000..6684cc7e8c1ce --- /dev/null +++ b/components/docupost/common/military-states.mjs @@ -0,0 +1,14 @@ +export default [ + { + label: "Armed Forces Europe, the Middle East, and Canada", + value: "AE", + }, + { + label: "Armed Forces Pacific", + value: "AP", + }, + { + label: "Armed Forces Americas (except Canada)", + value: "AA", + }, +]; diff --git a/components/docupost/common/states.mjs b/components/docupost/common/states.mjs new file mode 100644 index 0000000000000..9277c1ba3088d --- /dev/null +++ b/components/docupost/common/states.mjs @@ -0,0 +1,238 @@ +export default [ + { + label: "Alabama", + value: "AL", + }, + { + label: "Alaska", + value: "AK", + }, + { + label: "American Samoa", + value: "AS", + }, + { + label: "Arizona", + value: "AZ", + }, + { + label: "Arkansas", + value: "AR", + }, + { + label: "California", + value: "CA", + }, + { + label: "Colorado", + value: "CO", + }, + { + label: "Connecticut", + value: "CT", + }, + { + label: "Delaware", + value: "DE", + }, + { + label: "District of Columbia", + value: "DC", + }, + { + label: "Federated States of Micronesia", + value: "FM", + }, + { + label: "Florida", + value: "FL", + }, + { + label: "Georgia", + value: "GA", + }, + { + label: "Guam", + value: "GU", + }, + { + label: "Hawaii", + value: "HI", + }, + { + label: "Idaho", + value: "ID", + }, + { + label: "Illinois", + value: "IL", + }, + { + label: "Indiana", + value: "IN", + }, + { + label: "Iowa", + value: "IA", + }, + { + label: "Kansas", + value: "KS", + }, + { + label: "Kentucky", + value: "KY", + }, + { + label: "Louisiana", + value: "LA", + }, + { + label: "Maine", + value: "ME", + }, + { + label: "Marshall Islands", + value: "MH", + }, + { + label: "Maryland", + value: "MD", + }, + { + label: "Massachusetts", + value: "MA", + }, + { + label: "Michigan", + value: "MI", + }, + { + label: "Minnesota", + value: "MN", + }, + { + label: "Mississippi", + value: "MS", + }, + { + label: "Missouri", + value: "MO", + }, + { + label: "Montana", + value: "MT", + }, + { + label: "Nebraska", + value: "NE", + }, + { + label: "Nevada", + value: "NV", + }, + { + label: "New Hampshire", + value: "NH", + }, + { + label: "New Jersey", + value: "NJ", + }, + { + label: "New Mexico", + value: "NM", + }, + { + label: "New York", + value: "NY", + }, + { + label: "North Carolina", + value: "NC", + }, + { + label: "North Dakota", + value: "ND", + }, + { + label: "Northern Mariana Islands", + value: "MP", + }, + { + label: "Ohio", + value: "OH", + }, + { + label: "Oklahoma", + value: "OK", + }, + { + label: "Oregon", + value: "OR", + }, + { + label: "Palau", + value: "PW", + }, + { + label: "Pennsylvania", + value: "PA", + }, + { + label: "Puerto Rico", + value: "PR", + }, + { + label: "Rhode Island", + value: "RI", + }, + { + label: "South Carolina", + value: "SC", + }, + { + label: "South Dakota", + value: "SD", + }, + { + label: "Tennessee", + value: "TN", + }, + { + label: "Texas", + value: "TX", + }, + { + label: "Utah", + value: "UT", + }, + { + label: "Vermont", + value: "VT", + }, + { + label: "Virgin Islands", + value: "VI", + }, + { + label: "Virginia", + value: "VA", + }, + { + label: "Washington", + value: "WA", + }, + { + label: "West Virginia", + value: "WV", + }, + { + label: "Wisconsin", + value: "WI", + }, + { + label: "Wyoming", + value: "WY", + }, +]; diff --git a/components/docupost/docupost.app.mjs b/components/docupost/docupost.app.mjs new file mode 100644 index 0000000000000..a7e365aa2d6f9 --- /dev/null +++ b/components/docupost/docupost.app.mjs @@ -0,0 +1,106 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "docupost", + propDefinitions: { + toName: { + type: "string", + label: "Recipient's Name", + description: "The name of the recipient (who the letter/postcard is being sent to). Must be less than 40 characters.", + }, + toAddress1: { + type: "string", + label: "Recipient's Address Line 1", + description: "The first line of the recipient's address.", + }, + toAddress2: { + type: "string", + label: "Recipient's Address Line 2", + description: "The second line of the recipient's address.", + optional: true, + }, + toCity: { + type: "string", + label: "Recipient's City", + description: "The city of the recipient.", + }, + toState: { + type: "string", + label: "Recipient's State", + description: "The [2-letter abbreviation code](https://pe.usps.com/text/pub28/28apb.htm) for the recipient's U.S. state.", + options: constants.TWO_LETTER_ABBREVIATION_CODES, + }, + toZip: { + type: "string", + label: "Recipient's Postal Code", + description: "The 5-digit postal code for the recipient.", + }, + fromName: { + type: "string", + label: "Sender's Name", + description: "Your sender/return address name. Must be less than 40 characters.", + }, + fromAddress1: { + type: "string", + label: "Sender's Address Line 1", + description: "The first line of your sender/return address.", + }, + fromAddress2: { + type: "string", + label: "Sender's Address Line 2", + description: "The second line of your sender/return address.", + optional: true, + }, + fromCity: { + type: "string", + label: "Sender's City", + description: "The city of your sender/return address.", + }, + fromState: { + type: "string", + label: "Sender's State", + description: "The [2-letter abbreviation code](https://pe.usps.com/text/pub28/28apb.htm) for your sender's U.S. state.", + options: constants.TWO_LETTER_ABBREVIATION_CODES, + }, + fromZip: { + type: "string", + label: "Sender's Postal Code", + description: "The 5-digit postal code for your sender/return address.", + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getAuth(data) { + return { + ...data, + api_token: this.$auth.api_token, + }; + }, + async _makeRequest({ + $ = this, path, headers, params, ...args + } = {}) { + const response = await axios($, { + ...args, + url: this.getUrl(path), + headers: this.getAuth(headers), + params: this.getAuth(params), + }); + + if (response?.error) { + throw new Error(response.error); + } + + return response; + }, + post(args = {}) { + return this._makeRequest({ + method: "post", + ...args, + }); + }, + }, +}; diff --git a/components/docupost/package.json b/components/docupost/package.json new file mode 100644 index 0000000000000..7efb6acdb8bdf --- /dev/null +++ b/components/docupost/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/docupost", + "version": "0.1.0", + "description": "Pipedream DocuPost Components", + "main": "docupost.app.mjs", + "keywords": [ + "pipedream", + "docupost" + ], + "homepage": "https://pipedream.com/apps/docupost", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/docuseal/docuseal.app.mjs b/components/docuseal/docuseal.app.mjs new file mode 100644 index 0000000000000..1250896c22a0d --- /dev/null +++ b/components/docuseal/docuseal.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "docuseal", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/docuseal/package.json b/components/docuseal/package.json new file mode 100644 index 0000000000000..0d171125e236f --- /dev/null +++ b/components/docuseal/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/docuseal", + "version": "0.0.1", + "description": "Pipedream DocuSeal Components", + "main": "docuseal.app.mjs", + "keywords": [ + "pipedream", + "docuseal" + ], + "homepage": "https://pipedream.com/apps/docuseal", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/docusign/README.md b/components/docusign/README.md index da026e5d112e7..d46c006143bf9 100644 --- a/components/docusign/README.md +++ b/components/docusign/README.md @@ -1,7 +1,11 @@ # Overview -The Docusign API allows developers to integrate Docusign's electronic signature -capabilities into their own applications. With the API, developers can create, -send, and manage documents that require electronic signatures. Additionally, -the API can be used to track the status of document signing processes, as well -as manage account settings and users. +The DocuSign API enables you to integrate electronic signature workflows into your applications, automate the process of sending and receiving documents for signatures, and securely manage related data. Leveraging Pipedream's platform, you can build powerful automations that streamline how contracts and agreements are handled, enhance compliance, and improve overall efficiency. + +# Example Use Cases + +- **Automated Contract Workflow**: Trigger a Pipedream workflow when a new contract is added to a cloud storage platform like Dropbox. Automatically send the document for signature via DocuSign, and once signed, save the final version back to Dropbox and notify relevant parties through email or a Slack message. + +- **CRM Integration for Sales**: Upon closing a deal in a CRM system like Salesforce, a Pipedream workflow can be initiated to send the sales agreement through DocuSign for signature. After the document is fully signed, update the Salesforce record with the status of the agreement and attach the signed document for easy access. + +- **Onboarding Process Automation**: For HR teams, initiate a Pipedream workflow when a new employee is added to an HR management system like BambooHR. Send all necessary onboarding documents through DocuSign for e-signature. Once completed, store the signed documents in a secure HR folder and update the employee's records with their signed contracts. diff --git a/components/docusign/actions/download-documents/download-documents.mjs b/components/docusign/actions/download-documents/download-documents.mjs new file mode 100644 index 0000000000000..0ca9b852c8ab7 --- /dev/null +++ b/components/docusign/actions/download-documents/download-documents.mjs @@ -0,0 +1,111 @@ +import docusign from "../../docusign.app.mjs"; +import fs from "fs"; + +export default { + key: "docusign-download-documents", + name: "Download Documents", + description: "Download the documents of an envelope to the /tmp directory. [See the documentation here](https://developers.docusign.com/docs/esign-rest-api/how-to/download-envelope-documents/)", + version: "0.0.1", + type: "action", + props: { + docusign, + account: { + propDefinition: [ + docusign, + "account", + ], + }, + envelopeId: { + type: "string", + label: "Envelope ID", + description: "Identifier of the envelope to download documents from", + async options({ prevContext }) { + const baseUri = await this.docusign.getBaseUri({ + accountId: this.account, + }); + const { startPosition } = prevContext; + const { + envelopes = [], nextUri, endPosition, + } = await this.docusign.listEnvelopes(baseUri, { + start_position: startPosition, + from_date: "2000-01-01", + }); + return { + options: envelopes.map(({ + envelopeId: value, emailSubject: label, + }) => ({ + label, + value, + })), + context: { + startPosition: nextUri + ? endPosition + 1 + : undefined, + }, + }; + }, + }, + downloadType: { + type: "string", + label: "Download Type", + description: "Download envelope documents to the `/tmp` directory", + options: [ + { + label: "All Documents (PDF)", + value: "combined", + }, + { + label: "All Documents (Zip)", + value: "archive", + }, + { + label: "Certificate (PDF)", + value: "certificate", + }, + { + label: "Portfolio (PDF)", + value: "portfolio", + }, + ], + }, + filename: { + type: "string", + label: "Filename", + description: "The filename to save the file as in the `/tmp` directory including the file extension (.pdf or .zip)", + }, + }, + methods: { + getEnvelope($, baseUri, envelopeId) { + return this.docusign._makeRequest({ + $, + config: { + url: `${baseUri}envelopes/${envelopeId}`, + }, + }); + }, + async downloadToTmp($, baseUri, documentsUri, filePath) { + const content = await this.docusign._makeRequest({ + $, + config: { + url: `${baseUri}${documentsUri.slice(1)}/${this.downloadType}`, + responseType: "arraybuffer", + }, + }); + const rawcontent = content.toString("base64"); + const buffer = Buffer.from(rawcontent, "base64"); + fs.writeFileSync(filePath, buffer); + }, + }, + async run({ $ }) { + const baseUri = await this.docusign.getBaseUri({ + accountId: this.account, + }); + const envelope = await this.getEnvelope($, baseUri, this.envelopeId); + const filePath = `/tmp/${this.filename}`; + await this.downloadToTmp($, baseUri, envelope.documentsUri, filePath); + + $.export("$summary", `Successfully downloaded files to ${filePath}`); + + return filePath; + }, +}; diff --git a/components/docusign/package.json b/components/docusign/package.json index b74c37199f617..12f1f7b65675b 100644 --- a/components/docusign/package.json +++ b/components/docusign/package.json @@ -1,18 +1,18 @@ { "name": "@pipedream/docusign", - "version": "0.1.6", + "version": "0.2.1", "description": "Pipedream Docusign Components", - "main": "docusign.app.js", + "main": "docusign.app.mjs", "keywords": [ "pipedream", "docusign" ], "homepage": "https://pipedream.com/apps/docusign", "author": "Pipedream (https://pipedream.com/)", - "dependencies": { - "@pipedream/platform": "^1.2.0" - }, "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/docusign/sources/envelope-sent-or-complete/common.mjs b/components/docusign/sources/envelope-sent-or-complete/common.mjs deleted file mode 100644 index 76408254a0e95..0000000000000 --- a/components/docusign/sources/envelope-sent-or-complete/common.mjs +++ /dev/null @@ -1,79 +0,0 @@ -import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; - -export default { - dedupe: "unique", - props: { - db: "$.service.db", - timer: { - label: "Polling Interval", - description: "Pipedream will poll the Docusign API on this schedule", - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - status: { - type: "string[]", - label: "Status", - description: "The envelope status that you are checking for", - options: [ - "sent", - "completed", - ], - default: [ - "sent", - ], - }, - }, - methods: { - _getLastEvent() { - return this.db.get("lastEvent"); - }, - _setLastEvent(lastEvent) { - this.db.set("lastEvent", lastEvent); - }, - monthAgo() { - const monthAgo = new Date(); - monthAgo.setMonth(monthAgo.getMonth() - 1); - return monthAgo; - }, - generateMeta({ - envelopeId: id, emailSubject: summary, status, - }, ts) { - return { - id: `${id}${status}`, - summary, - ts, - }; - }, - }, - async run(event) { - const { timestamp: ts } = event; - const lastEvent = this._getLastEvent() || this.monthAgo().toISOString(); - const baseUri = await this.docusign.getBaseUri({ - accountId: this.account, - }); - let done = false; - const params = { - from_date: lastEvent, - status: this.status.join(), - }; - do { - const { - envelopes = [], - nextUri, - endPosition, - } = await this.docusign.listEnvelopes(baseUri, params); - if (nextUri) { - params.start_position += endPosition + 1; - } - else done = true; - - for (const envelope of envelopes) { - const meta = this.generateMeta(envelope, ts); - this.$emit(envelope, meta); - } - } while (!done); - this._setLastEvent(new Date(ts * 1000).toISOString()); - }, -}; diff --git a/components/docusign/sources/envelope-sent-or-complete/envelope-sent-or-complete.mjs b/components/docusign/sources/envelope-sent-or-complete/envelope-sent-or-complete.mjs deleted file mode 100644 index f9bcd0e9bccb6..0000000000000 --- a/components/docusign/sources/envelope-sent-or-complete/envelope-sent-or-complete.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import docusign from "../../docusign.app.mjs"; -import common from "./common.mjs"; - -export default { - ...common, - key: "docusign-envelope-sent-or-complete", - version: "0.0.5", - name: "Envelope Sent or Complete", - description: - "Emit new event when an envelope status is set to sent or complete", - type: "source", - props: { - docusign, - account: { - propDefinition: [ - docusign, - "account", - ], - }, - ...common.props, - }, -}; diff --git a/components/docusign/sources/new-change-in-envelope-status/common.mjs b/components/docusign/sources/new-change-in-envelope-status/common.mjs new file mode 100644 index 0000000000000..9eb04e4bf76da --- /dev/null +++ b/components/docusign/sources/new-change-in-envelope-status/common.mjs @@ -0,0 +1,89 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + dedupe: "unique", + props: { + db: "$.service.db", + timer: { + label: "Polling Interval", + description: "Pipedream will poll the Docusign API on this schedule", + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + status: { + type: "string[]", + label: "Status", + description: "Watch for envelopes that have been updated to the selected statuses", + options: [ + "any", + "completed", + "created", + "declined", + "deleted", + "delivered", + "processing", + "sent", + "signed", + "timedout", + "voided", + ], + default: [ + "any", + ], + }, + }, + methods: { + _getLastEvent() { + return this.db.get("lastEvent") || this.monthAgo().toISOString(); + }, + _setLastEvent(lastEvent) { + this.db.set("lastEvent", lastEvent); + }, + monthAgo() { + const monthAgo = new Date(); + monthAgo.setMonth(monthAgo.getMonth() - 1); + return monthAgo; + }, + generateMeta({ + envelopeId, emailSubject, statusChangedDateTime, + }) { + const ts = Date.parse(statusChangedDateTime); + return { + id: `${envelopeId}-${ts}`, + summary: emailSubject, + ts, + }; + }, + }, + async run(event) { + const { timestamp: ts } = event; + const lastEvent = this._getLastEvent(); + const baseUri = await this.docusign.getBaseUri({ + accountId: this.account, + }); + let done = false; + const params = { + from_date: lastEvent, + status: this.status.join(), + }; + do { + const { + envelopes = [], + nextUri, + endPosition, + } = await this.docusign.listEnvelopes(baseUri, params); + if (nextUri) { + params.start_position += endPosition + 1; + } + else done = true; + + for (const envelope of envelopes) { + const meta = this.generateMeta(envelope); + this.$emit(envelope, meta); + } + } while (!done); + this._setLastEvent(new Date(ts * 1000).toISOString()); + }, +}; diff --git a/components/docusign/sources/new-change-in-envelope-status/new-change-in-envelope-status.mjs b/components/docusign/sources/new-change-in-envelope-status/new-change-in-envelope-status.mjs new file mode 100644 index 0000000000000..97328bb5b3c20 --- /dev/null +++ b/components/docusign/sources/new-change-in-envelope-status/new-change-in-envelope-status.mjs @@ -0,0 +1,21 @@ +import docusign from "../../docusign.app.mjs"; +import common from "./common.mjs"; + +export default { + ...common, + key: "docusign-new-change-in-envelope-status", + version: "0.0.1", + name: "New Change in Envelope Status", + description: "Emit new event when an envelope's status is updated", + type: "source", + props: { + docusign, + account: { + propDefinition: [ + docusign, + "account", + ], + }, + ...common.props, + }, +}; diff --git a/components/docusign_developer/README.md b/components/docusign_developer/README.md index a8205cc9d1588..e0423519d1827 100644 --- a/components/docusign_developer/README.md +++ b/components/docusign_developer/README.md @@ -1,9 +1,11 @@ # Overview -With the Docusign Developer API, you can build applications that allow users to -electronically sign documents, track the status of documents, and manage -document workflows. Here are some examples of what you can build: +The DocuSign Developer API enables automation around electronic agreements, signatures, and approval processes. It's a powerful tool for streamlining document workflows, allowing you to create, send, and manage documents programmatically. With Pipedream, you can harness this capability to integrate DocuSign seamlessly with other services, triggering actions based on document status, automating follow-ups, syncing data with CRM systems, and more. -- An application that allows users to electronically sign documents -- An application that tracks the status of documents -- An application that manages document workflows +# Example Use Cases + +- **Automated Contract Workflow**: When a new contract is uploaded to a Google Drive folder, Pipedream detects the file, triggers the DocuSign API to send the contract for signature, and upon completion, saves the signed version back to Google Drive. This eliminates manual handling and speeds up contract execution. + +- **CRM Integration for Signed Agreements**: Once a document is signed in DocuSign, Pipedream updates the corresponding deal or contact in Salesforce with the status. It could also attach the signed document to the CRM record, ensuring sales teams have immediate access to the final agreements. + +- **Document Status Notifications**: Set up a Pipedream workflow that listens for status changes in DocuSign documents, such as completions or rejections. When a change is detected, Pipedream sends a real-time notification via Slack to the relevant parties, keeping everyone in the loop without manual checks. diff --git a/components/docusign_developer/actions/create-signature-request/create-signature-request.mjs b/components/docusign_developer/actions/create-signature-request/create-signature-request.mjs index 45905704c0752..5965967717b84 100644 --- a/components/docusign_developer/actions/create-signature-request/create-signature-request.mjs +++ b/components/docusign_developer/actions/create-signature-request/create-signature-request.mjs @@ -1,10 +1,10 @@ import docusign from "../../docusign_developer.app.mjs"; -import common from "../../../docusign/actions/common/common.mjs"; +import common from "@pipedream/docusign/actions/common/common.mjs"; export default { ...common, key: "docusign_developer-create-signature-request", - version: "0.1.1", + version: "0.1.2", name: "Create Signature Request", description: "Creates a signature request from a template [See the documentation](https://developers.docusign.com/docs/esign-rest-api/reference/envelopes/envelopes/create)", type: "action", diff --git a/components/docusign_developer/docusign_developer.app.mjs b/components/docusign_developer/docusign_developer.app.mjs index 9226b654e6e84..8554000a41167 100644 --- a/components/docusign_developer/docusign_developer.app.mjs +++ b/components/docusign_developer/docusign_developer.app.mjs @@ -1,4 +1,4 @@ -import common from "../docusign/common.mjs"; +import common from "@pipedream/docusign/common.mjs"; export default { ...common, diff --git a/components/docusign_developer/package.json b/components/docusign_developer/package.json index d2984f872202b..771e0b71d232b 100644 --- a/components/docusign_developer/package.json +++ b/components/docusign_developer/package.json @@ -1,18 +1,19 @@ { "name": "@pipedream/docusign_developer", - "version": "0.1.3", + "version": "0.2.1", "description": "Pipedream Docusign_developer Components", - "main": "docusign_developer.app.js", + "main": "docusign_developer.app.mjs", "keywords": [ "pipedream", "docusign_developer" ], "homepage": "https://pipedream.com/apps/docusign_developer", "author": "Pipedream (https://pipedream.com/)", - "dependencies": { - "@pipedream/platform": "^1.1.1" - }, "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/docusign": "^0.2.1", + "@pipedream/platform": "^3.0.3" } } diff --git a/components/docusign_developer/sources/envelope-sent-or-complete/envelope-sent-or-complete.mjs b/components/docusign_developer/sources/envelope-sent-or-complete/envelope-sent-or-complete.mjs deleted file mode 100644 index bdaad0415f05d..0000000000000 --- a/components/docusign_developer/sources/envelope-sent-or-complete/envelope-sent-or-complete.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import docusign from "../../docusign_developer.app.mjs"; -import common from "../../../docusign/sources/envelope-sent-or-complete/common.mjs"; - -export default { - ...common, - key: "docusign_developer-envelope-sent-or-complete", - version: "0.0.2", - name: "Envelope Sent or Complete", - description: - "Emit new event when an envelope status is set to sent or complete", - type: "source", - props: { - docusign, - account: { - propDefinition: [ - docusign, - "account", - ], - }, - ...common.props, - }, -}; diff --git a/components/docusign_developer/sources/new-change-in-envelope-status/new-change-in-envelope-status.mjs b/components/docusign_developer/sources/new-change-in-envelope-status/new-change-in-envelope-status.mjs new file mode 100644 index 0000000000000..bd99792c77504 --- /dev/null +++ b/components/docusign_developer/sources/new-change-in-envelope-status/new-change-in-envelope-status.mjs @@ -0,0 +1,21 @@ +import docusign from "../../docusign_developer.app.mjs"; +import common from "@pipedream/docusign/sources/new-change-in-envelope-status/common.mjs"; + +export default { + ...common, + key: "docusign_developer-new-change-in-envelope-status", + version: "0.0.2", + name: "New Change in Envelope Status", + description: "Emit new event when an envelope's status is updated", + type: "source", + props: { + docusign, + account: { + propDefinition: [ + docusign, + "account", + ], + }, + ...common.props, + }, +}; diff --git a/components/docusign_developer/sources/new-folder/new-folder.mjs b/components/docusign_developer/sources/new-folder/new-folder.mjs index 5ec6c4b3921ba..4d60ffa7a3c3f 100644 --- a/components/docusign_developer/sources/new-folder/new-folder.mjs +++ b/components/docusign_developer/sources/new-folder/new-folder.mjs @@ -1,10 +1,10 @@ import docusign from "../../docusign_developer.app.mjs"; -import common from "../../../docusign/sources/new-folder/common.mjs"; +import common from "@pipedream/docusign/sources/new-folder/common.mjs"; export default { ...common, key: "docusign_developer-new-folder", - version: "0.0.2", + version: "0.0.3", name: "New Folder", description: "Emit new event when a new folder is created", type: "source", diff --git a/components/dokan/README.md b/components/dokan/README.md new file mode 100644 index 0000000000000..1fa9e635c16bc --- /dev/null +++ b/components/dokan/README.md @@ -0,0 +1,11 @@ +# Overview + +The Dokan API allows you to manage an online marketplace powered by Dokan, an e-commerce solution based on WooCommerce. With this API, you can automate vendor management, product listings, order tracking, and commission calculations. Integrating Dokan with Pipedream opens doors to create powerful serverless workflows that can interact with other services, handle complex automation tasks in your marketplace, and streamline operations without manual intervention. + +# Example Use Cases + +- **Vendor Onboarding and Management**: Automate the process of adding new vendors to your marketplace. When a vendor submits their details via a web form (like Typeform or Google Forms), Pipedream can capture this data and use the Dokan API to create a new vendor profile, set up their store, and send a welcome email through SendGrid or another email service. + +- **Product Inventory Sync**: Keep product listings up-to-date across multiple platforms. For example, if inventory levels change in your ERP system, Pipedream can trigger an update in Dokan to reflect these changes. This ensures that your marketplace is always in sync with your actual stock, preventing overselling and maintaining customer satisfaction. + +- **Automated Commission Payouts**: Calculate and disburse vendor commissions automatically. Use Pipedream to schedule a workflow that totals sales for each vendor, calculates the owed commission based on your marketplace's rules, and processes payments through a payment gateway like Stripe or PayPal, minimizing administrative work regarding payouts. diff --git a/components/domo/domo.app.mjs b/components/domo/domo.app.mjs new file mode 100644 index 0000000000000..125c11738eb95 --- /dev/null +++ b/components/domo/domo.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "domo", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/domo/package.json b/components/domo/package.json new file mode 100644 index 0000000000000..2c2d07042ad7c --- /dev/null +++ b/components/domo/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/domo", + "version": "0.0.1", + "description": "Pipedream Domo Components", + "main": "domo.app.mjs", + "keywords": [ + "pipedream", + "domo" + ], + "homepage": "https://pipedream.com/apps/domo", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/donately/README.md b/components/donately/README.md index de30ca7de16ad..5a19ae92cf2f7 100644 --- a/components/donately/README.md +++ b/components/donately/README.md @@ -1,9 +1,11 @@ # Overview -With the Donately API, you can build applications that: +The Donately API provides a suite of endpoints for managing fundraising activities, processing donations, and analyzing donor data. With Pipedream, you can automate donation tracking, engage donors with personalized communications, and sync fundraising data across various platforms. It's a powerful tool for nonprofits and charitable organizations looking to streamline their fundraising operations and gain deeper insights into their donor base. -- Allow donors to make donations to your organization -- View a list of your organization's donations -- View information about a specific donation -- Update information about a donation -- Delete a donation +# Example Use Cases + +- **Automated Thank-You Emails**: Trigger an email to send automatically through SendGrid or another email service each time a new donation is received via Donately. Customize the message with donor details and donation amount to create a personal touch. + +- **Real-Time Donation Alerts**: Set up a workflow to post a message in a Slack channel whenever a new donation comes in. This keeps teams informed and can foster a sense of community and urgency around fundraising campaigns. + +- **Monthly Donation Reports**: Schedule a monthly workflow that aggregates donation data from Donately, processes it to generate insights, and compiles a report. The report is then sent to a Google Sheet for easy sharing and analysis by stakeholders. diff --git a/components/donately/package.json b/components/donately/package.json new file mode 100644 index 0000000000000..0cd60bd71af7b --- /dev/null +++ b/components/donately/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/donately", + "version": "0.6.0", + "description": "Pipedream donately Components", + "main": "donately.app.mjs", + "keywords": [ + "pipedream", + "donately" + ], + "homepage": "https://pipedream.com/apps/donately", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/donedone/README.md b/components/donedone/README.md new file mode 100644 index 0000000000000..b907bd8856010 --- /dev/null +++ b/components/donedone/README.md @@ -0,0 +1,11 @@ +# Overview + +The DoneDone API opens a realm of possibilities for tracking issues and managing projects within Pipedream's serverless platform. By harnessing this API, you can automate ticket creation, update project statuses, and streamline collaboration with your team. DoneDone's simple yet powerful API provides endpoints to manage tasks, projects, people, and more, effectively becoming a backbone for your project management workflow automation. + +# Example Use Cases + +- **Automated Issue Reporting from Support Emails**: Trigger a workflow on Pipedream when a new email arrives in a dedicated support mailbox (e.g., Gmail). Parse the email content and automatically create a new issue in DoneDone, including relevant details such as the sender's information and the issue description. + +- **Project Management Sync with Google Calendar**: Whenever a new project or milestone is added in DoneDone, use this event to trigger a Pipedream workflow that creates a corresponding event in Google Calendar. This keeps your team's schedule synced and ensures all deadlines are visible and tracked outside of DoneDone as well. + +- **Slack Notifications for DoneDone Issue Updates**: Set up a Pipedream workflow to listen for status updates on issues within DoneDone. When an issue is updated, automatically send a message to a designated Slack channel or direct message to a team member, providing instant updates on ticket progress without needing to check DoneDone. diff --git a/components/donedone/package.json b/components/donedone/package.json index 7855a2134b55c..7333142ca3169 100644 --- a/components/donedone/package.json +++ b/components/donedone/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/donorbox/donorbox.app.mjs b/components/donorbox/donorbox.app.mjs new file mode 100644 index 0000000000000..ba15e000f53b7 --- /dev/null +++ b/components/donorbox/donorbox.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "donorbox", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/donorbox/package.json b/components/donorbox/package.json new file mode 100644 index 0000000000000..6970ccc183d4d --- /dev/null +++ b/components/donorbox/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/donorbox", + "version": "0.0.1", + "description": "Pipedream Donorbox Components", + "main": "donorbox.app.mjs", + "keywords": [ + "pipedream", + "donorbox" + ], + "homepage": "https://pipedream.com/apps/donorbox", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/doppler/README.md b/components/doppler/README.md new file mode 100644 index 0000000000000..1d8acd2c92523 --- /dev/null +++ b/components/doppler/README.md @@ -0,0 +1,11 @@ +# Overview + +Doppler is an email marketing platform that allows users to create, send, and analyze email campaigns. Integrating Doppler with Pipedream enables you to automate tasks like synchronizing contacts, triggering email campaigns based on specific events, and capturing campaign analytics. Pipedream's serverless platform connects with Doppler via its API, letting you create custom workflows to extend your email marketing strategies with real-time data from various sources. + +# Example Use Cases + +- **Sync New Users to Doppler List**: Automatically add new users from your app's database to a Doppler mailing list whenever a new account is created. This ensures that your email marketing lists are always up-to-date without manual intervention. + +- **Send Triggered Emails After E-Commerce Transactions**: Connect Doppler with an e-commerce platform, like Shopify. Set up a workflow that triggers a personalized email campaign to customers who've completed a purchase, leveraging the power of Doppler's segmentation and customization features. + +- **Analyze Campaign Performance in Google Sheets**: After an email campaign is sent, collect performance data like open rates and click-through rates. Use Pipedream to send this data to a Google Sheet for further analysis and visualization, enabling data-driven decisions for future campaigns. diff --git a/components/doppler/package.json b/components/doppler/package.json index aa7e54f4a84db..578fafa887ab8 100644 --- a/components/doppler/package.json +++ b/components/doppler/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/doppler_marketing_automation/README.md b/components/doppler_marketing_automation/README.md index 119b2c37f5f46..07455ce467c60 100644 --- a/components/doppler_marketing_automation/README.md +++ b/components/doppler_marketing_automation/README.md @@ -1,14 +1,11 @@ # Overview -With the Doppler Marketing Automation API, you can build a variety of -applications that can automate your marketing tasks. Here are some examples of -what you can build: - -- An application that can send out automated emails based on certain triggers -- A web application that can track customer engagement and segment them - accordingly -- An application that can automatically add or remove customers from a mailing - list -- A tool that can help you measure the effectiveness of your marketing - campaigns -- And more! +The Doppler Marketing Automation API allows you to automate email marketing campaigns, manage subscriber lists, and analyze the performance of your email marketing efforts. When integrated with Pipedream, you can craft workflows that respond to events in real-time, sync data across various platforms, and personalize your marketing strategy, all while leveraging the serverless execution environment to scale as needed. + +# Example Use Cases + +- **Subscriber Segmentation and Targeted Campaigns**: Automate the process of segmenting subscribers based on their behavior by connecting Doppler with a CRM like Salesforce. When a new lead is added or updated in the CRM, it triggers a Pipedream workflow that adds or updates the subscriber in a Doppler's specific list, ensuring targeted campaigns are sent to the right audience. + +- **E-commerce Cart Abandonment Follow-ups**: Integrate Doppler with an e-commerce platform like Shopify. Configure a workflow on Pipedream that listens for cart abandonment events and automatically sends a personalized follow-up email via Doppler, encouraging the potential customer to complete their purchase. + +- **Event-driven Content Delivery**: Pair Doppler with a scheduling app like Google Calendar. Set up a Pipedream workflow that triggers an email campaign in Doppler whenever a new event is added to a specific Google Calendar, delivering timely content related to the event, such as webinar details, to your subscribers. diff --git a/components/doppler_marketing_automation/actions/add-subscriber/add-subscriber.mjs b/components/doppler_marketing_automation/actions/add-subscriber/add-subscriber.mjs deleted file mode 100644 index 768b2d3fb0d4b..0000000000000 --- a/components/doppler_marketing_automation/actions/add-subscriber/add-subscriber.mjs +++ /dev/null @@ -1,60 +0,0 @@ -import app from "../../doppler_marketing_automation.app.mjs"; - -export default { - name: "Add Subscriber", - version: "0.0.1", - key: "doppler_marketing_automation-add-subscriber", - description: "Add new subscriber. [See the documentation](https://restapi.fromdoppler.com/docs/resources#!/Subscribers/AccountsByAccountNameSubscribersPost)", - type: "action", - props: { - app, - email: { - label: "Subscriber Email", - description: "The subscriber email", - type: "string", - }, - birthday: { - type: "string", - label: "Birthday", - description: "Birthday of the subscriber in YYYY-MM-DD format. E.g. `1999-05-28`", - }, - firstName: { - type: "string", - label: "First Name", - description: "First name of the subscriber", - }, - lastName: { - type: "string", - label: "Last Name", - description: "Last name of the subscriber", - }, - }, - async run({ $ }) { - const response = await this.app.addSubscriber({ - $, - data: { - email: this.email, - fields: [ - { - name: "BIRTHDAY", - value: this.birthday, - }, - { - name: "FIRSTNAME", - value: this.firstName, - }, - { - name: "LASTNAME", - value: this.lastName, - }, - ], - }, - }); - - if (response) { - $.export("$summary", `Successfully added subscriber with email ${this.email}`); - } - - return response; - }, -}; diff --git a/components/doppler_marketing_automation/actions/add-update-subscriber/add-update-subscriber.mjs b/components/doppler_marketing_automation/actions/add-update-subscriber/add-update-subscriber.mjs new file mode 100644 index 0000000000000..7c6dd75742eff --- /dev/null +++ b/components/doppler_marketing_automation/actions/add-update-subscriber/add-update-subscriber.mjs @@ -0,0 +1,93 @@ +import { + COUNTRY_OPTIONS, GENDER_OPTIONS, +} from "../../common/constants.mjs"; +import app from "../../doppler_marketing_automation.app.mjs"; + +export default { + key: "doppler_marketing_automation-add-update-subscriber", + name: "Add or Update Subscriber", + description: "Adds a new subscriber or updates an existing one. [See the documentation](https://restapi.fromdoppler.com/docs/resources#!/Subscribers/AccountsByAccountNameListsByListIdSubscribersPost)", + version: "0.0.2", + type: "action", + props: { + app, + listId: { + propDefinition: [ + app, + "listId", + ], + }, + subscriberEmail: { + type: "string", + label: "Subscriber Email", + description: "The email of the subscriber to add or update.", + }, + firstName: { + type: "string", + label: "First Name", + description: "Subscriber first name", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Subscriber last name", + optional: true, + }, + birthDate: { + type: "string", + label: "Birth Date", + description: "The birth date of the subscriber", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "Optional country of the subscriber", + options: COUNTRY_OPTIONS, + optional: true, + }, + gender: { + type: "string", + label: "Gender", + description: "The gender of the subscriber", + options: GENDER_OPTIONS, + optional: true, + }, + }, + async run({ $ }) { + const fields = []; + if (this.firstName) fields.push({ + name: "FIRSTNAME", + value: this.firstName, + }); + if (this.lastName) fields.push({ + name: "LASTNAME", + value: this.lastName, + }); + if (this.birthDate) fields.push({ + name: "BIRTHDAY", + value: this.birthDate, + }); + if (this.country) fields.push({ + name: "COUNTRY", + value: this.country, + }); + if (this.gender) fields.push({ + name: "GENDER", + value: this.gender, + }); + + const response = await this.app.addOrUpdateSubscriber({ + $, + listId: this.listId, + data: { + email: this.subscriberEmail, + fields, + }, + }); + + $.export("$summary", `Successfully added or updated subscriber with email: ${this.subscriberEmail}`); + return response; + }, +}; diff --git a/components/doppler_marketing_automation/actions/get-subscriber/get-subscriber.mjs b/components/doppler_marketing_automation/actions/get-subscriber/get-subscriber.mjs index 77665ebee5b12..0678c6e9ff4e6 100644 --- a/components/doppler_marketing_automation/actions/get-subscriber/get-subscriber.mjs +++ b/components/doppler_marketing_automation/actions/get-subscriber/get-subscriber.mjs @@ -2,7 +2,7 @@ import app from "../../doppler_marketing_automation.app.mjs"; export default { name: "Get Subscriber", - version: "0.0.1", + version: "0.0.2", key: "doppler_marketing_automation-get-subscriber", description: "Get a subscriber. [See the documentation](https://restapi.fromdoppler.com/docs/resources#!/Subscribers/AccountsByAccountNameSubscribersByEmailGet)", type: "action", diff --git a/components/doppler_marketing_automation/actions/remove-subscriber/remove-subscriber.mjs b/components/doppler_marketing_automation/actions/remove-subscriber/remove-subscriber.mjs index cabbdcb1b5b97..d5ba344af5f8e 100644 --- a/components/doppler_marketing_automation/actions/remove-subscriber/remove-subscriber.mjs +++ b/components/doppler_marketing_automation/actions/remove-subscriber/remove-subscriber.mjs @@ -1,10 +1,10 @@ import app from "../../doppler_marketing_automation.app.mjs"; export default { - name: "Remove Subscriber", - version: "0.0.1", key: "doppler_marketing_automation-remove-subscriber", - description: "Remove a subscriber. [See the documentation](https://restapi.fromdoppler.com/docs/resources#!/Subscribers/AccountsByAccountNameListsByListIdSubscribersByEmailDelete)", + name: "Remove Subscriber", + description: "Removes a subscriber from a list completely. [See the documentation](https://restapi.fromdoppler.com/docs/resources#!/Subscribers/AccountsByAccountNameListsByListIdSubscribersByEmailDelete)", + version: "0.0.2", type: "action", props: { app, @@ -18,8 +18,8 @@ export default { propDefinition: [ app, "subscriberEmail", - (c) => ({ - listId: c.listId, + ({ listId }) => ({ + listId, }), ], }, @@ -30,11 +30,7 @@ export default { email: this.email, listId: this.listId, }); - - if (response) { - $.export("$summary", `Successfully removed subscriber with email ${this.email}`); - } - + $.export("$summary", `Successfully removed subscriber: ${this.email} from list: ${this.listId}`); return response; }, }; diff --git a/components/doppler_marketing_automation/actions/unsubscribe-email/unsubscribe-email.mjs b/components/doppler_marketing_automation/actions/unsubscribe-email/unsubscribe-email.mjs new file mode 100644 index 0000000000000..a58c73c2be9fb --- /dev/null +++ b/components/doppler_marketing_automation/actions/unsubscribe-email/unsubscribe-email.mjs @@ -0,0 +1,31 @@ +import app from "../../doppler_marketing_automation.app.mjs"; + +export default { + key: "doppler_marketing_automation-unsubscribe-email", + name: "Unsubscribe Email", + description: "Unsubscribe an email address from the account. Once unsubscribed, the user will not receive any more communication. [See the documentation](https://restapi.fromdoppler.com/docs/resources#!/Subscribers/AccountsByAccountNameUnsubscribedPost)", + version: "0.0.2", + type: "action", + props: { + app, + email: { + propDefinition: [ + app, + "subscriberEmail", + () => ({ + filter: ({ status }) => status != "unsubscribed", + }), + ], + }, + }, + async run({ $ }) { + const response = await this.app.unsubscribeSubscriber({ + $, + data: { + email: this.email, + }, + }); + $.export("$summary", `Successfully unsubscribed email: ${this.email}`); + return response; + }, +}; diff --git a/components/doppler_marketing_automation/common/constants.mjs b/components/doppler_marketing_automation/common/constants.mjs new file mode 100644 index 0000000000000..2f2fdb8ef86e7 --- /dev/null +++ b/components/doppler_marketing_automation/common/constants.mjs @@ -0,0 +1,1009 @@ +export const GENDER_OPTIONS = [ + { + label: "Female", + value: "F", + }, + { + label: "Male", + value: "M", + }, +]; + +export const COUNTRY_OPTIONS = [ + { + label: "Argentina", + value: "AR", + }, + { + label: "United States", + value: "US", + }, + { + label: "Brazil", + value: "BR", + }, + { + label: "Mexico", + value: "MX", + }, + { + label: "Chile", + value: "CL", + }, + { + label: "Guatemala", + value: "GT", + }, + { + label: "Paraguay", + value: "PY", + }, + { + label: "France", + value: "FR", + }, + { + label: "Italy", + value: "IT", + }, + { + label: "Spain", + value: "ES", + }, + { + label: "China", + value: "CN", + }, + { + label: "Japan", + value: "JP", + }, + { + label: "Afghanistan", + value: "AF", + }, + { + label: "Åland Islands", + value: "AX", + }, + { + label: "Albania", + value: "AL", + }, + { + label: "Algeria", + value: "DZ", + }, + { + label: "American Samoa", + value: "AS", + }, + { + label: "Andorra", + value: "AD", + }, + { + label: "Angola", + value: "AO", + }, + { + label: "Anguilla", + value: "AI", + }, + { + label: "Antarctica", + value: "AQ", + }, + { + label: "Antigua and Barbuda", + value: "AG", + }, + { + label: "Armenia", + value: "AM", + }, + { + label: "Aruba", + value: "AW", + }, + { + label: "Australia", + value: "AU", + }, + { + label: "Austria", + value: "AT", + }, + { + label: "Azerbaijan", + value: "AZ", + }, + { + label: "Bahamas", + value: "BS", + }, + { + label: "Bahrain", + value: "BH", + }, + { + label: "Bangladesh", + value: "BD", + }, + { + label: "Barbados", + value: "BB", + }, + { + label: "Belarus", + value: "BY", + }, + { + label: "Belgium", + value: "BE", + }, + { + label: "Belize", + value: "BZ", + }, + { + label: "Benin", + value: "BJ", + }, + { + label: "Bermuda", + value: "BM", + }, + { + label: "Bhutan", + value: "BT", + }, + { + label: "Bolivia, Plurinational State of", + value: "BO", + }, + { + label: "Bonaire, Sint Eustatius and Saba", + value: "BQ", + }, + { + label: "Bosnia and Herzegovina", + value: "BA", + }, + { + label: "Botswana", + value: "BW", + }, + { + label: "Bouvet Island", + value: "BV", + }, + { + label: "British Indian Ocean Territory", + value: "IO", + }, + { + label: "Brunei Darussalam", + value: "BN", + }, + { + label: "Bulgaria", + value: "BG", + }, + { + label: "Burkina Faso", + value: "BF", + }, + { + label: "Burundi", + value: "BI", + }, + { + label: "Cambodia", + value: "KH", + }, + { + label: "Cameroon", + value: "CM", + }, + { + label: "Canada", + value: "CA", + }, + { + label: "Cape Verde", + value: "CV", + }, + { + label: "Cayman Islands", + value: "KY", + }, + { + label: "Central African Republic", + value: "CF", + }, + { + label: "Chad", + value: "TD", + }, + { + label: "Christmas Island", + value: "CX", + }, + { + label: "Cocos (Keeling) Islands", + value: "CC", + }, + { + label: "Colombia", + value: "CO", + }, + { + label: "Comoros", + value: "KM", + }, + { + label: "Congo", + value: "CG", + }, + { + label: "Congo, the Democratic Republic of the", + value: "CD", + }, + { + label: "Cook Islands", + value: "CK", + }, + { + label: "Costa Rica", + value: "CR", + }, + { + label: "Côte d'Ivoire", + value: "CI", + }, + { + label: "Croatia", + value: "HR", + }, + { + label: "Cuba", + value: "CU", + }, + { + label: "Curaçao", + value: "CW", + }, + { + label: "Cyprus", + value: "CY", + }, + { + label: "Czech Republic", + value: "CZ", + }, + { + label: "Denmark", + value: "DK", + }, + { + label: "Djibouti", + value: "DJ", + }, + { + label: "Dominica", + value: "DM", + }, + { + label: "Dominican Republic", + value: "DO", + }, + { + label: "Ecuador", + value: "EC", + }, + { + label: "Egypt", + value: "EG", + }, + { + label: "El Salvador", + value: "SV", + }, + { + label: "Equatorial Guinea", + value: "GQ", + }, + { + label: "Eritrea", + value: "ER", + }, + { + label: "Estonia", + value: "EE", + }, + { + label: "Ethiopia", + value: "ET", + }, + { + label: "Falkland Islands (Malvinas)", + value: "FK", + }, + { + label: "Faroe Islands", + value: "FO", + }, + { + label: "Fiji", + value: "FJ", + }, + { + label: "Finland", + value: "FI", + }, + { + label: "French Guiana", + value: "GF", + }, + { + label: "French Polynesia", + value: "PF", + }, + { + label: "French Southern Territories", + value: "TF", + }, + { + label: "Gabon", + value: "GA", + }, + { + label: "Gambia", + value: "GM", + }, + { + label: "Georgia", + value: "GE", + }, + { + label: "Germany", + value: "DE", + }, + { + label: "Ghana", + value: "GH", + }, + { + label: "Gibraltar", + value: "GI", + }, + { + label: "Greece", + value: "GR", + }, + { + label: "Greenland", + value: "GL", + }, + { + label: "Grenada", + value: "GD", + }, + { + label: "Guadeloupe", + value: "GP", + }, + { + label: "Guam", + value: "GU", + }, + { + label: "Guernsey", + value: "GG", + }, + { + label: "Guinea", + value: "GN", + }, + { + label: "Guinea-Bissau", + value: "GW", + }, + { + label: "Guyana", + value: "GY", + }, + { + label: "Haiti", + value: "HT", + }, + { + label: "Heard Island and McDonald Islands", + value: "HM", + }, + { + label: "Holy See (Vatican City State)", + value: "VA", + }, + { + label: "Honduras", + value: "HN", + }, + { + label: "Hong Kong", + value: "HK", + }, + { + label: "Hungary", + value: "HU", + }, + { + label: "Iceland", + value: "IS", + }, + { + label: "India", + value: "IN", + }, + { + label: "Indonesia", + value: "ID", + }, + { + label: "Iran, Islamic Republic of", + value: "IR", + }, + { + label: "Iraq", + value: "IQ", + }, + { + label: "Ireland", + value: "IE", + }, + { + label: "Isle of Man", + value: "IM", + }, + { + label: "Israel", + value: "IL", + }, + { + label: "Jamaica", + value: "JM", + }, + { + label: "Jersey", + value: "JE", + }, + { + label: "Jordan", + value: "JO", + }, + { + label: "Kazakhstan", + value: "KZ", + }, + { + label: "Kenya", + value: "KE", + }, + { + label: "Kiribati", + value: "KI", + }, + { + label: "Korea, Democratic People's Republic of", + value: "KP", + }, + { + label: "Korea, Republic of", + value: "KR", + }, + { + label: "Kuwait", + value: "KW", + }, + { + label: "Kyrgyzstan", + value: "KG", + }, + { + label: "Lao People's Democratic Republic", + value: "LA", + }, + { + label: "Latvia", + value: "LV", + }, + { + label: "Lebanon", + value: "LB", + }, + { + label: "Lesotho", + value: "LS", + }, + { + label: "Liberia", + value: "LR", + }, + { + label: "Libya", + value: "LY", + }, + { + label: "Liechtenstein", + value: "LI", + }, + { + label: "Lithuania", + value: "LT", + }, + { + label: "Luxembourg", + value: "LU", + }, + { + label: "Macao", + value: "MO", + }, + { + label: "Macedonia, the Former Yugoslav Republic of", + value: "MK", + }, + { + label: "Madagascar", + value: "MG", + }, + { + label: "Malawi", + value: "MW", + }, + { + label: "Malaysia", + value: "MY", + }, + { + label: "Maldives", + value: "MV", + }, + { + label: "Mali", + value: "ML", + }, + { + label: "Malta", + value: "MT", + }, + { + label: "Marshall Islands", + value: "MH", + }, + { + label: "Martinique", + value: "MQ", + }, + { + label: "Mauritania", + value: "MR", + }, + { + label: "Mauritius", + value: "MU", + }, + { + label: "Mayotte", + value: "YT", + }, + { + label: "Micronesia, Federated States of", + value: "FM", + }, + { + label: "Moldova, Republic of", + value: "MD", + }, + { + label: "Monaco", + value: "MC", + }, + { + label: "Mongolia", + value: "MN", + }, + { + label: "Montenegro", + value: "ME", + }, + { + label: "Montserrat", + value: "MS", + }, + { + label: "Morocco", + value: "MA", + }, + { + label: "Mozambique", + value: "MZ", + }, + { + label: "Myanmar", + value: "MM", + }, + { + label: "Namibia", + value: "NA", + }, + { + label: "Nauru", + value: "NR", + }, + { + label: "Nepal", + value: "NP", + }, + { + label: "Netherlands", + value: "NL", + }, + { + label: "New Caledonia", + value: "NC", + }, + { + label: "New Zealand", + value: "NZ", + }, + { + label: "Nicaragua", + value: "NI", + }, + { + label: "Niger", + value: "NE", + }, + { + label: "Nigeria", + value: "NG", + }, + { + label: "Niue", + value: "NU", + }, + { + label: "Norfolk Island", + value: "NF", + }, + { + label: "Northern Mariana Islands", + value: "MP", + }, + { + label: "Norway", + value: "NO", + }, + { + label: "Oman", + value: "OM", + }, + { + label: "Pakistan", + value: "PK", + }, + { + label: "Palau", + value: "PW", + }, + { + label: "Palestine, State of", + value: "PS", + }, + { + label: "Panama", + value: "PA", + }, + { + label: "Papua New Guinea", + value: "PG", + }, + { + label: "Peru", + value: "PE", + }, + { + label: "Philippines", + value: "PH", + }, + { + label: "Pitcairn", + value: "PN", + }, + { + label: "Poland", + value: "PL", + }, + { + label: "Portugal", + value: "PT", + }, + { + label: "Puerto Rico", + value: "PR", + }, + { + label: "Qatar", + value: "QA", + }, + { + label: "Réunion", + value: "RE", + }, + { + label: "Romania", + value: "RO", + }, + { + label: "Russian Federation", + value: "RU", + }, + { + label: "Rwanda", + value: "RW", + }, + { + label: "Saint Barthélemy", + value: "BL", + }, + { + label: "Saint Helena, Ascension and Tristan da Cunha", + value: "SH", + }, + { + label: "Saint Kitts and Nevis", + value: "KN", + }, + { + label: "Saint Lucia", + value: "LC", + }, + { + label: "Saint Martin (French part)", + value: "MF", + }, + { + label: "Saint Pierre and Miquelon", + value: "PM", + }, + { + label: "Saint Vincent and the Grenadines", + value: "VC", + }, + { + label: "Samoa", + value: "WS", + }, + { + label: "San Marino", + value: "SM", + }, + { + label: "Sao Tome and Principe", + value: "ST", + }, + { + label: "Saudi Arabia", + value: "SA", + }, + { + label: "Senegal", + value: "SN", + }, + { + label: "Serbia", + value: "RS", + }, + { + label: "Seychelles", + value: "SC", + }, + { + label: "Sierra Leone", + value: "SL", + }, + { + label: "Singapore", + value: "SG", + }, + { + label: "Sint Maarten (Dutch part)", + value: "SX", + }, + { + label: "Slovakia", + value: "SK", + }, + { + label: "Slovenia", + value: "SI", + }, + { + label: "Solomon Islands", + value: "SB", + }, + { + label: "Somalia", + value: "SO", + }, + { + label: "South Africa", + value: "ZA", + }, + { + label: "South Georgia and the South Sandwich Islands", + value: "GS", + }, + { + label: "South Sudan", + value: "SS", + }, + { + label: "Sri Lanka", + value: "LK", + }, + { + label: "Sudan", + value: "SD", + }, + { + label: "Suriname", + value: "SR", + }, + { + label: "Svalbard and Jan Mayen", + value: "SJ", + }, + { + label: "Swaziland", + value: "SZ", + }, + { + label: "Sweden", + value: "SE", + }, + { + label: "Switzerland", + value: "CH", + }, + { + label: "Syrian Arab Republic", + value: "SY", + }, + { + label: "Taiwan, Province of China", + value: "TW", + }, + { + label: "Tajikistan", + value: "TJ", + }, + { + label: "Tanzania, United Republic of", + value: "TZ", + }, + { + label: "Thailand", + value: "TH", + }, + { + label: "Timor-Leste", + value: "TL", + }, + { + label: "Togo", + value: "TG", + }, + { + label: "Tokelau", + value: "TK", + }, + { + label: "Tonga", + value: "TO", + }, + { + label: "Trinidad and Tobago", + value: "TT", + }, + { + label: "Tunisia", + value: "TN", + }, + { + label: "Turkey", + value: "TR", + }, + { + label: "Turkmenistan", + value: "TM", + }, + { + label: "Turks and Caicos Islands", + value: "TC", + }, + { + label: "Tuvalu", + value: "TV", + }, + { + label: "Uganda", + value: "UG", + }, + { + label: "Ukraine", + value: "UA", + }, + { + label: "United Arab Emirates", + value: "AE", + }, + { + label: "United Kingdom", + value: "GB", + }, + { + label: "United States Minor Outlying Islands", + value: "UM", + }, + { + label: "Uruguay", + value: "UY", + }, + { + label: "Uzbekistan", + value: "UZ", + }, + { + label: "Vanuatu", + value: "VU", + }, + { + label: "Venezuela, Bolivarian Republic of", + value: "VE", + }, + { + label: "Viet Nam", + value: "VN", + }, + { + label: "Virgin Islands, British", + value: "VG", + }, + { + label: "Virgin Islands, U.S.", + value: "VI", + }, + { + label: "Wallis and Futuna", + value: "WF", + }, + { + label: "Western Sahara", + value: "EH", + }, + { + label: "Yemen", + value: "YE", + }, + { + label: "Zambia", + value: "ZM", + }, + { + label: "Zimbabwe", + value: "ZW", + }, +]; diff --git a/components/doppler_marketing_automation/doppler_marketing_automation.app.mjs b/components/doppler_marketing_automation/doppler_marketing_automation.app.mjs index 7868d8f697fc8..44c21284b3715 100644 --- a/components/doppler_marketing_automation/doppler_marketing_automation.app.mjs +++ b/components/doppler_marketing_automation/doppler_marketing_automation.app.mjs @@ -8,12 +8,17 @@ export default { label: "List ID", description: "The list ID", type: "string", - async options() { - const { items: resources } = await this.getLists(); - - return resources.map((resource) => ({ - value: resource.listId, - label: resource.name, + async options({ page }) { + const { items } = await this.listLists({ + params: { + page: page + 1, + }, + }); + return items.map(({ + listId: value, name: label, + }) => ({ + label, + value, })); }, }, @@ -21,56 +26,66 @@ export default { label: "Subscriber Email", description: "The subscriber email", type: "string", - async options({ listId }) { - const { items: resources } = await this.getSubscribers({ + async options({ + page, listId, filter = () => true, + }) { + const { items } = await this.listSubscribers({ listId, + params: { + page: page + 1, + }, }); - - return resources.map((resource) => resource.email); + return items.filter(filter).map(({ email }) => email); }, }, + fields: { + label: "Fields", + description: "Optional fields for a subscriber in JSON format", + type: "string[]", + optional: true, + }, + origin: { + label: "Origin Header", + description: "Value for the X-Doppler-Subscriber-Origin header", + type: "string", + optional: true, + }, }, methods: { - _apiKey() { - return this.$auth.api_key; + _baseUrl() { + return `https://restapi.fromdoppler.com/accounts/${this.$auth.account_name}`; }, - _accountName() { - return this.$auth.account_name; + _headers() { + return { + "Authorization": `token ${this.$auth.api_key}`, + }; }, - _apiUrl() { - return `https://restapi.fromdoppler.com/accounts/${this._accountName()}`; - }, - async _makeRequest({ - $ = this, path, ...args + _makeRequest({ + $ = this, path, ...opts }) { return axios($, { - url: `${this._apiUrl()}${path}`, - ...args, - headers: { - ...args.headers, - "Authorization": `token ${this._apiKey()}`, - }, + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, }); }, - async getLists(args = {}) { + listLists(args = {}) { return this._makeRequest({ path: "/lists", ...args, }); }, - async getSubscribers({ - listId, ...args + listSubscribers({ + listId = null, ...opts }) { - const extraPath = listId - ? `/lists/${listId}` - : ""; - return this._makeRequest({ - path: `${extraPath}/subscribers`, - ...args, + path: `${listId + ? `/lists/${listId}` + : ""}/subscribers`, + ...opts, }); }, - async getSubscriber({ + getSubscriber({ email, ...args }) { return this._makeRequest({ @@ -78,19 +93,28 @@ export default { ...args, }); }, - async addSubscriber(args = {}) { + addOrUpdateSubscriber({ + listId, ...opts + }) { return this._makeRequest({ - path: "/subscribers", - method: "post", - ...args, + method: "POST", + path: `/lists/${listId}/subscribers`, + ...opts, + }); + }, + unsubscribeSubscriber(opts = {}) { + return this._makeRequest({ + path: "/unsubscribed", + method: "POST", + ...opts, }); }, - async removeSubscriber({ + removeSubscriber({ email, listId, ...args }) { return this._makeRequest({ path: `/lists/${listId}/subscribers/${email}`, - method: "delete", + method: "DELETE", ...args, }); }, diff --git a/components/doppler_marketing_automation/package.json b/components/doppler_marketing_automation/package.json index b3034b7124ce7..7e42c86bfef44 100644 --- a/components/doppler_marketing_automation/package.json +++ b/components/doppler_marketing_automation/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/doppler_marketing_automation", - "version": "0.1.0", + "version": "0.2.1", "description": "Pipedream Doppler Marketing Automation Components", "main": "doppler_marketing_automation.app.mjs", "keywords": [ @@ -13,6 +13,6 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.5.1" + "@pipedream/platform": "^3.0.3" } } diff --git a/components/dopplerai/README.md b/components/dopplerai/README.md new file mode 100644 index 0000000000000..3b4e9d41d37c4 --- /dev/null +++ b/components/dopplerai/README.md @@ -0,0 +1,11 @@ +# Overview + +DopplerAI's API provides powerful tools for predictive analysis, tapping into AI to forecast trends, analyze data patterns, and make informed decisions. By integrating this API into Pipedream, you can automate workflows that react to insights, combine data from various sources, and trigger actions across apps. It's a fantastic way to turn data into actionable intelligence without manual intervention. + +# Example Use Cases + +- **Sales Forecasting Automation**: Set up a workflow that pulls sales data from your CRM, sends it to DopplerAI for forecast analysis, and then stores the predictions in a Google Sheets document for easy access and sharing among your sales team. + +- **Dynamic Pricing Strategy**: Create a Pipedream workflow that adjusts the pricing of your online store items based on DopplerAI's demand predictions. It can fetch product data from your eCommerce platform, run it through DopplerAI's API, and then update prices on your site accordingly. + +- **Social Media Sentiment Analysis**: Design a workflow where social media posts about your brand are collected via a platform like Twitter, analyzed by DopplerAI for sentiment, and the results are then used to automatically generate reports or notify your marketing team. diff --git a/components/dots_/README.md b/components/dots_/README.md deleted file mode 100644 index 56498629af634..0000000000000 --- a/components/dots_/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Overview - -Dots is a simple, powerful API for creating and manipulating dots on a HTML5 -Canvas. With Dots, you can create anything from simple animations to full-blown -games. - -Here are some examples of what you can build with the Dots API: - -- Animations -- Games -- Interactive applications -- Data visualizations diff --git a/components/dotsimple/actions/create-post/create-post.mjs b/components/dotsimple/actions/create-post/create-post.mjs new file mode 100644 index 0000000000000..b7d1031949c2d --- /dev/null +++ b/components/dotsimple/actions/create-post/create-post.mjs @@ -0,0 +1,71 @@ +import app from "../../dotsimple.app.mjs"; + +export default { + key: "dotsimple-create-post", + name: "Create Post", + description: "Create a new post on your DotSimple site. [See the documentation](https://help.dotsimple.io/en/articles/68-posts).", + version: "0.0.1", + type: "action", + props: { + app, + accountId: { + propDefinition: [ + app, + "accountId", + ], + }, + body: { + propDefinition: [ + app, + "contentBody", + ], + }, + url: { + propDefinition: [ + app, + "contentUrl", + ], + }, + }, + methods: { + createPost(args = {}) { + return this.app.post({ + path: "/posts", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createPost, + accountId, + body, + url, + } = this; + + const response = await createPost({ + $, + data: { + accounts: [ + accountId, + ], + versions: [ + { + account_id: accountId, + is_original: true, + content: [ + { + body, + url, + }, + ], + }, + ], + }, + }); + + $.export("$summary", `Successfully created a new post with ID \`${response.id}\``); + + return response; + }, +}; diff --git a/components/dotsimple/actions/delete-post/delete-post.mjs b/components/dotsimple/actions/delete-post/delete-post.mjs new file mode 100644 index 0000000000000..0fd92cd3fa729 --- /dev/null +++ b/components/dotsimple/actions/delete-post/delete-post.mjs @@ -0,0 +1,40 @@ +import app from "../../dotsimple.app.mjs"; + +export default { + key: "dotsimple-delete-post", + name: "Delete Post", + description: "Remove a post from your DotSimple site. [See the documentation](https://help.dotsimple.io/en/articles/68-posts)", + version: "0.0.1", + type: "action", + props: { + app, + postId: { + propDefinition: [ + app, + "postId", + ], + }, + }, + methods: { + deletePost({ + postId, ...args + } = {}) { + return this.app.delete({ + path: `/posts/${postId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + deletePost, + postId, + } = this; + const response = await deletePost({ + $, + postId, + }); + $.export("$summary", "Successfully deleted post."); + return response; + }, +}; diff --git a/components/dotsimple/actions/update-post/update-post.mjs b/components/dotsimple/actions/update-post/update-post.mjs new file mode 100644 index 0000000000000..a93e10ac9e933 --- /dev/null +++ b/components/dotsimple/actions/update-post/update-post.mjs @@ -0,0 +1,81 @@ +import app from "../../dotsimple.app.mjs"; + +export default { + key: "dotsimple-update-post", + name: "Update Post", + description: "Amend an existing post on your DotSimple site. [See the documentation](https://help.dotsimple.io/en/articles/68-posts)", + version: "0.0.1", + type: "action", + props: { + app, + postId: { + propDefinition: [ + app, + "postId", + ], + }, + accountId: { + propDefinition: [ + app, + "accountId", + ], + }, + body: { + propDefinition: [ + app, + "contentBody", + ], + }, + url: { + propDefinition: [ + app, + "contentUrl", + ], + }, + }, + methods: { + updatePost({ + postId, ...args + } = {}) { + return this.app.put({ + path: `/posts/${postId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + updatePost, + postId, + accountId, + body, + url, + } = this; + + const response = await updatePost({ + $, + postId, + data: { + accounts: [ + accountId, + ], + versions: [ + { + account_id: accountId, + is_original: true, + content: [ + { + body, + url, + }, + ], + }, + ], + }, + }); + + $.export("$summary", "Successfully updated post."); + + return response; + }, +}; diff --git a/components/dotsimple/common/constants.mjs b/components/dotsimple/common/constants.mjs new file mode 100644 index 0000000000000..c8d670a211a53 --- /dev/null +++ b/components/dotsimple/common/constants.mjs @@ -0,0 +1,13 @@ +const BASE_URL = "https://app.dotsimple.io"; +const WORKSPACE_PLACEHOLDER = ""; +const VERSION_PATH = `/app/api/${WORKSPACE_PLACEHOLDER}`; +const DEFAULT_MAX = 100; +const DEFAULT_LIMIT = 20; + +export default { + BASE_URL, + VERSION_PATH, + WORKSPACE_PLACEHOLDER, + DEFAULT_MAX, + DEFAULT_LIMIT, +}; diff --git a/components/dotsimple/common/utils.mjs b/components/dotsimple/common/utils.mjs new file mode 100644 index 0000000000000..903b2593ed3c2 --- /dev/null +++ b/components/dotsimple/common/utils.mjs @@ -0,0 +1,11 @@ +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +export default { + iterate, +}; diff --git a/components/dotsimple/dotsimple.app.mjs b/components/dotsimple/dotsimple.app.mjs new file mode 100644 index 0000000000000..25f31af5355b2 --- /dev/null +++ b/components/dotsimple/dotsimple.app.mjs @@ -0,0 +1,153 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; + +export default { + type: "app", + app: "dotsimple", + propDefinitions: { + accountId: { + type: "string", + label: "Account ID", + description: "The ID of the account you wish to use.", + async options() { + const { data } = await this.listAccounts(); + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + contentBody: { + type: "string", + label: "Content Body", + description: "The content of the post.", + }, + contentUrl: { + type: "string", + label: "Content URL", + description: "Insert a URL for the post. Facebook and LinkedIn support it.", + optional: true, + }, + postId: { + type: "string", + label: "Post ID", + description: "The ID of the post you wish to delete or update.", + async options({ page }) { + const { data } = await this.listPosts({ + params: { + page: page + 1, + }, + }); + return data.map(({ uuid }) => uuid); + }, + }, + }, + methods: { + getUrl(path) { + const baseUrl = + `${constants.BASE_URL}${constants.VERSION_PATH}` + .replace(constants.WORKSPACE_PLACEHOLDER, this.$auth.workspace_id); + return `${baseUrl}${path}`; + }, + getHeaders(headers) { + return { + Authorization: `Bearer ${this.$auth.access_token}`, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + put(args = {}) { + return this._makeRequest({ + method: "PUT", + ...args, + }); + }, + listAccounts(args = {}) { + return this._makeRequest({ + path: "/accounts", + ...args, + }); + }, + listPosts(args = {}) { + return this._makeRequest({ + path: "/posts", + ...args, + }); + }, + listMediaFiles(args = {}) { + return this._makeRequest({ + path: "/media", + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, resourceName = "data", + max = constants.DEFAULT_MAX, + }) { + let page = 1; + let resourcesCount = 0; + + while (true) { + const response = + await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + page, + per_page: constants.DEFAULT_LIMIT, + }, + }); + + const nextResources = resourceName && response[resourceName] || response; + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + yield resource; + resourcesCount += 1; + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + if (nextResources.length < constants.DEFAULT_LIMIT) { + console.log("No next page found"); + return; + } + + page += 1; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); + }, + }, +}; diff --git a/components/dotsimple/package.json b/components/dotsimple/package.json new file mode 100644 index 0000000000000..8ca06aafc62ea --- /dev/null +++ b/components/dotsimple/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/dotsimple", + "version": "0.1.0", + "description": "Pipedream DotSimple Components", + "main": "dotsimple.app.mjs", + "keywords": [ + "pipedream", + "dotsimple" + ], + "homepage": "https://pipedream.com/apps/dotsimple", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/dotsimple/sources/common/polling.mjs b/components/dotsimple/sources/common/polling.mjs new file mode 100644 index 0000000000000..619c42ea702bb --- /dev/null +++ b/components/dotsimple/sources/common/polling.mjs @@ -0,0 +1,64 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../dotsimple.app.mjs"; + +export default { + props: { + app, + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + isResourceRelevant() { + return true; + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + return { + debug: true, + }; + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + async processResources(resources) { + const { + isResourceRelevant, + processResource, + } = this; + + return Array.from(resources) + .filter(isResourceRelevant) + .forEach(processResource); + }, + }, + async run() { + const { + app, + getResourcesFn, + getResourcesFnArgs, + processResources, + } = this; + + const resources = await app.paginate({ + resourcesFn: getResourcesFn(), + resourcesFnArgs: getResourcesFnArgs(), + }); + + processResources(resources); + }, +}; diff --git a/components/dotsimple/sources/new-account-connected/new-account-connected.mjs b/components/dotsimple/sources/new-account-connected/new-account-connected.mjs new file mode 100644 index 0000000000000..d64fd47097375 --- /dev/null +++ b/components/dotsimple/sources/new-account-connected/new-account-connected.mjs @@ -0,0 +1,24 @@ +import common from "../common/polling.mjs"; + +export default { + ...common, + key: "dotsimple-new-account-connected", + name: "New Account Connected", + description: "Emit new event when a new account is connected. [See the documentation](https://help.dotsimple.io/en/articles/65-accounts).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourcesFn() { + return this.app.listAccounts; + }, + generateMeta(resource) { + return { + id: resource.uuid, + summary: `New Account: ${resource.uuid}`, + ts: Date.parse(resource.created_at), + }; + }, + }, +}; diff --git a/components/dotsimple/sources/new-file-uploaded/new-file-uploaded.mjs b/components/dotsimple/sources/new-file-uploaded/new-file-uploaded.mjs new file mode 100644 index 0000000000000..aaa8b484a1ab9 --- /dev/null +++ b/components/dotsimple/sources/new-file-uploaded/new-file-uploaded.mjs @@ -0,0 +1,24 @@ +import common from "../common/polling.mjs"; + +export default { + ...common, + key: "dotsimple-new-file-uploaded", + name: "New File Uploaded", + description: "Emit new event when a new file is uploaded. [See the documentation](https://help.dotsimple.io/en/articles/67-media-files).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourcesFn() { + return this.app.listMediaFiles; + }, + generateMeta(resource) { + return { + id: resource.uuid, + summary: `New File: ${resource.uuid}`, + ts: Date.parse(resource.created_at), + }; + }, + }, +}; diff --git a/components/dotsimple/sources/new-post-created/new-post-created.mjs b/components/dotsimple/sources/new-post-created/new-post-created.mjs new file mode 100644 index 0000000000000..3d27fc701cc75 --- /dev/null +++ b/components/dotsimple/sources/new-post-created/new-post-created.mjs @@ -0,0 +1,24 @@ +import common from "../common/polling.mjs"; + +export default { + ...common, + key: "dotsimple-new-post-created", + name: "New Post Created", + description: "Emit new event when a new post is created on the platform. [See the documentation](https://help.dotsimple.io/en/articles/68-posts).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourcesFn() { + return this.app.listPosts; + }, + generateMeta(resource) { + return { + id: resource.uuid, + summary: `New Post: ${resource.uuid}`, + ts: Date.parse(resource.created_at), + }; + }, + }, +}; diff --git a/components/dovetail/dovetail.app.mjs b/components/dovetail/dovetail.app.mjs new file mode 100644 index 0000000000000..89b7b8e96d448 --- /dev/null +++ b/components/dovetail/dovetail.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "dovetail", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/dovetail/package.json b/components/dovetail/package.json new file mode 100644 index 0000000000000..9614b0d9de9ff --- /dev/null +++ b/components/dovetail/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/dovetail", + "version": "0.0.1", + "description": "Pipedream Dovetail Components", + "main": "dovetail.app.mjs", + "keywords": [ + "pipedream", + "dovetail" + ], + "homepage": "https://pipedream.com/apps/dovetail", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/dpd2/README.md b/components/dpd2/README.md new file mode 100644 index 0000000000000..06b06e66f878a --- /dev/null +++ b/components/dpd2/README.md @@ -0,0 +1,11 @@ +# Overview + +The DPD API provides a gateway to manage and automate digital product sales. By integrating with Pipedream, you can craft serverless workflows that interact with your DPD account, harnessing the ability to automate tasks like creating products, updating customer details, or reacting to new sales in real-time. With Pipedream's ability to connect to a multitude of services, the DPD API can be part of a larger ecosystem, streamlining your e-commerce operations or enriching your customer data across platforms. + +# Example Use Cases + +- **Automate Digital Product Delivery**: When a new sale occurs, trigger a Pipedream workflow that sends a personalized email with the digital product or a download link to the customer. This automation enhances the buying experience, providing immediate delivery and reducing manual effort. + +- **Sync New Customers to a CRM**: Capture new customer data in real-time as they purchase through DPD. Utilize a Pipedream workflow to add or update these customers in your CRM system, like Salesforce or HubSpot, keeping a synchronized record for customer relationship management and marketing strategies. + +- **Generate Sales Reports**: Schedule a daily or weekly Pipedream workflow to aggregate sales data from DPD. Transform and send this data to Google Sheets or a BI tool such as Tableau for visualization. This workflow aids in tracking business performance and making informed decisions quickly. diff --git a/components/dpd2/common/constants.mjs b/components/dpd2/common/constants.mjs new file mode 100644 index 0000000000000..cddbd3f89a9dc --- /dev/null +++ b/components/dpd2/common/constants.mjs @@ -0,0 +1,30 @@ +const PURCHASE_STATUSES = [ + { + value: "ACT", + label: "Active", + }, + { + value: "PND", + label: "Pending", + }, + { + value: "RFD", + label: "Refunded", + }, + { + value: "ERR", + label: "Error", + }, + { + value: "CAN", + label: "Canceled", + }, + { + value: "HLD", + label: "Held", + }, +]; + +export default { + PURCHASE_STATUSES, +}; diff --git a/components/dpd2/dpd2.app.mjs b/components/dpd2/dpd2.app.mjs index 0542b4234d98c..93266f5284386 100644 --- a/components/dpd2/dpd2.app.mjs +++ b/components/dpd2/dpd2.app.mjs @@ -1,11 +1,152 @@ +import { + axios, ConfigurationError, +} from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "dpd2", - propDefinitions: {}, + propDefinitions: { + storefrontId: { + type: "string", + label: "Storefront ID", + description: "The ID of a storefront", + optional: true, + async options() { + const storefronts = await this.listStorefronts(); + return storefronts?.map(({ + id: value, name: label, + }) => ({ + label, + value, + })) || []; + }, + }, + productId: { + type: "string", + label: "Product ID", + description: "The ID of the product being purchased", + optional: true, + async options({ storefrontId }) { + const products = await this.listProducts({ + params: { + storefront_id: storefrontId, + }, + }); + return products?.map(({ + id: value, name: label, + }) => ({ + label, + value, + })) || []; + }, + }, + customerId: { + type: "string", + label: "Customer ID", + description: "The ID of a customer", + optional: true, + async options() { + const customers = await this.listCustomers(); + return customers?.map(({ + id: value, firstname, lastname, + }) => ({ + label: (`${firstname} ${lastname}`).trim(), + value, + })) || []; + }, + }, + subscriberId: { + type: "string", + label: "Subscriber ID", + description: "The ID of a subscriber", + optional: true, + async options({ storefrontId }) { + if (!storefrontId) { + return []; + } + const subscribers = await this.listSubscribers({ + storefrontId, + }); + return subscribers?.map(({ + id: value, username: label, + }) => ({ + label, + value, + })) || []; + }, + }, + status: { + type: "string", + label: "Status", + description: "Filter purchases by status", + optional: true, + options: constants.PURCHASE_STATUSES, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.getdpd.com/v2"; + }, + _auth() { + return { + username: this.$auth.api_username, + password: this.$auth.api_password, + }; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + method = "GET", + ...otherOpts + } = opts; + try { + return await axios($, { + ...otherOpts, + method, + url: `${this._baseUrl()}${path}`, + auth: this._auth(), + }); + } catch (e) { + if (method === "GET") { + console.log(`No ${path.split("/").pop()} found`); + } else { + throw new ConfigurationError(JSON.stringify(e)); + } + } + }, + listStorefronts(opts = {}) { + return this._makeRequest({ + path: "/storefronts", + ...opts, + }); + }, + listProducts(opts = {}) { + return this._makeRequest({ + path: "/products", + ...opts, + }); + }, + listPurchases(opts = {}) { + return this._makeRequest({ + path: "/purchases", + ...opts, + }); + }, + listCustomers(opts = {}) { + return this._makeRequest({ + path: "/customers", + ...opts, + }); + }, + listSubscribers({ + storefrontId, ...opts + }) { + return this._makeRequest({ + path: `/storefronts/${storefrontId}/subscribers`, + ...opts, + }); }, }, }; diff --git a/components/dpd2/package.json b/components/dpd2/package.json index fed9f9e16b371..a6a259500382f 100644 --- a/components/dpd2/package.json +++ b/components/dpd2/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/dpd2", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream DPD Components", "main": "dpd2.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" } -} \ No newline at end of file +} diff --git a/components/dpd2/sources/new-purchase-created/new-purchase-created.mjs b/components/dpd2/sources/new-purchase-created/new-purchase-created.mjs new file mode 100644 index 0000000000000..f53c72c380622 --- /dev/null +++ b/components/dpd2/sources/new-purchase-created/new-purchase-created.mjs @@ -0,0 +1,107 @@ +import dpd2 from "../../dpd2.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "dpd2-new-purchase-created", + name: "New Purchase Created", + description: "Emit new event when a purchase is made. [See the documentation](https://getdpd.com/docs/api/purchases.html#list-purchases)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + dpd2, + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + db: "$.service.db", + storefrontId: { + propDefinition: [ + dpd2, + "storefrontId", + ], + }, + productId: { + propDefinition: [ + dpd2, + "productId", + (c) => ({ + storefrontId: c.storefrontId, + }), + ], + }, + customerId: { + propDefinition: [ + dpd2, + "customerId", + ], + }, + subscriberId: { + propDefinition: [ + dpd2, + "subscriberId", + (c) => ({ + storefrontId: c.storefrontId, + }), + ], + }, + status: { + propDefinition: [ + dpd2, + "status", + ], + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastCreated() { + return this.db.get("lastCreated"); + }, + _setLastCreated(lastCreated) { + this.db.set("lastCreated", lastCreated); + }, + emitEvent(purchase) { + const meta = this.generateMeta(purchase); + this.$emit(purchase, meta); + }, + generateMeta(purchase) { + return { + id: purchase.id, + summary: `New Purchase: ${purchase.id}`, + ts: purchase.created_at, + }; + }, + async processEvent(max) { + const lastCreated = this._getLastCreated(); + let purchases = await this.dpd2.listPurchases({ + params: { + date_min: lastCreated, + status: this.status, + product_id: this.productId, + storefrontId: this.storefrontId, + customer_id: this.customerId, + subscriber_id: this.subscriberId, + }, + }); + if (!purchases?.length) { + return; + } + this._setLastCreated(purchases[purchases.length - 1].created_at); + if (max) { + purchases = purchases.slice(max * -1); + } + purchases.forEach((purchase) => this.emitEvent(purchase)); + }, + }, + async run() { + await this.processEvent(); + }, + sampleEmit, +}; diff --git a/components/dpd2/sources/new-purchase-created/test-event.mjs b/components/dpd2/sources/new-purchase-created/test-event.mjs new file mode 100644 index 0000000000000..ffc9e41236bf0 --- /dev/null +++ b/components/dpd2/sources/new-purchase-created/test-event.mjs @@ -0,0 +1,77 @@ +export default { + "id": 18937181, + "created_at": "1717016636", + "updated_at": "1717016638", + "storefront_id": 47210, + "salt": "a33658e5a5ca6ca9af2c731cb556ea0e81ef5dd2", + "status": "ACT", + "currency": "USD", + "subtotal": "1.00", + "discount": "0.00", + "tax": "0.00", + "shipping": "0.00", + "total": "1.00", + "processor_fee": "0.00", + "buyer_email": "test@sample.com", + "buyer_firstname": "George", + "buyer_lastname": "Washington", + "customer": { + "id": 9437584, + "first_name": "George", + "last_name": "Washington", + "email": "test@sample.com", + "receives_email": true, + "created_at": "1717016636", + "updated_at": "1717016636" + }, + "customer_notes": null, + "custom_fields": [], + "shipping_firstname": null, + "shipping_lastname": null, + "shipping_street": null, + "shipping_street2": null, + "shipping_city": null, + "shipping_state": null, + "shipping_zip_code": null, + "shipping_country_code": null, + "shipping_country": "", + "shipping_phone": null, + "shipping_vatin": null, + "shipping_business_name": null, + "shipping_carrier": null, + "shipping_tracking_number": null, + "requires_shipping": true, + "tangibles_to_ship": 1, + "billing_firstname": "George", + "billing_lastname": "Washington", + "billing_street": "1600 Pennsylvania Ave", + "billing_street2": null, + "billing_city": "Washington", + "billing_state": "DC", + "billing_zip_code": "20500", + "billing_country_code": "US", + "billing_card_type": null, + "billing_card_number": null, + "line_items": [ + { + "id": 28023183, + "purchase_id": 18937181, + "product_id": 239238, + "price": "1.00", + "quantity": 1, + "subtotal": "1.00", + "tax": "0.00", + "discount": "0.00", + "total": "1.00", + "product_name": "product1", + "download_limit": 0, + "download_count": 0, + "expires_at": "1717103036", + "product_keys": [], + "has_all_keys": false + } + ], + "coupons": [], + "ip_address": "47.225.32.185", + "marketing_optin": false +} \ No newline at end of file diff --git a/components/draftable/README.md b/components/draftable/README.md index 8bed8f9dfeb33..126e25686e08f 100644 --- a/components/draftable/README.md +++ b/components/draftable/README.md @@ -1,12 +1,11 @@ # Overview -The Draftable API enables developers to convert documents and images into -highly-compressed, draft versions that can be easily shared and reviewed. This -can be useful for a number of different applications, such as: - -- Collaborative document editing, where users can quickly add comments and - suggestions without slowing down the download or editing process -- Document sharing, where users can quickly share large documents without - overloading email servers or bandwidth limits -- Image compression, where users can quickly reduce the size of images without - losing quality or resolution +The Draftable API provides a powerful platform for comparing documents programmatically. Leverage this API via Pipedream to automate the comparison of text files, enabling quick and accurate detection of differences between versions. Whether you're tracking changes in legal contracts, software documentation, or manuscript revisions, integrating Draftable with Pipedream can streamline your comparison workflows, triggering actions in other apps based on the results. + +# Example Use Cases + +- **Contract Change Detection & Notification**: Automate a workflow that triggers whenever a new version of a contract is uploaded to a cloud storage service like Dropbox. Use Draftable to compare the new contract with the previous version and, if changes are detected, send an alert through Slack or email to the relevant parties. + +- **Version Control for Documentation**: Set up a Pipedream workflow that subscribes to GitHub commits on documentation repos. Utilize Draftable to compare the updated documentation with the existing one. If significant changes are found, automatically update the documentation on your website and notify your support team via a tool like Zendesk. + +- **Manuscript Revision Tracking for Publishers**: Create a workflow where manuscript revisions uploaded to Google Drive trigger a comparison with the previous draft using Draftable. If changes surpass a certain threshold, send the comparison result to the editorial team and update the project management tool, such as Trello or Asana, to signal that the manuscript is ready for the next stage of review. diff --git a/components/draftable/package.json b/components/draftable/package.json new file mode 100644 index 0000000000000..e5cbd47a68072 --- /dev/null +++ b/components/draftable/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/draftable", + "version": "0.6.0", + "description": "Pipedream draftable Components", + "main": "draftable.app.mjs", + "keywords": [ + "pipedream", + "draftable" + ], + "homepage": "https://pipedream.com/apps/draftable", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/drata/README.md b/components/drata/README.md index b5fb63e17d898..e99b034f964f6 100644 --- a/components/drata/README.md +++ b/components/drata/README.md @@ -3,3 +3,11 @@ [Drata](https://drata.com/) is a compliance automation platform. Drata’s platform is built by compliance and security experts so you don’t have to be one. With 75+ native integrations, you can easily connect your tech stack and automate evidence collection and testing. [Pipedream customers get 25% off Drata](https://drata.com/partner/pipedream), with implementation fees waived. Visit [https://drata.com/partner/pipedream](https://drata.com/partner/pipedream) for more information. + +# Example Use Cases + +- **Automated Evidence Collection for Audits**: Use the Drata API to periodically gather and upload evidence of compliance controls to Drata. Connect with GitHub to automatically pull in the latest commit logs or issue tracking data, ensuring continuous audit readiness. + +- **Real-time Compliance Alerts**: Set up a Pipedream workflow that listens for changes in your infrastructure or SaaS apps, using APIs like AWS CloudTrail or Okta. When a change is detected that might affect your compliance status, an alert is sent to a designated Slack channel via the Drata API. + +- **Compliance Status Dashboard**: Combine Drata with a dashboard tool like Google Sheets or a BI app to visualize your compliance status. Regularly fetch key compliance metrics from Drata through Pipedream and display them on your dashboard to keep your team informed. diff --git a/components/drchrono/drchrono.app.mjs b/components/drchrono/drchrono.app.mjs new file mode 100644 index 0000000000000..42cbae35f9903 --- /dev/null +++ b/components/drchrono/drchrono.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "drchrono", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/drchrono/package.json b/components/drchrono/package.json new file mode 100644 index 0000000000000..fd3a2d67fdb4c --- /dev/null +++ b/components/drchrono/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/drchrono", + "version": "0.0.1", + "description": "Pipedream DrChrono Components", + "main": "drchrono.app.mjs", + "keywords": [ + "pipedream", + "drchrono" + ], + "homepage": "https://pipedream.com/apps/drchrono", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/dreamhost/README.md b/components/dreamhost/README.md new file mode 100644 index 0000000000000..699da2a379abd --- /dev/null +++ b/components/dreamhost/README.md @@ -0,0 +1,11 @@ +# Overview + +The Dreamhost API enables you to automate tasks related to your hosting account. This includes managing domains, DNS records, and email accounts, as well as accessing usage stats and billing information. With Pipedream, you can create serverless workflows that leverage the Dreamhost API to integrate with other services, respond to events, and automate your web hosting operations, all without managing infrastructure. + +# Example Use Cases + +- **Automate Domain Renewal Notifications**: Trigger an email or Slack message using Pipedream's built-in actions when a domain is nearing its renewal date as detected through the Dreamhost API. + +- **Sync DNS Record Changes**: Whenever a DNS record is updated on Dreamhost, use Pipedream to sync the change across other platforms like Cloudflare or Route 53 to maintain consistent DNS configurations. + +- **Monitor Hosting Usage and Billing**: Set up a workflow on Pipedream that triggers monthly, fetching hosting usage and billing data from Dreamhost, and then sends a formatted report to your finance team's email or a Google Sheets document. diff --git a/components/dreamstudio/README.md b/components/dreamstudio/README.md new file mode 100644 index 0000000000000..312529bdded19 --- /dev/null +++ b/components/dreamstudio/README.md @@ -0,0 +1,11 @@ +# Overview + +The DreamStudio (Stable Diffusion) API taps into the power of AI to generate creative visual content from textual descriptions. With Pipedream, you can automate the process of creating images, integrate with various apps, and build complex workflows without servers. You can trigger image generation on demand, process user inputs, or schedule image creation - All seamlessly within Pipedream's ecosystem. + +# Example Use Cases + +- **Automated Social Media Posts**: Generate images from trending topics or hashtags and post them directly to social media platforms like Twitter or Instagram. Leverage Pipedream's event sources to trigger image creation, then use the Twitter app to share your AI-generated artwork. + +- **Content Generation for Blogs**: Enhance your blog posts by adding relevant, AI-generated images. Set up a scheduled workflow in Pipedream to pull text from your content management system (like WordPress), use the DreamStudio API to create images, and then automatically update your blog posts with these visuals. + +- **Personalized E-commerce Product Images**: Create custom product visuals for e-commerce platforms by taking product descriptions and generating unique images. Build a workflow that listens for new product listings via webhook, invokes the DreamStudio API for image generation, and uploads the results to the product's gallery on Shopify or WooCommerce. diff --git a/components/dribbble/README.md b/components/dribbble/README.md index 45f0cb2ecac92..05cabb6508852 100644 --- a/components/dribbble/README.md +++ b/components/dribbble/README.md @@ -1,14 +1,11 @@ # Overview -With the Dribbble API, you can build applications that help designers and -illustrators find inspiration, connect with other creatives, and share their -work with the world. - -Here are some examples of what you can build with the Dribbble API: - -- A platform for designers to share their work and get feedback from their - peers -- A social network for creatives to connect with each other and find new - collaborators -- A search engine for finding the best design talent from around the world -- A portfolio site for showcasing the work of individual designers +The Dribbble API unlocks the design world of Dribbble, letting you tap into a community of graphic designers, illustrators, and artists. By integrating with the API via Pipedream, you can automate tasks like tracking user stats, exploring popular shots, and posting updates automatically. With Pipedream's no-code platform, you can connect Dribbble to hundreds of other apps to streamline your design workflow, get insights, and engage with the design community. + +# Example Use Cases + +- **Auto-Post Design Updates**: Automatically post new shots or updates to your Dribbble account whenever new designs are added to your cloud storage platform, like Google Drive or Dropbox, keeping your portfolio fresh and up to date. + +- **Monitor Brand Mentions**: Keep tabs on your brand by setting up a workflow that listens for mentions or tags of your company in Dribbble comments or shots. Trigger actions like sending a Slack message to your marketing team to engage with the community or compile brand mentions for analysis. + +- **Weekly Design Digest**: Create a weekly digest of popular Dribbble shots based on likes, comments, or categories. Aggregate this content and send it as an email through SendGrid or post it to a company communication platform like Confluence to inspire your design team or keep stakeholders informed on design trends. diff --git a/components/drift/README.md b/components/drift/README.md index 7f73d1d8294d7..db7b3a91586a6 100644 --- a/components/drift/README.md +++ b/components/drift/README.md @@ -1,13 +1,11 @@ # Overview -The Drift API allows you to interact with all of the resources in Drift. This -includes retrieving and updating conversations, retrieving and updating -contacts, and more. +The Drift API, contrary to the customer messaging platform, is actually a geocoding service provided by Geocod.io. It allows you to convert physical addresses to geographic coordinates and vice versa, enrich addresses with detailed location data, and offers batch geocoding features for processing multiple addresses simultaneously. With Pipedream, you can harness this API for a variety of location-based automations, enriching customer data with geographical insights, automating logistics, or enhancing marketing campaigns with location targeting. -With the Drift API, you can build applications to: +# Example Use Cases -- Retrieve and update conversations -- Retrieve and update contacts -- Upload and retrieve files -- Send and receive messages -- And more! +- **Customer Address Verification**: Automate the process of verifying and standardizing customer addresses in your CRM when new contacts are added. Use Drift's geocoding capabilities to convert addresses into standardized formats and coordinates, ensuring data consistency and reliability for shipping or service provision. + +- **Event Location Broadcast**: When a new event is created in your company's event management system, use Drift to geocode the event address, then automatically post the event details along with a map link to company social media channels or send the information via email to your subscribers. + +- **Lead Geographical Segmentation**: Integrate Drift with your marketing automation platform to segment leads based on their location. As leads are captured, geocode their addresses, and use this data to trigger specific marketing campaigns tailored to their geographic region, optimizing ad spend and engagement. diff --git a/components/drift/package.json b/components/drift/package.json new file mode 100644 index 0000000000000..d3b526a5e573f --- /dev/null +++ b/components/drift/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/drift", + "version": "0.6.0", + "description": "Pipedream drift Components", + "main": "drift.app.mjs", + "keywords": [ + "pipedream", + "drift" + ], + "homepage": "https://pipedream.com/apps/drift", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/drimify/README.md b/components/drimify/README.md new file mode 100644 index 0000000000000..da38b98dd5650 --- /dev/null +++ b/components/drimify/README.md @@ -0,0 +1,11 @@ +# Overview + +Drimify offers a suite of tools to create engaging and interactive content like quizzes, games, and e-learning modules. With the Drimify API, you can automate the creation and management of these interactive elements, extract analytics, and personalize user experiences. On Pipedream, you can leverage these API capabilities to build serverless workflows that connect Drimify with other apps, streamlining processes like lead generation, user engagement tracking, and content updates. + +# Example Use Cases + +- **Lead Collection to CRM**: Capture leads with Drimify's interactive content and automatically add new contacts to a CRM like Salesforce. When a new lead completes a game or quiz, the workflow triggers, extracting the user's data and pushing it directly into the CRM. + +- **Content Update Notifications**: Send real-time updates via Slack or email whenever new content is published on Drimify. This workflow can monitor Drimify for new quizzes or games and immediately notify your team, ensuring everyone is up-to-date with the latest content. + +- **User Engagement Analytics to Google Sheets**: Collect user engagement data from Drimify and log it into a Google Sheet for analysis. Each time a user completes an interactive module, the workflow triggers, appending the relevant data to a Sheet, which can then be used for reporting and insights. diff --git a/components/drimify/package.json b/components/drimify/package.json index f4d80d396d7f3..100f2de4642a8 100644 --- a/components/drimify/package.json +++ b/components/drimify/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/drip/README.md b/components/drip/README.md index 058450571f8a4..bcac1a46c8bf7 100644 --- a/components/drip/README.md +++ b/components/drip/README.md @@ -1,10 +1,11 @@ # Overview -With the Drip API, you can: - -- Build drip campaigns that send emails, push notifications, or SMS messages to - your users -- Trigger actions in your app based on events that happen in Drip -- Build custom tools and dashboards on top of your Drip data -- Automate your workflows -- And much more! +The Drip API allows for powerful email marketing automation, providing tools to craft every interaction with your leads, trial users, and customers. With the Drip API on Pipedream, you can manage subscribers, send emails, track user actions, and more. This integration opens up possibilities for syncing subscriber data, triggering communication based on user behavior, and connecting your email marketing to a wider ecosystem of tools. + +# Example Use Cases + +- **Subscriber Segmentation and Tagging Automation**: Automatically tag subscribers in Drip based on their behavior or demographic data from your CRM platform. Use a workflow that listens to CRM updates, enriches the data if needed, and uses Drip API to segment audiences for personalized marketing campaigns. + +- **Customer Journey Email Triggers**: Set up an automation that triggers a series of targeted emails when a user performs a specific action on your website or app. For instance, when a user signs up, make an API call to Drip to enroll them in an onboarding email sequence. + +- **E-commerce Cart Abandonment Workflow**: Recover potentially lost sales by automating an email sequence for users who have abandoned their shopping carts. Connect your e-commerce platform to Drip through Pipedream. When a cart is abandoned, the workflow can trigger a reminder email or a special offer to encourage completion of the purchase. diff --git a/components/dripcel/dripcel.app.mjs b/components/dripcel/dripcel.app.mjs new file mode 100644 index 0000000000000..81a508ba7c5f4 --- /dev/null +++ b/components/dripcel/dripcel.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "dripcel", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/dripcel/package.json b/components/dripcel/package.json new file mode 100644 index 0000000000000..7f8dbe5d22ad8 --- /dev/null +++ b/components/dripcel/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/dripcel", + "version": "0.0.1", + "description": "Pipedream Dripcel Components", + "main": "dripcel.app.mjs", + "keywords": [ + "pipedream", + "dripcel" + ], + "homepage": "https://pipedream.com/apps/dripcel", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/dromo/README.md b/components/dromo/README.md new file mode 100644 index 0000000000000..6d4883f65eccc --- /dev/null +++ b/components/dromo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Dromo API is a logistics and shipment tracking service that provides real-time data on shipments across carriers. With this API, you can tap into a vast network of shipping information, automate tracking processes, send notifications on shipment updates, and integrate your logistics data with other business systems. On Pipedream, you can create workflows that leverage the Dromo API to connect with various apps, automate shipment tracking updates, and streamline logistics processes. + +# Example Use Cases + +- **Automated Shipment Tracking Updates**: Use the Dromo API on Pipedream to build a workflow that automatically polls for shipment status updates and posts these updates to a Slack channel, keeping your team informed in real-time. + +- **Order Fulfillment Process Integration**: Seamlessly integrate the Dromo API with an e-commerce platform like Shopify. Whenever an order is placed, trigger a workflow that creates a shipment in Dromo, then monitors for status changes and updates the order status back in Shopify. + +- **Centralized Logistics Dashboard**: Combine the Dromo API with a dashboard tool like Google Sheets or Data Studio on Pipedream. Create a workflow that fetches regular shipment status updates and populates a tracking dashboard, providing an at-a-glance view of all logistics activities. diff --git a/components/dropboard/README.md b/components/dropboard/README.md new file mode 100644 index 0000000000000..66089d40a309d --- /dev/null +++ b/components/dropboard/README.md @@ -0,0 +1,11 @@ +# Overview + +The Dropboard API enables management and optimization of shipping logistics, providing functionality to monitor shipments, manage inventory, and handle orders efficiently. With this API, Pipedream users can automate various aspects of logistics and supply chain operations, enhancing real-time decision-making and operational efficiency. + +# Example Use Cases + +- **Automated Order Processing and Shipment Tracking**: Connect Dropboard to Shopify using Pipedream to automate order processing. When a new order is placed in Shopify, trigger a workflow in Pipedream to create a shipment in Dropboard, then continually track the shipment status and update the customer via email or SMS. + +- **Inventory Management and Alerts**: Link Dropboard to Slack via Pipedream to manage inventory levels. Set up a workflow where Dropboard sends daily inventory data to Pipedream, which processes this data and sends a notification to a Slack channel if stock levels for critical items are low. + +- **Consolidated Reporting for Management**: Integrate Dropboard with Google Sheets using Pipedream to automate the generation of daily logistics reports. Extract data from Dropboard on shipments, inventory, and orders, process it in Pipedream, and automatically update a Google Sheet that is used for daily operational review and long-term analysis. diff --git a/components/dropboard/actions/create-client/create-client.mjs b/components/dropboard/actions/create-client/create-client.mjs new file mode 100644 index 0000000000000..f904b677ab125 --- /dev/null +++ b/components/dropboard/actions/create-client/create-client.mjs @@ -0,0 +1,37 @@ +import dropboard from "../../dropboard.app.mjs"; + +export default { + key: "dropboard-create-client", + name: "Create Client", + description: "Creates a new client within Dropboard. Note this is available only for recruiter plan users and may incur additional charges based on your organization's plan. [See the documentation](https://dropboard.readme.io/reference/clients-post)", + version: "0.0.2", + type: "action", + props: { + dropboard, + clientName: { + propDefinition: [ + dropboard, + "clientName", + ], + }, + clientPlanId: { + propDefinition: [ + dropboard, + "clientPlanId", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.dropboard.createClient({ + $, + data: { + name: this.clientName, + clientPlanId: this.clientPlanId, + }, + }); + + $.export("$summary", `Successfully created client with ID ${response.id}`); + return response; + }, +}; diff --git a/components/dropboard/actions/create-job/create-job.mjs b/components/dropboard/actions/create-job/create-job.mjs new file mode 100644 index 0000000000000..5ca8fd4158c78 --- /dev/null +++ b/components/dropboard/actions/create-job/create-job.mjs @@ -0,0 +1,101 @@ +import { parseObject } from "../../common/utils.mjs"; +import dropboard from "../../dropboard.app.mjs"; + +export default { + key: "dropboard-create-job", + name: "Create Job", + description: "Creates a new job within Dropboard. [See the documentation](https://dropboard.readme.io/reference/jobs-post)", + version: "0.0.2", + type: "action", + props: { + dropboard, + title: { + type: "string", + label: "Title", + description: "The title of the job", + }, + description: { + type: "string", + label: "Description", + description: "The description of the job", + }, + hiringManagerEmails: { + type: "string[]", + label: "Hiring Manager Emails", + description: "The emails of the hiring managers", + }, + status: { + type: "string", + label: "Status", + description: "The status of the job", + options: [ + "open", + "closed", + ], + optional: true, + }, + locations: { + type: "string[]", + label: "Locations", + description: "Locations of the job", + optional: true, + }, + qualifications: { + type: "string", + label: "Qualifications", + description: "The qualifications required for the job", + optional: true, + }, + responsibilities: { + type: "string", + label: "Responsibilities", + description: "The responsibilities of the job", + optional: true, + }, + compensation: { + type: "string", + label: "Compensation", + description: "The compensation for the job", + optional: true, + }, + openDateStart: { + type: "string", + label: "Open Date Start", + description: "If only open during a timeframe, this is the start day", + optional: true, + }, + openDateEnd: { + type: "string", + label: "Open Date End", + description: "If only open during a timeframe, this is the end day", + optional: true, + }, + jobCode: { + type: "string", + label: "Job Code", + description: "Specify a unique job code. Will be generated if not specified", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.dropboard.createJob({ + $, + data: { + title: this.title, + description: this.description, + hiringManagerEmails: parseObject(this.hiringManagerEmails), + clientId: this.clientId, + status: this.status, + locations: parseObject(this.locations), + qualifications: this.qualifications, + responsibilities: this.responsibilities, + compensation: this.compensation, + openDateStart: this.openDateStart, + openDateEnd: this.openDateEnd, + jobCode: this.jobCode, + }, + }); + $.export("$summary", `Successfully created job with Id: ${response.id}`); + return response; + }, +}; diff --git a/components/dropboard/actions/find-or-create-client/find-or-create-client.mjs b/components/dropboard/actions/find-or-create-client/find-or-create-client.mjs new file mode 100644 index 0000000000000..fe1a6cc29cde7 --- /dev/null +++ b/components/dropboard/actions/find-or-create-client/find-or-create-client.mjs @@ -0,0 +1,39 @@ +import dropboard from "../../dropboard.app.mjs"; + +export default { + key: "dropboard-find-or-create-client", + name: "Find or Create Client", + description: "Looks for a client within Dropboard. If not found, it will create a new one. [See the documentation](https://dropboard.readme.io/reference/clients-post)", + version: "0.0.2", + type: "action", + props: { + dropboard, + clientName: { + propDefinition: [ + dropboard, + "clientName", + ], + }, + clientPlanId: { + propDefinition: [ + dropboard, + "clientPlanId", + ], + optional: true, + }, + }, + async run({ $ }) { + const { + clientName, clientPlanId, + } = this; + + const response = await this.dropboard.findOrCreateClient({ + $, + clientName, + clientPlanId, + }); + + $.export("$summary", `Successfully found or created client: ${response.name}`); + return response; + }, +}; diff --git a/components/dropboard/common/utils.mjs b/components/dropboard/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/dropboard/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/dropboard/dropboard.app.mjs b/components/dropboard/dropboard.app.mjs new file mode 100644 index 0000000000000..19d391e9a074f --- /dev/null +++ b/components/dropboard/dropboard.app.mjs @@ -0,0 +1,168 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "dropboard", + propDefinitions: { + jobId: { + type: "string", + label: "Job ID", + description: "The ID of the job", + async options({ page }) { + const { list } = await this.listJobs({ + params: { + page: page + 1, + }, + }); + + return list.map(({ + id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + clientId: { + type: "string", + label: "Client ID", + description: "The id of the client this job should belong to, or not set to belong to the root organization", + async options({ page }) { + const { list } = await this.listClients({ + params: { + page: page + 1, + }, + }); + + return list.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + hiringManagerEmail: { + type: "string", + label: "Hiring Manager Email", + description: "The email of the hiring manager", + }, + jobDetails: { + type: "object", + label: "Job Details", + description: "Details of the job", + }, + clientName: { + type: "string", + label: "Client Name", + description: "The name of the client", + }, + clientPlanId: { + type: "string", + label: "Client Plan ID", + description: "The plan ID for the client", + async options({ page }) { + const { list } = await this.listPlans({ + params: { + page: page + 1, + }, + }); + + return list.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.dropboardhq.com/2024-02"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createWebhook({ + path, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/${path}/webhooks`, + ...opts, + }); + }, + deleteWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + }); + }, + createJob(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/jobs", + ...opts, + }); + }, + createClient(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/clients", + ...opts, + }); + }, + listClients(opts = {}) { + return this._makeRequest({ + path: "/clients", + ...opts, + }); + }, + listJobs(opts = {}) { + return this._makeRequest({ + path: "/jobs", + ...opts, + }); + }, + listPlans(opts = {}) { + return this._makeRequest({ + path: "/clients/plans", + ...opts, + }); + }, + async findOrCreateClient({ + clientName, ...otherOpts + }) { + const { list: clients } = await this._makeRequest({ + method: "GET", + path: "/clients", + params: { + search: clientName, + }, + }); + + if (clients.length > 0) { + return clients[0]; + } else { + return this.createClient({ + data: { + name: clientName, + ...otherOpts, + }, + }); + } + }, + }, +}; diff --git a/components/dropboard/package.json b/components/dropboard/package.json new file mode 100644 index 0000000000000..d5dc54a07b317 --- /dev/null +++ b/components/dropboard/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/dropboard", + "version": "0.1.1", + "description": "Pipedream Dropboard Components", + "main": "dropboard.app.mjs", + "keywords": [ + "pipedream", + "dropboard" + ], + "homepage": "https://pipedream.com/apps/dropboard", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/dropboard/sources/common/base.mjs b/components/dropboard/sources/common/base.mjs new file mode 100644 index 0000000000000..2b4c1409e291e --- /dev/null +++ b/components/dropboard/sources/common/base.mjs @@ -0,0 +1,53 @@ +import dropboard from "../../dropboard.app.mjs"; + +export default { + props: { + dropboard, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + clientId: { + propDefinition: [ + dropboard, + "clientId", + ], + optional: true, + }, + }, + methods: { + getData() { + return {}; + }, + }, + hooks: { + async activate() { + const data = await this.dropboard.createWebhook({ + data: { + url: this.http.endpoint, + clientId: this.clientId, + ...this.getData(), + }, + path: this.getPath(), + }); + this.db.set("webhookId", data.id); + }, + async deactivate() { + const webhookId = this.db.get("webhookId"); + await this.dropboard.deleteWebhook(webhookId); + }, + }, + async run({ body }) { + this.http.respond({ + status: 200, + }); + + const ts = Date.parse(body.created); + this.$emit(body, { + id: `${body.id}`, + summary: this.getSummary(body), + ts: ts, + }); + }, +}; diff --git a/components/dropboard/sources/new-candidate-instant/new-candidate-instant.mjs b/components/dropboard/sources/new-candidate-instant/new-candidate-instant.mjs new file mode 100644 index 0000000000000..b6762bd3ebe5a --- /dev/null +++ b/components/dropboard/sources/new-candidate-instant/new-candidate-instant.mjs @@ -0,0 +1,45 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "dropboard-new-candidate-instant", + name: "New Candidate Profile Created (Instant)", + description: "Emit new event when a candidate profile is created.", + version: "0.0.2", + type: "source", + dedupe: "unique", + props: { + ...common.props, + jobId: { + propDefinition: [ + common.props.dropboard, + "jobId", + ], + optional: true, + }, + hiringManagerEmail: { + propDefinition: [ + common.props.dropboard, + "hiringManagerEmail", + ], + optional: true, + }, + }, + methods: { + ...common.methods, + getData() { + return { + jobId: this.jobId, + hiringManagerEmail: this.hiringManagerEmail, + }; + }, + getPath() { + return "candidates"; + }, + getSummary(body) { + return `New Candidate: ${body.first} ${body.last} ${body.email}`; + }, + }, + sampleEmit, +}; diff --git a/components/dropboard/sources/new-candidate-instant/test-event.mjs b/components/dropboard/sources/new-candidate-instant/test-event.mjs new file mode 100644 index 0000000000000..1b3558b19e03c --- /dev/null +++ b/components/dropboard/sources/new-candidate-instant/test-event.mjs @@ -0,0 +1,23 @@ +export default { + "coverLetter": "", + "created": "2024-07-02T14:18:34.117Z", + "email": "candidate@email.com", + "first": "First Name", + "last": "Last Name", + "id": "can_GYUugigiy", + "jobId": "job_GYUugigiy", + "job": { + "id": "job_GYUugigiy", + "jobCode": "ASQOHJSAJL", + "status": "open", + "title": "Job Title", + "hiringManagerEmails": [ + "manager@email.com" + ] + }, + "resume": { + "fileName": "file.pdf", + "fileSize": 13264, + "href": "https://runicorn-assets.s3.amazonaws.com/document-uploads/dropboardhq.com/2024-0702/14:18:22:996-8564/file.pdf" + } +} \ No newline at end of file diff --git a/components/dropboard/sources/new-client-member-instant/new-client-member-instant.mjs b/components/dropboard/sources/new-client-member-instant/new-client-member-instant.mjs new file mode 100644 index 0000000000000..80470c885dfda --- /dev/null +++ b/components/dropboard/sources/new-client-member-instant/new-client-member-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "dropboard-new-client-member-instant", + name: "New Client Member (Instant)", + description: "Emit new event when a member is added to a recruiting client (recruiter plan only).", + version: "0.0.2", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getPath() { + return "clients/members"; + }, + getSummary(body) { + return `New member added to client ${body.clientId}`; + }, + }, + sampleEmit, +}; diff --git a/components/dropboard/sources/new-client-member-instant/test-event.mjs b/components/dropboard/sources/new-client-member-instant/test-event.mjs new file mode 100644 index 0000000000000..f29a17071c231 --- /dev/null +++ b/components/dropboard/sources/new-client-member-instant/test-event.mjs @@ -0,0 +1,8 @@ +export default { + "id": "usr_GYUugigiy", + "clientId": "co_GYUugigiy", + "created": "2024-07-02T15:55:47.127Z", + "email": "member@email.com", + "first": "First Name", + "last": "Last Name" +} \ No newline at end of file diff --git a/components/dropboard/sources/new-job-instant/new-job-instant.mjs b/components/dropboard/sources/new-job-instant/new-job-instant.mjs new file mode 100644 index 0000000000000..c552c56633226 --- /dev/null +++ b/components/dropboard/sources/new-job-instant/new-job-instant.mjs @@ -0,0 +1,37 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "dropboard-new-job-instant", + name: "New Job Created (Instant)", + description: "Emit new event when a new job is created.", + version: "0.0.2", + type: "source", + dedupe: "unique", + props: { + ...common.props, + hiringManagerEmail: { + propDefinition: [ + common.props.dropboard, + "hiringManagerEmail", + ], + optional: true, + }, + }, + methods: { + ...common.methods, + getData() { + return { + hiringManagerEmail: this.hiringManagerEmail, + }; + }, + getPath() { + return "jobs"; + }, + getSummary(body) { + return `New Job: ${body.title}`; + }, + }, + sampleEmit, +}; diff --git a/components/dropboard/sources/new-job-instant/test-event.mjs b/components/dropboard/sources/new-job-instant/test-event.mjs new file mode 100644 index 0000000000000..ae904fdbef8d7 --- /dev/null +++ b/components/dropboard/sources/new-job-instant/test-event.mjs @@ -0,0 +1,17 @@ +export default { + "id": "job_GYUugigiy", + "jobCode": "ASQOHJSAJL", + "created": "2024-07-02T14:23:18.627Z", + "hiringManagerEmails": [ + "manager@email.com" + ], + "publicLink": "https://dropboardhq.com/job/GYUugigiy/ASQOHJSAJL/", + "status": "open", + "title": "Job Title", + "compensation": "", + "description": "Description", + "location": [], + "qualifications": "", + "responsibilities": "", + "customSections": [] +} \ No newline at end of file diff --git a/components/dropbox/README.md b/components/dropbox/README.md index 7d64dc383fe0f..f8184be1e30dc 100644 --- a/components/dropbox/README.md +++ b/components/dropbox/README.md @@ -1,15 +1,11 @@ # Overview -With the Dropbox API, you can build a variety of applications that range from -simple file sharing to complex content management systems. Some examples of -what you can build using the Dropbox API include: - -- A file sharing application that allows users to share and sync files across - devices -- A content management system that allows users to manage and share files and - folders -- A note taking application that allows users to sync notes across devices -- A task management application that allows users to manage and share tasks and - files -- A document management application that allows users to manage and share - documents and files +The Dropbox API on Pipedream enables you to automate file and folder operations, streamlining workflows that involve storing, syncing, and sharing content. With this API, you can programmatically manage files, set up event-driven triggers based on changes within Dropbox, and leverage Pipedream's capabilities to connect with hundreds of other apps for extended automation scenarios. It's ideal for building custom file management solutions, archiving systems, or collaborative content workflows without writing extensive code. + +# Example Use Cases + +- **Automated Backup to Dropbox**: Whenever a new file is uploaded to an FTP server, trigger a Pipedream workflow that automatically uploads this file to a specified Dropbox folder. This can serve as an off-site backup system for important documents or media files, ensuring they are safe and accessible from anywhere. + +- **Content Approval Workflow**: Create a system where new files added to a specific Dropbox folder trigger a Pipedream workflow, sending an approval request via Slack to a designated approver. Once the approver responds with approval, the workflow moves the file to a 'Published' folder within Dropbox, or if rejected, sends a notification back to the submitter with feedback. + +- **Dropbox to Google Sheets Logging**: Every time a new file is added to a Dropbox folder, a Pipedream workflow extracts relevant metadata (like filename, size, and upload date) and appends it to a Google Sheets spreadsheet. This creates an ongoing log for tracking uploads, which is particularly useful for teams needing to maintain records of content updates and revisions. diff --git a/components/dropbox/actions/create-a-text-file/create-a-text-file.mjs b/components/dropbox/actions/create-a-text-file/create-a-text-file.mjs index 0c79a4828af49..006b99ad757c8 100644 --- a/components/dropbox/actions/create-a-text-file/create-a-text-file.mjs +++ b/components/dropbox/actions/create-a-text-file/create-a-text-file.mjs @@ -2,23 +2,26 @@ import dropbox from "../../dropbox.app.mjs"; export default { name: "Create a Text File", - description: "Creates a brand new text file from plain text content you specify. [See docs here](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesUpload__anchor)", + description: "Creates a brand new text file from plain text content you specify. [See the documentation](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesUpload__anchor)", key: "dropbox-create-a-text-file", - version: "0.0.8", + version: "0.0.10", type: "action", props: { dropbox, name: { type: "string", - label: "File name", - description: "Your new file name.", + label: "File Name", + description: "Your new file name. Example: `textfile.txt`", }, path: { propDefinition: [ dropbox, - "pathFolder", + "path", + () => ({ + filter: ({ metadata: { metadata: { [".tag"]: type } } }) => type === "folder", + }), ], - description: "The file path in the user's Dropbox to create the file. If not filled, it will be created in the root folder.", + description: "Type the folder name to search for it in the user's Dropbox. If not filled, it will be created in the root folder.", }, content: { type: "string", @@ -33,9 +36,13 @@ export default { path, } = this; + const filename = name.includes(".txt") + ? name + : `${name}.txt`; + const res = await this.dropbox.uploadFile({ contents: Buffer.from(content), - path: this.dropbox.getNormalizedPath(path, true) + name, + path: this.dropbox.getNormalizedPath(path, true) + filename, autorename: true, }); diff --git a/components/dropbox/actions/create-folder/create-folder.mjs b/components/dropbox/actions/create-folder/create-folder.mjs index f0f4a0d511e45..98eda414d04b9 100644 --- a/components/dropbox/actions/create-folder/create-folder.mjs +++ b/components/dropbox/actions/create-folder/create-folder.mjs @@ -2,24 +2,27 @@ import dropbox from "../../dropbox.app.mjs"; export default { name: "Create folder", - description: "Create a folder. [See docs here](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesCreateFolderV2__anchor)", + description: "Create a Folder. [See the documentation](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesCreateFolderV2__anchor)", key: "dropbox-create-folder", - version: "0.0.8", + version: "0.0.10", type: "action", props: { dropbox, name: { type: "string", - label: "Folder name", + label: "Folder Name", description: "Your new folder name.", }, path: { propDefinition: [ dropbox, - "pathFolder", + "path", + () => ({ + filter: ({ metadata: { metadata: { [".tag"]: type } } }) => type === "folder", + }), ], optional: true, - description: "The file path in the user's Dropbox to create the folder. If not filled, it will be created in the root folder.", + description: "Type the folder name to search for it in the user's Dropbox. If not filled, it will be created in the root folder.", }, autorename: { type: "boolean", diff --git a/components/dropbox/actions/create-or-append-to-a-text-file/create-or-append-to-a-text-file.mjs b/components/dropbox/actions/create-or-append-to-a-text-file/create-or-append-to-a-text-file.mjs index 17fbbbdc40563..a0edb5a976b28 100644 --- a/components/dropbox/actions/create-or-append-to-a-text-file/create-or-append-to-a-text-file.mjs +++ b/components/dropbox/actions/create-or-append-to-a-text-file/create-or-append-to-a-text-file.mjs @@ -2,24 +2,27 @@ import dropbox from "../../dropbox.app.mjs"; export default { name: "Create or Append to a Text File", - description: "Adds a new line to an existing text file, or creates a file if it doesn't exist. [See docs here](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesUpload__anchor)", + description: "Adds a new line to an existing text file, or creates a file if it doesn't exist. [See the documentation](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesUpload__anchor)", key: "dropbox-create-or-append-to-a-text-file", - version: "0.0.8", + version: "0.0.10", type: "action", props: { dropbox, name: { type: "string", - label: "File name", + label: "File Name", description: "Your new file name", }, path: { propDefinition: [ dropbox, - "pathFolder", + "path", + () => ({ + filter: ({ metadata: { metadata: { [".tag"]: type } } }) => type === "folder", + }), ], optional: true, - description: "The file path in the user's Dropbox to create the file. If not filled, it will be created in the root folder.", + description: "Type the folder name to search for it in the user's Dropbox. If not filled, it will be created in the root folder.", }, content: { type: "string", diff --git a/components/dropbox/actions/create-update-share-link/common.mjs b/components/dropbox/actions/create-update-share-link/common.mjs new file mode 100644 index 0000000000000..c6126eea0c126 --- /dev/null +++ b/components/dropbox/actions/create-update-share-link/common.mjs @@ -0,0 +1,10 @@ +import dropbox from "../../dropbox.app.mjs"; + +export default { + props: { + dropbox: { + ...dropbox, + reloadProps: true, + }, + }, +}; diff --git a/components/dropbox/actions/create-update-share-link/create-update-share-link.mjs b/components/dropbox/actions/create-update-share-link/create-update-share-link.mjs index cea53b8c00d46..307df74d48702 100644 --- a/components/dropbox/actions/create-update-share-link/create-update-share-link.mjs +++ b/components/dropbox/actions/create-update-share-link/create-update-share-link.mjs @@ -1,60 +1,80 @@ -import dropbox from "../../dropbox.app.mjs"; +import common from "./common.mjs"; import consts from "../../common/consts.mjs"; export default { name: "Create/Update a Share Link", - description: "Creates or updates a public share link to the file or folder (It allows to share the file or folder with anyone). [See docs here](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#sharingCreateSharedLinkWithSettings__anchor)", + description: "Creates or updates a public share link to the file or folder (It allows you to share the file or folder with anyone). [See the documentation](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#sharingCreateSharedLinkWithSettings__anchor)", key: "dropbox-create-update-share-link", - version: "0.0.8", + version: "0.0.10", type: "action", props: { - dropbox, + ...common.props, + alert: { + type: "alert", + alertType: "warning", + content: `Dropbox Free and Basic users are able to create a publicly-available share link which allows downloads. + \nIn order to utilize advanced link sharing functionality, you must be on a Dropbox Essentials plan or higher. See the Dropbox pricing [page](https://www.dropbox.com/plans?trigger=nr#:~:text=Collaborative%20sharing) for more details.`, + }, path: { propDefinition: [ - dropbox, - "pathFileFolder", + common.props.dropbox, + "path", () => ({ - omitRootFolder: true, + initialOptions: [], + filter: ({ metadata: { metadata: { [".tag"]: type } } }) => [ + "file", + "folder", + ].includes(type), }), ], - description: "The path to be shared by the shared link.", - }, - requirePassword: { - type: "boolean", - label: "Require password", - description: "Boolean flag to enable or disable password protection.", - default: false, - }, - linkPassword: { - type: "string", - label: "Link password", - description: "If `require_password` is `true`, this is needed to specify the password to access the link.", - optional: true, - }, - allowDownload: { - type: "boolean", - label: "Allow downloads", - description: "Boolean flag to allow or not allow capabilities for shared links.", + description: "Type the file or folder name to search for it in the user's Dropbox.", }, - expires: { - type: "string", - label: "Expires", - description: "Expiration time of the shared link. By default the link never expires. Make sure to use a valid [timestamp](https://dropbox.github.io/dropbox-sdk-js/global.html#Timestamp) value.", - optional: true, - }, - audience: { - type: "string", - label: "Audience", - description: "The new audience who can benefit from the access level specified by the link's access level specified in the `link_access_level` field of `LinkPermissions`. This is used in conjunction with team policies and shared folder policies to determine the final effective audience type in the `effective_audience` field of `LinkPermissions.", - optional: true, - options: consts.CREATE_SHARED_LINK_AUDIENCE_OPTIONS, - }, - access: { - type: "string", - label: "Access", - description: "Requested access level you want the audience to gain from this link. Note, modifying access level for an existing link is not supported.", - optional: true, - options: consts.CREATE_SHARED_LINK_ACCESS_OPTIONS, + }, + async additionalProps() { + const props = {}; + + const accountType = await this.getCurrentAccount(); + if (accountType !== "basic") { + props.requirePassword = { + type: "boolean", + label: "Require Password", + description: "Boolean flag to enable or disable password protection.", + default: false, + reloadProps: true, + }; + props.linkPassword = { + type: "string", + label: "Link Password", + description: "If `require_password` is `true`, this is needed to specify the password to access the link.", + hidden: !this.requirePassword, + }; + props.allowDownload = { + type: "boolean", + label: "Allow Downloads", + description: "Boolean flag to allow or not allow capabilities for shared links.", + }; + props.expires = { + type: "string", + label: "Expires", + description: "Expiration time of the shared link. By default the link never expires. Make sure to use a valid [timestamp](https://dropbox.github.io/dropbox-sdk-js/global.html#Timestamp) value. Example: `2024-07-18T20:00:00Z`", + optional: true, + }; + props.access = { + type: "string", + label: "Access", + description: "Requested access level you want the audience to gain from this link. Note, modifying access level for an existing link is not supported.", + optional: true, + options: consts.CREATE_SHARED_LINK_ACCESS_OPTIONS, + }; + } + + return props; + }, + methods: { + async getCurrentAccount() { + const dpx = await this.dropbox.sdk(); + const { result: { account_type: accountType } } = await dpx.usersGetCurrentAccount(); + return accountType[".tag"]; }, }, async run({ $ }) { @@ -63,29 +83,31 @@ export default { requirePassword, linkPassword, expires, - audience, access, - requestedVisibility, - allowDownload, - removeExpiration, } = this; + const accountType = await this.getCurrentAccount(); + const allowDownload = accountType === "basic" + ? true + : this.allowDownload; + if (requirePassword && !linkPassword) { throw new Error("Since the password is required, please add a linkPassword"); } + if (expires && Date.parse(expires) < Date.now()) { + throw new Error("Expire date must be later than the current datetime"); + } + const res = await this.dropbox.createSharedLink({ path: this.dropbox.getPath(path), settings: { require_password: requirePassword, link_password: linkPassword, expires, - audience, access, - requested_visibility: requestedVisibility, allow_download: allowDownload, }, - remove_expiration: removeExpiration, }); $.export("$summary", `Shared link for "${path?.label || path}" successfully created`); return res; diff --git a/components/dropbox/actions/delete-file-folder/delete-file-folder.mjs b/components/dropbox/actions/delete-file-folder/delete-file-folder.mjs index 261fed512eb3b..53877d72907d0 100644 --- a/components/dropbox/actions/delete-file-folder/delete-file-folder.mjs +++ b/components/dropbox/actions/delete-file-folder/delete-file-folder.mjs @@ -2,21 +2,25 @@ import dropbox from "../../dropbox.app.mjs"; export default { name: "Delete a File/Folder", - description: "Permanently removes a file/folder from the server. [See docs here](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesDeleteV2__anchor)", + description: "Permanently removes a file/folder from the server. [See documentation](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesDeleteV2__anchor)", key: "dropbox-delete-file-folder", - version: "0.0.8", + version: "0.0.10", type: "action", props: { dropbox, path: { propDefinition: [ dropbox, - "pathFileFolder", + "path", () => ({ - omitRootFolder: true, + initialOptions: [], + filter: ({ metadata: { metadata: { [".tag"]: type } } }) => [ + "file", + "folder", + ].includes(type), }), ], - description: "Path in the user's Dropbox to delete.", + description: "Type the file or folder name to search for it in the user's Dropbox.", }, }, async run({ $ }) { diff --git a/components/dropbox/actions/download-file-to-tmp/download-file-to-tmp.mjs b/components/dropbox/actions/download-file-to-tmp/download-file-to-tmp.mjs index d1a19d12b688b..5ca57f678eab3 100644 --- a/components/dropbox/actions/download-file-to-tmp/download-file-to-tmp.mjs +++ b/components/dropbox/actions/download-file-to-tmp/download-file-to-tmp.mjs @@ -6,20 +6,22 @@ export default { name: "Download File to TMP", description: "Download a specific file to the temporary directory. [See the documentation](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesDownload__anchor).", key: "dropbox-download-file-to-tmp", - version: "0.0.4", + version: "0.0.6", type: "action", props: { dropbox, path: { propDefinition: [ dropbox, - "pathFile", + "path", + () => ({ + initialOptions: [], + }), ], - description: "The file path in the user's Dropbox to download.", }, name: { type: "string", - label: "File name", + label: "File Name", description: "The new name of the file to be saved, including it's extension. e.g: `myFile.csv`", optional: true, }, @@ -33,16 +35,18 @@ export default { path, cleanup, } = await file(); + const extension = result.name.split(".").pop(); + const tmpPath = this.name ? `/tmp/${this.name}` - : path; + : `${path}.${extension}`; await fs.promises.appendFile(tmpPath, Buffer.from(result.fileBinary)); await cleanup(); delete result.fileBinary; - $.export("$summary", `File successfully saved in "/tmp/${this.name}"`); + $.export("$summary", `File successfully saved in "${tmpPath}"`); return { tmpPath, diff --git a/components/dropbox/actions/list-file-folders-in-a-folder/list-file-folders-in-a-folder.mjs b/components/dropbox/actions/list-file-folders-in-a-folder/list-file-folders-in-a-folder.mjs index 154fb40f69508..0e0fd2a540546 100644 --- a/components/dropbox/actions/list-file-folders-in-a-folder/list-file-folders-in-a-folder.mjs +++ b/components/dropbox/actions/list-file-folders-in-a-folder/list-file-folders-in-a-folder.mjs @@ -2,18 +2,22 @@ import dropbox from "../../dropbox.app.mjs"; export default { name: "List All Files/Subfolders in a Folder", - description: "Retrieves a list of files or subfolders in a specified folder [See the docs here](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesListFolder__anchor)", + description: "Retrieves a list of files or subfolders in a specified folder [See the documentation](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesListFolder__anchor)", key: "dropbox-list-file-folders-in-a-folder", - version: "0.0.8", + version: "0.0.10", type: "action", props: { dropbox, path: { propDefinition: [ dropbox, - "pathFolder", + "path", + () => ({ + initialOptions: [], + filter: ({ metadata: { metadata: { [".tag"]: type } } }) => type === "folder", + }), ], - description: "Scopes the search to a path in the user's Dropbox.", + description: "Type the folder name to search for it in the user's Dropbox.", }, recursive: { type: "boolean", @@ -23,7 +27,7 @@ export default { }, includeDeleted: { type: "boolean", - label: "Include deleted", + label: "Include Deleted", description: "If `true`, the results will include files and folders that used to exist but were deleted.", default: false, }, diff --git a/components/dropbox/actions/list-file-revisions/list-file-revisions.mjs b/components/dropbox/actions/list-file-revisions/list-file-revisions.mjs index c4d26d36f9d0e..c9b6137b263d6 100644 --- a/components/dropbox/actions/list-file-revisions/list-file-revisions.mjs +++ b/components/dropbox/actions/list-file-revisions/list-file-revisions.mjs @@ -3,23 +3,25 @@ import dropbox from "../../dropbox.app.mjs"; export default { name: "List File Revisions", - description: "Retrieves a list of file revisions needed to recover previous content. [See docs here](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesListRevisions__anchor)", + description: "Retrieves a list of file revisions needed to recover previous content. [See the documentation](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesListRevisions__anchor)", key: "dropbox-list-file-revisions", - version: "0.0.8", + version: "0.0.10", type: "action", props: { dropbox, path: { propDefinition: [ dropbox, - "pathFile", + "path", + () => ({ + initialOptions: [], + }), ], - description: "The file path for the file whose revisions you'd like to list.", }, mode: { type: "string", label: "Mode", - description: "Determines the behavior of the API in listing the revisions for a given file path or id.", + description: "Determines the behavior of the API in listing the revisions for a given file path or id. In `path` (default) mode, all revisions at the same file path as the latest file entry are returned. If revisions with the same file id are desired, then mode must be set to `id`.", optional: true, options: consts.LIST_FILE_REVISIONS_OPTIONS, }, diff --git a/components/dropbox/actions/move-file-folder/move-file-folder.mjs b/components/dropbox/actions/move-file-folder/move-file-folder.mjs index edc58066aa0b2..146eccc7c265c 100644 --- a/components/dropbox/actions/move-file-folder/move-file-folder.mjs +++ b/components/dropbox/actions/move-file-folder/move-file-folder.mjs @@ -2,30 +2,37 @@ import dropbox from "../../dropbox.app.mjs"; export default { name: "Move a File/Folder", - description: "Moves a file or folder to a different location in the user's Dropbox [See the docs here](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesMoveV2__anchor)", + description: "Moves a file or folder to a different location in the user's Dropbox [See the documentation](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesMoveV2__anchor)", key: "dropbox-move-file-folder", - version: "0.0.9", + version: "0.0.11", type: "action", props: { dropbox, pathFrom: { propDefinition: [ dropbox, - "pathFileFolder", + "path", () => ({ - omitRootFolder: true, + initialOptions: [], + filter: ({ metadata: { metadata: { [".tag"]: type } } }) => [ + "file", + "folder", + ].includes(type), }), ], - label: "Path from", - description: "The file/folder that you want to move.", + label: "Path From", + description: "Type the file or folder name to search for it in the user's Dropbox.", }, pathTo: { propDefinition: [ dropbox, - "pathFolder", + "path", + () => ({ + filter: ({ metadata: { metadata: { [".tag"]: type } } }) => type === "folder", + }), ], - label: "Path to", - description: "The new path of your file/folder", + label: "Path To", + description: "Type the folder name to search for it in the user's Dropbox.", }, autorename: { type: "boolean", @@ -35,7 +42,7 @@ export default { }, allowOwnershipTransfer: { type: "boolean", - label: "Allow ownership transfer", + label: "Allow Ownership Transfer", description: "Allow moves by owner even if it would result in an ownership transfer for the content being moved. This does not apply to copies.", optional: true, }, diff --git a/components/dropbox/actions/rename-file-folder/rename-file-folder.mjs b/components/dropbox/actions/rename-file-folder/rename-file-folder.mjs index fa5b90b8cbb66..ff194300ca8ca 100644 --- a/components/dropbox/actions/rename-file-folder/rename-file-folder.mjs +++ b/components/dropbox/actions/rename-file-folder/rename-file-folder.mjs @@ -2,22 +2,26 @@ import dropbox from "../../dropbox.app.mjs"; export default { name: "Rename a File/Folder", - description: "Renames a file or folder in the user's Dropbox [See the docs here](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesMoveV2__anchor)", + description: "Renames a file or folder in the user's Dropbox [See the documentation](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesMoveV2__anchor)", key: "dropbox-rename-file-folder", - version: "0.0.8", + version: "0.0.10", type: "action", props: { dropbox, pathFrom: { propDefinition: [ dropbox, - "pathFileFolder", + "path", () => ({ - omitRootFolder: true, + initialOptions: [], + filter: ({ metadata: { metadata: { [".tag"]: type } } }) => [ + "file", + "folder", + ].includes(type), }), ], label: "Path From", - description: "The file that you want to rename.", + description: "Type the file or folder name to search for it in the user's Dropbox.", }, newName: { type: "string", diff --git a/components/dropbox/actions/restore-a-file/restore-a-file.mjs b/components/dropbox/actions/restore-a-file/restore-a-file.mjs index 3eeb6468ecbdc..3e2253bbe4c01 100644 --- a/components/dropbox/actions/restore-a-file/restore-a-file.mjs +++ b/components/dropbox/actions/restore-a-file/restore-a-file.mjs @@ -2,21 +2,21 @@ import dropbox from "../../dropbox.app.mjs"; export default { name: "Restore a File", - description: "Restores a previous file version. [See docs here](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesRestore__anchor)", + description: "Restores a previous file version. [See the documentation](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesRestore__anchor)", key: "dropbox-restore-a-file", - version: "0.0.8", + version: "0.0.10", type: "action", props: { dropbox, path: { propDefinition: [ dropbox, - "pathFile", + "path", () => ({ - omitRootFolder: true, + initialOptions: [], }), ], - description: "The path to save the restored file.", + description: "Type the file name to search for it in the user's Dropbox.", }, rev: { propDefinition: [ diff --git a/components/dropbox/actions/search-files-folders/search-files-folders.mjs b/components/dropbox/actions/search-files-folders/search-files-folders.mjs index 2fe54b5a3875c..beac0b14da4ee 100644 --- a/components/dropbox/actions/search-files-folders/search-files-folders.mjs +++ b/components/dropbox/actions/search-files-folders/search-files-folders.mjs @@ -4,9 +4,9 @@ import consts from "../../common/consts.mjs"; export default { name: "Search files and folders", - description: "Searches for files and folders by name. [See the docs here](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesSearchV2__anchor)", + description: "Searches for files and folders by name. [See the documentation](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesSearchV2__anchor)", key: "dropbox-search-files-folders", - version: "0.0.8", + version: "0.0.10", type: "action", props: { dropbox, @@ -19,47 +19,49 @@ export default { path: { propDefinition: [ dropbox, - "pathFolder", + "path", + () => ({ + filter: ({ metadata: { metadata: { [".tag"]: type } } }) => type === "folder", + }), ], - optional: true, - description: "Scopes the search to a path in the user's Dropbox. Searches the entire Dropbox if not specified.", + description: "Type the folder name to search for it in the user's Dropbox.", }, orderBy: { type: "string", - label: "Order by", + label: "Order By", description: "By default, results are sorted by relevance.", optional: true, options: consts.SEARCH_FILE_FOLDER_ORDER_BY_OPTIONS, }, fileStatus: { type: "string", - label: "File status", + label: "File Status", description: "Restricts search to the given file status.", optional: true, options: consts.SEARCH_FILE_FOLDER_STATUS_OPTIONS, }, filenameOnly: { type: "boolean", - label: "Filename only", + label: "Filename Only", description: "Restricts search to only match on filenames.", optional: true, }, fileCategories: { type: "string[]", - label: "File categories", + label: "File Categories", description: "Restricts search to only the file categories specified. Only supported for active file search.", optional: true, options: consts.FILES_CATEGORIES_OPTIONS, }, fileExtensions: { type: "string[]", - label: "File extensions", + label: "File Extensions", description: "Restricts search to only the extensions specified. Only supported for active file search.", optional: true, }, includeHighlights: { type: "boolean", - label: "Include highlights", + label: "Include Highlights", description: "Whether to include highlight span from file title.", optional: true, }, diff --git a/components/dropbox/actions/upload-file/upload-file.mjs b/components/dropbox/actions/upload-file/upload-file.mjs index def119a0a4971..85e5d3a3030e2 100644 --- a/components/dropbox/actions/upload-file/upload-file.mjs +++ b/components/dropbox/actions/upload-file/upload-file.mjs @@ -2,26 +2,30 @@ import dropbox from "../../dropbox.app.mjs"; import consts from "../../common/consts.mjs"; import fs from "fs"; import got from "got"; +import { ConfigurationError } from "@pipedream/platform"; export default { name: "Upload a File", - description: "Uploads a file to a selected folder. [See docs here](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesUpload__anchor)", + description: "Uploads a file to a selected folder. [See the documentation](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesUpload__anchor)", key: "dropbox-upload-file", - version: "0.0.11", + version: "0.0.13", type: "action", props: { dropbox, path: { propDefinition: [ dropbox, - "pathFolder", + "path", + () => ({ + filter: ({ metadata: { metadata: { [".tag"]: type } } }) => type === "folder", + }), ], - description: "The file path in the user's Dropbox to create the file. If not filled, it will be created in the root folder.", + description: "Type the folder name to search for it in the user's Dropbox. If not filled, it will be created in the root folder.", }, name: { type: "string", - label: "File name", - description: "The name of your new file.", + label: "File Name", + description: "The name of your new file (make sure to include the file extension).", }, fileUrl: { type: "string", @@ -74,6 +78,10 @@ export default { clientModified, } = this; + if (!fileUrl && !filePath) { + throw new ConfigurationError("Must specify either File URL or File Path."); + } + const contents = fileUrl ? await got.stream(fileUrl) : fs.createReadStream(filePath); diff --git a/components/dropbox/actions/upload-multiple-files/upload-multiple-files.mjs b/components/dropbox/actions/upload-multiple-files/upload-multiple-files.mjs new file mode 100644 index 0000000000000..817b0b37b7321 --- /dev/null +++ b/components/dropbox/actions/upload-multiple-files/upload-multiple-files.mjs @@ -0,0 +1,135 @@ +import dropbox from "../../dropbox.app.mjs"; +import consts from "../../common/consts.mjs"; +import fs from "fs"; +import got from "got"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + name: "Upload Multiple Files", + description: "Uploads multiple file to a selected folder. [See the documentation](https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesUpload__anchor)", + key: "dropbox-upload-multiple-files", + version: "0.0.1", + type: "action", + props: { + dropbox, + path: { + propDefinition: [ + dropbox, + "path", + () => ({ + filter: ({ metadata: { metadata: { [".tag"]: type } } }) => type === "folder", + }), + ], + description: "The folder to upload to. Type the folder name to search for it in the user's Dropbox.", + }, + fileUrls: { + type: "string[]", + label: "File URLs", + description: "The URLs of the files you want to upload to Dropbox. Must specify either File URLs or File Paths.", + default: [], + optional: true, + }, + filePaths: { + type: "string[]", + label: "File Paths", + description: "The paths to the files, e.g. /tmp/myFile.csv . Must specify either File URLs or File Paths.", + default: [], + optional: true, + }, + filenames: { + type: "string[]", + label: "File Names", + description: "An array of filenames for the new files. Please provide a name for each URL and/or Path. Make sure to include the file extensions.", + }, + autorename: { + type: "boolean", + label: "Autorename", + description: "If there's a conflict, have Dropbox try to autorename the file to avoid the conflict.", + optional: true, + }, + mute: { + type: "boolean", + label: "Mute", + description: "Normally, users are made aware of any file modifications in their Dropbox account via notifications in the client software. If `true`, this will not result in a user notification.", + optional: true, + }, + strictConflict: { + type: "boolean", + label: "Strict Conflict", + description: "Be more strict about how each WriteMode detects conflict. For example, always return a conflict error when mode = WriteMode.update and the given \"rev\" doesn't match the existing file's \"rev\", even if the existing file has been deleted. This also forces a conflict even when the target path refers to a file with identical contents.", + optional: true, + }, + mode: { + type: "string", + label: "Mode", + description: "Selects what to do if the file already exists.", + options: consts.UPLOAD_FILE_MODE_OPTIONS, + optional: true, + }, + }, + async run({ $ }) { + const { + dropbox, + path, + fileUrls, + filePaths, + autorename, + mute, + strictConflict, + mode, + filenames, + } = this; + + if (!fileUrls?.length && !filePaths?.length) { + throw new ConfigurationError("Must specify either File URLs or File Paths."); + } + + const numFiles = fileUrls.length + filePaths.length; + if (numFiles !== filenames.length) { + throw new ConfigurationError(`Number of filenames must match number of files. Detected ${numFiles} file(s) and ${filenames.length} filename(s)`); + } + + const fileInfo = []; + const normalizedPath = dropbox.getNormalizedPath(path, true); + let i = 0; + + if (fileUrls?.length) { + for (const url of fileUrls) { + fileInfo.push({ + contents: await got.stream(url), + path: `${normalizedPath}${filenames[i]}`, + }); + i++; + } + } + + if (filePaths?.length) { + for (const filePath of filePaths) { + fileInfo.push({ + contents: fs.createReadStream(filePath), + path: `${normalizedPath}${filenames[i]}`, + }); + i++; + } + } + + const responses = []; + for (const file of fileInfo) { + const { result } = await dropbox.uploadFile({ + contents: file.contents, + autorename, + path: file.path, + mode: mode + ? { + ".tag": mode, + } + : undefined, + mute, + strict_conflict: strictConflict, + }); + responses.push(result); + } + $.export("$summary", "Files successfully uploaded"); + return responses; + }, +}; diff --git a/components/dropbox/common/config.mjs b/components/dropbox/common/config.mjs index 28d42773d7723..b75d10848c32d 100644 --- a/components/dropbox/common/config.mjs +++ b/components/dropbox/common/config.mjs @@ -1,6 +1,6 @@ export default { SEARCH_FILE_FOLDERS: { - DEFAULT_MAX_RESULTS: 100, + DEFAULT_MAX_RESULTS: 1000, }, LIST_FILES_IN_FOLDER: { DEFAULT_MAX_RESULTS: 100, diff --git a/components/dropbox/common/consts.mjs b/components/dropbox/common/consts.mjs index c3a7b2cf961d7..0edd17b699b4f 100644 --- a/components/dropbox/common/consts.mjs +++ b/components/dropbox/common/consts.mjs @@ -23,7 +23,6 @@ export default { LIST_FILE_REVISIONS_OPTIONS: [ "path", "id", - "other", ], CREATE_SHARED_LINK_AUDIENCE_OPTIONS: [ "public", @@ -34,10 +33,22 @@ export default { "other", ], CREATE_SHARED_LINK_ACCESS_OPTIONS: [ - "viewer", - "editor", - "max", - "other", + { + value: "viewer", + label: "Users who use the link can view and comment on the content", + }, + { + value: "editor", + label: "Users who use the link can edit, view and comment on the content. Note, not all file types support edit links yet.", + }, + { + value: "max", + label: "Request for the maximum access level you can set the link to", + }, + { + value: "default", + label: "Request for the default access level the user has set", + }, ], CREATE_SHARED_LINK_REQUESTED_VISIBILITY_OPTIONS: [ "public", diff --git a/components/dropbox/dropbox.app.mjs b/components/dropbox/dropbox.app.mjs index 59f2e1ebc256b..c88925fbdde31 100644 --- a/components/dropbox/dropbox.app.mjs +++ b/components/dropbox/dropbox.app.mjs @@ -12,78 +12,90 @@ export default { type: "app", app: "dropbox", propDefinitions: { - pathFolder: { + path: { type: "string", label: "Path", - description: "The folder path. (Please use a valid path to filter the values and type at least 2 characters after the latest '/' to perform the search.)", + description: "Type the file name to search for it in the user's Dropbox.", useQuery: true, withLabel: true, async options({ - prevContext, - query, - returnSimpleString, - omitRootFolder, - }) { - if (prevContext?.reachedLastPage) { - return []; - } - return this.getPathOptions( - query, + query, prevContext: { cursor }, + params, + initialOptions = [ { - omitFiles: true, - returnSimpleString, - omitRootFolder, + label: "Root Folder", + value: "", }, - ); - }, - }, - pathFile: { - type: "string", - label: "Path", - description: "The file path. (Please use a valid path to filter the values)", - useQuery: true, - withLabel: true, - async options({ - prevContext, - query, - returnSimpleString, - omitRootFolder, + ], + filter = ({ metadata: { metadata: { [".tag"]: type } } }) => type === "file", + mapper = ({ metadata: { metadata } }) => ({ + label: metadata.path_display, + value: metadata.path_lower, + }), }) { - if (prevContext?.reachedLastPage) { - return []; - } - return this.getPathOptions( - query, - { - omitFolders: true, - returnSimpleString, - omitRootFolder, - }, - ); - }, - }, - pathFileFolder: { - type: "string", - label: "Path", - description: "The file or folder path. (Please use a valid path to filter the values)", - useQuery: true, - withLabel: true, - async options({ - prevContext, - query, - returnSimpleString, - omitRootFolder, - }) { - if (prevContext?.reachedLastPage) { + try { + const dpx = await this.sdk(); + + if (cursor === null) { + return []; + } + + if (cursor) { + const { + result: { + matches, + cursor: nextCursor, + has_more: hasMore, + }, + } = await dpx.filesSearchContinueV2({ + cursor, + }); + + return { + options: matches + .filter(filter) + .map(mapper), + context: { + cursor: hasMore + ? nextCursor + : null, + }, + }; + } + + const { + result: { + matches, + cursor: nextCursor, + has_more: hasMore, + }, + } = await dpx.filesSearchV2({ + ...params, + query: query || "text", + options: { + path: params?.path || "", + max_results: config.SEARCH_FILE_FOLDERS.DEFAULT_MAX_RESULTS, + ...params?.options, + }, + }); + + return { + options: initialOptions.concat( + matches + .filter(filter) + .map(mapper), + ), + context: { + cursor: hasMore + ? nextCursor + : null, + }, + }; + + } catch (error) { + console.log("Error searching for files/folders", error); return []; } - return this.getPathOptions( - query, - { - returnSimpleString, - omitRootFolder, - }, - ); }, }, fileRevision: { @@ -178,7 +190,7 @@ export default { const revisions = await dpx.filesListRevisions({ path, mode: { - ".tag": "id", + ".tag": "path", }, }); return revisions.result?.entries?.map((revision) => ({ @@ -189,106 +201,6 @@ export default { this.normalizeError(err); } }, - async getPathOptions(path, opts = {}) { - try { - const { - omitFolders, - omitFiles, - omitRootFolder, - } = opts; - - let data = []; - path = path === "/" || path === null - ? "" - : path; - - if (path.length > 0 && !path.startsWith("/")) { - path = "/" + path; - } - - if (path === "") { - const entries = await this.listFilesFolders({ - path, - recursive: true, - include_mounted_folders: true, - }); - - data = entries.map((item) => ({ - path: item.path_display, - type: item[".tag"], - })); - - } else { - let subpath = ""; - let query = path; - if ((path.match(/\//g) || []).length > 1) { - const splitPath = path.split("/"); - query = splitPath.pop(); - subpath = splitPath.join("/"); - } - const res = await this.searchFilesFolders({ - query, - options: { - path: subpath, - }, - }); - - data = res.map(({ metadata }) => ({ - path: metadata.metadata.path_display, - type: metadata.metadata[".tag"], - })); - - const folders = data.filter((item) => item.type !== "file"); - for (const folder of folders) { - const entries = await this.listFilesFolders({ - path: folder.path, - recursive: true, - include_mounted_folders: true, - }); - const folderData = entries?.map((item) => ({ - path: item.path_display, - type: item[".tag"], - })); - data.push(...folderData); - } - } - - if (omitFiles) { - data = data.filter((item) => item.type !== "file"); - } - - if (omitFolders) { - data = data.filter((item) => item.type !== "folder"); - } - - data = data.map((item) => ({ - label: item.path, - value: item.path, - })); - - if (path === "" && !omitFolders && !omitRootFolder) { - data = [ - { - label: "Root Folder", - value: "", - }, - ...data, - ]; - } - - data.sort((a, b) => { - return a > b ? - 1 : - -1; - }); - - return data; - - } catch (err) { - console.log(err); - return []; - } - }, async initState(context) { const { path, diff --git a/components/dropbox/package.json b/components/dropbox/package.json index cf7626a85ea2c..9f56f31fb5a3c 100644 --- a/components/dropbox/package.json +++ b/components/dropbox/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/dropbox", - "version": "0.3.18", + "version": "0.4.0", "description": "Pipedream Dropbox Components", "main": "dropbox.app.mjs", "keywords": [ diff --git a/components/dropbox/sources/all-updates/all-updates.mjs b/components/dropbox/sources/all-updates/all-updates.mjs index ce2c376f9280a..6903ea6c64040 100644 --- a/components/dropbox/sources/all-updates/all-updates.mjs +++ b/components/dropbox/sources/all-updates/all-updates.mjs @@ -1,4 +1,5 @@ import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, @@ -6,7 +7,7 @@ export default { type: "source", key: "dropbox-all-updates", name: "New or Modified File or Folder", - version: "0.0.15", + version: "0.0.17", description: "Emit new event when a file or folder is added or modified. Make sure the number of files/folders in the watched folder does not exceed 4000.", props: { ...common.props, @@ -25,14 +26,20 @@ export default { }, hooks: { async activate() { - await this.getHistoricalEvents([ - "file", - "folder", - ]); + await this.getHistoricalEvents(this.getFileTypes()); const state = await this.dropbox.initState(this); this._setDropboxState(state); }, }, + methods: { + ...common.methods, + getFileTypes() { + return [ + "file", + "folder", + ]; + }, + }, async run() { const state = this._getDropboxState(); const { @@ -43,10 +50,14 @@ export default { let file = { ...update, }; - if (this.includeMediaInfo) { + const fileTypes = this.getFileTypes(); + if (!fileTypes.includes(file[".tag"])) { + continue; + } + if (this.includeMediaInfo && file[".tag"] === "file") { file = await this.getMediaInfo(update); } - if (this.includeLink) { + if (this.includeLink && file[".tag"] === "file") { file.link = await this.getTemporaryLink(update); } // new unique identification from merging the file id and the last file revision @@ -56,4 +67,5 @@ export default { this.$emit(file, this.getMeta(id, file.path_display || file.id)); } }, + sampleEmit, }; diff --git a/components/dropbox/sources/all-updates/test-event.mjs b/components/dropbox/sources/all-updates/test-event.mjs new file mode 100644 index 0000000000000..dd6739b995ece --- /dev/null +++ b/components/dropbox/sources/all-updates/test-event.mjs @@ -0,0 +1,14 @@ +export default { + ".tag": "file", + "name": "Document.docx", + "path_lower": "/document.docx", + "path_display": "/Document.docx", + "id": "id:yVswVBnnL7cAAAAAAAAAdg", + "client_modified": "2023-04-27T17:50:32Z", + "server_modified": "2023-07-17T19:28:05Z", + "rev": "01600b3cc31f26000000002574cf900", + "size": 10468, + "is_downloadable": true, + "content_hash": "065e09a66c9bb9264a6657083c9e7a06f8e2e9bc6a5420900c737250ed108147", + "link": "https://uc2aea9ca1ec13663730d4f924d1.dl.dropboxusercontent.com/cd/0/get/CWzN/file" +} \ No newline at end of file diff --git a/components/dropbox/sources/common/common.mjs b/components/dropbox/sources/common/common.mjs index 8970ab1c7a4f9..e7ea94bd81d6d 100644 --- a/components/dropbox/sources/common/common.mjs +++ b/components/dropbox/sources/common/common.mjs @@ -7,8 +7,12 @@ export default { path: { propDefinition: [ dropbox, - "pathFolder", + "path", + () => ({ + filter: ({ metadata: { metadata: { [".tag"]: type } } }) => type === "folder", + }), ], + description: "Type the folder name to search for it in the user's Dropbox.", }, recursive: { propDefinition: [ @@ -40,7 +44,7 @@ export default { if (!fileTypes.includes(file[".tag"])) { continue; } - if (this.includeLink) { + if (this.includeLink && file[".tag"] === "file") { file.link = await this.getTemporaryLink(file); } this.$emit(file, this.getMeta(file.id, file.path_display || file.id)); diff --git a/components/dropbox/sources/new-file/new-file.mjs b/components/dropbox/sources/new-file/new-file.mjs index 56b3c6b02dd96..e33f4b5b9ab88 100644 --- a/components/dropbox/sources/new-file/new-file.mjs +++ b/components/dropbox/sources/new-file/new-file.mjs @@ -7,7 +7,7 @@ export default { type: "source", key: "dropbox-new-file", name: "New File", - version: "0.0.16", + version: "0.0.18", description: "Emit new event when a new file is added to your account or a specific folder. Make sure the number of files/folders in the watched folder does not exceed 4000.", props: { ...common.props, diff --git a/components/dropbox/sources/new-folder/new-folder.mjs b/components/dropbox/sources/new-folder/new-folder.mjs index af654571ee13d..a39817937ccf7 100644 --- a/components/dropbox/sources/new-folder/new-folder.mjs +++ b/components/dropbox/sources/new-folder/new-folder.mjs @@ -1,4 +1,5 @@ import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, @@ -6,7 +7,7 @@ export default { type: "source", key: "dropbox-new-folder", name: "New Folder", - version: "0.0.15", + version: "0.0.17", description: "Emit new event when a new folder is created. Make sure the number of files/folders in the watched folder does not exceed 4000.", hooks: { async activate() { @@ -30,4 +31,5 @@ export default { this.$emit(update, this.getMeta(update.id, update.path_display || update.id)); } }, + sampleEmit, }; diff --git a/components/dropbox/sources/new-folder/test-event.mjs b/components/dropbox/sources/new-folder/test-event.mjs new file mode 100644 index 0000000000000..111ad3fa9e50f --- /dev/null +++ b/components/dropbox/sources/new-folder/test-event.mjs @@ -0,0 +1,14 @@ +export default { + ".tag": "folder", + "name": "Clients", + "path_lower": "/clients", + "path_display": "/Clients", + "id": "id:yVswVBnnL7cAAAAAAAAAbQ", + "shared_folder_id": "4005125857", + "sharing_info": { + "read_only": false, + "shared_folder_id": "4005125857", + "traverse_only": false, + "no_access": false + } +} \ No newline at end of file diff --git a/components/dropcontact/README.md b/components/dropcontact/README.md new file mode 100644 index 0000000000000..13d50f41946c5 --- /dev/null +++ b/components/dropcontact/README.md @@ -0,0 +1,11 @@ +# Overview + +The Dropcontact API on Pipedream allows you to enrich and clean contact data dynamically within a serverless workflow. It can find, verify, and correct email addresses, providing additional information like company details and social network profiles. Integrating Dropcontact into Pipedream workflows enables automated data enhancement tasks that can trigger actions in other apps for marketing, sales, or CRM purposes. + +# Example Use Cases + +- **Validate and Enrich Leads Automatically**: This workflow triggers when a new lead is captured, perhaps via a Typeform submission. Pipedream sends the lead’s data to Dropcontact to validate and enrich the email address, and then uses the enriched data to update the lead in a CRM like Salesforce or HubSpot. + +- **Email List Cleaning before Campaigns**: Before sending out a marketing campaign through an email service like Mailchimp, you can run your mailing list through a Pipedream workflow that uses Dropcontact to verify and clean the emails, reducing bounce rates and improving deliverability. + +- **Sync Enriched Contacts to Google Sheets**: Set up a Pipedream workflow where you drop a CSV into Google Drive. Pipedream parses the file, sends each contact to Dropcontact for enrichment, and appends the results to a Google Sheet, giving you an updated, enriched contact list for outreach or reporting. diff --git a/components/dropinblog/actions/create-post/create-post.mjs b/components/dropinblog/actions/create-post/create-post.mjs new file mode 100644 index 0000000000000..0516d5ed697f3 --- /dev/null +++ b/components/dropinblog/actions/create-post/create-post.mjs @@ -0,0 +1,112 @@ +import app from "../../dropinblog.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "dropinblog-create-post", + name: "Create Post", + description: "Allows you to create a new blog post in your DropInBlog account. Requires a private API key. [See the documentation](https://dropinblog.readme.io/reference/posts-create).", + version: "0.0.1", + type: "action", + props: { + app, + title: { + propDefinition: [ + app, + "title", + ], + }, + content: { + propDefinition: [ + app, + "content", + ], + }, + seoTitle: { + type: "string", + label: "SEO Title", + description: "SEO title for your post. If not set it will default to the title.", + optional: true, + }, + seoDescription: { + type: "string", + label: "SEO Description", + description: "SEO description for your post.", + optional: true, + }, + keyword: { + type: "string", + label: "Keyword", + description: "SEO keyword for your post.", + optional: true, + }, + publishedAt: { + type: "string", + label: "Published At", + description: "Published date and time for your post. If set the post will be published. If not set the post will be put in draft. Eg: `2021-01-01T00:00:00Z`", + optional: true, + }, + featuredImage: { + type: "string", + label: "Featured Image", + description: "Featured image URL for your post.", + optional: true, + }, + categoryIds: { + type: "integer[]", + description: "Category IDs to attach to your post.", + propDefinition: [ + app, + "categoryId", + ], + }, + authorId: { + propDefinition: [ + app, + "authorId", + ], + }, + }, + methods: { + createPost(args = {}) { + return this.app.post({ + path: "/posts", + headers: { + usePrivateApiKey: true, + }, + ...args, + }); + }, + }, + async run({ $ }) { + const { + createPost, + title, + content, + seoTitle, + seoDescription, + keyword, + publishedAt, + featuredImage, + categoryIds, + authorId, + } = this; + + const response = await createPost({ + $, + data: { + title, + content, + seo_title: seoTitle, + seo_description: seoDescription, + keyword, + published_at: publishedAt, + featured_image: featuredImage, + category_ids: utils.parseArray(categoryIds), + author_id: authorId, + }, + }); + + $.export("$summary", `Successfully created a new blog post with ID \`${response.data.post.id}\``); + return response; + }, +}; diff --git a/components/dropinblog/common/constants.mjs b/components/dropinblog/common/constants.mjs new file mode 100644 index 0000000000000..00fd64166b07e --- /dev/null +++ b/components/dropinblog/common/constants.mjs @@ -0,0 +1,15 @@ +const BLOG_PLACEHOLDER = ""; +const BASE_URL = "https://api.dropinblog.com"; +const VERSION_PATH = `/v2/blog/${BLOG_PLACEHOLDER}`; +const LAST_DATE_AT = "lastCreatedAt"; +const DEFAULT_MAX = 100; +const DEFAULT_LIMIT = 25; + +export default { + BLOG_PLACEHOLDER, + BASE_URL, + VERSION_PATH, + DEFAULT_MAX, + DEFAULT_LIMIT, + LAST_DATE_AT, +}; diff --git a/components/dropinblog/common/utils.mjs b/components/dropinblog/common/utils.mjs new file mode 100644 index 0000000000000..587292d060b01 --- /dev/null +++ b/components/dropinblog/common/utils.mjs @@ -0,0 +1,43 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +function getNestedProperty(obj, propertyString) { + const properties = propertyString.split("."); + return properties.reduce((prev, curr) => prev && prev[curr], obj); +} + +export default { + parseArray, + iterate, + getNestedProperty, +}; diff --git a/components/dropinblog/dropinblog.app.mjs b/components/dropinblog/dropinblog.app.mjs new file mode 100644 index 0000000000000..6deda5e70a972 --- /dev/null +++ b/components/dropinblog/dropinblog.app.mjs @@ -0,0 +1,162 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; + +export default { + type: "app", + app: "dropinblog", + propDefinitions: { + title: { + type: "string", + label: "Title", + description: "Title for your blog post.", + }, + content: { + type: "string", + label: "Body", + description: "The body content of your blog post in HTML.", + }, + categoryId: { + type: "integer", + label: "Category ID", + description: "Category ID to attach to your post.", + optional: true, + async options() { + const { data: { categories } } = await this.listCategories(); + return categories.map(({ + id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + authorId: { + type: "string", + label: "Author ID", + description: "Author ID to attach to your post.", + optional: true, + async options() { + const { data: { authors } } = await this.listAuthors(); + return authors.map(({ + id, name: label, + }) => ({ + label, + value: String(id), + })); + }, + }, + }, + methods: { + getUrl(path) { + const baseUrl = + `${constants.BASE_URL}${constants.VERSION_PATH}` + .replace(constants.BLOG_PLACEHOLDER, this.$auth.blog_id); + return `${baseUrl}${path}`; + }, + getHeaders({ + usePrivateApiKey, ...headers + } = {}) { + const { + private_api_key: privateApiKey, + public_api_key: publicApiKey, + } = this.$auth; + + const apiKey = usePrivateApiKey + ? privateApiKey + : publicApiKey; + + return { + "Authorization": `Bearer ${apiKey}`, + "Content-Type": "application/json", + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + listCategories(args = {}) { + return this._makeRequest({ + path: "/categories", + ...args, + }); + }, + listAuthors(args = {}) { + return this._makeRequest({ + path: "/authors", + ...args, + }); + }, + listPosts(args = {}) { + return this._makeRequest({ + path: "/posts", + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, resourceName, + lastDateAt, dateField, + max = constants.DEFAULT_MAX, + }) { + let page = 1; + let resourcesCount = 0; + + while (true) { + const response = + await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + page, + limit: constants.DEFAULT_LIMIT, + }, + }); + + const nextResources = utils.getNestedProperty(response, resourceName); + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + const dateIsGreaterThan = + lastDateAt + && Date.parse(resource[dateField]) > Date.parse(lastDateAt); + + if (!lastDateAt || dateIsGreaterThan) { + yield resource; + resourcesCount += 1; + } + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + if (!response.next_page_url) { + console.log("No next page."); + return; + } + + page += 1; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); + }, + }, +}; diff --git a/components/dropinblog/package.json b/components/dropinblog/package.json new file mode 100644 index 0000000000000..c3b3b9d4ba823 --- /dev/null +++ b/components/dropinblog/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/dropinblog", + "version": "0.1.0", + "description": "Pipedream DropInBlog Components", + "main": "dropinblog.app.mjs", + "keywords": [ + "pipedream", + "dropinblog" + ], + "homepage": "https://pipedream.com/apps/dropinblog", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/dropinblog/sources/common/polling.mjs b/components/dropinblog/sources/common/polling.mjs new file mode 100644 index 0000000000000..1b728c4cdfb7a --- /dev/null +++ b/components/dropinblog/sources/common/polling.mjs @@ -0,0 +1,105 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../dropinblog.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + const { + app, + setLastDateAt, + getDateField, + getResourcesFn, + getResourcesFnArgs, + getResourceName, + processResources, + } = this; + + const dateField = getDateField(); + + const resources = await app.paginate({ + max: constants.DEFAULT_LIMIT, + resourcesFn: getResourcesFn(), + resourcesFnArgs: getResourcesFnArgs(), + resourceName: getResourceName(), + dateField, + }); + + if (resources.length) { + const [ + firstResource, + ] = Array.from(resources).reverse(); + setLastDateAt(firstResource[dateField]); + } + + processResources(resources); + }, + }, + methods: { + setLastDateAt(value) { + this.db.set(constants.LAST_DATE_AT, value); + }, + getLastDateAt() { + return this.db.get(constants.LAST_DATE_AT); + }, + getDateField() { + throw new ConfigurationError("getDateField is not implemented"); + }, + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getResourceName() { + throw new ConfigurationError("getResourceName is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + processResources(resources) { + Array.from(resources) + .forEach(this.processResource); + }, + }, + async run() { + const { + app, + getDateField, + getLastDateAt, + getResourcesFn, + getResourcesFnArgs, + getResourceName, + processResources, + } = this; + + const resources = await app.paginate({ + resourcesFn: getResourcesFn(), + resourcesFnArgs: getResourcesFnArgs(), + resourceName: getResourceName(), + dateField: getDateField(), + lastDateAt: getLastDateAt(), + }); + + processResources(resources); + }, +}; diff --git a/components/dropinblog/sources/new-post-published/new-post-published.mjs b/components/dropinblog/sources/new-post-published/new-post-published.mjs new file mode 100644 index 0000000000000..8f1e7c3d903a4 --- /dev/null +++ b/components/dropinblog/sources/new-post-published/new-post-published.mjs @@ -0,0 +1,40 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "dropinblog-new-post-published", + name: "New Post Published", + description: "Emit new event when a new post is published. [See the documentation](https://dropinblog.readme.io/reference/posts-list).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getDateField() { + return "publishedAt"; + }, + getResourceName() { + return "data.posts"; + }, + getResourcesFn() { + return this.app.listPosts; + }, + getResourcesFnArgs() { + return { + debug: true, + params: { + statuses: "published", + }, + }; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: resource.title, + ts: Date.parse(resource[this.getDateField()]), + }; + }, + }, + sampleEmit, +}; diff --git a/components/dropinblog/sources/new-post-published/test-event.mjs b/components/dropinblog/sources/new-post-published/test-event.mjs new file mode 100644 index 0000000000000..e73d4b26493fe --- /dev/null +++ b/components/dropinblog/sources/new-post-published/test-event.mjs @@ -0,0 +1,50 @@ +export default { + "id": 42349491, + "title": "Welcome to DropInBlog ~ Blogging Made Easy", + "slug": "welcome-to-dropinblog", + "url": "https://dropindemo.com/blog/welcome-to-dropinblog", + "summary": "Congrats on Finding Us – We're Happy You're Here! DropInBlog aims to be the simplest blogging solution for existing websites. Our primary goal is to help you get a stylish blog up and running on your site in minutes. But because we are geeks at heart,", + "featuredImage": "https://dropinblog.net/34236460/files/featured/welcome-dib.png", + "featuredImageWebm": null, + "featuredImageMp4": null, + "publishedAt": "03/16/2024", + "publishedAtIso8601": "2024-03-16T16:44:00-05:00", + "updatedAt": "03/16/2024", + "updatedAtIso8601": "2024-03-16T16:45:06-05:00", + "keyword": "blogging", + "secondaryKeywords": [ + "embed a blog", + "wordpress alternatives", + "blog post SEO", + "optimizing your blog", + "blog features" + ], + "seoTitle": "Welcome to DropInBlog ~ Blogging Made Easy", + "seoDescription": "Built to make embedding a blog into your website fast & easy. Be up and blogging in minutes. Feature rich & SEO friendly. Javascript, PHP or JSON integration.", + "canonicalUrl": "", + "pinned": 0, + "readtime": "3 minute read", + "seo_score": 82, + "status": "published", + "visibility": "public", + "noindex": 0, + "author": { + "id": 217, + "name": "Jason", + "slug": "jason", + "photo": "https://dropinblog.net/34236460/authors/91ec7b998af85b554be184f7a9f62b28cec93c20.jpg", + "bio": "

Jason is a co-founder of DropInBlog, and a bit of a coding geek. He just wants to make everyone’s time on the web a little easier, and has over 10 years of experience building technical solutions to tough problems. When he’s not coding out a new feature for DiB, you can find him tinkering with his own projects or reading up on the latest tech stack.

" + }, + "categories": [ + { + "id": 898924871, + "title": "Announcements", + "slug": "announcements" + }, + { + "id": 898924873, + "title": "Tips", + "slug": "tips" + } + ] +}; diff --git a/components/dropmark/README.md b/components/dropmark/README.md new file mode 100644 index 0000000000000..8ac47e8e7025b --- /dev/null +++ b/components/dropmark/README.md @@ -0,0 +1,11 @@ +# Overview + +The Dropmark API enables you to interact programmatically with the Dropmark service, allowing you to create, update, and manage collections and items within those collections. With Pipedream, you can leverage this API to automate workflows that connect Dropmark with other services, process content, and respond to events. For instance, you could automate the organization of resources, sync content across platforms, or even curate collaborative collections effortlessly. + +# Example Use Cases + +- **Automated Resource Collection**: Set up a workflow where new files uploaded to Google Drive are automatically added to a specific Dropmark collection. This helps keep resources synced between platforms and ensures your team has the latest files at their fingertips in Dropmark. + +- **Content Curation Triggered by Social Media**: Trigger a workflow whenever a new post with a specific hashtag appears on Twitter. The workflow would then add that content to a Dropmark collection, creating a curated stream of social media content that can be reviewed and shared with your team or audience. + +- **Project Management Integration**: Create a task in Trello or another project management tool whenever a new item is added to a Dropmark collection. This can help keep track of review tasks or action items that need attention, linking creative resources directly to project workflows. diff --git a/components/dropmark/actions/get-activity/get-activity.mjs b/components/dropmark/actions/get-activity/get-activity.mjs new file mode 100644 index 0000000000000..f228dc6cf9954 --- /dev/null +++ b/components/dropmark/actions/get-activity/get-activity.mjs @@ -0,0 +1,19 @@ +import dropmark from "../../dropmark.app.mjs"; + +export default { + key: "dropmark-get-activity", + name: "Get Activity", + description: "Retrieves a blended feed of newly created collections, items, comments, and reactions. [See the documentation](https://support.dropmark.com/article/96-api)", + version: "0.0.1", + type: "action", + props: { + dropmark, + }, + async run({ $ }) { + const response = await this.dropmark.getActivityFeed({ + $, + }); + $.export("$summary", "Successfully retrieved activity feed"); + return response; + }, +}; diff --git a/components/dropmark/actions/get-items-in-collection/get-items-in-collection.mjs b/components/dropmark/actions/get-items-in-collection/get-items-in-collection.mjs new file mode 100644 index 0000000000000..3ec1785a4740a --- /dev/null +++ b/components/dropmark/actions/get-items-in-collection/get-items-in-collection.mjs @@ -0,0 +1,28 @@ +import dropmark from "../../dropmark.app.mjs"; + +export default { + key: "dropmark-get-items-in-collection", + name: "Get Items in Collection", + description: "Retrieves a list of items in a specific collection. [See the documentation](https://support.dropmark.com/article/96-api)", + version: "0.0.1", + type: "action", + props: { + dropmark, + collectionId: { + propDefinition: [ + dropmark, + "collectionId", + ], + }, + }, + async run({ $ }) { + const { items } = await this.dropmark.getCollectionItems({ + $, + collectionId: this.collectionId, + }); + $.export("$summary", `Successfully retrieved ${items.length} item${items.length === 1 + ? "" + : "s"} in collection ${this.collectionId}`); + return items; + }, +}; diff --git a/components/dropmark/dropmark.app.mjs b/components/dropmark/dropmark.app.mjs new file mode 100644 index 0000000000000..fc6232b0425d4 --- /dev/null +++ b/components/dropmark/dropmark.app.mjs @@ -0,0 +1,47 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "dropmark", + propDefinitions: { + collectionId: { + type: "string", + label: "Collection ID", + description: "The ID of the collection you want to retrieve", + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.username}.dropmark.com`; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + auth: { + username: this.$auth.username, + password: this.$auth.password, + }, + }); + }, + getCollectionItems({ + collectionId, ...opts + }) { + return this._makeRequest({ + path: `/${collectionId}.json`, + ...opts, + }); + }, + getActivityFeed(opts = {}) { + return this._makeRequest({ + path: "/activity.json", + ...opts, + }); + }, + }, +}; diff --git a/components/dropmark/package.json b/components/dropmark/package.json new file mode 100644 index 0000000000000..939feb3cf7537 --- /dev/null +++ b/components/dropmark/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/dropmark", + "version": "0.1.0", + "description": "Pipedream Dropmark Components", + "main": "dropmark.app.mjs", + "keywords": [ + "pipedream", + "dropmark" + ], + "homepage": "https://pipedream.com/apps/dropmark", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/dropmark/sources/common/base.mjs b/components/dropmark/sources/common/base.mjs new file mode 100644 index 0000000000000..c644a7659ff4e --- /dev/null +++ b/components/dropmark/sources/common/base.mjs @@ -0,0 +1,58 @@ +import dropmark from "../../dropmark.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + dropmark, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + getArgs() { + return {}; + }, + getResourceType() { + return false; + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run() { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + + const resourceFn = this.getResourceFn(); + const args = this.getArgs(); + const resourceType = this.getResourceType(); + + const response = await resourceFn(args); + const items = resourceType + ? response[resourceType] + : response; + for (const item of items) { + const ts = Date.parse(item.created_at); + if (ts > lastTs) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + maxTs = Math.max(ts, maxTs); + } + } + + this._setLastTs(maxTs); + }, +}; diff --git a/components/dropmark/sources/new-activity/new-activity.mjs b/components/dropmark/sources/new-activity/new-activity.mjs new file mode 100644 index 0000000000000..4aa2151307e8c --- /dev/null +++ b/components/dropmark/sources/new-activity/new-activity.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "dropmark-new-activity", + name: "New Activity", + description: "Emit new event when a new collection, item, comment, or reaction occurs. [See the documentation](https://support.dropmark.com/article/96-api)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.dropmark.getActivityFeed; + }, + generateMeta(item) { + return { + id: item.id, + summary: `New ${item.type}: ${item.name}`, + ts: Date.parse(item.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/dropmark/sources/new-activity/test-event.mjs b/components/dropmark/sources/new-activity/test-event.mjs new file mode 100644 index 0000000000000..c4957efb6f551 --- /dev/null +++ b/components/dropmark/sources/new-activity/test-event.mjs @@ -0,0 +1,25 @@ +export default { + "name": "My New Collezione", + "description": "", + "thumbnail": null, + "type": "collection", + "created_at": "2024-03-07 22:29:37 UTC", + "updated_at": "2024-03-07 22:29:37 UTC", + "user_id": 651250, + "user_name": "Test User", + "username": "username", + "user_email": "test@user.com", + "user_avatar": "https://gravatar.com/avatar/0559374f622501dc80e6981d1cdc484.png", + "thumbnails": { + "mini": "https://webimg.dropmark.com/mini/aHR0cHM6Ly9waXBlZHJlYW0uY29t.jpg", + "small": "https://webimg.dropmark.com/small/aHR0cHM6Ly9waXBlZHJlYW0uY29t.jpg", + "cropped": "https://webimg.dropmark.com/cropped/aHR0cHM6Ly9waXBlZHJlYW0uY29t.jpg", + "uncropped": "https://webimg.dropmark.com/uncropped/aHR0cHM6Ly9waXBlZHJlYW0uY29t.jpg", + "large": "https://webimg.dropmark.com/large/aHR0cHM6Ly9waXBlZHJlYW0uY29t.jpg" + }, + "url": "https://username.dropmark.com/1658751", + "short_url": "https://dpmk.in/Cg13YQ0f4", + "id": 1658751, + "latitude": null, + "longitude": null +} \ No newline at end of file diff --git a/components/dropmark/sources/new-item-in-collection/new-item-in-collection.mjs b/components/dropmark/sources/new-item-in-collection/new-item-in-collection.mjs new file mode 100644 index 0000000000000..e284843eb8972 --- /dev/null +++ b/components/dropmark/sources/new-item-in-collection/new-item-in-collection.mjs @@ -0,0 +1,43 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + type: "source", + key: "dropmark-new-item-in-collection", + name: "New Item in Collection", + description: "Emit new event when a new item is added to a collection in Dropmark. [See the documentation](https://support.dropmark.com/article/96-api)", + version: "0.0.1", + dedupe: "unique", + props: { + ...common.props, + collectionId: { + propDefinition: [ + common.props.dropmark, + "collectionId", + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.dropmark.getCollectionItems; + }, + getArgs() { + return { + collectionId: this.collectionId, + }; + }, + getResourceType() { + return "items"; + }, + generateMeta(item) { + return { + id: item.id, + summary: `New item ${item.name}`, + ts: Date.parse(item.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/dropmark/sources/new-item-in-collection/test-event.mjs b/components/dropmark/sources/new-item-in-collection/test-event.mjs new file mode 100644 index 0000000000000..7d1a83ebfb088 --- /dev/null +++ b/components/dropmark/sources/new-item-in-collection/test-event.mjs @@ -0,0 +1,42 @@ +export default { + "collection_id": 1658751, + "user_id": 651250, + "parent_id": null, + "name": "pipedream.com", + "description": null, + "thumbnail": null, + "sort": 1, + "type": "link", + "created_at": "2024-03-07 22:29:46 UTC", + "updated_at": "2024-03-07 22:29:46 UTC", + "deleted_at": null, + "reactions_total_count": 0, + "username": "username", + "user_name": "Test User", + "user_email": "test@user.com", + "user_avatar": "https://gravatar.com/avatar/0559374f622501dc80e6981d1cdc484.png", + "user_plan": "free", + "url": "https://username.dropmark.com/1658751/33923876", + "short_url": "https://dpmk.in/i/dL9JNhdMb", + "thumbnails": { + "mini": "https://webimg.dropmark.com/mini/aHR0cHM6Ly9waXBlZHJlYW0uY29t.jpg", + "small": "https://webimg.dropmark.com/small/aHR0cHM6Ly9waXBlZHJlYW0uY29t.jpg", + "cropped": "https://webimg.dropmark.com/cropped/aHR0cHM6Ly9waXBlZHJlYW0uY29t.jpg", + "uncropped": "https://webimg.dropmark.com/uncropped/aHR0cHM6Ly9waXBlZHJlYW0uY29t.jpg", + "large": "https://webimg.dropmark.com/large/aHR0cHM6Ly9waXBlZHJlYW0uY29t.jpg" + }, + "reactions": [], + "id": 33923876, + "content": "https://pipedream.com", + "link": "https://pipedream.com", + "preview": null, + "shareable": null, + "size": 0, + "mime": "url", + "latitude": null, + "longitude": null, + "is_url": true, + "comments": [], + "tags": [], + "metadata": [] +} \ No newline at end of file diff --git a/components/droxy/README.md b/components/droxy/README.md new file mode 100644 index 0000000000000..f3eb2ea6a7b97 --- /dev/null +++ b/components/droxy/README.md @@ -0,0 +1,11 @@ +# Overview + +The Droxy API enables the construction of powerful serverless workflows on Pipedream, tapping into Droxy's capability to manage Docker containers. You can automate container lifecycle, pull statistics, and handle dynamic scaling based on real-time data. Pipedream's robust platform supports connecting to countless services which means you can integrate Droxy with other apps to facilitate CI/CD pipelines, application monitoring, or even chatbot notifications for container status updates. + +# Example Use Cases + +- **Automated Development Environment Setup**: Create a workflow that listens for GitHub `push` events. When a developer pushes to a specific branch, trigger the workflow to deploy a fresh Docker container with the latest codebase using Droxy, then run integration tests automatically. + +- **Dynamic Resource Scaling**: Set up a workflow that monitors resource usage stats from Droxy. If the usage crosses a threshold, the workflow can scale up or down the number of containers accordingly, ensuring efficient resource utilization. + +- **Notification System for Container Health**: Build a workflow that periodically checks the health of containers via Droxy API. If a container is down or unhealthy, the workflow triggers an alert and sends a notification through Slack or sends an email via SendGrid, keeping the team updated instantly. diff --git a/components/droxy/package.json b/components/droxy/package.json index 8322fde7474d0..d91fce9279c1f 100644 --- a/components/droxy/package.json +++ b/components/droxy/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/dub/README.md b/components/dub/README.md new file mode 100644 index 0000000000000..5b1cfce0756c9 --- /dev/null +++ b/components/dub/README.md @@ -0,0 +1,11 @@ +# Overview + +The Dub API lets you automate the creation and management of personalized video messages. Pipedream’s platform simplifies the process of setting up triggers and actions to interact with the Dub API. You can automate video generation, distribution, and track engagement with ease. Customize your workflow to sync with CRMs, communication platforms, or data analysis tools for a powerful, streamlined video outreach process. + +# Example Use Cases + +- **Automated Personalized Video Outreach**: Trigger video creation with Dub upon a new contact being added to a CRM like Salesforce. Then, send the personalized video link via email or SMS, and log the response in your CRM. + +- **Video Engagement Tracking**: Set up a workflow where video views trigger real-time notifications or updates. For instance, when a video is watched, send a Slack message to the sales team or record the event in a Google Sheet. + +- **Event-Based Follow-Up Campaigns**: Combine Dub with a scheduling tool like Calendly. After a meeting is scheduled, use Dub to send a personalized follow-up video. Track responses and engagement to refine your follow-up strategy. diff --git a/components/dust/actions/talk-assistant/talk-assistant.mjs b/components/dust/actions/talk-assistant/talk-assistant.mjs new file mode 100644 index 0000000000000..17ec47e2b9f01 --- /dev/null +++ b/components/dust/actions/talk-assistant/talk-assistant.mjs @@ -0,0 +1,74 @@ +import { TIMEZONE_OPTIONS } from "../../common/constants.mjs"; +import dust from "../../dust.app.mjs"; + +export default { + key: "dust-talk-assistant", + name: "Talk to Assistant", + description: "Send a message to an assistant on Dust and receive an answer. [See the documentation](https://docs.dust.tt/reference/post_api-v1-w-wid-assistant-conversations-cid-messages)", + version: "0.0.1", + type: "action", + props: { + dust, + assistantId: { + propDefinition: [ + dust, + "assistantId", + ], + }, + content: { + type: "string", + label: "Message Content", + description: "The content of the message to be sent to the assistant", + }, + timezone: { + type: "string", + label: "Timezone", + description: "Set the timezone in which you want to operate.", + options: TIMEZONE_OPTIONS, + }, + username: { + type: "string", + label: "Username", + description: "The name to be displayed in the conversation.", + }, + email: { + type: "string", + label: "Email", + description: "Put an email if needed.", + }, + }, + async run({ $ }) { + const { + conversation, message, + } = await this.dust.sendMessageToAssistant({ + $, + data: { + message: { + content: this.content, + context: { + timezone: this.timezone, + username: this.username, + fullName: null, + email: this.email, + profilePictureUrl: null, + }, + mentions: [ + { + configurationId: this.assistantId, + }, + ], + }, + blocking: true, + visibility: "unlisted", + title: null, + }, + }); + + $.export("$summary", "Successfully sent message to assistant"); + return { + agentMessage: conversation.content[1][0].content, + conversationUrl: `https://dust.tt/w/${conversation.owner.sId}/assistant/${conversation.sId}`, + message, + }; + }, +}; diff --git a/components/dust/actions/upsert-document/upsert-document.mjs b/components/dust/actions/upsert-document/upsert-document.mjs new file mode 100644 index 0000000000000..1ee85d8bf46ce --- /dev/null +++ b/components/dust/actions/upsert-document/upsert-document.mjs @@ -0,0 +1,48 @@ +import dust from "../../dust.app.mjs"; + +export default { + key: "dust-upsert-document", + name: "Upsert Document", + description: "Upsert a document to a chosen Dust data source. [See the documentation](https://docs.dust.tt/reference/post_api-v1-w-wid-data-sources-name-documents-documentid)", + version: "0.0.1", + type: "action", + props: { + dust, + dataSourceId: { + propDefinition: [ + dust, + "dataSourceId", + ], + }, + documentId: { + type: "string", + label: "Document Id", + description: "An Id for the new document", + }, + content: { + type: "string", + label: "Content", + description: "The text content of the document to upsert.", + }, + lightDocumentOutput: { + type: "boolean", + label: "Light Document Output", + description: "If true, a lightweight version of the document will be returned in the response (excluding the text, chunks and vectors). Defaults to false.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.dust.upsertDocument({ + $, + dataSourceId: this.dataSourceId, + documentId: this.documentId, + data: { + text: this.content, + light_document_output: this.lightDocumentOutput, + }, + }); + + $.export("$summary", "Successfully uploaded document"); + return response; + }, +}; diff --git a/components/dust/common/constants.mjs b/components/dust/common/constants.mjs new file mode 100644 index 0000000000000..7e1205e5046a7 --- /dev/null +++ b/components/dust/common/constants.mjs @@ -0,0 +1,599 @@ +export const TIMEZONE_OPTIONS = [ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmara", + "Africa/Asmera", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Timbuktu", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/ComodRivadavia", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Atikokan", + "America/Atka", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Buenos_Aires", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Catamarca", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Ciudad_Juarez", + "America/Coral_Harbour", + "America/Cordoba", + "America/Costa_Rica", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Ensenada", + "America/Fort_Nelson", + "America/Fort_Wayne", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indianapolis", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Jujuy", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Knox_IN", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Louisville", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Mendoza", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montreal", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Nuuk", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Acre", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Rosario", + "America/Santa_Isabel", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Shiprock", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Virgin", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/South_Pole", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Ashkhabad", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Calcutta", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Chongqing", + "Asia/Chungking", + "Asia/Colombo", + "Asia/Dacca", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Harbin", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Istanbul", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kashgar", + "Asia/Kathmandu", + "Asia/Katmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macao", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qostanay", + "Asia/Qyzylorda", + "Asia/Rangoon", + "Asia/Riyadh", + "Asia/Saigon", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Tel_Aviv", + "Asia/Thimbu", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ujung_Pandang", + "Asia/Ulaanbaatar", + "Asia/Ulan_Bator", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faeroe", + "Atlantic/Faroe", + "Atlantic/Jan_Mayen", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/ACT", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Canberra", + "Australia/Currie", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/LHI", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/North", + "Australia/NSW", + "Australia/Perth", + "Australia/Queensland", + "Australia/South", + "Australia/Sydney", + "Australia/Tasmania", + "Australia/Victoria", + "Australia/West", + "Australia/Yancowinna", + "Brazil/Acre", + "Brazil/DeNoronha", + "Brazil/East", + "Brazil/West", + "Canada/Atlantic", + "Canada/Central", + "Canada/Eastern", + "Canada/Mountain", + "Canada/Newfoundland", + "Canada/Pacific", + "Canada/Saskatchewan", + "Canada/Yukon", + "CET", + "Chile/Continental", + "Chile/EasterIsland", + "CST6CDT", + "Cuba", + "EET", + "Egypt", + "Eire", + "EST", + "EST5EDT", + "Etc/GMT", + "Etc/GMT+0", + "Etc/GMT+1", + "Etc/GMT+10", + "Etc/GMT+11", + "Etc/GMT+12", + "Etc/GMT+2", + "Etc/GMT+3", + "Etc/GMT+4", + "Etc/GMT+5", + "Etc/GMT+6", + "Etc/GMT+7", + "Etc/GMT+8", + "Etc/GMT+9", + "Etc/GMT-0", + "Etc/GMT-1", + "Etc/GMT-10", + "Etc/GMT-11", + "Etc/GMT-12", + "Etc/GMT-13", + "Etc/GMT-14", + "Etc/GMT-2", + "Etc/GMT-3", + "Etc/GMT-4", + "Etc/GMT-5", + "Etc/GMT-6", + "Etc/GMT-7", + "Etc/GMT-8", + "Etc/GMT-9", + "Etc/GMT0", + "Etc/Greenwich", + "Etc/UCT", + "Etc/Universal", + "Etc/UTC", + "Etc/Zulu", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belfast", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Kyiv", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Nicosia", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Tiraspol", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zaporozhye", + "Europe/Zurich", + "Factory", + "GB", + "GB-Eire", + "GMT", + "GMT+0", + "GMT-0", + "GMT0", + "Greenwich", + "Hongkong", + "HST", + "Iceland", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Iran", + "Israel", + "Jamaica", + "Japan", + "Kwajalein", + "Libya", + "MET", + "Mexico/BajaNorte", + "Mexico/BajaSur", + "Mexico/General", + "MST", + "MST7MDT", + "Navajo", + "NZ", + "NZ-CHAT", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Johnston", + "Pacific/Kanton", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Ponape", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Samoa", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Truk", + "Pacific/Wake", + "Pacific/Wallis", + "Pacific/Yap", + "Poland", + "Portugal", + "PRC", + "PST8PDT", + "ROC", + "ROK", + "Singapore", + "Turkey", + "UCT", + "Universal", + "US/Alaska", + "US/Aleutian", + "US/Arizona", + "US/Central", + "US/East-Indiana", + "US/Eastern", + "US/Hawaii", + "US/Indiana-Starke", + "US/Michigan", + "US/Mountain", + "US/Pacific", + "US/Samoa", + "UTC", + "W-SU", + "WET", + "Zulu", +]; diff --git a/components/dust/dust.app.mjs b/components/dust/dust.app.mjs new file mode 100644 index 0000000000000..2a8dc14cf7b50 --- /dev/null +++ b/components/dust/dust.app.mjs @@ -0,0 +1,85 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "dust", + propDefinitions: { + assistantId: { + type: "string", + label: "Assistant ID", + description: "Select the name of your Assistant from the list. Your Dust Assistant must be a **Shared** or **Company** Assistant. Personal Assistants are not visible in this list.", + async options() { + const { agentConfigurations } = await this.listAssistants(); + + return agentConfigurations.map(({ + description, name, sId: value, + }) => ({ + label: `${name} - ${description}`, + value, + })); + }, + }, + dataSourceId: { + type: "string", + label: "Folder ID", + description: "ID of the data source.", + async options() { + const { data_sources: ds } = await this.listDataSources(); + + return ds.map(({ + description, name, dustAPIDataSourceId: value, + }) => ({ + label: `${name} - ${description}`, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return `https://dust.tt/api/v1/w/${this.$auth.workspace_id}`; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listAssistants(opts = {}) { + return this._makeRequest({ + path: "/assistant/agent_configurations", + ...opts, + }); + }, + listDataSources(opts = {}) { + return this._makeRequest({ + path: "/data_sources", + ...opts, + }); + }, + sendMessageToAssistant(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/assistant/conversations", + ...opts, + }); + }, + upsertDocument({ + documentId, dataSourceId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/data_sources/${dataSourceId}/documents/${documentId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/dust/package.json b/components/dust/package.json new file mode 100644 index 0000000000000..de2d79e946f0d --- /dev/null +++ b/components/dust/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/dust", + "version": "0.1.0", + "description": "Pipedream Dust Components", + "main": "dust.app.mjs", + "keywords": [ + "pipedream", + "dust" + ], + "homepage": "https://pipedream.com/apps/dust", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} diff --git a/components/dux_soup/README.md b/components/dux_soup/README.md new file mode 100644 index 0000000000000..503552431b8a0 --- /dev/null +++ b/components/dux_soup/README.md @@ -0,0 +1,11 @@ +# Overview + +The Dux Soup API enables users to automate interactions and manage leads on LinkedIn. Within Pipedream, you can harness this API to craft workflows that engage with your LinkedIn network, manage connections, and streamline your lead generation process. Automating tasks like sending connection requests, following up with contacts, and tracking profile visits can save valuable time and boost your LinkedIn marketing efforts. + +# Example Use Cases + +- **LinkedIn Lead Generation to CRM Integration**: Connect Dux Soup to your CRM platform on Pipedream. When a new lead is identified on LinkedIn, Dux Soup can automate the initial connection request, and upon acceptance, the contact's details are sent to your CRM for follow-up. + +- **LinkedIn Profile Automation and Notifications**: Use Dux Soup alongside Pipedream's built-in scheduling to regularly visit profiles of potential leads. Combine it with a notification service like Slack or email to get updates when significant engagement occurs, such as when a lead views your profile back, enabling timely follow-ups. + +- **Automated LinkedIn Outreach Sequences**: Create a sequence of messages for new LinkedIn connections using Dux Soup. Pipedream can trigger the sequence once a connection is made, sending personalized messages spread over a set period. This keeps engagement levels high and can lead to more productive networking opportunities. diff --git a/components/dux_soup/actions/connect-profile/connect-profile.mjs b/components/dux_soup/actions/connect-profile/connect-profile.mjs new file mode 100644 index 0000000000000..b8da9b1bd693b --- /dev/null +++ b/components/dux_soup/actions/connect-profile/connect-profile.mjs @@ -0,0 +1,31 @@ +import duxSoup from "../../dux_soup.app.mjs"; + +export default { + key: "dux_soup-connect-profile", + name: "Connect Profile", + description: "Queues a connection request to actively connect with a targeted LinkedIn profile. [See the documentation](https://support.dux-soup.com/article/115-remote-control-by-example)", + version: "0.0.1", + type: "action", + props: { + duxSoup, + targetProfileUrl: { + propDefinition: [ + duxSoup, + "targetProfileUrl", + ], + }, + }, + async run({ $ }) { + const response = await this.duxSoup.makeRequest({ + $, + requestBody: { + command: "connect", + params: { + profile: this.targetProfileURL, + }, + }, + }); + $.export("$summary", `Successfully queued connection request to profile ID: ${this.targetProfileUrl}`); + return response; + }, +}; diff --git a/components/dux_soup/actions/message-profile/message-profile.mjs b/components/dux_soup/actions/message-profile/message-profile.mjs new file mode 100644 index 0000000000000..6bffb59194838 --- /dev/null +++ b/components/dux_soup/actions/message-profile/message-profile.mjs @@ -0,0 +1,38 @@ +import duxSoup from "../../dux_soup.app.mjs"; + +export default { + key: "dux_soup-message-profile", + name: "Message Profile", + description: "Queues a direct message that will be sent to the targeted profile. [See the documentation](https://support.dux-soup.com/article/115-remote-control-by-example)", + version: "0.0.1", + type: "action", + props: { + duxSoup, + targetProfileUrl: { + propDefinition: [ + duxSoup, + "targetProfileUrl", + ], + }, + message: { + propDefinition: [ + duxSoup, + "message", + ], + }, + }, + async run({ $ }) { + const response = await this.duxSoup.makeRequest({ + $, + requestBody: { + command: "message", + params: { + profile: this.targetProfileURL, + messagetext: this.message, + }, + }, + }); + $.export("$summary", `Message queued to profile ${this.targetProfileUrl}`); + return response; + }, +}; diff --git a/components/dux_soup/actions/save-profile-lead/save-profile-lead.mjs b/components/dux_soup/actions/save-profile-lead/save-profile-lead.mjs new file mode 100644 index 0000000000000..d51173f15465b --- /dev/null +++ b/components/dux_soup/actions/save-profile-lead/save-profile-lead.mjs @@ -0,0 +1,31 @@ +import duxSoup from "../../dux_soup.app.mjs"; + +export default { + key: "dux_soup-save-profile-lead", + name: "Save Profile as Lead", + description: "Queues a profile save action to store the targeted profile as a lead. [See the documentation](https://support.dux-soup.com/article/115-remote-control-by-example)", + version: "0.0.1", + type: "action", + props: { + duxSoup, + targetProfileUrl: { + propDefinition: [ + duxSoup, + "targetProfileUrl", + ], + }, + }, + async run({ $ }) { + const response = await this.duxSoup.makeRequest({ + $, + requestBody: { + command: "saveaslead", + params: { + profile: this.targetProfileURL, + }, + }, + }); + $.export("$summary", `Successfully saved profile ${this.targetProfileUrl} as a lead`); + return response; + }, +}; diff --git a/components/dux_soup/dux_soup.app.mjs b/components/dux_soup/dux_soup.app.mjs new file mode 100644 index 0000000000000..5848dc4f8aa2d --- /dev/null +++ b/components/dux_soup/dux_soup.app.mjs @@ -0,0 +1,52 @@ +import { axios } from "@pipedream/platform"; +import jsSHA from "jssha"; + +export default { + type: "app", + app: "dux_soup", + propDefinitions: { + targetProfileUrl: { + type: "string", + label: "Target Profile URL", + description: "The URL of the LinkedIn profile you want to target", + }, + message: { + type: "string", + label: "Message", + description: "The message to send to the targeted LinkedIn profile", + }, + }, + methods: { + _getBaseUrl() { + return `${this.$auth.target_url}/queue`; + }, + _getCommandRequestBody(requestBody) { + return JSON.stringify({ + ...requestBody, + targeturl: `${this.$auth.target_url}/queue`, + userid: `${this.$auth.user_id}`, + timestamp: +new Date(), + }); + }, + _getSignature(commandRequestBody) { + let shaObj = new jsSHA("SHA-1", "TEXT"); + shaObj.setHMACKey(`${this.$auth.auth_key}`, "TEXT"); + shaObj.update(commandRequestBody); + return shaObj.getHMAC("B64"); + }, + makeRequest({ + $ = this, requestBody, + }) { + const commandRequestBody = this._getCommandRequestBody(requestBody); + return axios($, { + url: this._getBaseUrl(), + method: "POST", + headers: { + "X-Dux-Signature": this._getSignature(commandRequestBody), + "Content-Type": "application/json", + }, + data: commandRequestBody, + }); + }, + }, +}; diff --git a/components/dux_soup/package.json b/components/dux_soup/package.json new file mode 100644 index 0000000000000..68770ffc29984 --- /dev/null +++ b/components/dux_soup/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/dux_soup", + "version": "0.1.0", + "description": "Pipedream Dux Soup Components", + "main": "dux_soup.app.mjs", + "keywords": [ + "pipedream", + "dux_soup" + ], + "homepage": "https://pipedream.com/apps/dux_soup", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0", + "jssha": "^3.3.1" + } +} diff --git a/components/dynalist/README.md b/components/dynalist/README.md new file mode 100644 index 0000000000000..1535607954877 --- /dev/null +++ b/components/dynalist/README.md @@ -0,0 +1,11 @@ +# Overview + +The Dynalist API enables programmatic access to the powerful outlining and organizing features of Dynalist, a tool designed for structured note-taking and dynamic lists. With Pipedream, you can automate tasks, sync data, and connect Dynalist to other apps and services to enhance productivity. This could include creating new documents, updating lists, and extracting items for reporting or integration with project management tools. + +# Example Use Cases + +- **Integrate Dynalist with Calendar Apps**: When you create a new item in Dynalist marked with a specific keyword or hashtag, Pipedream can trigger a workflow that automatically adds a corresponding event to your Google Calendar or other calendar services, ensuring your tasks and deadlines are always synchronized. + +- **Sync Tasks Between Dynalist and Trello**: You can set up a Pipedream workflow that watches for new checked-off items in Dynalist. Whenever you complete a task in Dynalist, the workflow creates a new card in Trello or updates an existing one, allowing you to maintain an overview of your project's progress across different platforms. + +- **Automated Reporting for Completed Items**: Build a Pipedream workflow that periodically retrieves a list of checked items from Dynalist, formats them into a report, and sends this report via email or posts it to a Slack channel. This helps keep teams informed about completed tasks without manual intervention. diff --git a/components/dynalist/actions/edit-document/edit-document.mjs b/components/dynalist/actions/edit-document/edit-document.mjs new file mode 100644 index 0000000000000..7043b731ad5ee --- /dev/null +++ b/components/dynalist/actions/edit-document/edit-document.mjs @@ -0,0 +1,46 @@ +import { ConfigurationError } from "@pipedream/platform"; +import dynalist from "../../dynalist.app.mjs"; + +export default { + key: "dynalist-edit-document", + name: "Edit Document Title", + description: "Edits the title of a specific document in Dynalist. [See the documentation](https://apidocs.dynalist.io/)", + version: "0.0.1", + type: "action", + props: { + dynalist, + documentId: { + propDefinition: [ + dynalist, + "documentId", + ], + }, + newTitle: { + type: "string", + label: "New Title", + description: "The new title for the document or content", + }, + }, + async run({ $ }) { + const response = await this.dynalist.editDocumentTitle({ + $, + data: { + changes: [ + { + action: "edit", + type: "document", + file_id: this.documentId, + title: this.newTitle, + }, + ], + }, + }); + + if (response._code != "Ok") { + throw new ConfigurationError(response._msg); + } + + $.export("$summary", `Successfully updated the document title to "${this.newTitle}"`); + return response; + }, +}; diff --git a/components/dynalist/actions/get-document-content/get-document-content.mjs b/components/dynalist/actions/get-document-content/get-document-content.mjs new file mode 100644 index 0000000000000..462252c6a5257 --- /dev/null +++ b/components/dynalist/actions/get-document-content/get-document-content.mjs @@ -0,0 +1,34 @@ +import { ConfigurationError } from "@pipedream/platform"; +import dynalist from "../../dynalist.app.mjs"; + +export default { + key: "dynalist-get-document-content", + name: "Get Document Content", + description: "Fetches the content of a specific document. [See the documentation](https://apidocs.dynalist.io/#get-content-of-a-document)", + version: "0.0.1", + type: "action", + props: { + dynalist, + documentId: { + propDefinition: [ + dynalist, + "documentId", + ], + }, + }, + async run({ $ }) { + const response = await this.dynalist.fetchDocumentContent({ + $, + data: { + file_id: this.documentId, + }, + }); + + if (response._code != "Ok") { + throw new ConfigurationError(response._msg); + } + + $.export("$summary", `Successfully fetched content for document ID ${this.documentId}`); + return response; + }, +}; diff --git a/components/dynalist/actions/insert-document-content/insert-document-content.mjs b/components/dynalist/actions/insert-document-content/insert-document-content.mjs new file mode 100644 index 0000000000000..af6fb90b191f9 --- /dev/null +++ b/components/dynalist/actions/insert-document-content/insert-document-content.mjs @@ -0,0 +1,100 @@ +import { ConfigurationError } from "@pipedream/platform"; +import dynalist from "../../dynalist.app.mjs"; + +export default { + key: "dynalist-insert-document-content", + name: "Insert Document Content", + description: "Inserts content to a specific document. If the document has existing content, the new content will be appended. [See the documentation](https://apidocs.dynalist.io/)", + version: "0.0.1", + type: "action", + props: { + dynalist, + documentId: { + propDefinition: [ + dynalist, + "documentId", + ], + }, + parentId: { + propDefinition: [ + dynalist, + "parentId", + ({ documentId }) => ({ + documentId, + }), + ], + }, + index: { + type: "integer", + label: "Index", + description: "This field is an integer that's the zero-indexed position you want the file to land in the parent folder, supply -1 to place it at the end, and 0 to place it at the top.", + }, + content: { + type: "string", + label: "Content", + description: "The new item title.", + }, + note: { + type: "string", + label: "Note", + description: "The new item note.", + optional: true, + }, + checked: { + type: "boolean", + label: "Checked", + description: "The new item checked status.", + optional: true, + }, + checkbox: { + type: "boolean", + label: "Checkbox", + description: "Whether the new item should be a checklist.", + optional: true, + }, + heading: { + type: "integer", + label: "Heading", + description: "What heading level is new item, from level 1 to level 3. Default is 0", + min: 1, + max: 3, + optional: true, + }, + color: { + type: "integer", + label: "Color", + description: "What color label does the new item have, from color 1 to 6. Default is 0.", + min: 1, + max: 6, + optional: true, + }, + }, + async run({ $ }) { + const response = await this.dynalist.insertContentToDocument({ + $, + data: { + file_id: this.documentId, + changes: [ + { + parent_id: this.parentId, + action: "insert", + index: this.index, + content: this.content, + note: this.note, + checked: this.checked, + checkbox: this.checkbox, + heading: this.heading, + color: this.color, + }, + ], + }, + }); + + if (response._code != "Ok") { + throw new ConfigurationError(response._msg); + } + + $.export("$summary", `Successfully inserted content to document ${this.documentId}`); + return response; + }, +}; diff --git a/components/dynalist/common/utils.mjs b/components/dynalist/common/utils.mjs new file mode 100644 index 0000000000000..e532158741ff6 --- /dev/null +++ b/components/dynalist/common/utils.mjs @@ -0,0 +1,5 @@ +export const truncate = (str, n = 30) => { + return (str.length > n) + ? str.slice(0, n - 1) + "..." + : str; +}; diff --git a/components/dynalist/dynalist.app.mjs b/components/dynalist/dynalist.app.mjs new file mode 100644 index 0000000000000..b738b3e571a1e --- /dev/null +++ b/components/dynalist/dynalist.app.mjs @@ -0,0 +1,93 @@ +import { axios } from "@pipedream/platform"; +import { truncate } from "./common/utils.mjs"; + +export default { + type: "app", + app: "dynalist", + propDefinitions: { + documentId: { + type: "string", + label: "Document ID", + description: "The ID of the document.", + async options() { + const { files } = await this.listDocuments(); + + return files + .filter((file) => file.type === "document") + .map(({ + id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + parentId: { + type: "string", + label: "Parent ID", + description: "The ID of the parent item to insert under.", + async options({ documentId }) { + const { nodes } = await this.fetchDocumentContent({ + data: { + file_id: documentId, + }, + }); + + return nodes.map(({ + id: value, content, + }) => ({ + label: truncate(content), + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://dynalist.io/api/v1"; + }, + _data(data = {}) { + return { + "token": `${this.$auth.api_token}`, + ...data, + }; + }, + _makeRequest({ + $ = this, path, data, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + data: this._data(data), + ...opts, + }); + }, + editDocumentTitle(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/file/edit", + ...opts, + }); + }, + fetchDocumentContent(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/doc/read", + ...opts, + }); + }, + listDocuments(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/file/list", + ...opts, + }); + }, + insertContentToDocument(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/doc/edit", + ...opts, + }); + }, + }, +}; diff --git a/components/dynalist/package.json b/components/dynalist/package.json new file mode 100644 index 0000000000000..5e843474e56f2 --- /dev/null +++ b/components/dynalist/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/dynalist", + "version": "0.1.0", + "description": "Pipedream Dynalist Components", + "main": "dynalist.app.mjs", + "keywords": [ + "pipedream", + "dynalist" + ], + "homepage": "https://pipedream.com/apps/dynalist", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" + } +} diff --git a/components/dynamics_365_business_central_api/README.md b/components/dynamics_365_business_central_api/README.md index ddc33c0cdcd52..18c8f2667b5c5 100644 --- a/components/dynamics_365_business_central_api/README.md +++ b/components/dynamics_365_business_central_api/README.md @@ -1,15 +1,11 @@ # Overview -Microsoft Dynamics 365 Business Central is a powerful tool that can help -businesses automate their processes and improve their bottom line. The API -allows businesses to connect to their Business Central instance and access -their data. With the API, businesses can perform actions such as: - -- Create and update records -- Send and receive data -- Add and remove users -- Manage user permissions -- Access reports and analytics - -The possibilities are endless! Businesses can use the API to build custom -solutions that fit their specific needs. +The Microsoft Dynamics 365 Business Central API enables you to streamline business processes by automating tasks and integrating with other services. With this API, you can access Dynamics 365 Business Central data such as financials, sales, service, and operations. It's perfect for creating custom workflows in Pipedream that can manipulate data, synchronize information across platforms, generate reports, and notify team members of important events. + +# Example Use Cases + +- **Automated Invoice Processing**: When a new sale is recorded in another app like Shopify, use Pipedream to automatically create an invoice in Dynamics 365 Business Central, record the transaction, and send a confirmation email to the customer via SendGrid. + +- **Inventory Management**: Set up a workflow on Pipedream that monitors stock levels in Dynamics 365 Business Central. When inventory for a particular item falls below a threshold, generate a purchase order, and send it to the supplier using an email service like Mailgun, or directly post it to a supplier's portal via webhooks. + +- **Customer Relationship Updates**: Whenever a customer's details are updated in a CRM like Salesforce, use Pipedream to reflect those changes in Dynamics 365 Business Central. Additionally, segment customers based on their purchase history or interactions and enrich their profiles for targeted marketing campaigns. diff --git a/components/dynamics_365_business_central_api/actions/create-customer/create-customer.mjs b/components/dynamics_365_business_central_api/actions/create-customer/create-customer.mjs new file mode 100644 index 0000000000000..bcd6c1fdc0fa7 --- /dev/null +++ b/components/dynamics_365_business_central_api/actions/create-customer/create-customer.mjs @@ -0,0 +1,86 @@ +import dynamics from "../../dynamics_365_business_central_api.app.mjs"; + +export default { + key: "dynamics_365_business_central_api-create-customer", + name: "Create Customer", + description: "Creates a new customer. [See the documentation](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/api/dynamics_customer_create)", + version: "0.0.1", + type: "action", + props: { + dynamics, + companyId: { + propDefinition: [ + dynamics, + "companyId", + ], + }, + name: { + propDefinition: [ + dynamics, + "name", + ], + }, + email: { + propDefinition: [ + dynamics, + "email", + ], + }, + phone: { + propDefinition: [ + dynamics, + "phone", + ], + }, + address: { + propDefinition: [ + dynamics, + "address", + ], + }, + city: { + propDefinition: [ + dynamics, + "city", + ], + }, + state: { + propDefinition: [ + dynamics, + "state", + ], + }, + postalCode: { + propDefinition: [ + dynamics, + "postalCode", + ], + }, + country: { + propDefinition: [ + dynamics, + "country", + ], + }, + }, + async run({ $ }) { + const response = await this.dynamics.createCustomer({ + $, + companyId: this.companyId, + data: { + displayName: this.name, + phoneNumber: this.phone, + email: this.email, + address: { + street: this.address, + city: this.city, + state: this.state, + countryLetterCode: this.coutry, + postalCode: this.postalCode, + }, + }, + }); + $.export("$summary", `Successfully created customer with ID ${response.id}`); + return response; + }, +}; diff --git a/components/dynamics_365_business_central_api/actions/get-sales-order/get-sales-order.mjs b/components/dynamics_365_business_central_api/actions/get-sales-order/get-sales-order.mjs new file mode 100644 index 0000000000000..55339ccbc7618 --- /dev/null +++ b/components/dynamics_365_business_central_api/actions/get-sales-order/get-sales-order.mjs @@ -0,0 +1,36 @@ +import dynamics from "../../dynamics_365_business_central_api.app.mjs"; + +export default { + key: "dynamics_365_business_central_api-get-sales-order", + name: "Get Sales Order", + description: "Retrieves a sales order by ID. [See the documentation](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/api/dynamics_salesorder_get)", + version: "0.0.1", + type: "action", + props: { + dynamics, + companyId: { + propDefinition: [ + dynamics, + "companyId", + ], + }, + salesOrderId: { + propDefinition: [ + dynamics, + "salesOrderId", + (c) => ({ + companyId: c.companyId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.dynamics.getSalesOrder({ + $, + companyId: this.companyId, + salesOrderId: this.salesOrderId, + }); + $.export("$summary", `Successfully retrieved sales order with ID ${this.salesOrderId}`); + return response; + }, +}; diff --git a/components/dynamics_365_business_central_api/actions/update-customer/update-customer.mjs b/components/dynamics_365_business_central_api/actions/update-customer/update-customer.mjs new file mode 100644 index 0000000000000..1ba83dd68b05e --- /dev/null +++ b/components/dynamics_365_business_central_api/actions/update-customer/update-customer.mjs @@ -0,0 +1,115 @@ +import dynamics from "../../dynamics_365_business_central_api.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "dynamics_365_business_central_api-update-customer", + name: "Update Customer", + description: "Updates an existing customer. [See the documentation](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/api/dynamics_customer_update)", + version: "0.0.1", + type: "action", + props: { + dynamics, + companyId: { + propDefinition: [ + dynamics, + "companyId", + ], + }, + customerId: { + propDefinition: [ + dynamics, + "customerId", + (c) => ({ + companyId: c.companyId, + }), + ], + }, + name: { + propDefinition: [ + dynamics, + "name", + ], + optional: true, + }, + email: { + propDefinition: [ + dynamics, + "email", + ], + }, + phone: { + propDefinition: [ + dynamics, + "phone", + ], + }, + address: { + propDefinition: [ + dynamics, + "address", + ], + }, + city: { + propDefinition: [ + dynamics, + "city", + ], + }, + state: { + propDefinition: [ + dynamics, + "state", + ], + }, + postalCode: { + propDefinition: [ + dynamics, + "postalCode", + ], + }, + country: { + propDefinition: [ + dynamics, + "country", + ], + }, + }, + methods: { + async buildAddress($) { + const { address } = await this.dynamics.getCustomer({ + companyId: this.companyId, + customerId: this.customerId, + $, + }); + return { + street: this.address || address.street, + city: this.city || address.city, + state: this.state || address.state, + countryLetterCode: this.country || address.countryLetterCode, + postalCode: this.postalCode || address.postalCode, + }; + }, + }, + async run({ $ }) { + const hasAddress = this.address + || this.city + || this.state + || this.countryLetterCode + || this.postalCode; + const response = await this.dynamics.updateCustomer({ + $, + companyId: this.companyId, + customerId: this.customerId, + data: utils.cleanObject({ + displayName: this.name, + phoneNumber: this.phone, + email: this.email, + address: hasAddress + ? await this.buildAddress($) + : undefined, + }), + }); + $.export("$summary", `Successfully updated customer with ID ${this.customerId}`); + return response; + }, +}; diff --git a/components/dynamics_365_business_central_api/common/utils.mjs b/components/dynamics_365_business_central_api/common/utils.mjs new file mode 100644 index 0000000000000..6cb98d98150b1 --- /dev/null +++ b/components/dynamics_365_business_central_api/common/utils.mjs @@ -0,0 +1,10 @@ +export default { + cleanObject(o) { + for (var k in o || {}) { + if (typeof o[k] === "undefined") { + delete o[k]; + } + } + return o; + }, +}; diff --git a/components/dynamics_365_business_central_api/dynamics_365_business_central_api.app.mjs b/components/dynamics_365_business_central_api/dynamics_365_business_central_api.app.mjs index c890c05d1165d..daf06dc2c250f 100644 --- a/components/dynamics_365_business_central_api/dynamics_365_business_central_api.app.mjs +++ b/components/dynamics_365_business_central_api/dynamics_365_business_central_api.app.mjs @@ -1,11 +1,184 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "dynamics_365_business_central_api", - propDefinitions: {}, + propDefinitions: { + companyId: { + type: "string", + label: "Company ID", + description: "The identifier of a company", + async options() { + const { value } = await this.listCompanies(); + return value?.map(({ + id: value, name: label, + }) => ({ + label, + value, + })) || []; + }, + }, + customerId: { + type: "string", + label: "Customer ID", + description: "The identifier of a customer", + async options({ companyId }) { + const { value } = await this.listCustomers({ + companyId, + }); + return value?.map(({ + id: value, displayName: label, + }) => ({ + label, + value, + })) || []; + }, + }, + salesOrderId: { + type: "string", + label: "Sales Order ID", + description: "The identifier of a sales order", + async options({ companyId }) { + const { value } = await this.listSalesOrders({ + companyId, + }); + return value?.map(({ + id: value, number, customerName, orderDate, + }) => ({ + value, + label: `${number} - ${orderDate} - ${customerName}`, + })) || []; + }, + }, + name: { + type: "string", + label: "Name", + description: "The name of the customer", + }, + email: { + type: "string", + label: "Email", + description: "Email address of the customer", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Phone number of the customer", + optional: true, + }, + address: { + type: "string", + label: "Street Address", + description: "Street address of the customer", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "City of the customer", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "State of the customer", + optional: true, + }, + postalCode: { + type: "string", + label: "Postal Code", + description: "Postal code of the customer", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "Country of the customer", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://graph.microsoft.com/beta/financials"; + }, + _headers(headers) { + return { + ...headers, + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "Content-type": "application/json", + }; + }, + _makeRequest({ + $ = this, + path, + headers, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._headers(headers), + ...opts, + }); + }, + getSalesOrder({ + companyId, salesOrderId, ...opts + }) { + return this._makeRequest({ + path: `/companies(${companyId})/salesOrders(${salesOrderId})`, + ...opts, + }); + }, + getCustomer({ + companyId, customerId, ...opts + }) { + return this._makeRequest({ + path: `/companies(${companyId})/customers(${customerId})`, + ...opts, + }); + }, + listSalesOrders({ + companyId, ...opts + }) { + return this._makeRequest({ + path: `/companies(${companyId})/salesOrders`, + ...opts, + }); + }, + listCompanies(opts = {}) { + return this._makeRequest({ + path: "/companies", + ...opts, + }); + }, + listCustomers({ + companyId, ...opts + }) { + return this._makeRequest({ + path: `/companies(${companyId})/customers`, + ...opts, + }); + }, + createCustomer({ + companyId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/companies(${companyId})/customers`, + ...opts, + }); + }, + updateCustomer({ + companyId, customerId, ...opts + }) { + return this._makeRequest({ + method: "PATCH", + path: `/companies(${companyId})/customers(${customerId})`, + headers: { + "If-Match": "*", + }, + ...opts, + }); }, }, }; diff --git a/components/dynamics_365_business_central_api/package.json b/components/dynamics_365_business_central_api/package.json new file mode 100644 index 0000000000000..99470eef59fe6 --- /dev/null +++ b/components/dynamics_365_business_central_api/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/dynamics_365_business_central_api", + "version": "0.0.1", + "description": "Pipedream Dynamics 365 Business Central API Components", + "main": "dynamics_365_business_central_api.app.mjs", + "keywords": [ + "pipedream", + "dynamics_365_business_central_api" + ], + "homepage": "https://pipedream.com/apps/dropcontact", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/dynapictures/README.md b/components/dynapictures/README.md new file mode 100644 index 0000000000000..d7c993c319417 --- /dev/null +++ b/components/dynapictures/README.md @@ -0,0 +1,11 @@ +# Overview + +DynaPictures API allows users to automate image generation with dynamic content. It's useful for creating personalized images for marketing campaigns, social media, or e-commerce products. In Pipedream, you can harness DynaPictures API to build serverless workflows that trigger based on various events, manipulate and generate images, and connect with other services for a seamless automation pipeline. + +# Example Use Cases + +- **Personalized Social Media Posts**: Automate the creation of personalized social media images by integrating DynaPictures with Twitter. When a user fills out a form on your website, it triggers a Pipedream workflow that sends data to DynaPictures to generate an image, which is then posted to your Twitter account with a custom message. + +- **E-commerce Product Images**: Create dynamic product images on-the-fly by linking DynaPictures with Shopify. Whenever a new product is added to your Shopify store, Pipedream can trigger a workflow that calls DynaPictures to generate unique images based on product attributes, which are then uploaded back to the product listing. + +- **Event-Driven Marketing Campaigns**: Combine DynaPictures with an email platform like SendGrid for targeted marketing. Use Pipedream to listen for specific user activities from your website or app, generate personalized images with DynaPictures, and send a custom email via SendGrid to engage the user with relevant content. diff --git a/components/dynatrace_api/README.md b/components/dynatrace_api/README.md index 0f6de455639bc..7a4aed021a040 100644 --- a/components/dynatrace_api/README.md +++ b/components/dynatrace_api/README.md @@ -1,12 +1,11 @@ # Overview -With the Dynatrace API, you can build a range of integrations and applications -to automate your Dynatrace environment. +The Dynatrace API provides programmatic access to the vast array of monitoring, performance data, and management operations within the Dynatrace software intelligence platform. With this powerful API, you can automate your monitoring tasks, integrate with your CI/CD pipeline for performance testing, setup custom alerting mechanics, and pull valuable insights into application performance and infrastructure health. Leveraging the Dynatrace API in Pipedream workflows lets you connect and orchestrate these operations with hundreds of other services for enhanced DevOps automation. -Some examples of what you can build include: +# Example Use Cases -- Automated deployment pipelines -- Self-service Dynatrace for development and test teams -- Custom dashboards and reporting -- Extended monitoring integrations -- Anomaly detection algorithms +- **Automated Incident Response**: Trigger a Pipedream workflow when Dynatrace detects an anomaly or a threshold breach. The workflow can automatically create an incident in an incident management platform like PagerDuty or Opsgenie, notify the responsible team via Slack or Microsoft Teams, and even execute diagnostic scripts to gather more information. + +- **Performance Metrics to Data Warehouse**: Set up a scheduled Pipedream workflow that fetches key performance metrics from Dynatrace for applications and services. These metrics can be transformed if necessary and pushed to a data warehouse like Google BigQuery or Snowflake for long-term analysis, trend forecasting, and to inform business decisions. + +- **CI/CD Deployment Quality Gates**: Integrate Dynatrace within your deployment pipeline by creating a Pipedream workflow that is triggered on a new deployment event. The workflow can call Dynatrace APIs to assess the performance impact, compare pre and post-deployment metrics and decide whether to proceed with the deployment, rollback, or trigger additional tests based on quality thresholds. diff --git a/components/e2b/actions/run-code/run-code.mjs b/components/e2b/actions/run-code/run-code.mjs new file mode 100644 index 0000000000000..ad2280fcd842d --- /dev/null +++ b/components/e2b/actions/run-code/run-code.mjs @@ -0,0 +1,29 @@ +import app from "../../e2b.app.mjs"; + +export default { + key: "e2b-run-code", + name: "Run Code", + description: "Run or interpret code using the E2B service. [See the documentation](https://www.npmjs.com/package/e2b).", + version: "0.0.1", + type: "action", + props: { + app, + code: { + type: "string", + label: "Code", + description: "The code that will be interpreted by the E2B service. Eg. `print('Hello, World!')`.", + }, + }, + async run({ $ }) { + const { + app, + code, + } = this; + + const response = await app.runCode(code); + + $.export("$summary", "Successfully interpreted code."); + + return response; + }, +}; diff --git a/components/e2b/e2b.app.mjs b/components/e2b/e2b.app.mjs new file mode 100644 index 0000000000000..75f0dfd9efaf7 --- /dev/null +++ b/components/e2b/e2b.app.mjs @@ -0,0 +1,16 @@ +import { Sandbox } from "@e2b/code-interpreter"; + +export default { + type: "app", + app: "e2b", + methods: { + getSandbox() { + process.env.E2B_API_KEY = this.$auth.api_key; + return Sandbox.create(); + }, + async runCode(code) { + const sandbox = await this.getSandbox(); + return sandbox.runCode(code); + }, + }, +}; diff --git a/components/e2b/package.json b/components/e2b/package.json new file mode 100644 index 0000000000000..2387f95e4cef5 --- /dev/null +++ b/components/e2b/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/e2b", + "version": "0.1.0", + "description": "Pipedream E2B Components", + "main": "e2b.app.mjs", + "keywords": [ + "pipedream", + "e2b" + ], + "homepage": "https://pipedream.com/apps/e2b", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@e2b/code-interpreter": "^1.0.3" + } +} diff --git a/components/e_conomic/README.md b/components/e_conomic/README.md index eedf77ac23658..643a898b6cee4 100644 --- a/components/e_conomic/README.md +++ b/components/e_conomic/README.md @@ -1,7 +1,11 @@ # Overview -The E-conomic API enables developers to access the vast majority of data and -functions available through the E-conomic online accounting system. This -includes accessing and manipulating customer, invoice, product, and journal -data. The API also enables integration with third-party applications, such as -online shopping carts and order management systems. +The E-conomic API allows for the automation of accounting tasks, enabling seamless integration of financial data into custom workflows. With Pipedream's capability to connect to the E-conomic API, you can trigger actions based on invoice creation, update financial records in real-time, and sync data across various business tools. By harnessing this API, businesses can streamline their financial processes, reduce manual data entry, and gain clearer insights into their financial health. + +# Example Use Cases + +- **Automated Invoice Processing**: When a new invoice is created in E-conomic, trigger a Pipedream workflow to send a notification via Slack to the finance team for approval. Once approved, the workflow could update the invoice status in E-conomic and send a confirmation email to the customer. + +- **Expense Tracking and Reporting**: Set up a workflow that monitors new expenses entered in E-conomic. The workflow can aggregate this data weekly and automatically generate a report, which is then sent to Google Sheets for further analysis. A summary of this report can be dispatched to key stakeholders through an email using a service like Gmail. + +- **Synchronized Customer Management**: On the creation of a new customer in a CRM like Salesforce, trigger a Pipedream workflow to create the corresponding client in E-conomic. If the customer updates their details in Salesforce, the workflow will keep the records in sync, ensuring accurate and up-to-date financial data in E-conomic. diff --git a/components/e_goi/README.md b/components/e_goi/README.md index eb63fa1358402..26aaf43b7f6eb 100644 --- a/components/e_goi/README.md +++ b/components/e_goi/README.md @@ -1,14 +1,11 @@ # Overview -The E-goi API allows developers to access and integrate the functionality of -E-goi with other applications. - -Some example applications that can be built using the E-goi API include: - -- A CRM application that integrates with E-goi to allow users to manage their - contacts and campaigns -- An email marketing application that uses E-goi to send out mass emails -- A web application that allows users to sign up for newsletters and other - email lists -- A mobile application that allows users to manage their E-goi account and - campaigns on the go +E-goi's API lets you automate multichannel marketing campaigns, manage contacts, send out transactional emails, and much more. Pipedream, as a serverless integration and compute platform, amplifies these capabilities by allowing you to create workflows that trigger actions in E-goi based on events from a myriad of other services or custom logic. + +# Example Use Cases + +- **Sync New E-commerce Customers to E-goi**: Automatically add new customers from Shopify to a specific list in E-goi. When a customer is created in Shopify, the workflow triggers, adding the customer's details to E-goi for future email marketing campaigns. + +- **Update CRM Leads with Email Engagement Data**: Sync engagement data from E-goi to Salesforce leads. If someone opens an email or clicks a link, the corresponding lead in Salesforce is updated with this interaction, keeping sales teams informed about prospect engagement. + +- **Automate Support Ticket Creation from Email Replies**: When a customer replies to a marketing email, automatically create a support ticket in Zendesk. This ensures that customer inquiries are swiftly addressed without manual email monitoring. diff --git a/components/e_goi/package.json b/components/e_goi/package.json new file mode 100644 index 0000000000000..cfc8598e24f58 --- /dev/null +++ b/components/e_goi/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/e_goi", + "version": "0.6.0", + "description": "Pipedream e_goi Components", + "main": "e_goi.app.mjs", + "keywords": [ + "pipedream", + "e_goi" + ], + "homepage": "https://pipedream.com/apps/e_goi", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/eagle_doc/README.md b/components/eagle_doc/README.md new file mode 100644 index 0000000000000..7eafc26095cb6 --- /dev/null +++ b/components/eagle_doc/README.md @@ -0,0 +1,11 @@ +# Overview + +The Eagle Doc API allows users to automate document management processes, including generation, storage, and retrieval of documents. This API can be especially useful in environments where documentation needs to be dynamically created and linked with various data points from different sources. On Pipedream, leveraging Eagle Doc with other integrated apps can streamline workflows that require heavy documentation handling, such as in legal, finance, or HR sectors. + +# Example Use Cases + +- **Automated Contract Generation and Storage**: - Use Eagle Doc API on Pipedream to generate contracts based on data received from a CRM system like Salesforce. Once the contracts are generated, they can be automatically stored in Eagle Doc and linked back to the respective customer records in Salesforce. + +- **Dynamic Report Compilation for Projects**: - Combine Eagle Doc with Google Sheets on Pipedream to automatically generate end-of-month reports. Input data can be collected from Google Sheets, used to create detailed documents via Eagle Doc, and then these documents can be emailed to stakeholders using a service like SendGrid integrated into the workflow. + +- **Employee Onboarding Documentation**: - Set up a Pipedream workflow where new employee data from an HR platform like BambooHR triggers the creation of personalized onboarding documents through Eagle Doc. These documents can then be stored in a secure cloud storage like Google Drive and shared with specific folders or personnel. diff --git a/components/eagle_doc/eagle_doc.app.mjs b/components/eagle_doc/eagle_doc.app.mjs new file mode 100644 index 0000000000000..0799698a6c22c --- /dev/null +++ b/components/eagle_doc/eagle_doc.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "eagle_doc", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/eagle_doc/package.json b/components/eagle_doc/package.json new file mode 100644 index 0000000000000..1584c82bc12a0 --- /dev/null +++ b/components/eagle_doc/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/eagle_doc", + "version": "0.0.1", + "description": "Pipedream Eagle Doc Components", + "main": "eagle_doc.app.mjs", + "keywords": [ + "pipedream", + "eagle_doc" + ], + "homepage": "https://pipedream.com/apps/eagle_doc", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/easy_peasy_ai/actions/create-transcription/create-transcription.mjs b/components/easy_peasy_ai/actions/create-transcription/create-transcription.mjs new file mode 100644 index 0000000000000..4953222c5aeed --- /dev/null +++ b/components/easy_peasy_ai/actions/create-transcription/create-transcription.mjs @@ -0,0 +1,81 @@ +import app from "../../easy_peasy_ai.app.mjs"; + +export default { + key: "easy_peasy_ai-create-transcription", + name: "Create Transcription", + description: "Generates AI transcription for a given audio URL. [See the documentation](https://easy-peasy.ai/audios)", + version: "0.0.1", + type: "action", + props: { + app, + url: { + type: "string", + label: "Audio URL", + description: "The URL of the audio file to transcribe.", + }, + audioType: { + type: "string", + label: "Audio Type", + description: "The type of the audio file. Eg. `podcast`, `meeting`.", + optional: true, + }, + language: { + description: "The language of the audio E.g. `English`, `Chinese`, `French`.", + propDefinition: [ + app, + "language", + ], + }, + name: { + type: "string", + label: "Name", + description: "The name of the transcription.", + optional: true, + }, + detectSpeakers: { + type: "boolean", + label: "Detect Speakers", + description: "Whether to detect multiple speakers.", + optional: true, + }, + enhanceQuality: { + type: "boolean", + label: "Enhance Quality", + description: "Whether to use enhanced quality for transcription.", + optional: true, + }, + }, + methods: { + generateTranscription(args = {}) { + return this.app.post({ + path: "/transcriptions", + ...args, + }); + }, + }, + async run({ $ }) { + const { + generateTranscription, + url, + audioType, + language, + name, + detectSpeakers, + enhanceQuality, + } = this; + + const response = await generateTranscription({ + $, + data: { + url, + audio_type: audioType, + language, + name, + detect_speakers: detectSpeakers, + enhance_quality: enhanceQuality, + }, + }); + $.export("$summary", `Successfully created a transcription with ID \`${response.uuid}\`.`); + return response; + }, +}; diff --git a/components/easy_peasy_ai/actions/generate-image/generate-image.mjs b/components/easy_peasy_ai/actions/generate-image/generate-image.mjs new file mode 100644 index 0000000000000..6ff24d790f462 --- /dev/null +++ b/components/easy_peasy_ai/actions/generate-image/generate-image.mjs @@ -0,0 +1,93 @@ +import app from "../../easy_peasy_ai.app.mjs"; + +export default { + key: "easy_peasy_ai-generate-image", + name: "Generate Image", + description: "Generates an AI image based on the given prompt. [See the documentation](https://easy-peasy.ai/ai-images)", + version: "0.0.1", + type: "action", + props: { + app, + prompt: { + type: "string", + label: "Prompt", + description: "The textual description of the image to be generated. Eg. `A cat sitting on a chair`.", + }, + model: { + type: "string", + label: "Model", + description: "The model to use for image generation.", + options: [ + "DALL-E 3", + "Stable Diffusion XL", + "Stable Diffusion 3.0", + ], + }, + style: { + type: "string", + label: "Style", + description: "The style of the generated image.", + optional: true, + }, + artist: { + type: "string", + label: "Artist", + description: "The artist of the generated image.", + optional: true, + }, + dimensions: { + type: "string", + label: "Dimensions", + description: "The dimensions of the generated image. Eg. `512x512`.", + optional: true, + }, + useHD: { + type: "boolean", + label: "Use HD", + description: "Use high-definition image generation?", + optional: true, + }, + image: { + type: "string", + label: "Image", + description: "Image URL for Image-to-Image generations.", + optional: true, + }, + }, + methods: { + generateImage(args = {}) { + return this.app.post({ + path: "/generate-image", + ...args, + }); + }, + }, + async run({ $ }) { + const { + generateImage, + prompt, + model, + style, + artist, + dimensions, + useHD, + image, + } = this; + + const response = await generateImage({ + $, + data: { + prompt, + model, + style, + artist, + dimensions, + useHD, + image, + }, + }); + + $.export("$summary", "Successfully generated an image."); + return response; + }, +}; diff --git a/components/easy_peasy_ai/actions/generate-text/generate-text.mjs b/components/easy_peasy_ai/actions/generate-text/generate-text.mjs new file mode 100644 index 0000000000000..4eb176c3d4318 --- /dev/null +++ b/components/easy_peasy_ai/actions/generate-text/generate-text.mjs @@ -0,0 +1,105 @@ +import app from "../../easy_peasy_ai.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "easy_peasy_ai-generate-text", + name: "Generate Text", + description: "Generates text outputs for the templates. [See the documentation](https://easy-peasy.ai/presets)", + version: "0.0.1", + type: "action", + props: { + app, + preset: { + type: "string", + label: "Template", + description: "The template name to use for generating content.", + options: constants.TEMPLATES, + }, + keywords: { + type: "string", + label: "Keywords", + description: "Keywords to use for generating content. Eg. `Write an email to potential investors about my startup`. (maxlength: 1000)", + }, + tone: { + type: "string", + label: "Tone", + description: "The tone of the generated content. Eg. `friendly, funny, cheerful`.", + optional: true, + }, + extra1: { + type: "string", + label: "Extra 1", + description: "Background Information (maxlength: 1000). Eg. `I am a software engineer`.", + optional: true, + }, + extra2: { + type: "string", + label: "Extra 2", + description: "Background Information (maxlength: 1000). Eg. `I am starting a new business`.", + optional: true, + }, + extra3: { + type: "string", + label: "Extra 3", + description: "Background Information (maxlength: 1000). Eg. `I am a student`.", + optional: true, + }, + outputs: { + type: "integer", + label: "Outputs", + description: "The number of outputs to generate. Eg. `1`.", + optional: true, + }, + language: { + propDefinition: [ + app, + "language", + ], + }, + shouldUseGPT4: { + type: "boolean", + label: "Use GPT-4", + description: "Use advanced AI model? Use the GPT-4 model for generating content.", + optional: true, + }, + }, + methods: { + generateText(args = {}) { + return this.app.post({ + path: "/generate", + ...args, + }); + }, + }, + async run({ $ }) { + const { + generateText, + preset, + keywords, + tone, + extra1, + extra2, + extra3, + outputs, + language, + shouldUseGPT4, + } = this; + + const response = await generateText({ + $, + data: { + preset, + keywords, + tone, + extra1, + extra2, + extra3, + outputs, + language, + shouldUseGPT4, + }, + }); + $.export("$summary", "Successfully generated text content."); + return response; + }, +}; diff --git a/components/easy_peasy_ai/common/constants.mjs b/components/easy_peasy_ai/common/constants.mjs new file mode 100644 index 0000000000000..8ca14c1e07344 --- /dev/null +++ b/components/easy_peasy_ai/common/constants.mjs @@ -0,0 +1,89 @@ +const TEMPLATES = [ + "custom-generator", + "paragraph-writer", + "instagram-post-caption", + "linkedin-post-generator", + "full-blog-post", + "blog-post", + "aida-framework", + "before-after-bridge-framework", + "problem-agitate-solution-framework", + "sentence-expander", + "content-rewriter", + "content-summarizer", + "grammar-corrector", + "native-speaker", + "clickbait-title-generator", + "reply-to-messsage", + "reply-to-email", + "email-generation", + "email-subject-line-generation", + "resume-headline-generator", + "linkedin-bio-generator", + "linkedin-recommendation-generator", + "linkedin-headline-generator", + "performance-review-generator", + "smart-goal-generator", + "testimonial-and-review-generator", + "press-release-generator", + "catchy-tagline", + "headline-generator", + "amazon-product-description-paragraph", + "amazon-product-bullets", + "resume-objective-generator", + "job-description-generator", + "job-summary-generator", + "job-qualifications-generator", + "job-responsibilities-generator", + "interview-questions-generator", + "interview-feedback-generator", + "blog-post-title", + "blog-post-outline", + "blog-post-intro", + "blog-post-conclusion", + "startup-ideas", + "user-story", + "acceptance-criteria", + "translate-to-singlish", + "eli5", + "business-name", + "fill-the-gaps", + "youtube-video-title", + "youtube-video-description", + "youtube-video-ideas", + "youtube-video-script-outline", + "tiktok-video-caption", + "tiktok-hashtags-generator", + "quora-answers", + "about-me-generator", + "google-ads-headlines", + "google-ads-descriptions", + "facebook-ads-headlines", + "facebook-ads-primary-text", + "seo-title-meta-descriptions", + "cover-letter-generator", + "twitter-post", + "twitter-thread", + "twitter-bio-generator", + "song-idea-generator", + "song-lyrics-generator", + "album-title-generator", + "product-descriptions", + "ai-story-generator", + "poem-title-generator", + "poem-generator", + "quote-generator", + "ai-joke-generator", + "greetings-generator", + "tinder-bio", + "pick-up-line-generator", + "instagram-bio", + "podcast-episode-title-generator", + "podcast-episode-description-generator", + "podcast-show-notes-generator", + "real-estate-listing-generator", +]; + +export default { + TEMPLATES, +}; diff --git a/components/easy_peasy_ai/easy_peasy_ai.app.mjs b/components/easy_peasy_ai/easy_peasy_ai.app.mjs new file mode 100644 index 0000000000000..fe711c9b05044 --- /dev/null +++ b/components/easy_peasy_ai/easy_peasy_ai.app.mjs @@ -0,0 +1,42 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "easy_peasy_ai", + propDefinitions: { + language: { + type: "string", + label: "Language", + description: "The language of the generated content. Eg. `English`.", + optional: true, + }, + }, + methods: { + getUrl(path) { + return `https://easy-peasy.ai/api${path}`; + }, + getHeaders(headers) { + return { + "Content-Type": "application/json", + "Accept": "application/json", + ...headers, + "x-api-key": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + }, +}; diff --git a/components/easy_peasy_ai/package.json b/components/easy_peasy_ai/package.json new file mode 100644 index 0000000000000..233f5ad8b29aa --- /dev/null +++ b/components/easy_peasy_ai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/easy_peasy_ai", + "version": "0.1.0", + "description": "Pipedream Easy-Peasy.AI Components", + "main": "easy_peasy_ai.app.mjs", + "keywords": [ + "pipedream", + "easy_peasy_ai" + ], + "homepage": "https://pipedream.com/apps/easy_peasy_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" + } +} diff --git a/components/easy_project/README.md b/components/easy_project/README.md new file mode 100644 index 0000000000000..3c6298a6f0412 --- /dev/null +++ b/components/easy_project/README.md @@ -0,0 +1,11 @@ +# Overview + +The Easy Project API offers a suite of project management tools that integrate with Pipedream to automate workflows, streamline processes, and enhance team collaboration. By connecting the Easy Project API to Pipedream, you can create custom automations for tasks like syncing project data, monitoring project updates, and managing resources. The API allows for interactions with issues, time entries, and project structures, making it a versatile tool for project managers and teams who want to align their project management tooling with other business systems. + +# Example Use Cases + +- **Sync Project Issues with Google Sheets**: Automatically export new or updated issues from Easy Project to a Google Sheets spreadsheet. This workflow can be used to create reports, share project updates with stakeholders, or analyze project data for insights. + +- **Slack Notifications for Project Updates**: Set up a workflow where any updates to projects or tasks in Easy Project trigger a notification in Slack. This can be used to keep the team informed about project progress, new assignments, or urgent issues that need immediate attention. + +- **GitHub Integration for Development Projects**: Whenever a new commit is made in a GitHub repository, update the relevant project or task in Easy Project. This workflow is perfect for teams that want to keep their project management in sync with their codebase, ensuring that progress is tracked accurately across both platforms. diff --git a/components/easy_project/package.json b/components/easy_project/package.json index e408b43de7079..d058f8b0d51df 100644 --- a/components/easy_project/package.json +++ b/components/easy_project/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/easy_projects/README.md b/components/easy_projects/README.md new file mode 100644 index 0000000000000..555c673ee585e --- /dev/null +++ b/components/easy_projects/README.md @@ -0,0 +1,11 @@ +# Overview + +The Easy Projects API allows you to interact with your project management data programmatically. With this API, you can automate tasks, sync data across different platforms, extract reports, and streamline communication between team members. By integrating the Easy Projects API on Pipedream, you can create powerful workflows that trigger actions within Easy Projects or in other apps, based on events that occur within your project management space. + +# Example Use Cases + +- **Automate Task Creation from Emails**: Automatically create tasks in Easy Projects when you receive emails with a specific subject line in Gmail. This can streamline the process of task initiation from client or team member requests. + +- **Sync Project Tasks with Google Calendar**: Ensure that all tasks and their deadlines from Easy Projects are reflected in Google Calendar. When a new task is created or updated in Easy Projects, a corresponding event could be added or updated in Google Calendar. + +- **Slack Notifications for Project Updates**: Send real-time notifications to a designated Slack channel whenever a project is updated in Easy Projects. This keeps the whole team informed about project status changes or milestone completions. diff --git a/components/easycsv/README.md b/components/easycsv/README.md new file mode 100644 index 0000000000000..dd95054fd6a5f --- /dev/null +++ b/components/easycsv/README.md @@ -0,0 +1,11 @@ +# Overview + +The EasyCSV API is a powerful tool for converting CSV data into JSON and vice versa, making it a breeze to manage bulk data operations. With Pipedream, you can leverage this API to automate workflows, process and transform data on the fly, and integrate with numerous other services. By crafting workflows that respond to various triggers, you can streamline tasks that involve CSV data without writing extensive code. + +# Example Use Cases + +- **Bulk Import Tasks to Project Management Tools**: Use EasyCSV to parse a CSV file containing tasks and import them into project management apps like Trello or Asana. When a new CSV is uploaded to a cloud storage platform, Pipedream can trigger a workflow that processes the file and creates corresponding tasks in your project management tool. + +- **Send Form Submissions to a Database**: Transform form submissions exported as CSV with EasyCSV and send the data to a SQL database. Whenever a new form submission is received and exported to CSV, Pipedream can automate the ingestion of this data into your database, ensuring your datasets are always up to date. + +- **Sync Email Subscriber Lists**: Keep your email marketing subscriber list in sync across different platforms. When a CSV file of new subscribers is added to a designated directory, use Pipedream to parse the CSV via EasyCSV and update subscriber lists on email platforms like Mailchimp or SendGrid. diff --git a/components/easycsv/package.json b/components/easycsv/package.json index 674ce8f2f0258..afaf7d7a55c57 100644 --- a/components/easycsv/package.json +++ b/components/easycsv/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/easydns/README.md b/components/easydns/README.md index 4e400c56bd2d5..9d658f12390ba 100644 --- a/components/easydns/README.md +++ b/components/easydns/README.md @@ -1,9 +1,11 @@ # Overview -The easyDNS API can be used to perform a variety of tasks, including: +The easyDNS API enables powerful interaction with your easyDNS domain services, allowing for automated domain management tasks. It supports operations like updating DNS records, managing domain registrations, and checking domain availability, all through a programmatic interface. Leveraging the easyDNS API within Pipedream, you can create automated workflows that respond to events from a multitude of services, maintain domain health, and streamline your domain operations. -- Registering new domains -- Transferring domains -- Updating DNS records -- Renewing domains -- And more! +# Example Use Cases + +- **Automated DNS Record Updates**: Set up a Pipedream workflow that listens for webhook notifications from your deployment service (like GitHub Actions or Jenkins). Whenever a new version of your app is deployed, the workflow triggers an easyDNS API call to update the A or CNAME record to point to the new server's IP address. + +- **Domain Renewal Reminders**: Create a workflow that periodically checks the expiration dates of your domains through the easyDNS API. If any domain is nearing expiration, the workflow sends a reminder email using an email service like SendGrid, ensuring that you never lose access to your critical domain names. + +- **Dynamic DNS for Home Networks**: If you have a home server with a dynamic IP, set up a workflow that checks your current public IP at set intervals using an IP check service. If a change is detected, the workflow makes an API request to easyDNS to update your DNS record, keeping your home server accessible even with an ever-changing IP address. diff --git a/components/easydns/package.json b/components/easydns/package.json new file mode 100644 index 0000000000000..5224c0964b7a4 --- /dev/null +++ b/components/easydns/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/easydns", + "version": "0.6.0", + "description": "Pipedream easydns Components", + "main": "easydns.app.mjs", + "keywords": [ + "pipedream", + "easydns" + ], + "homepage": "https://pipedream.com/apps/easydns", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/easyhire/easyhire.app.mjs b/components/easyhire/easyhire.app.mjs new file mode 100644 index 0000000000000..65e2289496856 --- /dev/null +++ b/components/easyhire/easyhire.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "easyhire", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/easyhire/package.json b/components/easyhire/package.json new file mode 100644 index 0000000000000..2f9621890dd59 --- /dev/null +++ b/components/easyhire/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/easyhire", + "version": "0.0.1", + "description": "Pipedream easyhire Components", + "main": "easyhire.app.mjs", + "keywords": [ + "pipedream", + "easyhire" + ], + "homepage": "https://pipedream.com/apps/easyhire", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/easyly/README.md b/components/easyly/README.md new file mode 100644 index 0000000000000..5010aa63bd7a6 --- /dev/null +++ b/components/easyly/README.md @@ -0,0 +1,11 @@ +# Overview + +The Easyly API offers a suite of features aimed at simplifying complex tasks such as address validation, geocoding, and route optimization. With Pipedream's serverless platform, you can leverage this API to automate workflows that require address verification, coordinate mapping, or efficient route planning. The integration of Easyly with Pipedream allows you to set up triggers and actions without setting up a server, making it straightforward to incorporate real-time data processing and event-driven architectures into your applications. + +# Example Use Cases + +- **Address Validation for E-commerce Orders**: Before processing orders in an e-commerce app, use the Easyly API on Pipedream to validate customer addresses. This step can reduce delivery errors and improve customer satisfaction. + +- **Dynamic Route Optimization for Deliveries**: For logistics and delivery services, you could build a Pipedream workflow that uses the Easyly API to optimize routes based on the latest orders, considering factors like distance, traffic, and delivery windows. + +- **Geocoding for Real Estate Platforms**: Integrate Easyly with a real estate app to convert property addresses into geographical coordinates. This data can enrich property listings with precise location mapping. diff --git a/components/easypost/README.md b/components/easypost/README.md new file mode 100644 index 0000000000000..093572074a83d --- /dev/null +++ b/components/easypost/README.md @@ -0,0 +1,11 @@ +# Overview + +The EasyPost API simplifies the shipping process by offering a suite of tools for rate shopping, label creation, tracking, and address verification. With Pipedream, you can automate these shipping tasks, integrate with various services (like e-commerce platforms or inventory systems), and streamline your logistics operations. Pipedream lets you build serverless workflows using EasyPost’s API, enabling you to connect shipping data with other apps, react to shipment updates in real-time, and create custom notifications or reports. + +# Example Use Cases + +- **Order Fulfillment Automation**: When a new order comes in from an e-commerce platform like Shopify, you can use Pipedream to automatically create a shipping label with EasyPost. This workflow can capture the order details, calculate the best rate, generate a label, and update the order status with the tracking number. + +- **Shipment Tracking Updates**: Set up a Pipedream workflow that listens for webhook events from EasyPost for shipment tracking updates. The workflow can filter for specific status changes, like 'out for delivery', and send real-time notifications via SMS with Twilio or email through SendGrid to keep customers informed. + +- **Address Verification System**: Before shipping orders, you can validate customer addresses using EasyPost's Address Verification API. Integrate this with a CRM like Salesforce or HubSpot using Pipedream to ensure every address in your system is accurate, reducing the chances of failed deliveries and improving customer satisfaction. diff --git a/components/easypost/package.json b/components/easypost/package.json index 8aa0066fd84b5..54dc10f257e97 100644 --- a/components/easypost/package.json +++ b/components/easypost/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/easypromos/easypromos.app.mjs b/components/easypromos/easypromos.app.mjs new file mode 100644 index 0000000000000..9b199adf5ad4f --- /dev/null +++ b/components/easypromos/easypromos.app.mjs @@ -0,0 +1,139 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "easypromos", + propDefinitions: { + userId: { + type: "integer", + label: "User ID", + description: "The ID of the user", + async options({ + promotionId, prevContext, + }) { + const { + items, paging, + } = await this.getUsers({ + promotionId, + params: { + next_cursor: prevContext.nextCursor, + }, + }); + return { + options: items.map(({ + id: value, email: label, + }) => ({ + label, + value, + })), + context: { + nextCursor: paging.next_cursor, + }, + }; + }, + }, + promotionId: { + type: "integer", + label: "Promotion ID", + description: "The ID of the promotion", + async options({ prevContext }) { + const { + items, paging, + } = await this.getPromotions({ + params: { + next_cursor: prevContext.nextCursor, + }, + }); + return { + options: items.map(({ + id, title, internal_ref: ref, + }) => ({ + label: ref || title, + value: parseInt(id), + })), + context: { + nextCursor: paging.next_cursor, + }, + }; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.easypromosapp.com/v2"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + getCoinTransactions({ + promotionId, ...opts + }) { + return this._makeRequest({ + path: `/coin_transactions/${promotionId}`, + ...opts, + }); + }, + getUsers({ + promotionId, ...opts + }) { + return this._makeRequest({ + path: `/users/${promotionId}`, + ...opts, + }); + }, + getParticipations({ + promotionId, ...opts + }) { + return this._makeRequest({ + path: `/participations/${promotionId}`, + ...opts, + }); + }, + getPromotions(opts = {}) { + return this._makeRequest({ + path: "/promotions", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let nextCursor = null; + + do { + params.next_cursor = nextCursor; + const { + items, + paging: { next_cursor }, + } = await fn({ + params, + ...opts, + }); + for (const d of items) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + nextCursor = next_cursor; + hasMore = nextCursor; + + } while (hasMore); + }, + }, +}; diff --git a/components/easypromos/package.json b/components/easypromos/package.json new file mode 100644 index 0000000000000..ff4fa8d3e4e16 --- /dev/null +++ b/components/easypromos/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/easypromos", + "version": "0.1.0", + "description": "Pipedream Easypromos Components", + "main": "easypromos.app.mjs", + "keywords": [ + "pipedream", + "easypromos" + ], + "homepage": "https://pipedream.com/apps/easypromos", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/easypromos/sources/common/base.mjs b/components/easypromos/sources/common/base.mjs new file mode 100644 index 0000000000000..3123e3876ee1b --- /dev/null +++ b/components/easypromos/sources/common/base.mjs @@ -0,0 +1,87 @@ +import { + ConfigurationError, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import easypromos from "../../easypromos.app.mjs"; + +export default { + props: { + easypromos, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + info: { + type: "alert", + alertType: "info", + content: "The Easypromos API only works with \"White Label\" promotions, any other type will not appear in the list.", + }, + promotionId: { + propDefinition: [ + easypromos, + "promotionId", + ], + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + getOpts() { + return {}; + }, + async emitEvent(maxResults = false) { + const lastId = this._getLastId(); + let responseArray = []; + + try { + const response = this.easypromos.paginate({ + fn: this.getFunction(), + ...this.getOpts(), + params: { + order: "created_desc", + }, + }); + + for await (const item of response) { + if (item.id <= lastId) break; + responseArray.push(item); + } + } catch (err) { + console.log(err); + if (err?.response?.data?.code === 0) { + throw new ConfigurationError("You can only use this trigger with promotions that have the 'Virtual Coins' feature enabled."); + } + throw new ConfigurationError(err); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastId(responseArray[0].id); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id || item.transaction.id, + summary: this.getSummary(item), + ts: Date.parse(item.created || new Date()), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/easypromos/sources/new-coin-transaction/new-coin-transaction.mjs b/components/easypromos/sources/new-coin-transaction/new-coin-transaction.mjs new file mode 100644 index 0000000000000..2f0f305dccb2b --- /dev/null +++ b/components/easypromos/sources/new-coin-transaction/new-coin-transaction.mjs @@ -0,0 +1,37 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "easypromos-new-coin-transaction", + name: "New Coin Transaction", + description: "Emit new event when a user earns or spends coins.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + info2: { + type: "alert", + alertType: "warning", + content: "You can only use this trigger with promotions that have the 'Virtual Coins' feature enabled.", + }, + }, + methods: { + ...common.methods, + getFunction() { + return this.easypromos.getCoinTransactions; + }, + getOpts() { + return { + promotionId: this.promotionId, + }; + }, + getSummary({ + transaction, user, + }) { + return `Coin transaction: ${transaction.amount} for user ${user.email}`; + }, + }, + sampleEmit, +}; diff --git a/components/easypromos/sources/new-coin-transaction/test-event.mjs b/components/easypromos/sources/new-coin-transaction/test-event.mjs new file mode 100644 index 0000000000000..ef6dc9f2de2ec --- /dev/null +++ b/components/easypromos/sources/new-coin-transaction/test-event.mjs @@ -0,0 +1,54 @@ +export default { + "transaction": { + "id": 1, + "user_id": 1, + "promotion_id": 1, + "coin_id": 1, + "coin_name": "Points", + "amount": 5, + "balance": 5, + "created": "2019-08-24T14:15:22Z", + "transaction_type": 1, + "reason": "Earned for participating in a stage", + "extra": "string" + }, + "user": { + "id": 1, + "promotion_id": 1, + "first_name": "John", + "last_name": "Smith", + "nickname": "jsmith", + "login_type": "email", + "social_id": "string", + "external_id": "string", + "status": 0, + "email": "user@example.com", + "phone": "string", + "birthday": "2019-08-24", + "created": "2019-08-24T14:15:22Z", + "avatar_img": "http://example.com", + "language": "ara", + "country": "FR", + "custom_properties": [ + { + "id": 1, + "title": "Postal code", + "ref": "postalcode", + "value": "PC98776" + } + ], + "meta_data": { + "utm_source": "instagram", + "utm_medium": "ads", + "utm_campaign": "campaign-week1", + "referral_url": "http://example.com", + "ip": "192.168.0.1", + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", + "legals": { + "terms_url": "https://a.cstmapp.com/promo_terms/919755", + "privacy_url": "https://a.cstmapp.com/policy/987543", + "accepted_cookies": 1 + } + } + } +} \ No newline at end of file diff --git a/components/easypromos/sources/new-participation/new-participation.mjs b/components/easypromos/sources/new-participation/new-participation.mjs new file mode 100644 index 0000000000000..6333b45edf447 --- /dev/null +++ b/components/easypromos/sources/new-participation/new-participation.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "easypromos-new-participation", + name: "New Participation Submitted", + description: "Emit new event when a registered user submits a participation in the promotion.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.easypromos.getParticipations; + }, + getOpts() { + return { + promotionId: this.promotionId, + }; + }, + getSummary(participation) { + return `New Participation: ${participation.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/easypromos/sources/new-participation/test-event.mjs b/components/easypromos/sources/new-participation/test-event.mjs new file mode 100644 index 0000000000000..9a483b1afc87c --- /dev/null +++ b/components/easypromos/sources/new-participation/test-event.mjs @@ -0,0 +1,19 @@ +export default { + "id": 1, + "promotion_id": 1, + "stage_id": 1, + "user_id": 1, + "created": "2019-08-24T14:15:22Z", + "ip": "192.168.0.1", + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/201.0.429950705 Mobile/15E148 Safari/604.1", + "points": 786.43, + "data": [ + { + "ref": "Question1", + "title": "Pick your favorite city", + "values": [ + "Barcelona" + ] + } + ] +} \ No newline at end of file diff --git a/components/easypromos/sources/new-user/new-user.mjs b/components/easypromos/sources/new-user/new-user.mjs new file mode 100644 index 0000000000000..ed50831382516 --- /dev/null +++ b/components/easypromos/sources/new-user/new-user.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "easypromos-new-user", + name: "New User Registration", + description: "Emit new event when a user registers in the promotion.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.easypromos.getUsers; + }, + getOpts() { + return { + promotionId: this.promotionId, + }; + }, + getSummary(user) { + return `New User Registration: ${user.email}`; + }, + }, + sampleEmit, +}; diff --git a/components/easypromos/sources/new-user/test-event.mjs b/components/easypromos/sources/new-user/test-event.mjs new file mode 100644 index 0000000000000..956839b52c54c --- /dev/null +++ b/components/easypromos/sources/new-user/test-event.mjs @@ -0,0 +1,39 @@ +export default { + "id": 1, + "promotion_id": 1, + "first_name": "John", + "last_name": "Smith", + "nickname": "jsmith", + "login_type": "email", + "social_id": "string", + "external_id": "string", + "status": 0, + "email": "user@example.com", + "phone": "string", + "birthday": "2019-08-24", + "created": "2019-08-24T14:15:22Z", + "avatar_img": "http://example.com", + "language": "ara", + "country": "FR", + "custom_properties": [ + { + "id": 1, + "title": "Postal code", + "ref": "postalcode", + "value": "PC98776" + } + ], + "meta_data": { + "utm_source": "instagram", + "utm_medium": "ads", + "utm_campaign": "campaign-week1", + "referral_url": "http://example.com", + "ip": "192.168.0.1", + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", + "legals": { + "terms_url": "https://a.cstmapp.com/promo_terms/919755", + "privacy_url": "https://a.cstmapp.com/policy/987543", + "accepted_cookies": 1 + } + } +} \ No newline at end of file diff --git a/components/easysendy/README.md b/components/easysendy/README.md new file mode 100644 index 0000000000000..5729be157cf99 --- /dev/null +++ b/components/easysendy/README.md @@ -0,0 +1,11 @@ +# Overview + +The EasySendy API interfaces with a flexible email marketing solution, allowing users to manage, segment, and engage with their subscribers efficiently. With Pipedream, you can harness this API to automate your email campaigns, synchronize subscriber lists, or trigger event-based email flows. It's perfect for fine-tuning your email outreach strategy with minimal manual intervention. + +# Example Use Cases + +- **Automated Welcome Email Series**: Set up a workflow that listens for new subscribers added to a specific EasySendy list and automatically triggers a sequence of welcome emails, personalizing each message based on subscriber data. + +- **Subscriber List Sync**: Create a workflow that syncs subscriber lists between EasySendy and another app like Google Sheets. Whenever a new row is added to a specific sheet, the workflow adds that contact to an EasySendy subscriber list, keeping your records seamlessly up-to-date. + +- **Event-Driven Campaigns**: Construct a workflow that monitors user activity on your platform (via webhooks or a third-party service like Segment) and uses EasySendy to send targeted email campaigns based on specific actions, like abandoned cart reminders or product launch announcements. diff --git a/components/easysendy/actions/add-subscriber/add-subscriber.mjs b/components/easysendy/actions/add-subscriber/add-subscriber.mjs new file mode 100644 index 0000000000000..0bc58cf3575dd --- /dev/null +++ b/components/easysendy/actions/add-subscriber/add-subscriber.mjs @@ -0,0 +1,55 @@ +import { ConfigurationError } from "@pipedream/platform"; +import easysendy from "../../easysendy.app.mjs"; + +export default { + key: "easysendy-add-subscriber", + name: "Add Subscriber", + description: "Adds a new subscriber to an EasySendy list. [See the documentation](https://developers.easysendyapp.com/easysendypro/)", + version: "0.0.1", + type: "action", + props: { + easysendy, + email: { + type: "string", + label: "Email", + description: "The subscriber's email address.", + }, + listId: { + propDefinition: [ + easysendy, + "listId", + ], + }, + fname: { + type: "string", + label: "First Name", + description: "The subscriber's first name.", + optional: true, + }, + lname: { + type: "string", + label: "Last Name", + description: "The subscriber's last name.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.easysendy.addSubscriber({ + $, + data: { + req_type: "subscribe", + listUID: this.listId, + EMAIL: this.email, + FNAME: this.fname, + LNAME: this.lname, + }, + }); + + if (response.status === "error") { + throw new ConfigurationError(response.message); + } + + $.export("$summary", `Successfully added subscriber ${this.email} to list ${this.listId}`); + return response; + }, +}; diff --git a/components/easysendy/easysendy.app.mjs b/components/easysendy/easysendy.app.mjs new file mode 100644 index 0000000000000..e057e2e576444 --- /dev/null +++ b/components/easysendy/easysendy.app.mjs @@ -0,0 +1,68 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "easysendy", + propDefinitions: { + listId: { + type: "string", + label: "List ID", + description: "The unique identifier of the list to which the subscriber is being added.", + async options({ page }) { + const { listsData } = await this.listLists({ + params: { + pageNumber: page + 1, + }, + }); + + return listsData.map(({ + list_uid: value, + name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "http://api.easysendy.com/ver1"; + }, + _data(data) { + return { + ...data, + "api_key": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, data, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + data: this._data(data), + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + ...opts, + }); + }, + addSubscriber(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/subscribeAPI", + ...opts, + }); + }, + listLists(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/listAPI", + data: { + "req_type": "allLists", + }, + ...opts, + }); + }, + }, +}; diff --git a/components/easysendy/package.json b/components/easysendy/package.json new file mode 100644 index 0000000000000..4b5ee6b7c8949 --- /dev/null +++ b/components/easysendy/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/easysendy", + "version": "0.1.0", + "description": "Pipedream EasySendy Components", + "main": "easysendy.app.mjs", + "keywords": [ + "pipedream", + "easysendy" + ], + "homepage": "https://pipedream.com/apps/easysendy", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" + } +} diff --git a/components/easyship/README.md b/components/easyship/README.md index de12bc5b9ae5c..b6207b9c07c98 100644 --- a/components/easyship/README.md +++ b/components/easyship/README.md @@ -1,8 +1,17 @@ # Overview -With Easyship, you can build shipping solutions for a variety of needs. Here -are some examples: +The Easyship API unlocks the potential for seamless shipping processes, by integrating a multitude of courier services into your workflow. With this API, you can automate the retrieval of shipping rates, create shipments, manage orders, and track packages in transit. This streamlines the logistics side of e-commerce, ensuring that businesses can focus on what they do best – sell products – while the shipping details are handled efficiently in the background. -- Create a shipping solution for an online store -- Create a shipping solution for a marketplace -- Create a shipping solution for a logistics company +# Example Use Cases + +**Automated Order Fulfillment** + +- When a new order is placed in your e-commerce platform, such as Shopify, trigger a Pipedream workflow to automatically create a shipment with Easyship. The workflow could then retrieve the best shipping rates, select a preferred courier, and generate shipping labels, all without manual intervention. + +**Real-time Shipping Updates** + +- Set up a workflow that listens for webhook events from Easyship for tracking updates. Whenever there's a change in the shipment status, the workflow can push these updates to a Slack channel, an email, or an SMS service like Twilio, keeping customers informed every step of the way. + +**Centralized Shipping Dashboard** + +- Build a workflow that periodically pulls all your shipment data from Easyship and pushes it into a Google Sheets document or a dashboard application like Grafana. This enables you to have a centralized view of all shipping metrics and manage operations more effectively. diff --git a/components/echtpost_postcards/README.md b/components/echtpost_postcards/README.md new file mode 100644 index 0000000000000..2562aa7b5a62c --- /dev/null +++ b/components/echtpost_postcards/README.md @@ -0,0 +1,11 @@ +# Overview + +The EchtPost Postcards API allows you to programmatically send real, physical postcards. With Pipedream's capabilities, you can automate the sending of these postcards in response to various triggers or events. Imagine crafting personalized thank you notes to customers, sending holiday greetings, or running a postcard marketing campaign completely automated with Pipedream's serverless platform. + +# Example Use Cases + +- **Customer Appreciation Postcards**: After a customer makes a purchase on your e-commerce platform, Pipedream listens for the purchase event, then uses the EchtPost Postcards API to send a thank you postcard to the customer's provided address. + +- **Event Reminder Postcards**: Pipedream schedules and sends reminders for upcoming events. A week before an event stored in your Google Calendar, Pipedream triggers a workflow that uses the EchtPost Postcards API to mail out reminder postcards to all the attendees who have RSVP’d. + +- **Local Weather Updates**: Combine the OpenWeatherMap API with the EchtPost Postcards API on Pipedream to send postcards with weekly weather forecasts. Pipedream can pull weather data for different regions and automatically send postcards to residents with the upcoming weather summary. diff --git a/components/ecologi/.gitignore b/components/ecologi/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/ecologi/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/ecologi/README.md b/components/ecologi/README.md new file mode 100644 index 0000000000000..5140c5d9aba6c --- /dev/null +++ b/components/ecologi/README.md @@ -0,0 +1,11 @@ +# Overview + +The Ecologi API allows you to integrate with their platform to track and support climate projects. Through Pipedream, you can automate interactions with Ecologi, including retrieving your carbon offset data, funding climate projects, and planting trees. With Pipedream's serverless platform, you can create workflows that trigger based on various events, process data, and connect with thousands of other apps to perform actions conditionally based on your sustainability goals. + +# Example Use Cases + +- **Automated Monthly Offset Report**: Generate and send a monthly report detailing your carbon offset contributions. This workflow could retrieve your offset data from Ecologi, compile it into a summary, and email it to stakeholders using a service like SendGrid or directly through SMTP. + +- **E-commerce Carbon Neutral Orders**: Create a workflow that plants trees with Ecologi each time an order is placed on your e-commerce platform. For example, upon receiving a new order webhook from Shopify, the Pipedream workflow could calculate the required number of trees to offset the order's carbon footprint and use the Ecologi API to plant them. + +- **Social Impact Public Reporting**: Automatically update a public dashboard with your latest Ecologi impact stats. Whenever you reach a new milestone in trees planted or carbon offset, Pipedream can trigger a workflow to update a Google Sheet or push the data to a Data Studio report, showcasing your company's commitment to the environment. diff --git a/components/ecologi/actions/buy-offsets/buy-offsets.mjs b/components/ecologi/actions/buy-offsets/buy-offsets.mjs new file mode 100644 index 0000000000000..a1d1d2da72979 --- /dev/null +++ b/components/ecologi/actions/buy-offsets/buy-offsets.mjs @@ -0,0 +1,42 @@ +import app from "../../ecologi.app.mjs"; + +export default { + key: "ecologi-buy-offsets", + name: "Buy Offsets", + description: "Buy carbon avoidance credits through Ecologi. [See the documentation](https://docs.ecologi.com/docs/public-api-docs/e07bbee7fa605-purchase-carbon-avoidance)", + version: "0.0.1", + type: "action", + props: { + app, + number: { + propDefinition: [ + app, + "number", + ], + }, + units: { + propDefinition: [ + app, + "units", + ], + }, + test: { + propDefinition: [ + app, + "test", + ], + }, + }, + async run({ $ }) { + const response = await this.app.buyOffsets({ + $, + data: { + number: this.number, + units: this.units, + test: this.test, + }, + }); + $.export("$summary", "Successfully bought carbon avoidance credits"); + return response; + }, +}; diff --git a/components/ecologi/actions/buy-trees/buy-trees.mjs b/components/ecologi/actions/buy-trees/buy-trees.mjs new file mode 100644 index 0000000000000..525f6a8d7a85d --- /dev/null +++ b/components/ecologi/actions/buy-trees/buy-trees.mjs @@ -0,0 +1,44 @@ +import app from "../../ecologi.app.mjs"; + +export default { + key: "ecologi-buy-trees", + name: "Buy Trees", + description: "Purchase trees through Ecologi. [See the documentation](https://docs.ecologi.com/docs/public-api-docs/004342d262f93-purchase-trees)", + version: "0.0.1", + type: "action", + props: { + app, + number: { + propDefinition: [ + app, + "number", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + test: { + propDefinition: [ + app, + "test", + ], + }, + }, + async run({ $ }) { + const response = await this.app.buyTrees({ + $, + data: { + number: this.number, + name: this.name, + test: this.test, + }, + }); + + $.export("$summary", `Successfully bought ${this.number} tree(s)`); + + return response; + }, +}; diff --git a/components/ecologi/app/ecologi.app.ts b/components/ecologi/app/ecologi.app.ts deleted file mode 100644 index 47bb2e5b60478..0000000000000 --- a/components/ecologi/app/ecologi.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "ecologi", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/ecologi/common/constants.mjs b/components/ecologi/common/constants.mjs new file mode 100644 index 0000000000000..2f55b80bf384e --- /dev/null +++ b/components/ecologi/common/constants.mjs @@ -0,0 +1,6 @@ +export default { + UNIT_TYPES: [ + "KG", + "Tonnes", + ], +}; diff --git a/components/ecologi/ecologi.app.mjs b/components/ecologi/ecologi.app.mjs new file mode 100644 index 0000000000000..930746eecc444 --- /dev/null +++ b/components/ecologi/ecologi.app.mjs @@ -0,0 +1,69 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "ecologi", + propDefinitions: { + number: { + type: "integer", + label: "number", + description: "Number of trees of offsets to purchase", + }, + name: { + type: "string", + label: "name", + description: "The 'funded by' name for the trees", + optional: true, + }, + units: { + type: "string", + label: "units", + description: "The unit of the amount of offsets to purchase", + options: constants.UNIT_TYPES, + }, + test: { + type: "boolean", + label: "test", + description: "Whether this is a test transaction or not", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://public.ecologi.com"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + "Authorization": `Bearer ${this.$auth.api_key}`, + "Accept": "application/json", + ...headers, + }, + }); + }, + + async buyTrees(args = {}) { + return this._makeRequest({ + path: "/impact/trees", + method: "post", + ...args, + }); + }, + async buyOffsets(args = {}) { + return this._makeRequest({ + path: "/impact/carbon", + method: "post", + ...args, + }); + }, + }, +}; diff --git a/components/ecologi/package.json b/components/ecologi/package.json index f6f9cf1bb05a1..18c0bc2bc525b 100644 --- a/components/ecologi/package.json +++ b/components/ecologi/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/ecologi", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream Ecologi Components", "main": "dist/app/ecologi.app.mjs", "keywords": [ "pipedream", "ecologi" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/ecologi", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/ecwid/README.md b/components/ecwid/README.md index b174e69b4a263..9106326d73ee5 100644 --- a/components/ecwid/README.md +++ b/components/ecwid/README.md @@ -1,10 +1,11 @@ # Overview -With the ecwid API, you can build applications to: - -- Display and search products -- View customer info and order history -- Update inventory -- Calculate shipping costs -- Apply coupons and discounts -- Plus much more! +Ecwid's API offers dynamic access to an online store's data, allowing for the automation of tasks such as inventory management, order processing, and customer data analysis. With Pipedream's serverless integration platform, you can create custom workflows that trigger actions within Ecwid or synchronize data across various other apps and services. This capability can streamline operations, save time, and reduce the likelihood of human error. + +# Example Use Cases + +- **Automated Order Processing:** Set up a workflow that triggers whenever a new order is placed on Ecwid. This workflow can automatically send order details to a fulfillment service like ShipStation, generate and send an invoice using FreshBooks, and notify the customer with an update via Twilio SMS. + +- **Real-time Inventory Syncing:** Create a workflow that monitors inventory levels in Ecwid and updates the quantities in real time across multiple sales channels, such as Shopify or Amazon. This ensures that stock levels are consistent, avoiding overselling and providing customers with accurate availability information. + +- **Customer Relationship Management (CRM) Integration:** Design a workflow that takes newly registered customers on Ecwid and adds them to a CRM platform like Salesforce or HubSpot. This can be extended to segment customers based on purchase history, calculate lifetime value, and trigger personalized marketing campaigns. diff --git a/components/ecwid/actions/get-order/get-order.mjs b/components/ecwid/actions/get-order/get-order.mjs index d2e78460f3d0b..69a521a19a075 100644 --- a/components/ecwid/actions/get-order/get-order.mjs +++ b/components/ecwid/actions/get-order/get-order.mjs @@ -2,7 +2,7 @@ import ecwid from "../../ecwid.app.mjs"; export default { name: "Ecwid Get Order", - version: "0.0.4", + version: "0.0.5", key: "ecwid-get-order", description: "Get Ecwid Order by Order ID. Details of the structure are present [here](https://api-docs.ecwid.com/reference/get-order).", props: { diff --git a/components/ecwid/actions/update-order-status/update-order-status.mjs b/components/ecwid/actions/update-order-status/update-order-status.mjs index 8de812c152051..04064136561dc 100644 --- a/components/ecwid/actions/update-order-status/update-order-status.mjs +++ b/components/ecwid/actions/update-order-status/update-order-status.mjs @@ -2,7 +2,7 @@ import { FULFILMENT_STATUS_LIST } from "../../commons/commons.mjs"; import ecwid from "../../ecwid.app.mjs"; export default { name: "Ecwid Update Order Status", - version: "0.0.4", + version: "0.0.5", key: "ecwid-update-order-status", description: "Update the Status of an Ecwid Order. Makes use of the [Update Order API](https://api-docs.ecwid.com/reference/update-order).", props: { diff --git a/components/ecwid/ecwid.app.mjs b/components/ecwid/ecwid.app.mjs index 6265b48b24059..a77f68931dc41 100644 --- a/components/ecwid/ecwid.app.mjs +++ b/components/ecwid/ecwid.app.mjs @@ -5,7 +5,7 @@ export default { propDefinitions: {}, methods: { _accessToken() { - return this.$auth.oauth_access_token; + return this.$auth.client_secret; }, _apiUrl() { return `https://app.ecwid.com/api/v3/${this.$auth.storeId}`; diff --git a/components/ecwid/package.json b/components/ecwid/package.json index 18110564995e9..c8423358b7e2a 100644 --- a/components/ecwid/package.json +++ b/components/ecwid/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/ecwid", - "version": "0.0.9", + "version": "0.0.10", "description": "Pipedream Ecwid Components", "main": "dist/ecwid.app.mjs", "keywords": [ diff --git a/components/ecwid/sources/paid-orders/paid-orders.mjs b/components/ecwid/sources/paid-orders/paid-orders.mjs index 3e497db79e833..9d1fea402006e 100644 --- a/components/ecwid/sources/paid-orders/paid-orders.mjs +++ b/components/ecwid/sources/paid-orders/paid-orders.mjs @@ -4,7 +4,7 @@ import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; export default { name: "New Ecwid Paid Orders", - version: "0.0.3", + version: "0.0.4", key: "ecwid-paid-orders", description: "Search for new orders which are PAID and AWAITING_PROCESSING. Emits events for each order and" + " sets order fulfilment status to PROCESSING", diff --git a/components/edapp/README.md b/components/edapp/README.md index d5abbc43e6231..310c78723c2bb 100644 --- a/components/edapp/README.md +++ b/components/edapp/README.md @@ -1,11 +1,11 @@ # Overview -With EdApp, you can build interactive mobile learning experiences for your -employees or customers. +EdApp is a mobile-first microlearning platform with an API that enables programmatic interactions with its learning management system. With the EdApp API, you can automate course assignments, track learning progress, manage users, and pull detailed reports. This can significantly enhance learning experiences and administrative efficiency, particularly when integrated with Pipedream's serverless platform. Pipedream allows for the creation of custom workflows that leverage the EdApp API to interact with other services, inducing data-driven actions without manual intervention. -Some examples of what you can build with EdApp: +# Example Use Cases -- A training course on company policies and procedures -- An onboarding program for new employees -- A customer service course for call center agents -- A safety training program for manufacturing employees +- **Automated Course Enrollment Based on HR System Updates**: Whenever a new employee is added to the HR system, a Pipedream workflow triggers, automatically enrolling the new hire in relevant EdApp courses. + +- **Progress Tracking and Reporting**: Set up a Pipedream workflow that periodically fetches course completion data from EdApp and sends a comprehensive report to Slack, keeping the team updated on training progress. + +- **Course Completion and Certificate Generation**: Upon detection of a completed course in EdApp, a Pipedream workflow activates that generates a personalized certificate using a service like Canva or Google Slides and emails it to the learner. diff --git a/components/eden_ai/README.md b/components/eden_ai/README.md new file mode 100644 index 0000000000000..c2fd66c3fb8dc --- /dev/null +++ b/components/eden_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +Eden AI provides a simple API for integrating a variety of AI services from different providers. On Pipedream, you can leverage Eden AI to automate tasks that involve natural language processing, computer vision, and other AI capabilities. By connecting Eden AI with other apps, you can create powerful workflows for data enrichment, content moderation, or sentiment analysis without extensive AI expertise. + +# Example Use Cases + +- **Automated Content Moderation**: Use Eden AI to analyze user-generated content in real-time. When a new post is submitted to a platform like Reddit, trigger a Pipedream workflow that uses Eden AI to check for inappropriate content. If such content is detected, automatically flag the post and notify moderators. + +- **Sentiment Analysis for Customer Feedback**: Collect customer feedback via a form submission app like Typeform. When a new submission comes in, a Pipedream workflow can send the feedback to Eden AI for sentiment analysis. Based on the results, categorize the feedback as positive, negative, or neutral, and route it to the appropriate team or CRM like Salesforce. + +- **Image Recognition for Social Media**: With Eden AI, you can set up a workflow on Pipedream that monitors social media platforms like Twitter for brand mentions with images. When a new mention is found, analyze the image for logos or products using Eden AI's image recognition. Use this data to track brand presence or to engage with users posting about your brand. diff --git a/components/educateme/educateme.app.mjs b/components/educateme/educateme.app.mjs new file mode 100644 index 0000000000000..01fb72abfeade --- /dev/null +++ b/components/educateme/educateme.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "educateme", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/educateme/package.json b/components/educateme/package.json new file mode 100644 index 0000000000000..0730e5153d9ce --- /dev/null +++ b/components/educateme/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/educateme", + "version": "0.0.1", + "description": "Pipedream EducateMe Components", + "main": "educateme.app.mjs", + "keywords": [ + "pipedream", + "educateme" + ], + "homepage": "https://pipedream.com/apps/educateme", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/edusign/README.md b/components/edusign/README.md new file mode 100644 index 0000000000000..55282b70bb9b8 --- /dev/null +++ b/components/edusign/README.md @@ -0,0 +1,11 @@ +# Overview + +The Edusign API provides a suite of tools for managing electronic signatures and documents. Within Pipedream, you can use this API to automate workflows related to document preparation, signing processes, and post-signature tasks. By connecting to other apps available on Pipedream, such as CRMs, cloud storage, or communication tools, you can create seamless integrations for handling documents across various stages of your business processes. + +# Example Use Cases + +- **Automated Document Workflow Creation**: Upon receiving a new customer form submission from your website (e.g., via a Webhook), use the Edusign API to generate a signature request and send it to the customer. Once the document is signed, trigger an update in your CRM system with the signatory's data. + +- **Document Status Tracking**: Set up a workflow where you check the status of sent documents periodically. When a document is signed or if there's an update, use Pipedream to notify your team in Slack and archive the completed document in Google Drive. + +- **Onboarding Process Streamlining**: Automate your employee onboarding by triggering an Edusign signature request for employment contracts when a new hire is added to your HR system. Then, secure storage of the signed contracts can be ensured by uploading them to Dropbox and notifying the HR team through Microsoft Teams. diff --git a/components/edusign/package.json b/components/edusign/package.json index e57687d37d34b..63452d87b10e0 100644 --- a/components/edusign/package.json +++ b/components/edusign/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/efinder/README.md b/components/efinder/README.md new file mode 100644 index 0000000000000..184c979924c08 --- /dev/null +++ b/components/efinder/README.md @@ -0,0 +1,11 @@ +# Overview + +The efinder API allows you to search various databases for email addresses associated with a given domain. It's a tool tailored for those in sales, marketing, and recruitment who need to find contact information quickly and reliably. Integrating this API into Pipedream opens up possibilities for automating the process of lead generation, enriching contact lists, or syncing found emails to CRMs and other marketing tools. + +# Example Use Cases + +- **Lead Generation Automation**: Use efinder to search for emails by domain, then automatically add the found emails to a Google Sheets for tracking potential leads. Trigger this search by new domain entries in the sheet and keep your lead list growing without manual intervention. + +- **CRM Integration**: Integrate efinder with a CRM like HubSpot. Whenever a new domain is added to your sales pipeline, use efinder to find the emails and automatically populate the contact information in the CRM, ensuring your sales team has the latest info for outreach. + +- **Email Verification and Enrichment**: After collecting emails, use efinder in combination with an email verification service like Hunter to verify the emails and append additional information such as the contact's position or social media profiles. This workflow can enhance the quality of your contact lists and improve targeting for campaigns. diff --git a/components/efinder/package.json b/components/efinder/package.json index d60ee1df06b04..b77418cfcbb61 100644 --- a/components/efinder/package.json +++ b/components/efinder/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/egnyte/actions/create-folder/create-folder.mjs b/components/egnyte/actions/create-folder/create-folder.mjs new file mode 100644 index 0000000000000..bce22fdf880bc --- /dev/null +++ b/components/egnyte/actions/create-folder/create-folder.mjs @@ -0,0 +1,28 @@ +import egnyte from "../../egnyte.app.mjs"; + +export default { + key: "egnyte-create-folder", + name: "Create Folder", + description: "Creates a new folder in your Egnyte workspace. [See the documentation](https://developers.egnyte.com/docs/File_System_Management_API_Documentation#Create-a-Folder)", + version: "0.0.1", + type: "action", + props: { + egnyte, + folderPath: { + type: "string", + label: "Folder Path", + description: "The full path to the new folder. Example: `/Shared/test`", + }, + }, + async run({ $ }) { + const folderPath = this.folderPath[0] === "/" + ? this.folderPath.slice(1) + : this.folderPath; + const response = await this.egnyte.createFolder({ + $, + folderPath, + }); + $.export("$summary", `Created folder "${this.folderPath}"`); + return response; + }, +}; diff --git a/components/egnyte/actions/upload-file/upload-file.mjs b/components/egnyte/actions/upload-file/upload-file.mjs new file mode 100644 index 0000000000000..09b9c1afab861 --- /dev/null +++ b/components/egnyte/actions/upload-file/upload-file.mjs @@ -0,0 +1,60 @@ +import egnyte from "../../egnyte.app.mjs"; +import FormData from "form-data"; +import fs from "fs"; +import path from "path"; +import mime from "mime"; + +export default { + key: "egnyte-upload-file", + name: "Upload File", + description: "Uploads a file to a specified folder in Egnyte. [See the documentation](https://developers.egnyte.com/docs/File_System_Management_API_Documentation#Upload-a-File)", + version: "0.0.1", + type: "action", + props: { + egnyte, + filePath: { + type: "string", + label: "File Path", + description: "The path to a file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + }, + folderPath: { + type: "string", + label: "Folder Path", + description: "The full path to the folder where the file should be uploaded. Example: `/Shared/Documents", + }, + }, + async run({ $ }) { + const form = new FormData(); + + const filePath = this.filePath.includes("tmp/") + ? this.filePath + : `/tmp/${this.filePath}`; + + const filename = path.basename(filePath); + const contentType = mime.getType(filePath) || "application/octet-stream"; + + form.append("file", fs.createReadStream(filePath), { + filename, + contentType, + }); + + let folderPath = this.folderPath; + if (folderPath.startsWith("/")) { + folderPath = folderPath.slice(1); + } + if (folderPath.endsWith("/")) { + folderPath = folderPath.slice(0, -1); + } + + const response = await this.egnyte.uploadFile({ + $, + folderPath, + filename, + data: form, + headers: form.getHeaders(), + }); + + $.export("$summary", `Successfully uploaded file ${filename}`); + return response; + }, +}; diff --git a/components/egnyte/egnyte.app.mjs b/components/egnyte/egnyte.app.mjs new file mode 100644 index 0000000000000..220b1f4c16e87 --- /dev/null +++ b/components/egnyte/egnyte.app.mjs @@ -0,0 +1,59 @@ +import { axios } from "@pipedream/platform"; +import Bottleneck from "bottleneck"; +const limiter = new Bottleneck({ + minTime: 500, // 2 requests per second + maxConcurrent: 1, +}); +const axiosRateLimiter = limiter.wrap(axios); + +export default { + type: "app", + app: "egnyte", + methods: { + _baseUrl() { + return `https://${this.$auth.subdomain}.egnyte.com/pubapi/v1`; + }, + _makeRequest({ + $ = this, + path, + headers, + ...otherOpts + }) { + const config = { + url: `${this._baseUrl()}${path}`, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + ...otherOpts, + }; + return axiosRateLimiter($, config); + }, + getFolder({ + folderPath, ...opts + }) { + return this._makeRequest({ + path: `/fs/${folderPath}`, + ...opts, + }); + }, + createFolder({ folderPath }) { + return this._makeRequest({ + method: "POST", + path: `/fs/${folderPath}`, + data: { + action: "add_folder", + }, + }); + }, + uploadFile({ + folderPath, filename, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/fs-content/${folderPath}/${filename}`, + ...opts, + }); + }, + }, +}; diff --git a/components/egnyte/package.json b/components/egnyte/package.json new file mode 100644 index 0000000000000..d23f556e09fdd --- /dev/null +++ b/components/egnyte/package.json @@ -0,0 +1,22 @@ +{ + "name": "@pipedream/egnyte", + "version": "0.1.0", + "description": "Pipedream Egnyte Components", + "main": "egnyte.app.mjs", + "keywords": [ + "pipedream", + "egnyte" + ], + "homepage": "https://pipedream.com/apps/egnyte", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "bottleneck": "^2.19.5", + "form-data": "^4.0.1", + "mime": "^4.0.6", + "path": "^0.12.7" + } +} diff --git a/components/egnyte/sources/common/base.mjs b/components/egnyte/sources/common/base.mjs new file mode 100644 index 0000000000000..115064ffd2441 --- /dev/null +++ b/components/egnyte/sources/common/base.mjs @@ -0,0 +1,80 @@ +import egnyte from "../../egnyte.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + egnyte, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + folderPath: { + type: "string", + label: "Folder Path", + description: "The folder path (example: `/Shared/Documents`) to watch for updates.", + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + getResourceType() { + throw new Error("getResourceType is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run() { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + const resourceType = this.getResourceType(); + + // Recursively process folder and subfolders + const processFolder = async (folderPath) => { + const results = await this.egnyte.getFolder({ + folderPath, + params: { + sort_by: "last_modified", + sort_direction: "descending", + }, + }); + + const items = results[resourceType]; + if (!items?.length) { + return; + } + const newItems = []; + + for (const item of items) { + const ts = item.uploaded; + if (ts >= lastTs) { + newItems.push(item); + maxTs = Math.max(ts, maxTs); + } + } + + const folders = results.folders; + if (folders?.length) { + for (const folder of folders) { + await processFolder(folder.path); + } + } + + newItems.reverse().forEach((item) => { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }); + }; + + await processFolder(this.folderPath); + + this._setLastTs(maxTs); + }, +}; diff --git a/components/egnyte/sources/new-file-in-folder/new-file-in-folder.mjs b/components/egnyte/sources/new-file-in-folder/new-file-in-folder.mjs new file mode 100644 index 0000000000000..abb88b87f4f8d --- /dev/null +++ b/components/egnyte/sources/new-file-in-folder/new-file-in-folder.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "egnyte-new-file-in-folder", + name: "New File in Folder", + description: "Emit new event when a file is added within the specified folder in Egnyte. [See the documentation](https://developers.egnyte.com/docs/read/File_System_Management_API_Documentation#List-File-or-Folder)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceType() { + return "files"; + }, + generateMeta(file) { + return { + id: file.entry_id, + summary: `New file: ${file.name}`, + ts: file.uploaded, + }; + }, + }, +}; diff --git a/components/egnyte/sources/new-folder-added/new-folder-added.mjs b/components/egnyte/sources/new-folder-added/new-folder-added.mjs new file mode 100644 index 0000000000000..74b3f49a03afc --- /dev/null +++ b/components/egnyte/sources/new-folder-added/new-folder-added.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "egnyte-new-folder-added", + name: "New Folder", + description: "Emit new event when a folder is added within the specified folder in Egnyte. [See the documentation](https://developers.egnyte.com/docs/read/File_System_Management_API_Documentation#List-File-or-Folder).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceType() { + return "folders"; + }, + generateMeta(folder) { + return { + id: folder.folder_id, + summary: `New folder: ${folder.name}`, + ts: folder.uploaded, + }; + }, + }, +}; diff --git a/components/elastic_cloud/elastic_cloud.app.mjs b/components/elastic_cloud/elastic_cloud.app.mjs new file mode 100644 index 0000000000000..071ead5600f4e --- /dev/null +++ b/components/elastic_cloud/elastic_cloud.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "elastic_cloud", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/elastic_cloud/package.json b/components/elastic_cloud/package.json new file mode 100644 index 0000000000000..036d358228dc1 --- /dev/null +++ b/components/elastic_cloud/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/elastic_cloud", + "version": "0.0.1", + "description": "Pipedream Elastic Cloud Components", + "main": "elastic_cloud.app.mjs", + "keywords": [ + "pipedream", + "elastic_cloud" + ], + "homepage": "https://pipedream.com/apps/elastic_cloud", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/elastic_email/README.md b/components/elastic_email/README.md new file mode 100644 index 0000000000000..f31d44c1cad80 --- /dev/null +++ b/components/elastic_email/README.md @@ -0,0 +1,11 @@ +# Overview + +The Elastic Email API allows you to integrate a robust email sending platform into your Pipedream workflows. Use it to send emails, manage contacts, and track campaign statistics. With this API, you can automate email notifications, synchronize subscriber lists, or trigger marketing campaigns based on user actions. + +# Example Use Cases + +- **Automated Welcome Email**: Trigger a welcome email through Elastic Email when a new user signs up on your platform, using Pipedream's HTTP / Webhook trigger to start the workflow. + +- **Subscriber List Sync**: Maintain a synced subscriber list by updating Elastic Email contacts whenever a user updates their profile in your app, such as a CRM like Salesforce, which can be connected to Pipedream. + +- **Transactional Email Trigger**: Send a transactional email, like an order confirmation or password reset, from Elastic Email when an event occurs in an eCommerce platform like Shopify. diff --git a/components/elasticemail/README.md b/components/elasticemail/README.md new file mode 100644 index 0000000000000..93e70bbf157ec --- /dev/null +++ b/components/elasticemail/README.md @@ -0,0 +1,11 @@ +# Overview + +ElasticEmail API allows you to integrate powerful email sending and management capabilities into your applications. With it, you can send emails, manage contacts, create and manage campaigns, and track the results of your email marketing efforts. In Pipedream, you can harness this API to automate email workflows, synchronize data with other apps, or react to events with customized emails, all within a serverless environment that scales with your needs. + +# Example Use Cases + +- **Automated Welcome Emails**: Set up an event-driven workflow in Pipedream that triggers when a new user signs up on your platform. Use the ElasticEmail API to send a personalized welcome email, ensuring a warm onboarding experience. + +- **Scheduled Campaign Reporting**: Build a Pipedream workflow that runs on a schedule, say weekly, to fetch campaign statistics from ElasticEmail. Combine this data with Pipedream's built-in code steps to analyze performance, then send a report to a Slack channel or save it to Google Sheets for easy sharing and access. + +- **Dynamic List Management**: Create a workflow triggered by a webhook that monitors user activity. Depending on the behavior, such as purchasing a product or signing up for a newsletter, use the ElasticEmail API within the workflow to add or update user details in the appropriate contact list or segment, keeping your marketing lists up-to-date automatically. diff --git a/components/elasticemail/package.json b/components/elasticemail/package.json index c679de4b99309..6137c0b361617 100644 --- a/components/elasticemail/package.json +++ b/components/elasticemail/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/elevenlabs/README.md b/components/elevenlabs/README.md new file mode 100644 index 0000000000000..0e562d1f8f574 --- /dev/null +++ b/components/elevenlabs/README.md @@ -0,0 +1,11 @@ +# Overview + +The ElevenLabs API offers text-to-speech capabilities with realistic voice synthesis. Integrating this API on Pipedream allows you to build automated workflows that convert text content into spoken audio files. You can trigger these conversions from various events, process the text data, send it to the ElevenLabs API, and handle the audio output—all within a serverless environment. + +# Example Use Cases + +- **Automated Content Narration**: Convert blog posts to audio as they are published. Using a webhook to detect new content, Pipedream can send the text to ElevenLabs, and then store or distribute the audio file, possibly integrating with a CMS or a cloud storage service like Dropbox. + +- **Real-Time Audio Messaging**: Generate voice messages from chat inputs. With Pipedream, you can listen for messages on platforms like Slack, pass them through ElevenLabs to create audio, and then post the result back into the chat, offering an alternative for consuming text content. + +- **Voice-Enabled Alerts**: Send spoken alerts based on triggered events. For instance, if your application's health monitoring system detects an issue, Pipedream can fetch the details, run them through ElevenLabs to synthesize a voice alert, and then use Twilio to send a voice message to your phone. diff --git a/components/elevio/actions/create-article/create-article.mjs b/components/elevio/actions/create-article/create-article.mjs new file mode 100644 index 0000000000000..c2d48729094a7 --- /dev/null +++ b/components/elevio/actions/create-article/create-article.mjs @@ -0,0 +1,128 @@ +import app from "../../elevio.app.mjs"; + +export default { + key: "elevio-create-article", + name: "Create Article", + description: "Creates a new article in the Elevio knowledge base. [See the documentation](https://api-docs.elevio.help/en/articles/71-rest-api-articles).", + version: "0.0.1", + type: "action", + props: { + app, + categoryId: { + propDefinition: [ + app, + "categoryId", + ], + }, + restriction: { + propDefinition: [ + app, + "restriction", + ], + }, + discoverable: { + propDefinition: [ + app, + "discoverable", + ], + }, + isInternal: { + propDefinition: [ + app, + "isInternal", + ], + }, + notes: { + propDefinition: [ + app, + "notes", + ], + }, + status: { + propDefinition: [ + app, + "status", + ], + }, + title: { + propDefinition: [ + app, + "title", + ], + }, + body: { + propDefinition: [ + app, + "body", + ], + }, + keywords: { + propDefinition: [ + app, + "keywords", + ], + }, + tags: { + propDefinition: [ + app, + "tags", + ], + }, + externalId: { + propDefinition: [ + app, + "externalId", + ], + }, + }, + methods: { + createArticle(args = {}) { + return this.app.post({ + path: "/articles", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createArticle, + externalId, + restriction, + discoverable, + isInternal, + notes, + status, + title, + body, + keywords, + tags, + categoryId, + } = this; + const response = await createArticle({ + $, + data: { + article: { + external_id: externalId, + restriction, + discoverable, + is_internal: isInternal, + notes, + status, + category_id: categoryId, + keywords, + tags, + translations: [ + { + language_id: "en", + title, + body, + }, + ], + }, + }, + }); + + $.export("$summary", `Successfully created article with ID \`${response.article.id}\`.`); + return response; + }, +}; diff --git a/components/elevio/actions/delete-article/delete-article.mjs b/components/elevio/actions/delete-article/delete-article.mjs new file mode 100644 index 0000000000000..c1dddbe32577a --- /dev/null +++ b/components/elevio/actions/delete-article/delete-article.mjs @@ -0,0 +1,41 @@ +import app from "../../elevio.app.mjs"; + +export default { + key: "elevio-delete-article", + name: "Delete Article", + description: "Deletes an existing article from the Elevio knowledge base. [See the documentation](https://api-docs.elevio.help/en/articles/71-rest-api-articles).", + version: "0.0.1", + type: "action", + props: { + app, + articleId: { + propDefinition: [ + app, + "articleId", + ], + }, + }, + methods: { + deleteArticle({ + articleId, ...args + }) { + return this.app.delete({ + path: `/articles/${articleId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + deleteArticle, + articleId, + } = this; + + const response = await deleteArticle({ + $, + articleId, + }); + $.export("$summary", "Successfully deleted article."); + return response; + }, +}; diff --git a/components/elevio/actions/update-article/update-article.mjs b/components/elevio/actions/update-article/update-article.mjs new file mode 100644 index 0000000000000..1839f17903a37 --- /dev/null +++ b/components/elevio/actions/update-article/update-article.mjs @@ -0,0 +1,149 @@ +import app from "../../elevio.app.mjs"; + +export default { + key: "elevio-update-article", + name: "Update Article", + description: "Updates an existing article in the Elevio knowledge base. [See the documentation](https://api-docs.elevio.help/en/articles/71-rest-api-articles).", + version: "0.0.1", + type: "action", + props: { + app, + articleId: { + propDefinition: [ + app, + "articleId", + ], + }, + categoryId: { + propDefinition: [ + app, + "categoryId", + ], + }, + restriction: { + optional: true, + propDefinition: [ + app, + "restriction", + ], + }, + discoverable: { + optional: true, + propDefinition: [ + app, + "discoverable", + ], + }, + isInternal: { + optional: true, + propDefinition: [ + app, + "isInternal", + ], + }, + notes: { + optional: true, + propDefinition: [ + app, + "notes", + ], + }, + status: { + optional: true, + propDefinition: [ + app, + "status", + ], + }, + title: { + optional: true, + propDefinition: [ + app, + "title", + ], + }, + body: { + optional: true, + propDefinition: [ + app, + "body", + ], + }, + keywords: { + propDefinition: [ + app, + "keywords", + ], + }, + tags: { + propDefinition: [ + app, + "tags", + ], + }, + externalId: { + propDefinition: [ + app, + "externalId", + ], + }, + }, + methods: { + updateArticle({ + articleId, ...args + } = {}) { + return this.app.put({ + path: `/articles/${articleId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + updateArticle, + articleId, + externalId, + restriction, + discoverable, + isInternal, + notes, + status, + title, + body, + keywords, + tags, + categoryId, + } = this; + const response = await updateArticle({ + $, + articleId, + data: { + article: { + external_id: externalId, + restriction, + discoverable, + is_internal: isInternal, + notes, + status, + keywords, + tags, + category_id: categoryId, + ...((title || body) + ? { + translations: [ + { + language_id: "en", + title, + body, + }, + ], + } + : {}), + }, + }, + }); + + $.export("$summary", `Successfully updated article with ID \`${response.article.id}\`.`); + return response; + }, +}; diff --git a/components/elevio/common/constants.mjs b/components/elevio/common/constants.mjs new file mode 100644 index 0000000000000..ae8614c5a8fd5 --- /dev/null +++ b/components/elevio/common/constants.mjs @@ -0,0 +1,15 @@ +const BASE_URL = "https://api.elev.io"; +const VERSION_PATH = "/v1"; +const IS_FIRST_RUN = "isFirstRun"; +const LAST_DATE_AT = "lastDateAt"; +const DEFAULT_LIMIT = 100; +const DEFAULT_MAX = 600; + +export default { + BASE_URL, + VERSION_PATH, + IS_FIRST_RUN, + LAST_DATE_AT, + DEFAULT_LIMIT, + DEFAULT_MAX, +}; diff --git a/components/elevio/common/utils.mjs b/components/elevio/common/utils.mjs new file mode 100644 index 0000000000000..de7ee6c4a4692 --- /dev/null +++ b/components/elevio/common/utils.mjs @@ -0,0 +1,17 @@ +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +function getNestedProperty(obj, propertyString) { + const properties = propertyString.split("."); + return properties.reduce((prev, curr) => prev?.[curr], obj); +} + +export default { + iterate, + getNestedProperty, +}; diff --git a/components/elevio/elevio.app.mjs b/components/elevio/elevio.app.mjs new file mode 100644 index 0000000000000..6dbde8783ac30 --- /dev/null +++ b/components/elevio/elevio.app.mjs @@ -0,0 +1,225 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; + +export default { + type: "app", + app: "elevio", + propDefinitions: { + articleId: { + type: "string", + label: "Article ID", + description: "The identifier of the article", + async options({ page }) { + const { articles } = await this.listArticles({ + params: { + page: page + 1, + }, + }); + return articles.map(({ + id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + restriction: { + type: "string", + label: "Restriction", + description: "Restriction level of the article", + default: "unrestricted", + options: [ + "restricted", + "unrestricted", + "noaccess", + ], + }, + discoverable: { + type: "boolean", + label: "Discoverable", + description: "Whether the article is discoverable", + default: true, + }, + isInternal: { + type: "boolean", + label: "Internal", + description: "Whether the article is internal", + default: false, + }, + notes: { + type: "string", + label: "Notes", + description: "Notes associated with the article", + }, + status: { + type: "string", + label: "Status", + description: "Status of the article", + default: "published", + options: [ + "draft", + "published", + ], + }, + title: { + type: "string", + label: "Title", + description: "The title of the article", + }, + body: { + type: "string", + label: "Content", + description: "The content body of the article", + }, + categoryId: { + type: "string", + label: "Category ID", + description: "The category ID for the article", + async options() { + const { categories } = await this.listCategories(); + return categories.map(({ + id: value, translations, + }) => ({ + label: translations[0].title, + value, + })); + }, + }, + externalId: { + type: "string", + label: "External ID", + description: "The external identifier of the article", + optional: true, + }, + keywords: { + type: "string[]", + label: "Keywords", + description: "List of global keywords for tagging and discoverability.", + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "Tags associated with this article.", + optional: true, + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + const { + api_token: apiToken, + api_key: apiKey, + } = this.$auth; + return { + "Authorization": `Bearer ${apiToken}`, + "x-api-key": apiKey, + "Content-Type": "application/json", + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + put(args = {}) { + return this._makeRequest({ + method: "PUT", + ...args, + }); + }, + listCategories(args = {}) { + return this._makeRequest({ + path: "/categories", + ...args, + }); + }, + listArticles(args = {}) { + return this._makeRequest({ + path: "/articles", + ...args, + }); + }, + listCards({ + folder, ...args + } = {}) { + return this._makeRequest({ + path: `/cards/folders/${folder}`, + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, resourceName, + lastDateAt, dateField, + max = constants.DEFAULT_MAX, + }) { + let page = 1; + let resourcesCount = 0; + + while (true) { + const response = + await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + page, + page_size: constants.DEFAULT_LIMIT, + }, + }); + + const nextResources = utils.getNestedProperty(response, resourceName); + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + const isDateGreater = + lastDateAt + && Date.parse(resource[dateField]) > Date.parse(lastDateAt); + + if (!lastDateAt || isDateGreater) { + yield resource; + resourcesCount += 1; + } + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + if (nextResources.length < constants.DEFAULT_LIMIT) { + console.log("No next page found"); + return; + } + + page += 1; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); + }, + }, +}; diff --git a/components/elevio/package.json b/components/elevio/package.json new file mode 100644 index 0000000000000..9b8949c35ffcc --- /dev/null +++ b/components/elevio/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/elevio", + "version": "0.1.0", + "description": "Pipedream Elevio Components", + "main": "elevio.app.mjs", + "keywords": [ + "pipedream", + "elevio" + ], + "homepage": "https://pipedream.com/apps/elevio", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/elevio/sources/common/polling.mjs b/components/elevio/sources/common/polling.mjs new file mode 100644 index 0000000000000..06208d5e3c029 --- /dev/null +++ b/components/elevio/sources/common/polling.mjs @@ -0,0 +1,130 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../elevio.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + deploy() { + this.setIsFirstRun(true); + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + setIsFirstRun(value) { + this.db.set(constants.IS_FIRST_RUN, value); + }, + getIsFirstRun() { + return this.db.get(constants.IS_FIRST_RUN); + }, + setLastDateAt(value) { + this.db.set(constants.LAST_DATE_AT, value); + }, + getLastDateAt() { + return this.db.get(constants.LAST_DATE_AT); + }, + getDateField() { + throw new ConfigurationError("getDateField is not implemented"); + }, + sortFn() { + return; + }, + isResourceRelevant() { + return true; + }, + getResourceName() { + throw new ConfigurationError("getResourceName is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + async processResources(resources) { + const { + isResourceRelevant, + processResource, + } = this; + + return Array.from(resources) + .reverse() + .filter(isResourceRelevant) + .forEach(processResource); + }, + }, + async run() { + const { + app, + getDateField, + getLastDateAt, + getResourcesFn, + getResourcesFnArgs, + getResourceName, + processResources, + getIsFirstRun, + setIsFirstRun, + setLastDateAt, + sortFn, + } = this; + + const isFirstRun = getIsFirstRun(); + const dateField = getDateField(); + const lastDateAt = getLastDateAt(); + + const otherArgs = isFirstRun + ? { + max: constants.DEFAULT_LIMIT, + } + : { + dateField, + lastDateAt, + }; + + const resources = await app.paginate({ + resourcesFn: getResourcesFn(), + resourcesFnArgs: getResourcesFnArgs(), + resourceName: getResourceName(), + ...otherArgs, + }); + + const resourcesInDescendingOrder = + Array.from(resources) + .sort(sortFn); + + if (resourcesInDescendingOrder.length) { + const [ + firstResource, + ] = Array.from(resourcesInDescendingOrder); + if (firstResource) { + setLastDateAt(firstResource[dateField]); + } + } + + processResources(resourcesInDescendingOrder); + + if (isFirstRun) { + setIsFirstRun(false); + } + }, +}; diff --git a/components/elevio/sources/new-article-created/new-article-created.mjs b/components/elevio/sources/new-article-created/new-article-created.mjs new file mode 100644 index 0000000000000..be7878c105d7d --- /dev/null +++ b/components/elevio/sources/new-article-created/new-article-created.mjs @@ -0,0 +1,40 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "elevio-new-article-created", + name: "New Article Created", + description: "Emit new event any time a new article is created. [See the documentation](https://api-docs.elevio.help/en/articles/71-rest-api-articles).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + sortFn(a, b) { + return new Date(b.created_at) - new Date(a.created_at); + }, + getDateField() { + return "created_at"; + }, + getResourceName() { + return "articles"; + }, + getResourcesFn() { + return this.app.listArticles; + }, + getResourcesFnArgs() { + return { + debug: true, + }; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Article: ${resource.title}`, + ts: Date.parse(resource.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/elevio/sources/new-article-created/test-event.mjs b/components/elevio/sources/new-article-created/test-event.mjs new file mode 100644 index 0000000000000..39e3dcf542492 --- /dev/null +++ b/components/elevio/sources/new-article-created/test-event.mjs @@ -0,0 +1,56 @@ +export default { + "access": "public", + "access_domains": [], + "access_emails": [], + "article_groups": [ + "allizwell" + ], + "author": { + "email": "user@pipedream.com", + "gravatar": "https://www.gravatar.com/avatar/4acf7f734332df6f6a65789330874e23?e=user@pipedream.com&s=300&d=https%3A%2F%2Fui-avatars.com%2Fapi%2F/user%2Btest/300/69D59A/FFF/2/0.40", + "id": 70369, + "name": "user test" + }, + "category_groups": [ + "allizwell" + ], + "category_id": 2, + "created_at": "2025-02-06T21:23:20Z", + "creator": { + "email": "user@pipedream.com", + "gravatar": "https://www.gravatar.com/avatar/4acf7f734332df6f6a65789330874e23?e=user@pipedream.com&s=300&d=https%3A%2F%2Fui-avatars.com%2Fapi%2F/user%2Btest/300/69D59A/FFF/2/0.40", + "id": 70369, + "name": "user test" + }, + "discoverable": true, + "editor_version": "3", + "external_id": null, + "has_revision": false, + "id": 13, + "is_internal": false, + "keywords": [], + "last_published_at": "2025-02-06T21:23:42Z", + "notes": null, + "order": 999, + "pinned": false, + "restriction": "unrestricted", + "revision_status": null, + "slug": "test-6", + "smart_groups": [], + "source": "custom", + "status": "published", + "subcategory_groups": [ + "allizwell" + ], + "tags": [], + "title": "test 6", + "translations": [ + { + "id": 17, + "language_id": "en", + "title": "test 6", + "updated_at": "2025-02-06T21:23:42Z" + } + ], + "updated_at": "2025-02-06T21:23:42Z" +}; diff --git a/components/elevio/sources/new-feedback-created/new-feedback-created.mjs b/components/elevio/sources/new-feedback-created/new-feedback-created.mjs new file mode 100644 index 0000000000000..0cef86a01285a --- /dev/null +++ b/components/elevio/sources/new-feedback-created/new-feedback-created.mjs @@ -0,0 +1,57 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "elevio-new-feedback-created", + name: "New Feedback Created", + description: "Emit new event each time new feedback is submitted by a user via the elevio widget. [See the documentation](https://api-docs.elevio.help/en/articles/85-rest-api-hub-cards).", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + folder: { + type: "string", + label: "Folder", + description: "The folder to which the feedback belongs.", + options: [ + "inbox", + "completed", + "archive", + ], + }, + }, + methods: { + ...common.methods, + sortFn(a, b) { + return new Date(b.created_at) - new Date(a.created_at); + }, + isResourceRelevant(resource) { + return resource.type?.includes("feedback"); + }, + getDateField() { + return "created_at"; + }, + getResourceName() { + return "cards"; + }, + getResourcesFn() { + return this.app.listCards; + }, + getResourcesFnArgs() { + return { + debug: true, + folder: this.folder, + }; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Feedback: ${resource.title}`, + ts: Date.parse(resource.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/elevio/sources/new-feedback-created/test-event.mjs b/components/elevio/sources/new-feedback-created/test-event.mjs new file mode 100644 index 0000000000000..baa0cf89296d0 --- /dev/null +++ b/components/elevio/sources/new-feedback-created/test-event.mjs @@ -0,0 +1,36 @@ +export default { + "id": 1, + "type": "feedback_text", + "folder": "inbox", + "title": "Syncing articles", + "description": "This article is incomplete. Please fix.", + "relates_to_type": "article", + "relates_to_id": 42, + "custom_attributes": { "user_email": "roseanne@example.com" }, + "assignees": [ + { + "id": 1, + "email": "agent.amber@example.com", + "name": "Agent Amber", + "gravatar": "https://gravatar.com/avatar/abc123", + } + ], + "notes": "Add more details on different types of sync.", + "created_at": "2018-05-28T01:45:17.000000Z", + "updated_at": "2018-05-28T01:45:35.000000Z", + "last_updated_by": + { + "id": 1, + "email": "agent.amber@example.com", + "name": "Agent Amber", + "gravatar": "https://gravatar.com/avatar/abc123", + }, + "last_status_updated_by": + { + "id": 1, + "email": "agent.amber@example.com", + "name": "Agent Amber", + "gravatar": "https://gravatar.com/avatar/abc123", + }, + "last_status_update_at": "2018-05-28T01:45:35.000000Z" +}; diff --git a/components/elmah_io/README.md b/components/elmah_io/README.md index d98a1f6daa1c0..383b26f4298cd 100644 --- a/components/elmah_io/README.md +++ b/components/elmah_io/README.md @@ -1,8 +1,11 @@ # Overview -Elmah.io provides an API that allows developers to integrate their applications -with the elmah.io service. This lets developers easily view and manage their -application's error logs from one central location. Elmah.io also provides a -number of features that are not available through the standard Elmah UI, such -as filtering and searching of logs, custom dashboards, and integrations with a -number of third-party services. +The elmah.io API allows developers to automate error logging and management in their applications. By using this API, you can create robust monitoring systems that capture, analyze, and notify you of any application errors in real-time. Integrating elmah.io with Pipedream opens up possibilities for streamlining incident responses, correlating errors with system metrics, and improving application stability through automated workflows. With Pipedream's serverless platform, you can connect elmah.io to numerous other services to enhance your error management processes. + +# Example Use Cases + +- **Automated Error Notification to Communication Channels**: Integrate elmah.io with messaging platforms like Slack or Microsoft Teams using Pipedream. Set up a workflow to automatically post error notifications from elmah.io into a designated channel, ensuring that your development team is immediately informed of issues for a quicker response and resolution. + +- **Error-Triggered Issue Tracking Integration**: Connect elmah.io to issue tracking systems such as Jira or GitHub Issues on Pipedream. Whenever a new error is logged, a workflow creates a corresponding issue in your tracker, including error details. This helps to prioritize bug fixing and ensures that no error gets overlooked. + +- **Auto-remediation Workflows Based on Error Patterns**: Use Pipedream to analyze error patterns from elmah.io and trigger auto-remediation tasks. For instance, if a specific error occurs repeatedly within a short time, Pipedream can call an API to restart a service or server, or it could run a script to clear cache, aiming to resolve the issue before it escalates. diff --git a/components/elopage/README.md b/components/elopage/README.md index 93cdb8b57c36c..3afef8ece7bc6 100644 --- a/components/elopage/README.md +++ b/components/elopage/README.md @@ -1,17 +1,11 @@ # Overview -With elopage you can create and sell: +The elopage API allows you to automate processes around selling digital products, courses, memberships, and tickets. It provides endpoints to manage products, payments, users, and subscriptions. With Pipedream's serverless workflows, you can construct automations that respond to elopage events or orchestrate complex tasks involving multiple systems, streamlining your digital commerce operations. -- Ebooks -- Online courses -- Membership sites -- Digital downloads -- One-time products +# Example Use Cases -Plus, you can use elopage's powerful API to: +- **Automated Customer Onboarding**: Trigger a workflow when a new purchase is made on elopage. The Pipedream workflow can then enroll the customer in an email marketing campaign, send personalized welcome messages, and unlock access to digital content or courses within your system. -- Create products -- Manage users -- Create coupons -- Generate pdf certificates -- And much more! +- **Real-Time Sales Notifications**: Set up a Pipedream workflow that listens for successful transactions on elopage. Whenever a sale occurs, post a real-time notification to a Slack channel or send an SMS via Twilio, keeping your team informed about new business wins. + +- **Subscription Management and Analytics**: Implement a workflow that monitors subscription changes, such as renewals and cancellations, on elopage. Connect this data to a Google Sheets or a dashboard app like Geckoboard, providing up-to-date analytics and insights into recurring revenue and customer churn. diff --git a/components/elorus/README.md b/components/elorus/README.md new file mode 100644 index 0000000000000..f7fda86eeeefa --- /dev/null +++ b/components/elorus/README.md @@ -0,0 +1,11 @@ +# Overview + +The Elorus API allows you to automate and integrate your billing, invoicing, and financial management tasks. By connecting Elorus to Pipedream, you can harness its capabilities to streamline processes like invoice creation, payment tracking, and financial reporting. With Pipedream’s serverless platform, you can set up workflows that respond to events in real-time, interact with other APIs, and automate repetitive tasks without the need to manage infrastructure. + +# Example Use Cases + +- **Automated Invoice Processing**: Integrate Elorus with your e-commerce platform. When a new order is placed, trigger a workflow to create and send an invoice through Elorus automatically, ensuring timely billing with minimal manual intervention. + +- **Payment Status Monitoring**: Set up a workflow that checks for updated payment statuses on invoices in Elorus. Upon detection of a payment, send a notification via Slack or email to relevant teams, keeping everyone in the loop regarding the company's cash flow in real-time. + +- **Expense Tracking and Reporting**: Combine Elorus with Google Sheets to maintain a real-time expense report. Whenever a new expense is recorded in Elorus, append the details to a Google Sheet for simplified tracking and analysis, facilitating better financial decision-making. diff --git a/components/elorus/elorus.app.mjs b/components/elorus/elorus.app.mjs new file mode 100644 index 0000000000000..3713ce01c5bfb --- /dev/null +++ b/components/elorus/elorus.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "elorus", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/elorus/package.json b/components/elorus/package.json new file mode 100644 index 0000000000000..470bacd6ba693 --- /dev/null +++ b/components/elorus/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/elorus", + "version": "0.0.1", + "description": "Pipedream Elorus Components", + "main": "elorus.app.mjs", + "keywords": [ + "pipedream", + "elorus" + ], + "homepage": "https://pipedream.com/apps/elorus", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/email/package.json b/components/email/package.json new file mode 100644 index 0000000000000..b638260cd27de --- /dev/null +++ b/components/email/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/email", + "version": "0.6.0", + "description": "Pipedream email Components", + "main": "email.app.mjs", + "keywords": [ + "pipedream", + "email" + ], + "homepage": "https://pipedream.com/apps/email", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/email_verifier_api/README.md b/components/email_verifier_api/README.md new file mode 100644 index 0000000000000..c202b31c36199 --- /dev/null +++ b/components/email_verifier_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Email Verifier API provides a means to validate email addresses in real-time, checking their existence, validity, and quality. By integrating this API with Pipedream, you can automate workflows that improve your email marketing efforts, maintain a clean email list, and enhance user verification processes. Using Pipedream's serverless platform, you can connect the Email Verifier API with numerous other services to create custom, event-driven workflows. + +# Example Use Cases + +- **User Registration Validation**: Automatically verify email addresses when users sign up on your platform. Connect the Email Verifier API to your user registration process to ensure the emails collected are valid, reducing bounce rates and improving communication. + +- **Email List Cleaning**: Schedule a regular workflow to clean your email marketing lists. Use the Email Verifier API to filter out invalid emails, keeping your engagement rates high and protecting your sender reputation. + +- **Lead Verification in CRM**: Integrate the Email Verifier API with your Customer Relationship Management (CRM) system. Whenever a new lead is added, automatically verify their email address, ensuring your sales team focuses on high-quality leads. diff --git a/components/email_verifier_api/actions/verify-email/verify-email.mjs b/components/email_verifier_api/actions/verify-email/verify-email.mjs new file mode 100644 index 0000000000000..1a356cea1e7cb --- /dev/null +++ b/components/email_verifier_api/actions/verify-email/verify-email.mjs @@ -0,0 +1,25 @@ +import emailVerifierApi from "../../email_verifier_api.app.mjs"; + +export default { + key: "email_verifier_api-verify-email", + name: "Verify Email", + description: "Verify an email address with Email Verifier API. [See the documentation](https://www.emailverifierapi.com/app/v2-api-documentation/)", + version: "0.0.1", + type: "action", + props: { + emailVerifierApi, + email: { + type: "string", + label: "Email", + description: "The email address to verify", + }, + }, + async run({ $ }) { + const response = await this.emailVerifierApi.verifyEmail({ + $, + email: this.email, + }); + $.export("$summary", `Successfully retrieved verification data for email address \`${this.email}\``); + return response; + }, +}; diff --git a/components/email_verifier_api/email_verifier_api.app.mjs b/components/email_verifier_api/email_verifier_api.app.mjs new file mode 100644 index 0000000000000..679b00cba604e --- /dev/null +++ b/components/email_verifier_api/email_verifier_api.app.mjs @@ -0,0 +1,36 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "email_verifier_api", + propDefinitions: {}, + methods: { + _baseUrl() { + return "https://emailverifierapi.com/v2/"; + }, + _makeRequest({ + $ = this, + params, + ...opts + }) { + return axios($, { + url: this._baseUrl(), + params: { + ...params, + apiKey: this.$auth.api_key, + }, + ...opts, + }); + }, + verifyEmail({ + $, email, + }) { + return this._makeRequest({ + $, + params: { + email, + }, + }); + }, + }, +}; diff --git a/components/email_verifier_api/package.json b/components/email_verifier_api/package.json new file mode 100644 index 0000000000000..7e3ed1ab8ff4b --- /dev/null +++ b/components/email_verifier_api/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/email_verifier_api", + "version": "0.1.0", + "description": "Pipedream Email Verifier Api Components", + "main": "email_verifier_api.app.mjs", + "keywords": [ + "pipedream", + "email_verifier_api" + ], + "homepage": "https://pipedream.com/apps/email_verifier_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/emailable/README.md b/components/emailable/README.md new file mode 100644 index 0000000000000..580e1b7e28d55 --- /dev/null +++ b/components/emailable/README.md @@ -0,0 +1,11 @@ +# Overview + +The Emailable API offers robust email verification services, allowing you to improve deliverability and protect your sender reputation by validating email addresses in real-time. With Pipedream, you can integrate the Emailable API into serverless workflows, automating the process of cleaning your mailing lists, triggering actions based on email verification results, and syncing data across multiple platforms. + +# Example Use Cases + +- **Email Verification for New Subscribers**: When a new user subscribes via a web form, automatically verify their email address with Emailable to ensure it's valid before adding them to your email marketing platform or database. + +- **Scheduled List Cleaning**: Set up a scheduled workflow to regularly validate email addresses on your mailing lists. Connect Emailable with your email marketing service like Mailchimp or SendGrid to remove or tag invalid emails, keeping your lists up-to-date and reducing bounce rates. + +- **User Signup Validation**: Integrate Emailable with a user registration system. When a new user signs up, instantly verify their email with Emailable. If the email is invalid, trigger an immediate alert or prompt the user for an alternative email address, ensuring only valid users are onboarded. diff --git a/components/emaillistverify/actions/find-email/find-email.mjs b/components/emaillistverify/actions/find-email/find-email.mjs new file mode 100644 index 0000000000000..1fe0c12e33e4e --- /dev/null +++ b/components/emaillistverify/actions/find-email/find-email.mjs @@ -0,0 +1,44 @@ +import app from "../../emaillistverify.app.mjs"; + +export default { + key: "emaillistverify-find-email", + name: "Find Email", + description: "Generate a series of potential email addresses by synthesizing first names, last names, and company domains. [See the documentation](https://emaillistverify.com/docs/#tag/Email-Validation-API/operation/find-contact)", + version: "0.0.1", + type: "action", + props: { + app, + firstName: { + propDefinition: [ + app, + "firstName", + ], + }, + lastName: { + propDefinition: [ + app, + "lastName", + ], + }, + domain: { + propDefinition: [ + app, + "domain", + ], + }, + }, + async run({ $ }) { + const response = await this.app.findEmail({ + $, + data: { + first_name: this.firstName, + last_name: this.lastName, + domain: this.domain, + }, + }); + + $.export("$summary", `Successfully generated ${response.length} email addresses`); + + return response; + }, +}; diff --git a/components/emaillistverify/actions/verify-email/verify-email.mjs b/components/emaillistverify/actions/verify-email/verify-email.mjs new file mode 100644 index 0000000000000..3f81b1e302725 --- /dev/null +++ b/components/emaillistverify/actions/verify-email/verify-email.mjs @@ -0,0 +1,29 @@ +import app from "../../emaillistverify.app.mjs"; + +export default { + key: "emaillistverify-verify-email", + name: "Verify Email", + description: "Verify an email. [See the documentation](https://emaillistverify.com/docs/#tag/Email-Validation-API/operation/verifyEmail)", + version: "0.0.1", + type: "action", + props: { + app, + email: { + propDefinition: [ + app, + "email", + ], + }, + }, + async run({ $ }) { + const response = await this.app.verifyEmail({ + params: { + email: this.email, + }, + }); + + $.export("$summary", `Successfully verified the email. Result: '${response}'`); + + return response; + }, +}; diff --git a/components/emaillistverify/emaillistverify.app.mjs b/components/emaillistverify/emaillistverify.app.mjs new file mode 100644 index 0000000000000..450ec0e11e07a --- /dev/null +++ b/components/emaillistverify/emaillistverify.app.mjs @@ -0,0 +1,62 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "emaillistverify", + propDefinitions: { + email: { + type: "string", + label: "Email", + description: "Email to be verified", + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the contact", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the contact", + }, + domain: { + type: "string", + label: "Email domain", + description: "The domain to use for generating email addresses, i.e.: `gmail.com`", + }, + }, + methods: { + _baseUrl() { + return "https://apps.emaillistverify.com/api"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + params: { + ...params, + secret: `${this.$auth.api_key}`, + }, + }); + }, + async verifyEmail(args = {}) { + return this._makeRequest({ + path: "/verifyEmail", + ...args, + }); + }, + async findEmail(args = {}) { + return this._makeRequest({ + method: "post", + path: "/find-contact", + ...args, + }); + }, + }, +}; diff --git a/components/emaillistverify/package.json b/components/emaillistverify/package.json new file mode 100644 index 0000000000000..ad22d5ac91045 --- /dev/null +++ b/components/emaillistverify/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/emaillistverify", + "version": "0.1.0", + "description": "Pipedream EmailListVerify Components", + "main": "emaillistverify.app.mjs", + "keywords": [ + "pipedream", + "emaillistverify" + ], + "homepage": "https://pipedream.com/apps/emaillistverify", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/emailoctopus/README.md b/components/emailoctopus/README.md index 9f5fced2e574a..9fc10c03aa5b2 100644 --- a/components/emailoctopus/README.md +++ b/components/emailoctopus/README.md @@ -1,8 +1,11 @@ # Overview -The Emailoctopus API allows you to send bulk emails, track email opens and -clicks, and manage your Email octopus account. You can use the API to: +EmailOctopus is a cost-effective email marketing tool that lets you send emails and manage subscribers with ease. Using the EmailOctopus API on Pipedream, you can automate subscriber management, trigger email campaigns based on various actions, or synchronize your marketing efforts across different platforms. The API's capabilities facilitate a wide range of marketing workflows, from auto-updating mailing lists to sending targeted emails when specific conditions are met. -- Send bulk emails -- Track email opens and clicks -- Manage your Email octopus account +# Example Use Cases + +- **Subscriber Lifecycle Management**: Automatically add new subscribers to your EmailOctopus lists when they sign up via your website form, which could be managed by a service like Formstack or Typeform. Conversely, you could also listen for unsubscribes on EmailOctopus and update your CRM, such as Salesforce, to maintain data consistency. + +- **Behavior-Triggered Email Campaigns**: Integrate EmailOctopus with e-commerce platforms like Shopify to send personalized email campaigns based on customer behavior. For instance, trigger a welcome series when a customer creates an account or a re-engagement campaign if they haven't made a purchase in a specific time frame. + +- **Synchronize Email Campaigns with Social Media**: Use Pipedream to connect EmailOctopus with social media management tools like Buffer or Hootsuite. When you launch an email campaign, simultaneously post on social media to maximize your campaign's reach. If your email content includes a promotion, auto-tweet or post about it to drive multi-channel engagement. diff --git a/components/emelia/README.md b/components/emelia/README.md new file mode 100644 index 0000000000000..fcb9d9e65e9e6 --- /dev/null +++ b/components/emelia/README.md @@ -0,0 +1,11 @@ +# Overview + +The Emelia API enables streamlined email campaign management directly from Pipedream. By integrating Emelia with Pipedream, you can automate email sequences, monitor campaign performance, and trigger personalized follow-ups based on recipient actions. The API's functionality allows for the creation of data-driven workflows that can interact with subscriber lists, send out scheduled emails, and provide analytics, all within the scalable environment on Pipedream. + +# Example Use Cases + +- **Automated Customer Onboarding Emails**: Initiate a sequence of onboarding emails when a new user signs up for your service through a web form. Use Emelia to send a welcome email, followed by tailored messages based on user engagement, tracked and optimized using Pipedream's event-based triggers. + +- **Targeted Campaign Follow-ups**: After sending a campaign through Emelia, use Pipedream to listen for opens or clicks. Automatically tag users who engaged and send a personalized follow-up or offer a special discount to encourage conversion. + +- **E-commerce Cart Abandonment Recovery**: Combine Emelia with your e-commerce platform (like Shopify) to detect when a shopper abandons their cart. Trigger an Emelia email sequence via Pipedream to re-engage the customer with reminders or special offers, increasing the likelihood of completing the purchase. diff --git a/components/encharge/README.md b/components/encharge/README.md new file mode 100644 index 0000000000000..ff1136ddceea5 --- /dev/null +++ b/components/encharge/README.md @@ -0,0 +1,11 @@ +# Overview + +The Encharge API lets you automate your marketing by managing contacts, sending emails, and tracking user actions. Using Pipedream's serverless platform, you can connect Encharge with other apps to create custom workflows. Trigger actions based on events, update user segments, or synchronize data across your marketing stack. + +# Example Use Cases + +- **Automated Lead Scoring and Tagging**: When a user signs up on your platform, use Pipedream to trigger an Encharge workflow that scores the lead and tags them based on their activity. This can then tailor subsequent marketing efforts. + +- **Sync New Users to a CRM**: Once a new user is added in Encharge, automatically add them to your CRM platform, like Salesforce or HubSpot, with a Pipedream workflow. Keep all your user data in sync and accessible across your sales and marketing teams. + +- **Email Campaign Triggered by User Behavior**: Monitor user activity on your website or app. When a significant event occurs, such as a user completing a purchase, you can trigger an Encharge API call with Pipedream to send a personalized follow-up email or start a drip campaign. diff --git a/components/encharge/package.json b/components/encharge/package.json index 39f0b00ddf911..dd55516b6317f 100644 --- a/components/encharge/package.json +++ b/components/encharge/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/encodian/README.md b/components/encodian/README.md new file mode 100644 index 0000000000000..7fc6059d813e7 --- /dev/null +++ b/components/encodian/README.md @@ -0,0 +1,11 @@ +# Overview + +The Encodian API provides robust document management and manipulation capabilities, enabling users to convert, merge, split, OCR, and watermark documents. This API integrates smoothly with Pipedream, allowing for the automation of document processing tasks within various workflows. By leveraging Encodian with Pipedream, users can create efficient automations that handle large volumes of documents, reducing manual effort and improving productivity in document-centric processes. + +# Example Use Cases + +- **Automated Invoice Processing**: Integrate Encodian with an email app like Gmail on Pipedream to automatically extract invoices received via email, convert them from PDF to a more editable format like Word, apply OCR if needed, and store the processed files in a cloud storage service like Google Drive. + +- **Contract Management Automation**: Use Encodian to merge multiple contract sections stored in Dropbox into a single document, apply a digital signature using a service like DocuSign, and then archive the signed contract in a SharePoint folder, all orchestrated within a Pipedream workflow. + +- **Employee Onboarding Documents Handling**: Set up a workflow on Pipedream where Encodian converts all new employee onboarding documents uploaded to a specific Google Drive folder into PDF format, watermarks them with the company logo, and then emails the final copies to the HR department using Microsoft Outlook. diff --git a/components/encodian/actions/compare-text/compare-text.mjs b/components/encodian/actions/compare-text/compare-text.mjs new file mode 100644 index 0000000000000..24223c45ba9a2 --- /dev/null +++ b/components/encodian/actions/compare-text/compare-text.mjs @@ -0,0 +1,44 @@ +import app from "../../encodian.app.mjs"; + +export default { + key: "encodian-compare-text", + name: "Compare Text", + description: "Compares two texts answering if they are equal or not. [See the documentation](https://api.apps-encodian.com/index.html)", + version: "0.0.1", + type: "action", + props: { + app, + primaryText: { + propDefinition: [ + app, + "primaryText", + ], + }, + secondaryText: { + propDefinition: [ + app, + "secondaryText", + ], + }, + ignoreCase: { + propDefinition: [ + app, + "ignoreCase", + ], + }, + }, + async run({ $ }) { + const response = await this.app.compareTexts({ + $, + data: { + primaryText: this.primaryText, + secondaryText: this.secondaryText, + ignoreCase: this.ignoreCase, + }, + }); + + $.export("$summary", `The text comparison returned: '${response.result}'`); + + return response; + }, +}; diff --git a/components/encodian/actions/validate-email/validate-email.mjs b/components/encodian/actions/validate-email/validate-email.mjs new file mode 100644 index 0000000000000..17e55411ec273 --- /dev/null +++ b/components/encodian/actions/validate-email/validate-email.mjs @@ -0,0 +1,31 @@ +import app from "../../encodian.app.mjs"; + +export default { + key: "encodian-validate-email", + name: "Validate Email", + description: "Validate the syntax of an email address. [See the documentation](https://api.apps-encodian.com/index.html)", + version: "0.0.1", + type: "action", + props: { + app, + emailAddress: { + propDefinition: [ + app, + "emailAddress", + ], + }, + }, + async run({ $ }) { + const response = await this.app.validateEmail({ + $, + data: { + emailAddress: this.emailAddress, + regex: "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$", + }, + }); + + $.export("$summary", `The validation of the email address returned: '${response.result}'`); + + return response; + }, +}; diff --git a/components/encodian/actions/validate-url/validate-url.mjs b/components/encodian/actions/validate-url/validate-url.mjs new file mode 100644 index 0000000000000..a13b8a49c308f --- /dev/null +++ b/components/encodian/actions/validate-url/validate-url.mjs @@ -0,0 +1,31 @@ +import app from "../../encodian.app.mjs"; + +export default { + key: "encodian-validate-url", + name: "Validate URL", + description: "Validate the availability of the URL. [See the documentation](https://api.apps-encodian.com/index.html)", + version: "0.0.1", + type: "action", + props: { + app, + url: { + propDefinition: [ + app, + "url", + ], + }, + }, + async run({ $ }) { + const response = await this.app.validateUrl({ + $, + data: { + url: this.url, + validateURL: true, + }, + }); + + $.export("$summary", `The validation of the URL availability returned: '${response.result}'`); + + return response; + }, +}; diff --git a/components/encodian/encodian.app.mjs b/components/encodian/encodian.app.mjs new file mode 100644 index 0000000000000..8e2fbb2400aa6 --- /dev/null +++ b/components/encodian/encodian.app.mjs @@ -0,0 +1,75 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "encodian", + propDefinitions: { + primaryText: { + type: "string", + label: "Frist Text", + description: "The first text for comparison", + }, + secondaryText: { + type: "string", + label: "Second Text", + description: "The second text for comparison", + }, + ignoreCase: { + type: "boolean", + label: "Ignore Case", + description: "Toggle to specify whether case should be ignored during comparison", + }, + emailAddress: { + type: "string", + label: "Email Address", + description: "An email address to check validity", + }, + url: { + type: "string", + label: "URL", + description: "An URL to check availability, e.g. `https://www.google.com`", + }, + }, + methods: { + _baseUrl() { + return "https://api.apps-encodian.com/api/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "X-ApiKey": `${this.$auth.encodian_flowr_api_key}`, + }, + }); + }, + async validateUrl(args = {}) { + return this._makeRequest({ + method: "post", + path: "/Utility/ValidateUrlAvailability", + ...args, + }); + }, + async validateEmail(args = {}) { + return this._makeRequest({ + method: "post", + path: "/Utility/ValidateEmailAddress", + ...args, + }); + }, + async compareTexts(args = {}) { + return this._makeRequest({ + method: "post", + path: "/Utility/CompareText", + ...args, + }); + }, + }, +}; diff --git a/components/encodian/package.json b/components/encodian/package.json new file mode 100644 index 0000000000000..779b32c406c5d --- /dev/null +++ b/components/encodian/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/encodian", + "version": "0.1.0", + "description": "Pipedream Encodian Components", + "main": "encodian.app.mjs", + "keywords": [ + "pipedream", + "encodian" + ], + "homepage": "https://pipedream.com/apps/encodian", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.4" + } +} diff --git a/components/end/actions/end-workflow/end-workflow.mjs b/components/end/actions/end-workflow/end-workflow.mjs new file mode 100644 index 0000000000000..aee24f9e0c2f4 --- /dev/null +++ b/components/end/actions/end-workflow/end-workflow.mjs @@ -0,0 +1,21 @@ +import end from "../../end.app.mjs"; + +export default { + name: "End Workflow", + description: "Terminate workflow execution", + version: "0.0.3", + type: "action", + key: "end-end-workflow", + props: { + end, + reason: { + propDefinition: [ + end, + "reason", + ], + }, + }, + async run({ $ }) { + $.flow.exit(this.reason); + }, +}; diff --git a/components/end/end.app.mjs b/components/end/end.app.mjs new file mode 100644 index 0000000000000..65a42472f24f7 --- /dev/null +++ b/components/end/end.app.mjs @@ -0,0 +1,13 @@ +export default { + type: "app", + app: "end", + propDefinitions: { + reason: { + type: "string", + label: "Reason", + description: "Reason for ending workflow execution", + optional: true, + }, + }, + methods: {}, +}; diff --git a/components/end/package.json b/components/end/package.json new file mode 100644 index 0000000000000..2992802d26390 --- /dev/null +++ b/components/end/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/end", + "version": "0.0.4", + "description": "Pipedream End Components", + "main": "end.app.mjs", + "keywords": [ + "pipedream", + "end" + ], + "homepage": "https://pipedream.com/apps/end", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/endorsal/README.md b/components/endorsal/README.md new file mode 100644 index 0000000000000..434265e88eaa4 --- /dev/null +++ b/components/endorsal/README.md @@ -0,0 +1,11 @@ +# Overview + +The Endorsal API provides a way to programmatically manage and showcase customer testimonials. With Pipedream, you can automate the collection and display of endorsements, sync these testimonials with other marketing tools, and trigger actions based on new reviews. This seamless integration enables you to leverage customer feedback effectively to boost your brand's credibility and visibility. + +# Example Use Cases + +- **Automate Testimonial Collection**: Set up a Pipedream workflow that listens for a new customer sign-up via your CRM (e.g., Salesforce), then automatically sends a request for a testimonial to the customer through the Endorsal API. + +- **Sync Testimonials with Marketing Platforms**: When a new testimonial is received, use Pipedream to post it to your social media channels like Twitter or LinkedIn, or update your website's testimonial section in real time. + +- **Monitor and Alert for New Reviews**: Create a Pipedream workflow that checks for new Endorsal testimonials and sends an alert through Slack or email to your marketing team, so they can review and act quickly on the latest customer feedback. diff --git a/components/enedis/README.md b/components/enedis/README.md index 782cfbf331e26..d117a1902d631 100644 --- a/components/enedis/README.md +++ b/components/enedis/README.md @@ -1,5 +1,11 @@ # Overview -The Enedis Data-Connect API allows developers to access data from the French -electricity grid. This data can be used to build applications that help manage -the electricity grid, plan for power outages, and more. +Enedis Data-Connect API provides access to energy consumption data, enabling users to analyze and optimize electricity usage. By integrating with this API on Pipedream, you can automate the collection of meter readings, track energy consumption patterns, and even develop personalized energy-saving strategies. Pipedream's serverless platform simplifies connecting Enedis Data-Connect with other apps, allowing you to set up complex workflows without managing infrastructure. + +# Example Use Cases + +- **Energy Monitoring Dashboard**: Craft a workflow that periodically fetches energy consumption data from Enedis Data-Connect and pushes it to a Google Sheet. Use the sheet to power a live dashboard for real-time monitoring, leveraging Google Data Studio for visual analytics. + +- **Smart Home Integration**: Combine Enedis Data-Connect with smart home platforms like Philips Hue or Nest. Automate the dimming of lights or temperature adjustments based on real-time energy usage data, making your home more energy-efficient. + +- **Billing and Alerts System**: Implement a system that correlates energy consumption with billing cycles. Use the API to fetch usage data, calculate projected costs, and send alerts via email or SMS if projected consumption exceeds a set threshold, using SendGrid or Twilio on Pipedream. diff --git a/components/engage/README.md b/components/engage/README.md new file mode 100644 index 0000000000000..5a71fa9c37aeb --- /dev/null +++ b/components/engage/README.md @@ -0,0 +1,11 @@ +# Overview + +The Engage API lets you automate and streamline your customer communication by sending personalized messages. With this API, you can craft interactions based on user behavior, preferences, and feedback. On Pipedream, you can integrate Engage with your workflow to trigger actions based on events from other apps, manage contact lists, and analyze communication patterns — all in real-time and without managing servers. + +# Example Use Cases + +- **Customer Feedback Collection Workflow**: After a customer interaction with your service, use the Engage API to send a personalized feedback request. Combine this with a Google Sheets integration on Pipedream to record and analyze responses for quality assurance. + +- **User Onboarding Sequence**: Trigger an onboarding message sequence when a new user signs up for your service. Use Pipedream to listen for a new user event from your authentication service (like Auth0), and kick off a series of targeted messages via Engage to guide them through your platform's features. + +- **Abandoned Cart Reminder**: Connect Engage to an e-commerce platform like Shopify with Pipedream. When a cart is abandoned, send a reminder message to the potential customer, offering assistance or a promotional code to encourage purchase completion. diff --git a/components/engagebay/README.md b/components/engagebay/README.md index 3832e54ce3bed..5868f6b404848 100644 --- a/components/engagebay/README.md +++ b/components/engagebay/README.md @@ -1,9 +1,11 @@ # Overview -EngageBay's API enables you to connect to EngageBay and build integrations with -third-party applications. With the API, you can perform actions such as: +EngageBay is a comprehensive customer relationship management (CRM) and marketing automation platform designed to help businesses attract, engage, and convert web visitors into customers. By leveraging the EngageBay API with Pipedream, you can automate intricate tasks, synchronize data across different applications, and create personalized customer experiences. From automating lead capture to syncing contacts and creating custom email campaigns based on user actions, Pipedream's serverless platform enables you to craft workflows that streamline your sales and marketing processes. -- Create and manage contacts -- Create and manage leads -- Send and track email campaigns -- Manage your EngageBay account settings +# Example Use Cases + +- **Lead Score Updating in Real-Time**: Listen for new interactions on your website or app, score leads accordingly, and update their score in EngageBay. Use this information to trigger tailored marketing campaigns or sales team notifications. + +- **Automated Contact Synchronization**: Sync new contacts from platforms like Shopify or WooCommerce to EngageBay in real-time. When a new sale is made, automatically create or update a contact in EngageBay, ensuring your CRM reflects the most current customer data. + +- **Support Ticket Integration**: Integrate EngageBay with a helpdesk app like Zendesk. Whenever a support ticket is resolved, trigger a workflow that updates the customer's record in EngageBay with the resolution details and sends a follow-up satisfaction survey. diff --git a/components/engagebay/package.json b/components/engagebay/package.json new file mode 100644 index 0000000000000..07a90a1d41c54 --- /dev/null +++ b/components/engagebay/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/engagebay", + "version": "0.6.0", + "description": "Pipedream engagebay Components", + "main": "engagebay.app.mjs", + "keywords": [ + "pipedream", + "engagebay" + ], + "homepage": "https://pipedream.com/apps/engagebay", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/enigma/README.md b/components/enigma/README.md index d6c1631554c5d..25509a828500f 100644 --- a/components/enigma/README.md +++ b/components/enigma/README.md @@ -1,6 +1,11 @@ # Overview -The Enigma API enables developers to access data and content from a variety of -sources, including news, weather, and social media. With the Enigma API, you -can build applications that display real-time data, offer insights and -analysis, and provide a variety of other features and services. +The Enigma API offers access to a vast repository of global public data, which can be harnessed to enrich business intelligence, enhance analytics, and automate due diligence processes. By tapping into Enigma's curated datasets on Pipedream, you can seamlessly integrate real-time insights into your applications or workflows, perform comprehensive data analysis, and cross-reference multiple sources to uncover critical information on businesses, markets, and economies. + +# Example Use Cases + +- **Risk Assessment Automation**: Configure a Pipedream workflow to periodically pull company data from Enigma and assess the risk profile of potential clients or partners. This data can be cross-referenced with your internal risk parameters and automatically notify the compliance team if red flags are detected. + +- **Market Research and Analysis**: Set up a Pipedream pipeline that fetches industry-related datasets from Enigma on a scheduled basis. Integrate this data with a BI tool like Tableau (also available on Pipedream) to create dynamic, data-driven market analysis dashboards that update in real time. + +- **Enhanced Due Diligence Process**: Create a Pipedream workflow that uses Enigma to perform background checks on companies. The workflow can populate CRM systems like Salesforce with enriched company data, providing sales teams with up-to-date intelligence for more informed engagement strategies. diff --git a/components/enormail/README.md b/components/enormail/README.md index 5c43d96d6eea8..9a3906770faea 100644 --- a/components/enormail/README.md +++ b/components/enormail/README.md @@ -1,12 +1,11 @@ # Overview -With Enormail, you can easily create and send beautiful email newsletters, -manage your subscribers, and track your results. Plus, our easy-to-use API -makes it easy to integrate Enormail into your existing website or application. +The Enormail API brings email marketing campaigns to your fingertips, allowing you to automate subscriber management and email sending. You can craft workflows that respond to subscriber actions, update lists, and send targeted emails based on user behavior or predefined triggers. Pipedream's serverless platform magnifies Enormail's potential by enabling integrations with a multitude of apps to create custom automation chains, streamlining your marketing processes without having to write extensive code. -Here are some examples of what you can build with the Enormail API: +# Example Use Cases -- Send email newsletters to your subscribers -- Manage your subscribers list -- Track your results -- Integrate Enormail into your existing website or application +- **Subscriber Segmentation Workflow**: When a new subscriber is added to Enormail, automatically trigger a Pipedream workflow that cross-references their email with a database on Google Sheets. If they're a returning customer, update their subscriber details in Enormail with a 'VIP' tag, and send a personalized welcome-back email. + +- **Customer Feedback Loop**: After sending a campaign email through Enormail, use Pipedream to listen for opens and clicks. Trigger a follow-up email or SMS via Twilio to gather feedback if the subscriber interacts with the email. Store responses in a Google Sheets spreadsheet for analysis and subsequent action. + +- **E-commerce Cart Abandonment**: Integrate Enormail with an e-commerce platform like Shopify. Monitor for cart abandonments and use Pipedream to trigger an Enormail workflow that sends a tailored email sequence to the potential buyer, encouraging them to complete their purchase with a special discount or a reminder. diff --git a/components/enrichley/actions/validate-email/validate-email.mjs b/components/enrichley/actions/validate-email/validate-email.mjs new file mode 100644 index 0000000000000..30040ae3933d1 --- /dev/null +++ b/components/enrichley/actions/validate-email/validate-email.mjs @@ -0,0 +1,27 @@ +import enrichley from "../../enrichley.app.mjs"; + +export default { + key: "enrichley-validate-email", + name: "Validate Email", + description: "Checks the validity of a single email address using Enrichley. [See the documentation](https://enrichley.readme.io/reference/validatesingleemail)", + version: "0.0.1", + type: "action", + props: { + enrichley, + email: { + type: "string", + label: "Email", + description: "The email address to validate", + }, + }, + async run({ $ }) { + const response = await this.enrichley.validateEmail({ + $, + data: { + email: this.email, + }, + }); + $.export("$summary", `Successfully retrieved status for email: ${this.email}`); + return response; + }, +}; diff --git a/components/enrichley/enrichley.app.mjs b/components/enrichley/enrichley.app.mjs new file mode 100644 index 0000000000000..6c9e6fd8c91b0 --- /dev/null +++ b/components/enrichley/enrichley.app.mjs @@ -0,0 +1,32 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "enrichley", + methods: { + _baseUrl() { + return "https://api.enrichley.io/api/v1"; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "Accept": "application/json", + "X-API-Key": `${this.$auth.api_key}`, + }, + ...opts, + }); + }, + validateEmail(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/validate-single-email", + ...opts, + }); + }, + }, +}; diff --git a/components/enrichley/package.json b/components/enrichley/package.json new file mode 100644 index 0000000000000..ecff527d26545 --- /dev/null +++ b/components/enrichley/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/enrichley", + "version": "0.1.0", + "description": "Pipedream Enrichley Components", + "main": "enrichley.app.mjs", + "keywords": [ + "pipedream", + "enrichley" + ], + "homepage": "https://pipedream.com/apps/enrichley", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/enrow/README.md b/components/enrow/README.md new file mode 100644 index 0000000000000..ec15080230be8 --- /dev/null +++ b/components/enrow/README.md @@ -0,0 +1,11 @@ +# Overview + +The Enrow API offers the power to automate and enhance your e-commerce operations with features like product catalog management, order processing, and customer interactions. By leveraging the Enrow API on Pipedream, you can create custom workflows that trigger actions based on specific events, integrate with other services for a seamless back-end process, and manipulate data to fit your business needs. With Pipedream's serverless platform, you can execute complex sequences of tasks with minimal setup, tapping into the vast library of pre-built actions or writing your own code steps in Node.js. + +# Example Use Cases + +- **Sync New Orders to CRM**: When new orders come through on Enrow, automatically push them into your CRM system. This keeps your sales team informed and enables timely follow-ups or cross-selling opportunities. + +- **Inventory Management**: Monitor product stock levels via Enrow and trigger alerts or reorder actions when inventory runs low. This workflow can connect to supplier systems to automate restocking and keep sales flowing without manual oversight. + +- **Customer Review Solicitation**: After order fulfillment, use the Enrow API to trigger a sequence that reaches out to customers asking for product reviews. This could involve sending personalized emails or SMS messages, helping to build your reputation and provide valuable feedback. diff --git a/components/enrow/actions/find-single-email/find-single-email.mjs b/components/enrow/actions/find-single-email/find-single-email.mjs new file mode 100644 index 0000000000000..404624f61d4f6 --- /dev/null +++ b/components/enrow/actions/find-single-email/find-single-email.mjs @@ -0,0 +1,88 @@ +import enrow from "../../enrow.app.mjs"; + +export default { + key: "enrow-find-single-email", + name: "Find Single Email", + description: "Executes a single email search using Enrow email finder. [See the documentation](https://enrow.readme.io/reference/find-single-email)", + version: "0.0.1", + type: "action", + props: { + enrow, + fullname: { + type: "string", + label: "Full Name", + description: "The fullname of the person for which to find the email", + }, + alert: { + type: "alert", + alertType: "info", + content: "Each search needs at least the Company Domain or the Company Name to be given. The Company Domain will usually offer the best results, you can provide both if you want to optimize the results.", + }, + companyDomain: { + type: "string", + label: "Company Domain", + description: "The company's domain, multiple formats accepted (\"apple.com\", \"https://www.apple.com\"...)", + optional: true, + }, + companyName: { + type: "string", + label: "Company Name", + description: "The company's name (\"Apple\", \"Air France\", ...)", + optional: true, + }, + countryCode: { + type: "string", + label: "Country Code", + description: "The ISO 3166 Alpha-2 code of the country related to the search. Relevant when using a company name.", + optional: true, + }, + retrieveCompanyInfo: { + type: "boolean", + label: "Retrieve Company Info", + description: "Available for France only right now. If the email is a French company, it will send back a whole bunch of informations about it.", + optional: true, + }, + webhookUrl: { + type: "string", + label: "Webhook URL", + description: "A URL that will be notified through an http POST request once the single search is finished", + optional: true, + }, + callbackWithRerun: { + type: "boolean", + label: "Callback With Rerun", + description: "Use the `$.flow.rerun` Node.js helper to rerun the step when the search is completed. Overrides the `webhookUrl` prop. This will increase execution time and credit usage as a result. [See the documentation(https://pipedream.com/docs/code/nodejs/rerun/#flow-rerun)", + optional: true, + }, + }, + async run({ $ }) { + let response; + const { run } = $.context; + if (run.runs === 1) { + let webhook = this.webhookUrl; + if (this.callbackWithRerun) { + ({ resume_url: webhook } = $.flow.rerun(600000, null, 1)); + } + response = await this.enrow.executeSearch({ + $, + data: { + fullname: this.fullname, + company_domain: this.companyDomain, + company_name: this.companyName, + settings: { + country_code: this.countryCode, + retrieve_company_info: this.retrieveCompanyInfo, + webhook, + }, + }, + }); + } + + if (run.callback_request) { + response = run.callback_request.body; + } + + $.export("$summary", "Successfully executed search."); + return response; + }, +}; diff --git a/components/enrow/actions/get-single-email-finder-result/get-single-email-finder-result.mjs b/components/enrow/actions/get-single-email-finder-result/get-single-email-finder-result.mjs new file mode 100644 index 0000000000000..a52b813b8ed4f --- /dev/null +++ b/components/enrow/actions/get-single-email-finder-result/get-single-email-finder-result.mjs @@ -0,0 +1,27 @@ +import enrow from "../../enrow.app.mjs"; + +export default { + key: "enrow-get-single-email-finder-result", + name: "Get Single Email Finder Result", + description: "Retrieve a result from a single search executed via email finder function. [See the documentation](https://enrow.readme.io/reference/get-single-email-finder-result)", + version: "0.0.1", + type: "action", + props: { + enrow, + searchId: { + type: "string", + label: "Search ID", + description: "The identifier for the specific search.", + }, + }, + async run({ $ }) { + const response = await this.enrow.getResult({ + $, + params: { + id: this.searchId, + }, + }); + $.export("$summary", `Successfully fetched the result for search ID: ${this.searchId}`); + return response; + }, +}; diff --git a/components/enrow/enrow.app.mjs b/components/enrow/enrow.app.mjs index fb487c094bb9b..c5e290919fcc4 100644 --- a/components/enrow/enrow.app.mjs +++ b/components/enrow/enrow.app.mjs @@ -1,11 +1,39 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "enrow", propDefinitions: {}, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.enrow.io"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "x-api-key": `${this.$auth.api_key}`, + }, + }); + }, + getResult(opts = {}) { + return this._makeRequest({ + path: "/email/find/single", + ...opts, + }); + }, + executeSearch(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/email/find/single", + ...opts, + }); }, }, }; diff --git a/components/enrow/package.json b/components/enrow/package.json index 5eefb1d630101..fe45c628287da 100644 --- a/components/enrow/package.json +++ b/components/enrow/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/enrow", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Enrow Components", "main": "enrow.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" } -} \ No newline at end of file +} diff --git a/components/envoy/README.md b/components/envoy/README.md new file mode 100644 index 0000000000000..387dbf38aef70 --- /dev/null +++ b/components/envoy/README.md @@ -0,0 +1,11 @@ +# Overview + +The Envoy API empowers users to streamline workplace operations, particularly visitor management, deliveries, and room bookings. Leveraging Pipedream's capabilities, you can automate tasks involving these aspects. For example, you can receive notifications when guests arrive, sync visitor data with other systems, or automate the creation and management of room bookings. + +# Example Use Cases + +- **Visitor Arrival Notifications**: Get a real-time alert in Slack via Pipedream when a guest checks in through Envoy. This workflow can enhance security and hospitality by promptly notifying the relevant personnel of a visitor's arrival. + +- **Automate Guest Wi-Fi Credentials Distribution**: After a visitor registers in Envoy, trigger a workflow that automatically generates Wi-Fi credentials and sends them to the visitor’s email. This can help streamline the process of providing internet access to guests, ensuring a smooth experience. + +- **Sync Visitor Logs with Google Sheets**: Save visitor check-in and check-out times to a Google Sheet for record-keeping and analytics. This workflow can assist in maintaining accurate visitor logs and making data-driven decisions about workplace resource management. diff --git a/components/eodhd_apis/README.md b/components/eodhd_apis/README.md new file mode 100644 index 0000000000000..8191a022d2246 --- /dev/null +++ b/components/eodhd_apis/README.md @@ -0,0 +1,11 @@ +# Overview + +EODHD API offers comprehensive financial data, including end-of-day stocks, ETFs, and mutual fund prices, dividends, splits, and more across multiple exchanges globally. On Pipedream, you can harness this treasure trove of financial information to automate market analysis, update data stores, or trigger notifications based on stock movements or financial news. + +# Example Use Cases + +- **Portfolio Tracker**: Construct a workflow that checks your portfolio's stock prices daily using EODHD API, compares them with your target prices, and updates a Google Sheet. If any stock hits a target, trigger an email alert. + +- **Market News Digest**: Create a workflow that pulls the latest financial news for specific securities from EODHD API on a schedule. Filter for news impacting your watchlist, then compile a digest and send it via Slack or Discord to keep your team informed. + +- **Dividend Alert System**: Set up a system that monitors dividend announcements for stocks in your investment list. When a new dividend is declared, use EODHD API to fetch the details and push notifications through Twilio SMS or another preferred communication channel. diff --git a/components/epic_games/epic_games.app.mjs b/components/epic_games/epic_games.app.mjs new file mode 100644 index 0000000000000..d90bf36c290aa --- /dev/null +++ b/components/epic_games/epic_games.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "epic_games", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/epic_games/package.json b/components/epic_games/package.json new file mode 100644 index 0000000000000..46201c7cf85c7 --- /dev/null +++ b/components/epic_games/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/epic_games", + "version": "0.0.1", + "description": "Pipedream Epic Games Components", + "main": "epic_games.app.mjs", + "keywords": [ + "pipedream", + "epic_games" + ], + "homepage": "https://pipedream.com/apps/epic_games", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/epsy/actions/email-lookup/email-lookup.mjs b/components/epsy/actions/email-lookup/email-lookup.mjs new file mode 100644 index 0000000000000..16b3aa519c76a --- /dev/null +++ b/components/epsy/actions/email-lookup/email-lookup.mjs @@ -0,0 +1,31 @@ +import app from "../../epsy.app.mjs"; + +export default { + key: "epsy-email-lookup", + name: "Email Lookup", + description: "Request a lookup for the provided email. [See the documentation](https://irbis.espysys.com/developer/)", + version: "0.0.1", + type: "action", + props: { + app, + value: { + propDefinition: [ + app, + "value", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.emailLookup({ + $, + data: { + value: this.value, + lookupId: 67, + }, + }); + $.export("$summary", `Successfully sent request. Use the ID to get the results: '${response.id}'`); + + return response; + }, +}; diff --git a/components/epsy/actions/get-lookup-results/get-lookup-results.mjs b/components/epsy/actions/get-lookup-results/get-lookup-results.mjs new file mode 100644 index 0000000000000..3e29201876a5b --- /dev/null +++ b/components/epsy/actions/get-lookup-results/get-lookup-results.mjs @@ -0,0 +1,39 @@ +import app from "../../epsy.app.mjs"; + +export default { + key: "epsy-get-lookup-results", + name: "Get Lookup Results", + description: "Get the results of the lookup with the provided ID. [See the documentation](https://irbis.espysys.com/developer/)", + version: "0.0.1", + type: "action", + props: { + app, + value: { + propDefinition: [ + app, + "value", + ], + }, + searchId: { + propDefinition: [ + app, + "searchId", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.getLookupResults({ + $, + searchId: this.searchId, + data: { + value: this.value, + }, + params: { + key: `${this.app.$auth.api_key}`, + }, + }); + $.export("$summary", `Successfully retrieved the details of the request with ID: '${this.searchId}'`); + return response; + }, +}; diff --git a/components/epsy/actions/name-lookup/name-lookup.mjs b/components/epsy/actions/name-lookup/name-lookup.mjs new file mode 100644 index 0000000000000..d9ee0a0605bac --- /dev/null +++ b/components/epsy/actions/name-lookup/name-lookup.mjs @@ -0,0 +1,30 @@ +import app from "../../epsy.app.mjs"; + +export default { + key: "epsy-name-lookup", + name: "Name Lookup", + description: "Request a lookup for the provided name. [See the documentation](https://irbis.espysys.com/developer/)", + version: "0.0.1", + type: "action", + props: { + app, + value: { + propDefinition: [ + app, + "value", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.nameLookup({ + $, + data: { + value: this.value, + lookupId: 149, + }, + }); + $.export("$summary", `Successfully sent request. Use the ID to get the results: '${response.id}'`); + return response; + }, +}; diff --git a/components/epsy/epsy.app.mjs b/components/epsy/epsy.app.mjs new file mode 100644 index 0000000000000..68d30e4d4f900 --- /dev/null +++ b/components/epsy/epsy.app.mjs @@ -0,0 +1,96 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "epsy", + propDefinitions: { + value: { + type: "string", + label: "Value", + description: "Value to lookup", + }, + searchId: { + type: "string", + label: "Search ID", + description: "ID of the search", + async options() { + const response = await this.getSearchIds(); + const searchIds = response.list; + return searchIds.map(({ id }) => ({ + value: id, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://irbis.espysys.com/api"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + data, + ...otherOpts + } = opts; + console.log("Request Options:", { + url: this._baseUrl() + path, + headers: { + ...headers, + "accept": "application/json", + "content-type": "application/json", + }, + data: { + ...data, + key: `${this.$auth.api_key}`, + }, + ...otherOpts, + }); + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "accept": "application/json", + "content-type": "application/json", + }, + data: { + ...data, + key: `${this.$auth.api_key}`, + }, + }); + }, + async emailLookup(args = {}) { + return this._makeRequest({ + path: "/developer/combined_email", + method: "post", + ...args, + }); + }, + async nameLookup(args = {}) { + return this._makeRequest({ + path: "/developer/combined_name", + method: "post", + ...args, + }); + }, + async getLookupResults({ + searchId, ...args + }) { + return this._makeRequest({ + path: `/request-monitor/api-usage/${searchId}`, + ...args, + }); + }, + async getSearchIds(args = {}) { + return this._makeRequest({ + path: "/request-monitor/api-usage", + params: { + key: `${this.$auth.api_key}`, + }, + ...args, + }); + }, + }, +}; diff --git a/components/epsy/package.json b/components/epsy/package.json new file mode 100644 index 0000000000000..37524ff531fa8 --- /dev/null +++ b/components/epsy/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/epsy", + "version": "0.1.0", + "description": "Pipedream Epsy Components", + "main": "epsy.app.mjs", + "keywords": [ + "pipedream", + "epsy" + ], + "homepage": "https://pipedream.com/apps/epsy", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/equifax/equifax.app.mjs b/components/equifax/equifax.app.mjs new file mode 100644 index 0000000000000..e230f31e7f312 --- /dev/null +++ b/components/equifax/equifax.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "equifax", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/equifax/package.json b/components/equifax/package.json new file mode 100644 index 0000000000000..dde3139d50e18 --- /dev/null +++ b/components/equifax/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/equifax", + "version": "0.0.1", + "description": "Pipedream Equifax Components", + "main": "equifax.app.mjs", + "keywords": [ + "pipedream", + "equifax" + ], + "homepage": "https://pipedream.com/apps/equifax", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/erpnext/README.md b/components/erpnext/README.md index 6283d7ac93622..52eb0edaae6da 100644 --- a/components/erpnext/README.md +++ b/components/erpnext/README.md @@ -1,14 +1,11 @@ # Overview -ERPNext is a powerful, yet easy to use, business management software. It comes -with a rich set of features that can help you manage your business effectively. -However, what makes ERPNext even more powerful is its API. - -The ERPNext API gives you the ability to build custom applications that can -integrate with ERPNext and extend its functionality. With the ERPNext API, you -can build applications that can: - -- Automate tasks and workflows -- Retrieve and update data in ERPNext -- Generate reports and dashboards -- Integrate with third-party applications +ERPNext is an open-source enterprise resource planning (ERP) software that integrates core business functions like accounting, inventory, sales, purchase, and HR management into a single system. With the ERPNext API, you can automate these functions by triggering actions in ERPNext or syncing data with other systems. Pipedream can be a powerful partner here, as it allows you to set up complex integrations and workflows without the need for a dedicated backend. By using Pipedream, you can connect ERPNext with numerous other apps to streamline processes, react to events in real-time, and automate data transfers. + +# Example Use Cases + +- **Invoice Sync Workflow**: When a new sales invoice is created in ERPNext, a Pipedream workflow is triggered, which captures the invoice details and synchronizes them with a cloud accounting app like QuickBooks. This ensures financial records are updated across systems without manual data entry. + +- **Inventory Management Automation**: Set up a Pipedream workflow that monitors stock levels in ERPNext. When stock for a particular item falls below a threshold, the workflow automatically reorders the item from the supplier via an integrated supplier management system or sends a notification to the purchasing department using a communication platform like Slack. + +- **New Customer Onboarding**: When a new customer is added to ERPNext, a Pipedream workflow is kicked off that sends a welcome email using a service like SendGrid, adds the customer to a CRM like Salesforce, and schedules a follow-up task in a project management tool like Asana, ensuring a seamless onboarding process. diff --git a/components/erpnext/package.json b/components/erpnext/package.json new file mode 100644 index 0000000000000..0a2379eddb88f --- /dev/null +++ b/components/erpnext/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/erpnext", + "version": "0.6.0", + "description": "Pipedream erpnext Components", + "main": "erpnext.app.mjs", + "keywords": [ + "pipedream", + "erpnext" + ], + "homepage": "https://pipedream.com/apps/erpnext", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/error/actions/throw-error/throw-error.mjs b/components/error/actions/throw-error/throw-error.mjs new file mode 100644 index 0000000000000..4a724026334cb --- /dev/null +++ b/components/error/actions/throw-error/throw-error.mjs @@ -0,0 +1,27 @@ +import error from "../../error.app.mjs"; + +export default { + name: "Throw Error", + version: "0.0.1", + key: "error-throw-error", + description: "Quickly throw an error from your workflow.", + props: { + error, + name: { + propDefinition: [ + error, + "name", + ], + }, + errorMessage: { + propDefinition: [ + error, + "errorMessage", + ], + }, + }, + type: "action", + async run() { + this.error.maybeCreateAndThrowError(this.name, this.errorMessage); + }, +}; diff --git a/components/error/error.app.mjs b/components/error/error.app.mjs new file mode 100644 index 0000000000000..c7ced7cce41ab --- /dev/null +++ b/components/error/error.app.mjs @@ -0,0 +1,41 @@ +export default { + type: "app", + app: "error", + propDefinitions: { + name: { + type: "string", + label: "Error Name", + description: + "The **name** (class) of error to throw, which you can define as any custom string. This will show up in all of the standard Pipedream error handling destinations.", + default: "Error", + }, + errorMessage: { + type: "string", + label: "Error Message", + description: + "The error **message** to throw. This will show up in all of the standard Pipedream error handling destinations.", + optional: true, + }, + }, + methods: { + maybeCreateAndThrowError(name, message) { + const errorClass = global[name]; + + // Check if the error class exists and is a subclass of Error + if ( + typeof errorClass === "function" && + errorClass.prototype.isPrototypeOf.call(Error) + ) { + throw new errorClass(message); + } + + class DynamicError extends Error { + constructor(msg) { + super(msg); + this.name = name; + } + } + throw new DynamicError(message); + }, + }, +}; diff --git a/components/error/package.json b/components/error/package.json new file mode 100644 index 0000000000000..91f83c7bed9f5 --- /dev/null +++ b/components/error/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/error", + "version": "0.0.2", + "description": "Pipedream Error Components", + "main": "error.app.mjs", + "keywords": [ + "pipedream", + "error" + ], + "homepage": "https://pipedream.com/apps/error", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/escrow/README.md b/components/escrow/README.md index 2f1a1f6a56399..0683ad9d4c31f 100644 --- a/components/escrow/README.md +++ b/components/escrow/README.md @@ -1,11 +1,11 @@ # Overview -The Escrow.com API allows developers to create applications that can connect to -the Escrow.com platform and perform various tasks. +The Escrow.com API provides programmatic access to escrow services, allowing secure transactions where funds are held until predetermined conditions are met. By integrating with Pipedream, you can streamline the process of managing escrow transactions, automate updates to all parties involved, and link Escrow.com with other business tools for seamless financial operations. The API can trigger actions like creating transactions, checking status updates, and automating payments, ensuring a secure and efficient transaction environment. -Some examples of what you can build using the Escrow.com API include: +# Example Use Cases -- An application that allows users to buy and sell items using Escrow.com -- A tool that helps users to manage their Escrow.com account -- A platform that allows developers to create and manage their own Escrow.com - integrations +- **Automated Transaction Creation for Marketplaces**: When a purchase is made on a marketplace platform, Pipedream can automatically initiate an escrow transaction via the Escrow.com API, ensuring funds are securely held until the buyer confirms receipt and satisfaction with the goods or services. + +- **Real-time Status Updates to CRM Systems**: Keep sales teams informed by using Pipedream to watch for changes in transaction status on Escrow.com. Upon a status change, the workflow could update the corresponding deal or contact record in a CRM like Salesforce, ensuring the sales team has the latest information without manual checks. + +- **Payment Completion Upon Delivery Confirmation**: Connect the Escrow.com API to shipping services like FedEx or UPS within Pipedream. When delivery confirmation is received, the workflow can automatically instruct Escrow.com to release funds to the seller, streamlining the last step of the transaction process. diff --git a/components/escrow/package.json b/components/escrow/package.json new file mode 100644 index 0000000000000..6e9bcc879d452 --- /dev/null +++ b/components/escrow/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/escrow", + "version": "0.6.0", + "description": "Pipedream escrow Components", + "main": "escrow.app.mjs", + "keywords": [ + "pipedream", + "escrow" + ], + "homepage": "https://pipedream.com/apps/escrow", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/esendex/README.md b/components/esendex/README.md new file mode 100644 index 0000000000000..6a12263d24c50 --- /dev/null +++ b/components/esendex/README.md @@ -0,0 +1,11 @@ +# Overview + +The Esendex API enables seamless SMS and voice messaging integration into your applications, allowing for automated communication with customers or systems. On Pipedream, you can harness this API to create serverless workflows that react to various triggers, from inbound messages to scheduled times, and interact with other services to provide a cohesive automation experience. + +# Example Use Cases + +- **Automated Customer Support Notifications**: Trigger a workflow on Pipedream when a new ticket is created in your helpdesk software, like Zendesk, and use the Esendex API to send an SMS update to the customer with their ticket number and expected response time. + +- **Appointment Reminder System**: Use a scheduled trigger in Pipedream to sift through a Google Calendar or database for upcoming appointments and send a reminder via Esendex SMS to clients 24 hours before their scheduled time. + +- **Real-time Alerts for Server Downtime**: Connect Pipedream to monitoring tools like Datadog to initiate a workflow when a server issue is detected. Use the Esendex API to dispatch immediate SMS alerts to your IT team for rapid response. diff --git a/components/esignatures_io/README.md b/components/esignatures_io/README.md index f7160e5af225c..156dae2ac68ce 100644 --- a/components/esignatures_io/README.md +++ b/components/esignatures_io/README.md @@ -1,17 +1,11 @@ # Overview -The eSignatures.io API enables you to create, manage, and automate eSignatures -and eSignature workflows. With the eSignatures.io API, you can: +The eSignatures.io API enables automated, legally-binding online contract signing. With this API, you can generate contracts from templates, send them to signees, and track the entire signing process. Leveraging this functionality within Pipedream allows you to create powerful automations that streamline contract management, integrate with CRM systems, trigger actions upon contract completion, and much more, eliminating manual tasks and enhancing productivity. -- Create eSignature documents -- Manage eSignature workflows -- Automate eSignature document creation and management -- Integrate eSignature functionality into your existing workflow +# Example Use Cases -Here are a few examples of what you can build using the eSignatures.io API: +- **Contract Generation on New CRM Deal**: When a new deal is marked as won in a CRM like HubSpot, automatically generate a contract via eSignatures.io and send it to the client. Streamline the sales process by ensuring contracts are sent out immediately following a deal's closure. -- A web application that allows users to create and manage eSignature documents -- A mobile application that allows users to sign eSignature documents -- A service that automates the creation and management of eSignature workflows -- An integration that allows you to sign eSignature documents from within your - existing workflow +- **Contract Status Tracking in Project Management Tools**: Monitor contract signing status and, upon completion, update tasks in project management apps like Trello or Asana. This can automate the transition of tasks from 'pending contract' to 'in progress', ensuring project workflows are updated in real-time. + +- **Automated Follow-up Emails Post-Signing**: After a contract is fully signed, trigger an email through an email service like SendGrid to thank the signees, provide additional resources, or outline next steps. This can enhance client relations and ensure timely communication. diff --git a/components/espocrm/README.md b/components/espocrm/README.md new file mode 100644 index 0000000000000..25615de27d6cd --- /dev/null +++ b/components/espocrm/README.md @@ -0,0 +1,11 @@ +# Overview + +The EspoCRM API allows for robust interaction with your CRM data, enabling the automation of tasks, syncing data across platforms, and the enhancement of customer relationship management through customized workflows. By leveraging this API within Pipedream, you can design serverless workflows that react to CRM events, integrate with other services, and manipulate data to fit your business processes. Pipedream's no-code platform makes it straightforward to create, execute, and maintain these workflows, allowing you to focus on innovation rather than infrastructure. + +# Example Use Cases + +- **Lead Auto-qualification Workflow**: When a new lead is created in EspoCRM, this Pipedream workflow can automatically qualify leads based on predefined criteria such as location, budget, or industry. If the lead meets the criteria, the workflow could update the lead's status in EspoCRM and notify the sales team via Slack. + +- **Support Ticket Integration**: This Pipedream workflow watches for new support cases in EspoCRM. When a case is created, it gathers additional information from external sources (e.g., past orders from an e-commerce platform like Shopify) and updates the case with this data. It could also prioritize the case and assign it to the relevant agent based on workload or expertise. + +- **Contact Sync Across Platforms**: Maintain a synchronized contact list between EspoCRM and other platforms such as Mailchimp for email marketing campaigns. Whenever a contact is updated in EspoCRM, Pipedream can detect the change, process it, and update the corresponding contact list in Mailchimp, ensuring consistent information across your marketing tools. diff --git a/components/esputnik/README.md b/components/esputnik/README.md index 9438a9f628f7e..2020e75247d45 100644 --- a/components/esputnik/README.md +++ b/components/esputnik/README.md @@ -1,10 +1,11 @@ # Overview -With the eSputnik API, you can build a variety of applications and integrations -to automate your email marketing. Here are a few examples: - -- Create and send automated email campaigns -- Create and manage your email contact lists -- Build custom integrations with your CRM or ecommerce platform -- Segment your contacts according to their behavior or interests -- Analyze your email marketing performance with detailed reporting +The eSputnik API is a powerful tool for managing and automating your email marketing campaigns and other messaging services. Through the API, you can create targeted email sequences, manage contact lists, track email metrics, and dynamically segment your audience based on their interactions. By leveraging Pipedream's capabilities, you can connect eSputnik with hundreds of other apps to synchronize your marketing efforts, automate responses based on customer behavior, and analyze the effectiveness of your campaigns with advanced analytics. + +# Example Use Cases + +- **Automated Welcome Email Sequence**: Trigger an automated email sequence in eSputnik when a user signs up on your platform via Typeform. Use Pipedream to capture the signup event and initiate a personalized onboarding email series, ensuring new users receive timely and relevant information to engage with your service. + +- **Dynamic Audience Segmentation**: Sync customer purchase data from Shopify to eSputnik and use Pipedream to apply tags or move contacts into different segments based on their shopping behavior. This enables targeted campaigns and follow-ups that resonate with customers' specific interests and purchase history. + +- **Campaign Performance Dashboard**: Collect campaign performance data from eSputnik and send it to Google Sheets using Pipedream. Create a real-time dashboard in Google Sheets to visualize open rates, click-through rates, and conversion metrics, allowing for quick analysis and strategy adjustments. diff --git a/components/estreamdesk/README.md b/components/estreamdesk/README.md index ce21c64db96fe..a0a0ef3f5a05f 100644 --- a/components/estreamdesk/README.md +++ b/components/estreamdesk/README.md @@ -1,8 +1,11 @@ # Overview -The eStreamDesk API allows developers to access and integrate the functionality -of eStreamDesk with other applications. +The eStreamDesk API facilitates the creation, manipulation, and tracking of support tickets within the eStreamDesk helpdesk system. By leveraging the API on Pipedream, you can seamlessly connect your customer support operations with other services to automate notifications, synchronize data across platforms, and trigger custom workflows. This can lead to improved response times, better customer service, and more efficient resolution of support issues. -Some example API methods include managing account information, retrieving -files, and managing file storage. eStreamDesk is a file-sharing and management -service. +# Example Use Cases + +- **Ticket Creation via Email Trigger**: When an email is received by a specific address, Pipedream can parse the email content and automatically create a new support ticket in eStreamDesk. This ensures that customer inquiries are promptly logged as tickets in the helpdesk system without manual intervention. + +- **Slack Notification on Ticket Update**: Configure a workflow where any update to a support ticket in eStreamDesk triggers an automated notification in a designated Slack channel. This keeps the team instantly informed about ticket progress without the need to constantly check the helpdesk dashboard. + +- **Sync Support Tickets with CRM**: Integrate eStreamDesk with a CRM platform such as Salesforce. Whenever a support ticket is closed, the workflow can automatically update the corresponding customer record in Salesforce with the resolution details, maintaining a unified customer support history. diff --git a/components/etermin/README.md b/components/etermin/README.md index c7aa580e4ef8d..fcfeb626edf2f 100644 --- a/components/etermin/README.md +++ b/components/etermin/README.md @@ -1,12 +1,11 @@ # Overview -With the eTermin API, you can build software that allows users to: +The eTermin API empowers users to automate scheduling and calendar management tasks. With it, you can sync appointments, manage bookings, and update event details seamlessly. Pipedream's capabilities allow you to integrate these features into a multitude of workflows, connecting eTermin's API with other services to create powerful automation that can save time and reduce manual effort. From synchronizing appointments with Google Calendar to sending SMS reminders via Twilio, the possibilities stretch as far as your creativity. -- Book and cancel appointments -- Look up available appointment times -- Check in for appointments -- Receive notifications about upcoming appointments -- Manage their account settings +# Example Use Cases -This is just a small sample of what you can build – the possibilities are -endless! +- **Appointment Synchronization with Google Calendar**: Sync new eTermin appointments to a Google Calendar, ensuring all your events are up to date across platforms. When a new appointment is created in eTermin, a Pipedream workflow can automatically add that appointment to a specified Google Calendar, with details like time, date, and participant information. + +- **Customer Follow-Up with Email**: After an appointment is completed, trigger a workflow to send a follow-up email via SendGrid. This could include a personalized thank you, a request for feedback, or an invitation to book another appointment. The workflow would listen for a 'completed appointment' event from eTermin and use customer details to craft and send a tailored email. + +- **SMS Appointment Reminders**: Set up a Pipedream workflow to send SMS reminders to clients a day before their scheduled appointment. Using the eTermin API to fetch upcoming appointments and Twilio to send out SMS messages, this automation ensures clients are reminded of their appointments, which can help reduce no-shows and improve customer satisfaction. diff --git a/components/etermin/package.json b/components/etermin/package.json new file mode 100644 index 0000000000000..22587b6fd5925 --- /dev/null +++ b/components/etermin/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/etermin", + "version": "0.6.0", + "description": "Pipedream etermin Components", + "main": "etermin.app.mjs", + "keywords": [ + "pipedream", + "etermin" + ], + "homepage": "https://pipedream.com/apps/etermin", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/ethereum/README.md b/components/ethereum/README.md index 0e766085eb28e..32109d0e189a1 100644 --- a/components/ethereum/README.md +++ b/components/ethereum/README.md @@ -1,11 +1,11 @@ # Overview -The Etherscan API allows you to query various information on the Ethereum -blockchain. Some of the things you can do with the API include: - -- Get a list of all transactions for a given address -- Get the balance for a given address -- Get a list of all tokens for a given address -- Get the total supply of a given token -- Get a list of all tokens traded on a given day -- Get the price of a given token in USD +The Etherscan API provides a lens into the Ethereum blockchain, allowing you to query for transaction details, wallet balances, smart contract events, and more. By bringing Etherscan into Pipedream's serverless integration platform, you can automate interactions with Ethereum data, create event-driven workflows, and connect blockchain insights to hundreds of other apps without a line of backend code. + +# Example Use Cases + +- **Monitor Wallet Transactions**: Trigger a workflow whenever a transaction occurs involving a specific Ethereum wallet. Use this to notify yourself via services like Slack or email, or archive this data for analysis in Google Sheets or a database. + +- **Track Smart Contract Events**: Create automations that respond to specific events emitted by a smart contract. For instance, when a new token is minted or transferred, trigger a workflow that updates a dashboard in real-time or alerts a Discord channel for community engagement. + +- **Analyze Gas Prices for Transactions**: Schedule a workflow to fetch current Ethereum gas prices at regular intervals. Use this data to advise on the best times for executing transactions, and automatically adjust your app's suggested fees, or inform users via a bot on Telegram or SMS. diff --git a/components/etsy/README.md b/components/etsy/README.md index 42bca726e8bdd..a1eb60cea0628 100644 --- a/components/etsy/README.md +++ b/components/etsy/README.md @@ -1,6 +1,15 @@ # Overview Etsy is a global e-commerce platform specializing in handmade, vintage, and craft items. It offers a marketplace where artisans can connect directly with buyers. The term 'Etsy' is a trademark of Etsy, Inc. This application uses the Etsy API but is not endorsed or certified by Etsy. +The Etsy API provides a rich interface to interact with the popular e-commerce platform known for its unique, handmade, and vintage items. Using the Etsy API via Pipedream, you can automate various aspects of your Etsy shop, including inventory management, order processing, and customer engagement. It opens up possibilities for streamlining mundane tasks, reacting to shop events in real-time, and syncing data across different platforms, enhancing the efficiency and responsiveness of your online business operations. + # Getting Started You must have an Etsy shop in order to use this integration. If you don't already have one, please create one [here](https://www.etsy.com/sell) first. +# Example Use Cases + +- **Automated Order Processing Workflow**: Trigger a Pipedream workflow when a new sale occurs on Etsy. The workflow could automatically send order details to a Google Sheet for record-keeping, email the customer a personalized thank you message using SendGrid, and generate a shipping label through a service like EasyPost. + +- **Inventory Management System**: Implement a workflow that monitors your stock levels by regularly checking your Etsy listings. When the inventory for a specific item falls below a predefined threshold, the workflow could trigger a purchase order to your supplier through an email, create a task in a project management tool like Trello or Asana, or send you an alert via SMS using Twilio. + +- **Customer Engagement Enhancer**: Create a workflow that listens for reviews posted by customers on your Etsy shop. When a new review is detected, it can be automatically posted to your business's Twitter account to share positive feedback, or you might use the sentiment analysis from an app like MonkeyLearn to categorize the review and respond accordingly, whether that's thanking the customer or addressing concerns raised. diff --git a/components/etsy/actions/create-draft-listing-product/create-draft-listing-product.mjs b/components/etsy/actions/create-draft-listing-product/create-draft-listing-product.mjs index f1abc5ab446f8..4ed7233b221a4 100644 --- a/components/etsy/actions/create-draft-listing-product/create-draft-listing-product.mjs +++ b/components/etsy/actions/create-draft-listing-product/create-draft-listing-product.mjs @@ -1,12 +1,12 @@ -import app from "../../etsy.app.mjs"; import constants from "../../common/constants.mjs"; +import app from "../../etsy.app.mjs"; export default { key: "etsy-create-draft-listing-product", name: "Create Draft Listing Product", description: "Creates a physical draft listing product in a shop on the Etsy channel. [See the Documentation](https://developers.etsy.com/documentation/reference#operation/createDraftListing)", type: "action", - version: "0.0.2", + version: "0.0.3", props: { app, quantity: { diff --git a/components/etsy/actions/delete-listing/delete-listing.mjs b/components/etsy/actions/delete-listing/delete-listing.mjs index 7f42c5bb5c273..1c82a403dff41 100644 --- a/components/etsy/actions/delete-listing/delete-listing.mjs +++ b/components/etsy/actions/delete-listing/delete-listing.mjs @@ -5,7 +5,7 @@ export default { name: "Delete Listing", description: "Open API V3 endpoint to delete a ShopListing. A ShopListing can be deleted only if the state is one of the following: `SOLD_OUT`, `DRAFT`, `EXPIRED`, `INACTIVE`, `ACTIVE` and `is_available` or `ACTIVE` and has seller flags: `SUPRESSED` (frozen), `VACATION`, `CUSTOM_SHOPS` (pattern), `SELL_ON_FACEBOOK`. [See the Documentation](https://developers.etsy.com/documentation/reference#operation/deleteListing)", type: "action", - version: "0.0.2", + version: "0.0.3", props: { app, shopId: { diff --git a/components/etsy/actions/get-listing-inventory/get-listing-inventory.mjs b/components/etsy/actions/get-listing-inventory/get-listing-inventory.mjs new file mode 100644 index 0000000000000..d983e56a9c66d --- /dev/null +++ b/components/etsy/actions/get-listing-inventory/get-listing-inventory.mjs @@ -0,0 +1,37 @@ +import app from "../../etsy.app.mjs"; + +export default { + key: "etsy-get-listing-inventory", + name: "Get Listing Inventory", + description: "Retrieves the inventory record for a listing by listing ID. [See the Documentation](https://developer.etsy.com/documentation/reference/#operation/getListingInventory)", + type: "action", + version: "0.0.1", + props: { + app, + state: { + propDefinition: [ + app, + "state", + ], + }, + listingId: { + propDefinition: [ + app, + "listingId", + ({ state }) => ({ + state, + }), + ], + }, + }, + async run({ $: step }) { + const response = await this.app.getListingInventory({ + step, + listingId: this.listingId, + }); + + step.export("$summary", `Successfully retrieved listing inventory with ID ${this.listingId}.`); + + return response; + }, +}; diff --git a/components/etsy/actions/get-listing/get-listing.mjs b/components/etsy/actions/get-listing/get-listing.mjs index a946dbfc66891..19b334a6989f1 100644 --- a/components/etsy/actions/get-listing/get-listing.mjs +++ b/components/etsy/actions/get-listing/get-listing.mjs @@ -5,7 +5,7 @@ export default { name: "Get Listing", description: "Retrieves a listing record by listing ID. [See the Documentation](https://developers.etsy.com/documentation/reference#operation/getListing)", type: "action", - version: "0.0.2", + version: "0.0.3", props: { app, shopId: { diff --git a/components/etsy/actions/update-listing-inventory/update-listing-inventory.mjs b/components/etsy/actions/update-listing-inventory/update-listing-inventory.mjs new file mode 100644 index 0000000000000..efcab2891ac33 --- /dev/null +++ b/components/etsy/actions/update-listing-inventory/update-listing-inventory.mjs @@ -0,0 +1,106 @@ +import utils from "../../common/utils.mjs"; +import app from "../../etsy.app.mjs"; + +export default { + key: "etsy-update-listing-inventory", + name: "Update Listing Inventory", + description: "Updates the inventory for a listing identified by a listing ID. [See the Documentation](https://developer.etsy.com/documentation/reference/#operation/getListingInventory)", + type: "action", + version: "0.0.1", + props: { + app, + state: { + propDefinition: [ + app, + "state", + ], + }, + listingId: { + propDefinition: [ + app, + "listingId", + ({ + shopId, state, + }) => ({ + shopId, + state, + }), + ], + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.listingId) { + const listing = await this.app.getListingInventory({ + listingId: this.listingId, + }); + props.products = { + type: "string[]", + label: "Products", + description: "A list of products available in a listing, All field names in the object are lowercase. E.g. {\"sku\": \"string\",\"property_values\": [],\"offerings\": [{\"price\": 0,\"quantity\": 0,\"is_enabled\": true}]}", + default: listing.products.map((item) => { + delete item.product_id; + delete item.is_deleted; + item.offerings = item.offerings.map((offering) => { + delete offering.offering_id, + delete offering.is_deleted; + offering.price = offering.price.amount; + return offering; + }); + return JSON.stringify(item); + + }), + optional: true, + }; + props.priceOnProperty = { + type: "string[]", + label: "Price On Property", + description: "A list of unique listing property ID integers for the properties that change product prices, if any. For example, if you charge specific prices for different sized products in the same listing, then this array contains the property ID for size.", + default: listing.price_on_property, + optional: true, + }; + props.quantityOnProperty = { + type: "string[]", + label: "Quantity On Property", + description: "A list of unique listing property ID integers for the properties that change the quantity of the products, if any. For example, if you stock specific quantities of different colored products in the same listing, then this array contains the property ID for color.", + default: listing.quantity_on_property, + optional: true, + }; + props.skuOnProperty = { + type: "string[]", + label: "SKU On Property", + description: "A list of unique listing property ID integers for the properties that change the product SKU, if any. For example, if you use specific skus for different colored products in the same listing, then this array contains the property ID for color.", + default: listing.sku_on_property, + optional: true, + }; + } + return props; + }, + methods: { + updateListingInventory({ + listingId, ...args + }) { + return this.app.put({ + path: `/application/listings/${listingId}/inventory`, + ...args, + }); + }, + }, + async run({ $: step }) { + const response = await this.updateListingInventory({ + step, + listingId: this.listingId, + data: { + products: this.products && utils.parseObject(this.products), + price_on_property: this.priceOnProperty, + quantity_on_property: this.quantityOnProperty, + sku_on_property: this.skuOnProperty, + }, + }); + + step.export("$summary", `Successfully updated listing inventory with ID ${this.listingId}.`); + + return response; + }, +}; diff --git a/components/etsy/actions/update-listing-property/update-listing-property.mjs b/components/etsy/actions/update-listing-property/update-listing-property.mjs index db86f2db7c36a..eaffd051dda20 100644 --- a/components/etsy/actions/update-listing-property/update-listing-property.mjs +++ b/components/etsy/actions/update-listing-property/update-listing-property.mjs @@ -1,12 +1,12 @@ -import app from "../../etsy.app.mjs"; import propertyValues from "../../common/property-values.mjs"; +import app from "../../etsy.app.mjs"; export default { key: "etsy-update-listing-property", name: "Update Listing Property", description: "Updates or populates the properties list defining product offerings for a listing. Each offering requires both a `value` and a `value_id` that are valid for a `scale_id` assigned to the listing or that you assign to the listing with this request. [See the Documentation](https://developers.etsy.com/documentation/reference#operation/updateListingProperty)", type: "action", - version: "0.0.2", + version: "0.0.3", props: { app, shopId: { diff --git a/components/etsy/common/utils.mjs b/components/etsy/common/utils.mjs index a7e8a35885f72..30cc4ba114856 100644 --- a/components/etsy/common/utils.mjs +++ b/components/etsy/common/utils.mjs @@ -6,6 +6,26 @@ async function streamIterator(stream) { return resources; } +function parseObject(obj) { + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + return JSON.parse(obj); + } + return obj; +} + export default { streamIterator, + parseObject, }; diff --git a/components/etsy/etsy.app.mjs b/components/etsy/etsy.app.mjs index 2266098768e58..f1bc0c6362ebe 100644 --- a/components/etsy/etsy.app.mjs +++ b/components/etsy/etsy.app.mjs @@ -212,6 +212,14 @@ export default { ...args, }); }, + getListingInventory({ + listingId, ...args + }) { + return this.makeRequest({ + path: `/application/listings/${listingId}/inventory`, + ...args, + }); + }, getListingProperties({ shopId, listingId, ...args } = {}) { diff --git a/components/etsy/package.json b/components/etsy/package.json index 07d08f31a83e0..d6e2358da61cb 100644 --- a/components/etsy/package.json +++ b/components/etsy/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/etsy", - "version": "0.1.1", + "version": "0.2.0", "description": "Pipedream Etsy Components", "main": "etsy.app.mjs", "keywords": [ diff --git a/components/etsy/sources/listing-updated/listing-updated.mjs b/components/etsy/sources/listing-updated/listing-updated.mjs index 1cc8b591ab597..57a8fc8a19da8 100644 --- a/components/etsy/sources/listing-updated/listing-updated.mjs +++ b/components/etsy/sources/listing-updated/listing-updated.mjs @@ -1,5 +1,5 @@ -import common from "../common/polling.mjs"; import constants from "../../common/constants.mjs"; +import common from "../common/polling.mjs"; export default { ...common, @@ -7,7 +7,7 @@ export default { name: "Listing Updated", description: "Emit new event when a listing is updated. [See the Documentation](https://developers.etsy.com/documentation/reference#operation/getListingsByShop)", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", props: { ...common.props, diff --git a/components/etsy/sources/new-shop-receipt/new-shop-receipt.mjs b/components/etsy/sources/new-shop-receipt/new-shop-receipt.mjs index e1e7bedfc0642..2fdd280d7f3ee 100644 --- a/components/etsy/sources/new-shop-receipt/new-shop-receipt.mjs +++ b/components/etsy/sources/new-shop-receipt/new-shop-receipt.mjs @@ -1,5 +1,5 @@ -import common from "../common/polling.mjs"; import constants from "../../common/constants.mjs"; +import common from "../common/polling.mjs"; export default { ...common, @@ -7,7 +7,7 @@ export default { name: "New Shop Receipt", description: "Emit new event when a new shop receipt is created. [See the Documentation](https://developers.etsy.com/documentation/reference#operation/getShopReceipts)", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", methods: { ...common.methods, diff --git a/components/etsy/sources/new-transaction/new-transaction.mjs b/components/etsy/sources/new-transaction/new-transaction.mjs index 4b7b1dd40b32c..738d5bde30c87 100644 --- a/components/etsy/sources/new-transaction/new-transaction.mjs +++ b/components/etsy/sources/new-transaction/new-transaction.mjs @@ -1,5 +1,5 @@ -import common from "../common/polling.mjs"; import constants from "../../common/constants.mjs"; +import common from "../common/polling.mjs"; export default { ...common, @@ -7,7 +7,7 @@ export default { name: "New Transaction", description: "Emit new event when a new transaction is created. [See the Documentation](https://developers.etsy.com/documentation/reference#operation/getShopReceiptTransactionsByShop)", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", methods: { ...common.methods, diff --git a/components/evenium/README.md b/components/evenium/README.md index bfc27cc1890d2..b37d43c9ae6c6 100644 --- a/components/evenium/README.md +++ b/components/evenium/README.md @@ -1,12 +1,11 @@ # Overview -Evenium is a powerful API that enables developers to build a wide range of -applications. Here are some examples of what you can build with the Evenium -API: - -- A ticketing system for events -- A venue management system -- An event management system -- A registration system for events -- A payment system for events -- A social media platform for events +The Evenium API offers a toolkit for automating and integrating event management tasks into Pipedream's serverless platform. With the Evenium API, you can create workflows that automate event creation, attendee management, and real-time analytics tracking. This enables you to seamlessly synchronize event details with other apps, send personalized communications, and manage registrations with minimal manual intervention. + +# Example Workflows + +- **Automated Event Creation and Calendar Integration**: Set up a workflow where event details inputted into a Google Sheet are automatically used to create an event on Evenium. Then, trigger an action that adds this event to a Google Calendar, effectively syncing event planning across platforms. + +- **Dynamic Attendee Communication**: Craft a workflow that listens for new registrations on Evenium and automatically sends personalized email confirmations via SendGrid. Additionally, set up reminders or follow-up surveys post-event to engage with attendees and gather feedback. + +- **Real-time Analytics Dashboard**: Configure a workflow to pull event attendance data from Evenium and push it to a tool like Google Data Studio. This allows for the creation of a live dashboard that tracks attendee metrics, engagement levels, and overall event success in real-time. diff --git a/components/evenium/package.json b/components/evenium/package.json new file mode 100644 index 0000000000000..efe961caa5674 --- /dev/null +++ b/components/evenium/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/evenium", + "version": "0.6.0", + "description": "Pipedream evenium Components", + "main": "evenium.app.mjs", + "keywords": [ + "pipedream", + "evenium" + ], + "homepage": "https://pipedream.com/apps/evenium", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/eventbrite/README.md b/components/eventbrite/README.md index c3def8cd97f3c..9fdf7003de37d 100644 --- a/components/eventbrite/README.md +++ b/components/eventbrite/README.md @@ -1,14 +1,11 @@ # Overview -With the Eventbrite API, you can build applications that: +The Eventbrite API offers a powerful way to create, manage, and attend events programmatically. With Pipedream, you can leverage this API to automate a swath of tasks such as syncing attendee data, updating events in real-time, and connecting Eventbrite to other apps to streamline event workflows. By employing Pipedream's serverless platform, you can engineer intricate automations that respond to event triggers (like new event creation or ticket purchase) and conduct actions across your software stack without writing extensive code. -- Help event organizers sell tickets and promote their events -- Allow eventgoers to find and buy tickets to events -- Provide tools for event organizers to manage their events +# Example Use Cases -Here are some examples of things you can build using the Eventbrite API: +- **Automated Attendee Management**: Sync new Eventbrite attendees to Google Sheets in real-time. When a new attendee registers, trigger a Pipedream workflow that adds their details to a Google Sheet, enabling easy access and manipulation of attendee data for further event management and analytics. -- A ticket sales and event promotion website or app -- An event ticketing website or app -- A event management tool for event organizers -- A event search engine +- **Event Creation Broadcast**: Automatically post new Eventbrite events to social media platforms. Use Pipedream to detect when a new event is created on Eventbrite, and then craft and send tailored announcements to Twitter, LinkedIn, and Facebook, amplifying your event's reach without manual intervention. + +- **Post-Event Feedback Collection**: Send a follow-up survey to attendees after an event ends. Set up a Pipedream workflow that triggers when an Eventbrite event concludes, then use the SendGrid app to email attendees a feedback form. Aggregate and analyze responses to improve future events. diff --git a/components/eventee/README.md b/components/eventee/README.md new file mode 100644 index 0000000000000..0a4459d278736 --- /dev/null +++ b/components/eventee/README.md @@ -0,0 +1,11 @@ +# Overview + +Eventee API allows you to integrate your event management capabilities seamlessly into Pipedream's serverless platform. With Eventee, you can automate tasks such as event creation, attendee management, and real-time updates for your conferences or gatherings. In Pipedream, you can create workflows that trigger on specific Eventee events, process data, and connect with other apps to streamline your event coordination efforts. + +# Example Use Cases + +- **Automated Event Creation and Announcement**: When a new event is planned, use Eventee to create the event and then automatically announce it via platforms like Slack or email. This can be set up in Pipedream to trigger when a new event is added to a project management tool like Trello or Asana. + +- **Attendee Registration Sync**: Sync new attendee registrations from Eventee to a Google Sheet or a CRM like Salesforce. Each time an attendee registers through Eventee, a Pipedream workflow can add their details to your chosen app for easy tracking and management. + +- **Real-time Event Updates**: Send real-time updates to attendees via SMS or messaging apps such as WhatsApp or Telegram whenever there's a change in the event schedule. Pipedream can listen for updates from Eventee and relay them instantly to keep everyone informed. diff --git a/components/eventee/package.json b/components/eventee/package.json index d72fb65ba64df..3f57756f27c73 100644 --- a/components/eventee/package.json +++ b/components/eventee/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/eventzilla/README.md b/components/eventzilla/README.md index eec56e13333ba..3a3fcff2a4069 100644 --- a/components/eventzilla/README.md +++ b/components/eventzilla/README.md @@ -1,11 +1,11 @@ # Overview -With Eventzilla, you can easily craft beautiful event pages and manage -registrations with ease. +The Eventzilla API allows you to seamlessly integrate your event management into a variety of workflows and systems. With the API, you can automate the retrieval of event details, participant data, and handle registrations programmatically. It’s ideal for syncing event information across platforms, streamlining communication with attendees, and enhancing marketing efforts through targeted campaigns and analytics. -Here are some examples of what you can build with Eventzilla: +# Example Use Cases -- A stunning event page that will grab your attendees' attention -- A registration form that is easy for your attendees to fill out -- A event management system that will help you keep track of your attendees and - your event's progress +- **Automated Attendee Welcome Messages**: Trigger an email or SMS to attendees via SendGrid or Twilio when they register for an event on Eventzilla. This can provide them with additional information or a personal welcome, enhancing their experience from the start. + +- **Event Performance Dashboards**: Connect Eventzilla to Google Sheets or a BI tool like Tableau to automatically gather data on ticket sales, attendee demographics, and feedback. Use this data to create real-time dashboards to monitor event performance and gain actionable insights. + +- **Synchronized Marketing Campaigns**: Integrate Eventzilla with Mailchimp or a similar marketing platform to sync attendee lists. Leverage this integration to automate follow-up emails, post-event surveys, and targeted marketing campaigns for future events based on attendee interest and behavior. diff --git a/components/eventzilla/package.json b/components/eventzilla/package.json new file mode 100644 index 0000000000000..cb91a9e325b9f --- /dev/null +++ b/components/eventzilla/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/eventzilla", + "version": "0.6.0", + "description": "Pipedream eventzilla Components", + "main": "eventzilla.app.mjs", + "keywords": [ + "pipedream", + "eventzilla" + ], + "homepage": "https://pipedream.com/apps/eventzilla", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/everhour/README.md b/components/everhour/README.md index 41d43adcb69d7..24e76518d5f60 100644 --- a/components/everhour/README.md +++ b/components/everhour/README.md @@ -1,16 +1,11 @@ # Overview -Everhour provides an API that gives developers access to theEverhour data. This -allows developers to build integrations withEverhour that can display data in -new ways, or automate tasks such as tracking time or creating projects. +Everhour is a time tracking API that allows you to monitor project hours and manage tasks effectively. With Pipedream, you can automate workflows by integrating Everhour with various apps, streamlining time entry, syncing with project management tools, and generating custom reports. Whether you're consolidating time tracking data for invoicing or keeping project budgets in check, Everhour and Pipedream make a powerful duo for automating your time tracking and project management processes. -Some examples of what can be built using the Everhour API include: +# Example Use Cases -- A time tracking app that integrates with Everhour to display data in a new - way, or automate tasks such as tracking time or creating projects. -- A project management app that uses Everhour to track project progress and - tasks. -- An invoicing app that integrates with Everhour to create invoices based on - time tracking data. -- A reporting app that uses Everhour data to generate reports on project - progress or team performance. +- **Time Entry Automation**: Triggered when a task is marked complete in a project management tool like Asana, this workflow automatically logs time in Everhour for that task. It ensures accurate and timely updates to the time tracking system, without manual entry. + +- **Project Budget Monitoring**: Monitor project hours and budgets by setting up a workflow that sends a Slack alert when a project reaches a certain percentage of its allocated budget. This workflow uses the Everhour API to track time spent and calculates budget usage, keeping team members informed in real-time. + +- **Invoice Generation and Delivery**: At the end of a billing cycle, automatically generate an invoice based on tracked time from Everhour, and send it to clients via email (using SendGrid or another email service). This workflow can also attach detailed time reports, ensuring clients receive transparent and accurate billing information. diff --git a/components/everhour/actions/create-task/create-task.mjs b/components/everhour/actions/create-task/create-task.mjs new file mode 100644 index 0000000000000..af6ce6cea5bdb --- /dev/null +++ b/components/everhour/actions/create-task/create-task.mjs @@ -0,0 +1,84 @@ +import { STATUS_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import everhour from "../../everhour.app.mjs"; + +export default { + key: "everhour-create-task", + name: "Create Task", + description: "Creates a new task in Everhour. [See the documentation](https://everhour.docs.apiary.io/)", + version: "0.0.1", + type: "action", + props: { + everhour, + projectId: { + propDefinition: [ + everhour, + "projectId", + ], + }, + name: { + type: "string", + label: "Task Name", + description: "The name of the task to be created.", + }, + sectionId: { + propDefinition: [ + everhour, + "sectionId", + ({ projectId }) => ({ + projectId, + }), + ], + }, + tags: { + propDefinition: [ + everhour, + "tags", + ], + optional: true, + }, + position: { + type: "integer", + label: "Position", + description: "The position of the task", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "A description of the task", + optional: true, + }, + dueOn: { + type: "string", + label: "Due Date", + description: "The due date of the task. **Format: YYYY-MM-DD**", + optional: true, + }, + status: { + type: "string", + label: "Status", + description: "The status of the task", + options: STATUS_OPTIONS, + optional: true, + }, + }, + async run({ $ }) { + const response = await this.everhour.createTask({ + $, + projectId: this.projectId, + data: { + name: this.name, + section: this.sectionId, + tags: this.tags && parseObject(this.tags), + position: this.position, + description: this.description, + dueOn: this.dueOn, + status: this.status, + }, + }); + + $.export("$summary", `Successfully created task with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/everhour/actions/start-timer/start-timer.mjs b/components/everhour/actions/start-timer/start-timer.mjs new file mode 100644 index 0000000000000..81d203782731c --- /dev/null +++ b/components/everhour/actions/start-timer/start-timer.mjs @@ -0,0 +1,52 @@ +import everhour from "../../everhour.app.mjs"; + +export default { + key: "everhour-start-timer", + name: "Start Timer", + description: "Begins a new timer for a task. [See the documentation](https://everhour.docs.apiary.io/#reference/0/timers/start-timer)", + version: "0.0.1", + type: "action", + props: { + everhour, + projectId: { + propDefinition: [ + everhour, + "projectId", + ], + }, + taskId: { + propDefinition: [ + everhour, + "taskId", + ({ projectId }) => ({ + projectId, + }), + ], + }, + userDate: { + type: "string", + label: "User Date", + description: "Date string to associate with the timer. Format as 'YYYY-MM-DD'", + optional: true, + }, + comment: { + type: "string", + label: "Comment", + description: "An optional comment to associate with the timer", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.everhour.startTimer({ + $, + data: { + task: this.taskId, + userDate: this.userDate, + comment: this.comment, + }, + }); + + $.export("$summary", `Successfully started a timer for task ID: ${this.taskId}`); + return response; + }, +}; diff --git a/components/everhour/actions/stop-timer/stop-timer.mjs b/components/everhour/actions/stop-timer/stop-timer.mjs new file mode 100644 index 0000000000000..e533e1afe3c65 --- /dev/null +++ b/components/everhour/actions/stop-timer/stop-timer.mjs @@ -0,0 +1,17 @@ +import everhour from "../../everhour.app.mjs"; + +export default { + key: "everhour-stop-timer", + name: "Stop Timer", + description: "Halts the current running timer. [See the documentation](https://everhour.docs.apiary.io/#reference/timers/stop-timer)", + version: "0.0.1", + type: "action", + props: { + everhour, + }, + async run({ $ }) { + const response = await this.everhour.stopTimer(); + $.export("$summary", "Successfully stopped the timer"); + return response; + }, +}; diff --git a/components/everhour/common/constants.mjs b/components/everhour/common/constants.mjs new file mode 100644 index 0000000000000..5f6b8b3d178be --- /dev/null +++ b/components/everhour/common/constants.mjs @@ -0,0 +1,12 @@ +export const LIMIT = 100; + +export const STATUS_OPTIONS = [ + { + label: "Open", + value: "open", + }, + { + label: "Closed", + value: "closed", + }, +]; diff --git a/components/everhour/common/utils.mjs b/components/everhour/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/everhour/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/everhour/everhour.app.mjs b/components/everhour/everhour.app.mjs index f17704bba377a..c87e3d01422f5 100644 --- a/components/everhour/everhour.app.mjs +++ b/components/everhour/everhour.app.mjs @@ -1,11 +1,176 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + export default { type: "app", app: "everhour", - propDefinitions: {}, + propDefinitions: { + projectId: { + type: "string", + label: "Project ID", + description: "The ID of the project", + async options({ page }) { + const projects = await this.listProjects({ + params: { + limit: LIMIT, + page: page + 1, + }, + }); + + return projects.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + sectionId: { + type: "string", + label: "Section ID", + description: "The section id of the task", + async options({ projectId }) { + const sections = await this.listSections({ + projectId, + }); + + return sections.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + tags: { + type: "string[]", + label: "Tag IDs", + description: "The tag ids of the task", + async options() { + const tags = await this.listTags(); + + return tags.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + labels: { + type: "string[]", + label: "Tags", + description: "An array of tags associated with the task", + async options({ projectId }) { + const sections = await this.listSections({ + projectId, + }); + + return sections.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + taskId: { + type: "string", + label: "Task ID", + description: "The ID of the task", + async options({ projectId }) { + const tasks = await this.getProjectTasks({ + projectId, + }); + return tasks.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.everhour.com"; + }, + _headers() { + return { + "X-Api-Key": `${this.$auth.api_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listProjects(opts = {}) { + return this._makeRequest({ + path: "/projects", + ...opts, + }); + }, + listSections({ + projectId, opts, + }) { + return this._makeRequest({ + path: `/projects/${projectId}/sections`, + ...opts, + }); + }, + listTags() { + return this._makeRequest({ + path: "/tags", + }); + }, + getProjectTasks({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/projects/${projectId}/tasks`, + ...opts, + }); + }, + createTask({ + projectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/projects/${projectId}/tasks`, + ...opts, + }); + }, + startTimer(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/timers", + ...opts, + }); + }, + stopTimer(opts = {}) { + return this._makeRequest({ + method: "DELETE", + path: "/timers/current", + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/hooks", + ...opts, + }); + }, + deleteWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/hooks/${webhookId}`, + }); }, }, }; diff --git a/components/everhour/package.json b/components/everhour/package.json new file mode 100644 index 0000000000000..9093fbb1e87e5 --- /dev/null +++ b/components/everhour/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/everhour", + "version": "0.1.0", + "description": "Pipedream Everhour Components", + "main": "everhour.app.mjs", + "keywords": [ + "pipedream", + "everhour" + ], + "homepage": "https://pipedream.com/apps/everhour", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/everhour/sources/common/base.mjs b/components/everhour/sources/common/base.mjs new file mode 100644 index 0000000000000..40eeb696c6bd3 --- /dev/null +++ b/components/everhour/sources/common/base.mjs @@ -0,0 +1,58 @@ +import everhour from "../../everhour.app.mjs"; + +export default { + props: { + everhour, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getExtraData() { + return {}; + }, + }, + hooks: { + async activate() { + const response = await this.everhour.createWebhook({ + data: { + targetUrl: this.http.endpoint, + events: this.getEventType(), + ...this.getExtraData(), + }, + }); + this._setHookId(response.id); + }, + async deactivate() { + const webhookId = this._getHookId(); + await this.everhour.deleteWebhook(webhookId); + }, + }, + async run({ + body, headers, + }) { + if (headers["x-hook-secret"]) { + return this.http.respond({ + status: 200, + headers: { + "X-Hook-Secret": headers["x-hook-secret"], + }, + }); + } + + const ts = Date.parse(new Date()); + this.$emit(body, { + id: `${body.resource}-${ts}`, + summary: this.getSummary(body), + ts: ts, + }); + }, +}; diff --git a/components/everhour/sources/new-client-instant/new-client-instant.mjs b/components/everhour/sources/new-client-instant/new-client-instant.mjs new file mode 100644 index 0000000000000..5ed199c80ee61 --- /dev/null +++ b/components/everhour/sources/new-client-instant/new-client-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "everhour-new-client-instant", + name: "New Client (Instant)", + description: "Emit new event when a client is added.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return [ + "api:client:created", + ]; + }, + getSummary(body) { + return `New Client: ${body.payload.data.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/everhour/sources/new-client-instant/test-event.mjs b/components/everhour/sources/new-client-instant/test-event.mjs new file mode 100644 index 0000000000000..31104ffe31bf7 --- /dev/null +++ b/components/everhour/sources/new-client-instant/test-event.mjs @@ -0,0 +1,25 @@ +export default { + "event": "api:client:created", + "payload": { + "id": "9381500", + "data": { + "projects": [], + "id": 9381500, + "name": "Client Name", + "createdAt": "2024-10-22 14:25:29", + "lineItemMask": "%MEMBER% :: %PROJECT% :: for %PERIOD%", + "paymentDueDays": 0, + "reference": "", + "businessDetails": "", + "email": [ + "client@email.com" + ], + "invoicePublicNotes": "", + "excludedLabels": [], + "status": "active", + "enableResourcePlanner": false, + "favorite": false + } + }, + "createdAt": "2024-10-22 14:25:29" +} \ No newline at end of file diff --git a/components/everhour/sources/new-task-instant/new-task-instant.mjs b/components/everhour/sources/new-task-instant/new-task-instant.mjs new file mode 100644 index 0000000000000..385cc425bfb68 --- /dev/null +++ b/components/everhour/sources/new-task-instant/new-task-instant.mjs @@ -0,0 +1,38 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "everhour-new-task-instant", + name: "New Task Created (Instant)", + description: "Emit new event when a task is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + projectId: { + propDefinition: [ + common.props.everhour, + "projectId", + ], + }, + }, + methods: { + ...common.methods, + getExtraData() { + return { + project: this.projectId, + }; + }, + getEventType() { + return [ + "api:task:created", + ]; + }, + getSummary(body) { + return `New Task Created: ${body.payload.data.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/everhour/sources/new-task-instant/test-event.mjs b/components/everhour/sources/new-task-instant/test-event.mjs new file mode 100644 index 0000000000000..c324485881340 --- /dev/null +++ b/components/everhour/sources/new-task-instant/test-event.mjs @@ -0,0 +1,24 @@ +export default { + "event": "api:task:created", + "payload": { + "id": "ev:188217209666811", + "data": { + "createdBy": 1362384, + "iteration": "Section Name", + "position": 5, + "projects": [ + "ev:188193235916605" + ], + "section": 1164091, + "comments": 0, + "completed": false, + "id": "ev:188217209666811", + "type": "task", + "name": "Task name", + "status": "open", + "labels": [], + "createdAt": "2024-10-18 14:01:36" + } + }, + "createdAt": "2024-10-18 14:01:36" +} \ No newline at end of file diff --git a/components/everhour/sources/task-time-updated-instant/task-time-updated-instant.mjs b/components/everhour/sources/task-time-updated-instant/task-time-updated-instant.mjs new file mode 100644 index 0000000000000..4bf9034b807b3 --- /dev/null +++ b/components/everhour/sources/task-time-updated-instant/task-time-updated-instant.mjs @@ -0,0 +1,38 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "everhour-task-time-updated-instant", + name: "New Task Time Updated (Instant)", + description: "Emit new event when a task's time spent is modified in Everhour.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + projectId: { + propDefinition: [ + common.props.everhour, + "projectId", + ], + }, + }, + methods: { + ...common.methods, + getExtraData() { + return { + project: this.projectId, + }; + }, + getEventType() { + return [ + "api:time:updated", + ]; + }, + getSummary(body) { + return `Task Time Updated: ${body.payload.data.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/everhour/sources/task-time-updated-instant/test-event.mjs b/components/everhour/sources/task-time-updated-instant/test-event.mjs new file mode 100644 index 0000000000000..5d4caa2116255 --- /dev/null +++ b/components/everhour/sources/task-time-updated-instant/test-event.mjs @@ -0,0 +1,55 @@ +export default { + "event": "api:time:updated", + "payload": { + "id": "ev:188193235916608", + "data": { + "user": 1362384, + "history": [ + { + "id": 370578249, + "time": 60, + "previousTime": 0, + "action": "TIMER", + "source": "internal", + "createdAt": "2024-10-18 13:58:23", + "createdBy": 1362384 + } + ], + "lockReasons": [], + "cost": 42, + "isLocked": false, + "manualTime": 0, + "id": 214588860, + "date": "2024-10-18", + "time": 60, + "timerTime": 60, + "pastDateTime": 0, + "task": { + "createdBy": 1362384, + "position": 4, + "projects": [ + "ev:188193235916605" + ], + "section": 1163621, + "comments": 0, + "completed": false, + "id": "ev:188193235916608", + "type": "task", + "name": "Project Management", + "status": "open", + "labels": [], + "createdAt": "2024-10-15 19:25:59", + "time": { + "total": 60, + "users": { + "1362384": 60 + }, + "timerTime": 60 + } + }, + "createdAt": "2024-10-18 13:58:23", + "costRate": 2500 + } + }, + "createdAt": "2024-10-18 13:59:20" +} \ No newline at end of file diff --git a/components/eversign/README.md b/components/eversign/README.md index 2cfae11ab286a..66df16401545e 100644 --- a/components/eversign/README.md +++ b/components/eversign/README.md @@ -1,15 +1,14 @@ # Overview -With the Eversign API, you can easily integrate electronic signatures into your -own web applications. Here are some examples of what you can build with the -Eversign API: - -- A web form that allows users to sign documents electronically -- An online contract management system that allows users to sign contracts - electronically -- A document management system that allows users to sign documents - electronically -- A human resources management system that allows employees to sign documents - electronically -- An inventory management system that allows users to sign documents - electronically +The Eversign API enables you to automate document workflows by electronically signing, sending, and managing documents. With Eversign, you can create legally binding documents, send them out for signature, and track their status. This eliminates manual tasks, reduces errors, and speeds up the document handling process. Pipedream's integration with Eversign lets you trigger workflows based on document events, or perform actions like creating and sending documents automatically, thereby integrating seamless e-signature processes within your digital ecosystem. + +# Example Workflows + +**Automate Contract Generation for New Clients** +When a new client is added to your CRM (like Salesforce or HubSpot), trigger a Pipedream workflow that automatically creates a personalized contract using the Eversign API and sends it to the client for signature. On completion, store the signed contract in a cloud storage service like Google Drive. + +**Streamline Employee Onboarding** +Connect your HR platform (e.g., BambooHR) to Pipedream. When a new employee is onboarded, trigger a workflow that generates necessary employment documents with Eversign. After the employee signs, update their record in the HR platform and store the document in a secure location. + +**Trigger Notifications on Document Status Changes** +Set up a workflow that listens for status updates on documents sent via Eversign (e.g., sent, viewed, signed). When a document's status changes, trigger actions such as sending a real-time notification through Slack or email, updating a status dashboard, or triggering follow-up tasks in project management tools like Asana or Trello. diff --git a/components/everwebinar/README.md b/components/everwebinar/README.md index 07155a3b37e81..73bb849bdc2f4 100644 --- a/components/everwebinar/README.md +++ b/components/everwebinar/README.md @@ -1,6 +1,11 @@ # Overview -- Register new users for webinars -- Get information about upcoming webinars -- Purchase webinars -- Get information about previous webinars +The EverWebinar API lets you integrate the power of automated webinar marketing into your own apps and workflows. With EverWebinar, you can schedule and run automated webinars as if they were live, engaging audiences with interactive elements such as polls and chat simulations. Using Pipedream, you can trigger actions based on webinar events, sync participant data with other platforms, and automate follow-up communications, providing a seamless bridge between your webinar activities and other business processes. + +# Example Use Cases + +- **Webinar Registrant Follow-Up**: Automate the process of following up with registrants post-webinar by creating a workflow that triggers an email sequence with additional resources or a survey link when a participant attends or misses a webinar. + +- **CRM Integration for Lead Tracking**: Keep your customer relationship management (CRM) system up to date by using a Pipedream workflow to add or update contacts with details from EverWebinar registrants, tracking their interactions and engagement level across your marketing funnel. + +- **Social Media Engagement Post-Webinar**: Increase engagement by automatically posting on your social media platforms thanking participants after a webinar ends. Set up a Pipedream workflow to tweet or share a message on LinkedIn with a call to action or a teaser for the next event. diff --git a/components/everwebinar/package.json b/components/everwebinar/package.json new file mode 100644 index 0000000000000..3888c8a27b82d --- /dev/null +++ b/components/everwebinar/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/everwebinar", + "version": "0.6.0", + "description": "Pipedream everwebinar Components", + "main": "everwebinar.app.mjs", + "keywords": [ + "pipedream", + "everwebinar" + ], + "homepage": "https://pipedream.com/apps/everwebinar", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/ewebinar/README.md b/components/ewebinar/README.md new file mode 100644 index 0000000000000..66f265c497409 --- /dev/null +++ b/components/ewebinar/README.md @@ -0,0 +1,11 @@ +# Overview + +The eWebinar API allows you to automate interactions with your eWebinar sessions, such as listing upcoming webinars, registering participants, and retrieving attendee data. By integrating the eWebinar API with Pipedream, you can craft workflows that connect your webinars with other apps and services, enabling you to streamline event management, follow-up processes, and audience engagement. + +# Example Use Cases + +- **Sync New Registrants to CRM**: Register a participant for an eWebinar event in Pipedream, then automatically add that participant's details to a CRM like Salesforce or HubSpot. This keeps your sales or customer service team updated in real time. + +- **Automate Follow-Up Emails**: After a webinar, use Pipedream to trigger a workflow that sends personalized follow-up emails through services like SendGrid or Mailchimp, based on attendee engagement and questions asked during the webinar. + +- **Integrate Webinar Analytics with Dashboard Tools**: Collect attendance and interaction data from eWebinar, then feed it into a data visualization tool like Google Sheets or Data Studio. This allows for real-time monitoring of webinar performance metrics. diff --git a/components/exact/README.md b/components/exact/README.md index 92f5bae21f357..1c625199674ed 100644 --- a/components/exact/README.md +++ b/components/exact/README.md @@ -1,6 +1,11 @@ # Overview -The Exact API allows developers to access the functionality of the Exact Online -accounting software from their own applications. With the API, developers can -create, view, and update account records, create and manage invoices and -quotes, and view and update inventory levels. +The Exact API provides a suite of endpoints for interacting with Exact's cloud-based financial software, which includes tools for accounting, CRM, and ERP functionalities. By leveraging this API on Pipedream, you can automate data flows across various business functions, sync financial records, manage customer relationships, and streamline operational processes. Pipedream's serverless execution model allows for crafting intricate workflows that respond in real-time to events, schedule operations, and connect with countless other apps to extend the functionality of Exact's ecosystem. + +# Example Use Cases + +- **Sync Invoices with Google Sheets**: Automatically export new or updated invoices from Exact to a Google Sheets spreadsheet. This workflow provides a simple way to keep finance teams updated on invoicing without manual exports, ensuring real-time visibility into accounts receivable. + +- **Customer Onboarding Automation**: When a new customer is added in Exact, trigger a workflow that collects additional data from forms or other systems, enriches the customer profile, and then updates the CRM records in Exact. This can streamline the onboarding process and ensure customer data integrity. + +- **Inventory Tracking and Alerts**: Monitor inventory levels in Exact and trigger alerts via email or messaging platforms like Slack when stock for key products falls below a certain threshold. This workflow can initiate purchase orders or notify management, enabling quick response to inventory needs. diff --git a/components/exhibitday/README.md b/components/exhibitday/README.md new file mode 100644 index 0000000000000..e8157d9955db4 --- /dev/null +++ b/components/exhibitday/README.md @@ -0,0 +1,11 @@ +# Overview + +The ExhibitDay API allows you to interact with ExhibitDay's event planning and trade show management features programmatically. Using this API within Pipedream, you can automate tasks like synchronizing event data, managing trade show inventory, and tracking your team's performance at events. By leveraging Pipedream's connectivity, you can trigger workflows based on new events, updates, or specific conditions that you define, streamlining your event management process. + +# Example Use Cases + +- **Synchronize ExhibitDay Events with Google Calendar**: Automate the process of adding new events from ExhibitDay to a Google Calendar. Whenever a new trade show or event is added in ExhibitDay, Pipedream triggers a workflow to create an equivalent event in a specified Google Calendar, ensuring your schedule is always up-to-date. + +- **Automate Lead Follow-Up Emails**: After an event concludes, use ExhibitDay's API to fetch attendee data and send personalized follow-up emails via a service like SendGrid. Pipedream can automate this flow, sending out thank you messages or further engagement materials, helping to solidify connections made during the event. + +- **Sync Trade Show Inventory with a Database**: Keep your inventory management system in sync with ExhibitDay. Whenever inventory items are checked out or returned in ExhibitDay, Pipedream triggers a workflow to update the corresponding records in a connected database such as Airtable, maintaining accurate inventory levels in real-time. diff --git a/components/exist/exist.app.mjs b/components/exist/exist.app.mjs new file mode 100644 index 0000000000000..87f4e0d7a94a1 --- /dev/null +++ b/components/exist/exist.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "exist", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/exist/package.json b/components/exist/package.json new file mode 100644 index 0000000000000..499316a3deb81 --- /dev/null +++ b/components/exist/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/exist", + "version": "0.0.1", + "description": "Pipedream Exist Components", + "main": "exist.app.mjs", + "keywords": [ + "pipedream", + "exist" + ], + "homepage": "https://pipedream.com/apps/exist", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/expedy/README.md b/components/expedy/README.md new file mode 100644 index 0000000000000..9f425c197c2ba --- /dev/null +++ b/components/expedy/README.md @@ -0,0 +1,11 @@ +# Overview + +The Expedy API provides a platform for automating the management of expenses, allowing you to upload receipts, categorize expenses, and generate reports. By leveraging the Expedy API within Pipedream, you can create serverless workflows that handle various aspects of expense tracking and report generation, without the need to manage infrastructure. This can lead to a significant reduction in manual work, increasing efficiency for individuals or finance teams. + +# Example Use Cases + +- **Automate Expense Reporting**: Create a Pipedream workflow that listens for new emails with receipts, extracts the attachment using an email service like Gmail, and then uploads the receipt to Expedy for processing and categorization. + +- **Daily Expense Summary**: Schedule a daily Pipedream workflow that retrieves your latest expenses from Expedy and compiles them into a summary report, which is then sent to you via Slack to keep you updated on your spending. + +- **Receipts to Cloud Storage Sync**: Set up a workflow that triggers whenever you add a new expense in Expedy, extracts the receipt image, and then uploads it to a cloud storage service like Dropbox, creating a backup for your records. diff --git a/components/expensify/README.md b/components/expensify/README.md index 50dc775d36639..2a904a8bf5431 100644 --- a/components/expensify/README.md +++ b/components/expensify/README.md @@ -1,10 +1,11 @@ # Overview -With Expensify, you can: - -- Generate and submit expense reports -- Approve or reject expense reports -- Add photos of receipts -- Track expenses by project or category -- Analyze spending trends -- And more! +The Expensify API enables the automation of expense reporting and management tasks. By harnessing this API within Pipedream, you can craft workflows that streamline the expense submission process, synchronize financial data across platforms, and trigger actions based on expense report statuses. With Pipedream’s serverless platform, these automations can run in the background, allowing for real-time data processing and interaction between Expensify and a myriad of other apps and services. + +# Example Use Cases + +- **Expense Approval Notifications**: When a new expense report is submitted in Expensify, trigger a workflow in Pipedream that sends a Slack message to the approver with report details, enabling quicker review and approval. + +- **Synchronized Expense Recording**: Every time an expense is approved in Expensify, use Pipedream to automatically record the expense in a Google Sheet or push it to an accounting software like QuickBooks, ensuring financial records are always up-to-date. + +- **Receipt Processing Automation**: Upon receiving a new receipt image in Expensify, trigger a workflow that uses image recognition to extract data from the receipt and populate an expense report in Expensify, reducing manual data entry. diff --git a/components/expofp/README.md b/components/expofp/README.md index f426e9a4b04ac..4323807344d0e 100644 --- a/components/expofp/README.md +++ b/components/expofp/README.md @@ -1,15 +1,11 @@ # Overview -Using the ExpoFP API, you can easily develop applications that can take -advantage of the huge range of features available on the Expo platform. - -Here are some examples of what you can build using the ExpoFP API: - -- A mobile app that can be used to book appointments and track events at your - business -- A social network for connecting with friends and family -- A gaming platform that lets you play your favorite games with friends -- A news feed that keeps you up-to-date with the latest news and events -- A messaging app that lets you chat with friends and family -- A GPS-based tracking system that lets you see where your friends and family - are +The ExpoFP API enables developers to integrate floor plan management and interaction within their applications. With this API, you can access detailed information about exhibition spaces, including booth details, exhibitor information, and floor plan layouts. When you combine ExpoFP with Pipedream's serverless integration and compute platform, you can automate event management tasks, synchronize floor plan data with other systems, and enhance visitor engagement by connecting with CRM or marketing automation tools. + +# Example Use Cases + +- **Automated Event Registration Sync**: Create a workflow on Pipedream that triggers when a new exhibitor registers for an event via your CRM. The workflow would automatically assign a booth on the ExpoFP floor plan and update the CRM with the booth number, ensuring seamless coordination between sales and event layouts. + +- **Real-Time Notifications for Floor Plan Changes**: Set up a Pipedream workflow that listens for updates to floor plans on ExpoFP. Whenever a change is detected, the workflow could send real-time notifications to event managers or exhibitors via Slack, email, or SMS, keeping all stakeholders informed about the latest layout. + +- **Analytics Integration for Visitor Tracking**: Design a Pipedream workflow that integrates ExpoFP with analytics platforms like Google Analytics. Track visitor interactions with the floor plan—such as booth clicks or views—and feed this data into your analytics system to gain insights into visitor behavior and preferences, enabling data-driven decision making for future events. diff --git a/components/expofp/actions/add-exhibitor-booth/add-exhibitor-booth.mjs b/components/expofp/actions/add-exhibitor-booth/add-exhibitor-booth.mjs new file mode 100644 index 0000000000000..e6c55504b8ee6 --- /dev/null +++ b/components/expofp/actions/add-exhibitor-booth/add-exhibitor-booth.mjs @@ -0,0 +1,62 @@ +import expofp from "../../expofp.app.mjs"; + +export default { + name: "Add Exhibitor Booth", + version: "0.0.1", + key: "expofp-add-exhibitor-booth", + description: + "Adds an exhibitor booth. [See the documentation](https://expofp.docs.apiary.io/#reference/0/add-exhibitor-booth/add-exhibitor-booth)", + type: "action", + props: { + expofp, + eventId: { + propDefinition: [ + expofp, + "eventId", + ], + }, + exhibitorId: { + propDefinition: [ + expofp, + "exhibitorId", + ({ eventId }) => ({ + eventId, + }), + ], + }, + boothName: { + propDefinition: [ + expofp, + "boothName", + ({ eventId }) => ({ + eventId, + }), + ], + }, + }, + methods: { + async addExhibitorBooth(args) { + return this.expofp._makeRequest({ + path: "/add-exhibitor-booth", + method: "post", + ...args, + }); + }, + }, + async run({ $ }) { + const { // eslint-disable-next-line no-unused-vars + expofp, ...data + } = this; + const response = await this.addExhibitorBooth({ + $, + data, + }); + + $.export( + "$summary", + `Successfully added booth "${this.boothName}"`, + ); + + return response; + }, +}; diff --git a/components/expofp/actions/add-exhibitor/add-exhibitor.mjs b/components/expofp/actions/add-exhibitor/add-exhibitor.mjs index 549f6fb0a5c4f..acde918dda1df 100644 --- a/components/expofp/actions/add-exhibitor/add-exhibitor.mjs +++ b/components/expofp/actions/add-exhibitor/add-exhibitor.mjs @@ -3,10 +3,10 @@ import common from "../common/add-or-update-exhibitor.mjs"; export default { ...common, name: "Add Exhibitor", - version: "0.0.1", + version: "0.0.2", key: "expofp-add-exhibitor", description: - "Adds an exhibitor. [See docs here](https://expofp.docs.apiary.io/#reference/0/add-exhibitor/add-exhibitor)", + "Adds an exhibitor. [See the documentation](https://expofp.docs.apiary.io/#reference/0/add-exhibitor/add-exhibitor)", type: "action", methods: { ...common.methods, diff --git a/components/expofp/actions/get-booth/get-booth.mjs b/components/expofp/actions/get-booth/get-booth.mjs index edde22a4b2003..a8ea1ec4f6224 100644 --- a/components/expofp/actions/get-booth/get-booth.mjs +++ b/components/expofp/actions/get-booth/get-booth.mjs @@ -2,9 +2,9 @@ import expofp from "../../expofp.app.mjs"; export default { name: "Get Booth", - version: "0.0.2", + version: "0.0.3", key: "expofp-get-booth", - description: "Get details of a booth. [See docs here](https://expofp.docs.apiary.io/#reference/0/get-booth-details/get-booth-details)", + description: "Get details of a booth. [See the documentation](https://expofp.docs.apiary.io/#reference/0/get-booth-details/get-booth-details)", type: "action", methods: { async getBooth(args) { @@ -23,10 +23,14 @@ export default { "eventId", ], }, - boothId: { - label: "Booth ID/Name", - type: "string", - description: "The booth ID or name. E.g. `101` or `A21`", + boothName: { + propDefinition: [ + expofp, + "boothName", + ({ eventId }) => ({ + eventId, + }), + ], }, }, async run({ $ }) { diff --git a/components/expofp/actions/get-exhibitor/get-exhibitor.mjs b/components/expofp/actions/get-exhibitor/get-exhibitor.mjs index e40bf8997e7c0..8d216e11e9170 100644 --- a/components/expofp/actions/get-exhibitor/get-exhibitor.mjs +++ b/components/expofp/actions/get-exhibitor/get-exhibitor.mjs @@ -2,9 +2,9 @@ import expofp from "../../expofp.app.mjs"; export default { name: "Get Exhibitor", - version: "0.0.2", + version: "0.0.3", key: "expofp-get-exhibitor", - description: "Get details of an exhibitor. [See docs here](https://expofp.docs.apiary.io/#reference/0/get-exhibitor-details/get-exhibitor-details)", + description: "Get details of an exhibitor. [See the documentation](https://expofp.docs.apiary.io/#reference/0/get-exhibitor-details/get-exhibitor-details)", type: "action", methods: { async getExhibitor(args) { diff --git a/components/expofp/actions/list-all-events/list-all-events.mjs b/components/expofp/actions/list-all-events/list-all-events.mjs index f7e2caa5eb101..6a2ae5a90da10 100644 --- a/components/expofp/actions/list-all-events/list-all-events.mjs +++ b/components/expofp/actions/list-all-events/list-all-events.mjs @@ -2,10 +2,10 @@ import expofp from "../../expofp.app.mjs"; export default { name: "List All Events", - version: "0.0.1", + version: "0.0.2", key: "expofp-list-all-events", description: - "List all events. [See docs here](https://expofp.docs.apiary.io/#reference/0/list-all-events/list-all-events)", + "List all events. [See the documentation](https://expofp.docs.apiary.io/#reference/0/list-all-events/list-all-events)", type: "action", methods: { async listAllEvents(args) { diff --git a/components/expofp/actions/update-booth/update-booth.mjs b/components/expofp/actions/update-booth/update-booth.mjs index 17e99ff2e67db..61ab6933021fd 100644 --- a/components/expofp/actions/update-booth/update-booth.mjs +++ b/components/expofp/actions/update-booth/update-booth.mjs @@ -2,9 +2,9 @@ import expofp from "../../expofp.app.mjs"; export default { name: "Update Booth", - version: "0.0.2", + version: "0.0.3", key: "expofp-update-booth", - description: "Updates a booth. [See docs here](https://expofp.docs.apiary.io/#reference/0/update-booth/update-booth)", + description: "Updates a booth. [See the documentation](https://expofp.docs.apiary.io/#reference/0/update-booth/update-booth)", type: "action", methods: { async updateBooth(args) { @@ -23,10 +23,14 @@ export default { "eventId", ], }, - boothId: { - label: "Booth ID/Name", - type: "string", - description: "The booth ID or name. E.g. `101` or `A21`", + boothName: { + propDefinition: [ + expofp, + "boothName", + ({ eventId }) => ({ + eventId, + }), + ], }, adminNotes: { label: "Admin Notes", diff --git a/components/expofp/actions/update-exhibitor/update-exhibitor.mjs b/components/expofp/actions/update-exhibitor/update-exhibitor.mjs index f6637740d93d6..2d52cbe73fc11 100644 --- a/components/expofp/actions/update-exhibitor/update-exhibitor.mjs +++ b/components/expofp/actions/update-exhibitor/update-exhibitor.mjs @@ -4,9 +4,9 @@ import common from "../common/add-or-update-exhibitor.mjs"; export default { ...common, name: "Update Exhibitor", - version: "0.0.2", + version: "0.0.3", key: "expofp-update-exhibitor", - description: "Updates an exhibitor. [See docs here](https://expofp.docs.apiary.io/#reference/0/update-exhibitor/update-exhibitor)", + description: "Updates an exhibitor. [See the documentation](https://expofp.docs.apiary.io/#reference/0/update-exhibitor/update-exhibitor)", type: "action", methods: { async updateExhibitor(args) { diff --git a/components/expofp/expofp.app.mjs b/components/expofp/expofp.app.mjs index a2a587129cd45..83e51dd446a2f 100644 --- a/components/expofp/expofp.app.mjs +++ b/components/expofp/expofp.app.mjs @@ -28,12 +28,26 @@ export default { }, }); - return exhibitors.map((event) => ({ - label: event.name, - value: event.id, + return exhibitors.map((item) => ({ + label: item.name, + value: item.id, })); }, }, + boothName: { + label: "Booth Name", + type: "string", + description: "Select a booth or provide a booth name", + async options({ eventId }) { + const booths = await this.getBooths({ + data: { + expoId: eventId, + }, + }); + + return booths.map(({ name }) => name); + }, + }, }, methods: { _apiToken() { @@ -67,5 +81,12 @@ export default { ...args, }); }, + async getBooths(args) { + return this._makeRequest({ + path: "/list-booths", + method: "post", + ...args, + }); + }, }, }; diff --git a/components/expofp/package.json b/components/expofp/package.json index baa2feda2336e..5e58394e047b5 100644 --- a/components/expofp/package.json +++ b/components/expofp/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/expofp", - "version": "0.1.1", + "version": "0.2.0", "description": "Pipedream ExpoFP Components", "main": "expofp.app.mjs", "keywords": [ diff --git a/components/extensiv_integration_manager/extensiv_integration_manager.app.mjs b/components/extensiv_integration_manager/extensiv_integration_manager.app.mjs new file mode 100644 index 0000000000000..5ac85e71e90de --- /dev/null +++ b/components/extensiv_integration_manager/extensiv_integration_manager.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "extensiv_integration_manager", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/extensiv_integration_manager/package.json b/components/extensiv_integration_manager/package.json new file mode 100644 index 0000000000000..bb64d7733a3a3 --- /dev/null +++ b/components/extensiv_integration_manager/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/extensiv_integration_manager", + "version": "0.0.1", + "description": "Pipedream Extensiv Integration Manager Components", + "main": "extensiv_integration_manager.app.mjs", + "keywords": [ + "pipedream", + "extensiv_integration_manager" + ], + "homepage": "https://pipedream.com/apps/extensiv_integration_manager", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/eyepop_ai/eyepop_ai.app.mjs b/components/eyepop_ai/eyepop_ai.app.mjs new file mode 100644 index 0000000000000..ba30db6e8bda3 --- /dev/null +++ b/components/eyepop_ai/eyepop_ai.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "eyepop_ai", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/eyepop_ai/package.json b/components/eyepop_ai/package.json new file mode 100644 index 0000000000000..e1abdbad03aa1 --- /dev/null +++ b/components/eyepop_ai/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/eyepop_ai", + "version": "0.0.1", + "description": "Pipedream EyePop.ai Components", + "main": "eyepop_ai.app.mjs", + "keywords": [ + "pipedream", + "eyepop_ai" + ], + "homepage": "https://pipedream.com/apps/eyepop_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/ez_texting/README.md b/components/ez_texting/README.md new file mode 100644 index 0000000000000..c99912968f09e --- /dev/null +++ b/components/ez_texting/README.md @@ -0,0 +1,11 @@ +# Overview + +The iHomefinder API provides real estate data services, including property listings, market info, and lead management. With Pipedream, you can automate how you interact with this data. For instance, you can sync new listings to a CRM, update your website with the latest properties, or trigger marketing emails when a new lead is captured. + +# Example Use Cases + +- **Automate CRM Updates with New Listings**: Listen for new property listings in the iHomefinder API and automatically update your CRM—like Salesforce or HubSpot—with fresh listings to keep sales teams informed and responsive. + +- **Dynamic Website Content Updates**: Use the iHomefinder API to fetch new property data and automatically push updates to your website's listings page. This can be done through Pipedream's HTTP actions or by integrating with a CMS like WordPress. + +- **Lead Management with Email Automation**: When a new lead registers through iHomefinder, trigger workflows in Pipedream to add the lead to an email marketing tool like Mailchimp, and send them a personalized welcome email along with curated property recommendations. diff --git a/components/ez_texting_/README.md b/components/ez_texting_/README.md new file mode 100644 index 0000000000000..ee044c5bd8113 --- /dev/null +++ b/components/ez_texting_/README.md @@ -0,0 +1,11 @@ +# Overview + +The EZ Texting API allows you to automate SMS messaging, making it a breeze to send bulk messages, set up marketing campaigns, or deliver notifications. On Pipedream, you can harness this power to create custom workflows that trigger based on various events or schedules. Think of syncing your CRM contacts, automating alerts, or even integrating with other apps to expand your communication strategy. + +# Example Use Cases + +- **Automated Customer Follow-Up**: After a customer submits a form on your website, you can build a workflow that captures that event, extracts the customer's phone number, and sends a follow-up text via EZ Texting. This ensures immediate engagement and can improve customer satisfaction. + +- **Scheduled Appointment Reminders**: If you have a calendar or booking system, you can connect it to EZ Texting through Pipedream to automatically send SMS reminders for appointments. Schedule a workflow that checks upcoming appointments daily and texts reminders to reduce no-shows. + +- **Alerts From Monitoring Systems**: Combine EZ Texting with a monitoring app like Uptime Robot on Pipedream to promptly notify the relevant team via SMS if a website or service goes down, ensuring quick response times and minimizing downtime. diff --git a/components/ez_texting_/package.json b/components/ez_texting_/package.json index cbd1660db72d4..229f6f47c644f 100644 --- a/components/ez_texting_/package.json +++ b/components/ez_texting_/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/ezeep_blue/README.md b/components/ezeep_blue/README.md new file mode 100644 index 0000000000000..72a021f5b5091 --- /dev/null +++ b/components/ezeep_blue/README.md @@ -0,0 +1,11 @@ +# Overview + +The ezeep Blue API offers a cloud-based platform for managing and streamlining your printing needs, enabling you to print documents from any device, anywhere. With Pipedream's serverless integration capabilities, you can harness the ezeep Blue API to automate printing workflows, sync your print jobs with other apps, and track printing across your organization. It's all about making printing tasks simpler, more flexible, and easily integrated with your existing software stack. + +# Example Use Cases + +- **Automated Document Printing**: Trigger a print job on ezeep Blue whenever a new document is added to a specific Dropbox folder. This workflow ensures that hard copies are automatically created for new digital content without manual intervention. + +- **Print Job Notifications**: Set up a workflow that sends a Slack message to a designated channel whenever a print job is completed. This can keep team members in the loop about printing status, especially useful in collaborative environments where document printing is frequent. + +- **Scheduled Reports Printing**: Combine ezeep Blue with Google Sheets to print weekly performance reports. Use Pipedream's cron job feature to schedule the workflow, ensuring that physical copies of important metrics are always available for meetings. diff --git a/components/f15five/README.md b/components/f15five/README.md index 6947f46c10486..c82ecf84fe95b 100644 --- a/components/f15five/README.md +++ b/components/f15five/README.md @@ -1,22 +1,11 @@ # Overview -The 15Five API enables you to build custom integrations and applications on top -of the 15Five platform. With the API, you can access data stored in 15Five, -including: +The 15Five API grants access to a performance management platform designed to help employees grow and achieve their goals. With Pipedream, you can leverage this API to automate feedback collection, synchronize performance data across various systems, and trigger actions based on employee responses. By interconnecting 15Five with other apps, you can streamline HR processes, enhance employee engagement strategies, and maintain a pulse on organizational health. -- Users -- Companies -- Goals -- Check-ins -- Feedback -- Recognition +# Example Use Cases -You can use the API to build applications that help 15Five users manage their -work and achieve their goals. For example, you could build a custom integration -that links 15Five with another tool that your team uses, such as a project -management tool or a CRM. Or, you could build a custom application that helps -users track their progress on their goals, or provides them with recognition -for a job well done. +- **Automated Feedback Aggregation**: Collect weekly check-in data from 15Five and compile it into a centralized reporting tool like Google Sheets. This could help managers track progress over time and prepare for one-on-one meetings with direct reports. - Whatever you build, the 15Five API gives you the power to customize the 15Five - experience for your users in a way that best meets their needs. +- **Employee Recognition System**: Trigger a recognition workflow whenever an employee receives a high score on their 15Five review. This could be tied to a rewards platform like Bonusly, automatically granting points or accolades to the employee. + +- **Performance Alert System**: Set up a monitoring system that notifies team leads or HR when an employee's 15Five responses indicate potential burnout or disengagement. This would allow quick intervention and support, linked to communication platforms like Slack or email. diff --git a/components/facebook_conversions/README.md b/components/facebook_conversions/README.md new file mode 100644 index 0000000000000..5dc906ed5da58 --- /dev/null +++ b/components/facebook_conversions/README.md @@ -0,0 +1,11 @@ +# Overview + +The Facebook Conversions API allows you to send web and offline events from your server directly to Facebook's systems, helping to enhance ad targeting, decrease reliance on cookies, and measure customer actions in ways that respect their privacy. With Pipedream, you can integrate this API into serverless workflows to automate data tracking and improve ad performance. You can, for example, track when a user completes a purchase, signs up for a newsletter, or takes another valuable action and relay this data to Facebook in real time. + +# Example Use Cases + +- **Sync E-commerce Transactions**: Automate the sending of server-side e-commerce transaction events (like purchases or cart additions) to Facebook Conversions API every time a new order is processed in your online store. Connect Pipedream to an e-commerce platform like Shopify to trigger this workflow. + +- **Lead Tracking for CRM**: Whenever a new lead is captured in your CRM system, such as Salesforce, push that event to Facebook Conversions API. This allows you to optimize your Facebook ad campaigns based on lead generation data and enhance retargeting strategies. + +- **Custom Event Triggering from Webhooks**: Configure a webhook to listen for specific user actions on your website or app. When the designated event occurs, such as a form submission or a milestone achievement, trigger a Pipedream workflow that sends this event information to Facebook Conversions API for improved ad targeting. diff --git a/components/facebook_conversions/package.json b/components/facebook_conversions/package.json index 0af71e3c9edac..95e383af79c1b 100644 --- a/components/facebook_conversions/package.json +++ b/components/facebook_conversions/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/facebook_groups/README.md b/components/facebook_groups/README.md index 8c138cbf98f55..28a0c5fe63bc1 100644 --- a/components/facebook_groups/README.md +++ b/components/facebook_groups/README.md @@ -1,7 +1,19 @@ +# Overview + +The Facebook Groups API provides a suite of features allowing you to manage group activities, automate content moderation, and engage community members. With this API on Pipedream, you can streamline group management, analyze engagement data, schedule posts, and integrate with other services for extensive automation potential. For instance, you can trigger workflows based on new posts, comments, or member activities, auto-respond to queries, or synchronize group data with CRM platforms, broadening your group's reach and efficacy. + # Getting Started You need to install the Pipedream app in your Facebook Group in order to make any API calls, or use any prebuilt triggers or actions. To install Pipedream, navigate to the group where you're an administrator, and in the left sidebar, under **Settings** > **Group Settings**, you'll find **Manage Advanced Settings**. Click the Edit button under **Apps**. ![Apps](https://res.cloudinary.com/dpenc2lit/image/upload/v1687367319/mycology_fb_bpfmh3.png) Search for "Pipedream", and add the app to your Facebook Group. Once the Pipedream app is successfully added to your group, you can start building automated workflows on Pipedream using Facebook Groups. -![Add App to Group](https://res.cloudinary.com/dpenc2lit/image/upload/v1687367707/Screenshot_2023-06-21_at_10.12.35_AM_ua3z3p.png) \ No newline at end of file +![Add App to Group](https://res.cloudinary.com/dpenc2lit/image/upload/v1687367707/Screenshot_2023-06-21_at_10.12.35_AM_ua3z3p.png) + +# Example Use Cases + +- **Automated Content Moderation**: Set up a workflow that listens for new posts and comments within your Facebook Group, then leverages sentiment analysis or keyword filtering to identify and remove inappropriate content automatically, ensuring a safe community space. + +- **New Member Onboarding**: Create a workflow that triggers when new members join your Facebook Group. It sends personalized welcome messages and adds member information to your CRM, such as Salesforce or HubSpot, to keep track of the growing community and facilitate engagement tracking. + +- **Event Promotion and Follow-Up**: Connect your Facebook Group with a calendar service like Google Calendar to promote upcoming events. When a new event is created in the calendar, a post is automatically generated in the group. After the event, trigger a workflow that collects attendee feedback via a Google Form and posts a thank you message in the group. \ No newline at end of file diff --git a/components/facebook_lead_ads/README.md b/components/facebook_lead_ads/README.md new file mode 100644 index 0000000000000..40453df4bccc9 --- /dev/null +++ b/components/facebook_lead_ads/README.md @@ -0,0 +1,11 @@ +# Overview + +The Facebook Lead Ads API allows for automated retrieval and integration of leads generated from Facebook Lead Ads into various business processes and systems. On Pipedream, you can harness this API to create serverless workflows that trigger actions based on new lead data, sync leads to CRMs, send personalized follow-up emails, and more, without managing infrastructure. + +# Example Use Cases + +- **Sync Leads to a CRM**: Automatically push new leads from Facebook Lead Ads to your CRM, like Salesforce or HubSpot, on Pipedream. This workflow can enrich lead data, assign them to sales reps, or trigger internal notifications. + +- **Send Custom Email Responses**: When a new lead is captured on Facebook, use Pipedream to send a tailored email response through an email service such as SendGrid. This helps in engaging leads promptly and increases the chances of conversion. + +- **Update Google Sheets with Lead Data**: Collect and organize new Facebook leads in a Google Sheet. This Pipedream workflow allows for easy sharing and analysis within a team, or for triggering further automations based on sheet updates. diff --git a/components/facebook_lead_ads/package.json b/components/facebook_lead_ads/package.json index 6b63840b32e22..4a2517a4f9019 100644 --- a/components/facebook_lead_ads/package.json +++ b/components/facebook_lead_ads/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/facebook_marketing/README.md b/components/facebook_marketing/README.md new file mode 100644 index 0000000000000..00b0998353938 --- /dev/null +++ b/components/facebook_marketing/README.md @@ -0,0 +1,11 @@ +# Overview + +The Facebook Custom Audiences API allows you to create and manage your Custom Audiences on Facebook for ad targeting. With Pipedream, you can automate workflows involving Custom Audiences, such as synchronizing email lists, updating audience membership based on customer interactions, or triggering ad campaigns in response to specific events. Pipedream's serverless platform simplifies integrating this API with hundreds of other apps, enabling complex automation without the need for a dedicated backend. + +# Example Use Cases + +- **Sync New Email Subscribers to Custom Audience**: Automatically add new subscribers from your email marketing platform (e.g., Mailchimp) to a Facebook Custom Audience, ensuring your ads reach those who've shown recent interest in your content. + +- **Update Custom Audience Based on Purchase Behavior**: Trigger a workflow with a webhook when a customer makes a purchase. Use the Custom Audiences API to add them to a "Recent Purchasers" audience for upsell campaigns or exclude them from initial acquisition ads to optimize ad spend. + +- **Automate Audience Management with CRM Events**: When a contact's status updates in your CRM (like Salesforce), use Pipedream to adjust their membership in Custom Audiences. For instance, moving a lead to a "Warm Leads" audience if they've interacted with a sales rep, enhancing the relevance of your ad targeting. diff --git a/components/facebook_pages/README.md b/components/facebook_pages/README.md new file mode 100644 index 0000000000000..6625be380050b --- /dev/null +++ b/components/facebook_pages/README.md @@ -0,0 +1,11 @@ +# Overview + +The Facebook Pages API on Pipedream allows you to automate interactions with your Facebook Page. Using Pipedream's serverless platform, you can create workflows that respond to events, publish new content, manage posts, and analyze Page performance data. This API enables seamless integration with other apps and services, allowing for complex automations and data-driven decision-making. + +# Example Use Cases + +- **Automate Content Posting**: Create a workflow that schedules and posts content to your Facebook Page. When you add new content to a Google Sheets spreadsheet, Pipedream triggers a workflow that automatically publishes the content to your Page at the scheduled time. + +- **Audience Engagement Tracker**: Craft a workflow that monitors comments and messages on your Page. Leveraging Pipedream's built-in code steps, you can analyze sentiment, flag important items, and even respond automatically. Connect with Slack to send notifications when a user interaction requires personal attention. + +- **Performance Reporting**: Set up a recurring workflow that gathers analytics from your Facebook Page and compiles them into a report. Integrate with email services like SendGrid to send weekly performance insights directly to your inbox or to your marketing team. diff --git a/components/faceup/faceup.app.mjs b/components/faceup/faceup.app.mjs new file mode 100644 index 0000000000000..57da4ef5add21 --- /dev/null +++ b/components/faceup/faceup.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "faceup", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/faceup/package.json b/components/faceup/package.json new file mode 100644 index 0000000000000..02671fecd918a --- /dev/null +++ b/components/faceup/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/faceup", + "version": "0.1.0", + "description": "Pipedream FaceUp Components", + "main": "faceup.app.mjs", + "keywords": [ + "pipedream", + "faceup" + ], + "homepage": "https://pipedream.com/apps/faceup", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/faceup/sources/new-internal-comment-instant/new-internal-comment-instant.mjs b/components/faceup/sources/new-internal-comment-instant/new-internal-comment-instant.mjs new file mode 100644 index 0000000000000..e3d47c2fa45b5 --- /dev/null +++ b/components/faceup/sources/new-internal-comment-instant/new-internal-comment-instant.mjs @@ -0,0 +1,33 @@ +import faceup from "../../faceup.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "faceup-new-internal-comment-instant", + name: "New Internal Comment (Instant)", + description: "Emit new event when a new internal comment is created. Must create webhook within the Faceup UI and enter the URL of this source to receive events. [See the documentation](https://support.faceup.com/en/article/webhooks)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + faceup, + http: "$.interface.http", + }, + methods: { + generateMeta(body) { + const { data: { message } } = body; + return { + id: message.id, + summary: `New Internal Comment ID: ${message.id}`, + ts: Date.parse(message.created_at), + }; + }, + }, + async run(event) { + const { body } = event; + if (body?.event === "InternalCommentCreated") { + const meta = this.generateMeta(body); + this.$emit(body, meta); + } + }, + sampleEmit, +}; diff --git a/components/faceup/sources/new-internal-comment-instant/test-event.mjs b/components/faceup/sources/new-internal-comment-instant/test-event.mjs new file mode 100644 index 0000000000000..755a2a8f9dddb --- /dev/null +++ b/components/faceup/sources/new-internal-comment-instant/test-event.mjs @@ -0,0 +1,16 @@ +export default { + "event": "InternalCommentCreated", + "data": { + "author": { + "id": "TWFuYWdlcjoyMjkzNg==", + "name": "Test User" + }, + "message": { + "id": "Q29tcGFueVJlcG9ydEludGVybmFsQ29tbWVudDoyMTIyMQ==", + "created_at": "2024-07-03T16:41:14.397Z" + }, + "report": { + "id": "Q29tcGFueVJlcG9ydDozODkyMA==" + } + } +} \ No newline at end of file diff --git a/components/faceup/sources/new-message-instant/new-message-instant.mjs b/components/faceup/sources/new-message-instant/new-message-instant.mjs new file mode 100644 index 0000000000000..6b0f9274bd604 --- /dev/null +++ b/components/faceup/sources/new-message-instant/new-message-instant.mjs @@ -0,0 +1,33 @@ +import faceup from "../../faceup.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "faceup-new-message-instant", + name: "New Message (Instant)", + description: "Emit new event when a new message from a sender is created. Must create webhook within the Faceup UI and enter the URL of this source to receive events. [See the documentation](https://support.faceup.com/en/article/webhooks)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + faceup, + http: "$.interface.http", + }, + methods: { + generateMeta(body) { + const { data: { message } } = body; + return { + id: message.id, + summary: `New Message ID: ${message.id}`, + ts: Date.parse(message.created_at), + }; + }, + }, + async run(event) { + const { body } = event; + if (body?.event === "MessageCreated") { + const meta = this.generateMeta(body); + this.$emit(body, meta); + } + }, + sampleEmit, +}; diff --git a/components/faceup/sources/new-message-instant/test-event.mjs b/components/faceup/sources/new-message-instant/test-event.mjs new file mode 100644 index 0000000000000..f32577fd39a36 --- /dev/null +++ b/components/faceup/sources/new-message-instant/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "event": "MessageCreated", + "data": { + "message": { + "id": "Q29tcGFueVJlcG9ydEZvbGxvd1VwQ29tbWVudDozMTEzOA==", + "created_at": "2024-07-03T16:40:01.276Z" + }, + "author": { + "type": "member" + }, + "report": { + "id": "Q29tcGFueVJlcG9ydDozODkyMA==" + } + } +} \ No newline at end of file diff --git a/components/faceup/sources/new-report-instant/new-report-instant.mjs b/components/faceup/sources/new-report-instant/new-report-instant.mjs new file mode 100644 index 0000000000000..3781002130330 --- /dev/null +++ b/components/faceup/sources/new-report-instant/new-report-instant.mjs @@ -0,0 +1,33 @@ +import faceup from "../../faceup.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "faceup-new-report-instant", + name: "New Report (Instant)", + description: "Emit new event when a new report is created. Must create webhook within the Faceup UI and enter the URL of this source to receive events. [See the documentation](https://support.faceup.com/en/article/webhooks)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + faceup, + http: "$.interface.http", + }, + methods: { + generateMeta(body) { + const { data: { report } } = body; + return { + id: report.id, + summary: `New Report ID: ${report.id}`, + ts: Date.parse(report.created_at), + }; + }, + }, + async run(event) { + const { body } = event; + if (body?.event === "ReportCreated") { + const meta = this.generateMeta(body); + this.$emit(body, meta); + } + }, + sampleEmit, +}; diff --git a/components/faceup/sources/new-report-instant/test-event.mjs b/components/faceup/sources/new-report-instant/test-event.mjs new file mode 100644 index 0000000000000..06f57c69b8eab --- /dev/null +++ b/components/faceup/sources/new-report-instant/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "event": "ReportCreated", + "data": { + "report": { + "id": "Q29tcGFueVJlcG9ydDozOTE4MQ==", + "tag": "#bc11e", + "origin": "Member", + "justification": "Unknown", + "priority": null, + "status": "Open", + "source": "ReportingSystem", + "created_at": "2024-07-03T04:00:00.000Z" + } + } +} \ No newline at end of file diff --git a/components/facturadirecta/README.md b/components/facturadirecta/README.md index e545d87e7ed52..89abca90c2d0a 100644 --- a/components/facturadirecta/README.md +++ b/components/facturadirecta/README.md @@ -1,6 +1,11 @@ # Overview -FacturaDirecta is a powerful API that lets you create and manage invoices, -quotes, and other documents. You can use it to automate your billing process, -or to create custom documents for your clients. FacturaDirecta is easy to use -and has a wide range of features, making it a great choice for any business. +The FacturaDirecta API enables you to automate and integrate your billing and accounting systems, thereby streamlining financial operations within your business. Through Pipedream, you can trigger workflows on new events in FacturaDirecta, manipulate and analyze invoice data, synchronize contacts, and manage products. This can save time, reduce errors, and provide real-time financial insights. + +# Example Use Cases + +- **Automated Invoice Generation and Emailing**: When a new order is placed via an e-commerce platform like Shopify, trigger a Pipedream workflow that creates an invoice in FacturaDirecta and then emails it to the customer. This reduces manual data entry and ensures invoices are sent out promptly. + +- **Expense Tracking and Reporting**: Use Pipedream to monitor new expenses added to FacturaDirecta, aggregate them weekly, and send a detailed report to your Slack channel. This helps in maintaining real-time visibility of the company's expenditures without manual intervention. + +- **Synchronized Customer Management**: When a new contact is added to your CRM, such as Salesforce, a Pipedream workflow can automatically create or update that contact in FacturaDirecta. This ensures consistency across business systems and saves time on contact management. diff --git a/components/faire/README.md b/components/faire/README.md new file mode 100644 index 0000000000000..7571c9d4e29a4 --- /dev/null +++ b/components/faire/README.md @@ -0,0 +1,11 @@ +# Overview + +The Faire API provides a gateway to automate retail operations, syncing products, orders, and inventory between Faire's marketplace and various business systems. Using Pipedream, you can connect Faire to a multitude of other services, crafting workflows that streamline retail processes, update inventory in real-time, and respond swiftly to new orders or customer inquiries. + +# Example Use Cases + +- **Order Fulfillment Automation**: When a new order is placed on Faire, trigger a Pipedream workflow that creates an order in your fulfillment system, like ShipStation or Shopify. Automatically send tracking information back to Faire when the order is shipped. + +- **Inventory Sync**: Set up a Pipedream workflow that regularly checks inventory levels in your database or ERP system. If the stock changes, it updates the inventory on Faire to keep product availability current, preventing overselling. + +- **Customer Support Ticket Creation**: Whenever a return request or a customer question comes through Faire, use Pipedream to create a ticket in your customer support platform, such as Zendesk, and assign it to the appropriate team, ensuring a quick response to customer needs. diff --git a/components/faktoora/README.md b/components/faktoora/README.md new file mode 100644 index 0000000000000..2f090fb833eb6 --- /dev/null +++ b/components/faktoora/README.md @@ -0,0 +1,11 @@ +# Overview + +The faktoora API offers a suite of tools for managing invoices and financial documents within applications. By integrating this API with Pipedream, you can automate tasks related to invoice creation, retrieval, and management, streamlining your financial operations. Pipedream's serverless execution model allows the API's capabilities to be woven into custom workflows that trigger on various events, process data, and connect to countless other apps to create powerful automations. + +# Example Use Cases + +- **Automated Invoice Processing Workflow**: When new invoices are issued via faktoora, this workflow can trigger, automatically capturing invoice data and storing it in a Google Sheets document for accounting purposes. It can also send out payment reminders to clients via email when due dates approach. + +- **Expense Tracking and Analysis**: Collect and analyze expenditure data by triggering a workflow whenever invoices are marked as paid in faktoora. The workflow could then feed this information into a data visualization tool like Tableau, giving you real-time insights into your financial health. + +- **Vendor Onboarding and Payment Automation**: Streamline vendor onboarding by using a workflow that, upon a new vendor signup in your system, creates a corresponding profile in faktoora. Automate subsequent invoice payments to these vendors when certain conditions are met, and log these transactions in your preferred accounting software. diff --git a/components/faktoora/actions/create-invoice/create-invoice.mjs b/components/faktoora/actions/create-invoice/create-invoice.mjs new file mode 100644 index 0000000000000..e42ff751cd352 --- /dev/null +++ b/components/faktoora/actions/create-invoice/create-invoice.mjs @@ -0,0 +1,106 @@ +import { parseObject } from "../../common/utils.mjs"; +import faktoora from "../../faktoora.app.mjs"; + +export default { + key: "faktoora-create-invoice", + name: "Create Invoice", + description: "Create a new ZUGFeRD/xrechnung invoice. [See the documentation](https://api.faktoora.com/api/v1/api-docs/static/index.html)", + version: "0.0.1", + type: "action", + props: { + faktoora, + format: { + type: "string", + label: "Format", + description: "The format of the invoice (ZUGFeRD/xrechnung).", + options: [ + "zf:1", + "zf:2", + "xrechnung", + ], + }, + invoiceNumber: { + propDefinition: [ + faktoora, + "invoiceNumber", + ], + }, + issueDate: { + type: "string", + label: "Issue Date", + description: "The issue date of the invoice (YYYYMMDD).", + }, + currency: { + type: "string", + label: "Currency", + description: "The currency of the invoice.", + }, + buyerName: { + type: "string", + label: "Buyer Name", + description: "The name of the buyer.", + }, + buyerPostcode: { + type: "string", + label: "Buyer Postcode", + description: "Buyer location postcode.", + }, + buyerStreet: { + type: "string", + label: "Buyer Street", + description: "Buyer location street.", + }, + buyerCity: { + type: "string", + label: "Buyer City", + description: "Buyer location city.", + }, + buyerCountry: { + type: "string", + label: "Buyer Country", + description: "Buyer location country.", + }, + invoiceItems: { + type: "string[]", + label: "Invoice Items", + description: "A list of product objects. E.g. **{\"productId\": \"39887\", \"name\": \"Freezer Z7749\", \"quantity\": \"1\", \"unitCode\": \"C62\", \"price\": 100, \"taxes\": [{\"typeCode\": \"VAT\", \"categoryCode\": \"S\", \"rate\": 16}] }**. For further information about init code [click here](https://www.unece.org/fileadmin/DAM/cefact/recommendations/rec20/rec20_rev3_Annex1e.pdf). For further information about Category Code [click here](https://docs.peppol.eu/poacc/billing/3.0/codelist/UNCL5305/)", + }, + additionalData: { + type: "object", + label: "Additional Data", + description: "An object to manual input other fields. Please check the fields from [the API doc > POST /invoice > Request Body](https://api.faktoora.com/api/v1/api-docs/static/index.html)", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.faktoora.createInvoice({ + $, + data: { + invoices: [ + { + format: this.format, + invoiceNumber: this.invoiceNumber, + issueDate: this.issueDate, + currency: this.currency, + buyer: { + name: this.buyerName, + postcode: this.buyerPostcode, + street: this.buyerStreet, + city: this.buyerCity, + country: this.buyerCountry, + }, + invoiceItems: this.invoiceItems && parseObject(this.invoiceItems) + .map((item, index) => ({ + id: `${index + 1}`, + product: item, + })), + ...(parseObject(this.additionalData) || {}), + }, + ], + }, + }); + + $.export("$summary", `Successfully created invoice with number ${this.invoiceNumber}`); + return response; + }, +}; diff --git a/components/faktoora/actions/get-invoice/get-invoice.mjs b/components/faktoora/actions/get-invoice/get-invoice.mjs new file mode 100644 index 0000000000000..0c1150c28ccd6 --- /dev/null +++ b/components/faktoora/actions/get-invoice/get-invoice.mjs @@ -0,0 +1,39 @@ +import { ConfigurationError } from "@pipedream/platform"; +import fs from "fs"; +import faktoora from "../../faktoora.app.mjs"; + +export default { + key: "faktoora-get-invoice", + name: "Download Invoice", + description: "Download an invoice using the unique invoice number to '/tmp' folder. [See the documentation](https://api.faktoora.com/api/v1/api-docs/static/index.html)", + version: "0.0.1", + type: "action", + props: { + faktoora, + invoiceNumber: { + propDefinition: [ + faktoora, + "invoiceNumber", + ], + }, + }, + async run({ $ }) { + try { + const response = await this.faktoora.fetchInvoice({ + $, + responseType: "arraybuffer", + params: { + invoiceNumber: this.invoiceNumber, + }, + }); + + const filePath = `/tmp/invoice-${this.invoiceNumber.replace(/\//g, "-")}.pdf`; + fs.writeFileSync(filePath, response); + + $.export("$summary", `Successfully downloaded invoice with number: ${this.invoiceNumber}`); + return filePath; + } catch (e) { + throw new ConfigurationError("Invoice not found."); + } + }, +}; diff --git a/components/faktoora/common/utils.mjs b/components/faktoora/common/utils.mjs new file mode 100644 index 0000000000000..154701004e72a --- /dev/null +++ b/components/faktoora/common/utils.mjs @@ -0,0 +1,18 @@ +export const parseObject = (obj) => { + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + return JSON.parse(obj); + } + return obj; +}; diff --git a/components/faktoora/faktoora.app.mjs b/components/faktoora/faktoora.app.mjs index 000530c6206b1..db59276ff6931 100644 --- a/components/faktoora/faktoora.app.mjs +++ b/components/faktoora/faktoora.app.mjs @@ -1,11 +1,45 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "faktoora", - propDefinitions: {}, + propDefinitions: { + invoiceNumber: { + type: "string", + label: "Invoice Number", + description: "The invoice number.", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `https://${this.$auth.environment}.faktoora.com/api/v1`; + }, + _headers() { + return { + "X-API-KEY": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: this._headers(), + }); + }, + createInvoice(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/invoices", + ...opts, + }); + }, + fetchInvoice(opts = {}) { + return this._makeRequest({ + path: "/invoices", + ...opts, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/faktoora/package.json b/components/faktoora/package.json index c93f448a870dd..02fd4a58c5494 100644 --- a/components/faktoora/package.json +++ b/components/faktoora/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/faktoora", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream faktoora Components", "main": "faktoora.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1", + "fs": "^0.0.1-security" } -} \ No newline at end of file +} diff --git a/components/fakturoid/actions/cancel-uncancel-invoice/cancel-uncancel-invoice.mjs b/components/fakturoid/actions/cancel-uncancel-invoice/cancel-uncancel-invoice.mjs new file mode 100644 index 0000000000000..783fc302950e8 --- /dev/null +++ b/components/fakturoid/actions/cancel-uncancel-invoice/cancel-uncancel-invoice.mjs @@ -0,0 +1,49 @@ +import constants from "../../common/constants.mjs"; +import fakturoid from "../../fakturoid.app.mjs"; + +export default { + key: "fakturoid-cancel-uncancel-invoice", + name: "Cancel or Uncancel Invoice", + description: "Cancels an existing invoice or revokes previous cancellation. [See the documentation](https://www.fakturoid.cz/api/v3)", + version: "0.0.1", + type: "action", + props: { + fakturoid, + accountSlug: { + propDefinition: [ + fakturoid, + "accountSlug", + ], + }, + invoiceId: { + propDefinition: [ + fakturoid, + "invoiceId", + ({ accountSlug }) => ({ + accountSlug, + }), + ], + }, + action: { + type: "string", + label: "Action", + description: "The action to perform on the invoice (cancel or uncancel)", + options: constants.ACTION_OPTIONS, + }, + }, + async run({ $ }) { + const response = await this.fakturoid.fireInvoice({ + $, + accountSlug: this.accountSlug, + invoiceId: this.invoiceId, + params: { + event: this.action, + }, + }); + + $.export("$summary", `${this.action === "cancel" + ? "Cancelled" + : "Uncancelled"} invoice with ID ${this.invoiceId}`); + return response; + }, +}; diff --git a/components/fakturoid/actions/create-invoice/create-invoice.mjs b/components/fakturoid/actions/create-invoice/create-invoice.mjs new file mode 100644 index 0000000000000..f11db2fb28c54 --- /dev/null +++ b/components/fakturoid/actions/create-invoice/create-invoice.mjs @@ -0,0 +1,139 @@ +import constants, { parseObject } from "../../common/constants.mjs"; +import fakturoid from "../../fakturoid.app.mjs"; + +export default { + key: "fakturoid-create-invoice", + name: "Create Invoice", + description: "Creates a new invoice. [See the documentation](https://www.fakturoid.cz/api/v3/invoices)", + version: "0.0.1", + type: "action", + props: { + fakturoid, + accountSlug: { + propDefinition: [ + fakturoid, + "accountSlug", + ], + }, + customId: { + type: "string", + label: "Custom Id", + description: "Identifier in your application", + optional: true, + }, + documentType: { + type: "string", + label: "Document Type", + description: "Type of document", + options: constants.DOCUMENT_TYPE_OPTIONS, + reloadProps: true, + optional: true, + }, + subjectId: { + propDefinition: [ + fakturoid, + "subjectId", + ({ accountSlug }) => ({ + accountSlug, + }), + ], + }, + orderNumber: { + type: "string", + label: "Order Number", + description: "Order number in your application", + optional: true, + }, + note: { + type: "string", + label: "Note", + description: "Additional notes for the invoice", + optional: true, + }, + due: { + type: "string", + label: "Due", + description: "Invoice due date in number of days from today", + optional: true, + }, + issuedOn: { + type: "string", + label: "Issued On", + description: "Date of issue. **Format: YYYY-MM-DD**", + optional: true, + }, + taxableFulfillmentDue: { + type: "string", + label: "Taxable Fulfillment Due", + description: "Chargeable event date.", + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "List of tags", + optional: true, + }, + roundTotal: { + type: "boolean", + label: "Round Total", + description: "Round total amount (VAT included)", + optional: true, + }, + subtotal: { + type: "string", + label: "Subtotal", + description: "Total without VAT", + optional: true, + }, + total: { + type: "string", + label: "Total", + description: "Total with VAT", + optional: true, + }, + lines: { + type: "string[]", + label: "Lines", + description: "List of object lines to invoice. [See the documentation](https://www.fakturoid.cz/api/v3/invoices#attributes). **Example: {\"name\": \"Hard work\",\"quantity\": \"1.0\",\"unit_name\": \"h\",\"unit_price\": \"40000\",\"vat_rate\": \"21\"}**", + }, + }, + async additionalProps() { + const props = {}; + if (this.documentType === "proforma") { + props.proformaFollowupDocument = { + type: "string", + label: "Proforma Followup Document", + description: "What to issue after a proforma is paid.", + options: constants.PROFORMA_OPTIONS, + optional: true, + }; + } + return props; + }, + async run({ $ }) { + const response = await this.fakturoid.createInvoice({ + $, + accountSlug: this.accountSlug, + data: { + custom_id: this.customId, + document_type: this.documentType, + proforma_followup_document: this.proformaFollowupDocument, + subject_id: this.subjectId, + order_number: this.orderNumber, + note: this.note, + due: this.due, + issued_on: this.issuedOn, + taxable_fulfillment_due: this.taxableFulfillmentDue, + tags: parseObject(this.tags), + round_total: this.roundTotal, + subtotal: this.subtotal && parseFloat(this.subtotal), + total: this.total && parseFloat(this.total), + lines: parseObject(this.lines), + }, + }); + + $.export("$summary", `Successfully created invoice with ID ${response.id}`); + return response; + }, +}; diff --git a/components/fakturoid/actions/pay-remove-payment-invoice/pay-remove-payment-invoice.mjs b/components/fakturoid/actions/pay-remove-payment-invoice/pay-remove-payment-invoice.mjs new file mode 100644 index 0000000000000..135fa078c2374 --- /dev/null +++ b/components/fakturoid/actions/pay-remove-payment-invoice/pay-remove-payment-invoice.mjs @@ -0,0 +1,109 @@ +import constants from "../../common/constants.mjs"; +import fakturoid from "../../fakturoid.app.mjs"; + +export default { + key: "fakturoid-pay-remove-payment-invoice", + name: "Pay or Remove Payment for Invoice", + description: "Executes payment for an invoice or removes an already applied payment. [See the documentation](https://www.fakturoid.cz/api/v3/invoice-payments)", + version: "0.0.1", + type: "action", + props: { + fakturoid, + accountSlug: { + propDefinition: [ + fakturoid, + "accountSlug", + ], + }, + invoiceId: { + propDefinition: [ + fakturoid, + "invoiceId", + ({ accountSlug }) => ({ + accountSlug, + }), + ], + }, + actionType: { + type: "string", + label: "Action Type", + description: "Specify if you want to execute or remove a payment", + options: constants.ACTION_TYPE_OPTIONS, + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.actionType === "execute") { + props.paidOn = { + type: "string", + label: "Paid On", + description: "Payment date. **Format: YYYY-MM-DD** Default: Today", + optional: true, + }; + props.currency = { + type: "string", + label: "Currency", + description: "Currency [ISO Code](https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes) (same as invoice currency)", + optional: true, + }; + props.amount = { + type: "string", + label: "Amount", + description: "Paid amount in document currency. Default: Remaining amount to pay", + optional: true, + }; + props.markDocumentAsPaid = { + type: "boolean", + label: "Mark Document As Paid", + description: "Mark document as paid? Default: true if the total paid amount becomes greater or equal to remaining amount to pay", + optional: true, + }; + } else if (this.actionType === "remove") { + props.paymentId = { + type: "string", + label: "Payment ID", + description: "ID of the payment to be removed.", + options: async () => { + const { payments } = await this.fakturoid.getInvoice({ + accountSlug: this.accountSlug, + invoiceId: this.invoiceId, + }); + + return payments.map(({ + id: value, paid_on: pOn, currency, amount, + }) => ({ + label: `${currency} ${amount} (${pOn})`, + value, + })); + }, + }; + } + return props; + }, + async run({ $ }) { + if (this.actionType === "execute") { + const response = await this.fakturoid.payInvoice({ + accountSlug: this.accountSlug, + invoiceId: this.invoiceId, + data: { + paid_on: this.paidOn, + currency: this.currency, + amount: this.amount && parseFloat(this.amount), + mark_document_as_paid: this.markDocumentAsPaid, + }, + }); + $.export("$summary", `Successfully executed payment for invoice ID ${this.invoiceId}`); + return response; + } else if (this.actionType === "remove") { + const response = await this.fakturoid.removePayment({ + $, + accountSlug: this.accountSlug, + invoiceId: this.invoiceId, + paymentId: this.paymentId, + }); + $.export("$summary", `Successfully removed payment ID ${this.paymentId} from invoice ID ${this.invoiceId}`); + return response; + } + }, +}; diff --git a/components/fakturoid/common/constants.mjs b/components/fakturoid/common/constants.mjs new file mode 100644 index 0000000000000..97dbb8ff255b2 --- /dev/null +++ b/components/fakturoid/common/constants.mjs @@ -0,0 +1,159 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; + +const DOCUMENT_TYPE_OPTIONS = [ + { + label: "Invoice", + value: "invoice", + }, + { + label: "Proforma", + value: "proforma", + }, + { + label: "Legacy partial proforma (cannot be set for new documents)", + value: "partial_proforma", + }, + { + label: "Correction document for an invoice", + value: "correction", + }, + { + label: "Tax document for a received payment", + value: "tax_document", + }, + { + label: "Final invoice for tax documents", + value: "final_invoice", + }, +]; + +const PROFORMA_OPTIONS = [ + { + label: "Invoice paid", + value: "final_invoice_paid", + }, + { + label: "Invoice with edit", + value: "final_invoice", + }, + { + label: "Document to payment", + value: "tax_document", + }, + { + label: "None", + value: "none", + }, +]; + +const ACTION_OPTIONS = [ + { + label: "Cancel", + value: "cancel", + }, + { + label: "Uncancel", + value: "undo_cancel", + }, +]; + +const ACTION_TYPE_OPTIONS = [ + { + label: "Execute Payment", + value: "execute", + }, + { + label: "Remove Payment", + value: "remove", + }, +]; + +const EVENT_OPTIONS = [ + { + label: "Invoice was updated", + value: "invoice_updated", + }, + { + label: "Invoice was deleted and moved to the trash", + value: "invoice_removed", + }, + { + label: "Invoice was restored from the trash", + value: "invoice_restored", + }, + { + label: "Invoice was marked as overdue", + value: "invoice_overdue", + }, + { + label: "Payment was added to document and marked it as paid", + value: "invoice_paid", + }, + { + label: "Payment was added to the invoice, but didn't mark it as paid", + value: "invoice_payment_added", + }, + { + label: "Invoice was marked as unpaid, and the payments in the additional payload were removed", + value: "invoice_payment_removed", + }, + { + label: "Email with the invoice was sent to the subject", + value: "invoice_sent", + }, + { + label: "Invoice was locked", + value: "invoice_locked", + }, + { + label: "Invoice was unlocked", + value: "invoice_unlocked", + }, + { + label: "Invoice was marked as cancelled", + value: "invoice_cancelled", + }, + { + label: "Cancellation was removed", + value: "invoice_cancellation_removed", + }, + { + label: "Invoice was marked as uncollectible", + value: "invoice_uncollectible", + }, + { + label: "Uncollectibility was removed", + value: "invoice_uncollectible_removed", + }, +]; + +export default { + DOCUMENT_TYPE_OPTIONS, + PROFORMA_OPTIONS, + ACTION_OPTIONS, + ACTION_TYPE_OPTIONS, + EVENT_OPTIONS, +}; diff --git a/components/fakturoid/common/utils.mjs b/components/fakturoid/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/fakturoid/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/fakturoid/fakturoid.app.mjs b/components/fakturoid/fakturoid.app.mjs new file mode 100644 index 0000000000000..e4c7d5228ab71 --- /dev/null +++ b/components/fakturoid/fakturoid.app.mjs @@ -0,0 +1,167 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "fakturoid", + propDefinitions: { + accountSlug: { + type: "string", + label: "Account", + description: "The account you want to use", + async options() { + const { accounts } = await this.getLoggedUser(); + + return accounts.map(({ + slug: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + subjectId: { + type: "string", + label: "Subject Id", + description: "The id of the subject", + async options({ + page, accountSlug, + }) { + const data = await this.listSubjects({ + accountSlug, + params: { + page: page + 1, + }, + }); + + return data.map(({ + id: value, name, full_name: fName, + }) => ({ + label: fName || name, + value, + })); + }, + }, + invoiceId: { + type: "string", + label: "Invoice ID", + description: "Unique identifier for the invoice", + async options({ + page, accountSlug, + }) { + const data = await this.listInvoices({ + accountSlug, + params: { + page: page + 1, + }, + }); + + return data.map(({ + id: value, client_name: cName, due_on: due, currency, total, + }) => ({ + label: `${cName} - ${currency} ${total} - Due On: ${due}`, + value, + })); + }, + }, + }, + methods: { + _baseUrl(accountSlug) { + return `https://app.fakturoid.cz/api/v3${accountSlug + ? `/accounts/${accountSlug}` + : ""}`; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, accountSlug, path, ...opts + }) { + return axios($, { + url: this._baseUrl(accountSlug) + path, + headers: this._headers(), + ...opts, + }); + }, + getLoggedUser() { + return this._makeRequest({ + path: "/user.json", + }); + }, + getInvoice({ + invoiceId, ...opts + }) { + return this._makeRequest({ + path: `/invoices/${invoiceId}.json`, + ...opts, + }); + }, + listSubjects(opts = {}) { + return this._makeRequest({ + path: "/subjects.json", + ...opts, + }); + }, + listInvoices(opts = {}) { + return this._makeRequest({ + path: "/invoices.json", + ...opts, + }); + }, + createInvoice(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/invoices.json", + ...opts, + }); + }, + fireInvoice({ + invoiceId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/invoices/${invoiceId}/fire.json`, + ...opts, + }); + }, + payInvoice({ + invoiceId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/invoices/${invoiceId}/payments.json`, + ...opts, + }); + }, + removePayment({ + invoiceId, paymentId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/invoices/${invoiceId}/payments/${paymentId}.json`, + ...opts, + }); + }, + async *paginate({ + fn, params = {}, ...opts + }) { + let hasMore = false; + let page = 0; + + do { + params.page = ++page; + const data = await fn({ + params, + ...opts, + }); + for (const d of data) { + yield d; + } + + hasMore = data.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/fakturoid/package.json b/components/fakturoid/package.json new file mode 100644 index 0000000000000..31085d2cedd89 --- /dev/null +++ b/components/fakturoid/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/fakturoid", + "version": "0.1.0", + "description": "Pipedream Fakturoid Components", + "main": "fakturoid.app.mjs", + "keywords": [ + "pipedream", + "fakturoid" + ], + "homepage": "https://pipedream.com/apps/fakturoid", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} + diff --git a/components/fakturoid/sources/common/base.mjs b/components/fakturoid/sources/common/base.mjs new file mode 100644 index 0000000000000..45859d6c232b8 --- /dev/null +++ b/components/fakturoid/sources/common/base.mjs @@ -0,0 +1,77 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import fakturoid from "../../fakturoid.app.mjs"; + +export default { + props: { + fakturoid, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + accountSlug: { + propDefinition: [ + fakturoid, + "accountSlug", + ], + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01T00:00:00"; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + getParams(lastDate) { + return { + since: lastDate, + }; + }, + getDateField() { + return "created_at"; + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const dateField = this.getDateField(); + const response = this.fakturoid.paginate({ + fn: this.getFunction(), + accountSlug: this.accountSlug, + params: this.getParams(lastDate), + }); + + let responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + if (responseArray.length) { + responseArray = responseArray + .sort((a, b) => Date.parse(b[dateField]) - Date.parse(a[dateField])); + + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastDate(responseArray[0][dateField]); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: `${item.id}-${item[dateField]}`, + summary: this.getSummary(item), + ts: Date.parse(item[dateField]), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/fakturoid/sources/invoice-updated/invoice-updated.mjs b/components/fakturoid/sources/invoice-updated/invoice-updated.mjs new file mode 100644 index 0000000000000..7a632c6c3615a --- /dev/null +++ b/components/fakturoid/sources/invoice-updated/invoice-updated.mjs @@ -0,0 +1,30 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "fakturoid-invoice-updated", + name: "New Invoice Updated", + description: "Emit new event when an invoice is created or updated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.fakturoid.listInvoices; + }, + getSummary(invoice) { + return `New invoice updated: ${invoice.number}`; + }, + getParams(lastDate) { + return { + updated_since: lastDate, + }; + }, + getDateField() { + return "updated_at"; + }, + }, + sampleEmit, +}; diff --git a/components/fakturoid/sources/invoice-updated/test-event.mjs b/components/fakturoid/sources/invoice-updated/test-event.mjs new file mode 100644 index 0000000000000..63e50c797044c --- /dev/null +++ b/components/fakturoid/sources/invoice-updated/test-event.mjs @@ -0,0 +1,146 @@ +export default { + "id": 27, + "custom_id": null, + "document_type": "invoice", + "proforma_followup_document": null, + "correction_id": null, + "number": "2023-0021", + "number_format_id": 31, + "variable_symbol": "20230021", + "your_name": "Alexandr Hejsek", + "your_street": "Hopsinková 14", + "your_city": "Praha", + "your_zip": "10000", + "your_country": "CZ", + "your_registration_no": "87654321", + "your_vat_no": "CZ12121212", + "your_local_vat_no": null, + "client_name": "Apple Czech s.r.o.", + "client_street": "Klimentská 1216/46", + "client_city": "Praha", + "client_zip": "11000", + "client_country": "CZ", + "client_registration_no": "28897501", + "client_vat_no": "CZ28897501", + "client_local_vat_no": null, + "client_has_delivery_address": false, + "client_delivery_name": null, + "client_delivery_street": null, + "client_delivery_city": null, + "client_delivery_zip": null, + "client_delivery_country": null, + "subject_id": 16, + "subject_custom_id": null, + "generator_id": null, + "related_id": null, + "paypal": false, + "gopay": false, + "token": "69UqMuxhiA", + "status": "sent", + "order_number": null, + "issued_on": "2023-11-30", + "taxable_fulfillment_due": "2023-11-30", + "due": 14, + "due_on": "2023-12-14", + "sent_at": "2023-12-01T09:05:47.117+01:00", + "paid_on": null, + "reminder_sent_at": null, + "cancelled_at": null, + "uncollectible_at": null, + "locked_at": null, + "webinvoice_seen_on": null, + "note": "Fakturujeme Vám následující položky", + "footer_note": "", + "private_note": null, + "tags": [], + "bank_account": "1234/2010", + "iban": null, + "swift_bic": null, + "iban_visibility": "automatically", + "show_already_paid_note_in_pdf": false, + "payment_method": "bank", + "custom_payment_method": null, + "hide_bank_account": false, + "currency": "CZK", + "exchange_rate": "1.0", + "language": "cz", + "transferred_tax_liability": false, + "supply_code": null, + "oss": "disabled", + "vat_price_mode": "with_vat", + "subtotal": "9133.6", + "total": "11000.0", + "native_subtotal": "9133.6", + "native_total": "11000.0", + "remaining_amount": "11000.0", + "remaining_native_amount": "11000.0", + "eet_records": [], + "lines": [ + { + "id": 46, + "name": "Grafická karta", + "quantity": "1.0", + "unit_name": "", + "unit_price": "8264.0", + "vat_rate": 21, + "unit_price_without_vat": "8264.0", + "unit_price_with_vat": "10000.0", + "total_price_without_vat": "8264.0", + "total_vat": "1736.0", + "native_total_price_without_vat": "8264.0", + "native_total_vat": "1736.0", + "inventory": { + "item_id": 26, + "sku": "KU994RUR8465", + "article_number_type": null, + "article_number": null, + "move_id": 56 + } + }, + { + "id": 47, + "name": "Jídlo", + "quantity": "5.0", + "unit_name": "", + "unit_price": "173.92", + "vat_rate": 15, + "unit_price_without_vat": "173.92", + "unit_price_with_vat": "200.0", + "total_price_without_vat": "869.6", + "total_vat": "130.4", + "native_total_price_without_vat": "869.6", + "native_total_vat": "130.4", + "inventory": null + } + ], + "vat_rates_summary": [ + { + "vat_rate": 21, + "base": "8264.0", + "vat": "1736.0", + "currency": "CZK", + "native_base": "8264.0", + "native_vat": "1736.0", + "native_currency": "CZK" + }, + { + "vat_rate": 15, + "base": "869.6", + "vat": "130.4", + "currency": "CZK", + "native_base": "869.6", + "native_vat": "130.4", + "native_currency": "CZK" + } + ], + "paid_advances": [], + "payments": [], + "attachments": null, + "html_url": "https://app.fakturoid.cz/applecorp/invoices/27", + "public_html_url": "https://app.fakturoid.cz/applecorp/p/69UqMuxhiA/2023-0021", + "url": "https://app.fakturoid.cz/api/v3/accounts/applecorp/invoices/27.json", + "pdf_url": "https://app.fakturoid.cz/api/v3/accounts/applecorp/invoices/27/download.pdf", + "subject_url": "https://app.fakturoid.cz/api/v3/accounts/applecorp/subjects/16.json", + "created_at": "2023-11-30T13:50:45.848+01:00", + "updated_at": "2023-12-01T09:05:47.187+01:00" +} \ No newline at end of file diff --git a/components/fakturoid/sources/new-contact/new-contact.mjs b/components/fakturoid/sources/new-contact/new-contact.mjs new file mode 100644 index 0000000000000..b90f04af741cf --- /dev/null +++ b/components/fakturoid/sources/new-contact/new-contact.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "fakturoid-new-contact", + name: "New Contact Added", + description: "Emit new event when a contact (subject) is added in Fakturoid.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.fakturoid.listSubjects; + }, + getSummary(contact) { + return `New Contact Added: ${contact.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/fakturoid/sources/new-contact/test-event.mjs b/components/fakturoid/sources/new-contact/test-event.mjs new file mode 100644 index 0000000000000..4e23ba4496728 --- /dev/null +++ b/components/fakturoid/sources/new-contact/test-event.mjs @@ -0,0 +1,53 @@ +export default { + "id": 16, + "custom_id": null, + "user_id": null, + "type": "customer", + "name": "Apple Czech s.r.o.", + "full_name": null, + "email": "pokus@test.cz", + "email_copy": null, + "phone": null, + "web": "https://www.apple.cz", + "street": "Klimentská 1216/46", + "city": "Praha", + "zip": "11000", + "country": "CZ", + "has_delivery_address": false, + "delivery_name": null, + "delivery_street": null, + "delivery_city": null, + "delivery_zip": null, + "delivery_country": null, + "due": null, + "currency": null, + "language": null, + "private_note": null, + "registration_no": "28897501", + "vat_no": "CZ28897501", + "local_vat_no": null, + "unreliable": null, + "unreliable_checked_at": null, + "legal_form": null, + "vat_mode": null, + "bank_account": null, + "iban": null, + "swift_bic": null, + "variable_symbol": null, + "setting_update_from_ares": "inherit", + "ares_update": false, + "setting_invoice_pdf_attachments": "inherit", + "setting_estimate_pdf_attachments": "inherit", + "setting_invoice_send_reminders": "inherit", + "suggestion_enabled": true, + "custom_email_text": null, + "overdue_email_text": null, + "invoice_from_proforma_email_text": null, + "thank_you_email_text": null, + "custom_estimate_email_text": null, + "webinvoice_history": null, + "html_url": "https://app.fakturoid.cz/applecorp/subjects/16", + "url": "https://app.fakturoid.cz/api/v3/accounts/applecorp/subjects/16.json", + "created_at": "2023-08-22T10:59:00.330+02:00", + "updated_at": "2023-08-22T10:59:00.330+02:00" +} \ No newline at end of file diff --git a/components/fakturoid/sources/new-invoice/new-invoice.mjs b/components/fakturoid/sources/new-invoice/new-invoice.mjs new file mode 100644 index 0000000000000..9369ed5788fc9 --- /dev/null +++ b/components/fakturoid/sources/new-invoice/new-invoice.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "fakturoid-new-invoice", + name: "New Invoice Created", + description: "Emit new event when a new invoice is created in Fakturoid.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.fakturoid.listInvoices; + }, + getSummary(invoice) { + return `New invoice created: ${invoice.number}`; + }, + }, + sampleEmit, +}; diff --git a/components/fakturoid/sources/new-invoice/test-event.mjs b/components/fakturoid/sources/new-invoice/test-event.mjs new file mode 100644 index 0000000000000..63e50c797044c --- /dev/null +++ b/components/fakturoid/sources/new-invoice/test-event.mjs @@ -0,0 +1,146 @@ +export default { + "id": 27, + "custom_id": null, + "document_type": "invoice", + "proforma_followup_document": null, + "correction_id": null, + "number": "2023-0021", + "number_format_id": 31, + "variable_symbol": "20230021", + "your_name": "Alexandr Hejsek", + "your_street": "Hopsinková 14", + "your_city": "Praha", + "your_zip": "10000", + "your_country": "CZ", + "your_registration_no": "87654321", + "your_vat_no": "CZ12121212", + "your_local_vat_no": null, + "client_name": "Apple Czech s.r.o.", + "client_street": "Klimentská 1216/46", + "client_city": "Praha", + "client_zip": "11000", + "client_country": "CZ", + "client_registration_no": "28897501", + "client_vat_no": "CZ28897501", + "client_local_vat_no": null, + "client_has_delivery_address": false, + "client_delivery_name": null, + "client_delivery_street": null, + "client_delivery_city": null, + "client_delivery_zip": null, + "client_delivery_country": null, + "subject_id": 16, + "subject_custom_id": null, + "generator_id": null, + "related_id": null, + "paypal": false, + "gopay": false, + "token": "69UqMuxhiA", + "status": "sent", + "order_number": null, + "issued_on": "2023-11-30", + "taxable_fulfillment_due": "2023-11-30", + "due": 14, + "due_on": "2023-12-14", + "sent_at": "2023-12-01T09:05:47.117+01:00", + "paid_on": null, + "reminder_sent_at": null, + "cancelled_at": null, + "uncollectible_at": null, + "locked_at": null, + "webinvoice_seen_on": null, + "note": "Fakturujeme Vám následující položky", + "footer_note": "", + "private_note": null, + "tags": [], + "bank_account": "1234/2010", + "iban": null, + "swift_bic": null, + "iban_visibility": "automatically", + "show_already_paid_note_in_pdf": false, + "payment_method": "bank", + "custom_payment_method": null, + "hide_bank_account": false, + "currency": "CZK", + "exchange_rate": "1.0", + "language": "cz", + "transferred_tax_liability": false, + "supply_code": null, + "oss": "disabled", + "vat_price_mode": "with_vat", + "subtotal": "9133.6", + "total": "11000.0", + "native_subtotal": "9133.6", + "native_total": "11000.0", + "remaining_amount": "11000.0", + "remaining_native_amount": "11000.0", + "eet_records": [], + "lines": [ + { + "id": 46, + "name": "Grafická karta", + "quantity": "1.0", + "unit_name": "", + "unit_price": "8264.0", + "vat_rate": 21, + "unit_price_without_vat": "8264.0", + "unit_price_with_vat": "10000.0", + "total_price_without_vat": "8264.0", + "total_vat": "1736.0", + "native_total_price_without_vat": "8264.0", + "native_total_vat": "1736.0", + "inventory": { + "item_id": 26, + "sku": "KU994RUR8465", + "article_number_type": null, + "article_number": null, + "move_id": 56 + } + }, + { + "id": 47, + "name": "Jídlo", + "quantity": "5.0", + "unit_name": "", + "unit_price": "173.92", + "vat_rate": 15, + "unit_price_without_vat": "173.92", + "unit_price_with_vat": "200.0", + "total_price_without_vat": "869.6", + "total_vat": "130.4", + "native_total_price_without_vat": "869.6", + "native_total_vat": "130.4", + "inventory": null + } + ], + "vat_rates_summary": [ + { + "vat_rate": 21, + "base": "8264.0", + "vat": "1736.0", + "currency": "CZK", + "native_base": "8264.0", + "native_vat": "1736.0", + "native_currency": "CZK" + }, + { + "vat_rate": 15, + "base": "869.6", + "vat": "130.4", + "currency": "CZK", + "native_base": "869.6", + "native_vat": "130.4", + "native_currency": "CZK" + } + ], + "paid_advances": [], + "payments": [], + "attachments": null, + "html_url": "https://app.fakturoid.cz/applecorp/invoices/27", + "public_html_url": "https://app.fakturoid.cz/applecorp/p/69UqMuxhiA/2023-0021", + "url": "https://app.fakturoid.cz/api/v3/accounts/applecorp/invoices/27.json", + "pdf_url": "https://app.fakturoid.cz/api/v3/accounts/applecorp/invoices/27/download.pdf", + "subject_url": "https://app.fakturoid.cz/api/v3/accounts/applecorp/subjects/16.json", + "created_at": "2023-11-30T13:50:45.848+01:00", + "updated_at": "2023-12-01T09:05:47.187+01:00" +} \ No newline at end of file diff --git a/components/fal_ai/actions/add-request-to-queue/add-request-to-queue.mjs b/components/fal_ai/actions/add-request-to-queue/add-request-to-queue.mjs new file mode 100644 index 0000000000000..4e7370c783b83 --- /dev/null +++ b/components/fal_ai/actions/add-request-to-queue/add-request-to-queue.mjs @@ -0,0 +1,124 @@ +import app from "../../fal_ai.app.mjs"; + +export default { + key: "fal_ai-add-request-to-queue", + name: "Add Request to Queue", + description: "Adds a request to the queue for asynchronous processing, including specifying a webhook URL for receiving updates. [See the documentation](https://fal.ai/docs/model-endpoints/queue#queue-endpoints).", + version: "0.0.1", + type: "action", + props: { + app, + appId: { + propDefinition: [ + app, + "appId", + ], + }, + data: { + type: "object", + label: "Data", + description: "Additional data to include with the request. [See the documentation](https://fal.ai/models/fal-ai/lora/api#schema-input) for more input fields.", + default: { + model_name: "stabilityai/stable-diffusion-xl-base-1.0", + prompt: "Photo of a european medieval 40 year old queen, silver hair, highly detailed face, detailed eyes, head shot, intricate crown, age spots, wrinkles", + }, + }, + reRunEnabled: { + type: "boolean", + label: "Rerun Enabled", + description: "Enable the step to rerun to retrieve the request response. [See the documentation](https://pipedream.com/docs/code/nodejs/rerun/#flowrerun).", + optional: true, + reloadProps: true, + default: false, + }, + }, + additionalProps() { + if (this.reRunEnabled) { + return { + reRunTimeoutInSecs: { + type: "integer", + label: "Rerun Timeout", + description: "The time in seconds to wait before rerunning the step to retrieve the request response. Eg. `30`. [See the documentation](https://pipedream.com/docs/code/nodejs/rerun/#flowrerun).", + optional: true, + min: 10, + }, + }; + } + + return { + falWebhook: { + type: "string", + label: "Webhook URL", + description: "The URL to receive updates via webhook.", + optional: true, + }, + }; + }, + methods: { + addToQueue({ + appId, ...args + } = {}) { + return this.app.post({ + path: `/${appId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + context: { + run: { + runs, + callback_request: callbackRequest, + }, + }, + } = $; + + const { + app, + addToQueue, + appId, + data, + falWebhook, + reRunEnabled, + reRunTimeoutInSecs, + } = this; + + if (!reRunEnabled) { + const response = await addToQueue({ + $, + appId, + params: { + fal_webhook: falWebhook, + }, + data, + }); + + $.export("$summary", `Successfully added the request to the queue with ID \`${response.request_id}\`.`); + return response; + } + + if (runs === 1) { + const timeout = 1000 * (reRunTimeoutInSecs || 10); + const { resume_url: resumeUrl } = $.flow.rerun(timeout, null, 1); + + return addToQueue({ + $, + appId, + params: { + fal_webhook: resumeUrl, + }, + data, + }); + } + + const response = await app.getRequestResponse({ + $, + appId, + requestId: callbackRequest.body?.request_id, + }); + + $.export("$summary", "Successfully retrieved the request response."); + return response; + }, +}; diff --git a/components/fal_ai/actions/cancel-request/cancel-request.mjs b/components/fal_ai/actions/cancel-request/cancel-request.mjs new file mode 100644 index 0000000000000..237e56cff93a3 --- /dev/null +++ b/components/fal_ai/actions/cancel-request/cancel-request.mjs @@ -0,0 +1,50 @@ +import app from "../../fal_ai.app.mjs"; + +export default { + key: "fal_ai-cancel-request", + name: "Cancel Request", + description: "Cancels a request in the queue. This allows you to stop a long-running task if it's no longer needed. [See the documentation](https://fal.ai/docs/model-endpoints/queue#queue-endpoints).", + version: "0.0.1", + type: "action", + props: { + app, + appId: { + propDefinition: [ + app, + "appId", + ], + }, + requestId: { + propDefinition: [ + app, + "requestId", + ], + }, + }, + methods: { + cancelRequest({ + appId, requestId, ...args + } = {}) { + return this.app.put({ + path: `/${appId}/requests/${requestId}/cancel`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + cancelRequest, + appId, + requestId, + } = this; + + const response = await cancelRequest({ + $, + appId, + requestId, + }); + + $.export("$summary", "Successfully canceled request."); + return response; + }, +}; diff --git a/components/fal_ai/actions/get-request-response/get-request-response.mjs b/components/fal_ai/actions/get-request-response/get-request-response.mjs new file mode 100644 index 0000000000000..1869e9d5c2c03 --- /dev/null +++ b/components/fal_ai/actions/get-request-response/get-request-response.mjs @@ -0,0 +1,40 @@ +import app from "../../fal_ai.app.mjs"; + +export default { + key: "fal_ai-get-request-response", + name: "Get Request Response", + description: "Gets the response of a completed request in the queue. This retrieves the results of your asynchronous task. [See the documentation](https://fal.ai/docs/model-endpoints/queue#queue-endpoints).", + version: "0.0.1", + type: "action", + props: { + app, + appId: { + propDefinition: [ + app, + "appId", + ], + }, + requestId: { + propDefinition: [ + app, + "requestId", + ], + }, + }, + async run({ $ }) { + const { + app, + appId, + requestId, + } = this; + + const response = await app.getRequestResponse({ + $, + appId, + requestId, + }); + + $.export("$summary", "Successfully retrieved the request response."); + return response; + }, +}; diff --git a/components/fal_ai/actions/get-request-status/get-request-status.mjs b/components/fal_ai/actions/get-request-status/get-request-status.mjs new file mode 100644 index 0000000000000..9a96f44b7c775 --- /dev/null +++ b/components/fal_ai/actions/get-request-status/get-request-status.mjs @@ -0,0 +1,53 @@ +import app from "../../fal_ai.app.mjs"; + +export default { + key: "fal_ai-get-request-status", + name: "Get Request Status", + description: "Gets the status of a request in the queue. This allows you to monitor the progress of your asynchronous tasks. [See the documentation](https://fal.ai/docs/model-endpoints/queue#queue-endpoints).", + version: "0.0.1", + type: "action", + props: { + app, + appId: { + propDefinition: [ + app, + "appId", + ], + }, + requestId: { + propDefinition: [ + app, + "requestId", + ], + }, + logs: { + propDefinition: [ + app, + "logs", + ], + }, + }, + async run({ $ }) { + const { + app, + appId, + requestId, + logs, + } = this; + + const response = await app.getRequestStatus({ + $, + appId, + requestId, + params: { + logs: logs + ? 1 + : undefined, + }, + }); + + $.export("$summary", `Successfully retrieved status as \`${response.status}\`.`); + + return response; + }, +}; diff --git a/components/fal_ai/fal_ai.app.mjs b/components/fal_ai/fal_ai.app.mjs new file mode 100644 index 0000000000000..b097f098ed170 --- /dev/null +++ b/components/fal_ai/fal_ai.app.mjs @@ -0,0 +1,73 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "fal_ai", + propDefinitions: { + appId: { + type: "string", + label: "App ID", + description: "The unique identifier for the app. Eg. `lora`.", + }, + requestId: { + type: "string", + label: "Request ID", + description: "The unique identifier for the request.", + }, + logs: { + type: "boolean", + label: "Enable Logs", + description: "Specify if logs should be enabled for the request status.", + optional: true, + }, + }, + methods: { + getUrl(path) { + return `https://queue.fal.run/fal-ai${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Authorization": `Key ${this.$auth.api_key}`, + "Content-Type": "application/json", + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + put(args = {}) { + return this._makeRequest({ + method: "PUT", + ...args, + }); + }, + getRequestStatus({ + appId, requestId, ...args + } = {}) { + return this._makeRequest({ + path: `/${appId}/requests/${requestId}/status`, + ...args, + }); + }, + getRequestResponse({ + appId, requestId, ...args + } = {}) { + return this._makeRequest({ + path: `/${appId}/requests/${requestId}`, + ...args, + }); + }, + }, +}; diff --git a/components/fal_ai/package.json b/components/fal_ai/package.json new file mode 100644 index 0000000000000..d0a3a59b08d02 --- /dev/null +++ b/components/fal_ai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/fal_ai", + "version": "0.1.0", + "description": "Pipedream fal.ai Components", + "main": "fal_ai.app.mjs", + "keywords": [ + "pipedream", + "fal_ai" + ], + "homepage": "https://pipedream.com/apps/fal_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" + } +} diff --git a/components/faraday/README.md b/components/faraday/README.md new file mode 100644 index 0000000000000..24b0498ddba6a --- /dev/null +++ b/components/faraday/README.md @@ -0,0 +1,11 @@ +# Overview + +The Faraday API empowers users to harness rich consumer data and predictive analytics for personalized marketing and strategic insights. On Pipedream, you can tap into this potential by creating workflows that trigger based on various events or schedules, process data with Faraday's capabilities, and then take actions – all without managing server infrastructure. Streamline customer interactions, forecast trends, and enrich contact lists by integrating other services for a seamless data-driven ecosystem. + +# Example Use Cases + +- **Customer Segmentation Workflow**: Automate the segmentation of customers using Faraday's predictive models. Whenever a new user signs up on your platform, trigger a Pipedream workflow that sends the user's data to Faraday for analysis. Then, based on the segmentation results, apply tags to the user profile in your CRM like Salesforce or HubSpot, enabling personalized marketing campaigns. + +- **Lead Scoring and Prioritization**: As leads come in through forms or landing pages, use Pipedream to pass lead information to the Faraday API to score and prioritize leads based on their likelihood to convert. After scoring, update the lead status in your sales tools, like Close or Pipedream's SQL service, to ensure sales teams focus on the most promising prospects first. + +- **Real Estate Market Analysis**: For real estate platforms, leverage Faraday to analyze market trends and predict property values. Set up a Pipedream workflow that periodically pulls property data, sends it to Faraday for prediction, and then posts the insights to a Slack channel or updates a Google Sheet. This can inform investment strategies and provide real-time market insights to clients and stakeholders. diff --git a/components/faros/README.md b/components/faros/README.md index 830b6dffe7c51..3b659320d1f7e 100644 --- a/components/faros/README.md +++ b/components/faros/README.md @@ -1,15 +1,11 @@ # Overview -With Faros, you can build applications that can understand and respond to user -questions in natural language. These applications can range from chatbots to -virtual assistants and beyond. Some examples of what you can build with Faros -include: - -- A chatbot that can understand and respond to questions about your products or - services -- A virtual assistant that can help users with tasks such as scheduling - appointments or setting reminders -- An intelligent search engine that can understand and respond to user queries - in natural language -- A natural language processing tool that can analyze text data to extract - information or insights +The Faros API opens a gateway to streamline data from engineering tools into analytics-ready formats. With it, you can automate the aggregation of data from project management, source control, CI/CD, and incident management systems into Faros's unified data model. It's about turning disparate data points into actionable insights, which can lead to more informed decision-making and streamlined workflows. + +# Example Use Cases + +- **DevOps Pipeline Monitoring**: Automate the process of tracking commits, builds, and deployments by piping data from GitHub, Jenkins, and AWS CodeDeploy (or similar services) into Faros. Gain real-time visibility into your pipeline's health and performance metrics to quickly identify and address bottlenecks. + +- **Sprint Progress Tracking**: Connect Faros with project management tools like Jira. Set up a workflow that automatically fetches sprint activity and task statuses, feeding them into Faros for enhanced tracking of team velocity and sprint completion rates, aiding in agile project management. + +- **Incident Response Analysis**: Integrate Faros with incident management platforms such as PagerDuty. Create a workflow to channel alerts, incident responses, and post-mortem data into Faros, providing a comprehensive view of incident impact and response effectiveness for continuous operational improvement. diff --git a/components/faros/package.json b/components/faros/package.json new file mode 100644 index 0000000000000..68e55746456f8 --- /dev/null +++ b/components/faros/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/faros", + "version": "0.6.0", + "description": "Pipedream faros Components", + "main": "faros.app.mjs", + "keywords": [ + "pipedream", + "faros" + ], + "homepage": "https://pipedream.com/apps/faros", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/fastfield_mobile_forms/README.md b/components/fastfield_mobile_forms/README.md new file mode 100644 index 0000000000000..784e5dae27ab8 --- /dev/null +++ b/components/fastfield_mobile_forms/README.md @@ -0,0 +1,11 @@ +# Overview + +The FastField Mobile Forms API allows you to automate interactions with your mobile forms. Using this API on Pipedream, you can trigger workflows based on form submissions, fetch and push data to and from your forms, and integrate with other services to streamline data processing. It’s ideal for creating powerful, serverless workflows that react to form data in real-time, update databases, send notifications, or even generate reports. + +# Example Use Cases + +- **Automated Data Entry into CRM**: When a new form submission is received, use the FastField API to extract relevant data and create a new lead or contact in your CRM system, like Salesforce or HubSpot. This reduces manual data entry and ensures your customer information is always up-to-date. + +- **Real-time Notifications**: Set up a workflow that sends an instant notification via email or a messaging app like Slack whenever a form is submitted. This keeps your team in the loop and allows for quick response to new entries, which is especially useful for time-sensitive data such as support tickets or service requests. + +- **Data Analysis and Reporting**: Capture form submissions and pass them to a data analysis tool like Google Sheets or Tableau. Analyze trends and generate visual reports to gain insights into your collected data. This automated workflow can help in making informed decisions based on user feedback or survey results. diff --git a/components/fastfield_mobile_forms/package.json b/components/fastfield_mobile_forms/package.json index 8ddfff85f7acd..cf93014f5b0c5 100644 --- a/components/fastfield_mobile_forms/package.json +++ b/components/fastfield_mobile_forms/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/fatture_in_cloud/README.md b/components/fatture_in_cloud/README.md new file mode 100644 index 0000000000000..8191a3956e5f2 --- /dev/null +++ b/components/fatture_in_cloud/README.md @@ -0,0 +1,11 @@ +# Overview + +Fatture in Cloud API offers comprehensive tools for managing invoices, clients, suppliers, and accounting records all via cloud-based operations. Utilizing this API on Pipedream allows you to seamlessly automate billing processes, synchronize financial data across various platforms, and trigger actions based on invoice activities. This integration can save time, reduce errors, and provide real-time insights into financial operations. + +# Example Use Cases + +- **Automated Invoice Generation and Emailing**: Automatically generate invoices when new orders are received on e-commerce platforms like Shopify or WooCommerce. Use Pipedream to connect Fatture in Cloud with these platforms, capture order details, generate an invoice, and then email it directly to the customer using a service like SendGrid or Mailgun. + +- **Expense Tracking and Notification**: Set up a workflow where expenses submitted through platforms like Expensify or receipts scanned via an app trigger entry creation in Fatture in Cloud. Configure additional actions to notify finance teams via Slack or email, ensuring timely updates and approvals. + +- **Synchronize Financial Records with Accounting Software**: Integrate Fatture in Cloud with accounting software like QuickBooks or Xero. Whenever a new invoice is created or updated in Fatture in Cloud, sync this data to the accounting software to keep financial records consistent and up-to-date across all platforms. diff --git a/components/fatture_in_cloud/fatture_in_cloud.app.mjs b/components/fatture_in_cloud/fatture_in_cloud.app.mjs new file mode 100644 index 0000000000000..c63a29d2422fa --- /dev/null +++ b/components/fatture_in_cloud/fatture_in_cloud.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "fatture_in_cloud", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/fatture_in_cloud/package.json b/components/fatture_in_cloud/package.json new file mode 100644 index 0000000000000..5910cb283766b --- /dev/null +++ b/components/fatture_in_cloud/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/fatture_in_cloud", + "version": "0.0.1", + "description": "Pipedream Fatture in Cloud Components", + "main": "fatture_in_cloud.app.mjs", + "keywords": [ + "pipedream", + "fatture_in_cloud" + ], + "homepage": "https://pipedream.com/apps/fatture_in_cloud", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/faunadb/README.md b/components/faunadb/README.md index 2e54f0db71e4e..0480de4a71aab 100644 --- a/components/faunadb/README.md +++ b/components/faunadb/README.md @@ -1,3 +1,16 @@ +# Overview + +Fauna API offers a powerful serverless database solution for modern applications. Its unique capabilities allow for highly scalable, secure, and flexible data management. With Pipedream, you can harness the power of Fauna to create intricate serverless workflows that react to various triggers, manage data efficiently, and connect seamlessly with other services and APIs to automate complex tasks. + +# Example Use Cases + +- **Scheduled Data Backups to Cloud Storage**: Use Pipedream's scheduled triggers to run a workflow that extracts data from your Fauna database and pushes it to cloud storage solutions like AWS S3 or Google Cloud Storage. This workflow can be set to run at regular intervals, ensuring your backups are always up-to-date. + +- **Real-time Data Sync with Third-Party Services**: Integrate Fauna with other apps to keep data in sync across platforms. For instance, you could create a workflow that watches for updates in your Fauna database and reflects those changes in a Shopify store, syncing inventory levels or product details automatically. + +- **Automated Email Notifications on Data Events**: Set up a workflow that sends out email notifications using a service like SendGrid whenever specific data changes occur in your Fauna database. For example, you might notify a user when their order status changes or when a new piece of content is available that fits their preferences. + + # FaunaDB Event Sources FaunaDB Event Sources collect data from FaunaDB (for example, changes to a collection) and emits them as individual events. These events can trigger [Pipedream workflows](https://docs.pipedream.com/workflows/), and are accessible as a [real-time, private SSE stream](https://docs.pipedream.com/api/sse/), and via [REST API](https://docs.pipedream.com/api/rest/). diff --git a/components/favro/.gitignore b/components/favro/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/favro/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/favro/README.md b/components/favro/README.md new file mode 100644 index 0000000000000..6aedb0154f3a5 --- /dev/null +++ b/components/favro/README.md @@ -0,0 +1,11 @@ +# Overview + +The Favro API gives you the power to automate and integrate your project management tools with a host of other services. With Favro and Pipedream, you can create custom workflows to synchronize tasks, update cards, manage boards, and collaborate more effectively across your organization. Whether you're syncing with a CRM, triggering notifications, or generating reports, Pipedream's serverless platform makes it simple to connect Favro with other apps to streamline your processes. + +# Example Use Cases + +- **Sync Favro Cards with Google Calendar Events**: When a new card is created in Favro, automatically create a corresponding event in Google Calendar. This ensures that tasks and deadlines from Favro are reflected in your schedule, keeping everyone on track. + +- **Post Slack Notifications for Favro Card Updates**: Set up a workflow where updates to cards in Favro trigger notifications in a specific Slack channel. This keeps your team informed in real-time about task progress or changes, fostering better communication and collaboration. + +- **Create Favro Cards from Email**: Automate the creation of Favro cards from incoming emails. For example, when an email tagged with a specific label arrives in Gmail, a new card is created in a designated Favro board, converting customer feedback or inquiries into actionable tasks. diff --git a/components/favro/actions/create-organization/create-organization.mjs b/components/favro/actions/create-organization/create-organization.mjs new file mode 100644 index 0000000000000..be5919da3f584 --- /dev/null +++ b/components/favro/actions/create-organization/create-organization.mjs @@ -0,0 +1,60 @@ +import app from "../../favro.app.mjs"; + +export default { + key: "favro-create-organization", + name: "Create Organization", + description: "Creates a new organization. [See the documentation](https://favro.com/developer/#create-an-organization)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "name", + ], + }, + organizationId: { + propDefinition: [ + app, + "organizationId", + ], + description: "Organization of the user that will be associated with the new organization", + + }, + userId: { + propDefinition: [ + app, + "userId", + (c) => ({ + organizationId: c.organizationId, + }), + ], + }, + role: { + propDefinition: [ + app, + "role", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.createOrganization({ + $, + data: { + name: this.name, + shareToUsers: [ + { + userId: this.userId, + role: this.role, + }, + ], + }, + }); + + $.export("$summary", `Successfully created organization named ${this.name}`); + + return response; + }, +}; diff --git a/components/favro/actions/list-users/list-users.mjs b/components/favro/actions/list-users/list-users.mjs new file mode 100644 index 0000000000000..103996c652638 --- /dev/null +++ b/components/favro/actions/list-users/list-users.mjs @@ -0,0 +1,30 @@ +import app from "../../favro.app.mjs"; + +export default { + key: "favro-list-users", + name: "List Users", + description: "List all users in the organization. [See the documentation](https://favro.com/developer/#get-all-users)", + + version: "0.0.1", + type: "action", + props: { + app, + organizationId: { + propDefinition: [ + app, + "organizationId", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.listUsers({ + $, + organizationId: this.organizationId, + }); + + $.export("$summary", `Successfully retrieved ${response.entities.length} user(s)`); + + return response; + }, +}; diff --git a/components/favro/actions/update-organization/update-organization.mjs b/components/favro/actions/update-organization/update-organization.mjs new file mode 100644 index 0000000000000..bc6e6d133310d --- /dev/null +++ b/components/favro/actions/update-organization/update-organization.mjs @@ -0,0 +1,62 @@ +import app from "../../favro.app.mjs"; + +export default { + key: "favro-update-organization", + name: "Update Organization", + description: "Updates an existing organization. [See the documentation](https://favro.com/developer/#update-an-organization)", + version: "0.0.1", + type: "action", + props: { + app, + organizationId: { + propDefinition: [ + app, + "organizationId", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + userId: { + propDefinition: [ + app, + "userId", + (c) => ({ + organizationId: c.organizationId, + }), + ], + }, + role: { + propDefinition: [ + app, + "role", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.updateOrganization({ + $, + organizationId: this.organizationId, + headers: { + organizationId: this.organizationId, + }, + data: { + name: this.name, + shareToUsers: [ + { + userId: this.userId, + role: this.role, + }, + ], + }, + }); + + $.export("$summary", `Successfully updated organization with ID '${this.organizationId}'`); + + return response; + }, +}; diff --git a/components/favro/app/favro.app.ts b/components/favro/app/favro.app.ts deleted file mode 100644 index 846855ee2634d..0000000000000 --- a/components/favro/app/favro.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "favro", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/favro/common/constants.mjs b/components/favro/common/constants.mjs new file mode 100644 index 0000000000000..dff41c019d42c --- /dev/null +++ b/components/favro/common/constants.mjs @@ -0,0 +1,9 @@ +export default { + USER_ROLES: [ + "administrator", + "fullMember", + "externalMember", + "guest", + "disabled", + ], +}; diff --git a/components/favro/favro.app.mjs b/components/favro/favro.app.mjs new file mode 100644 index 0000000000000..3eb971fc242e4 --- /dev/null +++ b/components/favro/favro.app.mjs @@ -0,0 +1,105 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "favro", + propDefinitions: { + organizationId: { + type: "string", + label: "Organization ID", + description: "ID of the organization", + async options() { + const response = await this.listOrganizations(); + const usersIds = response.entities; + return usersIds.map(({ + organizationId, name, + }) => ({ + value: organizationId, + label: name, + })); + }, + }, + userId: { + type: "string", + label: "User ID", + description: "ID of the user", + async options({ organizationId }) { + const response = await this.listUsers({ + organizationId, + }); + const usersIds = response.entities; + return usersIds.map(({ + userId, name, + }) => ({ + value: userId, + label: name, + })); + }, + }, + name: { + type: "string", + label: "Name", + description: "Name of the organization", + }, + role: { + type: "string", + label: "Role", + description: "Role of the user", + options: constants.USER_ROLES, + }, + }, + methods: { + _baseUrl() { + return "https://favro.com/api"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + auth: { + username: `${this.$auth.username}`, + password: `${this.$auth.api_token}`, + }, + }); + }, + async createOrganization(args = {}) { + return this._makeRequest({ + path: "/v1/organizations", + method: "post", + ...args, + }); + }, + async updateOrganization({ + organizationId, ...args + }) { + return this._makeRequest({ + path: `/v1/organizations/${organizationId}`, + method: "put", + ...args, + }); + }, + async listOrganizations(args = {}) { + return this._makeRequest({ + path: "/v1/organizations", + ...args, + }); + }, + async listUsers({ + organizationId, ...args + }) { + return this._makeRequest({ + path: "/v1/users", + headers: { + organizationId, + }, + ...args, + }); + }, + }, +}; diff --git a/components/favro/package.json b/components/favro/package.json index 0e206fe4f5a70..ccab17b046f92 100644 --- a/components/favro/package.json +++ b/components/favro/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/favro", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream Favro Components", - "main": "dist/app/favro.app.mjs", + "main": "favro.app.mjs", "keywords": [ "pipedream", "favro" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/favro", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/feathery/README.md b/components/feathery/README.md index b35f91c892880..27f020a9524f0 100644 --- a/components/feathery/README.md +++ b/components/feathery/README.md @@ -1,11 +1,11 @@ # Overview -With Feathery, you can build: - -- A social network for connecting with friends and family -- A task manager to keep track of your to-do list -- A note taking app to jot down ideas and thoughts -- A photo sharing app to share moments with your friends -- A weather app to track the forecast in your area -- A messaging app to chat with your friends in real-time -- A game to pass the time and have fun +Feathery is a powerful form builder designed to create polished, dynamic forms with ease. With the Feathery API, you can automate form submissions, synchronize data across platforms, and trigger custom workflows based on user interactions. This seamless integration on Pipedream lets you bridge Feathery forms with countless other services to streamline data collection, lead capture, and user surveys, enhancing the overall efficiency of data-driven processes. + +# Example Use Cases + +- **Form Submission to CRM**: Automatically create or update contacts in your CRM like Salesforce or HubSpot when a form is submitted on Feathery. This ensures your sales team has the latest lead info at their fingertips. + +- **Survey Data to Analytics Tools**: Feed survey responses from Feathery into analytics tools such as Google Sheets or Tableau. Analyze user feedback or poll results in real-time to make data-driven decisions swiftly. + +- **Multi-step Approval Workflows**: When a form is submitted, trigger a multi-step approval process. Notify team members on Slack or Microsoft Teams, and use conditional logic to approve or reject submissions, updating the form status on Feathery accordingly. diff --git a/components/feathery/package.json b/components/feathery/package.json new file mode 100644 index 0000000000000..f1bcd1017240e --- /dev/null +++ b/components/feathery/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/feathery", + "version": "0.6.0", + "description": "Pipedream feathery Components", + "main": "feathery.app.mjs", + "keywords": [ + "pipedream", + "feathery" + ], + "homepage": "https://pipedream.com/apps/feathery", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/fedex/README.md b/components/fedex/README.md index b04b3a525b884..3690009b3097d 100644 --- a/components/fedex/README.md +++ b/components/fedex/README.md @@ -1,13 +1,11 @@ # Overview -With the FedEx API, you can build a variety of applications and integrations -including: - -- A shipping estimation tool that provides real-time pricing for different - shipping options -- A tracking tool that provides up-to-date information on the status of your - shipments -- An automated shipping notification system that keeps your customers updated - on the status of their orders -- A package pickup and delivery tool that allows your customers to schedule - package pickups and deliveries with FedEx +The FedEx API provides a direct line to FedEx's shipping, tracking, and rate services, allowing you to automate the logistics within your business processes. Whether you're automating notifications based on shipment statuses, streamlining the creation of shipping labels, or integrating real-time shipping rates into your checkout process, this API offers a wealth of possibilities to enhance efficiency and customer satisfaction. + +# Example Use Cases + +- **Shipping Label Generation on Order Placement**: Automate the creation of shipping labels immediately after a customer places an order. When a new order is received in your eCommerce platform (like Shopify), Pipedream can trigger a workflow that utilizes the FedEx API to generate a shipping label and then email it to the customer or store it in a cloud storage service for easy access. + +- **Real-time Shipment Tracking for Customer Support**: Build a system that provides real-time tracking updates to your support team. Link FedEx API with a CRM like Zendesk on Pipedream, so whenever a customer inquires about their order, the support team can instantly fetch the latest tracking information to provide timely and accurate updates. + +- **Dynamic Shipping Rates at Checkout**: Integrate dynamic shipping rates into your website's checkout process. By connecting the FedEx API with your website backend through Pipedream, you can automatically calculate and display real-time shipping costs based on the customer's location and order size, ensuring transparency and preventing checkout abandonment. diff --git a/components/feedbin/README.md b/components/feedbin/README.md index 96a0fe0fc84fa..b702f03b91472 100644 --- a/components/feedbin/README.md +++ b/components/feedbin/README.md @@ -1,8 +1,17 @@ # Overview -With the Feedbin API, you can build a number of things, including: +The Feedbin API provides an interface to manage and interact with a user's RSS feed subscriptions and articles. By integrating with Pipedream, you can leverage this API to create automation workflows that enhance your reading experience, keep you informed, and integrate your favorite feeds with other services. With Pipedream's serverless platform, you can set up triggers based on new articles, organize content, and synchronize your Feedbin data across multiple applications. -- A RSS feed reader -- A news aggregator -- A podcast manager -- A research tool +# Example Use Cases + +**Automated Article Digest Email** + +- Gather new articles from your Feedbin subscriptions using a Pipedream trigger. Combine these articles into a digest and send it daily or weekly via an email service like SendGrid. This can keep you updated with your favorite content in a consolidated manner. + +**Content Sharing Workflow** + +- Whenever a new article is starred in Feedbin, trigger a Pipedream workflow to share the article on social media platforms such as Twitter or LinkedIn. This allows for seamless sharing of interesting reads with your followers. + +**Sync Read Articles to Notion** + +- Use Pipedream to monitor articles you've marked as read in Feedbin. Automatically create a corresponding entry in a Notion database for each read article. This is useful for keeping a personal archive of content you've engaged with, along with any notes or summaries you may want to add. diff --git a/components/feedbin/package-lock.json b/components/feedbin/package-lock.json index 4ab35f05e6f39..e238fc84a7950 100644 --- a/components/feedbin/package-lock.json +++ b/components/feedbin/package-lock.json @@ -57,9 +57,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -176,9 +176,9 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" }, "form-data": { "version": "4.0.0", diff --git a/components/feedblitz/README.md b/components/feedblitz/README.md index e483c22dd6d7c..635235f5a507e 100644 --- a/components/feedblitz/README.md +++ b/components/feedblitz/README.md @@ -1,9 +1,11 @@ # Overview -The FeedBlitz API allows developers to access the FeedBlitz service and build -various integrations and applications on top of it. With the FeedBlitz API, you -can: +FeedBlitz is an email marketing service that enables creators to manage newsletters, automated mailings, and RSS feed campaigns. By connecting the FeedBlitz API with Pipedream, you can automate content distribution, subscriber management, and engagement tracking, creating a seamless workflow for your email marketing efforts. This API leverages Pipedream's capabilities to integrate with numerous other services, allowing you to enhance your email campaigns with dynamic content, subscriber behavior data, and event-driven triggers. -- Automate the creation and management of FeedBlitz feeds -- Import and export data from FeedBlitz -- Build custom applications on top of FeedBlitz +# Example Use Cases + +- **Automated Content Digests**: Build a workflow where FeedBlitz automatically sends out a weekly email digest to subscribers by pulling the latest content from your blog's RSS feed. Pipedream can schedule this task and enrich the content by pulling in additional data from other services like social media analytics for a personalized touch. + +- **Subscriber Engagement Tracking**: Create a pipeline that tracks subscriber interactions with your emails. Use FeedBlitz to capture opens, clicks, and forwards, then send this data to a Google Sheets spreadsheet via Pipedream. This workflow can help you analyze engagement patterns and tailor content to boost your email marketing performance. + +- **Lead Generation Response**: Set up an instant notification system for when someone subscribes to your newsletter through FeedBlitz. Use Pipedream to connect with Slack, sending a real-time alert to your sales or marketing channel. This immediate response can kick-start the lead nurturing process without delay. diff --git a/components/feedblitz/package.json b/components/feedblitz/package.json new file mode 100644 index 0000000000000..d7f746340bec2 --- /dev/null +++ b/components/feedblitz/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/feedblitz", + "version": "0.6.0", + "description": "Pipedream feedblitz Components", + "main": "feedblitz.app.mjs", + "keywords": [ + "pipedream", + "feedblitz" + ], + "homepage": "https://pipedream.com/apps/feedblitz", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/feedier/README.md b/components/feedier/README.md index 51fde94bfad17..bb2956d51f093 100644 --- a/components/feedier/README.md +++ b/components/feedier/README.md @@ -1,10 +1,11 @@ # Overview -Using the Feedier API, you can build a variety of applications that allow users -to collect feedback from customers and track the results. Here are a few -examples: - -- A feedback form for website visitors -- A customer satisfaction survey -- A poll or quiz for website visitors -- A tool for gathering user feedback on new features or products +The [Feedier API](https://feedier.com) lets you harness the power of customer feedback by automating the collection and analysis process. With this API, you can create, retrieve, update, and delete feedback, along with managing carriers and rewards. It enables you to streamline the feedback loop, integrate with your CRM, and trigger actions based on customer responses. Pipedream's serverless platform opens up a world of possibilities for integrating Feedier with hundreds of other apps to automate workflows, analyze data, and respond in real-time to customer insights. + +# Example Use Cases + +- **Automated Feedback Collection to CRM**: Trigger a Pipedream workflow when a new feedback is submitted in Feedier. Use this data to create or update a customer profile in a CRM like Salesforce, ensuring that the latest customer feedback is always at your sales team's fingertips. + +- **Dynamic Reward Distribution**: Set up a Pipedream workflow that listens for high scores in feedback submissions. When a customer gives a high rating, automatically send them a reward or discount code via email using a service like SendGrid, to thank them for their input and encourage loyalty. + +- **Sentiment Analysis for Real-time Alerts**: Use Pipedream to pipe new feedback from Feedier into a sentiment analysis tool like MonkeyLearn. If negative sentiment is detected, trigger an alert in a team communication app like Slack or Microsoft Teams to prompt immediate action from customer support. diff --git a/components/feedier/package.json b/components/feedier/package.json new file mode 100644 index 0000000000000..d29fe8a196f7f --- /dev/null +++ b/components/feedier/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/feedier", + "version": "0.6.0", + "description": "Pipedream feedier Components", + "main": "feedier.app.mjs", + "keywords": [ + "pipedream", + "feedier" + ], + "homepage": "https://pipedream.com/apps/feedier", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/felt/felt.app.mjs b/components/felt/felt.app.mjs new file mode 100644 index 0000000000000..ddda6c1b94243 --- /dev/null +++ b/components/felt/felt.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "felt", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/felt/package.json b/components/felt/package.json new file mode 100644 index 0000000000000..197dabea501f3 --- /dev/null +++ b/components/felt/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/felt", + "version": "0.0.1", + "description": "Pipedream Felt Components", + "main": "felt.app.mjs", + "keywords": [ + "pipedream", + "felt" + ], + "homepage": "https://pipedream.com/apps/felt", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/fibery/README.md b/components/fibery/README.md index 3710f20d84418..b141fbb8905fd 100644 --- a/components/fibery/README.md +++ b/components/fibery/README.md @@ -1,11 +1,11 @@ # Overview -With Fibery, you can build: - -- A task management system -- A project management system -- A customer relationship management (CRM) system -- A knowledge management system -- A content management system (CMS) -- An e-commerce platform -- A data management system +Fibery is a versatile work management platform, and its API amplifies this versatility within Pipedream's environment. Leveraging the Fibery API on Pipedream, you can automate complex workflows that span across project management, product development, and collaborative functions. This includes actions like syncing issues across platforms, aggregating feedback into product roadmaps, or updating project timelines based on external triggers. With Pipedream, you can listen for webhooks, schedule tasks, and seamlessly connect Fibery with other apps to create a dynamic, interconnected workspace. + +# Example Use Cases + +- **Sync Issues with GitHub**: Automatically create or update Fibery entities when new GitHub issues are opened or updated. This keeps development tasks in sync, allowing teams using Fibery for project tracking to stay up-to-date with real-time changes in the code repository. + +- **Aggregate Customer Feedback**: Collect feedback from various sources like Zendesk, Intercom, or email, and create Fibery entities to track and prioritize customer input. This workflow enables product teams to integrate user feedback directly into their planning and development processes in Fibery. + +- **Content Calendar Automation**: When a new blog post is published in WordPress, trigger an update in Fibery to mark the content piece as 'Published' in the editorial calendar. This ensures real-time content tracking and aligns marketing efforts with actual content deployments. diff --git a/components/fidel_api/README.md b/components/fidel_api/README.md new file mode 100644 index 0000000000000..d9a8733097b4b --- /dev/null +++ b/components/fidel_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Fidel API allows you to link credit and debit cards to specific offers or programs, track transactions in real-time, and receive event-driven data. It's a powerful tool for creating loyalty and reward programs, providing insights into customer spending patterns, and automating financial operations. With Pipedream, you can craft workflows that leverage Fidel API's capabilities to trigger actions, sync data, and connect with other apps for enhanced automation. + +# Example Use Cases + +- **Transaction-Based Rewards Notifications**: When a customer makes a qualifying purchase with a linked card, you can use Fidel API with Pipedream to trigger an email or SMS, via SendGrid or Twilio, notifying them of the rewards they've earned. This keeps customers engaged and encourages repeat business. + +- **Real-Time Spending Dashboards**: Sync transaction data from Fidel API to Google Sheets or a database like PostgreSQL on every new transaction. Use this data to power real-time dashboards that monitor spending patterns, reward redemptions, and program effectiveness. + +- **Automated Expense Tracking**: For each transaction made on a company card, you could trigger a workflow that classifies the expense, creates a record in an accounting app like QuickBooks, and notifies the finance team via Slack or email. This streamlines expense management and ensures timely record-keeping. diff --git a/components/figma/README.md b/components/figma/README.md index a68f07b908db5..51dddc239875d 100644 --- a/components/figma/README.md +++ b/components/figma/README.md @@ -1,8 +1,11 @@ # Overview -With the Figma API you can: +The Figma API unlocks the power to automate and integrate design workflows, enabling both designers and developers to extract assets, update designs, and manage files programmatically. By leveraging the Figma API on Pipedream, you can create automated processes that sync design updates with other tools, notify team members of changes, or feed design information into other parts of your digital ecosystem. -- Build interactive prototypes -- Share shorcuts and snippets with the community -- Create and manage your design system -- And much more! +# Example Use Cases + +- **Design Sync to Cloud Storage**: Automatically upload new versions of design files from Figma to a cloud storage provider like Google Drive or Dropbox when a file is updated. Keep your assets backed up and accessible to team members who don't use Figma. + +- **Project Management Updates**: Trigger an update in your project management tool, such as Trello or Asana, when a Figma design file changes status. Ensure that project timelines and tasks are in sync with the design progress. + +- **Slack Notifications on Comments**: Send a message to a designated Slack channel whenever a new comment is added to a Figma file. Keep your team informed and quickly actionable on feedback without leaving their communication hub. diff --git a/components/figma/actions/delete-comment/delete-comment.mjs b/components/figma/actions/delete-comment/delete-comment.mjs index 8ec965f4e468e..570c6b95dbf92 100644 --- a/components/figma/actions/delete-comment/delete-comment.mjs +++ b/components/figma/actions/delete-comment/delete-comment.mjs @@ -1,19 +1,15 @@ -import figmaApp from "../../figma.app.mjs"; +import common from "../../common/common.mjs"; +const { figmaApp } = common.props; export default { + ...common, name: "Delete a Comment", description: "Delete a comment to a file. [See the docs here](https://www.figma.com/developers/api#delete-comments-endpoint)", key: "figma-delete-comment", - version: "0.0.1", + version: "0.0.3", type: "action", props: { - figmaApp, - teamId: { - propDefinition: [ - figmaApp, - "teamId", - ], - }, + ...common.props, projectId: { propDefinition: [ figmaApp, diff --git a/components/figma/actions/list-comments/list-comments.mjs b/components/figma/actions/list-comments/list-comments.mjs index 03ff4681cc715..17b4824579d3e 100644 --- a/components/figma/actions/list-comments/list-comments.mjs +++ b/components/figma/actions/list-comments/list-comments.mjs @@ -1,19 +1,15 @@ -import figmaApp from "../../figma.app.mjs"; +import common from "../../common/common.mjs"; +const { figmaApp } = common.props; export default { + ...common, name: "List Comments", description: "Lists all comments left on a file. [See the docs here](https://www.figma.com/developers/api#get-comments-endpoint)", key: "figma-list-comments", - version: "0.0.1", + version: "0.0.3", type: "action", props: { - figmaApp, - teamId: { - propDefinition: [ - figmaApp, - "teamId", - ], - }, + ...common.props, projectId: { propDefinition: [ figmaApp, diff --git a/components/figma/actions/post-a-comment/post-a-comment.mjs b/components/figma/actions/post-a-comment/post-a-comment.mjs index f28ef91ae571a..f5bc5150d858d 100644 --- a/components/figma/actions/post-a-comment/post-a-comment.mjs +++ b/components/figma/actions/post-a-comment/post-a-comment.mjs @@ -1,19 +1,15 @@ -import figmaApp from "../../figma.app.mjs"; +import common from "../../common/common.mjs"; +const { figmaApp } = common.props; export default { + ...common, name: "Post a Comment", description: "Posts a comment to a file. [See the docs here](https://www.figma.com/developers/api#post-comments-endpoint)", key: "figma-post-a-comment", - version: "0.0.1", + version: "0.0.3", type: "action", props: { - figmaApp, - teamId: { - propDefinition: [ - figmaApp, - "teamId", - ], - }, + ...common.props, projectId: { propDefinition: [ figmaApp, diff --git a/components/figma/common/common.mjs b/components/figma/common/common.mjs new file mode 100644 index 0000000000000..bf5f8ec44e794 --- /dev/null +++ b/components/figma/common/common.mjs @@ -0,0 +1,28 @@ +import figma from "../figma.app.mjs"; + +export default { + props: { + figmaApp: { + ...figma, + reloadProps: true, + }, + }, + methods: { + getTeamId() { + return this.figmaApp._getTeamId() ?? this.teamId; + }, + }, + additionalProps() { + const teamId = this.getTeamId(); + return teamId + ? {} + : { + teamId: { + propDefinition: [ + figma, + "teamId", + ], + }, + }; + }, +}; diff --git a/components/figma/figma.app.mjs b/components/figma/figma.app.mjs index 055d722a87fac..8e954d1be2bf2 100644 --- a/components/figma/figma.app.mjs +++ b/components/figma/figma.app.mjs @@ -7,13 +7,13 @@ export default { teamId: { type: "string", label: "Team Id", - description: "Your team Id. It is not currently possible to programmatically obtain the team id of a user just from a token. To obtain a team id, navigate to a team page of a team you are a part of. The team id will be present in the URL after the word team and before your team name.", + description: "Navigate to a team page of a team which you are a part of. You can find your Team ID between `/team` and your team name. If your team URL is `https://www.figma.com/files/team/1227693318965186187/Test's-team?fuid=234562`, enter `1227693318965186187` here.", }, projectId: { type: "string", label: "Project Id", description: "Id of the project to list files from", - async options({ teamId }) { + async options({ teamId = this._getTeamId() }) { const projects = await this.listTeamProjects(teamId); return projects.map((item) => ({ label: item.name, @@ -63,6 +63,9 @@ export default { "Authorization": `Bearer ${this.$auth.oauth_access_token}`, }; }, + _getTeamId() { + return this.$auth.team_id; + }, _getAxiosParams(opts = {}) { const res = { ...opts, diff --git a/components/figma/package.json b/components/figma/package.json index 9f61b22e9084c..1c46b734616df 100644 --- a/components/figma/package.json +++ b/components/figma/package.json @@ -1,8 +1,8 @@ { "name": "@pipedream/figma", - "version": "0.0.3", + "version": "0.1.0", "description": "Pipedream Figma Components", - "main": "figma.app.js", + "main": "figma.app.mjs", "keywords": [ "pipedream", "figma" @@ -13,6 +13,6 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^0.10.0" + "@pipedream/platform": "^1.6.1" } } diff --git a/components/figma/sources/new-comment/new-comment.mjs b/components/figma/sources/new-comment/new-comment.mjs index 112313302f404..208e6d2bca37e 100644 --- a/components/figma/sources/new-comment/new-comment.mjs +++ b/components/figma/sources/new-comment/new-comment.mjs @@ -1,43 +1,52 @@ +import { ConfigurationError } from "@pipedream/platform"; import { v4 as uuid } from "uuid"; -import figma from "../../figma.app.mjs"; +import common from "../../common/common.mjs"; export default { + ...common, key: "figma-new-comment", name: "New Comment (Instant)", description: "Emit new event when someone comments on a file", - version: "0.0.1", + version: "0.0.3", type: "source", dedupe: "unique", props: { - figma, + alert: { + type: "alert", + alertType: "info", + content: "A Figma Organization Plan or higher is required to use webhooks. See Figma's [pricing page](https://www.figma.com/pricing) for more details.", + }, + ...common.props, http: "$.interface.http", db: "$.service.db", - teamId: { - propDefinition: [ - figma, - "teamId", - ], - }, }, hooks: { async activate() { - const passcode = uuid(); - this.setPasscode(passcode); + try { + const passcode = uuid(); + this.setPasscode(passcode); - const hookId = await this.figma.createHook( - "FILE_COMMENT", - this.teamId, - this.http.endpoint, - passcode, - ); + const hookId = await this.figmaApp.createHook( + "FILE_COMMENT", + this.getTeamId(), + this.http.endpoint, + passcode, + ); - this.setHookId(hookId); + this.setHookId(hookId); + } catch (error) { + if (error.response.status === 400) { + throw new ConfigurationError("A Figma Organization Plan or higher is required to use webhooks. See Figma's [pricing page](https://www.figma.com/pricing) for more details."); + } + throw error; + } }, async deactivate() { - await this.figma.deleteHook(this.getHookId()); + await this.figmaApp.deleteHook(this.getHookId()); }, }, methods: { + ...common.methods, getHookId() { return this.db.get("hookId"); }, @@ -57,7 +66,7 @@ export default { }); if (event.body.passcode === this.getPasscode()) { - this.$emit(event.body, { + this.$emit(event.body, { summary: `New comment on a file (${event.body.comment_id})`, id: event.body.comment_id, ts: event.body.timestamp, diff --git a/components/file_store/package.json b/components/file_store/package.json index 22ea2f380906e..ce27b1debf06d 100644 --- a/components/file_store/package.json +++ b/components/file_store/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/fileforge/actions/generate-pdf/generate-pdf.mjs b/components/fileforge/actions/generate-pdf/generate-pdf.mjs new file mode 100644 index 0000000000000..9294fc2d2798a --- /dev/null +++ b/components/fileforge/actions/generate-pdf/generate-pdf.mjs @@ -0,0 +1,88 @@ +import FormData from "form-data"; +import fs from "fs"; +import { + checkTmp, + parseObject, +} from "../../common/utils.mjs"; +import fileforge from "../../fileforge.app.mjs"; + +export default { + key: "fileforge-generate-pdf", + name: "Generate PDF", + description: "Generate a PDF from provided HTML. [See the documentation](https://docs.fileforge.com/api-reference/api-reference/pdf/generate)", + version: "0.0.1", + type: "action", + props: { + fileforge, + alert: { + type: "alert", + alertType: "warning", + content: `An **\`index.html\`** file is required, and will be used as the main document. + Other documents may also be attached, such as stylesheets or images. + The path in the **\`filename\`** part of the multipart attachement will be respected during generation. + **Important notice:** during generation, the **\`index.html\`** file will be processed to include the base URL of the document. + This is required for assets to be loaded correctly. + To link your assets from the HTML file, you should not use a leading slash in the URL. + For example, use **\`\`** instead of **\`\`**.`, + }, + files: { + type: "string[]", + label: "HTML Files", + description: "The HTML files to convert to PDF. Each file should be a valid path to an HTML file saved to the `/tmp` directory (e.g. `/tmp/image.png`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory)..", + }, + test: { + type: "boolean", + label: "Test", + description: "Generate a test document instead of a production document. The generated document will contain a watermark. Defaults to true.", + optional: true, + }, + expiresAt: { + type: "string", + label: "Expires At", + description: "The expiration timestamp for the PDF in ISO 8601 format", + optional: true, + }, + fileName: { + type: "string", + label: "Filename", + description: "The desired filename for the generated PDF.", + optional: true, + }, + allowViewing: { + type: "boolean", + label: "Allow Viewing", + description: "Specifies whether viewing is allowed.", + optional: true, + }, + }, + async run({ $ }) { + const { + fileforge, + files, + ...data + } = this; + + const formData = new FormData(); + const parsedFiles = parseObject(files); + + for (const file of parsedFiles) { + formData.append("files", fs.createReadStream(checkTmp(file))); + } + + formData.append("options", JSON.stringify({ + ...data, + host: true, + }), { + contentType: "application/json", + }); + + const response = await fileforge.generatePDF({ + $, + data: formData, + headers: formData.getHeaders(), + }); + + $.export("$summary", "Successfully generated the PDF file."); + return response; + }, +}; diff --git a/components/fileforge/common/utils.mjs b/components/fileforge/common/utils.mjs new file mode 100644 index 0000000000000..d1e7ed1a22d98 --- /dev/null +++ b/components/fileforge/common/utils.mjs @@ -0,0 +1,31 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; + +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; diff --git a/components/fileforge/fileforge.app.mjs b/components/fileforge/fileforge.app.mjs new file mode 100644 index 0000000000000..85ec8b5e6ae3b --- /dev/null +++ b/components/fileforge/fileforge.app.mjs @@ -0,0 +1,33 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "fileforge", + methods: { + _baseUrl() { + return "https://api.fileforge.com"; + }, + _headers(headers = {}) { + return { + "X-API-Key": this.$auth.api_key, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(headers), + ...opts, + }); + }, + generatePDF(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/pdf/generate/", + ...opts, + }); + }, + }, +}; diff --git a/components/fileforge/package.json b/components/fileforge/package.json new file mode 100644 index 0000000000000..da73c8d49d8c2 --- /dev/null +++ b/components/fileforge/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/fileforge", + "version": "0.1.0", + "description": "Pipedream Fileforge Components", + "main": "fileforge.app.mjs", + "keywords": [ + "pipedream", + "fileforge" + ], + "homepage": "https://pipedream.com/apps/fileforge", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1", + "form-data": "^4.0.0" + } +} diff --git a/components/files_com/README.md b/components/files_com/README.md index 3fd370132d558..9318255a15c9d 100644 --- a/components/files_com/README.md +++ b/components/files_com/README.md @@ -1,10 +1,11 @@ # Overview -With the Files.com API, you can build applications that: - -- Upload, download, and manage files and folders -- Share files and folders internally or with anyone -- Store files securely in the cloud -- Preview documents, images, and media files -- Generate and manage URL shortlinks -- And much more! +The Files.com API offers the flexibility to automate file management and manipulate folder structures programmatically. With Files.com on Pipedream, you can construct workflows that handle file synchronization, sharing, and security. It's a powerhouse for creating custom triggers based on file events, automating content distribution, or integrating seamless file operations within a broader data processing ecosystem. + +# Example Use Cases + +- **Automated Backup to Cloud Storage**: Leverage Pipedream's scheduled triggers to periodically back up critical files from Files.com to a cloud storage service like Google Drive or Dropbox. When a new file is added to a designated folder in Files.com, have Pipedream copy it over to your backup storage, ensuring data redundancy and accessibility. + +- **Content Distribution Network**: Utilize Files.com as a central hub for distributing content. When new marketing materials are uploaded, a Pipedream workflow can be triggered to send these files to various platforms, such as a CMS like WordPress, or social media management tools like Buffer, to streamline content dissemination. + +- **User Activity Monitoring and Notification**: Set up a Pipedream workflow that monitors user activity on Files.com and sends alerts via email or a messaging app like Slack. For instance, when a file is downloaded or a new user is added to a workspace, an automatic notification could be sent to the relevant stakeholders to maintain oversight and security. diff --git a/components/files_com/package.json b/components/files_com/package.json new file mode 100644 index 0000000000000..3ecee00271b27 --- /dev/null +++ b/components/files_com/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/files_com", + "version": "0.6.0", + "description": "Pipedream files_com Components", + "main": "files_com.app.mjs", + "keywords": [ + "pipedream", + "files_com" + ], + "homepage": "https://pipedream.com/apps/files_com", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/filestack/README.md b/components/filestack/README.md new file mode 100644 index 0000000000000..64e84422472d7 --- /dev/null +++ b/components/filestack/README.md @@ -0,0 +1,11 @@ +# Overview + +The FileStack API allows for robust file handling and manipulation in the cloud. With this API, you can upload files from any URL or device, transform and convert media files, and deliver content with speed and reliability. When leveraged through Pipedream, the FileStack API becomes a potent tool for automating file workflows, integrating with numerous other services, and handling complex file operations without the need for server infrastructure. + +# Example Use Cases + +- **Automated Image Processing and Optimization**: When images are uploaded to FileStack, use Pipedream to automatically optimize and resize the images for different platforms. The workflow could trigger a notification or update a database with the new image metadata and URLs. + +- **Document Conversion and Distribution**: Convert uploaded documents into different formats (e.g., from PDF to DOCX) using FileStack, then use Pipedream to distribute the converted files via email using an app like SendGrid, or save them to cloud storage solutions like Dropbox or Google Drive. + +- **Content Moderation Pipeline**: Build a content moderation system that scans uploaded files for inappropriate content using FileStack's AI-powered content safety API. With Pipedream, if content is flagged, automatically notify moderators and log details to a service like Airtable or Google Sheets. diff --git a/components/filestack/actions/common/common.mjs b/components/filestack/actions/common/common.mjs new file mode 100644 index 0000000000000..dd1781a59167e --- /dev/null +++ b/components/filestack/actions/common/common.mjs @@ -0,0 +1,54 @@ +import filestack from "../../filestack.app.mjs"; +import fs from "fs"; + +export default { + props: { + filestack, + uploadedImageUrl: { + propDefinition: [ + filestack, + "uploadedImageUrl", + ], + }, + outputFilename: { + propDefinition: [ + filestack, + "outputFilename", + ], + }, + }, + methods: { + getSummary() { + return "Image transformed successfully"; + }, + getTransformations() { + return ""; + }, + }, + async run({ $ }) { + const { + uploadedImageUrl, outputFilename, + } = this; + + const response = await this.filestack.transformImage({ + $, + uploadedImageUrl, + transformations: this.getTransformations(), + }); + + $.export( + "$summary", + this.getSummary(), + ); + + if (outputFilename) { + const filePath = outputFilename?.includes?.("tmp/") + ? outputFilename + : `/tmp/${outputFilename}`; + + await fs.promises.writeFile(filePath, Buffer.from(response)); + return filePath; + } + else return response; + }, +}; diff --git a/components/filestack/actions/filter-image/filter-image.mjs b/components/filestack/actions/filter-image/filter-image.mjs new file mode 100644 index 0000000000000..6e16b26148e9d --- /dev/null +++ b/components/filestack/actions/filter-image/filter-image.mjs @@ -0,0 +1,93 @@ +import { ConfigurationError } from "@pipedream/platform"; +import common from "../common/common.mjs"; + +export default { + ...common, + key: "filestack-filter-image", + name: "Filter Image", + description: "Applies filters such as sharpening, blurring, sepia, monochrome, and more, to an uploaded image. [See the documentation](https://www.filestack.com/docs/api/processing/#image-filters)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + sharpenAmount: { + type: "integer", + label: "Sharpen", + description: "Sharpen amount to apply to the image.", + min: 0, + max: 20, + optional: true, + }, + blurAmount: { + type: "integer", + label: "Blur", + description: "Blur amount to apply to the image.", + min: 0, + max: 20, + optional: true, + }, + monochrome: { + type: "boolean", + label: "Monochrome", + description: "If true, converts the image to monochrome.", + optional: true, + }, + blackAndWhite: { + type: "integer", + label: "Black and White", + description: "Black and white threshold to apply to the image.", + min: 0, + max: 100, + optional: true, + }, + sepia: { + type: "integer", + label: "Sepia", + description: "Sepia tone to apply to the image.", + min: 0, + max: 100, + optional: true, + }, + pixelate: { + type: "integer", + label: "Pixelate", + description: "Pixelate amount to apply to the image.", + min: 2, + max: 100, + optional: true, + }, + oilPaint: { + type: "integer", + label: "Oil Paint", + description: "Oil paint amount to apply to the image.", + min: 2, + max: 100, + optional: true, + }, + negative: { + type: "boolean", + label: "Negative", + description: "If true, creates a negative image by portraying the lightest area as the darkest and the darkest areas as the lightest.", + optional: true, + }, + }, + methods: { + ...common.methods, + getTransformations() { + const transformations = []; + if (this.negative) transformations.push("negative"); + else { + if (this.monochrome) transformations.push("monochrome"); + if (this.sharpenAmount) transformations.push(`sharpen=amount:${this.sharpenAmount}`); + if (this.blurAmount) transformations.push(`blur=amount:${this.blurAmount}`); + if (this.blackAndWhite) transformations.push(`blackwhite=threshold:${this.blackAndWhite}`); + if (this.sepia) transformations.push(`sepia=tone:${this.sepia}`); + if (this.pixelate) transformations.push(`pixelate=amount:${this.pixelate}`); + if (this.oilPaint) transformations.push(`oil_paint=amount:${this.oilPaint}`); + } + + if (!transformations.length) throw new ConfigurationError("At least one filter must be specified."); + return transformations.join("/"); + }, + }, +}; diff --git a/components/filestack/actions/resize-image/resize-image.mjs b/components/filestack/actions/resize-image/resize-image.mjs new file mode 100644 index 0000000000000..a1a48ec1ae811 --- /dev/null +++ b/components/filestack/actions/resize-image/resize-image.mjs @@ -0,0 +1,71 @@ +import common from "../common/common.mjs"; + +export default { + ...common, + key: "filestack-resize-image", + name: "Resize Image", + description: + "Resizes an uploaded image to specified width and height. [See the documentation](https://www.filestack.com/docs/api/processing/#resize)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + width: { + type: "integer", + label: "Width", + description: "The width to resize the image to, in pixels.", + min: 1, + max: 10000, + }, + height: { + type: "integer", + label: "Height", + description: "The height to resize the image to, in pixels.", + min: 1, + max: 10000, + }, + mode: { + type: "string", + label: "Mode", + description: + "The possible methods by which the image should fit the specified dimensions.", + optional: true, + options: [ + { + value: "clip", + label: + "Preserving the aspect ratio, resize the image to be as large as possible while ensuring its dimensions are less than or equal to both those specified", + }, + { + value: "crop", + label: + "Preserving the aspect ratio, ensure the image covers both provided dimensions by clipping/cropping to fit", + }, + { + value: "max", + label: + "Do not enlarge if the dimensions of the provided image are already less than the specified width or height", + }, + { + value: "scale", + label: + "Ignore the aspect ratio of the provided image and stretch to both provided dimensions", + }, + ], + }, + }, + methods: { + ...common.methods, + getSummary() { + return `Image resized successfully to ${this.width}x${this.height}`; + }, + getTransformations() { + const transformations = [ + `resize=height:${this.height}`, + `width:${this.width}`, + ]; + if (this.mode) transformations.push(`,fit:${this.mode}`); + return transformations.join(); + }, + }, +}; diff --git a/components/filestack/actions/rotate-image/rotate-image.mjs b/components/filestack/actions/rotate-image/rotate-image.mjs new file mode 100644 index 0000000000000..97703819ff192 --- /dev/null +++ b/components/filestack/actions/rotate-image/rotate-image.mjs @@ -0,0 +1,29 @@ +import common from "../common/common.mjs"; + +export default { + ...common, + key: "filestack-rotate-image", + name: "Rotate Image", + description: "Rotates an uploaded image by a specified degree. [See the documentation](https://www.filestack.com/docs/api/processing/#rotate)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + degrees: { + type: "integer", + label: "Degrees", + description: "How much to rotate the image in degrees, clockwise from 0 degrees (no rotation) to 359 (nearly all the way around).", + min: 0, + max: 359, + }, + }, + methods: { + ...common.methods, + getSummary() { + return `Image rotated successfully by ${this.degrees} degrees`; + }, + getTransformations() { + return `rotate=deg:${this.degrees}`; + }, + }, +}; diff --git a/components/filestack/actions/upload-image/upload-image.mjs b/components/filestack/actions/upload-image/upload-image.mjs new file mode 100644 index 0000000000000..93fc6f055150a --- /dev/null +++ b/components/filestack/actions/upload-image/upload-image.mjs @@ -0,0 +1,77 @@ +import filestack from "../../filestack.app.mjs"; +import fs from "fs"; + +export default { + key: "filestack-upload-image", + name: "Upload Image", + description: + "Upload an image from a file or URL to FileStack. [See the documentation](https://www.filestack.com/docs/uploads/uploading/#upload-file)", + version: "0.0.1", + type: "action", + props: { + filestack, + fileOrUrl: { + propDefinition: [ + filestack, + "fileOrUrl", + ], + }, + }, + methods: { + getImageMimeType(path) { + const ext = path.split(".").pop(); + switch (ext) { + case "jpg": + return "jpeg"; + case "jpeg": + case "png": + case "gif": + case "bmp": + case "webp": + case "tiff": + return ext; + case "svg": + return "svg+xml"; + default: + return "*"; + } + }, + async getImageData() { + const { fileOrUrl } = this; + const isUrl = fileOrUrl.startsWith("http"); + return isUrl + ? { + data: { + url: fileOrUrl, + }, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + } + : { + data: fs.createReadStream( + fileOrUrl.includes("tmp/") + ? fileOrUrl + : `/tmp/${fileOrUrl}`, + ), + headers: { + "Content-Type": `image/${this.getImageMimeType(fileOrUrl)}`, + }, + }; + }, + }, + async run({ $ }) { + const args = await this.getImageData(); + + const response = await this.filestack.uploadImage({ + $, + ...args, + }); + + $.export( + "$summary", + "Successfully uploaded image", + ); + return response; + }, +}; diff --git a/components/filestack/filestack.app.mjs b/components/filestack/filestack.app.mjs new file mode 100644 index 0000000000000..461a445dcc97c --- /dev/null +++ b/components/filestack/filestack.app.mjs @@ -0,0 +1,61 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "filestack", + propDefinitions: { + fileOrUrl: { + type: "string", + label: "Image Path or URL", + description: + "The path to an image file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp). Alternatively, you can pass the direct URL to an image file.", + }, + uploadedImageUrl: { + type: "string", + label: "Uploaded Image URL", + description: + "The URL of an image uploaded to FileStack (you can use the **Upload Image** action). Example: `https://cdn.filestackcontent.com/pdn7PhZdT02GoYZCVYeF`", + }, + outputFilename: { + type: "string", + label: "Output File Name", + description: "If specified, the resulting image will be written to this filename in the `tmp` folder; otherwise, the raw data will be returned", + optional: true, + }, + }, + methods: { + _getAuth() { + return this.$auth.api_key; + }, + async _makeRequest({ + $ = this, ...otherOpts + }) { + return axios($, otherOpts); + }, + async uploadImage(args) { + return this._makeRequest({ + url: "https://www.filestackapi.com/api/store/S3", + method: "POST", + params: { + key: this._getAuth(), + }, + ...args, + }); + }, + async transformImage({ + transformations, uploadedImageUrl, ...args + }) { + const handle = uploadedImageUrl.split("/").pop(); + return this._makeRequest({ + url: [ + "https://cdn.filestackcontent.com", + this._getAuth(), + transformations, + handle, + ].join("/"), + responseType: "arraybuffer", + ...args, + }); + }, + }, +}; diff --git a/components/filestack/package.json b/components/filestack/package.json new file mode 100644 index 0000000000000..fbaf5555193cc --- /dev/null +++ b/components/filestack/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/filestack", + "version": "0.1.0", + "description": "Pipedream FileStack Components", + "main": "filestack.app.mjs", + "keywords": [ + "pipedream", + "filestack" + ], + "homepage": "https://pipedream.com/apps/filestack", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/fillout/README.md b/components/fillout/README.md new file mode 100644 index 0000000000000..784854846b750 --- /dev/null +++ b/components/fillout/README.md @@ -0,0 +1,11 @@ +# Overview + +The Fillout API provides robust tools for creating, managing, and analyzing custom forms and surveys. With Fillout, businesses can streamline the process of data collection and insights gathering directly from their users or customers. Using Pipedream's capabilities, you can integrate Fillout with hundreds of other services, enabling automated workflows that can trigger actions in other apps based on responses, synchronize data across platforms, or generate real-time notifications and reports. + +# Example Use Cases + +- **Customer Feedback to Slack Notification**: Automatically send a summary of each new Fillout survey response to a Slack channel. This workflow can keep a team instantly informed about customer feedback, improving response times and engagement. + +- **Survey Response Data to Google Sheets**: Store new Fillout responses in a Google Sheets spreadsheet. This is ideal for data analysis and sharing within teams who use Google Workspace. It allows for easy access and manipulation of data for insights and reporting. + +- **Automated Email Follow-ups**: Trigger an email sequence from a service like SendGrid or Mailgun when a form is completed. This can be used for lead nurturing by sending thank you emails, additional information, or promotional content based on the user's responses. diff --git a/components/fillout/fillout.app.mjs b/components/fillout/fillout.app.mjs new file mode 100644 index 0000000000000..3eec1a5a6d3b2 --- /dev/null +++ b/components/fillout/fillout.app.mjs @@ -0,0 +1,58 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "fillout", + propDefinitions: { + formId: { + type: "string", + label: "Form ID", + description: "The ID of the form", + async options() { + const forms = await this.getForms(); + return forms.map((form) => ({ + label: form.name, + value: form.formId, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.fillout.com/v1/api"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + getForms() { + return this._makeRequest({ + path: "/forms", + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhook/create", + ...opts, + }); + }, + deleteWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhook/delete", + ...opts, + }); + }, + }, +}; diff --git a/components/fillout/package.json b/components/fillout/package.json new file mode 100644 index 0000000000000..2a242824c843a --- /dev/null +++ b/components/fillout/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/fillout", + "version": "0.1.0", + "description": "Pipedream Fillout Components", + "main": "fillout.app.mjs", + "keywords": [ + "pipedream", + "fillout" + ], + "homepage": "https://pipedream.com/apps/fillout", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/fillout/sources/new-submission-instant/new-submission-instant.mjs b/components/fillout/sources/new-submission-instant/new-submission-instant.mjs new file mode 100644 index 0000000000000..12a3d5d2341b1 --- /dev/null +++ b/components/fillout/sources/new-submission-instant/new-submission-instant.mjs @@ -0,0 +1,54 @@ +import fillout from "../../fillout.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "fillout-new-submission-instant", + name: "New Submission (Instant)", + description: "Emit new event when a form receives a new submission.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + fillout, + http: { + type: "$.interface.http", + customResponse: false, + }, + db: "$.service.db", + formId: { + propDefinition: [ + fillout, + "formId", + ], + }, + }, + hooks: { + async activate() { + const data = await this.fillout.createWebhook({ + data: { + formId: this.formId, + url: this.http.endpoint, + }, + }); + this.db.set("webhookId", data.id); + }, + async deactivate() { + const webhookId = this.db.get("webhookId"); + if (webhookId) { + await this.fillout.deleteWebhook({ + data: { + webhookId, + }, + }); + } + }, + }, + async run({ body }) { + this.$emit(body, { + id: body.submission.submissionId, + summary: `New submission from form ${body.submission.submissionId}`, + ts: body.submission.submissionTime, + }); + }, + sampleEmit, +}; diff --git a/components/fillout/sources/new-submission-instant/test-event.mjs b/components/fillout/sources/new-submission-instant/test-event.mjs new file mode 100644 index 0000000000000..627e4ad660d69 --- /dev/null +++ b/components/fillout/sources/new-submission-instant/test-event.mjs @@ -0,0 +1,69 @@ +export default { + "formId": "m5q5df45apus", + "formName": "Form Name", + "submission": { + "submissionId": "abc", + "submissionTime": "2024-05-16T23:20:05.324Z", + "lastUpdatedAt": "2024-05-16T23:20:05.324Z", + "questions": [ + { + "id": "abcdef", + "name": "What's your name?", + "type": "ShortAnswer", + "value": "Timmy" + }, + ], + "calculations": [ + { + "id": "calculation1", + "name": "price", + "type": "number", + "value": "12.50" + } + ], + "urlParameters": [ + { + "id": "email", + "name": "email", + "value": "example@example.com", + } + ], + "quiz": { + "score": 5, + "maxScore": 10 + }, + "scheduling": [ + { + "id": "nLJtxBJgPA", + "name": "30 min meeting", + "value": { + "fullName": "John Smith", + "email": "john@smith.com", + "timezone": "Europe/London", + "eventStartTime": "2024-05-20T09:00:00.000Z", + "eventEndTime": "2024-05-20T09:30:00.000Z", + "eventId": "du5ckkaeacd5dlj16d7ajepp8g", + "eventUrl": "https://www.google.com/calendar/event?eid=ZHU1Y2trYWVhY2Q1ZGxqMTZkN2FqZXBwOGcgYXJjaGllQGZpbGxvdXQuY29t&authuser=john%40smith.com" + } + } + ], + "payments": [ + { + "id": "cLJtxCKgdL", + "name": "Complete checkout", + "value": { + "stripeCustomerId": "cus_Ppjz3Z80000000", + "stripeCustomerUrl": "https://dashboard.stripe.com/customers/cus_Ppjz3Z80000000", + "stripePaymentUrl": "https://dashboard.stripe.com/payments/pi_3PRF2cFMP2ckdpfG0s0ZdJqf", + "totalAmount": 1000, + "currency": "USD", + "email": "john@doe.com", + "discountCode": "10OFF", + "status": "succeeded", + "paymentId": "pi_3PRF2cFMP2ckdpfG0s0ZdJqf", + "stripeSubscriptionId": "sub_Ppjz3Z80000000" + } + } + ], + } +} \ No newline at end of file diff --git a/components/filter/README.md b/components/filter/README.md index 3cf28b880cf03..6b2f262e291fa 100644 --- a/components/filter/README.md +++ b/components/filter/README.md @@ -1,11 +1,11 @@ # Overview -The Filter API is a great way to build powerful workflows that can manipulate -and transform data. Here are some examples of what you can build using the -Filter API: - -- A workflow that transforms data from one format to another -- A workflow that filters out data that does not meet certain criteria -- A workflow that sorts data in a specific order -- A workflow that calculates statistics or aggregates data -- A workflow that combines data from multiple sources +The Filter API in Pipedream allows for real-time data processing within workflows. It's designed to evaluate data against predefined conditions, enabling workflows to branch or perform specific actions based on those conditions. This API is instrumental in creating efficient, targeted automations that respond dynamically to diverse datasets. Using the Filter API, you can refine streams of data, ensuring that subsequent steps in your Pipedream workflow only execute when the data meets your specified criteria. This cuts down on unnecessary processing and facilitates the creation of more intelligent, context-aware systems. + +# Example Use Cases + +- **Customer Support Ticket Prioritization**: Automate the prioritization of incoming customer support tickets by filtering them based on keywords, severity, or customer tier. Once filtered, tickets can be routed to the appropriate support staff or escalated if they meet certain critical conditions. + +- **Social Media Sentiment Analysis**: Stream social media mentions into a Pipedream workflow, and use the Filter API to only process posts with negative sentiment. These can then be forwarded to your customer relations team, or logged into a CRM for follow-up, ensuring proactive engagement with potentially dissatisfied customers. + +- **E-commerce Order Processing**: Implement a workflow that filters incoming e-commerce orders by value, location, or item availability. High-value orders could trigger an instant alert to the sales team, whereas orders with out-of-stock items could be placed on hold automatically, improving operational efficiency and customer satisfaction. diff --git a/components/filter/actions/common/common.mjs b/components/filter/actions/common/common.mjs deleted file mode 100644 index d6b3d1da15a45..0000000000000 --- a/components/filter/actions/common/common.mjs +++ /dev/null @@ -1,77 +0,0 @@ -import filter from "../../filter.app.mjs"; -import valueTypes from "../../common/value-types.mjs"; -import { - arrayConditions, - binaryConditions, - textConditions, -} from "../../common/conditions.mjs"; - -export default { - props: { - filter, - messageOnContinue: { - type: "string", - label: "Reason for continuing", - description: "The message that will be displayed when the workflow **continues**", - optional: true, - }, - messageOnEnd: { - type: "string", - label: "Reason for ending", - description: "The message that will be displayed when the workflow **ends**", - optional: true, - }, - initialValue: { - type: "any", - label: "Initial value", - description: "The 1st of 2 values to compare", - }, - condition: { - propDefinition: [ - filter, - "condition", - ], - }, - }, - async additionalProps() { - const props = {}; - if (binaryConditions.includes(this.condition)) { - props.secondValue = { - type: "any", - label: "Second value", - description: "The 2nd of 2 values to compare", - }; - } - if (arrayConditions.includes(this.condition)) { - props.arrayType = { - type: "string", - label: "Initial value type", - description: "Type of the value to search for in the array", - options: Object.values(valueTypes), - default: valueTypes.TEXT, - reloadProps: true, - }; - } - if (textConditions.includes(this.condition) || - (arrayConditions.includes(this.condition) && this.arrayType === valueTypes.TEXT)) { - props.caseSensitive = { - type: "boolean", - label: "Case sensitive", - description: "Whether the text evaluation should be case sensitive or not", - optional: true, - default: false, - }; - } - return props; - }, - async run({ $ }) { - const result = this.filter.checkCondition( - this.condition, - this.initialValue, - this.secondValue, - this.caseSensitive, - this.arrayType, - ); - return this.consolidateResult($, result); - }, -}; diff --git a/components/filter/actions/continue-based-on-condition/continue-based-on-condition.mjs b/components/filter/actions/continue-based-on-condition/continue-based-on-condition.mjs deleted file mode 100644 index 7d0759fde14c8..0000000000000 --- a/components/filter/actions/continue-based-on-condition/continue-based-on-condition.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - name: "Continue execution if a condition Is met", - version: "0.0.2", - key: "filter-continue-based-on-condition", - description: "Continue workflow execution only if a condition is met", - type: "action", - methods: { - consolidateResult($, result) { - !result && $.flow.exit(this.messageOnEnd); - return this.messageOnContinue; - }, - }, -}; diff --git a/components/filter/actions/end-based-on-condition/end-based-on-condition.mjs b/components/filter/actions/end-based-on-condition/end-based-on-condition.mjs deleted file mode 100644 index 1805ad16205bc..0000000000000 --- a/components/filter/actions/end-based-on-condition/end-based-on-condition.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - name: "End execution if a condition is met", - version: "0.0.2", - key: "filter-end-based-on-condition", - description: "End workflow execution if a condition is met", - type: "action", - methods: { - consolidateResult($, result) { - result && $.flow.exit(this.messageOnEnd); - return this.messageOnContinue; - }, - }, -}; diff --git a/components/filter/package.json b/components/filter/package.json index 24ab209333bdb..8f801cd0a0b1d 100644 --- a/components/filter/package.json +++ b/components/filter/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/filter", - "version": "0.0.4", + "version": "0.0.5", "description": "Pipedream Filter Components", "main": "filter.app.mjs", "keywords": [ diff --git a/components/finage/README.md b/components/finage/README.md new file mode 100644 index 0000000000000..0fde9b800d5bb --- /dev/null +++ b/components/finage/README.md @@ -0,0 +1,11 @@ +# Overview + +The Finage API provides real-time, historical, and market data across stocks, currencies, and cryptocurrencies. Integrating Finage with Pipedream allows you to create workflows that react to market changes, automate trading analysis, and consolidate financial data for reporting. With Pipedream, you can tap into webhooks, schedules, and over 800+ apps for extensive automation possibilities, leveraging Finage's vast financial datasets. + +# Example Use Cases + +- **Real-time Stock Alert System**: Create a Pipedream workflow that uses the Finage API to monitor specific stock prices. When the price hits a predefined threshold, the workflow can trigger an alert via email, SMS, or a messaging platform like Slack. This helps you act swiftly on market changes. + +- **Automated Currency Exchange Rate Analysis**: Use Pipedream to schedule a recurring workflow that fetches the latest currency exchange rates from Finage. The data can be processed and analyzed to identify trends, and results can be stored in Google Sheets or a database for further analysis or visualization. + +- **Cryptocurrency Data Aggregator**: Build a workflow on Pipedream that collates real-time cryptocurrency data from Finage with additional information from other crypto-related APIs. This aggregated dataset can be sent to a data visualization tool like Power BI or Tableau for an in-depth market analysis. diff --git a/components/finage/finage.app.mjs b/components/finage/finage.app.mjs new file mode 100644 index 0000000000000..3bb1cce1eab80 --- /dev/null +++ b/components/finage/finage.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "finage", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/finage/package.json b/components/finage/package.json new file mode 100644 index 0000000000000..f7538ee325b2a --- /dev/null +++ b/components/finage/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/finage", + "version": "0.0.1", + "description": "Pipedream Finage Components", + "main": "finage.app.mjs", + "keywords": [ + "pipedream", + "finage" + ], + "homepage": "https://pipedream.com/apps/finage", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/findymail/README.md b/components/findymail/README.md new file mode 100644 index 0000000000000..09ff861ad62ed --- /dev/null +++ b/components/findymail/README.md @@ -0,0 +1,11 @@ +# Overview + +The Findymail API provides a way to enrich and validate email contacts. Leveraging Pipedream's capabilities, you can automate workflows that trigger on various events to lookup emails, auto-populate CRM fields, validate email data, or even enhance marketing lists with accurate contact information. Using Findymail's API in Pipedream, you can create intricate serverless workflows that react to various triggers, such as webhooks, scheduled timers, or actions from other apps, to integrate email data enrichment into your business processes. + +# Example Use Cases + +- **Enrich New Subscriber Data**: When a new subscriber signs up on your platform, trigger a Pipedream workflow to lookup the provided email via the Findymail API. Use the enriched data to segment the subscriber into marketing campaigns or personalize communications. + +- **Maintain CRM Integrity**: Schedule a Pipedream workflow to periodically validate and update email addresses in your CRM. Connect Findymail with CRM platforms like Salesforce or HubSpot, ensuring the email data is correct and up-to-date. + +- **Automate Lead Generation**: On receiving a new lead's partial contact details from a form submission or a LinkedIn scrape, use a Pipedream workflow to find the full email address with Findymail. Then, dispatch the contact to a mailing list in Mailchimp or trigger a personalized outreach sequence. diff --git a/components/finerworks/README.md b/components/finerworks/README.md new file mode 100644 index 0000000000000..abb427d968783 --- /dev/null +++ b/components/finerworks/README.md @@ -0,0 +1,11 @@ +# Overview + +FinerWorks is an online platform that allows users to print and drop ship fine art prints and other related products. With the FinerWorks API, you can automate the process of creating orders, managing items, and integrating print-on-demand services into your business. When used in Pipedream, it unlocks possibilities for streamlining how orders are processed, integrating with other services like payment gateways or customer relationship management systems, and automating notifications for order updates. + +# Example Use Cases + +- **Automated Order Submission**: Automatically submit new orders to FinerWorks when a customer completes a purchase on your e-commerce platform. This workflow could trigger when a new order is placed, extract the necessary details, and use the FinerWorks API to create the order for printing and shipping. + +- **Inventory Sync and Update**: Keep your product listings up to date by synchronizing inventory levels between FinerWorks and your online storefront. Whenever an item's stock changes on FinerWorks, the workflow could update your storefront's inventory to reflect the current stock levels. + +- **Order Status Notifications**: Set up an automated system to notify customers about the status of their orders. This could involve a workflow that checks the status of orders on FinerWorks at regular intervals and sends an email update to customers using an email service such as SendGrid whenever their order status changes. diff --git a/components/finerworks/finerworks.app.mjs b/components/finerworks/finerworks.app.mjs new file mode 100644 index 0000000000000..18c88ec8e4d8a --- /dev/null +++ b/components/finerworks/finerworks.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "finerworks", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/finerworks/package.json b/components/finerworks/package.json new file mode 100644 index 0000000000000..2a9bd043fa4df --- /dev/null +++ b/components/finerworks/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/finerworks", + "version": "0.0.1", + "description": "Pipedream FinerWorks Components", + "main": "finerworks.app.mjs", + "keywords": [ + "pipedream", + "finerworks" + ], + "homepage": "https://pipedream.com/apps/finerworks", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/fingertip/fingertip.app.mjs b/components/fingertip/fingertip.app.mjs new file mode 100644 index 0000000000000..b02781c860871 --- /dev/null +++ b/components/fingertip/fingertip.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "fingertip", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/fingertip/package.json b/components/fingertip/package.json new file mode 100644 index 0000000000000..cef145e8735b5 --- /dev/null +++ b/components/fingertip/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/fingertip", + "version": "0.0.1", + "description": "Pipedream Fingertip Components", + "main": "fingertip.app.mjs", + "keywords": [ + "pipedream", + "fingertip" + ], + "homepage": "https://pipedream.com/apps/fingertip", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/finmei/README.md b/components/finmei/README.md new file mode 100644 index 0000000000000..a51a992b58b26 --- /dev/null +++ b/components/finmei/README.md @@ -0,0 +1,11 @@ +# Overview + +The Finmei API offers access to a platform geared towards financial data management. With Pipedream, you can harness this API to craft serverless workflows that automate financial tasks, integrate with other services, and streamline data analysis. This includes tracking expenses, aggregating financial reports, or syncing data across different financial tools. Pipedream's no-code platform allows you to connect Finmei to hundreds of other apps, trigger actions based on various events, and manage data with built-in CRON scheduling. + +# Example Use Cases + +- **Automate Expense Tracking**: Connect Finmei with an email service like Gmail in Pipedream. Automatically parse receipt emails and use Finmei's API to log those expenses into a user's account. This simplifies expense management and ensures financial data is consistently up-to-date. + +- **Aggregate Monthly Financial Reports**: Set up a monthly CRON job in Pipedream that triggers a workflow to fetch data from the Finmei API. Combine this data with other financial sources and use Pipedream's built-in data transformation capabilities to create comprehensive financial reports. + +- **Sync Finmei Data with Accounting Software**: Use Pipedream to build a workflow where financial entries from Finmei are automatically synced with accounting software like QuickBooks. Whenever a new transaction is added in Finmei, trigger an event in Pipedream that creates a corresponding entry in QuickBooks, ensuring seamless financial tracking across platforms. diff --git a/components/finmei/actions/create-invoice/create-invoice.mjs b/components/finmei/actions/create-invoice/create-invoice.mjs new file mode 100644 index 0000000000000..b654e04278519 --- /dev/null +++ b/components/finmei/actions/create-invoice/create-invoice.mjs @@ -0,0 +1,121 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { INVOICE_TYPES } from "../../common/constants.mjs"; +import finmei from "../../finmei.app.mjs"; +import { parseAsJSON } from "../../common/utils.mjs"; + +export default { + key: "finmei-create-invoice", + name: "Create Invoice", + description: + "Generates a new invoice within Finmei. [See the documentation](https://documenter.getpostman.com/view/835227/2s9YXh5NRs#01e4e494-2aaf-4d87-9b8d-e527b04af0a0)", + version: "0.0.1", + type: "action", + props: { + finmei, + type: { + type: "string", + label: "Type", + description: "The type of invoice to create", + options: INVOICE_TYPES, + }, + date: { + type: "string", + label: "Date", + description: "Date showed in the invoice. Format: `YYYY-MM-DD`", + }, + series: { + type: "string", + label: "Series", + description: "Invoice series in string format", + }, + currency: { + type: "string", + label: "Currency", + description: "Uppercase three letter currency code, e.g. `USD`", + }, + buyer: { + type: "object", + label: "Buyer", + description: + "The buyer info, as an object. [See the documentation](https://documenter.getpostman.com/view/835227/2s9YXh5NRs#01e4e494-2aaf-4d87-9b8d-e527b04af0a0) for the properties. Example: `{ \"type\": \"company\", \"company_name\": \"My Company\" }`", + }, + products: { + type: "string[]", + label: "Product(s)", + description: + "One or more products as JSON-stringified objects. [See the documentation](https://documenter.getpostman.com/view/835227/2s9YXh5NRs#01e4e494-2aaf-4d87-9b8d-e527b04af0a0) for the properties. Example: `{ \"name\": \"My Product\", \"units\": \"pcs\", \"quantity\": 2, \"price\": 10 }`", + }, + useDefaultSellerInfo: { + type: "boolean", + label: "Use Default Seller Info", + description: + "If true, you do not need to provide seller info. Your business info and latest invoice information will be used.", + optional: true, + default: true, + }, + additionalOptions: { + type: "object", + label: "Additional Options", + description: + "Additional parameters to send in the request. [See the documentation](https://documenter.getpostman.com/view/835227/2s9YXh5NRs#01e4e494-2aaf-4d87-9b8d-e527b04af0a0) for available parameters. Values will be parsed as JSON where applicable.", + optional: true, + }, + }, + async run({ $ }) { + let products, buyer; + try { + const value = parseAsJSON(this.products); + products = value.map(parseAsJSON); + } catch (e) { + throw new ConfigurationError( + `Error parsing JSON value in \`Product(s)\` prop as JSON: \`${e}\``, + ); + } + try { + buyer = parseAsJSON(this.buyer); + } catch (e) { + throw new ConfigurationError( + `Error parsing JSON value in \`Product(s)\` prop as JSON: \`${e}\``, + ); + } + + let additionalOptions = Object.fromEntries( + Object.entries(this.additionalOptions ?? {}).map(([ + key, + value, + ]) => { + // optional JSON parsing + try { + return [ + key, + JSON.parse(value), + ]; + } catch (e) { + return [ + key, + value, + ]; + } + }), + ); + + const response = await this.finmei.createInvoice({ + $, + data: { + type: this.type, + invoice_date: this.date, + series: this.series, + currency: this.currency, + use_default_seller_info: this.useDefaultSellerInfo, + buyer, + products, + ...additionalOptions, + }, + }); + $.export( + "$summary", + `Successfully created invoice (ID: ${response?.data?.id})`, + ); + return response; + }, +}; diff --git a/components/finmei/common/constants.mjs b/components/finmei/common/constants.mjs new file mode 100644 index 0000000000000..349a4c6ab6b77 --- /dev/null +++ b/components/finmei/common/constants.mjs @@ -0,0 +1,8 @@ +export const INVOICE_TYPES = [ + "regular_invoice", + "vat_invoice", + "preliminary_invoice", + "preliminary_vat_invoice", + "credit_invoice", + "credit_vat_invoice", +]; diff --git a/components/finmei/common/utils.mjs b/components/finmei/common/utils.mjs new file mode 100644 index 0000000000000..f48ac56310bf2 --- /dev/null +++ b/components/finmei/common/utils.mjs @@ -0,0 +1,5 @@ +export function parseAsJSON(value) { + return typeof value === "string" + ? JSON.parse(value) + : value; +} diff --git a/components/finmei/finmei.app.mjs b/components/finmei/finmei.app.mjs new file mode 100644 index 0000000000000..08dc181ca9db5 --- /dev/null +++ b/components/finmei/finmei.app.mjs @@ -0,0 +1,33 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "finmei", + propDefinitions: {}, + methods: { + _baseUrl() { + return "https://app.finmei.com/api"; + }, + async _makeRequest({ + $ = this, + headers, + ...otherOpts + }) { + return axios($, { + ...otherOpts, + baseURL: this._baseUrl(), + headers: { + ...headers, + "Authorization": `Bearer ${this.$auth.api_token}`, + }, + }); + }, + async createInvoice(args) { + return this._makeRequest({ + method: "POST", + url: "/invoices", + ...args, + }); + }, + }, +}; diff --git a/components/finmei/package.json b/components/finmei/package.json new file mode 100644 index 0000000000000..255cc881638d0 --- /dev/null +++ b/components/finmei/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/finmei", + "version": "0.1.0", + "description": "Pipedream Finmei Components", + "main": "finmei.app.mjs", + "keywords": [ + "pipedream", + "finmei" + ], + "homepage": "https://pipedream.com/apps/finmei", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.1" + } +} diff --git a/components/finmo/README.md b/components/finmo/README.md new file mode 100644 index 0000000000000..178a0a43d8a84 --- /dev/null +++ b/components/finmo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Finmo API allows for the automation of financial monitoring and transactions. By leveraging the API on Pipedream, you can create serverless workflows to trigger actions based on financial events, synchronize data across multiple platforms, and generate reports or notifications. Pipedream's ability to connect with a vast array of services means you can integrate Finmo with other financial tools, CRMs, or communication platforms, creating a highly personalized and automated financial management system. + +# Example Use Cases + +- **Automate Transaction Alerts**: Set up a workflow that triggers when a new transaction is logged in Finmo. The workflow could filter for transactions over a certain amount and send an alert via email or SMS through integrations like Twilio or SendGrid. + +- **Sync Financial Data with Accounting Software**: Create a workflow that, upon a new transaction or account update in Finmo, automatically updates the records in your accounting software, such as QuickBooks or Xero. This ensures your financials are always current without manual entry. + +- **Generate Monthly Financial Reports**: Configure a workflow that runs monthly to gather transaction data from Finmo, compile it into a report using Pipedream's built-in code steps, and deliver it to stakeholders through Slack, email, or another preferred communication channel. diff --git a/components/finnhub/README.md b/components/finnhub/README.md new file mode 100644 index 0000000000000..abde9179597c1 --- /dev/null +++ b/components/finnhub/README.md @@ -0,0 +1,11 @@ +# Overview + +Finnhub API offers real-time market data, financial statements, and various market indicators. It's a valuable tool for investors and developers building tools for market analysis, portfolio management, or financial data integration. Using Pipedream, you can connect the Finnhub API to various other apps and services, creating automated workflows that leverage live financial data to inform decisions, trigger alerts, or feed into data analysis pipelines. + +# Example Use Cases + +- **Stock Price Alert System**: Create a Pipedream workflow that monitors a stock's price via the Finnhub API. When the price hits a certain threshold, Pipedream sends a Slack message or an email to notify you instantly. + +- **Daily Financial News Digest**: Use Finnhub's news endpoint to fetch the latest financial news. Schedule a Pipedream workflow to aggregate daily news and send a formatted digest to your email or save it to Google Sheets for easy access and record-keeping. + +- **Market Sentiment Analysis**: Combine Finnhub with a sentiment analysis API to gauge market sentiment. Set up a workflow in Pipedream that fetches the latest tweets or news headlines about a particular stock using Finnhub, runs sentiment analysis, and logs the results to a database or a service like Airtable for trend tracking and decision-making. diff --git a/components/finnhub/finnhub.app.mjs b/components/finnhub/finnhub.app.mjs new file mode 100644 index 0000000000000..565608f53e08a --- /dev/null +++ b/components/finnhub/finnhub.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "finnhub", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/finnhub/package.json b/components/finnhub/package.json new file mode 100644 index 0000000000000..bc24c1f73b78c --- /dev/null +++ b/components/finnhub/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/finnhub", + "version": "0.0.1", + "description": "Pipedream Finnhub Components", + "main": "finnhub.app.mjs", + "keywords": [ + "pipedream", + "finnhub" + ], + "homepage": "https://pipedream.com/apps/finnhub", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/firebase_admin_sdk/README.md b/components/firebase_admin_sdk/README.md index c7a2641dea6b7..3a54b8591a4d3 100644 --- a/components/firebase_admin_sdk/README.md +++ b/components/firebase_admin_sdk/README.md @@ -1,8 +1,11 @@ # Overview -With the Firebase Admin SDK API, you can build a variety of different -applications and tools. For example, you could build a tool to manage your -Firebase project's users, or a tool to monitor your Firebase project's data -usage. You could also build an app that helps you manage your Firebase -project's settings, or an app that helps you troubleshoot your Firebase -project's data. +The Firebase Admin SDK API provides powerful backend functionality for Firebase apps. It allows you to interact with Firebase services like Firestore, Firebase Realtime Database, Firebase Storage, and Firebase Authentication directly from a server. With Pipedream, you can harness this API to automate complex workflows, respond to Firebase events in real-time, and integrate with countless other services. + +# Example Use Cases + +- **User Data Synchronization**: Synchronize user profiles in Firestore with user records in a CRM like Salesforce or HubSpot. Whenever a user updates their profile in your app, trigger a Pipedream workflow that updates the corresponding CRM record, ensuring data consistency. + +- **Automated Content Moderation**: Implement an automated content moderation flow that triggers whenever new text or images are uploaded to Firebase Storage. Use Google Cloud's Vision API or Perspective API to analyze the content, and if it violates policies, the workflow can automatically remove the content and notify administrators. + +- **Real-Time Analytics Aggregation**: When new events are logged in Firebase Analytics, use Pipedream to aggregate this data and send it to a data warehouse like Snowflake or Google BigQuery. This enables advanced analysis and the ability to join this data with other business data sources for comprehensive insights. diff --git a/components/firebase_admin_sdk/actions/common/base.mjs b/components/firebase_admin_sdk/actions/common/base.mjs index c232242646bbd..6afc233f5c348 100644 --- a/components/firebase_admin_sdk/actions/common/base.mjs +++ b/components/firebase_admin_sdk/actions/common/base.mjs @@ -10,6 +10,21 @@ export default { ], }, }, + methods: { + parseBooleanValues(data) { + Object.entries(data).forEach(([ + key, + value, + ]) => { + data[key] = value === "false" + ? false + : value === "true" + ? true + : value; + }); + return data; + }, + }, async run({ $ }) { try { await this.firebase.initializeApp(this.databaseRegion); diff --git a/components/firebase_admin_sdk/actions/create-document/create-document.mjs b/components/firebase_admin_sdk/actions/create-document/create-document.mjs index b210ec4951c1c..836a35d705426 100644 --- a/components/firebase_admin_sdk/actions/create-document/create-document.mjs +++ b/components/firebase_admin_sdk/actions/create-document/create-document.mjs @@ -4,8 +4,8 @@ export default { ...common, key: "firebase_admin_sdk-create-document", name: "Create Document", - description: "Creates a New Document. [See the docs here](https://googleapis.dev/nodejs/firestore/latest/CollectionReference.html#add)", - version: "0.0.7", + description: "Creates a New Document. [See the documentation](https://googleapis.dev/nodejs/firestore/latest/CollectionReference.html#add)", + version: "0.0.10", type: "action", props: { ...common.props, @@ -34,7 +34,8 @@ export default { methods: { ...common.methods, async getResponse() { - return this.firebase.createDocument(this.collection, this.data, this.customId); + const data = this.parseBooleanValues(this.data); + return this.firebase.createDocument(this.collection, data, this.customId); }, emitSummary($, response) { $.export("$summary", `Successfully added document ${response?._path?.segments[1] ?? ""}`); diff --git a/components/firebase_admin_sdk/actions/create-realtime-db-record/create-realtime-db-record.mjs b/components/firebase_admin_sdk/actions/create-realtime-db-record/create-realtime-db-record.mjs index 147c5f63eb431..82d2002a0522f 100644 --- a/components/firebase_admin_sdk/actions/create-realtime-db-record/create-realtime-db-record.mjs +++ b/components/firebase_admin_sdk/actions/create-realtime-db-record/create-realtime-db-record.mjs @@ -5,7 +5,7 @@ export default { key: "firebase_admin_sdk-create-realtime-db-record", name: "Create Firebase Realtime Database Record", description: "Creates or replaces a child object within your Firebase Realtime Database. [See the docs here](https://firebase.google.com/docs/reference/js/database#update)", - version: "0.0.4", + version: "0.0.7", type: "action", props: { ...common.props, diff --git a/components/firebase_admin_sdk/actions/list-documents/list-documents.mjs b/components/firebase_admin_sdk/actions/list-documents/list-documents.mjs index 23b1587ee2340..78fdef994649d 100644 --- a/components/firebase_admin_sdk/actions/list-documents/list-documents.mjs +++ b/components/firebase_admin_sdk/actions/list-documents/list-documents.mjs @@ -5,7 +5,7 @@ export default { key: "firebase_admin_sdk-list-documents", name: "List Documents", description: "Lists documents in a collection. [See the docs here](https://googleapis.dev/nodejs/firestore/latest/CollectionReference.html#listDocuments)", - version: "0.0.4", + version: "0.0.7", type: "action", props: { ...common.props, diff --git a/components/firebase_admin_sdk/actions/replicate-event-firestore/replicate-event-firestore.mjs b/components/firebase_admin_sdk/actions/replicate-event-firestore/replicate-event-firestore.mjs index cacf5f186c533..ba76ef90a820c 100644 --- a/components/firebase_admin_sdk/actions/replicate-event-firestore/replicate-event-firestore.mjs +++ b/components/firebase_admin_sdk/actions/replicate-event-firestore/replicate-event-firestore.mjs @@ -6,7 +6,7 @@ export default { key: "firebase_admin_sdk-replicate-event-firestore", name: "Save Event to Firestore", description: "Replicate event in Firestore", - version: "0.4.4", + version: "0.4.6", type: "action", props: { firebase_admin_sdk: { diff --git a/components/firebase_admin_sdk/actions/update-document/update-document.mjs b/components/firebase_admin_sdk/actions/update-document/update-document.mjs index 4f14c8d495012..79ed26d1824d1 100644 --- a/components/firebase_admin_sdk/actions/update-document/update-document.mjs +++ b/components/firebase_admin_sdk/actions/update-document/update-document.mjs @@ -3,9 +3,9 @@ import common from "../common/base.mjs"; export default { ...common, key: "firebase_admin_sdk-update-document", - name: "Update Documents", - description: "Updates a Document. [See the docs here](https://googleapis.dev/nodejs/firestore/latest/DocumentReference.html#update)", - version: "0.0.4", + name: "Update Document", + description: "Updates a Document. [See the documentation](https://googleapis.dev/nodejs/firestore/latest/DocumentReference.html#update)", + version: "0.0.7", type: "action", props: { ...common.props, @@ -39,7 +39,8 @@ export default { methods: { ...common.methods, async getResponse() { - return this.firebase.updateDocument(this.collection, this.document, this.data); + const data = this.parseBooleanValues(this.data); + return this.firebase.updateDocument(this.collection, this.document, data); }, emitSummary($) { $.export("$summary", `Successfully updated document ${this.document}`); diff --git a/components/firebase_admin_sdk/package.json b/components/firebase_admin_sdk/package.json index 6c5b4f7472d96..b82cfd4d18ade 100644 --- a/components/firebase_admin_sdk/package.json +++ b/components/firebase_admin_sdk/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/firebase_admin_sdk", - "version": "0.0.7", + "version": "0.0.10", "description": "Pipedream Firebase Admin SDK Components", "main": "firebase_admin_sdk.app.mjs", "keywords": [ @@ -12,8 +12,8 @@ "dependencies": { "@firebase/app-compat": "^0.1.25", "@firebase/app-types": "^0.7.0", - "@pipedream/platform": "^0.9.0", - "firebase-admin": "^10.0.1", + "@pipedream/platform": "^3.0.3", + "firebase-admin": "^13.0.2", "google-auth-library": "^7.11.0" }, "gitHead": "e12480b94cc03bed4808ebc6b13e7fdb3a1ba535", diff --git a/components/firebase_admin_sdk/sources/new-child-object/new-child-object.mjs b/components/firebase_admin_sdk/sources/new-child-object/new-child-object.mjs index d09a779ac8be5..4048ed3841159 100644 --- a/components/firebase_admin_sdk/sources/new-child-object/new-child-object.mjs +++ b/components/firebase_admin_sdk/sources/new-child-object/new-child-object.mjs @@ -5,7 +5,7 @@ export default { key: "firebase_admin_sdk-new-child-object", name: "New Child Object in a Realtime Database", description: "Emit new event when a new child object is discovered within a specific path", - version: "0.0.6", + version: "0.0.8", type: "source", dedupe: "unique", props: { diff --git a/components/firebase_admin_sdk/sources/new-doc-in-firestore-collection/new-doc-in-firestore-collection.mjs b/components/firebase_admin_sdk/sources/new-doc-in-firestore-collection/new-doc-in-firestore-collection.mjs index 262d6d6827a52..580a64195e94b 100644 --- a/components/firebase_admin_sdk/sources/new-doc-in-firestore-collection/new-doc-in-firestore-collection.mjs +++ b/components/firebase_admin_sdk/sources/new-doc-in-firestore-collection/new-doc-in-firestore-collection.mjs @@ -5,7 +5,7 @@ export default { key: "firebase_admin_sdk-new-doc-in-firestore-collection", name: "New Document in Firestore Collection", description: "Emit new event when a structured query returns new documents", - version: "0.0.6", + version: "0.0.8", type: "source", dedupe: "unique", props: { diff --git a/components/fireberry/README.md b/components/fireberry/README.md new file mode 100644 index 0000000000000..380bdff6d2b1d --- /dev/null +++ b/components/fireberry/README.md @@ -0,0 +1,11 @@ +# Overview + +The Fireberry API enables users to interact with Fireberry's suite of services programmatically. With its API, you can automate tasks related to their offerings. In Pipedream, you could leverage this API to create serverless workflows that respond to various triggers (like HTTP requests, emails, or schedule timings) and integrate with other apps to extend Fireberry's functionality. + +# Example Use Cases + +- **Automate Data Sync with Google Sheets**: Use the Fireberry API to pull data from your Fireberry account and automate the process of syncing it to a Google Sheet. This workflow can keep your datasets up-to-date without manual exports and imports. + +- **Send Notifications Based on Fireberry Events**: Set up a workflow that listens for specific events or updates in Fireberry, and then uses the Twilio API to send SMS messages or the SendGrid API to send emails as notifications to relevant stakeholders. + +- **Manage Customer Support Tickets**: Integrate Fireberry with a customer support platform like Zendesk. Whenever a new entry is added in Fireberry, a Pipedream workflow can create a corresponding ticket in Zendesk to streamline support processes. diff --git a/components/firecrawl/actions/crawl-url/crawl-url.mjs b/components/firecrawl/actions/crawl-url/crawl-url.mjs new file mode 100644 index 0000000000000..3fb7bf0ec4973 --- /dev/null +++ b/components/firecrawl/actions/crawl-url/crawl-url.mjs @@ -0,0 +1,193 @@ +import firecrawl from "../../firecrawl.app.mjs"; + +export default { + key: "firecrawl-crawl-url", + name: "Crawl URL", + description: "Crawls a given input URL and returns the contents of sub-pages. [See the documentation](https://docs.firecrawl.dev/api-reference/endpoint/crawl)", + version: "0.0.1", + type: "action", + props: { + firecrawl, + url: { + propDefinition: [ + firecrawl, + "url", + ], + }, + includes: { + propDefinition: [ + firecrawl, + "includes", + ], + optional: true, + }, + excludes: { + propDefinition: [ + firecrawl, + "excludes", + ], + optional: true, + }, + generateImgAltText: { + propDefinition: [ + firecrawl, + "generateImgAltText", + ], + optional: true, + }, + returnOnlyUrls: { + propDefinition: [ + firecrawl, + "returnOnlyUrls", + ], + optional: true, + }, + maxDepth: { + propDefinition: [ + firecrawl, + "maxDepth", + ], + optional: true, + }, + mode: { + propDefinition: [ + firecrawl, + "mode", + ], + optional: true, + }, + ignoreSitemap: { + propDefinition: [ + firecrawl, + "ignoreSitemap", + ], + optional: true, + }, + limit: { + propDefinition: [ + firecrawl, + "limit", + ], + optional: true, + }, + allowBackwardCrawling: { + propDefinition: [ + firecrawl, + "allowBackwardCrawling", + ], + optional: true, + }, + allowExternalContentLinks: { + propDefinition: [ + firecrawl, + "allowExternalContentLinks", + ], + optional: true, + }, + headers: { + propDefinition: [ + firecrawl, + "headers", + ], + optional: true, + }, + includeHtml: { + propDefinition: [ + firecrawl, + "includeHtml", + ], + optional: true, + }, + includeRawHtml: { + propDefinition: [ + firecrawl, + "includeRawHtml", + ], + optional: true, + }, + onlyIncludeTags: { + propDefinition: [ + firecrawl, + "onlyIncludeTags", + ], + optional: true, + }, + onlyMainContent: { + propDefinition: [ + firecrawl, + "onlyMainContent", + ], + optional: true, + }, + removeTags: { + propDefinition: [ + firecrawl, + "removeTags", + ], + optional: true, + }, + replaceAllPathsWithAbsolutePaths: { + propDefinition: [ + firecrawl, + "replaceAllPathsWithAbsolutePaths", + ], + optional: true, + }, + screenshot: { + propDefinition: [ + firecrawl, + "screenshot", + ], + optional: true, + }, + fullPageScreenshot: { + propDefinition: [ + firecrawl, + "fullPageScreenshot", + ], + optional: true, + }, + waitFor: { + propDefinition: [ + firecrawl, + "waitFor", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.firecrawl.crawl({ + $, + data: { + url: this.url, + crawlerOptions: { + includes: this.includes, + excludes: this.excludes, + generateImgAltText: this.generateImgAltText, + returnOnlyUrls: this.returnOnlyUrls, + maxDepth: parseInt(this.maxDepth), + mode: this.mode, + ignoreSitemap: this.ignoreSitemap, + limit: this.limit, + allowBackwardCrawling: this.allowBackwardCrawling, + allowExternalContentLinks: this.allowExternalContentLinks, + }, + pageOptions: { + headers: this.headers, + includeHtml: this.includeHtml, + includeRawHtml: this.includeRawHtml, + onlyIncludeTags: this.onlyIncludeTags, + onlyMainContent: this.onlyMainContent, + removeTags: this.removeTags, + replaceAllPathsWithAbsolutePaths: this.replaceAllPathsWithAbsolutePaths, + screenshot: this.screenshot, + fullPageScreenshot: this.fullPageScreenshot, + waitFor: parseInt(this.waitFor), + }, + }, + }); + + $.export("$summary", `Crawl job started with jobId: ${response.jobId}`); + return response; + }, +}; diff --git a/components/firecrawl/actions/get-crawl-status/get-crawl-status.mjs b/components/firecrawl/actions/get-crawl-status/get-crawl-status.mjs new file mode 100644 index 0000000000000..29a0471bde8b6 --- /dev/null +++ b/components/firecrawl/actions/get-crawl-status/get-crawl-status.mjs @@ -0,0 +1,27 @@ +import firecrawl from "../../firecrawl.app.mjs"; + +export default { + key: "firecrawl-get-crawl-status", + name: "Get Crawl Status", + description: "Obtains the status and data from a previous crawl operation. [See the documentation](https://docs.firecrawl.dev/api-reference/endpoint/status)", + version: "0.0.1", + type: "action", + props: { + firecrawl, + crawlId: { + propDefinition: [ + firecrawl, + "crawlId", + ], + }, + }, + async run({ $ }) { + const response = await this.firecrawl.getCrawlStatus({ + $, + crawlId: this.crawlId, + }); + + $.export("$summary", `Successfully retrieved status for crawl ID: ${this.crawlId}`); + return response; + }, +}; diff --git a/components/firecrawl/actions/scrape-page/scrape-page.mjs b/components/firecrawl/actions/scrape-page/scrape-page.mjs new file mode 100644 index 0000000000000..dbccb30dfa763 --- /dev/null +++ b/components/firecrawl/actions/scrape-page/scrape-page.mjs @@ -0,0 +1,150 @@ +import { parseObject } from "../../common/utils.mjs"; +import firecrawl from "../../firecrawl.app.mjs"; + +export default { + key: "firecrawl-scrape-page", + name: "Scrape Page", + description: "Scrapes a URL and returns content from that page. [See the documentation](https://docs.firecrawl.dev/api-reference/endpoint/scrape)", + version: "0.0.1", + type: "action", + props: { + firecrawl, + url: { + propDefinition: [ + firecrawl, + "url", + ], + description: "The URL to start scraping from.", + }, + extractorMode: { + propDefinition: [ + firecrawl, + "extractorMode", + ], + optional: true, + }, + extractionPrompt: { + propDefinition: [ + firecrawl, + "extractionPrompt", + ], + optional: true, + }, + extractionSchema: { + propDefinition: [ + firecrawl, + "extractionSchema", + ], + optional: true, + }, + + headers: { + propDefinition: [ + firecrawl, + "headers", + ], + optional: true, + }, + includeHtml: { + propDefinition: [ + firecrawl, + "includeHtml", + ], + optional: true, + }, + includeRawHtml: { + propDefinition: [ + firecrawl, + "includeRawHtml", + ], + optional: true, + }, + onlyIncludeTags: { + propDefinition: [ + firecrawl, + "onlyIncludeTags", + ], + optional: true, + }, + onlyMainContent: { + propDefinition: [ + firecrawl, + "onlyMainContent", + ], + optional: true, + }, + removeTags: { + propDefinition: [ + firecrawl, + "removeTags", + ], + optional: true, + }, + replaceAllPathsWithAbsolutePaths: { + propDefinition: [ + firecrawl, + "replaceAllPathsWithAbsolutePaths", + ], + optional: true, + }, + screenshot: { + propDefinition: [ + firecrawl, + "screenshot", + ], + optional: true, + }, + fullPageScreenshot: { + propDefinition: [ + firecrawl, + "fullPageScreenshot", + ], + optional: true, + }, + waitFor: { + propDefinition: [ + firecrawl, + "waitFor", + ], + optional: true, + }, + timeout: { + propDefinition: [ + firecrawl, + "timeout", + ], + optional: true, + }, + }, + async run({ $ }) { + const extractorOptions = {}; + if (this.extractorMode) extractorOptions.extractorMode = this.extractorMode; + if (this.extractionPrompt) extractorOptions.extractionPrompt = this.extractionPrompt; + if (this.extractionSchema) + extractorOptions.extractionSchema = parseObject(this.extractionSchema); + + const response = await this.firecrawl.scrape({ + $, + data: { + url: this.url, + pageOptions: { + headers: this.headers, + includeHtml: this.includeHtml, + includeRawHtml: this.includeRawHtml, + onlyIncludeTags: this.onlyIncludeTags, + onlyMainContent: this.onlyMainContent, + removeTags: this.removeTags, + replaceAllPathsWithAbsolutePaths: this.replaceAllPathsWithAbsolutePaths, + screenshot: this.screenshot, + fullPageScreenshot: this.fullPageScreenshot, + waitFor: parseInt(this.waitFor), + }, + extractorOptions, + timeout: this.timeout, + }, + }); + + $.export("$summary", `Successfully scraped content from ${this.url}`); + return response; + }, +}; diff --git a/components/firecrawl/common/utils.mjs b/components/firecrawl/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/firecrawl/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/firecrawl/firecrawl.app.mjs b/components/firecrawl/firecrawl.app.mjs new file mode 100644 index 0000000000000..493b148e10d6f --- /dev/null +++ b/components/firecrawl/firecrawl.app.mjs @@ -0,0 +1,190 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "firecrawl", + propDefinitions: { + url: { + type: "string", + label: "URL", + description: "The URL to start crawling from.", + }, + crawlId: { + type: "string", + label: "Crawl ID", + description: "Identifier of a certain crawl operation.", + }, + includes: { + type: "string[]", + label: "Includes", + description: "URL patterns to include.", + }, + excludes: { + type: "string[]", + label: "Excludes", + description: "URL patterns to exclude.", + }, + generateImgAltText: { + type: "boolean", + label: "Generate Image Alt Text", + description: "Generate alt text for images using LLMs (must have a paid plan).", + }, + returnOnlyUrls: { + type: "boolean", + label: "Return Only URLs", + description: "If true, returns only the URLs as a list on the crawl status. Attention: the return response will be a list of URLs inside the data, not a list of documents.", + }, + maxDepth: { + type: "string", + label: "Max Depth", + description: "Maximum depth to crawl relative to the entered URL. A maxDepth of 0 scrapes only the entered URL. A maxDepth of 1 scrapes the entered URL and all pages one level deep. A maxDepth of 2 scrapes the entered URL and all pages up to two levels deep. Higher values follow the same pattern.", + }, + mode: { + type: "string", + label: "Mode", + description: "The crawling mode to use. Fast mode crawls 4x faster websites without sitemap, but may not be as accurate and shouldn't be used in heavy js-rendered websites.", + options: [ + "default", + "fast", + ], + }, + ignoreSitemap: { + type: "boolean", + label: "Ignore Sitemap", + description: "Ignore the website sitemap when crawling.", + }, + limit: { + type: "integer", + label: "Limit", + description: "Maximum number of pages to crawl.", + }, + allowBackwardCrawling: { + type: "boolean", + label: "Allow Backward Crawling", + description: "Enables the crawler to navigate from a specific URL to previously linked pages. For instance, from 'example.com/product/123' back to 'example.com/product'.", + }, + allowExternalContentLinks: { + type: "boolean", + label: "Allow External Content Links", + description: "Allows the crawler to follow links to external websites.", + }, + headers: { + type: "object", + label: "Headers", + description: "Headers to send with the request. Can be used to send cookies, user-agent, etc..", + }, + includeHtml: { + type: "boolean", + label: "Include HTML", + description: "Include the HTML version of the content on page.", + }, + includeRawHtml: { + type: "boolean", + label: "Include Raw HTML", + description: "Include the raw HTML content of the page.", + }, + onlyIncludeTags: { + type: "string[]", + label: "Only Include Tags", + description: "Only include tags, classes, and ids from the page in the final output. Example: 'script, .ad, #footer'.", + }, + onlyMainContent: { + type: "boolean", + label: "Only Main Content", + description: "Only return the main content of the page excluding headers, navs, footers, etc..", + }, + removeTags: { + type: "string[]", + label: "Remove Tags", + description: "Tags, classes, and ids to remove from the page. Example: 'script, .ad, #footer'.", + }, + replaceAllPathsWithAbsolutePaths: { + type: "boolean", + label: "Replace All Paths With Absolute Paths", + description: "Replace all relative paths with absolute paths for images and links.", + }, + screenshot: { + type: "boolean", + label: "Screenshot", + description: "Include a screenshot of the top of the page that you are scraping.", + }, + fullPageScreenshot: { + type: "boolean", + label: "Full Page Screenshot", + description: "Include a full page screenshot of the page that you are scraping.", + }, + waitFor: { + type: "string", + label: "Wait For", + description: "Wait x amount of milliseconds for the page to load to fetch content.", + }, + extractorMode: { + type: "string", + label: "Mode", + description: "The extraction mode to use. 'markdown': Returns the scraped markdown content, does not perform LLM extraction. 'llm-extraction': Extracts information from the cleaned and parsed content using LLM. 'llm-extraction-from-raw-html': Extracts information directly from the raw HTML using LLM. 'llm-extraction-from-markdown': Extracts information from the markdown content using LLM.", + options: [ + "markdown", + "llm-extraction", + "llm-extraction-from-raw-html", + "llm-extraction-from-markdown", + ], + }, + extractionPrompt: { + type: "string", + label: "Extraction Prompt", + description: "A prompt describing what information to extract from the page. LLM extraction modes.", + }, + extractionSchema: { + type: "object", + label: "Extraction Schema", + description: "The schema for the data to be extracted, required only for LLM extraction modes.", + }, + timeout: { + type: "integer", + label: "Timeout", + description: "Timeout in milliseconds for the request.", + }, + }, + methods: { + _baseUrl() { + return "https://api.firecrawl.dev/v0"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + crawl(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/crawl", + ...opts, + }); + }, + getCrawlStatus({ + crawlId, ...opts + }) { + return this._makeRequest({ + method: "GET", + path: `/crawl/status/${crawlId}`, + ...opts, + }); + }, + scrape(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/scrape", + ...opts, + }); + }, + }, +}; diff --git a/components/firecrawl/package.json b/components/firecrawl/package.json new file mode 100644 index 0000000000000..4a31c1e8fe84e --- /dev/null +++ b/components/firecrawl/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/firecrawl", + "version": "0.1.0", + "description": "Pipedream FireCrawl Components", + "main": "firecrawl.app.mjs", + "keywords": [ + "pipedream", + "firecrawl" + ], + "homepage": "https://pipedream.com/apps/firecrawl", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/firefish/actions/unsubscribe-email/unsubscribe-email.mjs b/components/firefish/actions/unsubscribe-email/unsubscribe-email.mjs new file mode 100644 index 0000000000000..2e3c55902b16c --- /dev/null +++ b/components/firefish/actions/unsubscribe-email/unsubscribe-email.mjs @@ -0,0 +1,54 @@ +import firefish from "../../firefish.app.mjs"; + +export default { + key: "firefish-unsubscribe-email", + name: "Unsubscribe Email", + description: "Removes a particular contact or candidate from all existing firefish email subscriptions. [See the documentatio](https://developer.firefishsoftware.com/#002bb8c0-0b41-4016-b33c-026a46b499b2)", + version: "0.0.1", + type: "action", + props: { + firefish, + email: { + type: "string", + label: "Email", + description: "The email address of the subscriber you want to remove", + }, + }, + async run({ $ }) { + const contacts = await this.firefish.searchContacts({ + $, + params: { + "email-address": this.email, + }, + }); + for (const contact of contacts) { + contact.EmailMarketing = false; + await this.firefish.updateContact({ + $, + contactId: contact.Ref, + data: contact, + }); + } + + const candidates = await this.firefish.searchCandidates({ + $, + params: { + "email-address": this.email, + }, + }); + for (const candidate of candidates) { + candidate.EmailMarketing = false; + await this.firefish.updateCandidate({ + $, + candidateId: candidate.Ref, + data: candidate, + }); + } + + $.export("$summary", `Successfully removed ${this.email} from email marketing`); + return { + contacts, + candidates, + }; + }, +}; diff --git a/components/firefish/firefish.app.mjs b/components/firefish/firefish.app.mjs new file mode 100644 index 0000000000000..e157fdaba7120 --- /dev/null +++ b/components/firefish/firefish.app.mjs @@ -0,0 +1,56 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "firefish", + propDefinitions: {}, + methods: { + _baseUrl() { + return "https://api.firefishsoftware.com/api/v1.0"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + searchContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts/search", + ...opts, + }); + }, + searchCandidates(opts = {}) { + return this._makeRequest({ + path: "/candidates/search", + ...opts, + }); + }, + updateContact({ + contactId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/contacts/${contactId}`, + ...opts, + }); + }, + updateCandidate({ + candidateId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/candidates/${candidateId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/firefish/package.json b/components/firefish/package.json new file mode 100644 index 0000000000000..b80e5bcd3d986 --- /dev/null +++ b/components/firefish/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/firefish", + "version": "0.1.0", + "description": "Pipedream Firefish Components", + "main": "firefish.app.mjs", + "keywords": [ + "pipedream", + "firefish" + ], + "homepage": "https://pipedream.com/apps/firefish", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} diff --git a/components/firefish/sources/common/base.mjs b/components/firefish/sources/common/base.mjs new file mode 100644 index 0000000000000..2dfc6391be083 --- /dev/null +++ b/components/firefish/sources/common/base.mjs @@ -0,0 +1,57 @@ +import firefish from "../../firefish.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + firefish, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastCreated() { + return this.db.get("lastCreated"); + }, + _setLastCreated(lastCreated) { + this.db.set("lastCreated", lastCreated); + }, + async processEvent(limit) { + const lastCreated = this._getLastCreated(); + const resourceFn = this.getResourceFn(); + const results = await resourceFn({ + params: { + "from-date": lastCreated && lastCreated.slice(0, 10), + }, + }); + if (!results?.length) { + return; + } + this._setLastCreated(results[0].Created); + if (limit && results.length > limit) { + results.length = limit; + } + results.reverse().forEach((result) => { + const meta = this.generateMeta(result); + this.$emit(result, meta); + }); + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/firefish/sources/new-candidate-created/new-candidate-created.mjs b/components/firefish/sources/new-candidate-created/new-candidate-created.mjs new file mode 100644 index 0000000000000..b788fc67f9d22 --- /dev/null +++ b/components/firefish/sources/new-candidate-created/new-candidate-created.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "firefish-new-candidate-created", + name: "New Candidate Created", + description: "Emit new event when a new candidate is created. [See the documentation](https://developer.firefishsoftware.com/#0dc51713-8397-4aaa-a85e-a66eb8f94d9d)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.firefish.searchCandidates; + }, + generateMeta(candidate) { + return { + id: candidate.Ref, + summary: `New Candidate ID: ${candidate.Ref}`, + ts: Date.parse(candidate.Created), + }; + }, + }, + sampleEmit, +}; diff --git a/components/firefish/sources/new-candidate-created/test-event.mjs b/components/firefish/sources/new-candidate-created/test-event.mjs new file mode 100644 index 0000000000000..0865176c4b4a2 --- /dev/null +++ b/components/firefish/sources/new-candidate-created/test-event.mjs @@ -0,0 +1,28 @@ +export default { + "Ref": 48889, + "FirstName": "Gerardo", + "Surname": "Smitho", + "DateOfBirth": null, + "JobTitle": null, + "EmailAddress": "gerard.firefish@gmail.com", + "Address": { + "Address1": null, + "Address2": null, + "Address3": null, + "Town": null, + "County": null, + "Country": null, + "PostCode": null + }, + "MobileNumber": "07595878736", + "HomeNumber": null, + "WorkNumber": null, + "IsArchived": false, + "CreatedBy": "Superuser Role", + "Created": "2024-02-09T09:39:01.383Z", + "UpdatedBy": "Gerardo Smitho", + "Updated": "2024-02-09T09:43:13.773Z", + "LastActionRef": 80198, + "LastActionName": "Advert Application", + "LastActionDate": "2024-02-09T09:51:56.007Z" +} \ No newline at end of file diff --git a/components/firefish/sources/new-contact-created/new-contact-created.mjs b/components/firefish/sources/new-contact-created/new-contact-created.mjs new file mode 100644 index 0000000000000..08ad4c6c8f710 --- /dev/null +++ b/components/firefish/sources/new-contact-created/new-contact-created.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "firefish-new-contact-created", + name: "New Contact Created", + description: "Emit new event when a new contact is created. [See the documentation](https://developer.firefishsoftware.com/#fcb38fee-8ad7-4aec-b1bd-ba7871e8258c)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.firefish.searchContacts; + }, + generateMeta(contact) { + return { + id: contact.Ref, + summary: `New Contact ID: ${contact.Ref}`, + ts: Date.parse(contact.Created), + }; + }, + }, + sampleEmit, +}; diff --git a/components/firefish/sources/new-contact-created/test-event.mjs b/components/firefish/sources/new-contact-created/test-event.mjs new file mode 100644 index 0000000000000..6075ad41917b7 --- /dev/null +++ b/components/firefish/sources/new-contact-created/test-event.mjs @@ -0,0 +1,20 @@ +export default { + "Ref": 48749, + "FirstName": "Kade", + "Surname": "Tran", + "Title": null, + "CompanyRef": null, + "CompanyName": null, + "JobTitle": "EMS Helicopter Pilot", + "EmailAddress": "Kade@dud.com", + "MobileNumber": null, + "WorkNumber": null, + "IsArchived": false, + "CreatedBy": "System Administrator", + "Created": "2020-03-24T08:53:00.830Z", + "UpdatedBy": "System Administrator", + "Updated": "2020-03-24T08:53:00.830Z", + "LastActionRef": 78075, + "LastActionName": "Create Contact - Manual", + "LastActionDate": "2020-03-24T08:53:00.847Z" +} \ No newline at end of file diff --git a/components/fireflies/README.md b/components/fireflies/README.md new file mode 100644 index 0000000000000..1e4536e22bed6 --- /dev/null +++ b/components/fireflies/README.md @@ -0,0 +1,11 @@ +# Overview + +The Fireflies API allows you to harness the power of AI to record, transcribe, and search across your voice conversations. With Pipedream, you can create automated workflows that leverage these capabilities to streamline communication, enhance collaboration, and ensure important insights from meetings are captured and actionable. By integrating with other apps, you can trigger actions, sync data, and build an array of powerful automations around your Fireflies data. + +# Example Use Cases + +- **Transcription Alerts to Slack**: Use the Fireflies API on Pipedream to monitor for new meeting transcriptions. When a new transcription is available, trigger a workflow that sends a customized alert with key details and a transcription link to a designated Slack channel. This keeps your team informed and ensures prompt attention to important meeting outcomes. + +- **Meeting Insights into CRM**: After a sales call is transcribed by Fireflies, trigger a Pipedream workflow that parses the transcription for actionable items, decisions, or follow-ups and logs this information into your CRM, like Salesforce. Automate the creation of tasks or opportunities based on the context of the conversation, improving sales tracking and accountability. + +- **Voice Command Issue Tracking**: Combine Fireflies with a voice interface like Google Assistant. After dictating a quick meeting note or a task through your voice assistant, have the recording sent to Fireflies for transcription. The Pipedream workflow can then interpret the text, create an issue in a tool like GitHub or JIRA, and assign it to the relevant team member, streamlining task creation and project management. diff --git a/components/fireflies/actions/find-meeting-by-id/find-meeting-by-id.mjs b/components/fireflies/actions/find-meeting-by-id/find-meeting-by-id.mjs new file mode 100644 index 0000000000000..71ea2aaa11ea8 --- /dev/null +++ b/components/fireflies/actions/find-meeting-by-id/find-meeting-by-id.mjs @@ -0,0 +1,32 @@ +import fireflies from "../../fireflies.app.mjs"; +import queries from "../../common/queries.mjs"; + +export default { + key: "fireflies-find-meeting-by-id", + name: "Find Meeting by ID", + description: "Locates a specific user meeting by its unique ID. [See the documentation](https://docs.fireflies.ai/graphql-api/query/transcript)", + version: "0.0.1", + type: "action", + props: { + fireflies, + meetingId: { + propDefinition: [ + fireflies, + "meetingId", + ], + }, + }, + async run({ $ }) { + const meeting = await this.fireflies.query({ + $, + data: { + query: queries.getTranscript, + variables: { + transcriptId: this.meetingId, + }, + }, + }); + $.export("$summary", `Successfully found meeting with ID: ${this.meetingId}`); + return meeting; + }, +}; diff --git a/components/fireflies/actions/find-recent-meeting/find-recent-meeting.mjs b/components/fireflies/actions/find-recent-meeting/find-recent-meeting.mjs new file mode 100644 index 0000000000000..c0946ab4c7895 --- /dev/null +++ b/components/fireflies/actions/find-recent-meeting/find-recent-meeting.mjs @@ -0,0 +1,45 @@ +import fireflies from "../../fireflies.app.mjs"; +import queries from "../../common/queries.mjs"; + +export default { + key: "fireflies-find-recent-meeting", + name: "Find Recent Meeting", + description: "Retrieves the most recent meeting for a user. [See the documentation](https://docs.fireflies.ai/graphql-api/query/user)", + version: "0.0.1", + type: "action", + props: { + fireflies, + userId: { + propDefinition: [ + fireflies, + "userId", + ], + }, + }, + async run({ $ }) { + const { data: { user: { recent_meeting: meetingId } } } = await this.fireflies.query({ + $, + data: { + query: queries.getUser, + variables: { + userId: this.userId, + }, + }, + }); + if (!meetingId) { + $.export("$summary", `No meeting found for user with ID ${this.userId}`); + return; + } + const meeting = await this.fireflies.query({ + $, + data: { + query: queries.getTranscript, + variables: { + transcriptId: meetingId, + }, + }, + }); + $.export("$summary", `Successfully fetched the most recent meeting for user with ID ${this.userId}`); + return meeting; + }, +}; diff --git a/components/fireflies/actions/upload-audio/upload-audio.mjs b/components/fireflies/actions/upload-audio/upload-audio.mjs new file mode 100644 index 0000000000000..791c1819aa2af --- /dev/null +++ b/components/fireflies/actions/upload-audio/upload-audio.mjs @@ -0,0 +1,63 @@ +import fireflies from "../../fireflies.app.mjs"; +import mutations from "../../common/mutations.mjs"; + +export default { + key: "fireflies-upload-audio", + name: "Upload Audio", + description: "Creates and stores a new meeting in Fireflies, allowing it to be transcribed and shared. [See the documentation](https://docs.fireflies.ai/graphql-api/mutation/upload-audio)", + version: "0.0.1", + type: "action", + props: { + fireflies, + url: { + type: "string", + label: "URL", + description: "The url of media file to be transcribed. It MUST be a valid https string and publicly accessible to enable us download the audio / video file. Double check to see if the media file is downloadable and that the link is not a preview link before making the request. The media file must be either of these formats - mp3, mp4, wav, m4a, ogg", + }, + title: { + type: "string", + label: "Title", + description: "Title or name of the meeting, this will be used to identify the transcribed file", + }, + webhookUrl: { + type: "string", + label: "Webhook URL", + description: "The URL we should send webhooks to when your transcript is complete", + optional: true, + }, + callbackWithRerun: { + type: "boolean", + label: "Callback With Rerun", + description: "Use the `$.flow.rerun` Node.js helper to rerun the step when the transcription is completed. Overrides the `webhookUrl` prop. This will increase execution time and credit usage as a result. [See the documentation(https://pipedream.com/docs/code/nodejs/rerun/#flow-rerun)", + optional: true, + }, + }, + async run({ $ }) { + let response; + const { run } = $.context; + if (run.runs === 1) { + let webhookUrl = this.webhookUrl; + if (this.callbackWithRerun) { + ({ resume_url: webhookUrl } = $.flow.rerun(600000, null, 1)); + } + response = await this.fireflies.query({ + $, + data: { + query: mutations.uploadAudio, + variables: { + input: { + url: this.url, + title: this.title, + webhook: webhookUrl, + }, + }, + }, + }); + } + if (run.runs > 1) { + response = run.callback_request.body; + } + $.export("$summary", "Successfully created and stored a new meeting"); + return response; + }, +}; diff --git a/components/fireflies/common/constants.mjs b/components/fireflies/common/constants.mjs new file mode 100644 index 0000000000000..6414d992bb568 --- /dev/null +++ b/components/fireflies/common/constants.mjs @@ -0,0 +1,5 @@ +const DEFAULT_LIMIT = 50; + +export default { + DEFAULT_LIMIT, +}; diff --git a/components/fireflies/common/mutations.mjs b/components/fireflies/common/mutations.mjs new file mode 100644 index 0000000000000..4660e8bafe639 --- /dev/null +++ b/components/fireflies/common/mutations.mjs @@ -0,0 +1,11 @@ +export default { + uploadAudio: ` + mutation($input: AudioUploadInput) { + uploadAudio(input: $input) { + success + title + message + } + } + `, +}; diff --git a/components/fireflies/common/queries.mjs b/components/fireflies/common/queries.mjs new file mode 100644 index 0000000000000..417833f5df48d --- /dev/null +++ b/components/fireflies/common/queries.mjs @@ -0,0 +1,83 @@ +export default { + listTranscripts: ` + query Transcripts { + transcripts { + id + title + } + } + `, + listTranscriptsByDate: ` + query Transcripts($date: Float) { + transcripts(date: $date) { + id + title + transcript_url + duration + date + audio_url + video_url + sentences { + text + } + calendar_id + summary { + action_items + keywords + outline + overview + shorthand_bullet + } + user { + user_id + name + } + } + } + `, + getTranscript: ` + query Transcript($transcriptId: String!) { + transcript(id: $transcriptId) { + id + title + transcript_url + duration + date + audio_url + video_url + sentences { + text + } + calendar_id + summary { + action_items + keywords + outline + overview + shorthand_bullet + } + user { + user_id + name + } + } + } + `, + listUsers: ` + { + users { + name + user_id + } + } + `, + getUser: ` + query User($userId: String!) { + user(id: $userId) { + name + user_id + recent_meeting + } + } + `, +}; diff --git a/components/fireflies/fireflies.app.mjs b/components/fireflies/fireflies.app.mjs new file mode 100644 index 0000000000000..49a2bcf3ebede --- /dev/null +++ b/components/fireflies/fireflies.app.mjs @@ -0,0 +1,80 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import queries from "./common/queries.mjs"; + +export default { + type: "app", + app: "fireflies", + propDefinitions: { + meetingId: { + type: "string", + label: "Meeting ID", + description: "The unique identifier for the meeting.", + async options({ page }) { + const limit = constants.DEFAULT_LIMIT; + const { data: { transcripts } } = await this.query({ + data: { + query: queries.listTranscripts, + variables: { + limit, + skip: page * limit, + }, + }, + }); + return transcripts?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })) || []; + }, + }, + userId: { + type: "string", + label: "User ID", + description: "The unique identifier for the user.", + async options({ page }) { + const limit = constants.DEFAULT_LIMIT; + const { data: { users } } = await this.query({ + data: { + query: queries.listUsers, + variables: { + limit, + skip: page * limit, + }, + }, + }); + return users?.map(({ + user_id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.fireflies.ai/graphql"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl(), + headers: { + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + query(opts = {}) { + return this._makeRequest({ + method: "POST", + ...opts, + }); + }, + }, +}; diff --git a/components/fireflies/package.json b/components/fireflies/package.json new file mode 100644 index 0000000000000..02461c91d3989 --- /dev/null +++ b/components/fireflies/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/fireflies", + "version": "0.1.0", + "description": "Pipedream Fireflies Components", + "main": "fireflies.app.mjs", + "keywords": [ + "pipedream", + "fireflies" + ], + "homepage": "https://pipedream.com/apps/fireflies", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/fireflies/sources/new-meeting-created/new-meeting-created.mjs b/components/fireflies/sources/new-meeting-created/new-meeting-created.mjs new file mode 100644 index 0000000000000..2d7d1f9379691 --- /dev/null +++ b/components/fireflies/sources/new-meeting-created/new-meeting-created.mjs @@ -0,0 +1,77 @@ +import fireflies from "../../fireflies.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import constants from "../../common/constants.mjs"; +import queries from "../../common/queries.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "fireflies-new-meeting-created", + name: "New Meeting Created", + description: "Emit new event when a meeting with transcripts is created", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + fireflies, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || this.oneDayAgo; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + oneDayAgo() { + return Date.now() - 24 * 60 * 60 * 1000; + }, + emitEvent(result) { + const meta = this.generateMeta(result); + this.$emit(result, meta); + }, + generateMeta(result) { + return { + id: result.id, + summary: `New Meeting: ${result.title}`, + ts: Date.parse(result.date), + }; + }, + }, + async run() { + const lastDate = this._getLastDate(); + const limit = constants.DEFAULT_LIMIT; + const variables = { + date: lastDate, + limit, + skip: 0, + }; + let total; + const results = []; + + do { + const { data: { transcripts } } = await this.fireflies.query({ + data: { + query: queries.listTranscriptsByDate, + variables, + }, + }); + results.push(...transcripts); + total = transcripts?.length; + variables.skip += limit; + } while (total === limit); + + if (!results.length) { + return; + } + + this._setLastDate(results[0].date); + results.reverse().forEach((result) => this.emitEvent(result)); + }, + sampleEmit, +}; diff --git a/components/fireflies/sources/new-meeting-created/test-event.mjs b/components/fireflies/sources/new-meeting-created/test-event.mjs new file mode 100644 index 0000000000000..0a9c82e3a516e --- /dev/null +++ b/components/fireflies/sources/new-meeting-created/test-event.mjs @@ -0,0 +1,22 @@ +export default { + "id": "OvwNiwrXXWzo6q1v", + "title": "file_example", + "transcript_url": "https://app.fireflies.ai/view/OvwNiwrXXWzo6q1v", + "duration": 0.700952410697937, + "date": 1713201000000, + "audio_url": "https://rtmp-server-ff.s3.amazonaws.com/OvwNiwrXXWzo6q1v/audio.mp3?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAWZAJLUBIVRJ35B6I%2F20240415%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20240415T180424Z&X-Amz-Expires=216000&X-Amz-Signature=efdb3a278052264bcbeeeb88b3c7afaf32ccc438b750356ab9934a35e34ddc7d&X-Amz-SignedHeaders=host", + "video_url": null, + "sentences": null, + "calendar_id": null, + "summary": { + "action_items": "", + "keywords": [], + "outline": "", + "overview": "", + "shorthand_bullet": "" + }, + "user": { + "user_id": "VsFnU96JJB", + "name": "Test User" + } +} \ No newline at end of file diff --git a/components/firmalyzer_iotvas/README.md b/components/firmalyzer_iotvas/README.md index be7b2bb2bba71..22b5ae1d867c3 100644 --- a/components/firmalyzer_iotvas/README.md +++ b/components/firmalyzer_iotvas/README.md @@ -1,12 +1,11 @@ # Overview -The Firmalyzer IoTVAS API allows you to build a variety of applications that -can analyze firmware for vulnerabilities. Some examples of applications that -can be built using the Firmalyzer IoTVAS API include: - -- A vulnerability scanner that can check firmware for known vulnerabilities and - report them to the user. -- A tool that can automatically update firmware to the latest version, in order - to patch vulnerabilities. -- A system that can monitor firmware for changes and report back to the user if - any changes are detected. +Firmalyzer IoTVAS API provides a platform for assessing the security of IoT devices. By using this API, you can automate the analysis of firmware, uncover vulnerabilities, check for outdated software, and verify compliance with security standards. Integrating Firmalyzer with Pipedream allows for the seamless incorporation of IoT security checks into broader automation workflows, which can facilitate continuous monitoring, alerting, and reporting within your infrastructure. + +# Example Use Cases + +- **Automated Vulnerability Scanning Workflow**: Trigger a daily Pipedream workflow that sends your IoT device firmware to Firmalyzer IoTVAS API for analysis. Upon detection of new vulnerabilities, use the SendGrid app to notify your security team, and log the findings in a Google Sheet for record-keeping. + +- **CI/CD Pipeline Security Integration**: Integrate Firmalyzer IoTVAS API into your existing CI/CD pipeline on Pipedream. Whenever new firmware is ready for deployment, automatically scan it for security issues. If any high-risk vulnerabilities are found, halt the deployment process and open a GitHub issue for your development team to address. + +- **Compliance Monitoring and Reporting**: Use Pipedream to periodically check your IoT devices against the latest security standards with Firmalyzer IoTVAS API. Store compliance status in a Pipedream data store and if a device falls out of compliance, trigger an alert to your Slack channel and create a detailed report in a Markdown file for review by your compliance team. diff --git a/components/firmao/README.md b/components/firmao/README.md new file mode 100644 index 0000000000000..01e4246012d86 --- /dev/null +++ b/components/firmao/README.md @@ -0,0 +1,11 @@ +# Overview + +The Firmao API allows for the integration of Firmao's CRM and ERP features into Pipedream's serverless platform, enabling the automation of tasks such as managing contacts, projects, tasks, invoices, and timesheets. By leveraging Pipedream's capabilities, users can create custom workflows that respond to events in Firmao, or orchestrate actions between Firmao and other apps to streamline business processes. + +# Example Use Cases + +- **Sync New Contacts to a Google Sheet**: Whenever a new contact is added in Firmao, this workflow automatically adds the contact's details to a specified Google Sheet. This helps in maintaining an updated backup and easy sharing with team members who prefer working within spreadsheets. + +- **Send Slack Notifications for New Invoices**: Create a workflow that listens for new invoices created in Firmao and sends a notification with invoice details to a Slack channel. This keeps the finance team or the entire company informed about billing activities in real-time. + +- **Automate Project Creation from GitHub Issues**: When a new GitHub issue is tagged with a specific label, trigger a workflow that creates a corresponding project or task in Firmao. This bridges the gap between development tracking in GitHub and project management within Firmao. diff --git a/components/fiserv/fiserv.app.mjs b/components/fiserv/fiserv.app.mjs new file mode 100644 index 0000000000000..ed79ce5e839c8 --- /dev/null +++ b/components/fiserv/fiserv.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "fiserv", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/fiserv/package.json b/components/fiserv/package.json new file mode 100644 index 0000000000000..b1d6164ef173b --- /dev/null +++ b/components/fiserv/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/fiserv", + "version": "0.0.1", + "description": "Pipedream Fiserv Components", + "main": "fiserv.app.mjs", + "keywords": [ + "pipedream", + "fiserv" + ], + "homepage": "https://pipedream.com/apps/fiserv", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/fitbit/README.md b/components/fitbit/README.md index b6a7e18b1ecc2..967e7d72ef01e 100644 --- a/components/fitbit/README.md +++ b/components/fitbit/README.md @@ -1,8 +1,11 @@ # Overview -With the Fitbit API, you can build applications that: +The Fitbit API offers a window into the rich data from Fitbit devices and user profiles, allowing you to access metrics such as step count, sleep quality, heart rate, and more. With these insights, you can create personalized health dashboards, automate fitness challenges, synchronize health data with other apps, or even build custom notifications for user activity milestones. -- Retrieve Fitbit data for a user -- Track and monitor a user's fitness and activity data -- Analyze a user's fitness and activity data -- Display data from Fitbit on a third-party website or application +# Example Use Cases + +- **Sync Fitbit Data to Google Sheets**: Automatically update a Google Sheets spreadsheet with your daily activity stats. By leveraging Pipedream's ability to connect with Google Sheets, you can regularly import your step count, sleep data, and other fitness metrics into a sheet for analysis and record-keeping. + +- **Custom Email or SMS Reminders Based on Activity**: Set up a workflow that sends you an email or SMS when you haven't reached a certain activity threshold by a specific time of the day. By integrating with SendGrid for email or Twilio for SMS services on Pipedream, you can encourage users to stay active and hit their daily goals. + +- **Automated Social Media Updates for Achievements**: Share your fitness achievements automatically on social media platforms like Twitter or Facebook when you reach milestones like 10,000 steps. By hooking into Fitbit's API to monitor achievements and using Pipedream's integration with social media services, you can celebrate your successes and motivate others. diff --git a/components/fitbit/package.json b/components/fitbit/package.json new file mode 100644 index 0000000000000..27dfd2dccd54d --- /dev/null +++ b/components/fitbit/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/fitbit", + "version": "0.6.0", + "description": "Pipedream fitbit Components", + "main": "fitbit.app.mjs", + "keywords": [ + "pipedream", + "fitbit" + ], + "homepage": "https://pipedream.com/apps/fitbit", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/fivetran/README.md b/components/fivetran/README.md new file mode 100644 index 0000000000000..699d8675ddad8 --- /dev/null +++ b/components/fivetran/README.md @@ -0,0 +1,11 @@ +# Overview + +The Fivetran API enables automated, effortless replication of data from various sources into a cloud warehouse. By leveraging Fivetran in Pipedream, you can programmatically manage your Fivetran connectors, set up and control data pipelines, and trigger data syncs. Pipedream's serverless platform allows for the creation of workflows that can respond to HTTP requests, process events on a schedule, and interact with numerous other services to create complex automation solutions. + +# Example Use Cases + +- **Automated Connector Management**: Build a workflow that listens for webhooks from your application to trigger the creation or updating of Fivetran connectors whenever there are changes in your data sources or schema. + +- **Dynamic Sync Triggering**: Set up a scheduled workflow to check the status of Fivetran connectors and trigger syncs, ensuring that your data warehouse always has the latest data without manual intervention. + +- **Alerting and Monitoring**: Integrate Fivetran with Slack using Pipedream, to send notifications to a Slack channel when a data sync completes or if there are any issues with the connectors, keeping your team informed in real-time. diff --git a/components/fixer_io/README.md b/components/fixer_io/README.md new file mode 100644 index 0000000000000..bf3abf695ca5c --- /dev/null +++ b/components/fixer_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The Fixer API provides real-time exchange rate data for various currencies, making it invaluable for financial applications, data analysis, and e-commerce platforms. Within Pipedream, you can harness this API to create automated workflows that trigger on schedules or events, process currency data, and connect with other services to perform actions such as updating pricing models, sending alerts, or syncing financial data across platforms. + +# Example Use Cases + +- **Currency Conversion Alerts**: Set up a Pipedream workflow that monitors exchange rates for a specific currency pair using the Fixer API. Schedule the workflow to run at regular intervals, and if the exchange rate hits a certain threshold, trigger an alert via email, SMS, or a messaging app like Slack. + +- **Automated Pricing Adjustments**: Create a workflow for an e-commerce site where product prices in different currencies adjust automatically based on the latest exchange rates. The Fixer API fetches current rates, which are then applied to the prices stored in a data store or database, ensuring all product listings stay competitive and reflect current market conditions. + +- **Financial Dashboard Updates**: Build a Pipedream workflow that pulls the latest exchange rate data from the Fixer API at the start of each business day. This data can then be sent to a Google Sheet or a dashboard app like Geckoboard, providing a daily digest of currency fluctuations for financial analysis or reporting purposes. diff --git a/components/fixer_io/fixer_io.app.mjs b/components/fixer_io/fixer_io.app.mjs new file mode 100644 index 0000000000000..360a8ce94106e --- /dev/null +++ b/components/fixer_io/fixer_io.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "fixer_io", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/fixer_io/package.json b/components/fixer_io/package.json new file mode 100644 index 0000000000000..ff9dfcc3febf9 --- /dev/null +++ b/components/fixer_io/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/fixer_io", + "version": "0.0.1", + "description": "Pipedream Fixer Components", + "main": "fixer_io.app.mjs", + "keywords": [ + "pipedream", + "fixer_io" + ], + "homepage": "https://pipedream.com/apps/fixer_io", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/flash_by_velora_ai/actions/add-feedback/add-feedback.mjs b/components/flash_by_velora_ai/actions/add-feedback/add-feedback.mjs new file mode 100644 index 0000000000000..54a22473783eb --- /dev/null +++ b/components/flash_by_velora_ai/actions/add-feedback/add-feedback.mjs @@ -0,0 +1,128 @@ +import app from "../../flash_by_velora_ai.app.mjs"; + +export default { + key: "flash_by_velora_ai-add-feedback", + name: "Add Feedback", + description: "Adds customer feedback.", + version: "0.0.1", + type: "action", + props: { + app, + feedback: { + type: "string", + label: "Feedback", + description: "Actual text customer feedback.", + }, + title: { + type: "string", + label: "Title", + description: "Title of the customer feedback, if any.", + optional: true, + }, + source: { + type: "string", + label: "Source", + description: "Source where the feedback was received, for example, `GitHub`, `Slack`, etc.", + optional: true, + }, + upvote: { + type: "integer", + label: "Upvote", + description: "Count of upvotes for the feedback.", + optional: true, + }, + downvote: { + type: "integer", + label: "Downvote", + description: "Count of downvotes for the feedback.", + optional: true, + }, + contactName: { + type: "string", + label: "Contact Name", + description: "Name of the customer contact who provided the feedback.", + optional: true, + }, + contactEmail: { + type: "string", + label: "Contact Email", + description: "Email of the customer contact who provided the feedback.", + optional: true, + }, + handle: { + type: "string", + label: "Handle", + description: "Platform specific customer contact handle. Eg. `@pipedream`.", + optional: true, + }, + handleType: { + type: "string", + label: "Handle Type", + description: "Platform which the contact handle belongs to. Eg. `Twitter`, `GitHub`, etc.", + optional: true, + }, + companyName: { + type: "string", + label: "Company Name", + description: "Name of customer company to which the customer contact who provided feedback belongs.", + optional: true, + }, + companyDomain: { + type: "string", + label: "Company Domain", + description: "Email domain of customer company to which the customer contact who provided feedback belongs. Eg. `pipedream.com`.", + optional: true, + }, + feedbackAt: { + type: "string", + label: "Feedback At", + description: "Date and time, when the feedback was received. Eg. `2021-01-01T00:00:00`.", + optional: true, + }, + }, + methods: { + addFeedback(args = {}) { + return this.app.post({ + path: "/add-feedback", + ...args, + }); + }, + }, + async run({ $ }) { + const { + addFeedback, + feedback, + title, + source, + upvote, + downvote, + contactName, + contactEmail, + handle, + handleType, + companyName, + companyDomain, + feedbackAt, + } = this; + + const response = await addFeedback({ + $, + data: { + feedback, + title, + source, + upvote, + downvote, + contact_name: contactName, + contact_email: contactEmail, + handle, + handle_type: handleType, + company_name: companyName, + company_domain: companyDomain, + feedback_at: feedbackAt, + }, + }); + $.export("$summary", "Successfully added feedback."); + return response; + }, +}; diff --git a/components/flash_by_velora_ai/actions/upload-transcript/upload-transcript.mjs b/components/flash_by_velora_ai/actions/upload-transcript/upload-transcript.mjs new file mode 100644 index 0000000000000..06652d748235e --- /dev/null +++ b/components/flash_by_velora_ai/actions/upload-transcript/upload-transcript.mjs @@ -0,0 +1,55 @@ +import app from "../../flash_by_velora_ai.app.mjs"; + +export default { + key: "flash_by_velora_ai-upload-transcript", + name: "Upload Transcript", + description: "Upload a meeting transcript.", + version: "0.0.1", + type: "action", + props: { + app, + title: { + type: "string", + label: "Meeting Title", + description: "Title of the meeting.", + }, + fileUrl: { + type: "string", + label: "File URL", + description: "Transcript file (supported types: **text/plain**, **pdf**, **vtt**) or transcript text.", + }, + sourceType: { + type: "string", + label: "Source Type", + description: "Type of the source system of the file, for example, `Google Drive`, `Fireflies.ai`, etc.", + }, + }, + methods: { + uploadTranscript(args = {}) { + return this.app.post({ + path: "/upload-transcript", + ...args, + }); + }, + }, + async run({ $ }) { + const { + uploadTranscript, + title, + fileUrl, + sourceType, + } = this; + + const response = await uploadTranscript({ + $, + data: { + title, + file_url: fileUrl, + source_type: sourceType, + }, + }); + + $.export("$summary", "Successfully uploaded transcript."); + return response; + }, +}; diff --git a/components/flash_by_velora_ai/flash_by_velora_ai.app.mjs b/components/flash_by_velora_ai/flash_by_velora_ai.app.mjs new file mode 100644 index 0000000000000..73e1649822be9 --- /dev/null +++ b/components/flash_by_velora_ai/flash_by_velora_ai.app.mjs @@ -0,0 +1,34 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "flash_by_velora_ai", + methods: { + getUrl(path) { + return `https://flash-api.velora.ai/v1/api${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Content-Type": "application/json", + "Accept": "application/json", + "x-api-key": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + }, +}; diff --git a/components/flash_by_velora_ai/package.json b/components/flash_by_velora_ai/package.json new file mode 100644 index 0000000000000..b97df468e0721 --- /dev/null +++ b/components/flash_by_velora_ai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/flash_by_velora_ai", + "version": "0.1.0", + "description": "Pipedream Flash (by Velora AI) Components", + "main": "flash_by_velora_ai.app.mjs", + "keywords": [ + "pipedream", + "flash_by_velora_ai" + ], + "homepage": "https://pipedream.com/apps/flash_by_velora_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" + } +} diff --git a/components/flexie/README.md b/components/flexie/README.md index 539dfdd73717d..59f6d65377d2c 100644 --- a/components/flexie/README.md +++ b/components/flexie/README.md @@ -1,12 +1,11 @@ # Overview -With Flexie, you can easily create responsive, fluid layouts without having to -worry about media queries or pixel measurements. You can also create complex -animations and interactions using just a few lines of code. +The Flexie API offers a powerful way to leverage customer relationship management (CRM) functionality within automated workflows. With the Flexie API, you can create, retrieve, update, and delete records, such as leads, contacts, and deals. You can also automate communication with leads via email or SMS, manage tasks and appointments, and create custom workflows to streamline your sales process. -Here are some examples of what you can build with Flexie: +# Example Use Cases -- A responsive website or web application -- A fluid user interface that adapts to different screen sizes -- Complex animations and interactions -- A layout that rearranges itself based on user interactions +- **Lead Score Update to Email Notification**: When a lead's score reaches a certain threshold in Flexie, trigger an email notification to the sales team using the Gmail app on Pipedream. This instantly informs your sales reps about high-potential leads that require immediate attention. + +- **New Deal Alert via SMS**: Set up a workflow where a new deal creation in Flexie automatically sends an SMS through the Twilio app to the assigned salesperson. This ensures that they are instantly aware of new opportunities to follow up on, no matter where they are. + +- **Contact Sync to Google Sheets**: Maintain an up-to-date list of contacts by syncing new or updated contact records from Flexie to a Google Sheets document. This workflow can be used for reporting, auditing, or as an easy-to-access contact directory for other departments. diff --git a/components/flexie/package.json b/components/flexie/package.json new file mode 100644 index 0000000000000..af157f7195a04 --- /dev/null +++ b/components/flexie/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/flexie", + "version": "0.6.0", + "description": "Pipedream flexie Components", + "main": "flexie.app.mjs", + "keywords": [ + "pipedream", + "flexie" + ], + "homepage": "https://pipedream.com/apps/flexie", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/flexisign/actions/send-document-using-template/send-document-using-template.mjs b/components/flexisign/actions/send-document-using-template/send-document-using-template.mjs new file mode 100644 index 0000000000000..7b756883e9b9c --- /dev/null +++ b/components/flexisign/actions/send-document-using-template/send-document-using-template.mjs @@ -0,0 +1,67 @@ +import { snakeCaseToTitleCase } from "../../common/utils.mjs"; +import flexisign from "../../flexisign.app.mjs"; + +export default { + key: "flexisign-send-document-using-template", + name: "Send Document Using Template", + description: "Sends a signature request to the specified recipients for a document generated from a template. [See the documentation](https://flexisign.io/app/integrations/flexisignapi)", + version: "0.0.1", + type: "action", + props: { + flexisign, + templateId: { + propDefinition: [ + flexisign, + "templateId", + ], + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.templateId) { + const { data: { bodyStructure } } = await this.flexisign.getTemplateDetails({ + params: { + templateId: this.templateId, + }, + }); + + for (const [ + key, + value, + ] of Object.entries(bodyStructure)) { + if ([ + "templateId", + "recipientsCount", + ].includes(key)) continue; + + const title = snakeCaseToTitleCase(key); + props[key] = { + type: typeof value === "number" + ? "integer" + : "string", + label: title, + description: title, + default: typeof value === "number" + ? value + : undefined, + }; + } + } + return props; + }, + async run({ $ }) { + const { + flexisign, + ...data + } = this; + + const response = await flexisign.sendSignatureRequest({ + $, + data, + }); + + $.export("$summary", `Signature request sent for template ID: ${this.templateId}`); + return response; + }, +}; diff --git a/components/flexisign/common/utils.mjs b/components/flexisign/common/utils.mjs new file mode 100644 index 0000000000000..8b11b6b3ecf24 --- /dev/null +++ b/components/flexisign/common/utils.mjs @@ -0,0 +1,4 @@ +export const snakeCaseToTitleCase = (s) => + s.replace(/^_*(.)|_+(.)/g, (s, c, d) => c + ? c.toUpperCase() + : " " + d.toUpperCase()); diff --git a/components/flexisign/flexisign.app.mjs b/components/flexisign/flexisign.app.mjs new file mode 100644 index 0000000000000..6508323c4305f --- /dev/null +++ b/components/flexisign/flexisign.app.mjs @@ -0,0 +1,60 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "flexisign", + propDefinitions: { + templateId: { + type: "string", + label: "Template ID", + description: "The ID of the template to generate the document from", + async options() { + const { data: { list } } = await this.listTemplates(); + return list.map(({ + _id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.flexisign.io/v1"; + }, + _headers() { + return { + "api-key": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listTemplates(opts = {}) { + return this._makeRequest({ + path: "/templates/all", + ...opts, + }); + }, + getTemplateDetails(opts = {}) { + return this._makeRequest({ + path: "/template", + ...opts, + }); + }, + sendSignatureRequest(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/template/create-document", + ...opts, + }); + }, + }, +}; diff --git a/components/flexisign/package.json b/components/flexisign/package.json new file mode 100644 index 0000000000000..1ab1776a0f90f --- /dev/null +++ b/components/flexisign/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/flexisign", + "version": "0.1.0", + "description": "Pipedream FlexiSign Components", + "main": "flexisign.app.mjs", + "keywords": [ + "pipedream", + "flexisign" + ], + "homepage": "https://pipedream.com/apps/flexisign", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/flexmail/README.md b/components/flexmail/README.md new file mode 100644 index 0000000000000..ce004c8967da4 --- /dev/null +++ b/components/flexmail/README.md @@ -0,0 +1,11 @@ +# Overview + +Flexmail API offers a way to automate your email marketing campaigns by allowing you to manage contacts, send out emails, and track results. With Pipedream's ability to integrate with hundreds of services, you can set up complex workflows that respond to events from various apps by updating contact lists, sending personalized content, or triggering sequences of marketing actions based on user behavior or data changes. + +# Example Use Cases + +- **Automate Email Campaigns Based on E-Commerce Activity**: When a new order is placed in Shopify, use Pipedream to add that customer to a specific segment in Flexmail and trigger an automated email sequence, such as a thank you message, product recommendations, or a request for review. + +- **Sync New Subscribers Across Platforms**: Upon a new subscriber being added to a Wordpress site, Pipedream can capture that event and automatically add the subscriber to a Flexmail mailing list, ensuring your marketing efforts are consistent across all platforms. + +- **Dynamic Email Content Based on User Behavior**: If a user watches a webinar hosted on Zoom, Pipedream could trigger a workflow that sends a follow-up email via Flexmail with additional resources or calls to action personalized to the content of the webinar they attended. diff --git a/components/flipando/actions/get-all-apps/get-all-apps.mjs b/components/flipando/actions/get-all-apps/get-all-apps.mjs new file mode 100644 index 0000000000000..22cda5ad3e751 --- /dev/null +++ b/components/flipando/actions/get-all-apps/get-all-apps.mjs @@ -0,0 +1,21 @@ +import flipando from "../../flipando.app.mjs"; + +export default { + key: "flipando-get-all-apps", + name: "Get All Apps", + description: "Fetches a list of all apps that the user had created within flipando. [See the documentation](https://flipandoai.notion.site/Flipando-ai-API-Integration-Guide-6b508cfe1a5d4a249d20b926eac3a1d7#36b02715e5f440c9b21952b668e0e70c)", + version: "0.0.1", + type: "action", + props: { + flipando, + }, + async run({ $ }) { + const { results } = await this.flipando.listApps({ + $, + }); + $.export("$summary", `Fetched ${results.length} app${results.length === 1 + ? "" + : "s"}.`); + return results; + }, +}; diff --git a/components/flipando/actions/get-task/get-task.mjs b/components/flipando/actions/get-task/get-task.mjs new file mode 100644 index 0000000000000..c7ce500ee98ee --- /dev/null +++ b/components/flipando/actions/get-task/get-task.mjs @@ -0,0 +1,25 @@ +import flipando from "../../flipando.app.mjs"; + +export default { + key: "flipando-get-task", + name: "Get Task", + description: "Fetches data related to a specific task that is currently executed or had been executed previously. [See the documentation](https://flipandoai.notion.site/Flipando-ai-API-Integration-Guide-6b508cfe1a5d4a249d20b926eac3a1d7#36b02715e5f440c9b21952b668e0e70c)", + version: "0.0.1", + type: "action", + props: { + flipando, + taskId: { + type: "string", + label: "Task ID", + description: "The ID of the task to retrieve", + }, + }, + async run({ $ }) { + const response = await this.flipando.getTask({ + $, + taskId: this.taskId, + }); + $.export("$summary", `Successfully retrieved task ${this.taskId}`); + return response; + }, +}; diff --git a/components/flipando/actions/run-app/run-app.mjs b/components/flipando/actions/run-app/run-app.mjs new file mode 100644 index 0000000000000..858953193de2a --- /dev/null +++ b/components/flipando/actions/run-app/run-app.mjs @@ -0,0 +1,102 @@ +import flipando from "../../flipando.app.mjs"; +import fs from "fs"; +import FormData from "form-data"; + +export default { + key: "flipando-run-app", + name: "Run App", + description: "Executes a chosen app within Flipando. Returns a 'task_id' to be used in fetching the outcome of this action. [See the documentation]([See the documentation](https://flipandoai.notion.site/Flipando-ai-API-Integration-Guide-6b508cfe1a5d4a249d20b926eac3a1d7#36b02715e5f440c9b21952b668e0e70c))", + version: "0.0.1", + type: "action", + props: { + flipando, + appId: { + propDefinition: [ + flipando, + "appId", + ], + reloadProps: true, + }, + waitForCompletion: { + type: "boolean", + label: "Wait for Completion", + description: "Set to `true` to poll the API in 3-second intervals until the task is completed", + optional: true, + }, + }, + async additionalProps() { + const props = {}; + if (!this.appId) { + return props; + } + const { + results: { + inputs, has_docs: hasDocs, + }, + } = await this.flipando.getApp({ + appId: this.appId, + }); + for (const input of inputs) { + props[input.name] = { + type: "string", + label: input.name, + description: `Example: ${input.value}`, + }; + } + if (hasDocs) { + props.filePath = { + type: "string", + label: "File Path", + description: "The path to a document file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#the-tmp-directory).", + }; + props.fileDescription = { + type: "string", + label: "File Description", + description: "Description of the file being uploaded", + }; + } + return props; + }, + async run({ $ }) { + const { + flipando, + appId, + waitForCompletion, + filePath, + fileDescription, + ...inputs + } = this; + + let data = new FormData(); + data.append("inputs_data", JSON.stringify(inputs)); + if (filePath) { + data.append("file_description", fileDescription); + const path = filePath.includes("tmp/") + ? filePath + : `/tmp/${filePath}`; + data.append("file", fs.createReadStream(path)); + } + + let response = await flipando.executeApp({ + $, + appId, + data, + headers: data.getHeaders(), + }); + + if (waitForCompletion) { + const { results: { id: taskId } } = response; + const timer = (ms) => new Promise((res) => setTimeout(res, ms)); + while (response?.code !== 200) { + response = await flipando.getTask({ + $, + taskId, + }); + await timer(3000); + } + } + + $.export("$summary", `Successfully executed app with ID: ${appId}`); + return response; + }, +}; diff --git a/components/flipando/flipando.app.mjs b/components/flipando/flipando.app.mjs new file mode 100644 index 0000000000000..78684e69fca82 --- /dev/null +++ b/components/flipando/flipando.app.mjs @@ -0,0 +1,74 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "flipando", + propDefinitions: { + appId: { + type: "string", + label: "App ID", + description: "The ID of the app to execute", + async options() { + const { results } = await this.listApps(); + return results?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://flipando-backend.herokuapp.com/api"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, + }, + ...otherOpts, + }); + }, + executeApp({ + appId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/v1/integrations/applications/${appId}/completion`, + ...opts, + }); + }, + getTask({ + taskId, ...opts + }) { + return this._makeRequest({ + path: `/v2/integrations/tasks/${taskId}`, + ...opts, + }); + }, + getApp({ + appId, ...opts + }) { + return this._makeRequest({ + path: `/v2/integrations/applications/${appId}`, + ...opts, + }); + }, + listApps(opts = {}) { + return this._makeRequest({ + path: "/v2/integrations/applications", + ...opts, + }); + }, + }, +}; diff --git a/components/flipando/package.json b/components/flipando/package.json new file mode 100644 index 0000000000000..b6166e9da373c --- /dev/null +++ b/components/flipando/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/flipando", + "version": "0.1.0", + "description": "Pipedream Flipando Components", + "main": "flipando.app.mjs", + "keywords": [ + "pipedream", + "flipando" + ], + "homepage": "https://pipedream.com/apps/flipando", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5", + "form-data": "^4.0.0" + } +} diff --git a/components/flippingbook/actions/create-flipbook/create-flipbook.mjs b/components/flippingbook/actions/create-flipbook/create-flipbook.mjs new file mode 100644 index 0000000000000..4e096e8cccf1f --- /dev/null +++ b/components/flippingbook/actions/create-flipbook/create-flipbook.mjs @@ -0,0 +1,78 @@ +import flippingbook from "../../flippingbook.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; +import fs from "fs"; + +export default { + key: "flippingbook-create-flipbook", + name: "Create Flipbook", + description: "Generates a new flipbook from an input PDF file. [See the documentation](https://apidocs.flippingbook.com/#create-a-new-publication-possibly-attaching-a-new-source-file)", + version: "0.0.1", + type: "action", + props: { + flippingbook, + name: { + propDefinition: [ + flippingbook, + "name", + ], + }, + info: { + propDefinition: [ + flippingbook, + "info", + ], + }, + fileUrl: { + propDefinition: [ + flippingbook, + "fileUrl", + ], + }, + filePath: { + propDefinition: [ + flippingbook, + "filePath", + ], + }, + filename: { + propDefinition: [ + flippingbook, + "filename", + ], + }, + description: { + propDefinition: [ + flippingbook, + "description", + ], + }, + }, + async run({ $ }) { + if ((!this.fileUrl && !this.filePath) || (this.fileUrl && this.filePath)) { + throw new ConfigurationError("Please provide exactly one of File URL or File Path"); + } + const data = { + filename: this.filename, + name: this.name, + description: this.description, + }; + if (this.fileUrl) { + data.url = this.fileUrl; + } else { + const content = fs.readFileSync(this.filePath.includes("tmp/") + ? this.filePath + : `/tmp/${this.filePath}`); + data.data = content.toString("base64"); + } + + const response = await this.flippingbook.createFlipbook({ + $, + data, + }); + + if (!response.error) { + $.export("$summary", `Successfully created flipbook with ID: ${response.id}`); + } + return response; + }, +}; diff --git a/components/flippingbook/actions/find-flipbook-by-title/find-flipbook-by-title.mjs b/components/flippingbook/actions/find-flipbook-by-title/find-flipbook-by-title.mjs new file mode 100644 index 0000000000000..05e17d7d47d3a --- /dev/null +++ b/components/flippingbook/actions/find-flipbook-by-title/find-flipbook-by-title.mjs @@ -0,0 +1,29 @@ +import flippingbook from "../../flippingbook.app.mjs"; + +export default { + key: "flippingbook-find-flipbook-by-title", + name: "Find Flipbook by Title", + description: "Locates a specific flipbook using the provided title. [See the documentation](https://apidocs.flippingbook.com/#list-filtered-and-or-paged-publications-in-the-account)", + version: "0.0.1", + type: "action", + props: { + flippingbook, + title: { + type: "string", + label: "Title", + description: "Title of the flipbook. In order to match, the publication name must contain the exact value", + }, + }, + async run({ $ }) { + const { publications } = await this.flippingbook.listFlipbooks({ + $, + params: { + query: this.title, + }, + }); + $.export("$summary", `Found ${publications.length} flipbook${publications.length === 1 + ? "" + : "s"} mathcing the title ${this.title}`); + return publications; + }, +}; diff --git a/components/flippingbook/actions/update-flipbook/update-flipbook.mjs b/components/flippingbook/actions/update-flipbook/update-flipbook.mjs new file mode 100644 index 0000000000000..8e1b31a6a527a --- /dev/null +++ b/components/flippingbook/actions/update-flipbook/update-flipbook.mjs @@ -0,0 +1,86 @@ +import flippingbook from "../../flippingbook.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; +import fs from "fs"; + +export default { + key: "flippingbook-update-flipbook", + name: "Update Flipbook", + description: "Edits an existing flipbook by replacing it with a new input PDF file. [See the documentation](https://apidocs.flippingbook.com/#update-the-metadata-for-one-publication-possibly-attaching-a-new-source-file)", + version: "0.0.1", + type: "action", + props: { + flippingbook, + flipbookId: { + propDefinition: [ + flippingbook, + "flipbookId", + ], + }, + info: { + propDefinition: [ + flippingbook, + "info", + ], + }, + fileUrl: { + propDefinition: [ + flippingbook, + "fileUrl", + ], + }, + filePath: { + propDefinition: [ + flippingbook, + "filePath", + ], + }, + filename: { + propDefinition: [ + flippingbook, + "filename", + ], + }, + name: { + propDefinition: [ + flippingbook, + "name", + ], + optional: true, + }, + description: { + propDefinition: [ + flippingbook, + "description", + ], + }, + }, + async run({ $ }) { + if ((!this.fileUrl && !this.filePath) || (this.fileUrl && this.filePath)) { + throw new ConfigurationError("Please provide exactly one of File URL or File Path"); + } + const data = { + filename: this.filename, + name: this.name, + description: this.description, + }; + if (this.fileUrl) { + data.url = this.fileUrl; + } else { + const content = fs.readFileSync(this.filePath.includes("tmp/") + ? this.filePath + : `/tmp/${this.filePath}`); + data.data = content.toString("base64"); + } + + const response = await this.flippingbook.updateFlipbook({ + $, + flipbookId: this.flipbookId, + data, + }); + + if (!response.error) { + $.export("$summary", `Successfully updated flipbook with ID: ${this.flipbookId}`); + } + return response; + }, +}; diff --git a/components/flippingbook/flippingbook.app.mjs b/components/flippingbook/flippingbook.app.mjs new file mode 100644 index 0000000000000..5f6079ce6a759 --- /dev/null +++ b/components/flippingbook/flippingbook.app.mjs @@ -0,0 +1,129 @@ +import { axios } from "@pipedream/platform"; +const DEFAULT_LIMIT = 20; + +export default { + type: "app", + app: "flippingbook", + propDefinitions: { + flipbookId: { + type: "string", + label: "Flipbook ID", + description: "The unique identifier of the flipbook.", + async options({ page }) { + const { publications } = await this.listFlipbooks({ + params: { + count: DEFAULT_LIMIT, + offset: page * DEFAULT_LIMIT, + }, + }); + return publications?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + name: { + type: "string", + label: "Name", + description: "Name of the publication", + }, + info: { + type: "alert", + alertType: "info", + content: "Please provide exactly one of File URL or File Path.", + }, + fileUrl: { + type: "string", + label: "File URL", + description: "The URL of the .pdf file that will be used to create the source for the publication. The URL must be publicly accessible for at least for several minutes.", + optional: true, + }, + filePath: { + type: "string", + label: "File Path", + description: "The path to a .pdf file in the `/tmp` directory be used to create the source for the publication. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + optional: true, + }, + filename: { + type: "string", + label: "Filename", + description: "The name of your PDF file", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "Publication description", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api-tc.is.flippingbook.com/api/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + ...opts, + method: "POST", + path: "/fbonline/triggers", + }); + }, + deleteWebhook({ + hookId, ...opts + }) { + return this._makeRequest({ + ...opts, + method: "DELETE", + path: `/fbonline/triggers/${hookId}`, + }); + }, + getFlipbook({ + flipbookId, ...opts + }) { + return this._makeRequest({ + ...opts, + path: `/fbonline/publication/${flipbookId}`, + }); + }, + listFlipbooks(opts = {}) { + return this._makeRequest({ + ...opts, + path: "/fbonline/publication", + }); + }, + createFlipbook(opts = {}) { + return this._makeRequest({ + ...opts, + method: "POST", + path: "/fbonline/publication", + }); + }, + updateFlipbook({ + flipbookId, ...opts + }) { + return this._makeRequest({ + ...opts, + method: "POST", + path: `/fbonline/publication/${flipbookId}`, + }); + }, + }, +}; diff --git a/components/flippingbook/package.json b/components/flippingbook/package.json new file mode 100644 index 0000000000000..ba1607744d054 --- /dev/null +++ b/components/flippingbook/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/flippingbook", + "version": "0.1.0", + "description": "Pipedream FlippingBook Components", + "main": "flippingbook.app.mjs", + "keywords": [ + "pipedream", + "flippingbook" + ], + "homepage": "https://pipedream.com/apps/flippingbook", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} diff --git a/components/flippingbook/sources/common/base.mjs b/components/flippingbook/sources/common/base.mjs new file mode 100644 index 0000000000000..54ac6abd2c26c --- /dev/null +++ b/components/flippingbook/sources/common/base.mjs @@ -0,0 +1,41 @@ +import flippingbook from "../../flippingbook.app.mjs"; + +export default { + props: { + flippingbook, + db: "$.service.db", + http: "$.interface.http", + }, + hooks: { + async activate() { + const response = await this.flippingbook.createWebhook({ + data: { + trigger: { + endpoint: this.http.endpoint, + ...this.getHookParams(), + }, + }, + }); + this._setHookId(response.id); + }, + async deactivate() { + const hookId = this._getHookId(); + if (hookId) { + await this.flippingbook.deleteWebhook({ + hookId, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getHookParams() { + throw new Error("getHookParams is not implemented"); + }, + }, +}; diff --git a/components/flippingbook/sources/new-flipbook-instant/new-flipbook-instant.mjs b/components/flippingbook/sources/new-flipbook-instant/new-flipbook-instant.mjs new file mode 100644 index 0000000000000..489a2157cc230 --- /dev/null +++ b/components/flippingbook/sources/new-flipbook-instant/new-flipbook-instant.mjs @@ -0,0 +1,44 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "flippingbook-new-flipbook-instant", + name: "New Flipbook (Instant)", + description: "Emit new event when a new flipbook is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getHookParams() { + return { + triggerOn: [ + "publication", + ], + events: [ + "created", + ], + }; + }, + async getObject(flipbookId) { + const { publication } = await this.flippingbook.getFlipbook({ + flipbookId, + }); + return publication; + }, + generateMeta(flipbook) { + return { + id: flipbook.id, + summary: `New Flipbook Created: ${flipbook.name}`, + ts: Date.now(), + }; + }, + }, + async run(event) { + const { body: { objectId } } = event; + const object = await this.getObject(objectId); + this.$emit(object, this.generateMeta(object)); + }, + sampleEmit, +}; diff --git a/components/flippingbook/sources/new-flipbook-instant/test-event.mjs b/components/flippingbook/sources/new-flipbook-instant/test-event.mjs new file mode 100644 index 0000000000000..614b26ca7b1cd --- /dev/null +++ b/components/flippingbook/sources/new-flipbook-instant/test-event.mjs @@ -0,0 +1,58 @@ +export default { + "id": "76ae48cf12604e7ea95e82611b686398", + "links": [ + { + "rel": "self", + "type": "GET", + "href": "https://api-tc.is.flippingbook.com/api/v1/fbonline/publication/76ae48cf12604e7ea95e82611b686398" + }, + { + "rel": "edit", + "type": "POST", + "href": "https://api-tc.is.flippingbook.com/api/v1/fbonline/publication/76ae48cf12604e7ea95e82611b686398" + }, + { + "rel": "delete", + "type": "DELETE", + "href": "https://api-tc.is.flippingbook.com/api/v1/fbonline/publication/76ae48cf12604e7ea95e82611b686398" + }, + { + "rel": "fbonline/sources", + "type": "GET", + "href": "https://api-tc.is.flippingbook.com/api/v1/fbonline/publication/76ae48cf12604e7ea95e82611b686398/source" + }, + { + "rel": "fbonline/triggers", + "type": "GET", + "href": "https://api-tc.is.flippingbook.com/api/v1/fbonline/publication/76ae48cf12604e7ea95e82611b686398/trigger" + }, + { + "rel": "fbonline/tracked-links", + "type": "GET", + "href": "https://api-tc.is.flippingbook.com/api/v1/fbonline/tracked_links?publication-uid=76ae48cf12604e7ea95e82611b686398" + } + ], + "state": "HasContent, Published, CompletedAllStages", + "seoEnabled": false, + "name": "Pub", + "description": "This interactive flipbook is created with FlippingBook, a service for streaming PDFs online. No download, no waiting. Open and start reading right away!", + "hashId": "617972345", + "lastPdfName": "https://clickdimensions.com/links/TestPDFfile.pdf", + "contentRoot": "https://d17lvj5xn8sco6.cloudfront.net/98/63/68/1B/61/82/5E/A9/4E/7E/12/60/76/AE/48/CF/003EBFE2/", + "canonicalLink": "https://online.flippingbook.com/view/617972345/", + "modificationTime": "2024-06-03T21:01:17.766032Z", + "creationTime": "2024-06-03T21:01:05.5319Z", + "isDemoPublication": false, + "totalPages": 1, + "domain": "online.flippingbook.com", + "folder": null, + "cover": "https://d17lvj5xn8sco6.cloudfront.net/98/63/68/1B/61/82/5E/A9/4E/7E/12/60/76/AE/48/CF/003EBFE2/cover600.jpg", + "customizationOptions": { + "password": "", + "hardcoverEnabled": false, + "companyLogoEnabled": false, + "companyLogoUrl": "", + "rtlEnabled": false, + "theme": "gray" + } +} \ No newline at end of file diff --git a/components/flippingbook/sources/new-lead-instant/new-lead-instant.mjs b/components/flippingbook/sources/new-lead-instant/new-lead-instant.mjs new file mode 100644 index 0000000000000..bbf0027b5c598 --- /dev/null +++ b/components/flippingbook/sources/new-lead-instant/new-lead-instant.mjs @@ -0,0 +1,54 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "flippingbook-new-lead-instant", + name: "New Lead (Instant)", + description: "Emit new event when a new lead is created via a lead capture form.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + flipbookId: { + propDefinition: [ + common.props.flippingbook, + "flipbookId", + ], + optional: true, + }, + }, + methods: { + ...common.methods, + getHookParams() { + const params = { + triggerOn: [ + "publication", + ], + events: [ + "lead", + ], + }; + if (this.flipbookId) { + params.limitTo = { + parentObject: "publication", + parentObjectId: this.flipbookId, + }; + } + return params; + }, + generateMeta(body) { + return { + id: body.additionalInfo.uniqueKey, + summary: "New Lead Created", + ts: Date.now(), + }; + }, + }, + async run(event) { + const { body } = event; + this.$emit(body, this.generateMeta(body)); + }, + sampleEmit, +}; diff --git a/components/flippingbook/sources/new-lead-instant/test-event.mjs b/components/flippingbook/sources/new-lead-instant/test-event.mjs new file mode 100644 index 0000000000000..39d1e19618614 --- /dev/null +++ b/components/flippingbook/sources/new-lead-instant/test-event.mjs @@ -0,0 +1,18 @@ +export default { + "scope": "trackedLink", + "objectId": null, + "event": "lead", + "additionalInfo": { + "uniqueKey": "Your email:2:test@email.com", + "data": [ + { + "Name": "Your email", + "Type": "Email", + "Value": "test@email.com" + } + ], + "skipped": false, + "publicationId": "3325b2027ea24dbfa2fc0972292b5bf3", + "infoType": "lead" + } +} \ No newline at end of file diff --git a/components/float/README.md b/components/float/README.md new file mode 100644 index 0000000000000..556adab43a0af --- /dev/null +++ b/components/float/README.md @@ -0,0 +1,11 @@ +# Overview + +The Float API allows you to interact with Float's resource scheduling platform programmatically. With it, you can manage projects, tasks, people, and time off, as well as retrieve reports on workload and capacity. On Pipedream, the API can be leveraged to create automated workflows that link Float's scheduling capabilities with other apps, streamlining project management processes, syncing with calendars, and dynamically updating team schedules. + +# Example Use Cases + +- **Automated Project Assignment Notifications**: Trigger a workflow in Pipedream when a new project is assigned in Float. Send an email or Slack message to the designated team member with project details, deadlines, and any special instructions. + +- **Sync Float Schedule with Google Calendar**: Use Pipedream to watch for changes in tasks or project assignments in Float, then automatically create or update corresponding events in a Google Calendar, keeping everyone's schedule in sync. + +- **Track Time Off Requests**: When a time off request is submitted in Float, initiate a Pipedream workflow that logs the request in an HR management system like BambooHR, updates the team's availability in Float, and sends a notification to the team lead for approval. diff --git a/components/flodesk/README.md b/components/flodesk/README.md new file mode 100644 index 0000000000000..896def867b3c1 --- /dev/null +++ b/components/flodesk/README.md @@ -0,0 +1,11 @@ +# Overview + +The Flodesk API on Pipedream allows you to streamline email marketing efforts by enabling automation of subscriber management and email campaign triggers. With this integration, you can create workflows to sync subscriber data, send personalized emails based on customer actions, and analyze campaign performance, all in a serverless environment. Pipedream's ability to connect with hundreds of apps opens the door to craft custom automation sequences that fit your specific marketing needs. + +# Example Use Cases + +- **Sync New Ecommerce Customers to Flodesk**: Automatically add new customers from an ecommerce platform like Shopify to a Flodesk segment. When a new customer is created in Shopify, the workflow triggers, adding the customer's details to a specified Flodesk email list, ensuring they receive your latest marketing emails. + +- **Trigger Email Sequences from Webinar Sign-Ups**: Start an email sequence in Flodesk when someone signs up for a webinar via a tool like Zoom. This workflow can capture sign-up information and use it to trigger a personalized Flodesk email sequence, providing attendees with relevant information pre and post-webinar. + +- **Send Custom Emails Based on CRM Updates**: When a lead status is updated in a CRM like HubSpot, send a tailored email from Flodesk. This workflow listens for status changes in HubSpot and triggers an email via Flodesk that aligns with the lead's new stage in the sales funnel, ensuring timely and relevant communication. diff --git a/components/florm/README.md b/components/florm/README.md new file mode 100644 index 0000000000000..4fc96707e89a4 --- /dev/null +++ b/components/florm/README.md @@ -0,0 +1,11 @@ +# Overview + +Florm API allows you to craft surveys, quizzes, and forms and collect responses through a simple interface. With Pipedream, you can automate workflows by connecting Florm to various apps, triggering actions based on form submissions, and manipulating Florm data to fit your needs. Think of it as a bridge that lets your form responses flow into other systems, triggering emails, database updates, or even complex business logic. + +# Example Use Cases + +- **Automated Response Handling**: When a new response is submitted to a Florm form, Pipedream can automatically process that data. For instance, you could create a workflow that adds new leads to a CRM like Salesforce, tags them based on their answers, and sends a personalized email follow-up via SendGrid. + +- **Real-Time Notifications**: Set up a Pipedream workflow that listens for new Florm submissions and sends a Slack or Discord message to your team. This way, you can keep everyone in the loop about customer feedback or survey results the moment they come in. + +- **Data Aggregation**: Use Pipedream to funnel Florm responses into a Google Sheet for easy tracking and analysis. You can append new responses to a sheet, organize them based on criteria, or even integrate with Google Data Studio for advanced reporting and visualization. diff --git a/components/florm/package.json b/components/florm/package.json index 73b5ed58373b2..f0803e796f621 100644 --- a/components/florm/package.json +++ b/components/florm/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/flowiseai/README.md b/components/flowiseai/README.md new file mode 100644 index 0000000000000..4cf66cee2e2af --- /dev/null +++ b/components/flowiseai/README.md @@ -0,0 +1,11 @@ +# Overview + +The FlowiseAI API offers powerful AI-driven capabilities to enhance data with smart predictions and insights. In Pipedream, you can leverage this API to automate tasks, analyze large sets of data, and build intelligent workflows that respond to the AI's output. FlowiseAI can pair with various apps on Pipedream to enrich CRM data, optimize marketing campaigns, or streamline customer support processes. + +# Example Use Cases + +- **CRM Data Enrichment**: By integrating FlowiseAI with CRM platforms like Salesforce or HubSpot in Pipedream, you can automatically enrich contact records with predictive scoring. This could help sales teams prioritize leads that are most likely to convert based on the AI's insight. + +- **Customer Support Automation**: Connect FlowiseAI to a customer support app such as Zendesk. Use the API to analyze incoming support tickets and automatically route them to the appropriate team or generate responses based on predicted issues and solutions. + +- **Marketing Campaign Optimization**: Link FlowiseAI with an email marketing tool like Mailchimp on Pipedream. Analyze campaign metrics and audience behavior with the API to predict future engagement rates and segment audiences more effectively for targeted campaigns. diff --git a/components/flowiseai/actions/make-prediction/make-prediction.mjs b/components/flowiseai/actions/make-prediction/make-prediction.mjs new file mode 100644 index 0000000000000..02f1cb7dd8486 --- /dev/null +++ b/components/flowiseai/actions/make-prediction/make-prediction.mjs @@ -0,0 +1,48 @@ +import flowiseai from "../../flowiseai.app.mjs"; + +export default { + key: "flowiseai-make-prediction", + name: "Make Prediction", + description: "Calculates an output based on your created flow in Flowise. [See the documentation](https://docs.flowiseai.com/using-flowise/api#prediction-api)", + version: "0.0.1", + type: "action", + props: { + flowiseai, + flowId: { + propDefinition: [ + flowiseai, + "flowId", + ], + }, + question: { + propDefinition: [ + flowiseai, + "question", + ], + }, + history: { + propDefinition: [ + flowiseai, + "history", + ], + }, + }, + async run({ $ }) { + const history = typeof this.history === "string" + ? JSON.parse(this.history) + : this.history; + + const response = await this.flowiseai.makePrediction({ + $, + flowId: this.flowId, + data: { + question: this.question, + history, + }, + }); + + $.export("$summary", `Prediction calculated for flow ID ${this.flowId}`); + + return response; + }, +}; diff --git a/components/flowiseai/flowiseai.app.mjs b/components/flowiseai/flowiseai.app.mjs new file mode 100644 index 0000000000000..a792ec83d46b1 --- /dev/null +++ b/components/flowiseai/flowiseai.app.mjs @@ -0,0 +1,52 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "flowiseai", + propDefinitions: { + flowId: { + type: "string", + label: "Flow ID", + description: "The unique identifier of the flow you'd like to use for computations.", + }, + question: { + type: "string", + label: "Question", + description: "User's question. E.g. `Hey, how are you?`", + }, + history: { + type: "object", + label: "History", + description: "Provide list of history messages to the flow. Only works when using [Short Term Memory](https://docs.flowiseai.com/integrations/langchain/memory/short-term-memory). E.g. `[ { \"message\": \"Hello, how can I assist you?\", \"type\": \"apiMessage\" }, { \"type\": \"userMessage\", \"message\": \"Hello I am Bob\" }, { \"type\": \"apiMessage\", \"message\": \"Hello Bob! how can I assist you?\" } ]`", + optional: true, + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.url}/api/v1`; + }, + async _makeRequest(opts = {}) { + const { + $ = this, method = "GET", path, headers, ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + method, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + async makePrediction({ + flowId, ...args + }) { + return this._makeRequest({ + method: "POST", + path: `/prediction/${flowId}`, + ...args, + }); + }, + }, +}; diff --git a/components/flowiseai/package.json b/components/flowiseai/package.json new file mode 100644 index 0000000000000..27c1c628ecbb6 --- /dev/null +++ b/components/flowiseai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/flowiseai", + "version": "0.1.0", + "description": "Pipedream FlowiseAI Components", + "main": "flowiseai.app.mjs", + "keywords": [ + "pipedream", + "flowiseai" + ], + "homepage": "https://pipedream.com/apps/flowiseai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/flowla/flowla.app.mjs b/components/flowla/flowla.app.mjs new file mode 100644 index 0000000000000..f5589d19ccd07 --- /dev/null +++ b/components/flowla/flowla.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "flowla", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/flowla/package.json b/components/flowla/package.json new file mode 100644 index 0000000000000..0de8c72914316 --- /dev/null +++ b/components/flowla/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/flowla", + "version": "0.0.1", + "description": "Pipedream Flowla Components", + "main": "flowla.app.mjs", + "keywords": [ + "pipedream", + "flowla" + ], + "homepage": "https://pipedream.com/apps/flowla", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/fluent_support/README.md b/components/fluent_support/README.md new file mode 100644 index 0000000000000..f27594ee6d8f9 --- /dev/null +++ b/components/fluent_support/README.md @@ -0,0 +1,11 @@ +# Overview + +The Fluent Support API lets you interact with your Fluent Support ticketing system programmatically, enabling you to automate ticket management, sync customer data, and trigger actions based on ticket events. With Pipedream, you can harness this functionality to create custom workflows that respond to ticket creation, updates, and status changes, or to integrate ticket data with other apps and services. + +# Example Use Cases + +- **Automate Ticket Tagging and Assignment**: When a new ticket arrives, use Pipedream to analyze the content and automatically tag it with relevant keywords or route it to the appropriate support agent or team based on predefined rules. + +- **Sync Support Tickets with CRM**: Upon ticket closure, trigger a workflow that updates the corresponding customer record in a CRM like Salesforce or HubSpot, ensuring that the sales or account management team has the latest interaction data. + +- **Aggregate Support Metrics for Analysis**: Schedule a recurring workflow in Pipedream that fetches ticket data on a regular basis, compiles support metrics (such as response time and resolution rate), and sends a report to a data visualization tool like Google Sheets or Tableau for monitoring and analysis. diff --git a/components/fluidforms/fluidforms.app.mjs b/components/fluidforms/fluidforms.app.mjs new file mode 100644 index 0000000000000..e92ea8e9b831a --- /dev/null +++ b/components/fluidforms/fluidforms.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "fluidforms", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/fluidforms/package.json b/components/fluidforms/package.json new file mode 100644 index 0000000000000..ddb4d414abf35 --- /dev/null +++ b/components/fluidforms/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/fluidforms", + "version": "0.0.1", + "description": "Pipedream FluidForms Components", + "main": "fluidforms.app.mjs", + "keywords": [ + "pipedream", + "fluidforms" + ], + "homepage": "https://pipedream.com/apps/fluidforms", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/flutterwave/README.md b/components/flutterwave/README.md new file mode 100644 index 0000000000000..e4928faa724cf --- /dev/null +++ b/components/flutterwave/README.md @@ -0,0 +1,14 @@ +# Overview + +The Flutterwave API empowers developers to automate payment processes and integrate various payment services within applications. With this API, you can initiate and receive payments, manage transactions, and access customer data securely. Utilizing Pipedream, you can create workflows that trigger on specific events, process data, and integrate with an array of services without managing servers. + +# Example Use Cases + +- **Automated Payment Confirmation Emails** +Upon successful payment via Flutterwave, trigger a workflow in Pipedream to send confirmation emails to customers using a service like SendGrid. This ensures customers receive timely notifications about their transactions. + +- **Real-time Slack Notifications for High-Value Transactions** +Create a workflow that listens for Flutterwave transactions over a certain amount and sends an alert to a designated Slack channel. This workflow helps teams stay informed about critical financial activity. + +- **Sync Payments with Google Sheets for Accounting** +After receiving a new payment through Flutterwave, use a Pipedream workflow to add the transaction details to a Google Sheets spreadsheet. This can be part of an automated accounting system, making it easier to keep track of income. diff --git a/components/fluxguard/README.md b/components/fluxguard/README.md new file mode 100644 index 0000000000000..befaeec3b9a7c --- /dev/null +++ b/components/fluxguard/README.md @@ -0,0 +1,11 @@ +# Overview + +The Fluxguard API enables detailed monitoring of web properties by tracking changes in web page content, performance, SEO, and more. By integrating Fluxguard with Pipedream, you can build automated workflows that respond to these changes in real-time. For example, you can trigger alerts, synchronize data with other platforms, or even initiate deployment processes based on the insights provided by Fluxguard. + +# Example Use Cases + +- **Alerting on Content Changes**: Set up a workflow that listens to Fluxguard events and sends notifications via Slack, email, or SMS when specific changes are detected on your monitored web pages. This can be indispensable for content integrity and timely updates. + +- **Performance Monitoring and Reporting**: Create a workflow that captures Fluxguard performance events and logs them into a Google Sheet for analysis. This could also trigger a webhook to notify your development team if performance drops below a certain threshold. + +- **SEO Change Management**: Leverage Fluxguard to monitor SEO-critical elements on your site. You can build a workflow that integrates with Airtable, logging any SEO changes detected by Fluxguard, and setting tasks for your SEO team to review adjustments or updates needed to maintain rankings. diff --git a/components/fluxguard/package.json b/components/fluxguard/package.json index a233c2edd6bd2..54b2ecd2a1b86 100644 --- a/components/fluxguard/package.json +++ b/components/fluxguard/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/fly_io/README.md b/components/fly_io/README.md new file mode 100644 index 0000000000000..e4141fa40e63e --- /dev/null +++ b/components/fly_io/README.md @@ -0,0 +1,11 @@ +# Overview + +Fly.io is a platform that allows you to run full-stack apps and databases close to your users globally. The Fly.io API enables developers to manage applications, handle deployments, and scale their services dynamically. Using the Fly.io API with Pipedream provides a seamless way to automate these operations, integrate with other services, and enhance serverless workflow capabilities. + +# Example Use Cases + +- **Dynamic App Scaling Based on Traffic**: Automate the scaling of your applications on Fly.io based on real-time traffic data from Google Analytics. Set up a Pipedream workflow that triggers when a specified traffic threshold is reached on Google Analytics, then scales your Fly.io application instances up or down accordingly. + +- **Deployment Automation from GitHub**: Automate the deployment of your applications hosted on GitHub to Fly.io whenever a new commit is pushed to the main branch. Use a Pipedream workflow that listens for GitHub push events, builds your application, and deploys it to Fly.io, ensuring your live applications are always up-to-date with the latest code. + +- **Database Backup Notifications**: Set up a workflow on Pipedream to automate database backups on Fly.io and send notifications via Slack whenever a backup is completed. This workflow can ensure team members are always informed about the status of backups, enhancing data management practices. diff --git a/components/fly_io/actions/create-app/create-app.mjs b/components/fly_io/actions/create-app/create-app.mjs new file mode 100644 index 0000000000000..a8a913f807391 --- /dev/null +++ b/components/fly_io/actions/create-app/create-app.mjs @@ -0,0 +1,59 @@ +import app from "../../fly_io.app.mjs"; + +export default { + key: "fly_io-create-app", + name: "Create App", + description: "Create an app with the specified details in the request body. [See the documentation](https://docs.machines.dev/#tag/apps/post/apps)", + version: "0.0.1", + type: "action", + props: { + app, + appName: { + type: "string", + label: "App Name", + description: "The name of the app", + }, + enableSubdomains: { + type: "boolean", + label: "Enable Subdomains", + description: "Whether to enable subdomains for the app", + optional: true, + }, + network: { + type: "string", + label: "Network", + description: "The network for the app", + optional: true, + }, + }, + methods: { + createApp(args = {}) { + return this.app.post({ + path: "/apps", + ...args, + }); + }, + }, + async run({ $ }) { + const { + app, + createApp, + appName, + enableSubdomains, + network, + } = this; + + const response = await createApp({ + $, + data: { + app_name: appName, + enable_subdomains: enableSubdomains, + network, + org_slug: app.getOrgSlug(), + }, + }); + + $.export("$summary", `Successfully created app with ID \`${response.id}\``); + return response; + }, +}; diff --git a/components/fly_io/actions/create-machine/create-machine.mjs b/components/fly_io/actions/create-machine/create-machine.mjs new file mode 100644 index 0000000000000..d0852f6fd69a4 --- /dev/null +++ b/components/fly_io/actions/create-machine/create-machine.mjs @@ -0,0 +1,76 @@ +import app from "../../fly_io.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "fly_io-create-machine", + name: "Create Machine", + description: "Create a machine within a specific app using the details provided in the request body. [See the documentation](https://docs.machines.dev/#tag/machines/post/apps/%7Bapp_name%7D/machines)", + version: "0.0.1", + type: "action", + props: { + app, + appName: { + propDefinition: [ + app, + "appId", + () => ({ + mapper: ({ name }) => name, + }), + ], + }, + name: { + type: "string", + label: "Machine Name", + description: "The name of the machine within the app", + }, + config: { + type: "string", + label: "Machine Configuration", + description: "The configuration details of the machine in JSON format. Eg. `{ \"auto_destroy\": true, \"image\": \"ubuntu:latest\" }`", + }, + skipLaunch: { + type: "boolean", + label: "Skip Launch", + description: "Whether to skip the launch of the machine", + optional: true, + }, + skipServiceRegistration: { + type: "boolean", + label: "Skip Service Registration", + description: "Whether to skip the registration of the machine as a service", + optional: true, + }, + }, + methods: { + createMachine({ + appName, ...args + } = {}) { + return this.app.post({ + path: `/apps/${appName}/machines`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + createMachine, + appName, + name, + config, + skipLaunch, + skipServiceRegistration, + } = this; + const response = await createMachine({ + $, + appName, + data: { + name, + config: utils.valueToObject(config), + skip_launch: skipLaunch, + skip_service_registration: skipServiceRegistration, + }, + }); + $.export("$summary", `Successfully created machine with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/fly_io/actions/create-volume/create-volume.mjs b/components/fly_io/actions/create-volume/create-volume.mjs new file mode 100644 index 0000000000000..1d6cf4357ef9f --- /dev/null +++ b/components/fly_io/actions/create-volume/create-volume.mjs @@ -0,0 +1,68 @@ +import app from "../../fly_io.app.mjs"; + +export default { + key: "fly_io-create-volume", + name: "Create Volume", + description: "Create a volume for a specific app using the details provided in the request body. [See the documentation](https://docs.machines.dev/#tag/volumes/post/apps/%7Bapp_name%7D/volumes)", + version: "0.0.1", + type: "action", + props: { + app, + appName: { + propDefinition: [ + app, + "appId", + () => ({ + mapper: ({ name }) => name, + }), + ], + }, + name: { + type: "string", + label: "Volume Name", + description: "The name of the volume", + }, + sizeGb: { + type: "integer", + label: "Volume Size (GB)", + description: "The size of the volume in GB", + }, + region: { + type: "string", + label: "Volume Region", + description: "The region where the volume will be created", + optional: true, + }, + }, + methods: { + createVolume({ + appName, ...args + } = {}) { + return this.app.post({ + path: `/apps/${appName}/volumes`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + createVolume, + appName, + name, + sizeGb, + region, + } = this; + const response = await createVolume({ + $, + appName, + data: { + name, + size_gb: sizeGb, + region, + }, + }); + + $.export("$summary", `Successfully created volume with ID \`${response.id}\``); + return response; + }, +}; diff --git a/components/fly_io/common/constants.mjs b/components/fly_io/common/constants.mjs new file mode 100644 index 0000000000000..8adcb50cc3a20 --- /dev/null +++ b/components/fly_io/common/constants.mjs @@ -0,0 +1,7 @@ +const BASE_URL = "https://api.machines.dev"; +const VERSION_PATH = "/v1"; + +export default { + BASE_URL, + VERSION_PATH, +}; diff --git a/components/fly_io/common/utils.mjs b/components/fly_io/common/utils.mjs new file mode 100644 index 0000000000000..b7e58f17e7776 --- /dev/null +++ b/components/fly_io/common/utils.mjs @@ -0,0 +1,26 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function isJson(value) { + try { + JSON.parse(value); + } catch (e) { + return false; + } + return true; +} + +function valueToObject(value) { + if (typeof(value) === "object" || value === undefined) { + return value; + } + + if (!isJson(value)) { + throw new ConfigurationError(`Make sure the custom expression contains a valid JSON object: \`${value}\``); + } + + return JSON.parse(value); +} + +export default { + valueToObject, +}; diff --git a/components/fly_io/fly_io.app.mjs b/components/fly_io/fly_io.app.mjs new file mode 100644 index 0000000000000..e9d7cde3bedfd --- /dev/null +++ b/components/fly_io/fly_io.app.mjs @@ -0,0 +1,97 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "fly_io", + propDefinitions: { + appId: { + type: "string", + label: "App Name", + description: "The name of the app", + async options({ + mapper = ({ + id: value, name: label, + }) => ({ + value, + label, + }), + }) { + const { apps } = await this.listApps({ + params: { + org_slug: this.getOrgSlug(), + }, + }); + return apps.map(mapper); + }, + }, + machineId: { + type: "string", + label: "Machine ID", + description: "The ID of the machine", + async options({ appName }) { + const machines = await this.listMachines({ + appName, + }); + return machines.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + getOrgSlug() { + return this.$auth.org_slug; + }, + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.$auth.access_token}`, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + listApps(args = {}) { + return this._makeRequest({ + path: "/apps", + ...args, + }); + }, + listMachines({ + appName, ...args + } = {}) { + return this._makeRequest({ + path: `/apps/${appName}/machines`, + ...args, + }); + }, + listEvents({ + appName, machineId, ...args + } = {}) { + return this._makeRequest({ + path: `/apps/${appName}/machines/${machineId}/events`, + ...args, + }); + }, + }, +}; diff --git a/components/fly_io/package.json b/components/fly_io/package.json new file mode 100644 index 0000000000000..a6828bbad94a1 --- /dev/null +++ b/components/fly_io/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/fly_io", + "version": "0.1.0", + "description": "Pipedream Fly.io Components", + "main": "fly_io.app.mjs", + "keywords": [ + "pipedream", + "fly_io" + ], + "homepage": "https://pipedream.com/apps/fly_io", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "1.6.5" + } +} diff --git a/components/fly_io/sources/common/polling.mjs b/components/fly_io/sources/common/polling.mjs new file mode 100644 index 0000000000000..258511922ddd1 --- /dev/null +++ b/components/fly_io/sources/common/polling.mjs @@ -0,0 +1,48 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../fly_io.app.mjs"; + +export default { + props: { + app, + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + }, + async run() { + const { + getResourcesFn, + getResourcesFnArgs, + processResource, + } = this; + + const resourcesFn = getResourcesFn(); + const resources = await resourcesFn(getResourcesFnArgs()); + + Array.from(resources) + .reverse() + .forEach(processResource); + }, +}; diff --git a/components/fly_io/sources/new-event-created/new-event-created.mjs b/components/fly_io/sources/new-event-created/new-event-created.mjs new file mode 100644 index 0000000000000..df7980f0717de --- /dev/null +++ b/components/fly_io/sources/new-event-created/new-event-created.mjs @@ -0,0 +1,55 @@ +import common from "../common/polling.mjs"; + +export default { + ...common, + key: "fly_io-new-event-created", + name: "New Event Created", + description: "Emit new event when a new event is created in Fly.io. [See the documentation](https://docs.machines.dev/#tag/machines/get/apps/{app_name}/machines/{machine_id}/events)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + appName: { + propDefinition: [ + common.props.app, + "appId", + () => ({ + mapper: ({ name }) => name, + }), + ], + }, + machineId: { + propDefinition: [ + common.props.app, + "machineId", + ({ appName }) => ({ + appName, + }), + ], + }, + }, + methods: { + ...common.methods, + getResourcesFn() { + return this.app.listEvents; + }, + getResourcesFnArgs() { + const { + appName, + machineId, + } = this; + return { + appName, + machineId, + }; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Event: ${resource.id}`, + ts: resource.timestamp, + }; + }, + }, +}; diff --git a/components/focuster/README.md b/components/focuster/README.md new file mode 100644 index 0000000000000..d9dd413e56180 --- /dev/null +++ b/components/focuster/README.md @@ -0,0 +1,11 @@ +# Overview + +The Focuster API lets you interact with your Focuster account to manage tasks and priorities, providing a programmatic way to create, update, and delete actions, as well as retrieve your focus sessions. In Pipedream, you can harness this API to build powerful automations that boost productivity by integrating Focuster with other apps. This enables you to sync tasks, set reminders, and organize your workflow in a more dynamic and interconnected way. + +# Example Use Cases + +- **Sync Focuster Actions with Google Calendar**: Automatically add your Focuster actions to Google Calendar. When you create a new action in Focuster, a Pipedream workflow can catch that event and create a corresponding event in Google Calendar, ensuring you never miss a task due to a scheduling conflict. + +- **Aggregate Tasks from Multiple Platforms**: Collect tasks from various tools like Trello, Asana, or JIRA and input them into Focuster. Set up Pipedream to listen for new tasks on these platforms, then create corresponding actions in Focuster, centralizing your task management in one place. + +- **Daily Task Digest via Email**: Configure Pipedream to generate a daily digest of your Focuster actions and send it to your email. This workflow can retrieve your tasks every morning, format them, and send an email with your day's focus, helping you stay on track from the moment you start work. diff --git a/components/focuster/package.json b/components/focuster/package.json index 2838fa7b8495e..c1cc2d8903413 100644 --- a/components/focuster/package.json +++ b/components/focuster/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/fogbugz/README.md b/components/fogbugz/README.md new file mode 100644 index 0000000000000..bae37afaa7b80 --- /dev/null +++ b/components/fogbugz/README.md @@ -0,0 +1,11 @@ +# Overview + +The FogBugz API allows you to interact with your FogBugz software development tracking system programmatically. With this API, you can create, read, update, and delete cases; manage projects, areas, and categories; and perform searches to gather data-driven insights. Integrating FogBugz with Pipedream opens up possibilities for automating workflows, syncing data across platforms, and reacting to events within FogBugz in real-time. + +# Example Use Cases + +- **Automated Bug Reporting**: When a new bug is reported in your product through another platform, like Sentry or GitHub, you can automatically create a corresponding case in FogBugz using Pipedream. This ensures that bugs are tracked and managed efficiently without manual intervention. + +- **Project Management Enhancement**: Sync FogBugz cases to a Trello board or a Google Sheet to provide additional project management views. Each time a case is updated in FogBugz, Pipedream can move a Trello card to a different list or update a row in Google Sheets, keeping all project stakeholders in sync with development progress. + +- **Customer Support Integration**: Connect your customer support platform, such as Zendesk, to FogBugz via Pipedream. When a support ticket in Zendesk is flagged as a bug, automatically create a case in FogBugz and link the two, streamlining the process of tracking and resolving customer-reported issues. diff --git a/components/fogbugz/actions/create-case/create-case.mjs b/components/fogbugz/actions/create-case/create-case.mjs new file mode 100644 index 0000000000000..7228c1f1115eb --- /dev/null +++ b/components/fogbugz/actions/create-case/create-case.mjs @@ -0,0 +1,103 @@ +import { parseObject } from "../../common/utils.mjs"; +import fogbugz from "../../fogbugz.app.mjs"; + +export default { + key: "fogbugz-create-case", + name: "Create Case", + description: "Creates a new case on FogBugz. [See the documentation](https://support.fogbugz.com/hc/en-us/articles/360011330713-FogBugz-XML-API-Editing-Cases)", + version: "0.0.1", + type: "action", + props: { + fogbugz, + ixBugParent: { + propDefinition: [ + fogbugz, + "ixBugParentId", + ], + optional: true, + }, + sTitle: { + propDefinition: [ + fogbugz, + "sTitle", + ], + }, + sTags: { + propDefinition: [ + fogbugz, + "sTags", + ], + optional: true, + }, + ixProject: { + propDefinition: [ + fogbugz, + "ixProjectId", + ], + optional: true, + }, + ixArea: { + propDefinition: [ + fogbugz, + "ixAreaId", + ], + optional: true, + }, + ixCategory: { + propDefinition: [ + fogbugz, + "ixCategoryId", + ({ ixProject }) => ({ + ixProject, + }), + ], + optional: true, + }, + ixPersonId: { + propDefinition: [ + fogbugz, + "ixPersonId", + ], + label: "Ix Person Assigned To", + description: "The Id of the Person assigned to the case.", + optional: true, + }, + ixPriority: { + propDefinition: [ + fogbugz, + "ixPriorityId", + ], + optional: true, + }, + }, + methods: { + async createCase({ + data, ...opts + }) { + return await this.fogbugz.post({ + data: { + cmd: "new", + ...data, + }, + ...opts, + }); + }, + }, + async run({ $ }) { + const response = await this.createCase({ + $, + data: { + ixBugParent: this.ixBugParent, + sTitle: this. sTitle, + sTags: parseObject(this.sTags).toString(), + ixProject: this.ixProject, + ixArea: this.ixArea, + ixCategory: this.ixCategory, + ixPersonAssignedTo: this.ixPersonId, + ixPriority: this.ixPriority, + }, + }); + $.export("$summary", `Successfully created case with Id: ${response.data?.case?.ixBug}`); + return response; + }, +}; diff --git a/components/fogbugz/actions/list-projects/list-projects.mjs b/components/fogbugz/actions/list-projects/list-projects.mjs new file mode 100644 index 0000000000000..59069e9ac2a6b --- /dev/null +++ b/components/fogbugz/actions/list-projects/list-projects.mjs @@ -0,0 +1,48 @@ +import fogbugz from "../../fogbugz.app.mjs"; + +export default { + key: "fogbugz-list-projects", + name: "List Projects", + description: "Gets a list of projects in FogBugz. This can be used to quickly view all projects and their details. [See the documentation](https://support.fogbugz.com/hc/en-us/articles/360011242334-FogBugz-XML-API-Lists)", + version: "0.0.1", + type: "action", + props: { + fogbugz, + fWrite: { + type: "boolean", + label: "fWrite", + description: "Set to 1 if you only want the list of projects you have permission to write to (i.e. can edit the cases in this project). If this is left off or set to 0, then the API assumes you are looking for all areas that you have permission to read.", + optional: true, + }, + fIncludeDeleted: { + type: "boolean", + label: "Include Deleted", + description: "Set to 1 to include deleted projects in the results.", + optional: true, + }, + }, + methods: { + async listProjects({ + data, ...opts + }) { + return await this.fogbugz.post({ + data: { + cmd: "listProjects", + ...data, + }, + ...opts, + }); + }, + }, + async run({ $ }) { + const response = await this.listProjects({ + $, + data: { + fWrite: this.fWrite, + fIncludeDeleted: this.fIncludeDeleted, + }, + }); + $.export("$summary", "Successfully retrieved the list of projects"); + return response; + }, +}; diff --git a/components/fogbugz/actions/update-person/update-person.mjs b/components/fogbugz/actions/update-person/update-person.mjs new file mode 100644 index 0000000000000..e903c01d7b7cb --- /dev/null +++ b/components/fogbugz/actions/update-person/update-person.mjs @@ -0,0 +1,91 @@ +import fogbugz from "../../fogbugz.app.mjs"; + +export default { + key: "fogbugz-update-person", + name: "Update Person", + description: "Edits an existing person in FogBugz. [See the documentation](https://support.fogbugz.com/hc/en-us/articles/360011330733-FogBugz-XML-API-Editing-a-Person)", + version: "0.0.1", + type: "action", + props: { + fogbugz, + ixPersonId: { + propDefinition: [ + fogbugz, + "ixPersonId", + ], + }, + sEmail: { + type: "string", + label: "Email", + description: "The email of the user to update.", + optional: true, + }, + sFullName: { + type: "string", + label: "Full Name", + description: "The full name of the user to update.", + optional: true, + }, + nType: { + type: "string", + label: "Type", + description: "default 0. Set to 0 for a normal user, 1 for an administrator, 2 for a community user, and 3 for a virtual user.", + options: [ + "0", + "1", + "2", + "3", + ], + optional: true, + }, + sPassword: { + type: "string", + label: "Password", + description: "Set a new password to the user.", + secret: true, + optional: true, + }, + sPhone: { + type: "string", + label: "Phone", + description: "Set a new phone to the user.", + optional: true, + }, + fDeleted: { + type: "boolean", + label: "Deleted", + description: "Set fDeleted to True to mark a user as Inactive (does not delete the user, or their history).", + optional: true, + }, + }, + methods: { + async editPerson({ + data, ...opts + }) { + return await this.fogbugz.post({ + data: { + cmd: "editPerson", + ...data, + }, + ...opts, + }); + }, + }, + async run({ $ }) { + const response = await this.editPerson({ + $, + data: { + ixPerson: this.ixPersonId, + sEmail: this.sEmail, + sFullName: this.sFullName, + nType: this.nType, + sPassword: this.sPassword, + sPhone: this.sPhone, + fDeleted: this.fDeleted, + }, + }); + + $.export("$summary", `Successfully updated user with ID ${this.ixPersonId}`); + return response; + }, +}; diff --git a/components/fogbugz/common/constants.mjs b/components/fogbugz/common/constants.mjs new file mode 100644 index 0000000000000..d6d839dab6f8f --- /dev/null +++ b/components/fogbugz/common/constants.mjs @@ -0,0 +1,64 @@ +export const LIMIT = 100; +export const CASE_COLS = [ + "ixBug", + "ixBugParent", + "ixBugChildren", + "tags", + "fOpen", + "sTitle", + "sOriginalTitle", + "sLatestTextSummary", + "ixBugEventLatestText", + "ixProject", + "sProject", + "ixArea", + "sArea", + "ixGroup", + "ixPersonAssignedTo", + "sPersonAssignedTo", + "sEmailAssignedTo", + "ixPersonOpenedBy", + "ixPersonResolvedBy", + "ixPersonClosedBy", + "ixPersonLastEditedBy", + "ixStatus", + "ixBugDuplicates", + "ixBugOriginal", + "sStatus", + "ixPriority", + "sPriority", + "ixFixFor", + "sFixFor", + "dtFixFor", + "sVersion", + "sComputer", + "hrsOrigEst", + "hrsCurrEst", + "hrsElapsed", + "c", + "sCustomerEmail", + "ixMailbox", + "ixCategory", + "sCategory", + "dtOpened", + "dtResolved", + "dtClosed", + "ixBugEventLatest", + "dtLastUpdated", + "fReplied", + "fForwarded", + "sTicket", + "ixDiscussTopic", + "dtDue", + "sReleaseNotes", + "ixBugEventLastView", + "dtLastView", + "ixRelatedBugs", + "sScoutDescription", + "sScoutMessage", + "fScoutStopReporting", + "dtLastOccurrence", + "fSubscribed", + "dblStoryPts", + "nFixForOrder", +]; diff --git a/components/fogbugz/common/utils.mjs b/components/fogbugz/common/utils.mjs new file mode 100644 index 0000000000000..154701004e72a --- /dev/null +++ b/components/fogbugz/common/utils.mjs @@ -0,0 +1,18 @@ +export const parseObject = (obj) => { + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + return JSON.parse(obj); + } + return obj; +}; diff --git a/components/fogbugz/fogbugz.app.mjs b/components/fogbugz/fogbugz.app.mjs index 641d2363ee737..fb5e9c74917bb 100644 --- a/components/fogbugz/fogbugz.app.mjs +++ b/components/fogbugz/fogbugz.app.mjs @@ -1,11 +1,197 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "fogbugz", - propDefinitions: {}, + propDefinitions: { + filterId: { + type: "string", + label: "Filter ID", + description: "The ID of the filter to monitor for new cases.", + async options() { + const { data: { filters } } = await this.listFilters({ + cmd: "listFilter", + }); + + return filters.filter((item) => item.sFilter && item["#cdata-section"]).map((filter) => ({ + label: filter["#cdata-section"], + value: filter.sFilter, + })); + }, + }, + ixAreaId: { + type: "string", + label: "Ix Area Id", + description: "The Id of the Area related with the case.", + async options({ ixProject }) { + const { data: { areas } } = await this.post({ + data: { + cmd: "listAreas", + fWrite: 1, + ixProject, + }, + }); + + return areas.map(({ + ixArea: value, sArea: label, + }) => ({ + label, + value, + })); + }, + }, + ixBugParentId: { + type: "string", + label: "Ix Bug Parent Id", + description: "Make this case a subcase of another case.", + async options() { + const { data: { cases } } = await this.post({ + data: { + cmd: "listCases", + cols: [ + "sTitle", + ], + }, + }); + + return cases.map(({ + ixBug: value, sTitle, + }) => ({ + label: `${sTitle} - ID: ${value}`, + value, + })); + }, + }, + ixCategoryId: { + type: "string", + label: "Ix Category Id", + description: "The Id of the Category related with the case.", + async options() { + const { data: { categories } } = await this.post({ + data: { + cmd: "listCategories", + }, + }); + + return categories.map(({ + ixCategory: value, sCategory: label, + }) => ({ + label, + value, + })); + }, + }, + ixPersonId: { + type: "string", + label: "User ID", + description: "The ID of the user (ixPerson) to edit.", + async options() { + const { data: { people } } = await this.post({ + data: { + cmd: "listPeople", + fWrite: 1, + }, + }); + + return people.map(({ + ixPerson: value, sFullName, sEmail, + }) => ({ + label: `${sFullName} - ${sEmail}`, + value, + })); + }, + }, + ixPriorityId: { + type: "string", + label: "Ix Priority Id", + description: "The Id of the Priority related with the case.", + async options() { + const { data: { priorities } } = await this.post({ + data: { + cmd: "listPriorities", + }, + }); + + return priorities.map(({ + ixPriority: value, sPriority: label, + }) => ({ + label, + value, + })); + }, + }, + ixProjectId: { + type: "string", + label: "Ix Project Id", + description: "The Id of the Project related with the case.", + async options() { + const { data: { projects } } = await this.post({ + data: { + cmd: "listProjects", + fWrite: 1, + }, + }); + + return projects.map(({ + ixProject: value, sProject: label, + }) => ({ + label, + value, + })); + }, + }, + sTags: { + type: "string[]", + label: "sTags", + description: "A list of tags associated with the case.", + async options() { + const { data: { tags } } = await this.post({ + data: { + cmd: "listTags", + }, + }); + + return tags.map(({ + ixTag: value, sTag: label, + }) => ({ + label, + value, + })); + }, + }, + sTitle: { + type: "string", + label: "Case Title", + description: "The title of the case to create.", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `https://${this.$auth.your_site}.fogbugz.com/f/api/0/jsonapi`; + }, + _data(data) { + return { + ...data, + "token": `${this.$auth.api_token}`, + }; + }, + _makeRequest({ + $ = this, data, ...opts + }) { + const config = { + url: this._baseUrl(), + data: this._data(data), + ...opts, + }; + + return axios($, config); + }, + post(opts = {}) { + return this._makeRequest({ + method: "POST", + ...opts, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/fogbugz/package.json b/components/fogbugz/package.json index 8d6cf54ee126a..8eddcfd75158e 100644 --- a/components/fogbugz/package.json +++ b/components/fogbugz/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/fogbugz", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream FogBugz Components", "main": "fogbugz.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" } -} \ No newline at end of file +} diff --git a/components/fogbugz/sources/common/base.mjs b/components/fogbugz/sources/common/base.mjs new file mode 100644 index 0000000000000..2162c6165ffde --- /dev/null +++ b/components/fogbugz/sources/common/base.mjs @@ -0,0 +1,80 @@ +import { + ConfigurationError, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import fogbugz from "../../fogbugz.app.mjs"; + +export default { + props: { + fogbugz, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + getInitialValue() { + return 0; + }, + _getLastId() { + return this.db.get("lastId") || this.getInitialValue(); + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + getData() { + throw new ConfigurationError("getData is not implemented"); + }, + getIdField() { + throw new ConfigurationError("getIdField is not implemented"); + }, + getDataField() { + throw new ConfigurationError("getDataField is not implemented"); + }, + getSummary() { + throw new ConfigurationError("getSummary is not implemented"); + }, + async emitCaseEvents(maxResults = false) { + const lastId = this._getLastId(); + const { data } = await this.fogbugz.post({ + data: { + ...this.getData(), + max: 100000, + }, + }); + + const idField = this.getIdField(); + const dataField = this.getDataField(); + + const responseArray = data[dataField] + .filter((item) => item[idField] > lastId) + .sort((a, b) => b[idField] - a[idField]); + + if (responseArray.length) { + this._setLastId(responseArray[0][idField]); + } + + if (maxResults && responseArray.length > maxResults) { + responseArray.length = maxResults; + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item[idField], + summary: this.getSummary(item), + ts: new Date(), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitCaseEvents(25); + }, + }, + async run() { + await this.emitCaseEvents(); + }, +}; diff --git a/components/fogbugz/sources/new-case-in-filter/new-case-in-filter.mjs b/components/fogbugz/sources/new-case-in-filter/new-case-in-filter.mjs new file mode 100644 index 0000000000000..e6cf3facdcfdd --- /dev/null +++ b/components/fogbugz/sources/new-case-in-filter/new-case-in-filter.mjs @@ -0,0 +1,41 @@ +import { CASE_COLS } from "../../common/constants.mjs"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "fogbugz-new-case-in-filter", + name: "New Case in Filter", + description: "Emit new new event when there's a new case under a specified filter. Note this may not effectively work for filters that generate results too long, or filters with more than 50,000 cases, especially if your FogBugz site is running Ocelot.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + criteria: { + type: "string", + label: "criteria", + description: "The filter you want to apply to the search. For more details about how to combine multiple search criteria read our [Ultimate FogBugz Search Guide](https://support.fogbugz.com/hc/en-us/articles/360011257174-Searching-in-FogBugz-Syntax-and-the-Search-Axis-the-Ultimate-Guide-for-Complex-FogBugz-Searches#case-axes).", + }, + }, + methods: { + ...common.methods, + getData() { + return { + cmd: "search", + q: this.criteria, + cols: CASE_COLS, + }; + }, + getDataField() { + return "cases"; + }, + getIdField() { + return "ixBug"; + }, + getSummary(item) { + return `New case event created with Id: ${item.ixBug}`; + }, + }, + sampleEmit, +}; diff --git a/components/fogbugz/sources/new-case-in-filter/test-event.mjs b/components/fogbugz/sources/new-case-in-filter/test-event.mjs new file mode 100644 index 0000000000000..d75a8e7cbb64c --- /dev/null +++ b/components/fogbugz/sources/new-case-in-filter/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "ixBug":1, + "operations": [ + "edit", + "assign", + "resolve", + "email" + ], + "sTitle":"Case 01", + "sStatus":"Active", + "ixBugParent":0 +} \ No newline at end of file diff --git a/components/fogbugz/sources/new-caseevent/new-caseevent.mjs b/components/fogbugz/sources/new-caseevent/new-caseevent.mjs new file mode 100644 index 0000000000000..506d27fa9e2c4 --- /dev/null +++ b/components/fogbugz/sources/new-caseevent/new-caseevent.mjs @@ -0,0 +1,35 @@ +import { CASE_COLS } from "../../common/constants.mjs"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "fogbugz-new-caseevent", + name: "New Case Event", + description: "Emit new event instantaneously when something significant happens to a case.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getInitialValue() { + return "1970-01-01T00:00:00Z"; + }, + getData() { + return { + cmd: "listCases", + cols: CASE_COLS, + }; + }, + getDataField() { + return "cases"; + }, + getIdField() { + return "dtLastUpdated"; + }, + getSummary(item) { + return `New case event created with Id: ${item.ixBug}`; + }, + }, + sampleEmit, +}; diff --git a/components/fogbugz/sources/new-caseevent/test-event.mjs b/components/fogbugz/sources/new-caseevent/test-event.mjs new file mode 100644 index 0000000000000..d75a8e7cbb64c --- /dev/null +++ b/components/fogbugz/sources/new-caseevent/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "ixBug":1, + "operations": [ + "edit", + "assign", + "resolve", + "email" + ], + "sTitle":"Case 01", + "sStatus":"Active", + "ixBugParent":0 +} \ No newline at end of file diff --git a/components/fogbugz/sources/new-person/new-person.mjs b/components/fogbugz/sources/new-person/new-person.mjs new file mode 100644 index 0000000000000..319f9442680f0 --- /dev/null +++ b/components/fogbugz/sources/new-person/new-person.mjs @@ -0,0 +1,30 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "fogbugz-new-person", + name: "New Person Created", + description: "Emit new event when a new person or a user is created in FogBugz. It effectively tracks the addition of new users in the system.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getData() { + return { + cmd: "listPeople", + }; + }, + getDataField() { + return "people"; + }, + getIdField() { + return "ixPerson"; + }, + getSummary(item) { + return `New Person Created: ${item.sFullName} (${item.sEmail})`; + }, + }, + sampleEmit, +}; diff --git a/components/fogbugz/sources/new-person/test-event.mjs b/components/fogbugz/sources/new-person/test-event.mjs new file mode 100644 index 0000000000000..94fcf3d402784 --- /dev/null +++ b/components/fogbugz/sources/new-person/test-event.mjs @@ -0,0 +1,21 @@ +export default { + "ixPerson": 0, + "sFullName": "Full Name", + "sEmail": "email@test.com", + "sPhone": "123456789", + "fAdministrator": true, + "fCommunity": false, + "fVirtual": false, + "fDeleted": false, + "fNotify": true, + "sHomepage": "", + "sLocale": "*", + "sLanguage": "*", + "sTimeZoneKey": "*", + "sLDAPUid": "", + "dtLastActivity": "", + "fRecurseBugChildren": true, + "fPaletteExpanded": false, + "ixBugWorkingOn": 0, + "sFrom": "" +} \ No newline at end of file diff --git a/components/follow_up_boss/README.md b/components/follow_up_boss/README.md new file mode 100644 index 0000000000000..e06fcacf69de5 --- /dev/null +++ b/components/follow_up_boss/README.md @@ -0,0 +1,11 @@ +# Overview + +The Follow Up Boss API allows you to interact with your customer relationship management data programmatically. With it, you can create, read, update, and delete contacts, deals, and communication logs. On Pipedream, you leverage this API to build automations and integrations that streamline your sales process, synchronize data across platforms, and trigger personalized client interactions based on specific events or conditions. + +# Example Use Cases + +- **Sync New Contacts to a Google Sheet**: When new contacts are added to Follow Up Boss, automatically add them to a Google Sheet. This is useful for sharing leads with team members who prefer to see data in spreadsheet form or for creating backup copies of contact data. + +- **Send Custom Email Campaigns Using SendGrid**: Craft and send personalized email campaigns to segmented groups of contacts in Follow Up Boss using SendGrid. Trigger the emails based on specific tags or lead status updates, ensuring your communication is timely and relevant. + +- **Create Slack Notifications for High-Value Deals**: Set up alerts in a designated Slack channel when high-value deals are added or updated in Follow Up Boss. Keep your team instantly informed about significant sales opportunities to coordinate efforts and close deals faster. diff --git a/components/followup/README.md b/components/followup/README.md index 1e8acccae39a5..398959cfc54ad 100644 --- a/components/followup/README.md +++ b/components/followup/README.md @@ -1,19 +1,11 @@ # Overview -Followup is a great tool for automating tasks and keeping your team organized. -With the Followup API, you can integrate Followup into your own apps and -systems to automate tasks and keep your team organized. Here are some examples -of what you can build with the Followup API: +The Followup API facilitates the automation of email follow-ups, allowing users to schedule, track, and manage follow-up emails directly within their workflow. By integrating with Pipedream, you can leverage the API to craft intricate automation sequences that can improve response rates, ensure timely engagements, and enhance overall email productivity. Taking advantage of Pipedream's serverless platform, you can connect the Followup API with hundreds of other services to streamline communication tasks, set up reminders, or trigger actions based on email interactions. -- A task management system that automatically assigns tasks to team members and - sends reminders -- A customer relationship management (CRM) system that keeps track of customer - interactions and follow-ups -- A project management system that automatically assigns tasks to team members - and tracks project progress -- An email marketing system that sends automated follow-up emails to customers - and prospects -- A sales pipeline management system that keeps track of sales leads and - follow-ups -- A human resources (HR) system that automatically tracks employee vacations - and time off +# Example Use Cases + +- **Sales Lead Nurturing**: Automate follow-up emails to leads captured through a CRM like Salesforce. When a new lead is added to a sales pipeline, trigger a sequence of personalized follow-up emails through Followup to maintain engagement and move leads through the sales funnel. + +- **Customer Support Ticket Reminders**: Create a workflow where new support tickets from a platform like Zendesk automatically set up follow-up reminders. This ensures that customers receive timely updates, and support agents are reminded to close pending tickets. + +- **Event Follow-ups**: After an event, you can automatically send personalized follow-up emails to attendees. Integrate the Followup API with event management platforms such as Eventbrite, triggering customized email sequences thanking the attendees and providing additional resources or next steps. diff --git a/components/followup/package.json b/components/followup/package.json new file mode 100644 index 0000000000000..919fc2ea32180 --- /dev/null +++ b/components/followup/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/followup", + "version": "0.6.0", + "description": "Pipedream followup Components", + "main": "followup.app.mjs", + "keywords": [ + "pipedream", + "followup" + ], + "homepage": "https://pipedream.com/apps/followup", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/fomo/README.md b/components/fomo/README.md index 531fcc139ca48..6706da37a7323 100644 --- a/components/fomo/README.md +++ b/components/fomo/README.md @@ -1,16 +1,11 @@ # Overview -With the Fomo API, you can build a variety of applications that can help -increase engagement and conversions on your website or online store. Examples -of applications that can be built using the Fomo API include: +The Fomo API enables real-time social proof by broadcasting customer interactions, like purchases or sign-ups, to site visitors, fostering a sense of urgency and trust. Leveraging this API on Pipedream, developers can create dynamic, event-driven workflows that respond to these social cues with automated actions like sending emails, updating CRM records, or triggering custom notifications. -- A social proof notifications app that can display recent customer activity on - your site in real-time, such as recent purchases, sign-ups, or downloads. -- An email capture form that can be triggered when a user shows interest in a - product or service on your site. -- A live chat widget that can be used to engage with visitors in real-time and - answer any questions they may have. -- A referral program that can incentivize users to spread the word about your - products or services. -- A live blog or news feed that can keep visitors up-to-date on the latest - happenings on your site. +# Example Use Cases + +- **Boost Email Marketing with Real-time Triggers**: When a new purchase event is broadcasted by Fomo, a Pipedream workflow can automatically capture this event and use it to trigger a personalized follow-up email to the buyer, perhaps offering a discount on their next purchase or requesting a product review. + +- **Enhanced Customer Relations with CRM Integration**: Upon receiving a new 'sign-up' event via Fomo, Pipedream can seamlessly update a CRM platform like Salesforce or HubSpot, adding the new user to a targeted outreach campaign, ensuring they're engaged with relevant content from the outset. + +- **Social Proof Driven Inventory Management**: Fomo's 'product sold' events can be utilized by Pipedream workflows to monitor sales in real-time, interfacing with inventory management systems to adjust stock levels, reorder products, or analyze sales trends for future planning. diff --git a/components/forcemanager/README.md b/components/forcemanager/README.md new file mode 100644 index 0000000000000..dc86bd8b4a5e9 --- /dev/null +++ b/components/forcemanager/README.md @@ -0,0 +1,11 @@ +# Overview + +The ForceManager API allows for robust interaction with ForceManager's CRM platform. It facilitates access to data on sales, customer interactions, and performance metrics. With Pipedream, you can automate actions based on this data, integrate with other services, and create custom workflows that leverage events and triggers from ForceManager to streamline sales processes and data management. Interacting with the ForceManager API through Pipedream allows you to connect CRM data with other applications such as email, calendars, support systems, and more, creating a seamless flow of information across your business tools. + +# Example Use Cases + +- **Sync ForceManager Contacts with a Google Sheet**: Use the ForceManager API to pull contact data and automatically update a Google Sheet in real-time. This keeps your contact list current and can serve as a shared resource for cross-departmental information sharing. + +- **Create Slack Notifications for New Deals**: Set up a workflow where Pipedream listens for new deals in ForceManager and sends a message to a designated Slack channel. This keeps your team instantly informed about new sales opportunities and encourages quick action. + +- **Automate Weekly Sales Reports**: Generate automated weekly sales reports by aggregating data from ForceManager and sending it to an email address or storing it in Google Drive. This reduces manual work and ensures that stakeholders always have the latest performance data. diff --git a/components/forcemanager/actions/create-opportunity/create-opportunity.mjs b/components/forcemanager/actions/create-opportunity/create-opportunity.mjs new file mode 100644 index 0000000000000..fad8d7824dbe6 --- /dev/null +++ b/components/forcemanager/actions/create-opportunity/create-opportunity.mjs @@ -0,0 +1,96 @@ +import forcemanager from "../../forcemanager.app.mjs"; + +export default { + key: "forcemanager-create-opportunity", + name: "Create Opportunity", + description: "Creates a new business opportunity in ForceManager. [See the documentation](https://developer.forcemanager.com/#836754be-f32d-47d2-a8ab-73a147c62ca9)", + version: "0.0.1", + type: "action", + props: { + forcemanager, + name: { + type: "string", + label: "Name", + description: "Name of the business opportunity", + }, + accountId: { + propDefinition: [ + forcemanager, + "accountId", + ], + }, + branchId: { + propDefinition: [ + forcemanager, + "branchId", + ], + }, + statusId: { + propDefinition: [ + forcemanager, + "statusId", + ], + }, + salesProbability: { + type: "integer", + label: "Sales Probability", + description: "Probability of sale, a number between 0 and 10", + max: 10, + }, + salesRepId: { + propDefinition: [ + forcemanager, + "salesRepId", + ], + }, + salesForeCastDate: { + type: "string", + label: "Sales Forecast Date", + description: "Forecast sale date in ISO-8601 Format. Example: `2023-12-08T10:00:00+07:00`", + optional: true, + }, + currencyId: { + propDefinition: [ + forcemanager, + "currencyId", + ], + }, + total: { + type: "integer", + label: "Total", + description: "Total amount of the Opportunity", + optional: true, + }, + permissionLevel: { + type: "string", + label: "PermissionLevel", + description: "Defines the visibility of the Opportunity. Set from 1 - 5 with 5 being the highest level of permission", + options: [ + "1", + "2", + "3", + "4", + "5", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.forcemanager.createOpportunity({ + $, + data: { + reference: this.name, + accountId1: this.accountId, + branchId: this.branchId, + statusId: this.statusId, + salesProbability: this.salesProbability, + salesRepId: this.salesRepId, + salesForeCastDate: this.salesForeCastDate, + total: this.total, + permissionLevel: this.permissionLevel, + }, + }); + $.export("$summary", `Successfully created opportunity: ${this.name}`); + return response; + }, +}; diff --git a/components/forcemanager/actions/find-contact/find-contact.mjs b/components/forcemanager/actions/find-contact/find-contact.mjs new file mode 100644 index 0000000000000..c660943a0ec3c --- /dev/null +++ b/components/forcemanager/actions/find-contact/find-contact.mjs @@ -0,0 +1,55 @@ +import forcemanager from "../../forcemanager.app.mjs"; + +export default { + key: "forcemanager-find-contact", + name: "Find Contact", + description: "Search for an existing contact by email, name, or phone. [See the documentation](https://developer.forcemanager.com/#c1c37cd1-5cb9-473f-8918-7583ee0469e4)", + version: "0.0.1", + type: "action", + props: { + forcemanager, + email: { + type: "string", + label: "Email", + description: "Email address to search for", + optional: true, + }, + firstName: { + type: "string", + label: "First Name", + description: "First name to search for", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name to search for", + optional: true, + }, + phone1: { + type: "string", + label: "Phone", + description: "Phone number to search for", + optional: true, + }, + }, + async run({ $ }) { + const { + forcemanager, + ...fields + } = this; + const conditions = Object.keys(fields)?.length + ? Object.keys(fields).map((key) => `${key}='${fields[key]}'`) + : []; + const response = await forcemanager.listContacts({ + $, + params: { + where: conditions.join(" AND "), + }, + }); + $.export("$summary", `Found ${response.length} contact${response.length === 1 + ? "" + : "s"}`); + return response; + }, +}; diff --git a/components/forcemanager/actions/list-users/list-users.mjs b/components/forcemanager/actions/list-users/list-users.mjs new file mode 100644 index 0000000000000..4700c56a9e00b --- /dev/null +++ b/components/forcemanager/actions/list-users/list-users.mjs @@ -0,0 +1,28 @@ +import forcemanager from "../../forcemanager.app.mjs"; + +export default { + key: "forcemanager-list-users", + name: "List All Users", + description: "Returns a list of all users in the app. [See the documentation](https://developer.forcemanager.com/#d91467e1-c380-4cce-8207-840b570a5471)", + version: "0.0.1", + type: "action", + props: { + forcemanager, + }, + async run({ $ }) { + const results = this.forcemanager.paginate({ + resourceFn: this.forcemanager.listUsers, + args: { + $, + }, + }); + const users = []; + for await (const user of results) { + users.push(user); + } + $.export("$summary", `Successfully retrieved ${users.length} user${users.length === 1 + ? "" + : "s"}`); + return users; + }, +}; diff --git a/components/forcemanager/forcemanager.app.mjs b/components/forcemanager/forcemanager.app.mjs index 6d28ea21d8712..7728aed4d879f 100644 --- a/components/forcemanager/forcemanager.app.mjs +++ b/components/forcemanager/forcemanager.app.mjs @@ -1,11 +1,190 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "forcemanager", - propDefinitions: {}, + propDefinitions: { + accountId: { + type: "string", + label: "Account ID", + description: "Account associated with the business opportunity", + optional: true, + async options({ page }) { + const accounts = await this.listAccounts({ + params: { + page, + }, + }); + return accounts?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + salesRepId: { + type: "string", + label: "Sales Rep ID", + description: "The User associated with the Opportunity", + async options({ page }) { + const users = await this.listUsers({ + params: { + page, + }, + }); + return users?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + branchId: { + type: "string", + label: "Branch ID", + description: "Branch to whom the Opportunity has been assigned", + async options() { + const branches = await this.listBranches(); + return branches?.map(({ + id: value, descriptionES: label, + }) => ({ + value, + label, + })) || []; + }, + }, + statusId: { + type: "string", + label: "Status ID", + description: "Status of the Opportunity", + async options() { + const statuses = await this.listOpportunityStatuses(); + return statuses?.map(({ + id: value, descriptionUS: label, + }) => ({ + value, + label, + })) || []; + }, + }, + currencyId: { + type: "string", + label: "Currency ID", + description: "Currency related to the Opportunity amount", + optional: true, + async options() { + const currencies = await this.listCurrencies(); + return currencies?.map(({ + id: value, descriptionEN: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.forcemanager.com/api/v4"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "Content-Type": "application/json", + "Accept": "*/*", + "X-Session-Key": `${this.$auth.oauth_access_token}`, + }, + }); + }, + listAccounts(opts = {}) { + return this._makeRequest({ + path: "/accounts", + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users", + ...opts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts", + ...opts, + }); + }, + listBranches(opts = {}) { + return this._makeRequest({ + path: "/branches", + ...opts, + }); + }, + listOpportunities(opts = {}) { + return this._makeRequest({ + path: "/opportunities", + ...opts, + }); + }, + listActivities(opts = {}) { + return this._makeRequest({ + path: "/activities", + ...opts, + }); + }, + listCurrencies(opts = {}) { + return this._makeRequest({ + path: "/currencies", + ...opts, + }); + }, + listOpportunityStatuses(opts = {}) { + return this._makeRequest({ + path: "/opportunityStatuses", + ...opts, + }); + }, + createOpportunity(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/opportunities", + ...opts, + }); + }, + async *paginate({ + resourceFn, + args, + max, + }) { + let hasMore, count = 0; + args = { + ...args, + params: { + ...args.params, + page: 0, + }, + }; + do { + const results = await resourceFn(args); + for (const item of results) { + yield item; + count++; + if (max && count >= max) { + return; + } + } + hasMore = results?.length; + args.params.page++; + } while (hasMore); }, }, }; diff --git a/components/forcemanager/package.json b/components/forcemanager/package.json index e302a1c8115a1..e110bb8c38d2e 100644 --- a/components/forcemanager/package.json +++ b/components/forcemanager/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/forcemanager", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream ForceManager Components", "main": "forcemanager.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.4" } -} \ No newline at end of file +} diff --git a/components/forcemanager/sources/common/base.mjs b/components/forcemanager/sources/common/base.mjs new file mode 100644 index 0000000000000..a0ca2c51c31ce --- /dev/null +++ b/components/forcemanager/sources/common/base.mjs @@ -0,0 +1,75 @@ +import forcemanager from "../../forcemanager.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + forcemanager, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + emitEvent(item) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }, + generateMeta(item) { + return { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item.dateCreated), + }; + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + const resourceFn = this.getResourceFn(); + const results = this.forcemanager.paginate({ + resourceFn, + args: { + params: { + order: "dateCreated", + }, + }, + max, + }); + const items = []; + for await (const item of results) { + const ts = Date.parse(item.dateCreated); + if (ts > lastTs) { + items.push(item); + } else { + break; + } + } + if (!items?.length) { + return; + } + this._setLastTs(Date.parse(items[0].dateCreated)); + items.reverse().forEach((item) => this.emitEvent(item)); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/forcemanager/sources/new-activity/new-activity.mjs b/components/forcemanager/sources/new-activity/new-activity.mjs new file mode 100644 index 0000000000000..a22d0c913b324 --- /dev/null +++ b/components/forcemanager/sources/new-activity/new-activity.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "forcemanager-new-activity", + name: "New Activity", + description: "Emit new event when a new activity is created. [See the documentation](https://developer.forcemanager.com/#eadff4f2-ef77-44f3-ac48-16fe732ea127)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.forcemanager.listActivities; + }, + getSummary(activity) { + return `New Activity: ${activity.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/forcemanager/sources/new-activity/test-event.mjs b/components/forcemanager/sources/new-activity/test-event.mjs new file mode 100644 index 0000000000000..a8b872ea6cc23 --- /dev/null +++ b/components/forcemanager/sources/new-activity/test-event.mjs @@ -0,0 +1,42 @@ +export default { + "accountId": { + "id": 1186, + "value": "Polaroid" + }, + "checkin": false, + "checkinTypeId": 1, + "checkoutDate": null, + "comment": "Take samples", + "contactId": { + "id": 1056, + "value": "Roger Chase" + }, + "date": "2015-07-09T10:59:00Z", + "dateCreated": "2015-07-09T11:00:06.137Z", + "dateDeleted": null, + "dateUpdated": "2018-06-12T09:20:40.96Z", + "deleted": false, + "extId": null, + "geocoded": true, + "geocodingAccuracy": 1, + "id": 799, + "latitude": 41.3910821, + "longitude": 2.1291351, + "opportunityId": { + "id": 823, + "value": "Opportunity 2015-7" + }, + "permissionLevel": 2, + "readOnly": false, + "salesRepId": { + "id": 5548, + "value": "Luis Sánchez" + }, + "salesRepIdCreated": 5548, + "salesRepIdDeleted": null, + "salesRepIdUpdated": 51, + "typeId": { + "id": 6417, + "value": "Video call" + } +} \ No newline at end of file diff --git a/components/forcemanager/sources/new-contact/new-contact.mjs b/components/forcemanager/sources/new-contact/new-contact.mjs new file mode 100644 index 0000000000000..3af4ded1c4c07 --- /dev/null +++ b/components/forcemanager/sources/new-contact/new-contact.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "forcemanager-new-contact", + name: "New Contact", + description: "Emit new event when a new contact is created. [See the documentation](https://developer.forcemanager.com/#c1c37cd1-5cb9-473f-8918-7583ee0469e4)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.forcemanager.listContacts; + }, + getSummary(contact) { + return `New Contact: ${contact.firstName} ${contact.lastName}`; + }, + }, + sampleEmit, +}; diff --git a/components/forcemanager/sources/new-contact/test-event.mjs b/components/forcemanager/sources/new-contact/test-event.mjs new file mode 100644 index 0000000000000..06c838ac84975 --- /dev/null +++ b/components/forcemanager/sources/new-contact/test-event.mjs @@ -0,0 +1,51 @@ +export default { + "UseCompanyAddress": true, + "accountId": { + "id": 1, + "value": "" + }, + "address1": "2765 Smith Street", + "birthday": "2021-10-02T00:00:00Z", + "city": "St. Louis", + "comment": "Very professional", + "countryId": { + "id": 75, + "value": "United States" + }, + "dateCreated": "2021-12-16T14:05:53.027Z", + "dateDeleted": null, + "dateUpdated": null, + "deleted": false, + "email": "j.doe@email.com", + "email2": null, + "email3": null, + "extId": null, + "fax": "+39 789-5642", + "firstName": "John", + "gender": { + "id": 0, + "value": "Male" + }, + "id": 287, + "lastName": "Doe", + "linkedin": "John_Doe", + "marketingCommunications": true, + "permissionLevel": 2, + "phone1": "+39 687-9234", + "phone2": "+39 982-6501", + "postcode": "63124", + "readOnly": false, + "region": "MO", + "salesRepId": { + "id": 310, + "value": "" + }, + "salesRepIdCreated": 90, + "salesRepIdDeleted": null, + "salesRepIdUpdated": null, + "skype": "John_Doe", + "typeId": { + "id": 9, + "value": "Department of Administration" + } +} \ No newline at end of file diff --git a/components/forcemanager/sources/new-opportunity/new-opportunity.mjs b/components/forcemanager/sources/new-opportunity/new-opportunity.mjs new file mode 100644 index 0000000000000..eb15159e1e6c9 --- /dev/null +++ b/components/forcemanager/sources/new-opportunity/new-opportunity.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "forcemanager-new-opportunity", + name: "New Opportunity", + description: "Emit new event when a new opportunity is created. [See the documentation](https://developer.forcemanager.com/#742a3f5b-4701-46e8-b6bd-36488e233752)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.forcemanager.listOpportunities; + }, + getSummary(opportunity) { + return `New Opportunity: ${opportunity.reference}`; + }, + }, + sampleEmit, +}; diff --git a/components/forcemanager/sources/new-opportunity/test-event.mjs b/components/forcemanager/sources/new-opportunity/test-event.mjs new file mode 100644 index 0000000000000..7823c80c2ea88 --- /dev/null +++ b/components/forcemanager/sources/new-opportunity/test-event.mjs @@ -0,0 +1,55 @@ +export default { + "accountId1": { + "id": 1757, + "value": "Universal ltd" + }, + "accountId2": null, + "accountId3": null, + "address1": "Street Anthon, 125", + "address2": null, + "branchId": { + "id": 43, + "value": "Main unit" + }, + "city": "London", + "closedDate": null, + "comments": "It is an important opportunity", + "countryId": null, + "currencyId": { + "id": 1, + "value": "euro" + }, + "dateCreated": "2021-12-16T21:44:46.333Z", + "dateDeleted": null, + "dateGeocoded": null, + "dateUpdated": null, + "deleted": false, + "extId": null, + "geocoded": true, + "geocodingAccuracy": 20, + "id": 127, + "latitude": 41.3916328, + "longitude": 2.1293214, + "lostDate": null, + "permissionLevel": 0, + "postcode": "08950", + "readOnly": false, + "reference": "10x packages", + "region": null, + "salesForecastDate": null, + "salesProbability": 10, + "salesRepId": { + "id": 558, + "value": "Tadala Asnappar" + }, + "salesRepIdCreated": 90, + "salesRepIdDeleted": null, + "salesRepIdUpdated": null, + "statusId": { + "id": 7, + "value": "In progress" + }, + "total": 34560, + "type_id": null, + "wonDate": null +} \ No newline at end of file diff --git a/components/form_io/README.md b/components/form_io/README.md new file mode 100644 index 0000000000000..5363790db88d4 --- /dev/null +++ b/components/form_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The Form.io API empowers developers to manage forms and submissions on Pipedream, enabling the automation of workflows that require data collection and processing. With Form.io on Pipedream, you can create, update, and fetch forms, as well as handle form submissions. This is ideal for tasks like dynamically generating forms based on user input, streamlining data collection processes, and integrating form data with other services for further analysis or action. + +# Example Use Cases + +- **Form Submission Trigger to Slack Notification**: When a new form submission is received in Form.io, trigger a Pipedream workflow to send a custom alert to a Slack channel, informing the team about the submission details. + +- **Dynamic Form Creation and Data Storage**: Automatically create a new form in Form.io using a Pipedream workflow whenever a specific event occurs (like a new product launch), and store the form data in a Pipedream data store for later use or analysis. + +- **Form Data to Google Sheets Integration**: Use Pipedream to send new Form.io submissions to Google Sheets. Each submission can be added as a new row in a sheet, providing easy access for non-technical team members to review and process the data. diff --git a/components/formaloo/README.md b/components/formaloo/README.md new file mode 100644 index 0000000000000..480c9f5c31c5e --- /dev/null +++ b/components/formaloo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Formaloo API lets you interact with your forms and data on Formaloo. You can create new forms, fetch responses, analyze submissions, and more, right within Pipedream. Pipedream's serverless platform facilitates the automation of these tasks, enabling you to integrate Formaloo with hundreds of other services to streamline workflows, analyze data, and synchronize across platforms. + +# Example Use Cases + +- **Automate Response Collection**: Use Pipedream to trigger a workflow every time a new form submission is received on Formaloo. Capture the data, process it, and store it in a database like PostgreSQL or send it to Google Sheets for further analysis. + +- **Dynamic Form Creation**: With the Formaloo API on Pipedream, create workflows that dynamically generate new forms based on events from other apps, such as Trello or Asana. When a new project is created in your project management tool, trigger a workflow that creates a corresponding feedback form in Formaloo. + +- **Form Submission Notifications**: Set up a Pipedream workflow that sends a notification through email, Slack, or Discord whenever a form is submitted. This can keep your team instantly informed about new entries, customer feedback, or survey responses, ensuring timely responses and engagement. diff --git a/components/formatting/actions/add-subtract-time/add-subtract-time.ts b/components/formatting/actions/add-subtract-time/add-subtract-time.ts index dfc2131b3b2a4..b5acfa8062dee 100644 --- a/components/formatting/actions/add-subtract-time/add-subtract-time.ts +++ b/components/formatting/actions/add-subtract-time/add-subtract-time.ts @@ -6,6 +6,7 @@ import { DATE_FORMAT_PARSE_MAP, DEFAULT_FORMAT_VALUE, } from "../../common/date-time/dateFormats"; import { DATE_TIME_UNITS } from "../../common/date-time/dateTimeUnits"; +import sugar from "sugar"; const OPERATION_OPTIONS = { ADD: "Add", @@ -17,7 +18,7 @@ export default defineAction({ name: "[Date/Time] Add/Subtract Time", description: "Add or subtract time from a given input", key: "formatting-add-subtract-time", - version: "0.0.4", + version: "0.0.5", type: "action", props: { ...commonDateTime.props, @@ -85,7 +86,7 @@ export default defineAction({ const format = outputFormat ?? this.inputFormat ?? DEFAULT_FORMAT_VALUE; try { const { outputFn } = DATE_FORMAT_PARSE_MAP.get(format); - const output = outputFn(new Date(result)); + const output = outputFn(sugar.Date.create(result)); $.export( "$summary", @@ -96,6 +97,7 @@ export default defineAction({ ); return output; } catch (err) { + console.log("Error parsing date", err); throw new ConfigurationError("**Parse error** - check your input and if the selected format is correct."); } }, diff --git a/components/formatting/actions/compare-dates/compare-dates.ts b/components/formatting/actions/compare-dates/compare-dates.ts index 6915f8a5ecb01..c52735ac7f779 100644 --- a/components/formatting/actions/compare-dates/compare-dates.ts +++ b/components/formatting/actions/compare-dates/compare-dates.ts @@ -9,7 +9,7 @@ export default defineAction({ description: "Get the duration between two dates in days, hours, minutes, and seconds along with checking if they are the same.", key: "formatting-compare-dates", - version: "0.0.4", + version: "0.0.5", type: "action", props: { ...commonDateTime.props, diff --git a/components/formatting/actions/convert-html-to-markdown/convert-html-to-markdown.ts b/components/formatting/actions/convert-html-to-markdown/convert-html-to-markdown.ts index b7d69d3301340..7ac0ce4fe1650 100644 --- a/components/formatting/actions/convert-html-to-markdown/convert-html-to-markdown.ts +++ b/components/formatting/actions/convert-html-to-markdown/convert-html-to-markdown.ts @@ -7,7 +7,7 @@ export default defineAction({ name: "[Text] Convert HTML to Markdown", description: "Convert valid HTML to Markdown text", key: "formatting-convert-html-to-markdown", - version: "0.0.5", + version: "0.0.6", type: "action", props: { app, diff --git a/components/formatting/actions/convert-html-to-text/convert-html-to-text.ts b/components/formatting/actions/convert-html-to-text/convert-html-to-text.ts index c7208bc71fde2..2c7c9e96970d0 100644 --- a/components/formatting/actions/convert-html-to-text/convert-html-to-text.ts +++ b/components/formatting/actions/convert-html-to-text/convert-html-to-text.ts @@ -6,7 +6,7 @@ export default defineAction({ name: "[Text] Convert HTML to text", description: "Convert valid HTML to text", key: "formatting-convert-html-to-text", - version: "0.0.3", + version: "0.0.4", type: "action", props: { app, diff --git a/components/formatting/actions/convert-json-to-string/convert-json-to-string.ts b/components/formatting/actions/convert-json-to-string/convert-json-to-string.ts index 2e0214e2a06e8..3e8aeca735984 100644 --- a/components/formatting/actions/convert-json-to-string/convert-json-to-string.ts +++ b/components/formatting/actions/convert-json-to-string/convert-json-to-string.ts @@ -5,7 +5,7 @@ export default defineAction({ name: "[Data] Convert JSON to String", description: "Convert an object to a JSON format string", key: "formatting-convert-json-to-string", - version: "0.0.4", + version: "0.0.5", type: "action", props: { app, diff --git a/components/formatting/actions/convert-markdown-to-html/convert-markdown-to-html.ts b/components/formatting/actions/convert-markdown-to-html/convert-markdown-to-html.ts index 11c4d4ccfef93..6b166034730c0 100644 --- a/components/formatting/actions/convert-markdown-to-html/convert-markdown-to-html.ts +++ b/components/formatting/actions/convert-markdown-to-html/convert-markdown-to-html.ts @@ -7,7 +7,7 @@ export default defineAction({ name: "[Text] Convert Markdown to HTML", description: "Convert Markdown text to HTML", key: "formatting-convert-markdown-to-html", - version: "0.0.5", + version: "0.0.6", type: "action", props: { app, diff --git a/components/formatting/actions/date-time-format/date-time-format.ts b/components/formatting/actions/date-time-format/date-time-format.ts index 1550fe76df71b..1b786bc5f0bbe 100644 --- a/components/formatting/actions/date-time-format/date-time-format.ts +++ b/components/formatting/actions/date-time-format/date-time-format.ts @@ -3,14 +3,14 @@ import { ConfigurationError } from "@pipedream/platform"; import { DATE_FORMAT_PARSE_MAP } from "../../common/date-time/dateFormats"; import commonDateTime from "../../common/date-time/commonDateTime"; import app from "../../app/formatting.app"; -import moment from "moment"; +import sugar from "sugar"; export default defineAction({ ...commonDateTime, name: "[Date/Time] Format", - description: "Format a date string to another date string", + description: "Format a date string to another date string. For more examples on formatting, see the [Sugar Date Format](https://sugarjs.com/dates/#/Formatting) documentation.", key: "formatting-date-time-format", - version: "0.0.5", + version: "0.0.6", type: "action", props: { ...commonDateTime.props, @@ -19,7 +19,7 @@ export default defineAction({ app, "outputFormat", ], - description: "The format to convert the date to.", + description: "The format to convert the date to. For more examples on formatting, see the [Sugar Date Format](https://sugarjs.com/dates/#/Formatting) documentation.", optional: false, }, }, @@ -32,7 +32,7 @@ export default defineAction({ const response = DATE_FORMAT_PARSE_MAP.get(outputFormat); const output = response ? response.outputFn(dateObj) - : moment(dateObj).format(outputFormat); + : sugar.Date.format(dateObj, outputFormat); $.export("$summary", "Successfully formatted date/time"); return output; diff --git a/components/formatting/actions/extract-by-regular-expression/extract-by-regular-expression.ts b/components/formatting/actions/extract-by-regular-expression/extract-by-regular-expression.ts index 34a23f54e4f37..a13d59389d186 100644 --- a/components/formatting/actions/extract-by-regular-expression/extract-by-regular-expression.ts +++ b/components/formatting/actions/extract-by-regular-expression/extract-by-regular-expression.ts @@ -7,7 +7,7 @@ export default defineAction({ description: "Find a match for a regular expression pattern. Returns all matched groups with start and end position.", key: "formatting-extract-by-regular-expression", - version: "0.0.4", + version: "0.0.5", type: "action", props: { app, diff --git a/components/formatting/actions/extract-email-address/extract-email-address.ts b/components/formatting/actions/extract-email-address/extract-email-address.ts index 19eb3575e78be..969a20211d30a 100644 --- a/components/formatting/actions/extract-email-address/extract-email-address.ts +++ b/components/formatting/actions/extract-email-address/extract-email-address.ts @@ -7,7 +7,7 @@ export default defineAction({ description: "Find an email address out of a text field. Finds the first email address only.", key: "formatting-extract-email-address", - version: "0.0.5", + version: "0.0.6", type: "action", props: { ...commonExtractText.props, diff --git a/components/formatting/actions/extract-number/extract-number.ts b/components/formatting/actions/extract-number/extract-number.ts index 7aaa2f7556090..69443b4443ffd 100644 --- a/components/formatting/actions/extract-number/extract-number.ts +++ b/components/formatting/actions/extract-number/extract-number.ts @@ -7,7 +7,7 @@ export default defineAction({ description: "Find a number out of a text field. Finds the first number only.", key: "formatting-extract-number", - version: "0.0.4", + version: "0.0.5", type: "action", props: { ...commonExtractText.props, diff --git a/components/formatting/actions/extract-phone-number/extract-phone-number.ts b/components/formatting/actions/extract-phone-number/extract-phone-number.ts index c3e6b90383b25..592e3361d2c81 100644 --- a/components/formatting/actions/extract-phone-number/extract-phone-number.ts +++ b/components/formatting/actions/extract-phone-number/extract-phone-number.ts @@ -7,7 +7,7 @@ export default defineAction({ description: "Find a complete phone number out of a text field. Finds the first number only.", key: "formatting-extract-phone-number", - version: "0.0.4", + version: "0.0.5", type: "action", props: { ...commonExtractText.props, diff --git a/components/formatting/actions/extract-url/extract-url.ts b/components/formatting/actions/extract-url/extract-url.ts index 5bc9e4ece9218..311907d012875 100644 --- a/components/formatting/actions/extract-url/extract-url.ts +++ b/components/formatting/actions/extract-url/extract-url.ts @@ -7,7 +7,7 @@ export default defineAction({ description: "Find a web URL out of a text field. Finds the first URL only.", key: "formatting-extract-url", - version: "0.0.4", + version: "0.0.5", type: "action", props: { ...commonExtractText.props, diff --git a/components/formatting/actions/format-currency/format-currency.ts b/components/formatting/actions/format-currency/format-currency.ts index ef77d9c883e34..1a54078356603 100644 --- a/components/formatting/actions/format-currency/format-currency.ts +++ b/components/formatting/actions/format-currency/format-currency.ts @@ -9,7 +9,7 @@ export default defineAction({ name: "[Numbers] Format Currency", description: "Format a number as a currency", key: "formatting-format-currency", - version: "0.0.4", + version: "0.0.5", type: "action", props: { app, diff --git a/components/formatting/actions/format-number/format-number.ts b/components/formatting/actions/format-number/format-number.ts index d297008408cdd..b8292130f87d6 100644 --- a/components/formatting/actions/format-number/format-number.ts +++ b/components/formatting/actions/format-number/format-number.ts @@ -11,7 +11,7 @@ export default defineAction({ description: "Format a number to a new style. Does not perform any rounding or padding of the number.", key: "formatting-format-number", - version: "0.0.4", + version: "0.0.5", type: "action", props: { app, diff --git a/components/formatting/actions/parse-json/parse-json.ts b/components/formatting/actions/parse-json/parse-json.ts index e70b0492dca73..ad9b72b7085c6 100644 --- a/components/formatting/actions/parse-json/parse-json.ts +++ b/components/formatting/actions/parse-json/parse-json.ts @@ -5,7 +5,7 @@ export default defineAction({ name: "[Data] Parse JSON", description: "Parse a JSON string", key: "formatting-parse-json", - version: "0.0.4", + version: "0.0.5", type: "action", props: { app, diff --git a/components/formatting/actions/replace-text/replace-text.ts b/components/formatting/actions/replace-text/replace-text.ts index d2440017994da..18e8160f3309b 100644 --- a/components/formatting/actions/replace-text/replace-text.ts +++ b/components/formatting/actions/replace-text/replace-text.ts @@ -9,7 +9,7 @@ export default defineAction({ description: "Replace all instances of any character, word or phrase in the text with another character, word or phrase.", key: "formatting-replace-text", - version: "0.0.4", + version: "0.0.5", type: "action", props: { app, diff --git a/components/formatting/actions/set-default-value/set-default-value.ts b/components/formatting/actions/set-default-value/set-default-value.ts index 02b4c8282b026..15ef43aea4e6c 100644 --- a/components/formatting/actions/set-default-value/set-default-value.ts +++ b/components/formatting/actions/set-default-value/set-default-value.ts @@ -5,7 +5,7 @@ export default defineAction({ name: "[Text] Set Default Value", description: "Return a default value if the text is empty", key: "formatting-set-default-value", - version: "0.0.4", + version: "0.0.5", type: "action", props: { app, diff --git a/components/formatting/actions/split-text/split-text.ts b/components/formatting/actions/split-text/split-text.ts index 88bed71daa300..19bd83790c698 100644 --- a/components/formatting/actions/split-text/split-text.ts +++ b/components/formatting/actions/split-text/split-text.ts @@ -10,7 +10,7 @@ export default defineAction({ description: "Split the text on a character or word and return one or all segments", key: "formatting-split-text", - version: "0.0.4", + version: "0.0.5", type: "action", props: { app, @@ -52,22 +52,22 @@ export default defineAction({ summary = `Successfully split text into ${length} segments`; switch (segmentIndex) { - case INDEX_ALL_SEGMENTS: - result = arrResults; - break; + case INDEX_ALL_SEGMENTS: + result = arrResults; + break; // this case would not be needed if 0 was accepted as an option // see issue #5429 - case INDEX_ALL_SEGMENTS * -1: - result = arrResults[0]; - break; + case INDEX_ALL_SEGMENTS * -1: + result = arrResults[0]; + break; - default: - result = - arrResults[segmentIndex < 0 - ? length + segmentIndex - : segmentIndex]; - break; + default: + result = + arrResults[segmentIndex < 0 + ? length + segmentIndex + : segmentIndex]; + break; } } diff --git a/components/formatting/actions/transform-case/transform-case.ts b/components/formatting/actions/transform-case/transform-case.ts index 2b81fd94bf2a2..974b4c60b8a96 100644 --- a/components/formatting/actions/transform-case/transform-case.ts +++ b/components/formatting/actions/transform-case/transform-case.ts @@ -9,7 +9,7 @@ export default defineAction({ name: "[Text] Transform Case", description: "Transform case for a text input", key: "formatting-transform-case", - version: "0.0.4", + version: "0.0.5", type: "action", props: { app, diff --git a/components/formatting/actions/trim-whitespace/trim-whitespace.ts b/components/formatting/actions/trim-whitespace/trim-whitespace.ts index 282bdb3372515..2fd83ebdb787f 100644 --- a/components/formatting/actions/trim-whitespace/trim-whitespace.ts +++ b/components/formatting/actions/trim-whitespace/trim-whitespace.ts @@ -5,7 +5,7 @@ export default defineAction({ name: "[Text] Trim Whitespace", description: "Removes leading and trailing whitespace", key: "formatting-trim-whitespace", - version: "0.0.4", + version: "0.0.5", type: "action", props: { app, diff --git a/components/formatting/actions/url-decode/url-decode.ts b/components/formatting/actions/url-decode/url-decode.ts index f2b2a2afda929..c885a830257a4 100644 --- a/components/formatting/actions/url-decode/url-decode.ts +++ b/components/formatting/actions/url-decode/url-decode.ts @@ -5,7 +5,7 @@ export default defineAction({ name: "[Text] Decode URL", description: "Decode a URL string", key: "formatting-url-decode", - version: "0.0.4", + version: "0.0.5", type: "action", props: { app, diff --git a/components/formatting/actions/url-encode/url-encode.ts b/components/formatting/actions/url-encode/url-encode.ts index aacc089c8c67e..62a10b2d512ae 100644 --- a/components/formatting/actions/url-encode/url-encode.ts +++ b/components/formatting/actions/url-encode/url-encode.ts @@ -5,7 +5,7 @@ export default defineAction({ name: "[Text] Encode URL", description: "Encode a string as a URL", key: "formatting-url-encode", - version: "0.0.4", + version: "0.0.5", type: "action", props: { app, diff --git a/components/formatting/common/date-time/commonDateTime.ts b/components/formatting/common/date-time/commonDateTime.ts index 9aa26cd1a51b6..7180f1f97c694 100644 --- a/components/formatting/common/date-time/commonDateTime.ts +++ b/components/formatting/common/date-time/commonDateTime.ts @@ -9,6 +9,7 @@ export default { props: { app, inputDate: { + description: "A valid date string, in the format selected in **Input Format**. If the format is not set, Pipedream will attempt to infer it using the parser from [Sugar Date library](https://sugarjs.com/dates/#/Parsing).", propDefinition: [ app, "inputDate", diff --git a/components/formatting/common/date-time/dateFormats.ts b/components/formatting/common/date-time/dateFormats.ts index 2a5cd14493e99..8619142b21342 100644 --- a/components/formatting/common/date-time/dateFormats.ts +++ b/components/formatting/common/date-time/dateFormats.ts @@ -1,244 +1,163 @@ +import sugar from "sugar"; + interface DateFormat { - label: string; value: string; inputFn?: (s: string) => Date; outputFn: (d: Date) => string | number; } -export const DEFAULT_FORMAT_VALUE = "YYYY-MM-DDTHH:mm:ssZ"; +export const DEFAULT_FORMAT_VALUE = "2006-01-22T23:04:05+0000"; export const DEFAULT_INPUT_FUNCTION: DateFormat["inputFn"] = (str) => { const num = Number(str); - return new Date(num * 1000 || str); + return sugar.Date.create(num * 1000 || str); }; // https://tc39.es/ecma402/#table-datetimeformat-components const DATE_FORMATS: DateFormat[] = [ { - label: "Sun Jan 22 23:04:05 -0000 2006", - value: "ddd MMM DD HH:mm:ss Z YYYY", + value: "Sun Jan 22 23:04:05 +0000 2006", outputFn(dateObj) { - const date = dateObj - .toLocaleDateString("en-US", { - weekday: "short", - day: "2-digit", - month: "short", - }) - .replace(/,/g, ""); - - const time = dateObj.toLocaleTimeString("en-GB"); - - return `${date} ${time} -0000 ${dateObj.getFullYear()}`; + return sugar.Date.format(dateObj, "%a %b %d %H:%M:%S {ZZ} %Y"); }, }, { - label: "January 22 2006 23:04:05", - value: "MMMM DD YYYY HH:mm:ss", + value: "January 22 2006 23:04:05", outputFn(dateObj) { - const date = dateObj - .toLocaleString("en-US", { - day: "2-digit", - month: "long", - year: "numeric", - }) - .replace(/,/g, ""); - - const time = dateObj.toLocaleTimeString("en-GB"); - - return `${date} ${time}`; + return sugar.Date.format(dateObj, "%B %d %Y %H:%M:%S"); }, }, { - label: "January 22 2006", - value: "MMMM DD YYYY", + value: "January 22 2006", outputFn(dateObj) { - return dateObj - .toLocaleString("en-US", { - day: "2-digit", - month: "long", - year: "numeric", - }) - .replace(/,/g, ""); + return sugar.Date.format(dateObj, "%B %d %Y"); }, }, { - label: "Jan 22 2006", - value: "MMM DD YYYY", + value: "Jan 22 2006", outputFn(dateObj) { - return dateObj - .toLocaleString("en-US", { - day: "2-digit", - month: "short", - year: "numeric", - }) - .replace(/,/g, ""); + return sugar.Date.format(dateObj, "%b %d %Y"); }, }, { - label: "2006-01-22T23:04:05", - value: "YYYY-MM-DDTHH:mm:ss", + value: "2006-01-22T23:04:05", outputFn(dateObj) { - return dateObj - .toISOString() - .replace(/\.[0-9]{3}/g, "") - .replace(/Z/, "-0000"); + return [ + sugar.Date.format(dateObj, "%Y-%m-%d"), + "T", + sugar.Date.format(dateObj, "%H:%M:%S"), + ].join(""); }, }, { - label: "2006-01-22T23:04:05-0000", - value: "YYYY-MM-DDTHH:mm:ss", + value: "2006-01-22T23:04:05+0000", outputFn(dateObj) { - return dateObj - .toISOString() - .replace(/\.[0-9]{3}/g, "") - .replace(/Z/, "-0000"); + return [ + sugar.Date.format(dateObj, "%Y-%m-%d"), + "T", + sugar.Date.format(dateObj, "%H:%M:%S{ZZ}"), + ].join(""); }, }, { - label: "2006-01-22 23:04:05 -0000", - value: "YYYY-MM-DD HH:mm:ss Z", + value: "2006-01-22 23:04:05 +0000", outputFn(dateObj) { - return dateObj - .toISOString() - .replace(/T|(\.[0-9]{3})/g, " ") - .replace(/Z/, "-0000"); + return sugar.Date.format(dateObj, "%Y-%m-%d %H:%M:%S {ZZ}"); }, }, { - label: "2006-01-22 23:04", - value: "YYYY-MM-DD HH:mm", + value: "2006-01-22 23:04", outputFn(dateObj) { - return dateObj - .toISOString() - .replace(/T/g, " ") - .replace(/:[0-9]{2}\.[0-9]{3}Z/, ""); + return sugar.Date.format(dateObj, "%Y-%m-%d %H:%M"); }, }, { - label: "2006-01-22", - value: "YYYY-MM-DD", + value: "2006-01-22", outputFn(dateObj) { - return dateObj - .toLocaleDateString("en-GB", { - day: "2-digit", - month: "2-digit", - year: "numeric", - }) - .split("/") - .reverse() - .join("-"); + return sugar.Date.format(dateObj, "%Y-%m-%d"); }, }, { - label: "01-22-2006", - value: "MM-DD-YYYY", + value: "01-22-2006", outputFn(dateObj) { - return dateObj - .toLocaleDateString("en-US", { - day: "2-digit", - month: "2-digit", - year: "numeric", - }) - .replace(/\//g, "-"); + return sugar.Date.format(dateObj, "%m-%d-%Y"); }, }, { - label: "01/22/2006", - value: "MM/DD/YYYY", + value: "01/22/2006", outputFn(dateObj) { - return dateObj.toLocaleDateString("en-US", { - day: "2-digit", - month: "2-digit", - year: "numeric", - }); + return sugar.Date.format(dateObj, "%m/%d/%Y"); }, }, { - label: "01/22/06", - value: "MM/DD/YY", + value: "01/22/06", outputFn(dateObj) { - return dateObj.toLocaleDateString("en-US", { - day: "2-digit", - month: "2-digit", - year: "2-digit", - }); + return sugar.Date.format(dateObj, "%m/%d/{yy}"); }, }, { - label: "22-01-2006", - value: "DD-MM-YYYY", + value: "22-01-2006", inputFn(str) { const [ day, month, year, ] = str.split("-"); - const date = new Date( - Date.UTC(Number(year), Number(month) - 1, Number(day)), - ); - return date; + return sugar.Date.create(`${year}-${month}-${day}`, { + fromUTC: true, + }); }, outputFn(dateObj) { - return dateObj.toLocaleDateString("en-GB").replace(/\//g, "-"); + return sugar.Date.format(dateObj, "%d-%m-%Y"); }, }, { - label: "22/01/2006", - value: "DD/MM/YYYY", + value: "22/01/2006", inputFn(str) { const [ day, month, year, ] = str.split("/"); - const date = new Date( - Date.UTC(Number(year), Number(month) - 1, Number(day)), - ); - return date; + return sugar.Date.create(`${year}-${month}-${day}`, { + fromUTC: true, + }); }, outputFn(dateObj) { - return dateObj.toLocaleDateString("en-GB"); + return sugar.Date.format(dateObj, "%d/%m/%Y"); }, }, { - label: "22/01/06", - value: "DD/MM/YY", + value: "22/01/06", inputFn(str) { const [ day, month, year, ] = str.split("/"); - const date = new Date( - Date.UTC(Number(year), Number(month) - 1, Number(day) + 2000), - ); - return date; + return sugar.Date.create(`${Number(year) + 2000}-${month}-${day}`, { + fromUTC: true, + }); }, outputFn(dateObj) { - return dateObj.toLocaleDateString("en-GB", { - day: "2-digit", - month: "2-digit", - year: "2-digit", - }); + return sugar.Date.format(dateObj, "%d/%m/{yy}"); }, }, { - label: "1137971045", - value: "Unix time (seconds)", + value: "Unix time (seconds) Eg. 1137971045", inputFn(str) { - return new Date(Number(str) * 1000); + return sugar.Date.create(Number(str) * 1000); }, outputFn(dateObj) { return dateObj.valueOf() / 1000; }, }, { - label: "1137971045000", - value: "Unix time (milliseconds)", + value: "Unix time (milliseconds) Eg. 1137971045000", inputFn(str) { - return new Date(Number(str)); + return sugar.Date.create(Number(str)); }, outputFn(dateObj) { return dateObj.valueOf(); @@ -263,9 +182,4 @@ const mapData: [ export const DATE_FORMAT_PARSE_MAP = new Map(mapData); -export const DATE_FORMAT_OPTIONS = DATE_FORMATS.map(({ - label, value, -}) => ({ - label, - value, -})); +export const DATE_FORMAT_OPTIONS = DATE_FORMATS.map(({ value }) => value); diff --git a/components/formatting/package.json b/components/formatting/package.json index d12f50ec9b927..434033f0350e5 100644 --- a/components/formatting/package.json +++ b/components/formatting/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/formatting", - "version": "0.1.3", + "version": "0.2.0", "description": "Pipedream Formatting Components", "main": "dist/app/formatting.app.mjs", "keywords": [ @@ -20,9 +20,9 @@ "@pipedream/types": "^0.1.4", "html-to-text": "^8.2.1", "linkedom": "^0.14.26", - "moment": "^2.29.4", "pluralize": "^8.0.0", "showdown": "^2.1.0", + "sugar": "^2.0.6", "title-case": "^3.0.3" } } diff --git a/components/formbricks/README.md b/components/formbricks/README.md new file mode 100644 index 0000000000000..6b73abf89c9de --- /dev/null +++ b/components/formbricks/README.md @@ -0,0 +1,11 @@ +# Overview + +Formbricks is a flexible tool for building forms that can integrate seamlessly into any website. The API allows for rich interactions with the forms you create, enabling you to automate the gathering and processing of data. By plugging the Formbricks API into Pipedream, you can harness serverless workflows to react to form submissions in real-time, store responses, or trigger a multitude of actions across different platforms. Think of it as empowering your forms to communicate and act on the data without manual intervention. + +# Example Use Cases + +- **Automated Data Entry into a Database**: With the Formbricks API on Pipedream, capture form submissions and automatically insert the data into your SQL database. Each new entry can trigger a Pipedream workflow that validates the content and then passes it to your database, keeping your records up-to-date without manual input. + +- **Dynamic Email Responses Based on Form Input**: Create a workflow where the Formbricks API triggers an email sent through SendGrid or another email service provider in Pipedream. Depending on the form's input, tailor the email content to provide personalized responses or follow-up actions, enhancing the user experience. + +- **Slack Notifications for Team Collaboration**: Set up a Pipedream workflow that posts a message to a designated Slack channel every time a form submission occurs. This instant notification allows teams to quickly respond to new leads, support requests, or feedback, ensuring nothing falls through the cracks. diff --git a/components/formcan/README.md b/components/formcan/README.md new file mode 100644 index 0000000000000..d5f852603c279 --- /dev/null +++ b/components/formcan/README.md @@ -0,0 +1,11 @@ +# Overview + +The FormCan API enables automation and integration of FormCan form submissions into various workflows on Pipedream. With this API, you can trigger actions when new submissions come in, read and manage your forms, and interact with submitted data in real-time. This opens a plethora of possibilities to connect your FormCan data with other apps and services, streamline data processing, and enhance the overall responsiveness of your form-related operations. + +# Example Use Cases + +- **Automate Spreadsheet Updates**: When a new submission is received via FormCan, use the Pipedream workflow to add or update the data in a Google Sheets spreadsheet. This is perfect for keeping records without manual data entry. + +- **Dynamic Email Responses**: Set up a workflow where for each new FormCan submission, Pipedream sends a customized email response using SendGrid or another email service. This can be used for lead generation follow-ups, survey responses, or confirmation emails. + +- **CRM Integration**: With each FormCan submission, create or update a contact in a CRM like Salesforce or HubSpot. This ensures your customer relationship management system stays synced with the latest form input, keeping your sales or customer service team informed. diff --git a/components/formcan/package.json b/components/formcan/package.json index 159af6155af67..b87f14f083d52 100644 --- a/components/formcan/package.json +++ b/components/formcan/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/formcarry/README.md b/components/formcarry/README.md index 83ef2e29946aa..e6c32fd0ef24a 100644 --- a/components/formcarry/README.md +++ b/components/formcarry/README.md @@ -1,13 +1,11 @@ # Overview -With Formcarry, you can easily and quickly create powerful web forms without -having to write any code. Just enter your form information into the Formcarry -interface and our powerful API will take care of the rest. +Formcarry is an API for form processing, empowering developers to handle form submissions without the need for server-side code. With Formcarry, you can effortlessly collect, process, and integrate form data with various services. Using Pipedream, you can create serverless workflows that react to form submissions by triggering actions within Formcarry or in other apps, streamlining data collection and automation processes. -Here are some examples of what you can build with Formcarry: +# Example Use Cases -- A contact form for your website -- A feedback form for your business -- A survey for your customers -- A signup form for your newsletter -- A registration form for your event +- **Auto-Respond to Form Submissions**: Craft a Pipedream workflow that listens for new Formcarry submissions. Use this to trigger an automated email response, confirming receipt to the user with SendGrid. You can personalize the email response based on the form's content for a tailored experience. + +- **Form Submission Data to Google Sheets**: On receiving a new form submission via Formcarry, use Pipedream to add the data to a Google Sheet. This is ideal for lead tracking, survey responses, or managing registration details. It's a great way to keep all submissions organized and easily accessible for further analysis or team sharing. + +- **Slack Notification for Support Requests**: Set up a Pipedream workflow to send a Slack message to a support channel when a new support request is submitted through a Formcarry form. Include details from the submission in the message so your team can quickly respond to inquiries or issues. diff --git a/components/formcarry/formcarry.app.mjs b/components/formcarry/formcarry.app.mjs index 28b0481b10085..ef2b929af8e18 100644 --- a/components/formcarry/formcarry.app.mjs +++ b/components/formcarry/formcarry.app.mjs @@ -1,11 +1,63 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "formcarry", - propDefinitions: {}, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://formcarry.com/api"; + }, + _headers() { + return { + api_key: `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._headers(), + ...opts, + }); + }, + listSubmissions({ + formId, ...opts + }) { + return this._makeRequest({ + path: `/form/${formId}/submissions`, + ...opts, + }); + }, + async *paginate({ + fn, + args = {}, + resourceKey, + max, + }) { + args = { + ...args, + params: { + ...args?.params, + limit: 50, + page: 1, + }, + }; + let nextPage, count = 0; + do { + const response = await fn(args); + const items = response[resourceKey]; + for (const item of items) { + yield item; + if (max && ++count >= max) { + return; + } + } + nextPage = response.pagination.next_page; + args.params.page = nextPage; + } while (nextPage); }, }, }; diff --git a/components/formcarry/package.json b/components/formcarry/package.json new file mode 100644 index 0000000000000..ae0f801777cf4 --- /dev/null +++ b/components/formcarry/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/formcarry", + "version": "0.1.0", + "description": "Pipedream Formcarry Components", + "main": "formcarry.app.mjs", + "keywords": [ + "pipedream", + "formcarry" + ], + "homepage": "https://pipedream.com/apps/formcarry", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/formcarry/sources/new-form-submission/new-form-submission.mjs b/components/formcarry/sources/new-form-submission/new-form-submission.mjs new file mode 100644 index 0000000000000..99e75453e5caa --- /dev/null +++ b/components/formcarry/sources/new-form-submission/new-form-submission.mjs @@ -0,0 +1,82 @@ +import formcarry from "../../formcarry.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + key: "formcarry-new-form-submission", + name: "New Form Submission", + description: "Emit new event when the specified form receives a new submission. [See the documentation](https://formcarry.com/docs/formcarry-api/submissions-api#cc7f3010897b4c938c8829db46b18656)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + formcarry, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + formId: { + type: "string", + label: "Form ID", + description: "The ID of the form to watch for new submissions", + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + generateMeta(submission) { + return { + id: submission._id, + summary: `New Form Submission ID: ${submission._id}`, + ts: Date.parse(submission.createdAt), + }; + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + + const results = this.formcarry.paginate({ + fn: this.formcarry.listSubmissions, + args: { + formId: this.formId, + }, + resourceKey: "submissions", + max, + }); + + const submissions = []; + for await (const item of results) { + const ts = Date.parse(item.createdAt); + if (ts >= lastTs) { + submissions.push(item); + } else { + break; + } + } + + if (!submissions.length) { + return; + } + + this._setLastTs(Date.parse(submissions[0].createdAt)); + + submissions.forEach((submission) => { + const meta = this.generateMeta(submission); + this.$emit(submission, meta); + }); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/formdesk/README.md b/components/formdesk/README.md new file mode 100644 index 0000000000000..3e7cd8d6aec9c --- /dev/null +++ b/components/formdesk/README.md @@ -0,0 +1,11 @@ +# Overview + +Formdesk is a versatile tool that lets you create online forms, surveys, and quizzes. With the Formdesk API on Pipedream, you can automate data collection, analysis, and integration. Whether you're syncing form submissions to a database, triggering emails based on responses, or connecting Formdesk to other apps for seamless data flow, Pipedream’s serverless platform empowers you to build powerful, customized workflows. + +# Example Use Cases + +- **Automated Survey Response Collector**: Trigger a Pipedream workflow with each new Formdesk submission. Collect responses, analyze them in real-time, and automatically store them in a Google Sheets document for easy sharing and collaboration. + +- **Conditional Email Notifications**: Set up a Pipedream workflow that evaluates submissions for specific criteria—like a low satisfaction score—and sends a tailored email via SendGrid to address issues proactively, enhancing customer follow-ups. + +- **Integrated CRM Updates**: When a new form submission comes through Formdesk, use a Pipedream workflow to parse the data and update contacts or leads in Salesforce, ensuring your sales team always has the latest information at their fingertips. diff --git a/components/formidable_forms/formidable_forms.app.mjs b/components/formidable_forms/formidable_forms.app.mjs new file mode 100644 index 0000000000000..7d9c755b2a8cb --- /dev/null +++ b/components/formidable_forms/formidable_forms.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "formidable_forms", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/formidable_forms/package.json b/components/formidable_forms/package.json new file mode 100644 index 0000000000000..b5f089c16605f --- /dev/null +++ b/components/formidable_forms/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/formidable_forms", + "version": "0.0.1", + "description": "Pipedream Formidable Forms Components", + "main": "formidable_forms.app.mjs", + "keywords": [ + "pipedream", + "formidable_forms" + ], + "homepage": "https://pipedream.com/apps/formidable_forms", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/formpress/README.md b/components/formpress/README.md new file mode 100644 index 0000000000000..9d050cc6b28c7 --- /dev/null +++ b/components/formpress/README.md @@ -0,0 +1,11 @@ +# Overview + +The FormPress API allows you to create and manage forms dynamically, without needing a backend to process submissions. You can integrate FormPress with Pipedream to automate processes such as capturing form submissions, organizing data, triggering notifications, and connecting responses to various apps or services. Pipedream's serverless platform makes it simple to handle FormPress events and incorporate them into workflows, saving time and eliminating the need to manage infrastructure. + +# Example Use Cases + +- **Automated Data Collection and Storage**: Capture form submissions from FormPress and automatically store the data in a Google Sheet. Use this workflow to organize survey results, registration details, or feedback. + +- **Dynamic Response and Notification**: Trigger an email or SMS notification using Twilio or SendGrid when a new form submission is received. This can be useful for order confirmations, support ticket creation, or immediate follow-ups on inquiries. + +- **Integration with CRM Systems**: On form submission, instantly create or update contacts in your CRM such as Salesforce or HubSpot. This workflow ensures your sales or support teams have real-time information for leads or support requests. diff --git a/components/forms_on_fire/README.md b/components/forms_on_fire/README.md new file mode 100644 index 0000000000000..c41163874e1a4 --- /dev/null +++ b/components/forms_on_fire/README.md @@ -0,0 +1,11 @@ +# Overview + +The Forms On Fire API allows you to automate interactions with your mobile forms. Using it with Pipedream, you can trigger workflows based on form submissions, update or retrieve data from forms, and automate interactions with other apps. It's useful for extending the capabilities of your forms, integrating with third-party services, and streamlining data processes without manual intervention. + +# Example Use Cases + +- **Automate Data Entry to a Database**: When a form is submitted via Forms On Fire, use Pipedream to capture the data and insert it into a database like PostgreSQL or MySQL. This workflow can reduce manual data entry errors and save time. + +- **Sync Form Submissions with CRM**: Each time a form is completed, the workflow could trigger to create or update a contact in a CRM such as Salesforce or HubSpot. This ensures your customer information is always current and can trigger other CRM workflows. + +- **Send Custom Email Notifications**: Configure a workflow to send a custom email notification via SendGrid or Gmail when a form is submitted. This can inform team members about new entries or provide customers with a confirmation receipt. diff --git a/components/formsite/README.md b/components/formsite/README.md new file mode 100644 index 0000000000000..d48a09585ce5c --- /dev/null +++ b/components/formsite/README.md @@ -0,0 +1,11 @@ +# Overview + +The Formsite API enables you to automate interactions with your Formsite forms. You can retrieve form data, create new forms, or update existing ones, and even manage form submissions. With Pipedream's capabilities, you can integrate Formsite with countless other apps, create complex workflows, and trigger actions based on form submissions or other criteria. It's a powerful way to streamline data collection processes, feed collected data into other systems, or trigger events in other services in real-time. + +# Example Use Cases + +- **Automate Data Collection to Google Sheets**: When a form submission occurs, Pipedream can trigger a workflow that automatically adds the submitted data to a Google Sheets spreadsheet. This is great for keeping all responses organized and accessible for further analysis. + +- **Dynamic Email Notifications**: Use Pipedream to send custom email notifications via SendGrid or another email service when a form is submitted. You can tailor these emails based on the submission content, ensuring that the right people get the right information at the right time. + +- **CRM Integration**: Upon a new form submission, Pipedream can create or update contact records in your Customer Relationship Management (CRM) system like Salesforce. This ensures your sales or support teams have the latest information without manual data entry. diff --git a/components/formspree/README.md b/components/formspree/README.md index fe66624bbe507..68080c7fec522 100644 --- a/components/formspree/README.md +++ b/components/formspree/README.md @@ -1,11 +1,11 @@ # Overview -Formspree is a great way to build simple forms and gather data from users. Here -are some examples of what you can build using the Formspree API: +The Formspree API enables developers to collect form submissions directly into their existing workflows without having to manage servers or write server-side code. By leveraging Pipedream's capabilities, you can automate responses to form submissions, integrate with other services, and streamline data processing. This can result in improved productivity, instant notifications, and seamless data synchronization across various platforms. -- A contact form for your website -- A feedback form for your customers -- A survey for your users +# Example Use Cases -Formspree makes it easy to gather data from your users, and the possibilities -are endless! +- **Auto-Respond to Form Submissions**: Use Formspree to collect feedback or inquiries from your website visitors. Then, with Pipedream's workflows, automatically send a customized thank-you email or confirmation message to the submitter using the SendGrid API, ensuring they know their submission was received. + +- **Aggregate Form Data for Analytics**: Collect survey or registration data with Formspree, and use Pipedream to send each submission to Google Sheets. From there, perform real-time analytics or create dashboards in Google Data Studio to visualize trends and gather insights. + +- **Trigger Actions Based on Form Input**: Set up a Pipedream workflow that triggers a Zapier action whenever a Formspree submission meets certain criteria. For example, if a potential client requests a quote via a form, Pipedream could instruct Zapier to create a new lead in a CRM like Salesforce, initiating a tailored follow-up sequence. diff --git a/components/formspree/package.json b/components/formspree/package.json new file mode 100644 index 0000000000000..82be0f6000fc5 --- /dev/null +++ b/components/formspree/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/formspree", + "version": "0.6.0", + "description": "Pipedream formspree Components", + "main": "formspree.app.mjs", + "keywords": [ + "pipedream", + "formspree" + ], + "homepage": "https://pipedream.com/apps/formspree", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/formstack/README.md b/components/formstack/README.md index ab8ef7afa2aa2..d28a8f4278730 100644 --- a/components/formstack/README.md +++ b/components/formstack/README.md @@ -1,6 +1,11 @@ # Overview -Formstack is a powerful API that allows you to build sophisticated forms and -surveys. With Formstack, you can collect data from your users, process -payments, and even send notifications. Formstack is perfect for businesses of -all sizes. +The Formstack API facilitates the automation of form creation and submission processes, enhancing data collection and integration. With Pipedream's serverless platform, you can trigger workflows on form submissions, manipulate and route form data to various endpoints, and seamlessly connect Formstack with other apps to streamline tasks. + +# Example Use Cases + +- **Dynamic CRM Lead Entry**: Automatically create leads in Salesforce whenever a form is submitted on Formstack. Use data from the form to populate the lead details, ensuring that potential customer information is immediately captured and ready for follow-up by the sales team. + +- **Custom Email Notifications**: Craft and send personalized email notifications using SendGrid when a Formstack submission occurs. Tailor the content based on the submission data to provide respondents with relevant information, confirmation messages, or next-step instructions. + +- **Support Ticket Creation**: Integrate Formstack submissions into a Zendesk support system. When a customer submits a feedback or support form, a new ticket is generated in Zendesk, with all the pertinent information from the form directly mapped to the ticket fields, streamlining your support workflow. diff --git a/components/formstack_documents/README.md b/components/formstack_documents/README.md new file mode 100644 index 0000000000000..84eb38d974899 --- /dev/null +++ b/components/formstack_documents/README.md @@ -0,0 +1,11 @@ +# Overview + +The Formstack Documents API gives you the power to automate document creation and delivery, making it simple to generate custom PDFs, Word documents, PowerPoint presentations, and more, from your data. By leveraging the API within Pipedream, you can integrate dynamic document generation into your workflows. You might merge data from various sources, populate templates with real-time data, and send documents directly to customers or team members. + +# Example Use Cases + +- **Automate Contract Generation from CRM Updates**: When a CRM like Salesforce updates a deal to 'Closed Won,' trigger a workflow in Pipedream to populate a contract template in Formstack Documents with the deal details. Then, email the contract to the customer for signature. + +- **Dynamic Invoicing for E-commerce Orders**: Upon an order completion in an e-commerce platform such as Shopify, use Pipedream to capture the order details and create a tailored invoice using Formstack Documents. The invoice can then be emailed to the customer or saved to cloud storage like Google Drive. + +- **Custom Report Generation from Database Entries**: Trigger a Pipedream workflow with new or updated records from a database app like Airtable. Use the Formstack Documents API to fill in a report template with the latest data, and then send the report to a Slack channel for team updates. diff --git a/components/formtitan/README.md b/components/formtitan/README.md index c7a48c3d5b699..2e8d7596873ea 100644 --- a/components/formtitan/README.md +++ b/components/formtitan/README.md @@ -1,13 +1,11 @@ # Overview -With FormTitan, you can build a variety of forms and surveys, including: +The FormTitan API enables interaction with FormTitan's form-building and lead-capture capabilities programmatically. With Pipedream, you can harness this API to automate form data processing, synchronize data across various platforms, and trigger actions based on form submissions. This can streamline complex workflow scenarios, such as lead nurturing, survey data analysis, and customer feedback management, without the need to manually intervene. -- Contact forms -- Order forms -- Registration forms -- Job application forms -- Marketing surveys -- Customer satisfaction surveys -- Event registration forms +# Example Use Cases -And more! +- **Automated Lead Capture to CRM**: Upon receiving a new form submission in FormTitan, trigger a Pipedream workflow to parse the submission and create a new lead in your CRM, like Salesforce or HubSpot. This ensures that your sales team always has the latest leads to work with, without any manual entry. + +- **Dynamic Survey Analysis**: When a new survey is completed via FormTitan, use Pipedream to send the data to Google Sheets for storage. Then, trigger additional workflows that utilize Google Data Studio for real-time survey analysis, helping you to quickly glean insights and make data-driven decisions. + +- **Customer Feedback Loop**: Create a Pipedream workflow that listens for new feedback submissions via FormTitan. Once a submission is detected, automatically create a ticket in a customer service platform such as Zendesk, and send a personalized thank you email from SendGrid to the customer, acknowledging their input and closing the feedback loop efficiently. diff --git a/components/formtitan/package.json b/components/formtitan/package.json new file mode 100644 index 0000000000000..554c4f0d3d69c --- /dev/null +++ b/components/formtitan/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/formtitan", + "version": "0.6.0", + "description": "Pipedream formtitan Components", + "main": "formtitan.app.mjs", + "keywords": [ + "pipedream", + "formtitan" + ], + "homepage": "https://pipedream.com/apps/formtitan", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/foursquare/README.md b/components/foursquare/README.md index a72d11649d67a..8f9863796ef7d 100644 --- a/components/foursquare/README.md +++ b/components/foursquare/README.md @@ -1,15 +1,11 @@ # Overview -The Foursquare API gives developers access to the Foursquare database of venues -and users. With the API, developers can build applications that allow -Foursquare users to check in to venues, find nearby venues, get -recommendations, and more. - -Here are some examples of what you can build with the Foursquare API: - -- An app that allows users to check in to venues and share their location with - friends -- A venue discovery app that recommends nearby places to eat, drink, and shop -- A social network for foodies that lets users share photos and reviews of - dishes they've tried -- A travel app that helps users find things to do and see in different cities +The Foursquare API empowers developers to tap into rich data about venues, user check-ins, and location-based experiences. On Pipedream, you can harness this API to automate tasks such as fetching venue recommendations based on user preferences, keeping a log of customer visits to your business, or integrating location insights into your CRM. Combining Foursquare data with other services on Pipedream allows you to create powerful, location-aware business solutions without the heavy lifting of manual data handling. + +# Example Use Cases + +- **Local Event Recommendations**: Automatically send personalized local event recommendations to app users. When a user's location is detected within a new city, trigger a workflow that uses the Foursquare API to find trending venues or events nearby, then send these suggestions via Twilio SMS or email through SendGrid. + +- **Venue Visit Analytics**: Keep a pulse on business traffic by tracking check-ins at your venue. Set up a workflow that triggers every time someone checks into your venue on Foursquare, adding the check-in data to a Google Sheets spreadsheet for real-time analytics, or pushing the data into a data visualization tool like Tableau for deeper insights. + +- **Customer Engagement Booster**: Increase customer loyalty by offering discounts or rewards when they visit partner venues. Create a workflow that listens for check-ins at specific locations, then triggers a discount code generation via your e-commerce platform's API, and emails the offer to the customer using Mailchimp, thus encouraging repeat visits and cross-promotions. diff --git a/components/foursquare/actions/create-check-in/create-check-in.mjs b/components/foursquare/actions/create-check-in/create-check-in.mjs new file mode 100644 index 0000000000000..c555bbd19eaa5 --- /dev/null +++ b/components/foursquare/actions/create-check-in/create-check-in.mjs @@ -0,0 +1,36 @@ +import foursquare from "../../foursquare.app.mjs"; + +export default { + key: "foursquare-create-check-in", + name: "Create Check In", + description: "Allows the user to generate a new check-in at a specific location on Foursquare. [See the documentation](https://docs.foursquare.com/developer/reference/create-a-checkin)", + version: "0.0.1", + type: "action", + props: { + foursquare, + venueId: { + propDefinition: [ + foursquare, + "venueId", + ], + }, + shout: { + type: "string", + label: "Shout", + description: "A message about your check-in.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.foursquare.createCheckIn({ + $, + params: { + venueId: this.venueId, + shout: this.shout, + }, + }); + + $.export("$summary", `Successfully created check-in at venue ${this.venueId}`); + return response; + }, +}; diff --git a/components/foursquare/actions/create-tip/create-tip.mjs b/components/foursquare/actions/create-tip/create-tip.mjs new file mode 100644 index 0000000000000..02ab782941442 --- /dev/null +++ b/components/foursquare/actions/create-tip/create-tip.mjs @@ -0,0 +1,34 @@ +import foursquare from "../../foursquare.app.mjs"; + +export default { + key: "foursquare-create-tip", + name: "Create Tip", + description: "Allows the user to create a new tip for a venue on Foursquare. [See the documentation](https://docs.foursquare.com/developer/reference/add-a-tip)", + version: "0.0.1", + type: "action", + props: { + foursquare, + venueId: { + propDefinition: [ + foursquare, + "venueId", + ], + }, + tipText: { + type: "string", + label: "Tip Text", + description: "The text of the tip.", + }, + }, + async run({ $ }) { + const response = await this.foursquare.addTip({ + $, + params: { + venueId: this.venueId, + text: this.tipText, + }, + }); + $.export("$summary", `Successfully created tip for venue ${this.venueId}`); + return response; + }, +}; diff --git a/components/foursquare/common/constants.mjs b/components/foursquare/common/constants.mjs new file mode 100644 index 0000000000000..1ee23d0f982e4 --- /dev/null +++ b/components/foursquare/common/constants.mjs @@ -0,0 +1 @@ +export const LIMIT = 1; diff --git a/components/foursquare/foursquare.app.mjs b/components/foursquare/foursquare.app.mjs index 0b3e161e4ef35..955e231502802 100644 --- a/components/foursquare/foursquare.app.mjs +++ b/components/foursquare/foursquare.app.mjs @@ -1,11 +1,94 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + export default { type: "app", app: "foursquare", - propDefinitions: {}, + propDefinitions: { + venueId: { + type: "string", + label: "Venue ID", + description: "The ID of the venue where you want to interact.", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.foursquare.com/v2"; + }, + _headers() { + return { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, params = {}, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + params: { + ...params, + v: "20240430", + }, + ...opts, + }); + }, + createCheckIn(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/checkins/add", + ...opts, + }); + }, + addTip(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/tips/add", + ...opts, + }); + }, + getUserTips(opts = {}) { + return this._makeRequest({ + path: "/users/self/tips", + ...opts, + }); + }, + getUserCheckins(opts = {}) { + return this._makeRequest({ + path: "/users/self/checkins", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, dataField, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.limit = LIMIT; + params.offset = LIMIT * page; + page++; + + const { response } = await fn({ + params, + ...opts, + }); + + const items = response[dataField].items; + + for (const d of items) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = items.length; + + } while (hasMore); }, }, }; diff --git a/components/foursquare/package.json b/components/foursquare/package.json new file mode 100644 index 0000000000000..b204aab70cd84 --- /dev/null +++ b/components/foursquare/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/foursquare", + "version": "0.1.0", + "description": "Pipedream Foursquare Components", + "main": "foursquare.app.mjs", + "keywords": [ + "pipedream", + "foursquare" + ], + "homepage": "https://pipedream.com/apps/foursquare", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/foursquare/sources/common/base.mjs b/components/foursquare/sources/common/base.mjs new file mode 100644 index 0000000000000..97a30e5034f97 --- /dev/null +++ b/components/foursquare/sources/common/base.mjs @@ -0,0 +1,58 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import foursquare from "../../foursquare.app.mjs"; + +export default { + props: { + foursquare, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(created) { + this.db.set("lastDate", created); + }, + generateMeta(item) { + return { + id: item.id, + summary: this.getSummary(item), + ts: item.createdAt, + }; + }, + async startEvent(maxResults = 0) { + const lastDate = this._getLastDate(); + const response = this.foursquare.paginate({ + fn: this.getFn(), + dataField: this.getDataField(), + maxResults, + }); + + const responseArray = []; + for await (const item of response) { + if (item.createdAt <= lastDate) break; + responseArray.push(item); + } + + if (responseArray.length) this._setLastDate(responseArray[0].createdAt); + + for (const item of responseArray.reverse()) { + this.$emit(item, this.generateMeta(item)); + } + }, + }, + hooks: { + async deploy() { + await this.startEvent(25); + }, + }, + async run() { + await this.startEvent(); + }, +}; diff --git a/components/foursquare/sources/new-check-in/new-check-in.mjs b/components/foursquare/sources/new-check-in/new-check-in.mjs new file mode 100644 index 0000000000000..202238c40ffff --- /dev/null +++ b/components/foursquare/sources/new-check-in/new-check-in.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "foursquare-new-check-in", + name: "New Check-In", + description: "Emit new event when a user checks in.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSummary(item) { + return `New Check-In with Id: ${item.id}`; + }, + getDataField() { + return "checkins"; + }, + getFn() { + return this.foursquare.getUserCheckins; + }, + }, + sampleEmit, +}; diff --git a/components/foursquare/sources/new-check-in/test-event.mjs b/components/foursquare/sources/new-check-in/test-event.mjs new file mode 100644 index 0000000000000..6b470720822fd --- /dev/null +++ b/components/foursquare/sources/new-check-in/test-event.mjs @@ -0,0 +1,69 @@ +export default { + "id": "59b97e6cf79faa0d5e23abeb", + "createdAt": 1505328748, + "type": "checkin", + "private": true, + "visibility": "private", + "entities": [], + "shout": "Monday monday!", + "timeZoneOffset": -240, + "venue": { + "id": "4f0de7267bebfc146005dcc0", + "name": "Mattlantis 🐳" + }, + "likes": { + "count": 0, + "groups": [] + }, + "like": false, + "isMayor": false, + "photos": { + "count": 1, + "items": [ + { + "id": "59b97e6d270ee77dbf2b8aa0", + "createdAt": 1505328749, + "source": { + "name": "Swarm for iOS", + "url": "https://www.swarmapp.com" + }, + "prefix": "https://fastly.4sqi.net/img/general/", + "suffix": "/7294631_KK8FbnS6sqgrlXgZeCwkYbSn2efiAQBoT7x4p_hteis.jpg", + "width": 1440, + "height": 1920, + "user": { + "id": "123456", + "firstName": "John", + "lastName": "Smith", + "handle": "johnsmith", + "privateProfile": false, + "gender": "male", + "countryCode": "US", + "relationship": "self", + "photo": { + "prefix": "https://fastly.4sqi.net/img/user/", + "suffix": "/blank_boy.png", + "default": true + }, + "birthday": 19700101, + "isAnonymous": false + }, + "visibility": "private" + } + ], + "layout": { + "name": "single" + } + }, + "posts": { + "count": 0, + "textCount": 0 + }, + "comments": { + "count": 0 + }, + "source": { + "name": "Swarm for iOS", + "url": "https://www.swarmapp.com" + } +} \ No newline at end of file diff --git a/components/foursquare/sources/new-tip/new-tip.mjs b/components/foursquare/sources/new-tip/new-tip.mjs new file mode 100644 index 0000000000000..60cc5d938cb21 --- /dev/null +++ b/components/foursquare/sources/new-tip/new-tip.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "foursquare-new-tip", + name: "New Tip", + description: "Emit new event when a user adds a new tip.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSummary(item) { + return `New tip with Id: ${item.id}`; + }, + getDataField() { + return "tips"; + }, + getFn() { + return this.foursquare.getUserTips; + }, + }, + sampleEmit, +}; diff --git a/components/foursquare/sources/new-tip/test-event.mjs b/components/foursquare/sources/new-tip/test-event.mjs new file mode 100644 index 0000000000000..aae6f6da44241 --- /dev/null +++ b/components/foursquare/sources/new-tip/test-event.mjs @@ -0,0 +1,36 @@ +export default { + "id": "6515f99d8f151130670423d4", + "createdAt": 1695938973, + "text": "here is puppy number 2", + "type": "user", + "canonicalUrl": "https://foursquare.com/item/6515f99d8f151130670423d4", + "photo": { + "id": "6515f99e2425d010750f76fd", + "createdAt": 1695938974, + "source": { + "name": "Foursquare Web", + "url": "https://foursquare.com" + }, + "prefix": "https://fastly.4sqi.net/img/general/", + "suffix": "/1234567_VJqOKo4P9-W0VyQk0iOZZzqG178RAL1rswe4KpZ2YGg.png", + "width": 480, + "height": 650, + "visibility": "public" + }, + "photourl": "https://fastly.4sqi.net/img/general/original/1234567_VJqOKo4P9-W0VyQk0iOZZzqG178RAL1rswe4KpZ2YGg.png", + "likes": { + "count": 0, + "groups": [] + }, + "like": false, + "viewCount": 0, + "agreeCount": 0, + "disagreeCount": 0, + "todo": { + "count": 0 + }, + "venue": { + "id": "4f0de7267bebfc146005dcc0", + "name": "Mattlantis 🐳" + } +} \ No newline at end of file diff --git a/components/foxy/README.md b/components/foxy/README.md index 821291bd60695..a1689abcb987a 100644 --- a/components/foxy/README.md +++ b/components/foxy/README.md @@ -1,12 +1,11 @@ # Overview -Foxy provides APIs to easily add e-commerce to any website or web application. -With Foxy, you can add products, manage orders, and process payments. You can -also use Foxy to create custom shopping experiences or integrate with -third-party applications. +The Foxy API allows for robust e-commerce automation, enabling the connection between your storefront and various back-end systems. With this API, you can automate tasks such as order processing, inventory management, and customer communication. Utilize this functionality to streamline your e-commerce operations, reduce manual effort, and enhance customer satisfaction by ensuring timely updates and actions. -Here are some examples of what you can build with the Foxy API: +# Example Use Cases -- A simple e-commerce store -- A custom shopping experience for your website or web application -- An integration with a third-party application +- **Order Processing Workflow**: When a new order is placed via Foxy, trigger a Pipedream workflow to validate the order data, then automatically create an invoice using QuickBooks or Xero. Follow up by sending a personalized confirmation email to the customer with details about their order. + +- **Inventory Management Workflow**: Set up a Pipedream workflow that triggers when stock levels change in Foxy. The workflow could adjust inventory counts in a connected warehouse management app like ShipStation and update the stock status on your storefront. This ensures accurate inventory levels across all platforms and prevents overselling. + +- **Customer Follow-Up Workflow**: Create a Pipedream workflow that initiates when an order is marked as shipped in Foxy. Use this trigger to send a shipping confirmation email to the customer with tracking information, and schedule a follow-up email asking for a review or offering a discount on their next purchase, fostering customer loyalty. diff --git a/components/fractel/README.md b/components/fractel/README.md new file mode 100644 index 0000000000000..14400923f7160 --- /dev/null +++ b/components/fractel/README.md @@ -0,0 +1,11 @@ +# Overview + +The FracTEL API enables integration of advanced telecommunications features into applications, allowing for the management of voice and messaging functionalities. This API can be particularly powerful when used on Pipedream, where it can be combined with hundreds of other apps to create robust, automated workflows. These can range from triggering calls based on specific events to automating SMS notifications for critical alerts. + +# Example Use Cases + +- **Customer Support Ticket Voice Alerts**: Automatically initiate a phone call via FracTEL to a support manager when a high-priority support ticket is created in a system like Zendesk. This ensures immediate attention to critical issues without delay. + +- **Event-Driven SMS Notifications**: Set up a workflow where FracTEL sends a text message to a list of attendees from a Google Sheets spreadsheet whenever a new event is scheduled in a Google Calendar. This keeps everyone informed and can increase event attendance. + +- **Voicemail Transcription and Analysis**: Capture voicemails received on FracTEL, transcribe them using an AI service like Google Cloud Speech-to-Text, and analyze the sentiment with Google Cloud Natural Language API. This can help in prioritizing responses based on the urgency and sentiment of the voicemail. diff --git a/components/fractel/actions/call-phone/call-phone.mjs b/components/fractel/actions/call-phone/call-phone.mjs new file mode 100644 index 0000000000000..6e963cd74ec30 --- /dev/null +++ b/components/fractel/actions/call-phone/call-phone.mjs @@ -0,0 +1,44 @@ +import fractel from "../../fractel.app.mjs"; + +export default { + key: "fractel-call-phone", + name: "Call Phone", + description: "Initiates a new phone call to the provided number.", + version: "0.0.1", + type: "action", + props: { + fractel, + phoneNumber: { + propDefinition: [ + fractel, + "phoneNumber", + ], + }, + to: { + propDefinition: [ + fractel, + "to", + ], + }, + message: { + propDefinition: [ + fractel, + "message", + ], + }, + }, + async run({ $ }) { + const response = await this.fractel.initiateCall({ + $, + data: { + fonenumber: this.phoneNumber, + to: this.to, + service_type: "TTS", + service_id: this.message, + }, + }); + + $.export("$summary", `Successfully initiated a call with Id: ${response.call.id}`); + return response; + }, +}; diff --git a/components/fractel/actions/send-sms-mms/send-sms-mms.mjs b/components/fractel/actions/send-sms-mms/send-sms-mms.mjs new file mode 100644 index 0000000000000..a3d88a8299f29 --- /dev/null +++ b/components/fractel/actions/send-sms-mms/send-sms-mms.mjs @@ -0,0 +1,60 @@ +import { parseObject } from "../../common/utils.mjs"; +import fractel from "../../fractel.app.mjs"; + +export default { + key: "fractel-send-sms-mms", + name: "Send SMS or MMS", + description: "Allows to send an SMS or MMS to a particular phone number. [See the documentation](https://developer.fonestorm.com/reference/sendmessage)", + version: "0.0.1", + type: "action", + props: { + fractel, + phoneNumber: { + propDefinition: [ + fractel, + "phoneNumber", + ], + description: "The phone number to send the message to, including country code.", + }, + to: { + propDefinition: [ + fractel, + "to", + ], + }, + message: { + propDefinition: [ + fractel, + "message", + ], + description: "The message content for SMS or MMS.", + optional: true, + }, + media: { + propDefinition: [ + fractel, + "media", + ], + optional: true, + }, + }, + async run({ $ }) { + if (!this.message && !this.media) { + throw new Error("Either message or media must be provided."); + } + const response = await this.fractel.sendMessage({ + $, + data: { + to: this.to, + fonenumber: this.phoneNumber, + message: this.message, + media: this.media && parseObject(this.media), + }, + }); + + $.export("$summary", `Successfully sent ${this.message + ? "SMS" + : "MMS"} with Id: ${response.message.id}`); + return response; + }, +}; diff --git a/components/fractel/common/utils.mjs b/components/fractel/common/utils.mjs new file mode 100644 index 0000000000000..4b5f3f9d301f8 --- /dev/null +++ b/components/fractel/common/utils.mjs @@ -0,0 +1,22 @@ +export const parseObject = (obj) => { + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/fractel/fractel.app.mjs b/components/fractel/fractel.app.mjs new file mode 100644 index 0000000000000..b111abfb23be6 --- /dev/null +++ b/components/fractel/fractel.app.mjs @@ -0,0 +1,72 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "fractel", + propDefinitions: { + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number to call to, including country code.", + async options() { + const { fonenumbers } = await this.listFoneNumbers(); + return fonenumbers; + }, + }, + to: { + type: "string", + label: "To", + description: "The recipient phone number, including country code.", + }, + message: { + type: "string", + label: "Message", + description: "The message content for TTS.", + }, + media: { + type: "string", + label: "Media URL", + description: "Media URL of the media file for MMS.", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.fonestorm.com/v2"; + }, + _headers() { + return { + token: `${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + initiateCall(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/calls", + ...opts, + }); + }, + listFoneNumbers(opts = {}) { + return this._makeRequest({ + path: "/fonenumbers", + ...opts, + }); + }, + sendMessage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/messages/send", + ...opts, + }); + }, + }, +}; diff --git a/components/fractel/package.json b/components/fractel/package.json new file mode 100644 index 0000000000000..509e5d1ceacba --- /dev/null +++ b/components/fractel/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/fractel", + "version": "0.1.0", + "description": "Pipedream FracTEL Components", + "main": "fractel.app.mjs", + "keywords": [ + "pipedream", + "fractel" + ], + "homepage": "https://pipedream.com/apps/fractel", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} diff --git a/components/frame/README.md b/components/frame/README.md index f8bfec8d4a151..7aa1f370bb935 100644 --- a/components/frame/README.md +++ b/components/frame/README.md @@ -1,10 +1,11 @@ # Overview -With the Frame.io API, you can build tools for managing your video content and -collaboration workflow. For example, you can: - -- Upload and manage your video files -- Add comments and annotations to videos -- Create and manage projects -- Share videos with collaborators -- Track changes and activity in your projects +The Frame.io API hooks into their robust video collaboration platform, enabling automated workflows around video reviews, project updates, and asset management. With the API, you can programmatically interact with comments, accounts, projects, and more—perfect for integrating with other tools to streamline video production pipelines. + +# Example Use Cases + +- **Automated Feedback Collection**: When a new comment is made in Frame.io, trigger a Pipedream workflow that aggregates feedback and sends a formatted digest to Slack, ensuring immediate team visibility. + +- **Project Progress Tracking**: Set up a Pipedream workflow that listens for changes in project status on Frame.io. When a project moves to the next stage, the workflow can update a corresponding task in Asana and notify the team. + +- **Asset Backup and Archival**: Create a Pipedream workflow that responds to the upload of new assets to Frame.io, automatically backing them up to Google Drive or Dropbox, providing a secondary storage solution for important files. diff --git a/components/frame/actions/create-asset/create-asset.mjs b/components/frame/actions/create-asset/create-asset.mjs new file mode 100644 index 0000000000000..30a84eb70a4fc --- /dev/null +++ b/components/frame/actions/create-asset/create-asset.mjs @@ -0,0 +1,73 @@ +import frame from "../../frame.app.mjs"; + +export default { + key: "frame-create-asset", + name: "Create Asset", + description: "Creates a new asset in Frame.io. [See the documentation](https://developer.frame.io/api/reference/operation/createAsset/)", + version: "0.0.2", + type: "action", + props: { + frame, + accountId: { + propDefinition: [ + frame, + "accountId", + ], + }, + assetId: { + propDefinition: [ + frame, + "parentAssetId", + ({ accountId }) => ({ + accountId, + }), + ], + }, + type: { + type: "string", + label: "Type", + description: "The type of the asset (file or folder).", + options: [ + "file", + "folder", + ], + }, + name: { + type: "string", + label: "Name", + description: "The name the asset should have in Frame.io. This value does not have to match the name of the file on disk; it can be whatever you want it to be in Frame.io.", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "Brief description of the Asset.", + optional: true, + }, + sourceUrl: { + type: "string", + label: "Source URL", + description: "The URL of the source file.", + optional: true, + }, + }, + async run({ $ }) { + const { + frame, assetId, sourceUrl, ...data + } = this; + const response = await frame.createAsset({ + $, + assetId, + data: { + ...data, + ...(sourceUrl && { + source: { + url: sourceUrl, + }, + }), + }, + }); + $.export("$summary", `Successfully created asset (ID: ${response.id})`); + return response; + }, +}; diff --git a/components/frame/actions/create-comment/create-comment.mjs b/components/frame/actions/create-comment/create-comment.mjs new file mode 100644 index 0000000000000..b4074e34de394 --- /dev/null +++ b/components/frame/actions/create-comment/create-comment.mjs @@ -0,0 +1,81 @@ +import frame from "../../frame.app.mjs"; + +export default { + key: "frame-create-comment", + name: "Create Comment", + description: "Creates a new comment on an asset in Frame.io. [See the documentation](https://developer.frame.io/api/reference/operation/createComment/)", + version: "0.0.2", + type: "action", + props: { + frame, + accountId: { + propDefinition: [ + frame, + "accountId", + ], + }, + assetId: { + propDefinition: [ + frame, + "assetId", + ({ accountId }) => ({ + accountId, + }), + ], + }, + text: { + type: "string", + label: "Text", + description: "The body of the comment.", + }, + annotation: { + type: "string", + label: "Annotation", + description: "Serialized list of geometry and/or drawing data. [Learn more here](https://developer.frame.io/docs/workflows-assets/working-with-annotations)", + optional: true, + }, + page: { + type: "integer", + label: "Page", + description: "Page number for a comment (documents only).", + optional: true, + }, + timestamp: { + type: "string", + label: "Timestamp", + description: "Timestamp for the comment, in frames, starting at 0.", + optional: true, + }, + duration: { + type: "integer", + label: "Duration", + description: "Used to produce range-based comments, this is the duration measured in frames.", + optional: true, + }, + private: { + type: "boolean", + label: "Private", + description: "Set to true to make your comment a \"Team-only Comment\" that won't be visible to anonymous reviewers or Collaborators.", + optional: true, + }, + }, + async run({ $ }) { + const { + frame, assetId, text, annotation, page, timestamp, duration, + } = this; + const response = await frame.sendComment({ + $, + assetId, + data: { + text, + annotation, + page, + timestamp, + duration, + private: this.private, + }, + }); + $.export("$summary", `Successfully created comment (ID: ${response.id})`); + return response; + }, +}; diff --git a/components/frame/actions/create-project/create-project.mjs b/components/frame/actions/create-project/create-project.mjs new file mode 100644 index 0000000000000..fba16084b7ca3 --- /dev/null +++ b/components/frame/actions/create-project/create-project.mjs @@ -0,0 +1,60 @@ +import frame from "../../frame.app.mjs"; +import { parseObjectValues } from "../../common/utils.mjs"; + +export default { + key: "frame-create-project", + name: "Create Project", + description: "Creates a new project on Frame.io. [See the documentation](https://developer.frame.io/api/reference/operation/createProject/)", + version: "0.0.2", + type: "action", + props: { + frame, + accountId: { + propDefinition: [ + frame, + "accountId", + ], + }, + teamId: { + propDefinition: [ + frame, + "teamId", + ({ accountId }) => ({ + accountId, + }), + ], + }, + name: { + type: "string", + label: "Name", + description: "The name of the project.", + optional: true, + }, + private: { + type: "boolean", + label: "Private", + description: "If true, the project is private to the creating user", + optional: true, + }, + projectPreferences: { + type: "object", + label: "Project Preferences", + description: "Preferences to set for the project. [See the documentation](https://developer.frame.io/api/reference/operation/createProject/) for valid properties.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.frame.createProject({ + $, + teamId: this.teamId, + data: { + name: this.name, + private: this.private, + project_preferences: parseObjectValues(this.projectPreferences), + }, + }); + + $.export("$summary", `Successfully created project (ID: ${response.id})`); + return response; + }, +}; diff --git a/components/frame/actions/search-assets/search-assets.mjs b/components/frame/actions/search-assets/search-assets.mjs new file mode 100644 index 0000000000000..b4e81b8c62798 --- /dev/null +++ b/components/frame/actions/search-assets/search-assets.mjs @@ -0,0 +1,74 @@ +import frame from "../../frame.app.mjs"; + +export default { + key: "frame-search-assets", + name: "Search Assets", + description: "Performs advanced searching for assets in Frame.io. [See the documentation](https://developer.frame.io/api/reference/operation/librarySearchGet/)", + version: "0.0.2", + type: "action", + props: { + frame, + accountId: { + propDefinition: [ + frame, + "accountId", + ], + }, + query: { + type: "string", + label: "Query", + description: "Search text", + optional: true, + }, + sort: { + type: "string", + label: "Sort", + description: "Sorting parameters", + optional: true, + }, + includeDeleted: { + type: "boolean", + label: "Include Deleted", + description: "Flag to include soft-deleted records in results", + optional: true, + }, + filterType: { + type: "string", + label: "Filter Type", + description: "If specified, only assets of this type will be returned.", + optional: true, + options: [ + "file", + "folder", + ], + }, + page: { + type: "integer", + label: "Page", + description: "The page to retrieve", + optional: true, + }, + pageSize: { + type: "integer", + label: "Page Size", + description: "The number of results to include in the page", + optional: true, + }, + }, + async run({ $ }) { + let response = await this.frame.searchAssets({ + $, + params: { + account_id: this.accountId, + q: this.query, + include_deleted: this.includeDeleted, + page: this.page, + page_size: this.pageSize, + sort: this.sort, + }, + }); + if (this.filterType) response = response?.filter((e) => e.type === this.filterType); + $.export("$summary", `Successfully retrieved ${response?.length} assets`); + return response; + }, +}; diff --git a/components/frame/common/constants.mjs b/components/frame/common/constants.mjs new file mode 100644 index 0000000000000..1180727af1228 --- /dev/null +++ b/components/frame/common/constants.mjs @@ -0,0 +1,24 @@ +export const WEBHOOK_EVENT_OPTIONS = [ + "project.created", + "project.updated", + "project.deleted", + "asset.created", + "asset.copied", + "asset.updated", + "asset.deleted", + "asset.ready", + "asset.versioned", + "action.executed", + "interaction.executed", + "asset.label.updated", + "comment.created", + "comment.updated", + "comment.deleted", + "comment.completed", + "comment.uncompleted", + "reviewlink.created", + "collaborator.created", + "collaborator.deleted", + "teammember.created", + "teammember.deleted", +]; diff --git a/components/frame/common/utils.mjs b/components/frame/common/utils.mjs new file mode 100644 index 0000000000000..3c2177bdbe5cb --- /dev/null +++ b/components/frame/common/utils.mjs @@ -0,0 +1,11 @@ +export function parseObjectValues(obj) { + return obj && Object.fromEntries( + Object.entries(obj).map(([ + key, + value, + ]) => [ + key, + JSON.parse(value), + ]), + ); +} diff --git a/components/frame/frame.app.mjs b/components/frame/frame.app.mjs index 3df4646397f29..f98eca0df55e1 100644 --- a/components/frame/frame.app.mjs +++ b/components/frame/frame.app.mjs @@ -1,11 +1,168 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "frame", - propDefinitions: {}, + propDefinitions: { + accountId: { + type: "string", + label: "Account ID", + description: "Select an account or provide a custom ID.", + async options() { + const data = await this.listAccounts(); + return data?.map((account) => ({ + label: account.display_name, + value: account.id, + })); + }, + }, + teamId: { + type: "string", + label: "Team ID", + description: "The ID of the team.", + async options({ accountId }) { + const data = await this.listTeams(accountId); + return data?.map(({ + name, id, + }) => ({ + label: name, + value: id, + })); + }, + }, + assetId: { + type: "string", + label: "Asset ID", + description: "Select an asset (file) or provide a custom ID.", + useQuery: true, + async options({ + accountId, page, query, + }) { + return this.getAssetOptions({ + account_id: accountId, + q: query, + page, + }, "file"); + }, + }, + parentAssetId: { + type: "string", + label: "Parent Asset ID", + description: "Select a parent asset (folder) or provide a custom ID.", + useQuery: true, + async options({ + accountId, page, query, + }) { + return this.getAssetOptions({ + account_id: accountId, + q: query, + page, + }, "folder"); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.frame.io/v2"; + }, + async _makeRequest({ + $ = this, + headers, + ...otherOpts + }) { + return axios($, { + baseURL: this._baseUrl(), + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + ...otherOpts, + }); + }, + async listAccounts() { + return this._makeRequest({ + url: "/accounts", + }); + }, + async listTeams(accountId) { + return this._makeRequest({ + url: `/accounts/${accountId}/teams`, + }); + }, + async searchAssets(args) { + return this._makeRequest({ + url: "/search/library", + ...args, + }); + }, + async getAssetOptions(params, assetType) { + let data = await this.searchAssets({ + params, + }); + if (assetType) data = data?.filter((e) => e.type === assetType); + return data?.map(({ + name, id, + }) => ({ + label: name, + value: id, + })); + }, + async sendComment({ + assetId, ...args + }) { + return this._makeRequest({ + method: "POST", + url: `/assets/${assetId}/comments`, + ...args, + }); + }, + async createProject({ + teamId, ...args + }) { + return this._makeRequest({ + method: "POST", + url: `/teams/${teamId}/projects`, + ...args, + }); + }, + async createAsset({ + assetId, ...args + }) { + return this._makeRequest({ + method: "POST", + url: `/assets/${assetId}/children`, + ...args, + }); + }, + async createWebhook({ + teamId, ...args + }) { + return this._makeRequest({ + method: "POST", + url: `/teams/${teamId}/hooks`, + ...args, + }); + }, + async deleteWebhook(hookId) { + return this._makeRequest({ + method: "DELETE", + url: `/hooks/${hookId}`, + }); + }, + async getAsset(id) { + return this._makeRequest({ + url: `assets/${id}`, + }); + }, + async getComment(id) { + return this._makeRequest({ + url: `comments/${id}`, + }); + }, + async getProject(id) { + return this._makeRequest({ + url: `projects/${id}`, + }); }, }, }; diff --git a/components/frame/package.json b/components/frame/package.json new file mode 100644 index 0000000000000..f6ff0b963c2ed --- /dev/null +++ b/components/frame/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/frame", + "version": "0.2.0", + "description": "Pipedream Frame.io Components", + "main": "frame.app.mjs", + "keywords": [ + "pipedream", + "frame" + ], + "homepage": "https://pipedream.com/apps/frame", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.1" + } +} diff --git a/components/frame/sources/common/common.mjs b/components/frame/sources/common/common.mjs new file mode 100644 index 0000000000000..7c95f47716ce9 --- /dev/null +++ b/components/frame/sources/common/common.mjs @@ -0,0 +1,76 @@ +import app from "../../frame.app.mjs"; + +export default { + props: { + app, + http: "$.interface.http", + db: "$.service.db", + accountId: { + propDefinition: [ + app, + "accountId", + ], + }, + teamId: { + propDefinition: [ + app, + "teamId", + ({ accountId }) => ({ + accountId, + }), + ], + }, + }, + methods: { + async getResourceData() { + return undefined; + }, + getSummary() { + return "New event"; + }, + _getWebhookId() { + return this.db.get("webhookId"); + }, + _setWebhookId(value) { + this.db.set("webhookId", value); + }, + }, + hooks: { + async activate() { + const data = { + url: this.http.endpoint, + name: `Pipedream Source (${this.getSummary()})`, + events: this.getHookData(), + }; + + const { id } = await this.app.createWebhook({ + teamId: this.teamId, + data, + }); + this._setWebhookId(id); + }, + async deactivate() { + const id = this._getWebhookId(); + if (id) { + await this.app.deleteWebhook(id); + } + }, + }, + async run({ body }) { + if (body) { + const ts = Date.now(); + const id = body.resource.id; + const data = await this.getResourceData(id); + this.$emit({ + ...body, + ...(data && { + data, + }), + }, { + id: `${id}_${ts}`, + summary: this.getSummary(body), + ts, + }); + } + }, +}; diff --git a/components/frame/sources/new-asset-instant/new-asset-instant.mjs b/components/frame/sources/new-asset-instant/new-asset-instant.mjs new file mode 100644 index 0000000000000..cd7559bd35b39 --- /dev/null +++ b/components/frame/sources/new-asset-instant/new-asset-instant.mjs @@ -0,0 +1,26 @@ +import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "frame-new-asset-instant", + name: "New Asset (Instant)", + description: "Emit new event when an asset is uploaded. [See the documentation](https://developer.frame.io/api/reference/operation/createWebhookForTeam/)", + version: "0.1.0", + type: "source", + sampleEmit, + methods: { + ...common.methods, + getSummary() { + return "New Asset"; + }, + getHookData() { + return [ + "asset.created", + ]; + }, + async getResourceData(id) { + return this.app.getAsset(id); + }, + }, +}; diff --git a/components/frame/sources/new-asset-instant/test-event.mjs b/components/frame/sources/new-asset-instant/test-event.mjs new file mode 100644 index 0000000000000..947c27f74a3a2 --- /dev/null +++ b/components/frame/sources/new-asset-instant/test-event.mjs @@ -0,0 +1,239 @@ +export default { + data: { + scrub_sheet: null, + rating: null, + shared: false, + drm: null, + hls_manifest: null, + scrub_video: null, + filesize: 31719, + private: false, + workfront_approval_status: null, + status: "created", + timecode: null, + transcode_statuses: null, + h264_1080_best: null, + filetype: "image/jpeg", + workfront_task_connected: null, + upload_completed_at: null, + page_full: null, + waveform: null, + allow_original_playback: true, + archived_at: null, + _type: "file", + team_id: "string", + frames: 0, + workfront_task_id: null, + is_session_watermarked: false, + downloads: { + h264_1080_best: null, + h264_2160: null, + h264_360: null, + h264_540: null, + h264_720: null, + image_full: null, + image_high: null, + image_small: null, + }, + id: "string", + view_count: 0, + fps: null, + hard_deleted_at: null, + copy: false, + webm_1080_best: null, + name: "string", + h265_hdr_1080: null, + includes: null, + autoversion_id: null, + subtitle_tracks: null, + public_item_count: 0, + audio_tracks: null, + versions: 0, + page_thumb: null, + cover_asset_id: null, + webm_540: null, + image_full: null, + resource_id: null, + parent_id: "string", + metadata_flags: null, + creator: { + from_adobe: false, + digest_frequency: "0 * * * *", + image_256: + "string", + timezone_value: "America/New_York", + _type: "user", + image_128: + "string", + user_default_color: "#40ff9f", + id: "string", + email_confirm_by: null, + name: "string", + profile_image_original: null, + features_seen: null, + roles: null, + mfa_enforced_at: null, + integrations: {}, + image_64: + "string", + first_login_at: "datetime", + last_seen: null, + account_id: "string", + bio: null, + upload_url: + "string", + image_32: + "string", + email_preferences: null, + next_digest_date: "datetime", + from_google: true, + deleted_at: null, + inserted_at: "datetime", + joined_via: "organic", + updated_at: "datetime", + highest_account_role: null, + phone: null, + context: null, + location: null, + profile_image: + "string", + email: "string", + }, + image_small: null, + properties: null, + archive_status: null, + creator_id: "string", + upload_failed_at: null, + uploaded_at: "datetime", + page_high: null, + webm_720: null, + index: -4, + thumb: null, + h265_hdr_360: null, + workfront_task_title: null, + video_h264_180: null, + account_id: "string", + thumb_scrub: null, + project: { + _type: "project", + archive_status: "standard", + archived_at: null, + archived_file_count: 0, + archived_storage: 0, + collaborator_count: 0, + deleted_at: null, + description: null, + file_count: 5, + folder_count: 5, + id: "string", + ignore_archive: false, + inserted_at: "datetime", + invite_url: null, + name: "string", + owner_id: "string", + private: false, + read_only: false, + resource_id: null, + root_asset_id: "string", + shared: false, + storage: 105947, + team_id: "string", + updated_at: "datetime", + user_permissions: { + can_download: true, + can_manage_devices: true, + can_modify_template: false, + can_public_share_presentation: true, + can_public_share_review_link: true, + can_see_watermarking_settings: true, + can_share_downloadable_presentation: true, + can_share_downloadable_review_link: true, + can_share_unwatermarked_presentation: true, + can_share_unwatermarked_review_link: true, + can_share_without_drm_presentation: false, + can_share_without_drm_review_link: false, + can_share_without_forensic_watermark_presentation: true, + can_share_without_forensic_watermark_review_link: true, + can_view_devices: true, + }, + }, + label: "none", + frame_custom: null, + workfront_asset_locked_at: null, + archive_scheduled_at: null, + frame_cover: null, + user_permissions: { + can_download: true, + can_manage_devices: true, + can_modify_template: false, + can_public_share_presentation: true, + can_public_share_review_link: true, + can_see_watermarking_settings: true, + can_share_downloadable_presentation: true, + can_share_downloadable_review_link: true, + can_share_unwatermarked_presentation: true, + can_share_unwatermarked_review_link: true, + can_share_without_drm_presentation: false, + can_share_without_drm_review_link: false, + can_share_without_forensic_watermark_presentation: true, + can_share_without_forensic_watermark_review_link: true, + can_view_devices: true, + }, + original: + "string", + asset_type: null, + page_small: null, + master_manifest_concentrate: null, + h265_hdr_2160: null, + cover: null, + project_id: "string", + h264_2160: null, + deleted_at: null, + is_360: false, + transcoded_at: null, + h265_hdr_720: null, + inserted_at: "datetime", + thumb_540: null, + updated_at: "datetime", + type: "file", + is_hls_required: false, + workfront_connected: null, + image_high: null, + h265_hdr_540: null, + duration: null, + h264_720: null, + description: null, + source: { + _type: "asset_source", + external_id: null, + id: "string", + provider: null, + url: "string", + }, + archive_from: "datetime", + frame_thumb: null, + realtime_upload: null, + webm_360: null, + item_count: 0, + h264_540: null, + h264_360: null, + thumb_orig_ar_540: null, + checksums: null, + is_forensically_watermarked: false, + comment_count: 0, + }, + project: { + id: "string", + }, + resource: { + id: "string", + type: "asset", + }, + team: { + id: "string", + }, + type: "asset.created", + user: { + id: "string", + }, +}; diff --git a/components/frame/sources/new-comment-instant/new-comment-instant.mjs b/components/frame/sources/new-comment-instant/new-comment-instant.mjs new file mode 100644 index 0000000000000..8ac44a4ceb1af --- /dev/null +++ b/components/frame/sources/new-comment-instant/new-comment-instant.mjs @@ -0,0 +1,26 @@ +import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "frame-new-comment-instant", + name: "New Comment (Instant)", + description: "Emit new event when a new comment is left on an asset. [See the documentation](https://developer.frame.io/api/reference/operation/createWebhookForTeam/)", + version: "0.1.0", + type: "source", + sampleEmit, + methods: { + ...common.methods, + getSummary() { + return "New Comment"; + }, + getHookData() { + return [ + "comment.created", + ]; + }, + async getResourceData(id) { + return this.app.getComment(id); + }, + }, +}; diff --git a/components/frame/sources/new-comment-instant/test-event.mjs b/components/frame/sources/new-comment-instant/test-event.mjs new file mode 100644 index 0000000000000..c67ba5fd74bdf --- /dev/null +++ b/components/frame/sources/new-comment-instant/test-event.mjs @@ -0,0 +1,95 @@ +export default { + data: { + thumb: + "string", + private: null, + updated_at: "datetime", + deleted_at: null, + device_channel_input_id: null, + annotation: null, + _type: "comment", + id: "string", + completer_id: null, + completed: false, + parent_id: null, + target_asset_id: null, + timestamp_microseconds: null, + review_link_id: null, + text: "string", + duration: null, + owner_id: "string", + fov: 65, + read_count: 0, + owner: { + image_128: + "string", + joined_via: "organic", + integrations: {}, + roles: null, + updated_at: "datetime", + user_default_color: "#40ff9f", + deleted_at: null, + next_digest_date: "datetime", + location: null, + _type: "user", + from_google: true, + image_256: + "string", + id: "string", + profile_image: + "string", + mfa_enforced_at: null, + name: "string", + last_seen: null, + from_adobe: false, + inserted_at: "datetime", + bio: null, + account_id: "string", + highest_account_role: null, + features_seen: null, + phone: null, + email_confirm_by: null, + upload_url: + "string", + first_login_at: "datetime", + timezone_value: "America/New_York", + email_preferences: null, + image_64: + "string", + profile_image_original: null, + context: null, + digest_frequency: "0 * * * *", + image_32: + "string", + email: "string", + }, + yaw: null, + aspect_ratio: 1.777778, + inserted_at: "2024-04-18T23:52:37.051534Z", + like_count: 0, + frame: null, + timestamp: null, + comment_entities: [], + completed_at: null, + asset_id: "f963d1b3-23c5-48d5-a2a2-4107211e81e6", + text_edited_at: null, + pitch: null, + has_replies: false, + anonymous_user_id: null, + page: null, + }, + project: { + id: "string", + }, + resource: { + id: "string", + type: "comment", + }, + team: { + id: "string", + }, + type: "comment.created", + user: { + id: "string", + }, +}; diff --git a/components/frame/sources/new-project-instant/new-project-instant.mjs b/components/frame/sources/new-project-instant/new-project-instant.mjs new file mode 100644 index 0000000000000..d707aaac04c5c --- /dev/null +++ b/components/frame/sources/new-project-instant/new-project-instant.mjs @@ -0,0 +1,26 @@ +import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "frame-new-project-instant", + name: "New Project (Instant)", + description: "Emit new event when a new project is created. [See the documentation](https://developer.frame.io/api/reference/operation/createWebhookForTeam/)", + version: "0.1.0", + type: "source", + sampleEmit, + methods: { + ...common.methods, + getSummary() { + return "New Project"; + }, + getHookData() { + return [ + "project.created", + ]; + }, + async getResourceData(id) { + return this.app.getProject(id); + }, + }, +}; diff --git a/components/frame/sources/new-project-instant/test-event.mjs b/components/frame/sources/new-project-instant/test-event.mjs new file mode 100644 index 0000000000000..5dade33d9a417 --- /dev/null +++ b/components/frame/sources/new-project-instant/test-event.mjs @@ -0,0 +1,169 @@ +export default { + data: { + _type: "project", + archive_status: "standard", + archived_at: null, + archived_file_count: 0, + archived_storage: 0, + collaborator_count: 0, + deleted_at: null, + description: null, + file_count: 0, + folder_count: 0, + has_drm_assets: false, + id: "string", + ignore_archive: false, + inserted_at: "datetime", + invite_url: null, + name: "string", + owner_id: "string", + project_preferences: { + _type: "project_preference", + collaborator_can_download: true, + collaborator_can_invite: true, + collaborator_can_share: false, + deleted_at: null, + devices_enabled: false, + id: "string", + inserted_at: "datetime", + notify_on_new_asset: true, + notify_on_new_collaborator: true, + notify_on_new_comment: true, + notify_on_new_mention: true, + notify_on_updated_label: true, + notify_slack: false, + project_id: null, + review_link_preferences: null, + updated_at: "datetime", + user_id: null, + }, + read_only: false, + resource_id: null, + root_asset: { + filesize: 0, + private: false, + is_forensically_watermarked: false, + _type: "folder", + shared: false, + drm: null, + account_id: "string", + cover_asset_id: null, + workfront_task_connected: null, + workfront_asset_locked_at: null, + frames: 0, + comment_count: 0, + creator_id: "string", + id: "string", + copy: false, + archive_status: null, + name: "root", + team_id: "string", + is_360: false, + workfront_approval_status: null, + versions: 0, + resource_id: null, + uploaded_at: null, + inserted_at: "datetime", + project_id: "string", + is_session_watermarked: false, + deleted_at: null, + properties: null, + checksums: null, + rating: null, + index: 0, + workfront_task_title: null, + workfront_task_id: null, + public_item_count: 0, + label: "none", + workfront_connected: null, + is_hls_required: false, + audio_tracks: null, + user_permissions: { + can_download: true, + can_manage_devices: true, + can_modify_template: false, + can_public_share_presentation: true, + can_public_share_review_link: true, + can_see_watermarking_settings: true, + can_share_downloadable_presentation: true, + can_share_downloadable_review_link: true, + can_share_unwatermarked_presentation: true, + can_share_unwatermarked_review_link: true, + can_share_without_drm_presentation: false, + can_share_without_drm_review_link: false, + can_share_without_forensic_watermark_presentation: true, + can_share_without_forensic_watermark_review_link: true, + can_view_devices: true, + }, + realtime_upload: null, + archived_at: null, + parent_id: null, + allow_original_playback: true, + autoversion_id: null, + item_count: 0, + type: "folder", + subtitle_tracks: null, + updated_at: "datetime", + filetype: null, + fps: null, + duration: null, + description: null, + metadata_flags: null, + hard_deleted_at: null, + archive_scheduled_at: null, + }, + root_asset_id: "string", + shared: false, + storage: 0, + team_id: "string", + updated_at: "datetime", + user_permissions: { + can_download: true, + can_manage_devices: true, + can_modify_template: false, + can_public_share_presentation: true, + can_public_share_review_link: true, + can_see_watermarking_settings: true, + can_share_downloadable_presentation: true, + can_share_downloadable_review_link: true, + can_share_unwatermarked_presentation: true, + can_share_unwatermarked_review_link: true, + can_share_without_drm_presentation: false, + can_share_without_drm_review_link: false, + can_share_without_forensic_watermark_presentation: true, + can_share_without_forensic_watermark_review_link: true, + can_view_devices: true, + }, + user_preferences: { + _type: "project_preference", + collaborator_can_download: true, + collaborator_can_invite: true, + collaborator_can_share: false, + deleted_at: null, + devices_enabled: false, + id: "string", + inserted_at: "datetime", + notify_on_new_asset: true, + notify_on_new_collaborator: true, + notify_on_new_comment: true, + notify_on_new_mention: true, + notify_on_updated_label: true, + notify_slack: false, + project_id: "string", + review_link_preferences: null, + updated_at: "datetime", + user_id: "string", + }, + }, + resource: { + id: "string", + type: "project", + }, + team: { + id: "string", + }, + type: "project.created", + user: { + id: "string", + }, +}; diff --git a/components/frame/sources/new-webhook-event-instant/new-webhook-event-instant.mjs b/components/frame/sources/new-webhook-event-instant/new-webhook-event-instant.mjs new file mode 100644 index 0000000000000..77add38ffeed6 --- /dev/null +++ b/components/frame/sources/new-webhook-event-instant/new-webhook-event-instant.mjs @@ -0,0 +1,34 @@ +import common from "../common/common.mjs"; +import { WEBHOOK_EVENT_OPTIONS } from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "frame-new-webhook-event-instant", + name: "New Webhook Event (Instant)", + description: + "Emit new event when a new project is created. [See the documentation](https://developer.frame.io/api/reference/operation/createWebhookForTeam/)", + version: "0.0.2", + type: "source", + sampleEmit, + props: { + ...common.props, + eventTypes: { + type: "string[]", + label: "Event Types", + description: "The types of events to listen for.", + options: WEBHOOK_EVENT_OPTIONS, + }, + }, + methods: { + ...common.methods, + getSummary(body) { + return `New Event${body + ? ` (${body?.type})` + : ""}`; + }, + getHookData() { + return this.eventTypes; + }, + }, +}; diff --git a/components/frame/sources/new-webhook-event-instant/test-event.mjs b/components/frame/sources/new-webhook-event-instant/test-event.mjs new file mode 100644 index 0000000000000..ef76b8b175bfc --- /dev/null +++ b/components/frame/sources/new-webhook-event-instant/test-event.mjs @@ -0,0 +1,16 @@ +export default { + project: { + id: "string", + }, + resource: { + id: "string", + type: "resourceType", + }, + team: { + id: "string", + }, + type: "event.type", + user: { + id: "string", + }, +}; diff --git a/components/franconnect/franconnect.app.mjs b/components/franconnect/franconnect.app.mjs new file mode 100644 index 0000000000000..a014983e54eae --- /dev/null +++ b/components/franconnect/franconnect.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "franconnect", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/franconnect/package.json b/components/franconnect/package.json new file mode 100644 index 0000000000000..eb971fe733b0b --- /dev/null +++ b/components/franconnect/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/franconnect", + "version": "0.0.1", + "description": "Pipedream FranConnect Components", + "main": "franconnect.app.mjs", + "keywords": [ + "pipedream", + "franconnect" + ], + "homepage": "https://pipedream.com/apps/franconnect", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/frappe/README.md b/components/frappe/README.md index 329079a59e982..8363ca2504407 100644 --- a/components/frappe/README.md +++ b/components/frappe/README.md @@ -1,9 +1,11 @@ # Overview -The Frappe API can be used to build a variety of applications, including: +The Frappe API provides a robust interface to interact with the Frappe Framework, enabling developers to create, read, update, and delete records in Frappe apps, trigger workflows, and handle complex business logic. Leveraging Pipedream's capabilities, you can harness this API to automate tasks, sync data across different platforms, and build powerful serverless workflows that enhance the productivity and efficiency of your business processes. -- Web applications -- Mobile applications -- Desktop applications -- FrappeFrameworks -- Custom Frappe applications +# Example Use Cases + +- **Automated Invoice Generation and Distribution**: Use the Frappe API to monitor new sales orders and automatically create invoices in Frappe. Then, send these invoices to customers via email using the SendGrid or Mailgun app on Pipedream. + +- **Dynamic Inventory Management**: Create a workflow that triggers when stock levels change in an external inventory management system. Use the Frappe API to update the corresponding item quantities in Frappe's Stock module, ensuring real-time inventory accuracy. + +- **Customer Feedback Aggregation**: After a customer interaction within a Frappe app, trigger an automated workflow to send a feedback form using Google Forms or Typeform. Collect and store responses in Frappe, analyzing customer satisfaction and response rates over time. diff --git a/components/frappe/package.json b/components/frappe/package.json new file mode 100644 index 0000000000000..5bfafe9e72ca9 --- /dev/null +++ b/components/frappe/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/frappe", + "version": "0.6.0", + "description": "Pipedream frappe Components", + "main": "frappe.app.mjs", + "keywords": [ + "pipedream", + "frappe" + ], + "homepage": "https://pipedream.com/apps/frappe", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/fraudlabs_pro/README.md b/components/fraudlabs_pro/README.md index 8a60efa458521..afbaa8e9f93c6 100644 --- a/components/fraudlabs_pro/README.md +++ b/components/fraudlabs_pro/README.md @@ -1,21 +1,11 @@ # Overview -Fraud Detection for E-commerce +The FraudLabs Pro API offers a robust suite of fraud prevention tools that empower users to screen online transactions for fraud. It leverages advanced scoring analytics to rate the risk level of a transaction based on various verification checks, such as IP address, email, transaction velocity, and more. Using Pipedream, developers can integrate these capabilities into workflows to automate fraud checks, augment data analysis, and trigger actions based on fraud scores. -The FraudLabs Pro API enables e-commerce businesses to detect fraudulent orders -with ease. Businesses can simply send in an order’s information, and FraudLabs -Pro will return a fraud score and report explaining the decision. This allows -businesses to make informed decisions on whether to accept, flag, or reject an -order. +# Example Use Cases -FraudLabs Pro can help e-commerce businesses in the following ways: +- **E-commerce Transaction Screening**: Automate the process of verifying transactions on your e-commerce platform. When a new order is placed, trigger a workflow on Pipedream to send order details to FraudLabs Pro for analysis. Depending on the fraud score, the workflow could automatically approve, flag for review, or reject the order, and update the order status in your e-commerce system. -- Detect fraudulent orders -- Reduce chargebacks and fraud-related losses -- Prevent fraudsters from abusing the system +- **Account Creation Monitoring**: Monitor and analyze account sign-ups in real-time by integrating FraudLabs Pro with a user management platform. Each time a new user registers, a Pipedream workflow can be triggered to assess the risk of fraud. If the score exceeds a certain threshold, the account can be disabled and an alert sent to administrators. -Here are some examples of what you can build with the FraudLabs Pro API: - -- A fraud detection system for your e-commerce website -- A chargeback prevention system for your business -- A fraud monitoring system to track fraudsters and their activities +- **Payment Gateway Decision Automation**: Streamline the payment approval process by coupling FraudLabs Pro with your payment gateway. Upon payment submission, Pipedream can call the FraudLabs Pro API to assess the risk level. Based on the result, the workflow could either proceed with the payment process or hold the transaction for further review, and even notify your finance or security team. diff --git a/components/freeagent/README.md b/components/freeagent/README.md index dd61e9dbc001c..4ab8c6d8ce3fb 100644 --- a/components/freeagent/README.md +++ b/components/freeagent/README.md @@ -1,9 +1,13 @@ # Overview -With the FreeAgent API, you can build applications that: +FreeAgent, a powerful accounting tool, offers an API enabling businesses to automate various aspects of financial management. With the FreeAgent API, you can create, read, update, and delete invoices, bills, expenses, and much more, directly through Pipedream. This allows for the creation of custom automations that can save time, reduce errors, and streamline financial operations. -- Automate or bulk-update your client contact details -- Update your project details en masse -- Fetch invoices, contacts, projects, or any other data -- Generate reports -- And much more! +# Example Use Cases + +- **Automated Invoice Generation and Dispatch**: Upon a new sale from an e-commerce platform like Shopify, trigger a Pipedream workflow to create and send an invoice via FreeAgent, ensuring timely billing and consistency in invoice management. + +- **Expense Tracking and Reporting**: Connect FreeAgent to a time tracking app like Toggl. Use Pipedream to create an expense in FreeAgent whenever a new time entry is logged in Toggl, making sure all billable hours are accounted for and simplifying client billing. + +- **Financial Health Dashboard**: Integrate FreeAgent with a data visualization tool such as Google Sheets on Pipedream. Automate the export of financial data like cash flow, profit and loss, and overdue invoices into a Google Sheet for real-time financial analysis and decision-making. + +Please note, it was mentioned to write about FreeAgent API, but the link provided was for eversign. The examples are based on FreeAgent API. If you meant to ask about eversign, please let me know! diff --git a/components/freeagent/package.json b/components/freeagent/package.json new file mode 100644 index 0000000000000..f75cbfb76c73f --- /dev/null +++ b/components/freeagent/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/freeagent", + "version": "0.6.0", + "description": "Pipedream freeagent Components", + "main": "freeagent.app.mjs", + "keywords": [ + "pipedream", + "freeagent" + ], + "homepage": "https://pipedream.com/apps/freeagent", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/freedcamp/README.md b/components/freedcamp/README.md new file mode 100644 index 0000000000000..b3597a0d233eb --- /dev/null +++ b/components/freedcamp/README.md @@ -0,0 +1,11 @@ +# Overview + +The Freedcamp API enables you to interact with your project management tools programmatically, offering a way to streamline, automate, and integrate your project workflows. With Pipedream, you can harness this API to create custom automations, such as syncing tasks, managing milestones, and tracking project updates. Pipedream's serverless platform offers a low-code approach, making it significantly simpler to connect Freedcamp with other apps and services to optimize your project management processes. + +# Example Use Cases + +- **Automated Task Syncing with Google Sheets**: Seamlessly sync new tasks created in Freedcamp with a Google Sheets spreadsheet. Every time a team member creates a task in Freedcamp, Pipedream detects the event and adds a new row to the sheet, keeping your task tracking up-to-date across platforms. + +- **Slack Notifications for Project Milestones**: Keep your team informed with automated Slack messages whenever a milestone is reached in Freedcamp. Set up a Pipedream workflow that listens for milestone completions and then sends a customized notification to your Slack workspace, ensuring everyone is on the same page. + +- **GitHub Integration for Development Tasks**: Connect Freedcamp with GitHub to automate the creation of GitHub issues based on specific tasks in Freedcamp. When a task labeled as a 'bug' or 'feature request' is added, Pipedream can automatically create an issue in the respective GitHub repository, streamlining the development workflow. diff --git a/components/freelancer/README.md b/components/freelancer/README.md index 26a58cb3d948b..66cf01ca4da69 100644 --- a/components/freelancer/README.md +++ b/components/freelancer/README.md @@ -1,12 +1,11 @@ # Overview -The Freelancer API allows developers to tap into the world’s largest -outsourcing marketplace. With the API, developers can search for and submit -projects, get project details and messages, and access freelancer profiles. +The Freelancer API offers a programmatic way to interact with Freelancer.com, enabling you to automate tasks like searching for projects, posting jobs, managing bids, and communicating with users. By integrating this API with Pipedream, you can create powerful, serverless workflows that streamline your freelance business operations or help you build tools that assist others in managing their freelancing tasks. -Here are some examples of what you can build with the Freelancer API: +# Example Use Cases -- A project search and submission tool -- A project management tool -- A freelancer profile tool -- A messaging tool +- **Automated Job Posting**: Set up a workflow that automatically posts new jobs on Freelancer based on certain triggers. For example, when a new issue is labeled as "help wanted" in your GitHub repository, Pipedream could post a corresponding job listing on Freelancer to find developers. + +- **Bid Monitoring and Notification**: Create a Pipedream workflow that monitors new bids on your Freelancer projects and sends real-time notifications via Slack or email. This way, you can quickly review and respond to potential candidates without constantly checking the site. + +- **Project Management Sync**: Develop a system where project updates on Freelancer are synchronized with your project management tool, like Trello or Asana. When a freelancer submits work, the corresponding task in your project management app can be automatically marked as complete. diff --git a/components/freelancer/package.json b/components/freelancer/package.json new file mode 100644 index 0000000000000..7c8b8b2877cd2 --- /dev/null +++ b/components/freelancer/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/freelancer", + "version": "0.6.0", + "description": "Pipedream freelancer Components", + "main": "freelancer.app.mjs", + "keywords": [ + "pipedream", + "freelancer" + ], + "homepage": "https://pipedream.com/apps/freelancer", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/freshbooks/README.md b/components/freshbooks/README.md index 0bb6321b1b1bd..6f385a1cea2d7 100644 --- a/components/freshbooks/README.md +++ b/components/freshbooks/README.md @@ -1,9 +1,11 @@ # Overview -With the FreshBooks API, you can build applications that: +The FreshBooks API lets you automate invoicing, expense tracking, timekeeping and client management tasks. By harnessing this API within Pipedream, you can create custom automations that sync your financial data with other business applications, respond to client actions quickly, and streamline payment processes. The API enables you to pull reports, manage invoices, and update client information, making it a powerful tool for freelancers and small businesses looking to automate their accounting tasks. -- Create and manage invoices -- Track payments -- Manage clients -- Generate reports -- And more! +# Example Use Cases + +- **Invoice Automation Workflow**: When a new sale is recorded in your e-commerce platform (like Shopify), trigger a Pipedream workflow that creates a corresponding invoice in FreshBooks. This workflow can be set to collect necessary details such as customer information, items purchased, and prices, then pushes this data to FreshBooks to generate an invoice automatically. + +- **Expense Tracking Integration**: Connect FreshBooks to a receipt scanning app (like Expensify) using Pipedream. Each time a new receipt is scanned and categorized in the scanning app, the workflow triggers, creating or updating an expense entry in FreshBooks. This keeps your expense tracking up-to-date without manual data entry. + +- **Time Tracking and Billing Sync**: Integrate FreshBooks with a time tracking tool such as Toggl. Each time a time entry is logged in Toggl, a Pipedream workflow runs, pulling the time entry data and creating or updating an invoice in FreshBooks for the corresponding project or client, ensuring that billing is accurate and up-to-date. diff --git a/components/freshdesk/README.md b/components/freshdesk/README.md index 90db937e4d10e..4ab99f5b8feac 100644 --- a/components/freshdesk/README.md +++ b/components/freshdesk/README.md @@ -1,9 +1,11 @@ # Overview -With the Freshdesk API, you can create powerful customer support applications. -Here are some examples of what you can build: +The Freshdesk API empowers you to interact programmatically with your customer support platform, creating possibilities for automating repetitive tasks, integrating with other services, and enhancing customer experiences. With Pipedream, you can effortlessly connect Freshdesk to a multitude of apps, tapping into triggers and actions that streamline workflows. For instance, you can automate ticket creation, sync customer issues with a CRM, or trigger notifications based on ticket updates, all within a serverless environment. -- A ticketing system for tracking customer issues -- A knowledge base for self-service customer support -- A customer community for engaging customers and crowd-sourcing solutions -- A call center management system +# Example Use Cases + +- **Auto-ticket Generation from Emails**: When an email arrives at a specific mailbox, Pipedream can capture that event and use the Freshdesk API to create a new support ticket. This ensures that no customer query goes unanswered and allows support teams to manage all requests from Freshdesk's centralized interface. + +- **CRM Sync with Ticket Updates**: Once a ticket in Freshdesk is updated—be it a status change, priority update, or note addition—a Pipedream workflow can push these updates to a connected CRM, like Salesforce. This keeps sales and support teams aligned on customer interactions, providing a seamless customer service experience. + +- **Slack Notifications for Urgent Tickets**: Using Pipedream's integration with Slack, build a workflow that listens for new tickets tagged as 'urgent' in Freshdesk. Then, automatically send a message to a designated Slack channel or direct message to a team member, ensuring immediate attention is given to critical support issues. diff --git a/components/freshlearn/README.md b/components/freshlearn/README.md new file mode 100644 index 0000000000000..9eeab4077d6bb --- /dev/null +++ b/components/freshlearn/README.md @@ -0,0 +1,11 @@ +# Overview + +The Freshlearn API provides a gateway to interact with Freshlearn's learning management system, allowing for the automation of course creation, user management, and analytics retrieval. By leveraging this API within Pipedream, you can create powerful serverless workflows that integrate Freshlearn with other apps to enhance the educational experience, streamline administrative tasks, and analyze course performance. + +# Example Use Cases + +- **Automated Course Enrollment**: Synchronize sign-ups from a Google Form to Freshlearn by automatically enrolling new users into a specific course once they submit the form, ensuring a seamless registration process. + +- **Course Performance Dashboard**: Collect analytics from Freshlearn and send them to Google Sheets using a scheduled Pipedream workflow, providing a regularly updated overview of how well different courses are performing. + +- **Slack Notifications for Learner Progress**: Set up a workflow that monitors learner progress within Freshlearn and sends updates or congratulations messages to a Slack channel when learners achieve milestones, keeping the community engaged. diff --git a/components/freshmarketer/README.md b/components/freshmarketer/README.md new file mode 100644 index 0000000000000..5c20c4d1450b0 --- /dev/null +++ b/components/freshmarketer/README.md @@ -0,0 +1,11 @@ +# Overview + +The Freshmarketer API enables automation of marketing efforts, providing tools to manage campaigns, leads, and more. In Pipedream, you can leverage these capabilities to create custom workflows that interact with Freshmarketer's data and connect it to other services. Combining Freshmarketer with Pipedream's serverless platform allows you to automate actions like syncing contacts, triggering campaigns based on external events, or analyzing marketing performance with external analytics tools. + +# Example Use Cases + +- **Sync New Shopify Customers to Freshmarketer**: Automatically add new customers from your Shopify store as contacts in Freshmarketer, tagging them based on the products they purchased. This makes targeted campaigns simpler and more effective. + +- **Automated Support Ticket Creation**: When a lead scores reach a certain threshold in Freshmarketer, create a support ticket in Freshdesk for personalized engagement. This workflow unites marketing intelligence with customer support. + +- **Slack Notifications for Campaign Performance**: Send daily summaries of campaign performance to a designated Slack channel. This workflow keeps teams informed and enables quick reactions to marketing data. diff --git a/components/freshmarketer/actions/add-update-contact/add-update-contact.mjs b/components/freshmarketer/actions/add-update-contact/add-update-contact.mjs new file mode 100644 index 0000000000000..ed79488b048b7 --- /dev/null +++ b/components/freshmarketer/actions/add-update-contact/add-update-contact.mjs @@ -0,0 +1,149 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { SUBSCRIPTION_STATUS_OPTIONS } from "../../common/constants.mjs"; +import freshmarketer from "../../freshmarketer.app.mjs"; + +export default { + key: "freshmarketer-add-update-contact", + name: "Add or Update Contact", + description: "Create a new contact or updates an existing one.", + version: "0.0.{{ts}}", + type: "action", + props: { + freshmarketer, + email: { + propDefinition: [ + freshmarketer, + "contactEmail", + ], + description: "Email on which enttity needs to be created or updated.", + }, + firstName: { + type: "string", + label: "First Name", + description: "First name of the contact.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the contact.", + optional: true, + }, + subscriptionStatus: { + type: "string", + label: "Subscription Status", + description: "Status of subscription that the contact is in.", + optional: true, + options: SUBSCRIPTION_STATUS_OPTIONS, + }, + jobTitle: { + type: "string", + label: "Job Title", + description: "Designation of the contact in the account he belongs to.", + optional: true, + }, + workNumber: { + type: "string", + label: "Work Number", + description: "Work phone number of the contact.", + optional: true, + }, + externalId: { + type: "string", + label: "External Id", + description: "External ID of the contact.", + optional: true, + }, + mobileNumber: { + type: "string", + label: "Mobile Number", + description: "Mobile phone number of the contact.", + optional: true, + }, + address: { + type: "string", + label: "Address", + description: "Address of the contact.", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "City that the contact belongs to.", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "State that the contact belongs to.", + optional: true, + }, + zipcode: { + type: "string", + label: "Zipcode", + description: "Zipcode of the region that the contact belongs to.", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "Country that the contact belongs to.", + optional: true, + }, + territoryId: { + propDefinition: [ + freshmarketer, + "territoryId", + ], + optional: true, + }, + leadSourceId: { + propDefinition: [ + freshmarketer, + "leadSourceId", + ], + optional: true, + }, + ownerId: { + propDefinition: [ + freshmarketer, + "ownerId", + ], + optional: true, + }, + }, + async run({ $ }) { + if (!this.email && !this.mobileNumber && ! this.externalId) { + throw new ConfigurationError("Either Email/Mobile/Twitter ID is required to create a contact."); + } + const response = await this.freshmarketer.upsertContact({ + $, + data: { + unique_identifier: { + emails: this.email, + }, + contact: { + first_name: this.firstName, + last_name: this.lastName, + subscription_status: this.subscriptionStatus, + job_title: this.jobTitle, + emails: this.email, + work_number: this.workNumber, + external_id: this.externalId, + mobile_number: this.mobileNumber, + address: this.address, + city: this.city, + state: this.state, + zipcode: this.zipcode, + country: this.country, + territory_id: this.territoryId, + lead_source_id: this.leadSourceId && this.leadSourceId.toString(), + owner_id: this.ownerId && this.ownerId.toString(), + }, + }, + }); + + $.export("$summary", `The contact was successfully created/updated with Id: ${response.contact.id}`); + return response; + }, +}; diff --git a/components/freshmarketer/actions/find-contact/find-contact.mjs b/components/freshmarketer/actions/find-contact/find-contact.mjs new file mode 100644 index 0000000000000..9684481945622 --- /dev/null +++ b/components/freshmarketer/actions/find-contact/find-contact.mjs @@ -0,0 +1,39 @@ +import freshmarketer from "../../freshmarketer.app.mjs"; + +export default { + key: "freshmarketer-find-contact", + name: "Find Contact", + description: "Searches for a contact by email and returns contact details if found. [See the documentation](https://developers.freshworks.com/crm/api/#search)", + version: "0.0.1", + type: "action", + props: { + freshmarketer, + contactEmail: { + propDefinition: [ + freshmarketer, + "contactEmail", + ], + }, + }, + async run({ $ }) { + const response = await this.freshmarketer.searchContactByEmail({ + $, + data: { + filter_rule: [ + { + attribute: "contact_email.email", + operator: "is_in", + value: this.contactEmail, + }, + ], + }, + }); + + if (!response.contacts.length) { + throw new Error("Contact not found"); + } + + $.export("$summary", `Successfully found contact with email ${this.contactEmail}`); + return response.contacts[0]; + }, +}; diff --git a/components/freshmarketer/actions/remove-contact-from-list/remove-contact-from-list.mjs b/components/freshmarketer/actions/remove-contact-from-list/remove-contact-from-list.mjs new file mode 100644 index 0000000000000..c82c0ee0cfd61 --- /dev/null +++ b/components/freshmarketer/actions/remove-contact-from-list/remove-contact-from-list.mjs @@ -0,0 +1,77 @@ +import { ConfigurationError } from "@pipedream/platform"; +import freshmarketer from "../../freshmarketer.app.mjs"; + +export default { + key: "freshmarketer-remove-contact-from-list", + name: "Remove Contact From List", + description: "Removes a contact from a specific list by email or contact ID. [See the documentation](https://developers.freshworks.com/crm/api/#remove_contact_from_list)", + version: "0.0.1", + type: "action", + props: { + freshmarketer, + listId: { + propDefinition: [ + freshmarketer, + "listId", + ], + }, + contactId: { + propDefinition: [ + freshmarketer, + "contactId", + ({ listId }) => ({ + listId, + }), + ], + optional: true, + }, + contactEmail: { + propDefinition: [ + freshmarketer, + "contactEmail", + ], + optional: true, + }, + }, + async run({ $ }) { + if ((!this.contactId && !this.contactEmail) || (this.contactId && this.contactEmail)) { + throw new ConfigurationError("You must provide either a Contact ID or a Contact Email."); + } + + let contactId = this.contactId; + + if (!contactId && this.contactEmail) { + const searchResponse = await this.freshmarketer.searchContactByEmail({ + data: { + filter_rule: [ + { + attribute: "contact_email.email", + operator: "is_in", + value: this.contactEmail, + }, + ], + }, + }); + + if (searchResponse.contacts.length) { + contactId = searchResponse.contacts[0].id; + } else { + throw new Error("Contact not found with the provided email."); + } + } + + const response = await this.freshmarketer.removeContactFromList({ + $, + listId: this.listId, + data: { + ids: [ + contactId, + ], + }, + }); + + $.export("$summary", `Successfully removed contact from list with ID: ${this.listId}`); + + return response; + }, +}; diff --git a/components/freshmarketer/common/constants.mjs b/components/freshmarketer/common/constants.mjs new file mode 100644 index 0000000000000..1aac6b3ed625c --- /dev/null +++ b/components/freshmarketer/common/constants.mjs @@ -0,0 +1,22 @@ +export const SUBSCRIPTION_STATUS_OPTIONS = [ + { + value: "0", + label: "Unsubscribed", + }, + { + value: "1", + label: "Subscribed", + }, + { + value: "2", + label: "Not Subscribed", + }, + { + value: "3", + label: "Reported as Spam", + }, + { + value: "4", + label: "Bounced", + }, +]; diff --git a/components/freshmarketer/freshmarketer.app.mjs b/components/freshmarketer/freshmarketer.app.mjs index ed021089e2b0b..e62757ffc8196 100644 --- a/components/freshmarketer/freshmarketer.app.mjs +++ b/components/freshmarketer/freshmarketer.app.mjs @@ -1,11 +1,204 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "freshmarketer", - propDefinitions: {}, + propDefinitions: { + contactId: { + type: "string", + label: "Contact ID", + description: "The ID of the contact", + async options({ + page, listId, + }) { + const { contacts } = await this.listContacts({ + listId, + params: { + page, + }, + }); + + return contacts.map(({ + id: value, email: label, + }) => ({ + label, + value, + })); + }, + }, + leadSourceId: { + type: "string", + label: "Lead Source ID", + description: "ID of the source where contact came from.", + async options({ page }) { + const { lead_sources: data } = await this.listSelectors({ + selector: "lead_sources", + params: { + page, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + listId: { + type: "string", + label: "List ID", + description: "The ID of the list", + async options({ page }) { + const { lists } = await this.listLists({ + params: { + page, + }, + }); + + return lists.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + ownerId: { + type: "string", + label: "Owner ID", + description: "ID of the user to whom the contact has been assigned.", + async options({ page }) { + const { users } = await this.listSelectors({ + selector: "owners", + params: { + page, + }, + }); + + return users.map(({ + id: value, email: label, + }) => ({ + label, + value, + })); + }, + }, + territoryId: { + type: "string", + label: "Territory ID", + description: "ID of the territory that the contact belongs to.", + async options({ page }) { + const { territories } = await this.listSelectors({ + selector: "territories", + params: { + page, + }, + }); + + return territories.map(({ + id: value, email: label, + }) => ({ + label, + value, + })); + }, + }, + contactEmail: { + type: "string", + label: "Contact Email", + description: "The email of the contact", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `https://${this.$auth.domain}.myfreshworks.com/crm/sales/api`; + }, + _headers() { + return { + "Authorization": `Token token=${this.$auth.api_key}`, + "Content-Type": "application/json", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listContacts({ + listId, ...opts + }) { + return this._makeRequest({ + path: `/contacts/lists/${listId}`, + ...opts, + }); + }, + listLists(opts = {}) { + return this._makeRequest({ + path: "/lists", + ...opts, + }); + }, + listSelectors({ + selector, ...opts + }) { + return this._makeRequest({ + path: `/selector/${selector}`, + ...opts, + }); + }, + searchContactByEmail(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/filtered_search/contact", + ...opts, + }); + }, + removeContactFromList({ + listId, ...otps + }) { + return this._makeRequest({ + method: "PUT", + path: `/lists/${listId}/remove_contacts`, + ...otps, + }); + }, + upsertContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contacts/upsert", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const { contacts } = await fn({ + params, + ...opts, + }); + for (const d of contacts) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = contacts.length; + + } while (hasMore); }, }, -}; \ No newline at end of file +}; diff --git a/components/freshmarketer/package.json b/components/freshmarketer/package.json index ee19a4a12fe44..2b1cd305595bf 100644 --- a/components/freshmarketer/package.json +++ b/components/freshmarketer/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/freshmarketer", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Freshmarketer Components", "main": "freshmarketer.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" } -} \ No newline at end of file +} + diff --git a/components/freshmarketer/sources/common/base.mjs b/components/freshmarketer/sources/common/base.mjs new file mode 100644 index 0000000000000..4ce70a4d77c80 --- /dev/null +++ b/components/freshmarketer/sources/common/base.mjs @@ -0,0 +1,67 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import freshmarketer from "../../freshmarketer.app.mjs"; + +export default { + props: { + freshmarketer, + db: "$.service.db", + listId: { + propDefinition: [ + freshmarketer, + "listId", + ], + }, + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01T00:00:00Z"; + }, + _setLastDate(updatedAt) { + this.db.set("lastDate", updatedAt); + }, + generateMeta(contact) { + const ts = Date.parse(contact.updated_at || new Date()); + return { + id: `${contact.id}-${ts}`, + summary: this.getSummary(contact), + ts: ts, + }; + }, + async startEvent(maxResults = 0) { + const lastDate = this._getLastDate(); + + const data = this.freshmarketer.paginate({ + fn: this.freshmarketer.listContacts, + listId: this.listId, + params: { + sort: "updated_at", + sort_type: "desc", + }, + }); + + const responseArray = await this.prepareData({ + data, + lastDate, + maxResults, + }); + + for (const item of responseArray.reverse()) { + this.$emit(item, this.generateMeta(item)); + } + }, + }, + hooks: { + async deploy() { + await this.startEvent(25); + }, + }, + async run() { + await this.startEvent(); + }, +}; diff --git a/components/freshmarketer/sources/new-contact-added-to-list/new-contact-added-to-list.mjs b/components/freshmarketer/sources/new-contact-added-to-list/new-contact-added-to-list.mjs new file mode 100644 index 0000000000000..16c7a2aa93b44 --- /dev/null +++ b/components/freshmarketer/sources/new-contact-added-to-list/new-contact-added-to-list.mjs @@ -0,0 +1,36 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "freshmarketer-new-contact-added-to-list", + name: "New Contact Added to List", + description: "Emit new event when a contact is added to a specific list.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSummary(contact) { + return `Contact ${contact.email} added to list ${this.listId}`; + }, + async prepareData({ + data, + lastDate, + maxResults, + }) { + const responseArray = []; + for await (const item of data) { + if (Date.parse(item.updated_at) <= Date.parse(lastDate)) break; + responseArray.push(item); + } + + if (responseArray.length) this._setLastDate(responseArray[0].updated_at); + if (maxResults && responseArray.length > maxResults) { + responseArray.length = maxResults; + } + return responseArray; + }, + }, + sampleEmit, +}; diff --git a/components/freshmarketer/sources/new-contact-added-to-list/test-event.mjs b/components/freshmarketer/sources/new-contact-added-to-list/test-event.mjs new file mode 100644 index 0000000000000..043a5aac0edff --- /dev/null +++ b/components/freshmarketer/sources/new-contact-added-to-list/test-event.mjs @@ -0,0 +1,20 @@ +export default { + "partial": true, + "id": 2000003516, + "job_title": "CEO", + "lead_score": 0, + "email": "jamessampleton@gmail.com", + "work_number": "(473)-160-8261", + "mobile_number": "1-926-555-9503", + "open_deals_amount": "0.0", + "display_name": "James Sampleton (sample)", + "avatar": "https://lh3.googleusercontent.com/-DbQggdfJ2_w/Vi4cRujEXKI/AAAAAAAAABs/-Byl2CFY3lI/w140-h140-p/Image3.png", + "last_contacted_mode": "email_outgoing", + "last_contacted": "2016-02-08T02:36:07-08:00", + "first_name": "James", + "last_name": "Sampleton (sample)", + "city": "San Diego", + "country": "USA", + "created_at": "2016-02-11T02:36:06-08:00", + "updated_at": "2016-02-10T02:36:07-08:00" +} \ No newline at end of file diff --git a/components/freshmarketer/sources/new-contact-removed-from-list/new-contact-removed-from-list.mjs b/components/freshmarketer/sources/new-contact-removed-from-list/new-contact-removed-from-list.mjs new file mode 100644 index 0000000000000..f56877f6d25ce --- /dev/null +++ b/components/freshmarketer/sources/new-contact-removed-from-list/new-contact-removed-from-list.mjs @@ -0,0 +1,55 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "freshmarketer-new-contact-removed-from-list", + name: "New Contact Removed From List", + description: "Emit new event as soon as a contact is removed from a list.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSummary(contact) { + return `Contact ${contact.email} removed from list`; + }, + _getContacts() { + return this.db.get("contacts") || []; + }, + _setContacts(contacts) { + this.db.set("contacts", contacts); + }, + async prepareData({ data }) { + const contacts = this._getContacts(); + + let newData = []; + for await (const { + id, email, + } of data) { + newData.push({ + id, + email, + }); + } + + this._setContacts(newData); + + let idsArray = []; + for (const { id } of newData) { + idsArray.push(id); + } + + const filteredIds = contacts.filter((item) => !idsArray.includes(item.id)); + + const responseArray = []; + if (filteredIds.length) { + for (const item of filteredIds) { + responseArray.push(item); + } + } + return responseArray; + }, + }, + sampleEmit, +}; diff --git a/components/freshmarketer/sources/new-contact-removed-from-list/test-event.mjs b/components/freshmarketer/sources/new-contact-removed-from-list/test-event.mjs new file mode 100644 index 0000000000000..2fee108b7fa7e --- /dev/null +++ b/components/freshmarketer/sources/new-contact-removed-from-list/test-event.mjs @@ -0,0 +1,4 @@ +export default { + "id": 2000003516, + "email": "jamessampleton@gmail.com", +} \ No newline at end of file diff --git a/components/freshping/README.md b/components/freshping/README.md index 91ee6d64c38aa..f3bc0efe5f891 100644 --- a/components/freshping/README.md +++ b/components/freshping/README.md @@ -1,5 +1,11 @@ # Overview -With the Freshping API, you can build a monitoring system for your website or -application. You can use it to track the uptime and response time of your site -or application, as well as to monitor the status of yourFreshping checks. +The Freshping API allows you to automate uptime and performance monitoring for your websites and web services. By integrating with Pipedream, you can harness this capability to create customized alerts, log incidents, and trigger actions based on the health of your monitored services. This opens up possibilities for proactive maintenance, real-time status updates, and seamless incident management by connecting to various apps and services. + +# Example Use Cases + +- **Incident Logging to Google Sheets**: When Freshping detects downtime or any performance issues, you can automatically log these incidents into a Google Sheets spreadsheet. This workflow helps maintain an organized record of outages or performance dips, facilitating trend analysis and reporting. + +- **Slack Notification for Downtime**: Set up a workflow where Freshping alerts of any detected downtime are instantly pushed as notifications to a designated Slack channel. This keeps your team informed in real-time, enabling swift response to outages and maintaining service quality. + +- **Automated Support Ticket Creation**: Integrate Freshping with a ticketing system like Zendesk; every time Freshping detects an issue, automatically generate a support ticket. This ensures that your support team can start working on a resolution as soon as possible, improving response times and customer satisfaction. diff --git a/components/freshping/package.json b/components/freshping/package.json new file mode 100644 index 0000000000000..bf439939609e8 --- /dev/null +++ b/components/freshping/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/freshping", + "version": "0.6.0", + "description": "Pipedream freshping Components", + "main": "freshping.app.mjs", + "keywords": [ + "pipedream", + "freshping" + ], + "homepage": "https://pipedream.com/apps/freshping", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/freshsales/README.md b/components/freshsales/README.md index e5cda0de8c59b..e166995f3e2ce 100644 --- a/components/freshsales/README.md +++ b/components/freshsales/README.md @@ -1,13 +1,11 @@ # Overview -With the Freshsales API, you can build a variety of applications and -integrations to extend the functionality of your Freshsales account. Here are -some examples of what you can build: - -- A custom app to import data from another CRM system into Freshsales -- An integration to send data from Freshsales to a third-party data analysis - platform -- A tool to automate repetitive tasks within Freshsales, such as lead - assignment or follow-up emails -- A dashboard to visualize Freshsales data in real-time -- A support ticketing system that integrates with Freshsales +The Freshsales API offers a suite of functionalities to enhance your CRM experience by automating sales processes, syncing customer data, and integrating with other business applications. By leveraging Pipedream's serverless platform, you can create custom automations that trigger actions within Freshsales or in other apps, based on events in Freshsales. This opens up possibilities for sales teams to streamline lead management, enhance customer engagement, and analyze sales performance. + +# Example Use Cases + +- **Lead Scoring Automation**: Automatically update the lead score in Freshsales based on customer interactions tracked in other tools. For instance, increase a lead's score when they open a marketing email sent via SendGrid. + +- **Deal Progression Notifications**: Set up a workflow that sends real-time Slack notifications to a sales channel when a deal moves to a new stage in the Freshsales pipeline, keeping the team instantly informed. + +- **Customer Success Handover**: Automate the process of creating a task in project management tools like Asana when a deal is won in Freshsales, ensuring smooth handover from sales to customer success teams. diff --git a/components/freshservice/README.md b/components/freshservice/README.md index f09599ae5760e..823ebe0990238 100644 --- a/components/freshservice/README.md +++ b/components/freshservice/README.md @@ -1,11 +1,11 @@ # Overview -Freshservice is a powerful API that allows you to build a wide variety of -applications and integrations. Here are just a few examples of what you can -build: - -- A custom ticketing system -- An integration with your company's CRM system -- A tool to help your team track projects and tasks -- A system to automate customer support requests -- And much more! +The Freshservice API opens up a realm of possibilities for streamlining IT service management processes. It allows you to interact programmatically with Freshservice features, such as creating tickets, managing users, updating service catalog items, and more. Leveraging the Freshservice API on Pipedream, you can automate repetitive tasks, create complex workflows that react to events in Freshservice in real time, and connect Freshservice to a multitude of other services to enhance its capabilities and your productivity. + +# Example Use Cases + +- **Ticket Management Automation**: Automate the creation, updating, and closing of tickets in Freshservice based on triggers from email, support forms, or chatbots. For instance, use Pipedream to listen for incoming emails through a platform like Gmail, parse the content, and automatically create tickets in Freshservice with the relevant details extracted from the email. + +- **Asset Lifecycle Updates**: Keep asset records up-to-date by triggering workflows in Pipedream when changes occur in third-party asset management tools, such as Snipe-IT. When assets are added or updated in Snipe-IT, a Pipedream workflow can automatically reflect those changes in the Freshservice asset inventory, ensuring data consistency. + +- **User Provisioning and Management**: Streamline user onboarding and offboarding by integrating Freshservice with HR platforms like BambooHR. When a new employee is added to BambooHR, a Pipedream workflow can create a new user in Freshservice, assign them to the correct groups, and even initiate the IT onboarding ticket sequence. Conversely, when an employee leaves, the workflow can deactivate their Freshservice account and kick off the offboarding process. diff --git a/components/freshstatus/README.md b/components/freshstatus/README.md index 7f8a21fafaa3e..acdd11e40d15d 100644 --- a/components/freshstatus/README.md +++ b/components/freshstatus/README.md @@ -1,19 +1,11 @@ # Overview -The Freshstatus API allows developers to integrate Freshstatus into their -applications, enabling them to create, update, and manage their Freshstatus -account data. With the Freshstatus API, developers can manage: +Freshstatus by Freshworks is an API that lets you manage incident updates and maintenance events for your services. With it, you can communicate real-time status to your users, schedule maintenance windows, and keep a pulse on your system's health, all programmatically. When integrated with Pipedream, Freshstatus becomes even more powerful, allowing you to automate status updates, sync with your monitoring tools, and trigger notifications across various platforms. -- Users -- Organizations -- Services -- Incidents -- Maintenance windows +# Example Use Cases -Example applications that can be built using the Freshstatus API include: +- **Incident Alerting through Communication Channels**: Set up a workflow that triggers an alert on Slack, Teams, or Discord when a new incident is reported or an existing one is updated in Freshstatus. This keeps your team immediately informed and can speed up response times. -- A Freshstatus client for a desktop or mobile application -- A Freshstatus integration for a third-party ticketing system -- A Freshstatus dashboard for displaying status information -- A Freshstatus widget for a website or blog -- A Freshstatus bot for a chat application +- **Scheduled Maintenance Reminder**: Create a workflow that sends a reminder email to your customers a day before a scheduled maintenance. Use the Freshstatus API to fetch upcoming maintenances and connect to an email service like SendGrid to automate the notification process. + +- **Status Sync with Monitoring Tools**: Integrate Freshstatus with a monitoring tool like Datadog or Uptime Robot using Pipedream. When the monitoring tool detects downtime or issues, it automatically updates the status on Freshstatus, ensuring your status page reflects the most current information. diff --git a/components/freshstatus/package.json b/components/freshstatus/package.json new file mode 100644 index 0000000000000..a51b09fb9fb7d --- /dev/null +++ b/components/freshstatus/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/freshstatus", + "version": "0.6.0", + "description": "Pipedream freshstatus Components", + "main": "freshstatus.app.mjs", + "keywords": [ + "pipedream", + "freshstatus" + ], + "homepage": "https://pipedream.com/apps/freshstatus", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/frontapp/README.md b/components/frontapp/README.md index 8224914fc1b8c..c468388edcebc 100644 --- a/components/frontapp/README.md +++ b/components/frontapp/README.md @@ -1,16 +1,11 @@ # Overview -With the Front API, you can build a variety of applications and integrations to -help you and your team manage your email inboxes. Here are a few examples of -what you can build: +The Front API empowers you to automate the handling of your team's email inbox, collaborate on conversations, and streamline communication workflows. By harnessing this API on Pipedream, you can craft custom integrations that trigger actions in Front in response to events, synchronize data across platforms, and augment your team's productivity with automated tasks. This can include creating or updating conversations and contacts, managing tags, or firing off custom automation rules within Front. -- A personalized email assistant that can help you triage and respond to emails -- An integration with your CRM that automatically adds emails from customers to - your CRM -- A tool that flags important emails and sends you notifications about them -- An app that helps you find emails from a specific sender or with a certain - subject line -- A script that archives all of your old emails +# Example Use Cases -These are just a few examples of what you can build with the Front API – the -possibilities are really only limited by your imagination! +- **Automated Customer Support Ticket Creation**: Trigger a workflow on Pipedream when an email arrives at a certain inbox in Front. Analyze the contents using natural language processing (like with Google Cloud's NLP API) to categorize the request and automatically generate a support ticket in a tool like Jira or Zendesk. + +- **CRM Synchronization**: When a conversation is tagged with a specific keyword in Front, use that event to trigger a Pipedream workflow that updates a contact record in your CRM system, such as Salesforce or HubSpot. This ensures that sales or support teams have the latest interaction data without manual data entry. + +- **Lead Qualification Automation**: Set up a Pipedream workflow that listens for new conversations in Front. Use a service like Clearbit to enrich contact information, then apply custom logic to score and route leads to the appropriate team member in Front. This can help prioritize high-value leads and distribute them efficiently. diff --git a/components/frontapp/actions/send-new-message/send-new-message.mjs b/components/frontapp/actions/send-new-message/send-new-message.mjs index ac922f15a0192..257b2e8949631 100644 --- a/components/frontapp/actions/send-new-message/send-new-message.mjs +++ b/components/frontapp/actions/send-new-message/send-new-message.mjs @@ -5,7 +5,7 @@ export default { key: "frontapp-send-new-message", name: "Send New Message", description: "Sends a new message from a channel. It will create a new conversation. [See the docs here](https://dev.frontapp.com/reference/post_channels-channel-id-messages).", - version: "0.2.4", + version: "0.2.5", type: "action", props: { frontApp, @@ -38,7 +38,7 @@ export default { body: { type: "string", label: "Body", - description: "Body of the message", + description: "Body of the message. Accepts HTML", }, text: { type: "string", diff --git a/components/frontapp/package.json b/components/frontapp/package.json index 94a0fe556ff83..8c608f79bf12d 100644 --- a/components/frontapp/package.json +++ b/components/frontapp/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/frontapp", - "version": "0.5.0", + "version": "0.5.1", "description": "Pipedream Frontapp Components", "main": "frontapp.app.mjs", "keywords": [ diff --git a/components/frontegg/README.md b/components/frontegg/README.md new file mode 100644 index 0000000000000..9e8123f90c4e4 --- /dev/null +++ b/components/frontegg/README.md @@ -0,0 +1,11 @@ +# Overview + +The Frontegg API provides a suite of functionalities centered around user management, authentication, and security. It simplifies the integration of these essential features into any application. On Pipedream, you can tap into Frontegg's capabilities to automate and enhance user experience by crafting workflows that respond to events, manage user data, and enforce security protocols. + +# Example Use Cases + +- **User Registration Automation**: Create a workflow on Pipedream that triggers when a new user signs up via Frontegg. The workflow can automatically add the user data to a CRM like Salesforce, send a welcome email via SendGrid, and log the activity in a Google Sheet for record-keeping. + +- **Security Alerts and Compliance**: Set up a Pipedream workflow that listens for Frontegg security events, like login attempts from untrusted locations. When an alert is triggered, the workflow can post a notification in a Slack channel, create a ticket in Jira for investigation, and archive the event details securely to AWS S3. + +- **Periodic User Data Sync**: Configure a Pipedream scheduled workflow that periodically retrieves user data from Frontegg and updates a Mailchimp mailing list for marketing campaigns. It can also synchronize user role updates to a database like PostgreSQL, ensuring access levels in your application stay current. diff --git a/components/frontify/frontify.app.mjs b/components/frontify/frontify.app.mjs new file mode 100644 index 0000000000000..f7902b68e5e75 --- /dev/null +++ b/components/frontify/frontify.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "frontify", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/frontify/package.json b/components/frontify/package.json new file mode 100644 index 0000000000000..7d1d59196f72b --- /dev/null +++ b/components/frontify/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/frontify", + "version": "0.0.1", + "description": "Pipedream Frontify Components", + "main": "frontify.app.mjs", + "keywords": [ + "pipedream", + "frontify" + ], + "homepage": "https://pipedream.com/apps/frontify", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/components/ftrack/README.md b/components/ftrack/README.md new file mode 100644 index 0000000000000..4a07f449fabd4 --- /dev/null +++ b/components/ftrack/README.md @@ -0,0 +1,11 @@ +# Overview + +The FTrack API is a powerhouse for creative project management, enabling users to automate and integrate a wide array of tasks related to project tracking, asset management, and team collaboration. With Pipedream, you can harness this API to create custom workflows that trigger actions within FTrack or connect it with other apps to streamline your creative operations. It's a toolset for improving efficiency by automating repetitive tasks, syncing data across applications, and orchestrating complex project management activities. + +# Example Use Cases + +- **Automate Asset Review Process**: Trigger a workflow in Pipedream when a new asset is uploaded to FTrack to notify stakeholders, initiate a review process, or log the action in another project management tool like Asana or Trello. This ensures timely reviews and maintains project momentum. + +- **Sync Project Data with External Databases**: Automatically export project updates or milestones from FTrack to an external database such as Google Sheets or Airtable. This can be particularly useful for reporting, audits, or when you need to share project status with external stakeholders who might not have access to FTrack. + +- **Enhance Communication with Messaging Platforms**: Send automatic updates or notifications to Slack channels or Microsoft Teams when certain events occur in FTrack, like task completions or status changes. This keeps the whole team aligned and promptly informed about project progress. diff --git a/components/full_contact/README.md b/components/full_contact/README.md index ae21caa3b82c6..461d1dcd9bf8f 100644 --- a/components/full_contact/README.md +++ b/components/full_contact/README.md @@ -1,9 +1,11 @@ # Overview -The Full Contact API lets you build a wide variety of applications, including: +FullContact's API enriches contact information, letting you turn partial identities like an email address into a full profile including social presence, demographic data, and more. This is invaluable for enhancing customer understanding and personalizing interactions. By leveraging Pipedream's powerful integrations, you can automate workflows that consume FullContact's enriched data, facilitating tasks like lead enrichment, audience segmentation, and personalized marketing at scale. -- CRM applications -- Contact management applications -- Address book apps -- Social media applications -- And more! +# Example Use Cases + +- **Lead Enrichment on CRM Update**: Trigger a workflow when a new lead is added to a CRM like HubSpot. Use FullContact to enrich the lead's information and update their CRM profile with additional details such as social media handles or job titles. This enriched data can then drive more tailored sales strategies. + +- **Personalized Email Campaigns**: On receiving a new subscriber in an email marketing tool like Mailchimp, pass the email to FullContact for enrichment. Use the returned data to segment the subscribers based on demographics or interests and tailor email campaigns, thus boosting engagement and conversions. + +- **Customer Support Enhancement**: When a support ticket is created in a platform like Zendesk, use FullContact to enrich the customer's profile information. Provide this additional context to support agents, enabling them to offer a more personalized and informed customer service experience. diff --git a/components/fullenrich/actions/enrich-contact/enrich-contact.mjs b/components/fullenrich/actions/enrich-contact/enrich-contact.mjs new file mode 100644 index 0000000000000..3fb76a6ae42f9 --- /dev/null +++ b/components/fullenrich/actions/enrich-contact/enrich-contact.mjs @@ -0,0 +1,108 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../fullenrich.app.mjs"; + +export default { + key: "fullenrich-enrich-contact", + name: "Enrich Contact", + description: "Starts the enrichment process for a specified contact. [See the documentation](https://docs.fullenrich.com/startbulk)", + version: "0.0.2", + type: "action", + props: { + app, + name: { + type: "string", + label: "Name", + description: "The name of the action.", + }, + webhookUrl: { + type: "string", + label: "Webhook URL", + description: "The Webhook URL that will be triggered when the enrichment is done.", + optional: true, + }, + firstname: { + type: "string", + label: "First Name", + description: "The first name of the contact to enrich.", + }, + lastname: { + type: "string", + label: "Last Name", + description: "The last name of the contact to enrich.", + }, + domain: { + type: "string", + label: "Domain", + description: "The domain of the contact's company (e.g., example.com). Optional if **Company Name** is provided.", + optional: true, + }, + companyName: { + type: "string", + label: "Company Name", + description: "The name of the contact's company. Optional if a **Domain** is provided.", + optional: true, + }, + linkedinUrl: { + type: "string", + label: "LinkedIn URL", + description: "The LinkedIn URL of the contact to increase the probability of finding emails and phones.", + optional: true, + }, + enrichFields: { + type: "string[]", + label: "Enrich Fields", + description: "The fields to enrich. By default, the action enriches contact emails and phones.", + optional: true, + options: [ + "contact.emails", + "contact.phones", + ], + }, + }, + methods: { + enrichContacts(args = {}) { + return this.app.post({ + path: "/contact/enrich/bulk", + ...args, + }); + }, + }, + async run({ $ }) { + const { + enrichContacts, + name, + webhookUrl, + firstname, + lastname, + domain, + companyName, + linkedinUrl, + enrichFields, + } = this; + + if (!domain && !companyName) { + throw new ConfigurationError("You must provide either a **Domain** or a **Company Name**."); + } + + const response = await enrichContacts({ + $, + data: { + name, + webhook_url: webhookUrl, + datas: [ + { + firstname, + lastname, + domain, + company_name: companyName, + linkedin_url: linkedinUrl, + enrich_fields: enrichFields, + }, + ], + }, + }); + + $.export("$summary", `Successfully started the enrichment process with ID \`${response.enrichment_id}\`.`); + return response; + }, +}; diff --git a/components/fullenrich/actions/get-enrichment-result/get-enrichment-result.mjs b/components/fullenrich/actions/get-enrichment-result/get-enrichment-result.mjs new file mode 100644 index 0000000000000..5165a50aaf134 --- /dev/null +++ b/components/fullenrich/actions/get-enrichment-result/get-enrichment-result.mjs @@ -0,0 +1,51 @@ +import app from "../../fullenrich.app.mjs"; + +export default { + key: "fullenrich-get-enrichment-result", + name: "Get Enrichment Result", + description: "Get the enrichment result for a specified contact. [See the documentation](https://docs.fullenrich.com/getbulk).", + version: "0.0.2", + type: "action", + props: { + app, + enrichmentId: { + type: "string", + label: "Enrichment ID", + description: "The ID of the enrichment to get the result for.", + }, + forceResults: { + type: "boolean", + label: "Force Results", + description: "This parameter forces the API to return what has been found so far, even if the enrichment is not finished. This may result in missing information and is not recommended for regular use.", + optional: true, + }, + }, + methods: { + getEnrichmentResult({ + enrichmentId, ...args + } = {}) { + return this.app._makeRequest({ + path: `/contact/enrich/bulk/${enrichmentId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + getEnrichmentResult, + enrichmentId, + forceResults, + } = this; + + const response = await getEnrichmentResult({ + $, + enrichmentId, + params: { + forceResults, + }, + }); + + $.export("$summary", `Successfully fetched enrichment result with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/fullenrich/fullenrich.app.mjs b/components/fullenrich/fullenrich.app.mjs new file mode 100644 index 0000000000000..a491517f68e93 --- /dev/null +++ b/components/fullenrich/fullenrich.app.mjs @@ -0,0 +1,33 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "fullenrich", + methods: { + getUrl(path) { + return `https://app.fullenrich.com/api/v1${path}`; + }, + getHeaders(headers) { + return { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.$auth.api_key}`, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + }, +}; diff --git a/components/fullenrich/package.json b/components/fullenrich/package.json new file mode 100644 index 0000000000000..111c3c4c4e3c9 --- /dev/null +++ b/components/fullenrich/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/fullenrich", + "version": "0.1.1", + "description": "Pipedream FullEnrich Components", + "main": "fullenrich.app.mjs", + "keywords": [ + "pipedream", + "fullenrich" + ], + "homepage": "https://pipedream.com/apps/fullenrich", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" + } +} diff --git a/components/fullstory/README.md b/components/fullstory/README.md index 749761553cc7d..1f774a1eab8f4 100644 --- a/components/fullstory/README.md +++ b/components/fullstory/README.md @@ -1,10 +1,11 @@ # Overview -Fullstory provides APIs that allow you to build a variety of features into your -web or mobile app. Here are some examples of what you can build: - -- A searchable record of all user interactions on your website or app -- Insights and analytics on user behavior -- The ability to replay user sessions to debug issues -- Funnels and conversion rate optimization tools -- Custom reporting and integration with other business tools +The Fullstory API unlocks a treasure trove of user session data, enabling developers to automate deep insights into customer experiences on their digital platforms. Leveraging this API on Pipedream allows you to craft custom workflows that respond to specific user interactions, seamlessly integrate session insights with other tools, and trigger actions based on user behavior metrics. It's a boon for teams aiming to enhance user experience, address pain points, and bolster conversion rates. + +# Example Use Cases + +- **Customer Support Ticket Creation:** Automatically generate support tickets in a tool like Zendesk when a Fullstory session replay shows a user experiencing a bug or getting stuck, ensuring fast and efficient customer support. + +- **User Journey Analytics:** Feed Fullstory session data into a data warehousing tool such as Google BigQuery. Analyze the paths users take, discover drop-off points, and use this data to optimize the user journey. + +- **Real-time Slack Alerts for UX Issues:** Set up alerts in Slack to notify relevant teams whenever Fullstory detects frustration signals, like rage clicks or thrashing mouse movements. This allows immediate action to enhance user experience. diff --git a/components/function/README.md b/components/function/README.md new file mode 100644 index 0000000000000..80115fee2169c --- /dev/null +++ b/components/function/README.md @@ -0,0 +1,11 @@ +# Overview + +The Function API allows you to run code snippets in various programming languages directly through API calls. This capability can extend Pipedream workflows by enabling custom logic processing, data transformation, or any operation that requires server-side code execution without deploying or managing servers. By using the Function API, you can incorporate code that's beyond the native actions available within Pipedream, offering a flexible and powerful way to handle complex tasks in your automations. + +# Example Use Cases + +- **Data Transformation and Aggregation**: Integrate Function API within a Pipedream workflow to process incoming data from a webhook. Transform the data structure, filter out unnecessary elements, or aggregate information before sending it to a database like PostgreSQL. + +- **Custom Authentication Logic**: When handling webhooks that require custom authentication, use the Function API to implement the necessary logic. Verify signatures or decrypt data to ensure secure communication between your Pipedream workflow and other services. + +- **Image or Document Processing**: In a workflow where you need to manipulate media, the Function API can perform tasks such as image resizing, format conversion, or PDF generation. Once processed, the results can be uploaded to a cloud storage service like Amazon S3. diff --git a/components/function/package.json b/components/function/package.json index a3121d2734509..8660fe03213ae 100644 --- a/components/function/package.json +++ b/components/function/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/funnelcockpit/README.md b/components/funnelcockpit/README.md new file mode 100644 index 0000000000000..db5d41292db0c --- /dev/null +++ b/components/funnelcockpit/README.md @@ -0,0 +1,11 @@ +# Overview + +The FunnelCockpit API enables the automation of marketing funnel tasks, such as managing contacts, sending out emails, or updating campaigns. With Pipedream, you can harness this API to create powerful workflows that trigger on various events, manage data across different platforms, and streamline your digital marketing efforts. + +# Example Use Cases + +- **Sync Contacts to CRM**: Automatically add new contacts from your FunnelCockpit campaigns into a CRM like Salesforce or HubSpot. When a contact is created in FunnelCockpit, the workflow is triggered, creating or updating the contact in your CRM with the latest information. + +- **Email Campaign Trigger**: Utilize the FunnelCockpit API to send targeted emails through an email service provider like SendGrid or Mailchimp. Set up a Pipedream workflow that triggers an email campaign based on specific user behaviors or milestones reached within your FunnelCockpit funnels. + +- **Analytics Reporting**: Generate reports by integrating FunnelCockpit with Google Sheets or Data Studio. When a new sale or conversion is recorded in FunnelCockpit, Pipedream can automate the process of logging this data into your chosen analytics tool, providing real-time insights and data visualization. diff --git a/components/funnelcockpit/package.json b/components/funnelcockpit/package.json index 33e202d4fc6d9..9d640158fcb87 100644 --- a/components/funnelcockpit/package.json +++ b/components/funnelcockpit/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/gagelist/actions/create-gage/create-gage.mjs b/components/gagelist/actions/create-gage/create-gage.mjs new file mode 100644 index 0000000000000..f057d67532019 --- /dev/null +++ b/components/gagelist/actions/create-gage/create-gage.mjs @@ -0,0 +1,84 @@ +import gagelist from "../../gagelist.app.mjs"; + +export default { + key: "gagelist-create-gage", + name: "Create Gage", + description: "Creates a new gage on GageList. [See the documentation](https://gagelist.com/developer-resources/add-gage-record/)", + version: "0.0.1", + type: "action", + props: { + gagelist, + status: { + propDefinition: [ + gagelist, + "status", + ], + }, + manufacturer: { + propDefinition: [ + gagelist, + "manufacturer", + ], + }, + lastCalibrationDate: { + propDefinition: [ + gagelist, + "lastCalibrationDate", + ], + }, + calibrationDueDate: { + propDefinition: [ + gagelist, + "calibrationDueDate", + ], + }, + controlNumber: { + propDefinition: [ + gagelist, + "controlNumber", + ], + }, + type: { + propDefinition: [ + gagelist, + "type", + ], + }, + model: { + propDefinition: [ + gagelist, + "model", + ], + }, + condition: { + propDefinition: [ + gagelist, + "condition", + ], + }, + instructions: { + propDefinition: [ + gagelist, + "instructions", + ], + }, + }, + async run({ $ }) { + const response = await this.gagelist.createGage({ + $, + data: { + Status: this.status, + Manufacturer: this.manufacturer, + LastCalibrationDate: this.lastCalibrationDate, + CalibrationDueDate: this.calibrationDueDate, + ControlNumber: this.controlNumber, + Type: this.type, + Model: this.model, + ConditionAquired: this.condition, + CalibrationInstructions: this.instructions, + }, + }); + $.export("$summary", "Gage successfully created"); + return response; + }, +}; diff --git a/components/gagelist/actions/find-gage-by-id/find-gage-by-id.mjs b/components/gagelist/actions/find-gage-by-id/find-gage-by-id.mjs new file mode 100644 index 0000000000000..d7a71b996dd8e --- /dev/null +++ b/components/gagelist/actions/find-gage-by-id/find-gage-by-id.mjs @@ -0,0 +1,26 @@ +import gagelist from "../../gagelist.app.mjs"; + +export default { + key: "gagelist-find-gage-by-id", + name: "Find Gage by ID", + description: "Finds and retrieves a gage based on its ID in GageList. [See the documentation](https://gagelist.com/developer-resources/get-a-single-gage-record/)", + version: "0.0.1", + type: "action", + props: { + gagelist, + gageId: { + propDefinition: [ + gagelist, + "gageId", + ], + }, + }, + async run({ $ }) { + const response = await this.gagelist.getGage({ + $, + gageId: this.gageId, + }); + $.export("$summary", `Successfully retrieved gage with ID: ${this.gageId}`); + return response; + }, +}; diff --git a/components/gagelist/actions/update-gage/update-gage.mjs b/components/gagelist/actions/update-gage/update-gage.mjs new file mode 100644 index 0000000000000..57e5c5c6db5f8 --- /dev/null +++ b/components/gagelist/actions/update-gage/update-gage.mjs @@ -0,0 +1,103 @@ +import gagelist from "../../gagelist.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "gagelist-update-gage", + name: "Update Gage", + description: "Updates a specific gage using its ID on GageList. [See the documentation](https://gagelist.com/developer-resources/update-gage-record/)", + version: "0.0.1", + type: "action", + props: { + gagelist, + gageId: { + propDefinition: [ + gagelist, + "gageId", + ], + }, + status: { + propDefinition: [ + gagelist, + "status", + ], + optional: true, + }, + manufacturer: { + propDefinition: [ + gagelist, + "manufacturer", + ], + }, + lastCalibrationDate: { + propDefinition: [ + gagelist, + "lastCalibrationDate", + ], + }, + calibrationDueDate: { + propDefinition: [ + gagelist, + "calibrationDueDate", + ], + }, + controlNumber: { + propDefinition: [ + gagelist, + "controlNumber", + ], + }, + type: { + propDefinition: [ + gagelist, + "type", + ], + }, + model: { + propDefinition: [ + gagelist, + "model", + ], + }, + condition: { + propDefinition: [ + gagelist, + "condition", + ], + }, + instructions: { + propDefinition: [ + gagelist, + "instructions", + ], + }, + }, + async run({ $ }) { + const { + gagelist, + gageId, + ...fields + } = this; + + if (!Object.keys(fields).length) { + throw new ConfigurationError("Must update at least one field"); + } + + const response = await gagelist.updateGage({ + $, + data: { + Id: gageId, + Status: this.status, + Manufacturer: this.manufacturer, + LastCalibrationDate: this.lastCalibrationDate, + CalibrationDueDate: this.calibrationDueDate, + ControlNumber: this.controlNumber, + Type: this.type, + Model: this.model, + ConditionAquired: this.condition, + CalibrationInstructions: this.instructions, + }, + }); + $.export("$summary", `Successfully updated gage with ID: ${gageId}`); + return response; + }, +}; diff --git a/components/gagelist/common/constants.mjs b/components/gagelist/common/constants.mjs new file mode 100644 index 0000000000000..dd023228dae19 --- /dev/null +++ b/components/gagelist/common/constants.mjs @@ -0,0 +1,24 @@ +const DEFAULT_LIMIT = 50; + +const STATUS = [ + "Active", + "Employee Owned", + "Inactive", + "Lost", + "Out of Calibration", + "Out for Repair", + "Reference Only", + "Retired", +]; + +const CONDITION = [ + "New", + "Repaired", + "Used", +]; + +export default { + DEFAULT_LIMIT, + STATUS, + CONDITION, +}; diff --git a/components/gagelist/gagelist.app.mjs b/components/gagelist/gagelist.app.mjs new file mode 100644 index 0000000000000..ed13dbafe7a96 --- /dev/null +++ b/components/gagelist/gagelist.app.mjs @@ -0,0 +1,169 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "gagelist", + propDefinitions: { + gageId: { + type: "string", + label: "Gage ID", + description: "The ID of the gage", + async options({ page }) { + const { data } = await this.listGages({ + params: { + record_number: constants.DEFAULT_LIMIT, + start: (page * constants.DEFAULT_LIMIT) + 1, + }, + }); + return data?.map(({ Id: id }) => ({ + value: `${id}`, + label: `Gage ID# ${id}`, + })) || []; + }, + }, + manufacturer: { + type: "string", + label: "Manufacturer", + description: "Manufacturer of the gage", + optional: true, + async options() { + const { data } = await this.listManufacturers(); + return data?.map(({ Name: name }) => name) || []; + }, + }, + status: { + type: "string", + label: "Status", + description: "Status of the gage", + options: constants.STATUS, + }, + lastCalibrationDate: { + type: "string", + label: "Last Calibration Date", + description: "The last calibration date of the gage in ISO-8601 format. Example `2024-03-15T02:27:16Z`", + optional: true, + }, + calibrationDueDate: { + type: "string", + label: "Next Calibration Due", + description: "The next calibration due date of the gage in ISO-8601 format. Example: `2024-03-15T02:27:16Z`", + optional: true, + }, + controlNumber: { + type: "string", + label: "Control Number", + description: "Control number of the gage", + optional: true, + }, + type: { + type: "string", + label: "Type", + description: "Type of the gague. Example: `Flow Meter`", + optional: true, + }, + model: { + type: "string", + label: "Model", + description: "The model of the gage", + optional: true, + }, + condition: { + type: "string", + label: "Condition Acquired", + description: "The condition of the gage", + optional: true, + options: constants.CONDITION, + }, + instructions: { + type: "string", + label: "Instructions", + description: "Calibration instructions for the gage", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://gagelist.net/GageList/api"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + listGages(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/Gage/List", + ...opts, + }); + }, + listManufacturers(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/Manufacturer/List", + ...opts, + }); + }, + listCalibrations(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/Calibration/List", + ...opts, + }); + }, + getGage({ + gageId, ...opts + }) { + return this._makeRequest({ + path: `/Gage/Detail/${gageId}`, + ...opts, + }); + }, + createGage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/Gage/Create", + ...opts, + }); + }, + updateGage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/Gage/Update", + ...opts, + }); + }, + async *paginate({ + resourceFn, + params = {}, + }) { + params = { + ...params, + record_number: constants.DEFAULT_LIMIT, + start: 0, + }; + while (true) { + const { data } = await resourceFn({ + params, + }); console.log(data); + if (!data?.length) { + return; + } + for (const item of data) { + yield item; + } + params.start += constants.DEFAULT_LIMIT; + } + }, + }, +}; diff --git a/components/gagelist/package.json b/components/gagelist/package.json new file mode 100644 index 0000000000000..589c56bc1e565 --- /dev/null +++ b/components/gagelist/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/gagelist", + "version": "0.1.0", + "description": "Pipedream GageList Components", + "main": "gagelist.app.mjs", + "keywords": [ + "pipedream", + "gagelist" + ], + "homepage": "https://pipedream.com/apps/gagelist", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/gagelist/sources/common/base.mjs b/components/gagelist/sources/common/base.mjs new file mode 100644 index 0000000000000..b4cac2036ca75 --- /dev/null +++ b/components/gagelist/sources/common/base.mjs @@ -0,0 +1,89 @@ +import gagelist from "../../gagelist.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + gagelist, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + getParams() { + return {}; + }, + generateMeta(item) { + return { + id: item.Id, + summary: this.getSummary(item), + ts: Date.parse(this.getTsField(item)), + }; + }, + async getResources(resourceFn, params) { + const resources = []; + const items = this.gagelist.paginate({ + resourceFn, + params, + }); + for await (const item of items) { + resources.push(item); + } + return resources.reverse(); + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + const resourceFn = this.getResourceFn(); + const params = this.getParams(); + const tsField = this.getTsField(); + + const items = await this.getResources(resourceFn, params); + + let newItems = []; + for (const item of items) { + const ts = Date.parse(item[tsField]); + if (ts > lastTs) { + newItems.push(item); + maxTs = Math.max(ts, maxTs); + } + } + this._setLastTs(maxTs); + + if (max) { + newItems = newItems.slice(0, max); + } + + newItems.forEach((item) => { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }); + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + getTsField() { + throw new Error("getTsField is not implemented"); + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/gagelist/sources/new-calibration/new-calibration.mjs b/components/gagelist/sources/new-calibration/new-calibration.mjs new file mode 100644 index 0000000000000..5d4699e92208b --- /dev/null +++ b/components/gagelist/sources/new-calibration/new-calibration.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "gagelist-new-calibration", + name: "New Calibration Created", + description: "Emit new event when a new calibration is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.gagelist.listCalibrations; + }, + getTsField() { + return "CreatedDate"; + }, + getSummary(item) { + return `New Calibration ID: ${item.Id}`; + }, + }, + sampleEmit, +}; diff --git a/components/gagelist/sources/new-calibration/test-event.mjs b/components/gagelist/sources/new-calibration/test-event.mjs new file mode 100644 index 0000000000000..eb38dd5e36833 --- /dev/null +++ b/components/gagelist/sources/new-calibration/test-event.mjs @@ -0,0 +1,36 @@ +export default { + "Id": 3, + "RecordNumber": "2-1", + "EquipmentRefId": 2, + "MasterStandard": "dgfh", + "Type": "gfhs", + "Manufacturer": "erye", + "Website": "sdfh", + "Model": "sdfh", + "SerialNumber": "dsgds", + "TypesMeasurement": "dsrh", + "UnitOfMeasure": "erth", + "RangeOrSize": "reh", + "Tolerance": "sdfh", + "Location": "stradrhaeing", + "ResponsibleUser": "dfh", + "Interval": "qgr", + "Years": 12, + "Months": 2, + "Days": 4, + "CalibrationInstructions": "qerge", + "CalibrationEnvironment": "fbsdf", + "CalibrationTestMode": "aerh", + "CreatedBy": "API Demo", + "UpdatedBy": "API Demo", + "IsDeleted": false, + "UpdatedDate": "2024-08-15T22:21:16Z", + "CreatedDate": "2024-08-15T17:21:16-05:00", + "ControlNumber": "dsf", + "NISTNumber": "erh", + "AssetNo": "dfsh", + "ConditionAquired": "dfh", + "SourceOrVendor": "adfhad", + "DateAquired": "2024-03-14T00:00:00", + "Area": "arhae" +} \ No newline at end of file diff --git a/components/gagelist/sources/new-gage/new-gage.mjs b/components/gagelist/sources/new-gage/new-gage.mjs new file mode 100644 index 0000000000000..a14ddb614e89c --- /dev/null +++ b/components/gagelist/sources/new-gage/new-gage.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "gagelist-new-gage", + name: "New Gage Created", + description: "Emit new event each time a new gage is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.gagelist.listGages; + }, + getTsField() { + return "CreatedDate"; + }, + getSummary(item) { + return `New Gage ID: ${item.Id}`; + }, + }, + sampleEmit, +}; diff --git a/components/gagelist/sources/new-gage/test-event.mjs b/components/gagelist/sources/new-gage/test-event.mjs new file mode 100644 index 0000000000000..0b98761d7efb0 --- /dev/null +++ b/components/gagelist/sources/new-gage/test-event.mjs @@ -0,0 +1,21 @@ +export default { + "Id": 7, + "Status": "Active", + "LastCalibrationDate": "2024-03-14T00:00:00", + "CalibrationDueDate": "2024-03-14T00:00:00", + "MasterStandard": "No", + "Type": "Caliper", + "SerialNumber": "7", + "Years": 1, + "Months": 2, + "Days": 4, + "CreatedBy": "API Demo", + "UpdatedBy": "API Demo", + "IsDeleted": false, + "ControlNumber": "7", + "UpdatedDate": "2024-03-15T09:03:05Z", + "AssetNo": "7", + "PurchasePrice": 100, + "IntervalCategory": "_", + "CreatedDate": "2024-03-15T04:03:05-05:00" +} \ No newline at end of file diff --git a/components/gagelist/sources/new-manufacturer/new-manufacturer.mjs b/components/gagelist/sources/new-manufacturer/new-manufacturer.mjs new file mode 100644 index 0000000000000..69187a7cabf2c --- /dev/null +++ b/components/gagelist/sources/new-manufacturer/new-manufacturer.mjs @@ -0,0 +1,31 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "gagelist-new-manufacturer", + name: "New Manufacturer Created", + description: "Emit new event when a new manufacturer is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.gagelist.listManufacturers; + }, + getTsField() { + return "UpdatedDate"; + }, + getSummary(item) { + return `New Manufacturer: ${item.Name}`; + }, + async getResources(resourceFn, params) { + const { data } = await resourceFn({ + params, + }); + return data; + }, + }, + sampleEmit, +}; diff --git a/components/gagelist/sources/new-manufacturer/test-event.mjs b/components/gagelist/sources/new-manufacturer/test-event.mjs new file mode 100644 index 0000000000000..797771d8c96fd --- /dev/null +++ b/components/gagelist/sources/new-manufacturer/test-event.mjs @@ -0,0 +1,10 @@ +export default { + "Id": 2, + "Name": "A.G. DAVIS/AA GAGE", + "Address": "6533 Sims Drive, Sterling Heights, MI 48313", + "Phone": "586-977-9000", + "Fax": "586-977-9190", + "Website": "www.agdavis.com", + "IsDeleted": false, + "UpdatedDate": "2020-05-15T00:20:14-05:00" +} \ No newline at end of file diff --git a/components/gainsight_nxt/actions/create-or-update-company/create-or-update-company.mjs b/components/gainsight_nxt/actions/create-or-update-company/create-or-update-company.mjs new file mode 100644 index 0000000000000..9e0f536672c31 --- /dev/null +++ b/components/gainsight_nxt/actions/create-or-update-company/create-or-update-company.mjs @@ -0,0 +1,133 @@ +import { parseObjectEntries } from "../../common/utils.mjs"; +import app from "../../gainsight_nxt.app.mjs"; + +export default { + key: "gainsight_nxt-create-or-update-company", + name: "Create or Update Company", + description: "Create or update a company record. [See the documentation](https://support.gainsight.com/gainsight_nxt/API_and_Developer_Docs/Company_and_Relationship_API/Company_API_Documentation#Parameters)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + type: "string", + label: "Name", + description: "The name of the company. If a company record with this name exists, it will be updated, otherwise a new one will be created.", + }, + industry: { + type: "string", + label: "Industry", + description: "The industry name of the company.", + optional: true, + }, + arr: { + type: "string", + label: "Annual Recurring Revenue (ARR)", + description: "The annual recurring revenue of the company, as a currency value.", + optional: true, + }, + employees: { + type: "integer", + label: "Employees", + description: "The number of employees the company has.", + optional: true, + }, + lifecycleInWeeks: { + type: "integer", + label: "Life Cycle in Weeks", + description: "The number of weeks the entire process goes through.", + optional: true, + }, + originalContractDate: { + type: "string", + label: "Original Contract Date", + description: "The date the engagement with the client started, in `YYYY-MM-DD` format.", + optional: true, + }, + renewalDate: { + type: "string", + label: "Renewal Date", + description: "The upcoming renewal date of the contract, in `YYYY-MM-DD` format.", + optional: true, + }, + stage: { + type: "string", + label: "Stage", + description: "The current stage of the company in the sales pipeline.", + optional: true, + options: [ + "New Customer", + "Kicked Off", + "Launched", + "Adopting", + "Will Churn", + "Churn", + ], + }, + status: { + type: "string", + label: "Status", + description: "The current status of the company.", + optional: true, + options: [ + "Active", + "Inactive", + "Churn", + ], + }, + additionalOptions: { + type: "object", + label: "Additional Options", + description: + "Additional parameters to send in the request. [See the documentation](https://support.gainsight.com/gainsight_nxt/API_and_Developer_Docs/Company_and_Relationship_API/Company_API_Documentation#Parameters) for available parameters. Values will be parsed as JSON where applicable.", + optional: true, + }, + }, + async run({ $ }) { + const data = { + records: [ + { + Name: this.name, + Industry: this.industry, + ARR: this.arr, + Employees: this.employees, + LifecycleInWeeks: this.lifecycleInWeeks, + OriginalContractDate: this.originalContractDate, + RenewalDate: this.renewalDate, + Stage: this.stage, + Status: this.status, + ...(this.additionalOptions && parseObjectEntries(this.additionalOptions)), + }, + ], + }; + + let summary = ""; + let result; + try { + const updateReq = await this.app.updateCompany({ + $, + data, + }); + result = updateReq; + summary = updateReq.result === true + ? `Successfully updated company '${this.name}'` + : `Error updating company '${this.name}'`; + } + catch (err) { + const createReq = await this.app.createCompany({ + $, + data, + }); + result = createReq; + summary = createReq.result === true + ? `Successfully created company '${this.name}'` + : `Error creating company '${this.name}'`; + } + + $.export( + "$summary", + summary, + ); + return result; + }, +}; diff --git a/components/gainsight_nxt/actions/create-or-update-custom-object/create-or-update-custom-object.mjs b/components/gainsight_nxt/actions/create-or-update-custom-object/create-or-update-custom-object.mjs new file mode 100644 index 0000000000000..f097bcae5e394 --- /dev/null +++ b/components/gainsight_nxt/actions/create-or-update-custom-object/create-or-update-custom-object.mjs @@ -0,0 +1,73 @@ +import app from "../../gainsight_nxt.app.mjs"; + +export default { + key: "gainsight_nxt-create-or-update-custom-object", + name: "Create or Update Custom Object", + description: "Create or update a custom object record. [See the documentation](https://support.gainsight.com/gainsight_nxt/API_and_Developer_Docs/Custom_Object_API/Gainsight_Custom_Object_API_Documentation#Insert_API)", + version: "0.0.1", + type: "action", + props: { + app, + objectName: { + propDefinition: [ + app, + "objectName", + ], + }, + infoBox: { + type: "alert", + alertType: "info", + content: "Custom object fields may be suffixed with `__gc`, e.g. if you've named your field \"Object Name\", its key would be `Object_Name__gc`. Check the object configuration in the Gainsight platform for the correct field names.", + }, + fields: { + type: "string[]", + label: "Key Field(s)", + description: "The field(s) which identify this object (max 3), e.g. `fieldName1`. If a record with the same key field(s) exists, it will be updated, otherwise a new one will be created.", + }, + fieldValues: { + type: "object", + label: "Field Values", + description: "The record data to create or update, as key-value pairs.", + }, + }, + async run({ $ }) { + const { objectName } = this; + const data = { + records: [ + this.fieldValues, + ], + }; + + let summary = ""; + let result; + try { + result = await this.app.updateCustomObject({ + $, + objectName, + data, + params: { + keys: this.fields.join?.() ?? this.fields, + }, + }); + summary = result.result === true + ? `Successfully updated custom object ${objectName}` + : `Error updating custom object ${objectName}`; + } + catch (err) { + result = await this.app.createCustomObject({ + $, + objectName, + data, + }); + summary = result.result === true + ? `Successfully created custom object ${objectName}` + : `Error creating custom object ${objectName}`; + } + + $.export( + "$summary", + summary, + ); + return result; + }, +}; diff --git a/components/gainsight_nxt/actions/create-or-update-person/create-or-update-person.mjs b/components/gainsight_nxt/actions/create-or-update-person/create-or-update-person.mjs new file mode 100644 index 0000000000000..64042f8158fb2 --- /dev/null +++ b/components/gainsight_nxt/actions/create-or-update-person/create-or-update-person.mjs @@ -0,0 +1,63 @@ +import app from "../../gainsight_nxt.app.mjs"; + +export default { + key: "gainsight_nxt-create-or-update-person", + name: "Create or Update Person", + description: "Create or update a person's record. [See the documentation](https://support.gainsight.com/gainsight_nxt/API_and_Developer_Docs/Person_API/People_API_Documentation#Person)", + version: "0.0.1", + type: "action", + props: { + app, + email: { + type: "string", + label: "Email", + description: "The email address of the person. If a record with this email exists, it will be updated, otherwise a new one will be created.", + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the person.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the person.", + optional: true, + }, + linkedinUrl: { + type: "string", + label: "LinkedIn URL", + description: "The LinkedIn URL of the person.", + optional: true, + }, + location: { + type: "string", + label: "Location", + description: "The location of the person.", + optional: true, + }, + additionalFields: { + type: "object", + label: "Additional Fields", + description: "Additional fields to include in the request body. [See the documentation](https://support.gainsight.com/gainsight_nxt/API_and_Developer_Docs/Person_API/People_API_Documentation#Person) for all available fields.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.app.createOrUpdatePerson({ + $, + data: { + Email: this.email, + FirstName: this.firstName, + LastName: this.lastName, + LinkedinUrl: this.linkedinUrl, + Location: this.location, + ...this.additionalFields, + }, + }); + + $.export("$summary", `Successfully upserted person with email ${this.email}`); + return response; + }, +}; diff --git a/components/gainsight_nxt/common/utils.mjs b/components/gainsight_nxt/common/utils.mjs new file mode 100644 index 0000000000000..9c3cdbf569744 --- /dev/null +++ b/components/gainsight_nxt/common/utils.mjs @@ -0,0 +1,22 @@ +function optionalParseAsJSON(value) { + try { + return JSON.parse(value); + } catch (e) { + return value; + } +} + +export function parseObjectEntries(value) { + const obj = typeof value === "string" + ? JSON.parse(value) + : value; + return Object.fromEntries( + Object.entries(obj).map(([ + key, + value, + ]) => [ + key, + optionalParseAsJSON(value), + ]), + ); +} diff --git a/components/gainsight_nxt/gainsight_nxt.app.mjs b/components/gainsight_nxt/gainsight_nxt.app.mjs new file mode 100644 index 0000000000000..aef6c222e128a --- /dev/null +++ b/components/gainsight_nxt/gainsight_nxt.app.mjs @@ -0,0 +1,92 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "gainsight_nxt", + propDefinitions: { + objectName: { + type: "string", + label: "Custom Object", + description: "The name of the custom object.", + async options() { + const { data } = await this.listCustomObjects(); + return data?.filter?.((obj) => obj.objectType === "CUSTOM").map(( { + label, objectName, + }) => ({ + label, + value: objectName, + })); + }, + }, + }, + methods: { + _baseUrl() { + return `${this.$auth.customer_domain}/v1`; + }, + async _makeRequest({ + $ = this, + path, + headers = {}, + ...otherOpts + } = {}) { + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + "content-type": "application/json", + "accept": "application/json, text/plain, */*", + "accept-language": "en-GB,en-US;q=0.9,en;q=0.8", + "accesskey": `${this.$auth.access_key}`, + ...headers, + }, + }); + }, + async updateCompany(args) { + return this._makeRequest({ + path: "/data/objects/Company", + method: "PUT", + params: { + keys: "Name", + }, + ...args, + }); + }, + async createCompany(args) { + return this._makeRequest({ + path: "/data/objects/Company", + method: "POST", + ...args, + }); + }, + async createOrUpdatePerson(args) { + return this._makeRequest({ + path: "/peoplemgmt/v1.0/people", + method: "PUT", + ...args, + }); + }, + async listCustomObjects() { + return this._makeRequest({ + path: "/meta/services/objects/list", + }); + }, + async updateCustomObject({ + objectName, ...args + }) { + return this._makeRequest({ + path: `/data/objects/${objectName}`, + method: "PUT", + ...args, + }); + }, + async createCustomObject({ + objectName, ...args + }) { + return this._makeRequest({ + path: `/data/objects/${objectName}`, + method: "POST", + ...args, + }); + }, + }, +}; diff --git a/components/gainsight_nxt/package.json b/components/gainsight_nxt/package.json new file mode 100644 index 0000000000000..567d30336b6b0 --- /dev/null +++ b/components/gainsight_nxt/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/gainsight_nxt", + "version": "0.1.0", + "description": "Pipedream Gainsight Components", + "main": "gainsight_nxt.app.mjs", + "keywords": [ + "pipedream", + "gainsight_nxt" + ], + "homepage": "https://pipedream.com/apps/gainsight_nxt", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/gainsight_px/actions/create-account/create-account.mjs b/components/gainsight_px/actions/create-account/create-account.mjs new file mode 100644 index 0000000000000..02d48e1979306 --- /dev/null +++ b/components/gainsight_px/actions/create-account/create-account.mjs @@ -0,0 +1,68 @@ +import app from "../../gainsight_px.app.mjs"; + +export default { + key: "gainsight_px-create-account", + name: "Create Account", + description: "Create a new account with the given data. [See the documentation](https://gainsightpx.docs.apiary.io/#reference/accounts/v1accounts/create-account)", + version: "0.0.1", + type: "action", + props: { + app, + id: { + propDefinition: [ + app, + "id", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + propertyKeys: { + propDefinition: [ + app, + "propertyKeys", + ], + }, + countryName: { + propDefinition: [ + app, + "countryName", + ], + }, + stateName: { + propDefinition: [ + app, + "stateName", + ], + }, + city: { + propDefinition: [ + app, + "city", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.createAccount({ + $, + data: { + id: this.id, + name: this.name, + propertyKeys: this.propertyKeys, + location: { + countryName: this.countryName, + stateName: this.stateName, + city: this.city, + }, + }, + }); + + $.export("$summary", `Successfully created account with the name '${this.name}'`); + + return response; + }, +}; diff --git a/components/gainsight_px/actions/create-user/create-user.mjs b/components/gainsight_px/actions/create-user/create-user.mjs new file mode 100644 index 0000000000000..1112a0987d22f --- /dev/null +++ b/components/gainsight_px/actions/create-user/create-user.mjs @@ -0,0 +1,68 @@ +import app from "../../gainsight_px.app.mjs"; + +export default { + key: "gainsight_px-create-user", + name: "Create User", + description: "Creates a new user with the given data. [See the documentation](https://gainsightpx.docs.apiary.io/#reference/users/v1users/create-user)", + version: "0.0.1", + type: "action", + props: { + app, + id: { + propDefinition: [ + app, + "id", + ], + label: "Identify ID", + description: "Identifier of the user", + }, + propertyKeys: { + propDefinition: [ + app, + "propertyKeys", + ], + }, + type: { + propDefinition: [ + app, + "type", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + firstName: { + propDefinition: [ + app, + "firstName", + ], + }, + lastName: { + propDefinition: [ + app, + "lastName", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.createUser({ + $, + data: { + identifyId: this.id, + propertyKeys: this.propertyKeys, + type: this.type, + email: this.email, + firstName: this.firstName, + lastName: this.lastName, + }, + }); + + $.export("$summary", `Successfully created user with ID '${this.id}'`); + + return response; + }, +}; diff --git a/components/gainsight_px/actions/delete-user/delete-user.mjs b/components/gainsight_px/actions/delete-user/delete-user.mjs new file mode 100644 index 0000000000000..2e5471c93770f --- /dev/null +++ b/components/gainsight_px/actions/delete-user/delete-user.mjs @@ -0,0 +1,31 @@ +import app from "../../gainsight_px.app.mjs"; + +export default { + key: "gainsight_px-delete-user", + name: "Delete User", + description: "Deletes a user with he specified identifyId. [See the documentation](https://gainsightpx.docs.apiary.io/#reference/users/v1usersdelete/delete-user)", + version: "0.0.1", + type: "action", + props: { + app, + identifyId: { + propDefinition: [ + app, + "identifyId", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.deleteUser({ + $, + data: { + identifyId: this.identifyId, + }, + }); + + $.export("$summary", `Successfully deleted user with ID ${this.identifyId}`); + + return response; + }, +}; diff --git a/components/gainsight_px/common/contants.mjs b/components/gainsight_px/common/contants.mjs new file mode 100644 index 0000000000000..b642cd32c903a --- /dev/null +++ b/components/gainsight_px/common/contants.mjs @@ -0,0 +1,8 @@ +export default { + USER_TYPES: [ + "LEAD", + "USER", + "VISITOR", + "EMPTY_USER_TYPE", + ], +}; diff --git a/components/gainsight_px/gainsight_px.app.mjs b/components/gainsight_px/gainsight_px.app.mjs new file mode 100644 index 0000000000000..ba14f54206449 --- /dev/null +++ b/components/gainsight_px/gainsight_px.app.mjs @@ -0,0 +1,131 @@ +import { axios } from "@pipedream/platform"; +import contants from "./common/contants.mjs"; + +export default { + type: "app", + app: "gainsight_px", + propDefinitions: { + id: { + type: "string", + label: "ID", + description: "Unique identifier for the account", + }, + name: { + type: "string", + label: "Name", + description: "Name associated with the account", + }, + propertyKeys: { + type: "string[]", + label: "Property Keys", + description: "At least one tag key. The key can be found by clicking on `Administration` >`Set Up` > `Products` > Tag Key. For example: AP-xxx-1", + }, + countryName: { + type: "string", + label: "County Name", + description: "Name of the country associated with the account", + optional: true, + }, + stateName: { + type: "string", + label: "State Name", + description: "Name of the State associated with the account", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "City associated with the account", + optional: true, + }, + identifyId: { + type: "string", + label: "Identify ID", + description: "Identifier of the user", + async options() { + const response = await this.listUsers(); + const userIds = response.users; + return userIds.map(({ + identifyId, email, + }) => ({ + label: email, + value: identifyId, + })); + }, + }, + type: { + type: "string", + label: "User Type", + description: "Type of the user", + options: contants.USER_TYPES, + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "Email of the user", + optional: true, + }, + firstName: { + type: "string", + label: "First Name", + description: "First Name of the user", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last Name of the user", + optional: true, + }, + }, + methods: { + _baseUrl() { + return this.$auth.base_endpoint; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "X-APTRINSIC-API-KEY": `${this.$auth.api_key}`, + "Accept": "application/json", + }, + }); + }, + async createAccount(args = {}) { + return this._makeRequest({ + path: "/accounts", + method: "post", + ...args, + }); + }, + async deleteUser(args = {}) { + return this._makeRequest({ + path: "/users/delete", + method: "delete", + ...args, + }); + }, + async createUser(args = {}) { + return this._makeRequest({ + path: "/users", + method: "post", + ...args, + }); + }, + async listUsers(args = {}) { + return this._makeRequest({ + path: "/users", + ...args, + }); + }, + }, +}; diff --git a/components/gainsight_px/package.json b/components/gainsight_px/package.json new file mode 100644 index 0000000000000..0e0afb17bbd18 --- /dev/null +++ b/components/gainsight_px/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/gainsight_px", + "version": "0.1.0", + "description": "Pipedream Gainsight PX Components", + "main": "gainsight_px.app.mjs", + "keywords": [ + "pipedream", + "gainsight_px" + ], + "homepage": "https://pipedream.com/apps/gainsight_px", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/gami5d/actions/record-observation/record-observation.mjs b/components/gami5d/actions/record-observation/record-observation.mjs new file mode 100644 index 0000000000000..da5110fc17548 --- /dev/null +++ b/components/gami5d/actions/record-observation/record-observation.mjs @@ -0,0 +1,58 @@ +import gami5d from "../../gami5d.app.mjs"; + +export default { + key: "gami5d-record-observation", + name: "Record Observation", + description: "Record an observation for evaluation in the gami5d platform. [See the documentation](https://app.gami5d.com/web/api/docs)", + version: "0.0.1", + type: "action", + props: { + gami5d, + playerUniqueRef: { + type: "string", + label: "Player Unique Ref", + description: "The unique reference for the player in your system to identify the player and link this observation. This reference should have been provided when creating the player. Example: `P256310`. If the unique reference is not found, then a new player will be created.", + reloadProps: true, + }, + userId: { + type: "string", + label: "User ID", + description: "Provide an ID which will be marked as the author of this record. Example: `john.doe@myinc.com`", + optional: true, + }, + }, + async additionalProps() { + const props = {}; + const attributes = await this.gami5d.listAttributes(); + for (const attribute of attributes) { + props[attribute.key] = { + type: "string", + label: attribute.key, + description: attribute.label, + }; + } + return props; + }, + async run({ $ }) { + const attributes = await this.gami5d.listAttributes(); + const attributeValues = {}; + for (const attribute of attributes) { + attributeValues[attribute.key] = this[attribute.key]; + } + + const response = await this.gami5d.recordObservation({ + $, + data: { + client_id: this.gami5d.$auth.client_id, + project_id: this.gami5d.$auth.project_id, + player_unique_ref: this.playerUniqueRef, + user_id: this.userId, + attributes: [ + attributeValues, + ], + }, + }); + $.export("$summary", "Successfully recorded observation"); + return response; + }, +}; diff --git a/components/gami5d/gami5d.app.mjs b/components/gami5d/gami5d.app.mjs new file mode 100644 index 0000000000000..b20c61c21fa73 --- /dev/null +++ b/components/gami5d/gami5d.app.mjs @@ -0,0 +1,46 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "gami5d", + methods: { + _baseUrl() { + return "https://app.gami5d.com"; + }, + _getAuth() { + return { + username: `${this.$auth.access_key}`, + password: `${this.$auth.secret_access_key}`, + }; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + auth: this._getAuth(), + }); + }, + listAttributes(opts = {}) { + const { + client_id: clientId, project_id: projectId, + } = this.$auth; + return this._makeRequest({ + path: `/attribute/${clientId}/${projectId}/list/basic`, + ...opts, + }); + }, + recordObservation(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/observation", + ...opts, + }); + }, + }, +}; diff --git a/components/gami5d/package.json b/components/gami5d/package.json new file mode 100644 index 0000000000000..76934c88322b9 --- /dev/null +++ b/components/gami5d/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/gami5d", + "version": "0.1.0", + "description": "Pipedream Gami5d Components", + "main": "gami5d.app.mjs", + "keywords": [ + "pipedream", + "gami5d" + ], + "homepage": "https://pipedream.com/apps/gami5d", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/gan_ai/README.md b/components/gan_ai/README.md new file mode 100644 index 0000000000000..1ed84e29f7ac6 --- /dev/null +++ b/components/gan_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +Gan.AI is a dynamic API that leverages the power of generative adversarial networks (GANs) to create synthetic datasets, simulate user behaviors, and generate realistic images or text. With Gan.AI, developers can enhance their applications by integrating AI-generated content that improves user engagement and simulates various scenarios for testing or training models. Using Pipedream, you can automate workflows involving Gan.AI to process data, trigger AI-based actions, and connect outputs to other apps for extended functionality. + +# Example Use Cases + +- **Synthetic Data Generation for Machine Learning**: Automatically generate and feed synthetic datasets created by Gan.AI into machine learning training models on platforms like TensorFlow or PyTorch. This is particularly useful for scenarios where data privacy is paramount, or original datasets are too small. + +- **User Behavior Simulation for App Testing**: Use Gan.AI to simulate user interactions and behaviors to test application performance and UX. This can be connected to analytics tools like Google Analytics on Pipedream to analyze the impact of these behaviors in real-time. + +- **Dynamic Content Creation for Social Media**: Set up a workflow where Gan.AI generates images or text based on trending topics, which are then automatically posted to social media platforms like Twitter or Instagram. This can help in maintaining active engagement with followers without manual intervention. diff --git a/components/gan_ai/actions/create-videos/create-videos.mjs b/components/gan_ai/actions/create-videos/create-videos.mjs new file mode 100644 index 0000000000000..d424975ac2f59 --- /dev/null +++ b/components/gan_ai/actions/create-videos/create-videos.mjs @@ -0,0 +1,55 @@ +import utils from "../../common/utils.mjs"; +import app from "../../gan_ai.app.mjs"; + +export default { + key: "gan_ai-create-videos", + name: "Create Videos", + description: "Creates videos in bulk by passing tags and values. Requires a project ID. [See the documentation](https://docs.gan.ai/create-video/create-videos)", + version: "0.0.1", + type: "action", + props: { + app, + projectId: { + propDefinition: [ + app, + "projectId", + ], + }, + data: { + type: "string[]", + label: "Data", + description: "The payload is a list of dictionaries, where all the parameters required for video generation have to provide a `unique_id` along with the rest of the values you want to set. Eg. `[{ \"names\": \"Manash\", \"unique_id\": \"abc123\" }, { \"names\": \"Manash2\", \"unique_id\": \"cba321\" } ]`", + }, + }, + methods: { + createVideos(args = {}) { + return this.app.post({ + path: "/create_video/bulk", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createVideos, + projectId, + data, + } = this; + + const response = await createVideos({ + $, + params: { + project_id: projectId, + }, + data: utils.parseArray(data), + }); + + if (!response.length) { + $.export("$summary", "No videos were created"); + return response; + } + + $.export("$summary", `Successfully created \`${response.length}\` video(s)`); + return response; + }, +}; diff --git a/components/gan_ai/common/constants.mjs b/components/gan_ai/common/constants.mjs new file mode 100644 index 0000000000000..c55571a30cad8 --- /dev/null +++ b/components/gan_ai/common/constants.mjs @@ -0,0 +1,5 @@ +const BASE_URL = "https://api.gan.ai"; + +export default { + BASE_URL, +}; diff --git a/components/gan_ai/common/utils.mjs b/components/gan_ai/common/utils.mjs new file mode 100644 index 0000000000000..e6ed4fa271055 --- /dev/null +++ b/components/gan_ai/common/utils.mjs @@ -0,0 +1,50 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function isJson(value) { + try { + JSON.parse(value); + } catch (e) { + return false; + } + + return true; +} + +function valueToObject(value) { + if (typeof(value) === "object") { + return value; + } + + if (!isJson(value)) { + throw new ConfigurationError(`Make sure the custom expression contains a valid JSON object: \`${value}\``); + } + + return JSON.parse(value); +} + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +export default { + parseArray: (value) => parseArray(value).map(valueToObject), +}; diff --git a/components/gan_ai/gan_ai.app.mjs b/components/gan_ai/gan_ai.app.mjs new file mode 100644 index 0000000000000..36884357d1b26 --- /dev/null +++ b/components/gan_ai/gan_ai.app.mjs @@ -0,0 +1,54 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "gan_ai", + propDefinitions: { + projectId: { + type: "string", + label: "Project ID", + description: "The unique identifier for the user's project in the GAN AI systems.", + async options() { + const { data } = await this.getProjects(); + return data.map(({ + project_id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + getUrl(path) { + return `https://api.gan.ai${path}`; + }, + getHeaders(headers) { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...otherOpts + } = {}) { + return axios($, { + ...otherOpts, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + getProjects(args = {}) { + return this._makeRequest({ + path: "/projects/v2", + ...args, + }); + }, + }, +}; diff --git a/components/gan_ai/package.json b/components/gan_ai/package.json new file mode 100644 index 0000000000000..55bd81a567e45 --- /dev/null +++ b/components/gan_ai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/gan_ai", + "version": "0.1.0", + "description": "Pipedream Gan.AI Components", + "main": "gan_ai.app.mjs", + "keywords": [ + "pipedream", + "gan_ai" + ], + "homepage": "https://pipedream.com/apps/gan_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "1.6.4" + } +} diff --git a/components/gatekeeper/README.md b/components/gatekeeper/README.md new file mode 100644 index 0000000000000..41d77814b205d --- /dev/null +++ b/components/gatekeeper/README.md @@ -0,0 +1,11 @@ +# Overview + +The Gatekeeper API empowers users to streamline vendor and contract management through its robust suite of features on Pipedream. With this API, you can automate processes like vendor approvals, contract renewals, and compliance checks. By integrating Gatekeeper with other apps on Pipedream, you get the power to create custom, automated workflows that can save time, reduce human error, and ensure timely actions on vendor-related activities. + +# Example Use Cases + +- **Vendor Onboarding Automation**: Automate the vendor onboarding process by using the Gatekeeper API to create new vendor records when a form is submitted through Google Forms. Set up a workflow that triggers upon form submission, creates the vendor in Gatekeeper, and sends a welcome email to the vendor using SendGrid. + +- **Contract Renewal Reminders**: Use the Gatekeeper API to monitor contract end dates and set up a Pipedream workflow that sends Slack notifications to the responsible team members 30 days before a contract is due to expire. This ensures that renewals are handled proactively, without missing any critical deadlines. + +- **Compliance Audit Trails**: Build a workflow that leverages the Gatekeeper API to periodically fetch reports on vendor compliance status. Integrate this with a data visualization tool like Google Data Studio to represent compliance levels across vendors, making it easier to identify areas of risk and take corrective action. diff --git a/components/gatherup/README.md b/components/gatherup/README.md index 6967abe26eb27..8c9f077e2bb62 100644 --- a/components/gatherup/README.md +++ b/components/gatherup/README.md @@ -1,8 +1,11 @@ # Overview -You can use the GatherUp API to build a variety of applications, including: +The GatherUp API facilitates the automation of customer feedback and review management processes. It enables seamless integration with Pipedream's serverless platform for creating workflows that can aggregate reviews, trigger actions based on customer feedback, and enhance reputation management. By leveraging the GatherUp API, businesses can automate the collection and analysis of customer reviews, respond to feedback promptly, and harness insights to improve service quality and customer satisfaction. -- A customer feedback management system -- A customer engagement platform -- A customer loyalty program -- A customer experience management system +# Example Use Cases + +- **Automated Review Collection to Slack**: Set up a Pipedream workflow that automatically retrieves new customer reviews from GatherUp and posts them to a designated Slack channel. This enables real-time notification and collaboration among team members to act on customer feedback swiftly. + +- **Sentiment Analysis with Google Cloud Natural Language API**: Create a workflow where reviews collected via the GatherUp API are analyzed using Google Cloud's Natural Language API for sentiment analysis. The results can be used to categorize feedback into positive, neutral, or negative sentiment, which can then inform customer service and marketing strategies. + +- **Trigger Email Campaigns Based on Feedback with SendGrid**: Implement a Pipedream workflow that triggers personalized email campaigns using SendGrid based on the nature of feedback received. For instance, follow-up emails can be sent to customers who left negative reviews to address their concerns, or thank-you emails to those who left positive feedback, fostering better customer relationships. diff --git a/components/geckoboard/README.md b/components/geckoboard/README.md new file mode 100644 index 0000000000000..fd916fb71c1b1 --- /dev/null +++ b/components/geckoboard/README.md @@ -0,0 +1,11 @@ +# Overview + +The Geckoboard API grants you the power to craft custom dashboards that display key metrics and performance indicators for your business. With this API, you can push data to widgets, create and manage dashboards, and automate the sharing of insightful data visualizations. When leveraged through Pipedream, you can build workflows that connect Geckoboard with various other apps, enabling real-time data updates, integrating with your current tools, and powering automated reporting processes. + +# Example Use Cases + +- **Sales Metrics Dashboard Automation**: Automate the update of a sales metrics dashboard in Geckoboard by triggering a Pipedream workflow with webhook events from your e-commerce platform. Whenever a new sale is processed, the workflow pushes the latest sales data to your Geckoboard widgets, keeping your team informed in real-time. + +- **Customer Support Stats Reporting**: Connect Geckoboard with a customer support app like Zendesk using Pipedream. Set up a workflow that periodically fetches support ticket data and updates a Geckoboard dashboard, providing your team with up-to-date insights into support metrics like average response time and ticket resolution rate. + +- **Social Media Performance Tracking**: Integrate social media platforms with Geckoboard by using Pipedream to poll for new data from apps like Twitter or Facebook. Create a workflow that gathers the latest engagement metrics and automatically populates your Geckoboard social media performance dashboard, giving you a live view of your social media impact. diff --git a/components/gemini_public/README.md b/components/gemini_public/README.md index 7ef34175ca293..6af9cde26b5b9 100644 --- a/components/gemini_public/README.md +++ b/components/gemini_public/README.md @@ -1,9 +1,11 @@ # Overview -The Gemini Public API allows you to build a variety of applications that -interact with the Gemini Exchange. Some examples of what you can build include: +The Gemini Public API provides a gateway to interact with Gemini, a cryptocurrency exchange platform, allowing users to access market data, such as current prices, volume, and order book information. Through Pipedream, developers can harness this data to build powerful, serverless workflows that react to market changes, automate trading analysis, and integrate cryptocurrency data into external applications or services. -- A trading bot that automatically buys or sells based on market conditions -- A monitoring tool that tracks your Gemini account balance and activity -- A wallet service that allows you to send and receive Gemini currency -- An analytics tool that provides insights into Gemini market data +# Example Use Cases + +- **Automated Crypto Market Alerts**: Build a workflow that monitors cryptocurrency prices on Gemini. When a specific asset hits a target price, trigger an alert via email or SMS using integrations like SendGrid or Twilio. This can help users act quickly on price movements. + +- **Portfolio Valuation Scheduler**: Create a daily or hourly scheduled workflow on Pipedream that fetches your portfolio balances from Gemini, calculates the current valuation based on live market data, and logs this information to a Google Sheet for tracking and historical analysis. + +- **Real-Time Trading Dashboard**: Pipe Gemini market data into a real-time dashboard using a service like Geckoboard or Klipfolio. Set up a Pipedream workflow that pushes current price and volume information at regular intervals, to keep the dashboard updated with the latest market conditions. diff --git a/components/gemini_public/package.json b/components/gemini_public/package.json new file mode 100644 index 0000000000000..816dafa7a0c85 --- /dev/null +++ b/components/gemini_public/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/gemini_public", + "version": "0.6.0", + "description": "Pipedream gemini_public Components", + "main": "gemini_public.app.mjs", + "keywords": [ + "pipedream", + "gemini_public" + ], + "homepage": "https://pipedream.com/apps/gemini_public", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/gender_api/README.md b/components/gender_api/README.md index 16bd2fbcc6350..9460ced1e0780 100644 --- a/components/gender_api/README.md +++ b/components/gender_api/README.md @@ -1,14 +1,11 @@ # Overview -The Gender API is a simple, free API that allows you to determine the gender of -a first name. This is perfect for any application or website where you need to -know the gender of a person's name, such as for personalized content, targetted -advertising, or for sorting results. - -Here are some examples of what you can build with the Gender API: - -- A name directory that sorts names by gender -- A website that generates personalized content based on the user's gender -- A social networking site that groups users by gender -- An advertising platform that target ads based on gender -- A sorting tool that sorts names or results by gender +The Gender API on Pipedream allows you to determine the gender of a name programmatically. By leveraging the API, you can enrich user profiles, tailor content, segment audiences, and ensure targeted communication. It's a powerful tool for marketers, HR professionals, and application developers who seek to personalize user experiences or analyze demographic data without manual guesswork. + +# Example Use Cases + +- **Personalized Marketing Campaigns**: Integrate Gender API with an email marketing platform like Mailchimp via Pipedream. Automatically assign genders to subscriber names and segment email lists, enabling tailored content and promotions based on gender. + +- **User Onboarding Customization**: Use the Gender API within a Pipedream workflow to customize the onboarding process of a SaaS application. When a new user signs up, their name is sent to Gender API, and the response is used to personalize their welcome experience with appropriate pronouns and content. + +- **Demographic Data Analysis**: Combine Gender API with Google Sheets on Pipedream. For surveys or form submissions that include a name but no gender field, automatically infer gender and populate the sheet for streamlined demographic analysis. diff --git a/components/genderapi_io/actions/email-to-gender/email-to-gender.mjs b/components/genderapi_io/actions/email-to-gender/email-to-gender.mjs new file mode 100644 index 0000000000000..174e6a0f41db8 --- /dev/null +++ b/components/genderapi_io/actions/email-to-gender/email-to-gender.mjs @@ -0,0 +1,37 @@ +import app from "../../genderapi_io.app.mjs"; + +export default { + key: "genderapi_io-email-to-gender", + name: "Email to Gender", + description: "Send an Email to Gender request to GenderAPI. [See the documentation](https://www.genderapi.io/api-documentation#single-email)", + version: "0.0.1", + type: "action", + props: { + app, + email: { + propDefinition: [ + app, + "email", + ], + }, + country: { + propDefinition: [ + app, + "country", + ], + }, + }, + async run({ $ }) { + const response = await this.app.emailToGender({ + $, + params: { + email: this.email, + country: this.country, + }, + }); + + $.export("$summary", `Successfully sent the request. Result: '${response.gender}'`); + + return response; + }, +}; diff --git a/components/genderapi_io/actions/name-to-gender/name-to-gender.mjs b/components/genderapi_io/actions/name-to-gender/name-to-gender.mjs new file mode 100644 index 0000000000000..43279db79c993 --- /dev/null +++ b/components/genderapi_io/actions/name-to-gender/name-to-gender.mjs @@ -0,0 +1,37 @@ +import app from "../../genderapi_io.app.mjs"; + +export default { + key: "genderapi_io-name-to-gender", + name: "Name to Gender", + description: "Send a Name to Gender request to GenderAPI. [See the documentation](https://www.genderapi.io/api-documentation#single-name)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "name", + ], + }, + country: { + propDefinition: [ + app, + "country", + ], + }, + }, + async run({ $ }) { + const response = await this.app.nameToGender({ + $, + params: { + name: this.name, + country: this.country, + }, + }); + + $.export("$summary", `Successfully sent the request. Result: '${response.gender}'`); + + return response; + }, +}; diff --git a/components/genderapi_io/common/constants.mjs b/components/genderapi_io/common/constants.mjs new file mode 100644 index 0000000000000..53bb1f2f8623b --- /dev/null +++ b/components/genderapi_io/common/constants.mjs @@ -0,0 +1,253 @@ +export default { + COUNTRY_CODES: [ + "CR", + "TG", + "TJ", + "ZA", + "IM", + "PE", + "LC", + "CH", + "RU", + "MP", + "CK", + "SI", + "AU", + "KR", + "IT", + "FI", + "GF", + "SC", + "SX", + "TT", + "TK", + "MY", + "SY", + "MN", + "TF", + "KP", + "AM", + "DZ", + "UY", + "TD", + "DJ", + "BI", + "MK", + "MU", + "LI", + "NU", + "GR", + "GY", + "CG", + "NF", + "ML", + "AX", + "GM", + "SA", + "CX", + "BH", + "NE", + "BN", + "MF", + "CD", + "DK", + "BJ", + "ME", + "SJ", + "BO", + "JO", + "CV", + "VE", + "CI", + "UZ", + "TN", + "IS", + "EH", + "TM", + "GA", + "LS", + "TZ", + "AT", + "LT", + "NP", + "BG", + "IL", + "GU", + "PK", + "PT", + "HR", + "VU", + "PF", + "BM", + "MR", + "GE", + "HU", + "TW", + "MM", + "VG", + "YE", + "SR", + "PN", + "VA", + "PR", + "KW", + "SE", + "GB", + "UM", + "VN", + "CF", + "PA", + "VC", + "JP", + "IR", + "AF", + "LY", + "MZ", + "RO", + "QA", + "CM", + "GG", + "BY", + "SD", + "BQ", + "MO", + "KY", + "AR", + "BR", + "ZW", + "NR", + "NZ", + "AW", + "FJ", + "ID", + "SV", + "CN", + "FM", + "HT", + "CC", + "RW", + "BA", + "TL", + "JM", + "KM", + "KE", + "WS", + "TO", + "PY", + "SH", + "CY", + "GH", + "MA", + "SG", + "LK", + "PH", + "SM", + "WF", + "TR", + "PS", + "BZ", + "CU", + "TV", + "AD", + "SB", + "DM", + "LR", + "OM", + "SO", + "DO", + "AL", + "BL", + "FR", + "GW", + "MS", + "BB", + "CA", + "MG", + "KH", + "LA", + "GP", + "BV", + "HN", + "TH", + "DE", + "LB", + "KZ", + "AS", + "EC", + "NO", + "AO", + "FK", + "ET", + "GS", + "MD", + "AG", + "BE", + "MV", + "SZ", + "CZ", + "CL", + "BT", + "NL", + "EG", + "MQ", + "SN", + "FO", + "EE", + "AQ", + "ST", + "KN", + "BW", + "MH", + "NI", + "PG", + "VI", + "IQ", + "KG", + "US", + "ZM", + "MC", + "GI", + "NC", + "GT", + "BF", + "YT", + "LU", + "UA", + "IE", + "LV", + "GD", + "MW", + "BS", + "AZ", + "SK", + "GQ", + "TC", + "RE", + "IN", + "ES", + "GL", + "KI", + "HK", + "CO", + "SS", + "RS", + "IO", + "NG", + "UG", + "CW", + "SL", + "ER", + "JE", + "AE", + "HM", + "PM", + "BD", + "MT", + "AI", + "GN", + "PW", + "NA", + "MX", + "PL", + ], +}; diff --git a/components/genderapi_io/genderapi_io.app.mjs b/components/genderapi_io/genderapi_io.app.mjs new file mode 100644 index 0000000000000..19e846dcde6bc --- /dev/null +++ b/components/genderapi_io/genderapi_io.app.mjs @@ -0,0 +1,66 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "genderapi_io", + propDefinitions: { + name: { + type: "string", + label: "Name", + description: "Name to be checked", + }, + email: { + type: "string", + label: "Email", + description: "Email to be checked", + }, + country: { + type: "string", + label: "Country", + description: "Country filter", + options: constants.COUNTRY_CODES, + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.genderapi.io/api"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "Content-Type": "application/x-www-form-urlencoded", + }, + params: { + ...params, + key: `${this.$auth.api_key}`, + }, + }); + }, + async nameToGender(args = {}) { + return this._makeRequest({ + method: "post", + path: "/", + ...args, + }); + }, + async emailToGender(args = {}) { + return this._makeRequest({ + method: "post", + path: "/email/", + ...args, + }); + }, + }, +}; diff --git a/components/genderapi_io/package.json b/components/genderapi_io/package.json new file mode 100644 index 0000000000000..05222cf37c523 --- /dev/null +++ b/components/genderapi_io/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/genderapi_io", + "version": "0.1.0", + "description": "Pipedream GenderAPI.io Components", + "main": "genderapi_io.app.mjs", + "keywords": [ + "pipedream", + "genderapi_io" + ], + "homepage": "https://pipedream.com/apps/genderapi_io", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/genderize/README.md b/components/genderize/README.md new file mode 100644 index 0000000000000..e6f322bfb6311 --- /dev/null +++ b/components/genderize/README.md @@ -0,0 +1,11 @@ +# Overview + +The Genderize API lets you predict the gender of a name. With Pipedream, you can integrate this API to enrich and automate data flows that involve personal names. For instance, you can customize marketing messages, analyze demographics, or manage user data more inclusively. The API's simple structure (providing name, gender, probability, and count) makes it immensely useful for applications in CRM systems, marketing tools, and user interface personalizations. + +# Example Use Cases + +- **Automated User Greeting Customization in Emails**: Tailor email marketing campaigns by customizing greetings based on the predicted gender of the recipient’s name. Using Pipedream to connect Genderize with an email platform like SendGrid can automate this process. When a new subscriber is added, deduce their gender, and send a personalized welcome email that resonates more with the recipient. + +- **Enhanced CRM Data by Gender for Targeted Advertising**: Improve CRM databases by appending gender data to user profiles. Integrate Genderize with Salesforce via Pipedream workflows. Automatically fetch names from new or updated CRM entries, determine the gender, and append this information back to each customer profile. This enriched data aids in segmenting contacts for more targeted, gender-specific marketing campaigns. + +- **User Signup Form Enhancement for Gender Prediction**: Streamline user experience on signup forms by using the Genderize API to predict and auto-fill the gender field as a user types their name. This can be particularly useful in applications where gender data improves user interaction but where explicitly asking it might deter signups. Use Pipedream to connect the Genderize API directly to your web form's backend, enhancing it without altering the frontend design. diff --git a/components/genderize/genderize.app.mjs b/components/genderize/genderize.app.mjs new file mode 100644 index 0000000000000..4ae21563beea2 --- /dev/null +++ b/components/genderize/genderize.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "genderize", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/genderize/package.json b/components/genderize/package.json new file mode 100644 index 0000000000000..f860e7b27ff0e --- /dev/null +++ b/components/genderize/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/genderize", + "version": "0.0.1", + "description": "Pipedream Genderize Components", + "main": "genderize.app.mjs", + "keywords": [ + "pipedream", + "genderize" + ], + "homepage": "https://pipedream.com/apps/genderize", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/generated_photos/README.md b/components/generated_photos/README.md index 24d68ce04e5d8..c997e3ccd8376 100644 --- a/components/generated_photos/README.md +++ b/components/generated_photos/README.md @@ -1,8 +1,11 @@ # Overview -The Generated Photos API lets developers generate realistic AI photos of people. This can be useful for sourcing diverse stock photos or building applications that require lifelike images of people. +The Generated Photos API offers a platform to create AI-generated human faces that are realistic and customizable. With this API, you can fine-tune the appearance of these faces based on age, emotion, ethnicity, and more. Integrating the Generated Photos API with Pipedream allows you to automate workflows that require unique human avatars, such as populating dummy user profiles for testing UI/UX design, generating characters for gaming environments, or creating diverse personas for marketing campaigns. -Here are some examples of what you can build with the Generated Photos API: -- Photo and graphic editors -- An application using demo profiles, personas, or avatars -- A social media application that requires lifelike images of people \ No newline at end of file +# Example Use Cases + +- **Dynamic User Profile Generation in Web Development**: Use the Generated Photos API to automate the creation of user avatars for sample databases in web applications. Trigger a workflow on Pipedream that listens for new user sign-ups, then calls the Generated Photos API to generate a profile picture, and finally inserts that image into a database or user management platform like Firebase or Auth0. + +- **Personalized Marketing Campaigns**: Craft personalized marketing materials by integrating the Generated Photos API with email marketing services like SendGrid or Mailchimp. Set up a Pipedream workflow that generates a unique human avatar based on demographic data, then attaches that image to a marketing email template, creating a more engaging experience for each recipient. + +- **Enhanced Gaming Experiences**: Improve the immersion of your game by generating unique non-playable character (NPC) faces. Build a Pipedream workflow that periodically calls the Generated Photos API to produce avatars, then uploads these images to cloud storage like Amazon S3 or Google Cloud Storage to be fetched by the game server and displayed to players in real-time. diff --git a/components/geoapify/README.md b/components/geoapify/README.md new file mode 100644 index 0000000000000..afe581a71d86f --- /dev/null +++ b/components/geoapify/README.md @@ -0,0 +1,11 @@ +# Overview + +The Geoapify API offers a suite of location-based services, including geocoding, routing, and map data. With Pipedream, you can leverage these services to create serverless workflows that react to various triggers, transform and analyze location data, and integrate with other APIs or services. Whether you're building apps that require address lookup, route optimization, or location intelligence, integrating Geoapify with Pipedream enables you to automate and scale these operations efficiently. + +# Example Use Cases + +- **Customer Address Verification**: Automate the process of verifying customer addresses during signup by using Geoapify's Geocoding API. When a new customer registers, trigger a Pipedream workflow that validates the address and updates your CRM or database if the address is valid. + +- **Dynamic Route Planning for Deliveries**: Use Pipedream to react to new orders in your e-commerce platform (like Shopify). The workflow could employ Geoapify's Routing API to calculate optimal delivery routes and send them directly to your delivery drivers' apps or email. + +- **Real Estate Market Analysis**: Create a workflow that triggers on a schedule to fetch property listings from a real estate platform (like Zillow). Use Geoapify's Places API to enrich listings with neighborhood data and generate market analysis reports. diff --git a/components/geoapify/actions/get-ip-location/get-ip-location.mjs b/components/geoapify/actions/get-ip-location/get-ip-location.mjs new file mode 100644 index 0000000000000..3632a88e13948 --- /dev/null +++ b/components/geoapify/actions/get-ip-location/get-ip-location.mjs @@ -0,0 +1,28 @@ +import geoapify from "../../geoapify.app.mjs"; + +export default { + key: "geoapify-get-ip-location", + name: "Get IP Location", + description: "Retrieves geographical coordinates for a given IP address. [See the documentation](https://apidocs.geoapify.com/docs/ip-geolocation/)", + version: "0.0.1", + type: "action", + props: { + geoapify, + ipAddress: { + type: "string", + label: "IP Address", + description: "The IP address to lookup", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.geoapify.geolocateIP({ + $, + params: { + ipAddress: this.ipAddress, + }, + }); + $.export("$summary", `Retrieved location for IP: ${response.ip}`); + return response; + }, +}; diff --git a/components/geoapify/actions/get-route/get-route.mjs b/components/geoapify/actions/get-route/get-route.mjs new file mode 100644 index 0000000000000..d6e06c6c9482b --- /dev/null +++ b/components/geoapify/actions/get-route/get-route.mjs @@ -0,0 +1,70 @@ +import geoapify from "../../geoapify.app.mjs"; + +export default { + key: "geoapify-get-route", + name: "Get Route", + description: "Calculates a route between two sets of latitude and longitude points. [See the documentation](https://apidocs.geoapify.com/docs/routing/)", + version: "0.0.1", + type: "action", + props: { + geoapify, + fromLatitude: { + type: "string", + label: "From Latitude", + description: "The latitude of the starting point", + }, + fromLongitude: { + type: "string", + label: "From Longitude", + description: "The longitude of the starting point", + }, + toLatitude: { + type: "string", + label: "To Latitude", + description: "The latitude of the destination point", + }, + toLongitude: { + type: "string", + label: "To Longitude", + description: "The longitude of the destination point", + }, + mode: { + propDefinition: [ + geoapify, + "mode", + ], + }, + type: { + propDefinition: [ + geoapify, + "routeOptimizationType", + ], + }, + units: { + propDefinition: [ + geoapify, + "units", + ], + }, + format: { + propDefinition: [ + geoapify, + "format", + ], + }, + }, + async run({ $ }) { + const route = await this.geoapify.calculateRoute({ + $, + params: { + waypoints: `${this.fromLatitude},${this.fromLongitude}|${this.toLatitude},${this.toLongitude}`, + mode: this.mode, + type: this.type, + units: this.units, + format: this.format, + }, + }); + $.export("$summary", "Route calculated successfully"); + return route; + }, +}; diff --git a/components/geoapify/actions/search-address/search-address.mjs b/components/geoapify/actions/search-address/search-address.mjs new file mode 100644 index 0000000000000..602868683baa6 --- /dev/null +++ b/components/geoapify/actions/search-address/search-address.mjs @@ -0,0 +1,41 @@ +import geoapify from "../../geoapify.app.mjs"; + +export default { + key: "geoapify-search-address", + name: "Search Address", + description: "Retrieves geocoding information for a given address. [See the documentation](https://apidocs.geoapify.com/docs/geocoding/forward-geocoding/)", + version: "0.0.1", + type: "action", + props: { + geoapify, + address: { + type: "string", + label: "Address", + description: "The address to search. E.g. `44 Montgomery St., San Francisco, CA 94104`", + }, + type: { + propDefinition: [ + geoapify, + "locationType", + ], + }, + format: { + propDefinition: [ + geoapify, + "format", + ], + }, + }, + async run({ $ }) { + const response = await this.geoapify.geocodeAddress({ + $, + params: { + text: this.address, + type: this.type, + format: this.format, + }, + }); + $.export("$summary", `Successfully retrieved geocoding information for address: ${this.address}`); + return response; + }, +}; diff --git a/components/geoapify/common/constants.mjs b/components/geoapify/common/constants.mjs new file mode 100644 index 0000000000000..7a955ff5b0055 --- /dev/null +++ b/components/geoapify/common/constants.mjs @@ -0,0 +1,54 @@ +const LOCATION_TYPES = [ + "country", + "state", + "city", + "postcode", + "street", + "amenity", + "locality", +]; + +const FORMATS = [ + "json", + "xml", + "geojson", +]; + +const TRANSPORTATION_MODES = [ + "drive", + "light_truck", + "medium_truck", + "truck", + "heavy_truck", + "truck_dangerous_goods", + "long_truck", + "bus", + "scooter", + "motorcycle", + "bicycle", + "mountain_bike", + "road_bike", + "walk", + "hike", + "transit", + "approximated_transit", +]; + +const ROUTE_OPTIMIZATION_TYPES = [ + "balanced", + "short", + "less_maneuvers", +]; + +const UNITS = [ + "metric", + "imperial", +]; + +export default { + LOCATION_TYPES, + FORMATS, + TRANSPORTATION_MODES, + ROUTE_OPTIMIZATION_TYPES, + UNITS, +}; diff --git a/components/geoapify/geoapify.app.mjs b/components/geoapify/geoapify.app.mjs new file mode 100644 index 0000000000000..460541e6ffd88 --- /dev/null +++ b/components/geoapify/geoapify.app.mjs @@ -0,0 +1,81 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "geoapify", + propDefinitions: { + locationType: { + type: "string", + label: "Type", + description: "The location type. If set to `locality`, returns administrative areas including postcodes, districts, cities, counties, and states.", + options: constants.LOCATION_TYPES, + optional: true, + }, + format: { + type: "string", + label: "Format", + description: "The format of the response, default value is `geojson`", + options: constants.FORMATS, + optional: true, + }, + mode: { + type: "string", + label: "Mode", + description: "The mode of transportation", + options: constants.TRANSPORTATION_MODES, + }, + routeOptimizationType: { + type: "string", + label: "Type", + description: "The route optimization type", + options: constants.ROUTE_OPTIMIZATION_TYPES, + optional: true, + }, + units: { + type: "string", + label: "Units", + description: "Distance units for the calculated route, the default value is `metric`", + options: constants.UNITS, + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.geoapify.com/v1"; + }, + _makeRequest({ + $ = this, + path, + params, + ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + params: { + ...params, + apiKey: this.$auth.api_key, + }, + }); + }, + geocodeAddress(opts = {}) { + return this._makeRequest({ + path: "/geocode/search", + ...opts, + }); + }, + calculateRoute(opts = {}) { + return this._makeRequest({ + path: "/routing", + ...opts, + }); + }, + geolocateIP(opts = {}) { + return this._makeRequest({ + path: "/ipinfo", + ...opts, + }); + }, + }, +}; diff --git a/components/geoapify/package.json b/components/geoapify/package.json new file mode 100644 index 0000000000000..bade1a758a129 --- /dev/null +++ b/components/geoapify/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/geoapify", + "version": "0.1.0", + "description": "Pipedream Geoapify Components", + "main": "geoapify.app.mjs", + "keywords": [ + "pipedream", + "geoapify" + ], + "homepage": "https://pipedream.com/apps/geoapify", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/geocodio/README.md b/components/geocodio/README.md index 2e114ac810cda..5c530e1783855 100644 --- a/components/geocodio/README.md +++ b/components/geocodio/README.md @@ -1,10 +1,14 @@ # Overview -Geocodio can be used to build a number of different applications and tools, -including: - -- Location-based applications and services -- Geocoding and reverse geocoding -- Mapping and GIS applications -- Address validation and cleaning -- Data analysis and visualization +Geocodio is a tool for geocoding and reverse geocoding addresses into coordinates, or vice versa. It's a powerhouse for location data transformation, crucial for businesses needing to analyze spatial information, visualize location data on maps, or automate location-based notifications. With Pipedream's serverless platform, one can harness Geocodio's API to create dynamic and responsive workflows that react to events in real time, such as updating a map with real-time delivery locations or triggering alerts based on geographical areas. + +# Example Use Cases + +**Real-time Delivery Tracking** +Automatically geocode delivery addresses to GPS coordinates as soon as an order is placed. Integrate Geocodio with a logistics app to update a live map that tracks the delivery vehicle's position for both the operations team and the customer. + +**Event Attendee Mapping** +Geocode the addresses of event attendees to analyze the geographical distribution of participants. Connect Geocodio with data visualization tools to help in event planning, such as selecting the optimal venue location or targeting marketing efforts in areas with high attendee density. + +**Location-based Alerts** +Set up a workflow that combines Geocodio with communication platforms like Twilio or SendGrid. Whenever a person or asset enters a predefined geofenced area, trigger an alert through SMS or email, which could be useful for security monitoring or real-time promotions for customers near a retail store. diff --git a/components/geocodio/package.json b/components/geocodio/package.json new file mode 100644 index 0000000000000..88fb1d0789db9 --- /dev/null +++ b/components/geocodio/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/geocodio", + "version": "0.6.0", + "description": "Pipedream geocodio Components", + "main": "geocodio.app.mjs", + "keywords": [ + "pipedream", + "geocodio" + ], + "homepage": "https://pipedream.com/apps/geocodio", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/geodb_cities/README.md b/components/geodb_cities/README.md new file mode 100644 index 0000000000000..b57fb0ca653d9 --- /dev/null +++ b/components/geodb_cities/README.md @@ -0,0 +1,11 @@ +# Overview + +The GeoDB Cities API lets you tap into a rich dataset of worldwide cities, their attributes, and related data. On Pipedream, you can use this API to create workflows that automate location-based tasks, enrich data with geographical context, or power apps with location intelligence. For instance, you could trigger a workflow whenever a new city is added to a database, gather demographic information based on city names, or even integrate with travel platforms to plan itineraries. + +# Example Use Cases + +- **Weather Alert System**: Create a workflow that monitors weather conditions in multiple cities. When certain conditions are met, like the forecast of a storm, trigger notifications to users in the affected cities through an email or SMS service like SendGrid or Twilio. + +- **Local Event Finder**: Build a workflow that fetches events from a service like Eventbrite based on the city data retrieved from GeoDB Cities. You can filter events by categories or keywords and send a curated list of local events to subscribers via a messaging platform like Slack. + +- **Travel Itinerary Planner**: Use the GeoDB Cities API to offer suggestions for travel destinations. Combine this with the Skyscanner Flight Search API to find the best flight options and craft a complete itinerary. Notify the user with options through a service like Telegram or save the plan to a Google Sheet. diff --git a/components/geokeo/README.md b/components/geokeo/README.md new file mode 100644 index 0000000000000..631cfd627deb2 --- /dev/null +++ b/components/geokeo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Geokeo API offers geolocation services, enabling users to fetch location data based on IP addresses, perform geocoding, and reverse geocoding. With these capabilities, you can enhance applications by integrating dynamic location-based features, from auto-filling addresses to analyzing geographic data. In Pipedream, this API can be particularly powerful for automating workflows that depend on geographical information, such as customizing user experiences based on location, automating location-triggered alerts, or aggregating geographic data for analytics. + +# Example Use Cases + +- **User Location-Based Content Customization**: Automatically customize content on a user's website visit based on their geographical location. When a user visits your site, use their IP to fetch their location from Geokeo via Pipedream. Depending on their region, dynamically display localized marketing campaigns, currency, or language preferences directly relevant to the user's locale. + +- **Location-Based Alerts**: Set up an automated system that sends alerts or notifications based on the geographical location of an event or user activity. For example, if you operate a service that needs to inform users about weather conditions, use Geokeo to detect the user's location via their IP and integrate with a weather API to send real-time alerts about weather changes in their area. + +- **Geographic Data Aggregation for Analytics**: Collect and analyze geographic data for business intelligence. Use Geokeo to reverse geocode user locations from collected IPs and aggregate this data in a database connected via Pipedream. Analyze patterns, such as most active regions, or integrate with Google Analytics to enhance demographic insights, helping in strategic decision-making and targeted marketing. diff --git a/components/geokeo/actions/forward-geocoding/forward-geocoding.mjs b/components/geokeo/actions/forward-geocoding/forward-geocoding.mjs new file mode 100644 index 0000000000000..f5bd98bcab8a6 --- /dev/null +++ b/components/geokeo/actions/forward-geocoding/forward-geocoding.mjs @@ -0,0 +1,30 @@ +import app from "../../geokeo.app.mjs"; + +export default { + key: "geokeo-forward-geocoding", + name: "Forward Geocoding", + description: "Convert address data into geographic coordinates. [See the documentation](https://geokeo.com/documentation.php#request-forward-geocoding)", + version: "0.0.1", + type: "action", + props: { + app, + q: { + propDefinition: [ + app, + "q", + ], + }, + }, + async run({ $ }) { + const response = await this.app.forwardGeocoding({ + $, + params: { + q: this.q, + }, + }); + + $.export("$summary", `${response.results.length} results found`); + + return response; + }, +}; diff --git a/components/geokeo/actions/reverse-geocoding/reverse-geocoding.mjs b/components/geokeo/actions/reverse-geocoding/reverse-geocoding.mjs new file mode 100644 index 0000000000000..02a725253f37b --- /dev/null +++ b/components/geokeo/actions/reverse-geocoding/reverse-geocoding.mjs @@ -0,0 +1,37 @@ +import app from "../../geokeo.app.mjs"; + +export default { + key: "geokeo-reverse-geocoding", + name: "Reverse geocoding", + description: "Convert geographic coordinates to address data. [See the documentation](https://geokeo.com/documentation.php#request-reverse-geocoding)", + version: "0.0.1", + type: "action", + props: { + app, + lat: { + propDefinition: [ + app, + "lat", + ], + }, + lng: { + propDefinition: [ + app, + "lng", + ], + }, + }, + async run({ $ }) { + const response = await this.app.reverseGeocoding({ + $, + params: { + lat: this.lat, + lng: this.lng, + }, + }); + + $.export("$summary", `${response.results.length} results found`); + + return response; + }, +}; diff --git a/components/geokeo/geokeo.app.mjs b/components/geokeo/geokeo.app.mjs new file mode 100644 index 0000000000000..251b9387ac327 --- /dev/null +++ b/components/geokeo/geokeo.app.mjs @@ -0,0 +1,56 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "geokeo", + propDefinitions: { + q: { + type: "string", + label: "Query", + description: "The query can have up to 50 characters", + }, + lat: { + type: "string", + label: "Latitude", + description: "This is the latitude of the place you want to reverse geocode, i.e. `40.74842`", + }, + lng: { + type: "string", + label: "Longitude", + description: "This is the longitude of the place you want to reverse geocode, i.e. `-73.9856`", + }, + }, + methods: { + _baseUrl() { + return "https://geokeo.com/geocode/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + params: { + ...params, + api: `${this.$auth.api_key}`, + }, + }); + }, + async forwardGeocoding(args = {}) { + return this._makeRequest({ + path: "/search.php", + ...args, + }); + }, + async reverseGeocoding(args = {}) { + return this._makeRequest({ + path: "/reverse.php", + ...args, + }); + }, + }, +}; diff --git a/components/geokeo/package.json b/components/geokeo/package.json new file mode 100644 index 0000000000000..dc79545f9b46f --- /dev/null +++ b/components/geokeo/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/geokeo", + "version": "0.1.0", + "description": "Pipedream Geokeo Components", + "main": "geokeo.app.mjs", + "keywords": [ + "pipedream", + "geokeo" + ], + "homepage": "https://pipedream.com/apps/geokeo", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/getaccept/README.md b/components/getaccept/README.md new file mode 100644 index 0000000000000..cf6cb773229c4 --- /dev/null +++ b/components/getaccept/README.md @@ -0,0 +1,11 @@ +# Overview + +The GetAccept API offers a suite of capabilities for automating and enhancing document workflow and e-signature processes. Within Pipedream's serverless environment, you can leverage the GetAccept API to create documents, send them for e-signature, track their status, and manage recipients. By integrating GetAccept with other apps on Pipedream, you can build powerful workflows to streamline how your business handles agreements, sales proposals, and contracts. + +# Example Use Cases + +- **Automated Document Workflow**: Trigger a workflow in Pipedream when a new lead is added to your CRM like Salesforce. Automatically generate a sales agreement using GetAccept, send it to the new lead for signing, and update the lead's status in your CRM upon completion. + +- **Contract Renewal Reminders**: Set up a scheduled Pipedream workflow that checks for contracts nearing their expiration date. Automatically send a reminder to your customer to renew their contract through GetAccept, and upon acknowledgement, update your internal databases or notify your team via apps like Slack. + +- **Event-Driven Document Updates**: When a document's status changes to signed in GetAccept, trigger a Pipedream workflow that updates related records in your project management tool, e.g., Asana. Then, send a personalized thank you email through SendGrid to the signer, and archive the final signed document in cloud storage like Google Drive. diff --git a/components/getemails/README.md b/components/getemails/README.md new file mode 100644 index 0000000000000..b90d35ed63327 --- /dev/null +++ b/components/getemails/README.md @@ -0,0 +1,11 @@ +# Overview + +The GetEmails API provides a means to identify anonymous traffic on your website and retrieve contact information for remarketing purposes. With this API integrated into Pipedream workflows, you can automate the process of capturing leads, enriching customer profiles, and triggering personalized communication. Pipedream’s serverless platform allows you to connect GetEmails to a vast array of services, enabling seamless data flow between lead capture and your marketing or CRM tools. + +# Example Use Cases + +- **Lead Capture to Email Marketing**: Once you obtain leads via GetEmails, set up a Pipedream workflow to automatically add these contacts to an email marketing platform like Mailchimp. This ensures immediate engagement with newly captured leads through welcome emails or nurturing campaigns. + +- **Sync Leads to CRM**: Integrate GetEmails with a CRM like Salesforce using Pipedream. Automatically create new contacts or update existing records as soon as new email data is available, ensuring your sales team has the latest information for follow-up. + +- **Webhook Triggered Lead Alerts**: Use Pipedream to create a workflow where inbound leads from GetEmails trigger a webhook, sending real-time notifications to Slack or another messaging platform. Stay alert with instant updates whenever a new lead is captured. diff --git a/components/getemails/package.json b/components/getemails/package.json index 8d8a557f400b3..81c82bdf7d0cf 100644 --- a/components/getemails/package.json +++ b/components/getemails/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/getform/README.md b/components/getform/README.md index 9430f9002a652..588af3f4f4534 100644 --- a/components/getform/README.md +++ b/components/getform/README.md @@ -1,12 +1,11 @@ # Overview -Getform allows you to build powerful forms and surveys without code. You can -use Getform to collect leads, orders, feedback, and more. Getform makes it easy -to build forms and surveys that look great and work well. +Getform is a powerful API that allows you to collect form submissions without any server-side code, making it an excellent tool for static sites. By using Pipedream, you can automate workflows based on form submissions, such as sending data to databases, triggering email notifications, or integrating with various other services for extended functionality. The API facilitates seamless integration of forms into your development process, while Pipedream acts as the engine to connect and automate actions based on the incoming data. -Here are some examples of what you can build with Getform: +# Example Use Cases -- A contact form for your website -- A survey to collect customer feedback -- An order form for your ecommerce store -- A lead capture form for your sales team +- **Automated Email Responses**: Trigger an email to the submitter using SendGrid whenever a new form submission is received on Getform. This workflow can enhance user engagement by promptly acknowledging their inquiries or sign-ups. + +- **CRM Integration**: On a new form submission, add the submitter's details to a CRM platform like Salesforce. This workflow effectively captures leads and streamlines the follow-up process for sales or customer service teams. + +- **Google Sheets Logging**: Append new form submissions to a Google Sheets document. This allows for easy tracking and analysis of submissions over time, without manual data entry. diff --git a/components/getprospect/README.md b/components/getprospect/README.md index 320f1d279482a..97ad48a0bdd67 100644 --- a/components/getprospect/README.md +++ b/components/getprospect/README.md @@ -1,12 +1,11 @@ # Overview -GetProspect is an API that allows you to generate leads and collect data from -multiple sources. You can use GetProspect to build a variety of applications, -including: - -- A lead capture form that collects data from prospects -- A CRM that integrates with GetProspect to track and manage your leads -- A marketing automation tool that uses GetProspect to generate leads and track - their progress -- A Salesforce integration that allows you to transfer data from GetProspect to - your CRM +GetProspect offers a powerful API to automate the process of finding and verifying professional email addresses. Harnessing this capability within Pipedream, you can create streamlined workflows that not only gather leads but also integrate them into your marketing or sales pipelines. Imagine enhancing your CRM with fresh leads, syncing contact info across tools, or setting up alerts when certain lead criteria are met—all of this can be orchestrated within Pipedream's serverless platform. + +# Example Use Cases + +- **Lead Enrichment for CRM**: When a new lead is added to your CRM (e.g., Salesforce), trigger a workflow that uses the GetProspect API to find additional contact information. Once retrieved, update the lead's record in Salesforce with enriched data, ensuring your sales team has the most current information at their fingertips. + +- **Automated Lead Qualification**: Set up a Pipedream workflow that listens for new sign-ups on your website. For each sign-up, use GetProspect to obtain their professional email and verify its validity. Then, based on the lead's company size or industry (fetched via GetProspect), use conditional logic to route qualified leads to a Slack channel for immediate sales engagement. + +- **Periodic Lead Synchronization**: Build a workflow that periodically fetches new contacts from a Google Sheet or another source. With GetProspect API, validate and enrich these contacts, then sync them to your email marketing platform (like Mailchimp) for seamless campaign targeting. This ensures your mailing lists are always current and populated with verified emails. diff --git a/components/getresponse/README.md b/components/getresponse/README.md index 0967dc414e4c8..c8d2b46d3fb70 100644 --- a/components/getresponse/README.md +++ b/components/getresponse/README.md @@ -1,6 +1,11 @@ # Overview -The GetResponse API allows developers to access and manage GetResponse account -data and campaign statistics. Using the API, you can create and manage -campaigns, subscribers, and segmentations. You can also access campaign -analytics and deliver emails to subscribers. +GetResponse API integrates email marketing and online campaign management tools into your applications for creating and managing mailing lists, newsletters, automated campaigns, and more. With Pipedream, this functionality expands, allowing you to automate workflows between GetResponse and various apps. Integration can trigger actions like updating contacts, processing event-driven emails, or syncing subscriber lists across platforms. + +# Example Use Cases + +- **Sync New Shopify Customers to GetResponse Contacts**: Whenever a new customer is added in Shopify, trigger a Pipedream workflow to automatically add that customer to a specified GetResponse contact list. This ensures your email marketing targets all recent customers without manual intervention. + +- **Email Campaign Responses to Slack Notifications**: Configure a workflow that listens for specific responses or campaign events from GetResponse, and posts a notification to a designated Slack channel. This allows your marketing team to get real-time feedback and engage quickly with the campaign's performance. + +- **Automated Lead Scoring with Google Sheets**: Set up a Pipedream workflow that receives new subscriber notifications from GetResponse, cross-references with engagement data on Google Sheets to assign lead scores, and updates the contact's custom field in GetResponse with the calculated score. This empowers more tailored and effective follow-up communications. diff --git a/components/getscreenshot/README.md b/components/getscreenshot/README.md index 7c42070b85636..4da699a731cdb 100644 --- a/components/getscreenshot/README.md +++ b/components/getscreenshot/README.md @@ -1,5 +1,11 @@ # Overview -Use the GetScreenshot API to take screenshots of entire websites, web -pages, or specific elements on a page. Or use it to create PDFs of web -pages or specific elements on a page. +The GetScreenshot API is a potent tool for capturing webpage snapshots programmatically. Integrating GetScreenshot with Pipedream opens up a plethora of automation possibilities, from monitoring website updates in real-time to generating visual reports for clients. With Pipedream, you can create workflows that trigger screenshots based on specific events, store or send them to stakeholders, and even analyze webpage changes without manual intervention. + +# Example Use Cases + +- **Automated Website Monitoring**: Trigger daily screenshots of your website's homepage. Use Pipedream to compare these snapshots to detect visual changes or downtime, and automatically alert your team via Slack or email if any significant alterations are found. + +- **Client Reporting Automation**: Generate weekly screenshots of a client's website performance dashboard. With Pipedream, connect GetScreenshot to a service like Dropbox or Google Drive to save these images, and then automatically compile them into a PDF report using a tool like PDF.co API. Email the report to the client with a pre-written summary of weekly performance. + +- **Competitor Analysis Workflow**: Schedule monthly screenshots of competitor websites and product pages. Pipedream can orchestrate a flow where these images are stored in a database, like Airtable, for easy comparison and trend analysis. Notify the marketing team with insights via a tool like Twilio SendGrid to inform strategic decisions. diff --git a/components/getswift/README.md b/components/getswift/README.md new file mode 100644 index 0000000000000..29fa63d28410a --- /dev/null +++ b/components/getswift/README.md @@ -0,0 +1,11 @@ +# Overview + +The Getswift API provides programmatic access to the Getswift delivery platform, enabling you to manage and track deliveries, drivers, and orders. Integrating this API with Pipedream allows you to create automated workflows that can optimize delivery operations, sync data with other business tools, and respond to events in real-time. With Pipedream, you can trigger actions based on Getswift events, process delivery data, and connect to countless other services to extend the capabilities of your logistics operations. + +# Example Use Cases + +- **Automate Order Dispatch**: When a new order is received in your e-commerce platform (like Shopify), a Pipedream workflow can automatically create a delivery task in Getswift. This ensures that the dispatch process begins immediately, without manual intervention. + +- **Sync Delivery Status with a Database**: Track delivery updates in real-time by using a Pipedream workflow that listens for status changes from Getswift and updates a corresponding record in a database like PostgreSQL. This keeps your internal systems in sync and allows for real-time reporting and analytics. + +- **Send Delivery Notifications**: Enhance customer experience by sending SMS or email notifications via Twilio or SendGrid whenever the status of a delivery changes. A Pipedream workflow can monitor Getswift delivery status updates and trigger these notifications, keeping customers informed every step of the way. diff --git a/components/ghost_org_admin_api/README.md b/components/ghost_org_admin_api/README.md index 52b7a9bfe5780..36865c641b73f 100644 --- a/components/ghost_org_admin_api/README.md +++ b/components/ghost_org_admin_api/README.md @@ -1,9 +1,11 @@ # Overview -With the Ghost.org API, you can build a wide range of applications and -integrations. Here are some examples: +The Ghost.org (Admin API) provides a powerful platform for content management and distribution, allowing developers to programmatically interact with their Ghost site. With this API, you can automate content creation, manage posts, pages, and tags, and dynamically adjust site settings. This enables seamless content workflows, from drafting and scheduling posts to curating featured articles and managing user access. When used with Pipedream, these capabilities expand, enabling integrations with countless other services to create sophisticated, automated content pipelines that can save time and enhance your content strategy. -- A desktop or mobile app for managing your Ghost blog -- A Ghost-powered website or custom CMS -- An integration with another service or platform (such as Zapier or IFTTT) -- A tool or script for automating tasks with Ghost +# Example Use Cases + +- **Content Scheduling and Social Media Distribution**: Automate the scheduling of posts in Ghost and distribute them across social media platforms. When a new post is published on Ghost, trigger a workflow that shares the post link and summary to Twitter, LinkedIn, and Facebook, expanding your reach without additional manual effort. + +- **Automated Content Backup**: Protect your content by setting up a regular backup. Use a scheduled Pipedream workflow to fetch all your posts from Ghost and store them in a cloud service like Google Drive or Dropbox. You can ensure your content is safe and recoverable, even if disasters strike. + +- **User Engagement Insights**: Enhance your engagement by tracking how users interact with your content. Whenever a new comment is added to a post, trigger a workflow that collects these interactions and sends them to a Google Sheets document for analysis. Pair this with email notifications or Slack messages to keep your team informed about user feedback in real time. diff --git a/components/ghost_org_admin_api/actions/create-member/create-member.mjs b/components/ghost_org_admin_api/actions/create-member/create-member.mjs index 8351e6bd2ab06..75118a5e9c5e4 100644 --- a/components/ghost_org_admin_api/actions/create-member/create-member.mjs +++ b/components/ghost_org_admin_api/actions/create-member/create-member.mjs @@ -3,8 +3,8 @@ import app from "../../ghost_org_admin_api.app.mjs"; export default { key: "ghost_org_admin_api-create-member", name: "Create Member", - description: "Create a new member in Ghost. [See the docs here](https://ghost.org/docs/admin-api/#creating-a-member)", - version: "0.0.3", + description: "Create a new member in Ghost. [See the documentation](https://ghost.org/docs/admin-api/#creating-a-member)", + version: "0.0.4", type: "action", props: { app, diff --git a/components/ghost_org_admin_api/actions/create-post/create-post.mjs b/components/ghost_org_admin_api/actions/create-post/create-post.mjs index 9a6c242a9cb30..3fd0aa53810da 100644 --- a/components/ghost_org_admin_api/actions/create-post/create-post.mjs +++ b/components/ghost_org_admin_api/actions/create-post/create-post.mjs @@ -5,7 +5,7 @@ export default { name: "Create post", description: "Create a post. [See the documentation](https://ghost.org/docs/admin-api/#creating-a-post).", type: "action", - version: "0.0.4", + version: "0.0.5", props: { ghostAdminApi, title: { diff --git a/components/ghost_org_admin_api/actions/update-member/update-member.mjs b/components/ghost_org_admin_api/actions/update-member/update-member.mjs index 7fde7dedc9433..9320bc68cc1a3 100644 --- a/components/ghost_org_admin_api/actions/update-member/update-member.mjs +++ b/components/ghost_org_admin_api/actions/update-member/update-member.mjs @@ -1,10 +1,11 @@ import app from "../../ghost_org_admin_api.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; export default { key: "ghost_org_admin_api-update-member", name: "Update Member", - description: "Update a member in Ghost. [See the docs here](https://ghost.org/docs/admin-api/#updating-a-member)", - version: "0.0.3", + description: "Update a member in Ghost. [See the documentation](https://ghost.org/docs/admin-api/#updating-a-member)", + version: "0.0.4", type: "action", props: { app, @@ -34,6 +35,9 @@ export default { }, }, async run({ $ }) { + if (!this.name && !this.note && !this.labels) { + throw new ConfigurationError("Must provide at least one of `name`, `note`, or `labels`"); + } const res = await this.app.updateMember({ $, memberId: this.member, diff --git a/components/ghost_org_admin_api/common/constants.mjs b/components/ghost_org_admin_api/common/constants.mjs index 560dd78fd9a83..4c3ac3ed22d89 100644 --- a/components/ghost_org_admin_api/common/constants.mjs +++ b/components/ghost_org_admin_api/common/constants.mjs @@ -1,4 +1,4 @@ -const VERSION_PATH = "/ghost/api/v3/admin"; +const VERSION_PATH = "/ghost/api/admin"; const DEFAULT_LIMIT = 100; const MAX_RESOURCES = 200; diff --git a/components/ghost_org_admin_api/ghost_org_admin_api.app.mjs b/components/ghost_org_admin_api/ghost_org_admin_api.app.mjs index a96bceda01c56..32448d5050e94 100644 --- a/components/ghost_org_admin_api/ghost_org_admin_api.app.mjs +++ b/components/ghost_org_admin_api/ghost_org_admin_api.app.mjs @@ -51,12 +51,16 @@ export default { }, methods: { getURL(path) { - return `${this.$auth.admin_api_url}${constants.VERSION_PATH}${path}`; + const { admin_api_url: domain } = this.$auth; + return `${domain.includes("https://") + ? "" + : "https://"}${domain}${constants.VERSION_PATH}${path}`; }, getHeader() { const token = this.getToken(); return { - Authorization: `Ghost ${token}`, + "Authorization": `Ghost ${token}`, + "Accept-Version": "v5.0", }; }, getToken() { diff --git a/components/ghost_org_admin_api/package-lock.json b/components/ghost_org_admin_api/package-lock.json index 0c17160f8733c..6938c9bb9a863 100644 --- a/components/ghost_org_admin_api/package-lock.json +++ b/components/ghost_org_admin_api/package-lock.json @@ -1,174 +1,245 @@ { - "name": "@pipedream/ghost_admin", - "version": "0.1.4", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@pipedream/platform": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@pipedream/platform/-/platform-1.6.0.tgz", - "integrity": "sha512-/qP5qEdY+FD0ylBDwfqC+Uz0J9zNWQRKhvQ30M+hAB1VWTcZZkUng+Df+ZMooUqZBT7jnC8t6fk5eHkSSeHukw==", - "requires": { - "axios": "^1.6.5", - "fp-ts": "^2.0.2", - "io-ts": "^2.0.0", - "querystring": "^0.2.1" - } - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "axios": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", - "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", - "requires": { - "follow-redirects": "^1.15.4", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" - }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" - }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "fp-ts": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.2.tgz", - "integrity": "sha512-CkqAjnIKFqvo3sCyoBTqgJvF+bHrSik584S9nhTjtBESLx26cbtVMR/T9a6ApChOcSDAaM3JydDmWDUn4EEXng==" - }, - "io-ts": { - "version": "2.2.21", - "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.21.tgz", - "integrity": "sha512-zz2Z69v9ZIC3mMLYWIeoUcwWD6f+O7yP92FMVVaXEOSZH1jnVBmET/urd/uoarD1WGBY4rCj8TAyMPzsGNzMFQ==" - }, - "jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", - "requires": { - "jws": "^3.2.2", - "lodash": "^4.17.21", - "ms": "^2.1.1", - "semver": "^7.3.8" - } - }, - "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "querystring": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", - "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==" - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "name": "@pipedream/ghost_org_admin_api", + "version": "0.1.5", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@pipedream/ghost_org_admin_api", + "version": "0.1.5", + "dependencies": { + "@pipedream/platform": "^3.0.0", + "jsonwebtoken": "^9.0.0" + } + }, + "node_modules/@pipedream/platform": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@pipedream/platform/-/platform-3.0.0.tgz", + "integrity": "sha512-qlSJBF0Gn5RloFEPr072hSf6pRSU++Zjs0j6X9ZE2SMmfg777qYR4RQ5HkTHEELEyA948xEOqkBt3bFplpfEbw==", + "dependencies": { + "axios": "^1.6.5", + "fp-ts": "^2.0.2", + "io-ts": "^2.0.0", + "querystring": "^0.2.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fp-ts": { + "version": "2.16.8", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.8.tgz", + "integrity": "sha512-nmDtNqmMZkOxu0M5hkrS9YA15/KPkYkILb6Axg9XBAoUoYEtzg+LFmVWqZrl9FNttsW0qIUpx9RCA9INbv+Bxw==" + }, + "node_modules/io-ts": { + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.21.tgz", + "integrity": "sha512-zz2Z69v9ZIC3mMLYWIeoUcwWD6f+O7yP92FMVVaXEOSZH1jnVBmET/urd/uoarD1WGBY4rCj8TAyMPzsGNzMFQ==", + "peerDependencies": { + "fp-ts": "^2.5.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/querystring": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", + "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } + ] + }, + "node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } + } } diff --git a/components/ghost_org_admin_api/package.json b/components/ghost_org_admin_api/package.json index b8a667fdc144e..b5f5d2d230e06 100644 --- a/components/ghost_org_admin_api/package.json +++ b/components/ghost_org_admin_api/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/ghost_org_admin_api", - "version": "0.1.4", + "version": "0.1.5", "description": "Pipedream Ghost_org_admin_api Components", "main": "ghost_org_admin_api.app.mjs", "keywords": [ @@ -10,7 +10,7 @@ "homepage": "https://pipedream.com/apps/ghost_org_admin_api", "author": "Pipedream (https://pipedream.com/)", "dependencies": { - "@pipedream/platform": "^1.0.0", + "@pipedream/platform": "^3.0.0", "jsonwebtoken": "^9.0.0" }, "gitHead": "e12480b94cc03bed4808ebc6b13e7fdb3a1ba535", diff --git a/components/ghost_org_admin_api/sources/member-created/member-created.mjs b/components/ghost_org_admin_api/sources/member-created/member-created.mjs index e49830626a3e6..fb4206b409bfe 100644 --- a/components/ghost_org_admin_api/sources/member-created/member-created.mjs +++ b/components/ghost_org_admin_api/sources/member-created/member-created.mjs @@ -6,7 +6,7 @@ export default { key: "ghost_org_admin_api-member-created", name: "New Member Created (Instant)", description: "Emit new event for each new member added to a site.", - version: "0.0.8", + version: "0.0.9", dedupe: "unique", methods: { ...common.methods, diff --git a/components/ghost_org_admin_api/sources/member-deleted/member-deleted.mjs b/components/ghost_org_admin_api/sources/member-deleted/member-deleted.mjs index 1ee9e5d7d0e03..2139674cbcf47 100644 --- a/components/ghost_org_admin_api/sources/member-deleted/member-deleted.mjs +++ b/components/ghost_org_admin_api/sources/member-deleted/member-deleted.mjs @@ -6,7 +6,7 @@ export default { key: "ghost_org_admin_api-member-deleted", name: "Member Deleted (Instant)", description: "Emit new event each time a member is deleted from a site.", - version: "0.0.8", + version: "0.0.9", dedupe: "unique", methods: { ...common.methods, diff --git a/components/ghost_org_admin_api/sources/member-updated/member-updated.mjs b/components/ghost_org_admin_api/sources/member-updated/member-updated.mjs index 309be5873b7eb..c8bdf49b3a145 100644 --- a/components/ghost_org_admin_api/sources/member-updated/member-updated.mjs +++ b/components/ghost_org_admin_api/sources/member-updated/member-updated.mjs @@ -6,7 +6,7 @@ export default { key: "ghost_org_admin_api-member-updated", name: "Member Updated (Instant)", description: "Emit new event each time a member is updated.", - version: "0.0.8", + version: "0.0.9", methods: { ...common.methods, getEvent() { diff --git a/components/ghost_org_admin_api/sources/new-tag/new-tag.mjs b/components/ghost_org_admin_api/sources/new-tag/new-tag.mjs index 989688dab2e09..d5f4eb5f5e904 100644 --- a/components/ghost_org_admin_api/sources/new-tag/new-tag.mjs +++ b/components/ghost_org_admin_api/sources/new-tag/new-tag.mjs @@ -6,7 +6,7 @@ export default { key: "ghost_org_admin_api-new-tag", name: "Tag Added (Instant)", description: "Emit new event for each new tag created on a site.", - version: "0.0.8", + version: "0.0.9", methods: { ...common.methods, getEvent() { diff --git a/components/ghost_org_admin_api/sources/page-published/page-published.mjs b/components/ghost_org_admin_api/sources/page-published/page-published.mjs index a95f75ab0f4d1..c1ef53441dd63 100644 --- a/components/ghost_org_admin_api/sources/page-published/page-published.mjs +++ b/components/ghost_org_admin_api/sources/page-published/page-published.mjs @@ -6,7 +6,7 @@ export default { key: "ghost_org_admin_api-page-published", name: "Page Published (Instant)", description: "Emit new event for each new page published on a site.", - version: "0.0.8", + version: "0.0.9", methods: { ...common.methods, getEvent() { diff --git a/components/ghost_org_admin_api/sources/post-published/post-published.mjs b/components/ghost_org_admin_api/sources/post-published/post-published.mjs index 9764944b4f218..ac4032a116697 100644 --- a/components/ghost_org_admin_api/sources/post-published/post-published.mjs +++ b/components/ghost_org_admin_api/sources/post-published/post-published.mjs @@ -6,7 +6,7 @@ export default { key: "ghost_org_admin_api-post-published", name: "Post Published (Instant)", description: "Emit new event for each new post published on a site.", - version: "0.0.8", + version: "0.0.9", methods: { ...common.methods, getEvent() { diff --git a/components/ghost_org_content_api/README.md b/components/ghost_org_content_api/README.md index 776f6d62c1958..60126bef75cf6 100644 --- a/components/ghost_org_content_api/README.md +++ b/components/ghost_org_content_api/README.md @@ -1,12 +1,11 @@ # Overview -Ghost.org provides a content API that enables developers to build various types -of applications and tools. Some example applications that can be built using -the Ghost.org API include: - -- A content management system (CMS) for publishing content -- A forum or discussion board -- A blogroll or list of blogs -- A social media aggregator -- A news feed or news aggregator -- An e-commerce store +The Ghost.org (Content API) unlocks the power of content automation and integration for developers and content creators. With this API, you can programmatically access and manipulate your blog's posts, tags, authors, and settings. It's perfect for streamlining content workflows, from syncing with other platforms to analyzing and optimizing your content strategy. + +# Example Use Cases + +- **Automated Content Distribution**: Trigger a workflow in Pipedream to automatically publish new Ghost blog posts to social media platforms like Twitter or LinkedIn. Each time a post is published on Ghost, the Pipedream workflow can post a link to the relevant social media accounts, ensuring your followers are immediately updated with the latest content. + +- **Dynamic Content Analysis**: Set up a Pipedream workflow that fetches new posts from Ghost and runs them through a sentiment analysis API like IBM Watson. This can help gauge the tone and emotion of your content over time. The results could be stored in a Google Sheet for easy tracking and visualization, allowing for data-driven content strategy adjustments. + +- **Newsletter Automation**: Integrate Ghost with an email service provider like SendGrid using Pipedream. Whenever a new post is published on Ghost, the workflow can automatically generate and send a newsletter to your subscribers, keeping them engaged with the latest articles without manual intervention. diff --git a/components/ghost_org_content_api/actions/find-author/find-author.mjs b/components/ghost_org_content_api/actions/find-author/find-author.mjs index 49447054a574f..4b1524dabe371 100644 --- a/components/ghost_org_content_api/actions/find-author/find-author.mjs +++ b/components/ghost_org_content_api/actions/find-author/find-author.mjs @@ -5,9 +5,9 @@ import utils from "../../common/utils.mjs"; export default { key: "ghost_org_content_api-find-author", name: "Find author", - description: "Find an author. [See the docs here](https://ghost.org/docs/content-api/#authors).", + description: "Find an author. [See the documentation](https://ghost.org/docs/content-api/#authors).", type: "action", - version: "0.0.1", + version: "0.0.2", props: { ghostContentApi, name: { diff --git a/components/ghost_org_content_api/common/constants.mjs b/components/ghost_org_content_api/common/constants.mjs index 4d53f77489ba9..b854ed174815b 100644 --- a/components/ghost_org_content_api/common/constants.mjs +++ b/components/ghost_org_content_api/common/constants.mjs @@ -1,4 +1,4 @@ -const VERSION_PATH = "/ghost/api/v3/content"; +const VERSION_PATH = "/ghost/api/content"; const DEFAULT_LIMIT = 100; const MAX_RESOURCES = 200; const LAST_CREATED_AT = "lastCreatedAt"; diff --git a/components/ghost_org_content_api/ghost_org_content_api.app.mjs b/components/ghost_org_content_api/ghost_org_content_api.app.mjs index 5b1ce65b40fac..a4da0ed964c7d 100644 --- a/components/ghost_org_content_api/ghost_org_content_api.app.mjs +++ b/components/ghost_org_content_api/ghost_org_content_api.app.mjs @@ -7,7 +7,10 @@ export default { propDefinitions: {}, methods: { getURL(path) { - return `${this.$auth.admin_domain}${constants.VERSION_PATH}${path}`; + const { admin_domain: domain } = this.$auth; + return `${domain.includes("https://") + ? "" + : "https://"}${domain}${constants.VERSION_PATH}${path}`; }, async makeRequest({ $ = this, path, params, ...args @@ -18,13 +21,16 @@ export default { key: this.$auth.content_api_key, ...params, }, + headers: { + "Accept-Version": "v5.0", + }, ...args, }; return axios($, config); }, async getAuthors(args = {}) { return this.makeRequest({ - path: "/authors", + path: "/authors/", ...args, }); }, diff --git a/components/ghost_org_content_api/package.json b/components/ghost_org_content_api/package.json index e694450427c32..88027a55c4463 100644 --- a/components/ghost_org_content_api/package.json +++ b/components/ghost_org_content_api/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/ghost_org_content_api", - "version": "0.1.3", + "version": "0.1.4", "description": "Pipedream Ghost_org_content_api Components", "main": "ghost_org_content_api.app.mjs", "keywords": [ @@ -9,11 +9,11 @@ ], "homepage": "https://pipedream.com/apps/ghost_org_content_api", "author": "Pipedream (https://pipedream.com/)", - "dependencies": { - "@pipedream/platform": "^1.2.0" - }, "gitHead": "e12480b94cc03bed4808ebc6b13e7fdb3a1ba535", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" } } diff --git a/components/ghost_org_content_api/sources/common/timer-based.mjs b/components/ghost_org_content_api/sources/common/timer-based.mjs index f7464da6bae66..ed85856463cba 100644 --- a/components/ghost_org_content_api/sources/common/timer-based.mjs +++ b/components/ghost_org_content_api/sources/common/timer-based.mjs @@ -9,7 +9,7 @@ export default { timer: { type: "$.interface.timer", label: "Polling schedule", - description: "How often to poll the Feedbin API", + description: "How often to poll the Ghost API", default: { intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, }, diff --git a/components/ghost_org_content_api/sources/new-author/new-author.mjs b/components/ghost_org_content_api/sources/new-author/new-author.mjs index a643caeba2584..55428d682c11e 100644 --- a/components/ghost_org_content_api/sources/new-author/new-author.mjs +++ b/components/ghost_org_content_api/sources/new-author/new-author.mjs @@ -7,7 +7,7 @@ export default { key: "ghost_org_content_api-new-author", name: "New Author", description: "Emit new event for each new author added on a site.", - version: "0.0.3", + version: "0.0.4", dedupe: "unique", methods: { ...common.methods, diff --git a/components/giantcampaign/README.md b/components/giantcampaign/README.md new file mode 100644 index 0000000000000..ea0d27dafa5d3 --- /dev/null +++ b/components/giantcampaign/README.md @@ -0,0 +1,11 @@ +# Overview + +The GiantCampaign API facilitates the automation and integration of email marketing capabilities into various applications and workflows. With this API, you can manage contact lists, design and send email campaigns, track performance metrics, and segment your audience for targeted marketing. Pipedream, with its ability to connect to hundreds of apps, provides a robust platform to create serverless workflows that leverage the capabilities of GiantCampaign. By tapping into event sources and actions, you can seamlessly integrate GiantCampaign with other services to enhance your marketing efforts and streamline processes. + +# Example Use Cases + +- **Automated Contact Syncing**: Sync new subscribers from a Google Sheets spreadsheet to your GiantCampaign contact list. Every time a row is added to the spreadsheet, Pipedream triggers a workflow that automatically adds the contact to a specified list in GiantCampaign. + +- **Dynamic Campaign Triggering**: Trigger an email campaign in GiantCampaign based on customer activity from your e-commerce platform, like Shopify. When a customer completes a purchase, Pipedream can automatically send a follow-up campaign from GiantCampaign, perhaps offering a discount on future purchases or asking for feedback. + +- **Performance-Driven Reactivation**: Re-engage inactive subscribers by connecting GiantCampaign with a CRM like HubSpot. Pipedream can monitor engagement metrics from your email campaigns and tag inactive users in your CRM, triggering a reactivation workflow in GiantCampaign tailored to win back their attention. diff --git a/components/gift_up/README.md b/components/gift_up/README.md index a74463e17a8e0..5b984a1fbe5b3 100644 --- a/components/gift_up/README.md +++ b/components/gift_up/README.md @@ -1,12 +1,11 @@ # Overview -With Gift Up!, you can build a Gifts app that lets users send each other -virtual gifts. The app would need to integrate with the Gift Up! API to send -and receive gifts. +The Gift Up! API provides a powerful platform for managing gift cards and vouchers. Through Pipedream, you can automate gift card creation, distribution, and tracking, making it a breeze to handle bulk operations, integrate with e-commerce platforms, or even trigger personalized marketing campaigns. The API’s capabilities also include redeeming gift cards, updating their balance, and retrieving detailed information for customer support. -Here are some examples of what you could build with the Gift Up! API: +# Example Use Cases -- A gift app that lets users send each other virtual gifts -- A gift shop that sells virtual gifts -- A gift exchange service that lets users trade gifts with each other -- A gift registry that lets users track their gift wishlists +- **Automated Birthday Vouchers**: Set up a workflow that triggers on customer birthdays, pulled from a customer database or CRM like HubSpot. The workflow creates a personalized Gift Up! voucher and sends it via email, making customers feel valued and encouraging repeat business. + +- **E-commerce Integration for Loyalty Rewards**: Connect Gift Up! with Shopify or WooCommerce. When a customer reaches a certain loyalty tier or order milestone, Pipedream workflow triggers to issue a unique Gift Up! discount code, which is automatically emailed to the customer to incentivize future purchases. + +- **Event-Driven Promotional Campaigns**: Create a Pipedream workflow that listens for specific events or social media triggers, such as a hashtag mention on Twitter using the Twitter API. In response, the workflow generates a Gift Up! promo code and sends a congratulatory message to the user, boosting engagement and brand awareness. diff --git a/components/gift_up/package.json b/components/gift_up/package.json new file mode 100644 index 0000000000000..2853459eb2ea0 --- /dev/null +++ b/components/gift_up/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/gift_up", + "version": "0.6.0", + "description": "Pipedream gift_up Components", + "main": "gift_up.app.mjs", + "keywords": [ + "pipedream", + "gift_up" + ], + "homepage": "https://pipedream.com/apps/gift_up", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/gigasheet/README.md b/components/gigasheet/README.md new file mode 100644 index 0000000000000..bc10b6c5395b3 --- /dev/null +++ b/components/gigasheet/README.md @@ -0,0 +1,11 @@ +# Overview + +The Gigasheet API enables users to manipulate large-scale data sheets within the cloud effortlessly. Through Pipedream, you can leverage this functionality to automate data analysis, manipulation, and enrichment workflows. By connecting Gigasheet with other apps, you can streamline processes like data import, transformation, and sharing, making Pipedream an ideal platform to enhance productivity and data handling efficiency. + +# Example Use Cases + +- **Automated Data Enrichment Pipeline**: Pull data from various sources like CRM or databases into Gigasheet using Pipedream's numerous built-in integrations. Process and enrich the data within Gigasheet and then export the enhanced data automatically to other systems or stakeholders. + +- **Incident Report Analysis**: Automatically send logs or incident reports from monitoring tools like Sentry or Datadog to Gigasheet for analysis. Use Pipedream to parse, filter, and visualize data to quickly identify trends or issues, sending summarized insights back to the team via Slack or email. + +- **Marketing Data Aggregation**: Collect marketing performance data from platforms like Google Analytics or Facebook Ads, import it into Gigasheet for cross-platform analysis, and use Pipedream to distribute custom reports to relevant team members or upload them to cloud storage solutions such as Google Drive. diff --git a/components/giphy/README.md b/components/giphy/README.md index 6c77ebe4c75df..54b12a206afe2 100644 --- a/components/giphy/README.md +++ b/components/giphy/README.md @@ -1,9 +1,11 @@ # Overview -Using the Giphy API, you can build applications that: +The Giphy API provides programmatic access to Giphy's vast library of animated GIFs, allowing you to search, share, and explore this rich visual content. Leveraging Pipedream, you can harness the power of Giphy to add engaging visuals to your applications, automate the delivery of GIFs in response to events, or enrich your social media interactions with dynamic imagery. It opens up fun and expressive ways to inject personality into chatbots, forums, or any platform that could use a splash of animated flair. -- Allow users to search for and browse GIFs -- Embed GIFs in articles or blog posts -- Create GIF user interfaces and animations -- Automatically generate GIFs from images or video -- And much more! +# Example Use Cases + +- **Automated Social Media Responses**: Set up a workflow on Pipedream that listens for specific keywords on Twitter using the Twitter app. When a tweet with a certain hashtag appears, trigger the Giphy API to find a related GIF and post it as a response, adding a visual punch to your social media engagement. + +- **Dynamic Email Marketing Content**: Integrate Giphy with an email platform like SendGrid on Pipedream. Automate the inclusion of trending GIFs in your marketing emails by pulling the latest popular GIFs from Giphy and embedding them in your email campaigns, ensuring your content is fresh and eye-catching. + +- **Enhanced Chatbot Interactions**: Use Pipedream to connect the Giphy API with a chat application like Slack. Create a bot that serves up a relevant GIF when certain phrases or commands are entered in the chat. This adds an element of surprise and delight, making interactions with the chatbot more engaging and entertaining. diff --git a/components/gist/README.md b/components/gist/README.md index df1f1a3e67e39..0600b8f0eeeef 100644 --- a/components/gist/README.md +++ b/components/gist/README.md @@ -1,11 +1,11 @@ # Overview -The Gist API lets you create, read, update, and delete gists. With the Gist -API, you can manage your code snippets on GetGist. +The Gist API empowers you to automate interactions with your Gist CRM platform, efficiently managing contacts, tags, and conversations. Tapping into this potential with Pipedream allows you to build seamless automation workflows that sync data between Gist and other services, trigger actions based on customer behavior, and keep your sales and support teams aligned with real-time information. -Here are some examples of what you can build with the Gist API: +# Example Use Cases -- Create new gists -- Read existing gists -- Update existing gists -- Delete existing gists +- **Sync New Subscribers to a Mailing List:** Automatically add new subscribers from Gist to your Mailchimp list. When a user subscribes through a Gist form, trigger a Pipedream workflow that adds their contact info to a targeted Mailchimp audience, ensuring they receive your latest marketing emails. + +- **Create Support Tickets from Conversations:** Turn Gist conversations into support tickets in Zendesk. Set up a workflow in Pipedream that listens for new messages or tagged conversations in Gist, and automatically creates a corresponding ticket in Zendesk with all the necessary details, streamlining your support process. + +- **Update CRM Contacts with Webinar Attendance:** After hosting a webinar via Zoom, use Pipedream to cross-reference attendees with your Gist contacts. Automatically tag contacts who attended the webinar, enriching your CRM with engagement data to inform follow-up campaigns and lead scoring. diff --git a/components/gistly/README.md b/components/gistly/README.md new file mode 100644 index 0000000000000..c1700efad6c86 --- /dev/null +++ b/components/gistly/README.md @@ -0,0 +1,18 @@ +# Overview + +Gistly API allows you to fetch transcripts or subtitles from YouTube videos in various formats. It's a powerful tool for integrating video content into your applications, enabling you to process and analyze video transcripts programmatically. + +- Instantly fetch transcripts from a library of billions of YouTube videos +- Extract accurate and time-stamped video transcripts for content analysis +- High performance and high availability API for bulk requests + +# API Details + +- **Endpoint**: `GET https://api.gist.ly/youtube/transcript` +- **Authorization**: Requires an API key in the Authorization header. +- **Parameters**: + - `url` or `videoId`: Specify the YouTube video. + - `text`: Boolean to return plain text transcript. + - `chunkSize`: Maximum characters per transcript chunk (optional). + +For more details, visit [Gistly's API documentation](https://gist.ly/youtube-transcript-api#doc). diff --git a/components/gistly/actions/get-transcript/get-transcript.mjs b/components/gistly/actions/get-transcript/get-transcript.mjs new file mode 100644 index 0000000000000..ca2e4cd94d067 --- /dev/null +++ b/components/gistly/actions/get-transcript/get-transcript.mjs @@ -0,0 +1,55 @@ +import app from "../../gistly.app.mjs"; + +export default { + key: "gistly-get-transcript", + name: "Get Transcript", + description: + "Fetches transcript/subtitles from a YouTube video using Gistly API.", + version: "0.0.1", + type: "action", + props: { + app, + videoUrl: { + propDefinition: [ + app, + "videoUrl", + ], + optional: true, + }, + videoId: { + propDefinition: [ + app, + "videoId", + ], + optional: true, + }, + text: { + propDefinition: [ + app, + "text", + ], + }, + chunkSize: { + propDefinition: [ + app, + "chunkSize", + ], + }, + }, + async run({ $ }) { + const params = { + url: this.videoUrl, + videoId: this.videoId, + text: this.text, + chunkSize: this.chunkSize, + }; + + const response = await this.app.getTranscript({ + $, + params, + }); + + $.export("$summary", "Successfully fetched the transcript for the video."); + return response; + }, +}; diff --git a/components/gistly/gistly.app.mjs b/components/gistly/gistly.app.mjs new file mode 100644 index 0000000000000..3ebbb97510807 --- /dev/null +++ b/components/gistly/gistly.app.mjs @@ -0,0 +1,56 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "gistly", + propDefinitions: { + videoUrl: { + type: "string", + label: "YouTube Video URL", + description: "The URL of the YouTube video to fetch the transcript from", + }, + videoId: { + type: "string", + label: "YouTube Video ID", + description: "The ID of the YouTube video to fetch the transcript from", + }, + text: { + type: "boolean", + label: "Plain Text", + description: "Return plain text transcript", + default: false, + }, + chunkSize: { + type: "integer", + label: "Chunk Size", + description: "Maximum characters per transcript chunk", + optional: true, + }, + }, + methods: { + _apiKey() { + return this.$auth.api_key; + }, + _apiUrl() { + return "https://api.gist.ly"; + }, + _makeRequest({ + $ = this, path, ...args + }) { + return axios($, { + url: `${this._apiUrl()}${path}`, + ...args, + headers: { + "x-api-key": this._apiKey(), + "Content-Type": "application/json", + }, + }); + }, + getTranscript(args = {}) { + return this._makeRequest({ + path: "/youtube/transcript", + ...args, + }); + }, + }, +}; diff --git a/components/gistly/package.json b/components/gistly/package.json new file mode 100644 index 0000000000000..e0c0e2574b53b --- /dev/null +++ b/components/gistly/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/gistly", + "version": "0.1.0", + "description": "Pipedream Gistly Components", + "main": "gistly.app.mjs", + "keywords": [ + "pipedream", + "gistly" + ], + "homepage": "https://pipedream.com/apps/gistly", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/gitea/README.md b/components/gitea/README.md index 5b237a80bc261..b2568e6294c2b 100644 --- a/components/gitea/README.md +++ b/components/gitea/README.md @@ -1,9 +1,11 @@ # Overview -With the Gitea API, you can: +Gitea is an open-source, self-hosted Git service, similar to GitHub. With the Gitea API, you can automate and enhance your software development practices by creating workflows that trigger actions within Gitea or across other connected services. You can manage repositories, automate issue tracking, synchronize with task management systems, and orchestrate continuous integration and deployment processes. Pipedream serves as an integration platform to connect Gitea with hundreds of other apps, allowing for complex automations that can reduce manual effort and improve efficiency. -- Create and manage repositories -- Create and manage issues -- Create and manage pull requests -- Create and manage comments -- Explore repositories and users +# Example Use Cases + +- **Repository Management Automation**: Automate the creation, deletion, and updating of Gitea repositories. When a new project is started in your project management tool, such as Trello, Pipedream can listen for this event and create a corresponding repository in Gitea, set up default issue labels, and even invite team members. + +- **Issue Synchronization Workflow**: Sync Gitea issues with an external tracking system like Jira. Whenever an issue is reported in Gitea, Pipedream can create a corresponding issue in Jira and keep statuses and comments in sync between the two platforms, ensuring that all team members have visibility into project issues, regardless of the system they prefer. + +- **CI/CD Pipeline Trigger**: Trigger a Continuous Integration/Continuous Deployment (CI/CD) pipeline in a tool like Jenkins whenever code is pushed to a specific branch in Gitea. Using Pipedream to listen for push events, you can automatically initiate build and test sequences, and if successful, deploy the latest code to staging or production environments. diff --git a/components/gitea/package.json b/components/gitea/package.json new file mode 100644 index 0000000000000..dcacc97aebd11 --- /dev/null +++ b/components/gitea/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/gitea", + "version": "0.6.0", + "description": "Pipedream gitea Components", + "main": "gitea.app.mjs", + "keywords": [ + "pipedream", + "gitea" + ], + "homepage": "https://pipedream.com/apps/gitea", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/github/README.md b/components/github/README.md index f1e84c6bdcc9b..e35fe21d8ba4e 100644 --- a/components/github/README.md +++ b/components/github/README.md @@ -1,6 +1,10 @@ +# Overview + +The GitHub API is a powerful gateway to interaction with GitHub's vast web of data and services, offering a suite of endpoints to manipulate and retrieve information on repositories, pull requests, issues, and more. Harnessing this API on Pipedream, you can orchestrate automated workflows that respond to events in real-time, manage repository data, streamline collaborative processes, and connect GitHub with other services for a more integrated development lifecycle. + # Github API Integration Platform -### Connect Github + 1000s of apps, remarkably fast. +### Connect Github to 2,200+ apps, remarkably fast. --- @@ -14,7 +18,7 @@ Click the image below to watch a brief demo on YouTube.


- + Pipedream demo static image

@@ -25,7 +29,7 @@ Click the image below to watch a brief demo on YouTube. - [Event Sources](#event-sources) - Sources trigger workflows. They emit events from services like GitHub, Slack, Airtable, RSS and [more](https://pipedream.com/apps). When you want to run a workflow when an event happens in any third-party app, you're using an event source. -- [Actions](#actions) - Actions are pre-built code steps that you can use in a workflow to perform common operations across Pipedream's 500+ API integrations. For example, you can use actions to send email, add a row to a Google Sheet, [and more](https://pipedream.com/apps). +- [Actions](#actions) - Actions are pre-built code steps that you can use in a workflow to perform common operations across Pipedream's 2000+ API integrations. For example, you can use actions to send email, add a row to a Google Sheet, [and more](https://pipedream.com/apps). - [Custom code](#code) - Most integrations require custom logic. Code is often the best way to express that logic, so Pipedream allows you to run any [Node.js](https://pipedream.com/docs/code/nodejs/), [Python](https://pipedream.com/docs/code/python/), [Golang](https://pipedream.com/docs/code/go/), or [Bash](https://pipedream.com/docs/code/bash/) code. You can import any package from the languages' package managers, connect to any Pipedream connected app, and more. Pipedream is "low-code" in the best way: you can use pre-built components when you're performing common actions, but you can write custom code when you need to. @@ -49,15 +53,30 @@ Workflows are a sequence of linear [steps](https://pipedream.com/docs/workflows/ [Event Sources](https://pipedream.com/docs/sources/) watch for new data from services like GitHub, Slack, Airtable, RSS and [more](https://pipedream.com/apps). When a source finds a new event, it emits it, triggering any linked workflows. -- [Custom Events](https://pipedream.com/new?h=eyJuIjoiQ3VzdG9tIFdlYmhvb2sgRXZlbnRzIHdpdGggdGhlIEdpdEh1YiBBUEkiLCJ2IjoyLCJ0IjpbInNjX3Y0aXh4Sm4iXSwicyI6W10sImMiOnt9fQ) - Build your own event source using one or multiple events ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/custom-events.js)) -- [New Branch](https://pipedream.com/new?h=eyJuIjoiTmV3IEJyYW5jaCAoSW5zdGFudCkgd2l0aCB0aGUgR2l0SHViIEFQSSIsInYiOjIsInQiOlsic2NfRWdpcnJxeSJdLCJzIjpbXSwiYyI6e319) - Triggered when a new branch is created. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/new-branch.js)) -- [New Commit](https://pipedream.com/new?h=eyJuIjoiTmV3IENvbW1pdCB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6WyJzY19BM2lwUkJZIl0sInMiOltdLCJjIjp7fX0) - Triggered when a new commit comment is created. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/new-commit.js)) -- [New Commit Comment](https://pipedream.com/new?h=eyJuIjoiTmV3IENvbW1pdCBDb21tZW50IChJbnN0YW50KSB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6WyJzY19RMmlCQnBZIl0sInMiOltdLCJjIjp7fX0) - Triggered when a new comment on a commit is created. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/new-commit-comment.js)) -- [New Issue](https://pipedream.com/new?h=eyJuIjoiTmV3IElzc3VlIChJbnN0YW50KSB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6WyJzY19uNWkwMFlrIl0sInMiOltdLCJjIjp7fX0) - Triggered when a new issue is created. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/new-issue.js)) -- [New Label](https://pipedream.com/new?h=eyJuIjoiTmV3IExhYmVsIChJbnN0YW50KSB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6WyJzY185OWl2djU0Il0sInMiOltdLCJjIjp7fX0) - Triggered when a new label is created. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/new-label.js)) -- [New Mention](https://pipedream.com/new?h=eyJuIjoiTmV3IE1lbnRpb24gd2l0aCB0aGUgR2l0SHViIEFQSSIsInYiOjIsInQiOlsic2NfTWVpYU5EIl0sInMiOltdLCJjIjp7fX0b) - Triggers when your username is mentioned in a Commit, Comment, Issue or Pull Request. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/new-mention.js)) -- [New Milestone](https://pipedream.com/new?h=eyJuIjoiTmV3IE1pbGVzdG9uZSAoSW5zdGFudCkgd2l0aCB0aGUgR2l0SHViIEFQSSIsInYiOjIsInQiOlsic2Nfb2dpOTlEbyJdLCJzIjpbXSwiYyI6e319) - Triggered when a new milestone is created. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/new-milestone.js)) -- [New Pull Request](https://pipedream.com/new?h=eyJuIjoiTmV3IG9yIFVwZGF0ZWQgUHVsbCBSZXF1ZXN0IChJbnN0YW50KSB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6WyJzY19WUmlnZzMzIl0sInMiOltdLCJjIjp7fX0) - Triggered when a new pull request is created. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/new-pull-request.js)) +- [New Branch](https://pipedream.com/new?h=eyJuIjoiTmV3IEJyYW5jaCB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6WyJzY19hOGlkeDhBIl0sInMiOltdLCJjIjp7fX0) - Triggers an event when a branch is created. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-branch/new-branch.mjs)) +- [New Card in Column (Classic Projects)](https://pipedream.com/new?h=eyJuIjoiTmV3IENhcmQgaW4gQ29sdW1uIChDbGFzc2ljIFByb2plY3RzKSB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6WyJzY181WmlHOWdCIl0sInMiOltdLCJjIjp7fX0) - Triggers an event when a (classic) project card is created or moved to a specific column. For Projects V2 use New Issue with Status trigger. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-card-in-column/new-card-in-column.mjs)) +- [New Collaborator](https://pipedream.com/new?h=eyJuIjoiTmV3IENvbGxhYm9yYXRvciB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6WyJzY182UWlrVlJyIl0sInMiOltdLCJjIjp7fX0) - Triggers an event when a collaborator is added. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-collaborator/new-collaborator.mjs)) +- [New Commit](https://pipedream.com/new?h=eyJuIjoiTmV3IENvbW1pdCB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6WyJzY18zdmliVlo1Il0sInMiOltdLCJjIjp7fX0) - Triggers an event when commits are pushed to a branch. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-commit/new-commit.mjs)) +- [New Commit Comment](https://pipedream.com/new?h=eyJuIjoiTmV3IENvbW1pdCBDb21tZW50IHdpdGggdGhlIEdpdEh1YiBBUEkiLCJ2IjoyLCJ0IjpbInNjX2RvaVdvTTYiXSwicyI6W10sImMiOnt9fQ) - Triggers an event when a commit comment is created. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-commit-comment/new-commit-comment.mjs)) +- [New Discussion](https://pipedream.com/new?h=eyJuIjoiTmV3IERpc2N1c3Npb24gd2l0aCB0aGUgR2l0SHViIEFQSSIsInYiOjIsInQiOlsic2NfeU9pakJNSyJdLCJzIjpbXSwiYyI6e319) - Triggers an event when a discussion is created. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-discussion/new-discussion.mjs)) +- [New Fork](https://pipedream.com/new?h=eyJuIjoiTmV3IEZvcmsgd2l0aCB0aGUgR2l0SHViIEFQSSIsInYiOjIsInQiOlsic2NfamRpWmU0MCJdLCJzIjpbXSwiYyI6e319) - Triggers an event when a repository is forked. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-fork/new-fork.mjs)) +- [New Gist](https://pipedream.com/new?h=eyJuIjoiTmV3IEdpc3Qgd2l0aCB0aGUgR2l0SHViIEFQSSIsInYiOjIsInQiOlsic2NfcE1pdm5ZMiJdLCJzIjpbXSwiYyI6e319) - Triggers an event when new gists are created by the authenticated user. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-gist/new-gist.mjs)) +- [New Issue with Status (Projects V2)](https://pipedream.com/new?h=eyJuIjoiTmV3IElzc3VlIHdpdGggU3RhdHVzIChQcm9qZWN0cyBWMikgd2l0aCB0aGUgR2l0SHViIEFQSSIsInYiOjIsInQiOlsic2NfZ2xpM09vZyJdLCJzIjpbXSwiYyI6e319) - Triggers an event when a project issue is tagged with a specific status. Currently supports Organization Projects only. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-issue-with-status/new-issue-with-status.mjs)) +- [New Label](https://pipedream.com/new?h=eyJuIjoiTmV3IExhYmVsIHdpdGggdGhlIEdpdEh1YiBBUEkiLCJ2IjoyLCJ0IjpbInNjX0I1aXdWZVEiXSwicyI6W10sImMiOnt9fQ) - Triggers an event when a new label is created. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-label/new-label.mjs)) +- [New Mention](https://pipedream.com/new?h=eyJuIjoiTmV3IE1lbnRpb24gd2l0aCB0aGUgR2l0SHViIEFQSSIsInYiOjIsInQiOlsic2NfMUxpS2xCTSJdLCJzIjpbXSwiYyI6e319) - Triggers an event when you are @mentioned in a new commit, comment, issue, or pull request. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-mention/new-mention.mjs)) +- [New Notification](https://pipedream.com/new?h=eyJuIjoiTmV3IE5vdGlmaWNhdGlvbiB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6WyJzY19XR2kwNlkzIl0sInMiOltdLCJjIjp7fX0) - Triggers an event when you receive a new notification. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-notification/new-notification.mjs)) +- [New or Updated Issue](https://pipedream.com/new?h=eyJuIjoiTmV3IG9yIFVwZGF0ZWQgSXNzdWUgd2l0aCB0aGUgR2l0SHViIEFQSSIsInYiOjIsInQiOlsic2NfNVppRzl6UiJdLCJzIjpbXSwiYyI6e319) - Triggers an event when an issue is created or updated. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-or-updated-issue/new-or-updated-issue.mjs)) +- [New or Updated Milestone](https://pipedream.com/new?h=eyJuIjoiTmV3IG9yIFVwZGF0ZWQgTWlsZXN0b25lIHdpdGggdGhlIEdpdEh1YiBBUEkiLCJ2IjoyLCJ0IjpbInNjX2RvaVdvangiXSwicyI6W10sImMiOnt9fQ) - Triggers an event when a milestone is created or updated. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-or-updated-milestone/new-or-updated-milestone.mjs)) +- [New or Updated Pull Request](https://pipedream.com/new?h=eyJuIjoiTmV3IG9yIFVwZGF0ZWQgUHVsbCBSZXF1ZXN0IHdpdGggdGhlIEdpdEh1YiBBUEkiLCJ2IjoyLCJ0IjpbInNjXzN2aWJWd2siXSwicyI6W10sImMiOnt9fQ) - Triggers an event when a pull request is opened or updated. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-or-updated-pull-request/new-or-updated-pull-request.mjs)) +- [New Organization](https://pipedream.com/new?h=eyJuIjoiTmV3IE9yZ2FuaXphdGlvbiB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6WyJzY195T2lqQllWIl0sInMiOltdLCJjIjp7fX0) - Triggers an event when the authenticated user is added to a new organization. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-organization/new-organization.mjs)) +- [New Release](https://pipedream.com/new?h=eyJuIjoiTmV3IHJlbGVhc2Ugd2l0aCB0aGUgR2l0SHViIEFQSSIsInYiOjIsInQiOlsic2NfamRpWmVwbCJdLCJzIjpbXSwiYyI6e319) - Triggers an event when a new release is created. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-release/new-release.mjs)) +- [New Repository](https://pipedream.com/new?h=eyJuIjoiTmV3IHJlbGVhc2Ugd2l0aCB0aGUgR2l0SHViIEFQSSIsInYiOjIsInQiOlsic2NfamRpWmVwbCJdLCJzIjpbXSwiYyI6e319) - Triggers an event when new repositories are created. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-repository/new-repository.mjs)) +- [New Review Request](https://pipedream.com/new?h=eyJuIjoiTmV3IFJldmlldyBSZXF1ZXN0IHdpdGggdGhlIEdpdEh1YiBBUEkiLCJ2IjoyLCJ0IjpbInNjX01laUU2MFEiXSwicyI6W10sImMiOnt9fQ) - Triggers an event when you or a team you're a member of are requested to review a pull request. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-review-request/new-review-request.mjs)) +- [New Security Alert](https://pipedream.com/new?h=eyJuIjoiTmV3IFNlY3VyaXR5IEFsZXJ0IHdpdGggdGhlIEdpdEh1YiBBUEkiLCJ2IjoyLCJ0IjpbInNjX0I1aXdWZTQiXSwicyI6W10sImMiOnt9fQ) - Triggers an event when GitHub discovers a security vulnerability in one of your repositories. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-security-alert/new-security-alert.mjs)) +- [New Star By User](https://pipedream.com/new?h=eyJuIjoiTmV3IFN0YXIgQnkgVXNlciB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6WyJzY18xTGlLbEJkIl0sInMiOltdLCJjIjp7fX0) - Triggers an event when the specified user stars a repository. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-star-by-user/new-star-by-user.mjs)) +- [New Star](https://pipedream.com/new?h=eyJuIjoiTmV3IFN0YXJzIHdpdGggdGhlIEdpdEh1YiBBUEkiLCJ2IjoyLCJ0IjpbInNjX3gwaXB5VlciXSwicyI6W10sImMiOnt9fQ) - Triggers an event when a repository is starred. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-star/new-star.mjs)) +- [New Team](https://pipedream.com/new?h=eyJuIjoiTmV3IFRlYW0gd2l0aCB0aGUgR2l0SHViIEFQSSIsInYiOjIsInQiOlsic2NfRHBpV3ZPSyJdLCJzIjpbXSwiYyI6e319) - Triggers an event when the user is added to a new team. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-team/new-team.mjs)) +- [New Webhook Event (Instant)](https://pipedream.com/new?h=eyJuIjoiTmV3IFdlYmhvb2sgRXZlbnQgKEluc3RhbnQpIHdpdGggdGhlIEdpdEh1YiBBUEkiLCJ2IjoyLCJ0IjpbInNjXzhuaWE1TVkiXSwicyI6W10sImMiOnt9fQ) - Triggers an event for each selected event type. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/webhook-events/webhook-events.mjs)) You can also consume events emitted by sources using [Pipedream's REST API](https://pipedream.com/docs/api/rest/) or a private, real-time [SSE stream](https://pipedream.com/docs/api/sse/). @@ -82,11 +101,24 @@ You can find the code for all pre-built sources in [the `components` directory]( ## Github API Actions ([explore](https://pipedream.com/apps/github)) -[Actions](https://pipedream.com/docs/components/actions/) are pre-built code steps that you can use in a workflow to perform common operations across Pipedream's 500+ API integrations. For example, you can use actions to send email, add a row to a Google Sheet, [and more](https://pipedream.com/apps). - -- [Create Issue](https://pipedream.com/new?h=eyJuIjoiQ3JlYXRlIElzc3VlIHdpdGggdGhlIEdpdEh1YiBBUEkiLCJ2IjoyLCJ0IjpbXSwicyI6W3sia2V5IjoiZ2l0aHViLWNyZWF0ZS1pc3N1ZSJ9XSwiYyI6e319) - Triggers when your username is mentioned in a Commit, Comment, Issue or Pull Request. ([code](https://github.com/PipedreamHQ/pipedream/tree/master/components/github/actions/create-issue)) -- [Get Repository](https://pipedream.com/new?h=eyJuIjoiR2V0IFJlcG8gd2l0aCB0aGUgR2l0SHViIEFQSSIsInYiOjIsInQiOltdLCJzIjpbeyJrZXkiOiJnaXRodWItZ2V0LXJlcG8ifV0sImMiOnt9fQ) - Triggered when a new milestone is created. ([code](https://github.com/PipedreamHQ/pipedream/tree/master/components/github/actions/get-repo)) -- [Search Issues](https://pipedream.com/new?h=eyJuIjoiU2VhcmNoIElzc3VlcyBhbmQgUHVsbCBSZXF1ZXN0cyB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6W10sInMiOlt7ImtleSI6ImdpdGh1Yi1zZWFyY2gtaXNzdWVzLWFuZC1wdWxsLXJlcXVlc3RzIn1dLCJjIjp7fX0) - Triggered when a new pull request is created. ([code](https://github.com/PipedreamHQ/pipedream/tree/master/components/github/actions/search-issues-and-pull-requests)) +[Actions](https://pipedream.com/docs/components/actions/) are pre-built code steps that you can use in a workflow to perform common operations across Pipedream's 2,000+ API integrations. For example, you can use actions to send email, add a row to a Google Sheet, [and more](https://pipedream.com/apps). + +- [Create Issue](https://pipedream.com/new?h=eyJuIjoiQ3JlYXRlIElzc3VlIHdpdGggdGhlIEdpdEh1YiBBUEkiLCJ2IjoyLCJ0IjpbXSwicyI6W3sia2V5IjoiZ2l0aHViLWNyZWF0ZS1pc3N1ZSJ9XSwiYyI6e319) - Create a new issue in a Gihub repo. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/actions/create-issue/create-issue.mjs)) +- [Search Issues and Pull Requests](https://pipedream.com/new?h=eyJuIjoiU2VhcmNoIElzc3VlcyBhbmQgUHVsbCBSZXF1ZXN0cyB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6W10sInMiOlt7ImtleSI6ImdpdGh1Yi1zZWFyY2gtaXNzdWVzLWFuZC1wdWxsLXJlcXVlc3RzIn1dLCJjIjp7fX0) - Find issues and pull requests by state and keyword. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/actions/search-issues-and-pull-requests/search-issues-and-pull-requests.mjs)) +- [Create Branch](https://pipedream.com/new?h=eyJuIjoiQ3JlYXRlIEJyYW5jaCB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6W10sInMiOlt7ImtleSI6ImdpdGh1Yi1jcmVhdGUtYnJhbmNoIn1dLCJjIjp7fX0) - Create a new branch in a Github repo. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/actions/create-branch/create-branch.mjs)) +- [Create Gist](https://pipedream.com/new?h=eyJuIjoiQ3JlYXRlIEdpc3Qgd2l0aCB0aGUgR2l0SHViIEFQSSIsInYiOjIsInQiOltdLCJzIjpbeyJrZXkiOiJnaXRodWItY3JlYXRlLWdpc3QifV0sImMiOnt9fQ) - Allows you to add a new gist with one or more files. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/actions/create-gist/create-gist.mjs)) +- [Create Issue Comment](https://pipedream.com/new?h=eyJuIjoiQ3JlYXRlIElzc3VlIENvbW1lbnQgd2l0aCB0aGUgR2l0SHViIEFQSSIsInYiOjIsInQiOltdLCJzIjpbeyJrZXkiOiJnaXRodWItY3JlYXRlLWlzc3VlLWNvbW1lbnQifV0sImMiOnt9fQ) - Create a new comment in a issue. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/actions/create-issue-comment/create-issue-comment.mjs)) +- [Create or update file contents](https://pipedream.com/new?h=eyJuIjoiQ3JlYXRlIG9yIHVwZGF0ZSBmaWxlIGNvbnRlbnRzIHdpdGggdGhlIEdpdEh1YiBBUEkiLCJ2IjoyLCJ0IjpbXSwicyI6W3sia2V5IjoiZ2l0aHViLWNyZWF0ZS1vci11cGRhdGUtZmlsZS1jb250ZW50cyJ9XSwiYyI6e319) - Create or update a file in a repository. This will replace an existing file. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/actions/create-or-update-file-contents/create-or-update-file-contents.mjs)) +- [Create Pull Request](https://pipedream.com/new?h=eyJuIjoiQ3JlYXRlIFB1bGwgUmVxdWVzdCB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6W10sInMiOlt7ImtleSI6ImdpdGh1Yi1jcmVhdGUtcHVsbC1yZXF1ZXN0In1dLCJjIjp7fX0) - Creates a new pull request for a specified repository. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/actions/create-pull-request/create-pull-request.mjs)) +- [Create Repository](https://pipedream.com/new?h=eyJuIjoiQ3JlYXRlIFJlcG9zaXRvcnkgd2l0aCB0aGUgR2l0SHViIEFQSSIsInYiOjIsInQiOltdLCJzIjpbeyJrZXkiOiJnaXRodWItY3JlYXRlLXJlcG9zaXRvcnkifV0sImMiOnt9fQ) - Creates a new repository for the authenticated user. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/actions/create-repository/create-repository.mjs)) +- [Get Issue Assignees](https://pipedream.com/new?h=eyJuIjoiR2V0IElzc3VlIEFzc2lnbmVlcyB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6W10sInMiOlt7ImtleSI6ImdpdGh1Yi1nZXQtaXNzdWUtYXNzaWduZWVzIn1dLCJjIjp7fX0) - Get assignees for an issue in a Github repo. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/actions/get-issue-assignees/get-issue-assignees.mjs)) +- [Get Repository](https://pipedream.com/new?h=eyJuIjoiR2V0IFJlcG9zaXRvcnkgd2l0aCB0aGUgR2l0SHViIEFQSSIsInYiOjIsInQiOltdLCJzIjpbeyJrZXkiOiJnaXRodWItZ2V0LXJlcG9zaXRvcnkifV0sImMiOnt9fQ) - Get specific repository. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/actions/get-repository/get-repository.mjs)) +- [Get Repository Content](https://pipedream.com/new?h=eyJuIjoiR2V0IFJlcG9zaXRvcnkgQ29udGVudCB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6W10sInMiOlt7ImtleSI6ImdpdGh1Yi1nZXQtcmVwb3NpdG9yeS1jb250ZW50In1dLCJjIjp7fX0) - Get the content of a file or directory in a specific repository. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/actions/get-repository-content/get-repository-content.mjs)) +- [Get Reviewers](https://pipedream.com/new?h=eyJuIjoiR2V0IFJldmlld2VycyB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6W10sInMiOlt7ImtleSI6ImdpdGh1Yi1nZXQtcmV2aWV3ZXJzIn1dLCJjIjp7fX0) - Get reviewers for a PR. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/actions/get-reviewers/get-reviewers.mjs)) +- [List Gists for a User](https://pipedream.com/new?h=eyJuIjoiTGlzdCBHaXN0cyBmb3IgYSBVc2VyIHdpdGggdGhlIEdpdEh1YiBBUEkiLCJ2IjoyLCJ0IjpbXSwicyI6W3sia2V5IjoiZ2l0aHViLWxpc3QtZ2lzdHMtZm9yLWEtdXNlciJ9XSwiYyI6e319) - Lists public gists for the specified user. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/actions/list-gists-for-a-user/list-gists-for-a-user.mjs)) +- [List Releases](https://pipedream.com/new?h=eyJuIjoiTGlzdCBSZWxlYXNlcyB3aXRoIHRoZSBHaXRIdWIgQVBJIiwidiI6MiwidCI6W10sInMiOlt7ImtleSI6ImdpdGh1Yi1saXN0LXJlbGVhc2VzIn1dLCJjIjp7fX0) - List releases for a repository. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/actions/list-releases/list-releases.mjs)) +- [Update Gist](https://pipedream.com/new?h=eyJuIjoiVXBkYXRlIEdpc3Qgd2l0aCB0aGUgR2l0SHViIEFQSSIsInYiOjIsInQiOltdLCJzIjpbeyJrZXkiOiJnaXRodWItdXBkYXRlLWdpc3QifV0sImMiOnt9fQ) - Allows you to update a gist's description and to update, delete, or rename gist files. Files from the previous version of the gist that aren't explicitly changed during an edit are unchanged. At least one of description or files is required. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/actions/update-gist/update-gist.mjs)) +- [Update Issue](https://pipedream.com/new?h=eyJuIjoiVXBkYXRlIElzc3VlIHdpdGggdGhlIEdpdEh1YiBBUEkiLCJ2IjoyLCJ0IjpbXSwicyI6W3sia2V5IjoiZ2l0aHViLXVwZGF0ZS1pc3N1ZSJ9XSwiYyI6e319) - Update a new issue in a Github repo. ([code](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/actions/update-issue/update-issue.mjs)) You can [create your own actions](https://pipedream.com/docs/components/quickstart/nodejs/actions/), which you can re-use across workflows. You can also [publish actions to the entire Pipedream community](https://pipedream.com/docs/components/guidelines/), making them available for anyone to use. @@ -255,3 +287,23 @@ If you'd like to report a suspected vulnerability or security issue, or have any Note: Event Source [New Card in Column](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-card-in-column/new-card-in-column.mjs) only supports legacy (classic) projects. Please [reach out](https://pipedream.com/support/) to the Pipedream team with any technical issues or questions about the Github integration. We're happy to help! + +# Getting Started + +## Github Triggers: Webhooks vs. Polling +The Github triggers in Pipedream enable you to get notified immediately via a webhook if you have `admin` rights on the repo you're watching. Otherwise you can still poll for updates at a regular interval for any other repo where you might not have `admin` rights. + +**Example: New or Updated Issue** +If you are an admin on the repo, this trigger will be configured as a webhook — so any time there is a new or updated issue in the repo, an event will immediately get emitted. + + +If you do not have `admin` rights on the repo you're watching, you can configure the Pipedream trigger to poll for updates on a regular interval. + + +# Example Use Cases + +- **Automated Issue Management Workflow**: Trigger a workflow on Pipedream when new GitHub issues are opened. Automatically label them based on content, assign to the correct team member, or prioritize by sending details to a project management tool like Trello or Jira. + +- **Code Quality Control Workflow**: Upon each push to a repository, use Pipedream to run the code through automated tests and linters, reporting back the status directly in the commit or pull request. Integrate with Slack to notify the development team about the code quality status or any failed checks. + +- **Release Management Workflow**: Automate the process of releasing new versions of software. When a new tag is pushed to GitHub, Pipedream can build the code, run tests, deploy the release to production environments, and notify stakeholders through email or a messaging app like Microsoft Teams. diff --git a/components/github/actions/common/asyncProps.mjs b/components/github/actions/common/asyncProps.mjs new file mode 100644 index 0000000000000..178bf3e5f1734 --- /dev/null +++ b/components/github/actions/common/asyncProps.mjs @@ -0,0 +1,60 @@ +export default { + assignees: { + label: "Assignees", + description: "One or more Users to assign to this issue", + type: "string[]", + optional: true, + options: async () => { + const collaborators = await this.github.getRepositoryCollaborators({ + repoFullname: this.repoFullname, + }); + + return collaborators.map(({ login }) => login); + }, + }, + labels: { + label: "Labels", + description: "The label(s) to add to the issue", + type: "string[]", + optional: true, + options: async () => { + const labels = await this.github.getRepositoryLabels({ + repoFullname: this.repoFullname, + }); + + return labels.map(({ name }) => name); + }, + }, + milestoneNumber: { + type: "integer", + label: "Milestone Number", + description: "The number of a milestone to associate the issue with", + optional: true, + options: async () => { + const items = await this.github.getRepositoryMilestones({ + repoFullname: this.repoFullname, + }); + + return items.map((item) => ({ + label: item.title, + value: +item.number, + })); + }, + }, + pullNumber: { + type: "integer", + label: "Pull Request Number", + description: "The pull request to get reviewers for", + options: async ({ page }) => { + const prs = await this.github.getRepositoryPullRequests({ + page: page + 1, + repoFullname: this.repoFullname, + }); + + return prs.map((pr) => ({ + label: pr.title, + value: +pr.number, + })); + }, + }, +}; diff --git a/components/github/actions/common/constants.mjs b/components/github/actions/common/constants.mjs index b7a28e1828a5f..293413ae9a39c 100644 --- a/components/github/actions/common/constants.mjs +++ b/components/github/actions/common/constants.mjs @@ -6,6 +6,9 @@ const PULL_REQUEST_STATES = [ "PENDING", ]; +const LIMIT = 100; + export default { PULL_REQUEST_STATES, + LIMIT, }; diff --git a/components/github/actions/create-branch/create-branch.mjs b/components/github/actions/create-branch/create-branch.mjs index d9417cc7fce00..771003f8f4a87 100644 --- a/components/github/actions/create-branch/create-branch.mjs +++ b/components/github/actions/create-branch/create-branch.mjs @@ -4,8 +4,8 @@ import github from "../../github.app.mjs"; export default { key: "github-create-branch", name: "Create Branch", - description: "Create a new branch in a Github repo. [See docs here](https://docs.github.com/en/rest/git/refs?apiVersion=2022-11-28#create-a-reference)", - version: "0.0.9", + description: "Create a new branch in a Github repo. [See the documentation](https://docs.github.com/en/rest/git/refs?apiVersion=2022-11-28#create-a-reference)", + version: "0.0.15", type: "action", props: { github, @@ -17,12 +17,12 @@ export default { }, branchName: { label: "Branch Name", - description: "The name of the new branch that will be crated", + description: "The name of the new branch that will be created", type: "string", }, branchSha: { label: "Source Branch", - description: "The source branch that will be used to create the new branch", + description: "The source branch that will be used to create the new branch. Defaults to the repository's default branch (usually `main` or `master`)", propDefinition: [ github, "branch", diff --git a/components/github/actions/create-gist/create-gist.mjs b/components/github/actions/create-gist/create-gist.mjs index 32f1401e5a6b4..b3e402f2550d0 100644 --- a/components/github/actions/create-gist/create-gist.mjs +++ b/components/github/actions/create-gist/create-gist.mjs @@ -4,8 +4,8 @@ import utils from "../../actions/common/utils.mjs"; export default { key: "github-create-gist", name: "Create Gist", - description: "Allows you to add a new gist with one or more files. [See docs here](https://docs.github.com/en/rest/gists/gists?apiVersion=2022-11-28#create-a-gist)", - version: "0.0.4", + description: "Allows you to add a new gist with one or more files. [See the documentation](https://docs.github.com/en/rest/gists/gists?apiVersion=2022-11-28#create-a-gist)", + version: "0.0.9", type: "action", props: { github, diff --git a/components/github/actions/create-issue-comment/create-issue-comment.mjs b/components/github/actions/create-issue-comment/create-issue-comment.mjs index 7fb9b0a4e631d..870569a7aa9f3 100644 --- a/components/github/actions/create-issue-comment/create-issue-comment.mjs +++ b/components/github/actions/create-issue-comment/create-issue-comment.mjs @@ -3,8 +3,8 @@ import github from "../../github.app.mjs"; export default { key: "github-create-issue-comment", name: "Create Issue Comment", - description: "Create a new comment in a issue. [See docs here](https://docs.github.com/en/rest/issues/comments#create-an-issue-comment)", - version: "0.0.15", + description: "Create a new comment in a issue. [See the documentation](https://docs.github.com/en/rest/issues/comments#create-an-issue-comment)", + version: "0.0.20", type: "action", props: { github, diff --git a/components/github/actions/create-issue/create-issue.mjs b/components/github/actions/create-issue/create-issue.mjs index 4399308772200..18ce38e07a10a 100644 --- a/components/github/actions/create-issue/create-issue.mjs +++ b/components/github/actions/create-issue/create-issue.mjs @@ -1,10 +1,12 @@ +import { checkPushPermission } from "../../common/utils.mjs"; import github from "../../github.app.mjs"; +import asyncProps from "../common/asyncProps.mjs"; export default { key: "github-create-issue", name: "Create Issue", - description: "Create a new issue in a Gihub repo. [See docs here](https://docs.github.com/en/rest/issues/issues#create-an-issue)", - version: "0.2.14", + description: "Create a new issue in a Gihub repo. [See the documentation](https://docs.github.com/en/rest/issues/issues#create-an-issue)", + version: "0.3.2", type: "action", props: { github, @@ -13,6 +15,7 @@ export default { github, "repoFullname", ], + reloadProps: true, }, title: { label: "Title", @@ -21,47 +24,41 @@ export default { }, body: { label: "Body", - description: "The contents of the issue", + description: "The text body of the issue", type: "string", optional: true, }, - labels: { - label: "Labels", - description: "Labels to associate with this issue. NOTE: Only users with push access can set labels for new issues", - optional: true, - propDefinition: [ - github, - "labels", - (c) => ({ - repoFullname: c.repoFullname, - }), - ], - }, - assignees: { - label: "Assignees", - description: "Logins for Users to assign to this issue. NOTE: Only users with push access can set assignees for new issues", - optional: true, - propDefinition: [ - github, - "collaborators", - (c) => ({ - repoFullname: c.repoFullname, - }), - ], - }, + }, + methods: { + checkPushPermission, + }, + async additionalProps() { + const canPush = await this.checkPushPermission(); + return canPush + ? { + assignees: asyncProps.assignees, + labels: asyncProps.labels, + milestoneNumber: asyncProps.milestoneNumber, + } + : { + infoBox: { + type: "alert", + alertType: "info", + content: "Labels, assignees and milestones can only be set by users with push access to the repository.", + }, + }; }, async run({ $ }) { - const response = await this.github.createIssue({ - repoFullname: this.repoFullname, - data: { - title: this.title, - body: this.body, - labels: this.labels, - assignees: this.assignees, - }, + const { // eslint-disable-next-line no-unused-vars + github, repoFullname, infoBox, ...data + } = this; + + const response = await github.createIssue({ + repoFullname, + data, }); - $.export("$summary", "Successfully created issue."); + $.export("$summary", `Successfully created issue (ID: ${response.id})`); return response; }, diff --git a/components/github/actions/create-or-update-file-contents/create-or-update-file-contents.mjs b/components/github/actions/create-or-update-file-contents/create-or-update-file-contents.mjs index 6cfa9b26c24fc..8013baacf1616 100644 --- a/components/github/actions/create-or-update-file-contents/create-or-update-file-contents.mjs +++ b/components/github/actions/create-or-update-file-contents/create-or-update-file-contents.mjs @@ -2,9 +2,9 @@ import github from "../../github.app.mjs"; export default { key: "github-create-or-update-file-contents", - name: "Create or update file contents", - description: "Create or update a file in a repository. This will replace an existing file. [See docs here](https://docs.github.com/en/rest/repos/contents#create-or-update-file-contents)", - version: "0.0.11", + name: "Create or Update File Contents", + description: "Create or update a file in a repository. [See the documentation](https://docs.github.com/en/rest/repos/contents#create-or-update-file-contents)", + version: "0.1.2", type: "action", props: { github, @@ -32,21 +32,25 @@ export default { default: "Pipedream - {{steps.trigger.context.workflow_name}} ({{steps.trigger.context.workflow_id}})", }, branch: { - label: "Branch", + propDefinition: [ + github, + "branch", + (c) => ({ + repoFullname: c.repoFullname, + }), + ], description: - "The branch name. Defaults to the repository’s default branch (usually `master`)", - type: "string", + "The branch to use. Defaults to the repository's default branch (usually `main` or `master`)", optional: true, }, }, async run({ $ }) { - - const response = await this.github.createOrUpdateFileContent({ - repoFullname: this.repoFullname, - path: this.path, - commitMessage: this.commitMessage, - fileContent: this.fileContent, - branch: this.branch, + const { + github, branch, ...data + } = this; + const response = await github.createOrUpdateFileContent({ + ...data, + branch: branch && branch.split("/")[1], }); $.export("$summary", `Successfully set contents of ${this.path}${this.branch diff --git a/components/github/actions/create-pull-request/create-pull-request.mjs b/components/github/actions/create-pull-request/create-pull-request.mjs index 988465a3b5828..c6bbbca828f19 100644 --- a/components/github/actions/create-pull-request/create-pull-request.mjs +++ b/components/github/actions/create-pull-request/create-pull-request.mjs @@ -1,15 +1,11 @@ import { ConfigurationError } from "@pipedream/platform"; -import { toSingleLineString } from "../../common/utils.mjs"; import github from "../../github.app.mjs"; export default { key: "github-create-pull-request", name: "Create Pull Request", - description: toSingleLineString(` - Creates a new pull request for a specified repository. - [See docs here](https://docs.github.com/en/rest/pulls/pulls#create-a-pull-request) - `), - version: "0.0.6", + description: "Creates a new pull request for a specified repository. [See the documentation](https://docs.github.com/en/rest/pulls/pulls#create-a-pull-request)", + version: "0.1.2", type: "action", props: { github, @@ -18,20 +14,8 @@ export default { github, "repoFullname", ], - }, - head: { - propDefinition: [ - github, - "branch", - (c) => ({ - repoFullname: c.repoFullname, - }), - ], - label: "Head Branch", - description: toSingleLineString(` - The name of the branch where your changes are implemented. - For cross-repository pull requests in the same network, \`namespace\` head with a user like this: \`username:branch\`. - `), + label: "Base Repository", + description: "The base repository, where the pull request will be created.", }, base: { propDefinition: [ @@ -42,58 +26,48 @@ export default { }), ], label: "Base Branch", - description: toSingleLineString(` - The name of the branch you want the changes pulled into. - This should be an existing branch on the current repository. - You cannot submit a pull request to one repository that requests a merge to a base of another repository. - `), + description: "The base branch, where the changes will be received.", }, - org: { + headRepo: { propDefinition: [ github, - "orgName", + "repoFullname", ], - optional: true, + label: "Head Repository", + description: "The head repository, where the changes originate from. This can, but does not have to, be the same repository.", }, - headRepo: { + head: { propDefinition: [ github, - "repoOrg", + "branch", (c) => ({ - org: c.org, + repoFullname: c.headRepo, }), ], - label: "Head Repository's Name", - description: toSingleLineString(` - The name of the repository where the changes in the pull request were made. - This field is required for cross-repository pull requests if both repositories are owned by the same organization. - `), - optional: true, + label: "Head Branch", + description: "The head branch, where the changes originate from", }, body: { label: "Body", - description: "The contents of the pull request.", + description: "The text description of the pull request.", type: "string", optional: true, }, maintainerCanModify: { - label: "Maintainer Can Modify", + label: "Maintainers Can Modify", description: "Indicates whether [maintainers can modify](https://docs.github.com/articles/allowing-changes-to-a-pull-request-branch-created-from-a-fork/) the pull request.", type: "boolean", optional: true, }, draft: { label: "Is Draft", - description: toSingleLineString(` - Indicates whether the pull request is a draft. - See "[Draft Pull Requests](https://docs.github.com/articles/about-pull-requests#draft-pull-requests)" in the GitHub Help documentation to learn more. - `), + description: "Indicates whether the pull request is a draft. See \"[Draft Pull Requests](https://docs.github.com/articles/about-pull-requests#draft-pull-requests)\" in the GitHub Help documentation to learn more.", type: "boolean", optional: true, }, title: { label: "Title", - description: "The title of the new pull request.", + description: "The title of the pull request.", type: "string", optional: true, }, @@ -106,10 +80,7 @@ export default { }), ], label: "Issue", - description: toSingleLineString(` - An issue in the repository to convert to a pull request. - The issue title, body, and comments will become the title, body, and comments on the new pull request. - `), + description: "An issue in the repository to convert to a pull request. The issue title, body, and comments will become the title, body, and comments on the new pull request.", min: 1, optional: true, }, @@ -117,10 +88,7 @@ export default { async run({ $ }) { if (!this.issue && !this.title) { - throw new ConfigurationError(toSingleLineString(` - Title is required if Issue is unspecified. - You can either specify a new pull request with Title or convert an existing issue to a pull request with Issue. - `)); + throw new ConfigurationError("Title is required if Issue is unspecified. You can either specify a new pull request with Title or convert an existing issue to a pull request with Issue."); } if (this.issue && this.title) { diff --git a/components/github/actions/create-repository/create-repository.mjs b/components/github/actions/create-repository/create-repository.mjs index 8708477b070d9..96588e80e4039 100644 --- a/components/github/actions/create-repository/create-repository.mjs +++ b/components/github/actions/create-repository/create-repository.mjs @@ -3,8 +3,8 @@ import github from "../../github.app.mjs"; export default { key: "github-create-repository", name: "Create Repository", - description: "Creates a new repository for the authenticated user. [See docs here](https://docs.github.com/en/rest/repos/repos#create-a-repository-for-the-authenticated-user)", - version: "0.0.10", + description: "Creates a new repository for the authenticated user. [See the documentation](https://docs.github.com/en/rest/repos/repos#create-a-repository-for-the-authenticated-user)", + version: "0.0.15", type: "action", props: { github, diff --git a/components/github/actions/create-workflow-dispatch/create-workflow-dispatch.mjs b/components/github/actions/create-workflow-dispatch/create-workflow-dispatch.mjs new file mode 100644 index 0000000000000..0add6e3bca5dc --- /dev/null +++ b/components/github/actions/create-workflow-dispatch/create-workflow-dispatch.mjs @@ -0,0 +1,57 @@ +import { ConfigurationError } from "@pipedream/platform"; +import github from "../../github.app.mjs"; + +export default { + key: "github-create-workflow-dispatch", + name: "Create Workflow Dispatch", + description: "Creates a new workflow dispatch event. [See the documentation](https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#create-a-workflow-dispatch-event)", + version: "0.0.2", + type: "action", + props: { + github, + repoFullname: { + propDefinition: [ + github, + "repoFullname", + ], + }, + workflowId: { + propDefinition: [ + github, + "workflowId", + ({ repoFullname }) => ({ + repoFullname, + }), + ], + }, + ref: { + type: "string", + label: "Ref", + description: "The git reference for the workflow. The reference can be a branch or tag name.", + }, + inputs: { + type: "object", + label: "Inputs", + description: "Input keys and values configured in the workflow file. The maximum number of properties is 10. Any default properties configured in the workflow file will be used when inputs are omitted.", + optional: true, + }, + }, + async run({ $ }) { + try { + const response = await this.github.createWorkflowDispatch({ + repoFullname: this.repoFullname, + workflowId: this.workflowId, + data: { + ref: this.ref, + inputs: this.inputs, + }, + }); + + $.export("$summary", "Workflow dispatch successfully created!"); + + return response; + } catch (e) { + throw new ConfigurationError(e?.response?.data?.message); + } + }, +}; diff --git a/components/github/actions/disable-workflow/disable-workflow.mjs b/components/github/actions/disable-workflow/disable-workflow.mjs new file mode 100644 index 0000000000000..43fc93c209c3d --- /dev/null +++ b/components/github/actions/disable-workflow/disable-workflow.mjs @@ -0,0 +1,42 @@ +import { ConfigurationError } from "@pipedream/platform"; +import github from "../../github.app.mjs"; + +export default { + key: "github-disable-workflow", + name: "Disable Workflow", + description: "Disables a workflow and sets the **state** of the workflow to **disabled_manually**. [See the documentation](https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#disable-a-workflow)", + version: "0.0.2", + type: "action", + props: { + github, + repoFullname: { + propDefinition: [ + github, + "repoFullname", + ], + }, + workflowId: { + propDefinition: [ + github, + "workflowId", + ({ repoFullname }) => ({ + repoFullname, + }), + ], + }, + }, + async run({ $ }) { + try { + const response = await this.github.disableWorkflow({ + repoFullname: this.repoFullname, + workflowId: this.workflowId, + }); + + $.export("$summary", `Successfully disabled the workflow with Id: ${this.workflowId}!`); + + return response; + } catch (e) { + throw new ConfigurationError(e?.response?.data?.message); + } + }, +}; diff --git a/components/github/actions/enable-workflow/enable-workflow.mjs b/components/github/actions/enable-workflow/enable-workflow.mjs new file mode 100644 index 0000000000000..98c34c27ea57c --- /dev/null +++ b/components/github/actions/enable-workflow/enable-workflow.mjs @@ -0,0 +1,37 @@ +import github from "../../github.app.mjs"; + +export default { + key: "github-enable-workflow", + name: "Enable Workflow", + description: "Enables a workflow and sets the **state** of the workflow to **active**. [See the documentation](https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#enable-a-workflow)", + version: "0.0.2", + type: "action", + props: { + github, + repoFullname: { + propDefinition: [ + github, + "repoFullname", + ], + }, + workflowId: { + propDefinition: [ + github, + "workflowId", + ({ repoFullname }) => ({ + repoFullname, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.github.enableWorkflow({ + repoFullname: this.repoFullname, + workflowId: this.workflowId, + }); + + $.export("$summary", `Successfully enabled the workflow with Id: ${this.workflowId}!`); + + return response; + }, +}; diff --git a/components/github/actions/get-issue-assignees/get-issue-assignees.mjs b/components/github/actions/get-issue-assignees/get-issue-assignees.mjs index 701ee0d40e489..5c1983963d0d1 100644 --- a/components/github/actions/get-issue-assignees/get-issue-assignees.mjs +++ b/components/github/actions/get-issue-assignees/get-issue-assignees.mjs @@ -3,8 +3,8 @@ import github from "../../github.app.mjs"; export default { key: "github-get-issue-assignees", name: "Get Issue Assignees", - description: "Get assignees for an issue in a Gihub repo. [See docs here](https://docs.github.com/en/rest/issues/issues#get-an-issue)", - version: "0.0.15", + description: "Get assignees for an issue in a Gihub repo. [See the documentation](https://docs.github.com/en/rest/issues/issues#get-an-issue)", + version: "0.0.20", type: "action", props: { github, diff --git a/components/github/actions/get-repository-content/get-repository-content.mjs b/components/github/actions/get-repository-content/get-repository-content.mjs index b1d541277348a..876794c1eb2eb 100644 --- a/components/github/actions/get-repository-content/get-repository-content.mjs +++ b/components/github/actions/get-repository-content/get-repository-content.mjs @@ -1,14 +1,10 @@ -import { toSingleLineString } from "../../common/utils.mjs"; import github from "../../github.app.mjs"; export default { key: "github-get-repository-content", name: "Get Repository Content", - description: toSingleLineString(` - Get the content of a file or directory in a specific repository. - [See docs here](https://docs.github.com/en/rest/repos/contents#get-repository-content) - `), - version: "0.0.14", + description: "Get the content of a file or directory in a specific repository. [See the documentation](https://docs.github.com/en/rest/repos/contents#get-repository-content)", + version: "0.1.2", type: "action", props: { github, @@ -20,25 +16,41 @@ export default { }, path: { label: "Path", - description: toSingleLineString(` - The file path or directory to retrieve. - When left unspecified, this action will retrieve the contents of the - repository's root directory. - `), + description: "The file path or directory to retrieve. Defaults to the repository's root directory.", type: "string", default: "", optional: true, }, mediaType: { label: "Media Type", - description: toSingleLineString(` - [Custom media types](https://docs.github.com/en/rest/overview/media-types) are used in the API to let consumers choose the format of the data they wish to receive. - This is done by adding one or more of the following types to the Accept header when you make a request. - Media types are specific to resources, allowing them to change independently and support - formats that other resources don't. - `), + description: "The media type of the response. [See the documentation](https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#get-repository-content) for more information.", type: "string", - default: "", + options: [ + { + value: "application/vnd.github.raw+json", + label: "Returns the raw file contents for files and symlinks", + }, + { + value: "application/vnd.github.html+json", + label: "Returns the file contents in HTML", + }, + { + value: "application/vnd.github.object+json", + label: "Returns the contents in a consistent object format regardless of the content type", + }, + ], + optional: true, + }, + branch: { + propDefinition: [ + github, + "branch", + (c) => ({ + repoFullname: c.repoFullname, + }), + ], + description: + "The branch to use. Defaults to the repository's default branch (usually `main` or `master`)", optional: true, }, }, @@ -47,6 +59,9 @@ export default { repoFullname: this.repoFullname, path: this.path, mediaType: this.mediaType, + params: { + ref: this.branch?.split?.("/")[0], + }, }); $.export("$summary", "Successfully retrieved repository content."); diff --git a/components/github/actions/get-repository/get-repository.mjs b/components/github/actions/get-repository/get-repository.mjs index 7fe6af05ecd7f..2c00d8a3e1432 100644 --- a/components/github/actions/get-repository/get-repository.mjs +++ b/components/github/actions/get-repository/get-repository.mjs @@ -2,9 +2,9 @@ import github from "../../github.app.mjs"; export default { key: "github-get-repository", - name: "Get Repository", - description: "Get specific repository. [See docs here](https://docs.github.com/en/rest/repos/repos#get-a-repository)", - version: "0.0.15", + name: "Get Repository Info", + description: "Get information for a specific repository. [See the documentation](https://docs.github.com/en/rest/repos/repos#get-a-repository)", + version: "0.0.20", type: "action", props: { github, @@ -16,11 +16,12 @@ export default { }, }, async run({ $ }) { + const { repoFullname } = this; const response = await this.github.getRepo({ - repoFullname: this.repoFullname, + repoFullname, }); - $.export("$summary", "Successfully retrieved repository."); + $.export("$summary", `Successfully retrieved repository ${repoFullname}`); return response; }, diff --git a/components/github/actions/get-reviewers/get-reviewers.mjs b/components/github/actions/get-reviewers/get-reviewers.mjs index 8fa7f55201703..460e690335946 100644 --- a/components/github/actions/get-reviewers/get-reviewers.mjs +++ b/components/github/actions/get-reviewers/get-reviewers.mjs @@ -1,12 +1,12 @@ import github from "../../github.app.mjs"; +import asyncProps from "../common/asyncProps.mjs"; import constants from "../common/constants.mjs"; -import { ConfigurationError } from "@pipedream/platform"; export default { key: "github-get-reviewers", name: "Get Reviewers", - description: "Get reviewers for a PR ([see docs](https://docs.github.com/en/rest/pulls/reviews#list-reviews-for-a-pull-request)) or Commit SHA ([see docs](https://docs.github.com/en/rest/commits/commits#list-pull-requests-associated-with-a-commit)).", - version: "0.0.15", + description: "Get reviewers for a PR ([see documentation](https://docs.github.com/en/rest/pulls/reviews#list-reviews-for-a-pull-request)) or Commit SHA ([see documentation](https://docs.github.com/en/rest/commits/commits#list-pull-requests-associated-with-a-commit)).", + version: "0.1.2", type: "action", props: { github, @@ -16,30 +16,37 @@ export default { "repoFullname", ], }, - pullNumber: { - propDefinition: [ - github, - "pullNumber", - (c) => ({ - repoFullname: c.repoFullname, - }), - ], - optional: true, - }, - commitSha: { + prOrCommit: { type: "string", - label: "Commit SHA", - description: "A commit SHA. This field will have precendence over **PR Number**", - optional: true, + label: "PR or Commit", + description: "Whether to get reviewers for a [pull request](https://docs.github.com/en/rest/pulls/reviews#list-reviews-for-a-pull-request) or a [commit SHA](https://docs.github.com/en/rest/commits/commits#list-pull-requests-associated-with-a-commit).", + options: [ + "Pull Request", + "Commit SHA", + ], + reloadProps: true, }, reviewStates: { type: "string[]", label: "Review States", - description: "Filter by these review states. Default includes `APPROVED` and `CHANGES_REQUESTED` only", + description: "Filter by these review states", options: constants.PULL_REQUEST_STATES, optional: true, }, }, + async additionalProps() { + return this.prOrCommit === "Pull Request" + ? { + pullNumber: asyncProps.pullNumber, + } + : { + commitSha: { + type: "string", + label: "Commit SHA", + description: "A commit SHA to get reviewers for", + }, + }; + }, methods: { getReviewers(reviews) { const reviewers = reviews @@ -47,10 +54,7 @@ export default { if (this.reviewStates?.length) { return this.reviewStates.includes(review.state); // user-defined states } - return [ - "APPROVED", - "CHANGES_REQUESTED", - ].includes(review.state); // default states + return true; // default states: all }) .map((review) => review.user.login); return this.uniqueReviewers(reviewers); @@ -62,10 +66,6 @@ export default { }, }, async run({ $ }) { - if (!(this.pullNumber || this.commitSha)) { - throw new ConfigurationError("Please provide a **PR Number** or a **Commit SHA**"); - } - let pullNumber = this.pullNumber; if (this.commitSha) { @@ -89,7 +89,7 @@ export default { const reviewers = this.getReviewers(reviews); - $.export("$summary", "Successfully retrieved reviewers."); + $.export("$summary", "Successfully retrieved reviewers"); return reviewers; }, diff --git a/components/github/actions/get-workflow-run/get-workflow-run.mjs b/components/github/actions/get-workflow-run/get-workflow-run.mjs new file mode 100644 index 0000000000000..23ad751085a3b --- /dev/null +++ b/components/github/actions/get-workflow-run/get-workflow-run.mjs @@ -0,0 +1,37 @@ +import github from "../../github.app.mjs"; + +export default { + key: "github-get-workflow-run", + name: "Get Workflow Run", + description: "Gets a specific workflow run. [See the documentation](https://docs.github.com/en/rest/actions/workflow-runs?apiVersion=2022-11-28#get-a-workflow-run)", + version: "0.0.2", + type: "action", + props: { + github, + repoFullname: { + propDefinition: [ + github, + "repoFullname", + ], + }, + workflowRunId: { + propDefinition: [ + github, + "workflowRunId", + ({ repoFullname }) => ({ + repoFullname, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.github.getWorkflowRun({ + repoFullname: this.repoFullname, + workflowRunId: this.workflowRunId, + }); + + $.export("$summary", `Successfully retrieved the workflow run with Id: ${this.workflowRunId}!.`); + + return response; + }, +}; diff --git a/components/github/actions/list-gists-for-a-user/list-gists-for-a-user.mjs b/components/github/actions/list-gists-for-a-user/list-gists-for-a-user.mjs index ce2a1b90be816..79ba77bd4777c 100644 --- a/components/github/actions/list-gists-for-a-user/list-gists-for-a-user.mjs +++ b/components/github/actions/list-gists-for-a-user/list-gists-for-a-user.mjs @@ -1,34 +1,51 @@ +import { ConfigurationError } from "@pipedream/platform"; import github from "../../github.app.mjs"; export default { key: "github-list-gists-for-a-user", name: "List Gists for a User", - description: "Lists public gists for the specified user. [See docs here](https://docs.github.com/en/rest/gists/gists?apiVersion=2022-11-28#list-gists-for-a-user)", - version: "0.0.4", + description: "Lists public gists for the specified user. [See the documentation](https://docs.github.com/en/rest/gists/gists?apiVersion=2022-11-28#list-gists-for-a-user)", + version: "0.1.2", type: "action", props: { - github, - username: { - label: "Username", - description: "The username of the user whose gists you want to list", - type: "string", + github: { + ...github, + reloadProps: true, }, since: { - label: "Since", - description: "Only show notifications updated after the given time. This is a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ.", + label: "Filter by Timestamp", + description: "Only show notifications updated since the given time. This should be a timestamp in ISO 8601 format, e.g. `2018-05-16T09:30:10Z` or [another standard date/time format](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format).", type: "string", optional: true, }, }, + async additionalProps() { + const { login } = await this.github.getAuthenticatedUser(); + return { + username: { + label: "Username", + description: "The username of the user whose gists you want to list", + type: "string", + default: login, + }, + }; + }, async run({ $ }) { const PER_PAGE = 100; const MAX_PAGES = 50; let page = 1; const data = []; + const date = this.since && new Date(this.since); + if (date && isNaN(date.valueOf())) { + throw new ConfigurationError("Invalid date string provided"); + } + + const since = date?.toISOString(); + while (true) { const res = await this.github.listGistsFromUser(this.username, { - since: this.since, + since, per_page: PER_PAGE, page, }); @@ -45,11 +62,11 @@ export default { } if (data.length === 0) { - $.export("$summary", `No gists found for user "${this.username}".`); + $.export("$summary", `No gists found for user "${this.username}"`); return; } - $.export("$summary", `Successfully fetched ${data.length} gist(s).`); + $.export("$summary", `Successfully fetched ${data.length} gist(s)`); return data; }, }; diff --git a/components/github/actions/list-releases/list-releases.mjs b/components/github/actions/list-releases/list-releases.mjs index a0fe41f07fbe0..0c4f4aa04a16e 100644 --- a/components/github/actions/list-releases/list-releases.mjs +++ b/components/github/actions/list-releases/list-releases.mjs @@ -1,12 +1,10 @@ -// Import necessary modules import github from "../../github.app.mjs"; -// Define the action export default { key: "github-list-releases", name: "List Releases", description: "List releases for a repository [See the documentation](https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#list-releases)", - version: "0.0.3", + version: "0.0.8", type: "action", props: { github, diff --git a/components/github/actions/list-workflow-runs/list-workflow-runs.mjs b/components/github/actions/list-workflow-runs/list-workflow-runs.mjs new file mode 100644 index 0000000000000..68c1cab4053ee --- /dev/null +++ b/components/github/actions/list-workflow-runs/list-workflow-runs.mjs @@ -0,0 +1,50 @@ +import github from "../../github.app.mjs"; + +export default { + key: "github-list-workflow-runs", + name: "List Workflow Runs", + description: "List workflowRuns for a repository [See the documentation](https://docs.github.com/en/rest/actions/workflow-runs?apiVersion=2022-11-28#list-workflow-runs-for-a-repository)", + version: "0.0.2", + type: "action", + props: { + github, + repoFullname: { + propDefinition: [ + github, + "repoFullname", + ], + }, + limit: { + type: "integer", + label: "Limit", + description: "The maximum quantity to be returned.", + default: 100, + }, + }, + async run({ $ }) { + let page = 1; + const perPage = 100; + let allWorkflowRuns = []; + let count = 0; + + while (count < this.limit) { + const { workflow_runs: workflowRuns } = await this.github.listWorkflowRuns({ + repoFullname: this.repoFullname, + perPage: perPage, + page: page, + }); + + if (workflowRuns.length === 0) { + break; + } + + allWorkflowRuns = allWorkflowRuns.concat(workflowRuns); + count += workflowRuns.length; + page += 1; + } + + $.export("$summary", `Successfully retrieved ${allWorkflowRuns.length} workflow runs.`); + + return allWorkflowRuns; + }, +}; diff --git a/components/github/actions/search-issues-and-pull-requests/search-issues-and-pull-requests.mjs b/components/github/actions/search-issues-and-pull-requests/search-issues-and-pull-requests.mjs index 0c1574c5acaab..a2dd6de5bf32c 100644 --- a/components/github/actions/search-issues-and-pull-requests/search-issues-and-pull-requests.mjs +++ b/components/github/actions/search-issues-and-pull-requests/search-issues-and-pull-requests.mjs @@ -3,27 +3,36 @@ import github from "../../github.app.mjs"; export default { key: "github-search-issues-and-pull-requests", name: "Search Issues and Pull Requests", - description: "Find issues and pull requests by state and keyword. [See docs here](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests)", - version: "0.1.14", + description: "Find issues and pull requests by state and keyword. [See the documentation](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests)", + version: "0.2.2", type: "action", props: { github, + infoBox: { + type: "alert", + alertType: "info", + content: `Example query: \`bug report in:title type:issue repo:octocat/Hello-World\` + +This will return issues in the repository [octocat/Hello-World](https://github.com/octocat/Hello-World) with the title including the words "bug report".`, + }, query: { label: "Query", - description: "The query contains one or more search keywords and qualifiers", + description: "The query contains one or more search keywords and qualifiers. [See the documentation](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests) for more information and examples", type: "string", }, maxResults: { label: "Maximum Results", - description: "The maximum of resources that will be returned", + description: "The maximum amount of items to retrieve", type: "integer", default: 100, }, }, - async run() { - return this.github.searchIssueAndPullRequests({ + async run({ $ }) { + const response = await this.github.searchIssueAndPullRequests({ query: this.query, maxResults: this.maxResults, }); + $.export("$summary", `Successfully fetched ${response.length} items`); + return response; }, }; diff --git a/components/github/actions/star-repo/star-repo.mjs b/components/github/actions/star-repo/star-repo.mjs new file mode 100644 index 0000000000000..8b4e5e8ffbf73 --- /dev/null +++ b/components/github/actions/star-repo/star-repo.mjs @@ -0,0 +1,39 @@ +import github from "../../github.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "github-star-repo", + name: "Star Repo", + description: "Star a repository. [See the docs](https://docs.github.com/en/rest/activity/starring?apiVersion=2022-11-28#star-a-repository-for-the-authenticated-user) for more info.", + version: "0.0.2", + type: "action", + props: { + github, + repoFullname: { + type: "string", + label: "Repository", + description: "The name of the repository (not case sensitive). The format should be `owner/repo` (for example, `PipedreamHQ/pipedream`).", + }, + }, + async run({ $ }) { + const { repoFullname } = this; + + // Verify repo exists + let repo; + try { + repo = await this.github.getRepo({ + repoFullname, + }); + } catch (err) { + throw new ConfigurationError(`Couldn't find the **${repoFullname}** repo. Please verify the name (\`owner/repo\` format) and try again.`); + } + + // Star the repo + await this.github.starRepo({ + repoFullname, + }); + + $.export("$summary", `Successfully starred [${repo.full_name}](${repo.html_url})`); + return repo.html_url; + }, +}; diff --git a/components/github/actions/update-gist/update-gist.mjs b/components/github/actions/update-gist/update-gist.mjs index 2fdd60faaf4ef..eecd528efa446 100644 --- a/components/github/actions/update-gist/update-gist.mjs +++ b/components/github/actions/update-gist/update-gist.mjs @@ -5,11 +5,16 @@ import { ConfigurationError } from "@pipedream/platform"; export default { key: "github-update-gist", name: "Update Gist", - description: "Allows you to update a gist's description and to update, delete, or rename gist files. Files from the previous version of the gist that aren't explicitly changed during an edit are unchanged. At least one of description or files is required. [See docs here](https://docs.github.com/en/rest/gists/gists?apiVersion=2022-11-28#update-a-gist)", - version: "0.0.4", + description: "Allows you to update a gist's description and to update, delete, or rename gist files. [See the documentation](https://docs.github.com/en/rest/gists/gists?apiVersion=2022-11-28#update-a-gist)", + version: "0.0.9", type: "action", props: { github, + infoAlert: { + type: "alert", + alertType: "info", + content: "Files from the previous version of the gist that aren't explicitly changed during an edit are unchanged. At least one of description or files is required.", + }, gistId: { propDefinition: [ github, diff --git a/components/github/actions/update-issue/update-issue.mjs b/components/github/actions/update-issue/update-issue.mjs index fe1f2af0d6459..7c5f45c02221c 100644 --- a/components/github/actions/update-issue/update-issue.mjs +++ b/components/github/actions/update-issue/update-issue.mjs @@ -1,19 +1,20 @@ -import github from "../../github.app.mjs"; +import createIssue from "../create-issue/create-issue.mjs"; + +const { + props: { + github, repoFullname, ...props + }, additionalProps, methods, +} = createIssue; export default { key: "github-update-issue", name: "Update Issue", - description: "Update a new issue in a Gihub repo. [See docs here](https://docs.github.com/en/rest/issues/issues#update-an-issue)", - version: "0.1.14", + description: "Update a new issue in a Gihub repo. [See the documentation](https://docs.github.com/en/rest/issues/issues#update-an-issue)", + version: "0.2.2", type: "action", props: { github, - repoFullname: { - propDefinition: [ - github, - "repoFullname", - ], - }, + repoFullname, issueNumber: { label: "Issue Number", description: "The number that identifies the issue.", @@ -26,55 +27,21 @@ export default { }), ], }, - title: { - label: "Title", - description: "The title of the issue", - type: "string", - }, - body: { - label: "Body", - description: "The contents of the issue", - type: "string", - optional: true, - }, - labels: { - label: "Labels", - description: "Labels to associate with this issue. NOTE: Only users with push access can set labels for new issues", - optional: true, - propDefinition: [ - github, - "labels", - (c) => ({ - repoFullname: c.repoFullname, - }), - ], - }, - assignees: { - label: "Assignees", - description: "Logins for Users to assign to this issue. NOTE: Only users with push access can set assignees for new issues", - optional: true, - propDefinition: [ - github, - "collaborators", - (c) => ({ - repoFullname: c.repoFullname, - }), - ], - }, + ...props, }, + additionalProps, + methods, async run({ $ }) { + const { // eslint-disable-next-line no-unused-vars + github, repoFullname, issueNumber, infoBox, ...data + } = this; const response = await this.github.updateIssue({ - repoFullname: this.repoFullname, - issueNumber: this.issueNumber, - data: { - title: this.title, - body: this.body, - labels: this.labels, - assignees: this.assignees, - }, + repoFullname, + issueNumber, + data, }); - $.export("$summary", "Successfully created issue."); + $.export("$summary", `Successfully updated issue #${issueNumber}`); return response; }, diff --git a/components/github/actions/update-project-v2-item-status/update-project-v2-item-status.mjs b/components/github/actions/update-project-v2-item-status/update-project-v2-item-status.mjs new file mode 100644 index 0000000000000..d3b1b374c3a14 --- /dev/null +++ b/components/github/actions/update-project-v2-item-status/update-project-v2-item-status.mjs @@ -0,0 +1,109 @@ +import github from "../../github.app.mjs"; + +export default { + key: "github-update-project-v2-item-status", + name: "Update Project (V2) Item Status", + description: "Update the status of an item in the selected Project (V2). [See the documentation](https://docs.github.com/en/graphql/reference/mutations#updateprojectv2itemfieldvalue)", + version: "0.0.4", + type: "action", + props: { + github, + org: { + propDefinition: [ + github, + "orgName", + ], + }, + repo: { + propDefinition: [ + github, + "repoOrg", + ({ org }) => ({ + org, + }), + ], + optional: true, + }, + project: { + propDefinition: [ + github, + "projectV2", + ({ + org, repo, + }) => ({ + org, + repo, + }), + ], + }, + item: { + propDefinition: [ + github, + "projectItem", + ({ + org, repo, project, + }) => ({ + org, + repo, + project, + }), + ], + }, + status: { + propDefinition: [ + github, + "status", + ({ + org, repo, project, + }) => ({ + org, + repo, + project, + }), + ], + type: "string", + description: "The status to set for the item", + }, + moveToTop: { + type: "boolean", + label: "Move to Top", + description: "If true, moves the item to the top of the column instead of the bottom.", + optional: true, + }, + }, + async run({ $ }) { + const { + github, org: repoOwner, repo: repoName, project, item, status, + } = this; + const { id: fieldId } = await github.getProjectV2StatusField({ + repoOwner, + repoName, + project, + }); + + const projectId = await github.getProjectV2Id({ + repoOwner, + repoName, + project, + }); + + const response = await github.updateProjectV2ItemStatus({ + projectId, + itemId: item, + fieldId, + value: { + singleSelectOptionId: status, + }, + }); + + if (this.moveToTop) { + await github.updateProjectV2ItemPosition({ + projectId, + itemId: item, + }); + } + + $.export("$summary", "Successfully updated item"); + return response.updateProjectV2ItemFieldValue?.projectV2Item; + }, +}; diff --git a/components/github/common/mutations.mjs b/components/github/common/mutations.mjs new file mode 100644 index 0000000000000..140315412c20b --- /dev/null +++ b/components/github/common/mutations.mjs @@ -0,0 +1,22 @@ +const updateProjectItemMutation = ` + mutation moveItemToColumn($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: ProjectV2FieldValue!) { + updateProjectV2ItemFieldValue(input: {projectId: $projectId, itemId: $itemId, fieldId: $fieldId, value: $value}) { + projectV2Item { + id + } + } + } +`; + +const updateProjectItemPositionMutation = ` + mutation moveProjectV2Item($projectId: ID!, $itemId: ID!, $afterId: ID) { + updateProjectV2ItemPosition(input: { projectId: $projectId, itemId: $itemId, afterId: $afterId }) { + clientMutationId + } + } +`; + +export default { + updateProjectItemMutation, + updateProjectItemPositionMutation, +}; diff --git a/components/github/common/queries.mjs b/components/github/common/queries.mjs index 70e672802d033..987c4be4f4dc2 100644 --- a/components/github/common/queries.mjs +++ b/components/github/common/queries.mjs @@ -59,6 +59,7 @@ const organizationStatusFieldsQuery = ` projectV2(number: $project) { field(name: "Status") { ... on ProjectV2SingleSelectField { + id options { name id @@ -76,6 +77,7 @@ const statusFieldsQuery = ` projectV2(number: $project) { field(name: "Status") { ... on ProjectV2SingleSelectField { + id options { name id @@ -88,13 +90,18 @@ const statusFieldsQuery = ` `; const projectItemsQuery = ` - query ($repoOwner: String!, $repoName: String!, $project: Int!, $historicalEventsNumber: Int!) { + query ($repoOwner: String!, $repoName: String!, $project: Int!, $amount: Int!) { repository(name: $repoName, owner: $repoOwner) { projectV2(number: $project) { - items(last: $historicalEventsNumber) { + items(last: $amount) { nodes { id type + fieldValueByName(name: "Title") { + ... on ProjectV2ItemFieldTextValue { + text + } + } } } } @@ -103,13 +110,18 @@ const projectItemsQuery = ` `; const organizationProjectItemsQuery = ` - query ($repoOwner: String!, $project: Int!, $historicalEventsNumber: Int!) { + query ($repoOwner: String!, $project: Int!, $amount: Int!) { organization(login: $repoOwner) { projectV2(number: $project) { - items(last: $historicalEventsNumber) { + items(last: $amount) { nodes { id type + fieldValueByName(name: "Title") { + ... on ProjectV2ItemFieldTextValue { + text + } + } } } } @@ -142,6 +154,26 @@ const projectItemQuery = ` } `; +const orgProjectIdQuery = ` + query ($repoOwner: String!, $project: Int!) { + organization(login: $repoOwner) { + projectV2(number: $project) { + id + } + } + } +`; + +const repoProjectIdQuery = ` + query ($repoOwner: String!, $repoName: String!, $project: Int!) { + repository(name: $repoName, owner: $repoOwner) { + projectV2(number: $project) { + id + } + } + } +`; + export default { discussionsQuery, projectsQuery, @@ -151,4 +183,6 @@ export default { projectItemsQuery, organizationProjectItemsQuery, projectItemQuery, + orgProjectIdQuery, + repoProjectIdQuery, }; diff --git a/components/github/common/utils.mjs b/components/github/common/utils.mjs index e011fd87c8355..aa4395d22455a 100644 --- a/components/github/common/utils.mjs +++ b/components/github/common/utils.mjs @@ -1,22 +1,24 @@ -/** - * A utility function that accepts a string as an argument and reformats it in - * order to remove newline characters and consecutive spaces. Useful when - * dealing with very long templated strings that are split into multiple lines. - * - * @example - * // returns "This is a much cleaner string" - * toSingleLineString(` - * This is a much - * cleaner string - * `); - * - * @param {string} multiLineString the input string to reformat - * @returns a formatted string based on the content of the input argument, - * without newlines and multiple spaces - */ -export function toSingleLineString(multiLineString) { - return multiLineString - .trim() - .replace(/\n/g, " ") - .replace(/\s{2,}/g, " "); +async function getUserRepoPermissions(github, repoFullname) { + const { login: username } = await github.getAuthenticatedUser(); + const { user: { permissions } } = await github.getUserRepoPermissions({ + repoFullname, + username, + }); + return permissions; +} + +export async function checkAdminPermission() { + const { + github, repoFullname, + } = this; + const { admin } = await getUserRepoPermissions(github, repoFullname); + return admin; +} + +export async function checkPushPermission() { + const { + github, repoFullname, + } = this; + const { push } = await getUserRepoPermissions(github, repoFullname); + return push; } diff --git a/components/github/github.app.mjs b/components/github/github.app.mjs index ea3af919c85ed..14bef3980b13f 100644 --- a/components/github/github.app.mjs +++ b/components/github/github.app.mjs @@ -1,9 +1,11 @@ import { Octokit } from "@octokit/core"; import { paginateRest } from "@octokit/plugin-paginate-rest"; -import queries from "./common/queries.mjs"; import { axios, ConfigurationError, } from "@pipedream/platform"; +import constants from "./actions/common/constants.mjs"; +import mutations from "./common/mutations.mjs"; +import queries from "./common/queries.mjs"; const CustomOctokit = Octokit.plugin(paginateRest); @@ -13,7 +15,7 @@ export default { propDefinitions: { orgName: { label: "Organization", - description: "The name of the Github organization. The name is not case sensitive.", + description: "The name of the Github organization (not case sensitive).", type: "string", async options() { const organizations = await this.getOrganizations(); @@ -23,7 +25,7 @@ export default { }, repoFullname: { label: "Repository", - description: "The name of the repository. The name is not case sensitive", + description: "The name of the repository (not case sensitive). The format should be `owner/repo` (for example, `PipedreamHQ/pipedream`).", type: "string", async options({ org }) { const repositories = await this.getRepos({ @@ -96,23 +98,43 @@ export default { }, status: { label: "Item Status", - description: "The status for a project item", - type: "string", + description: "The status(es) to emit events for. If not specified, events will be emitted for all statuses.", + type: "string[]", async options({ org, repo, project, }) { - const { statuses } = await this.getProjectV2Statuses({ + const { options } = await this.getProjectV2StatusField({ repoOwner: org, repoName: repo, project, }); - return statuses.map((status) => ({ + return options.map((status) => ({ label: status.name, value: status.id, })); }, }, + projectItem: { + label: "Project (V2) Item", + description: "The project item to update", + type: "string", + async options({ + org, repo, project, + }) { + const items = await this.getProjectV2Items({ + repoOwner: org, + repoName: repo, + project, + amount: 100, + }); + + return items.map((status) => ({ + label: status.fieldValueByName?.text ?? status.id, + value: status.id, + })); + }, + }, labels: { label: "Labels", description: "The labels", @@ -141,12 +163,15 @@ export default { label: "Issue Number", description: "The issue number", type: "integer", - async options({ repoFullname }) { + async options({ + repoFullname, page = 0, + }) { const issues = await this.getRepositoryIssues({ + page: page + 1, repoFullname, }); - return issues.map((issue) => ({ + return issues?.filter?.((issue) => !issue.pull_request).map((issue) => ({ label: issue.title, value: +issue.number, })); @@ -161,9 +186,7 @@ export default { }) { const branches = await this.getBranches({ repoFullname, - params: { - page: page + 1, - }, + page: page + 1, }); return branches.map((branch) => ({ @@ -176,8 +199,11 @@ export default { type: "integer", label: "PR Number", description: "A pull request number", - async options({ repoFullname }) { + async options({ + repoFullname, page = 0, + }) { const prs = await this.getRepositoryPullRequests({ + page: page + 1, repoFullname, }); @@ -187,6 +213,21 @@ export default { })); }, }, + milestoneNumber: { + type: "integer", + label: "Milestone Number", + description: "The number of a milestone to associate this issue with.", + async options({ repoFullname }) { + const items = await this.getRepositoryMilestones({ + repoFullname, + }); + + return items.map((item) => ({ + label: item.title, + value: +item.number, + })); + }, + }, column: { label: "Column", description: "The column in a project board", @@ -231,6 +272,48 @@ export default { })); }, }, + workflowId: { + type: "string", + label: "Workflow Id", + description: "The Id of the workflow.", + async options({ + repoFullname, page, + }) { + const { workflows: items } = await this.listWorkflows({ + repoFullname, + perPage: constants.LIMIT, + page, + }); + + return items.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + workflowRunId: { + type: "string", + label: "Workflow Run Id", + description: "The Id of the workflow Run.", + async options({ + repoFullname, page, + }) { + const { workflow_runs: items } = await this.listWorkflowRuns({ + repoFullname, + perPage: constants.LIMIT, + page, + }); + + return items.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, }, methods: { _baseApiUrl() { @@ -269,7 +352,7 @@ export default { console.error(exception); const status = exception?.status; if (status && (status === 404 || status === 403)) { - throw new ConfigurationError(`The request failed with status "${status}". It is likely that your token doesn't have sufficient permissions to execute that request. [see mor information here](https://docs.github.com/en/rest/overview/authenticating-to-the-rest-api?apiVersion=2022-11-28#about-authentication).`); + throw new ConfigurationError(`The request failed with status "${status}". It's likely that your connected account doesn't have sufficient permissions to execute that request. [See more information here](https://docs.github.com/en/rest/overview/authenticating-to-the-rest-api?apiVersion=2022-11-28#about-authentication).`); } throw exception; }, @@ -317,6 +400,7 @@ export default { repoFullname, path, mediaType, + ...args }) { return this._makeRequest({ path: `/repos/${repoFullname}/contents/${path}`, @@ -325,6 +409,7 @@ export default { Accept: mediaType, }, }), + ...args, }); }, async getRepositoryLabels({ repoFullname }) { @@ -333,10 +418,14 @@ export default { async getRepositoryCollaborators({ repoFullname }) { return this._client().paginate(`GET /repos/${repoFullname}/collaborators`, {}); }, - async getRepositoryIssues({ repoFullname }) { - return this._client().paginate(`GET /repos/${repoFullname}/issues`, { + async getRepositoryIssues({ + repoFullname, ...args + }) { + const results = await this._client().request(`GET /repos/${repoFullname}/issues`, { state: "all", + ...args, }); + return results.data; }, async getRepositoryProjects({ repoFullname }) { return this._client().paginate(`GET /repos/${repoFullname}/projects`, {}); @@ -358,7 +447,7 @@ export default { response?.organization?.projectsV2?.pageInfo?.endCursor, }; }, - async getProjectV2Statuses({ + async getProjectV2StatusField({ repoOwner, repoName, project, }) { const response = await this.graphql(repoName ? @@ -369,10 +458,56 @@ export default { project, }); - return { - statuses: response?.repository?.projectV2?.field?.options ?? - response?.organization?.projectV2?.field?.options, - }; + return (response.repository ?? response.organization).projectV2?.field; + + }, + async getProjectV2Id({ + repoOwner, repoName, project, + }) { + const response = await this.graphql(repoName ? + queries.repoProjectIdQuery : + queries.orgProjectIdQuery, { + repoOwner, + repoName, + project, + }); + + return (response.repository ?? response.organization).projectV2?.id; + + }, + async getProjectV2Items({ + repoName, repoOwner, project, amount, + }) { + const response = await this.graphql(repoName ? + queries.projectItemsQuery : + queries.organizationProjectItemsQuery, + { + repoOwner, + repoName, + project, + amount, + }); + + return (response.repository ?? response.organization).projectV2?.items?.nodes; + }, + async updateProjectV2ItemStatus({ + projectId, itemId, fieldId, value, + }) { + return this.graphql(mutations.updateProjectItemMutation, { + projectId, + itemId, + fieldId, + value, + }); + }, + async updateProjectV2ItemPosition({ + projectId, itemId, + }) { + return this.graphql(mutations.updateProjectItemPositionMutation, { + projectId, + itemId, + afterId: null, + }); }, async getProjectColumns({ project }) { return this._client().paginate(`GET /projects/${project}/columns`, {}); @@ -484,8 +619,11 @@ export default { return issues; }, - async getRepositoryPullRequests({ repoFullname }) { - return this._client().paginate(`GET /repos/${repoFullname}/pulls`, {}); + async getRepositoryPullRequests({ + repoFullname, ...args + }) { + const response = await this._client().request(`GET /repos/${repoFullname}/pulls`, args ?? {}); + return response.data; }, async getPullRequestForCommit({ repoFullname, sha, @@ -539,6 +677,11 @@ export default { const fileExists = await this._makeRequest({ path: `/repos/${repoFullname}/contents/${path}`, validateStatus: () => true, + ...(branch && { + params: { + ref: branch, + }, + }), }); if (fileExists.sha) { console.log("File exists, overwriting."); @@ -585,6 +728,66 @@ export default { return response.data; }, + async listWorkflows({ + repoFullname, + perPage, + page, + }) { + const response = await this._client().request(`GET /repos/${repoFullname}/actions/workflows`, { + per_page: perPage, + page: page, + }); + + return response.data; + }, + async listWorkflowRuns({ + repoFullname, + perPage, + page, + }) { + const response = await this._client().request(`GET /repos/${repoFullname}/actions/runs`, { + per_page: perPage, + page: page, + }); + + return response.data; + }, + async getWorkflowRun({ + repoFullname, workflowRunId, + }) { + const response = await this._client().request(`GET /repos/${repoFullname}/actions/runs/${workflowRunId}`); + + return response.data; + }, + createWorkflowDispatch({ + repoFullname, + workflowId, + ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/repos/${repoFullname}/actions/workflows/${workflowId}/dispatches`, + ...opts, + }); + }, + disableWorkflow({ + repoFullname, + workflowId, + }) { + return this._makeRequest({ + method: "PUT", + path: `/repos/${repoFullname}/actions/workflows/${workflowId}/disable`, + }); + }, + enableWorkflow({ + repoFullname, + workflowId, + }) { + return this._makeRequest({ + method: "PUT", + path: `/repos/${repoFullname}/actions/workflows/${workflowId}/enable`, + }); + }, async getUserRepoPermissions({ repoFullname, username, }) { @@ -592,6 +795,13 @@ export default { return response.data; }, + async getOrgUserInfo({ + org, username, + }) { + const response = await this._client().request(`GET /orgs/${org}/memberships/${username}`, {}); + + return response.data; + }, async getRepositoryLatestPullRequests({ repoFullname, ...args }) { @@ -678,6 +888,15 @@ export default { per_page: 100, }); + return response.data; + }, + async starRepo({ + repoFullname, ...args + }) { + const response = await this._client().request(`PUT /user/starred/${repoFullname}`, { + ...args, + }); + return response.data; }, }, diff --git a/components/github/package.json b/components/github/package.json index 802db4b0c6f59..d64845359b7c2 100644 --- a/components/github/package.json +++ b/components/github/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/github", - "version": "1.1.1", + "version": "1.6.2", "description": "Pipedream Github Components", "main": "github.app.mjs", "keywords": [ @@ -13,9 +13,8 @@ "@octokit/core": "^4.2.4", "@octokit/plugin-paginate-rest": "^2.17.0", "@octokit/webhooks-definitions": "^3.29.0", - "@pipedream/platform": "^1.5.1" + "@pipedream/platform": "^3.0.3" }, - "gitHead": "e12480b94cc03bed4808ebc6b13e7fdb3a1ba535", "publishConfig": { "access": "public" } diff --git a/components/github/sources/common/common-flex-new-or-updated.mjs b/components/github/sources/common/common-flex-new-or-updated.mjs index 590e6af2b9c73..5f600020f5a7c 100644 --- a/components/github/sources/common/common-flex-new-or-updated.mjs +++ b/components/github/sources/common/common-flex-new-or-updated.mjs @@ -1,4 +1,5 @@ import common from "./common-flex.mjs"; +import { getRelevantHeaders } from "./utils.mjs"; export default { ...common, @@ -20,7 +21,9 @@ export default { return !this.eventTypes || this.eventTypes.includes(type); }, async onWebhookTrigger(event) { - const { body } = event; + const { + body, headers, + } = event; const action = body?.action; if (action && this.checkEventType(action)) { const item = this.getBodyItem(body); @@ -29,9 +32,8 @@ export default { const summary = this.getSummary(action, item); this.$emit({ - action, - [item]: item, - sender: body.sender, + ...body, + ...getRelevantHeaders(headers), }, { id, summary, diff --git a/components/github/sources/common/common-flex.mjs b/components/github/sources/common/common-flex.mjs index 9a036cec59a9f..4320a8b197c88 100644 --- a/components/github/sources/common/common-flex.mjs +++ b/components/github/sources/common/common-flex.mjs @@ -1,6 +1,7 @@ import github from "../../github.app.mjs"; import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; -import { checkAdminPermission } from "./utils.mjs"; +import { getRelevantHeaders } from "./utils.mjs"; +import { checkAdminPermission } from "../../common/utils.mjs"; export default { props: { @@ -15,18 +16,24 @@ export default { db: "$.service.db", }, async additionalProps() { + const getDocsInfo = (docsLink) => ({ + type: "alert", + alertType: "info", + content: `[See the GitHub documentation](${docsLink}) for more information on the event format.`, + }); if (await this.checkAdminPermission()) { return { http: { type: "$.interface.http", }, ...this.getHttpAdditionalProps(), + docsInfo: getDocsInfo(this.getHttpDocsLink()), }; } else { return { info: { type: "alert", - alertType: "info", + alertType: "warning", content: "Admin rights on the repo are required in order to register webhooks. In order to continue setting up your source, configure a polling interval below to check for new events.", }, timer: { @@ -36,6 +43,7 @@ export default { }, }, ...this.getTimerAdditionalProps(), + docsInfo: getDocsInfo(this.getTimerDocsLink()), }; } }, @@ -139,24 +147,30 @@ export default { return items; }, emitEvent({ - id, item, + id, item, headers = {}, }) { const ts = Date.now(); const summary = this.getSummary(item); - this.$emit(item, { + this.$emit({ + ...item, + ...getRelevantHeaders(headers), + }, { id, summary, ts, }); }, async onWebhookTrigger(event) { - const { body } = event; + const { + body, headers, + } = event; if (this.shouldEmitWebhookEvent(body)) { const item = this.getWebhookEventItem(body); const id = this.getId(item); this.emitEvent({ id, item, + headers, }); } }, @@ -188,6 +202,12 @@ export default { this._setSavedItems(savedItems); }, + getHttpDocsLink() { + return "https://docs.github.com/en/webhooks/webhook-events-and-payloads"; + }, + getTimerDocsLink() { + return "https://docs.github.com/en/rest?apiVersion=2022-11-28"; + }, }, hooks: { async activate() { diff --git a/components/github/sources/common/common-polling-pr-notifications.mjs b/components/github/sources/common/common-polling-pr-notifications.mjs new file mode 100644 index 0000000000000..e971d806038b7 --- /dev/null +++ b/components/github/sources/common/common-polling-pr-notifications.mjs @@ -0,0 +1,41 @@ +import common from "./common-polling.mjs"; + +export default { + ...common, + methods: { + ...common.methods, + async getAndProcessData(maxEmits = 0) { + const savedIds = this._getSavedIds(); + const items = await this.getItems(); + + const urlData = new Map(); + let amountEmits = 0; + + const promises = items?. + filter?.((item) => !savedIds.includes(this.getItemId(item))) + .map((item) => (async () => { + if (item?.subject?.notification !== null) { + const url = item.subject.url; + if (!urlData.has(url)) { + urlData.set(url, await this.github.getFromUrl({ + url: item.subject.url, + })); + } + const pullRequest = urlData.get(url); + if (!maxEmits || (amountEmits < maxEmits)) { + this.$emit(pullRequest, { + id: pullRequest.id, + ...this.getItemMetadata(pullRequest), + }); + amountEmits++; + } + } + savedIds.push(this.getItemId(item)); + })()); + + if (promises?.length) await Promise.allSettled(promises); + + this._setSavedIds(savedIds); + }, + }, +}; diff --git a/components/github/sources/common/common-polling.mjs b/components/github/sources/common/common-polling.mjs index 7c58a15f35955..a6a56840cc9a5 100644 --- a/components/github/sources/common/common-polling.mjs +++ b/components/github/sources/common/common-polling.mjs @@ -12,4 +12,39 @@ export default { }, db: "$.service.db", }, + methods: { + _getSavedIds() { + return this.db.get("savedIds") || []; + }, + _setSavedIds(value) { + this.db.set("savedIds", value); + }, + getItemId(item) { + return item.id; + }, + async getAndProcessData(maxEmits = 0) { + const savedIds = this._getSavedIds(); + const items = await this.getItems(); + + items?.filter?.((item) => !savedIds.includes(this.getItemId(item))).forEach((item, index) => { + if ((!maxEmits) || (index < maxEmits)) { + this.$emit(item, { + id: this.getItemId(item), + ...this.getItemMetadata(item), + }); + } + savedIds.push(this.getItemId(item)); + }); + + this._setSavedIds(savedIds); + }, + }, + hooks: { + async deploy() { + await this.getAndProcessData(5); + }, + }, + async run() { + await this.getAndProcessData(); + }, }; diff --git a/components/github/sources/common/common-webhook-orgs.mjs b/components/github/sources/common/common-webhook-orgs.mjs index e4ffd0266358d..1393c6fb4cc40 100644 --- a/components/github/sources/common/common-webhook-orgs.mjs +++ b/components/github/sources/common/common-webhook-orgs.mjs @@ -1,4 +1,6 @@ +import { ConfigurationError } from "@pipedream/platform"; import github from "../../github.app.mjs"; +import { checkOrgAdminPermission } from "./utils.mjs"; export default { props: { @@ -8,6 +10,7 @@ export default { github, "orgName", ], + reloadProps: true, }, repo: { propDefinition: [ @@ -21,6 +24,10 @@ export default { db: "$.service.db", http: "$.interface.http", }, + async additionalProps() { + await this.requireAdminPermission(); + return {}; + }, methods: { _getWebhookId() { return this.db.get("webhookId"); @@ -37,9 +44,16 @@ export default { loadHistoricalEvents() { return true; }, + checkOrgAdminPermission, + async requireAdminPermission() { + if (!await this.checkOrgAdminPermission()) { + throw new ConfigurationError("Webhooks are only supported on organizations where you have admin access."); + } + }, }, hooks: { async deploy() { + await this.requireAdminPermission(); await this.loadHistoricalEvents(); }, async activate() { diff --git a/components/github/sources/common/common-webhook.mjs b/components/github/sources/common/common-webhook.mjs index 897b54703d95f..fa022146905bb 100644 --- a/components/github/sources/common/common-webhook.mjs +++ b/components/github/sources/common/common-webhook.mjs @@ -1,6 +1,6 @@ import { ConfigurationError } from "@pipedream/platform"; import github from "../../github.app.mjs"; -import { checkAdminPermission } from "./utils.mjs"; +import { checkAdminPermission } from "../../common/utils.mjs"; export default { props: { diff --git a/components/github/sources/common/constants.mjs b/components/github/sources/common/constants.mjs index bb24d00b44a35..d94be0e9f455a 100644 --- a/components/github/sources/common/constants.mjs +++ b/components/github/sources/common/constants.mjs @@ -1,6 +1,10 @@ export default { HISTORICAL_EVENTS: 25, - ISSUE_TYPE: "ISSUE", + PROJECT_ITEM_TYPES: [ + "Issue", + "PullRequest", + "DraftIssue", + ], REPOSITORY_WEBHOOK_EVENTS: [ { label: "Activity related to a branch protection rule", @@ -345,4 +349,17 @@ export default { value: "opened", }, ], + PULL_REQUEST_REVIEW_STATES: [ + "approved", + "changes_requested", + "commented", + ], +}; + +export const SAMPLE_GITHUB_HEADERS = { + github_headers: { + "x-github-delivery": "string", + "x-github-event": "string", + "x-github-hook-id": "string", + }, }; diff --git a/components/github/sources/common/utils.mjs b/components/github/sources/common/utils.mjs index dd3f1a8bad7ef..e5a0f8eafd2b3 100644 --- a/components/github/sources/common/utils.mjs +++ b/components/github/sources/common/utils.mjs @@ -1,9 +1,21 @@ -export async function checkAdminPermission() { - const { repoFullname } = this; +export async function checkOrgAdminPermission() { + const { org } = this; const { login: username } = await this.github.getAuthenticatedUser(); - const { user: { permissions: { admin } } } = await this.github.getUserRepoPermissions({ - repoFullname, + const { role } = await this.github.getOrgUserInfo({ + org, username, }); - return admin; + return role === "admin"; +} + +export function getRelevantHeaders(headers = {}) { + return Object.keys(headers)?.length + ? { + github_headers: { + "x-github-delivery": headers["x-github-delivery"], + "x-github-event": headers["x-github-event"], + "x-github-hook-id": headers["x-github-hook-id"], + }, + } + : {}; } diff --git a/components/github/sources/new-branch/common-sample-events.mjs b/components/github/sources/new-branch/common-sample-events.mjs index 9e3816a7e783a..b95fe5ab5dce4 100644 --- a/components/github/sources/new-branch/common-sample-events.mjs +++ b/components/github/sources/new-branch/common-sample-events.mjs @@ -1,5 +1,8 @@ +import { SAMPLE_GITHUB_HEADERS } from "../common/constants.mjs"; + export function getSampleWebhookEvent() { return { + ...SAMPLE_GITHUB_HEADERS, ref: "branch_name", ref_type: "branch", master_branch: "main", diff --git a/components/github/sources/new-branch/new-branch.mjs b/components/github/sources/new-branch/new-branch.mjs index dc26230a350b3..7129b182e55d9 100644 --- a/components/github/sources/new-branch/new-branch.mjs +++ b/components/github/sources/new-branch/new-branch.mjs @@ -3,15 +3,12 @@ import { getSampleTimerEvent, getSampleWebhookEvent, } from "./common-sample-events.mjs"; -const DOCS_LINK = - "https://docs.github.com/en/webhooks/webhook-events-and-payloads#create"; - export default { ...common, key: "github-new-branch", - name: "New Branch", - description: `Emit new event when a branch is created [See the documentation](${DOCS_LINK})`, - version: "1.0.1", + name: "New Branch Created", + description: "Emit new event when a branch is created.", + version: "1.0.9", type: "source", dedupe: "unique", methods: { @@ -35,5 +32,11 @@ export default { getPollingData(args) { return this.github.getBranches(args); }, + getHttpDocsLink() { + return "https://docs.github.com/en/webhooks/webhook-events-and-payloads#create"; + }, + getTimerDocsLink() { + return "https://docs.github.com/en/rest/branches/branches?apiVersion=2022-11-28#list-branches"; + }, }, }; diff --git a/components/github/sources/new-card-in-column/new-card-in-column.mjs b/components/github/sources/new-card-in-column/new-card-in-column.mjs index c1501205182aa..1a98ba066163e 100644 --- a/components/github/sources/new-card-in-column/new-card-in-column.mjs +++ b/components/github/sources/new-card-in-column/new-card-in-column.mjs @@ -1,12 +1,13 @@ import common from "../common/common-webhook.mjs"; import constants from "../common/constants.mjs"; +import { getRelevantHeaders } from "../common/utils.mjs"; export default { ...common, key: "github-new-card-in-column", name: "New Card in Column (Classic Projects)", description: "Emit new event when a (classic) project card is created or moved to a specific column. For Projects V2 use `New Issue with Status` trigger. [More information here](https://docs.github.com/en/issues/organizing-your-work-with-project-boards/tracking-work-with-project-boards/adding-issues-and-pull-requests-to-a-project-board)", - version: "1.0.1", + version: "1.0.8", type: "source", props: { ...common.props, @@ -66,10 +67,14 @@ export default { per_page: constants.HISTORICAL_EVENTS, }); for (const card of cards) { - await this.processCard(card); + await this.processCard({ + card, + }); } }, - async processCard(card) { + async processCard({ + card, headers = {}, + }) { const meta = this.generateMeta(card); const issue = await this.github.getIssueFromProjectCard({ repoFullname: this.repoFullname, @@ -78,11 +83,14 @@ export default { this.$emit({ card, issue, + ...getRelevantHeaders(headers), }, meta); }, }, async run(event) { - const card = event.body.project_card; + const { + headers, body: { project_card: card }, + } = event; if (!card) { console.log("No card in event. Skipping event."); return; @@ -93,6 +101,9 @@ export default { return; } - await this.processCard(card); + await this.processCard({ + card, + headers, + }); }, }; diff --git a/components/github/sources/new-collaborator/common-sample-events.mjs b/components/github/sources/new-collaborator/common-sample-events.mjs index 2f5735b53c3c4..542c15650bc5c 100644 --- a/components/github/sources/new-collaborator/common-sample-events.mjs +++ b/components/github/sources/new-collaborator/common-sample-events.mjs @@ -1,5 +1,8 @@ +import { SAMPLE_GITHUB_HEADERS } from "../common/constants.mjs"; + export function getSampleWebhookEvent() { return { + ...SAMPLE_GITHUB_HEADERS, login: "octocat", id: 1, node_id: "MDQ6VXNlcjE=", diff --git a/components/github/sources/new-collaborator/new-collaborator.mjs b/components/github/sources/new-collaborator/new-collaborator.mjs index 91c73d5e37d17..38a7de15340a5 100644 --- a/components/github/sources/new-collaborator/new-collaborator.mjs +++ b/components/github/sources/new-collaborator/new-collaborator.mjs @@ -3,15 +3,12 @@ import { getSampleTimerEvent, getSampleWebhookEvent, } from "./common-sample-events.mjs"; -const DOCS_LINK = - "https://docs.github.com/en/webhooks/webhook-events-and-payloads#member"; - export default { ...common, key: "github-new-collaborator", name: "New Collaborator", - description: `Emit new event when a collaborator is added [See the documentation](${DOCS_LINK})`, - version: "1.0.1", + description: "Emit new event when a collaborator is added", + version: "1.0.9", type: "source", dedupe: "unique", methods: { @@ -35,5 +32,11 @@ export default { getPollingData(args) { return this.github.getRepositoryLatestCollaborators(args); }, + getHttpDocsLink() { + return "https://docs.github.com/en/webhooks/webhook-events-and-payloads#member"; + }, + getTimerDocsLink() { + return "https://docs.github.com/en/rest/collaborators/collaborators?apiVersion=2022-11-28#list-repository-collaborators"; + }, }, }; diff --git a/components/github/sources/new-commit-comment/common-sample-events.mjs b/components/github/sources/new-commit-comment/common-sample-events.mjs index ee6968abe16dc..57ca7dc47a717 100644 --- a/components/github/sources/new-commit-comment/common-sample-events.mjs +++ b/components/github/sources/new-commit-comment/common-sample-events.mjs @@ -1,5 +1,8 @@ +import { SAMPLE_GITHUB_HEADERS } from "../common/constants.mjs"; + export function getSampleWebhookEvent() { return { + ...SAMPLE_GITHUB_HEADERS, html_url: "https://github.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e#commitcomment-1", url: "https://api.github.com/repos/octocat/Hello-World/comments/1", diff --git a/components/github/sources/new-commit-comment/new-commit-comment.mjs b/components/github/sources/new-commit-comment/new-commit-comment.mjs index 0c57ecc521330..9d72453a2525b 100644 --- a/components/github/sources/new-commit-comment/new-commit-comment.mjs +++ b/components/github/sources/new-commit-comment/new-commit-comment.mjs @@ -3,17 +3,22 @@ import { getSampleTimerEvent, getSampleWebhookEvent, } from "./common-sample-events.mjs"; -const DOCS_LINK = - "https://docs.github.com/en/webhooks/webhook-events-and-payloads#commit_comment"; - export default { ...common, key: "github-new-commit-comment", name: "New Commit Comment", - description: `Emit new event when a commit comment is created [See the documentation](${DOCS_LINK})`, - version: "1.0.1", + description: "Emit new event when a commit comment is created", + version: "1.0.9", type: "source", dedupe: "unique", + props: { + eventTypeInfo: { + type: "alert", + alertType: "info", + content: "**Note:** commit comments are not the same as pull request comments. [See the GitHub documentation](https://docs.github.com/en/rest/guides/working-with-comments?apiVersion=2022-11-28) for more information.", + }, + ...common.props, + }, methods: { ...common.methods, getSampleTimerEvent, @@ -35,5 +40,11 @@ export default { getPollingData(args) { return this.github.getRepositoryLatestCommitComments(args); }, + getHttpDocsLink() { + return "https://docs.github.com/en/webhooks/webhook-events-and-payloads#commit_comment"; + }, + getTimerDocsLink() { + return "https://docs.github.com/en/rest/commits/comments?apiVersion=2022-11-28#list-commit-comments-for-a-repository"; + }, }, }; diff --git a/components/github/sources/new-commit/common-sample-events.mjs b/components/github/sources/new-commit/common-sample-events.mjs index b6fa56453d1ab..672efb9193029 100644 --- a/components/github/sources/new-commit/common-sample-events.mjs +++ b/components/github/sources/new-commit/common-sample-events.mjs @@ -1,5 +1,8 @@ +import { SAMPLE_GITHUB_HEADERS } from "../common/constants.mjs"; + export function getSampleWebhookEvent() { return { + ...SAMPLE_GITHUB_HEADERS, id: "6dcb09b5b57875f334f61aebed695e2e4193db5e", tree_id: "21ffc34d6bc29947ef34d4ddbecade90139dad45", distinct: true, diff --git a/components/github/sources/new-commit/new-commit.mjs b/components/github/sources/new-commit/new-commit.mjs index 4e57d804c1b54..9ca5982ea1d95 100644 --- a/components/github/sources/new-commit/new-commit.mjs +++ b/components/github/sources/new-commit/new-commit.mjs @@ -3,18 +3,20 @@ import { getSampleTimerEvent, getSampleWebhookEvent, } from "./common-sample-events.mjs"; -const DOCS_LINK = - "https://docs.github.com/en/webhooks/webhook-events-and-payloads#push"; - export default { ...common, key: "github-new-commit", name: "New Commit", - description: `Emit new event when commits are pushed to a branch [See the documentation](${DOCS_LINK})`, - version: "1.0.1", + description: "Emit new event when commits are pushed to a branch", + version: "1.0.10", type: "source", dedupe: "unique", props: { + eventTypeInfo: { + type: "alert", + alertType: "info", + content: "**Note:** one event is emitted for each individual commit, even if they are received at the same time.", + }, ...common.props, branch: { propDefinition: [ @@ -45,12 +47,15 @@ export default { return `New commit: ${item.commit?.message ?? item.message}`; }, async onWebhookTrigger(event) { - const { body } = event; + const { + body, headers, + } = event; if (body?.ref?.split?.("refs/heads/").pop() === this.branch.split("/").pop()) { body.commits.forEach((commit) => { const { id } = commit; this.emitEvent({ id, + headers, item: commit, }); }); @@ -92,5 +97,11 @@ export default { this._setSavedItems(savedItems); this._setLastTimestamp(Date.now()); }, + getHttpDocsLink() { + return "https://docs.github.com/en/webhooks/webhook-events-and-payloads#push"; + }, + getTimerDocsLink() { + return "https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#list-commits"; + }, }, }; diff --git a/components/github/sources/new-discussion/common-sample-events.mjs b/components/github/sources/new-discussion/common-sample-events.mjs index a16d550200aa2..b9111d24b0dc4 100644 --- a/components/github/sources/new-discussion/common-sample-events.mjs +++ b/components/github/sources/new-discussion/common-sample-events.mjs @@ -1,5 +1,8 @@ +import { SAMPLE_GITHUB_HEADERS } from "../common/constants.mjs"; + export function getSampleWebhookEvent() { return { + ...SAMPLE_GITHUB_HEADERS, repository_url: "https://api.github.com/repos/octocat/Hello-World", category: { id: 40911353, diff --git a/components/github/sources/new-discussion/new-discussion.mjs b/components/github/sources/new-discussion/new-discussion.mjs index a55086a228faf..b49f996fbd724 100644 --- a/components/github/sources/new-discussion/new-discussion.mjs +++ b/components/github/sources/new-discussion/new-discussion.mjs @@ -3,15 +3,12 @@ import { getSampleTimerEvent, getSampleWebhookEvent, } from "./common-sample-events.mjs"; -const DOCS_LINK = - "https://docs.github.com/en/webhooks/webhook-events-and-payloads#discussion"; - export default { ...common, key: "github-new-discussion", name: "New Discussion", - description: `Emit new event when a discussion is created [See the documentation](${DOCS_LINK})`, - version: "1.0.1", + description: "Emit new event when a discussion is created", + version: "1.0.9", type: "source", dedupe: "unique", methods: { @@ -42,5 +39,11 @@ export default { return dateA - dateB; }); }, + getHttpDocsLink() { + return "https://docs.github.com/en/webhooks/webhook-events-and-payloads#discussion"; + }, + getTimerDocsLink() { + return "https://docs.github.com/en/graphql/reference/objects#discussion"; + }, }, }; diff --git a/components/github/sources/new-fork/common-sample-events.mjs b/components/github/sources/new-fork/common-sample-events.mjs index 92d6aa77955ca..ca7e37fcaeda5 100644 --- a/components/github/sources/new-fork/common-sample-events.mjs +++ b/components/github/sources/new-fork/common-sample-events.mjs @@ -1,5 +1,8 @@ +import { SAMPLE_GITHUB_HEADERS } from "../common/constants.mjs"; + export function getSampleWebhookEvent() { return { + ...SAMPLE_GITHUB_HEADERS, id: 1296269, node_id: "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", name: "Hello-World", diff --git a/components/github/sources/new-fork/new-fork.mjs b/components/github/sources/new-fork/new-fork.mjs index 1b151185ee455..808a434e281e0 100644 --- a/components/github/sources/new-fork/new-fork.mjs +++ b/components/github/sources/new-fork/new-fork.mjs @@ -3,15 +3,12 @@ import { getSampleTimerEvent, getSampleWebhookEvent, } from "./common-sample-events.mjs"; -const DOCS_LINK = - "https://docs.github.com/en/webhooks/webhook-events-and-payloads#fork"; - export default { ...common, key: "github-new-fork", name: "New Fork", - description: `Emit new event when a repository is forked [See the documentation](${DOCS_LINK})`, - version: "1.0.1", + description: "Emit new event when a repository is forked", + version: "1.0.9", type: "source", dedupe: "unique", methods: { @@ -35,5 +32,11 @@ export default { getPollingData(args) { return this.github.getRepositoryForks(args); }, + getHttpDocsLink() { + return "https://docs.github.com/en/webhooks/webhook-events-and-payloads#fork"; + }, + getTimerDocsLink() { + return "https://docs.github.com/en/rest/repos/forks?apiVersion=2022-11-28#list-forks"; + }, }, }; diff --git a/components/github/sources/new-gist/new-gist.mjs b/components/github/sources/new-gist/new-gist.mjs index 10d88f8c2385c..dd6c444f0c857 100644 --- a/components/github/sources/new-gist/new-gist.mjs +++ b/components/github/sources/new-gist/new-gist.mjs @@ -4,19 +4,20 @@ export default { ...common, key: "github-new-gist", name: "New Gist", - description: "Emit new events when new gists are created by the authenticated user", - version: "0.1.15", + description: "Emit new events when new gists are created by the authenticated user. [See the documentatoion](https://docs.github.com/en/rest/gists/gists?apiVersion=20.2.31-28#list-gists-for-the-authenticated-user)", + version: "0.2.3", type: "source", dedupe: "unique", - async run() { - const gists = await this.github.getGists(); - - gists.map((gist) => { - this.$emit(gist, { - id: gist.id, - summary: `New gist ${gist.id}`, - ts: Date.parse(gist.created_at), - }); - }); + methods: { + ...common.methods, + async getItems() { + return this.github.getGists(); + }, + getItemMetadata(item) { + return { + summary: `New gist: ${item.id}`, + ts: Date.parse(item.created_at), + }; + }, }, }; diff --git a/components/github/sources/new-issue-with-status/new-issue-with-status.mjs b/components/github/sources/new-issue-with-status/new-issue-with-status.mjs index 5c7206fac4c22..cf323f3b3fce5 100644 --- a/components/github/sources/new-issue-with-status/new-issue-with-status.mjs +++ b/components/github/sources/new-issue-with-status/new-issue-with-status.mjs @@ -1,13 +1,14 @@ import queries from "../../common/queries.mjs"; import common from "../common/common-webhook-orgs.mjs"; import constants from "../common/constants.mjs"; +import { getRelevantHeaders } from "../common/utils.mjs"; export default { ...common, key: "github-new-issue-with-status", - name: "New Issue with Status (Projects V2)", - description: "Emit new event when a project issue is tagged with a specific status. Currently supports Organization Projects only. [More information here](https://docs.github.com/en/issues/planning-and-tracking-with-projects/managing-items-in-your-project/adding-items-to-your-project)", - version: "0.0.16", + name: "Project Item Status Changed", + description: "Emit new event when a project item is tagged with a specific status. Currently supports Organization Projects only. [More information here](https://docs.github.com/en/issues/planning-and-tracking-with-projects/managing-items-in-your-project/adding-items-to-your-project)", + version: "0.1.6", type: "source", dedupe: "unique", props: { @@ -38,6 +39,14 @@ export default { project: c.project, }), ], + optional: true, + }, + itemType: { + type: "string[]", + label: "Filter Item Type", + description: "The item type(s) to emit events or. If not specified, events will be emitted for all item types.", + optional: true, + options: constants.PROJECT_ITEM_TYPES, }, }, methods: { @@ -47,32 +56,33 @@ export default { "projects_v2_item", ]; }, - generateMeta(issue, statusName) { - const { number } = issue; - const ts = Date.parse(issue.updated_at); + generateMeta(item, statusName) { + const { id } = item; + const ts = Date.parse(item.updated_at); return { - id: `${number}-${ts}`, - summary: `Issue #${number} in ${statusName} status`, + id: `${id}-${ts}`, + summary: `Item #${id} to status "${statusName}"`, ts, }; }, - isRelevant(item, issueNumber, statusName) { + isRelevant(event) { + const fieldChanged = event.changes?.field_value?.field_name; + if (fieldChanged !== "Status") { + return; + } + let isRelevant = true; let message = ""; - const { - type, - isArchived, - fieldValueByName: { optionId }, - } = item; - if (type !== constants.ISSUE_TYPE) { - message = `Not an issue: ${type}. Skipping...`; - isRelevant = false; - } else if (isArchived) { - message = "Issue is archived. Skipping..."; + const statusId = event.changes.field_value.to.id; + const itemType = event.projects_v2_item.content_type; + + if (this.status?.length && !this.status.includes(statusId)) { + const statusName = event.changes.field_value.to.name; + message = `Status "${statusName}". Skipping...`; isRelevant = false; - } else if (optionId !== this.status) { - message = `Issue #${issueNumber} in ${statusName} status. Skipping...`; + } else if (this.itemType?.length && !this.itemType.includes(itemType)) { + message = `Item type "${itemType}". Skipping...`; isRelevant = false; } @@ -85,61 +95,36 @@ export default { }); return node; }, - async processEvent(event) { - const item = await this.getProjectItem({ - nodeId: event.projects_v2_item.node_id, - }); + async processEvent({ + event, headers, + }) { + const item = event.projects_v2_item; - const issueNumber = item.content.number; - const statusName = item.fieldValueByName.name; - - if (!this.isRelevant(item, issueNumber, statusName)) { + if (!this.isRelevant(event)) { return; } - const repoName = this.repo ?? item.content.repository.name; - - const issue = await this.github.getIssue({ - repoFullname: `${this.org}/${repoName}`, - issueNumber, - }); - - console.log(`Emitting issue #${issueNumber}`); - const meta = this.generateMeta(issue, statusName); - this.$emit(issue, meta); - }, - async loadHistoricalEvents() { - const response = await this.github.graphql(this.repo ? - queries.projectItemsQuery : - queries.organizationProjectItemsQuery, - { - repoOwner: this.org, - repoName: this.repo, - project: this.project, - historicalEventsNumber: constants.HISTORICAL_EVENTS, - }); + console.log(`Emitting item #${item.id}`); - const items = response?.repository?.projectV2?.items?.nodes ?? - response?.organization?.projectV2?.items?.nodes; - - for (const node of items) { - if (node.type === constants.ISSUE_TYPE) { - const event = { - projects_v2_item: { - node_id: node.id, - }, - }; - await this.processEvent(event); - } - } + const statusName = event.changes.field_value.to.name; + const meta = this.generateMeta(item, statusName); + this.$emit({ + ...event, + ...getRelevantHeaders(headers), + }, meta); }, }, - async run({ body: event }) { + async run({ + body: event, headers, + }) { if (event.zen) { console.log(event.zen); return; } - await this.processEvent(event); + await this.processEvent({ + event, + headers, + }); }, }; diff --git a/components/github/sources/new-label/common-sample-events.mjs b/components/github/sources/new-label/common-sample-events.mjs index 6e43570642d95..1e11349894436 100644 --- a/components/github/sources/new-label/common-sample-events.mjs +++ b/components/github/sources/new-label/common-sample-events.mjs @@ -1,5 +1,8 @@ +import { SAMPLE_GITHUB_HEADERS } from "../common/constants.mjs"; + export function getSampleWebhookEvent() { return { + ...SAMPLE_GITHUB_HEADERS, id: 208045947, node_id: "MDU6TGFiZWwyMDgwNDU5NDc=", url: "https://api.github.com/repos/octocat/Hello-World/labels/enhancement", diff --git a/components/github/sources/new-label/new-label.mjs b/components/github/sources/new-label/new-label.mjs index da0436ebae574..e753c46f4f329 100644 --- a/components/github/sources/new-label/new-label.mjs +++ b/components/github/sources/new-label/new-label.mjs @@ -3,15 +3,12 @@ import { getSampleTimerEvent, getSampleWebhookEvent, } from "./common-sample-events.mjs"; -const DOCS_LINK = - "https://docs.github.com/en/webhooks/webhook-events-and-payloads#label"; - export default { ...common, key: "github-new-label", name: "New Label", - description: `Emit new event when a new label is created [See the documentation](${DOCS_LINK})`, - version: "1.0.1", + description: "Emit new event when a new label is created", + version: "1.0.9", type: "source", dedupe: "unique", methods: { @@ -35,5 +32,11 @@ export default { getPollingData(args) { return this.github.getRepositoryLatestLabels(args); }, + getHttpDocsLink() { + return "https://docs.github.com/en/webhooks/webhook-events-and-payloads#label"; + }, + getTimerDocsLink() { + return "https://docs.github.com/en/rest/issues/labels?apiVersion=2022-11-28#list-labels-for-a-repository"; + }, }, }; diff --git a/components/github/sources/new-mention/new-mention.mjs b/components/github/sources/new-mention/new-mention.mjs index 618a8a78156c4..685b55e56dcd4 100644 --- a/components/github/sources/new-mention/new-mention.mjs +++ b/components/github/sources/new-mention/new-mention.mjs @@ -4,15 +4,9 @@ export default { ...common, key: "github-new-mention", name: "New Mention", - description: "Emit new events when you are @mentioned in a new commit, comment, issue or pull request", - version: "0.1.16", + description: "Emit new event when you are @mentioned in a new commit, comment, issue or pull request. [See the documentation](https://docs.github.com/en/rest/activity/notifications?apiVersion=20.2.31-28#list-notifications-for-the-authenticated-user)", + version: "0.2.3", type: "source", - hooks: { - async activate() { - const user = await this.github.getAuthenticatedUser(); - this._setUserLogin(user.login); - }, - }, dedupe: "unique", methods: { ...common.methods, @@ -22,38 +16,78 @@ export default { _setUserLogin(userLogin) { this.db.set("userLogin", userLogin); }, - }, - async run() { - const login = this._getUserLogin(); - - const notifications = await this.github.getFilteredNotifications({ - reason: "mention", - data: { - participating: true, - all: true, - }, - }); - - for (const notification of notifications) { - const subject = await this.github.getFromUrl({ - url: notification?.subject?.url, + _getLastDate() { + return this.db.get("lastDate"); + }, + _setLastDate(value) { + this.db.set("lastDate", value); + }, + async retrieveUserLogin() { + let login = this._getUserLogin(); + if (!login) { + const user = await this.github.getAuthenticatedUser(); + login = user.login; + this._setUserLogin(login); + } + return login; + }, + async getItems() { + const date = this._getLastDate(); + this._setLastDate(new Date().toISOString()); + return this.github.getFilteredNotifications({ + reason: "mention", + data: { + participating: true, + all: true, + ...(date && { + since: date, + }), + }, }); + }, + async getAndProcessData(maxEmits = 0) { + const login = await this.retrieveUserLogin(); + const savedIds = this._getSavedIds(); + const items = await this.getItems(); - if (!subject.comments_url) continue; + const urlData = new Map(); + let amountEmits = 0; - const comments = await this.github.getFromUrl({ - url: subject.comments_url, - }); + const promises = items?.map((item) => (async () => { + const url = item?.subject?.url; + if (!urlData.has(url)) { + urlData.set(url, await this.github.getFromUrl({ + url: item.subject.url, + })); + } + const subject = urlData.get(url); + const commentsUrl = subject.comments_url; + if (!commentsUrl) return; - for (const comment of comments) { - if (comment?.body?.includes(`@${login}`)) { - this.$emit(comment, { - id: comment.id, - summary: `New notification ${comment.id}`, - ts: Date.parse(comment.created_at), - }); + if (!urlData.has(commentsUrl)) { + urlData.set(commentsUrl, await this.github.getFromUrl({ + url: commentsUrl, + })); } - } - } + const comments = urlData.get(commentsUrl); + comments?.filter?.((comment) => { + return !savedIds.includes(comment.id) && comment.body?.includes(`@${login}`); + }).forEach((comment) => { + if (!maxEmits || (amountEmits < maxEmits)) { + this.$emit(comment, { + id: comment.id, + summary: `New mention: ${comment.id}`, + ts: Date.parse(comment.created_at), + }); + amountEmits++; + } + savedIds.push(comment.id); + }); + })()); + + if (promises?.length) await Promise.allSettled(promises); + + this._setSavedIds(savedIds); + }, }, }; diff --git a/components/github/sources/new-notification/new-notification.mjs b/components/github/sources/new-notification/new-notification.mjs index 47da8d845df81..fde2f32bc0f49 100644 --- a/components/github/sources/new-notification/new-notification.mjs +++ b/components/github/sources/new-notification/new-notification.mjs @@ -4,24 +4,25 @@ export default { ...common, key: "github-new-notification", name: "New Notification", - description: "Emit new events when you received a new notification", - version: "0.1.15", + description: "Emit new event when the authenticated user receives a new notification. [See the documentation](https://docs.github.com/en/rest/activity/notifications?apiVersion=20.2.31-28#list-notifications-for-the-authenticated-user)", + version: "0.2.3", type: "source", dedupe: "unique", - async run() { - const notifications = await this.github.getFilteredNotifications({ - data: { - participating: true, - all: true, - }, - }); - - notifications.map((notification) => { - this.$emit(notification, { - id: notification.id, - summary: `New notification ${notification.id}`, - ts: Date.parse(notification.created_at), + methods: { + ...common.methods, + async getItems() { + return this.github.getFilteredNotifications({ + data: { + participating: true, + all: true, + }, }); - }); + }, + getItemMetadata(item) { + return { + summary: `New notification: ${item.id}`, + ts: Date.parse(item.created_at), + }; + }, }, }; diff --git a/components/github/sources/new-or-updated-issue/common-sample-events.mjs b/components/github/sources/new-or-updated-issue/common-sample-events.mjs index 9026736565d0b..e36253c4d9770 100644 --- a/components/github/sources/new-or-updated-issue/common-sample-events.mjs +++ b/components/github/sources/new-or-updated-issue/common-sample-events.mjs @@ -1,5 +1,8 @@ +import { SAMPLE_GITHUB_HEADERS } from "../common/constants.mjs"; + export function getSampleWebhookEvent() { return { + ...SAMPLE_GITHUB_HEADERS, action: "opened", issue: { url: "https://api.github.com/repos/octocat/Hello-World/issues/1347", diff --git a/components/github/sources/new-or-updated-issue/new-or-updated-issue.mjs b/components/github/sources/new-or-updated-issue/new-or-updated-issue.mjs index 8fcb2f861cfac..c1616193220b5 100644 --- a/components/github/sources/new-or-updated-issue/new-or-updated-issue.mjs +++ b/components/github/sources/new-or-updated-issue/new-or-updated-issue.mjs @@ -4,15 +4,12 @@ import { getSampleTimerEvent, getSampleWebhookEvent, } from "./common-sample-events.mjs"; -const DOCS_LINK = - "https://docs.github.com/en/webhooks-and-events/webhooks/webhook-events-and-payloads#issues"; - export default { ...common, key: "github-new-or-updated-issue", name: "New or Updated Issue", - description: `Emit new events when an issue is created or updated [See the documentation](${DOCS_LINK})`, - version: "1.0.1", + description: "Emit new events when an issue is created or updated", + version: "1.1.6", type: "source", dedupe: "unique", methods: { @@ -23,7 +20,7 @@ export default { type: "string[]", label: "Filter Event Types", optional: true, - description: `Specify the type(s) of activity that should emit events. [See the documentation](${DOCS_LINK}) for more information on each type. By default, events will be emitted for all activity.`, + description: "Specify the type(s) of activity that should emit events. By default, events will be emitted for all activity.", options: constants.EVENT_TYPES_ISSUES, }, }; @@ -49,5 +46,11 @@ export default { sort, }); }, + getHttpDocsLink() { + return "https://docs.github.com/en/webhooks/webhook-events-and-payloads#issues"; + }, + getTimerDocsLink() { + return "https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#list-repository-issues"; + }, }, }; diff --git a/components/github/sources/new-or-updated-milestone/common-sample-events.mjs b/components/github/sources/new-or-updated-milestone/common-sample-events.mjs index d39306054d291..b7c413b6c25bc 100644 --- a/components/github/sources/new-or-updated-milestone/common-sample-events.mjs +++ b/components/github/sources/new-or-updated-milestone/common-sample-events.mjs @@ -1,5 +1,8 @@ +import { SAMPLE_GITHUB_HEADERS } from "../common/constants.mjs"; + export function getSampleWebhookEvent() { return { + ...SAMPLE_GITHUB_HEADERS, action: "created", milestone: { url: "https://api.github.com/repos/octocat/Hello-World/milestones/1", diff --git a/components/github/sources/new-or-updated-milestone/new-or-updated-milestone.mjs b/components/github/sources/new-or-updated-milestone/new-or-updated-milestone.mjs index 6253193177fbc..44247a21fbfec 100644 --- a/components/github/sources/new-or-updated-milestone/new-or-updated-milestone.mjs +++ b/components/github/sources/new-or-updated-milestone/new-or-updated-milestone.mjs @@ -4,15 +4,12 @@ import { getSampleTimerEvent, getSampleWebhookEvent, } from "./common-sample-events.mjs"; -const DOCS_LINK = - "https://docs.github.com/en/webhooks-and-events/webhooks/webhook-events-and-payloads#milestone"; - export default { ...common, key: "github-new-or-updated-milestone", name: "New or Updated Milestone", - description: `Emit new events when a milestone is created or updated [See the documentation](${DOCS_LINK})`, - version: "1.0.1", + description: "Emit new event when a milestone is created or updated", + version: "1.1.6", type: "source", dedupe: "unique", methods: { @@ -23,7 +20,7 @@ export default { type: "string[]", label: "Filter Event Types", optional: true, - description: `Specify the type(s) of activity that should emit events. [See the documentation](${DOCS_LINK}) for more information on each type. By default, events will be emitted for all activity.`, + description: "Specify the type(s) of activity that should emit events. By default, events will be emitted for all activity.", options: constants.EVENT_TYPES_MILESTONES, }, }; @@ -46,5 +43,11 @@ export default { repoFullname, }); }, + getHttpDocsLink() { + return "https://docs.github.com/en/webhooks/webhook-events-and-payloads#milestone"; + }, + getTimerDocsLink() { + return "https://docs.github.com/en/rest/issues/milestones?apiVersion=2022-11-28#list-milestones"; + }, }, }; diff --git a/components/github/sources/new-or-updated-pull-request/common-sample-events.mjs b/components/github/sources/new-or-updated-pull-request/common-sample-events.mjs index 6e9ecd2ccd3f7..3b6d0448bbbec 100644 --- a/components/github/sources/new-or-updated-pull-request/common-sample-events.mjs +++ b/components/github/sources/new-or-updated-pull-request/common-sample-events.mjs @@ -1,5 +1,8 @@ +import { SAMPLE_GITHUB_HEADERS } from "../common/constants.mjs"; + export function getSampleWebhookEvent() { return { + ...SAMPLE_GITHUB_HEADERS, action: "closed", pull_request: { url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", diff --git a/components/github/sources/new-or-updated-pull-request/new-or-updated-pull-request.mjs b/components/github/sources/new-or-updated-pull-request/new-or-updated-pull-request.mjs index 7a9d924767a70..6caa83c4adf70 100644 --- a/components/github/sources/new-or-updated-pull-request/new-or-updated-pull-request.mjs +++ b/components/github/sources/new-or-updated-pull-request/new-or-updated-pull-request.mjs @@ -4,15 +4,12 @@ import { getSampleTimerEvent, getSampleWebhookEvent, } from "./common-sample-events.mjs"; -const DOCS_LINK = - "https://docs.github.com/en/webhooks-and-events/webhooks/webhook-events-and-payloads#pull_request"; - export default { ...common, key: "github-new-or-updated-pull-request", name: "New or Updated Pull Request", - description: `Emit new events when a pull request is opened or updated [See the documentation](${DOCS_LINK})`, - version: "1.1.1", + description: "Emit new event when a pull request is opened or updated", + version: "1.2.6", type: "source", dedupe: "unique", methods: { @@ -23,7 +20,7 @@ export default { type: "string[]", label: "Filter Event Types", optional: true, - description: `Specify the type(s) of activity that should emit events. [See the documentation](${DOCS_LINK}) for more information on each type. By default, events will be emitted for all activity.`, + description: "Specify the type(s) of activity that should emit events. By default, events will be emitted for all activity.", options: constants.EVENT_TYPES_PULL_REQUEST, }, }; @@ -49,5 +46,11 @@ export default { sort, }); }, + getHttpDocsLink() { + return "https://docs.github.com/en/webhooks/webhook-events-and-payloads#pull_request"; + }, + getTimerDocsLink() { + return "https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28"; + }, }, }; diff --git a/components/github/sources/new-organization/new-organization.mjs b/components/github/sources/new-organization/new-organization.mjs index 4e8e3a8be8d12..5e16d9c13400d 100644 --- a/components/github/sources/new-organization/new-organization.mjs +++ b/components/github/sources/new-organization/new-organization.mjs @@ -4,19 +4,20 @@ export default { ...common, key: "github-new-organization", name: "New Organization", - description: "Emit new events when the authenticated user is added to a new organization", - version: "0.1.15", + description: "Emit new event when the authenticated user is added to a new organization. [See the documentation](https://docs.github.com/en/rest/orgs/orgs?apiVersion=20.2.31-28#list-organizations-for-the-authenticated-user)", + version: "0.2.3", type: "source", dedupe: "unique", - async run() { - const organizations = await this.github.getOrganizations(); - - organizations.map((organization) => { - this.$emit(organization, { - id: organization.id, - summary: `New organization ${organization.id}`, - ts: new Date(), - }); - }); + methods: { + ...common.methods, + async getItems() { + return this.github.getOrganizations(); + }, + getItemMetadata(item) { + return { + summary: `New organization: "${item.login}"`, + ts: Date.now(), + }; + }, }, }; diff --git a/components/github/sources/new-release/common-sample-events.mjs b/components/github/sources/new-release/common-sample-events.mjs index 3786bf2a4b610..3588436c725aa 100644 --- a/components/github/sources/new-release/common-sample-events.mjs +++ b/components/github/sources/new-release/common-sample-events.mjs @@ -1,5 +1,8 @@ +import { SAMPLE_GITHUB_HEADERS } from "../common/constants.mjs"; + export function getSampleWebhookEvent() { return { + ...SAMPLE_GITHUB_HEADERS, url: "https://api.github.com/repos/octocat/Hello-World/releases/1", assets_url: "https://api.github.com/repos/octocat/Hello-World/releases/1/assets", diff --git a/components/github/sources/new-release/new-release.mjs b/components/github/sources/new-release/new-release.mjs index 840ed9d374bfa..05c79ad2b0b58 100644 --- a/components/github/sources/new-release/new-release.mjs +++ b/components/github/sources/new-release/new-release.mjs @@ -3,15 +3,12 @@ import { getSampleTimerEvent, getSampleWebhookEvent, } from "./common-sample-events.mjs"; -const DOCS_LINK = - "https://docs.github.com/en/webhooks/webhook-events-and-payloads#fork"; - export default { ...common, key: "github-new-release", name: "New release", - description: `Emit new event when a new release is created [See the documentation](${DOCS_LINK})`, - version: "1.0.1", + description: "Emit new event when a new release is created", + version: "1.0.9", type: "source", dedupe: "unique", methods: { @@ -38,5 +35,11 @@ export default { per_page: 100, }); }, + getHttpDocsLink() { + return "https://docs.github.com/en/webhooks/webhook-events-and-payloads#release"; + }, + getTimerDocsLink() { + return "https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28"; + }, }, }; diff --git a/components/github/sources/new-repository/new-repository.mjs b/components/github/sources/new-repository/new-repository.mjs index 96d57e43a4986..8f95903a5af49 100644 --- a/components/github/sources/new-repository/new-repository.mjs +++ b/components/github/sources/new-repository/new-repository.mjs @@ -4,19 +4,20 @@ export default { ...common, key: "github-new-repository", name: "New Repository", - description: "Emit new events when new repositories are created", - version: "0.1.15", + description: "Emit new event when a new repository is created or when the authenticated user receives access. [See the documentation](https://docs.github.com/en/rest/repos/repos?apiVersion=20.2.31-28#list-repositories-for-the-authenticated-user)", + version: "0.2.3", type: "source", dedupe: "unique", - async run() { - const repositories = await this.github.getRepos(); - - repositories.map((repository) => { - this.$emit(repository, { - id: repository.id, - summary: `New repository ${repository.id}`, - ts: Date.parse(repository.created_at), - }); - }); + methods: { + ...common.methods, + async getItems() { + return this.github.getRepos(); + }, + getItemMetadata(item) { + return { + summary: `New repository: "${item.full_name}"`, + ts: Date.now(), + }; + }, }, }; diff --git a/components/github/sources/new-review-request/new-review-request.mjs b/components/github/sources/new-review-request/new-review-request.mjs index 9a9cd03fbb269..34326b41c0a0b 100644 --- a/components/github/sources/new-review-request/new-review-request.mjs +++ b/components/github/sources/new-review-request/new-review-request.mjs @@ -1,34 +1,40 @@ -import common from "../common/common-polling.mjs"; +import common from "../common/common-polling-pr-notifications.mjs"; export default { ...common, key: "github-new-review-request", name: "New Review Request", - description: "Emit new events when you or a team you're a member of are requested to review a pull request", - version: "0.1.15", + description: "Emit new event for new review request notifications. [See the documentation](https://docs.github.com/en/rest/activity/notifications?apiVersion=20.2.31-28#list-notifications-for-the-authenticated-user)", + version: "0.2.3", type: "source", dedupe: "unique", - async run() { - const notifications = await this.github.getFilteredNotifications({ - reason: "review_requested", - data: { - participating: true, - all: true, - }, - }); - - for (const notification of notifications) { - if (notification.subject.notification === null) continue; - - const pullRequest = await this.github.getFromUrl({ - url: notification.subject.url, - }); - - this.$emit(pullRequest, { - id: pullRequest.id, - summary: `New notification ${pullRequest.id}`, - ts: Date.parse(pullRequest.created_at), + methods: { + ...common.methods, + _getLastDate() { + return this.db.get("lastDate"); + }, + _setLastDate(value) { + this.db.set("lastDate", value); + }, + async getItems() { + const date = this._getLastDate(); + this._setLastDate(new Date().toISOString()); + return this.github.getFilteredNotifications({ + reason: "review_requested", + data: { + participating: true, + all: true, + ...(date && { + since: date, + }), + }, }); - } + }, + getItemMetadata(item) { + return { + summary: `New review request: "${item.title ?? item.id}"`, + ts: Date.now(), + }; + }, }, }; diff --git a/components/github/sources/new-security-alert/new-security-alert.mjs b/components/github/sources/new-security-alert/new-security-alert.mjs index d1032bfd9ad2e..0bdddf72cb15a 100644 --- a/components/github/sources/new-security-alert/new-security-alert.mjs +++ b/components/github/sources/new-security-alert/new-security-alert.mjs @@ -4,31 +4,26 @@ export default { ...common, key: "github-new-security-alert", name: "New Security Alert", - description: "Emit new events when GitHub discovers a security vulnerability in one of your repositories", - version: "0.1.17", + description: "Emit new event for security alert notifications. [See the documentation](https://docs.github.com/en/rest/activity/notifications?apiVersion=20.2.31-28#list-notifications-for-the-authenticated-user)", + version: "0.2.3", type: "source", dedupe: "unique", - async run() { - const notifications = await this.github.getFilteredNotifications({ - reason: "security_alert", - data: { - participating: true, - all: true, - }, - }); - - for (const notification of notifications) { - if (notification.subject.notification === null) continue; - - const pullRequest = await this.github.getFromUrl({ - url: notification.subject.url, - }); - - this.$emit(pullRequest, { - id: pullRequest.id, - summary: `New notification ${pullRequest.id}`, - ts: Date.parse(pullRequest.created_at), + methods: { + ...common.methods, + async getItems() { + return this.github.getFilteredNotifications({ + reason: "security_alert", + data: { + participating: true, + all: true, + }, }); - } + }, + getItemMetadata(item) { + return { + summary: `New security alert: "${item.title ?? item.id}"`, + ts: Date.now(), + }; + }, }, }; diff --git a/components/github/sources/new-star-by-user/new-star-by-user.mjs b/components/github/sources/new-star-by-user/new-star-by-user.mjs index 5a7083b861783..549dc65152230 100644 --- a/components/github/sources/new-star-by-user/new-star-by-user.mjs +++ b/components/github/sources/new-star-by-user/new-star-by-user.mjs @@ -4,8 +4,8 @@ export default { ...common, key: "github-new-star-by-user", name: "New Star By User", - description: "Emit new events when the specified user stars a repository", - version: "0.0.2", + description: "Emit new events when the specified user stars a repository. [See the documentation](https://docs.github.com/en/rest/activity/starring?apiVersion=2022-11-28#list-repositories-starred-by-a-user)", + version: "0.0.8", type: "source", dedupe: "unique", props: { @@ -23,17 +23,15 @@ export default { const response = await this.github._client().request(`GET /users/${user}/starred`); return response.data; }, - }, - async run() { - const user = this.user ?? (await this.github.getAuthenticatedUser()).login; - const stars = await this.getUserStars(user); - - stars?.forEach((star) => { - this.$emit(star, { - id: star.id, - summary: `New star: ${star.full_name}`, + async getItems() { + const user = this.user ?? (await this.github.getAuthenticatedUser()).login; + return this.getUserStars(user); + }, + getItemMetadata(item) { + return { + summary: `New star: ${item.full_name}`, ts: Date.now(), - }); - }); + }; + }, }, }; diff --git a/components/github/sources/new-star/common-sample-events.mjs b/components/github/sources/new-star/common-sample-events.mjs index d7c5cbd7f7222..8da53578f5ed6 100644 --- a/components/github/sources/new-star/common-sample-events.mjs +++ b/components/github/sources/new-star/common-sample-events.mjs @@ -1,5 +1,8 @@ +import { SAMPLE_GITHUB_HEADERS } from "../common/constants.mjs"; + export function getSampleWebhookEvent() { return { + ...SAMPLE_GITHUB_HEADERS, action: "created", starred_at: "2024-01-04T22:15:09Z", sender: { diff --git a/components/github/sources/new-star/new-star.mjs b/components/github/sources/new-star/new-star.mjs index a1a7404c058b3..2d70d59836caf 100644 --- a/components/github/sources/new-star/new-star.mjs +++ b/components/github/sources/new-star/new-star.mjs @@ -3,14 +3,12 @@ import { getSampleTimerEvent, getSampleWebhookEvent, } from "./common-sample-events.mjs"; -const DOCS_LINK = "https://docs.github.com/en/webhooks/webhook-events-and-payloads#star"; - export default { ...common, key: "github-new-star", name: "New Stars", - description: `Emit new event when a repository is starred [See the documentation](${DOCS_LINK})`, - version: "1.0.1", + description: "Emit new event when a repository is starred", + version: "1.0.9", type: "source", dedupe: "unique", methods: { @@ -41,5 +39,11 @@ export default { per_page: 100, }); }, + getHttpDocsLink() { + return "https://docs.github.com/en/webhooks/webhook-events-and-payloads#star"; + }, + getTimerDocsLink() { + return "https://docs.github.com/en/rest/activity/starring?apiVersion=2022-11-28#list-stargazers"; + }, }, }; diff --git a/components/github/sources/new-team/new-team.mjs b/components/github/sources/new-team/new-team.mjs index f7f85fbc51f77..f95739eb2be8f 100644 --- a/components/github/sources/new-team/new-team.mjs +++ b/components/github/sources/new-team/new-team.mjs @@ -4,19 +4,20 @@ export default { ...common, key: "github-new-team", name: "New Team", - description: "Emit new events when the user is added to a new team", - version: "0.1.15", + description: "Emit new event when the authenticated user is added to a new team. [See the documentation](https://docs.github.com/en/rest/teams/teams?apiVersion=20.2.31-28#list-teams-for-the-authenticated-user)", + version: "0.2.3", type: "source", dedupe: "unique", - async run() { - const teams = await this.github.getTeams(); - - teams.map((team) => { - this.$emit(team, { - id: team.id, - summary: `New team ${team.id}`, - ts: Date.parse(team.created_at), - }); - }); + methods: { + ...common.methods, + async getItems() { + return this.github.getTeams(); + }, + getItemMetadata(item) { + return { + summary: `New team: "${item.name ?? item.slug}"`, + ts: Date.now(), + }; + }, }, }; diff --git a/components/github/sources/new-workflow-job-completed/new-workflow-job-completed.mjs b/components/github/sources/new-workflow-job-completed/new-workflow-job-completed.mjs new file mode 100644 index 0000000000000..609aace3cd26c --- /dev/null +++ b/components/github/sources/new-workflow-job-completed/new-workflow-job-completed.mjs @@ -0,0 +1,47 @@ +import common from "../common/common-webhook.mjs"; +import { getRelevantHeaders } from "../common/utils.mjs"; + +export default { + ...common, + key: "github-new-workflow-job-completed", + name: "New Workflow Job Completed (Instant)", + description: "Emit new event when a job in a workflow is completed, regardless of whether the job was successful or unsuccessful.", + type: "source", + version: "0.0.2", + dedupe: "unique", + methods: { + ...common.methods, + getWebhookEvents() { + return [ + "workflow_job", + ]; + }, + }, + async run(event) { + const { + headers, + body, + } = event; + + // skip initial response from Github or not completed + if (body?.zen || body?.action != "completed") { + return; + } + + this.$emit({ + ...getRelevantHeaders(headers), + ...body, + }, { + id: headers["x-github-delivery"], + summary: "New workflow job completed.", + ts: new Date(), + }); + }, + async activate() { + const isAdmin = await this.checkAdminPermission(); + if (!isAdmin) { + throw new Error("Webhooks are only supported on repos where you have admin access."); + } + await this.createWebhook(); + }, +}; diff --git a/components/github/sources/new-workflow-run-completed/new-workflow-run-completed.mjs b/components/github/sources/new-workflow-run-completed/new-workflow-run-completed.mjs new file mode 100644 index 0000000000000..d135a271fe576 --- /dev/null +++ b/components/github/sources/new-workflow-run-completed/new-workflow-run-completed.mjs @@ -0,0 +1,47 @@ +import common from "../common/common-webhook.mjs"; +import { getRelevantHeaders } from "../common/utils.mjs"; + +export default { + ...common, + key: "github-new-workflow-run-completed", + name: "New Workflow Run Completed (Instant)", + description: "Emit new event when a Github Actions workflow run completes", + type: "source", + version: "0.0.2", + dedupe: "unique", + methods: { + ...common.methods, + getWebhookEvents() { + return [ + "workflow_run", + ]; + }, + }, + async run(event) { + const { + headers, + body, + } = event; + + // skip initial response from Github or not completed + if (body?.zen || body?.action != "completed") { + return; + } + + this.$emit({ + ...getRelevantHeaders(headers), + ...body, + }, { + id: headers["x-github-delivery"], + summary: "New workflow run completed.", + ts: new Date(), + }); + }, + async activate() { + const isAdmin = await this.checkAdminPermission(); + if (!isAdmin) { + throw new Error("Webhooks are only supported on repos where you have admin access."); + } + await this.createWebhook(); + }, +}; diff --git a/components/github/sources/webhook-events/webhook-events.mjs b/components/github/sources/webhook-events/webhook-events.mjs index 71ca783a2e0c1..c44639ec2a16c 100644 --- a/components/github/sources/webhook-events/webhook-events.mjs +++ b/components/github/sources/webhook-events/webhook-events.mjs @@ -1,5 +1,6 @@ import common from "../common/common-webhook.mjs"; import constants from "../common/constants.mjs"; +import { getRelevantHeaders } from "../common/utils.mjs"; export default { ...common, @@ -7,17 +8,37 @@ export default { name: "New Webhook Event (Instant)", description: "Emit new event for each selected event type", type: "source", - version: "1.0.0", + version: "1.0.9", + dedupe: "unique", props: { + docsInfo: { + type: "alert", + alertType: "info", + content: "[See the GitHub documentation](https://docs.github.com/en/webhooks/webhook-events-and-payloads) for more information on available events.", + }, ...common.props, events: { label: "Webhook Events", description: "The event types to be emitted", type: "string[]", options: constants.REPOSITORY_WEBHOOK_EVENTS, + reloadProps: true, }, }, - dedupe: "unique", + async additionalProps() { + await this.requireAdminPermission(); + const props = {}; + if (this.events?.length && this.events.includes("pull_request_review")) { + props.reviewState = { + type: "string", + label: "Review State", + description: "Filter `pull_request_review` events by review state", + options: constants.PULL_REQUEST_REVIEW_STATES, + optional: true, + }; + } + return props; + }, methods: { ...common.methods, getWebhookEvents() { @@ -36,9 +57,16 @@ export default { return; } - this.$emit(body, { + if (headers["x-github-event"] === "pull_request_review" && this.reviewState && body.review.state !== this.reviewState) { + return; + } + + this.$emit({ + ...getRelevantHeaders(headers), + ...body, + }, { id: headers["x-github-delivery"], - summary: `New event ${headers["x-github-hook-installation-target-id"]} of type ${headers["x-github-hook-installation-target-type"]}}`, + summary: `New event ${headers["x-github-hook-installation-target-id"]} of type ${headers["x-github-hook-installation-target-type"]}`, ts: new Date(), }); }, diff --git a/components/gitlab/README.md b/components/gitlab/README.md index 1c582d7d6d03f..84e327d0bb709 100644 --- a/components/gitlab/README.md +++ b/components/gitlab/README.md @@ -1,11 +1,11 @@ # Overview -Gitlab API allows developers to access the functionality of Gitlab. With the -Gitlab API, developers can integrate Gitlab with other applications, create -custom applications, or automate tasks. +The GitLab API provides programmatic access to your GitLab projects, allowing you to automate common tasks, manage issues, merge requests, and more. With the GitLab API on Pipedream, you can create customized workflows that integrate with other services, streamline your development process, and enhance project management. By leveraging the power of serverless, you can set up triggers for GitLab events and perform actions across a variety of apps without managing infrastructure. -Some examples of what you can build using the Gitlab API include: +# Example Use Cases -- Automate tasks such as creating and managing repositories -- Integrate Gitlab with other applications such as your chat application -- Create custom applications on top of Gitlab +- **Automated Issue Labeling**: Trigger a workflow whenever a new issue is created in GitLab. Analyze the issue's content using a natural language processing (NLP) service like AWS Comprehend, then automatically update the issue with relevant labels based on the analysis. + +- **Merge Request Coordinator**: Upon creation of a new merge request in GitLab, launch a Pipedream workflow that notifies a Slack channel. Include merge request details, facilitating quick team feedback and approvals. Additionally, set up criteria to auto-approve or auto-merge when all conditions are met, e.g., all required reviewers have approved and CI tests have passed. + +- **Project Monitoring and Reporting**: Use the GitLab API to fetch daily activity logs of your projects. Integrate this data with a time-tracking app like Toggl to generate comprehensive reports on development progress, which can then be sent to an email address or a Google Sheets document for further analysis. diff --git a/components/gitlab/actions/create-branch/create-branch.mjs b/components/gitlab/actions/create-branch/create-branch.mjs index 0688604201ddb..74f062235e62f 100644 --- a/components/gitlab/actions/create-branch/create-branch.mjs +++ b/components/gitlab/actions/create-branch/create-branch.mjs @@ -4,7 +4,7 @@ export default { key: "gitlab-create-branch", name: "Create Branch", description: "Create a new branch in the repository. [See the documentation](https://docs.gitlab.com/ee/api/branches.html#create-repository-branch)", - version: "0.3.1", + version: "0.3.2", type: "action", props: { gitlab, diff --git a/components/gitlab/actions/create-epic/create-epic.mjs b/components/gitlab/actions/create-epic/create-epic.mjs index 68faebd3c2c68..39d5fb483133c 100644 --- a/components/gitlab/actions/create-epic/create-epic.mjs +++ b/components/gitlab/actions/create-epic/create-epic.mjs @@ -5,25 +5,26 @@ export default { key: "gitlab-create-epic", name: "Create Epic", description: "Creates a new epic. [See the documentation](https://docs.gitlab.com/ee/api/epics.html#new-epic)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { gitlab, - groupPath: { + groupId: { propDefinition: [ gitlab, - "groupPath", + "groupId", ], }, parent_id: { propDefinition: [ gitlab, "epicIid", - (c) => ({ - groupId: c.groupPath, + ({ groupId }) => ({ + groupId, }), ], label: "Parent ID", + optional: true, }, title: { propDefinition: [ @@ -36,8 +37,8 @@ export default { propDefinition: [ gitlab, "groupLabels", - (c) => ({ - groupId: c.groupPath, + ({ groupId }) => ({ + groupId, }), ], }, @@ -115,7 +116,7 @@ export default { ])); data.labels = data.labels?.join(); - const response = await this.gitlab.createEpic(this.groupPath, { + const response = await this.gitlab.createEpic(this.groupId, { data, }); $.export("$summary", `Created epic ${this.title}`); diff --git a/components/gitlab/actions/create-issue/create-issue.mjs b/components/gitlab/actions/create-issue/create-issue.mjs index 9f40ca22a9771..10f2d00a02380 100644 --- a/components/gitlab/actions/create-issue/create-issue.mjs +++ b/components/gitlab/actions/create-issue/create-issue.mjs @@ -5,7 +5,7 @@ export default { key: "gitlab-create-issue", name: "Create issue", description: "Creates a new issue. [See the documentation](https://docs.gitlab.com/ee/api/issues.html#new-issue)", - version: "0.2.1", + version: "0.2.2", type: "action", props: { gitlab, diff --git a/components/gitlab/actions/get-issue/get-issue.mjs b/components/gitlab/actions/get-issue/get-issue.mjs index e3f31291c1cf2..fd1b99c3e11c9 100644 --- a/components/gitlab/actions/get-issue/get-issue.mjs +++ b/components/gitlab/actions/get-issue/get-issue.mjs @@ -4,7 +4,7 @@ export default { key: "gitlab-get-issue", name: "Get Issue", description: "Gets a single issue from repository. [See the documentation](https://docs.gitlab.com/ee/api/issues.html#single-project-issue)", - version: "0.2.1", + version: "0.2.2", type: "action", props: { gitlab, diff --git a/components/gitlab/actions/get-repo-branch/get-repo-branch.mjs b/components/gitlab/actions/get-repo-branch/get-repo-branch.mjs index 4f14f3110b929..c2488b99b4593 100644 --- a/components/gitlab/actions/get-repo-branch/get-repo-branch.mjs +++ b/components/gitlab/actions/get-repo-branch/get-repo-branch.mjs @@ -4,7 +4,7 @@ export default { key: "gitlab-get-repo-branch", name: "Get Repo Branch", description: "Get a single project repository branch. [See the documentation](https://docs.gitlab.com/ee/api/branches.html#get-single-repository-branch)", - version: "0.2.1", + version: "0.2.2", type: "action", props: { gitlab, diff --git a/components/gitlab/actions/list-commits/list-commits.mjs b/components/gitlab/actions/list-commits/list-commits.mjs index 311501a881987..96df8562c49ac 100644 --- a/components/gitlab/actions/list-commits/list-commits.mjs +++ b/components/gitlab/actions/list-commits/list-commits.mjs @@ -5,7 +5,7 @@ export default { key: "gitlab-list-commits", name: "List Commits", description: "List commits in a repository branch. [See the documentation](https://docs.gitlab.com/ee/api/commits.html#list-repository-commits)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { gitlab, diff --git a/components/gitlab/actions/list-repo-branches/list-repo-branches.mjs b/components/gitlab/actions/list-repo-branches/list-repo-branches.mjs index b8b4fc5e5fd26..c0ce31e6eb9ef 100644 --- a/components/gitlab/actions/list-repo-branches/list-repo-branches.mjs +++ b/components/gitlab/actions/list-repo-branches/list-repo-branches.mjs @@ -4,7 +4,7 @@ export default { key: "gitlab-list-repo-branches", name: "List Repo Branches", description: "Get a list of repository branches from a project. [See the documentation](https://docs.gitlab.com/ee/api/branches.html#list-repository-branches)", - version: "0.2.1", + version: "0.2.2", type: "action", props: { gitlab, diff --git a/components/gitlab/actions/search-issues/search-issues.mjs b/components/gitlab/actions/search-issues/search-issues.mjs index 68762679e40d5..a00dbe4326ccf 100644 --- a/components/gitlab/actions/search-issues/search-issues.mjs +++ b/components/gitlab/actions/search-issues/search-issues.mjs @@ -6,7 +6,7 @@ export default { key: "gitlab-search-issues", name: "Search Issues", description: "Search for issues in a repository with a query. [See the documentation](https://docs.gitlab.com/ee/api/issues.html#list-issues)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { gitlab, diff --git a/components/gitlab/actions/update-epic/update-epic.mjs b/components/gitlab/actions/update-epic/update-epic.mjs index 1b11a5fce1b8e..6bad6a71fbbab 100644 --- a/components/gitlab/actions/update-epic/update-epic.mjs +++ b/components/gitlab/actions/update-epic/update-epic.mjs @@ -5,22 +5,22 @@ export default { key: "gitlab-update-epic", name: "Update Epic", description: "Updates an epic. [See the documentation](https://docs.gitlab.com/ee/api/epics.html#update-epic)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { gitlab, - groupPath: { + groupId: { propDefinition: [ gitlab, - "groupPath", + "groupId", ], }, epicIid: { propDefinition: [ gitlab, "epicIid", - (c) => ({ - groupId: c.groupPath, + ({ groupId }) => ({ + groupId, }), ], }, @@ -28,8 +28,8 @@ export default { propDefinition: [ gitlab, "groupLabels", - (c) => ({ - groupId: c.groupPath, + ({ groupId }) => ({ + groupId, }), ], label: "Add labels", @@ -51,8 +51,8 @@ export default { propDefinition: [ gitlab, "groupLabels", - (c) => ({ - groupPath: c.groupPath, + ({ groupId }) => ({ + groupId, }), ], description: "Comma-separated label names for an issue. Set to an empty string to unassign all labels.", @@ -61,8 +61,8 @@ export default { propDefinition: [ gitlab, "epicIid", - (c) => ({ - groupPath: c.groupPath, + ({ groupId }) => ({ + groupId, }), ], label: "Parent Id", @@ -73,8 +73,8 @@ export default { propDefinition: [ gitlab, "groupLabels", - (c) => ({ - groupPath: c.groupPath, + ({ groupId }) => ({ + groupId, }), ], label: "Remove labels", @@ -163,7 +163,7 @@ export default { ])); data.labels = data.labels?.join(); - const response = await this.gitlab.updateEpic(this.groupPath, this.epicIid, { + const response = await this.gitlab.updateEpic(this.groupId, this.epicIid, { data, }); $.export("$summary", `Updated epic ${this.epicIid}`); diff --git a/components/gitlab/actions/update-issue/update-issue.mjs b/components/gitlab/actions/update-issue/update-issue.mjs index 131d43fd42a8b..8d5fb4b3c86ac 100644 --- a/components/gitlab/actions/update-issue/update-issue.mjs +++ b/components/gitlab/actions/update-issue/update-issue.mjs @@ -5,7 +5,7 @@ export default { key: "gitlab-update-issue", name: "Update Issue", description: "Updates an existing project issue. [See the documentation](https://docs.gitlab.com/ee/api/issues.html#edit-issue)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { gitlab, @@ -75,14 +75,17 @@ export default { }, }, async run({ $ }) { + const labels = Array.isArray(this.labels) + ? this.labels.join() + : this.labels; const data = lodash.pickBy({ title: this.title, description: this.description, assignee_ids: this.assignee_ids, state_event: this.stateEvent, discussion_locked: this.discussionLocked, + labels, }); - data.labels = data.labels?.join(); const response = await this.gitlab.editIssue(this.projectId, this.issueIid, { data, }); diff --git a/components/gitlab/gitlab.app.mjs b/components/gitlab/gitlab.app.mjs index f4e12ff593df2..cc25a96e4a420 100644 --- a/components/gitlab/gitlab.app.mjs +++ b/components/gitlab/gitlab.app.mjs @@ -23,10 +23,28 @@ export default { })); }, }, - groupPath: { + groupId: { type: "string", label: "Group ID", - description: "The group path, as displayed in the main group page. You must be an Owner of this group", + description: "Select a Group or use a custom Group ID. You must be an Owner of this group", + async options({ page }) { + const response = await this.listGroups({ + params: { + min_access_level: 50, // owner role + top_level_only: true, // only can use on root groups + page: page + 1, + }, + }); + return response.map((group) => ({ + label: group.full_path, + value: group.id, + })); + }, + }, + groupPath: { + type: "string", + label: "Group Path", + description: "Select a Group or use a custom Group Path, as displayed in the main group page. You must be an Owner of this group", async options({ page }) { const response = await this.listGroups({ params: { @@ -78,7 +96,7 @@ export default { epicIid: { type: "string", label: "Epic Internal ID", - description: "The internal ID of a project's epic", + description: "The internal ID of a project's epic. [This feature is restricted to Gitlab's Premium and Ultimate tiers.](https://docs.gitlab.com/ee/api/epics.html)", async options({ page, groupId, }) { @@ -125,7 +143,7 @@ export default { page: page + 1, }, }); - return response.data.map((label) => label.name); + return response?.map?.((label) => label.name); }, }, assignee: { @@ -273,7 +291,7 @@ export default { }, listProjects(opts = {}) { return this._makeRequest({ - path: `/users/${this._userId()}/projects`, + path: "/projects", ...opts, }); }, diff --git a/components/gitlab/package.json b/components/gitlab/package.json index 76c9df65285e8..dd16f0abc1e39 100644 --- a/components/gitlab/package.json +++ b/components/gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/gitlab", - "version": "0.5.5", + "version": "0.5.6", "description": "Pipedream Gitlab Components", "main": "gitlab.app.mjs", "keywords": [ diff --git a/components/gitlab/sources/new-audit-event/new-audit-event.mjs b/components/gitlab/sources/new-audit-event/new-audit-event.mjs index 1c0e8fb51c9d9..74dedcab77a48 100644 --- a/components/gitlab/sources/new-audit-event/new-audit-event.mjs +++ b/components/gitlab/sources/new-audit-event/new-audit-event.mjs @@ -1,6 +1,5 @@ import gitlab from "../../gitlab.app.mjs"; import base from "../common/base.mjs"; -import fetch from "node-fetch"; import { create_destination, list_destinations, @@ -12,7 +11,7 @@ export default { key: "gitlab-new-audit-event", name: "New Audit Event (Instant)", description: "Emit new event when a new audit event is created", - version: "0.1.2", + version: "0.1.3", dedupe: "unique", type: "source", props: { @@ -40,18 +39,16 @@ export default { const query = create_destination(this.http.endpoint, this.groupPath); try { - await fetch(`https://${this._getBaseApiUrl()}/api/graphql`, { + await this.gitlab._makeRequest({ + url: `https://${this._getBaseApiUrl()}/api/graphql`, method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json", "Authorization": `Bearer ${this.gitlab.$auth.oauth_access_token}`, }, - body: JSON.stringify({ - query, - }), - }) - .then((r) => r.json()); + data: query, + }); } catch (err) { console.log(`Error thrown during activation: ${JSON.stringify(err)}`); diff --git a/components/gitlab/sources/new-branch/new-branch.mjs b/components/gitlab/sources/new-branch/new-branch.mjs index 0478322fabc8b..e1e9040b745d8 100644 --- a/components/gitlab/sources/new-branch/new-branch.mjs +++ b/components/gitlab/sources/new-branch/new-branch.mjs @@ -6,7 +6,7 @@ export default { key: "gitlab-new-branch", name: "New Branch (Instant)", description: "Emit new event when a new branch is created", - version: "0.1.1", + version: "0.1.2", dedupe: "unique", type: "source", hooks: { diff --git a/components/gitlab/sources/new-commit-comment/new-commit-comment.mjs b/components/gitlab/sources/new-commit-comment/new-commit-comment.mjs index a0a8b9462c42c..e2a57aac30206 100644 --- a/components/gitlab/sources/new-commit-comment/new-commit-comment.mjs +++ b/components/gitlab/sources/new-commit-comment/new-commit-comment.mjs @@ -6,7 +6,7 @@ export default { key: "gitlab-new-commit-comment", name: "New Commit Comment (Instant)", description: "Emit new event when a commit receives a comment", - version: "0.1.1", + version: "0.1.2", dedupe: "unique", type: "source", hooks: { diff --git a/components/gitlab/sources/new-commit/new-commit.mjs b/components/gitlab/sources/new-commit/new-commit.mjs index 965d8ffb21939..0ed0991b993b8 100644 --- a/components/gitlab/sources/new-commit/new-commit.mjs +++ b/components/gitlab/sources/new-commit/new-commit.mjs @@ -7,7 +7,7 @@ export default { key: "gitlab-new-commit", name: "New Commit (Instant)", description: "Emit new event when a new commit is pushed to a branch", - version: "0.1.2", + version: "0.1.3", dedupe: "unique", type: "source", props: { diff --git a/components/gitlab/sources/new-issue/new-issue.mjs b/components/gitlab/sources/new-issue/new-issue.mjs index c4c512e6cd5be..e995c1c7280eb 100644 --- a/components/gitlab/sources/new-issue/new-issue.mjs +++ b/components/gitlab/sources/new-issue/new-issue.mjs @@ -6,7 +6,7 @@ export default { key: "gitlab-new-issue", name: "New Issue (Instant)", description: "Emit new event when an issue is created in a project", - version: "0.1.1", + version: "0.1.2", dedupe: "unique", type: "source", hooks: { diff --git a/components/gitlab/sources/new-mention/new-mention.mjs b/components/gitlab/sources/new-mention/new-mention.mjs index e9f8efb6168d9..b027ab406f2eb 100644 --- a/components/gitlab/sources/new-mention/new-mention.mjs +++ b/components/gitlab/sources/new-mention/new-mention.mjs @@ -7,7 +7,7 @@ export default { key: "gitlab-new-mention", name: "New Mention (Instant)", description: "Emit new event when you are @mentioned in a new commit, comment, issue or pull request", - version: "0.1.1", + version: "0.1.2", dedupe: "unique", type: "source", props: { @@ -20,6 +20,7 @@ export default { projectId: c.projectId, }), ], + optional: false, label: "Username", description: "The GitLab Username whose mentions will emit events", withLabel: true, diff --git a/components/gitlab/sources/new-merge-request/new-merge-request.mjs b/components/gitlab/sources/new-merge-request/new-merge-request.mjs index 96d46cf4d6db0..f2187b68e420e 100644 --- a/components/gitlab/sources/new-merge-request/new-merge-request.mjs +++ b/components/gitlab/sources/new-merge-request/new-merge-request.mjs @@ -6,7 +6,7 @@ export default { key: "gitlab-new-merge-request", name: "New Merge Request (Instant)", description: "Emit new event when a merge request is created", - version: "0.1.1", + version: "0.1.2", dedupe: "unique", type: "source", hooks: { @@ -18,7 +18,7 @@ export default { methods: { ...base.methods, isNewMergeRequest(event) { - const { action } = event.object_attributes; + const action = event?.object_attributes?.action; const expectedAction = "open"; return action === expectedAction; }, diff --git a/components/gitlab/sources/new-milestone/new-milestone.mjs b/components/gitlab/sources/new-milestone/new-milestone.mjs index 88e785fc74cee..8d8e07c66f260 100644 --- a/components/gitlab/sources/new-milestone/new-milestone.mjs +++ b/components/gitlab/sources/new-milestone/new-milestone.mjs @@ -5,7 +5,7 @@ export default { key: "gitlab-new-milestone", name: "New Milestone", description: "Emit new event when a milestone is created in a project", - version: "0.1.2", + version: "0.1.3", dedupe: "greatest", type: "source", props: { @@ -24,23 +24,9 @@ export default { ], }, }, - hooks: { - async activate() { - const milestones = await this.gitlab.listMilestones(this.projectId, { - params: { - max: 1, - }, - }); - if (milestones.length > 0) { - const lastProcessedMilestoneTime = milestones[0].created_at; - this.db.set("lastProcessedMilestoneTime", lastProcessedMilestoneTime); - console.log(`Polling GitLab milestones created after ${lastProcessedMilestoneTime}`); - } - }, - }, methods: { _getLastProcessedMilestoneTime() { - return this.db.get("lastProcessedMilestoneTime") || 0; + return this.db.get("lastProcessedMilestoneTime"); }, _setLastProcessedMilestoneTime(lastProcessedMilestoneTime) { this.db.set("lastProcessedMilestoneTime", lastProcessedMilestoneTime); @@ -59,7 +45,9 @@ export default { }, }, async run() { - let lastProcessedMilestoneTime = this._getLastProcessedMilestoneTime(); + const isoDateNow = new Date().toISOString() + .slice(0, -5) + "Z"; + let lastProcessedMilestoneTime = this._getLastProcessedMilestoneTime() ?? isoDateNow; const newOrUpdatedMilestones = await this.gitlab.listMilestones(this.projectId, { params: { updated_after: lastProcessedMilestoneTime, @@ -72,6 +60,9 @@ export default { if (milestones.length === 0) { console.log("No new GitLab milestones detected"); + if (!this._getLastProcessedMilestoneTime()) { + this._setLastProcessedMilestoneTime(lastProcessedMilestoneTime); + } return; } diff --git a/components/gitlab/sources/new-project/new-project.mjs b/components/gitlab/sources/new-project/new-project.mjs index 2ea4c3f07327b..2b128aeca9bb5 100644 --- a/components/gitlab/sources/new-project/new-project.mjs +++ b/components/gitlab/sources/new-project/new-project.mjs @@ -5,7 +5,7 @@ export default { key: "gitlab-new-project", name: "New Project", description: "Emit new event when a project (i.e. repository) is created", - version: "0.1.2", + version: "0.1.3", dedupe: "greatest", type: "source", props: { diff --git a/components/gitlab/sources/new-review-request/new-review-request.mjs b/components/gitlab/sources/new-review-request/new-review-request.mjs index 79b18649e2d4f..86feb54c5b2fa 100644 --- a/components/gitlab/sources/new-review-request/new-review-request.mjs +++ b/components/gitlab/sources/new-review-request/new-review-request.mjs @@ -6,7 +6,7 @@ export default { key: "gitlab-new-review-request", name: "New Review Request (Instant)", description: "Emit new event when a reviewer is added to a merge request", - version: "0.1.1", + version: "0.1.2", dedupe: "unique", type: "source", hooks: { @@ -37,16 +37,16 @@ export default { // as part of their response. We can check the presence of // the `assignees` attribute within those changes to verify // if there are new review requests. - const { assignees } = event.changes; - if (!assignees) { + const { reviewers } = event.changes; + if (!reviewers) { console.log(`No new assignees in merge request "${title}"`); return []; } // If the assignees of the merge request changed, we need to compute // the difference in order to extract the new reviewers. - const previousAssignees = new Set(assignees.previous.map((a) => a.username)); - const newAssignees = assignees.current.filter((a) => !previousAssignees.has(a.username)); + const previousAssignees = new Set(reviewers.previous.map((a) => a.username)); + const newAssignees = reviewers.current.filter((a) => !previousAssignees.has(a.username)); if (newAssignees.length > 0) { console.log(`Assignees added to merge request "${title}": ${newAssignees.map((a) => a.username).join(", ")}`); } diff --git a/components/gitlab_developer_app/README.md b/components/gitlab_developer_app/README.md new file mode 100644 index 0000000000000..7e12ff1342fd9 --- /dev/null +++ b/components/gitlab_developer_app/README.md @@ -0,0 +1,11 @@ +# Overview + +The GitLab (Developer App) API on Pipedream allows you to automate your development workflow by connecting GitLab with other services and creating custom, serverless workflows. With this API, you can trigger actions on events in GitLab, like pushes, merge requests, or issues, and perform operations such as creating new commits, managing issues, or deploying code. It simplifies your DevOps cycle, offers extensive automation capabilities, and integrates with numerous third-party tools, all from within Pipedream's seamless integration platform. + +# Example Use Cases + +- **Automated Code Review Assignments**: When a new merge request is created in GitLab, the workflow can automatically assign a reviewer from a predefined list or based on file paths affected in the request. Notify the reviewer through Slack to streamline the process. + +- **Issue Management and Tracking**: On the creation of a new issue in GitLab, create a corresponding task in Trello or Asana. Sync comments and status updates between GitLab and the project management tool to keep everyone on the same page without manual updates. + +- **Continuous Deployment Pipeline**: Trigger a workflow when a push to the master branch in a GitLab repository occurs. Build the code using a CI tool like CircleCI, then deploy to a server or platform like AWS or Heroku. Finally, send a notification of the deployment's success or failure through email or a messaging app like Discord. diff --git a/components/gitlab_developer_app/actions/create-branch/create-branch.mjs b/components/gitlab_developer_app/actions/create-branch/create-branch.mjs new file mode 100644 index 0000000000000..b2226dce7f8af --- /dev/null +++ b/components/gitlab_developer_app/actions/create-branch/create-branch.mjs @@ -0,0 +1,22 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/actions/create-branch/create-branch.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-create-branch", + version: "0.0.1", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitlab_developer_app/actions/create-epic/create-epic.mjs b/components/gitlab_developer_app/actions/create-epic/create-epic.mjs new file mode 100644 index 0000000000000..b56c7ad7452b4 --- /dev/null +++ b/components/gitlab_developer_app/actions/create-epic/create-epic.mjs @@ -0,0 +1,21 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/actions/create-epic/create-epic.mjs"; +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-create-epic", + version: "0.0.1", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitlab_developer_app/actions/create-issue/create-issue.mjs b/components/gitlab_developer_app/actions/create-issue/create-issue.mjs new file mode 100644 index 0000000000000..6d69fab7737f8 --- /dev/null +++ b/components/gitlab_developer_app/actions/create-issue/create-issue.mjs @@ -0,0 +1,22 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/actions/create-issue/create-issue.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-create-issue", + version: "0.0.1", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitlab_developer_app/actions/get-issue/get-issue.mjs b/components/gitlab_developer_app/actions/get-issue/get-issue.mjs new file mode 100644 index 0000000000000..ecdae4575d656 --- /dev/null +++ b/components/gitlab_developer_app/actions/get-issue/get-issue.mjs @@ -0,0 +1,22 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/actions/get-issue/get-issue.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-get-issue", + version: "0.0.1", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitlab_developer_app/actions/get-repo-branch/get-repo-branch.mjs b/components/gitlab_developer_app/actions/get-repo-branch/get-repo-branch.mjs new file mode 100644 index 0000000000000..18135cd745cf2 --- /dev/null +++ b/components/gitlab_developer_app/actions/get-repo-branch/get-repo-branch.mjs @@ -0,0 +1,22 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/actions/get-repo-branch/get-repo-branch.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-get-repo-branch", + version: "0.0.1", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitlab_developer_app/actions/list-commits/list-commits.mjs b/components/gitlab_developer_app/actions/list-commits/list-commits.mjs new file mode 100644 index 0000000000000..8e9d749b59a9a --- /dev/null +++ b/components/gitlab_developer_app/actions/list-commits/list-commits.mjs @@ -0,0 +1,22 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/actions/list-commits/list-commits.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-list-commits", + version: "0.0.2", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitlab_developer_app/actions/list-repo-branches/list-repo-branches.mjs b/components/gitlab_developer_app/actions/list-repo-branches/list-repo-branches.mjs new file mode 100644 index 0000000000000..4a2ad20cae132 --- /dev/null +++ b/components/gitlab_developer_app/actions/list-repo-branches/list-repo-branches.mjs @@ -0,0 +1,22 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/actions/list-repo-branches/list-repo-branches.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-list-repo-branches", + version: "0.0.1", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitlab_developer_app/actions/search-issues/search-issues.mjs b/components/gitlab_developer_app/actions/search-issues/search-issues.mjs new file mode 100644 index 0000000000000..8c5033594c30f --- /dev/null +++ b/components/gitlab_developer_app/actions/search-issues/search-issues.mjs @@ -0,0 +1,22 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/actions/search-issues/search-issues.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-search-issues", + version: "0.0.1", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitlab_developer_app/actions/update-epic/update-epic.mjs b/components/gitlab_developer_app/actions/update-epic/update-epic.mjs new file mode 100644 index 0000000000000..2ea00e29e0669 --- /dev/null +++ b/components/gitlab_developer_app/actions/update-epic/update-epic.mjs @@ -0,0 +1,22 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/actions/update-epic/update-epic.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-update-epic", + version: "0.0.1", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitlab_developer_app/actions/update-issue/update-issue.mjs b/components/gitlab_developer_app/actions/update-issue/update-issue.mjs new file mode 100644 index 0000000000000..c920fa14d3294 --- /dev/null +++ b/components/gitlab_developer_app/actions/update-issue/update-issue.mjs @@ -0,0 +1,22 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/actions/update-issue/update-issue.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-update-issue", + version: "0.0.1", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitlab_developer_app/common/utils.mjs b/components/gitlab_developer_app/common/utils.mjs new file mode 100644 index 0000000000000..d42df055ddeb0 --- /dev/null +++ b/components/gitlab_developer_app/common/utils.mjs @@ -0,0 +1,40 @@ +export function adjustPropDefinitions(props, app) { + return Object.fromEntries( + Object.entries(props).map(([ + key, + prop, + ]) => { + if (typeof prop === "string") return [ + key, + prop, + ]; + const { + propDefinition, ...otherValues + } = prop; + if (propDefinition) { + const [ + , ...otherDefs + ] = propDefinition; + return [ + key, + { + propDefinition: [ + app, + ...otherDefs, + ], + ...otherValues, + }, + ]; + } + return [ + key, + otherValues.type === "app" + ? null + : otherValues, + ]; + }) + .filter(([ + , value, + ]) => value), + ); +} diff --git a/components/gitlab_developer_app/gitlab_developer_app.app.mjs b/components/gitlab_developer_app/gitlab_developer_app.app.mjs new file mode 100644 index 0000000000000..f0acf0cef2a71 --- /dev/null +++ b/components/gitlab_developer_app/gitlab_developer_app.app.mjs @@ -0,0 +1,6 @@ +import common from "../gitlab/gitlab.app.mjs"; + +export default { + ...common, + app: "gitlab_developer_app", +}; diff --git a/components/gitlab_developer_app/package.json b/components/gitlab_developer_app/package.json new file mode 100644 index 0000000000000..2b2f4b4e0b262 --- /dev/null +++ b/components/gitlab_developer_app/package.json @@ -0,0 +1,20 @@ +{ + "name": "@pipedream/gitlab_developer_app", + "version": "0.1.1", + "description": "Pipedream GitLab (Developer App) Components", + "main": "gitlab_developer_app.app.mjs", + "keywords": [ + "pipedream", + "gitlab_developer_app" + ], + "homepage": "https://pipedream.com/apps/gitlab_developer_app", + "author": "Pipedream (https://pipedream.com/)", + "dependencies": { + "@pipedream/platform": "^1.6.4", + "lodash": "^4.17.21", + "uuid": "^8.3.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/components/gitlab_developer_app/sources/new-audit-event/new-audit-event.mjs b/components/gitlab_developer_app/sources/new-audit-event/new-audit-event.mjs new file mode 100644 index 0000000000000..22342654d28a9 --- /dev/null +++ b/components/gitlab_developer_app/sources/new-audit-event/new-audit-event.mjs @@ -0,0 +1,21 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/sources/new-audit-event/new-audit-event.mjs"; +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-new-audit-event", + version: "0.0.1", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitlab_developer_app/sources/new-branch/new-branch.mjs b/components/gitlab_developer_app/sources/new-branch/new-branch.mjs new file mode 100644 index 0000000000000..7b33d38ed0199 --- /dev/null +++ b/components/gitlab_developer_app/sources/new-branch/new-branch.mjs @@ -0,0 +1,21 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/sources/new-branch/new-branch.mjs"; +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-new-branch", + version: "0.0.2", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitlab_developer_app/sources/new-commit-comment/new-commit-comment.mjs b/components/gitlab_developer_app/sources/new-commit-comment/new-commit-comment.mjs new file mode 100644 index 0000000000000..e150550caa4de --- /dev/null +++ b/components/gitlab_developer_app/sources/new-commit-comment/new-commit-comment.mjs @@ -0,0 +1,21 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/sources/new-commit-comment/new-commit-comment.mjs"; +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-new-commit-comment", + version: "0.0.1", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitlab_developer_app/sources/new-commit/new-commit.mjs b/components/gitlab_developer_app/sources/new-commit/new-commit.mjs new file mode 100644 index 0000000000000..0829809b1c2f0 --- /dev/null +++ b/components/gitlab_developer_app/sources/new-commit/new-commit.mjs @@ -0,0 +1,21 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/sources/new-commit/new-commit.mjs"; +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-new-commit", + version: "0.0.1", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitlab_developer_app/sources/new-issue/new-issue.mjs b/components/gitlab_developer_app/sources/new-issue/new-issue.mjs new file mode 100644 index 0000000000000..d9addfda12927 --- /dev/null +++ b/components/gitlab_developer_app/sources/new-issue/new-issue.mjs @@ -0,0 +1,21 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/sources/new-issue/new-issue.mjs"; +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-new-issue", + version: "0.0.1", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitlab_developer_app/sources/new-mention/new-mention.mjs b/components/gitlab_developer_app/sources/new-mention/new-mention.mjs new file mode 100644 index 0000000000000..5b6cb41365d29 --- /dev/null +++ b/components/gitlab_developer_app/sources/new-mention/new-mention.mjs @@ -0,0 +1,21 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/sources/new-mention/new-mention.mjs"; +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-new-mention", + version: "0.0.1", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitlab_developer_app/sources/new-merge-request/new-merge-request.mjs b/components/gitlab_developer_app/sources/new-merge-request/new-merge-request.mjs new file mode 100644 index 0000000000000..d473a02d5af05 --- /dev/null +++ b/components/gitlab_developer_app/sources/new-merge-request/new-merge-request.mjs @@ -0,0 +1,21 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/sources/new-merge-request/new-merge-request.mjs"; +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-new-merge-request", + version: "0.0.1", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitlab_developer_app/sources/new-milestone/new-milestone.mjs b/components/gitlab_developer_app/sources/new-milestone/new-milestone.mjs new file mode 100644 index 0000000000000..2c291c5406aeb --- /dev/null +++ b/components/gitlab_developer_app/sources/new-milestone/new-milestone.mjs @@ -0,0 +1,21 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/sources/new-milestone/new-milestone.mjs"; +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-new-milestone", + version: "0.0.1", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitlab_developer_app/sources/new-project/new-project.mjs b/components/gitlab_developer_app/sources/new-project/new-project.mjs new file mode 100644 index 0000000000000..65ff7e274316f --- /dev/null +++ b/components/gitlab_developer_app/sources/new-project/new-project.mjs @@ -0,0 +1,21 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/sources/new-project/new-project.mjs"; +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-new-project", + version: "0.0.1", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitlab_developer_app/sources/new-review-request/new-review-request.mjs b/components/gitlab_developer_app/sources/new-review-request/new-review-request.mjs new file mode 100644 index 0000000000000..5993904c01646 --- /dev/null +++ b/components/gitlab_developer_app/sources/new-review-request/new-review-request.mjs @@ -0,0 +1,21 @@ +import app from "../../gitlab_developer_app.app.mjs"; +import common from "../../../gitlab/sources/new-review-request/new-review-request.mjs"; +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "gitlab_developer_app-new-review-request", + version: "0.0.1", + name, + description, + type, + props: { + gitlab: app, + ...props, + }, +}; diff --git a/components/gitter/README.md b/components/gitter/README.md index 217324c8b5cc7..62a2c4e75e6b5 100644 --- a/components/gitter/README.md +++ b/components/gitter/README.md @@ -1,13 +1,11 @@ # Overview -Gitter is a messaging platform developed by British company GitLab. It allows -developers to communicate with each other in real-time, and work on code -collaboration. +The Gitter API enables your bots to perform automated actions within Gitter chat rooms. From sending messages to fetching conversation histories, the Gitter API serves as a bridge to interact with Gitter programmatically. With Pipedream, you can harness this API to create custom workflows that trigger on specific events in Gitter, process data, and connect to countless other services to achieve a wide array of tasks. These can range from notifications and chatbots to complex integrations with project management tools or CRMs. -Gitter has an API which developers can use to build applications on top of the -platform. Here are some examples of what you can build with the Gitter API: +# Example Use Cases -- A messaging app for developers to communicate with each other in real-time -- A code collaboration tool for developers to work on code together -- A platform for developers to share code snippets and collaborate on code - projects +- **Streamline Incident Reporting**: Automatically send alerts into a Gitter room from monitoring tools like Datadog or Sentry when an incident occurs. The workflow can include formatting the alert with essential details, tagging the relevant team members, and even creating a follow-up task in a project management tool like Jira. + +- **Daily Digests and Reminders**: Compile a daily digest of messages from specified Gitter rooms and send them out via email using SendGrid. Additionally, set up reminders for upcoming events or deadlines by reading from a Google Calendar and posting messages back into Gitter. + +- **Chatbot Integration for Task Management**: Create a chatbot that listens for specific commands or keywords in Gitter chat rooms. When triggered, the chatbot can create tasks in Trello, schedule events in Google Calendar, or update a record in Airtable, all depending on the context of the conversation. diff --git a/components/gitter/package.json b/components/gitter/package.json new file mode 100644 index 0000000000000..89bd445359773 --- /dev/null +++ b/components/gitter/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/gitter", + "version": "0.6.0", + "description": "Pipedream gitter Components", + "main": "gitter.app.mjs", + "keywords": [ + "pipedream", + "gitter" + ], + "homepage": "https://pipedream.com/apps/gitter", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/givebutter/README.md b/components/givebutter/README.md index 8a9d731c90445..a85a426ee2eb9 100644 --- a/components/givebutter/README.md +++ b/components/givebutter/README.md @@ -1,10 +1,11 @@ # Overview -With the Givebutter API, you can build a variety of applications that can help -you raise money for your cause. Here are some examples: - -- A donation form that allows people to donate to your cause easily -- A fundraising thermometer that shows how close you are to your fundraising - goal -- A social media campaign that encourages people to donate to your cause -- A crowdfunding campaign that allows people to support your cause financially +The Givebutter API taps into the platform's fundraising capabilities, allowing developers to manage campaigns, track donations, and engage with supporters. It's designed for nonprofits and community organizations looking to automate their fundraising efforts and integrate with other services for a seamless workflow. By leveraging the Givebutter API on Pipedream, one can create custom automations that sync data between tools, send real-time notifications, and analyze fundraising activities without manual input, ensuring that resources are focused on the mission rather than administrative tasks. + +# Example Use Cases + +- **Automate Donation Acknowledgements**: Trigger an automated 'Thank You' email or SMS to donors immediately after a donation is received via Givebutter. Connect the Givebutter API with an email service like SendGrid or a messaging service like Twilio on Pipedream to set up this personalized donor engagement workflow. + +- **Sync Donor Data with CRM**: Each time a new donation is recorded on Givebutter, automatically sync donor details and donation amounts to a CRM like Salesforce or HubSpot. This Pipedream workflow helps maintain up-to-date records, which is critical for analyzing fundraising efforts and building lasting relationships with supporters. + +- **Real-time Fundraising Dashboards**: Integrate Givebutter with a data visualization tool like Google Sheets or a BI app such as Tableau to automatically feed donation data into custom dashboards. This workflow allows for real-time tracking of fundraising progress and helps teams make data-driven decisions on their campaigns. diff --git a/components/givingfuel/README.md b/components/givingfuel/README.md new file mode 100644 index 0000000000000..0b9046249c47e --- /dev/null +++ b/components/givingfuel/README.md @@ -0,0 +1,11 @@ +# Overview + +The GivingFuel API enables non-profit organizations to automate their donation processes, manage donor information, and analyze donation data directly within Pipedream. By leveraging this API, you can create workflows that streamline the collection and use of donation information, trigger actions based on donation events, and integrate with other apps to enrich the donor's journey or facilitate the management of fundraising campaigns. + +# Example Use Cases + +- **Donation Triggered Thank You Email**: When a new donation is received via GivingFuel, a workflow can be triggered that sends a personalized thank you email to the donor. This can be done by integrating with email platforms like SendGrid or Mailgun within Pipedream. + +- **Donor Data Synchronization**: Keep your CRM updated by synchronizing new donor information from GivingFuel to CRM systems like Salesforce or HubSpot. Whenever a new donor contributes, the workflow creates or updates their record in your CRM, ensuring your donor database is always current. + +- **Donation Summary Reports**: Generate daily or weekly donation summary reports and send them to your team via Slack or email. This allows for real-time insights into fundraising progress and helps inform strategy decisions based on the latest data. diff --git a/components/gladia/README.md b/components/gladia/README.md new file mode 100644 index 0000000000000..1e76ce922c55a --- /dev/null +++ b/components/gladia/README.md @@ -0,0 +1,11 @@ +# Overview + +The Gladia API offers a suite of AI-powered capabilities, including image recognition, text analysis, and language processing. With its integration on Pipedream, you can build serverless workflows to automate tasks like content moderation, data enrichment, and real-time translations. Leverage Gladia's AI functions within Pipedream to create powerful, event-driven workflows that respond to various triggers such as webhooks, schedules, or app events. + +# Example Use Cases + +- **Content Moderation Pipeline**: Use Gladia's content moderation API on Pipedream to scan user-submitted images for inappropriate content. When an image is uploaded to your platform, trigger a workflow that passes the image to Gladia for analysis, and based on the results, automatically flag or remove the image, notify moderators, or take other appropriate actions. + +- **Sentiment Analysis Feedback Loop**: Analyze customer feedback by connecting Gladia's sentiment analysis to support ticket submissions. When a new ticket is created in a helpdesk app like Zendesk, trigger a Pipedream workflow that uses Gladia to assess the sentiment of the submission. Use this data to prioritize urgent negative feedback and route tickets to the right team, or to gather insights on customer satisfaction over time. + +- **Multilingual Chatbot Enhancer**: Enhance a chatbot's multilingual capabilities by integrating Gladia's language translation API into a Pipedream workflow. As messages come in from users speaking various languages, the workflow can translate them into a preferred language for the support team, and vice versa, ensuring clear communication and broadening the chatbot's audience reach. diff --git a/components/gleap/README.md b/components/gleap/README.md new file mode 100644 index 0000000000000..78573b1ef8bfc --- /dev/null +++ b/components/gleap/README.md @@ -0,0 +1,11 @@ +# Overview + +The Gleap API allows for the seamless integration of customer feedback into your product development cycle. With Gleap, you can collect bug reports, feature requests, and general feedback, all enriched with screenshots, replay videos, and logs to give you comprehensive insights into user experiences. In Pipedream, you can harness this API to automate workflows, connect feedback to your issue trackers or communication platforms, and analyze data to inform product improvements. + +# Example Use Cases + +- **Sync Feedback to Project Management Tools:** Automatically create new tasks or tickets in project management tools like Jira or Trello whenever a new feedback is submitted through Gleap. This keeps your development team instantly in the loop and ensures no user feedback slips through the cracks. + +- **Trigger Notifications Based on Feedback Severity:** Set up a workflow that evaluates the severity of feedback from Gleap and triggers instant alerts through communication platforms like Slack, Discord, or email for high-severity reports, ensuring rapid response to critical issues. + +- **Feedback Aggregation and Analysis:** Collect and aggregate feedback data from Gleap into a Google Sheets or Airtable base for advanced analysis and reporting. Use this data to spot trends, prioritize product enhancements, and make data-driven decisions. diff --git a/components/glide/README.md b/components/glide/README.md new file mode 100644 index 0000000000000..239e218d85f68 --- /dev/null +++ b/components/glide/README.md @@ -0,0 +1,11 @@ +# Overview + +The Glide API allows you to create feature-rich, mobile-friendly apps directly from data sources like Google Sheets or Excel. On Pipedream, you can harness this seamless bridge between your data and the app interface to automate tasks, sync data in real time, and dynamically control your Glide apps. Combine Glide with other apps on Pipedream to enhance functionality, like triggering notifications, processing payments, or managing user data. + +# Example Use Cases + +- **Sync Google Sheets with Glide**: Automate the updating of a Glide app’s data by watching for changes in a connected Google Sheets document. When a row is added or modified, Pipedream can trigger a workflow that updates the app data, ensuring your Glide app is always current without manual intervention. + +- **Process Payments with Stripe**: Create a workflow that integrates Glide with Stripe. When a user makes a purchase within your Glide app, trigger an event that Pipedream captures, creating a new payment intent in Stripe. Pipedream can then confirm the payment and update the app, providing a smooth transaction experience. + +- **Manage Users with SendGrid**: Set up a Pipedream workflow to manage user onboarding. When a new user signs up in your Glide app, use Pipedream to send their details to SendGrid, triggering a welcome email sequence. This keeps your users engaged and informed from the moment they start using your app. diff --git a/components/glide/package.json b/components/glide/package.json index 172d81fee01c6..e00ed6f813112 100644 --- a/components/glide/package.json +++ b/components/glide/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/globalping/globalping.app.mjs b/components/globalping/globalping.app.mjs new file mode 100644 index 0000000000000..4fc928c2c4cf6 --- /dev/null +++ b/components/globalping/globalping.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "globalping", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/globalping/package.json b/components/globalping/package.json new file mode 100644 index 0000000000000..f68ac22fab9b3 --- /dev/null +++ b/components/globalping/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/globalping", + "version": "0.0.1", + "description": "Pipedream Globalping Components", + "main": "globalping.app.mjs", + "keywords": [ + "pipedream", + "globalping" + ], + "homepage": "https://pipedream.com/apps/globalping", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/gloria_ai/actions/create-lead/create-lead.mjs b/components/gloria_ai/actions/create-lead/create-lead.mjs new file mode 100644 index 0000000000000..b1420a5be1f90 --- /dev/null +++ b/components/gloria_ai/actions/create-lead/create-lead.mjs @@ -0,0 +1,72 @@ +import gloriaAi from "../../gloria_ai.app.mjs"; + +export default { + key: "gloria_ai-create-lead", + name: "Create Lead", + description: "Creates a new lead/contact in Gloria.ai. [See the documentation](https://api.iamgloria.com/api).", + version: "0.0.1", + type: "action", + props: { + gloriaAi, + leadName: { + propDefinition: [ + gloriaAi, + "leadName", + ], + }, + phone: { + propDefinition: [ + gloriaAi, + "phone", + ], + }, + email: { + propDefinition: [ + gloriaAi, + "email", + ], + }, + initiation: { + propDefinition: [ + gloriaAi, + "initiation", + ], + }, + tags: { + propDefinition: [ + gloriaAi, + "tags", + ], + }, + status: { + propDefinition: [ + gloriaAi, + "status", + ], + }, + }, + async run({ $ }) { + const response = await this.gloriaAi.createContact({ + $, + data: { + tenantId: this.gloriaAi.$auth.tenant_id, + createdAt: Date.now(), + name: [ + this.leadName, + ], + phone: this.phone && [ + this.phone, + ], + email: this.email && [ + this.email, + ], + origin: "api", + initiation: this.initiation, + tags: this.tags, + status: this.status, + }, + }); + $.export("$summary", `Successfully created lead with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/gloria_ai/gloria_ai.app.mjs b/components/gloria_ai/gloria_ai.app.mjs new file mode 100644 index 0000000000000..10245bf98797d --- /dev/null +++ b/components/gloria_ai/gloria_ai.app.mjs @@ -0,0 +1,78 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "gloria_ai", + propDefinitions: { + leadName: { + type: "string", + label: "Lead Name", + description: "The name of the lead", + }, + phone: { + type: "string", + label: "Phone", + description: "Phone number of the lead", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "Email address of the lead", + optional: true, + }, + initiation: { + type: "string", + label: "Initiation", + description: "Initiation details for the lead", + options: [ + "Inbound", + "Outbound", + ], + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "Tags associated with the lead", + optional: true, + }, + status: { + type: "string", + label: "Status", + description: "Status of the lead", + options: [ + "active", + "archived", + "favorite", + ], + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.iamgloria.com/api/v1"; + }, + _makeRequest({ + $ = this, + path, + ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `Bearer ${this.$auth.api_key}`, + "tenant-id": `${this.$auth.tenant_id}`, + }, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contacts", + ...opts, + }); + }, + }, +}; diff --git a/components/gloria_ai/package.json b/components/gloria_ai/package.json new file mode 100644 index 0000000000000..0a3f48057feb3 --- /dev/null +++ b/components/gloria_ai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/gloria_ai", + "version": "0.1.0", + "description": "Pipedream Gloria AI Components", + "main": "gloria_ai.app.mjs", + "keywords": [ + "pipedream", + "gloria_ai" + ], + "homepage": "https://pipedream.com/apps/gloria_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/gloww/actions/create-live-session/create-live-session.mjs b/components/gloww/actions/create-live-session/create-live-session.mjs new file mode 100644 index 0000000000000..8d3ba92a379f7 --- /dev/null +++ b/components/gloww/actions/create-live-session/create-live-session.mjs @@ -0,0 +1,51 @@ +import { ConfigurationError } from "@pipedream/platform"; +import gloww from "../../gloww.app.mjs"; + +export default { + key: "gloww-create-live-session", + name: "Create Live Session", + version: "0.0.1", + description: "Create a new Live Session [See the documentation](https://gloww.com/faq/)", + type: "action", + props: { + gloww, + sessionId: { + propDefinition: [ + gloww, + "sessionId", + ], + optional: true, + }, + templateId: { + propDefinition: [ + gloww, + "templateId", + ], + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "The name of the Live Session.", + }, + }, + async run({ $ }) { + if ((!this.sessionId && !this.templateId) || (this.sessionId && this.templateId)) { + throw new ConfigurationError("You must provide either Session Id or Template Id."); + } + + const response = await this.gloww.createSession({ + $, + data: { + name: this.sessionName, + }, + params: { + fromTemplateId: this.templateId, + fromSessionId: this.sessionId, + }, + }); + + $.export("$summary", `A new live session with Id: ${response.id} was successfully created!`); + return response; + }, +}; diff --git a/components/gloww/gloww.app.mjs b/components/gloww/gloww.app.mjs new file mode 100644 index 0000000000000..fdae56815dd93 --- /dev/null +++ b/components/gloww/gloww.app.mjs @@ -0,0 +1,70 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "gloww", + propDefinitions: { + sessionId: { + type: "string", + label: "Session Id", + description: "The Id of the session on which the new session is based.", + async options() { + const { sessions } = await this.listSuggestions(); + + return sessions.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + templateId: { + type: "string", + label: "Template Id", + description: "The Id of the template on which the new session is based.", + async options() { + const { templates } = await this.listSuggestions(); + + return templates.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _apiUrl() { + return "https://api.gloww.com"; + }, + _getHeaders() { + return { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: `${this._apiUrl()}${path}`, + headers: this._getHeaders(), + ...opts, + }); + }, + createSession(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/api/liveSessions", + ...opts, + }); + }, + listSuggestions(opts = {}) { + return this._makeRequest({ + path: "/integrations/suggestions/sessions", + ...opts, + }); + }, + }, +}; diff --git a/components/gloww/package.json b/components/gloww/package.json new file mode 100644 index 0000000000000..760e590308790 --- /dev/null +++ b/components/gloww/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/gloww", + "version": "0.1.0", + "description": "Pipedream Gloww Components", + "main": "gloww.app.mjs", + "keywords": [ + "pipedream", + "gloww" + ], + "homepage": "https://pipedream.com/apps/gloww", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} diff --git a/components/gmail/README.md b/components/gmail/README.md index ae264e08ab935..f198f4fdaaefd 100644 --- a/components/gmail/README.md +++ b/components/gmail/README.md @@ -1,16 +1,144 @@ # Overview -By connecting your personal Gmail account to Pipedream, you'll be able to incorporate email into whatever you're building with any of the 900+ apps that are available on Pipedream. + +By connecting your personal Gmail account to Pipedream, you'll be able to incorporate email into whatever you're building with any of the thousands of apps that are available on Pipedream. # Getting Started -To connect your Gmail account to Pipedream, just accept the prompt when connecting your Gmail account. +To connect your Gmail account to Pipedream, just accept the prompt when connecting your Gmail account. To connect your account with your own OAuth client, see below. + +## Configuring a Gmail OAuth client in Google Cloud Platform + +To get started, you'll need the following: + +- A Google Cloud account +- Basic familiarity with Google Cloud Console + +## Quickstart + +1. Create a custom Gmail client in Google Cloud Console +2. Enable Gmail API and Pub/Sub API +3. Create OAuth credentials and a service account +4. Set up a custom OAuth client in Pipedream +5. Connect your Gmail account using the custom client and service account + +For detailed instructions, follow the steps below. + +## Detailed Setup Instructions + +### 1. Create a Gmail app + +1. Sign in to the [Google Cloud Console](https://console.cloud.google.com/welcome) +2. Select an existing project, or create a new one + + ![Select an existing project or create a new one in the Google Cloud Console](https://res.cloudinary.com/pipedreamin/image/upload/v1663268100/docs/components/CleanShot_2022-09-15_at_14.54.34_vajyds.png) + +3. Select **APIs & Services** +4. Click **Enable APIs & Services** + + ![Select "Enable APIs & Services to open a menu to enable the Gmail API for Pipedream to connect to](https://res.cloudinary.com/pipedreamin/image/upload/v1663268316/docs/components/CleanShot_2022-09-15_at_14.58.06_jshirk.png) + +5. Search for and select **[Gmail API](https://console.cloud.google.com/apis/library/gmail.googleapis.com)** +6. Click **Enable** + + ![Search for and select the Gmail Enterprise API](https://res.cloudinary.com/pipedreamin/image/upload/v1663268442/docs/components/CleanShot_2022-09-15_at_15.00.22_skvwei.gif) + +7. Search for and select **[Cloud Pub/Sub API](https://console.cloud.google.com/apis/library/pubsub.googleapis.com)** +8. Click **Enable** + + ![Search for and select the Cloud Pub/Sub API](https://res.cloudinary.com/dpenc2lit/image/upload/v1724881089/Screenshot_2024-08-28_at_2.36.59_PM_ds4knm.png) + +> **Note:** If you encounter issues with API enablement, ensure you have the necessary permissions in your Google Cloud project. + +### 2. Set up the OAuth consent screen + +1. Click **OAuth consent screen** on the left side + + ![Click "OAuth consent screen" in the left navigation menu](https://res.cloudinary.com/pipedreamin/image/upload/v1663268506/docs/components/CleanShot_2022-09-15_at_15.01.24_wravfb.png) + +2. Set up the OAuth consent screen: + + - Select User Type (Internal for Google Workspace users, External for others) + - Fill in required fields + - Add scopes: `email`, `profile`, `https://www.googleapis.com/auth/gmail.modify`, `https://www.googleapis.com/auth/gmail.settings.basic` + - Add your email as a test user + - Review and complete the setup + + ![Select "External" in the OAuth Consent Screen](https://res.cloudinary.com/pipedreamin/image/upload/v1663268545/docs/components/CleanShot_2022-09-15_at_15.02.22_fiekq1.png) + +### 3. Create OAuth Credentials in Google and Custom OAuth Client in Pipedream + +1. Navigate to the **Credentials** section on the left side. + + ![Open the Credentials menu in the left hand nav bar](https://res.cloudinary.com/pipedreamin/image/upload/v1663269973/docs/components/CleanShot_2022-09-15_at_15.13.52_yvllxi.png) + +2. Click **Create Credentials** at the top and select **OAuth client ID** + + ![Click create credentials to start the process](https://res.cloudinary.com/pipedreamin/image/upload/v1663270014/docs/components/CleanShot_2022-09-15_at_15.14.15_hjulis.png) + + ![Select the OAuth Client ID option](https://res.cloudinary.com/pipedreamin/image/upload/v1663270093/docs/components/CleanShot_2022-09-15_at_15.14.39_juqtnm.png) + +3. Select **Web application** for **Application type** + + ![Web application is the type of OAuth credential we're generating](https://res.cloudinary.com/pipedreamin/image/upload/v1663270117/docs/components/CleanShot_2022-09-15_at_15.14.56_hlseq6.png) + +4. Name the app, e.g. "Pipedream". +5. In a new window, navigate to the [Accounts](https://pipedream.com/accounts) page in **Pipedream**, and click **OAuth Clients**. + + ![Custom OAuth Client creation on Pipedream](https://res.cloudinary.com/dpenc2lit/image/upload/v1724882777/Screenshot_2024-08-28_at_2.53.15_PM_rxtusm.png) + +6. Click **New OAuth Client**, and search for Gmail. +7. Name your OAuth Client, and click **Continue**. +8. Copy the **Redirect URI**, and return to your previous window. +9. On your Google Cloud app configuration page, click **Add URI** and paste the Redirect URI from the previous step. + + ![Add the Pipedream URL to the Callback Redirect URL option](https://res.cloudinary.com/pipedreamin/image/upload/v1663270187/docs/components/CleanShot_2022-09-15_at_15.16.10_hvbocb.png) + +10. Click **Create** to create your new OAuth keys. + + ![Store the Client ID and Client Secret keys](https://res.cloudinary.com/pipedreamin/image/upload/v1663270250/docs/components/CleanShot_2022-09-15_at_15.16.29_hvxnkx.png) + +11. Copy the Client ID and Client Secret, and paste them in your OAuth Client configuration on Pipedream. + + ![Custom OAuth Client creation on Pipedream](https://res.cloudinary.com/dpenc2lit/image/upload/v1724956524/Screenshot_2024-08-29_at_11.34.55_AM_t7tjkh.png) + +> **Important:** When creating the OAuth client ID, make sure to copy the Redirect URI from Pipedream exactly as shown to avoid authentication errors. + +### 4. Create service account + +1. Navigate to **[Credentials](https://console.cloud.google.com/apis/credentials?)** under APIs & Services, and click **Create Credentials** > **Service Account**. + + ![Service Account Creation](https://res.cloudinary.com/dpenc2lit/image/upload/v1724964633/Screenshot_2024-08-29_at_1.44.04_PM_om14xp.png) + +2. Add a name and description for your service account, and grant the service account the role **Pub/Sub Admin**, and click **Done**. + + ![Role administering](https://res.cloudinary.com/dpenc2lit/image/upload/v1724964633/Screenshot_2024-08-29_at_1.47.02_PM_chdjkl.png) + +3. Click on the service account that you created, and click **Keys** > **Add Key** > **Create New Key** > **JSON**. This will download the service account JSON credentials to your computer. Be sure to save this securely. + + ![Create private key](https://res.cloudinary.com/dpenc2lit/image/upload/v1724964634/Screenshot_2024-08-29_at_1.47.34_PM_tmalc7.png) + +### 5. Connect your Gmail account in Pipedream + +1. From the Pipedream Accounts page, click **OAuth Clients**. Next to your newly created Gmail client, click the three-dot menu on the righthand side and click **Connect Account**. Or you can also connect your account from the workflow builder, when configuring the Gmail trigger. +2. While configuring the New Email Received trigger, you should be prompted to input your Service Account Key JSON. + +### 6. Publish your custom Gmail app (required for External app type only) + +Google has a [7 day expiration window](https://developers.google.com/identity/protocols/oauth2#:~:text=A%20Google%20Cloud,Connect%20equivalents) on refresh tokens for **External** applications with a publishing status of "Testing", so you will need to **Publish** your application in order to maintain your account connection. + +1. Navigate to your application, and click **OAuth Consent Screen** on the lefthand sidebar. +2. Under **Publishing status**, click **Publish App**. If you included any sensitive or restricted scopes in your app, there will be a disclosure stating that you will need to go through the process of verification. Click **Confirm**. +3. Your application will not be available externally unless you share your **client_id** with others, and you will not have to go through the verification process unless you intend to onboard over 100 users. +4. The publishing status should be set to **In production**, and your account should maintain its connection without an expiration window. -## What's the difference between the **Gmail** app and the **Gmail Developer App**? +![Publish your application](https://res.cloudinary.com/dpenc2lit/image/upload/v1698166716/Screenshot_2023-10-24_at_9.50.06_AM_lve7wq.png) -Google requires integration platforms such as Pipedream, to restrict the scopes users can enable for Gmail, in order to regulate the ability of users to build workflows and connect applications. +![Confirmation of changes](https://res.cloudinary.com/dpenc2lit/image/upload/v1698166716/Screenshot_2023-10-24_at_9.50.18_AM_mndtyc.png) -Currently, the **Gmail** app on Pipedream is limited to only sending emails on behalf of your Gmail email address associated with this app. +# Troubleshooting -However, the **Gmail Developer App** is unrestricted because you have the ability to control the scopes through creating your own Google Cloud Project. +- **Authentication Failed**: Double-check that your Redirect URI is correct and that you've added your email as a test user in the OAuth consent screen. +- **API Not Enabled**: Ensure both Gmail API and Pub/Sub API are enabled in your Google Cloud project. +- **Service Account Issues**: Verify that your service account has the "Pub/Sub Admin" role and that you've correctly pasted the JSON key into Pipedream. -Because you can define your own scopes, you can read and write emails with your own Google Cloud Project. To learn how to create and authenticate a **Gmail Developer App** read [our setup guide](https://pipedream.com/apps/gmail-custom-oauth). +If you continue to experience issues, please contact Pipedream support for further assistance. diff --git a/components/gmail/actions/add-label-to-email/add-label-to-email.mjs b/components/gmail/actions/add-label-to-email/add-label-to-email.mjs new file mode 100644 index 0000000000000..ac27b17dbf3b7 --- /dev/null +++ b/components/gmail/actions/add-label-to-email/add-label-to-email.mjs @@ -0,0 +1,41 @@ +import gmail from "../../gmail.app.mjs"; + +export default { + key: "gmail-add-label-to-email", + name: "Add Label to Email", + description: "Add label(s) to an email message. [See the docs](https://developers.google.com/gmail/api/reference/rest/v1/users.messages/modify)", + version: "0.0.6", + type: "action", + props: { + gmail, + message: { + propDefinition: [ + gmail, + "message", + ], + }, + addLabelIds: { + propDefinition: [ + gmail, + "label", + ], + type: "string[]", + label: "Labels", + }, + }, + async run({ $ }) { + const { + gmail, + message, + addLabelIds = [], + } = this; + + const response = await gmail.updateLabels({ + message, + addLabelIds, + }); + + $.export("$summary", `Successfully added ${addLabelIds.length} label(s)`); + return response; + }, +}; diff --git a/components/gmail/actions/approve-workflow/approve-workflow.mjs b/components/gmail/actions/approve-workflow/approve-workflow.mjs new file mode 100644 index 0000000000000..2c0ecf1afcb08 --- /dev/null +++ b/components/gmail/actions/approve-workflow/approve-workflow.mjs @@ -0,0 +1,55 @@ +import gmail from "../../gmail.app.mjs"; + +export default { + key: "gmail-approve-workflow", + name: "Approve Workflow", + description: "Suspend the workflow until approved by email. [See the documentation](https://pipedream.com/docs/code/nodejs/rerun#flowsuspend)", + version: "0.0.2", + type: "action", + props: { + gmail, + to: { + propDefinition: [ + gmail, + "to", + ], + }, + subject: { + propDefinition: [ + gmail, + "subject", + ], + }, + body: { + propDefinition: [ + gmail, + "body", + ], + description: "Include an email body to send. Supports HTML", + optional: true, + }, + bodyType: { + propDefinition: [ + gmail, + "bodyType", + ], + hidden: true, + default: "html", + }, + }, + async run({ $ }) { + const { + resume_url, cancel_url, + } = $.flow.suspend(); + const approvalText = `Click here to approve:
${resume_url}

Cancel here:
${cancel_url}`; + const opts = await this.gmail.getOptionsToSendEmail($, { + ...this, + body: this.body + ? `${this.body}

${approvalText}` + : `${approvalText}`, + }); + const response = await this.gmail.sendEmail(opts); + $.export("$summary", `Successfully sent email to ${this.to}`); + return response; + }, +}; diff --git a/components/gmail/actions/create-draft/create-draft.mjs b/components/gmail/actions/create-draft/create-draft.mjs new file mode 100644 index 0000000000000..2caecb090ff6a --- /dev/null +++ b/components/gmail/actions/create-draft/create-draft.mjs @@ -0,0 +1,88 @@ +import gmail from "../../gmail.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; +import utils from "../../common/utils.mjs"; + +export default { + key: "gmail-create-draft", + name: "Create Draft", + description: "Create a draft from your Google Workspace email account. [See the documentation](https://developers.google.com/gmail/api/reference/rest/v1/users.drafts/create)", + version: "0.0.4", + type: "action", + props: { + gmail, + to: { + propDefinition: [ + gmail, + "to", + ], + }, + cc: { + propDefinition: [ + gmail, + "cc", + ], + }, + bcc: { + propDefinition: [ + gmail, + "bcc", + ], + }, + subject: { + propDefinition: [ + gmail, + "subject", + ], + }, + body: { + propDefinition: [ + gmail, + "body", + ], + }, + bodyType: { + propDefinition: [ + gmail, + "bodyType", + ], + }, + attachmentFilenames: { + propDefinition: [ + gmail, + "attachmentFilenames", + ], + }, + attachmentUrlsOrPaths: { + propDefinition: [ + gmail, + "attachmentUrlsOrPaths", + ], + }, + inReplyTo: { + propDefinition: [ + gmail, + "message", + ], + label: "In Reply To", + description: "Specify the `message-id` this email is replying to.", + optional: true, + }, + mimeType: { + propDefinition: [ + gmail, + "mimeType", + ], + }, + }, + async run({ $ }) { + this.attachmentFilenames = utils.parseArray(this.attachmentFilenames); + this.attachmentUrlsOrPaths = utils.parseArray(this.attachmentUrlsOrPaths); + if (this.attachmentFilenames?.length !== this.attachmentUrlsOrPaths?.length) { + throw new ConfigurationError("Must specify the same number of `Attachment Filenames` and `Attachment URLs or Paths`"); + } + const opts = await this.gmail.getOptionsToSendEmail($, this); + const response = await this.gmail.createDraft(opts); + $.export("$summary", "Successfully created a draft message"); + return response; + }, +}; diff --git a/components/gmail/actions/download-attachment/download-attachment.mjs b/components/gmail/actions/download-attachment/download-attachment.mjs new file mode 100644 index 0000000000000..7df20e90e01f2 --- /dev/null +++ b/components/gmail/actions/download-attachment/download-attachment.mjs @@ -0,0 +1,61 @@ +import gmail from "../../gmail.app.mjs"; +import fs from "fs"; +import path from "path"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "gmail-download-attachment", + name: "Download Attachment", + description: "Download an attachment by attachmentId to the /tmp directory. [See the documentation](https://developers.google.com/gmail/api/reference/rest/v1/users.messages.attachments/get)", + version: "0.0.4", + type: "action", + props: { + gmail, + messageId: { + propDefinition: [ + gmail, + "messageWithAttachments", + ], + }, + attachmentId: { + propDefinition: [ + gmail, + "attachmentId", + ({ messageId }) => ({ + messageId, + }), + ], + withLabel: true, + }, + filename: { + type: "string", + label: "Filename", + description: "Name of the new file. Example: `test.jpg`", + optional: true, + }, + }, + async run({ $ }) { + const attachmentId = this.attachmentId.value || this.attachmentId; + + const attachment = await this.gmail.getAttachment({ + messageId: this.messageId, + attachmentId, + }); + + const filename = this.filename || this.attachmentId.label; + if (!filename) { + throw new ConfigurationError("Please enter a filename to save the downloaded file as in the `/tmp` directory."); + } + + const filePath = path.join("/tmp", filename); + const buffer = Buffer.from(attachment.data, "base64"); + fs.writeFileSync(filePath, buffer); + + $.export("$summary", `Successfully created file ${filename} in \`/tmp\` directory`); + + return { + filename, + filePath, + }; + }, +}; diff --git a/components/gmail/actions/find-email/find-email.mjs b/components/gmail/actions/find-email/find-email.mjs new file mode 100644 index 0000000000000..10ecd205e4b89 --- /dev/null +++ b/components/gmail/actions/find-email/find-email.mjs @@ -0,0 +1,57 @@ +import gmail from "../../gmail.app.mjs"; + +export default { + key: "gmail-find-email", + name: "Find Email", + description: "Find an email using Google's Search Engine. [See the docs](https://developers.google.com/gmail/api/reference/rest/v1/users.messages/list)", + version: "0.0.7", + type: "action", + props: { + gmail, + q: { + propDefinition: [ + gmail, + "q", + ], + }, + labels: { + propDefinition: [ + gmail, + "label", + ], + type: "string[]", + label: "Labels", + description: "Only return messages with labels that match all of the specified labels.", + optional: true, + }, + includeSpamTrash: { + type: "boolean", + label: "Include Spam and Trash?", + description: "Include messages from `SPAM` and `TRASH` in the results. Defaults to `false`.", + optional: true, + default: false, + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "Maximum number of messages to return. This field defaults to 100. The maximum allowed value for this field is 500.", + default: 100, + optional: true, + }, + }, + async run({ $ }) { + const { messages = [] } = await this.gmail.listMessages({ + q: this.q, + labelIds: this.labels, + includeSpamTrash: this.includeSpamTrash, + maxResults: this.maxResults, + }); + const messageIds = messages.map(({ id }) => id); + const messagesToEmit = await this.gmail.getMessages(messageIds); + const suffix = messagesToEmit.length === 1 + ? "" + : "s"; + $.export("$summary", `Successfully found ${messagesToEmit.length} message${suffix}`); + return messagesToEmit; + }, +}; diff --git a/components/gmail/actions/list-labels/list-labels.mjs b/components/gmail/actions/list-labels/list-labels.mjs new file mode 100644 index 0000000000000..5588f7092e430 --- /dev/null +++ b/components/gmail/actions/list-labels/list-labels.mjs @@ -0,0 +1,17 @@ +import gmail from "../../gmail.app.mjs"; + +export default { + key: "gmail-list-labels", + name: "List Labels", + description: "List all the existing labels in the connected account. [See the docs](https://developers.google.com/gmail/api/reference/rest/v1/users.labels/list)", + version: "0.0.1", + type: "action", + props: { + gmail, + }, + async run({ $ }) { + const resp = await this.gmail.listLabels(); + $.export("$summary", `Successfully retrieved ${resp.labels.length} labels`); + return resp; + }, +}; diff --git a/components/gmail/actions/remove-label-from-email/remove-label-from-email.mjs b/components/gmail/actions/remove-label-from-email/remove-label-from-email.mjs new file mode 100644 index 0000000000000..b349f91353084 --- /dev/null +++ b/components/gmail/actions/remove-label-from-email/remove-label-from-email.mjs @@ -0,0 +1,42 @@ +import gmail from "../../gmail.app.mjs"; + +export default { + key: "gmail-remove-label-from-email", + name: "Remove Label from Email", + description: "Remove label(s) from an email message. [See the docs](https://developers.google.com/gmail/api/reference/rest/v1/users.messages/modify)", + version: "0.0.3", + type: "action", + props: { + gmail, + message: { + propDefinition: [ + gmail, + "message", + ], + }, + removeLabelIds: { + propDefinition: [ + gmail, + "label", + ], + type: "string[]", + label: "Labels", + description: "The labels to remove from the email", + }, + }, + async run({ $ }) { + const { + gmail, + message, + removeLabelIds = [], + } = this; + + const response = await gmail.updateLabels({ + message, + removeLabelIds, + }); + + $.export("$summary", `Successfully removed ${removeLabelIds.length} label(s)`); + return response; + }, +}; diff --git a/components/gmail/actions/send-email/send-email.mjs b/components/gmail/actions/send-email/send-email.mjs index 95eb2860e6114..271402d401a4e 100644 --- a/components/gmail/actions/send-email/send-email.mjs +++ b/components/gmail/actions/send-email/send-email.mjs @@ -1,11 +1,12 @@ -/* eslint-disable pipedream/props-description */ import gmail from "../../gmail.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; +import utils from "../../common/utils.mjs"; export default { key: "gmail-send-email", name: "Send Email", - description: "Send an email from your Google Workspace email account", - version: "0.1.1", + description: "Send an email from your Google Workspace email account. [See the documentation](https://developers.google.com/gmail/api/reference/rest/v1/users.messages/send)", + version: "0.1.7", type: "action", props: { gmail, @@ -33,6 +34,12 @@ export default { "fromName", ], }, + fromEmail: { + propDefinition: [ + gmail, + "fromEmail", + ], + }, replyTo: { propDefinition: [ gmail, @@ -57,17 +64,26 @@ export default { "bodyType", ], }, - attachments: { + attachmentFilenames: { + propDefinition: [ + gmail, + "attachmentFilenames", + ], + }, + attachmentUrlsOrPaths: { propDefinition: [ gmail, - "attachments", + "attachmentUrlsOrPaths", ], }, inReplyTo: { propDefinition: [ gmail, - "inReplyTo", + "message", ], + label: "In Reply To", + description: "Specify the `message-id` this email is replying to.", + optional: true, }, mimeType: { propDefinition: [ @@ -77,6 +93,11 @@ export default { }, }, async run({ $ }) { + this.attachmentFilenames = utils.parseArray(this.attachmentFilenames); + this.attachmentUrlsOrPaths = utils.parseArray(this.attachmentUrlsOrPaths); + if (this.attachmentFilenames?.length !== this.attachmentUrlsOrPaths?.length) { + throw new ConfigurationError("Must specify the same number of `Attachment Filenames` and `Attachment URLs or Paths`"); + } const opts = await this.gmail.getOptionsToSendEmail($, this); const response = await this.gmail.sendEmail(opts); $.export("$summary", `Successfully sent email to ${this.to}`); diff --git a/components/gmail_custom_oauth/actions/update-org-signature/update-org-signature.mjs b/components/gmail/actions/update-org-signature/update-org-signature.mjs similarity index 79% rename from components/gmail_custom_oauth/actions/update-org-signature/update-org-signature.mjs rename to components/gmail/actions/update-org-signature/update-org-signature.mjs index 1ec9127b8dcf8..589619533f273 100644 --- a/components/gmail_custom_oauth/actions/update-org-signature/update-org-signature.mjs +++ b/components/gmail/actions/update-org-signature/update-org-signature.mjs @@ -1,16 +1,17 @@ import googleCloud from "../../../google_cloud/google_cloud.app.mjs"; import base from "../update-primary-signature/update-primary-signature.mjs"; +import gmail from "../../gmail.app.mjs"; export default { ...base, - key: "gmail_custom_oauth-update-org-signature", + key: "gmail-update-org-signature", name: "Update Signature for Email in Organization", description: `Update the signature for a specific email address in an organization. - A Google Cloud service account with delegated domain-wide authority is required for this action. [See docs here](https://developers.google.com/gmail/api/reference/rest/v1/users.settings.sendAs/update)`, - version: "0.0.5", + A Google Cloud service account with delegated domain-wide authority is required for this action. [See the documentation](https://developers.google.com/gmail/api/reference/rest/v1/users.settings.sendAs/update)`, + version: "0.0.4", type: "action", props: { - gmail: base.props.gmail, + gmail, googleCloud, signature: base.props.signature, email: { @@ -20,6 +21,7 @@ export default { }, }, methods: { + ...base.methods, async createOpts() { /** * Get Service Account credentials from connected Google Cloud account diff --git a/components/gmail/actions/update-primary-signature/update-primary-signature.mjs b/components/gmail/actions/update-primary-signature/update-primary-signature.mjs new file mode 100644 index 0000000000000..5b227efa6d4d2 --- /dev/null +++ b/components/gmail/actions/update-primary-signature/update-primary-signature.mjs @@ -0,0 +1,32 @@ +import gmail from "../../gmail.app.mjs"; + +export default { + key: "gmail-update-primary-signature", + name: "Update Signature for Primary Email Address", + description: "Update the signature for the primary email address. [See the documentation](https://developers.google.com/gmail/api/reference/rest/v1/users.settings.sendAs/update)", + version: "0.0.4", + type: "action", + props: { + gmail, + signature: { + type: "string", + label: "Signature", + description: "The new signature.", + }, + }, + methods: { + async createOpts() { + const { email } = await this.gmail.userInfo(); + return { + signature: this.signature, + email, + }; + }, + }, + async run({ $ }) { + const opts = await this.createOpts(); + const response = await this.gmail.updateSignature(opts); + $.export("$summary", `Successfully updated signature for ${opts.email}`); + return response; + }, +}; diff --git a/components/gmail/common/constants.mjs b/components/gmail/common/constants.mjs index de8d4949af083..1d4ff4a9c2f8f 100644 --- a/components/gmail/common/constants.mjs +++ b/components/gmail/common/constants.mjs @@ -3,9 +3,12 @@ const BODY_TYPES = { HTML: "html", PLAINTEXT: "plaintext", }; -const HISTORICAL_EVENTS = 25; +const HISTORICAL_EVENTS = 10; +const DEFAULT_LIMIT = 100; + export default { USER_ID, BODY_TYPES, HISTORICAL_EVENTS, + DEFAULT_LIMIT, }; diff --git a/components/gmail/common/utils.mjs b/components/gmail/common/utils.mjs new file mode 100644 index 0000000000000..ae408a74c6adb --- /dev/null +++ b/components/gmail/common/utils.mjs @@ -0,0 +1,12 @@ +function parseArray(arr) { + if (!arr) { + return undefined; + } + return typeof arr === "string" + ? JSON.parse(arr) + : arr; +} + +export default { + parseArray, +}; diff --git a/components/gmail/gmail.app.mjs b/components/gmail/gmail.app.mjs index 2e633ff4c9530..35632c6042280 100644 --- a/components/gmail/gmail.app.mjs +++ b/components/gmail/gmail.app.mjs @@ -4,9 +4,10 @@ import { ConfigurationError, } from "@pipedream/platform"; import { convert } from "html-to-text"; -import mime from "mime"; +import mime from "mime/types/standard.js"; import MailComposer from "nodemailer/lib/mail-composer/index.js"; import constants from "./common/constants.mjs"; +import { google } from "googleapis"; export default { type: "app", @@ -15,12 +16,17 @@ export default { message: { type: "string", label: "Message", - async options({ prevContext }) { + description: "The identifier of a message", + useQuery: true, + async options({ + prevContext, query, + }) { const { messages, nextPageToken, } = await this.listMessages({ pageToken: prevContext?.nextPageToken, + q: query, }); const options = await Promise.all(messages.map(async (message) => { @@ -41,7 +47,41 @@ export default { }; }, }, - + messageWithAttachments: { + type: "string", + label: "Message", + description: "The identifier of a message", + useQuery: true, + async options({ + prevContext, query, + }) { + const { + messages, + nextPageToken, + } = await this.listMessages({ + pageToken: prevContext?.nextPageToken, + q: query, + }); + const options = await Promise.all(messages.map(async (message) => { + const { payload } = await this.getMessage({ + id: message.id, + }); + const { value: subject } = payload.headers.find(({ name }) => name === "Subject"); + const hasAttachments = payload?.parts?.filter(({ body }) => body.attachmentId ); + return { + label: subject, + value: message.id, + hasAttachments: !!hasAttachments?.length, + }; + })); + return { + options: options?.filter(({ hasAttachments }) => hasAttachments) || [], + context: { + nextPageToken, + }, + }; + }, + }, label: { type: "string", label: "Label", @@ -54,6 +94,27 @@ export default { })); }, }, + messageLabels: { + type: "string[]", + label: "Message Labels", + description: "Labels are used to categorize messages and threads within the user's mailbox", + async options({ + messageId, type = "add", + }) { + const { labels } = await this.listLabels(); + const { labelIds } = await this.getMessage({ + id: messageId, + }); + return labels + .filter(({ id }) => type === "add" + ? !labelIds.includes(id) + : labelIds.includes(id)) + .map((label) => ({ + label: label.name, + value: label.id, + })); + }, + }, signature: { type: "string", label: "Signature", @@ -83,6 +144,27 @@ export default { } }, }, + attachmentId: { + type: "string", + label: "Attachment", + description: "Identifier of the attachment to download", + async options({ messageId }) { + try { + const { payload: { parts } } = await this.getMessage({ + id: messageId, + }); + return parts?.filter(({ body }) => body.attachmentId ) + ?.map(({ + body, filename, + }) => ({ + value: body.attachmentId, + label: filename, + })) || []; + } catch { + return []; + } + }, + }, q: { type: "string", label: "Search Query", @@ -113,6 +195,18 @@ export default { description: "Specify the name that will be displayed in the \"From\" section of the email.", optional: true, }, + fromEmail: { + type: "string", + label: "From Email", + description: "Specify the email address that will be displayed in the \"From\" section of the email.", + optional: true, + async options() { + const { sendAs } = await this.listSignatures(); + return sendAs + .filter(({ sendAsEmail }) => sendAsEmail) + .map(({ sendAsEmail }) => sendAsEmail); + }, + }, replyTo: { type: "string", label: "Reply To", @@ -125,7 +219,7 @@ export default { description: "Specify a subject for the email.", }, body: { - type: "any", + type: "string", label: "Email Body", description: "Include an email body as either plain text or HTML. If HTML, make sure to set the \"Body Type\" prop to `html`.", }, @@ -137,10 +231,16 @@ export default { default: "plaintext", options: Object.values(constants.BODY_TYPES), }, - attachments: { - type: "object", - label: "Attachments", - description: "Add any attachments you'd like to include as objects. The `key` should be the **filename** and the `value` should be the **url** for the attachment. The **filename** must contain the file extension (i.e. `.jpeg`, `.txt`) and the **url** is the download link for the file.", + attachmentFilenames: { + type: "string[]", + label: "Attachment Filenames", + description: "Array of the names of the files to attach. Must contain the file extension (e.g. `.jpeg`, `.txt`). Use in conjuction with `Attachment URLs or Paths`.", + optional: true, + }, + attachmentUrlsOrPaths: { + type: "string[]", + label: "Attachment URLs or Paths", + description: "Array of the URLs of the download links for the files, or the local paths (e.g. `/tmp/my-file.txt`). Use in conjuction with `Attachment Filenames`.", optional: true, }, inReplyTo: { @@ -155,15 +255,18 @@ export default { description: "Mime Type of attachments. Setting the mime-type will override using the filename extension to determine attachment's content type.", optional: true, options() { - return Object.values(mime._types); + return Object.keys(mime); }, }, }, methods: { + getToken() { + return this.$auth.oauth_access_token; + }, _client() { const auth = new gmail.auth.OAuth2(); auth.setCredentials({ - access_token: this.$auth.oauth_access_token, + access_token: this.getToken(), }); return gmail.gmail({ version: "v1", @@ -175,11 +278,12 @@ export default { name: fromName, email, } = await this.userInfo(); + const fromEmail = props.fromEmail || email; const opts = { from: props.fromName - ? `${props.fromName} <${email}>` - : `${fromName} <${email}>`, + ? `${props.fromName} <${fromEmail}>` + : `${fromName} <${fromEmail}>`, to: props.to, cc: props.cc, bcc: props.bcc, @@ -204,15 +308,14 @@ export default { } } - if (props.attachments) { - opts.attachments = Object.entries(props.attachments) - .map(([ - filename, - path, - ]) => ({ - filename, - path, - })); + if (props.attachmentFilenames?.length && props.attachmentUrlsOrPaths?.length) { + opts.attachments = []; + for (let i = 0; i < props.attachmentFilenames.length; i++) { + opts.attachments.push({ + filename: props.attachmentFilenames[i], + path: props.attachmentUrlsOrPaths[i], + }); + } } if (props.bodyType === constants.BODY_TYPES.HTML) { @@ -247,10 +350,11 @@ export default { return data; }, async getMessage({ id }) { - const { data } = await this._client().users.messages.get({ + const fn = () => this._client().users.messages.get({ userId: constants.USER_ID, id, }); + const { data } = await this.retryWithExponentialBackoff(fn); return data; }, async listHistory(opts = {}) { @@ -273,6 +377,14 @@ export default { })); return Promise.all(promises); }, + async *getAllMessages(ids = []) { + for (const id of ids) { + const message = await this.getMessage({ + id, + }); + yield message; + } + }, async listSignatures() { const { data } = await this._client().users.settings.sendAs.list({ userId: constants.USER_ID, @@ -333,16 +445,15 @@ export default { } } }, - async addLabelToEmail({ - message, label, + async updateLabels({ + message, addLabelIds = [], removeLabelIds = [], }) { const response = await this._client().users.messages.modify({ userId: constants.USER_ID, id: message, requestBody: { - addLabelIds: [ - label, - ], + addLabelIds, + removeLabelIds, }, }); return response.data; @@ -374,5 +485,61 @@ export default { } } }, + _serviceAccountAuth(credentials, impersonatedUser) { + const scopes = [ + "https://www.googleapis.com/auth/gmail.settings.basic", + ]; + return new google.auth.JWT( + credentials.client_email, + null, + credentials.private_key, + scopes, + impersonatedUser, + ); + }, + async updateSignature({ + signature, email, credentials, + }) { + const opts = { + userId: "me", + sendAsEmail: email, + requestBody: { + signature, + }, + }; + if (credentials) opts.auth = this._serviceAccountAuth(credentials, email); + return this._client().users.settings.sendAs.patch(opts); + }, + async getAttachment({ + messageId, attachmentId, + }) { + const { data } = await this._client().users.messages.attachments.get({ + userId: "me", + messageId, + id: attachmentId, + }); + return data; + }, + retryWithExponentialBackoff(func, maxAttempts = 3, baseDelayS = 2) { + let attempt = 0; + + const execute = async () => { + try { + return await func(); + } catch (error) { + if (attempt >= maxAttempts) { + throw error; + } + + const delayMs = Math.pow(baseDelayS, attempt) * 1000; + await new Promise((resolve) => setTimeout(resolve, delayMs)); + + attempt++; + return execute(); + } + }; + + return execute(); + }, }, }; diff --git a/components/gmail/package.json b/components/gmail/package.json index d8e1ec0a69022..7095b9626eaf5 100644 --- a/components/gmail/package.json +++ b/components/gmail/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/gmail", - "version": "0.0.12", + "version": "0.2.1", "description": "Pipedream Gmail Components", "main": "gmail.app.mjs", "keywords": [ @@ -14,12 +14,14 @@ }, "dependencies": { "@google-cloud/local-auth": "^2.1.0", + "@google-cloud/pubsub": "^4.7.0", "@googleapis/gmail": "^0.3.4", - "@pipedream/platform": "^1.1.1", + "@pipedream/platform": "^3.0.3", "google-auth-library": "^8.7.0", "googleapis": "^105.0.0", "html-to-text": "^8.2.1", "mime": "^3.0.0", - "nodemailer": "^6.7.8" + "nodemailer": "^6.7.8", + "uuid": "^10.0.0" } } diff --git a/components/gmail/sources/common/base.mjs b/components/gmail/sources/common/base.mjs new file mode 100644 index 0000000000000..fcd3d7714119b --- /dev/null +++ b/components/gmail/sources/common/base.mjs @@ -0,0 +1,92 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + isValidType(data) { + return typeof(data) === "string" + || data instanceof Buffer + || ArrayBuffer.isView(data); + }, + decodeContent(message) { + const MULTIPART_MIME_TYPE = "multipart"; + const { + payload: { + mimeType, body, parts, + }, + } = message; + + if (!mimeType.startsWith(MULTIPART_MIME_TYPE)) { + return !this.isValidType(body?.data) + ? message + : { + ...message, + decodedContent: Buffer.from(body.data, "base64").toString(), + }; + } + + const [ + firstPart, + ] = parts; + + return !this.isValidType(firstPart?.body?.data) + ? message + : { + ...message, + decodedContent: Buffer.from(firstPart.body.data, "base64").toString(), + }; + }, + parseEmail(emailStr) { + if (!emailStr) { + return undefined; + } + + const regex = /^(.*)<(.+)>$/; + const match = emailStr.match(regex); + + if (match) { + return { + name: match[1].trim(), + email: match[2].trim(), + }; + } + return { + name: null, + email: emailStr.trim(), + }; + }, + getHeaderValue(headers, key) { + return headers.find(({ name }) => name.toLowerCase() === key)?.value; + }, + processEmail(msg) { + // Process and structure the email data + const headers = msg.payload.headers; + return { + "id": msg.id, + "threadId": msg.threadId, + "subject": this.getHeaderValue(headers, "subject"), + "from": this.parseEmail(this.getHeaderValue(headers, "from")), + "to": this.parseEmail(this.getHeaderValue(headers, "to")), + "reply-to": this.parseEmail(this.getHeaderValue(headers, "reply-to")), + "date": this.getHeaderValue(headers, "date"), + "snippet": msg.snippet, + }; + }, + emitEvent(message) { + message = { + ...message, + parsedHeaders: this.processEmail(message), + }; + const meta = this.generateMeta(message); + this.$emit(this.decodeContent(message), meta); + }, + }, +}; diff --git a/components/gmail/sources/common/polling-history.mjs b/components/gmail/sources/common/polling-history.mjs new file mode 100644 index 0000000000000..9cd47f49eda67 --- /dev/null +++ b/components/gmail/sources/common/polling-history.mjs @@ -0,0 +1,120 @@ +import common from "./base.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + ...common, + hooks: { + ...common.hooks, + async deploy() { + const historyId = await this.getHistoryId(); + if (!historyId) { + return; + } + this._setLastHistoryId(historyId); + await this.emitRecentMessages(); + }, + }, + methods: { + ...common.methods, + _getLastHistoryId() { + return this.db.get("lastHistoryId"); + }, + _setLastHistoryId(lastHistoryId) { + this.db.set("lastHistoryId", lastHistoryId); + }, + async getHistoryId() { + const params = { + maxResults: constants.HISTORICAL_EVENTS, + }; + if (this.labels?.length) { + params.labelIds = this.labels; + } + let { messages } = await this.gmail.listMessages({ + ...params, + }); + if (!messages?.length) { + return; + } + const messageIds = messages.map(({ id }) => id); + messages = await this.gmail.getMessages(messageIds); + messages = messages.sort((a, b) => (a.internalDate - b.internalDate)); + const { historyId } = await this.gmail.getMessage({ + id: messages[messages.length - 1].id, + }); + return historyId; + }, + async emitHistories(startHistoryId) { + const opts = { + startHistoryId, + historyTypes: this.getHistoryTypes(), + }; + + const length = this.labels?.length > 0 + ? this.labels.length + : 1; + let maxHistoryId = 0; + + for (let i = 0; i < length; i++) { + if (this.labels) { + opts.labelId = this.labels[i]; + } + + const { + history, historyId, + } = await this.gmail.listHistory(opts); + + if (!history) { + continue; + } + + maxHistoryId = Math.max(maxHistoryId, historyId); + const responseArray = this.filterHistory(history); + + for (const item of responseArray) { + await this.emitFullMessage(item.messages[0].id); + } + } + if (maxHistoryId > 0) { + this._setLastHistoryId(maxHistoryId); + } + }, + async emitRecentMessages() { + const opts = { + maxResults: constants.HISTORICAL_EVENTS, + }; + if (this.labels?.length) { + opts.labelIds = this.labels; + } + let { messages } = await this.gmail.listMessages(opts); + if (!messages?.length) { + return; + } + messages = messages.sort((a, b) => (a.internalDate - b.internalDate)); + for (const message of messages) { + await this.emitFullMessage(message.id); + } + }, + async emitFullMessage(id) { + let message; + try { + message = await this.gmail.getMessage({ + id, + }); + if (this.excludeLabels && message.labelIds.some((i) => this.excludeLabels.includes(i))) { + return; + } + } catch { + console.log(`Message ${id} not found`); + } + this.emitEvent(message); + }, + }, + async run() { + let lastHistoryId = this._getLastHistoryId(); + + if (!lastHistoryId) { + lastHistoryId = await this.getHistoryId(); + } + await this.emitHistories(lastHistoryId); + }, +}; diff --git a/components/gmail/sources/common/polling-messages.mjs b/components/gmail/sources/common/polling-messages.mjs new file mode 100644 index 0000000000000..a7a1de404fa86 --- /dev/null +++ b/components/gmail/sources/common/polling-messages.mjs @@ -0,0 +1,70 @@ +import common from "./base.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + ...common, + hooks: { + ...common.hooks, + async deploy() { + const messageIds = await this.getMessageIds(constants.HISTORICAL_EVENTS); + if (!messageIds?.length) { + return; + } + await this.processHistoricalEvents(messageIds); + }, + }, + methods: { + ...common.methods, + getLastDate() { + return this.db.get("lastDate"); + }, + setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + constructQuery(lastDate) { + const { q: query } = this; + const after = !query?.includes("after:") && lastDate + ? `after:${lastDate / 1000}` + : ""; + return [ + after, + query, + ].join(" ").trim(); + }, + async getMessageIds(max, lastDate = 0) { + console.log("Polling for new messages..."); + const { messages } = await this.gmail.listMessages({ + q: this.constructQuery(lastDate), + labelIds: this.getLabels(), + maxResults: max, + }); + return messages?.map((message) => message.id); + }, + async processMessageIds(messageIds, lastDate) { + let maxDate = lastDate; + const messages = this.gmail.getAllMessages(messageIds); + for await (const message of messages) { + if (message.internalDate >= lastDate) { + this.emitEvent(message); + maxDate = Math.max(maxDate, message.internalDate); + } + } + if (maxDate) this.setLastDate(maxDate); + }, + async processHistoricalEvents(messageIds) { + let messages = await this.gmail.getMessages(messageIds); + messages = messages.sort((a, b) => (a.internalDate - b.internalDate)); + this.setLastDate(messages[messages.length - 1].internalDate); + messages.forEach((message) => this.emitEvent(message)); + }, + }, + async run() { + const lastDate = this.getLastDate(); + const messageIds = await this.getMessageIds(constants.DEFAULT_LIMIT, lastDate); + if (!messageIds?.length) { + console.log("There are no new messages. Exiting..."); + return; + } + await this.processMessageIds(messageIds.reverse(), lastDate); + }, +}; diff --git a/components/gmail/sources/common/verify-client-id.mjs b/components/gmail/sources/common/verify-client-id.mjs new file mode 100644 index 0000000000000..79b6b2e577332 --- /dev/null +++ b/components/gmail/sources/common/verify-client-id.mjs @@ -0,0 +1,9 @@ +import { PD_OFFICIAL_GMAIL_OAUTH_CLIENT_ID } from "@pipedream/platform"; + +export default { + methods: { + async checkClientId() { + return !this.gmail.$auth.oauth_client_id.includes(PD_OFFICIAL_GMAIL_OAUTH_CLIENT_ID); + }, + }, +}; diff --git a/components/gmail/sources/new-attachment-received/new-attachment-received.mjs b/components/gmail/sources/new-attachment-received/new-attachment-received.mjs new file mode 100644 index 0000000000000..9e0942fc3b608 --- /dev/null +++ b/components/gmail/sources/new-attachment-received/new-attachment-received.mjs @@ -0,0 +1,70 @@ +import gmail from "../../gmail.app.mjs"; +import common from "../common/polling-messages.mjs"; + +export default { + ...common, + key: "gmail-new-attachment-received", + name: "New Attachment Received", + description: "Emit new event for each attachment in a message received. This source is capped at 100 max new messages per run.", + version: "0.0.6", + type: "source", + dedupe: "unique", + props: { + ...common.props, + gmail, + q: { + propDefinition: [ + gmail, + "q", + ], + }, + labels: { + propDefinition: [ + gmail, + "label", + ], + type: "string[]", + label: "Labels", + optional: true, + }, + }, + methods: { + ...common.methods, + constructQuery(lastDate) { + const { q: query } = this; + const hasAttachment = query?.includes("has:attachment") + ? "" + : "has:attachment"; + const after = !query?.includes("after:") && lastDate + ? `after:${lastDate / 1000}` + : ""; + return [ + hasAttachment, + after, + query, + ].join(" ").trim(); + }, + getLabels() { + return this.labels; + }, + generateMeta(attachment, message) { + return { + id: `${message.id}${attachment.partId}`, + summary: `New Attachment: ${attachment.filename}`, + ts: +message.internalDate, + }; + }, + emitEvent(message) { + if (message) { + const { parts: attachments } = message.payload; + + attachments.filter((attachment) => attachment.body.attachmentId).forEach((attachment) => { + this.$emit({ + message, + attachment, + }, this.generateMeta(attachment, message)); + }); + } + }, + }, +}; diff --git a/components/gmail/sources/new-email-matching-search/new-email-matching-search.mjs b/components/gmail/sources/new-email-matching-search/new-email-matching-search.mjs new file mode 100644 index 0000000000000..110f6fb12bd66 --- /dev/null +++ b/components/gmail/sources/new-email-matching-search/new-email-matching-search.mjs @@ -0,0 +1,46 @@ +import gmail from "../../gmail.app.mjs"; +import common from "../common/polling-messages.mjs"; + +export default { + ...common, + key: "gmail-new-email-matching-search", + name: "New Email Matching Search", + description: "Emit new event when an email matching the search criteria is received. This source is capped at 100 max new messages per run.", + version: "0.0.5", + type: "source", + dedupe: "unique", + props: { + ...common.props, + gmail, + q: { + propDefinition: [ + gmail, + "q", + ], + }, + labels: { + propDefinition: [ + gmail, + "label", + ], + type: "string[]", + label: "Labels", + optional: true, + }, + }, + methods: { + ...common.methods, + getLabels() { + return this.labels; + }, + generateMeta(message) { + const selectedHeader = message.payload.headers.find(({ name }) => name === "Subject"); + const subject = selectedHeader?.value || "No subject"; + return { + id: message.id, + summary: `New email: ${subject}`, + ts: +message.internalDate, + }; + }, + }, +}; diff --git a/components/gmail/sources/new-email-received/README.md b/components/gmail/sources/new-email-received/README.md new file mode 100644 index 0000000000000..9e2ae651691ed --- /dev/null +++ b/components/gmail/sources/new-email-received/README.md @@ -0,0 +1,3 @@ +# Overview + +The Gmail - New Email Received (Instant) source enables you to trigger Pipedream workflows based on real-time changes to your Gmail inbox. diff --git a/components/gmail/sources/new-email-received/new-email-received.mjs b/components/gmail/sources/new-email-received/new-email-received.mjs new file mode 100644 index 0000000000000..d55c748d8753a --- /dev/null +++ b/components/gmail/sources/new-email-received/new-email-received.mjs @@ -0,0 +1,585 @@ +import gmail from "../../gmail.app.mjs"; +import common from "../common/polling-history.mjs"; +import { + axios, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + ConfigurationError, +} from "@pipedream/platform"; +import { PubSub } from "@google-cloud/pubsub"; +import { v4 as uuidv4 } from "uuid"; +import verifyClient from "../common/verify-client-id.mjs"; + +export default { + ...common, + key: "gmail-new-email-received", + name: "New Email Received", + description: "Emit new event when a new email is received.", + type: "source", + version: "0.1.10", + dedupe: "unique", + props: { + gmail, + db: "$.service.db", + triggerType: { + type: "string", + label: "Trigger Type", + options: [ + "webhook", + "polling", + ], + description: + "Configuring this source as a `webhook` (instant) trigger requires a custom OAuth client. [Refer to the guide here to get started](https://pipedream.com/apps/gmail/#getting-started).", + reloadProps: true, + }, + serviceAccountKeyJson: { + type: "string", + label: "Service Account Key JSON", + optional: true, + hidden: true, + reloadProps: true, + }, + serviceAccountKeyJsonInstructions: { + type: "alert", + alertType: "info", + content: `1) [Create a service account in GCP](https://cloud.google.com/iam/docs/creating-managing-service-accounts) and set the following permission: **Pub/Sub Admin** + \n2) [Generate a service account key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) + \n3) Download the key details in JSON format + \n4) Open the JSON in a text editor, and **copy and paste its contents here**. + `, + hidden: true, + }, + topicType: { + type: "string", + label: "Pub/Sub Topic", + description: + "Do you have an existing Pub/Sub topic, or would you like to create a new one?", + options: [ + "existing", + "new", + ], + optional: true, + hidden: true, + reloadProps: true, + }, + topic: { + type: "string", + label: "Pub/Sub Topic Name", + description: "Select a Pub/Sub topic from your GCP account to watch", + async options() { + return this.getTopics(); + }, + optional: true, + hidden: true, + reloadProps: true, + }, + labels: { + propDefinition: [ + gmail, + "label", + ], + type: "string[]", + label: "Labels", + default: [ + "INBOX", + ], + optional: true, + hidden: true, + }, + excludeLabels: { + propDefinition: [ + gmail, + "label", + ], + type: "string[]", + label: "Exclude Labels", + description: "Emails with the specified labels will be excluded from results", + optional: true, + hidden: true, + }, + permissionAlert: { + type: "alert", + alertType: "error", + content: `Unable to grant publish permission to Gmail API service account. + \n1. Navigate to your [Google Cloud PubSub Topics List](https://console.cloud.google.com/cloudpubsub) + \n2. Select "View Permissions" for the topic you intend to use for this source. + \n3. Click "ADD PRINCIPAL" + \n4. Select "Pub/Sub Publisher" for the Role. + \n5. Enter \`serviceAccount:gmail-api-push@system.gserviceaccount.com\` as the principal. + \n6. Click "Save" + `, + hidden: true, + }, + latencyWarningAlert: { + type: "alert", + alertType: "warning", + content: + "Please allow up to 1 minute for deployment. We're setting up your real-time email notifications behind the scenes.", + hidden: true, + }, + }, + async additionalProps(props) { + const newProps = {}; + if (this.triggerType === "polling") { + newProps.timer = { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }; + } + if (this.triggerType === "webhook") { + // verify that a Custom OAuth client is being used + const isValidClientId = await this.checkClientId(); + if (!isValidClientId) { + throw new ConfigurationError( + "Configuring this source as a `webhook` (instant) trigger requires a custom OAuth client. [Refer to the guide here to get started](https://pipedream.com/apps/gmail/#getting-started).", + ); + } + + newProps.http = { + type: "$.interface.http", + customResponse: true, + }; + newProps.timer = { + type: "$.interface.timer", + default: { + intervalSeconds: 60 * 60, + }, + hidden: true, + }; + + props.serviceAccountKeyJson.hidden = false; + props.serviceAccountKeyJson.optional = false; + props.serviceAccountKeyJsonInstructions.hidden = false; + + if (!this.serviceAccountKeyJson) { + return newProps; + } + + props.topicType.hidden = false; + props.topicType.optional = false; + + if (!this.topicType) { + return newProps; + } + + // create topic prop + let topicName = this.topic; + if (this.topicType === "new") { + const authKeyJSON = JSON.parse(this.serviceAccountKeyJson); + const { project_id: projectId } = authKeyJSON; + topicName = `projects/${projectId}/topics/${this.convertNameToValidPubSubTopicName( + uuidv4(), + )}`; + props.topic.default = topicName; + props.topic.reloadProps = false; + } else { + props.topic.hidden = false; + props.topic.optional = false; + } + + if (this.topic || this.topicType === "new") { + const topic = await this.getOrCreateTopic(topicName); + + // Retrieves the IAM policy for the topic + let hasPublisherRole; + try { + const [ + policy, + ] = await topic.iam.getPolicy(); + hasPublisherRole = policy.bindings.find( + ({ + members, role, + }) => + members.includes( + "serviceAccount:gmail-api-push@system.gserviceaccount.com", + ) && role === "roles/pubsub.publisher", + ); + } catch { + console.log("Could not retrieve iam policy"); + } + + if (!hasPublisherRole) { + // Grant publish permission to Gmail API service account + try { + await topic.iam.setPolicy({ + bindings: [ + { + role: "roles/pubsub.publisher", + members: [ + "serviceAccount:gmail-api-push@system.gserviceaccount.com", + ], + }, + ], + }); + console.log("Permissions granted to Gmail API service account."); + } catch { + props.permissionAlert.hidden = false; + return newProps; + } + } + + props.latencyWarningAlert.hidden = false; + + const { + historyId, expiration, + } = await this.setupGmailNotifications(topicName); + newProps.initialHistoryId = { + type: "string", + default: historyId, + hidden: true, + }; + newProps.expiration = { + type: "string", + default: expiration, + hidden: true, + }; + } + } + props.labels.hidden = false; + props.excludeLabels.hidden = false; + return newProps; + }, + hooks: { + ...common.hooks, + async activate() { + if (this.triggerType === "polling") { + return; + } + + const sdkParams = this.sdkParams(); + const pubSubClient = new PubSub(sdkParams); + + const currentTopic = { + name: this.topic, + }; + + // Create subscription + const pushEndpoint = this.http.endpoint; + const subscriptionName = + this.convertNameToValidPubSubTopicName(pushEndpoint); + const subscriptionOptions = { + pushConfig: { + pushEndpoint, + }, + }; + const [ + subscriptionResult, + ] = await pubSubClient + .topic(currentTopic.name) + .createSubscription(subscriptionName, subscriptionOptions); + this._setSubscriptionName(subscriptionResult.name); + }, + async deactivate() { + if (this.triggerType === "polling") { + return; + } + + const sdkParams = this.sdkParams(); + const pubSubClient = new PubSub(sdkParams); + + const subscriptionName = this._getSubscriptionName(); + if (subscriptionName) { + await pubSubClient.subscription(subscriptionName).delete(); + } + }, + }, + methods: { + ...verifyClient.methods, + ...common.methods, + _getTopicName() { + return this.db.get("topicName"); + }, + _setTopicName(topicName) { + this.db.set("topicName", topicName); + }, + _getSubscriptionName() { + return this.db.get("subscriptionName"); + }, + _setSubscriptionName(subscriptionName) { + this.db.set("subscriptionName", subscriptionName); + }, + _getLastProcessedHistoryId() { + return this.db.get("lastProcessedHistoryId"); + }, + _setLastProcessedHistoryId(lastProcessedHistoryId) { + this.db.set("lastProcessedHistoryId", lastProcessedHistoryId); + }, + _getExpiration() { + return this.db.get("expiration"); + }, + _setExpiration(expiration) { + this.db.set("expiration", expiration); + }, + _getLastReceivedTime() { + return this.db.get("lastReceivedTime"); + }, + _setLastReceivedTime(lastReceivedTime) { + this.db.set("lastReceivedTime", lastReceivedTime); + }, + sdkParams() { + const authKeyJSON = JSON.parse(this.serviceAccountKeyJson); + const { + project_id: projectId, client_email, private_key, + } = authKeyJSON; + const sdkParams = { + credentials: { + client_email, + private_key, + }, + projectId, + }; + return sdkParams; + }, + async getTopics() { + const sdkParams = this.sdkParams(); + const pubSubClient = new PubSub(sdkParams); + const topics = (await pubSubClient.getTopics())[0]; + if (topics.length > 0) { + return topics.map((topic) => topic.name); + } + return []; + }, + convertNameToValidPubSubTopicName(name) { + // For valid names, see https://cloud.google.com/pubsub/docs/admin#resource_names + return ( + name + // Must not start with `goog`. We add a `pd-` at the beginning if that's the case. + .replace(/(^goog.*)/g, "pd-$1") + // Must start with a letter, otherwise we add `pd-` at the beginning. + .replace(/^(?![a-zA-Z]+)/, "pd-") + // Only certain characters are allowed, the rest will be replaced with a `-`. + .replace(/[^a-zA-Z0-9_\-.~+%]+/g, "-") + ); + }, + makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: `https://gmail.googleapis.com/gmail/v1${path}`, + headers: { + Authorization: `Bearer ${this.gmail.getToken()}`, + }, + ...opts, + }); + }, + async setupGmailNotifications(topicName) { + // Set up Gmail push notifications using OAuth token + const watchResponse = await this.makeRequest({ + method: "POST", + path: "/users/me/watch", + data: { + topicName, + labelIds: this.labels || [ + "INBOX", + ], + }, + }); + console.log("Watch response:", watchResponse); + return watchResponse; + }, + async getOrCreateTopic(name) { + const sdkParams = this.sdkParams(); + const pubSubClient = new PubSub(sdkParams); + const topicName = name || this.topic; + // Create or get Pub/Sub topic + let topic; + try { + [ + topic, + ] = await pubSubClient.createTopic(topicName); + console.log(`Topic ${topicName} created.`); + } catch (error) { + if (error.code === 6) { + // Already exists + topic = pubSubClient.topic(topicName); + } else { + throw error; + } + } + return topic; + }, + getHistoryTypes() { + return [ + "messageAdded", + ]; + }, + generateMeta(message) { + return { + id: message.id, + summary: message.snippet, + ts: +message.internalDate, + }; + }, + filterHistory(history) { + let filteredHistory = history.filter((item) => item.messagesAdded?.length); + if (this.labels) { + filteredHistory = filteredHistory.filter((item) => + item.messagesAdded[0].message.labelIds && + item.messagesAdded[0].message.labelIds.some((i) => this.labels.includes(i))); + } + if (this.excludeLabels) { + filteredHistory = filteredHistory.filter((item) => + item.messagesAdded[0].message.labelIds && + !(item.messagesAdded[0].message.labelIds.some((i) => this.excludeLabels.includes(i)))); + } + return filteredHistory; + }, + async getMessageDetails(ids) { + const messages = await Promise.all(ids.map(async (id) => { + try { + const message = await this.gmail.getMessage({ + id, + }); + return message; + } catch { + console.log(`Could not find message ${id}`); + return null; + } + })); + return messages; + }, + async getHistoryResponses(startHistoryId) { + const historyResponses = []; + for (const labelId of this.labels) { + const response = await this.gmail.listHistory({ + startHistoryId, + historyTypes: [ + "messageAdded", + ], + labelId, + }); + historyResponses.push(response); + } + return historyResponses; + }, + }, + async run(event) { + if (this.triggerType === "polling") { + let lastHistoryId = this._getLastHistoryId(); + + if (!lastHistoryId) { + lastHistoryId = await this.getHistoryId(); + } + await this.emitHistories(lastHistoryId); + } + + if (this.triggerType === "webhook") { + if (event.timestamp) { + // event was triggered by timer + const topicName = this._getTopicName(); + if (topicName) { + // renew Gmail push notifications if expiring within the next hour + // or if no email has been received within the last hour + const currentExpiration = this._getExpiration(); + const lastReceivedTime = this._getLastReceivedTime(); + if ( + (+currentExpiration < (event.timestamp + 3600) * 1000) + || (lastReceivedTime < (event.timestamp - 3600) * 1000) + ) { + const { expiration } = await this.setupGmailNotifications(topicName); + this._setExpiration(expiration); + } + return; + } else { + // first run, no need to renew push notifications + this._setTopicName(this.topic); + const initialHistoryId = this.initialHistoryId || this._getLastHistoryId(); + this._setLastProcessedHistoryId(initialHistoryId); + this._setExpiration(this.expiration); + return; + } + } + + this.http.respond({ + status: 200, + }); + + // Extract the Pub/Sub message data + const pubsubMessage = event.body.message; + if (!pubsubMessage) { + return; + } + const decodedData = JSON.parse( + Buffer.from(pubsubMessage.data, "base64").toString(), + ); + + console.log("Decoded Pub/Sub data:", decodedData); + + const { historyId: receivedHistoryId } = decodedData; + + // Retrieve the last processed historyId + const lastProcessedHistoryId = this._getLastProcessedHistoryId(); + console.log("Last processed historyId:", lastProcessedHistoryId); + + // Use the minimum of lastProcessedHistoryId and the received historyId + let startHistoryId = Math.min( + parseInt(lastProcessedHistoryId), + parseInt(receivedHistoryId), + ); + console.log("Using startHistoryId:", startHistoryId); + + // Fetch the history + let historyResponses; + try { + historyResponses = await this.getHistoryResponses(startHistoryId); + } catch { + // catch error thrown if startHistoryId is invalid or expired + + // emit recent messages to attempt to avoid missing any messages + await this.emitRecentMessages(); + + // set startHistoryId to the historyId received from the webhook + startHistoryId = parseInt(receivedHistoryId); + console.log("Using startHistoryId:", startHistoryId); + historyResponses = await this.getHistoryResponses(startHistoryId); + } + + console.log( + "History responses:", + JSON.stringify(historyResponses, null, 2), + ); + + // Process history to find new messages + const newMessages = []; + for (const historyResponse of historyResponses) { + if (historyResponse.history) { + const historyResponseFiltered = this.filterHistory(historyResponse.history); + for (const historyItem of historyResponseFiltered) { + newMessages.push( + ...historyItem.messagesAdded.map((msg) => msg.message), + ); + } + } + } + + console.log("New messages found:", newMessages.length); + + // Fetch full message details for new messages + const newMessageIds = newMessages?.map(({ id }) => id) || []; + const messageDetails = await this.getMessageDetails(newMessageIds); + + if (!messageDetails?.length) { + return; + } + + console.log("Fetched message details count:", messageDetails.length); + + // Store the latest historyId in the db + let latestHistoryId = receivedHistoryId; + for (const historyResponse of historyResponses) { + latestHistoryId = Math.max(latestHistoryId, historyResponse.historyId); + } + this._setLastProcessedHistoryId(latestHistoryId); + console.log("Updated lastProcessedHistoryId:", latestHistoryId); + + this._setLastReceivedTime(Date.now()); + + messageDetails.forEach((message) => { + if (message?.id) { + this.emitEvent(message); + } + }); + } + }, +}; diff --git a/components/gmail/sources/new-labeled-email/new-labeled-email.mjs b/components/gmail/sources/new-labeled-email/new-labeled-email.mjs new file mode 100644 index 0000000000000..5679b00a32226 --- /dev/null +++ b/components/gmail/sources/new-labeled-email/new-labeled-email.mjs @@ -0,0 +1,49 @@ +import gmail from "../../gmail.app.mjs"; +import common from "../common/polling-history.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "gmail-new-labeled-email", + name: "New Labeled Email", + description: "Emit new event when a new email is labeled.", + type: "source", + version: "0.0.7", + dedupe: "unique", + props: { + ...common.props, + gmail, + labels: { + propDefinition: [ + gmail, + "label", + ], + type: "string[]", + label: "Labels", + }, + }, + methods: { + ...common.methods, + getHistoryTypes() { + return [ + "labelAdded", + "messageAdded", + ]; + }, + generateMeta(message) { + return { + id: `${message.id}-${message.historyId}`, + summary: `A new message with ID: ${message.id} was labeled"`, + ts: +message.internalDate, + }; + }, + filterHistory(history) { + return history.filter((item) => + (item.labelsAdded && item.labelsAdded[0].labelIds.some((i) => this.labels.includes(i))) + || (item.messagesAdded + && item.messagesAdded[0].message.labelIds + && item.messagesAdded[0].message.labelIds.some((i) => this.labels.includes(i)))); + }, + }, + sampleEmit, +}; diff --git a/components/gmail/sources/new-labeled-email/test-event.mjs b/components/gmail/sources/new-labeled-email/test-event.mjs new file mode 100644 index 0000000000000..ef2a280a89f79 --- /dev/null +++ b/components/gmail/sources/new-labeled-email/test-event.mjs @@ -0,0 +1,62 @@ +export default { + "id": "1669d3bc6ac196e4", + "threadId": "1669d3bc6ac196e4", + "labelIds": [ + "Label_17", + "CATEGORY_PERSONAL" + ], + "snippet": "Hello", + "payload": { + "partId": "", + "mimeType": "multipart/alternative", + "filename": "", + "headers": [], + "body": { + "size": 0 + }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { + "name": "Content-Type", + "value": "text/plain; charset=\"UTF-8\"" + }, + { + "name": "Content-Transfer-Encoding", + "value": "quoted-printable" + } + ], + "body": { + "size": 69, + "data": "SGV5L8NCuGQpw0K" + } + }, + { + "partId": "1", + "mimeType": "text/html", + "filename": "", + "headers": [ + { + "name": "Content-Type", + "value": "text/html; charset=\"UTF-8\"" + }, + { + "name": "Content-Transfer-Encoding", + "value": "quoted-printable" + } + ], + "body": { + "size": 390, + "data": "PGRpd9udCB0PjwvZGl2Pg0K" + } + } + ] + }, + "sizeEstimate": 5224, + "historyId": "2423873", + "internalDate": "1540236205000", + "decodedContent": "Hello\r\nᐧ\r\n" +} \ No newline at end of file diff --git a/components/gmail/sources/new-sent-email/new-sent-email.mjs b/components/gmail/sources/new-sent-email/new-sent-email.mjs new file mode 100644 index 0000000000000..5c42690dea4af --- /dev/null +++ b/components/gmail/sources/new-sent-email/new-sent-email.mjs @@ -0,0 +1,37 @@ +import gmail from "../../gmail.app.mjs"; +import common from "../common/polling-messages.mjs"; + +export default { + ...common, + key: "gmail-new-sent-email", + name: "New Sent Email", + description: "Emit new event for each new email sent. (Maximum of 100 events emited per execution)", + version: "0.0.6", + type: "source", + dedupe: "unique", + props: { + ...common.props, + gmail, + q: { + propDefinition: [ + gmail, + "q", + ], + }, + }, + methods: { + ...common.methods, + getLabels() { + return [ + "SENT", + ]; + }, + generateMeta(message) { + return { + id: message.id, + summary: message.snippet, + ts: +message.internalDate, + }; + }, + }, +}; diff --git a/components/gmail_custom_oauth/README.md b/components/gmail_custom_oauth/README.md deleted file mode 100644 index 37330411bb40e..0000000000000 --- a/components/gmail_custom_oauth/README.md +++ /dev/null @@ -1,102 +0,0 @@ -# Overview -By connecting your personal Gmail account to Pipedream, you'll be able to incorporate email into whatever you're building with any of the 900+ apps that are available on Pipedream. - -# Getting Started - -The Google Developer App in Pipedream can integrate with either a personal Gmail account or a Google workspace email account. Either option involves creating a custom Google App in the Google Cloud Console. This process does not involve any code or special approval by Google. The steps are outlined below: - -## Creating a Gmail app - -In order to connect your personal or workspace Gmail account to Pipedream, you'll need to create a custom OAuth app in Google Cloud. - -1. Sign in to the [Google Cloud Console](https://cloud.google.com/) -2. Select an existing project or create a new one - - ![Select an exisiting project or create a a new one in the Google Cloud Console](https://res.cloudinary.com/pipedreamin/image/upload/v1663268100/docs/components/CleanShot_2022-09-15_at_14.54.34_vajyds.png) - -3. Select **APIs & Services** -4. Click **Enable APIs & Services** - - ![Select "Enable APIs & Services to open a menu to enable the Gmail API for Pipedream to connect to](https://res.cloudinary.com/pipedreamin/image/upload/v1663268316/docs/components/CleanShot_2022-09-15_at_14.58.06_jshirk.png) - -5. Search for and select **Gmail API** -6. Click **Enable** - - ![Search for an select the Gmail Enterprise API](https://res.cloudinary.com/pipedreamin/image/upload/v1663268442/docs/components/CleanShot_2022-09-15_at_15.00.22_skvwei.gif) - -7. Click **OAuth consent screen** on the left side - - ![Click "OAuth consent screen" in the left navigation menu](https://res.cloudinary.com/pipedreamin/image/upload/v1663268506/docs/components/CleanShot_2022-09-15_at_15.01.24_wravfb.png) - -8. Select **External** User Type and click “Create” - - ![Select "External" in the OAuth Consent Screen](https://res.cloudinary.com/pipedreamin/image/upload/v1663268545/docs/components/CleanShot_2022-09-15_at_15.02.22_fiekq1.png) - -9. Fill in the required fields and click **Save and Continue** -10. Click **Add or remove scopes** and select the `https://mail.google.com/` scope and then click "Update" -11. Click **Save and Continue** to finish the **Scopes** step -12. Add your own email as a **Test User** by clicking **Add Users*& then typing in your email in the prompt then clicking **Add** again. Then finally click **Save and Continue** to finish the Test Users portion. -13. You should be prompted with a **Summary** page. - -Now you've created an unlisted Gmail App that you can integrate with Pipedream. - -## Create OAuth Credentials - -You will need to generate a set of OAuth credentials to connect your new Gmail app to Pipedream properly. - -1. Navigate to the **Credentials** section on the left side. - - ![Open the Credentials menu in the left hand nav bar](https://res.cloudinary.com/pipedreamin/image/upload/v1663269973/docs/components/CleanShot_2022-09-15_at_15.13.52_yvllxi.png) - -2. Click **Create Credentials** at the top and select **“*OAuth client ID** - - ![Click create credentials to start the process](https://res.cloudinary.com/pipedreamin/image/upload/v1663270014/docs/components/CleanShot_2022-09-15_at_15.14.15_hjulis.png) - - ![Select the OAuth Client ID option](https://res.cloudinary.com/pipedreamin/image/upload/v1663270093/docs/components/CleanShot_2022-09-15_at_15.14.39_juqtnm.png) - -3. Select **Web application** for **Application type** - - ![Web application is the type of OAuth credential we're generating](https://res.cloudinary.com/pipedreamin/image/upload/v1663270117/docs/components/CleanShot_2022-09-15_at_15.14.56_hlseq6.png) - -4. Name the app “Pipedream” -5. Click **Add URI** and enter `https://api.pipedream.com/connect/oauth/oa_G7Ain6/callback` - - ![Add the Pipedream URL to the Callback Redirect URL option](https://res.cloudinary.com/pipedreamin/image/upload/v1663270187/docs/components/CleanShot_2022-09-15_at_15.16.10_hvbocb.png) - -6. Click **Create** to create your new OAuth keys -7. Note the client ID and client Secret, but keep these private and secure - - ![Store the Client ID and Client Secret keys](https://res.cloudinary.com/pipedreamin/image/upload/v1663270250/docs/components/CleanShot_2022-09-15_at_15.16.29_hvxnkx.png) - -## Connect your Gmail app Pipedream with your Gmail app OAuth crendentials - -At this point, you should have a Gmail App under your Google Project, and a set of OAuth keys. - -1. Now when prompted in Pipedream after trying to connect a Gmail Developer App, copy and paste your OAuth credentials. -2. Also select the scopes you chose when defining the app. We recommend using `https://mail.google.com/` -3. Then click **Connect** -4. If you did not publish your Gmail App in the Google Cloud Console, just click **Continue** to ignore the warning. - - ![Click continue if presented with a warning about an unpublished app](https://res.cloudinary.com/pipedreamin/image/upload/v1663269902/docs/components/CleanShot_2022-09-15_at_15.19.58_jnzlwc.png) - -5. Check all of the necessary scopes you'll need for your workflows - - ![Check all scopes to include grant your integration permission](https://res.cloudinary.com/pipedreamin/image/upload/v1663269729/docs/components/CleanShot_2022-09-15_at_15.20.26_jlnyw4.gif) - -7. Click the final **Connect** and your custom Gmail app should be integrated into Pipedream! - -## Publish your custom Gmail app -Google has a [7 day expiration window](https://developers.google.com/identity/protocols/oauth2#:~:text=A%20Google%20Cloud,Connect%20equivalents) on refresh tokens for applications with a publishing status of "Testing", so you will need to **Publish** your application in order to maintain your account connection. - -1. Navigate to your application, and click **OAuth Consent Screen** on the lefthand sidebar. -2. Under **Publishing status**, click **Publish App**. If you included any sensitive or restricted scopes in your app, there will be a disclosure stating that you will need to go through the process of verification. Click **Confirm**. -3. Your application will not be available externally unless you share your **client_id** with others, and you will not have to go through the verification process unless you intend to onboard over 100 users. -4. The publishing status should be set to **In production**, and your account should maintain its connection without an expiration window. - -![Publish your application](https://res.cloudinary.com/dpenc2lit/image/upload/v1698166716/Screenshot_2023-10-24_at_9.50.06_AM_lve7wq.png) - -![Confirmation of changes](https://res.cloudinary.com/dpenc2lit/image/upload/v1698166716/Screenshot_2023-10-24_at_9.50.18_AM_mndtyc.png) - -# Troubleshooting -**Application disconnects after 7 days**
-If your developer application disconnects after 7 days, you need to follow the steps above to Publish your custom Gmail app in order to keep your account connected. \ No newline at end of file diff --git a/components/gmail_custom_oauth/actions/add-label-to-email/add-label-to-email.mjs b/components/gmail_custom_oauth/actions/add-label-to-email/add-label-to-email.mjs deleted file mode 100644 index eb46314ba756f..0000000000000 --- a/components/gmail_custom_oauth/actions/add-label-to-email/add-label-to-email.mjs +++ /dev/null @@ -1,33 +0,0 @@ -import gmail from "../../gmail_custom_oauth.app.mjs"; - -export default { - key: "gmail_custom_oauth-add-label-to-email", - name: "Add Label to Email", - description: "Add a label to an email message. [See the docs](https://developers.google.com/gmail/api/reference/rest/v1/users.messages/modify)", - version: "0.0.8", - type: "action", - props: { - gmail, - message: { - propDefinition: [ - gmail, - "message", - ], - }, - label: { - propDefinition: [ - gmail, - "label", - ], - withLabel: true, - }, - }, - async run({ $ }) { - const response = await this.gmail.addLabelToEmail({ - message: this.message, - label: this.label.value, - }); - $.export("$summary", `Successfully added ${this.label.label} label to email`); - return response; - }, -}; diff --git a/components/gmail_custom_oauth/actions/create-draft/create-draft.mjs b/components/gmail_custom_oauth/actions/create-draft/create-draft.mjs deleted file mode 100644 index 5a4fa31519182..0000000000000 --- a/components/gmail_custom_oauth/actions/create-draft/create-draft.mjs +++ /dev/null @@ -1,88 +0,0 @@ -/* eslint-disable pipedream/props-description */ -import gmail from "../../gmail_custom_oauth.app.mjs"; - -export default { - key: "gmail_custom_oauth-create-draft", - name: "Create Draft", - description: "Create a draft from your Google Workspace email account", - version: "0.0.4", - type: "action", - props: { - gmail, - to: { - propDefinition: [ - gmail, - "to", - ], - }, - cc: { - propDefinition: [ - gmail, - "cc", - ], - }, - bcc: { - propDefinition: [ - gmail, - "bcc", - ], - }, - fromName: { - propDefinition: [ - gmail, - "fromName", - ], - }, - replyTo: { - propDefinition: [ - gmail, - "replyTo", - ], - }, - subject: { - propDefinition: [ - gmail, - "subject", - ], - }, - body: { - propDefinition: [ - gmail, - "body", - ], - }, - bodyType: { - propDefinition: [ - gmail, - "bodyType", - ], - }, - attachments: { - propDefinition: [ - gmail, - "attachments", - ], - }, - inReplyTo: { - propDefinition: [ - gmail, - "message", - ], - label: "In Reply To", - description: "Specify the `message-id` this email is replying to.", - optional: true, - }, - mimeType: { - propDefinition: [ - gmail, - "mimeType", - ], - }, - }, - async run({ $ }) { - const opts = await this.gmail.getOptionsToSendEmail($, this); - const response = await this.gmail.createDraft(opts); - $.export("$summary", "Successfully created a draft message"); - return response; - }, -}; diff --git a/components/gmail_custom_oauth/actions/download-attachment/download-attachment.mjs b/components/gmail_custom_oauth/actions/download-attachment/download-attachment.mjs deleted file mode 100644 index 4e23cd22b0a08..0000000000000 --- a/components/gmail_custom_oauth/actions/download-attachment/download-attachment.mjs +++ /dev/null @@ -1,51 +0,0 @@ -import gmail from "../../gmail_custom_oauth.app.mjs"; -import fs from "fs"; -import path from "path"; - -export default { - key: "gmail_custom_oauth-download-attachment", - name: "Download Attachement", - description: "Download an attachment by attachmentId to the /tmp directory. [See the docs](https://developers.google.com/gmail/api/reference/rest/v1/users.messages.attachments/get)", - version: "0.0.2", - type: "action", - props: { - gmail, - messageId: { - propDefinition: [ - gmail, - "message", - ], - }, - attachmentId: { - propDefinition: [ - gmail, - "attachmentId", - ({ messageId }) => ({ - messageId, - }), - ], - }, - filename: { - type: "string", - label: "Filename", - description: "Name of the new file. Example: `test.jpg`", - }, - }, - async run({ $ }) { - const attachment = await this.gmail.getAttachment({ - messageId: this.messageId, - attachmentId: this.attachmentId, - }); - - const filePath = path.join("/tmp", this.filename); - const buffer = Buffer.from(attachment.data, "base64"); - fs.writeFileSync(filePath, buffer); - - $.export("$summary", `Successfully created file ${this.filename} in \`/tmp\` directory`); - - return { - filePath, - ...attachment, - }; - }, -}; diff --git a/components/gmail_custom_oauth/actions/find-email/find-email.mjs b/components/gmail_custom_oauth/actions/find-email/find-email.mjs deleted file mode 100644 index 3b5548fa36ee0..0000000000000 --- a/components/gmail_custom_oauth/actions/find-email/find-email.mjs +++ /dev/null @@ -1,61 +0,0 @@ -import gmail from "../../gmail_custom_oauth.app.mjs"; - -export default { - key: "gmail_custom_oauth-find-email", - name: "Find Email", - description: "Find an email using Google's Search Engine. [See the docs](https://developers.google.com/gmail/api/reference/rest/v1/users.messages/list)", - version: "0.0.9", - type: "action", - props: { - gmail, - q: { - propDefinition: [ - gmail, - "q", - ], - }, - labels: { - propDefinition: [ - gmail, - "label", - ], - type: "string[]", - label: "Labels", - description: "Only return messages with labels that match all of the specified labels.", - optional: true, - }, - includeSpamTrash: { - type: "boolean", - label: "Include Spam and Trash?", - description: "Include messages from `SPAM` and `TRASH` in the results. Defaults to `false`.", - optional: true, - default: false, - }, - }, - async run({ $ }) { - const messageIds = []; - let pageToken; - - do { - const { - messages = [], - nextPageToken, - } = await this.gmail.listMessages({ - q: this.q, - labelIds: this.labels, - includeSpamTrash: this.includeSpamTrash, - pageToken, - }); - messageIds.push(...messages.map(({ id }) => id)); - pageToken = nextPageToken; - } while (pageToken); - - const messages = await this.gmail.getMessages(messageIds); - - const suffix = messages.length === 1 - ? "" - : "s"; - $.export("$summary", `Successfully found ${messages.length} message${suffix}`); - return messages; - }, -}; diff --git a/components/gmail_custom_oauth/actions/send-email/send-email.mjs b/components/gmail_custom_oauth/actions/send-email/send-email.mjs deleted file mode 100644 index a0c7c22fc7fd5..0000000000000 --- a/components/gmail_custom_oauth/actions/send-email/send-email.mjs +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable pipedream/required-properties-name */ -/* eslint-disable pipedream/required-properties-description */ -/* eslint-disable pipedream/required-properties-version */ -/* eslint-disable pipedream/required-properties-type */ -import base from "../../../gmail/actions/send-email/send-email.mjs"; -import overrideApp from "../../common/override-app.mjs"; - -overrideApp(base); - -export default { - ...base, - key: "gmail_custom_oauth-send-email", - version: "0.1.1", - props: { - ...base.props, - inReplyTo: { - propDefinition: [ - base.props.gmail, - "message", - ], - label: "In Reply To", - description: "Specify the `message-id` this email is replying to.", - optional: true, - }, - }, -}; diff --git a/components/gmail_custom_oauth/actions/update-primary-signature/update-primary-signature.mjs b/components/gmail_custom_oauth/actions/update-primary-signature/update-primary-signature.mjs deleted file mode 100644 index c299fa7964249..0000000000000 --- a/components/gmail_custom_oauth/actions/update-primary-signature/update-primary-signature.mjs +++ /dev/null @@ -1,32 +0,0 @@ -import gmail from "../../gmail_custom_oauth.app.mjs"; - -export default { - key: "gmail_custom_oauth-update-primary-signature", - name: "Update Signature for Primary Email Address", - description: "Update the signature for the primary email address. [See docs here](https://developers.google.com/gmail/api/reference/rest/v1/users.settings.sendAs/update)", - version: "0.0.5", - type: "action", - props: { - gmail, - signature: { - type: "string", - label: "Signature", - description: "The new signature.", - }, - }, - methods: { - async createOpts() { - const { email } = await this.gmail.userInfo(); - return { - signature: this.signature, - email, - }; - }, - }, - async run({ $ }) { - const opts = await this.createOpts(); - const response = await this.gmail.updateSignature(opts); - $.export("$summary", `Successfully updated signature for ${opts.email}`); - return response; - }, -}; diff --git a/components/gmail_custom_oauth/common/override-app.mjs b/components/gmail_custom_oauth/common/override-app.mjs deleted file mode 100644 index eaf83ad061027..0000000000000 --- a/components/gmail_custom_oauth/common/override-app.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import app from "../gmail_custom_oauth.app.mjs"; -const appName = "gmail"; - -/** - * This function substitutes the app so that $auth and propDefinitions correspond to the correct app - * This is done to avoid duplicating code because all component props and methods are shared between - * gorgias_oauth and gorgias (api_key) apps - * @param {*} base app to inject - in this case, gorgias_oauth - */ -function overrideApp(base) { - base.props[appName] = app; - - Object.keys(base.props) - .filter((prop) => prop != appName) - .forEach((prop) => { - const { propDefinition } = base.props[prop]; - if (propDefinition?.length > 0) { - propDefinition[0] = app; - } - }); -} - -export default overrideApp; diff --git a/components/gmail_custom_oauth/gmail_custom_oauth.app.mjs b/components/gmail_custom_oauth/gmail_custom_oauth.app.mjs deleted file mode 100644 index 5de848941f413..0000000000000 --- a/components/gmail_custom_oauth/gmail_custom_oauth.app.mjs +++ /dev/null @@ -1,95 +0,0 @@ -import gmail from "@googleapis/gmail"; -import { axios } from "@pipedream/platform"; -import { google } from "googleapis"; -import base from "../gmail/gmail.app.mjs"; - -export default { - ...base, - type: "app", - app: "gmail_custom_oauth", - propDefinitions: { - ...base.propDefinitions, - attachmentId: { - type: "string", - label: "Attachment", - description: "Identifier of the attachment to download", - async options({ messageId }) { - const { payload: { parts } } = await this.getMessage({ - id: messageId, - }); - return parts?.filter(({ body }) => body.attachmentId ) - ?.map(({ body }) => body.attachmentId ) || []; - }, - }, - }, - methods: { - ...base.methods, - _apiUrl() { - return "https://www.googleapis.com/gmail/v1/users/me"; - }, - _getHeaders() { - return { - "Authorization": `Bearer ${this.$auth.oauth_access_token}`, - }; - }, - async _makeRequest({ - $ = this, path, ...opts - }) { - const config = { - url: `${this._apiUrl()}/${path}`, - headers: this._getHeaders(), - ...opts, - }; - - return axios($, config); - }, - _accessToken() { - return this.$auth.oauth_access_token; - }, - _client() { - const auth = new gmail.auth.OAuth2(); - auth.setCredentials({ - access_token: this._accessToken(), - }); - return gmail.gmail({ - version: "v1", - auth, - }); - }, - _serviceAccountAuth(credentials, impersonatedUser) { - const scopes = [ - "https://www.googleapis.com/auth/gmail.settings.basic", - ]; - return new google.auth.JWT( - credentials.client_email, - null, - credentials.private_key, - scopes, - impersonatedUser, - ); - }, - async updateSignature({ - signature, email, credentials, - }) { - const opts = { - userId: "me", - sendAsEmail: email, - requestBody: { - signature, - }, - }; - if (credentials) opts.auth = this._serviceAccountAuth(credentials, email); - return this._client().users.settings.sendAs.patch(opts); - }, - async getAttachment({ - messageId, attachmentId, - }) { - const { data } = await this._client().users.messages.attachments.get({ - userId: "me", - messageId, - id: attachmentId, - }); - return data; - }, - }, -}; diff --git a/components/gmail_custom_oauth/package.json b/components/gmail_custom_oauth/package.json deleted file mode 100644 index e0268e9f7de54..0000000000000 --- a/components/gmail_custom_oauth/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "@pipedream/gmail_custom_oauth", - "version": "0.1.6", - "description": "Pipedream Gmail (Consumer) Components", - "main": "gmail_custom_oauth.app.mjs", - "keywords": [ - "pipedream", - "gmail_custom_oauth" - ], - "homepage": "https://pipedream.com/apps/gmail_custom_oauth", - "author": "Pipedream (https://pipedream.com/)", - "publishConfig": { - "access": "public" - }, - "dependencies": { - "@googleapis/gmail": "^0.3.4", - "@pipedream/platform": "^1.1.1", - "googleapis": "^109.0.1", - "html-to-text": "^8.2.1", - "nodemailer": "^6.7.8" - } -} diff --git a/components/gmail_custom_oauth/sources/common/polling.mjs b/components/gmail_custom_oauth/sources/common/polling.mjs deleted file mode 100644 index 357d530b5ffd1..0000000000000 --- a/components/gmail_custom_oauth/sources/common/polling.mjs +++ /dev/null @@ -1,96 +0,0 @@ -import gmail from "../../gmail_custom_oauth.app.mjs"; -import constants from "../../../gmail/common/constants.mjs"; -import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; - -export default { - props: { - gmail, - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - q: { - propDefinition: [ - gmail, - "q", - ], - }, - labels: { - propDefinition: [ - gmail, - "label", - ], - type: "string[]", - label: "Labels", - optional: true, - }, - }, - hooks: { - async deploy() { - console.log(`Fetching last ${constants.HISTORICAL_EVENTS} historical events...`); - const response = await this.gmail.listMessages({ - q: this.q, - labelIds: this.labels, - maxResults: constants.HISTORICAL_EVENTS, - }); - - const messageIds = response?.messages?.map((message) => message.id); - - if (messageIds?.length) { - this.setLastMessageId(messageIds[0]); - await this.processMessageIds(messageIds.reverse()); - } - }, - }, - methods: { - getLastMessageId() { - return this.db.get("lastMessageId"); - }, - setLastMessageId(lastMessageId) { - if (lastMessageId) { - this.db.set("lastMessageId", lastMessageId); - } - }, - processMessageIds() { - throw new Error("processMessageIds not implemented"); - }, - }, - async run() { - const lastMessageId = this.getLastMessageId(); - - const response = await this.gmail.listMessages({ - q: this.q, - labelIds: this.labels, - maxResults: 100, - }); - - let messageIds = response?.messages?.map((message) => message.id); - - if (!messageIds?.length) { - console.log("No new message. Exiting..."); - return; - } - - this.setLastMessageId(messageIds[0]); - - const index = messageIds.indexOf(lastMessageId); - if (index !== -1) { - messageIds = messageIds.slice(0, index); - } - - const numMessages = messageIds.length; - if (!numMessages) { - console.log("No new message. Exiting..."); - return; - } - - const suffix = numMessages === 1 - ? "" - : "s"; - console.log(`Received ${numMessages} new message${suffix}. Please be patient while more information for each message is being fetched.`); - await this.processMessageIds(messageIds.reverse()); - }, -}; diff --git a/components/gmail_custom_oauth/sources/new-attachment-received/new-attachment-received.mjs b/components/gmail_custom_oauth/sources/new-attachment-received/new-attachment-received.mjs deleted file mode 100644 index 45eee54ad436c..0000000000000 --- a/components/gmail_custom_oauth/sources/new-attachment-received/new-attachment-received.mjs +++ /dev/null @@ -1,50 +0,0 @@ -import common from "../common/polling.mjs"; - -export default { - ...common, - key: "gmail_custom_oauth-new-attachment-received", - name: "New Attachment Received", - description: "Emit new event for each attachment in a message received. This source is capped at 100 max new messages per run.", - version: "0.0.7", - type: "source", - dedupe: "unique", - methods: { - ...common.methods, - generateMeta(attachment, message) { - return { - id: attachment.body.attachmentId, - summary: `New attachment: ${attachment.filename}`, - ts: message.internalDate, - }; - }, - emitEvents(messages) { - for (const message of messages) { - const attachments = message.payload.parts.filter((part) => part.body.attachmentId); - const numAttachments = attachments.length; - if (!numAttachments) continue; - const suffix = numAttachments === 1 - ? "" - : "s"; - console.log(`Emitting event${suffix} for ${numAttachments} attachment${suffix} found for message`); - for (const attachment of attachments) { - const meta = this.generateMeta(attachment, message); - this.$emit({ - message, - attachment, - }, meta); - } - } - }, - filterMessagesWithAttachments(messages) { - return messages.filter( - (message) => message.payload.parts?.filter((part) => part.body?.attachmentId).length, - ); - }, - async processMessageIds(messageIds) { - const messages = this.filterMessagesWithAttachments( - await this.gmail.getMessages(messageIds), - ); - this.emitEvents(messages); - }, - }, -}; diff --git a/components/gmail_custom_oauth/sources/new-email-received/new-email-received.mjs b/components/gmail_custom_oauth/sources/new-email-received/new-email-received.mjs deleted file mode 100644 index f3cc3d3c132df..0000000000000 --- a/components/gmail_custom_oauth/sources/new-email-received/new-email-received.mjs +++ /dev/null @@ -1,66 +0,0 @@ -import common from "../common/polling.mjs"; - -export default { - ...common, - key: "gmail_custom_oauth-new-email-received", - name: "New Email Received", - description: "Emit new event when an email is received. This source is capped at 100 max new messages per run.", - version: "0.0.8", - type: "source", - dedupe: "unique", - methods: { - ...common.methods, - generateMeta(message) { - const selectedHeader = message.payload.headers.find(({ name }) => name === "Subject"); - const subject = selectedHeader?.value || "No subject"; - return { - id: message.id, - summary: `New email: ${subject}`, - ts: message.internalDate, - }; - }, - isValidType(data) { - return typeof(data) === "string" - || data instanceof Buffer - || ArrayBuffer.isView(data); - }, - decodeContent(message) { - const MULTIPART_MIME_TYPE = "multipart"; - const { - payload: { - mimeType, body, parts, - }, - } = message; - - if (!mimeType.startsWith(MULTIPART_MIME_TYPE)) { - return !this.isValidType(body?.data) - ? message - : { - ...message, - decodedContent: Buffer.from(body.data, "base64").toString(), - }; - } - - const [ - firstPart, - ] = parts; - - return !this.isValidType(firstPart?.body?.data) - ? message - : { - ...message, - decodedContent: Buffer.from(firstPart.body.data, "base64").toString(), - }; - }, - emitEvents(messages) { - for (const message of messages) { - const meta = this.generateMeta(message); - this.$emit(this.decodeContent(message), meta); - } - }, - async processMessageIds(messageIds) { - const messages = await this.gmail.getMessages(messageIds); - this.emitEvents(messages); - }, - }, -}; diff --git a/components/gmail_custom_oauth/sources/new-labeled-email/new-labeled-email.mjs b/components/gmail_custom_oauth/sources/new-labeled-email/new-labeled-email.mjs deleted file mode 100644 index eaff6ff69934b..0000000000000 --- a/components/gmail_custom_oauth/sources/new-labeled-email/new-labeled-email.mjs +++ /dev/null @@ -1,94 +0,0 @@ -import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; -import sampleEmit from "./test-event.mjs"; -import gmail from "../../gmail_custom_oauth.app.mjs"; - -export default { - key: "gmail_custom_oauth-new-labeled-email", - name: "New Labeled Email", - description: "Emit new event when a new email is labeled.", - type: "source", - version: "0.0.4", - dedupe: "unique", - props: { - gmail, - db: "$.service.db", - timer: { - label: "Polling interval", - description: "Pipedream will poll the Gmail API on this schedule", - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - label: { - propDefinition: [ - gmail, - "label", - ], - }, - }, - methods: { - _getLastHistoryId() { - return this.db.get("lastHistoryId"); - }, - _setLastHistoryId(lastHistoryId) { - this.db.set("lastHistoryId", lastHistoryId); - }, - generateMeta({ - id, messages, - }) { - return { - id: id, - summary: `A new message with ID: ${messages[0].id} was labeled with "${this.label}"`, - ts: Date.now(), - }; - }, - async getHistoryId() { - const { messages } = await this.gmail.listMessages({ - params: { - labelIds: this.label, - }, - }); - if (messages.length > 25) messages.length = 25; - const { id } = messages[messages.length - 1]; - const { historyId } = await this.gmail.getMessage({ - id, - }); - return historyId; - }, - async emitHistories(startHistoryId) { - const { - history, historyId, - } = await this.gmail.listHistory({ - startHistoryId, - historyTypes: "labelAdded", - labelId: this.label, - }); - - if (!history) { - return; - } - this._setLastHistoryId(historyId); - const responseArray = history.filter((item) => item.labelsAdded); - responseArray.forEach((item) => { - const meta = this.generateMeta(item); - this.$emit(item, meta); - }); - }, - }, - hooks: { - async deploy() { - const historyId = await this.getHistoryId(); - await this.emitHistories(historyId); - }, - }, - async run() { - let lastHistoryId = this._getLastHistoryId(); - - if (!lastHistoryId) { - lastHistoryId = await this.getHistoryId(); - } - await this.emitHistories(lastHistoryId); - }, - sampleEmit, -}; diff --git a/components/gmail_custom_oauth/sources/new-labeled-email/test-event.mjs b/components/gmail_custom_oauth/sources/new-labeled-email/test-event.mjs deleted file mode 100644 index eb6740f9c4376..0000000000000 --- a/components/gmail_custom_oauth/sources/new-labeled-email/test-event.mjs +++ /dev/null @@ -1,26 +0,0 @@ -export default { - "id": "332932", - "messages": [ - { - "id": "1872ede547346f26", - "threadId": "1872ede547346f26" - } - ], - "labelsAdded": [ - { - "message": { - "id": "1872ede547346f26", - "threadId": "1872ede547346f26", - "labelIds": [ - "UNREAD", - "Label_1", - "CATEGORY_PERSONAL", - "INBOX" - ] - }, - "labelIds": [ - "Label_1" - ] - } - ] - } \ No newline at end of file diff --git a/components/gmail_custom_oauth/sources/new-sent-email/new-sent-email.mjs b/components/gmail_custom_oauth/sources/new-sent-email/new-sent-email.mjs deleted file mode 100644 index 7ff21e44a7825..0000000000000 --- a/components/gmail_custom_oauth/sources/new-sent-email/new-sent-email.mjs +++ /dev/null @@ -1,78 +0,0 @@ -import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; -import app from "../../gmail_custom_oauth.app.mjs"; - -export default { - key: "gmail_custom_oauth-new-sent-email", - name: "New Sent Email", - description: "Emit new event for each new email sent. (Maximum of 300 events emited per execution)", - version: "0.0.4", - type: "source", - props: { - app, - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - }, - methods: { - setLastMessageId(id) { - this.db.set("lastMessageId", id); - }, - getLastMessageId() { - return this.db.get("lastMessageId"); - }, - emit(event) { - this.$emit(event, { - id: event.id, - summary: event.snippet, - ts: new Date(event.internalDate), - }); - }, - }, - async run() { - let pageToken = null; - let lastMessageId = this.getLastMessageId(); - let firstExecutionId = null; - const promises = []; - const MAX_MESSAGES = 300; - loop1: - while (true) { - const res = await this.app.listMessages({ - labelIds: [ - "SENT", - ], - pageToken, - }); - - if (!firstExecutionId) { - firstExecutionId = res.messages[0].id; - } - pageToken = res.nextPageToken; - - for (const message of res.messages) { - if (message.id === lastMessageId) { - break loop1; - } - promises.push(this.app.getMessage({ - id: message.id, - })); - } - - if (!pageToken || promises.length >= MAX_MESSAGES) { - break; - } - } - - if (firstExecutionId) { - this.setLastMessageId(firstExecutionId); - } - - const details = await Promise.all(promises); - for (const detail of details.reverse()) { - this.emit(detail); - } - }, -}; diff --git a/components/gmodstore/README.md b/components/gmodstore/README.md new file mode 100644 index 0000000000000..609d5395bb3b1 --- /dev/null +++ b/components/gmodstore/README.md @@ -0,0 +1,11 @@ +# Overview + +The GmodStore API gives you the tools to interact with the GmodStore marketplace programmatically. Through this API, you can automate tasks like retrieving information on products, managing licenses, and handling user data. Using Pipedream, you can create workflows that integrate the GmodStore API with various other services to streamline processes, analyze data, and improve the efficiency of your operations. Pipedream’s serverless platform allows for seamless execution of these integrations, making it simpler to set up automated tasks without managing infrastructure. + +# Example Use Cases + +- **Automated License Validation**: Validate GmodStore licenses for your products in real-time by setting up a workflow in Pipedream that triggers on user registration or purchase events from your site. When triggered, it calls the GmodStore API to ensure that the license is active and valid before granting access to your content. + +- **Sales Monitoring and Notifications**: Monitor your GmodStore product sales by creating a Pipedream workflow that periodically calls the GmodStore API to fetch recent sales data. Connect this to a communication app like Slack or Discord to receive instant notifications about new sales, allowing you to keep tabs on your revenue stream effortlessly. + +- **Customer Support Ticket Integration**: Enhance your customer support by integrating GmodStore with a support ticket system like Zendesk. Use Pipedream to watch for new support requests on GmodStore, then automatically create and prioritize tickets in Zendesk, centralizing your customer support efforts and speeding up response times. diff --git a/components/gmodstore/package.json b/components/gmodstore/package.json index acaf208f16214..79b77aae7dc3f 100644 --- a/components/gmodstore/package.json +++ b/components/gmodstore/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/go/README.md b/components/go/README.md new file mode 100644 index 0000000000000..4e09db5e8e070 --- /dev/null +++ b/components/go/README.md @@ -0,0 +1,11 @@ +# Overview + +You can execute custom Go scripts on-demand or in response to various triggers and integrate with thousands of apps supported by Pipedream. Writing with Go on Pipedream enables backend operations like data processing, automation, or invoking other APIs, all within the Pipedream ecosystem. By leveraging Go's performance and efficiency, you can design powerful and fast workflows to streamline complex tasks. + +# Example Use Cases + +- **Process Webhook Requests with Go**: Parse and handle incoming webhook data using a Go script. After processing, trigger actions in other services like Slack, sending notifications about the processed data. + +- **Scheduled Data Aggregation**: Set up a cron job to routinely fetch data from external APIs (e.g., GitHub, Twitter) using Go. Process and aggregate this data, then store it in a Pipedream data store or send it to a Google Sheet for analysis and reporting. + +- **IoT Device Data Processing**: Receive and process data from IoT devices on a real-time basis. Use Go to parse the device data, perform necessary computations, and then forward the results to services like AWS S3 or MQTT brokers for further use or visualization. diff --git a/components/go/package.json b/components/go/package.json new file mode 100644 index 0000000000000..8a872369ef99d --- /dev/null +++ b/components/go/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/go", + "version": "0.6.0", + "description": "Pipedream go Components", + "main": "go.app.mjs", + "keywords": [ + "pipedream", + "go" + ], + "homepage": "https://pipedream.com/apps/go", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/go_upc/README.md b/components/go_upc/README.md new file mode 100644 index 0000000000000..df15322eaf82a --- /dev/null +++ b/components/go_upc/README.md @@ -0,0 +1,11 @@ +# Overview + +The Go-UPC API allows users to look up products using their UPC (Universal Product Code) to access detailed information like the product name, description, image, and manufacturer. Leveraging Go-UPC within Pipedream empowers you to automate inventory tracking, streamline product data collection, or enhance e-commerce experiences. By integrating this API, you can enrich datasets, trigger actions based on product lookups, and connect this data to countless other services supported by Pipedream. + +# Example Use Cases + +- **Inventory Alert System**: Set up a workflow that monitors product stock levels by UPC codes. When a product falls below a certain threshold, the Go-UPC API can be used to fetch product details, and Pipedream can automatically send an alert through email, Slack, or SMS, prompting a restock. + +- **E-commerce Product Catalog Integration**: Create a workflow that uses the Go-UPC API to populate an e-commerce platform's product catalog. Whenever a new UPC is added to your inventory system, Pipedream can trigger a workflow that fetches product details from Go-UPC and updates the product listings on platforms like Shopify or WooCommerce. + +- **Price Comparison Engine**: Develop a Pipedream workflow that utilizes the Go-UPC API to build a price comparison engine. By integrating with other APIs or databases, the workflow can compare the UPC fetched product details with prices from different vendors, and if a better price is found, update an in-app recommendation or notify a user with the best available price option. diff --git a/components/gobio_link/README.md b/components/gobio_link/README.md index adea60ca4ff76..eba608f89f5ff 100644 --- a/components/gobio_link/README.md +++ b/components/gobio_link/README.md @@ -1,9 +1,11 @@ # Overview -[Gobio.link](https://gobio.link/) lets you build amazing things: +The gobio.link API allows you to create and manage smart links that can redirect users based on various rules like location, device, language, and more. With Pipedream, you can automate processes that involve link creation, performance tracking, and dynamic redirection rules. By integrating the gobio.link API with Pipedream, you can tie its capabilities into workflows that interact with other apps to streamline marketing campaigns, personalize user experiences, and analyze engagement data. -- A to-do list -- A messaging app -- A social network -- A news feed -- A blog +# Example Use Cases + +- **Dynamic Marketing Campaigns**: Automate the creation of gobio.link smart links within a Pipedream workflow that triggers whenever a new product is added to your eCommerce platform. The links can direct users to different landing pages based on their device type, optimizing the user experience for mobile or desktop shoppers. + +- **Personalized Content Distribution**: Set up a Pipedream workflow that generates personalized gobio.link URLs for a segmented mailing list in your email marketing app. Based on subscriber data, craft links that redirect to content tailored to their interests, increasing the relevance and effectiveness of your campaigns. + +- **Engagement Analytics Automation**: Combine gobio.link with analytics tools through Pipedream workflows to track link performance. When a smart link is clicked, capture the event in Pipedream and log the data in your analytics platform, enabling real-time insights and data-driven decision-making. diff --git a/components/gocanvas/README.md b/components/gocanvas/README.md index 13c65943dd65d..892ba969c3ed5 100644 --- a/components/gocanvas/README.md +++ b/components/gocanvas/README.md @@ -1,11 +1,11 @@ # Overview -The GoCanvas API enables you to build applications that can create, edit, and -view Canvas documents. +The GoCanvas API allows for the automation of document processing tasks, enabling data extraction from PDFs with ease, thus facilitating the seamless transfer of this data to other systems or databases. This proves particularly useful for businesses aiming to digitize paper-based processes, automate data entry, or integrate with other digital tools to enhance productivity and reduce manual errors. -With the GoCanvas API, you can: +# Example Use Cases -- Create new Canvas documents -- Edit existing Canvas documents -- View Canvas documents -- Delete Canvas documents +- **Automated Invoice Processing**: Trigger a Pipedream workflow whenever a new invoice is received in GoCanvas. Extract key information such as invoice number, amount, and line item details, then sync this data to your accounting software like QuickBooks or an Excel spreadsheet in real-time for streamlined financial management. + +- **Customer Onboarding Documents**: When a new customer fills out an onboarding form in GoCanvas, use Pipedream to process the form, extracting relevant data, and then add the customer details directly to your CRM tool such as Salesforce. Additionally, trigger a welcome email via SendGrid to engage the customer instantly. + +- **Job Application Sorting**: Optimize your HR processes by setting up a Pipedream workflow that activates when a new job application is submitted through GoCanvas. Extract applicant details and resume content, then automatically score and sort candidates based on predefined criteria, or add them to an Airtable base for a simplified review process. diff --git a/components/gocanvas/actions/create-dispatch/create-dispatch.mjs b/components/gocanvas/actions/create-dispatch/create-dispatch.mjs new file mode 100644 index 0000000000000..cc21e5d520f90 --- /dev/null +++ b/components/gocanvas/actions/create-dispatch/create-dispatch.mjs @@ -0,0 +1,49 @@ +import gocanvas from "../../gocanvas.app.mjs"; + +export default { + key: "gocanvas-create-dispatch", + name: "Create Dispatch", + description: "Creates a dispatch item in GoCanvas. [See the documentation](https://help.gocanvas.com/hc/en-us/article_attachments/26468076609559)", + version: "0.0.1", + type: "action", + props: { + gocanvas, + form: { + propDefinition: [ + gocanvas, + "form", + ], + description: "The name of the form you want to create a prepopulated submission for", + }, + entries: { + type: "object", + label: "Entries", + description: `DIEntry elements consisting of label/value pairs. + \n Label: Either the Export Label or plain Label field attribute (caseinsensitive) as defined in the form builder. + \n Value: The value assigned to this Dispatch Item entry. + `, + }, + }, + async run({ $ }) { + let entriesString = ""; + for (const [ + key, + value, + ] of Object.entries(this.entries)) { + entriesString += ``; + } + const response = await this.gocanvas.dispatchItems({ + $, + data: ` + + + + ${entriesString} + + + `, + }); + $.export("$summary", `Successfully created dispatch item in ${this.form}`); + return response; + }, +}; diff --git a/components/gocanvas/actions/create-or-update-reference-data/create-or-update-reference-data.mjs b/components/gocanvas/actions/create-or-update-reference-data/create-or-update-reference-data.mjs new file mode 100644 index 0000000000000..3280cc055cde4 --- /dev/null +++ b/components/gocanvas/actions/create-or-update-reference-data/create-or-update-reference-data.mjs @@ -0,0 +1,71 @@ +import gocanvas from "../../gocanvas.app.mjs"; +import { parse } from "csv-parse/sync"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "gocanvas-create-or-update-reference-data", + name: "Create or Update Reference Data", + description: "Creates or updates GoCanvas reference data. [See the documentation](https://help.gocanvas.com/hc/en-us/article_attachments/26468076609559)", + version: "0.0.1", + type: "action", + props: { + gocanvas, + name: { + type: "string", + label: "Reference Data Name", + description: "The attribute name of the dataset to operate on. Will be created if it doesn't already exist.", + }, + data: { + type: "string", + label: "Data", + description: `A string of comma separated values representing the data to create/update. **Include Column names**: + \n Example: + \n Column1,Column2,Column3 + \n Data1Column1,Data1Column2,Data1Column3 + \n Data2Column1,Data2Column2,Data3Column3 + `, + }, + }, + methods: { + csvToXml(data) { + const records = parse(data, { + columns: true, + trim: true, + }); + + if (!records?.length) { + throw new ConfigurationError("No data items found to create/update. Please enter column names and at least 1 row of data."); + } + + // Extract columns + const columns = Object.keys(records[0]); + let result = ""; + result += columns.map((col) => `${col}`).join(""); + result += "\n\n"; + + // Extract rows + result += records + .map((row) => { + const rowValues = columns.map((col) => `${row[col]}`).join(""); + return ` ${rowValues}`; + }) + .join("\n"); + + result += "\n"; + return result; + }, + }, + async run({ $ }) { + const response = await this.gocanvas.createUpdateReferenceData({ + $, + data: ` + + + ${await this.csvToXml(this.data)} + + `, + }); + $.export("$summary", "Successfully created/updated reference data"); + return response; + }, +}; diff --git a/components/gocanvas/actions/delete-dispatch/delete-dispatch.mjs b/components/gocanvas/actions/delete-dispatch/delete-dispatch.mjs new file mode 100644 index 0000000000000..ddafebef637d6 --- /dev/null +++ b/components/gocanvas/actions/delete-dispatch/delete-dispatch.mjs @@ -0,0 +1,46 @@ +import gocanvas from "../../gocanvas.app.mjs"; + +export default { + key: "gocanvas-delete-dispatch", + name: "Delete Dispatch", + description: "Removes a specific dispatch from GoCanvas. [See the documentation](https://help.gocanvas.com/hc/en-us/article_attachments/26468076609559)", + version: "0.0.1", + type: "action", + props: { + gocanvas, + form: { + propDefinition: [ + gocanvas, + "form", + ], + }, + dispatchId: { + propDefinition: [ + gocanvas, + "dispatchId", + (c) => ({ + form: c.form, + }), + ], + }, + }, + async run({ $ }) { + const description = await this.gocanvas.getDispatchDescription({ + $, + dispatchId: this.dispatchId, + }); + const response = await this.gocanvas.dispatchItems({ + $, + data: ` + + + + + + + `, + }); + $.export("$summary", `Successfully deleted dispatch with ID ${this.dispatchId}`); + return response; + }, +}; diff --git a/components/gocanvas/gocanvas.app.mjs b/components/gocanvas/gocanvas.app.mjs index 45e1ba69a4cb3..40ec3505be34a 100644 --- a/components/gocanvas/gocanvas.app.mjs +++ b/components/gocanvas/gocanvas.app.mjs @@ -1,11 +1,151 @@ +import { axios } from "@pipedream/platform"; +import xml2js from "xml2js"; + export default { type: "app", app: "gocanvas", - propDefinitions: {}, + propDefinitions: { + form: { + type: "string", + label: "Form", + description: "The identifier of a form", + async options() { + const forms = await this.listForms(); + return forms?.map((form) => form.Name[0]) || []; + }, + }, + dispatchId: { + type: "string", + label: "Dispatch ID", + description: "Identifier of a dispatch", + async options({ + page, form, + }) { + const dispatches = await this.getActiveDispatches({ + form, + data: { + page: page + 1, + }, + }); + return dispatches?.map(({ + $, Description: desc, + }) => ({ + value: $.Id, + label: desc[0], + })) || []; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://www.gocanvas.com/apiv2/"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + params: { + ...params, + username: `${this.$auth.username}`, + }, + headers: { + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + async getActiveDispatches({ + form, ...opts + }) { + const response = await this._makeRequest({ + path: "/dispatch_export", + ...opts, + }); + const { CanvasResult: { Dispatches: dispatches } } = await new xml2js + .Parser().parseStringPromise(response); + if (!dispatches?.length) { + return []; + } + return dispatches + .flatMap((d) => d.Dispatch || []) + .filter((d) => d.Status[0] !== "deleted") + .filter((d) => !form || d.Form[0] === form); + }, + async listForms(opts = {}) { + const response = await this._makeRequest({ + path: "/forms", + ...opts, + }); + const { CanvasResult: { Forms: forms } } = await new xml2js + .Parser().parseStringPromise(response); + if (!forms?.length) { + return []; + } + return forms.flatMap((form) => form.Form || []); + }, + async listSubmissions(opts = {}) { + const response = await this._makeRequest({ + path: "/submissions", + ...opts, + }); + const { CanvasResult: { Submissions: submissions } } = await new xml2js + .Parser().parseStringPromise(response); + if (!submissions?.length) { + return []; + } + return submissions.flatMap((sub) => sub.Submission || []); + }, + async getDispatchDescription({ + dispatchId, ...opts + }) { + const dispatches = await this.getActiveDispatches({ + ...opts, + }); + const dispatch = dispatches.find(({ $ }) => $.Id === dispatchId); + return dispatch.Description[0]; + }, + dispatchItems(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/dispatch_items", + ...opts, + }); + }, + createUpdateReferenceData(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/reference_datas", + ...opts, + }); + }, + async *paginate({ + fn, + params, + max, + }) { + params = { + ...params, + page: 1, + }; + let total, count = 0; + do { + const results = await fn({ + params, + }); + for (const item of results) { + yield item; + if (max && ++count >= max) { + return; + } + } + total = results?.length; + params.page++; + } while (total); }, }, }; diff --git a/components/gocanvas/package.json b/components/gocanvas/package.json new file mode 100644 index 0000000000000..d5437280de080 --- /dev/null +++ b/components/gocanvas/package.json @@ -0,0 +1,20 @@ +{ + "name": "@pipedream/gocanvas", + "version": "0.0.1", + "description": "Pipedream GoCanvas Components", + "main": "gocanvas.app.mjs", + "keywords": [ + "pipedream", + "gocanvas" + ], + "homepage": "https://pipedream.com/apps/gocanvas", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "csv-parse": "^5.5.6", + "xml2js": "^0.6.2" + } +} diff --git a/components/gocanvas/sources/new-submission-received/new-submission-received.mjs b/components/gocanvas/sources/new-submission-received/new-submission-received.mjs new file mode 100644 index 0000000000000..5b655fb8f18df --- /dev/null +++ b/components/gocanvas/sources/new-submission-received/new-submission-received.mjs @@ -0,0 +1,103 @@ +import gocanvas from "../../gocanvas.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + key: "gocanvas-new-submission-received", + name: "New Submission Recieved", + description: "Emit new event when a new submission is uploaded to GoCanvas.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + gocanvas, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + form: { + propDefinition: [ + gocanvas, + "form", + ], + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastSubmissionDate() { + return this.db.get("lastSubmissionDate"); + }, + _setLastSubmissionDate(lastSubmissionDate) { + this.db.set("lastSubmissionDate", lastSubmissionDate); + }, + generateMeta(submission) { + return { + id: submission.ResponseID, + summary: `New Submission: ${submission.ResponseID}`, + ts: Date.parse(submission.Date), + }; + }, + currentDate() { + const currentDate = new Date(); + return `${String(currentDate.getMonth() + 1) + .padStart(2, "0")}/${String(currentDate.getDate()) + .padStart(2, "0")}/${currentDate.getFullYear()}`; + }, + formatResponse(obj) { + if (Array.isArray(obj) && obj.length === 1) { + return this.formatResponse(obj[0]); + } else if (typeof obj === "object" && obj !== null) { + return Object.fromEntries( + Object.entries(obj).map(([ + key, + value, + ]) => [ + key, + this.formatResponse(value), + ]), + ); + } else { + return obj; + } + }, + async processEvent(max) { + let lastSubmissionDate = this._getLastSubmissionDate(); + + const params = { + form_name: this.form, + }; + if (lastSubmissionDate) { + params.begin_date = new Date(lastSubmissionDate).toLocaleDateString("en-US"); + params.end_date = this.currentDate(); + } + + const results = this.gocanvas.paginate({ + fn: this.gocanvas.listSubmissions, + params, + max, + }); + + for await (const result of results) { + const submission = this.formatResponse(result); + const meta = this.generateMeta(submission); + this.$emit(submission, meta); + + if (!lastSubmissionDate + || Date.parse(submission.Date) >= Date.parse(lastSubmissionDate)) { + lastSubmissionDate = submission.Date; + } + } + + this._setLastSubmissionDate(lastSubmissionDate); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/godaddy/README.md b/components/godaddy/README.md index dd9867df924d2..bf73287015484 100644 --- a/components/godaddy/README.md +++ b/components/godaddy/README.md @@ -1,8 +1,11 @@ # Overview -With the GoDaddy API, you can build applications that: +The GoDaddy API provides programmatic access to manage aspects of your GoDaddy domain and hosting services. Through Pipedream, you can automate domain availability checks, renewals, and DNS record management. This enables seamless integration of domain-related operations within your automated workflows, such as dynamically updating DNS records or triggering actions based on domain registration events. -- Automate the process of registering and managing domains -- Enable end-users to manage their domain names and settings -- Allow resellers to manage domain names on behalf of their customers -- Provide access to GoDaddy's extensive domain name search data +# Example Use Cases + +- **Automated Domain Renewal Notifications**: Create a workflow that triggers monthly to check the expiration status of your domains using the GoDaddy API. If a domain is approaching its expiration date, Pipedream can send an alert via email, SMS, or a Slack message, ensuring you never miss a renewal. + +- **Dynamic DNS Update on IP Change**: Set up a Pipedream workflow that listens for IP address change events from your ISP. On detecting a change, the workflow uses the GoDaddy API to automatically update the A record of your domain, thus maintaining your domain's access to the correct IP. + +- **Domain Availability Monitoring for Brand Protection**: Configure a Pipedream workflow to periodically check the availability of domains containing your brand through the GoDaddy API. If a desired domain becomes available or a potentially infringing domain is registered, the workflow can notify you or automatically initiate a domain purchase or dispute process. diff --git a/components/godial/README.md b/components/godial/README.md index e87c0353b2c9d..6f7fb9d57ddc3 100644 --- a/components/godial/README.md +++ b/components/godial/README.md @@ -1,8 +1,11 @@ # Overview -The GoDial API can be used to build a variety of applications, including: +GoDial turns your phone into a call center, enabling businesses to manage calls and contacts efficiently. The API provides programmatic access to this functionality, allowing you to trigger calls, manage contacts, and automate call logging. With Pipedream, you can harness GoDial's capabilities to create workflows that connect call activities with CRM platforms, customer support tickets, and notification systems, streamlining communication processes within your business. -- A dialer application to make phone calls -- A VoIP application to make calls over the internet -- A call center application to manage customer calls -- An IVR system to manage phone calls for businesses +# Example Use Cases + +- **CRM Integration for Customer Follow-Up**: Automate the creation of follow-up tasks in a CRM like Salesforce or HubSpot when a call ends in GoDial. Using Pipedream, you can capture call details, check the call outcome, and create a task in the CRM for sales agents to execute tailored follow-up actions based on the call's result. + +- **Support Ticket Creation**: Instantly generate support tickets in tools like Zendesk or Jira when a call is identified as a support query in GoDial. Pipedream can take the call data, classify the type of support needed, and open a new ticket with all relevant details, ensuring that customer issues are addressed promptly and efficiently. + +- **Real-Time Notifications for Missed Calls**: Set up a workflow that sends real-time alerts via Slack or email when a call is missed in GoDial. With Pipedream, you can monitor missed call events, filter based on importance or client, and dispatch notifications to the responsible team members to enable quick call-backs and minimize customer wait times. diff --git a/components/gohighlevel/README.md b/components/gohighlevel/README.md index 5dde5de3515dc..dbde69fe78514 100644 --- a/components/gohighlevel/README.md +++ b/components/gohighlevel/README.md @@ -1,13 +1,11 @@ # Overview -GoHighLevel is a powerful API that enables you to quickly and easily build a -wide variety of applications. +The HighLevel API offers a suite of tools for customer relationship management, marketing automation, and business growth. Leveraging this API on Pipedream allows you to automate interactions with leads, manage contacts, and streamline communication workflows. This integration can help you track customer interactions, automate follow-ups, and sync data across platforms, ultimately improving productivity and customer engagement. -Here are just a few examples of what you can build using GoHighLevel: +# Example Use Cases -- A shopping cart application -- A social networking website -- An online forum -- A blog -- A video sharing website -- An online customer support system +- **Automated Lead Capture to CRM**: Automatically add new leads captured from various sources like web forms, chatbots, or landing pages directly to HighLevel. Sync this data with Pipedream workflows to create or update contacts, ensuring your CRM reflects real-time interactions. + +- **Smart Outreach Campaigns**: Trigger personalized outreach campaigns in HighLevel based on customer behavior or milestones. Use Pipedream to listen for specific events, like a purchase or signup, and initiate targeted follow-up emails or SMS messages enhancing customer retention and upsell opportunities. + +- **Support Ticket Integration**: Connect HighLevel to a support platform like Zendesk or Help Scout via Pipedream. When a support ticket is resolved, automatically update the corresponding contact in HighLevel to maintain an up-to-date record of customer interactions, aiding in service improvement and personalized communication. diff --git a/components/gohighlevel/package.json b/components/gohighlevel/package.json new file mode 100644 index 0000000000000..85e8e263c0d9c --- /dev/null +++ b/components/gohighlevel/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/gohighlevel", + "version": "0.6.0", + "description": "Pipedream gohighlevel Components", + "main": "gohighlevel.app.mjs", + "keywords": [ + "pipedream", + "gohighlevel" + ], + "homepage": "https://pipedream.com/apps/gohighlevel", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/gong/README.md b/components/gong/README.md new file mode 100644 index 0000000000000..5b6d72694a490 --- /dev/null +++ b/components/gong/README.md @@ -0,0 +1,11 @@ +# Overview + +The Gong API allows you to tap into Gong's conversation intelligence capabilities, enabling you to retrieve call recordings, transcripts, and analytics data for sales engagements. By integrating Gong with Pipedream, you can automate workflows that react to this sales data, enrich CRM records, trigger follow-up actions, and sync insights across your sales stack. It's a goldmine for sales teams looking to leverage conversational insights and make data-driven decisions. + +# Example Use Cases + +- **Automated CRM Updates**: Pull conversation data from Gong and push relevant insights into CRM records. Keep your sales team informed with the latest call summaries and action items directly within the CRM interface. + +- **Real-time Alerts for Keyword Mentions**: Set up a workflow that monitors Gong transcripts for specific keywords or phrases. Notify the sales manager or team members in Slack or via email when these are detected, ensuring quick follow-up on important deals or customer concerns. + +- **Meeting Insights to Data Warehouse**: Aggregate conversation analytics from Gong and send them to a data warehouse like BigQuery. Enable deeper analysis of sales calls to uncover trends, which can inform strategy and training. diff --git a/components/gong/actions/add-new-call/add-new-call.mjs b/components/gong/actions/add-new-call/add-new-call.mjs index 36a4925559309..38b21904435c7 100644 --- a/components/gong/actions/add-new-call/add-new-call.mjs +++ b/components/gong/actions/add-new-call/add-new-call.mjs @@ -8,7 +8,7 @@ export default { name: "Add New Call", description: "Add a new call. [See the documentation](https://us-66463.app.gong.io/settings/api/documentation#post-/v2/calls)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { app, clientUniqueId: { diff --git a/components/gong/actions/list-calls/list-calls.mjs b/components/gong/actions/list-calls/list-calls.mjs index abe28e2b2c72b..3fc6631be0b4e 100644 --- a/components/gong/actions/list-calls/list-calls.mjs +++ b/components/gong/actions/list-calls/list-calls.mjs @@ -5,7 +5,7 @@ export default { name: "List calls", description: "List calls. [See the documentation](https://us-66463.app.gong.io/settings/api/documentation#get-/v2/calls)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { app, fromDateTime: { diff --git a/components/gong/actions/retrieve-transcripts-of-calls/retrieve-transcripts-of-calls.mjs b/components/gong/actions/retrieve-transcripts-of-calls/retrieve-transcripts-of-calls.mjs index 4b281c086bbe7..5c37201adccb5 100644 --- a/components/gong/actions/retrieve-transcripts-of-calls/retrieve-transcripts-of-calls.mjs +++ b/components/gong/actions/retrieve-transcripts-of-calls/retrieve-transcripts-of-calls.mjs @@ -5,7 +5,7 @@ export default { name: "Retrieve Transcripts Of Calls", description: "Retrieve transcripts of calls. [See the documentation](https://us-66463.app.gong.io/settings/api/documentation#post-/v2/calls/transcript)", type: "action", - version: "0.0.1", + version: "0.0.3", props: { app, fromDateTime: { @@ -29,6 +29,19 @@ export default { "workspaceId", ], }, + callIds: { + propDefinition: [ + app, + "callIds", + ], + }, + returnSimplifiedTranscript: { + type: "boolean", + label: "Return Simplified Transcript", + description: "If true, returns a simplified version of the transcript with normalized speaker IDs and formatted timestamps", + optional: true, + default: false, + }, }, methods: { retrieveTranscriptsOfCalls(args = {}) { @@ -37,21 +50,101 @@ export default { ...args, }); }, + + millisToTimestamp(millis) { + const totalSeconds = Math.floor(millis / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + + return `[${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}]`; + }, + + simplifyTranscript(originalResponse) { + const simplified = { + ...originalResponse, + callTranscripts: originalResponse.callTranscripts.map((callTranscript) => { + // Create a map of unique speaker IDs to simplified names + const speakerMap = new Map(); + let speakerCounter = 1; + let currentSpeaker = null; + let currentTopic = null; + let formattedTranscript = ""; + + // Process each sentence maintaining chronological order + const allSentences = []; + callTranscript.transcript.forEach((segment) => { + segment.sentences.forEach((sentence) => { + allSentences.push({ + ...sentence, + speakerId: segment.speakerId, + topic: segment.topic, + }); + }); + }); + + // Sort by start time + allSentences.sort((a, b) => a.start - b.start); + + // Process sentences + allSentences.forEach((sentence) => { + // Map speaker ID to simplified name + if (!speakerMap.has(sentence.speakerId)) { + speakerMap.set(sentence.speakerId, `Speaker ${speakerCounter}`); + speakerCounter++; + } + + const speaker = speakerMap.get(sentence.speakerId); + const timestamp = this.millisToTimestamp(sentence.start); + + // Handle topic changes + if (sentence.topic !== currentTopic) { + currentTopic = sentence.topic; + if (currentTopic) { + formattedTranscript += `\nTopic: ${currentTopic}\n-------------------\n\n`; + } + } + + // Add speaker name only if it changes + if (speaker !== currentSpeaker) { + currentSpeaker = speaker; + formattedTranscript += `\n${speaker}:\n`; + } + + // Add timestamp and text + formattedTranscript += `${timestamp} ${sentence.text}\n`; + }); + + return { + callId: callTranscript.callId, + formattedTranscript: formattedTranscript.trim(), + }; + }), + }; + + return simplified; + }, }, - run({ $: step }) { + + async run({ $: step }) { const { - // eslint-disable-next-line no-unused-vars - app, retrieveTranscriptsOfCalls, + returnSimplifiedTranscript, + simplifyTranscript, ...filter } = this; - return retrieveTranscriptsOfCalls({ + const response = await retrieveTranscriptsOfCalls({ step, data: { filter, }, - summary: (response) => `Successfully retrieved transcripts of calls with request ID \`${response.requestId}\``, + summary: (response) => `Successfully retrieved transcripts of calls with request ID \`${response.requestId}\`.`, }); + + if (returnSimplifiedTranscript) { + return simplifyTranscript(response); + } + + return response; }, }; diff --git a/components/gong/gong.app.mjs b/components/gong/gong.app.mjs index 681595c9afc0f..828b21a82dd6f 100644 --- a/components/gong/gong.app.mjs +++ b/components/gong/gong.app.mjs @@ -66,6 +66,21 @@ export default { label: "To Date Time", description: "Date and time (in ISO-8601 format: `2018-02-18T02:30:00-07:00` or `2018-02-18T08:00:00Z`, where Z stands for UTC) until which to list recorded calls. Returns calls that started up to but excluding specified date and time. If not provided, list ends with most recent call.", }, + callIds: { + type: "string[]", + label: "Call IDs", + description: "List of calls Ids to be filtered. If not supplied, returns all calls between **From Date Time** and **To Date Time**.", + optional: true, + async options() { + const { calls } = await this.listCalls(); + return calls.map(({ + id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, }, methods: { exportSummary(step) { diff --git a/components/gong/package.json b/components/gong/package.json index ac7811567cffb..1d792f2301947 100644 --- a/components/gong/package.json +++ b/components/gong/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/gong", - "version": "0.1.0", + "version": "0.1.2", "description": "Pipedream Gong Components", "main": "gong.app.mjs", "keywords": [ @@ -10,7 +10,7 @@ "homepage": "https://pipedream.com/apps/gong", "author": "Pipedream (https://pipedream.com/)", "dependencies": { - "@pipedream/platform": "^1.5.1" + "@pipedream/platform": "^3.0.3" }, "publishConfig": { "access": "public" diff --git a/components/gong/sources/new-call/new-call.mjs b/components/gong/sources/new-call/new-call.mjs index fe8c0b1eba23c..91b44d543eccb 100644 --- a/components/gong/sources/new-call/new-call.mjs +++ b/components/gong/sources/new-call/new-call.mjs @@ -6,7 +6,7 @@ export default { name: "New Call", description: "Triggers when a new call is added. [See the documentation](https://us-66463.app.gong.io/settings/api/documentation#get-/v2/calls)", type: "source", - version: "0.0.1", + version: "0.0.2", dedupe: "unique", methods: { ...common.methods, diff --git a/components/goodbits/README.md b/components/goodbits/README.md new file mode 100644 index 0000000000000..e1b0cb35e678a --- /dev/null +++ b/components/goodbits/README.md @@ -0,0 +1,11 @@ +# Overview + +The Goodbits API provides a way to extend the functionality of your email marketing efforts by allowing you to automate the creation and management of newsletters. With Pipedream, you can harness this API to craft workflows that streamline your email campaigns, such as synchronizing subscriber lists, triggering email sends based on external events, and analyzing campaign performance data. By integrating Goodbits with other apps available on Pipedream, you can build a robust, automated system that keeps your audience engaged and informed with less manual effort. + +# Example Use Cases + +- **Automate Newsletter Creation**: Trigger a Pipedream workflow whenever content is published on your CMS (like WordPress). The workflow can fetch the new content and use the Goodbits API to create and populate a newsletter with the latest articles, ready for review and send-out. + +- **Synchronize Subscribers with CRM**: Maintain a consistent subscriber list by connecting Goodbits to your CRM (like Salesforce). When a new contact is added to your CRM, automatically add them as a subscriber to your Goodbits mailing list, ensuring your email campaigns reach the latest prospects. + +- **Analyze Campaign Performance**: After sending a newsletter, use a Pipedream workflow to collect campaign metrics from Goodbits and send them to a data visualization tool (like Google Sheets or Data Studio). This allows for real-time analysis and actionable insights on the effectiveness of your email campaigns. diff --git a/components/goodreads/README.md b/components/goodreads/README.md index c02b37b946680..4539e60382044 100644 --- a/components/goodreads/README.md +++ b/components/goodreads/README.md @@ -1,6 +1,11 @@ # Overview -With the Goodreads API, you can access data about books, authors, and events. -This includes information such as title, author, publication date, and -description. You can also use the Goodreads API to search for specific books, -authors, or events. +The Goodreads API allows you to tap into a vast database of books, reviews, and reading data. With it, you can fetch details about books, find similar books, and access user reviews and ratings. On Pipedream, this translates into a wealth of opportunities for automating tasks related to literature discovery, sharing reading experiences, and connecting with fellow readers. + +# Example Use Cases + +- **Automated Reading Lists**: Create workflows that generate personalized reading lists based on genre preferences and past ratings. When a user updates their preferences or ratings on Goodreads, Pipedream can trigger a sequence that pulls related book suggestions and compiles them into a list, which can then be emailed or sent via a messaging app. + +- **Review Aggregator**: Monitor new reviews for specific books or authors and aggregate them into a digest. Whenever a new review is posted on Goodreads, Pipedream can capture this data and add it to a spreadsheet or database. This could be particularly useful for authors or publishers looking to track public reception. + +- **Social Reading Updates**: Share your reading progress or reviews automatically across social media platforms. When you mark a book as 'read' or post a review on Goodreads, Pipedream can post an update to Twitter, LinkedIn, or Facebook, keeping your network engaged with your reading journey. diff --git a/components/goodreads/package.json b/components/goodreads/package.json new file mode 100644 index 0000000000000..a10ba20f302f4 --- /dev/null +++ b/components/goodreads/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/goodreads", + "version": "0.6.0", + "description": "Pipedream goodreads Components", + "main": "goodreads.app.mjs", + "keywords": [ + "pipedream", + "goodreads" + ], + "homepage": "https://pipedream.com/apps/goodreads", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/goody/README.md b/components/goody/README.md new file mode 100644 index 0000000000000..b0c87baa21757 --- /dev/null +++ b/components/goody/README.md @@ -0,0 +1,11 @@ +# Overview + +The Goody API allows for the creation of digital rewards and incentives, facilitating the sending of gift cards and other perks electronically. In Pipedream, you can harness this API to automate the distribution of rewards, integrate with CRM systems, or track and analyze the impact of your rewards program. By leveraging Pipedream’s capability to connect with numerous other platforms, you can create multifaceted workflows that trigger based on various events or conditions. + +# Example Use Cases + +- **Customer Milestone Rewards**: Automatically send a digital gift card via the Goody API when a customer reaches a certain spending threshold in your e-commerce platform. + +- **Employee Recognition Program**: Set up a workflow that listens for positive feedback in a team communication app like Slack and then uses Goody API to send a thank you note with a small gift card. + +- **Survey Participation Incentives**: After a user completes a survey from a tool like Typeform, trigger a workflow that sends a reward through Goody API to thank them for their time and insights. diff --git a/components/google/README.md b/components/google/README.md index c8efff75ebd29..bf850b3a4f301 100644 --- a/components/google/README.md +++ b/components/google/README.md @@ -1,10 +1,11 @@ # Overview -Some things you can build using the Google API include: - -- A web application that retrieves data from the Google API and displays it to - the user -- A mobile application that uses the Google API to provide location-based - services -- A desktop application that accesses the Google API to perform searches and - other tasks +The Google API on Pipedream is a powerhouse for automating interactions with various Google services, like Google Sheets, Gmail, Calendar, and Drive. With it, you can read and write data, manage emails, calendar events, and files, and integrate with other APIs for enhanced workflows. + +# Example Use Cases + +- **Automate Gmail inbox management**: Create workflows that filter incoming emails, label them, or trigger actions in other apps based on the content or attachments of the emails. + +- **Dynamic Google Sheets reporting**: Set up a workflow that collects data from different sources, such as sales figures from a CRM or statistics from a marketing platform, and populates or updates a Google Sheet in real-time. + +- **Google Calendar event coordination**: Build a system that listens for new events in a Google Calendar, sends reminders via SMS or Slack, and automatically creates Zoom or Microsoft Teams meeting links. diff --git a/components/google/package.json b/components/google/package.json new file mode 100644 index 0000000000000..05859da59c2bc --- /dev/null +++ b/components/google/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/google", + "version": "0.6.0", + "description": "Pipedream google Components", + "main": "google.app.mjs", + "keywords": [ + "pipedream", + "google" + ], + "homepage": "https://pipedream.com/apps/google", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/google_ad_manager/actions/create-report/create-report.mjs b/components/google_ad_manager/actions/create-report/create-report.mjs new file mode 100644 index 0000000000000..e7b0cee1f6a11 --- /dev/null +++ b/components/google_ad_manager/actions/create-report/create-report.mjs @@ -0,0 +1,376 @@ +import app from "../../google_ad_manager.app.mjs"; +import dimensions from "../../common/dimensions.mjs"; +import metrics from "../../common/metrics.mjs"; +import relativeDateRanges from "../../common/relative-date-ranges.mjs"; +import utils from "../../common/utils.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "google_ad_manager-create-report", + name: "Create Report", + description: "Create a report in Google Ad Manager. [See the documentation](https://developers.google.com/ad-manager/api/beta/reference/rest/v1/networks.reports/create)", + version: "0.0.1", + type: "action", + props: { + app, + parent: { + label: "Parent Network", + description: "The parent resource where this Report will be created. Format: `networks/{networkCode}`.", + propDefinition: [ + app, + "network", + ], + }, + name: { + label: "Name", + description: "Identifier. The resource name of the report. Report resource names have the form: `networks/{networkCode}/reports/{reportId}`.", + type: "string", + }, + visibility: { + type: "string", + label: "Visibility", + description: "The visibility of the report.", + options: constants.VISIBILITY_OPTIONS, + }, + displayName: { + type: "string", + label: "Display Name", + description: "The display name of the report.", + optional: true, + }, + scheduleOptions: { + type: "object", + label: "Schedule Options", + description: "The options for a scheduled report. [See the documentation](https://developers.google.com/ad-manager/api/beta/reference/rest/v1/networks.reports#ScheduleOptions).", + optional: true, + }, + dimensions: { + type: "string[]", + label: "Dimensions", + description: "The list of dimensions to report on. If empty, the report will have no dimensions, and any metrics will be totals.", + options: Object.values(dimensions), + }, + metrics: { + type: "string[]", + label: "Metrics", + description: "The list of metrics to report on. If empty, the report will have no metrics.", + options: Object.values(metrics), + }, + filters: { + type: "string[]", + label: "Filters", + description: "The filters for this report. Each row must be formatted as a JSON object. [See the documentation](https://developers.google.com/ad-manager/api/beta/reference/rest/v1/networks.reports#filter).", + optional: true, + }, + timeZone: { + type: "string", + label: "Time Zone", + description: "The time zone the date range is defined in for this report. Defaults to publisher's time zone if not specified. Time zone in IANA format. Acceptable values depend on the report type. Publisher time zone is always accepted. Use `America/Los_Angeles` for pacific time, or `Etc/UTC` for UTC.", + optional: true, + }, + currencyCode: { + type: "string", + label: "Currency Code", + description: "The ISO 4217 currency code for this report. Defaults to publisher currency code if not specified.", + optional: true, + }, + customDimensionKeyIds: { + type: "string[]", + label: "Custom Dimension Key IDs", + description: "Custom Dimension keys that represent `CUSTOM_DIMENSION_*` dimensions. The index of this repeated field corresponds to the index on each dimension. For example, `customDimensionKeyIds[0]` describes `CUSTOM_DIMENSION_0_VALUE_ID` and `CUSTOM_DIMENSION_0_VALUE`.", + optional: true, + }, + lineItemCustomFieldIds: { + type: "string[]", + label: "Line Item Custom Field IDs", + description: "Custom field IDs that represent `LINE_ITEM_CUSTOM_FIELD_*` dimensions. The index of this repeated field corresponds to the index on each dimension. For example, `lineItemCustomFieldIds[0]` describes `LINE_ITEM_CUSTOM_FIELD_0_OPTION_ID` and `LINE_ITEM_CUSTOM_FIELD_0_VALUE`.", + optional: true, + }, + orderCustomFieldIds: { + type: "string[]", + label: "Order Custom Field IDs", + description: "Custom field IDs that represent `ORDER_CUSTOM_FIELD_*` dimensions. The index of this repeated field corresponds to the index on each dimension. For example, `orderCustomFieldIds[0]` describes `ORDER_CUSTOM_FIELD_0_OPTION_ID` and `ORDER_CUSTOM_FIELD_0_VALUE`.", + optional: true, + }, + creativeCustomFieldIds: { + type: "string[]", + label: "Creative Custom Field IDs", + description: "Custom field IDs that represent `CREATIVE_CUSTOM_FIELD_*` dimensions. The index of this repeated field corresponds to the index on each dimension. For example, `creativeCustomFieldIds[0]` describes `CREATIVE_CUSTOM_FIELD_0_OPTION_ID` and `CREATIVE_CUSTOM_FIELD_0_VALUE`.", + optional: true, + }, + reportType: { + type: "string", + label: "Report Type", + description: "The type of this report.", + options: constants.REPORT_TYPE_OPTIONS, + }, + timePeriodColumn: { + type: "string", + label: "Time Period Column", + description: "Include a time period column to introduce comparison columns in the report for each generated period. For example, set to `QUARTERS` here to have a column for each quarter present in the primary date range. If `PREVIOUS PERIOD` is specified in **Comparison Date Range**, then each quarter column will also include comparison values for its relative previous quarter.", + optional: true, + options: constants.TIME_PERIOD_COLUMN_OPTIONS, + }, + flags: { + type: "string[]", + label: "Flags", + description: "List of flags for this report. Used to flag rows in a result set based on a set of defined filters. Each row must be formatted as a JSON object. [See the documentation](https://developers.google.com/ad-manager/api/beta/reference/rest/v1/networks.reports#Flag).", + optional: true, + }, + sorts: { + type: "string[]", + label: "Sorts", + description: "Default sorts to apply to this report. Each row must be formatted as a JSON object. [See the documentation](https://developers.google.com/ad-manager/api/beta/reference/rest/v1/networks.reports#Sort).", + optional: true, + }, + dateRange: { + type: "string", + label: "Date Range Type", + description: "The date range of this report.", + options: Object.values(constants.DATE_RANGE_TYPE), + reloadProps: true, + }, + comparisonDateRange: { + type: "string", + label: "Comparison Date Range Type", + description: "The comparison date range of this report. If unspecified, the report will not have any comparison metrics.", + optional: true, + options: Object.values(constants.DATE_RANGE_TYPE), + reloadProps: true, + }, + }, + methods: { + createReport({ + parent, ...args + } = {}) { + return this.app.post({ + path: `/${parent}/reports`, + ...args, + }); + }, + getComparisonDateRangeProps() { + const { comparisonDateRange } = this; + if (comparisonDateRange === constants.DATE_RANGE_TYPE.FIXED) { + return { + comparisonStartDateYear: { + type: "integer", + label: "Comparison Start Date Year", + description: "Year of the date. Must be from `1` to `9999`, or `0` to specify a date without a year.", + }, + comparisonStartDateMonth: { + type: "integer", + label: "Comparison Start Date Month", + description: "Month of a year. Must be from `1` to `12`, or `0` to specify a year without a month and day.", + }, + comparisonStartDateDay: { + type: "integer", + label: "Comparison Start Date Day", + description: "Day of a month. Must be from `1` to `31` and valid for the year and month, or `0` to specify a year by itself or a year and month where the day isn't significant.", + }, + comparisonEndDateYear: { + type: "integer", + label: "Comparison End Date Year", + description: "Year of the date. Must be from `1` to `9999`, or `0` to specify a date without a year.", + }, + comparisonEndDateMonth: { + type: "integer", + label: "Comparison End Date Month", + description: "Month of a year. Must be from `1` to `12`, or `0` to specify a year without a month and day.", + }, + comparisonEndDateDay: { + type: "integer", + label: "Comparison End Date Day", + description: "Day of a month. Must be from `1` to `31` and valid for the year and month, or `0` to specify a year by itself or a year and month where the day isn't significant.", + }, + }; + } else if (comparisonDateRange === constants.DATE_RANGE_TYPE.RELATIVE) { + return { + comparisonRelative: { + type: "string", + label: "Comparison Relative Date Range", + description: "The relative date range of this report.", + options: Object.values(relativeDateRanges), + }, + }; + } + }, + getDateRangeValues() { + const { + dateRange, + relative, + startDateYear, + startDateMonth, + startDateDay, + endDateYear, + endDateMonth, + endDateDay, + } = this; + + if (dateRange === constants.DATE_RANGE_TYPE.FIXED) { + return { + dateRange: { + fixed: { + startDate: { + year: startDateYear, + month: startDateMonth, + day: startDateDay, + }, + endDate: { + year: endDateYear, + month: endDateMonth, + day: endDateDay, + }, + }, + }, + }; + } + return { + dateRange: { + relative, + }, + }; + }, + getComparisonDateRangeValues() { + const { + comparisonDateRange, + comparisonRelative, + comparisonStartDateYear, + comparisonStartDateMonth, + comparisonStartDateDay, + comparisonEndDateYear, + comparisonEndDateMonth, + comparisonEndDateDay, + } = this; + + if (comparisonDateRange === constants.DATE_RANGE_TYPE.FIXED) { + return { + comparisonDateRange: { + fixed: { + startDate: { + year: comparisonStartDateYear, + month: comparisonStartDateMonth, + day: comparisonStartDateDay, + }, + endDate: { + year: comparisonEndDateYear, + month: comparisonEndDateMonth, + day: comparisonEndDateDay, + }, + }, + }, + }; + } else if (comparisonDateRange === constants.DATE_RANGE_TYPE.RELATIVE) { + return { + comparisonDateRange: { + relative: comparisonRelative, + }, + }; + } + }, + }, + additionalProps() { + const { dateRange } = this; + + if (dateRange === "fixed") { + return { + startDateYear: { + type: "integer", + label: "Start Date Year", + description: "Year of the date. Must be from `1` to `9999`, or `0` to specify a date without a year.", + }, + startDateMonth: { + type: "integer", + label: "Start Date Month", + description: "Month of a year. Must be from `1` to `12`, or `0` to specify a year without a month and day.", + }, + startDateDay: { + type: "integer", + label: "Start Date Day", + description: "Day of a month. Must be from `1` to `31` and valid for the year and month, or `0` to specify a year by itself or a year and month where the day isn't significant.", + }, + endDateYear: { + type: "integer", + label: "End Date Year", + description: "Year of the date. Must be from `1` to `9999`, or `0` to specify a date without a year.", + }, + endDateMonth: { + type: "integer", + label: "End Date Month", + description: "Month of a year. Must be from `1` to `12`, or `0` to specify a year without a month and day.", + }, + endDateDay: { + type: "integer", + label: "End Date Day", + description: "Day of a month. Must be from `1` to `31` and valid for the year and month, or `0` to specify a year by itself or a year and month where the day isn't significant.", + }, + ...this.getComparisonDateRangeProps(), + }; + } + + return { + relative: { + type: "string", + label: "Relative Date Range", + description: "The relative date range of this report.", + options: Object.values(relativeDateRanges), + }, + ...this.getComparisonDateRangeProps(), + }; + }, + async run({ $ }) { + const { + getDateRangeValues, + getComparisonDateRangeValues, + createReport, + parent, + name, + visibility, + displayName, + scheduleOptions, + dimensions, + metrics, + filters, + timeZone, + currencyCode, + customDimensionKeyIds, + lineItemCustomFieldIds, + orderCustomFieldIds, + creativeCustomFieldIds, + reportType, + timePeriodColumn, + flags, + sorts, + } = this; + + const response = await createReport({ + $, + parent, + data: { + name, + visibility, + displayName, + scheduleOptions, + reportDefinition: { + dimensions, + metrics, + filters: utils.parseArray(filters), + timeZone, + currencyCode, + customDimensionKeyIds, + lineItemCustomFieldIds, + orderCustomFieldIds, + creativeCustomFieldIds, + reportType, + timePeriodColumn, + flags: utils.parseArray(flags), + sorts: utils.parseArray(sorts), + ...getDateRangeValues(), + ...getComparisonDateRangeValues(), + }, + }, + }); + + $.export("$summary", `Successfully created report in network ${parent}`); + return response; + }, +}; diff --git a/components/google_ad_manager/common/constants.mjs b/components/google_ad_manager/common/constants.mjs new file mode 100644 index 0000000000000..37584495810a9 --- /dev/null +++ b/components/google_ad_manager/common/constants.mjs @@ -0,0 +1,35 @@ +const BASE_URL = "https://admanager.googleapis.com"; +const VERSION_PATH = "/v1"; + +const DATE_RANGE_TYPE = { + FIXED: "fixed", + RELATIVE: "relative", +}; + +const VISIBILITY_OPTIONS = [ + "HIDDEN", + "DRAFT", + "SAVED", +]; + +const REPORT_TYPE_OPTIONS = [ + "REPORT_TYPE_UNSPECIFIED", + "HISTORICAL", +]; + +const TIME_PERIOD_COLUMN_OPTIONS = [ + "TIME_PERIOD_COLUMN_UNSPECIFIED", + "TIME_PERIOD_COLUMN_DATE", + "TIME_PERIOD_COLUMN_WEEK", + "TIME_PERIOD_COLUMN_MONTH", + "TIME_PERIOD_COLUMN_QUARTER", +]; + +export default { + BASE_URL, + VERSION_PATH, + DATE_RANGE_TYPE, + VISIBILITY_OPTIONS, + REPORT_TYPE_OPTIONS, + TIME_PERIOD_COLUMN_OPTIONS, +}; diff --git a/components/google_ad_manager/common/dimensions.mjs b/components/google_ad_manager/common/dimensions.mjs new file mode 100644 index 0000000000000..7dd96706e0bdc --- /dev/null +++ b/components/google_ad_manager/common/dimensions.mjs @@ -0,0 +1,2190 @@ +export default { + DIMENSION_UNSPECIFIED: { + label: "Dimension Unspecified", + value: "DIMENSION_UNSPECIFIED", + }, + ADVERTISER_DOMAIN_NAME: { + label: "Advertiser Domain Name", + value: "ADVERTISER_DOMAIN_NAME", + }, + ADVERTISER_EXTERNAL_ID: { + label: "Advertiser External ID", + value: "ADVERTISER_EXTERNAL_ID", + }, + ADVERTISER_ID: { + label: "Advertiser ID", + value: "ADVERTISER_ID", + }, + ADVERTISER_LABELS: { + label: "Advertiser Labels", + value: "ADVERTISER_LABELS", + }, + ADVERTISER_LABEL_IDS: { + label: "Advertiser Label IDs", + value: "ADVERTISER_LABEL_IDS", + }, + ADVERTISER_NAME: { + label: "Advertiser Name", + value: "ADVERTISER_NAME", + }, + ADVERTISER_PRIMARY_CONTACT: { + label: "Advertiser Primary Contact", + value: "ADVERTISER_PRIMARY_CONTACT", + }, + AD_LOCATION: { + label: "Ad Location", + value: "AD_LOCATION", + }, + AD_LOCATION_NAME: { + label: "Ad Location Name", + value: "AD_LOCATION_NAME", + }, + AD_UNIT_CODE: { + label: "Ad Unit Code", + value: "AD_UNIT_CODE", + }, + AD_UNIT_CODE_LEVEL_1: { + label: "Ad Unit Code Level 1", + value: "AD_UNIT_CODE_LEVEL_1", + }, + AD_UNIT_CODE_LEVEL_10: { + label: "Ad Unit Code Level 10", + value: "AD_UNIT_CODE_LEVEL_10", + }, + AD_UNIT_CODE_LEVEL_11: { + label: "Ad Unit Code Level 11", + value: "AD_UNIT_CODE_LEVEL_11", + }, + AD_UNIT_CODE_LEVEL_12: { + label: "Ad Unit Code Level 12", + value: "AD_UNIT_CODE_LEVEL_12", + }, + AD_UNIT_CODE_LEVEL_13: { + label: "Ad Unit Code Level 13", + value: "AD_UNIT_CODE_LEVEL_13", + }, + AD_UNIT_CODE_LEVEL_14: { + label: "Ad Unit Code Level 14", + value: "AD_UNIT_CODE_LEVEL_14", + }, + AD_UNIT_CODE_LEVEL_15: { + label: "Ad Unit Code Level 15", + value: "AD_UNIT_CODE_LEVEL_15", + }, + AD_UNIT_CODE_LEVEL_16: { + label: "Ad Unit Code Level 16", + value: "AD_UNIT_CODE_LEVEL_16", + }, + AD_UNIT_CODE_LEVEL_2: { + label: "Ad Unit Code Level 2", + value: "AD_UNIT_CODE_LEVEL_2", + }, + AD_UNIT_CODE_LEVEL_3: { + label: "Ad Unit Code Level 3", + value: "AD_UNIT_CODE_LEVEL_3", + }, + AD_UNIT_CODE_LEVEL_4: { + label: "Ad Unit Code Level 4", + value: "AD_UNIT_CODE_LEVEL_4", + }, + AD_UNIT_CODE_LEVEL_5: { + label: "Ad Unit Code Level 5", + value: "AD_UNIT_CODE_LEVEL_5", + }, + AD_UNIT_CODE_LEVEL_6: { + label: "Ad Unit Code Level 6", + value: "AD_UNIT_CODE_LEVEL_6", + }, + AD_UNIT_CODE_LEVEL_7: { + label: "Ad Unit Code Level 7", + value: "AD_UNIT_CODE_LEVEL_7", + }, + AD_UNIT_CODE_LEVEL_8: { + label: "Ad Unit Code Level 8", + value: "AD_UNIT_CODE_LEVEL_8", + }, + AD_UNIT_CODE_LEVEL_9: { + label: "Ad Unit Code Level 9", + value: "AD_UNIT_CODE_LEVEL_9", + }, + AD_UNIT_DEPTH: { + label: "Ad Unit Depth", + value: "AD_UNIT_DEPTH", + }, + AD_UNIT_ID: { + label: "Ad Unit ID", + value: "AD_UNIT_ID", + }, + AD_UNIT_ID_ALL_LEVEL: { + label: "Ad Unit ID All Level", + value: "AD_UNIT_ID_ALL_LEVEL", + }, + AD_UNIT_ID_LEVEL_1: { + label: "Ad Unit ID Level 1", + value: "AD_UNIT_ID_LEVEL_1", + }, + AD_UNIT_ID_LEVEL_10: { + label: "Ad Unit ID Level 10", + value: "AD_UNIT_ID_LEVEL_10", + }, + AD_UNIT_ID_LEVEL_11: { + label: "Ad Unit ID Level 11", + value: "AD_UNIT_ID_LEVEL_11", + }, + AD_UNIT_ID_LEVEL_12: { + label: "Ad Unit ID Level 12", + value: "AD_UNIT_ID_LEVEL_12", + }, + AD_UNIT_ID_LEVEL_13: { + label: "Ad Unit ID Level 13", + value: "AD_UNIT_ID_LEVEL_13", + }, + AD_UNIT_ID_LEVEL_14: { + label: "Ad Unit ID Level 14", + value: "AD_UNIT_ID_LEVEL_14", + }, + AD_UNIT_ID_LEVEL_15: { + label: "Ad Unit ID Level 15", + value: "AD_UNIT_ID_LEVEL_15", + }, + AD_UNIT_ID_LEVEL_16: { + label: "Ad Unit ID Level 16", + value: "AD_UNIT_ID_LEVEL_16", + }, + AD_UNIT_ID_LEVEL_2: { + label: "Ad Unit ID Level 2", + value: "AD_UNIT_ID_LEVEL_2", + }, + AD_UNIT_ID_LEVEL_3: { + label: "Ad Unit ID Level 3", + value: "AD_UNIT_ID_LEVEL_3", + }, + AD_UNIT_ID_LEVEL_4: { + label: "Ad Unit ID Level 4", + value: "AD_UNIT_ID_LEVEL_4", + }, + AD_UNIT_ID_LEVEL_5: { + label: "Ad Unit ID Level 5", + value: "AD_UNIT_ID_LEVEL_5", + }, + AD_UNIT_ID_LEVEL_6: { + label: "Ad Unit ID Level 6", + value: "AD_UNIT_ID_LEVEL_6", + }, + AD_UNIT_ID_LEVEL_7: { + label: "Ad Unit ID Level 7", + value: "AD_UNIT_ID_LEVEL_7", + }, + AD_UNIT_ID_LEVEL_8: { + label: "Ad Unit ID Level 8", + value: "AD_UNIT_ID_LEVEL_8", + }, + AD_UNIT_ID_LEVEL_9: { + label: "Ad Unit ID Level 9", + value: "AD_UNIT_ID_LEVEL_9", + }, + AD_UNIT_ID_TOP_LEVEL: { + label: "Ad Unit ID Top Level", + value: "AD_UNIT_ID_TOP_LEVEL", + }, + AD_UNIT_NAME: { + label: "Ad Unit Name", + value: "AD_UNIT_NAME", + }, + AD_UNIT_NAME_ALL_LEVEL: { + label: "Ad Unit Name All Level", + value: "AD_UNIT_NAME_ALL_LEVEL", + }, + AD_UNIT_NAME_LEVEL_1: { + label: "Ad Unit Name Level 1", + value: "AD_UNIT_NAME_LEVEL_1", + }, + AD_UNIT_NAME_LEVEL_10: { + label: "Ad Unit Name Level 10", + value: "AD_UNIT_NAME_LEVEL_10", + }, + AD_UNIT_NAME_LEVEL_11: { + label: "Ad Unit Name Level 11", + value: "AD_UNIT_NAME_LEVEL_11", + }, + AD_UNIT_NAME_LEVEL_12: { + label: "Ad Unit Name Level 12", + value: "AD_UNIT_NAME_LEVEL_12", + }, + AD_UNIT_NAME_LEVEL_13: { + label: "Ad Unit Name Level 13", + value: "AD_UNIT_NAME_LEVEL_13", + }, + AD_UNIT_NAME_LEVEL_14: { + label: "Ad Unit Name Level 14", + value: "AD_UNIT_NAME_LEVEL_14", + }, + AD_UNIT_NAME_LEVEL_15: { + label: "Ad Unit Name Level 15", + value: "AD_UNIT_NAME_LEVEL_15", + }, + AD_UNIT_NAME_LEVEL_16: { + label: "Ad Unit Name Level 16", + value: "AD_UNIT_NAME_LEVEL_16", + }, + AD_UNIT_NAME_LEVEL_2: { + label: "Ad Unit Name Level 2", + value: "AD_UNIT_NAME_LEVEL_2", + }, + AD_UNIT_NAME_LEVEL_3: { + label: "Ad Unit Name Level 3", + value: "AD_UNIT_NAME_LEVEL_3", + }, + AD_UNIT_NAME_LEVEL_4: { + label: "Ad Unit Name Level 4", + value: "AD_UNIT_NAME_LEVEL_4", + }, + AD_UNIT_NAME_LEVEL_5: { + label: "Ad Unit Name Level 5", + value: "AD_UNIT_NAME_LEVEL_5", + }, + AD_UNIT_NAME_LEVEL_6: { + label: "Ad Unit Name Level 6", + value: "AD_UNIT_NAME_LEVEL_6", + }, + AD_UNIT_NAME_LEVEL_7: { + label: "Ad Unit Name Level 7", + value: "AD_UNIT_NAME_LEVEL_7", + }, + AD_UNIT_NAME_LEVEL_8: { + label: "Ad Unit Name Level 8", + value: "AD_UNIT_NAME_LEVEL_8", + }, + AD_UNIT_NAME_LEVEL_9: { + label: "Ad Unit Name Level 9", + value: "AD_UNIT_NAME_LEVEL_9", + }, + AD_UNIT_NAME_TOP_LEVEL: { + label: "Ad Unit Name Top Level", + value: "AD_UNIT_NAME_TOP_LEVEL", + }, + AD_UNIT_REWARD_AMOUNT: { + label: "Ad Unit Reward Amount", + value: "AD_UNIT_REWARD_AMOUNT", + }, + AD_UNIT_REWARD_TYPE: { + label: "Ad Unit Reward Type", + value: "AD_UNIT_REWARD_TYPE", + }, + AD_UNIT_STATUS: { + label: "Ad Unit Status", + value: "AD_UNIT_STATUS", + }, + AD_UNIT_STATUS_NAME: { + label: "Ad Unit Status Name", + value: "AD_UNIT_STATUS_NAME", + }, + APP_VERSION: { + label: "App Version", + value: "APP_VERSION", + }, + BACKFILL_ADVERTISER_EXTERNAL_ID: { + label: "Backfill Advertiser External ID", + value: "BACKFILL_ADVERTISER_EXTERNAL_ID", + }, + BACKFILL_ADVERTISER_ID: { + label: "Backfill Advertiser ID", + value: "BACKFILL_ADVERTISER_ID", + }, + BACKFILL_ADVERTISER_LABELS: { + label: "Backfill Advertiser Labels", + value: "BACKFILL_ADVERTISER_LABELS", + }, + BACKFILL_ADVERTISER_LABEL_IDS: { + label: "Backfill Advertiser Label IDs", + value: "BACKFILL_ADVERTISER_LABEL_IDS", + }, + BACKFILL_ADVERTISER_NAME: { + label: "Backfill Advertiser Name", + value: "BACKFILL_ADVERTISER_NAME", + }, + BACKFILL_ADVERTISER_PRIMARY_CONTACT: { + label: "Backfill Advertiser Primary Contact", + value: "BACKFILL_ADVERTISER_PRIMARY_CONTACT", + }, + BACKFILL_CREATIVE_BILLING_TYPE: { + label: "Backfill Creative Billing Type", + value: "BACKFILL_CREATIVE_BILLING_TYPE", + }, + BACKFILL_CREATIVE_BILLING_TYPE_NAME: { + label: "Backfill Creative Billing Type Name", + value: "BACKFILL_CREATIVE_BILLING_TYPE_NAME", + }, + BACKFILL_CREATIVE_CLICK_THROUGH_URL: { + label: "Backfill Creative Click Through URL", + value: "BACKFILL_CREATIVE_CLICK_THROUGH_URL", + }, + BACKFILL_CREATIVE_ID: { + label: "Backfill Creative ID", + value: "BACKFILL_CREATIVE_ID", + }, + BACKFILL_CREATIVE_NAME: { + label: "Backfill Creative Name", + value: "BACKFILL_CREATIVE_NAME", + }, + BACKFILL_CREATIVE_THIRD_PARTY_VENDOR: { + label: "Backfill Creative Third Party Vendor", + value: "BACKFILL_CREATIVE_THIRD_PARTY_VENDOR", + }, + BACKFILL_CREATIVE_TYPE: { + label: "Backfill Creative Type", + value: "BACKFILL_CREATIVE_TYPE", + }, + BACKFILL_CREATIVE_TYPE_NAME: { + label: "Backfill Creative Type Name", + value: "BACKFILL_CREATIVE_TYPE_NAME", + }, + BACKFILL_LINE_ITEM_ARCHIVED: { + label: "Backfill Line Item Archived", + value: "BACKFILL_LINE_ITEM_ARCHIVED", + }, + BACKFILL_LINE_ITEM_COMPANION_DELIVERY_OPTION: { + label: "Backfill Line Item Companion Delivery Option", + value: "BACKFILL_LINE_ITEM_COMPANION_DELIVERY_OPTION", + }, + BACKFILL_LINE_ITEM_COMPANION_DELIVERY_OPTION_NAME: { + label: "Backfill Line Item Companion Delivery Option Name", + value: "BACKFILL_LINE_ITEM_COMPANION_DELIVERY_OPTION_NAME", + }, + BACKFILL_LINE_ITEM_COMPUTED_STATUS: { + label: "Backfill Line Item Computed Status", + value: "BACKFILL_LINE_ITEM_COMPUTED_STATUS", + }, + BACKFILL_LINE_ITEM_COMPUTED_STATUS_NAME: { + label: "Backfill Line Item Computed Status Name", + value: "BACKFILL_LINE_ITEM_COMPUTED_STATUS_NAME", + }, + BACKFILL_LINE_ITEM_CONTRACTED_QUANTITY: { + label: "Backfill Line Item Contracted Quantity", + value: "BACKFILL_LINE_ITEM_CONTRACTED_QUANTITY", + }, + BACKFILL_LINE_ITEM_COST_PER_UNIT: { + label: "Backfill Line Item Cost Per Unit", + value: "BACKFILL_LINE_ITEM_COST_PER_UNIT", + }, + BACKFILL_LINE_ITEM_COST_TYPE: { + label: "Backfill Line Item Cost Type", + value: "BACKFILL_LINE_ITEM_COST_TYPE", + }, + BACKFILL_LINE_ITEM_COST_TYPE_NAME: { + label: "Backfill Line Item Cost Type Name", + value: "BACKFILL_LINE_ITEM_COST_TYPE_NAME", + }, + BACKFILL_LINE_ITEM_CREATIVE_END_DATE: { + label: "Backfill Line Item Creative End Date", + value: "BACKFILL_LINE_ITEM_CREATIVE_END_DATE", + }, + BACKFILL_LINE_ITEM_CREATIVE_ROTATION_TYPE: { + label: "Backfill Line Item Creative Rotation Type", + value: "BACKFILL_LINE_ITEM_CREATIVE_ROTATION_TYPE", + }, + BACKFILL_LINE_ITEM_CREATIVE_ROTATION_TYPE_NAME: { + label: "Backfill Line Item Creative Rotation Type Name", + value: "BACKFILL_LINE_ITEM_CREATIVE_ROTATION_TYPE_NAME", + }, + BACKFILL_LINE_ITEM_CREATIVE_START_DATE: { + label: "Backfill Line Item Creative Start Date", + value: "BACKFILL_LINE_ITEM_CREATIVE_START_DATE", + }, + BACKFILL_LINE_ITEM_CURRENCY_CODE: { + label: "Backfill Line Item Currency Code", + value: "BACKFILL_LINE_ITEM_CURRENCY_CODE", + }, + BACKFILL_LINE_ITEM_DELIVERY_INDICATOR: { + label: "Backfill Line Item Delivery Indicator", + value: "BACKFILL_LINE_ITEM_DELIVERY_INDICATOR", + }, + BACKFILL_LINE_ITEM_DELIVERY_RATE_TYPE: { + label: "Backfill Line Item Delivery Rate Type", + value: "BACKFILL_LINE_ITEM_DELIVERY_RATE_TYPE", + }, + BACKFILL_LINE_ITEM_DELIVERY_RATE_TYPE_NAME: { + label: "Backfill Line Item Delivery Rate Type Name", + value: "BACKFILL_LINE_ITEM_DELIVERY_RATE_TYPE_NAME", + }, + BACKFILL_LINE_ITEM_DISCOUNT_ABSOLUTE: { + label: "Backfill Line Item Discount Absolute", + value: "BACKFILL_LINE_ITEM_DISCOUNT_ABSOLUTE", + }, + BACKFILL_LINE_ITEM_DISCOUNT_PERCENTAGE: { + label: "Backfill Line Item Discount Percentage", + value: "BACKFILL_LINE_ITEM_DISCOUNT_PERCENTAGE", + }, + BACKFILL_LINE_ITEM_END_DATE: { + label: "Backfill Line Item End Date", + value: "BACKFILL_LINE_ITEM_END_DATE", + }, + BACKFILL_LINE_ITEM_END_DATE_TIME: { + label: "Backfill Line Item End Date Time", + value: "BACKFILL_LINE_ITEM_END_DATE_TIME", + }, + BACKFILL_LINE_ITEM_ENVIRONMENT_TYPE: { + label: "Backfill Line Item Environment Type", + value: "BACKFILL_LINE_ITEM_ENVIRONMENT_TYPE", + }, + BACKFILL_LINE_ITEM_ENVIRONMENT_TYPE_NAME: { + label: "Backfill Line Item Environment Type Name", + value: "BACKFILL_LINE_ITEM_ENVIRONMENT_TYPE_NAME", + }, + BACKFILL_LINE_ITEM_EXTERNAL_DEAL_ID: { + label: "Backfill Line Item External Deal ID", + value: "BACKFILL_LINE_ITEM_EXTERNAL_DEAL_ID", + }, + BACKFILL_LINE_ITEM_EXTERNAL_ID: { + label: "Backfill Line Item External ID", + value: "BACKFILL_LINE_ITEM_EXTERNAL_ID", + }, + BACKFILL_LINE_ITEM_FREQUENCY_CAP: { + label: "Backfill Line Item Frequency Cap", + value: "BACKFILL_LINE_ITEM_FREQUENCY_CAP", + }, + BACKFILL_LINE_ITEM_ID: { + label: "Backfill Line Item ID", + value: "BACKFILL_LINE_ITEM_ID", + }, + BACKFILL_LINE_ITEM_LAST_MODIFIED_BY_APP: { + label: "Backfill Line Item Last Modified By App", + value: "BACKFILL_LINE_ITEM_LAST_MODIFIED_BY_APP", + }, + BACKFILL_LINE_ITEM_LIFETIME_CLICKS: { + label: "Backfill Line Item Lifetime Clicks", + value: "BACKFILL_LINE_ITEM_LIFETIME_CLICKS", + }, + BACKFILL_LINE_ITEM_LIFETIME_IMPRESSIONS: { + label: "Backfill Line Item Lifetime Impressions", + value: "BACKFILL_LINE_ITEM_LIFETIME_IMPRESSIONS", + }, + BACKFILL_LINE_ITEM_LIFETIME_VIEWABLE_IMPRESSIONS: { + label: "Backfill Line Item Lifetime Viewable Impressions", + value: "BACKFILL_LINE_ITEM_LIFETIME_VIEWABLE_IMPRESSIONS", + }, + BACKFILL_LINE_ITEM_MAKEGOOD: { + label: "Backfill Line Item Makegood", + value: "BACKFILL_LINE_ITEM_MAKEGOOD", + }, + BACKFILL_LINE_ITEM_NAME: { + label: "Backfill Line Item Name", + value: "BACKFILL_LINE_ITEM_NAME", + }, + BACKFILL_LINE_ITEM_NON_CPD_BOOKED_REVENUE: { + label: "Backfill Line Item Non-CPD Booked Revenue", + value: "BACKFILL_LINE_ITEM_NON_CPD_BOOKED_REVENUE", + }, + BACKFILL_LINE_ITEM_OPTIMIZABLE: { + label: "Backfill Line Item Optimizable", + value: "BACKFILL_LINE_ITEM_OPTIMIZABLE", + }, + BACKFILL_LINE_ITEM_PRIMARY_GOAL_TYPE: { + label: "Backfill Line Item Primary Goal Type", + value: "BACKFILL_LINE_ITEM_PRIMARY_GOAL_TYPE", + }, + BACKFILL_LINE_ITEM_PRIMARY_GOAL_TYPE_NAME: { + label: "Backfill Line Item Primary Goal Type Name", + value: "BACKFILL_LINE_ITEM_PRIMARY_GOAL_TYPE_NAME", + }, + BACKFILL_LINE_ITEM_PRIMARY_GOAL_UNIT_TYPE: { + label: "Backfill Line Item Primary Goal Unit Type", + value: "BACKFILL_LINE_ITEM_PRIMARY_GOAL_UNIT_TYPE", + }, + BACKFILL_LINE_ITEM_PRIMARY_GOAL_UNIT_TYPE_NAME: { + label: "Backfill Line Item Primary Goal Unit Type Name", + value: "BACKFILL_LINE_ITEM_PRIMARY_GOAL_UNIT_TYPE_NAME", + }, + BACKFILL_LINE_ITEM_PRIORITY: { + label: "Backfill Line Item Priority", + value: "BACKFILL_LINE_ITEM_PRIORITY", + }, + BACKFILL_LINE_ITEM_RESERVATION_STATUS: { + label: "Backfill Line Item Reservation Status", + value: "BACKFILL_LINE_ITEM_RESERVATION_STATUS", + }, + BACKFILL_LINE_ITEM_RESERVATION_STATUS_NAME: { + label: "Backfill Line Item Reservation Status Name", + value: "BACKFILL_LINE_ITEM_RESERVATION_STATUS_NAME", + }, + BACKFILL_LINE_ITEM_START_DATE: { + label: "Backfill Line Item Start Date", + value: "BACKFILL_LINE_ITEM_START_DATE", + }, + BACKFILL_LINE_ITEM_START_DATE_TIME: { + label: "Backfill Line Item Start Date Time", + value: "BACKFILL_LINE_ITEM_START_DATE_TIME", + }, + BACKFILL_LINE_ITEM_TYPE: { + label: "Backfill Line Item Type", + value: "BACKFILL_LINE_ITEM_TYPE", + }, + BACKFILL_LINE_ITEM_TYPE_NAME: { + label: "Backfill Line Item Type Name", + value: "BACKFILL_LINE_ITEM_TYPE_NAME", + }, + BACKFILL_LINE_ITEM_UNLIMITED_END: { + label: "Backfill Line Item Unlimited End", + value: "BACKFILL_LINE_ITEM_UNLIMITED_END", + }, + BACKFILL_LINE_ITEM_VALUE_COST_PER_UNIT: { + label: "Backfill Line Item Value Cost Per Unit", + value: "BACKFILL_LINE_ITEM_VALUE_COST_PER_UNIT", + }, + BACKFILL_LINE_ITEM_WEB_PROPERTY_CODE: { + label: "Backfill Line Item Web Property Code", + value: "BACKFILL_LINE_ITEM_WEB_PROPERTY_CODE", + }, + BACKFILL_MASTER_COMPANION_CREATIVE_ID: { + label: "Backfill Master Companion Creative ID", + value: "BACKFILL_MASTER_COMPANION_CREATIVE_ID", + }, + BACKFILL_MASTER_COMPANION_CREATIVE_NAME: { + label: "Backfill Master Companion Creative Name", + value: "BACKFILL_MASTER_COMPANION_CREATIVE_NAME", + }, + BACKFILL_ORDER_AGENCY: { + label: "Backfill Order Agency", + value: "BACKFILL_ORDER_AGENCY", + }, + BACKFILL_ORDER_AGENCY_ID: { + label: "Backfill Order Agency ID", + value: "BACKFILL_ORDER_AGENCY_ID", + }, + BACKFILL_ORDER_BOOKED_CPC: { + label: "Backfill Order Booked CPC", + value: "BACKFILL_ORDER_BOOKED_CPC", + }, + BACKFILL_ORDER_BOOKED_CPM: { + label: "Backfill Order Booked CPM", + value: "BACKFILL_ORDER_BOOKED_CPM", + }, + BACKFILL_ORDER_DELIVERY_STATUS: { + label: "Backfill Order Delivery Status", + value: "BACKFILL_ORDER_DELIVERY_STATUS", + }, + BACKFILL_ORDER_DELIVERY_STATUS_NAME: { + label: "Backfill Order Delivery Status Name", + value: "BACKFILL_ORDER_DELIVERY_STATUS_NAME", + }, + BACKFILL_ORDER_END_DATE: { + label: "Backfill Order End Date", + value: "BACKFILL_ORDER_END_DATE", + }, + BACKFILL_ORDER_END_DATE_TIME: { + label: "Backfill Order End Date Time", + value: "BACKFILL_ORDER_END_DATE_TIME", + }, + BACKFILL_ORDER_EXTERNAL_ID: { + label: "Backfill Order External ID", + value: "BACKFILL_ORDER_EXTERNAL_ID", + }, + BACKFILL_ORDER_ID: { + label: "Backfill Order ID", + value: "BACKFILL_ORDER_ID", + }, + BACKFILL_ORDER_LABELS: { + label: "Backfill Order Labels", + value: "BACKFILL_ORDER_LABELS", + }, + BACKFILL_ORDER_LABEL_IDS: { + label: "Backfill Order Label IDs", + value: "BACKFILL_ORDER_LABEL_IDS", + }, + BACKFILL_ORDER_LIFETIME_CLICKS: { + label: "Backfill Order Lifetime Clicks", + value: "BACKFILL_ORDER_LIFETIME_CLICKS", + }, + BACKFILL_ORDER_LIFETIME_IMPRESSIONS: { + label: "Backfill Order Lifetime Impressions", + value: "BACKFILL_ORDER_LIFETIME_IMPRESSIONS", + }, + BACKFILL_ORDER_NAME: { + label: "Backfill Order Name", + value: "BACKFILL_ORDER_NAME", + }, + BACKFILL_ORDER_PO_NUMBER: { + label: "Backfill Order PO Number", + value: "BACKFILL_ORDER_PO_NUMBER", + }, + BACKFILL_ORDER_PROGRAMMATIC: { + label: "Backfill Order Programmatic", + value: "BACKFILL_ORDER_PROGRAMMATIC", + }, + BACKFILL_ORDER_SALESPERSON: { + label: "Backfill Order Salesperson", + value: "BACKFILL_ORDER_SALESPERSON", + }, + BACKFILL_ORDER_SECONDARY_SALESPEOPLE: { + label: "Backfill Order Secondary Salespeople", + value: "BACKFILL_ORDER_SECONDARY_SALESPEOPLE", + }, + BACKFILL_ORDER_SECONDARY_SALESPEOPLE_ID: { + label: "Backfill Order Secondary Salespeople ID", + value: "BACKFILL_ORDER_SECONDARY_SALESPEOPLE_ID", + }, + BACKFILL_ORDER_SECONDARY_TRAFFICKERS: { + label: "Backfill Order Secondary Traffickers", + value: "BACKFILL_ORDER_SECONDARY_TRAFFICKERS", + }, + BACKFILL_ORDER_SECONDARY_TRAFFICKERS_ID: { + label: "Backfill Order Secondary Traffickers ID", + value: "BACKFILL_ORDER_SECONDARY_TRAFFICKERS_ID", + }, + BACKFILL_ORDER_START_DATE: { + label: "Backfill Order Start Date", + value: "BACKFILL_ORDER_START_DATE", + }, + BACKFILL_ORDER_START_DATE_TIME: { + label: "Backfill Order Start Date Time", + value: "BACKFILL_ORDER_START_DATE_TIME", + }, + BACKFILL_ORDER_TRAFFICKER: { + label: "Backfill Order Trafficker", + value: "BACKFILL_ORDER_TRAFFICKER", + }, + BACKFILL_ORDER_TRAFFICKER_ID: { + label: "Backfill Order Trafficker ID", + value: "BACKFILL_ORDER_TRAFFICKER_ID", + }, + BACKFILL_ORDER_UNLIMITED_END: { + label: "Backfill Order Unlimited End", + value: "BACKFILL_ORDER_UNLIMITED_END", + }, + BACKFILL_PROGRAMMATIC_BUYER_ID: { + label: "Backfill Programmatic Buyer ID", + value: "BACKFILL_PROGRAMMATIC_BUYER_ID", + }, + BACKFILL_PROGRAMMATIC_BUYER_NAME: { + label: "Backfill Programmatic Buyer Name", + value: "BACKFILL_PROGRAMMATIC_BUYER_NAME", + }, + BRANDING_TYPE: { + label: "Branding Type", + value: "BRANDING_TYPE", + }, + BRANDING_TYPE_NAME: { + label: "Branding Type Name", + value: "BRANDING_TYPE_NAME", + }, + BROWSER_CATEGORY: { + label: "Browser Category", + value: "BROWSER_CATEGORY", + }, + BROWSER_CATEGORY_NAME: { + label: "Browser Category Name", + value: "BROWSER_CATEGORY_NAME", + }, + BROWSER_ID: { + label: "Browser ID", + value: "BROWSER_ID", + }, + BROWSER_NAME: { + label: "Browser Name", + value: "BROWSER_NAME", + }, + CARRIER_ID: { + label: "Carrier ID", + value: "CARRIER_ID", + }, + CARRIER_NAME: { + label: "Carrier Name", + value: "CARRIER_NAME", + }, + CLASSIFIED_ADVERTISER_ID: { + label: "Classified Advertiser ID", + value: "CLASSIFIED_ADVERTISER_ID", + }, + CLASSIFIED_ADVERTISER_NAME: { + label: "Classified Advertiser Name", + value: "CLASSIFIED_ADVERTISER_NAME", + }, + CLASSIFIED_BRAND_ID: { + label: "Classified Brand ID", + value: "CLASSIFIED_BRAND_ID", + }, + CLASSIFIED_BRAND_NAME: { + label: "Classified Brand Name", + value: "CLASSIFIED_BRAND_NAME", + }, + CONTENT_ID: { + label: "Content ID", + value: "CONTENT_ID", + }, + CONTENT_NAME: { + label: "Content Name", + value: "CONTENT_NAME", + }, + COUNTRY_ID: { + label: "Country ID", + value: "COUNTRY_ID", + }, + COUNTRY_NAME: { + label: "Country Name", + value: "COUNTRY_NAME", + }, + CREATIVE_BILLING_TYPE: { + label: "Creative Billing Type", + value: "CREATIVE_BILLING_TYPE", + }, + CREATIVE_BILLING_TYPE_NAME: { + label: "Creative Billing Type Name", + value: "CREATIVE_BILLING_TYPE_NAME", + }, + CREATIVE_CLICK_THROUGH_URL: { + label: "Creative Click Through URL", + value: "CREATIVE_CLICK_THROUGH_URL", + }, + CREATIVE_ID: { + label: "Creative ID", + value: "CREATIVE_ID", + }, + CREATIVE_NAME: { + label: "Creative Name", + value: "CREATIVE_NAME", + }, + CREATIVE_TECHNOLOGY: { + label: "Creative Technology", + value: "CREATIVE_TECHNOLOGY", + }, + CREATIVE_TECHNOLOGY_NAME: { + label: "Creative Technology Name", + value: "CREATIVE_TECHNOLOGY_NAME", + }, + CREATIVE_THIRD_PARTY_VENDOR: { + label: "Creative Third Party Vendor", + value: "CREATIVE_THIRD_PARTY_VENDOR", + }, + CREATIVE_TYPE: { + label: "Creative Type", + value: "CREATIVE_TYPE", + }, + CREATIVE_TYPE_NAME: { + label: "Creative Type Name", + value: "CREATIVE_TYPE_NAME", + }, + DATE: { + label: "Date", + value: "DATE", + }, + DAY_OF_WEEK: { + label: "Day of Week", + value: "DAY_OF_WEEK", + }, + DEMAND_CHANNEL: { + label: "Demand Channel", + value: "DEMAND_CHANNEL", + }, + DEMAND_CHANNEL_NAME: { + label: "Demand Channel Name", + value: "DEMAND_CHANNEL_NAME", + }, + DEMAND_SUBCHANNEL: { + label: "Demand Subchannel", + value: "DEMAND_SUBCHANNEL", + }, + DEMAND_SUBCHANNEL_NAME: { + label: "Demand Subchannel Name", + value: "DEMAND_SUBCHANNEL_NAME", + }, + DEVICE: { + label: "Device", + value: "DEVICE", + }, + DEVICE_CATEGORY: { + label: "Device Category", + value: "DEVICE_CATEGORY", + }, + DEVICE_CATEGORY_NAME: { + label: "Device Category Name", + value: "DEVICE_CATEGORY_NAME", + }, + DEVICE_NAME: { + label: "Device Name", + value: "DEVICE_NAME", + }, + EXCHANGE_THIRD_PARTY_COMPANY_ID: { + label: "Exchange Third Party Company ID", + value: "EXCHANGE_THIRD_PARTY_COMPANY_ID", + }, + EXCHANGE_THIRD_PARTY_COMPANY_NAME: { + label: "Exchange Third Party Company Name", + value: "EXCHANGE_THIRD_PARTY_COMPANY_NAME", + }, + FIRST_LOOK_PRICING_RULE_ID: { + label: "First Look Pricing Rule ID", + value: "FIRST_LOOK_PRICING_RULE_ID", + }, + FIRST_LOOK_PRICING_RULE_NAME: { + label: "First Look Pricing Rule Name", + value: "FIRST_LOOK_PRICING_RULE_NAME", + }, + HOUR: { + label: "Hour", + value: "HOUR", + }, + INTERACTION_TYPE: { + label: "Interaction Type", + value: "INTERACTION_TYPE", + }, + INTERACTION_TYPE_NAME: { + label: "Interaction Type Name", + value: "INTERACTION_TYPE_NAME", + }, + INVENTORY_FORMAT: { + label: "Inventory Format", + value: "INVENTORY_FORMAT", + }, + INVENTORY_FORMAT_NAME: { + label: "Inventory Format Name", + value: "INVENTORY_FORMAT_NAME", + }, + INVENTORY_TYPE: { + label: "Inventory Type", + value: "INVENTORY_TYPE", + }, + INVENTORY_TYPE_NAME: { + label: "Inventory Type Name", + value: "INVENTORY_TYPE_NAME", + }, + IS_ADX_DIRECT: { + label: "Is AdX Direct", + value: "IS_ADX_DIRECT", + }, + IS_FIRST_LOOK_DEAL: { + label: "Is First Look Deal", + value: "IS_FIRST_LOOK_DEAL", + }, + KEY_VALUES_ID: { + label: "Key Values ID", + value: "KEY_VALUES_ID", + }, + KEY_VALUES_NAME: { + label: "Key Values Name", + value: "KEY_VALUES_NAME", + }, + LINE_ITEM_ARCHIVED: { + label: "Line Item Archived", + value: "LINE_ITEM_ARCHIVED", + }, + LINE_ITEM_COMPANION_DELIVERY_OPTION: { + label: "Line Item Companion Delivery Option", + value: "LINE_ITEM_COMPANION_DELIVERY_OPTION", + }, + LINE_ITEM_COMPANION_DELIVERY_OPTION_NAME: { + label: "Line Item Companion Delivery Option Name", + value: "LINE_ITEM_COMPANION_DELIVERY_OPTION_NAME", + }, + LINE_ITEM_COMPUTED_STATUS: { + label: "Line Item Computed Status", + value: "LINE_ITEM_COMPUTED_STATUS", + }, + LINE_ITEM_COMPUTED_STATUS_NAME: { + label: "Line Item Computed Status Name", + value: "LINE_ITEM_COMPUTED_STATUS_NAME", + }, + LINE_ITEM_CONTRACTED_QUANTITY: { + label: "Line Item Contracted Quantity", + value: "LINE_ITEM_CONTRACTED_QUANTITY", + }, + LINE_ITEM_COST_PER_UNIT: { + label: "Line Item Cost Per Unit", + value: "LINE_ITEM_COST_PER_UNIT", + }, + LINE_ITEM_COST_TYPE: { + label: "Line Item Cost Type", + value: "LINE_ITEM_COST_TYPE", + }, + LINE_ITEM_COST_TYPE_NAME: { + label: "Line Item Cost Type Name", + value: "LINE_ITEM_COST_TYPE_NAME", + }, + LINE_ITEM_CREATIVE_END_DATE: { + label: "Line Item Creative End Date", + value: "LINE_ITEM_CREATIVE_END_DATE", + }, + LINE_ITEM_CREATIVE_ROTATION_TYPE: { + label: "Line Item Creative Rotation Type", + value: "LINE_ITEM_CREATIVE_ROTATION_TYPE", + }, + LINE_ITEM_CREATIVE_ROTATION_TYPE_NAME: { + label: "Line Item Creative Rotation Type Name", + value: "LINE_ITEM_CREATIVE_ROTATION_TYPE_NAME", + }, + LINE_ITEM_CREATIVE_START_DATE: { + label: "Line Item Creative Start Date", + value: "LINE_ITEM_CREATIVE_START_DATE", + }, + LINE_ITEM_CURRENCY_CODE: { + label: "Line Item Currency Code", + value: "LINE_ITEM_CURRENCY_CODE", + }, + LINE_ITEM_DELIVERY_INDICATOR: { + label: "Line Item Delivery Indicator", + value: "LINE_ITEM_DELIVERY_INDICATOR", + }, + LINE_ITEM_DELIVERY_RATE_TYPE: { + label: "Line Item Delivery Rate Type", + value: "LINE_ITEM_DELIVERY_RATE_TYPE", + }, + LINE_ITEM_DELIVERY_RATE_TYPE_NAME: { + label: "Line Item Delivery Rate Type Name", + value: "LINE_ITEM_DELIVERY_RATE_TYPE_NAME", + }, + LINE_ITEM_DISCOUNT_ABSOLUTE: { + label: "Line Item Discount Absolute", + value: "LINE_ITEM_DISCOUNT_ABSOLUTE", + }, + LINE_ITEM_DISCOUNT_PERCENTAGE: { + label: "Line Item Discount Percentage", + value: "LINE_ITEM_DISCOUNT_PERCENTAGE", + }, + LINE_ITEM_END_DATE: { + label: "Line Item End Date", + value: "LINE_ITEM_END_DATE", + }, + LINE_ITEM_END_DATE_TIME: { + label: "Line Item End Date Time", + value: "LINE_ITEM_END_DATE_TIME", + }, + LINE_ITEM_ENVIRONMENT_TYPE: { + label: "Line Item Environment Type", + value: "LINE_ITEM_ENVIRONMENT_TYPE", + }, + LINE_ITEM_ENVIRONMENT_TYPE_NAME: { + label: "Line Item Environment Type Name", + value: "LINE_ITEM_ENVIRONMENT_TYPE_NAME", + }, + LINE_ITEM_EXTERNAL_DEAL_ID: { + label: "Line Item External Deal ID", + value: "LINE_ITEM_EXTERNAL_DEAL_ID", + }, + LINE_ITEM_EXTERNAL_ID: { + label: "Line Item External ID", + value: "LINE_ITEM_EXTERNAL_ID", + }, + LINE_ITEM_FREQUENCY_CAP: { + label: "Line Item Frequency Cap", + value: "LINE_ITEM_FREQUENCY_CAP", + }, + LINE_ITEM_ID: { + label: "Line Item ID", + value: "LINE_ITEM_ID", + }, + LINE_ITEM_LAST_MODIFIED_BY_APP: { + label: "Line Item Last Modified By App", + value: "LINE_ITEM_LAST_MODIFIED_BY_APP", + }, + LINE_ITEM_LIFETIME_CLICKS: { + label: "Line Item Lifetime Clicks", + value: "LINE_ITEM_LIFETIME_CLICKS", + }, + LINE_ITEM_LIFETIME_IMPRESSIONS: { + label: "Line Item Lifetime Impressions", + value: "LINE_ITEM_LIFETIME_IMPRESSIONS", + }, + LINE_ITEM_LIFETIME_VIEWABLE_IMPRESSIONS: { + label: "Line Item Lifetime Viewable Impressions", + value: "LINE_ITEM_LIFETIME_VIEWABLE_IMPRESSIONS", + }, + LINE_ITEM_MAKEGOOD: { + label: "Line Item Makegood", + value: "LINE_ITEM_MAKEGOOD", + }, + LINE_ITEM_NAME: { + label: "Line Item Name", + value: "LINE_ITEM_NAME", + }, + LINE_ITEM_NON_CPD_BOOKED_REVENUE: { + label: "Line Item Non-CPD Booked Revenue", + value: "LINE_ITEM_NON_CPD_BOOKED_REVENUE", + }, + LINE_ITEM_OPTIMIZABLE: { + label: "Line Item Optimizable", + value: "LINE_ITEM_OPTIMIZABLE", + }, + LINE_ITEM_PRIMARY_GOAL_TYPE: { + label: "Line Item Primary Goal Type", + value: "LINE_ITEM_PRIMARY_GOAL_TYPE", + }, + LINE_ITEM_PRIMARY_GOAL_TYPE_NAME: { + label: "Line Item Primary Goal Type Name", + value: "LINE_ITEM_PRIMARY_GOAL_TYPE_NAME", + }, + LINE_ITEM_PRIMARY_GOAL_UNITS_ABSOLUTE: { + label: "Line Item Primary Goal Units Absolute", + value: "LINE_ITEM_PRIMARY_GOAL_UNITS_ABSOLUTE", + }, + LINE_ITEM_PRIMARY_GOAL_UNITS_PERCENTAGE: { + label: "Line Item Primary Goal Units Percentage", + value: "LINE_ITEM_PRIMARY_GOAL_UNITS_PERCENTAGE", + }, + LINE_ITEM_PRIMARY_GOAL_UNIT_TYPE: { + label: "Line Item Primary Goal Unit Type", + value: "LINE_ITEM_PRIMARY_GOAL_UNIT_TYPE", + }, + LINE_ITEM_PRIMARY_GOAL_UNIT_TYPE_NAME: { + label: "Line Item Primary Goal Unit Type Name", + value: "LINE_ITEM_PRIMARY_GOAL_UNIT_TYPE_NAME", + }, + LINE_ITEM_PRIORITY: { + label: "Line Item Priority", + value: "LINE_ITEM_PRIORITY", + }, + LINE_ITEM_RESERVATION_STATUS: { + label: "Line Item Reservation Status", + value: "LINE_ITEM_RESERVATION_STATUS", + }, + LINE_ITEM_RESERVATION_STATUS_NAME: { + label: "Line Item Reservation Status Name", + value: "LINE_ITEM_RESERVATION_STATUS_NAME", + }, + LINE_ITEM_START_DATE: { + label: "Line Item Start Date", + value: "LINE_ITEM_START_DATE", + }, + LINE_ITEM_START_DATE_TIME: { + label: "Line Item Start Date Time", + value: "LINE_ITEM_START_DATE_TIME", + }, + LINE_ITEM_TYPE: { + label: "Line Item Type", + value: "LINE_ITEM_TYPE", + }, + LINE_ITEM_TYPE_NAME: { + label: "Line Item Type Name", + value: "LINE_ITEM_TYPE_NAME", + }, + LINE_ITEM_UNLIMITED_END: { + label: "Line Item Unlimited End", + value: "LINE_ITEM_UNLIMITED_END", + }, + LINE_ITEM_VALUE_COST_PER_UNIT: { + label: "Line Item Value Cost Per Unit", + value: "LINE_ITEM_VALUE_COST_PER_UNIT", + }, + LINE_ITEM_WEB_PROPERTY_CODE: { + label: "Line Item Web Property Code", + value: "LINE_ITEM_WEB_PROPERTY_CODE", + }, + MASTER_COMPANION_CREATIVE_ID: { + label: "Master Companion Creative ID", + value: "MASTER_COMPANION_CREATIVE_ID", + }, + MASTER_COMPANION_CREATIVE_NAME: { + label: "Master Companion Creative Name", + value: "MASTER_COMPANION_CREATIVE_NAME", + }, + MOBILE_APP_FREE: { + label: "Mobile App Free", + value: "MOBILE_APP_FREE", + }, + MOBILE_APP_ICON_URL: { + label: "Mobile App Icon URL", + value: "MOBILE_APP_ICON_URL", + }, + MOBILE_APP_ID: { + label: "Mobile App ID", + value: "MOBILE_APP_ID", + }, + MOBILE_APP_NAME: { + label: "Mobile App Name", + value: "MOBILE_APP_NAME", + }, + MOBILE_APP_OWNERSHIP_STATUS: { + label: "Mobile App Ownership Status", + value: "MOBILE_APP_OWNERSHIP_STATUS", + }, + MOBILE_APP_OWNERSHIP_STATUS_NAME: { + label: "Mobile App Ownership Status Name", + value: "MOBILE_APP_OWNERSHIP_STATUS_NAME", + }, + MOBILE_APP_STORE: { + label: "Mobile App Store", + value: "MOBILE_APP_STORE", + }, + MOBILE_APP_STORE_NAME: { + label: "Mobile App Store Name", + value: "MOBILE_APP_STORE_NAME", + }, + MOBILE_INVENTORY_TYPE: { + label: "Mobile Inventory Type", + value: "MOBILE_INVENTORY_TYPE", + }, + MOBILE_INVENTORY_TYPE_NAME: { + label: "Mobile Inventory Type Name", + value: "MOBILE_INVENTORY_TYPE_NAME", + }, + MOBILE_SDK_VERSION_NAME: { + label: "Mobile SDK Version Name", + value: "MOBILE_SDK_VERSION_NAME", + }, + MONTH_YEAR: { + label: "Month Year", + value: "MONTH_YEAR", + }, + NATIVE_AD_FORMAT_ID: { + label: "Native Ad Format ID", + value: "NATIVE_AD_FORMAT_ID", + }, + NATIVE_AD_FORMAT_NAME: { + label: "Native Ad Format Name", + value: "NATIVE_AD_FORMAT_NAME", + }, + NATIVE_STYLE_ID: { + label: "Native Style ID", + value: "NATIVE_STYLE_ID", + }, + NATIVE_STYLE_NAME: { + label: "Native Style Name", + value: "NATIVE_STYLE_NAME", + }, + OPERATING_SYSTEM_CATEGORY: { + label: "Operating System Category", + value: "OPERATING_SYSTEM_CATEGORY", + }, + OPERATING_SYSTEM_CATEGORY_NAME: { + label: "Operating System Category Name", + value: "OPERATING_SYSTEM_CATEGORY_NAME", + }, + OPERATING_SYSTEM_VERSION_ID: { + label: "Operating System Version ID", + value: "OPERATING_SYSTEM_VERSION_ID", + }, + OPERATING_SYSTEM_VERSION_NAME: { + label: "Operating System Version Name", + value: "OPERATING_SYSTEM_VERSION_NAME", + }, + ORDER_AGENCY: { + label: "Order Agency", + value: "ORDER_AGENCY", + }, + ORDER_AGENCY_ID: { + label: "Order Agency ID", + value: "ORDER_AGENCY_ID", + }, + ORDER_BOOKED_CPC: { + label: "Order Booked CPC", + value: "ORDER_BOOKED_CPC", + }, + ORDER_BOOKED_CPM: { + label: "Order Booked CPM", + value: "ORDER_BOOKED_CPM", + }, + ORDER_DELIVERY_STATUS: { + label: "Order Delivery Status", + value: "ORDER_DELIVERY_STATUS", + }, + ORDER_DELIVERY_STATUS_NAME: { + label: "Order Delivery Status Name", + value: "ORDER_DELIVERY_STATUS_NAME", + }, + ORDER_END_DATE: { + label: "Order End Date", + value: "ORDER_END_DATE", + }, + ORDER_END_DATE_TIME: { + label: "Order End Date Time", + value: "ORDER_END_DATE_TIME", + }, + ORDER_EXTERNAL_ID: { + label: "Order External ID", + value: "ORDER_EXTERNAL_ID", + }, + ORDER_ID: { + label: "Order ID", + value: "ORDER_ID", + }, + ORDER_LABELS: { + label: "Order Labels", + value: "ORDER_LABELS", + }, + ORDER_LABEL_IDS: { + label: "Order Label IDs", + value: "ORDER_LABEL_IDS", + }, + ORDER_LIFETIME_CLICKS: { + label: "Order Lifetime Clicks", + value: "ORDER_LIFETIME_CLICKS", + }, + ORDER_LIFETIME_IMPRESSIONS: { + label: "Order Lifetime Impressions", + value: "ORDER_LIFETIME_IMPRESSIONS", + }, + ORDER_NAME: { + label: "Order Name", + value: "ORDER_NAME", + }, + ORDER_PO_NUMBER: { + label: "Order PO Number", + value: "ORDER_PO_NUMBER", + }, + ORDER_PROGRAMMATIC: { + label: "Order Programmatic", + value: "ORDER_PROGRAMMATIC", + }, + ORDER_SALESPERSON: { + label: "Order Salesperson", + value: "ORDER_SALESPERSON", + }, + ORDER_SECONDARY_SALESPEOPLE: { + label: "Order Secondary Salespeople", + value: "ORDER_SECONDARY_SALESPEOPLE", + }, + ORDER_SECONDARY_SALESPEOPLE_ID: { + label: "Order Secondary Salespeople ID", + value: "ORDER_SECONDARY_SALESPEOPLE_ID", + }, + ORDER_SECONDARY_TRAFFICKERS: { + label: "Order Secondary Traffickers", + value: "ORDER_SECONDARY_TRAFFICKERS", + }, + ORDER_SECONDARY_TRAFFICKERS_ID: { + label: "Order Secondary Traffickers ID", + value: "ORDER_SECONDARY_TRAFFICKERS_ID", + }, + ORDER_START_DATE: { + label: "Order Start Date", + value: "ORDER_START_DATE", + }, + ORDER_START_DATE_TIME: { + label: "Order Start Date Time", + value: "ORDER_START_DATE_TIME", + }, + ORDER_TRAFFICKER: { + label: "Order Trafficker", + value: "ORDER_TRAFFICKER", + }, + ORDER_TRAFFICKER_ID: { + label: "Order Trafficker ID", + value: "ORDER_TRAFFICKER_ID", + }, + ORDER_UNLIMITED_END: { + label: "Order Unlimited End", + value: "ORDER_UNLIMITED_END", + }, + PLACEMENT_ID: { + label: "Placement ID", + value: "PLACEMENT_ID", + }, + PLACEMENT_ID_ALL: { + label: "Placement ID All", + value: "PLACEMENT_ID_ALL", + }, + PLACEMENT_NAME: { + label: "Placement Name", + value: "PLACEMENT_NAME", + }, + PLACEMENT_NAME_ALL: { + label: "Placement Name All", + value: "PLACEMENT_NAME_ALL", + }, + PLACEMENT_STATUS: { + label: "Placement Status", + value: "PLACEMENT_STATUS", + }, + PLACEMENT_STATUS_ALL: { + label: "Placement Status All", + value: "PLACEMENT_STATUS_ALL", + }, + PLACEMENT_STATUS_NAME: { + label: "Placement Status Name", + value: "PLACEMENT_STATUS_NAME", + }, + PLACEMENT_STATUS_NAME_ALL: { + label: "Placement Status Name All", + value: "PLACEMENT_STATUS_NAME_ALL", + }, + PROGRAMMATIC_BUYER_ID: { + label: "Programmatic Buyer ID", + value: "PROGRAMMATIC_BUYER_ID", + }, + PROGRAMMATIC_BUYER_NAME: { + label: "Programmatic Buyer Name", + value: "PROGRAMMATIC_BUYER_NAME", + }, + PROGRAMMATIC_CHANNEL: { + label: "Programmatic Channel", + value: "PROGRAMMATIC_CHANNEL", + }, + PROGRAMMATIC_CHANNEL_NAME: { + label: "Programmatic Channel Name", + value: "PROGRAMMATIC_CHANNEL_NAME", + }, + RENDERED_CREATIVE_SIZE: { + label: "Rendered Creative Size", + value: "RENDERED_CREATIVE_SIZE", + }, + REQUESTED_AD_SIZES: { + label: "Requested Ad Sizes", + value: "REQUESTED_AD_SIZES", + }, + REQUEST_TYPE: { + label: "Request Type", + value: "REQUEST_TYPE", + }, + REQUEST_TYPE_NAME: { + label: "Request Type Name", + value: "REQUEST_TYPE_NAME", + }, + SITE: { + label: "Site", + value: "SITE", + }, + TARGETING_ID: { + label: "Targeting ID", + value: "TARGETING_ID", + }, + TARGETING_NAME: { + label: "Targeting Name", + value: "TARGETING_NAME", + }, + TARGETING_TYPE: { + label: "Targeting Type", + value: "TARGETING_TYPE", + }, + TARGETING_TYPE_NAME: { + label: "Targeting Type Name", + value: "TARGETING_TYPE_NAME", + }, + TRAFFIC_SOURCE: { + label: "Traffic Source", + value: "TRAFFIC_SOURCE", + }, + TRAFFIC_SOURCE_NAME: { + label: "Traffic Source Name", + value: "TRAFFIC_SOURCE_NAME", + }, + UNIFIED_PRICING_RULE_ID: { + label: "Unified Pricing Rule ID", + value: "UNIFIED_PRICING_RULE_ID", + }, + UNIFIED_PRICING_RULE_NAME: { + label: "Unified Pricing Rule Name", + value: "UNIFIED_PRICING_RULE_NAME", + }, + VIDEO_PLCMT: { + label: "Video Placement", + value: "VIDEO_PLCMT", + }, + VIDEO_PLCMT_NAME: { + label: "Video Placement Name", + value: "VIDEO_PLCMT_NAME", + }, + WEEK: { + label: "Week", + value: "WEEK", + }, + YIELD_GROUP_BUYER_NAME: { + label: "Yield Group Buyer Name", + value: "YIELD_GROUP_BUYER_NAME", + }, + YIELD_GROUP_ID: { + label: "Yield Group ID", + value: "YIELD_GROUP_ID", + }, + YIELD_GROUP_NAME: { + label: "Yield Group Name", + value: "YIELD_GROUP_NAME", + }, + LINE_ITEM_CUSTOM_FIELD_0_OPTION_ID: { + label: "Line Item Custom Field 0 Option ID", + value: "LINE_ITEM_CUSTOM_FIELD_0_OPTION_ID", + }, + LINE_ITEM_CUSTOM_FIELD_1_OPTION_ID: { + label: "Line Item Custom Field 1 Option ID", + value: "LINE_ITEM_CUSTOM_FIELD_1_OPTION_ID", + }, + LINE_ITEM_CUSTOM_FIELD_2_OPTION_ID: { + label: "Line Item Custom Field 2 Option ID", + value: "LINE_ITEM_CUSTOM_FIELD_2_OPTION_ID", + }, + LINE_ITEM_CUSTOM_FIELD_3_OPTION_ID: { + label: "Line Item Custom Field 3 Option ID", + value: "LINE_ITEM_CUSTOM_FIELD_3_OPTION_ID", + }, + LINE_ITEM_CUSTOM_FIELD_4_OPTION_ID: { + label: "Line Item Custom Field 4 Option ID", + value: "LINE_ITEM_CUSTOM_FIELD_4_OPTION_ID", + }, + LINE_ITEM_CUSTOM_FIELD_5_OPTION_ID: { + label: "Line Item Custom Field 5 Option ID", + value: "LINE_ITEM_CUSTOM_FIELD_5_OPTION_ID", + }, + LINE_ITEM_CUSTOM_FIELD_6_OPTION_ID: { + label: "Line Item Custom Field 6 Option ID", + value: "LINE_ITEM_CUSTOM_FIELD_6_OPTION_ID", + }, + LINE_ITEM_CUSTOM_FIELD_7_OPTION_ID: { + label: "Line Item Custom Field 7 Option ID", + value: "LINE_ITEM_CUSTOM_FIELD_7_OPTION_ID", + }, + LINE_ITEM_CUSTOM_FIELD_8_OPTION_ID: { + label: "Line Item Custom Field 8 Option ID", + value: "LINE_ITEM_CUSTOM_FIELD_8_OPTION_ID", + }, + LINE_ITEM_CUSTOM_FIELD_9_OPTION_ID: { + label: "Line Item Custom Field 9 Option ID", + value: "LINE_ITEM_CUSTOM_FIELD_9_OPTION_ID", + }, + LINE_ITEM_CUSTOM_FIELD_10_OPTION_ID: { + label: "Line Item Custom Field 10 Option ID", + value: "LINE_ITEM_CUSTOM_FIELD_10_OPTION_ID", + }, + LINE_ITEM_CUSTOM_FIELD_11_OPTION_ID: { + label: "Line Item Custom Field 11 Option ID", + value: "LINE_ITEM_CUSTOM_FIELD_11_OPTION_ID", + }, + LINE_ITEM_CUSTOM_FIELD_12_OPTION_ID: { + label: "Line Item Custom Field 12 Option ID", + value: "LINE_ITEM_CUSTOM_FIELD_12_OPTION_ID", + }, + LINE_ITEM_CUSTOM_FIELD_13_OPTION_ID: { + label: "Line Item Custom Field 13 Option ID", + value: "LINE_ITEM_CUSTOM_FIELD_13_OPTION_ID", + }, + LINE_ITEM_CUSTOM_FIELD_14_OPTION_ID: { + label: "Line Item Custom Field 14 Option ID", + value: "LINE_ITEM_CUSTOM_FIELD_14_OPTION_ID", + }, + LINE_ITEM_CUSTOM_FIELD_0_VALUE: { + label: "Line Item Custom Field 0 Value", + value: "LINE_ITEM_CUSTOM_FIELD_0_VALUE", + }, + LINE_ITEM_CUSTOM_FIELD_1_VALUE: { + label: "Line Item Custom Field 1 Value", + value: "LINE_ITEM_CUSTOM_FIELD_1_VALUE", + }, + LINE_ITEM_CUSTOM_FIELD_2_VALUE: { + label: "Line Item Custom Field 2 Value", + value: "LINE_ITEM_CUSTOM_FIELD_2_VALUE", + }, + LINE_ITEM_CUSTOM_FIELD_3_VALUE: { + label: "Line Item Custom Field 3 Value", + value: "LINE_ITEM_CUSTOM_FIELD_3_VALUE", + }, + LINE_ITEM_CUSTOM_FIELD_4_VALUE: { + label: "Line Item Custom Field 4 Value", + value: "LINE_ITEM_CUSTOM_FIELD_4_VALUE", + }, + LINE_ITEM_CUSTOM_FIELD_5_VALUE: { + label: "Line Item Custom Field 5 Value", + value: "LINE_ITEM_CUSTOM_FIELD_5_VALUE", + }, + LINE_ITEM_CUSTOM_FIELD_6_VALUE: { + label: "Line Item Custom Field 6 Value", + value: "LINE_ITEM_CUSTOM_FIELD_6_VALUE", + }, + LINE_ITEM_CUSTOM_FIELD_7_VALUE: { + label: "Line Item Custom Field 7 Value", + value: "LINE_ITEM_CUSTOM_FIELD_7_VALUE", + }, + LINE_ITEM_CUSTOM_FIELD_8_VALUE: { + label: "Line Item Custom Field 8 Value", + value: "LINE_ITEM_CUSTOM_FIELD_8_VALUE", + }, + LINE_ITEM_CUSTOM_FIELD_9_VALUE: { + label: "Line Item Custom Field 9 Value", + value: "LINE_ITEM_CUSTOM_FIELD_9_VALUE", + }, + LINE_ITEM_CUSTOM_FIELD_10_VALUE: { + label: "Line Item Custom Field 10 Value", + value: "LINE_ITEM_CUSTOM_FIELD_10_VALUE", + }, + LINE_ITEM_CUSTOM_FIELD_11_VALUE: { + label: "Line Item Custom Field 11 Value", + value: "LINE_ITEM_CUSTOM_FIELD_11_VALUE", + }, + LINE_ITEM_CUSTOM_FIELD_12_VALUE: { + label: "Line Item Custom Field 12 Value", + value: "LINE_ITEM_CUSTOM_FIELD_12_VALUE", + }, + LINE_ITEM_CUSTOM_FIELD_13_VALUE: { + label: "Line Item Custom Field 13 Value", + value: "LINE_ITEM_CUSTOM_FIELD_13_VALUE", + }, + LINE_ITEM_CUSTOM_FIELD_14_VALUE: { + label: "Line Item Custom Field 14 Value", + value: "LINE_ITEM_CUSTOM_FIELD_14_VALUE", + }, + ORDER_CUSTOM_FIELD_0_OPTION_ID: { + label: "Order Custom Field 0 Option ID", + value: "ORDER_CUSTOM_FIELD_0_OPTION_ID", + }, + ORDER_CUSTOM_FIELD_1_OPTION_ID: { + label: "Order Custom Field 1 Option ID", + value: "ORDER_CUSTOM_FIELD_1_OPTION_ID", + }, + ORDER_CUSTOM_FIELD_2_OPTION_ID: { + label: "Order Custom Field 2 Option ID", + value: "ORDER_CUSTOM_FIELD_2_OPTION_ID", + }, + ORDER_CUSTOM_FIELD_3_OPTION_ID: { + label: "Order Custom Field 3 Option ID", + value: "ORDER_CUSTOM_FIELD_3_OPTION_ID", + }, + ORDER_CUSTOM_FIELD_4_OPTION_ID: { + label: "Order Custom Field 4 Option ID", + value: "ORDER_CUSTOM_FIELD_4_OPTION_ID", + }, + ORDER_CUSTOM_FIELD_5_OPTION_ID: { + label: "Order Custom Field 5 Option ID", + value: "ORDER_CUSTOM_FIELD_5_OPTION_ID", + }, + ORDER_CUSTOM_FIELD_6_OPTION_ID: { + label: "Order Custom Field 6 Option ID", + value: "ORDER_CUSTOM_FIELD_6_OPTION_ID", + }, + ORDER_CUSTOM_FIELD_7_OPTION_ID: { + label: "Order Custom Field 7 Option ID", + value: "ORDER_CUSTOM_FIELD_7_OPTION_ID", + }, + ORDER_CUSTOM_FIELD_8_OPTION_ID: { + label: "Order Custom Field 8 Option ID", + value: "ORDER_CUSTOM_FIELD_8_OPTION_ID", + }, + ORDER_CUSTOM_FIELD_9_OPTION_ID: { + label: "Order Custom Field 9 Option ID", + value: "ORDER_CUSTOM_FIELD_9_OPTION_ID", + }, + ORDER_CUSTOM_FIELD_10_OPTION_ID: { + label: "Order Custom Field 10 Option ID", + value: "ORDER_CUSTOM_FIELD_10_OPTION_ID", + }, + ORDER_CUSTOM_FIELD_11_OPTION_ID: { + label: "Order Custom Field 11 Option ID", + value: "ORDER_CUSTOM_FIELD_11_OPTION_ID", + }, + ORDER_CUSTOM_FIELD_12_OPTION_ID: { + label: "Order Custom Field 12 Option ID", + value: "ORDER_CUSTOM_FIELD_12_OPTION_ID", + }, + ORDER_CUSTOM_FIELD_13_OPTION_ID: { + label: "Order Custom Field 13 Option ID", + value: "ORDER_CUSTOM_FIELD_13_OPTION_ID", + }, + ORDER_CUSTOM_FIELD_14_OPTION_ID: { + label: "Order Custom Field 14 Option ID", + value: "ORDER_CUSTOM_FIELD_14_OPTION_ID", + }, + ORDER_CUSTOM_FIELD_0_VALUE: { + label: "Order Custom Field 0 Value", + value: "ORDER_CUSTOM_FIELD_0_VALUE", + }, + ORDER_CUSTOM_FIELD_1_VALUE: { + label: "Order Custom Field 1 Value", + value: "ORDER_CUSTOM_FIELD_1_VALUE", + }, + ORDER_CUSTOM_FIELD_2_VALUE: { + label: "Order Custom Field 2 Value", + value: "ORDER_CUSTOM_FIELD_2_VALUE", + }, + ORDER_CUSTOM_FIELD_3_VALUE: { + label: "Order Custom Field 3 Value", + value: "ORDER_CUSTOM_FIELD_3_VALUE", + }, + ORDER_CUSTOM_FIELD_4_VALUE: { + label: "Order Custom Field 4 Value", + value: "ORDER_CUSTOM_FIELD_4_VALUE", + }, + ORDER_CUSTOM_FIELD_5_VALUE: { + label: "Order Custom Field 5 Value", + value: "ORDER_CUSTOM_FIELD_5_VALUE", + }, + ORDER_CUSTOM_FIELD_6_VALUE: { + label: "Order Custom Field 6 Value", + value: "ORDER_CUSTOM_FIELD_6_VALUE", + }, + ORDER_CUSTOM_FIELD_7_VALUE: { + label: "Order Custom Field 7 Value", + value: "ORDER_CUSTOM_FIELD_7_VALUE", + }, + ORDER_CUSTOM_FIELD_8_VALUE: { + label: "Order Custom Field 8 Value", + value: "ORDER_CUSTOM_FIELD_8_VALUE", + }, + ORDER_CUSTOM_FIELD_9_VALUE: { + label: "Order Custom Field 9 Value", + value: "ORDER_CUSTOM_FIELD_9_VALUE", + }, + ORDER_CUSTOM_FIELD_10_VALUE: { + label: "Order Custom Field 10 Value", + value: "ORDER_CUSTOM_FIELD_10_VALUE", + }, + ORDER_CUSTOM_FIELD_11_VALUE: { + label: "Order Custom Field 11 Value", + value: "ORDER_CUSTOM_FIELD_11_VALUE", + }, + ORDER_CUSTOM_FIELD_12_VALUE: { + label: "Order Custom Field 12 Value", + value: "ORDER_CUSTOM_FIELD_12_VALUE", + }, + ORDER_CUSTOM_FIELD_13_VALUE: { + label: "Order Custom Field 13 Value", + value: "ORDER_CUSTOM_FIELD_13_VALUE", + }, + ORDER_CUSTOM_FIELD_14_VALUE: { + label: "Order Custom Field 14 Value", + value: "ORDER_CUSTOM_FIELD_14_VALUE", + }, + CREATIVE_CUSTOM_FIELD_0_OPTION_ID: { + label: "Creative Custom Field 0 Option ID", + value: "CREATIVE_CUSTOM_FIELD_0_OPTION_ID", + }, + CREATIVE_CUSTOM_FIELD_1_OPTION_ID: { + label: "Creative Custom Field 1 Option ID", + value: "CREATIVE_CUSTOM_FIELD_1_OPTION_ID", + }, + CREATIVE_CUSTOM_FIELD_2_OPTION_ID: { + label: "Creative Custom Field 2 Option ID", + value: "CREATIVE_CUSTOM_FIELD_2_OPTION_ID", + }, + CREATIVE_CUSTOM_FIELD_3_OPTION_ID: { + label: "Creative Custom Field 3 Option ID", + value: "CREATIVE_CUSTOM_FIELD_3_OPTION_ID", + }, + CREATIVE_CUSTOM_FIELD_4_OPTION_ID: { + label: "Creative Custom Field 4 Option ID", + value: "CREATIVE_CUSTOM_FIELD_4_OPTION_ID", + }, + CREATIVE_CUSTOM_FIELD_5_OPTION_ID: { + label: "Creative Custom Field 5 Option ID", + value: "CREATIVE_CUSTOM_FIELD_5_OPTION_ID", + }, + CREATIVE_CUSTOM_FIELD_6_OPTION_ID: { + label: "Creative Custom Field 6 Option ID", + value: "CREATIVE_CUSTOM_FIELD_6_OPTION_ID", + }, + CREATIVE_CUSTOM_FIELD_7_OPTION_ID: { + label: "Creative Custom Field 7 Option ID", + value: "CREATIVE_CUSTOM_FIELD_7_OPTION_ID", + }, + CREATIVE_CUSTOM_FIELD_8_OPTION_ID: { + label: "Creative Custom Field 8 Option ID", + value: "CREATIVE_CUSTOM_FIELD_8_OPTION_ID", + }, + CREATIVE_CUSTOM_FIELD_9_OPTION_ID: { + label: "Creative Custom Field 9 Option ID", + value: "CREATIVE_CUSTOM_FIELD_9_OPTION_ID", + }, + CREATIVE_CUSTOM_FIELD_10_OPTION_ID: { + label: "Creative Custom Field 10 Option ID", + value: "CREATIVE_CUSTOM_FIELD_10_OPTION_ID", + }, + CREATIVE_CUSTOM_FIELD_11_OPTION_ID: { + label: "Creative Custom Field 11 Option ID", + value: "CREATIVE_CUSTOM_FIELD_11_OPTION_ID", + }, + CREATIVE_CUSTOM_FIELD_12_OPTION_ID: { + label: "Creative Custom Field 12 Option ID", + value: "CREATIVE_CUSTOM_FIELD_12_OPTION_ID", + }, + CREATIVE_CUSTOM_FIELD_13_OPTION_ID: { + label: "Creative Custom Field 13 Option ID", + value: "CREATIVE_CUSTOM_FIELD_13_OPTION_ID", + }, + CREATIVE_CUSTOM_FIELD_14_OPTION_ID: { + label: "Creative Custom Field 14 Option ID", + value: "CREATIVE_CUSTOM_FIELD_14_OPTION_ID", + }, + CREATIVE_CUSTOM_FIELD_0_VALUE: { + label: "Creative Custom Field 0 Value", + value: "CREATIVE_CUSTOM_FIELD_0_VALUE", + }, + CREATIVE_CUSTOM_FIELD_1_VALUE: { + label: "Creative Custom Field 1 Value", + value: "CREATIVE_CUSTOM_FIELD_1_VALUE", + }, + CREATIVE_CUSTOM_FIELD_2_VALUE: { + label: "Creative Custom Field 2 Value", + value: "CREATIVE_CUSTOM_FIELD_2_VALUE", + }, + CREATIVE_CUSTOM_FIELD_3_VALUE: { + label: "Creative Custom Field 3 Value", + value: "CREATIVE_CUSTOM_FIELD_3_VALUE", + }, + CREATIVE_CUSTOM_FIELD_4_VALUE: { + label: "Creative Custom Field 4 Value", + value: "CREATIVE_CUSTOM_FIELD_4_VALUE", + }, + CREATIVE_CUSTOM_FIELD_5_VALUE: { + label: "Creative Custom Field 5 Value", + value: "CREATIVE_CUSTOM_FIELD_5_VALUE", + }, + CREATIVE_CUSTOM_FIELD_6_VALUE: { + label: "Creative Custom Field 6 Value", + value: "CREATIVE_CUSTOM_FIELD_6_VALUE", + }, + CREATIVE_CUSTOM_FIELD_7_VALUE: { + label: "Creative Custom Field 7 Value", + value: "CREATIVE_CUSTOM_FIELD_7_VALUE", + }, + CREATIVE_CUSTOM_FIELD_8_VALUE: { + label: "Creative Custom Field 8 Value", + value: "CREATIVE_CUSTOM_FIELD_8_VALUE", + }, + CREATIVE_CUSTOM_FIELD_9_VALUE: { + label: "Creative Custom Field 9 Value", + value: "CREATIVE_CUSTOM_FIELD_9_VALUE", + }, + CREATIVE_CUSTOM_FIELD_10_VALUE: { + label: "Creative Custom Field 10 Value", + value: "CREATIVE_CUSTOM_FIELD_10_VALUE", + }, + CREATIVE_CUSTOM_FIELD_11_VALUE: { + label: "Creative Custom Field 11 Value", + value: "CREATIVE_CUSTOM_FIELD_11_VALUE", + }, + CREATIVE_CUSTOM_FIELD_12_VALUE: { + label: "Creative Custom Field 12 Value", + value: "CREATIVE_CUSTOM_FIELD_12_VALUE", + }, + CREATIVE_CUSTOM_FIELD_13_VALUE: { + label: "Creative Custom Field 13 Value", + value: "CREATIVE_CUSTOM_FIELD_13_VALUE", + }, + CREATIVE_CUSTOM_FIELD_14_VALUE: { + label: "Creative Custom Field 14 Value", + value: "CREATIVE_CUSTOM_FIELD_14_VALUE", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_0_OPTION_ID: { + label: "Backfill Line Item Custom Field 0 Option ID", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_0_OPTION_ID", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_1_OPTION_ID: { + label: "Backfill Line Item Custom Field 1 Option ID", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_1_OPTION_ID", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_2_OPTION_ID: { + label: "Backfill Line Item Custom Field 2 Option ID", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_2_OPTION_ID", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_3_OPTION_ID: { + label: "Backfill Line Item Custom Field 3 Option ID", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_3_OPTION_ID", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_4_OPTION_ID: { + label: "Backfill Line Item Custom Field 4 Option ID", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_4_OPTION_ID", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_5_OPTION_ID: { + label: "Backfill Line Item Custom Field 5 Option ID", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_5_OPTION_ID", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_6_OPTION_ID: { + label: "Backfill Line Item Custom Field 6 Option ID", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_6_OPTION_ID", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_7_OPTION_ID: { + label: "Backfill Line Item Custom Field 7 Option ID", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_7_OPTION_ID", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_8_OPTION_ID: { + label: "Backfill Line Item Custom Field 8 Option ID", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_8_OPTION_ID", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_9_OPTION_ID: { + label: "Backfill Line Item Custom Field 9 Option ID", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_9_OPTION_ID", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_10_OPTION_ID: { + label: "Backfill Line Item Custom Field 10 Option ID", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_10_OPTION_ID", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_11_OPTION_ID: { + label: "Backfill Line Item Custom Field 11 Option ID", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_11_OPTION_ID", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_12_OPTION_ID: { + label: "Backfill Line Item Custom Field 12 Option ID", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_12_OPTION_ID", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_13_OPTION_ID: { + label: "Backfill Line Item Custom Field 13 Option ID", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_13_OPTION_ID", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_14_OPTION_ID: { + label: "Backfill Line Item Custom Field 14 Option ID", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_14_OPTION_ID", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_0_VALUE: { + label: "Backfill Line Item Custom Field 0 Value", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_0_VALUE", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_1_VALUE: { + label: "Backfill Line Item Custom Field 1 Value", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_1_VALUE", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_2_VALUE: { + label: "Backfill Line Item Custom Field 2 Value", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_2_VALUE", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_3_VALUE: { + label: "Backfill Line Item Custom Field 3 Value", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_3_VALUE", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_4_VALUE: { + label: "Backfill Line Item Custom Field 4 Value", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_4_VALUE", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_5_VALUE: { + label: "Backfill Line Item Custom Field 5 Value", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_5_VALUE", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_6_VALUE: { + label: "Backfill Line Item Custom Field 6 Value", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_6_VALUE", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_7_VALUE: { + label: "Backfill Line Item Custom Field 7 Value", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_7_VALUE", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_8_VALUE: { + label: "Backfill Line Item Custom Field 8 Value", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_8_VALUE", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_9_VALUE: { + label: "Backfill Line Item Custom Field 9 Value", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_9_VALUE", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_10_VALUE: { + label: "Backfill Line Item Custom Field 10 Value", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_10_VALUE", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_11_VALUE: { + label: "Backfill Line Item Custom Field 11 Value", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_11_VALUE", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_12_VALUE: { + label: "Backfill Line Item Custom Field 12 Value", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_12_VALUE", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_13_VALUE: { + label: "Backfill Line Item Custom Field 13 Value", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_13_VALUE", + }, + BACKFILL_LINE_ITEM_CUSTOM_FIELD_14_VALUE: { + label: "Backfill Line Item Custom Field 14 Value", + value: "BACKFILL_LINE_ITEM_CUSTOM_FIELD_14_VALUE", + }, + BACKFILL_ORDER_CUSTOM_FIELD_0_OPTION_ID: { + label: "Backfill Order Custom Field 0 Option ID", + value: "BACKFILL_ORDER_CUSTOM_FIELD_0_OPTION_ID", + }, + BACKFILL_ORDER_CUSTOM_FIELD_1_OPTION_ID: { + label: "Backfill Order Custom Field 1 Option ID", + value: "BACKFILL_ORDER_CUSTOM_FIELD_1_OPTION_ID", + }, + BACKFILL_ORDER_CUSTOM_FIELD_2_OPTION_ID: { + label: "Backfill Order Custom Field 2 Option ID", + value: "BACKFILL_ORDER_CUSTOM_FIELD_2_OPTION_ID", + }, + BACKFILL_ORDER_CUSTOM_FIELD_3_OPTION_ID: { + label: "Backfill Order Custom Field 3 Option ID", + value: "BACKFILL_ORDER_CUSTOM_FIELD_3_OPTION_ID", + }, + BACKFILL_ORDER_CUSTOM_FIELD_4_OPTION_ID: { + label: "Backfill Order Custom Field 4 Option ID", + value: "BACKFILL_ORDER_CUSTOM_FIELD_4_OPTION_ID", + }, + BACKFILL_ORDER_CUSTOM_FIELD_5_OPTION_ID: { + label: "Backfill Order Custom Field 5 Option ID", + value: "BACKFILL_ORDER_CUSTOM_FIELD_5_OPTION_ID", + }, + BACKFILL_ORDER_CUSTOM_FIELD_6_OPTION_ID: { + label: "Backfill Order Custom Field 6 Option ID", + value: "BACKFILL_ORDER_CUSTOM_FIELD_6_OPTION_ID", + }, + BACKFILL_ORDER_CUSTOM_FIELD_7_OPTION_ID: { + label: "Backfill Order Custom Field 7 Option ID", + value: "BACKFILL_ORDER_CUSTOM_FIELD_7_OPTION_ID", + }, + BACKFILL_ORDER_CUSTOM_FIELD_8_OPTION_ID: { + label: "Backfill Order Custom Field 8 Option ID", + value: "BACKFILL_ORDER_CUSTOM_FIELD_8_OPTION_ID", + }, + BACKFILL_ORDER_CUSTOM_FIELD_9_OPTION_ID: { + label: "Backfill Order Custom Field 9 Option ID", + value: "BACKFILL_ORDER_CUSTOM_FIELD_9_OPTION_ID", + }, + BACKFILL_ORDER_CUSTOM_FIELD_10_OPTION_ID: { + label: "Backfill Order Custom Field 10 Option ID", + value: "BACKFILL_ORDER_CUSTOM_FIELD_10_OPTION_ID", + }, + BACKFILL_ORDER_CUSTOM_FIELD_11_OPTION_ID: { + label: "Backfill Order Custom Field 11 Option ID", + value: "BACKFILL_ORDER_CUSTOM_FIELD_11_OPTION_ID", + }, + BACKFILL_ORDER_CUSTOM_FIELD_12_OPTION_ID: { + label: "Backfill Order Custom Field 12 Option ID", + value: "BACKFILL_ORDER_CUSTOM_FIELD_12_OPTION_ID", + }, + BACKFILL_ORDER_CUSTOM_FIELD_13_OPTION_ID: { + label: "Backfill Order Custom Field 13 Option ID", + value: "BACKFILL_ORDER_CUSTOM_FIELD_13_OPTION_ID", + }, + BACKFILL_ORDER_CUSTOM_FIELD_14_OPTION_ID: { + label: "Backfill Order Custom Field 14 Option ID", + value: "BACKFILL_ORDER_CUSTOM_FIELD_14_OPTION_ID", + }, + BACKFILL_ORDER_CUSTOM_FIELD_0_VALUE: { + label: "Backfill Order Custom Field 0 Value", + value: "BACKFILL_ORDER_CUSTOM_FIELD_0_VALUE", + }, + BACKFILL_ORDER_CUSTOM_FIELD_1_VALUE: { + label: "Backfill Order Custom Field 1 Value", + value: "BACKFILL_ORDER_CUSTOM_FIELD_1_VALUE", + }, + BACKFILL_ORDER_CUSTOM_FIELD_2_VALUE: { + label: "Backfill Order Custom Field 2 Value", + value: "BACKFILL_ORDER_CUSTOM_FIELD_2_VALUE", + }, + BACKFILL_ORDER_CUSTOM_FIELD_3_VALUE: { + label: "Backfill Order Custom Field 3 Value", + value: "BACKFILL_ORDER_CUSTOM_FIELD_3_VALUE", + }, + BACKFILL_ORDER_CUSTOM_FIELD_4_VALUE: { + label: "Backfill Order Custom Field 4 Value", + value: "BACKFILL_ORDER_CUSTOM_FIELD_4_VALUE", + }, + BACKFILL_ORDER_CUSTOM_FIELD_5_VALUE: { + label: "Backfill Order Custom Field 5 Value", + value: "BACKFILL_ORDER_CUSTOM_FIELD_5_VALUE", + }, + BACKFILL_ORDER_CUSTOM_FIELD_6_VALUE: { + label: "Backfill Order Custom Field 6 Value", + value: "BACKFILL_ORDER_CUSTOM_FIELD_6_VALUE", + }, + BACKFILL_ORDER_CUSTOM_FIELD_7_VALUE: { + label: "Backfill Order Custom Field 7 Value", + value: "BACKFILL_ORDER_CUSTOM_FIELD_7_VALUE", + }, + BACKFILL_ORDER_CUSTOM_FIELD_8_VALUE: { + label: "Backfill Order Custom Field 8 Value", + value: "BACKFILL_ORDER_CUSTOM_FIELD_8_VALUE", + }, + BACKFILL_ORDER_CUSTOM_FIELD_9_VALUE: { + label: "Backfill Order Custom Field 9 Value", + value: "BACKFILL_ORDER_CUSTOM_FIELD_9_VALUE", + }, + BACKFILL_ORDER_CUSTOM_FIELD_10_VALUE: { + label: "Backfill Order Custom Field 10 Value", + value: "BACKFILL_ORDER_CUSTOM_FIELD_10_VALUE", + }, + BACKFILL_ORDER_CUSTOM_FIELD_11_VALUE: { + label: "Backfill Order Custom Field 11 Value", + value: "BACKFILL_ORDER_CUSTOM_FIELD_11_VALUE", + }, + BACKFILL_ORDER_CUSTOM_FIELD_12_VALUE: { + label: "Backfill Order Custom Field 12 Value", + value: "BACKFILL_ORDER_CUSTOM_FIELD_12_VALUE", + }, + BACKFILL_ORDER_CUSTOM_FIELD_13_VALUE: { + label: "Backfill Order Custom Field 13 Value", + value: "BACKFILL_ORDER_CUSTOM_FIELD_13_VALUE", + }, + BACKFILL_ORDER_CUSTOM_FIELD_14_VALUE: { + label: "Backfill Order Custom Field 14 Value", + value: "BACKFILL_ORDER_CUSTOM_FIELD_14_VALUE", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_0_OPTION_ID: { + label: "Backfill Creative Custom Field 0 Option ID", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_0_OPTION_ID", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_1_OPTION_ID: { + label: "Backfill Creative Custom Field 1 Option ID", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_1_OPTION_ID", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_2_OPTION_ID: { + label: "Backfill Creative Custom Field 2 Option ID", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_2_OPTION_ID", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_3_OPTION_ID: { + label: "Backfill Creative Custom Field 3 Option ID", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_3_OPTION_ID", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_4_OPTION_ID: { + label: "Backfill Creative Custom Field 4 Option ID", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_4_OPTION_ID", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_5_OPTION_ID: { + label: "Backfill Creative Custom Field 5 Option ID", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_5_OPTION_ID", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_6_OPTION_ID: { + label: "Backfill Creative Custom Field 6 Option ID", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_6_OPTION_ID", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_7_OPTION_ID: { + label: "Backfill Creative Custom Field 7 Option ID", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_7_OPTION_ID", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_8_OPTION_ID: { + label: "Backfill Creative Custom Field 8 Option ID", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_8_OPTION_ID", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_9_OPTION_ID: { + label: "Backfill Creative Custom Field 9 Option ID", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_9_OPTION_ID", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_10_OPTION_ID: { + label: "Backfill Creative Custom Field 10 Option ID", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_10_OPTION_ID", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_11_OPTION_ID: { + label: "Backfill Creative Custom Field 11 Option ID", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_11_OPTION_ID", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_12_OPTION_ID: { + label: "Backfill Creative Custom Field 12 Option ID", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_12_OPTION_ID", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_13_OPTION_ID: { + label: "Backfill Creative Custom Field 13 Option ID", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_13_OPTION_ID", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_14_OPTION_ID: { + label: "Backfill Creative Custom Field 14 Option ID", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_14_OPTION_ID", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_0_VALUE: { + label: "Backfill Creative Custom Field 0 Value", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_0_VALUE", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_1_VALUE: { + label: "Backfill Creative Custom Field 1 Value", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_1_VALUE", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_2_VALUE: { + label: "Backfill Creative Custom Field 2 Value", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_2_VALUE", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_3_VALUE: { + label: "Backfill Creative Custom Field 3 Value", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_3_VALUE", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_4_VALUE: { + label: "Backfill Creative Custom Field 4 Value", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_4_VALUE", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_5_VALUE: { + label: "Backfill Creative Custom Field 5 Value", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_5_VALUE", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_6_VALUE: { + label: "Backfill Creative Custom Field 6 Value", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_6_VALUE", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_7_VALUE: { + label: "Backfill Creative Custom Field 7 Value", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_7_VALUE", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_8_VALUE: { + label: "Backfill Creative Custom Field 8 Value", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_8_VALUE", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_9_VALUE: { + label: "Backfill Creative Custom Field 9 Value", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_9_VALUE", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_10_VALUE: { + label: "Backfill Creative Custom Field 10 Value", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_10_VALUE", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_11_VALUE: { + label: "Backfill Creative Custom Field 11 Value", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_11_VALUE", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_12_VALUE: { + label: "Backfill Creative Custom Field 12 Value", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_12_VALUE", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_13_VALUE: { + label: "Backfill Creative Custom Field 13 Value", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_13_VALUE", + }, + BACKFILL_CREATIVE_CUSTOM_FIELD_14_VALUE: { + label: "Backfill Creative Custom Field 14 Value", + value: "BACKFILL_CREATIVE_CUSTOM_FIELD_14_VALUE", + }, + CUSTOM_DIMENSION_0_VALUE_ID: { + label: "Custom Dimension 0 Value ID", + value: "CUSTOM_DIMENSION_0_VALUE_ID", + }, + CUSTOM_DIMENSION_1_VALUE_ID: { + label: "Custom Dimension 1 Value ID", + value: "CUSTOM_DIMENSION_1_VALUE_ID", + }, + CUSTOM_DIMENSION_2_VALUE_ID: { + label: "Custom Dimension 2 Value ID", + value: "CUSTOM_DIMENSION_2_VALUE_ID", + }, + CUSTOM_DIMENSION_3_VALUE_ID: { + label: "Custom Dimension 3 Value ID", + value: "CUSTOM_DIMENSION_3_VALUE_ID", + }, + CUSTOM_DIMENSION_4_VALUE_ID: { + label: "Custom Dimension 4 Value ID", + value: "CUSTOM_DIMENSION_4_VALUE_ID", + }, + CUSTOM_DIMENSION_5_VALUE_ID: { + label: "Custom Dimension 5 Value ID", + value: "CUSTOM_DIMENSION_5_VALUE_ID", + }, + CUSTOM_DIMENSION_6_VALUE_ID: { + label: "Custom Dimension 6 Value ID", + value: "CUSTOM_DIMENSION_6_VALUE_ID", + }, + CUSTOM_DIMENSION_7_VALUE_ID: { + label: "Custom Dimension 7 Value ID", + value: "CUSTOM_DIMENSION_7_VALUE_ID", + }, + CUSTOM_DIMENSION_8_VALUE_ID: { + label: "Custom Dimension 8 Value ID", + value: "CUSTOM_DIMENSION_8_VALUE_ID", + }, + CUSTOM_DIMENSION_9_VALUE_ID: { + label: "Custom Dimension 9 Value ID", + value: "CUSTOM_DIMENSION_9_VALUE_ID", + }, + CUSTOM_DIMENSION_0_VALUE: { + label: "Custom Dimension 0 Value", + value: "CUSTOM_DIMENSION_0_VALUE", + }, + CUSTOM_DIMENSION_1_VALUE: { + label: "Custom Dimension 1 Value", + value: "CUSTOM_DIMENSION_1_VALUE", + }, + CUSTOM_DIMENSION_2_VALUE: { + label: "Custom Dimension 2 Value", + value: "CUSTOM_DIMENSION_2_VALUE", + }, + CUSTOM_DIMENSION_3_VALUE: { + label: "Custom Dimension 3 Value", + value: "CUSTOM_DIMENSION_3_VALUE", + }, + CUSTOM_DIMENSION_4_VALUE: { + label: "Custom Dimension 4 Value", + value: "CUSTOM_DIMENSION_4_VALUE", + }, + CUSTOM_DIMENSION_5_VALUE: { + label: "Custom Dimension 5 Value", + value: "CUSTOM_DIMENSION_5_VALUE", + }, + CUSTOM_DIMENSION_6_VALUE: { + label: "Custom Dimension 6 Value", + value: "CUSTOM_DIMENSION_6_VALUE", + }, + CUSTOM_DIMENSION_7_VALUE: { + label: "Custom Dimension 7 Value", + value: "CUSTOM_DIMENSION_7_VALUE", + }, + CUSTOM_DIMENSION_8_VALUE: { + label: "Custom Dimension 8 Value", + value: "CUSTOM_DIMENSION_8_VALUE", + }, + CUSTOM_DIMENSION_9_VALUE: { + label: "Custom Dimension 9 Value", + value: "CUSTOM_DIMENSION_9_VALUE", + }, +}; diff --git a/components/google_ad_manager/common/metrics.mjs b/components/google_ad_manager/common/metrics.mjs new file mode 100644 index 0000000000000..c24c32b68a414 --- /dev/null +++ b/components/google_ad_manager/common/metrics.mjs @@ -0,0 +1,666 @@ +export default { + METRIC_UNSPECIFIED: { + label: "Metric Unspecified", + value: "METRIC_UNSPECIFIED", + }, + ACTIVE_VIEW_AVERAGE_VIEWABLE_TIME: { + label: "Active View Average Viewable Time", + value: "ACTIVE_VIEW_AVERAGE_VIEWABLE_TIME", + }, + ACTIVE_VIEW_ELIGIBLE_IMPRESSIONS: { + label: "Active View Eligible Impressions", + value: "ACTIVE_VIEW_ELIGIBLE_IMPRESSIONS", + }, + ACTIVE_VIEW_MEASURABLE_IMPRESSIONS: { + label: "Active View Measurable Impressions", + value: "ACTIVE_VIEW_MEASURABLE_IMPRESSIONS", + }, + ACTIVE_VIEW_MEASURABLE_IMPRESSIONS_RATE: { + label: "Active View Measurable Impressions Rate", + value: "ACTIVE_VIEW_MEASURABLE_IMPRESSIONS_RATE", + }, + ACTIVE_VIEW_VIEWABLE_IMPRESSIONS: { + label: "Active View Viewable Impressions", + value: "ACTIVE_VIEW_VIEWABLE_IMPRESSIONS", + }, + ACTIVE_VIEW_VIEWABLE_IMPRESSIONS_RATE: { + label: "Active View Viewable Impressions Rate", + value: "ACTIVE_VIEW_VIEWABLE_IMPRESSIONS_RATE", + }, + ADSENSE_ACTIVE_VIEW_AVERAGE_VIEWABLE_TIME: { + label: "AdSense Active View Average Viewable Time", + value: "ADSENSE_ACTIVE_VIEW_AVERAGE_VIEWABLE_TIME", + }, + ADSENSE_ACTIVE_VIEW_ELIGIBLE_IMPRESSIONS: { + label: "AdSense Active View Eligible Impressions", + value: "ADSENSE_ACTIVE_VIEW_ELIGIBLE_IMPRESSIONS", + }, + ADSENSE_ACTIVE_VIEW_MEASURABLE_IMPRESSIONS: { + label: "AdSense Active View Measurable Impressions", + value: "ADSENSE_ACTIVE_VIEW_MEASURABLE_IMPRESSIONS", + }, + ADSENSE_ACTIVE_VIEW_MEASURABLE_IMPRESSIONS_RATE: { + label: "AdSense Active View Measurable Impressions Rate", + value: "ADSENSE_ACTIVE_VIEW_MEASURABLE_IMPRESSIONS_RATE", + }, + ADSENSE_ACTIVE_VIEW_VIEWABLE_IMPRESSIONS: { + label: "AdSense Active View Viewable Impressions", + value: "ADSENSE_ACTIVE_VIEW_VIEWABLE_IMPRESSIONS", + }, + ADSENSE_ACTIVE_VIEW_VIEWABLE_IMPRESSIONS_RATE: { + label: "AdSense Active View Viewable Impressions Rate", + value: "ADSENSE_ACTIVE_VIEW_VIEWABLE_IMPRESSIONS_RATE", + }, + ADSENSE_AVERAGE_ECPM: { + label: "AdSense Average eCPM", + value: "ADSENSE_AVERAGE_ECPM", + }, + ADSENSE_CLICKS: { + label: "AdSense Clicks", + value: "ADSENSE_CLICKS", + }, + ADSENSE_CTR: { + label: "AdSense CTR", + value: "ADSENSE_CTR", + }, + ADSENSE_IMPRESSIONS: { + label: "AdSense Impressions", + value: "ADSENSE_IMPRESSIONS", + }, + ADSENSE_PERCENT_CLICKS: { + label: "AdSense Percent Clicks", + value: "ADSENSE_PERCENT_CLICKS", + }, + ADSENSE_PERCENT_IMPRESSIONS: { + label: "AdSense Percent Impressions", + value: "ADSENSE_PERCENT_IMPRESSIONS", + }, + ADSENSE_PERCENT_REVENUE: { + label: "AdSense Percent Revenue", + value: "ADSENSE_PERCENT_REVENUE", + }, + ADSENSE_PERCENT_REVENUE_WITHOUT_CPD: { + label: "AdSense Percent Revenue Without CPD", + value: "ADSENSE_PERCENT_REVENUE_WITHOUT_CPD", + }, + ADSENSE_RESPONSES_SERVED: { + label: "AdSense Responses Served", + value: "ADSENSE_RESPONSES_SERVED", + }, + ADSENSE_REVENUE: { + label: "AdSense Revenue", + value: "ADSENSE_REVENUE", + }, + AD_EXCHANGE_ACTIVE_VIEW_AVERAGE_VIEWABLE_TIME: { + label: "Ad Exchange Active View Average Viewable Time", + value: "AD_EXCHANGE_ACTIVE_VIEW_AVERAGE_VIEWABLE_TIME", + }, + AD_EXCHANGE_ACTIVE_VIEW_ELIGIBLE_IMPRESSIONS: { + label: "Ad Exchange Active View Eligible Impressions", + value: "AD_EXCHANGE_ACTIVE_VIEW_ELIGIBLE_IMPRESSIONS", + }, + AD_EXCHANGE_ACTIVE_VIEW_MEASURABLE_IMPRESSIONS: { + label: "Ad Exchange Active View Measurable Impressions", + value: "AD_EXCHANGE_ACTIVE_VIEW_MEASURABLE_IMPRESSIONS", + }, + AD_EXCHANGE_ACTIVE_VIEW_MEASURABLE_IMPRESSIONS_RATE: { + label: "Ad Exchange Active View Measurable Impressions Rate", + value: "AD_EXCHANGE_ACTIVE_VIEW_MEASURABLE_IMPRESSIONS_RATE", + }, + AD_EXCHANGE_ACTIVE_VIEW_VIEWABLE_IMPRESSIONS: { + label: "Ad Exchange Active View Viewable Impressions", + value: "AD_EXCHANGE_ACTIVE_VIEW_VIEWABLE_IMPRESSIONS", + }, + AD_EXCHANGE_ACTIVE_VIEW_VIEWABLE_IMPRESSIONS_RATE: { + label: "Ad Exchange Active View Viewable Impressions Rate", + value: "AD_EXCHANGE_ACTIVE_VIEW_VIEWABLE_IMPRESSIONS_RATE", + }, + AD_EXCHANGE_AVERAGE_ECPM: { + label: "Ad Exchange Average eCPM", + value: "AD_EXCHANGE_AVERAGE_ECPM", + }, + AD_EXCHANGE_CLICKS: { + label: "Ad Exchange Clicks", + value: "AD_EXCHANGE_CLICKS", + }, + AD_EXCHANGE_CTR: { + label: "Ad Exchange CTR", + value: "AD_EXCHANGE_CTR", + }, + AD_EXCHANGE_IMPRESSIONS: { + label: "Ad Exchange Impressions", + value: "AD_EXCHANGE_IMPRESSIONS", + }, + AD_EXCHANGE_PERCENT_CLICKS: { + label: "Ad Exchange Percent Clicks", + value: "AD_EXCHANGE_PERCENT_CLICKS", + }, + AD_EXCHANGE_PERCENT_IMPRESSIONS: { + label: "Ad Exchange Percent Impressions", + value: "AD_EXCHANGE_PERCENT_IMPRESSIONS", + }, + AD_EXCHANGE_PERCENT_REVENUE: { + label: "Ad Exchange Percent Revenue", + value: "AD_EXCHANGE_PERCENT_REVENUE", + }, + AD_EXCHANGE_PERCENT_REVENUE_WITHOUT_CPD: { + label: "Ad Exchange Percent Revenue Without CPD", + value: "AD_EXCHANGE_PERCENT_REVENUE_WITHOUT_CPD", + }, + AD_EXCHANGE_RESPONSES_SERVED: { + label: "Ad Exchange Responses Served", + value: "AD_EXCHANGE_RESPONSES_SERVED", + }, + AD_EXCHANGE_REVENUE: { + label: "Ad Exchange Revenue", + value: "AD_EXCHANGE_REVENUE", + }, + AD_REQUESTS: { + label: "Ad Requests", + value: "AD_REQUESTS", + }, + AD_SERVER_ACTIVE_VIEW_AVERAGE_VIEWABLE_TIME: { + label: "Ad Server Active View Average Viewable Time", + value: "AD_SERVER_ACTIVE_VIEW_AVERAGE_VIEWABLE_TIME", + }, + AD_SERVER_ACTIVE_VIEW_ELIGIBLE_IMPRESSIONS: { + label: "Ad Server Active View Eligible Impressions", + value: "AD_SERVER_ACTIVE_VIEW_ELIGIBLE_IMPRESSIONS", + }, + AD_SERVER_ACTIVE_VIEW_MEASURABLE_IMPRESSIONS: { + label: "Ad Server Active View Measurable Impressions", + value: "AD_SERVER_ACTIVE_VIEW_MEASURABLE_IMPRESSIONS", + }, + AD_SERVER_ACTIVE_VIEW_MEASURABLE_IMPRESSIONS_RATE: { + label: "Ad Server Active View Measurable Impressions Rate", + value: "AD_SERVER_ACTIVE_VIEW_MEASURABLE_IMPRESSIONS_RATE", + }, + AD_SERVER_ACTIVE_VIEW_VIEWABLE_IMPRESSIONS: { + label: "Ad Server Active View Viewable Impressions", + value: "AD_SERVER_ACTIVE_VIEW_VIEWABLE_IMPRESSIONS", + }, + AD_SERVER_ACTIVE_VIEW_VIEWABLE_IMPRESSIONS_RATE: { + label: "Ad Server Active View Viewable Impressions Rate", + value: "AD_SERVER_ACTIVE_VIEW_VIEWABLE_IMPRESSIONS_RATE", + }, + AD_SERVER_AVERAGE_ECPM: { + label: "Ad Server Average eCPM", + value: "AD_SERVER_AVERAGE_ECPM", + }, + AD_SERVER_AVERAGE_ECPM_WITHOUT_CPD: { + label: "Ad Server Average eCPM Without CPD", + value: "AD_SERVER_AVERAGE_ECPM_WITHOUT_CPD", + }, + AD_SERVER_CLICKS: { + label: "Ad Server Clicks", + value: "AD_SERVER_CLICKS", + }, + AD_SERVER_CPD_REVENUE: { + label: "Ad Server CPD Revenue", + value: "AD_SERVER_CPD_REVENUE", + }, + AD_SERVER_CTR: { + label: "Ad Server CTR", + value: "AD_SERVER_CTR", + }, + AD_SERVER_IMPRESSIONS: { + label: "Ad Server Impressions", + value: "AD_SERVER_IMPRESSIONS", + }, + AD_SERVER_PERCENT_CLICKS: { + label: "Ad Server Percent Clicks", + value: "AD_SERVER_PERCENT_CLICKS", + }, + AD_SERVER_PERCENT_IMPRESSIONS: { + label: "Ad Server Percent Impressions", + value: "AD_SERVER_PERCENT_IMPRESSIONS", + }, + AD_SERVER_PERCENT_REVENUE: { + label: "Ad Server Percent Revenue", + value: "AD_SERVER_PERCENT_REVENUE", + }, + AD_SERVER_PERCENT_REVENUE_WITHOUT_CPD: { + label: "Ad Server Percent Revenue Without CPD", + value: "AD_SERVER_PERCENT_REVENUE_WITHOUT_CPD", + }, + AD_SERVER_RESPONSES_SERVED: { + label: "Ad Server Responses Served", + value: "AD_SERVER_RESPONSES_SERVED", + }, + AD_SERVER_REVENUE: { + label: "Ad Server Revenue", + value: "AD_SERVER_REVENUE", + }, + AD_SERVER_REVENUE_WITHOUT_CPD: { + label: "Ad Server Revenue Without CPD", + value: "AD_SERVER_REVENUE_WITHOUT_CPD", + }, + AUCTIONS_WON: { + label: "Auctions Won", + value: "AUCTIONS_WON", + }, + AVERAGE_ECPM: { + label: "Average eCPM", + value: "AVERAGE_ECPM", + }, + AVERAGE_ECPM_WITHOUT_CPD: { + label: "Average eCPM Without CPD", + value: "AVERAGE_ECPM_WITHOUT_CPD", + }, + BIDS: { + label: "Bids", + value: "BIDS", + }, + BIDS_IN_AUCTION: { + label: "Bids in Auction", + value: "BIDS_IN_AUCTION", + }, + CALLOUTS: { + label: "Callouts", + value: "CALLOUTS", + }, + CLICKS: { + label: "Clicks", + value: "CLICKS", + }, + CODE_SERVED_COUNT: { + label: "Code Served Count", + value: "CODE_SERVED_COUNT", + }, + CTR: { + label: "CTR", + value: "CTR", + }, + GOOGLE_SOLD_AUCTION_COVIEWED_IMPRESSIONS: { + label: "Google Sold Auction Coviewed Impressions", + value: "GOOGLE_SOLD_AUCTION_COVIEWED_IMPRESSIONS", + }, + GOOGLE_SOLD_AUCTION_IMPRESSIONS: { + label: "Google Sold Auction Impressions", + value: "GOOGLE_SOLD_AUCTION_IMPRESSIONS", + }, + GOOGLE_SOLD_COVIEWED_IMPRESSIONS: { + label: "Google Sold Coviewed Impressions", + value: "GOOGLE_SOLD_COVIEWED_IMPRESSIONS", + }, + GOOGLE_SOLD_IMPRESSIONS: { + label: "Google Sold Impressions", + value: "GOOGLE_SOLD_IMPRESSIONS", + }, + GOOGLE_SOLD_RESERVATION_COVIEWED_IMPRESSIONS: { + label: "Google Sold Reservation Coviewed Impressions", + value: "GOOGLE_SOLD_RESERVATION_COVIEWED_IMPRESSIONS", + }, + GOOGLE_SOLD_RESERVATION_IMPRESSIONS: { + label: "Google Sold Reservation Impressions", + value: "GOOGLE_SOLD_RESERVATION_IMPRESSIONS", + }, + IMPRESSIONS: { + label: "Impressions", + value: "IMPRESSIONS", + }, + PARTNER_SALES_FILLED_POD_REQUESTS: { + label: "Partner Sales Filled Pod Requests", + value: "PARTNER_SALES_FILLED_POD_REQUESTS", + }, + PARTNER_SALES_FILL_RATE: { + label: "Partner Sales Fill Rate", + value: "PARTNER_SALES_FILL_RATE", + }, + PARTNER_SALES_PARTNER_MATCH_RATE: { + label: "Partner Sales Partner Match Rate", + value: "PARTNER_SALES_PARTNER_MATCH_RATE", + }, + PARTNER_SALES_QUERIES: { + label: "Partner Sales Queries", + value: "PARTNER_SALES_QUERIES", + }, + PARTNER_SALES_UNFILLED_IMPRESSIONS: { + label: "Partner Sales Unfilled Impressions", + value: "PARTNER_SALES_UNFILLED_IMPRESSIONS", + }, + PARTNER_SALES_UNMATCHED_QUERIES: { + label: "Partner Sales Unmatched Queries", + value: "PARTNER_SALES_UNMATCHED_QUERIES", + }, + PARTNER_SOLD_CODE_SERVED: { + label: "Partner Sold Code Served", + value: "PARTNER_SOLD_CODE_SERVED", + }, + PARTNER_SOLD_COVIEWED_IMPRESSIONS: { + label: "Partner Sold Coviewed Impressions", + value: "PARTNER_SOLD_COVIEWED_IMPRESSIONS", + }, + PARTNER_SOLD_IMPRESSIONS: { + label: "Partner Sold Impressions", + value: "PARTNER_SOLD_IMPRESSIONS", + }, + PROGRAMMATIC_ELIGIBLE_AD_REQUESTS: { + label: "Programmatic Eligible Ad Requests", + value: "PROGRAMMATIC_ELIGIBLE_AD_REQUESTS", + }, + PROGRAMMATIC_MATCH_RATE: { + label: "Programmatic Match Rate", + value: "PROGRAMMATIC_MATCH_RATE", + }, + PROGRAMMATIC_RESPONSES_SERVED: { + label: "Programmatic Responses Served", + value: "PROGRAMMATIC_RESPONSES_SERVED", + }, + RESPONSES_SERVED: { + label: "Responses Served", + value: "RESPONSES_SERVED", + }, + REVENUE: { + label: "Revenue", + value: "REVENUE", + }, + REVENUE_WITHOUT_CPD: { + label: "Revenue Without CPD", + value: "REVENUE_WITHOUT_CPD", + }, + SUCCESSFUL_RESPONSES: { + label: "Successful Responses", + value: "SUCCESSFUL_RESPONSES", + }, + UNFILLED_IMPRESSIONS: { + label: "Unfilled Impressions", + value: "UNFILLED_IMPRESSIONS", + }, + UNMATCHED_AD_REQUESTS: { + label: "Unmatched Ad Requests", + value: "UNMATCHED_AD_REQUESTS", + }, + USER_MESSAGES_OFFERWALL_MESSAGES_SHOWN: { + label: "User Messages Offerwall Messages Shown", + value: "USER_MESSAGES_OFFERWALL_MESSAGES_SHOWN", + }, + USER_MESSAGES_OFFERWALL_SUCCESSFUL_ENGAGEMENTS: { + label: "User Messages Offerwall Successful Engagements", + value: "USER_MESSAGES_OFFERWALL_SUCCESSFUL_ENGAGEMENTS", + }, + VIDEO_INTERACTION_AVERAGE_INTERACTION_RATE: { + label: "Video Interaction Average Interaction Rate", + value: "VIDEO_INTERACTION_AVERAGE_INTERACTION_RATE", + }, + VIDEO_INTERACTION_COLLAPSES: { + label: "Video Interaction Collapses", + value: "VIDEO_INTERACTION_COLLAPSES", + }, + VIDEO_INTERACTION_EXPANDS: { + label: "Video Interaction Expands", + value: "VIDEO_INTERACTION_EXPANDS", + }, + VIDEO_INTERACTION_FULL_SCREENS: { + label: "Video Interaction Full Screens", + value: "VIDEO_INTERACTION_FULL_SCREENS", + }, + VIDEO_INTERACTION_MUTES: { + label: "Video Interaction Mutes", + value: "VIDEO_INTERACTION_MUTES", + }, + VIDEO_INTERACTION_PAUSES: { + label: "Video Interaction Pauses", + value: "VIDEO_INTERACTION_PAUSES", + }, + VIDEO_INTERACTION_RESUMES: { + label: "Video Interaction Resumes", + value: "VIDEO_INTERACTION_RESUMES", + }, + VIDEO_INTERACTION_REWINDS: { + label: "Video Interaction Rewinds", + value: "VIDEO_INTERACTION_REWINDS", + }, + VIDEO_INTERACTION_UNMUTES: { + label: "Video Interaction Unmutes", + value: "VIDEO_INTERACTION_UNMUTES", + }, + VIDEO_INTERACTION_VIDEO_SKIPS: { + label: "Video Interaction Video Skips", + value: "VIDEO_INTERACTION_VIDEO_SKIPS", + }, + VIDEO_REAL_TIME_CREATIVE_SERVES: { + label: "Video Real Time Creative Serves", + value: "VIDEO_REAL_TIME_CREATIVE_SERVES", + }, + VIDEO_REAL_TIME_ERROR_100_COUNT: { + label: "Video Real Time Error 100 Count", + value: "VIDEO_REAL_TIME_ERROR_100_COUNT", + }, + VIDEO_REAL_TIME_ERROR_101_COUNT: { + label: "Video Real Time Error 101 Count", + value: "VIDEO_REAL_TIME_ERROR_101_COUNT", + }, + VIDEO_REAL_TIME_ERROR_102_COUNT: { + label: "Video Real Time Error 102 Count", + value: "VIDEO_REAL_TIME_ERROR_102_COUNT", + }, + VIDEO_REAL_TIME_ERROR_200_COUNT: { + label: "Video Real Time Error 200 Count", + value: "VIDEO_REAL_TIME_ERROR_200_COUNT", + }, + VIDEO_REAL_TIME_ERROR_201_COUNT: { + label: "Video Real Time Error 201 Count", + value: "VIDEO_REAL_TIME_ERROR_201_COUNT", + }, + VIDEO_REAL_TIME_ERROR_202_COUNT: { + label: "Video Real Time Error 202 Count", + value: "VIDEO_REAL_TIME_ERROR_202_COUNT", + }, + VIDEO_REAL_TIME_ERROR_203_COUNT: { + label: "Video Real Time Error 203 Count", + value: "VIDEO_REAL_TIME_ERROR_203_COUNT", + }, + VIDEO_REAL_TIME_ERROR_300_COUNT: { + label: "Video Real Time Error 300 Count", + value: "VIDEO_REAL_TIME_ERROR_300_COUNT", + }, + VIDEO_REAL_TIME_ERROR_301_COUNT: { + label: "Video Real Time Error 301 Count", + value: "VIDEO_REAL_TIME_ERROR_301_COUNT", + }, + VIDEO_REAL_TIME_ERROR_302_COUNT: { + label: "Video Real Time Error 302 Count", + value: "VIDEO_REAL_TIME_ERROR_302_COUNT", + }, + VIDEO_REAL_TIME_ERROR_303_COUNT: { + label: "Video Real Time Error 303 Count", + value: "VIDEO_REAL_TIME_ERROR_303_COUNT", + }, + VIDEO_REAL_TIME_ERROR_400_COUNT: { + label: "Video Real Time Error 400 Count", + value: "VIDEO_REAL_TIME_ERROR_400_COUNT", + }, + VIDEO_REAL_TIME_ERROR_401_COUNT: { + label: "Video Real Time Error 401 Count", + value: "VIDEO_REAL_TIME_ERROR_401_COUNT", + }, + VIDEO_REAL_TIME_ERROR_402_COUNT: { + label: "Video Real Time Error 402 Count", + value: "VIDEO_REAL_TIME_ERROR_402_COUNT", + }, + VIDEO_REAL_TIME_ERROR_403_COUNT: { + label: "Video Real Time Error 403 Count", + value: "VIDEO_REAL_TIME_ERROR_403_COUNT", + }, + VIDEO_REAL_TIME_ERROR_405_COUNT: { + label: "Video Real Time Error 405 Count", + value: "VIDEO_REAL_TIME_ERROR_405_COUNT", + }, + VIDEO_REAL_TIME_ERROR_406_COUNT: { + label: "Video Real Time Error 406 Count", + value: "VIDEO_REAL_TIME_ERROR_406_COUNT", + }, + VIDEO_REAL_TIME_ERROR_407_COUNT: { + label: "Video Real Time Error 407 Count", + value: "VIDEO_REAL_TIME_ERROR_407_COUNT", + }, + VIDEO_REAL_TIME_ERROR_408_COUNT: { + label: "Video Real Time Error 408 Count", + value: "VIDEO_REAL_TIME_ERROR_408_COUNT", + }, + VIDEO_REAL_TIME_ERROR_409_COUNT: { + label: "Video Real Time Error 409 Count", + value: "VIDEO_REAL_TIME_ERROR_409_COUNT", + }, + VIDEO_REAL_TIME_ERROR_410_COUNT: { + label: "Video Real Time Error 410 Count", + value: "VIDEO_REAL_TIME_ERROR_410_COUNT", + }, + VIDEO_REAL_TIME_ERROR_500_COUNT: { + label: "Video Real Time Error 500 Count", + value: "VIDEO_REAL_TIME_ERROR_500_COUNT", + }, + VIDEO_REAL_TIME_ERROR_501_COUNT: { + label: "Video Real Time Error 501 Count", + value: "VIDEO_REAL_TIME_ERROR_501_COUNT", + }, + VIDEO_REAL_TIME_ERROR_502_COUNT: { + label: "Video Real Time Error 502 Count", + value: "VIDEO_REAL_TIME_ERROR_502_COUNT", + }, + VIDEO_REAL_TIME_ERROR_503_COUNT: { + label: "Video Real Time Error 503 Count", + value: "VIDEO_REAL_TIME_ERROR_503_COUNT", + }, + VIDEO_REAL_TIME_ERROR_600_COUNT: { + label: "Video Real Time Error 600 Count", + value: "VIDEO_REAL_TIME_ERROR_600_COUNT", + }, + VIDEO_REAL_TIME_ERROR_601_COUNT: { + label: "Video Real Time Error 601 Count", + value: "VIDEO_REAL_TIME_ERROR_601_COUNT", + }, + VIDEO_REAL_TIME_ERROR_602_COUNT: { + label: "Video Real Time Error 602 Count", + value: "VIDEO_REAL_TIME_ERROR_602_COUNT", + }, + VIDEO_REAL_TIME_ERROR_603_COUNT: { + label: "Video Real Time Error 603 Count", + value: "VIDEO_REAL_TIME_ERROR_603_COUNT", + }, + VIDEO_REAL_TIME_ERROR_604_COUNT: { + label: "Video Real Time Error 604 Count", + value: "VIDEO_REAL_TIME_ERROR_604_COUNT", + }, + VIDEO_REAL_TIME_ERROR_900_COUNT: { + label: "Video Real Time Error 900 Count", + value: "VIDEO_REAL_TIME_ERROR_900_COUNT", + }, + VIDEO_REAL_TIME_ERROR_901_COUNT: { + label: "Video Real Time Error 901 Count", + value: "VIDEO_REAL_TIME_ERROR_901_COUNT", + }, + VIDEO_REAL_TIME_IMPRESSIONS: { + label: "Video Real Time Impressions", + value: "VIDEO_REAL_TIME_IMPRESSIONS", + }, + VIDEO_REAL_TIME_MATCHED_QUERIES: { + label: "Video Real Time Matched Queries", + value: "VIDEO_REAL_TIME_MATCHED_QUERIES", + }, + VIDEO_REAL_TIME_TOTAL_ERROR_COUNT: { + label: "Video Real Time Total Error Count", + value: "VIDEO_REAL_TIME_TOTAL_ERROR_COUNT", + }, + VIDEO_REAL_TIME_TOTAL_QUERIES: { + label: "Video Real Time Total Queries", + value: "VIDEO_REAL_TIME_TOTAL_QUERIES", + }, + VIDEO_REAL_TIME_UNMATCHED_QUERIES: { + label: "Video Real Time Unmatched Queries", + value: "VIDEO_REAL_TIME_UNMATCHED_QUERIES", + }, + VIDEO_VIEWERSHIP_AUTO_PLAYS: { + label: "Video Viewership Auto Plays", + value: "VIDEO_VIEWERSHIP_AUTO_PLAYS", + }, + VIDEO_VIEWERSHIP_AVERAGE_VIEW_RATE: { + label: "Video Viewership Average View Rate", + value: "VIDEO_VIEWERSHIP_AVERAGE_VIEW_RATE", + }, + VIDEO_VIEWERSHIP_AVERAGE_VIEW_TIME: { + label: "Video Viewership Average View Time", + value: "VIDEO_VIEWERSHIP_AVERAGE_VIEW_TIME", + }, + VIDEO_VIEWERSHIP_CLICK_TO_PLAYS: { + label: "Video Viewership Click to Plays", + value: "VIDEO_VIEWERSHIP_CLICK_TO_PLAYS", + }, + VIDEO_VIEWERSHIP_COMPLETES: { + label: "Video Viewership Completes", + value: "VIDEO_VIEWERSHIP_COMPLETES", + }, + VIDEO_VIEWERSHIP_COMPLETION_RATE: { + label: "Video Viewership Completion Rate", + value: "VIDEO_VIEWERSHIP_COMPLETION_RATE", + }, + VIDEO_VIEWERSHIP_ENGAGED_VIEWS: { + label: "Video Viewership Engaged Views", + value: "VIDEO_VIEWERSHIP_ENGAGED_VIEWS", + }, + VIDEO_VIEWERSHIP_FIRST_QUARTILES: { + label: "Video Viewership First Quartiles", + value: "VIDEO_VIEWERSHIP_FIRST_QUARTILES", + }, + VIDEO_VIEWERSHIP_MIDPOINTS: { + label: "Video Viewership Midpoints", + value: "VIDEO_VIEWERSHIP_MIDPOINTS", + }, + VIDEO_VIEWERSHIP_SKIP_BUTTONS_SHOWN: { + label: "Video Viewership Skip Buttons Shown", + value: "VIDEO_VIEWERSHIP_SKIP_BUTTONS_SHOWN", + }, + VIDEO_VIEWERSHIP_STARTS: { + label: "Video Viewership Starts", + value: "VIDEO_VIEWERSHIP_STARTS", + }, + VIDEO_VIEWERSHIP_THIRD_QUARTILES: { + label: "Video Viewership Third Quartiles", + value: "VIDEO_VIEWERSHIP_THIRD_QUARTILES", + }, + VIDEO_VIEWERSHIP_TOTAL_ERROR_COUNT: { + label: "Video Viewership Total Error Count", + value: "VIDEO_VIEWERSHIP_TOTAL_ERROR_COUNT", + }, + VIDEO_VIEWERSHIP_TOTAL_ERROR_RATE: { + label: "Video Viewership Total Error Rate", + value: "VIDEO_VIEWERSHIP_TOTAL_ERROR_RATE", + }, + VIDEO_VIEWERSHIP_VIDEO_LENGTH: { + label: "Video Viewership Video Length", + value: "VIDEO_VIEWERSHIP_VIDEO_LENGTH", + }, + VIDEO_VIEWERSHIP_VIEW_THROUGH_RATE: { + label: "Video Viewership View Through Rate", + value: "VIDEO_VIEWERSHIP_VIEW_THROUGH_RATE", + }, + YIELD_GROUP_ESTIMATED_CPM: { + label: "Yield Group Estimated CPM", + value: "YIELD_GROUP_ESTIMATED_CPM", + }, + YIELD_GROUP_ESTIMATED_REVENUE: { + label: "Yield Group Estimated Revenue", + value: "YIELD_GROUP_ESTIMATED_REVENUE", + }, + YIELD_GROUP_IMPRESSIONS: { + label: "Yield Group Impressions", + value: "YIELD_GROUP_IMPRESSIONS", + }, + YIELD_GROUP_MEDIATION_FILL_RATE: { + label: "Yield Group Mediation Fill Rate", + value: "YIELD_GROUP_MEDIATION_FILL_RATE", + }, + YIELD_GROUP_MEDIATION_MATCHED_QUERIES: { + label: "Yield Group Mediation Matched Queries", + value: "YIELD_GROUP_MEDIATION_MATCHED_QUERIES", + }, + YIELD_GROUP_MEDIATION_PASSBACKS: { + label: "Yield Group Mediation Passbacks", + value: "YIELD_GROUP_MEDIATION_PASSBACKS", + }, + YIELD_GROUP_MEDIATION_THIRD_PARTY_ECPM: { + label: "Yield Group Mediation Third Party ECPM", + value: "YIELD_GROUP_MEDIATION_THIRD_PARTY_ECPM", + }, +}; diff --git a/components/google_ad_manager/common/relative-date-ranges.mjs b/components/google_ad_manager/common/relative-date-ranges.mjs new file mode 100644 index 0000000000000..6867c5e26834e --- /dev/null +++ b/components/google_ad_manager/common/relative-date-ranges.mjs @@ -0,0 +1,114 @@ +export default { + RELATIVE_DATE_RANGE_UNSPECIFIED: { + value: "RELATIVE_DATE_RANGE_UNSPECIFIED", + label: "Relative Date Range Unspecified", + }, + TODAY: { + value: "TODAY", + label: "Today", + }, + YESTERDAY: { + value: "YESTERDAY", + label: "Yesterday", + }, + THIS_WEEK: { + value: "THIS_WEEK", + label: "This Week", + }, + THIS_WEEK_TO_DATE: { + value: "THIS_WEEK_TO_DATE", + label: "This Week To Date", + }, + THIS_MONTH: { + value: "THIS_MONTH", + label: "This Month", + }, + THIS_MONTH_TO_DATE: { + value: "THIS_MONTH_TO_DATE", + label: "This Month To Date", + }, + THIS_QUARTER: { + value: "THIS_QUARTER", + label: "This Quarter", + }, + THIS_QUARTER_TO_DATE: { + value: "THIS_QUARTER_TO_DATE", + label: "This Quarter To Date", + }, + THIS_YEAR: { + value: "THIS_YEAR", + label: "This Year", + }, + THIS_YEAR_TO_DATE: { + value: "THIS_YEAR_TO_DATE", + label: "This Year To Date", + }, + LAST_WEEK: { + value: "LAST_WEEK", + label: "Last Week", + }, + LAST_MONTH: { + value: "LAST_MONTH", + label: "Last Month", + }, + LAST_QUARTER: { + value: "LAST_QUARTER", + label: "Last Quarter", + }, + LAST_YEAR: { + value: "LAST_YEAR", + label: "Last Year", + }, + LAST_7_DAYS: { + value: "LAST_7_DAYS", + label: "Last 7 Days", + }, + LAST_30_DAYS: { + value: "LAST_30_DAYS", + label: "Last 30 Days", + }, + LAST_60_DAYS: { + value: "LAST_60_DAYS", + label: "Last 60 Days", + }, + LAST_90_DAYS: { + value: "LAST_90_DAYS", + label: "Last 90 Days", + }, + LAST_180_DAYS: { + value: "LAST_180_DAYS", + label: "Last 180 Days", + }, + LAST_360_DAYS: { + value: "LAST_360_DAYS", + label: "Last 360 Days", + }, + LAST_365_DAYS: { + value: "LAST_365_DAYS", + label: "Last 365 Days", + }, + LAST_3_MONTHS: { + value: "LAST_3_MONTHS", + label: "Last 3 Months", + }, + LAST_6_MONTHS: { + value: "LAST_6_MONTHS", + label: "Last 6 Months", + }, + LAST_12_MONTHS: { + value: "LAST_12_MONTHS", + label: "Last 12 Months", + }, + ALL_AVAILABLE: { + value: "ALL_AVAILABLE", + label: "All Available", + }, + PREVIOUS_PERIOD: { + value: "PREVIOUS_PERIOD", + label: "Previous Period", + }, + SAME_PERIOD_PREVIOUS_YEAR: { + value: "SAME_PERIOD_PREVIOUS_YEAR", + label: "Same Period Previous Year", + }, +}; diff --git a/components/google_ad_manager/common/utils.mjs b/components/google_ad_manager/common/utils.mjs new file mode 100644 index 0000000000000..650af25ed3a8e --- /dev/null +++ b/components/google_ad_manager/common/utils.mjs @@ -0,0 +1,51 @@ +import { ConfigurationError } from "@pipedream/platform"; + +const parseJson = (input) => { + const parse = (value) => { + if (typeof(value) === "string") { + try { + return parseJson(JSON.parse(value)); + } catch (e) { + return value; + } + } else if (typeof(value) === "object" && value !== null) { + return Object.entries(value) + .reduce((acc, [ + key, + val, + ]) => Object.assign(acc, { + [key]: parse(val), + }), {}); + } + return value; + }; + + return parse(input); +}; + +function parseArray(value) { + try { + if (!value) { + return; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +export default { + parseArray: (value) => parseArray(value)?.map(parseJson), +}; diff --git a/components/google_ad_manager/google_ad_manager.app.mjs b/components/google_ad_manager/google_ad_manager.app.mjs new file mode 100644 index 0000000000000..bf0cf4fea0900 --- /dev/null +++ b/components/google_ad_manager/google_ad_manager.app.mjs @@ -0,0 +1,49 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "google_ad_manager", + propDefinitions: { + network: { + type: "string", + label: "Network", + description: "The network code of the parent network to create the report in.", + async options() { + const { networks = [] } = await this.listNetworks(); + return networks.map(({ name }) => name); + }, + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + listNetworks() { + return this._makeRequest({ + path: "/networks", + }); + }, + }, +}; diff --git a/components/google_ad_manager/package.json b/components/google_ad_manager/package.json new file mode 100644 index 0000000000000..533bd23e477ab --- /dev/null +++ b/components/google_ad_manager/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/google_ad_manager", + "version": "0.1.0", + "description": "Pipedream Google Ad Manager Components", + "main": "google_ad_manager.app.mjs", + "keywords": [ + "pipedream", + "google_ad_manager" + ], + "homepage": "https://pipedream.com/apps/google_ad_manager", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" + } +} diff --git a/components/google_address_validation/google_address_validation.app.mjs b/components/google_address_validation/google_address_validation.app.mjs new file mode 100644 index 0000000000000..921fe058a59b3 --- /dev/null +++ b/components/google_address_validation/google_address_validation.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "google_address_validation", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/google_address_validation/package.json b/components/google_address_validation/package.json new file mode 100644 index 0000000000000..91950338c4248 --- /dev/null +++ b/components/google_address_validation/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/google_address_validation", + "version": "0.0.1", + "description": "Pipedream Google Address Validation Components", + "main": "google_address_validation.app.mjs", + "keywords": [ + "pipedream", + "google_address_validation" + ], + "homepage": "https://pipedream.com/apps/google_address_validation", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/google_ads/README.md b/components/google_ads/README.md index 6f057ee6b7215..91d1497bca086 100644 --- a/components/google_ads/README.md +++ b/components/google_ads/README.md @@ -18,3 +18,44 @@ and campaigns programmatically. With the API, you can automate common tasks, such as creating and managing campaigns, adding and removing keywords, and adjusting bids. You can also use the API to get information about your campaigns, such as campaign stats, keyword stats, and ad performance. + +## Customizing API requests with the Pipedream proxy + +The Pipedream components interact with Google Ads API through Pipedream's proxy service, which handles authentication and developer token requirements. + +The component accepts a standard Google Ads API request object with the following structure: + +```javascript +const googleAdsReq = { + method: "get|post|put|delete", // HTTP method + url: "/v16/...", // Google Ads API endpoint path + headers: { + "Authorization": `Bearer ${this.googleAds.$auth.oauth_access_token}` + }, + data: {} // Optional request body for POST/PUT requests +} +``` + +To make different API calls while using the proxy: + +1. Modify the `url` path to match your desired Google Ads API endpoint +2. Update the `method` to match the required HTTP method +3. Add any necessary request body data in the `data` field +4. Include any required headers (Authorization is automatically included) + +Example for a custom query: + +```javascript +const googleAdsReq = { + method: "post", + url: "/v16/customers/1234567890/googleAds:search", + headers: { + "Authorization": `Bearer ${this.googleAds.$auth.oauth_access_token}` + }, + data: { + query: "SELECT campaign.id, campaign.name FROM campaign" + } +} +``` + +The proxy endpoint will remain the same: `https://eolid4dq1k0t9hi.m.pipedream.net` diff --git a/components/google_ads/actions/add-contact-to-list-by-email/add-contact-to-list-by-email.mjs b/components/google_ads/actions/add-contact-to-list-by-email/add-contact-to-list-by-email.mjs index 3c60cf50a8d7d..585f84d675492 100644 --- a/components/google_ads/actions/add-contact-to-list-by-email/add-contact-to-list-by-email.mjs +++ b/components/google_ads/actions/add-contact-to-list-by-email/add-contact-to-list-by-email.mjs @@ -1,14 +1,16 @@ import crypto from "crypto"; import googleAds from "../../google_ads.app.mjs"; +import common from "../common/common.mjs"; export default { + ...common, key: "google_ads-add-contact-to-list-by-email", name: "Add Contact to Customer List by Email", description: "Adds a contact to a specific customer list in Google Ads. Lists typically update in 6 to 12 hours after operation. [See the documentation](https://developers.google.com/google-ads/api/docs/remarketing/audience-segments/customer-match/get-started)", - version: "0.0.1", + version: "0.1.2", type: "action", props: { - googleAds, + ...common.props, contactEmail: { propDefinition: [ googleAds, @@ -19,23 +21,37 @@ export default { propDefinition: [ googleAds, "userListId", + ({ + accountId, customerClientId, + }) => ({ + accountId, + customerClientId, + }), ], }, }, async run({ $ }) { - const offlineUserDataJob = await this.googleAds.createOfflineUserDataJob({ + const { + googleAds, accountId, customerClientId, contactEmail, userListId, + } = this; + const offlineUserDataJob = await googleAds.createOfflineUserDataJob({ + $, + accountId, + customerClientId, data: { job: { customerMatchUserListMetadata: { - userList: `customers/${this.googleAds.$auth.login_customer_id}/userLists/${this.userListId}`, + userList: `customers/${customerClientId ?? accountId}/userLists/${userListId}`, }, type: "CUSTOMER_MATCH_USER_LIST", }, }, }); - await this.googleAds.addContactToCustomerList({ + await googleAds.addContactToCustomerList({ $, + accountId, + customerClientId, path: offlineUserDataJob.resourceName, data: { operations: [ @@ -43,7 +59,7 @@ export default { create: { userIdentifiers: [ { - hashedEmail: crypto.createHash("sha256").update(this.contactEmail) + hashedEmail: crypto.createHash("sha256").update(contactEmail) .digest("hex"), }, ], @@ -53,12 +69,14 @@ export default { }, }); - const response = await this.googleAds.runOfflineUserDataJob({ + const response = await googleAds.runOfflineUserDataJob({ $, + accountId, + customerClientId, path: offlineUserDataJob.resourceName, }); - $.export("$summary", `Added contact ${this.contactEmail} to the user list ${this.userListId}`); + $.export("$summary", `Added contact ${contactEmail} to user list ${userListId}`); return response; }, }; diff --git a/components/google_ads/actions/common/common.mjs b/components/google_ads/actions/common/common.mjs new file mode 100644 index 0000000000000..7efa315d3327b --- /dev/null +++ b/components/google_ads/actions/common/common.mjs @@ -0,0 +1,5 @@ +import props from "../../common/props.mjs"; + +export default { + props, +}; diff --git a/components/google_ads/actions/common/props.mjs b/components/google_ads/actions/common/props.mjs new file mode 100644 index 0000000000000..b8f744f4b31f2 --- /dev/null +++ b/components/google_ads/actions/common/props.mjs @@ -0,0 +1,15 @@ +const DEFAULT_DOCS = + "https://developers.google.com/google-ads/api/rest/reference/rest/v18/UserList"; + +export const getAdditionalFields = (docsLink = DEFAULT_DOCS) => ({ + type: "object", + label: "Additional Fields", + description: `Additional fields and values to be created. [See the documentation](${docsLink}) for available fields. Values will be parsed as JSON where applicable.`, + optional: true, +}); + +export const getListTypeInfo = (docsLink = DEFAULT_DOCS) => ({ + type: "alert", + alertType: "info", + content: `[See the documentation](${docsLink}) for more information on this type of customer list.`, +}); diff --git a/components/google_ads/actions/create-customer-list/common-constants.mjs b/components/google_ads/actions/create-customer-list/common-constants.mjs new file mode 100644 index 0000000000000..96a44d1f8d4e6 --- /dev/null +++ b/components/google_ads/actions/create-customer-list/common-constants.mjs @@ -0,0 +1,234 @@ +import countryCodeOptions from "./common-country-code-options.mjs"; + +export const USER_LIST_TYPES = { + CRM_BASED: "crmBasedUserList", + RULE_BASED: "ruleBasedUserList", + LOGICAL: "logicalUserList", + BASIC: "basicUserList", + LOOKALIKE: "lookalikeUserList", +}; + +const USER_LIST_CRM_BASED_PROPS = { + uploadKeyType: { + type: "string", + label: "Upload Key Type", + description: + "Matching key type of the list. Mixed data types are not allowed on the same list.", + options: [ + { + label: + "Members are matched from customer info such as email address, phone number or physical address.", + value: "CONTACT_INFO", + }, + { + label: + "Members are matched from a user id generated and assigned by the advertiser.", + value: "CRM_ID", + }, + { + label: "Members are matched from mobile advertising IDs.", + value: "MOBILE_ADVERTISING_ID", + }, + ], + }, + dataSourceType: { + type: "string", + label: "Data Source Type", + description: + "Data source of the list. Only customers on the allow-list can create third-party sourced CRM lists.", + optional: true, + default: "FIRST_PARTY", + options: [ + { + label: "The uploaded data is first-party data.", + value: "FIRST_PARTY", + }, + { + label: "The uploaded data is from a third-party credit bureau,", + value: "THIRD_PARTY_CREDIT_BUREAU", + }, + { + label: "The uploaded data is from a third-party voter file.", + value: "THIRD_PARTY_VOTER_FILE", + }, + ], + }, + appId: { + type: "string", + label: "App ID", + description: + "A string that uniquely identifies a mobile application from which the data was collected. [See the documentation](https://developers.google.com/google-ads/api/rest/reference/rest/v18/UserList#CrmBasedUserListInfo) for more details.", + optional: true, + }, +}; + +const USER_LIST_RULE_BASED_PROPS = { + prepopulationStatus: { + type: "boolean", + label: "Request Prepopulation", + description: + "If true, past site visitors or app users who match the list definition will be included in the list. This will only add past users from within the last 30 days, depending on the list's membership duration and the date when the remarketing tag is added.", + optional: true, + }, + flexibleRuleUserList: { + type: "object", + label: "Flexible Rule Customer List", + description: + "Flexible rule representation of visitors with one or multiple actions. [See the documentation](https://developers.google.com/google-ads/api/rest/reference/rest/v18/UserList#FlexibleRuleUserListInfo) on how to build this object. Values will be parsed as JSON where applicable.", + optional: true, + }, +}; + +const USER_LIST_LOGICAL_PROPS = { + rules: { + type: "string[]", + label: "Rules", + description: + "Logical list rules that define this customer list. [See the documentation](https://developers.google.com/google-ads/api/rest/reference/rest/v18/UserList#UserListLogicalRuleInfo) on how to build each object. Values will be parsed as JSON where applicable.", + }, +}; + +const USER_LIST_BASIC_PROPS = { + conversionActions: { + type: "string[]", + label: "Conversion action(s)", + description: "One or more [conversion actions](https://developers.google.com/google-ads/api/rest/reference/rest/v18/ConversionAction) to build the list with.", + optional: true, + options: async() => { + const { + accountId, customerClientId, + } = this; + const response = await this.googleAds.listConversionActions({ + accountId, + customerClientId, + }); + return response?.map(({ + conversionAction: { + resourceName, name, + }, + }) => ({ + label: name, + value: resourceName, + })); + }, + }, + remarketingActions: { + type: "string[]", + label: "Remarketing action(s)", + description: "One or more [remarketing actions](https://developers.google.com/google-ads/api/rest/reference/rest/v18/RemarketingAction).", + optional: true, + options: async() => { + const { + accountId, customerClientId, + } = this; + const response = await this.googleAds.listRemarketingActions({ + accountId, + customerClientId, + }); + return response?.map(({ + remarketingAction: { + resourceName, name, + }, + }) => ({ + label: name, + value: resourceName, + })); + }, + }, +}; + +const USER_LIST_LOOKALIKE_PROPS = { + seedUserListIds: { + type: "string[]", + label: "Seed User List(s)", + description: "One or more customer lists from which this list is derived.", + options: async() => { + const { + accountId, customerClientId, + } = this; + const response = await this.googleAds.listUserLists({ + accountId, + customerClientId, + }); + return response?.map(({ + userList: { + id, name, + }, + }) => ({ + label: name, + value: id, + })); + }, + }, + expansionLevel: { + type: "string", + label: "Expansion Level", + description: + "Expansion level, reflecting the size of the lookalike audience", + optional: true, + options: [ + { + label: + "Expansion to a small set of users that are similar to the seed lists", + value: "NARROW", + }, + { + label: + "Expansion to a medium set of users that are similar to the seed lists. Includes all users of NARROW, and more.", + value: "BALANCED", + }, + { + label: + "Expansion to a large set of users that are similar to the seed lists. Includes all users of BALANCED, and more.", + value: "BROAD", + }, + ], + }, + countryCodes: { + type: "string[]", + label: "Country Codes", + description: "Countries targeted by the Lookalike.", + optional: true, + options: countryCodeOptions, + }, +}; + +export const USER_LIST_TYPE_OPTIONS = [ + { + label: "CRM-based - a list of provided customers", + value: USER_LIST_TYPES.CRM_BASED, + docsLink: + "https://developers.google.com/google-ads/api/rest/reference/rest/v18/UserList#CrmBasedUserListInfo", + props: USER_LIST_CRM_BASED_PROPS, + }, + { + label: "Rule-Based - a customer list generated by a rule", + value: USER_LIST_TYPES.RULE_BASED, + docsLink: + "https://developers.google.com/google-ads/api/rest/reference/rest/v18/UserList#RuleBasedUserListInfo", + props: USER_LIST_RULE_BASED_PROPS, + }, + { + label: "Logical - a custom combination of customer lists", + value: USER_LIST_TYPES.LOGICAL, + docsLink: + "https://developers.google.com/google-ads/api/rest/reference/rest/v18/UserList#LogicalUserListInfo", + props: USER_LIST_LOGICAL_PROPS, + }, + { + label: + "Basic - a customer list targeting as a collection of conversions or remarketing actions", + value: USER_LIST_TYPES.BASIC, + docsLink: + "https://developers.google.com/google-ads/api/rest/reference/rest/v18/UserList#BasicUserListInfo", + props: USER_LIST_BASIC_PROPS, + }, + { + label: + "Lookalike - a list composed of customers similar to those of a configurable seed", + value: USER_LIST_TYPES.LOOKALIKE, + docsLink: + "https://developers.google.com/google-ads/api/rest/reference/rest/v18/UserList#LookalikeUserListInfo", + props: USER_LIST_LOOKALIKE_PROPS, + }, +]; diff --git a/components/google_ads/actions/create-customer-list/common-country-code-options.mjs b/components/google_ads/actions/create-customer-list/common-country-code-options.mjs new file mode 100644 index 0000000000000..2ca6ee2576f3e --- /dev/null +++ b/components/google_ads/actions/create-customer-list/common-country-code-options.mjs @@ -0,0 +1,998 @@ +export default [ + { + label: "Andorra", + value: "AD", + }, + { + label: "United Arab Emirates", + value: "AE", + }, + { + label: "Afghanistan", + value: "AF", + }, + { + label: "Antigua and Barbuda", + value: "AG", + }, + { + label: "Anguilla", + value: "AI", + }, + { + label: "Albania", + value: "AL", + }, + { + label: "Armenia", + value: "AM", + }, + { + label: "Angola", + value: "AO", + }, + { + label: "Antarctica", + value: "AQ", + }, + { + label: "Argentina", + value: "AR", + }, + { + label: "American Samoa", + value: "AS", + }, + { + label: "Austria", + value: "AT", + }, + { + label: "Australia", + value: "AU", + }, + { + label: "Aruba", + value: "AW", + }, + { + label: "Åland Islands", + value: "AX", + }, + { + label: "Azerbaijan", + value: "AZ", + }, + { + label: "Bosnia and Herzegovina", + value: "BA", + }, + { + label: "Barbados", + value: "BB", + }, + { + label: "Bangladesh", + value: "BD", + }, + { + label: "Belgium", + value: "BE", + }, + { + label: "Burkina Faso", + value: "BF", + }, + { + label: "Bulgaria", + value: "BG", + }, + { + label: "Bahrain", + value: "BH", + }, + { + label: "Burundi", + value: "BI", + }, + { + label: "Benin", + value: "BJ", + }, + { + label: "Saint Barthélemy", + value: "BL", + }, + { + label: "Bermuda", + value: "BM", + }, + { + label: "Brunei Darussalam", + value: "BN", + }, + { + label: "Bolivia, Plurinational State of", + value: "BO", + }, + { + label: "Bonaire, Sint Eustatius and Saba", + value: "BQ", + }, + { + label: "Brazil", + value: "BR", + }, + { + label: "Bahamas", + value: "BS", + }, + { + label: "Bhutan", + value: "BT", + }, + { + label: "Bouvet Island", + value: "BV", + }, + { + label: "Botswana", + value: "BW", + }, + { + label: "Belarus", + value: "BY", + }, + { + label: "Belize", + value: "BZ", + }, + { + label: "Canada", + value: "CA", + }, + { + label: "Cocos (Keeling) Islands", + value: "CC", + }, + { + label: "Congo, Democratic Republic of the", + value: "CD", + }, + { + label: "Central African Republic", + value: "CF", + }, + { + label: "Congo", + value: "CG", + }, + { + label: "Switzerland", + value: "CH", + }, + { + label: "Côte d'Ivoire", + value: "CI", + }, + { + label: "Cook Islands", + value: "CK", + }, + { + label: "Chile", + value: "CL", + }, + { + label: "Cameroon", + value: "CM", + }, + { + label: "China", + value: "CN", + }, + { + label: "Colombia", + value: "CO", + }, + { + label: "Costa Rica", + value: "CR", + }, + { + label: "Cuba", + value: "CU", + }, + { + label: "Cabo Verde", + value: "CV", + }, + { + label: "Curaçao", + value: "CW", + }, + { + label: "Christmas Island", + value: "CX", + }, + { + label: "Cyprus", + value: "CY", + }, + { + label: "Czechia", + value: "CZ", + }, + { + label: "Germany", + value: "DE", + }, + { + label: "Djibouti", + value: "DJ", + }, + { + label: "Denmark", + value: "DK", + }, + { + label: "Dominica", + value: "DM", + }, + { + label: "Dominican Republic", + value: "DO", + }, + { + label: "Algeria", + value: "DZ", + }, + { + label: "Ecuador", + value: "EC", + }, + { + label: "Estonia", + value: "EE", + }, + { + label: "Egypt", + value: "EG", + }, + { + label: "Western Sahara", + value: "EH", + }, + { + label: "Eritrea", + value: "ER", + }, + { + label: "Spain", + value: "ES", + }, + { + label: "Ethiopia", + value: "ET", + }, + { + label: "Finland", + value: "FI", + }, + { + label: "Fiji", + value: "FJ", + }, + { + label: "Falkland Islands (Malvinas)", + value: "FK", + }, + { + label: "Micronesia, Federated States of", + value: "FM", + }, + { + label: "Faroe Islands", + value: "FO", + }, + { + label: "France", + value: "FR", + }, + { + label: "Gabon", + value: "GA", + }, + { + label: "United Kingdom of Great Britain and Northern Ireland", + value: "GB", + }, + { + label: "Grenada", + value: "GD", + }, + { + label: "Georgia", + value: "GE", + }, + { + label: "French Guiana", + value: "GF", + }, + { + label: "Guernsey", + value: "GG", + }, + { + label: "Ghana", + value: "GH", + }, + { + label: "Gibraltar", + value: "GI", + }, + { + label: "Greenland", + value: "GL", + }, + { + label: "Gambia", + value: "GM", + }, + { + label: "Guinea", + value: "GN", + }, + { + label: "Guadeloupe", + value: "GP", + }, + { + label: "Equatorial Guinea", + value: "GQ", + }, + { + label: "Greece", + value: "GR", + }, + { + label: "South Georgia and the South Sandwich Islands", + value: "GS", + }, + { + label: "Guatemala", + value: "GT", + }, + { + label: "Guam", + value: "GU", + }, + { + label: "Guinea-Bissau", + value: "GW", + }, + { + label: "Guyana", + value: "GY", + }, + { + label: "Hong Kong", + value: "HK", + }, + { + label: "Heard Island and McDonald Islands", + value: "HM", + }, + { + label: "Honduras", + value: "HN", + }, + { + label: "Croatia", + value: "HR", + }, + { + label: "Haiti", + value: "HT", + }, + { + label: "Hungary", + value: "HU", + }, + { + label: "Indonesia", + value: "ID", + }, + { + label: "Ireland", + value: "IE", + }, + { + label: "Israel", + value: "IL", + }, + { + label: "Isle of Man", + value: "IM", + }, + { + label: "India", + value: "IN", + }, + { + label: "British Indian Ocean Territory", + value: "IO", + }, + { + label: "Iraq", + value: "IQ", + }, + { + label: "Iran, Islamic Republic of", + value: "IR", + }, + { + label: "Iceland", + value: "IS", + }, + { + label: "Italy", + value: "IT", + }, + { + label: "Jersey", + value: "JE", + }, + { + label: "Jamaica", + value: "JM", + }, + { + label: "Jordan", + value: "JO", + }, + { + label: "Japan", + value: "JP", + }, + { + label: "Kenya", + value: "KE", + }, + { + label: "Kyrgyzstan", + value: "KG", + }, + { + label: "Cambodia", + value: "KH", + }, + { + label: "Kiribati", + value: "KI", + }, + { + label: "Comoros", + value: "KM", + }, + { + label: "Saint Kitts and Nevis", + value: "KN", + }, + { + label: "Korea, Democratic People's Republic of", + value: "KP", + }, + { + label: "Korea, Republic of", + value: "KR", + }, + { + label: "Kuwait", + value: "KW", + }, + { + label: "Cayman Islands", + value: "KY", + }, + { + label: "Kazakhstan", + value: "KZ", + }, + { + label: "Lao People's Democratic Republic", + value: "LA", + }, + { + label: "Lebanon", + value: "LB", + }, + { + label: "Saint Lucia", + value: "LC", + }, + { + label: "Liechtenstein", + value: "LI", + }, + { + label: "Sri Lanka", + value: "LK", + }, + { + label: "Liberia", + value: "LR", + }, + { + label: "Lesotho", + value: "LS", + }, + { + label: "Lithuania", + value: "LT", + }, + { + label: "Luxembourg", + value: "LU", + }, + { + label: "Latvia", + value: "LV", + }, + { + label: "Libya", + value: "LY", + }, + { + label: "Morocco", + value: "MA", + }, + { + label: "Monaco", + value: "MC", + }, + { + label: "Moldova, Republic of", + value: "MD", + }, + { + label: "Montenegro", + value: "ME", + }, + { + label: "Saint Martin (French part)", + value: "MF", + }, + { + label: "Madagascar", + value: "MG", + }, + { + label: "Marshall Islands", + value: "MH", + }, + { + label: "North Macedonia", + value: "MK", + }, + { + label: "Mali", + value: "ML", + }, + { + label: "Myanmar", + value: "MM", + }, + { + label: "Mongolia", + value: "MN", + }, + { + label: "Macao", + value: "MO", + }, + { + label: "Northern Mariana Islands", + value: "MP", + }, + { + label: "Martinique", + value: "MQ", + }, + { + label: "Mauritania", + value: "MR", + }, + { + label: "Montserrat", + value: "MS", + }, + { + label: "Malta", + value: "MT", + }, + { + label: "Mauritius", + value: "MU", + }, + { + label: "Maldives", + value: "MV", + }, + { + label: "Malawi", + value: "MW", + }, + { + label: "Mexico", + value: "MX", + }, + { + label: "Malaysia", + value: "MY", + }, + { + label: "Mozambique", + value: "MZ", + }, + { + label: "Namibia", + value: "NA", + }, + { + label: "New Caledonia", + value: "NC", + }, + { + label: "Niger", + value: "NE", + }, + { + label: "Norfolk Island", + value: "NF", + }, + { + label: "Nigeria", + value: "NG", + }, + { + label: "Nicaragua", + value: "NI", + }, + { + label: "Netherlands, Kingdom of the", + value: "NL", + }, + { + label: "Norway", + value: "NO", + }, + { + label: "Nepal", + value: "NP", + }, + { + label: "Nauru", + value: "NR", + }, + { + label: "Niue", + value: "NU", + }, + { + label: "New Zealand", + value: "NZ", + }, + { + label: "Oman", + value: "OM", + }, + { + label: "Panama", + value: "PA", + }, + { + label: "Peru", + value: "PE", + }, + { + label: "French Polynesia", + value: "PF", + }, + { + label: "Papua New Guinea", + value: "PG", + }, + { + label: "Philippines", + value: "PH", + }, + { + label: "Pakistan", + value: "PK", + }, + { + label: "Poland", + value: "PL", + }, + { + label: "Saint Pierre and Miquelon", + value: "PM", + }, + { + label: "Pitcairn", + value: "PN", + }, + { + label: "Puerto Rico", + value: "PR", + }, + { + label: "Palestine, State of", + value: "PS", + }, + { + label: "Portugal", + value: "PT", + }, + { + label: "Palau", + value: "PW", + }, + { + label: "Paraguay", + value: "PY", + }, + { + label: "Qatar", + value: "QA", + }, + { + label: "Réunion", + value: "RE", + }, + { + label: "Romania", + value: "RO", + }, + { + label: "Serbia", + value: "RS", + }, + { + label: "Russian Federation", + value: "RU", + }, + { + label: "Rwanda", + value: "RW", + }, + { + label: "Saudi Arabia", + value: "SA", + }, + { + label: "Solomon Islands", + value: "SB", + }, + { + label: "Seychelles", + value: "SC", + }, + { + label: "Sudan", + value: "SD", + }, + { + label: "Sweden", + value: "SE", + }, + { + label: "Singapore", + value: "SG", + }, + { + label: "Saint Helena, Ascension and Tristan da Cunha", + value: "SH", + }, + { + label: "Slovenia", + value: "SI", + }, + { + label: "Svalbard and Jan Mayen", + value: "SJ", + }, + { + label: "Slovakia", + value: "SK", + }, + { + label: "Sierra Leone", + value: "SL", + }, + { + label: "San Marino", + value: "SM", + }, + { + label: "Senegal", + value: "SN", + }, + { + label: "Somalia", + value: "SO", + }, + { + label: "Suriname", + value: "SR", + }, + { + label: "South Sudan", + value: "SS", + }, + { + label: "Sao Tome and Principe", + value: "ST", + }, + { + label: "El Salvador", + value: "SV", + }, + { + label: "Sint Maarten (Dutch part)", + value: "SX", + }, + { + label: "Syrian Arab Republic", + value: "SY", + }, + { + label: "Eswatini", + value: "SZ", + }, + { + label: "Turks and Caicos Islands", + value: "TC", + }, + { + label: "Chad", + value: "TD", + }, + { + label: "French Southern Territories", + value: "TF", + }, + { + label: "Togo", + value: "TG", + }, + { + label: "Thailand", + value: "TH", + }, + { + label: "Tajikistan", + value: "TJ", + }, + { + label: "Tokelau", + value: "TK", + }, + { + label: "Timor-Leste", + value: "TL", + }, + { + label: "Turkmenistan", + value: "TM", + }, + { + label: "Tunisia", + value: "TN", + }, + { + label: "Tonga", + value: "TO", + }, + { + label: "Türkiye", + value: "TR", + }, + { + label: "Trinidad and Tobago", + value: "TT", + }, + { + label: "Tuvalu", + value: "TV", + }, + { + label: "Taiwan, Province of China", + value: "TW", + }, + { + label: "Tanzania, United Republic of", + value: "TZ", + }, + { + label: "Ukraine", + value: "UA", + }, + { + label: "Uganda", + value: "UG", + }, + { + label: "United States Minor Outlying Islands", + value: "UM", + }, + { + label: "United States of America", + value: "US", + }, + { + label: "Uruguay", + value: "UY", + }, + { + label: "Uzbekistan", + value: "UZ", + }, + { + label: "Holy See", + value: "VA", + }, + { + label: "Saint Vincent and the Grenadines", + value: "VC", + }, + { + label: "Venezuela, Bolivarian Republic of", + value: "VE", + }, + { + label: "Virgin Islands (British)", + value: "VG", + }, + { + label: "Virgin Islands (U.S.)", + value: "VI", + }, + { + label: "Viet Nam", + value: "VN", + }, + { + label: "Vanuatu", + value: "VU", + }, + { + label: "Wallis and Futuna", + value: "WF", + }, + { + label: "Samoa", + value: "WS", + }, + { + label: "Yemen", + value: "YE", + }, + { + label: "Mayotte", + value: "YT", + }, + { + label: "South Africa", + value: "ZA", + }, + { + label: "Zambia", + value: "ZM", + }, + { + label: "Zimbabwe", + value: "ZW", + }, +]; diff --git a/components/google_ads/actions/create-customer-list/create-customer-list.mjs b/components/google_ads/actions/create-customer-list/create-customer-list.mjs new file mode 100644 index 0000000000000..ea88147d781ed --- /dev/null +++ b/components/google_ads/actions/create-customer-list/create-customer-list.mjs @@ -0,0 +1,161 @@ +import { + USER_LIST_TYPES, USER_LIST_TYPE_OPTIONS, +} from "./common-constants.mjs"; +import { + parseObject, parseStringObject, +} from "../../common/utils.mjs"; +import common from "../common/common.mjs"; +import { + getAdditionalFields, getListTypeInfo, +} from "../common/props.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + ...common, + key: "google_ads-create-customer-list", + name: "Create Customer List", + description: "Create a new customer list in Google Ads. [See the documentation](https://developers.google.com/google-ads/api/rest/reference/rest/v18/UserList)", + version: "0.0.3", + type: "action", + props: { + ...common.props, + name: { + type: "string", + label: "Name", + description: "The name of the customer list.", + }, + description: { + type: "string", + label: "Description", + description: "Description of the customer list.", + optional: true, + }, + listType: { + type: "string", + label: "List Type", + description: "The [type of customer list](https://developers.google.com/google-ads/api/rest/reference/rest/v18/UserList#CrmBasedUserListInfo) to create.", + options: USER_LIST_TYPE_OPTIONS.map(({ + label, value, + }) => ({ + label, + value, + })), + reloadProps: true, + }, + }, + methods: { + parseFields(obj) { + switch (this.listType) { + case USER_LIST_TYPES.CRM_BASED: + break; + + case USER_LIST_TYPES.RULE_BASED: + if (obj.prepopulationStatus) { + obj.prepopulationStatus = "REQUESTED"; + } + if (obj.flexibleRuleUserList) { + obj.flexibleRuleUserList = parseObject(obj.flexibleRuleUserList); + } + break; + + case USER_LIST_TYPES.LOGICAL: { + let { rules } = obj; + if (rules) { + rules = rules.map?.((rule) => parseStringObject(rule)) ?? parseStringObject(rules); + } + break; + } + + case USER_LIST_TYPES.BASIC: + if (obj?.conversionActions?.length || obj?.remarketingActions?.length) { + obj.actions = [ + ...(obj.conversionActions ?? []).map((conversionAction) => ({ + conversionAction, + })), + ...(obj.remarketingActions ?? []).map((remarketingAction) => ({ + remarketingAction, + })), + ]; + delete obj.conversionActions; + delete obj.remarketingActions; + } else { + throw new ConfigurationError("Select at least one Conversion or Remarketing action to build the list with"); + } + break; + + case USER_LIST_TYPES.LOOKALIKE: + break; + } + + return obj; + }, + }, + additionalProps() { + const { listType } = this; + + if (!listType) { + return {}; + } + + const option = USER_LIST_TYPE_OPTIONS.find( + ({ value }) => value === listType, + ); + if (!option) { + throw new ConfigurationError("Select a valid List Type to proceed."); + } + + const { + docsLink, props, + } = option; + + const newProps = { + listTypeInfo: getListTypeInfo(docsLink), + }; + + Object.assign(newProps, props); + + newProps.additionalFields = getAdditionalFields(docsLink); + + return newProps; + }, + async run({ $ }) { + const { + googleAds, + accountId, + customerClientId, + name, + description, + listType, + additionalFields, + ...data + } = this; + const { results: { [0]: response } } = await googleAds.createUserList({ + $, + accountId, + customerClientId, + data: { + operations: [ + { + create: { + name, + description, + [listType]: this.parseFields(data), + ...parseObject(additionalFields), + }, + }, + ], + }, + }); + + const id = response.resourceName.split("/").pop(); + + $.export( + "$summary", + `Created customer list of type \`${listType}\` with ID \`${id}\``, + ); + return { + id, + ...response, + }; + }, +}; diff --git a/components/google_ads/actions/create-report/common-constants.mjs b/components/google_ads/actions/create-report/common-constants.mjs new file mode 100644 index 0000000000000..e0b2ee19d7a54 --- /dev/null +++ b/components/google_ads/actions/create-report/common-constants.mjs @@ -0,0 +1,55 @@ +export const DATE_RANGE_OPTIONS = [ + { + value: "CUSTOM", + label: "Specify a custom date range", + }, + { + value: "TODAY", + label: "Today only", + }, + { + value: "YESTERDAY", + label: "Yesterday only", + }, + { + value: "LAST_7_DAYS", + label: "The last 7 days not including today", + }, + { + value: "LAST_BUSINESS_WEEK", + label: + "The 5 day business week, Monday through Friday, of the previous business week", + }, + { + value: "THIS_MONTH", + label: "All days in the current month", + }, + { + value: "LAST_MONTH", + label: "All days in the previous month", + }, + { + value: "LAST_14_DAYS", + label: "The last 14 days not including today", + }, + { + value: "LAST_30_DAYS", + label: "The last 30 days not including today", + }, + { + value: "THIS_WEEK_SUN_TODAY", + label: "The period between the previous Sunday and the current day", + }, + { + value: "THIS_WEEK_MON_TODAY", + label: "The period between the previous Monday and the current day", + }, + { + value: "LAST_WEEK_SUN_SAT", + label: "The 7-day period starting with the previous Sunday", + }, + { + value: "LAST_WEEK_MON_SUN", + label: "The 7-day period starting with the previous Monday", + }, +]; diff --git a/components/google_ads/actions/create-report/create-report.mjs b/components/google_ads/actions/create-report/create-report.mjs new file mode 100644 index 0000000000000..6e33899d17d05 --- /dev/null +++ b/components/google_ads/actions/create-report/create-report.mjs @@ -0,0 +1,266 @@ +import common from "../common/common.mjs"; +import { adGroup } from "../../common/resources/adGroup.mjs"; +import { ad } from "../../common/resources/ad.mjs"; +import { campaign } from "../../common/resources/campaign.mjs"; +import { customer } from "../../common/resources/customer.mjs"; +import { ConfigurationError } from "@pipedream/platform"; +import { DATE_RANGE_OPTIONS } from "./common-constants.mjs"; +import { checkPrefix } from "../../common/utils.mjs"; + +const RESOURCES = [ + adGroup, + ad, + campaign, + customer, +]; + +export default { + ...common, + key: "google_ads-create-report", + name: "Create Report", + description: "Generates a report from your Google Ads data. [See the documentation](https://developers.google.com/google-ads/api/fields/v18/overview)", + version: "0.1.1", + type: "action", + props: { + ...common.props, + resource: { + type: "string", + label: "Resource", + description: "The resource to generate a report for.", + options: RESOURCES.map((r) => r.resourceOption), + reloadProps: true, + }, + }, + additionalProps() { + const resource = RESOURCES.find((r) => r.resourceOption.value === this.resource); + if (!resource) throw new ConfigurationError("Select one of the available resources."); + + const { + label, value, + } = resource.resourceOption; + + return { + docsAlert: { + type: "alert", + alertType: "info", + content: `[See the documentation](https://developers.google.com/google-ads/api/fields/v18/${value}) for more information on available fields, segments and metrics.`, + }, + objectFilter: { + type: "string[]", + label: `Filter by ${label}s`, + description: `Select the ${label}s to generate a report for (or leave blank for all ${label}s)`, + optional: true, + useQuery: true, + options: async ({ + query, prevContext: { nextPageToken: pageToken }, + }) => { + const { + accountId, customerClientId, resource, + } = this; + const { + results, nextPageToken, + } = await this.googleAds.listResources({ + accountId, + customerClientId, + resource, + query, + pageToken, + }); + const options = results?.map?.((item) => this.getResourceOption(item, resource)); + return { + options, + context: { + nextPageToken, + }, + }; + }, + }, + dateRange: { + type: "string", + label: "Date Range", + description: "Select a date range for the report", + options: DATE_RANGE_OPTIONS, + optional: true, + reloadProps: true, + }, + ...(this.dateRange === "CUSTOM" && { + startDate: { + type: "string", + label: "Start Date", + description: "The start date, in `YYYY-MM-DD` format", + }, + endDate: { + type: "string", + label: "End Date", + description: "The end date, in `YYYY-MM-DD` format", + }, + }), + fields: { + type: "string[]", + label: "Fields", + description: "Select any fields you want to include in your report.", + options: resource.fields, + optional: true, + reloadProps: true, + }, + segments: { + type: "string[]", + label: "Segments", + description: "Select any segments you want to include in your report. See the documentation [here](https://developers.google.com/google-ads/api/fields/v18/segments)", + options: resource.segments, + default: [ + "segments.date", + ], + optional: true, + reloadProps: true, + }, + metrics: { + type: "string[]", + label: "Metrics", + description: "Select any metrics you want to include in your report. See the documentation [here](https://developers.google.com/google-ads/api/fields/v18/metrics)", + options: resource.metrics, + optional: true, + reloadProps: true, + }, + orderBy: { + type: "string", + label: "Order By", + description: "The field to order the results by", + optional: true, + options: [ + this.fields, + this.segments, + this.metrics, + ].filter((v) => v).flatMap((value) => { + let returnValue = value; + if (typeof value === "string") { + try { + returnValue = JSON.parse(value); + } catch (err) { + returnValue = value.split(","); + } + } + return returnValue?.map?.((str) => str.trim()); + }), + }, + direction: { + type: "string", + label: "Direction", + description: "The direction to order the results by, if `Order By` is specified", + optional: true, + options: [ + { + label: "Ascending", + value: "ASC", + }, + { + label: "Descending", + value: "DESC", + }, + ], + default: "ASC", + }, + limit: { + type: "integer", + label: "Limit", + description: "The maximum number of results to return", + optional: true, + }, + }; + }, + methods: { + getResourceOption(item, resource) { + let label, value; + switch (resource) { + case "campaign": + label = item.campaign.name; + value = item.campaign.id; + break; + + case "customer": + label = item.customer.descriptiveName; + value = item.customer.id; + break; + + case "ad_group": + label = item.adGroup.name; + value = item.adGroup.id; + break; + + case "ad_group_ad": + label = item.adGroupAd.ad.name; + value = item.adGroupAd.ad.id; + break; + } + + return { + label, + value, + }; + }, + buildQuery() { + const { + resource, fields, segments, metrics, limit, orderBy, direction, objectFilter, dateRange, + } = this; + + const filteredSegments = dateRange + ? segments + : segments?.filter((i) => i !== "segments.date"); + + const selection = [ + ...checkPrefix(fields, resource), + ...checkPrefix(filteredSegments, "segments"), + ...checkPrefix(metrics, "metrics"), + ]; + + if (!selection.length) { + throw new ConfigurationError("Select at least one field, segment or metric."); + } + + let query = `SELECT ${selection.join(", ")} FROM ${resource}`; + if (objectFilter) { + query += ` WHERE ${resource === "ad_group_ad" + ? "ad_group_ad.ad" + : resource}.id IN (${objectFilter.join?.(", ") ?? objectFilter})`; + } + if (dateRange) { + const dateClause = dateRange === "CUSTOM" + ? `BETWEEN '${this.startDate}' AND '${this.endDate}'` + : `DURING ${dateRange}`; + query += ` ${objectFilter + ? "AND" + : "WHERE"} segments.date ${dateClause}`; + } + + if (orderBy && direction) { + query += ` ORDER BY ${orderBy} ${direction}`; + } + if (limit) { + query += ` LIMIT ${limit}`; + } + + return query; + }, + }, + async run({ $ }) { + const query = this.buildQuery(); + const results = (await this.googleAds.createReport({ + $, + accountId: this.accountId, + customerClientId: this.customerClientId, + data: { + query, + }, + })) ?? []; + + const { length } = results; + + $.export("$summary", `Successfully obtained ${length} result${length === 1 + ? "" + : "s"}`); + return { + query, + results, + }; + }, +}; diff --git a/components/google_ads/actions/send-offline-conversion/common-constants.mjs b/components/google_ads/actions/send-offline-conversion/common-constants.mjs new file mode 100644 index 0000000000000..1c2bc84e3d31a --- /dev/null +++ b/components/google_ads/actions/send-offline-conversion/common-constants.mjs @@ -0,0 +1,40 @@ +export const CONVERSION_TYPE_OPTIONS = [ + { + label: + "Conversions that occur when a user clicks on an ad's call extension.", + value: "AD_CALL", + }, + { + label: + "Conversions that occur when a user on a mobile device clicks a phone number.", + value: "CLICK_TO_CALL", + }, + { + label: + "Conversions that occur when a user downloads a mobile app from the Google Play Store.", + value: "GOOGLE_PLAY_DOWNLOAD", + }, + { + label: + "Conversions that occur when a user makes a purchase in an app through Android billing.", + value: "GOOGLE_PLAY_IN_APP_PURCHASE", + }, + { + label: "Call conversions that are tracked by the advertiser and uploaded.", + value: "UPLOAD_CALLS", + }, + { + label: + "Conversions that are tracked by the advertiser and uploaded with attributed clicks.", + value: "UPLOAD_CLICKS", + }, + { + label: "Conversions that occur on a webpage.", + value: "WEBPAGE", + }, + { + label: + "Conversions that occur when a user calls a dynamically-generated phone number from an advertiser's website.", + value: "WEBSITE_CALL", + }, +]; diff --git a/components/google_ads/actions/send-offline-conversion/send-offline-conversion.mjs b/components/google_ads/actions/send-offline-conversion/send-offline-conversion.mjs new file mode 100644 index 0000000000000..86078f6c465e0 --- /dev/null +++ b/components/google_ads/actions/send-offline-conversion/send-offline-conversion.mjs @@ -0,0 +1,56 @@ +import { parseObject } from "../../common/utils.mjs"; +import common from "../common/common.mjs"; +import { getAdditionalFields } from "../common/props.mjs"; +import { CONVERSION_TYPE_OPTIONS } from "./common-constants.mjs"; + +export default { + ...common, + key: "google_ads-send-offline-conversion", + name: "Send Offline Conversion", + description: "Send an event from to Google Ads to track offline conversions. [See the documentation](https://developers.google.com/google-ads/api/rest/reference/rest/v18/ConversionAction)", + version: "0.0.3", + type: "action", + props: { + ...common.props, + name: { + type: "string", + label: "Name", + description: "The name of the conversion action.", + }, + type: { + type: "string", + label: "Type", + description: "[The type](https://developers.google.com/google-ads/api/rest/reference/rest/v18/ConversionAction#ConversionActionType) of the conversion action.", + options: CONVERSION_TYPE_OPTIONS, + }, + additionalFields: getAdditionalFields("https://developers.google.com/google-ads/api/rest/reference/rest/v18/ConversionAction"), + }, + async run({ $ }) { + const { + googleAds, accountId, customerClientId, additionalFields, ...data + } = this; + const { results: { [0]: response } } = await googleAds.createConversionAction({ + $, + accountId, + customerClientId, + data: { + operations: [ + { + create: { + ...data, + ...parseObject(additionalFields), + }, + }, + ], + }, + }); + + const id = response.resourceName.split("/").pop(); + + $.export("$summary", `Created conversion action with ID ${id}`); + return { + id, + ...response, + }; + }, +}; diff --git a/components/google_ads/common/props.mjs b/components/google_ads/common/props.mjs new file mode 100644 index 0000000000000..b7ac950069cf5 --- /dev/null +++ b/components/google_ads/common/props.mjs @@ -0,0 +1,20 @@ +import googleAds from "../google_ads.app.mjs"; + +export default { + googleAds, + accountId: { + propDefinition: [ + googleAds, + "accountId", + ], + }, + customerClientId: { + propDefinition: [ + googleAds, + "customerClientId", + ({ accountId }) => ({ + accountId, + }), + ], + }, +}; diff --git a/components/google_ads/common/queries.mjs b/components/google_ads/common/queries.mjs new file mode 100644 index 0000000000000..4abf7ff79390b --- /dev/null +++ b/components/google_ads/common/queries.mjs @@ -0,0 +1,122 @@ +function listCustomerClients(query) { + const fields = [ + "client_customer", + "descriptive_name", + "id", + "level", + "manager", + ] + .map((s) => `customer_client.${s}`) + .join(", "); + + const condition = query + ? `customer_client.descriptive_name LIKE '%${query}%'` + : "customer_client.level <= 3"; + + return `SELECT ${fields} FROM customer_client WHERE ${condition}`; +} + +function listUserLists() { + const fields = [ + "id", + "name", + "type", + ].map((s) => `user_list.${s}`).join(", "); + + return `SELECT ${fields} FROM user_list`; +} + +function listConversionActions() { + const fields = [ + "name", + ].map((s) => `conversion_action.${s}`).join(", "); + + return `SELECT ${fields} FROM conversion_action`; +} + +function listRemarketingActions() { + const fields = [ + "name", + ].map((s) => `remarketing_action.${s}`).join(", "); + + return `SELECT ${fields} FROM remarketing_action`; +} + +function listLeadForms() { + const assetFields = [ + "id", + ].map(((s) => `asset.${s}`)); + const leadFormFields = [ + "business_name", + "headline", + ].map((s) => `asset.lead_form_asset.${s}`); + + return `SELECT ${[ + ...assetFields, + ...leadFormFields, + ].join(", ")} FROM asset WHERE asset.type = 'LEAD_FORM'`; +} + +function listLeadFormSubmissionData(id) { + const fields = [ + "custom_lead_form_submission_fields", + "gclid", + "id", + "lead_form_submission_fields", + "submission_date_time", + ].map((s) => `lead_form_submission_data.${s}`).join(", "); + return `SELECT ${fields} FROM lead_form_submission_data WHERE asset.id = '${id}'`; +} + +function listCampaigns({ + fields, savedIds, +}) { + const defaultFields = [ + "id", + "name", + ].map((s) => `campaign.${s}`); + if (typeof fields === "string") { + fields = fields.split(",").map((s) => s.trim()); + } + if (!fields?.length) { + fields = defaultFields; + } else { + defaultFields.forEach((f) => { + if (!fields.includes(f)) { + fields.push(f); + } + }); + } + + const filter = savedIds?.length + ? ` WHERE ${savedIds.map((id) => `campaign.id != ${id}`).join(" AND ")}` + : ""; + + return `SELECT ${fields.join(", ")} FROM campaign${filter}`; +} + +function listResources(resource, query) { + const name = resource === "customer" + ? "descriptive_name" + : "name"; + const fieldResource = resource === "ad_group_ad" + ? "ad_group_ad.ad" + : resource; + + let result = `SELECT ${fieldResource}.id, ${fieldResource}.${name} FROM ${resource}`; + if (query) { + result += ` WHERE ${fieldResource}.${name} LIKE '%${query}%'`; + } + return result; +} + +export const QUERIES = { + listCampaigns, + listConversionActions, + listCustomerClients, + listLeadForms, + listLeadFormSubmissionData, + listRemarketingActions, + listResources, + listUserLists, +}; diff --git a/components/google_ads/common/resources/ad.mjs b/components/google_ads/common/resources/ad.mjs new file mode 100644 index 0000000000000..586d9394c716e --- /dev/null +++ b/components/google_ads/common/resources/ad.mjs @@ -0,0 +1,330 @@ +import { getOption } from "../utils.mjs"; + +const fields = [ + "action_items", + "ad.added_by_google_ads", + "ad.app_ad.descriptions", + "ad.app_ad.headlines", + "ad.app_ad.html5_media_bundles", + "ad.app_ad.html5_media_bundles.asset", + "ad.app_ad.images", + "ad.app_ad.images.asset", + "ad.app_ad.mandatory_ad_text", + "ad.app_ad.youtube_videos", + "ad.app_ad.youtube_videos.asset", + "ad.app_engagement_ad.descriptions", + "ad.app_engagement_ad.headlines", + "ad.app_engagement_ad.images", + "ad.app_engagement_ad.images.asset", + "ad.app_engagement_ad.videos", + "ad.app_engagement_ad.videos.asset", + "ad.app_pre_registration_ad.descriptions", + "ad.app_pre_registration_ad.headlines", + "ad.app_pre_registration_ad.images", + "ad.app_pre_registration_ad.images.asset", + "ad.app_pre_registration_ad.youtube_videos", + "ad.app_pre_registration_ad.youtube_videos.asset", + "ad.call_ad.business_name", + "ad.call_ad.call_tracked", + "ad.call_ad.conversion_action", + "ad.call_ad.conversion_reporting_state", + "ad.call_ad.country_code", + "ad.call_ad.description1", + "ad.call_ad.description2", + "ad.call_ad.disable_call_conversion", + "ad.call_ad.headline1", + "ad.call_ad.headline2", + "ad.call_ad.path1", + "ad.call_ad.path2", + "ad.call_ad.phone_number", + "ad.call_ad.phone_number_verification_url", + "ad.demand_gen_product_ad.breadcrumb1", + "ad.demand_gen_product_ad.breadcrumb2", + "ad.demand_gen_product_ad.business_name", + "ad.demand_gen_product_ad.call_to_action", + "ad.demand_gen_product_ad.call_to_action.asset", + "ad.demand_gen_product_ad.description", + "ad.demand_gen_product_ad.headline", + "ad.demand_gen_product_ad.logo_image", + "ad.demand_gen_product_ad.logo_image.asset", + "ad.device_preference", + "ad.discovery_carousel_ad.business_name", + "ad.discovery_carousel_ad.call_to_action_text", + "ad.discovery_carousel_ad.carousel_cards", + "ad.discovery_carousel_ad.carousel_cards.asset", + "ad.discovery_carousel_ad.description", + "ad.discovery_carousel_ad.headline", + "ad.discovery_carousel_ad.logo_image", + "ad.discovery_carousel_ad.logo_image.asset", + "ad.discovery_multi_asset_ad.business_name", + "ad.discovery_multi_asset_ad.call_to_action_text", + "ad.discovery_multi_asset_ad.descriptions", + "ad.discovery_multi_asset_ad.headlines", + "ad.discovery_multi_asset_ad.lead_form_only", + "ad.discovery_multi_asset_ad.logo_images", + "ad.discovery_multi_asset_ad.logo_images.asset", + "ad.discovery_multi_asset_ad.marketing_images", + "ad.discovery_multi_asset_ad.marketing_images.asset", + "ad.discovery_multi_asset_ad.portrait_marketing_images", + "ad.discovery_multi_asset_ad.portrait_marketing_images.asset", + "ad.discovery_multi_asset_ad.square_marketing_images", + "ad.discovery_multi_asset_ad.square_marketing_images.asset", + "ad.discovery_video_responsive_ad.breadcrumb1", + "ad.discovery_video_responsive_ad.breadcrumb2", + "ad.discovery_video_responsive_ad.business_name", + "ad.discovery_video_responsive_ad.call_to_actions", + "ad.discovery_video_responsive_ad.call_to_actions.asset", + "ad.discovery_video_responsive_ad.descriptions", + "ad.discovery_video_responsive_ad.headlines", + "ad.discovery_video_responsive_ad.logo_images", + "ad.discovery_video_responsive_ad.logo_images.asset", + "ad.discovery_video_responsive_ad.long_headlines", + "ad.discovery_video_responsive_ad.videos", + "ad.discovery_video_responsive_ad.videos.asset", + "ad.display_upload_ad.display_upload_product_type", + "ad.display_upload_ad.media_bundle", + "ad.display_upload_ad.media_bundle.asset", + "ad.display_url", + "ad.expanded_dynamic_search_ad.description", + "ad.expanded_dynamic_search_ad.description2", + "ad.expanded_text_ad.description", + "ad.expanded_text_ad.description2", + "ad.expanded_text_ad.headline_part1", + "ad.expanded_text_ad.headline_part2", + "ad.expanded_text_ad.headline_part3", + "ad.expanded_text_ad.path1", + "ad.expanded_text_ad.path2", + "ad.final_app_urls", + "ad.final_mobile_urls", + "ad.final_url_suffix", + "ad.final_urls", + "ad.hotel_ad", + "ad.id", + "ad.image_ad.image_asset.asset", + "ad.image_ad.image_url", + "ad.image_ad.mime_type", + "ad.image_ad.name", + "ad.image_ad.pixel_height", + "ad.image_ad.pixel_width", + "ad.image_ad.preview_image_url", + "ad.image_ad.preview_pixel_height", + "ad.image_ad.preview_pixel_width", + "ad.legacy_app_install_ad", + "ad.legacy_responsive_display_ad.accent_color", + "ad.legacy_responsive_display_ad.allow_flexible_color", + "ad.legacy_responsive_display_ad.business_name", + "ad.legacy_responsive_display_ad.call_to_action_text", + "ad.legacy_responsive_display_ad.description", + "ad.legacy_responsive_display_ad.format_setting", + "ad.legacy_responsive_display_ad.logo_image", + "ad.legacy_responsive_display_ad.long_headline", + "ad.legacy_responsive_display_ad.main_color", + "ad.legacy_responsive_display_ad.marketing_image", + "ad.legacy_responsive_display_ad.price_prefix", + "ad.legacy_responsive_display_ad.promo_text", + "ad.legacy_responsive_display_ad.short_headline", + "ad.legacy_responsive_display_ad.square_logo_image", + "ad.legacy_responsive_display_ad.square_marketing_image", + "ad.local_ad.call_to_actions", + "ad.local_ad.descriptions", + "ad.local_ad.headlines", + "ad.local_ad.logo_images", + "ad.local_ad.logo_images.asset", + "ad.local_ad.marketing_images", + "ad.local_ad.marketing_images.asset", + "ad.local_ad.path1", + "ad.local_ad.path2", + "ad.local_ad.videos", + "ad.local_ad.videos.asset", + "ad.name", + "ad.resource_name", + "ad.responsive_display_ad.accent_color", + "ad.responsive_display_ad.allow_flexible_color", + "ad.responsive_display_ad.business_name", + "ad.responsive_display_ad.call_to_action_text", + "ad.responsive_display_ad.control_spec.enable_asset_enhancements", + "ad.responsive_display_ad.control_spec.enable_autogen_video", + "ad.responsive_display_ad.descriptions", + "ad.responsive_display_ad.format_setting", + "ad.responsive_display_ad.headlines", + "ad.responsive_display_ad.logo_images", + "ad.responsive_display_ad.logo_images.asset", + "ad.responsive_display_ad.long_headline", + "ad.responsive_display_ad.main_color", + "ad.responsive_display_ad.marketing_images", + "ad.responsive_display_ad.marketing_images.asset", + "ad.responsive_display_ad.price_prefix", + "ad.responsive_display_ad.promo_text", + "ad.responsive_display_ad.square_logo_images", + "ad.responsive_display_ad.square_logo_images.asset", + "ad.responsive_display_ad.square_marketing_images", + "ad.responsive_display_ad.square_marketing_images.asset", + "ad.responsive_display_ad.youtube_videos", + "ad.responsive_display_ad.youtube_videos.asset", + "ad.responsive_search_ad.descriptions", + "ad.responsive_search_ad.headlines", + "ad.responsive_search_ad.path1", + "ad.responsive_search_ad.path2", + "ad.shopping_comparison_listing_ad.headline", + "ad.shopping_product_ad", + "ad.shopping_smart_ad", + "ad.smart_campaign_ad.descriptions", + "ad.smart_campaign_ad.headlines", + "ad.system_managed_resource_source", + "ad.text_ad.description1", + "ad.text_ad.description2", + "ad.text_ad.headline", + "ad.tracking_url_template", + "ad.travel_ad", + "ad.type", + "ad.url_collections", + "ad.url_custom_parameters", + "ad.video_ad.bumper.action_button_label", + "ad.video_ad.bumper.action_headline", + "ad.video_ad.bumper.companion_banner.asset", + "ad.video_ad.in_feed.description1", + "ad.video_ad.in_feed.description2", + "ad.video_ad.in_feed.headline", + "ad.video_ad.in_feed.thumbnail", + "ad.video_ad.in_stream.action_button_label", + "ad.video_ad.in_stream.action_headline", + "ad.video_ad.in_stream.companion_banner.asset", + "ad.video_ad.non_skippable.action_button_label", + "ad.video_ad.non_skippable.action_headline", + "ad.video_ad.non_skippable.companion_banner.asset", + "ad.video_ad.out_stream.description", + "ad.video_ad.out_stream.headline", + "ad.video_ad.video.asset", + "ad.video_responsive_ad.breadcrumb1", + "ad.video_responsive_ad.breadcrumb2", + "ad.video_responsive_ad.call_to_actions", + "ad.video_responsive_ad.companion_banners", + "ad.video_responsive_ad.companion_banners.asset", + "ad.video_responsive_ad.descriptions", + "ad.video_responsive_ad.headlines", + "ad.video_responsive_ad.long_headlines", + "ad.video_responsive_ad.videos", + "ad.video_responsive_ad.videos.asset", + "ad_group", + "ad_strength", + "labels", + "policy_summary.approval_status", + "policy_summary.policy_topic_entries", + "policy_summary.review_status", + "primary_status", + "primary_status_reasons", + "resource_name", + "status", +].map((f) => getOption(f, "ad_group_ad")); + +const segments = [ + "ad_destination_type", + "ad_network_type", + "click_type", + "conversion_action", + "conversion_action_category", + "conversion_action_name", + "conversion_adjustment", + "conversion_lag_bucket", + "conversion_or_adjustment_lag_bucket", + "date", + "device", + "external_conversion_source", + "keyword.ad_group_criterion", + "keyword.info.match_type", + "keyword.info.text", + "new_versus_returning_customers", + "slot", +].map((f) => getOption(f, "segments")); + +const metrics = [ + "absolute_top_impression_percentage", + "active_view_cpm", + "active_view_ctr", + "active_view_impressions", + "active_view_measurability", + "active_view_measurable_cost_micros", + "active_view_measurable_impressions", + "active_view_viewability", + "all_conversions", + "all_conversions_by_conversion_date", + "all_conversions_from_interactions_rate", + "all_conversions_value", + "all_conversions_value_by_conversion_date", + "all_new_customer_lifetime_value", + "average_cart_size", + "average_cost", + "average_cpc", + "average_cpe", + "average_cpm", + "average_cpv", + "average_order_value_micros", + "average_page_views", + "average_time_on_site", + "bounce_rate", + "clicks", + "conversions", + "conversions_by_conversion_date", + "conversions_from_interactions_rate", + "conversions_value", + "conversions_value_by_conversion_date", + "cost_micros", + "cost_of_goods_sold_micros", + "cost_per_all_conversions", + "cost_per_conversion", + "cost_per_current_model_attributed_conversion", + "cross_device_conversions", + "cross_device_conversions_value_micros", + "cross_sell_cost_of_goods_sold_micros", + "cross_sell_gross_profit_micros", + "cross_sell_revenue_micros", + "cross_sell_units_sold", + "ctr", + "current_model_attributed_conversions", + "current_model_attributed_conversions_value", + "engagement_rate", + "engagements", + "gmail_forwards", + "gmail_saves", + "gmail_secondary_clicks", + "gross_profit_margin", + "gross_profit_micros", + "impressions", + "interaction_event_types", + "interaction_rate", + "interactions", + "lead_cost_of_goods_sold_micros", + "lead_gross_profit_micros", + "lead_revenue_micros", + "lead_units_sold", + "new_customer_lifetime_value", + "orders", + "percent_new_visitors", + "revenue_micros", + "top_impression_percentage", + "units_sold", + "value_per_all_conversions", + "value_per_all_conversions_by_conversion_date", + "value_per_conversion", + "value_per_conversions_by_conversion_date", + "value_per_current_model_attributed_conversion", + "video_quartile_p100_rate", + "video_quartile_p25_rate", + "video_quartile_p50_rate", + "video_quartile_p75_rate", + "video_view_rate", + "video_views", + "view_through_conversions", +].map((f) => getOption(f, "metrics")); + +const resourceOption = { + label: "Ad", + value: "ad_group_ad", +}; + +export const ad = { + fields, + segments, + metrics, + resourceOption, +}; diff --git a/components/google_ads/common/resources/adGroup.mjs b/components/google_ads/common/resources/adGroup.mjs new file mode 100644 index 0000000000000..7958d432c5a24 --- /dev/null +++ b/components/google_ads/common/resources/adGroup.mjs @@ -0,0 +1,170 @@ +import { getOption } from "../utils.mjs"; + +const fields = [ + "ad_rotation_mode", + "audience_setting.use_audience_grouped", + "base_ad_group", + "campaign", + "cpc_bid_micros", + "cpm_bid_micros", + "cpv_bid_micros", + "display_custom_bid_dimension", + "effective_cpc_bid_micros", + "effective_target_cpa_micros", + "effective_target_cpa_source", + "effective_target_roas", + "effective_target_roas_source", + "excluded_parent_asset_field_types", + "excluded_parent_asset_set_types", + "final_url_suffix", + "id", + "labels", + "name", + "optimized_targeting_enabled", + "percent_cpc_bid_micros", + "primary_status", + "primary_status_reasons", + "resource_name", + "status", + "target_cpa_micros", + "target_cpm_micros", + "target_roas", + "targeting_setting.target_restrictions", + "tracking_url_template", + "type", + "url_custom_parameters", +].map((f) => getOption(f, "ad_group")); + +const segments = [ + "ad_destination_type", + "ad_network_type", + "asset_interaction_target.asset", + "asset_interaction_target.interaction_on_this_asset", + "auction_insight_domain", + "click_type", + "conversion_action", + "conversion_action_category", + "conversion_action_name", + "conversion_adjustment", + "conversion_lag_bucket", + "conversion_or_adjustment_lag_bucket", + "date", + "device", + "external_conversion_source", + "hour", + "new_versus_returning_customers", + "slot", +].map((f) => getOption(f, "segments")); + +const metrics = [ + "absolute_top_impression_percentage", + "active_view_cpm", + "active_view_ctr", + "active_view_impressions", + "active_view_measurability", + "active_view_measurable_cost_micros", + "active_view_measurable_impressions", + "active_view_viewability", + "all_conversions", + "all_conversions_by_conversion_date", + "all_conversions_from_interactions_rate", + "all_conversions_value", + "all_conversions_value_by_conversion_date", + "all_new_customer_lifetime_value", + "auction_insight_search_absolute_top_impression_percentage", + "auction_insight_search_impression_share", + "auction_insight_search_outranking_share", + "auction_insight_search_overlap_rate", + "auction_insight_search_position_above_rate", + "auction_insight_search_top_impression_percentage", + "average_cart_size", + "average_cost", + "average_cpc", + "average_cpe", + "average_cpm", + "average_cpv", + "average_order_value_micros", + "average_page_views", + "average_time_on_site", + "bounce_rate", + "clicks", + "content_impression_share", + "content_rank_lost_impression_share", + "conversions", + "conversions_by_conversion_date", + "conversions_from_interactions_rate", + "conversions_value", + "conversions_value_by_conversion_date", + "cost_micros", + "cost_of_goods_sold_micros", + "cost_per_all_conversions", + "cost_per_conversion", + "cost_per_current_model_attributed_conversion", + "cross_device_conversions", + "cross_device_conversions_value_micros", + "cross_sell_cost_of_goods_sold_micros", + "cross_sell_gross_profit_micros", + "cross_sell_revenue_micros", + "cross_sell_units_sold", + "ctr", + "current_model_attributed_conversions", + "current_model_attributed_conversions_value", + "engagement_rate", + "engagements", + "gmail_forwards", + "gmail_saves", + "gmail_secondary_clicks", + "gross_profit_margin", + "gross_profit_micros", + "impressions", + "interaction_event_types", + "interaction_rate", + "interactions", + "lead_cost_of_goods_sold_micros", + "lead_gross_profit_micros", + "lead_revenue_micros", + "lead_units_sold", + "new_customer_lifetime_value", + "orders", + "percent_new_visitors", + "phone_calls", + "phone_impressions", + "phone_through_rate", + "relative_ctr", + "revenue_micros", + "search_absolute_top_impression_share", + "search_budget_lost_absolute_top_impression_share", + "search_budget_lost_top_impression_share", + "search_exact_match_impression_share", + "search_impression_share", + "search_rank_lost_absolute_top_impression_share", + "search_rank_lost_impression_share", + "search_rank_lost_top_impression_share", + "search_top_impression_share", + "top_impression_percentage", + "units_sold", + "value_per_all_conversions", + "value_per_all_conversions_by_conversion_date", + "value_per_conversion", + "value_per_conversions_by_conversion_date", + "value_per_current_model_attributed_conversion", + "video_quartile_p100_rate", + "video_quartile_p25_rate", + "video_quartile_p50_rate", + "video_quartile_p75_rate", + "video_view_rate", + "video_views", + "view_through_conversions", +].map((f) => getOption(f, "metrics")); + +const resourceOption = { + label: "Ad Group", + value: "ad_group", +}; + +export const adGroup = { + fields, + segments, + metrics, + resourceOption, +}; diff --git a/components/google_ads/common/resources/campaign.mjs b/components/google_ads/common/resources/campaign.mjs new file mode 100644 index 0000000000000..a05729e8fe24e --- /dev/null +++ b/components/google_ads/common/resources/campaign.mjs @@ -0,0 +1,285 @@ +import { getOption } from "../utils.mjs"; + +const fields = [ + "accessible_bidding_strategy", + "ad_serving_optimization_status", + "advertising_channel_sub_type", + "advertising_channel_type", + "app_campaign_setting.app_id", + "app_campaign_setting.app_store", + "app_campaign_setting.bidding_strategy_goal_type", + "asset_automation_settings", + "audience_setting.use_audience_grouped", + "base_campaign", + "bidding_strategy", + "bidding_strategy_system_status", + "bidding_strategy_type", + "campaign_budget", + "campaign_group", + "commission.commission_rate_micros", + "discovery_campaign_settings.upgraded_targeting", + "dynamic_search_ads_setting.domain_name", + "dynamic_search_ads_setting.feeds", + "dynamic_search_ads_setting.language_code", + "dynamic_search_ads_setting.use_supplied_urls_only", + "end_date", + "excluded_parent_asset_field_types", + "excluded_parent_asset_set_types", + "experiment_type", + "final_url_suffix", + "frequency_caps", + "geo_target_type_setting.negative_geo_target_type", + "geo_target_type_setting.positive_geo_target_type", + "hotel_property_asset_set", + "hotel_setting.hotel_center_id", + "id", + "labels", + "listing_type", + "local_campaign_setting.location_source_type", + "local_services_campaign_settings.category_bids", + "manual_cpa", + "manual_cpc.enhanced_cpc_enabled", + "manual_cpm", + "manual_cpv", + "maximize_conversion_value.target_roas", + "maximize_conversions.target_cpa_micros", + "name", + "network_settings.target_content_network", + "network_settings.target_google_search", + "network_settings.target_google_tv_network", + "network_settings.target_partner_search_network", + "network_settings.target_search_network", + "network_settings.target_youtube", + "optimization_goal_setting.optimization_goal_types", + "optimization_score", + "payment_mode", + "percent_cpc.cpc_bid_ceiling_micros", + "percent_cpc.enhanced_cpc_enabled", + "performance_max_upgrade.performance_max_campaign", + "performance_max_upgrade.pre_upgrade_campaign", + "performance_max_upgrade.status", + "primary_status", + "primary_status_reasons", + "real_time_bidding_setting.opt_in", + "resource_name", + "selective_optimization.conversion_actions", + "serving_status", + "shopping_setting.advertising_partner_ids", + "shopping_setting.campaign_priority", + "shopping_setting.disable_product_feed", + "shopping_setting.enable_local", + "shopping_setting.feed_label", + "shopping_setting.merchant_id", + "shopping_setting.use_vehicle_inventory", + "start_date", + "status", + "target_cpa.cpc_bid_ceiling_micros", + "target_cpa.cpc_bid_floor_micros", + "target_cpa.target_cpa_micros", + "target_cpm.target_frequency_goal.target_count", + "target_cpm.target_frequency_goal.time_unit", + "target_impression_share.cpc_bid_ceiling_micros", + "target_impression_share.location", + "target_impression_share.location_fraction_micros", + "target_roas.cpc_bid_ceiling_micros", + "target_roas.cpc_bid_floor_micros", + "target_roas.target_roas", + "target_spend.cpc_bid_ceiling_micros", + "target_spend.target_spend_micros", + "targeting_setting.target_restrictions", + "tracking_setting.tracking_url", + "tracking_url_template", + "travel_campaign_settings.travel_account_id", + "url_custom_parameters", + "url_expansion_opt_out", + "vanity_pharma.vanity_pharma_display_url_mode", + "vanity_pharma.vanity_pharma_text", + "video_brand_safety_suitability", +].map((f) => getOption(f, "campaign")); + +const segments = [ + "ad_destination_type", + "ad_network_type", + "asset_interaction_target.asset", + "asset_interaction_target.interaction_on_this_asset", + "auction_insight_domain", + "click_type", + "conversion_action", + "conversion_action_category", + "conversion_action_name", + "conversion_adjustment", + "conversion_attribution_event_type", + "conversion_lag_bucket", + "conversion_or_adjustment_lag_bucket", + "conversion_value_rule_primary_dimension", + "date", + "device", + "external_conversion_source", + "hour", + "new_versus_returning_customers", + "recommendation_type", + "sk_ad_network_ad_event_type", + "sk_ad_network_attribution_credit", + "sk_ad_network_coarse_conversion_value", + "sk_ad_network_conversion_value", + "sk_ad_network_postback_sequence_index", + "sk_ad_network_source_app.sk_ad_network_source_app_id", + "sk_ad_network_source_domain", + "sk_ad_network_source_type", + "sk_ad_network_user_type", + "slot", +].map((f) => getOption(f, "segments")); + +const metrics = [ + "absolute_top_impression_percentage", + "active_view_cpm", + "active_view_ctr", + "active_view_impressions", + "active_view_measurability", + "active_view_measurable_cost_micros", + "active_view_measurable_impressions", + "active_view_viewability", + "all_conversions", + "all_conversions_by_conversion_date", + "all_conversions_from_click_to_call", + "all_conversions_from_directions", + "all_conversions_from_interactions_rate", + "all_conversions_from_location_asset_click_to_call", + "all_conversions_from_location_asset_directions", + "all_conversions_from_location_asset_menu", + "all_conversions_from_location_asset_order", + "all_conversions_from_location_asset_other_engagement", + "all_conversions_from_location_asset_store_visits", + "all_conversions_from_location_asset_website", + "all_conversions_from_menu", + "all_conversions_from_order", + "all_conversions_from_other_engagement", + "all_conversions_from_store_visit", + "all_conversions_from_store_website", + "all_conversions_value", + "all_conversions_value_by_conversion_date", + "all_new_customer_lifetime_value", + "auction_insight_search_absolute_top_impression_percentage", + "auction_insight_search_impression_share", + "auction_insight_search_outranking_share", + "auction_insight_search_overlap_rate", + "auction_insight_search_position_above_rate", + "auction_insight_search_top_impression_percentage", + "average_cart_size", + "average_cost", + "average_cpc", + "average_cpe", + "average_cpm", + "average_cpv", + "average_impression_frequency_per_user", + "average_order_value_micros", + "average_page_views", + "average_target_cpa_micros", + "average_target_roas", + "average_time_on_site", + "bounce_rate", + "clicks", + "content_budget_lost_impression_share", + "content_impression_share", + "content_rank_lost_impression_share", + "conversions", + "conversions_by_conversion_date", + "conversions_from_interactions_rate", + "conversions_value", + "conversions_value_by_conversion_date", + "cost_micros", + "cost_of_goods_sold_micros", + "cost_per_all_conversions", + "cost_per_conversion", + "cost_per_current_model_attributed_conversion", + "cross_device_conversions", + "cross_device_conversions_value_micros", + "cross_sell_cost_of_goods_sold_micros", + "cross_sell_gross_profit_micros", + "cross_sell_revenue_micros", + "cross_sell_units_sold", + "ctr", + "current_model_attributed_conversions", + "current_model_attributed_conversions_from_interactions_rate", + "current_model_attributed_conversions_from_interactions_value_per_interaction", + "current_model_attributed_conversions_value", + "current_model_attributed_conversions_value_per_cost", + "eligible_impressions_from_location_asset_store_reach", + "engagement_rate", + "engagements", + "gmail_forwards", + "gmail_saves", + "gmail_secondary_clicks", + "gross_profit_margin", + "gross_profit_micros", + "impressions", + "interaction_event_types", + "interaction_rate", + "interactions", + "invalid_click_rate", + "invalid_clicks", + "lead_cost_of_goods_sold_micros", + "lead_gross_profit_micros", + "lead_revenue_micros", + "lead_units_sold", + "new_customer_lifetime_value", + "optimization_score_uplift", + "optimization_score_url", + "orders", + "percent_new_visitors", + "phone_calls", + "phone_impressions", + "phone_through_rate", + "publisher_organic_clicks", + "publisher_purchased_clicks", + "publisher_unknown_clicks", + "relative_ctr", + "revenue_micros", + "search_absolute_top_impression_share", + "search_budget_lost_absolute_top_impression_share", + "search_budget_lost_impression_share", + "search_budget_lost_top_impression_share", + "search_click_share", + "search_exact_match_impression_share", + "search_impression_share", + "search_rank_lost_absolute_top_impression_share", + "search_rank_lost_impression_share", + "search_rank_lost_top_impression_share", + "search_top_impression_share", + "sk_ad_network_installs", + "sk_ad_network_total_conversions", + "top_impression_percentage", + "unique_users", + "units_sold", + "value_per_all_conversions", + "value_per_all_conversions_by_conversion_date", + "value_per_conversion", + "value_per_conversions_by_conversion_date", + "value_per_current_model_attributed_conversion", + "video_quartile_p100_rate", + "video_quartile_p25_rate", + "video_quartile_p50_rate", + "video_quartile_p75_rate", + "video_view_rate", + "video_views", + "view_through_conversions", + "view_through_conversions_from_location_asset_click_to_call", + "view_through_conversions_from_location_asset_directions", + "view_through_conversions_from_location_asset_menu", + "view_through_conversions_from_location_asset_order", + "view_through_conversions_from_location_asset_other_engagement", + "view_through_conversions_from_location_asset_store_visits", + "view_through_conversions_from_location_asset_website", +].map((f) => getOption(f, "metrics")); + +const resourceOption = { + label: "Campaign", + value: "campaign", +}; + +export const campaign = { + fields, + segments, + metrics, + resourceOption, +}; diff --git a/components/google_ads/common/resources/customer.mjs b/components/google_ads/common/resources/customer.mjs new file mode 100644 index 0000000000000..f29f3288970fe --- /dev/null +++ b/components/google_ads/common/resources/customer.mjs @@ -0,0 +1,177 @@ +import { getOption } from "../utils.mjs"; + +const fields = [ + "auto_tagging_enabled", + "call_reporting_setting.call_conversion_action", + "call_reporting_setting.call_conversion_reporting_enabled", + "call_reporting_setting.call_reporting_enabled", + "conversion_tracking_setting.accepted_customer_data_terms", + "conversion_tracking_setting.conversion_tracking_id", + "conversion_tracking_setting.conversion_tracking_status", + "conversion_tracking_setting.cross_account_conversion_tracking_id", + "conversion_tracking_setting.enhanced_conversions_for_leads_enabled", + "conversion_tracking_setting.google_ads_conversion_customer", + "currency_code", + "customer_agreement_setting.accepted_lead_form_terms", + "descriptive_name", + "final_url_suffix", + "has_partners_badge", + "id", + "image_asset_auto_migration_done", + "image_asset_auto_migration_done_date_time", + "local_services_settings.granular_insurance_statuses", + "local_services_settings.granular_license_statuses", + "location_asset_auto_migration_done", + "location_asset_auto_migration_done_date_time", + "manager", + "optimization_score", + "optimization_score_weight", + "pay_per_conversion_eligibility_failure_reasons", + "remarketing_setting.google_global_site_tag", + "resource_name", + "status", + "test_account", + "time_zone", + "tracking_url_template", + "video_brand_safety_suitability", +].map((f) => getOption(f, "customer")); + +const segments = [ + "ad_network_type", + "auction_insight_domain", + "click_type", + "conversion_action", + "conversion_action_category", + "conversion_action_name", + "conversion_adjustment", + "conversion_lag_bucket", + "conversion_or_adjustment_lag_bucket", + "conversion_value_rule_primary_dimension", + "date", + "device", + "external_conversion_source", + "hour", + "new_versus_returning_customers", + "recommendation_type", + "sk_ad_network_ad_event_type", + "sk_ad_network_attribution_credit", + "sk_ad_network_coarse_conversion_value", + "sk_ad_network_conversion_value", + "sk_ad_network_postback_sequence_index", + "sk_ad_network_source_app.sk_ad_network_source_app_id", + "sk_ad_network_source_domain", + "sk_ad_network_source_type", + "sk_ad_network_user_type", + "slot", +].map((f) => getOption(f, "segments")); + +const metrics = [ + "absolute_top_impression_percentage", + "active_view_cpm", + "active_view_ctr", + "active_view_impressions", + "active_view_measurability", + "active_view_measurable_cost_micros", + "active_view_measurable_impressions", + "active_view_viewability", + "all_conversions", + "all_conversions_by_conversion_date", + "all_conversions_from_interactions_rate", + "all_conversions_from_location_asset_click_to_call", + "all_conversions_from_location_asset_directions", + "all_conversions_from_location_asset_menu", + "all_conversions_from_location_asset_order", + "all_conversions_from_location_asset_other_engagement", + "all_conversions_from_location_asset_store_visits", + "all_conversions_from_location_asset_website", + "all_conversions_value", + "all_conversions_value_by_conversion_date", + "all_new_customer_lifetime_value", + "auction_insight_search_absolute_top_impression_percentage", + "auction_insight_search_impression_share", + "auction_insight_search_outranking_share", + "auction_insight_search_overlap_rate", + "auction_insight_search_position_above_rate", + "auction_insight_search_top_impression_percentage", + "average_cart_size", + "average_cost", + "average_cpc", + "average_cpe", + "average_cpm", + "average_cpv", + "average_order_value_micros", + "clicks", + "content_budget_lost_impression_share", + "content_impression_share", + "content_rank_lost_impression_share", + "conversions", + "conversions_by_conversion_date", + "conversions_from_interactions_rate", + "conversions_value", + "conversions_value_by_conversion_date", + "cost_micros", + "cost_of_goods_sold_micros", + "cost_per_all_conversions", + "cost_per_conversion", + "cross_device_conversions", + "cross_device_conversions_value_micros", + "cross_sell_cost_of_goods_sold_micros", + "cross_sell_gross_profit_micros", + "cross_sell_revenue_micros", + "cross_sell_units_sold", + "ctr", + "eligible_impressions_from_location_asset_store_reach", + "engagement_rate", + "engagements", + "gross_profit_margin", + "gross_profit_micros", + "impressions", + "interaction_event_types", + "interaction_rate", + "interactions", + "invalid_click_rate", + "invalid_clicks", + "lead_cost_of_goods_sold_micros", + "lead_gross_profit_micros", + "lead_revenue_micros", + "lead_units_sold", + "new_customer_lifetime_value", + "optimization_score_uplift", + "optimization_score_url", + "orders", + "revenue_micros", + "search_budget_lost_impression_share", + "search_exact_match_impression_share", + "search_impression_share", + "search_rank_lost_impression_share", + "sk_ad_network_installs", + "sk_ad_network_total_conversions", + "top_impression_percentage", + "units_sold", + "value_per_all_conversions", + "value_per_all_conversions_by_conversion_date", + "value_per_conversion", + "value_per_conversions_by_conversion_date", + "video_view_rate", + "video_views", + "view_through_conversions", + "view_through_conversions_from_location_asset_click_to_call", + "view_through_conversions_from_location_asset_directions", + "view_through_conversions_from_location_asset_menu", + "view_through_conversions_from_location_asset_order", + "view_through_conversions_from_location_asset_other_engagement", + "view_through_conversions_from_location_asset_store_visits", + "view_through_conversions_from_location_asset_website", +].map((f) => getOption(f, "metrics")); + +const resourceOption = { + label: "Customer", + value: "customer", +}; + +export const customer = { + fields, + segments, + metrics, + resourceOption, +}; diff --git a/components/google_ads/common/utils.mjs b/components/google_ads/common/utils.mjs new file mode 100644 index 0000000000000..905162bb8dbf1 --- /dev/null +++ b/components/google_ads/common/utils.mjs @@ -0,0 +1,48 @@ +import { ConfigurationError } from "@pipedream/platform"; + +export function parseObject(value = {}) { + return Object.fromEntries(Object.entries(value).map(([ + key, + value, + ]) => { + try { + return [ + key, + JSON.parse(value), + ]; + } catch (err) { + return [ + key, + value, + ]; + } + })); +} + +export function parseStringObject(value = "{}") { + try { + return typeof value === "string" + ? JSON.parse(value) + : value; + } catch (err) { + throw new ConfigurationError(`Error parsing JSON value \`${value}\` +\\ +**${err.toString()}**`); + } +} + +export function getOption(label, prefix) { + return { + label, + value: `${prefix}.${label}`, + }; +} + +export function checkPrefix(value, prefix) { + const checkStr = (s) => s && (s?.startsWith?.(prefix) + ? s + : `${prefix}.${s}`); + return Array.isArray(value ?? []) + ? (value ?? []).map(checkStr) + : checkStr(value); +} diff --git a/components/google_ads/google_ads.app.mjs b/components/google_ads/google_ads.app.mjs index c285c482013fc..7bf1d2c0e4c95 100644 --- a/components/google_ads/google_ads.app.mjs +++ b/components/google_ads/google_ads.app.mjs @@ -1,4 +1,5 @@ import { axios } from "@pipedream/platform"; +import { QUERIES } from "./common/queries.mjs"; export default { type: "app", @@ -11,73 +12,239 @@ export default { }, userListId: { type: "string", - label: "User List ID", - description: "The ID of the user list to add the contact to.", - async options() { - const { results = [] } = await this.listLists(); - - return results.map(({ + label: "Customer List ID", + description: "Select a Customer List to add the contact to, or provide a custom Customer List ID.", + async options({ + accountId, customerClientId, + }) { + const response = await this.listUserLists({ + accountId, + customerClientId, + }); + return response?.filter(({ userList: { type } }) => type === "CRM_BASED")?.map(({ userList: { - id: value, name: label, + id, name, + }, + }) => ({ + label: name, + value: id, + })); + }, + }, + accountId: { + type: "string", + label: "Use Google Ads As", + description: "Select an account from the list of [customers directly accessible by the authenticated user](https://developers.google.com/google-ads/api/rest/reference/rest/v18/customers/listAccessibleCustomers). This is usually a **Manager Account**, used as `login-customer-id`", + async options() { + const response = await this.listAccessibleCustomers(); + return response?.map(((resourceName) => ({ + label: resourceName, + value: resourceName.split("/").pop(), + }))); + }, + }, + customerClientId: { + type: "string", + label: "Managed Account", + description: "Select a [customer client](https://developers.google.com/google-ads/api/reference/rpc/v18/CustomerClient) from the list of [customers linked to the selected account](https://developers.google.com/google-ads/api/docs/account-management/get-account-hierarchy).", + useQuery: true, + optional: true, + async options({ + accountId, query, + }) { + const response = await this.listCustomerClients({ + accountId, + query, + }); + return response?.map(({ + customerClient: { + descriptiveName, id, manager, + }, + }) => ({ + label: `${manager + ? "[Manager] " + : ""}${descriptiveName}`, + value: id, + })).filter(({ value }) => value !== accountId); + }, + }, + leadFormId: { + type: "string", + label: "Lead Form ID", + description: "Select a [Lead Form](https://developers.google.com/google-ads/api/rest/reference/rest/v18/Asset#LeadFormAsset) to watch for new entries.", + async options({ + accountId, customerClientId, + }) { + const response = await this.listLeadForms({ + accountId, + customerClientId, + }); + return response?.map(({ + asset: { + id, leadFormAsset: { + businessName, headline, + }, }, }) => ({ - label, - value, + label: `${businessName} - ${headline}`, + value: id, })); }, }, }, methods: { _baseUrl() { - return "https://googleads.googleapis.com"; + return "https://eolid4dq1k0t9hi.m.pipedream.net"; }, - _headers() { + _headers(accountId) { return { "Authorization": `Bearer ${this.$auth.oauth_access_token}`, - "developer-token": `${this.$auth.developer_token}`, + "login-customer-id": accountId, }; }, _makeRequest({ - $ = this, path, ...opts + $ = this, accountId, customerClientId, path, ...opts }) { - return axios($, { - url: this._baseUrl() + path, - headers: this._headers(), + const data = { + headers: this._headers(accountId), + path: path.replace("{customerClientId}", customerClientId ?? accountId), ...opts, + }; + return axios($, { + method: "post", + url: this._baseUrl(), + data, }); }, - addContactToCustomerList({ - path, ...opts + async search({ + query, ...args }) { - return this._makeRequest({ - method: "POST", - path: `/v15/${path}:addOperations`, - ...opts, + console.log("Executing query: ", query); + const response = await this._makeRequest({ + path: "/v18/customers/{customerClientId}/googleAds:search", + method: "post", + data: { + query, + }, + ...args, + }); + return response; + }, + async listAccessibleCustomers() { + const response = await this._makeRequest({ + path: "/v18/customers:listAccessibleCustomers", + }); + return response.resourceNames; + }, + async listCustomerClients({ + query, ...args + }) { + const { results } = await this.search({ + query: QUERIES.listCustomerClients(query), + ...args, + }); + return results; + }, + async createReport(args) { + const { results } = await this.search(args); + return results; + }, + async createUserList(args) { + const response = await this._makeRequest({ + path: "v18/customers/{customerClientId}/userLists:mutate", + method: "post", + ...args, + }); + return response; + }, + async listUserLists(args) { + const { results } = await this.search({ + query: QUERIES.listUserLists(), + ...args, + }); + return results; + }, + async listConversionActions(args) { + const { results } = await this.search({ + query: QUERIES.listConversionActions(), + ...args, + }); + return results; + }, + async listRemarketingActions(args) { + const { results } = await this.search({ + query: QUERIES.listRemarketingActions(), + ...args, + }); + return results; + }, + async listLeadForms(args) { + const { results } = await this.search({ + query: QUERIES.listLeadForms(), + ...args, + }); + return results; + }, + async listCampaigns({ + query, ...args + }) { + const { results } = await this.search({ + query: QUERIES.listCampaigns(query), + ...args, }); + return results; }, - createOfflineUserDataJob(opts = {}) { + async listResources({ + resource, query, pageToken, ...args + }) { + return this.search({ + query: QUERIES.listResources(resource, query), + params: { + pageToken, + }, + ...args, + }); + }, + async getLeadFormData({ + leadFormId, ...args + }) { + const { results } = await this.search({ + query: QUERIES.listLeadFormSubmissionData(leadFormId), + ...args, + }); + return results; + }, + async createConversionAction(args) { + const response = await this._makeRequest({ + path: "v18/customers/{customerClientId}/conversionActions:mutate", + method: "post", + ...args, + }); + return response; + }, + async addContactToCustomerList({ + path, ...opts + }) { return this._makeRequest({ method: "POST", - path: `/v15/customers/${this.$auth.login_customer_id}/offlineUserDataJobs:create`, + path: `/v18/${path}:addOperations`, ...opts, }); }, - runOfflineUserDataJob({ path }) { + async createOfflineUserDataJob(opts = {}) { return this._makeRequest({ method: "POST", - path: `/v15/${path}:run`, + path: "/v18/customers/{customerClientId}/offlineUserDataJobs:create", + ...opts, }); }, - listLists() { + async runOfflineUserDataJob({ + path, ...args + }) { return this._makeRequest({ method: "POST", - path: `/v15/customers/${this.$auth.login_customer_id}/googleAds:search`, - data: { - query: `SELECT - user_list.id, - user_list.name - FROM user_list`, - }, + path: `/v18/${path}:run`, + ...args, }); }, }, diff --git a/components/google_ads/package.json b/components/google_ads/package.json index 209b56babbe24..de089808df0dd 100644 --- a/components/google_ads/package.json +++ b/components/google_ads/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/google_ads", - "version": "0.1.0", + "version": "0.3.1", "description": "Pipedream Google Ads Components", "main": "google_ads.app.mjs", "keywords": [ @@ -13,7 +13,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.5.1", + "@pipedream/platform": "^3.0.3", "crypto": "^1.0.1" } } diff --git a/components/google_ads/sources/common/common.mjs b/components/google_ads/sources/common/common.mjs new file mode 100644 index 0000000000000..d9d5eb5413677 --- /dev/null +++ b/components/google_ads/sources/common/common.mjs @@ -0,0 +1,54 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import props from "../../common/props.mjs"; + +export default { + props: { + ...props, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.getAndProcessData(5); + }, + }, + methods: { + _getSavedIds() { + return this.db.get("savedIds") ?? []; + }, + _setSavedIds(value) { + this.db.set("savedIds", value); + }, + getTimestamp() { + return Date.now(); + }, + getItemId({ id }) { + return id; + }, + async getAndProcessData(max = 0) { + const savedIds = this._getSavedIds(); + const items = await this.getItems(savedIds); + items?.filter((item) => !savedIds.includes(this.getItemId(item))) + .forEach((item, index) => { + const id = this.getItemId(item); + if (!max || index < max) { + this.$emit(item, { + id, + summary: this.getSummary(item), + ts: this.getTimestamp(item), + }); + } + savedIds.push(id); + }); + this._setSavedIds(savedIds); + }, + }, + async run() { + await this.getAndProcessData(); + }, +}; diff --git a/components/google_ads/sources/new-campaign-created/new-campaign-created.mjs b/components/google_ads/sources/new-campaign-created/new-campaign-created.mjs new file mode 100644 index 0000000000000..5fd28a85b98dd --- /dev/null +++ b/components/google_ads/sources/new-campaign-created/new-campaign-created.mjs @@ -0,0 +1,51 @@ +import common from "../common/common.mjs"; +import { campaign } from "../../common/resources/campaign.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "google_ads-new-campaign-created", + name: "New Campaign Created", + description: "Emit new event when a new campaign is created. [See the documentation](https://developers.google.com/google-ads/api/fields/v18/campaign)", + version: "0.0.3", + type: "source", + dedupe: "unique", + sampleEmit, + props: { + ...common.props, + customerClientId: { + ...common.props.customerClientId, + }, + fields: { + type: "string[]", + label: "Extra Fields", + description: "Additional [campaign fields](https://developers.google.com/google-ads/api/fields/v18/campaign) to emit in the event", + options: campaign.fields, + optional: true, + default: [ + "campaign.id", + "campaign.name", + ], + }, + }, + methods: { + ...common.methods, + getSummary({ name }) { + return `New Campaign: "${name}"`; + }, + async getItems(savedIds) { + const { + accountId, customerClientId, fields, + } = this; + const items = await this.googleAds.listCampaigns({ + accountId, + customerClientId, + query: { + fields, + savedIds, + }, + }); + return items?.map(({ campaign }) => campaign); + }, + }, +}; diff --git a/components/google_ads/sources/new-campaign-created/test-event.mjs b/components/google_ads/sources/new-campaign-created/test-event.mjs new file mode 100644 index 0000000000000..ed2b92184dd7c --- /dev/null +++ b/components/google_ads/sources/new-campaign-created/test-event.mjs @@ -0,0 +1,5 @@ +export default { + resourceName: "customers/123/campaigns/456", + name: "Campaign Name", + id: "456", +}; diff --git a/components/google_ads/sources/new-lead-form-entry/new-lead-form-entry.mjs b/components/google_ads/sources/new-lead-form-entry/new-lead-form-entry.mjs new file mode 100644 index 0000000000000..325d3fdf755b3 --- /dev/null +++ b/components/google_ads/sources/new-lead-form-entry/new-lead-form-entry.mjs @@ -0,0 +1,51 @@ +import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; + +const { googleAds } = common.props; + +export default { + ...common, + key: "google_ads-new-lead-form-entry", + name: "New Lead Form Entry", + description: "Emit new event for new leads on a Lead Form. [See the documentation](https://developers.google.com/google-ads/api/fields/v18/lead_form_submission_data)", + version: "0.0.3", + type: "source", + dedupe: "unique", + sampleEmit, + props: { + ...common.props, + leadFormId: { + propDefinition: [ + googleAds, + "leadFormId", + (({ + accountId, customerClientId, + }) => ({ + accountId, + customerClientId, + })), + ], + }, + docsAlert: { + type: "alert", + alertType: "info", + content: "If needed, see Google's documentation on [submission fields](https://developers.google.com/google-ads/api/reference/rpc/v18/LeadFormSubmissionField) and [custom submission fields](https://developers.google.com/google-ads/api/reference/rpc/v18/CustomLeadFormSubmissionField).", + }, + }, + methods: { + ...common.methods, + getSummary() { + return "New Lead"; + }, + getItems() { + const { + accountId, customerClientId, leadFormId, + } = this; + return this.googleAds.getLeadFormData({ + accountId, + customerClientId, + leadFormId, + }); + }, + }, +}; diff --git a/components/google_ads/sources/new-lead-form-entry/test-event.mjs b/components/google_ads/sources/new-lead-form-entry/test-event.mjs new file mode 100644 index 0000000000000..0d9fb9bee6cb8 --- /dev/null +++ b/components/google_ads/sources/new-lead-form-entry/test-event.mjs @@ -0,0 +1,18 @@ +export default { + resourceName: "customers/123/leadFormSubmissionData/456", + custom_lead_form_submission_fields: [ + { + question_text: "Text", + field_value: "Value" + } + ], + gclid: "789", + id: "456", + lead_form_submission_fields: [ + { + field_type: "TYPE", + field_value: "Value" + } + ], + submission_date_time: "2019-01-01 12:32:45-08:00", +}; diff --git a/components/google_analytics/README.md b/components/google_analytics/README.md index cae9f889e18fd..37b3ab13be8a7 100644 --- a/components/google_analytics/README.md +++ b/components/google_analytics/README.md @@ -1,15 +1,11 @@ # Overview -The Google Analytics API lets you access data from your Google Analytics -account to build custom reports and dashboards. With the API, you can query -data from your account, customize the data returned, and access data in real -time. - -Here are some examples of what you can build with the Google Analytics API: - -- A dashboard to track your website's traffic -- A report to show which keywords are driving traffic to your website -- A tool to track how users are interacting with your website -- A report to show which browsers and devices are being used to access your - website -- A tool to track conversions and goal completions on your website +The Google Analytics API unlocks the power of your analytics data, enabling automated access to custom reports, real-time statistics, and user behavior analysis. With it, you can extract actionable insights, push data to other platforms, and tailor your business strategy with precision. Leveraging Pipedream's serverless platform, these capabilities can be seamlessly integrated into workflows that trigger actions in other apps, send alerts, or even feed data warehouses for deeper analysis. + +# Example Use Cases + +- **Real-time Alerts for Traffic Spikes**: Trigger a notification in Slack using Pipedream when your website experiences an unexpected surge in real-time users. This allows your team to immediately investigate potential causes or capitalize on the trending traffic. + +- **Custom Reporting to Google Sheets**: Automate the delivery of daily Google Analytics reports to a Google Sheets document. Employ Pipedream to schedule and extract custom data points such as session duration, bounce rate, and conversion metrics, providing your marketing team with up-to-date insights. + +- **Audience Segmentation for Email Campaigns**: Use the API to segment your audience based on their interactions with your website. Connect Pipedream with a service like Mailchimp to enrich email lists and personalize campaigns, aiming to increase engagement and conversion rates. diff --git a/components/google_analytics/actions/create-ga4-property/create-ga4-property.mjs b/components/google_analytics/actions/create-ga4-property/create-ga4-property.mjs index 6503a5a8e6228..5c46fc42fdace 100644 --- a/components/google_analytics/actions/create-ga4-property/create-ga4-property.mjs +++ b/components/google_analytics/actions/create-ga4-property/create-ga4-property.mjs @@ -1,13 +1,11 @@ import googleAnalytics from "../../google_analytics.app.mjs"; -import { - INDUSTRY_CATEGORY_OPTIONS, TIMEZONE_OPTIONS, -} from "../../common/constants.mjs"; +import constants from "../../common/constants.mjs"; export default { key: "google_analytics-create-ga4-property", name: "Create GA4 Property", description: "Creates a new GA4 property. [See the documentation](https://developers.google.com/analytics/devguides/config/admin/v1/rest/v1beta/properties/create)", - version: "0.0.1", + version: "0.1.0", type: "action", props: { googleAnalytics, @@ -26,14 +24,14 @@ export default { type: "string", label: "Time Zone", description: "The reporting time zone for the property. Must be a valid value from [the IANA timezone database](https://www.iana.org/time-zones).", - options: TIMEZONE_OPTIONS, + options: constants.TIMEZONE_OPTIONS, }, industryCategory: { type: "string", label: "Industry Category", description: "The industry category associated with the property.", optional: true, - options: INDUSTRY_CATEGORY_OPTIONS, + options: constants.INDUSTRY_CATEGORY_OPTIONS, }, currencyCode: { type: "string", diff --git a/components/google_analytics/actions/create-key-event/create-key-event.mjs b/components/google_analytics/actions/create-key-event/create-key-event.mjs new file mode 100644 index 0000000000000..ea7e46ac99141 --- /dev/null +++ b/components/google_analytics/actions/create-key-event/create-key-event.mjs @@ -0,0 +1,71 @@ +import app from "../../google_analytics.app.mjs"; + +export default { + key: "google_analytics-create-key-event", + name: "Create Key Event", + description: "Creates a new key event. [See the documentation](https://developers.google.com/analytics/devguides/config/admin/v1/rest/v1beta/properties.keyEvents/create)", + version: "0.0.1", + type: "action", + props: { + app, + parent: { + type: "string", + label: "Parent", + description: "The resource name of the parent property where this Key Event will be created. Format: `properties/123`", + }, + eventName: { + type: "string", + label: "Event Name", + description: "Immutable. The event name for this key event. Examples: `click`, `purchase`", + }, + countingMethod: { + type: "string", + label: "Counting Method", + description: "The method by which Key Events will be counted across multiple events within a session.", + options: [ + { + label: "Counting method not specified.", + value: "COUNTING_METHOD_UNSPECIFIED", + }, + { + label: "Each Event instance is considered a Key Event.", + value: "ONCE_PER_EVENT", + }, + { + label: "An Event instance is considered a Key Event at most once per session per user.", + value: "ONCE_PER_SESSION", + }, + ], + }, + }, + methods: { + createKeyEvent({ + parent, ...args + } = {}) { + return this.app.post({ + path: `/${parent}/keyEvents`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + createKeyEvent, + parent, + eventName, + countingMethod, + } = this; + + const response = await createKeyEvent({ + $, + parent, + data: { + eventName, + countingMethod, + }, + }); + + $.export("$summary", `Successfully created key event with name ${eventName} and counting method ${countingMethod} in parent ${parent}`); + return response; + }, +}; diff --git a/components/google_analytics/actions/run-report-in-ga4/run-report-in-ga4.mjs b/components/google_analytics/actions/run-report-in-ga4/run-report-in-ga4.mjs index cd0a797cc3c36..a291e351d95d6 100644 --- a/components/google_analytics/actions/run-report-in-ga4/run-report-in-ga4.mjs +++ b/components/google_analytics/actions/run-report-in-ga4/run-report-in-ga4.mjs @@ -2,7 +2,7 @@ import analytics from "../../google_analytics.app.mjs"; export default { key: "google_analytics-run-report-in-ga4", - version: "0.0.3", + version: "0.1.0", name: "Run Report in GA4", description: "Returns a customized report of your Google Analytics event data. Reports contain statistics derived from data collected by the Google Analytics tracking code. [See the documentation here](https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/properties/runReport)", type: "action", diff --git a/components/google_analytics/actions/run-report/run-report.mjs b/components/google_analytics/actions/run-report/run-report.mjs index af3ecd9a75b33..7122ff4ccb730 100644 --- a/components/google_analytics/actions/run-report/run-report.mjs +++ b/components/google_analytics/actions/run-report/run-report.mjs @@ -2,7 +2,7 @@ import analytics from "../../google_analytics.app.mjs"; export default { key: "google_analytics-run-report", - version: "0.0.3", + version: "0.1.0", name: "Run Report", description: "Return report metrics based on a start and end date. [See the docs here](https://developers.google.com/analytics/devguides/reporting/core/v4/rest?hl=en)", type: "action", diff --git a/components/google_analytics/common/constants.mjs b/components/google_analytics/common/constants.mjs index 8e4161bc08036..9ab62449ce6e1 100644 --- a/components/google_analytics/common/constants.mjs +++ b/components/google_analytics/common/constants.mjs @@ -1,4 +1,4 @@ -export const INDUSTRY_CATEGORY_OPTIONS = [ +const INDUSTRY_CATEGORY_OPTIONS = [ { value: "INDUSTRY_CATEGORY_UNSPECIFIED", label: "Industry category unspecified", @@ -109,7 +109,7 @@ export const INDUSTRY_CATEGORY_OPTIONS = [ }, ]; -export const TIMEZONE_OPTIONS = [ +const TIMEZONE_OPTIONS = [ "Africa/Abidjan", "Africa/Algiers", "Africa/Bissau", @@ -380,3 +380,20 @@ export const TIMEZONE_OPTIONS = [ "Pacific/Tarawa", "Pacific/Tongatapu", ]; + +const API = { + DATA: { + BASE_URL: "https://analyticsdata.googleapis.com", + VERSION_PATH: "/v1beta", + }, + ADMIN: { + BASE_URL: "https://analyticsadmin.googleapis.com", + VERSION_PATH: "/v1beta", + }, +}; + +export default { + INDUSTRY_CATEGORY_OPTIONS, + TIMEZONE_OPTIONS, + API, +}; diff --git a/components/google_analytics/common/utils.mjs b/components/google_analytics/common/utils.mjs new file mode 100644 index 0000000000000..b56eb1088789a --- /dev/null +++ b/components/google_analytics/common/utils.mjs @@ -0,0 +1,9 @@ +function monthAgo() { + const monthAgo = new Date(); + monthAgo.setMonth(monthAgo.getMonth() - 1); + return monthAgo.toISOString().split("T")[0]; +} + +export default { + monthAgo, +}; diff --git a/components/google_analytics/google_analytics.app.mjs b/components/google_analytics/google_analytics.app.mjs index e7e5752c19b69..9ed56f6a63ee1 100644 --- a/components/google_analytics/google_analytics.app.mjs +++ b/components/google_analytics/google_analytics.app.mjs @@ -1,5 +1,6 @@ import analyticsreporting from "@googleapis/analyticsreporting"; import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; export default { type: "app", @@ -24,65 +25,68 @@ export default { }, }, methods: { - monthAgo() { - const monthAgo = new Date(); - monthAgo.setMonth(monthAgo.getMonth() - 1); - return monthAgo.toISOString().split("T")[0]; - }, _accessToken() { return this.$auth.oauth_access_token; }, + client() { + const auth = new analyticsreporting.auth.OAuth2(); + auth.setCredentials({ + access_token: this._accessToken(), + }); + return analyticsreporting.analyticsreporting({ + version: "v4", + auth, + }); + }, + queryReports(data) { + const client = this.client(); + return client.reports.batchGet(data); + }, getHeaders(headers = {}) { return { authorization: `Bearer ${this._accessToken()}`, ...headers, }; }, - makeRequest(customConfig) { - const { - $ = this, - headers, - ...otherConfig - } = customConfig; - + getUrl(path, api = constants.API.ADMIN) { + return `${api.BASE_URL}${api.VERSION_PATH}${path}`; + }, + makeRequest({ + $ = this, path, headers, api, ...args + } = {}) { const config = { + url: this.getUrl(path, api), headers: this.getHeaders(headers), - ...otherConfig, + ...args, }; return axios($, config); }, - async queryReportsGA4(args = {}) { + post(args = {}) { return this.makeRequest({ - url: `https://analyticsdata.googleapis.com/v1beta/properties/${args.property}:runReport`, method: "POST", ...args, }); }, - async listAccounts() { - return this.makeRequest({ - url: "https://analyticsadmin.googleapis.com/v1beta/accounts", + createProperty(args) { + return this.post({ + path: "/properties", + ...args, }); }, - async createProperty(args) { + listAccounts(args = {}) { return this.makeRequest({ - url: "https://analyticsadmin.googleapis.com/v1beta/properties", - method: "POST", + path: "/accounts", ...args, }); }, - client() { - const auth = new analyticsreporting.auth.OAuth2(); - auth.setCredentials({ - access_token: this._accessToken(), - }); - return analyticsreporting.analyticsreporting({ - version: "v4", - auth, + queryReportsGA4({ + property, ...args + } = {}) { + return this.post({ + api: constants.API.DATA, + path: `/properties/${property}:runReport`, + ...args, }); }, - async queryReports(data) { - const client = this.client(); - return client.reports.batchGet(data); - }, }, }; diff --git a/components/google_analytics/package.json b/components/google_analytics/package.json index 3bcb94a55399a..2dfecc84c4cfb 100644 --- a/components/google_analytics/package.json +++ b/components/google_analytics/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/google_analytics", - "version": "0.1.0", + "version": "0.2.0", "description": "Pipedream Google_analytics Components", "main": "google_analytics.app.mjs", "keywords": [ @@ -14,6 +14,6 @@ }, "dependencies": { "@googleapis/analyticsreporting": "^1.0.0", - "@pipedream/platform": "^1.2.0" + "@pipedream/platform": "^3.0.0" } } diff --git a/components/google_analytics/sources/page-opened/page-opened.mjs b/components/google_analytics/sources/page-opened/page-opened.mjs index 1c0ee58be7b33..05233d396fbf1 100644 --- a/components/google_analytics/sources/page-opened/page-opened.mjs +++ b/components/google_analytics/sources/page-opened/page-opened.mjs @@ -1,9 +1,10 @@ import analytics from "../../google_analytics.app.mjs"; import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import utils from "../../common/utils.mjs"; export default { key: "google_analytics-page-opened", - version: "0.0.4", + version: "0.1.0", name: "New Page Opened", description: "Emit new event when a page is viewed", type: "source", @@ -26,7 +27,7 @@ export default { }, hooks: { async deploy() { - const startDate = this.analytics.monthAgo(); + const startDate = utils.monthAgo(); this._setStartDate(startDate); await this.processEvent(); }, diff --git a/components/google_appsheet/.gitignore b/components/google_appsheet/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/google_appsheet/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/google_appsheet/README.md b/components/google_appsheet/README.md index 165dea32a8e1a..7575b933a28bc 100644 --- a/components/google_appsheet/README.md +++ b/components/google_appsheet/README.md @@ -1,8 +1,11 @@ # Overview -Appsheets is a powerful API that allows you to quickly and easily create apps -that can be used on a variety of devices, including smartphones, tablets, and -computers. With Appsheet, you can easily create apps that allow you to track -and manage your data, create reports and visualizations, and even share your -data with other users. Appsheet is also easy to use, with a simple interface -that makes it easy to get started. +The Google Appsheet API enables programmatic interactions with your custom AppSheet applications, allowing you to streamline processes, automate actions, and interlink your apps with other services. Leveraging Pipedream's powerful serverless platform, you can create workflows that react to events in real-time, automate tasks, and connect to countless other services with minimal effort. Whether you're updating datasets, syncing with external systems, or triggering complex chains of actions, combining AppSheet with Pipedream can supercharge your productivity and enhance your app's capabilities. + +# Example Use Cases + +- **Automated Data Syncing Between AppSheet and Google Sheets**: Create a workflow that listens for updates in your AppSheet app and automatically reflects those changes in a connected Google Sheet. This ensures data consistency between your app and the spreadsheet, providing an up-to-date backup and an easy way to generate reports. + +- **Dynamic Email Alerts Based on AppSheet Events**: Set up a Pipedream workflow that triggers an email notification via an app like SendGrid whenever a specific event occurs in AppSheet, such as a new order being placed. This keeps stakeholders informed in real-time and can be customized for different recipients and criteria. + +- **Scheduled Data Backups to Cloud Storage**: Configure a recurring workflow on Pipedream that fetches data from your AppSheet application at regular intervals and stores backups in a cloud service such as Amazon S3. This automation ensures you always have a recent backup of your AppSheet data without manual intervention. diff --git a/components/google_appsheet/actions/add-row/add-row.mjs b/components/google_appsheet/actions/add-row/add-row.mjs new file mode 100644 index 0000000000000..2b87b541a56e6 --- /dev/null +++ b/components/google_appsheet/actions/add-row/add-row.mjs @@ -0,0 +1,19 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "google_appsheet-add-row", + name: "Add Row", + description: "Adds a new row to a specific table in the AppSheet app. [See the documentation](https://support.google.com/appsheet/answer/10104797?hl=en&ref_topic=10105767&sjid=1665780.0.1444403316-SA#)", + version: "0.0.1", + type: "action", + methods: { + ...common.methods, + getAction() { + return "Add"; + }, + getSummary() { + return "Added a new row successfully"; + }, + }, +}; diff --git a/components/google_appsheet/actions/common/base.mjs b/components/google_appsheet/actions/common/base.mjs new file mode 100644 index 0000000000000..30d5e095636c4 --- /dev/null +++ b/components/google_appsheet/actions/common/base.mjs @@ -0,0 +1,47 @@ +import { parseObject } from "../../common/utils.mjs"; +import appsheet from "../../google_appsheet.app.mjs"; + +export default { + props: { + appsheet, + tableName: { + propDefinition: [ + appsheet, + "tableName", + ], + }, + row: { + propDefinition: [ + appsheet, + "row", + ], + }, + }, + methods: { + getData() { + return {}; + }, + }, + async run({ $ }) { + const dataRow = parseObject(this.row); + const rows = dataRow + ? [ + dataRow, + ] + : []; + + const response = await this.appsheet.post({ + $, + tableName: this.tableName, + data: { + Action: this.getAction(), + Rows: rows, + ...this.getData(), + }, + }); + console.log("response: ", response); + + $.export("$summary", this.getSummary(response)); + return response; + }, +}; diff --git a/components/google_appsheet/actions/delete-row/delete-row.mjs b/components/google_appsheet/actions/delete-row/delete-row.mjs new file mode 100644 index 0000000000000..68d2fd6d6fa3d --- /dev/null +++ b/components/google_appsheet/actions/delete-row/delete-row.mjs @@ -0,0 +1,35 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "google_appsheet-delete-row", + name: "Delete Row", + description: "Deletes a specific row from a table in the AppSheet app. [See the documentation](https://support.google.com/appsheet/answer/10105399?hl=en&ref_topic=10105767&sjid=1665780.0.1444403316-SA)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + alert: { + type: "alert", + alertType: "info", + content: "The `Row` value may contain field values of the key field values of the record to be deleted.", + }, + row: { + propDefinition: [ + common.props.appsheet, + "row", + ], + description: "The `Row` value may contain field values of the key field values of the record to be deleted.", + optional: true, + }, + }, + methods: { + ...common.methods, + getAction() { + return "Delete"; + }, + getSummary(response) { + return `${response.Rows.length} successfully delete!`; + }, + }, +}; diff --git a/components/google_appsheet/actions/get-rows/get-rows.mjs b/components/google_appsheet/actions/get-rows/get-rows.mjs new file mode 100644 index 0000000000000..6b4fc95f7d3f3 --- /dev/null +++ b/components/google_appsheet/actions/get-rows/get-rows.mjs @@ -0,0 +1,45 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "google_appsheet-get-rows", + name: "Get Rows", + description: "Read existing records in a table in the AppSheet app. [See the documentation](https://support.google.com/appsheet/answer/10104797?hl=en&ref_topic=10105767&sjid=1665780.0.1444403316-SA#)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + selector: { + type: "string", + label: "Selector", + description: "You can specify an expression to select and format the rows returned. **Example: Filter(TableName, [Column] = \"Value\")** [See the documentation](https://support.google.com/appsheet/answer/10105770?hl=en&ref_topic=10105767&sjid=3242006823758562345-NC)", + optional: true, + }, + row: { + propDefinition: [ + common.props.appsheet, + "row", + ], + description: "You can also filter the results using the `Row` value. The `Row` value may contain field values of the key field values of the record to be retrieved. **Example:** `{ \"First Name\": \"John\" }`", + optional: true, + }, + }, + methods: { + ...common.methods, + getAction() { + return "Find"; + }, + getData() { + return this.selector + ? { + Properties: { + Selector: this.selector, + }, + } + : {}; + }, + getSummary(response) { + return `Successfully retrieved ${ response.length || 0} rows`; + }, + }, +}; diff --git a/components/google_appsheet/actions/update-row/update-row.mjs b/components/google_appsheet/actions/update-row/update-row.mjs new file mode 100644 index 0000000000000..a8809fab8eb24 --- /dev/null +++ b/components/google_appsheet/actions/update-row/update-row.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "google_appsheet-update-row", + name: "Update Row", + description: "Updates an existing row in a specific table in the AppSheet app. [See the documentation](https://support.google.com/appsheet/answer/10105002?hl=en&ref_topic=10105767&sjid=1665780.0.1444403316-SA)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + alert: { + type: "alert", + alertType: "info", + content: `The \`Row\` value must include the key field values of the record to be updated. + \nThe \`Row\` value may contain one or more field values of other fields to be updated in the record. + \nIf a field's name is omitted, that field's value is not changed. If the field can be assigned a string value and the field value you specify is "" then the field's value will be cleared.`, + }, + }, + methods: { + ...common.methods, + getAction() { + return "Edit"; + }, + getSummary(response) { + return `${response.Rows.length} successfully updated!`; + }, + }, +}; diff --git a/components/google_appsheet/app/google_appsheet.app.ts b/components/google_appsheet/app/google_appsheet.app.ts deleted file mode 100644 index faa122b0a8bbf..0000000000000 --- a/components/google_appsheet/app/google_appsheet.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "google_appsheet", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); diff --git a/components/google_appsheet/common/utils.mjs b/components/google_appsheet/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/google_appsheet/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/google_appsheet/google_appsheet.app.mjs b/components/google_appsheet/google_appsheet.app.mjs new file mode 100644 index 0000000000000..a69f338378891 --- /dev/null +++ b/components/google_appsheet/google_appsheet.app.mjs @@ -0,0 +1,43 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "google_appsheet", + propDefinitions: { + tableName: { + type: "string", + label: "Table Name", + description: "Name of the table. **Select Data > Tables** and expand the table details to view the table name.", + }, + row: { + type: "object", + label: "Row", + description: "JSON object representing the row data", + }, + }, + methods: { + _baseUrl(tableName) { + return `https://api.appsheet.com/api/v2/apps/${this.$auth.app_id}/tables/${encodeURIComponent(tableName)}/Action`; + }, + _headers() { + return { + "ApplicationAccessKey": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, tableName, ...opts + }) { + return axios($, { + url: this._baseUrl(tableName), + headers: this._headers(), + ...opts, + }); + }, + post(opts = {}) { + return this._makeRequest({ + method: "POST", + ...opts, + }); + }, + }, +}; diff --git a/components/google_appsheet/package.json b/components/google_appsheet/package.json index c46728211ea7f..662868a02a3a3 100644 --- a/components/google_appsheet/package.json +++ b/components/google_appsheet/package.json @@ -1,18 +1,18 @@ { "name": "@pipedream/google_appsheet", - "version": "0.0.3", + "version": "0.1.0", "description": "Pipedream Google Appsheet Components", - "main": "dist/app/google_appsheet.app.mjs", + "main": "google_appsheet.app.mjs", "keywords": [ "pipedream", "google_appsheet" ], - "files": [ - "dist" - ], "homepage": "https://pipedream.com/apps/google_appsheet", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/google_calendar/README.md b/components/google_calendar/README.md index 195a2b77d138a..f29f854d5bcf0 100644 --- a/components/google_calendar/README.md +++ b/components/google_calendar/README.md @@ -1,20 +1,11 @@ # Overview -The Google Calendar API gives developers access to Google Calendar data, -allowing them to create their own applications that can read, write, and update -calendar data. With the API, users can integrate their own calendar -applications with Google Calendar, giving them the ability to manage their -calendars in one place. The API also provides the ability to search for events -and create new events. +The Google Calendar API lets you dip into the powerhouse of scheduling, allowing for the reading, creation, and manipulation of events and calendars directly from your applications. Through Pipedream, you can seamlessly integrate Google Calendar into a myriad of workflows, automating event management, syncing with other services, setting up custom reminders, or even collating data for reporting. The key here is to streamline your calendar-related processes, ensuring that your time management is as efficient and automated as possible. -Possible applications that could be built using the Google Calendar API -include: +# Example Use Cases -- A calendar application that integrates with Google Calendar, allowing users - to manage their calendars in one place. -- A calendar application that allows users to search for events and create new - events. -- A to-do list application that integrates with Google Calendar, allowing users - to see their tasks and events in one place. -- A reminder application that uses Google Calendar data to remind users of - upcoming events. +- **Event Synchronization Across Platforms**: Create a workflow that listens for new events on Google Calendar and replicates them on other platforms like Outlook or Apple Calendar. This ensures your schedule remains in sync across different services. + +- **Automated Meeting Prep**: Set up a Pipedream workflow that triggers a sequence of events whenever a new meeting is scheduled on Google Calendar. This could send out reminder emails with attached agendas to participants, book conference rooms through your office management system, or check the attendees' LinkedIn profiles to provide you with recent updates about them before the meeting. + +- **Dynamic Event Response**: Implement a workflow that reacts to updated or canceled events on Google Calendar. On event cancellation, it could trigger a message on Slack to inform team members and update project management tools to reschedule associated tasks. When an event is updated, it can automatically adjust reminders or send out new invites if the attendee list changes. diff --git a/components/google_calendar/actions/add-attendees-to-event/add-attendees-to-event.mjs b/components/google_calendar/actions/add-attendees-to-event/add-attendees-to-event.mjs new file mode 100644 index 0000000000000..7c4e455c853c3 --- /dev/null +++ b/components/google_calendar/actions/add-attendees-to-event/add-attendees-to-event.mjs @@ -0,0 +1,61 @@ +import googleCalendar from "../../google_calendar.app.mjs"; +import createEventCommon from "../common/create-event-common.mjs"; + +export default { + key: "google_calendar-add-attendees-to-event", + name: "Add Attendees To Event", + description: "Add attendees to an existing event. [See the documentation](https://googleapis.dev/nodejs/googleapis/latest/calendar/classes/Resource$Events.html#update)", + version: "0.0.2", + type: "action", + props: { + googleCalendar, + calendarId: { + propDefinition: [ + googleCalendar, + "calendarId", + ], + }, + eventId: { + propDefinition: [ + googleCalendar, + "eventId", + (c) => ({ + calendarId: c.calendarId, + }), + ], + }, + attendees: { + label: "Attendees", + type: "string", + description: "Enter either an array or a comma separated list of email addresses of attendees", + }, + sendUpdates: { + propDefinition: [ + googleCalendar, + "sendUpdates", + ], + }, + }, + async run({ $ }) { + const updatedAttendees = createEventCommon.methods.formatAttendees(this.attendees); + const currentEvent = await this.googleCalendar.getEvent({ + eventId: this.eventId, + calendarId: this.calendarId, + }); + if (currentEvent?.attendees && currentEvent.attendees.length) { + updatedAttendees.push(...currentEvent.attendees); + } + const response = await this.googleCalendar.updateEvent({ + calendarId: this.calendarId, + eventId: this.eventId, + sendUpdates: this.sendUpdates, + requestBody: { + ...currentEvent, + attendees: updatedAttendees, + }, + }); + + $.export("$summary", `Successfully updated event attendees: "${response.id}"`); + return response; + }, +}; diff --git a/components/google_calendar/actions/common/create-event-common.mjs b/components/google_calendar/actions/common/create-event-common.mjs index c33223ea134fa..b0f8d146491c3 100644 --- a/components/google_calendar/actions/common/create-event-common.mjs +++ b/components/google_calendar/actions/common/create-event-common.mjs @@ -1,4 +1,5 @@ -import googleCalendar from "../../google_calendar.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; +import constants from "../../common/constants.mjs"; export default { props: ({ isUpdate }) => ( @@ -9,6 +10,18 @@ export default { description: "Enter a title for the event, (e.g., `My event`)", optional: true, }, + eventStartDate: { + label: "Event Start Date", + type: "string", + description: "For all-day events, enter the Event day in the format `yyyy-mm-dd`. For events with time, format according to [RFC3339](https://www.rfc-editor.org/rfc/rfc3339.html#section-1): `yyyy-mm-ddThh:mm:ss+01:00`. A time zone offset is required unless a time zone is explicitly specified in timeZone.", + optional: isUpdate, + }, + eventEndDate: { + label: "Event End Date", + type: "string", + description: "For all-day events, enter the Event day in the format `yyyy-mm-dd`. For events with time, format according to [RFC3339](https://www.rfc-editor.org/rfc/rfc3339.html#section-1): `yyyy-mm-ddThh:mm:ss+01:00`. A time zone offset is required unless a time zone is explicitly specified in timeZone.", + optional: isUpdate, + }, location: { label: "Event Location", type: "string", @@ -23,45 +36,38 @@ export default { }, attendees: { label: "Attendees", - type: "string[]", - description: "Enter an array of email addresses for any attendees", + type: "string", + description: "Enter either an array or a comma separated list of email addresses of attendees", optional: true, }, - eventStartDate: { - label: "Event Start Date", + repeatFrequency: { type: "string", - description: "For all-day events, enter the Event day in the format `yyyy-mm-dd`. For events with time, format according to [RFC3339](https://www.rfc-editor.org/rfc/rfc3339.html#section-1): `yyyy-mm-ddThh:mm:ss+01:00`. A time zone offset is required unless a time zone is explicitly specified in timeZone.", - optional: isUpdate, - }, - eventEndDate: { - label: "Event End Date", - type: "string", - description: "For all-day events, enter the Event day in the format `yyyy-mm-dd`. For events with time, format according to [RFC3339](https://www.rfc-editor.org/rfc/rfc3339.html#section-1): `yyyy-mm-ddThh:mm:ss+01:00`. A time zone offset is required unless a time zone is explicitly specified in timeZone.", - optional: isUpdate, - }, - recurrence: { - label: "Recurrence", - type: "string[]", - description: "Recurrence rule(s) for the event. For example, `FREQ=DAILY;INTERVAL=2` means once every two days, `RRULE:FREQ=YEARLY` means annually.\nYou can combine multiple recurrence rules. [See the documentation](https://developers.google.com/calendar/api/concepts/events-calendars#recurrence_rule)", + label: "Repeat Frequency", + description: "Select a frequency to make this event repeating", optional: true, + options: Object.keys(constants.REPEAT_FREQUENCIES), + reloadProps: true, }, - timeZone: { - propDefinition: [ - googleCalendar, - "timeZone", - ], + repeatInterval: { + type: "integer", + label: "Repeat Interval", + description: "Enter 1 to \"repeat every day\", enter 2 to \"repeat every other day\", etc. Defaults to 1.", + optional: true, + hidden: true, }, - sendUpdates: { - propDefinition: [ - googleCalendar, - "sendUpdates", - ], + repeatUntil: { + type: "string", + label: "Repeat Until", + description: "The event will repeat only until this date, if set", + optional: true, + hidden: true, }, - sendNotifications: { - propDefinition: [ - googleCalendar, - "sendNotifications", - ], + repeatTimes: { + type: "integer", + label: "Repeat How Many Times?", + description: "Limit the number of times this event will occur", + optional: true, + hidden: true, }, } ), @@ -86,6 +92,11 @@ export default { * ] */ let attendees = []; + if (typeof selectedAttendees === "string") { + selectedAttendees = selectedAttendees.includes("[") && selectedAttendees.includes("]") + ? JSON.parse(selectedAttendees) + : selectedAttendees.replaceAll(" ", "").split(","); + } if (selectedAttendees && Array.isArray(selectedAttendees)) { attendees = selectedAttendees.map((email) => ({ email, @@ -119,5 +130,37 @@ export default { timeZone, }; }, + /** + * Format recurrence prop + * https://developers.google.com/calendar/api/concepts/events-calendars#recurrence_rule + */ + formatRecurrence({ + repeatFrequency, + repeatInterval, + repeatTimes, + repeatUntil, + }) { + if (!repeatFrequency) { + return; + } + if (repeatTimes && repeatUntil) { + throw new ConfigurationError("Only one of `Repeat Until` or Repeat `How Many Times` may be entered"); + } + + let recurrence = `RRULE:FREQ=${repeatFrequency}`; + if (repeatInterval) { + recurrence = `${recurrence};INTERVAL=${repeatInterval}`; + } + if (repeatTimes) { + recurrence = `${recurrence};COUNT=${repeatTimes}`; + } + if (repeatUntil) { + const date = repeatUntil.slice(0, 10).replaceAll("-", ""); + recurrence = `${recurrence};UNTIL=${date}`; + } + return [ + recurrence, + ]; + }, }, }; diff --git a/components/google_calendar/actions/create-event/create-event.mjs b/components/google_calendar/actions/create-event/create-event.mjs index 6ed199677b76e..6470edf1f05e0 100644 --- a/components/google_calendar/actions/create-event/create-event.mjs +++ b/components/google_calendar/actions/create-event/create-event.mjs @@ -1,48 +1,145 @@ import googleCalendar from "../../google_calendar.app.mjs"; import createEventCommon from "../common/create-event-common.mjs"; import { v4 as uuidv4 } from "uuid"; +import constants from "../../common/constants.mjs"; export default { key: "google_calendar-create-event", name: "Create Event", - description: "Create an event to the Google Calendar. [See the documentation](https://googleapis.dev/nodejs/googleapis/latest/calendar/classes/Resource$Events.html#insert)", - version: "0.2.1", + description: "Create an event in a Google Calendar. [See the documentation](https://developers.google.com/calendar/api/v3/reference/events/insert)", + version: "0.2.4", type: "action", props: { googleCalendar, + addType: { + type: "string", + label: "Type of Add", + description: "Whether to perform a quick add or a detailed event", + options: [ + { + label: "Add Detailed Event", + value: "detailed", + }, + { + label: "Add Quick Event using Natural Language", + value: "quick", + }, + ], + reloadProps: true, + }, calendarId: { propDefinition: [ googleCalendar, "calendarId", ], }, - createMeetRoom: { - label: "Create Meet Room", - description: "Create a Google Meet room for this event.", - type: "boolean", + text: { + type: "string", + label: "Describe Event", + description: "Write a plain text description of event, and Google will parse this string to create the event. eg. 'Meet with Michael 10am 7/22/2024' or 'Call Sarah at 1:30PM on Friday'", + hidden: true, + }, + summary: { + label: "Event Title", + type: "string", + description: "Enter a title for the event, (e.g., `My event`)", optional: true, + hidden: true, }, - ...createEventCommon.props({ - isUpdate: false, - }), colorId: { propDefinition: [ googleCalendar, "colorId", ], + hidden: true, + }, + timeZone: { + propDefinition: [ + googleCalendar, + "timeZone", + ], + hidden: true, + }, + sendUpdates: { + propDefinition: [ + googleCalendar, + "sendUpdates", + ], + hidden: true, + }, + createMeetRoom: { + type: "boolean", + label: "Create Meet Room", + description: "Whether to create a Google Meet room for this event.", + optional: true, + hidden: true, + }, + visibility: { + type: "string", + label: "Visibility", + description: "Visibility of the event", + options: [ + "default", + "public", + "private", + "confidential", + ], + optional: true, + hidden: true, }, }, + async additionalProps(props) { + const isDetailed = this.addType === "detailed"; + + props.text.hidden = isDetailed; + + props.summary.hidden = !isDetailed; + props.colorId.hidden = !isDetailed; + props.timeZone.hidden = !isDetailed; + props.sendUpdates.hidden = !isDetailed; + props.createMeetRoom.hidden = !isDetailed; + props.visibility.hidden = !isDetailed; + + if (isDetailed) { + const commonProps = createEventCommon.props({ + isUpdate: false, + }); + if (this.repeatFrequency) { + const frequency = constants.REPEAT_FREQUENCIES[this.repeatFrequency]; + commonProps.repeatInterval.description = `Enter 1 to "repeat every ${frequency}", enter 2 to "repeat every other ${frequency}", etc. Defaults to 1.`; + commonProps.repeatInterval.hidden = !this.repeatFrequency; + commonProps.repeatUntil.hidden = !this.repeatFrequency; + commonProps.repeatTimes.hidden = !this.repeatFrequency; + } + return commonProps; + } + return {}; + }, methods: { ...createEventCommon.methods, }, async run({ $ }) { - const timeZone = this.getTimeZone(this.timeZone); + if (this.addType === "quick") { + const quickResponse = await this.googleCalendar.quickAddEvent({ + calendarId: this.calendarId, + text: this.text, + }); + $.export("$summary", `Successfully added a quick event: "${quickResponse.id}"`); + return quickResponse; + } + + const timeZone = await this.getTimeZone(this.timeZone); const attendees = this.formatAttendees(this.attendees); + const recurrence = this.formatRecurrence({ + repeatFrequency: this.repeatFrequency, + repeatInterval: this.repeatInterval, + repeatTimes: this.repeatTimes, + repeatUntil: this.repeatUntil, + }); const data = { calendarId: this.calendarId, sendUpdates: this.sendUpdates, - sendNotifications: this.sendNotifications, resource: { summary: this.summary, location: this.location, @@ -55,9 +152,10 @@ export default { date: this.eventEndDate, timeZone, }), - recurrence: this.recurrence, + recurrence, attendees, colorId: this.colorId, + visibility: this.visibility, }, }; @@ -75,7 +173,7 @@ export default { const response = await this.googleCalendar.createEvent(data); - $.export("$summary", `Successfully created event: "${response.id}"`); + $.export("$summary", `Successfully created event with ID: "${response.id}"`); return response; }, diff --git a/components/google_calendar/actions/delete-event/delete-event.mjs b/components/google_calendar/actions/delete-event/delete-event.mjs index 407f9679883a4..c5887e5f79205 100644 --- a/components/google_calendar/actions/delete-event/delete-event.mjs +++ b/components/google_calendar/actions/delete-event/delete-event.mjs @@ -3,8 +3,8 @@ import googleCalendar from "../../google_calendar.app.mjs"; export default { key: "google_calendar-delete-event", name: "Delete an Event", - description: "Delete an event to the Google Calendar. [See the documentation](https://googleapis.dev/nodejs/googleapis/latest/calendar/classes/Resource$Events.html#delete)", - version: "0.1.4", + description: "Delete an event from a Google Calendar. [See the documentation](https://googleapis.dev/nodejs/googleapis/latest/calendar/classes/Resource$Events.html#delete)", + version: "0.1.6", type: "action", props: { googleCalendar, diff --git a/components/google_calendar/actions/get-calendar/get-calendar.mjs b/components/google_calendar/actions/get-calendar/get-calendar.mjs index a55e5bc253499..76c70d6596fb5 100644 --- a/components/google_calendar/actions/get-calendar/get-calendar.mjs +++ b/components/google_calendar/actions/get-calendar/get-calendar.mjs @@ -4,7 +4,7 @@ export default { key: "google_calendar-get-calendar", name: "Retrieve Calendar Details", description: "Retrieve calendar details of a Google Calendar. [See the documentation](https://googleapis.dev/nodejs/googleapis/latest/calendar/classes/Resource$Calendars.html#get)", - version: "0.1.5", + version: "0.1.7", type: "action", props: { googleCalendar, diff --git a/components/google_calendar/actions/get-event/get-event.mjs b/components/google_calendar/actions/get-event/get-event.mjs index 039e7923bdac8..6c944d0d7af1e 100644 --- a/components/google_calendar/actions/get-event/get-event.mjs +++ b/components/google_calendar/actions/get-event/get-event.mjs @@ -4,7 +4,7 @@ export default { key: "google_calendar-get-event", name: "Retrieve Event Details", description: "Retrieve event details from Google Calendar. [See the documentation](https://googleapis.dev/nodejs/googleapis/latest/calendar/classes/Resource$Events.html#get)", - version: "0.1.5", + version: "0.1.7", type: "action", props: { googleCalendar, diff --git a/components/google_calendar/actions/list-calendars/list-calendars.mjs b/components/google_calendar/actions/list-calendars/list-calendars.mjs index 62422f704c74c..c9e07b17073d7 100644 --- a/components/google_calendar/actions/list-calendars/list-calendars.mjs +++ b/components/google_calendar/actions/list-calendars/list-calendars.mjs @@ -4,7 +4,7 @@ export default { key: "google_calendar-list-calendars", name: "List Calendars", description: "Retrieve a list of calendars from Google Calendar. [See the documentation](https://googleapis.dev/nodejs/googleapis/latest/calendar/classes/Resource$Calendarlist.html#list)", - version: "0.1.5", + version: "0.1.7", type: "action", props: { googleCalendar, @@ -12,7 +12,9 @@ export default { async run({ $ }) { const { items: calendars } = await this.googleCalendar.listCalendars(); - $.export("$summary", `Successfully retrieved ${calendars.length} calendar(s)`); + $.export("$summary", `Successfully retrieved ${calendars.length} calendar${calendars.length === 1 + ? "" + : "s"}`); return calendars; }, diff --git a/components/google_calendar/actions/list-events-by-type/list-events-by-type.mjs b/components/google_calendar/actions/list-events-by-type/list-events-by-type.mjs deleted file mode 100644 index e7ac9e5561ce0..0000000000000 --- a/components/google_calendar/actions/list-events-by-type/list-events-by-type.mjs +++ /dev/null @@ -1,56 +0,0 @@ -import googleCalendar from "../../google_calendar.app.mjs"; -import utils from "../../common/utils.mjs"; - -export default { - key: "google_calendar-list-events-by-type", - name: "List Calendar Events by Type", - description: "Retrieve a list of events filtered by type (\"default\", \"focusTime\", \"outOfOffice\", \"workingLocation\") from the Google Calendar. [See the documentation](https://developers.google.com/calendar/api/v3/reference/events/list)", - version: "0.0.1", - type: "action", - props: { - googleCalendar, - calendarId: { - propDefinition: [ - googleCalendar, - "calendarId", - ], - }, - eventTypes: { - propDefinition: [ - googleCalendar, - "eventTypes", - ], - }, - maxResults: { - propDefinition: [ - googleCalendar, - "maxResults", - ], - }, - }, - async run({ $ }) { - const args = utils.filterEmptyValues({ - calendarId: this.calendarId, - eventTypes: this.eventTypes, - }); - const events = []; - let total, done = false, count = 0; - do { - const response = await this.googleCalendar.listEvents(args); - for (const item of response.items) { - events.push(item); - count++; - if (this.maxResults && count >= this.maxResults) { - done = true; - break; - } - } - total = response.items?.length; - args.syncToken = response.nextSyncToken; - } while (total && !done); - - $.export("$summary", `Successfully retrieved ${events.length} event(s)`); - - return events; - }, -}; diff --git a/components/google_calendar/actions/list-events/list-events.mjs b/components/google_calendar/actions/list-events/list-events.mjs index 7841a29cf0ae6..6c1796aebd35e 100644 --- a/components/google_calendar/actions/list-events/list-events.mjs +++ b/components/google_calendar/actions/list-events/list-events.mjs @@ -1,11 +1,12 @@ import googleCalendar from "../../google_calendar.app.mjs"; import utils from "../../common/utils.mjs"; +import { ConfigurationError } from "@pipedream/platform"; export default { key: "google_calendar-list-events", name: "List Events", description: "Retrieve a list of event from the Google Calendar. [See the documentation](https://developers.google.com/calendar/api/v3/reference/events/list)", - version: "0.0.5", + version: "0.0.7", type: "action", props: { googleCalendar, @@ -40,12 +41,6 @@ export default { ], default: "", }, - pageToken: { - propDefinition: [ - googleCalendar, - "pageToken", - ], - }, privateExtendedProperty: { propDefinition: [ googleCalendar, @@ -82,12 +77,6 @@ export default { "singleEvents", ], }, - syncToken: { - propDefinition: [ - googleCalendar, - "syncToken", - ], - }, timeMax: { propDefinition: [ googleCalendar, @@ -120,30 +109,44 @@ export default { }, }, async run({ $ }) { + if (this.orderBy === "startTime" && !this.singleEvents) { + throw new ConfigurationError("Single Events must be `true` to order by `startTime`"); + } + const args = utils.filterEmptyValues({ calendarId: this.calendarId, iCalUID: this.iCalUID, maxAttendees: this.maxAttendees, - maxResults: this.maxResults, orderBy: this.orderBy || undefined, - pageToken: this.pageToken, privateExtendedProperty: this.privateExtendedProperty, q: this.q, sharedExtendedProperty: this.sharedExtendedProperty, showDeleted: this.showDeleted, showHiddenInvitations: this.showHiddenInvitations, singleEvents: this.singleEvents, - syncToken: this.syncToken, timeMax: this.timeMax, timeMin: this.timeMin, timeZone: this.timeZone, updatedMin: this.updatedMin, eventTypes: this.eventTypes, }); - const response = await this.googleCalendar.listEvents(args); - $.export("$summary", `Successfully retrieved ${response.items.length} event(s)`); + const events = []; + do { + const { + items, nextPageToken, + } = await this.googleCalendar.listEvents(args); + events.push(...items); + args.pageToken = nextPageToken; + } while (args.pageToken && (!this.maxResults || events.length < this.maxResults)); + if (events.length > this.maxResults) { + events.length = this.maxResults; + } + + $.export("$summary", `Successfully retrieved ${events.length} event${events.length === 1 + ? "" + : "s"}`); - return response; + return events; }, }; diff --git a/components/google_calendar/actions/query-free-busy-calendars/query-free-busy-calendars.mjs b/components/google_calendar/actions/query-free-busy-calendars/query-free-busy-calendars.mjs index c72ac886d2271..5ecdfddc5244f 100644 --- a/components/google_calendar/actions/query-free-busy-calendars/query-free-busy-calendars.mjs +++ b/components/google_calendar/actions/query-free-busy-calendars/query-free-busy-calendars.mjs @@ -4,7 +4,7 @@ export default { key: "google_calendar-query-free-busy-calendars", name: "Retrieve Free/Busy Calendar Details", description: "Retrieve free/busy calendar details from Google Calendar. [See the documentation](https://googleapis.dev/nodejs/googleapis/latest/calendar/classes/Resource$Freebusy.html#query)", - version: "0.1.5", + version: "0.1.7", type: "action", props: { googleCalendar, diff --git a/components/google_calendar/actions/quick-add-event/quick-add-event.mjs b/components/google_calendar/actions/quick-add-event/quick-add-event.mjs index a93aa28dbcd87..bdacef99c7e2b 100644 --- a/components/google_calendar/actions/quick-add-event/quick-add-event.mjs +++ b/components/google_calendar/actions/quick-add-event/quick-add-event.mjs @@ -1,10 +1,11 @@ import googleCalendar from "../../google_calendar.app.mjs"; +import createEventCommon from "../common/create-event-common.mjs"; export default { key: "google_calendar-quick-add-event", name: "Add Quick Event", description: "Create a quick event to the Google Calendar. [See the documentation](https://googleapis.dev/nodejs/googleapis/latest/calendar/classes/Resource$Events.html#quickAdd)", - version: "0.1.4", + version: "0.1.6", type: "action", props: { googleCalendar, @@ -15,9 +16,15 @@ export default { ], }, text: { - label: "Event Title", type: "string", - description: "Enter a title for the event, (e.g., `My event`)", + label: "Describe Event", + description: "Write a plain text description of event, and Google will parse this string to create the event. eg. 'Meet with Michael 10am 7/22/2024' or 'Call Sarah at 1:30PM on Friday'", + }, + attendees: { + label: "Attendees", + type: "string", + description: "Enter either an array or a comma separated list of email addresses of attendees", + optional: true, }, }, async run({ $ }) { @@ -26,6 +33,18 @@ export default { text: this.text, }); + if (this.attendees) { + const update = await this.googleCalendar.updateEvent({ + calendarId: this.calendarId, + eventId: response.id, + requestBody: { + ...response, + attendees: createEventCommon.methods.formatAttendees(this.attendees), + }, + }); + response.attendees = update.attendees; + } + $.export("$summary", `Successfully added a quick event: "${response.id}"`); return response; diff --git a/components/google_calendar/actions/update-event-attendees/update-event-attendees.mjs b/components/google_calendar/actions/update-event-attendees/update-event-attendees.mjs deleted file mode 100644 index e5d820ca09473..0000000000000 --- a/components/google_calendar/actions/update-event-attendees/update-event-attendees.mjs +++ /dev/null @@ -1,75 +0,0 @@ -import googleCalendar from "../../google_calendar.app.mjs"; - -export default { - key: "google_calendar-update-event-attendees", - name: "Update attendees of an event", - description: "Update attendees of an existing event. [See the documentation](https://googleapis.dev/nodejs/googleapis/latest/calendar/classes/Resource$Events.html#update)", - version: "0.1.4", - type: "action", - props: { - googleCalendar, - calendarId: { - propDefinition: [ - googleCalendar, - "calendarId", - ], - }, - eventId: { - propDefinition: [ - googleCalendar, - "eventId", - (c) => ({ - calendarId: c.calendarId, - }), - ], - }, - attendees: { - label: "Attendees", - type: "string[]", - description: "Enter an array of email addresses for any attendees", - }, - sendUpdates: { - propDefinition: [ - googleCalendar, - "sendUpdates", - ], - }, - sendNotifications: { - propDefinition: [ - googleCalendar, - "sendNotifications", - ], - }, - }, - methods: { - formatAttendees(selectedAttendees) { - let attendees = []; - if (selectedAttendees && Array.isArray(selectedAttendees)) { - attendees = selectedAttendees.map((email) => ({ - email, - })); - } - return attendees; - }, - }, - async run({ $ }) { - const updatedAttendees = this.formatAttendees(this.attendees); - const currentEvent = await this.googleCalendar.getEvent({ - eventId: this.eventId, - calendarId: this.calendarId, - }); - const response = await this.googleCalendar.updateEvent({ - calendarId: this.calendarId, - eventId: this.eventId, - sendUpdates: this.sendUpdates, - sendNotifications: this.sendNotifications, - requestBody: { - ...currentEvent, - attendees: updatedAttendees, - }, - }); - - $.export("$summary", `Successfully updated event attendees: "${response.id}"`); - return response; - }, -}; diff --git a/components/google_calendar/actions/update-event/update-event.mjs b/components/google_calendar/actions/update-event/update-event.mjs index 9be5f19c1daf9..b8583faa1f5a1 100644 --- a/components/google_calendar/actions/update-event/update-event.mjs +++ b/components/google_calendar/actions/update-event/update-event.mjs @@ -1,11 +1,12 @@ import googleCalendar from "../../google_calendar.app.mjs"; import createEventCommon from "../common/create-event-common.mjs"; +import constants from "../../common/constants.mjs"; export default { key: "google_calendar-update-event", name: "Update Event", description: "Update an event from Google Calendar. [See the documentation](https://googleapis.dev/nodejs/googleapis/latest/calendar/classes/Resource$Events.html#update)", - version: "0.0.6", + version: "0.0.9", type: "action", props: { googleCalendar, @@ -27,6 +28,28 @@ export default { ...createEventCommon.props({ isUpdate: true, }), + timeZone: { + propDefinition: [ + googleCalendar, + "timeZone", + ], + }, + sendUpdates: { + propDefinition: [ + googleCalendar, + "sendUpdates", + ], + }, + }, + async additionalProps(props) { + if (this.repeatFrequency) { + const frequency = constants.REPEAT_FREQUENCIES[this.repeatFrequency]; + props.repeatInterval.description = `Enter 1 to "repeat every ${frequency}", enter 2 to "repeat every other ${frequency}", etc. Defaults to 1.`; + } + props.repeatInterval.hidden = !this.repeatFrequency; + props.repeatUntil.hidden = !this.repeatFrequency; + props.repeatTimes.hidden = !this.repeatFrequency; + return {}; }, methods: { ...createEventCommon.methods, @@ -37,27 +60,32 @@ export default { eventId: this.eventId, }); - const timeZone = this.getTimeZone(this.timeZone || currentEvent.start.timeZone); + const timeZone = await this.getTimeZone(this.timeZone || currentEvent.start.timeZone); const attendees = this.formatAttendees(this.attendees, currentEvent.attendees); + const recurrence = this.formatRecurrence({ + repeatFrequency: this.repeatFrequency, + repeatInterval: this.repeatInterval, + repeatTimes: this.repeatTimes, + repeatUntil: this.repeatUntil, + }); const response = await this.googleCalendar.updateEvent({ calendarId: this.calendarId, eventId: this.eventId, sendUpdates: this.sendUpdates, - sendNotifications: this.sendNotifications, requestBody: { summary: this.summary || currentEvent.summary, location: this.location || currentEvent.location, description: this.description || currentEvent.description, start: this.getDateParam({ - date: this.eventStartDate || currentEvent.start.dateTime, + date: this.eventStartDate || currentEvent.start.dateTime || currentEvent.start.date, timeZone: timeZone || currentEvent.start.timeZone, }), end: this.getDateParam({ - date: this.eventEndDate || currentEvent.end.dateTime, + date: this.eventEndDate || currentEvent.end.dateTime || currentEvent.start.date, timeZone: timeZone || currentEvent.end.timeZone, }), - recurrence: this.recurrence, + recurrence, attendees, }, }); diff --git a/components/google_calendar/common/constants.mjs b/components/google_calendar/common/constants.mjs index 82d70b6e3e84a..4cf4e1e5d0c79 100644 --- a/components/google_calendar/common/constants.mjs +++ b/components/google_calendar/common/constants.mjs @@ -78,6 +78,14 @@ const API = { }, }; +const REPEAT_FREQUENCIES = { + DAILY: "day", + WEEKLY: "week", + MONTHLY: "month", + YEARLY: "year", +}; + export default { API, + REPEAT_FREQUENCIES, }; diff --git a/components/google_calendar/google_calendar.app.mjs b/components/google_calendar/google_calendar.app.mjs index 296036e131151..4aa42ec1f1670 100644 --- a/components/google_calendar/google_calendar.app.mjs +++ b/components/google_calendar/google_calendar.app.mjs @@ -1,6 +1,7 @@ import timezones from "moment-timezone"; import calendar from "@googleapis/calendar"; import constants from "./common/constants.mjs"; +import { closest } from "color-2-name"; export default { type: "app", @@ -36,27 +37,27 @@ export default { label: "Event ID", type: "string", description: "Select an event from Google Calendar.", - async options({ - calendarId, prevContext, - }) { - const { nextPageToken } = prevContext; - if (nextPageToken === false) { - return []; - } + async options({ calendarId }) { + const monthAgo = new Date(new Date().setMonth(new Date().getMonth() - 1)).toISOString(); const response = await this.listEvents({ calendarId, - pageToken: nextPageToken, + maxResults: 100, + timeMin: monthAgo, }); - const options = response.items.map((item) => ({ - label: item.summary, - value: item.id, - })); - return { - options, - context: { - nextPageToken: response.nextPageToken ?? false, - }, - }; + const options = response.items.map((item) => { + let label = item.summary || item.id; + const date = item.start && (item.start.date + ? item.start.date + : item.start.dateTime.slice(0, 10)); + if (date) { + label += ` - ${date}`; + } + return { + label, + value: item.id, + }; + }); + return options.reverse(); }, }, iCalUID: { @@ -66,20 +67,20 @@ export default { type: "string", }, maxAttendees: { - label: "Max attendees", + label: "Max Attendees", description: "The maximum number of attendees to include in the response. If there are more than the specified number of attendees, only the participant is returned. Optional.", optional: true, type: "integer", }, maxResults: { - label: "Max results", + label: "Max Results", description: "Maximum number of events returned on one result page. The number of events in the resulting page may be less than this value, or none at all, even if there are more events matching the query. Incomplete pages can be detected by a non-empty nextPageToken field in the response. By default the value is 250 events. The page size can never be larger than 2500 events. Optional.", optional: true, type: "integer", }, orderBy: { - label: "Order by", - description: "The order of the events returned in the result. Optional. The default is an unspecified, stable order.", + label: "Order By", + description: "The order of the events returned in the result. Optional. The default is an unspecified, stable order. Must set Single Events to `true` to order by `startTime`.", optional: true, type: "string", options: [ @@ -94,12 +95,6 @@ export default { ], default: "startTime", }, - pageToken: { - label: "Page token", - description: "Token specifying which result page to return. Optional.", - optional: true, - type: "string", - }, privateExtendedProperty: { label: "Private extended property", description: "Extended properties constraint specified as propertyName=value. Matches only private properties. This parameter might be repeated multiple times to return events that match all given constraints.", @@ -113,37 +108,37 @@ export default { type: "string", }, sharedExtendedProperty: { - label: "Shared extended property", + label: "Shared Extended Property", description: "Extended properties constraint specified as propertyName=value. Matches only shared properties. This parameter might be repeated multiple times to return events that match all given constraints.", optional: true, type: "string", }, showDeleted: { - label: "Show deleted", + label: "Show Deleted", description: "Whether to include deleted events (with status equals \"cancelled\") in the result. Cancelled instances of recurring events (but not the underlying recurring event) will still be included if showDeleted and singleEvents are both False. If showDeleted and singleEvents are both True, only single instances of deleted events (but not the underlying recurring events) are returned. Optional. The default is False.", optional: true, type: "boolean", }, showHiddenInvitations: { - label: "Show hidden invitations", + label: "Show Hidden Invitations", description: "Whether to include hidden invitations in the result. Optional. The default is False.", optional: true, type: "boolean", }, singleEvents: { - label: "Single events", + label: "Single Events", description: "Whether to expand recurring events into instances and only return single one-off events and instances of recurring events, but not the underlying recurring events themselves. Optional. The default is False.", optional: true, type: "boolean", }, syncToken: { - label: "Sync token", + label: "Sync Token", description: "Token obtained from the nextSyncToken field returned on the last page of results from the previous list request. It makes the result of this list request contain only entries that have changed since then. All events deleted since the previous list request will always be in the result set and it is not allowed to set showDeleted to False.", optional: true, type: "string", }, timeMax: { - label: "Max time", + label: "Max Time", description: "Upper bound (exclusive) for an event's time to filter by. Must be an RFC3339 timestamp with mandatory time zone offset, for example, 2011-06-03T10:00:00-07:00, 2011-06-03T10:00:00Z. Milliseconds may be provided but are ignored. Must be greater than Min Time.", optional: true, type: "string", @@ -170,13 +165,13 @@ export default { }, }, updatedMin: { - label: "Minimum updated time", + label: "Minimum Updated Time", description: "Lower bound for an event's last modification time (as a RFC3339 timestamp) to filter by. When specified, entries deleted since this time will always be included regardless of showDeleted. Optional. The default is not to filter by last modification time.", optional: true, type: "string", }, ruleId: { - label: "ACL rule identifier", + label: "ACL Rule Identifier", type: "string", description: "ACL rule identifier.", async options({ @@ -255,16 +250,10 @@ export default { "none", ], }, - sendNotifications: { - label: "Send Notifications", - type: "boolean", - description: "Whether to send notifications about the event update", - optional: true, - }, colorId: { label: "Color ID", type: "string", - description: "The color of the event. This is an ID referring to an entry in the event section of the colors definition (see the colors endpoint).", + description: "The color assigned to this event on your calendar. You can only select a color from the list of event colors provided from your calendar. This setting will only affect your calendar.", optional: true, async options() { const response = await this.listColors(); @@ -272,7 +261,7 @@ export default { key, value, ]) => ({ - label: `Background ${value.background} | Foreground ${value.foreground}`, + label: `${closest(value.background).name} (${value.background})`, value: key, })); }, @@ -297,26 +286,6 @@ export default { refresh_token: this?.$auth?.oauth_refresh_token, }; }, - async calendarList() { - const calendar = this.client(); - return calendar.calendarList.list(); - }, - async list(config) { - const calendar = this.client(); - return calendar.events.list(config); - }, - // for config key value pairs - https://developers.google.com/calendar/v3/reference/events/list - async getEvents(config) { - return this.list(config); - }, - async watch(config) { - const calendar = this.client(); - return calendar.events.watch(config); - }, - async stop(config) { - const calendar = this.client(); - return calendar.channels.stop(config); - }, client() { const auth = new calendar.auth.OAuth2(); auth.setCredentials({ @@ -327,6 +296,27 @@ export default { auth, }); }, + retryWithExponentialBackoff(func, maxAttempts = 3, baseDelayS = 2) { + let attempt = 0; + + const execute = async () => { + try { + return await func(); + } catch (error) { + if (attempt >= maxAttempts) { + throw error; + } + + const delayMs = Math.pow(baseDelayS, attempt) * 1000; + await new Promise((resolve) => setTimeout(resolve, delayMs)); + + attempt++; + return execute(); + } + }; + + return execute(); + }, async requestHandler({ api, method, args = {}, }) { @@ -336,7 +326,8 @@ export default { } = args; try { const calendar = this.client(); - const response = await calendar[api][method](otherArgs); + const fn = () => calendar[api][method](otherArgs); + const response = await this.retryWithExponentialBackoff(fn); return returnOnlyData ? response.data : response; @@ -485,6 +476,9 @@ export default { args, }); }, + // Used to get the nextSyncToken. Since we don't need + // to actually retrieve any events, we set "updatedMin" + // to the current timestamp async fullSync(calendarId) { let nextSyncToken = null; let nextPageToken = null; @@ -493,6 +487,8 @@ export default { await this.listEvents({ calendarId, pageToken: nextPageToken, + orderBy: "updated", + updatedMin: new Date().toISOString(), }); nextPageToken = syncResp?.nextPageToken; nextSyncToken = syncResp?.nextSyncToken; diff --git a/components/google_calendar/package.json b/components/google_calendar/package.json index 657aed988707c..c63b99a82c8be 100644 --- a/components/google_calendar/package.json +++ b/components/google_calendar/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/google_calendar", - "version": "0.5.1", + "version": "0.5.7", "description": "Pipedream Google_calendar Components", "main": "google_calendar.app.mjs", "keywords": [ @@ -11,7 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "dependencies": { "@googleapis/calendar": "^1.0.2", - "@pipedream/platform": "^1.6.0", + "@pipedream/platform": "^3.0.0", + "color-2-name": "^1.4.4", "lodash.get": "^4.4.2", "moment-timezone": "^0.5.33", "uuid": "^8.3.2" diff --git a/components/google_calendar/sources/common/common.mjs b/components/google_calendar/sources/common/common.mjs index b326f8aba3777..f02d96b5fe35b 100644 --- a/components/google_calendar/sources/common/common.mjs +++ b/components/google_calendar/sources/common/common.mjs @@ -40,7 +40,7 @@ export default { start, } = event; return { - summary, + summary: summary || `Event ID: ${id}`, id, ts: +new Date(start.dateTime), }; @@ -48,9 +48,9 @@ export default { async processEvents(event) { const intervalData = this.getIntervalData(event); const config = this.getConfig(intervalData); - const resp = await this.googleCalendar.getEvents(config); + const resp = await this.googleCalendar.listEvents(config); - const events = resp?.data?.items; + const events = resp?.data?.items || resp?.items; if (!Array.isArray(events)) { console.log("nothing to emit"); return; diff --git a/components/google_calendar/sources/event-cancelled/event-cancelled.mjs b/components/google_calendar/sources/event-cancelled/event-cancelled.mjs index 08143bf9bc58d..2793f18236110 100644 --- a/components/google_calendar/sources/event-cancelled/event-cancelled.mjs +++ b/components/google_calendar/sources/event-cancelled/event-cancelled.mjs @@ -1,11 +1,12 @@ import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "google_calendar-event-cancelled", name: "New Cancelled Event", description: "Emit new event when a Google Calendar event is cancelled or deleted", - version: "0.1.7", + version: "0.1.10", type: "source", dedupe: "unique", props: { @@ -35,4 +36,5 @@ export default { return event.status === "cancelled"; }, }, + sampleEmit, }; diff --git a/components/google_calendar/sources/event-cancelled/test-event.mjs b/components/google_calendar/sources/event-cancelled/test-event.mjs new file mode 100644 index 0000000000000..1cde868ed0de5 --- /dev/null +++ b/components/google_calendar/sources/event-cancelled/test-event.mjs @@ -0,0 +1,31 @@ +export default { + "kind": "calendar#event", + "etag": "\"3442821871082000\"", + "id": "7aefibs2i2chltfk626kbtse91", + "status": "cancelled", + "htmlLink": "https://www.google.com/calendar/event?eid=N2FlZmliczJpMmNobHRmgcjA0bGJ0NDhhbmgyNmdvNmlzajRyZm1qbDBAZw", + "created": "2024-07-19T17:41:08.000Z", + "updated": "2024-07-19T17:42:15.541Z", + "summary": "test event", + "creator": { + "email": "test@sample.com" + }, + "organizer": { + "email": "r04l6isj4rfmjl0@group.calendar.google.com", + "displayName": "Test", + "self": true + }, + "start": { + "date": "2024-07-20" + }, + "end": { + "date": "2024-07-21" + }, + "transparency": "transparent", + "iCalUID": "7aefibs2i2c6kbtse91@google.com", + "sequence": 1, + "reminders": { + "useDefault": false + }, + "eventType": "default" +} \ No newline at end of file diff --git a/components/google_calendar/sources/event-ended/event-ended.mjs b/components/google_calendar/sources/event-ended/event-ended.mjs index 8c7a84520af93..61649f1cb7ffe 100644 --- a/components/google_calendar/sources/event-ended/event-ended.mjs +++ b/components/google_calendar/sources/event-ended/event-ended.mjs @@ -1,11 +1,12 @@ import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "google_calendar-event-ended", name: "New Ended Event", description: "Emit new event when a Google Calendar event ends", - version: "0.1.7", + version: "0.1.10", type: "source", dedupe: "unique", props: { @@ -41,4 +42,5 @@ export default { return eventEnd && msFromEnd > 0 && msFromEnd < intervalMs; }, }, + sampleEmit, }; diff --git a/components/google_calendar/sources/event-ended/test-event.mjs b/components/google_calendar/sources/event-ended/test-event.mjs new file mode 100644 index 0000000000000..5528aafe18225 --- /dev/null +++ b/components/google_calendar/sources/event-ended/test-event.mjs @@ -0,0 +1,31 @@ +export default { + "kind": "calendar#event", + "etag": "\"3442822827690000\"", + "id": "4pkio0bslskikpvqqo0pol763n", + "status": "confirmed", + "htmlLink": "https://www.google.com/calendar/event?eid=N3B2cXFvMHBvbDc2M24gcjA0bGJ0NDhhbmgyNmdvNmlzajRyZm1qbDBAZw", + "created": "2024-07-19T17:50:13.000Z", + "updated": "2024-07-19T17:50:13.845Z", + "creator": { + "email": "test@sample.com" + }, + "organizer": { + "email": "r04lbt6isj4rfmjl0@group.calendar.google.com", + "displayName": "Test", + "self": true + }, + "start": { + "dateTime": "2024-07-19T13:50:00-04:00", + "timeZone": "America/Detroit" + }, + "end": { + "dateTime": "2024-07-19T13:55:00-04:00", + "timeZone": "America/Detroit" + }, + "iCalUID": "4pkio0bqo0pol763n@google.com", + "sequence": 0, + "reminders": { + "useDefault": true + }, + "eventType": "default" +} \ No newline at end of file diff --git a/components/google_calendar/sources/event-start/event-start.mjs b/components/google_calendar/sources/event-start/event-start.mjs deleted file mode 100644 index 7da7656d14852..0000000000000 --- a/components/google_calendar/sources/event-start/event-start.mjs +++ /dev/null @@ -1,60 +0,0 @@ -import common from "../common/common.mjs"; -import sampleEmit from "./test-event.mjs"; - -export default { - ...common, - key: "google_calendar-event-start", - name: "New Event Start", - description: "Emit new event when the specified time before the Google Calendar event starts", - version: "0.1.8", - type: "source", - dedupe: "unique", - props: { - ...common.props, - calendarId: { - propDefinition: [ - common.props.googleCalendar, - "calendarId", - ], - }, - minutesBefore: { - type: "integer", - label: "Minutes Before", - description: "Emit the event this many minutes before the event starts", - default: 5, - }, - }, - methods: { - ...common.methods, - getMillisecondsBefore() { - return +this.minutesBefore * 60 * 1000; - }, - getConfig({ - now, intervalMs, - }) { - const { - getMillisecondsBefore, - calendarId, - } = this; - - const timeMin = new Date(now.getTime() - getMillisecondsBefore()).toISOString(); - const timeMax = new Date(now.getTime() + intervalMs).toISOString(); - return { - calendarId, - timeMax, - timeMin, - singleEvents: true, - orderBy: "startTime", - }; - }, - isRelevant(event, { - now, intervalMs, - }) { - const start = new Date(event?.start?.dateTime); - const msFromStart = start.getTime() - now.getTime(); - return msFromStart > 0 - && msFromStart < (this.getMillisecondsBefore() + intervalMs); - }, - }, - sampleEmit, -}; diff --git a/components/google_calendar/sources/event-start/test-event.mjs b/components/google_calendar/sources/event-start/test-event.mjs deleted file mode 100644 index d57850f60a73d..0000000000000 --- a/components/google_calendar/sources/event-start/test-event.mjs +++ /dev/null @@ -1,32 +0,0 @@ -export default { - "kind": "calendar#meeting", - "etag": "\"3299999999966000\"", - "id": "3459g2tr92dbuwecv8m939jplk", - "status": "upcoming", - "htmlLink": "https://www.google.com/calendar/event?eid=MzQ1OWcydHI5MmRidXlldm45bTg5MzlwamsgeHhudkBzYW5kYm94LmNvbQ", - "created": "2025-09-13T08:25:45.000Z", - "updated": "2025-09-13T08:25:45.983Z", - "summary": "Sample Event 2005", - "creator": { - "email": "xhx.v@sandbox.com", - "self": true - }, - "organizer": { - "email": "xhx.v@sandbox.com", - "self": true - }, - "start": { - "dateTime": "2025-09-13T18:30:00+09:00", - "timeZone": "Asia/Tokyo" - }, - "end": { - "dateTime": "2025-09-13T19:00:00+09:00", - "timeZone": "Asia/Tokyo" - }, - "iCalUID": "3459g2tr92dbuwecv8m939jplk@google.com", - "sequence": 1, - "reminders": { - "useDefault": true - }, - "eventType": "normal" - } \ No newline at end of file diff --git a/components/google_calendar/sources/new-calendar/new-calendar.mjs b/components/google_calendar/sources/new-calendar/new-calendar.mjs index 54edc1604400a..e68ba327e018c 100644 --- a/components/google_calendar/sources/new-calendar/new-calendar.mjs +++ b/components/google_calendar/sources/new-calendar/new-calendar.mjs @@ -1,11 +1,13 @@ import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; export default { key: "google_calendar-new-calendar", name: "New Calendar Created", description: "Emit new event when a calendar is created.", - version: "0.1.7", + version: "0.1.10", type: "source", + dedupe: "unique", props: { ...common.props, db: "$.service.db", @@ -58,4 +60,5 @@ export default { const calendarIds = calendars.map((item) => item.id); this.setCalendarIds(calendarIds); }, + sampleEmit, }; diff --git a/components/google_calendar/sources/new-calendar/test-event.mjs b/components/google_calendar/sources/new-calendar/test-event.mjs new file mode 100644 index 0000000000000..bd6dd023fa452 --- /dev/null +++ b/components/google_calendar/sources/new-calendar/test-event.mjs @@ -0,0 +1,18 @@ +export default { + "kind": "calendar#calendarListEntry", + "etag": "\"1721410772595000\"", + "id": "r04lbtisj4rfmjl0@group.calendar.google.com", + "summary": "Test", + "timeZone": "America/Detroit", + "colorId": "7", + "backgroundColor": "#42d692", + "foregroundColor": "#000000", + "selected": true, + "accessRole": "owner", + "defaultReminders": [], + "conferenceProperties": { + "allowedConferenceSolutionTypes": [ + "hangoutsMeet" + ] + } +} \ No newline at end of file diff --git a/components/google_calendar/sources/new-event-search/new-event-search.mjs b/components/google_calendar/sources/new-event-search/new-event-search.mjs index 8bdc9b9a88e1d..374ababff2691 100644 --- a/components/google_calendar/sources/new-event-search/new-event-search.mjs +++ b/components/google_calendar/sources/new-event-search/new-event-search.mjs @@ -5,7 +5,7 @@ export default { key: "google_calendar-new-event-search", name: "New Event Matching a Search", description: "Emit new event when a Google Calendar event is created that matches a search", - version: "0.1.7", + version: "0.1.10", type: "source", dedupe: "unique", props: { @@ -37,7 +37,7 @@ export default { }, isRelevant(event, { past }) { const created = new Date(event.created); - // created in last 5 mins and not cancelled + // created since last run and not cancelled return created > past && event.status !== "cancelled"; }, }, diff --git a/components/google_calendar/sources/new-event/new-event.mjs b/components/google_calendar/sources/new-event/new-event.mjs deleted file mode 100644 index 58ba879003b41..0000000000000 --- a/components/google_calendar/sources/new-event/new-event.mjs +++ /dev/null @@ -1,36 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - key: "google_calendar-new-event", - name: "New Event Created", - description: "Emit new event when a Google Calendar event is created", - version: "0.1.7", - type: "source", - dedupe: "unique", - props: { - ...common.props, - calendarId: { - propDefinition: [ - common.props.googleCalendar, - "calendarId", - ], - }, - }, - methods: { - ...common.methods, - getConfig({ past }) { - const updatedMin = past.toISOString(); - return { - calendarId: this.calendarId, - updatedMin, - singleEvents: true, - orderBy: "startTime", - }; - }, - isRelevant(event, { past }) { - const created = new Date(event.created); - return created > past && event.status !== "cancelled"; - }, - }, -}; diff --git a/components/google_calendar/sources/new-or-updated-event-instant/new-or-updated-event-instant.mjs b/components/google_calendar/sources/new-or-updated-event-instant/new-or-updated-event-instant.mjs index d2d8d565899d5..1ca4ecb79b0bd 100644 --- a/components/google_calendar/sources/new-or-updated-event-instant/new-or-updated-event-instant.mjs +++ b/components/google_calendar/sources/new-or-updated-event-instant/new-or-updated-event-instant.mjs @@ -8,7 +8,7 @@ export default { type: "source", name: "New Created or Updated Event (Instant)", description: "Emit new event when a Google Calendar events is created or updated (does not emit cancelled events)", - version: "0.1.11", + version: "0.1.14", dedupe: "unique", props: { googleCalendar, @@ -199,16 +199,18 @@ export default { } return new Date(min); }, - getChannelIds() { - const channelIds = []; + getCalendarIdForChannelId(incomingChannelId) { for (const calendarId of this.calendarIds) { - const channelId = this.db.get(`${calendarId}.channelId`); - channelIds.push(channelId); + if (this.db.get(`${calendarId}.channelId`) === incomingChannelId) { + return calendarId; + } } - return channelIds; + return null; }, }, async run(event) { + let calendarId = null; // calendar ID matching incoming channel ID + // refresh watch if (event.interval_seconds) { // get time @@ -224,9 +226,9 @@ export default { } } else { // Verify channel ID - const channelIds = this.getChannelIds(); const incomingChannelId = event?.headers?.["x-goog-channel-id"]; - if (!channelIds.includes(incomingChannelId)) { + calendarId = this.getCalendarIdForChannelId(incomingChannelId); + if (!calendarId) { console.log( `Unexpected channel ID ${incomingChannelId}. This likely means there are multiple, older subscriptions active.`, ); @@ -252,40 +254,49 @@ export default { } // Fetch and emit events - for (const calendarId of this.calendarIds) { + const checkCalendarIds = calendarId + ? [ + calendarId, + ] + : this.calendarIds; + for (const calendarId of checkCalendarIds) { const syncToken = this.getNextSyncToken(calendarId); let nextSyncToken = null; let nextPageToken = null; while (!nextSyncToken) { - const { - data: syncData = {}, - status: syncStatus, - } = await this.googleCalendar.listEvents({ - returnOnlyData: false, - calendarId, - syncToken, - pageToken: nextPageToken, - }); - if (syncStatus === 410) { - console.log("Sync token invalid, resyncing"); - nextSyncToken = await this.googleCalendar.fullSync(this.calendarId); - break; - } - nextPageToken = syncData.nextPageToken; - nextSyncToken = syncData.nextSyncToken; - - const { items: events = [] } = syncData; - events - .filter(this.isEventRelevant, this) - .forEach((event) => { - const { status } = event; - if (status === "cancelled") { - console.log("Event cancelled. Exiting."); - return; - } - const meta = this.generateMeta(event); - this.$emit(event, meta); + try { + const { data: syncData = {} } = await this.googleCalendar.listEvents({ + returnOnlyData: false, + calendarId, + syncToken, + pageToken: nextPageToken, + maxResults: 2500, }); + + nextPageToken = syncData.nextPageToken; + nextSyncToken = syncData.nextSyncToken; + + const { items: events = [] } = syncData; + events + .filter(this.isEventRelevant, this) + .forEach((event) => { + const { status } = event; + if (status === "cancelled") { + console.log("Event cancelled. Exiting."); + return; + } + const meta = this.generateMeta(event); + this.$emit(event, meta); + }); + } catch (error) { + if (error === "Sync token is no longer valid, a full sync is required.") { + console.log("Sync token invalid, resyncing"); + nextSyncToken = await this.googleCalendar.fullSync(calendarId); + break; + } else { + throw error; + } + } } this.setNextSyncToken(calendarId, nextSyncToken); diff --git a/components/google_calendar/sources/new-or-updated-event/new-or-updated-event.mjs b/components/google_calendar/sources/new-or-updated-event/new-or-updated-event.mjs deleted file mode 100644 index 363d937afd570..0000000000000 --- a/components/google_calendar/sources/new-or-updated-event/new-or-updated-event.mjs +++ /dev/null @@ -1,48 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - key: "google_calendar-new-or-updated-event", - name: "New Created or Updated Event", - description: "Emit new event when a Google Calendar events is created or updated (does not emit cancelled events)", - version: "0.1.7", - type: "source", - dedupe: "unique", - props: { - ...common.props, - calendarId: { - propDefinition: [ - common.props.googleCalendar, - "calendarId", - ], - }, - }, - methods: { - ...common.methods, - getConfig({ past }) { - const updatedMin = past.toISOString(); - return { - calendarId: this.calendarId, - updatedMin, - singleEvents: true, - orderBy: "startTime", - }; - }, - isRelevant(event) { - return event.status !== "cancelled"; - }, - generateMeta(event) { - const { - id, - summary, - updated: tsString, - } = event; - const ts = Date.parse(tsString); - return { - id: `${id}-${ts}`, - summary, - ts, - }; - }, - }, -}; diff --git a/components/google_calendar/sources/upcoming-event-alert/test-event.mjs b/components/google_calendar/sources/upcoming-event-alert/test-event.mjs new file mode 100644 index 0000000000000..251a120a1109a --- /dev/null +++ b/components/google_calendar/sources/upcoming-event-alert/test-event.mjs @@ -0,0 +1,32 @@ +export default { + "kind": "calendar#event", + "etag": "\"3442838491454000\"", + "id": "0dip62r3f3d85o35jjnjcmqbmo", + "status": "confirmed", + "htmlLink": "https://www.google.com/calendar/event?eid=MGRpcDYycjNW8zNWgbWljaGVsbGUucGlwZWRyZWFtQG0", + "created": "2024-07-19T20:00:45.000Z", + "updated": "2024-07-19T20:00:45.727Z", + "summary": "test", + "creator": { + "email": "test@sample.com", + "self": true + }, + "organizer": { + "email": "test@sample.com", + "self": true + }, + "start": { + "dateTime": "2024-07-19T16:07:00-04:00", + "timeZone": "America/Detroit" + }, + "end": { + "dateTime": "2024-07-19T17:07:00-04:00", + "timeZone": "America/Detroit" + }, + "iCalUID": "0dip62r35jjnjcmqbmo@google.com", + "sequence": 0, + "reminders": { + "useDefault": true + }, + "eventType": "default" +} \ No newline at end of file diff --git a/components/google_calendar/sources/upcoming-event-alert/upcoming-event-alert.mjs b/components/google_calendar/sources/upcoming-event-alert/upcoming-event-alert.mjs index 55cafc6ac2172..7ed35921fd97d 100644 --- a/components/google_calendar/sources/upcoming-event-alert/upcoming-event-alert.mjs +++ b/components/google_calendar/sources/upcoming-event-alert/upcoming-event-alert.mjs @@ -1,5 +1,6 @@ import taskScheduler from "../../../pipedream/sources/new-scheduled-tasks/new-scheduled-tasks.mjs"; import googleCalendar from "../../google_calendar.app.mjs"; +import sampleEmit from "./test-event.mjs"; export default { key: "google_calendar-upcoming-event-alert", @@ -7,7 +8,7 @@ export default { description: `Emit new event based on a time interval before an upcoming event in the calendar. This source uses Pipedream's Task Scheduler. [See the documentation](https://pipedream.com/docs/examples/waiting-to-execute-next-step-of-workflow/#step-1-create-a-task-scheduler-event-source) for more information and instructions for connecting your Pipedream account.`, - version: "0.0.6", + version: "0.0.9", type: "source", props: { pipedream: taskScheduler.props.pipedream, @@ -20,15 +21,11 @@ export default { "calendarId", ], }, - eventId: { + eventTypes: { propDefinition: [ googleCalendar, - "eventId", - (c) => ({ - calendarId: c.calendarId, - }), + "eventTypes", ], - optional: true, }, time: { type: "integer", @@ -94,27 +91,21 @@ export default { async getCalendarEvents() { const calendarEvents = []; const params = { + returnOnlyData: false, calendarId: this.calendarId, + eventTypes: this.eventTypes, }; - if (this.eventId) { - const item = await this.googleCalendar.getEvent({ - ...params, - eventId: this.eventId, - }); - calendarEvents.push(item); - } else { - do { - const { - data: { - items, nextPageToken, - }, - } = await this.googleCalendar.getEvents(params); - if (items?.length) { - calendarEvents.push(...items); - } - params.pageToken = nextPageToken; - } while (params.pageToken); - } + do { + const { + data: { + items, nextPageToken, + }, + } = await this.googleCalendar.listEvents(params); + if (items?.length) { + calendarEvents.push(...items); + } + params.pageToken = nextPageToken; + } while (params.pageToken); return calendarEvents; }, }, @@ -157,4 +148,5 @@ export default { this._setScheduledEventIds(scheduledEventIds); this._setScheduledCalendarEventIds(scheduledCalendarEventIds); }, + sampleEmit, }; diff --git a/components/google_calendar/sources/upcoming-event-type-alert/upcoming-event-type-alert.mjs b/components/google_calendar/sources/upcoming-event-type-alert/upcoming-event-type-alert.mjs deleted file mode 100644 index 2bfc256ff0f33..0000000000000 --- a/components/google_calendar/sources/upcoming-event-type-alert/upcoming-event-type-alert.mjs +++ /dev/null @@ -1,149 +0,0 @@ -import taskScheduler from "../../../pipedream/sources/new-scheduled-tasks/new-scheduled-tasks.mjs"; -import googleCalendar from "../../google_calendar.app.mjs"; - -export default { - key: "google_calendar-upcoming-event-type-alert", - name: "New Upcoming Event Type Alert", - description: `Emit new event based on a time interval before an upcoming event of the specified type ("default", "focusTime", "outOfOffice", "workingLocation") in the calendar. This source uses Pipedream's Task Scheduler. - [See the documentation](https://pipedream.com/docs/examples/waiting-to-execute-next-step-of-workflow/#step-1-create-a-task-scheduler-event-source) - for more information and instructions for connecting your Pipedream account.`, - version: "0.0.1", - type: "source", - props: { - pipedream: taskScheduler.props.pipedream, - googleCalendar, - db: "$.service.db", - http: "$.interface.http", - calendarId: { - propDefinition: [ - googleCalendar, - "calendarId", - ], - }, - eventTypes: { - propDefinition: [ - googleCalendar, - "eventTypes", - ], - }, - time: { - type: "integer", - label: "Minutes Before", - description: "Number of minutes to trigger before the start of the calendar event.", - min: 0, - reloadProps: true, - }, - }, - async additionalProps() { - const props = {}; - if (this.time > 0) { - props.timer = { - type: "$.interface.timer", - description: "Poll the API to schedule alerts for any newly created events", - default: { - intervalSeconds: 60 * this.time, - }, - }; - } - return props; - }, - hooks: { - async deactivate() { - const ids = this._getScheduledEventIds(); - if (!ids?.length) { - return; - } - for (const id of ids) { - if (await this.deleteEvent({ - body: { - id, - }, - })) { - console.log("Cancelled scheduled event"); - } - } - this._setScheduledEventIds(); - }, - }, - methods: { - ...taskScheduler.methods, - _getScheduledEventIds() { - return this.db.get("scheduledEventIds"); - }, - _setScheduledEventIds(ids) { - this.db.set("scheduledEventIds", ids); - }, - _getScheduledCalendarEventIds() { - return this.db.get("scheduledCalendarEventIds"); - }, - _setScheduledCalendarEventIds(ids) { - this.db.set("scheduledCalendarEventIds", ids); - }, - _hasDeployed() { - const result = this.db.get("hasDeployed"); - this.db.set("hasDeployed", true); - return result; - }, - subtractMinutes(date, minutes) { - return date.getTime() - minutes * 60000; - }, - async getCalendarEvents() { - const calendarEvents = []; - const params = { - calendarId: this.calendarId, - eventTypes: this.eventTypes, - }; - do { - const { - data: { - items, nextPageToken, - }, - } = await this.googleCalendar.getEvents(params); - if (items?.length) { - calendarEvents.push(...items); - } - params.pageToken = nextPageToken; - } while (params.pageToken); - return calendarEvents; - }, - }, - async run(event) { - // self subscribe only on the first time - if (!this._hasDeployed()) { - await this.selfSubscribe(); - } - - const scheduledEventIds = this._getScheduledEventIds() || []; - - // incoming scheduled event - if (event.$channel === this.selfChannel()) { - const remainingScheduledEventIds = scheduledEventIds.filter((id) => id !== event["$id"]); - this._setScheduledEventIds(remainingScheduledEventIds); - this.emitEvent(event, `Upcoming ${event.summary} event`); - return; - } - - // schedule new events - const scheduledCalendarEventIds = this._getScheduledCalendarEventIds() || {}; - const calendarEvents = await this.getCalendarEvents(); - - for (const calendarEvent of calendarEvents) { - const startTime = calendarEvent.start - ? (new Date(calendarEvent.start.dateTime || calendarEvent.start.date)) - : null; - if (!startTime - || startTime.getTime() < Date.now() - || scheduledCalendarEventIds[calendarEvent.id]) - { - continue; - } - const later = new Date(this.subtractMinutes(startTime, this.time)); - - const scheduledEventId = this.emitScheduleEvent(calendarEvent, later); - scheduledEventIds.push(scheduledEventId); - scheduledCalendarEventIds[calendarEvent.id] = true; - } - this._setScheduledEventIds(scheduledEventIds); - this._setScheduledCalendarEventIds(scheduledCalendarEventIds); - }, -}; diff --git a/components/google_chat/README.md b/components/google_chat/README.md new file mode 100644 index 0000000000000..c611899301723 --- /dev/null +++ b/components/google_chat/README.md @@ -0,0 +1,131 @@ +# Overview + +The Google Chat API allows you to build custom bots for Google Chat, enabling automated interactions with users within a chat space. By leveraging this API on Pipedream, you can create powerful workflows that respond to messages, automate tasks, and connect Google Chat with other services. Pipedream's serverless platform provides a seamless way to invoke these APIs based on triggers from Google Chat or other apps. + +# Example Use Cases + +- **Automated Helpdesk Bot**: Deploy a bot that listens for specific keywords in Google Chat messages. When a user mentions a support-related keyword, the bot can reply with helpful information or create a ticket in a service like Zendesk. + +- **Meeting Scheduler**: Set up a workflow that triggers when someone requests a meeting in a Google Chat room. The bot can interact with Google Calendar to find available slots and propose meeting times directly within the chat. + +- **CI/CD Notifications**: Integrate with GitHub to send updates on code commits, pull requests, or build statuses from your CI/CD pipeline into a Google Chat room, keeping your development team informed in real-time. + + +# Getting Started + +## Creating a Google Chat app +In order to connect your workspace Google Chat account to Pipedream, you'll need do the following: +1. Create a Google Chat app in Google Cloud (a Google workspace account is required). +2. Connect this app using custom OAuth clients on Pipedream. See the directions [here](https://pipedream.com/docs/connected-accounts/oauth-clients) on how to configure a custom OAuth client on Pipedream. + +1. Sign in to the [Google Cloud Console](https://cloud.google.com/) +2. Select an existing project or create a new one + + ![Select an existing project or create a a new one in the Google Cloud Console](https://res.cloudinary.com/pipedreamin/image/upload/v1663268100/docs/components/CleanShot_2022-09-15_at_14.54.34_vajyds.png) + +3. Select **APIs & Services** +4. Click **Enable APIs & Services** + + ![Select "Enable APIs & Services to open a menu to enable the Google Chat API for Pipedream to connect to](https://res.cloudinary.com/pipedreamin/image/upload/v1663268316/docs/components/CleanShot_2022-09-15_at_14.58.06_jshirk.png) + +5. Search for and select **Chat API** +6. Click **Enable** + + ![Search for and select the Google Chat API](https://res.cloudinary.com/dpenc2lit/image/upload/v1704485195/Screenshot_2024-01-05_at_12.04.19_PM_ypy1dz.png) + +7. Click **OAuth consent screen** on the left side + + ![Click "OAuth consent screen" in the left navigation menu](https://res.cloudinary.com/dpenc2lit/image/upload/v1704750653/Screenshot_2024-01-08_at_1.50.38_PM_ihkhn7.png) + +8. If you only intend to use this application within your organization, select **Internal** (recommended) and click "Create." In this mode, your app is limited to Google Workspace users within your organization. If you select **External**, you will need to go through the process of app verification in order use any sensitive or restricted scopes. + + ![Select "Internal" in the OAuth Consent Screen](https://res.cloudinary.com/dpenc2lit/image/upload/v1704750730/Screenshot_2024-01-08_at_1.52.05_PM_pgxebn.png) + +9. Fill in the required fields and click **Save and Continue** +10. Under **Authorized Domains**, add `pipedream.com` +11. Click **Add or remove scopes** and Filter by `Chat API` select whichever scopes you intend to use and then click "Update". For more information about available Google Chat scopes, please see this [overview](https://developers.google.com/chat/api/guides/auth#chat-api-scopes). +12. Click **Save and Continue** to finish the **Scopes** step +13. You should be prompted with a **Summary** page. + +## Create OAuth Credentials in Google Cloud + +You will need to generate a set of OAuth credentials to connect your new Google Chat app to Pipedream. + +1. Navigate to the **Credentials** section on the left side. + + ![Open the Credentials menu in the left hand nav bar](https://res.cloudinary.com/pipedreamin/image/upload/v1663269973/docs/components/CleanShot_2022-09-15_at_15.13.52_yvllxi.png) + +2. Click **Create Credentials** at the top and select **“*OAuth client ID** + + ![Click create credentials to start the process](https://res.cloudinary.com/pipedreamin/image/upload/v1663270014/docs/components/CleanShot_2022-09-15_at_15.14.15_hjulis.png) + + ![Select the OAuth Client ID option](https://res.cloudinary.com/pipedreamin/image/upload/v1663270093/docs/components/CleanShot_2022-09-15_at_15.14.39_juqtnm.png) + +3. Select **Web application** for **Application type** + + ![Web application is the type of OAuth credential we're generating](https://res.cloudinary.com/pipedreamin/image/upload/v1663270117/docs/components/CleanShot_2022-09-15_at_15.14.56_hlseq6.png) + +4. Give your app a name. + +## Create a Custom OAuth Client in Pipedream + +5. Navigate to the workspace where you want to connect your Google Chat app, and go to **Accounts**, then **OAuth Clients**, and click **New OAuth Client. + +6. Search for the "Google Chat" app and name your custom OAuth client. You will get a unique **Redirect URI** which you will need to configure in Google. + +7. In the Google Cloud app configuration, under **Authorized redirect URIs**, click **Add URI** and enter the Redirect URI of your custom Google Chat client. + + ![Add the Pipedream URL to the Callback Redirect URL option](https://res.cloudinary.com/dpenc2lit/image/upload/v1704486173/Screenshot_2024-01-05_at_12.22.39_PM_oyvppi.png) + +8. Click **Create** to create your new OAuth keys +9. Note the client ID and client Secret, but keep these private and secure + + ![Store the Client ID and Client Secret keys](https://res.cloudinary.com/pipedreamin/image/upload/v1663270250/docs/components/CleanShot_2022-09-15_at_15.16.29_hvxnkx.png) + +10. Add your Client ID and Client Secret to your custom OAuth Google Chat client on Pipedream. + +## Configure your Google Chat application + +1. Click **Enable APIs & Services** on the top-left navigation bar, then **Google Chat API**. + +2. Click **Configuration** + +3. Fill in the required details - please note that the values you provide here will be the name of the app that you add to your Google Chat workspace. + +4. You can name the application whatever you'd like for the app to be called within the Google Chat workspace, e.g. **Pipedream** + +5. If you'd like to use the Pipedream logo for the avatar, use the URL [https://pipedream.com/s.v0/app_13GhYE/logo/orig](https://pipedream.com/s.v0/app_13GhYE/logo/orig) for the **Avatar URL** + +6. Add a **Description**. + +7. Select any **Interactive features** you require for your app. + +8. Add an **App URL**, ideally, an HTTPS URL that you control, or you can provide the following app url [https://pipedream.com/apps/google-chat-developer-app](https://pipedream.com/apps/google-chat-developer-app). + +9. Under **Visibility**, add the email addresses of the individuals or groups within your Google Workspace organization. + +10. Click **Save**. + +![App Configuration Settings](https://res.cloudinary.com/dpenc2lit/image/upload/v1704751866/Screenshot_2024-01-08_at_2.10.44_PM_z3eoa0.png) + +## Connect your Google Chat app Pipedream with your Google Chat app OAuth credentials + +At this point, you should have a Google Chat App under your Google Project, and a custom OAuth client on Pipedream. Make sure that the scopes in your custom OAuth client match the scopes you enabled in Google Cloud. + +You should now be able to use your Google Chat application that you created on Pipedream! + +## Publish your Google Chat app (EXTERNAL ONLY) +Google has a [7 day expiration window](https://developers.google.com/identity/protocols/oauth2#:~:text=A%20Google%20Cloud,Connect%20equivalents) on refresh tokens for applications that are set to **External** users with a publishing status of "Testing", so you will need to **Publish** your application in order to maintain your account connection. + +1. Navigate to your application, and click **OAuth Consent Screen** on the lefthand sidebar. +2. Under **Publishing status**, click **Publish App**. If you included any sensitive or restricted scopes in your app, there will be a disclosure stating that you will need to go through the process of verification. Click **Confirm**. +3. Your application will not be available externally unless you share your **client_id** with others, and you will not have to go through the verification process unless you intend to onboard over 100 users. +4. The publishing status should be set to **In production**, and your account should maintain its connection without an expiration window. + +![Publish your application](https://res.cloudinary.com/dpenc2lit/image/upload/v1698166716/Screenshot_2023-10-24_at_9.50.06_AM_lve7wq.png) + +![Confirmation of changes](https://res.cloudinary.com/dpenc2lit/image/upload/v1698166716/Screenshot_2023-10-24_at_9.50.18_AM_mndtyc.png) + +# Troubleshooting +**Application disconnects after 7 days**
+If your developer application disconnects after 7 days, you need to follow the steps above to Publish your Google Chat app in order to keep your account connected. diff --git a/components/google_chat_developer_app/README.md b/components/google_chat_developer_app/README.md index c91fd961b64ee..b81592d86c91c 100644 --- a/components/google_chat_developer_app/README.md +++ b/components/google_chat_developer_app/README.md @@ -115,6 +115,15 @@ Google has a [7 day expiration window](https://developers.google.com/identity/pr ![Confirmation of changes](https://res.cloudinary.com/dpenc2lit/image/upload/v1698166716/Screenshot_2023-10-24_at_9.50.18_AM_mndtyc.png) +# Example Use Cases + +- **Automated Helpdesk Bot**: Create a bot within Google Chat that listens for keywords related to IT support and automatically responds with troubleshooting advice or escalates the issue by creating a ticket in a service like Zendesk or Jira. + +- **Project Management Notifications**: Set up a workflow that monitors project management tools such as Asana or Trello for updates, and then posts these updates to a dedicated Google Chat space, ensuring your team stays informed about project progress in real-time. + +- **Meeting Coordinator**: Develop a bot that helps schedule meetings by integrating with Google Calendar. When a meeting request is mentioned in a chat, the bot can check participants' availability, propose times, and send calendar invites, streamlining the scheduling process. + + # Troubleshooting **Application disconnects after 7 days**
If your developer application disconnects after 7 days, you need to follow the steps above to Publish your Google Chat app in order to keep your account connected. \ No newline at end of file diff --git a/components/google_chat_developer_app/package.json b/components/google_chat_developer_app/package.json index a4f5af3632d16..45e796082f4ca 100644 --- a/components/google_chat_developer_app/package.json +++ b/components/google_chat_developer_app/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/google_classroom/README.md b/components/google_classroom/README.md index c1f81c3f3fc13..920d47327b56e 100644 --- a/components/google_classroom/README.md +++ b/components/google_classroom/README.md @@ -1,17 +1,11 @@ # Overview -With the Google Classroom API, you can manage your Google Classroom classes and -coursework. You can also use the API to create and manage announcements, -assignments, and student submissions. +The Google Classroom API lets you tap into the educational space by managing courses, assignments, and student interactions programmatically. With this API, you can automate course creation, roster management, and content distribution, effectively bridging the gap between various educational tools and platforms. Leveraging Pipedream's serverless execution environment, you can build powerful workflows that respond to events in Classroom, sync data across platforms, and streamline the educational process for teachers and students alike. -Here are some example applications you could build with the Google Classroom -API: +# Example Use Cases -- A course management system that allows teachers to create and manage their - courses online -- An announcement system that allows teachers to post announcements to their - classes -- An assignment management system that allows teachers to create and manage - assignments for their classes -- A student submission system that allows teachers to view and grade student - submissions +- **Automated Course Management**: Create workflows that automatically set up new courses at the beginning of a term, including setting up course materials in Google Drive and scheduling calendar events for class times. When a new term starts, this automation can save educators hours of setup time. + +- **Assignment Distribution and Collection**: As soon as assignments are created in Google Classroom, trigger a workflow on Pipedream that generates a template in Google Docs, shares it with the students, and then collects submitted assignments for review. This can vastly simplify the distribution and collection process for instructors. + +- **Grading and Feedback Integration**: When grades are posted in Google Classroom, use Pipedream to trigger notifications to students via email or a messaging app like Slack. Additionally, integrate with a tool like Google Sheets to automatically update a master gradebook with the latest scores. diff --git a/components/google_cloud/README.md b/components/google_cloud/README.md index d9588558cabc1..b990ec0f8a754 100644 --- a/components/google_cloud/README.md +++ b/components/google_cloud/README.md @@ -1,7 +1,11 @@ # Overview -The Google Cloud API allows developers to access a variety of Google Cloud -services from their own applications. Services that can be accessed include -Google Cloud Storage, Google Cloud Datastore, Google Cloud Functions, and -Google Cloud Pub/Sub. With the Google Cloud API, developers can build a variety -of applications that take advantage of Google Cloud services. +The Google Cloud API opens a world of possibilities for enhancing cloud operations and automating tasks. It empowers you to manage, scale, and fine-tune various services within the Google Cloud Platform (GCP) programmatically. With Pipedream, you can harness this power to create intricate workflows, trigger cloud functions based on events from other apps, manage resources, and analyze data, all in a serverless environment. The ability to interconnect GCP services with numerous other apps enriches automation, making it easier to synchronize data, streamline development workflows, and deploy applications efficiently. + +# Example Use Cases + +- **Automated Backups to Cloud Storage**: Trigger regular backups of your application data from various sources like MySQL, MongoDB, or even third-party services like Dropbox to Google Cloud Storage. Use Pipedream's cron job features to schedule and manage backup routines without manual intervention. + +- **Real-time Data Processing with Pub/Sub**: Instantly process data from IoT devices or application logs by pushing them to Google Pub/Sub. Set up a Pipedream workflow that listens for new messages in a specific topic, processes the data, and forwards it to BigQuery for analysis or to Slack for real-time alerts. + +- **Dynamic Scaling with Compute Engine**: Automatically scale your Compute Engine resources based on application load without manual oversight. Configure a workflow in Pipedream that monitors metrics from Google Operations (formerly Stackdriver), and adjusts the number of virtual machine instances in your Compute Engine environment accordingly. diff --git a/components/google_cloud_translate/README.md b/components/google_cloud_translate/README.md index 9762cc5324d82..b35934613cb3b 100644 --- a/components/google_cloud_translate/README.md +++ b/components/google_cloud_translate/README.md @@ -1,10 +1,11 @@ # Overview -Google Cloud Translate API can be used to build applications that can translate -between languages. Some examples of applications that can be built using Google -Cloud Translate API include: - -- A language learning app that can translate between languages. -- A translation app that can translate documents or webpages from one language - to another. -- A chat app that can translate messages between users in different languages. +The Google Cloud Translate API empowers developers to dynamically translate text between thousands of language pairs, integrate language translation into applications, websites, tools, and other solutions. By leveraging this API within Pipedream's serverless platform, you can create automated workflows that respond to events from hundreds of sources, transforming and routing your data to various destinations, all while breaking down language barriers. + +# Example Use Cases + +- **Content Localization Automation:** Automatically translate new content posted to a CMS or blog into multiple languages. When content is added to a platform like WordPress, trigger a Pipedream workflow that translates the text using Google Cloud Translate, then posts the translated versions as new entries, effectively localizing the content for different audiences. + +- **Customer Support Ticket Translation:** Create a workflow to help support teams manage international queries. When a non-English support ticket is received in a system like Zendesk, use Pipedream to trigger a translation of the ticket's text. The workflow could then append the translation to the ticket, or post it in a Slack channel dedicated to support staff, facilitating quicker and more effective responses to global users. + +- **Real-Time Chat Translation for Communication Apps:** Enhance communication in apps like Slack or Discord by building a bot that listens to channels and translates messages on the fly. Use Pipedream to detect when a message in a specified language is posted and invoke the Google Cloud Translate API to translate the message, then publish the translated text back into the channel, allowing for seamless cross-lingual conversations. diff --git a/components/google_cloud_vision_api/README.md b/components/google_cloud_vision_api/README.md new file mode 100644 index 0000000000000..92e65fee29c13 --- /dev/null +++ b/components/google_cloud_vision_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Google Cloud Vision API allows you to analyze images in the cloud, harnessing Google's machine learning technology. You can detect and classify multiple objects, face and landmark detection, handwriting recognition, and image attributes. Combining this with Pipedream's serverless platform catalyzes the creation of automated workflows to process images, trigger actions, and integrate with other services seamlessly. + +# Example Use Cases + +- **Content Moderation System**: Use Google Cloud Vision API to detect explicit content in images uploaded to a cloud storage service like Dropbox or Google Drive. Once an image is flagged, Pipedream can automate notifications, remove the image, or log the event in a database for review. + +- **Label-based Image Organization**: Employ the API to tag images with labels and then use Pipedream to sort and store them in relevant folders in services such as Google Photos or Amazon S3. This can be useful for digital asset management, making it easier to search and retrieve images based on their content. + +- **Automated Data Extraction from Scanned Documents**: Leverage the API's OCR capabilities to extract text from scanned documents or images. Pipedream can take this data, format it, and then input it into a spreadsheet on Google Sheets or a record in Airtable, streamlining data entry and archival processes. diff --git a/components/google_contacts/README.md b/components/google_contacts/README.md index b988c314cbcfb..0e7084563b9dc 100644 --- a/components/google_contacts/README.md +++ b/components/google_contacts/README.md @@ -1,8 +1,11 @@ # Overview -The Google People API lets you manage contact information for people in your -Google account. You can use this API to create and manage contacts, as well as -groups of contacts. With the People API, you can list all the contacts in your -account, as well as their information, such as email addresses, phone numbers, -and contact photos. You can also use the People API to create new contacts, or -to add new information to existing contacts. +The Google Contacts API allows you to integrate your contact data with custom applications, enabling you to sync, manage, and leverage your contact list for a variety of functions. Through Pipedream, you can seamlessly connect the Google Contacts API to various other services, streamlining contact management tasks such as syncing contact details across platforms, automating outreach or follow-ups, and gathering insights from contact interactions. + +# Example Use Cases + +- **Automated Contact Syncing with CRM**: Automatically update your CRM whenever you add or modify a contact in Google Contacts. This ensures that sales teams always have the most up-to-date contact information, and it saves time by removing the need for manual data entry. + +- **Birthday Reminder System**: Create a workflow that scans your Google Contacts for birthdays, then integrates with an email service app like SendGrid on Pipedream to send out personalized birthday greetings or offers on their special day. This can help in maintaining customer relationships and increasing brand loyalty. + +- **Contact Enrichment and Segmentation**: Set up a workflow that enriches new Google Contacts with additional data from a service like Clearbit. The enriched data can be used to segment contacts into different marketing campaigns based on their demographics, job roles, or interaction history, making your marketing efforts more targeted and effective. diff --git a/components/google_contacts/actions/list-contacts/list-contacts.mjs b/components/google_contacts/actions/list-contacts/list-contacts.mjs index 209548b1cf1dc..f37ed4d1942a0 100644 --- a/components/google_contacts/actions/list-contacts/list-contacts.mjs +++ b/components/google_contacts/actions/list-contacts/list-contacts.mjs @@ -6,7 +6,7 @@ export default { key: "google_contacts-list-contacts", name: "List Contacts", description: "Lists all contacts of the authenticated user. [See the documentation](https://developers.google.com/people/api/rest/v1/people.connections/list)", - version: "0.0.4", + version: "0.0.5", type: "action", props: { ...common.props, @@ -26,7 +26,7 @@ export default { const contacts = []; do { const { - connections, + connections = [], nextPageToken, } = await this.googleContacts.listContacts(client, params); params.pageToken = nextPageToken; diff --git a/components/google_contacts/package.json b/components/google_contacts/package.json index 60cc88216a810..3c06b6fa422dd 100644 --- a/components/google_contacts/package.json +++ b/components/google_contacts/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/google_contacts", - "version": "0.2.0", + "version": "0.2.1", "description": "Pipedream Google Contacts Components", "main": "google_contacts.app.mjs", "keywords": [ diff --git a/components/google_dialogflow/README.md b/components/google_dialogflow/README.md index 29ad16c838e61..628ac1efb424c 100644 --- a/components/google_dialogflow/README.md +++ b/components/google_dialogflow/README.md @@ -1,14 +1,11 @@ # Overview -The Google Dialogflow API allows developers to create engaging conversational -applications. With Dialogflow, you can natural language processing (NLP) to -derive meaning from user queries, and create conversational applications that -can fulfill user intents. - -Dialogflow can be used to build a variety of applications, including: - -- Virtual assistants -- Chatbots -- Voice assistants -- Customer service applications -- Robotic process automation (RPA) applications +Google Dialogflow API empowers you to create conversational interfaces for websites, apps, and messaging platforms. Think chatbots that can engage in human-like dialogue, provide customer support, guide through sales processes, or control smart home devices with voice commands. With Pipedream's integration capabilities, you can create automated workflows that trigger actions in other apps based on Dialogflow's processed input, enabling seamless interaction across a plethora of services. + +# Example Use Cases + +- **Customer Support Automation**: Integrate Google Dialogflow with a CRM like Salesforce on Pipedream. When Dialogflow interprets a customer's query for support, it can automatically create a new support ticket in Salesforce, assign it to the right agent, and even provide the agent with suggested solutions based on past interactions. + +- **Survey Feedback Analysis**: Connect Dialogflow with tools like Google Sheets on Pipedream to automatically parse customer feedback from surveys. As Dialogflow analyzes the sentiment and content of the feedback, it can categorize the responses and store them in a Google Sheet for easy analysis and reporting. + +- **E-commerce Personal Shopper Assistant**: Use Dialogflow to power a smart shopping assistant that communicates with customers. Tie this into Shopify with Pipedream, and you can set up a workflow where the chatbot's product recommendations are dynamically generated based on inventory levels or ongoing promotions, creating a personalized shopping experience. diff --git a/components/google_directory/README.md b/components/google_directory/README.md index 5cbac991cbfc3..ead22bb99a04a 100644 --- a/components/google_directory/README.md +++ b/components/google_directory/README.md @@ -1,9 +1,11 @@ # Overview -With the Google Directory API, you can: +The Google Directory API enables you to perform administrative operations on users, groups, organizational units, and devices within a Google Workspace domain. With Pipedream, you can harness this API to create automated workflows that manage directory resources, sync information, and streamline admin tasks. Pipedream's serverless platform allows you to trigger these workflows on schedules or events, integrating seamlessly with other apps to enrich and act upon the data. -- List all users in your Google Apps domain -- Get information about a specific user -- Update a user's name, email address, and other profile information -- Change a user's password -- Suspend or delete a user +# Example Use Cases + +- **Automated User Provisioning**: When a new employee is added to your HR system (e.g., BambooHR), trigger a Pipedream workflow to create a new user in Google Workspace, assign them to the appropriate organizational unit, and enroll them in essential groups for email lists and access permissions. + +- **Dynamic Group Membership Updates**: Use a Pipedream workflow to monitor changes in project management tools like Asana or Trello. When a member's project role changes, automatically update their Google Group memberships to reflect their new responsibilities, ensuring they have the right access and communication channels. + +- **Directory Sync with External Databases**: Schedule a regular Pipedream workflow that fetches user information from an external database (such as PostgreSQL) and updates user profiles in Google Directory to keep contact details and organizational information in sync across business systems. diff --git a/components/google_docs/README.md b/components/google_docs/README.md index 27fde56213c0c..f00279bf8e6bc 100644 --- a/components/google_docs/README.md +++ b/components/google_docs/README.md @@ -1,14 +1,11 @@ # Overview -You can use the Google Docs API to build a wide variety of applications, -including: - -- Word processors -- Spreadsheets -- Presentation software -- Database management tools -- Data visualization tools -- Document management systems -- Web scraping tools -- Text analysis tools -- And much more! +The Google Docs API allows you to create, read, and update Google Docs programmatically, enabling a wide range of automations and integrations with other apps and services. With Pipedream, you can harness this API to craft custom serverless workflows that trigger on various events, like form submissions, emails, or scheduled times, and perform actions like updating a document, extracting content, or even generating templated reports. + +# Example Use Cases + +- **Content Management Automation**: Automatically update a Google Docs content repository when a new blog post is submitted through a CMS like WordPress. Pipedream can watch for new posts and update a master document that tracks all published content. + +- **Contract Generation from CRM Events**: Generate personalized contracts in Google Docs when a new deal is marked as won in a CRM platform like Salesforce. With Pipedream, the workflow can populate a contract template with details from the CRM and share the document with relevant parties. + +- **Meeting Notes Distribution**: After a meeting, distribute notes or action items captured in Google Docs to all attendees using their email addresses. Pipedream can extract the contents of a document and send customized emails through a service like SendGrid, ensuring everyone stays in the loop. diff --git a/components/google_docs/actions/append-image/append-image.mjs b/components/google_docs/actions/append-image/append-image.mjs index 19616795cba20..71e5a369f330d 100644 --- a/components/google_docs/actions/append-image/append-image.mjs +++ b/components/google_docs/actions/append-image/append-image.mjs @@ -3,8 +3,8 @@ import googleDocs from "../../google_docs.app.mjs"; export default { key: "google_docs-append-image", name: "Append Image to Document", - description: "Appends an image to the end of a document. [See the docs](https://developers.google.com/docs/api/reference/rest/v1/documents/request#InsertInlineImageRequest)", - version: "0.0.3", + description: "Appends an image to the end of a document. [See the documentation](https://developers.google.com/docs/api/reference/rest/v1/documents/request#InsertInlineImageRequest)", + version: "0.0.6", type: "action", props: { googleDocs, @@ -28,13 +28,11 @@ export default { }, }, async run({ $ }) { - const image = { + await this.googleDocs.appendImage(this.docId, { uri: this.imageUri, - }; - const { data } = await this.googleDocs.appendImage(this.docId, image, this.appendAtBeginning); - $.export("$summary", "Successfully appended image to doc"); - return { - documentId: data.documentId, - }; + }, this.appendAtBeginning); + const doc = this.googleDocs.getDocument(this.docId); + $.export("$summary", `Successfully appended image to document with ID: ${this.docId}`); + return doc; }, }; diff --git a/components/google_docs/actions/append-text/append-text.mjs b/components/google_docs/actions/append-text/append-text.mjs index 69a1c9610f189..712f38c3374c1 100644 --- a/components/google_docs/actions/append-text/append-text.mjs +++ b/components/google_docs/actions/append-text/append-text.mjs @@ -3,8 +3,8 @@ import googleDocs from "../../google_docs.app.mjs"; export default { key: "google_docs-append-text", name: "Append Text", - description: "Append text to an existing document. [See the docs](https://developers.google.com/docs/api/reference/rest/v1/documents/request#InsertTextRequest)", - version: "0.1.2", + description: "Append text to an existing document. [See the documentation](https://developers.google.com/docs/api/reference/rest/v1/documents/request#InsertTextRequest)", + version: "0.1.5", type: "action", props: { googleDocs, @@ -28,13 +28,11 @@ export default { }, }, async run({ $ }) { - const text = { + await this.googleDocs.insertText(this.docId, { text: this.text, - }; - await this.googleDocs.insertText(this.docId, text, this.appendAtBeginning); - $.export("$summary", "Successfully appended text to doc"); - return { - documentId: this.docId, - }; + }, this.appendAtBeginning); + const doc = this.googleDocs.getDocument(this.docId); + $.export("$summary", `Successfully appended text to document with ID: ${this.docId}`); + return doc; }, }; diff --git a/components/google_docs/actions/create-document-from-template/create-document-from-template.mjs b/components/google_docs/actions/create-document-from-template/create-document-from-template.mjs new file mode 100644 index 0000000000000..faf70debd6d97 --- /dev/null +++ b/components/google_docs/actions/create-document-from-template/create-document-from-template.mjs @@ -0,0 +1,32 @@ +import app from "../../google_docs.app.mjs"; +import common from "@pipedream/google_drive/actions/create-file-from-template/create-file-from-template.mjs"; + +import utils from "../../common/utils.mjs"; + +const { + // eslint-disable-next-line no-unused-vars + name, description, type, ...others +} = common; +const props = utils.adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "google_docs-create-document-from-template", + name: "Create New Document From Template", + version: "0.0.1", + description, + type, + props: { + googleDrive: app, + ...props, + templateId: { + propDefinition: [ + app, + "docId", + ], + label: "Template", + description: + "Select the template document you'd like to use as the template, or use a custom expression to reference a document ID from a previous step. Template documents should contain placeholders in the format `{{xyz}}`.", + }, + }, +}; diff --git a/components/google_docs/actions/create-document-from-text/create-document-from-text.mjs b/components/google_docs/actions/create-document-from-text/create-document-from-text.mjs deleted file mode 100644 index 2437bfd76057c..0000000000000 --- a/components/google_docs/actions/create-document-from-text/create-document-from-text.mjs +++ /dev/null @@ -1,30 +0,0 @@ -import googleDocs from "../../google_docs.app.mjs"; - -export default { - key: "google_docs-create-document-from-text", - name: "Create New Document from Text", - description: "Create a new document from plain text. [See the docs](https://developers.google.com/docs/api/reference/rest/v1/documents/create)", - version: "0.0.3", - type: "action", - props: { - googleDocs, - title: "string", - text: { - propDefinition: [ - googleDocs, - "text", - ], - }, - }, - async run({ $ }) { - const { documentId } = await this.googleDocs.createEmptyDoc(this.title); - const text = { - text: this.text, - }; - await this.googleDocs.insertText(documentId, text); - $.export("$summary", "Successfully created doc"); - return { - documentId, - }; - }, -}; diff --git a/components/google_docs/actions/create-document/create-document.mjs b/components/google_docs/actions/create-document/create-document.mjs index 8945536c1cb1b..9b1a20da98dc9 100644 --- a/components/google_docs/actions/create-document/create-document.mjs +++ b/components/google_docs/actions/create-document/create-document.mjs @@ -3,16 +3,59 @@ import googleDocs from "../../google_docs.app.mjs"; export default { key: "google_docs-create-document", name: "Create a New Document", - description: "Create a new, empty document. To add content after creating the document, pass the document ID exported by this step to the Append Text action. [See the docs](https://developers.google.com/docs/api/reference/rest/v1/documents/create)", - version: "0.1.2", + description: "Create a new document. [See the documentation](https://developers.google.com/docs/api/reference/rest/v1/documents/create)", + version: "0.1.5", type: "action", props: { googleDocs, - title: "string", + title: { + type: "string", + label: "Title", + description: "Title of the new document", + }, + text: { + propDefinition: [ + googleDocs, + "text", + ], + optional: true, + }, + folderId: { + propDefinition: [ + googleDocs, + "folderId", + ], + optional: true, + }, }, async run({ $ }) { - const createdDoc = await this.googleDocs.createEmptyDoc(this.title); - $.export("$summary", "Successfully created doc"); - return createdDoc; + // Create Doc + const { documentId } = await this.googleDocs.createEmptyDoc(this.title); + + // Insert text + if (this.text) { + await this.googleDocs.insertText(documentId, { + text: this.text, + }); + } + + // Move file + if (this.folderId) { + // Get file to get parents to remove + const file = await this.googleDocs.getFile(documentId); + + // Move file, removing old parents, adding new parent folder + await this.googleDocs.updateFile(documentId, { + fields: "*", + removeParents: file.parents.join(","), + addParents: this.folderId, + }); + } + + // Get updated doc resource to return + const doc = await this.googleDocs.getDocument(documentId); + + $.export("$summary", `Successfully created document with ID: ${documentId}`); + return doc; }, }; diff --git a/components/google_docs/actions/find-document/find-document.mjs b/components/google_docs/actions/find-document/find-document.mjs new file mode 100644 index 0000000000000..83d7934a45d2a --- /dev/null +++ b/components/google_docs/actions/find-document/find-document.mjs @@ -0,0 +1,36 @@ +import app from "../../google_docs.app.mjs"; +import common from "@pipedream/google_drive/actions/find-file/find-file.mjs"; +import { getListFilesOpts } from "@pipedream/google_drive/common/utils.mjs"; + +import utils from "../../common/utils.mjs"; + +const { + // eslint-disable-next-line no-unused-vars + name, description, type, ...others +} = common; +const props = utils.adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "google_docs-find-document", + name: "Find Document", + version: "0.0.1", + description, + type, + props: { + googleDrive: app, + ...props, + }, + async run({ $ }) { + const q = this.getQuery(); + const opts = getListFilesOpts(this.drive, { + q, + }); + const files = (await this.googleDrive.listFilesInPage(null, opts)).files?.filter(({ mimeType }) => mimeType === "application/vnd.google-apps.document") || []; + + $.export("$summary", `Successfully found ${files.length} file${files.length === 1 + ? "" + : "s"} with the query "${q}"`); + return files; + }, +}; diff --git a/components/google_docs/actions/get-document/get-document.mjs b/components/google_docs/actions/get-document/get-document.mjs index ce94a97d89a30..5a68e88be88a2 100644 --- a/components/google_docs/actions/get-document/get-document.mjs +++ b/components/google_docs/actions/get-document/get-document.mjs @@ -3,8 +3,8 @@ import googleDocs from "../../google_docs.app.mjs"; export default { key: "google_docs-get-document", name: "Get Document", - description: "Get the contents of the latest version of a document. [See the docs](https://developers.google.com/docs/api/reference/rest/v1/documents/get)", - version: "0.1.1", + description: "Get the contents of the latest version of a document. [See the documentation](https://developers.google.com/docs/api/reference/rest/v1/documents/get)", + version: "0.1.4", type: "action", props: { googleDocs, @@ -15,7 +15,11 @@ export default { ], }, }, - async run() { - return this.googleDocs.getDocument(this.docId); + async run({ $ }) { + const response = await this.googleDocs.getDocument(this.docId); + + $.export("$summary", `Successfully retrieved document with ID: ${this.docId}`); + + return response; }, }; diff --git a/components/google_docs/actions/replace-image/replace-image.mjs b/components/google_docs/actions/replace-image/replace-image.mjs index 059369d9768e5..92c801f59692c 100644 --- a/components/google_docs/actions/replace-image/replace-image.mjs +++ b/components/google_docs/actions/replace-image/replace-image.mjs @@ -3,8 +3,8 @@ import googleDocs from "../../google_docs.app.mjs"; export default { key: "google_docs-replace-image", name: "Replace Image", - description: "Replace image in a existing document. [See the docs](https://developers.google.com/docs/api/reference/rest/v1/documents/request#ReplaceImageRequest)", - version: "0.0.3", + description: "Replace image in a existing document. [See the documentation](https://developers.google.com/docs/api/reference/rest/v1/documents/request#ReplaceImageRequest)", + version: "0.0.6", type: "action", props: { googleDocs, @@ -37,9 +37,8 @@ export default { uri: this.imageUri, }; await this.googleDocs.replaceImage(this.docId, image); - $.export("$summary", "Successfully replaced image in doc"); - return { - documentId: this.docId, - }; + const doc = this.googleDocs.getDocument(this.docId); + $.export("$summary", `Successfully replaced image in doc with ID: ${this.docId}`); + return doc; }, }; diff --git a/components/google_docs/actions/replace-text/replace-text.mjs b/components/google_docs/actions/replace-text/replace-text.mjs index 4ccbe9277d9c0..f578bd40c1077 100644 --- a/components/google_docs/actions/replace-text/replace-text.mjs +++ b/components/google_docs/actions/replace-text/replace-text.mjs @@ -3,8 +3,8 @@ import googleDocs from "../../google_docs.app.mjs"; export default { key: "google_docs-replace-text", name: "Replace Text", - description: "Replace all instances of matched text in a existing document. [See the docs](https://developers.google.com/docs/api/reference/rest/v1/documents/request#ReplaceAllTextRequest)", - version: "0.0.3", + description: "Replace all instances of matched text in an existing document. [See the documentation](https://developers.google.com/docs/api/reference/rest/v1/documents/request#ReplaceAllTextRequest)", + version: "0.0.6", type: "action", props: { googleDocs, @@ -45,9 +45,8 @@ export default { }, }; await this.googleDocs.replaceText(this.docId, text); - $.export("$summary", "Successfully replaced text in doc"); - return { - documentId: this.docId, - }; + const doc = this.googleDocs.getDocument(this.docId); + $.export("$summary", `Successfully replaced text in doc with ID: ${this.docId}`); + return doc; }, }; diff --git a/components/google_docs/common/utils.mjs b/components/google_docs/common/utils.mjs new file mode 100644 index 0000000000000..4517bf53ca8e7 --- /dev/null +++ b/components/google_docs/common/utils.mjs @@ -0,0 +1,68 @@ +function getTextContentFromDocument(content) { + let textContent = ""; + content.forEach((element) => { + if (element.paragraph) { + element.paragraph.elements.forEach((textRun) => { + if (textRun.textRun) { + textContent += textRun.textRun.content; + } + }); + } + }); + return textContent; +} + +function addTextContentToDocument(response) { + const textContent = getTextContentFromDocument(response.body.content); + return { + textContent, + ...response, + }; +} + +function adjustPropDefinitions(props, app) { + return Object.fromEntries( + Object.entries(props).map(([ + key, + prop, + ]) => { + if (typeof prop === "string") return [ + key, + prop, + ]; + const { + propDefinition, ...otherValues + } = prop; + if (propDefinition) { + const [ + , ...otherDefs + ] = propDefinition; + return [ + key, + { + propDefinition: [ + app, + ...otherDefs, + ], + ...otherValues, + }, + ]; + } + return [ + key, + otherValues.type === "app" + ? null + : otherValues, + ]; + }) + .filter(([ + , value, + ]) => value), + ); +} + +export default { + getTextContentFromDocument, + addTextContentToDocument, + adjustPropDefinitions, +}; diff --git a/components/google_docs/google_docs.app.mjs b/components/google_docs/google_docs.app.mjs index 9912749109b7d..9623b990184fd 100644 --- a/components/google_docs/google_docs.app.mjs +++ b/components/google_docs/google_docs.app.mjs @@ -1,5 +1,6 @@ import docs from "@googleapis/docs"; -import googleDrive from "../google_drive/google_drive.app.mjs"; +import googleDrive from "@pipedream/google_drive"; +import utils from "./common/utils.mjs"; export default { type: "app", @@ -9,12 +10,13 @@ export default { docId: { type: "string", label: "Document", - description: "Select a document or enter a custom expression to pass a value from a previous step (e.g., `{{steps.foo.$return_value.documentId}}`) or to manually enter a static ID (e.g., `1KuEN7k8jVP3Qi0_svM5OO8oEuiLkq0csihobF67eat8`).", + description: "Search for and select a document. You can also use a custom expression to pass a value from a previous step (e.g., `{{steps.foo.$return_value.documentId}}`) or you can enter a static ID (e.g., `1KuEN7k8jVP3Qi0_svM5OO8oEuiLkq0csihobF67eat8`).", + useQuery: true, async options({ - prevContext, driveId, + prevContext, driveId, query, }) { const { nextPageToken } = prevContext; - return this.listDocsOptions(driveId, nextPageToken); + return this.listDocsOptions(driveId, query, nextPageToken); }, }, imageId: { @@ -35,7 +37,7 @@ export default { imageUri: { type: "string", label: "Image URL", - description: "The URL of the image you want to insert to the doc", + description: "The URL of the image you want to insert into the doc", }, text: { type: "string", @@ -104,7 +106,8 @@ export default { const { data } = await this.docs().documents.get({ documentId, }); - return data; + const doc = utils.addTextContentToDocument(data); + return doc; }, async createEmptyDoc(title) { const { data: createdDoc } = await this.docs().documents.create({ @@ -128,8 +131,11 @@ export default { async replaceImage(documentId, image) { return this._batchUpdate(documentId, "replaceImage", image); }, - async listDocsOptions(driveId, pageToken = null) { - const q = "mimeType='application/vnd.google-apps.document'"; + async listDocsOptions(driveId, query, pageToken = null) { + let q = "mimeType='application/vnd.google-apps.document'"; + if (query) { + q = `${q} and name contains '${query}'`; + } let request = { q, }; diff --git a/components/google_docs/package.json b/components/google_docs/package.json index be44a1268a6d7..d3f2d39b36b28 100644 --- a/components/google_docs/package.json +++ b/components/google_docs/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/google_docs", - "version": "0.3.6", + "version": "0.4.2", "description": "Pipedream Google_docs Components", "main": "google_docs.app.mjs", "keywords": [ @@ -13,6 +13,7 @@ "access": "public" }, "dependencies": { - "@googleapis/docs": "^0.2.0" + "@googleapis/docs": "^3.3.0", + "@pipedream/google_drive": "^0.8.8" } } diff --git a/components/google_docs/sources/common/base.mjs b/components/google_docs/sources/common/base.mjs new file mode 100644 index 0000000000000..d25a51a0c33a6 --- /dev/null +++ b/components/google_docs/sources/common/base.mjs @@ -0,0 +1,83 @@ +import newFilesInstant from "@pipedream/google_drive/sources/new-files-instant/new-files-instant.mjs"; +import googleDrive from "../../google_docs.app.mjs"; +import { MY_DRIVE_VALUE } from "@pipedream/google_drive/common/constants.mjs"; + +export default { + ...newFilesInstant, + props: { + googleDrive, + db: "$.service.db", + http: "$.interface.http", + timer: newFilesInstant.props.timer, + folders: { + propDefinition: [ + googleDrive, + "folderId", + ], + type: "string[]", + description: "(Optional) The folders you want to watch. Leave blank to watch for any new document.", + optional: true, + }, + }, + hooks: { + ...newFilesInstant.hooks, + async deploy() { + // Emit sample records on the first run + const docs = await this.getDocuments(5); + await this.emitFiles(docs); + }, + }, + methods: { + ...newFilesInstant.methods, + getDriveId() { + return googleDrive.methods.getDriveId(MY_DRIVE_VALUE); + }, + shouldProcess(file) { + return ( + file.mimeType.includes("document") && + newFilesInstant.methods.shouldProcess.bind(this)(file) + ); + }, + getDocumentsFromFolderOpts(folderId) { + const mimeQuery = "mimeType = 'application/vnd.google-apps.document'"; + let opts = { + q: `${mimeQuery} and parents in '${folderId}' and trashed = false`, + }; + return opts; + }, + async getDocumentsFromFiles(files, limit) { + return files.reduce(async (acc, file) => { + const docs = await acc; + const fileInfo = await this.googleDrive.getFile(file.id); + return docs.length >= limit + ? docs + : docs.concat(fileInfo); + }, []); + }, + async getDocuments(limit) { + const foldersIds = this.folders; + + if (!foldersIds?.length) { + const opts = this.getDocumentsFromFolderOpts("root"); + const { files } = await this.googleDrive.listFilesInPage(null, opts); + return this.getDocumentsFromFiles(files, limit); + } + + return foldersIds.reduce(async (docs, folderId) => { + const opts = this.getDocumentsFromFolderOpts(folderId); + const { files } = await this.googleDrive.listFilesInPage(null, opts); + const nextDocuments = await this.getDocumentsFromFiles(files, limit); + return (await docs).concat(nextDocuments); + }, []); + }, + async emitFiles(files) { + for (const file of files) { + if (!this.shouldProcess(file)) { + continue; + } + const doc = await this.googleDrive.getDocument(file.id); + this.$emit(doc, this.generateMeta(doc)); + } + }, + }, +}; diff --git a/components/google_docs/sources/new-document-created/new-document-created.mjs b/components/google_docs/sources/new-document-created/new-document-created.mjs new file mode 100644 index 0000000000000..1fb69e568c26a --- /dev/null +++ b/components/google_docs/sources/new-document-created/new-document-created.mjs @@ -0,0 +1,38 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "google_docs-new-document-created", + name: "New Document Created (Instant)", + description: "Emit new event when a new document is created in Google Docs. [See the documentation](https://developers.google.com/drive/api/reference/rest/v3/changes/watch)", + version: "0.0.3", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + generateMeta(doc) { + return { + id: doc.documentId, + summary: `New Document: ${doc.documentId}`, + ts: Date.now(), + }; + }, + async processChanges() { + const lastFileCreatedTime = this._getLastFileCreatedTime(); + const timeString = new Date(lastFileCreatedTime).toISOString(); + + const args = this.getListFilesOpts({ + q: `mimeType != "application/vnd.google-apps.folder" and createdTime > "${timeString}" and trashed = false`, + orderBy: "createdTime desc", + fields: "*", + }); + + const { files } = await this.googleDrive.listFilesInPage(null, args); + if (!files?.length) { + return; + } + await this.emitFiles(files); + this._setLastFileCreatedTime(Date.parse(files[0].createdTime)); + }, + }, +}; diff --git a/components/google_docs/sources/new-or-updated-document/new-or-updated-document.mjs b/components/google_docs/sources/new-or-updated-document/new-or-updated-document.mjs new file mode 100644 index 0000000000000..9c6a32a23c7f0 --- /dev/null +++ b/components/google_docs/sources/new-or-updated-document/new-or-updated-document.mjs @@ -0,0 +1,51 @@ +import common from "../common/base.mjs"; +import { + GOOGLE_DRIVE_NOTIFICATION_CHANGE, + GOOGLE_DRIVE_NOTIFICATION_UPDATE, +} from "@pipedream/google_drive/common/constants.mjs"; + +export default { + ...common, + key: "google_docs-new-or-updated-document", + name: "New or Updated Document (Instant)", + description: "Emit new event when a document is created or updated in Google Docs. [See the documentation](https://developers.google.com/drive/api/reference/rest/v3/changes/watch)", + version: "0.0.3", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getUpdateTypes() { + return [ + GOOGLE_DRIVE_NOTIFICATION_CHANGE, + GOOGLE_DRIVE_NOTIFICATION_UPDATE, + ]; + }, + generateMeta(doc) { + return { + id: doc.revisionId, + summary: `Document Updated: ${doc.documentId}`, + ts: Date.now(), + }; + }, + async processChanges(changedFiles) { + const filteredFiles = this.checkMinimumInterval(changedFiles); + + for (const file of filteredFiles) { + file.parents = (await this.googleDrive.getFile(file.id, { + fields: "parents", + })).parents; + + console.log(file); // see what file was processed + + if (!this.shouldProcess(file)) { + console.log(`Skipping file ${file.name}`); + continue; + } + + const doc = await this.googleDrive.getDocument(file.id); + const meta = this.generateMeta(doc); + this.$emit(doc, meta); + } + }, + }, +}; diff --git a/components/google_drive/README.md b/components/google_drive/README.md index 9be4b8f819154..f5f362b6a9065 100644 --- a/components/google_drive/README.md +++ b/components/google_drive/README.md @@ -1,13 +1,14 @@ # Overview -Using the Google Drive API, you can build applications that: - -- Create and manage files and folders -- Download and upload files -- Share and organize files -- Search for files -- Track changes to files -- And much more! +The Google Drive API on Pipedream allows you to automate various file management tasks, such as creating, reading, updating, and deleting files within your Google Drive. You can also share files, manage permissions, and monitor changes to files and folders. This opens up possibilities for creating workflows that seamlessly integrate with other apps and services, streamlining document handling, backup processes, and collaborative workflows. + +# Example Use Cases + +- **Automated Backup to Google Drive**: Create a workflow on Pipedream that triggers at regular intervals to back up important files from your company’s server to a designated Google Drive folder. This workflow can ensure that your data is regularly saved to a secure, cloud-based location without manual intervention. + +- **Content Approval Process**: Streamline the content approval process by setting up a Pipedream workflow that watches for new files in a Google Drive "Pending Approval" folder. When a new file is detected, an email or message can be sent to the approver using an app like Gmail or Slack. Once reviewed, the file can be moved to an "Approved" folder automatically if certain conditions are met. + +- **Synchronize Files Across Cloud Platforms**: Build a workflow on Pipedream that monitors a specific folder in Google Drive for new files and automatically replicates them to another cloud storage service, such as Dropbox or OneDrive. This ensures that your files are accessible across different platforms and kept in sync without needing to upload them to each service manually. # Troubleshooting diff --git a/components/google_drive/actions/add-file-sharing-preference/add-file-sharing-preference.mjs b/components/google_drive/actions/add-file-sharing-preference/add-file-sharing-preference.mjs index 2772c30026680..effc1cfdbfb95 100644 --- a/components/google_drive/actions/add-file-sharing-preference/add-file-sharing-preference.mjs +++ b/components/google_drive/actions/add-file-sharing-preference/add-file-sharing-preference.mjs @@ -1,3 +1,12 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { + GOOGLE_DRIVE_GRANTEE_DOMAIN, + GOOGLE_DRIVE_GRANTEE_GROUP, + GOOGLE_DRIVE_GRANTEE_USER, + GOOGLE_DRIVE_ROLE_OPTIONS, + GOOGLE_DRIVE_ROLE_OPTION_FILEORGANIZER, + GOOGLE_DRIVE_ROLE_WRITER, +} from "../../common/constants.mjs"; import googleDrive from "../../google_drive.app.mjs"; /** @@ -8,10 +17,10 @@ import googleDrive from "../../google_drive.app.mjs"; */ export default { key: "google_drive-add-file-sharing-preference", - name: "Add File Sharing Preference", + name: "Share File or Folder", description: - "Add a [sharing](https://support.google.com/drive/answer/7166529) permission to the sharing preferences of a file or folder and provide a sharing URL. [See the docs](https://developers.google.com/drive/api/v3/reference/permissions/create) for more information", - version: "0.1.2", + "Add a [sharing permission](https://support.google.com/drive/answer/7166529) to the sharing preferences of a file or folder and provide a sharing URL. [See the documentation](https://developers.google.com/drive/api/v3/reference/permissions/create)", + version: "0.2.0", type: "action", props: { googleDrive, @@ -22,52 +31,121 @@ export default { ], optional: true, }, - fileOrFolderId: { + useFileOrFolder: { + type: "string", + label: "Use File or Folder", + description: "Whether to use a file or a folder for this action", + reloadProps: true, + options: [ + "File", + "Folder", + ], + }, + fileId: { propDefinition: [ googleDrive, - "fileOrFolderId", + "fileId", (c) => ({ drive: c.drive, }), ], - optional: false, - description: "The file or folder to share", + hidden: true, + description: "The file to share. You must specify either a file or a folder.", }, - role: { + folderId: { propDefinition: [ googleDrive, - "role", + "folderId", + (c) => ({ + drive: c.drive, + }), ], + hidden: true, + description: "The folder to share. You must specify either a file or a folder.", }, type: { propDefinition: [ googleDrive, "type", ], + reloadProps: true, }, - domain: { - propDefinition: [ - googleDrive, - "domain", - ], - }, - emailAddress: { - propDefinition: [ - googleDrive, - "emailAddress", - ], - }, + }, + async additionalProps(previousProps) { + const { + fileId, folderId, type, useFileOrFolder, + } = this; + + if (useFileOrFolder === "File") { + previousProps.fileId.hidden = false; + previousProps.folderId.hidden = true; + } else if (useFileOrFolder === "Folder") { + previousProps.fileId.hidden = true; + previousProps.folderId.hidden = false; + } + + const obj = {}; + if (!(fileId || folderId) || !type) return obj; + + const emailAddress = { + type: "string", + label: "Email Address", + description: + "Enter the email address of the user that you'd like to share the file or folder with (e.g. `alex@altostrat.com`).", + }; + + switch (type) { + case GOOGLE_DRIVE_GRANTEE_DOMAIN: + obj.domain = { + type: "string", + label: "Domain", + description: + "Enter the domain of the G Suite organization that you'd like to share the file or folder with (e.g. `altostrat.com`). All G Suite organization users under this domain will have access to the file you share.", + }; + break; + case GOOGLE_DRIVE_GRANTEE_GROUP: + obj.emailAddress = { + ...emailAddress, + description: + "Enter the email address of the group that you'd like to share the file or folder with (e.g. `hiking-club@altostrat.com`)", + }; + break; + case GOOGLE_DRIVE_GRANTEE_USER: + obj.emailAddress = emailAddress; + break; + + default: + break; + } + + const isFolder = !!folderId; + const options = GOOGLE_DRIVE_ROLE_OPTIONS; + + if (isFolder) { + const writerOpt = options.find(({ value }) => value === GOOGLE_DRIVE_ROLE_WRITER); + writerOpt.label = writerOpt.label.replace(/Writer/, "Contributor"); + options.push(GOOGLE_DRIVE_ROLE_OPTION_FILEORGANIZER); + } + + return { + ...obj, + role: { + type: "string", + label: "Role", + description: "The role granted by this permission", + options, + }, + }; }, async run({ $ }) { const { - fileOrFolderId, - role, - type, - domain, - emailAddress, + fileId, folderId, role, type, domain, emailAddress, } = this; + if (!(fileId || folderId)) { + throw new ConfigurationError("You must specify either a file or a folder"); + } // Create the permission for the file - await this.googleDrive.createPermission(fileOrFolderId, { + await this.googleDrive.createPermission(folderId ?? fileId, { role, type, domain, @@ -75,9 +153,16 @@ export default { }); // Get the file to get the `webViewLink` sharing URL - const resp = await this.googleDrive.getFile(this.fileOrFolderId); + const resp = await this.googleDrive.getFile(folderId ?? fileId); const webViewLink = resp.webViewLink; - $.export("$summary", `Successfully added a sharing permission to the file, "${resp.name}"`); + $.export( + "$summary", + `Successfully shared ${folderId + ? "folder" + : "file"} "${resp.name}" with ${type} "${ + emailAddress ?? domain ?? "" + }" with role '${role}'`, + ); return webViewLink; }, }; diff --git a/components/google_drive/actions/copy-file/copy-file.mjs b/components/google_drive/actions/copy-file/copy-file.mjs index 86e03cda11532..a3b2b97595fca 100644 --- a/components/google_drive/actions/copy-file/copy-file.mjs +++ b/components/google_drive/actions/copy-file/copy-file.mjs @@ -3,8 +3,8 @@ import googleDrive from "../../google_drive.app.mjs"; export default { key: "google_drive-copy-file", name: "Copy File", - description: "Create a copy of the specified file. [See the docs](https://developers.google.com/drive/api/v3/reference/files/copy) for more information", - version: "0.1.1", + description: "Create a copy of the specified file. [See the documentation](https://developers.google.com/drive/api/v3/reference/files/copy) for more information", + version: "0.1.7", type: "action", props: { googleDrive, diff --git a/components/google_drive/actions/create-file-from-template/create-file-from-template.mjs b/components/google_drive/actions/create-file-from-template/create-file-from-template.mjs index 71443b6cf10e3..a6129e2a89815 100644 --- a/components/google_drive/actions/create-file-from-template/create-file-from-template.mjs +++ b/components/google_drive/actions/create-file-from-template/create-file-from-template.mjs @@ -8,7 +8,7 @@ export default { key: "google_drive-create-file-from-template", name: "Create New File From Template", description: "Create a new Google Docs file from a template. Optionally include placeholders in the template document that will get replaced from this action. [See documentation](https://www.npmjs.com/package/google-docs-mustaches)", - version: "0.1.1", + version: "0.1.7", type: "action", props: { googleDrive, @@ -18,7 +18,7 @@ export default { "fileId", ], description: - "Select the template document you'd like to use as the template, or use a custom expression to reference a document ID from a previous step. Template documents should contain placeholders in the format {{xyz}}.", + "Select the template document you'd like to use as the template, or use a custom expression to reference a document ID from a previous step. Template documents should contain placeholders in the format `{{xyz}}`.", }, folderId: { propDefinition: [ @@ -49,7 +49,7 @@ export default { replaceValues: { type: "object", label: "Replace text placeholders", - description: "Replace text placeholders in the document. Use the format {{xyz}} in the document but exclude the curly braces in the key. (eg. `{{myPlaceholder}}` in the document will be replaced by the value of the key `myPlaceholder` in the action.", + description: "Replace text placeholders in the document. Use the format `{{xyz}}` in the document but exclude the curly braces in the key. (eg. `{{myPlaceholder}}` in the document will be replaced by the value of the key `myPlaceholder` in the action.", }, }, async run({ $ }) { diff --git a/components/google_drive/actions/create-file-from-text/create-file-from-text.mjs b/components/google_drive/actions/create-file-from-text/create-file-from-text.mjs index 9bde42fd74253..8114e26e48f1f 100644 --- a/components/google_drive/actions/create-file-from-text/create-file-from-text.mjs +++ b/components/google_drive/actions/create-file-from-text/create-file-from-text.mjs @@ -4,8 +4,8 @@ import { Readable } from "stream"; export default { key: "google_drive-create-file-from-text", name: "Create New File From Text", - description: "Create a new file from plain text. [See the docs](https://developers.google.com/drive/api/v3/reference/files/create) for more information", - version: "0.1.1", + description: "Create a new file from plain text. [See the documentation](https://developers.google.com/drive/api/v3/reference/files/create) for more information", + version: "0.2.0", type: "action", props: { googleDrive, @@ -33,6 +33,7 @@ export default { googleDrive, "fileName", ], + label: "File Name", description: "The name of the file you want to create (e.g., `myFile.txt`)", }, @@ -43,24 +44,66 @@ export default { optional: true, default: "", }, + mimeType: { + type: "string", + label: "Conversion Format", + description: + "The [format](https://developers.google.com/drive/api/v3/ref-export-formats) in which the text is presented", + optional: true, + default: "text/plain", + options: [ + { + value: "text/plain", + label: "Plain Text", + }, + { + value: "text/markdown", + label: "Markdown", + }, + { + value: "text/html", + label: "HTML", + }, + { + value: "application/rtf", + label: "Rich Text", + }, + { + value: "text/csv", + label: "CSV", + }, + ], + }, }, async run({ $ }) { const { parentId, name, content, + mimeType, } = this; const file = Readable.from([ content, ]); + const drive = this.googleDrive.drive(); const driveId = this.googleDrive.getDriveId(this.drive); - const resp = await this.googleDrive.createFile({ - mimeType: "text/plain", - file, - name, - parentId, - driveId, + const parent = parentId ?? driveId; + + const { data: resp } = await drive.files.create({ + supportsAllDrives: true, + media: { + mimeType, + body: file, + }, + requestBody: { + name, + mimeType: "application/vnd.google-apps.document", + parents: [ + parent, + ], + }, }); + $.export("$summary", `Successfully created a new file, "${resp.name}"`); return resp; }, diff --git a/components/google_drive/actions/create-file/create-file.mjs b/components/google_drive/actions/create-file/create-file.mjs deleted file mode 100644 index 0d4960fd61669..0000000000000 --- a/components/google_drive/actions/create-file/create-file.mjs +++ /dev/null @@ -1,242 +0,0 @@ -import googleDrive from "../../google_drive.app.mjs"; -import fs from "fs"; -import got from "got@13.0.0"; -import { toSingleLineString } from "../../common/utils.mjs"; - -export default { - key: "google_drive-create-file", - name: "Create a New File", - description: "Create a new file from a URL or /tmp/filepath. [See the docs](https://developers.google.com/drive/api/v3/reference/files/create) for more information", - version: "0.1.3", - type: "action", - props: { - googleDrive, - drive: { - propDefinition: [ - googleDrive, - "watchedDrive", - ], - }, - parent: { - propDefinition: [ - googleDrive, - "folderId", - (c) => ({ - drive: c.drive, - }), - ], - label: "Parent Folder", - description: toSingleLineString(` - The ID of the parent folder which contains the file. If not specified, the file will be - placed directly in the drive's top-level folder. - `), - optional: true, - }, - uploadType: { - propDefinition: [ - googleDrive, - "uploadType", - ], - }, - fileUrl: { - propDefinition: [ - googleDrive, - "fileUrl", - ], - }, - filePath: { - propDefinition: [ - googleDrive, - "filePath", - ], - }, - ignoreDefaultVisibility: { - type: "boolean", - label: "Ignore Default Visibility", - description: toSingleLineString(` - Whether to ignore the domain's default visibility settings for the created - file. Domain administrators can choose to make all uploaded files visible to the domain - by default; this parameter bypasses that behavior for the request. Permissions are still - inherited from parent folders. - `), - default: false, - }, - includePermissionsForView: { - type: "string", - label: "Include Permissions For View", - description: toSingleLineString(` - Specifies which additional view's permissions to include in the response. Only - 'published' is supported. - `), - optional: true, - options: [ - "published", - ], - }, - keepRevisionForever: { - propDefinition: [ - googleDrive, - "keepRevisionForever", - ], - default: false, - }, - ocrLanguage: { - propDefinition: [ - googleDrive, - "ocrLanguage", - ], - }, - useContentAsIndexableText: { - propDefinition: [ - googleDrive, - "useContentAsIndexableText", - ], - default: false, - }, - supportsAllDrives: { - type: "boolean", - label: "Supports All Drives", - description: toSingleLineString(` - Whether to include shared drives. Set to 'true' if saving to a shared drive. - Defaults to 'false' if left blank. - `), - optional: true, - }, - contentHintsIndexableText: { - type: "string", - label: "Content Hints Indexable Text", - description: toSingleLineString(` - Text to be indexed for the file to improve fullText queries. This is limited to 128KB in - length and may contain HTML elements. - `), - optional: true, - }, - contentRestrictionsReadOnly: { - type: "boolean", - label: "Content Restrictions Read Only", - description: toSingleLineString(` - Whether the content of the file is read-only. If a file is read-only, a new revision of - the file may not be added, comments may not be added or modified, and the title of the file - may not be modified. - `), - optional: true, - }, - contentRestrictionsReason: { - type: "string", - label: "Content Restrictions Reason", - description: toSingleLineString(` - Reason for why the content of the file is restricted. This is only mutable on requests - that also set readOnly=true. - `), - optional: true, - }, - copyRequiresWriterPermission: { - type: "boolean", - label: "Copy Requires Writer Permission", - description: toSingleLineString(` - Whether the options to copy, print, or download this file, should be disabled for - readers and commentators - `), - optional: true, - }, - description: { - type: "string", - label: "Description", - description: "A short description of the file", - optional: true, - }, - folderColorRgb: { - type: "string", - label: "Folder Color RGB", - description: toSingleLineString(` - The color for a folder as an RGB hex string. If an unsupported color is specified, - the closest color in the palette will be used instead. - `), - optional: true, - }, - mimeType: { - propDefinition: [ - googleDrive, - "mimeType", - ], - }, - name: { - propDefinition: [ - googleDrive, - "fileName", - ], - description: "Name of the file", - }, - originalFilename: { - type: "string", - label: "Original Filename", - description: - "The original filename of the uploaded content if available, or else the original value of the name field. This is only available for files with binary content in Google Drive.", - optional: true, - }, - shortcutDetailsTargetId: { - type: "string", - label: "Shortcut Details Target ID", - description: "The ID of the file that this shortcut points to", - optional: true, - }, - starred: { - type: "boolean", - label: "Starred", - description: "Whether the user has starred the file", - optional: true, - }, - writersCanShare: { - type: "boolean", - label: "Writers Can Share", - description: - "Whether users with only writer permission can modify the file's permissions. Not populated for items in shared drives.", - optional: true, - }, - }, - async run({ $ }) { - const body = this.fileUrl - ? await got.stream(this.fileUrl) - : fs.createReadStream(this.filePath); - const driveId = this.googleDrive.getDriveId(this.drive); - const resp = await this.googleDrive.createFile({ - ignoreDefaultVisibility: this.ignoreDefaultVisibility, - includePermissionsForView: this.includePermissionsForView, - keepRevisionForever: this.keepRevisionForever, - ocrLanguage: this.ocrLanguage, - useContentAsIndexableText: this.useContentAsIndexableText, - supportsAllDrives: this.supportsAllDrives, - resource: { - name: this.name, - originalFilename: this.originalFilename, - parents: [ - this.parent ?? driveId, - ], - mimeType: this.mimeType, - description: this.description, - folderColorRgb: this.folderColorRgb, - shortcutDetails: { - targetId: this.shortcutDetailsTargetId, - }, - starred: this.starred, - writersCanShare: this.writersCanShare, - contentHints: { - indexableText: this.contentHintsIndexableText, - }, - contentRestrictions: { - readOnly: this.contentRestrictionsReadOnly, - reason: this.contentRestrictionsReason, - }, - copyRequiresWriterPermission: this.copyRequiresWriterPermission, - }, - media: { - mimeType: this.mimeType, - uploadType: this.uploadType, - body, - }, - fields: "*", - }); - $.export("$summary", `Successfully created a new file, "${resp.name}"`); - return resp; - }, -}; diff --git a/components/google_drive/actions/create-folder/create-folder.mjs b/components/google_drive/actions/create-folder/create-folder.mjs index 65d515496c10c..37769355cd5f6 100644 --- a/components/google_drive/actions/create-folder/create-folder.mjs +++ b/components/google_drive/actions/create-folder/create-folder.mjs @@ -7,13 +7,13 @@ import { import { MY_DRIVE_VALUE, GOOGLE_DRIVE_FOLDER_MIME_TYPE, -} from "../../constants.mjs"; +} from "../../common/constants.mjs"; export default { key: "google_drive-create-folder", name: "Create Folder", - description: "Create a new empty folder. [See the docs](https://developers.google.com/drive/api/v3/reference/files/create) for more information", - version: "0.1.1", + description: "Create a new empty folder. [See the documentation](https://developers.google.com/drive/api/v3/reference/files/create) for more information", + version: "0.1.8", type: "action", props: { googleDrive, @@ -32,6 +32,7 @@ export default { drive: c.drive, }), ], + label: "Parent Folder", description: toSingleLineString(` Select a folder in which to place the new folder. If not specified, the folder will be placed directly in the drive's top-level folder. @@ -77,9 +78,17 @@ export default { } else { q += ` and '${driveId}' in parents`; } - const folders = (await this.googleDrive.listFilesInPage(null, getListFilesOpts(drive, { + + const opts = getListFilesOpts(driveId, { + // Used for querying 'shared with me' folders that the user does not have direct access to + // within a shared drive (e.g., when the user can't select the driveId of the shared drive). + corpora: "user", + includeItemsFromAllDrives: true, + supportsAllDrives: true, q, - }))).files; + }); + + const folders = (await this.googleDrive.listFilesInPage(null, opts)).files; if (folders.length) { $.export("$summary", "Found existing folder, therefore not creating folder. Returning found folder."); diff --git a/components/google_drive/actions/create-shared-drive/create-shared-drive.mjs b/components/google_drive/actions/create-shared-drive/create-shared-drive.mjs index a7a2dfbf531a9..5a918522d3ee7 100644 --- a/components/google_drive/actions/create-shared-drive/create-shared-drive.mjs +++ b/components/google_drive/actions/create-shared-drive/create-shared-drive.mjs @@ -3,8 +3,8 @@ import googleDrive from "../../google_drive.app.mjs"; export default { key: "google_drive-create-shared-drive", name: "Create Shared Drive", - description: "Create a new shared drive. [See the docs](https://developers.google.com/drive/api/v3/reference/drives/create) for more information", - version: "0.1.1", + description: "Create a new shared drive. [See the documentation](https://developers.google.com/drive/api/v3/reference/drives/create) for more information", + version: "0.1.8", type: "action", props: { googleDrive, @@ -12,7 +12,6 @@ export default { type: "string", label: "Name", description: "The name of the new shared drive", - optional: true, }, }, async run({ $ }) { diff --git a/components/google_drive/actions/delete-file/delete-file.mjs b/components/google_drive/actions/delete-file/delete-file.mjs index 4685c10e81c8c..e6b13489050dd 100644 --- a/components/google_drive/actions/delete-file/delete-file.mjs +++ b/components/google_drive/actions/delete-file/delete-file.mjs @@ -4,11 +4,16 @@ export default { key: "google_drive-delete-file", name: "Delete File", description: - "Permanently delete a file or folder without moving it to the trash. [See the docs](https://developers.google.com/drive/api/v3/reference/files/delete) for more information", - version: "0.1.1", + "Permanently delete a file or folder without moving it to the trash. [See the documentation](https://developers.google.com/drive/api/v3/reference/files/delete) for more information", + version: "0.1.8", type: "action", props: { googleDrive, + infoAlert: { + type: "alert", + alertType: "warning", + content: "This action will **permanently** delete a file. If you want to move it to the trash instead, use the **[Move File to Trash](https://pipedream.com/apps/google-drive/actions/move-file-to-trash)** action.", + }, drive: { propDefinition: [ googleDrive, @@ -28,10 +33,12 @@ export default { }, }, async run({ $ }) { - await this.googleDrive.deleteFile(this.fileId); - $.export("$summary", "Successfully deleted the file"); + const { fileId } = this; + await this.googleDrive.deleteFile(fileId); + $.export("$summary", `Successfully deleted file (ID: ${fileId}`); return { success: true, + fileId, }; }, }; diff --git a/components/google_drive/actions/delete-shared-drive/delete-shared-drive.mjs b/components/google_drive/actions/delete-shared-drive/delete-shared-drive.mjs index 7da8ff7532a84..927f4048ea6aa 100644 --- a/components/google_drive/actions/delete-shared-drive/delete-shared-drive.mjs +++ b/components/google_drive/actions/delete-shared-drive/delete-shared-drive.mjs @@ -3,28 +3,30 @@ import googleDrive from "../../google_drive.app.mjs"; export default { key: "google_drive-delete-shared-drive", name: "Delete Shared Drive", - description: "Delete a shared drive without any content. [See the docs](https://developers.google.com/drive/api/v3/reference/drives/delete) for more information", - version: "0.1.1", + description: "Delete a shared drive without any content. [See the documentation](https://developers.google.com/drive/api/v3/reference/drives/delete) for more information", + version: "0.1.7", type: "action", props: { googleDrive, drive: { propDefinition: [ googleDrive, - "watchedDrive", + "sharedDrive", ], description: "Select a [shared drive](https://support.google.com/a/users/answer/9310351) to delete.", - default: "", + optional: false, }, }, async run({ $ }) { + const { drive } = this; await this.googleDrive.deleteSharedDrive( - this.googleDrive.getDriveId(this.drive), + this.googleDrive.getDriveId(drive), ); $.export("$summary", "Successfully deleted the shared drive"); return { success: true, + drive, }; }, }; diff --git a/components/google_drive/actions/download-file/download-file.mjs b/components/google_drive/actions/download-file/download-file.mjs index e32166162c39f..37e7989b8a921 100644 --- a/components/google_drive/actions/download-file/download-file.mjs +++ b/components/google_drive/actions/download-file/download-file.mjs @@ -2,7 +2,7 @@ import googleDrive from "../../google_drive.app.mjs"; import fs from "fs"; import stream from "stream"; import { promisify } from "util"; -import { GOOGLE_DRIVE_MIME_TYPE_PREFIX } from "../../constants.mjs"; +import { GOOGLE_DRIVE_MIME_TYPE_PREFIX } from "../../common/constants.mjs"; import googleWorkspaceExportFormats from "../google-workspace-export-formats.mjs"; import { toSingleLineString } from "../../common/utils.mjs"; @@ -17,8 +17,8 @@ import { toSingleLineString } from "../../common/utils.mjs"; export default { key: "google_drive-download-file", name: "Download File", - description: "Download a file. [See the docs](https://developers.google.com/drive/api/v3/manage-downloads) for more information", - version: "0.1.1", + description: "Download a file. [See the documentation](https://developers.google.com/drive/api/v3/manage-downloads) for more information", + version: "0.1.7", type: "action", props: { googleDrive, @@ -27,7 +27,6 @@ export default { googleDrive, "watchedDrive", ], - optional: true, }, fileId: { @@ -44,7 +43,7 @@ export default { type: "string", label: "Destination File Path", description: toSingleLineString(` - The destination path for the file in the [\`/tmp\` + The destination file name or path [in the \`/tmp\` directory](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory) (e.g., \`/tmp/myFile.csv\`) `), @@ -113,8 +112,14 @@ export default { // Stream file to `filePath` const pipeline = promisify(stream.pipeline); - await pipeline(file, fs.createWriteStream(this.filePath)); + const filePath = this.filePath.includes("tmp/") + ? this.filePath + : `/tmp/${this.filePath}`; + await pipeline(file, fs.createWriteStream(filePath)); $.export("$summary", `Successfully downloaded the file, "${fileMetadata.name}"`); - return fileMetadata; + return { + fileMetadata, + filePath, + }; }, }; diff --git a/components/google_drive/actions/find-file/find-file.mjs b/components/google_drive/actions/find-file/find-file.mjs index 8d391ea26a2df..261eabaee5db5 100644 --- a/components/google_drive/actions/find-file/find-file.mjs +++ b/components/google_drive/actions/find-file/find-file.mjs @@ -1,11 +1,12 @@ import googleDrive from "../../google_drive.app.mjs"; import { getListFilesOpts } from "../../common/utils.mjs"; +import commonSearchQuery from "../../common/commonSearchQuery.mjs"; export default { key: "google_drive-find-file", name: "Find File", - description: "Search for a specific file by name. [See the docs](https://developers.google.com/drive/api/v3/search-files) for more information", - version: "0.1.1", + description: "Search for a specific file by name. [See the documentation](https://developers.google.com/drive/api/v3/search-files) for more information", + version: "0.1.7", type: "action", props: { googleDrive, @@ -16,20 +17,17 @@ export default { ], optional: true, }, - nameSearchTerm: { - propDefinition: [ - googleDrive, - "fileNameSearchTerm", - ], - }, + ...commonSearchQuery.props, }, + methods: commonSearchQuery.methods, async run({ $ }) { + const q = this.getQuery(); const opts = getListFilesOpts(this.drive, { - q: `name contains '${this.nameSearchTerm}'`, + q, }); const files = (await this.googleDrive.listFilesInPage(null, opts)).files; // eslint-disable-next-line multiline-ternary - $.export("$summary", `Successfully found ${files.length} file${files.length === 1 ? "" : "s"} containing the term, "${this.nameSearchTerm}"`); + $.export("$summary", `Successfully found ${files.length} file${files.length === 1 ? "" : "s"} with the query "${q}"`); return files; }, }; diff --git a/components/google_drive/actions/find-folder/find-folder.mjs b/components/google_drive/actions/find-folder/find-folder.mjs index e4136b9415baa..8d57a5db04e65 100644 --- a/components/google_drive/actions/find-folder/find-folder.mjs +++ b/components/google_drive/actions/find-folder/find-folder.mjs @@ -1,13 +1,13 @@ import googleDrive from "../../google_drive.app.mjs"; import { getListFilesOpts } from "../../common/utils.mjs"; -import { GOOGLE_DRIVE_FOLDER_MIME_TYPE } from "../../constants.mjs"; +import { GOOGLE_DRIVE_FOLDER_MIME_TYPE } from "../../common/constants.mjs"; export default { key: "google_drive-find-folder", name: "Find Folder", - description: "Search for a specific folder by name. [See the docs](https://developers.google.com/drive/api/v3/search-files) for more information", - version: "0.1.1", + description: "Search for a specific folder by name. [See the documentation](https://developers.google.com/drive/api/v3/search-files) for more information", + version: "0.1.7", type: "action", props: { googleDrive, diff --git a/components/google_drive/actions/find-forms/find-forms.mjs b/components/google_drive/actions/find-forms/find-forms.mjs index 216ff87661d29..588cf528a579c 100644 --- a/components/google_drive/actions/find-forms/find-forms.mjs +++ b/components/google_drive/actions/find-forms/find-forms.mjs @@ -1,11 +1,12 @@ import googleDrive from "../../google_drive.app.mjs"; import { getListFilesOpts } from "../../common/utils.mjs"; +import commonSearchQuery from "../../common/commonSearchQuery.mjs"; export default { key: "google_drive-find-forms", name: "Find Forms", - description: "List Google Form documents or search for a Form by name. [See the docs](https://developers.google.com/drive/api/v3/search-files) for more information", - version: "0.0.2", + description: "List Google Form documents or search for a Form by name. [See the documentation](https://developers.google.com/drive/api/v3/search-files) for more information", + version: "0.0.8", type: "action", props: { googleDrive, @@ -28,27 +29,30 @@ export default { description: "The ID of the parent folder which contains the file. If not specified, it will list files from the drive's top-level folder.", optional: true, }, - nameSearchTerm: { - propDefinition: [ - googleDrive, - "fileNameSearchTerm", - ], - optional: true, + queryAlert: { + type: "alert", + alertType: "info", + content: "If no query or search name is specified, all forms in the selected drive/folder will be returned.", + }, + ...commonSearchQuery.props, + searchQuery: { + ...commonSearchQuery.props.searchQuery, + description: + "Search for a file with a query. [See the documentation](https://developers.google.com/drive/api/guides/ref-search-terms) for more information. If specified, `Search Name` and `Parent Folder` will be ignored.", }, }, + methods: commonSearchQuery.methods, async run({ $ }) { - let q = "mimeType = 'application/vnd.google-apps.form'"; - if (this.nameSearchTerm) { - q = `${q} and name contains '${this.nameSearchTerm}'`; - } - if (this.folderId) { - q = `${q} and "${this.folderId}" in parents`; - } + const q = this.getQuery("form", this.folderId); const opts = getListFilesOpts(this.drive, { - q: q.trim(), + q, }); const files = (await this.googleDrive.listFilesInPage(null, opts)).files; - $.export("$summary", `Successfully found ${files.length} form(s)`); - return files; + $.export("$summary", `Successfully found ${files.length} form(s) with the query "${q}"`); + return files.map((file) => ({ + ...file, + url: `https://docs.google.com/forms/d/${file.id}`, + })) + ; }, }; diff --git a/components/google_drive/actions/find-spreadsheets/find-spreadsheets.mjs b/components/google_drive/actions/find-spreadsheets/find-spreadsheets.mjs index c723bb3814144..d38e6b625be01 100644 --- a/components/google_drive/actions/find-spreadsheets/find-spreadsheets.mjs +++ b/components/google_drive/actions/find-spreadsheets/find-spreadsheets.mjs @@ -1,11 +1,12 @@ import googleDrive from "../../google_drive.app.mjs"; import { getListFilesOpts } from "../../common/utils.mjs"; +import commonSearchQuery from "../../common/commonSearchQuery.mjs"; export default { key: "google_drive-find-spreadsheets", name: "Find Spreadsheets", - description: "Search for a specific spreadsheet by name. [See the docs](https://developers.google.com/drive/api/v3/search-files) for more information", - version: "0.1.1", + description: "Search for a specific spreadsheet by name. [See the documentation](https://developers.google.com/drive/api/v3/search-files) for more information", + version: "0.1.7", type: "action", props: { googleDrive, @@ -28,27 +29,29 @@ export default { description: "The ID of the parent folder which contains the file. If not specified, it will list files from the drive's top-level folder.", optional: true, }, - nameSearchTerm: { - propDefinition: [ - googleDrive, - "fileNameSearchTerm", - ], - optional: true, + queryAlert: { + type: "alert", + alertType: "info", + content: "If no query or search name is specified, all spreadsheets in the selected drive/folder will be returned.", + }, + ...commonSearchQuery.props, + searchQuery: { + ...commonSearchQuery.props.searchQuery, + description: + "Search for a file with a query. [See the documentation](https://developers.google.com/drive/api/guides/ref-search-terms) for more information. If specified, `Search Name` and `Parent Folder` will be ignored.", }, }, + methods: commonSearchQuery.methods, async run({ $ }) { - let q = "mimeType = 'application/vnd.google-apps.spreadsheet'"; - if (this.nameSearchTerm) { - q = `${q} and name contains '${this.nameSearchTerm}'`; - } - if (this.folderId) { - q = `${q} and "${this.folderId}" in parents`; - } + const q = this.getQuery("spreadsheet", this.folderId); const opts = getListFilesOpts(this.drive, { - q: q.trim(), + q, }); const files = (await this.googleDrive.listFilesInPage(null, opts)).files; - $.export("$summary", `Successfully found ${files.length} spreadsheet(s)`); - return files; + $.export("$summary", `Successfully found ${files.length} spreadsheet(s) with the query "${q}"`); + return files.map((file) => ({ + ...file, + url: `https://docs.google.com/spreadsheets/d/${file.id}`, + })); }, }; diff --git a/components/google_drive/actions/get-file-by-id/common-file-fields.mjs b/components/google_drive/actions/get-file-by-id/common-file-fields.mjs new file mode 100644 index 0000000000000..7e55bd33db929 --- /dev/null +++ b/components/google_drive/actions/get-file-by-id/common-file-fields.mjs @@ -0,0 +1,293 @@ +/* eslint-disable max-len */ +export const FILE_FIELD_OPTIONS = [ + { + label: + "\"kind\" - Identifies what kind of resource this is. Value: the fixed string \"drive#file\".", + value: "kind", + }, + { + label: + "\"driveId\" - ID of the shared drive the file resides in. Only populated for items in shared drives.", + value: "driveId", + }, + { + label: + "\"fileExtension\" - The final component of fullFileExtension. This is only available for files with binary content in Google Drive.", + value: "fileExtension", + }, + { + label: + "\"copyRequiresWriterPermission\" - Whether the options to copy, print, or download this file, should be disabled for readers and commenters.", + value: "copyRequiresWriterPermission", + }, + { + label: + "\"md5Checksum\" - The MD5 checksum for the content of the file. This is only applicable to files with binary content in Google Drive.", + value: "md5Checksum", + }, + { + label: + "\"contentHints\" - Additional information about the content of the file. These fields are never populated in responses.", + value: "contentHints", + }, + { + label: + "\"writersCanShare\" - Whether users with only writer permission can modify the file's permissions. Not populated for items in shared drives.", + value: "writersCanShare", + }, + { + label: "\"viewedByMe\" - Whether the file has been viewed by this user.", + value: "viewedByMe", + }, + { + label: + "\"mimeType\" - The MIME type of the file. Google Drive attempts to automatically detect an appropriate value from uploaded content, if no value is provided.", + value: "mimeType", + }, + { + label: + "\"exportLinks\" - Links for exporting Docs Editors files to specific formats.", + value: "exportLinks", + }, + { + label: + "\"parents\" - The IDs of the parent folders which contain the file.", + value: "parents", + }, + { + label: + "\"thumbnailLink\" - A short-lived link to the file's thumbnail, if available.", + value: "thumbnailLink", + }, + { + label: "\"iconLink\" - A static, unauthenticated link to the file's icon.", + value: "iconLink", + }, + { + label: + "\"shared\" - Whether the file has been shared. Not populated for items in shared drives.", + value: "shared", + }, + { + label: "\"lastModifyingUser\" - The last user to modify the file.", + value: "lastModifyingUser", + }, + { + label: + "\"owners\" - The owner of this file. Only certain legacy files may have more than one owner. This field isn't populated for items in shared drives.", + value: "owners", + }, + { + label: + "\"headRevisionId\" - The ID of the file's head revision. This is currently only available for files with binary content in Google Drive.", + value: "headRevisionId", + }, + { + label: + "\"sharingUser\" - The user who shared the file with the requesting user, if applicable.", + value: "sharingUser", + }, + { + label: + "\"webViewLink\" - A link for opening the file in a relevant Google editor or viewer in a browser.", + value: "webViewLink", + }, + { + label: + "\"webContentLink\" - A link for downloading the content of the file in a browser. This is only available for files with binary content in Google Drive.", + value: "webContentLink", + }, + { + label: + "\"size\" - Size in bytes of blobs and first party editor files. Won't be populated for files that have no size, like shortcuts and folders.", + value: "size", + }, + { + label: + "\"permissions\" - The full list of permissions for the file. This is only available if the requesting user can share the file.", + value: "permissions", + }, + { + label: + "\"hasThumbnail\" - Whether this file has a thumbnail. This does not indicate whether the requesting app has access to the thumbnail.", + value: "hasThumbnail", + }, + { + label: "\"spaces\" - The list of spaces which contain the file.", + value: "spaces", + }, + { + label: + "\"folderColorRgb\" - The color for a folder or a shortcut to a folder as an RGB hex string.", + value: "folderColorRgb", + }, + { + label: "\"id\" - The ID of the file.", + value: "id", + }, + { + label: + "\"name\" - The name of the file. This is not necessarily unique within a folder.", + value: "name", + }, + { + label: "\"description\" - A short description of the file.", + value: "description", + }, + { + label: "\"starred\" - Whether the user has starred the file.", + value: "starred", + }, + { + label: + "\"trashed\" - Whether the file has been trashed, either explicitly or from a trashed parent folder.", + value: "trashed", + }, + { + label: + "\"explicitlyTrashed\" - Whether the file has been explicitly trashed, as opposed to recursively trashed from a parent folder.", + value: "explicitlyTrashed", + }, + { + label: + "\"createdTime\" - The time at which the file was created (RFC 3339 date-time).", + value: "createdTime", + }, + { + label: + "\"modifiedTime\" - The last time the file was modified by anyone (RFC 3339 date-time).", + value: "modifiedTime", + }, + { + label: + "\"modifiedByMeTime\" - The last time the file was modified by the user (RFC 3339 date-time).", + value: "modifiedByMeTime", + }, + { + label: + "\"viewedByMeTime\" - The last time the file was viewed by the user (RFC 3339 date-time).", + value: "viewedByMeTime", + }, + { + label: + "\"sharedWithMeTime\" - The time at which the file was shared with the user, if applicable (RFC 3339 date-time).", + value: "sharedWithMeTime", + }, + { + label: + "\"quotaBytesUsed\" - The number of storage quota bytes used by the file. This includes the head revision as well as previous revisions with keepForever enabled.", + value: "quotaBytesUsed", + }, + { + label: + "\"version\" - A monotonically increasing version number for the file. This reflects every change made to the file on the server, even those not visible to the user.", + value: "version", + }, + { + label: + "\"originalFilename\" - The original filename of the uploaded content if available, or else the original value of the name field. This is only available for files with binary content in Google Drive.", + value: "originalFilename", + }, + { + label: + "\"ownedByMe\" - Whether the user owns the file. Not populated for items in shared drives.", + value: "ownedByMe", + }, + { + label: + "\"fullFileExtension\" - The full file extension extracted from the name field. May contain multiple concatenated extensions, such as \"tar.gz\". This is only available for files with binary content in Google Drive.", + value: "fullFileExtension", + }, + { + label: + "\"properties\" - A collection of arbitrary key-value pairs which are visible to all apps.Entries with null values are cleared in update and copy requests.", + value: "properties", + }, + { + label: + "\"appProperties\" - A collection of arbitrary key-value pairs which are private to the requesting app.", + value: "appProperties", + }, + { + label: + "\"isAppAuthorized\" - Whether the file was created or opened by the requesting app.", + value: "isAppAuthorized", + }, + { + label: + "\"capabilities\" - Capabilities the current user has on this file. Each capability corresponds to a fine-grained action that a user may take.", + value: "capabilities", + }, + { + label: + "\"hasAugmentedPermissions\" - Whether there are permissions directly on this file. This field is only populated for items in shared drives.", + value: "hasAugmentedPermissions", + }, + { + label: + "\"trashingUser\" - If the file has been explicitly trashed, the user who trashed it. Only populated for items in shared drives.", + value: "trashingUser", + }, + { + label: + "\"thumbnailVersion\" - The thumbnail version for use in thumbnail cache invalidation.", + value: "thumbnailVersion", + }, + { + label: + "\"trashedTime\" - The time that the item was trashed (RFC 3339 date-time). Only populated for items in shared drives.", + value: "trashedTime", + }, + { + label: "\"modifiedByMe\" - Whether the file has been modified by this user.", + value: "modifiedByMe", + }, + { + label: + "\"permissionIds\" - files.list of permission IDs for users with access to this file.", + value: "permissionIds", + }, + { + label: + "\"imageMediaMetadata\" - Additional metadata about image media, if available.", + value: "imageMediaMetadata", + }, + { + label: + "\"videoMediaMetadata\" - Additional metadata about video media. This may not be available immediately upon upload.", + value: "videoMediaMetadata", + }, + { + label: + "\"shortcutDetails\" - Shortcut file details. Only populated for shortcut files.", + value: "shortcutDetails", + }, + { + label: + "\"contentRestrictions\" - Restrictions for accessing the content of the file. Only populated if such a restriction exists.", + value: "contentRestrictions", + }, + { + label: "\"resourceKey\" - A key needed to access the item via a shared link.", + value: "resourceKey", + }, + { + label: + "\"linkShareMetadata\" - LinkShare related details. Contains details about the link URLs that clients are using to refer to this item.", + value: "linkShareMetadata", + }, + { + label: "\"labelInfo\" - An overview of the labels on the file.", + value: "labelInfo", + }, + { + label: + "\"sha1Checksum\" - The SHA1 checksum associated with this file, if available. This field is only populated for files with content stored in Google Drive.", + value: "sha1Checksum", + }, + { + label: + "\"sha256Checksum\" - The SHA256 checksum associated with this file, if available. This field is only populated for files with content stored in Google Drive.", + value: "sha256Checksum", + }, +]; diff --git a/components/google_drive/actions/get-file-by-id/get-file-by-id.mjs b/components/google_drive/actions/get-file-by-id/get-file-by-id.mjs new file mode 100644 index 0000000000000..cb8723dea9b1a --- /dev/null +++ b/components/google_drive/actions/get-file-by-id/get-file-by-id.mjs @@ -0,0 +1,56 @@ +import googleDrive from "../../google_drive.app.mjs"; +import { FILE_FIELD_OPTIONS } from "./common-file-fields.mjs"; + +export default { + key: "google_drive-get-file-by-id", + name: "Get File By ID", + description: "Get info on a specific file. [See the documentation](https://developers.google.com/drive/api/reference/rest/v3/files/get) for more information", + version: "0.0.4", + type: "action", + props: { + googleDrive, + drive: { + propDefinition: [ + googleDrive, + "watchedDrive", + ], + optional: true, + }, + fileIdTip: { + type: "alert", + alertType: "info", + content: "You can use actions such as **Find File** or **List Files** to obtain a file ID, and use its value here.", + }, + fileId: { + propDefinition: [ + googleDrive, + "fileId", + (c) => ({ + drive: c.drive, + }), + ], + description: "The file to obtain info for. You can select a file or use a file ID from a previous step.", + }, + fields: { + type: "string[]", + label: "Fields", + description: "Customize the fields to obtain for the file. [See the documentation](https://developers.google.com/drive/api/reference/rest/v3/files) for more information.", + optional: true, + options: FILE_FIELD_OPTIONS, + }, + }, + async run({ $ }) { + const { + googleDrive, fileId, fields, + } = this; + const strFields = typeof fields === "string" + ? fields + : fields?.join(); + const response = await googleDrive.getFile(fileId, { + fields: strFields, + }); + + $.export("$summary", "Successfully fetched file info"); + return response; + }, +}; diff --git a/components/google_drive/actions/get-folder-id-for-path/get-folder-id-for-path.mjs b/components/google_drive/actions/get-folder-id-for-path/get-folder-id-for-path.mjs index 33d533fff959b..4ad1c908aa6c7 100644 --- a/components/google_drive/actions/get-folder-id-for-path/get-folder-id-for-path.mjs +++ b/components/google_drive/actions/get-folder-id-for-path/get-folder-id-for-path.mjs @@ -11,8 +11,8 @@ import googleDrive from "../../google_drive.app.mjs"; export default { key: "google_drive-get-folder-id-for-path", name: "Get Folder ID for a Path", - description: "Retrieve a folderId for a path. [See the docs](https://developers.google.com/drive/api/v3/search-files) for more information", - version: "0.1.1", + description: "Retrieve a folderId for a path. [See the documentation](https://developers.google.com/drive/api/v3/search-files) for more information", + version: "0.1.9", type: "action", props: { googleDrive, diff --git a/components/google_drive/actions/get-shared-drive/get-shared-drive.mjs b/components/google_drive/actions/get-shared-drive/get-shared-drive.mjs index 7e399c9ae2aea..c9d8471cdec84 100644 --- a/components/google_drive/actions/get-shared-drive/get-shared-drive.mjs +++ b/components/google_drive/actions/get-shared-drive/get-shared-drive.mjs @@ -3,19 +3,16 @@ import googleDrive from "../../google_drive.app.mjs"; export default { key: "google_drive-get-shared-drive", name: "Get Shared Drive", - description: "Get a shared drive's metadata by ID. [See the docs](https://developers.google.com/drive/api/v3/reference/drives/get) for more information", - version: "0.1.1", + description: "Get metadata for one or all shared drives. [See the documentation](https://developers.google.com/drive/api/v3/reference/drives/get) for more information", + version: "0.1.7", type: "action", props: { googleDrive, drive: { propDefinition: [ googleDrive, - "watchedDrive", + "sharedDrive", ], - description: - "Select a [shared drive](https://support.google.com/a/users/answer/9310351).", - default: "", }, useDomainAdminAccess: { propDefinition: [ @@ -26,12 +23,15 @@ export default { }, async run({ $ }) { const resp = await this.googleDrive.getSharedDrive( - this.googleDrive.getDriveId(this.drive), + this.drive ?? null, { useDomainAdminAccess: this.useDomainAdminAccess, }, ); - $.export("$summary", `Successfully fetched the shared drive, "${resp.name}"`); + const summary = resp.drives + ? `${resp.drives.length} shared drives` + : `the shared drive "${resp.name}"`; + $.export("$summary", `Successfully fetched ${summary}`); return resp; }, }; diff --git a/components/google_drive/actions/list-files/list-files.mjs b/components/google_drive/actions/list-files/list-files.mjs index b3657aada468e..ba1a569af9686 100644 --- a/components/google_drive/actions/list-files/list-files.mjs +++ b/components/google_drive/actions/list-files/list-files.mjs @@ -5,7 +5,7 @@ export default { key: "google_drive-list-files", name: "List Files", description: "List files from a specific folder. [See the documentation](https://developers.google.com/drive/api/v3/reference/files/list) for more information", - version: "0.1.4", + version: "0.1.11", type: "action", props: { googleDrive, @@ -31,7 +31,7 @@ export default { fields: { type: "string", label: "Fields", - description: "The paths of the fields you want included in the response. If not specified, the response includes a default set of fields specific to this method. For development you can use the special value `*` to return all fields, but you'll achieve greater performance by only selecting the fields you need.\n\n**eg:** `files(id,mimeType,name,webContentLink,webViewLink)`", + description: "The fields you want included in the response [(see the documentation for available fields)](https://developers.google.com/drive/api/reference/rest/v3/files). If not specified, the response includes a default set of fields specific to this method. For development you can use the special value `*` to return all fields, but you'll achieve greater performance by only selecting the fields you need.\n\n**eg:** `files(id,mimeType,name,webContentLink,webViewLink)`", optional: true, }, filterText: { @@ -39,14 +39,31 @@ export default { description: "Filter by file name that contains a specific text", type: "string", optional: true, + reloadProps: true, }, trashed: { label: "Trashed", type: "boolean", - description: "List trashed files or non-trashed files. Keep it empty to include both.", + description: "If `true`, list **only** trashed files. If `false`, list **only** non-trashed files. Keep it empty to include both.", optional: true, }, }, + async additionalProps() { + const props = {}; + if (this.filterText) { + props.filterType = { + type: "string", + label: "Filter Type", + description: "Whether to return files with names containing the Filter Text or files with names that match the Filter Text exactly. Defaults to \"CONTAINS\"", + options: [ + "CONTAINS", + "EXACT MATCH", + ], + default: "CONTAINS", + }; + } + return props; + }, async run({ $ }) { const opts = getListFilesOpts(this.drive, { q: "", @@ -57,7 +74,9 @@ export default { if (this.filterText) { opts.q += `${opts.q ? " AND " - : ""}name contains '${this.filterText}'`; + : ""}name ${this.filterType === "CONTAINS" + ? "contains" + : "="} '${this.filterText}'`; } if (typeof this.trashed !== "undefined") { opts.q += `${opts.q diff --git a/components/google_drive/actions/move-file-to-trash/move-file-to-trash.mjs b/components/google_drive/actions/move-file-to-trash/move-file-to-trash.mjs index e462208cad860..ab16df984a109 100644 --- a/components/google_drive/actions/move-file-to-trash/move-file-to-trash.mjs +++ b/components/google_drive/actions/move-file-to-trash/move-file-to-trash.mjs @@ -1,14 +1,19 @@ import googleDrive from "../../google_drive.app.mjs"; -import { GOOGLE_DRIVE_FOLDER_MIME_TYPE } from "../../constants.mjs"; +import { GOOGLE_DRIVE_FOLDER_MIME_TYPE } from "../../common/constants.mjs"; export default { key: "google_drive-move-file-to-trash", name: "Move File to Trash", - description: "Move a file or folder to trash. [See the docs](https://developers.google.com/drive/api/v3/reference/files/update) for more information", - version: "0.1.1", + description: "Move a file or folder to trash. [See the documentation](https://developers.google.com/drive/api/v3/reference/files/update) for more information", + version: "0.1.7", type: "action", props: { googleDrive, + infoAlert: { + type: "alert", + alertType: "info", + content: "If you want to **permanently** delete a file instead, use the **[Move File to Trash](https://pipedream.com/apps/google-drive/actions/delete-file)** action.", + }, drive: { propDefinition: [ googleDrive, diff --git a/components/google_drive/actions/move-file/move-file.mjs b/components/google_drive/actions/move-file/move-file.mjs index d13591eb49c79..6977ccf850979 100644 --- a/components/google_drive/actions/move-file/move-file.mjs +++ b/components/google_drive/actions/move-file/move-file.mjs @@ -3,8 +3,8 @@ import googleDrive from "../../google_drive.app.mjs"; export default { key: "google_drive-move-file", name: "Move File", - description: "Move a file from one folder to another. [See the docs](https://developers.google.com/drive/api/v3/reference/files/update) for more information", - version: "0.1.1", + description: "Move a file from one folder to another. [See the documentation](https://developers.google.com/drive/api/v3/reference/files/update) for more information", + version: "0.1.7", type: "action", props: { googleDrive, diff --git a/components/google_drive/actions/replace-file/replace-file.mjs b/components/google_drive/actions/replace-file/replace-file.mjs deleted file mode 100644 index 882e49b7be178..0000000000000 --- a/components/google_drive/actions/replace-file/replace-file.mjs +++ /dev/null @@ -1,134 +0,0 @@ -import path from "path"; -import googleDrive from "../../google_drive.app.mjs"; -import { - omitEmptyStringValues, - getFileStream, - streamToBuffer, - byteToMB, -} from "../../common/utils.mjs"; -import { - GOOGLE_DRIVE_UPLOAD_TYPE_MEDIA, - GOOGLE_DRIVE_UPLOAD_TYPE_RESUMABLE, -} from "../../constants.mjs"; - -export default { - key: "google_drive-replace-file", - name: "Replace File", - description: "Upload a file that replaces an existing file. [See the docs](https://developers.google.com/drive/api/v3/reference/files/update) for more information", - version: "0.1.1", - type: "action", - props: { - googleDrive, - drive: { - propDefinition: [ - googleDrive, - "watchedDrive", - ], - optional: true, - }, - fileId: { - propDefinition: [ - googleDrive, - "fileId", - (c) => ({ - drive: c.drive, - }), - ], - optional: false, - description: "The file to update", - }, - fileUrl: { - propDefinition: [ - googleDrive, - "fileUrl", - ], - }, - filePath: { - propDefinition: [ - googleDrive, - "filePath", - ], - }, - name: { - propDefinition: [ - googleDrive, - "fileName", - ], - label: "Name", - description: "The name of the new file (e.g., `myFile.csv`)", - }, - mimeType: { - propDefinition: [ - googleDrive, - "mimeType", - ], - description: "The MIME type of the new file (e.g., `image/jpeg`)", - }, - uploadType: { - propDefinition: [ - googleDrive, - "uploadType", - ], - optional: true, - }, - }, - async run({ $ }) { - const { - fileId, - fileUrl, - filePath, - name, - mimeType, - } = this; - let { uploadType } = this; - if (!fileUrl && !filePath) { - throw new Error("One of File URL and File Path is required."); - } - const fileStream = await getFileStream({ - $, - fileUrl, - filePath, - }); - - if (!uploadType || uploadType === "") { - try { - // Its necessary to get the file stream again, after user streamToBuffer function and pass - // the same object to updateFileMedia function, the function will throw an error about - // circular json structure. - // Deep clone is very slow in this case, so its better get the stream again - const fileBuffer = await streamToBuffer(await getFileStream({ - $, - fileUrl, - filePath, - })); - const bufferSize = byteToMB(Buffer.byteLength(fileBuffer)); - uploadType = bufferSize > 5 - ? GOOGLE_DRIVE_UPLOAD_TYPE_RESUMABLE - : undefined; - } catch (err) { - console.log(err); - uploadType = undefined; - } - } - - if (uploadType === GOOGLE_DRIVE_UPLOAD_TYPE_MEDIA) { - uploadType = undefined; - } - console.log(`Upload type: ${uploadType}`); - // Update file media separately from metadata to prevent multipart upload, - // which `google-apis-nodejs-client` doesn't seem to support for - // [files.update](https://bit.ly/3lP5sWn) - await this.googleDrive.updateFileMedia(fileId, fileStream, omitEmptyStringValues({ - mimeType, - uploadType, - })); - - const resp = await this.googleDrive.updateFile(fileId, omitEmptyStringValues({ - name: name || path.basename(fileUrl || filePath), - mimeType, - uploadType, - })); - $.export("$summary", "Successfully replaced the file"); - return resp; - }, -}; diff --git a/components/google_drive/actions/search-shared-drives/search-shared-drives.mjs b/components/google_drive/actions/search-shared-drives/search-shared-drives.mjs index db53713b7cbda..d696f213e0882 100644 --- a/components/google_drive/actions/search-shared-drives/search-shared-drives.mjs +++ b/components/google_drive/actions/search-shared-drives/search-shared-drives.mjs @@ -3,8 +3,8 @@ import googleDrive from "../../google_drive.app.mjs"; export default { key: "google_drive-search-shared-drives", name: "Search for Shared Drives", - description: "Search for shared drives with query options. [See the docs](https://developers.google.com/drive/api/v3/search-shareddrives) for more information", - version: "0.1.1", + description: "Search for shared drives with query options. [See the documentation](https://developers.google.com/drive/api/v3/search-shareddrives) for more information", + version: "0.1.8", type: "action", props: { googleDrive, diff --git a/components/google_drive/actions/update-file/update-file.mjs b/components/google_drive/actions/update-file/update-file.mjs index 45e146065ec31..2a027e9e0b442 100644 --- a/components/google_drive/actions/update-file/update-file.mjs +++ b/components/google_drive/actions/update-file/update-file.mjs @@ -3,21 +3,42 @@ import { toSingleLineString, getFileStream, } from "../../common/utils.mjs"; +import { additionalProps } from "../../common/filePathOrUrl.mjs"; export default { key: "google_drive-update-file", name: "Update File", - description: "Update a file's metadata and/or content. [See the docs](https://developers.google.com/drive/api/v3/reference/files/update) for more information", - version: "0.1.1", + description: "Update a file's metadata and/or content. [See the documentation](https://developers.google.com/drive/api/v3/reference/files/update) for more information", + version: "1.0.0", type: "action", + additionalProps, props: { googleDrive, + updateType: { + type: "string", + label: "Update Type", + description: "Whether to update content or metadata only", + options: [ + { + label: "Upload content from File URL", + value: "File URL", + }, + { + label: "Upload content from File Path", + value: "File Path", + }, + { + label: "Update file metadata only", + value: "File Metadata", + }, + ], + reloadProps: true, + }, drive: { propDefinition: [ googleDrive, "watchedDrive", ], - optional: true, }, fileId: { @@ -36,6 +57,8 @@ export default { "fileUrl", ], description: "The URL of the file to use to update content", + optional: false, + hidden: true, }, filePath: { propDefinition: [ @@ -47,6 +70,8 @@ export default { directory](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory) (e.g., \`/tmp/myFile.csv\`) with which to update content `), + optional: false, + hidden: true, }, name: { propDefinition: [ @@ -104,12 +129,10 @@ export default { }, advanced: { type: "object", - label: "Advanced Options", + label: "Additional Options", optional: true, description: toSingleLineString(` - Specify less-common properties that you want to use. See [Files: update] - (https://developers.google.com/drive/api/v3/reference/files/update#request-body) for a list - of supported properties. + Any additional parameters to pass in the request. [See the documentation](https://developers.google.com/drive/api/v3/reference/files/update#request-body) for all available parameters. `), }, }, @@ -126,16 +149,21 @@ export default { ocrLanguage, useContentAsIndexableText, advanced, + updateType, } = this; - const fileStream = - fileUrl || filePath - ? await getFileStream({ - $, - fileUrl, - filePath, - }) - : undefined; + let fileStream; + if (updateType === "File URL") { + fileStream = await getFileStream({ + $, + fileUrl, + }); + } else if (updateType === "File Path") { + fileStream = await getFileStream({ + $, + filePath, + }); + } // Update file content, if set, separately from metadata to prevent // multipart upload, which `google-apis-nodejs-client` doesn't seem to diff --git a/components/google_drive/actions/update-shared-drive/update-shared-drive.mjs b/components/google_drive/actions/update-shared-drive/update-shared-drive.mjs index aae3fdeb42037..f15353d1aedd4 100644 --- a/components/google_drive/actions/update-shared-drive/update-shared-drive.mjs +++ b/components/google_drive/actions/update-shared-drive/update-shared-drive.mjs @@ -3,8 +3,8 @@ import googleDrive from "../../google_drive.app.mjs"; export default { key: "google_drive-update-shared-drive", name: "Update Shared Drive", - description: "Update an existing shared drive. [See the docs](https://developers.google.com/drive/api/v3/reference/drives/update) for more information", - version: "0.1.1", + description: "Update an existing shared drive. [See the documentation](https://developers.google.com/drive/api/v3/reference/drives/update) for more information", + version: "0.1.7", type: "action", props: { googleDrive, @@ -16,6 +16,7 @@ export default { description: "Select a [shared drive](https://support.google.com/a/users/answer/9310351) to update", default: "", + optional: false, }, useDomainAdminAccess: { propDefinition: [ @@ -23,25 +24,24 @@ export default { "useDomainAdminAccess", ], }, + themeId: { + propDefinition: [ + googleDrive, + "themeId", + ], + }, backgroundImageLink: { type: "string", label: "Background Image Link", description: - "A link to the new background image for the shared drive. Cannot be set if `Theme ID` is set in the same request.", + "A link to the new background image for the shared drive. Cannot be set if `Theme ID` is used (it already sets the background image).", optional: true, }, colorRgb: { type: "string", label: "Color", description: - "The new color of this shared drive as an RGB hex string. Cannot be set if `Theme ID` is set in the same request.", - optional: true, - }, - themeId: { - type: "string", - label: "Theme ID", - description: - "The ID of the theme from which the background image and color will be set. Cannot be set if `Color` or `Background Image Link` is set in the same request.", + "The new color of this shared drive as an RGB hex string. Cannot be set if `Theme ID` is used (it already sets the color).", optional: true, }, restrictions: { diff --git a/components/google_drive/actions/upload-file/upload-file.mjs b/components/google_drive/actions/upload-file/upload-file.mjs index 91630561acaee..92cdbbf6e3ae4 100644 --- a/components/google_drive/actions/upload-file/upload-file.mjs +++ b/components/google_drive/actions/upload-file/upload-file.mjs @@ -4,16 +4,21 @@ import { getFileStream, omitEmptyStringValues, } from "../../common/utils.mjs"; -import { GOOGLE_DRIVE_UPLOAD_TYPE_MULTIPART } from "../../constants.mjs"; +import { GOOGLE_DRIVE_UPLOAD_TYPE_MULTIPART } from "../../common/constants.mjs"; +import { + additionalProps, updateType, +} from "../../common/filePathOrUrl.mjs"; export default { key: "google_drive-upload-file", name: "Upload File", - description: "Copy an existing file to Google Drive. [See the docs](https://developers.google.com/drive/api/v3/manage-uploads) for more information", - version: "0.1.3", + description: "Upload a file to Google Drive. [See the documentation](https://developers.google.com/drive/api/v3/manage-uploads) for more information", + version: "1.0.0", type: "action", + additionalProps, props: { googleDrive, + updateType, drive: { propDefinition: [ googleDrive, @@ -38,12 +43,16 @@ export default { googleDrive, "fileUrl", ], + optional: false, + hidden: true, }, filePath: { propDefinition: [ googleDrive, "filePath", ], + optional: false, + hidden: true, }, name: { propDefinition: [ @@ -88,9 +97,6 @@ export default { mimeType, } = this; let { uploadType } = this; - if (!fileUrl && !filePath) { - throw new Error("One of File URL and File Path is required."); - } const driveId = this.googleDrive.getDriveId(this.drive); const filename = name || path.basename(fileUrl || filePath); @@ -98,7 +104,9 @@ export default { const file = await getFileStream({ $, fileUrl, - filePath, + filePath: filePath?.startsWith("/tmp/") + ? filePath + : `/tmp/${filePath}`, }); console.log(`Upload type: ${uploadType}.`); diff --git a/components/google_drive/common/commonSearchQuery.mjs b/components/google_drive/common/commonSearchQuery.mjs new file mode 100644 index 0000000000000..7768529b5df57 --- /dev/null +++ b/components/google_drive/common/commonSearchQuery.mjs @@ -0,0 +1,48 @@ +import { ConfigurationError } from "@pipedream/platform"; +import googleDrive from "../google_drive.app.mjs"; + +export default { + methods: { + getQuery(type, folderId) { + const { + searchQuery, nameSearchTerm, + } = this; + if (!searchQuery && !nameSearchTerm && !type) { + throw new ConfigurationError("You must specify a search query or name."); + } + let q = type + ? `mimeType = 'application/vnd.google-apps.${type}'` + : ""; + if (searchQuery) { + q = (!q || searchQuery.includes(q)) + ? searchQuery + : `${q} and ${searchQuery}`; + } else { + if (nameSearchTerm) { + const nameQuery = `name contains '${nameSearchTerm}'`; + q = q + ? `${q} and ${nameQuery}` + : nameQuery; + } + if (folderId) { + q = `${q} and "${folderId}" in parents`; + } + } + return q.trim(); + }, + }, + props: { + nameSearchTerm: { + propDefinition: [ + googleDrive, + "fileNameSearchTerm", + ], + }, + searchQuery: { + propDefinition: [ + googleDrive, + "searchQuery", + ], + }, + }, +}; diff --git a/components/google_drive/common/constants.mjs b/components/google_drive/common/constants.mjs new file mode 100644 index 0000000000000..87671dcb36ae4 --- /dev/null +++ b/components/google_drive/common/constants.mjs @@ -0,0 +1,264 @@ +/** + * @typedef {string} UpdateType - a type of push notification as defined by + * the [Google Drive API docs](https://bit.ly/3wcsY2X) + */ + +/** + * A new channel was successfully created. You can expect to start receiving + * notifications for it. + * + * @type {UpdateType} + */ +const GOOGLE_DRIVE_NOTIFICATION_SYNC = "sync"; + +/** + * A new resource was created or shared + * + * @type {UpdateType} + */ +const GOOGLE_DRIVE_NOTIFICATION_ADD = "add"; + +/** + * An existing resource was deleted or unshared + * + * @type {UpdateType} + */ +const GOOGLE_DRIVE_NOTIFICATION_REMOVE = "remove"; + +/** + * One or more properties (metadata) of a resource have been updated + * + * @type {UpdateType} + */ +const GOOGLE_DRIVE_NOTIFICATION_UPDATE = "update"; + +/** + * A resource has been moved to the trash + * + * @type {UpdateType} + */ +const GOOGLE_DRIVE_NOTIFICATION_TRASH = "trash"; + +/** + * A resource has been removed from the trash + * + * @type {UpdateType} + */ +const GOOGLE_DRIVE_NOTIFICATION_UNTRASH = "untrash"; + +/** + * One or more new changelog items have been added + * + * @type {UpdateType} + */ +const GOOGLE_DRIVE_NOTIFICATION_CHANGE = "change"; + +/** + * All the available Google Drive update types + * @type {UpdateType[]} + */ +const GOOGLE_DRIVE_UPDATE_TYPES = [ + GOOGLE_DRIVE_NOTIFICATION_SYNC, + GOOGLE_DRIVE_NOTIFICATION_ADD, + GOOGLE_DRIVE_NOTIFICATION_REMOVE, + GOOGLE_DRIVE_NOTIFICATION_UPDATE, + GOOGLE_DRIVE_NOTIFICATION_TRASH, + GOOGLE_DRIVE_NOTIFICATION_UNTRASH, + GOOGLE_DRIVE_NOTIFICATION_CHANGE, +]; +const GOOGLE_DRIVE_UPDATE_TYPE_OPTIONS = [ + { + label: `'${GOOGLE_DRIVE_NOTIFICATION_SYNC}' - A channel was successfully created. You can expect to start receiving notifications for it.`, + value: GOOGLE_DRIVE_NOTIFICATION_SYNC, + }, + { + label: `'${GOOGLE_DRIVE_NOTIFICATION_ADD}' - A resource was created or shared.`, + value: GOOGLE_DRIVE_NOTIFICATION_ADD, + }, + { + label: `'${GOOGLE_DRIVE_NOTIFICATION_REMOVE}' - An existing resource was deleted or unshared.`, + value: GOOGLE_DRIVE_NOTIFICATION_REMOVE, + }, + { + label: `'${GOOGLE_DRIVE_NOTIFICATION_UPDATE}' - One or more properties (metadata) of a resource have been updated.`, + value: GOOGLE_DRIVE_NOTIFICATION_UPDATE, + }, + { + label: `'${GOOGLE_DRIVE_NOTIFICATION_TRASH}' - A resource has been moved to the trash.`, + value: GOOGLE_DRIVE_NOTIFICATION_TRASH, + }, + { + label: `'${GOOGLE_DRIVE_NOTIFICATION_UNTRASH}' - A resource has been removed from the trash.`, + value: GOOGLE_DRIVE_NOTIFICATION_UNTRASH, + }, + { + label: `'${GOOGLE_DRIVE_NOTIFICATION_CHANGE}' - One or more changelog items have been added.`, + value: GOOGLE_DRIVE_NOTIFICATION_CHANGE, + }, +]; + +/** + * This is a custom string value to represent the 'My Drive' Google Drive, which + * is represented as `null` by the Google Drive API. In order to simplify the + * code by avoiding null values, we assign this special value to the 'My Drive' + * drive. + */ +const MY_DRIVE_VALUE = "My Drive"; + +/** + * This is a legacy value for the `MY_DRIVE_VALUE` constant, supporting workflow configurations + * using this value. + */ +const LEGACY_MY_DRIVE_VALUE = "myDrive"; + +/** + * The maximum amount of time a subscription can be active without expiring is + * 24 hours. In order to minimize subscription renewals (which involve the + * execution of an event source) we set the expiration of subscriptions to its + * maximum allowed value. + * + * More information can be found in the API docs: + * https://developers.google.com/drive/api/v3/push#optional-properties + */ +const WEBHOOK_SUBSCRIPTION_EXPIRATION_TIME_MILLISECONDS = 24 * 60 * 60 * 1000; + +/** + * The default time interval between webhook subscription renewals. Since + * subscriptions expire after 24 hours at most, we set this time to 95% of this + * time window by default to make sure the event sources don't miss any events + * due to an expired subscription not being renewed on time. + * + * More information can be found in the API docs: + * https://developers.google.com/drive/api/v3/push#optional-properties + */ +const WEBHOOK_SUBSCRIPTION_RENEWAL_SECONDS = + (WEBHOOK_SUBSCRIPTION_EXPIRATION_TIME_MILLISECONDS * 0.95) / 1000; + +/** + * The maximum number of path segments to include in an option label for a prop whose value is a + * file ID. To make sure the file name is displayed in the option label in the UI, we truncate paths + * with more than this many path segments. + */ +const MAX_FILE_OPTION_PATH_SEGMENTS = 3; + +/** + * The MIME type prefix of Google Drive MIME types as defined by the [Google + * Drive API docs](https://developers.google.com/drive/api/v3/mime-types) + */ +const GOOGLE_DRIVE_MIME_TYPE_PREFIX = "application/vnd.google-apps"; + +/** + * The MIME type of Google Drive folders as defined by the [Google Drive API + * docs](https://developers.google.com/drive/api/v3/mime-types) + */ +const GOOGLE_DRIVE_FOLDER_MIME_TYPE = "application/vnd.google-apps.folder"; + +const GOOGLE_DRIVE_ROLE_OWNER = "owner"; +const GOOGLE_DRIVE_ROLE_ORGANIZER = "organizer"; +const GOOGLE_DRIVE_ROLE_FILEORGANIZER = "fileOrganizer"; +const GOOGLE_DRIVE_ROLE_WRITER = "writer"; +const GOOGLE_DRIVE_ROLE_COMMENTER = "commenter"; +const GOOGLE_DRIVE_ROLE_READER = "reader"; +/** + * All of the available Google Drive roles granted by a permission as defined by the [Google + * Drive API docs](https://developers.google.com/drive/api/v3/reference/permissions) + */ + +const GOOGLE_DRIVE_ROLE_OPTIONS = [ + { + label: "Writer - Can make changes, accept or reject suggestions, and share the file with others.", + value: GOOGLE_DRIVE_ROLE_WRITER, + }, + { + label: "Commenter - Can make comments and suggestions, but can't change or share the file with others.", + value: GOOGLE_DRIVE_ROLE_COMMENTER, + }, + { + label: "Viewer - Can access, but can't change or share the file with others.", + value: GOOGLE_DRIVE_ROLE_READER, + }, +]; + +const GOOGLE_DRIVE_ROLE_OPTION_FILEORGANIZER = { + label: "(Advanced) Content Manager - add, edit, move, delete and share content", + value: GOOGLE_DRIVE_ROLE_FILEORGANIZER, +}; +const GOOGLE_DRIVE_ROLE_OPTION_OWNER = { + label: "(Advanced) File Owner - this will transfer ownership of the file.", + value: GOOGLE_DRIVE_ROLE_OWNER, +}; + +const GOOGLE_DRIVE_GRANTEE_USER = "user"; +const GOOGLE_DRIVE_GRANTEE_GROUP = "group"; +const GOOGLE_DRIVE_GRANTEE_DOMAIN = "domain"; +const GOOGLE_DRIVE_GRANTEE_ANYONE = "anyone"; +/** + * All of the available Google Drive grantee types as defined by the [Google Drive API + * docs](https://developers.google.com/drive/api/v3/reference/permissions) + */ +const GOOGLE_DRIVE_GRANTEE_TYPES = [ + GOOGLE_DRIVE_GRANTEE_USER, + GOOGLE_DRIVE_GRANTEE_GROUP, + GOOGLE_DRIVE_GRANTEE_DOMAIN, + GOOGLE_DRIVE_GRANTEE_ANYONE, +]; + +export const GOOGLE_DRIVE_UPLOAD_TYPE_MEDIA = "media"; +export const GOOGLE_DRIVE_UPLOAD_TYPE_RESUMABLE = "resumable"; +export const GOOGLE_DRIVE_UPLOAD_TYPE_MULTIPART = "multipart"; +const GOOGLE_DRIVE_UPLOAD_TYPES = [ + GOOGLE_DRIVE_UPLOAD_TYPE_MEDIA, + GOOGLE_DRIVE_UPLOAD_TYPE_RESUMABLE, + GOOGLE_DRIVE_UPLOAD_TYPE_MULTIPART, +]; +const GOOGLE_DRIVE_UPLOAD_TYPE_OPTIONS = [ + { + label: "Simple upload. Upload the media only, without any metadata.", + value: GOOGLE_DRIVE_UPLOAD_TYPE_MEDIA, + }, + { + label: "Resumable upload. Upload the file in a resumable fashion, using a series of at least two requests where the first request includes the metadata.", + value: GOOGLE_DRIVE_UPLOAD_TYPE_RESUMABLE, + }, + { + label: "Multipart upload. Upload both the media and its metadata, in a single request.", + value: GOOGLE_DRIVE_UPLOAD_TYPE_MULTIPART, + }, +]; + +export { + GOOGLE_DRIVE_NOTIFICATION_SYNC, + GOOGLE_DRIVE_NOTIFICATION_ADD, + GOOGLE_DRIVE_NOTIFICATION_REMOVE, + GOOGLE_DRIVE_NOTIFICATION_UPDATE, + GOOGLE_DRIVE_NOTIFICATION_TRASH, + GOOGLE_DRIVE_NOTIFICATION_UNTRASH, + GOOGLE_DRIVE_NOTIFICATION_CHANGE, + GOOGLE_DRIVE_UPDATE_TYPES, + GOOGLE_DRIVE_UPDATE_TYPE_OPTIONS, + MY_DRIVE_VALUE, + LEGACY_MY_DRIVE_VALUE, + WEBHOOK_SUBSCRIPTION_EXPIRATION_TIME_MILLISECONDS, + WEBHOOK_SUBSCRIPTION_RENEWAL_SECONDS, + MAX_FILE_OPTION_PATH_SEGMENTS, + GOOGLE_DRIVE_MIME_TYPE_PREFIX, + GOOGLE_DRIVE_FOLDER_MIME_TYPE, + GOOGLE_DRIVE_UPLOAD_TYPES, + GOOGLE_DRIVE_UPLOAD_TYPE_OPTIONS, + // Google Drive Roles + GOOGLE_DRIVE_ROLE_OWNER, + GOOGLE_DRIVE_ROLE_ORGANIZER, + GOOGLE_DRIVE_ROLE_FILEORGANIZER, + GOOGLE_DRIVE_ROLE_WRITER, + GOOGLE_DRIVE_ROLE_COMMENTER, + GOOGLE_DRIVE_ROLE_READER, + GOOGLE_DRIVE_ROLE_OPTIONS, + GOOGLE_DRIVE_ROLE_OPTION_FILEORGANIZER, + GOOGLE_DRIVE_ROLE_OPTION_OWNER, + // Google Drive Grantee Types + GOOGLE_DRIVE_GRANTEE_USER, + GOOGLE_DRIVE_GRANTEE_GROUP, + GOOGLE_DRIVE_GRANTEE_DOMAIN, + GOOGLE_DRIVE_GRANTEE_ANYONE, + GOOGLE_DRIVE_GRANTEE_TYPES, +}; diff --git a/components/google_drive/common/filePathOrUrl.mjs b/components/google_drive/common/filePathOrUrl.mjs new file mode 100644 index 0000000000000..964f5580d917e --- /dev/null +++ b/components/google_drive/common/filePathOrUrl.mjs @@ -0,0 +1,27 @@ +export const updateType = { + type: "string", + label: "Use File URL or File Path", + description: "Whether to upload a file from a URL or from the `/tmp` folder", + options: [ + "File URL", + "File Path", + ], + reloadProps: true, +}; + +export async function additionalProps(previousProps) { + const { updateType } = this; + + if (updateType === "File URL") { + previousProps.fileUrl.hidden = false; + previousProps.filePath.hidden = true; + } else if (updateType === "File Path") { + previousProps.fileUrl.hidden = true; + previousProps.filePath.hidden = false; + } else { + previousProps.fileUrl.hidden = true; + previousProps.filePath.hidden = true; + } + + return {}; +} diff --git a/components/google_drive/common/utils.mjs b/components/google_drive/common/utils.mjs index bdcc306c67e7e..9ff1989b5737f 100644 --- a/components/google_drive/common/utils.mjs +++ b/components/google_drive/common/utils.mjs @@ -4,7 +4,7 @@ import { MY_DRIVE_VALUE, LEGACY_MY_DRIVE_VALUE, MAX_FILE_OPTION_PATH_SEGMENTS, -} from "../constants.mjs"; +} from "../common/constants.mjs"; /** * Returns whether the specified drive ID corresponds to the authenticated diff --git a/components/google_drive/constants.mjs b/components/google_drive/constants.mjs deleted file mode 100644 index ccbe3885ade95..0000000000000 --- a/components/google_drive/constants.mjs +++ /dev/null @@ -1,200 +0,0 @@ -/** - * @typedef {string} UpdateType - a type of push notification as defined by - * the [Google Drive API docs](https://bit.ly/3wcsY2X) - */ - -/** - * A new channel was successfully created. You can expect to start receiving - * notifications for it. - * - * @type {UpdateType} - */ -const GOOGLE_DRIVE_NOTIFICATION_SYNC = "sync"; - -/** - * A new resource was created or shared - * - * @type {UpdateType} - */ -const GOOGLE_DRIVE_NOTIFICATION_ADD = "add"; - -/** - * An existing resource was deleted or unshared - * - * @type {UpdateType} - */ -const GOOGLE_DRIVE_NOTIFICATION_REMOVE = "remove"; - -/** - * One or more properties (metadata) of a resource have been updated - * - * @type {UpdateType} - */ -const GOOGLE_DRIVE_NOTIFICATION_UPDATE = "update"; - -/** - * A resource has been moved to the trash - * - * @type {UpdateType} - */ -const GOOGLE_DRIVE_NOTIFICATION_TRASH = "trash"; - -/** - * A resource has been removed from the trash - * - * @type {UpdateType} - */ -const GOOGLE_DRIVE_NOTIFICATION_UNTRASH = "untrash"; - -/** - * One or more new changelog items have been added - * - * @type {UpdateType} - */ -const GOOGLE_DRIVE_NOTIFICATION_CHANGE = "change"; - -/** - * All the available Google Drive update types - * @type {UpdateType[]} - */ -const GOOGLE_DRIVE_UPDATE_TYPES = [ - GOOGLE_DRIVE_NOTIFICATION_SYNC, - GOOGLE_DRIVE_NOTIFICATION_ADD, - GOOGLE_DRIVE_NOTIFICATION_REMOVE, - GOOGLE_DRIVE_NOTIFICATION_UPDATE, - GOOGLE_DRIVE_NOTIFICATION_TRASH, - GOOGLE_DRIVE_NOTIFICATION_UNTRASH, - GOOGLE_DRIVE_NOTIFICATION_CHANGE, -]; - -/** - * This is a custom string value to represent the 'My Drive' Google Drive, which - * is represented as `null` by the Google Drive API. In order to simplify the - * code by avoiding null values, we assign this special value to the 'My Drive' - * drive. - */ -const MY_DRIVE_VALUE = "My Drive"; - -/** - * This is a legacy value for the `MY_DRIVE_VALUE` constant, supporting workflow configurations - * using this value. - */ -const LEGACY_MY_DRIVE_VALUE = "myDrive"; - -/** - * The maximum amount of time a subscription can be active without expiring is - * 24 hours. In order to minimize subscription renewals (which involve the - * execution of an event source) we set the expiration of subscriptions to its - * maximum allowed value. - * - * More information can be found in the API docs: - * https://developers.google.com/drive/api/v3/push#optional-properties - */ -const WEBHOOK_SUBSCRIPTION_EXPIRATION_TIME_MILLISECONDS = 24 * 60 * 60 * 1000; - -/** - * The default time interval between webhook subscription renewals. Since - * subscriptions expire after 24 hours at most, we set this time to 95% of this - * time window by default to make sure the event sources don't miss any events - * due to an expired subscription not being renewed on time. - * - * More information can be found in the API docs: - * https://developers.google.com/drive/api/v3/push#optional-properties - */ -const WEBHOOK_SUBSCRIPTION_RENEWAL_SECONDS = - (WEBHOOK_SUBSCRIPTION_EXPIRATION_TIME_MILLISECONDS * 0.95) / 1000; - -/** - * The maximum number of path segments to include in an option label for a prop whose value is a - * file ID. To make sure the file name is displayed in the option label in the UI, we truncate paths - * with more than this many path segments. - */ -const MAX_FILE_OPTION_PATH_SEGMENTS = 3; - -/** - * The MIME type prefix of Google Drive MIME types as defined by the [Google - * Drive API docs](https://developers.google.com/drive/api/v3/mime-types) - */ -const GOOGLE_DRIVE_MIME_TYPE_PREFIX = "application/vnd.google-apps"; - -/** - * The MIME type of Google Drive folders as defined by the [Google Drive API - * docs](https://developers.google.com/drive/api/v3/mime-types) - */ -const GOOGLE_DRIVE_FOLDER_MIME_TYPE = "application/vnd.google-apps.folder"; - -const GOOGLE_DRIVE_ROLE_OWNER = "owner"; -const GOOGLE_DRIVE_ROLE_ORGANIZER = "organizer"; -const GOOGLE_DRIVE_ROLE_FILEORGANIZER = "fileOrganizer"; -const GOOGLE_DRIVE_ROLE_WRITER = "writer"; -const GOOGLE_DRIVE_ROLE_COMMENTER = "commenter"; -const GOOGLE_DRIVE_ROLE_READER = "reader"; -/** - * All of the available Google Drive roles granted by a permission as defined by the [Google - * Drive API docs](https://developers.google.com/drive/api/v3/reference/permissions) - */ -const GOOGLE_DRIVE_ROLES = [ - GOOGLE_DRIVE_ROLE_OWNER, - GOOGLE_DRIVE_ROLE_ORGANIZER, - GOOGLE_DRIVE_ROLE_FILEORGANIZER, - GOOGLE_DRIVE_ROLE_WRITER, - GOOGLE_DRIVE_ROLE_COMMENTER, - GOOGLE_DRIVE_ROLE_READER, -]; - -const GOOGLE_DRIVE_GRANTEE_USER = "user"; -const GOOGLE_DRIVE_GRANTEE_GROUP = "group"; -const GOOGLE_DRIVE_GRANTEE_DOMAIN = "domain"; -const GOOGLE_DRIVE_GRANTEE_ANYONE = "anyone"; -/** - * All of the available Google Drive grantee types as defined by the [Google Drive API - * docs](https://developers.google.com/drive/api/v3/reference/permissions) - */ -const GOOGLE_DRIVE_GRANTEE_TYPES = [ - GOOGLE_DRIVE_GRANTEE_USER, - GOOGLE_DRIVE_GRANTEE_GROUP, - GOOGLE_DRIVE_GRANTEE_DOMAIN, - GOOGLE_DRIVE_GRANTEE_ANYONE, -]; - -export const GOOGLE_DRIVE_UPLOAD_TYPE_MEDIA = "media"; -export const GOOGLE_DRIVE_UPLOAD_TYPE_RESUMABLE = "resumable"; -export const GOOGLE_DRIVE_UPLOAD_TYPE_MULTIPART = "multipart"; -const GOOGLE_DRIVE_UPLOAD_TYPES = [ - GOOGLE_DRIVE_UPLOAD_TYPE_MEDIA, - GOOGLE_DRIVE_UPLOAD_TYPE_RESUMABLE, - GOOGLE_DRIVE_UPLOAD_TYPE_MULTIPART, -]; - -export { - GOOGLE_DRIVE_NOTIFICATION_SYNC, - GOOGLE_DRIVE_NOTIFICATION_ADD, - GOOGLE_DRIVE_NOTIFICATION_REMOVE, - GOOGLE_DRIVE_NOTIFICATION_UPDATE, - GOOGLE_DRIVE_NOTIFICATION_TRASH, - GOOGLE_DRIVE_NOTIFICATION_UNTRASH, - GOOGLE_DRIVE_NOTIFICATION_CHANGE, - GOOGLE_DRIVE_UPDATE_TYPES, - MY_DRIVE_VALUE, - LEGACY_MY_DRIVE_VALUE, - WEBHOOK_SUBSCRIPTION_EXPIRATION_TIME_MILLISECONDS, - WEBHOOK_SUBSCRIPTION_RENEWAL_SECONDS, - MAX_FILE_OPTION_PATH_SEGMENTS, - GOOGLE_DRIVE_MIME_TYPE_PREFIX, - GOOGLE_DRIVE_FOLDER_MIME_TYPE, - GOOGLE_DRIVE_UPLOAD_TYPES, - // Google Drive Roles - GOOGLE_DRIVE_ROLE_OWNER, - GOOGLE_DRIVE_ROLE_ORGANIZER, - GOOGLE_DRIVE_ROLE_FILEORGANIZER, - GOOGLE_DRIVE_ROLE_WRITER, - GOOGLE_DRIVE_ROLE_COMMENTER, - GOOGLE_DRIVE_ROLE_READER, - GOOGLE_DRIVE_ROLES, - // Google Drive Grantee Types - GOOGLE_DRIVE_GRANTEE_USER, - GOOGLE_DRIVE_GRANTEE_GROUP, - GOOGLE_DRIVE_GRANTEE_DOMAIN, - GOOGLE_DRIVE_GRANTEE_ANYONE, - GOOGLE_DRIVE_GRANTEE_TYPES, -}; diff --git a/components/google_drive/google_drive.app.mjs b/components/google_drive/google_drive.app.mjs index 729b22550a386..f36a9b4ec744c 100644 --- a/components/google_drive/google_drive.app.mjs +++ b/components/google_drive/google_drive.app.mjs @@ -10,12 +10,10 @@ import { MY_DRIVE_VALUE, WEBHOOK_SUBSCRIPTION_EXPIRATION_TIME_MILLISECONDS, GOOGLE_DRIVE_FOLDER_MIME_TYPE, - GOOGLE_DRIVE_ROLES, GOOGLE_DRIVE_GRANTEE_TYPES, - GOOGLE_DRIVE_GRANTEE_ANYONE, - GOOGLE_DRIVE_ROLE_READER, - GOOGLE_DRIVE_UPLOAD_TYPES, -} from "./constants.mjs"; + GOOGLE_DRIVE_UPLOAD_TYPE_OPTIONS, + GOOGLE_DRIVE_UPDATE_TYPE_OPTIONS, +} from "./common/constants.mjs"; import googleMimeTypes from "./actions/google-mime-types.mjs"; import { @@ -42,6 +40,31 @@ export default { return this._listDriveOptions(nextPageToken); }, }, + sharedDrive: { + type: "string", + label: "Shared Drive", + description: "Select a [Shared Drive](https://support.google.com/a/users/answer/9310351) or leave blank to retrieve all available shared drives.", + optional: true, + async options({ prevContext }) { + const { nextPageToken } = prevContext; + return this._listDriveOptions(nextPageToken, false); + }, + }, + themeId: { + type: "string", + label: "Theme ID", + description: "The theme from which the background image and color will be set. Cannot be set if `Color` or `Background Image Link` are used.", + optional: true, + async options() { + const { driveThemes } = await this.getAbout("driveThemes"); + return driveThemes?.map(({ + id, colorRgb, + }) => ({ + label: `${id} (${colorRgb})`, + value: id, + })); + }, + }, folderId: { type: "string", label: "Folder", @@ -51,6 +74,9 @@ export default { drive, baseOpts = { q: `mimeType = '${GOOGLE_DRIVE_FOLDER_MIME_TYPE}'`, + sharedWithMe: true, + supportsAllDrives: true, + includeItemsFromAllDrives: true, }, }) { const { nextPageToken } = prevContext; @@ -117,16 +143,15 @@ export default { type: "string[]", label: "Types of updates", description: `The types of updates you want to watch for on these files. - [See Google's docs] - (https://developers.google.com/drive/api/v3/push#understanding-drive-api-notification-events).`, + [See Google's docs](https://developers.google.com/drive/api/v3/push#understanding-drive-api-notification-events).`, default: GOOGLE_DRIVE_UPDATE_TYPES, - options: GOOGLE_DRIVE_UPDATE_TYPES, + options: GOOGLE_DRIVE_UPDATE_TYPE_OPTIONS, }, watchForPropertiesChanges: { type: "boolean", label: "Watch for changes to file properties", - description: `Watch for changes to [file properties](https://developers.google.com/drive/api/v3/properties) - in addition to changes to content. **Defaults to \`false\`, watching for only changes to content**.`, + description: `Watch for changes to [custom file properties](https://developers.google.com/drive/api/v3/properties) + in addition to changes to content. **Defaults to \`false\`, watching only for changes to content**.`, optional: true, default: false, }, @@ -158,7 +183,13 @@ export default { fileNameSearchTerm: { type: "string", label: "Search Name", - description: "Enter the name of a file to search for.", + description: "Search for a file by name (equivalent to the query `name contains [value]`).", + optional: true, + }, + searchQuery: { + type: "string", + label: "Search Query", + description: "Search for a file with a query. [See the documentation](https://developers.google.com/drive/api/guides/ref-search-terms) for more information. If specified, `Search Name` will be ignored.", optional: true, }, mimeType: { @@ -182,14 +213,8 @@ export default { uploadType: { type: "string", label: "Upload Type", - description: `The type of upload request to the /upload URI. If you are uploading data - (using an /upload URI), this field is required. If you are creating a metadata-only file, - this field is not required. - media - Simple upload. Upload the media only, without any metadata. - multipart - Multipart upload. Upload both the media and its metadata, in a single request. - resumable - Resumable upload. Upload the file in a resumable fashion, using a series of - at least two requests where the first request includes the metadata.`, - options: GOOGLE_DRIVE_UPLOAD_TYPES, + description: "The type of upload request to the /upload URI. Required if you are uploading data, but not if are creating a metadata-only file. [See the documentation](https://developers.google.com/drive/api/reference/rest/v2/files/update#path-parameters) for more information.", + options: GOOGLE_DRIVE_UPLOAD_TYPE_OPTIONS, }, useDomainAdminAccess: { type: "boolean", @@ -198,37 +223,13 @@ export default { optional: true, default: false, }, - role: { - type: "string", - label: "Role", - description: "The role granted by this permission", - optional: true, - default: GOOGLE_DRIVE_ROLE_READER, - options: GOOGLE_DRIVE_ROLES, - }, type: { type: "string", label: "Type", description: - "The type of the grantee. If **Type** is `user` or `group`, you must provide an **Email Address** for the user or group. When **Type** is `domain`, you must provide a `Domain`. Sharing with a domain is only valid for G Suite users.", - optional: true, - default: GOOGLE_DRIVE_GRANTEE_ANYONE, + "The type of the grantee. Sharing with a domain is only valid for G Suite users.", options: GOOGLE_DRIVE_GRANTEE_TYPES, }, - domain: { - type: "string", - label: "Domain", - description: - "The domain of the G Suite organization to which this permission refers if **Type** is `domain` (e.g., `yourcomapany.com`)", - optional: true, - }, - emailAddress: { - type: "string", - label: "Email Address", - description: - "The email address of the user or group to which this permission refers if **Type** is `user` or `group`", - optional: true, - }, ocrLanguage: { type: "string", label: "OCR Language", @@ -456,7 +457,7 @@ export default { pageToken = nextPageToken; } }, - async _listDriveOptions(pageToken) { + async _listDriveOptions(pageToken, myDrive = true) { const { drives, nextPageToken, @@ -467,14 +468,14 @@ export default { // only do this during the first page of options (i.e. when `pageToken` is // undefined). const options = - pageToken !== undefined - ? [] - : [ + myDrive && pageToken === undefined + ? [ { label: "My Drive", value: MY_DRIVE_VALUE, }, - ]; + ] + : []; for (const d of drives) { options.push({ label: d.name, @@ -816,8 +817,6 @@ export default { }; }, async activateFileHook(channelID, url, fileId) { - channelID = channelID || uuid(); - const { expiration, resourceId, @@ -830,7 +829,6 @@ export default { return { expiration, resourceId, - channelID, }; }, async deactivateHook(channelID, resourceId) { @@ -851,16 +849,16 @@ export default { await this.stopNotifications(channelID, resourceId); }, async renewSubscription(drive, subscription, url, channelID, pageToken) { - const newChannelID = channelID || uuid(); const driveId = this.getDriveId(drive); const newPageToken = pageToken || (await this.getPageToken(driveId)); const { expiration, resourceId, + newChannelID, } = await this.checkResubscription( subscription, - newChannelID, + channelID, newPageToken, url, drive, @@ -880,6 +878,7 @@ export default { endpoint, drive, ) { + const newChannelID = uuid(); const driveId = this.getDriveId(drive); if (subscription && subscription.resourceId) { console.log( @@ -893,7 +892,7 @@ export default { expiration, resourceId, } = await this.watchDrive( - channelID, + newChannelID, endpoint, pageToken, driveId, @@ -901,15 +900,21 @@ export default { return { expiration, resourceId, + newChannelID, }; }, - async renewFileSubscription(subscription, url, channelID, fileId, nextRunTimestamp) { + async renewFileSubscription( + subscription, + url, + channelID, + newChannelID, + fileId, + nextRunTimestamp, + ) { if (nextRunTimestamp && subscription?.expiration < nextRunTimestamp) { return subscription; } - const newChannelID = channelID || uuid(); - if (subscription?.resourceId) { console.log( `Notifications for resource ${subscription.resourceId} are expiring at ${subscription.expiration}. Renewing`, @@ -923,13 +928,12 @@ export default { expiration, resourceId, } = await this.watchFile( - channelID, + newChannelID, url, fileId, ); return { - newChannelID, expiration, resourceId, }; @@ -1263,7 +1267,7 @@ export default { */ async createPermission(fileId, opts = {}) { const { - role = "reader", + role, type, domain, emailAddress, diff --git a/components/google_drive/package.json b/components/google_drive/package.json index 6baa12698c69c..24226df14c896 100644 --- a/components/google_drive/package.json +++ b/components/google_drive/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/google_drive", - "version": "0.6.16", + "version": "0.8.10", "description": "Pipedream Google_drive Components", "main": "google_drive.app.mjs", "keywords": [ @@ -13,6 +13,8 @@ "@googleapis/drive": "^2.3.0", "@pipedream/platform": "^1.4.0", "cron-parser": "^4.9.0", + "google-docs-mustaches": "^1.2.2", + "got": "13.0.0", "lodash": "^4.17.21", "mime-db": "^1.51.0", "uuid": "^8.3.2" diff --git a/components/google_drive/sources/changes-to-specific-files-shared-drive/changes-to-specific-files-shared-drive.mjs b/components/google_drive/sources/changes-to-specific-files-shared-drive/changes-to-specific-files-shared-drive.mjs index d129a94e54116..e69ea6d2b0dc9 100644 --- a/components/google_drive/sources/changes-to-specific-files-shared-drive/changes-to-specific-files-shared-drive.mjs +++ b/components/google_drive/sources/changes-to-specific-files-shared-drive/changes-to-specific-files-shared-drive.mjs @@ -14,7 +14,8 @@ import { GOOGLE_DRIVE_NOTIFICATION_CHANGE, GOOGLE_DRIVE_NOTIFICATION_ADD, GOOGLE_DRIVE_NOTIFICATION_UPDATE, -} from "../../constants.mjs"; +} from "../../common/constants.mjs"; +import commonDedupeChanges from "../common-dedupe-changes.mjs"; /** * This source uses the Google Drive API's @@ -25,8 +26,8 @@ export default { ...common, key: "google_drive-changes-to-specific-files-shared-drive", name: "Changes to Specific Files (Shared Drive)", - description: "Watches for changes to specific files in a shared drive, emitting an event any time a change is made to one of those files", - version: "0.1.3", + description: "Watches for changes to specific files in a shared drive, emitting an event when a change is made to one of those files", + version: "0.2.4", type: "source", // Dedupe events based on the "x-goog-message-number" header for the target channel: // https://developers.google.com/drive/api/v3/push#making-watch-requests @@ -37,13 +38,12 @@ export default { type: "string[]", label: "Files", description: "The files you want to watch for changes.", - optional: true, - default: [], options({ prevContext }) { const { nextPageToken } = prevContext; return this.googleDrive.listFilesOptions(nextPageToken, this.getListFilesOpts()); }, }, + ...commonDedupeChanges.props, }, hooks: { async deploy() { @@ -54,6 +54,7 @@ export default { const args = this.getListFilesOpts({ q: `mimeType != "application/vnd.google-apps.folder" and modifiedTime > "${timeString}" and trashed = false`, fields: "files", + pageSize: 5, }); const { files } = await this.googleDrive.listFilesInPage(null, args); @@ -96,7 +97,9 @@ export default { }, getChanges(headers) { if (!headers) { - return; + return { + change: { }, + }; } return { change: { @@ -119,7 +122,9 @@ export default { console.log(`Processing ${changedFiles.length} changed files`); console.log(`Changed files: ${JSON.stringify(changedFiles, null, 2)}!!!`); console.log(`Files: ${this.files}!!!`); - for (const file of changedFiles) { + + const filteredFiles = this.checkMinimumInterval(changedFiles); + for (const file of filteredFiles) { if (!this.isFileRelevant(file)) { console.log(`Skipping event for irrelevant file ${file.id}`); continue; diff --git a/components/google_drive/sources/changes-to-specific-files/changes-to-specific-files.mjs b/components/google_drive/sources/changes-to-specific-files/changes-to-specific-files.mjs index 0d38746f8176e..0939d3d9f591d 100644 --- a/components/google_drive/sources/changes-to-specific-files/changes-to-specific-files.mjs +++ b/components/google_drive/sources/changes-to-specific-files/changes-to-specific-files.mjs @@ -2,9 +2,7 @@ import cronParser from "cron-parser"; import sampleEmit from "./test-event.mjs"; import includes from "lodash/includes.js"; import { v4 as uuid } from "uuid"; - -import { MY_DRIVE_VALUE } from "../../constants.mjs"; - +import { MY_DRIVE_VALUE } from "../../common/constants.mjs"; import changesToSpecificFiles from "../changes-to-specific-files-shared-drive/changes-to-specific-files-shared-drive.mjs"; /** @@ -16,8 +14,8 @@ export default { ...changesToSpecificFiles, key: "google_drive-changes-to-specific-files", name: "Changes to Specific Files", - description: "Watches for changes to specific files, emitting an event any time a change is made to one of those files. To watch for changes to [shared drive](https://support.google.com/a/users/answer/9310351) files, use the **Changes to Specific Files (Shared Drive)** source instead.", - version: "0.1.3", + description: "Watches for changes to specific files, emitting an event when a change is made to one of those files. To watch for changes to [shared drive](https://support.google.com/a/users/answer/9310351) files, use the **Changes to Specific Files (Shared Drive)** source instead.", + version: "0.2.4", type: "source", // Dedupe events based on the "x-goog-message-number" header for the target channel: // https://developers.google.com/drive/api/v3/push#making-watch-requests @@ -48,7 +46,7 @@ export default { // You can pass the same channel ID in watch requests for multiple files, so // our channel ID is fixed for this component to simplify the state we have to // keep track of. - const channelID = this._getChannelID() || uuid(); + const channelID = uuid(); // Subscriptions are keyed on Google's resourceID, "an opaque value that // identifies the watched resource". This value is included in request @@ -116,10 +114,9 @@ export default { } }, async renewFileSubscriptions(event) { - // Assume subscription & channelID may all be undefined at - // this point Handle their absence appropriately. const subscriptions = this._getSubscriptions() || {}; - const channelID = this._getChannelID() || uuid(); + const channelID = this._getChannelID(); + const newChannelID = uuid(); const nextRunTimestamp = this._getNextTimerEventTimestamp(event); @@ -140,6 +137,7 @@ export default { subscription, this.http.endpoint, channelID, + newChannelID, fileID, nextRunTimestamp, ); @@ -149,7 +147,7 @@ export default { }; } this._setSubscriptions(subscriptions); - this._setChannelID(channelID); + this._setChannelID(newChannelID); }, }, async run(event) { @@ -222,7 +220,14 @@ export default { return; } - this.processChange(file, headers); + const [ + checkedFile, + ] = this.checkMinimumInterval([ + file, + ]); + if (checkedFile) { + this.processChange(file, headers); + } }, sampleEmit, }; diff --git a/components/google_drive/sources/common-dedupe-changes.mjs b/components/google_drive/sources/common-dedupe-changes.mjs new file mode 100644 index 0000000000000..dea635457bda8 --- /dev/null +++ b/components/google_drive/sources/common-dedupe-changes.mjs @@ -0,0 +1,53 @@ +export default { + props: { + intervalAlert: { + type: "alert", + alertType: "info", + content: `This source can emit many events in quick succession while a file is being edited. By default, it will not emit another event for the same file for at least 1 minute. +\\ +You can change or disable this minimum interval using the prop \`Minimum Interval Per File\`.`, + }, + perFileInterval: { + type: "integer", + label: "Minimum Interval Per File", + description: "How many minutes to wait until the same file can emit another event.\n\nIf set to `0`, this interval is disabled and all events will be emitted.", + min: 0, + max: 60, + default: 1, + optional: true, + }, + }, + methods: { + _getFileIntervals() { + return this.db.get("fileIntervals") ?? {}; + }, + _setFileIntervals(value) { + this.db.set("fileIntervals", value); + }, + checkMinimumInterval(files) { + const interval = this.perFileInterval; + if (!interval) return files; + + const now = Date.now(); + const minTimestamp = now - (interval * 1000 * 60); + + const savedData = this._getFileIntervals(); + Object.entries(savedData).forEach(([ + key, + value, + ]) => { + if (value < minTimestamp) delete savedData[key]; + }); + + const filteredFiles = files.filter(({ id }) => { + const exists = !!savedData[id]; + if (!exists) { + savedData[id] = now; + } + return !exists; + }); + this._setFileIntervals(savedData); + return filteredFiles; + }, + }, +}; diff --git a/components/google_drive/sources/common-webhook.mjs b/components/google_drive/sources/common-webhook.mjs index 361556c10ef7c..0448ab485f448 100644 --- a/components/google_drive/sources/common-webhook.mjs +++ b/components/google_drive/sources/common-webhook.mjs @@ -2,8 +2,9 @@ import includes from "lodash/includes.js"; import { v4 as uuid } from "uuid"; import googleDrive from "../google_drive.app.mjs"; -import { WEBHOOK_SUBSCRIPTION_RENEWAL_SECONDS } from "../constants.mjs"; +import { WEBHOOK_SUBSCRIPTION_RENEWAL_SECONDS } from "../common/constants.mjs"; import { getListFilesOpts } from "../common/utils.mjs"; +import commonDedupeChanges from "./common-dedupe-changes.mjs"; export default { props: { @@ -18,12 +19,6 @@ export default { description: "Defaults to My Drive. To select a [Shared Drive](https://support.google.com/a/users/answer/9310351) instead, select it from this list.", optional: false, }, - watchForPropertiesChanges: { - propDefinition: [ - googleDrive, - "watchForPropertiesChanges", - ], - }, timer: { label: "Push notification renewal schedule", description: @@ -32,6 +27,7 @@ export default { static: { intervalSeconds: WEBHOOK_SUBSCRIPTION_RENEWAL_SECONDS, }, + hidden: true, }, }, hooks: { @@ -73,6 +69,7 @@ export default { }, }, methods: { + ...commonDedupeChanges.methods, _getSubscription() { return this.db.get("subscription"); }, diff --git a/components/google_drive/sources/new-files-instant/new-files-instant.mjs b/components/google_drive/sources/new-files-instant/new-files-instant.mjs index 4f866e977bee7..d532cf6b0488f 100644 --- a/components/google_drive/sources/new-files-instant/new-files-instant.mjs +++ b/components/google_drive/sources/new-files-instant/new-files-instant.mjs @@ -3,14 +3,14 @@ import sampleEmit from "./test-event.mjs"; import { GOOGLE_DRIVE_NOTIFICATION_ADD, GOOGLE_DRIVE_NOTIFICATION_CHANGE, -} from "../../constants.mjs"; +} from "../../common/constants.mjs"; export default { ...common, key: "google_drive-new-files-instant", name: "New Files (Instant)", - description: "Emit new event any time a new file is added in your linked Google Drive", - version: "0.1.4", + description: "Emit new event when a new file is added in your linked Google Drive", + version: "0.1.11", type: "source", dedupe: "unique", props: { @@ -19,7 +19,7 @@ export default { type: "string[]", label: "Folders", description: - "(Optional) The folders you want to watch for changes. Leave blank to watch for any new file in the Drive.", + "(Optional) The folders you want to watch. Leave blank to watch for any new file in the Drive.", optional: true, default: [], options({ prevContext }) { @@ -50,7 +50,7 @@ export default { q: `mimeType != "application/vnd.google-apps.folder" and createdTime > "${timeString}" and trashed = false`, orderBy: "createdTime desc", fields: "*", - pageSize: 25, + pageSize: 5, }); const { files } = await this.googleDrive.listFilesInPage(null, args); diff --git a/components/google_drive/sources/new-or-modified-comments/new-or-modified-comments.mjs b/components/google_drive/sources/new-or-modified-comments/new-or-modified-comments.mjs index 5d70495cb9e55..c094923910762 100644 --- a/components/google_drive/sources/new-or-modified-comments/new-or-modified-comments.mjs +++ b/components/google_drive/sources/new-or-modified-comments/new-or-modified-comments.mjs @@ -9,33 +9,39 @@ // 2) A timer that runs on regular intervals, renewing the notification channel as needed import common from "../common-webhook.mjs"; -import { GOOGLE_DRIVE_NOTIFICATION_CHANGE } from "../../constants.mjs"; +import { GOOGLE_DRIVE_NOTIFICATION_CHANGE } from "../../common/constants.mjs"; export default { ...common, key: "google_drive-new-or-modified-comments", - name: "New or Modified Comments", + name: "New or Modified Comments (Instant)", description: - "Emits a new event any time a file comment is added, modified, or deleted in your linked Google Drive", - version: "0.1.4", + "Emit new event when a comment is created or modified in the selected file", + version: "1.0.3", type: "source", // Dedupe events based on the "x-goog-message-number" header for the target channel: // https://developers.google.com/drive/api/v3/push#making-watch-requests dedupe: "unique", + props: { + ...common.props, + fileId: { + propDefinition: [ + common.props.googleDrive, + "fileId", + (c) => ({ + drive: c.drive, + }), + ], + description: "The file to watch for comments", + }, + }, hooks: { async deploy() { - const daysAgo = new Date(); - daysAgo.setDate(daysAgo.getDate() - 30); - const timeString = daysAgo.toISOString(); - - const args = this.getListFilesOpts({ - q: `mimeType != "application/vnd.google-apps.folder" and modifiedTime > "${timeString}" and trashed = false`, - fields: "files", - }); - - const { files } = await this.googleDrive.listFilesInPage(null, args); - - await this.processChanges(files); + await this.processChanges([ + { + id: this.fileId, + }, + ]); }, async activate() { await common.hooks.activate.bind(this)(); @@ -82,7 +88,9 @@ export default { }, getChanges(headers) { if (!headers) { - return; + return { + change: { }, + }; } return { change: { @@ -96,6 +104,9 @@ export default { }, async processChanges(changedFiles, headers) { const changes = this.getChanges(headers); + if (changedFiles?.length) { + changedFiles = changedFiles.filter(({ id }) => id === this.fileId); + } for (const file of changedFiles) { const lastCommentTimeForFile = this._getLastCommentTimeForFile(file.id); @@ -113,7 +124,6 @@ export default { const eventToEmit = { comment, - file, ...changes, }; const meta = this.generateMeta(comment, headers); diff --git a/components/google_drive/sources/new-or-modified-files/new-or-modified-files.mjs b/components/google_drive/sources/new-or-modified-files/new-or-modified-files.mjs index fa982f82effa6..21f5bb4461651 100644 --- a/components/google_drive/sources/new-or-modified-files/new-or-modified-files.mjs +++ b/components/google_drive/sources/new-or-modified-files/new-or-modified-files.mjs @@ -14,14 +14,17 @@ import { GOOGLE_DRIVE_NOTIFICATION_ADD, GOOGLE_DRIVE_NOTIFICATION_CHANGE, GOOGLE_DRIVE_NOTIFICATION_UPDATE, -} from "../../constants.mjs"; +} from "../../common/constants.mjs"; +import commonDedupeChanges from "../common-dedupe-changes.mjs"; + +const { googleDrive } = common.props; export default { ...common, key: "google_drive-new-or-modified-files", - name: "New or Modified Files", - description: "Emit new event any time any file in your linked Google Drive is added, modified, or deleted", - version: "0.2.0", + name: "New or Modified Files (Instant)", + description: "Emit new event when a file in the selected Drive is created, modified or trashed.", + version: "0.3.4", type: "source", // Dedupe events based on the "x-goog-message-number" header for the target channel: // https://developers.google.com/drive/api/v3/push#making-watch-requests @@ -30,9 +33,9 @@ export default { ...common.props, folders: { type: "string[]", - label: "Folders", + label: "Folder(s)", description: - "(Optional) The folders you want to watch for changes. Leave blank to watch for any new file in the Drive.", + "The folder(s) to watch for changes. Leave blank to watch for any new or modified file in the Drive.", optional: true, default: [], options({ prevContext }) { @@ -52,6 +55,13 @@ export default { return this.googleDrive.listFilesOptions(nextPageToken, opts); }, }, + watchForPropertiesChanges: { + propDefinition: [ + googleDrive, + "watchForPropertiesChanges", + ], + }, + ...commonDedupeChanges.props, }, hooks: { async deploy() { @@ -62,6 +72,7 @@ export default { const args = this.getListFilesOpts({ q: `mimeType != "application/vnd.google-apps.folder" and modifiedTime > "${timeString}" and trashed = false`, fields: "files", + pageSize: 5, }); const { files } = await this.googleDrive.listFilesInPage(null, args); @@ -105,7 +116,9 @@ export default { }, async getChanges(headers) { if (!headers) { - return; + return { + change: { }, + }; } const resourceUri = headers["x-goog-resource-uri"]; const metadata = await this.googleDrive.getFileMetadata(`${resourceUri}&fields=*`); @@ -121,7 +134,9 @@ export default { async processChanges(changedFiles, headers) { const changes = await this.getChanges(headers); - for (const file of changedFiles) { + const filteredFiles = this.checkMinimumInterval(changedFiles); + + for (const file of filteredFiles) { file.parents = (await this.googleDrive.getFile(file.id, { fields: "parents", })).parents; diff --git a/components/google_drive/sources/new-or-modified-folders/new-or-modified-folders.mjs b/components/google_drive/sources/new-or-modified-folders/new-or-modified-folders.mjs index 04d2ed7563602..1a6e4ccee51c8 100644 --- a/components/google_drive/sources/new-or-modified-folders/new-or-modified-folders.mjs +++ b/components/google_drive/sources/new-or-modified-folders/new-or-modified-folders.mjs @@ -13,14 +13,14 @@ import { GOOGLE_DRIVE_NOTIFICATION_ADD, GOOGLE_DRIVE_NOTIFICATION_CHANGE, GOOGLE_DRIVE_NOTIFICATION_UPDATE, -} from "../../constants.mjs"; +} from "../../common/constants.mjs"; export default { ...common, key: "google_drive-new-or-modified-folders", - name: "New or Modified Folders", - description: "Emit new event any time any folder in your linked Google Drive is added, modified, or deleted", - version: "0.1.2", + name: "New or Modified Folders (Instant)", + description: "Emit new event when a folder is created or modified in the selected Drive", + version: "0.1.9", type: "source", // Dedupe events based on the "x-goog-message-number" header for the target channel: // https://developers.google.com/drive/api/v3/push#making-watch-requests @@ -34,6 +34,7 @@ export default { const args = this.getListFilesOpts({ q: `mimeType = "application/vnd.google-apps.folder" and modifiedTime > "${timeString}" and trashed = false`, fields: "files(id, mimeType)", + pageSize: 5, }); const { files } = await this.googleDrive.listFilesInPage(null, args); @@ -70,7 +71,9 @@ export default { }, async getChanges(headers) { if (!headers) { - return; + return { + change: { }, + }; } const resourceUri = headers["x-goog-resource-uri"]; const metadata = await this.googleDrive.getFileMetadata(`${resourceUri}&fields=*`); diff --git a/components/google_drive/sources/new-shared-drive/new-shared-drive.mjs b/components/google_drive/sources/new-shared-drive/new-shared-drive.mjs index 1db5b865a5f98..08084b2de4c17 100644 --- a/components/google_drive/sources/new-shared-drive/new-shared-drive.mjs +++ b/components/google_drive/sources/new-shared-drive/new-shared-drive.mjs @@ -5,7 +5,7 @@ export default { key: "google_drive-new-shared-drive", name: "New Shared Drive", description: "Emits a new event any time a shared drive is created.", - version: "0.1.1", + version: "0.1.7", type: "source", dedupe: "unique", props: { diff --git a/components/google_drive/sources/new-spreadsheet/new-spreadsheet.mjs b/components/google_drive/sources/new-spreadsheet/new-spreadsheet.mjs index 38b9dd801462c..461bdf22c3d7f 100644 --- a/components/google_drive/sources/new-spreadsheet/new-spreadsheet.mjs +++ b/components/google_drive/sources/new-spreadsheet/new-spreadsheet.mjs @@ -5,21 +5,25 @@ export default { key: "google_drive-new-spreadsheet", type: "source", name: "New Spreadsheet (Instant)", - description: "Emit new event each time a new spreadsheet is created in a drive.", - version: "0.1.2", + description: "Emit new event when a new spreadsheet is created in a drive.", + version: "0.1.9", props: { googleDrive: newFilesInstant.props.googleDrive, db: newFilesInstant.props.db, http: newFilesInstant.props.http, drive: newFilesInstant.props.drive, timer: newFilesInstant.props.timer, - folders: newFilesInstant.props.folders, + folders: { + ...newFilesInstant.props.folders, + description: + "(Optional) The folders you want to watch. Leave blank to watch for any new spreadsheet in the Drive.", + }, }, hooks: { ...newFilesInstant.hooks, async deploy() { // Emit sample records on the first run - const spreadsheets = await this.getSpreadsheets(10); + const spreadsheets = await this.getSpreadsheets(5); for (const fileInfo of spreadsheets) { const createdTime = Date.parse(fileInfo.createdTime); this.$emit(fileInfo, { diff --git a/components/google_fit_developer_app/README.md b/components/google_fit_developer_app/README.md index cc3cf030f8400..e398051594620 100644 --- a/components/google_fit_developer_app/README.md +++ b/components/google_fit_developer_app/README.md @@ -1,5 +1,5 @@ # Overview -By connecting your personal Google Fit account to Pipedream, you'll be able to incorporate your fitness data into whatever you're building with any of the 1,700+ apps that are available on Pipedream. +Google Fit (Developer App) provides a robust API for accessing and storing a user's health and wellness data collected from various devices and apps. With it, you can read and write different types of fitness data, such as steps, calories burned, and heart rate, enabling the development of personalized health dashboards, proactive fitness reminders, and integrative health reports. Using Pipedream, this data can be ingested and combined with other services to automate health tracking, set goals, and even inform healthcare providers or coaching systems of a user's progress. # Getting Started The Google Fit Developer App in Pipedream can integrate with either a personal Gmail account or a Google workspace email account. Either option involves creating a custom Google App in the Google Cloud Console. This process does not involve any code or special approval by Google. The steps are outlined below: @@ -96,6 +96,14 @@ Google has a [7 day expiration window](https://developers.google.com/identity/pr ![Confirmation of changes](https://res.cloudinary.com/dpenc2lit/image/upload/v1698166716/Screenshot_2023-10-24_at_9.50.18_AM_mndtyc.png) +# Example Use Cases + +- **Daily Health Summary Email**: Combine Google Fit data with Pipedream's email service to send a daily summary of a user's fitness activity. A workflow could fetch the previous day's data, including steps taken, calories burned, and sleep analysis, then format this information and send it via an automated email to encourage consistent fitness tracking and goal setting. + +- **Smart Home Integration for Fitness Reminders**: Integrate Google Fit data with smart home devices using Pipedream. If a user's activity level is lower than a set threshold by a certain time of day, the workflow could trigger a smart home device, like a smart speaker, to remind the user to take a walk or perform their workout routine. + +- **Health Dashboard Sync**: Sync Google Fit data with a custom health dashboard app. Data can be pulled at regular intervals to update the dashboard, allowing users or health coaches to view trends and insights over time. This could be extended by connecting it to a data visualization tool like Google Data Studio for enhanced reporting capabilities. + # Troubleshooting **Application disconnects after 7 days**
If your developer application disconnects after 7 days, you need to follow the steps above to Publish your Google Fit app in order to keep your account connected. \ No newline at end of file diff --git a/components/google_fit_developer_app/package.json b/components/google_fit_developer_app/package.json index 4f68982d9e285..fd8d61449a7bb 100644 --- a/components/google_fit_developer_app/package.json +++ b/components/google_fit_developer_app/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/google_forms/README.md b/components/google_forms/README.md new file mode 100644 index 0000000000000..5fde498c697a5 --- /dev/null +++ b/components/google_forms/README.md @@ -0,0 +1,11 @@ +# Overview + +The Google Forms API lets you programmatically access and manipulate your forms and responses. In Pipedream, integrating Google Forms can automate mundane tasks, sync form data with other services, and trigger actions based on submission events. You can create, edit, and collaborate on forms, and analyze responses in real-time combined with Pipedream's serverless platform to create powerful workflows. + +# Example Use Cases + +- **Auto-Respond to Form Submissions**: Set up a workflow that sends a customized email to the respondent using the Gmail app whenever a new Google Form submission is received. This can provide immediate engagement and confirm receipt of their responses. + +- **Save Submissions to a Spreadsheet**: Automatically add new Google Forms responses to a Google Sheets spreadsheet. This workflow streamlines data collection and allows for advanced analysis and reporting capabilities. + +- **Sync to a CRM**: On new form submission, use the Pipedream workflow to parse and send the data to a CRM like Salesforce or HubSpot. This keeps your sales or customer service teams updated with fresh leads or inquiries without manual data entry. diff --git a/components/google_gemini/README.md b/components/google_gemini/README.md new file mode 100644 index 0000000000000..27925307fa38c --- /dev/null +++ b/components/google_gemini/README.md @@ -0,0 +1,11 @@ +# Overview + +The Google Gemini API is a cutting-edge tool from Google that enables developers to leverage AI models like Imagen and MusicLM to create and manipulate images and music based on textual descriptions. With Pipedream, you can harness this API to automate workflows that integrate AI-generated content into a variety of applications, from generating visuals for social media posts to composing background music for videos. Pipedream's serverless platform allows you to connect Google Gemini API with other apps to create complex, event-driven workflows without managing infrastructure. + +# Example Use Cases + +- **Dynamic Social Media Content Generation**: Combine Google Gemini with Twitter on Pipedream to automatically generate and post images on Twitter based on trending topics. Use Twitter triggers to monitor keywords, then invoke Gemini to create relevant images, and post them as tweets to engage your audience. + +- **Automated Blog Illustrations**: Integrate Google Gemini with WordPress on Pipedream. Set up a workflow that listens for new blog post events, utilizes Gemini to create illustrations based on the post's content, and automatically updates the blog with the newly generated images, enriching the visual appeal of your articles. + +- **Personalized Email Campaigns**: Link Google Gemini with an email service like SendGrid on Pipedream. Design a workflow that crafts emails with custom images generated by Gemini, corresponding to the interests or previous interactions of the recipient, thus enhancing the personal touch and effectiveness of your marketing campaigns. diff --git a/components/google_gemini/actions/common/generate-content.mjs b/components/google_gemini/actions/common/generate-content.mjs new file mode 100644 index 0000000000000..3417d27d239c2 --- /dev/null +++ b/components/google_gemini/actions/common/generate-content.mjs @@ -0,0 +1,92 @@ +import app from "../../google_gemini.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + model: { + propDefinition: [ + app, + "model", + () => ({ + filter: ({ + description, + supportedGenerationMethods, + }) => ![ + "discontinued", + "deprecated", + ].some((keyword) => description.includes(keyword)) + && supportedGenerationMethods?.includes(constants.MODEL_METHODS.GENERATE_CONTENT), + }), + ], + }, + }, + async additionalProps() { + const { + model, + responseFormat, + } = this; + + const { + outputTokenLimit, + temperature, + topP, + topK, + maxTemperature, + } = await this.app.getModel({ + model, + }); + + return { + ...(responseFormat && { + responseSchema: { + type: "string", + label: "Response Schema", + description: "Define the structure of the JSON response. Must be a valid JSON schema object. Leave empty to let Gemini determine the structure.", + optional: true, + }, + }), + ...(outputTokenLimit && { + maxOutputTokens: { + type: "integer", + label: "Max Output Tokens", + description: `The maximum number of tokens to generate in the response. Eg. \`${outputTokenLimit}\`.`, + optional: true, + max: outputTokenLimit, + }, + }), + ...(temperature && { + temperature: { + type: "string", + label: "Temperature", + description: `Controls the randomness of the generated text. Lower values make the text more deterministic, while higher values make it more random. Eg. \`${temperature}\`.${maxTemperature + ? ` Where max temperature is \`${maxTemperature}\`.` + : ""}`, + optional: true, + }, + }), + ...(topP && { + topP: { + type: "string", + label: "Top P", + description: `Controls the diversity of the generated text. Lower values make the text more deterministic, while higher values make it more random. Eg. \`${topP}\`.`, + optional: true, + }, + }), + ...(topK && { + topK: { + type: "integer", + label: "Top K", + description: `Controls the diversity of the generated text. Lower values make the text more deterministic, while higher values make it more random. Eg. \`${topK}\`.`, + optional: true, + }, + }), + stopSequences: { + type: "string[]", + label: "Stop Sequences", + description: "The set of character sequences (up to 5) that will stop output generation. If specified, the API will stop at the first appearance of a `stop_sequence`. The stop sequence will not be included as part of the response.", + optional: true, + }, + }; + }, +}; diff --git a/components/google_gemini/actions/generate-content-from-text-and-image/generate-content-from-text-and-image.mjs b/components/google_gemini/actions/generate-content-from-text-and-image/generate-content-from-text-and-image.mjs index ff2564ae3ebc7..e3faed0ad6bde 100644 --- a/components/google_gemini/actions/generate-content-from-text-and-image/generate-content-from-text-and-image.mjs +++ b/components/google_gemini/actions/generate-content-from-text-and-image/generate-content-from-text-and-image.mjs @@ -1,34 +1,41 @@ import fs from "fs"; import { ConfigurationError } from "@pipedream/platform"; -import app from "../../google_gemini.app.mjs"; -import constants from "../../common/constants.mjs"; +import common from "../common/generate-content.mjs"; +import utils from "../../common/utils.mjs"; export default { + ...common, key: "google_gemini-generate-content-from-text-and-image", name: "Generate Content from Text and Image", description: "Generates content from both text and image input using the Gemini API. [See the documentation](https://ai.google.dev/tutorials/rest_quickstart#text-and-image_input)", - version: "0.0.1", + version: "0.1.1", type: "action", props: { - app, + ...common.props, text: { propDefinition: [ - app, + common.props.app, "text", ], }, mimeType: { propDefinition: [ - app, + common.props.app, "mimeType", ], }, imagePaths: { propDefinition: [ - app, + common.props.app, "imagePaths", ], }, + responseFormat: { + propDefinition: [ + common.props.app, + "responseFormat", + ], + }, }, methods: { fileToGenerativePart(path, mimeType) { @@ -46,9 +53,17 @@ export default { async run({ $ }) { const { app, + model, text, imagePaths, mimeType, + responseFormat, + responseSchema, + maxOutputTokens, + temperature, + topP, + topK, + stopSequences, } = this; if (!Array.isArray(imagePaths)) { @@ -61,7 +76,7 @@ export default { const response = await app.generateContent({ $, - modelType: constants.MODEL_TYPE.GEMINI_PRO_VISION, + model, data: { contents: [ { @@ -73,6 +88,21 @@ export default { ], }, ], + ...( + responseFormat || maxOutputTokens || temperature || topP || topK || stopSequences?.length + ? { + generationConfig: { + responseMimeType: "application/json", + responseSchema: utils.parse(responseSchema), + maxOutputTokens, + temperature, + topP, + topK, + stopSequences, + }, + } + : {} + ), }, }); diff --git a/components/google_gemini/actions/generate-content-from-text/generate-content-from-text.mjs b/components/google_gemini/actions/generate-content-from-text/generate-content-from-text.mjs index cbfed62f3c7b4..5a0c3da386a30 100644 --- a/components/google_gemini/actions/generate-content-from-text/generate-content-from-text.mjs +++ b/components/google_gemini/actions/generate-content-from-text/generate-content-from-text.mjs @@ -1,30 +1,45 @@ -import app from "../../google_gemini.app.mjs"; -import constants from "../../common/constants.mjs"; +import common from "../common/generate-content.mjs"; +import utils from "../../common/utils.mjs"; export default { + ...common, key: "google_gemini-generate-content-from-text", name: "Generate Content from Text", description: "Generates content from text input using the Google Gemini API. [See the documentation](https://ai.google.dev/tutorials/rest_quickstart#text-only_input)", - version: "0.0.1", + version: "0.1.1", type: "action", props: { - app, + ...common.props, text: { propDefinition: [ - app, + common.props.app, "text", ], }, + responseFormat: { + propDefinition: [ + common.props.app, + "responseFormat", + ], + }, }, async run({ $ }) { const { app, + model, text, + responseFormat, + responseSchema, + maxOutputTokens, + temperature, + topP, + topK, + stopSequences, } = this; const response = await app.generateContent({ $, - modelType: constants.MODEL_TYPE.GEMINI_PRO, + model, data: { contents: [ { @@ -35,6 +50,21 @@ export default { ], }, ], + ...( + responseFormat || maxOutputTokens || temperature || topP || topK || stopSequences?.length + ? { + generationConfig: { + responseMimeType: "application/json", + responseSchema: utils.parse(responseSchema), + maxOutputTokens, + temperature, + topP, + topK, + stopSequences, + }, + } + : {} + ), }, }); diff --git a/components/google_gemini/common/constants.mjs b/components/google_gemini/common/constants.mjs index 3648c16989abe..e673d65556807 100644 --- a/components/google_gemini/common/constants.mjs +++ b/components/google_gemini/common/constants.mjs @@ -1,17 +1,12 @@ const BASE_URL = "https://generativelanguage.googleapis.com"; -const VERSION_PATH = "/v1beta/models"; -const LAST_CREATED_AT = "lastCreatedAt"; -const DEFAULT_MAX = 600; +const VERSION_PATH = "/v1beta"; -const MODEL_TYPE = { - GEMINI_PRO: "gemini-pro", - GEMINI_PRO_VISION: "gemini-pro-vision", +const MODEL_METHODS = { + GENERATE_CONTENT: "generateContent", }; export default { BASE_URL, VERSION_PATH, - DEFAULT_MAX, - LAST_CREATED_AT, - MODEL_TYPE, + MODEL_METHODS, }; diff --git a/components/google_gemini/common/utils.mjs b/components/google_gemini/common/utils.mjs new file mode 100644 index 0000000000000..899f34cfaf092 --- /dev/null +++ b/components/google_gemini/common/utils.mjs @@ -0,0 +1,24 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function emptyStrToUndefined(value) { + const trimmed = typeof(value) === "string" && value.trim(); + return trimmed === "" + ? undefined + : value; +} + +function parse(value) { + const valueToParse = emptyStrToUndefined(value); + if (typeof(valueToParse) === "object" || valueToParse === undefined) { + return valueToParse; + } + try { + return JSON.parse(valueToParse); + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid object"); + } +} + +export default { + parse, +}; diff --git a/components/google_gemini/google_gemini.app.mjs b/components/google_gemini/google_gemini.app.mjs index 21cc578aa0b0c..98132ef3abf63 100644 --- a/components/google_gemini/google_gemini.app.mjs +++ b/components/google_gemini/google_gemini.app.mjs @@ -30,6 +30,53 @@ export default { label: "Image File Paths", description: "The local file paths of the images to use in the content generation. The path to the image file saved to the `/tmp` directory (e.g. `/tmp/example.pdf`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", }, + model: { + type: "string", + label: "Model", + description: "The model to use for content generation", + reloadProps: true, + async options({ + prevContext: { pageToken }, + filter = (model) => model, + }) { + if (pageToken === null) { + return []; + } + const { + models, + nextPageToken, + } = await this.listModels({ + params: { + pageToken, + }, + }); + + const options = models + .filter(filter) + .map(({ + name: value, + displayName: label, + }) => ({ + label, + value, + })); + + return { + options, + context: { + pageToken: nextPageToken || null, + }, + }; + }, + }, + responseFormat: { + type: "boolean", + label: "JSON Output", + description: "Enable to receive responses in structured JSON format instead of plain text. Useful for automated processing, data extraction, or when you need to parse the response programmatically. You can optionally define a specific schema for the response structure.", + optional: true, + default: false, + reloadProps: true, + }, }, methods: { getUrl(path) { @@ -61,10 +108,27 @@ export default { }); }, generateContent({ - modelType, ...args + model, ...args } = {}) { + const pathPrefix = model.startsWith("models/") + ? model + : `models/${model}`; return this.post({ - path: `/${modelType}:generateContent`, + path: `/${pathPrefix}:${constants.MODEL_METHODS.GENERATE_CONTENT}`, + ...args, + }); + }, + listModels(args = {}) { + return this.makeRequest({ + path: "/models", + ...args, + }); + }, + getModel({ + model, ...args + } = {}) { + return this.makeRequest({ + path: `/${model}`, ...args, }); }, diff --git a/components/google_gemini/package.json b/components/google_gemini/package.json index cde352739f5f8..d7b0ef2e3b5f1 100644 --- a/components/google_gemini/package.json +++ b/components/google_gemini/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/google_gemini", - "version": "0.1.0", + "version": "0.2.1", "description": "Pipedream Google Gemini Components", "main": "google_gemini.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/google_maps_platform/.gitignore b/components/google_maps_platform/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/google_maps_platform/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/google_maps_platform/README.md b/components/google_maps_platform/README.md new file mode 100644 index 0000000000000..b97b1a15c4f74 --- /dev/null +++ b/components/google_maps_platform/README.md @@ -0,0 +1,11 @@ +# Overview + +The Google Maps (Places API) offers detailed information about physical locations, including places of interest, reviews, and other metadata. In Pipedream, you can leverage this API to create dynamic workflows that respond to location data. Whether you're building apps that track asset locations, automate location-based alerts, or provide users with local info, integrating with Google Maps can add a powerful spatial context to your services. + +# Example Use Cases + +- **Local Restaurant Alert System**: Create a Pipedream workflow that triggers daily, uses the Google Maps (Places API) to find top-rated restaurants in a specified area, and sends the list via email or to a Slack channel. This could be used by travel agencies to provide clients with personalized travel itineraries. + +- **Event Venue Checker**: Use the Places API within a Pipedream workflow to monitor the status of venues for upcoming events. The workflow could poll for changes in opening hours or temporary closures and notify event planners through SMS or a project management tool like Trello. + +- **Real Estate Price Analyzer**: Real estate platforms could employ a Pipedream workflow that integrates Places API data with property listings. This workflow would enrich property information with local amenities and points of interest, offering potential buyers more comprehensive insights through a custom API or a CRM like Salesforce. diff --git a/components/google_maps_platform/actions/get-place-details/get-place-details.mjs b/components/google_maps_platform/actions/get-place-details/get-place-details.mjs new file mode 100644 index 0000000000000..12de9b93285e1 --- /dev/null +++ b/components/google_maps_platform/actions/get-place-details/get-place-details.mjs @@ -0,0 +1,26 @@ +import app from "../../google_maps_platform.app.mjs"; + +export default { + key: "google_maps_platform-get-place-details", + name: "Get Place Details", + description: "Retrieves detailed information for a specific place using its Place ID. [See the documentation](https://developers.google.com/maps/documentation/places/web-service/place-details)", + version: "0.0.1", + type: "action", + props: { + app, + placeId: { + type: "string", + label: "Place ID", + description: "A textual identifier that uniquely identifies a place, returned from Search Places Action.", + }, + }, + async run({ $ }) { + const response = await this.app.getPlaceDetails({ + $, + placeId: this.placeId, + }); + + $.export("$summary", `Retrieved details for Place ID ${this.placeId}`); + return response; + }, +}; diff --git a/components/google_maps_platform/actions/search-places/search-places.mjs b/components/google_maps_platform/actions/search-places/search-places.mjs new file mode 100644 index 0000000000000..dcb8ae35cb644 --- /dev/null +++ b/components/google_maps_platform/actions/search-places/search-places.mjs @@ -0,0 +1,123 @@ +import { + LANGUAGE_CODE_OPTIONS, + PRICE_LEVEL_OPTIONS, + RANK_PREFERENCE_OPTIONS, +} from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import app from "../../google_maps_platform.app.mjs"; + +export default { + key: "google_maps_platform-search-places", + name: "Search Places", + description: "Searches for places based on location, radius, and optional filters like keywords, place type, or name. [See the documentation](https://developers.google.com/maps/documentation/places/web-service/text-search)", + version: "0.0.1", + type: "action", + props: { + app, + textQuery: { + type: "string", + label: "Text Query", + description: "The text string on which to search, for example: \"restaurant\", \"123 Main Street\", or \"best place to visit in San Francisco\". The API returns candidate matches based on this string and orders the results based on their perceived relevance.", + }, + includedType: { + type: "string", + label: "Included Type", + description: "Restricts the results to places matching the specified type defined by [Table A](https://developers.google.com/maps/documentation/places/web-service/place-types#table-a). Only one type may be specified.", + optional: true, + }, + includePureServiceAreaBusinesses: { + type: "boolean", + label: "Include Pure Service Area Businesses", + description: "If set to `true`, the response includes businesses that visit or deliver to customers directly, but don't have a physical business location. If set to `false`, the API returns only businesses with a physical business location.", + optional: true, + }, + languageCode: { + type: "string", + label: "Language Code", + description: "The language in which to return results.", + options: LANGUAGE_CODE_OPTIONS, + optional: true, + }, + locationBias: { + type: "object", + label: "Location Bias", + description: "Specifies an area to search. This location serves as a bias which means results around the specified location can be returned, including results outside the specified area. [See the documentation](https://developers.google.com/maps/documentation/places/web-service/text-search#location-bias) for further information.", + optional: true, + }, + locationRestriction: { + type: "string", + label: "Location Restriction", + description: "Specifies an area to search. Results outside the specified area are not returned.", + optional: true, + }, + evOptions: { + type: "object", + label: "EV Options", + description: "Specifies parameters for identifying available electric vehicle (EV) charging connectors and charging rates. [See the documentation](https://developers.google.com/maps/documentation/places/web-service/text-search#evoptions) for further information.", + optional: true, + }, + minRating: { + type: "string", + label: "Min Rating", + description: "Restricts results to only those whose average user rating is greater than or equal to this limit. Values must be between 0.0 and 5.0 (inclusive) in increments of 0.5. For example: 0, 0.5, 1.0, ... , 5.0 inclusive. Values are rounded up to the nearest 0.5. For example, a value of 0.6 eliminates all results with a rating less than 1.0.", + optional: true, + }, + openNow: { + type: "boolean", + label: "Open Now", + description: "If `true`, return only those places that are open for business at the time the query is sent. If `false`, return all businesses regardless of open status. Places that don't specify opening hours in the Google Places database are returned if you set this parameter to `false`.", + optional: true, + }, + priceLevels: { + type: "string[]", + label: "Price Levels", + description: "Restrict the search to places that are marked at certain price levels. The default is to select all price levels.", + options: PRICE_LEVEL_OPTIONS, + optional: true, + }, + rankPreference: { + type: "string", + label: "Rank Preference", + description: "Specifies how the results are ranked in the response based on the type of query: For a categorical query such as \"Restaurants in New York City\", RELEVANCE (rank results by search relevance) is the default. You can set Rank Preference to RELEVANCE or DISTANCE (rank results by distance). For a non-categorical query such as \"Mountain View, CA\", it is recommended that you leave Rank Preference unset.", + options: RANK_PREFERENCE_OPTIONS, + optional: true, + }, + regionCode: { + type: "string", + label: "Region Code", + description: "The region code used to format the response, specified as a [two-character CLDR code](https://www.unicode.org/cldr/charts/46/supplemental/territory_language_information.html) value. This parameter can also have a bias effect on the search results.", + optional: true, + }, + strictTypeFiltering: { + type: "boolean", + label: "Strict Type Filtering", + description: "Used with the `Included Type` parameter. When set to `true`, only places that match the specified types specified by includeType are returned. When `false`, the default, the response can contain places that don't match the specified types.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.app.searchPlaces({ + $, + data: { + textQuery: this.textQuery, + includedType: this.includedType, + includePureServiceAreaBusinesses: this.includePureServiceAreaBusinesses, + languageCode: this.languageCode, + locationBias: parseObject(this.locationBias), + locationRestriction: this.locationRestriction, + evOptions: parseObject(this.evOptions), + minRating: this.minRating, + openNow: this.openNow, + priceLevels: parseObject(this.priceLevels), + rankPreference: this.rankPreference, + regionCode: this.regionCode, + strictTypeFiltering: this.strictTypeFiltering, + }, + }); + + const placeCount = response.places?.length || 0; + $.export("$summary", `Found ${placeCount} place(s)`); + + return response; + }, +}; diff --git a/components/google_maps_platform/app/google_maps_platform.app.ts b/components/google_maps_platform/app/google_maps_platform.app.ts deleted file mode 100644 index b9271d2b6472a..0000000000000 --- a/components/google_maps_platform/app/google_maps_platform.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "google_maps_platform", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/google_maps_platform/common/constants.mjs b/components/google_maps_platform/common/constants.mjs new file mode 100644 index 0000000000000..5862d59bf62a3 --- /dev/null +++ b/components/google_maps_platform/common/constants.mjs @@ -0,0 +1,364 @@ +export const LANGUAGE_CODE_OPTIONS = [ + { + label: "Afrikaans", + value: "af", + }, + { + label: "Albanian", + value: "sq", + }, + { + label: "Amharic", + value: "am", + }, + { + label: "Arabic", + value: "ar", + }, + { + label: "Armenian", + value: "hy", + }, + { + label: "Azerbaijani", + value: "az", + }, + { + label: "Basque", + value: "eu", + }, + { + label: "Belarusian", + value: "be", + }, + { + label: "Bengali", + value: "bn", + }, + { + label: "Bosnian", + value: "bs", + }, + { + label: "Bulgarian", + value: "bg", + }, + { + label: "Burmese", + value: "my", + }, + { + label: "Catalan", + value: "ca", + }, + { + label: "Chinese", + value: "zh", + }, + { + label: "Chinese (Simplified)", + value: "zh-CN", + }, + { + label: "Chinese (Hong Kong)", + value: "zh-HK", + }, + { + label: "Chinese (Traditional)", + value: "zh-TW", + }, + { + label: "Croatian", + value: "hr", + }, + { + label: "Czech", + value: "cs", + }, + { + label: "Danish", + value: "da", + }, + { + label: "Dutch", + value: "nl", + }, + { + label: "English", + value: "en", + }, + { + label: "English (Australian)", + value: "en-AU", + }, + { + label: "English (Great Britain)", + value: "en-GB", + }, + { + label: "Estonian", + value: "et", + }, + { + label: "Farsi", + value: "fa", + }, + { + label: "Finnish", + value: "fi", + }, + { + label: "Filipino", + value: "fil", + }, + { + label: "French", + value: "fr", + }, + { + label: "French (Canada)", + value: "fr-CA", + }, + { + label: "Galician", + value: "gl", + }, + { + label: "Georgian", + value: "ka", + }, + { + label: "German", + value: "de", + }, + { + label: "Greek", + value: "el", + }, + { + label: "Gujarati", + value: "gu", + }, + { + label: "Hebrew", + value: "iw", + }, + { + label: "Hindi", + value: "hi", + }, + { + label: "Hungarian", + value: "hu", + }, + { + label: "Icelandic", + value: "is", + }, + { + label: "Indonesian", + value: "id", + }, + { + label: "Italian", + value: "it", + }, + { + label: "Japanese", + value: "ja", + }, + { + label: "Kannada", + value: "kn", + }, + { + label: "Kazakh", + value: "kk", + }, + { + label: "Khmer", + value: "km", + }, + { + label: "Korean", + value: "ko", + }, + { + label: "Kyrgyz", + value: "ky", + }, + { + label: "Lao", + value: "lo", + }, + { + label: "Latvian", + value: "lv", + }, + { + label: "Lithuanian", + value: "lt", + }, + { + label: "Macedonian", + value: "mk", + }, + { + label: "Malay", + value: "ms", + }, + { + label: "Malayalam", + value: "ml", + }, + { + label: "Marathi", + value: "mr", + }, + { + label: "Mongolian", + value: "mn", + }, + { + label: "Nepali", + value: "ne", + }, + { + label: "Norwegian", + value: "no", + }, + { + label: "Polish", + value: "pl", + }, + { + label: "Portuguese", + value: "pt", + }, + { + label: "Portuguese (Brazil)", + value: "pt-BR", + }, + { + label: "Portuguese (Portugal)", + value: "pt-PT", + }, + { + label: "Punjabi", + value: "pa", + }, + { + label: "Romanian", + value: "ro", + }, + { + label: "Russian", + value: "ru", + }, + { + label: "Serbian", + value: "sr", + }, + { + label: "Sinhalese", + value: "si", + }, + { + label: "Slovak", + value: "sk", + }, + { + label: "Slovenian", + value: "sl", + }, + { + label: "Spanish", + value: "es", + }, + { + label: "Spanish (Latin America)", + value: "es-419", + }, + { + label: "Swahili", + value: "sw", + }, + { + label: "Swedish", + value: "sv", + }, + { + label: "Tamil", + value: "ta", + }, + { + label: "Telugu", + value: "te", + }, + { + label: "Thai", + value: "th", + }, + { + label: "Turkish", + value: "tr", + }, + { + label: "Ukrainian", + value: "uk", + }, + { + label: "Urdu", + value: "ur", + }, + { + label: "Uzbek", + value: "uz", + }, + { + label: "Vietnamese", + value: "vi", + }, + { + label: "Zulu", + value: "zu", + }, +]; + +export const PRICE_LEVEL_OPTIONS = [ + { + label: "Place price level is unspecified or unknown.", + value: "PRICE_LEVEL_UNSPECIFIED", + }, + { + label: "Place provides free services.", + value: "PRICE_LEVEL_FREE", + }, + { + label: "Place provides inexpensive services.", + value: "PRICE_LEVEL_INEXPENSIVE", + }, + { + label: "Place provides moderately priced services.", + value: "PRICE_LEVEL_MODERATE", + }, + { + label: "Place provides expensive services.", + value: "PRICE_LEVEL_EXPENSIVE", + }, + { + label: "Place provides very expensive services.", + value: "PRICE_LEVEL_VERY_EXPENSIVE", + }, +]; + +export const RANK_PREFERENCE_OPTIONS = [ + { + label: "Rank results by search relevance.", + value: "RELEVANCE", + }, + { + label: "Rank results by distance.", + value: "DISTANCE", + }, +]; diff --git a/components/google_maps_platform/common/utils.mjs b/components/google_maps_platform/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/google_maps_platform/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/google_maps_platform/google_maps_platform.app.mjs b/components/google_maps_platform/google_maps_platform.app.mjs new file mode 100644 index 0000000000000..d6137f34f33da --- /dev/null +++ b/components/google_maps_platform/google_maps_platform.app.mjs @@ -0,0 +1,42 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "google_maps_platform", + methods: { + _baseUrl() { + return "https://places.googleapis.com/v1/places"; + }, + _headers() { + return { + "X-Goog-Api-Key": this.$auth.api_key, + "Content-Type": "application/json", + "X-Goog-FieldMask": "*", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + searchPlaces(opts = {}) { + return this._makeRequest({ + method: "POST", + path: ":searchText", + ...opts, + }); + }, + getPlaceDetails({ + placeId, ...opts + }) { + return this._makeRequest({ + path: `/${placeId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/google_maps_platform/package.json b/components/google_maps_platform/package.json index 70a98483e4ed1..98de8f15249da 100644 --- a/components/google_maps_platform/package.json +++ b/components/google_maps_platform/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/google_maps_platform", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream Google Maps Platform (Places API) Components", - "main": "dist/app/google_maps_platform.app.mjs", + "main": "google_maps_platform.app.mjs", "keywords": [ "pipedream", "google_maps_platform" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/google_maps_platform", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/google_meet/README.md b/components/google_meet/README.md new file mode 100644 index 0000000000000..73d7121bc98a0 --- /dev/null +++ b/components/google_meet/README.md @@ -0,0 +1,11 @@ +# Overview + +The Google Meet API allows developers to automate aspects of the video conferencing service, like creating and managing meetings. With Pipedream, these capabilities can be leveraged to build custom workflows, integrating Google Meet with other services to streamline scheduling, notification, and management processes. For instance, you can automate the creation of meetings in response to calendar events, sync meeting details with a CRM, or initiate follow-up actions once a meeting concludes. + +# Example Use Cases + +- **Automated Meeting Scheduler:** Sync Google Calendar events with Google Meet to automatically create meetings for new calendar events. Use Pipedream to listen for new events on a Google Calendar and then use the Google Meet API to set up corresponding video conferences, sending invites to participants. + +- **Meeting Minute Distribution:** After a meeting ends, use Pipedream to trigger a workflow that sends out the meeting recording and summary to all participants via email. This can be done by integrating Google Meet with an email app like Gmail, capturing the end of meeting event, and distributing the necessary follow-up materials. + +- **CRM Integration for Customer Calls:** When a new meeting is scheduled with a customer, use Pipedream to log the event to your CRM, like Salesforce. This workflow can automatically create a new activity or log within the CRM whenever a Google Meet invite is sent, ensuring sales teams have up-to-date information on customer interactions. diff --git a/components/google_meet/actions/schedule-meeting/schedule-meeting.mjs b/components/google_meet/actions/schedule-meeting/schedule-meeting.mjs new file mode 100644 index 0000000000000..1d57647a6e147 --- /dev/null +++ b/components/google_meet/actions/schedule-meeting/schedule-meeting.mjs @@ -0,0 +1,167 @@ +import googleMeet from "../../google_meet.app.mjs"; +import { v4 as uuidv4 } from "uuid"; + +export default { + key: "google_meet-schedule-meeting", + name: "Schedule Meeting", + description: "Creates a new event in Google Calendar with a Google Meet link. [See the documentation](https://developers.google.com/calendar/api/v3/reference/events/insert)", + version: "0.0.1", + type: "action", + props: { + googleMeet, + calendarId: { + propDefinition: [ + googleMeet, + "calendarId", + ], + }, + summary: { + label: "Event Title", + type: "string", + description: "Enter a title for the event, (e.g., `My event`)", + optional: true, + }, + location: { + label: "Event Location", + type: "string", + description: "Specify the location of the event", + optional: true, + }, + description: { + label: "Event Description", + type: "string", + description: "Enter a description for the event", + optional: true, + }, + attendees: { + label: "Attendees", + type: "string[]", + description: "Enter an array of email addresses for any attendees", + optional: true, + }, + eventStartDate: { + label: "Event Start Date", + type: "string", + description: "For all-day events, enter the Event day in the format `yyyy-mm-dd`. For events with time, format according to [RFC3339](https://www.rfc-editor.org/rfc/rfc3339.html#section-1): `yyyy-mm-ddThh:mm:ss+01:00`. A time zone offset is required unless a time zone is explicitly specified in timeZone.", + }, + eventEndDate: { + label: "Event End Date", + type: "string", + description: "For all-day events, enter the Event day in the format `yyyy-mm-dd`. For events with time, format according to [RFC3339](https://www.rfc-editor.org/rfc/rfc3339.html#section-1): `yyyy-mm-ddThh:mm:ss+01:00`. A time zone offset is required unless a time zone is explicitly specified in timeZone.", + }, + recurrence: { + label: "Recurrence", + type: "string[]", + description: "Recurrence rule(s) for the event. For example, `RRULE:FREQ=DAILY;INTERVAL=2` means once every two days, `RRULE:FREQ=YEARLY` means annually.\nYou can combine multiple recurrence rules. [See the documentation](https://developers.google.com/calendar/api/concepts/events-calendars#recurrence_rule)", + optional: true, + }, + timeZone: { + propDefinition: [ + googleMeet, + "timeZone", + ], + }, + sendUpdates: { + propDefinition: [ + googleMeet, + "sendUpdates", + ], + }, + sendNotifications: { + propDefinition: [ + googleMeet, + "sendNotifications", + ], + }, + colorId: { + propDefinition: [ + googleMeet, + "colorId", + ], + }, + }, + methods: { + async getTimeZone(selectedTimeZone) { + const { value: timeZone } = selectedTimeZone ?? await this.googleMeet.getSettings({ + setting: "timezone", + }); + return timeZone; + }, + formatAttendees(selectedAttendees, currentAttendees) { + let attendees = []; + if (selectedAttendees && Array.isArray(selectedAttendees)) { + attendees = selectedAttendees.map((email) => ({ + email, + })); + } else if (currentAttendees && Array.isArray(currentAttendees)) { + return currentAttendees.map((attendee) => ({ + email: attendee.email, + })); + } + return attendees; + }, + checkDateOrDateTimeInput(date, type) { + if (type === "date") { + return date && date.length <= 10 + ? date + : undefined; + } + if (type === "dateTime") { + return date && date.length > 10 + ? date + : undefined; + } + }, + getDateParam({ + date, + timeZone, + }) { + return { + date: this.checkDateOrDateTimeInput(date, "date"), + dateTime: this.checkDateOrDateTimeInput(date, "dateTime"), + timeZone, + }; + }, + }, + async run({ $ }) { + const timeZone = await this.getTimeZone(this.timeZone); + const attendees = this.formatAttendees(this.attendees); + + const data = { + calendarId: this.calendarId, + sendUpdates: this.sendUpdates, + sendNotifications: this.sendNotifications, + resource: { + summary: this.summary, + location: this.location, + description: this.description, + start: this.getDateParam({ + date: this.eventStartDate, + timeZone, + }), + end: this.getDateParam({ + date: this.eventEndDate, + timeZone, + }), + recurrence: this.recurrence, + attendees, + colorId: this.colorId, + conferenceData: { + createRequest: { + requestId: uuidv4(), + conferenceSolutionKey: { + type: "hangoutsMeet", + }, + }, + }, + }, + conferenceDataVersion: 1, + }; + + const response = await this.googleMeet.createEvent(data); + + $.export("$summary", `Successfully created event: "${response.id}"`); + + return response; + }, +}; diff --git a/components/google_meet/google_meet.app.mjs b/components/google_meet/google_meet.app.mjs new file mode 100644 index 0000000000000..7dd4b42764605 --- /dev/null +++ b/components/google_meet/google_meet.app.mjs @@ -0,0 +1,141 @@ +import calendar from "@googleapis/calendar"; +import timezones from "moment-timezone"; + +export default { + type: "app", + app: "google_meet", + propDefinitions: { + calendarId: { + label: "Calendar ID", + type: "string", + description: "Optionally select the calendar, defaults to the primary calendar for the logged-in user", + default: "primary", + optional: true, + async options({ prevContext }) { + const { nextPageToken } = prevContext; + if (nextPageToken === false) { + return []; + } + const response = await this.listCalendars({ + pageToken: nextPageToken, + }); + const options = response.items.map((item) => ({ + label: item.summary, + value: item.id, + })); + return { + options, + context: { + nextPageToken: response.nextPageToken ?? false, + }, + }; + }, + }, + timeZone: { + type: "string", + label: "Time Zone", + description: "Time zone used in the response. Optional. The default is the time zone of the calendar.", + optional: true, + options() { + const timeZonesList = timezones.tz.names().map((timezone) => { + return { + label: timezone, + value: timezone, + }; + }); + return timeZonesList; + }, + }, + sendUpdates: { + label: "Send Updates", + type: "string", + description: "Configure whether to send notifications about the event", + optional: true, + options: [ + "all", + "externalOnly", + "none", + ], + }, + sendNotifications: { + label: "Send Notifications", + type: "boolean", + description: "Whether to send notifications about the event update", + optional: true, + }, + colorId: { + label: "Color ID", + type: "string", + description: "The color of the event. This is an ID referring to an entry in the event section of the colors definition (see the colors endpoint).", + optional: true, + async options() { + const response = await this.listColors(); + return Object.entries(response.event).map(([ + key, + value, + ]) => ({ + label: `Background ${value.background} | Foreground ${value.foreground}`, + value: key, + })); + }, + }, + }, + methods: { + client() { + const auth = new calendar.auth.OAuth2(); + auth.setCredentials({ + access_token: this.$auth.oauth_access_token, + }); + return calendar.calendar({ + version: "v3", + auth, + }); + }, + async requestHandler({ + api, method, args = {}, + }) { + const { + returnOnlyData = true, + ...otherArgs + } = args; + try { + const calendar = this.client(); + const response = await calendar[api][method](otherArgs); + return returnOnlyData + ? response.data + : response; + } catch (error) { + console.error(JSON.stringify(error.response?.data, null, 2)); + throw error.response?.data?.error?.message ?? error; + } + }, + listCalendars(args = {}) { + return this.requestHandler({ + api: "calendarList", + method: "list", + args, + }); + }, + listColors(args = {}) { + return this.requestHandler({ + api: "colors", + method: "get", + args, + }); + }, + getSettings(args = {}) { + return this.requestHandler({ + api: "settings", + method: "get", + args, + }); + }, + createEvent(args = {}) { + return this.requestHandler({ + api: "events", + method: "insert", + args, + }); + }, + }, +}; diff --git a/components/google_meet/package.json b/components/google_meet/package.json new file mode 100644 index 0000000000000..769e4ea69191a --- /dev/null +++ b/components/google_meet/package.json @@ -0,0 +1,20 @@ +{ + "name": "@pipedream/google_meet", + "version": "0.1.0", + "description": "Pipedream Google Meet Components", + "main": "google_meet.app.mjs", + "keywords": [ + "pipedream", + "google_meet" + ], + "homepage": "https://pipedream.com/apps/google_meet", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@googleapis/calendar": "^9.7.0", + "moment-timezone": "^0.5.45", + "uuid": "^9.0.1" + } +} diff --git a/components/google_merchant_center/README.md b/components/google_merchant_center/README.md new file mode 100644 index 0000000000000..eb275ae9a0097 --- /dev/null +++ b/components/google_merchant_center/README.md @@ -0,0 +1,11 @@ +# Overview + +The Google Merchant Center API allows for programmatic interaction with your Google Merchant account, enabling data uploads, fetching product status, and managing listings directly. By leveraging this API within Pipedream, you can automate numerous tasks, such as syncing inventory levels, updating pricing, or managing product information across platforms. + +# Example Use Cases + +- **Automate Product Listings Updates**: Use the API to watch for changes in your inventory database and automatically update product listings in Google Merchant Center when stock levels or prices change. + +- **Sync Orders with CRM**: After a purchase is made through a Google Shopping ad, trigger a workflow to create or update the customer data and order details in your CRM system. + +- **Monitor Product Status Reports**: Set up a Pipedream workflow to periodically fetch and analyze product status reports from Google Merchant Center. Notify your team via Slack or email if there are any issues that need attention. diff --git a/components/google_my_business/README.md b/components/google_my_business/README.md new file mode 100644 index 0000000000000..06a5d2dfbd05a --- /dev/null +++ b/components/google_my_business/README.md @@ -0,0 +1,11 @@ +# Overview + +The Google My Business API enables businesses to manage their online presence across Google, including Search and Maps. Through Pipedream, you can automate various aspects of your Google My Business account, such as reading and responding to customer reviews, updating business information, and posting new content. This API provides a powerful way to engage with customers and maintain accurate, up-to-date business listings, all through programmable interactions that can save time and enhance visibility. + +# Example Use Cases + +- **Automated Review Management**: Monitor and respond to new reviews. Use sentiment analysis to determine the tone of the review, provided by an NLP service like Google Cloud Natural Language API, and craft automatic responses or escalate to human operators based on the sentiment score. + +- **Business Information Sync**: Keep business information updated across multiple platforms. Whenever you update your hours, description, or contact information in your primary business management tool, synchronize those changes with your Google My Business listing via Pipedream. + +- **Dynamic Posts for Promotions**: Automatically generate and post content to your Google My Business account. For example, when a new product is added to your eCommerce platform like Shopify, trigger a workflow that posts an announcement on your Google My Business profile highlighting the new arrival. diff --git a/components/google_my_business/package.json b/components/google_my_business/package.json index 0b2ee2209bf63..e21832ceb085e 100644 --- a/components/google_my_business/package.json +++ b/components/google_my_business/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/google_my_business", - "version": "0.1.2", + "version": "0.1.3", "description": "Pipedream Google My Business Components", "main": "dist/app/google_my_business.app.mjs", "keywords": [ diff --git a/components/google_my_business/sources/common.ts b/components/google_my_business/sources/common.ts index 5134645c83bd9..a547db8101bb9 100644 --- a/components/google_my_business/sources/common.ts +++ b/components/google_my_business/sources/common.ts @@ -34,11 +34,11 @@ export default { }, }, methods: { - setLastRun(value: string) { + setLastRun(value: number) { this.db.set("lastRun", value); }, getLastRun() { - const lastRun: string = this.db.get("lastRun"); + const lastRun: number = this.db.get("lastRun"); return lastRun ? new Date(lastRun) : null; @@ -50,10 +50,10 @@ export default { throw new Error("getSummary() not implemented in component"); }, async getAndProcessData() { + const currentRun: number = Date.now(); const lastRun: Date = this.getLastRun(); const items: EntityWithCreateTime[] = await this.getItems(); - const ts = Date.now(); - this.setLastRun(ts - 30000); + this.setLastRun(currentRun); const filteredItems = lastRun ? items.filter(({ createTime }) => new Date(createTime) >= lastRun) @@ -63,7 +63,7 @@ export default { this.$emit(item, { id: this.app.getCleanName(item.name), summary: this.getSummary(item), - ts, + ts: new Date(item.createTime), }); }); }, diff --git a/components/google_my_business/sources/new-post-created/new-post-created.ts b/components/google_my_business/sources/new-post-created/new-post-created.ts index 2fe89a08febb1..493e55e46102e 100644 --- a/components/google_my_business/sources/new-post-created/new-post-created.ts +++ b/components/google_my_business/sources/new-post-created/new-post-created.ts @@ -10,7 +10,7 @@ export default defineSource({ key: "google_my_business-new-post-created", name: "New Post Created", description: `Emit new event for each new local post on a location [See the documentation](${DOCS_LINK})`, - version: "0.0.2", + version: "0.0.3", type: "source", dedupe: "unique", methods: { @@ -28,9 +28,11 @@ export default defineSource({ return this.app.listPosts(params, false); }, getSummary({ summary }: LocalPost) { - return `New Post${summary ? `: "${summary.length > 50 - ? summary.slice(0, 45) + "[...]" - : summary}"` : ''}`; + return `New Post${summary + ? `: "${summary.length > 50 + ? summary.slice(0, 45) + "[...]" + : summary}"` + : ""}`; }, }, }); diff --git a/components/google_my_business/sources/new-review-created/new-review-created.ts b/components/google_my_business/sources/new-review-created/new-review-created.ts index c6423bbeb5b3f..d3ef0376156ec 100644 --- a/components/google_my_business/sources/new-review-created/new-review-created.ts +++ b/components/google_my_business/sources/new-review-created/new-review-created.ts @@ -10,7 +10,7 @@ export default defineSource({ key: "google_my_business-new-review-created", name: "New Review Created", description: `Emit new event for each new review on a location [See the documentation](${DOCS_LINK})`, - version: "0.0.2", + version: "0.0.3", type: "source", dedupe: "unique", methods: { @@ -28,9 +28,11 @@ export default defineSource({ return this.app.listReviews(params); }, getSummary({ comment }: Review) { - return `New Review${comment ? `: "${comment.length > 50 - ? comment.slice(0, 45) + "[...]" - : comment}"` : ''}`; + return `New Review${comment + ? `: "${comment.length > 50 + ? comment.slice(0, 45) + "[...]" + : comment}"` + : ""}`; }, }, }); diff --git a/components/google_palm_api/README.md b/components/google_palm_api/README.md new file mode 100644 index 0000000000000..0cef0954a6b83 --- /dev/null +++ b/components/google_palm_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Google PaLM API is a cutting-edge language model that allows developers to integrate advanced natural language understanding into their applications. On Pipedream, you can harness this power to create serverless workflows that react to various triggers and perform actions based on the insights and outputs from PaLM. Whether it's generating content, summarizing text, or understanding user intent, PaLM's capabilities can be integrated into Pipedream workflows to automate complex tasks involving language. + +# Example Use Cases + +- **Automated Customer Support**: Connect Google PaLM to your customer support system to analyze and respond to customer inquiries. PaLM can provide instant, accurate replies or escalate complex issues to human agents. + +- **Content Generation and Curation**: Use PaLM in a Pipedream workflow to draft blog posts, generate product descriptions, or summarize lengthy articles. Combine it with a CMS platform like WordPress to post content directly to your site. + +- **Language Translation Bot**: Build a translation bot that integrates Google PaLM with a messaging platform like Slack. PaLM can understand and translate messages in real-time, facilitating smooth communication in multilingual teams. diff --git a/components/google_photos/README.md b/components/google_photos/README.md index 88d32a28d1b13..3f59e85f40192 100644 --- a/components/google_photos/README.md +++ b/components/google_photos/README.md @@ -1,15 +1,11 @@ # Overview -Google Photos is a photo sharing and storage service developed by Google. It -was announced at the Google I/O conference on May 28, 2015. It allows users to -store, share, and edit photos and videos. The service also provides a suite of -tools for developers to build photo-sharing applications. - -The Google Photos API provides a set of tools that developers can use to build -applications that enable users to share and edit photos and videos. The API -lets developers create, view, and edit albums, photos, and videos; upload and -download photos and videos; and search for photos and videos. - -Albums, photos, and videos can be shared with other users, and users can add -comments to photos and videos. The API also provides a set of tools for -developers to build photo-sharing applications. +The Google Photos API allows you to integrate with Google's photo storage and sharing service, enabling you to create powerful automations around photo and album management. You can upload photos, create and manage albums, share photos and albums, and retrieve media items. When combined with Pipedream's ability to connect to hundreds of other apps, you can craft workflows that automate digital asset management, enhance productivity, and sync data across platforms. + +# Example Use Cases + +- **Photo Backup Automation**: Trigger a workflow on Pipedream that backs up new photos from connected devices or services (like Dropbox or social media platforms) directly to Google Photos. This ensures that your visual content is safely stored and organized in one place without manual intervention. + +- **Social Media Content Syndication**: Upon uploading a new photo to Google Photos, automatically post that image to various social media accounts like Twitter, Facebook, or Instagram. This workflow is great for marketers and content creators wanting to streamline their cross-platform media sharing process. + +- **Event-Triggered Photo Sharing**: Create a workflow where event-based triggers (such as a new purchase from a Stripe transaction or a new booking from a Calendly event) prompt the sharing of specific albums or photos from Google Photos to clients or stakeholders. This can be used to enhance customer engagement or to deliver visual project updates automatically. diff --git a/components/google_play/README.md b/components/google_play/README.md new file mode 100644 index 0000000000000..615f501d92087 --- /dev/null +++ b/components/google_play/README.md @@ -0,0 +1,11 @@ +# Overview + +The Google Play API allows developers to interact with various aspects of the Google Play Store, including app releases, reviews, and sales data. Leveraging this API on Pipedream enables you to automate tasks such as monitoring app performance, updating app listings, and processing user feedback. By connecting Google Play with Pipedream, you can create powerful workflows that save time, provide real-time data insights, and enhance app engagement and user satisfaction. + +# Example Use Cases + +- **Automate App Review Monitoring**: Automatically track and analyze user reviews on your Google Play Store listings. Set up a workflow on Pipedream that triggers alerts or emails to your customer support team when a review contains specific keywords (like "bug" or "error"), helping you respond quickly to user concerns. + +- **Sales Reporting Automation**: Integrate Google Play with data visualization tools like Google Sheets or Tableau on Pipedream. Automatically export sales and revenue data daily or weekly, allowing for real-time financial analysis and trend spotting without manual data entry, enhancing your decision-making process. + +- **Dynamic App Content Updates**: Use the Google Play API to update app listings or content based on user feedback or performance metrics. Create a Pipedream workflow that listens for specific triggers like a dip in user engagement, then automatically updates app descriptions, graphics, or promotions to boost visibility and user interaction. diff --git a/components/google_play/google_play.app.mjs b/components/google_play/google_play.app.mjs new file mode 100644 index 0000000000000..65aa81a58f907 --- /dev/null +++ b/components/google_play/google_play.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "google_play", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/google_play/package.json b/components/google_play/package.json new file mode 100644 index 0000000000000..dfe2ec5d966d5 --- /dev/null +++ b/components/google_play/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/google_play", + "version": "0.0.1", + "description": "Pipedream Google Play Components", + "main": "google_play.app.mjs", + "keywords": [ + "pipedream", + "google_play" + ], + "homepage": "https://pipedream.com/apps/google_play", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/google_postmaster_tools_api/README.md b/components/google_postmaster_tools_api/README.md new file mode 100644 index 0000000000000..67926723c018e --- /dev/null +++ b/components/google_postmaster_tools_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Google Postmaster Tools API offers insights into the performance and deliverability of emails sent to Gmail users. It provides data on spam rates, feedback loops, delivery errors, and more, allowing senders to optimize email campaigns and enhance inbox placement. In Pipedream, you can harness this data to automate monitoring, analyze sending patterns, and integrate email performance metrics with other services for comprehensive analysis and action. + +# Example Use Cases + +- **Monitor Domain Reputation**: Set up a Pipedream workflow to regularly check your domain's reputation score via the Google Postmaster Tools API. When the reputation falls below a certain threshold, automatically trigger an alert via email or Slack, prompting you to investigate and address potential issues. + +- **Aggregate Deliverability Stats**: Create a Pipedream workflow that fetches deliverability data from the API at the end of each day. Combine this with a data visualization tool like Google Sheets or Tableau to generate daily reports, offering clear insights into email performance trends over time. + +- **Automated Feedback Loop Response**: Set up a Pipedream workflow that listens for feedback loop notifications from the Google Postmaster Tools API. Use this data to automatically unsubscribe users or adjust email strategies in your Customer Relationship Management (CRM) system, such as Salesforce or HubSpot, to maintain a healthy sender reputation. diff --git a/components/google_postmaster_tools_api/common/constants.mjs b/components/google_postmaster_tools_api/common/constants.mjs new file mode 100644 index 0000000000000..d1b96d019adef --- /dev/null +++ b/components/google_postmaster_tools_api/common/constants.mjs @@ -0,0 +1,65 @@ +export const REPUTATION_OPTIONS = [ + { + label: "High", + value: "HIGH", + }, + { + label: "Medium", + value: "MEDIUM", + }, + { + label: "Low", + value: "LOW", + }, + { + label: "Bad", + value: "BAD", + }, +]; + +export const ERROR_OPTIONS = [ + { + value: "RATE_LIMIT_EXCEEDED", + label: + "The Domain or IP is sending traffic at a suspiciously high rate, due to which temporary rate limits have been imposed. The limit will be lifted when Gmail is confident enough of the nature of the traffic.", + }, + { + value: "SUSPECTED_SPAM", + label: + "The traffic is suspected to be spam, by Gmail, for various reasons.", + }, + { + value: "CONTENT_SPAMMY", + label: "The traffic is suspected to be spammy, specific to the content.", + }, + { + value: "BAD_ATTACHMENT", + label: "Traffic contains attachments not supported by Gmail.", + }, + { + value: "BAD_DMARC_POLICY", + label: "The sender domain has set up a DMARC rejection policy.", + }, + { + value: "LOW_IP_REPUTATION", + label: "The IP reputation of the sending IP is very low.", + }, + { + value: "LOW_DOMAIN_REPUTATION", + label: "The Domain reputation of the sending domain is very low.", + }, + { + value: "IP_IN_RBL", + label: + "The IP is listed in one or more public Real-time Blackhole Lists. Work with the RBL to get your IP delisted.", + }, + { + value: "DOMAIN_IN_RBL", + label: + "The Domain is listed in one or more public Real-time Blackhole Lists. Work with the RBL to get your domain delisted.", + }, + { + value: "BAD_PTR_RECORD", + label: "The sending IP is missing a PTR record.", + }, +]; diff --git a/components/google_postmaster_tools_api/google_postmaster_tools_api.app.mjs b/components/google_postmaster_tools_api/google_postmaster_tools_api.app.mjs new file mode 100644 index 0000000000000..a71a821d304c2 --- /dev/null +++ b/components/google_postmaster_tools_api/google_postmaster_tools_api.app.mjs @@ -0,0 +1,133 @@ +import { axios } from "@pipedream/platform"; +import { + ERROR_OPTIONS, REPUTATION_OPTIONS, +} from "./common/constants.mjs"; + +const percentageRatioText = "(can be a percentage, e.g. `10%` or a ratio between 0 and 1, e.g. `0.1`)"; + +export default { + type: "app", + app: "google_postmaster_tools_api", + propDefinitions: { + domain: { + type: "string", + label: "Domain Name", + description: "Select a domain or provide a custom domain name.", + async options({ prevContext: { pageToken } }) { + const response = await this.listDomains({ + params: { + pageToken, + }, + }); + const nextPageToken = response?.nextPageToken; + return { + context: { + pageToken: nextPageToken, + }, + options: response?.domains?.map(({ name }) => name.split("/").pop()) ?? [], + }; + }, + }, + matchAllFilters: { + type: "boolean", + label: "Match All Filters", + description: "If `true`, events will only be emitted if they match **all** of the selected criteria. The default behavior is matching any of them.", + optional: true, + default: false, + }, + ipReputation: { + type: "string[]", + label: "IP Reputation", + description: "Emit events only when there is at least one IP address in one of the specified categories", + options: REPUTATION_OPTIONS, + optional: true, + }, + domainReputation: { + type: "string[]", + label: "Domain Reputation", + description: "Emit events only when the domain reputation matches one of the specified categories", + options: REPUTATION_OPTIONS, + optional: true, + }, + userReportedSpamRatio: { + type: "string", + label: "User Reported Spam Ratio Greater Than or Equal To", + description: `Emit events only when the user reported spam ratio is greater than, or equal to, the specified value ${percentageRatioText}`, + optional: true, + }, + spfSuccessRatio: { + type: "string", + label: "SPF Success Ratio Less Than", + description: `Emit events only when the SPF success ratio is less than the specified value ${percentageRatioText}`, + optional: true, + }, + dkimSuccessRatio: { + type: "string", + label: "DKIM Success Ratio Less Than", + description: `Emit events only when the DKIM success ratio is less than the specified value ${percentageRatioText}`, + optional: true, + }, + dmarcSuccessRatio: { + type: "string", + label: "DMARC Success Ratio Less Than", + description: `Emit events only when the DMARC success ratio is less than the specified value ${percentageRatioText}`, + optional: true, + }, + outboundEncryptionRatio: { + type: "string", + label: "Outbound Encryption Ratio Less Than", + description: `Emit events only when the outbound encryption ratio is less than the specified value ${percentageRatioText}`, + optional: true, + }, + inboundEncryptionRatio: { + type: "string", + label: "Inbound Encryption Ratio Less Than", + description: `Emit events only when the inbound encryption ratio is less than the specified value ${percentageRatioText}`, + optional: true, + }, + errorRatio: { + type: "string", + label: "Error Ratio Greater Than or Equal To", + description: `Emit events only when the error ratio (in the specified \`Error Categories\`, or any if not specified) is greater than, or equal to, the specified value ${percentageRatioText}`, + optional: true, + }, + errorCategories: { + type: "string", + label: "Error Categories", + description: "Emit events only when one of the specified categories equals or exceeds the specified `Error Ratio`", + options: ERROR_OPTIONS, + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://gmailpostmastertools.googleapis.com/v1"; + }, + async _makeRequest({ + $ = this, headers, ...otherOpts + }) { + return axios($, { + ...otherOpts, + baseURL: this._baseUrl(), + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + async listDomains(args) { + return this._makeRequest({ + url: "/domains", + ...args, + }); + }, + async getDomainTrafficStats({ + domainName, ...args + }) { + return this._makeRequest({ + url: `/domains/${domainName}/trafficStats`, + ...args, + }); + }, + }, +}; diff --git a/components/google_postmaster_tools_api/package.json b/components/google_postmaster_tools_api/package.json new file mode 100644 index 0000000000000..c513ba6797504 --- /dev/null +++ b/components/google_postmaster_tools_api/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/google_postmaster_tools_api", + "version": "0.2.0", + "description": "Pipedream Google Postmaster Tools Components", + "main": "google_postmaster_tools_api.app.mjs", + "keywords": [ + "pipedream", + "google_postmaster_tools_api" + ], + "homepage": "https://pipedream.com/apps/google_postmaster_tools_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/google_postmaster_tools_api/sources/new-matched-traffic-stats/new-matched-traffic-stats.mjs b/components/google_postmaster_tools_api/sources/new-matched-traffic-stats/new-matched-traffic-stats.mjs new file mode 100644 index 0000000000000..85acbd081f22c --- /dev/null +++ b/components/google_postmaster_tools_api/sources/new-matched-traffic-stats/new-matched-traffic-stats.mjs @@ -0,0 +1,256 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import googlePostmasterToolsApi from "../../google_postmaster_tools_api.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "google_postmaster_tools_api-new-matched-traffic-stats", + name: "New Matched Traffic Stats", + description: + "Emits a new event when traffic stats match certain criteria. [See the documentation](https://developers.google.com/gmail/postmaster/reference/rest)", + version: "0.1.0", + type: "source", + dedupe: "unique", + sampleEmit, + props: { + googlePostmasterToolsApi, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + domain: { + propDefinition: [ + googlePostmasterToolsApi, + "domain", + ], + }, + filterInfo: { + type: "alert", + alertType: "info", + content: "By default, events will be emitted when matching **any** of the configured filters. If you want **all** configured filters to be required instead, you can use the `Match All Filters` prop below.", + }, + matchAllFilters: { + propDefinition: [ + googlePostmasterToolsApi, + "matchAllFilters", + ], + }, + ipReputation: { + propDefinition: [ + googlePostmasterToolsApi, + "ipReputation", + ], + }, + domainReputation: { + propDefinition: [ + googlePostmasterToolsApi, + "domainReputation", + ], + }, + userReportedSpamRatio: { + propDefinition: [ + googlePostmasterToolsApi, + "userReportedSpamRatio", + ], + }, + spfSuccessRatio: { + propDefinition: [ + googlePostmasterToolsApi, + "spfSuccessRatio", + ], + }, + dkimSuccessRatio: { + propDefinition: [ + googlePostmasterToolsApi, + "dkimSuccessRatio", + ], + }, + dmarcSuccessRatio: { + propDefinition: [ + googlePostmasterToolsApi, + "dmarcSuccessRatio", + ], + }, + outboundEncryptionRatio: { + propDefinition: [ + googlePostmasterToolsApi, + "outboundEncryptionRatio", + ], + }, + inboundEncryptionRatio: { + propDefinition: [ + googlePostmasterToolsApi, + "inboundEncryptionRatio", + ], + }, + errorRatio: { + propDefinition: [ + googlePostmasterToolsApi, + "errorRatio", + ], + }, + errorCategories: { + propDefinition: [ + googlePostmasterToolsApi, + "errorCategories", + ], + }, + }, + methods: { + _getSavedItems() { + return this.db.get("savedItems") ?? []; + }, + _setSavedItems(value) { + this.db.set("savedItems", value); + }, + getDateValues(date) { + const [ + year, + month, + day, + ] = date.toISOString().split("T")[0].split("-").map((i) => Number(i)); + return { + year, + month, + day, + }; + }, + async getTrafficStats() { + const today = new Date(); + const oneDayAgo = new Date(today); + oneDayAgo.setDate(oneDayAgo.getDate() - 7); + const endDate = this.getDateValues(today); + const startDate = this.getDateValues(oneDayAgo); + + return this.googlePostmasterToolsApi.getDomainTrafficStats({ + domainName: this.domain, + params: { + "startDate.day": startDate.day, + "startDate.month": startDate.month, + "startDate.year": startDate.year, + "endDate.day": endDate.day, + "endDate.month": endDate.month, + "endDate.year": endDate.year, + }, + }); + }, + filterIpReputation(item) { + let { ipReputation } = this; + if (typeof ipReputation === "string") + ipReputation = ipReputation.split(","); + if (!ipReputation?.length) return undefined; + return item.ipReputations.some( + ({ + reputation, ipCount, + }) => + ipReputation.includes(reputation) && ipCount > 0, + ); + }, + filterDomainReputation(item) { + let { domainReputation } = this; + if (typeof domainReputation === "string") + domainReputation = domainReputation.split(","); + if (!domainReputation?.length) return undefined; + return domainReputation.includes(item.domainReputation); + }, + filterRatio(prop, value, greaterOrEqual = false) { + if (prop?.endsWith("%")) prop = Number(prop.slice(0, -1)) / 100; + const ratio = Number(prop); + if (isNaN(ratio) || value === undefined) return undefined; + return greaterOrEqual + ? value >= ratio + : value < ratio; + }, + filterSpamRatio(item) { + return this.filterRatio( + this.userReportedSpamRatio, + item.userReportedSpamRatio, + true, + ); + }, + filterSpfSuccessRatio(item) { + return this.filterRatio(this.spfSuccessRatio, item.spfSuccessRatio); + }, + filterDkimSuccessRatio(item) { + return this.filterRatio(this.dkimSuccessRatio, item.dkimSuccessRatio); + }, + filterDmarcSuccessRatio(item) { + return this.filterRatio(this.dmarcSuccessRatio, item.dmarcSuccessRatio); + }, + filterOutboundEncryptionRatio(item) { + return this.filterRatio( + this.outboundEncryptionRatio, + item.outboundEncryptionRatio, + ); + }, + filterInboundEncryptionRatio(item) { + return this.filterRatio( + this.inboundEncryptionRatio, + item.inboundEncryptionRatio, + ); + }, + filterErrorRatio(item) { + const ratio = Number(this.errorRatio); + if (isNaN(ratio)) return undefined; + + let { errorCategories } = this; + if (typeof errorCategories === "string") + errorCategories = errorCategories.split(","); + + return item.deliveryErrors.some( + ({ + errorType, errorRatio, + }) => + (!errorCategories || errorCategories.includes(errorType)) && errorRatio > ratio, + ); + }, + matchesCriteria(item) { + // Filters return undefined if the prop is not set, or true/false otherwise + // Filters are a logical AND - if any filter returns false, the item is not emitted + const filters = [ + this.filterIpReputation, + this.filterDomainReputation, + this.filterSpamRatio, + this.filterSpfSuccessRatio, + this.filterDkimSuccessRatio, + this.filterDmarcSuccessRatio, + this.filterOutboundEncryptionRatio, + this.filterInboundEncryptionRatio, + this.filterErrorRatio, + ]; + + let hasMatch = false; + + for (let filter of filters) { + const result = filter(item); + if (result === true) { + if (!this.matchAllFilters) return true; + hasMatch = true; + } else if (result === false && this.matchAllFilters) { + return false; + } + } + + return hasMatch; + }, + }, + async run() { + const savedItems = this._getSavedItems(); + const ts = Date.now(); + const stats = await this.getTrafficStats(); + stats?.trafficStats?.filter(({ name }) => !savedItems.includes(name)).forEach((item) => { + const id = item.name; + if (this.matchesCriteria(item)) { + this.$emit(item, { + id, + summary: `Matched Traffic Stats: ${id}`, + ts, + }); + savedItems.push(id); + } + }); + this._setSavedItems(savedItems); + }, +}; diff --git a/components/google_postmaster_tools_api/sources/new-matched-traffic-stats/test-event.mjs b/components/google_postmaster_tools_api/sources/new-matched-traffic-stats/test-event.mjs new file mode 100644 index 0000000000000..776a5b5c31236 --- /dev/null +++ b/components/google_postmaster_tools_api/sources/new-matched-traffic-stats/test-event.mjs @@ -0,0 +1,45 @@ +export default { + name: "domains/domain.com/trafficStats/20240412", + userReportedSpamRatio: 0.001, + ipReputations: [ + { + reputation: "BAD", + }, + { + reputation: "LOW", + }, + { + reputation: "MEDIUM", + }, + { + reputation: "HIGH", + ipCount: "13", + sampleIps: [ + "51.345.124.153", + "51.345.7.112", + "51.345.7.114", + "51.345.7.30-51.345.7.33", + "51.345.7.35-51.345.7.37", + "51.345.7.46", + "51.345.7.92", + "51.345.7.99", + ], + }, + ], + domainReputation: "MEDIUM", + spfSuccessRatio: 1, + dkimSuccessRatio: 1, + dmarcSuccessRatio: 1, + inboundEncryptionRatio: 1, + deliveryErrors: [ + { + errorClass: "TEMPORARY_ERROR", + errorType: "SUSPECTED_SPAM", + }, + { + errorClass: "PERMANENT_ERROR", + errorType: "BAD_ATTACHMENT", + errorRatio: 0.002, + }, + ], +}; diff --git a/components/google_recaptcha/README.md b/components/google_recaptcha/README.md index 4f8db31b69aec..d498fe19eb9c2 100644 --- a/components/google_recaptcha/README.md +++ b/components/google_recaptcha/README.md @@ -1,15 +1,11 @@ # Overview -The Google reCAPTCHA API allows you to build various types of CAPTCHA forms, -including: +Google reCAPTCHA API helps you detect abusive traffic on your website without any user friction. It does this by presenting CAPTCHA challenges to users they need to solve before submitting forms, ensuring these actions are not generated by bots. The API returns a score, helping you decide what action to take for your website: for instance, you might require further verification for low-scoring users. With Pipedream's ability to integrate with various APIs, you can create custom workflows that trigger upon the result of a reCAPTCHA verification, streamlining both security and user experience. -- Simple CAPTCHA forms that can be used to protect your website from spam and - abuse -- Advanced CAPTCHA forms that can be used to protect your website from more - sophisticated attacks +# Example Use Cases -Here are some examples of what you can build using the Google reCAPTCHA API: +- **User Verification Workflow**: After a user completes a reCAPTCHA challenge on your site, use Pipedream to send the token to Google's reCAPTCHA API for verification. Depending on the score, trigger different actions: for high scores, proceed with form submission; for low scores, prompt additional verification or log the attempt for review. -- A simple CAPTCHA form that can be used to protect your website from spam -- An advanced CAPTCHA form that can be used to protect your website from more - sophisticated attacks +- **Anti-Fraud Order Processing**: Integrate reCAPTCHA verification with an e-commerce platform like Shopify. Before processing a payment, verify the user with reCAPTCHA. If the score is high, automate the order process; if it's low, flag the order and alert your fraud prevention team for manual review. + +- **Automated Content Moderation**: For a user-generated content platform, implement reCAPTCHA to vet submissions. Use Pipedream workflows to verify submissions and based on reCAPTCHA scores, either automatically approve posts, queue them for moderation, or block them. Link this with a content management system (CMS) like WordPress for seamless operation. diff --git a/components/google_safebrowsing/README.md b/components/google_safebrowsing/README.md new file mode 100644 index 0000000000000..363f1ed70a629 --- /dev/null +++ b/components/google_safebrowsing/README.md @@ -0,0 +1,11 @@ +# Overview + +The Google Safe Browsing API lets you check URLs against Google's constantly updated lists of unsafe web resources. These include social engineering sites (like phishing and deceptive sites) and sites that host malware or unwanted software. Within Pipedream, you can leverage this API to automate the process of scanning URLs in various contexts, such as user-generated content, emails, or within applications that require high-security measures. + +# Example Use Cases + +- **Automated URL Check in User Submissions:** Create a Pipedream workflow that automatically checks URLs submitted by users through a form or a chatbot. If the URL is flagged as unsafe, the workflow can alert a moderator or log the incident for further review. + +- **Email Security Scanning:** Build a Pipedream workflow that scans links in incoming emails using the Google Safe Browsing API. Connect this with your email service to automatically quarantine or flag emails containing malicious links, enhancing your email security. + +- **Link Validation in Content Publishing:** Integrate the Google Safe Browsing API in a Pipedream workflow that validates links in content before publishing on your website. This can ensure that external links in blog posts or articles do not lead to harmful sites, protecting your readers and your site's reputation. diff --git a/components/google_search_console/README.md b/components/google_search_console/README.md index 392d590a21005..cada189f1f1ec 100644 --- a/components/google_search_console/README.md +++ b/components/google_search_console/README.md @@ -1,10 +1,11 @@ # Overview -You can use the Google Search Console API to build a variety of tools and -applications. Here are a few examples: - -- A tool that fetches data from the Google Search Console API and displays it - in a user-friendly interface. -- A dashboard that visualizes data fetched from the Google Search Console API. -- A tool that allows users to input a query and receive results from the Google - Search Console API in real-time. +The Google Search Console API opens a treasure trove of data and insights about your website’s presence in Google Search results. You can get detailed reports on your site's search traffic, manage and test your site's sitemaps and robots.txt files, and see which queries bring users to your site. On Pipedream, utilize this API to automate checks on site performance, integrate with other tools for deeper analysis, or keep tabs on your SEO strategy's effectiveness. + +# Example Use Cases + +- **SEO Performance Report to Slack**: Automate daily or weekly SEO performance reports. Use the Google Search Console API to fetch search analytics data, then send a summary report to a Slack channel, keeping the team informed about trends, keyword rankings, and click-through rates. + +- **Sync Search Results with Google Sheets**: Create a workflow that periodically pulls data from the Google Search Console API and adds it to a Google Sheet. This is useful for maintaining an evolving dataset for deeper analysis, historical reference, or sharing insights across teams without giving direct access to the Search Console. + +- **Automatic Sitemap Submission**: Set up a Pipedream workflow that triggers whenever a new sitemap is generated in your content management system (CMS). The workflow can then automatically submit the sitemap to Google Search Console via API, ensuring Google has the latest structure of your site for crawling and indexing. diff --git a/components/google_search_console/package.json b/components/google_search_console/package.json new file mode 100644 index 0000000000000..21aa00a5f7282 --- /dev/null +++ b/components/google_search_console/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/google_search_console", + "version": "0.6.0", + "description": "Pipedream google_search_console Components", + "main": "google_search_console.app.mjs", + "keywords": [ + "pipedream", + "google_search_console" + ], + "homepage": "https://pipedream.com/apps/google_search_console", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/google_sheets/README.md b/components/google_sheets/README.md index aab19972afb98..3ea3e1fc35b59 100644 --- a/components/google_sheets/README.md +++ b/components/google_sheets/README.md @@ -1,9 +1,11 @@ # Overview -Some examples of things you can build using the Google Sheets API include: +The Google Sheets API allows for the creation, reading, updating, and deletion of data within Google Sheets, enabling a robust platform for spreadsheet management and data manipulation. Through Pipedream, you can craft serverless workflows that respond to various triggers, such as webhook events, emails, or scheduled times, to interact with Google Sheets. This synergy can automate reporting, synchronize data across applications, manage inventory, track leads in a CRM, or even conduct survey analysis by updating and retrieving sheet data on the fly. -- A web app that lets users input data into a Google Sheet -- A script that automatically updates a Google Sheet with data from another - source -- A tool that generates graphs and charts from data in a Google Sheet -- A service that sends data from a Google Sheet to another API or application +# Example Use Cases + +- **Automated Data Entry**: Streamline data collection from various sources like forms or customer interactions directly into Google Sheets. For instance, capture data from Typeform submissions and append them as new rows in a spreadsheet for analysis or record-keeping. + +- **Real-time CRM Update**: Keep your CRM and Google Sheets in sync. Whenever a new contact is added to HubSpot, automatically add their details to a designated Google Sheet. This ensures that sales data are always up-to-date and available for team collaboration or reporting. + +- **Inventory Management**: Connect Stripe to Google Sheets to monitor product sales. Each time a sale is made, the workflow deducts the sold quantity from the inventory list in a Google Sheet, allowing for real-time inventory tracking and alerts when stock levels are low. diff --git a/components/google_sheets/actions/add-column/add-column.mjs b/components/google_sheets/actions/add-column/add-column.mjs new file mode 100644 index 0000000000000..881a1ee3fd0f5 --- /dev/null +++ b/components/google_sheets/actions/add-column/add-column.mjs @@ -0,0 +1,92 @@ +import googleSheets from "../../google_sheets.app.mjs"; + +export default { + key: "google_sheets-add-column", + name: "Create Column", + description: "Create a new column in a spreadsheet. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/batchUpdate)", + version: "0.1.7", + type: "action", + props: { + googleSheets, + drive: { + propDefinition: [ + googleSheets, + "watchedDrive", + ], + description: "The drive containing the spreadsheet to edit. If you are connected with any [Google Shared Drives](https://support.google.com/a/users/answer/9310351), you can select it here.", + }, + sheetId: { + propDefinition: [ + googleSheets, + "sheetID", + (c) => ({ + driveId: googleSheets.methods.getDriveId(c.drive), + }), + ], + }, + worksheetId: { + propDefinition: [ + googleSheets, + "worksheetIDs", + (c) => ({ + sheetId: c.sheetId, + }), + ], + type: "string", + label: "Worksheet", + }, + column: { + propDefinition: [ + googleSheets, + "column", + ], + description: "Insert new column to the RIGHT of this column. Leave blank to insert at start of sheet", + optional: true, + default: "", + }, + }, + methods: { + async getColumnCount() { + const sheets = (await this.googleSheets.getSpreadsheet(this.sheetId)).sheets + .filter((s) => s.properties.sheetId == this.worksheetId); + return sheets[0].properties.gridProperties.columnCount; + }, + }, + async run() { + const colIndex = this.googleSheets._getColumnIndex(this.column); + const colCount = await this.getColumnCount(); + const requests = []; + // if inserting a column outside of the grid limits, append to end + if (colIndex >= colCount) { + requests.push([ + { + appendDimension: { + sheetId: this.worksheetId, + dimension: "COLUMNS", + length: 1, + }, + }, + ]); + } else { + requests.push([ + { + insertRange: { + range: { + sheetId: this.worksheetId, + startColumnIndex: colIndex, + endColumnIndex: colIndex + 1, + }, + shiftDimension: "COLUMNS", + }, + }, + ]); + } + const request = { + spreadsheetId: this.sheetId, + requestBody: { + requests, + }, + }; + return await this.googleSheets.batchUpdate(request); + }, +}; diff --git a/components/google_sheets/actions/add-multiple-rows/add-multiple-rows.mjs b/components/google_sheets/actions/add-multiple-rows/add-multiple-rows.mjs index fd91fab6a37dc..3b746478915b0 100644 --- a/components/google_sheets/actions/add-multiple-rows/add-multiple-rows.mjs +++ b/components/google_sheets/actions/add-multiple-rows/add-multiple-rows.mjs @@ -1,10 +1,17 @@ -import googleSheets from "../../google_sheets.app.mjs"; +import common from "../common/worksheet.mjs"; +import { ConfigurationError } from "@pipedream/platform"; +import { + parseArray, getWorksheetHeaders, +} from "../../common/utils.mjs"; + +const { googleSheets } = common.props; export default { + ...common, key: "google_sheets-add-multiple-rows", name: "Add Multiple Rows", - description: "Add multiple rows of data to a Google Sheet", - version: "0.2.2", + description: "Add multiple rows of data to a Google Sheet. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/append)", + version: "0.2.10", type: "action", props: { googleSheets, @@ -23,14 +30,23 @@ export default { }), ], }, - sheetName: { + worksheetId: { propDefinition: [ googleSheets, - "sheetName", + "worksheetIDs", (c) => ({ sheetId: c.sheetId, }), ], + type: "string", + label: "Worksheet Id", + reloadProps: true, + }, + headersDisplay: { + propDefinition: [ + googleSheets, + "headersDisplay", + ], }, rows: { propDefinition: [ @@ -38,6 +54,12 @@ export default { "rows", ], }, + rowsDescription: { + propDefinition: [ + googleSheets, + "rowsDescription", + ], + }, resetRowFormat: { type: "boolean", label: "Reset Row Format", @@ -45,16 +67,30 @@ export default { optional: true, }, }, + async additionalProps() { + const props = {}; + if (!this.sheetId || !this.worksheetId) { + return props; + } + const worksheet = await this.getWorksheetById(this.sheetId, this.worksheetId); + const rowHeaders = await getWorksheetHeaders(this, this.sheetId, worksheet?.properties?.title); + if (rowHeaders.length) { + return { + headersDisplay: { + type: "alert", + alertType: "info", + content: `Possible Row Headers: **\`${rowHeaders.join(", ")}\`**`, + hidden: false, + }, + }; + } + }, async run() { - let rows = this.rows; - let inputValidated = true; - if (!Array.isArray(rows)) { - rows = JSON.parse(this.rows); - } + const rows = parseArray(this.rows); - if (!rows || !rows.length || !Array.isArray(rows)) { + if (!rows) { inputValidated = false; } else { rows.forEach((row) => { if (!Array.isArray(row)) { inputValidated = false; } }); @@ -64,12 +100,13 @@ export default { if (!inputValidated) { console.error("Data Submitted:"); console.error(rows); - throw new Error("Rows data is not an array of arrays. Please enter an array of arrays in the `Rows` parameter above. If you're trying to send a single rows to Google Sheets, search for the action to add a single row to Sheets or try modifying the code for this step."); + throw new ConfigurationError("Rows data is not an array of arrays. Please enter an array of arrays in the `Rows` parameter above. If you're trying to send a single rows to Google Sheets, search for the action to add a single row to Sheets or try modifying the code for this step."); } + const worksheet = await this.getWorksheetById(this.sheetId, this.worksheetId); const addRowsResponse = await this.googleSheets.addRowsToSheet({ spreadsheetId: this.sheetId, - range: this.sheetName, + range: worksheet?.properties?.title, rows, }); diff --git a/components/google_sheets/actions/add-single-row/add-single-row.mjs b/components/google_sheets/actions/add-single-row/add-single-row.mjs index 2f6e7a14f0555..779834cb319d4 100644 --- a/components/google_sheets/actions/add-single-row/add-single-row.mjs +++ b/components/google_sheets/actions/add-single-row/add-single-row.mjs @@ -1,11 +1,16 @@ -import googleSheets from "../../google_sheets.app.mjs"; +import common from "../common/worksheet.mjs"; import { ConfigurationError } from "@pipedream/platform"; +import { parseArray } from "../../common/utils.mjs"; +import { isDynamicExpression } from "../common/worksheet.mjs"; + +const { googleSheets } = common.props; export default { + ...common, key: "google_sheets-add-single-row", name: "Add Single Row", - description: "Add a single row of data to Google Sheets", - version: "2.1.2", + description: "Add a single row of data to Google Sheets. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/append)", + version: "2.1.12", type: "action", props: { googleSheets, @@ -23,46 +28,92 @@ export default { driveId: googleSheets.methods.getDriveId(c.drive), }), ], - description: "", - withLabel: true, + reloadProps: true, }, - sheetName: { + worksheetId: { propDefinition: [ googleSheets, - "sheetName", + "worksheetIDs", (c) => ({ sheetId: c.sheetId?.value || c.sheetId, }), ], - description: "", - }, - hasHeaders: { - type: "string", - label: "Does the first row of the sheet have headers?", - description: "If the first row of your document has headers we'll retrieve them to make it easy to enter the value for each column.", - options: [ - "Yes", - "No", - ], + description: "Select a worksheet or enter a custom expression. When referencing a spreadsheet dynamically, you must provide a custom expression for the worksheet.", + async options({ sheetId }) { + // If sheetId is a dynamic reference, don't load options + if (isDynamicExpression(sheetId)) { + return []; + } + + // Otherwise, call the original options function with the correct context + const origOptions = googleSheets.propDefinitions.worksheetIDs.options; + return origOptions.call(this, { + sheetId, + }); + }, reloadProps: true, }, + hasHeaders: common.props.hasHeaders, }, async additionalProps() { - const sheetId = this.sheetId?.value || this.sheetId; + const { + sheetId, + worksheetId, + hasHeaders, + } = this; + + // If using dynamic expressions for either sheetId or worksheetId, return only array input + if (isDynamicExpression(sheetId) || isDynamicExpression(worksheetId)) { + return { + myColumnData: { + type: "string[]", + label: "Values", + description: "Provide a value for each cell of the row. Google Sheets accepts strings, numbers and boolean values for each cell. To set a cell to an empty value, pass an empty string.", + }, + }; + } + const props = {}; - if (this.hasHeaders === "Yes") { - const { values } = await this.googleSheets.getSpreadsheetValues(sheetId, `${this.sheetName}!1:1`); - if (!values[0]?.length) { - throw new ConfigurationError("Cound not find a header row. Please either add headers and click \"Refresh fields\" or adjust the action configuration to continue."); - } - for (let i = 0; i < values[0]?.length; i++) { - props[`col_${i.toString().padStart(4, "0")}`] = { + if (hasHeaders) { + try { + const worksheet = await this.getWorksheetById(sheetId, worksheetId); + const { values } = await this.googleSheets.getSpreadsheetValues(sheetId, `${worksheet?.properties?.title}!1:1`); + + if (!values?.[0]?.length) { + throw new ConfigurationError("Could not find a header row. Please either add headers and click \"Refresh fields\" or set 'Does the first row of the sheet have headers?' to false."); + } + + for (let i = 0; i < values[0]?.length; i++) { + props[`col_${i.toString().padStart(4, "0")}`] = { + type: "string", + label: values[0][i], + optional: true, + }; + } + props.allColumns = { type: "string", - label: values[0][i], - optional: true, + hidden: true, + default: JSON.stringify(values), + }; + } catch (err) { + console.error("Error fetching headers:", err); + // Fallback to basic column input if headers can't be fetched + return { + headerError: { + type: "string", + label: "Header Fetch Error", + description: `Unable to fetch headers: ${err.message}. Using simple column input instead.`, + optional: true, + hidden: true, + }, + myColumnData: { + type: "string[]", + label: "Values", + description: "Provide a value for each cell of the row. Google Sheets accepts strings, numbers and boolean values for each cell. To set a cell to an empty value, pass an empty string.", + }, }; } - } else if (this.hasHeaders === "No") { + } else { props.myColumnData = { type: "string[]", label: "Values", @@ -72,12 +123,24 @@ export default { return props; }, async run({ $ }) { - const sheetId = this.sheetId?.value || this.sheetId; + const { + sheetId, + worksheetId, + } = this; + + const { name: sheetName } = await this.googleSheets.getFile(sheetId, { + fields: "name", + }); + + const worksheet = await this.getWorksheetById(sheetId, worksheetId); + let cells; - if (this.hasHeaders === "Yes") { - // TODO: If we could create a variable using this.allColumns in additionalProps, we dont need - // to call getSpreadsheetValues here again. - const { values: rows } = await this.googleSheets.getSpreadsheetValues(sheetId, `${this.sheetName}!1:1`); + if (this.hasHeaders + && !isDynamicExpression(sheetId) + && !isDynamicExpression(worksheetId) + && this.allColumns + ) { + const rows = JSON.parse(this.allColumns); const [ headers, ] = rows; @@ -85,32 +148,35 @@ export default { .map((_, i) => `col_${i.toString().padStart(4, "0")}`) .map((column) => this[column] ?? ""); } else { + // For dynamic references or no headers, use the array input cells = this.googleSheets.sanitizedArray(this.myColumnData); } - // validate input + // Validate input if (!cells || !cells.length) { - throw new Error("Please enter an array of elements in `Cells / Column Values`."); - } else if (!Array.isArray(cells)) { - throw new Error("Cell / Column data is not an array. Please enter an array of elements in `Cells / Column Values`."); + throw new ConfigurationError("Please enter an array of elements in `Cells / Column Values`."); + } + cells = parseArray(cells); + if (!cells) { + throw new ConfigurationError("Cell / Column data is not an array. Please enter an array of elements in `Cells / Column Values`."); } else if (Array.isArray(cells[0])) { - throw new Error("Cell / Column data is a multi-dimensional array. A one-dimensional is expected. If you're trying to send multiple rows to Google Sheets, search for the action to add multiple rows to Sheets."); + throw new ConfigurationError("Cell / Column data is a multi-dimensional array. A one-dimensional is expected. If you're trying to send multiple rows to Google Sheets, search for the action to add multiple rows to Sheets."); } const { - arr, + arr: sanitizedCells, convertedIndexes, } = this.googleSheets.arrayValuesToString(cells); const data = await this.googleSheets.addRowsToSheet({ spreadsheetId: sheetId, - range: this.sheetName, + range: worksheet?.properties?.title, rows: [ - arr, + sanitizedCells, ], }); - let summary = `Added 1 row to [${this.sheetId?.label || this.sheetId} (${data.updatedRange})](https://docs.google.com/spreadsheets/d/${sheetId}).`; + let summary = `Added 1 row to [${sheetName || sheetId} (${data.updatedRange})](https://docs.google.com/spreadsheets/d/${sheetId}).`; if (convertedIndexes.length > 0) { summary += " We detected something other than a string/number/boolean in at least one of the fields and automatically converted it to a string."; } diff --git a/components/google_sheets/actions/clear-cell/clear-cell.mjs b/components/google_sheets/actions/clear-cell/clear-cell.mjs index 7107416f41197..d61877701cede 100644 --- a/components/google_sheets/actions/clear-cell/clear-cell.mjs +++ b/components/google_sheets/actions/clear-cell/clear-cell.mjs @@ -1,10 +1,13 @@ -import googleSheets from "../../google_sheets.app.mjs"; +import common from "../common/worksheet.mjs"; + +const { googleSheets } = common.props; export default { + ...common, key: "google_sheets-clear-cell", name: "Clear Cell", - description: "Delete the content of a specific cell in a spreadsheet", - version: "0.1.3", + description: "Delete the content of a specific cell in a spreadsheet. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/clear)", + version: "0.1.11", type: "action", props: { googleSheets, @@ -24,14 +27,16 @@ export default { }), ], }, - sheetName: { + worksheetId: { propDefinition: [ googleSheets, - "sheetName", + "worksheetIDs", (c) => ({ sheetId: c.sheetId, }), ], + type: "string", + label: "Worksheet Id", }, cell: { type: "string", @@ -40,9 +45,10 @@ export default { }, }, async run() { + const worksheet = await this.getWorksheetById(this.sheetId, this.worksheetId); const request = { spreadsheetId: this.sheetId, - range: `${this.sheetName}!${this.cell}`, + range: `${worksheet?.properties?.title}!${this.cell}`, }; return await this.googleSheets.clearSheetValues(request); }, diff --git a/components/google_sheets/actions/clear-row/clear-row.mjs b/components/google_sheets/actions/clear-row/clear-row.mjs deleted file mode 100644 index 0c0dfcd1cba7d..0000000000000 --- a/components/google_sheets/actions/clear-row/clear-row.mjs +++ /dev/null @@ -1,50 +0,0 @@ -import googleSheets from "../../google_sheets.app.mjs"; - -export default { - key: "google_sheets-clear-row", - name: "Clear Row", - description: "Delete the content of a row in a spreadsheet. Deleted rows will appear as blank rows.", - version: "0.1.2", - type: "action", - props: { - googleSheets, - drive: { - propDefinition: [ - googleSheets, - "watchedDrive", - ], - description: "The drive containing the spreadsheet to edit. If you are connected with any [Google Shared Drives](https://support.google.com/a/users/answer/9310351), you can select it here.", - }, - sheetId: { - propDefinition: [ - googleSheets, - "sheetID", - (c) => ({ - driveId: googleSheets.methods.getDriveId(c.drive), - }), - ], - }, - sheetName: { - propDefinition: [ - googleSheets, - "sheetName", - (c) => ({ - sheetId: c.sheetId, - }), - ], - }, - row: { - propDefinition: [ - googleSheets, - "row", - ], - }, - }, - async run() { - const request = { - spreadsheetId: this.sheetId, - range: `${this.sheetName}!${this.row}:${this.row}`, - }; - return await this.googleSheets.clearSheetValues(request); - }, -}; diff --git a/components/google_sheets/actions/clear-rows/clear-rows.mjs b/components/google_sheets/actions/clear-rows/clear-rows.mjs new file mode 100644 index 0000000000000..628e824d6b518 --- /dev/null +++ b/components/google_sheets/actions/clear-rows/clear-rows.mjs @@ -0,0 +1,61 @@ +import common from "../common/worksheet.mjs"; + +const { googleSheets } = common.props; + +export default { + ...common, + key: "google_sheets-clear-rows", + name: "Clear Rows", + description: "Delete the content of a row or rows in a spreadsheet. Deleted rows will appear as blank rows. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/clear)", + version: "0.1.9", + type: "action", + props: { + googleSheets, + drive: { + propDefinition: [ + googleSheets, + "watchedDrive", + ], + description: "The drive containing the spreadsheet to edit. If you are connected with any [Google Shared Drives](https://support.google.com/a/users/answer/9310351), you can select it here.", + }, + sheetId: { + propDefinition: [ + googleSheets, + "sheetID", + (c) => ({ + driveId: googleSheets.methods.getDriveId(c.drive), + }), + ], + }, + worksheetId: { + propDefinition: [ + googleSheets, + "worksheetIDs", + (c) => ({ + sheetId: c.sheetId, + }), + ], + type: "string", + label: "Worksheet Id", + }, + startIndex: { + type: "integer", + label: "Start Index", + description: "Row number of the start (inclusive) of the range of rows to clear", + }, + endIndex: { + type: "integer", + label: "End Index", + description: "Row number of the end (exclusive) of the range of rows to clear", + optional: true, + }, + }, + async run() { + const worksheet = await this.getWorksheetById(this.sheetId, this.worksheetId); + const request = { + spreadsheetId: this.sheetId, + range: `${worksheet?.properties?.title}!${this.startIndex}:${this.endIndex || this.startIndex}`, + }; + return await this.googleSheets.clearSheetValues(request); + }, +}; diff --git a/components/google_sheets/actions/common/worksheet.mjs b/components/google_sheets/actions/common/worksheet.mjs new file mode 100644 index 0000000000000..47a082d112814 --- /dev/null +++ b/components/google_sheets/actions/common/worksheet.mjs @@ -0,0 +1,32 @@ +import googleSheets from "../../google_sheets.app.mjs"; + +// Helper function to check if a value is a dynamic expression +export const isDynamicExpression = (value) => { + if (!value || typeof value !== "string") return false; + return value.trim().startsWith("{{") && value.trim().endsWith("}}"); +}; + +export default { + props: { + googleSheets, + hasHeaders: { + type: "boolean", + label: "Does the first row of the sheet have headers?", + description: "If the first row of your document has headers, we'll retrieve them to make it easy to enter the value for each column. Note: When using a dynamic reference for the worksheet ID (e.g. `{{steps.foo.$return_value}}`), this setting is ignored.", + reloadProps: true, + }, + }, + methods: { + async getWorksheetById(sheetId, worksheetId) { + try { + const { sheets } = await this.googleSheets.getSpreadsheet(sheetId); + return sheets + .find(({ properties: { sheetId } }) => String(sheetId) === String(worksheetId)); + } catch (error) { + const errorMsg = `Error fetching worksheet with ID \`${worksheetId}\` - ${error.message}`; + console.log(error); + throw new Error(errorMsg); + } + }, + }, +}; diff --git a/components/google_sheets/actions/copy-worksheet/copy-worksheet.mjs b/components/google_sheets/actions/copy-worksheet/copy-worksheet.mjs index e27170e663e45..135af86446e50 100644 --- a/components/google_sheets/actions/copy-worksheet/copy-worksheet.mjs +++ b/components/google_sheets/actions/copy-worksheet/copy-worksheet.mjs @@ -3,8 +3,8 @@ import googleSheets from "../../google_sheets.app.mjs"; export default { key: "google_sheets-copy-worksheet", name: "Copy Worksheet", - description: "Copy an existing worksheet to another Google Sheets file", - version: "0.1.2", + description: "Copy an existing worksheet to another Google Sheets file. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.sheets/copyTo)", + version: "0.1.7", type: "action", props: { googleSheets, @@ -45,7 +45,7 @@ export default { driveId: googleSheets.methods.getDriveId(c.drive), }), ], - description: "The spreadsheet to copy the worksheetsheet to", + description: "The spreadsheet to copy the worksheet to", }, }, async run() { diff --git a/components/google_sheets/actions/create-column/create-column.mjs b/components/google_sheets/actions/create-column/create-column.mjs deleted file mode 100644 index 56a076bd51fac..0000000000000 --- a/components/google_sheets/actions/create-column/create-column.mjs +++ /dev/null @@ -1,92 +0,0 @@ -import googleSheets from "../../google_sheets.app.mjs"; - -export default { - key: "google_sheets-create-column", - name: "Create Column", - description: "Create a new column in a spreadsheet", - version: "0.1.2", - type: "action", - props: { - googleSheets, - drive: { - propDefinition: [ - googleSheets, - "watchedDrive", - ], - description: "The drive containing the spreadsheet to edit. If you are connected with any [Google Shared Drives](https://support.google.com/a/users/answer/9310351), you can select it here.", - }, - sheetId: { - propDefinition: [ - googleSheets, - "sheetID", - (c) => ({ - driveId: googleSheets.methods.getDriveId(c.drive), - }), - ], - }, - worksheetId: { - propDefinition: [ - googleSheets, - "worksheetIDs", - (c) => ({ - sheetId: c.sheetId, - }), - ], - type: "string", - label: "Worksheet", - }, - column: { - propDefinition: [ - googleSheets, - "column", - ], - description: "Insert new column to the RIGHT of this column. Leave blank to insert at start of sheet", - optional: true, - default: "", - }, - }, - methods: { - async getColumnCount() { - const sheets = (await this.googleSheets.getSpreadsheet(this.sheetId)).sheets - .filter((s) => s.properties.sheetId == this.worksheetId); - return sheets[0].properties.gridProperties.columnCount; - }, - }, - async run() { - const colIndex = this.googleSheets._getColumnIndex(this.column); - const colCount = await this.getColumnCount(); - const requests = []; - // if inserting a column outside of the grid limits, append to end - if (colIndex >= colCount) { - requests.push([ - { - appendDimension: { - sheetId: this.worksheetId, - dimension: "COLUMNS", - length: 1, - }, - }, - ]); - } else { - requests.push([ - { - insertRange: { - range: { - sheetId: this.worksheetId, - startColumnIndex: colIndex, - endColumnIndex: colIndex + 1, - }, - shiftDimension: "COLUMNS", - }, - }, - ]); - } - const request = { - spreadsheetId: this.sheetId, - requestBody: { - requests, - }, - }; - return await this.googleSheets.batchUpdate(request); - }, -}; diff --git a/components/google_sheets/actions/create-spreadsheet/create-spreadsheet.mjs b/components/google_sheets/actions/create-spreadsheet/create-spreadsheet.mjs index fad7879a10122..d61457391a5d6 100644 --- a/components/google_sheets/actions/create-spreadsheet/create-spreadsheet.mjs +++ b/components/google_sheets/actions/create-spreadsheet/create-spreadsheet.mjs @@ -3,8 +3,8 @@ import googleSheets from "../../google_sheets.app.mjs"; export default { key: "google_sheets-create-spreadsheet", name: "Create Spreadsheet", - description: "Create a blank spreadsheet or duplicate an existing spreadsheet", - version: "0.1.3", + description: "Create a blank spreadsheet or duplicate an existing spreadsheet. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/create)", + version: "0.1.8", type: "action", props: { googleSheets, diff --git a/components/google_sheets/actions/create-worksheet/create-worksheet.mjs b/components/google_sheets/actions/create-worksheet/create-worksheet.mjs index 712b775aa4050..58c9987ca6c83 100644 --- a/components/google_sheets/actions/create-worksheet/create-worksheet.mjs +++ b/components/google_sheets/actions/create-worksheet/create-worksheet.mjs @@ -3,8 +3,8 @@ import googleSheets from "../../google_sheets.app.mjs"; export default { key: "google_sheets-create-worksheet", name: "Create Worksheet", - description: "Create a blank worksheet with a title", - version: "0.1.2", + description: "Create a blank worksheet with a title. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/batchUpdate)", + version: "0.1.7", type: "action", props: { googleSheets, diff --git a/components/google_sheets/actions/delete-row/delete-row.mjs b/components/google_sheets/actions/delete-row/delete-row.mjs deleted file mode 100644 index e4bf81b3effab..0000000000000 --- a/components/google_sheets/actions/delete-row/delete-row.mjs +++ /dev/null @@ -1,65 +0,0 @@ -import googleSheets from "../../google_sheets.app.mjs"; - -export default { - key: "google_sheets-delete-row", - name: "Delete Row", - description: "Deletes a specific row in a spreadsheet", - version: "0.1.2", - type: "action", - props: { - googleSheets, - drive: { - propDefinition: [ - googleSheets, - "watchedDrive", - ], - description: "The drive containing the spreadsheet to edit. If you are connected with any [Google Shared Drives](https://support.google.com/a/users/answer/9310351), you can select it here.", - }, - sheetId: { - propDefinition: [ - googleSheets, - "sheetID", - (c) => ({ - driveId: googleSheets.methods.getDriveId(c.drive), - }), - ], - }, - worksheetId: { - propDefinition: [ - googleSheets, - "worksheetIDs", - (c) => ({ - sheetId: c.sheetId, - }), - ], - type: "string", - label: "Worksheet", - }, - row: { - propDefinition: [ - googleSheets, - "row", - ], - }, - }, - async run() { - const request = { - spreadsheetId: this.sheetId, - requestBody: { - requests: [ - { - deleteDimension: { - range: { - "sheetId": this.worksheetId, - "dimension": "ROWS", - "startIndex": this.row - 1, - "endIndex": this.row, - }, - }, - }, - ], - }, - }; - return await this.googleSheets.batchUpdate(request); - }, -}; diff --git a/components/google_sheets/actions/delete-rows/delete-rows.mjs b/components/google_sheets/actions/delete-rows/delete-rows.mjs index 0839c4a07b4a3..555b187d510ee 100644 --- a/components/google_sheets/actions/delete-rows/delete-rows.mjs +++ b/components/google_sheets/actions/delete-rows/delete-rows.mjs @@ -4,7 +4,7 @@ export default { key: "google_sheets-delete-rows", name: "Delete Rows", description: "Deletes the specified rows from a spreadsheet. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#deletedimensionrequest)", - version: "0.0.2", + version: "0.0.7", type: "action", props: { googleSheets, @@ -44,6 +44,7 @@ export default { type: "integer", label: "End Index", description: "Row number of the end (exclusive) of the range of rows to delete", + optional: true, }, }, async run() { @@ -57,7 +58,9 @@ export default { "sheetId": this.worksheetId, "dimension": "ROWS", "startIndex": this.startIndex - 1, - "endIndex": this.endIndex - 1, + "endIndex": this.endIndex + ? this.endIndex - 1 + : this.startIndex, }, }, }, diff --git a/components/google_sheets/actions/delete-worksheet/delete-worksheet.mjs b/components/google_sheets/actions/delete-worksheet/delete-worksheet.mjs index 45ef7087588be..55477e9ad0b71 100644 --- a/components/google_sheets/actions/delete-worksheet/delete-worksheet.mjs +++ b/components/google_sheets/actions/delete-worksheet/delete-worksheet.mjs @@ -3,8 +3,8 @@ import googleSheets from "../../google_sheets.app.mjs"; export default { key: "google_sheets-delete-worksheet", name: "Delete Worksheet", - description: "Delete a specific worksheet", - version: "0.1.2", + description: "Delete a specific worksheet. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/batchUpdate)", + version: "0.1.7", type: "action", props: { googleSheets, diff --git a/components/google_sheets/actions/find-row/find-row.mjs b/components/google_sheets/actions/find-row/find-row.mjs index bd61beda16302..8d6db60fe5cd5 100644 --- a/components/google_sheets/actions/find-row/find-row.mjs +++ b/components/google_sheets/actions/find-row/find-row.mjs @@ -1,10 +1,13 @@ -import googleSheets from "../../google_sheets.app.mjs"; +import common from "../common/worksheet.mjs"; + +const { googleSheets } = common.props; export default { + ...common, key: "google_sheets-find-row", name: "Find Row", - description: "Find one or more rows by a column and value", - version: "0.2.2", + description: "Find one or more rows by a column and value. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/get)", + version: "0.2.10", type: "action", props: { googleSheets, @@ -23,14 +26,16 @@ export default { }), ], }, - sheetName: { + worksheetId: { propDefinition: [ googleSheets, - "sheetName", + "worksheetIDs", (c) => ({ sheetId: c.sheetId, }), ], + type: "string", + label: "Worksheet Id", }, column: { propDefinition: [ @@ -43,17 +48,28 @@ export default { label: "Value", description: "The value to search for", }, + exportRow: { + type: "boolean", + label: "Export Row", + description: "Set to `true` to return cell values for the entire row", + optional: true, + }, }, async run() { + const worksheet = await this.getWorksheetById(this.sheetId, this.worksheetId); const sheets = this.googleSheets.sheets(); const colValues = (await sheets.spreadsheets.values.get({ spreadsheetId: this.sheetId, - range: `${this.sheetName}!${this.column}:${this.column}`, + range: `${worksheet?.properties?.title}!${this.column}:${this.column}`, })).data.values; + if (!colValues?.length) { + return []; + } + const rows = []; - return colValues.reduce((values, value, index) => { + const result = colValues.reduce((values, value, index) => { if (value == this.value) { rows.push({ value, @@ -62,6 +78,27 @@ export default { }); } return rows; - }); + }, []); + + if (!this.exportRow) { + return result; + } + + const indexes = result.map(({ index }) => index); + const { data: { values } } = + await sheets.spreadsheets.values.get({ + spreadsheetId: this.sheetId, + range: `${worksheet?.properties?.title}`, + }); + return values.reduce((acc, row, index) => { + if (indexes.includes(index)) { + return acc.concat({ + row, + index, + googleSheetsRowNumber: index + 1, + }); + } + return acc; + }, []); }, }; diff --git a/components/google_sheets/actions/get-cell/get-cell.mjs b/components/google_sheets/actions/get-cell/get-cell.mjs index 3e77ad625951a..d285fe1ea5192 100644 --- a/components/google_sheets/actions/get-cell/get-cell.mjs +++ b/components/google_sheets/actions/get-cell/get-cell.mjs @@ -1,10 +1,13 @@ -import googleSheets from "../../google_sheets.app.mjs"; +import common from "../common/worksheet.mjs"; + +const { googleSheets } = common.props; export default { + ...common, key: "google_sheets-get-cell", name: "Get Cell", - description: "Fetch the contents of a specific cell in a spreadsheet", - version: "0.1.2", + description: "Fetch the contents of a specific cell in a spreadsheet. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/get)", + version: "0.1.9", type: "action", props: { googleSheets, @@ -23,14 +26,16 @@ export default { }), ], }, - sheetName: { + worksheetId: { propDefinition: [ googleSheets, - "sheetName", + "worksheetIDs", (c) => ({ sheetId: c.sheetId, }), ], + type: "string", + label: "Worksheet Id", }, cell: { propDefinition: [ @@ -40,11 +45,15 @@ export default { }, }, async run() { + const worksheet = await this.getWorksheetById(this.sheetId, this.worksheetId); const sheets = this.googleSheets.sheets(); - return (await sheets.spreadsheets.values.get({ + const values = (await sheets.spreadsheets.values.get({ spreadsheetId: this.sheetId, - range: `${this.sheetName}!${this.cell}:${this.cell}`, + range: `${worksheet?.properties?.title}!${this.cell}:${this.cell}`, })).data.values; + if (values?.length) { + return values[0][0]; + } }, }; diff --git a/components/google_sheets/actions/get-spreadsheet-by-id/get-spreadsheet-by-id.mjs b/components/google_sheets/actions/get-spreadsheet-by-id/get-spreadsheet-by-id.mjs index d0a633bce3361..2c3e31010ae18 100644 --- a/components/google_sheets/actions/get-spreadsheet-by-id/get-spreadsheet-by-id.mjs +++ b/components/google_sheets/actions/get-spreadsheet-by-id/get-spreadsheet-by-id.mjs @@ -3,8 +3,8 @@ import googleSheets from "../../google_sheets.app.mjs"; export default { key: "google_sheets-get-spreadsheet-by-id", name: "Get Spreadsheet by ID", - description: "Returns the spreadsheet at the given ID. [See the docs](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/get) for more information", - version: "0.1.2", + description: "Returns the spreadsheet at the given ID. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/get) for more information", + version: "0.1.8", type: "action", props: { googleSheets, @@ -22,7 +22,6 @@ export default { driveId: googleSheets.methods.getDriveId(c.drive), }), ], - optional: false, }, }, async run({ $ }) { diff --git a/components/google_sheets/actions/get-values-in-range/get-values-in-range.mjs b/components/google_sheets/actions/get-values-in-range/get-values-in-range.mjs index 16807f520ee4c..8a8d2241d6e8e 100644 --- a/components/google_sheets/actions/get-values-in-range/get-values-in-range.mjs +++ b/components/google_sheets/actions/get-values-in-range/get-values-in-range.mjs @@ -1,10 +1,13 @@ -import googleSheets from "../../google_sheets.app.mjs"; +import common from "../common/worksheet.mjs"; + +const { googleSheets } = common.props; export default { + ...common, key: "google_sheets-get-values-in-range", name: "Get Values in Range", - description: "Get values from a range of cells using A1 notation.", - version: "0.1.2", + description: "Get all values or values from a range of cells using A1 notation. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/get)", + version: "0.1.9", type: "action", props: { googleSheets, @@ -23,28 +26,34 @@ export default { }), ], }, - sheetName: { + worksheetId: { propDefinition: [ googleSheets, - "sheetName", + "worksheetIDs", (c) => ({ sheetId: c.sheetId, }), ], + type: "string", + label: "Worksheet Id", }, range: { propDefinition: [ googleSheets, "range", ], + optional: true, }, }, async run() { + const worksheet = await this.getWorksheetById(this.sheetId, this.worksheetId); const sheets = this.googleSheets.sheets(); return (await sheets.spreadsheets.values.get({ spreadsheetId: this.sheetId, - range: `${this.sheetName}!${this.range}`, + range: this.range + ? `${worksheet?.properties?.title}!${this.range}` + : `${worksheet?.properties?.title}`, })).data.values; }, }; diff --git a/components/google_sheets/actions/get-values/get-values.mjs b/components/google_sheets/actions/get-values/get-values.mjs deleted file mode 100644 index 38be80136140e..0000000000000 --- a/components/google_sheets/actions/get-values/get-values.mjs +++ /dev/null @@ -1,44 +0,0 @@ -import googleSheets from "../../google_sheets.app.mjs"; - -export default { - key: "google_sheets-get-values", - name: "Get Values", - description: "Get all values from a sheet.", - version: "0.1.2", - type: "action", - props: { - googleSheets, - drive: { - propDefinition: [ - googleSheets, - "watchedDrive", - ], - }, - sheetId: { - propDefinition: [ - googleSheets, - "sheetID", - (c) => ({ - driveId: googleSheets.methods.getDriveId(c.drive), - }), - ], - }, - sheetName: { - propDefinition: [ - googleSheets, - "sheetName", - (c) => ({ - sheetId: c.sheetId, - }), - ], - }, - }, - async run() { - const sheets = this.googleSheets.sheets(); - - return (await sheets.spreadsheets.values.get({ - spreadsheetId: this.sheetId, - range: `${this.sheetName}`, - })).data.values; - }, -}; diff --git a/components/google_sheets/actions/insert-anchored-note/insert-anchored-note.mjs b/components/google_sheets/actions/insert-anchored-note/insert-anchored-note.mjs index f459ebff61a92..e2c96006cfcae 100644 --- a/components/google_sheets/actions/insert-anchored-note/insert-anchored-note.mjs +++ b/components/google_sheets/actions/insert-anchored-note/insert-anchored-note.mjs @@ -4,8 +4,8 @@ import app from "../../google_sheets.app.mjs"; export default { key: "google_sheets-insert-anchored-note", name: "Insert an Anchored Note", - description: "Insert a note on a spreadsheet cell. [See the docs here](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/batchUpdate)", - version: "0.1.2", + description: "Insert a note on a spreadsheet cell. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/batchUpdate)", + version: "0.1.7", type: "action", props: { app, @@ -18,12 +18,9 @@ export default { sheetId: { propDefinition: [ app, - "fileId", - ({ drive }) => ({ - drive, - baseOpts: { - q: "mimeType = 'application/vnd.google-apps.spreadsheet'", - }, + "sheetID", + (c) => ({ + driveId: app.methods.getDriveId(c.drive), }), ], }, diff --git a/components/google_sheets/actions/insert-comment/insert-comment.mjs b/components/google_sheets/actions/insert-comment/insert-comment.mjs index 4c3ca719097e7..f30169b81f6ef 100644 --- a/components/google_sheets/actions/insert-comment/insert-comment.mjs +++ b/components/google_sheets/actions/insert-comment/insert-comment.mjs @@ -3,8 +3,8 @@ import app from "../../google_sheets.app.mjs"; export default { key: "google_sheets-insert-comment", name: "Insert Comment", - description: "Insert a comment into a spreadsheet. [See the docs here](https://developers.google.com/drive/api/v3/reference/comments/create)", - version: "0.1.2", + description: "Insert a comment into a spreadsheet. [See the documentation](https://developers.google.com/drive/api/v3/reference/comments/create)", + version: "0.1.8", type: "action", props: { app, @@ -17,12 +17,9 @@ export default { fileId: { propDefinition: [ app, - "fileId", - ({ drive }) => ({ - drive, - baseOpts: { - q: "mimeType = 'application/vnd.google-apps.spreadsheet'", - }, + "sheetID", + (c) => ({ + driveId: app.methods.getDriveId(c.drive), }), ], }, diff --git a/components/google_sheets/actions/list-worksheets/list-worksheets.mjs b/components/google_sheets/actions/list-worksheets/list-worksheets.mjs index c894a73dbada4..81ecb50903832 100644 --- a/components/google_sheets/actions/list-worksheets/list-worksheets.mjs +++ b/components/google_sheets/actions/list-worksheets/list-worksheets.mjs @@ -3,8 +3,8 @@ import googleSheets from "../../google_sheets.app.mjs"; export default { key: "google_sheets-list-worksheets", name: "List Worksheets", - description: "Get a list of all worksheets in a spreadsheet", - version: "0.1.2", + description: "Get a list of all worksheets in a spreadsheet. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/get)", + version: "0.1.7", type: "action", props: { googleSheets, diff --git a/components/google_sheets/actions/update-cell/update-cell.mjs b/components/google_sheets/actions/update-cell/update-cell.mjs index bac4ba4f4b218..4dd29a023705b 100644 --- a/components/google_sheets/actions/update-cell/update-cell.mjs +++ b/components/google_sheets/actions/update-cell/update-cell.mjs @@ -1,10 +1,13 @@ -import googleSheets from "../../google_sheets.app.mjs"; +import common from "../common/worksheet.mjs"; + +const { googleSheets } = common.props; export default { + ...common, key: "google_sheets-update-cell", name: "Update Cell", - description: "Update a cell in a spreadsheet", - version: "0.1.2", + description: "Update a cell in a spreadsheet. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/update)", + version: "0.1.9", type: "action", props: { googleSheets, @@ -25,14 +28,16 @@ export default { ], description: "The spreadsheet containing the worksheet to update", }, - sheetName: { + worksheetId: { propDefinition: [ googleSheets, - "sheetName", + "worksheetIDs", (c) => ({ sheetId: c.sheetId, }), ], + type: "string", + label: "Worksheet Id", }, cell: { propDefinition: [ @@ -51,9 +56,10 @@ export default { }, }, async run() { + const worksheet = await this.getWorksheetById(this.sheetId, this.worksheetId); const request = { spreadsheetId: this.sheetId, - range: `${this.sheetName}!${this.cell}:${this.cell}`, + range: `${worksheet?.properties?.title}!${this.cell}:${this.cell}`, valueInputOption: "USER_ENTERED", resource: { values: [ diff --git a/components/google_sheets/actions/update-multiple-rows/update-multiple-rows.mjs b/components/google_sheets/actions/update-multiple-rows/update-multiple-rows.mjs new file mode 100644 index 0000000000000..e29bc2f551676 --- /dev/null +++ b/components/google_sheets/actions/update-multiple-rows/update-multiple-rows.mjs @@ -0,0 +1,124 @@ +import common from "../common/worksheet.mjs"; +import { + parseArray, getWorksheetHeaders, +} from "../../common/utils.mjs"; +const { googleSheets } = common.props; + +export default { + ...common, + key: "google_sheets-update-multiple-rows", + name: "Update Multiple Rows", + description: "Update multiple rows in a spreadsheet defined by a range. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/update)", + version: "0.1.9", + type: "action", + props: { + googleSheets, + drive: { + propDefinition: [ + googleSheets, + "watchedDrive", + ], + description: + "The drive containing the worksheet to update. If you are connected with any [Google Shared Drives](https://support.google.com/a/users/answer/9310351), you can select it here.", + }, + sheetId: { + propDefinition: [ + googleSheets, + "sheetID", + (c) => ({ + driveId: googleSheets.methods.getDriveId(c.drive), + }), + ], + description: "The spreadsheet containing the worksheet to update", + }, + worksheetId: { + propDefinition: [ + googleSheets, + "worksheetIDs", + (c) => ({ + sheetId: c.sheetId, + }), + ], + type: "string", + label: "Worksheet Id", + reloadProps: true, + }, + headersDisplay: { + propDefinition: [ + googleSheets, + "headersDisplay", + ], + }, + range: { + propDefinition: [ + googleSheets, + "range", + ], + }, + rows: { + propDefinition: [ + googleSheets, + "rows", + ], + }, + rowsDescription: { + propDefinition: [ + googleSheets, + "rowsDescription", + ], + }, + }, + async additionalProps() { + const props = {}; + if (!this.sheetId || !this.worksheetId) { + return props; + } + const worksheet = await this.getWorksheetById(this.sheetId, this.worksheetId); + const rowHeaders = await getWorksheetHeaders(this, this.sheetId, worksheet?.properties?.title); + if (rowHeaders.length) { + return { + headersDisplay: { + type: "alert", + alertType: "info", + content: `Possible Row Headers: **\`${rowHeaders.join(", ")}\`**`, + hidden: false, + }, + }; + } + }, + async run() { + let inputValidated = true; + + const rows = parseArray(this.rows); + + if (!rows) { + inputValidated = false; + } else { + rows.forEach((row) => { + if (!Array.isArray(row)) { + inputValidated = false; + } + }); + } + + // Throw an error if input validation failed + if (!inputValidated) { + console.error("Data Submitted:"); + console.error(rows); + throw new Error( + "Rows data is not an array of arrays. Please enter an array of arrays in the `Rows` parameter above. If you're trying to send a single rows to Google Sheets, search for the action to add a single row to Sheets or try modifying the code for this step.", + ); + } + + const worksheet = await this.getWorksheetById(this.sheetId, this.worksheetId); + const request = { + spreadsheetId: this.sheetId, + range: `${worksheet?.properties?.title}!${this.range}`, + valueInputOption: "USER_ENTERED", + resource: { + values: rows, + }, + }; + return await this.googleSheets.updateSpreadsheet(request); + }, +}; diff --git a/components/google_sheets/actions/update-row/update-row.mjs b/components/google_sheets/actions/update-row/update-row.mjs index 6a9681e22cce8..e03d24825f7fd 100644 --- a/components/google_sheets/actions/update-row/update-row.mjs +++ b/components/google_sheets/actions/update-row/update-row.mjs @@ -1,10 +1,16 @@ -import googleSheets from "../../google_sheets.app.mjs"; +import common from "../common/worksheet.mjs"; +import { ConfigurationError } from "@pipedream/platform"; +import { parseArray } from "../../common/utils.mjs"; +import { isDynamicExpression } from "../common/worksheet.mjs"; + +const { googleSheets } = common.props; export default { + ...common, key: "google_sheets-update-row", name: "Update Row", - description: "Update a row in a spreadsheet", - version: "0.1.2", + description: "Update a row in a spreadsheet. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/update)", + version: "0.1.10", type: "action", props: { googleSheets, @@ -24,15 +30,30 @@ export default { }), ], description: "The spreadsheet containing the worksheet to update", + reloadProps: true, }, - sheetName: { + worksheetId: { propDefinition: [ googleSheets, - "sheetName", + "worksheetIDs", (c) => ({ - sheetId: c.sheetId, + sheetId: c.sheetId?.value || c.sheetId, }), ], + description: "Select a worksheet or enter a custom expression. When referencing a spreadsheet dynamically, you must provide a custom expression for the worksheet.", + async options({ sheetId }) { + // If sheetId is a dynamic reference, don't load options + if (isDynamicExpression(sheetId)) { + return []; + } + + // Otherwise, call the original options function with the correct context + const origOptions = googleSheets.propDefinitions.worksheetIDs.options; + return origOptions.call(this, { + sheetId, + }); + }, + reloadProps: true, }, row: { propDefinition: [ @@ -40,29 +61,123 @@ export default { "row", ], }, - cells: { - propDefinition: [ - googleSheets, - "cells", - ], - }, + hasHeaders: common.props.hasHeaders, + }, + async additionalProps() { + const { + sheetId, + worksheetId, + row, + hasHeaders, + } = this; + + // If using dynamic expressions for either sheetId or worksheetId, return only array input + if (isDynamicExpression(sheetId) || isDynamicExpression(worksheetId)) { + return { + myColumnData: { + type: "string[]", + label: "Values", + description: "Provide a value for each cell of the row. Google Sheets accepts strings, numbers and boolean values for each cell. To set a cell to an empty value, pass an empty string.", + }, + }; + } + + const props = {}; + if (hasHeaders && row) { + try { + const worksheet = await this.getWorksheetById(sheetId, worksheetId); + const { values } = await this.googleSheets.getSpreadsheetValues(sheetId, `${worksheet?.properties?.title}!1:1`); + + if (!values?.[0]?.length) { + throw new ConfigurationError("Could not find a header row. Please either add headers and click \"Refresh fields\" or set 'Does the first row of the sheet have headers?' to false."); + } + + const { values: rowValues } = !isNaN(row) + ? await this.googleSheets.getSpreadsheetValues(sheetId, `${worksheet?.properties?.title}!${row}:${row}`) + : {}; + + for (let i = 0; i < values[0]?.length; i++) { + props[`col_${i.toString().padStart(4, "0")}`] = { + type: "string", + label: values[0][i], + optional: true, + default: rowValues?.[0]?.[i], + }; + } + props.allColumns = { + type: "string", + hidden: true, + default: JSON.stringify(values), + }; + } catch (err) { + console.error("Error fetching headers:", err); + // Fallback to basic column input if headers can't be fetched + return { + headerError: { + type: "string", + label: "Header Fetch Error", + description: `Unable to fetch headers: ${err.message}. Using simple column input instead.`, + optional: true, + hidden: true, + }, + myColumnData: { + type: "string[]", + label: "Values", + description: "Provide a value for each cell of the row. Google Sheets accepts strings, numbers and boolean values for each cell. To set a cell to an empty value, pass an empty string.", + }, + }; + } + } else { + props.myColumnData = { + type: "string[]", + label: "Values", + description: "Provide a value for each cell of the row. Google Sheets accepts strings, numbers and boolean values for each cell. To set a cell to an empty value, pass an empty string.", + }; + } + return props; }, async run() { - const cells = this.cells; + const { + sheetId, + worksheetId, + row, + } = this; + + let cells; + if (this.hasHeaders + && !isDynamicExpression(sheetId) + && !isDynamicExpression(worksheetId) + && this.allColumns + ) { + // Only use header-based processing if we have the allColumns prop and no dynamic expressions + const rows = JSON.parse(this.allColumns); + const [ + headers, + ] = rows; + cells = headers + .map((_, i) => `col_${i.toString().padStart(4, "0")}`) + .map((column) => this[column] ?? ""); + } else { + // For dynamic references or no headers, use the array input + cells = this.googleSheets.sanitizedArray(this.myColumnData); + } // validate input if (!cells || !cells.length) { - throw new Error("Please enter an array of elements in `Cells / Column Values`."); + throw new ConfigurationError("Please enter an array of elements in `Row Values`."); } - if (!Array.isArray(cells)) { - throw new Error("Cell / Column data is not an array. Please enter an array of elements in `Cells / Column Values`."); + cells = parseArray(cells); + if (!cells) { + throw new ConfigurationError("Row Values is not an array. Please enter an array of elements in `Row Values`."); } if (Array.isArray(cells[0])) { - throw new Error("Cell / Column data is a multi-dimensional array. A one-dimensional is expected."); + throw new ConfigurationError("Row Values is a multi-dimensional array. A one-dimensional is expected."); } + + const worksheet = await this.getWorksheetById(sheetId, worksheetId); const request = { - spreadsheetId: this.sheetId, - range: `${this.sheetName}!${this.row}:${this.row}`, + spreadsheetId: sheetId, + range: `${worksheet?.properties?.title}!${row}:${row}`, valueInputOption: "USER_ENTERED", resource: { values: [ @@ -70,6 +185,7 @@ export default { ], }, }; + return await this.googleSheets.updateSpreadsheet(request); }, }; diff --git a/components/google_sheets/actions/update-rows/update-rows.mjs b/components/google_sheets/actions/update-rows/update-rows.mjs deleted file mode 100644 index ea89273752100..0000000000000 --- a/components/google_sheets/actions/update-rows/update-rows.mjs +++ /dev/null @@ -1,89 +0,0 @@ -import googleSheets from "../../google_sheets.app.mjs"; - -export default { - key: "google_sheets-update-rows", - name: "Update Rows", - description: "Update multiple rows in a spreadsheet defined by a range", - version: "0.1.2", - type: "action", - props: { - googleSheets, - drive: { - propDefinition: [ - googleSheets, - "watchedDrive", - ], - description: - "The drive containing the worksheet to update. If you are connected with any [Google Shared Drives](https://support.google.com/a/users/answer/9310351), you can select it here.", - }, - sheetId: { - propDefinition: [ - googleSheets, - "sheetID", - (c) => ({ - driveId: googleSheets.methods.getDriveId(c.drive), - }), - ], - description: "The spreadsheet containing the worksheet to update", - }, - sheetName: { - propDefinition: [ - googleSheets, - "sheetName", - (c) => ({ - sheetId: c.sheetId, - }), - ], - }, - range: { - propDefinition: [ - googleSheets, - "range", - ], - }, - rows: { - propDefinition: [ - googleSheets, - "rows", - ], - }, - }, - async run() { - let rows = this.rows; - - let inputValidated = true; - - if (!Array.isArray(rows)) { - rows = JSON.parse(this.rows); - } - - if (!rows || !rows.length || !Array.isArray(rows)) { - inputValidated = false; - } else { - rows.forEach((row) => { - if (!Array.isArray(row)) { - inputValidated = false; - } - }); - } - - // Throw an error if input validation failed - if (!inputValidated) { - console.error("Data Submitted:"); - console.error(rows); - throw new Error( - "Rows data is not an array of arrays. Please enter an array of arrays in the `Rows` parameter above. If you're trying to send a single rows to Google Sheets, search for the action to add a single row to Sheets or try modifying the code for this step.", - ); - } - - const request = { - spreadsheetId: this.sheetId, - range: `${this.sheetName}!${this.range}`, - valueInputOption: "USER_ENTERED", - resource: { - values: rows, - }, - }; - return await this.googleSheets.updateSpreadsheet(request); - }, -}; diff --git a/components/google_sheets/actions/upsert-row/upsert-row.mjs b/components/google_sheets/actions/upsert-row/upsert-row.mjs index 0389348a91b1a..575fec689d294 100644 --- a/components/google_sheets/actions/upsert-row/upsert-row.mjs +++ b/components/google_sheets/actions/upsert-row/upsert-row.mjs @@ -1,9 +1,11 @@ import { v4 as uuid } from "uuid"; -import { VALUE_RENDER_OPTION } from "../../constants.mjs"; -import googleSheets from "../../google_sheets.app.mjs"; +import common from "../common/worksheet.mjs"; +import { VALUE_RENDER_OPTION } from "../../common/constants.mjs"; import { omitEmptyKey, toSingleLineString, -} from "../../utils.mjs"; +} from "../../common/utils.mjs"; + +const { googleSheets } = common.props; /** * This action performs an upsert operation, similar to the MySQL `INSERT INTO ... ON DUPLICATE KEY @@ -18,10 +20,11 @@ import { * comment](https://github.com/PipedreamHQ/pipedream/issues/1824#issuecomment-949940177). */ export default { + ...common, key: "google_sheets-upsert-row", name: "Upsert Row", - description: "Upsert a row of data in a Google Sheet", - version: "0.1.3", + description: "Upsert a row of data in a Google Sheet. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/append)", + version: "0.1.11", type: "action", props: { googleSheets, @@ -40,14 +43,16 @@ export default { }), ], }, - sheetName: { + worksheetId: { propDefinition: [ googleSheets, - "sheetName", + "worksheetIDs", (c) => ({ sheetId: c.sheetId, }), ], + type: "string", + label: "Worksheet Id", }, insert: { propDefinition: [ @@ -94,12 +99,13 @@ export default { async run({ $ }) { const { sheetId, - sheetName, + worksheetId, insert, column, value, updates, } = this; + const worksheet = await this.getWorksheetById(sheetId, worksheetId); const colIndex = this.googleSheets._getColumnIndex(column) - 1; const keyValue = value ? value @@ -126,7 +132,7 @@ export default { keyValue, // A1 ], [ - this.googleSheets.buildMatchFormula("A1", sheetName, { + this.googleSheets.buildMatchFormula("A1", worksheet?.properties?.title, { column, searchType: 0, }), // A2 @@ -146,7 +152,7 @@ export default { // INSERT ROW const result = await this.googleSheets.addRowsToSheet({ spreadsheetId: sheetId, - range: sheetName, + range: worksheet?.properties?.title, rows: [ insert, ], @@ -158,7 +164,7 @@ export default { // UPDATE ROW const updateParams = [ sheetId, - sheetName, + worksheet?.properties?.title, matchedRow, ]; const sanitizedUpdates = omitEmptyKey(updates); diff --git a/components/google_sheets/constants.mjs b/components/google_sheets/common/constants.mjs similarity index 100% rename from components/google_sheets/constants.mjs rename to components/google_sheets/common/constants.mjs diff --git a/components/google_sheets/common/utils.mjs b/components/google_sheets/common/utils.mjs new file mode 100644 index 0000000000000..b895e4130da61 --- /dev/null +++ b/components/google_sheets/common/utils.mjs @@ -0,0 +1,79 @@ +/** + * This method creates an object composed of the own and inherited enumerable string keyed + * properties of `object` whose keys are not empty strings + * + * @example + * // returns {} + * omitEmptykey({ "": "bar" }); + * + * @param {object} object the source object + * @param {...*} [object.omittedObj] the properties of `object` whose keys are not empty strings + * @returns {object} the new object + */ +function omitEmptyKey({ + /* eslint-disable-next-line no-unused-vars */ + "": _, ...omittedObj +} = {}) { + return omittedObj; +} + +/** + * Taken from {@linkcode ../aws/sources/common/utils.mjs utils.mjs}. + * A utility function that accepts a string as an argument and reformats it in + * order to remove newline characters and consecutive spaces. Useful when + * dealing with very long templated strings that are split into multiple lines. + * + * @example + * // returns "This is a much cleaner string" + * toSingleLineString(` + * This is a much + * cleaner string + * `); + * + * @param {string} multiLineString the input string to reformat + * @returns a formatted string based on the content of the input argument, + * without newlines and multiple spaces + */ +function toSingleLineString(multiLineString) { + return multiLineString + .trim() + .replace(/\n/g, " ") + .replace(/\s{2,}/g, " "); +} + +function parseArray(value) { + if (!value) { + return []; + } + if (Array.isArray(value)) { + return value; + } + try { + const parsed = JSON.parse(value); + if (!Array.isArray(parsed)) { + return false; + } + return parsed; + } catch { + return false; + } +} + +async function getWorksheetHeaders(ctx, sheetId, worksheetName) { + const headers = []; + const { values } = await ctx.googleSheets.getSpreadsheetValues(sheetId, `${worksheetName}!1:1`); + if (!values[0]?.length) { + return headers; + } + for (let i = 0; i < values[0]?.length; i++) { + headers.push(values[0][i]); + } + return headers; +} + +export { + omitEmptyKey, + toSingleLineString, + parseArray, + getWorksheetHeaders, +}; diff --git a/components/google_sheets/google_sheets.app.mjs b/components/google_sheets/google_sheets.app.mjs index 37be5a1e2ed4b..6aaab67d4e882 100644 --- a/components/google_sheets/google_sheets.app.mjs +++ b/components/google_sheets/google_sheets.app.mjs @@ -1,9 +1,9 @@ import { axios } from "@pipedream/platform"; import sheets from "@googleapis/sheets"; -import googleDrive from "../google_drive/google_drive.app.mjs"; +import googleDrive from "@pipedream/google_drive"; import { INSERT_DATA_OPTION, VALUE_INPUT_OPTION, -} from "./constants.mjs"; +} from "./common/constants.mjs"; import isArray from "lodash/isArray.js"; import get from "lodash/get.js"; import isString from "lodash/isString.js"; @@ -22,8 +22,7 @@ export default { cells: { type: "string[]", label: "Cells / Column Values", - description: - "Enter individual cell values or a custom expression to pass an array with each element representing a cell/column value.", + description: "Enter individual cell values or a custom expression to pass an array with each element representing a cell/column value.", }, range: { type: "string", @@ -43,31 +42,33 @@ export default { rows: { type: "string", label: "Row Values", - description: - "Provide an array of arrays. Each nested array should represent a row, with each element of the nested array representing a cell/column value (e.g., passing `[[\"Foo\",1,2],[\"Bar\",3,4]]` will insert two rows of data with three columns each). The most common pattern is to reference an array of arrays exported by a previous step (e.g., `{{steps.foo.$return_value}}`). You may also enter or construct a string that will `JSON.parse()` to an array of arrays.", + description: "Provide an array of arrays", + }, + rowsDescription: { + type: "alert", + alertType: "neutral", + content: "Each nested array should represent a row, with each element of the nested array representing a cell/column value (e.g., passing `[[\"Foo\",1,2],[\"Bar\",3,4]]` will insert two rows of data with three columns each). The most common pattern is to reference an array of arrays exported by a previous step (e.g., `{{steps.foo.$return_value}}`). You may also enter or construct a string that will `JSON.parse()` to an array of arrays.", + }, + headersDisplay: { + type: "alert", + alertType: "info", + content: "", + hidden: true, }, sheetID: { type: "string", label: "Spreadsheet", description: "The Spreadsheet ID", + useQuery: true, options({ + query, prevContext, driveId, }) { const { nextPageToken } = prevContext; - return this.listSheetsOptions(driveId, nextPageToken); - }, - }, - sheetName: { - type: "string", - label: "Sheet Name", - description: "Your sheet name", - async options({ sheetId }) { - const { sheets } = await this.getSpreadsheet(sheetId); - return sheets.map((sheet) => sheet.properties.title); + return this.listSheetsOptions(driveId, nextPageToken, query); }, }, - // TODO: is this a duplicate of the prop above? worksheetIDs: { type: "string[]", label: "Worksheet(s)", @@ -134,9 +135,13 @@ export default { endCoord, ] = range.split(":"); const startCol = startCoord.replace(/\d/g, ""); - const endCol = endCoord.replace(/\d/g, ""); + const endCol = endCoord + ? endCoord.replace(/\d/g, "") + : startCol; const startRow = parseInt(startCoord.replace(/\D/g, ""), 10) - 1; // 0-based index - const endRow = parseInt(endCoord.replace(/\D/g, ""), 10); + const endRow = endCoord + ? parseInt(endCoord.replace(/\D/g, ""), 10) + : startRow; return { sheetName, @@ -247,8 +252,11 @@ export default { } return sum; }, - async listSheetsOptions(driveId, pageToken = null) { - const q = "mimeType='application/vnd.google-apps.spreadsheet'"; + async listSheetsOptions(driveId, pageToken = null, query) { + const searchQuery = query + ? ` and name contains '${query}'` + : ""; + const q = `mimeType='application/vnd.google-apps.spreadsheet'${searchQuery}`; let request = { q, }; diff --git a/components/google_sheets/package.json b/components/google_sheets/package.json index a3ad240c4e5f8..7beb5705bc674 100644 --- a/components/google_sheets/package.json +++ b/components/google_sheets/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/google_sheets", - "version": "0.6.2", + "version": "0.7.12", "description": "Pipedream Google_sheets Components", "main": "google_sheets.app.mjs", "keywords": [ @@ -11,8 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "dependencies": { "@googleapis/sheets": "^0.3.0", - "@pipedream/platform": "^1.4.0", - "@pipedream/google_drive": "^0.6.12", + "@pipedream/google_drive": "^0.6.19", + "@pipedream/platform": "^3.0.0", "lodash": "^4.17.21", "uuidv4": "^6.2.6", "zlib": "^1.0.5" diff --git a/components/google_sheets/sources/common/http-based/base.mjs b/components/google_sheets/sources/common/http-based/base.mjs index f7a35ed4b4ada..984ffb1572b08 100644 --- a/components/google_sheets/sources/common/http-based/base.mjs +++ b/components/google_sheets/sources/common/http-based/base.mjs @@ -10,9 +10,10 @@ import { v4 as uuid } from "uuid"; -import { WEBHOOK_SUBSCRIPTION_RENEWAL_SECONDS } from "../../../../google_drive/constants.mjs"; +import { + MY_DRIVE_VALUE, WEBHOOK_SUBSCRIPTION_RENEWAL_SECONDS, +} from "../../../../google_drive/common/constants.mjs"; import googleSheets from "../../../google_sheets.app.mjs"; -import { MY_DRIVE_VALUE } from "../../../../google_drive/constants.mjs"; /** * The number of events that will be automatically sent whenever the event @@ -40,6 +41,14 @@ export default { intervalSeconds: WEBHOOK_SUBSCRIPTION_RENEWAL_SECONDS, }, }, + watchedDrive: { + propDefinition: [ + googleSheets, + "watchedDrive", + ], + description: "Defaults to My Drive. To select a [Shared Drive](https://support.google.com/a/users/answer/9310351) instead, select it from this list.", + }, + sheetID: { propDefinition: [ googleSheets, @@ -63,7 +72,7 @@ export default { * for starting and stopping watch notifications tied to the desired file. */ async activate() { - const channelID = this._getChannelID() || uuid(); + const channelID = uuid(); const { startPageToken, @@ -137,7 +146,7 @@ export default { throw new Error("activateHook is not implemented"); }, processSpreadsheet() { - throw new Error("processEvent is not implemented"); + throw new Error("processSpreadsheet is not implemented"); }, async renewSubscription() { throw new Error("renewSubscription is not implemented"); diff --git a/components/google_sheets/sources/common/http-based/sheet.mjs b/components/google_sheets/sources/common/http-based/sheet.mjs index 764f49184f2cf..0b1c5ac37dbac 100644 --- a/components/google_sheets/sources/common/http-based/sheet.mjs +++ b/components/google_sheets/sources/common/http-based/sheet.mjs @@ -31,16 +31,17 @@ export default { // Assume subscription & channelID may all be undefined at // this point Handle their absence appropriately. const subscription = this._getSubscription(); - const channelID = this._getChannelID() || uuid(); + const channelID = this._getChannelID(); + const newChannelID = uuid(); const { expiration, resourceId, - newChannelID, } = await this.googleSheets.renewFileSubscription( subscription, this.http.endpoint, channelID, + newChannelID, this.getSheetId(), ); diff --git a/components/google_sheets/sources/common/new-row-added.mjs b/components/google_sheets/sources/common/new-row-added.mjs index 7ffff7ad5892d..0923500c6ee88 100644 --- a/components/google_sheets/sources/common/new-row-added.mjs +++ b/components/google_sheets/sources/common/new-row-added.mjs @@ -19,16 +19,15 @@ export default { }, }, methods: { - getMeta(spreadsheet, worksheet, rowNumber, row) { - const { sheetId: worksheetId } = worksheet; - const { spreadsheetId: sheetId } = spreadsheet; + _getRowHashes() { + return this.db.get("rowHashes") || {}; + }, + _setRowHashes(rowHashes) { + this.db.set("rowHashes", rowHashes); + }, + getMeta(worksheet, rowNumber) { const ts = Date.now(); - const rowHash = crypto - .createHash("md5") - .update(JSON.stringify(row)) - .digest("base64"); - // Include a hash of the row data in id to dedupe events for the same new row - const id = `${sheetId}${worksheetId}${rowNumber}${rowHash}`; + const id = `${worksheet.properties.sheetId}${rowNumber}${ts}`; const summary = `New row #${rowNumber} in ${worksheet.properties.title}`; return { id, @@ -154,11 +153,21 @@ export default { : newRowCount, ); + const rowHashes = this._getRowHashes(); for (const [ index, newRow, ] of newRowValues.values.entries()) { const rowNumber = lowerBound + index; + const rowHash = crypto + .createHash("md5") + .update(JSON.stringify(newRow)) + .digest("base64"); + const rowHashString = `${worksheet.properties.sheetId}${rowNumber}${rowHash}`; + if (rowHashes[rowHashString]) { + continue; + } + rowHashes[rowHashString] = true; this.$emit( { newRow, @@ -166,9 +175,10 @@ export default { worksheet, rowNumber, }, - this.getMeta(spreadsheet, worksheet, rowNumber, newRow), + this.getMeta(worksheet, rowNumber), ); } + this._setRowHashes(rowHashes); } }, }, diff --git a/components/google_sheets/sources/common/new-updates.mjs b/components/google_sheets/sources/common/new-updates.mjs index b8e2e1e16dd88..d36aa070f61dc 100644 --- a/components/google_sheets/sources/common/new-updates.mjs +++ b/components/google_sheets/sources/common/new-updates.mjs @@ -1,4 +1,3 @@ -import crypto from "crypto"; import base from "./http-based/base.mjs"; import zlib from "zlib"; @@ -20,23 +19,15 @@ export default { }, }, methods: { - getMeta(spreadsheet, worksheet, changes) { + getMeta(spreadsheet, worksheet) { const { sheetId: worksheetId, title: worksheetTitle, } = worksheet.properties; - const { - spreadsheetId: sheetId, - properties: { title: sheetTitle }, - } = spreadsheet; - - const changesHash = crypto - .createHash("md5") - .update(JSON.stringify(changes)) - .digest("base64"); + const { properties: { title: sheetTitle } } = spreadsheet; const ts = Date.now(); - const id = `${sheetId}${worksheetId}${changesHash}${ts}`; + const id = `${worksheetId}${ts}`; const summary = `${sheetTitle} - ${worksheetTitle}`; return { id, @@ -217,7 +208,7 @@ export default { worksheet, changes, }, - this.getMeta(spreadsheet, worksheet, changes), + this.getMeta(spreadsheet, worksheet), ); } this._setSheetValues( diff --git a/components/google_sheets/sources/new-comment/new-comment.mjs b/components/google_sheets/sources/new-comment/new-comment.mjs new file mode 100644 index 0000000000000..12c9cf56250fa --- /dev/null +++ b/components/google_sheets/sources/new-comment/new-comment.mjs @@ -0,0 +1,62 @@ +import httpBase from "../common/http-based/sheet.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...httpBase, + key: "google_sheets-new-comment", + name: "New Comment (Instant)", + description: "Emit new event each time a comment is added to a spreadsheet.", + version: "0.0.5", + dedupe: "unique", + type: "source", + methods: { + ...httpBase.methods, + _getLastTs() { + return this.db.get("lastTs"); + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + generateMeta(comment) { + return { + id: comment.id, + summary: `New Comment: ${comment.content}`, + ts: Date.parse(comment.createdTime), + }; + }, + takeSheetSnapshot() {}, + getSheetId() { + return this.sheetID.toString(); + }, + async processSpreadsheet() { + const comments = []; + const lastTs = this._getLastTs(); + const results = this.googleSheets.listComments(this.sheetID, lastTs); + for await (const comment of results) { + comments.push(comment); + } + if (!comments.length) { + return; + } + this._setLastTs(comments[0].createdTime); + comments.reverse().forEach((comment) => { + const meta = this.generateMeta(comment); + this.$emit(comment, meta); + }); + }, + }, + async run(event) { + if (event.timestamp) { + // Component was invoked by timer + return this.renewSubscription(); + } + + if (!this.isEventRelevant(event)) { + console.log("Sync notification, exiting early"); + return; + } + + await this.processSpreadsheet(); + }, + sampleEmit, +}; diff --git a/components/google_sheets/sources/new-comment/test-event.mjs b/components/google_sheets/sources/new-comment/test-event.mjs new file mode 100644 index 0000000000000..cf1631d72304f --- /dev/null +++ b/components/google_sheets/sources/new-comment/test-event.mjs @@ -0,0 +1,21 @@ +export default { + "id": "AAABM3vICvg", + "kind": "drive#comment", + "createdTime": "2024-05-08T21:32:04.823Z", + "modifiedTime": "2024-05-08T21:32:04.823Z", + "anchor": "{\"type\":\"workbook-range\",\"uid\":0,\"range\":\"1600938329\"}", + "replies": [], + "author": { + "displayName": "Test User", + "kind": "drive#user", + "me": true, + "photoLink": "//lh3.googleusercontent.com/a/ACg8ocKv3FxHiUdLT981ghC9w01W50yqe5fi2XWOSA4TgnZf8pCxmg=s50-c-k-no" + }, + "deleted": false, + "htmlContent": "comment", + "content": "comment", + "quotedFileContent": { + "mimeType": "text/html", + "value": "1" + } +} \ No newline at end of file diff --git a/components/google_sheets/sources/new-row-added-shared-drive/new-row-added-shared-drive.mjs b/components/google_sheets/sources/new-row-added-shared-drive/new-row-added-shared-drive.mjs deleted file mode 100644 index c009108ccdca9..0000000000000 --- a/components/google_sheets/sources/new-row-added-shared-drive/new-row-added-shared-drive.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import httpBase from "../common/http-based/drive.mjs"; -import sampleEmit from "./test-event.mjs"; -import newRowAdded from "../common/new-row-added.mjs"; - -export default { - ...httpBase, - ...newRowAdded, - key: "google_sheets-new-row-added-shared-drive", - name: "New Row Added (Shared Drive, Instant)", - description: "Emit new events each time a row or rows are added to the bottom of a spreadsheet in a shared drive", - version: "0.1.3", - dedupe: "unique", - type: "source", - props: { - ...httpBase.props, - ...newRowAdded.props, - }, - methods: { - ...httpBase.methods, - ...newRowAdded.methods, - }, - sampleEmit, -}; diff --git a/components/google_sheets/sources/new-row-added-shared-drive/test-event.mjs b/components/google_sheets/sources/new-row-added-shared-drive/test-event.mjs deleted file mode 100644 index 8d4f37bee6460..0000000000000 --- a/components/google_sheets/sources/new-row-added-shared-drive/test-event.mjs +++ /dev/null @@ -1,19 +0,0 @@ -export default { - "newRow": [ - "BA" - ], - "range": "Sheet2!6:1000", - "worksheet": { - "properties": { - "sheetId": 29223230, - "title": "Sheet2", - "index": 1, - "sheetType": "GRID", - "gridProperties": { - "rowCount": 1000, - "columnCount": 26 - } - } - }, - "rowNumber": 10 - } \ No newline at end of file diff --git a/components/google_sheets/sources/new-row-added/new-row-added.mjs b/components/google_sheets/sources/new-row-added/new-row-added.mjs index 7dc1b2035af30..3c7bdeceb0c40 100644 --- a/components/google_sheets/sources/new-row-added/new-row-added.mjs +++ b/components/google_sheets/sources/new-row-added/new-row-added.mjs @@ -1,13 +1,14 @@ import httpBase from "../common/http-based/sheet.mjs"; import newRowAdded from "../common/new-row-added.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...httpBase, ...newRowAdded, key: "google_sheets-new-row-added", name: "New Row Added (Instant)", - description: "Emit new events each time a row or rows are added to the bottom of a spreadsheet. To use this source with a spreadsheet in a [Shared Drive](https://support.google.com/a/users/answer/9310351), use the **New Row Added (Shared Drive, Instant)** source instead.", - version: "0.1.2", + description: "Emit new event each time a row or rows are added to the bottom of a spreadsheet.", + version: "0.1.9", dedupe: "unique", type: "source", props: { @@ -18,4 +19,5 @@ export default { ...httpBase.methods, ...newRowAdded.methods, }, + sampleEmit, }; diff --git a/components/google_sheets/sources/new-row-added/test-event.mjs b/components/google_sheets/sources/new-row-added/test-event.mjs new file mode 100644 index 0000000000000..4df198f8611f2 --- /dev/null +++ b/components/google_sheets/sources/new-row-added/test-event.mjs @@ -0,0 +1,19 @@ +export default { + "newRow": [ + "a" + ], + "range": "Sheet1!1:1000", + "worksheet": { + "properties": { + "sheetId": 1942312195, + "title": "Sheet1", + "index": 1, + "sheetType": "GRID", + "gridProperties": { + "rowCount": 1000, + "columnCount": 26 + } + } + }, + "rowNumber": 1 +} \ No newline at end of file diff --git a/components/google_sheets/sources/new-updates-shared-drive/new-updates-shared-drive.mjs b/components/google_sheets/sources/new-updates-shared-drive/new-updates-shared-drive.mjs deleted file mode 100644 index 92469f5d12155..0000000000000 --- a/components/google_sheets/sources/new-updates-shared-drive/new-updates-shared-drive.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import httpBase from "../common/http-based/drive.mjs"; -import sampleEmit from "./test-event.mjs"; -import newUpdates from "../common/new-updates.mjs"; - -export default { - ...httpBase, - ...newUpdates, - key: "google_sheets-new-updates-shared-drive", - type: "source", - name: "New Updates (Shared Drive, Instant)", - description: "Emit new event each time a row or cell is updated in a spreadsheet in a shared drive", - version: "0.2.0", - dedupe: "unique", - props: { - ...httpBase.props, - ...newUpdates.props, - }, - methods: { - ...httpBase.methods, - ...newUpdates.methods, - }, - sampleEmit, -}; diff --git a/components/google_sheets/sources/new-updates-shared-drive/test-event.mjs b/components/google_sheets/sources/new-updates-shared-drive/test-event.mjs deleted file mode 100644 index ed30007ffddc6..0000000000000 --- a/components/google_sheets/sources/new-updates-shared-drive/test-event.mjs +++ /dev/null @@ -1 +0,0 @@ -export default "{\n \"worksheet\": {\n \"properties\": {\n \"sheetId\": 358595775,\n \"title\": \"Test\",\n \"index\": 0,\n \"sheetType\": \"GRID\",\n \"gridProperties\": {\n \"rowCount\": 1031,\n \"columnCount\": 50\n }\n }\n },\n \"currentValues\": {\n \"values\": [\n [\n \"C1\",\n \"C2\",\n \"\",\n \"C3\",\n \"\",\n \"C5\",\n \"\",\n \"C7\",\n \"C8\"\n ],\n [\n \"V1\",\n \"V2\",\n \"V0002\",\n \"V3\",\n \"V4\",\n \"V5\",\n \"V0006\",\n \"V7\",\n \"V8\"\n ],\n [\n \"A\"\n ],\n [\n \"B\"\n ],\n [\n \"C1\"\n ]\n ],\n \"range\": \"Test!A1:AX1031\",\n \"majorDimension\": \"ROWS\"\n },\n \"changes\": [\n {\n \"cell\": \"A:5\",\n \"previous_value\": \"\",\n \"new_value\": \"C1\"\n }\n ]\n}" \ No newline at end of file diff --git a/components/google_sheets/sources/new-updates/new-updates.mjs b/components/google_sheets/sources/new-updates/new-updates.mjs index c3d0f9fc293cf..16b57f1f35376 100644 --- a/components/google_sheets/sources/new-updates/new-updates.mjs +++ b/components/google_sheets/sources/new-updates/new-updates.mjs @@ -8,8 +8,8 @@ export default { key: "google_sheets-new-updates", type: "source", name: "New Updates (Instant)", - description: "Emit new event each time a row or cell is updated in a spreadsheet. To use this source with a spreadsheet in a [Shared Drive](https://support.google.com/a/users/answer/9310351), use the **New Updates (Shared Drive, Instant)** source instead.", - version: "0.2.0", + description: "Emit new event each time a row or cell is updated in a spreadsheet.", + version: "0.2.7", dedupe: "unique", props: { ...httpBase.props, diff --git a/components/google_sheets/sources/new-worksheet-shared-drive/new-worksheet-shared-drive.mjs b/components/google_sheets/sources/new-worksheet-shared-drive/new-worksheet-shared-drive.mjs deleted file mode 100644 index acb9138efd5da..0000000000000 --- a/components/google_sheets/sources/new-worksheet-shared-drive/new-worksheet-shared-drive.mjs +++ /dev/null @@ -1,25 +0,0 @@ -import httpBase from "../common/http-based/drive.mjs"; -import newWorksheet from "../common/new-worksheet.mjs"; - -export default { - ...httpBase, - ...newWorksheet, - key: "google_sheets-new-worksheet-shared-drive", - type: "source", - name: "New Worksheet (Shared Drive, Instant)", - description: "Emit new event each time a new worksheet is created in a spreadsheet in a shared drive", - version: "0.1.2", - dedupe: "unique", - hooks: { - ...httpBase.hooks, - ...newWorksheet.hooks, - }, - props: { - ...httpBase.props, - ...newWorksheet.props, - }, - methods: { - ...httpBase.methods, - ...newWorksheet.methods, - }, -}; diff --git a/components/google_sheets/sources/new-worksheet/new-worksheet.mjs b/components/google_sheets/sources/new-worksheet/new-worksheet.mjs index eb9beebc2f7a6..0e6f1af5cf8d5 100644 --- a/components/google_sheets/sources/new-worksheet/new-worksheet.mjs +++ b/components/google_sheets/sources/new-worksheet/new-worksheet.mjs @@ -1,5 +1,6 @@ import httpBase from "../common/http-based/sheet.mjs"; import newWorksheet from "../common/new-worksheet.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...httpBase, @@ -7,8 +8,8 @@ export default { key: "google_sheets-new-worksheet", type: "source", name: "New Worksheet (Instant)", - description: "Emit new event each time a new worksheet is created in a spreadsheet. To use this source with a spreadsheet in a [Shared Drive](https://support.google.com/a/users/answer/9310351), use the **New Worksheet (Shared Drive, Instant)** source instead.", - version: "0.1.2", + description: "Emit new event each time a new worksheet is created in a spreadsheet.", + version: "0.1.10", dedupe: "unique", hooks: { ...httpBase.hooks, @@ -22,4 +23,5 @@ export default { ...httpBase.methods, ...newWorksheet.methods, }, + sampleEmit, }; diff --git a/components/google_sheets/sources/new-worksheet/test-event.mjs b/components/google_sheets/sources/new-worksheet/test-event.mjs new file mode 100644 index 0000000000000..2dbe48d891e23 --- /dev/null +++ b/components/google_sheets/sources/new-worksheet/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "properties": { + "sheetId": 0, + "title": "Sheet1", + "index": 0, + "sheetType": "GRID", + "gridProperties": { + "rowCount": 1007, + "columnCount": 28 + } + } +} \ No newline at end of file diff --git a/components/google_sheets/utils.mjs b/components/google_sheets/utils.mjs deleted file mode 100644 index a8c4216e8df78..0000000000000 --- a/components/google_sheets/utils.mjs +++ /dev/null @@ -1,47 +0,0 @@ -/** - * This method creates an object composed of the own and inherited enumerable string keyed - * properties of `object` whose keys are not empty strings - * - * @example - * // returns {} - * omitEmptykey({ "": "bar" }); - * - * @param {object} object the source object - * @param {...*} [object.omittedObj] the properties of `object` whose keys are not empty strings - * @returns {object} the new object - */ -function omitEmptyKey({ - /* eslint-disable-next-line no-unused-vars */ - "": _, ...omittedObj -} = {}) { - return omittedObj; -} - -/** - * Taken from {@linkcode ../aws/sources/common/utils.mjs utils.mjs}. - * A utility function that accepts a string as an argument and reformats it in - * order to remove newline characters and consecutive spaces. Useful when - * dealing with very long templated strings that are split into multiple lines. - * - * @example - * // returns "This is a much cleaner string" - * toSingleLineString(` - * This is a much - * cleaner string - * `); - * - * @param {string} multiLineString the input string to reformat - * @returns a formatted string based on the content of the input argument, - * without newlines and multiple spaces - */ -function toSingleLineString(multiLineString) { - return multiLineString - .trim() - .replace(/\n/g, " ") - .replace(/\s{2,}/g, " "); -} - -export { - omitEmptyKey, - toSingleLineString, -}; diff --git a/components/google_slides/README.md b/components/google_slides/README.md index 1f5ac63576429..f6a15ca0223c6 100644 --- a/components/google_slides/README.md +++ b/components/google_slides/README.md @@ -1,13 +1,11 @@ # Overview -Using the Google Slides API, you can build applications that do the following: - -- Create new presentations or edit existing ones -- Add or remove slides -- Add or remove slide content -- Change slide layouts -- Format text -- Insert or delete images -- Crop images -- Add or remove comments -- And more! +The Google Slides API allows you to create, read, and edit Google Slides presentations programmatically. With Pipedream's integration, you can automate your slide workflows, enabling dynamic content updates, collaboration enhancements, and streamlined reporting. By plugging into Pipedream's serverless platform, you can trigger slide creation or updates based on events from other apps, process data for presentations, and handle sharing and publishing tasks—all with minimal fuss. + +# Example Use Cases + +- **Automated Data Visualization Updates**: Create a workflow that pulls the latest sales data from a Google Sheets document. With the Google Slides API, you can update charts and tables in your company's weekly report presentation with fresh data, without manual intervention. + +- **Event-triggered Slideshow Creation**: Set up a trigger that listens for new product launches from your e-commerce platform. Once a launch is detected, use the Google Slides API to generate a product overview presentation, populating it with images, descriptions, and specifications pulled from your CMS. + +- **Collaboration and Sharing Workflow**: Implement a process where a new slide deck is crafted for each project kick-off. After the slides are created using the Google Slides API, integrate with Gmail or Slack to automatically notify team members, share the presentation, and collect feedback. diff --git a/components/google_slides/package.json b/components/google_slides/package.json index 0b2020347857f..c25733a48f2fc 100644 --- a/components/google_slides/package.json +++ b/components/google_slides/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/google_slides", - "version": "0.1.0", + "version": "0.1.1", "description": "Pipedream Google Slides Components", "main": "google_slides.app.mjs", "keywords": [ @@ -13,7 +13,6 @@ "@googleapis/slides": "^0.7.1", "@googleapis/drive": "^2.3.0", "@pipedream/platform": "^0.9.0", - "axios": "^0.21.1", "mime-db": "^1.51.0", "uuid": "^8.3.2" }, diff --git a/components/google_slides/sources/new-presentation/new-presentation.mjs b/components/google_slides/sources/new-presentation/new-presentation.mjs index 6f8b18035a065..03f67a74017f2 100644 --- a/components/google_slides/sources/new-presentation/new-presentation.mjs +++ b/components/google_slides/sources/new-presentation/new-presentation.mjs @@ -6,12 +6,12 @@ export default { type: "source", name: "New Presentation (Instant)", description: "Emit new event each time a new presentation is created in a drive.", - version: "0.0.2", + version: "0.0.3", hooks: { ...newFilesInstant.hooks, async deploy() { // Emit sample records on the first run - const slides = await this.getPresentations(10); + const slides = await this.getPresentations(5); for (const fileInfo of slides) { const createdTime = Date.parse(fileInfo.createdTime); this.$emit(fileInfo, { @@ -22,6 +22,14 @@ export default { } }, }, + props: { + ...newFilesInstant.props, + folders: { + ...newFilesInstant.props.folders, + description: + "(Optional) The folders you want to watch. Leave blank to watch for any new presentation in the Drive.", + }, + }, methods: { ...newFilesInstant.methods, shouldProcess(file) { diff --git a/components/google_tag_manager/README.md b/components/google_tag_manager/README.md new file mode 100644 index 0000000000000..ad6bda918673e --- /dev/null +++ b/components/google_tag_manager/README.md @@ -0,0 +1,12 @@ +# Overview + +The Google Tag Manager API allows you to manage your tags, triggers, variables, and related configurations programmatically. Through Pipedream, you can integrate these API capabilities into serverless workflows to automate complex tagging tasks, sync configurations across environments, and streamline marketing or analytics operations. Pipedream's platform facilitates the creation of custom automations with Google Tag Manager, helping you deploy tags faster, manage user permissions, and control versions of your tag configurations. + +# Example Use Cases + +- **Automated Tag Deployment**: Build a workflow that listens for new product additions in your e-commerce platform, such as Shopify, and automatically creates and deploys corresponding tags in Google Tag Manager to track product performance. + +- **Sync Configurations Across Environments**: Set up a Pipedream workflow that syncs tag configurations between staging and production environments in Google Tag Manager. Whenever a tag is updated or published in staging, the same changes can be applied to production automatically. + +- **User Access Management**: Create a workflow that automates the process of managing user permissions in Google Tag Manager. When a new team member is added to your project management tool, like Jira or Asana, the workflow can grant them the appropriate level of access in Google Tag Manager. +. diff --git a/components/google_tasks/README.md b/components/google_tasks/README.md index 8db6add3f4346..571b26668b4a8 100644 --- a/components/google_tasks/README.md +++ b/components/google_tasks/README.md @@ -1,10 +1,14 @@ # Overview -With the Google Tasks API, you can build applications that +The Google Tasks API allows you to manage and manipulate a user's tasks and task lists on Google Tasks directly from Pipedream. With this API, you can create, read, update, and delete tasks, as well as manage the lists themselves. This opens up possibilities for automating task management, syncing tasks with other systems, and creating custom task-based workflows that can increase productivity and ensure nothing falls through the cracks. -- Create new tasks -- Read, update, and delete existing tasks -- View task lists +# Example Use Cases + +- **Task Synchronization Workflow**: Sync tasks between Google Tasks and a project management tool like Trello or Asana. Every time a new task is added in Google Tasks, it triggers a Pipedream workflow that creates a corresponding card or task in the project management app. This keeps all team members aligned regardless of the tools they prefer to use. + +- **Email to Task Conversion**: Convert incoming emails from Gmail to tasks in Google Tasks. Use Pipedream to monitor a Gmail inbox for specific criteria, such as emails from a particular sender or emails marked with a specific label. When an email matches the criteria, the workflow creates a new task in Google Tasks with the email content as the task description, making sure you follow up on important messages. + +- **Daily Task Digest**: Generate a daily summary of tasks due and send it via a communication platform like Slack or Microsoft Teams. Pipedream can schedule a workflow that fetches tasks due within the next 24 hours from Google Tasks and then formats and sends a message with the list to a designated team channel or direct message, ensuring everyone is aware of their priorities for the day. # Troubleshooting diff --git a/components/google_vertex_ai/README.md b/components/google_vertex_ai/README.md new file mode 100644 index 0000000000000..bbdb0a285b2d2 --- /dev/null +++ b/components/google_vertex_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +With the Google Vertex AI API, you can tap into a robust suite of AI tools offered by Google Cloud to build, deploy, and scale machine learning models. Whether you're processing data, training custom models, or using pre-trained ones, Vertex AI provides a unified platform for AI development. In Pipedream, you can create serverless workflows that interact with Vertex AI, allowing you to automate tasks like model training, prediction, and resource management without provisioning your own infrastructure. + +# Example Use Cases + +- **Automate Machine Learning Workflows**: Trigger a Pipedream workflow to automatically train a Vertex AI model with new data by connecting to a data source like Google Cloud Storage. Once training is complete, you can deploy the model to an endpoint and notify your team via Slack. + +- **Process Data with Vertex AI Pipelines**: Integrate Vertex AI with Pipedream's cron scheduler to run preprocessing jobs on datasets at set intervals. Use the results to update models, perform feature engineering, or push insights to a Google Sheets spreadsheet for easy analysis and reporting. + +- **Real-time Predictions with Event Triggers**: Set up a webhook in Pipedream to receive real-time data from a source app like Stripe for transaction fraud detection. Pass the data to Vertex AI for prediction and, based on the response, trigger an automated action, such as flagging the transaction, or emailing a summary report using SendGrid. diff --git a/components/google_vertex_ai/actions/analyze-image-video/analyze-image-video.mjs b/components/google_vertex_ai/actions/analyze-image-video/analyze-image-video.mjs new file mode 100644 index 0000000000000..0e125e97e7d9a --- /dev/null +++ b/components/google_vertex_ai/actions/analyze-image-video/analyze-image-video.mjs @@ -0,0 +1,62 @@ +import vertexAi from "../../google_vertex_ai.app.mjs"; + +export default { + key: "google_vertex_ai-analyze-image-video", + name: "Analyze Image/Video", + description: "Examines an image or video following given instructions. Results will contain the analysis findings. [See the documentation](https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.publishers.models/generateContent)", + version: "0.0.1", + type: "action", + props: { + vertexAi, + projectId: { + propDefinition: [ + vertexAi, + "projectId", + ], + }, + instructions: { + type: "string", + label: "Instructions", + description: "The rules for analysis of the input image/video", + }, + url: { + type: "string", + label: "URL", + description: "The URL of the file or video to analyze. Only GCS URIs are supported. Please make sure that the path is a valid GCS path. Example: `gs://cloud-samples-data/generative-ai/image/mount_fuji.jpg`", + }, + mimeType: { + type: "string", + label: "Mime Type", + description: "The mimeType of the image or video to analyze", + optional: true, + default: "image/jpeg", + }, + }, + async run({ $ }) { + const response = await this.vertexAi.generateContent({ + $, + projectId: this.projectId, + model: "gemini-1.0-pro-vision", + data: { + contents: [ + { + role: "user", + parts: [ + { + fileData: { + mimeType: this.mimeType, + fileUri: this.url, + }, + }, + { + text: this.instructions, + }, + ], + }, + ], + }, + }); + $.export("$summary", "Successfully analyzed resource"); + return response; + }, +}; diff --git a/components/google_vertex_ai/actions/analyze-text-sentiment/analyze-text-sentiment.mjs b/components/google_vertex_ai/actions/analyze-text-sentiment/analyze-text-sentiment.mjs new file mode 100644 index 0000000000000..ff9560cd4bded --- /dev/null +++ b/components/google_vertex_ai/actions/analyze-text-sentiment/analyze-text-sentiment.mjs @@ -0,0 +1,44 @@ +import vertexAi from "../../google_vertex_ai.app.mjs"; + +export default { + key: "google_vertex_ai-analyze-text-sentiment", + name: "Analyze Text Sentiment", + description: "Analyzes a specified text for its underlying sentiment. [See the documentation](https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.publishers.models/generateContent)", + version: "0.0.1", + type: "action", + props: { + vertexAi, + projectId: { + propDefinition: [ + vertexAi, + "projectId", + ], + }, + text: { + type: "string", + label: "Text", + description: "The content to analyze for sentiment", + }, + }, + async run({ $ }) { + const response = await this.vertexAi.generateContent({ + $, + projectId: this.projectId, + model: "gemini-1.0-pro", + data: { + contents: [ + { + role: "user", + parts: [ + { + text: `${this.text}\nClassify the sentiment of the message:\n`, + }, + ], + }, + ], + }, + }); + $.export("$summary", "Successfully analyzed text sentiment"); + return response; + }, +}; diff --git a/components/google_vertex_ai/actions/classify-text/classify-text.mjs b/components/google_vertex_ai/actions/classify-text/classify-text.mjs new file mode 100644 index 0000000000000..7f2657650701f --- /dev/null +++ b/components/google_vertex_ai/actions/classify-text/classify-text.mjs @@ -0,0 +1,50 @@ +import vertexAi from "../../google_vertex_ai.app.mjs"; + +export default { + key: "google_vertex_ai-classify-text", + name: "Classify Text", + description: "Groups a provided text into predefined categories. [See the documentation](https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.publishers.models/generateContent)", + version: "0.0.1", + type: "action", + props: { + vertexAi, + projectId: { + propDefinition: [ + vertexAi, + "projectId", + ], + }, + categories: { + type: "string[]", + label: "Categories", + description: "The categories to group the text into", + }, + text: { + type: "string", + label: "Text", + description: "The text to classify", + }, + }, + async run({ $ }) { + const categories = (this.categories.map((category) => `\n- ${category}`)).join(""); + const response = await this.vertexAi.generateContent({ + $, + projectId: this.projectId, + model: "gemini-1.0-pro", + data: { + contents: [ + { + role: "user", + parts: [ + { + text: `Define the categories for the text below?\nOptions:${categories}\n\nText: ${this.text}`, + }, + ], + }, + ], + }, + }); + $.export("$summary", "Text classified into categories"); + return response; + }, +}; diff --git a/components/google_vertex_ai/google_vertex_ai.app.mjs b/components/google_vertex_ai/google_vertex_ai.app.mjs new file mode 100644 index 0000000000000..b85d7c3109141 --- /dev/null +++ b/components/google_vertex_ai/google_vertex_ai.app.mjs @@ -0,0 +1,71 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "google_vertex_ai", + propDefinitions: { + projectId: { + type: "string", + label: "Project ID", + description: "Identifier of a project", + async options({ prevContext }) { + const params = prevContext.nextPageToken + ? { + pageToken: prevContext.nextPageToken, + } + : {}; + const { + projects, nextPageToken, + } = await this.listProjects({ + params, + }); + const options = projects?.map(({ + projectId: value, name: label, + }) => ({ + value, + label, + })) || []; + return { + options, + context: { + nextPageToken, + }, + }; + }, + }, + }, + methods: { + _baseUrl() { + return "https://us-central1-aiplatform.googleapis.com/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }, + ...otherOpts, + }); + }, + listProjects(opts = {}) { + return this._makeRequest({ + url: "https://cloudresourcemanager.googleapis.com/v1/projects", + ...opts, + }); + }, + generateContent({ + projectId, model, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/projects/${projectId}/locations/us-central1/publishers/google/models/${model}:generateContent`, + ...opts, + }); + }, + }, +}; diff --git a/components/google_vertex_ai/package.json b/components/google_vertex_ai/package.json new file mode 100644 index 0000000000000..a7641148f313d --- /dev/null +++ b/components/google_vertex_ai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/google_vertex_ai", + "version": "0.1.0", + "description": "Pipedream Google Vertex AI Components", + "main": "google_vertex_ai.app.mjs", + "keywords": [ + "pipedream", + "google_vertex_ai" + ], + "homepage": "https://pipedream.com/apps/google_vertex_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.4" + } +} diff --git a/components/google_workspace/README.md b/components/google_workspace/README.md index 64fd7d34544dd..1630c87f9f4fa 100644 --- a/components/google_workspace/README.md +++ b/components/google_workspace/README.md @@ -1,11 +1,11 @@ # Overview -With the Google Workspace Admin API, you can build tools to help manage your -Google Workspace account. For example, you can: - -- Create and manage user accounts -- Add and remove users from groups -- Update user settings -- Suspend or delete user accounts -- View audit logs -- Generate reports +The Google Workspace Admin API unlocks the power to manage your organization's Google Workspace services. With Pipedream, leverage this API to automate user account management tasks, control devices, manage groups, and gain insights through reports. Simplify complex operational workflows and keep your Workspace environment finely tuned and secure without manual intervention. + +# Example Use Cases + +- **Automated User Onboarding and Offboarding**: Streamline the process of adding or removing users from your organization. When an employee joins or leaves, trigger a workflow to create or delete a user account, assign or revoke licenses, and update group memberships, all synced with your HR management system. + +- **Security Monitoring and Alerts**: Build a monitoring system that checks for security anomalies or compliance issues. Set up workflows to audit password changes, 2-factor authentication enrollment, or file sharing permissions. Should a discrepancy arise, automatically notify your security team or enforce policy changes. + +- **Directory Synchronization across Cloud Services**: Keep user data consistent across various cloud platforms. Whenever there's a change in a user's profile in your HR database, a Pipedream workflow can sync those updates to Google Workspace, ensuring that contact information and organizational structure are always up to date. diff --git a/components/google_workspace/package.json b/components/google_workspace/package.json index 1b62d408d2ff4..308217fed7dfc 100644 --- a/components/google_workspace/package.json +++ b/components/google_workspace/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/google_workspace", - "version": "0.0.6", + "version": "0.0.7", "description": "Pipedream Google Workspace Admin Components", "main": "dist/app/google_workspace.app.mjs", "keywords": [ diff --git a/components/google_workspace/sources/common.ts b/components/google_workspace/sources/common.ts index 1456881b2b55e..33e9b059b0d20 100644 --- a/components/google_workspace/sources/common.ts +++ b/components/google_workspace/sources/common.ts @@ -1,6 +1,9 @@ import admin from "@googleapis/admin"; import { uuid } from "uuidv4"; -import { ConfigurationError, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; import googleWorkspace from "../app/google_workspace.app"; import constants from "../common/constants"; @@ -123,6 +126,14 @@ export default { const expirationTSInMS = this.getExpirationTS(); const token = this.getToken(); + if (expirationTSInMS && expirationTSInMS < currentTSInMS) { + await this.renewWebhook(); + } + + if (!headers) { + return console.log("Timer triggered!"); + } + if (headers?.["x-goog-channel-token"] !== token) { throw new ConfigurationError("Webhook token is not valid!"); } @@ -139,10 +150,6 @@ export default { }; this.$emit(data, this.getMetadata(data)); }); - - if (expirationTSInMS && expirationTSInMS < currentTSInMS) { - await this.renewWebhook(); - } }, }; diff --git a/components/google_workspace/sources/new-admin-activity-by-app-name/new-admin-activity-by-app-name.ts b/components/google_workspace/sources/new-admin-activity-by-app-name/new-admin-activity-by-app-name.ts index d5e50ecf899cc..2c2458bb0164b 100644 --- a/components/google_workspace/sources/new-admin-activity-by-app-name/new-admin-activity-by-app-name.ts +++ b/components/google_workspace/sources/new-admin-activity-by-app-name/new-admin-activity-by-app-name.ts @@ -6,7 +6,7 @@ export default defineSource({ key: "google_workspace-new-admin-activity-by-app-name", name: "New Admin Activity By Application Name", description: "Emit new admin activities by selected user", - version: "0.0.3", + version: "0.0.4", type: "source", dedupe: "unique", props: { diff --git a/components/google_workspace/sources/new-admin-activity-by-user-and-app-name/new-admin-activity-by-user-and-app-name.ts b/components/google_workspace/sources/new-admin-activity-by-user-and-app-name/new-admin-activity-by-user-and-app-name.ts index 79a88ceb9a19f..eedddaa6af93a 100644 --- a/components/google_workspace/sources/new-admin-activity-by-user-and-app-name/new-admin-activity-by-user-and-app-name.ts +++ b/components/google_workspace/sources/new-admin-activity-by-user-and-app-name/new-admin-activity-by-user-and-app-name.ts @@ -6,7 +6,7 @@ export default defineSource({ key: "google_workspace-new-admin-activity-by-user-and-app-name", name: "New Admin Activity By User And Application Name", description: "Emit new admin activities by selected user and application name", - version: "0.0.3", + version: "0.0.4", type: "source", dedupe: "unique", props: { diff --git a/components/google_workspace/sources/new-admin-activity-by-user/new-admin-activity-by-user.ts b/components/google_workspace/sources/new-admin-activity-by-user/new-admin-activity-by-user.ts index 70e96db7dd51d..113a12bf6ad1c 100644 --- a/components/google_workspace/sources/new-admin-activity-by-user/new-admin-activity-by-user.ts +++ b/components/google_workspace/sources/new-admin-activity-by-user/new-admin-activity-by-user.ts @@ -6,7 +6,7 @@ export default defineSource({ key: "google_workspace-new-admin-activity-by-user", name: "New Admin Activity By User", description: "Emit new admin activities by selected user", - version: "0.0.3", + version: "0.0.4", type: "source", dedupe: "unique", props: { diff --git a/components/google_workspace/sources/new-admin-activity/new-admin-activity.ts b/components/google_workspace/sources/new-admin-activity/new-admin-activity.ts index b92451b26ecd6..057b4b59d6449 100644 --- a/components/google_workspace/sources/new-admin-activity/new-admin-activity.ts +++ b/components/google_workspace/sources/new-admin-activity/new-admin-activity.ts @@ -6,7 +6,7 @@ export default defineSource({ key: "google_workspace-new-admin-activity", name: "New Admin Activity", description: "Emit new admin activities", - version: "0.0.3", + version: "0.0.4", type: "source", dedupe: "unique", methods: { diff --git a/components/gorgias/package.json b/components/gorgias/package.json new file mode 100644 index 0000000000000..a92476109da75 --- /dev/null +++ b/components/gorgias/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/gorgias", + "version": "0.6.0", + "description": "Pipedream gorgias Components", + "main": "gorgias.app.mjs", + "keywords": [ + "pipedream", + "gorgias" + ], + "homepage": "https://pipedream.com/apps/gorgias", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/gorgias_oauth/README.md b/components/gorgias_oauth/README.md index af8f8fa6a811e..d0d3d0153a11f 100644 --- a/components/gorgias_oauth/README.md +++ b/components/gorgias_oauth/README.md @@ -1,13 +1,11 @@ # Overview -With the Gorgias API, you can build integrations that allow your customers to: - -- Login to your app using their Gorgias account -- Connect their Gorgias account to your app -- View and manage their Gorgias account settings -- Edit their Gorgias profile -- Access their Gorgias contact list -- Send and receive Gorgias messages -- Perform Gorgias searches -- View and manage their Gorgias calendar -- Receive real-time updates from Gorgias +Gorgias is a helpdesk platform that centralizes customer interactions and automates common support tasks. Utilizing the Gorgias API on Pipedream allows you to streamline customer service processes by creating automated workflows. With these integrations, you can sync customer data, manage tickets, automate responses, and track support metrics, effectively turning Gorgias into an engine for customer support automation. + +# Example Use Cases + +- **Sync Support Tickets with CRM**: Automatically create or update customer profiles in a CRM like Salesforce when a new support ticket is created in Gorgias. This ensures that customer support interactions are logged and accessible for sales or support teams, providing a 360-degree view of customer communications. + +- **Automate Customer Satisfaction Surveys**: Following the resolution of a support ticket, trigger a workflow that sends out a customer satisfaction survey using a survey platform such as Typeform. The responses can be collected and analyzed for insights into the support experience, driving improvements in service quality. + +- **Slack Notifications for Urgent Tickets**: Set up a workflow that monitors support tickets for specific keywords or tags indicating urgency, and automatically sends a notification to a designated Slack channel. This enables immediate team collaboration to resolve high-priority issues faster. diff --git a/components/gorgias_oauth/actions/create-customer/create-customer.mjs b/components/gorgias_oauth/actions/create-customer/create-customer.mjs index aa43db08972fc..5ebf75f3e9189 100644 --- a/components/gorgias_oauth/actions/create-customer/create-customer.mjs +++ b/components/gorgias_oauth/actions/create-customer/create-customer.mjs @@ -1,11 +1,11 @@ -import gorgias_oauth from "../../gorgias_oauth.app.mjs"; import channels from "../../common/customer-channels.mjs"; +import gorgias_oauth from "../../gorgias_oauth.app.mjs"; export default { key: "gorgias_oauth-create-customer", name: "Create Customer", description: "Create a new customer. [See the docs](https://developers.gorgias.com/reference/post_api-customers)", - version: "0.0.3", + version: "0.0.6", type: "action", props: { gorgias_oauth, diff --git a/components/gorgias_oauth/actions/create-ticket-message/create-ticket-message.mjs b/components/gorgias_oauth/actions/create-ticket-message/create-ticket-message.mjs new file mode 100644 index 0000000000000..faaa171588977 --- /dev/null +++ b/components/gorgias_oauth/actions/create-ticket-message/create-ticket-message.mjs @@ -0,0 +1,217 @@ +import gorgiasOauth from "../../gorgias_oauth.app.mjs"; +import { + axios, ConfigurationError, +} from "@pipedream/platform"; + +export default { + key: "gorgias_oauth-create-ticket-message", + name: "Create Ticket Message", + description: "Create a message for a ticket in the Gorgias system. [See the documentation](https://developers.gorgias.com/reference/create-ticket-message)", + version: "0.0.2", + type: "action", + props: { + gorgiasOauth, + ticketId: { + propDefinition: [ + gorgiasOauth, + "ticketId", + ], + description: "The ID of the ticket to add a message to", + }, + fromAgent: { + type: "boolean", + label: "From Agent", + description: "Whether the message was sent by your company to a customer, or the opposite", + reloadProps: true, + }, + fromUser: { + propDefinition: [ + gorgiasOauth, + "userId", + ], + label: "From User", + description: "User who sent the message", + optional: false, + hidden: true, + }, + toUser: { + propDefinition: [ + gorgiasOauth, + "userId", + ], + label: "To User", + description: "The user receiving the message", + optional: false, + hidden: true, + }, + fromCustomer: { + propDefinition: [ + gorgiasOauth, + "customerId", + ], + label: "From Customer", + description: "The customer who sent the message", + hidden: true, + }, + toCustomer: { + propDefinition: [ + gorgiasOauth, + "customerId", + ], + label: "To Customer", + description: "The customer receiving the message", + hidden: true, + }, + message: { + type: "string", + label: "Message", + description: "Message of the ticket. Accepts HTML", + }, + via: { + propDefinition: [ + gorgiasOauth, + "via", + ], + optional: false, + }, + subject: { + propDefinition: [ + gorgiasOauth, + "subject", + ], + optional: true, + }, + attachmentUrl: { + type: "string", + label: "Attachment URL", + description: "The URL to access to the attached file", + optional: true, + }, + attachmentName: { + type: "string", + label: "Attachment File Name", + description: "The name of the file to attach", + optional: true, + }, + sentDatetime: { + type: "string", + label: "Sent Datetime", + description: "When the message was sent. If ommited, the message will be sent by Gorgias. E.g. `2025-01-27T19:38:52.028Z`", + optional: true, + }, + }, + additionalProps(props) { + props.toUser.hidden = this.fromAgent; + props.fromCustomer.hidden = this.fromAgent; + props.toCustomer.hidden = !this.fromAgent; + props.fromUser.hidden = !this.fromAgent; + return {}; + }, + methods: { + async getAttachmentInfo($, url) { + const { headers } = await axios($, { + method: "HEAD", + url, + returnFullResponse: true, + }); + return { + contentType: headers["content-type"], + size: headers["content-length"], + }; + }, + async getEmail($, id, type = "from") { + const { + gorgiasOauth: { + retrieveUser, retrieveCustomer, + }, + } = this; + const fn = this.fromAgent + ? type === "from" + ? retrieveUser + : retrieveCustomer + : type === "from" + ? retrieveCustomer + : retrieveUser; + + const { email } = await fn({ + $, + id, + }); + return email; + }, + }, + async run({ $ }) { + if ((this.attachmentUrl && !this.attachmentName) + || (!this.attachmentUrl && this.attachmentName) + ) { + throw new ConfigurationError("Must enter both Attachment URL and Attachment File Name"); + } + + let contentType, size; + if (this.attachmentUrl) { + ({ + contentType, size, + } = await this.getAttachmentInfo($, this.attachmentUrl)); + } + + const fromId = this.fromAgent + ? this.fromUser + : this.fromCustomer; + + const toId = this.fromAgent + ? this.toCustomer + : this.toUser; + + if (!fromId) { + throw new ConfigurationError(`"${this.fromAgent + ? "From User" + : "From Customer"}" is required when "From Agent" is set to \`${this.fromAgent}\``); + } + if (!toId) { + throw new ConfigurationError(`"${this.fromAgent + ? "To Customer" + : "To User"}" is required when "From Agent" is set to \`${this.fromAgent}\``); + } + + const response = await this.gorgiasOauth.createMessage({ + $, + ticketId: this.ticketId, + data: { + channel: "email", + source: { + from: { + address: await this.getEmail($, fromId, "from"), + }, + to: [ + { + address: await this.getEmail($, toId, "to"), + }, + ], + }, + body_html: this.message, + via: this.via, + subject: this.subject, + from_agent: this.fromAgent, + sent_datetime: this.sentDatetime, + attachments: this.attachmentUrl && [ + { + url: this.attachmentUrl, + name: this.attachmentName, + content_type: contentType, + size, + }, + ], + sender: { + id: fromId, + }, + receiver: { + id: toId, + }, + }, + }); + + $.export("$summary", `Succesfully created ticket message with ID: ${response.id}`); + + return response; + }, +}; diff --git a/components/gorgias_oauth/actions/create-ticket/create-ticket.mjs b/components/gorgias_oauth/actions/create-ticket/create-ticket.mjs index e5a7c2dca2a97..2ce3fddb9fd33 100644 --- a/components/gorgias_oauth/actions/create-ticket/create-ticket.mjs +++ b/components/gorgias_oauth/actions/create-ticket/create-ticket.mjs @@ -4,7 +4,7 @@ export default { key: "gorgias_oauth-create-ticket", name: "Create Ticket", description: "Create a new ticket. [See the docs](https://developers.gorgias.com/reference/post_api-tickets)", - version: "0.0.4", + version: "0.0.7", type: "action", props: { gorgias_oauth, @@ -16,9 +16,10 @@ export default { label: "From Address", }, subject: { - type: "string", - label: "Subject", - description: "The subject of the message", + propDefinition: [ + gorgias_oauth, + "subject", + ], }, message: { type: "string", diff --git a/components/gorgias_oauth/actions/list-tickets/list-tickets.mjs b/components/gorgias_oauth/actions/list-tickets/list-tickets.mjs index 1972032045e5d..0684b34552830 100644 --- a/components/gorgias_oauth/actions/list-tickets/list-tickets.mjs +++ b/components/gorgias_oauth/actions/list-tickets/list-tickets.mjs @@ -4,7 +4,7 @@ export default { key: "gorgias_oauth-list-tickets", name: "List Tickets", description: "List all tickets. [See the docs](https://developers.gorgias.com/reference/get_api-tickets)", - version: "0.0.4", + version: "0.0.7", type: "action", props: { gorgias_oauth, diff --git a/components/gorgias_oauth/actions/retrieve-customer/retrieve-customer.mjs b/components/gorgias_oauth/actions/retrieve-customer/retrieve-customer.mjs index 3860de5e7752b..5638146b8e8fb 100644 --- a/components/gorgias_oauth/actions/retrieve-customer/retrieve-customer.mjs +++ b/components/gorgias_oauth/actions/retrieve-customer/retrieve-customer.mjs @@ -4,7 +4,7 @@ export default { key: "gorgias_oauth-retrieve-customer", name: "Retrieve a Customer", description: "Retrieve a customer. [See the docs](https://developers.gorgias.com/reference/get_api-customers-id-)", - version: "0.0.3", + version: "0.0.6", type: "action", props: { gorgias_oauth, diff --git a/components/gorgias_oauth/actions/update-customer/update-customer.mjs b/components/gorgias_oauth/actions/update-customer/update-customer.mjs index d5be082264e83..2876bd520ccea 100644 --- a/components/gorgias_oauth/actions/update-customer/update-customer.mjs +++ b/components/gorgias_oauth/actions/update-customer/update-customer.mjs @@ -1,16 +1,16 @@ -import gorgias_oauth from "../../gorgias_oauth.app.mjs"; -import channels from "../../common/customer-channels.mjs"; import { ConfigurationError } from "@pipedream/platform"; import { pick, pickBy, } from "lodash-es"; +import channels from "../../common/customer-channels.mjs"; +import gorgias_oauth from "../../gorgias_oauth.app.mjs"; export default { key: "gorgias_oauth-update-customer", name: "Update Customer", description: "Update a customer. [See the docs](https://developers.gorgias.com/reference/put_api-customers-id-)", - version: "0.0.3", + version: "0.0.6", type: "action", props: { gorgias_oauth, diff --git a/components/gorgias_oauth/actions/update-ticket/update-ticket.mjs b/components/gorgias_oauth/actions/update-ticket/update-ticket.mjs new file mode 100644 index 0000000000000..6a1a6976cf482 --- /dev/null +++ b/components/gorgias_oauth/actions/update-ticket/update-ticket.mjs @@ -0,0 +1,152 @@ +import { parseObject } from "../../common/utils.mjs"; +import gorgiasOauth from "../../gorgias_oauth.app.mjs"; + +export default { + key: "gorgias_oauth-update-ticket", + name: "Update Ticket", + description: "Updates a predefined ticket in the Gorgias system. [See the documentation](https://developers.gorgias.com/reference/update-ticket)", + version: "0.0.3", + type: "action", + props: { + gorgiasOauth, + ticketId: { + propDefinition: [ + gorgiasOauth, + "ticketId", + ], + }, + assigneeTeamId: { + propDefinition: [ + gorgiasOauth, + "assigneeTeamId", + ], + optional: true, + }, + assigneeUserId: { + propDefinition: [ + gorgiasOauth, + "userId", + ], + optional: true, + }, + channel: { + propDefinition: [ + gorgiasOauth, + "channel", + ], + }, + closedDatetime: { + type: "string", + label: "Closed Datetime", + description: "When the ticket was closed (ISO 8601 format)", + optional: true, + }, + customerId: { + propDefinition: [ + gorgiasOauth, + "customerId", + ], + optional: true, + }, + customerEmail: { + type: "string", + label: "Customer Email", + description: "The email of the customer linked to the ticket", + optional: true, + }, + externalId: { + propDefinition: [ + gorgiasOauth, + "externalId", + ], + }, + fromAgent: { + type: "boolean", + label: "From Agent", + description: "Whether the first message of the ticket was sent by your company to a customer, or the opposite", + optional: true, + }, + isUnread: { + type: "boolean", + label: "Is Unread", + description: "Whether the ticket is unread for you", + optional: true, + }, + language: { + propDefinition: [ + gorgiasOauth, + "language", + ], + }, + spam: { + type: "boolean", + label: "Spam", + description: "Whether the ticket is considered as spam or not", + optional: true, + }, + status: { + type: "string", + label: "Status", + description: "The status of the ticket. Default: `open`", + options: [ + "open", + "closed", + ], + optional: true, + }, + subject: { + propDefinition: [ + gorgiasOauth, + "subject", + ], + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "Tags linked to the ticket", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.gorgiasOauth.updateTicket({ + $, + ticketId: this.ticketId, + data: { + assignee_team: this.assigneeTeamId + ? { + id: this.assigneeTeamId, + } + : undefined, + assignee_user: this.assigneeUserId + ? { + id: this.assigneeUserId, + } + : undefined, + channel: this.channel, + closed_datetime: this.closedDatetime, + customer: this.customerId + ? { + id: this.customerId, + email: this.customerEmail, + } + : undefined, + external_id: this.externalId, + from_agent: this.fromAgent, + is_unread: this.isUnread, + language: this.language, + spam: this.spam, + status: this.status, + subject: this.subject, + tags: this.tags + ? parseObject(this.tags)?.map((tag) => ({ + name: tag, + })) + : undefined, + }, + }); + + $.export("$summary", `Successfully updated ticket with ID ${this.ticketId}`); + return response; + }, +}; diff --git a/components/gorgias_oauth/common/utils.mjs b/components/gorgias_oauth/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/gorgias_oauth/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/gorgias_oauth/gorgias_oauth.app.mjs b/components/gorgias_oauth/gorgias_oauth.app.mjs index 506b983d51553..ccb76f930d129 100644 --- a/components/gorgias_oauth/gorgias_oauth.app.mjs +++ b/components/gorgias_oauth/gorgias_oauth.app.mjs @@ -60,7 +60,6 @@ export default { type: "integer", label: "Ticket ID", description: "The ID of a ticket to watch for new messages", - optional: true, async options({ prevContext }) { const { data: tickets, @@ -134,6 +133,73 @@ export default { description: "Maximum number to return", optional: true, }, + assigneeTeamId: { + type: "integer", + label: "Assignee Team ID", + description: "The ID of the team assigned to the ticket", + async options({ prevContext }) { + const { + data: teams, + meta, + } = await this.listTeams({ + params: { + cursor: prevContext.nextCursor, + }, + }); + return { + options: teams.map(({ + id: value, name: label, + }) => ({ + label, + value, + })), + context: { + nextCursor: meta.next_cursor, + }, + }; + }, + }, + assigneeUserId: { + type: "integer", + label: "Assignee User ID", + description: "The ID of the user assigned to the ticket", + }, + subject: { + type: "string", + label: "Subject", + description: "The subject of the ticket", + }, + tagId: { + type: "string", + label: "Tag ID", + description: "The tag id.", + optional: true, + async options({ prevContext: { cursor } }) { + if (cursor === null) { + return []; + } + const { + meta: { next_cursor: nextCursor }, + data: tags, + } = await this.listTags({ + params: { + cursor, + }, + }); + const options = tags.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + return { + options, + context: { + cursor: nextCursor, + }, + }; + }, + }, }, methods: { _defaultConfig({ @@ -263,6 +329,14 @@ export default { path: `customers/${id}`, }); }, + async retrieveUser({ + $, id, + }) { + return this._makeRequest({ + $, + path: `users/${id}`, + }); + }, async listCustomers({ $, params, }) { @@ -299,11 +373,42 @@ export default { path: `tickets/${id}`, }); }, + async updateTicket({ + ticketId, + ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/tickets/${ticketId}`, + ...opts, + }); + }, listUsers(opts = {}) { return this._makeRequest({ path: "/users", ...opts, }); }, + listTeams(opts = {}) { + return this._makeRequest({ + path: "/teams", + ...opts, + }); + }, + listTags(opts = {}) { + return this._makeRequest({ + path: "/tags", + ...opts, + }); + }, + createMessage({ + ticketId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/tickets/${ticketId}/messages`, + ...opts, + }); + }, }, }; diff --git a/components/gorgias_oauth/package.json b/components/gorgias_oauth/package.json index da2cdad981f99..ad3a6cef51a8a 100644 --- a/components/gorgias_oauth/package.json +++ b/components/gorgias_oauth/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/gorgias_oauth", - "version": "0.3.9", + "version": "0.5.1", "description": "Pipedream Gorgias OAuth Components", "main": "gorgias_oauth.app.mjs", "keywords": [ @@ -15,7 +15,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.4.1", + "@pipedream/platform": "^2.0.0", "lodash-es": "^4.17.21" } } diff --git a/components/gorgias_oauth/sources/ticket-created/ticket-created.mjs b/components/gorgias_oauth/sources/ticket-created/ticket-created.mjs index 92cb622ac9963..4e2d69d7d6c80 100644 --- a/components/gorgias_oauth/sources/ticket-created/ticket-created.mjs +++ b/components/gorgias_oauth/sources/ticket-created/ticket-created.mjs @@ -7,7 +7,7 @@ export default { key: "gorgias_oauth-ticket-created", name: "New Ticket", description: "Emit new event when a ticket is created. [See the documentation](https://developers.gorgias.com/reference/the-event-object)", - version: "0.1.4", + version: "0.1.7", type: "source", props: { ...base.props, @@ -33,6 +33,15 @@ export default { "userId", ], }, + tagIds: { + type: "string[]", + label: "Tag IDs", + description: "The tag ids to filter tickets by.", + propDefinition: [ + base.props.gorgias_oauth, + "tagId", + ], + }, }, methods: { ...base.methods, @@ -40,9 +49,11 @@ export default { return eventTypes.TICKET_CREATED; }, isRelevant(ticket) { + const tagIds = ticket.tags.map(({ id }) => id); return (!this.channel || ticket.channel === this.channel) && (!this.via || ticket.via === this.via) - && (!this.assigneeId || ticket?.assignee_user_id === this.assigneeId); + && (!this.assigneeId || ticket?.assignee_user_id === this.assigneeId) + && (!this.tagIds || this.tagIds.some((tagId) => tagIds.includes(tagId))); }, async processHistoricalEvent(event) { const ticket = await this.retrieveTicket(event.object_id); diff --git a/components/gorgias_oauth/sources/ticket-message-created/ticket-message-created.mjs b/components/gorgias_oauth/sources/ticket-message-created/ticket-message-created.mjs index 8ad2709f27c5a..e8fb51f71b901 100644 --- a/components/gorgias_oauth/sources/ticket-message-created/ticket-message-created.mjs +++ b/components/gorgias_oauth/sources/ticket-message-created/ticket-message-created.mjs @@ -1,6 +1,6 @@ +import constants from "../../common/constants.mjs"; import base from "../common/base.mjs"; import eventTypes from "../common/event-types.mjs"; -import constants from "../../common/constants.mjs"; import sampleEmit from "./test-event.mjs"; export default { @@ -8,7 +8,7 @@ export default { key: "gorgias_oauth-ticket-message-created", name: "New Ticket Message", description: "Emit new event when a ticket message is created. [See the documentation](https://developers.gorgias.com/reference/the-event-object)", - version: "0.1.4", + version: "0.1.7", type: "source", props: { ...base.props, @@ -25,6 +25,7 @@ export default { base.props.gorgias_oauth, "ticketId", ], + optional: true, }, sourceType: { type: "string", diff --git a/components/gorgias_oauth/sources/ticket-updated/ticket-updated.mjs b/components/gorgias_oauth/sources/ticket-updated/ticket-updated.mjs index dc6fcc78fd7da..f5698e179b4f3 100644 --- a/components/gorgias_oauth/sources/ticket-updated/ticket-updated.mjs +++ b/components/gorgias_oauth/sources/ticket-updated/ticket-updated.mjs @@ -7,7 +7,7 @@ export default { key: "gorgias_oauth-ticket-updated", name: "New Updated Ticket", description: "Emit new event when a ticket is updated. [See the documentation](https://developers.gorgias.com/reference/the-event-object)", - version: "0.1.4", + version: "0.1.7", type: "source", props: { ...base.props, diff --git a/components/gorillastack/README.md b/components/gorillastack/README.md new file mode 100644 index 0000000000000..cb267e6ba329c --- /dev/null +++ b/components/gorillastack/README.md @@ -0,0 +1,11 @@ +# Overview + +The GorillaStack API enables automation and integration of cloud cost management and optimization tools. With GorillaStack, you can automate real-time actions, get insights into your cloud usage, and set up rules to control cloud costs. Using this API within Pipedream, you can create powerful serverless workflows that respond to various triggers and perform actions like shutting down unused resources, notifying teams about cost spikes, or adjusting resources based on load. + +# Example Use Cases + +- **Cost Alerting and Notification Workflow**: Automate notifications via Slack or email in Pipedream when GorillaStack detects cost anomalies or threshold breaches in your cloud environment. This helps keep teams informed and react quickly to unexpected cost changes. + +- **Resource Management Workflow**: Implement a system where GorillaStack flags idle resources, and Pipedream triggers Lambda functions to stop or terminate these resources after office hours or during low-usage periods. You can even incorporate approval steps with human intervention before taking action. + +- **Cost Optimization Reporting Workflow**: Generate and send regular cost optimization reports by aggregating data from GorillaStack. Use Pipedream to process this data and compile comprehensive reports, then distribute them through channels like Google Sheets or Data Studio for easy access and analysis. diff --git a/components/gosquared/README.md b/components/gosquared/README.md index dc8943a0b31d8..d5d95736f43fd 100644 --- a/components/gosquared/README.md +++ b/components/gosquared/README.md @@ -1,5 +1,11 @@ # Overview -The GoSquared API allows you to access all of your GoSquared data, including -analytics, campaigns, and more. With the API, you can build custom dashboards, -integrations, and applications on top of your GoSquared data. +GoSquared offers a powerful analytics platform which, when integrated via its API, enables a deep dive into website traffic, user behavior, and real-time analytics. The API can be used to track events, fetch analytics data, manage contacts, and trigger communications based on user actions. With Pipedream, you can harness this API to create custom workflows that react to data from GoSquared, automate tasks, and connect with countless other apps for a seamless data pipeline. + +# Example Use Cases + +- **Real-time User Activity Alerts**: Send custom notifications via Slack or email when a high-value user performs a specific action on your site. For example, receive an alert when a user who has previously made a purchase visits your pricing page. + +- **Dynamic Audience Segmentation**: Automatically segment users in your CRM like Salesforce or HubSpot based on their activity tracked by GoSquared. This could involve tagging users who spend over a certain amount of time on your site or complete a specific action, allowing for targeted follow-up campaigns. + +- **Automated Customer Support**: Trigger a support workflow in Zendesk or Intercom when GoSquared detects a user is struggling on a help page or has visited the support section multiple times without resolution, enabling proactive customer support engagement. diff --git a/components/gosquared/package.json b/components/gosquared/package.json new file mode 100644 index 0000000000000..b0ffe53ac330c --- /dev/null +++ b/components/gosquared/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/gosquared", + "version": "0.6.0", + "description": "Pipedream gosquared Components", + "main": "gosquared.app.mjs", + "keywords": [ + "pipedream", + "gosquared" + ], + "homepage": "https://pipedream.com/apps/gosquared", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/goto_meeting/goto_meeting.app.mjs b/components/goto_meeting/goto_meeting.app.mjs new file mode 100644 index 0000000000000..aae71d3197a52 --- /dev/null +++ b/components/goto_meeting/goto_meeting.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "goto_meeting", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/goto_meeting/package.json b/components/goto_meeting/package.json new file mode 100644 index 0000000000000..833cf5eb82af4 --- /dev/null +++ b/components/goto_meeting/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/goto_meeting", + "version": "0.0.1", + "description": "Pipedream GoTo Meeting Components", + "main": "goto_meeting.app.mjs", + "keywords": [ + "pipedream", + "goto_meeting" + ], + "homepage": "https://pipedream.com/apps/goto_meeting", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/gotowebinar/README.md b/components/gotowebinar/README.md new file mode 100644 index 0000000000000..e34ea67d0d511 --- /dev/null +++ b/components/gotowebinar/README.md @@ -0,0 +1,11 @@ +# Overview + +The GoTo Webinar API enables you to automate interactions with your webinars on GoTo Webinar. With this API, you can create, update, and retrieve your webinars, manage registrants, send reminders, and extract analytics data, all through programmatic means. Integrating it with Pipedream, you can build powerful workflows that trigger on specific events, take action using the API, and connect with other apps to streamline your webinar management process. + +# Example Use Cases + +- **Automate Webinar Creation and Setup**: Use the GoTo Webinar API on Pipedream to automatically schedule new webinars, set up registration fields, and customize follow-up emails. You can trigger this workflow by a new event in your calendar app or upon receiving an incoming webhook from your custom app. + +- **Sync Registrants to a CRM**: Automatically add new GoTo Webinar registrants to your CRM, such as Salesforce or HubSpot. When someone registers for a webinar, the workflow triggers and pushes their contact information to your CRM, ensuring your sales team has the latest leads at their fingertips. + +- **Post-Webinar Engagement**: After a webinar concludes, trigger a workflow that sends a follow-up email with a survey link, adds the participant to a re-engagement campaign, or enrolls them in further educational content. Integrate with Mailchimp or another email marketing service to personalize the follow-up experience. diff --git a/components/govee/README.md b/components/govee/README.md new file mode 100644 index 0000000000000..39153e040b989 --- /dev/null +++ b/components/govee/README.md @@ -0,0 +1,11 @@ +# Overview + +The Govee API enables you to interact with your Govee smart home devices. Through Pipedream, you can automate actions like adjusting lighting color and brightness, monitoring temperature and humidity levels, and creating alerts based on the state of your devices. With Pipedream's capability to join multiple APIs, these interactions can become intelligent responses to inputs from other sources, such as calendars, weather services, or IoT triggers. + +# Example Use Cases + +- **Smart Home Dashboard Integration**: Create a custom dashboard that displays the status of all Govee devices in your home. Use Pipedream to collect data from Govee API, such as the current color and brightness of lights, temperature, and humidity readings, then display this data in real-time on your dashboard app. + +- **Weather-Responsive Lighting**: Adjust your Govee lights based on local weather conditions. Set up a Pipedream workflow that uses the Weather API to fetch the current weather, then change the lighting in your home to match the mood. For example, cooler, blue lights when it's raining or warm, yellow lights when it's sunny. + +- **Calendar Event-Driven Scenes**: Sync your Govee lights with your Google Calendar events. A Pipedream workflow can monitor your calendar for specific event tags or keywords and then adjust your home lighting to a preset scene or color to remind you of upcoming appointments or to set the mood for an event. diff --git a/components/govee/package.json b/components/govee/package.json index 0589e3b430c6f..0687c9d88a17e 100644 --- a/components/govee/package.json +++ b/components/govee/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/gozen_growth/actions/create-or-update-contact/create-or-update-contact.mjs b/components/gozen_growth/actions/create-or-update-contact/create-or-update-contact.mjs new file mode 100644 index 0000000000000..adf52f053b573 --- /dev/null +++ b/components/gozen_growth/actions/create-or-update-contact/create-or-update-contact.mjs @@ -0,0 +1,43 @@ +import gozenGrowth from "../../gozen_growth.app.mjs"; + +export default { + key: "gozen_growth-create-or-update-contact", + name: "Create Or Update Contact", + description: "Create or update a contact a on Gozen Growth. [See the documentation](https://docs.gozen.io/docs/automation/how-to-use-webhook-trigger)", + version: "0.0.1", + type: "action", + props: { + gozenGrowth, + emailAddress: { + type: "string", + label: "Email Address", + description: "The email address of the contact. Must be a valid email address.", + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the contact. String without special characters.", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the contact. String without special characters.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.gozenGrowth.createOrUpdateContact({ + $, + data: { + contact: { + email_address: this.emailAddress, + first_name: this.firstName, + last_name: this.lastName, + }, + }, + }); + + $.export("$summary", `Successfully created or updated contact with email: ${this.emailAddress}`); + return response; + }, +}; diff --git a/components/gozen_growth/gozen_growth.app.mjs b/components/gozen_growth/gozen_growth.app.mjs new file mode 100644 index 0000000000000..4265b1cd91f1d --- /dev/null +++ b/components/gozen_growth/gozen_growth.app.mjs @@ -0,0 +1,31 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "gozen_growth", + methods: { + _baseUrl() { + return `${this.$auth.webhook_url}`; + }, + _headers() { + return { + "Authorization": `${this.$auth.authentication_token}`, + }; + }, + _makeRequest({ + $ = this, ...opts + }) { + return axios($, { + url: this._baseUrl(), + headers: this._headers(), + ...opts, + }); + }, + async createOrUpdateContact(opts = {}) { + return this._makeRequest({ + method: "POST", + ...opts, + }); + }, + }, +}; diff --git a/components/gozen_growth/package.json b/components/gozen_growth/package.json new file mode 100644 index 0000000000000..0004587aa051c --- /dev/null +++ b/components/gozen_growth/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/gozen_growth", + "version": "0.1.0", + "description": "Pipedream GoZen Growth Components", + "main": "gozen_growth.app.mjs", + "keywords": [ + "pipedream", + "gozen_growth" + ], + "homepage": "https://pipedream.com/apps/gozen_growth", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} + diff --git a/components/gpt_trainer/README.md b/components/gpt_trainer/README.md new file mode 100644 index 0000000000000..babdcc9620f10 --- /dev/null +++ b/components/gpt_trainer/README.md @@ -0,0 +1,11 @@ +# Overview + +The gpt-trainer API is a tool designed to train, run, and manage custom GPT-2 and GPT-3 models. It provides endpoints for submitting training data, starting the training process, and generating text from the trained model. With Pipedream's serverless integration platform, you can automate workflows that interact with the gpt-trainer API. You can trigger workflows using webhooks, schedule them, or even run them in response to events from other apps. Integrate the gpt-trainer API with other services on Pipedream to create powerful applications such as automated content creation, personalized messaging, or AI-driven data analysis. + +# Example Use Cases + +- **Automated Content Generation Workflow**: Trigger a Pipedream workflow with a new RSS feed item. Extract the content, send it to the gpt-trainer API to generate a summary, and post that summary to a Slack channel for team updates. + +- **Personalized Email Campaign Workflow**: Start a Pipedream workflow with a new subscriber event from Mailchimp. Use the gpt-trainer API to create a personalized welcome message based on the subscriber's interests, and then send the custom message using the SendGrid app. + +- **AI-Driven Social Media Management Workflow**: Use Pipedream to listen for mentions of your brand on Twitter with the Twitter app. Forward these mentions to the gpt-trainer API to generate a context-aware response, then post this response directly to your Twitter account. diff --git a/components/gptzero_detect_ai/actions/scan-file/scan-file.mjs b/components/gptzero_detect_ai/actions/scan-file/scan-file.mjs new file mode 100644 index 0000000000000..c4d5577b1799c --- /dev/null +++ b/components/gptzero_detect_ai/actions/scan-file/scan-file.mjs @@ -0,0 +1,50 @@ +import { ConfigurationError } from "@pipedream/platform"; +import FormData from "form-data"; +import fs from "fs"; +import { + checkTmp, parseObject, +} from "../../common/utils.mjs"; +import gptzeroDetectAi from "../../gptzero_detect_ai.app.mjs"; + +export default { + key: "gptzero_detect_ai-scan-file", + name: "Scan File for AI Detection", + description: "This endpoint takes in file(s) input and returns the model's result. [See the documentation](https://gptzero.stoplight.io/docs/gptzero-api/0a8e7efa751a6-ai-detection-on-an-array-of-files)", + version: "0.0.1", + type: "action", + props: { + gptzeroDetectAi, + alert: { + type: "alert", + alertType: "info", + content: `By default, the maximum number of files that can be submitted simultaneously is **50**. + \nThe maximum file size for all files combined is **15 MB**. + \nEach file's document will be truncated to **50,000** characters.`, + }, + files: { + type: "string[]", + label: "Files", + description: "A list of paths to files in the `/tmp` directory to analyze. Each file's document will be truncated to 50,000 characters. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp).", + }, + }, + async run({ $ }) { + if (this.files.length > 50) { + throw new ConfigurationError("The maximum number of files that can be submitted simultaneously is 50."); + } + + const data = new FormData(); + for (const filePath of parseObject(this.files)) { + const file = fs.createReadStream(checkTmp(filePath)); + data.append("files", file); + } + + const response = await this.gptzeroDetectAi.detectFiles({ + $, + data, + headers: data.getHeaders(), + }); + + $.export("$summary", `Successfully scanned ${this.files.length} file(s) for AI detection`); + return response; + }, +}; diff --git a/components/gptzero_detect_ai/actions/scan-text/scan-text.mjs b/components/gptzero_detect_ai/actions/scan-text/scan-text.mjs new file mode 100644 index 0000000000000..c3a25d03d8a48 --- /dev/null +++ b/components/gptzero_detect_ai/actions/scan-text/scan-text.mjs @@ -0,0 +1,35 @@ +import gptzeroDetectAi from "../../gptzero_detect_ai.app.mjs"; + +export default { + key: "gptzero_detect_ai-scan-text", + name: "Scan Text for AI Detection", + description: "This endpoint takes in a single text input and runs AI detection. The document will be truncated to 50,000 characters. [See the documentation](https://gptzero.stoplight.io/docs/gptzero-api/d2144a785776b-ai-detection-on-single-string)", + version: "0.0.1", + type: "action", + props: { + gptzeroDetectAi, + document: { + type: "string", + label: "Document", + description: "The text you want to analyze. The text will be truncated to 50,000 characters.", + }, + multilingual: { + type: "boolean", + label: "Multilingual", + description: "When this option is `true`, a special multilingual AI detection model will be used. Currently supported languages are French and Spanish.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.gptzeroDetectAi.detectText({ + $, + data: { + document: this.document, + multilingual: this.multilingual, + }, + }); + + $.export("$summary", "Successfully ran AI detection on the document."); + return response; + }, +}; diff --git a/components/gptzero_detect_ai/common/utils.mjs b/components/gptzero_detect_ai/common/utils.mjs new file mode 100644 index 0000000000000..0cd1a12b6a4ba --- /dev/null +++ b/components/gptzero_detect_ai/common/utils.mjs @@ -0,0 +1,31 @@ +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; + +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/gptzero_detect_ai/gptzero_detect_ai.app.mjs b/components/gptzero_detect_ai/gptzero_detect_ai.app.mjs new file mode 100644 index 0000000000000..709798dfad4aa --- /dev/null +++ b/components/gptzero_detect_ai/gptzero_detect_ai.app.mjs @@ -0,0 +1,42 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "gptzero_detect_ai", + methods: { + _baseUrl() { + return "https://api.gptzero.me/v2/predict"; + }, + _headers(headers = {}) { + return { + "Accept": "application/json", + "Content-Type": "application/json", + "x-api-key": `${this.$auth.api_key}`, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(headers), + ...opts, + }); + }, + detectFiles(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/files", + ...opts, + }); + }, + detectText(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/text", + ...opts, + }); + }, + }, +}; diff --git a/components/gptzero_detect_ai/package.json b/components/gptzero_detect_ai/package.json new file mode 100644 index 0000000000000..1d3776f2eacde --- /dev/null +++ b/components/gptzero_detect_ai/package.json @@ -0,0 +1,20 @@ +{ + "name": "@pipedream/gptzero_detect_ai", + "version": "0.1.0", + "description": "Pipedream GPTZero: Detect AI Components", + "main": "gptzero_detect_ai.app.mjs", + "keywords": [ + "pipedream", + "gptzero_detect_ai" + ], + "homepage": "https://pipedream.com/apps/gptzero_detect_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1", + "form-data": "^4.0.0" + } +} + diff --git a/components/grab_your_reviews/README.md b/components/grab_your_reviews/README.md new file mode 100644 index 0000000000000..908e181bb1521 --- /dev/null +++ b/components/grab_your_reviews/README.md @@ -0,0 +1,11 @@ +# Overview + +The Grab Your Reviews API opens a door to collect and manage customer feedback from multiple review platforms. It offers programmatic access to manipulate and retrieve reviews, respond to customer feedback, and analyze sentiment across different services. Within Pipedream, you can seamlessly integrate these capabilities into workflows, triggering actions based on new reviews, syncing data across databases or CRMs, and deriving insights by connecting to analytics tools. + +# Example Use Cases + +- **Sync Reviews to Google Sheets**: Automatically send new reviews collected from various platforms to a Google Sheets document. This workflow triggers every time Grab Your Reviews fetches a new review, creating a row in a designated spreadsheet with details like the reviewer’s name, rating, and comments, enabling easy monitoring and aggregation of customer feedback. + +- **Post Reviews to Slack for Team Visibility**: Keep your team instantly informed with timely notifications. This example workflow triggers with each new review captured by Grab Your Reviews. It formats the review contents and sends a message to a specified Slack channel, allowing teams to react quickly to customer feedback. + +- **Generate Monthly Review Reports**: Compile and send monthly reports summarizing reviews and ratings. Set up a workflow on Pipedream that, on a monthly schedule, gathers review data from Grab Your Reviews, analyzes sentiment and ratings, and emails a comprehensive report to stakeholders using a service like SendGrid to keep everyone updated on customer feedback trends. diff --git a/components/graceblocks/graceblocks.app.mjs b/components/graceblocks/graceblocks.app.mjs new file mode 100644 index 0000000000000..cd3ef079d4b83 --- /dev/null +++ b/components/graceblocks/graceblocks.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "graceblocks", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/graceblocks/package.json b/components/graceblocks/package.json new file mode 100644 index 0000000000000..b8e6510a13a98 --- /dev/null +++ b/components/graceblocks/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/graceblocks", + "version": "0.0.1", + "description": "Pipedream Graceblocks Components", + "main": "graceblocks.app.mjs", + "keywords": [ + "pipedream", + "graceblocks" + ], + "homepage": "https://pipedream.com/apps/graceblocks", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/grade_us/README.md b/components/grade_us/README.md new file mode 100644 index 0000000000000..db16b276e7ab9 --- /dev/null +++ b/components/grade_us/README.md @@ -0,0 +1,11 @@ +# Overview + +The Grade.us API enables you to automate the collection and management of customer reviews. With this API, you can streamline review requests, responses, reporting, and alerting of reviews across different platforms. Integrating the Grade.us API with Pipedream allows you to create powerful workflows to manage your online reputation efficiently. You can trigger actions based on review events, analyze sentiment, connect with CRM systems, or initiate marketing campaigns based on the reviews collected. + +# Example Use Cases + +- **Automate Review Request Follow-ups**: After closing a ticket in a customer service platform like Zendesk, use Pipedream to trigger a workflow that sends an automatic review request to the customer via the Grade.us API, helping you to increase the number of reviews. + +- **Review Alerts and Sentiment Analysis**: Analyze new reviews for negative sentiment using a sentiment analysis service like MonkeyLearn. If a review is negative, create a high-priority task in a task management app like Asana and alert the team via a Slack message. + +- **Generate Performance Reports**: Schedule a weekly workflow in Pipedream that fetches all reviews from the past week using the Grade.us API, aggregates review scores, and sends a performance report to your team's email using a service like SendGrid. diff --git a/components/grafana/grafana.app.mjs b/components/grafana/grafana.app.mjs new file mode 100644 index 0000000000000..2f53cbc58cecb --- /dev/null +++ b/components/grafana/grafana.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "grafana", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/grafana/package.json b/components/grafana/package.json new file mode 100644 index 0000000000000..dc30556fefa43 --- /dev/null +++ b/components/grafana/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/grafana", + "version": "0.0.1", + "description": "Pipedream Grafana Components", + "main": "grafana.app.mjs", + "keywords": [ + "pipedream", + "grafana" + ], + "homepage": "https://pipedream.com/apps/grafana", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/grafbase/README.md b/components/grafbase/README.md new file mode 100644 index 0000000000000..c1dee74fb663b --- /dev/null +++ b/components/grafbase/README.md @@ -0,0 +1,11 @@ +# Overview + +The Grafbase API allows you to interact with your Grafbase backend, enabling CRUD operations on your data models, managing authentication, and triggering custom business logic. Grafbase provides real-time updates and serverless deployment which makes it a perfect partner for Pipedream's serverless platform. You can build workflows to automate tasks, integrate with various services, and respond to events without managing infrastructure. + +# Example Use Cases + +- **Real-time Data Sync with Google Sheets**: Use Pipedream to watch for updates in your Grafbase models and reflect those changes into a Google Sheet. This can keep your reporting and analytics up-to-date with the latest data from your applications. + +- **Content Moderation Pipeline**: Build a workflow that triggers on new content submissions via Grafbase. Use Pipedream to send the content to a service like AWS Comprehend for sentiment analysis and flag content that doesn't meet community guidelines. + +- **Enhanced E-commerce Experience**: Connect Grafbase to a payment platform like Stripe through Pipedream. When a new order is placed, trigger a workflow that creates or updates customer data in Grafbase and handles payment processing with Stripe. diff --git a/components/grain/actions/get-recording/get-recording.mjs b/components/grain/actions/get-recording/get-recording.mjs new file mode 100644 index 0000000000000..7d3dc27775aff --- /dev/null +++ b/components/grain/actions/get-recording/get-recording.mjs @@ -0,0 +1,57 @@ +import { + INTELLIGENCE_NOTES_FORMAT_OPTIONS, + TRANSCRIPT_FORMAT_OPTIONS, +} from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import grain from "../../grain.app.mjs"; + +export default { + key: "grain-get-recording", + name: "Get Recording", + description: "Fetches a specific recording by its ID from Grain, optionally including the transcript and intelligence notes. [See the documentation](https://grainhq.notion.site/grain-public-api-877184aa82b54c77a875083c1b560de9)", + version: "0.0.1", + type: "action", + props: { + grain, + recordId: { + propDefinition: [ + grain, + "recordId", + ], + }, + transcriptFormat: { + type: "string", + label: "Transcript Format", + description: "Format for the transcript", + options: TRANSCRIPT_FORMAT_OPTIONS, + optional: true, + }, + intelligenceNotesFormat: { + type: "string", + label: "Intelligence Notes Format", + description: "Format for the intelligence notes", + options: INTELLIGENCE_NOTES_FORMAT_OPTIONS, + optional: true, + }, + allowedIntelligenceNotes: { + type: "string[]", + label: "Allowed Intelligence Notes", + description: "Whitelist of intelligence notes section titles", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.grain.fetchRecording({ + $, + recordId: this.recordId, + params: { + transcript_format: this.transcriptFormat, + intelligence_notes_format: this.intelligenceNotesFormat, + allowed_intelligence_notes: parseObject(this.allowedIntelligenceNotes), + }, + }); + + $.export("$summary", `Successfully fetched recording with ID ${this.recordId}`); + return response; + }, +}; diff --git a/components/grain/common/constants.mjs b/components/grain/common/constants.mjs new file mode 100644 index 0000000000000..d7deeadb1afb4 --- /dev/null +++ b/components/grain/common/constants.mjs @@ -0,0 +1,25 @@ +export const TRANSCRIPT_FORMAT_OPTIONS = [ + { + label: "JSON", + value: "json", + }, + { + label: "VTT", + value: "vtt", + }, +]; + +export const INTELLIGENCE_NOTES_FORMAT_OPTIONS = [ + { + label: "JSON", + value: "json", + }, + { + label: "Markdown", + value: "md", + }, + { + label: "Text", + value: "text", + }, +]; diff --git a/components/grain/common/utils.mjs b/components/grain/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/grain/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/grain/grain.app.mjs b/components/grain/grain.app.mjs new file mode 100644 index 0000000000000..3f2e01a992f9e --- /dev/null +++ b/components/grain/grain.app.mjs @@ -0,0 +1,113 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "grain", + propDefinitions: { + recordId: { + type: "string", + label: "Record ID", + description: "The ID of the recording to fetch", + async options({ prevContext: { nextPage } }) { + const { + recordings, cursor, + } = await this.listRecordings({ + params: { + cursor: nextPage, + }, + }); + return { + options: recordings.map(({ + id: value, title: label, + }) => ({ + value, + label, + })), + context: { + nextPage: cursor, + }, + }; + }, + }, + viewId: { + type: "string", + label: "View ID", + description: "The ID of the view to fetch", + async options({ + type, prevContext: { nextPage }, + }) { + const { + views, cursor, + } = await this.listViews({ + params: { + type_filter: type, + cursor: nextPage, + }, + }); + return { + options: views.map(({ + id: value, name: label, + }) => ({ + value, + label, + })), + context: { + nextPage: cursor, + }, + }; + }, + }, + }, + methods: { + _baseUrl() { + return "https://grain.com/_/public-api"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listRecordings(opts = {}) { + return this._makeRequest({ + path: "/recordings", + ...opts, + }); + }, + listViews(opts = {}) { + return this._makeRequest({ + path: "/views", + ...opts, + }); + }, + fetchRecording({ + recordId, ...opts + }) { + return this._makeRequest({ + path: `/recordings/${recordId}`, + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/hooks", + ...opts, + }); + }, + deleteWebhook(hookId) { + return this._makeRequest({ + method: "DELETE", + path: `/hooks/${hookId}`, + }); + }, + }, +}; diff --git a/components/grain/package.json b/components/grain/package.json new file mode 100644 index 0000000000000..830218d3126c8 --- /dev/null +++ b/components/grain/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/grain", + "version": "0.1.0", + "description": "Pipedream Grain Components", + "main": "grain.app.mjs", + "keywords": [ + "pipedream", + "grain" + ], + "homepage": "https://pipedream.com/apps/grain", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/grain/sources/common/base.mjs b/components/grain/sources/common/base.mjs new file mode 100644 index 0000000000000..ae3f5a16dc562 --- /dev/null +++ b/components/grain/sources/common/base.mjs @@ -0,0 +1,44 @@ +import grain from "../../grain.app.mjs"; + +export default { + props: { + grain, + http: "$.interface.http", + db: "$.service.db", + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + }, + hooks: { + async activate() { + const response = await this.grain.createWebhook({ + data: { + version: 2, + hook_url: this.http.endpoint, + view_id: this.viewId, + actions: this.getAction(), + }, + }); + this._setHookId(response.id); + }, + async deactivate() { + const webhookId = this._getHookId(); + await this.grain.deleteWebhook(webhookId); + }, + }, + async run({ body }) { + if (!body.data) return; + + const ts = Date.parse(new Date()); + this.$emit(body, { + id: `${body.data.id}-${ts}`, + summary: this.getSummary(body), + ts, + }); + }, +}; diff --git a/components/grain/sources/new-highlight-instant/new-highlight-instant.mjs b/components/grain/sources/new-highlight-instant/new-highlight-instant.mjs new file mode 100644 index 0000000000000..9f6eb8ca6d585 --- /dev/null +++ b/components/grain/sources/new-highlight-instant/new-highlight-instant.mjs @@ -0,0 +1,36 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "grain-new-highlight-instant", + name: "New Highlight (Instant)", + description: "Emit new event when a highlight that matches the filter is added.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + viewId: { + propDefinition: [ + common.props.grain, + "viewId", + () => ({ + type: "highlights", + }), + ], + }, + }, + methods: { + ...common.methods, + getAction() { + return [ + "added", + ]; + }, + getSummary({ data }) { + return `New highlight added: ${data.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/grain/sources/new-highlight-instant/test-event.mjs b/components/grain/sources/new-highlight-instant/test-event.mjs new file mode 100644 index 0000000000000..2fec46429c6b9 --- /dev/null +++ b/components/grain/sources/new-highlight-instant/test-event.mjs @@ -0,0 +1,17 @@ +export default { + "type": "highlight_added", + "user_id": "aea95745-99e9-4609-8623-c9efa2926b82", + "data": { + "id": "vjQRUKsWw0aFpCT3531eGbr8V0HJrMjKMEIcAUmP", + "recording_id": "b5185ccb-9a08-458c-9be1-db17a03fb14c", + "text": "testing 123 #test", + "transcript": "expected, that there was a mews in a lane which runs down by one wall of the garden. I lent the ostlers a hand in rubbing down their horses, and received in exchange twopence, a glass of half-and-half, two fills of shag tobacco, and as much information as I could desire about Miss Adler, to say nothing of half a dozen other people in", + "speakers": ["Andy Arbol"], + "timestamp": 3080, + "duration": 15000, + "created_datetime": "2021-07-29T23:16:34Z", + "url": "https://grain.com/highlight/vjQRUKsWw0aFpCT3531eGbr8V0HJrMjKMEIcAUmP", + "thumbnail_url": "https://media.grain.com/clips/v1/a14e5af9-d28e-43e9-902b-bc07419082eb/57zB8z52l7BKPoOvkS9KNyUi7LDSsNEh.jpeg", + "tags": ["test"] + } +} \ No newline at end of file diff --git a/components/grain/sources/new-recording-instant/new-recording-instant.mjs b/components/grain/sources/new-recording-instant/new-recording-instant.mjs new file mode 100644 index 0000000000000..9584835dceaeb --- /dev/null +++ b/components/grain/sources/new-recording-instant/new-recording-instant.mjs @@ -0,0 +1,36 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "grain-new-recording-instant", + name: "New Recording (Instant)", + description: "Emit new event when a recording that matches the filter is added.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + viewId: { + propDefinition: [ + common.props.grain, + "viewId", + () => ({ + type: "recordings", + }), + ], + }, + }, + methods: { + ...common.methods, + getAction() { + return [ + "added", + ]; + }, + getSummary({ data }) { + return `New recording added: ${data.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/grain/sources/new-recording-instant/test-event.mjs b/components/grain/sources/new-recording-instant/test-event.mjs new file mode 100644 index 0000000000000..bfac368e987bb --- /dev/null +++ b/components/grain/sources/new-recording-instant/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "type": "recording_added", + "user_id": "aea95745-99e9-4609-8623-c9efa2926b82", + "data": { + "id": "b5185ccb-9a08-458c-9be1-db17a03fb14c", + "title": "Sample Recording", + "url": "https://grain.com/recordings/b5185ccb-9a08-458c-9be1-db17a03fb14c/Kz5t1kAyPtt78hcxbSOJHJzFiPpZmUIeDVFXWzP0", + "start_datetime": "2021-07-29T23:13:17Z", + "end_datetime": "2021-07-29T23:16:18Z", + "public_thumbnail_url": null // Only non-null if recording share state is public + } +} \ No newline at end of file diff --git a/components/grain/sources/new-story-instant/new-story-instant.mjs b/components/grain/sources/new-story-instant/new-story-instant.mjs new file mode 100644 index 0000000000000..68fe04f1b1bba --- /dev/null +++ b/components/grain/sources/new-story-instant/new-story-instant.mjs @@ -0,0 +1,36 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "grain-new-story-instant", + name: "New Story (Instant)", + description: "Emit new event when a story that matches the filter is added.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + viewId: { + propDefinition: [ + common.props.grain, + "viewId", + () => ({ + type: "stories", + }), + ], + }, + }, + methods: { + ...common.methods, + getAction() { + return [ + "added", + ]; + }, + getSummary({ data }) { + return `New story added: ${data.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/grain/sources/new-story-instant/test-event.mjs b/components/grain/sources/new-story-instant/test-event.mjs new file mode 100644 index 0000000000000..e163749cbc950 --- /dev/null +++ b/components/grain/sources/new-story-instant/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "type": "story_added", + "user_id": "aea95745-99e9-4609-8623-c9efa2926b82", + "data": { + "id": "1aff0fe4-6575-4d5f-a462-aaf09f5f17a6", + "title": "My customer story", + "description": "A customer journey with ACME Corp", + "url": "https://grain.com/app/stories/89bd4a02-25f5-42c0-bd40-aa4c94be13ce", + "public_url": "https://grain.com/share/story/89bd4a02-25f5-42c0-bd40-aa4c94be13ce/2hAEpxLsIN8hDQ48aQ1Yi1MIirv1qCPSJNhxXEoj", + "banner_image_url": "https://media.grain.com/public/story_thumbnails/07.png", + "created_datetime": "2021-07-29T23:16:34Z", + "last_edited_datetime": "2021-08-29T23:16:34Z", + "tags": ["customer"] + } +} \ No newline at end of file diff --git a/components/grain/sources/removed-highlight-instant/removed-highlight-instant.mjs b/components/grain/sources/removed-highlight-instant/removed-highlight-instant.mjs new file mode 100644 index 0000000000000..e284d0f258548 --- /dev/null +++ b/components/grain/sources/removed-highlight-instant/removed-highlight-instant.mjs @@ -0,0 +1,36 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "grain-removed-highlight-instant", + name: "New Highlight Removed (Instant)", + description: "Emit new event when a highlight is removed.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + viewId: { + propDefinition: [ + common.props.grain, + "viewId", + () => ({ + type: "highlights", + }), + ], + }, + }, + methods: { + ...common.methods, + getAction() { + return [ + "removed", + ]; + }, + getSummary({ data }) { + return `Highlight removed from recording ${data.recording_id}`; + }, + }, + sampleEmit, +}; diff --git a/components/grain/sources/removed-highlight-instant/test-event.mjs b/components/grain/sources/removed-highlight-instant/test-event.mjs new file mode 100644 index 0000000000000..20fe6132f063a --- /dev/null +++ b/components/grain/sources/removed-highlight-instant/test-event.mjs @@ -0,0 +1,8 @@ +export default { + "type": "highlight_removed", + "user_id": "aea95745-99e9-4609-8623-c9efa2926b82", + "data": { + "id": "vjQRUKsWw0aFpCT3531eGbr8V0HJrMjKMEIcAUmP", + "recording_id": "b5185ccb-9a08-458c-9be1-db17a03fb14c" + } +} \ No newline at end of file diff --git a/components/grain/sources/removed-recording-instant/removed-recording-instant.mjs b/components/grain/sources/removed-recording-instant/removed-recording-instant.mjs new file mode 100644 index 0000000000000..f62a24fab8513 --- /dev/null +++ b/components/grain/sources/removed-recording-instant/removed-recording-instant.mjs @@ -0,0 +1,36 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "grain-removed-recording-instant", + name: "New Recording Removed (Instant)", + description: "Emit new event when a recording is removed.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + viewId: { + propDefinition: [ + common.props.grain, + "viewId", + () => ({ + type: "recordings", + }), + ], + }, + }, + methods: { + ...common.methods, + getAction() { + return [ + "removed", + ]; + }, + getSummary({ data }) { + return `Recording removed: ${data.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/grain/sources/removed-recording-instant/test-event.mjs b/components/grain/sources/removed-recording-instant/test-event.mjs new file mode 100644 index 0000000000000..e1f711fd981e2 --- /dev/null +++ b/components/grain/sources/removed-recording-instant/test-event.mjs @@ -0,0 +1,7 @@ +export default { + "type": "recording_removed", + "user_id": "aea95745-99e9-4609-8623-c9efa2926b82", + "data": { + "id": "b5185ccb-9a08-458c-9be1-db17a03fb14c" + } +} \ No newline at end of file diff --git a/components/grain/sources/removed-story-instant/removed-story-instant.mjs b/components/grain/sources/removed-story-instant/removed-story-instant.mjs new file mode 100644 index 0000000000000..15f37cbfaa605 --- /dev/null +++ b/components/grain/sources/removed-story-instant/removed-story-instant.mjs @@ -0,0 +1,36 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "grain-removed-story-instant", + name: "New Story Removed (Instant)", + description: "Emit new event when a story is removed.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + viewId: { + propDefinition: [ + common.props.grain, + "viewId", + () => ({ + type: "stories", + }), + ], + }, + }, + methods: { + ...common.methods, + getAction() { + return [ + "removed", + ]; + }, + getSummary({ data }) { + return `New story removed: ${data.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/grain/sources/removed-story-instant/test-event.mjs b/components/grain/sources/removed-story-instant/test-event.mjs new file mode 100644 index 0000000000000..79c9215c51ba7 --- /dev/null +++ b/components/grain/sources/removed-story-instant/test-event.mjs @@ -0,0 +1,7 @@ +export default { + "type": "story_removed", + "user_id": "aea95745-99e9-4609-8623-c9efa2926b82", + "data": { + "id": "1aff0fe4-6575-4d5f-a462-aaf09f5f17a6" + } +} \ No newline at end of file diff --git a/components/grain/sources/updated-highlight-instant/test-event.mjs b/components/grain/sources/updated-highlight-instant/test-event.mjs new file mode 100644 index 0000000000000..b892cad27d725 --- /dev/null +++ b/components/grain/sources/updated-highlight-instant/test-event.mjs @@ -0,0 +1,17 @@ +export default { + "type": "highlight_updated", + "user_id": "aea95745-99e9-4609-8623-c9efa2926b82", + "data": { + "id": "vjQRUKsWw0aFpCT3531eGbr8V0HJrMjKMEIcAUmP", + "recording_id": "b5185ccb-9a08-458c-9be1-db17a03fb14c", + "text": "testing 123 #test", + "transcript": "expected, that there was a mews in a lane which runs down by one wall of the garden. I lent the ostlers a hand in rubbing down their horses, and received in exchange twopence, a glass of half-and-half, two fills of shag tobacco, and as much information as I could desire about Miss Adler, to say nothing of half a dozen other people in", + "speakers": ["Andy Arbol"], + "timestamp": 3080, + "duration": 15000, + "created_datetime": "2021-07-29T23:16:34Z", + "url": "https://grain.com/highlight/vjQRUKsWw0aFpCT3531eGbr8V0HJrMjKMEIcAUmP", + "thumbnail_url": "https://media.grain.com/clips/v1/a14e5af9-d28e-43e9-902b-bc07419082eb/57zB8z52l7BKPoOvkS9KNyUi7LDSsNEh.jpeg", + "tags": ["test"] + } +} \ No newline at end of file diff --git a/components/grain/sources/updated-highlight-instant/updated-highlight-instant.mjs b/components/grain/sources/updated-highlight-instant/updated-highlight-instant.mjs new file mode 100644 index 0000000000000..f5147e298b111 --- /dev/null +++ b/components/grain/sources/updated-highlight-instant/updated-highlight-instant.mjs @@ -0,0 +1,36 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "grain-updated-highlight-instant", + name: "New Highlight Updated (Instant)", + description: "Emit new event when a highlight is updated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + viewId: { + propDefinition: [ + common.props.grain, + "viewId", + () => ({ + type: "highlights", + }), + ], + }, + }, + methods: { + ...common.methods, + getAction() { + return [ + "updated", + ]; + }, + getSummary({ data }) { + return `New highlight updated: ${data.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/grain/sources/updated-recording-instant/test-event.mjs b/components/grain/sources/updated-recording-instant/test-event.mjs new file mode 100644 index 0000000000000..5a440d2062bd2 --- /dev/null +++ b/components/grain/sources/updated-recording-instant/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "type": "recording_updated", + "user_id": "aea95745-99e9-4609-8623-c9efa2926b82", + "data": { + "id": "b5185ccb-9a08-458c-9be1-db17a03fb14c", + "title": "Sample Recording", + "url": "https://grain.com/recordings/b5185ccb-9a08-458c-9be1-db17a03fb14c/Kz5t1kAyPtt78hcxbSOJHJzFiPpZmUIeDVFXWzP0", + "start_datetime": "2021-07-29T23:13:17Z", + "end_datetime": "2021-07-29T23:16:18Z", + "public_thumbnail_url": null // Only non-null if recording share state is public + } +} \ No newline at end of file diff --git a/components/grain/sources/updated-recording-instant/updated-recording-instant.mjs b/components/grain/sources/updated-recording-instant/updated-recording-instant.mjs new file mode 100644 index 0000000000000..5615b8e41430b --- /dev/null +++ b/components/grain/sources/updated-recording-instant/updated-recording-instant.mjs @@ -0,0 +1,36 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "grain-updated-recording-instant", + name: "New Recording Updated (Instant)", + description: "Emit new event when a recording is updated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + viewId: { + propDefinition: [ + common.props.grain, + "viewId", + () => ({ + type: "recordings", + }), + ], + }, + }, + methods: { + ...common.methods, + getAction() { + return [ + "updated", + ]; + }, + getSummary({ data }) { + return `New recording updated: ${data.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/grain/sources/updated-story-instant/test-event.mjs b/components/grain/sources/updated-story-instant/test-event.mjs new file mode 100644 index 0000000000000..00ce668c0c07c --- /dev/null +++ b/components/grain/sources/updated-story-instant/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "type": "story_updated", + "user_id": "aea95745-99e9-4609-8623-c9efa2926b82", + "data": { + "id": "1aff0fe4-6575-4d5f-a462-aaf09f5f17a6", + "title": "My customer story", + "description": "A customer journey with ACME Corp", + "url": "https://grain.com/app/stories/89bd4a02-25f5-42c0-bd40-aa4c94be13ce", + "public_url": "https://grain.com/share/story/89bd4a02-25f5-42c0-bd40-aa4c94be13ce/2hAEpxLsIN8hDQ48aQ1Yi1MIirv1qCPSJNhxXEoj", + "banner_image_url": "https://media.grain.com/public/story_thumbnails/07.png", + "created_datetime": "2021-07-29T23:16:34Z", + "last_edited_datetime": "2021-08-29T23:16:34Z", + "tags": ["customer"] + } +} \ No newline at end of file diff --git a/components/grain/sources/updated-story-instant/updated-story-instant.mjs b/components/grain/sources/updated-story-instant/updated-story-instant.mjs new file mode 100644 index 0000000000000..20c794c9eeca0 --- /dev/null +++ b/components/grain/sources/updated-story-instant/updated-story-instant.mjs @@ -0,0 +1,36 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "grain-updated-story-instant", + name: "New Story Updated (Instant)", + description: "Emit new event when a story is updated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + viewId: { + propDefinition: [ + common.props.grain, + "viewId", + () => ({ + type: "stories", + }), + ], + }, + }, + methods: { + ...common.methods, + getAction() { + return [ + "updated", + ]; + }, + getSummary({ data }) { + return `New story updated: ${data.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/graphhopper/README.md b/components/graphhopper/README.md index f01c8f2acf4bb..680733d1045f4 100644 --- a/components/graphhopper/README.md +++ b/components/graphhopper/README.md @@ -1,11 +1,11 @@ # Overview -With the GraphHopper API, you can build a variety of applications that make use -of routing, geocoding, and other map-related features. Some examples of what -you can build include: - -- A routing application that provides turn-by-turn directions for users -- A geocoding application that helps users find addresses or points of interest -- A mapping application that shows users an interactive map of an area -- An integration with a ride-sharing service that provides directions and - pickup locations +GraphHopper is a powerful routing engine that leverages OpenStreetMap data to provide various services like route optimization, map matching, and travel time calculation. With the GraphHopper API, you can embed routing capabilities into your apps, automate the creation of efficient travel routes, and analyze spatial data to derive insights on movement patterns. On Pipedream, you can create workflows that harness GraphHopper's features to automate logistics, streamline dispatch systems, and perform geospatial analysis in conjunction with other apps and services. + +# Example Use Cases + +- **Dynamic Route Optimization for Deliveries**: Automatically optimize delivery routes by integrating the GraphHopper API with e-commerce platforms such as Shopify or WooCommerce. When a new order is placed, Pipedream triggers a workflow that calculates the most efficient route for delivery based on the customer's address, traffic conditions, and other constraints. This maximizes delivery efficiency and reduces shipping costs. + +- **Real-Time Fleet Tracking and Management**: Enhance fleet management by connecting GraphHopper with GPS tracking services like Google Maps. As vehicles report their locations, Pipedream processes this data and uses the GraphHopper API to update routes in real-time, accounting for delays or changes in conditions. This can improve fleet performance and ensure timely deliveries. + +- **Geospatial Data Analysis for Urban Planning**: Use GraphHopper with data visualization tools such as Google Data Studio to analyze movement patterns within a city. By feeding traffic data and route histories into Pipedream, you can leverage GraphHopper's map matching and routing services to identify congestion hotspots and inform urban infrastructure planning decisions. diff --git a/components/graphy/README.md b/components/graphy/README.md new file mode 100644 index 0000000000000..18325507ba039 --- /dev/null +++ b/components/graphy/README.md @@ -0,0 +1,11 @@ +# Overview + +Graphy is an API designed to help businesses create customizable and collaborative dashboards and reports. Integrating Graphy with Pipedream allows for seamless automation of data visualization workflows, where you can trigger actions based on events, sync data across various apps, or even manage your dashboard's content programmatically. Pipedream's serverless platform provides you with the ability to connect Graphy to hundreds of other apps to enhance your reporting and analytics processes without writing extensive code. + +# Example Use Cases + +- **Automated Dashboard Updates**: Set up a workflow that listens for updates in a Google Sheets document. Whenever a change is detected, the workflow can trigger an update to a specific Graphy dashboard, ensuring your visualizations always reflect the latest data. + +- **Slack Notification for Metrics**: Create a workflow that regularly checks certain metrics in Graphy. If a metric crosses a predefined threshold, send an alert to a Slack channel to prompt immediate action or discussion among your team members. + +- **Event-Driven Report Generation**: Implement a workflow where, upon receiving a webhook from a CRM like HubSpot after a new deal is closed, a detailed performance report is automatically generated in Graphy and shared via email with stakeholders. diff --git a/components/graphy/package.json b/components/graphy/package.json index a18a598886db9..b8a6ef6e43cf2 100644 --- a/components/graphy/package.json +++ b/components/graphy/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/gravity_forms/README.md b/components/gravity_forms/README.md new file mode 100644 index 0000000000000..40b7cd2b781d8 --- /dev/null +++ b/components/gravity_forms/README.md @@ -0,0 +1,11 @@ +# Overview + +The Gravity Forms API allows you to interact programmatically with Gravity Forms, a WordPress form plugin, to create, update, delete, and retrieve form entries. Within Pipedream, you can leverage this API to automate workflows, integrate with other apps, and manipulate form data in real-time. By connecting Gravity Forms to Pipedream's serverless platform, you open up possibilities to sync form submissions with databases, trigger notifications, or even process payments automatically. + +# Example Use Cases + +- **Sync Form Submissions with Google Sheets**: When a new form submission is received, use the Gravity Forms API to capture the data and append it to a Google Sheet. This can be useful for tracking submissions or analyzing data without manual entry. + +- **Send Custom Email Notifications**: After a form submission, employ the Gravity Forms API to trigger a custom email notification. You can include details from the submission in the email, and conditionally send different emails based on form data. This enhances communication and allows for immediate follow-up actions. + +- **Integrate with a CRM**: Automatically create or update contacts in a CRM like Salesforce or HubSpot when a new form entry is submitted. This workflow can help maintain an up-to-date customer database, improve lead tracking, and streamline sales processes. diff --git a/components/greenhouse/README.md b/components/greenhouse/README.md index ed7752e86cb26..f2ea8e3b4a557 100644 --- a/components/greenhouse/README.md +++ b/components/greenhouse/README.md @@ -1,22 +1,11 @@ # Overview -Greenhouse is a recruitment software platform that helps companies source, -hire, and onboard employees. With the Greenhouse API, you can access data -about: +The Greenhouse API offers a powerful suite of tools for automating and enhancing the recruitment process. It allows you to programmatically access candidate information, job listings, scorecards, and scheduling details, which opens a myriad of possibilities for streamlining recruiting workflows. By leveraging the Greenhouse API on Pipedream, you can automate repetitive tasks, integrate with other HR systems, analyze recruitment data, and build custom event-driven workflows to improve the efficiency and effectiveness of your hiring process. -- Job postings -- Applicants -- Interviews -- Offers -- Hires +# Example Use Cases -This data can be used to create custom applications or integrations that help -streamline your recruiting process. Some examples of what you can build with -the Greenhouse API include: +- **Automated Candidate Screening**: Trigger a workflow in Pipedream when new candidates apply, automatically screening their resumes with AI-based tools, and then creating tasks in project management apps like Asana for recruiters to follow up with top candidates. -- A tool that allows candidates to apply for multiple positions with a single - click -- A dashboard that displays real-time data about your hiring pipeline -- An integration with your HRIS system that automatically creates profiles for - new hires -- A chatbot that answers questions about open positions or the hiring process +- **Interview Scheduling Coordination**: Connect Greenhouse with calendar apps like Google Calendar using Pipedream. When an interview is scheduled in Greenhouse, automatically find and book an available time slot, send calendar invites to all participants, and post a message with details in a Slack channel dedicated to the hiring team. + +- **Recruitment Analytics Reporting**: Compile recruitment metrics by setting up a Pipedream workflow that periodically fetches data from Greenhouse, processes the analytics (like time-to-hire or diversity stats), and then generates a report in Google Sheets or visualizes the data on a BI tool like Tableau for the HR team's review. diff --git a/components/greenhouse/actions/add-attachment-to-candidate/add-attachment-to-candidate.mjs b/components/greenhouse/actions/add-attachment-to-candidate/add-attachment-to-candidate.mjs new file mode 100644 index 0000000000000..678ebd3c63ff8 --- /dev/null +++ b/components/greenhouse/actions/add-attachment-to-candidate/add-attachment-to-candidate.mjs @@ -0,0 +1,95 @@ +import { ConfigurationError } from "@pipedream/platform"; +import fs from "fs"; +import { CONTENT_TYPE_OPTIONS } from "../../common/constants.mjs"; +import { checkTmp } from "../../common/utils.mjs"; +import greenhouse from "../../greenhouse.app.mjs"; + +export default { + key: "greenhouse-add-attachment-to-candidate", + name: "Add Attachment to Candidate", + description: "Adds an attachment to a specific candidate or prospect. [See the documentation](https://developers.greenhouse.io/harvest.html#post-add-attachment)", + version: "0.0.1", + type: "action", + props: { + greenhouse, + userId: { + propDefinition: [ + greenhouse, + "userId", + ], + }, + candidateId: { + propDefinition: [ + greenhouse, + "candidateId", + ], + }, + filename: { + type: "string", + label: "Filename", + description: "Name of the file.", + }, + type: { + type: "string", + label: "Type", + description: "The type of the file.", + options: [ + "resume", + "cover_letter", + "admin_only", + ], + }, + file: { + type: "string", + label: "File", + description: "The path to the image file saved to the `/tmp` directory (e.g. `/tmp/example.jpg`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory). (if you are providing content, you do not need to provide url).", + optional: true, + }, + url: { + type: "string", + label: "URL", + description: "Url of the attachment (if you are providing the url, you do not need to provide the content.) *Please note, shareable links from cloud services such as Google Drive will result in a corrupted file. Please use machine accessbile URLs*.", + optional: true, + }, + contentType: { + type: "string", + label: "Content Type", + description: "The content-type of the document you are sending. When using a URL, this generally isn't needed, as the responding server will deliver a content type. This should be included for encoded content.", + optional: true, + options: CONTENT_TYPE_OPTIONS, + }, + }, + async run({ $ }) { + if ((this.file && this.url) || (!this.file && !this.url)) { + throw new ConfigurationError("You must provide either File or URL"); + } + + let encodedFile; + + if (this.file) { + if (!this.contentType) { + throw new ConfigurationError("You must provide the Content-Type"); + } + const file = fs.readFileSync(checkTmp(this.file)); + encodedFile = Buffer(file).toString("base64"); + } + + const response = await this.greenhouse.addAttachmentToCandidate({ + $, + headers: { + "On-Behalf-Of": this.userId, + }, + candidateId: this.candidateId, + data: { + filename: this.filename, + type: this.type, + content: encodedFile, + url: this.url, + content_type: this.contentType, + }, + }); + + $.export("$summary", `Successfully added attachment to candidate ${this.candidateId}`); + return response; + }, +}; diff --git a/components/greenhouse/actions/common/base-create.mjs b/components/greenhouse/actions/common/base-create.mjs new file mode 100644 index 0000000000000..53cda4d7090ca --- /dev/null +++ b/components/greenhouse/actions/common/base-create.mjs @@ -0,0 +1,181 @@ +import { + checkPhoneNumbers, + parseObject, +} from "../../common/utils.mjs"; +import greenhouse from "../../greenhouse.app.mjs"; + +export default { + props: { + greenhouse, + userId: { + propDefinition: [ + greenhouse, + "userId", + ], + }, + firstName: { + propDefinition: [ + greenhouse, + "firstName", + ], + }, + lastName: { + propDefinition: [ + greenhouse, + "lastName", + ], + }, + company: { + propDefinition: [ + greenhouse, + "company", + ], + optional: true, + }, + title: { + propDefinition: [ + greenhouse, + "title", + ], + optional: true, + }, + phoneNumbers: { + propDefinition: [ + greenhouse, + "phoneNumbers", + ], + optional: true, + }, + addressses: { + propDefinition: [ + greenhouse, + "addressses", + ], + optional: true, + }, + emailAddresses: { + propDefinition: [ + greenhouse, + "emailAddresses", + ], + optional: true, + }, + websiteAddresses: { + propDefinition: [ + greenhouse, + "websiteAddresses", + ], + optional: true, + }, + socialMediaAddresses: { + propDefinition: [ + greenhouse, + "socialMediaAddresses", + ], + optional: true, + }, + tags: { + propDefinition: [ + greenhouse, + "tags", + ], + optional: true, + }, + customFields: { + propDefinition: [ + greenhouse, + "customFields", + ], + optional: true, + }, + recruiterId: { + propDefinition: [ + greenhouse, + "userId", + ], + label: "Recruiter Id", + description: "The ID of the recruiter - either id or email must be present.", + optional: true, + }, + recruiterEmail: { + propDefinition: [ + greenhouse, + "recruiterEmail", + ], + optional: true, + }, + coordinatorId: { + propDefinition: [ + greenhouse, + "userId", + ], + label: "Coordinator Id", + description: "The ID of the coordinator - either id or email must be present.", + optional: true, + }, + coordinatorEmail: { + propDefinition: [ + greenhouse, + "coordinatorEmail", + ], + optional: true, + }, + }, + async run({ $ }) { + const addData = this.getData(); + if (this.recruiterEmail || this.recruiterId) { + addData.recruiter = { + email: this.recruiterEmail, + id: this.recruiterId, + }; + } + if (this.coordinatorEmail || this.coordinatorId) { + addData.coordinator = { + email: this.coordinatorEmail, + id: this.coordinatorId, + }; + } + + const fn = this.getFunc(); + const response = await fn({ + $, + headers: { + "On-Behalf-Of": this.userId, + }, + data: { + first_name: this.firstName, + last_name: this.lastName, + company: this.company, + title: this.title, + phone_numbers: parseObject(checkPhoneNumbers(this.phoneNumbers))?.map((item) => ({ + value: item, + type: "other", + })), + addressses: parseObject(this.addressses)?.map((item) => ({ + value: item, + type: "other", + })), + email_addresses: parseObject(this.emailAddresses)?.map((item) => ({ + value: item, + type: "other", + })), + website_addresses: parseObject(this.websiteAddresses)?.map((item) => ({ + value: item, + type: "other", + })), + social_media_addresses: parseObject(this.socialMediaAddresses)?.map((item) => ({ + value: item, + })), + tags: parseObject(this.tags), + custom_fields: this.customFields && Object.keys(this.customFields).map((key) => ({ + id: key, + value: this.customFields[key], + })), + ...addData, + }, + }); + + $.export("$summary", this.getSummary(response)); + return response; + }, +}; diff --git a/components/greenhouse/actions/create-candidate/create-candidate.mjs b/components/greenhouse/actions/create-candidate/create-candidate.mjs new file mode 100644 index 0000000000000..14be34f1e06bc --- /dev/null +++ b/components/greenhouse/actions/create-candidate/create-candidate.mjs @@ -0,0 +1,62 @@ +import { parseObject } from "../../common/utils.mjs"; +import common from "../common/base-create.mjs"; + +export default { + ...common, + key: "greenhouse-create-candidate", + name: "Create Candidate", + description: "Creates a new candidate entry in Greenhouse. [See the documentation](https://developers.greenhouse.io/harvest.html#post-add-candidate)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + educations: { + propDefinition: [ + common.props.greenhouse, + "educations", + ], + optional: true, + }, + employments: { + type: "string[]", + label: "Employments", + description: "A list of employment record objects. **Format: {\"company_name\": \"Greenhouse\",\"title\": \"Engineer\",\"start_date\": \"2001-09-15T00:00:00.000Z\",\"end_date\": \"2004-05-15T00:00:00.000Z\"}**", + optional: true, + }, + activityFeedNotes: { + type: "string[]", + label: "Activity Feed Notes", + description: "A list of activity feed objects. **Format: {\"body\": \"John Locke was moved into Recruiter Phone Screen for Accounting Manager on 03/27/2014 by Boone Carlyle\",\"visibility\": \"admin_only\"}. Visibility can be one of: admin_only, private or public**", + optional: true, + }, + jobIds: { + propDefinition: [ + common.props.greenhouse, + "jobIds", + ], + }, + }, + methods: { + getData() { + return { + educations: parseObject(this.educations)?.map((item) => ({ + degree_id: item, + })), + employments: parseObject(this.employments), + activity_feed_notes: parseObject(this.activityFeedNotes)?.map((item) => ({ + ...item, + user_id: this.userId, + })), + applications: parseObject(this.jobIds)?.map((item) => ({ + job_id: item, + })), + }; + }, + getFunc() { + return this.greenhouse.createCandidate; + }, + getSummary(response) { + return `Successfully created candidate with Id: ${response.id}!`; + }, + }, +}; diff --git a/components/greenhouse/actions/create-prospect/create-prospect.mjs b/components/greenhouse/actions/create-prospect/create-prospect.mjs new file mode 100644 index 0000000000000..fed361fa02d79 --- /dev/null +++ b/components/greenhouse/actions/create-prospect/create-prospect.mjs @@ -0,0 +1,36 @@ +import { parseObject } from "../../common/utils.mjs"; +import common from "../common/base-create.mjs"; + +export default { + ...common, + key: "greenhouse-create-prospect", + name: "Create Prospect", + description: "Creates a new prospect entry in Greenhouse. [See the documentation](https://developers.greenhouse.io/harvest.html#post-add-prospect)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + jobIds: { + propDefinition: [ + common.props.greenhouse, + "jobIds", + ], + optional: true, + }, + }, + methods: { + getData() { + return { + applications: parseObject(this.jobIds)?.map((item) => ({ + job_id: item, + })), + }; + }, + getFunc() { + return this.greenhouse.createProspect; + }, + getSummary(response) { + return `Successfully created prospect with Id: ${response.id}!`; + }, + }, +}; diff --git a/components/greenhouse/common/constants.mjs b/components/greenhouse/common/constants.mjs new file mode 100644 index 0000000000000..ced7587995d9c --- /dev/null +++ b/components/greenhouse/common/constants.mjs @@ -0,0 +1,31 @@ +export const CONTENT_TYPE_OPTIONS = [ + "application/atom+xml", + "application/javascript", + "application/json", + "application/msgpack", + "application/msword", + "application/pdf", + "application/rss+xml", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-powerpoint", + "application/xml", + "application/x-www-form-urlencoded", + "application/x-yaml", + "application/zip", + "multipart/form-data", + "image/bmp", + "image/gif", + "image/jpeg", + "image/png", + "image/tiff", + "text/calendar", + "text/css", + "text/csv", + "text/html", + "text/javascript", + "text/plain", + "text/vcard", + "video/mpeg", +]; diff --git a/components/greenhouse/common/utils.mjs b/components/greenhouse/common/utils.mjs new file mode 100644 index 0000000000000..f77b6ac759c25 --- /dev/null +++ b/components/greenhouse/common/utils.mjs @@ -0,0 +1,40 @@ +import { ConfigurationError } from "@pipedream/platform"; + +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; + +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; + +export const checkPhoneNumbers = (phoneNumbers) => { + phoneNumbers.map((phoneNumber) => { + if (!phoneNumber.startsWith("+")) + throw new ConfigurationError("The phone numbers must start with '+'"); + }); +}; diff --git a/components/greenhouse/greenhouse.app.mjs b/components/greenhouse/greenhouse.app.mjs index fdee0035dc178..865a5c36f5a70 100644 --- a/components/greenhouse/greenhouse.app.mjs +++ b/components/greenhouse/greenhouse.app.mjs @@ -1,11 +1,255 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "greenhouse", - propDefinitions: {}, + propDefinitions: { + userId: { + type: "string", + label: "User Id", + description: "The identification of the user who is registering.", + async options({ page }) { + const data = await this.listUsers({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + firstName: { + type: "string", + label: "First Name", + description: "The person's first name.", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The person's last name.", + }, + company: { + type: "string", + label: "Company", + description: "The person's company.", + }, + title: { + type: "string", + label: "Title", + description: "The person's title.", + }, + phoneNumbers: { + type: "string[]", + label: "Phone Numbers", + description: "A list of phone numbers. The phone number includes a plus sign (+), then country code, city code, and local phone number.", + }, + addressses: { + type: "string[]", + label: "Addressses", + description: "A list of addresses.", + }, + emailAddresses: { + type: "string[]", + label: "Email Addresses", + description: "A list of email addresses.", + }, + websiteAddresses: { + type: "string[]", + label: "Website Addresses", + description: "A list of website addresses .", + }, + socialMediaAddresses: { + type: "string[]", + label: "Social Media Addresses", + description: "A list of social media addresses.", + }, + tags: { + type: "string[]", + label: "Tags", + description: "A list of tags as strings.", + }, + customFields: { + type: "object", + label: "Custom Fields", + description: "An object containing new custom field values. The fields are the custom field Id.", + }, + recruiterEmail: { + type: "string", + label: "Recruiter Email", + description: "The email of the recruiter - either id or email must be present.", + }, + coordinatorEmail: { + type: "string", + label: "Coordinator Email", + description: "The email of the coordinator - either id or email must be present.", + }, + jobIds: { + type: "string[]", + label: "Job Ids", + description: "An array of job ids to which the person will be assigned.", + async options({ page }) { + const data = await this.listJobs({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + educations: { + type: "string[]", + label: "Educations", + description: "A list of education records.", + async options() { + const data = await this.listDegrees(); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + candidateId: { + type: "string", + label: "Candidate ID", + description: "The ID of the candidate whose application or status changes.", + async options({ page }) { + const data = await this.listCandidates({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + id: value, first_name: firstName, last_name: lastName, + }) => ({ + label: `${firstName} ${lastName}`, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://harvest.greenhouse.io/v1"; + }, + _auth() { + return { + "username": `${this.$auth.api_key}`, + "password": "", + }; + }, + _makeRequest({ + $ = this, path, headers = {}, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + auth: this._auth(), + headers, + ...opts, + }); + }, + createCandidate(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/candidates", + ...opts, + }); + }, + createProspect(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/prospects", + ...opts, + }); + }, + listApplications(opts = {}) { + return this._makeRequest({ + path: "/applications", + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users", + ...opts, + }); + }, + listInterviews(opts = {}) { + return this._makeRequest({ + path: "/scheduled_interviews", + ...opts, + }); + }, + listJobs(opts = {}) { + return this._makeRequest({ + path: "/jobs", + ...opts, + }); + }, + listDegrees(opts = {}) { + return this._makeRequest({ + path: "/degrees", + ...opts, + }); + }, + listCandidates(opts = {}) { + return this._makeRequest({ + path: "/candidates", + ...opts, + }); + }, + getActivity(candidateId) { + return this._makeRequest({ + path: `/candidates/${candidateId}/activity_feed`, + }); + }, + addAttachmentToCandidate({ + candidateId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/candidates/${candidateId}/attachments`, + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const data = await fn({ + params, + ...opts, + }); + for (const d of data) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = data.length; + + } while (hasMore); }, }, }; diff --git a/components/greenhouse/package.json b/components/greenhouse/package.json new file mode 100644 index 0000000000000..6ed0ecb62b214 --- /dev/null +++ b/components/greenhouse/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/greenhouse", + "version": "0.1.0", + "description": "Pipedream Greenhouse Components", + "main": "greenhouse.app.mjs", + "keywords": [ + "pipedream", + "greenhouse" + ], + "homepage": "https://pipedream.com/apps/greenhouse", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} diff --git a/components/greenhouse/sources/common/base.mjs b/components/greenhouse/sources/common/base.mjs new file mode 100644 index 0000000000000..3d7b634667752 --- /dev/null +++ b/components/greenhouse/sources/common/base.mjs @@ -0,0 +1,58 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import greenhouse from "../../greenhouse.app.mjs"; + +export default { + props: { + greenhouse, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(created) { + this.db.set("lastDate", created); + }, + generateMeta(item) { + return { + id: item.id, + summary: this.getSummary(item), + ts: item.createdAt, + }; + }, + async startEvent(maxResults = 0) { + const lastDate = this._getLastDate(); + const response = this.greenhouse.paginate({ + fn: this.getFn(), + dataField: this.getDataField(), + maxResults, + }); + + const responseArray = []; + for await (const item of response) { + if (item.createdAt <= lastDate) break; + responseArray.push(item); + } + + if (responseArray.length) this._setLastDate(responseArray[0].createdAt); + + for (const item of responseArray.reverse()) { + this.$emit(item, this.generateMeta(item)); + } + }, + }, + hooks: { + async deploy() { + await this.startEvent(25); + }, + }, + async run() { + await this.startEvent(); + }, +}; diff --git a/components/greenhouse/sources/new-candidate-application/new-candidate-application.mjs b/components/greenhouse/sources/new-candidate-application/new-candidate-application.mjs new file mode 100644 index 0000000000000..fd4de73603554 --- /dev/null +++ b/components/greenhouse/sources/new-candidate-application/new-candidate-application.mjs @@ -0,0 +1,73 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import greenhouse from "../../greenhouse.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "greenhouse-new-candidate-application", + name: "New Candidate Application", + description: "Emit new event when a candidate submits a new application.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + greenhouse, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01"; + }, + _setLastDate(created) { + this.db.set("lastDate", created); + }, + generateMeta(item) { + return { + id: item.id, + summary: `New candidate application with Id: ${item.id}`, + ts: new Date(), + }; + }, + async startEvent(maxResults) { + const lastDate = this._getLastDate(); + const response = this.greenhouse.paginate({ + fn: this.greenhouse.listApplications, + params: { + created_after: lastDate, + }, + }); + + let responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + responseArray = responseArray.reverse(); + + if (responseArray.length) { + if (responseArray.length > maxResults) { + responseArray.length = maxResults; + } + this._setLastDate(responseArray[0].applied_at); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, this.generateMeta(item)); + } + }, + }, + hooks: { + async deploy() { + await this.startEvent(25); + }, + }, + async run() { + await this.startEvent(); + }, + sampleEmit, +}; diff --git a/components/greenhouse/sources/new-candidate-application/test-event.mjs b/components/greenhouse/sources/new-candidate-application/test-event.mjs new file mode 100644 index 0000000000000..6f0c0e3b41cf0 --- /dev/null +++ b/components/greenhouse/sources/new-candidate-application/test-event.mjs @@ -0,0 +1,83 @@ +export default { + "id": 69102626, + "candidate_id": 53883394, + "prospect": false, + "applied_at": "2017-09-27T12:03:02.728Z", + "rejected_at": "2017-09-27T12:11:40.877Z", + "last_activity_at": "2017-09-28T12:29:30.481Z", + "location": { + "address": "New York, New York, USA" + }, + "source": { + "id": 16, + "public_name": "LinkedIn (Prospecting)" + }, + "credited_to": { + "id": 165372, + "first_name": "Joel", + "last_name": "Job Admin", + "name": "Joel Job Admin", + "employee_id": null + }, + "rejection_reason": { + "id": 9504, + "name": "Hired another candidate", + "type": { + "id": 1, + "name": "We rejected them" + } + }, + "rejection_details": { + "custom_fields": { + "custom_rejection_question_field": null + }, + "keyed_custom_fields": { + "custom_rejection_question_field": { + "name": "Custom Rejection Question Field", + "type": "short_text", + "value": null + } + } + }, + "jobs": [ + { + "id": 149995, + "name": "DevOps Engineer" + } + ], + "job_post_id": 123, + "status": "rejected", + "current_stage": { + "id": 1073533, + "name": "Take Home Test" + }, + "answers": [ + { + "question": "How did you hear about this job?", + "answer": "A friend" + }, + { + "question": "Website", + "answer": "https://example.com" + }, + { + "question": "LinkedIn Profile", + "answer": "https://linkedin.com/example" + } + ], + "prospective_office": null, + "prospective_department": null, + "prospect_detail": { + "prospect_pool": null, + "prospect_stage": null, + "prospect_owner": null + }, + "attachments": [ + { + "filename": "John_Locke_Offer_Packet_09_27_2017.pdf", + "url": "https://prod-heroku.s3.amazonaws.com/...", + "type": "offer_packet", + "created_at": "2020-09-27T18:45:27.137Z" + } + ] +} \ No newline at end of file diff --git a/components/greenhouse/sources/new-scheduled-interview/new-scheduled-interview.mjs b/components/greenhouse/sources/new-scheduled-interview/new-scheduled-interview.mjs new file mode 100644 index 0000000000000..a3ba5167be6aa --- /dev/null +++ b/components/greenhouse/sources/new-scheduled-interview/new-scheduled-interview.mjs @@ -0,0 +1,81 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import greenhouse from "../../greenhouse.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "greenhouse-new-scheduled-interview", + name: "New Scheduled Interview", + description: "Emit new event when a new interview is scheduled within a specific time period.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + greenhouse, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + startsAfter: { + type: "string", + label: "Starts After", + description: "Only return scheduled interviews scheduled to start at or after this timestamp. Timestamps must be in in [ISO-8601](https://developers.greenhouse.io/harvest.html#general-considerations) format.", + }, + endsBefore: { + type: "string", + label: "Ends Before", + description: "Only return scheduled interviews scheduled to end before this timestamp. Timestamps must be in in [ISO-8601](https://developers.greenhouse.io/harvest.html#general-considerations) format.", + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(id) { + this.db.set("lastId", id); + }, + generateMeta(interview) { + return { + id: interview.id, + summary: `New Interview Scheduled: ${interview.id}`, + ts: Date.parse(interview.updated_at), + }; + }, + async startEvent(maxResults) { + const lastId = this._getLastId(); + const response = this.greenhouse.paginate({ + fn: this.greenhouse.listInterviews, + params: { + starts_after: this.startsAfter, + ends_before: this.endsBefore, + }, + maxResults, + }); + + let responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + responseArray.sort((a, b) => b.id - a.id); + responseArray.filter((item) => item.id > lastId); + + if (responseArray.length) this._setLastId(responseArray[0].id); + + for (const item of responseArray.reverse()) { + this.$emit(item, this.generateMeta(item)); + } + }, + }, + hooks: { + async deploy() { + await this.startEvent(25); + }, + }, + async run() { + await this.startEvent(); + }, + sampleEmit, +}; diff --git a/components/greenhouse/sources/new-scheduled-interview/test-event.mjs b/components/greenhouse/sources/new-scheduled-interview/test-event.mjs new file mode 100644 index 0000000000000..0dce97b95ff65 --- /dev/null +++ b/components/greenhouse/sources/new-scheduled-interview/test-event.mjs @@ -0,0 +1,37 @@ +export default { + "id": 9128481, + "application_id": 4684156, + "external_event_id": "event123", + "start": { + "date_time": "2014-03-26T22:15:00.000Z" + }, + "end": { + "date_time": "2014-03-26T22:30:00.000Z" + }, + "location": "Big Conference Room", + "video_conferencing_url": "http://example.com", + "status": "awaiting_feedback", + "created_at": "2016-02-10T14:31:51.019Z", + "updated_at": "2016-05-23T20:43:11.679Z", + "interview": { + "id": 7001, + "name": "Culture Fit" + }, + "organizer": { + "id": 2000, + "first_name": "Jack", + "last_name": "Shepard", + "name": "Jack Shepard", + "employee_id": "12345" + }, + "interviewers": [ + { + "id": 4080, + "employee_id": "employee123", + "name": "Kate Austen", + "email": "kate.austen@example.com", + "response_status": "needs_action", + "scorecard_id": 11274 + } + ] +} \ No newline at end of file diff --git a/components/greenhouse/sources/watch-candidates/test-event.mjs b/components/greenhouse/sources/watch-candidates/test-event.mjs new file mode 100644 index 0000000000000..9e48a2cb02f8e --- /dev/null +++ b/components/greenhouse/sources/watch-candidates/test-event.mjs @@ -0,0 +1,13 @@ +export default { + "id": 6756789, + "created_at": "2014-04-01T15:55:29Z", + "subject": "Candidate Rejected", + "body": "Reason: Lacking hustle\n\nThis candidate turned out to be problematic for us...", + "user": { + "id": 214, + "first_name": "Boone", + "last_name": "Carlyle", + "name": "Boone Carlyle", + "employee_id": "67890" + } +} \ No newline at end of file diff --git a/components/greenhouse/sources/watch-candidates/watch-candidates.mjs b/components/greenhouse/sources/watch-candidates/watch-candidates.mjs new file mode 100644 index 0000000000000..feb940d3b5d96 --- /dev/null +++ b/components/greenhouse/sources/watch-candidates/watch-candidates.mjs @@ -0,0 +1,64 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import greenhouse from "../../greenhouse.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "greenhouse-watch-candidates", + name: "New Candidate Watching", + description: "Emit new event when a candidate's application or status changes.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + greenhouse, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + candidateId: { + propDefinition: [ + greenhouse, + "candidateId", + ], + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(id) { + this.db.set("lastId", id); + }, + generateMeta(interview) { + return { + id: interview.id, + summary: `New activity for candidate ID ${this.candidateId}`, + ts: Date.parse(interview.updated_at), + }; + }, + }, + async run() { + const lastId = this._getLastId(); + const { + notes, emails, activities, + } = await this.greenhouse.getActivity(this.candidateId); + const feed = [ + ...notes, + ...emails, + ...activities, + ]; + + feed.sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at)) + .filter((item) => item.id > lastId); + + if (feed.length) this._setLastId(feed[0].id); + + for (const item of feed.reverse()) { + this.$emit(item, this.generateMeta(item)); + } + }, + sampleEmit, +}; diff --git a/components/greptile/actions/query-codebase/query-codebase.mjs b/components/greptile/actions/query-codebase/query-codebase.mjs new file mode 100644 index 0000000000000..f56442e74d66f --- /dev/null +++ b/components/greptile/actions/query-codebase/query-codebase.mjs @@ -0,0 +1,68 @@ +import app from "../../greptile.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "greptile-query-codebase", + name: "Query Codebase", + description: "Search the user's codebase using a natural language query. [See the documentation](https://docs.greptile.com/apps/overview)", + version: "0.0.1", + type: "action", + props: { + app, + query: { + type: "string", + label: "Query", + description: "A string containing the question in natural language.", + }, + repositories: { + type: "string[]", + label: "Repositories", + description: "List of repositories indexed in Greptile to reference while answering your question. Array of JSON objects with keys `remote`, `branch` and `repository` eg. `{\"remote\":\"github\",\"branch\":\"main\",\"repository\":\"PipedreamHQ/pipedream\"}`", + }, + sessionId: { + type: "string", + label: "Session ID", + description: "Only use this if you intend to need to retrieve chat history later.", + optional: true, + }, + genius: { + type: "boolean", + label: "Genius", + description: "Genius requests are smarter but 8-10 seconds slower, great for complex usecases like reviewing PR and updating technical docs.", + optional: true, + }, + }, + methods: { + queryCodebase(args = {}) { + return this.app.post({ + path: "/query", + ...args, + }); + }, + }, + async run({ $ }) { + const { + queryCodebase, + query, + repositories, + sessionId, + genius, + } = this; + + const response = await queryCodebase({ + $, + data: { + messages: [ + { + content: query, + }, + ], + repositories: utils.parseArray(repositories), + sessionId, + genius, + }, + }); + $.export("$summary", "Successfully queried the codebase."); + return response; + }, +}; diff --git a/components/greptile/common/constants.mjs b/components/greptile/common/constants.mjs new file mode 100644 index 0000000000000..b34c236c932fa --- /dev/null +++ b/components/greptile/common/constants.mjs @@ -0,0 +1,7 @@ +const BASE_URL = "https://api.greptile.com"; +const VERSION_PATH = "/v2"; + +export default { + BASE_URL, + VERSION_PATH, +}; diff --git a/components/greptile/common/utils.mjs b/components/greptile/common/utils.mjs new file mode 100644 index 0000000000000..eceb6a9167739 --- /dev/null +++ b/components/greptile/common/utils.mjs @@ -0,0 +1,50 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function isJson(value) { + value = + typeof(value) !== "string" + ? JSON.stringify(value) + : value; + + try { + value = JSON.parse(value); + } catch (e) { + return false; + } + + return typeof(value) === "object" && value !== null; +} + +function valueToObject(value) { + if (!isJson(value)) { + return value; + } + return JSON.parse(value); +} + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid JSON array object"); + } +} + +export default { + parseArray: (value) => parseArray(value).map(valueToObject), +}; diff --git a/components/greptile/greptile.app.mjs b/components/greptile/greptile.app.mjs new file mode 100644 index 0000000000000..34aa7202a75d0 --- /dev/null +++ b/components/greptile/greptile.app.mjs @@ -0,0 +1,33 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "greptile", + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + }, +}; diff --git a/components/greptile/package.json b/components/greptile/package.json new file mode 100644 index 0000000000000..27143190f69db --- /dev/null +++ b/components/greptile/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/greptile", + "version": "0.1.0", + "description": "Pipedream Greptile Components", + "main": "greptile.app.mjs", + "keywords": [ + "pipedream", + "greptile" + ], + "homepage": "https://pipedream.com/apps/greptile", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/grist/.gitignore b/components/grist/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/grist/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/grist/README.md b/components/grist/README.md index cbb3bc0d897c1..05d1e06478f32 100644 --- a/components/grist/README.md +++ b/components/grist/README.md @@ -1,14 +1,11 @@ # Overview -Using the Grist API, you can build applications that can: - -- Create and manage customer orders -- Create and manage products -- Create and manageblogs -- Create and manage users -- Create and manage events -- Create and manage files -- Create and manage photos -- Create and manage galleries -- Create and manage comments -- Create and manage pages +Grist API on Pipedream enables you to automate data management tasks in your Grist documents. This might mean syncing data across different platforms, triggering notifications based on data changes, or processing data through custom logic. With Pipedream, you can use the Grist API to build workflows that react to events in real-time, connect to hundreds of other services, manipulate data in sophisticated ways, and create custom endpoints to integrate with your Grist data. + +# Example Use Cases + +- **Automated Data Backup**: Save a copy of your Grist tables to Google Drive or Dropbox at regular intervals. Pipedream can schedule a workflow that fetches the latest data from Grist and automatically uploads it to your preferred cloud storage, ensuring your data is always backed up. + +- **Project Management Updates**: Sync task updates from Grist to project management tools like Trello or Asana. When a status is updated in a Grist table, Pipedream can catch this event and use it to update a card in Trello or a task in Asana, keeping project statuses aligned across your tools. + +- **Email Campaign Management**: When a new contact is added to a Grist table, trigger a workflow that adds the contact to your Mailchimp list and sends a welcome email. Pipedream can monitor your Grist table for new entries and automatically update your email marketing campaigns, keeping your outreach efforts seamless. diff --git a/components/grist/actions/add-records/add-records.mjs b/components/grist/actions/add-records/add-records.mjs new file mode 100644 index 0000000000000..172da38d75a24 --- /dev/null +++ b/components/grist/actions/add-records/add-records.mjs @@ -0,0 +1,64 @@ +import utils from "../../common/utils.mjs"; +import app from "../../grist.app.mjs"; + +export default { + key: "grist-add-records", + name: "Add Records", + description: "Appends new records to a chosen table in Grist. [See the documentation](https://support.getgrist.com/api/#tag/records/operation/addRecords)", + version: "0.0.2", + type: "action", + props: { + app, + docId: { + propDefinition: [ + app, + "docId", + ], + }, + tableId: { + propDefinition: [ + app, + "tableId", + ({ docId }) => ({ + docId, + }), + ], + }, + records: { + propDefinition: [ + app, + "records", + ], + }, + noParse: { + propDefinition: [ + app, + "noParse", + ], + }, + }, + async run({ $ }) { + const { + app, + docId, + tableId, + noParse, + records, + } = this; + + const response = await app.addRecords({ + $, + docId, + tableId, + params: { + noparse: noParse, + }, + data: { + records: utils.parseArray(records), + }, + }); + + $.export("$summary", `Successfully added \`${response.records.length}\` record(s) to the table.`); + return response; + }, +}; diff --git a/components/grist/actions/add-update-records/add-update-records.mjs b/components/grist/actions/add-update-records/add-update-records.mjs new file mode 100644 index 0000000000000..89b3dcfcbcab6 --- /dev/null +++ b/components/grist/actions/add-update-records/add-update-records.mjs @@ -0,0 +1,116 @@ +import app from "../../grist.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "grist-add-update-records", + name: "Add Or Update Records", + description: "Add records in a specified table or updates existing matching records. [See the documentation](https://support.getgrist.com/api/#tag/records/operation/replaceRecords)", + version: "0.0.2", + type: "action", + props: { + app, + docId: { + propDefinition: [ + app, + "docId", + ], + }, + tableId: { + propDefinition: [ + app, + "tableId", + ({ docId }) => ({ + docId, + }), + ], + }, + noParse: { + propDefinition: [ + app, + "noParse", + ], + }, + onMany: { + type: "string", + label: "On Many", + description: "Which records to update if multiple records are found to match.", + optional: true, + options: [ + { + value: "first", + label: "The first matching record (default)", + }, + { + value: "none", + label: "Do not update anything", + }, + { + value: "all", + label: "Update all matches", + }, + ], + }, + noAdd: { + type: "boolean", + label: "No Add", + description: "Set to true to prohibit adding records.", + optional: true, + }, + noUpdate: { + type: "boolean", + label: "No Update", + description: "Set to true to prohibit updating records.", + optional: true, + }, + records: { + description: app.propDefinitions.records.description + " Instead of an id, a `require` object is provided, with the same structure as `fields`. If no query parameter options are set, then the operation is as follows. First, we check if a record exists matching the values specified for columns in `require`. If so, we update it by setting the values specified for columns in fields. If not, we create a new record with a combination of the values in `require` and `fields`, with `fields` taking priority if the same column is specified in both. The query parameters allow for variations on this behavior. Eg. `[ { \"require\": { \"pet\": \"cat\" }, \"fields\": { \"popularity\": 67 } } ]`", + propDefinition: [ + app, + "records", + ], + }, + }, + methods: { + addUpdateRecords({ + docId, tableId, ...args + } = {}) { + return this.app.put({ + path: `/docs/${docId}/tables/${tableId}/records`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + addUpdateRecords, + docId, + tableId, + noParse: noparse, + onMany: onmany, + noAdd: noadd, + noUpdate: noupdate, + records, + } = this; + + await addUpdateRecords({ + $, + docId, + tableId, + params: { + noparse, + onmany, + noadd, + noupdate, + }, + data: { + records: utils.parseArray(records), + }, + }); + + $.export("$summary", "Successfully ran this action"); + + return { + success: true, + }; + }, +}; diff --git a/components/grist/actions/find-records/find-records.mjs b/components/grist/actions/find-records/find-records.mjs new file mode 100644 index 0000000000000..7461d7855d4f7 --- /dev/null +++ b/components/grist/actions/find-records/find-records.mjs @@ -0,0 +1,75 @@ +import utils from "../../common/utils.mjs"; +import app from "../../grist.app.mjs"; + +export default { + key: "grist-find-records", + name: "Find Records", + description: "Searches for records in a specified table. [See the documentation](https://support.getgrist.com/api/#tag/records/operation/listRecords)", + version: "0.0.2", + type: "action", + props: { + app, + docId: { + propDefinition: [ + app, + "docId", + ], + }, + tableId: { + propDefinition: [ + app, + "tableId", + ({ docId }) => ({ + docId, + }), + ], + }, + filter: { + type: "string", + label: "Filter", + description: "This is a JSON object mapping column names to arrays of allowed values. For example, to filter column pet for values cat and dog, the filter would be `{\"pet\": [\"cat\", \"dog\"]}`. Multiple columns can be filtered. For example the filter for pet being either `cat` or `dog`, AND `size` being either `tiny` or `outrageously small`, would be `{\"pet\": [\"cat\", \"dog\"], \"size\": [\"tiny\", \"outrageously small\"]}`", + optional: true, + }, + limit: { + type: "integer", + label: "Limit", + description: "Return at most this number of rows. A value of 0 is equivalent to having no limit.", + optional: true, + }, + }, + methods: { + findRecords({ + docId, tableId, ...args + } = {}) { + return this.app._makeRequest({ + path: `/docs/${docId}/tables/${tableId}/records`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + findRecords, + docId, + tableId, + filter, + limit, + } = this; + + filter && utils.valueToObject(filter); + + const response = await findRecords({ + $, + docId, + tableId, + params: { + filter, + limit, + }, + }); + + $.export("$summary", `Successfully found \`${response.records.length}\` record(s) in the table.`); + + return response; + }, +}; diff --git a/components/grist/app/grist.app.ts b/components/grist/app/grist.app.ts deleted file mode 100644 index 45982fe0adb4f..0000000000000 --- a/components/grist/app/grist.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "grist", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); diff --git a/components/grist/common/constants.mjs b/components/grist/common/constants.mjs new file mode 100644 index 0000000000000..6d96310262c9d --- /dev/null +++ b/components/grist/common/constants.mjs @@ -0,0 +1,9 @@ +const BASE_URL = "https://docs.getgrist.com"; +const VERSION_PATH = "/api"; +const WEBHOOK_ID = "webhookId"; + +export default { + BASE_URL, + VERSION_PATH, + WEBHOOK_ID, +}; diff --git a/components/grist/common/utils.mjs b/components/grist/common/utils.mjs new file mode 100644 index 0000000000000..890861edfe736 --- /dev/null +++ b/components/grist/common/utils.mjs @@ -0,0 +1,51 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function isJson(value) { + try { + JSON.parse(value); + } catch (e) { + return false; + } + + return true; +} + +function valueToObject(value) { + if (typeof(value) === "object") { + return value; + } + + if (!isJson(value)) { + throw new ConfigurationError(`Make sure the custom expression contains a valid JSON object: ${value}`); + } + + return JSON.parse(value); +} + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid JSON array object"); + } +} + +export default { + valueToObject, + parseArray: (value) => parseArray(value).map(valueToObject), +}; diff --git a/components/grist/grist.app.mjs b/components/grist/grist.app.mjs new file mode 100644 index 0000000000000..c328f4a94badf --- /dev/null +++ b/components/grist/grist.app.mjs @@ -0,0 +1,91 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "grist", + propDefinitions: { + docId: { + type: "string", + label: "Document ID", + description: "The ID of the Grist document to operate on. You can find this in the **Settings** menu of the document.", + }, + tableId: { + type: "string", + label: "Table ID", + description: "The ID of the table to operate on.", + async options({ docId }) { + const { tables } = await this.listTables({ + docId, + }); + return tables.map(({ id: value }) => value); + }, + }, + noParse: { + type: "boolean", + label: "Do Not Parse", + description: "Set to `true` to prohibit parsing strings according to the column type.", + optional: true, + }, + records: { + type: "string[]", + label: "Data Records", + description: "The data for the records to append or update. Each record should be a JSON-formatted string, mapping column names to [cell values](https://support.getgrist.com/code/modules/GristData/#cellvalue). Eg. `[ { \"fields\": { \"pet\": \"cat\", \"popularity\": 67 } } ]`.", + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.$auth.api_key}`, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + put(args = {}) { + return this._makeRequest({ + method: "PUT", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + listTables({ + docId, ...args + } = {}) { + return this._makeRequest({ + path: `/docs/${docId}/tables`, + ...args, + }); + }, + addRecords({ + docId, tableId, ...args + } = {}) { + return this.post({ + path: `/docs/${docId}/tables/${tableId}/records`, + ...args, + }); + }, + }, +}; diff --git a/components/grist/package.json b/components/grist/package.json index 1584c2aed202f..81e2ada007ca7 100644 --- a/components/grist/package.json +++ b/components/grist/package.json @@ -1,18 +1,18 @@ { "name": "@pipedream/grist", - "version": "0.0.3", + "version": "0.1.1", "description": "Pipedream Grist Components", - "main": "dist/app/grist.app.mjs", + "main": "grist.app.mjs", "keywords": [ "pipedream", "grist" ], - "files": [ - "dist" - ], "homepage": "https://pipedream.com/apps/grist", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" } } diff --git a/components/grist/sources/common/events.mjs b/components/grist/sources/common/events.mjs new file mode 100644 index 0000000000000..632d8a41fe9bb --- /dev/null +++ b/components/grist/sources/common/events.mjs @@ -0,0 +1,4 @@ +export default { + ADD: "add", + UPDATE: "update", +}; diff --git a/components/grist/sources/common/webhook.mjs b/components/grist/sources/common/webhook.mjs new file mode 100644 index 0000000000000..fc072cc6c1fe1 --- /dev/null +++ b/components/grist/sources/common/webhook.mjs @@ -0,0 +1,118 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../grist.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + http: "$.interface.http", + docId: { + propDefinition: [ + app, + "docId", + ], + }, + tableId: { + propDefinition: [ + app, + "tableId", + ({ docId }) => ({ + docId, + }), + ], + }, + }, + hooks: { + async activate() { + const { + http, + docId, + tableId, + getEventTypes, + createWebhooks, + setWebhookId, + } = this; + + const response = + await createWebhooks({ + docId, + data: { + webhooks: [ + { + fields: { + url: http.endpoint, + enabled: true, + eventTypes: getEventTypes(), + tableId, + }, + }, + ], + }, + }); + + setWebhookId(response?.webhooks?.[0]?.id); + }, + async deactivate() { + const { + docId, + getWebhookId, + deleteWebhook, + } = this; + + const webhookId = getWebhookId(); + if (webhookId) { + await deleteWebhook({ + docId, + webhookId, + }); + } + }, + }, + methods: { + setWebhookId(value) { + this.db.set(constants.WEBHOOK_ID, value); + }, + getWebhookId() { + return this.db.get(constants.WEBHOOK_ID); + }, + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getEventTypes() { + throw new ConfigurationError("getEventTypes is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resources) { + resources.forEach((resource) => { + this.$emit(resource, this.generateMeta(resource)); + }); + }, + createWebhooks({ + docId, ...args + } = {}) { + return this.app.post({ + debug: true, + path: `/docs/${docId}/webhooks`, + ...args, + }); + }, + deleteWebhook({ + docId, webhookId, ...args + } = {}) { + return this.app.delete({ + debug: true, + path: `/docs/${docId}/webhooks/${webhookId}`, + ...args, + }); + }, + }, + async run({ body }) { + this.processResource(body); + }, +}; diff --git a/components/grist/sources/new-or-updated-record-instant/new-or-updated-record-instant.mjs b/components/grist/sources/new-or-updated-record-instant/new-or-updated-record-instant.mjs new file mode 100644 index 0000000000000..d8a7bce1c287f --- /dev/null +++ b/components/grist/sources/new-or-updated-record-instant/new-or-updated-record-instant.mjs @@ -0,0 +1,29 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; + +export default { + ...common, + key: "grist-new-or-updated-record-instant", + name: "New or Updated Record (Instant)", + description: "Emit new event once a record is updated or newly created in Grist. [See the documentation](https://support.getgrist.com/api/#tag/webhooks)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventTypes() { + return [ + events.ADD, + events.UPDATE, + ]; + }, + generateMeta(resource) { + const ts = Date.now(); + return { + id: `${resource.id}-${ts}`, + summary: `New Or Updated Record ID ${resource.id}`, + ts, + }; + }, + }, +}; diff --git a/components/grist/sources/new-record-instant/new-record-instant.mjs b/components/grist/sources/new-record-instant/new-record-instant.mjs new file mode 100644 index 0000000000000..6bd2dfa5bc8f5 --- /dev/null +++ b/components/grist/sources/new-record-instant/new-record-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; + +export default { + ...common, + key: "grist-new-record-instant", + name: "New Record (Instant)", + description: "Emit new event when a record is just created. [See the documentation](https://support.getgrist.com/api/#tag/webhooks)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventTypes() { + return [ + events.ADD, + ]; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Record ID ${resource.id}`, + ts: Date.now(), + }; + }, + }, +}; diff --git a/components/groovehq/README.md b/components/groovehq/README.md new file mode 100644 index 0000000000000..a109b3e48bcf7 --- /dev/null +++ b/components/groovehq/README.md @@ -0,0 +1,11 @@ +# Overview + +The GrooveHQ API lets you automate your customer support tasks by interfacing with Groove's ticketing system. With it, you can create, update, and manage support tickets, work with customers' data, and leverage knowledge base articles. Integrate GrooveHQ with Pipedream to trigger workflows on new tickets, synchronize customer information with other business tools, and streamline support operations by automating repetitive tasks. + +# Example Use Cases + +- **Ticket Management Automation**: Automatically categorize and assign new tickets in GrooveHQ based on specific keywords or customer data. Use Pipedream to parse incoming tickets, apply logic to determine the appropriate category and assignee, and update the ticket in GrooveHQ. + +- **Synchronize Support Tickets with a CRM**: Keep your CRM up-to-date with the latest support interactions by using Pipedream to synchronize ticket information from GrooveHQ to CRM platforms like Salesforce or HubSpot. Whenever a ticket is created or updated in GrooveHQ, the corresponding record in your CRM is automatically updated or created. + +- **Customer Feedback Collection**: After a ticket is marked as solved in GrooveHQ, trigger a Pipedream workflow that sends a follow-up email to the customer asking for feedback. Use this process to gather valuable insights and funnel them into a Google Sheet or a data analytics tool for review and action. diff --git a/components/groovehq/groovehq.app.mjs b/components/groovehq/groovehq.app.mjs new file mode 100644 index 0000000000000..73cd1fdb48e7b --- /dev/null +++ b/components/groovehq/groovehq.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "groovehq", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/groovehq/package.json b/components/groovehq/package.json new file mode 100644 index 0000000000000..ea086f2563e69 --- /dev/null +++ b/components/groovehq/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/groovehq", + "version": "0.0.1", + "description": "Pipedream GrooveHQ Components", + "main": "groovehq.app.mjs", + "keywords": [ + "pipedream", + "groovehq" + ], + "homepage": "https://pipedream.com/apps/groovehq", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/groqcloud/actions/create-chat-completion/create-chat-completion.mjs b/components/groqcloud/actions/create-chat-completion/create-chat-completion.mjs new file mode 100644 index 0000000000000..b52de64249678 --- /dev/null +++ b/components/groqcloud/actions/create-chat-completion/create-chat-completion.mjs @@ -0,0 +1,62 @@ +import app from "../../groqcloud.app.mjs"; + +export default { + key: "groqcloud-create-chat-completion", + name: "Create Chat Completion", + description: "Creates a model response for the given chat conversation. [See the documentation](https://console.groq.com/docs/api-reference#chat-create)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "name", + ], + }, + content: { + propDefinition: [ + app, + "content", + ], + }, + role: { + propDefinition: [ + app, + "role", + ], + }, + model: { + propDefinition: [ + app, + "model", + ], + }, + seed: { + propDefinition: [ + app, + "seed", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createChatCompletion({ + $, + data: { + messages: [ + { + name: this.name, + role: this.role, + content: this.content, + }, + ], + model: this.model, + seed: this.seed, + }, + }); + + $.export("$summary", `Successfully generated a response with the ID '${response.id}'`); + + return response; + }, +}; diff --git a/components/groqcloud/common/constants.mjs b/components/groqcloud/common/constants.mjs new file mode 100644 index 0000000000000..43e2c0836fa3a --- /dev/null +++ b/components/groqcloud/common/constants.mjs @@ -0,0 +1,8 @@ +export default { + ROLES: [ + "user", + "system", + "assistant", + "tool", + ], +}; diff --git a/components/groqcloud/groqcloud.app.mjs b/components/groqcloud/groqcloud.app.mjs new file mode 100644 index 0000000000000..23cddfc0e1ab0 --- /dev/null +++ b/components/groqcloud/groqcloud.app.mjs @@ -0,0 +1,76 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "groqcloud", + propDefinitions: { + name: { + type: "string", + label: "Name", + description: "An optional name for the participant. Provides the model information to differentiate between participants of the same role", + optional: true, + }, + content: { + type: "string", + label: "Content", + description: "The contents of the message", + }, + role: { + type: "string", + label: "Author Role", + description: "The role of the message's author", + options: constants.ROLES, + }, + model: { + type: "string", + label: "Model", + description: "ID of the model to use", + async options() { + const response = await this.getModels({}); + const modelsIDs = response.data; + return modelsIDs.map(({ id }) => id); + }, + }, + seed: { + type: "integer", + label: "Seed", + description: "If specified, repeated requests with the same seed and parameters will return the same result", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.groq.com/openai/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + async createChatCompletion(args = {}) { + return this._makeRequest({ + method: "post", + path: "/chat/completions", + ...args, + }); + }, + async getModels(args = {}) { + return this._makeRequest({ + path: "/models", + ...args, + }); + }, + }, +}; diff --git a/components/groqcloud/package.json b/components/groqcloud/package.json new file mode 100644 index 0000000000000..ba319875e4d27 --- /dev/null +++ b/components/groqcloud/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/groqcloud", + "version": "0.1.0", + "description": "Pipedream Groq Cloud Components", + "main": "groqcloud.app.mjs", + "keywords": [ + "pipedream", + "groqcloud" + ], + "homepage": "https://pipedream.com/apps/groqcloud", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} diff --git a/components/groundhogg/README.md b/components/groundhogg/README.md new file mode 100644 index 0000000000000..3ffea04b8bd2e --- /dev/null +++ b/components/groundhogg/README.md @@ -0,0 +1,11 @@ +# Overview + +The GroundHogg API provides a set of endpoints to interact with your marketing automation data. On Pipedream, you can leverage these endpoints to create, update, and manage contacts, tags, funnels, and emails among other entities within GroundHogg. With Pipedream, this integration becomes part of a larger ecosystem, allowing you to connect GroundHogg with hundreds of other apps, automate workflows, and manipulate data in real-time. This can dramatically improve your marketing efforts by enabling personalized and timely interactions with your audience. + +# Example Use Cases + +- **Sync New Shopify Customers to GroundHogg**: Automatically create new contacts in GroundHogg whenever a customer makes their first purchase on Shopify. This workflow keeps your marketing data up-to-date and allows you to immediately engage new customers with targeted campaigns. + +- **Schedule Follow-up Emails After Zoom Webinars**: After a Zoom webinar ends, use Pipedream to identify attendees and no-shows. With that data, create or update contacts in GroundHogg and trigger personalized follow-up emails to continue the conversation or provide additional resources. + +- **Slack Notifications for High-Value Opportunities**: Identify high-value opportunities in GroundHogg based on contact tagging or funnel stages. Use Pipedream to send custom notifications to a Slack channel when such opportunities are detected, ensuring your team can act quickly on potential deals. diff --git a/components/growsurf/README.md b/components/growsurf/README.md index 7e1bdfad843ab..20df75a408622 100644 --- a/components/growsurf/README.md +++ b/components/growsurf/README.md @@ -1,13 +1,11 @@ # Overview -Growsurf API lets you run powerful referral programs. With referral programs, -you can reward your users for referring new users to your product or service. -You can use the API to create and manage your referral programs, track -referrals, and pay out rewards. +GrowSurf is a referral platform that can amplify your growth efforts by incentivizing word-of-mouth marketing. By harnessing the GrowSurf API on Pipedream, you can automate campaign management, track participant performance, reward achievements, and sync referral data across your marketing stack. This seamless integration enables personalized engagement with your audience, targeted follow-ups, and real-time analytics for informed decision-making. -Here are some examples of what you can build with the Growsurf API: +# Example Use Cases -- A referral program for your product or service -- A loyalty program for your customers -- A referral system for your website or blog -- A way to track referrals and pay out rewards +- **Automated Referral Reward Fulfillment**: Trigger a Pipedream workflow when a GrowSurf participant reaches a certain milestone or earns a reward. Automatically issue discount codes or send personalized rewards via email, integrating with services like SendGrid or Mailchimp. + +- **Dynamic CRM Updates**: Sync new GrowSurf campaign participant data to your CRM platform, such as Salesforce or HubSpot. When a user signs up through your referral program, add them as a contact in your CRM and tag them for segmentation and targeted marketing campaigns. + +- **Real-Time Analytics Dashboard**: Use Pipedream to send GrowSurf campaign data to a Google Sheets or Airtable base. Create a real-time dashboard that visualizes referral performance metrics, helping you quickly assess the effectiveness of your campaigns and make data-driven adjustments. diff --git a/components/gryd/README.md b/components/gryd/README.md new file mode 100644 index 0000000000000..15f2823342de2 --- /dev/null +++ b/components/gryd/README.md @@ -0,0 +1,11 @@ +# Overview + +The Gryd API is a resourceful tool for accessing data related to real estate, such as apartment listings, property details, and pricing information. Within Pipedream's serverless platform, you can employ Gryd API to automate workflows, sync data across various apps, and extract real-time insights from the housing market. Leveraging Pipedream's ability to connect to hundreds of apps, you can integrate Gryd data with other services, such as CRMs, messaging apps, or spreadsheets, to streamline operations, alert users, or analyze property trends. + +# Example Use Cases + +- **New Listing Alert System**: Trigger a workflow in Pipedream whenever a new property listing is added to Gryd. Use this trigger to send a formatted message to a Slack channel or an email via SendGrid to notify real estate agents or interested parties about new opportunities on the market. + +- **Dynamic Pricing Analysis**: Schedule a daily or weekly workflow in Pipedream to pull the latest property pricing from Gryd API, then send the data to a Google Sheet. This can be used to track market trends, analyze pricing strategies, or update website listings with current pricing information. + +- **CRM Integration for Lead Generation**: When Gryd has a new potential lead or inquiry, use Pipedream to capture that event and add or update the lead information in a CRM like Salesforce. Further, you can automate a follow-up task creation for a sales rep to ensure timely engagement with the prospect. diff --git a/components/gtmetrix/README.md b/components/gtmetrix/README.md new file mode 100644 index 0000000000000..03ab6ab6d5195 --- /dev/null +++ b/components/gtmetrix/README.md @@ -0,0 +1,11 @@ +# Overview + +The GTmetrix API provides an interface to test the loading speed of your website, offering insights into performance issues and potential optimizations. By integrating this API with Pipedream, you can automate performance monitoring, receive alerts, and combine data with other services for in-depth analysis. For instance, you could trigger a performance report after a site update, log results to a spreadsheet for tracking, or compare your metrics against industry standards. + +# Example Use Cases + +- **Automated Performance Check on Deployment**: Trigger a GTmetrix test whenever your website deployment process completes. Use a webhook to start the test and save the results to Google Sheets for historical performance tracking. + +- **Scheduled Performance Monitoring**: Set up a cron job in Pipedream that periodically triggers GTmetrix tests for your website. Send alerts via email or Slack if performance drops below a certain threshold. + +- **Competitive Analysis Workflow**: Compare your site's performance against competitors by running GTmetrix tests for multiple URLs. Use Pipedream to send the results to Airtable, creating a dashboard for easy comparison. diff --git a/components/gumroad/README.md b/components/gumroad/README.md index bad62f68a87ea..75c3a4ae15af9 100644 --- a/components/gumroad/README.md +++ b/components/gumroad/README.md @@ -1,10 +1,11 @@ # Overview -With the Gumroad API, you can build applications that can: - -- Create and manage products -- Create and manage coupons -- Checkout customers -- Handle webhooks -- Manage affiliates -- And more! +The Gumroad API lets you tap into your digital storefront with ease, enabling seamless automation of your e-commerce activities. It's a game-changer for creators and entrepreneurs who use Gumroad to sell products. With this API on Pipedream, you can automate tasks like order processing, customer management, and sales reporting. Imagine syncing new purchases with your CRM, sending custom thank-you emails post-purchase, or compiling sales data without lifting a finger. Pipedream's serverless platform turns these ideas into reality, connecting Gumroad to countless other apps with zero server maintenance. + +# Example Use Cases + +- **Automate Customer Follow-up Emails**: When a customer completes a purchase on Gumroad, trigger an automated workflow on Pipedream that sends a personalized thank-you email from your Gmail. This keeps engagement high and can boost customer loyalty. + +- **Sync Purchases with a CRM**: Use Pipedream to capture new Gumroad sales and automatically create or update contacts in your CRM, such as HubSpot. This workflow ensures your customer relationships are always up-to-date, allowing for targeted marketing campaigns and sales follow-ups. + +- **Generate Real-time Sales Reports**: Every time you make a sale on Gumroad, Pipedream can trigger a workflow that adds the sale data to a Google Sheet. Use this to maintain an up-to-the-minute sales report, giving you the insights you need to make informed business decisions. diff --git a/components/gupshup/README.md b/components/gupshup/README.md new file mode 100644 index 0000000000000..3608c06abff87 --- /dev/null +++ b/components/gupshup/README.md @@ -0,0 +1,11 @@ +# Overview + +The Gupshup API enables developers to build communication solutions with extensive messaging capabilities. On Pipedream, you can harness this power to create serverless workflows that interact with Gupshup's messaging services. Automate sending messages, create chatbots, or build complex communication systems that react to incoming messages or events. The workflows can be triggered by webhooks, schedules, or other apps' events, and you can integrate Gupshup with numerous other services available on Pipedream. + +# Example Use Cases + +- **Automated Customer Support**: Deploy a chatbot that uses Gupshup to handle incoming customer queries. Integrate with a CRM like Salesforce on Pipedream to fetch customer data and provide personalized responses. + +- **Marketing Campaign Tracker**: Send out bulk marketing messages via Gupshup and track responses with Pipedream workflows. Connect to Google Sheets to log responses and analyze the effectiveness of your campaign. + +- **Event-Based Notifications**: Set up a workflow that listens for specific triggers—like a new sale in Shopify—and sends out custom notifications through Gupshup to the relevant stakeholders. diff --git a/components/guru/README.md b/components/guru/README.md index 094e95ff81a5f..6278367abf420 100644 --- a/components/guru/README.md +++ b/components/guru/README.md @@ -1,11 +1,11 @@ # Overview -Guru is a powerful API that enables you to build a wide variety of -applications. Below are just a few examples of what you can build with Guru: - -- A powerful search engine that can search through a large database of - documents very quickly. -- An application that can automatically generate comments on documents based on - their content. -- A system that can automatically tag documents with relevant keywords, making - it easy to find them later. +The Guru API on Pipedream enables the automation of knowledge sharing and management tasks within your team or organization. Using this API, you can programmatically interact with Guru's knowledge base, including retrieving card details, creating new cards, and updating existing content. By leveraging Pipedream's serverless platform, you can craft workflows that trigger based on events from other services, process the data, and perform actions in Guru to keep your team's knowledge up to date and accessible. + +# Example Use Cases + +- **Sync Knowledge Base with Support Tickets**: When a new support ticket is solved in Zendesk, automatically create a Guru card summarizing the issue and resolution. This ensures that your support team's knowledge is continuously updated and available for future reference. + +- **Content Approval Workflow**: Use Guru's API to flag content for review. When a new card is created in Guru, trigger a Pipedream workflow that sends a notification to a Slack channel for content approval. Once approved, update the card's status in Guru to reflect it's ready for the team. + +- **Weekly Knowledge Digest**: Set up a Pipedream workflow that runs weekly, collecting the most viewed or recently updated Guru cards. Compile this list into a digest and distribute it via email using SendGrid to keep the team informed of the latest knowledge entries and updates. diff --git a/components/h_supertools_analytics_tool/README.md b/components/h_supertools_analytics_tool/README.md new file mode 100644 index 0000000000000..f0041c08372b8 --- /dev/null +++ b/components/h_supertools_analytics_tool/README.md @@ -0,0 +1,11 @@ +# Overview + +The H-supertools Analytics Tool API provides insights about SEO and online marketing. With Pipedream, you can leverage these insights in automated workflows. Imagine triggering actions based on keyword volumes, tracking your website's position for certain keywords, or getting alerts when your SEO performance changes. Pipedream's serverless platform lets you connect the H-supertools Analytics Tool with various other apps and services, enabling a host of data-driven applications. + +# Example Use Cases + +- **SEO Keyword Alerts**: Create a workflow on Pipedream that checks keyword rankings daily using the H-supertools Analytics Tool API. If rankings drop below a certain threshold, send an alert via Slack or email to your marketing team. + +- **Content Trend Analysis**: Set up a Pipedream workflow to fetch trending keywords weekly from the H-supertools Analytics Tool API. Use this data to guide your content creation strategy, automatically generating a report and storing it in Google Sheets for team review. + +- **Competitor Monitoring**: Build a Pipedream workflow that monitors the top keywords for which your competitors rank. Combine this with the Twitter API to automatically post updates when there are significant changes in the rankings, keeping your social media followers informed. diff --git a/components/habitica/README.md b/components/habitica/README.md index ab26e84ab5fe7..43ce440faf1d8 100644 --- a/components/habitica/README.md +++ b/components/habitica/README.md @@ -1,13 +1,11 @@ # Overview -Habitica is a website and app that helps you form good habits and stick to -them. You can use the Habitica API to help you build applications that help -people form and stick to good habits. +The Habitica API lets you gamify your life by interacting programmatically with your habits, dailies, to-dos, and rewards on the Habitica platform. This API offers endpoints for creating, fetching, updating, and deleting tasks, as well as retrieving user information and stats. When paired with Pipedream's serverless platform, the Habitica API becomes even more powerful, enabling customized, automated workflows that can respond to events in other apps or scheduled triggers to enhance your productivity and maintain your focus. -Here are some examples of what you can build using the Habitica API: +# Example Use Cases -- An app that helps people track their water intake -- An app that helps people track their steps -- An app that helps people track their eating habits -- An app that helps people track their sleeping habits -- An app that helps people track their exercise habits +- **Task Completion Rewards System**: Automatically trigger a reward in another service, like sending a congratulatory email through SendGrid or unlocking a special bonus in a loyalty app, when you check off a specific set of dailies or to-dos in Habitica. + +- **Smart Home Integration**: Connect your Habitica tasks to smart home devices using IFTTT or Home Assistant. For instance, mark a daily as done when you've switched off your bedroom lights, or trigger a smart coffee maker to start brewing once you've completed your morning routine tasks. + +- **Health and Fitness Sync**: Sync your fitness app data, like steps from Fitbit or workouts from Strava, to automatically complete related Habits or Dailies in Habitica. Use Pipedream's built-in code steps to interpret your fitness activity and mark tasks as done or update your Habitica character's stats accordingly. diff --git a/components/habitify/README.md b/components/habitify/README.md new file mode 100644 index 0000000000000..bacb6234f1349 --- /dev/null +++ b/components/habitify/README.md @@ -0,0 +1,11 @@ +# Overview + +The Habitify API lets you interact with your habits and data within the Habitify platform programmatically. By leveraging this API in Pipedream, you can automate custom workflows that help track habits, measure progress, and push notifications, among others. Pipedream's serverless platform makes it easy to connect Habitify with hundreds of other apps to automate complex tasks without writing extensive code. + +# Example Use Cases + +- **Habit Streak Tracker**: Send a daily summary of your habit streaks to your email or Slack. This workflow can be set to trigger at the end of the day, summarizing which habits you've completed and your current streaks, keeping you motivated. + +- **Habit Progress Logger**: Log your Habitify data to a Google Sheets spreadsheet. Each time you mark a habit as done in Habitify, the workflow triggers and records the habit, the completion time, and the date in a new row in Google Sheets, giving you a detailed and customizable log of your progress. + +- **Motivational Reminders**: Connect Habitify with Twilio to send SMS reminders for habits you haven’t marked off halfway through your day. This workflow can help ensure you stay on track with your habits by providing that extra nudge when needed. diff --git a/components/hacker_news/README.md b/components/hacker_news/README.md index 297ade57b5014..a424aa24f272c 100644 --- a/components/hacker_news/README.md +++ b/components/hacker_news/README.md @@ -1,3 +1,15 @@ +# Overview + +The Hacker News API on Pipedream allows you to tap into the vibrant pool of stories, comments, and user data from one of tech's favorite forums. By leveraging this API, you can automate the process of extracting data for analysis, tracking mentions of specific topics, or even monitoring the performance of your own submissions. The real power lies in integrating this wealth of information with other services to create custom, automated workflows that save time and keep you informed. + +# Example Use Cases + +- **Trending Alerts to Slack**: Use the Hacker News API to monitor stories gaining traction. Set up a workflow that checks for stories with a high number of points or comments and automatically sends a summary of these trending topics to a designated Slack channel. This keeps your team in the loop with minimal effort. + +- **Competitor Mention Tracker**: Keep an eye on the mention of competitors or specific technologies. Create a workflow that searches comments and stories for predefined keywords and compiles them into a report delivered to your email. This can help with market analysis and competitive intelligence. + +- **Personal Dashboard Update**: If you are actively participating in Hacker News, you might want a dashboard that tracks your posts’ performance. Set up a workflow to pull data on your stories, such as points and comment counts, and push updates to a Google Sheet. This provides a personal analytics platform to track and analyze your engagement. + ![pipedream](https://i.ibb.co/LPhXtH1/logo.png)

diff --git a/components/hackerone/README.md b/components/hackerone/README.md new file mode 100644 index 0000000000000..e282add7ad07d --- /dev/null +++ b/components/hackerone/README.md @@ -0,0 +1,11 @@ +# Overview + +The HackerOne API enables automated interactions with the HackerOne platform, facilitating the management of vulnerability coordination and bug bounty programs. Through this API, users can programmatically access their reports, manage vulnerabilities, and streamline communications with researchers. When used within Pipedream, you can leverage these functionalities to automate workflows, integrate with other services like Slack, GitHub, and Jira, and enhance your security operations by ensuring timely responses and updates. + +# Example Use Cases + +- **Automated Vulnerability Alert System**: Create a workflow in Pipedream that triggers whenever a new vulnerability report is submitted via HackerOne. This workflow can parse the report details and automatically send alerts to a Slack channel, ensuring that your security team is immediately informed about new potential threats. + +- **Enhanced Issue Tracking Integration**: Set up a Pipedream workflow that automatically creates an issue in GitHub or Jira when a new HackerOne report reaches a certain severity level. This helps in prioritizing and tracking the resolution of high-impact vulnerabilities without manual intervention, streamlining your security response process. + +- **Scheduled Report Summaries**: Develop a workflow in Pipedream that runs on a scheduled basis (e.g., weekly or monthly) to fetch recent vulnerability reports from HackerOne. It can then compile these into a summary report and email it to your security team or stakeholders. This ensures regular updates on the security landscape and keeps everyone informed about the status of reported vulnerabilities. diff --git a/components/hackerone/actions/create-group/create-group.mjs b/components/hackerone/actions/create-group/create-group.mjs new file mode 100644 index 0000000000000..69b976e0fcd03 --- /dev/null +++ b/components/hackerone/actions/create-group/create-group.mjs @@ -0,0 +1,49 @@ +import app from "../../hackerone.app.mjs"; + +export default { + key: "hackerone-create-group", + name: "Create Group", + description: "Create a new organization group. [See the documentation](https://api.hackerone.com/customer-resources/?javascript#organizations-create-group)", + version: "0.0.1", + type: "action", + props: { + app, + organizationId: { + propDefinition: [ + app, + "organizationId", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + permissions: { + propDefinition: [ + app, + "permissions", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createGroup({ + $, + organizationId: this.organizationId, + data: { + data: { + type: "organization-member-group", + attributes: { + name: this.name, + permissions: this.permissions, + }, + }, + }, + }); + + $.export("$summary", `Successfully created the group '${response.data.attributes.name}'`); + + return response; + }, +}; diff --git a/components/hackerone/actions/create-invitation/create-invitation.mjs b/components/hackerone/actions/create-invitation/create-invitation.mjs new file mode 100644 index 0000000000000..b65d3ec2b699d --- /dev/null +++ b/components/hackerone/actions/create-invitation/create-invitation.mjs @@ -0,0 +1,49 @@ +import app from "../../hackerone.app.mjs"; + +export default { + key: "hackerone-create-invitation", + name: "Create Invitation", + description: "Invite a recipient to an organization using their email address. [See the documentation](https://api.hackerone.com/customer-resources/?shell#organizations-create-an-invitation)", + version: "0.0.1", + type: "action", + props: { + app, + organizationId: { + propDefinition: [ + app, + "organizationId", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + organizationAdmin: { + propDefinition: [ + app, + "organizationAdmin", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createInvitation({ + $, + organizationId: this.organizationId, + data: { + data: { + type: "invitation-organization-member", + attributes: { + email: this.email, + organization_admin: this.organizationAdmin, + }, + }, + }, + }); + + $.export("$summary", `Successfully sent invitation to '${this.email}'`); + + return response; + }, +}; diff --git a/components/hackerone/actions/get-members/get-members.mjs b/components/hackerone/actions/get-members/get-members.mjs new file mode 100644 index 0000000000000..d60a980f2e7e6 --- /dev/null +++ b/components/hackerone/actions/get-members/get-members.mjs @@ -0,0 +1,28 @@ +import app from "../../hackerone.app.mjs"; + +export default { + key: "hackerone-get-members", + name: "Get Members", + description: "List all members of an organization. [See the documentation](https://api.hackerone.com/customer-resources/#organizations-get-all-members)", + version: "0.0.1", + type: "action", + props: { + app, + organizationId: { + propDefinition: [ + app, + "organizationId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getMembers({ + $, + organizationId: this.organizationId, + }); + + $.export("$summary", `Successfully retrieved ${response.data.length} members`); + + return response; + }, +}; diff --git a/components/hackerone/common/constants.mjs b/components/hackerone/common/constants.mjs new file mode 100644 index 0000000000000..da691c2802210 --- /dev/null +++ b/components/hackerone/common/constants.mjs @@ -0,0 +1,12 @@ +export default { + PERMISSIONS: [ + "asset_inventory_manager", + "asset_inventory_viewer", + "group_manager", + "program_admin", + "read_only_member", + "report_analyst", + "report_reward_manager", + "user_manager", + ], +}; diff --git a/components/hackerone/hackerone.app.mjs b/components/hackerone/hackerone.app.mjs new file mode 100644 index 0000000000000..fa8beb0d62ea8 --- /dev/null +++ b/components/hackerone/hackerone.app.mjs @@ -0,0 +1,96 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "hackerone", + propDefinitions: { + name: { + type: "string", + label: "Name", + description: "Name of the organization", + }, + email: { + type: "string", + label: "Email", + description: "Email of the invitee", + }, + organizationAdmin: { + type: "boolean", + label: "Organization Admin", + description: "Sets the invitee as an organization admin", + }, + permissions: { + type: "string[]", + label: "Permissions", + description: "The permissions added to the new organization group", + options: constants.PERMISSIONS, + }, + organizationId: { + type: "string", + label: "Organization ID", + description: "ID of the organization", + async options() { + const { data: organizationsIDs } = await this.getOrganizations(); + + return organizationsIDs.map(({ id }) => ({ + value: id, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.hackerone.com/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + auth, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + auth: { + ...auth, + username: `${this.$auth.api_token_identifier}`, + password: `${this.$auth.api_token}`, + }, + }); + }, + async createInvitation({ + organizationId, ...args + }) { + return this._makeRequest({ + method: "post", + path: `/organizations/${organizationId}/invitations`, + ...args, + }); + }, + async createGroup({ + organizationId, ...args + }) { + return this._makeRequest({ + method: "post", + path: `/organizations/${organizationId}/groups`, + ...args, + }); + }, + async getOrganizations(args = {}) { + return this._makeRequest({ + path: "/me/organizations", + ...args, + }); + }, + async getMembers({ + organizationId, ...args + }) { + return this._makeRequest({ + path: `/organizations/${organizationId}/members`, + ...args, + }); + }, + }, +}; diff --git a/components/hackerone/package.json b/components/hackerone/package.json new file mode 100644 index 0000000000000..4c801eae826b0 --- /dev/null +++ b/components/hackerone/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/hackerone", + "version": "0.1.0", + "description": "Pipedream HackerOne Components", + "main": "hackerone.app.mjs", + "keywords": [ + "pipedream", + "hackerone" + ], + "homepage": "https://pipedream.com/apps/hackerone", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/handwrytten/README.md b/components/handwrytten/README.md new file mode 100644 index 0000000000000..370efd26bf485 --- /dev/null +++ b/components/handwrytten/README.md @@ -0,0 +1,11 @@ +# Overview + +The Handwrytten API allows you to automate the sending of handwritten notes to clients, customers, or friends. Integrating this API within Pipedream workflows enables you to create personalized outreach at scale, triggered by various business events such as customer signups, purchases, or feedback. It's an innovative way to add a human touch to your communication strategy through technology. + +# Example Workflows + +- **Customer Appreciation Notes**: After a customer makes a purchase, trigger a Pipedream workflow that uses the Handwrytten API to send a personalized thank you note. This can be paired with an e-commerce platform like Shopify to listen for new orders. + +- **Birthday Wishes Automation**: Connect Handwrytten to a CRM like HubSpot through Pipedream. Whenever a contact's birthday is approaching, the workflow automatically generates and sends a handwritten birthday card, improving customer relations and personalization. + +- **Event Follow-Up Messages**: After an event, use a workflow that integrates Handwrytten with event management apps like Eventbrite. Send out custom handwritten notes to attendees, thanking them for their participation and inviting them to future events. diff --git a/components/hansei/README.md b/components/hansei/README.md new file mode 100644 index 0000000000000..d5c40e1873ff8 --- /dev/null +++ b/components/hansei/README.md @@ -0,0 +1,11 @@ +# Overview + +The Hansei API enables you to extract meaningful insights from reflections and journals using AI. By integrating this API with Pipedream, you can automate the process of analyzing and responding to journal entries, streamlining mental health tracking, and personal growth exercises. Pipedream’s serverless platform offers a robust way to connect Hansei with various other apps, allowing for seamless workflow creation and data manipulation. + +# Example Use Cases + +- **Mental Health Tracker Integration**: Automate the sending of daily reflections from a journaling app to Hansei for analysis. Then, trigger actions based on the emotional sentiment detected, such as sending a summary to a therapist's email via SendGrid or creating a reminder to meditate if stress levels are high. + +- **Personal Growth Dashboard**: Use Hansei to evaluate personal journal entries and push the insights to a Google Sheets or Airtable dashboard. This can track progress over time on personal goals and provide visual feedback on emotional trends or recurring themes in your reflections. + +- **Team Morale Monitoring**: Collect team members' weekly reflection submissions from a platform like Slack, analyze them with Hansei for overall sentiment, and then send a report to the team leader or HR department to gauge team morale and identify areas where support might be needed. diff --git a/components/hansei/actions/get-bot-conversations/get-bot-conversations.mjs b/components/hansei/actions/get-bot-conversations/get-bot-conversations.mjs new file mode 100644 index 0000000000000..59325255f0d55 --- /dev/null +++ b/components/hansei/actions/get-bot-conversations/get-bot-conversations.mjs @@ -0,0 +1,28 @@ +import hansei from "../../hansei.app.mjs"; + +export default { + key: "hansei-get-bot-conversations", + name: "Get Bot Conversations", + description: "Retrieves a list of conversations with the specified Bot in Hansei. [See the documentation](hhttps://developers.hansei.app/operation/operation-getbotconversations)", + version: "0.0.1", + type: "action", + props: { + hansei, + botId: { + propDefinition: [ + hansei, + "botId", + ], + }, + }, + async run({ $ }) { + const response = await this.hansei.listConversations({ + $, + botId: this.botId, + }); + $.export("$summary", `Successfully retrieved ${response.length} Conversation${response.length === 1 + ? "" + : "s"}`); + return response; + }, +}; diff --git a/components/hansei/actions/get-bots/get-bots.mjs b/components/hansei/actions/get-bots/get-bots.mjs new file mode 100644 index 0000000000000..2c04230792d44 --- /dev/null +++ b/components/hansei/actions/get-bots/get-bots.mjs @@ -0,0 +1,21 @@ +import hansei from "../../hansei.app.mjs"; + +export default { + key: "hansei-get-bots", + name: "Get Bots", + description: "Retrieves a list of Bots in Hansei. [See the documentation](https://developers.hansei.app/operation/operation-getbots)", + version: "0.0.1", + type: "action", + props: { + hansei, + }, + async run({ $ }) { + const response = await this.hansei.listBots({ + $, + }); + $.export("$summary", `Successfully retrieved ${response.length} Bot${response.length === 1 + ? "" + : "s"}`); + return response; + }, +}; diff --git a/components/hansei/actions/get-collections/get-collections.mjs b/components/hansei/actions/get-collections/get-collections.mjs new file mode 100644 index 0000000000000..d733f3ba5fe5e --- /dev/null +++ b/components/hansei/actions/get-collections/get-collections.mjs @@ -0,0 +1,21 @@ +import hansei from "../../hansei.app.mjs"; + +export default { + key: "hansei-get-collections", + name: "Get Collections", + description: "Retrieves a list of Collections in Hansei. [See the documentation](https://developers.hansei.app/operation/operation-getcollections)", + version: "0.0.1", + type: "action", + props: { + hansei, + }, + async run({ $ }) { + const response = await this.hansei.listCollections({ + $, + }); + $.export("$summary", `Successfully retrieved ${response.length} Collection${response.length === 1 + ? "" + : "s"}`); + return response; + }, +}; diff --git a/components/hansei/hansei.app.mjs b/components/hansei/hansei.app.mjs index d6aff20ddc5cd..604f42ee8894c 100644 --- a/components/hansei/hansei.app.mjs +++ b/components/hansei/hansei.app.mjs @@ -1,11 +1,60 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "hansei", - propDefinitions: {}, + propDefinitions: { + botId: { + type: "string", + label: "Bot ID", + description: "Identifier of a bot", + async options() { + const bots = await this.listBots(); + return bots?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.hansei.app/public/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, path, ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "X-API-KEY": this.$auth.api_key, + "Content-Type": "application/json", + }, + }); + }, + listBots(opts = {}) { + return this._makeRequest({ + path: "/bots", + ...opts, + }); + }, + listCollections(opts = {}) { + return this._makeRequest({ + path: "/collections", + ...opts, + }); + }, + listConversations({ + botId, ...opts + }) { + return this._makeRequest({ + path: `/bots/${botId}/conversations`, + ...opts, + }); }, }, }; diff --git a/components/hansei/package.json b/components/hansei/package.json index c0973fd5024a2..cd99cda3e19a7 100644 --- a/components/hansei/package.json +++ b/components/hansei/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/hansei", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Hansei Components", "main": "hansei.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" } -} \ No newline at end of file +} diff --git a/components/happy_scribe/README.md b/components/happy_scribe/README.md new file mode 100644 index 0000000000000..93ae19176d389 --- /dev/null +++ b/components/happy_scribe/README.md @@ -0,0 +1,11 @@ +# Overview + +The Happy Scribe API provides tools for converting speech to text and vice versa, enabling developers to automate the transcription and subtitling of audio and video files. With the Happy Scribe integration on Pipedream, you can create workflows that trigger on various events, process content, and connect to other apps. Automate transcription jobs, sync transcripts to databases, notify teams when transcriptions are ready, or even analyze text for insights. + +# Example Use Cases + +- **Automated Podcast Transcription**: When a new podcast episode is uploaded to cloud storage (like S3 or Google Drive), trigger a Pipedream workflow that submits the audio file to Happy Scribe for transcription. Once the transcript is ready, the workflow can store it in a database and notify the team via Slack. + +- **Video Subtitle Generation and Publishing**: For new videos added to a CMS or YouTube channel, trigger a Pipedream workflow to request subtitles from Happy Scribe. Then, the subtitles can be reviewed or automatically published as part of the video's metadata, enhancing SEO and accessibility. + +- **Sentiment Analysis on Transcribed Meetings**: After a Zoom meeting ends, a Pipedream workflow can send the recording to Happy Scribe for transcription. Another action could then pass the transcript to a sentiment analysis service like MonkeyLearn to gauge the overall tone of the meeting, which could be valuable for HR and team management. diff --git a/components/happyfox_chat/README.md b/components/happyfox_chat/README.md index 78ef8a9f2e89a..a3a429a00f26a 100644 --- a/components/happyfox_chat/README.md +++ b/components/happyfox_chat/README.md @@ -1,17 +1,11 @@ # Overview -With HappyFox Chat API, you can easily build a powerful chat application for -your website or blog. The API allows you to easily add chat functionality to -your site, and provides a number of features to make your chat experience more -powerful and engaging. +The HappyFox Chat API equips you with the capability to enhance customer support interactions by automating chat-related tasks, extracting chat data for analysis, and connecting chat events with other business tools. By leveraging this API with Pipedream's serverless platform, you can create powerful workflows that respond to chat events in real-time, synchronize chat data across platforms, and augment your support system with advanced features like dynamic responses or customer satisfaction analysis. -Here are some examples of what you can build with HappyFox Chat API: +# Example Use Cases -- A customer support chat application, to provide live support to your - customers -- A live chat application for your website or blog, to engage with your - visitors in real-time -- A chat application for your online business, to engage with your customers - and prospects -- A private chat application for your team or organization, to improve - communication and collaboration +- **Automated Customer Support Ticket Creation**: When a chat ends with an unresolved issue, automatically create a support ticket in a tool like Zendesk or JIRA. Extract customer details and chat transcripts from HappyFox Chat and use them to populate the ticket, ensuring a seamless transition from chat to in-depth support. + +- **Real-Time Chat Analysis and Alerts**: Analyze chat messages in real-time to detect keywords or phrases that indicate a high-priority issue or a sales opportunity. Use this analysis to trigger alerts in Slack or Microsoft Teams, directing your team's attention where it's needed most, or to even initiate follow-up actions like sending discount codes or additional resources to the customer. + +- **Post-Chat Feedback Collection**: After a chat session concludes, trigger an automated email or SMS to the customer asking for feedback. Connect HappyFox Chat with email platforms like SendGrid or SMS services like Twilio to gather customer satisfaction data, which you can then store in a database or spreadsheet for review and action. diff --git a/components/happyfox_chat/package.json b/components/happyfox_chat/package.json new file mode 100644 index 0000000000000..ebb9bf5c64a22 --- /dev/null +++ b/components/happyfox_chat/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/happyfox_chat", + "version": "0.6.0", + "description": "Pipedream happyfox_chat Components", + "main": "happyfox_chat.app.mjs", + "keywords": [ + "pipedream", + "happyfox_chat" + ], + "homepage": "https://pipedream.com/apps/happyfox_chat", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/harmonic/README.md b/components/harmonic/README.md new file mode 100644 index 0000000000000..69a7b80a247a4 --- /dev/null +++ b/components/harmonic/README.md @@ -0,0 +1,11 @@ +# Overview + +The Harmonic API offers machine learning-driven insights for sales and customer interactions. By analyzing communication data, it can provide actionable insights, such as customer sentiment, engagement, and overall sales performance. When leveraged within Pipedream, the Harmonic API enables the automation of workflows that can enhance CRM activities, tailor customer outreach, and streamline sales processes. With Pipedream's capacity for integrating various apps, the data processed by the Harmonic API can trigger actions across multiple platforms, enriching customer profiles, automating alerts, and synchronizing with marketing efforts. + +# Example Use Cases + +- **Sales Engagement Scoring**: Analyze customer interactions from emails or calls and use Harmonic sentiment scores to prioritize sales engagements. Integrate with a CRM like Salesforce to update contact records with engagement scores, ensuring high-potential leads are addressed promptly. + +- **Automated Support Tickets Creation**: Monitor customer feedback through support channels, and use Harmonic's sentiment analysis to automatically generate support tickets in a tool like Zendesk when negative sentiment is detected. Prioritize and route tickets based on the severity of the sentiment. + +- **Marketing Campaign Adjustment**: Examine customer responses to marketing emails by using Harmonic to gauge the sentiment and engagement level. Connect this data to a marketing platform like Mailchimp to segment audiences and tailor future campaigns based on the insights received. diff --git a/components/harmonic/harmonic.app.mjs b/components/harmonic/harmonic.app.mjs new file mode 100644 index 0000000000000..d65e7590fd4dc --- /dev/null +++ b/components/harmonic/harmonic.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "harmonic", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/harmonic/package.json b/components/harmonic/package.json new file mode 100644 index 0000000000000..34cac87f8923e --- /dev/null +++ b/components/harmonic/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/harmonic", + "version": "0.0.1", + "description": "Pipedream Harmonic Components", + "main": "harmonic.app.mjs", + "keywords": [ + "pipedream", + "harmonic" + ], + "homepage": "https://pipedream.com/apps/harmonic", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/harry_potter_api/README.md b/components/harry_potter_api/README.md index 5e54ba42178f1..7e8fb0447801f 100644 --- a/components/harry_potter_api/README.md +++ b/components/harry_potter_api/README.md @@ -1,9 +1,11 @@ # Overview -With the Harry Potter API, you can build applications that: +The Harry Potter API offers a magical touch to any developer's toolkit, allowing you to conjure data about characters, spells, houses, and more from the Harry Potter universe. With Pipedream, you can automate enchanting workflows that leverage this data, creating applications that respond to trivia, organize Harry Potter-themed events, or integrate with chatbots for interactive storytelling. Let's explore the castle's hidden corridors where APIs meet serverless wizardry to automate the magical. -- Fetch information about all of the characters in the Harry Potter series -- Fetch information about specific characters -- Fetch information about the houses in Harry Potter -- Fetch information about the members of each house -- Fetch information about the various spells in Harry Potter +# Example Use Cases + +- **Sorting Hat Quiz Automation**: Streamline the creation of a Sorting Hat quiz by triggering a workflow with form submissions from your website. Use the Harry Potter API to fetch questions and sort users into Hogwarts houses. Results could be emailed automatically or posted to a Slack channel for community engagement. + +- **Daily Prophet Newsletter**: Design a daily or weekly email blast that sends subscribers interesting facts or quotes from the Harry Potter universe. The workflow can use the Harry Potter API to pull random trivia and format it into an email template, then send it through an email service like SendGrid. + +- **Spell Incantation Training App**: Build an interactive training module for aspiring witches and wizards. When a user speaks a spell into a voice recognition system like Google's Dialogflow, trigger a Pipedream workflow that checks the spell's accuracy using the Harry Potter API and returns feedback, perhaps even keeping score in a Google Sheet. diff --git a/components/harry_potter_api/package.json b/components/harry_potter_api/package.json new file mode 100644 index 0000000000000..ac268ca2c9e3c --- /dev/null +++ b/components/harry_potter_api/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/harry_potter_api", + "version": "0.6.0", + "description": "Pipedream harry_potter_api Components", + "main": "harry_potter_api.app.mjs", + "keywords": [ + "pipedream", + "harry_potter_api" + ], + "homepage": "https://pipedream.com/apps/harry_potter_api", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/harvest/README.md b/components/harvest/README.md index 953eac11e086f..6df8419d3a695 100644 --- a/components/harvest/README.md +++ b/components/harvest/README.md @@ -1,16 +1,11 @@ # Overview -The Harvest API allows developers to programmatically access data and objects -in Harvest, a web-based time tracking application. With the API, developers can -create applications that submit and retrieve time tracking data, as well as -automate various aspects of the Harvest user experience. +Harvest is a time tracking and invoicing tool that can streamline the way freelancers and businesses record time for various projects and tasks. By leveraging the Harvest API on Pipedream, you can automate complex workflows that integrate time tracking data with other business tools. Generate reports, sync project data, and manage invoices with minimal manual intervention. Pipedream's serverless platform lets you create these automations using simple, code-driven components, enabling a seamless connection between Harvest and a multitude of other apps. -Some examples of what you can build using the Harvest API include: +# Example Use Cases -- A time tracking application that automatically submits time tracking data to - Harvest on behalf of the user -- A reporting application that retrieves time tracking data from Harvest and - presents it in various charts and graphs -- An integration with a third-party project management application that pulls - in time tracking data from Harvest to provide a more complete picture of - project progress +- **Automated Invoicing Workflow**: Create an automation that triggers at the end of each billing cycle. It compiles timesheet data from Harvest and generates invoices automatically. Then, it sends these invoices to clients via email using a service like SendGrid or directly through accounting software like QuickBooks. + +- **Slack Time Tracking Reminders**: Set up a Pipedream workflow that sends reminders to a Slack channel or directly to team members. Reminders prompt users to submit their timesheets if they haven't been completed by a certain time each day or week. The Harvest API checks for unsubmitted timesheets and triggers the Slack notifications accordingly. + +- **Project Management Sync**: Build a workflow that synchronizes project and task updates between Harvest and a project management tool such as Trello or Asana. When a new project is created or a task is updated in Harvest, the corresponding card or task in the project management app is created or updated, keeping both systems in sync and up-to-date. diff --git a/components/hashnode/README.md b/components/hashnode/README.md new file mode 100644 index 0000000000000..61b7c9075064b --- /dev/null +++ b/components/hashnode/README.md @@ -0,0 +1,11 @@ +# Overview + +The Hashnode API enables developers to programmatically interact with the Hashnode blogging platform. Using the API on Pipedream, you can automate content publication, data syncing, and notification workflows. This allows you to streamline your blogging process, engage with your audience more effectively, and integrate with other services to enrich your Hashnode presence. + +# Example Use Cases + +- **Automated Blog Post Publication**: Schedule and automate the posting of articles on your Hashnode blog. This can be done by setting up a workflow that triggers on a specific schedule, fetches content from a data source like Google Docs or Markdown files, and publishes it to your Hashnode blog using the API. + +- **Sync Hashnode Interactions to a CRM**: Track interactions on your Hashnode blog, such as comments and reactions, and sync them to a Customer Relationship Management (CRM) system. By doing this, you can create a more personalized experience for your readers and maintain an organized record of your audience's engagement. + +- **Distribute Content Across Social Platforms**: When a new article is published on Hashnode, use Pipedream to automatically share it to social networks like Twitter, LinkedIn, or Facebook. This workflow can amplify your content's reach by posting updates to your social media profiles, ensuring your latest posts get the visibility they deserve. diff --git a/components/hashnode/hashnode.app.mjs b/components/hashnode/hashnode.app.mjs new file mode 100644 index 0000000000000..99cd80a8fb602 --- /dev/null +++ b/components/hashnode/hashnode.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "hashnode", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/hashnode/package.json b/components/hashnode/package.json new file mode 100644 index 0000000000000..71b6c2e7fe33c --- /dev/null +++ b/components/hashnode/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/hashnode", + "version": "0.0.1", + "description": "Pipedream Hashnode Components", + "main": "hashnode.app.mjs", + "keywords": [ + "pipedream", + "hashnode" + ], + "homepage": "https://pipedream.com/apps/hashnode", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/hasura/README.md b/components/hasura/README.md new file mode 100644 index 0000000000000..7c7b188ce096e --- /dev/null +++ b/components/hasura/README.md @@ -0,0 +1,11 @@ +# Overview + +The Hasura API offers a powerful way to interact with your Hasura backend using GraphQL to build, query, and mutate your data. On Pipedream, you can leverage this API to create dynamic workflows that react to events in your Hasura instance, automate CRUD operations, and integrate with other services. By using Hasura with Pipedream, you gain the ability to automate tasks, sync data across apps, and respond in real-time to changes in your database. + +# Example Use Cases + +- **Automate Data Entry**: Sync data from a Google Sheet to your Hasura database. Every time a new row is added in Google Sheets, a Pipedream workflow triggers a GraphQL mutation to insert the data into a specified table in Hasura. + +- **Real-time Slack Notifications**: Send a Slack message to a channel whenever a new record is inserted in a Hasura table. This Pipedream workflow listens to Hasura events and uses a Slack step to notify your team in real-time about new entries or changes. + +- **Sync Users with Mailchimp**: Automatically add new users from your Hasura user table to a Mailchimp audience. When a new user is created in Hasura, trigger a Pipedream workflow to add that user's email to Mailchimp, helping to keep your marketing efforts in sync with your user base. diff --git a/components/hasura/hasura.app.mjs b/components/hasura/hasura.app.mjs new file mode 100644 index 0000000000000..3c30abd4e3b7c --- /dev/null +++ b/components/hasura/hasura.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "hasura", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/hasura/package.json b/components/hasura/package.json new file mode 100644 index 0000000000000..d638c6296edc8 --- /dev/null +++ b/components/hasura/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/hasura", + "version": "0.0.1", + "description": "Pipedream Hasura Components", + "main": "hasura.app.mjs", + "keywords": [ + "pipedream", + "hasura" + ], + "homepage": "https://pipedream.com/apps/hasura", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/have_i_been_pwned/have_i_been_pwned.app.mjs b/components/have_i_been_pwned/have_i_been_pwned.app.mjs new file mode 100644 index 0000000000000..b9f8b515d3dc8 --- /dev/null +++ b/components/have_i_been_pwned/have_i_been_pwned.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "have_i_been_pwned", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/have_i_been_pwned/package.json b/components/have_i_been_pwned/package.json new file mode 100644 index 0000000000000..3a19ed14cd872 --- /dev/null +++ b/components/have_i_been_pwned/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/have_i_been_pwned", + "version": "0.0.1", + "description": "Pipedream Have I Been Pwned Components", + "main": "have_i_been_pwned.app.mjs", + "keywords": [ + "pipedream", + "have_i_been_pwned" + ], + "homepage": "https://pipedream.com/apps/have_i_been_pwned", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/headless_testing/README.md b/components/headless_testing/README.md index c811e9a1c0655..cf70eef5da3e9 100644 --- a/components/headless_testing/README.md +++ b/components/headless_testing/README.md @@ -1,12 +1,11 @@ # Overview -The Headless Testing API allows you to build and test web applications without -a web browser. This is useful for testing web applications that are not -compatible with web browsers, or for testing web applications that are only -accessible through a web browser. +The Headless Testing API unlocks the power of automated browser tasks, enabling developers to run end-to-end tests in a headless Chrome environment. This service is crucial for continuous integration pipelines, allowing for the execution of scripts that verify the functionality and performance of web applications without the need for a graphical user interface. With Pipedream, you can connect the Headless Testing API to a myriad of other services, triggering automated tests based on specific events, storing results, and even notifying team members about test outcomes. -Here are some examples of what you can build with the Headless Testing API: +# Example Use Cases -- A web application that is compatible with web browsers -- A web application that is only accessible through a web browser -- A web application that is not compatible with web browsers +- **Continuous Deployment Testing**: Automate browser tests with Headless Testing API every time a new commit is pushed to your GitHub repository. Use Pipedream's GitHub trigger to start tests and store test results in a Google Sheet for easy tracking and historical analysis. + +- **Scheduled Regression Testing**: Set up a daily or weekly scheduled workflow on Pipedream to run regression tests through Headless Testing API. Aggregate the test results and send a summary report via email using SendGrid, Slack, or another notification service to keep your team informed. + +- **Real-Time Alerting**: Combine the Headless Testing API with a monitoring tool like UptimeRobot on Pipedream. Trigger tests when your monitoring tool detects a website downtime or performance issue, then parse the results and escalate alerts through communication platforms like Twilio or Discord if critical failures are found. diff --git a/components/headless_testing/package.json b/components/headless_testing/package.json new file mode 100644 index 0000000000000..09772d3be2805 --- /dev/null +++ b/components/headless_testing/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/headless_testing", + "version": "0.6.0", + "description": "Pipedream headless_testing Components", + "main": "headless_testing.app.mjs", + "keywords": [ + "pipedream", + "headless_testing" + ], + "homepage": "https://pipedream.com/apps/headless_testing", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/heap/README.md b/components/heap/README.md index fe9b7e220476a..1a2cae3b03484 100644 --- a/components/heap/README.md +++ b/components/heap/README.md @@ -1,11 +1,11 @@ # Overview -Heap is an analytics tool that allows developers to track user engagement and -conversions within their web or mobile app. Heap provides an API that -developers can use to build custom integrations, dashboards, and reports. +The Heap API enables you to automate and integrate your user analytics data with other applications. With Heap, you can extract insights on how users interact with your product, track events without code, and funnel this data into your CRM, marketing tools, or custom dashboards. It's about connecting the dots between user actions and your strategic moves. Heap's API lets you push or pull data, so you're always up-to-date on user behavior and can personalize user experiences at scale. -Some examples of what you can build using the Heap API include: +# Example Use Cases -- A dashboard to track app usage and engagement metrics -- A report to track conversion rates and revenue -- A custom integration to track user behavior in your app +- **Sync User Data to CRM**: Automatically push user engagement metrics from Heap to your Customer Relationship Management (CRM) system. Keep sales informed with the latest user activity for better lead scoring and personalized follow-ups. + +- **Trigger Email Campaigns Based on User Behavior**: When Heap identifies a significant user action, like reaching a milestone in your app, use that signal to trigger an automated email campaign in a tool like SendGrid. Capitalize on user engagement at its peak to increase conversion rates. + +- **Create Custom Dashboards with Google Sheets**: Pull data from Heap into Google Sheets to build custom dashboards. Use this live data to make informed decisions and share analytics with stakeholders who may not have direct access to Heap or prefer data in a spreadsheet format. diff --git a/components/heap/package.json b/components/heap/package.json new file mode 100644 index 0000000000000..cd41b841f0ec6 --- /dev/null +++ b/components/heap/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/heap", + "version": "0.6.0", + "description": "Pipedream heap Components", + "main": "heap.app.mjs", + "keywords": [ + "pipedream", + "heap" + ], + "homepage": "https://pipedream.com/apps/heap", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/heartbeat/README.md b/components/heartbeat/README.md index 9019bbd4a855e..eea461365d096 100644 --- a/components/heartbeat/README.md +++ b/components/heartbeat/README.md @@ -1,9 +1,11 @@ # Overview -The heartbeat API enables you to build a variety of applications that can send -and receive real-time messages. Here are some examples of what you can build: +The Heartbeat API offers a powerful way to connect and automate your community engagement. With this API, you can monitor community signals, manage members, and trigger events based on community interactions. By integrating Heartbeat with Pipedream, you can create dynamic, serverless workflows that act upon these signals and bring efficiency to your community management tasks. Whether it's automating welcome messages, syncing member data with other platforms, or analyzing community health, the possibilities are vast. -- A chat application for customer support -- A internal chat application for your company -- A messaging application for your website or blog -- An online gaming application +# Example Use Cases + +- **Community Health Dashboard**: Sync Heartbeat community interaction data to a Google Sheets document using Pipedream. Analyze key metrics to build a health dashboard that updates in real-time, giving you instant insights into member engagement levels and overall community vibrancy. + +- **Automated Member Management**: Set up a workflow that listens for new members joining in Heartbeat and automatically adds them to a CRM like Salesforce. This can help you keep track of your growing community and streamline member onboarding processes, ensuring no one falls through the cracks. + +- **Real-time Alerts**: Create a Pipedream workflow that sends real-time notifications to a Slack channel when a member posts a question or needs support in Heartbeat. This ensures prompt responses from your team, boosting engagement and member satisfaction. diff --git a/components/heedjy/README.md b/components/heedjy/README.md new file mode 100644 index 0000000000000..94b78a5fc771c --- /dev/null +++ b/components/heedjy/README.md @@ -0,0 +1,11 @@ +# Overview + +The Heedjy API provides functionalities to understand and predict user behavior on digital platforms. With this API integrated into Pipedream, you can automate processes that depend on user data analytics, predict churn, personalize content, or optimize user experiences in real-time. Pipedream's serverless platform allows you to create intricate workflows that can respond to Heedjy's insights, enabling applications to adapt and engage with users more effectively. + +# Example Use Cases + +- **User Engagement Tracking**: Build a workflow on Pipedream that listens for user engagement events from your app, sends them to Heedjy for analysis, and stores the insights in a Google Sheets document for easy review and action planning. + +- **Churn Prediction Response**: Set up a Pipedream workflow that utilizes Heedjy's churn prediction capabilities to flag at-risk users, then automatically triggers an email campaign through SendGrid to re-engage them with personalized offers or messages. + +- **Content Personalization Engine**: Craft a Pipedream workflow that pulls content performance data from your CMS, analyzes it with Heedjy for engagement patterns, and uses the results to tailor future content recommendations pushed via a Slack bot to your editorial team. diff --git a/components/height/actions/create-task/create-task.mjs b/components/height/actions/create-task/create-task.mjs new file mode 100644 index 0000000000000..5c2c962a6e08d --- /dev/null +++ b/components/height/actions/create-task/create-task.mjs @@ -0,0 +1,66 @@ +import height from "../../height.app.mjs"; + +export default { + key: "height-create-task", + name: "Create Task", + description: "Creates a new task within your workspace. [See the documentation](https://height.notion.site/Create-a-task-b50565736830422684b28ae570a53a9e)", + version: "0.0.1", + type: "action", + props: { + height, + taskName: { + propDefinition: [ + height, + "taskName", + ], + }, + listIds: { + propDefinition: [ + height, + "listIds", + ], + }, + description: { + propDefinition: [ + height, + "description", + ], + }, + status: { + propDefinition: [ + height, + "status", + ], + }, + assigneeIds: { + propDefinition: [ + height, + "assigneeIds", + ], + }, + parentTaskId: { + propDefinition: [ + height, + "taskId", + ], + label: "Parent Task ID", + description: "The task ID of the parent task", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.height.createTask({ + $, + data: { + name: this.taskName, + listIds: this.listIds, + description: this.description, + status: this.status, + assigneesIds: this.assigneeIds, + parentTaskId: this.parentTaskId, + }, + }); + $.export("$summary", `Successfully created task ${this.taskName}`); + return response; + }, +}; diff --git a/components/height/actions/search-tasks/search-tasks.mjs b/components/height/actions/search-tasks/search-tasks.mjs new file mode 100644 index 0000000000000..71d6f7b3a4cf8 --- /dev/null +++ b/components/height/actions/search-tasks/search-tasks.mjs @@ -0,0 +1,38 @@ +import height from "../../height.app.mjs"; + +export default { + key: "height-search-tasks", + name: "Search Tasks", + description: "Searches for tasks within your workspace using a text query. [See the documentation](https://height.notion.site/Search-tasks-bb201e3db042442e9a1d0686a7b271a2)", + version: "0.0.1", + type: "action", + props: { + height, + query: { + type: "string", + label: "Query", + description: "Full text search on the task", + }, + }, + async run({ $ }) { + const { list } = await this.height.searchTasks({ + $, + params: { + query: this.query, + filters: { + status: { + values: [ + "backLog", + "inProgress", + "done", + ], + }, + }, + }, + }); + $.export("$summary", `Found ${list.length} task${list.length === 1 + ? "" + : "s"} matching the query "${this.query}"`); + return list; + }, +}; diff --git a/components/height/actions/update-task/update-task.mjs b/components/height/actions/update-task/update-task.mjs new file mode 100644 index 0000000000000..aa565df72e644 --- /dev/null +++ b/components/height/actions/update-task/update-task.mjs @@ -0,0 +1,165 @@ +import height from "../../height.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "height-update-task", + name: "Update Task", + description: "Updates a specified task within your workspace. [See the documentation](https://height.notion.site/Update-tasks-53d72cb0059a4e0e81cc2fcbfcbf9d0a)", + version: "0.0.1", + type: "action", + props: { + height, + taskId: { + propDefinition: [ + height, + "taskId", + ], + }, + taskName: { + propDefinition: [ + height, + "taskName", + ], + optional: true, + }, + description: { + propDefinition: [ + height, + "description", + ], + }, + listIdsToAdd: { + propDefinition: [ + height, + "listIds", + (c) => ({ + taskId: c.taskId, + excludeTaskLists: true, + }), + ], + optional: true, + label: "Lists to Add", + description: "Lists to add the task to", + }, + listIdsToRemove: { + propDefinition: [ + height, + "listIds", + (c) => ({ + taskId: c.taskId, + }), + ], + optional: true, + label: "Lists to Remove", + description: "Lists to remove the task from", + }, + assigneeIdsToAdd: { + propDefinition: [ + height, + "assigneeIds", + (c) => ({ + taskId: c.taskId, + excludeTaskAssignees: true, + }), + ], + label: "Assignees to Add", + description: "The assignees to add to this task", + }, + assigneeIdsToRemove: { + propDefinition: [ + height, + "assigneeIds", + (c) => ({ + taskId: c.taskId, + }), + ], + label: "Assignees to Remove", + description: "The assignees to remove from this task", + }, + parentTaskId: { + propDefinition: [ + height, + "taskId", + ], + label: "Parent Task ID", + description: "The task ID of the parent task", + optional: true, + }, + }, + async run({ $ }) { + if (!this.taskName + && !this.description + && !this.listIdsToAdd + && !this.listIdsToRemove + && !this.assigneeIdsToAdd + && !this.assigneeIdsToRemove + && !this.parentTaskId + ) { + throw new ConfigurationError("Please specify at least one field to update"); + } + + const effects = []; + if (this.taskName) { + effects.push({ + type: "name", + name: this.taskName, + }); + } + if (this.description) { + effects.push({ + type: "description", + description: { + message: this.description, + }, + }); + } + if (this.listIdsToAdd || this.listIdsToRemove) { + effects.push({ + type: "lists", + listIds: { + add: this.listIdsToAdd, + remove: this.listIdsToRemove, + }, + }); + } + if (this.assigneeIdsToAdd || this.assigneeIdsToRemove) { + effects.push({ + type: "assignees", + assigneeIds: { + add: this.assigneeIdsToAdd, + remove: this.assigneeIdsToRemove, + }, + }); + } + if (this.parentTaskId) { + effects.push({ + type: "parentTask", + parentTaskId: this.parentTaskId, + }); + } + let response; + try { + response = await this.height.updateTask({ + $, + data: { + patches: [ + { + taskIds: [ + this.taskId, + ], + effects, + }, + ], + }, + }); + } catch (e) { + const messageText = JSON.parse(e.message).error.message; + if (messageText === "listIds can't be empty.") { + throw new Error("Task must belong to at least one list"); + } + throw new Error(messageText); + } + $.export("$summary", `Task ${this.taskId} updated successfully`); + return response; + }, +}; diff --git a/components/height/height.app.mjs b/components/height/height.app.mjs new file mode 100644 index 0000000000000..419c25238f513 --- /dev/null +++ b/components/height/height.app.mjs @@ -0,0 +1,167 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "height", + propDefinitions: { + listIds: { + type: "string[]", + label: "List IDs", + description: "The list(s) to which the task belongs", + async options({ + taskId, excludeTaskLists = false, + }) { + let { list } = await this.listLists(); + if (taskId) { + const { listIds } = await this.getTask({ + taskId, + }); + list = excludeTaskLists + ? list.filter(({ id }) => !listIds.includes(id)) + : list.filter(({ id }) => listIds.includes(id)); + } + list = list.filter(({ type }) => type === "list" || type === "projects"); + return list?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + assigneeIds: { + type: "string[]", + label: "Assignee IDs", + description: "The IDs of the users to assign to the task", + optional: true, + async options({ + taskId, excludeTaskAssignees = false, + }) { + let { list } = await this.listUsers(); + if (taskId) { + const { assigneesIds } = await this.getTask({ + taskId, + }); + list = excludeTaskAssignees + ? list.filter(({ id }) => !assigneesIds.includes(id)) + : list.filter(({ id }) => assigneesIds.includes(id)); + } + return list?.map(({ + id: value, username: label, + }) => ({ + value, + label, + })) || []; + }, + }, + taskId: { + type: "string", + label: "Task ID", + description: "The ID of the task", + async options() { + const { list } = await this.searchTasks({ + params: { + filters: { + status: { + values: [ + "backLog", + "inProgress", + "done", + ], + }, + }, + }, + }); + return list?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + taskName: { + type: "string", + label: "Task Name", + description: "The name of the task", + }, + description: { + type: "string", + label: "Description", + description: "The description of the task", + optional: true, + }, + status: { + type: "string", + label: "Status", + description: "The status of the task", + optional: true, + options: [ + "backLog", + "inProgress", + "done", + ], + }, + }, + methods: { + _baseUrl() { + return "https://api.height.app"; + }, + _headers() { + return { + "Authorization": `api-key ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._headers(), + ...opts, + }); + }, + getTask({ + taskId, ...opts + }) { + return this._makeRequest({ + path: `/tasks/${taskId}`, + ...opts, + }); + }, + listLists(opts = {}) { + return this._makeRequest({ + path: "/lists", + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users", + ...opts, + }); + }, + searchTasks(opts = {}) { + return this._makeRequest({ + path: "/tasks", + ...opts, + }); + }, + createTask(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/tasks", + ...opts, + }); + }, + updateTask(opts = {}) { + return this._makeRequest({ + method: "PATCH", + path: "/tasks", + ...opts, + }); + }, + }, +}; diff --git a/components/height/package.json b/components/height/package.json new file mode 100644 index 0000000000000..9838e0725457d --- /dev/null +++ b/components/height/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/height", + "version": "0.1.0", + "description": "Pipedream Height Components", + "main": "height.app.mjs", + "keywords": [ + "pipedream", + "height" + ], + "homepage": "https://pipedream.com/apps/height", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} diff --git a/components/height/sources/common/base.mjs b/components/height/sources/common/base.mjs new file mode 100644 index 0000000000000..06f51ea34d5cf --- /dev/null +++ b/components/height/sources/common/base.mjs @@ -0,0 +1,31 @@ +import height from "../../height.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + height, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || "1970-01-01T00:00:01Z"; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/height/sources/common/tasks.mjs b/components/height/sources/common/tasks.mjs new file mode 100644 index 0000000000000..bdfc3180b006f --- /dev/null +++ b/components/height/sources/common/tasks.mjs @@ -0,0 +1,51 @@ +import common from "./base.mjs"; + +export default { + ...common, + methods: { + ...common.methods, + getTsField() { + throw new Error("getTsField is not implemented"); + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + getSearchFilters(lastTs, tsField) { + const filters = {}; + filters[tsField] = { + values: [], + gt: { + date: lastTs, + }, + }; + return filters; + }, + generateMeta(task, tsField) { + const ts = Date.parse(task[tsField]); + return { + id: `${task.id}${ts}`, + summary: this.getSummary(task), + ts, + }; + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + const tsField = this.getTsField(); + const filters = this.getSearchFilters(lastTs, tsField); + const { list } = await this.height.searchTasks({ + params: { + filters, + limit: max, + }, + }); + for (const task of list.reverse()) { + this.$emit(task, this.generateMeta(task, tsField)); + maxTs = Date.parse(task[tsField]) > Date.parse(maxTs) + ? task[tsField] + : maxTs; + } + this._setLastTs(maxTs); + }, + }, +}; diff --git a/components/height/sources/new-completed-task/new-completed-task.mjs b/components/height/sources/new-completed-task/new-completed-task.mjs new file mode 100644 index 0000000000000..bc162a5b64e0c --- /dev/null +++ b/components/height/sources/new-completed-task/new-completed-task.mjs @@ -0,0 +1,22 @@ +import common from "../common/tasks.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "height-new-completed-task", + name: "New Completed Task", + description: "Emit new event when a task is marked as complete.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTsField() { + return "completedAt"; + }, + getSummary(task) { + return `Task Completed: ${task.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/height/sources/new-completed-task/test-event.mjs b/components/height/sources/new-completed-task/test-event.mjs new file mode 100644 index 0000000000000..277246115d5f8 --- /dev/null +++ b/components/height/sources/new-completed-task/test-event.mjs @@ -0,0 +1,53 @@ +export default { + "id": "ef519e11-a85c-4573-8df3-67f33180905f", + "type": "project", + "appearance": null, + "model": "task", + "createdAt": "2024-06-06T21:23:48.648Z", + "createdUserId": "0a57c078-683f-4fb7-aefb-e83242215d8b", + "commentedByUserIds": [], + "deleted": false, + "deletedAt": null, + "deletedByUserId": null, + "name": "task", + "nameRichText": "

task

", + "nameType": "markdown", + "description": "", + "descriptionRichText": "", + "descriptionType": "markdown", + "index": 16, + "assigneesIds": [], + "statusSetId": null, + "status": "done", + "parentTaskId": null, + "teamIds": [], + "listIds": [ + "d9e63325-46af-42d9-a6a9-bccfeee041a7", + "7b51e4c6-f63a-4967-bddf-499debc17a34" + ], + "commentsAggregateCount": 0, + "lastActivityAt": "2024-06-07T15:19:05.728Z", + "completed": true, + "completedAt": "2024-06-07T15:19:05.685Z", + "completedByUserId": "3ecdbea0-42a3-4aa7-96dc-87ea925f3585", + "startedAt": "2024-06-07T15:19:05.691Z", + "completedIn": "64517046", + "inProgressFor": "3", + "delayedFor": "64517043", + "orderIndex": 1779884110, + "subtasksIds": [], + "completedSubtasksIds": [], + "fields": [], + "gitBranches": [], + "gitPullRequests": [], + "parentTasks": [], + "subscribersIds": [ + "0a57c078-683f-4fb7-aefb-e83242215d8b" + ], + "redacted": null, + "links": [], + "url": "https://height.app/aCs1znNK1Y/T-16", + "trashedAt": null, + "trashedByUserId": null, + "lastDescriptionActivity": null +} \ No newline at end of file diff --git a/components/height/sources/new-task-added/new-task-added.mjs b/components/height/sources/new-task-added/new-task-added.mjs new file mode 100644 index 0000000000000..ea338e831b6ec --- /dev/null +++ b/components/height/sources/new-task-added/new-task-added.mjs @@ -0,0 +1,22 @@ +import common from "../common/tasks.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "height-new-task-added", + name: "New Task Added", + description: "Emit new event when a new task is added in Height", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTsField() { + return "createdAt"; + }, + getSummary(task) { + return `New Task: ${task.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/height/sources/new-task-added/test-event.mjs b/components/height/sources/new-task-added/test-event.mjs new file mode 100644 index 0000000000000..0b8054ba4387d --- /dev/null +++ b/components/height/sources/new-task-added/test-event.mjs @@ -0,0 +1,52 @@ +export default { + "id": "667c312b-44b5-4405-a06a-5d6d3dbbee2e", + "type": "task", + "appearance": null, + "model": "task", + "createdAt": "2024-06-06T19:48:14.781Z", + "createdUserId": "0a57c078-683f-4fb7-aefb-e83242215d8b", + "commentedByUserIds": [], + "deleted": false, + "deletedAt": null, + "deletedByUserId": null, + "name": "task", + "nameRichText": "

task

", + "nameType": "markdown", + "description": "", + "descriptionRichText": "", + "descriptionType": "markdown", + "index": 12, + "assigneesIds": [], + "statusSetId": null, + "status": "backLog", + "parentTaskId": null, + "teamIds": [], + "listIds": [ + "d9e63325-46af-42d9-a6a9-bccfeee041a7" + ], + "commentsAggregateCount": 0, + "lastActivityAt": "2024-06-06T20:35:14.515Z", + "completed": false, + "completedAt": null, + "completedByUserId": null, + "startedAt": null, + "completedIn": null, + "inProgressFor": null, + "delayedFor": null, + "orderIndex": 1936257377, + "subtasksIds": [], + "completedSubtasksIds": [], + "fields": [], + "gitBranches": [], + "gitPullRequests": [], + "parentTasks": [], + "subscribersIds": [ + "0a57c078-683f-4fb7-aefb-e83242215d8b" + ], + "redacted": null, + "links": [], + "url": "https://height.app/aCs1znNK1Y/T-12", + "trashedAt": null, + "trashedByUserId": null, + "lastDescriptionActivity": null +} \ No newline at end of file diff --git a/components/height/sources/new-updated-task/new-updated-task.mjs b/components/height/sources/new-updated-task/new-updated-task.mjs new file mode 100644 index 0000000000000..1a14b898981e4 --- /dev/null +++ b/components/height/sources/new-updated-task/new-updated-task.mjs @@ -0,0 +1,22 @@ +import common from "../common/tasks.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "height-new-updated-task", + name: "New or Updated Task", + description: "Emit new event whenever a task is created or updated in Height.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTsField() { + return "lastActivityAt"; + }, + getSummary(task) { + return `Task Updated: ${task.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/height/sources/new-updated-task/test-event.mjs b/components/height/sources/new-updated-task/test-event.mjs new file mode 100644 index 0000000000000..0b8054ba4387d --- /dev/null +++ b/components/height/sources/new-updated-task/test-event.mjs @@ -0,0 +1,52 @@ +export default { + "id": "667c312b-44b5-4405-a06a-5d6d3dbbee2e", + "type": "task", + "appearance": null, + "model": "task", + "createdAt": "2024-06-06T19:48:14.781Z", + "createdUserId": "0a57c078-683f-4fb7-aefb-e83242215d8b", + "commentedByUserIds": [], + "deleted": false, + "deletedAt": null, + "deletedByUserId": null, + "name": "task", + "nameRichText": "

task

", + "nameType": "markdown", + "description": "", + "descriptionRichText": "", + "descriptionType": "markdown", + "index": 12, + "assigneesIds": [], + "statusSetId": null, + "status": "backLog", + "parentTaskId": null, + "teamIds": [], + "listIds": [ + "d9e63325-46af-42d9-a6a9-bccfeee041a7" + ], + "commentsAggregateCount": 0, + "lastActivityAt": "2024-06-06T20:35:14.515Z", + "completed": false, + "completedAt": null, + "completedByUserId": null, + "startedAt": null, + "completedIn": null, + "inProgressFor": null, + "delayedFor": null, + "orderIndex": 1936257377, + "subtasksIds": [], + "completedSubtasksIds": [], + "fields": [], + "gitBranches": [], + "gitPullRequests": [], + "parentTasks": [], + "subscribersIds": [ + "0a57c078-683f-4fb7-aefb-e83242215d8b" + ], + "redacted": null, + "links": [], + "url": "https://height.app/aCs1znNK1Y/T-12", + "trashedAt": null, + "trashedByUserId": null, + "lastDescriptionActivity": null +} \ No newline at end of file diff --git a/components/helcim/README.md b/components/helcim/README.md index 6b6fad447faa7..fdba28e94c498 100644 --- a/components/helcim/README.md +++ b/components/helcim/README.md @@ -1,11 +1,11 @@ # Overview -You can use the Helcim API to: - -- Build a payment form to accept credit card payments on your website -- Integrate Helcim Commerce with your existing shopping cart or e-commerce - platform -- Process recurring payments -- Create and manage customer profiles -- Generate reports and access your transaction history -- …and much more! +Helcim is a payment platform that lets you process transactions, manage customers, and handle various aspects of your business's finances. Using Pipedream, you can tap into the Helcim API to create automations that streamline payment processing, sync transaction data with other business tools, and trigger actions based on payment events. It's an ideal way to integrate sales data with other systems, automate accounting tasks, and enhance customer relationship management. + +# Example Use Cases + +- **Real-Time Payment Notifications**: Create a workflow that listens for new transactions via Helcim. Whenever a payment is processed, use Pipedream to send real-time notifications to your team via Slack or email. This keeps everyone in the loop about sales activity without the need for manual checks. + +- **Automated Accounting Integration**: Sync Helcim transactions with accounting software like QuickBooks or Xero. Set up a Pipedream workflow that triggers each time a new sale is recorded in Helcim, automatically creating a corresponding invoice or updating financial records in your accounting system, saving time and reducing errors. + +- **Dynamic Customer Segmentation**: Use Helcim transactions to segment customers based on purchase history. Set up a Pipedream workflow where, after a transaction is completed, customer data is enriched and segmented in a CRM like HubSpot or Salesforce. This allows for targeted marketing campaigns and personalized follow-ups. diff --git a/components/helium/README.md b/components/helium/README.md index 14d6c965748df..58a61afd101ca 100644 --- a/components/helium/README.md +++ b/components/helium/README.md @@ -1,12 +1,11 @@ # Overview -With the Helium Console API, you can build a variety of applications and -integrations. Here are some examples: - -- A bot that monitors your Twitter mentions and automatically responds to them -- A bot that automatically posts new articles from your blog to your Twitter - timeline -- An integration that automatically posts new GitHub issues to a Slack channel -- A bot that automatically tweets out a daily summary of your blog traffic -- An integration that automatically posts new comments from your blog to a - Discord channel +The Helium (Console) API serves as a gateway to the Helium network, a decentralized blockchain network for IoT devices. It enables you to interact with the network by fetching device data, managing hotspots, and tracking sensor activity. With Pipedream, you can automate workflows that respond to this data in real-time. This allows for the creation of complex IoT applications, such as automated alerts, data analysis, and integration with other services for enhanced functionality. + +# Example Use Cases + +- **Real-Time IoT Data Processing and Alerting**: Using the Helium (Console) API on Pipedream, you can set up a workflow that listens for data from your IoT sensors and processes it instantly. If the data meets certain conditions – say, a temperature threshold is exceeded – you can automatically trigger notifications via email, SMS, or messaging platforms. + +- **IoT Data Aggregation and Analysis**: Collect data from multiple IoT devices on the Helium network. With Pipedream, feed this data into analytics tools or databases like Google Sheets or Amazon S3. Perform real-time analysis and visualize trends to make informed decisions about your IoT infrastructure. + +- **Automated Device Management**: Create a workflow that monitors the status of your Helium devices. When a device goes offline, automatically attempt a remote reboot and, if that fails, notify your maintenance team via a task in project management tools like Jira or Asana. diff --git a/components/helium/package.json b/components/helium/package.json new file mode 100644 index 0000000000000..9217d94c53ba8 --- /dev/null +++ b/components/helium/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/helium", + "version": "0.6.0", + "description": "Pipedream helium Components", + "main": "helium.app.mjs", + "keywords": [ + "pipedream", + "helium" + ], + "homepage": "https://pipedream.com/apps/helium", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/helloleads/README.md b/components/helloleads/README.md new file mode 100644 index 0000000000000..2c1baa0579f5d --- /dev/null +++ b/components/helloleads/README.md @@ -0,0 +1,11 @@ +# Overview + +The HelloLeads API integrates with Pipedream to automate your lead management processes. By leveraging this API on Pipedream, you can streamline contact syncing, lead tracking, and perform actions based on lead status changes. With serverless workflows, you can connect HelloLeads with various apps like CRMs, communication platforms, or data analysis tools to enhance lead engagement, follow-up efficiency, and data-driven decision-making. + +# Example Use Cases + +- **Sync New Leads to Google Sheets**: Automatically add new leads from HelloLeads to a Google Sheet for easy tracking and analysis. This workflow can trigger whenever a new lead is captured, ensuring your data is always up-to-date for reporting purposes. + +- **Create Tasks in Asana for Follow-ups**: Create tasks in Asana for your sales team whenever a lead reaches a certain stage in HelloLeads. This helps in ensuring timely follow-ups and that no lead falls through the cracks by automating the task creation process. + +- **Send Custom Email Alerts for Hot Leads**: Set up a workflow that sends personalized email alerts via SendGrid when a lead is marked as hot in HelloLeads. This allows your sales team to act quickly on potential opportunities by receiving instant notifications. diff --git a/components/hellosign/README.md b/components/hellosign/README.md index dd8d527204684..4d1ff33b1037b 100644 --- a/components/hellosign/README.md +++ b/components/hellosign/README.md @@ -1,9 +1,11 @@ # Overview -With the Hellosign API, you can build applications that: +The Dropbox Sign (formerly HelloSign) API enables seamless integration of electronic signature workflows into any application or service. On Pipedream, you can harness this API to automate signature requests, track document status, and streamline document management processes. By combining Dropbox Sign with Pipedream's capabilities, you can create powerful automations that save time, reduce errors, and maintain a high level of professional engagement in document signing experiences. -- Send and manage documents -- Automate document workflows -- Track the status of documents -- Store and retrieve data about documents and signers -- Embed signing into your website or app +# Example Use Cases + +- **Automated Contract Workflow for HR**: Start an automated workflow when a new employee is added to an HR management system like BambooHR. Use Dropbox Sign to send employment contracts for signature and automatically store signed documents back in the HR system. + +- **Dynamic Document Generation and Signing**: Generate personalized documents using customer data from a CRM like Salesforce. Then, dispatch them for signing via Dropbox Sign and monitor the signing progress. Completed documents could then trigger downstream actions like updating CRM records or initiating onboarding workflows. + +- **Invoice Approval and Archiving**: When a new invoice is created in an accounting app like QuickBooks, trigger a Pipedream workflow to request signatures via Dropbox Sign. Once signed, save the invoice to cloud storage like Google Drive and update the invoice status in QuickBooks. diff --git a/components/help_scout/README.md b/components/help_scout/README.md index 16dca417e6a61..400452df5a6e9 100644 --- a/components/help_scout/README.md +++ b/components/help_scout/README.md @@ -1,15 +1,11 @@ # Overview -What can you build with the Help Scout API? +The Help Scout API provides programmatic access to customer support functionalities, allowing the automation of ticketing, customer communication, and reporting tasks. With this API, you can read and send messages, manage conversations, work with mailboxes, create and update customers’ information, and generate reports. Utilizing the API on Pipedream, you can automate workflows that respond to events in Help Scout in real-time, integrate customer support data with other business tools, and streamline support operations. -Help Scout is a customer service platform that enables companies to deliver -great customer service. With the Help Scout API, you can extend the -functionality of Help Scout in a variety of ways. +# Example Use Cases -Here are a few examples of what you can build with the Help Scout API: +- **Sync Customer Support Data with CRM**: When a new conversation is created or updated in Help Scout, trigger a workflow that adds or updates the customer details in a CRM like Salesforce, keeping both systems in sync. -- A custom integration that connect Help Scout with another tool you use -- A reporting tool that gives you insights into your customer service data -- A help desk automation tool that streamlines your customer service workflow -- A tool that allows you to provide self-service support to your customers -- Any other creative solution you can think of! +- **Automate Customer Support Follow-ups**: After a conversation is marked as resolved in Help Scout, launch a Pipedream workflow that automatically sends a follow-up email after a specific period to gather feedback or offer further assistance. + +- **Real-Time Support Analytics Dashboard**: Use Pipedream to collect data from Help Scout on conversation statuses, response times, and customer satisfaction. Then, push this data to a dashboard service like Google Sheets or Geckoboard to create a real-time analytics display for your support team. diff --git a/components/help_scout/actions/add-note/add-note.mjs b/components/help_scout/actions/add-note/add-note.mjs new file mode 100644 index 0000000000000..ebb02f2770e0b --- /dev/null +++ b/components/help_scout/actions/add-note/add-note.mjs @@ -0,0 +1,43 @@ +import helpScout from "../../help_scout.app.mjs"; + +export default { + key: "help_scout-add-note", + name: "Add Note to Conversation", + description: "Adds a note to an existing conversation in Help Scout. [See the documentation](https://developer.helpscout.com/mailbox-api/endpoints/conversations/threads/note/)", + version: "0.0.1", + type: "action", + props: { + helpScout, + conversationId: { + propDefinition: [ + helpScout, + "conversationId", + ], + }, + text: { + propDefinition: [ + helpScout, + "text", + ], + }, + userId: { + propDefinition: [ + helpScout, + "userId", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.helpScout.addNoteToConversation({ + $, + conversationId: this.conversationId, + data: { + text: this.text, + user: this.userId, + }, + }); + $.export("$summary", `Successfully added note to conversation ID: ${this.conversationId}`); + return response; + }, +}; diff --git a/components/help_scout/actions/create-customer/create-customer.mjs b/components/help_scout/actions/create-customer/create-customer.mjs new file mode 100644 index 0000000000000..cec309bb7c831 --- /dev/null +++ b/components/help_scout/actions/create-customer/create-customer.mjs @@ -0,0 +1,205 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { + GENDER_OPTIONS, + PHOTO_TYPE_OPTIONS, +} from "../../common/constants.mjs"; +import { + cleanObject, + parseObject, +} from "../../common/utils.mjs"; +import helpScout from "../../help_scout.app.mjs"; + +export default { + key: "help_scout-create-customer", + name: "Create Customer", + description: "Creates a new customer record in Help Scout. [See the documentation](https://developer.helpscout.com/mailbox-api/endpoints/customers/create/)", + version: "0.0.1", + type: "action", + props: { + helpScout, + firstName: { + type: "string", + label: "First Name", + description: "First name of the customer. When defined it must be between 1 and 40 characters.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the customer. When defined it must be between 1 and 40 characters.", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The phone number that will be used when creating a new customer.", + optional: true, + }, + photoUrl: { + type: "string", + label: "Photo URL", + description: "URL of the customer's photo. Max length 200 characters.", + optional: true, + }, + jobTitle: { + type: "string", + label: "Job Title", + description: "Job title. Max length 60 characters.", + optional: true, + }, + photoType: { + type: "string", + label: "Photo Type", + description: "The type of photo.", + options: PHOTO_TYPE_OPTIONS, + optional: true, + }, + background: { + type: "string", + label: "Background", + description: "This is the Notes field from the user interface. Max length 200 characters.", + optional: true, + }, + location: { + type: "string", + label: "Location", + description: "Location of the customer. Max length 60 characters.", + optional: true, + }, + organization: { + type: "string", + label: "Organization", + description: "Organization. Max length 60 characters.", + optional: true, + }, + gender: { + type: "string", + label: "Gender", + description: "Gender of this customer.", + options: GENDER_OPTIONS, + optional: true, + }, + age: { + type: "string", + label: "Age", + description: "Customer's age.", + optional: true, + }, + emails: { + type: "string[]", + label: "Emails", + description: "List of email entries. **Format: {\"type\":\"home\",\"value\":\"customer@email.com\"}**. see [Create Email](https://developer.helpscout.com/mailbox-api/endpoints/customers/emails/create) for the object documentation.", + optional: true, + }, + phones: { + type: "string[]", + label: "Phones", + description: "List of phone entries. **Format: {\"type\":\"work\",\"value\":\"222-333-4444\"}**. see [Create Phone](https://developer.helpscout.com/mailbox-api/endpoints/customers/phones/create) for the object documentation.", + optional: true, + }, + chats: { + type: "string[]", + label: "Chats", + description: "List of chat entries. **Format: {\"type\":\"aim\",\"value\":\"jsprout\"}**. see [Create Chat Handle](https://developer.helpscout.com/mailbox-api/endpoints/customers/chat_handles/create) for the object documentation.", + optional: true, + }, + socialProfiles: { + type: "string[]", + label: "Social Profiles", + description: "List of social profile entries. **Format: {\"type\":\"googleplus\",\"value\":\"https://api.helpscout.net/+HelpscoutNet\"}**. see [Create Social Profile](https://developer.helpscout.com/mailbox-api/endpoints/customers/social_profiles/create) for the object documentation.", + optional: true, + }, + websites: { + type: "string[]", + label: "Websites", + description: "List of websites entries. **Format: {\"value\":\"https://api.helpscout.net/\"}**. see [Create Website](https://developer.helpscout.com/mailbox-api/endpoints/customers/websites/create) for the object documentation.", + optional: true, + }, + addressCity: { + type: "string", + label: "Address City", + description: "The city of the customer.", + optional: true, + }, + addressState: { + type: "string", + label: "Address State", + description: "The state of the customer.", + optional: true, + }, + addressPostalCode: { + type: "string", + label: "Address Postal Code", + description: "The postal code of the customer.", + optional: true, + }, + addressCountry: { + type: "string", + label: "Address Country", + description: "The [ISO 3166 Alpha-2 code](https://www.iban.com/country-codes) country of the customer.", + optional: true, + }, + addressLines: { + type: "string[]", + label: "Address Lines", + description: "A list of address lines.", + optional: true, + }, + properties: { + type: "string[]", + label: "Properties", + description: "List of social profile entries. **Format: {\"type\":\"googleplus\",\"value\":\"https://api.helpscout.net/+HelpscoutNet\"}**. see [Create Social Profile](https://developer.helpscout.com/mailbox-api/endpoints/customers/social_profiles/create) for the object documentation.", + optional: true, + }, + }, + async run({ $ }) { + const address = cleanObject({ + city: this.addressCity, + state: this.addressState, + postalCode: this.addressPostalCode, + country: this.addressCountry, + lines: parseObject(this.addressLines), + properties: parseObject(this.properties), + }); + + let data = {}; + + data = cleanObject({ + firstName: this.firstName, + lastName: this.lastName, + phone: this.phone, + photoUrl: this.photoUrl, + jobTitle: this.jobTitle, + photoType: this.photoType, + background: this.background, + location: this.location, + organization: this.organization, + gender: this.gender, + age: this.age, + emails: parseObject(this.emails), + phones: parseObject(this.phones), + chats: parseObject(this.chats), + socialProfiles: parseObject(this.socialProfiles), + websites: parseObject(this.websites), + }); + + if (Object.keys(address).length) data.address = address; + + if (!Object.keys(data).length) { + throw new ConfigurationError("At least one field or customer entry must be defined."); + } + + try { + const response = await this.helpScout.createCustomer({ + $, + data, + }); + + $.export("$summary", "Successfully created the new customer."); + return response; + } catch ({ message }) { + const error = JSON.parse(message)._embedded.errors[0]; + throw new ConfigurationError(`Path: ${error.path} - ${error.message}`); + } + }, +}; diff --git a/components/help_scout/actions/send-reply/send-reply.mjs b/components/help_scout/actions/send-reply/send-reply.mjs new file mode 100644 index 0000000000000..61673ae81a8b5 --- /dev/null +++ b/components/help_scout/actions/send-reply/send-reply.mjs @@ -0,0 +1,53 @@ +import helpScout from "../../help_scout.app.mjs"; + +export default { + key: "help_scout-send-reply", + name: "Send Reply", + description: "Sends a reply to a conversation. Be careful as this sends an actual email to the customer. [See the documentation](https://developer.helpscout.com/mailbox-api/endpoints/conversations/threads/reply/)", + version: "0.0.1", + type: "action", + props: { + helpScout, + conversationId: { + propDefinition: [ + helpScout, + "conversationId", + ], + }, + customerId: { + propDefinition: [ + helpScout, + "customerId", + ], + }, + text: { + propDefinition: [ + helpScout, + "text", + ], + description: "The content of the reply.", + }, + draft: { + type: "boolean", + label: "Draft", + description: "If set to true, a draft reply is created.", + default: false, + }, + }, + async run({ $ }) { + const response = await this.helpScout.sendReplyToConversation({ + $, + conversationId: this.conversationId, + data: { + customer: { + id: this.customerId, + }, + text: this.text, + draft: this.draft, + }, + }); + + $.export("$summary", `Reply sent successfully to conversation ID: ${this.conversationId}`); + return response; + }, +}; diff --git a/components/help_scout/common/constants.mjs b/components/help_scout/common/constants.mjs new file mode 100644 index 0000000000000..76d42ac05aaa4 --- /dev/null +++ b/components/help_scout/common/constants.mjs @@ -0,0 +1,16 @@ +export const PHOTO_TYPE_OPTIONS = [ + "unknown", + "gravatar", + "twitter", + "facebook", + "googleprofile", + "googleplus", + "linkedin", + "instagram", +]; + +export const GENDER_OPTIONS = [ + "male", + "female", + "unknown", +]; diff --git a/components/help_scout/common/utils.mjs b/components/help_scout/common/utils.mjs new file mode 100644 index 0000000000000..8fabcd59a844c --- /dev/null +++ b/components/help_scout/common/utils.mjs @@ -0,0 +1,33 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; + +export const cleanObject = (o) => { + for (var k in o || {}) { + if (typeof o[k] === "undefined") { + delete o[k]; + } + } + return o; +}; diff --git a/components/help_scout/help_scout.app.mjs b/components/help_scout/help_scout.app.mjs index 7f6e469dcb48f..4b8b4ad42e146 100644 --- a/components/help_scout/help_scout.app.mjs +++ b/components/help_scout/help_scout.app.mjs @@ -1,11 +1,150 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "help_scout", - propDefinitions: {}, + propDefinitions: { + agentId: { + type: "string", + label: "Agent ID", + description: "ID of the agent to whom the conversation is assigned.", + }, + conversationId: { + type: "string", + label: "Conversation ID", + description: "The unique identifier of the conversation.", + async options({ page }) { + const { _embedded: { conversations } } = await this.listConversations({ + params: { + page: page + 1, + }, + }); + + return conversations.map(({ + id: value, subject, primaryCustomer: { email }, + }) => ({ + label: `${subject}(${value}) - ${email}`, + value, + })); + }, + }, + customerId: { + type: "string", + label: "Customer ID", + description: "The unique identifier of the customer.", + async options({ page }) { + const { _embedded: { customers } } = await this.listCustomers({ + params: { + page: page + 1, + }, + }); + + return customers.map(({ + id: value, firstName, lastName, _embedded: { emails }, + }) => ({ + label: `${firstName} ${lastName} - ${emails[0].id}`, + value, + })); + }, + }, + userId: { + type: "string", + label: "User ID", + description: "The unique identifier of the user.", + async options({ page }) { + const { _embedded: { users } } = await this.listUsers({ + params: { + page: page + 1, + }, + }); + + return users.map(({ + id: value, email: label, + }) => ({ + label, + value, + })); + }, + }, + text: { + type: "string", + label: "Text", + description: "The content of the note.", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.helpscout.net/v2"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listConversations(opts = {}) { + return this._makeRequest({ + path: "/conversations", + ...opts, + }); + }, + listCustomers(opts = {}) { + return this._makeRequest({ + path: "/customers", + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users", + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + }); + }, + addNoteToConversation({ + conversationId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/conversations/${conversationId}/notes`, + ...opts, + }); + }, + createCustomer(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/customers", + ...opts, + }); + }, + sendReplyToConversation({ + conversationId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/conversations/${conversationId}/reply`, + ...opts, + }); }, }, }; diff --git a/components/help_scout/package.json b/components/help_scout/package.json new file mode 100644 index 0000000000000..2cbc959a07578 --- /dev/null +++ b/components/help_scout/package.json @@ -0,0 +1,20 @@ +{ + "name": "@pipedream/help_scout", + "version": "0.1.0", + "description": "Pipedream Help Scout Components", + "main": "help_scout.app.mjs", + "keywords": [ + "pipedream", + "help_scout" + ], + "homepage": "https://pipedream.com/apps/help_scout", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "crypto": "^1.0.1" + } +} + diff --git a/components/help_scout/sources/common/base.mjs b/components/help_scout/sources/common/base.mjs new file mode 100644 index 0000000000000..90329148e98d3 --- /dev/null +++ b/components/help_scout/sources/common/base.mjs @@ -0,0 +1,84 @@ +import crypto from "crypto"; +import helpScout from "../../help_scout.app.mjs"; + +export default { + props: { + helpScout, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + label: { + type: "string", + label: "Label", + description: "Label associated with this WebHook for better clarity.", + optional: true, + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + _getSecret() { + return this.db.get("secret"); + }, + _setSecret(secret) { + this.db.set("secret", secret); + }, + getExtraData() { + return {}; + }, + }, + hooks: { + async activate() { + const secret = crypto.randomBytes(64).toString("hex"); + const { headers } = await this.helpScout.createWebhook({ + returnFullResponse: true, + data: { + url: this.http.endpoint, + events: this.getEventType(), + label: this.label, + secret, + }, + }); + this._setSecret(secret); + this._setHookId(headers["resource-id"]); + }, + async deactivate() { + const webhookId = this._getHookId(); + await this.helpScout.deleteWebhook(webhookId); + }, + }, + async run({ + bodyRaw, body, headers, + }) { + const hsSignature = headers["x-helpscout-signature"]; + if (hsSignature) { + const secret = this._getSecret(); + const hash = crypto.createHmac("sha1", secret) + .update(bodyRaw) + .digest("base64"); + + if (hash != hsSignature) { + return this.http.respond({ + status: 400, + }); + } + } + + const ts = Date.parse(new Date()); + this.$emit(body, { + id: body.id, + summary: this.getSummary(body), + ts: ts, + }); + + return this.http.respond({ + status: 200, + }); + }, +}; diff --git a/components/help_scout/sources/new-conversation-assigned-instant/new-conversation-assigned-instant.mjs b/components/help_scout/sources/new-conversation-assigned-instant/new-conversation-assigned-instant.mjs new file mode 100644 index 0000000000000..947d6252fa4e8 --- /dev/null +++ b/components/help_scout/sources/new-conversation-assigned-instant/new-conversation-assigned-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "help_scout-new-conversation-assigned-instant", + name: "New Conversation Assigned (Instant)", + description: "Emit new event when a conversation is assigned to an agent. [See the documentation](https://developer.helpscout.com/)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return [ + "convo.assigned", + ]; + }, + getSummary(body) { + return `New conversation assigned to ${body.assignee.email}`; + }, + }, + sampleEmit, +}; diff --git a/components/help_scout/sources/new-conversation-assigned-instant/test-event.mjs b/components/help_scout/sources/new-conversation-assigned-instant/test-event.mjs new file mode 100644 index 0000000000000..1935a35e8e9e9 --- /dev/null +++ b/components/help_scout/sources/new-conversation-assigned-instant/test-event.mjs @@ -0,0 +1,137 @@ +export default { + "id": 291938, + "type": "email", + "folderId": "1234", + "isDraft": "false", + "number": 349, + "owner": { + "id": 1234, + "firstName": "Jack", + "lastName": "Sprout", + "email": "jack.sprout@gmail.com", + "phone": null, + "type": "user" + }, + "mailbox": { + "id": 1234, + "name": "My Mailbox" + }, + "customer": { + "id": 29418, + "firstName": "Vernon", + "lastName": "Bear", + "email": "vbear@mywork.com", + "phone": "800-555-1212", + "type": "customer" + }, + "threadCount": 4, + "status": "active", + "subject": "I need help!", + "preview": "Hello, I tried to download the file off your site...", + "createdBy": { + "id": 29418, + "firstName": "Vernon", + "lastName": "Bear", + "email": "vbear@mywork.com", + "phone": null, + "type": "customer" + }, + "createdAt": "2012-07-23T12:34:12Z", + "modifiedAt": "2012-07-24T20:18:33Z", + "closedAt": null, + "closedBy": null, + "source": { + "type": "email", + "via": "customer" + }, + "cc": [ + "cc1@somewhere.com", + "cc2@somewhere.com" + ], + "bcc": [ + "bcc1@somewhere.com", + "bcc2@somewhere.com" + ], + "tags": [ + "tag1", + "tag2" + ], + "customFields": [ + { + "fieldId": 1, + "name": "Team", + "value": "Development" + }, + { + "fieldId": 2, + "name": "Customer Disposition", + "value": "Happy" + } + ], + "threads": [ + { + "id": 88171881, + "assignedTo": { + "id": 1234, + "firstName": "Jack", + "lastName": "Sprout", + "email": "jack.sprout@gmail.com", + "phone": null, + "type": "user" + }, + "status": "active", + "createdAt": "2012-07-23T12:34:12Z", + "createdBy": { + "id": 1234, + "firstName": "Jack", + "lastName": "Sprout", + "email": "jack.sprout@gmail.com", + "phone": null, + "type": "user" + }, + "source": { + "type": "web", + "via": "user" + }, + "type": "message", + "state": "published", + "customer": { + "id": 29418, + "firstName": "Vernon", + "lastName": "Bear", + "email": "vbear@mywork.com", + "phone": "800-555-1212", + "type": "customer" + }, + "fromMailbox": null, + "body": "This is what I have to say. Thank you.", + "to": [ + "customer@somewhere.com" + ], + "cc": [ + "cc1@somewhere.com", + "cc2@somewhere.com" + ], + "bcc": [ + "bcc1@somewhere.com", + "bcc2@somewhere.com" + ], + "attachments": [ + { + "id": 12391, + "mimeType": "image/jpeg", + "filename": "logo.jpg", + "size": 22, + "width": 160, + "height": 160, + "url": "https://secure.helpscout.net/some-url/logo.jpg" + } + ], + "tags": [ + "tag1", + "tag2", + "tag3" + ] + } + ] +} \ No newline at end of file diff --git a/components/help_scout/sources/new-conversation-created-instant/new-conversation-created-instant.mjs b/components/help_scout/sources/new-conversation-created-instant/new-conversation-created-instant.mjs new file mode 100644 index 0000000000000..37f0b97e12d52 --- /dev/null +++ b/components/help_scout/sources/new-conversation-created-instant/new-conversation-created-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "help_scout-new-conversation-created-instant", + name: "New Conversation Created (Instant)", + description: "Emit new event when a new conversation is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return [ + "convo.created", + ]; + }, + getSummary(body) { + return `New conversation created: ${body.subject}`; + }, + }, + sampleEmit, +}; diff --git a/components/help_scout/sources/new-conversation-created-instant/test-event.mjs b/components/help_scout/sources/new-conversation-created-instant/test-event.mjs new file mode 100644 index 0000000000000..d8cc91fd9e8de --- /dev/null +++ b/components/help_scout/sources/new-conversation-created-instant/test-event.mjs @@ -0,0 +1,137 @@ +export default { + "id": 291938, + "type": "email", + "folderId": "1234", + "isDraft": "false", + "number": 349, + "owner": { + "id": 1234, + "firstName": "Jack", + "lastName": "Sprout", + "email": "jack.sprout@gmail.com", + "phone": null, + "type": "user" + }, + "mailbox": { + "id": 1234, + "name": "My Mailbox" + }, + "customer": { + "id": 29418, + "firstName": "Vernon", + "lastName": "Bear", + "email": "vbear@mywork.com", + "phone": "800-555-1212", + "type": "customer" + }, + "threadCount": 4, + "status": "active", + "subject": "I need help!", + "preview": "Hello, I tried to download the file off your site...", + "createdBy": { + "id": 29418, + "firstName": "Vernon", + "lastName": "Bear", + "email": "vbear@mywork.com", + "phone": null, + "type": "customer" + }, + "createdAt": "2012-07-23T12:34:12Z", + "modifiedAt": "2012-07-24T20:18:33Z", + "closedAt": null, + "closedBy": null, + "source": { + "type": "email", + "via": "customer" + }, + "cc": [ + "cc1@somewhere.com", + "cc2@somewhere.com" + ], + "bcc": [ + "bcc1@somewhere.com", + "bcc2@somewhere.com" + ], + "tags": [ + "tag1", + "tag2" + ], + "customFields": [ + { + "fieldId": 1, + "name": "Team", + "value": "Development" + }, + { + "fieldId": 2, + "name": "Customer Disposition", + "value": "Happy" + } + ], + "threads": [ + { + "id": 88171881, + "assignedTo": { + "id": 1234, + "firstName": "Jack", + "lastName": "Sprout", + "email": "jack.sprout@gmail.com", + "phone": null, + "type": "user" + }, + "status": "active", + "createdAt": "2012-07-23T12:34:12Z", + "createdBy": { + "id": 1234, + "firstName": "Jack", + "lastName": "Sprout", + "email": "jack.sprout@gmail.com", + "phone": null, + "type": "user" + }, + "source": { + "type": "web", + "via": "user" + }, + "type": "message", + "state": "published", + "customer": { + "id": 29418, + "firstName": "Vernon", + "lastName": "Bear", + "email": "vbear@mywork.com", + "phone": "800-555-1212", + "type": "customer" + }, + "fromMailbox": null, + "body": "This is what I have to say. Thank you.", + "to": [ + "customer@somewhere.com" + ], + "cc": [ + "cc1@somewhere.com", + "cc2@somewhere.com" + ], + "bcc": [ + "bcc1@somewhere.com", + "bcc2@somewhere.com" + ], + "attachments": [ + { + "id": 12391, + "mimeType": "image/jpeg", + "filename": "logo.jpg", + "size": 22, + "width": 160, + "height": 160, + "url": "https://secure.helpscout.net/some-url/logo.jpg" + } + ], + "tags": [ + "tag1", + "tag2", + "tag3" + ] + } + ] + } \ No newline at end of file diff --git a/components/help_scout/sources/new-customer-instant/new-customer-instant.mjs b/components/help_scout/sources/new-customer-instant/new-customer-instant.mjs new file mode 100644 index 0000000000000..766f7a78b6863 --- /dev/null +++ b/components/help_scout/sources/new-customer-instant/new-customer-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "help_scout-new-customer-instant", + name: "New Customer Added (Instant)", + description: "Emit new event when a new customer is added.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return [ + "customer.created", + ]; + }, + getSummary(body) { + return `New customer created: ${body.firstName} ${body.lastName} - ${body._embedded.emails[0].value}`; + }, + }, + sampleEmit, +}; diff --git a/components/help_scout/sources/new-customer-instant/test-event.mjs b/components/help_scout/sources/new-customer-instant/test-event.mjs new file mode 100644 index 0000000000000..7070a334032db --- /dev/null +++ b/components/help_scout/sources/new-customer-instant/test-event.mjs @@ -0,0 +1,58 @@ +export default { + "id": 29418, + "firstName": "Vernon", + "lastName": "Bear", + "photoUrl": "http://twitter.com/img/some-avatar.jpg", + "photoType": "twitter", + "gender": "Male", + "age": "30-35", + "organization": "Acme, Inc", + "jobTitle": "CEO and Co-Founder", + "location": "Greater Dallas/FT Worth Area", + "background": "I've worked with Vernon before and he's really great.", + "createdAt": "2012-07-23T12:34:12Z", + "modifiedAt": "2012-07-24T20:18:33Z", + "address": { + "id": 1234, + "lines": [ + "123 West Main St", + "Suite 123" + ], + "city": "Dallas", + "state": "TX", + "postcalCode": "74206", + "country": "US", + "createdAt": "2012-07-23T12:34:12Z", + "modifiedAt": "2012-07-24T20:18:33Z" + }, + "socialProfiles": [ + { + "id": 9184, + "value": "https://twitter.com/helpscout", + "type": "twitter" + }, + ], + "emails": [{ + "id": 98131, + "value": "vbear@mywork.com", + "location": "work" + }, + ], + "phones": [{ + "id": 22381, + "value": "222-333-4444", + "location": "home" + }, + ], + "chats": [{ + "id": 77183, + "value": "jsprout", + "type": "aim" + }, + ], + "websites": [{ + "id": 5584, + "value": "http://www.somewhere.com" + }, + ] +} \ No newline at end of file diff --git a/components/help_scout/yarn.lock b/components/help_scout/yarn.lock new file mode 100644 index 0000000000000..144b3d8041ea3 --- /dev/null +++ b/components/help_scout/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +crypto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037" + integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig== diff --git a/components/helpcrunch/README.md b/components/helpcrunch/README.md new file mode 100644 index 0000000000000..0e14116acd348 --- /dev/null +++ b/components/helpcrunch/README.md @@ -0,0 +1,11 @@ +# Overview + +The HelpCrunch API provides a platform for customer communication, enabling automation of messaging, data synchronization, and customer support activities. It allows you to create and update users, send messages, and manage conversations directly through API calls. Integrating HelpCrunch with Pipedream lets you connect these capabilities to hundreds of other apps, streamlining workflows that can respond dynamically to customer interactions, sync data across platforms, or trigger communications based on specific events. + +# Example Use Cases + +- **Automated Customer Onboarding Messages**: Trigger a welcome message or email series from HelpCrunch when a new user is added to your CRM like Salesforce. This ensures immediate engagement with new prospects or customers. + +- **Support Ticket Creation and Alerts**: Create a HelpCrunch conversation as a support ticket when an issue is reported through a form submission on Typeform. Additionally, send Slack notifications to your support team to promptly address the ticket. + +- **Sync User Profiles Across Platforms**: Keep user data consistent by updating HelpCrunch profiles whenever a customer's information is changed in a connected app like Intercom or HubSpot. This ensures that all customer-facing teams have the most up-to-date information. diff --git a/components/helper_functions/README.md b/components/helper_functions/README.md index 5e94f9796dbdc..a7a204d148799 100644 --- a/components/helper_functions/README.md +++ b/components/helper_functions/README.md @@ -1,20 +1,14 @@ # Overview -Pipedream provides a set of helper functions that make it easy to manipulate -and work with data in your workflow. You can use these helper functions to: +The Helper Functions app on Pipedream is a set of pre-built functions that streamline common tasks in your workflows. It acts like a Swiss Army knife for developers, providing essential tools such as format conversion, date manipulation, and text processing. By leveraging these functions, you can reduce the boilerplate code needed for routine operations, speeding up the development of intricate automations. The Helper Functions API can be a game changer when it comes to tasks like parsing dates in user-friendly formats, encoding and decoding data, or generating UUIDs, making them more efficient and less error-prone. -- Perform mathematical operations on data -- Format data -- Extract data from arrays and objects -- Convert data from one format to another -- Manipulate dates and times -- Generate random data +# Example Use Cases -Here are some examples of what you can build using the Helper Functions API: +- **Format User Input for Database Storage** + In an app where users submit data through forms, the Helper Functions API can be used to sanitize and format user input before it is stored in a database, such as Airtable. This ensures that data is clean and uniform, simplifying retrieval and analysis. -- A calculator that performs mathematical operations on data -- A formatter that formats data -- An extractor that extracts data from arrays and objects -- A converter that converts data from one format to another -- A date and time manipulator -- A random data generator +- **Process Webhook Payloads** + When dealing with incoming webhooks from apps like GitHub, the Helper Functions API can parse and transform JSON payloads. This allows you to extract specific data points and reformat them for use in other apps like Slack for team notifications or JIRA for creating issues. + +- **Automate Content Publication Workflow** + A content calendar on Google Sheets can trigger a workflow that uses Helper Functions to parse dates and format post titles. The workflow could then use this data to automatically schedule and publish content on platforms like WordPress or social media apps. diff --git a/components/helper_functions/actions/csv-file-to-objects/csv-file-to-objects.mjs b/components/helper_functions/actions/csv-file-to-objects/csv-file-to-objects.mjs new file mode 100644 index 0000000000000..7b8223bee667e --- /dev/null +++ b/components/helper_functions/actions/csv-file-to-objects/csv-file-to-objects.mjs @@ -0,0 +1,81 @@ +import { readFileSync } from "fs"; +import path from "path"; +import { parse } from "csv-parse/sync"; +import app from "../../helper_functions.app.mjs"; + +export default { + key: "helper_functions-csv-file-to-objects", + name: "CSV File To Objects", + description: "Convert a CSV file to an array of objects.", + version: "0.0.1", + type: "action", + props: { + app, + filePath: { + type: "string", + label: "CSV File Path", + description: "The path to the file saved to the `/tmp` directory (e.g. `/tmp/example.csv`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + }, + hasHeaders: { + type: "boolean", + label: "File Contains Headers", + description: "Set to `true` if the first row of the CSV contains headers. If there are headers in the file, the keys of the objects will be the header values. If there are no headers, each object will be an array of values.", + optional: true, + default: false, + }, + skipEmptyLines: { + type: "boolean", + label: "Skip Empty Lines", + description: "Set to `true` to skip empty lines in the file.", + optional: true, + default: true, + }, + skipRecordsWithEmptyValues: { + type: "boolean", + label: "Skip Records With Empty Values", + description: "Set to `true` to skip records with empty values. Don't generate records for lines containing empty values, empty Buffer or equals to `null` and `undefined` if their value was casted.", + optional: true, + default: false, + }, + skipRecordsWithError: { + type: "boolean", + label: "Skip Records With Error", + description: "Set to `true` to skip records with errors. Tolerates parsing errors. It skips the records containing an error inside and directly go process the next record.", + optional: true, + default: false, + }, + }, + async run({ $ }) { + const { + filePath, + hasHeaders, + skipEmptyLines, + skipRecordsWithEmptyValues, + skipRecordsWithError, + } = this; + + let fileContent; + try { + fileContent = readFileSync(path.resolve(filePath), "utf8"); + } catch (error) { + console.error("Error reading file:", error); + throw error; + } + + try { + const records = parse(fileContent, { + columns: hasHeaders, + skip_empty_lines: skipEmptyLines, + skip_records_with_empty_values: skipRecordsWithEmptyValues, + skip_records_with_error: skipRecordsWithError, + }); + + $.export("$summary", `Converted ${records.length} records from CSV to objects.`); + return records; + + } catch (error) { + console.error("Error converting CSV to objects:", error); + throw error; + } + }, +}; diff --git a/components/helper_functions/actions/trigger-workflow/trigger-workflow.mjs b/components/helper_functions/actions/trigger-workflow/trigger-workflow.mjs new file mode 100644 index 0000000000000..4f3594d7d5978 --- /dev/null +++ b/components/helper_functions/actions/trigger-workflow/trigger-workflow.mjs @@ -0,0 +1,35 @@ +import helperFunctions from "../../helper_functions.app.mjs"; + +export default { + key: "helper_functions-trigger-workflow", + name: "Trigger Workflow", + description: "Trigger another Pipedream workflow in your workspace.", + version: "0.0.2", + type: "action", + props: { + helperFunctions, + workflowId: { + type: "string", + label: "Workflow ID", + description: "The ID of the workflow to trigger. Workflow IDs are formatted as `p_******` and you can find a workflow’s ID within the workflow builder URL.", + }, + event: { + type: "object", + label: "Event", + description: "The event to be sent to the triggered workflow as the triggering event. In the triggered workflow, you can reference this event object with a custom expression (e.g., `{{steps.trigger.event}}`).", + optional: true, + }, + }, + async run({ $ }) { + const { + workflowId, + event = {}, + } = this; + + const result = await $.flow.trigger(workflowId, event); + + $.export("$summary", `Successfully triggered workflow ID **${workflowId}**`); + + return result; + }, +}; diff --git a/components/helper_functions/package.json b/components/helper_functions/package.json index fc12ea6106118..76d6138c6880b 100644 --- a/components/helper_functions/package.json +++ b/components/helper_functions/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/helper_functions", - "version": "0.3.13", + "version": "0.5.0", "description": "Pipedream Helper_functions Components", "main": "helper_functions.app.mjs", "keywords": [ @@ -14,7 +14,8 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.1.1", + "@pipedream/platform": "^3.0.0", + "csv-parse": "^5.5.6", "streamifier": "^0.1.1", "xml-js": "^1.6.11" } diff --git a/components/helpspace/README.md b/components/helpspace/README.md new file mode 100644 index 0000000000000..2bcb4e86fe160 --- /dev/null +++ b/components/helpspace/README.md @@ -0,0 +1,11 @@ +# Overview + +The Helpspace API facilitates seamless integration of customer support functionalities into various platforms. With Pipedream, you can harness this API to automate ticket tracking, customer communication, and support resource management. By connecting Helpspace to other apps, you can streamline support workflows, sync data across services, and even trigger actions based on customer interactions or support metrics. + +# Example Use Cases + +- **Automated Ticket Creation from Emails**: Set up a Pipedream workflow that listens for incoming emails and automatically generates support tickets in Helpspace. This allows for a rapid response time and ensures that no customer queries slip through the cracks. + +- **Sync Support Tickets with a CRM**: With Pipedream, you can connect Helpspace to a CRM like Salesforce. Whenever a support ticket is updated or closed in Helpspace, the corresponding customer record in Salesforce is updated, keeping all teams in sync about customer issues. + +- **Slack Alerts for High-Priority Tickets**: Create a workflow in Pipedream where high-priority support tickets in Helpspace trigger instant notifications in a designated Slack channel, allowing your support team to quickly mobilize and address urgent customer concerns. diff --git a/components/helpspace/actions/create-customer/create-customer.mjs b/components/helpspace/actions/create-customer/create-customer.mjs new file mode 100644 index 0000000000000..2e6c550de1c28 --- /dev/null +++ b/components/helpspace/actions/create-customer/create-customer.mjs @@ -0,0 +1,77 @@ +import helpspace from "../../helpspace.app.mjs"; + +export default { + key: "helpspace-create-customer", + name: "Create Customer", + description: "Creates a new customer in Helpspace. [See the documentation](https://documentation.helpspace.com/api-customers)", + version: "0.0.1", + type: "action", + props: { + helpspace, + name: { + propDefinition: [ + helpspace, + "name", + ], + }, + email: { + propDefinition: [ + helpspace, + "email", + ], + }, + jobTitle: { + propDefinition: [ + helpspace, + "jobTitle", + ], + }, + address: { + propDefinition: [ + helpspace, + "address", + ], + }, + city: { + propDefinition: [ + helpspace, + "city", + ], + }, + state: { + propDefinition: [ + helpspace, + "state", + ], + }, + postalCode: { + propDefinition: [ + helpspace, + "postalCode", + ], + }, + country: { + propDefinition: [ + helpspace, + "country", + ], + }, + }, + async run({ $ }) { + const { data } = await this.helpspace.createCustomer({ + $, + data: { + name: this.name, + email: this.email, + job_title: this.jobTitle, + address: this.address, + city: this.city, + state: this.state, + postal_code: this.postalCode, + country: this.country, + }, + }); + $.export("$summary", `Successfully created customer with ID: ${data.id}`); + return data; + }, +}; diff --git a/components/helpspace/actions/create-ticket/create-ticket.mjs b/components/helpspace/actions/create-ticket/create-ticket.mjs new file mode 100644 index 0000000000000..8df487c184a9b --- /dev/null +++ b/components/helpspace/actions/create-ticket/create-ticket.mjs @@ -0,0 +1,67 @@ +import helpspace from "../../helpspace.app.mjs"; + +export default { + key: "helpspace-create-ticket", + name: "Create Ticket", + description: "Creates a new ticket in Helpspace. [See the documentation](https://documentation.helpspace.com/api-tickets)", + version: "0.0.1", + type: "action", + props: { + helpspace, + channelId: { + type: "integer", + label: "Channel ID", + description: "The ID of the channel", + }, + subject: { + type: "string", + label: "Subject", + description: "Name of the ticket", + }, + fromContact: { + propDefinition: [ + helpspace, + "customerEmail", + ], + }, + message: { + type: "string", + label: "Message", + description: "The message text of the ticket", + }, + status: { + type: "string", + label: "Status", + description: "Status of the ticket", + optional: true, + options: [ + "unassigned", + "open", + "escalated", + "spam", + "waiting", + "closed", + ], + }, + }, + async run({ $ }) { + const { data } = await this.helpspace.createTicket({ + $, + data: { + channel: { + id: this.channelId, + }, + subject: this.subject, + from_contact: { + email: this.fromContact, + }, + status: this.status, + message: { + body: this.message, + }, + }, + }); + $.export("$summary", `Successfully created ticket with ID: ${data.id}`); + return data; + }, +}; diff --git a/components/helpspace/actions/update-customer/update-customer.mjs b/components/helpspace/actions/update-customer/update-customer.mjs new file mode 100644 index 0000000000000..e71062bdee039 --- /dev/null +++ b/components/helpspace/actions/update-customer/update-customer.mjs @@ -0,0 +1,87 @@ +import helpspace from "../../helpspace.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "helpspace-update-customer", + name: "Update Customer", + description: "Updates a customer's details in Helpspace. [See the documentation](https://documentation.helpspace.com/api-customers)", + version: "0.0.1", + type: "action", + props: { + helpspace, + customerId: { + propDefinition: [ + helpspace, + "customerId", + ], + }, + name: { + propDefinition: [ + helpspace, + "name", + ], + optional: true, + }, + email: { + propDefinition: [ + helpspace, + "email", + ], + optional: true, + }, + jobTitle: { + propDefinition: [ + helpspace, + "jobTitle", + ], + }, + address: { + propDefinition: [ + helpspace, + "address", + ], + }, + city: { + propDefinition: [ + helpspace, + "city", + ], + }, + state: { + propDefinition: [ + helpspace, + "state", + ], + }, + postalCode: { + propDefinition: [ + helpspace, + "postalCode", + ], + }, + country: { + propDefinition: [ + helpspace, + "country", + ], + }, + }, + async run({ $ }) { + const { data } = await this.helpspace.updateCustomer({ + $, + customerId: this.customerId, + data: utils.cleanObject({ + name: this.name, + email: this.email, + job_title: this.jobTitle, + address: this.address, + city: this.city, + state: this.state, + postal_code: this.postalCode, + country: this.country, + }), + }); + $.export("$summary", `Updated customer ${data.id}`); + return data; + }, +}; diff --git a/components/helpspace/common/utils.mjs b/components/helpspace/common/utils.mjs new file mode 100644 index 0000000000000..6cb98d98150b1 --- /dev/null +++ b/components/helpspace/common/utils.mjs @@ -0,0 +1,10 @@ +export default { + cleanObject(o) { + for (var k in o || {}) { + if (typeof o[k] === "undefined") { + delete o[k]; + } + } + return o; + }, +}; diff --git a/components/helpspace/helpspace.app.mjs b/components/helpspace/helpspace.app.mjs index 6077a375b4dc0..492c4051dc517 100644 --- a/components/helpspace/helpspace.app.mjs +++ b/components/helpspace/helpspace.app.mjs @@ -1,11 +1,146 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "helpspace", - propDefinitions: {}, + propDefinitions: { + customerId: { + type: "string", + label: "Customer ID", + description: "The ID of the customer", + async options({ page }) { + const { data } = await this.listCustomers({ + params: { + page, + }, + }); + return data?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + customerEmail: { + type: "string", + label: "Customer Email", + description: "The email address of the customer", + async options({ page }) { + const { data } = await this.listCustomers({ + params: { + page, + }, + }); + return data?.map(({ + email: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + name: { + type: "string", + label: "Name", + description: "The name of the customer", + }, + email: { + type: "string", + label: "Email", + description: "Email address of the customer", + }, + jobTitle: { + type: "string", + label: "Job Title", + description: "Job title of the customer", + optional: true, + }, + address: { + type: "string", + label: "Street Address", + description: "Street address of the customer", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "City of the customer", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "State of the customer", + optional: true, + }, + postalCode: { + type: "string", + label: "Postal Code", + description: "Postal Code of the customer", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "Country of the customer", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.helpspace.com/api/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `Bearer ${this.$auth.access_token}`, + "Hs-Client-Id": `${this.$auth.client_id}`, + }, + }); + }, + updateWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhook", + ...opts, + }); + }, + listCustomers(opts = {}) { + return this._makeRequest({ + path: "/customers", + ...opts, + }); + }, + createCustomer(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/customers", + ...opts, + }); + }, + createTicket(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/tickets", + ...opts, + }); + }, + updateCustomer({ + customerId, ...opts + }) { + return this._makeRequest({ + method: "PATCH", + path: `/customers/${customerId}`, + ...opts, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/helpspace/package.json b/components/helpspace/package.json index 36be396c9fc8f..d8a37efb03ed2 100644 --- a/components/helpspace/package.json +++ b/components/helpspace/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/helpspace", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Helpspace Components", "main": "helpspace.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" } -} \ No newline at end of file +} diff --git a/components/helpspace/sources/common/base.mjs b/components/helpspace/sources/common/base.mjs new file mode 100644 index 0000000000000..9fad01ade4cfc --- /dev/null +++ b/components/helpspace/sources/common/base.mjs @@ -0,0 +1,59 @@ +import helpspace from "../../helpspace.app.mjs"; + +export default { + props: { + helpspace, + http: "$.interface.http", + }, + hooks: { + async activate() { + await this.helpspace.updateWebhook({ + data: { + webhooks: { + enabled: true, + url: this.http.endpoint, + trigger: this.getTrigger(), + }, + }, + }); + }, + }, + methods: { + getBaseTrigger() { + return { + ticket: { + created: false, + assigned: false, + deleted: false, + status_updated: false, + channel_updated: false, + tags_updated: false, + customer_message_created: false, + agent_message_created: false, + note_created: false, + }, + customer: { + created: false, + updated: false, + deleted: false, + }, + tag: { + created: false, + updated: false, + deleted: false, + }, + }; + }, + getTrigger() { + throw new Error("getTrigger is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run({ body }) { + const { data } = body; + const meta = this.generateMeta(data); + this.$emit(data, meta); + }, +}; diff --git a/components/helpspace/sources/new-customer/new-customer.mjs b/components/helpspace/sources/new-customer/new-customer.mjs new file mode 100644 index 0000000000000..c6a0b2e84fdf1 --- /dev/null +++ b/components/helpspace/sources/new-customer/new-customer.mjs @@ -0,0 +1,33 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "helpspace-new-customer", + name: "New Customer (Instant)", + description: "Emit new event when a new customer signs up on Helpspace. Note: Users may only have one active Helpspace webhook at a time. [See the documentation](https://documentation.helpspace.com/article/340/webhook)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTrigger() { + const baseTrigger = this.getBaseTrigger(); + return { + ...baseTrigger, + customer: { + ...baseTrigger.customer, + created: true, + }, + }; + }, + generateMeta(customer) { + return { + id: customer.id, + summary: `New Customer ${customer.name}`, + ts: Date.parse(customer.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/helpspace/sources/new-customer/test-event.mjs b/components/helpspace/sources/new-customer/test-event.mjs new file mode 100644 index 0000000000000..f6f7c42c6047e --- /dev/null +++ b/components/helpspace/sources/new-customer/test-event.mjs @@ -0,0 +1,17 @@ +export default { + "id": 1, + "id_link": "https:\/\/api.helpspace.com\/api\/v1\/customer\/1", + "name": "New Customer", + "email": "some@email.de", + "job_title": "Master of ...", + "address": "Some Address", + "city": "Boston", + "state": "Massachusetts", + "postal_code": "02135", + "timezone": "America/New_York", + "country": "America", + "note": "Some Text", + "locale": "en-US", + "created_at": "2021-09-03T12:40:43+00:00", + "updated_at": "2021-09-03T12:40:43+00:00", +}; diff --git a/components/helpspace/sources/new-ticket/new-ticket.mjs b/components/helpspace/sources/new-ticket/new-ticket.mjs new file mode 100644 index 0000000000000..f39be5892ced1 --- /dev/null +++ b/components/helpspace/sources/new-ticket/new-ticket.mjs @@ -0,0 +1,33 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "helpspace-new-ticket", + name: "New Ticket (Instant)", + description: "Emit new event when a new ticket is opened in HelpSpace. Note: Users may only have one active Helpspace webhook at a time. [See the documentation](https://documentation.helpspace.com/article/340/webhook)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTrigger() { + const baseTrigger = this.getBaseTrigger(); + return { + ...baseTrigger, + ticket: { + ...baseTrigger.ticket, + created: true, + }, + }; + }, + generateMeta(ticket) { + return { + id: ticket.id, + summary: `New Ticket ${ticket.subject}`, + ts: Date.parse(ticket.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/helpspace/sources/new-ticket/test-event.mjs b/components/helpspace/sources/new-ticket/test-event.mjs new file mode 100644 index 0000000000000..950c170f96eb8 --- /dev/null +++ b/components/helpspace/sources/new-ticket/test-event.mjs @@ -0,0 +1,34 @@ +export default { + "id": 2517, + "subject": "New Ticket name", + "channel": { + "id": 1, + "name": "Support", + "value": "support@your-domain.com", + "type": "email", + }, + "from_contact": { + "id": 1008, + "user_id": 950, + "name": "Sam Smith", + "value": "sam@smith.com", + "type": "email", + }, + "status": "closed", + "assignee": { + "id": 3, + "name": "Joe", + "email": "joe@your-domain.com", + }, + "team": null, + "creator": { + "id": 3, + "name": "Main Account", + "email": "support@your-domain.com", + }, + "tags": [], + "last_contact": "2022-02-04T13:36:55+00:00", + "created_at": "2021-09-10T23:06:50+00:00", + "updated_at": "2022-02-04T13:37:04+00:00", + "deleted_at": null, +}; diff --git a/components/helpspot/actions/common/request-base.mjs b/components/helpspot/actions/common/request-base.mjs new file mode 100644 index 0000000000000..39d9f75a96ca0 --- /dev/null +++ b/components/helpspot/actions/common/request-base.mjs @@ -0,0 +1,131 @@ +import { + NOTE_IS_HTML, + NOTE_TYPE_OPTIONS, + OPENED_VIA_OPTIONS, +} from "../../common/constants.mjs"; +import helpspot from "../../helpspot.app.mjs"; + +export default { + props: { + helpspot, + tNote: { + type: "string", + label: "Note", + description: "The note of the request", + }, + xCategory: { + propDefinition: [ + helpspot, + "xCategory", + ], + }, + fNoteType: { + type: "string", + label: "Note Type", + description: "The type of the note", + options: NOTE_TYPE_OPTIONS, + optional: true, + }, + fNoteIsHTML: { + type: "string", + label: "Note Is HTML?", + description: "whether the note is HTML or text", + optional: true, + options: NOTE_IS_HTML, + }, + sTitle: { + type: "string", + label: "Subject", + description: "The title used as email subject", + optional: true, + }, + xStatus: { + propDefinition: [ + helpspot, + "xStatus", + ], + optional: true, + }, + sUserId: { + type: "string", + label: "User Id", + description: "The Id of the customer", + optional: true, + }, + sFirstName: { + type: "string", + label: "First Name", + description: "The first name of the request creator", + optional: true, + }, + sLastName: { + type: "string", + label: "Last Name", + description: "The last name of the request creator", + optional: true, + }, + sEmail: { + type: "string", + label: "Email", + description: "The email of the request creator", + optional: true, + }, + sPhone: { + type: "string", + label: "Phone", + description: "The phone number of the request creator", + optional: true, + }, + fUrgent: { + type: "boolean", + label: "Urgent", + description: "Whether the request is urgent or not", + optional: true, + }, + fOpenedVia: { + type: "integer", + label: "Opened Via", + description: "Request opened via", + options: OPENED_VIA_OPTIONS, + optional: true, + }, + emailFrom: { + propDefinition: [ + helpspot, + "emailFrom", + ], + optional: true, + }, + emailCC: { + type: "string[]", + label: "Email CC", + description: "A list of emails to CC on the request", + optional: true, + }, + emailBCC: { + type: "string[]", + label: "Email BCC", + description: "A list of emails to BCC on the request", + optional: true, + }, + emailStaff: { + propDefinition: [ + helpspot, + "emailStaff", + ], + optional: true, + }, + }, + async run({ $ }) { + await this.getValidation(); + + const fn = this.getFunction(); + const response = await fn({ + $, + data: this.getData(), + }); + + $.export("$summary", this.getSummary(response)); + return response; + }, +}; diff --git a/components/helpspot/actions/create-request/create-request.mjs b/components/helpspot/actions/create-request/create-request.mjs new file mode 100644 index 0000000000000..550591c2f86e2 --- /dev/null +++ b/components/helpspot/actions/create-request/create-request.mjs @@ -0,0 +1,45 @@ +import { parseObject } from "../../common/utils.mjs"; +import common from "../common/request-base.mjs"; + +export default { + ...common, + key: "helpspot-create-request", + name: "Create Request", + description: "Creates a new user request. [See the documentation](https://support.helpspot.com/index.php?pg=kb.page&id=164#private.request.create)", + version: "0.0.1", + type: "action", + methods: { + getValidation() { + if (!this.sFirstName && !this.sLastName && !this.sUserId && !this.sEmail && !this.sPhone) { + throw new Error("You must provide at least one of the following: First Name, Last Name, User ID, Email, or Phone."); + } + }, + getFunction() { + return this.helpspot.createRequest; + }, + getData() { + return { + tNote: this.tNote, + xCategory: this.xCategory, + fNoteType: this.fNoteType && parseInt(this.fNoteType), + fNoteIsHTML: this.fNoteIsHTML && parseInt(this.fNoteIsHTML), + sTitle: this.sTitle, + xStatus: this.xStatus, + sUserId: this.sUserId, + sFirstName: this.sFirstName, + sLastName: this.sLastName, + sEmail: this.sEmail, + sPhone: this.sPhone, + fUrgent: +this.fUrgent, + fOpenedVia: this.fOpenedVia, + email_from: this.emailFrom, + email_cc: parseObject(this.emailCC)?.join(), + email_bcc: parseObject(this.emailBCC)?.join(), + email_staff: parseObject(this.emailStaff)?.join(), + }; + }, + getSummary(response) { + return `Successfully created request with Id: ${response.xRequest}`; + }, + }, +}; diff --git a/components/helpspot/actions/update-request/update-request.mjs b/components/helpspot/actions/update-request/update-request.mjs new file mode 100644 index 0000000000000..0f8202da5c03e --- /dev/null +++ b/components/helpspot/actions/update-request/update-request.mjs @@ -0,0 +1,53 @@ +import { parseObject } from "../../common/utils.mjs"; +import common from "../common/request-base.mjs"; + +export default { + ...common, + key: "helpspot-update-request", + name: "Update Request", + description: "Updates an existing user request. [See the documentation](https://support.helpspot.com/index.php?pg=kb.page&id=164#private.request.update)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + xRequest: { + propDefinition: [ + common.props.helpspot, + "xRequest", + ], + }, + }, + methods: { + getValidation() { + return true; + }, + getFunction() { + return this.helpspot.updateRequest; + }, + getData() { + return { + xRequest: this.xRequest, + tNote: this.tNote, + xCategory: this.xCategory, + fNoteType: this.fNoteType && parseInt(this.fNoteType), + fNoteIsHTML: this.fNoteIsHTML && parseInt(this.fNoteIsHTML), + sTitle: this.sTitle, + xStatus: this.xStatus, + sUserId: this.sUserId, + sFirstName: this.sFirstName, + sLastName: this.sLastName, + sEmail: this.sEmail, + sPhone: this.sPhone, + fUrgent: +this.fUrgent, + fOpenedVia: this.fOpenedVia, + email_from: this.emailFrom, + email_cc: parseObject(this.emailCC)?.join(), + email_bcc: parseObject(this.emailBCC)?.join(), + email_staff: parseObject(this.emailStaff)?.join(), + }; + }, + getSummary() { + return `Successfully updated request with ID ${this.xRequest}`; + }, + }, +}; diff --git a/components/helpspot/common/constants.mjs b/components/helpspot/common/constants.mjs new file mode 100644 index 0000000000000..4c3264b552835 --- /dev/null +++ b/components/helpspot/common/constants.mjs @@ -0,0 +1,82 @@ +export const LIMIT = 100; + +export const NOTE_TYPE_OPTIONS = [ + { + label: "Private", + value: "0", + }, + { + label: "Public", + value: "1", + }, + { + label: "External", + value: "2", + }, +]; + +export const NOTE_IS_HTML = [ + { + label: "Text", + value: "0", + }, + { + label: "HTML", + value: "1", + }, +]; + +export const OPENED_VIA_OPTIONS = [ + { + label: "Email", + value: 1, + }, + { + label: "Phone", + value: 2, + }, + { + label: "Walk In", + value: 3, + }, + { + label: "Mail", + value: 4, + }, + { + label: "Other", + value: 5, + }, + { + label: "Web Service", + value: 6, + }, + { + label: "Web Form", + value: 7, + }, + { + label: "Forum", + value: 8, + }, + { + label: "Instant Messenger", + value: 9, + }, + { + label: "Fax", + value: 10, + }, + { + label: "Voicemail", + value: 11, + }, + { + label: "Staff Initiated", + value: 12, + }, + { + label: "Tab Widget", + value: 13, + }, +]; diff --git a/components/helpspot/common/utils.mjs b/components/helpspot/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/helpspot/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/helpspot/helpspot.app.mjs b/components/helpspot/helpspot.app.mjs new file mode 100644 index 0000000000000..5c05332f988d2 --- /dev/null +++ b/components/helpspot/helpspot.app.mjs @@ -0,0 +1,224 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "helpspot", + propDefinitions: { + xCategory: { + type: "string", + label: "Category", + description: "The category id of the request.", + async options({ page }) { + const { category } = await this.listCategories({ + params: { + page, + }, + }); + + return Object.entries(category).map(([ + , { + xCategory: value, sCategory: label, + }, + ]) => ({ + label, + value, + })); + }, + }, + emailFrom: { + type: "string", + label: "Send Email From", + description: "The ID of the mailbox to send emails from", + async options({ page }) { + const { mailbox } = await this.listMailboxes({ + params: { + page, + }, + }); + + return mailbox.map(({ + xMailbox: value, sReplyName, sReplyEmail, + }) => ({ + label: `${sReplyName} - ${sReplyEmail}`, + value, + })); + }, + }, + emailStaff: { + type: "string[]", + label: "Email Staff", + description: "List of staff to email", + async options({ page }) { + const { person } = await this.listActiveStaff({ + params: { + page, + }, + }); + + return person.map(({ + xPerson: value, sFname, sLname, sEmail, + }) => ({ + label: `${sFname} ${sLname} - ${sEmail}`, + value, + })); + }, + }, + xStatus: { + type: "string", + label: "Status", + description: "Select a status of the request", + async options({ page }) { + const { status } = await this.listStatuses({ + params: { + page, + }, + }); + + return status.map(({ + xStatus: value, sStatus: label, + }) => ({ + label, + value, + })); + }, + }, + xRequest: { + type: "string", + label: "Request Id", + description: "The Id of the request", + async options({ page }) { + const { request } = await this.listRequests({ + params: { + page, + }, + }); + + return request.map(({ + xRequest: value, sTitle: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.subdomain}.helpspot.com/api/index.php`; + }, + _headers() { + return { + "Authorization": `Bearer ${this.$auth.api_token}`, + "content-type": "multipart/form-data", + }; + }, + _makeRequest({ + $ = this, methodParam, params, ...opts + }) { + return axios($, { + url: this._baseUrl(), + headers: this._headers(), + params: { + ...params, + method: methodParam, + output: "json", + }, + ...opts, + }); + }, + createRequest(opts = {}) { + return this._makeRequest({ + method: "POST", + methodParam: "private.request.create", + ...opts, + }); + }, + getRequest(opts = {}) { + return this._makeRequest({ + methodParam: "private.request.get", + ...opts, + }); + }, + listCategories(opts = {}) { + return this._makeRequest({ + methodParam: "private.request.getCategories", + ...opts, + }); + }, + listMailboxes(opts = {}) { + return this._makeRequest({ + methodParam: "private.request.getMailboxes", + ...opts, + }); + }, + listActiveStaff(opts = {}) { + return this._makeRequest({ + methodParam: "private.util.getActiveStaff", + ...opts, + }); + }, + listStatuses(opts = {}) { + return this._makeRequest({ + methodParam: "private.request.getStatusTypes", + ...opts, + }); + }, + listRequests(opts = {}) { + return this._makeRequest({ + methodParam: "private.request.search", + ...opts, + }); + }, + listChanges(opts = {}) { + return this._makeRequest({ + methodParam: "private.request.getChanged", + ...opts, + }); + }, + multiGet(opts = {}) { + return this._makeRequest({ + methodParam: "private.request.multiGet", + ...opts, + }); + }, + updateRequest(opts = {}) { + return this._makeRequest({ + method: "POST", + methodParam: "private.request.update", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, field, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.length = LIMIT; + params.start = LIMIT * page++; + const response = await fn({ + params, + ...opts, + }); + + for (const d of response[field]) { + yield await this.getRequest({ + params: { + xRequest: d.xRequest || d, + }, + }); + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = response[field].length; + + } while (hasMore); + }, + }, +}; diff --git a/components/helpspot/package.json b/components/helpspot/package.json new file mode 100644 index 0000000000000..0a8d7a9ce0e6c --- /dev/null +++ b/components/helpspot/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/helpspot", + "version": "0.1.0", + "description": "Pipedream HelpSpot Components", + "main": "helpspot.app.mjs", + "keywords": [ + "pipedream", + "helpspot" + ], + "homepage": "https://pipedream.com/apps/helpspot", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} + diff --git a/components/helpspot/sources/common/base.mjs b/components/helpspot/sources/common/base.mjs new file mode 100644 index 0000000000000..de53604c54645 --- /dev/null +++ b/components/helpspot/sources/common/base.mjs @@ -0,0 +1,55 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import helpspot from "../../helpspot.app.mjs"; + +export default { + props: { + helpspot, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 1; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + _getMaxDate({ request_history: { item } }) { + const history = Object.entries(item).map(([ + , item, + ]) => item.dtGMTChange); + return Date.parse(history[history.length - 1]) / 1000; + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + let responseArray = await this.getItems(maxResults, lastDate); + + if (responseArray.length) { + this._setLastDate(responseArray[0].lastDate); + } + + for (const item of responseArray.reverse()) { + const ts = item.lastDate; + + this.$emit(item, { + id: `${item.xRequest}-${ts}`, + summary: this.getSummary(item), + ts: ts, + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/helpspot/sources/new-request/new-request.mjs b/components/helpspot/sources/new-request/new-request.mjs new file mode 100644 index 0000000000000..14d6f68cb6714 --- /dev/null +++ b/components/helpspot/sources/new-request/new-request.mjs @@ -0,0 +1,37 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "helpspot-new-request", + name: "New Request Created", + description: "Emit new event when a new request is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSummary({ xRequest }) { + return `New Request: ${xRequest}`; + }, + async getItems(maxResults, lastDate) { + const responseArray = []; + + const response = this.helpspot.paginate({ + fn: this.helpspot.listRequests, + field: "request", + params: { + afterDate: lastDate, + }, + maxResults, + }); + + for await (const item of response) { + item.lastDate = Date.parse(item.dtGMTOpened) / 1000; + responseArray.push(item); + } + return responseArray; + }, + }, + sampleEmit, +}; diff --git a/components/helpspot/sources/new-request/test-event.mjs b/components/helpspot/sources/new-request/test-event.mjs new file mode 100644 index 0000000000000..c41ce457b8f47 --- /dev/null +++ b/components/helpspot/sources/new-request/test-event.mjs @@ -0,0 +1,53 @@ +export default { + "xRequest": 12416, + "fOpenedVia": "Phone", + "xOpenedViaId": 0, + "xPortal": 0, + "xMailboxToSendFrom": 0, + "xPersonOpenedBy": "Username", + "xPersonAssignedTo": "Username", + "fOpen": 1, + "xStatus": "Active", + "fUrgent": 0, + "xCategory": "Other", + "dtGMTOpened": "Oct 3, 2024", + "dtGMTClosed": "", + "iLastReplyBy": "Username", + "fTrash": 0, + "dtGMTTrashed": "", + "sRequestPassword": "eiCkgBIZEBkslivbqLsk", + "sTitle": "Title test", + "sUserId": "", + "sFirstName": "First Name", + "sLastName": "", + "sEmail": "", + "sPhone": "", + "fullname": "Full Name", + "request_history": { + "item": { + "34": { + "xRequestHistory": 34, + "xRequest": 12416, + "xPerson": "Username", + "dtGMTChange": "Oct 3 2024, 12:02 PM", + "fPublic": 1, + "fInitial": 1, + "fNoteIsHTML": 1, + "fMergedFromRequest": 0, + "tNote": "

Note test

", + "tEmailHeaders": "", + "fPinned": 0, + "cc": "", + "bcc": "", + "to": "", + "staff_notified": [ + "" + ], + "external": 0, + "person_type": "staff", + "files": [] + } + } + }, + "lastDate": 17279136000 +} \ No newline at end of file diff --git a/components/helpspot/sources/request-update/request-update.mjs b/components/helpspot/sources/request-update/request-update.mjs new file mode 100644 index 0000000000000..d616290cf0c66 --- /dev/null +++ b/components/helpspot/sources/request-update/request-update.mjs @@ -0,0 +1,49 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "helpspot-request-update", + name: "New Request Updated", + description: "Emit new event when a request is updated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSummary({ xRequest }) { + return `New request updated: ${xRequest}`; + }, + async getItems(maxResults, lastDate) { + const { xRequest } = await this.helpspot.listChanges({ + params: { + dtGMTChange: lastDate, + }, + }); + + if (!xRequest.length) return []; + + let { request: items } = await this.helpspot.multiGet({ + params: { + xRequest, + }, + }); + + items.reverse(); + + if (maxResults && items.length > maxResults) { + items.length = maxResults; + } + + const responseArray = []; + + for (const item of items) { + item.lastDate = this._getMaxDate(item); + responseArray.push(item); + } + + return responseArray; + }, + }, + sampleEmit, +}; diff --git a/components/helpspot/sources/request-update/test-event.mjs b/components/helpspot/sources/request-update/test-event.mjs new file mode 100644 index 0000000000000..d366e94fdef11 --- /dev/null +++ b/components/helpspot/sources/request-update/test-event.mjs @@ -0,0 +1,172 @@ +export default { + "xRequest": 12410, + "fOpenedVia": "Web Service", + "xOpenedViaId": 0, + "xPortal": 0, + "xMailboxToSendFrom": 1, + "xPersonOpenedBy": "Username", + "xPersonAssignedTo": "", + "fOpen": 1, + "xStatus": "Escalated", + "fUrgent": 1, + "xCategory": "Server", + "dtGMTOpened": "Oct 3, 2024", + "dtGMTClosed": "", + "iLastReplyBy": "Username", + "fTrash": 0, + "dtGMTTrashed": "", + "sRequestPassword": "QrdtUSeDqXzYpfnBULov", + "sTitle": "Re: subject test", + "sUserId": "", + "sFirstName": "First Name", + "sLastName": "Last name", + "sEmail": "user@email.com", + "sPhone": "11111111111", + "fullname": "Full Name", + "request_history": { + "item": { + "24": { + "xRequestHistory": 24, + "xRequest": 12410, + "xPerson": "Username", + "dtGMTChange": "Oct 3 2024, 10:01 AM", + "fPublic": 0, + "fInitial": 1, + "fNoteIsHTML": 1, + "fMergedFromRequest": 0, + "tLog": "", + "tNote": "

note test

", + "tEmailHeaders": "", + "fPinned": 0, + "external": 0, + "person_type": "staff", + "files": [] + }, + "25": { + "xRequestHistory": 25, + "xRequest": 12410, + "xPerson": "Username", + "dtGMTChange": "Oct 3 2024, 10:08 AM", + "fPublic": 0, + "fInitial": 0, + "fNoteIsHTML": 1, + "fMergedFromRequest": 0, + "tLog": "", + "tNote": "

note 06

", + "tEmailHeaders": "", + "fPinned": 0, + "external": 0, + "person_type": "staff", + "files": [] + }, + "26": { + "xRequestHistory": 26, + "xRequest": 12410, + "xPerson": "Username", + "dtGMTChange": "Oct 3 2024, 10:08 AM", + "fPublic": 0, + "fInitial": 0, + "fNoteIsHTML": 0, + "fMergedFromRequest": 0, + "tLog": "Request Changed:", + "tNote": "note test", + "tEmailHeaders": "", + "fPinned": 0, + "external": 0, + "person_type": "staff", + "files": [] + }, + "27": { + "xRequestHistory": 27, + "xRequest": 12410, + "xPerson": "Username", + "dtGMTChange": "Oct 3 2024, 10:10 AM", + "fPublic": 0, + "fInitial": 0, + "fNoteIsHTML": 0, + "fMergedFromRequest": 0, + "tLog": "", + "tNote": "note test", + "tEmailHeaders": "", + "fPinned": 0, + "external": 0, + "person_type": "staff", + "files": [] + }, + "28": { + "xRequestHistory": 28, + "xRequest": 12410, + "xPerson": "Username", + "dtGMTChange": "Oct 3 2024, 10:10 AM", + "fPublic": 0, + "fInitial": 0, + "fNoteIsHTML": 0, + "fMergedFromRequest": 0, + "tLog": "Request Changed:", + "tNote": "", + "tEmailHeaders": "", + "fPinned": 0, + "external": 0, + "person_type": "staff", + "files": [] + }, + "40": { + "xRequestHistory": 40, + "xRequest": 12410, + "xPerson": "Username", + "dtGMTChange": "Oct 3 2024, 04:43 PM", + "fPublic": 0, + "fInitial": 0, + "fNoteIsHTML": 0, + "fMergedFromRequest": 0, + "tLog": "Request Changed:", + "tNote": "", + "tEmailHeaders": "", + "fPinned": 0, + "external": 0, + "person_type": "staff", + "files": [] + }, + "48": { + "xRequestHistory": 48, + "xRequest": 12410, + "xPerson": "Username", + "dtGMTChange": "Oct 3 2024, 04:45 PM", + "fPublic": 1, + "fInitial": 0, + "fNoteIsHTML": 1, + "fMergedFromRequest": 0, + "tNote": "

note test

", + "tEmailHeaders": "", + "fPinned": 0, + "cc": "test@email.com", + "bcc": "test@bcc.com", + "to": "", + "staff_notified": [ + "" + ], + "external": 0, + "person_type": "staff", + "files": [] + }, + "49": { + "xRequestHistory": 49, + "xRequest": 12410, + "xPerson": "Username", + "dtGMTChange": "Oct 3 2024, 04:45 PM", + "fPublic": 0, + "fInitial": 0, + "fNoteIsHTML": 0, + "fMergedFromRequest": 0, + "tLog": "Request Changed:", + "tNote": "", + "tEmailHeaders": "", + "fPinned": 0, + "external": 0, + "person_type": "staff", + "files": [] + } + } + }, + "lastDate": 1727973900 +} \ No newline at end of file diff --git a/components/helpwise/README.md b/components/helpwise/README.md index 7d161b147d885..111d20604016b 100644 --- a/components/helpwise/README.md +++ b/components/helpwise/README.md @@ -1,12 +1,11 @@ # Overview -The Helpwise API enables you to build a variety of different applications that -can help you manage your customer interactions more effectively. Some examples -of applications that you can build using the Helpwise API include: - -- A customer service application that enables you to track and respond to - customer queries more effectively. -- A Sales CRM application that gives you visibility into your sales pipeline - and helps you close more deals. -- A marketing automation application that helps you track and nurture your - leads more effectively. +The Helpwise API empowers you to automate and enhance your shared inbox experience. With it, you can programmatically manage emails, SMS, and other communications within your team's shared inboxes. By leveraging the Helpwise API on Pipedream, you can create efficient workflows that respond to inbound messages, synchronize communications with CRM platforms, organize customer interactions, and streamline various support processes. + +# Example Use Cases + +- **Automated Ticket Labeling and Assignment**: When a new email arrives in Helpwise, use Pipedream to analyze its content and automatically assign it to the appropriate team member based on predefined criteria such as keywords or sender info. Pipedream can also add relevant labels to help categorize and prioritize the support tickets. + +- **Synchronized CRM Updates**: After resolving a support issue in Helpwise, trigger a workflow on Pipedream to update the customer's record in your CRM, such as Salesforce or HubSpot. This can include logging the resolution details, updating the status of the ticket, and ensuring that all team members have the latest information. + +- **Customer Feedback Collection**: Configure Pipedream to send a follow-up survey via email or SMS to customers whose tickets have been closed in Helpwise. Collect the feedback and aggregate the results in a Google Sheet or a dashboard tool like Tableau for analysis, helping your team to improve service quality. diff --git a/components/here/README.md b/components/here/README.md index 23f27ca74a95e..c96ab456cb067 100644 --- a/components/here/README.md +++ b/components/here/README.md @@ -1,10 +1,11 @@ # Overview -You can use the HERE API to build a variety of things, including: - -- Location-based applications -- Map-based applications -- Navigation applications -- Location-based games -- Augmented reality applications -- Location-based analytics applications +The HERE API provides a suite of location-based services including maps, geocoding, places, routing, and traffic. With Pipedream, you can automate workflows that require real-time location intelligence. For instance, enhance logistics with dynamic routing, personalize customer interactions with geolocation data, or monitor assets by integrating location updates into your systems. + +# Example Use Cases + +- **Real-time Delivery Tracking**: Use the HERE API to track delivery vehicles. With Pipedream, you can trigger notifications in Slack or email whenever a vehicle enters or exits a geofenced area, ensuring timely updates on delivery statuses. + +- **Event-Driven Map Updates**: Keep a dynamic map that reflects real-time changes. When a Pipedream workflow detects updates in a database or receives a webhook, it can fetch new geocoded locations from the HERE API and update a shared map on a platform like Google Sheets or a custom web app. + +- **Automated Routing for Field Service**: Optimize routes for field service personnel. Using Pipedream workflows, you can collect job tickets, leverage the HERE API to calculate the most efficient routes, and then send these optimized routes to workers' mobile devices via SMS or a messaging app like WhatsApp. diff --git a/components/herobot_chatbot_marketing/README.md b/components/herobot_chatbot_marketing/README.md new file mode 100644 index 0000000000000..4c27bcf5563f9 --- /dev/null +++ b/components/herobot_chatbot_marketing/README.md @@ -0,0 +1,11 @@ +# Overview + +The HeroBot Chatbot Marketing API lets you interact with the HeroBot platform to manage and deliver chatbot services. With this API, you can automate conversations, broadcast messages, and gather insights from chatbot interactions. Pipedream's serverless platform enables you to create workflows integrating HeroBot with other apps, triggering actions based on events, and processing data without managing infrastructure. + +# Example Use Cases + +- **Broadcast Messages Based on External Triggers**: Automate the process of sending broadcast messages through your HeroBot chatbot when triggered by external events, such as a new email subscriber, a completed survey, or a customer support ticket update. + +- **Chatbot Analytics to Google Sheets**: Collect and send chatbot interaction data to Google Sheets for analysis. Each time a conversation ends or reaches a milestone, append the relevant data to a Google Sheet for tracking performance, user feedback, or common queries. + +- **Sync Chatbot Contacts with CRM**: Keep your CRM up-to-date by syncing new contacts collected by your HeroBot chatbot. Whenever a user provides their details, create or update their record in your CRM, ensuring your sales team has the latest information for follow-ups. diff --git a/components/heroku/README.md b/components/heroku/README.md index 576b4e58e92e0..7b9891ac15a9c 100644 --- a/components/heroku/README.md +++ b/components/heroku/README.md @@ -1,6 +1,11 @@ # Overview -Heroku is a cloud platform that you can use to deploy, manage, and scale your -applications. You can use the Heroku API to manage your applications, add-ons, -and dynos. You can also use the Heroku API to access your account data, create -new apps, view your app logs, and more. +The Heroku API offers programmatic access to Heroku's cloud platform, enabling developers to automate, extend, and integrate their app's lifecycle events with other services. With Pipedream, you can harness the Heroku API for powerful automation, such as managing apps, dynos, add-ons, and configuring scaling operations. Pipedream's ability to connect with multiple services allows for creating complex workflows, such as syncing your development pipeline with external project management tools or triggering alerts based on app metrics. + +# Example Use Cases + +- **Continuous Deployment Workflow**: Use GitHub actions to push code to a repository, then trigger a Pipedream workflow that automatically deploys the latest code to a Heroku app. Integrate Slack to send notifications to your team once the deployment is successful. + +- **Database Backup Automation**: Schedule a recurring Pipedream workflow that takes a backup of your Heroku Postgres database and uploads it to Amazon S3 or Dropbox. Use this to ensure regular backups and maintain a secure offsite copy of your data. + +- **Dynos Management and Scaling**: Create a workflow that monitors your application's response times or other critical performance metrics using Prometheus or Datadog. If certain thresholds are crossed, the workflow can scale your Heroku dynos up or down and inform you via email or SMS with Twilio. diff --git a/components/heroku/common/constants.mjs b/components/heroku/common/constants.mjs new file mode 100644 index 0000000000000..c446d21da03ca --- /dev/null +++ b/components/heroku/common/constants.mjs @@ -0,0 +1,46 @@ +const ENTITIES = [ + { + value: "api:addon-attachment", + label: "addon-attachment - An add-on has been attached or removed from the app", + }, + { + value: "api:addon", + label: "addon - An add-on for the app has been newly provisioned or deleted, or its details have been modified", + }, + { + value: "api:app", + label: "app - The app itself has been provisioned or deleted, or its details have been modified", + }, + { + value: "api:build", + label: "build - A new build for the app has been initiated or the build’s status has changed since the last notification", + }, + { + value: "api:collaborator", + label: "collaborator - A collaborator has been added or removed from the app, or an existing collaborator’s details have been modified", + }, + { + value: "api:domain", + label: "domain - Custom domain details have been added or removed from the app", + }, + { + value: "api:dyno", + label: "dyno - A new dyno has begun running for the app", + }, + { + value: "api:formation", + label: "formation - The dyno formation for a particular process type has been modified", + }, + { + value: "api:release", + label: "release - A new release for the app has been initiated or the release’s status has changed since the last notification", + }, + { + value: "api:sni-endpoint", + label: "sni-endpoint - An SNI endpoint has been specified or removed for the app, or the existing SNI endpoint’s details have been modified", + }, +]; + +export default { + ENTITIES, +}; diff --git a/components/heroku/heroku.app.mjs b/components/heroku/heroku.app.mjs index f6921cc192a59..c994c684b3f58 100644 --- a/components/heroku/heroku.app.mjs +++ b/components/heroku/heroku.app.mjs @@ -1,11 +1,71 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "heroku", - propDefinitions: {}, + propDefinitions: { + appId: { + type: "string", + label: "App ID", + description: "The ID of the app", + async options() { + const apps = await this.listApps(); + return apps?.map((app) => ({ + label: app.name, + value: app.id, + })) || []; + }, + }, + entities: { + type: "string[]", + label: "Entities", + description: "The entity or entities to subscribe to", + options: constants.ENTITIES, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.heroku.com"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + Accept: "application/vnd.heroku+json; version=3", + }, + }); + }, + listApps(opts = {}) { + return this._makeRequest({ + path: "/apps", + ...opts, + }); + }, + createWebhookSubscription({ + appId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/apps/${appId}/webhooks`, + ...opts, + }); + }, + deleteWebhookSubscription({ + appId, hookId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/apps/${appId}/webhooks/${hookId}`, + ...opts, + }); }, }, }; diff --git a/components/heroku/package.json b/components/heroku/package.json new file mode 100644 index 0000000000000..ad831ce609311 --- /dev/null +++ b/components/heroku/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/heroku", + "version": "0.0.1", + "description": "Pipedream Heroku Components", + "main": "heroku.app.mjs", + "keywords": [ + "pipedream", + "heroku" + ], + "homepage": "https://pipedream.com/apps/heroku", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/heroku/sources/new-webhook-event-instant/new-webhook-event-instant.mjs b/components/heroku/sources/new-webhook-event-instant/new-webhook-event-instant.mjs new file mode 100644 index 0000000000000..89f2dc4f9b745 --- /dev/null +++ b/components/heroku/sources/new-webhook-event-instant/new-webhook-event-instant.mjs @@ -0,0 +1,74 @@ +import heroku from "../../heroku.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "heroku-new-webhook-event-instant", + name: "New Webhook Event (Instant)", + description: "Emit new event on each webhook event. [See the documentation](https://devcenter.heroku.com/articles/app-webhooks-schema#webhook-create)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + heroku, + http: "$.interface.http", + db: "$.service.db", + appId: { + propDefinition: [ + heroku, + "appId", + ], + }, + entities: { + propDefinition: [ + heroku, + "entities", + ], + }, + }, + hooks: { + async activate() { + const { id } = await this.heroku.createWebhookSubscription({ + appId: this.appId, + data: { + include: this.entities, + level: "notify", + url: this.http.endpoint, + }, + }); + this._setHookId(id); + }, + async deactivate() { + const hookId = this._getHookId(); + if (hookId) { + await this.heroku.deleteWebhookSubscription({ + appId: this.appId, + hookId, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + generateMeta(event) { + return { + id: event.id, + summary: `New ${event.webhook_metadata.event.include} - ${event.action} Event`, + ts: Date.now(), + }; + }, + }, + async run(event) { + const { body } = event; + if (!body) { + return; + } + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, + sampleEmit, +}; diff --git a/components/heroku/sources/new-webhook-event-instant/test-event.mjs b/components/heroku/sources/new-webhook-event-instant/test-event.mjs new file mode 100644 index 0000000000000..fa44155a44ad1 --- /dev/null +++ b/components/heroku/sources/new-webhook-event-instant/test-event.mjs @@ -0,0 +1,69 @@ +export default { + "id": "a5b27512-1f14-4f45-bc26-fe6bf03686fe", + "data": { + "id": "9a4eaeed-24cc-4b23-a8ca-7867b251eaf3", + "acm": false, + "name": "", + "team": null, + "owner": { + "id": "7b25912d-b147-48d6-b03d-f0fc72be381b", + "email": "" + }, + "space": null, + "stack": { + "id": "74cfe988-7527-4ca9-9667-77bb9f3029cf", + "name": "heroku-24" + }, + "region": { + "id": "59accabd-516d-4f0e-83e6-6e3757701145", + "name": "us" + }, + "git_url": "", + "web_url": "", + "repo_size": null, + "slug_size": null, + "created_at": "2024-11-07T16:38:38Z", + "updated_at": "2024-11-07T17:14:52Z", + "archived_at": null, + "build_stack": { + "id": "74cfe988-7527-4ca9-9667-77bb9f3029cf", + "name": "heroku-24" + }, + "maintenance": false, + "released_at": "2024-11-07T16:38:39Z", + "organization": null, + "internal_routing": null, + "buildpack_provided_description": null + }, + "actor": { + "id": "7b25912d-b147-48d6-b03d-f0fc72be381b", + "email": "" + }, + "action": "update", + "version": "application/vnd.heroku+json; version=3", + "resource": "app", + "sequence": null, + "created_at": "2024-11-07T17:14:52.475272Z", + "updated_at": "2024-11-07T17:14:52.475276Z", + "published_at": "2024-11-07T17:14:52Z", + "previous_data": { + "name": "", + "git_url": "", + "updated_at": "2024-11-07T17:13:42Z" + }, + "webhook_metadata": { + "attempt": { + "id": "16e90948-01d8-4ba0-97de-5868a4aed0bf" + }, + "delivery": { + "id": "bbf72057-15f2-40b0-87c7-99ccf95e1666" + }, + "event": { + "id": "a5b27512-1f14-4f45-bc26-fe6bf03686fe", + "include": "api:app" + }, + "webhook": { + "id": "c385bb39-42ab-49c6-9255-108ff4cabdbd" + } + } +} \ No newline at end of file diff --git a/components/heygen/README.md b/components/heygen/README.md new file mode 100644 index 0000000000000..53226e6007c55 --- /dev/null +++ b/components/heygen/README.md @@ -0,0 +1,11 @@ +# Overview + +The HeyGen API offers tools for generating visual content, such as social media posts, banners, and other graphics programmatically. Integrating this API with Pipedream allows you to automate the creation and distribution of visual assets based on various triggers and data sources. For example, you can generate new images when a new product is added to your inventory, create customized social media posts from RSS feed items, or even automate weekly visual reports. + +# Example Use Cases + +- **Automated Social Media Posts**: When a new blog post is published, the RSS trigger in Pipedream detects the update and triggers a workflow. The HeyGen API is then used to create a social media graphic with the blog post's title and featured image. The graphic is then posted to various social media platforms via Pipedream's Twitter, LinkedIn, or Facebook integrations. + +- **Dynamic Ad Campaign Generation**: Set up a workflow that listens to a Google Sheets update via Pipedream's Google Sheets trigger. Whenever a new product is listed in the sheet, the HeyGen API generates promotional banners with product details. These banners can be directly uploaded to Google Ads or Facebook Ads platforms through their respective Pipedream integrations. + +- **Personalized Email Campaigns**: Combine HeyGen with Pipedream's Email trigger to send out personalized email campaigns. The HeyGen API creates custom images with the recipient's name or other personalized details. Pipedream then sends an email using an SMTP service or integrates with SendGrid to distribute the personalized emails to your subscribers’ list. diff --git a/components/heysummit/README.md b/components/heysummit/README.md index 91aa3607f29e8..23cbdb857a216 100644 --- a/components/heysummit/README.md +++ b/components/heysummit/README.md @@ -1,8 +1,11 @@ # Overview -HeySummit allows you to build a variety of online events, including: +The HeySummit API lets you automate and integrate your event management tasks with ease. With this API, you can access attendee data, control event schedules, retrieve talk details, and more. By using Pipedream's serverless platform, you can construct workflows that trigger actions in other apps or services whenever specific events occur in HeySummit. This opens up a myriad of possibilities for event organizers to streamline processes, improve attendee engagement, and gain valuable insights from their events. -- Webinars -- Virtual conferences -- Online courses -- And more! +# Example Use Cases + +- **Attendee Registration to Email Campaigns**: When a new attendee registers for an event on HeySummit, automatically add them to an email marketing campaign on Mailchimp. This keeps participants engaged with regular updates and personalized content. + +- **Speaker Coordination Workflow**: Upon the addition of a new speaker to an event in HeySummit, trigger a sequence of tasks such as sending a welcome email through SendGrid, creating a task in Asana for the event coordinator, and updating a Google Sheet with the speaker's details for streamlined team coordination. + +- **Post-Event Feedback Collection**: After an event ends, automatically send a feedback survey link from Typeform to all attendees via Twilio SMS. This immediate request for feedback can lead to higher response rates and more valuable insights for future event improvements. diff --git a/components/heyy/actions/create-contact/create-contact.mjs b/components/heyy/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..a377a2636dff7 --- /dev/null +++ b/components/heyy/actions/create-contact/create-contact.mjs @@ -0,0 +1,102 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../heyy.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "heyy-create-contact", + name: "Create Contact", + description: "Creates a new contact for the business. [See the documentation](https://documenter.getpostman.com/view/27408936/2sA2r3a6DW#a1249b8d-10cf-446a-be35-eb8793ffa967).", + version: "0.0.1", + type: "action", + props: { + app, + phoneNumber: { + propDefinition: [ + app, + "phoneNumber", + ], + }, + firstName: { + propDefinition: [ + app, + "firstName", + ], + }, + lastName: { + propDefinition: [ + app, + "lastName", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + labels: { + propDefinition: [ + app, + "labels", + ], + }, + attributes: { + propDefinition: [ + app, + "attributes", + ], + }, + }, + methods: { + createContact(args = {}) { + return this.app.post({ + path: "/contacts", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createContact, + phoneNumber, + firstName, + lastName, + email, + labels, + attributes, + } = this; + + if (!utils.isPhoneNumberValid(phoneNumber)) { + throw new ConfigurationError(`The phone number \`${phoneNumber}\` is invalid. Please provide a valid phone number.`); + } + + const response = await createContact({ + $, + data: { + phoneNumber, + firstName, + lastName, + email, + ...(labels?.length && { + labels: labels.map((name) => ({ + name, + })), + }), + attributes: + attributes && Object.entries(attributes) + .reduce((acc, [ + externalId, + value, + ]) => ([ + ...acc, + { + externalId, + value, + }, + ]), []), + }, + }); + $.export("$summary", `Successfully created contact with ID \`${response.data.id}\`.`); + return response; + }, +}; diff --git a/components/heyy/actions/send-whatsapp-message/send-whatsapp-message.mjs b/components/heyy/actions/send-whatsapp-message/send-whatsapp-message.mjs new file mode 100644 index 0000000000000..7ea702a060f37 --- /dev/null +++ b/components/heyy/actions/send-whatsapp-message/send-whatsapp-message.mjs @@ -0,0 +1,161 @@ +import app from "../../heyy.app.mjs"; +import constants from "../../common/constants.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "heyy-send-whatsapp-message", + name: "Send WhatsApp Message", + description: "Sends a WhatsApp message to a contact. [See the documentation](https://documenter.getpostman.com/view/27408936/2sa2r3a6dw)", + version: "0.0.1", + type: "action", + props: { + app, + channelId: { + propDefinition: [ + app, + "channelId", + ], + }, + phoneNumber: { + label: "Phone Number", + description: "The phone number of the contact.", + propDefinition: [ + app, + "contactId", + () => ({ + mapper: ({ + firstName, phoneNumber: value, + }) => ({ + label: firstName || value, + value, + }), + }), + ], + }, + msgType: { + type: "string", + label: "Message Type", + description: "The type of message to send.", + options: Object.values(constants.MSG_TYPE), + reloadProps: true, + }, + }, + additionalProps() { + const { msgType } = this; + + const bodyText = { + type: "string", + label: "Body Text", + description: "The text of the message to send.", + }; + + if (msgType === constants.MSG_TYPE.TEXT) { + return { + bodyText, + }; + } + + if (msgType === constants.MSG_TYPE.IMAGE) { + return { + bodyText, + fileId: { + type: "string", + label: "File ID", + description: "The ID of the file to attach to the message.", + }, + }; + } + + if (msgType === constants.MSG_TYPE.TEMPLATE) { + return { + messageTemplateId: { + type: "string", + label: "Message Template ID", + description: "The ID of the message template to use.", + optional: true, + options: async ({ page }) => { + const { data: { messageTemplates } } = await this.app.getMessageTemplates({ + params: { + page, + sortBy: "updatedAt", + order: "DESC", + }, + }); + return messageTemplates.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }; + } + + if (msgType === constants.MSG_TYPE.INTERACTIVE) { + return { + bodyText, + buttons: { + type: "string[]", + label: "Buttons", + description: "The buttons to include in the message. Each row should have a JSON formated string. Eg. `{ \"id\": \"STRING\", \"title\": \"STRING\" }`.", + }, + headerText: { + type: "string", + label: "Header Text", + description: "The header text of the message to send.", + optional: true, + }, + footerText: { + type: "string", + label: "Footer Text", + description: "The footer text of the message to send.", + optional: true, + }, + }; + } + + return {}; + }, + methods: { + sendWhatsappMessage({ + channelId, ...args + } = {}) { + return this.app.post({ + path: `/${channelId}/whatsapp_messages/send`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + sendWhatsappMessage, + channelId, + phoneNumber, + msgType, + bodyText, + fileId, + messageTemplateId, + headerText, + footerText, + buttons, + } = this; + + const response = await sendWhatsappMessage({ + $, + channelId, + data: { + phoneNumber, + type: msgType, + bodyText, + fileId, + messageTemplateId, + headerText, + footerText, + buttons: utils.parseArray(buttons), + }, + }); + $.export("$summary", "Succesfully sent WhatsApp message."); + return response; + }, +}; diff --git a/components/heyy/actions/update-contact/update-contact.mjs b/components/heyy/actions/update-contact/update-contact.mjs new file mode 100644 index 0000000000000..156c5669b2099 --- /dev/null +++ b/components/heyy/actions/update-contact/update-contact.mjs @@ -0,0 +1,115 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../heyy.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "heyy-update-contact", + name: "Update Contact", + description: "Updates the details of a contact under your business. [See the documentation](https://documenter.getpostman.com/view/27408936/2sA2r3a6DW#5a5ee22b-c16e-4d46-ae5d-3844b6501a34).", + version: "0.0.1", + type: "action", + props: { + app, + contactId: { + propDefinition: [ + app, + "contactId", + ], + }, + phoneNumber: { + optional: true, + propDefinition: [ + app, + "phoneNumber", + ], + }, + firstName: { + optional: true, + propDefinition: [ + app, + "firstName", + ], + }, + lastName: { + propDefinition: [ + app, + "lastName", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + labels: { + propDefinition: [ + app, + "labels", + ], + }, + attributes: { + propDefinition: [ + app, + "attributes", + ], + }, + }, + methods: { + updateContact({ + contactId, ...args + } = {}) { + return this.app.put({ + path: `/contacts/${contactId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + updateContact, + contactId, + phoneNumber, + firstName, + lastName, + email, + labels, + attributes, + } = this; + + if (phoneNumber && !utils.isPhoneNumberValid(phoneNumber)) { + throw new ConfigurationError(`The phone number \`${phoneNumber}\` is invalid. Please provide a valid phone number.`); + } + + const response = await updateContact({ + $, + contactId, + data: { + phoneNumber, + firstName, + lastName, + email, + ...(labels?.length && { + labels: labels.map((name) => ({ + name, + })), + }), + attributes: + attributes && Object.entries(attributes) + .reduce((acc, [ + externalId, + value, + ]) => ([ + ...acc, + { + externalId, + value, + }, + ]), []), + }, + }); + + $.export("$summary", `Successfully updated contact with ID \`${response.data.id}\`.`); + return response; + }, +}; diff --git a/components/heyy/actions/upload-file/upload-file.mjs b/components/heyy/actions/upload-file/upload-file.mjs new file mode 100644 index 0000000000000..6869a3a417ee5 --- /dev/null +++ b/components/heyy/actions/upload-file/upload-file.mjs @@ -0,0 +1,62 @@ +import { createReadStream } from "fs"; +import FormData from "form-data"; +import app from "../../heyy.app.mjs"; + +export default { + key: "heyy-upload-file", + name: "Upload File", + description: "Uploads a file. [See the documentation](https://documenter.getpostman.com/view/27408936/2sA2r3a6DW#67e41b81-318c-4ed0-be78-e92fd39f3530).", + version: "0.0.1", + type: "action", + props: { + app, + filePath: { + type: "string", + label: "File Path", + description: "The file to be uploaded, please provide a file from `/tmp`. To upload a file to `/tmp` folder, please follow the doc [here](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + }, + format: { + type: "string", + label: "Format", + description: "The format of the file to be uploaded.", + options: [ + "IMAGE", + "VIDEO", + "DOCUMENT", + ], + }, + }, + methods: { + uploadFile(args = {}) { + return this.app.post({ + path: "/upload_file", + ...args, + }); + }, + }, + async run({ $ }) { + const { + uploadFile, + filePath, + format, + } = this; + + const file = filePath.startsWith("/tmp") + ? filePath + : `/tmp/${filePath}`; + + const data = new FormData(); + data.append("file", createReadStream(file)); + data.append("format", format); + + const response = await uploadFile({ + $, + headers: { + "Content-Type": "multipart/form-data", + }, + data, + }); + $.export("$summary", `Succesfully uploaded file with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/heyy/common/constants.mjs b/components/heyy/common/constants.mjs new file mode 100644 index 0000000000000..a137b766d45b6 --- /dev/null +++ b/components/heyy/common/constants.mjs @@ -0,0 +1,21 @@ +const BASE_URL = "https://api.hey-y.io"; +const VERSION_PATH = "/api/v2.0"; +const LAST_CREATED_AT = "lastCreatedAt"; +const DEFAULT_MAX = 600; +const WEBHOOK_ID = "webhookId"; + +const MSG_TYPE = { + TEXT: "TEXT", + IMAGE: "IMAGE", + TEMPLATE: "TEMPLATE", + INTERACTIVE: "INTERACTIVE", +}; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_MAX, + LAST_CREATED_AT, + MSG_TYPE, + WEBHOOK_ID, +}; diff --git a/components/heyy/common/utils.mjs b/components/heyy/common/utils.mjs new file mode 100644 index 0000000000000..9c78ff9021a00 --- /dev/null +++ b/components/heyy/common/utils.mjs @@ -0,0 +1,60 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function isJson(value) { + try { + JSON.parse(value); + } catch (e) { + return false; + } + + return true; +} + +function parse(value) { + if (!Object.keys(value).length) { + throw new ConfigurationError("Please provide at least one object property."); + } + + if (typeof(value) === "object") { + return value; + } + + if (isJson(value)) { + return JSON.parse(value); + } + + throw new ConfigurationError("Make sure the custom expression contains a valid object"); +} + +function parseArray(value) { + try { + if (!value) { + return; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +function isPhoneNumberValid(phoneNumber) { + const pattern = new RegExp("^\\+[1-9]{1}[0-9]{0,2}[2-9]{1}[0-9]{2}[2-9]{1}[0-9]{2}[0-9]{4}$"); + return pattern.test(phoneNumber); +} + +export default { + parseArray: (value) => parseArray(value)?.map(parse), + isPhoneNumberValid, +}; diff --git a/components/heyy/heyy.app.mjs b/components/heyy/heyy.app.mjs new file mode 100644 index 0000000000000..277b8886d2427 --- /dev/null +++ b/components/heyy/heyy.app.mjs @@ -0,0 +1,154 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "heyy", + propDefinitions: { + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number of the contact. It must be in E.164 format. Eg: `+14155552671`. For more information please see [here](https://en.wikipedia.org/wiki/E.164).", + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the contact.", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the contact.", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "The email address of the contact.", + optional: true, + }, + labels: { + type: "string[]", + label: "Labels", + description: "The labels associated with the contact.", + optional: true, + async options() { + const { data } = await this.getLabels(); + return data.map(({ name }) => name); + }, + }, + attributes: { + type: "object", + label: "Attributes", + description: "The attributes associated with the contact.", + optional: true, + }, + contactId: { + type: "string", + label: "Contact ID", + description: "The unique identifier of the contact.", + async options({ + page, + mapper = ({ + id: value, firstName, phoneNumber, + }) => ({ + label: firstName || phoneNumber, + value, + }), + }) { + const { data: { contacts } } = await this.getContacts({ + params: { + page, + sortBy: "updatedAt", + order: "DESC", + }, + }); + return contacts.map(mapper); + }, + }, + channelId: { + type: "string", + label: "Channel ID", + description: "The unique identifier of the channel.", + async options({ + mapper = ({ + id: value, whatsappPhoneNumber: { name: label }, + }) => ({ + label, + value, + }), + }) { + const { data } = await this.getChannels(); + return data.map(mapper); + }, + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + Authorization: `Bearer ${this.$auth.api_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + put(args = {}) { + return this._makeRequest({ + method: "PUT", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + getLabels(args = {}) { + return this._makeRequest({ + path: "/labels", + ...args, + }); + }, + getAttributes(args = {}) { + return this._makeRequest({ + path: "/attributes", + ...args, + }); + }, + getContacts(args = {}) { + return this._makeRequest({ + path: "/contacts", + ...args, + }); + }, + getMessageTemplates(args = {}) { + return this._makeRequest({ + path: "/message_templates", + ...args, + }); + }, + getChannels(args = {}) { + return this._makeRequest({ + path: "/channels", + ...args, + }); + }, + }, +}; diff --git a/components/heyy/package.json b/components/heyy/package.json new file mode 100644 index 0000000000000..daadbe845922f --- /dev/null +++ b/components/heyy/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/heyy", + "version": "0.1.0", + "description": "Pipedream Heyy Components", + "main": "heyy.app.mjs", + "keywords": [ + "pipedream", + "heyy" + ], + "homepage": "https://pipedream.com/apps/heyy", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3", + "form-data": "^4.0.1" + } +} diff --git a/components/heyy/sources/new-incoming-message-instant/new-incoming-message-instant.mjs b/components/heyy/sources/new-incoming-message-instant/new-incoming-message-instant.mjs new file mode 100644 index 0000000000000..16152ae56cb07 --- /dev/null +++ b/components/heyy/sources/new-incoming-message-instant/new-incoming-message-instant.mjs @@ -0,0 +1,87 @@ +import app from "../../heyy.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "heyy-new-incoming-message-instant", + name: "New Incoming Message", + description: "Emit new event when a business gets a new incoming message. [See the documentation](https://documenter.getpostman.com/view/27408936/2sA2r3a6DW#eda04a28-4c5b-4709-a3f4-204dba6bcc18).", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + app, + db: "$.service.db", + http: "$.interface.http", + channelId: { + propDefinition: [ + app, + "channelId", + ], + }, + }, + hooks: { + async activate() { + const { + createWebhook, + http: { endpoint: url }, + channelId, + setWebhookId, + } = this; + const response = + await createWebhook({ + data: { + url, + type: "WHATSAPP_MESSAGE_RECEIVED", + channelId, + }, + }); + + setWebhookId(response.data.id); + }, + async deactivate() { + const webhookId = this.getWebhookId(); + if (webhookId) { + await this.deleteWebhook({ + webhookId, + }); + } + }, + }, + methods: { + setWebhookId(value) { + this.db.set(constants.WEBHOOK_ID, value); + }, + getWebhookId() { + return this.db.get(constants.WEBHOOK_ID); + }, + generateMeta({ data: { whatsappMessage: resource } }) { + return { + id: resource.metaMessageId, + summary: `New Incomming Message ${resource.metaMessageId}`, + ts: parseInt(resource.timestamp), + }; + }, + processResource(resource) { + this.$emit(resource, this.generateMeta(resource)); + }, + createWebhook(args = {}) { + return this.app.post({ + debug: true, + path: "/api_webhooks", + ...args, + }); + }, + deleteWebhook({ + webhookId, ...args + } = {}) { + return this.app.delete({ + debug: true, + path: `/api_webhooks/${webhookId}`, + ...args, + }); + }, + }, + async run({ body }) { + this.processResource(body); + }, +}; diff --git a/components/heyzine/README.md b/components/heyzine/README.md new file mode 100644 index 0000000000000..09fccf29b8a9b --- /dev/null +++ b/components/heyzine/README.md @@ -0,0 +1,14 @@ +# Overview + +Heyzine API allows users to create and manage interactive, digital flipbooks from PDFs. With this API, you can automate the conversion of documents into engaging flipbooks, update existing flipbooks, manage pages, and integrate viewer statistics into analytics tools. Using Pipedream, you can harness this API to automate workflows, such as generating marketing materials, creating reports from data, or disseminating information in a visually appealing format. + +# Example Use Cases + +- **Automated Marketing Material Distribution**: + Automatically convert new marketing PDFs uploaded to Google Drive into flipbooks using Heyzine API. Then, share these flipbooks via Mailchimp to a subscriber list, ensuring that marketing materials are distributed as soon as they are ready. + +- **Monthly Report Generation and Distribution**: + Convert monthly sales report PDFs into flipbooks. Using Heyzine API on Pipedream, set up a workflow where each month's sales data from Salesforce is automatically fetched, converted to a PDF, turned into a flipbook, and then emailed to stakeholders using SendGrid. + +- **Real-time Content Publishing in Response to Social Media Trends**: + Monitor Twitter for specific keywords using the Twitter API. When a trending topic matches your business, automatically fetch related articles stored as PDFs, convert them into flipbooks using Heyzine API, and then publish these flipbooks on your WordPress site to capitalize on trending topics quickly. diff --git a/components/heyzine/actions/create-flipbook/create-flipbook.mjs b/components/heyzine/actions/create-flipbook/create-flipbook.mjs new file mode 100644 index 0000000000000..14e64398f8af6 --- /dev/null +++ b/components/heyzine/actions/create-flipbook/create-flipbook.mjs @@ -0,0 +1,27 @@ +import heyzine from "../../heyzine.app.mjs"; + +export default { + key: "heyzine-create-flipbook", + name: "Create Flipbook", + description: "Generates a new flipbook from a PDF file. [See the documentation](https://heyzine.com/developers#rest-api)", + version: "0.0.1", + type: "action", + props: { + heyzine, + pdf: { + type: "string", + label: "PDF", + description: "Url pointing to the pdf to be converted. Must be a direct link to a PDF file.", + }, + }, + async run({ $ }) { + const response = await this.heyzine.makeRequest({ + $, + params: { + pdf: this.pdf, + }, + }); + $.export("$summary", `Successfully created a new flipbook with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/heyzine/heyzine.app.mjs b/components/heyzine/heyzine.app.mjs new file mode 100644 index 0000000000000..327cddb1b40ba --- /dev/null +++ b/components/heyzine/heyzine.app.mjs @@ -0,0 +1,26 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "heyzine", + methods: { + _baseUrl() { + return "https://heyzine.com/api1/rest"; + }, + makeRequest(opts = {}) { + const { + $ = this, + params = {}, + ...otherOpts + } = opts; console.log(params); + return axios($, { + ...otherOpts, + url: this._baseUrl(), + params: { + ...params, + "k": `${this.$auth.api_key}`, + }, + }); + }, + }, +}; diff --git a/components/heyzine/package.json b/components/heyzine/package.json new file mode 100644 index 0000000000000..4782f4bf3976c --- /dev/null +++ b/components/heyzine/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/heyzine", + "version": "0.1.0", + "description": "Pipedream Heyzine Components", + "main": "heyzine.app.mjs", + "keywords": [ + "pipedream", + "heyzine" + ], + "homepage": "https://pipedream.com/apps/heyzine", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/highergov/highergov.app.mjs b/components/highergov/highergov.app.mjs new file mode 100644 index 0000000000000..c79c7ff168aa2 --- /dev/null +++ b/components/highergov/highergov.app.mjs @@ -0,0 +1,41 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "highergov", + methods: { + _baseUrl() { + return "https://www.highergov.com/zapier"; + }, + _headers() { + return { + "Content-Type": "application/json", + "Accept": "application/json", + "X-API-KEY": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + subscribeWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/pipeline/subscribe/", + ...opts, + }); + }, + unsubscribeWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/pipeline/unsubscribe/", + ...opts, + }); + }, + }, +}; diff --git a/components/highergov/package.json b/components/highergov/package.json new file mode 100644 index 0000000000000..4ab0fa38e5339 --- /dev/null +++ b/components/highergov/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/highergov", + "version": "0.1.0", + "description": "Pipedream HigherGov Components", + "main": "highergov.app.mjs", + "keywords": [ + "pipedream", + "highergov" + ], + "homepage": "https://pipedream.com/apps/highergov", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} diff --git a/components/highergov/sources/new-pursuit-added-instant/new-pursuit-added-instant.mjs b/components/highergov/sources/new-pursuit-added-instant/new-pursuit-added-instant.mjs new file mode 100644 index 0000000000000..e4fd077cdfe72 --- /dev/null +++ b/components/highergov/sources/new-pursuit-added-instant/new-pursuit-added-instant.mjs @@ -0,0 +1,39 @@ +import highergov from "../../highergov.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "highergov-new-pursuit-added-instant", + name: "New Pursuit Added (Instant)", + description: "Emit new event when a pursuit is added to the pipeline.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + highergov, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + }, + hooks: { + async activate() { + await this.highergov.subscribeWebhook({ + data: { + target_url: this.http.endpoint, + }, + }); + }, + async deactivate() { + await this.highergov.unsubscribeWebhook(); + }, + }, + async run({ body }) { + this.$emit(body, { + id: body.opp_key, + summary: `New pursuit added: ${body.title}`, + ts: Date.parse(body.current_datetime), + }); + }, + sampleEmit, +}; diff --git a/components/highergov/sources/new-pursuit-added-instant/test-event.mjs b/components/highergov/sources/new-pursuit-added-instant/test-event.mjs new file mode 100644 index 0000000000000..9ea44899fda07 --- /dev/null +++ b/components/highergov/sources/new-pursuit-added-instant/test-event.mjs @@ -0,0 +1,46 @@ +export default { + "title": "string", + "description_text": "string", + "source_id": "string", + "source_id_version": "string", + "captured_date": "string", + "posted_date": "string", + "due_date": "string", + "agency": { + "agency_key": "string", + "agency_name": "string", + "agency_abbreviation": "string", + "agency_type": "string", + "path": "string" + }, + "naics_code": { + "naics_code": "string" + }, + "psc_code": { + "psc_code": "string" + }, + "primary_contact_email": { + "contact_title": "string", + "contact_name": "string", + "contact_first_name": "string", + "contact_last_name": "string", + "contact_email": "string", + "contact_phone": "string" + }, + "secondary_contact_email": { + "contact_title": "string", + "contact_name": "string", + "contact_first_name": "string", + "contact_last_name": "string", + "contact_email": "string", + "contact_phone": "string" + }, + "set_aside": "string", + "opp_key": "string", + "version_key": "string", + "source_type": "string", + "unweighted_value": "string", + "current_datetime": "string", + "user_email": "string", + "path": "string" +} \ No newline at end of file diff --git a/components/highlevel_oauth/.gitignore b/components/highlevel_oauth/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/highlevel_oauth/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/highlevel_oauth/README.md b/components/highlevel_oauth/README.md new file mode 100644 index 0000000000000..e6cf7ce1e4014 --- /dev/null +++ b/components/highlevel_oauth/README.md @@ -0,0 +1,11 @@ +# Overview + +The HighLevel (OAuth) API provides a suite of tools designed for marketing agencies and businesses to automate their operations, manage customer relations, and drive growth. With Pipedream, you can leverage HighLevel's capabilities to streamline workflows, such as synchronizing contact information, triggering custom actions based on client interactions, and analyzing marketing data. Integrating the HighLevel API into Pipedream workflows allows for a seamless connection with other apps and services, enabling complex automations with minimal effort. + +# Example Use Cases + +- **Sync New Contacts to Google Sheets**: When a new contact is added in HighLevel, automatically push their details to a Google Sheets spreadsheet. This workflow can help maintain an updated list for reporting or further analysis. + +- **Trigger SMS or Email Campaigns Based on Activity**: Set up triggers in Pipedream that respond to specific client activities or statuses in HighLevel. For instance, send a personalized SMS or email when a client books an appointment or reaches a certain stage in the funnel. + +- **Aggregate Data for Dashboard Reporting**: Collect key metrics from HighLevel, such as campaign performance or sales figures, and send them to a business intelligence tool like Google Data Studio for comprehensive dashboard reporting. diff --git a/components/highlevel_oauth/actions/common/common.mjs b/components/highlevel_oauth/actions/common/common.mjs new file mode 100644 index 0000000000000..3d4308d2478b0 --- /dev/null +++ b/components/highlevel_oauth/actions/common/common.mjs @@ -0,0 +1,60 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObjectEntries } from "../../common/utils.mjs"; +import app from "../../highlevel_oauth.app.mjs"; + +export default { + props: { + app: { + ...app, + reloadProps: true, + }, + name: { + type: "string", + label: "Name", + description: "Full name of the contact, e.g. `Rosan Deo`", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "Email of the contact, e.g. `rosan@deos.com`", + optional: true, + }, + phone: { + type: "string", + label: "Phone Number", + description: "Phone number of the contact, e.g. `+1 888-888-8888`", + optional: true, + }, + additionalOptions: { + type: "object", + label: "Additional Options", + description: + "Additional parameters to send in the request. [See the documentation](https://highlevel.stoplight.io/docs/integrations/4c8362223c17b-create-contact) for available parameters. Values will be parsed as JSON where applicable.", + optional: true, + }, + }, + async additionalProps() { + const locationId = this.app.getLocationId(); + if (!locationId) { + throw new ConfigurationError("This component requires you to authenticate as a **location**, not as an agency/company. *(`locationId` field is missing from `$auth`)*"); + } + + return {}; + }, + methods: { + getData(useLocation = true) { + const { + app, additionalOptions, locationId, ...data + } = this; + return { + app, + ...(useLocation && { + locationId: locationId ?? app.getLocationId(), + }), + ...data, + ...(additionalOptions && parseObjectEntries(additionalOptions)), + }; + }, + }, +}; diff --git a/components/highlevel_oauth/actions/create-contact/create-contact.mjs b/components/highlevel_oauth/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..288d0bf1a2a9e --- /dev/null +++ b/components/highlevel_oauth/actions/create-contact/create-contact.mjs @@ -0,0 +1,38 @@ +import common from "../common/common.mjs"; + +const { + props: { + app, ...props + }, +} = common; + +export default { + ...common, + key: "highlevel_oauth-create-contact", + name: "Create Contact", + description: "Creates a new contact on HighLevel. [See the documentation](https://highlevel.stoplight.io/docs/integrations/4c8362223c17b-create-contact)", + version: "0.0.1", + type: "action", + props: { + app, + locationId: { + propDefinition: [ + app, + "locationId", + ], + }, + ...props, + }, + async run({ $ }) { + const { + app, ...data + } = this.getData(); + const response = await app.createContact({ + $, + data, + }); + + $.export("$summary", `Successfully created contact (ID: ${response?.contact?.id})`); + return response; + }, +}; diff --git a/components/highlevel_oauth/actions/update-contact/update-contact.mjs b/components/highlevel_oauth/actions/update-contact/update-contact.mjs new file mode 100644 index 0000000000000..6637abe890535 --- /dev/null +++ b/components/highlevel_oauth/actions/update-contact/update-contact.mjs @@ -0,0 +1,41 @@ +import common from "../common/common.mjs"; + +const { + props: { + app, ...props + }, +} = common; + +export default { + ...common, + key: "highlevel_oauth-update-contact", + name: "Update Contact", + description: "Updates a selected contact on HighLevel. [See the documentation](https://highlevel.stoplight.io/docs/integrations/9ce5a739d4fb9-update-contact)", + version: "0.0.1", + type: "action", + props: { + app, + contactId: { + propDefinition: [ + app, + "contactId", + ], + }, + ...props, + }, + async run({ $ }) { + const { + app, + contactId, ...data + + } = this.getData(false); + const response = await app.updateContact({ + $, + contactId, + data, + }); + + $.export("$summary", `Successfully updated contact (ID: ${contactId})`); + return response; + }, +}; diff --git a/components/highlevel_oauth/actions/upsert-contact/upsert-contact.mjs b/components/highlevel_oauth/actions/upsert-contact/upsert-contact.mjs new file mode 100644 index 0000000000000..524125f93b504 --- /dev/null +++ b/components/highlevel_oauth/actions/upsert-contact/upsert-contact.mjs @@ -0,0 +1,38 @@ +import common from "../common/common.mjs"; + +const { + props: { + app, ...props + }, +} = common; + +export default { + ...common, + key: "highlevel_oauth-upsert-contact", + name: "Upsert Contact", + description: "Creates or updates a contact on HighLevel. [See the documentation](https://highlevel.stoplight.io/docs/integrations/f71bbdd88f028-upsert-contact)", + version: "0.0.1", + type: "action", + props: { + app, + locationId: { + propDefinition: [ + app, + "locationId", + ], + }, + ...props, + }, + async run({ $ }) { + const { + app, ...data + } = this.getData(); + const response = await app.upsertContact({ + $, + data, + }); + + $.export("$summary", `Successfully upserted contact (ID: ${response?.contact?.id})`); + return response; + }, +}; diff --git a/components/highlevel_oauth/app/highlevel_oauth.app.ts b/components/highlevel_oauth/app/highlevel_oauth.app.ts deleted file mode 100644 index 4649c3187d4e5..0000000000000 --- a/components/highlevel_oauth/app/highlevel_oauth.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "highlevel_oauth", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/highlevel_oauth/common/utils.mjs b/components/highlevel_oauth/common/utils.mjs new file mode 100644 index 0000000000000..9c3cdbf569744 --- /dev/null +++ b/components/highlevel_oauth/common/utils.mjs @@ -0,0 +1,22 @@ +function optionalParseAsJSON(value) { + try { + return JSON.parse(value); + } catch (e) { + return value; + } +} + +export function parseObjectEntries(value) { + const obj = typeof value === "string" + ? JSON.parse(value) + : value; + return Object.fromEntries( + Object.entries(obj).map(([ + key, + value, + ]) => [ + key, + optionalParseAsJSON(value), + ]), + ); +} diff --git a/components/highlevel_oauth/highlevel_oauth.app.mjs b/components/highlevel_oauth/highlevel_oauth.app.mjs new file mode 100644 index 0000000000000..f960606db53e5 --- /dev/null +++ b/components/highlevel_oauth/highlevel_oauth.app.mjs @@ -0,0 +1,87 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "highlevel_oauth", + propDefinitions: { + locationId: { + type: "string", + label: "Location ID", + description: "If not specified, defaults to the authenticated location.", + optional: true, + }, + contactId: { + type: "string", + label: "Contact ID", + description: "Search for a contact or provide a custom contact ID", + useQuery: true, + async options({ query }) { + const { contacts } = await this.searchContacts({ + params: { + query, + limit: 100, + locationId: this.getLocationId(), + }, + }); + return contacts?.map(({ + id, name, email, + }) => ({ + label: name ?? email ?? id, + value: id, + })); + }, + }, + }, + methods: { + getLocationId() { + return this.$auth.locationId; + }, + _baseUrl() { + return "https://services.leadconnectorhq.com"; + }, + async _makeRequest({ + $ = this, + headers, + ...otherOpts + }) { + return axios($, { + baseURL: this._baseUrl(), + headers: { + ...headers, + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "Version": "2021-07-28", + }, + ...otherOpts, + }); + }, + async createContact(args) { + return this._makeRequest({ + method: "POST", + url: "/contacts/", + ...args, + }); + }, + async updateContact({ + contactId, ...args + }) { + return this._makeRequest({ + method: "PUT", + url: `/contacts/${contactId}`, + ...args, + }); + }, + async upsertContact(args) { + return this._makeRequest({ + method: "POST", + url: "/contacts/upsert", + ...args, + }); + }, + async searchContacts(args) { + return this._makeRequest({ + url: "/contacts/", + ...args, + }); + }, + }, +}; diff --git a/components/highlevel_oauth/package.json b/components/highlevel_oauth/package.json index 41edf1aabf156..e3a641a3c9d26 100644 --- a/components/highlevel_oauth/package.json +++ b/components/highlevel_oauth/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/highlevel_oauth", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream HighLevel (OAuth) Components", - "main": "dist/app/highlevel_oauth.app.mjs", + "main": "highlevel_oauth.app.mjs", "keywords": [ "pipedream", "highlevel_oauth" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/highlevel_oauth", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" } -} \ No newline at end of file +} diff --git a/components/highrise/README.md b/components/highrise/README.md index 11ca6e49f8d88..c8ecc6eff6941 100644 --- a/components/highrise/README.md +++ b/components/highrise/README.md @@ -1,8 +1,11 @@ # Overview -Highrise is a customer relationship management tool that allows users to track -their interactions with clients and customers. The Highrise API allows -developers to access and manipulate data stored in Highrise, including -contacts, companies, deals, notes, and tasks. With the Highrise API, developers -can build a variety of applications, such as CRM systems, customer support -tools, and sales tracking applications. +The Highrise API offers a programmatic way to interact with your Highrise CRM data, enabling automated workflows that can save time and improve data consistency. With Pipedream, you can build serverless workflows that trigger on new contact additions, updates to existing contacts, or specific actions like emails received or tasks completed in Highrise. Utilizing Pipedream's ability to integrate with a multitude of other services, you can seamlessly connect Highrise to your broader business processes, such as marketing campaigns, support ticketing systems, or project management tools. + +# Example Use Cases + +- **Contact Synchronization Workflow**: When a new contact is added to Highrise, trigger a Pipedream workflow that adds the contact to a Mailchimp list, ensuring your email marketing campaigns are always targeting the most up-to-date list of contacts. + +- **Support Ticket Integration**: Use Pipedream to monitor Highrise for new support-related notes or tags on contact records. Once detected, automatically create support tickets in Zendesk with the relevant contact info pulled from Highrise, streamlining the initiation of support cases. + +- **Project Management Trigger**: Set up a workflow in Pipedream that watches for completed tasks in Highrise associated with specific deals. When a task is marked as completed, automatically create a follow-up task in Asana or Trello for your project management team to take the next steps in the sales or project pipeline. diff --git a/components/highrise/package.json b/components/highrise/package.json new file mode 100644 index 0000000000000..1116e73cbba61 --- /dev/null +++ b/components/highrise/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/highrise", + "version": "0.6.0", + "description": "Pipedream highrise Components", + "main": "highrise.app.mjs", + "keywords": [ + "pipedream", + "highrise" + ], + "homepage": "https://pipedream.com/apps/highrise", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/hippo_video/README.md b/components/hippo_video/README.md new file mode 100644 index 0000000000000..7eeaaab170688 --- /dev/null +++ b/components/hippo_video/README.md @@ -0,0 +1,11 @@ +# Overview + +The Hippo Video API lets you harness the power of video in your business processes. Through Pipedream, you can automate video creation, management, and distribution workflows. This means you can trigger actions in Hippo Video based on events from other apps, or use Hippo Video events to kick off processes elsewhere. Whether it's automating video emails in your CRM, monitoring video analytics, or managing video content at scale, Pipedream's serverless platform empowers you to build and run workflows seamlessly. + +# Example Use Cases + +- **Customer Support Video Responses**: Automate sending personalized video responses to support ticket submissions via Zendesk. When a ticket is created, trigger a workflow to create a video in Hippo Video, then reply back to the ticket with the video link for a more engaging customer support experience. + +- **Video Testimonials Pipeline**: Streamline collecting and publishing customer testimonials. Set up a workflow that triggers when a form is submitted, use Hippo Video to record and manage the testimonial, and then post the approved videos onto your company website or social media platforms through CMS or social media APIs. + +- **Marketing Automation with Video Analytics**: Enhance your marketing campaigns by triggering actions based on video view analytics. Create a Pipedream workflow that listens for video view events from Hippo Video, analyze the viewing patterns, and integrate with email marketing platforms such as Mailchimp to follow up with viewers based on their engagement level. diff --git a/components/hippo_video/actions/create-contact/create-contact.mjs b/components/hippo_video/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..b2b6694500d2b --- /dev/null +++ b/components/hippo_video/actions/create-contact/create-contact.mjs @@ -0,0 +1,71 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import hippoVideo from "../../hippo_video.app.mjs"; + +export default { + key: "hippo_video-create-contact", + name: "Create Contact", + description: "Creates a new contact in Hippo Video. [See the documentation](https://documenter.getpostman.com/view/5278433/Tz5naxpW#a4d73ffe-a2b6-4d68-a1ee-9fbc1417a955)", + version: "0.0.1", + type: "action", + props: { + hippoVideo, + contactEmail: { + type: "string", + label: "Contact Email", + description: "Email Address of the Contact/Lead/Prospect.", + }, + firstName: { + type: "string", + label: "First Name", + description: "First Name of the Contact/Lead/Prospect.", + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last Name of the Contact/Lead/Prospect.", + }, + companyName: { + type: "string", + label: "Company name", + description: "Company Name of the Contact/Lead/Prospect.", + }, + notes: { + type: "string", + label: "Notes", + description: "Notes for the lead/prospect/contact.", + }, + context: { + type: "string", + label: "Context", + description: "If sales, will be added as a prospect. If empty, will be added as people.", + optional: true, + }, + listIds: { + propDefinition: [ + hippoVideo, + "listIds", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.hippoVideo.createContact({ + $, + data: { + contact_email: this.contactEmail, + first_name: this.firstName, + last_name: this.lastName, + company_name: this.companyName, + notes: this.notes, + context: this.context, + list_ids: parseObject(this.listIds)?.join(), + }, + }); + + if (!response.status) throw new ConfigurationError(response.message); + + $.export("$summary", `Successfully created contact with Id: ${response.contact_id}`); + return response; + }, +}; diff --git a/components/hippo_video/actions/send-personalization-request/send-personalization-request.mjs b/components/hippo_video/actions/send-personalization-request/send-personalization-request.mjs new file mode 100644 index 0000000000000..9dfe67b622905 --- /dev/null +++ b/components/hippo_video/actions/send-personalization-request/send-personalization-request.mjs @@ -0,0 +1,46 @@ +import { ConfigurationError } from "@pipedream/platform"; +import FormData from "form-data"; +import fs from "fs"; +import { checkTmp } from "../../common/utils.mjs"; +import hippoVideo from "../../hippo_video.app.mjs"; + +export default { + key: "hippo_video-send-personalization-request", + name: "Send Personalization Request", + description: "Sends a personalization request for a specified video. [See the documentation](https://help.hippovideo.io/support/solutions/articles/19000099793-bulk-video-personalization-and-tracking-api)", + version: "0.0.1", + type: "action", + props: { + hippoVideo, + videoId: { + propDefinition: [ + hippoVideo, + "videoId", + ], + }, + file: { + type: "string", + label: "File", + description: "csv, xls, and xlsx file saved to the [`/tmp` directory](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory). To get file schema, [see documentation](https://help.hippovideo.io/support/solutions/articles/19000099793-bulk-video-personalization-and-tracking-api)", + }, + }, + async run({ $ }) { + const formData = new FormData(); + const file = fs.createReadStream(checkTmp(this.file)); + + formData.append("file", file); + formData.append("video_id", this.videoId); + + const response = await this.hippoVideo.personalizeVideo({ + $, + data: formData, + headers: formData.getHeaders(), + maxBodyLength: Infinity, + }); + + if (response.code != 200) throw new ConfigurationError(response.message || response.type); + + $.export("$summary", `Successfully sent personalization request for video Id: ${this.videoId}`); + return response; + }, +}; diff --git a/components/hippo_video/actions/upload-video/upload-video.mjs b/components/hippo_video/actions/upload-video/upload-video.mjs new file mode 100644 index 0000000000000..847aa58e9994e --- /dev/null +++ b/components/hippo_video/actions/upload-video/upload-video.mjs @@ -0,0 +1,37 @@ +import { ConfigurationError } from "@pipedream/platform"; +import hippoVideo from "../../hippo_video.app.mjs"; + +export default { + key: "hippo_video-upload-video", + name: "Upload Video", + description: "Uploads a video from a given URL. [See the documentation](https://help.hippovideo.io/support/solutions/articles/19000100703-import-api)", + version: "0.0.1", + type: "action", + props: { + hippoVideo, + title: { + type: "string", + label: "Title", + description: "Title of the video.", + }, + url: { + type: "string", + label: "Video URL", + description: "Public URL of the videos to be downloaded and uploaded to HippoVideo.", + }, + }, + async run({ $ }) { + const response = await this.hippoVideo.uploadVideo({ + $, + data: { + title: this.title, + url: this.url, + }, + }); + + if (response.code != "200") throw new ConfigurationError(response.message); + + $.export("$summary", `Video uploaded successfully with Id: ${response.video_id}`); + return response; + }, +}; diff --git a/components/hippo_video/common/constants.mjs b/components/hippo_video/common/constants.mjs new file mode 100644 index 0000000000000..a3fed4e052594 --- /dev/null +++ b/components/hippo_video/common/constants.mjs @@ -0,0 +1,26 @@ +export const EVENT_OPTIONS = [ + { + value: "3", + label: "Viewing Sessions", + }, + { + value: "1", + label: "Video Created", + }, + { + value: "4", + label: "CTA Lead Genaration", + }, + { + value: "5", + label: "CTA Annotations", + }, + { + value: "6", + label: "CT", + }, + { + value: "18", + label: "Video Transcode", + }, +]; diff --git a/components/hippo_video/common/utils.mjs b/components/hippo_video/common/utils.mjs new file mode 100644 index 0000000000000..d1e7ed1a22d98 --- /dev/null +++ b/components/hippo_video/common/utils.mjs @@ -0,0 +1,31 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; + +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; diff --git a/components/hippo_video/hippo_video.app.mjs b/components/hippo_video/hippo_video.app.mjs index 18c2c58a0e6cd..e6e6ff9448dc9 100644 --- a/components/hippo_video/hippo_video.app.mjs +++ b/components/hippo_video/hippo_video.app.mjs @@ -1,11 +1,110 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "hippo_video", - propDefinitions: {}, + propDefinitions: { + listIds: { + type: "string[]", + label: "List Ids", + description: "A list of lists you want to add the prospect.", + async options({ page }) { + const { contacts_list: data } = await this.listLists({ + params: { + page, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + videoId: { + type: "string", + label: "Video ID", + description: "The ID of the video to be personalized.", + async options({ page }) { + const { videos } = await this.listVideos({ + params: { + page: page + 1, + }, + }); + + return videos.map(({ + id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://www.hippovideo.io"; + }, + _params(params = {}) { + return { + ...params, + email: `${this.$auth.email}`, + api_key: `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, params, ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + params: this._params(params), + headers: { + "Accept": "*/*", + }, + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contacts", + ...opts, + }); + }, + listLists(opts = {}) { + return this._makeRequest({ + path: "/contacts/names", + ...opts, + }); + }, + listVideos(opts = {}) { + return this._makeRequest({ + path: "/api/v1/me/videos/list", + ...opts, + }); + }, + personalizeVideo(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/api/v1/me/video/bulk_personalize", + ...opts, + }); + }, + uploadVideo(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/api/v1/me/video/import", + ...opts, + }); + }, + updateWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhook", + ...opts, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/hippo_video/package.json b/components/hippo_video/package.json index 017dfdfae0fdb..b1fa650fe4391 100644 --- a/components/hippo_video/package.json +++ b/components/hippo_video/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/hippo_video", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Hippo Video Components", "main": "hippo_video.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5", + "form-data": "^4.0.0" } -} \ No newline at end of file +} diff --git a/components/hippo_video/sources/new-triggered-event-instant/new-triggered-event-instant.mjs b/components/hippo_video/sources/new-triggered-event-instant/new-triggered-event-instant.mjs new file mode 100644 index 0000000000000..35c5258e13690 --- /dev/null +++ b/components/hippo_video/sources/new-triggered-event-instant/new-triggered-event-instant.mjs @@ -0,0 +1,67 @@ +import { v4 as uuidv4 } from "uuid"; +import { EVENT_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import hippoVideo from "../../hippo_video.app.mjs"; + +export default { + key: "hippo_video-new-triggered-event-instant", + name: "New Event Triggered (Instant)", + description: "Emit new event when any of the selected events are triggered.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + hippoVideo, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + alert: { + type: "alert", + alertType: "warning", + content: "Please note that you can have only one webhook in your Hippo Video account, any change here will overwrite the current webhook configuration.", + }, + eventIds: { + type: "string[]", + label: "Event Ids", + description: "The Id of the events you want to be triggered.", + options: EVENT_OPTIONS, + }, + }, + methods: { + removeHook() { + this.hippoVideo.updateWebhook({ + data: { + url: this.http.endpoint, + secret_key: uuidv4(), + }, + }); + }, + }, + hooks: { + async activate() { + await this.removeHook(); + await this.hippoVideo.updateWebhook({ + data: { + events: parseObject(this.eventIds), + url: this.http.endpoint, + secret_key: uuidv4(), + }, + }); + }, + async deactivate() { + await this.removeHook(); + }, + }, + async run(event) { + const { body } = event; + const ts = Date.parse(body.eventTimestamp || new Date()); + + this.$emit(body, { + id: body.hook?.uuid || body.video_token, + summary: `New event with event name: ${body.eventName || body.type} successfully triggered!`, + ts: ts, + }); + }, +}; diff --git a/components/hive/README.md b/components/hive/README.md new file mode 100644 index 0000000000000..7725018290e00 --- /dev/null +++ b/components/hive/README.md @@ -0,0 +1,11 @@ +# Overview + +The Hive API allows for robust interaction with Hive's project and team management tools. By leveraging the Hive API within Pipedream, you can automate tasks, sync data across platforms, and create custom workflows that enhance productivity. For instance, you could automate project creation, update task statuses, or sync information to other business tools. Pipedream's serverless platform simplifies integration, enabling you to focus on crafting workflows without worrying about infrastructure. + +# Example Use Cases + +- **Sync Hive Tasks with Google Calendar**: Create a workflow that listens for new tasks in Hive and automatically adds them as events in Google Calendar. This ensures your schedule is always up-to-date with your current tasks and deadlines. + +- **Automate Slack Notifications for Completed Tasks**: Set up a workflow that monitors task statuses in Hive. When a task is marked as complete, it triggers a Pipedream workflow that sends a message to a designated Slack channel, keeping the team informed of progress without manual updates. + +- **Create GitHub Issues from Hive Actions**: Whenever a new action is created in Hive that requires development work, trigger a workflow that creates a corresponding issue in GitHub. This keeps your software development in sync with project management, streamlining the process from planning to coding. diff --git a/components/hiveage/README.md b/components/hiveage/README.md index 62ff6e73a044b..d795b5ec80add 100644 --- a/components/hiveage/README.md +++ b/components/hiveage/README.md @@ -1,13 +1,11 @@ # Overview -With the Hiveage API, you can build a variety of applications and integrations -to help manage your business finances. Here are some examples of what you can -build: - -- A simple invoicing application to create and send invoices to your customers -- A time tracking application to track billable hours and generate invoices -- An expenses tracking application to track business expenses and generate - expense reports -- A payments processing application to process payments from your customers -- An accounting integration to connect your financial data to your accounting - software +The Hiveage API empowers you to automate your invoicing and financial workflows directly within Pipedream. By leveraging this API, you can create, fetch, update, and manage invoices, clients, and payments programmatically. Imagine syncing invoice data with your CRM, triggering notifications upon receiving payments, or compiling financial reports without manual input. This seamless integration with Pipedream enables you to build custom workflows that streamline your billing cycle, enhance data consistency, and save you time. + +# Example Use Cases + +- **Automated Invoice Creation and Dispatch**: Generate invoices based on triggers from e-commerce platforms like Shopify. When a new order is placed, Pipedream can capture the event, create an invoice in Hiveage, and send it to the customer automatically. + +- **Payment Status Monitoring and Alerts**: Set up a workflow to monitor unpaid invoices. When a payment status changes in Hiveage, trigger a Pipedream workflow that sends a Slack message to your finance team or updates a record in Google Sheets, keeping everyone informed in real-time. + +- **Expense Tracking and Reporting**: Whenever a new expense is logged in Hiveage, use Pipedream to add the expense to an Airtable base. Periodically compile these expenses into a report and email it using SendGrid for a clear view of financial health. diff --git a/components/hiveage/package.json b/components/hiveage/package.json new file mode 100644 index 0000000000000..70afb243d051a --- /dev/null +++ b/components/hiveage/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/hiveage", + "version": "0.6.0", + "description": "Pipedream hiveage Components", + "main": "hiveage.app.mjs", + "keywords": [ + "pipedream", + "hiveage" + ], + "homepage": "https://pipedream.com/apps/hiveage", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/holded/README.md b/components/holded/README.md new file mode 100644 index 0000000000000..294fbc768e9a0 --- /dev/null +++ b/components/holded/README.md @@ -0,0 +1,11 @@ +# Overview + +The Holded API offers access to a versatile platform that combines accounting, invoicing, inventory, and project management. It's designed to streamline various business processes. With Pipedream, you can connect Holded to hundreds of other apps to automate workflows. This means you can sync invoices with your CRM, get real-time alerts for new sales, or even automate inventory updates. The API's endpoints allow you to manage contacts, products, accounting, and more, enabling your business to stay organized and efficient. + +# Example Use Cases + +- **Invoice Creation to Slack Notification**: Automate the process of sending notifications to a Slack channel whenever a new invoice is created in Holded. This can keep your team immediately informed about new sales or billing activity. + +- **Contact Sync between Holded and Google Sheets**: Set up a workflow that syncs new contacts added to Holded with a Google Sheets spreadsheet. This can be used for easy review, sharing with team members, or as a simple CRM solution. + +- **Project Management Updates to Trello**: Whenever a project is updated in Holded, automatically create a card on a Trello board with the details. This helps in tracking project progress across different management tools. diff --git a/components/home_assistant/README.md b/components/home_assistant/README.md index 121903c513ec1..8e3a0d1f9bdc5 100644 --- a/components/home_assistant/README.md +++ b/components/home_assistant/README.md @@ -1,13 +1,11 @@ # Overview -With the Home Assistant API, you can build a variety of things, including: - -- A system that automatically turns lights on and off based on whether someone - is home -- A system that controls the temperature of your home based on whether someone - is home -- A system that notifies you when something important happens at home, like a - door being opened or a window being broken -- A system that allows you to control your home's appliances and devices - remotely -- And much more! +The Home Assistant API unlocks the potential to automate and interact with your smart home devices programmatically. With Pipedream, you can tap into this power, triggering workflows based on events in your home, such as motion detection or door openings, and controlling devices based on external data sources or schedules. This integration allows for the creation of personalized, complex scenarios that can enhance security, energy efficiency, and convenience within your smart home. + +# Example Use Cases + +- **Smart Energy Management**: Automate the control of smart thermostats or lights with Home Assistant based on real-time data from weather APIs on Pipedream. For example, adjust the thermostat setting when the outside temperature drops or turn off lights when the local sunrise time is reached. + +- **Intruder Alert System**: Tie in motion sensors or cameras with communication apps like Twilio or Slack on Pipedream. If unexpected movement is detected while the system is armed, Home Assistant can trigger a workflow that sends an SMS or a message to a Slack channel, alerting you immediately. + +- **Effortless Entertainment Setup**: Simplify your movie night by creating a workflow that dims the lights, lowers the blinds, and sets your entertainment system to the right input when you start playing a movie on a service like Plex. This workflow could also pause your media player when a doorbell is pressed, integrating with a device like Ring. diff --git a/components/home_assistant/package.json b/components/home_assistant/package.json new file mode 100644 index 0000000000000..e811e7b8fb03f --- /dev/null +++ b/components/home_assistant/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/home_assistant", + "version": "0.6.0", + "description": "Pipedream home_assistant Components", + "main": "home_assistant.app.mjs", + "keywords": [ + "pipedream", + "home_assistant" + ], + "homepage": "https://pipedream.com/apps/home_assistant", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/home_connect/README.md b/components/home_connect/README.md new file mode 100644 index 0000000000000..22c5effe5ca78 --- /dev/null +++ b/components/home_connect/README.md @@ -0,0 +1,11 @@ +# Overview + +The Home Connect API allows your smart home devices to communicate with Pipedream’s serverless platform, enabling you to create custom automations and interactions. With this API, you can control and monitor your connected home appliances from Bosch, Siemens, Neff, Gaggenau, and Thermador. Fetch appliance status, send commands, and receive event notifications—all programmable in Pipedream to trigger workflows that fit into your digital ecosystem. + +# Example Use Cases + +- **Morning Routine Automation**: Start your day smoothly by creating a workflow that brews your coffee as soon as your morning alarm goes off. Integrate with the Google Calendar API to check your first appointment and ensure the coffee is ready when you are. + +- **Laundry Status Notifications**: Get real-time alerts on your phone or via email when your washing machine cycle completes. Pair Home Connect with Twilio or SendGrid on Pipedream to send these notifications, so you can manage your time without worrying about checking the laundry repeatedly. + +- **Smart Oven Preheating**: Prepare dinner effortlessly by preheating your oven with a single voice command. Use Home Connect with a Voice Assistant service on Pipedream to trigger the oven to preheat to your desired temperature while you focus on prepping the ingredients. diff --git a/components/home_connect/actions/get-appliances/get-appliances.mjs b/components/home_connect/actions/get-appliances/get-appliances.mjs new file mode 100644 index 0000000000000..a217acf53c265 --- /dev/null +++ b/components/home_connect/actions/get-appliances/get-appliances.mjs @@ -0,0 +1,19 @@ +import app from "../../home_connect.app.mjs"; + +export default { + key: "home_connect-get-appliances", + name: "Get Home Appliances", + description: "Retrieves a list of paired home appliances. [See the documentation](https://apiclient.home-connect.com/#/appliances/get_home_appliances)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.getAppliances($); + + $.export("$summary", "Successfully retrieved paired home appliances"); + + return response; + }, +}; diff --git a/components/home_connect/actions/get-programs/get-programs.mjs b/components/home_connect/actions/get-programs/get-programs.mjs new file mode 100644 index 0000000000000..0c276c9e41ca0 --- /dev/null +++ b/components/home_connect/actions/get-programs/get-programs.mjs @@ -0,0 +1,28 @@ +import app from "../../home_connect.app.mjs"; + +export default { + key: "home_connect-get-programs", + name: "Get Available Programs", + description: "Get a list of available programs of a home appliance. [See the documentation](https://api-docs.home-connect.com/programs-and-options/#cleaning-robot_cleaning-mode-option)", + version: "0.0.1", + type: "action", + props: { + app, + haId: { + propDefinition: [ + app, + "haId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getAvailablePrograms({ + $, + haId: this.haId, + }); + + $.export("$summary", `Successfully retrieved available programs for appliance ${this.haId}`); + + return response; + }, +}; diff --git a/components/home_connect/actions/get-status/get-status.mjs b/components/home_connect/actions/get-status/get-status.mjs new file mode 100644 index 0000000000000..11176f38a22ef --- /dev/null +++ b/components/home_connect/actions/get-status/get-status.mjs @@ -0,0 +1,28 @@ +import app from "../../home_connect.app.mjs"; + +export default { + key: "home_connect-get-status", + name: "Get Home Appliance Status", + description: "Gets the status information of a home appliance. [See the documentation](https://api-docs.home-connect.com/general/#best-practices)", + version: "0.0.1", + type: "action", + props: { + app, + haId: { + propDefinition: [ + app, + "haId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getApplianceStatus({ + $, + haId: this.haId, + }); + + $.export("$summary", `Successfully retrieved the status of the home appliance with ID ${this.haId}`); + + return response; + }, +}; diff --git a/components/home_connect/home_connect.app.mjs b/components/home_connect/home_connect.app.mjs index 9e27374f3127b..27790bf0c9c3c 100644 --- a/components/home_connect/home_connect.app.mjs +++ b/components/home_connect/home_connect.app.mjs @@ -1,11 +1,66 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "home_connect", - propDefinitions: {}, + propDefinitions: { + haId: { + type: "string", + label: "Home Appliance ID", + description: "The unique identifier of the home appliance.", + async options() { + const { data: { homeappliances: resources } } = await this.getAppliances(); + + return resources.map(({ + haId, name, + }) => ({ + value: haId, + label: name, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `https://${this.$auth.host_environment}/api`; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + async getAppliances(args = {}) { + return this._makeRequest({ + path: "/homeappliances", + ...args, + }); + }, + async getApplianceStatus({ + haId, ...args + }) { + return this._makeRequest({ + path: `/homeappliances/${haId}/status`, + ...args, + }); + }, + async getAvailablePrograms({ + haId, ...args + }) { + return this._makeRequest({ + path: `/homeappliances/${haId}/programs/available`, + ...args, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/home_connect/package.json b/components/home_connect/package.json index f7011f5862f8a..e6ab2edf42e8f 100644 --- a/components/home_connect/package.json +++ b/components/home_connect/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/home_connect", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Home Connect Components", "main": "home_connect.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" } -} \ No newline at end of file +} diff --git a/components/homerun/actions/add-candidate-note/add-candidate-note.mjs b/components/homerun/actions/add-candidate-note/add-candidate-note.mjs new file mode 100644 index 0000000000000..cfe0cd72a6633 --- /dev/null +++ b/components/homerun/actions/add-candidate-note/add-candidate-note.mjs @@ -0,0 +1,58 @@ +import app from "../../homerun.app.mjs"; + +export default { + key: "homerun-add-candidate-note", + name: "Add Candidate Note", + description: "Adds a note to a candidate's profile in Homerun. [See the documentation](https://developers.homerun.co/#tag/Job-Application-Notes/operation/job-applications.job-application-id.notes.post).", + version: "0.0.1", + type: "action", + props: { + app, + jobApplicationId: { + propDefinition: [ + app, + "jobApplicationId", + ], + }, + note: { + type: "string", + label: "Note Content", + description: "The content of the note to add.", + }, + displayName: { + type: "string", + label: "Display Name", + description: "Name of the note's author.", + }, + }, + methods: { + addCandidateNote({ + jobApplicationId, ...args + } = {}) { + return this.app.post({ + path: `/job-applications/${jobApplicationId}/notes`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + addCandidateNote, + jobApplicationId, + note, + displayName, + } = this; + + const response = await addCandidateNote({ + $, + jobApplicationId, + data: { + note, + display_name: displayName, + }, + }); + + $.export("$summary", "Successfully added a note."); + return response; + }, +}; diff --git a/components/homerun/actions/create-job-application/create-job-application.mjs b/components/homerun/actions/create-job-application/create-job-application.mjs new file mode 100644 index 0000000000000..ca099128a8542 --- /dev/null +++ b/components/homerun/actions/create-job-application/create-job-application.mjs @@ -0,0 +1,144 @@ +import app from "../../homerun.app.mjs"; + +export default { + key: "homerun-create-job-application", + name: "Create Job Application", + description: "Creates a new job application. [See the documentation](https://developers.homerun.co/#tag/Job-Applications/operation/job-applications.post).", + version: "0.0.1", + type: "action", + props: { + app, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the applicant. Make sure you don't include numbers or special characters.", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the applicant. Make sure you don't include numbers or special characters.", + }, + email: { + type: "string", + label: "Email", + description: "The email of the applicant.", + }, + dateOfBirth: { + type: "string", + label: "Date of Birth", + description: "The date of birth of the applicant in the format of `YYYY-MM-DD`.", + optional: true, + }, + vacancyId: { + optional: true, + propDefinition: [ + app, + "vacancyId", + ], + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number of the applicant.", + optional: true, + }, + photo: { + type: "string", + label: "Photo", + description: "The URL of the applicant's photo.", + optional: true, + }, + experience: { + type: "string", + label: "Experience", + description: "The experience of the applicant.", + optional: true, + }, + education: { + type: "string", + label: "Education", + description: "The education of the applicant.", + optional: true, + }, + facebook: { + type: "string", + label: "Facebook", + description: "The Facebook URL of the applicant. eg. `https://facebook.com/username`", + optional: true, + }, + twitter: { + type: "string", + label: "X", + description: "The X URL of the applicant. eg. `https://x.com/username`", + optional: true, + }, + linkedin: { + type: "string", + label: "LinkedIn", + description: "The LinkedIn URL of the applicant. eg. `https://linkedin.com/in/username`", + optional: true, + }, + github: { + type: "string", + label: "GitHub", + description: "The GitHub URL of the applicant. eg. `https://github.com/username`", + optional: true, + }, + website: { + type: "string", + label: "Website", + description: "The website URL of the applicant. eg. `https://username.com`", + optional: true, + }, + }, + methods: { + createJobApplication(args = {}) { + return this.app.post({ + path: "/job-applications", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createJobApplication, + firstName, + lastName, + email, + dateOfBirth, + vacancyId, + phoneNumber, + photo, + experience, + education, + facebook, + twitter, + linkedin, + github, + website, + } = this; + + const response = await createJobApplication({ + $, + data: { + vacancyId, + first_name: firstName, + last_name: lastName, + email, + date_of_birth: dateOfBirth, + phone_number: phoneNumber, + photo, + experience, + education, + facebook, + twitter, + linkedin, + github, + website, + }, + }); + + $.export("$summary", "Successfully created a job application."); + return response; + }, +}; diff --git a/components/homerun/common/constants.mjs b/components/homerun/common/constants.mjs new file mode 100644 index 0000000000000..06aa3cf920c69 --- /dev/null +++ b/components/homerun/common/constants.mjs @@ -0,0 +1,14 @@ +const BASE_URL = "https://api.homerun.co"; +const VERSION_PATH = "/v2"; +const DEFAULT_LIMIT = 100; +const DEFAULT_MAX = 1000; + +const LAST_DATE_AT = "lastDateAt"; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_LIMIT, + DEFAULT_MAX, + LAST_DATE_AT, +}; diff --git a/components/homerun/common/utils.mjs b/components/homerun/common/utils.mjs new file mode 100644 index 0000000000000..de7ee6c4a4692 --- /dev/null +++ b/components/homerun/common/utils.mjs @@ -0,0 +1,17 @@ +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +function getNestedProperty(obj, propertyString) { + const properties = propertyString.split("."); + return properties.reduce((prev, curr) => prev?.[curr], obj); +} + +export default { + iterate, + getNestedProperty, +}; diff --git a/components/homerun/homerun.app.mjs b/components/homerun/homerun.app.mjs new file mode 100644 index 0000000000000..947a394e2bc2e --- /dev/null +++ b/components/homerun/homerun.app.mjs @@ -0,0 +1,143 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; + +export default { + type: "app", + app: "homerun", + propDefinitions: { + vacancyId: { + type: "string", + label: "Vacancy ID", + description: "The ID of the vacancy.", + async options({ page }) { + const { data } = await this.listVacancies({ + params: { + page: page + 1, + }, + }); + return data.map(({ + id: value, + title: label, + }) => ({ + label, + value, + })); + }, + }, + jobApplicationId: { + type: "string", + label: "Job Application ID", + description: "The ID of the job application.", + async options({ page }) { + const { data } = await this.listJobApplications({ + params: { + page: page + 1, + }, + }); + return data.map(({ + id: value, + personal_info: { + first_name: firstName, + last_name: lastName, + email, + }, + }) => ({ + label: `${firstName} ${lastName} (${email})`, + value, + })); + }, + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + Authorization: `Bearer ${this.$auth.api_key}`, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + listVacancies(args = {}) { + return this._makeRequest({ + path: "/vacancies", + ...args, + }); + }, + listJobApplications(args = {}) { + return this._makeRequest({ + path: "/job-applications", + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, resourceName, + lastDateAt, dateField, + max = constants.DEFAULT_MAX, + }) { + let page = 1; + let resourcesCount = 0; + + while (true) { + const response = + await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + page, + perPage: constants.DEFAULT_LIMIT, + }, + }); + + const nextResources = utils.getNestedProperty(response, resourceName); + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + const isDateGreaterThanLasDate = + lastDateAt + && Date.parse(resource[dateField]) > Date.parse(lastDateAt); + + if (!lastDateAt || isDateGreaterThanLasDate) { + yield resource; + resourcesCount += 1; + } + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + if (nextResources.length < constants.DEFAULT_LIMIT) { + console.log("No next page found"); + return; + } + + page += 1; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); + }, + }, +}; diff --git a/components/homerun/package.json b/components/homerun/package.json new file mode 100644 index 0000000000000..f772b26e33911 --- /dev/null +++ b/components/homerun/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/homerun", + "version": "0.1.0", + "description": "Pipedream Homerun Components", + "main": "homerun.app.mjs", + "keywords": [ + "pipedream", + "homerun" + ], + "homepage": "https://pipedream.com/apps/homerun", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/homerun/sources/common/polling.mjs b/components/homerun/sources/common/polling.mjs new file mode 100644 index 0000000000000..976a0a9ff291f --- /dev/null +++ b/components/homerun/sources/common/polling.mjs @@ -0,0 +1,95 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../homerun.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + setLastDateAt(value) { + this.db.set(constants.LAST_DATE_AT, value); + }, + getLastDateAt() { + return this.db.get(constants.LAST_DATE_AT); + }, + getDateField() { + throw new ConfigurationError("getDateField is not implemented"); + }, + isResourceRelevant() { + return true; + }, + getResourceName() { + throw new ConfigurationError("getResourceName is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + async processResources(resources) { + const { + isResourceRelevant, + processResource, + } = this; + + return Array.from(resources) + .filter(isResourceRelevant) + .forEach(processResource); + }, + }, + async run() { + const { + app, + getDateField, + getLastDateAt, + getResourcesFn, + getResourcesFnArgs, + getResourceName, + processResources, + setLastDateAt, + } = this; + + const dateField = getDateField(); + const lastDateAt = getLastDateAt(); + + const resources = await app.paginate({ + resourcesFn: getResourcesFn(), + resourcesFnArgs: getResourcesFnArgs(), + resourceName: getResourceName(), + dateField, + lastDateAt, + }); + + if (resources.length) { + const [ + firstResource, + ] = Array.from(resources).reverse(); + if (firstResource) { + setLastDateAt(firstResource[dateField]); + } + } + + processResources(resources); + }, +}; diff --git a/components/homerun/sources/new-job-application-created/new-job-application-created.mjs b/components/homerun/sources/new-job-application-created/new-job-application-created.mjs new file mode 100644 index 0000000000000..882a61cfc3108 --- /dev/null +++ b/components/homerun/sources/new-job-application-created/new-job-application-created.mjs @@ -0,0 +1,35 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "homerun-new-job-application-created", + name: "New Job Application Created", + description: "Emit new event when a candidate submits an application for a job posting. [See the documentation](https://developers.homerun.co/#tag/Job-Applications/operation/job-applications.index).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getDateField() { + return "created_at"; + }, + getResourceName() { + return "data"; + }, + getResourcesFn() { + return this.app.listJobApplications; + }, + getResourcesFnArgs() { + return {}; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Job Application: ${resource.personal_info.email}`, + ts: Date.parse(resource.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/homerun/sources/new-job-application-created/test-event.mjs b/components/homerun/sources/new-job-application-created/test-event.mjs new file mode 100644 index 0000000000000..92b606c4978a0 --- /dev/null +++ b/components/homerun/sources/new-job-application-created/test-event.mjs @@ -0,0 +1,55 @@ +export default { + "id": "string", + "rating": 0, + "retention_period_ends_at": "2019-08-24T14:15:22Z", + "sourced": true, + "total_votes": 0, + "homerun_url": "string", + "created_at": "2019-08-24T14:15:22Z", + "disqualified": true, + "hired_at": "2019-08-24T14:15:22Z", + "first_reply_at": "2019-08-24T14:15:22Z", + "personal_info": { + "first_name": "string", + "last_name": "string", + "email": "user@example.com", + "city": "string", + "country": "string", + "date_of_birth": "2019-08-24", + "phone_number": "string", + "photo": "string" + }, + "vacancy_id": "string", + "stage": { + "name": "string" + }, + "notes": [ + { + "display_name": "string", + "is_sensitive": true, + "note": "string", + "created_at": "2019-08-24T14:15:22Z", + "updated_at": "2019-08-24T14:15:22Z" + } + ], + "sources": [ + { + "name": "Homerun Career Page" + } + ], + "disqualification_reason": { + "name": "Too junior" + }, + "question_answers": [ + { + "language": "English", + "original_question": "Can you describe your ideal work environment?", + "original_question_type": "file", + "answer": { + "name": "my-resume.pdf", + "type": "resume", + "temporary_download_url": "string" + } + } + ] +}; diff --git a/components/homerun/sources/new-vacancy-created/new-vacancy-created.mjs b/components/homerun/sources/new-vacancy-created/new-vacancy-created.mjs new file mode 100644 index 0000000000000..a96f85389a1bc --- /dev/null +++ b/components/homerun/sources/new-vacancy-created/new-vacancy-created.mjs @@ -0,0 +1,35 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "homerun-new-vacancy-created", + name: "New Vacancy Created", + description: "Emit new event when a new vacancy is created. [See the documentation](https://developers.homerun.co/#tag/Vacancies/operation/vacancies.get).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getDateField() { + return "created_at"; + }, + getResourceName() { + return "data"; + }, + getResourcesFn() { + return this.app.listVacancies; + }, + getResourcesFnArgs() { + return {}; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Vacancy: ${resource.title}`, + ts: Date.parse(resource.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/homerun/sources/new-vacancy-created/test-event.mjs b/components/homerun/sources/new-vacancy-created/test-event.mjs new file mode 100644 index 0000000000000..ceaac36ce9e30 --- /dev/null +++ b/components/homerun/sources/new-vacancy-created/test-event.mjs @@ -0,0 +1,32 @@ +export default { + "id": "string", + "application_form_url": "http://example.com", + "job_url": "http://example.com", + "share_image_url": "http://example.com", + "status": "public", + "title": "string", + "description": "string", + "page_content": "string", + "total_candidate_count": 0, + "type": "Contracted", + "expires_at": "2019-08-24T14:15:22Z", + "is_remote": true, + "location_type": "on-site", + "location": { + "name": "string", + "country": "string", + "address": "string", + "postal_code": "string", + "city": "string", + "region": "string" + }, + "department": { + "name": "string" + }, + "stages": [ + { + "name": "string" + } + ], + "created_at": "2019-08-24T14:15:22Z" +}; diff --git a/components/honeybadger/README.md b/components/honeybadger/README.md index 41b8e239350b4..9283b1f4a9c78 100644 --- a/components/honeybadger/README.md +++ b/components/honeybadger/README.md @@ -1,9 +1,11 @@ # Overview -You can use the Honeybadger API to build: +The Honeybadger API lets you tap into a real-time error tracking and monitoring service designed for web developers. It provides hooks to get notified about errors, track deployments, and manage error occurrences in your apps. With Pipedream, you can harness this API to automate responses to errors, synchronize error data across systems, and create customized alerts that feed into your team's communication channels or task management tools. -- A system to monitor your website for uptime -- A system to monitor your website for errors -- A system to track your website's performance -- A system to collect customer feedback -- A system to generate reports on your website's traffic +# Example Use Cases + +- **Automated Error Response System**: When Honeybadger catches an error, trigger a Pipedream workflow that automatically creates a GitHub issue for bug tracking, notifies the responsible developer via Slack, and logs the error in a Google Sheet for record keeping. This ensures quick response times and centralizes error handling. + +- **Deployment Tracking with Notifications**: Use Pipedream to listen for new deployments through Honeybadger's deployment tracking. On a successful deployment, send a notification through Twilio SMS to key stakeholders and post a summary in a dedicated Discord channel to keep the team informed. + +- **Error Trend Analysis**: Collect error data over time by sending error reports from Honeybadger to an Amazon S3 bucket using Pipedream. Then, use AWS Lambda to analyze trends and trigger alerts or create visualizations with Amazon QuickSight to monitor the health of your applications. diff --git a/components/honeybadger/package.json b/components/honeybadger/package.json new file mode 100644 index 0000000000000..835f97f9df27b --- /dev/null +++ b/components/honeybadger/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/honeybadger", + "version": "0.6.0", + "description": "Pipedream honeybadger Components", + "main": "honeybadger.app.mjs", + "keywords": [ + "pipedream", + "honeybadger" + ], + "homepage": "https://pipedream.com/apps/honeybadger", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/honeyhive/honeyhive.app.mjs b/components/honeyhive/honeyhive.app.mjs new file mode 100644 index 0000000000000..da120cd35b2e2 --- /dev/null +++ b/components/honeyhive/honeyhive.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "honeyhive", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/honeyhive/package.json b/components/honeyhive/package.json new file mode 100644 index 0000000000000..fa5c22cb98d10 --- /dev/null +++ b/components/honeyhive/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/honeyhive", + "version": "0.0.1", + "description": "Pipedream HoneyHive Components", + "main": "honeyhive.app.mjs", + "keywords": [ + "pipedream", + "honeyhive" + ], + "homepage": "https://pipedream.com/apps/honeyhive", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/hookdeck/README.md b/components/hookdeck/README.md new file mode 100644 index 0000000000000..ba3e53ec43694 --- /dev/null +++ b/components/hookdeck/README.md @@ -0,0 +1,11 @@ +# Overview + +The Hookdeck API facilitates the management of webhooks, providing reliable webhook delivery, monitoring, and debugging solutions. With this API on Pipedream, you can automate workflows concerning incoming webhook data—transforming, routing, and ensuring they trigger the subsequent actions without fail. This could range from logging data for analysis, conditionally processing and forwarding webhook events to other endpoints, or integrating with third-party services for extended automation. + +# Example Use Cases + +- **Logging Webhook Data to Google Sheets**: Capture incoming webhooks and log them into a Google Sheet for record-keeping and further analysis. Useful for teams that need to collaborate on webhook data or require an easy-to-access log. + +- **Conditional Processing and Slack Notifications**: Inspect webhook payloads to perform conditional processing. For example, if an error is detected in a webhook event, automatically send a notification with details to a designated Slack channel to alert your team. + +- **Multi-Step Orchestration with Twilio SMS**: Upon receiving a webhook indicating a customer action, use Pipedream to orchestrate a multi-step workflow. This might involve enriching the data with an external API, then sending a custom SMS via Twilio to the customer based on the enriched information. diff --git a/components/hootsuite/README.md b/components/hootsuite/README.md new file mode 100644 index 0000000000000..1391e695ad14e --- /dev/null +++ b/components/hootsuite/README.md @@ -0,0 +1,11 @@ +# Overview + +The Hootsuite API offers a variety of endpoints to automate social media management tasks such as scheduling posts, managing social content, and analyzing social media performance. By leveraging Pipedream, you can create serverless workflows to interact with the Hootsuite API; you can schedule posts at optimal times, aggregate metrics for reporting, or even respond to social media activity in real-time. Pipedream's platform allows the seamless integration of the Hootsuite API with hundreds of other apps to streamline social media workflows, monitor brand presence, and engage with audiences effectively. + +# Example Use Cases + +- **Automated Social Media Posting**: Schedule and post content to multiple social media platforms. Use Pipedream's cron jobs to trigger posts at specific times, ensuring your content hits the right audience at the right time without manual intervention. + +- **Social Media Analytics Dashboard**: Pull analytics data from Hootsuite and send it to a Google Sheets spreadsheet using Pipedream. Automate weekly or monthly reports, giving you insights into engagement rates, follower growth, and content performance without manually compiling data. + +- **Real-Time Social Media Monitoring and Response**: Set up a workflow that listens for specific keywords or brand mentions on social media via Hootsuite and automatically responds or notifies your team in Slack. This encourages timely engagement and helps maintain brand reputation by staying on top of social interactions. diff --git a/components/hostaway/README.md b/components/hostaway/README.md new file mode 100644 index 0000000000000..ce26d871414c7 --- /dev/null +++ b/components/hostaway/README.md @@ -0,0 +1,11 @@ +# Overview + +The Hostaway API allows for automation and integration with their property management platform. It provides endpoints for managing listings, bookings, messages, and more. On Pipedream, you can leverage these endpoints to create automated workflows that connect Hostaway with various other services, such as calendars, email, and messaging apps, or even custom functions for enhanced property management. Whether you're aiming to synchronize booking calendars or automate guest communication, the Hostaway API on Pipedream enables you to streamline operations and enhance guest experiences with ease. + +# Example Use Cases + +- **Sync Hostaway Bookings with Google Calendar**: Automatically create or update events in a Google Calendar when new bookings are made in Hostaway. This can keep your team informed and ensure that everyone's calendar is always up-to-date with the latest booking information. + +- **Automate Guest Communication**: Send personalized welcome emails or messages via SendGrid or Twilio when a new booking is recorded on Hostaway. This workflow can be expanded to send out check-in instructions, local recommendations, or a thank-you message post-checkout, enhancing the guest experience without manual effort. + +- **Aggregate Reviews for Analysis**: Collect reviews from Hostaway and store them in a Google Sheet or send them to a data warehouse like Snowflake. Analyze guest feedback to identify trends and areas for improvement, or use this data to automate responses to recurring questions or concerns. diff --git a/components/hostaway/actions/create-reservation/create-reservation.mjs b/components/hostaway/actions/create-reservation/create-reservation.mjs new file mode 100644 index 0000000000000..8d87be3427ef2 --- /dev/null +++ b/components/hostaway/actions/create-reservation/create-reservation.mjs @@ -0,0 +1,76 @@ +import hostaway from "../../hostaway.app.mjs"; + +export default { + key: "hostaway-create-reservation", + name: "Create Reservation", + description: "Creates a new reservation in Hostaway. [See the documentation](https://api.hostaway.com/documentation#create-a-reservation)", + version: "0.0.1", + type: "action", + props: { + hostaway, + channelId: { + propDefinition: [ + hostaway, + "channelId", + ], + }, + listingMapId: { + propDefinition: [ + hostaway, + "listingId", + ], + }, + arrivalDate: { + type: "string", + label: "Arrival Date", + description: "Arrival date in `YYYY-MM-DD` format, e.g. `2024-08-15`", + }, + departureDate: { + type: "string", + label: "Departure Date", + description: "Departure date in `YYYY-MM-DD` format, e.g. `2024-08-19`", + }, + guestName: { + type: "string", + label: "Guest Name", + description: "Name of the guest", + optional: true, + }, + guestEmail: { + type: "string", + label: "Guest Email", + description: "Email address of the guest", + optional: true, + }, + numberOfGuests: { + type: "integer", + label: "Number of Guests", + description: "Number of guests for the reservation", + optional: true, + }, + additionalFields: { + type: "object", + label: "Additional Fields", + description: "Additional fields to set for the reservation. [See the documentation](https://api.hostaway.com/documentation#reservation-object) for all available fields.", + optional: true, + }, + }, + async run({ $ }) { + const { + hostaway, additionalFields = {}, ...data + } = this; + const { result } = await hostaway.createReservation({ + $, + data: { + ...data, + ...additionalFields, + }, + }); + + if (result?.id) { + $.export("summary", `Successfully created reservation (ID: ${result.id})`); + } + + return result; + }, +}; diff --git a/components/hostaway/actions/create-task/create-task.mjs b/components/hostaway/actions/create-task/create-task.mjs index f00ff2a62ae4a..c9a99b8fbb1e2 100644 --- a/components/hostaway/actions/create-task/create-task.mjs +++ b/components/hostaway/actions/create-task/create-task.mjs @@ -5,7 +5,7 @@ export default { key: "hostaway-create-task", name: "Create Task", description: "Creates a new task in Hostaway. [See the documentation](https://api.hostaway.com/documentation#create-task)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { hostaway, diff --git a/components/hostaway/actions/send-message-to-guest/send-message-to-guest.mjs b/components/hostaway/actions/send-message-to-guest/send-message-to-guest.mjs index c8c96c88eefe1..7d6f625e59849 100644 --- a/components/hostaway/actions/send-message-to-guest/send-message-to-guest.mjs +++ b/components/hostaway/actions/send-message-to-guest/send-message-to-guest.mjs @@ -5,7 +5,7 @@ export default { key: "hostaway-send-message-to-guest", name: "Send Message To Guest", description: "Send a conversation message to a guest in Hostaway. [See the documentation](https://api.hostaway.com/documentation#send-conversation-message)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { hostaway, diff --git a/components/hostaway/actions/update-task/update-task.mjs b/components/hostaway/actions/update-task/update-task.mjs index b215df2bb03a2..87ef60ad2c62d 100644 --- a/components/hostaway/actions/update-task/update-task.mjs +++ b/components/hostaway/actions/update-task/update-task.mjs @@ -6,7 +6,7 @@ export default { key: "hostaway-update-task", name: "Update Task", description: "Updates an existing task in Hostaway. [See the documentation](https://api.hostaway.com/documentation#update-task)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { hostaway, diff --git a/components/hostaway/common/constants.mjs b/components/hostaway/common/constants.mjs index f65b47bc874dc..a2aa0082bf9b8 100644 --- a/components/hostaway/common/constants.mjs +++ b/components/hostaway/common/constants.mjs @@ -42,9 +42,73 @@ const COMMUNICATION_TYPES = [ "whatsapp", ]; +const CHANNEL_OPTIONS = [ + { + value: 2018, + label: "airbnbOfficial", + }, + { + value: 2002, + label: "homeaway", + }, + { + value: 2005, + label: "bookingcom", + }, + { + value: 2007, + label: "expedia", + }, + { + value: 2009, + label: "homeawayical", + }, + { + value: 2010, + label: "vrboical", + }, + { + value: 2000, + label: "direct", + }, + { + value: 2013, + label: "bookingengine", + }, + { + value: 2015, + label: "customIcal", + }, + { + value: 2016, + label: "tripadvisorical", + }, + { + value: 2017, + label: "wordpress", + }, + { + value: 2019, + label: "marriott", + }, + { + value: 2020, + label: "partner", + }, + { + value: 2021, + label: "gds", + }, + { + value: 2022, + label: "google", + }, +]; + export default { DEFAULT_LIMIT, CATEGORIES, TASK_STATUS, COMMUNICATION_TYPES, + CHANNEL_OPTIONS, }; diff --git a/components/hostaway/hostaway.app.mjs b/components/hostaway/hostaway.app.mjs index 67d5440ab9e05..53c9d56ba16df 100644 --- a/components/hostaway/hostaway.app.mjs +++ b/components/hostaway/hostaway.app.mjs @@ -118,6 +118,12 @@ export default { })) || []; }, }, + channelId: { + type: "integer", + label: "Channel", + description: "Identifier of the channel", + options: constants.CHANNEL_OPTIONS, + }, }, methods: { _baseUrl() { @@ -211,5 +217,12 @@ export default { ...args, }); }, + createReservation(args = {}) { + return this._makeRequest({ + path: "/reservations", + method: "POST", + ...args, + }); + }, }, }; diff --git a/components/hostaway/package.json b/components/hostaway/package.json index e02b92079800b..cb4bda8897403 100644 --- a/components/hostaway/package.json +++ b/components/hostaway/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/hostaway", - "version": "0.1.0", + "version": "0.2.0", "description": "Pipedream Hostaway Components", "main": "hostaway.app.mjs", "keywords": [ diff --git a/components/hostaway/sources/common/common.mjs b/components/hostaway/sources/common/common.mjs index 00e4327421c4a..763e55849d9b9 100644 --- a/components/hostaway/sources/common/common.mjs +++ b/components/hostaway/sources/common/common.mjs @@ -44,12 +44,13 @@ export default { }, async run(event) { const { body } = event; - if (!body || body?.data === "test") { - this.http.respond({ - status: 200, - }); - return; - } + + this.http.respond({ + status: 200, + }); + + if (!body || body?.data === "test") return; + const { event: eventType, data, } = body; diff --git a/components/hostaway/sources/new-message-received/new-message-received.mjs b/components/hostaway/sources/new-message-received/new-message-received.mjs index 2bb97c3af3f9c..819e3c3417af2 100644 --- a/components/hostaway/sources/new-message-received/new-message-received.mjs +++ b/components/hostaway/sources/new-message-received/new-message-received.mjs @@ -5,7 +5,7 @@ export default { key: "hostaway-new-message-received", name: "New Message Received", description: "Emit new event when a new message is received in Hostaway.", - version: "0.0.1", + version: "0.0.3", type: "source", dedupe: "unique", methods: { diff --git a/components/hostaway/sources/reservation-created/reservation-created.mjs b/components/hostaway/sources/reservation-created/reservation-created.mjs index 352b1957f6416..28cc0d4fa4f77 100644 --- a/components/hostaway/sources/reservation-created/reservation-created.mjs +++ b/components/hostaway/sources/reservation-created/reservation-created.mjs @@ -3,9 +3,9 @@ import common from "../common/common.mjs"; export default { ...common, key: "hostaway-reservation-created", - name: "Reservation Created", + name: "New Reservation Created", description: "Emit new event when a new reservation is created in Hostaway.", - version: "0.0.1", + version: "0.0.3", type: "source", dedupe: "unique", methods: { diff --git a/components/hostaway/sources/reservation-updated/reservation-updated.mjs b/components/hostaway/sources/reservation-updated/reservation-updated.mjs index e6209ae76edbe..14d34647fa9ee 100644 --- a/components/hostaway/sources/reservation-updated/reservation-updated.mjs +++ b/components/hostaway/sources/reservation-updated/reservation-updated.mjs @@ -3,9 +3,9 @@ import common from "../common/common.mjs"; export default { ...common, key: "hostaway-reservation-updated", - name: "Reservation Updated", + name: "New Reservation Updated", description: "Emit new event when a reservation is updated in Hostaway.", - version: "0.0.1", + version: "0.0.3", type: "source", dedupe: "unique", methods: { diff --git a/components/hotjar/README.md b/components/hotjar/README.md new file mode 100644 index 0000000000000..bed0acebe4b69 --- /dev/null +++ b/components/hotjar/README.md @@ -0,0 +1,11 @@ +# Overview + +The Hotjar API lets you tap into real-time feedback and user behavior analytics. With Pipedream, you can automate workflows using this data to enhance user experience, respond to feedback quickly, and integrate user insights into your broader ecosystem of tools. By crafting custom functions or responding to webhooks in Pipedream, you can connect Hotjar with other services to create seamless, data-driven processes. + +# Example Use Cases + +- **Trigger Email Campaigns Based on User Feedback**: When Hotjar captures negative user feedback, trigger an automated email outreach in a service like Mailchimp or SendGrid from Pipedream. This workflow can help you address concerns proactively and improve customer satisfaction. + +- **Create Trello Cards for User Issues**: Use the Hotjar API to monitor for specific user issues or feedback, and automatically create Trello cards for each issue. This can help your product or customer service team prioritize and track user-reported problems efficiently. + +- **Sync User Feedback to Google Sheets**: Set up a workflow that sends new user feedback from Hotjar directly to a Google Sheets document. Use this to analyze trends, share insights with your team, and maintain a living document of user experiences. diff --git a/components/hotjar/package.json b/components/hotjar/package.json index 10a2b00f584db..c4c0730a00af7 100644 --- a/components/hotjar/package.json +++ b/components/hotjar/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/hotmart/.gitignore b/components/hotmart/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/hotmart/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/hotmart/README.md b/components/hotmart/README.md new file mode 100644 index 0000000000000..3b772da9417a6 --- /dev/null +++ b/components/hotmart/README.md @@ -0,0 +1,11 @@ +# Overview + +The Hotmart API connects you to a platform where creators sell and distribute digital products. With Pipedream, you can automate actions within Hotmart or sync its data with other apps. For example, you might automate participant registration for a course after a new sale or update customer info across your CRM system. The API provides endpoints to manage products, sales, and affiliates, offering vast possibilities for integration and automation. + +# Example Use Cases + +- **Automate Participant Enrollment**: When a new sale is completed in Hotmart, trigger a Pipedream workflow to enroll the customer in the corresponding online course platform, like Teachable or Thinkific. + +- **Sync Sales Data to Google Sheets**: Keep your finance team updated by pushing new sales data from Hotmart into a Google Sheets spreadsheet. Use this data for real-time financial reporting or sales analysis. + +- **Update CRM with New Customer Info**: After a sale, use a Pipedream workflow to add or update the customer's information in your CRM, such as HubSpot. This ensures your customer relations team always has the latest data. diff --git a/components/hotmart/actions/get-comissions/get-comissions.mjs b/components/hotmart/actions/get-comissions/get-comissions.mjs new file mode 100644 index 0000000000000..e6ba898b9a87f --- /dev/null +++ b/components/hotmart/actions/get-comissions/get-comissions.mjs @@ -0,0 +1,55 @@ +import app from "../../hotmart.app.mjs"; + +export default { + key: "hotmart-get-comissions", + name: "Get Comissions", + description: "Get sales commission information for sale participants. [See the documentation](https://developers.hotmart.com/docs/pt-BR/v1/sales/sales-commissions/)", + version: "0.0.1", + type: "action", + props: { + app, + productId: { + propDefinition: [ + app, + "productId", + ], + }, + startDate: { + propDefinition: [ + app, + "startDate", + ], + }, + endDate: { + propDefinition: [ + app, + "endDate", + ], + }, + }, + async run({ $ }) { + let results = []; + let stop = false; + + while (!stop) { + const { + items, page_info: { total_results }, + } = await this.app.getComissions({ + $, + params: { + product_id: this.productId, + start_date: this.startDate, + end_date: this.endDate, + }, + }); + + results = results.concat(items); + + stop = total_results <= results.length; + } + + $.export("$summary", `Successfully retrieved ${results.length} comission(s)`); + + return results; + }, +}; diff --git a/components/hotmart/actions/get-sales-history/get-sales-history.mjs b/components/hotmart/actions/get-sales-history/get-sales-history.mjs new file mode 100644 index 0000000000000..2d7297fbc42f3 --- /dev/null +++ b/components/hotmart/actions/get-sales-history/get-sales-history.mjs @@ -0,0 +1,56 @@ +import app from "../../hotmart.app.mjs"; + +export default { + key: "hotmart-get-sales-history", + name: "Get Sales History", + description: "Retrieve sales history from the Hotmart account. [See the documentation](https://developers.hotmart.com/docs/pt-BR/v1/sales/sales-history/)", + version: "0.0.1", + type: "action", + props: { + app, + productId: { + propDefinition: [ + app, + "productId", + ], + }, + buyerEmail: { + propDefinition: [ + app, + "buyerEmail", + ], + }, + offerCode: { + propDefinition: [ + app, + "offerCode", + ], + }, + }, + async run({ $ }) { + let results = []; + let stop = false; + + while (!stop) { + const { + items, page_info: { total_results }, + } = await this.app.getSalesHistory({ + $, + params: { + product_id: this.productId, + buyer_email: this.buyerEmail, + offer_code: this.offerCode, + }, + subscriber_code: this.subscriberCode, + }); + + results = results.concat(items); + + stop = total_results <= results.length; + } + + $.export("$summary", `Successfully retrieved ${results.length} sale(s)`); + + return results; + }, +}; diff --git a/components/hotmart/actions/get-subscriptions/get-subscriptions.mjs b/components/hotmart/actions/get-subscriptions/get-subscriptions.mjs new file mode 100644 index 0000000000000..9a4ae8c145d1b --- /dev/null +++ b/components/hotmart/actions/get-subscriptions/get-subscriptions.mjs @@ -0,0 +1,30 @@ +import app from "../../hotmart.app.mjs"; + +export default { + key: "hotmart-get-subscriptions", + name: "Get Subscriptions", + description: "Get subscribers from hotmart profile. [See the documentation](https://developers.hotmart.com/docs/pt-BR/v1/subscription/get-subscribers/)", + version: "0.0.1", + type: "action", + props: { + app, + productId: { + propDefinition: [ + app, + "productId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getSubscriptions({ + $, + params: { + product_id: this.productId, + }, + }); + + $.export("$summary", `Successfully retrieved ${response.items.length} subscription(s)`); + + return response; + }, +}; diff --git a/components/hotmart/app/hotmart.app.ts b/components/hotmart/app/hotmart.app.ts deleted file mode 100644 index 69f74aaab0709..0000000000000 --- a/components/hotmart/app/hotmart.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "hotmart", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/hotmart/hotmart.app.mjs b/components/hotmart/hotmart.app.mjs new file mode 100644 index 0000000000000..506d34d2eaf3d --- /dev/null +++ b/components/hotmart/hotmart.app.mjs @@ -0,0 +1,93 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "hotmart", + propDefinitions: { + offerCode: { + type: "string", + label: "Offer Code", + description: "Code of the offer", + optional: true, + }, + buyerEmail: { + type: "string", + label: "Buyer Email", + description: "Email of the buyer", + optional: true, + }, + productId: { + type: "string", + label: "Product ID", + description: "ID of the product", + optional: true, + }, + startDate: { + type: "string", + label: "Start Date", + description: "Start date for the query, in milliseconds", + optional: true, + }, + endDate: { + type: "string", + label: "End Date", + description: "End date for the query, in milliseconds", + optional: true, + }, + subscriberCode: { + type: "string", + label: "Subscriber Code", + description: "ID code of the subscriber", + async options() { + const response = await this.getSubscriprions({}); + const accountIDs = response.items; + return accountIDs.map(({ + subscriber_code, subscriber, + }) => ({ + value: subscriber_code, + label: subscriber.name, + })); + }, + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.environment}.hotmart.com/payments/api/v1`; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "Content-Type": "application/json", + }, + }); + }, + async getSubscriptions(args = {}) { + return this._makeRequest({ + path: "/subscriptions", + ...args, + }); + }, + async getComissions(args = {}) { + return this._makeRequest({ + path: "/sales/commissions", + ...args, + }); + }, + async getSalesHistory(args = {}) { + return this._makeRequest({ + path: "/sales/history", + ...args, + }); + }, + }, +}; diff --git a/components/hotmart/package.json b/components/hotmart/package.json index f92046d769fb9..fcaad941c8f05 100644 --- a/components/hotmart/package.json +++ b/components/hotmart/package.json @@ -1,16 +1,19 @@ { "name": "@pipedream/hotmart", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Hotmart Components", - "main": "dist/app/hotmart.app.mjs", + "main": "hotmart.app.mjs", "keywords": [ "pipedream", "hotmart" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/hotmart", "author": "Pipedream (https://pipedream.com/)", + "dependencies": { + "@pipedream/platform": "^1.6.4" + }, + "gitHead": "e12480b94cc03bed4808ebc6b13e7fdb3a1ba535", "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/hotspotsystem/README.md b/components/hotspotsystem/README.md index edfedb8ed5767..3a6d8a02fb778 100644 --- a/components/hotspotsystem/README.md +++ b/components/hotspotsystem/README.md @@ -1,18 +1,11 @@ # Overview -HotspotSystem offers a powerful and easy to use API that enables you to build a -wide range of applications and integrations on top of our hotspot platform. +The HotspotSystem API enables automation of hotspot management tasks, such as creating and managing user accounts, adjusting access packages, and retrieving usage statistics. Leveraging Pipedream's capabilities, you can build powerful workflows to interact with this API, automating tasks that otherwise would take considerable manual effort. Use Pipedream to integrate HotspotSystem with a myriad of services for notifications, data analysis, customer management, and more, all in real-time. -Some examples of what you can build using the HotspotSystem API: +# Example Use Cases -- A custom hotspot sign-up page that integrated with your existing website or - application -- A custom hotspot splash page that branded with your own logo and branding -- A custom reporting tool that allows you to track hotspot usage and - performance -- A custom application that allows you to manage hotspot settings and users - from a central location +- **Automated User Onboarding**: Create a workflow that triggers when a new user signs up via your app. Use the HotspotSystem API to automatically create a hotspot account and assign a predefined access package for the user. Then, integrate with an email service like SendGrid on Pipedream to send personalized welcome emails containing their new hotspot login details. -The possibilities are endless - With the HotspotSystem API you can build the -perfect solution for your specific needs. Contact us to find out more about how -we can help you get the most out of our hotspot platform. +- **Usage Monitoring and Alerts**: Set up a Pipedream workflow that periodically checks user data consumption through the HotspotSystem API. If a user hits a data limit, trigger an alert using Twilio to send an SMS to the user, and optionally update their package or suspend service until the next billing cycle. + +- **Customer Support Ticket Integration**: Implement a system where if a user reports an issue via a support ticket on a platform like Zendesk, Pipedream can trigger a workflow that queries HotspotSystem for the user's connection logs and status. This information can be appended to the support ticket, giving customer service reps immediate context to troubleshoot issues faster. diff --git a/components/howuku/README.md b/components/howuku/README.md new file mode 100644 index 0000000000000..89e2eaf4846c1 --- /dev/null +++ b/components/howuku/README.md @@ -0,0 +1,11 @@ +# Overview + +The Howuku API offers tools for website analytics, feedback collection, and customer engagement. Leveraging Howuku on Pipedream allows you to automate responses to customer interactions, analyze web traffic in real-time, and integrate with other services for enhanced data processing and action triggers. It's a powerful way to streamline your user experience analysis, gain insights from user feedback, and push data to other platforms for extended functionality. + +# Example Use Cases + +- **Sync Feedback with a CRM**: Automatically push user feedback collected via Howuku into your CRM software, such as Salesforce or HubSpot. This workflow can tag feedback for follow-up, aid in customer support, or inform sales strategies. + +- **Real-time Alerting for User Issues**: Set up alerts in communication platforms like Slack or Discord when Howuku detects an issue on your site, such as a surge in 404 errors or a significant drop in user engagement, enabling immediate action. + +- **Aggregate Analytics Data**: Compile analytics data from Howuku into a Google Sheets or Airtable database for advanced analysis or reporting. Use this workflow to monitor trends over time or share insights with team members who may not have direct access to the Howuku dashboard. diff --git a/components/howuku/actions/get-survey-responses/get-survey-responses.mjs b/components/howuku/actions/get-survey-responses/get-survey-responses.mjs new file mode 100644 index 0000000000000..08fb878347f50 --- /dev/null +++ b/components/howuku/actions/get-survey-responses/get-survey-responses.mjs @@ -0,0 +1,44 @@ +import howuku from "../../howuku.app.mjs"; + +export default { + key: "howuku-get-survey-responses", + name: "Get Survey Responses", + description: "Retrieves a list of survey responses from Howuku. [See the documentation](https://rest.howuku.com/#survey)", + version: "0.0.1", + type: "action", + props: { + howuku, + startDate: { + type: "string", + label: "Start Date", + description: "The result will return survey responses after the start date. Enter in ISO 8601 format. Example: `2020-10-27T08:30:28.000Z`", + optional: true, + }, + endDate: { + type: "string", + label: "End Date", + description: "The result will return survey responses before the end date. Enter in ISO 8601 format. Example: `2020-10-27T08:30:28.000Z`", + optional: true, + }, + limit: { + type: "integer", + label: "Limit", + description: "The result will return latest X number of specified number of heatmaps", + default: 50, + }, + }, + async run({ $ }) { + const response = await this.howuku.listSurveys({ + $, + params: { + startdate: this.startDate, + enddate: this.endDate, + limit: this.limit, + }, + }); + $.export("$summary", `Successfully retrieved ${response.length} survey${response.length === 1 + ? "" + : "s"}`); + return response; + }, +}; diff --git a/components/howuku/howuku.app.mjs b/components/howuku/howuku.app.mjs new file mode 100644 index 0000000000000..22f61ac620bab --- /dev/null +++ b/components/howuku/howuku.app.mjs @@ -0,0 +1,32 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "howuku", + propDefinitions: {}, + methods: { + _baseUrl() { + return "https://api.howuku.com/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `${this.$auth.api_key}`, + }, + }); + }, + listSurveys(opts = {}) { + return this._makeRequest({ + path: "/survey", + ...opts, + }); + }, + }, +}; diff --git a/components/howuku/package.json b/components/howuku/package.json new file mode 100644 index 0000000000000..da21dd86c5ea0 --- /dev/null +++ b/components/howuku/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/howuku", + "version": "0.1.0", + "description": "Pipedream Howuku Components", + "main": "howuku.app.mjs", + "keywords": [ + "pipedream", + "howuku" + ], + "homepage": "https://pipedream.com/apps/howuku", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/howuku/sources/common/base.mjs b/components/howuku/sources/common/base.mjs new file mode 100644 index 0000000000000..5d5910d8a8b0e --- /dev/null +++ b/components/howuku/sources/common/base.mjs @@ -0,0 +1,62 @@ +import howuku from "../../howuku.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + howuku, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastDateTime() { + return this.db.get("lastDateTime"); + }, + _setLastDateTime(lastDateTime) { + this.db.set("lastDateTime", lastDateTime); + }, + async processEvent(limit) { + const lastDateTime = this._getLastDateTime(); + let maxDateTime = lastDateTime; + const resourceFn = this.getResourceFn(); + const params = limit + ? { + limit, + } + : { + startdate: lastDateTime, + }; + const items = await resourceFn({ + params, + }); + for (const item of items) { + if (!lastDateTime || Date.parse(item.dt) > Date.parse(lastDateTime)) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + if (Date.parse(item.dt) > Date.parse(maxDateTime)) { + maxDateTime = item.dt; + } + } + } + this._setLastDateTime(maxDateTime); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/howuku/sources/new-incoming-survey/new-incoming-survey.mjs b/components/howuku/sources/new-incoming-survey/new-incoming-survey.mjs new file mode 100644 index 0000000000000..1750b948e707e --- /dev/null +++ b/components/howuku/sources/new-incoming-survey/new-incoming-survey.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "howuku-new-incoming-survey", + name: "New Incoming Survey", + description: "Emit new event when a new incoming survey is received. [See the documentation](https://rest.howuku.com/#survey)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.howuku.listSurveys; + }, + generateMeta(survey) { + const ts = Date.parse(survey.dt); + return { + id: `${survey.id}${ts}`, + summary: `New response for survey: ${survey.question_id}`, + ts, + }; + }, + }, +}; diff --git a/components/hr_partner/README.md b/components/hr_partner/README.md new file mode 100644 index 0000000000000..c3043c07633e9 --- /dev/null +++ b/components/hr_partner/README.md @@ -0,0 +1,11 @@ +# Overview + +The HR Partner API provides a suite of tools to manage employee records, leave requests, recruitment processes, and more in a centralized HR system. With Pipedream, you can harness this API to create automated workflows that streamline HR tasks, sync data with other business systems, and trigger actions based on employee activities. The API enables you to programmatically interact with HR Partner's platform, allowing you to create, read, update, and delete various HR-related data points. + +# Example Use Cases + +- **Employee Onboarding Automation**: Automate the onboarding process by triggering a workflow when a new employee is added in HR Partner. The workflow can send welcome emails, create accounts in third-party services like Slack or Trello, and schedule introductory meetings in Google Calendar. + +- **Leave Request Notification**: Set up a workflow that notifies managers or team members via Slack or email when an employee submits a leave request in HR Partner. The automation can include details of the leave request for quick review and action. + +- **Recruitment Process Management**: Streamline the hiring process by automating candidate tracking. When a new candidate is added in HR Partner, trigger a Pipedream workflow to add the candidate's information to a Google Sheets spreadsheet, send task assignments to the hiring team, and schedule interviews in the team's calendars. diff --git a/components/hr_partner/package.json b/components/hr_partner/package.json index 23433babeaef4..ca64408b7f82d 100644 --- a/components/hr_partner/package.json +++ b/components/hr_partner/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/html_2_pdf/README.md b/components/html_2_pdf/README.md new file mode 100644 index 0000000000000..0c91efc631fe4 --- /dev/null +++ b/components/html_2_pdf/README.md @@ -0,0 +1,11 @@ +# Overview + +The HTML 2 PDF API allows you to convert HTML documents to PDFs. In Pipedream, you can integrate this API into workflows to automate document generation tasks. This can be incredibly useful for generating reports, invoices, or any other document where you start with HTML and need a PDF output. You can trigger these conversions with various events – like a new form submission, a scheduled trigger, or even updates in a database. + +# Example Use Cases + +- **Scheduled Report Generation**: Combine HTML 2 PDF with Pipedream's cron trigger to automate weekly or monthly report generation. Start with dynamic HTML templates populated with the latest data from your database using a SQL query in Pipedream, then convert these to PDFs for distribution. + +- **Invoice Creation Upon New Orders**: Upon a new order in an eCommerce app like Shopify, trigger a workflow in Pipedream that generates an HTML invoice with the order details and then use HTML 2 PDF to convert this invoice into a PDF. Email the PDF to the customer or store it in cloud storage like Google Drive. + +- **Form Submission to PDF Conversion**: When a new form submission is received via Typeform or Google Forms, use a Pipedream workflow to format the submission into an HTML document. Then, with HTML 2 PDF, convert the submission into a PDF for archival purposes or to kick off an approval process. diff --git a/components/html_2_pdf/actions/generate-pdf/generate-pdf.mjs b/components/html_2_pdf/actions/generate-pdf/generate-pdf.mjs new file mode 100644 index 0000000000000..54ef6699b33b8 --- /dev/null +++ b/components/html_2_pdf/actions/generate-pdf/generate-pdf.mjs @@ -0,0 +1,178 @@ +import { ConfigurationError } from "@pipedream/platform"; +import fs from "fs"; +import { + PAGE_SIZE_OPTIONS, UNIT_OPTIONS, +} from "../../common/constants.mjs"; +import html2Pdf from "../../html_2_pdf.app.mjs"; + +export default { + key: "html_2_pdf-generate-pdf", + name: "Generate PDF", + description: "Creates a PDF from a URL or HTML string. [See the documentation](https://www.html2pdf.co.uk/api-documentation)", + version: "0.0.1", + type: "action", + props: { + html2Pdf, + url: { + type: "string", + label: "URL", + description: "The URL you want to convert to PDF.", + optional: true, + }, + html: { + type: "string", + label: "HTML", + description: "The HTML you want to convert to PDF.", + optional: true, + }, + header: { + type: "string", + label: "Header", + description: "Header in HTML format.", + optional: true, + }, + footer: { + type: "string", + label: "Footer", + description: "Footer in HTML format.", + optional: true, + }, + pageOffset: { + type: "integer", + label: "Page Offset", + description: "Number to offset your page number", + optional: true, + }, + pageSize: { + type: "string", + label: "Page Size", + description: "The standard page formats.", + optional: true, + options: PAGE_SIZE_OPTIONS, + }, + orientation: { + type: "string", + label: "Orientation", + description: "The page orientation.", + optional: true, + options: [ + "portrait", + "landscape", + ], + }, + width: { + type: "string", + label: "Width", + description: "Custom width (overrides 'Page Size').", + optional: true, + }, + height: { + type: "string", + label: "Height", + description: "Custom height (overrides 'Page Size').", + optional: true, + }, + top: { + type: "string", + label: "Top Margin", + description: "You need one if you want to display a header.", + optional: true, + }, + bottom: { + type: "string", + label: "Bottom Margin", + description: "You need one if you want to display a footer.", + optional: true, + }, + left: { + type: "string", + label: "Left", + description: "Left margin.", + optional: true, + }, + right: { + type: "string", + label: "Right", + description: "Right margin.", + optional: true, + }, + unit: { + type: "string", + label: "Unit", + description: "Unit for the custom width, height and margins.", + optional: true, + options: UNIT_OPTIONS, + }, + cssMediaType: { + type: "string", + label: "CSS Media Type", + description: "The layout CSS media type.", + options: [ + "print", + "screen", + ], + optional: true, + }, + optimizeLayout: { + type: "boolean", + label: "Optimize Layout", + description: "Set this to **true** if you want us to auto-optimize your layout. This helps static items on your page to only show up once, instead of on every page.", + optional: true, + }, + lazyLoad: { + type: "boolean", + label: "Lazy Load", + description: "Set this to **true** if you want our software to wait for content to lazy load.", + optional: true, + }, + waitTime: { + type: "integer", + label: "Wait Time", + description: "**Time in milliseconds** that you want our software to wait after the page is loaded. You typically use this when you have long running JavaScript for graphs and tables.", + optional: true, + }, + css: { + type: "string", + label: "CSS", + description: "Use custom CSS to overwrite the default styles of your page.", + optional: true, + }, + }, + async run({ $ }) { + if ((!this.url && !this.html) || (this.url && this.html)) { + throw new ConfigurationError("You must provide either URL or HTML."); + } + const response = await this.html2Pdf.createPdf({ + $, + responseType: "arraybuffer", + params: { + url: this.url, + html: this.html, + header: this.header, + footer: this.footer, + page_offset: this.pageOffset, + page_size: this.pageSize, + orientation: this.orientation, + width: this.width, + height: this.height, + top: this.top, + bottom: this.bottom, + left: this.left, + right: this.right, + unit: this.unit, + css_media_type: this.cssMediaType, + optimize_layout: this.optimizeLayout, + lazy_load: this.lazyLoad, + wait_time: this.waitTime, + css: this.css, + }, + }); + + const filePath = `/tmp/${Date.now()}.pdf`; + fs.writeFileSync(filePath, response); + + $.export("$summary", "Generated PDF is saved at"); + $.export("file_path", filePath); + return filePath; + }, +}; diff --git a/components/html_2_pdf/common/constants.mjs b/components/html_2_pdf/common/constants.mjs new file mode 100644 index 0000000000000..656f9789ec16d --- /dev/null +++ b/components/html_2_pdf/common/constants.mjs @@ -0,0 +1,141 @@ +export const UNIT_OPTIONS = [ + { + label: "Millimeters (Default)", + value: "mm", + }, + { + label: "Centimeters", + value: "cm", + }, + { + label: "Inches", + value: "in", + }, + { + label: "Pixels", + value: "px", + }, +]; + +export const PAGE_SIZE_OPTIONS = [ + { + value: "A0", + label: "A0 - 841 x 1189 mm, 33.1 x 46.8 in", + }, + { + value: "A1", + label: "A1 - 594 x 841 mm, 23.4 x 33.1 in", + }, + { + value: "A2", + label: "A2 - 420 x 594 mm, 16.5 x 23.4 in", + }, + { + value: "A3", + label: "A3 - 297 x 420 mm, 11.7 x 16.5 in", + }, + { + value: "A4", + label: "A4 - 210 x 297 mm, 8.3 x 11.7 in", + }, + { + value: "A5", + label: "A5 - 148 x 210 mm, 5.8 x 8.3 in", + }, + { + value: "A6", + label: "A6 - 105 x 148 mm, 4.1 x 5.8 in", + }, + { + value: "A7", + label: "A7 - 74 x 105 mm, 2.9 x 4.1 in", + }, + { + value: "A8", + label: "A8 - 52 x 74 mm, 2.1 x 2.9 in", + }, + { + value: "A9", + label: "A9 - 37 x 52 mm, 1.5 x 2.1 in", + }, + { + value: "B0", + label: "B0 - 1000 x 1414 mm, 39.4 x 55.7 in", + }, + { + value: "B1", + label: "B1 - 707 x 1000 mm, 27.8 x 39.4 in", + }, + { + value: "B2", + label: "B2 - 500 x 707 mm, 19.7 x 27.8 in", + }, + { + value: "B3", + label: "B3 - 353 x 500 mm, 13.9 x 19.7 in", + }, + { + value: "B4", + label: "B4 - 250 x 353 mm, 9.8 x 13.9 in", + }, + { + value: "B5", + label: "B5 - 176 x 250 mm, 6.9 x 9.8 in", + }, + { + value: "B6", + label: "B6 - 125 x 176 mm, 4.9 x 6.9 in", + }, + { + value: "B7", + label: "B7 - 88 x 125 mm, 3.5 x 4.9 in", + }, + { + value: "B8", + label: "B8 - 62 x 88 mm, 2.4 x 3.5 in", + }, + { + value: "B9", + label: "B9 - 44 x 62 mm, 1.7 x 2.4 in", + }, + { + value: "B10", + label: "B10 - 31 x 44 mm, 1.2 x 1.7 in", + }, + { + value: "C5E", + label: "C5E - 163 x 229 mm", + }, + { + value: "Comm10E", + label: "Comm10E - 105 x 241 mm, U.S. Common 10 Envelope", + }, + { + value: "DLE", + label: "DLE - 110 x 220 mm", + }, + { + value: "Executive", + label: "Executive - 7.25 x 10.5 in", + }, + { + value: "Folio", + label: "Folio - 210 x 330 mm", + }, + { + value: "Ledger", + label: "Ledger - 17 x 11 in", + }, + { + value: "Legal", + label: "Legal - 8.5 x 14.0 in", + }, + { + value: "Letter", + label: "Letter - 8.5 x 11.0 in", + }, + { + value: "Tabloid", + label: "Tabloid - 11 x 17 in", + }, +]; diff --git a/components/html_2_pdf/html_2_pdf.app.mjs b/components/html_2_pdf/html_2_pdf.app.mjs new file mode 100644 index 0000000000000..6683a6197e206 --- /dev/null +++ b/components/html_2_pdf/html_2_pdf.app.mjs @@ -0,0 +1,60 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "html_2_pdf", + propDefinitions: { + source: { + type: "string", + label: "Source Type", + description: "Indicates whether the input is a URL or HTML string.", + options: [ + { + label: "URL", + value: "url", + }, + { + label: "HTML", + value: "html", + }, + ], + }, + content: { + type: "string", + label: "Content", + description: "The URL or HTML string to convert to PDF.", + }, + licenseKey: { + type: "string", + label: "License Key", + description: "Your HTML2PDF license key.", + secret: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.html2pdf.co.uk"; + }, + _params(params) { + return { + ...params, + "license": `${this.$auth.license_key}`, + }; + }, + _makeRequest({ + $ = this, params, ...opts + }) { + return axios($, { + url: this._baseUrl(), + params: this._params(params), + ...opts, + }); + }, + async createPdf(opts = {}) { + return this._makeRequest({ + method: "POST", + ...opts, + }); + }, + }, +}; diff --git a/components/html_2_pdf/package.json b/components/html_2_pdf/package.json new file mode 100644 index 0000000000000..ddd0bcc8af3c7 --- /dev/null +++ b/components/html_2_pdf/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/html_2_pdf", + "version": "0.1.0", + "description": "Pipedream HTML 2 PDF Components", + "main": "html_2_pdf.app.mjs", + "keywords": [ + "pipedream", + "html_2_pdf" + ], + "homepage": "https://pipedream.com/apps/html_2_pdf", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "fs": "^0.0.1-security", + "@pipedream/platform": "^1.5.1" + } +} diff --git a/components/html_css_to_image/README.md b/components/html_css_to_image/README.md index 144cf38d1ff40..48fdbc69d07b0 100644 --- a/components/html_css_to_image/README.md +++ b/components/html_css_to_image/README.md @@ -1,8 +1,11 @@ # Overview -With the HTML/CSS to Image API you can: +The HTML/CSS to Image API lets you programmatically create images from HTML and CSS, which is perfect for generating custom graphics, charts, or even to convert web content for sharing on social media. On Pipedream, you can harness this API within a serverless workflow, triggering image generation from various events, managing the data flow, and integrating with countless other apps to create powerful automations. -- Generate images from HTML/CSS code snippets -- Use CSS selectors to grab specific elements from the HTML code -- Use HTML5 canvas to crop, filter, or resize the images -- Output the resulting images in various formats (JPEG, PNG, GIF, WebP) +# Example Use Cases + +- **Generate Custom Invoices**: Automate the creation of visually appealing invoices by triggering the HTML/CSS to Image API whenever a new order is received on an e-commerce platform like Shopify. Pipedream can capture new order events and then use the HTML/CSS to Image API to create a custom-styled invoice image that can be emailed to the customer or stored in cloud storage like Google Drive. + +- **Social Media Content Creation**: Whenever a new blog post is published in WordPress, use Pipedream to trigger an automated workflow that generates a promotional image with the HTML/CSS to Image API, including the title and a snippet. The image can then be posted automatically to social networks like Twitter or Facebook via their respective APIs, boosting engagement with visually consistent and branded content. + +- **Dynamic Email Personalization**: For email marketing campaigns managed with Mailchimp, you can create personalized images for each recipient based on user data. Trigger the HTML/CSS to Image API with Pipedream when a campaign is sent, passing in subscriber information to generate custom images. These images can make emails more engaging, with personalized charts, greetings, or offers, and can be included in the Mailchimp emails for a unique touch. diff --git a/components/html_to_image/actions/convert-html-to-image/convert-html-to-image.mjs b/components/html_to_image/actions/convert-html-to-image/convert-html-to-image.mjs new file mode 100644 index 0000000000000..7253d884e3011 --- /dev/null +++ b/components/html_to_image/actions/convert-html-to-image/convert-html-to-image.mjs @@ -0,0 +1,50 @@ +import htmlToImage from "../../html_to_image.app.mjs"; + +export default { + key: "html_to_image-convert-html-to-image", + name: "Convert HTML to Image", + description: "Create an image from HTML. [See the documentation](https://docs.htmlcsstoimg.com/html-to-image-api/html-css-to-image-api).", + version: "0.0.1", + type: "action", + props: { + htmlToImage, + htmlContent: { + propDefinition: [ + htmlToImage, + "htmlContent", + ], + }, + cssContent: { + propDefinition: [ + htmlToImage, + "cssContent", + ], + }, + font: { + type: "string", + label: "Font", + description: "Google Font Name that needs to be imported when generating image from HTML & CSS. To pass multiple fonts, separate them with | sign. Example - `Roboto|Georgia`", + optional: true, + }, + quality: { + propDefinition: [ + htmlToImage, + "quality", + ], + }, + }, + async run({ $ }) { + const response = await this.htmlToImage.convertToImage({ + $, + data: { + html_content: this.htmlContent, + css_content: this.cssContent, + font: this.font, + quality: this.quality, + generate_img_url: true, + }, + }); + $.export("$summary", "Successfully converted HTML to image"); + return response; + }, +}; diff --git a/components/html_to_image/actions/convert-html-to-pdf/convert-html-to-pdf.mjs b/components/html_to_image/actions/convert-html-to-pdf/convert-html-to-pdf.mjs new file mode 100644 index 0000000000000..5f040a34b14ee --- /dev/null +++ b/components/html_to_image/actions/convert-html-to-pdf/convert-html-to-pdf.mjs @@ -0,0 +1,71 @@ +import htmlToImage from "../../html_to_image.app.mjs"; + +export default { + key: "html_to_image-convert-html-to-pdf", + name: "Convert HTML to PDF", + description: "Create a PDF file from HTML. [See the documentation](https://docs.htmlcsstoimg.com/html-to-image-api/html-css-to-pdf-api).", + version: "0.0.1", + type: "action", + props: { + htmlToImage, + htmlContent: { + propDefinition: [ + htmlToImage, + "htmlContent", + ], + }, + cssContent: { + propDefinition: [ + htmlToImage, + "cssContent", + ], + }, + paperSize: { + propDefinition: [ + htmlToImage, + "paperSize", + ], + }, + landscape: { + propDefinition: [ + htmlToImage, + "landscape", + ], + }, + displayHeaderFooter: { + propDefinition: [ + htmlToImage, + "displayHeaderFooter", + ], + }, + printBackground: { + propDefinition: [ + htmlToImage, + "printBackground", + ], + }, + preferCssPageSize: { + type: "boolean", + label: "Prefer CSS Page Size", + description: "Get size from CSS styles. Default: `true`", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.htmlToImage.convertToPdf({ + $, + data: { + html_content: this.htmlContent, + css_content: this.cssContent, + paper_size: this.paperSize, + landscape: this.landscape, + displayHeaderFooter: this.displayHeaderFooter, + printBackground: this.printBackground, + preferCssPageSize: this.preferCssPageSize, + generate_pdf_url: true, + }, + }); + $.export("$summary", "Successfully converted HTML to PDF"); + return response; + }, +}; diff --git a/components/html_to_image/actions/convert-url-to-image/convert-url-to-image.mjs b/components/html_to_image/actions/convert-url-to-image/convert-url-to-image.mjs new file mode 100644 index 0000000000000..9b119b1db81b3 --- /dev/null +++ b/components/html_to_image/actions/convert-url-to-image/convert-url-to-image.mjs @@ -0,0 +1,64 @@ +import htmlToImage from "../../html_to_image.app.mjs"; + +export default { + key: "html_to_image-convert-url-to-image", + name: "Convert URL to Image", + description: "Capture a screenshot from a URL. [See the documentation](https://docs.htmlcsstoimg.com/html-to-image-api/screenshot-capture-api).", + version: "0.0.1", + type: "action", + props: { + htmlToImage, + url: { + propDefinition: [ + htmlToImage, + "url", + ], + }, + viewPortWidth: { + type: "integer", + label: "View Port Width", + description: "Width of View Port. Default value is 1080", + optional: true, + }, + viewPortHeight: { + type: "integer", + label: "View Port Height", + description: "Height of View Port. Default value is 720", + optional: true, + }, + quality: { + propDefinition: [ + htmlToImage, + "quality", + ], + }, + fullPage: { + type: "boolean", + label: "Full Page", + description: "Whether to capture full-page screenshot of the URL. Default value is `false`.", + optional: true, + }, + waitUntil: { + propDefinition: [ + htmlToImage, + "waitUntil", + ], + }, + }, + async run({ $ }) { + const response = await this.htmlToImage.convertToImage({ + $, + data: { + url: this.url, + viewPortWidth: this.viewPortWidth, + viewPortHeight: this.viewPortHeight, + quality: this.quality, + full_page: this.fullPage, + wait_till: this.waitUntil, + generate_img_url: true, + }, + }); + $.export("$summary", "Successfully converted URL to image"); + return response; + }, +}; diff --git a/components/html_to_image/actions/convert-url-to-pdf/convert-url-to-pdf.mjs b/components/html_to_image/actions/convert-url-to-pdf/convert-url-to-pdf.mjs new file mode 100644 index 0000000000000..03da750085b99 --- /dev/null +++ b/components/html_to_image/actions/convert-url-to-pdf/convert-url-to-pdf.mjs @@ -0,0 +1,64 @@ +import htmlToImage from "../../html_to_image.app.mjs"; + +export default { + key: "html_to_image-convert-url-to-pdf", + name: "Convert URL to PDF", + description: "Create a PDF from a URL. [See the documentation](https://docs.htmlcsstoimg.com/html-to-image-api/url-to-pdf-api).", + version: "0.0.1", + type: "action", + props: { + htmlToImage, + url: { + propDefinition: [ + htmlToImage, + "url", + ], + }, + paperSize: { + propDefinition: [ + htmlToImage, + "paperSize", + ], + }, + landscape: { + propDefinition: [ + htmlToImage, + "landscape", + ], + }, + displayHeaderFooter: { + propDefinition: [ + htmlToImage, + "displayHeaderFooter", + ], + }, + printBackground: { + propDefinition: [ + htmlToImage, + "printBackground", + ], + }, + waitUntil: { + propDefinition: [ + htmlToImage, + "waitUntil", + ], + }, + }, + async run({ $ }) { + const response = await this.htmlToImage.convertToPdf({ + $, + data: { + url: this.url, + paper_size: this.paperSize, + landscape: this.landscape, + displayHeaderFooter: this.displayHeaderFooter, + printBackground: this.printBackground, + wait_till: this.waitUntil, + generate_pdf_url: true, + }, + }); + $.export("$summary", "Successfully converted URL to PDF"); + return response; + }, +}; diff --git a/components/html_to_image/html_to_image.app.mjs b/components/html_to_image/html_to_image.app.mjs new file mode 100644 index 0000000000000..40a99bec48083 --- /dev/null +++ b/components/html_to_image/html_to_image.app.mjs @@ -0,0 +1,99 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "html_to_image", + propDefinitions: { + url: { + type: "string", + label: "URL", + description: "The URL of the webpage", + }, + htmlContent: { + type: "string", + label: "HTML Content", + description: "HTML Content to be used", + }, + cssContent: { + type: "string", + label: "CSS Content", + description: "CSS Content to be used", + optional: true, + }, + quality: { + type: "integer", + label: "Quality", + description: "Quality of the image should be in the range 30-100. Default value is 30.", + optional: true, + }, + paperSize: { + type: "string", + label: "Paper Size", + description: "Size of the paper", + options: [ + "A3", + "A4", + "A5", + "Letter", + "Legal", + ], + optional: true, + }, + landscape: { + type: "boolean", + label: "Landscape", + description: "Page orientation where the content is formatted horizontally. By default the page orientation is Portrait", + optional: true, + }, + displayHeaderFooter: { + type: "boolean", + label: "Display Header Footer", + description: "Generated PDF with have header and Footer on each page", + optional: true, + }, + printBackground: { + type: "boolean", + label: "Print Background", + description: "Prints any background colors or images used on the web page to the PDF. Its default value is `true`.", + optional: true, + }, + waitUntil: { + type: "integer", + label: "Wait Until", + description: " Number of seconds to wait before capturing a screenshot from the URL. Default value is 0", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.htmlcsstoimg.com/api/v1"; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "CLIENT-API-KEY": this.$auth.api_key, + }, + ...opts, + }); + }, + convertToImage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/generateImage", + ...opts, + }); + }, + convertToPdf(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/generatePdf", + ...opts, + }); + }, + }, +}; diff --git a/components/html_to_image/package.json b/components/html_to_image/package.json new file mode 100644 index 0000000000000..a9f9a4a32fabe --- /dev/null +++ b/components/html_to_image/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/html_to_image", + "version": "0.1.0", + "description": "Pipedream HTML to Image Components", + "main": "html_to_image.app.mjs", + "keywords": [ + "pipedream", + "html_to_image" + ], + "homepage": "https://pipedream.com/apps/html_to_image", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/http/README.md b/components/http/README.md index a2737b8dac9ae..090f7e490bd40 100644 --- a/components/http/README.md +++ b/components/http/README.md @@ -1,29 +1,163 @@ -## HTTP Event Source +# Overview -Pipedream is a platform for running hosted, backend components. An HTTP Event Source is essentially a [RequestBin](https://requestbin.com) that can be managed via API. +Build, test, and send HTTP requests without code using your Pipedream workflows. The HTTP / Webhook action is a tool to build HTTP requests with a Postman-like graphical interface. -Components come with a [built-in key-value store](/COMPONENT-API.md#db), an interface for passing input via [props](/COMPONENT-API.md#props), and more. You deploy and manage components using Pipedream's [REST API](https://docs.pipedream.com/api/rest/), [CLI](https://docs.pipedream.com/cli/reference/), or [UI](https://pipedream.com/sources). +![An interface for configuring an HTTP request within Pipedream's workflow system. The current selection is a GET request with fields for the request URL, authorization type (set to 'None' with a note explaining "This request does not use authorization"), parameters, headers (with a count of 1, though the detail is not visible), and body. Below the main configuration area is an option to "Include Response Headers," and a button labeled "Configure to test." The overall layout suggests a user-friendly, no-code approach to setting up custom HTTP requests.](https://res.cloudinary.com/pipedreamin/image/upload/v1712765140/marketplace/apps/http/CleanShot_2024-04-10_at_11.53.07_u5anis.png) -[Components can emit events](/COMPONENT-API.md#emit), which can be retrieved programmatically via [CLI](https://docs.pipedream.com/cli/reference/), [API](https://docs.pipedream.com/api/rest/) or [SSE](https://docs.pipedream.com/api/sse/). They can also trigger [Pipedream workflows](https://docs.pipedream.com/workflows/) on every event. For example, you can process items from an RSS feed and access the items via REST API, or trigger code to run on every new item using the SSE interface or a workflow. +## Point and click HTTP requests -## Quickstart +Define the target URL, HTTP verb, headers, query parameters, and payload body without writing custom code. -[Install the Pipedream CLI](https://docs.pipedream.com/cli/install/), then run: +![A screenshot of Pipedream's HTTP Request Configuration interface with a GET request type selected. The request URL is set to 'https://api.openai.com/v1/models'. The 'Auth' tab is highlighted, indicating that authentication is required for this request. In the headers section, there are two headers configured: 'User-Agent' is set to 'pipedream/1', and 'Authorization' is set to 'Bearer {{openai_api_key}}', showing how the OpenAI account's API key is dynamically inserted into the headers to handle authentication automatically.](https://res.cloudinary.com/pipedreamin/image/upload/v1712765340/marketplace/apps/http/CleanShot_2024-04-10_at_12.08.25_ab89hj.png) + +[Here's an example workflow that uses the HTTP / Webhook action to send an authenticated API request to OpenAI.](https://pipedream.com/new?h=tch_v4fzLp) + +## Focus on integrating, not authenticating + +This action can also use your connected accounts with third-party APIs. Selecting an integrated app will automatically update the request’s headers to authenticate with the app properly, and even inject your token dynamically. + +![This GIF depicts the process of selecting an application within Pipedream's HTTP Request Builder. A user hovers the cursor over the 'Auth' tab and clicks on a dropdown menu labeled 'Authorization Type', then scrolls through a list of applications to choose from for authorization purposes. The interface provides a streamlined and intuitive method for users to authenticate their HTTP requests by selecting the relevant app in the configuration settings.](https://res.cloudinary.com/pipedreamin/image/upload/v1712765587/marketplace/apps/http/CleanShot_2024-04-10_at_12.12.34_e6zrft.gif) + +Pipedream integrates with thousands of APIs, but if you can’t find a Pipedream integration simply use [Environment Variables](https://pipedream.com/docs/environment-variables) in your request headers to authenticate with. + +## Compatible with no code actions or Node.js and Python + +The HTTP/Webhook action exports HTTP response data for use in subsequent workflow steps, enabling easy data transformation, further API calls, database storage, and more. + +Response data is available for both coded (Node.js, Python) and no-code steps within your workflow. + +![An image showing the Pipedream interface where the HTTP Webhook action has returned response data as a step export. The interface highlights a structured view of the returned data with collapsible sections. We can see 'steps.custom_request1' expanded to show 'return_value' which is an object containing a 'list'. Inside the list, an item 'data' is expanded to reveal an element with an 'id' of 'whisper-1', indicating a model created by and owned by 'openai-internal'. Options to 'Copy Path' and 'Copy Value' are available for easy access to the data points.](https://res.cloudinary.com/pipedreamin/image/upload/v1712765724/marketplace/apps/http/CleanShot_2024-04-10_at_12.15.11_mkezj8.png) + +# Getting Started + +The HTTP / Webhook action is flexible. You can use it with your [Pipedream connected accounts](https://pipedream.com/docs/connected-accounts), import cURL commands, or manually define authentication headers with API keys stored in [Environment Variables](https://pipedream.com/docs/environment-variables). + +## Connecting to an API with a Pipedream connected account + +We recommend choosing this approach to authenticate your HTTP requests. Pipedream will manage your connected accounts, even rotating OAuth tokens, and you can focus on implementing the payload details. + +Start by adding the HTTP/Webhook action as a new step in your workflow. + +![The image displays a user interface within Pipedream's workflow editor. On the left sidebar, under "Apps," various application options are listed, including Node, Python, OpenAI (ChatGPT), and others, each with a number indicating available actions. A red arrow points to the "HTTP / Webhook" option, which is highlighted and shows that there are 10 actions available for it. On the right, a panel titled "Actions → Popular" lists options such as "Build API Request," "Run custom code," and others, suggesting these are commonly used or recommended actions for building workflows. The overall design of the interface emphasizes user-friendliness and ease of navigation.](https://res.cloudinary.com/pipedreamin/image/upload/v1712765837/marketplace/apps/http/CleanShot_2024-04-10_at_12.16.59_y1mu9t.png) + +Then select Use app to search for the API you’d like to send this HTTP request to. Then use 'Select App' to find and choose the API for your HTTP request: + +![The image shows a close-up of the HTTP Request Configuration pane in Pipedream's interface, focusing on the 'Auth' tab. A red arrow points to the 'Authorization Type' dropdown menu, which is expanded to show different authentication types including 'None', 'Basic Auth', 'Bearer Token', and highlighted is the 'Select an app' option. This demonstrates the step where a user can select the type of authorization for their HTTP request, with the option to easily choose an app for integrated authentication.](https://res.cloudinary.com/pipedreamin/image/upload/v1712766063/marketplace/apps/http/CleanShot_2024-04-10_at_12.20.36_kr51sx.png) + +Then connect your account, if the app has OAuth then Pipedream will initiate the OAuth sequence and automatically rotate your tokens for you. + +After connecting your account, the URL, verb, and headers update to enable a valid authenticated request. + +Then click **Test** to send the HTTP request: + +![The image displays the Pipedream HTTP Request Configuration panel with a GET method selected and the request URL 'https://api.openai.com/v1/models' entered. Two headers are configured: 'User-Agent' with the value 'pipedream/1' and 'Authorization' with a dynamic variable 'Bearer {{openai_api_key}}' to automatically insert the OpenAI API key. A red arrow points towards the 'Authorization' header field, emphasizing the insertion of the API key. Below the headers, there's an option to 'Include Response Headers', and at the bottom, there's a 'Test' button to send the HTTP request.](https://res.cloudinary.com/pipedreamin/image/upload/v1712766296/marketplace/apps/http/CleanShot_2024-04-10_at_12.24.25_f9ne9h.png) + + +In the **Exports** tab, you’ll see the data the API responds with. You can click Copy Path next to an attribute to copy the path to that specific variable to your clipboard for easy pasting in other steps. + +![The image shows a section of the Pipedream workflow interface under the 'Exports' tab. It details the output of a custom HTTP request step labeled 'steps.custom_request1'. The output, named '$return_value', is an object represented as a list. The list is expanded to reveal 'data' with 30 entries. The first entry is further expanded, displaying details of a model with the ID 'whisper-1', indicating it's an object called 'model' that was created at the UNIX timestamp '1677532384' and is owned by 'openai-internal'.](https://res.cloudinary.com/pipedreamin/image/upload/v1712766450/marketplace/apps/http/CleanShot_2024-04-10_at_12.27.19_xr9kyp.gif) + +### Connecting to an API with an imported cURL command + +API documentation sometimes include cURL commands as examples. The HTTP request builder action can recognize cURL and update your request configuration to match. + +For example, the OpenAI documentation includes an example cURL command to send a prompt to ChatGPT: ```bash -pd deploy http-new-requests +curl https://api.openai.com/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "model": "gpt-3.5-turbo", + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant." + }, + { + "role": "user", + "content": "Hello!" + } + ] + }' ``` -This deploys an [HTTP event source](#what-are-http-event-sources) and creates a unique endpoint URL you can send any HTTP requests to: +Configure the HTTP request to match this cURL command by clicking *Import cURL*: + +![The image shows a segment of the Pipedream HTTP Request Configuration interface, where the 'GET' method is selected as the type of HTTP request. A red arrow points towards the 'Import cURL' button located in the top right corner, indicating where users can click to import a cURL command into the workflow for configuration. The 'Enter request URL' field is highlighted, awaiting the user's input. Additionally, tabs for 'Auth', 'Params', 'Headers', and 'Body' are visible, with a notation that there is one header set. The 'Configure' label at the top indicates the setup process is 'Incomplete'.](https://res.cloudinary.com/pipedreamin/image/upload/v1712767353/marketplace/apps/http/CleanShot_2024-04-10_at_12.42.18_ula7yc.png) + +Then paste in the cURL command and click *Import*: + +![The image shows a dialog box titled "Import Curl" within Pipedream's interface, where a cURL command is ready to be imported. The cURL command includes the API endpoint 'https://api.openai.com/v1/chat/completions', headers for 'Content-Type' and 'Authorization', and a JSON payload specifying a model 'gpt-3.5-turbo' with a series of messages. A red arrow points towards the 'IMPORT' button, indicating that clicking this will import the cURL command into the Pipedream workflow. The 'CANCEL' button is also visible for opting not to proceed with the import.](https://res.cloudinary.com/pipedreamin/image/upload/v1712767458/marketplace/apps/http/CleanShot_2024-04-10_at_12.44.02_xjufrs.png) + +This will configure the HTTP request to match the details of the cURL statement: -```text - id: dc_abc123 - name: http - endpoint: https://myendpoint.m.pipedream.net +![The image is a screenshot of the Pipedream HTTP Request Configuration panel after importing a cURL command. It displays a filled-out form with a GET request set to the URL 'https://api.openai.com/v1/chat/completions'. The 'Auth' tab is highlighted, indicating that authentication has been configured. The 'Headers' tab shows '3' indicating multiple headers have been set, including 'Content-Type' with the value 'application/json'. The 'Body' tab is active, showing 'model' set to 'gpt-3.5-turbo' and 'messages' containing a JSON array with two objects, each specifying a 'role' and 'content', indicating the structure of the data to be sent with the request.](https://res.cloudinary.com/pipedreamin/image/upload/v1712767841/marketplace/apps/http/CleanShot_2024-04-10_at_12.50.29_ia5ago.png) + +Don't forget to inject your API keys into the headers. We recommend using [Environment Variables](https://pipedream.com/docs/environment-variables) to store your non-Pipedream managed account API keys. + +For example, storing your OpenAI API key as `OPEN_API_KEY` you can then reference it in the `Authorization` header like so: + +``` +{{ process.env["OPEN_API_KEY"] }} ``` -The CLI will automatically listen for new requests to this URL, displaying them in your shell as soon as they arrive. **Send a test request using the provided cURL command to give it a try**. +Then your key will be injected into the request. + +# Troubleshooting + +You may run into issues integrating with other APIs. Typically these are errors thrown by the API you’re attempting to integrate with. + +Common HTTP status codes provide a standard, but implementations may vary. Please check the API documentation for the service or app you’re integrating with for more details. + +Below are a list of common errors when sending HTTP requests to 3rd party APIs: + +## Resource Not Found (404) + +This means that the API request successfully authenticated, but the resource you’re attempting to retrieve or act on doesn’t exist. + +For example, if you’re attempting to update a Google Sheet but that sheet has been deleted, then you’ll see a 404 HTTP status code. + +## Method not allowed (405) + +This error message means that you attempted to access an endpoint but the HTTP verb the request is using isn’t supported. + +For example, you might have sent a `GET` request instead of a `POST` request or vice versa. This error is usually fixed by double checking that the HTTP action the request is configured with matches what the endpoint supports. + +## Invalid Request (422) + +This error indicates the request's payload lacks data or contains invalid data. + +Usually the API’s response will give you a list of errors that give more details. + +For example, if you attempt to create a Google Calendar event, but the `start_at` date is **after** the `end_at` date, then the API will return a 422 error because you can’t create an event that ends before it starts. + +If this error happens only intermittently, it could mean that downstream data is missing or occasionally produces invalid payloads. You’ll need to select the broken use case in the builder, and test it to find the culprit. + +## Rate limited (429) + +APIs typically implement rate limits. A rate limit defines how many times you’re allowed to send requests to the API within a specific time frame. + +Exceeding this limit prompts the API to respond with a 429 status code and reject the requests. + +You can use Concurrency and Rate Limiting controls within your Pipedream workflow settings to respect these 3rd party API limits, and you can also enable Automatic Retries to attempt the request again. + +## Not Authorized or Forbidden (401) + +This error means that either your request isn’t authentication properly, meaning that your API token is missing or is in the incorrect header; or potentially it means that your API key is valid but it doesn’t have the permissions required to access that resource. + +For example, you might see this error if your API key is missing a character, or is incorrectly formatted in the request. + +A common cause is omitting 'Bearer' or the space after it in the **`Authorization`** header: **`Bearer ${your API key here}`**. + +An authorization error can happen if you attempt to modify a resource your account on that service doesn’t have permission to access. + +For example, modifying a Google Sheet not shared with your Google Drive account will result in a permissions error. + +## Internal Server Error (50x) + +This means that the API encountered a fatal error and it wasn’t able to perform your request. It could be that this service is having an outage, or there’s a specific issue with this particular resource you’re attempting to access or modify. -You can retrieve requests to this endpoint programmatically, using Pipedream's [REST API](https://docs.pipedream.com/api/rest/#get-source-events), [CLI](https://docs.pipedream.com/cli/reference/#command-reference) or a [private SSE stream](https://docs.pipedream.com/api/sse/) tied to your event source. +Your request might be configured correctly, and your authentication headers properly set, but the issue is within the 3rd party’s API. We recommend contacting their customer support for more details. -You can also run any Node.js code on HTTP requests to filter or transform them, issue a custom HTTP response, and more — [see the example components below](#example-http-sources). +You can also enable Automatic Retries on your workflow to automatically retry failed requests in case of an intermittent outage. diff --git a/components/http/actions/verify-hmac-signature/verify-hmac-signature.mjs b/components/http/actions/verify-hmac-signature/verify-hmac-signature.mjs new file mode 100644 index 0000000000000..0600bfa70b1e0 --- /dev/null +++ b/components/http/actions/verify-hmac-signature/verify-hmac-signature.mjs @@ -0,0 +1,68 @@ +import crypto from "crypto"; + +export default { + name: "Verify HMAC Signature", + version: "0.0.1", + key: "http-verify-hmac-signature", + description: "Validate HMAC signature for incoming HTTP webhook requests. Make sure to configure the HTTP trigger to \"Return a custom response from your workflow\".", + type: "action", + props: { + http: "$.interface.http", + secret: { + type: "string", + label: "Secret", + description: "Your secret key used for validation", + secret: true, + }, + signature: { + type: "string", + label: "Signature", + description: "The HMAC signature received from the incoming webhook, typically provided in a specific HTTP header", + }, + bodyRaw: { + type: "string", + label: "Raw Body", + description: "The raw request body received from the webhook caller, provided as a string without any parsing or modifications", + }, + customResponse: { + type: "boolean", + label: "Return Error to Webhook Caller", + description: "If `True`, returns a `401: Invalid credentials` error in the case of invalid authorization. **Make sure to configure the HTTP trigger to \"Return a custom response from your workflow\"**. If `False`, does not return a custom response in the case of invalid auth.", + default: true, + }, + }, + methods: { + _checkHmac(secret, signature, bodyRaw) { + const expectedSignature = crypto + .createHmac("sha256", secret) + .update(bodyRaw, "utf8") + .digest(); + + const signatureBuffer = Buffer.from(signature, "hex"); + if (signatureBuffer.length !== expectedSignature.length) { + return false; + } + return crypto.timingSafeEqual(signatureBuffer, expectedSignature); + }, + }, + async run({ $ }) { + const valid = this._checkHmac( + this.secret, + this.signature, + this.bodyRaw, + ); + + if (!valid) { + if (this.customResponse) { + await $.respond({ + status: 401, + headers: {}, + body: "Invalid credentials", + }); + } + return $.flow.exit("Invalid credentials"); + } + + $.export("$summary", "HTTP request successfully authenticated"); + }, +}; diff --git a/components/http/package.json b/components/http/package.json index 781985f1a78ff..65291c52efda4 100644 --- a/components/http/package.json +++ b/components/http/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/http", - "version": "0.4.1", + "version": "0.5.0", "description": "Pipedream Http Components", "main": "http.app.js", "keywords": [ diff --git a/components/https_airbyte_com/README.md b/components/https_airbyte_com/README.md new file mode 100644 index 0000000000000..9fab2c7f2ad68 --- /dev/null +++ b/components/https_airbyte_com/README.md @@ -0,0 +1,11 @@ +# Overview + +The Airbyte API allows for creating and managing data integration pipelines between various sources and destinations, automating data synchronization tasks, and monitoring the status of those pipelines. On Pipedream, you can leverage the Airbyte API to build intricate workflows that react to data events, manipulate and store data, and connect to other services to create rich, automated data pipelines. + +# Example Use Cases + +- **Sync New Database Entries to CRM**: Trigger a workflow whenever new entries are added to a Postgres database; use the Airbyte API to sync this new data to a CRM platform like Salesforce, keeping sales data updated in real-time. + +- **Automated Reporting Pipeline**: Combine data from multiple sources, like Google Analytics and advertising platforms, via Airbyte at regular intervals. Then, process and send the aggregated data to Google Sheets for easy access and automated reporting. + +- **Real-time Data Backup**: Set up a workflow that uses Airbyte to replicate data from primary databases to secondary storage solutions, such as Amazon S3, ensuring real-time or scheduled backups for disaster recovery purposes. diff --git a/components/httpsms/README.md b/components/httpsms/README.md new file mode 100644 index 0000000000000..99b82735745ca --- /dev/null +++ b/components/httpsms/README.md @@ -0,0 +1,11 @@ +# Overview + +The httpSMS API enables you to send SMS messages programmatically. With it, you can integrate SMS capabilities into your workflows on Pipedream. This is useful for alerting users, sending verification codes, or providing updates via text message. On Pipedream, you can connect the httpSMS API to various triggers and actions, creating automated processes that handle a multitude of SMS-related tasks. + +# Example Use Cases + +- **Customer Order Updates via SMS**: Trigger a workflow when a new order is placed on your e-commerce platform. Use the httpSMS API to send an update to the customer's phone number with order confirmation and shipping details. + +- **SMS Alerts for System Monitoring**: Set up a Pipedream workflow that monitors your website's uptime. Connect it to the httpSMS API to send an SMS alert if the site goes down, allowing for quick response to outages. + +- **Two-factor Authentication (2FA)**: Enhance security by implementing 2FA. When a user attempts to log in, trigger a Pipedream workflow that uses httpSMS API to send a one-time code to the user's mobile device. diff --git a/components/httpsms/package.json b/components/httpsms/package.json index 44d9e19fb34ae..1b50bd2bb1887 100644 --- a/components/httpsms/package.json +++ b/components/httpsms/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/hub_planner/README.md b/components/hub_planner/README.md new file mode 100644 index 0000000000000..5cf5617d1ed65 --- /dev/null +++ b/components/hub_planner/README.md @@ -0,0 +1,11 @@ +# Overview + +The HUB Planner API is a toolset for managing resources, projects, and bookings in a visual and interactive way. By using the API with Pipedream, you can automate complex workflows, sync data across multiple platforms, and create custom integrations to streamline operations. From scheduling resources to analyzing project data, the opportunities are vast for enhancing project management efficiency. + +# Example Use Cases + +- **Resource Availability Notifications**: Use the HUB Planner API to monitor the availability of resources and trigger notifications. For instance, when a resource becomes available, Pipedream can send an automated Slack message to the relevant project manager, nudging them to allocate the resource to an impending project. + +- **Project Time Tracking Integration**: Integrate HUB Planner with a time-tracking app like Toggl. Whenever a booking is made or updated in HUB Planner, Pipedream can capture the event and log the hours directly into Toggl, ensuring that time tracking is up-to-date across systems. + +- **Automated Reporting**: Create workflows that automatically generate reports based on bookings and resource utilization data from HUB Planner. These reports can be compiled into a Google Sheets document and emailed weekly to stakeholders, providing consistent insights into resource management. diff --git a/components/hubspot/README.md b/components/hubspot/README.md index 86329cd76957e..325fcdc98cccb 100644 --- a/components/hubspot/README.md +++ b/components/hubspot/README.md @@ -1,34 +1,88 @@ # Overview -The Pipedream HubSpot app enables you to build event-driven workflows that interact with the HubSpot API. We have a variety of pre-built triggers and actions that don't require any code to configure. +The HubSpot API enables developers to integrate into HubSpots CRM, CMS, Conversations, and other features. It allows for automated management of contacts, companies, deals, and marketing campaigns, enabling custom workflows, data synchronization, and task automation. This streamlines operations and boosts customer engagement, with real-time updates for rapid response to market changes. # Getting Started -## Connecting HubSpot to Pipedream - You can install the Pipedream HubSpot app in the [Accounts](https://pipedream.com/accounts) section of your account, or directly in a workflow. -### Accounts +## From Accounts 1. Visit [https://pipedream.com/accounts](https://pipedream.com/accounts). 2. Click on the **Connect An App** button in the top-right. 3. Search for "HubSpot" among the list of apps, and select it. 4. You will be prompted to sign in to HubSpot and select the account you'd like to connect. 5. Finally, HubSpot will ask to give Pipedream access to your account. Click "Connect app" to proceed. + +![A screenshot of a user interface for adding a contact to a HubSpot account, with a prominent red arrow pointing to a dropdown menu titled "Select a HubSpot account...".](https://res.cloudinary.com/pipedreamin/image/upload/v1713894384/marketplace/apps/hubspot/CleanShot_2024-04-23_at_13.46.00_n2ve4a.png) + 6. That's it! You can now use this HubSpot account to trigger a workflow, or [link it to any code step](/connected-accounts/#connecting-accounts). -### Within a workflow +## Within a workflow 1. [Create a new workflow](https://pipedream.com/new). 2. Search for "HubSpot" in the text field when selecting a trigger. 3. Select the one of the triggers that appear, based on your use case. 4. Click the **Connect Account** button near the top of the trigger. This will prompt you to select any existing HubSpot accounts you've previously authenticated with Pipedream, or you can select a **New** account. Clicking **New** opens a new window asking you to allow Pipedream access to your HubSpot account. + +![A screenshot of a user interface for adding a contact to a HubSpot account, with a prominent red arrow pointing to a dropdown menu titled "Select a HubSpot account...".](https://res.cloudinary.com/pipedreamin/image/upload/v1713894384/marketplace/apps/hubspot/CleanShot_2024-04-23_at_13.46.00_n2ve4a.png) + 5. That's it! You can now connect to the HubSpot API using any of the HubSpot triggers within a Pipedream workflow. -## Privacy Policy +# Troubleshooting + +By default, a successful HubSpot API request will return a 200 status code alongside the corresponding data payload. + +Below are a list of common errors from HubSpots API by HTTP status code. + +## 401 Unauthorized + +Returned when the authentication provided is invalid. See our Authentication Overview for details on authenticating API requests. + +## 403 Forbidden + +Returned when the authentication provided does not have the proper permissions to access the specific URL. For example, an OAuth token with only content access would receive a 403 error when trying to access the Deals API, which requires contacts access. If your API key or private app has the necessary permissions and you're still encountering this error, please contact HubSpot support for assistance. + +## 429 Too Many Requests + +Returned when your account or app exceeds its API rate limits. For suggestions on how to work within those limits, click here. + +## 477 Migration in Progress + +Returned when a HubSpot account is in the midst of migration between data hosting locations. In this event, HubSpot provides a Retry-After response header indicating the time in seconds to wait before attempting your request again, typically up to 24 hours. + +## 502/504 Timeouts + +Returned when HubSpot's processing limits are reached, which are in place to prevent performance degradation due to excessive requests from a single client. If you encounter these timeouts, pause your requests briefly before retrying. + +## 503 Service Temporarily Unavailable + +Returned when HubSpot is temporarily unavailable. Upon receiving this response, take a short break before resuming your requests. + +## 521 Web Server Is Down + +Returned when HubSpot's server is down, usually as a temporary issue. If this occurs, pause your requests momentarily, then attempt to reconnect. + +## 522 Connection Timed Out + +Returned when the connection between HubSpot and your application times out. Contact HubSpot support if you encounter this error. + +## 523 Origin Is Unreachable + +Returned when HubSpot cannot reach your application. If this happens, pause your requests briefly before trying again. + +## 524 Timeout + +Returned when a response is not received within 100 seconds, possibly due to HubSpot's server being overloaded. Pause your requests for a short period, then retry. + +## 525/526 SSL Issues + +Returned when there's an issue with the SSL certificate or if the SSL handshake fails. In such cases, reach out to HubSpot support for assistance. + +# Example Use Cases -Please see the [Pipedream Privacy Policy](https://pipedream.com/privacy) for a detailed description of how we manage your data. +- **Lead Scoring and Segmentation**: Enhance leads automatically by pulling in enrichment data from external sources and categorizing them in HubSpot. Use customer interaction data from social media or support platforms to adjust lead scores and segment leads into appropriate marketing campaigns within HubSpot. -## Troubleshooting +- **Automated Ticketing and Support**: Create a workflow where new support queries on HubSpot trigger the creation of tickets in a tool like Zendesk. The workflow can also update the customer record in HubSpot with the ticket information, ensuring the sales team has visibility into customer issues. -Please [reach out](https://pipedream.com/support/) to the Pipedream team with any technical issues or questions about the HubSpot integration. We're happy to help! +- **Syncing Contacts with Email Platforms**: Set up a bi-directional sync between HubSpot and email marketing platforms like Mailchimp. When a contact is updated in HubSpot, it triggers an update in Mailchimp and vice versa. This keeps mailing lists current and enables targeted campaign efforts based on the most relevant data. diff --git a/components/hubspot/actions/add-contact-to-list/add-contact-to-list.mjs b/components/hubspot/actions/add-contact-to-list/add-contact-to-list.mjs index e27d9227383ca..5b6d1da63c97b 100644 --- a/components/hubspot/actions/add-contact-to-list/add-contact-to-list.mjs +++ b/components/hubspot/actions/add-contact-to-list/add-contact-to-list.mjs @@ -4,7 +4,7 @@ export default { key: "hubspot-add-contact-to-list", name: "Add Contact to List", description: "Adds a contact to a specific static list. [See the documentation](https://legacydocs.hubspot.com/docs/methods/lists/add_contact_to_list)", - version: "0.0.8", + version: "0.0.14", type: "action", props: { hubspot, @@ -33,9 +33,15 @@ export default { list, contactEmail, } = this; - const response = await this.hubspot.addContactsToList(list.value, [ - contactEmail, - ], $); + const response = await this.hubspot.addContactsToList({ + $, + listId: list.value, + data: { + emails: [ + contactEmail, + ], + }, + }); $.export("$summary", "Successfully added contact to list"); return response; }, diff --git a/components/hubspot/actions/batch-create-or-update-contact/batch-create-or-update-contact.mjs b/components/hubspot/actions/batch-create-or-update-contact/batch-create-or-update-contact.mjs index 52c4af810b499..69475b445dcf9 100644 --- a/components/hubspot/actions/batch-create-or-update-contact/batch-create-or-update-contact.mjs +++ b/components/hubspot/actions/batch-create-or-update-contact/batch-create-or-update-contact.mjs @@ -3,14 +3,14 @@ import hubspot from "../../hubspot.app.mjs"; export default { key: "hubspot-batch-create-or-update-contact", name: "Batch Create or Update Contact", - description: "Create or update a batch of contacts by its ID. [See the documentation](https://developers.hubspot.com/docs/api/crm/contacts)", - version: "0.0.5", + description: "Create or update a batch of contacts by its ID or email. [See the documentation](https://developers.hubspot.com/docs/api/crm/contacts)", + version: "0.0.11", type: "action", props: { hubspot, contacts: { label: "Contacts Array", - description: "Provide a **list of contacts** to be created or updated. If the provided contact has the prop ID, this action will attempt to update it.\n\n**Expected format for create:** `{ \"company\": \"Biglytics\", \"email\": \"bcooper@biglytics.net\", \"firstname\": \"Bryan\", \"lastname\": \"Cooper\", \"phone\": \"(877) 929-0687\", \"website\": \"biglytics.net\" }` \n\n**Expected format for update:** `{ \"id\": \"101\", \"company\": \"Biglytics\", \"email\": \"bcooper@biglytics.net\", \"firstname\": \"Bryan\", \"lastname\": \"Cooper\", \"phone\": \"(877) 929-0687\", \"website\": \"biglytics.net\" }`", + description: "Provide a **list of contacts** to be created or updated. If the provided contact has the prop ID or if the provided email already exists, this action will attempt to update it.\n\n**Expected format for create:** `{ \"company\": \"Biglytics\", \"email\": \"bcooper@biglytics.net\", \"firstname\": \"Bryan\", \"lastname\": \"Cooper\", \"phone\": \"(877) 929-0687\", \"website\": \"biglytics.net\" }` \n\n**Expected format for update:** `{ \"id\": \"101\", \"company\": \"Biglytics\", \"email\": \"bcooper@biglytics.net\", \"firstname\": \"Bryan\", \"lastname\": \"Cooper\", \"phone\": \"(877) 929-0687\", \"website\": \"biglytics.net\" }`", type: "string[]", }, }, @@ -24,16 +24,47 @@ export default { } return contacts; }, + async searchExistingContactProperties(contacts, $) { + const emails = contacts.map(({ email }) => email); + const { results } = await this.hubspot.searchCRM({ + $, + object: "contact", + data: { + filters: [ + { + propertyName: "email", + operator: "IN", + values: emails, + }, + ], + }, + }); + const updateEmails = results?.map(({ properties }) => properties.email); + const insertProperties = contacts.filter(({ email }) => !updateEmails.includes(email)) + .map((properties) => ({ + properties, + })); + const updateProperties = []; + for (const contact of results) { + updateProperties.push({ + id: contact.id, + properties: contacts.find(({ email }) => contact.properties.email === email), + }); + } + return { + insertProperties, + updateProperties, + }; + }, }, async run({ $ }) { const contacts = this.parseContactArray(this.contacts); - const insertProperties = contacts.filter((contact) => (!Object.prototype.hasOwnProperty.call(contact, "id"))) - .map((properties) => ({ - properties, - })); + const { + insertProperties, updateProperties, + } = await this.searchExistingContactProperties(contacts, $); - const updateProperties = contacts.filter((contact) => (Object.prototype.hasOwnProperty.call(contact, "id"))) + const updatePropertiesWithId = contacts.filter((contact) => (Object.prototype.hasOwnProperty.call(contact, "id"))) .map(({ id, ...properties }) => ({ @@ -41,6 +72,10 @@ export default { properties, })); + if (updatePropertiesWithId?.length) { + updateProperties.push(...updatePropertiesWithId); + } + let response = {}; response.created = await this.hubspot.batchCreateContacts({ $, diff --git a/components/hubspot/actions/common-create-object.mjs b/components/hubspot/actions/common-create-object.mjs deleted file mode 100644 index e18dfea28b5ac..0000000000000 --- a/components/hubspot/actions/common-create-object.mjs +++ /dev/null @@ -1,68 +0,0 @@ -import common from "./common-create.mjs"; - -export default { - ...common, - props: { - ...common.props, - // Re-defining propertyGroups so this.getObjectType() can be called from async options - // eslint-disable-next-line pipedream/props-description - propertyGroups: { - type: "string[]", - label: "Property Groups", - reloadProps: true, - async options() { - const { results: groups } = await this.hubspot.getPropertyGroups(this.getObjectType()); - return groups.map((group) => ({ - label: group.label, - value: group.name, - })); - }, - }, - }, - methods: { - ...common.methods, - isRelevantProperty(property) { - const isInPropertyGroups = this.propertyGroups?.includes(property.groupName); - return common.methods.isRelevantProperty(property) && isInPropertyGroups; - }, - }, - async run({ $ }) { - const { - hubspot, - /* eslint-disable no-unused-vars */ - propertyGroups, - $db, - updateIfExists, - ...properties - } = this; - const objectType = this.getObjectType(); - - // checkbox (string[]) props must be semicolon separated strings - Object.keys(properties) - .forEach((key) => { - let value = properties[key]; - if (Array.isArray(value)) { - properties[key] = value.join(";"); - } - }); - try { - const response = await hubspot.createObject(objectType, properties, $); - const objectName = hubspot.getObjectTypeName(objectType); - $.export("$summary", `Successfully created ${objectName}`); - - return response; - } catch (err) { - if (updateIfExists && err?.message) { - const errorObj = JSON.parse(err?.message); - if (errorObj?.category === "CONFLICT") { - const objectId = parseInt(errorObj.message.replace(/[^\d]/g, "")); - const response = await hubspot.updateObject(objectType, properties, objectId, $); - const objectName = hubspot.getObjectTypeName(objectType); - $.export("$summary", `Successfully updated ${objectName}`); - return response; - } - } - throw err; - } - }, -}; diff --git a/components/hubspot/actions/common-create.mjs b/components/hubspot/actions/common-create.mjs deleted file mode 100644 index 79a4775f3c707..0000000000000 --- a/components/hubspot/actions/common-create.mjs +++ /dev/null @@ -1,109 +0,0 @@ -import hubspot from "../hubspot.app.mjs"; -import { - OBJECT_TYPE, HUBSPOT_OWNER, -} from "../common/constants.mjs"; - -/** - * Returns an options method for a CRM object type, intended to be used in - * additionalProps - * - * @param {string} objectTypeName The object type name of the CRM object - * @returns The options method - */ -function getOptionsMethod(objectTypeName) { - switch (objectTypeName) { - case HUBSPOT_OWNER: - return (opts) => this.hubspot.getOwnersOptions(opts); - case OBJECT_TYPE.COMPANY: - return (opts) => this.hubspot.getCompaniesOptions(opts); - case OBJECT_TYPE.CONTACT: - return (opts) => this.hubspot.getContactsOptions(opts); - case OBJECT_TYPE.DEAL: - return (opts) => this.hubspot.getDealsOptions(opts); - case OBJECT_TYPE.LINE_ITEM: - return (opts) => this.hubspot.getLineItemsOptions(opts); - case OBJECT_TYPE.TICKET: - return (opts) => this.hubspot.getTicketsOptions(opts); - case OBJECT_TYPE.QUOTE: - return (opts) => this.hubspot.getQuotesOptions(opts); - case OBJECT_TYPE.CALL: - return (opts) => this.hubspot.getCallsOptions(opts); - case OBJECT_TYPE.EMAIL: - return (opts) => this.hubspot.getEmailsOptions(opts); - case OBJECT_TYPE.MEETING: - return (opts) => this.hubspot.getMeetingsOptions(opts); - case OBJECT_TYPE.NOTE: - return (opts) => this.hubspot.getNotesOptions(opts); - case OBJECT_TYPE.TASK: - return (opts) => this.hubspot.getTasksOptions(opts); - default: - return () => []; - } -} - -export default { - props: { - hubspot, - }, - methods: { - getObjectType() { - throw new Error("getObjectType is not implemented"); - }, - isRelevantProperty(property) { - return !property.modificationMetadata?.readOnlyValue - && (!property.hidden || property.name === "hs_email_direction") // hack - Hubspot's "hs_email_direction" property is hidden AND required - && !property.label.includes("(legacy)") - && (!property.options || property.options.length <= 500); // too many prop options cause the action to fail - }, - makeLabelValueOptions(property) { - const options = property.options - .filter((o) => !o.hidden) - .map(({ - label, value, - }) => ({ - label, - value, - })) - .filter(({ label }) => label); - - return options.length - ? options - : undefined; - }, - makePropDefinition(property, requiredProperties) { - let type = "string"; - let options = this.makeLabelValueOptions(property); - - if (property.referencedObjectType) { - const objectTypeName = this.hubspot.getObjectTypeName(property.referencedObjectType); - options = getOptionsMethod(objectTypeName); - } - - if (property.fieldType === "checkbox") { - type = "string[]"; - } - - return { - type, - name: property.name, - label: property.label, - description: property.description, - optional: !requiredProperties.includes(property), - options, - }; - }, - }, - async additionalProps() { - const schema = await this.hubspot.getSchema(this.getObjectType()); - const { results: properties } = await this.hubspot.getProperties(this.getObjectType()); - return properties - .filter(this.isRelevantProperty) - .map((property) => this.makePropDefinition(property, schema.requiredProperties)) - .reduce((props, { - name, ...definition - }) => { - props[name] = definition; - return props; - }, {}); - }, -}; diff --git a/components/hubspot/actions/common-get-object.mjs b/components/hubspot/actions/common-get-object.mjs deleted file mode 100644 index 5b243d9864cf5..0000000000000 --- a/components/hubspot/actions/common-get-object.mjs +++ /dev/null @@ -1,51 +0,0 @@ -import hubspot from "../hubspot.app.mjs"; - -export default { - props: { - hubspot, - objectId: { - type: "string", - label: "Object ID", - description: "Hubspot's internal ID for the object", - async options(opts) { - return this.hubspot.createOptions(this.getObjectType(), opts); - }, - }, - // eslint-disable-next-line pipedream/props-description - additionalProperties: { - type: "string[]", - label: "Additional properties to retrieve", - optional: true, - async options({ page }) { - if (page !== 0) { - return []; - } - const { results: properties } = await this.hubspot.getProperties(this.getObjectType()); - return properties.map((property) => ({ - label: property.label, - value: property.name, - })); - }, - }, - }, - methods: { - getObjectType() { - throw new Error("getObjectType is not implemented"); - }, - }, - async run({ $ }) { - const objectType = this.getObjectType(); - - const object = await this.hubspot.getObject( - objectType, - this.objectId, - this.additionalProperties, - $, - ); - - const objectName = this.hubspot.getObjectTypeName(objectType); - $.export("$summary", `Successfully fetched ${objectName}`); - - return object; - }, -}; diff --git a/components/hubspot/actions/common-update-object.mjs b/components/hubspot/actions/common-update-object.mjs deleted file mode 100644 index 63a7a08674b50..0000000000000 --- a/components/hubspot/actions/common-update-object.mjs +++ /dev/null @@ -1,54 +0,0 @@ -import common from "./common-create.mjs"; - -export default { - ...common, - props: { - // Re-defining propertyGroups so this.getObjectType() can be called from async options - // eslint-disable-next-line pipedream/props-description - propertyGroups: { - type: "string[]", - label: "Property Groups", - reloadProps: true, - async options() { - const { results: groups } = await this.hubspot.getPropertyGroups(this.getObjectType()); - return groups.map((group) => ({ - label: group.label, - value: group.name, - })); - }, - }, - }, - methods: { - ...common.methods, - isRelevantProperty(property) { - const isInPropertyGroups = this.propertyGroups?.includes(property.groupName); - return common.methods.isRelevantProperty(property) && isInPropertyGroups; - }, - }, - async run({ $ }) { - const { - hubspot, - /* eslint-disable no-unused-vars */ - propertyGroups, - $db, - objectId, - ...properties - } = this; - const objectType = this.getObjectType(); - - // checkbox (string[]) props must be semicolon separated strings - Object.keys(properties) - .forEach((key) => { - let value = properties[key]; - if (Array.isArray(value)) { - properties[key] = value.join(";"); - } - }); - - const response = await hubspot.updateObject(objectType, properties, objectId, $); - const objectName = hubspot.getObjectTypeName(objectType); - - $.export("$summary", `Successfully updated ${objectName}`); - return response; - }, -}; diff --git a/components/hubspot/actions/common/common-app-prop.mjs b/components/hubspot/actions/common/common-app-prop.mjs new file mode 100644 index 0000000000000..c7c1252b09765 --- /dev/null +++ b/components/hubspot/actions/common/common-app-prop.mjs @@ -0,0 +1,10 @@ +import hubspot from "../../hubspot.app.mjs"; + +export default { + props: { + hubspot: { + ...hubspot, + reloadProps: true, + }, + }, +}; diff --git a/components/hubspot/actions/common/common-create-object.mjs b/components/hubspot/actions/common/common-create-object.mjs new file mode 100644 index 0000000000000..a9da818ea6e2d --- /dev/null +++ b/components/hubspot/actions/common/common-create-object.mjs @@ -0,0 +1,107 @@ +import common from "./common-create.mjs"; + +export default { + ...common, + props: { + ...common.props, + // Re-defining propertyGroups so this.getObjectType() can be called from async options + // eslint-disable-next-line pipedream/props-description + propertyGroups: { + type: "string[]", + label: "Property Groups", + hidden: true, + reloadProps: true, + async options() { + const { results: groups } = await this.hubspot.getPropertyGroups({ + objectType: this.getObjectType(), + }); + return groups.map((group) => ({ + label: group.label, + value: group.name, + })); + }, + }, + objectProperties: { + type: "object", + label: "Object Properties", + description: "Enter the object properties to create as a JSON object", + }, + }, + methods: { + ...common.methods, + isRelevantProperty(property) { + const isInPropertyGroups = this.propertyGroups?.includes(property.groupName); + const isDefaultProperty = this.isDefaultProperty(property); + return common.methods.isRelevantProperty(property) + && isInPropertyGroups + && !isDefaultProperty; + }, + isDefaultProperty() { + return false; + }, + createObject(opts = {}) { + return this.hubspot.createObject(opts); + }, + }, + async run({ $ }) { + const { + hubspot, + /* eslint-disable no-unused-vars */ + propertyGroups, + customObjectType, + contactId, + $db, + updateIfExists, + objectProperties, + ...otherProperties + } = this; + const objectType = this.getObjectType(); + + const properties = objectProperties + ? typeof objectProperties === "string" + ? JSON.parse(objectProperties) + : objectProperties + : otherProperties; + + // checkbox (string[]) props must be semicolon separated strings + Object.keys(properties) + .forEach((key) => { + let value = properties[key]; + if (Array.isArray(value)) { + properties[key] = value.join(";"); + } + }); + try { + const response = await this.createObject({ + $, + objectType, + data: { + properties, + }, + }); + const objectName = hubspot.getObjectTypeName(objectType); + $.export("$summary", `Successfully created ${objectName}`); + + return response; + } catch (err) { + if (updateIfExists && err?.message) { + const errorObj = JSON.parse(err?.message); + if (errorObj?.category === "CONFLICT" || errorObj?.category === "OBJECT_ALREADY_EXISTS") { + const objectId = parseInt(errorObj.message.replace(/[^\d]/g, "")); + const response = await hubspot.updateObject({ + $, + objectType, + objectId, + data: { + properties, + }, + }); + const objectName = hubspot.getObjectTypeName(objectType); + $.export("$summary", `Successfully updated ${objectName}`); + return response; + } + } + throw err; + } + }, +}; diff --git a/components/hubspot/actions/common/common-create.mjs b/components/hubspot/actions/common/common-create.mjs new file mode 100644 index 0000000000000..d28ba834155e0 --- /dev/null +++ b/components/hubspot/actions/common/common-create.mjs @@ -0,0 +1,162 @@ +import { + OBJECT_TYPE, HUBSPOT_OWNER, +} from "../../common/constants.mjs"; +import appProp from "./common-app-prop.mjs"; + +/** + * Returns an options method for a CRM object type, intended to be used in + * additionalProps + * + * @param {string} objectTypeName The object type name of the CRM object + * @returns The options method + */ +function getOptionsMethod(objectTypeName) { + switch (objectTypeName) { + case HUBSPOT_OWNER: + return (opts) => this.hubspot.getOwnersOptions(opts); + case OBJECT_TYPE.COMPANY: + return (opts) => this.hubspot.getCompaniesOptions(opts); + case OBJECT_TYPE.CONTACT: + return (opts) => this.hubspot.getContactsOptions(opts); + case OBJECT_TYPE.DEAL: + return (opts) => this.hubspot.getDealsOptions(opts); + case OBJECT_TYPE.LINE_ITEM: + return (opts) => this.hubspot.getLineItemsOptions(opts); + case OBJECT_TYPE.TICKET: + return (opts) => this.hubspot.getTicketsOptions(opts); + case OBJECT_TYPE.QUOTE: + return (opts) => this.hubspot.getQuotesOptions(opts); + case OBJECT_TYPE.CALL: + return (opts) => this.hubspot.getCallsOptions(opts); + case OBJECT_TYPE.EMAIL: + return (opts) => this.hubspot.getEmailsOptions(opts); + case OBJECT_TYPE.MEETING: + return (opts) => this.hubspot.getMeetingsOptions(opts); + case OBJECT_TYPE.NOTE: + return (opts) => this.hubspot.getNotesOptions(opts); + case OBJECT_TYPE.TASK: + return (opts) => this.hubspot.getTasksOptions(opts); + default: + return () => []; + } +} + +export default { + props: { + ...appProp.props, + }, + methods: { + getObjectType() { + throw new Error("getObjectType is not implemented"); + }, + isRelevantProperty(property) { + return !property.modificationMetadata?.readOnlyValue + && (!property.hidden || property.name === "hs_email_direction") // hack - Hubspot's "hs_email_direction" property is hidden AND required + && !property.label.includes("(legacy)") + && (!property.options || property.options.length <= 500); // too many prop options cause the action to fail + }, + makeLabelValueOptions(property) { + const options = property.options + .filter((o) => !o.hidden) + .map(({ + label, value, + }) => ({ + label, + value, + })) + .filter(({ label }) => label); + + return options.length + ? options + : undefined; + }, + async makePropDefinition(property, requiredProperties) { + let type = "string"; + let options = this.makeLabelValueOptions(property); + + if (property.referencedObjectType) { + const objectTypeName = this.hubspot.getObjectTypeName(property.referencedObjectType); + options = getOptionsMethod(objectTypeName); + } + + if (property.name === "hs_timestamp") { + property.description += ". Enter date in ISO-8601 format. Example: `2024-06-25T15:43:49.214Z`"; + } + + const objectType = this.hubspot.getObjectTypeName(this.getObjectType()); + let reloadProps; + if (property.name === "hs_pipeline") { + options = await this.hubspot.getPipelinesOptions(objectType); + reloadProps = true; + } + if (property.name === "hs_pipeline_stage") { + options = await this.hubspot.getPipelineStagesOptions(objectType, this.hs_pipeline); + } + if (property.name === "hs_all_assigned_business_unit_ids") { + try { + options = await this.hubspot.getBusinessUnitOptions(); + } catch { + console.log("Could not load business units"); + } + property.description += " For use with the Business Units Add-On."; + } + + return { + type, + name: property.name, + label: property.label, + description: property.description, + optional: !requiredProperties.includes(property.name), + options, + reloadProps, + }; + }, + }, + async additionalProps(existingProps) { + const objectType = this.getObjectType(); + try { + const schema = await this.hubspot.getSchema({ + objectType, + }); + const { results: properties } = await this.hubspot.getProperties({ + objectType, + }); + const relevantProperties = properties.filter(this.isRelevantProperty); + + const propDefinitions = []; + if (this.propertyGroups && !relevantProperties?.length) { + propDefinitions.push({ + type: "alert", + alertType: "info", + name: "infoAlert", + content: `No writable properties found for Property Group(s): ${this.propertyGroups.join(", ")}`, + }); + } + + for (const property of relevantProperties) { + propDefinitions.push(await this.makePropDefinition(property, schema.requiredProperties)); + } + + if (existingProps.objectProperties) { + existingProps.objectProperties.hidden = true; + existingProps.objectProperties.optional = true; + } + if (existingProps.propertyGroups) { + existingProps.propertyGroups.hidden = false; + } + + return propDefinitions + .reduce((props, { + name, ...definition + }) => { + props[name] = definition; + return props; + }, {}); + } catch { + if (existingProps.propertyGroups) { + existingProps.propertyGroups.optional = true; + } + return {}; + } + }, +}; diff --git a/components/hubspot/actions/common/common-get-object.mjs b/components/hubspot/actions/common/common-get-object.mjs new file mode 100644 index 0000000000000..2778189222b4a --- /dev/null +++ b/components/hubspot/actions/common/common-get-object.mjs @@ -0,0 +1,102 @@ +import hubspot from "../../hubspot.app.mjs"; +import { + DEFAULT_CONTACT_PROPERTIES, + DEFAULT_COMPANY_PROPERTIES, + DEFAULT_DEAL_PROPERTIES, + DEFAULT_TICKET_PROPERTIES, + DEFAULT_PRODUCT_PROPERTIES, + DEFAULT_LINE_ITEM_PROPERTIES, +} from "../../common/constants.mjs"; + +export default { + props: { + hubspot, + objectId: { + type: "string", + label: "Object ID", + description: "Hubspot's internal ID for the object", + async options(opts) { + return this.hubspot.createOptions(this.getObjectType(), opts); + }, + reloadProps: true, + }, + info: { + type: "alert", + alertType: "info", + content: "", + hidden: true, + }, + // eslint-disable-next-line pipedream/props-description + additionalProperties: { + type: "string[]", + label: "Additional properties to retrieve", + optional: true, + async options({ page }) { + if (page !== 0) { + return []; + } + const { results: properties } = await this.hubspot.getProperties({ + objectType: this.getObjectType(), + }); + const defaultProperties = this.getDefaultProperties(this.getObjectType()); + return properties + .filter(({ name }) => !defaultProperties.includes(name)) + .map((property) => ({ + label: property.label, + value: property.name, + })); + }, + }, + }, + async additionalProps(props) { + return { + info: { + ...props.info, + content: `Properties:\n\`${this.getDefaultProperties(this.getObjectType()).join(", ")}\``, + hidden: false, + }, + }; + }, + methods: { + getObjectType() { + throw new Error("getObjectType is not implemented"); + }, + getDefaultProperties(objectType) { + if (objectType === "contact") { + return DEFAULT_CONTACT_PROPERTIES; + } else if (objectType === "company") { + return DEFAULT_COMPANY_PROPERTIES; + } else if (objectType === "deal") { + return DEFAULT_DEAL_PROPERTIES; + } else if (objectType === "ticket") { + return DEFAULT_TICKET_PROPERTIES; + } else if (objectType === "product") { + return DEFAULT_PRODUCT_PROPERTIES; + } else if (objectType === "line_item") { + return DEFAULT_LINE_ITEM_PROPERTIES; + } else { + return []; + } + }, + }, + async run({ $ }) { + const objectType = this.getObjectType(); + const { additionalProperties = [] } = this; + const defaultProperties = this.getDefaultProperties(this.getObjectType()); + + const object = await this.hubspot.getObject( + objectType, + this.objectId, + [ + ...defaultProperties, + ...additionalProperties, + ], + $, + ); + + const objectName = this.hubspot.getObjectTypeName(objectType); + $.export("$summary", `Successfully fetched ${objectName}`); + + return object; + }, +}; diff --git a/components/hubspot/actions/common/common-update-object.mjs b/components/hubspot/actions/common/common-update-object.mjs new file mode 100644 index 0000000000000..7fd6e740929c2 --- /dev/null +++ b/components/hubspot/actions/common/common-update-object.mjs @@ -0,0 +1,75 @@ +import common from "./common-create.mjs"; + +export default { + ...common, + props: { + // Re-defining propertyGroups so this.getObjectType() can be called from async options + // eslint-disable-next-line pipedream/props-description + propertyGroups: { + type: "string[]", + label: "Property Groups", + reloadProps: true, + async options() { + const { results: groups } = await this.hubspot.getPropertyGroups({ + objectType: this.getObjectType(), + }); + return groups.map((group) => ({ + label: group.label, + value: group.name, + })); + }, + }, + objectProperties: { + type: "object", + label: "Object Properties", + description: "Enter the object properties to update as a JSON object", + }, + }, + methods: { + ...common.methods, + isRelevantProperty(property) { + const isInPropertyGroups = this.propertyGroups?.includes(property.groupName); + return common.methods.isRelevantProperty(property) && isInPropertyGroups; + }, + }, + async run({ $ }) { + const { + hubspot, + /* eslint-disable no-unused-vars */ + propertyGroups, + customObjectType, + $db, + objectId, + objectProperties, + ...otherProperties + } = this; + const objectType = this.getObjectType(); + + const properties = objectProperties + ? typeof objectProperties === "string" + ? JSON.parse(objectProperties) + : objectProperties + : otherProperties; + + // checkbox (string[]) props must be semicolon separated strings + Object.keys(properties) + .forEach((key) => { + let value = properties[key]; + if (Array.isArray(value)) { + properties[key] = value.join(";"); + } + }); + + const response = await hubspot.updateObject({ + objectType, + objectId, + data: { + properties, + }, + }); + const objectName = hubspot.getObjectTypeName(objectType); + + $.export("$summary", `Successfully updated ${objectName}`); + return response; + }, +}; diff --git a/components/hubspot/actions/create-associations/create-associations.mjs b/components/hubspot/actions/create-associations/create-associations.mjs index fdb3630e425b3..0117af0e8de14 100644 --- a/components/hubspot/actions/create-associations/create-associations.mjs +++ b/components/hubspot/actions/create-associations/create-associations.mjs @@ -1,10 +1,11 @@ import hubspot from "../../hubspot.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; export default { key: "hubspot-create-associations", name: "Create Associations", description: "Create associations between objects. [See the documentation](https://developers.hubspot.com/docs/api/crm/associations#endpoint?spec=POST-/crm/v3/associations/{fromObjectType}/{toObjectType}/batch/create)", - version: "0.0.8", + version: "1.0.0", type: "action", props: { hubspot, @@ -12,6 +13,9 @@ export default { propDefinition: [ hubspot, "objectType", + () => ({ + includeCustom: true, + }), ], label: "From Object Type", description: "The type of the object being associated", @@ -32,6 +36,9 @@ export default { propDefinition: [ hubspot, "objectType", + () => ({ + includeCustom: true, + }), ], label: "To Object Type", description: "Type of the objects the from object is being associated with", @@ -58,21 +65,65 @@ export default { description: "Id's of the objects the from object is being associated with", }, }, + methods: { + async getAssociationCategory({ + $, fromObjectType, toObjectType, associationType, + }) { + const { results } = await this.hubspot.getAssociationTypes({ + $, + fromObjectType, + toObjectType, + associationType, + }); + const association = results.find(({ typeId }) => typeId === this.associationType); + return association.category; + }, + }, async run({ $ }) { const { fromObjectType, fromObjectId, toObjectType, associationType, - toObjectIds, } = this; - const response = await this.hubspot.createAssociations( + let toObjectIds; + if (Array.isArray(this.toObjectIds)) { + toObjectIds = this.toObjectIds; + } else { + try { + toObjectIds = JSON.parse(this.toObjectIds); + } catch { + throw new ConfigurationError("Could not parse \"To Objects\" array."); + } + } + + const associationCategory = await this.getAssociationCategory({ + $, fromObjectType, toObjectType, - fromObjectId, - toObjectIds, associationType, - ); + }); + const response = await this.hubspot.createAssociations({ + $, + fromObjectType, + toObjectType, + data: { + inputs: toObjectIds.map((toId) => ({ + from: { + id: fromObjectId, + }, + to: { + id: toId, + }, + types: [ + { + associationCategory, + associationTypeId: associationType, + }, + ], + })), + }, + }); const l = response.results.length; $.export("$summary", `Successfully created ${l} association${l === 1 ? "" diff --git a/components/hubspot/actions/create-communication/create-communication.mjs b/components/hubspot/actions/create-communication/create-communication.mjs new file mode 100644 index 0000000000000..e76262e73ab55 --- /dev/null +++ b/components/hubspot/actions/create-communication/create-communication.mjs @@ -0,0 +1,111 @@ +import common from "../common/common-create.mjs"; +import appProp from "../common/common-app-prop.mjs"; +import { ConfigurationError } from "@pipedream/platform"; +import { ASSOCIATION_CATEGORY } from "../../common/constants.mjs"; + +export default { + ...common, + key: "hubspot-create-communication", + name: "Create Communication", + description: "Create a WhatsApp, LinkedIn, or SMS message. [See the documentation](https://developers.hubspot.com/beta-docs/reference/api/crm/engagements/communications/v3#post-%2Fcrm%2Fv3%2Fobjects%2Fcommunications)", + version: "0.0.7", + type: "action", + props: { + ...appProp.props, + toObjectType: { + propDefinition: [ + appProp.props.hubspot, + "objectType", + ], + label: "Associated Object Type", + description: "Type of object the communication is being associated with", + optional: true, + }, + toObjectId: { + propDefinition: [ + appProp.props.hubspot, + "objectId", + (c) => ({ + objectType: c.toObjectType, + }), + ], + label: "Associated Object", + description: "ID of object the communication is being associated with", + optional: true, + }, + associationType: { + propDefinition: [ + appProp.props.hubspot, + "associationType", + (c) => ({ + fromObjectType: "communications", + toObjectType: c.toObjectType, + }), + ], + description: "A unique identifier to indicate the association type between the communication and the other object", + optional: true, + }, + objectProperties: { + type: "object", + label: "Object Properties", + description: "Enter the `communication` properties as a JSON object", + }, + }, + methods: { + ...common.methods, + getObjectType() { + return "communications"; + }, + }, + async run({ $ }) { + const { + hubspot, + /* eslint-disable no-unused-vars */ + toObjectType, + toObjectId, + associationType, + getObjectType, + objectProperties, + ...otherProperties + } = this; + + if ((toObjectId && !associationType) || (!toObjectId && associationType)) { + throw new ConfigurationError("Both `toObjectId` and `associationType` must be entered"); + } + + const properties = objectProperties + ? typeof objectProperties === "string" + ? JSON.parse(objectProperties) + : objectProperties + : otherProperties; + + const response = await hubspot.createObject({ + $, + objectType: getObjectType(), + data: { + associations: toObjectId + ? [ + { + types: [ + { + associationTypeId: associationType, + associationCategory: ASSOCIATION_CATEGORY.HUBSPOT_DEFINED, + }, + ], + to: { + id: toObjectId, + }, + }, + ] + : undefined, + properties: { + ...properties, + hs_communication_logged_from: "CRM", + }, + }, + }); + + $.export("$summary", `Successfully created communication with ID ${response.id}`); + return response; + }, +}; diff --git a/components/hubspot/actions/create-company/create-company.mjs b/components/hubspot/actions/create-company/create-company.mjs index fcec420d85aa3..bdadf977ab617 100644 --- a/components/hubspot/actions/create-company/create-company.mjs +++ b/components/hubspot/actions/create-company/create-company.mjs @@ -1,12 +1,12 @@ import { OBJECT_TYPE } from "../../common/constants.mjs"; -import common from "../common-create-object.mjs"; +import common from "../common/common-create-object.mjs"; export default { ...common, key: "hubspot-create-company", name: "Create Company", description: "Create a company in Hubspot. [See the documentation](https://developers.hubspot.com/docs/api/crm/companies#endpoint?spec=POST-/crm/v3/objects/companies)", - version: "0.0.10", + version: "0.0.18", type: "action", methods: { ...common.methods, diff --git a/components/hubspot/actions/create-contact/create-contact.mjs b/components/hubspot/actions/create-contact/create-contact.mjs deleted file mode 100644 index d8c89d7ca9672..0000000000000 --- a/components/hubspot/actions/create-contact/create-contact.mjs +++ /dev/null @@ -1,25 +0,0 @@ -import { OBJECT_TYPE } from "../../common/constants.mjs"; -import common from "../common-create-object.mjs"; - -export default { - ...common, - key: "hubspot-create-contact", - name: "Create Contact", - description: "Create a contact in Hubspot. [See the documentation](https://developers.hubspot.com/docs/api/crm/contacts#endpoint?spec=POST-/crm/v3/objects/contacts)", - version: "0.0.10", - type: "action", - props: { - ...common.props, - updateIfExists: { - label: "Update If Exists", - description: "When selected, if Hubspot returns an error upon creation the resource should be updated.", - type: "boolean", - }, - }, - methods: { - ...common.methods, - getObjectType() { - return OBJECT_TYPE.CONTACT; - }, - }, -}; diff --git a/components/hubspot/actions/create-custom-object/create-custom-object.mjs b/components/hubspot/actions/create-custom-object/create-custom-object.mjs new file mode 100644 index 0000000000000..aec39fd9d33a4 --- /dev/null +++ b/components/hubspot/actions/create-custom-object/create-custom-object.mjs @@ -0,0 +1,27 @@ +import common from "../common/common-create-object.mjs"; +import appProp from "../common/common-app-prop.mjs"; + +export default { + ...common, + key: "hubspot-create-custom-object", + name: "Create Custom Object", + description: "Create a new custom object in Hubspot. [See the documentation](https://developers.hubspot.com/beta-docs/guides/api/crm/objects/custom-objects#create-a-custom-object)", + version: "1.0.0", + type: "action", + props: { + ...appProp.props, + customObjectType: { + propDefinition: [ + appProp.props.hubspot, + "customObjectType", + ], + }, + ...common.props, + }, + methods: { + ...common.methods, + getObjectType() { + return this.customObjectType; + }, + }, +}; diff --git a/components/hubspot/actions/create-deal/create-deal.mjs b/components/hubspot/actions/create-deal/create-deal.mjs index adf89b44fbdb4..94eab59ad6dff 100644 --- a/components/hubspot/actions/create-deal/create-deal.mjs +++ b/components/hubspot/actions/create-deal/create-deal.mjs @@ -1,17 +1,48 @@ import { OBJECT_TYPE } from "../../common/constants.mjs"; -import common from "../common-create-object.mjs"; +import common from "../common/common-create-object.mjs"; export default { ...common, key: "hubspot-create-deal", name: "Create Deal", description: "Create a deal in Hubspot. [See the documentation](https://developers.hubspot.com/docs/api/crm/deals#endpoint?spec=POST-/crm/v3/objects/deals)", - version: "0.0.10", + version: "0.0.18", type: "action", + props: { + ...common.props, + dealname: { + type: "string", + label: "Deal Name", + description: "Name of the deal", + }, + pipeline: { + propDefinition: [ + common.props.hubspot, + "dealPipeline", + ], + description: "Pipeline of the deal", + }, + dealstage: { + propDefinition: [ + common.props.hubspot, + "stages", + (c) => ({ + pipeline: c.pipeline, + }), + ], + type: "string", + description: "Stage of the deal", + }, + }, methods: { ...common.methods, getObjectType() { return OBJECT_TYPE.DEAL; }, + isDefaultProperty(property) { + return property.name === "dealname" + || property.name === "pipeline" + || property.name === "dealstage"; + }, }, }; diff --git a/components/hubspot/actions/create-engagement/create-engagement.mjs b/components/hubspot/actions/create-engagement/create-engagement.mjs index de19c9aed8d0b..b4d8a2b749ab7 100644 --- a/components/hubspot/actions/create-engagement/create-engagement.mjs +++ b/components/hubspot/actions/create-engagement/create-engagement.mjs @@ -1,7 +1,7 @@ -import common from "../common-create.mjs"; +import common from "../common/common-create.mjs"; import { ConfigurationError } from "@pipedream/platform"; import { - API_PATH, ASSOCIATION_CATEGORY, + ASSOCIATION_CATEGORY, ENGAGEMENT_TYPE_OPTIONS, } from "../../common/constants.mjs"; export default { @@ -9,7 +9,7 @@ export default { key: "hubspot-create-engagement", name: "Create Engagement", description: "Create a new engagement for a contact. [See the documentation](https://developers.hubspot.com/docs/api/crm/engagements)", - version: "0.0.10", + version: "0.0.17", type: "action", props: { ...common.props, @@ -18,28 +18,7 @@ export default { label: "Engagement Type", description: "The type of engagement to create", reloadProps: true, - options: [ - { - label: "Note", - value: "notes", - }, - { - label: "Task", - value: "tasks", - }, - { - label: "Meeting", - value: "meetings", - }, - { - label: "Email", - value: "emails", - }, - { - label: "Call", - value: "calls", - }, - ], + options: ENGAGEMENT_TYPE_OPTIONS, }, toObjectType: { propDefinition: [ @@ -74,25 +53,29 @@ export default { description: "A unique identifier to indicate the association type between the task and the other object", optional: true, }, + objectProperties: { + type: "object", + label: "Object Properties", + description: "Enter the `engagement` properties as a JSON object", + }, }, methods: { ...common.methods, getObjectType() { return this.engagementType; }, - async createEngagement(objectType, properties, associations, $) { - return this.hubspot.makeRequest( - API_PATH.CRMV3, - `/objects/${objectType}`, - { - method: "POST", - data: { - properties, - associations, - }, - $, + isRelevantProperty(property) { + return common.methods.isRelevantProperty(property) && !property.name.includes("hs_pipeline"); + }, + createEngagement(objectType, properties, associations, $) { + return this.hubspot.createObject({ + objectType, + data: { + properties, + associations, }, - ); + $, + }); }, }, async run({ $ }) { @@ -104,13 +87,20 @@ export default { toObjectId, associationType, $db, - ...properties + objectProperties, + ...otherProperties } = this; if ((toObjectId && !associationType) || (!toObjectId && associationType)) { throw new ConfigurationError("Both `toObjectId` and `associationType` must be entered"); } + const properties = objectProperties + ? typeof objectProperties === "string" + ? JSON.parse(objectProperties) + : objectProperties + : otherProperties; + const objectType = this.getObjectType(); const associations = toObjectId @@ -129,6 +119,10 @@ export default { ] : undefined; + if (properties.hs_task_reminders) { + properties.hs_task_reminders = Date.parse(properties.hs_task_reminders); + } + const engagement = await this.createEngagement(objectType, properties, associations, $); const objectName = hubspot.getObjectTypeName(objectType); diff --git a/components/hubspot/actions/create-lead/create-lead.mjs b/components/hubspot/actions/create-lead/create-lead.mjs new file mode 100644 index 0000000000000..07bcb6a3fc4f8 --- /dev/null +++ b/components/hubspot/actions/create-lead/create-lead.mjs @@ -0,0 +1,56 @@ +import { + OBJECT_TYPE, ASSOCIATION_CATEGORY, +} from "../../common/constants.mjs"; +import common from "../common/common-create-object.mjs"; +import appProp from "../common/common-app-prop.mjs"; + +export default { + ...common, + key: "hubspot-create-lead", + name: "Create Lead", + description: "Create a lead in Hubspot. [See the documentation](https://developers.hubspot.com/beta-docs/guides/api/crm/objects/leads#create-leads)", + version: "0.0.6", + type: "action", + props: { + ...appProp.props, + contactId: { + propDefinition: [ + appProp.props.hubspot, + "objectId", + () => ({ + objectType: "contact", + }), + ], + label: "Contact ID", + description: "The contact to associate with the lead", + }, + ...common.props, + }, + methods: { + ...common.methods, + getObjectType() { + return OBJECT_TYPE.LEAD; + }, + createObject(opts) { + return this.hubspot.createObject({ + ...opts, + data: { + ...opts?.data, + associations: [ + { + types: [ + { + associationCategory: ASSOCIATION_CATEGORY.HUBSPOT_DEFINED, + associationTypeId: 578, // ID for "Lead with Primary Contact" + }, + ], + to: { + id: this.contactId, + }, + }, + ], + }, + }); + }, + }, +}; diff --git a/components/hubspot/actions/create-or-update-contact/create-or-update-contact.mjs b/components/hubspot/actions/create-or-update-contact/create-or-update-contact.mjs index bc4334aa6a40b..f0c6e86a2cbb0 100644 --- a/components/hubspot/actions/create-or-update-contact/create-or-update-contact.mjs +++ b/components/hubspot/actions/create-or-update-contact/create-or-update-contact.mjs @@ -1,91 +1,25 @@ import { OBJECT_TYPE } from "../../common/constants.mjs"; -import common from "../common-create-object.mjs"; +import common from "../common/common-create-object.mjs"; export default { ...common, key: "hubspot-create-or-update-contact", name: "Create or Update Contact", - description: "Creates a new contact or updates an existing contact based on email address. [See the documentation](https://developers.hubspot.com/docs/api/crm/contacts#endpoint?spec=POST-/crm/v3/objects/contacts)", - version: "0.0.8", + description: "Create or update a contact in Hubspot. [See the documentation](https://developers.hubspot.com/docs/api/crm/contacts#endpoint?spec=POST-/crm/v3/objects/contacts)", + version: "0.0.16", type: "action", - async additionalProps() { - const schema = await this.hubspot.getSchema(this.getObjectType()); - const { results: properties } = await this.hubspot.getProperties(this.getObjectType()); - const props = properties - .filter(this.isRelevantProperty) - .map((property) => this.makePropDefinition(property, schema.requiredProperties)) - .reduce((p, { - name, ...definition - }) => { - p[name] = definition; - return p; - }, {}); - const { - email, ...otherProps - } = props; - return email - ? { - email, - ...otherProps, - } - : props; + props: { + ...common.props, + updateIfExists: { + label: "Update If Exists", + description: "When selected, if Hubspot returns an error upon creation the resource should be updated.", + type: "boolean", + }, }, methods: { ...common.methods, getObjectType() { return OBJECT_TYPE.CONTACT; }, - async getContact(email, $) { - if (!email) { - return; - } - const data = { - object: "contacts", - filters: [ - { - propertyName: "email", - operator: "EQ", - value: email, - }, - ], - }; - return this.hubspot.searchCRM(data, $); - }, - }, - async run({ $ }) { - const { - hubspot, - /* eslint-disable no-unused-vars */ - propertyGroups, - $db, - ...properties - } = this; - const objectType = this.getObjectType(); - - // checkbox (string[]) props must be semicolon separated strings - Object.keys(properties) - .forEach((key) => { - let value = properties[key]; - if (Array.isArray(value)) { - properties[key] = value.join(";"); - } - }); - - // search for the contact with the email provided - const { - total, results, - } = await this.getContact(this.email, $) || {}; - - // if contact is found, update it. if not, create new contact. - const response = total > 0 - ? await hubspot.updateObject(objectType, properties, results[0].id, $) - : await hubspot.createObject(objectType, properties, $); - - const objectName = hubspot.getObjectTypeName(objectType); - $.export("$summary", `Successfully ${total > 0 - ? "updated" - : "created"} ${objectName}`); - - return response; }, }; diff --git a/components/hubspot/actions/create-ticket/create-ticket.mjs b/components/hubspot/actions/create-ticket/create-ticket.mjs index ad19e245e1d15..49d4603f5749c 100644 --- a/components/hubspot/actions/create-ticket/create-ticket.mjs +++ b/components/hubspot/actions/create-ticket/create-ticket.mjs @@ -1,17 +1,45 @@ import { OBJECT_TYPE } from "../../common/constants.mjs"; -import common from "../common-create-object.mjs"; +import common from "../common/common-create-object.mjs"; export default { ...common, key: "hubspot-create-ticket", name: "Create Ticket", description: "Create a ticket in Hubspot. [See the documentation](https://developers.hubspot.com/docs/api/crm/tickets)", - version: "0.0.1", + version: "0.0.9", type: "action", + props: { + ...common.props, + subject: { + type: "string", + label: "Ticket Name", + description: "The name of the ticket", + }, + hs_pipeline: { + propDefinition: [ + common.props.hubspot, + "ticketPipeline", + ], + }, + hs_pipeline_stage: { + propDefinition: [ + common.props.hubspot, + "ticketStage", + (c) => ({ + pipelineId: c.hs_pipeline, + }), + ], + }, + }, methods: { ...common.methods, getObjectType() { return OBJECT_TYPE.TICKET; }, + isDefaultProperty(property) { + return property.name === "subject" + || property.name === "hs_pipeline" + || property.name === "hs_pipeline_stage"; + }, }, }; diff --git a/components/hubspot/actions/enroll-contact-into-workflow/enroll-contact-into-workflow.mjs b/components/hubspot/actions/enroll-contact-into-workflow/enroll-contact-into-workflow.mjs index 2ac0e974b3dac..91f375ae822af 100644 --- a/components/hubspot/actions/enroll-contact-into-workflow/enroll-contact-into-workflow.mjs +++ b/components/hubspot/actions/enroll-contact-into-workflow/enroll-contact-into-workflow.mjs @@ -3,8 +3,8 @@ import hubspot from "../../hubspot.app.mjs"; export default { key: "hubspot-enroll-contact-into-workflow", name: "Enroll Contact Into Workflow", - description: "Add a contact to a workflow. Note: The Workflows API currently only supports contact-based workflows. [See the documentation](https://legacydocs.hubspot.com/docs/methods/workflows/add_contact)", - version: "0.0.8", + description: "Add a contact to a workflow. Note: The Workflows API currently only supports contact-based workflows and is only available for Marketing Hub Enterprise accounts. [See the documentation](https://legacydocs.hubspot.com/docs/methods/workflows/add_contact)", + version: "0.0.14", type: "action", props: { hubspot, @@ -27,7 +27,11 @@ export default { workflow, contactEmail, } = this; - const response = await this.hubspot.addContactsIntoWorkflow(workflow, contactEmail, $); + const response = await this.hubspot.addContactsIntoWorkflow({ + workflowId: workflow, + contactEmail, + $, + }); $.export("$summary", "Successfully added contact into workflow"); return response; diff --git a/components/hubspot/actions/get-company/get-company.mjs b/components/hubspot/actions/get-company/get-company.mjs index e8dd4e663f712..9ef9444053763 100644 --- a/components/hubspot/actions/get-company/get-company.mjs +++ b/components/hubspot/actions/get-company/get-company.mjs @@ -1,12 +1,12 @@ import { OBJECT_TYPE } from "../../common/constants.mjs"; -import common from "../common-get-object.mjs"; +import common from "../common/common-get-object.mjs"; export default { ...common, key: "hubspot-get-company", name: "Get Company", description: "Gets a company. [See the documentation](https://developers.hubspot.com/docs/api/crm/companies#endpoint?spec=GET-/crm/v3/objects/companies/{companyId})", - version: "0.0.8", + version: "0.0.14", type: "action", props: { ...common.props, diff --git a/components/hubspot/actions/get-contact/get-contact.mjs b/components/hubspot/actions/get-contact/get-contact.mjs index 20f6890a15be4..a655e8f088716 100644 --- a/components/hubspot/actions/get-contact/get-contact.mjs +++ b/components/hubspot/actions/get-contact/get-contact.mjs @@ -1,12 +1,12 @@ import { OBJECT_TYPE } from "../../common/constants.mjs"; -import common from "../common-get-object.mjs"; +import common from "../common/common-get-object.mjs"; export default { ...common, key: "hubspot-get-contact", name: "Get Contact", description: "Gets a contact. [See the documentation](https://developers.hubspot.com/docs/api/crm/contacts#endpoint?spec=GET-/crm/v3/objects/contacts/{contactId})", - version: "0.0.8", + version: "0.0.14", type: "action", props: { ...common.props, diff --git a/components/hubspot/actions/get-deal/get-deal.mjs b/components/hubspot/actions/get-deal/get-deal.mjs index ddcbf434fc741..b4c4bbb230a41 100644 --- a/components/hubspot/actions/get-deal/get-deal.mjs +++ b/components/hubspot/actions/get-deal/get-deal.mjs @@ -1,12 +1,12 @@ import { OBJECT_TYPE } from "../../common/constants.mjs"; -import common from "../common-get-object.mjs"; +import common from "../common/common-get-object.mjs"; export default { ...common, key: "hubspot-get-deal", name: "Get Deal", description: "Gets a deal. [See the documentation](https://developers.hubspot.com/docs/api/crm/deals#endpoint?spec=GET-/crm/v3/objects/deals/{dealId})", - version: "0.0.8", + version: "0.0.14", type: "action", props: { ...common.props, diff --git a/components/hubspot/actions/get-file-public-url/get-file-public-url.mjs b/components/hubspot/actions/get-file-public-url/get-file-public-url.mjs index 4956737717a6f..f9a6fd51d7d7e 100644 --- a/components/hubspot/actions/get-file-public-url/get-file-public-url.mjs +++ b/components/hubspot/actions/get-file-public-url/get-file-public-url.mjs @@ -4,7 +4,7 @@ export default { key: "hubspot-get-file-public-url", name: "Get File Public URL", description: "Get a publicly available URL for a file that was uploaded using a Hubspot form. [See the documentation](https://developers.hubspot.com/docs/api/files/files#endpoint?spec=GET-/files/v3/files/{fileId}/signed-url)", - version: "0.0.8", + version: "0.0.14", type: "action", props: { hubspot, @@ -27,16 +27,18 @@ export default { fileUrl, expirationSeconds, } = this; - const { results: files } = await this.hubspot.searchFiles({ - url: fileUrl, - }); - const fileId = files?.[0]?.id; + const { results: files } = await this.hubspot.searchFiles(); + const file = files.find(({ url }) => url === fileUrl ); + const fileId = file.id; if (!fileId) { throw new Error(`File not found at ${fileUrl}`); } // result: { url: string } - const result = await this.hubspot.getSignedUrl(fileId, { - expirationSeconds, + const result = await this.hubspot.getSignedUrl({ + fileId, + params: { + expirationSeconds, + }, }); $.export("$summary", "Successfully retrieved a publicly available URL"); return result; diff --git a/components/hubspot/actions/search-crm/search-crm.mjs b/components/hubspot/actions/search-crm/search-crm.mjs index 02626d99ba846..e4fe78ace48d6 100644 --- a/components/hubspot/actions/search-crm/search-crm.mjs +++ b/components/hubspot/actions/search-crm/search-crm.mjs @@ -1,13 +1,22 @@ import hubspot from "../../hubspot.app.mjs"; import { - SEARCHABLE_OBJECT_TYPES, SEARCHABLE_OBJECT_PROPERTIES, + SEARCHABLE_OBJECT_TYPES, + DEFAULT_CONTACT_PROPERTIES, + DEFAULT_COMPANY_PROPERTIES, + DEFAULT_DEAL_PROPERTIES, + DEFAULT_TICKET_PROPERTIES, + DEFAULT_PRODUCT_PROPERTIES, + DEFAULT_LINE_ITEM_PROPERTIES, + DEFAULT_LEAD_PROPERTIES, } from "../../common/constants.mjs"; +import common from "../common/common-create.mjs"; +import { ConfigurationError } from "@pipedream/platform"; export default { key: "hubspot-search-crm", name: "Search CRM", - description: "Search companies, contacts, deals, feedback submissions, products, tickets, line-items, or quotes. [See the documentation](https://developers.hubspot.com/docs/api/crm/search)", - version: "0.0.6", + description: "Search companies, contacts, deals, feedback submissions, products, tickets, line-items, quotes, leads, or custom objects. [See the documentation](https://developers.hubspot.com/docs/api/crm/search)", + version: "1.0.0", type: "action", props: { hubspot, @@ -15,56 +24,224 @@ export default { type: "string", label: "Object Type", description: "Type of CRM object to search for", - options: SEARCHABLE_OBJECT_TYPES, + options: [ + ...SEARCHABLE_OBJECT_TYPES, + { + label: "Custom Object", + value: "custom_object", + }, + ], + reloadProps: true, + }, + createIfNotFound: { + type: "boolean", + label: "Create if not found?", + description: "Set to `true` to create the Hubspot object if it doesn't exist", + default: false, + optional: true, reloadProps: true, }, }, async additionalProps() { const props = {}; - props.searchProperty = { - type: "string", - label: "Search Property", - description: "The field to search", - options: this.getSearchProperties(), - }; + + if (this.objectType === "custom_object") { + try { + props.customObjectType = { + type: "string", + label: "Custom Object Type", + options: async () => await this.getCustomObjectTypes(), + reloadProps: true, + }; + } catch { + props.customObjectType = { + type: "string", + label: "Custom Object Type", + reloadProps: true, + }; + } + } + if (!this.objectType || (this.objectType === "custom_object" && !this.customObjectType)) { + return props; + } + + let schema; + const objectType = this.customObjectType ?? this.objectType; + try { + schema = await this.hubspot.getSchema({ + objectType, + }); + const properties = schema.properties; + const searchableProperties = schema.searchableProperties?.map((prop) => { + const propData = properties.find(({ name }) => name === prop); + return { + label: propData.label, + value: propData.name, + }; + }); + + props.searchProperty = { + type: "string", + label: "Search Property", + description: "The field to search", + options: searchableProperties, + }; + } catch { + props.searchProperty = { + type: "string", + label: "Search Property", + description: "The field to search", + }; + } + props.searchValue = { type: "string", label: "Search Value", - description: "Search for objects where the specified search field/property matches the search value", + description: "Search for objects where the specified search field/property contains an exact match of the search value", }; - // eslint-disable-next-line pipedream/props-description - props.additionalProperties = { - type: "string[]", - label: "Properties to return with the results", - optional: true, - options: async ({ page }) => { - if (page !== 0) { - return []; + const defaultProperties = this.getDefaultProperties(); + if (defaultProperties?.length) { + props.info = { + type: "alert", + alertType: "info", + content: `Properties:\n\`${defaultProperties.join(", ")}\``, + }; + } + + try { + // eslint-disable-next-line pipedream/props-description + props.additionalProperties = { + type: "string[]", + label: "Additional properties to retrieve", + optional: true, + options: async ({ page }) => { + if (page !== 0) { + return []; + } + const { results: properties } = await this.hubspot.getProperties({ + objectType: this.customObjectType ?? this.objectType, + }); + const defaultProperties = this.getDefaultProperties(); + return properties.filter(({ name }) => !defaultProperties.includes(name)) + .map((property) => ({ + label: property.label, + value: property.name, + })); + }, + }; + } catch { + props.additionalProperties = { + type: "string[]", + label: "Additional properties to retrieve", + optional: true, + }; + } + + let creationProps = {}; + if (this.createIfNotFound && objectType) { + try { + const { results: properties } = await this.hubspot.getProperties({ + objectType, + }); + const relevantProperties = properties.filter(this.isRelevantProperty); + const propDefinitions = []; + for (const property of relevantProperties) { + propDefinitions.push(await this.makePropDefinition(property, schema.requiredProperties)); } - const { results: properties } = await this.hubspot.getProperties(this.objectType); - return properties.map((property) => ({ - label: property.label, - value: property.name, - })); - }, + creationProps = propDefinitions + .reduce((props, { + name, ...definition + }) => { + props[name] = definition; + return props; + }, {}); + } catch { + props.creationProps = { + type: "object", + label: "Object Properties", + description: "A JSON object containing the object to create if not found", + }; + } + } + return { + ...props, + ...creationProps, }; - return props; }, methods: { - getSearchProperties() { - return SEARCHABLE_OBJECT_PROPERTIES[this.objectType]; + ...common.methods, + getObjectType() { + return this.objectType; + }, + getDefaultProperties() { + if (this.objectType === "contact") { + return DEFAULT_CONTACT_PROPERTIES; + } else if (this.objectType === "company") { + return DEFAULT_COMPANY_PROPERTIES; + } else if (this.objectType === "deal") { + return DEFAULT_DEAL_PROPERTIES; + } else if (this.objectType === "ticket") { + return DEFAULT_TICKET_PROPERTIES; + } else if (this.objectType === "product") { + return DEFAULT_PRODUCT_PROPERTIES; + } else if (this.objectType === "line_item") { + return DEFAULT_LINE_ITEM_PROPERTIES; + } else if (this.objectType === "lead") { + return DEFAULT_LEAD_PROPERTIES; + } else { + return []; + } + }, + async getCustomObjectTypes() { + const { results } = await this.hubspot.listSchemas(); + return results?.map(({ + fullyQualifiedName: value, labels, + }) => ({ + value, + label: labels.plural, + })) || []; }, }, async run({ $ }) { const { + hubspot, objectType, - additionalProperties, + customObjectType, + additionalProperties = [], searchProperty, searchValue, + /* eslint-disable no-unused-vars */ + info, + createIfNotFound, + creationProps, + ...otherProperties } = this; + + const actualObjectType = customObjectType ?? objectType; + + const schema = await this.hubspot.getSchema({ + objectType: actualObjectType, + }); + + if (!schema.searchableProperties.includes(searchProperty)) { + throw new ConfigurationError( + `Property \`${searchProperty}\` is not a searchable property of object type \`${objectType}\`. ` + + `\n\nAvailable searchable properties are: \`${schema.searchableProperties.join("`, `")}\``, + ); + } + + const properties = creationProps + ? typeof creationProps === "string" + ? JSON.parse(creationProps) + : creationProps + : otherProperties; + + const defaultProperties = this.getDefaultProperties(); const data = { - object: objectType, - properties: additionalProperties, + properties: [ + ...defaultProperties, + ...additionalProperties, + ], filters: [ { propertyName: searchProperty, @@ -73,7 +250,25 @@ export default { }, ], }; - const { results } = await this.hubspot.searchCRM(data, $); + const { results } = await hubspot.searchCRM({ + object: actualObjectType, + data, + $, + }); + + if (!results?.length && createIfNotFound) { + const response = await hubspot.createObject({ + $, + objectType: actualObjectType, + data: { + properties, + }, + }); + const objectName = hubspot.getObjectTypeName(actualObjectType); + $.export("$summary", `Successfully created ${objectName}`); + return response; + } + $.export("$summary", `Successfully retrieved ${results?.length} object(s).`); return results; }, diff --git a/components/hubspot/actions/update-company/update-company.mjs b/components/hubspot/actions/update-company/update-company.mjs index e8b5178e63471..0bc5bd0bec760 100644 --- a/components/hubspot/actions/update-company/update-company.mjs +++ b/components/hubspot/actions/update-company/update-company.mjs @@ -1,13 +1,13 @@ import { OBJECT_TYPE } from "../../common/constants.mjs"; -import common from "../common-update-object.mjs"; -import hubspot from "../../hubspot.app.mjs"; +import common from "../common/common-update-object.mjs"; +import appProp from "../common/common-app-prop.mjs"; export default { ...common, key: "hubspot-update-company", name: "Update Company", description: "Update a company in Hubspot. [See the documentation](https://developers.hubspot.com/docs/api/crm/companies)", - version: "0.0.7", + version: "0.0.14", type: "action", methods: { ...common.methods, @@ -16,10 +16,10 @@ export default { }, }, props: { - hubspot, + ...appProp.props, objectId: { propDefinition: [ - hubspot, + appProp.props.hubspot, "objectId", () => ({ objectType: "company", diff --git a/components/hubspot/actions/update-contact/update-contact.mjs b/components/hubspot/actions/update-contact/update-contact.mjs index ae852a77da020..e26796205e810 100644 --- a/components/hubspot/actions/update-contact/update-contact.mjs +++ b/components/hubspot/actions/update-contact/update-contact.mjs @@ -1,13 +1,13 @@ import { OBJECT_TYPE } from "../../common/constants.mjs"; -import common from "../common-update-object.mjs"; -import hubspot from "../../hubspot.app.mjs"; +import common from "../common/common-update-object.mjs"; +import appProp from "../common/common-app-prop.mjs"; export default { ...common, key: "hubspot-update-contact", name: "Update Contact", description: "Update a contact in Hubspot. [See the documentation](https://developers.hubspot.com/docs/api/crm/contacts#endpoint?spec=POST-/crm/v3/objects/contacts)", - version: "0.0.8", + version: "0.0.15", type: "action", methods: { ...common.methods, @@ -16,10 +16,10 @@ export default { }, }, props: { - hubspot, + ...appProp.props, objectId: { propDefinition: [ - hubspot, + appProp.props.hubspot, "objectId", () => ({ objectType: "contact", diff --git a/components/hubspot/actions/update-custom-object/update-custom-object.mjs b/components/hubspot/actions/update-custom-object/update-custom-object.mjs new file mode 100644 index 0000000000000..2b679831acc62 --- /dev/null +++ b/components/hubspot/actions/update-custom-object/update-custom-object.mjs @@ -0,0 +1,37 @@ +import common from "../common/common-update-object.mjs"; +import appProp from "../common/common-app-prop.mjs"; + +export default { + ...common, + key: "hubspot-update-custom-object", + name: "Update Custom Object", + description: "Update a custom object in Hubspot. [See the documentation](https://developers.hubspot.com/beta-docs/guides/api/crm/objects/custom-objects#update-existing-custom-objects)", + version: "1.0.0", + type: "action", + methods: { + ...common.methods, + getObjectType() { + return this.customObjectType; + }, + }, + props: { + ...appProp.props, + customObjectType: { + propDefinition: [ + appProp.props.hubspot, + "customObjectType", + ], + }, + objectId: { + propDefinition: [ + appProp.props.hubspot, + "objectId", + (c) => ({ + objectType: c.customObjectType, + }), + ], + description: "The ID of the custom object", + }, + ...common.props, + }, +}; diff --git a/components/hubspot/actions/update-deal/update-deal.mjs b/components/hubspot/actions/update-deal/update-deal.mjs new file mode 100644 index 0000000000000..4156969743262 --- /dev/null +++ b/components/hubspot/actions/update-deal/update-deal.mjs @@ -0,0 +1,31 @@ +import { OBJECT_TYPE } from "../../common/constants.mjs"; +import common from "../common/common-update-object.mjs"; +import appProp from "../common/common-app-prop.mjs"; + +export default { + ...common, + key: "hubspot-update-deal", + name: "Update Deal", + description: "Update a deal in Hubspot. [See the documentation](https://developers.hubspot.com/beta-docs/guides/api/crm/objects/deals#update-deals)", + version: "0.0.5", + type: "action", + methods: { + ...common.methods, + getObjectType() { + return OBJECT_TYPE.DEAL; + }, + }, + props: { + ...appProp.props, + objectId: { + propDefinition: [ + appProp.props.hubspot, + "objectId", + () => ({ + objectType: "deal", + }), + ], + }, + ...common.props, + }, +}; diff --git a/components/hubspot/actions/update-lead/update-lead.mjs b/components/hubspot/actions/update-lead/update-lead.mjs new file mode 100644 index 0000000000000..705b8537f9f68 --- /dev/null +++ b/components/hubspot/actions/update-lead/update-lead.mjs @@ -0,0 +1,28 @@ +import { OBJECT_TYPE } from "../../common/constants.mjs"; +import common from "../common/common-update-object.mjs"; +import appProp from "../common/common-app-prop.mjs"; + +export default { + ...common, + key: "hubspot-update-lead", + name: "Update Lead", + description: "Update a lead in Hubspot. [See the documentation](https://developers.hubspot.com/beta-docs/guides/api/crm/objects/leads#update-leads)", + version: "0.0.6", + type: "action", + methods: { + ...common.methods, + getObjectType() { + return OBJECT_TYPE.LEAD; + }, + }, + props: { + ...appProp.props, + objectId: { + propDefinition: [ + appProp.props.hubspot, + "leadId", + ], + }, + ...common.props, + }, +}; diff --git a/components/hubspot/common/constants.mjs b/components/hubspot/common/constants.mjs index aa1f2d8019ba6..721f175ce1d14 100644 --- a/components/hubspot/common/constants.mjs +++ b/components/hubspot/common/constants.mjs @@ -3,7 +3,6 @@ import { OBJECT_TYPE, OBJECT_TYPES, SEARCHABLE_OBJECT_TYPES, - SEARCHABLE_OBJECT_PROPERTIES, } from "./object-types.mjs"; const BASE_URL = "https://api.hubapi.com"; @@ -11,7 +10,7 @@ const API_PATH = { PROPERTIES: "/properties/v1", CONTACTS: "/contacts/v1", ENGAGEMENTS: "/engagements/v1", - EMAIL: "/email/public/v1/", + EMAIL: "/email/public/v1", EVENTS: "/events/v3", FORMS: "/forms/v2", FORM_INTEGRATIONS: "/form-integrations/v1", @@ -22,6 +21,7 @@ const API_PATH = { CMS: "/cms/v3", AUTOMATION: "/automation/v2", DEAL: "/deals/v1", + BUSINESS_UNITS: "/business-units/v3", }; /** Association categories for association types, as defined by the [Hubspot API @@ -33,13 +33,180 @@ const ASSOCIATION_CATEGORY = { INTEGRATOR_DEFINED: "INTEGRATOR_DEFINED", }; +const DEFAULT_LIMIT = 100; + +const DEFAULT_CONTACT_PROPERTIES = [ + "firstname", + "lastname", + "email", + "company", + "website", + "mobilephone", + "phone", + "fax", + "address", + "city", + "state", + "zip", + "salutation", + "country", + "jobtitle", + "hs_createdate", + "hs_email_domain", + "hs_object_id", + "lastmodifieddate", + "hs_persona", + "hs_language", + "lifecyclestage", + "createdate", + "numemployees", + "annualrevenue", + "industry", +]; + +const DEFAULT_COMPANY_PROPERTIES = [ + "name", + "domain", + "industry", + "about_us", + "phone", + "address", + "address2", + "city", + "state", + "zip", + "country", + "website", + "type", + "description", + "founded_year", + "hs_createdate", + "hs_lastmodifieddate", + "hs_object_id", + "is_public", + "timezone", + "total_money_raised", + "total_revenue", + "owneremail", + "ownername", + "numberofemployees", + "annualrevenue", + "lifecyclestage", + "createdate", + "web_technologies", +]; + +const DEFAULT_DEAL_PROPERTIES = [ + "dealtype", + "dealname", + "amount", + "description", + "closedate", + "createdate", + "num_associated_contacts", + "hs_forecast_amount", + "hs_forecast_probability", + "hs_manual_forecast_category", + "hs_next_step", + "hs_object_id", + "hs_lastmodifieddate", + "hubspot_owner_id", + "hubspot_team_id", +]; + +const DEFAULT_TICKET_PROPERTIES = [ + "subject", + "content", + "source_type", + "createdate", + "hs_pipeline", + "hs_pipeline_stage", + "hs_resolution", + "hs_ticket_category", + "hs_ticket_id", + "hs_ticket_priority", + "hs_lastmodifieddate", + "hubspot_owner_id", + "hubspot_team_id", +]; + +const DEFAULT_PRODUCT_PROPERTIES = [ + "createdate", + "description", + "name", + "price", + "tax", + "hs_lastmodifieddate", +]; + +const DEFAULT_LINE_ITEM_PROPERTIES = [ + "name", + "description", + "price", + "quantity", + "amount", + "discount", + "tax", + "createdate", + "hs_object_id", + "hs_product_id", + "hs_images", + "hs_lastmodifieddate", + "hs_line_item_currency_code", + "hs_sku", + "hs_url", + "hs_cost_of_goods_sold", + "hs_discount_percentage", + "hs_term_in_months", +]; + +const DEFAULT_LEAD_PROPERTIES = [ + "hs_associated_contact_email", + "hs_associated_contact_lastname", + "hs_lead_name", + "hs_associated_company_domain", + "hs_associated_contact_firstname", + "hs_associated_company_name", +]; + +const ENGAGEMENT_TYPE_OPTIONS = [ + { + label: "Note", + value: "notes", + }, + { + label: "Task", + value: "tasks", + }, + { + label: "Meeting", + value: "meetings", + }, + { + label: "Email", + value: "emails", + }, + { + label: "Call", + value: "calls", + }, +]; + export { OBJECT_TYPE, OBJECT_TYPES, SEARCHABLE_OBJECT_TYPES, - SEARCHABLE_OBJECT_PROPERTIES, HUBSPOT_OWNER, BASE_URL, API_PATH, ASSOCIATION_CATEGORY, + DEFAULT_LIMIT, + DEFAULT_CONTACT_PROPERTIES, + DEFAULT_COMPANY_PROPERTIES, + DEFAULT_DEAL_PROPERTIES, + DEFAULT_TICKET_PROPERTIES, + DEFAULT_PRODUCT_PROPERTIES, + DEFAULT_LINE_ITEM_PROPERTIES, + DEFAULT_LEAD_PROPERTIES, + ENGAGEMENT_TYPE_OPTIONS, }; diff --git a/components/hubspot/common/object-types.mjs b/components/hubspot/common/object-types.mjs index 6476744846388..26b3c15073edc 100644 --- a/components/hubspot/common/object-types.mjs +++ b/components/hubspot/common/object-types.mjs @@ -17,6 +17,7 @@ const OBJECT_TYPE = { NOTE: "note", EMAIL: "email", TASK: "task", + LEAD: "lead", }; /** @@ -77,6 +78,10 @@ const OBJECT_TYPES = [ label: "Tasks", value: OBJECT_TYPE.TASK, }, + { + label: "Leads", + value: OBJECT_TYPE.LEAD, + }, ]; const SEARCHABLE_OBJECT_TYPES_ARRAY = [ @@ -87,75 +92,83 @@ const SEARCHABLE_OBJECT_TYPES_ARRAY = [ OBJECT_TYPE.PRODUCT, OBJECT_TYPE.TICKET, OBJECT_TYPE.LINE_ITEM, + OBJECT_TYPE.LEAD, ]; const SEARCHABLE_OBJECT_TYPES = OBJECT_TYPES.filter( (type) => SEARCHABLE_OBJECT_TYPES_ARRAY.includes(type.value), ); -const SEARCHABLE_OBJECT_PROPERTIES = { - [OBJECT_TYPE.COMPANY]: [ - "name", - "domain", - "createdate", - "hs_lastmodifieddate", - "hs_object_id", - ], - [OBJECT_TYPE.CONTACT]: [ - "firstname", - "lastname", - "email", - "lastmodifieddate", - "hs_object_id", - "createdate", - ], - [OBJECT_TYPE.DEAL]: [ - "dealname", - "amount", - "closedate", - "pipeline", - "dealstage", - "createdate", - "hs_lastmodifieddate", - "hs_object_id", - ], - [OBJECT_TYPE.FEEDBACK_SUBMISSION]: [ - "hs_createdate", - "hs_lastmodifieddate", - "hs_object_id", - ], - [OBJECT_TYPE.PRODUCT]: [ - "name", - "description", - "price", - "createdate", - "hs_lastmodifieddate", - "hs_object_id", - ], - [OBJECT_TYPE.TICKET]: [ - "content", - "hs_pipeline", - "hs_pipeline_stage", - "hs_ticket_category", - "hs_ticket_priority", - "subject", - "createdate", - "hs_lastmodifieddate", - "hs_object_id", - ], - [OBJECT_TYPE.LINE_ITEM]: [ - "quantity", - "amount", - "price", - "createdate", - "hs_lastmodifieddate", - "hs_object_id", - ], -}; +const ENGAGEMENT_TYPES = [ + { + label: "Note", + value: "NOTE", + }, + { + label: "Task", + value: "TASK", + }, + { + label: "Meeting", + value: "MEETING", + }, + { + label: "Email", + value: "EMAIL", + }, + { + label: "Call", + value: "CALL", + }, +]; + +const EMAIL_EVENT_TYPES = [ + { + label: "Sent", + value: "SENT", + }, + { + label: "Dropped", + value: "DROPPED", + }, + { + label: "Processed", + value: "PROCESSED", + }, + { + label: "Delivered", + value: "DELIVERED", + }, + { + label: "Deferred", + value: "DEFERRED", + }, + { + label: "Bounce", + value: "BOUNCE", + }, + { + label: "Open", + value: "OPEN", + }, + { + label: "Click", + value: "CLICK", + }, + { + label: "Status Change", + value: "STATUSCHANGE", + }, + { + label: "Spam Report", + value: "SPAMREPORT", + }, +]; export { OBJECT_TYPE, OBJECT_TYPES, HUBSPOT_OWNER, SEARCHABLE_OBJECT_TYPES, - SEARCHABLE_OBJECT_PROPERTIES, + ENGAGEMENT_TYPES, + EMAIL_EVENT_TYPES, }; diff --git a/components/hubspot/hubspot.app.mjs b/components/hubspot/hubspot.app.mjs index b393a1e42afe4..cfd45c3b12fbd 100644 --- a/components/hubspot/hubspot.app.mjs +++ b/components/hubspot/hubspot.app.mjs @@ -1,12 +1,24 @@ import { axios } from "@pipedream/platform"; import { API_PATH, - ASSOCIATION_CATEGORY, BASE_URL, HUBSPOT_OWNER, OBJECT_TYPE, OBJECT_TYPES, + DEFAULT_LIMIT, + DEFAULT_CONTACT_PROPERTIES, + DEFAULT_COMPANY_PROPERTIES, + DEFAULT_DEAL_PROPERTIES, + DEFAULT_TICKET_PROPERTIES, + DEFAULT_PRODUCT_PROPERTIES, + DEFAULT_LINE_ITEM_PROPERTIES, } from "./common/constants.mjs"; +import Bottleneck from "bottleneck"; +const limiter = new Bottleneck({ + minTime: 250, // 4 requests per second + maxConcurrent: 1, +}); +const axiosRateLimiter = limiter.wrap(axios); export default { type: "app", @@ -18,33 +30,21 @@ export default { description: "Select the lists to watch for new contacts.", withLabel: true, async options({ - prevContext, listType, + page, listType, }) { - const { offset = 0 } = prevContext; - const params = { - count: 250, - offset, - }; const { lists } = await this.getLists({ listType, - ...params, - }); - const options = lists.map((result) => { - const { - name: label, - listId, - } = result; - return { - label, - value: listId, - }; - }); - return { - options, - context: { - offset: params.offset + params.count, + params: { + count: DEFAULT_LIMIT, + offset: page * DEFAULT_LIMIT, }, - }; + }); + return lists?.map(({ + listId: value, name: label, + }) => ({ + value, + label, + })) || []; }, }, dealPipeline: { @@ -52,17 +52,15 @@ export default { label: "Pipeline", description: "Select the pipeline to watch for new deals in", async options() { - const { results } = await this.getPipelines("deal"); - return results.map((result) => { - const { - label, - id: value, - } = result; - return { - label, - value, - }; + const { results } = await this.getPipelines({ + objectType: "deal", }); + return results?.map(({ + id: value, label, + }) => ({ + value, + label, + })) || []; }, }, stages: { @@ -70,37 +68,48 @@ export default { label: "Stages", description: "Select the stages to watch for new deals in.", async options({ pipeline }) { - const { results } = await this.getDealStages(pipeline); - return results.map((result) => { - const { - label, - id, - } = result; - return { - label, - value: id, - }; + const { results } = await this.getDealStages({ + pipelineId: pipeline, }); + return results?.map(({ + id: value, label, + }) => ({ + value, + label, + })) || []; }, }, objectType: { type: "string", label: "Object Type", description: "Watch for new events concerning the object type specified.", - options: OBJECT_TYPES, + async options({ includeCustom = false }) { + const objectTypes = OBJECT_TYPES; + if (includeCustom) { + const { results } = await this.listSchemas(); + const customObjects = results?.map(({ + fullyQualifiedName: value, labels, + }) => ({ + value, + label: labels.plural, + })) || []; + objectTypes.push(...customObjects); + } + return objectTypes; + }, }, objectSchema: { type: "string", label: "Object Schema", description: "Watch for new events of objects with the specified custom schema.", async options() { - const response = await this.listSchemas(); - return response?.results?.map(({ - objectTypeId, name, + const { results } = await this.listSchemas(); + return results?.map(({ + objectTypeId: value, name: label, }) => ({ - label: name, - value: objectTypeId, - })); + value, + label, + })) || []; }, }, objectId: { @@ -110,7 +119,9 @@ export default { async options({ objectType, ...opts }) { - return this.createOptions(objectType, opts); + return objectType + ? await this.createOptions(objectType, opts) + : []; }, }, objectIds: { @@ -120,7 +131,9 @@ export default { async options({ objectType, ...opts }) { - return this.createOptions(objectType, opts); + return objectType + ? await this.createOptions(objectType, opts) + : []; }, }, forms: { @@ -128,29 +141,19 @@ export default { label: "Form", description: "Watch for new submissions of the specified forms.", withLabel: true, - async options(prevContext) { - const { offset } = prevContext; - const params = { - count: 50, - offset: offset || 0, - }; - const results = await this.getForms(); - const options = results.map((result) => { - const { - name: label, - guid, - } = result; - return { - label, - value: guid, - }; - }); - return { - options, - context: { - offset: params.offset + params.count, + async options({ page }) { + const results = await this.getForms({ + params: { + count: DEFAULT_LIMIT, + offset: page * DEFAULT_LIMIT, }, - }; + }); + return results?.map(({ + guid: value, name: label, + }) => ({ + value, + label, + })) || []; }, }, channel: { @@ -179,7 +182,9 @@ export default { label: "Property Groups", reloadProps: true, async options({ objectType }) { - const { results: groups } = await this.getPropertyGroups(objectType); + const { results: groups } = await this.getPropertyGroups({ + objectType, + }); return groups.map((group) => ({ label: group.label, value: group.name, @@ -215,8 +220,122 @@ export default { description: "Select the properties to include in the contact object", optional: true, default: [], - async options() { - return this.createPropertiesArray(); + async options({ excludeDefaultProperties }) { + const properties = await this.getContactProperties(); + const relevantProperties = excludeDefaultProperties + ? properties.filter(({ name }) => !DEFAULT_CONTACT_PROPERTIES.includes(name)) + : properties; + return relevantProperties?.map(({ + name: value, label, + }) => ({ + value, + label, + })) || []; + }, + }, + companyProperties: { + type: "string[]", + label: "Company Properties", + description: "Select the properties to include in the company object", + optional: true, + default: [], + async options({ excludeDefaultProperties }) { + const { results: properties } = await this.getProperties({ + objectType: "companies", + }); + const relevantProperties = excludeDefaultProperties + ? properties.filter(({ name }) => !DEFAULT_COMPANY_PROPERTIES.includes(name)) + : properties; + return relevantProperties?.map(({ + name: value, label, + }) => ({ + value, + label, + })) || []; + }, + }, + dealProperties: { + type: "string[]", + label: "Deal Properties", + description: "Select the properties to include in the deal object", + optional: true, + default: [], + async options({ excludeDefaultProperties }) { + const { results: properties } = await this.getProperties({ + objectType: "deals", + }); + const relevantProperties = excludeDefaultProperties + ? properties.filter(({ name }) => !DEFAULT_DEAL_PROPERTIES.includes(name)) + : properties; + return relevantProperties?.map(({ + name: value, label, + }) => ({ + value, + label, + })) || []; + }, + }, + ticketProperties: { + type: "string[]", + label: "Ticket Properties", + description: "Select the properties to include in the ticket object", + optional: true, + default: [], + async options({ excludeDefaultProperties }) { + const { results: properties } = await this.getProperties({ + objectType: "tickets", + }); + const relevantProperties = excludeDefaultProperties + ? properties.filter(({ name }) => !DEFAULT_TICKET_PROPERTIES.includes(name)) + : properties; + return relevantProperties?.map(({ + name: value, label, + }) => ({ + value, + label, + })) || []; + }, + }, + productProperties: { + type: "string[]", + label: "Product Properties", + description: "Select the properties to include in the product object", + optional: true, + default: [], + async options({ excludeDefaultProperties }) { + const { results: properties } = await this.getProperties({ + objectType: "products", + }); + const relevantProperties = excludeDefaultProperties + ? properties.filter(({ name }) => !DEFAULT_PRODUCT_PROPERTIES.includes(name)) + : properties; + return relevantProperties?.map(({ + name: value, label, + }) => ({ + value, + label, + })) || []; + }, + }, + lineItemProperties: { + type: "string[]", + label: "Line Item Properties", + description: "Select the properties to include in the line item object", + optional: true, + default: [], + async options({ excludeDefaultProperties }) { + const { results: properties } = await this.getProperties({ + objectType: "line_items", + }); + const relevantProperties = excludeDefaultProperties + ? properties.filter(({ name }) => !DEFAULT_LINE_ITEM_PROPERTIES.includes(name)) + : properties; + return relevantProperties?.map(({ + name: value, label, + }) => ({ + value, + label, + })) || []; }, }, workflow: { @@ -254,16 +373,81 @@ export default { async options({ fromObjectType, toObjectType, }) { - const { results: associationTypes } = await this.getAssociationTypes( + if (!fromObjectType || !toObjectType) { + return []; + } + const { results: associationTypes } = await this.getAssociationTypes({ fromObjectType, toObjectType, - ); + }); return associationTypes.map((associationType) => ({ label: associationType.label ?? `${fromObjectType}_to_${toObjectType}`, value: associationType.typeId, })); }, }, + ticketPipeline: { + type: "string", + label: "Pipeline", + description: "The pipeline of the ticket", + async options() { + const { results } = await this.getPipelines({ + objectType: "ticket", + }); + return results?.map(({ + id: value, label, + }) => ({ + value, + label, + })) || []; + }, + }, + ticketStage: { + type: "string", + label: "Ticket Stage", + description: "The stage of the ticket", + async options({ pipelineId }) { + const stages = await this.getTicketStages({ + pipelineId, + }); + return stages.map(({ + id: value, label, + }) => ({ + value, + label, + })); + }, + }, + leadId: { + type: "string", + label: "Lead ID", + description: "The identifier of the lead", + async options() { + const { results } = await this.listObjectsInPage("lead", undefined, { + properties: "hs_lead_name", + }); + return results?.map(({ + id: value, properties, + }) => ({ + value, + label: properties?.hs_lead_name || value, + })) || []; + }, + }, + customObjectType: { + type: "string", + label: "Custom Object Type", + description: "The type of custom object to create", + async options() { + const { results } = await this.listSchemas(); + return results?.map(({ + name, fullyQualifiedName, + }) => ({ + label: name, + value: fullyQualifiedName, + }) ) || []; + }, + }, }, methods: { _getHeaders() { @@ -272,21 +456,41 @@ export default { "Content-Type": "application/json", }; }, - async makeRequest(api, endpoint, opts = {}) { - const { - method = "GET", - params, - data, - $, - } = opts; - const config = { - method, + // Recursively trim string values in accordance with Hubspot's validation rules + // https://developers.hubspot.com/changelog/breaking-change-enhanced-validations-for-non-string-properties-in-hubspots-crmobject-apis + trimStringValues(obj) { + if (typeof obj === "string") { + return obj.trim(); + } else if (Array.isArray(obj)) { + return obj.map(this.trimStringValues); + } else if (obj !== null && typeof obj === "object") { + return Object.fromEntries( + Object.entries(obj).map(([ + key, + value, + ]) => [ + key, + this.trimStringValues(value), + ]), + ); + } + return obj; + }, + makeRequest({ + $ = this, + api, + endpoint, + data, + params, + ...otherOpts + }) { + return axiosRateLimiter($, { url: `${BASE_URL}${api}${endpoint}`, headers: this._getHeaders(), - params, - data, - }; - return axios($ ?? this, config); + data: data && this.trimStringValues(data), + params: params && this.trimStringValues(params), + ...otherOpts, + }); }, getObjectTypeName(objectType) { if (!objectType.endsWith("s")) { @@ -340,112 +544,223 @@ export default { return id; } }, - async searchCRM({ - object, ...data - }, $) { - return this.makeRequest(API_PATH.CRMV3, `/objects/${object}/search`, { - method: "POST", - data, - $, - }); + /** + * Returns a list of prop options for a CRM object type + * + * @param {*} referencedObjectType The object type to + * @param {object} [opts = {}] - an object representing configuration + * options used to create prop options, intended to be passed from the first + * argument of the prop's async options + * @returns a list of prop options + */ + async createOptions(referencedObjectType, opts = {}) { + const { + prevContext, + page, + } = opts; + const { nextAfter } = prevContext; + if (page !== 0 && !nextAfter) { + return []; + } + const { + paging, + results, + } = await this.listObjectsInPage(referencedObjectType, nextAfter); + return { + options: results.map((object) => ({ + label: this.getObjectLabel(object, referencedObjectType) ?? object.id, + value: object.id, + })), + context: { + nextAfter: paging?.next.after, + }, + }; }, - async getBlogPosts(params, $) { - return this.makeRequest(API_PATH.CMS, "/blogs/posts", { + getCompaniesOptions(opts) { + return this.createOptions(OBJECT_TYPE.COMPANY, opts); + }, + getContactsOptions(opts) { + return this.createOptions(OBJECT_TYPE.CONTACT, opts); + }, + getLineItemsOptions(opts) { + return this.createOptions(OBJECT_TYPE.LINE_ITEM, opts); + }, + getTicketsOptions(opts) { + return this.createOptions(OBJECT_TYPE.TICKET, opts); + }, + getQuotesOptions(opts) { + return this.createOptions(OBJECT_TYPE.QUOTE, opts); + }, + getCallsOptions(opts) { + return this.createOptions(OBJECT_TYPE.CALL, opts); + }, + getTasksOptions(opts) { + return this.createOptions(OBJECT_TYPE.TASK, opts); + }, + getNotesOptions(opts) { + return this.createOptions(OBJECT_TYPE.NOTE, opts); + }, + getMeetingsOptions(opts) { + return this.createOptions(OBJECT_TYPE.MEETING, opts); + }, + getEmailsOptions(opts) { + return this.createOptions(OBJECT_TYPE.EMAIL, opts); + }, + async getOwnersOptions(params) { + const { results } = await this.getOwners({ params, - $, }); + return results.map((object) => ({ + label: object.email, + value: object.id, + })); }, - async getContactProperties($) { - return this.makeRequest(API_PATH.PROPERTIES, "/contacts/properties", { - $, + async getPipelinesOptions(objectType) { + const { results } = await this.getPipelines({ + objectType, }); + return results?.map((pipeline) => ({ + label: pipeline.label, + value: pipeline.id, + })) || []; }, - async createPropertiesArray($) { - const allProperties = await this.getContactProperties($); - return allProperties.map((property) => property.name); + async getPipelineStagesOptions(objectType, pipelineId) { + if (!pipelineId) { + return []; + } + const { stages } = await this.getPipeline({ + objectType, + pipelineId, + }); + return stages?.map((stage) => ({ + label: stage.label, + value: stage.id, + })) || []; + }, + async getBusinessUnitOptions() { + const { results } = await this.getBusinessUnits(); + return results?.map((unit) => ({ + label: unit.name, + value: unit.id, + })) || []; + }, + searchCRM({ + object, ...opts + }) { + return this.makeRequest({ + api: API_PATH.CRMV3, + method: "POST", + endpoint: `/objects/${object}/search`, + ...opts, + }); }, - async getDealProperties($) { - return this.makeRequest(API_PATH.PROPERTIES, "/deals/properties", { - $, + getBlogPosts(opts = {}) { + return this.makeRequest({ + api: API_PATH.CMS, + endpoint: "/blogs/posts", + ...opts, }); }, - async getDealStages(pipelineId, $) { - return this.makeRequest(API_PATH.CRMV3, `/pipelines/deal/${pipelineId}/stages`, { - $, + getContactProperties(opts = {}) { + return this.makeRequest({ + api: API_PATH.PROPERTIES, + endpoint: "/contacts/properties", + ...opts, }); }, - async getEmailEvents(params, $) { - return this.makeRequest(API_PATH.EMAIL, "/events", { - params, - $, + getDealProperties(opts = {}) { + return this.makeRequest({ + api: API_PATH.PROPERTIES, + endpoint: "/deals/properties", + ...opts, }); }, - async getEngagements(params, $) { - return this.makeRequest( - API_PATH.ENGAGEMENTS, - "/engagements/paged", - { - params, - $, - }, - ); + getDealStages({ + pipelineId, ...opts + }) { + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: `/pipelines/deal/${pipelineId}/stages`, + ...opts, + }); }, - async getEvents(params, $) { - return this.makeRequest(API_PATH.EVENTS, "/events", { - params, - $, + async getTicketStages({ pipelineId }) { + const { results: pipelines } = await this.getPipelines({ + objectType: "ticket", + }); + const { stages } = pipelines.find(({ id }) => id == pipelineId); + return stages; + }, + getEmailEvents(opts = {}) { + return this.makeRequest({ + api: API_PATH.EMAIL, + endpoint: "/events", + ...opts, }); }, - async getForms(params, $) { - return this.makeRequest(API_PATH.FORMS, "/forms", { - params, - $, + getEngagements(opts = {}) { + return this.makeRequest({ + api: API_PATH.ENGAGEMENTS, + endpoint: "/engagements/paged", + ...opts, }); }, - async getFormSubmissions({ - formId, ...params - }, $) { - return this.makeRequest( - API_PATH.FORM_INTEGRATIONS, - `/submissions/forms/${formId}`, - { - params, - $, - }, - ); + getEvents(opts = {}) { + return this.makeRequest({ + api: API_PATH.EVENTS, + endpoint: "/events", + ...opts, + }); }, - async getLists(params, $) { - const { - listType, - ...otherParams - } = params; + getForms(opts = {}) { + return this.makeRequest({ + api: API_PATH.FORMS, + endpoint: "/forms", + ...opts, + }); + }, + getFormSubmissions({ + formId, ...opts + }) { + return this.makeRequest({ + api: API_PATH.FORM_INTEGRATIONS, + endpoint: `/submissions/forms/${formId}`, + ...opts, + }); + }, + getLists({ + listType, ...opts + }) { const basePath = "/lists"; const path = listType ? `${basePath}/${listType}` : basePath; - return this.makeRequest(API_PATH.CONTACTS, path, { - params: otherParams, - $, + return this.makeRequest({ + api: API_PATH.CONTACTS, + endpoint: path, + ...opts, }); }, - async getListContacts(params, listId, $) { - return this.makeRequest( - API_PATH.CONTACTS, - `/lists/${listId}/contacts/all`, - { - params, - $, - }, - ); + getListContacts({ + listId, ...opts + }) { + return this.makeRequest({ + api: API_PATH.CONTACTS, + endpoint: `/lists/${listId}/contacts/all`, + ...opts, + }); }, - async getOwners(params, $) { - return this.makeRequest(API_PATH.CRMV3, "/owners", { - params, - $, + getOwners(opts = {}) { + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: "/owners", + ...opts, }); }, - async listObjectsInPage(objectType, after, params, $) { - return this.makeRequest(API_PATH.CRMV3, `/objects/${objectType}`, { + listObjectsInPage(objectType, after, params, $) { + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: `/objects/${objectType}`, params: { after, ...params, @@ -453,335 +768,270 @@ export default { $, }); }, - async getObjects(objectType, $) { - const params = { - limit: 100, - }; - let results = null; - const objects = []; - while (!results || params.next) { - results = await this.makeRequest( - API_PATH.CRMV3, - `/objects/${objectType}`, - { - params, - $, - }, - ); - params.next = results.paging?.next?.after; - for (const result of results.results) { - objects.push(result); - } - } - return objects; - }, - async getContact(contactId, properties, $) { - const params = { - properties, - }; - return this.makeRequest( - API_PATH.CRMV3, - `/objects/contacts/${contactId}`, - { - params, - $, - }, - ); + getContact({ + contactId, ...opts + }) { + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: `/objects/contacts/${contactId}`, + ...opts, + }); }, - async getObject(objectType, objectId, properties, $) { + getObject(objectType, objectId, properties, $) { const params = { properties: properties?.join(","), }; - - return this.makeRequest( - API_PATH.CRMV3, - `/objects/${objectType}/${objectId}`, - { - params, - $, - }, - ); - }, - async getLineItem(lineItemId, $) { - return this.makeRequest( - API_PATH.CRMV3, - `/objects/line_items/${lineItemId}`, - { - $, - }, - ); - }, - async getPublishingChannels($) { - return this.makeRequest( - API_PATH.BROADCAST, - "/channels/setting/publish/current", - { - $, - }, - ); - }, - async getBroadcastMessages(params, $) { - return this.makeRequest( - API_PATH.BROADCAST, - "/broadcasts", - { - params, - $, - }, - ); - }, - async getEmailSubscriptionsTimeline(params, $) { - return this.makeRequest( - API_PATH.EMAIL, - "/subscriptions/timeline", - { - params, - $, - }, - ); - }, - async getPipelines(objectType, $) { - return this.makeRequest(API_PATH.CRMV3, `/pipelines/${objectType}`, { + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: `/objects/${objectType}/${objectId}`, + params, $, }); }, - async createObject(objectType, properties, $) { - return this.makeRequest( - API_PATH.CRMV3, - `/objects/${objectType}`, - { - method: "POST", - data: { - properties, - }, - $, - }, - ); - }, - async updateObject(objectType, properties, objectId, $) { - return this.makeRequest( - API_PATH.CRMV3, - `/objects/${objectType}/${objectId}`, - { - method: "PATCH", - data: { - properties, - }, - $, - }, - ); + getLineItem({ + lineItemId, ...opts + }) { + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: `/objects/line_items/${lineItemId}`, + ...opts, + }); }, - async getPropertyGroups(objectType, $) { - return this.makeRequest(API_PATH.CRMV3, `/properties/${objectType}/groups`, { - $, + getPublishingChannels(opts = {}) { + return this.makeRequest({ + api: API_PATH.BROADCAST, + endpoint: "/channels/setting/publish/current", + ...opts, }); }, - async getProperties(objectType, $) { - return this.makeRequest(API_PATH.CRMV3, `/properties/${objectType}`, { - $, + getBroadcastMessages(opts = {}) { + return this.makeRequest({ + api: API_PATH.BROADCAST, + endpoint: "/broadcasts", + ...opts, }); }, - async listSchemas($) { - return this.makeRequest(API_PATH.CRMV3, "/schemas", { - $, + getEmailSubscriptionsTimeline(opts = {}) { + return this.makeRequest({ + api: API_PATH.EMAIL, + endpoint: "/subscriptions/timeline", + ...opts, }); }, - async getSchema(objectType, $) { - return this.makeRequest(API_PATH.CRMV3, `/schemas/${objectType}`, { - $, + getBusinessUnits(opts = {}) { + return this.makeRequest({ + api: API_PATH.BUSINESS_UNITS, + endpoint: `/business-units/user/${this.$auth.oauth_uid}`, + ...opts, }); }, - /** - * Returns a list of prop options for a CRM object type - * - * @param {*} referencedObjectType The object type to - * @param {object} [opts = {}] - an object representing configuration - * options used to create prop options, intended to be passed from the first - * argument of the prop's async options - * @returns a list of prop options - */ - async createOptions(referencedObjectType, opts = {}) { - const { - prevContext, - page, - } = opts; - const { nextAfter } = prevContext; - if (page !== 0 && !nextAfter) { - return []; - } - const { - paging, - results, - } = await this.listObjectsInPage(referencedObjectType, nextAfter); - return { - options: results.map((object) => ({ - label: this.getObjectLabel(object, referencedObjectType) ?? object.id, - value: object.id, - })), - context: { - nextAfter: paging?.next.after, - }, - }; + getPipeline({ + objectType, pipelineId, ...opts + }) { + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: `/pipelines/${objectType}/${pipelineId}`, + ...opts, + }); }, - async getCompaniesOptions(opts) { - return this.createOptions(OBJECT_TYPE.COMPANY, opts); + getPipelines({ + objectType, ...opts + }) { + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: `/pipelines/${objectType}`, + ...opts, + }); }, - async getContactsOptions(opts) { - return this.createOptions(OBJECT_TYPE.CONTACT, opts); + createObject({ + objectType, ...opts + }) { + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: `/objects/${objectType}`, + method: "POST", + ...opts, + }); }, - async getLineItemsOptions(opts) { - return this.createOptions(OBJECT_TYPE.LINE_ITEM, opts); + updateObject({ + objectType, objectId, ...opts + }) { + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: `/objects/${objectType}/${objectId}`, + method: "PATCH", + ...opts, + }); }, - async getTicketsOptions(opts) { - return this.createOptions(OBJECT_TYPE.TICKET, opts); + getPropertyGroups({ + objectType, ...opts + }) { + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: `/properties/${objectType}/groups`, + ...opts, + }); }, - async getQuotesOptions(opts) { - return this.createOptions(OBJECT_TYPE.QUOTE, opts); + getProperties({ + objectType, ...opts + }) { + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: `/properties/${objectType}`, + ...opts, + }); }, - async getCallsOptions(opts) { - return this.createOptions(OBJECT_TYPE.CALL, opts); + listSchemas(opts = {}) { + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: "/schemas", + ...opts, + }); }, - async getTasksOptions(opts) { - return this.createOptions(OBJECT_TYPE.TASK, opts); + getSchema({ + objectType, ...opts + }) { + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: `/schemas/${objectType}`, + ...opts, + }); }, - async getNotesOptions(opts) { - return this.createOptions(OBJECT_TYPE.NOTE, opts); + searchFiles(opts = {}) { + return this.makeRequest({ + api: API_PATH.FILES, + endpoint: "/files/search", + ...opts, + }); }, - async getMeetingsOptions(opts) { - return this.createOptions(OBJECT_TYPE.MEETING, opts); + getSignedUrl({ + fileId, ...opts + }) { + return this.makeRequest({ + api: API_PATH.FILES, + endpoint: `/files/${fileId}/signed-url`, + ...opts, + }); }, - async getEmailsOptions(opts) { - return this.createOptions(OBJECT_TYPE.EMAIL, opts); + addContactsToList({ + listId, ...opts + }) { + return this.makeRequest({ + api: API_PATH.CONTACTS, + endpoint: `/lists/${listId}/add`, + method: "POST", + ...opts, + }); }, - async getOwnersOptions(params) { - const { results } = await this.getOwners(params); - return results.map((object) => ({ - label: object.email, - value: object.id, - })); + getAssociationTypes({ + fromObjectType, toObjectType, ...opts + }) { + return this.makeRequest({ + api: API_PATH.CRMV4, + endpoint: `/associations/${fromObjectType}/${toObjectType}/labels`, + ...opts, + }); }, - async searchFiles(params, $) { - return this.makeRequest(API_PATH.FILES, "/files/search", { - params, - $, + createAssociation({ + fromObjectType, toObjectType, fromId, toId, ...opts + }) { + return this.makeRequest({ + api: API_PATH.CRMV4, + endpoint: `/objects/${fromObjectType}/${fromId}/associations/${toObjectType}/${toId}`, + ...opts, }); }, - async getSignedUrl(fileId, params, $) { - return this.makeRequest(API_PATH.FILES, `/files/${fileId}/signed-url`, { - params, - $, + createAssociations({ + fromObjectType, toObjectType, ...opts + }) { + return this.makeRequest({ + api: API_PATH.CRMV4, + endpoint: `/associations/${fromObjectType}/${toObjectType}/batch/create`, + method: "POST", + ...opts, }); }, - async addContactsToList(listId, emails, $) { - return this.makeRequest( - API_PATH.CONTACTS, - `/lists/${listId}/add`, - { - method: "POST", - data: { - emails, - }, - $, - }, - ); + listWorkflows(opts = {}) { + return this.makeRequest({ + api: API_PATH.AUTOMATION, + endpoint: "/workflows", + ...opts, + }); }, - async getAssociationTypes(fromObjectType, toObjectType, $) { - return this.makeRequest(API_PATH.CRMV4, `/associations/${fromObjectType}/${toObjectType}/labels`, { - $, + addContactsIntoWorkflow({ + workflowId, contactEmail, ...opts + }) { + return this.makeRequest({ + api: API_PATH.AUTOMATION, + endpoint: `/workflows/${workflowId}/enrollments/contacts/${contactEmail}`, + method: "POST", + ...opts, }); }, - async createAssociation(fromObjectType, toObjectType, fromId, toId, $) { - return this.makeRequest( - API_PATH.CRMV4, - `/objects/${fromObjectType}/${fromId}/associations/${toObjectType}/${toId}`, - { - $, - }, - ); - }, - async createAssociations(fromObjectType, toObjectType, fromId, toIds, associationTypeId, $) { - return this.makeRequest( - API_PATH.CRMV4, - `/associations/${fromObjectType}/${toObjectType}/batch/create`, - { - method: "POST", - data: { - inputs: toIds.map((toId) => ({ - from: { - id: fromId, - }, - to: { - id: toId, - }, - types: [ - { - associationCategory: ASSOCIATION_CATEGORY.HUBSPOT_DEFINED, - associationTypeId: associationTypeId, - }, - ], - })), - }, - $, - }, - ); - }, - async listWorkflows() { - return this.makeRequest( - API_PATH.AUTOMATION, - "/workflows", - ); - }, - async addContactsIntoWorkflow(workflowId, contactEmail, $) { - return this.makeRequest( - API_PATH.AUTOMATION, - `/workflows/${workflowId}/enrollments/contacts/${contactEmail}`, - { - $, - method: "POST", - }, - ); + batchCreateContacts(opts = {}) { + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: "/objects/contacts/batch/create", + method: "POST", + ...opts, + }); }, - async batchCreateContacts({ - $, - data, + batchUpdateContacts(opts = {}) { + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: "/objects/contacts/batch/update", + method: "POST", + ...opts, + }); + }, + getDeal({ + dealId, ...opts }) { - return this.makeRequest( - API_PATH.CRMV3, - "/objects/contacts/batch/create", - { - method: "POST", - data, - $, - }, - ); + return this.makeRequest({ + api: API_PATH.DEAL, + endpoint: `/deal/${dealId}`, + ...opts, + }); }, - async batchUpdateContacts({ - $, - data, + getMemberships({ + objectType, objectId, ...opts }) { - return this.makeRequest( - API_PATH.CRMV3, - "/objects/contacts/batch/update", - { - method: "POST", - data, - $, - }, - ); + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: `/lists/records/${objectType}/${objectId}/memberships`, + ...opts, + }); }, - async getDeal({ - $, - dealId, - params, + translateLegacyListId(opts = {}) { + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: "/lists/idmapping", + ...opts, + }); + }, + batchGetObjects({ + objectType, ...opts }) { - return this.makeRequest( - API_PATH.DEAL, - `/deal/${dealId}`, - { - params, - $, - }, - ); + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: `/objects/${objectType}/batch/read`, + method: "POST", + ...opts, + }); + }, + listNotes(opts = {}) { + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: "/objects/notes", + ...opts, + }); + }, + listTasks(opts = {}) { + return this.makeRequest({ + api: API_PATH.CRMV3, + endpoint: "/objects/tasks", + ...opts, + }); }, }, }; diff --git a/components/hubspot/package.json b/components/hubspot/package.json index 3dfcb9db0bfdb..8377287eeb952 100644 --- a/components/hubspot/package.json +++ b/components/hubspot/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/hubspot", - "version": "0.8.0", + "version": "1.0.0", "description": "Pipedream Hubspot Components", "main": "hubspot.app.mjs", "keywords": [ @@ -10,7 +10,7 @@ "homepage": "https://pipedream.com/apps/hubspot", "author": "Pipedream (https://pipedream.com/)", "dependencies": { - "@pipedream/platform": "^1.6.0", + "@pipedream/platform": "^3.0.0", "bottleneck": "^2.19.5" }, "gitHead": "e12480b94cc03bed4808ebc6b13e7fdb3a1ba535", diff --git a/components/hubspot/sources/common/common.mjs b/components/hubspot/sources/common/common.mjs index 282042f08d8cf..282c2750edcde 100644 --- a/components/hubspot/sources/common/common.mjs +++ b/components/hubspot/sources/common/common.mjs @@ -1,5 +1,4 @@ import hubspot from "../../hubspot.app.mjs"; -import Bottleneck from "bottleneck"; import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; export default { @@ -14,26 +13,62 @@ export default { }, }, methods: { - _limiter() { - return new Bottleneck({ - minTime: 250, // max 4 requests per second - }); - }, - async _requestWithLimiter(limiter, resourceFn, params) { - return limiter.schedule(async () => await resourceFn(params)); - }, _getAfter() { return this.db.get("after") || new Date().setDate(new Date().getDate() - 1); // 1 day ago }, _setAfter(after) { this.db.set("after", after); }, + async getWriteOnlyProperties(resourceName) { + const { results: properties } = await this.hubspot.getProperties({ + objectType: resourceName, + }); + return properties.filter(({ modificationMetadata }) => !modificationMetadata.readOnlyValue); + }, + getChunks(items) { + const MAX_CHUNK_SIZE = 45; + return Array.from({ + length: Math.ceil(items.length / MAX_CHUNK_SIZE), + }) + .map((_, index) => index * MAX_CHUNK_SIZE) + .map((begin) => items.slice(begin, begin + MAX_CHUNK_SIZE)); + }, + processChunk({ + batchRequestFn, + mapper = ({ id }) => ({ + id, + }), + }) { + return async (chunk) => { + const { results } = await batchRequestFn(chunk.map(mapper)); + return results; + }; + }, + async processChunks({ + chunks, ...args + }) { + const promises = chunks.map(this.processChunk(args)); + const results = await Promise.all(promises); + return results.flat(); + }, + async processEvents(resources, after) { + let maxTs = after; + for (const result of resources) { + if (await this.isRelevant(result, after)) { + this.emitEvent(result); + const ts = this.getTs(result); + if (ts > maxTs) { + maxTs = ts; + } + } + } + this._setAfter(maxTs); + }, async paginate(params, resourceFn, resultType = null, after = null) { let results = null; let maxTs = after || 0; - const limiter = this._limiter(); while (!results || params.after) { - results = await this._requestWithLimiter(limiter, resourceFn, params); + results = await resourceFn(params); if (results.paging) { params.after = results.paging.next.after; } else { @@ -69,10 +104,9 @@ export default { let results, items; let count = 0; let maxTs = after || 0; - const limiter = this._limiter(); while (hasMore && (!limitRequest || count < limitRequest)) { count++; - results = await this._requestWithLimiter(limiter, resourceFn, params); + results = await resourceFn(params); hasMore = results.hasMore; if (hasMore) { params.offset = results.offset; @@ -83,7 +117,7 @@ export default { items = results; } for (const item of items) { - if (this.isRelevant(item, after)) { + if (await this.isRelevant(item, after)) { this.emitEvent(item); const ts = this.getTs(item); if (ts > maxTs) { @@ -136,7 +170,7 @@ export default { }, async run() { const after = this._getAfter(); - const params = this.getParams(after); + const params = await this.getParams(after); await this.processResults(after, params); }, }; diff --git a/components/hubspot/sources/company-updated/company-updated.mjs b/components/hubspot/sources/company-updated/company-updated.mjs deleted file mode 100644 index 2b22573d37a85..0000000000000 --- a/components/hubspot/sources/company-updated/company-updated.mjs +++ /dev/null @@ -1,48 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - key: "hubspot-company-updated", - name: "Company Updated", - description: "Emit new event each time a company is updated.", - version: "0.0.12", - dedupe: "unique", - type: "source", - hooks: {}, - methods: { - ...common.methods, - getTs(company) { - return Date.parse(company.updatedAt); - }, - generateMeta(company) { - const { - id, - properties, - } = company; - const ts = this.getTs(company); - return { - id: `${id}${ts}`, - summary: properties.name, - ts, - }; - }, - isRelevant(company, updatedAfter) { - return this.getTs(company) > updatedAfter; - }, - getParams() { - return { - limit: 100, - sorts: [ - { - propertyName: "hs_lastmodifieddate", - direction: "DESCENDING", - }, - ], - object: "companies", - }; - }, - async processResults(after, params) { - await this.searchCRM(params, after); - }, - }, -}; diff --git a/components/hubspot/sources/contact-updated/README.md b/components/hubspot/sources/contact-updated/README.md deleted file mode 100644 index 76dbe6f8aa14f..0000000000000 --- a/components/hubspot/sources/contact-updated/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Overview - -Use this source to trigger a workflow each time a contact is updated in Hubspot. - -# Getting Started - -1. [Create a new workflow](https://pipedream.com/new). -2. In the trigger step, search for "HubSpot". Select it and search for the **Contact Updated** source. - -# Troubleshooting - -Please [reach out](https://pipedream.com/support/) to the Pipedream team with any technical issues or questions about the HubSpot integration. We're happy to help! \ No newline at end of file diff --git a/components/hubspot/sources/contact-updated/contact-updated.mjs b/components/hubspot/sources/contact-updated/contact-updated.mjs deleted file mode 100644 index 7a8a1d92aa039..0000000000000 --- a/components/hubspot/sources/contact-updated/contact-updated.mjs +++ /dev/null @@ -1,57 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - key: "hubspot-contact-updated", - name: "Contact Updated", - description: "Emit new event each time a contact is updated.", - version: "0.1.0", - dedupe: "unique", - type: "source", - props: { - ...common.props, - properties: { - propDefinition: [ - common.props.hubspot, - "contactProperties", - ], - }, - }, - methods: { - ...common.methods, - getTs(contact) { - return Date.parse(contact.updatedAt); - }, - generateMeta(contact) { - const { - id, - properties, - } = contact; - const ts = this.getTs(contact); - return { - id: `${id}${ts}`, - summary: `${properties.firstname} ${properties.lastname}`, - ts, - }; - }, - isRelevant(contact, updatedAfter) { - return this.getTs(contact) > updatedAfter; - }, - getParams() { - return { - limit: 100, - sorts: [ - { - propertyName: "lastmodifieddate", - direction: "DESCENDING", - }, - ], - properties: this.properties, - object: "contacts", - }; - }, - async processResults(after, params) { - await this.searchCRM(params, after); - }, - }, -}; diff --git a/components/hubspot/sources/delete-blog-article/delete-blog-article.mjs b/components/hubspot/sources/delete-blog-article/delete-blog-article.mjs index 0a542f9b6768a..375d2a8fd3c94 100644 --- a/components/hubspot/sources/delete-blog-article/delete-blog-article.mjs +++ b/components/hubspot/sources/delete-blog-article/delete-blog-article.mjs @@ -1,14 +1,14 @@ import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "hubspot-delete-blog-article", name: "Deleted Blog Posts", description: "Emit new event for each deleted blog post.", - version: "0.0.12", + version: "0.0.19", dedupe: "unique", type: "source", - hooks: {}, methods: { ...common.methods, getTs(blogpost) { @@ -28,9 +28,11 @@ export default { }, getParams(after) { return { - limit: 100, - deletedAt__gte: after, - sort: "-updatedAt", + params: { + limit: 100, + deletedAt__gte: after, + sort: "-updatedAt", + }, }; }, async processResults(after, params) { @@ -42,4 +44,5 @@ export default { ); }, }, + sampleEmit, }; diff --git a/components/hubspot/sources/delete-blog-article/test-event.mjs b/components/hubspot/sources/delete-blog-article/test-event.mjs new file mode 100644 index 0000000000000..9010d6ea228a4 --- /dev/null +++ b/components/hubspot/sources/delete-blog-article/test-event.mjs @@ -0,0 +1,37 @@ +export default { + "archivedAt": 1719245790266, + "archivedInDashboard": false, + "attachedStylesheets": [], + "authorName": "Test User", + "categoryId": 3, + "contentGroupId": "23646627706", + "contentTypeCategory": 3, + "created": "2024-06-20T22:37:03.004Z", + "createdById": "9555737", + "currentState": "DRAFT", + "currentlyPublished": false, + "domain": "", + "enableGoogleAmpOutputOverride": false, + "featuredImage": "", + "featuredImageAltText": "", + "htmlTitle": "hello world", + "id": "170912644748", + "language": "en", + "layoutSections": {}, + "linkRelCanonicalUrl": "", + "metaDescription": "", + "name": "hello world", + "publicAccessRules": [], + "publicAccessRulesEnabled": false, + "publishDate": "2024-06-20T22:37:02Z", + "slug": "blog/-temporary-slug-2deeca87-12e6-4d77-b44b-4a7493780db8", + "state": "DRAFT", + "tagIds": [], + "translations": {}, + "updated": "2024-06-24T16:16:30.266Z", + "updatedById": "9555737", + "url": "", + "useFeaturedImage": true, + "widgetContainers": {}, + "widgets": {} +} \ No newline at end of file diff --git a/components/hubspot/sources/line-item-updated/line-item-updated.mjs b/components/hubspot/sources/line-item-updated/line-item-updated.mjs deleted file mode 100644 index 55af2b22bc138..0000000000000 --- a/components/hubspot/sources/line-item-updated/line-item-updated.mjs +++ /dev/null @@ -1,45 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - key: "hubspot-line-item-updated", - name: "Line Item Updated", - description: "Emit new event each time a line item is updated.", - version: "0.0.12", - dedupe: "unique", - type: "source", - hooks: {}, - methods: { - ...common.methods, - getTs(lineItem) { - return Date.parse(lineItem.updatedAt); - }, - generateMeta(lineItem) { - const { id } = lineItem; - const ts = this.getTs(lineItem); - return { - id: `${id}${ts}`, - summary: `Line Item ID: ${id}`, - ts, - }; - }, - isRelevant(lineItem, updatedAfter) { - return this.getTs(lineItem) > updatedAfter; - }, - getParams() { - return { - limit: 100, - sorts: [ - { - propertyName: "hs_lastmodifieddate", - direction: "DESCENDING", - }, - ], - object: "line_items", - }; - }, - async processResults(after, params) { - await this.searchCRM(params, after); - }, - }, -}; diff --git a/components/hubspot/sources/new-blog-article/new-blog-article.mjs b/components/hubspot/sources/new-blog-article/new-blog-article.mjs deleted file mode 100644 index 81eaedd8eab3e..0000000000000 --- a/components/hubspot/sources/new-blog-article/new-blog-article.mjs +++ /dev/null @@ -1,43 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - key: "hubspot-new-blog-article", - name: "New Blog Posts", - description: "Emit new event for each new blog post.", - version: "0.0.15", - dedupe: "unique", - type: "source", - hooks: {}, - methods: { - ...common.methods, - getTs(blogpost) { - return Date.parse(blogpost.created); - }, - generateMeta(blogpost) { - const { - id, - name: summary, - } = blogpost; - const ts = this.getTs(blogpost); - return { - id, - summary, - ts, - }; - }, - getParams(after) { - return { - limit: 100, - createdAfter: after, // return entries created since event last ran - }; - }, - async processResults(after, params) { - await this.paginate( - params, - this.hubspot.getBlogPosts.bind(this), - "results", - ); - }, - }, -}; diff --git a/components/hubspot/sources/new-company-property-change/new-company-property-change.mjs b/components/hubspot/sources/new-company-property-change/new-company-property-change.mjs index 9ca81a1245ad9..0aba3cea35dce 100644 --- a/components/hubspot/sources/new-company-property-change/new-company-property-change.mjs +++ b/components/hubspot/sources/new-company-property-change/new-company-property-change.mjs @@ -1,12 +1,13 @@ import common from "../common/common.mjs"; -import { API_PATH } from "../../common/constants.mjs"; +import { DEFAULT_LIMIT } from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "hubspot-new-company-property-change", name: "New Company Property Change", description: "Emit new event when a specified property is provided or updated on a company. [See the documentation](https://developers.hubspot.com/docs/api/crm/companies)", - version: "0.0.5", + version: "0.0.12", dedupe: "unique", type: "source", props: { @@ -16,12 +17,11 @@ export default { label: "Property", description: "The company property to watch for changes", async options() { - const { results: properties } = await this.hubspot.getProperties("companies"); + const properties = await this.getWriteOnlyProperties("companies"); return properties.map((property) => property.name); }, }, }, - hooks: {}, methods: { ...common.methods, getTs(company) { @@ -49,54 +49,53 @@ export default { getParams(after) { return { object: "companies", - limit: 50, - properties: [ - this.property, - ], - sorts: [ - { - propertyName: "hs_lastmodifieddate", - direction: "DESCENDING", - }, - ], - filterGroups: [ - { - filters: [ - { - propertyName: this.property, - operator: "HAS_PROPERTY", - }, - { - propertyName: "hs_lastmodifieddate", - operator: "GTE", - value: after, - }, - ], - }, - ], + data: { + limit: DEFAULT_LIMIT, + properties: [ + this.property, + ], + sorts: [ + { + propertyName: "hs_lastmodifieddate", + direction: "DESCENDING", + }, + ], + filterGroups: [ + { + filters: [ + { + propertyName: this.property, + operator: "HAS_PROPERTY", + }, + { + propertyName: "hs_lastmodifieddate", + operator: "GTE", + value: after, + }, + ], + }, + ], + }, }; }, - async batchGetCompanies(inputs) { - return this.hubspot.makeRequest( - API_PATH.CRMV3, - "/objects/companies/batch/read", - { - method: "POST", - data: { - properties: [ - this.property, - ], - propertiesWithHistory: [ - this.property, - ], - inputs, - }, + batchGetCompanies(inputs) { + return this.hubspot.batchGetObjects({ + objectType: "companies", + data: { + properties: [ + this.property, + ], + propertiesWithHistory: [ + this.property, + ], + inputs, }, - ); + }); }, async processResults(after, params) { - const { results: properties } = await this.hubspot.getProperties("companies"); + const properties = await this.getWriteOnlyProperties("companies"); const propertyNames = properties.map((property) => property.name); + if (!propertyNames.includes(this.property)) { throw new Error(`Property "${this.property}" not supported for Companies. See Hubspot's default company properties documentation - https://knowledge.hubspot.com/companies/hubspot-crm-default-company-properties`); } @@ -107,24 +106,13 @@ export default { return; } - const inputs = updatedCompanies.map(({ id }) => ({ - id, - })); - // get companies w/ `propertiesWithHistory` - const { results } = await this.batchGetCompanies(inputs); - - let maxTs = after; - for (const result of results) { - if (this.isRelevant(result, after)) { - this.emitEvent(result); - const ts = this.getTs(result); - if (ts > maxTs) { - maxTs = ts; - } - } - } + const results = await this.processChunks({ + batchRequestFn: this.batchGetCompanies, + chunks: this.getChunks(updatedCompanies), + }); - this._setAfter(maxTs); + this.processEvents(results, after); }, }, + sampleEmit, }; diff --git a/components/hubspot/sources/new-company-property-change/test-event.mjs b/components/hubspot/sources/new-company-property-change/test-event.mjs new file mode 100644 index 0000000000000..51d4b5fbcb59e --- /dev/null +++ b/components/hubspot/sources/new-company-property-change/test-event.mjs @@ -0,0 +1,23 @@ +export default { + "id": "21511366947", + "properties": { + "createdate": "2024-06-20T03:09:18.816Z", + "hs_lastmodifieddate": "2024-06-24T16:25:20.510Z", + "hs_object_id": "21511366947", + "industry": "ACCOUNTING" + }, + "propertiesWithHistory": { + "industry": [ + { + "value": "ACCOUNTING", + "timestamp": "2024-06-24T16:25:20.510Z", + "sourceType": "CRM_UI", + "sourceId": "userId:9555737", + "updatedByUserId": 9555737 + } + ] + }, + "createdAt": "2024-06-20T03:09:18.816Z", + "updatedAt": "2024-06-24T16:25:20.510Z", + "archived": false +} \ No newline at end of file diff --git a/components/hubspot/sources/new-company/new-company.mjs b/components/hubspot/sources/new-company/new-company.mjs deleted file mode 100644 index 4541fe2957dee..0000000000000 --- a/components/hubspot/sources/new-company/new-company.mjs +++ /dev/null @@ -1,48 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - key: "hubspot-new-company", - name: "New Companies", - description: "Emit new event for each new company added.", - version: "0.0.15", - dedupe: "unique", - type: "source", - hooks: {}, - methods: { - ...common.methods, - getTs(company) { - return Date.parse(company.createdAt); - }, - generateMeta(company) { - const { - id, - properties, - } = company; - const ts = this.getTs(company); - return { - id, - summary: properties.name, - ts, - }; - }, - isRelevant(company, createdAfter) { - return this.getTs(company) > createdAfter; - }, - getParams() { - return { - limit: 100, - sorts: [ - { - propertyName: "createdate", - direction: "DESCENDING", - }, - ], - object: "companies", - }; - }, - async processResults(after, params) { - await this.searchCRM(params, after); - }, - }, -}; diff --git a/components/hubspot/sources/new-contact-in-list/new-contact-in-list.mjs b/components/hubspot/sources/new-contact-in-list/new-contact-in-list.mjs deleted file mode 100644 index f1eca3d4f97c2..0000000000000 --- a/components/hubspot/sources/new-contact-in-list/new-contact-in-list.mjs +++ /dev/null @@ -1,75 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - key: "hubspot-new-contact-in-list", - name: "New Contact in List", - description: "Emit new event for each new contact in a list.", - version: "0.1.0", - dedupe: "unique", - type: "source", - props: { - ...common.props, - lists: { - propDefinition: [ - common.props.hubspot, - "lists", - ], - }, - properties: { - propDefinition: [ - common.props.hubspot, - "contactProperties", - ], - }, - }, - methods: { - ...common.methods, - generateMeta(contact, list) { - const { - vid, - properties, - } = contact; - const { - value, - label, - } = list; - return { - id: `${vid}${value}`, - summary: `${properties?.firstname?.value} ${properties?.lastname?.value} added to ${label}`, - ts: Date.now(), - }; - }, - async emitEvent(contact, properties, list) { - const contactInfo = await this.hubspot.getContact( - contact.vid, - properties, - ); - const meta = this.generateMeta(contact, list); - this.$emit({ - contact, - contactInfo, - }, meta); - }, - getParams() { - return { - count: 100, - }; - }, - async processResults() { - const properties = this.properties; - for (let list of this.lists) { - const params = this.getParams(); - let hasMore = true; - while (hasMore) { - const results = await this.hubspot.getListContacts(params, list.value); - hasMore = results["has-more"]; - if (hasMore) params.vidOffset = results["vid-offset"]; - for (const contact of results.contacts) { - await this.emitEvent(contact, properties, list); - } - } - } - }, - }, -}; diff --git a/components/hubspot/sources/new-contact-property-change/new-contact-property-change.mjs b/components/hubspot/sources/new-contact-property-change/new-contact-property-change.mjs index 7328352a9b60b..b38c713d04dcf 100644 --- a/components/hubspot/sources/new-contact-property-change/new-contact-property-change.mjs +++ b/components/hubspot/sources/new-contact-property-change/new-contact-property-change.mjs @@ -1,12 +1,13 @@ import common from "../common/common.mjs"; -import { API_PATH } from "../../common/constants.mjs"; +import { DEFAULT_LIMIT } from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "hubspot-new-contact-property-change", name: "New Contact Property Change", description: "Emit new event when a specified property is provided or updated on a contact. [See the documentation](https://developers.hubspot.com/docs/api/crm/contacts)", - version: "0.0.7", + version: "0.0.14", dedupe: "unique", type: "source", props: { @@ -21,7 +22,6 @@ export default { }, }, }, - hooks: {}, methods: { ...common.methods, getTs(contact) { @@ -49,50 +49,48 @@ export default { getParams(after) { return { object: "contacts", - limit: 50, - properties: [ - this.property, - ], - sorts: [ - { - propertyName: "lastmodifieddate", - direction: "DESCENDING", - }, - ], - filterGroups: [ - { - filters: [ - { - propertyName: this.property, - operator: "HAS_PROPERTY", - }, - { - propertyName: "lastmodifieddate", - operator: "GTE", - value: after, - }, - ], - }, - ], + data: { + limit: DEFAULT_LIMIT, + properties: [ + this.property, + ], + sorts: [ + { + propertyName: "lastmodifieddate", + direction: "DESCENDING", + }, + ], + filterGroups: [ + { + filters: [ + { + propertyName: this.property, + operator: "HAS_PROPERTY", + }, + { + propertyName: "lastmodifieddate", + operator: "GTE", + value: after, + }, + ], + }, + ], + }, }; }, - async batchGetContacts(inputs) { - return this.hubspot.makeRequest( - API_PATH.CRMV3, - "/objects/contacts/batch/read", - { - method: "POST", - data: { - properties: [ - this.property, - ], - propertiesWithHistory: [ - this.property, - ], - inputs, - }, + batchGetContacts(inputs) { + return this.hubspot.batchGetObjects({ + objectType: "contacts", + data: { + properties: [ + this.property, + ], + propertiesWithHistory: [ + this.property, + ], + inputs, }, - ); + }); }, async processResults(after, params) { const properties = await this.hubspot.getContactProperties(); @@ -107,33 +105,13 @@ export default { return; } - const batchSize = 45; - let maxTs = after; - - for (let i = 0; i < updatedContacts.length; i += batchSize) { - const batchInputs = updatedContacts.slice(i, i + batchSize).map(({ id }) => ({ - id, - })); - - // get contacts w/ `propertiesWithHistory` - const { results } = await this.batchGetContacts(batchInputs); - - maxTs = after; - - for (const result of results) { - if (this.isRelevant(result, after)) { - this.emitEvent(result); - const ts = this.getTs(result); - if (ts > maxTs) { - maxTs = ts; - } - } - } - - after = maxTs; - } + const results = await this.processChunks({ + batchRequestFn: this.batchGetContacts, + chunks: this.getChunks(updatedContacts), + }); - this._setAfter(maxTs); + this.processEvents(results, after); }, }, + sampleEmit, }; diff --git a/components/hubspot/sources/new-contact-property-change/test-event.mjs b/components/hubspot/sources/new-contact-property-change/test-event.mjs new file mode 100644 index 0000000000000..b1ae2a57683d0 --- /dev/null +++ b/components/hubspot/sources/new-contact-property-change/test-event.mjs @@ -0,0 +1,30 @@ +export default { + "id": "32192850234", + "properties": { + "createdate": "2024-06-21T21:26:22.880Z", + "hs_object_id": "32192850234", + "lastmodifieddate": "2024-06-24T16:33:51.276Z", + "lifecyclestage": "marketingqualifiedlead" + }, + "propertiesWithHistory": { + "lifecyclestage": [ + { + "value": "marketingqualifiedlead", + "timestamp": "2024-06-24T16:33:41.647Z", + "sourceType": "CRM_UI", + "sourceId": "userId:9555737", + "updatedByUserId": 9555737 + }, + { + "value": "lead", + "timestamp": "2024-06-21T21:26:22.880Z", + "sourceType": "CRM_UI", + "sourceId": "userId:9555737", + "updatedByUserId": 9555737 + } + ] + }, + "createdAt": "2024-06-21T21:26:22.880Z", + "updatedAt": "2024-06-24T16:33:51.276Z", + "archived": false +} \ No newline at end of file diff --git a/components/hubspot/sources/new-contact/new-contact.mjs b/components/hubspot/sources/new-contact/new-contact.mjs deleted file mode 100644 index 4ef149fa211a7..0000000000000 --- a/components/hubspot/sources/new-contact/new-contact.mjs +++ /dev/null @@ -1,57 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - key: "hubspot-new-contact", - name: "New Contacts", - description: "Emit new event for each new contact added.", - version: "0.1.0", - dedupe: "unique", - type: "source", - props: { - ...common.props, - properties: { - propDefinition: [ - common.props.hubspot, - "contactProperties", - ], - }, - }, - methods: { - ...common.methods, - getTs(contact) { - return Date.parse(contact.createdAt); - }, - generateMeta(contact) { - const { - id, - properties, - } = contact; - const ts = this.getTs(contact); - return { - id, - summary: `${properties.firstname} ${properties.lastname}`, - ts, - }; - }, - isRelevant(contact, createdAfter) { - return this.getTs(contact) > createdAfter; - }, - getParams() { - return { - limit: 100, - sorts: [ - { - propertyName: "createdate", - direction: "DESCENDING", - }, - ], - properties: this.properties, - object: "contacts", - }; - }, - async processResults(after, params) { - await this.searchCRM(params, after); - }, - }, -}; diff --git a/components/hubspot/sources/new-custom-object-property-change/new-custom-object-property-change.mjs b/components/hubspot/sources/new-custom-object-property-change/new-custom-object-property-change.mjs new file mode 100644 index 0000000000000..f8f7a9d547186 --- /dev/null +++ b/components/hubspot/sources/new-custom-object-property-change/new-custom-object-property-change.mjs @@ -0,0 +1,122 @@ +import common from "../common/common.mjs"; +import { DEFAULT_LIMIT } from "../../common/constants.mjs"; + +export default { + ...common, + key: "hubspot-new-custom-object-property-change", + name: "New Custom Object Property Change", + description: "Emit new event when a specified property is provided or updated on a custom object.", + version: "0.0.4", + dedupe: "unique", + type: "source", + props: { + ...common.props, + objectSchema: { + propDefinition: [ + common.props.hubspot, + "objectSchema", + ], + }, + property: { + type: "string", + label: "Property", + description: "The custom object property to watch for changes", + async options() { + const properties = await this.getWriteOnlyProperties(this.objectSchema); + return properties.map((property) => property.name); + }, + }, + }, + methods: { + ...common.methods, + getTs(object) { + const history = object.propertiesWithHistory[this.property]; + if (!history || !(history.length > 0)) { + return; + } + return Date.parse(history[0].timestamp); + }, + generateMeta(object) { + const { + id, + properties, + } = object; + const ts = this.getTs(object); + return { + id: `${id}${ts}`, + summary: properties[this.property], + ts, + }; + }, + isRelevant(object, updatedAfter) { + return !updatedAfter || this.getTs(object) > updatedAfter; + }, + getParams(after) { + return { + object: this.objectSchema, + data: { + limit: DEFAULT_LIMIT, + properties: [ + this.property, + ], + sorts: [ + { + propertyName: "hs_lastmodifieddate", + direction: "DESCENDING", + }, + ], + filterGroups: [ + { + filters: [ + { + propertyName: this.property, + operator: "HAS_PROPERTY", + }, + { + propertyName: "hs_lastmodifieddate", + operator: "GTE", + value: after, + }, + ], + }, + ], + }, + }; + }, + batchGetCustomObjects(inputs) { + return this.hubspot.batchGetObjects({ + objectType: this.objectSchema, + data: { + properties: [ + this.property, + ], + propertiesWithHistory: [ + this.property, + ], + inputs, + }, + }); + }, + async processResults(after, params) { + const properties = await this.getWriteOnlyProperties(this.objectSchema); + const propertyNames = properties.map((property) => property.name); + + if (!propertyNames.includes(this.property)) { + throw new Error(`Property "${this.property}" not supported for custom object ${this.objectSchema}.`); + } + + const updatedObjects = await this.getPaginatedItems(this.hubspot.searchCRM, params); + + if (!updatedObjects.length) { + return; + } + + const results = await this.processChunks({ + batchRequestFn: this.batchGetCustomObjects, + chunks: this.getChunks(updatedObjects), + }); + + this.processEvents(results, after); + }, + }, +}; diff --git a/components/hubspot/sources/new-deal-in-stage/new-deal-in-stage.mjs b/components/hubspot/sources/new-deal-in-stage/new-deal-in-stage.mjs index 680d414327941..1d1af820996c6 100644 --- a/components/hubspot/sources/new-deal-in-stage/new-deal-in-stage.mjs +++ b/components/hubspot/sources/new-deal-in-stage/new-deal-in-stage.mjs @@ -1,14 +1,15 @@ import common from "../common/common.mjs"; +import { DEFAULT_LIMIT } from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "hubspot-new-deal-in-stage", name: "New Deal In Stage", description: "Emit new event for each new deal in a stage.", - version: "0.0.16", + version: "0.0.23", dedupe: "unique", type: "source", - hooks: {}, props: { ...common.props, pipeline: { @@ -67,16 +68,18 @@ export default { ], }; return { - limit: 100, - filterGroups: [ - filterGroup, - ], - sorts: [ - { - propertyName: "hs_lastmodifieddate", - direction: "DESCENDING", - }, - ], + data: { + limit: DEFAULT_LIMIT, + filterGroups: [ + filterGroup, + ], + sorts: [ + { + propertyName: "hs_lastmodifieddate", + direction: "DESCENDING", + }, + ], + }, object: "deals", }; }, @@ -115,4 +118,5 @@ export default { } }, }, + sampleEmit, }; diff --git a/components/hubspot/sources/new-deal-in-stage/test-event.mjs b/components/hubspot/sources/new-deal-in-stage/test-event.mjs new file mode 100644 index 0000000000000..d165c9614c987 --- /dev/null +++ b/components/hubspot/sources/new-deal-in-stage/test-event.mjs @@ -0,0 +1,16 @@ +export default { + "id": "12388054600", + "properties": { + "amount": "1", + "closedate": "2024-06-24T16:37:59.812Z", + "createdate": "2023-03-03T15:43:17.850Z", + "dealname": "deal1", + "dealstage": "closedwon", + "hs_lastmodifieddate": "2024-06-24T16:38:00.660Z", + "hs_object_id": "12388054600", + "pipeline": "default" + }, + "createdAt": "2023-03-03T15:43:17.850Z", + "updatedAt": "2024-06-24T16:38:00.660Z", + "archived": false +} \ No newline at end of file diff --git a/components/hubspot/sources/new-deal-property-change/new-deal-property-change.mjs b/components/hubspot/sources/new-deal-property-change/new-deal-property-change.mjs index 15ff037a65b3a..68bb4b23cf643 100644 --- a/components/hubspot/sources/new-deal-property-change/new-deal-property-change.mjs +++ b/components/hubspot/sources/new-deal-property-change/new-deal-property-change.mjs @@ -1,12 +1,13 @@ import common from "../common/common.mjs"; -import { API_PATH } from "../../common/constants.mjs"; +import { DEFAULT_LIMIT } from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "hubspot-new-deal-property-change", name: "New Deal Property Change", description: "Emit new event when a specified property is provided or updated on a deal. [See the documentation](https://developers.hubspot.com/docs/api/crm/deals)", - version: "0.0.6", + version: "0.0.13", dedupe: "unique", type: "source", props: { @@ -21,7 +22,6 @@ export default { }, }, }, - hooks: {}, methods: { ...common.methods, getTs(deal) { @@ -46,50 +46,48 @@ export default { getParams(after) { return { object: "deals", - limit: 50, - properties: [ - this.property, - ], - sorts: [ - { - propertyName: "hs_lastmodifieddate", - direction: "DESCENDING", - }, - ], - filterGroups: [ - { - filters: [ - { - propertyName: this.property, - operator: "HAS_PROPERTY", - }, - { - propertyName: "hs_lastmodifieddate", - operator: "GTE", - value: after, - }, - ], - }, - ], + data: { + limit: DEFAULT_LIMIT, + properties: [ + this.property, + ], + sorts: [ + { + propertyName: "hs_lastmodifieddate", + direction: "DESCENDING", + }, + ], + filterGroups: [ + { + filters: [ + { + propertyName: this.property, + operator: "HAS_PROPERTY", + }, + { + propertyName: "hs_lastmodifieddate", + operator: "GTE", + value: after, + }, + ], + }, + ], + }, }; }, - async batchGetDeals(inputs) { - return this.hubspot.makeRequest( - API_PATH.CRMV3, - "/objects/deals/batch/read", - { - method: "POST", - data: { - properties: [ - this.property, - ], - propertiesWithHistory: [ - this.property, - ], - inputs, - }, + batchGetDeals(inputs) { + return this.hubspot.batchGetObjects({ + objectType: "deals", + data: { + properties: [ + this.property, + ], + propertiesWithHistory: [ + this.property, + ], + inputs, }, - ); + }); }, async processResults(after, params) { const properties = await this.hubspot.getDealProperties(); @@ -104,24 +102,13 @@ export default { return; } - const inputs = updatedDeals.map(({ id }) => ({ - id, - })); - // get deals w/ `propertiesWithHistory` - const { results } = await this.batchGetDeals(inputs); - - let maxTs = after; - for (const result of results) { - if (this.isRelevant(result, after)) { - this.emitEvent(result); - const ts = this.getTs(result); - if (ts > maxTs) { - maxTs = ts; - } - } - } + const results = await this.processChunks({ + batchRequestFn: this.batchGetDeals, + chunks: this.getChunks(updatedDeals), + }); - this._setAfter(maxTs); + this.processEvents(results, after); }, }, + sampleEmit, }; diff --git a/components/hubspot/sources/new-deal-property-change/test-event.mjs b/components/hubspot/sources/new-deal-property-change/test-event.mjs new file mode 100644 index 0000000000000..c55a4d8a9a3ec --- /dev/null +++ b/components/hubspot/sources/new-deal-property-change/test-event.mjs @@ -0,0 +1,30 @@ +export default { + "id": "12388054600", + "properties": { + "createdate": "2023-03-03T15:43:17.850Z", + "dealtype": "existingbusiness", + "hs_lastmodifieddate": "2024-06-24T16:50:02.188Z", + "hs_object_id": "12388054600" + }, + "propertiesWithHistory": { + "dealtype": [ + { + "value": "existingbusiness", + "timestamp": "2024-06-24T16:50:02.188Z", + "sourceType": "CRM_UI", + "sourceId": "userId:9555737", + "updatedByUserId": 9555737 + }, + { + "value": "newbusiness", + "timestamp": "2023-05-11T19:35:49.898Z", + "sourceType": "CRM_UI", + "sourceId": "userId:9555737", + "updatedByUserId": 9555737 + } + ] + }, + "createdAt": "2023-03-03T15:43:17.850Z", + "updatedAt": "2024-06-24T16:50:02.188Z", + "archived": false +} \ No newline at end of file diff --git a/components/hubspot/sources/new-deal-updated/new-deal-updated.mjs b/components/hubspot/sources/new-deal-updated/new-deal-updated.mjs deleted file mode 100644 index dda2fd5737283..0000000000000 --- a/components/hubspot/sources/new-deal-updated/new-deal-updated.mjs +++ /dev/null @@ -1,89 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - key: "hubspot-new-deal-updated", - name: "New Deal Updated", - description: "Emit new event each time a deal is updated. [See the documentation](https://developers.hubspot.com/docs/api/crm/search)", - version: "0.0.17", - type: "source", - dedupe: "unique", - props: { - ...common.props, - pipeline: { - propDefinition: [ - common.props.hubspot, - "dealPipeline", - ], - description: "Filter deals by pipeline", - optional: true, - }, - stage: { - propDefinition: [ - common.props.hubspot, - "stages", - (c) => ({ - pipeline: c.pipeline, - }), - ], - type: "string", - label: "Stage", - description: "Filter deals by stage", - optional: true, - }, - }, - hooks: {}, - methods: { - ...common.methods, - getTs(deal) { - return Date.parse(deal.updatedAt); - }, - generateMeta(deal) { - const { - id, - properties, - } = deal; - const ts = this.getTs(deal); - return { - id: `${id}${ts}`, - summary: properties.dealname, - ts, - }; - }, - isRelevant(deal, updatedAfter) { - return this.getTs(deal) > updatedAfter; - }, - getParams() { - const params = { - limit: 100, - sorts: [ - { - propertyName: "hs_lastmodifieddate", - direction: "DESCENDING", - }, - ], - object: "deals", - }; - if (this.pipeline) { - params.filters = [ - { - propertyName: "pipeline", - operator: "EQ", - value: this.pipeline, - }, - ]; - if (this.stage) { - params.filters.push({ - propertyName: "dealstage", - operator: "EQ", - value: this.stage, - }); - } - } - return params; - }, - async processResults(after, params) { - await this.searchCRM(params, after); - }, - }, -}; diff --git a/components/hubspot/sources/new-deal/new-deal.mjs b/components/hubspot/sources/new-deal/new-deal.mjs deleted file mode 100644 index 36b90ec1ba143..0000000000000 --- a/components/hubspot/sources/new-deal/new-deal.mjs +++ /dev/null @@ -1,89 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - key: "hubspot-new-deal", - name: "New Deals", - description: "Emit new event for each new deal created. [See the documentation](https://developers.hubspot.com/docs/api/crm/search)", - version: "0.0.17", - dedupe: "unique", - type: "source", - props: { - ...common.props, - pipeline: { - propDefinition: [ - common.props.hubspot, - "dealPipeline", - ], - description: "Filter deals by pipeline", - optional: true, - }, - stage: { - propDefinition: [ - common.props.hubspot, - "stages", - (c) => ({ - pipeline: c.pipeline, - }), - ], - type: "string", - label: "Stage", - description: "Filter deals by stage", - optional: true, - }, - }, - hooks: {}, - methods: { - ...common.methods, - getTs(deal) { - return Date.parse(deal.createdAt); - }, - generateMeta(deal) { - const { - id, - properties, - } = deal; - const ts = this.getTs(deal); - return { - id, - summary: properties.dealname, - ts, - }; - }, - isRelevant(deal, createdAfter) { - return this.getTs(deal) > createdAfter; - }, - getParams() { - const params = { - limit: 100, - sorts: [ - { - propertyName: "createdate", - direction: "DESCENDING", - }, - ], - object: "deals", - }; - if (this.pipeline) { - params.filters = [ - { - propertyName: "pipeline", - operator: "EQ", - value: this.pipeline, - }, - ]; - if (this.stage) { - params.filters.push({ - propertyName: "dealstage", - operator: "EQ", - value: this.stage, - }); - } - } - return params; - }, - async processResults(after, params) { - await this.searchCRM(params, after); - }, - }, -}; diff --git a/components/hubspot/sources/new-email-event/new-email-event.mjs b/components/hubspot/sources/new-email-event/new-email-event.mjs index a74076c7078b0..08c5c0b103875 100644 --- a/components/hubspot/sources/new-email-event/new-email-event.mjs +++ b/components/hubspot/sources/new-email-event/new-email-event.mjs @@ -1,14 +1,26 @@ import common from "../common/common.mjs"; +import { DEFAULT_LIMIT } from "../../common/constants.mjs"; +import { EMAIL_EVENT_TYPES } from "../../common/object-types.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "hubspot-new-email-event", name: "New Email Event", description: "Emit new event for each new Hubspot email event.", - version: "0.0.15", + version: "0.0.22", dedupe: "unique", type: "source", - hooks: {}, + props: { + ...common.props, + type: { + type: "string", + label: "Event Type", + description: "Filter results by the email event type", + options: EMAIL_EVENT_TYPES, + optional: true, + }, + }, methods: { ...common.methods, getTs(emailEvent) { @@ -27,11 +39,16 @@ export default { ts, }; }, - getParams() { - const startTimestamp = new Date(); + getParams(after) { + const params = { + limit: DEFAULT_LIMIT, + startTimestamp: after, + }; + if (this.type) { + params.eventType = this.type; + } return { - limit: 100, - startTimestamp, + params, }; }, async processResults(after, params) { @@ -42,4 +59,5 @@ export default { ); }, }, + sampleEmit, }; diff --git a/components/hubspot/sources/new-email-event/test-event.mjs b/components/hubspot/sources/new-email-event/test-event.mjs new file mode 100644 index 0000000000000..fbe39d6854bf4 --- /dev/null +++ b/components/hubspot/sources/new-email-event/test-event.mjs @@ -0,0 +1,17 @@ +export default { + "appId": 20185, + "appName": "AbBatch", + "created": 1401715737236, + "emailCampaignId": 13054799, + "hmid": "CiQ4ZWE5NTlmYy00MmU1LTRlZDctOTJjZC0zNjI0ZGRlNGYwODQQs+gDGNmdASAAKM/mnAYwoRY6Em1heGlha0BodWJzcG90LmNvbUCUhZPm5ShIAVooMWQ5NmYyODcyNDFjMmQzYWU1MDEwNTc3NmRkMDI5NzQ4ZjVjY2U3NXCR9A0=", + "id": "8ea959fc-42e5-4ed7-92cd-3624dde4f084", + "portalId": 62515, + "recipient": "maxiak@hubspot.com", + "sendId": "1d96f287241c2d3ae50105776dd029748f5cce75", + "sentBy": { + "created": 1401715737236, + "id": "8ea959fc-42e5-4ed7-92cd-3624dde4f084" + }, + "subject": "test", + "type": "SENT" +} \ No newline at end of file diff --git a/components/hubspot/sources/new-email-subscriptions-timeline/new-email-subscriptions-timeline.mjs b/components/hubspot/sources/new-email-subscriptions-timeline/new-email-subscriptions-timeline.mjs index 4f7e3835bb0c3..66752c1234d00 100644 --- a/components/hubspot/sources/new-email-subscriptions-timeline/new-email-subscriptions-timeline.mjs +++ b/components/hubspot/sources/new-email-subscriptions-timeline/new-email-subscriptions-timeline.mjs @@ -1,21 +1,21 @@ import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "hubspot-new-email-subscriptions-timeline", name: "New Email Subscriptions Timeline", - description: "Emit new event when new email timeline subscription added for the portal.", - version: "0.0.12", + description: "Emit new event when a new email timeline subscription is added for the portal.", + version: "0.0.19", dedupe: "unique", type: "source", - hooks: {}, methods: { ...common.methods, getTs(timeline) { return timeline.timestamp; }, generateMeta(timeline) { - const { normalizedEmailId: id } = timeline; + const { recipient: id } = timeline; const ts = this.getTs(timeline); return { id: `${id}${ts}`, @@ -26,10 +26,11 @@ export default { isRelevant(timeline, createdAfter) { return this.getTs(timeline) > createdAfter; }, - getParams() { - const startTimestamp = new Date(); + getParams(after) { return { - startTimestamp, + params: { + startTimestamp: after, + }, }; }, async processResults(after, params) { @@ -41,4 +42,5 @@ export default { ); }, }, + sampleEmit, }; diff --git a/components/hubspot/sources/new-email-subscriptions-timeline/test-event.mjs b/components/hubspot/sources/new-email-subscriptions-timeline/test-event.mjs new file mode 100644 index 0000000000000..bb6ad4f1d8ad4 --- /dev/null +++ b/components/hubspot/sources/new-email-subscriptions-timeline/test-event.mjs @@ -0,0 +1,17 @@ +export default { + "timestamp": 1401975207000, + "portalId": 62515, + "recipient": "6d4b537e-c5ac-11e3-a673-00262df65d03@some.email.com", + "changes": [ + { + "change": "BOUNCED", + "source": "SOURCE_NON_DELIVERY_REPORT", + "portalId": 62515, + "changeType": "PORTAL_BOUNCE", + "causedByEvent": { + "id": "6d72d39c-87da-3ced-bfdf-5f0213363827", + "created": 1401975207000 + } + } + ] +} \ No newline at end of file diff --git a/components/hubspot/sources/new-engagement/new-engagement.mjs b/components/hubspot/sources/new-engagement/new-engagement.mjs index 31f3c7872a496..ac42ff8de0948 100644 --- a/components/hubspot/sources/new-engagement/new-engagement.mjs +++ b/components/hubspot/sources/new-engagement/new-engagement.mjs @@ -1,25 +1,37 @@ import common from "../common/common.mjs"; +import { ENGAGEMENT_TYPES } from "../../common/object-types.mjs"; +import { DEFAULT_LIMIT } from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "hubspot-new-engagement", name: "New Engagement", description: "Emit new event for each new engagement created. This action returns a maximum of 5000 records at a time, make sure you set a correct time range so you don't miss any events", - version: "0.0.16", + version: "0.0.24", dedupe: "unique", type: "source", - hooks: {}, + props: { + ...common.props, + types: { + type: "string[]", + label: "Engagement Types", + description: "Filter results by the type of engagment", + options: ENGAGEMENT_TYPES, + optional: true, + }, + }, methods: { ...common.methods, getTs(engagement) { - return Date.parse(engagement.createdAt); + return engagement.engagement.createdAt; }, generateMeta(engagement) { const { id, type, } = engagement.engagement; - const ts = this.getTs(engagement.engagement); + const ts = this.getTs(engagement); return { id, summary: type, @@ -27,13 +39,21 @@ export default { }; }, isRelevant(engagement, createdAfter) { - return this.getTs(engagement.engagement) > createdAfter; + if (this.getTs(engagement) < createdAfter) { + return false; + } + if (this.types?.length) { + return this.types.includes(engagement.engagement.type); + } + return true; }, }, async run() { const createdAfter = this._getAfter(); const params = { - limit: 250, + params: { + limit: DEFAULT_LIMIT, + }, }; await this.paginateUsingHasMore( @@ -44,4 +64,5 @@ export default { 20, ); }, + sampleEmit, }; diff --git a/components/hubspot/sources/new-engagement/test-event.mjs b/components/hubspot/sources/new-engagement/test-event.mjs new file mode 100644 index 0000000000000..3dc0c0186b67c --- /dev/null +++ b/components/hubspot/sources/new-engagement/test-event.mjs @@ -0,0 +1,42 @@ +export default { + "engagement": { + "id": 54890657094, + "portalId": 6890009, + "active": true, + "createdAt": 1719006221998, + "lastUpdated": 1719006222686, + "createdBy": 9555737, + "modifiedBy": 9555737, + "ownerId": 41488296, + "type": "TASK", + "timestamp": 1719403200000, + "allAccessibleTeamIds": [], + "queueMembershipIds": [], + "bodyPreviewIsTruncated": false + }, + "associations": { + "contactIds": [], + "companyIds": [], + "dealIds": [], + "ownerIds": [], + "workflowIds": [], + "ticketIds": [], + "contentIds": [], + "quoteIds": [], + "marketingEventIds": [], + "partnerClientIds": [] + }, + "attachments": [], + "scheduledTasks": [], + "metadata": { + "body": "

", + "status": "NOT_STARTED", + "forObjectType": "OWNER", + "subject": "task", + "taskType": "TODO", + "reminders": [], + "sendDefaultReminder": false, + "priority": "NONE", + "isAllDay": false + } +} \ No newline at end of file diff --git a/components/hubspot/sources/new-event/new-event.mjs b/components/hubspot/sources/new-event/new-event.mjs index 3a69b6959682c..1e6026623037f 100644 --- a/components/hubspot/sources/new-event/new-event.mjs +++ b/components/hubspot/sources/new-event/new-event.mjs @@ -1,11 +1,14 @@ import common from "../common/common.mjs"; +import { ConfigurationError } from "@pipedream/platform"; +import { DEFAULT_LIMIT } from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "hubspot-new-event", name: "New Events", - description: "Emit new event for each new Hubspot event.", - version: "0.0.15", + description: "Emit new event for each new Hubspot event. Note: Only available for Marketing Hub Enterprise, Sales Hub Enterprise, Service Hub Enterprise, or CMS Hub Enterprise accounts", + version: "0.0.23", dedupe: "unique", type: "source", props: { @@ -26,7 +29,21 @@ export default { ], }, }, - hooks: {}, + hooks: { + async deploy() { + try { + await this.hubspot.getEvents({ + params: { + objectType: this.objectType, + objectId: this.objectIds[0], + }, + }); + } + catch { + throw new ConfigurationError("Error occurred. Please verify that your Hubspot account is one of: Marketing Hub Enterprise, Sales Hub Enterprise, Service Hub Enterprise, or CMS Hub Enterprise"); + } + }, + }, methods: { ...common.methods, getTs() { @@ -48,10 +65,12 @@ export default { }, getEventParams(objectId, occurredAfter) { return { - limit: 100, - objectType: this.objectType, - objectId, - occurredAfter, + params: { + limit: DEFAULT_LIMIT, + objectType: this.objectType, + objectId, + occurredAfter, + }, }; }, async processResults(after) { @@ -66,4 +85,5 @@ export default { } }, }, + sampleEmit, }; diff --git a/components/hubspot/sources/new-event/test-event.mjs b/components/hubspot/sources/new-event/test-event.mjs new file mode 100644 index 0000000000000..4e53600f30dad --- /dev/null +++ b/components/hubspot/sources/new-event/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "occurredAt": "2024-06-24T17:34:56.077Z", + "eventType": "string", + "id": "string", + "objectId": "string", + "properties": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string" + }, + "objectType": "string" +} \ No newline at end of file diff --git a/components/hubspot/sources/new-form-submission/new-form-submission.mjs b/components/hubspot/sources/new-form-submission/new-form-submission.mjs index 63fff12658b06..a577add786d52 100644 --- a/components/hubspot/sources/new-form-submission/new-form-submission.mjs +++ b/components/hubspot/sources/new-form-submission/new-form-submission.mjs @@ -1,11 +1,12 @@ import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "hubspot-new-form-submission", name: "New Form Submission", description: "Emit new event for each new submission of a form.", - version: "0.0.17", + version: "0.0.24", dedupe: "unique", type: "source", props: { @@ -18,7 +19,6 @@ export default { withLabel: false, }, }, - hooks: {}, methods: { ...common.methods, getTs(result) { @@ -40,7 +40,9 @@ export default { }, getParams() { return { - limit: 50, + params: { + limit: 50, + }, }; }, async processResults(after, baseParams) { @@ -60,4 +62,5 @@ export default { ); }, }, + sampleEmit, }; diff --git a/components/hubspot/sources/new-form-submission/test-event.mjs b/components/hubspot/sources/new-form-submission/test-event.mjs new file mode 100644 index 0000000000000..681949b4db099 --- /dev/null +++ b/components/hubspot/sources/new-form-submission/test-event.mjs @@ -0,0 +1,11 @@ +export default { + "submittedAt": 1719252562899, + "values": [ + { + "name": "email", + "value": "test@sample.com", + "objectTypeId": "0-1" + } + ], + "pageUrl": "https://share.hsforms.com/13MJGKokMSAmW69lIF_JiKg43od5" +} \ No newline at end of file diff --git a/components/hubspot/sources/new-line-item/new-line-item.mjs b/components/hubspot/sources/new-line-item/new-line-item.mjs deleted file mode 100644 index f224fa734ae7b..0000000000000 --- a/components/hubspot/sources/new-line-item/new-line-item.mjs +++ /dev/null @@ -1,45 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - key: "hubspot-new-line-item", - name: "New Line Item", - description: "Emit new event for each new line item added.", - version: "0.0.12", - dedupe: "unique", - type: "source", - hooks: {}, - methods: { - ...common.methods, - getTs(lineItem) { - return Date.parse(lineItem.createdAt); - }, - generateMeta(lineItem) { - const { id } = lineItem; - const ts = this.getTs(lineItem); - return { - id, - summary: `New Line Item ID: ${id}`, - ts, - }; - }, - isRelevant(lineItem, createdAfter) { - return this.lineItem > createdAfter; - }, - getParams() { - return { - limit: 100, - sorts: [ - { - propertyName: "createdate", - direction: "DESCENDING", - }, - ], - object: "line_items", - }; - }, - async processResults(after, params) { - await this.searchCRM(params, after); - }, - }, -}; diff --git a/components/hubspot/sources/new-note/new-note.mjs b/components/hubspot/sources/new-note/new-note.mjs new file mode 100644 index 0000000000000..ec84702f39ede --- /dev/null +++ b/components/hubspot/sources/new-note/new-note.mjs @@ -0,0 +1,56 @@ +import common from "../common/common.mjs"; +import { + DEFAULT_LIMIT, OBJECT_TYPES, +} from "../../common/constants.mjs"; + +export default { + ...common, + key: "hubspot-new-note", + name: "New Note Created", + description: "Emit new event for each new note created. [See the documentation](https://developers.hubspot.com/docs/reference/api/crm/engagements/notes#get-%2Fcrm%2Fv3%2Fobjects%2Fnotes)", + version: "1.0.0", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTs(note) { + return Date.parse(note.createdAt); + }, + generateMeta(note) { + return { + id: note.id, + summary: `New Note: ${note.properties.hs_body_preview || note.id}`, + ts: this.getTs(note), + }; + }, + isRelevant(note, createdAfter) { + return this.getTs(note) > createdAfter; + }, + async getParams() { + const { results: allProperties } = await this.hubspot.getProperties({ + objectType: "notes", + }); + const properties = allProperties.map(({ name }) => name); + + const objectTypes = OBJECT_TYPES.map(({ value }) => value); + const { results: custom } = await this.hubspot.listSchemas(); + const customObjects = custom?.map(({ fullyQualifiedName }) => fullyQualifiedName); + const associations = [ + ...objectTypes, + ...customObjects, + ]; + + return { + params: { + limit: DEFAULT_LIMIT, + properties: properties.join(","), + associations: associations.join(","), + }, + }; + }, + async processResults(after, params) { + const notes = await this.getPaginatedItems(this.hubspot.listNotes.bind(this), params); + await this.processEvents(notes, after); + }, + }, +}; diff --git a/components/hubspot/sources/new-or-updated-blog-article/new-or-updated-blog-article.mjs b/components/hubspot/sources/new-or-updated-blog-article/new-or-updated-blog-article.mjs new file mode 100644 index 0000000000000..2733c6b995d09 --- /dev/null +++ b/components/hubspot/sources/new-or-updated-blog-article/new-or-updated-blog-article.mjs @@ -0,0 +1,66 @@ +import common from "../common/common.mjs"; +import { DEFAULT_LIMIT } from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "hubspot-new-or-updated-blog-article", + name: "New or Updated Blog Post", + description: "Emit new event for each new or updated blog post in Hubspot.", + version: "0.0.6", + dedupe: "unique", + type: "source", + props: { + ...common.props, + newOnly: { + type: "boolean", + label: "New Only", + description: "Emit events only for new blog articles", + default: false, + optional: true, + }, + }, + methods: { + ...common.methods, + getTs(blogpost) { + return this.newOnly + ? Date.parse(blogpost.created) + : Date.parse(blogpost.updated); + }, + generateMeta(blogpost) { + const { + id, + name: summary, + } = blogpost; + const ts = this.getTs(blogpost); + return { + id: this.newOnly + ? id + : `${id}-${ts}`, + summary, + ts, + }; + }, + isRelevant(blogpost, updatedAfter) { + return this.getTs(blogpost) > updatedAfter; + }, + getParams(after) { + return { + params: { + limit: DEFAULT_LIMIT, + updated__gte: after, + sort: "-updatedAt", + }, + }; + }, + async processResults(after, params) { + await this.paginate( + params, + this.hubspot.getBlogPosts.bind(this), + "results", + after, + ); + }, + }, + sampleEmit, +}; diff --git a/components/hubspot/sources/new-or-updated-blog-article/test-event.mjs b/components/hubspot/sources/new-or-updated-blog-article/test-event.mjs new file mode 100644 index 0000000000000..02fb5462dcbbe --- /dev/null +++ b/components/hubspot/sources/new-or-updated-blog-article/test-event.mjs @@ -0,0 +1,37 @@ +export default { + "archivedAt": 0, + "archivedInDashboard": false, + "attachedStylesheets": [], + "authorName": "Author", + "categoryId": 3, + "contentGroupId": "23646627706", + "contentTypeCategory": 3, + "created": "2024-06-20T22:37:03.004Z", + "createdById": "9555737", + "currentState": "DRAFT", + "currentlyPublished": false, + "domain": "", + "enableGoogleAmpOutputOverride": false, + "featuredImage": "", + "featuredImageAltText": "", + "htmlTitle": "Blog Post", + "id": "170912644748", + "language": "en", + "layoutSections": {}, + "linkRelCanonicalUrl": "", + "metaDescription": "", + "name": "Blog Post", + "publicAccessRules": [], + "publicAccessRulesEnabled": false, + "publishDate": "2024-06-20T22:37:02Z", + "slug": "", + "state": "DRAFT", + "tagIds": [], + "translations": {}, + "updated": "2024-06-20T22:37:23.772Z", + "updatedById": "9555737", + "url": "", + "useFeaturedImage": true, + "widgetContainers": {}, + "widgets": {} +} \ No newline at end of file diff --git a/components/hubspot/sources/new-or-updated-company/new-or-updated-company.mjs b/components/hubspot/sources/new-or-updated-company/new-or-updated-company.mjs new file mode 100644 index 0000000000000..b61c788c3e5ba --- /dev/null +++ b/components/hubspot/sources/new-or-updated-company/new-or-updated-company.mjs @@ -0,0 +1,88 @@ +import common from "../common/common.mjs"; +import { + DEFAULT_LIMIT, DEFAULT_COMPANY_PROPERTIES, +} from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "hubspot-new-or-updated-company", + name: "New or Updated Company", + description: "Emit new event for each new or updated company in Hubspot.", + version: "0.0.6", + dedupe: "unique", + type: "source", + props: { + ...common.props, + info: { + type: "alert", + alertType: "info", + content: `Properties:\n\`${DEFAULT_COMPANY_PROPERTIES.join(", ")}\``, + }, + properties: { + propDefinition: [ + common.props.hubspot, + "companyProperties", + () => ({ + excludeDefaultProperties: true, + }), + ], + label: "Additional properties to retrieve", + }, + newOnly: { + type: "boolean", + label: "New Only", + description: "Emit events only for new companies", + default: false, + optional: true, + }, + }, + methods: { + ...common.methods, + getTs(company) { + return this.isNew + ? Date.parse(company.createdAt) + : Date.parse(company.updatedAt); + }, + generateMeta(company) { + const { + id, + properties, + } = company; + const ts = this.getTs(company); + return { + id: this.newOnly + ? id + : `${id}-${ts}`, + summary: properties.name, + ts, + }; + }, + isRelevant(company, updatedAfter) { + return this.getTs(company) > updatedAfter; + }, + getParams() { + const { properties = [] } = this; + return { + data: { + limit: DEFAULT_LIMIT, + sorts: [ + { + propertyName: "hs_lastmodifieddate", + direction: "DESCENDING", + }, + ], + properties: [ + ...DEFAULT_COMPANY_PROPERTIES, + ...properties, + ], + }, + object: "companies", + }; + }, + async processResults(after, params) { + await this.searchCRM(params, after); + }, + }, + sampleEmit, +}; diff --git a/components/hubspot/sources/new-or-updated-company/test-event.mjs b/components/hubspot/sources/new-or-updated-company/test-event.mjs new file mode 100644 index 0000000000000..97108d247cf4d --- /dev/null +++ b/components/hubspot/sources/new-or-updated-company/test-event.mjs @@ -0,0 +1,37 @@ +export default { + "id": "21511366947", + "properties": { + "about_us": null, + "address": null, + "address2": null, + "annualrevenue": null, + "city": null, + "country": null, + "createdate": "2024-06-20T03:09:18.816Z", + "description": null, + "domain": null, + "founded_year": null, + "hs_createdate": null, + "hs_lastmodifieddate": "2024-06-20T03:09:23.664Z", + "hs_object_id": "21511366947", + "industry": null, + "is_public": null, + "lifecyclestage": "lead", + "name": "Company", + "numberofemployees": null, + "owneremail": null, + "ownername": null, + "phone": null, + "state": null, + "timezone": null, + "total_money_raised": null, + "total_revenue": null, + "type": null, + "web_technologies": null, + "website": null, + "zip": null + }, + "createdAt": "2024-06-20T03:09:18.816Z", + "updatedAt": "2024-06-20T03:09:23.664Z", + "archived": false +} \ No newline at end of file diff --git a/components/hubspot/sources/new-or-updated-contact/new-or-updated-contact.mjs b/components/hubspot/sources/new-or-updated-contact/new-or-updated-contact.mjs new file mode 100644 index 0000000000000..04df26dda6553 --- /dev/null +++ b/components/hubspot/sources/new-or-updated-contact/new-or-updated-contact.mjs @@ -0,0 +1,122 @@ +import common from "../common/common.mjs"; +import { + DEFAULT_LIMIT, DEFAULT_CONTACT_PROPERTIES, +} from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "hubspot-new-or-updated-contact", + name: "New or Updated Contact", + description: "Emit new event for each new or updated contact in Hubspot.", + version: "0.0.6", + dedupe: "unique", + type: "source", + props: { + ...common.props, + info: { + type: "alert", + alertType: "info", + content: `Properties:\n\`${DEFAULT_CONTACT_PROPERTIES.join(", ")}\``, + }, + properties: { + propDefinition: [ + common.props.hubspot, + "contactProperties", + () => ({ + excludeDefaultProperties: true, + }), + ], + label: "Additional properties to retrieve", + }, + lists: { + propDefinition: [ + common.props.hubspot, + "lists", + ], + withLabel: false, + optional: true, + }, + newOnly: { + type: "boolean", + label: "New Only", + description: "Emit events only for new contacts", + default: false, + optional: true, + }, + }, + methods: { + ...common.methods, + getTs(contact) { + return this.newOnly + ? Date.parse(contact.createdAt) + : Date.parse(contact.updatedAt); + }, + generateMeta(contact) { + const { id } = contact; + const ts = this.getTs(contact); + return { + id: this.newOnly + ? id + : `${id}-${ts}`, + summary: `Record ID: ${id}`, + ts, + }; + }, + async translateListIds(lists) { + const listIds = []; + for (const list of lists) { + const { listId } = await this.hubspot.translateLegacyListId({ + params: { + legacyListId: list, + }, + }); + listIds.push(listId); + } + return listIds; + }, + async isRelevant(contact, updatedAfter) { + if (this.getTs(contact) < updatedAfter) { + return false; + } + if (this.lists?.length) { + const { results } = await this.hubspot.getMemberships({ + objectType: "contacts", + objectId: contact.id, + }); + const contactListIds = results?.map(({ listId }) => listId) || []; + const listIds = await this.translateListIds(this.lists); + for (const list of listIds) { + if (contactListIds.includes(list)) { + return true; + } + } + return false; + } + return true; + }, + getParams() { + const { properties = [] } = this; + return { + data: { + limit: DEFAULT_LIMIT, + sorts: [ + { + propertyName: "lastmodifieddate", + direction: "DESCENDING", + }, + ], + properties: [ + ...DEFAULT_CONTACT_PROPERTIES, + ...properties, + ], + }, + object: "contacts", + }; + }, + async processResults(after, params) { + await this.searchCRM(params, after); + }, + }, + sampleEmit, +}; diff --git a/components/hubspot/sources/new-or-updated-contact/test-event.mjs b/components/hubspot/sources/new-or-updated-contact/test-event.mjs new file mode 100644 index 0000000000000..5ffa5c7f315eb --- /dev/null +++ b/components/hubspot/sources/new-or-updated-contact/test-event.mjs @@ -0,0 +1,34 @@ +export default { + "id": "31612976545", + "properties": { + "address": null, + "annualrevenue": null, + "city": null, + "company": null, + "country": null, + "createdate": "2024-06-20T00:57:37.690Z", + "email": null, + "fax": null, + "firstname": "Hello", + "hs_createdate": null, + "hs_email_domain": null, + "hs_language": null, + "hs_object_id": "31612976545", + "hs_persona": null, + "industry": null, + "jobtitle": null, + "lastmodifieddate": "2024-06-20T02:08:27.180Z", + "lastname": "World", + "lifecyclestage": "lead", + "mobilephone": null, + "numemployees": null, + "phone": null, + "salutation": null, + "state": null, + "website": null, + "zip": null + }, + "createdAt": "2024-06-20T00:57:37.690Z", + "updatedAt": "2024-06-20T02:08:27.180Z", + "archived": false +} \ No newline at end of file diff --git a/components/hubspot/sources/new-or-updated-crm-object/new-or-updated-crm-object.mjs b/components/hubspot/sources/new-or-updated-crm-object/new-or-updated-crm-object.mjs index 54dcd2d2f5a4a..11ae0e1d1cef6 100644 --- a/components/hubspot/sources/new-or-updated-crm-object/new-or-updated-crm-object.mjs +++ b/components/hubspot/sources/new-or-updated-crm-object/new-or-updated-crm-object.mjs @@ -1,11 +1,13 @@ import common from "../common/common.mjs"; +import { DEFAULT_LIMIT } from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "hubspot-new-or-updated-crm-object", name: "New or Updated CRM Object", description: "Emit new event each time a CRM Object of the specified object type is updated.", - version: "0.0.12", + version: "0.0.19", dedupe: "unique", type: "source", props: { @@ -17,7 +19,6 @@ export default { ], }, }, - hooks: {}, methods: { ...common.methods, getTs(object) { @@ -43,13 +44,15 @@ export default { ? "lastmodifieddate" : "hs_lastmodifieddate"; return { - limit: 100, - sorts: [ - { - propertyName, - direction: "DESCENDING", - }, - ], + data: { + limit: DEFAULT_LIMIT, + sorts: [ + { + propertyName, + direction: "DESCENDING", + }, + ], + }, object, }; }, @@ -61,4 +64,5 @@ export default { await this.searchCRM(params, after); }, }, + sampleEmit, }; diff --git a/components/hubspot/sources/new-or-updated-crm-object/test-event.mjs b/components/hubspot/sources/new-or-updated-crm-object/test-event.mjs new file mode 100644 index 0000000000000..7ae40b6f48ed5 --- /dev/null +++ b/components/hubspot/sources/new-or-updated-crm-object/test-event.mjs @@ -0,0 +1,14 @@ +export default { + "id": "12951", + "properties": { + "createdate": "2022-11-23T15:43:37.313Z", + "email": "michelle.bergeron@gmail.com", + "firstname": "Test", + "hs_object_id": "12951", + "lastmodifieddate": "2024-06-24T17:59:13.790Z", + "lastname": "User" + }, + "createdAt": "2022-11-23T15:43:37.313Z", + "updatedAt": "2024-06-24T17:59:13.790Z", + "archived": false +} \ No newline at end of file diff --git a/components/hubspot/sources/new-or-updated-custom-object/new-or-updated-custom-object.mjs b/components/hubspot/sources/new-or-updated-custom-object/new-or-updated-custom-object.mjs index a8b42f1a4e7e3..59163a149a703 100644 --- a/components/hubspot/sources/new-or-updated-custom-object/new-or-updated-custom-object.mjs +++ b/components/hubspot/sources/new-or-updated-custom-object/new-or-updated-custom-object.mjs @@ -1,11 +1,13 @@ import common from "../common/common.mjs"; +import { DEFAULT_LIMIT } from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "hubspot-new-or-updated-custom-object", name: "New or Updated Custom Object", description: "Emit new event each time a Custom Object of the specified schema is updated.", - version: "0.0.1", + version: "0.0.8", dedupe: "unique", type: "source", props: { @@ -17,7 +19,6 @@ export default { ], }, }, - hooks: {}, methods: { ...common.methods, getTs(object) { @@ -40,13 +41,15 @@ export default { }, getObjectParams(object) { return { - limit: 100, - sorts: [ - { - propertyName: "hs_lastmodifieddate", - direction: "DESCENDING", - }, - ], + data: { + limit: DEFAULT_LIMIT, + sorts: [ + { + propertyName: "hs_lastmodifieddate", + direction: "DESCENDING", + }, + ], + }, object, }; }, @@ -55,4 +58,5 @@ export default { await this.searchCRM(params, after); }, }, + sampleEmit, }; diff --git a/components/hubspot/sources/new-or-updated-custom-object/test-event.mjs b/components/hubspot/sources/new-or-updated-custom-object/test-event.mjs new file mode 100644 index 0000000000000..00e1f941fd4cc --- /dev/null +++ b/components/hubspot/sources/new-or-updated-custom-object/test-event.mjs @@ -0,0 +1,11 @@ +export default { + "id": "13733831509", + "properties": { + "hs_createdate": "2024-06-24T18:20:11.270Z", + "hs_lastmodifieddate": "2024-06-24T18:20:11.270Z", + "hs_object_id": "13733831509" + }, + "createdAt": "2024-06-24T18:20:11.270Z", + "updatedAt": "2024-06-24T18:20:11.270Z", + "archived": false +} \ No newline at end of file diff --git a/components/hubspot/sources/new-or-updated-deal/new-or-updated-deal.mjs b/components/hubspot/sources/new-or-updated-deal/new-or-updated-deal.mjs new file mode 100644 index 0000000000000..5699a2472fd59 --- /dev/null +++ b/components/hubspot/sources/new-or-updated-deal/new-or-updated-deal.mjs @@ -0,0 +1,126 @@ +import common from "../common/common.mjs"; +import { + DEFAULT_LIMIT, DEFAULT_DEAL_PROPERTIES, +} from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "hubspot-new-or-updated-deal", + name: "New or Updated Deal", + description: "Emit new event for each new or updated deal in Hubspot", + version: "0.0.6", + dedupe: "unique", + type: "source", + props: { + ...common.props, + info: { + type: "alert", + alertType: "info", + content: `Properties:\n\`${DEFAULT_DEAL_PROPERTIES.join(", ")}\``, + }, + properties: { + propDefinition: [ + common.props.hubspot, + "dealProperties", + () => ({ + excludeDefaultProperties: true, + }), + ], + label: "Additional properties to retrieve", + }, + pipeline: { + propDefinition: [ + common.props.hubspot, + "dealPipeline", + ], + description: "Filter deals by pipeline", + optional: true, + }, + stage: { + propDefinition: [ + common.props.hubspot, + "stages", + (c) => ({ + pipeline: c.pipeline, + }), + ], + type: "string", + label: "Stage", + description: "Filter deals by stage", + optional: true, + }, + newOnly: { + type: "boolean", + label: "New Only", + description: "Emit events only for new deals", + default: false, + optional: true, + }, + }, + methods: { + ...common.methods, + getTs(deal) { + return this.newOnly + ? Date.parse(deal.createdAt) + : Date.parse(deal.updatedAt); + }, + generateMeta(deal) { + const { + id, + properties, + } = deal; + const ts = this.getTs(deal); + return { + id: this.newOnly + ? id + : `${id}-${ts}`, + summary: properties.dealname, + ts, + }; + }, + isRelevant(deal, updatedAfter) { + return this.getTs(deal) > updatedAfter; + }, + getParams() { + const { properties = [] } = this; + const params = { + data: { + limit: DEFAULT_LIMIT, + sorts: [ + { + propertyName: "hs_lastmodifieddate", + direction: "DESCENDING", + }, + ], + properties: [ + ...DEFAULT_DEAL_PROPERTIES, + ...properties, + ], + }, + object: "deals", + }; + if (this.pipeline) { + params.data.filters = [ + { + propertyName: "pipeline", + operator: "EQ", + value: this.pipeline, + }, + ]; + if (this.stage) { + params.data.filters.push({ + propertyName: "dealstage", + operator: "EQ", + value: this.stage, + }); + } + } + return params; + }, + async processResults(after, params) { + await this.searchCRM(params, after); + }, + }, + sampleEmit, +}; diff --git a/components/hubspot/sources/new-or-updated-deal/test-event.mjs b/components/hubspot/sources/new-or-updated-deal/test-event.mjs new file mode 100644 index 0000000000000..1c14610d50d72 --- /dev/null +++ b/components/hubspot/sources/new-or-updated-deal/test-event.mjs @@ -0,0 +1,23 @@ +export default { + "id": "20223680134", + "properties": { + "amount": null, + "closedate": null, + "createdate": "2024-06-20T20:36:02.551Z", + "dealname": "deal", + "dealtype": null, + "description": null, + "hs_forecast_amount": null, + "hs_forecast_probability": null, + "hs_lastmodifieddate": "2024-06-20T20:36:03.656Z", + "hs_manual_forecast_category": null, + "hs_next_step": null, + "hs_object_id": "20223680134", + "hubspot_owner_id": null, + "hubspot_team_id": null, + "num_associated_contacts": "0" + }, + "createdAt": "2024-06-20T20:36:02.551Z", + "updatedAt": "2024-06-20T20:36:03.656Z", + "archived": false +} \ No newline at end of file diff --git a/components/hubspot/sources/new-or-updated-line-item/new-or-updated-line-item.mjs b/components/hubspot/sources/new-or-updated-line-item/new-or-updated-line-item.mjs new file mode 100644 index 0000000000000..ba5b60f0fe948 --- /dev/null +++ b/components/hubspot/sources/new-or-updated-line-item/new-or-updated-line-item.mjs @@ -0,0 +1,85 @@ +import common from "../common/common.mjs"; +import { + DEFAULT_LIMIT, DEFAULT_LINE_ITEM_PROPERTIES, +} from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "hubspot-new-or-updated-line-item", + name: "New or Updated Line Item", + description: "Emit new event for each new line item added or updated in Hubspot.", + version: "0.0.6", + dedupe: "unique", + type: "source", + props: { + ...common.props, + info: { + type: "alert", + alertType: "info", + content: `Properties:\n\`${DEFAULT_LINE_ITEM_PROPERTIES.join(", ")}\``, + }, + properties: { + propDefinition: [ + common.props.hubspot, + "lineItemProperties", + () => ({ + excludeDefaultProperties: true, + }), + ], + label: "Additional properties to retrieve", + }, + newOnly: { + type: "boolean", + label: "New Only", + description: "Emit events only for new line items", + default: false, + optional: true, + }, + }, + methods: { + ...common.methods, + getTs(lineItem) { + return this.newOnly + ? Date.parse(lineItem.createdAt) + : Date.parse(lineItem.updatedAt); + }, + generateMeta(lineItem) { + const { id } = lineItem; + const ts = this.getTs(lineItem); + return { + id: this.newOnly + ? id + : `${id}-${ts}`, + summary: `Line Item ID: ${id}`, + ts, + }; + }, + isRelevant(lineItem, updatedAfter) { + return this.getTs(lineItem) > updatedAfter; + }, + getParams() { + const { properties = [] } = this; + return { + data: { + limit: DEFAULT_LIMIT, + sorts: [ + { + propertyName: "hs_lastmodifieddate", + direction: "DESCENDING", + }, + ], + properties: [ + ...DEFAULT_LINE_ITEM_PROPERTIES, + ...properties, + ], + }, + object: "line_items", + }; + }, + async processResults(after, params) { + await this.searchCRM(params, after); + }, + }, + sampleEmit, +}; diff --git a/components/hubspot/sources/new-or-updated-line-item/test-event.mjs b/components/hubspot/sources/new-or-updated-line-item/test-event.mjs new file mode 100644 index 0000000000000..33d91852c5c17 --- /dev/null +++ b/components/hubspot/sources/new-or-updated-line-item/test-event.mjs @@ -0,0 +1,26 @@ +export default { + "id": "9742634370", + "properties": { + "amount": "1.00", + "createdate": "2024-06-20T22:16:10.936Z", + "description": null, + "discount": null, + "hs_cost_of_goods_sold": null, + "hs_discount_percentage": null, + "hs_images": null, + "hs_lastmodifieddate": "2024-06-20T22:16:10.936Z", + "hs_line_item_currency_code": "USD", + "hs_object_id": "9742634370", + "hs_product_id": "2848741041", + "hs_sku": null, + "hs_term_in_months": null, + "hs_url": null, + "name": "product", + "price": "1", + "quantity": "1", + "tax": null + }, + "createdAt": "2024-06-20T22:16:10.936Z", + "updatedAt": "2024-06-20T22:16:10.936Z", + "archived": false +} \ No newline at end of file diff --git a/components/hubspot/sources/new-or-updated-product/new-or-updated-product.mjs b/components/hubspot/sources/new-or-updated-product/new-or-updated-product.mjs new file mode 100644 index 0000000000000..aa35307531b69 --- /dev/null +++ b/components/hubspot/sources/new-or-updated-product/new-or-updated-product.mjs @@ -0,0 +1,88 @@ +import common from "../common/common.mjs"; +import { + DEFAULT_LIMIT, DEFAULT_PRODUCT_PROPERTIES, +} from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "hubspot-new-or-updated-product", + name: "New or Updated Product", + description: "Emit new event for each new or updated product in Hubspot.", + version: "0.0.6", + dedupe: "unique", + type: "source", + props: { + ...common.props, + info: { + type: "alert", + alertType: "info", + content: `Properties:\n\`${DEFAULT_PRODUCT_PROPERTIES.join(", ")}\``, + }, + properties: { + propDefinition: [ + common.props.hubspot, + "productProperties", + () => ({ + excludeDefaultProperties: true, + }), + ], + label: "Additional properties to retrieve", + }, + newOnly: { + type: "boolean", + label: "New Only", + description: "Emit events only for new products", + default: false, + optional: true, + }, + }, + methods: { + ...common.methods, + getTs(product) { + return this.newOnly + ? Date.parse(product.createdAt) + : Date.parse(product.updatedAt); + }, + generateMeta(product) { + const { + id, + properties, + } = product; + const ts = this.getTs(product); + return { + id: this.newOnly + ? id + : `${id}-${ts}`, + summary: properties.name, + ts, + }; + }, + isRelevant(product, updatedAfter) { + return this.getTs(product) > updatedAfter; + }, + getParams() { + const { properties = [] } = this; + return { + data: { + limit: DEFAULT_LIMIT, + sorts: [ + { + propertyName: "hs_lastmodifieddate", + direction: "DESCENDING", + }, + ], + properties: [ + ...DEFAULT_PRODUCT_PROPERTIES, + ...properties, + ], + }, + object: "products", + }; + }, + async processResults(after, params) { + await this.searchCRM(params, after); + }, + }, + sampleEmit, +}; diff --git a/components/hubspot/sources/new-or-updated-product/test-event.mjs b/components/hubspot/sources/new-or-updated-product/test-event.mjs new file mode 100644 index 0000000000000..0ee8b84b6e6e0 --- /dev/null +++ b/components/hubspot/sources/new-or-updated-product/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "id": "2848741041", + "properties": { + "createdate": "2024-06-20T21:33:00.925Z", + "description": null, + "hs_lastmodifieddate": "2024-06-20T21:33:00.925Z", + "hs_object_id": "2848741041", + "name": "product", + "price": "1", + "tax": null + }, + "createdAt": "2024-06-20T21:33:00.925Z", + "updatedAt": "2024-06-20T21:33:00.925Z", + "archived": false +} \ No newline at end of file diff --git a/components/hubspot/sources/new-product/new-product.mjs b/components/hubspot/sources/new-product/new-product.mjs deleted file mode 100644 index 5d1e106a81d2b..0000000000000 --- a/components/hubspot/sources/new-product/new-product.mjs +++ /dev/null @@ -1,48 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - key: "hubspot-new-product", - name: "New Products", - description: "Emit new event for each new product created.", - version: "0.0.12", - dedupe: "unique", - type: "source", - hooks: {}, - methods: { - ...common.methods, - getTs(product) { - return Date.parse(product.createdAt); - }, - generateMeta(product) { - const { - id, - properties, - } = product; - const ts = this.getTs(product); - return { - id, - summary: properties.name, - ts, - }; - }, - isRelevant(product, createdAfter) { - return this.getTs(product) > createdAfter; - }, - getParams() { - return { - limit: 100, - sorts: [ - { - propertyName: "createdate", - direction: "DESCENDING", - }, - ], - object: "products", - }; - }, - async processResults(after, params) { - await this.searchCRM(params, after); - }, - }, -}; diff --git a/components/hubspot/sources/new-social-media-message/new-social-media-message.mjs b/components/hubspot/sources/new-social-media-message/new-social-media-message.mjs index 83692be36d932..54ffde10eb4ee 100644 --- a/components/hubspot/sources/new-social-media-message/new-social-media-message.mjs +++ b/components/hubspot/sources/new-social-media-message/new-social-media-message.mjs @@ -1,11 +1,12 @@ import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "hubspot-new-social-media-message", name: "New Social Media Message", - description: "Emit new event when a message is posted from HubSpot to the specified social media channel", - version: "0.0.12", + description: "Emit new event when a message is posted from HubSpot to the specified social media channel. Note: Only available for Marketing Hub Enterprise accounts", + version: "0.0.19", type: "source", dedupe: "unique", props: { @@ -17,7 +18,6 @@ export default { ], }, }, - hooks: {}, methods: { ...common.methods, getTs(message) { @@ -40,8 +40,10 @@ export default { }, getParams(after) { return { - withChannelKeys: this.channel, - since: after, + params: { + withChannelKeys: this.channel, + since: after, + }, }; }, async processResults(after, params) { @@ -53,4 +55,5 @@ export default { ); }, }, + sampleEmit, }; diff --git a/components/hubspot/sources/new-social-media-message/test-event.mjs b/components/hubspot/sources/new-social-media-message/test-event.mjs new file mode 100644 index 0000000000000..eefa30e4e9d38 --- /dev/null +++ b/components/hubspot/sources/new-social-media-message/test-event.mjs @@ -0,0 +1,82 @@ +export default { + "broadcastGuid": "297987720", + "portalId": 6890009, + "groupGuid": "b4280073-efc6-4f75-84ca-ffd9aa542e40", + "campaignGuid": null, + "channelKey": "Twitter:1204207424409747457", + "channelGuid": "65391531-afc4-33f3-a1c6-7f8149109c87", + "clientTag": null, + "createdAt": 1719254193805, + "userUpdatedAt": 1719254198527, + "triggerAt": 1719254198527, + "finishedAt": 1719254202889, + "status": "SUCCESS", + "message": null, + "content": { + "originalLink": null, + "firstComment": null, + "charCount": "7", + "link": null, + "generationMode": null, + "description": null, + "body": "testing", + "title": null, + "linkPreviewSuppressed": null, + "cloneFailed": null, + "photoUrl": "", + "originalBody": "testing", + "createdBy": "9555737", + "imageUrl": null, + "uncompressedLinks": null, + "thumbUrl": null, + "fileId": null + }, + "linkGuid": null, + "messageUrl": null, + "foreignId": "1805309090157015220", + "taskQueueId": null, + "linkTaskQueueId": null, + "remoteContentId": null, + "remoteContentType": null, + "createdBy": 9555737, + "updatedBy": 9555737, + "campaignName": null, + "broadcastMediaType": "NONE", + "wasDraft": false, + "channel": null, + "serviceId": null, + "taggedLocation": null, + "targetLanguages": [], + "targetLocations": [], + "targeting": {}, + "targetLanguageLabels": [], + "targetLocationLabels": [], + "retweets": 0, + "likes": 0, + "replies": 0, + "clicks": 0, + "interactionsCount": 0, + "extraData": { + "files": [], + "body": null, + "updatedClientTag": null, + "nonFatalValidationErrorsAndMetadata": {}, + "playlistIds": [], + "tags": [], + "languageId": null, + "categoryId": null, + "privacyStatus": null, + "license": null, + "embeddable": true, + "notifySubscribers": true, + "madeForKids": false + }, + "foreignIdForBoost": "1805309090157015220", + "specialistSuggestionData": null, + "isPublished": true, + "isPending": false, + "isRetry": false, + "messageText": "testing", + "agentSuggestionData": null, + "isFailed": false +} \ No newline at end of file diff --git a/components/hubspot/sources/new-task/new-task.mjs b/components/hubspot/sources/new-task/new-task.mjs new file mode 100644 index 0000000000000..7c9244b4cfd51 --- /dev/null +++ b/components/hubspot/sources/new-task/new-task.mjs @@ -0,0 +1,56 @@ +import common from "../common/common.mjs"; +import { + DEFAULT_LIMIT, OBJECT_TYPES, +} from "../../common/constants.mjs"; + +export default { + ...common, + key: "hubspot-new-task", + name: "New Task Created", + description: "Emit new event for each new task created. [See the documentation](https://developers.hubspot.com/docs/reference/api/crm/engagements/tasks#get-%2Fcrm%2Fv3%2Fobjects%2Ftasks)", + version: "1.0.0", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTs(task) { + return Date.parse(task.createdAt); + }, + generateMeta(task) { + return { + id: task.id, + summary: `New Task: ${task.properties.hs_task_subject || task.id}`, + ts: this.getTs(task), + }; + }, + isRelevant(task, createdAfter) { + return this.getTs(task) > createdAfter; + }, + async getParams() { + const { results: allProperties } = await this.hubspot.getProperties({ + objectType: "tasks", + }); + const properties = allProperties.map(({ name }) => name); + + const objectTypes = OBJECT_TYPES.map(({ value }) => value); + const { results: custom } = await this.hubspot.listSchemas(); + const customObjects = custom?.map(({ fullyQualifiedName }) => fullyQualifiedName); + const associations = [ + ...objectTypes, + ...customObjects, + ]; + + return { + params: { + limit: DEFAULT_LIMIT, + properties: properties.join(","), + associations: associations.join(","), + }, + }; + }, + async processResults(after, params) { + const tasks = await this.getPaginatedItems(this.hubspot.listTasks.bind(this), params); + await this.processEvents(tasks, after); + }, + }, +}; diff --git a/components/hubspot/sources/new-ticket-property-change/new-ticket-property-change.mjs b/components/hubspot/sources/new-ticket-property-change/new-ticket-property-change.mjs index a21a6f2db2fbb..7bea8dad6bb9e 100644 --- a/components/hubspot/sources/new-ticket-property-change/new-ticket-property-change.mjs +++ b/components/hubspot/sources/new-ticket-property-change/new-ticket-property-change.mjs @@ -1,12 +1,13 @@ import common from "../common/common.mjs"; -import { API_PATH } from "../../common/constants.mjs"; +import { DEFAULT_LIMIT } from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "hubspot-new-ticket-property-change", name: "New Ticket Property Change", description: "Emit new event when a specified property is provided or updated on a ticket. [See the documentation](https://developers.hubspot.com/docs/api/crm/tickets)", - version: "0.0.5", + version: "0.0.13", dedupe: "unique", type: "source", props: { @@ -16,12 +17,11 @@ export default { label: "Property", description: "The ticket property to watch for changes", async options() { - const { results: properties } = await this.hubspot.getProperties("tickets"); + const properties = await this.getWriteOnlyProperties("tickets"); return properties.map((property) => property.name); }, }, }, - hooks: {}, methods: { ...common.methods, getTs(ticket) { @@ -49,78 +49,70 @@ export default { getParams(after) { return { object: "tickets", - limit: 50, - properties: [ - this.property, - ], - sorts: [ - { - propertyName: "hs_lastmodifieddate", - direction: "DESCENDING", - }, - ], - filterGroups: [ - { - filters: [ - { - propertyName: this.property, - operator: "HAS_PROPERTY", - }, - { - propertyName: "hs_lastmodifieddate", - operator: "GTE", - value: after, - }, - ], - }, - ], + data: { + limit: DEFAULT_LIMIT, + properties: [ + this.property, + ], + sorts: [ + { + propertyName: "hs_lastmodifieddate", + direction: "DESCENDING", + }, + ], + filterGroups: [ + { + filters: [ + { + propertyName: this.property, + operator: "HAS_PROPERTY", + }, + { + propertyName: "hs_lastmodifieddate", + operator: "GTE", + value: after, + }, + ], + }, + ], + }, }; }, - async batchGetTickets(inputs) { - return this.hubspot.makeRequest( - API_PATH.CRMV3, - "/objects/tickets/batch/read", - { - method: "POST", - data: { - properties: [ - this.property, - ], - propertiesWithHistory: [ - this.property, - ], - inputs, - }, + batchGetTickets(inputs) { + return this.hubspot.batchGetObjects({ + objectType: "tickets", + data: { + properties: [ + this.property, + ], + propertiesWithHistory: [ + this.property, + ], + inputs, }, - ); + }); }, async processResults(after, params) { - const { results: properties } = await this.hubspot.getProperties("tickets"); + const properties = await this.getWriteOnlyProperties("tickets"); const propertyNames = properties.map((property) => property.name); + if (!propertyNames.includes(this.property)) { throw new Error(`Property "${this.property}" not supported for Tickets. See Hubspot's default ticket properties documentation - https://knowledge.hubspot.com/tickets/hubspots-default-ticket-properties`); } const updatedTickets = await this.getPaginatedItems(this.hubspot.searchCRM, params); - const inputs = updatedTickets.map(({ id }) => ({ - id, - })); - // get tickets w/ `propertiesWithHistory` - const { results } = await this.batchGetTickets(inputs); - - let maxTs = after; - for (const result of results) { - if (this.isRelevant(result, after)) { - this.emitEvent(result); - const ts = this.getTs(result); - if (ts > maxTs) { - maxTs = ts; - } - } + if (!updatedTickets.length) { + return; } - this._setAfter(maxTs); + const results = await this.processChunks({ + batchRequestFn: this.batchGetTickets, + chunks: this.getChunks(updatedTickets), + }); + + this.processEvents(results, after); }, }, + sampleEmit, }; diff --git a/components/hubspot/sources/new-ticket-property-change/test-event.mjs b/components/hubspot/sources/new-ticket-property-change/test-event.mjs new file mode 100644 index 0000000000000..26883105b406f --- /dev/null +++ b/components/hubspot/sources/new-ticket-property-change/test-event.mjs @@ -0,0 +1,23 @@ +export default { + "id": "2863894640", + "properties": { + "createdate": "2024-06-20T21:01:29.961Z", + "hs_lastmodifieddate": "2024-06-24T18:48:27.770Z", + "hs_object_id": "2863894640", + "hs_ticket_priority": "LOW" + }, + "propertiesWithHistory": { + "hs_ticket_priority": [ + { + "value": "LOW", + "timestamp": "2024-06-24T18:48:27.770Z", + "sourceType": "CRM_UI", + "sourceId": "userId:9555737", + "updatedByUserId": 9555737 + } + ] + }, + "createdAt": "2024-06-20T21:01:29.961Z", + "updatedAt": "2024-06-24T18:48:27.770Z", + "archived": false +} \ No newline at end of file diff --git a/components/hubspot/sources/new-ticket/new-ticket.mjs b/components/hubspot/sources/new-ticket/new-ticket.mjs index 635a91db6ce10..40cf67a46e59f 100644 --- a/components/hubspot/sources/new-ticket/new-ticket.mjs +++ b/components/hubspot/sources/new-ticket/new-ticket.mjs @@ -1,14 +1,35 @@ import common from "../common/common.mjs"; +import { + DEFAULT_LIMIT, DEFAULT_TICKET_PROPERTIES, +} from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "hubspot-new-ticket", - name: "New Tickets", + name: "New Ticket", description: "Emit new event for each new ticket created.", - version: "0.0.12", + version: "0.0.19", dedupe: "unique", type: "source", - hooks: {}, + props: { + ...common.props, + info: { + type: "alert", + alertType: "info", + content: `Properties:\n\`${DEFAULT_TICKET_PROPERTIES.join(", ")}\``, + }, + properties: { + propDefinition: [ + common.props.hubspot, + "ticketProperties", + () => ({ + excludeDefaultProperties: true, + }), + ], + label: "Additional properties to retrieve", + }, + }, methods: { ...common.methods, getTs(ticket) { @@ -30,14 +51,21 @@ export default { return this.getTs(ticket) > createdAfter; }, getParams() { + const { properties = [] } = this; return { - limit: 100, - sorts: [ - { - propertyName: "createdate", - direction: "DESCENDING", - }, - ], + data: { + limit: DEFAULT_LIMIT, + sorts: [ + { + propertyName: "createdate", + direction: "DESCENDING", + }, + ], + properties: [ + ...DEFAULT_TICKET_PROPERTIES, + ...properties, + ], + }, object: "tickets", }; }, @@ -45,4 +73,5 @@ export default { await this.searchCRM(params, after); }, }, + sampleEmit, }; diff --git a/components/hubspot/sources/new-ticket/test-event.mjs b/components/hubspot/sources/new-ticket/test-event.mjs new file mode 100644 index 0000000000000..138422369e836 --- /dev/null +++ b/components/hubspot/sources/new-ticket/test-event.mjs @@ -0,0 +1,22 @@ +export default { + "id": "2863894640", + "properties": { + "content": null, + "createdate": "2024-06-20T21:01:29.961Z", + "hs_lastmodifieddate": "2024-06-20T21:01:31.821Z", + "hs_object_id": "2863894640", + "hs_pipeline": "0", + "hs_pipeline_stage": "1", + "hs_resolution": null, + "hs_ticket_category": null, + "hs_ticket_id": "2863894640", + "hs_ticket_priority": null, + "hubspot_owner_id": "41488296", + "hubspot_team_id": null, + "source_type": null, + "subject": "ticket" + }, + "createdAt": "2024-06-20T21:01:29.961Z", + "updatedAt": "2024-06-20T21:01:31.821Z", + "archived": false +} \ No newline at end of file diff --git a/components/hubspot/sources/product-updated/product-updated.mjs b/components/hubspot/sources/product-updated/product-updated.mjs deleted file mode 100644 index 639f56d899408..0000000000000 --- a/components/hubspot/sources/product-updated/product-updated.mjs +++ /dev/null @@ -1,48 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - key: "hubspot-product-updated", - name: "Product Updated", - description: "Emit new event each time a product is updated.", - version: "0.0.12", - dedupe: "unique", - type: "source", - hooks: {}, - methods: { - ...common.methods, - getTs(product) { - return Date.parse(product.updatedAt); - }, - generateMeta(product) { - const { - id, - properties, - } = product; - const ts = this.getTs(product); - return { - id: `${id}${ts}`, - summary: properties.name, - ts, - }; - }, - isRelevant(product, updatedAfter) { - return this.getTs(product) > updatedAfter; - }, - getParams() { - return { - limit: 100, - sorts: [ - { - propertyName: "hs_lastmodifieddate", - direction: "DESCENDING", - }, - ], - object: "products", - }; - }, - async processResults(after, params) { - await this.searchCRM(params, after); - }, - }, -}; diff --git a/components/hubspot/sources/updated-blog-article/updated-blog-article.mjs b/components/hubspot/sources/updated-blog-article/updated-blog-article.mjs deleted file mode 100644 index bc5bba9a975ff..0000000000000 --- a/components/hubspot/sources/updated-blog-article/updated-blog-article.mjs +++ /dev/null @@ -1,49 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - key: "hubspot-updated-blog-article", - name: "Updated Blog Posts", - description: "Emit new event for each updated blog post.", - version: "0.0.12", - dedupe: "unique", - type: "source", - hooks: {}, - methods: { - ...common.methods, - getTs(blogpost) { - return Date.parse(blogpost.updated); - }, - generateMeta(blogpost) { - const { - id, - name: summary, - updated, - created, - } = blogpost; - if (created != updated) { - const ts = Date.parse(blogpost.created); - return { - id: id + updated, - summary, - ts, - }; - } - }, - getParams(after) { - return { - limit: 100, - updated__gte: after, - sort: "-updatedAt", - }; - }, - async processResults(after, params) { - await this.paginate( - params, - this.hubspot.getBlogPosts.bind(this), - "results", - after, - ); - }, - }, -}; diff --git a/components/hubspot_developer_app/README.md b/components/hubspot_developer_app/README.md new file mode 100644 index 0000000000000..33fd11a909afd --- /dev/null +++ b/components/hubspot_developer_app/README.md @@ -0,0 +1,11 @@ +# Overview + +The HubSpot Developer API provides a wide array of endpoints to manage and automate HubSpot's marketing, sales, and service software features. Using Pipedream, you can connect this API to create custom, serverless workflows that trigger based on events in HubSpot, perform operations on your data, or sync data between HubSpot and other services. This unlocks potential for enhanced lead management, customer engagement tracking, and personalized content delivery, all while reducing manual workload. + +# Example Use Cases + +- **Sync New Contacts to a Google Sheet**: Automate the process of adding new HubSpot contacts to a Google Sheet. Each time a new contact is created in HubSpot, a Pipedream workflow triggers and appends the contact's details to your sheet, keeping your records up-to-date without manual entry. + +- **Enrich HubSpot Contacts with Clearbit**: When a new contact is added in HubSpot, trigger a Pipedream workflow that uses the Clearbit Enrichment API to gather additional data about the contact. This data is then used to update the contact's profile in HubSpot, providing richer insights for personalized marketing campaigns. + +- **Automate Slack Notifications for Deal Stage Updates**: Create a Pipedream workflow that monitors changes to deal stages in HubSpot. Whenever a deal moves to a critical stage, trigger a notification to a specified Slack channel, ensuring your sales team stays informed of deal progress in real-time. diff --git a/components/hubstaff/actions/create-task/create-task.mjs b/components/hubstaff/actions/create-task/create-task.mjs new file mode 100644 index 0000000000000..156a0ced63ad9 --- /dev/null +++ b/components/hubstaff/actions/create-task/create-task.mjs @@ -0,0 +1,64 @@ +import { ConfigurationError } from "@pipedream/platform"; +import hubstaff from "../../hubstaff.app.mjs"; + +export default { + key: "hubstaff-create-task", + name: "Create Task", + description: "Creates a new task on your Hubstaff organization. [See the documentation](https://developer.hubstaff.com/docs/hubstaff_v2#!/tasks/postV2ProjectsProjectIdTasks)", + version: "0.0.1", + type: "action", + props: { + hubstaff, + organizationId: { + propDefinition: [ + hubstaff, + "organizationId", + ], + }, + projectId: { + propDefinition: [ + hubstaff, + "projectId", + ({ organizationId }) => ({ + organizationId, + }), + ], + }, + summary: { + propDefinition: [ + hubstaff, + "summary", + ], + }, + assigneeId: { + propDefinition: [ + hubstaff, + "userIds", + ({ projectId }) => ({ + projectId, + }), + ], + type: "string", + label: "User ID", + description: "Assignee user ID for this task.", + }, + }, + async run({ $ }) { + try { + const response = await this.hubstaff.createTask({ + $, + projectId: this.projectId, + data: { + summary: this.summary, + assignee_id: this.assigneeId, + }, + }); + + $.export("$summary", `Successfully created task with ID ${response.task.id}`); + return response; + } catch ({ message }) { + const { error } = JSON.parse(message); + throw new ConfigurationError(error); + } + }, +}; diff --git a/components/hubstaff/actions/list-tasks/list-tasks.mjs b/components/hubstaff/actions/list-tasks/list-tasks.mjs new file mode 100644 index 0000000000000..54903b2b8aaaa --- /dev/null +++ b/components/hubstaff/actions/list-tasks/list-tasks.mjs @@ -0,0 +1,71 @@ +import { INCLUDE_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import hubstaff from "../../hubstaff.app.mjs"; + +export default { + key: "hubstaff-list-tasks", + name: "List Tasks", + description: "Retrieves a list of all tasks from your Hubstaff organization. [See the documentation](https://developer.hubstaff.com/docs/hubstaff_v2#!/tasks/getV2OrganizationsOrganizationIdTasks)", + version: "0.0.1", + type: "action", + props: { + hubstaff, + organizationId: { + propDefinition: [ + hubstaff, + "organizationId", + ], + }, + projectId: { + propDefinition: [ + hubstaff, + "projectId", + ({ organizationId }) => ({ + organizationId, + }), + ], + type: "string[]", + optional: true, + }, + status: { + propDefinition: [ + hubstaff, + "status", + ], + type: "string[]", + optional: true, + }, + userIds: { + propDefinition: [ + hubstaff, + "userIds", + (c) => ({ + organizationId: c.organizationId, + }), + ], + optional: true, + }, + include: { + type: "string[]", + label: "Include", + description: "Specify related data to side load.", + options: INCLUDE_OPTIONS, + optional: true, + }, + }, + async run({ $ }) { + const response = await this.hubstaff.listAllTasks({ + $, + organizationId: this.organizationId, + params: { + include: parseObject(this.include), + project_ids: parseObject(this.projectId), + user_ids: parseObject(this.userIds), + status: parseObject(this.status), + }, + }); + + $.export("$summary", `Successfully retrieved ${response.tasks.length} task(s)`); + return response; + }, +}; diff --git a/components/hubstaff/actions/update-task/update-task.mjs b/components/hubstaff/actions/update-task/update-task.mjs new file mode 100644 index 0000000000000..f30b15df3e695 --- /dev/null +++ b/components/hubstaff/actions/update-task/update-task.mjs @@ -0,0 +1,95 @@ +import { ConfigurationError } from "@pipedream/platform"; +import hubstaff from "../../hubstaff.app.mjs"; + +export default { + key: "hubstaff-update-task", + name: "Update Task", + description: "Update a specific task within your Hubstaff organization. [See the documentation](https://developer.hubstaff.com/docs/hubstaff_v2#!/tasks/putV2TasksTaskId)", + version: "0.0.1", + type: "action", + props: { + hubstaff, + organizationId: { + propDefinition: [ + hubstaff, + "organizationId", + ], + }, + projectId: { + propDefinition: [ + hubstaff, + "projectId", + ({ organizationId }) => ({ + organizationId, + }), + ], + }, + taskId: { + propDefinition: [ + hubstaff, + "taskId", + ({ + organizationId, projectId, + }) => ({ + organizationId, + projectId, + }), + ], + }, + summary: { + propDefinition: [ + hubstaff, + "summary", + ], + }, + assigneeId: { + propDefinition: [ + hubstaff, + "userIds", + ({ projectId }) => ({ + projectId, + }), + ], + type: "string", + label: "User ID", + description: "Assignee user ID for this task.", + }, + status: { + propDefinition: [ + hubstaff, + "status", + ], + options: [ + "active", + "completed", + ], + optional: true, + }, + lockVersion: { + type: "integer", + label: "Lock Version", + description: "The lock version from the task fetch in order to update.", + default: 0, + }, + }, + async run({ $ }) { + try { + const response = await this.hubstaff.updateTask({ + $, + taskId: this.taskId, + data: { + summary: this.summary, + lock_version: this.lockVersion, + status: this.status, + assignee_id: this.assigneeId, + }, + }); + + $.export("$summary", `Successfully updated task with ID ${this.taskId}`); + return response; + } catch ({ message }) { + const { error } = JSON.parse(message); + throw new ConfigurationError(error); + } + }, +}; diff --git a/components/hubstaff/common/constants.mjs b/components/hubstaff/common/constants.mjs new file mode 100644 index 0000000000000..f9b5f37866dd2 --- /dev/null +++ b/components/hubstaff/common/constants.mjs @@ -0,0 +1,41 @@ +export const INCLUDE_OPTIONS = [ + { + label: "Users", + value: "users", + }, + { + label: "Projects", + value: "projects", + }, +]; + +export const STATUS_OPTIONS = [ + { + value: "active", + label: "Active", + }, + { + value: "completed", + label: "Completed", + }, + { + value: "deleted", + label: "Deleted", + }, + { + value: "archived", + label: "Archived", + }, + { + value: "archived_native_active", + label: "Archived Native Active", + }, + { + value: "archived_native_completed", + label: "Archived Native Completed", + }, + { + value: "archived_native_deleted", + label: "Archived Native Deleted", + }, +]; diff --git a/components/hubstaff/common/utils.mjs b/components/hubstaff/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/hubstaff/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/hubstaff/hubstaff.app.mjs b/components/hubstaff/hubstaff.app.mjs new file mode 100644 index 0000000000000..891862b9d9d0f --- /dev/null +++ b/components/hubstaff/hubstaff.app.mjs @@ -0,0 +1,264 @@ +import { axios } from "@pipedream/platform"; +import { STATUS_OPTIONS } from "./common/constants.mjs"; + +export default { + type: "app", + app: "hubstaff", + propDefinitions: { + organizationId: { + type: "string", + label: "Organization ID", + description: "The ID of the organization", + async options({ prevContext }) { + const params = {}; + if (prevContext.nextToken) { + params.page_start_id = prevContext.nextToken; + } + const { + organizations, pagination, + } = await this.listOrganizations({ + params, + }); + return { + options: organizations.map(({ + name: label, id: value, + }) => ({ + label, + value, + })), + context: { + nextToken: pagination + ? pagination.next_page_start_id + : false, + }, + }; + }, + }, + projectId: { + type: "string", + label: "Project ID", + description: "The ID of the project", + async options({ + prevContext, organizationId, + }) { + const params = {}; + if (prevContext.nextToken) { + params.page_start_id = prevContext.nextToken; + } + const { + projects, pagination, + } = await this.listProjects({ + params, + organizationId, + }); + + return { + options: projects.map(({ + name: label, id: value, + }) => ({ + label, + value, + })), + context: { + nextToken: pagination + ? pagination.next_page_start_id + : false, + }, + }; + }, + }, + taskId: { + type: "string", + label: "Task ID", + description: "The ID of the task", + async options({ + prevContext, organizationId, projectId, + }) { + const params = {}; + if (prevContext.nextToken) { + params.page_start_id = prevContext.nextToken; + } + const { + tasks, pagination, + } = await this.listTasks({ + params, + organizationId, + projectId, + }); + + return { + options: tasks.map(({ + id: value, summary: label, + }) => ({ + label, + value, + })), + context: { + nextToken: pagination + ? pagination.next_page_start_id + : false, + }, + }; + }, + }, + userIds: { + type: "string[]", + label: "User IDs", + description: "List of user IDs", + async options({ + prevContext, organizationId, projectId, + }) { + const params = { + include: [ + "users", + ], + }; + if (prevContext.nextToken) { + params.page_start_id = prevContext.nextToken; + } + const { + members, users, pagination, + } = await this.listUsers({ + params, + organizationId, + projectId, + }); + + return { + options: members.map(({ user_id: value }) => ({ + label: users.find((user) => user.id === value).name, + value, + })), + context: { + nextToken: pagination + ? pagination.next_page_start_id + : false, + }, + }; + }, + }, + status: { + type: "string", + label: "Status", + description: "The status of the task", + options: STATUS_OPTIONS, + }, + summary: { + type: "string", + label: "Summary", + description: "A brief summary of the task", + }, + }, + methods: { + _baseUrl() { + return "https://api.hubstaff.com/v2"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listClients({ organizationId }) { + return this._makeRequest({ + path: `/organizations/${organizationId}/clients`, + }); + }, + listOrganizations() { + return this._makeRequest({ + path: "/organizations", + }); + }, + listProjects({ organizationId }) { + return this._makeRequest({ + path: `/organizations/${organizationId}/projects`, + }); + }, + listSchedules({ + organizationId, ...opts + }) { + return this._makeRequest({ + path: `/organizations/${organizationId}/attendance_schedules`, + ...opts, + }); + }, + listTasks({ projectId }) { + return this._makeRequest({ + path: `/projects/${projectId}/tasks`, + }); + }, + listUsers({ + organizationId, projectId, ...opts + }) { + return this._makeRequest({ + path: `${projectId + ? `/projects/${projectId}` + : `/organizations/${organizationId}`}/members`, + ...opts, + }); + }, + createTask({ + projectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/projects/${projectId}/tasks`, + ...opts, + }); + }, + updateTask({ + taskId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/tasks/${taskId}`, + ...opts, + }); + }, + listAllTasks({ + organizationId, ...opts + }) { + return this._makeRequest({ + path: `/organizations/${organizationId}/tasks`, + ...opts, + }); + }, + async *paginate({ + fn, params = {}, model, ...opts + }) { + let nextToken = false; + let page = 0; + + do { + params.page = ++page; + delete params.page_start_id; + if (nextToken) { + params.page_start_id = nextToken; + } + const { + pagination, [model]: data, + } = await fn({ + params, + ...opts, + }); + + for (const d of data) { + yield d; + } + + nextToken = pagination + ? pagination.next_page_start_id + : false; + + } while (nextToken); + }, + }, +}; diff --git a/components/hubstaff/package.json b/components/hubstaff/package.json new file mode 100644 index 0000000000000..097f6d98167bc --- /dev/null +++ b/components/hubstaff/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/hubstaff", + "version": "0.1.0", + "description": "Pipedream Hubstaff Components", + "main": "hubstaff.app.mjs", + "keywords": [ + "pipedream", + "hubstaff" + ], + "homepage": "https://pipedream.com/apps/hubstaff", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/hubstaff/sources/common/base.mjs b/components/hubstaff/sources/common/base.mjs new file mode 100644 index 0000000000000..812eff6aa72fb --- /dev/null +++ b/components/hubstaff/sources/common/base.mjs @@ -0,0 +1,74 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import hubstaff from "../../hubstaff.app.mjs"; + +export default { + props: { + hubstaff, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + organizationId: { + propDefinition: [ + hubstaff, + "organizationId", + ], + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + getParams() { + return {}; + }, + async emitEvent(maxResults = false) { + const lastId = this._getLastId(); + + const response = this.hubstaff.paginate({ + fn: this.getFunction(), + model: this.getModel(), + params: this.getParams(), + organizationId: this.organizationId, + }); + + let responseArray = []; + for await (const item of response) { + if (item.id > lastId) { + responseArray.push(item); + } + } + + responseArray.reverse(); + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastId(responseArray[0].id); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item.created_at), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/hubstaff/sources/new-client/new-client.mjs b/components/hubstaff/sources/new-client/new-client.mjs new file mode 100644 index 0000000000000..70f876312b259 --- /dev/null +++ b/components/hubstaff/sources/new-client/new-client.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "hubstaff-new-client", + name: "New Client Created", + description: "Emit new event when a new client is created in Hubstaff.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getModel() { + return "clients"; + }, + getFunction() { + return this.hubstaff.listClients; + }, + getSummary(item) { + return `New Client: ${item.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/hubstaff/sources/new-client/test-event.mjs b/components/hubstaff/sources/new-client/test-event.mjs new file mode 100644 index 0000000000000..8ce1080390b23 --- /dev/null +++ b/components/hubstaff/sources/new-client/test-event.mjs @@ -0,0 +1,33 @@ +export default { + "id": 0, + "organization_id": 0, + "name": "string", + "emails": [ + "string" + ], + "phone": "string", + "address": "string", + "project_ids": [ + 0 + ], + "inherit_invoice_notes": true, + "invoice_notes": "string", + "inherit_net_terms": true, + "net_terms": "string", + "status": "string", + "created_at": "2024-07-31T13:50:20.664Z", + "updated_at": "2024-07-31T13:50:20.664Z", + "budget": { + "type": "string", + "rate": "string", + "cost": 0, + "hours": 0, + "start_date": "2024-07-31", + "alerts": { + "near_limit": 0 + }, + "recurrence": "string", + "include_non_billable": true + }, + "metadata": {} +} \ No newline at end of file diff --git a/components/hubstaff/sources/new-schedule/new-schedule.mjs b/components/hubstaff/sources/new-schedule/new-schedule.mjs new file mode 100644 index 0000000000000..3f6204ec9a26b --- /dev/null +++ b/components/hubstaff/sources/new-schedule/new-schedule.mjs @@ -0,0 +1,31 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "hubstaff-new-schedule", + name: "New Schedule Created", + description: "Emit new event when a schedule is created in Hubstaff.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getModel() { + return "attendance_schedules"; + }, + getFunction() { + return this.hubstaff.listSchedules; + }, + getParams() { + return { + "date[start]": "1970-01-01", + "date[stop]": ((d) => new Date(d.getFullYear() + 1000, d.getMonth(), d.getDate()))(new Date), + }; + }, + getSummary(item) { + return `New Schedule: ${item.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/hubstaff/sources/new-schedule/test-event.mjs b/components/hubstaff/sources/new-schedule/test-event.mjs new file mode 100644 index 0000000000000..5112222e6003b --- /dev/null +++ b/components/hubstaff/sources/new-schedule/test-event.mjs @@ -0,0 +1,13 @@ +export default { + "id": 1234567, + "user_id": 1234567, + "organization_id": 1234567, + "created_at": "2024-08-01T14:56:16.366748Z", + "updated_at": "2024-08-01T14:56:16.366748Z", + "start_date": "2024-08-02", + "use_time_zone": "user", + "start_time": "09:00:00", + "duration": 28800, + "minimum_time": 27072, + "repeat_schedule": "never" +} \ No newline at end of file diff --git a/components/hugging_face/README.md b/components/hugging_face/README.md new file mode 100644 index 0000000000000..45eda360dc5f9 --- /dev/null +++ b/components/hugging_face/README.md @@ -0,0 +1,11 @@ +# Overview + +The Hugging Face API provides access to a vast range of machine learning models, primarily for natural language processing (NLP) tasks like text classification, translation, summarization, and question answering. It lets you leverage pre-trained models and fine-tune them on your data. Using the API within Pipedream, you can automate workflows that involve language processing, integrate AI insights into your apps, or respond to events with AI-generated content. + +# Example Use Cases + +- **Automated Content Summarization**: Ingest articles or long-form content from a webhook or RSS feed, use the Hugging Face API to summarize the content, and post the summaries to a Slack channel or a CMS. + +- **Language Translation Bot**: Create a workflow that listens for messages in a specific Slack channel, uses the Hugging Face API to translate the messages into a different language, and posts the translation back into the channel or forwards it to an international team. + +- **Customer Feedback Analysis**: Analyze customer feedback submitted via a form or helpdesk tool using the Hugging Face API's sentiment analysis model. Classify the feedback as positive, neutral, or negative, and log the results in a Google Sheet for easy tracking and visualization. diff --git a/components/hullo/actions/add-update-member/add-update-member.mjs b/components/hullo/actions/add-update-member/add-update-member.mjs new file mode 100644 index 0000000000000..58105ab8d6255 --- /dev/null +++ b/components/hullo/actions/add-update-member/add-update-member.mjs @@ -0,0 +1,102 @@ +import hullo from "../../hullo.app.mjs"; + +export default { + key: "hullo-add-update-member", + name: "Add or Update Member", + description: "Adds a new member or updates an existing member's data in Hullo. [See the documentation](https://app.hullo.me/docs/index.html#?route=post-/endpoints/members)", + version: "0.0.1", + type: "action", + props: { + hullo, + phoneNumber: { + propDefinition: [ + hullo, + "phoneNumber", + ], + }, + fullName: { + type: "string", + label: "Full Name", + description: "The full name of the member. REQUIRED if creating a new member.", + optional: true, + }, + registrationDate: { + type: "string", + label: "Registration Date", + description: "The date the member was registered in ISO-8601 format. Example: `2000-01-23T04:56:07.000+00:00`", + optional: true, + }, + groups: { + type: "string[]", + label: "Groups", + description: "An array containing the names of groups this member belongs to", + optional: true, + }, + attributes: { + propDefinition: [ + hullo, + "attributes", + ], + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (!this.attributes?.length) { + return props; + } + for (const attribute of this.attributes) { + props[attribute] = { + type: "string", + label: attribute, + description: `Value for ${attribute}`, + }; + } + return props; + }, + methods: { + async formatAttributes(attributeKeys, attributeValues) { + const attributes = await this.hullo.listAttributes(); + const formattedAttributes = {}; + for (const key of attributeKeys) { + const { type } = attributes.find(({ name }) => name === key); + const value = type === "NUMBER" + ? +attributeValues[key] + : type === "LIST" + ? JSON.parse(attributeValues[key]) + : attributeValues[key]; + formattedAttributes[key] = [ + value, + ]; + } + return formattedAttributes; + }, + }, + async run({ $ }) { + const { + hullo, + formatAttributes, + phoneNumber, + fullName, + registrationDate, + groups, + attributes, + ...attributeValues + } = this; + + const response = await hullo.addOrUpdateMember({ + $, + data: { + phoneNumber, + fullName, + registrationDate, + groups, + attributes: attributes?.length + ? await formatAttributes(attributes, attributeValues) + : undefined, + }, + }); + $.export("$summary", `Successfully added or updated member with phone number ${this.phoneNumber}`); + return response; + }, +}; diff --git a/components/hullo/actions/send-message/send-message.mjs b/components/hullo/actions/send-message/send-message.mjs new file mode 100644 index 0000000000000..80d04b153ee6c --- /dev/null +++ b/components/hullo/actions/send-message/send-message.mjs @@ -0,0 +1,34 @@ +import hullo from "../../hullo.app.mjs"; + +export default { + key: "hullo-send-message", + name: "Send Message", + description: "Sends a personalized message to a Hullo member. [See the documentation](https://app.hullo.me/docs/index.html#?route=post-/endpoints/messages)", + version: "0.0.1", + type: "action", + props: { + hullo, + phoneNumber: { + propDefinition: [ + hullo, + "phoneNumber", + ], + }, + messageText: { + type: "string", + label: "Message Text", + description: "The message text to send. Min length: 1, Max length: 640", + }, + }, + async run({ $ }) { + const response = await this.hullo.sendMessage({ + $, + data: { + phoneNumber: this.phoneNumber, + messageText: this.messageText, + }, + }); + $.export("$summary", `Successfully sent message to member with phone number: ${this.phoneNumber}`); + return response; + }, +}; diff --git a/components/hullo/hullo.app.mjs b/components/hullo/hullo.app.mjs new file mode 100644 index 0000000000000..2cdfbd108d208 --- /dev/null +++ b/components/hullo/hullo.app.mjs @@ -0,0 +1,62 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "hullo", + propDefinitions: { + attributes: { + type: "string[]", + label: "Attributes", + description: "The attributes that describe the member", + optional: true, + async options() { + const attributes = await this.listAttributes(); + return attributes?.map(({ name }) => name ) || []; + }, + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number of the member", + }, + }, + methods: { + _baseUrl() { + return "https://app.hullo.me/api/endpoints"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "X-API-KEY": `${this.$auth.api_key}`, + }, + }); + }, + listAttributes(opts = {}) { + return this._makeRequest({ + path: "/attributes", + ...opts, + }); + }, + sendMessage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/messages", + ...opts, + }); + }, + addOrUpdateMember(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/members", + ...opts, + }); + }, + }, +}; diff --git a/components/hullo/package.json b/components/hullo/package.json new file mode 100644 index 0000000000000..205eb5e0837bd --- /dev/null +++ b/components/hullo/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/hullo", + "version": "0.1.0", + "description": "Pipedream Hullo Components", + "main": "hullo.app.mjs", + "keywords": [ + "pipedream", + "hullo" + ], + "homepage": "https://pipedream.com/apps/hullo", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/humanitix/README.md b/components/humanitix/README.md index 50727dc5a68d1..06ee5ff181271 100644 --- a/components/humanitix/README.md +++ b/components/humanitix/README.md @@ -1,15 +1,11 @@ # Overview -With the Humanitix API, you can build applications that allow users to: +The Humanitix API provides a programmable interface to interact with the Humanitix event platform, enabling you to automate and integrate event management processes with other systems. With this API, you can retrieve event details, manage attendees, and handle bookings programmatically. When used on Pipedream, you can leverage serverless workflows to connect Humanitix with a myriad of other apps and services, streamlining tasks like attendee communication, marketing efforts, and data analysis. -- Book tickets to events -- Donate to causes -- Purchase products -- Register for courses +# Example Use Cases -Here are some examples of what you can build with the Humanitix API: +- **Automated Attendee Engagement**: After an attendee registers for an event on Humanitix, use Pipedream to trigger a workflow that sends a personalized welcome email via SendGrid, adds the attendee to a Mailchimp list for future updates, and posts their name and company to a Slack channel to acknowledge their registration internally. -- A ticketing system for events -- A donation platform for causes -- An ecommerce site for products -- A registration system for courses +- **Real-Time Event Performance Dashboard**: Set up a Pipedream workflow to pull event data from Humanitix regularly. Push this data to Google Sheets or a database like PostgreSQL. Use this data source to create a live dashboard with a tool like Google Data Studio, providing real-time insights into ticket sales and attendance figures. + +- **Post-Event Feedback Collection**: After an event concludes, trigger a workflow on Pipedream to send out a Typeform survey to all attendees for feedback. Collect responses and store them in a cloud storage service like AWS S3 or Google Drive, and then automatically summarize the feedback in a report using natural language processing (NLP) services and email it to the event organizers. diff --git a/components/humanlayer/humanlayer.app.mjs b/components/humanlayer/humanlayer.app.mjs new file mode 100644 index 0000000000000..97cb1840555ed --- /dev/null +++ b/components/humanlayer/humanlayer.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "humanlayer", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/humanlayer/package.json b/components/humanlayer/package.json new file mode 100644 index 0000000000000..774e75f0b852e --- /dev/null +++ b/components/humanlayer/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/humanlayer", + "version": "0.0.1", + "description": "Pipedream HumanLayer Components", + "main": "humanlayer.app.mjs", + "keywords": [ + "pipedream", + "humanlayer" + ], + "homepage": "https://pipedream.com/apps/humanlayer", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/humanloop/README.md b/components/humanloop/README.md new file mode 100644 index 0000000000000..249c16ba61a12 --- /dev/null +++ b/components/humanloop/README.md @@ -0,0 +1,11 @@ +# Overview + +The HumanLoop API provides a robust platform for incorporating AI and machine learning model feedback loops into applications, enabling continuous improvement of models based on human input. With Pipedream's capabilities, you can trigger workflows upon receiving data, process and analyze that data, and send it to HumanLoop to further train your AI models. This integration allows you to automate the data annotation process, handle user feedback, and improve your machine learning models over time. + +# Example Use Cases + +- **Automated Data Annotation Pipeline**: Create a workflow in Pipedream that ingests data from various sources (like forms, emails, or databases), processes it to determine if it needs annotation, and sends it to HumanLoop. Once annotated, the data can be sent back to your ML model for retraining, all within an automated, serverless environment. + +- **Customer Feedback Analysis**: Connect your customer support platform to Pipedream, where you can funnel customer feedback and inquiries to HumanLoop. Use HumanLoop's interface for annotating and classifying the feedback, which can then inform business decisions, improve customer support bots, or guide product development. + +- **Sentiment Analysis Improvement**: Integrate HumanLoop with a social media listening tool via Pipedream to analyze sentiment on posts related to your brand. The workflow can filter and send ambiguous cases to HumanLoop for review and annotation, which in turn refines the sentiment analysis algorithm for more accurate future detections. diff --git a/components/humor_api/README.md b/components/humor_api/README.md new file mode 100644 index 0000000000000..8852f8efd41b6 --- /dev/null +++ b/components/humor_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Humor API is a tool that offers a variety of endpoints to add some laughter to your applications. Integrated into Pipedream, you can harness this API to create custom workflows that trigger actions across various services, responding to events with a light-hearted touch. Use cases may include sending jokes as icebreakers in emails, posting humorous content to social media, or integrating fun prompts into chatbots. + +# Example Use Cases + +- **Automated Joke Emails**: Set up a workflow that triggers daily, using the Humor API to fetch a random joke which is then sent to a mailing list with the help of the Gmail app. It's a fun way to start the day and engage with your audience. + +- **Social Media Humor Scheduling**: Create a Pipedream workflow that pulls the funniest jokes or memes using the Humor API and schedules them for posting on social platforms like Twitter or Facebook, adding a consistent stream of humor to your social media presence. + +- **Chatbot Humor Integration**: Enhance a chatbot on platforms like Slack or Discord by integrating the Humor API. Use Pipedream to fetch a joke or a funny quote when specific commands are used, injecting a dose of levity into conversations. diff --git a/components/hunter/README.md b/components/hunter/README.md index fc53ff53c9c1c..0d3eda6f855c6 100644 --- a/components/hunter/README.md +++ b/components/hunter/README.md @@ -1,9 +1,9 @@ # Overview -With the Hunter API, you can build applications that: +The Hunter API on Pipedream provides a potent suite for automating email discovery and verification processes. By interfacing with Hunter, you can programmatically find email addresses associated with a domain, verify the deliverability of emails, and retrieve other data points like the organization behind an address. This capability is invaluable for lead generation, sales outreach, and data enrichment workflows, where accurate contact information is crucial. -- Search for email addresses associated with a domain name -- Verify the accuracy of an email address -- Find out who owns a specific email address -- Get detailed information about a specific email address -- Generate a list of all email addresses associated with a domain name +# Example Use Cases + +- **Lead Generation Automation**: Set up a workflow that triggers whenever your company's database adds a new potential client's domain. The Hunter API fetches the most common email pattern for that domain, and perhaps even specific email addresses. Integrate with CRM platforms like HubSpot or Salesforce on Pipedream to automatically populate new leads with this data. +- **Email Verification Before Campaigns**: Before launching an email marketing campaign, ensure email lists have high deliverability. Create a workflow that takes a list of emails, uses Hunter to verify each, and filters out undeliverable addresses. This can tie in with email marketing tools like Mailchimp on Pipedream, automatically updating subscriber lists. +- **Enriching Contact Details in Support Tickets**: When a new support ticket is created, a workflow can call the Hunter API to find additional contact information about the person who submitted the ticket. This data can then be pushed to a helpdesk system such as Zendesk, providing support staff with valuable context and potential contact alternatives. diff --git a/components/hunter/package.json b/components/hunter/package.json new file mode 100644 index 0000000000000..0e80b6e334cbc --- /dev/null +++ b/components/hunter/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/hunter", + "version": "0.6.0", + "description": "Pipedream hunter Components", + "main": "hunter.app.mjs", + "keywords": [ + "pipedream", + "hunter" + ], + "homepage": "https://pipedream.com/apps/hunter", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/hygraph/README.md b/components/hygraph/README.md new file mode 100644 index 0000000000000..5c50a680c62de --- /dev/null +++ b/components/hygraph/README.md @@ -0,0 +1,11 @@ +# Overview + +Hygraph API allows you to interact with a headless content management system that lets you manage and deliver content across multiple platforms. Leveraging this API on Pipedream, you can automate content creation, update workflows, and synchronize data across various services. You can trigger workflows on content changes, enrich the content with external APIs, or even automate content moderation. + +# Example Use Cases + +- **Content Sync with External Storage**: Automatically export new content items from Hygraph to an external storage service like Amazon S3 whenever they are published, ensuring backups and enabling easy distribution. + +- **Automated Content Approvals**: Set up a workflow that listens for new content submissions on Hygraph, runs them through a sentiment analysis service to flag negative content, and auto-approves or escalates items based on the results. + +- **Multi-Platform Publishing**: When content is updated in Hygraph, use Pipedream to push that content to other platforms like Shopify for product information, or to WordPress for blog posts, keeping all platforms synchronized with the latest updates. diff --git a/components/hypeauditor/README.md b/components/hypeauditor/README.md new file mode 100644 index 0000000000000..962d22c5b263c --- /dev/null +++ b/components/hypeauditor/README.md @@ -0,0 +1,11 @@ +# Overview + +The HypeAuditor API provides deep insights into social media account analytics and influencer effectiveness, making it a powerful tool for marketers who want to optimize their influencer campaigns. Using this API, you can access detailed data concerning influencer audience demographics, engagement rates, authenticity, and more. This can aid in identifying the right influencers for your brand, tracking campaign performance, and refining your marketing strategies over time. + +# Example Use Cases + +- **Influencer Vetting Automation**: Automate the process of selecting influencers by setting up a workflow that triggers whenever a new influencer is added to your campaign list in Google Sheets. The workflow fetches data from the HypeAuditor API to analyze the influencer’s audience quality and engagement rates, and then logs this data back in Google Sheets or sends a summary report via email using Gmail. This helps in making informed decisions quickly and efficiently. + +- **Campaign Performance Dashboard**: Create a real-time dashboard using tools like Google Data Studio or Tableau. Set up a Pipedream workflow that periodically collects data from the HypeAuditor API about your active influencers' performance metrics. Integrate this data with your existing marketing data in Google BigQuery or a similar data warehouse to visualize overall campaign effectiveness and adjust strategies as needed. + +- **Alert System for Influencer Activity Changes**: Monitor significant changes in influencer metrics such as follower count or engagement rate. Utilize a Pipedream workflow that checks daily for updates via the HypeAuditor API and compares it against a threshold set by you. If changes exceed this threshold, automatically send alerts through Slack or email. This can help you respond swiftly to potential issues like declining engagement or sudden spikes in follower counts, which could indicate fraudulent activity. diff --git a/components/hypeauditor/actions/get-instagram-report/get-instagram-report.mjs b/components/hypeauditor/actions/get-instagram-report/get-instagram-report.mjs new file mode 100644 index 0000000000000..d113b14e1f6b8 --- /dev/null +++ b/components/hypeauditor/actions/get-instagram-report/get-instagram-report.mjs @@ -0,0 +1,44 @@ +import app from "../../hypeauditor.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "hypeauditor-get-instagram-report", + name: "Get Instagram Report", + description: "Returns a report about the specified Instagram user. [See the documentation](https://hypeauditor.readme.io/reference/report_instagram#requests)", + version: "0.0.2", + type: "action", + props: { + app, + username: { + propDefinition: [ + app, + "username", + ], + }, + userId: { + propDefinition: [ + app, + "userId", + ], + }, + }, + + async run({ $ }) { + if (!this.userId && !this.username) { + throw new ConfigurationError("You need to provide either a User ID or Username"); + } + + const response = await this.app.getInstagramReport({ + $, + params: { + username: this.userId ?? this.username, + v: "2", + }, + + }); + + $.export("$summary", `Successfully sent the request. Report state: '${response.result.report_state}'`); + + return response; + }, +}; diff --git a/components/hypeauditor/actions/get-tiktok-report/get-tiktok-report.mjs b/components/hypeauditor/actions/get-tiktok-report/get-tiktok-report.mjs new file mode 100644 index 0000000000000..fe28793af4966 --- /dev/null +++ b/components/hypeauditor/actions/get-tiktok-report/get-tiktok-report.mjs @@ -0,0 +1,31 @@ +import app from "../../hypeauditor.app.mjs"; + +export default { + key: "hypeauditor-get-tiktok-report", + name: "Get Tiktok Report", + description: "Returns a report about the specified Tiktok channel. [See the documentation](https://hypeauditor.readme.io/reference/get_report_tiktok)", + version: "0.0.2", + type: "action", + props: { + app, + channelUsername: { + propDefinition: [ + app, + "channelUsername", + ], + optional: false, + }, + }, + + async run({ $ }) { + const response = await this.app.getTiktokReport({ + $, + data: { + channel: this.channelUsername, + }, + }); + $.export("$summary", `Successfully sent the request. Report state: '${response.result.report_state}'`); + + return response; + }, +}; diff --git a/components/hypeauditor/actions/get-twitch-report/get-twitch-report.mjs b/components/hypeauditor/actions/get-twitch-report/get-twitch-report.mjs new file mode 100644 index 0000000000000..4707447ce6820 --- /dev/null +++ b/components/hypeauditor/actions/get-twitch-report/get-twitch-report.mjs @@ -0,0 +1,29 @@ +import hypeauditor from "../../hypeauditor.app.mjs"; + +export default { + key: "hypeauditor-get-twitch-report", + name: "Get Twitch Report", + description: "Generates a Twitch report for a specified channel. [See the documentation](https://hypeauditor.readme.io/reference/report_twitch)", + version: "0.0.1", + type: "action", + props: { + hypeauditor, + channel: { + propDefinition: [ + hypeauditor, + "twitchChannel", + ], + }, + }, + async run({ $ }) { + const { + hypeauditor, ...params + } = this; + const response = await hypeauditor.getTwitchReport({ + $, + params, + }); + $.export("$summary", `Successfully fetched Twitch report for channel ${this.channel}`); + return response; + }, +}; diff --git a/components/hypeauditor/actions/get-youtube-report/get-youtube-report.mjs b/components/hypeauditor/actions/get-youtube-report/get-youtube-report.mjs new file mode 100644 index 0000000000000..b972858db0ee8 --- /dev/null +++ b/components/hypeauditor/actions/get-youtube-report/get-youtube-report.mjs @@ -0,0 +1,42 @@ +import app from "../../hypeauditor.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "hypeauditor-get-youtube-report", + name: "Get Youtube Report", + description: "Returns a report about the specified Youtube channel. [See the documentation](https://hypeauditor.readme.io/reference/report_youtube)", + version: "0.0.2", + type: "action", + props: { + app, + channelId: { + propDefinition: [ + app, + "channelId", + ], + }, + channelUsername: { + propDefinition: [ + app, + "channelUsername", + ], + }, + }, + + async run({ $ }) { + if (!this.channelId && !this.channelUsername) { + throw new ConfigurationError("You need to inform a Channel ID or Channel Username"); + } + + const response = await this.app.getYoutubeReport({ + $, + data: { + channel: this.channelId ?? this.channelUsername, + }, + }); + + $.export("$summary", `Successfully sent the request. Report state: '${response.result.report_state}'`); + + return response; + }, +}; diff --git a/components/hypeauditor/hypeauditor.app.mjs b/components/hypeauditor/hypeauditor.app.mjs new file mode 100644 index 0000000000000..a1c4a6df258be --- /dev/null +++ b/components/hypeauditor/hypeauditor.app.mjs @@ -0,0 +1,88 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "hypeauditor", + propDefinitions: { + twitchChannel: { + type: "string", + label: "Twitch Channel Username", + description: "The Twitch username (e.g. `nasa`) from a Twitch channel URL (in this example, `https://www.twitch.tv/nasa`).", + }, + username: { + type: "string", + label: "Username", + description: "Username to request the report", + optional: true, + }, + userId: { + type: "string", + label: "User ID", + description: "User ID to request the report", + optional: true, + }, + channelUsername: { + type: "string", + label: "Channel Username", + description: "Identify the user by their Channel Username", + optional: true, + }, + channelId: { + type: "string", + label: "Channel ID", + description: "Identify the user by their Channel ID", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://hypeauditor.com/api/method"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "content-type": "application/x-www-form-urlencoded", + "x-auth-id": `${this.$auth.client_id}`, + "x-auth-token": `${this.$auth.api_token}`, + "user-agent": "pipedream/1", + }, + }); + }, + async getInstagramReport(args = {}) { + return this._makeRequest({ + path: "/auditor.report/", + method: "post", + ...args, + }); + }, + async getYoutubeReport(args = {}) { + return this._makeRequest({ + path: "/auditor.youtube/", + method: "post", + ...args, + }); + }, + async getTiktokReport(args = {}) { + return this._makeRequest({ + path: "/auditor.tiktok", + method: "post", + ...args, + }); + }, + async getTwitchReport(opts = {}) { + return this._makeRequest({ + path: "/auditor.twitch/", + ...opts, + }); + }, + }, +}; diff --git a/components/hypeauditor/package.json b/components/hypeauditor/package.json new file mode 100644 index 0000000000000..f74084cbdc14b --- /dev/null +++ b/components/hypeauditor/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/hypeauditor", + "version": "0.2.0", + "description": "Pipedream HypeAuditor Components", + "main": "hypeauditor.app.mjs", + "keywords": [ + "pipedream", + "hypeauditor" + ], + "homepage": "https://pipedream.com/apps/hypeauditor", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/hyperise/README.md b/components/hyperise/README.md new file mode 100644 index 0000000000000..fa087ea365fd6 --- /dev/null +++ b/components/hyperise/README.md @@ -0,0 +1,11 @@ +# Overview + +The Hyperise API lets you dynamically personalize images with user-specific data, making it easier to create tailored visual content for each recipient. With Pipedream, you can automate this personalization process by integrating Hyperise with various apps to generate and share custom images on the fly. This could be particularly useful for marketing campaigns, sales outreach, or social media engagement where personalized content significantly boosts user interaction. + +# Example Use Cases + +- **Automated Personalized Email Campaigns**: Integrate Hyperise with an email service like SendGrid on Pipedream. Automatically generate personalized images for each subscriber and include them in targeted email campaigns to enhance engagement and conversion rates. + +- **Dynamic Social Media Posts**: Connect Hyperise with a social media platform like Twitter. Use Pipedream to personalize images with user data and post them to your social media account, providing a unique experience that can increase follower interaction and brand recognition. + +- **Custom Analytics Dashboard**: Combine Hyperise with a data visualization tool like Google Sheets. Use Pipedream to fetch analytics data, create personalized images reflecting individual user metrics, and update a dashboard that provides users with a visual and personalized summary of their data. diff --git a/components/hyperise/actions/create-personalised-short-link/create-personalised-short-link.mjs b/components/hyperise/actions/create-personalised-short-link/create-personalised-short-link.mjs new file mode 100644 index 0000000000000..f4a88243e0bc7 --- /dev/null +++ b/components/hyperise/actions/create-personalised-short-link/create-personalised-short-link.mjs @@ -0,0 +1,90 @@ +import hyperise from "../../hyperise.app.mjs"; + +export default { + key: "hyperise-create-personalised-short-link", + name: "Create Personalised Short Link", + description: "Creates a personalised short URL from provided inputs. [See the documentation](https://support.hyperise.com/en/api/Personalised-Short-Links-API)", + version: "0.0.1", + type: "action", + props: { + hyperise, + imageTemplateHash: { + propDefinition: [ + hyperise, + "imageTemplateHash", + ], + }, + destinationUrl: { + type: "string", + label: "Destination URL", + description: "The URL to create a personalised short URL for", + }, + pageTitle: { + type: "string", + label: "Page Title", + description: "Title of the page", + }, + pageDescription: { + type: "string", + label: "Page Description", + description: "Description of the page", + }, + firstName: { + type: "string", + label: "First Name", + description: "Personalisation Data - first name", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Personalisation Data", + optional: true, + }, + profileUrl: { + type: "string", + label: "Profile URL", + description: "Personalisation Data - Image URL of the prospects profile image", + optional: true, + }, + jobTitle: { + type: "string", + label: "Job Title", + description: "Personalisation Data - job title", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "Personalisation Data - email address", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Personalisation Data - phone number", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.hyperise.createPersonalisedShortUrl({ + $, + data: { + image_hash: this.imageTemplateHash, + url: this.destinationUrl, + title: this.pageTitle, + desc: this.pageDescription, + query_params: { + first_name: this.firstName, + last_name: this.lastName, + profile_url: this.profileUrl, + job_title: this.jobTitle, + email: this.email, + phone: this.phone, + }, + }, + }); + $.export("$summary", `Successfully created personalised short URL: ${response.link}`); + return response; + }, +}; diff --git a/components/hyperise/hyperise.app.mjs b/components/hyperise/hyperise.app.mjs index 30ec3b2793a11..cc92a287b7959 100644 --- a/components/hyperise/hyperise.app.mjs +++ b/components/hyperise/hyperise.app.mjs @@ -1,11 +1,70 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "hyperise", - propDefinitions: {}, + propDefinitions: { + imageTemplateHash: { + type: "string", + label: "Image Template Hash", + description: "The image hash of the template to use", + async options() { + const templates = await this.listTemplates(); + return templates?.map(({ + image_hash: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://app.hyperise.io/api/v1/regular"; + }, + _authParams(params) { + return { + ...params, + api_token: `${this.$auth.api_token}`, + }; + }, + _makeRequest({ + $ = this, + path, + params, + data, + ...otherOpts + }) { + const config = { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + }; + if (data) { + config.data = this._authParams(data); + } else { + config.params = this._authParams(params); + } + return axios($, config); + }, + listTemplates(opts = {}) { + return this._makeRequest({ + path: "/image-templates", + ...opts, + }); + }, + getImageViews(opts = {}) { + return this._makeRequest({ + path: "/image-impressions", + ...opts, + }); + }, + createPersonalisedShortUrl(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/short-links", + ...opts, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/hyperise/package.json b/components/hyperise/package.json index b7ef73d23c8f1..08f79b5147596 100644 --- a/components/hyperise/package.json +++ b/components/hyperise/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/hyperise", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Hyperise Components", "main": "hyperise.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" } -} \ No newline at end of file +} diff --git a/components/hyperise/sources/new-image-impression/new-image-impression.mjs b/components/hyperise/sources/new-image-impression/new-image-impression.mjs new file mode 100644 index 0000000000000..851f87a54bb0c --- /dev/null +++ b/components/hyperise/sources/new-image-impression/new-image-impression.mjs @@ -0,0 +1,74 @@ +import hyperise from "../../hyperise.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "hyperise-new-image-impression", + name: "New Image Impression", + description: "Emit new event when a new personalised image is viewed. [See the documentation](https://support.hyperise.com/en/api/Image-Views-API)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + hyperise, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + imageTemplateHash: { + propDefinition: [ + hyperise, + "imageTemplateHash", + ], + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || this.oneDayAgo(); + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + oneDayAgo() { + return new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString() + .slice(0, 19) + "Z"; + }, + emitEvent(data) { + const meta = this.generateMeta(data); + this.$emit(data, meta); + }, + generateMeta(data) { + return { + id: data.id, + summary: `New View of Image: ${data.image_name}`, + ts: Date.parse(data.processed_at), + }; + }, + }, + async run() { + const lastDate = this._getLastDate(); + let maxDate = lastDate; + const impressions = []; + const results = await this.hyperise.getImageViews({ + params: { + image_hash: this.imageTemplateHash, + date_from: lastDate, + }, + }); + for (const impression of results) { + const ts = Date.parse(impression.processed_at); + if (ts > Date.parse(lastDate)) { + impressions.push(impression); + if (ts > Date.parse(maxDate)) { + maxDate = impression.processed_at; + } + } + } + impressions.reverse().forEach((impression) => this.emitEvent(impression)); + this._setLastDate(maxDate); + }, + sampleEmit, +}; diff --git a/components/hyperise/sources/new-image-impression/test-event.mjs b/components/hyperise/sources/new-image-impression/test-event.mjs new file mode 100644 index 0000000000000..d0356271207aa --- /dev/null +++ b/components/hyperise/sources/new-image-impression/test-event.mjs @@ -0,0 +1,43 @@ +export default { + "id": 24298436, + "business_name": "", + "first_name": "", + "image_name": "My new image", + "image_url": "https://img.hyperise.io/i/sW140JarK.png?time=1710274146651&editor_preview=1", + "enrichment_data": { + "email": "", + "title": "", + "first_name": "", + "last_name": "", + "gender": "", + "job_title": "", + "profile_url": "", + "business_name": "", + "category": "", + "business_address": "", + "street": "", + "street2": "", + "city": "", + "state": "", + "zip": "", + "country": "", + "revenue": "", + "employees_range": "", + "business_lat": 52.951214, + "business_long": -1.1461797, + "business_phone": "", + "business_website": "", + "website_screenshot": "", + "logo_url": "", + "custom_image_1": false, + "custom_image_2": false, + "custom_image_3": false, + "custom_text_1": "", + "custom_text_2": "", + "custom_text_3": "", + "custom_text_4": "", + "custom_text_5": "", + "qr_url": "" + }, + "processed_at": "2024-03-12T20:12:28Z" +} \ No newline at end of file diff --git a/components/hyros/README.md b/components/hyros/README.md new file mode 100644 index 0000000000000..9dceab0fe8508 --- /dev/null +++ b/components/hyros/README.md @@ -0,0 +1,11 @@ +# Overview + +The Hyros API offers the ability to track and analyze customer interactions across various marketing channels, aiming to improve ad tracking and ROI. Integrated within Pipedream, it allows for the automation of data collection, real-time analytics, and operational workflows. By leveraging Pipedream's serverless platform, developers can create workflows that respond to events from Hyros, trigger actions based on data insights, or synchronize data with other apps seamlessly. + +# Example Use Cases + +- **Lead Scoring and Segmentation**: Use Hyros to monitor customer interactions and score leads based on engagement. In Pipedream, create a workflow that updates a CRM like Salesforce or HubSpot with scored and segmented leads for personalized follow-up campaigns. + +- **Real-time Alerting for High-Value Activities**: Set up a Pipedream workflow that listens for events via the Hyros API, identifying high-value customer actions. Trigger alerts or tasks in collaboration tools like Slack or Asana when a customer performs a key action, ensuring your team responds swiftly. + +- **Automated Ad Spend Adjustment**: Implement a Pipedream workflow that utilizes Hyros data to analyze the performance of ad campaigns. Connect with advertising platforms like Google Ads or Facebook Ads to automatically adjust spend based on real-time conversion data received from Hyros. diff --git a/components/hystruct/hystruct.app.mjs b/components/hystruct/hystruct.app.mjs new file mode 100644 index 0000000000000..7528d14b59089 --- /dev/null +++ b/components/hystruct/hystruct.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "hystruct", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/hystruct/package.json b/components/hystruct/package.json new file mode 100644 index 0000000000000..83eda02d3bad1 --- /dev/null +++ b/components/hystruct/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/hystruct", + "version": "0.0.1", + "description": "Pipedream Hystruct Components", + "main": "hystruct.app.mjs", + "keywords": [ + "pipedream", + "hystruct" + ], + "homepage": "https://pipedream.com/apps/hystruct", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/iauditor_by_safetyculture/README.md b/components/iauditor_by_safetyculture/README.md new file mode 100644 index 0000000000000..1cdbb48cf1380 --- /dev/null +++ b/components/iauditor_by_safetyculture/README.md @@ -0,0 +1,11 @@ +# Overview + +iAuditor by SafetyCulture API allows you to tap into a rich reservoir of safety and quality inspection data, enabling you to automate workflows around inspection management. With this API on Pipedream, you can create custom integrations to trigger actions based on audit completions, new issues, or updates in iAuditor. Streamline safety processes, connect with other tools, and manipulate inspection data to fit your needs. + +# Example Use Cases + +- **Automate Issue Tracking**: Integrate iAuditor with a project management tool like Jira on Pipedream. When an audit identifies a new issue, automatically create a Jira ticket, ensuring prompt attention from the responsible teams. + +- **Sync Inspection Data to a Database**: Set up a workflow that triggers when an audit is completed. The workflow processes the results and syncs them to a cloud database like Google Sheets or AWS DynamoDB for advanced reporting and analysis. + +- **Real-Time Notifications for Audits**: Connect iAuditor to communication platforms such as Slack or Microsoft Teams. When an audit is completed or an issue is flagged, send an instant notification to a designated channel, keeping the team informed and responsive. diff --git a/components/ibm_cloud_natural_language_understanding/ibm_cloud_natural_language_understanding.app.mjs b/components/ibm_cloud_natural_language_understanding/ibm_cloud_natural_language_understanding.app.mjs new file mode 100644 index 0000000000000..9b6cccb21bb6f --- /dev/null +++ b/components/ibm_cloud_natural_language_understanding/ibm_cloud_natural_language_understanding.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "ibm_cloud_natural_language_understanding", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/ibm_cloud_natural_language_understanding/package.json b/components/ibm_cloud_natural_language_understanding/package.json new file mode 100644 index 0000000000000..3dfbb26960c47 --- /dev/null +++ b/components/ibm_cloud_natural_language_understanding/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/ibm_cloud_natural_language_understanding", + "version": "0.0.1", + "description": "Pipedream IBM Cloud - Natural Language Understanding Components", + "main": "ibm_cloud_natural_language_understanding.app.mjs", + "keywords": [ + "pipedream", + "ibm_cloud_natural_language_understanding" + ], + "homepage": "https://pipedream.com/apps/ibm_cloud_natural_language_understanding", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/ibm_cloud_speech_to_text/README.md b/components/ibm_cloud_speech_to_text/README.md index 571ec38d0f269..12acedc2d18cc 100644 --- a/components/ibm_cloud_speech_to_text/README.md +++ b/components/ibm_cloud_speech_to_text/README.md @@ -1,11 +1,11 @@ # Overview -The IBM Cloud Speech to Text API can be used to transcribe audio files in a -variety of different languages. This can be useful for a number of different -applications, such as: - -- Transcribing audio files for transcriptions -- Transcribing audio files for subtitles -- Transcribing audio files for meeting minutes -- Transcribing audio files for legal documents -- Etc. +The IBM Cloud - Speech to Text API transforms spoken language into written text, offering a powerful tool for creating transcriptions, enabling voice control and command features, and feeding speech into analytics platforms. With Pipedream, you can build automated workflows that leverage this capability, such as transcribing meetings in real-time, analyzing customer service calls for sentiment and keywords, or even creating subtitles for videos. The ability to connect with other apps on Pipedream allows for complex workflows that can turn spoken data into actionable insights or accessible content. + +# Example Use Cases + +- **Customer Support Call Analysis**: Use IBM Cloud - Speech to Text to transcribe customer support calls. Feed the transcriptions into a sentiment analysis API, like the one offered by Google Cloud Natural Language, to gauge customer satisfaction and identify pain points in your service. + +- **Meeting Minutes Automation**: Directly transcribe online meeting audio using IBM Cloud - Speech to Text, then use the Pipedream platform to filter, format, and send the text to a document storage app like Google Docs, automatically creating shareable meeting minutes. + +- **Content Creation for Accessibility**: Transcribe podcast episodes or YouTube video speech to create written content. With IBM Cloud - Speech to Text via Pipedream, automatically post the transcriptions to your website or content management system (CMS), improving SEO and making the content accessible to a wider audience, including those with hearing impairments. diff --git a/components/ibm_cloud_speech_to_text/package.json b/components/ibm_cloud_speech_to_text/package.json new file mode 100644 index 0000000000000..259c5519b5933 --- /dev/null +++ b/components/ibm_cloud_speech_to_text/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/ibm_cloud_speech_to_text", + "version": "0.6.0", + "description": "Pipedream ibm_cloud_speech_to_text Components", + "main": "ibm_cloud_speech_to_text.app.mjs", + "keywords": [ + "pipedream", + "ibm_cloud_speech_to_text" + ], + "homepage": "https://pipedream.com/apps/ibm_cloud_speech_to_text", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/ibm_x_force_exchange/README.md b/components/ibm_x_force_exchange/README.md new file mode 100644 index 0000000000000..90f8ced37a415 --- /dev/null +++ b/components/ibm_x_force_exchange/README.md @@ -0,0 +1,11 @@ +# Overview + +The IBM X-Force Exchange API offers a comprehensive threat intelligence database, allowing users to access risk scores, reports, and historical data on various threats. With Pipedream, you can automate workflows involving threat analysis, monitoring, and response. By leveraging its capabilities, you can streamline security operations, such as fetching threat intelligence, updating threat databases, and alerting based on specific indicators of compromise (IOCs). + +# Example Use Cases + +- **Automated Threat Intelligence Gathering**: Connect the IBM X-Force Exchange API to Pipedream to regularly pull threat intelligence. Set up a schedule to automatically fetch the latest threat reports, and use Pipedream's built-in key-value store to track changes or updates in threat data. + +- **Real-Time Security Alerts**: Use the API to monitor for specific IOCs. When the API returns data matching your criteria, trigger an alert workflow on Pipedream that sends notifications via Slack, email, or SMS, keeping your team informed about potential threats instantly. + +- **Incident Response Coordination**: Integrate IBM X-Force Exchange with a ticketing system like Zendesk on Pipedream. When new threats are detected, automatically create tickets to ensure your security team prioritizes and responds to incidents swiftly and efficiently. diff --git a/components/ibm_x_force_exchange/ibm_x_force_exchange.app.mjs b/components/ibm_x_force_exchange/ibm_x_force_exchange.app.mjs new file mode 100644 index 0000000000000..e42f1ad391649 --- /dev/null +++ b/components/ibm_x_force_exchange/ibm_x_force_exchange.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "ibm_x_force_exchange", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/ibm_x_force_exchange/package.json b/components/ibm_x_force_exchange/package.json new file mode 100644 index 0000000000000..60096c5ca3380 --- /dev/null +++ b/components/ibm_x_force_exchange/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/ibm_x_force_exchange", + "version": "0.0.1", + "description": "Pipedream IBM X-Force Exchange Components", + "main": "ibm_x_force_exchange.app.mjs", + "keywords": [ + "pipedream", + "ibm_x_force_exchange" + ], + "homepage": "https://pipedream.com/apps/ibm_x_force_exchange", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/ical/ical.app.mjs b/components/ical/ical.app.mjs new file mode 100644 index 0000000000000..f1c36ebd29958 --- /dev/null +++ b/components/ical/ical.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "ical", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/ical/package.json b/components/ical/package.json new file mode 100644 index 0000000000000..f811332cf5992 --- /dev/null +++ b/components/ical/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/ical", + "version": "0.0.1", + "description": "Pipedream iCal Components", + "main": "ical.app.mjs", + "keywords": [ + "pipedream", + "ical" + ], + "homepage": "https://pipedream.com/apps/ical", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/icontact/README.md b/components/icontact/README.md new file mode 100644 index 0000000000000..bb07db14f6d85 --- /dev/null +++ b/components/icontact/README.md @@ -0,0 +1,11 @@ +# Overview + +The iContact API provides a programmable way to manage email marketing campaigns, contacts, lists, and messages within iContact's service. Through this API, you can automate the creation and sending of emails, manage subscribers, and track the performance of your campaigns. When used on Pipedream, the iContact API allows you to create serverless workflows that integrate with other apps and services, trigger actions based on various conditions, and automate repetitive tasks. + +# Example Use Cases + +- **Automated Contact Syncing**: Sync contacts between iContact and a CRM like Salesforce automatically. When a new contact is added to Salesforce, a Pipedream workflow triggers to add that contact to a specified iContact list, ensuring your marketing list is always up-to-date. + +- **Email Campaign Trigger Based on User Activity**: Set up a Pipedream workflow to send a personalized email campaign through iContact when a user performs a specific action within your app (e.g., signing up, making a purchase). This can improve engagement by providing timely and relevant email content. + +- **Metrics Monitoring and Reporting**: Monitor key performance indicators for your iContact campaigns, such as open rate, click-through rate, and unsubscribes. Use Pipedream to aggregate this data and send a daily or weekly report to a Slack channel, keeping your team informed of campaign performance. diff --git a/components/icypeas/actions/domain-search/domain-search.mjs b/components/icypeas/actions/domain-search/domain-search.mjs new file mode 100644 index 0000000000000..21dec039d307a --- /dev/null +++ b/components/icypeas/actions/domain-search/domain-search.mjs @@ -0,0 +1,28 @@ +import icypeas from "../../icypeas.app.mjs"; + +export default { + key: "icypeas-domain-search", + name: "Domain or Company Search", + description: "Performs a search using a domain or company name as input. [See the documentation](https://api-doc.icypeas.com/find-emails/single-search/domain-scan)", + version: "0.0.1", + type: "action", + props: { + icypeas, + domainOrCompany: { + propDefinition: [ + icypeas, + "domainOrCompany", + ], + }, + }, + async run({ $ }) { + const response = await this.icypeas.domainSearch({ + $, + data: { + domainOrCompany: this.domainOrCompany, + }, + }); + $.export("$summary", `Search completed for ${this.domainOrCompany}`); + return response; + }, +}; diff --git a/components/icypeas/actions/email-verification/email-verification.mjs b/components/icypeas/actions/email-verification/email-verification.mjs new file mode 100644 index 0000000000000..669e6c8a2cff2 --- /dev/null +++ b/components/icypeas/actions/email-verification/email-verification.mjs @@ -0,0 +1,28 @@ +import icypeas from "../../icypeas.app.mjs"; + +export default { + key: "icypeas-email-verification", + name: "Email Verification", + description: "Performs an email verification check. [See the documentation](https://api-doc.icypeas.com/find-emails/single-search/email-verification)", + version: "0.0.1", + type: "action", + props: { + icypeas, + email: { + propDefinition: [ + icypeas, + "email", + ], + }, + }, + async run({ $ }) { + const response = await this.icypeas.verifyEmail({ + $, + data: { + email: this.email, + }, + }); + $.export("$summary", `Email verification status for ${this.email} retrieved successfully!`); + return response; + }, +}; diff --git a/components/icypeas/actions/get-single-result/get-single-result.mjs b/components/icypeas/actions/get-single-result/get-single-result.mjs new file mode 100644 index 0000000000000..150cc2ed1286c --- /dev/null +++ b/components/icypeas/actions/get-single-result/get-single-result.mjs @@ -0,0 +1,28 @@ +import icypeas from "../../icypeas.app.mjs"; + +export default { + key: "icypeas-get-single-result", + name: "Get Single Search Result", + description: "Retrieves a result from a single search instance. [See the documentation](https://api-doc.icypeas.com/find-emails/search-item)", + version: "0.0.1", + type: "action", + props: { + icypeas, + _id: { + propDefinition: [ + icypeas, + "_id", + ], + }, + }, + async run({ $ }) { + const response = await this.icypeas.retrieveSingleSearchResult({ + $, + data: { + id: this._id, + }, + }); + $.export("$summary", `Successfully retrieved the search result for instance ID ${this._id}`); + return response; + }, +}; diff --git a/components/icypeas/common/constants.mjs b/components/icypeas/common/constants.mjs new file mode 100644 index 0000000000000..9975390fe8151 --- /dev/null +++ b/components/icypeas/common/constants.mjs @@ -0,0 +1 @@ +export const LIMIT = 50; diff --git a/components/icypeas/icypeas.app.mjs b/components/icypeas/icypeas.app.mjs index 3f525486e826e..f26a34ada6165 100644 --- a/components/icypeas/icypeas.app.mjs +++ b/components/icypeas/icypeas.app.mjs @@ -1,11 +1,140 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + export default { type: "app", app: "icypeas", - propDefinitions: {}, + propDefinitions: { + domainOrCompany: { + type: "string", + label: "Domain Or Company", + description: "The domain or the company name you want to scan.", + }, + email: { + type: "string", + label: "Email", + description: "The specific email to be verified.", + }, + _id: { + type: "string", + label: "Search Instance ID", + description: "The identifier of the specific search instance.", + async options({ prevContext: { page } }) { + const data = { + next: true, + limit: LIMIT, + }; + if (page) { + data.sorts = page; + } + const { + items, sorts, + } = await this.retrieveSingleSearchResult({ + data, + }); + + return { + options: items.map(({ + _id: value, results, + }) => ({ + label: results.emails[0]?.email || value, + value, + })), + context: { + page: sorts, + }, + }; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://app.icypeas.com/api"; + }, + _headers() { + return { + "Content-Type": "application/json", + "Authorization": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: this._headers(), + }); + }, + domainSearch(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/domain-search", + ...opts, + }); + }, + verifyEmail(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/email-verification", + ...opts, + }); + }, + getMe() { + return this._makeRequest({ + method: "POST", + path: "/a/actions/subscription-information", + data: { + "email": this.$auth.email, + }, + }); + }, + async retrieveSingleSearchResult({ + data, ...opts + }) { + const { userId } = await this.getMe(); + + return this._makeRequest({ + method: "POST", + path: "/bulk-single-searchs/read", + data: { + ...data, + user: userId, + mode: "single", + }, + ...opts, + }); + }, + async *paginate({ + fn, data = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = null; + + do { + if (page) { + data.sorts = page; + } + const { + items, + sorts, + } = await fn({ + data, + ...opts, + }); + for (const d of items) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + page = sorts; + + hasMore = items.length; + + } while (hasMore); }, }, -}; \ No newline at end of file +}; diff --git a/components/icypeas/package.json b/components/icypeas/package.json index 02cf6e021eaba..772c8a43f03b3 100644 --- a/components/icypeas/package.json +++ b/components/icypeas/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/icypeas", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Icypeas Components", "main": "icypeas.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" } -} \ No newline at end of file +} + diff --git a/components/icypeas/sources/new-single-result/new-single-result.mjs b/components/icypeas/sources/new-single-result/new-single-result.mjs new file mode 100644 index 0000000000000..3ffbbfa537084 --- /dev/null +++ b/components/icypeas/sources/new-single-result/new-single-result.mjs @@ -0,0 +1,71 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import { LIMIT } from "../../common/constants.mjs"; +import icypeas from "../../icypeas.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "icypeas-new-single-result", + name: "New Single Search Result", + description: "Emit new event when a single search result comes in.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + icypeas, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01T00:00:00.001Z"; + }, + _setLastDate(created) { + this.db.set("lastDate", created); + }, + generateMeta(item) { + return { + id: item._id, + summary: `New result with Id: ${item._id}`, + ts: item.system.createdAt, + }; + }, + async startEvent(maxResults = 0) { + const lastDate = this._getLastDate(); + + const data = this.icypeas.paginate({ + fn: this.icypeas.retrieveSingleSearchResult, + maxResults, + data: { + next: true, + limit: LIMIT, + }, + }); + + const responseArray = []; + for await (const item of data) { + if (Date.parse(item.system.createdAt) <= Date.parse(lastDate)) break; + responseArray.push(item); + } + + if (responseArray.length) this._setLastDate(responseArray[0].system.createdAt); + + for (const item of responseArray.reverse()) { + this.$emit(item, this.generateMeta(item)); + } + }, + }, + hooks: { + async deploy() { + await this.startEvent(25); + }, + }, + async run() { + await this.startEvent(); + }, + sampleEmit, +}; diff --git a/components/icypeas/sources/new-single-result/test-event.mjs b/components/icypeas/sources/new-single-result/test-event.mjs new file mode 100644 index 0000000000000..c52801b39c003 --- /dev/null +++ b/components/icypeas/sources/new-single-result/test-event.mjs @@ -0,0 +1,27 @@ +export default { + "name": "My cool search", + "user": "#USERID#", + "file": "#FILEID#", + "results": { + "firstname": "Example", + "lastname": "Email", + "fullname": "Example Email", + "gender": "UNKNOWN", + "emails": [ + { + "email": "example-email@icypeas.com", + "certainty": "ultra_sure", + "mxProvider": "google", + "mxRecords": ["google.com"] + } + ], + "phones": [] + }, + "order": 0, + "status": "FOUND", + "system": { + "createdAt": "2023-01-01T13:49:49.630Z", + "modifiedAt": "2023-01-01T13:49:49.630Z" + }, + "_id": "oSmI5YYBMa6Snk9TvjDA" +} \ No newline at end of file diff --git a/components/ideal_postcodes/actions/email-validation/email-validation.mjs b/components/ideal_postcodes/actions/email-validation/email-validation.mjs new file mode 100644 index 0000000000000..326e9867e3086 --- /dev/null +++ b/components/ideal_postcodes/actions/email-validation/email-validation.mjs @@ -0,0 +1,50 @@ +import app from "../../ideal_postcodes.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "ideal_postcodes-email-validation", + name: "Email Validation", + description: "Validate email addresses using Ideal Postcodes. [See the documentation](https://docs.ideal-postcodes.co.uk/docs/api/email-validation).", + version: "0.0.1", + type: "action", + props: { + app, + query: { + type: "string", + label: "Email Address", + description: "The email address to validate.", + }, + tags: { + propDefinition: [ + app, + "tags", + ], + }, + }, + methods: { + validateEmail(args = {}) { + return this.app._makeRequest({ + path: "/emails", + ...args, + }); + }, + }, + async run({ $ }) { + const { + validateEmail, + query, + tags, + } = this; + + const response = await validateEmail({ + $, + params: { + query, + tags: utils.encode(tags), + }, + }); + + $.export("$summary", `Successfully validated email address with message \`${response.message}\``); + return response; + }, +}; diff --git a/components/ideal_postcodes/actions/find-place/find-place.mjs b/components/ideal_postcodes/actions/find-place/find-place.mjs new file mode 100644 index 0000000000000..e5fffcfc245d5 --- /dev/null +++ b/components/ideal_postcodes/actions/find-place/find-place.mjs @@ -0,0 +1,74 @@ +import app from "../../ideal_postcodes.app.mjs"; + +export default { + key: "ideal_postcodes-find-place", + name: "Find Place", + description: "Query for geographical places across countries. Each query will return a list of place suggestions, which consists of a place name, descriptive name, and id. [See the documentation](https://docs.ideal-postcodes.co.uk/docs/api/find-place)", + version: "0.0.1", + type: "action", + props: { + app, + query: { + type: "string", + label: "Query", + description: "Specifies the place you wish to query.", + }, + countryIso: { + type: "string", + label: "Country ISO", + description: "Filter by country ISO code (3 letter code, ISO 3166-1 standard). Filter by multiple countries with a comma separated list. E.g. `GBR,IRL`.", + optional: true, + }, + biasCountryIso: { + type: "string", + label: "Bias Country ISO", + description: "Bias by country ISO code. Uses 3 letter country code (ISO 3166-1) standard. Bias by multiple countries with a comma separated list. E.g. `GBR,IRL`.", + optional: true, + }, + biasLonlat: { + type: "string", + label: "Bias Geolocation", + description: "Bias search to a geospatial circle determined by an origin and radius in meters. Max radius is `50000`. Uses the format `[longitude],[latitude],[radius in metres]`. Only one geospatial bias may be provided.", + optional: true, + }, + biasIp: { + type: "boolean", + label: "Bias Geolocation of IP", + description: "Biases search based on approximate geolocation of IP address. Set `true` to enable.", + optional: true, + }, + }, + methods: { + findPlaces(args = {}) { + return this.app._makeRequest({ + path: "/places", + ...args, + }); + }, + }, + async run({ $ }) { + const { + findPlaces, + query, + countryIso, + biasCountryIso, + biasLonlat, + biasIp, + } = this; + + const response = await findPlaces({ + $, + params: { + query, + country_iso: countryIso, + bias_country_iso: biasCountryIso, + bias_lonlat: biasLonlat, + bias_ip: biasIp, + }, + }); + + $.export("$summary", `Successfully found places matching query "${query}"`); + + return response; + }, +}; diff --git a/components/ideal_postcodes/actions/phone-number-validation/phone-number-validation.mjs b/components/ideal_postcodes/actions/phone-number-validation/phone-number-validation.mjs new file mode 100644 index 0000000000000..efb67076d120b --- /dev/null +++ b/components/ideal_postcodes/actions/phone-number-validation/phone-number-validation.mjs @@ -0,0 +1,59 @@ +import app from "../../ideal_postcodes.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "ideal_postcodes-phone-number-validation", + name: "Phone Number Validation", + description: "Validates a phone number and returns information about it. [See the documentation](https://docs.ideal-postcodes.co.uk/docs/api/phone-number-validation).", + version: "0.0.1", + type: "action", + props: { + app, + query: { + type: "string", + label: "Phone Number", + description: "Specifies the phone number to validate. Phone number must include a country code in acceptable format. For instance, UK phone numbers should be suffixed `+44`, `44` or `0044`.", + }, + currentCarrier: { + type: "boolean", + label: "Current Carrier", + description: "When set to `true` the current network of the phone number will be retrieved and populated. Note that this operation is potentially slow depending on the network and local conditions.", + optional: true, + }, + tags: { + propDefinition: [ + app, + "tags", + ], + }, + }, + methods: { + validatePhoneNumber(args = {}) { + return this.app._makeRequest({ + path: "/phone_numbers", + ...args, + }); + }, + }, + async run({ $ }) { + const { + validatePhoneNumber, + query, + currentCarrier, + tags, + } = this; + + const response = await validatePhoneNumber({ + $, + params: { + query, + current_carrier: currentCarrier, + tags: utils.encode(tags), + }, + }); + + $.export("$summary", `Successfully validated phone number with message \`${response.message}\``); + + return response; + }, +}; diff --git a/components/ideal_postcodes/common/utils.mjs b/components/ideal_postcodes/common/utils.mjs new file mode 100644 index 0000000000000..d41b2c1c77f78 --- /dev/null +++ b/components/ideal_postcodes/common/utils.mjs @@ -0,0 +1,7 @@ +function encode(value) { + return value && encodeURIComponent(value); +} + +export default { + encode, +}; diff --git a/components/ideal_postcodes/ideal_postcodes.app.mjs b/components/ideal_postcodes/ideal_postcodes.app.mjs new file mode 100644 index 0000000000000..d7f84d9bf0ff9 --- /dev/null +++ b/components/ideal_postcodes/ideal_postcodes.app.mjs @@ -0,0 +1,34 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "ideal_postcodes", + propDefinitions: { + tags: { + type: "string", + label: "Tags", + description: "A comma separated list of tags to query over. Useful if you want to specify the circumstances in which the request was made. If multiple tags are specified, the response will only comprise of requests for which all the tags are satisfied - i.e. searching `foo,bar` will only query requests which tagged both `foo` and `bar`.", + optional: true, + }, + }, + methods: { + getUrl(path) { + return `https://api.ideal-postcodes.co.uk/v1${path}`; + }, + getHeaders(headers) { + return { + ...headers, + Authorization: `api_key="${this.$auth.api_key}"`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + }, +}; diff --git a/components/ideal_postcodes/package.json b/components/ideal_postcodes/package.json new file mode 100644 index 0000000000000..0f321d5890dfd --- /dev/null +++ b/components/ideal_postcodes/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/ideal_postcodes", + "version": "0.1.0", + "description": "Pipedream Ideal Postcodes Components", + "main": "ideal_postcodes.app.mjs", + "keywords": [ + "pipedream", + "ideal_postcodes" + ], + "homepage": "https://pipedream.com/apps/ideal_postcodes", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/idealpostcodes/README.md b/components/idealpostcodes/README.md index 20d9c09716dc4..54ede2864c007 100644 --- a/components/idealpostcodes/README.md +++ b/components/idealpostcodes/README.md @@ -1,19 +1,11 @@ # Overview -The IdealPostcodes API provides a simple, powerful interface for managing -postcodes and addresses in your applications. With the IdealPostcodes API, you -can: +IdealPostcodes API provides access to UK address data, enabling applications to perform address lookups, validation, and auto-completion. By leveraging this API on Pipedream, you can automate workflows that require accurate UK postal address information. This integration simplifies customer onboarding, ensures data accuracy for shipping, and enhances user experience with auto-filled address fields. -- Look up postcodes and addresses -- Validate addresses -- Format addresses -- Geocode addresses -- Reverse geocode postcodes -- Get information about postcode districts and sectors +# Example Use Cases -Here are some examples of what you can build with the IdealPostcodes API: +- **Customer Onboarding Automation**: When a new customer signs up on your e-commerce platform, trigger a Pipedream workflow that uses IdealPostcodes to validate their shipping address. The validated address can then be stored in your CRM, like Salesforce, ensuring that all customer data is accurate and deliveries are sent to the right location. -- A postcode lookup tool -- An address validation service -- A geocoding service -- A postcode finder +- **Scheduled Address Data Sync**: Set up a Pipedream workflow that runs at regular intervals to validate and update the addresses of all your customers in a database, like Airtable. This helps maintain clean and accurate data for marketing campaigns and customer communication, reducing bounce rates and improving engagement. + +- **Dynamic Address Auto-Completion**: Implement an auto-complete feature on your web forms using IdealPostcodes. When a user begins typing an address, trigger a Pipedream workflow that queries the API for address suggestions and dynamically populates the form options. This can be integrated with web services like Shopify, enhancing the checkout process and reducing cart abandonment. diff --git a/components/idealspot/README.md b/components/idealspot/README.md index 31683c821c50d..86db5d867d0b1 100644 --- a/components/idealspot/README.md +++ b/components/idealspot/README.md @@ -1,14 +1,11 @@ # Overview -The IdealSpot API enables developers to programmatically access the IdealSpot -platform. With the IdealSpot API, developers can create, manage, and retrieve -listings and property information. - -Some examples of what you can build with the IdealSpot API: - -- A website or app that displays listings from the IdealSpot platform -- A tool that helps people find their ideal home or office space -- A real estate agent or broker website or app that displays listings from the - IdealSpot platform -- A website or app that helps people compare different listings on the - IdealSpot platform +The IdealSpot API allows users to access valuable geolocation data to inform business decision-making. With this API, you can obtain insights on foot traffic, demographic profiles, competitive landscapes, and commercial real estate availability. It's a powerful tool for businesses looking to analyze and optimize site selection for physical locations. By leveraging the IdealSpot API on Pipedream, you can automate and enhance data-driven decisions, seamlessly integrating this rich geolocation data with other business applications, streamlining workflows, and gaining real-time market intelligence. + +# Example Use Cases + +- **Market Analysis Automation**: Trigger a Pipedream workflow whenever a new market analysis request is posted to your CRM. Use the IdealSpot API to fetch relevant geolocation data and compile a comprehensive market report, then store this report in Google Drive and notify the sales team via Slack. + +- **Real-Time Site Selection Alerts**: Set up a Pipedream workflow that periodically checks IdealSpot for new real estate listings in desired areas. When an ideal spot is found, automatically populate the details into an Airtable base, and send out a notification via email or SMS to the relevant stakeholders. + +- **Competitive Landscape Monitoring**: Configure a workflow to monitor competitors' locations. Use IdealSpot API to track competitor activity in specified areas, and when changes are detected, such as new stores opening or closing, update a dashboard in Tableau or send a digest to the business intelligence team through Microsoft Teams. diff --git a/components/identitycheck/actions/create-verification/create-verification.mjs b/components/identitycheck/actions/create-verification/create-verification.mjs new file mode 100644 index 0000000000000..514b6cc5d58f8 --- /dev/null +++ b/components/identitycheck/actions/create-verification/create-verification.mjs @@ -0,0 +1,44 @@ +import app from "../../identitycheck.app.mjs"; + +export default { + key: "identitycheck-create-verification", + name: "Create Verification", + description: "Create a identity check. [See the documentation](https://stackgo.notion.site/How-to-Generate-an-IdentityCheck-API-Key-38a12805b43249a480a96b346c491740)", + version: "0.0.1", + type: "action", + props: { + app, + firstName: { + propDefinition: [ + app, + "firstName", + ], + }, + lastName: { + propDefinition: [ + app, + "lastName", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createVerification({ + $, + data: { + firstName: this.firstName, + lastName: this.lastName, + email: this.email, + }, + }); + + $.export("$summary", `Successfully created an identity check with the following URL: '${response.data.idUrl}'`); + + return response; + }, +}; diff --git a/components/identitycheck/identitycheck.app.mjs b/components/identitycheck/identitycheck.app.mjs new file mode 100644 index 0000000000000..a3d7a4c124043 --- /dev/null +++ b/components/identitycheck/identitycheck.app.mjs @@ -0,0 +1,51 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "identitycheck", + propDefinitions: { + firstName: { + type: "string", + label: "First Name", + description: "First Name for the identity check", + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last Name for the identity check", + }, + email: { + type: "string", + label: "Email", + description: "Email address", + }, + }, + methods: { + _baseUrl() { + return "https://identity.stackgo.io/api"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "Authorization": `Basic ${this.$auth.api_key}`, + }, + }); + }, + async createVerification(args = {}) { + return this._makeRequest({ + method: "post", + path: "/direct-verification", + ...args, + }); + }, + }, +}; diff --git a/components/identitycheck/package.json b/components/identitycheck/package.json new file mode 100644 index 0000000000000..469d1760bfc81 --- /dev/null +++ b/components/identitycheck/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/identitycheck", + "version": "0.1.0", + "description": "Pipedream IdentityCheck Components", + "main": "identitycheck.app.mjs", + "keywords": [ + "pipedream", + "identitycheck" + ], + "homepage": "https://pipedream.com/apps/identitycheck", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} diff --git a/components/idx_broker/README.md b/components/idx_broker/README.md new file mode 100644 index 0000000000000..9938ef2c86d16 --- /dev/null +++ b/components/idx_broker/README.md @@ -0,0 +1,11 @@ +# Overview + +The IDX Broker API taps into real estate listing data, letting you craft customized property search experiences on your site. With IDX Broker and Pipedream, you can automate tasks like syncing listing data, managing leads, and updating property features. The API provides a wealth of endpoints, allowing you to query for active listings, featured properties, market reports, and more. This makes it a sturdy choice for real estate professionals wishing to streamline their digital operations. + +# Example Use Cases + +- **Sync New Listings to Google Sheets**: Automatically push new real estate listings from IDX Broker to a Google Sheets spreadsheet. This workflow ensures your data stays current and can be easily shared with your team or clients. + +- **Post Featured Listings to Social Media**: When a new property is marked as featured in IDX Broker, have Pipedream post it to your social media accounts, like Facebook or Twitter, to drive engagement and direct potential buyers to your listings. + +- **Send Lead Notifications via SMS**: Capture new leads from IDX Broker and use Twilio within Pipedream to send an SMS notification to the agent responsible. This keeps your response time to potential clients lightning-fast. diff --git a/components/if_else/if_else.app.mjs b/components/if_else/if_else.app.mjs new file mode 100644 index 0000000000000..31991101dc985 --- /dev/null +++ b/components/if_else/if_else.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "if_else", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/if_else/package.json b/components/if_else/package.json new file mode 100644 index 0000000000000..ca9d2908baee4 --- /dev/null +++ b/components/if_else/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/if_else", + "version": "0.0.1", + "description": "Pipedream If/Else Components", + "main": "if_else.app.mjs", + "keywords": [ + "pipedream", + "if_else" + ], + "homepage": "https://pipedream.com/apps/if_else", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/ifttt/README.md b/components/ifttt/README.md index 589b343dfe238..cce83bc4b2c17 100644 --- a/components/ifttt/README.md +++ b/components/ifttt/README.md @@ -1,11 +1,11 @@ # Overview -With the IFTTT API, you can build a wide variety of applications and -integrations. Here are just a few examples: - -- A to-do list app that adds new items to a spreadsheet -- A weather app that sends you a notification when it's going to rain -- A social media app that posts updates to a blog -- An e-commerce app that track your order status and sends you shipping updates -- A fitness app that logs your workout data and shares your progress with - friends +IFTTT (If This Then That) is a service that lets you create chains of conditional statements, known as applets. These applets can automate tasks across a variety of web services and APIs. By integrating IFTTT with Pipedream, you unlock the ability to handle complex logic, manage state, transform data, and interact with over 600+ supported apps in Pipedream’s ecosystem. You can trigger workflows on Pipedream with events from IFTTT and vice versa, creating a seamless automation experience. + +# Example Use Cases + +- **Smart Home Alerts to Slack**: Trigger a Pipedream workflow when your smart home device (connected via IFTTT) detects motion. The workflow processes the event and sends a formatted alert message to a Slack channel, keeping your team informed of security at your premises. + +- **Social Media Performance Tracking**: When a new post is made on your Instagram account (through IFTTT), trigger a workflow on Pipedream to retrieve the post’s performance data like likes and comments after 24 hours. Then, log this data in a Google Sheets spreadsheet for analysis and reporting. + +- **Email Digest of Daily Activities**: Use IFTTT to monitor your fitness app for daily activity summaries. Once it detects a summary, it triggers a Pipedream workflow which aggregates the data and sends you an email digest using the SendGrid app on Pipedream at the end of the day. diff --git a/components/ignisign/actions/create-signature-request/create-signature-request.mjs b/components/ignisign/actions/create-signature-request/create-signature-request.mjs new file mode 100644 index 0000000000000..10f9f8967dfa6 --- /dev/null +++ b/components/ignisign/actions/create-signature-request/create-signature-request.mjs @@ -0,0 +1,139 @@ +import { ConfigurationError } from "@pipedream/platform"; +import FormData from "form-data"; +import fs from "fs"; +import { LANGUAGE_OPTIONS } from "../../common/constants.mjs"; +import { + checkTmp, parseObject, +} from "../../common/utils.mjs"; +import ignisign from "../../ignisign.app.mjs"; + +export default { + key: "ignisign-create-signature-request", + name: "Create Signature Request", + description: "Creates a document signature request through IgniSign. [See the documentation](https://ignisign.io/docs/ignisign-api/init-signature-request)", + version: "0.0.1", + type: "action", + props: { + ignisign, + signerIds: { + propDefinition: [ + ignisign, + "signerIds", + ], + }, + documentLabel: { + type: "string", + label: "Document Label", + description: "A user-friendly label to identify the document.", + optional: true, + }, + documentDescription: { + type: "string", + label: "Document Description", + description: "A detailed, human-readable description of the document.", + optional: true, + }, + documentExternalId: { + type: "string", + label: "Document External Id", + description: "An optional external identifier that can be used to reference the document from external systems. It's a free text. Ignisign's system do not interprete it.", + optional: true, + }, + file: { + type: "string", + label: "Document File", + description: "The file to be uploaded, please provide a file from `/tmp`. To upload a file to `/tmp` folder, please follow the doc [here](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + }, + title: { + type: "string", + label: "Title", + description: "The title of the signature request.", + }, + description: { + type: "string", + label: "Description", + description: "The description of the signature request.", + optional: true, + }, + expirationDateIsActivated: { + type: "boolean", + label: "Expiration Date Is Activated", + description: "Indicates whether the expiration date is activated.", + reloadProps: true, + optional: true, + }, + expirationDate: { + type: "string", + label: "Expiration Date", + description: "The expiration date. The action linked to this date is performed every 5 minutes, at 5, 10, 15... 55.", + optional: true, + hidden: true, + }, + language: { + type: "string", + label: "Language", + description: "Represents the languages for signatures supported by a signature profile.", + options: LANGUAGE_OPTIONS, + optional: true, + }, + }, + async additionalProps(props) { + props.expirationDate.hidden = !this.expirationDateIsActivated; + return {}; + }, + async run({ $ }) { + const data = new FormData(); + + const { signatureRequestId } = await this.ignisign.initSignatureRequest(); + + const { documentId } = await this.ignisign.initDocument({ + data: { + signatureRequestId, + label: this.documentLabel, + description: this.documentDescription, + externalId: this.documentExternalId, + }, + }); + + const path = checkTmp(this.file); + if (!fs.existsSync(path)) { + await this.ignisign.closeSignatureRequest({ + signatureRequestId, + }); + throw new ConfigurationError("File does not exist!"); + } + const file = fs.createReadStream(path); + data.append("file", file); + + await this.ignisign.uploadFile({ + documentId, + data, + headers: data.getHeaders(), + }); + + await this.ignisign.updateSignatureRequest({ + signatureRequestId, + data: { + title: this.title, + description: this.description, + expirationDateIsActivated: this.expirationDateIsActivated, + expirationDate: this.expirationDate, + language: this.language, + documentIds: [ + documentId, + ], + signerIds: parseObject(this.signerIds), + }, + }); + + await this.ignisign.publishSignatureRequest({ + $, + signatureRequestId, + }); + + $.export("$summary", `Successfully published signature request with ID ${signatureRequestId}`); + return { + signatureRequestId, + }; + }, +}; diff --git a/components/ignisign/actions/create-signer/create-signer.mjs b/components/ignisign/actions/create-signer/create-signer.mjs new file mode 100644 index 0000000000000..ff6431d13d2d4 --- /dev/null +++ b/components/ignisign/actions/create-signer/create-signer.mjs @@ -0,0 +1,95 @@ +import ignisign from "../../ignisign.app.mjs"; + +export default { + key: "ignisign-create-signer", + name: "Create Signer", + description: "Creates a new signer entity in IgniSign. [See the documentation](https://ignisign.io/docs/ignisign-api/create-signer)", + version: "0.0.1", + type: "action", + props: { + ignisign, + signerProfileId: { + propDefinition: [ + ignisign, + "signerProfileId", + ], + optional: true, + }, + externalId: { + propDefinition: [ + ignisign, + "externalId", + ], + optional: true, + }, + firstName: { + propDefinition: [ + ignisign, + "firstName", + ], + optional: true, + }, + lastName: { + propDefinition: [ + ignisign, + "lastName", + ], + optional: true, + }, + email: { + propDefinition: [ + ignisign, + "email", + ], + }, + phoneNumber: { + propDefinition: [ + ignisign, + "phoneNumber", + ], + optional: true, + }, + nationality: { + propDefinition: [ + ignisign, + "nationality", + ], + optional: true, + }, + birthDate: { + propDefinition: [ + ignisign, + "birthDate", + ], + optional: true, + }, + birthPlace: { + propDefinition: [ + ignisign, + "birthPlace", + ], + optional: true, + }, + birthCountry: { + propDefinition: [ + ignisign, + "birthCountry", + ], + optional: true, + }, + }, + async run({ $ }) { + const { + ignisign, + ...data + } = this; + + const response = await ignisign.createSigner({ + $, + data, + }); + + $.export("$summary", `Successfully created signer with ID: ${response.signerId}`); + return response; + }, +}; diff --git a/components/ignisign/actions/get-signature-proof/get-signature-proof.mjs b/components/ignisign/actions/get-signature-proof/get-signature-proof.mjs new file mode 100644 index 0000000000000..3904030bca8e7 --- /dev/null +++ b/components/ignisign/actions/get-signature-proof/get-signature-proof.mjs @@ -0,0 +1,47 @@ +import fs from "fs"; +import stream from "stream"; +import { promisify } from "util"; +import ignisign from "../../ignisign.app.mjs"; + +export default { + key: "ignisign-get-signature-proof", + name: "Get Signature Proof", + description: "Retrieves a proof file for a specific signature. [See the documentation](https://ignisign.io/docs/category/ignisign-api)", + version: "0.0.1", + type: "action", + props: { + ignisign, + signatureRequestId: { + propDefinition: [ + ignisign, + "signatureRequestId", + ], + }, + documentId: { + propDefinition: [ + ignisign, + "documentId", + ({ signatureRequestId }) => ({ + signatureRequestId, + }), + ], + withLabel: true, + }, + }, + async run({ $ }) { + const response = await this.ignisign.getSignatureProof({ + $, + documentId: this.documentId.value, + responseType: "stream", + }); + + const pipeline = promisify(stream.pipeline); + await pipeline(response, fs.createWriteStream(`/tmp/${this.documentId.label}`)); + + $.export("$summary", `Successfully retrieved signature proof for request ID: ${this.signatureRequestId} and saved in /tmp directory.`); + return { + filename: this.documentId.label, + filepath: `/tmp/${this.documentId.label}`, + }; + }, +}; diff --git a/components/ignisign/common/constants.mjs b/components/ignisign/common/constants.mjs new file mode 100644 index 0000000000000..bed1b9fdc4632 --- /dev/null +++ b/components/ignisign/common/constants.mjs @@ -0,0 +1,14 @@ +export const LANGUAGE_OPTIONS = [ + "EN", + "FR", + "DE", + "ES", + "IT", + "PT", + "NL", + "PL", + "JA", + "KO", + "AR", + "HE", +]; diff --git a/components/ignisign/common/utils.mjs b/components/ignisign/common/utils.mjs new file mode 100644 index 0000000000000..6c207c1f7fa3d --- /dev/null +++ b/components/ignisign/common/utils.mjs @@ -0,0 +1,36 @@ +export const camelCaseToTitleCase = (text) => { + const result = text.replace(/([A-Z])/g, " $1"); + return result.charAt(0).toUpperCase() + result.slice(1); +}; + +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; + +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/ignisign/ignisign.app.mjs b/components/ignisign/ignisign.app.mjs new file mode 100644 index 0000000000000..39a125d97d540 --- /dev/null +++ b/components/ignisign/ignisign.app.mjs @@ -0,0 +1,303 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "ignisign", + propDefinitions: { + signatureProfileId: { + type: "string", + label: "Signature Profile ID", + description: "The unique identifier of the signature profile", + async options() { + const data = await this.listSignatureProfiles(); + + return data.map(({ + _id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + signerProfileId: { + type: "string", + label: "Signer Profile ID", + description: "The unique identifier of the signer profile", + async options() { + const data = await this.listSignerProfiles(); + + return data.map(({ + _id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + signerIds: { + type: "string[]", + label: "Signer IDs", + description: "The unique identifier of the signers", + async options({ page }) { + const { signers } = await this.listSigners({ + params: { + page, + }, + }); + + return signers.map(({ + signerId: value, email: label, + }) => ({ + label, + value, + })); + }, + }, + signatureRequestId: { + type: "string", + label: "Signature Request ID", + description: "The unique identifier of the asignature requests", + async options({ page }) { + const { signatureRequests } = await this.listSignatureRequests({ + params: { + page: page + 1, + }, + }); + + return signatureRequests.filter(({ status }) => status === "COMPLETED").map(({ + _id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + documentId: { + type: "string", + label: "Document ID", + description: "The unique identifier of the asignature request documents", + async options({ + page, signatureRequestId, + }) { + const { documents } = await this.getSignatureRequestContext({ + signatureRequestId, + params: { + page, + }, + }); + + return documents.map(({ + _id: value, fileName: label, + }) => ({ + label, + value, + })); + }, + }, + externalId: { + type: "string", + label: "External ID", + description: "An external identifier for the signer", + }, + email: { + type: "string", + label: "Email", + description: "The email of the signer", + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the signer", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the signer", + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number of the signer", + }, + nationality: { + type: "string", + label: "Nationality", + description: "The nationality of the signer in ISO 3166-1 alpha-2", + }, + birthDate: { + type: "string", + label: "Birth Date", + description: "The birth date of the signer", + }, + birthPlace: { + type: "string", + label: "Birth Place", + description: "The place of birth of the signer", + }, + birthCountry: { + type: "string", + label: "Birth Country", + description: "The country of birth of the signer in ISO 3166-1 alpha-2", + }, + }, + methods: { + _baseUrl(envs = `/applications/${this.$auth.app_id}/envs/${this.$auth.app_env}`) { + return `https://api.ignisign.io/v4${envs}`; + }, + _headers(headers = {}) { + return { + ...headers, + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, envs, ...opts + }) { + return axios($, { + url: this._baseUrl(envs) + path, + headers: this._headers(headers), + ...opts, + }); + }, + createSigner(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/signers", + ...opts, + }); + }, + listSignatureProfiles(opts = {}) { + return this._makeRequest({ + path: "/signature-profiles", + ...opts, + }); + }, + listSignatureRequests(opts = {}) { + return this._makeRequest({ + path: "/signature-requests", + ...opts, + }); + }, + listSigners(opts = {}) { + return this._makeRequest({ + path: "/signers-paginate", + ...opts, + }); + }, + listSignerProfiles(opts = {}) { + return this._makeRequest({ + path: "/signer-profiles", + ...opts, + }); + }, + initDocument(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/init-documents", + ...opts, + }); + }, + initSignatureRequest(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/signature-requests", + ...opts, + }); + }, + updateSignatureRequest({ + signatureRequestId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/signature-requests/${signatureRequestId}`, + envs: "", + ...opts, + }); + }, + getSignatureRequestContext({ signatureRequestId }) { + return this._makeRequest({ + path: `/signature-requests/${signatureRequestId}/context`, + envs: "", + }); + }, + getSignatureProof({ + documentId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/documents/${documentId}/signature-proof/PDF_WITH_SIGNATURES`, + envs: "", + ...opts, + }); + }, + getSignerProfileInputs({ signerProfileId }) { + return this._makeRequest({ + path: `/signer-profiles/${signerProfileId}/inputs-needed`, + }); + }, + getSignerDetails({ signerId }) { + return this._makeRequest({ + path: `/signers/${signerId}/details`, + }); + }, + uploadFile({ + documentId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/documents/${documentId}/file`, + envs: "", + ...opts, + }); + }, + publishSignatureRequest({ signatureRequestId }) { + return this._makeRequest({ + method: "POST", + path: `/signature-requests/${signatureRequestId}/publish`, + envs: "", + }); + }, + closeSignatureRequest({ signatureRequestId }) { + return this._makeRequest({ + method: "POST", + path: `/signature-requests/${signatureRequestId}/close`, + envs: "", + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + envs: "", + }); + }, + disableWebhookEvents(webhookId) { + return this._makeRequest({ + method: "POST", + path: `/webhooks/${webhookId}/disabled-events`, + envs: "", + data: { + topics: { + "ALL": true, + "APP": true, + "SIGNATURE": true, + "SIGNATURE_REQUEST": true, + "SIGNER": true, + "SIGNATURE_PROFILE": true, + "SIGNATURE_SESSION": true, + "SIGNATURE_SIGNER_IMAGE": true, + "ID_PROOFING": true, + "SIGNER_AUTH": true, + }, + }, + }); + }, + }, +}; diff --git a/components/ignisign/package.json b/components/ignisign/package.json new file mode 100644 index 0000000000000..1b6342af79b25 --- /dev/null +++ b/components/ignisign/package.json @@ -0,0 +1,22 @@ +{ + "name": "@pipedream/ignisign", + "version": "0.1.0", + "description": "Pipedream IgniSign Components", + "main": "ignisign.app.mjs", + "keywords": [ + "pipedream", + "ignisign" + ], + "homepage": "https://pipedream.com/apps/ignisign", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "form-data": "^4.0.1", + "fs": "^0.0.1-security", + "stream": "^0.0.3", + "util": "^0.12.5" + } +} diff --git a/components/ignisign/sources/new-signature-proof-instant/new-signature-proof-instant.mjs b/components/ignisign/sources/new-signature-proof-instant/new-signature-proof-instant.mjs new file mode 100644 index 0000000000000..013057fea24f7 --- /dev/null +++ b/components/ignisign/sources/new-signature-proof-instant/new-signature-proof-instant.mjs @@ -0,0 +1,54 @@ +import ignisign from "../../ignisign.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "ignisign-new-signature-proof-instant", + name: "New Signature Proof Instant", + description: "Emit new event when a signature proof is generated. [See the documentation](https://ignisign.io/docs/webhooks/signatureproof)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ignisign, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + }, + hooks: { + async activate() { + const [ + response, + ] = await this.ignisign.createWebhook({ + data: { + url: this.http.endpoint, + description: this.description, + }, + }); + await this.ignisign.disableWebhookEvents(response._id); + this._setHookId(response._id); + }, + async deactivate() { + const webhookId = this._getHookId(); + await this.ignisign.deleteWebhook(webhookId); + }, + }, + async run({ body }) { + const ts = Date.parse(new Date()); + this.$emit(body, { + id: `${body.signatureRequestId}-${ts}`, + summary: `New signature proof generated for signature request ID: ${body.content.signatureRequestId}`, + ts: ts, + }); + }, + sampleEmit, +}; diff --git a/components/ignisign/sources/new-signature-proof-instant/test-event.mjs b/components/ignisign/sources/new-signature-proof-instant/test-event.mjs new file mode 100644 index 0000000000000..6a55d69c5de4f --- /dev/null +++ b/components/ignisign/sources/new-signature-proof-instant/test-event.mjs @@ -0,0 +1,23 @@ +export default { + "appId": "appId_e5d6bcfb-e9f4-4858-8027-b65e1a170bd7", + "appEnv": "DEVELOPMENT", + "topic": "SIGNATURE_PROOF", + "action": "GENERATED", + "msgNature": "SUCCESS", + "content": { + "appEnv": "DEVELOPMENT", + "appId": "appId_e5d6bcfb-e9f4-4858-8027-b65e1a170bd7", + "signatureRequestId": "672d206d10e44e00125eac63", + "documents": [ + { + "documentId": "672d206d9902ba0012e9a7f0", + "documentExternalId": "123123123", + "token": "5wvyV2d0Q0SapHS0PcdgrZgUbTHbUEVVq3uaZd8CcLIAA", + "url": "https://sign.ignisign.io/signature-requests/672d206d10e44e00125eac63/proofs?t=5wvyV2d0Q0SapHS0PcdgrZgUbTHbUEVVq3uaZd8CcLIAA" + } + ], + "signatureProofToken": "9CdzHRTIQFZZlGSBciSUmnLXahOl4J0dZqV3wwl9XzYIAA", + "signatureProofUrl": "https://sign.ignisign.io/signature-requests/672d206d10e44e00125eac63/proofs?t=9CdzHRTIQFZZlGSBciSUmnLXahOl4J0dZqV3wwl9XzYIAA" + }, + "verificationToken": "N3gVBJB4TZZuE3MZSBeCqKo25ijHHFU2SvdiIMxm8znMAA" +} \ No newline at end of file diff --git a/components/ikas/ikas.app.mjs b/components/ikas/ikas.app.mjs new file mode 100644 index 0000000000000..80efc0a056b37 --- /dev/null +++ b/components/ikas/ikas.app.mjs @@ -0,0 +1,27 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "ikas", + methods: { + _baseUrl() { + return "https://api.myikas.com/api/v1/admin/graphql"; + }, + _headers() { + return { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + makeRequest({ + $ = this, ...opts + }) { + return axios($, { + method: "POST", + url: this._baseUrl(), + headers: this._headers(), + ...opts, + }); + }, + }, +}; diff --git a/components/ikas/package.json b/components/ikas/package.json new file mode 100644 index 0000000000000..067e330b4bbc6 --- /dev/null +++ b/components/ikas/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/ikas", + "version": "0.1.0", + "description": "Pipedream Ikas Components", + "main": "ikas.app.mjs", + "keywords": [ + "pipedream", + "ikas" + ], + "homepage": "https://pipedream.com/apps/ikas", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/ikas/sources/common/base.mjs b/components/ikas/sources/common/base.mjs new file mode 100644 index 0000000000000..568ef6c72766b --- /dev/null +++ b/components/ikas/sources/common/base.mjs @@ -0,0 +1,71 @@ +import ikas from "../../ikas.app.mjs"; + +export default { + props: { + ikas, + http: { + type: "$.interface.http", + customResponse: false, + }, + db: "$.service.db", + alert: { + type: "alert", + alertType: "warning", + content: "Please note that you can have only one webhook of each type at the same time, any change will overwrite the current webhook configuration.", + }, + }, + methods: { + _getWebhookId() { + return this.db.get("webhookId"); + }, + _setWebhookId(webhookId) { + return this.db.set("webhookId", webhookId); + }, + }, + hooks: { + async activate() { + const { data } = await this.ikas.makeRequest({ + data: { + "query": `mutation { + saveWebhook( + input: { + scopes: "${this.getScope()}" + endpoint: "${this.http.endpoint}" + } + ) { + createdAt + deleted + endpoint + id + scope + updatedAt + } + }`, + }, + }); + this._setWebhookId(data.saveWebhook[0].id); + }, + async deactivate() { + const webhookId = this._getWebhookId(); + if (webhookId) { + await this.ikas.makeRequest({ + data: { + "query": `mutation { + deleteWebhook(scopes: ["${this.getScope()}"]) + }`, + }, + }); + } + }, + }, + async run({ body }) { + const data = JSON.parse(body.data); + body.data = data; + + this.$emit(body, { + id: body.id, + summary: this.getSummary(data), + ts: Date.parse(body.createdAt), + }); + }, +}; diff --git a/components/ikas/sources/new-customer-instant/new-customer-instant.mjs b/components/ikas/sources/new-customer-instant/new-customer-instant.mjs new file mode 100644 index 0000000000000..6ba59639614e9 --- /dev/null +++ b/components/ikas/sources/new-customer-instant/new-customer-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ikas-new-customer-instant", + name: "New Customer (Instant)", + description: "Emit new event when a customer account is created on ikas. **You can only have one webhook of each type at the same time.**", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getScope() { + return "store/customer/created"; + }, + getSummary(data) { + return `New customer created with Id: ${data.id}.`; + }, + }, + sampleEmit, +}; diff --git a/components/ikas/sources/new-customer-instant/test-event.mjs b/components/ikas/sources/new-customer-instant/test-event.mjs new file mode 100644 index 0000000000000..2fc4ba0521e1e --- /dev/null +++ b/components/ikas/sources/new-customer-instant/test-event.mjs @@ -0,0 +1,40 @@ +export default { + "authorizedAppId": "12345678-1234-1234-1234-123456789012", + "createdAt": "2024-07-11T19:06:04.736Z", + "data": { + "createdAt": "2024-07-11T19:04:19.158Z", + "updatedAt": "2024-07-11T19:04:19.158Z", + "merchantId": "12345678-1234-1234-1234-123456789012", + "deleted": false, + "firstName": "Customer ", + "lastName": "Test", + "fullName": "Customer Test", + "email": "customer@test.com", + "isEmailVerified": false, + "attributes": null, + "phone": null, + "isPhoneVerified": false, + "addresses": [], + "note": null, + "accountStatus": null, + "subscriptionStatus": "NOT_SUBSCRIBED", + "smsSubscriptionStatus": null, + "phoneSubscriptionStatus": null, + "tagIds": null, + "customerGroupIds": null, + "preferredLanguage": null, + "b2bStatus": null, + "priceListRules": null, + "priceListId": null, + "isEmailBounced": false, + "customerSegmentIds": [], + "gender": null, + "birthDate": null, + "customerSequence": 1, + "id": "12345678-1234-1234-1234-123456789012", + }, + "id": "12345678-1234-1234-1234-123456789012-12345678-1234-1234-1234-123456789012", + "merchantId": "12345678-1234-1234-1234-123456789012", + "scope": "store/customer/created", + "signature": "373045525decbad86b3a09f4bac07cd2a6611801c8a2c9ec9be4973be269a522", +}; diff --git a/components/ikas/sources/new-order-instant/new-order-instant.mjs b/components/ikas/sources/new-order-instant/new-order-instant.mjs new file mode 100644 index 0000000000000..23f920cd3f754 --- /dev/null +++ b/components/ikas/sources/new-order-instant/new-order-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ikas-new-order-instant", + name: "New Order Created (Instant)", + description: "Emit new event when a new order is created on ikas. **You can only have one webhook of each type at the same time.**", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getScope() { + return "store/order/created"; + }, + getSummary(data) { + return `New order created with Id: ${data.id}.`; + }, + }, + sampleEmit, +}; diff --git a/components/ikas/sources/new-order-instant/test-event.mjs b/components/ikas/sources/new-order-instant/test-event.mjs new file mode 100644 index 0000000000000..d79edf2d4ef1a --- /dev/null +++ b/components/ikas/sources/new-order-instant/test-event.mjs @@ -0,0 +1,106 @@ +export default { + "authorizedAppId": "12345678-1234-1234-1234-123456789012", + "createdAt": "2024-07-11T19:06:04.736Z", + "data": { + "createdAt": "2024-07-11T21:22:48.992Z", + "updatedAt": "2024-07-11T21:23:16.543Z", + "merchantId": "12345678-1234-1234-1234-123456789012", + "deleted": false, + "billingAddress": null, + "cartId": "12345678-1234-1234-1234-123456789012", + "cartStatus": 2, + "checkoutId": "12345678-1234-1234-1234-123456789012", + "availableShippingMethods": [], + "createdBy": 2, + "campaignOffers": [], + "clientIp": "123.123.12.12", + "currencyCode": "USD", + "currencyRates": [], + "customer": { + "id": "12345678-1234-1234-1234-123456789012", + "firstName": "Customer", + "lastName": "Test", + "fullName": "Customer Test", + "email": "customer@test.com", + "notificationsAccepted": false, + "isGuestCheckout": true, + "lastOrderDate": null, + }, + "dueDate": "2024-07-11T21:23:16.377Z", + "itemCount": 1, + "note": null, + "giftPackageLines": [], + "orderLineItems": [ + { + "createdAt": "2024-07-11T21:23:16.543Z", + "updatedAt": "2024-07-11T21:23:16.543Z", + "deleted": false, + "price": 123, + "discountPrice": null, + "finalPrice": 123, + "taxValue": null, + "quantity": 1, + "currencyCode": "USD", + "discount": null, + "variant": { + "id": "12345678-1234-1234-1234-123456789012", + "productId": "12345678-1234-1234-1234-123456789012", + "name": "product test", + "barcodeList": [], + "variantValues": [], + "slug": "product-test", + "tagIds": [], + "tags": [], + "prices": [ + { + "sellPrice": 123, + }, + ], + "type": 1, + "categories": [], + }, + "status": "UNFULFILLED", + "sourceId": null, + "id": "12345678-1234-1234-1234-123456789012", + }, + ], + "orderNumber": "1001", + "orderedAt": "2024-07-11T21:22:48.993Z", + "priceListId": null, + "salesChannelId": "ADMIN", + "shippingAddress": null, + "shippingMethod": "SHIPMENT", + "storefrontRouting": null, + "totalFinalPrice": 123, + "netTotalFinalPrice": 123, + "totalPrice": 123, + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", + "taxLines": [], + "attributes": [], + "branchSession": null, + "branchSessionId": null, + "invoices": [], + "orderPackages": [], + "orderPaymentStatus": "WAITING", + "orderTagIds": null, + "paymentMethods": [], + "priceList": null, + "salesChannel": { + "id": "ADMIN", + "name": "ADMIN", + "type": 6, + }, + "status": "DRAFT", + "storefront": null, + "storefrontTheme": null, + "terminalId": null, + "sourceId": null, + "archived": false, + "orderSequence": 1, + "id": "12345678-1234-1234-1234-123456789012", + }, + "id": "12345678-1234-1234-1234-123456789012-12345678-1234-1234-1234-123456789012", + "merchantId": "12345678-1234-1234-1234-123456789012", + "scope": "store/order/created", + "signature": "373045525decbad86b3a09f4bac07cd2a6611801c8a2c9ec9be4973be269a522", +}; diff --git a/components/ikas/sources/new-product-instant/new-product-instant.mjs b/components/ikas/sources/new-product-instant/new-product-instant.mjs new file mode 100644 index 0000000000000..d352592a74739 --- /dev/null +++ b/components/ikas/sources/new-product-instant/new-product-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ikas-new-product-instant", + name: "New Product Created (Instant)", + description: "Emit new event when a product is created on ikas. **You can only have one webhook of each type at the same time.**", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getScope() { + return "store/product/created"; + }, + getSummary(data) { + return `New product created with Id: ${data.id}.`; + }, + }, + sampleEmit, +}; diff --git a/components/ikas/sources/new-product-instant/test-event.mjs b/components/ikas/sources/new-product-instant/test-event.mjs new file mode 100644 index 0000000000000..55b02a0b17053 --- /dev/null +++ b/components/ikas/sources/new-product-instant/test-event.mjs @@ -0,0 +1,87 @@ +export default { + "authorizedAppId": "12345678-1234-1234-1234-123456789012", + "createdAt": "2024-07-11T19:06:04.736Z", + "data": { + "deleted": false, + "name": "product test", + "shortDescription": null, + "weight": null, + "maxQuantityPerCart": null, + "groupVariantsByVariantTypeId": null, + "type": "PHYSICAL", + "productVariantTypes": [], + "categoryIds": null, + "variants": [ + { + "deleted": false, + "sku": null, + "barcodeList": null, + "weight": null, + "isActive": true, + "sellIfOutOfStock": null, + "images": null, + "prices": [ + { + "sellPrice": 123, + "discountPrice": null, + "buyPrice": null, + "currency": null, + "priceListId": null, + }, + ], + "hsCode": null, + "unit": null, + "bundleSettings": null, + "fileId": null, + "id": "12345678-1234-1234-1234-123456789012", + }, + ], + "attributes": [], + "tagIds": null, + "brandId": null, + "vendorId": null, + "metaData": { + "deleted": false, + "slug": "product-test", + "pageTitle": "product test", + "canonicals": null, + "disableIndex": null, + "targetType": 1, + "targetId": "12345678-1234-1234-1234-123456789012", + "metadataOverrides": null, + "id": "12345678-1234-1234-1234-123456789012", + }, + "salesChannels": [ + { + "id": "12345678-1234-1234-1234-123456789012", + "status": 1, + "maxQuantityPerCart": null, + "minQuantityPerCart": null, + "quantitySettings": null, + "productVolumeDiscountId": null, + }, + { + "id": "12345678-1234-1234-1234-123456789012", + "status": 1, + "maxQuantityPerCart": null, + "minQuantityPerCart": null, + "quantitySettings": null, + "productVolumeDiscountId": null, + }, + ], + "productOptionSetId": null, + "baseUnit": null, + "googleTaxonomyId": null, + "dynamicPriceListIds": null, + "productVolumeDiscountId": null, + "brand": null, + "id": "12345678-1234-1234-1234-123456789012", + "createdAt": "2024-07-11T21:20:50.506Z", + "updatedAt": "2024-07-11T21:20:50.506Z", + "merchantId": "12345678-1234-1234-1234-123456789012", + }, + "id": "12345678-1234-1234-1234-123456789012-12345678-1234-1234-1234-123456789012", + "merchantId": "12345678-1234-1234-1234-123456789012", + "scope": "store/product/created", + "signature": "373045525decbad86b3a09f4bac07cd2a6611801c8a2c9ec9be4973be269a522", +}; diff --git a/components/ikigai/ikigai.app.mjs b/components/ikigai/ikigai.app.mjs new file mode 100644 index 0000000000000..81f54f00f1174 --- /dev/null +++ b/components/ikigai/ikigai.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "ikigai", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/ikigai/package.json b/components/ikigai/package.json new file mode 100644 index 0000000000000..ce5c326429b38 --- /dev/null +++ b/components/ikigai/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/ikigai", + "version": "0.0.1", + "description": "Pipedream Ikigai Components", + "main": "ikigai.app.mjs", + "keywords": [ + "pipedream", + "ikigai" + ], + "homepage": "https://pipedream.com/apps/ikigai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/illumidesk/README.md b/components/illumidesk/README.md new file mode 100644 index 0000000000000..68554f32d6f5c --- /dev/null +++ b/components/illumidesk/README.md @@ -0,0 +1,11 @@ +# Overview + +The Illumidesk API integrates virtual learning environments with tools that facilitate instruction and collaboration. On Pipedream, you can leverage this API to automate educational workflows, manage courses, and interact with users. Pipedream’s serverless platform lets you create workflows triggered by various events without maintaining infrastructure, focusing on the logic and integration between services. + +# Example Use Cases + +- **Automate User Enrollment**: Sync Illumidesk course enrollments with a database by triggering a workflow whenever a new user registers. Enroll them in the appropriate course based on their profile and log their details. + +- **Assignment Submission Notifications**: Set up a workflow to send notifications via Slack or email when a student submits an assignment. Use Pipedream's ability to connect with SMTP services or Slack to deliver timely updates to instructors. + +- **Course Material Updates**: Whenever course material is updated on Illumidesk, trigger a Pipedream workflow to update the material on connected platforms like Google Classroom or GitHub, ensuring all resources are consistent across platforms. diff --git a/components/illumidesk/actions/create-course-lesson/create-course-lesson.mjs b/components/illumidesk/actions/create-course-lesson/create-course-lesson.mjs new file mode 100644 index 0000000000000..c03a3c48485b9 --- /dev/null +++ b/components/illumidesk/actions/create-course-lesson/create-course-lesson.mjs @@ -0,0 +1,57 @@ +import illumidesk from "../../illumidesk.app.mjs"; + +export default { + key: "illumidesk-create-course-lesson", + name: "Create Course Lesson", + description: "Create a new lesson in a course. [See the documentation](https://developers.illumidesk.com/reference/courses_lessons_create)", + version: "0.0.1", + type: "action", + props: { + illumidesk, + campusSlug: { + propDefinition: [ + illumidesk, + "campusSlug", + ], + }, + courseSlug: { + propDefinition: [ + illumidesk, + "courseSlug", + (c) => ({ + campusSlug: c.campusSlug, + }), + ], + }, + title: { + type: "string", + label: "Title", + description: "Title of the new lesson", + }, + description: { + type: "string", + label: "Description", + description: "Description of the new lesson", + optional: true, + }, + order: { + type: "integer", + label: "Order", + description: "An integer representing the order of the lesson (0 respresents the first position in the list)", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.illumidesk.createLesson({ + $, + courseSlug: this.courseSlug, + data: { + title: this.title, + description: this.description, + order: this.order, + }, + }); + $.export("$summary", `Successfully created lesson with ID: ${response.uuid}`); + return response; + }, +}; diff --git a/components/illumidesk/actions/create-course/create-course.mjs b/components/illumidesk/actions/create-course/create-course.mjs index 85293a3e7fb73..d5bd43623ec2a 100644 --- a/components/illumidesk/actions/create-course/create-course.mjs +++ b/components/illumidesk/actions/create-course/create-course.mjs @@ -4,7 +4,7 @@ export default { key: "illumidesk-create-course", name: "Create Course", description: "Create a new course. [See the documentation](https://developers.illumidesk.com/reference/campuses_courses_create)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { illumidesk, diff --git a/components/illumidesk/actions/invite-course-member/invite-course-member.mjs b/components/illumidesk/actions/invite-course-member/invite-course-member.mjs index 0e79f17e776b5..f5940698e9e87 100644 --- a/components/illumidesk/actions/invite-course-member/invite-course-member.mjs +++ b/components/illumidesk/actions/invite-course-member/invite-course-member.mjs @@ -6,7 +6,7 @@ export default { key: "illumidesk-invite-course-member", name: "Invite Course Member", description: "Invites a user to a selected course. [See the documentation](https://developers.illumidesk.com/reference/courses_invitations_create)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { illumidesk, diff --git a/components/illumidesk/actions/list-courses/list-courses.mjs b/components/illumidesk/actions/list-courses/list-courses.mjs index 2517763370353..a3cba4925ff80 100644 --- a/components/illumidesk/actions/list-courses/list-courses.mjs +++ b/components/illumidesk/actions/list-courses/list-courses.mjs @@ -4,7 +4,7 @@ export default { key: "illumidesk-list-courses", name: "List Courses", description: "List all the courses associated with a given campus. [See the documentation](https://developers.illumidesk.com/reference/campuses_public_campuses_courses_list)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { illumidesk, diff --git a/components/illumidesk/illumidesk.app.mjs b/components/illumidesk/illumidesk.app.mjs index 0891c2e07f0f2..8837d4015fc1c 100644 --- a/components/illumidesk/illumidesk.app.mjs +++ b/components/illumidesk/illumidesk.app.mjs @@ -126,6 +126,15 @@ export default { ...args, }); }, + createLesson({ + courseSlug, ...args + }) { + return this._makeRequest({ + method: "POST", + path: `/courses/${courseSlug}/lessons/`, + ...args, + }); + }, async *paginate({ resourceFn, args, }) { diff --git a/components/illumidesk/package.json b/components/illumidesk/package.json index 261be0baf526d..34f3cfc777a5e 100644 --- a/components/illumidesk/package.json +++ b/components/illumidesk/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/illumidesk", - "version": "0.1.0", + "version": "0.1.1", "description": "Pipedream Illumidesk Components", "main": "illumidesk.app.mjs", "keywords": [ diff --git a/components/illumidesk/sources/common/base.mjs b/components/illumidesk/sources/common/base.mjs index 3db0966fc94ce..b7ed4f3c3ec56 100644 --- a/components/illumidesk/sources/common/base.mjs +++ b/components/illumidesk/sources/common/base.mjs @@ -17,15 +17,6 @@ export default { "campusSlug", ], }, - courseSlug: { - propDefinition: [ - illumidesk, - "courseSlug", - (c) => ({ - campusSlug: c.campusSlug, - }), - ], - }, }, hooks: { async deploy() { diff --git a/components/illumidesk/sources/new-course-activity/new-course-activity.mjs b/components/illumidesk/sources/new-course-activity/new-course-activity.mjs index 051fe7e8a7e91..4b0f4e888272f 100644 --- a/components/illumidesk/sources/new-course-activity/new-course-activity.mjs +++ b/components/illumidesk/sources/new-course-activity/new-course-activity.mjs @@ -5,9 +5,21 @@ export default { key: "illumidesk-new-course-activity", name: "New Course Activity", description: "Emit new event when a new course activity is created", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "unique", + props: { + ...common.props, + courseSlug: { + propDefinition: [ + common.props.illumidesk, + "courseSlug", + (c) => ({ + campusSlug: c.campusSlug, + }), + ], + }, + }, methods: { ...common.methods, getResourceFn() { diff --git a/components/illumidesk/sources/new-course-lesson/new-course-lesson.mjs b/components/illumidesk/sources/new-course-lesson/new-course-lesson.mjs index c4ad547302a6e..661ea9e6c55c2 100644 --- a/components/illumidesk/sources/new-course-lesson/new-course-lesson.mjs +++ b/components/illumidesk/sources/new-course-lesson/new-course-lesson.mjs @@ -5,9 +5,21 @@ export default { key: "illumidesk-new-course-lesson", name: "New Course Lesson", description: "Emit new event when a new lesson for a specific course is created", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "unique", + props: { + ...common.props, + courseSlug: { + propDefinition: [ + common.props.illumidesk, + "courseSlug", + (c) => ({ + campusSlug: c.campusSlug, + }), + ], + }, + }, methods: { ...common.methods, getResourceFn() { diff --git a/components/illumidesk/sources/new-course-member/new-course-member.mjs b/components/illumidesk/sources/new-course-member/new-course-member.mjs index b5db20d1dc843..8af0d69d24bf0 100644 --- a/components/illumidesk/sources/new-course-member/new-course-member.mjs +++ b/components/illumidesk/sources/new-course-member/new-course-member.mjs @@ -5,9 +5,21 @@ export default { key: "illumidesk-new-course-member", name: "New Course Member", description: "Emit new event when a new member is added to a course.", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "unique", + props: { + ...common.props, + courseSlug: { + propDefinition: [ + common.props.illumidesk, + "courseSlug", + (c) => ({ + campusSlug: c.campusSlug, + }), + ], + }, + }, methods: { ...common.methods, getResourceFn() { diff --git a/components/illumidesk/sources/new-course/new-course.mjs b/components/illumidesk/sources/new-course/new-course.mjs new file mode 100644 index 0000000000000..fa498788a396e --- /dev/null +++ b/components/illumidesk/sources/new-course/new-course.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "illumidesk-new-course", + name: "New Course", + description: "Emit new event when a new course is created", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.illumidesk.listCoursesByCampus; + }, + getArgs() { + return { + campusSlug: this.campusSlug, + }; + }, + generateMeta(course) { + return { + id: course.uuid, + summary: course.name, + ts: Date.parse(course[this.getTsField()]), + }; + }, + }, +}; diff --git a/components/ilovepdf/README.md b/components/ilovepdf/README.md new file mode 100644 index 0000000000000..821e6ab6aa114 --- /dev/null +++ b/components/ilovepdf/README.md @@ -0,0 +1,11 @@ +# Overview + +The iLovePDF API lets you automate PDF processing tasks like merging, splitting, compressing, and converting PDFs to other formats. iLovePDF's robust functionality can be harnessed in Pipedream workflows, which may include handling PDFs generated from various triggers, processing them as needed, and connecting to other services for storage, data extraction, or further actions based on the transformed PDFs. + +# Example Use Cases + +- **Automated Invoice Processing**: Retrieve invoices from an email app like Gmail when they arrive, use iLovePDF to merge multiple PDFs into one, then save the merged file to Google Drive and notify accounting via Slack. + +- **PDF Archival Workflow**: Trigger a workflow whenever new PDFs are uploaded to Dropbox. Use iLovePDF to compress these PDFs, then store the compressed versions back in Dropbox in an archive folder, and log the details to a Google Sheet for record keeping. + +- **E-book Conversion and Distribution**: When new e-book content is pushed to a GitHub repository, use iLovePDF to convert documents from Word to PDF, then distribute the finished PDFs using SendGrid to email subscribers or upload to a CMS like WordPress for content delivery. diff --git a/components/imagekit_io/README.md b/components/imagekit_io/README.md new file mode 100644 index 0000000000000..b1de39a718e31 --- /dev/null +++ b/components/imagekit_io/README.md @@ -0,0 +1,12 @@ +# Overview + +ImageKit.io API lets you manage, optimize, and deliver images dynamically for your web applications. On Pipedream, you can integrate this API to construct serverless workflows that automate your image operations and connect with other services. You can upload images from various sources, apply real-time transformations, and track media assets without managing infrastructure. + +# Example Use Cases + +- **Automate Image Uploads**: Build a workflow that triggers every time a new image is added to a Dropbox folder, automatically uploading it to ImageKit.io, applying specified transformations, and then storing the optimized image URL in a Google Sheet. + +- **Content Moderation**: Implement a content moderation flow that takes new images uploaded to ImageKit.io, sends them to an AI service like AWS Rekognition for analysis, and flags content that doesn't meet predefined safety standards. + +- **Social Media Integration**: Create a workflow that listens for new posts on a CMS like WordPress, fetches the images from those posts, resizes and watermarks them using ImageKit.io, and then posts the optimized images to social media platforms like Twitter or Instagram. +. diff --git a/components/imagga/README.md b/components/imagga/README.md new file mode 100644 index 0000000000000..ed882ed688321 --- /dev/null +++ b/components/imagga/README.md @@ -0,0 +1,11 @@ +# Overview + +The Imagga API is a powerful image recognition tool that enables you to automate the process of analyzing and tagging images. With its AI-driven capabilities, you can extract a wealth of information from visual content. It offers features such as categorization, color extraction, and auto-tagging, making it incredibly useful for building workflows that require image analysis. + +# Example Use Cases + +- **Content Moderation Workflow**: Route images uploaded to a cloud storage platform through Imagga to detect and flag inappropriate content. If unsuitable images are found, send alerts or move the images to a separate folder for review. + +- **Digital Asset Management**: Enhance your asset library by automatically tagging and categorizing images as they are uploaded. Connect Imagga to a CMS to add metadata, making it easier for users to search and retrieve media assets. + +- **E-commerce Product Listing Automation**: Automate the process of listing products by using Imagga to identify and tag product images. Connect to an e-commerce platform like Shopify to create new product listings with relevant tags and categories. diff --git a/components/imagior/actions/generate-image/generate-image.mjs b/components/imagior/actions/generate-image/generate-image.mjs new file mode 100644 index 0000000000000..b377d7839bbf3 --- /dev/null +++ b/components/imagior/actions/generate-image/generate-image.mjs @@ -0,0 +1,80 @@ +import imagior from "../../imagior.app.mjs"; + +export default { + key: "imagior-generate-image", + name: "Generate Image", + description: "Generates a unique and robust image using a provided template. [See the documentation](https://docs.imagior.com/api-reference/image-generate)", + version: "0.0.1", + type: "action", + props: { + imagior, + templateId: { + propDefinition: [ + imagior, + "templateId", + ], + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (!this.templateId) { + return props; + } + const { elements } = await this.imagior.listTemplateElements({ + templateId: this.templateId, + }); + for (const [ + key, + value, + ] of Object.entries(elements)) { + props[`customize_${key}`] = { + type: "boolean", + label: `Customize ${key}`, + optional: true, + reloadProps: true, + }; + if (this[`customize_${key}`]) { + for (const elementKey of Object.keys(value)) { + props[`${key}_${elementKey}`] = { + type: "string", + label: `${key} - ${elementKey}`, + optional: true, + }; + } + } + } + return props; + }, + async run({ $ }) { + const elements = {}; + const { elements: allElements } = await this.imagior.listTemplateElements({ + $, + templateId: this.templateId, + }); + for (const [ + key, + value, + ] of Object.entries(allElements)) { + if (!this[`customize_${key}`]) { + continue; + } + elements[key] = {}; + for (const elementKey of Object.keys(value)) { + if (this[`${key}_${elementKey}`]) { + elements[key][elementKey] = this[`${key}_${elementKey}`]; + } + } + } + + const response = await this.imagior.generateImage({ + $, + data: { + templateId: this.templateId, + elements, + }, + }); + $.export("$summary", `${response.message}`); + return response; + }, +}; diff --git a/components/imagior/imagior.app.mjs b/components/imagior/imagior.app.mjs new file mode 100644 index 0000000000000..53d5897050d88 --- /dev/null +++ b/components/imagior/imagior.app.mjs @@ -0,0 +1,62 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "imagior", + propDefinitions: { + templateId: { + type: "string", + label: "Template ID", + description: "The unique ID of the design template to use", + async options() { + const templates = await this.listTemplates(); + return templates?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.imagior.com"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + listTemplates(opts = {}) { + return this._makeRequest({ + path: "/templates/all", + ...opts, + }); + }, + listTemplateElements({ + templateId, ...opts + }) { + return this._makeRequest({ + path: `/templates/${templateId}/elements`, + ...opts, + }); + }, + generateImage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/image/generate", + ...opts, + }); + }, + }, +}; diff --git a/components/imagior/package.json b/components/imagior/package.json new file mode 100644 index 0000000000000..6d05708249593 --- /dev/null +++ b/components/imagior/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/imagior", + "version": "0.1.0", + "description": "Pipedream Imagior Components", + "main": "imagior.app.mjs", + "keywords": [ + "pipedream", + "imagior" + ], + "homepage": "https://pipedream.com/apps/imagior", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/imagior/sources/new-template/new-template.mjs b/components/imagior/sources/new-template/new-template.mjs new file mode 100644 index 0000000000000..124e8fd1cb377 --- /dev/null +++ b/components/imagior/sources/new-template/new-template.mjs @@ -0,0 +1,63 @@ +import imagior from "../../imagior.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + key: "imagior-new-template", + name: "New Template Created", + description: "Emit new event when a new template is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + imagior, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + generateMeta(template) { + return { + id: template.id, + summary: `New Template: ${template.name}`, + ts: Date.parse(template.createdAt), + }; + }, + }, + async run() { + const lastTs = this._getLastTs(); + + const templates = await this.imagior.listTemplates({ + params: { + sort: "createdAt", + order: "desc", + }, + }); + + if (!templates?.length) { + return; + } + + const newTemplates = templates.filter(({ createdAt }) => Date.parse(createdAt) >= lastTs); + + if (!newTemplates?.length) { + return; + } + + this._setLastTs(Date.parse(newTemplates[0].createdAt)); + + newTemplates.forEach((template) => { + const meta = this.generateMeta(template); + this.$emit(template, meta); + }); + }, +}; diff --git a/components/imap/README.md b/components/imap/README.md index d46527be610e2..2015ce5927f1e 100644 --- a/components/imap/README.md +++ b/components/imap/README.md @@ -1,20 +1,11 @@ # Overview -The Internet Message Access Protocol (IMAP) is a mail protocol used for -accessing email on a remote web server. +Using Pipedream's IMAP API, developers can automate interactions with their email inbox, enabling serverless workflows that perform actions based on incoming emails. This could include parsing email contents, triggering events upon receiving emails from specific senders, attaching labels, and much more. By leveraging IMAP, Pipedream can act as a bridge between your email and other services, streamlining processes that would otherwise require manual intervention. -IMAP provides a way to access email stored on a server, allowing you to read, -write, and delete emails from your account. +# Example Use Cases -IMAP is a popular protocol for accessing email, and is supported by most email -providers. +- **Email Attachment Extraction and Storage:** Automatically detect when a new email with attachments arrives in your inbox. Download the attachments and save them to a cloud storage service like Google Drive or Dropbox using Pipedream's built-in connectors. -Here are some things you can build using the IMAP API: +- **Customer Support Ticket Creation:** Upon receiving an email to a support inbox, use Pipedream to parse the email, extract relevant information (like the sender's email, subject, and body), and create a ticket in a customer support platform such as Zendesk or Help Scout. -- A web-based email client -- A desktop email client -- A mobile email client -- A command-line email client -- A script to automatically delete old emails -- A script to automatically forward emails to another address -- A script to automatically save attachments to your computer +- **Lead Qualification:** When a new lead submits information via email, Pipedream can parse the email content and use it to populate fields in a CRM like Salesforce. Additionally, it can trigger a workflow that scores the lead's quality and assigns it to the appropriate sales team member. diff --git a/components/imap/package.json b/components/imap/package.json index dcb336c078be0..5d547ddb71e17 100644 --- a/components/imap/package.json +++ b/components/imap/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/imap", - "version": "0.0.4", + "version": "0.0.5", "description": "Pipedream IMAP Components", "main": "imap.app.mjs", "keywords": [ diff --git a/components/imap/sources/new-email/new-email.mjs b/components/imap/sources/new-email/new-email.mjs index 591974fab2e7c..01db0aee81074 100644 --- a/components/imap/sources/new-email/new-email.mjs +++ b/components/imap/sources/new-email/new-email.mjs @@ -9,7 +9,7 @@ export default { key: "imap-new-email", name: "New Email", description: "Emit new event for each new email in a mailbox", - version: "0.0.4", + version: "0.0.5", type: "source", dedupe: "unique", props: { diff --git a/components/imejis_io/README.md b/components/imejis_io/README.md new file mode 100644 index 0000000000000..81bb1010a173d --- /dev/null +++ b/components/imejis_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The Imejis.io API offers a suite of features that allow for the manipulation and management of images. Using Pipedream as a serverless integration platform, you can automate complex image operations, organize image collections, and connect image data with other services and apps. You can create workflows that respond to events, process images in real-time, and seamlessly integrate with numerous apps available in Pipedream's ecosystem. + +# Example Use Cases + +- **Auto-Resize Images for Social Media**: Automatically resize and optimize images uploaded to a cloud storage bucket (like AWS S3) for different social media platforms. When a new image is uploaded, trigger a Pipedream workflow that uses Imejis.io to adjust the image to predefined sizes and then posts the resulting images to social media apps like Twitter or Instagram. + +- **Image Tagging and Cataloging**: Streamline the process of tagging and organizing digital assets. Set up a Pipedream workflow that triggers when new images are added to a CMS. Use Imejis.io to analyze and tag images with relevant metadata, then store this information in a database like Airtable or Google Sheets for easy searching and retrieval. + +- **Dynamic Image Generation for E-commerce**: Generate customized product images on-the-fly for an e-commerce platform. Whenever a product's details are updated in your inventory management system, a Pipedream workflow can invoke Imejis.io to create images with the updated product information and overlay text or graphics. These images can then be automatically uploaded to your e-commerce site or sent to a CDN for distribution. diff --git a/components/imejis_io/package.json b/components/imejis_io/package.json index ec298d3482f24..7d3cf1d8ce358 100644 --- a/components/imejis_io/package.json +++ b/components/imejis_io/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/imgbb/README.md b/components/imgbb/README.md index 3f64321c02bd5..b56b0a6de38f4 100644 --- a/components/imgbb/README.md +++ b/components/imgbb/README.md @@ -1,9 +1,11 @@ # Overview -You can use the imgbb API to build a number of different things, including: +The imgbb API offers a straightforward solution for uploading images to the imgbb platform. Once uploaded, images can be shared across the web with ease, thanks to the provided URL links. This opens up a world of possibilities for automating image storage and distribution workflows on Pipedream. Whether you're managing a content-heavy blog, streamlining an eCommerce site with dynamic product images, or automating social media postings, using the imgbb API with Pipedream can save time and reduce manual effort. -- A simple image hosting service -- A image sharing service -- An image editing service -- A photoblogging platform -- And much more! +# Example Use Cases + +- **Blog Post Automation**: Automate the process of publishing articles with images for a blog. Whenever a new Markdown file is pushed to a GitHub repository, Pipedream can trigger a workflow that uploads associated images to imgbb, retrieves the image URLs, and inserts them into the blog post's content before publishing it to a CMS like WordPress. + +- **eCommerce Product Management**: Simplify the process of updating product images in an online store. Set up a Pipedream workflow that watches for new product images in a Dropbox folder, uploads them to imgbb, and then updates the product listings on Shopify with the new image URLs, ensuring that the store's catalog is always up-to-date with high-quality visuals. + +- **Social Media Content Scheduling**: Create a seamless social media management system. When a new image is added to a designated Google Drive folder, Pipedream can trigger a workflow that uploads the image to imgbb and schedules a post with the imgbb URL on social media platforms like Twitter or Facebook using their respective APIs, keeping your social media feeds fresh and engaging without manual intervention. diff --git a/components/imgix/README.md b/components/imgix/README.md new file mode 100644 index 0000000000000..1b811596959d9 --- /dev/null +++ b/components/imgix/README.md @@ -0,0 +1,11 @@ +# Overview + +The imgix API offers dynamic image processing and optimization. You can manipulate images on-the-fly by changing query parameters in the image URL, enabling a myriad of transformations like resizing, cropping, adjusting quality, format conversion, and applying filters. Integrating imgix with Pipedream allows you to automate workflows that involve image manipulation, optimization for different devices and contexts, and the dynamic delivery of images. + +# Example Use Cases + +- **Automated Image Optimization for Web Content**: Trigger a Pipedream workflow when new content is posted to a CMS like WordPress. The workflow grabs the image URLs, sends them to the imgix API for optimization and compression, and updates the CMS with the optimized image URLs. This ensures faster page loads and improved SEO. + +- **Dynamic Social Media Image Generation**: Set up a workflow that listens for new social media posts on platforms like Twitter or Instagram. For each new post, the workflow uses imgix to create multiple versions of attached images, optimizing them for different social media platforms and devices, then posts the images back to the respective platform. + +- **On-Demand E-commerce Product Image Customization**: Create a Pipedream workflow that integrates with an e-commerce platform like Shopify. Whenever a new product is added or updated, the workflow sends product images to imgix to apply company branding, watermarks, or to generate different image styles. The processed images are then automatically associated with the product listings. diff --git a/components/imgix/imgix.app.mjs b/components/imgix/imgix.app.mjs new file mode 100644 index 0000000000000..c811fe529a226 --- /dev/null +++ b/components/imgix/imgix.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "imgix", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/imgix/package.json b/components/imgix/package.json new file mode 100644 index 0000000000000..70eea481f2149 --- /dev/null +++ b/components/imgix/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/imgix", + "version": "0.0.1", + "description": "Pipedream imgix Components", + "main": "imgix.app.mjs", + "keywords": [ + "pipedream", + "imgix" + ], + "homepage": "https://pipedream.com/apps/imgix", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/imgur/README.md b/components/imgur/README.md index d21d1e5353810..7e4610f0e73be 100644 --- a/components/imgur/README.md +++ b/components/imgur/README.md @@ -1,7 +1,11 @@ # Overview -With the Imgur API you can build: +The Imgur API offers a way to programmatically engage with the Imgur platform, allowing for the upload and management of images, as well as access to Imgur's vast gallery of community images. With Pipedream, you can harness this API to create automated workflows that respond to various triggers and integrate with other services. This can facilitate tasks like automated image sharing, content moderation, or social media management. -- An image hosting application -- An image sharing application -- An image editing application +# Example Use Cases + +- **Automated Image Backup to Cloud Storage**: Trigger a Pipedream workflow whenever a new image is uploaded to Imgur. Automatically save a copy of the image to a cloud storage service like Google Drive or Dropbox, ensuring you have a backup of all uploaded images. + +- **Social Media Content Pipeline**: Use Pipedream to monitor Imgur for new images tagged with specific keywords. Once an image is detected, the workflow could push the content to your social media platforms, such as Twitter or Facebook, enabling a consistent flow of content sourced from Imgur's community-driven galleries. + +- **Moderation Alert System**: Create a Pipedream workflow that scans new Imgur uploads for content that doesn't adhere to your platform's guidelines. If such content is identified, the workflow can trigger alerts or emails to moderators, integrate with a task management app like Trello, or directly invoke moderation actions via the Imgur API. diff --git a/components/implisense_api/README.md b/components/implisense_api/README.md new file mode 100644 index 0000000000000..3345d15114c69 --- /dev/null +++ b/components/implisense_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Implisense API provides access to detailed company data from millions of companies, allowing you to enrich your CRM with firmographics, create targeted B2B marketing campaigns, and perform market analysis. The API offers methods to retrieve information like company profiles, financial data, and business relations. Integrating the Implisense API into Pipedream workflows lets you automate the process of data capture and action-taking based on company insights. + +# Example Use Cases + +- **Enrich CRM Records**: Trigger a workflow when a new lead is added to your CRM. Use the Implisense API to fetch additional data about the company and update the CRM record with enriched information like company size, revenue, and key contacts. + +- **Automate Lead Scoring**: Use the Implisense API to score leads based on company data. Combine this with your scoring algorithm in a Pipedream workflow to prioritize leads automatically in your sales funnel. + +- **Market Analysis Reports**: On a scheduled basis, run a Pipedream workflow that uses the Implisense API to gather data on industry trends, financial health of sectors, and emerging market players. Use this data to generate insights for strategic planning. diff --git a/components/impression/README.md b/components/impression/README.md index d2a5bb0df2b82..ec9cee9a920ca 100644 --- a/components/impression/README.md +++ b/components/impression/README.md @@ -1,6 +1,11 @@ # Overview -You can use the Impression API to create customized, digital signatures for -your documents. With this API, you can create signatures that include your -name, company name, title, and contact information. You can also choose to -include a logo, photo, or other image in your signature. +The Impression API provides robust solutions for automating compliance tracking and streamlining security processes. Leveraging it on Pipedream enables users to create dynamic, serverless workflows that integrate seamlessly with various services and apps. With this API, you can automate the collection of compliance evidence, monitor security controls, and receive real-time alerts for compliance status changes. This not only saves time but also ensures a consistent and up-to-date overview of your security posture. + +# Example Use Cases + +- **Automated Compliance Reporting**: Set up a workflow on Pipedream where the Impression API periodically fetches compliance status reports, then formats and sends them to stakeholders through email or messaging apps like Slack. This ensures that everyone is kept in the loop regarding the organization's compliance status without manual intervention. + +- **Real-Time Alerting System**: Create a Pipedream workflow that utilizes the Impression API to monitor changes in compliance status. When a potential issue is detected, trigger an alert that sends notifications to a dedicated Slack channel or via SMS through a service like Twilio. This immediate feedback allows for swift action to rectify any compliance issues. + +- **Integration with Project Management Tools**: Construct a workflow where the Impression API updates project management tools like JIRA or Trello with the latest compliance tasks and statuses. As compliance evidence is collected or tasks are completed, this workflow can automatically create and update issues or cards, keeping the whole team informed and ensuring that nothing falls through the cracks. diff --git a/components/impression/package.json b/components/impression/package.json new file mode 100644 index 0000000000000..4e1bc7c1c24f7 --- /dev/null +++ b/components/impression/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/impression", + "version": "0.6.0", + "description": "Pipedream impression Components", + "main": "impression.app.mjs", + "keywords": [ + "pipedream", + "impression" + ], + "homepage": "https://pipedream.com/apps/impression", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/infinity/README.md b/components/infinity/README.md new file mode 100644 index 0000000000000..a4fb053cb0eb6 --- /dev/null +++ b/components/infinity/README.md @@ -0,0 +1,11 @@ +# Overview + +The Infinity API allows you to interact programmatically with Infinity, a flexible project management platform. With the Infinity API on Pipedream, you can automate project and task management, synchronize data across multiple platforms, and streamline workflows by triggering actions within Infinity or responding to events in Infinity. Pipedream's serverless platform enables you to connect Infinity to hundreds of apps with minimal effort, empowering you to build custom integrations and automations that suit your team's needs. + +# Example Use Cases + +- **Sync Infinity Tasks to Google Calendar**: Create events in Google Calendar automatically whenever a new task with a due date is created in Infinity. This ensures that your schedule stays updated with all your project deadlines. + +- **Aggregate Feedback from Typeform**: After receiving new form submissions in Typeform, use Pipedream to parse the responses and create corresponding tasks or notes in Infinity. This can be especially useful for gathering customer feedback or managing survey results. + +- **Post Slack Notifications on Task Completion**: Set up a workflow that listens for task completion events in Infinity, then sends a notification to a designated Slack channel. This keeps the team informed about project progress without manual updates. diff --git a/components/influxdb_cloud/README.md b/components/influxdb_cloud/README.md index eb096fd5affe5..d4dfe8f90d1fe 100644 --- a/components/influxdb_cloud/README.md +++ b/components/influxdb_cloud/README.md @@ -1,9 +1,11 @@ # Overview -The InfluxDB Cloud API allows you to build a variety of applications that -interact with InfluxDB. Some examples of what you can build include: +Harness the power of InfluxDB Cloud API on Pipedream to build robust data workflows. InfluxDB Cloud, a time-series database, is ideal for managing high-velocity data and extracting insights in real-time. On Pipedream, you can easily trigger workflows based on InfluxDB data, automate data ingestion, and connect with countless other services to analyze, visualize, and act upon your data. -- A web interface for InfluxDB -- A monitoring tool for InfluxDB -- A tool to visualize InfluxDB data -- An InfluxDB iOS or Android app +# Example Use Cases + +- **Real-time Alerting System**: Build a workflow that monitors your InfluxDB Cloud data streams for specific conditions or thresholds. When a data point exceeds the preset limits, automatically trigger an alert using Pipedream's integration with apps such as Slack or Twilio, notifying your team instantly for quick response. + +- **IoT Device Data Aggregation**: Create an automation that collects time-series data from IoT devices. Use Pipedream to ingest this data into InfluxDB Cloud for storage and analysis. Further enhance the workflow by integrating with AWS Lambda or Google Cloud Functions to process the data, providing insights like predictive maintenance or usage patterns. + +- **Custom Analytics Dashboard**: Leverage Pipedream to periodically extract key metrics from InfluxDB Cloud and feed them into a business intelligence tool such as Google Sheets or Tableau. This empowers you to present data in a user-friendly format, with the ability to set up custom triggers to refresh the dashboard based on event-driven data updates from InfluxDB. diff --git a/components/influxdb_cloud/package.json b/components/influxdb_cloud/package.json new file mode 100644 index 0000000000000..82eaad0fe91d6 --- /dev/null +++ b/components/influxdb_cloud/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/influxdb_cloud", + "version": "0.6.0", + "description": "Pipedream influxdb_cloud Components", + "main": "influxdb_cloud.app.mjs", + "keywords": [ + "pipedream", + "influxdb_cloud" + ], + "homepage": "https://pipedream.com/apps/influxdb_cloud", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/infobip/README.md b/components/infobip/README.md index 600c440c770eb..64c17fa54824e 100644 --- a/components/infobip/README.md +++ b/components/infobip/README.md @@ -1,10 +1,11 @@ # Overview -The Infobip API can be used to build a variety of applications and services. -Some examples include: - -- A messaging app that allows users to send and receive text messages -- A customer service chatbot that can handle customer inquiries -- An appointment scheduling app that allows users to book appointments with - service providers -- A voice call service that allows users to make and receive calls +The Infobip API is a communication platform that enables seamless integration of messaging, voice, and email functionalities into various applications. With Infobip, you can automate notifications, authenticate users via one-time passwords, engage customers across multiple channels, and track communication performance. Pipedream's serverless execution environment lets you create sophisticated workflows that harness the capabilities of Infobip by triggering actions based on events, manipulating data, and connecting with numerous other apps. + +# Example Use Cases + +- **Customer Support Automation**: When a new ticket is created in Zendesk, use Infobip to send an SMS confirmation to the customer. Automatically escalate unresolved issues by triggering a voice call after a set period, enhancing customer experience and response times. + +- **Multi-channel Marketing Campaigns**: Trigger an Infobip workflow from a Shopify order event. Send personalized SMS messages for order confirmations, then follow up with email campaigns for related products, feedback requests, or loyalty program invites, all orchestrated within Pipedream. + +- **Two-factor Authentication (2FA)**: Implement 2FA by integrating Infobip with a custom authentication system. Generate and send one-time passwords via SMS when a user attempts to log in, and verify the tokens within Pipedream workflows to enhance security across your application. diff --git a/components/infobip/actions/send-sms/send-sms.mjs b/components/infobip/actions/send-sms/send-sms.mjs new file mode 100644 index 0000000000000..8748e4d8a7857 --- /dev/null +++ b/components/infobip/actions/send-sms/send-sms.mjs @@ -0,0 +1,84 @@ +import infobip from "../../infobip.app.mjs"; + +export default { + key: "infobip-send-sms", + name: "Send SMS", + description: "Sends an SMS message to a specified number. [See the documentation](https://www.infobip.com/docs/api)", + version: "0.0.1", + type: "action", + props: { + infobip, + phoneNumber: { + propDefinition: [ + infobip, + "phoneNumber", + ], + optional: true, + }, + text: { + propDefinition: [ + infobip, + "text", + ], + optional: true, + }, + from: { + propDefinition: [ + infobip, + "from", + ], + optional: true, + }, + flash: { + type: "boolean", + label: "FLash", + description: "Allows for sending a flash SMS to automatically appear on recipient devices without interaction.", + optional: true, + }, + notifyUrl: { + type: "string", + label: "Notify URL", + description: "The URL on your call back server on to which a delivery report will be sent. The [retry cycle](https://www.infobip.com/docs/sms/api#notify-url) for when your URL becomes unavailable uses the following formula: `1min + (1min * retryNumber * retryNumber)`.", + optional: true, + }, + entityId: { + propDefinition: [ + infobip, + "entityId", + ], + optional: true, + }, + applicationId: { + propDefinition: [ + infobip, + "applicationId", + ], + optional: true, + }, + }, + async run({ $ }) { + const { + infobip, + phoneNumber, + ...data + } = this; + const response = await infobip.sendSms({ + $, + data: { + messages: [ + { + destinations: [ + { + to: phoneNumber, + }, + ], + data, + }, + ], + }, + }); + + $.export("$summary", response.messages[0].status.description); + return response; + }, +}; diff --git a/components/infobip/actions/send-viber-text-message/send-viber-text-message.mjs b/components/infobip/actions/send-viber-text-message/send-viber-text-message.mjs new file mode 100644 index 0000000000000..d80e396fd3225 --- /dev/null +++ b/components/infobip/actions/send-viber-text-message/send-viber-text-message.mjs @@ -0,0 +1,53 @@ +import infobip from "../../infobip.app.mjs"; + +export default { + key: "infobip-send-viber-text-message", + name: "Send Viber Text Message", + description: "Send a text message to multiple recipients via Viber. [See the documentation](https://www.infobip.com/docs/api/channels/viber/viber-business-messages/send-viber-messages)", + version: "0.0.1", + type: "action", + props: { + infobip, + from: { + propDefinition: [ + infobip, + "from", + ], + }, + to: { + propDefinition: [ + infobip, + "to", + ], + }, + contentText: { + type: "string", + label: "Content Text", + description: "The text content to send in bulk messages.", + }, + }, + async run({ $ }) { + const response = await this.infobip.sendViberMessage({ + $, + data: { + messages: [ + { + sender: this.from, + destinations: [ + { + to: this.to, + }, + ], + content: { + type: "TEXT", + text: this.contentText, + }, + }, + ], + }, + }); + + $.export("$summary", response.messages[0].status.description); + return response; + }, +}; diff --git a/components/infobip/actions/send-whatsapp-text-message/send-whatsapp-text-message.mjs b/components/infobip/actions/send-whatsapp-text-message/send-whatsapp-text-message.mjs new file mode 100644 index 0000000000000..2a961ae73f681 --- /dev/null +++ b/components/infobip/actions/send-whatsapp-text-message/send-whatsapp-text-message.mjs @@ -0,0 +1,58 @@ +import infobip from "../../infobip.app.mjs"; + +export default { + key: "infobip-send-whatsapp-text-message", + name: "Send WhatsApp Text Message", + description: "Sends a WhatsApp text message to a specified number. [See the documentation](https://www.infobip.com/docs/api#channels/whatsapp/send-whatsapp-text-message)", + version: "0.0.1", + type: "action", + props: { + infobip, + from: { + propDefinition: [ + infobip, + "from", + ], + description: "Registered WhatsApp sender number. Must be in international format and comply with [WhatsApp's requirements](https://www.infobip.com/docs/whatsapp/get-started#phone-number-what-you-need-to-know).", + }, + to: { + propDefinition: [ + infobip, + "phoneNumber", + ], + description: "Message recipient number. Must be in international format.", + }, + text: { + propDefinition: [ + infobip, + "text", + ], + }, + messageId: { + propDefinition: [ + infobip, + "messageId", + ], + optional: true, + }, + }, + async run({ $ }) { + const { + infobip, + text, + ...data + } = this; + + const response = await infobip.sendWhatsappMessage({ + $, + data: { + content: { + text, + }, + ...data, + }, + }); + $.export("$summary", response.status.description); + return response; + }, +}; diff --git a/components/infobip/common/constants.mjs b/components/infobip/common/constants.mjs new file mode 100644 index 0000000000000..ea830c15a04cb --- /dev/null +++ b/components/infobip/common/constants.mjs @@ -0,0 +1 @@ +export const LIMIT = 100; diff --git a/components/infobip/infobip.app.mjs b/components/infobip/infobip.app.mjs index 749bdbdbcddff..39a35e784cad2 100644 --- a/components/infobip/infobip.app.mjs +++ b/components/infobip/infobip.app.mjs @@ -1,11 +1,166 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + export default { type: "app", app: "infobip", - propDefinitions: {}, + propDefinitions: { + applicationId: { + type: "string", + label: "Application ID", + description: "Required for application use in a send request for outbound traffic. Returned in notification events. For more details, [see the Infobip documentation](https://www.infobip.com/docs/cpaas-x/application-and-entity-management).", + async options({ page }) { + const { results } = await this.listApplications({ + params: { + page: page, + size: LIMIT, + }, + }); + + return results.map(({ + applicationId: value, applicationName: label, + }) => ({ + label, + value, + })); + }, + }, + entityId: { + type: "string", + label: "Entity Id", + description: "Required for entity use in a send request for outbound traffic. Returned in notification events. For more details, [see the Infobip documentation](https://www.infobip.com/docs/cpaas-x/application-and-entity-management).", + async options({ page }) { + const { results } = await this.listEntities({ + params: { + page: page, + size: LIMIT, + }, + }); + + return results.map(({ + entityId: value, entityName: label, + }) => ({ + label, + value, + })); + }, + }, + resourceKey: { + type: "string", + label: "Resource Key", + description: "Required if `Resource` not present.", + async options({ + page, channel, + }) { + const { results } = await this.listResources({ + params: { + page: page, + size: LIMIT, + channel, + }, + }); + + return results.map(({ resourceId }) => resourceId); + }, + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "Message destination address. Addresses must be in international format (Example: 41793026727).", + }, + text: { + type: "string", + label: "Text", + description: "Content of the message being sent.", + }, + from: { + type: "string", + label: "From", + description: "The sender ID which can be alphanumeric or numeric (e.g., CompanyName). Make sure you don't exceed [character limit](https://www.infobip.com/docs/sms/get-started#sender-names).", + }, + to: { + type: "string", + label: "To", + description: "The destination address of the message.", + }, + messageId: { + type: "string", + label: "Message ID", + description: "The ID that uniquely identifies the message sent via WhatsApp.", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return (this.$auth.base_url.startsWith("https://")) + ? this.$auth.base_url + : `https://${this.$auth.base_url}`; + }, + _headers() { + return { + "Authorization": `App ${this.$auth.api_key}`, + "Content-type": "application/json", + }; + }, + _makeRequest({ + $ = this, path, ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: this._headers(), + }); + }, + listApplications(opts = {}) { + return this._makeRequest({ + path: "/provisioning/1/applications", + ...opts, + }); + }, + listEntities(opts = {}) { + return this._makeRequest({ + path: "/provisioning/1/entities", + ...opts, + }); + }, + listResources(opts = {}) { + return this._makeRequest({ + path: "/provisioning/1/associations", + ...opts, + }); + }, + sendSms(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/sms/2/text/advanced", + ...opts, + }); + }, + sendViberMessage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/viber/2/messages", + ...opts, + }); + }, + sendWhatsappMessage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/whatsapp/1/message/text", + ...opts, + }); + }, + createHook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/resource-management/1/inbound-message-configurations", + ...opts, + }); + }, + deleteHook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/resource-management/1/inbound-message-configurations/${webhookId}`, + }); }, }, }; diff --git a/components/infobip/package.json b/components/infobip/package.json new file mode 100644 index 0000000000000..99b7557839902 --- /dev/null +++ b/components/infobip/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/infobip", + "version": "0.1.0", + "description": "Pipedream Infobip Components", + "main": "infobip.app.mjs", + "keywords": [ + "pipedream", + "infobip" + ], + "homepage": "https://pipedream.com/apps/infobip", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/infobip/sources/common/base.mjs b/components/infobip/sources/common/base.mjs new file mode 100644 index 0000000000000..b09c0dd7bc24f --- /dev/null +++ b/components/infobip/sources/common/base.mjs @@ -0,0 +1,73 @@ +import { ConfigurationError } from "@pipedream/platform"; +import infobip from "../../infobip.app.mjs"; + +export default { + props: { + infobip, + http: "$.interface.http", + db: "$.service.db", + keyword: { + type: "string", + label: "Keyword", + description: "Omitting this value or sending `NULL` will set keyword to `NULL` because it is a valid keyword which will match all values.", + optional: true, + }, + resource: { + type: "string", + label: "Number", + description: "Required if `Number Key` not present.", + optional: true, + }, + }, + methods: { + prepareDate(fieldName) { + let resources = {}; + if (this.resource) { + resources = { + [fieldName]: this.resource, + }; + } else { + resources = { + [`${fieldName}Key`]: this.resourceKey, + }; + } + return resources; + }, + getFieldName() { + return "number"; + }, + }, + hooks: { + async activate() { + const fieldName = this.getFieldName(); + + if (((!this.resource) && (!this.resourceKey)) || ((this.resource) && (this.resourceKey))) { + throw new ConfigurationError(`You must provide either '${fieldName}' or '${fieldName} key'.`); + } + + const { configurationKey } = await this.infobip.createHook({ + data: { + keyword: this.keyword, + channel: this.getChannel(), + forwarding: { + type: "HTTP_FORWARD", + url: this.http.endpoint, + }, + ...this.prepareDate(fieldName), + }, + }); + this.db.set("webhookId", configurationKey); + }, + async deactivate() { + const webhookId = this.db.get("webhookId"); + await this.infobip.deleteHook(webhookId); + }, + }, + async run({ body }) { + this.$emit(body, { + id: body.messageId, + summary: this.getSummary(body), + ts: Date.parse(body.receivedAt), + }); + }, +}; diff --git a/components/infobip/sources/new-sms-message-instant/new-sms-message-instant.mjs b/components/infobip/sources/new-sms-message-instant/new-sms-message-instant.mjs new file mode 100644 index 0000000000000..f2fe59172b319 --- /dev/null +++ b/components/infobip/sources/new-sms-message-instant/new-sms-message-instant.mjs @@ -0,0 +1,37 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "infobip-new-sms-message-instant", + name: "New SMS Message (Instant)", + description: "Emit new event when a new SMS message is received.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + resourceKey: { + propDefinition: [ + common.props.infobip, + "resourceKey", + () => ({ + channel: "SMS", + }), + ], + label: "Number Key", + description: "Required if `Number` not present.", + optional: true, + }, + }, + methods: { + ...common.methods, + getChannel() { + return "SMS"; + }, + getSummary(body) { + return `New SMS from ${body.from}: ${body.text}`; + }, + }, + sampleEmit, +}; diff --git a/components/infobip/sources/new-sms-message-instant/test-event.mjs b/components/infobip/sources/new-sms-message-instant/test-event.mjs new file mode 100644 index 0000000000000..4cc8f942b09f8 --- /dev/null +++ b/components/infobip/sources/new-sms-message-instant/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "messageId": "817790313235066447", + "from": "385916242493", + "to": "385921004026", + "text": "QUIZ Correct answer is Paris", + "cleanText": "Correct answer is Paris", + "keyword": "QUIZ", + "receivedAt": "2016-10-06T09:28:39.220+0000", + "smsCount": 1, + "price": { + "pricePerMessage": 0, + "currency": "EUR" + }, + "callbackData": "callbackData" +} \ No newline at end of file diff --git a/components/infobip/sources/new-whatsapp-message-instant/new-whatsapp-message-instant.mjs b/components/infobip/sources/new-whatsapp-message-instant/new-whatsapp-message-instant.mjs new file mode 100644 index 0000000000000..832620e49b061 --- /dev/null +++ b/components/infobip/sources/new-whatsapp-message-instant/new-whatsapp-message-instant.mjs @@ -0,0 +1,37 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "infobip-new-whatsapp-message-instant", + name: "New Whatsapp Message (Instant)", + description: "Emit new event when a new message is received on Whatsapp.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + resourceKey: { + propDefinition: [ + common.props.infobip, + "resourceKey", + () => ({ + channel: "WHATSAPP", + }), + ], + label: "Number Key", + description: "Required if `Number` not present.", + optional: true, + }, + }, + methods: { + ...common.methods, + getChannel() { + return "WHATSAPP"; + }, + getSummary(body) { + return `New Whatsapp message from ${body.from}: ${body.message.text}`; + }, + }, + sampleEmit, +}; diff --git a/components/infobip/sources/new-whatsapp-message-instant/test-event.mjs b/components/infobip/sources/new-whatsapp-message-instant/test-event.mjs new file mode 100644 index 0000000000000..784c3d2f9612d --- /dev/null +++ b/components/infobip/sources/new-whatsapp-message-instant/test-event.mjs @@ -0,0 +1,18 @@ +export default { + "from": "string", + "to": "string", + "integrationType": "string", + "receivedAt": "2024-04-24T00:16:14Z", + "messageId": "string", + "pairedMessageId": "string", + "callbackData": "string", + "message": { + "type": "TEXT", + "trackingData": "givenTrackingData", + "text": "givenText" + }, + "price": { + "pricePerMessage": 0, + "currency": "string" + } +} \ No newline at end of file diff --git a/components/infolobby/actions/create-comment/create-comment.mjs b/components/infolobby/actions/create-comment/create-comment.mjs new file mode 100644 index 0000000000000..fa30428e69f6b --- /dev/null +++ b/components/infolobby/actions/create-comment/create-comment.mjs @@ -0,0 +1,48 @@ +import app from "../../infolobby.app.mjs"; + +export default { + key: "infolobby-create-comment", + name: "Create Comment", + description: "Create a new Comment for a record. [See the documentation](https://infolobby.com/site/apidocs/4/working-with-record-comments/)", + version: "0.0.1", + type: "action", + props: { + app, + tableId: { + propDefinition: [ + app, + "tableId", + ], + }, + itemId: { + propDefinition: [ + app, + "itemId", + (c) => ({ + tableId: c.tableId, + }), + ], + }, + comment: { + propDefinition: [ + app, + "comment", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createComment({ + $, + tableId: this.tableId, + itemId: this.itemId, + data: { + comment: this.comment, + }, + + }); + + $.export("$summary", `Successfully created Comment with ID '${response}'`); + + return response; + }, +}; diff --git a/components/infolobby/infolobby.app.mjs b/components/infolobby/infolobby.app.mjs new file mode 100644 index 0000000000000..a4f4fac8229a3 --- /dev/null +++ b/components/infolobby/infolobby.app.mjs @@ -0,0 +1,67 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "infolobby", + propDefinitions: { + itemId: { + type: "string", + label: "Item ID", + description: "ID of the Item", + async options({ tableId }) { + const itemIds = await this.listItems({ + tableId, + }); + return itemIds.map(({ item_id }) => ({ + value: item_id, + })); + }, + }, + tableId: { + type: "string", + label: "Table ID", + description: "ID of the Table. You can get it from the developer tab in the table settings at InfoLobby", + }, + comment: { + type: "string", + label: "Comment", + description: "Comment to register in selected the item", + }, + }, + methods: { + _baseUrl() { + return "https://infolobby.com/api"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + async createComment({ + tableId, itemId, ...args + }) { + return this._makeRequest({ + method: "post", + path: `/table/${tableId}/record/${itemId}/comments/create`, + ...args, + }); + }, + async listItems({ + tableId, ...args + }) { + return this._makeRequest({ + path: `/table/${tableId}/records/query`, + ...args, + }); + }, + }, +}; diff --git a/components/infolobby/package.json b/components/infolobby/package.json new file mode 100644 index 0000000000000..eb85b7f7d5d81 --- /dev/null +++ b/components/infolobby/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/infolobby", + "version": "0.1.0", + "description": "Pipedream InfoLobby Components", + "main": "infolobby.app.mjs", + "keywords": [ + "pipedream", + "infolobby" + ], + "homepage": "https://pipedream.com/apps/infolobby", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.6" + } +} diff --git a/components/infusionsoft/README.md b/components/infusionsoft/README.md index aeb38bdb02f3d..f3bc693a04068 100644 --- a/components/infusionsoft/README.md +++ b/components/infusionsoft/README.md @@ -1,21 +1,11 @@ # Overview -The Keap API enables developers to build integrations and applications for -Infusionsoft customers. With the Keap API, you can: +The Keap (Infusionsoft) API is a powerful tool that unlocks advanced customer relationship management, marketing automation, and sales features. It allows you to manage contacts, create and update customer records, send emails, set up marketing campaigns, and more, automating and integrating these processes within your business workflows. With Pipedream's serverless execution environment, you can harness this API to craft custom automations, trigger sequences based on specific actions, and connect Keap to a multitude of other apps and services, streamlining your sales and marketing efforts. -- Query and update data in Infusionsoft -- AutomateInfusionsoft tasks -- Build custom Infusionsoft reports -- Trigger Infusionsoft workflows -- And much more! +# Example Use Cases -Here are some examples of things you can build with the Keap API: +- **Contact Segmentation Workflow**: Sync new contacts from a Google Sheets spreadsheet to Keap. Each time a row is added to the spreadsheet, a Pipedream workflow can trigger, creating or updating a contact in Keap, tagging them based on specified criteria, and enrolling them in the appropriate marketing campaigns. -- A CRM integration that updates Infusionsoft contact records when a new deal - is won in your sales CRM -- A reporting tool that shows Infusionsoft campaign stats in real-time -- An ecommerce integration that automatically adds Infusionsoft contacts to a - campaign when they purchase a product on your site -- A tool that allows Infusionsoft users to schedulegenerate and send mass - personalised email campaigns -- And much more! +- **Lead Qualification and Alerting Workflow**: Connect Keap to Slack for instant notifications. When a new lead fills out a form on your website, Keap can capture their data; Pipedream then assesses the lead's score and sends a message to a dedicated Slack channel if the score is above a certain threshold, alerting your sales team to hot leads. + +- **E-commerce Follow-Up Workflow**: After an order is placed using Shopify, use Pipedream to trigger a follow-up sequence in Keap. The workflow can check the customer's purchase history and, depending on the context (e.g., first-time buyer, repeat customer, high-value purchase), it can initiate personalized post-purchase follow-up sequences, request feedback, or offer loyalty discounts. diff --git a/components/inksprout/README.md b/components/inksprout/README.md index 733bd73a33e13..2632176c4eff8 100644 --- a/components/inksprout/README.md +++ b/components/inksprout/README.md @@ -1,13 +1,11 @@ # Overview -With Inksprout, you can build a variety of applications that allow users to -create and manage their own digital content. Here are some examples of what you -can build with the Inksprout API: - -- A content management system that allows users to create and manage their own - digital content -- A social media platform that allows users to share their digital content with - others -- A marketplace that allows users to buy and sell digital content -- A learning platform that allows users to create and share their own digital - learning materials +Inksprout is a platform that streamlines social media content creation by automatically generating succinct captions and summaries. With the Inksprout API on Pipedream, you can craft workflows that capitalize on its AI-driven content enhancement to elevate your social media management. Automate caption generation, schedule posts for optimal engagement, and analyze content performance all in one seamless pipeline. Harness Inksprout’s capabilities to maintain a consistent and engaging online presence, while freeing up precious time to focus on deeper strategy and content creation. + +# Example Use Cases + +- **Caption Generation for Scheduled Posts**: Create a workflow where blog posts or articles are fed into Inksprout to generate captions. These captions are then automatically scheduled as social media posts via platforms like Twitter or Facebook through their respective APIs. Use Pipedream's cron scheduling to time these posts for peak engagement times. + +- **Social Media Analytics with Automated Summaries**: Connect Inksprout to analytics tools like Google Analytics to receive reports on social media performance. Automatically send article links to Inksprout to get summaries, which can be included in internal performance reports or shared with stakeholders to give quick insights into content engagement. + +- **Content Calendar Automation**: Draft a workflow that uses Inksprout to generate post captions and summaries for a content calendar. Integrate with Google Sheets or Airtable to organize and store these captions along with their post dates, hashtags, and status updates, creating a dynamic and automated content planning system. diff --git a/components/inmobile/README.md b/components/inmobile/README.md new file mode 100644 index 0000000000000..9a141d830617c --- /dev/null +++ b/components/inmobile/README.md @@ -0,0 +1,11 @@ +# Overview + +The InMobile API affords you the ability to automate and integrate SMS messaging services into your business workflows. Through Pipedream, you can craft serverless workflows that leverage these capabilities to interact with customers, send alerts, and integrate with other services for a seamless communication ecosystem. Harnessing the power of the InMobile API on Pipedream lets you send messages, manage contacts, and track message delivery within custom workflows, without the fuss of server maintenance. + +# Example Use Cases + +- **Customer Feedback Collection via SMS and Google Sheets**: Trigger a Pipedream workflow with a new row in a Google Sheet, which contains customer data. Use the InMobile API to send an SMS to these customers inviting feedback. Record the responses back into Google Sheets for data analysis and follow-up. + +- **E-commerce Order Confirmation and Tracking**: Connect the InMobile API to an e-commerce platform like Shopify. When a new order is placed, trigger a workflow that sends an SMS confirmation to the customer. Follow up with delivery status updates via SMS as the order is processed and shipped. + +- **IT Alert System with SMS Notifications**: Pair the InMobile API with monitoring tools such as Datadog. Set up a Pipedream workflow that triggers an SMS alert to IT personnel when a critical system anomaly is detected. This enables rapid response to maintain system uptime and reliability. diff --git a/components/inmobile/package.json b/components/inmobile/package.json index 3e7ae8ec4ceff..7197bea82766e 100644 --- a/components/inmobile/package.json +++ b/components/inmobile/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/inoreader/README.md b/components/inoreader/README.md new file mode 100644 index 0000000000000..21ab998579990 --- /dev/null +++ b/components/inoreader/README.md @@ -0,0 +1,11 @@ +# Overview + +The Inoreader API taps into the functionality of the Inoreader content reader, allowing the automation of tasks like subscribing to new feeds, listing articles, or marking items as read. In Pipedream, this can be leveraged to create custom workflows that integrate with other apps, trigger actions based on new content, or manage content consumption in a more efficient way. + +# Example Use Cases + +- **Automated Content Distribution**: Create a workflow that monitors specific Inoreader folders or tags for new articles and automatically shares them to social media platforms like Twitter or Facebook with predefined hashtags. + +- **Email Digests from Starred Articles**: Set up a Pipedream workflow that collects articles you've starred throughout the day in Inoreader, and sends a neatly formatted digest to your email every evening, ensuring you never miss an important read. + +- **Sync Read Status with Task Managers**: Develop a system where articles marked as read in Inoreader trigger updates in a task management app like Todoist, marking related tasks as complete, keeping your reading and productivity apps in sync. diff --git a/components/insertchat/actions/create-lead/create-lead.mjs b/components/insertchat/actions/create-lead/create-lead.mjs new file mode 100644 index 0000000000000..1d1fce45231fd --- /dev/null +++ b/components/insertchat/actions/create-lead/create-lead.mjs @@ -0,0 +1,77 @@ +import insertchat from "../../insertchat.app.mjs"; + +export default { + key: "insertchat-create-lead", + name: "Create Lead", + description: "Creates a new lead within Insertchat. [See the documentation](https://www.postman.com/gold-star-239225/insertchat/request/uiugp1c/create-a-lead)", + version: "0.0.1", + type: "action", + props: { + insertchat, + chatbotId: { + propDefinition: [ + insertchat, + "chatbotId", + ], + }, + firstName: { + type: "string", + label: "First Name", + description: "First name of the lead", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the lead", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "Email address of the lead", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Phone number of the lead", + optional: true, + }, + address: { + type: "string", + label: "Address", + description: "Address of the lead", + optional: true, + }, + website: { + type: "string", + label: "Website", + description: "Website of the lead", + optional: true, + }, + company: { + type: "string", + label: "Company", + description: "Company of the lead", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.insertchat.createLead({ + $, + data: { + widget_uid: this.chatbotId, + first_name: this.firstName, + last_name: this.lastName, + email: this.email, + phone: this.phone, + address: this.address, + website: this.website, + company: this.company, + }, + }); + $.export("$summary", `Created lead with ID: ${response.uid}`); + return response; + }, +}; diff --git a/components/insertchat/actions/delete-lead/delete-lead.mjs b/components/insertchat/actions/delete-lead/delete-lead.mjs new file mode 100644 index 0000000000000..e908d3437073a --- /dev/null +++ b/components/insertchat/actions/delete-lead/delete-lead.mjs @@ -0,0 +1,26 @@ +import insertchat from "../../insertchat.app.mjs"; + +export default { + key: "insertchat-delete-lead", + name: "Delete Lead", + description: "Deletes an existing lead from InsertChat. [See the documentation](https://www.postman.com/gold-star-239225/insertchat/request/2vgc20j/delete-a-lead)", + version: "0.0.1", + type: "action", + props: { + insertchat, + leadId: { + propDefinition: [ + insertchat, + "leadId", + ], + }, + }, + async run({ $ }) { + const response = await this.insertchat.deleteLead({ + $, + leadId: this.leadId, + }); + $.export("$summary", `Successfully deleted lead with ID: ${this.leadId}`); + return response; + }, +}; diff --git a/components/insertchat/actions/push-message-existing-chat/push-message-existing-chat.mjs b/components/insertchat/actions/push-message-existing-chat/push-message-existing-chat.mjs new file mode 100644 index 0000000000000..996bc0a1fe873 --- /dev/null +++ b/components/insertchat/actions/push-message-existing-chat/push-message-existing-chat.mjs @@ -0,0 +1,55 @@ +import insertchat from "../../insertchat.app.mjs"; + +export default { + key: "insertchat-push-message-existing-chat", + name: "Push Message to Existing Chat", + description: "Pushes a new message into an existing chat session in InsertChat. [See the documentation](https://www.postman.com/gold-star-239225/insertchat/request/me7mcwa/push-a-message-into-a-chat-session)", + version: "0.0.1", + type: "action", + props: { + insertchat, + chatbotId: { + propDefinition: [ + insertchat, + "chatbotId", + ], + }, + chatSessionId: { + propDefinition: [ + insertchat, + "chatSessionId", + (c) => ({ + chatbotId: c.chatbotId, + }), + ], + }, + role: { + type: "string", + label: "Role", + description: "Role to send message as", + options: [ + "user", + "assistant", + ], + }, + message: { + type: "string", + label: "Message Content", + description: "The content of the message to be pushed into the chat session", + }, + }, + run({ $ }) { + // method works, but times out if we await the response + this.insertchat.pushMessage({ + $, + data: new URLSearchParams({ + widget_uid: this.chatbotId, + chat_uid: this.chatSessionId, + role: this.role, + input: this.message, + }), + }); + $.export("$summary", `Successfully pushed message to chat session ${this.chatSessionId}`); + // nothing to return + }, +}; diff --git a/components/insertchat/insertchat.app.mjs b/components/insertchat/insertchat.app.mjs new file mode 100644 index 0000000000000..45b944864f5c7 --- /dev/null +++ b/components/insertchat/insertchat.app.mjs @@ -0,0 +1,162 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "insertchat", + propDefinitions: { + chatbotId: { + type: "string", + label: "Chatbot ID", + description: "The unique identifier for the chatbot", + async options({ page }) { + const { data } = await this.listChatbots({ + params: { + page: page + 1, + }, + }); + return data?.map(({ + uid: value, label, + }) => ({ + value, + label, + })) || []; + }, + }, + leadId: { + type: "string", + label: "Lead ID", + description: "The unique identifier for the lead", + async options({ page }) { + const { data } = await this.listLeads({ + params: { + page: page + 1, + }, + }); + return data?.map(({ + uid: value, first_name: firstName, last_name: lastName, + }) => ({ + value, + label: firstName || lastName + ? (`${firstName} ${lastName}`).trim() + : value, + })) || []; + }, + }, + chatSessionId: { + type: "string", + label: "Chat Session ID", + description: "The unique identifier for the chat session", + async options({ + chatbotId, page, + }) { + const { data } = await this.listChatSessions({ + chatbotId, + page: page + 1, + }); + return data?.map(({ + uid: value, label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _appId() { + return this.$auth.app_uid; + }, + _baseUrl() { + return "https://api.insertchat.com/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + listChatbots(opts = {}) { + return this._makeRequest({ + path: `/${this._appId()}/widgets`, + ...opts, + }); + }, + listLeads(opts = {}) { + return this._makeRequest({ + path: `/${this._appId()}/contacts`, + ...opts, + }); + }, + listChatSessions({ + chatbotId, ...opts + }) { + return this._makeRequest({ + path: `/${this._appId()}/chats/history/${chatbotId}?expand[0]=messages`, + ...opts, + }); + }, + createLead(opts = {}) { + return this._makeRequest({ + method: "POST", + path: `/${this._appId()}/contacts`, + ...opts, + }); + }, + deleteLead({ + leadId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/${this._appId()}/contacts/${leadId}`, + ...opts, + }); + }, + pushMessage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/embeds/messages", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + ...opts, + }); + }, + async *paginate({ + fn, + args, + max, + }) { + args = { + ...args, + params: { + ...args?.params, + page: 1, + }, + }; + let done, count = 0; + do { + const { + data, meta, + } = await fn(args); + for (const item of data) { + yield item; + if (max && ++count >= max) { + return; + } + done = args.params.page === meta.last_page; + args.params.page++; + } + } while (!done); + }, + }, +}; diff --git a/components/insertchat/package.json b/components/insertchat/package.json new file mode 100644 index 0000000000000..7b39139ab935f --- /dev/null +++ b/components/insertchat/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/insertchat", + "version": "0.1.0", + "description": "Pipedream InsertChat Components", + "main": "insertchat.app.mjs", + "keywords": [ + "pipedream", + "insertchat" + ], + "homepage": "https://pipedream.com/apps/insertchat", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/insertchat/sources/common/base.mjs b/components/insertchat/sources/common/base.mjs new file mode 100644 index 0000000000000..30c3e68d735a4 --- /dev/null +++ b/components/insertchat/sources/common/base.mjs @@ -0,0 +1,74 @@ +import insertchat from "../../insertchat.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + insertchat, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + getArgs() { + return {}; + }, + getTsField() { + return "created_at"; + }, + generateMeta(item) { + return { + id: item.uid, + summary: this.getSummary(item), + ts: Date.parse(item[this.getTsField()]), + }; + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + const resourceFn = this.getResourceFn(); + const args = this.getArgs(); + const tsField = this.getTsField(); + + const items = this.insertchat.paginate({ + fn: resourceFn, + args, + max, + }); + + for await (const item of items) { + const ts = Date.parse(item[tsField]); + if (ts > lastTs) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + maxTs = Math.max(ts, maxTs); + } + } + + this._setLastTs(maxTs); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/insertchat/sources/new-ai-chatbot/new-ai-chatbot.mjs b/components/insertchat/sources/new-ai-chatbot/new-ai-chatbot.mjs new file mode 100644 index 0000000000000..3bd9c821f759d --- /dev/null +++ b/components/insertchat/sources/new-ai-chatbot/new-ai-chatbot.mjs @@ -0,0 +1,20 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "insertchat-new-ai-chatbot", + name: "New AI Chatbot", + description: "Emit new event when a new AI chatbot is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.insertchat.listChatbots; + }, + getSummary(item) { + return `New Chatbot ID: ${item.uid}`; + }, + }, +}; diff --git a/components/insertchat/sources/new-chat-session/new-chat-session.mjs b/components/insertchat/sources/new-chat-session/new-chat-session.mjs new file mode 100644 index 0000000000000..49d433e88e9c9 --- /dev/null +++ b/components/insertchat/sources/new-chat-session/new-chat-session.mjs @@ -0,0 +1,34 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "insertchat-new-chat-session", + name: "New Chat Session", + description: "Emit new event when a new chat session is initiated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + chatbotId: { + propDefinition: [ + common.props.insertchat, + "chatbotId", + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.insertchat.listChatSessions; + }, + getArgs() { + return { + chatbotId: this.chatbotId, + }; + }, + getSummary(item) { + return `New Chat Session ID: ${item.uid}`; + }, + }, +}; diff --git a/components/insertchat/sources/new-lead/new-lead.mjs b/components/insertchat/sources/new-lead/new-lead.mjs new file mode 100644 index 0000000000000..17ab6f9ceeffe --- /dev/null +++ b/components/insertchat/sources/new-lead/new-lead.mjs @@ -0,0 +1,20 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "insertchat-new-lead", + name: "New Lead Created", + description: "Emit new event when a new lead is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.insertchat.listLeads; + }, + getSummary(item) { + return `New Lead ID: ${item.uid}`; + }, + }, +}; diff --git a/components/insightly/README.md b/components/insightly/README.md index 123ecb64fc44d..88a84d92731a8 100644 --- a/components/insightly/README.md +++ b/components/insightly/README.md @@ -1,9 +1,11 @@ # Overview -Insightly is a CRM software that helps businesses manage their customer -relationships. It offers a wide range of features including contact management, -lead capture, task management, deals, projects, and reporting. +Insightly's API offers a suite of functions that let you tap into your customer relationship management in real-time, integrating customer data, sales, and project info across a diverse array of business applications and workflows. With Pipedream, you can automate interactions with your Insightly account, sync data effortlessly, and connect to hundreds of apps to streamline tasks, from lead management to project tracking. -The Insightly API allows developers to access and integrate the functionality -of Insightly with other applications. Some example API methods include managing -contacts, managing deals, and retrieving project information. +# Example Use Cases + +- **Automated Lead Capture to Insightly from Multiple Sources**: Trigger a Pipedream workflow when a new lead comes in from web forms, chatbots, or emails. Use this data to create or update leads in Insightly, ensuring that your CRM is always up-to-date with the freshest lead information. + +- **Email Campaign Response Tracking**: Monitor campaign metrics from your preferred email marketing platform (like Mailchimp) in Pipedream. Use the Insightly API to log these interactions against the appropriate contact or lead, giving sales teams instant insights into engagement levels. + +- **Project Management Sync-Up**: Keep project details in lockstep between Insightly and your project management tool (e.g., Trello or Asana). When a project is updated in Insightly, automatically reflect changes on the corresponding project board, keeping all stakeholders aligned with the latest project status. diff --git a/components/insighto_ai/README.md b/components/insighto_ai/README.md new file mode 100644 index 0000000000000..9977b137a6e87 --- /dev/null +++ b/components/insighto_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +Insighto.ai offers a powerful API for tapping into advanced analytics and AI capabilities. With Insighto.ai, you can enrich your business data with AI-driven insights, perform sentiment analysis, and automate responses based on customer interaction patterns. Leveraging this API on Pipedream allows you to integrate these capabilities seamlessly with other apps to create dynamic, data-driven workflows that can automate processes, enhance customer interactions, and drive decision-making with real-time analytics. + +# Example Use Cases + +- **Customer Feedback Analysis and Notification**: Automatically analyze customer feedback submitted via various platforms using Insighto.ai's sentiment analysis. Based on the sentiment, generate and send detailed reports or alerts to Slack to keep your team updated on customer sentiment trends. + +- **Enhanced CRM Data**: Integrate Insighto.ai with Salesforce on Pipedream. Automatically enrich CRM entries with AI-generated insights about customer behavior and preferences, enabling more personalized marketing and sales strategies. + +- **Real-time Support Ticket Categorization**: Use Insighto.ai to analyze and categorize support tickets by urgency and sentiment as they are created. Integrate with Zendesk to automatically route tickets based on their categorization, improving response times and support quality. diff --git a/components/insighto_ai/actions/add-text-blob/add-text-blob.mjs b/components/insighto_ai/actions/add-text-blob/add-text-blob.mjs new file mode 100644 index 0000000000000..05ee99152fadd --- /dev/null +++ b/components/insighto_ai/actions/add-text-blob/add-text-blob.mjs @@ -0,0 +1,88 @@ +import app from "../../insighto_ai.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "insighto_ai-add-text-blob", + name: "Add Text Blob", + description: "Adds a text blob into an existing data source. [See the documentation](https://api.insighto.ai/docs#/datasource/add_datasourcefile_text_blob_api_v1_datasource__datasource_id__text_blob_post)", + version: "0.0.1", + type: "action", + props: { + app, + dataSourceId: { + propDefinition: [ + app, + "dataSourceId", + ], + }, + dataSourceType: { + propDefinition: [ + app, + "dataSourceType", + ({ dataSourceId }) => ({ + dataSourceId, + }), + ], + }, + name: { + type: "string", + label: "Name", + description: "The name of the text blob.", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "The description of the text blob.", + optional: true, + }, + orgId: { + optional: true, + propDefinition: [ + app, + "orgId", + ], + }, + data: { + type: "object", + label: "Attributes", + description: "The attributes of the text blob.", + optional: true, + }, + }, + methods: { + addTextBlob({ + dataSourceId, ...args + } = {}) { + return this.app.post({ + path: `/datasource/${dataSourceId}/text_blob`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + addTextBlob, + dataSourceId, + dataSourceType, + name, + description, + orgId, + data, + } = this; + + const response = await addTextBlob({ + $, + dataSourceId, + params: { + ds_type: dataSourceType, + name, + description, + org_id: orgId, + }, + data: utils.parse(data), + }); + $.export("$summary", `Successfully added text blob with ID \`${response.data?.id}\``); + return response; + }, +}; diff --git a/components/insighto_ai/actions/create-contact/create-contact.mjs b/components/insighto_ai/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..a605053bfbc7b --- /dev/null +++ b/components/insighto_ai/actions/create-contact/create-contact.mjs @@ -0,0 +1,107 @@ +import app from "../../insighto_ai.app.mjs"; + +export default { + key: "insighto_ai-create-contact", + name: "Create Contact", + description: "Creates a new contact within the system. [See the documentation](https://api.insighto.ai/docs#/contact/create_contact_api_v1_contact_post)", + version: "0.0.1", + type: "action", + props: { + app, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the contact.", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the contact.", + }, + email: { + type: "string", + label: "Email", + description: "The email address of the contact.", + }, + orgId: { + optional: true, + propDefinition: [ + app, + "orgId", + ], + }, + firstAssistantId: { + label: "First Assistant ID", + description: "The ID of the first assistant.", + optional: true, + propDefinition: [ + app, + "assistantId", + ], + }, + lastAssistantId: { + label: "Last Assistant ID", + description: "The ID of the last assistant.", + optional: true, + propDefinition: [ + app, + "assistantId", + ], + }, + firstWidgetId: { + label: "First Widget ID", + description: "The ID of the first widget.", + optional: true, + propDefinition: [ + app, + "widgetId", + ], + }, + lastWidgetId: { + label: "Last Widget ID", + description: "The ID of the last widget.", + optional: true, + propDefinition: [ + app, + "widgetId", + ], + }, + }, + methods: { + createContact(args = {}) { + return this.app.post({ + path: "/contact", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createContact, + firstName, + lastName, + email, + orgId, + firstAssistantId, + lastAssistantId, + firstWidgetId, + lastWidgetId, + } = this; + + const response = await createContact({ + $, + data: { + first_name: firstName, + last_name: lastName, + email, + org_id: orgId, + first_assistant_id: firstAssistantId, + last_assistant_id: lastAssistantId, + first_widget_id: firstWidgetId, + last_widget_id: lastWidgetId, + }, + }); + $.export("$summary", `Successfully created contact with ID \`${response.data?.id}\``); + return response; + }, +}; diff --git a/components/insighto_ai/common/constants.mjs b/components/insighto_ai/common/constants.mjs new file mode 100644 index 0000000000000..937b2cc3cdd11 --- /dev/null +++ b/components/insighto_ai/common/constants.mjs @@ -0,0 +1,11 @@ +const BASE_URL = "https://api.insighto.ai"; +const VERSION_PATH = "/api/v1"; +const LAST_CREATED_AT = "lastCreatedAt"; +const DEFAULT_MAX = 600; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_MAX, + LAST_CREATED_AT, +}; diff --git a/components/insighto_ai/common/utils.mjs b/components/insighto_ai/common/utils.mjs new file mode 100644 index 0000000000000..22064a1364b96 --- /dev/null +++ b/components/insighto_ai/common/utils.mjs @@ -0,0 +1,39 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function emptyStrToUndefined(value) { + const trimmed = typeof(value) === "string" && value.trim(); + return trimmed === "" + ? undefined + : value; +} + +function parse(value) { + const valueToParse = emptyStrToUndefined(value); + if (typeof(valueToParse) === "object" || valueToParse === undefined) { + return valueToParse; + } + try { + return JSON.parse(valueToParse); + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid object"); + } +} + +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +function getNestedProperty(obj, propertyString) { + const properties = propertyString.split("."); + return properties.reduce((prev, curr) => prev && prev[curr], obj); +} + +export default { + parse, + iterate, + getNestedProperty, +}; diff --git a/components/insighto_ai/insighto_ai.app.mjs b/components/insighto_ai/insighto_ai.app.mjs new file mode 100644 index 0000000000000..2345f026323ff --- /dev/null +++ b/components/insighto_ai/insighto_ai.app.mjs @@ -0,0 +1,210 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; + +export default { + type: "app", + app: "insighto_ai", + propDefinitions: { + assistantId: { + type: "string", + label: "Assistant ID", + description: "The ID of the assistant.", + async options({ page }) { + const { data: { items } } = await this.listAssistants({ + params: { + page: page + 1, + }, + }); + return items.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + widgetId: { + type: "string", + label: "Widget ID", + description: "The ID of the widget.", + async options({ page }) { + const { data: { items } } = await this.listWidgets({ + params: { + page: page + 1, + }, + }); + return items.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + orgId: { + type: "string", + label: "Organization ID", + description: "The ID of the organization.", + async options({ page }) { + const { data: { items } } = await this.listUserOrgs({ + params: { + page: page + 1, + }, + }); + return items.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + dataSourceId: { + type: "string", + label: "Data Source ID", + description: "The ID of the target data source.", + async options({ page }) { + const { data: { items } } = await this.listDataSources({ + params: { + page: page + 1, + }, + }); + return items.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + dataSourceType: { + type: "string", + label: "Data Source Type", + description: "The type of the data source.", + async options({ dataSourceId }) { + const { data: { ds_type: dsType } } = await this.getDataSource({ + dataSourceId, + }); + return [ + dsType, + ]; + }, + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "accept": "application/json", + }; + }, + getAuthParams(params) { + return { + ...params, + "api_key": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, headers, params, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + params: this.getAuthParams(params), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + listAssistants(args = {}) { + return this._makeRequest({ + path: "/assistant/list", + ...args, + }); + }, + listWidgets(args = {}) { + return this._makeRequest({ + path: "/widget/list", + ...args, + }); + }, + listUserOrgs(args = {}) { + return this._makeRequest({ + path: "/user/list/orgs", + ...args, + }); + }, + listDataSources(args = {}) { + return this._makeRequest({ + path: "/datasource/list", + ...args, + }); + }, + getDataSource({ + dataSourceId, ...args + } = {}) { + return this._makeRequest({ + path: `/datasource/${dataSourceId}`, + ...args, + }); + }, + listContacts(args = {}) { + return this._makeRequest({ + path: "/contact/list", + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, resourceName, + max = constants.DEFAULT_MAX, + }) { + let page = 1; + let resourcesCount = 0; + + while (true) { + const response = + await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + page, + }, + }); + + const nextResources = utils.getNestedProperty(response, resourceName); + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + yield resource; + resourcesCount += 1; + + if (resourcesCount >= max) { + return; + } + } + + if (resourcesCount >= response.total) { + console.log("There are no more resources to fetch"); + return; + } + + page += 1; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); + }, + }, +}; diff --git a/components/insighto_ai/package.json b/components/insighto_ai/package.json new file mode 100644 index 0000000000000..5cd5205974819 --- /dev/null +++ b/components/insighto_ai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/insighto_ai", + "version": "0.1.0", + "description": "Pipedream Insighto.ai Components", + "main": "insighto_ai.app.mjs", + "keywords": [ + "pipedream", + "insighto_ai" + ], + "homepage": "https://pipedream.com/apps/insighto_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "1.6.5" + } +} diff --git a/components/insighto_ai/sources/common/polling.mjs b/components/insighto_ai/sources/common/polling.mjs new file mode 100644 index 0000000000000..902c28c4fb60d --- /dev/null +++ b/components/insighto_ai/sources/common/polling.mjs @@ -0,0 +1,59 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../insighto_ai.app.mjs"; +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getResourceName() { + throw new ConfigurationError("getResourceName is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + processResources(resources) { + Array.from(resources) + .reverse() + .forEach(this.processResource); + }, + }, + async run({ $ }) { + const { + app, + getResourcesFn, + getResourcesFnArgs, + getResourceName, + processResources, + } = this; + + const resources = await app.paginate({ + resourcesFn: getResourcesFn(), + resourcesFnArgs: getResourcesFnArgs($), + resourceName: getResourceName(), + }); + + processResources(resources); + }, +}; diff --git a/components/insighto_ai/sources/new-contact/new-contact.mjs b/components/insighto_ai/sources/new-contact/new-contact.mjs new file mode 100644 index 0000000000000..bdd9375f12d5c --- /dev/null +++ b/components/insighto_ai/sources/new-contact/new-contact.mjs @@ -0,0 +1,32 @@ +import common from "../common/polling.mjs"; + +export default { + ...common, + key: "insighto_ai-new-contact", + name: "New Contact Created", + description: "Emit new event when a new contact is created in Insighto AI. [See the documentation](https://api.insighto.ai/docs#/contact/read_contact_list_api_v1_contact_list_get)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceName() { + return "data.items"; + }, + getResourcesFn() { + return this.app.listContacts; + }, + getResourcesFnArgs($) { + return { + $, + }; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Contact: ${resource.email}`, + ts: Date.parse(resource.created_at), + }; + }, + }, +}; diff --git a/components/insites/README.md b/components/insites/README.md new file mode 100644 index 0000000000000..1207bc8786150 --- /dev/null +++ b/components/insites/README.md @@ -0,0 +1,11 @@ +# Overview + +The Insites API offers a suite of tools for website testing and monitoring, enabling users to automate the process of checking website quality, performance, and compliance with SEO and accessibility standards. With Pipedream, you can harness this API to create workflows that trigger on events across your apps, perform actions based on website analysis results, and automate repetitive tasks that ensure your website maintains high standards for your users. + +# Example Use Cases + +- **Automated Website Health Report**: Generate a weekly website health report using Insites API. The workflow can be scheduled to run regularly on Pipedream, analyzing your site's SEO, performance, and accessibility. Results could then be sent to your email or saved to Google Sheets for record-keeping and further analysis. + +- **Real-Time SEO Alerts**: Set up a Pipedream workflow that uses Insites API to monitor your website for SEO optimizations. Whenever a change is detected that could impact your search engine rankings, the workflow can instantly alert you via Slack or another messaging platform, allowing you to take immediate action. + +- **Compliance Status Dashboard**: Create a dashboard using Pipedream and the Insites API that checks your website for compliance with various standards, such as GDPR or web accessibility guidelines. The workflow can update the dashboard in real-time, hosted on a service like AWS S3, providing a go-to resource for your team to monitor compliance status. diff --git a/components/instabot_chatbot_platform/README.md b/components/instabot_chatbot_platform/README.md index 3c05b752d7808..ac7016fe7732a 100644 --- a/components/instabot_chatbot_platform/README.md +++ b/components/instabot_chatbot_platform/README.md @@ -1,4 +1,11 @@ # Overview -With the Instabot API, you can build bots that can automate tasks on Instagram, -such as liking and following users, commenting on posts, and more. +The Instabot API offers the ability to programmatically interact with the Instabot platform, enabling the automation of chatbot conversations and data. By leveraging this API with Pipedream, you can create powerful workflows to enhance user engagement, collect insightful data, and streamline communications. It supports various operations like sending messages, managing users, and retrieving conversation logs, which can be harnessed for targeted marketing, customer support automation, and real-time analytics. + +# Example Use Cases + +- **Lead Qualification and CRM Integration**: Automatically capture leads from Instabot conversations and add them to a CRM like Salesforce or HubSpot. You can filter and score leads based on conversation data and user responses, ensuring only qualified leads are sent to your sales team. + +- **Support Ticket Creation**: Convert customer queries received through Instabot into support tickets in helpdesk systems like Zendesk or Freshdesk. Analyze the conversation's context to set the priority and assign the ticket to the correct support team or individual. + +- **Event Registration and Follow-up**: Use Instabot to facilitate event registration. Following a successful registration conversation, trigger a sequence of actions such as adding the user to an email campaign in Mailchimp, sending a calendar invite via Google Calendar, and following up with tailored messages post-event. diff --git a/components/instabot_chatbot_platform/package.json b/components/instabot_chatbot_platform/package.json new file mode 100644 index 0000000000000..2c012dff28f95 --- /dev/null +++ b/components/instabot_chatbot_platform/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/instabot_chatbot_platform", + "version": "0.6.0", + "description": "Pipedream instabot_chatbot_platform Components", + "main": "instabot_chatbot_platform.app.mjs", + "keywords": [ + "pipedream", + "instabot_chatbot_platform" + ], + "homepage": "https://pipedream.com/apps/instabot_chatbot_platform", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/instagram_business/README.md b/components/instagram_business/README.md new file mode 100644 index 0000000000000..455f7db00e63c --- /dev/null +++ b/components/instagram_business/README.md @@ -0,0 +1,11 @@ +# Overview + +The Instagram for Business API on Pipedream allows you to automate interactions with your Instagram business account. You can manage posts, comments, messages, and get insights into your audience and post performance. Pipedream's serverless platform simplifies the process of setting up these integrations, allowing you to create complex workflows without managing any infrastructure. + +# Example Use Cases + +- **Automated Content Posting**: Schedule and auto-publish content to your Instagram Business account. You can design a workflow that takes posts from a content calendar - Perhaps stored in Google Sheets - And publishes them on your Instagram at the best times for engagement. + +- **Message and Comment Moderation**: Keep your community healthy by moderating comments and messages. Use sentiment analysis to flag negative comments or spam and automatically respond or hide them. You could harness the power of a natural language processing API like Google's Perspective API to assess the tone of the content. + +- **Instagram Insights to Dashboard**: Aggregate insights and analytics from your Instagram Business account and send them to a dashboard. A workflow might fetch metrics like engagement rates and follower growth, then push this data to a tool like Google Data Studio for visualization and monitoring. diff --git a/components/instant/instant.app.mjs b/components/instant/instant.app.mjs new file mode 100644 index 0000000000000..08bd3e054823f --- /dev/null +++ b/components/instant/instant.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "instant", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/instant/package.json b/components/instant/package.json new file mode 100644 index 0000000000000..c28a63f3428c3 --- /dev/null +++ b/components/instant/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/instant", + "version": "0.0.1", + "description": "Pipedream Instant Components", + "main": "instant.app.mjs", + "keywords": [ + "pipedream", + "instant" + ], + "homepage": "https://pipedream.com/apps/instant", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/instantly/actions/add-lead-campaign/add-lead-campaign.mjs b/components/instantly/actions/add-lead-campaign/add-lead-campaign.mjs new file mode 100644 index 0000000000000..d85fc5392ffba --- /dev/null +++ b/components/instantly/actions/add-lead-campaign/add-lead-campaign.mjs @@ -0,0 +1,50 @@ +import { parseObject } from "../../common/utils.mjs"; +import instantly from "../../instantly.app.mjs"; + +export default { + key: "instantly-add-lead-campaign", + name: "Add Lead to Campaign", + description: "Adds a lead to a campaign for tracking or further actions. [See the documentation](https://developer.instantly.ai/lead/add-leads-to-a-campaign)", + version: "0.0.1", + type: "action", + props: { + instantly, + leads: { + propDefinition: [ + instantly, + "leads", + ], + }, + campaignId: { + propDefinition: [ + instantly, + "campaignId", + ], + }, + skipIfInWorkspace: { + type: "boolean", + label: "Skip if in Workspace", + description: "Skip lead if it exists in any campaigns in the workspace", + optional: true, + }, + skipIfInCampaign: { + type: "boolean", + label: "Skip if in Campaign", + description: "Skip lead if it exists in the campaign", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.instantly.addLeadsToCampaign({ + $, + data: { + leads: parseObject(this.leads), + campaign_id: this.campaignId, + skip_if_in_workspace: this.skipIfInWorkspace, + skip_if_in_campaign: this.skipIfInCampaign, + }, + }); + $.export("$summary", `Added ${response.leads_uploaded} leads to campaign ${this.campaignId}`); + return response; + }, +}; diff --git a/components/instantly/actions/add-tags-campaign/add-tags-campaign.mjs b/components/instantly/actions/add-tags-campaign/add-tags-campaign.mjs new file mode 100644 index 0000000000000..6b59814d975ef --- /dev/null +++ b/components/instantly/actions/add-tags-campaign/add-tags-campaign.mjs @@ -0,0 +1,40 @@ +import { parseObject } from "../../common/utils.mjs"; +import instantly from "../../instantly.app.mjs"; + +export default { + key: "instantly-add-tags-campaign", + name: "Add Tags to Campaign", + description: "Adds tags to a specific campaign. [See the documentation](https://developer.instantly.ai/tags/assign-or-unassign-a-tag)", + version: "0.0.1", + type: "action", + props: { + instantly, + campaignIds: { + propDefinition: [ + instantly, + "campaignId", + ], + type: "string[]", + }, + tagIds: { + propDefinition: [ + instantly, + "tagIds", + ], + }, + }, + async run({ $ }) { + const response = await this.instantly.addTagsToCampaign({ + $, + data: { + campaign_id: this.campaignId, + tag_ids: parseObject(this.tagIds), + resource_type: 2, + assign: true, + resource_ids: parseObject(this.campaignIds), + }, + }); + $.export("$summary", response.message); + return response; + }, +}; diff --git a/components/instantly/actions/update-lead-status/update-lead-status.mjs b/components/instantly/actions/update-lead-status/update-lead-status.mjs new file mode 100644 index 0000000000000..db5bf892e2042 --- /dev/null +++ b/components/instantly/actions/update-lead-status/update-lead-status.mjs @@ -0,0 +1,47 @@ +import { ConfigurationError } from "@pipedream/platform"; +import instantly from "../../instantly.app.mjs"; + +export default { + key: "instantly-update-lead-status", + name: "Update Lead Status", + description: "Updates the status of a lead in a campaign. [See the documentation](https://developer.instantly.ai/lead/update-lead-status)", + version: "0.0.1", + type: "action", + props: { + instantly, + campaignId: { + propDefinition: [ + instantly, + "campaignId", + ], + }, + email: { + propDefinition: [ + instantly, + "email", + ], + }, + newStatus: { + propDefinition: [ + instantly, + "newStatus", + ], + }, + }, + async run({ $ }) { + try { + const response = await this.instantly.updateLeadStatus({ + $, + data: { + email: this.email, + new_status: this.newStatus, + campaign_id: this.campaignId, + }, + }); + $.export("$summary", `Updated lead ${this.email} to status '${this.newStatus}'`); + return response; + } catch ({ response }) { + throw new ConfigurationError(response.data.error); + } + }, +}; diff --git a/components/instantly/common/constants.mjs b/components/instantly/common/constants.mjs new file mode 100644 index 0000000000000..b0e103c9c6726 --- /dev/null +++ b/components/instantly/common/constants.mjs @@ -0,0 +1,77 @@ +export const LIMIT = 100; + +export const EVENT_TYPE_OPTIONS = [ + { + label: "Email Sent", + value: "email_sent", + }, + { + label: "Email Bounced", + value: "email_bounced", + }, + { + label: "Email Opened", + value: "email_opened", + }, + { + label: "Email Link Clicked", + value: "email_link_clicked", + }, + { + label: "Reply Received", + value: "reply_received", + }, + { + label: "Lead Unsubscribed", + value: "lead_unsubscribed", + }, + { + label: "Campaign Completed", + value: "campaign_completed", + }, + { + label: "Account Error", + value: "account_error", + }, + { + label: "Lead Not Interested", + value: "lead_not_interested", + }, + { + label: "Lead Neutral", + value: "lead_neutral", + }, + { + label: "Lead Meeting Booked", + value: "lead_meeting_booked", + }, + { + label: "Lead Meeting Completed", + value: "lead_meeting_completed", + }, + { + label: "Lead Closed", + value: "lead_closed", + }, + { + label: "Lead Out of Office", + value: "lead_out_of_office", + }, + { + label: "Lead Wrong Person", + value: "lead_wrong_person", + }, +]; + +export const NEW_STATUS_OPTIONS = [ + "Active", + "Completed", + "Unsubscribed", + "Interested", + "Meeting Booked", + "Meeting Completed", + "Closed", + "Out of Office", + "Not Interested", + "Wrong Person", +]; diff --git a/components/instantly/common/utils.mjs b/components/instantly/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/instantly/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/instantly/instantly.app.mjs b/components/instantly/instantly.app.mjs new file mode 100644 index 0000000000000..2877295fecddc --- /dev/null +++ b/components/instantly/instantly.app.mjs @@ -0,0 +1,152 @@ +import { axios } from "@pipedream/platform"; +import { + EVENT_TYPE_OPTIONS, + LIMIT, + NEW_STATUS_OPTIONS, +} from "./common/constants.mjs"; + +export default { + type: "app", + app: "instantly", + propDefinitions: { + campaignId: { + type: "string", + label: "Campaign ID", + description: "The ID of the campaign", + async options({ page }) { + const campaigns = await this.listCampaigns({ + params: { + limit: LIMIT, + skip: LIMIT * page, + }, + }); + return campaigns.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + tagIds: { + type: "string[]", + label: "Tags Id", + description: "List of tag Ids to add", + async options({ page }) { + const { data } = await this.listTags({ + params: { + limit: LIMIT, + skip: LIMIT * page, + }, + }); + return data.map(({ + id: value, label, + }) => ({ + label, + value, + })); + }, + }, + leads: { + type: "string[]", + label: "Leads", + description: "An array of lead objects to add to the campaign. **Example: [{ \"email\":\"john2@abc.com\", \"first_name\":\"John\", \"last_name\":\"Doe\", \"company_name\":\"Instantly\", \"personalization\":\"Loved your latest post\", \"phone\":\"123456789\", \"website\":\"instantly.ai\", \"custom_variables\":{ \"favorite_restaurant\":\"Chipotle\", \"language\":\"English\"}}]**", + }, + skipIfInWorkspace: { + type: "boolean", + label: "Skip if in Workspace", + description: "Skip lead if it exists in any campaigns in the workspace", + optional: true, + }, + skipIfInCampaign: { + type: "boolean", + label: "Skip if in Campaign", + description: "Skip lead if it exists in the campaign", + optional: true, + }, + eventType: { + type: "string", + label: "Event Type", + description: "Type of event to filter", + options: EVENT_TYPE_OPTIONS, + }, + email: { + type: "string", + label: "Lead Email", + description: "Email address of the lead", + }, + newStatus: { + type: "string", + label: "New Status", + description: "New status to assign to the lead", + options: NEW_STATUS_OPTIONS, + }, + }, + methods: { + _baseUrl() { + return "https://api.instantly.ai/api/v1"; + }, + _params(params = {}) { + return { + ...params, + api_key: `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, params, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + params: this._params(params), + ...opts, + }); + }, + listCampaigns(opts = {}) { + return this._makeRequest({ + path: "/campaign/list", + ...opts, + }); + }, + listTags(opts = {}) { + return this._makeRequest({ + path: "/custom-tag", + ...opts, + }); + }, + addTagsToCampaign(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/custom-tag/toggle-tag-resource", + ...opts, + }); + }, + addLeadsToCampaign(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/lead/add", + ...opts, + }); + }, + updateLeadStatus(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/lead/update/status", + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhook/subscribe", + ...opts, + }); + }, + deleteWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhook/unsubscribe", + ...opts, + }); + }, + }, +}; diff --git a/components/instantly/package.json b/components/instantly/package.json new file mode 100644 index 0000000000000..cca202a1b1e66 --- /dev/null +++ b/components/instantly/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/instantly", + "version": "0.1.0", + "description": "Pipedream Instantly Components", + "main": "instantly.app.mjs", + "keywords": [ + "pipedream", + "instantly" + ], + "homepage": "https://pipedream.com/apps/instantly", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/instantly/sources/new-event-instant/new-event-instant.mjs b/components/instantly/sources/new-event-instant/new-event-instant.mjs new file mode 100644 index 0000000000000..e6bed8eda4e29 --- /dev/null +++ b/components/instantly/sources/new-event-instant/new-event-instant.mjs @@ -0,0 +1,67 @@ +import instantly from "../../instantly.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "instantly-new-event-instant", + name: "New Event in Instantly (Instant)", + description: "Emit new event when an activity occurs in your Instantly workspace.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + instantly, + http: "$.interface.http", + db: "$.service.db", + campaignId: { + propDefinition: [ + instantly, + "campaignId", + ], + optional: true, + }, + eventType: { + propDefinition: [ + instantly, + "eventType", + ], + optional: true, + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + }, + hooks: { + async activate() { + const response = await this.instantly.createWebhook({ + data: { + hookUrl: this.http.endpoint, + event_type: this.eventType, + campaign: this.campaignId, + }, + }); + this._setHookId(response.id); + }, + async deactivate() { + const webhookId = this._getHookId(); + await this.instantly.deleteWebhook({ + data: { + hook_id: webhookId, + }, + }); + }, + }, + async run({ body }) { + const ts = Date.parse(new Date()); + this.$emit(body, { + id: `${body.resource}-${ts}`, + summary: `New event from ${body.lead_email} for campaign ${body.campaign_name}`, + ts: ts, + }); + }, + sampleEmit, +}; diff --git a/components/instantly/sources/new-event-instant/test-event.mjs b/components/instantly/sources/new-event-instant/test-event.mjs new file mode 100644 index 0000000000000..da174700f93f5 --- /dev/null +++ b/components/instantly/sources/new-event-instant/test-event.mjs @@ -0,0 +1,18 @@ +export default { + "timestamp": "2025-01-08T22:06:16.129Z", + "event_type": "lead_not_interested", + "workspace": "c1b30c69-7fcd-88df-e1151d8f7ec7", + "campaign_id": "c7c1103b-0185-ba08-e925bc5ca574", + "unibox_url": null, + "campaign_name": "My Campaign", + "lead_email": "john@abc.com", + "email": "john@abc.com", + "phone": "123456789", + "website": "instantly.ai", + "language": "English", + "lastName": "Doe", + "firstName": "John", + "companyName": "Instantly", + "personalization": "Loved your latest post", + "favorite_restaurant": "Chipotle" +} \ No newline at end of file diff --git a/components/instapaper/README.md b/components/instapaper/README.md index 6ccd3c1b9a3d5..ef6505d5ebdc9 100644 --- a/components/instapaper/README.md +++ b/components/instapaper/README.md @@ -1,9 +1,11 @@ # Overview -With the Instapaper API, you can build applications that: +The Instapaper API offers a streamlined way to interact with saved articles. With it, you can automate your reading list management by adding new items, moving through folders, or marking them as read or unread. Integrating the Instapaper API with Pipedream lets you create custom workflows, triggering actions in Instapaper or other apps based on specific conditions or schedules. -- Fetch a list of articles saved by a user -- Add new articles to a user's saved list -- Remove articles from a user's saved list -- Retrieve the contents of an article -- Update the title or URL of an article +# Example Use Cases + +- **Content Digest Delivery**: Combine Instapaper with an email service like SendGrid on Pipedream. Automate a weekly digest by collecting articles saved in Instapaper over the week and emailing the list to yourself or your team. + +- **Read-Later Archive**: Integrate Instapaper with cloud storage services like Dropbox. Automatically archive articles marked as read in Instapaper to a dedicated Dropbox folder for long-term storage and retrieval. + +- **Social Media Sharing**: Link Instapaper with Twitter through Pipedream. Whenever you highlight a passage in an article on Instapaper, automatically tweet the quote with a link to the article, sharing insightful snippets with your followers in real time. diff --git a/components/intellexer_api/README.md b/components/intellexer_api/README.md new file mode 100644 index 0000000000000..6a7c00aa65f24 --- /dev/null +++ b/components/intellexer_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Intellexer API offers a suite of linguistic and semantic analysis tools that can enhance text-based applications. With it, you can extract meaning, relations, and facts from the text, enabling smarter data management and decision-making processes. When paired with Pipedream's serverless execution model, the Intellexer API can be used to automate content analysis, enhance search functionalities, and preprocess data for more complex workflows. + +# Example Use Cases + +- **Semantic Search Enhancement**: Integrate Intellexer API's semantic analysis with your database searches. Process user queries through Intellexer to grasp the semantic meaning, and then use those insights to fetch more relevant results from your database app on Pipedream, like PostgreSQL or MongoDB. + +- **Automated Content Summarization**: Connect Intellexer API with a content creation platform, such as WordPress or Contentful. Automatically summarize articles, blog posts, or documents as they are published, providing readers with quick, digestible summaries generated through Pipedream workflows. + +- **Sentiment Analysis Feedback Loop**: Use Intellexer API to gauge customer sentiment by analyzing feedback from a customer support platform like Zendesk. Automate the process in Pipedream to categorize tickets by sentiment, allowing for prioritized responses to negative feedback and improved overall customer experience. diff --git a/components/intellihr/README.md b/components/intellihr/README.md new file mode 100644 index 0000000000000..4b703ea205d0e --- /dev/null +++ b/components/intellihr/README.md @@ -0,0 +1,11 @@ +# Overview + +The IntelliHR API offers a gateway to a comprehensive human resource management system, focusing on performance, analytics, and automation. It enables developers to integrate various aspects of HR management, such as employee data, performance reviews, and training records, into their own applications or workflows. Using the API on Pipedream, one can seamlessly connect IntelliHR with other services to automate tasks, sync data, and create responsive HR solutions that operate in real-time. + +# Example Use Cases + +- **Automate Employee Onboarding Processes**: Streamline the onboarding of new hires by creating a Pipedream workflow that triggers on new employee records in IntelliHR. The workflow can send out welcome emails, schedule training sessions, and assign tasks in project management tools like Asana or Trello automatically. + +- **Sync Employee Data with Payroll Services**: Ensure employee data is consistent across platforms by crafting a workflow that listens for updates in IntelliHR, then syncs changed details to a payroll service like Gusto or ADP. This can include updates to personal information, job positions, or salary changes, maintaining data integrity without manual intervention. + +- **Generate Custom Reports and Alerts**: Design a workflow that aggregates data from IntelliHR, such as performance metrics or attendance records, and feeds it into a reporting tool like Google Sheets or Tableau. Set up alerts that notify managers via Slack or email when specific thresholds are met, enabling timely responses to HR insights. diff --git a/components/intelliprint/README.md b/components/intelliprint/README.md new file mode 100644 index 0000000000000..50ae497253ee4 --- /dev/null +++ b/components/intelliprint/README.md @@ -0,0 +1,11 @@ +# Overview + +The Intelliprint API offers a suite of printing solutions, enabling developers to automate the process of sending documents for print. With this API, you can create workflows in Pipedream to programmatically manage print jobs, customize printing options, and track the status of each task. It's ideal for businesses looking to integrate print capabilities into their services without the overhead of managing printers and supplies. + +# Example Use Cases + +- **Automated Invoice Printing**: Trigger a Pipedream workflow to send invoices for print automatically whenever a new sale is recorded in your e-commerce platform. This can ensure that all paperwork is printed and ready for dispatch without manual intervention. + +- **Custom Business Cards on Demand**: Set up a Pipedream workflow that listens for a webhook from a CRM app when new employees are added. The workflow can then use the Intelliprint API to print business cards with the employee's details, streamlining the onboarding process. + +- **Print Job Status Tracking**: Construct a Pipedream workflow that periodically checks the status of print jobs submitted through Intelliprint. When a job is complete, the workflow could send a notification via apps like Slack or email, keeping relevant teams informed. diff --git a/components/intelliprint/package.json b/components/intelliprint/package.json index 94c8467e24091..dabe9e4eda77b 100644 --- a/components/intelliprint/package.json +++ b/components/intelliprint/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/intercom/README.md b/components/intercom/README.md index fa8e3d308df95..ba71004a534a4 100644 --- a/components/intercom/README.md +++ b/components/intercom/README.md @@ -1,16 +1,8 @@ -## Overview +# Overview -With the Intercom API, you can build a variety of applications that can -interact with Intercom data. Here are some examples: +The Intercom API offers rich capabilities for enhancing customer communication and support workflows. By leveraging this API on Pipedream, you can automate tasks, sync customer data across platforms, and create personalized interactions. Whether you are managing user segments, sending targeted messages, or updating customer profiles, the Intercom API's robust set of endpoints allows for intricate and useful automations within your business processes. -- An application that displays a list of Intercom users -- An application that allows you to search for Intercom users by name or email -- An application that displays a list of Intercom conversations -- An application that allows you to view and reply to Intercom conversations -- An application that displays a list of Intercom events -- An application that allows you to view and track Intercom events - -## Getting Started +# Getting Started 1. First, sign up for Pipedream at [https://pipedream.com](https://pipedream.com). 2. [Create a new workflow](https://pipedream.com/new). @@ -18,10 +10,18 @@ interact with Intercom data. Here are some examples: 4. [Add a new step](/workflows/steps/) and search for "Intercom". This will display [actions](/components#actions) associated with the Intercom app. You can choose to either "Run Node.js code with Intercom" or select one of the prebuilt actions for performing common API operations. 5. Once you've added a step, press the **Connect Account** button near the top. If this is your first time authorizing Pipedream's access to your Intercom account, you'll be prompted to accept that access, and Pipedream will store the authorization grant to enable the workflow to access the Intercom API. If you've already linked an Intercom account via Pipedream, pressing **Connect Account** will list any existing accounts you've linked. -### Removing Pipedream's access to your Intercom account +## Removing Pipedream's access to your Intercom account You can revoke Pipedream's access to your Intercom account by uninstalling the app from your list of Intercom apps in your account. As soon as you do, any Pipedream workflows that connect to Intercom will immediately fail to work. You can delete any Intercom connected accounts in [your list of Pipedream Accounts](https://pipedream.com/accounts), as well. + +# Example Use Cases + +- **Sync Intercom Users to a Google Sheet for Reporting**: Automatically export new and updated Intercom users to a Google Sheet. This workflow can be used to maintain an up-to-date record of your users for reporting, data analysis, or to provide other teams with user information without giving direct access to Intercom. + +- **Automated Customer Support Ticket Creation**: Set up a workflow that listens for specific keywords or tags in Intercom conversations and creates tickets in an external system like Jira or Zendesk. This allows for a seamless support experience where tickets are cataloged and prioritized outside of Intercom based on custom criteria. + +- **Trigger Email Campaigns from User Events**: Connect Intercom with an email marketing platform like MailChimp or SendGrid. When a user completes a significant action in your app (e.g., signing up, making a purchase), trigger a personalized email campaign that enhances their customer journey and encourages engagement. \ No newline at end of file diff --git a/components/intercom/actions/add-tag-to-contact/add-tag-to-contact.mjs b/components/intercom/actions/add-tag-to-contact/add-tag-to-contact.mjs new file mode 100644 index 0000000000000..96b52e3297ce3 --- /dev/null +++ b/components/intercom/actions/add-tag-to-contact/add-tag-to-contact.mjs @@ -0,0 +1,75 @@ +import intercom from "../../intercom.app.mjs"; + +export default { + key: "intercom-add-tag-to-contact", + name: "Add Tag To Contact", + description: "Adds a specific tag to a contact in Intercom. [See the documentation](https://developers.intercom.com/docs/references/rest-api/api.intercom.io/contacts/attachtagtocontact).", + version: "0.0.1", + type: "action", + props: { + intercom, + contactId: { + type: "string", + label: "Contact ID", + description: "The unique identifier for the contact which is given by Intercom. Eg. `63a07ddf05a32042dffac965`.", + propDefinition: [ + intercom, + "userIds", + () => ({ + data: { + query: { + operator: "OR", + value: [ + { + field: "role", + operator: "=", + value: "user", + }, + { + field: "role", + operator: "=", + value: "lead", + }, + ], + }, + }, + }), + ], + }, + tagId: { + propDefinition: [ + intercom, + "tagId", + ], + }, + }, + methods: { + addTagToContact({ + contactId, ...args + } = {}) { + return this.intercom.makeRequest({ + method: "POST", + endpoint: `contacts/${contactId}/tags`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + addTagToContact, + contactId, + tagId, + } = this; + + const response = await addTagToContact({ + $, + contactId, + data: { + id: tagId, + }, + }); + + $.export("$summary", `Successfully added tag to contact with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/intercom/actions/create-note/create-note.mjs b/components/intercom/actions/create-note/create-note.mjs index 617f7c2557560..983acec13b55b 100644 --- a/components/intercom/actions/create-note/create-note.mjs +++ b/components/intercom/actions/create-note/create-note.mjs @@ -4,7 +4,7 @@ export default { key: "intercom-create-note", name: "Create Note", description: "Creates a note for a specific user. [See the docs here](https://developers.intercom.com/intercom-api-reference/reference/create-note-for-contact)", - version: "0.0.2", + version: "0.0.5", type: "action", props: { intercom, diff --git a/components/intercom/actions/reply-to-conversation/reply-to-conversation.mjs b/components/intercom/actions/reply-to-conversation/reply-to-conversation.mjs new file mode 100644 index 0000000000000..845dd7ef6270b --- /dev/null +++ b/components/intercom/actions/reply-to-conversation/reply-to-conversation.mjs @@ -0,0 +1,203 @@ +import intercom from "../../intercom.app.mjs"; + +export default { + key: "intercom-reply-to-conversation", + name: "Reply To Conversation", + description: "Add a reply or a note to an existing conversation thread. [See the documentation](https://developers.intercom.com/docs/references/rest-api/api.intercom.io/conversations/replyconversation).", + version: "0.0.1", + type: "action", + props: { + intercom, + conversationId: { + propDefinition: [ + intercom, + "conversationId", + ], + }, + replyType: { + type: "string", + label: "Reply Type", + description: "The type of the reply.", + options: [ + { + label: "Contact Reply", + value: "user", + }, + { + label: "Admin Reply", + value: "admin", + }, + ], + reloadProps: true, + }, + messageType: { + propDefinition: [ + intercom, + "messageType", + ({ replyType: type }) => ({ + type, + }), + ], + }, + body: { + type: "string", + label: "Body", + description: "The text body of the comment.", + }, + attachmentUrls: { + type: "string[]", + label: "Attachment URLs", + description: "A list of image URLs that will be added as attachments. You can include up to 10 URLs.", + optional: true, + }, + }, + additionalProps() { + const { + replyType, + replyOnBehalfOf, + } = this; + + if (replyType === "admin") { + return { + adminId: { + type: "string", + label: "Admin ID", + description: "The id of the admin who is authoring the comment.", + options: async () => { + const { admins } = await this.intercom.listAdmins(); + return admins.map((admin) => ({ + label: admin.name, + value: admin.id, + })); + }, + }, + }; + } + + return { + replyOnBehalfOf: { + type: "string", + label: "Reply On Behalf Of", + description: "The user ID of the user on whose behalf the reply is being made.", + options: [ + { + label: "Intercom User ID", + value: "intercom_user_id", + }, + { + label: "Email", + value: "email", + }, + { + label: "User ID", + value: "user_id", + }, + ], + reloadProps: true, + }, + ...(replyOnBehalfOf === "intercom_user_id" && { + intercomUserId: { + type: "string", + label: "Intercom User ID", + description: "The identifier for the contact as given by Intercom.", + options: async () => { + const results = await this.intercom.searchContacts({ + query: { + field: "role", + operator: "=", + value: "user", + }, + }); + return results.map((user) => ({ + label: user.name || user.id, + value: user.id, + })); + }, + }, + }), + ...(replyOnBehalfOf === "email" && { + email: { + type: "string", + label: "Email", + description: "The email you have defined for the user.", + options: async () => { + const results = await this.intercom.searchContacts({ + query: { + field: "role", + operator: "=", + value: "user", + }, + }); + return results.map((user) => ({ + label: user.name || user.id, + value: user.email, + })); + }, + }, + }), + ...(replyOnBehalfOf === "user_id" && { + userId: { + type: "string", + label: "User ID", + description: "The external ID you have defined for the contact.", + options: async () => { + const results = await this.intercom.searchContacts({ + query: { + field: "role", + operator: "=", + value: "user", + }, + }); + return results.map((user) => ({ + label: user.name || user.id, + value: user.external_id, + })); + }, + }, + }), + }; + }, + methods: { + replyToConversation({ + conversationId, ...args + } = {}) { + return this.intercom.makeRequest({ + method: "POST", + endpoint: `conversations/${conversationId}/parts`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + replyToConversation, + conversationId, + body, + attachmentUrls, + replyType, + adminId, + intercomUserId, + email, + userId, + messageType, + } = this; + + const response = await replyToConversation({ + $, + conversationId, + data: { + body, + attachment_urls: attachmentUrls, + admin_id: adminId, + intercom_user_id: intercomUserId, + email, + user_id: userId, + message_type: messageType, + type: replyType, + }, + }); + + $.export("$summary", "Reply or note added successfully"); + return response; + }, +}; diff --git a/components/intercom/actions/send-incoming-message/send-incoming-message.mjs b/components/intercom/actions/send-incoming-message/send-incoming-message.mjs index a27d50fdb3d6d..f376b64dc4eed 100644 --- a/components/intercom/actions/send-incoming-message/send-incoming-message.mjs +++ b/components/intercom/actions/send-incoming-message/send-incoming-message.mjs @@ -4,7 +4,7 @@ export default { key: "intercom-send-incoming-message", name: "Send Incoming Message", description: "Send a message from a user into your Intercom app. [See the docs here](https://developers.intercom.com/intercom-api-reference/reference/create-a-conversation)", - version: "0.0.2", + version: "0.0.5", type: "action", props: { intercom, diff --git a/components/intercom/actions/send-message-to-contact/send-message-to-contact.mjs b/components/intercom/actions/send-message-to-contact/send-message-to-contact.mjs new file mode 100644 index 0000000000000..0a41b5f7ebace --- /dev/null +++ b/components/intercom/actions/send-message-to-contact/send-message-to-contact.mjs @@ -0,0 +1,111 @@ +import intercom from "../../intercom.app.mjs"; + +export default { + key: "intercom-send-message-to-contact", + name: "Send Message To Contact", + description: "Send a message to a contact in Intercom. [See the documentation](https://developers.intercom.com/docs/references/rest-api/api.intercom.io/messages/createmessage).", + version: "0.0.1", + type: "action", + props: { + intercom, + messageType: { + type: "string", + label: "Message Type", + description: "The kind of message being created.", + options: [ + "in_app", + "email", + ], + }, + subject: { + type: "string", + label: "Subject", + description: "The title of the email.", + }, + body: { + description: "The content of the message. HTML and plaintext are supported.", + propDefinition: [ + intercom, + "body", + ], + }, + template: { + type: "string", + label: "Template", + description: "The style of the outgoing message.", + options: [ + "plain", + "personal", + ], + }, + fromId: { + type: "string", + label: "From ID", + description: "The sender of the message. The identifier for the admin which is given by Intercom. If not provided, the default sender will be used.", + propDefinition: [ + intercom, + "adminId", + ], + }, + toType: { + type: "string", + label: "To Type", + description: "The type of the recipient of the message.", + options: [ + "user", + "lead", + ], + }, + toId: { + type: "string", + label: "To ID", + description: "The recipient of the message. The identifier for the contact which is given by Intercom. Eg. `536e564f316c83104c000020`.", + propDefinition: [ + intercom, + "userIds", + ], + }, + }, + methods: { + sendMessage(args = {}) { + return this.intercom.makeRequest({ + method: "POST", + endpoint: "messages", + ...args, + }); + }, + }, + async run({ $ }) { + const { + sendMessage, + messageType, + subject, + body, + template, + fromId, + toType, + toId, + } = this; + + const response = await sendMessage({ + $, + data: { + message_type: messageType, + subject, + body, + template, + from: { + type: "admin", + id: fromId, + }, + to: { + type: toType, + id: toId, + }, + }, + }); + + $.export("$summary", `Successfully sent message with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/intercom/actions/upsert-contact/upsert-contact.mjs b/components/intercom/actions/upsert-contact/upsert-contact.mjs new file mode 100644 index 0000000000000..dc5540ca3b2b9 --- /dev/null +++ b/components/intercom/actions/upsert-contact/upsert-contact.mjs @@ -0,0 +1,131 @@ +import { ROLE_OPTIONS } from "../../common/constants.mjs"; +import intercom from "../../intercom.app.mjs"; + +export default { + key: "intercom-upsert-contact", + name: "Upsert Contact", + description: "Create a new contact. If there is already a contact with the email provided, the existing contact will be updated. [See the docs here](https://developers.intercom.com/docs/references/rest-api/api.intercom.io/contacts/createcontact)", + version: "0.0.2", + type: "action", + props: { + intercom, + email: { + type: "string", + label: "Email", + description: "The contact's email.", + }, + role: { + type: "string", + label: "Role", + description: "The role of the contact.", + options: ROLE_OPTIONS, + optional: true, + }, + externalId: { + type: "string", + label: "External Id", + description: "A unique identifier for the contact which is given to Intercom.", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The contact's phone number.", + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "The contact's name.", + optional: true, + }, + avatar: { + type: "string", + label: "Avatar", + description: "An image URL containing the avatar of a contact.", + optional: true, + }, + unsubscribedFromEmails: { + type: "boolean", + label: "Unsubscribed From Emails", + description: "Whether the contact is unsubscribed from emails.", + optional: true, + }, + customAttributes: { + type: "object", + label: "Custom Attributes", + description: "The custom attributes which are set for the contact.", + optional: true, + }, + }, + async run({ $ }) { + let response = {}; + let requestType = "created"; + let data = { + email: this.email, + role: this.role, + externalId: this.externalId, + phone: this.phone, + name: this.name, + avatar: this.avatar, + unsubscribedFromEmails: this.unsubscribedFromEmails, + customAttributes: this.customAttributes, + }; + + data = Object.entries(data).filter(([ + , + value, + ]) => (value != "" && value != undefined)) + .reduce((obj, [ + key, + value, + ]) => Object.assign(obj, { + [key]: value, + }), {}); + + const { + data: contact, total_count: total, + } = await this.intercom.searchContact({ + data: { + query: { + operator: "AND", + value: [ + { + field: "email", + operator: "=", + value: this.email, + }, + ], + }, + pagination: { + per_page: 1, + }, + }, + }); + + if (total) { + const { + id: contactId, + // eslint-disable-next-line no-unused-vars + owner_id, + ...contactInfos + } = contact[0]; + response = await this.intercom.updateContact({ + $, + contactId, + data: { + ...contactInfos, + ...data, + }, + }); + requestType = "updated"; + } else { + response = await this.intercom.createContact({ + $, + data, + }); + } + $.export("$summary", `Successfully ${requestType} contact with ID ${response.id}`); + return response; + }, +}; diff --git a/components/intercom/common/constants.mjs b/components/intercom/common/constants.mjs new file mode 100644 index 0000000000000..cfa8b3e7d544e --- /dev/null +++ b/components/intercom/common/constants.mjs @@ -0,0 +1,10 @@ +export const ROLE_OPTIONS = [ + { + label: "User", + value: "user", + }, + { + label: "Lead", + value: "lead", + }, +]; diff --git a/components/intercom/intercom.app.mjs b/components/intercom/intercom.app.mjs index 930a4ea2f2317..957211a1489ea 100644 --- a/components/intercom/intercom.app.mjs +++ b/components/intercom/intercom.app.mjs @@ -8,14 +8,15 @@ export default { type: "string[]", label: "Users", description: "Users to watch for new events", - async options() { - const data = { + async options({ + data = { query: { field: "role", operator: "=", value: "user", }, - }; + }, + }) { const results = await this.searchContacts(data); return results.map((user) => ({ label: user.name || user.id, @@ -28,6 +29,80 @@ export default { label: "Body", description: "The text of the note.", }, + tagId: { + type: "string", + label: "Tag ID", + description: "The unique identifier for the tag which is given by Intercom. Eg. `7522907`.", + async options() { + const { data: tags } = await this.listTags(); + return tags.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + conversationId: { + type: "string", + label: "Conversation ID", + description: "The Intercom provisioned identifier for the conversation or the string `last` to reply to the last part of the conversation.", + async options({ prevContext: { startingAfter } }) { + if (startingAfter === null) { + return []; + } + const response = await this.listConversations({ + params: { + per_page: 20, + starting_after: startingAfter, + }, + }); + const options = response.conversations.map((conversation) => ({ + label: conversation.title || conversation.id, + value: conversation.id, + })); + return { + options, + context: { + startingAfter: response.pages.next?.starting_after || null, + }, + }; + }, + }, + messageType: { + type: "string", + label: "Message Type", + description: "The kind of message being created.", + options({ type = "user" }) { + if (type === "user") { + return [ + "comment", + ]; + } + + if (type === "admin") { + return [ + "comment", + "note", + ]; + } + return []; + }, + }, + adminId: { + type: "string", + label: "Admin ID", + description: "The unique identifier for the admin which is given by Intercom. Eg. `25`.", + async options() { + const { admins } = await this.listAdmins(); + return admins.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, }, methods: { monthAgo() { @@ -45,22 +120,22 @@ export default { * @params {Object} [opts.data] - The request body * @returns {*} The response may vary depending on the specific API request. */ - async makeRequest(opts) { - const { - method, - url, - endpoint, - data, - $, - } = opts; + async makeRequest({ + method, + url, + endpoint, + $, + ...opts + }) { const config = { method, url: url ?? `https://api.intercom.io/${endpoint}`, headers: { - Authorization: `Bearer ${this.$auth.oauth_access_token}`, - Accept: "application/json", + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "Accept": "application/json", + "Intercom-Version": "2.12", }, - data, + ...opts, }; return axios($ || this, config); }, @@ -76,7 +151,7 @@ export default { * Used to retrieve only new results * @returns {Array} The complete list of paginated items */ - async paginate(itemType, method, data, isSearch = false, lastCreatedAt) { + async paginate(itemType, method, data, isSearch = false, lastCreatedAt, resourceKey = "data") { let results = null; let done = false; let items = []; @@ -91,14 +166,14 @@ export default { data, }); if (lastCreatedAt) { - for (const item of results.data) { + for (const item of results[resourceKey]) { if (item.created_at > lastCreatedAt) items.push(item); else done = true; } } else { - items = items.concat(results.data); + items = items.concat(results[resourceKey]); if (!startingAfter) done = true; } @@ -190,7 +265,7 @@ export default { * @returns {Array} List of conversations matching search query */ async searchConversations(data) { - return this.paginate("conversations", "POST", data, true); + return this.paginate("conversations", "POST", data, true, null, "conversations"); }, /** * Create a note for a specific user @@ -210,6 +285,29 @@ export default { $, }); }, + searchContact(opts = {}) { + return this.makeRequest({ + method: "POST", + endpoint: "contacts/search", + ...opts, + }); + }, + createContact(opts = {}) { + return this.makeRequest({ + method: "POST", + endpoint: "contacts", + ...opts, + }); + }, + updateContact({ + contactId, ...opts + }) { + return this.makeRequest({ + method: "PUT", + endpoint: `contacts/${contactId}`, + ...opts, + }); + }, /** * Create an incoming message from a user * @params {Object} data - The request body parameters including a `from` object and @@ -224,5 +322,24 @@ export default { $, }); }, + listTags() { + return this.makeRequest({ + endpoint: "tags", + }); + }, + searchTickets(data) { + return this.paginate("tickets", "POST", data, true, null, "tickets"); + }, + listConversations(args = {}) { + return this.makeRequest({ + endpoint: "conversations", + ...args, + }); + }, + listAdmins() { + return this.makeRequest({ + endpoint: "admins", + }); + }, }, }; diff --git a/components/intercom/package.json b/components/intercom/package.json index 67b142505e23a..dda100fd76c39 100644 --- a/components/intercom/package.json +++ b/components/intercom/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/intercom", - "version": "0.3.8", + "version": "0.6.0", "description": "Pipedream Intercom Components", "main": "intercom.app.mjs", "keywords": [ @@ -14,7 +14,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.2.0", + "@pipedream/platform": "^3.0.3", "crypto": "^1.0.1" } } diff --git a/components/intercom/sources/common.mjs b/components/intercom/sources/common.mjs deleted file mode 100644 index e3ea21ecfcbf8..0000000000000 --- a/components/intercom/sources/common.mjs +++ /dev/null @@ -1,26 +0,0 @@ -import intercom from "../intercom.app.mjs"; -import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; - -export default { - props: { - intercom, - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - label: "Polling Interval", - description: "Pipedream will poll the API on this schedule", - }, - }, - methods: { - _getLastUpdate() { - const monthAgo = this.intercom.monthAgo(); - return this.db.get("lastUpdate") || Math.floor(monthAgo / 1000); - }, - _setLastUpdate(lastUpdate) { - this.db.set("lastUpdate", lastUpdate); - }, - }, -}; diff --git a/components/intercom/sources/common/common.mjs b/components/intercom/sources/common/common.mjs new file mode 100644 index 0000000000000..2b1c1878d160b --- /dev/null +++ b/components/intercom/sources/common/common.mjs @@ -0,0 +1,26 @@ +import intercom from "../../intercom.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + intercom, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + label: "Polling Interval", + description: "Pipedream will poll the API on this schedule", + }, + }, + methods: { + _getLastUpdate() { + const monthAgo = this.intercom.monthAgo(); + return this.db.get("lastUpdate") || Math.floor(monthAgo / 1000); + }, + _setLastUpdate(lastUpdate) { + this.db.set("lastUpdate", lastUpdate); + }, + }, +}; diff --git a/components/intercom/sources/conversation-closed/conversation-closed.mjs b/components/intercom/sources/conversation-closed/conversation-closed.mjs index 1b4c135a9b72e..e078aa6b739cf 100644 --- a/components/intercom/sources/conversation-closed/conversation-closed.mjs +++ b/components/intercom/sources/conversation-closed/conversation-closed.mjs @@ -1,11 +1,11 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; export default { ...common, key: "intercom-conversation-closed", name: "New Closed Conversation", description: "Emit new event each time a conversation is closed.", - version: "0.0.3", + version: "0.0.7", type: "source", dedupe: "unique", methods: { diff --git a/components/intercom/sources/lead-added-email/lead-added-email.mjs b/components/intercom/sources/lead-added-email/lead-added-email.mjs index 4eb3c7a32ba92..708899ad07513 100644 --- a/components/intercom/sources/lead-added-email/lead-added-email.mjs +++ b/components/intercom/sources/lead-added-email/lead-added-email.mjs @@ -1,11 +1,11 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; export default { ...common, key: "intercom-lead-added-email", name: "Lead Added Email", description: "Emit new event each time a lead adds their email address.", - version: "0.0.3", + version: "0.0.7", type: "source", dedupe: "unique", methods: { diff --git a/components/intercom/sources/new-admin-reply/new-admin-reply.mjs b/components/intercom/sources/new-admin-reply/new-admin-reply.mjs index 1c3e8910631a7..d698ef3f7cbd1 100644 --- a/components/intercom/sources/new-admin-reply/new-admin-reply.mjs +++ b/components/intercom/sources/new-admin-reply/new-admin-reply.mjs @@ -1,11 +1,11 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; export default { ...common, key: "intercom-new-admin-reply", name: "New Reply From Admin", description: "Emit new event each time an admin replies to a conversation.", - version: "0.0.3", + version: "0.0.7", type: "source", dedupe: "unique", methods: { diff --git a/components/intercom/sources/new-company/new-company.mjs b/components/intercom/sources/new-company/new-company.mjs index 7d8c4dd373e6b..f8568a187e959 100644 --- a/components/intercom/sources/new-company/new-company.mjs +++ b/components/intercom/sources/new-company/new-company.mjs @@ -1,11 +1,11 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; export default { ...common, key: "intercom-new-company", name: "New Companies", description: "Emit new event each time a new company is added.", - version: "0.0.3", + version: "0.0.7", type: "source", dedupe: "unique", methods: { diff --git a/components/intercom/sources/new-conversation-rating-added/new-conversation-rating-added.mjs b/components/intercom/sources/new-conversation-rating-added/new-conversation-rating-added.mjs new file mode 100644 index 0000000000000..2fffd96a24f15 --- /dev/null +++ b/components/intercom/sources/new-conversation-rating-added/new-conversation-rating-added.mjs @@ -0,0 +1,43 @@ +import common from "../common/common.mjs"; + +export default { + ...common, + key: "intercom-new-conversation-rating-added", + name: "New Conversation Rating Added", + description: "Emit new event each time a new rating is added to a conversation.", + version: "0.0.3", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + generateMeta(conversation) { + const ts = conversation.conversation_rating.created_at; + return { + id: `${conversation.id}-${ts}`, + summary: `New rating added to conversation with ID: ${conversation.id}`, + ts, + }; + }, + }, + async run() { + let lastRatingCreatedAt = this._getLastUpdate(); + const data = { + query: { + field: "conversation_rating.requested_at", + operator: ">", + value: lastRatingCreatedAt, + }, + }; + + const results = await this.intercom.searchConversations(data); + for (const conversation of results) { + const createdAt = conversation.conversation_rating.created_at; + if (createdAt > lastRatingCreatedAt) + lastRatingCreatedAt = createdAt; + const meta = this.generateMeta(conversation); + this.$emit(conversation, meta); + } + + this._setLastUpdate(lastRatingCreatedAt); + }, +}; diff --git a/components/intercom/sources/new-conversation/new-conversation.mjs b/components/intercom/sources/new-conversation/new-conversation.mjs index 5ddfc8344552a..f0d37d11c44cb 100644 --- a/components/intercom/sources/new-conversation/new-conversation.mjs +++ b/components/intercom/sources/new-conversation/new-conversation.mjs @@ -1,11 +1,11 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; export default { ...common, key: "intercom-new-conversation", name: "New Conversations", description: "Emit new event each time a new conversation is added.", - version: "0.0.3", + version: "0.0.7", type: "source", dedupe: "unique", methods: { diff --git a/components/intercom/sources/new-event/new-event.mjs b/components/intercom/sources/new-event/new-event.mjs index 4ca30572c1f14..3294e3355ed09 100644 --- a/components/intercom/sources/new-event/new-event.mjs +++ b/components/intercom/sources/new-event/new-event.mjs @@ -1,11 +1,11 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; export default { ...common, key: "intercom-new-event", name: "New Event", description: "Emit new event for each new Intercom event for a user.", - version: "0.0.3", + version: "0.0.7", type: "source", dedupe: "unique", props: { diff --git a/components/intercom/sources/new-lead/new-lead.mjs b/components/intercom/sources/new-lead/new-lead.mjs index 26a750be2c543..15a85d31d94a0 100644 --- a/components/intercom/sources/new-lead/new-lead.mjs +++ b/components/intercom/sources/new-lead/new-lead.mjs @@ -1,11 +1,11 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; export default { ...common, key: "intercom-new-lead", name: "New Leads", description: "Emit new event each time a new lead is added.", - version: "0.0.3", + version: "0.0.7", type: "source", dedupe: "unique", methods: { diff --git a/components/intercom/sources/new-ticket/new-ticket.mjs b/components/intercom/sources/new-ticket/new-ticket.mjs new file mode 100644 index 0000000000000..d47b534341fd9 --- /dev/null +++ b/components/intercom/sources/new-ticket/new-ticket.mjs @@ -0,0 +1,49 @@ +import common from "../common/common.mjs"; + +export default { + ...common, + key: "intercom-new-ticket", + name: "New Tickets", + description: "Emit new event when a new ticket is created in Intercom.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + generateMeta({ + id, ticket_attributes, created_at: createdAt, + }) { + return { + id, + summary: ticket_attributes?._default_title_ || `New Ticket ${id}`, + ts: createdAt, + }; + }, + }, + async run() { + let lastTicketCreatedAt = this._getLastUpdate(); + const data = { + query: { + operator: "AND", + value: [ + { + field: "created_at", + operator: ">", + value: lastTicketCreatedAt, + }, + ], + }, + }; + + const results = await this.intercom.searchTickets(data); + for (const ticket of results) { + if (ticket.created_at > lastTicketCreatedAt) { + lastTicketCreatedAt = ticket.created_at; + } + const meta = this.generateMeta(ticket); + this.$emit(ticket, meta); + } + + this._setLastUpdate(lastTicketCreatedAt); + }, +}; diff --git a/components/intercom/sources/new-topic/new-topic.mjs b/components/intercom/sources/new-topic/new-topic.mjs index 2cf98d8458dbe..cf6adf2d330c7 100644 --- a/components/intercom/sources/new-topic/new-topic.mjs +++ b/components/intercom/sources/new-topic/new-topic.mjs @@ -1,12 +1,12 @@ -import app from "../../intercom.app.mjs"; -import { v4 as uuid } from "uuid"; import crypto from "crypto"; +import { v4 as uuid } from "uuid"; +import app from "../../intercom.app.mjs"; export default { key: "intercom-new-topic", name: "New Topic (Instant)", description: "Emit new event for each new topic that you subscribed in your webhook. [See more here](https://developers.intercom.com/building-apps/docs/setting-up-webhooks).", - version: "0.0.1", + version: "0.0.4", type: "source", dedupe: "unique", props: { diff --git a/components/intercom/sources/new-unsubscription/new-unsubscription.mjs b/components/intercom/sources/new-unsubscription/new-unsubscription.mjs index 246924a7eef5a..19c0c46552022 100644 --- a/components/intercom/sources/new-unsubscription/new-unsubscription.mjs +++ b/components/intercom/sources/new-unsubscription/new-unsubscription.mjs @@ -1,11 +1,11 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; export default { ...common, key: "intercom-new-unsubscription", name: "New Unsubscriptions", description: "Emit new event each time a user unsubscribes from receiving emails.", - version: "0.0.3", + version: "0.0.7", type: "source", dedupe: "unique", methods: { diff --git a/components/intercom/sources/new-user-reply/new-user-reply.mjs b/components/intercom/sources/new-user-reply/new-user-reply.mjs index 6fa6afb171cea..df3a240511ec8 100644 --- a/components/intercom/sources/new-user-reply/new-user-reply.mjs +++ b/components/intercom/sources/new-user-reply/new-user-reply.mjs @@ -1,11 +1,11 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; export default { ...common, key: "intercom-new-user-reply", name: "New Reply From User", description: "Emit new event each time a user replies to a conversation.", - version: "0.0.3", + version: "0.0.7", type: "source", dedupe: "unique", methods: { diff --git a/components/intercom/sources/new-user/new-user.mjs b/components/intercom/sources/new-user/new-user.mjs index 05ccd077dae54..605c7c5789a4c 100644 --- a/components/intercom/sources/new-user/new-user.mjs +++ b/components/intercom/sources/new-user/new-user.mjs @@ -1,11 +1,11 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; export default { ...common, key: "intercom-new-user", name: "New Users", description: "Emit new event each time a new user is added.", - version: "0.0.3", + version: "0.0.7", type: "source", dedupe: "unique", methods: { diff --git a/components/intercom/sources/tag-added-to-conversation/tag-added-to-conversation.mjs b/components/intercom/sources/tag-added-to-conversation/tag-added-to-conversation.mjs index 4bf036b95657a..26ac2540cf88a 100644 --- a/components/intercom/sources/tag-added-to-conversation/tag-added-to-conversation.mjs +++ b/components/intercom/sources/tag-added-to-conversation/tag-added-to-conversation.mjs @@ -1,11 +1,11 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; export default { ...common, key: "intercom-tag-added-to-conversation", name: "Tag Added To Conversation", description: "Emit new event each time a new tag is added to a conversation.", - version: "0.0.3", + version: "0.0.7", type: "source", dedupe: "unique", methods: { @@ -33,9 +33,15 @@ export default { const results = await this.intercom.searchConversations(data); for (const conversation of results) { + if (!conversation?.tags) { + continue; + } for (const tag of conversation.tags.tags) { const meta = this.generateMeta(conversation, tag); - this.$emit(tag, meta); + this.$emit({ + conversation, + ...tag, + }, meta); } } }, diff --git a/components/intercom/sources/tag-added-to-lead/tag-added-to-lead.mjs b/components/intercom/sources/tag-added-to-lead/tag-added-to-lead.mjs index 42a2c0d79b1d4..64cfcb39681b1 100644 --- a/components/intercom/sources/tag-added-to-lead/tag-added-to-lead.mjs +++ b/components/intercom/sources/tag-added-to-lead/tag-added-to-lead.mjs @@ -1,11 +1,11 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; export default { ...common, key: "intercom-tag-added-to-lead", name: "Tag Added To Lead", description: "Emit new event each time a new tag is added to a lead.", - version: "0.0.3", + version: "0.0.7", type: "source", dedupe: "unique", methods: { diff --git a/components/intercom/sources/tag-added-to-user/tag-added-to-user.mjs b/components/intercom/sources/tag-added-to-user/tag-added-to-user.mjs index 7a55bd9384364..11e0c9871df69 100644 --- a/components/intercom/sources/tag-added-to-user/tag-added-to-user.mjs +++ b/components/intercom/sources/tag-added-to-user/tag-added-to-user.mjs @@ -1,11 +1,11 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; export default { ...common, key: "intercom-tag-added-to-user", name: "Tag Added To User", description: "Emit new event each time a new tag is added to a user.", - version: "0.0.3", + version: "0.0.7", type: "source", dedupe: "unique", methods: { diff --git a/components/interseller/README.md b/components/interseller/README.md index eb5c98198f7f6..ad12d00b06f6a 100644 --- a/components/interseller/README.md +++ b/components/interseller/README.md @@ -1,13 +1,11 @@ # Overview -The Interseller API can be used to build a variety of applications, including: - -- An application to search for and contact potential customers -- A CRM application to manage your customer relationships -- A sales intelligence application to gather information about potential - customers -- A marketing application to target potential customers with personalized - messages -- An accounting application to track your sales and customers -- A customer service application to help your customers with their inquiries -- A human resources application to keep track of your employees +The Interseller API provides the power to streamline recruitment and sales processes by automating outreach and follow-ups. With Pipedream, you can leverage this API to create workflows that trigger based on specific criteria such as new candidate profiles or responses to outreach emails. These workflows can enrich lead data, sync information across platforms, or update your team with real-time notifications, reducing manual tasks and enhancing productivity. + +# Example Use Cases + +- **Automated Lead Enrichment Workflow**: When a new lead is added in Interseller, trigger a Pipedream workflow that uses Clearbit to enrich the lead's data with additional details like their company size or industry. Then, update the lead's profile in Interseller with this new information, ensuring your outreach is tailored and informed. + +- **Outreach Follow-Up Automation**: Set up a workflow on Pipedream that listens for a signal of no response after an initial outreach email sent via Interseller. Once triggered, it could automatically send a follow-up email through Interseller or schedule a task in a CRM like HubSpot, reminding your team to reach out personally. + +- **Real-Time Slack Notifications for New Replies**: Create a Pipedream workflow that monitors for new replies to Interseller campaigns. Once a reply is detected, the workflow sends a custom alert to a designated Slack channel. This ensures that your team can quickly respond to potential leads or engage in timely follow-ups. diff --git a/components/interzoid/README.md b/components/interzoid/README.md new file mode 100644 index 0000000000000..aa59c52fbef89 --- /dev/null +++ b/components/interzoid/README.md @@ -0,0 +1,11 @@ +# Overview + +The Interzoid API offers a plethora of data-driven APIs that enable you to enrich, standardize, and deduplicate data across various fields such as demographics, financials, and text. With these capabilities, you can enhance data quality, drive better analytics, and create more intelligent workflows and applications. In Pipedream, you can integrate these APIs into serverless workflows, triggering actions based on various events, manipulating and routing data to other apps, services, or data stores with ease. + +# Example Use Cases + +- **Verify Email Addresses in Real-Time**: Upon receiving new email sign-ups through a web form, use Interzoid's Email Information API to verify and score each email in a Pipedream workflow. If the score is acceptable, automatically add the email to a Mailchimp list for marketing campaigns, otherwise flag for review. + +- **Standardize and Clean Customer Data**: As new customer data comes in via CRM platforms like Salesforce, harness Interzoid's Global Telephone Information API to format international phone numbers and the Get Full Name Match API to normalize names. Your Pipedream workflow can then update the customer profiles in Salesforce with this standardized information, ensuring data consistency. + +- **Currency Exchange Rate Analysis**: Use the Interzoid Get Currency Rate API within Pipedream to fetch real-time currency exchange rates. Combine this data with a time-based trigger to create a daily snapshot of exchange rates. This can be stored in a Google Sheet for historical analysis or used to update pricing information across e-commerce platforms. diff --git a/components/intuiface/README.md b/components/intuiface/README.md index 4285c65968476..9a1b9e55094dc 100644 --- a/components/intuiface/README.md +++ b/components/intuiface/README.md @@ -1,11 +1,13 @@ # Overview -With the Intuiface API, you can build a variety of interactive applications, -including: +The Intuiface API lets you interact with your interactive experiences crafted in Intuiface, a platform for creating and deploying interactive digital content. With Pipedream's robust serverless platform, you can automate tasks involving your Intuiface experiences. For instance, you can synchronize user interaction data with CRM systems, update live content dynamically, or monitor and control experiences remotely. -- Educational games -- Information dashboards -- Training simulations -- Marketing experiences +# Example Use Cases -And more! +- **Real-time Analytics to Data Warehouse**: Sync interaction data from Intuiface experiences to a data warehouse like BigQuery in real-time. Each time users interact with your content, send that data to BigQuery for in-depth analytics and visualization. + +- **Dynamic Content Updates**: Automatically fetch and update content within your Intuiface experiences using external sources such as CMS or databases. When new content is added to your CMS, trigger an update in your Intuiface experience to reflect changes immediately. + +- **Experience Health Monitoring and Alerts**: Regularly check the status of your Intuiface experiences and trigger alerts if any issues are detected. Use Pipedream's cron job feature to schedule regular API calls to Intuiface, and integrate with Slack to send real-time notifications to your team if an experience goes offline or experiences errors. + +Please note that the provided API link in the question is associated with RAWG Video Game Database API, which seems to be incorrect in the context of Intuiface. Thus, the example use cases are based on typical capabilities of an Intuiface API, assuming it would operate similarly to other experience management platforms. If Intuiface has a different set of capabilities or if there's a different API URL, the examples provided would need to be adjusted to fit those specific features and endpoints. diff --git a/components/invidious/actions/search-suggestions/search-suggestions.mjs b/components/invidious/actions/search-suggestions/search-suggestions.mjs new file mode 100644 index 0000000000000..4dbe10a2edccb --- /dev/null +++ b/components/invidious/actions/search-suggestions/search-suggestions.mjs @@ -0,0 +1,42 @@ +import app from "../../invidious.app.mjs"; + +export default { + key: "invidious-search-suggestions", + name: "Get Search Suggestions", + description: "Get search suggestions for a given query. [See the documentation](https://docs.invidious.io/api/#get-apiv1searchsuggestions)", + version: "0.0.1", + type: "action", + props: { + app, + q: { + description: "The query to get search suggestions for.", + propDefinition: [ + app, + "query", + ], + }, + }, + methods: { + getSearchSuggestions(args = {}) { + return this.app._makeRequest({ + path: "/search/suggestions", + ...args, + }); + }, + }, + async run({ $ }) { + const { + getSearchSuggestions, + q, + } = this; + + const response = await getSearchSuggestions({ + $, + params: { + q, + }, + }); + $.export("$summary", "Successfully fetched search suggestions."); + return response; + }, +}; diff --git a/components/invidious/actions/search/search.mjs b/components/invidious/actions/search/search.mjs new file mode 100644 index 0000000000000..00c6fa943e4d9 --- /dev/null +++ b/components/invidious/actions/search/search.mjs @@ -0,0 +1,117 @@ +import utils from "../../common/utils.mjs"; +import app from "../../invidious.app.mjs"; + +export default { + key: "invidious-search", + name: "Search Videos", + description: "Search for videos on Invidious. [See the documentation](https://docs.invidious.io/api/#get-apiv1search)", + version: "0.0.1", + type: "action", + props: { + app, + q: { + propDefinition: [ + app, + "query", + ], + }, + sort: { + type: "string", + label: "Sort By", + description: "The criteria to sort the search results by.", + optional: true, + options: [ + "relevance", + "rating", + "date", + "views", + ], + }, + date: { + type: "string", + label: "Date", + description: "The date to filter the search results by.", + optional: true, + options: [ + "hour", + "today", + "week", + "month", + "year", + ], + }, + type: { + type: "string", + label: "Type", + description: "The type of videos to search for. Default: `all`", + optional: true, + options: [ + "video", + "playlist", + "channel", + "movie", + "show", + "all", + ], + }, + features: { + type: "string[]", + label: "Features", + description: "The features to filter the search results by. (comma separated: e.g. `hd,subtitles,3d,live`", + optional: true, + options: [ + "hd", + "subtitles", + "creative_commons", + "3d", + "live", + "purchased", + "4k", + "360", + "location", + "hdr", + "vr180", + ], + }, + region: { + type: "string", + label: "Search Region", + description: "ISO 3166 country code (default: `US`)", + optional: true, + }, + }, + methods: { + searchVideos(args = {}) { + return this.app._makeRequest({ + path: "/search", + ...args, + }); + }, + }, + async run({ $ }) { + const { + searchVideos, + q, + sort, + date, + type, + features, + region, + } = this; + + const response = await searchVideos({ + $, + params: { + q, + sort, + date, + type, + features: features && utils.arrayToCommaSeparatedList(features), + region, + }, + }); + + $.export("$summary", `Successfully fetched \`${response?.length}\` search results.`); + return response; + }, +}; diff --git a/components/invidious/common/constants.mjs b/components/invidious/common/constants.mjs new file mode 100644 index 0000000000000..423221f82e83c --- /dev/null +++ b/components/invidious/common/constants.mjs @@ -0,0 +1,5 @@ +const VERSION_PATH = "/api/v1"; + +export default { + VERSION_PATH, +}; diff --git a/components/invidious/common/utils.mjs b/components/invidious/common/utils.mjs new file mode 100644 index 0000000000000..77b5446cf8236 --- /dev/null +++ b/components/invidious/common/utils.mjs @@ -0,0 +1,32 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +function arrayToCommaSeparatedList(array = []) { + return parseArray(array)?.join(","); +} + +export default { + arrayToCommaSeparatedList, +}; diff --git a/components/invidious/invidious.app.mjs b/components/invidious/invidious.app.mjs new file mode 100644 index 0000000000000..79ce12fbc2a37 --- /dev/null +++ b/components/invidious/invidious.app.mjs @@ -0,0 +1,38 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "invidious", + propDefinitions: { + query: { + type: "string", + label: "Search Query", + description: "The query to search for videos.", + }, + }, + methods: { + getUrl(path) { + const { instance_url: instanceUrl } = this.$auth; + const baseUrl = instanceUrl.endsWith("/") + ? instanceUrl.slice(0, -1) + : instanceUrl; + return `${baseUrl}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + Authorization: `Bearer ${this.$auth.api_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + }, +}; diff --git a/components/invidious/package.json b/components/invidious/package.json new file mode 100644 index 0000000000000..cd774661aa6d5 --- /dev/null +++ b/components/invidious/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/invidious", + "version": "0.1.0", + "description": "Pipedream Invidious Components", + "main": "invidious.app.mjs", + "keywords": [ + "pipedream", + "invidious" + ], + "homepage": "https://pipedream.com/apps/invidious", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/invision_community/README.md b/components/invision_community/README.md new file mode 100644 index 0000000000000..0867d57445e48 --- /dev/null +++ b/components/invision_community/README.md @@ -0,0 +1,11 @@ +# Overview + +The Invision Community API allows developers to integrate and automate community management tasks for forums and member interactions. By leveraging this API on Pipedream, you can create workflows that manage users, content, and notifications efficiently. This integration makes it possible to streamline community operations, engage users dynamically, and link community activities with other business functions like marketing, support, or CRM systems. + +# Example Use Cases + +- **Automated Member Welcome Messages**: Automatically send personalized welcome emails or direct messages to new members using the Invision Community API when they join your community. This can be extended by integrating with email services like SendGrid or communication platforms like Slack to deliver these messages. + +- **Synchronize Community Data with a CRM**: Keep your CRM up-to-date with the latest member data from your Invision Community. Whenever a user updates their profile or achieves a new rank, trigger a workflow to update their corresponding record in CRM systems like Salesforce or HubSpot. This ensures that the user's latest achievements and status are always reflected in your broader business processes. + +- **Moderation Alerts for Content Management**: Set up a workflow that monitors forum posts and topics for specific keywords or phrases that might require moderator attention. Use the API to flag these items and notify moderators via platforms like Discord or Microsoft Teams. This helps maintain community standards and ensures prompt response to content that violates guidelines. diff --git a/components/invision_community/actions/create-forum-topic/create-forum-topic.mjs b/components/invision_community/actions/create-forum-topic/create-forum-topic.mjs new file mode 100644 index 0000000000000..1664525a612e1 --- /dev/null +++ b/components/invision_community/actions/create-forum-topic/create-forum-topic.mjs @@ -0,0 +1,96 @@ +import { parseObject } from "../../common/utils.mjs"; +import invisionCommunity from "../../invision_community.app.mjs"; + +export default { + key: "invision_community-create-forum-topic", + name: "Create Forum Topic", + description: "Creates a new forum topic. [See the documentation](https://invisioncommunity.com/developers/rest-api?endpoint=forums/topics/postindex)", + version: "0.0.1", + type: "action", + props: { + invisionCommunity, + forumId: { + propDefinition: [ + invisionCommunity, + "forumId", + ], + }, + title: { + propDefinition: [ + invisionCommunity, + "title", + ], + }, + postContent: { + propDefinition: [ + invisionCommunity, + "postContent", + ], + }, + author: { + propDefinition: [ + invisionCommunity, + "authorId", + ], + }, + tags: { + propDefinition: [ + invisionCommunity, + "tags", + ], + optional: true, + }, + openTime: { + propDefinition: [ + invisionCommunity, + "openTime", + ], + optional: true, + }, + closeTime: { + propDefinition: [ + invisionCommunity, + "closeTime", + ], + optional: true, + }, + hidden: { + propDefinition: [ + invisionCommunity, + "hidden", + ], + }, + pinned: { + propDefinition: [ + invisionCommunity, + "pinned", + ], + }, + featured: { + propDefinition: [ + invisionCommunity, + "featured", + ], + }, + }, + async run({ $ }) { + + const response = await this.invisionCommunity.createForumTopic({ + $, + params: { + forum: this.forumId, + title: this.title, + post: this.postContent, + author: this.author, + tags: parseObject(this.tags)?.join(","), + open_time: this.openTime, + close_time: this.closeTime, + hidden: +this.hidden, + pinned: +this.pinned, + featured: +this.featured, + }, + }); + $.export("$summary", `Successfully created forum topic with title "${this.title}"`); + return response; + }, +}; diff --git a/components/invision_community/actions/create-member/create-member.mjs b/components/invision_community/actions/create-member/create-member.mjs new file mode 100644 index 0000000000000..c879cc8bc8c26 --- /dev/null +++ b/components/invision_community/actions/create-member/create-member.mjs @@ -0,0 +1,61 @@ +import invisionCommunity from "../../invision_community.app.mjs"; + +export default { + key: "invision_community-create-member", + name: "Create Member", + description: "Creates a new member. [See the documentation](https://invisioncommunity.com/developers/rest-api?endpoint=core/members/postindex)", + version: "0.0.1", + type: "action", + props: { + invisionCommunity, + name: { + propDefinition: [ + invisionCommunity, + "name", + ], + }, + email: { + propDefinition: [ + invisionCommunity, + "email", + ], + }, + password: { + propDefinition: [ + invisionCommunity, + "password", + ], + optional: true, + }, + groupId: { + propDefinition: [ + invisionCommunity, + "groupId", + ], + optional: true, + }, + validated: { + propDefinition: [ + invisionCommunity, + "validated", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.invisionCommunity.createMember({ + $, + params: { + name: this.name, + email: this.email, + password: this.password, + group: this.groupId, + registrationIpAddress: this.registrationIpAddress, + validated: this.validated, + }, + }); + + $.export("$summary", `Successfully created member with ID ${response.id}`); + return response; + }, +}; diff --git a/components/invision_community/actions/update-member/update-member.mjs b/components/invision_community/actions/update-member/update-member.mjs new file mode 100644 index 0000000000000..7f8925409e79f --- /dev/null +++ b/components/invision_community/actions/update-member/update-member.mjs @@ -0,0 +1,69 @@ +import invisionCommunity from "../../invision_community.app.mjs"; + +export default { + key: "invision_community-update-member", + name: "Update Member", + description: "Updates an existing member's details. [See the documentation](https://invisioncommunity.com/developers/rest-api?endpoint=core/members/postitem)", + version: "0.0.1", + type: "action", + props: { + invisionCommunity, + memberId: { + propDefinition: [ + invisionCommunity, + "memberId", + ], + }, + name: { + propDefinition: [ + invisionCommunity, + "name", + ], + optional: true, + }, + email: { + propDefinition: [ + invisionCommunity, + "email", + ], + optional: true, + }, + password: { + propDefinition: [ + invisionCommunity, + "password", + ], + optional: true, + }, + groupId: { + propDefinition: [ + invisionCommunity, + "groupId", + ], + optional: true, + }, + validated: { + propDefinition: [ + invisionCommunity, + "validated", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.invisionCommunity.updateMember({ + $, + memberId: this.memberId, + params: { + name: this.name, + email: this.email, + password: this.password, + group: this.groupId, + registrationIpAddress: this.registrationIpAddress, + validated: this.validated, + }, + }); + $.export("$summary", `Successfully updated member with Id: ${this.memberId}`); + return response; + }, +}; diff --git a/components/invision_community/common/utils.mjs b/components/invision_community/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/invision_community/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/invision_community/invision_community.app.mjs b/components/invision_community/invision_community.app.mjs new file mode 100644 index 0000000000000..f663020237b54 --- /dev/null +++ b/components/invision_community/invision_community.app.mjs @@ -0,0 +1,220 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "invision_community", + propDefinitions: { + name: { + type: "string", + label: "Name", + description: "The name of the new member.", + }, + email: { + type: "string", + label: "Email", + description: "The email of the new member.", + }, + password: { + type: "string", + label: "Password", + description: "The password of the new member.", + }, + groupId: { + type: "integer", + label: "Group ID", + description: "The group ID of the new member.", + async options({ page }) { + const { results: data } = await this.listGroups({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + validated: { + type: "boolean", + label: "Validated", + description: "Whether the new member is validated.", + }, + memberId: { + type: "integer", + label: "Member ID", + description: "The ID of the member to update.", + async options({ page }) { + const { results: data } = await this.listMembers({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + forumId: { + type: "integer", + label: "Forum ID", + description: "The ID of the forum to create the topic in.", + async options({ page }) { + const { results: data } = await this.listForums({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + title: { + type: "string", + label: "Title", + description: "The title of the new topic.", + }, + postContent: { + type: "string", + label: "Post Content", + description: "The content of the first post in the new topic.", + }, + authorId: { + type: "integer", + label: "Author Id", + description: "The ID of the author of the new topic.", + default: 0, + async options({ page }) { + const { results: data } = await this.listMembers({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + tags: { + type: "string[]", + label: "Tags", + description: "The tags for the new topic.", + }, + openTime: { + type: "string", + label: "Open Time", + description: "The open time of the new topic Format: YYYY-MM-DDTHH:MM:SS.", + }, + closeTime: { + type: "string", + label: "Close Time", + description: "The close time of the new topic. Format: YYYY-MM-DDTHH:MM:SS.", + }, + hidden: { + type: "boolean", + label: "Hidden", + description: "Whether the new topic is hidden.", + }, + pinned: { + type: "boolean", + label: "Pinned", + description: "Whether the new topic is pinned.", + }, + featured: { + type: "boolean", + label: "Featured", + description: "Whether the new topic is featured.", + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.url}/api`; + }, + _auth() { + return { + username: `${this.$auth.api_key}`, + password: "", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + auth: this._auth(), + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/core/webhooks", + ...opts, + }); + }, + deleteWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/core/webhooks/${webhookId}`, + }); + }, + createMember(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/core/members", + ...opts, + }); + }, + listForums(opts = {}) { + return this._makeRequest({ + path: "/forums/forums", + ...opts, + }); + }, + listGroups(opts = {}) { + return this._makeRequest({ + path: "/core/groups", + ...opts, + }); + }, + listMembers(opts = {}) { + return this._makeRequest({ + path: "/core/members", + ...opts, + }); + }, + updateMember({ + memberId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/core/members/${memberId}`, + ...opts, + }); + }, + createForumTopic(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/forums/topics", + ...opts, + }); + }, + }, +}; + diff --git a/components/invision_community/package.json b/components/invision_community/package.json new file mode 100644 index 0000000000000..34845ca41e5c2 --- /dev/null +++ b/components/invision_community/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/invision_community", + "version": "0.1.0", + "description": "Pipedream Invision Community Components", + "main": "invision_community.app.mjs", + "keywords": [ + "pipedream", + "invision_community" + ], + "homepage": "https://pipedream.com/apps/invision_community", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/invision_community/sources/common/base.mjs b/components/invision_community/sources/common/base.mjs new file mode 100644 index 0000000000000..b00f859b4868e --- /dev/null +++ b/components/invision_community/sources/common/base.mjs @@ -0,0 +1,52 @@ +import invisionCommunity from "../../invision_community.app.mjs"; + +export default { + props: { + invisionCommunity, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + }, + methods: { + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + _getHookId() { + return this.db.get("hookId"); + }, + generateMeta(body) { + return { + id: body.id, + summary: this.getSummary(body), + ts: Date.parse(body.joined || body.publish_date || body.date), + }; + }, + }, + hooks: { + async activate() { + const webhook = await this.invisionCommunity.createWebhook({ + params: { + url: this.http.endpoint, + events: this.getEvents(), + content_header: "application/json", + }, + }); + + this._setHookId(webhook.id); + }, + async deactivate() { + const hookId = this._getHookId(); + return await this.invisionCommunity.deleteWebhook(hookId); + }, + }, + async run({ body }) { + this.http.respond({ + status: 200, + }); + + this.$emit(body, this.generateMeta(body)); + + }, +}; diff --git a/components/invision_community/sources/new-forum-topic-instant/new-forum-topic-instant.mjs b/components/invision_community/sources/new-forum-topic-instant/new-forum-topic-instant.mjs new file mode 100644 index 0000000000000..fd047d1bc4861 --- /dev/null +++ b/components/invision_community/sources/new-forum-topic-instant/new-forum-topic-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "invision_community-new-forum-topic-instant", + name: "New Forum Topic (Instant)", + description: "Emit new event when a new topic is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "forumsTopic_create", + ]; + }, + getSummary(body) { + return `New topic with Id: ${body.id} created successfully!`; + }, + }, + sampleEmit, +}; diff --git a/components/invision_community/sources/new-forum-topic-instant/test-event.mjs b/components/invision_community/sources/new-forum-topic-instant/test-event.mjs new file mode 100644 index 0000000000000..d92d996d6ae12 --- /dev/null +++ b/components/invision_community/sources/new-forum-topic-instant/test-event.mjs @@ -0,0 +1,155 @@ +export default { + "id": 4, + "title": "Topic Title", + "forum": { + "id": 2, + "name": "A Test Forum", + "path": "A Test Category > A Test Forum", + "type": "discussions", + "topics": 2, + "url": "https://123123.invisionservice.com/forum/2-a-test-forum/", + "parentId": 1, + "permissions": { + "perm_id": 2, + "perm_view": "*", + "perm_2": "*", + "perm_3": "3,4,6", + "perm_4": "3,4,6", + "perm_5": "3,4,6", + "perm_6": null, + "perm_7": null + }, + "club": 0 + }, + "posts": 1, + "views": 0, + "prefix": null, + "tags": [], + "firstPost": { + "id": 4, + "item_id": 4, + "author": { + "id": 1, + "name": "Author", + "title": null, + "timeZone": "America/Sao_Paulo", + "formattedName": "Author", + "primaryGroup": { + "id": 4, + "name": "Administrators", + "formattedName": "Administrators" + }, + "secondaryGroups": [], + "email": "email@test.com", + "joined": "2024-06-27T14:05:00Z", + "registrationIpAddress": "", + "warningPoints": 0, + "reputationPoints": 0, + "photoUrl": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201024%201024%22%20style%3D%22background", + "photoUrlIsDefault": true, + "coverPhotoUrl": "", + "profileUrl": "https://123123.invisionservice.com/profile/1-author/", + "validating": false, + "posts": 3, + "lastActivity": "2024-06-27T15:52:43Z", + "lastVisit": "2024-06-27T15:52:43Z", + "lastPost": "2024-06-27T15:53:37Z", + "birthday": null, + "profileViews": 0, + "customFields": { + "1": { + "name": "Personal Information", + "fields": { + "1": { + "name": "About Me", + "value": null + } + } + } + }, + "rank": { + "id": 3, + "name": "Newbie", + "icon": "//content.invisioncic.com/123123/monthly_2024_06/1_Newbie.svg", + "points": 0 + }, + "achievements_points": 10, + "allowAdminEmails": false, + "completed": true + }, + "date": "2024-06-27T15:53:37Z", + "content": "

\n\tTopic Content\n

\n", + "hidden": false, + "url": "https://123123.invisionservice.com/topic/4-topic-title/?do=findComment&comment=4", + "reactions": [] + }, + "lastPost": { + "id": 4, + "item_id": 4, + "author": { + "id": 1, + "name": "Author", + "title": null, + "timeZone": "America/Sao_Paulo", + "formattedName": "Author", + "primaryGroup": { + "id": 4, + "name": "Administrators", + "formattedName": "Administrators" + }, + "secondaryGroups": [], + "email": "email@test.com", + "joined": "2024-06-27T14:05:00Z", + "registrationIpAddress": "", + "warningPoints": 0, + "reputationPoints": 0, + "photoUrl": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201024%201024%22%20style%3D%22background", + "photoUrlIsDefault": true, + "coverPhotoUrl": "", + "profileUrl": "https://123123.invisionservice.com/profile/1-author/", + "validating": false, + "posts": 3, + "lastActivity": "2024-06-27T15:52:43Z", + "lastVisit": "2024-06-27T15:52:43Z", + "lastPost": "2024-06-27T15:53:37Z", + "birthday": null, + "profileViews": 0, + "customFields": { + "1": { + "name": "Personal Information", + "fields": { + "1": { + "name": "About Me", + "value": null + } + } + } + }, + "rank": { + "id": 3, + "name": "Newbie", + "icon": "//content.invisioncic.com/123123/monthly_2024_06/1_Newbie.svg", + "points": 0 + }, + "achievements_points": 10, + "allowAdminEmails": false, + "completed": true + }, + "date": "2024-06-27T15:53:37Z", + "content": "

\n\tTopic Content\n

\n", + "hidden": false, + "url": "https://123123.invisionservice.com/topic/4-topic-title/?do=findComment&comment=4", + "reactions": [] + }, + "bestAnswer": null, + "locked": false, + "hidden": false, + "pinned": false, + "featured": false, + "archived": false, + "poll": null, + "url": "https://123123.invisionservice.com/topic/4-topic-title/", + "rating": 0, + "is_future_entry": 0, + "publish_date": "2024-06-27T15:53:37Z" +} \ No newline at end of file diff --git a/components/invision_community/sources/new-member-instant/new-member-instant.mjs b/components/invision_community/sources/new-member-instant/new-member-instant.mjs new file mode 100644 index 0000000000000..acccdf7ec1620 --- /dev/null +++ b/components/invision_community/sources/new-member-instant/new-member-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "invision_community-new-member-instant", + name: "New Member (Instant)", + description: "Emit new event when a new member account is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "member_create", + ]; + }, + getSummary(body) { + return `New member with Id: ${body.id} created successfully!`; + }, + }, + sampleEmit, +}; diff --git a/components/invision_community/sources/new-member-instant/test-event.mjs b/components/invision_community/sources/new-member-instant/test-event.mjs new file mode 100644 index 0000000000000..d64634122d70f --- /dev/null +++ b/components/invision_community/sources/new-member-instant/test-event.mjs @@ -0,0 +1,49 @@ +export default { + "id": 5, + "name": "Member Name", + "title": null, + "timeZone": "UTC", + "formattedName": "Member Name", + "primaryGroup": { + "id": 3, + "name": "Members", + "formattedName": "Members" + }, + "secondaryGroups": [], + "email": "email@test.com", + "joined": "2024-06-27T15:47:02Z", + "registrationIpAddress": "123.123.12.12", + "warningPoints": 0, + "reputationPoints": 0, + "photoUrl": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201024%201024%22%20style%3D%22background%", + "photoUrlIsDefault": true, + "coverPhotoUrl": "", + "profileUrl": "https://123123.invisionservice.com/profile/5-member-name/", + "validating": false, + "posts": 0, + "lastActivity": null, + "lastVisit": null, + "lastPost": null, + "birthday": null, + "profileViews": null, + "customFields": { + "1": { + "name": "Personal Information", + "fields": { + "1": { + "name": "About Me", + "value": null + } + } + } + }, + "rank": { + "id": 3, + "name": "Newbie", + "icon": "//content.invisioncic.com/123123/monthly_2024_06/1_Newbie.svg", + "points": 0 + }, + "achievements_points": null, + "allowAdminEmails": true, + "completed": true +} \ No newline at end of file diff --git a/components/invision_community/sources/new-topic-post-instant/new-topic-post-instant.mjs b/components/invision_community/sources/new-topic-post-instant/new-topic-post-instant.mjs new file mode 100644 index 0000000000000..9e9f996720c3f --- /dev/null +++ b/components/invision_community/sources/new-topic-post-instant/new-topic-post-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "invision_community-new-topic-post-instant", + name: "New Topic Post (Instant)", + description: "Emit new event when a new post in a topic is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "forumsTopicPost_create", + ]; + }, + getSummary(body) { + return `New post with Id: ${body.id} created successfully!`; + }, + }, + sampleEmit, +}; diff --git a/components/invision_community/sources/new-topic-post-instant/test-event.mjs b/components/invision_community/sources/new-topic-post-instant/test-event.mjs new file mode 100644 index 0000000000000..efdf2b95f7004 --- /dev/null +++ b/components/invision_community/sources/new-topic-post-instant/test-event.mjs @@ -0,0 +1,58 @@ +export default { + "id": 5, + "item_id": 4, + "author": { + "id": 1, + "name": "Author", + "title": null, + "timeZone": "America/Sao_Paulo", + "formattedName": "Author", + "primaryGroup": { + "id": 4, + "name": "Administrators", + "formattedName": "Administrators" + }, + "secondaryGroups": [], + "email": "email@test.com", + "joined": "2024-06-27T14:05:00Z", + "registrationIpAddress": "", + "warningPoints": 0, + "reputationPoints": 0, + "photoUrl": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201024%201024%22%20style%3D%22background", + "photoUrlIsDefault": true, + "coverPhotoUrl": "", + "profileUrl": "https://123123.invisionservice.com/profile/1-author/", + "validating": false, + "posts": 4, + "lastActivity": "2024-06-27T15:52:43Z", + "lastVisit": "2024-06-27T15:52:43Z", + "lastPost": "2024-06-27T15:55:05Z", + "birthday": null, + "profileViews": 0, + "customFields": { + "1": { + "name": "Personal Information", + "fields": { + "1": { + "name": "About Me", + "value": null + } + } + } + }, + "rank": { + "id": 3, + "name": "Newbie", + "icon": "//content.invisioncic.com/123123/monthly_2024_06/1_Newbie.svg", + "points": 0 + }, + "achievements_points": 25, + "allowAdminEmails": false, + "completed": true + }, + "date": "2024-06-27T15:55:05Z", + "content": "

\n\tPost Content\n

\n", + "hidden": false, + "url": "https://123123.invisionservice.com/topic/4-topic-title/?do=findComment&comment=5", + "reactions": [] +} \ No newline at end of file diff --git a/components/invoice_ninja/README.md b/components/invoice_ninja/README.md index f47a83ed32b2f..84a9fac6388fc 100644 --- a/components/invoice_ninja/README.md +++ b/components/invoice_ninja/README.md @@ -1,9 +1,11 @@ # Overview -With the Invoice Ninja API, you can build applications that can: +The Invoice Ninja API lets you create, view, update, and delete elements of your invoicing process such as invoices, clients, payments, and products. Leveraging this API on Pipedream allows you to automate invoice generation, synchronize client data across platforms, and trigger actions based on payment statuses. With Pipedream's serverless platform, you can connect Invoice Ninja to numerous apps and orchestrate workflows that save time, reduce errors, and enhance data consistency. -- Create and manage invoices -- Accept payments -- Generate reports -- Manage clients -- And much more! +# Example Use Cases + +- **Automated Invoice Generation from CRM Deals**: When a deal is marked as won in a CRM like Salesforce, trigger a workflow to automatically create an invoice in Invoice Ninja with the deal details. This ensures timely billing and reduces manual data entry. + +- **Payment Status Sync to Accounting Software**: Whenever a payment is recorded in Invoice Ninja, use Pipedream to update the payment status in accounting software like QuickBooks. This keeps financial records up to date without manual intervention. + +- **Slack Notifications for Overdue Invoices**: Set up a scheduled workflow to check for overdue invoices in Invoice Ninja and send alert messages to a designated Slack channel. This helps in prompt follow-up for payments and improves cash flow management. diff --git a/components/invoice_ninja/package.json b/components/invoice_ninja/package.json new file mode 100644 index 0000000000000..a69e093d27bcd --- /dev/null +++ b/components/invoice_ninja/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/invoice_ninja", + "version": "0.6.0", + "description": "Pipedream invoice_ninja Components", + "main": "invoice_ninja.app.mjs", + "keywords": [ + "pipedream", + "invoice_ninja" + ], + "homepage": "https://pipedream.com/apps/invoice_ninja", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/invoiced/README.md b/components/invoiced/README.md index 27c8b25feab13..14b751732b235 100644 --- a/components/invoiced/README.md +++ b/components/invoiced/README.md @@ -1,18 +1,11 @@ # Overview -Invoiced is a powerful API that allows you to easily create and manage invoices -and payments. With Invoiced, you can: +The Invoiced API empowers users to automate billing and invoicing processes with ease. It facilitates the creation, sending, and management of invoices, along with tracking payments and customer interaction. By harnessing the Invoiced API on Pipedream, you can craft workflows that respond to business events in real-time, sync invoice data with accounting software, or trigger customer engagement actions upon payment statuses. -- Create and manage invoices -- Accept payments -- Automate collections -- Send reminders -- And much more! +# Example Use Cases -Here are some examples of what you can build with the Invoiced API: +- **Automated Invoice Generation and Delivery**: When a new sale is recorded in a CRM like Salesforce, Pipedream can trigger the Invoiced API to create and send a corresponding invoice to the customer, ensuring billing is prompt and tied directly to sales activity. -- A billing system for your business -- An invoicing system for your clients -- A payment gateway for your website -- An accounting system for your personal finances -- And much more! +- **Payment Status Monitoring and Alerts**: Use Pipedream to set up a workflow where payment status updates from Invoiced trigger notifications. For example, when an invoice is paid, you could send a Slack message to your finance team and update the customer’s record in a database like Airtable. + +- **Subscription Management on Payment Failure**: If Invoiced indicates a subscription payment has failed, Pipedream can automatically pause the user's access to the service and alert customer support to reach out via an email platform such as SendGrid. diff --git a/components/invoiced/package.json b/components/invoiced/package.json new file mode 100644 index 0000000000000..108b8066b6355 --- /dev/null +++ b/components/invoiced/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/invoiced", + "version": "0.6.0", + "description": "Pipedream invoiced Components", + "main": "invoiced.app.mjs", + "keywords": [ + "pipedream", + "invoiced" + ], + "homepage": "https://pipedream.com/apps/invoiced", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/invoicing_plus/invoicing_plus.app.mjs b/components/invoicing_plus/invoicing_plus.app.mjs new file mode 100644 index 0000000000000..0bb14ce61c272 --- /dev/null +++ b/components/invoicing_plus/invoicing_plus.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "invoicing_plus", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/invoicing_plus/package.json b/components/invoicing_plus/package.json new file mode 100644 index 0000000000000..be6a84f0034b1 --- /dev/null +++ b/components/invoicing_plus/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/invoicing_plus", + "version": "0.0.1", + "description": "Pipedream Invoicing.Plus Components", + "main": "invoicing_plus.app.mjs", + "keywords": [ + "pipedream", + "invoicing_plus" + ], + "homepage": "https://pipedream.com/apps/invoicing_plus", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/ip2location/README.md b/components/ip2location/README.md index f31802cc6bbb7..6790aa8664656 100644 --- a/components/ip2location/README.md +++ b/components/ip2location/README.md @@ -1,10 +1,11 @@ # Overview -IP2Location provides a number of API calls that can be used to determine the -location of an IP address. This can be useful for a number of applications, -including: - -- Determining the location of a user for targeted content -- Tracking the location of a user for security purposes -- Allowing or blocking access to content based on location -- And more! +The IP2Location API enables you to uncover valuable information about an IP address. With it, you can pinpoint a user's geographic location, such as country, region, city, and even latitude and longitude coordinates. This data is essential for various tasks like localizing content, analyzing traffic, or enhancing security by detecting potentially malicious activity. Using Pipedream, you can automate actions based on IP location data, integrating it with hundreds of other services without writing any code. + +# Example Use Cases + +- **Geo-targeted Content Personalization**: Craft personalized user experiences by deploying a Pipedream workflow that uses IP2Location to detect a visitor’s location. Combine this with a CMS like WordPress to dynamically tailor content on your website, showing region-specific offers, language, or currency. + +- **Security Alerts and Fraud Detection**: Implement a Pipedream workflow that leverages IP2Location for real-time analysis of IP addresses accessing your systems. Integrate with Slack or Email to get instant notifications when suspicious activity is detected, like logins from high-risk countries or multiple locations in a short span. + +- **Traffic Analysis and Reporting**: Use IP2Location in a Pipedream workflow to enrich logs with geolocation data. Integrate with data visualization tools like Google Sheets or Tableau to create dashboards that track and display user locations, helping you understand your global audience better. diff --git a/components/ip2location_io/README.md b/components/ip2location_io/README.md new file mode 100644 index 0000000000000..711c72587be2c --- /dev/null +++ b/components/ip2location_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The IP2Location.io API enables you to identify a user's geographical location based on their IP address. With it, you can fetch details like country, region, city, latitude, longitude, ZIP code, time zone, ISP, domain, and more. On Pipedream, this becomes a powerful tool to enrich event data, customize user experiences based on location, or even detect suspicious activities by comparing known user locations to new, anomalous ones. Since Pipedream can connect to countless APIs, you can automate processes that depend on geolocation information, triggering actions across different services. + +# Example Use Cases + +- **Personalize User Content**: Use IP2Location.io to tailor content on your website for visitors based on their location. When your Pipedream workflow receives a web request, it can call the API for location data and then push that info to your CMS to serve location-specific pages or ads. + +- **Security Alerts**: Set up a workflow that checks the location of user logins against their typical login locations. On detecting a login from an unusual location, automatically send alerts through Slack or email using Pipedream's built-in actions. + +- **Data Enrichment for Analytics**: Enhance your analytics by appending geolocation data to user activities. As events flow through Pipedream from your app, use IP2Location.io to add location context before saving the enriched data to a service like Google Sheets or your own database. diff --git a/components/ip2proxy/README.md b/components/ip2proxy/README.md index 9b98c3b01e92a..992ba91ed7d09 100644 --- a/components/ip2proxy/README.md +++ b/components/ip2proxy/README.md @@ -1,11 +1,11 @@ # Overview -The IP2Proxy API can be used to build applications that can perform the -following tasks: - -- Determine the country, region, city, ISP, domain name, and proxy type of an - IP address -- Detect if an IP address is a Tor exit node -- Detect if an IP address is a public proxy -- Detect if an IP address is a datacenter IP -- Detect if an IP address is blacklisted +The IP2Proxy API helps you detect and prevent fraud by identifying proxy and VPN traffic. With this API, you can programmatically check IP addresses and uncover whether they're originating from known data centers, residential proxies, or public VPNs. This is crucial for maintaining the integrity of your web services and preventing abuse. By integrating IP2Proxy with Pipedream, you can automate actions based on the traffic’s legitimacy, enrich data streams with proxy detection, and enhance security measures within your digital ecosystem. + +# Example Use Cases + +- **Automated Fraud Detection**: Create a Pipedream workflow that triggers whenever a new user signs up on your platform. The workflow uses the IP2Proxy API to check if the IP address is from a proxy or VPN. If it is, the workflow can automatically flag the account for review or restrict certain actions to prevent potential fraud. + +- **Dynamic Content Delivery**: Develop a Pipedream workflow that detects the type of IP address accessing your service and tailors content delivery accordingly. For instance, if IP2Proxy indicates a regular residential IP, provide full access, but for proxy IPs, limit access or serve different content to protect digital assets. + +- **Security Log Enrichment**: Construct a Pipedream workflow that receives logs from your security system. Each log entry containing an IP address is processed through the IP2Proxy API. The enriched logs then include data on whether the IP is associated with a proxy or VPN, which can be crucial for in-depth security analysis and threat hunting. diff --git a/components/ip2whois/README.md b/components/ip2whois/README.md index ca78827b93efc..031970925471d 100644 --- a/components/ip2whois/README.md +++ b/components/ip2whois/README.md @@ -1,10 +1,11 @@ # Overview -With IP2WHOIS, you can build applications that: - -- Look up the whois record for an IP address -- Get information on a domain name -- Get reverse DNS (PTR) records for an IP address -- Verify that an email address exists -- Get IP addresses for a domain name -- And more! +The IP2WHOIS API is a lookup tool that can fetch WHOIS information for a given IP address or domain. This data includes registration details, contact information, and sometimes even personal or organization-related data of the domain owner. Leveraging this API on Pipedream allows you to create automated workflows that can enrich IP data, monitor domain registration changes, or integrate WHOIS information into security analysis. By tapping into Pipedream's capabilities, you can trigger actions based on this data across various apps and services. + +# Example Use Cases + +- **Domain Registration Monitoring**: Automate the process of monitoring domain registrations for your brand. Trigger an alert or notification when a domain is registered with your brand name, using Pipedream to send the alert through email, Slack, or another messaging service. + +- **Security Analysis and Alerting**: Use IP2WHOIS as part of a security workflow to analyze and alert on domain or IP-related threats. When an IP is flagged by your internal systems, automatically fetch WHOIS data and cross-reference it with threat databases, then send the analysis to a SIEM system like Splunk for further investigation. + +- **Lead Generation and Enrichment**: Improve your lead generation process by enriching lead data with domain registration details. When a new sign-up occurs, use Pipedream to trigger a WHOIS lookup and append the domain information to the lead's profile in a CRM like Salesforce or HubSpot. diff --git a/components/ipbase/README.md b/components/ipbase/README.md new file mode 100644 index 0000000000000..cb8a9781a84bb --- /dev/null +++ b/components/ipbase/README.md @@ -0,0 +1,11 @@ +# Overview + +The ipbase API offers the ability to get detailed information about IP addresses, including geolocation data, ISP details, and connection type. Using this API on Pipedream allows for the automation of tasks like analyzing web traffic, customizing user content based on location, and enhancing cybersecurity measures by detecting unusual IP activities. On Pipedream, you can integrate this API into workflows that trigger automatically, process the data, and connect with other services for comprehensive solutions. + +# Example Use Cases + +- **Geo-targeted Content Delivery**: Use the ipbase API in a Pipedream workflow to customize content based on the user's location. When a user visits your website, capture their IP and use ipbase to fetch their geolocation. With this data in hand, tailor the website content or ads to align with their local language, currency, or cultural preferences. + +- **Security Alert System**: Develop a workflow that leverages ipbase to monitor access to your systems. Each time a login attempt is made, the API can check the IP address for geolocation and known threat databases. If the login attempt comes from a suspicious location or is on a threat list, the workflow can automatically alert your security team via an app like Slack or send an email through a service like Gmail. + +- **Traffic Analysis Dashboard**: Build a Pipedream workflow that collects IP addresses from your web server logs. Use ipbase to enrich those IPs with geolocation data, then aggregate and push this enriched data into a business intelligence tool like Google Sheets or a dashboard app like Grafana. This setup can give real-time insights into user demographics and help make data-driven decisions for marketing campaigns. diff --git a/components/ipdata_co/README.md b/components/ipdata_co/README.md index 442bcab60aee7..69ca84ca40b44 100644 --- a/components/ipdata_co/README.md +++ b/components/ipdata_co/README.md @@ -1,8 +1,11 @@ # Overview -Examples of what you can build with the ipdata.co API: +The ipdata.co API allows you to enrich IP addresses with geolocation and other relevant data points. With this API integrated into Pipedream, you can automate actions based on the physical location or details of an IP address. This enables a wide range of applications such as customizing content based on user location, flagging fraudulent activity, or tailoring marketing campaigns to specific regions. -- A tool to check the IP address of a website or server -- A tool to geolocate an IP address -- A tool to track the location of a user or device -- A tool to build a custom map of IP addresses +# Example Use Cases + +- **Location-Based Content Customization**: Use ipdata.co to determine the visitor’s geographic location and integrate this data with a CMS like WordPress on Pipedream. Dynamically serve localized content, modify language settings, or alter the webpage's currency and pricing based on the visitor's country or city for a personalized browsing experience. + +- **Security and Fraud Detection**: Leverage the IP data to analyze traffic patterns and flag suspicious activity. Combine ipdata.co with apps like Slack to send real-time alerts to your security team when an IP address from an unusual location attempts to access your system or when multiple logins are detected from varying geolocations, indicating potential account sharing or fraud. + +- **Targeted Marketing Automation**: Integrate ipdata.co with email marketing platforms such as Mailchimp on Pipedream. Craft and dispatch targeted email campaigns by segmenting your audience based on their IP-derived geolocation. This allows for more relevant offers, increasing engagement and conversion rates for regional promotions and events. diff --git a/components/ipdata_co/package.json b/components/ipdata_co/package.json new file mode 100644 index 0000000000000..02c3324050e1c --- /dev/null +++ b/components/ipdata_co/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/ipdata_co", + "version": "0.6.0", + "description": "Pipedream ipdata_co Components", + "main": "ipdata_co.app.mjs", + "keywords": [ + "pipedream", + "ipdata_co" + ], + "homepage": "https://pipedream.com/apps/ipdata_co", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/ipinfo_io/README.md b/components/ipinfo_io/README.md new file mode 100644 index 0000000000000..5ccfecdec69d6 --- /dev/null +++ b/components/ipinfo_io/README.md @@ -0,0 +1,12 @@ +# Overview + +IPinfo.io API offers a fast and simple way to gather IP address data, such as geolocation, ISP details, and more. When used within Pipedream, this API becomes a powerful tool to enrich data workflows, trigger location-based actions, and analyze web traffic. With Pipedream's serverless platform, you can easily integrate IPinfo.io with numerous apps to create custom automations without managing infrastructure. + +# Example Use Cases + +- **Enrich User Data for Analytics**: Capture IP addresses from webhooks or logs and use the IPinfo.io API in Pipedream to enrich them with location data. Feed the enhanced data into analytics platforms like Google Analytics for deeper insights into your audience's demographics. + +- **Customize Content Based on User Location**: Implement a serverless function on Pipedream that uses IPinfo.io to determine a user's location. Connect this to a CMS like WordPress to dynamically tailor content or ads shown to the user based on their geographic region. + +- **Security Alerts for Unusual Access**: Set up a Pipedream workflow that monitors your service's access logs. Use IPinfo.io API to analyze IP addresses and flag any from unexpected locations. Combine this with an alerting app like Slack to notify your team instantly about potential security threats. +. diff --git a/components/iqair_airvisual/README.md b/components/iqair_airvisual/README.md index c48c8bd2d280e..d06bdd53f8c8f 100644 --- a/components/iqair_airvisual/README.md +++ b/components/iqair_airvisual/README.md @@ -1,10 +1,11 @@ # Overview -With the IQAir AirVisual API, you can build applications that track and display -air quality data. Here are some examples of what you can build: - -- A real-time air quality map that shows air quality data for a specific - location -- A historical air quality data viewer that shows trends over time -- An air quality forecast tool that predicts air quality for a specific - location +The IQAir AirVisual API provides access to air quality data from around the world, including real-time pollution levels, forecasts, and historical data. With this data at your fingertips on Pipedream, you can build tailored automations that respond to air quality changes. Imagine triggering notifications, adjusting smart home settings, or compiling analytical reports—all based on accurate environmental insights. + +# Example Use Cases + +- **Smart Home Air Quality Adjustment**: Automatically control air purifiers and HVAC systems in a smart home setup. When the API reports poor air quality levels in your area, trigger an IoT device to turn on or adjust settings, ensuring clean indoor air. + +- **Health Alert System**: Create a personal health alert system for those with respiratory conditions. Set up a workflow that monitors air quality levels and sends alerts via SMS or email when they reach a threshold that could affect health. + +- **Environmental Data Reporting**: Generate regular environmental reports for a specific location. Schedule a Pipedream workflow to retrieve weekly or monthly air quality data from the API and compile it into a report, which can be saved to Google Drive or sent to a list of subscribers via an email service like SendGrid. diff --git a/components/ironclad/actions/create-record/create-record.mjs b/components/ironclad/actions/create-record/create-record.mjs new file mode 100644 index 0000000000000..e6b3a19ba4571 --- /dev/null +++ b/components/ironclad/actions/create-record/create-record.mjs @@ -0,0 +1,103 @@ +import ironclad from "../../ironclad.app.mjs"; + +export default { + key: "ironclad-create-record", + name: "Create Record", + description: "Creates a new record in Ironclad. [See the documentation](https://developer.ironcladapp.com/reference/create-a-record)", + version: "0.0.1", + type: "action", + props: { + ironclad, + name: { + type: "string", + label: "Name", + description: "Name of the record", + }, + type: { + propDefinition: [ + ironclad, + "recordType", + ], + }, + links: { + propDefinition: [ + ironclad, + "recordId", + ], + type: "string[]", + label: "Links", + description: "Record ID's to link to the new record", + }, + parent: { + propDefinition: [ + ironclad, + "recordId", + ], + label: "Parent", + description: "Record ID to be set as the parent of the current record", + }, + children: { + propDefinition: [ + ironclad, + "recordId", + ], + type: "string[]", + label: "Children", + description: "Record ID's to be set as child records of the current record", + }, + properties: { + propDefinition: [ + ironclad, + "properties", + ], + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (!this.properties?.length) { + return props; + } + const { properties } = await this.ironclad.getRecordsSchema(); + for (const property of this.properties) { + props[property] = { + type: properties[property].type === "boolean" + ? "boolean" + : "string", + label: properties[property].displayName, + description: properties[property].description ?? `Value of ${properties[property].displayName}`, + }; + } + return props; + }, + async run({ $ }) { + const { properties } = await this.ironclad.getRecordsSchema(); + const propertiesData = {}; + for (const property of this.properties) { + propertiesData[property] = { + type: properties[property].type, + value: this[property], + }; + } + + const response = await this.ironclad.createRecord({ + $, + data: { + name: this.name, + type: this.type, + links: this.links?.length && this.links.map((link) => ({ + recordId: link, + })), + parent: this.parent && { + recordId: this.parent, + }, + children: this.children?.length && this.children.map((child) => ({ + recordId: child, + })), + properties: propertiesData, + }, + }); + $.export("$summary", `Created record with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/ironclad/actions/launch-workflow/launch-workflow.mjs b/components/ironclad/actions/launch-workflow/launch-workflow.mjs new file mode 100644 index 0000000000000..f8f7c1d8b239b --- /dev/null +++ b/components/ironclad/actions/launch-workflow/launch-workflow.mjs @@ -0,0 +1,105 @@ +import ironclad from "../../ironclad.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; +import { + getAttributeDescription, parseValue, +} from "../../common/utils.mjs"; + +export default { + key: "ironclad-launch-workflow", + name: "Launch Workflow", + description: "Launches a new workflow in Ironclad. [See the documentation](https://developer.ironcladapp.com/reference/launch-a-new-workflow)", + version: "0.0.1", + type: "action", + props: { + ironclad, + templateId: { + propDefinition: [ + ironclad, + "templateId", + ], + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (!this.templateId) { + return props; + } + const { schema } = await this.ironclad.getWorkflowSchema({ + templateId: this.templateId, + }); + for (const [ + key, + value, + ] of Object.entries(schema)) { + if (!value.readOnly) { + props[key] = { + type: value.type === "boolean" + ? "boolean" + : value.type === "array" + ? "string[]" + : "string", + label: value.displayName, + description: getAttributeDescription(value), + optional: (!(key === "counterpartyName") && !value.displayName.toLowerCase().includes("required")), + }; + if (key === "paperSource") { + props[key].options = [ + "Counterparty paper", + "Our paper", + ]; + } + if (key === "recordType") { + const { recordTypes } = await this.ironclad.getRecordsSchema(); + props[key].options = Object.values(recordTypes) + .map((recordType) => recordType.displayName); + } + } + } + return props; + }, + async run({ $ }) { + const { + ironclad, + templateId, + ...attributes + } = this; + + const parsedAttributes = {}; + for (const [ + key, + value, + ] of Object.entries(attributes)) { + parsedAttributes[key] = parseValue(value); + } + + try { + const response = await ironclad.launchWorkflow({ + $, + params: { + useDefaultValues: true, + }, + data: { + template: templateId, + attributes: parsedAttributes, + }, + }); + $.export("$summary", `Workflow launched successfully with ID ${response.id}`); + return response; + } catch (error) { + const msg = JSON.parse(error.message); + const { schema } = await ironclad.getWorkflowSchema({ + templateId, + }); + if (msg.code === "MISSING_PARAM") { + const paramNames = (JSON.parse(msg.param)).map((p) => `\`${schema[p].displayName}\``); + throw new ConfigurationError(`Please enter or update the following required parameters: ${paramNames.join(", ")}`); + } + if (msg.code === "INVALID_PARAM") { + const paramName = schema[msg.metadata.keyPath].displayName; + throw new ConfigurationError(`Invalid parameter: \`${paramName}\`. ${msg.message}`); + } + throw new ConfigurationError(msg.message); + } + }, +}; diff --git a/components/ironclad/actions/update-workflow/update-workflow.mjs b/components/ironclad/actions/update-workflow/update-workflow.mjs new file mode 100644 index 0000000000000..4c3a222736541 --- /dev/null +++ b/components/ironclad/actions/update-workflow/update-workflow.mjs @@ -0,0 +1,112 @@ +import ironclad from "../../ironclad.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; +import { + getAttributeDescription, parseValue, +} from "../../common/utils.mjs"; + +export default { + key: "ironclad-update-workflow", + name: "Update Workflow Metadata", + description: "Updates the metadata of an existing workflow. [See the documentation]()", + version: "0.0.1", + type: "action", + props: { + ironclad, + workflowId: { + propDefinition: [ + ironclad, + "workflowId", + ], + reloadProps: true, + }, + comment: { + type: "string", + label: "Comment", + description: "A comment that explains the updates you are making to the workflow", + optional: true, + }, + }, + async additionalProps() { + const props = {}; + if (!this.workflowId) { + return props; + } + const { schema } = await this.ironclad.getWorkflow({ + workflowId: this.workflowId, + }); + for (const [ + key, + value, + ] of Object.entries(schema)) { + if (!value?.readOnly) { + props[key] = { + type: value.type === "boolean" + ? "boolean" + : value.type === "array" + ? "string[]" + : "string", + label: value.displayName, + description: getAttributeDescription(value), + optional: true, + }; + if (key === "paperSource") { + props[key].options = [ + "Counterparty paper", + "Our paper", + ]; + } + } + } + return props; + }, + async run({ $ }) { + const { + ironclad, + workflowId, + comment, + ...attributes + } = this; + + const parsedAttributes = {}; + for (const [ + key, + value, + ] of Object.entries(attributes)) { + parsedAttributes[key] = parseValue(value); + } + + try { + const response = await ironclad.updateWorkflowMetadata({ + $, + workflowId: workflowId, + data: { + updates: Object.entries(parsedAttributes).map(([ + key, + value, + ]) => ({ + action: "set", + path: key, + value, + })), + comment: comment, + }, + }); + $.export("$summary", `Workflow ${workflowId} updated successfully`); + return response; + } catch (error) { + const msg = JSON.parse(error.message); + const { schema } = await ironclad.getWorkflow({ + workflowId, + }); + if (msg.code === "MISSING_PARAM") { + const paramNames = (JSON.parse(msg.param)).map((p) => `\`${schema[p].displayName}\``); + throw new ConfigurationError(`Please enter or update the following required parameters: ${paramNames.join(", ")}`); + } + if (msg.code === "INVALID_PARAM") { + const paramName = schema[msg.metadata.keyPath].displayName; + throw new ConfigurationError(`Invalid parameter: \`${paramName}\`. ${msg.message}`); + } + throw new ConfigurationError(msg.message); + } + }, +}; diff --git a/components/ironclad/common/utils.mjs b/components/ironclad/common/utils.mjs new file mode 100644 index 0000000000000..fcf1ae0908023 --- /dev/null +++ b/components/ironclad/common/utils.mjs @@ -0,0 +1,64 @@ +export function getAttributeDescription({ + type, displayName, elementType, +}) { + const description = `Value of ${displayName}`; + if (type === "address") { + return `${description}. Example: \`{ + "lines": [ + "325 5th Street", + "Suite 200" + ], + "locality": "San Francisco", + "region": "California", + "postcode": "94107", + "country": "USA" + }\``; + } + if (type === "monetaryAmount") { + return `${description}. Example: \`{ + "currency": "USD", + "amount": 25.37 + }\``; + } + if (type === "date") { + return `${description}. Example: \`2021-05-11T17:16:53-07:00\``; + } + if (type === "duration") { + return `${description}. Example \`{ + "years": 1, + "months": 2, + "weeks": 3, + "days": 4 + }\``; + } + if (type === "email") { + return `${description}. Example: \`test@gmail.com\``; + } + if (type === "array") { + if (elementType.type === "document") { + return `${description}. Array of type \`${elementType.type}\`. Example: \`{"url": "https://your.file.server.test/test-doc-1.docx"}\``; + } + if (elementType.type === "object") { + return `${description}. Array of type \`${elementType.type}\`. See the [docs](https://developer.ironcladapp.com/docs/launch-a-workflow#32-create-request-body-attributes) for more information about field types.`; + } + return `${description}. Array of type \`${elementType.type}\`.`; + } + return description; +} + +export function parseValue(value) { + if (!value) { + return undefined; + } + try { + if (typeof value === "string") { + return JSON.parse(value); + } + if (Array.isArray(value)) { + return value.map(JSON.parse); + } + return value; + } catch { + return value; + } +} diff --git a/components/ironclad/ironclad.app.mjs b/components/ironclad/ironclad.app.mjs new file mode 100644 index 0000000000000..5f1dd54a5d9dd --- /dev/null +++ b/components/ironclad/ironclad.app.mjs @@ -0,0 +1,197 @@ +import { axios } from "@pipedream/platform"; +import events from "./sources/common/events.mjs"; + +export default { + type: "app", + app: "ironclad", + propDefinitions: { + recordType: { + type: "string", + label: "Type", + description: "The type of the record", + async options() { + const { recordTypes } = await this.getRecordsSchema(); + return Object.entries(recordTypes).map(([ + key, + value, + ]) => ({ + value: key, + label: value.displayName, + })); + }, + }, + recordId: { + type: "string", + label: "Record ID", + description: "The identifier of a record", + optional: true, + async options({ page }) { + const { list } = await this.listRecords({ + params: { + page, + }, + }); + return list?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + properties: { + type: "string[]", + label: "Properties", + description: "Properties to add to the record", + async options() { + const { properties } = await this.getRecordsSchema(); + return Object.entries(properties).map(([ + key, + value, + ]) => ({ + value: key, + label: value.displayName, + })); + }, + }, + templateId: { + type: "string", + label: "Template ID", + description: "The identifier of a workflow template", + async options() { + const { list } = await this.listWorkflowSchemas(); + return list?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + workflowId: { + type: "string", + label: "Workflow ID", + description: "The identifier of a workflow", + async options({ page }) { + const { list } = await this.listWorkflows({ + params: { + page, + }, + }); + return list?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })) || []; + }, + }, + selectedEvents: { + type: "string[]", + label: "Selected Events", + description: "Select the Ironclad events to emit", + async options() { + return events.map((event) => ({ + label: event.replace(/_/g, " ").toUpperCase(), + value: event, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://ironcladapp.com/public/api/v1"; + }, + _makeRequest(opts = {}) { + const { + $, path, ...otherOpts + } = opts; + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + ...otherOpts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteWebhook({ + webhookId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + ...opts, + }); + }, + getRecordsSchema(opts = {}) { + return this._makeRequest({ + path: "/records/metadata", + ...opts, + }); + }, + getWorkflow({ + workflowId, ...opts + }) { + return this._makeRequest({ + path: `/workflows/${workflowId}`, + ...opts, + }); + }, + getWorkflowSchema({ + templateId, ...opts + }) { + return this._makeRequest({ + path: `/workflow-schemas/${templateId}?form=launch`, + ...opts, + }); + }, + listWorkflowSchemas(opts = {}) { + return this._makeRequest({ + path: "/workflow-schemas?form=launch", + ...opts, + }); + }, + listWorkflows(opts = {}) { + return this._makeRequest({ + path: "/workflows", + ...opts, + }); + }, + listRecords(opts = {}) { + return this._makeRequest({ + path: "/records", + ...opts, + }); + }, + launchWorkflow(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/workflows", + ...opts, + }); + }, + createRecord(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/records", + ...opts, + }); + }, + updateWorkflowMetadata({ + workflowId, ...opts + }) { + return this._makeRequest({ + method: "PATCH", + path: `/workflows/${workflowId}/attributes`, + ...opts, + }); + }, + }, +}; diff --git a/components/ironclad/package.json b/components/ironclad/package.json new file mode 100644 index 0000000000000..2f5b2a90419dd --- /dev/null +++ b/components/ironclad/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/ironclad", + "version": "0.1.0", + "description": "Pipedream Ironclad Components", + "main": "ironclad.app.mjs", + "keywords": [ + "pipedream", + "ironclad" + ], + "homepage": "https://pipedream.com/apps/ironclad", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/ironclad/sources/common/base.mjs b/components/ironclad/sources/common/base.mjs new file mode 100644 index 0000000000000..5728c5aab0f68 --- /dev/null +++ b/components/ironclad/sources/common/base.mjs @@ -0,0 +1,54 @@ +import ironclad from "../../ironclad.app.mjs"; + +export default { + props: { + ironclad, + db: "$.service.db", + http: "$.interface.http", + }, + hooks: { + async activate() { + const { id } = await this.ironclad.createWebhook({ + data: { + targetURL: this.http.endpoint, + events: this.getEvents(), + }, + }); + this._setHookId(id); + }, + async deactivate() { + const webhookId = this._getHookId(); + if (webhookId) { + await this.ironclad.deleteWebhook({ + webhookId, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + generateMeta(event) { + return { + id: event.timestamp, + summary: `New ${event.payload.event} event`, + ts: Date.parse(event.timestamp), + }; + }, + getEvents() { + throw new Error("getEvents is not implemented"); + }, + }, + async run(event) { + const { body } = event; + if (!body) { + return; + } + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, +}; diff --git a/components/ironclad/sources/common/events.mjs b/components/ironclad/sources/common/events.mjs new file mode 100644 index 0000000000000..cdf9e8e3e49d8 --- /dev/null +++ b/components/ironclad/sources/common/events.mjs @@ -0,0 +1,36 @@ +export default [ + "workflow_launched", + "workflow_updated", + "workflow_completed", + "workflow_cancelled", + "workflow_approval_status_changed", + "workflow_attribute_updated", + "workflow_comment_added", + "workflow_comment_removed", + "workflow_comment_updated", + "workflow_comment_reaction_added", + "workflow_comment_reaction_removed", + "workflow_counterparty_invite_sent", + "workflow_counterparty_invite_revoked", + "workflow_documents_added", + "workflow_documents_removed", + "workflow_documents_updated", + "workflow_documents_renamed", + "workflow_document_edited", + "workflow_changed_turn", + "workflow_paused", + "workflow_resumed", + "workflow_roles_assigned", + "workflow_signature_packet_sent", + "workflow_signature_packet_signer_first_viewed", + "workflow_signature_packet_signer_viewed", + "workflow_signature_packet_uploaded", + "workflow_signature_packet_signatures_collected", + "workflow_signature_packet_fully_signed", + "workflow_signature_packet_cancelled", + "workflow_signer_added", + "workflow_signer_removed", + "workflow_signer_reassigned", + "workflow_step_updated", + "*", +]; diff --git a/components/ironclad/sources/new-approval-event-instant/new-approval-event-instant.mjs b/components/ironclad/sources/new-approval-event-instant/new-approval-event-instant.mjs new file mode 100644 index 0000000000000..b0053bfd79597 --- /dev/null +++ b/components/ironclad/sources/new-approval-event-instant/new-approval-event-instant.mjs @@ -0,0 +1,21 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ironclad-new-approval-event-instant", + name: "New Approval Event Instant", + description: "Emit new event when a fresh approval event is generated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "workflow_approval_status_changed", + ]; + }, + }, + sampleEmit, +}; diff --git a/components/ironclad/sources/new-approval-event-instant/test-event.mjs b/components/ironclad/sources/new-approval-event-instant/test-event.mjs new file mode 100644 index 0000000000000..31679edf501fd --- /dev/null +++ b/components/ironclad/sources/new-approval-event-instant/test-event.mjs @@ -0,0 +1,14 @@ +export default { + "companyID": "674f5545728c89fc7fabd5ec", + "payload": { + "approvalID": "approver137e74661e0b40e38e3a52dff067cd0c", + "approvalName": "Legal", + "event": "workflow_approval_status_changed", + "status": "approved", + "userEmail": "", + "userID": "67520d8a6e3fe48d53795c29", + "workflowID": "67535b8446f877a720c22b29" + }, + "timestamp": "2024-12-06T20:52:19.625Z", + "webhookID": "67535d36de552a07db17c508" +} \ No newline at end of file diff --git a/components/ironclad/sources/new-workflow-document-event-instant/new-workflow-document-event-instant.mjs b/components/ironclad/sources/new-workflow-document-event-instant/new-workflow-document-event-instant.mjs new file mode 100644 index 0000000000000..b1adf0fbdc8fd --- /dev/null +++ b/components/ironclad/sources/new-workflow-document-event-instant/new-workflow-document-event-instant.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ironclad-new-workflow-document-event-instant", + name: "New Workflow Document Event (Instant)", + description: "Emit new event when a workflow document event is freshly established.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "workflow_documents_added", + "workflow_documents_removed", + "workflow_documents_updated", + "workflow_documents_renamed", + "workflow_document_edited", + ]; + }, + }, + sampleEmit, +}; diff --git a/components/ironclad/sources/new-workflow-document-event-instant/test-event.mjs b/components/ironclad/sources/new-workflow-document-event-instant/test-event.mjs new file mode 100644 index 0000000000000..f69363e5dea4c --- /dev/null +++ b/components/ironclad/sources/new-workflow-document-event-instant/test-event.mjs @@ -0,0 +1,14 @@ +export default { + "companyID": "674f5545728c89fc7fabd5ec", + "payload": { + "documentKeys": [ + "RQrFu8XdyR" + ], + "event": "workflow_documents_renamed", + "templateID": "674f55a7cba94ae9d1484c57", + "userID": "67520d8a6e3fe48d53795c29", + "workflowID": "67535b8446f877a720c22b29" + }, + "timestamp": "2024-12-06T20:49:31.127Z", + "webhookID": "67535d6e50dfae9e6b22e157" +} \ No newline at end of file diff --git a/components/ironclad/sources/new-workflow-event-instant/new-workflow-event-instant.mjs b/components/ironclad/sources/new-workflow-event-instant/new-workflow-event-instant.mjs new file mode 100644 index 0000000000000..72587a5e60088 --- /dev/null +++ b/components/ironclad/sources/new-workflow-event-instant/new-workflow-event-instant.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ironclad-new-workflow-event-instant", + name: "New Workflow Event (Instant)", + description: "Emit new event when a new workflow event is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + events: { + propDefinition: [ + common.props.ironclad, + "selectedEvents", + ], + }, + }, + methods: { + ...common.methods, + getEvents() { + return this.events; + }, + }, + sampleEmit, +}; diff --git a/components/ironclad/sources/new-workflow-event-instant/test-event.mjs b/components/ironclad/sources/new-workflow-event-instant/test-event.mjs new file mode 100644 index 0000000000000..2284d53eaf6ec --- /dev/null +++ b/components/ironclad/sources/new-workflow-event-instant/test-event.mjs @@ -0,0 +1,10 @@ +export default { + "companyID": "674f5545728c89fc7fabd5ec", + "payload": { + "event": "workflow_updated", + "templateID": "674f55a7cba94ae9d1484c57", + "workflowID": "67535b8446f877a720c22b29" + }, + "timestamp": "2024-12-06T20:42:55.776Z", + "webhookID": "67535d88411fe73b06b27dd3" +} \ No newline at end of file diff --git a/components/iscraper/README.md b/components/iscraper/README.md new file mode 100644 index 0000000000000..e8700962308c2 --- /dev/null +++ b/components/iscraper/README.md @@ -0,0 +1,11 @@ +# Overview + +The iScraper API is a powerful tool for extracting information from Instagram, making it possible to gather rich data sets from user profiles, hashtags, and posts. When leveraged through Pipedream, iScraper can become a part of diverse workflows, automating the collection of social media insights and integrating them with other services for analysis, reporting, or social media management. With Pipedream, you can create event-driven workflows that react to new data from iScraper, orchestrate data flow across multiple services, and handle complex tasks without managing infrastructure. + +# Example Use Cases + +- **Instagram Hashtag Analysis**: Automate the collection of posts using specific hashtags with iScraper, then feed this data into a Google Sheets spreadsheet. Use this workflow for trend analysis, brand monitoring, or content strategy development. + +- **Instagram Profile Monitor**: Monitor changes in follower counts, posts, or engagement rates of specific Instagram profiles by scheduling regular iScraper checks. Send alerts via email or Slack when significant changes are detected, empowering rapid response to social media dynamics. + +- **Competitor Content Strategy**: Track competitor Instagram accounts, scraping post data and analyzing content patterns. Combine iScraper with a sentiment analysis API to gauge audience reaction, then store insights in a CRM like Salesforce for strategic planning. diff --git a/components/iscraper/iscraper.app.mjs b/components/iscraper/iscraper.app.mjs new file mode 100644 index 0000000000000..ef154935ecdc2 --- /dev/null +++ b/components/iscraper/iscraper.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "iscraper", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/iscraper/package.json b/components/iscraper/package.json new file mode 100644 index 0000000000000..2513aa727d4eb --- /dev/null +++ b/components/iscraper/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/iscraper", + "version": "0.0.1", + "description": "Pipedream iScraper Components", + "main": "iscraper.app.mjs", + "keywords": [ + "pipedream", + "iscraper" + ], + "homepage": "https://pipedream.com/apps/iscraper", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/isn/README.md b/components/isn/README.md index 26759d1f34848..21c0af9362fd9 100644 --- a/components/isn/README.md +++ b/components/isn/README.md @@ -1,15 +1,11 @@ # Overview -The ISN API allows developers to access data stored in the ISN system. This -data includes information on properties, inspections, vendors, and more. Using -the ISN API, developers can build applications that allow users to view and -manipulate this data. - -Some examples of applications that can be built using the ISN API include: - -- A property management application that allows users to view and update - property information -- An inspection scheduling application that allows users to schedule and track - inspections -- A vendor management application that allows users to view and update vendor - information +The ISN API (Inspection Support Network) offers a robust interface for accessing and managing data related to property inspections - Think of it as the digital toolbox for inspection professionals. By integrating with Pipedream, you can use triggers and actions to automate tasks, sync data across other apps, and streamline your inspection workflows. From automatically scheduling inspections based on customer input to syncing inspection results with CRM platforms, the efficiency gains are substantial. + +# Example Use Cases + +- **Automated Inspection Scheduling**: Whenever a new appointment is booked through a calendar app like Google Calendar, a Pipedream workflow can trigger the creation of a corresponding inspection in ISN, ensuring seamless scheduling without double handling. + +- **Dynamic Report Generation**: After an inspection is completed, Pipedream can automatically retrieve the results from ISN and generate a report using a service like Google Docs. This document can then be shared with relevant parties, such as real estate agents or homeowners, through email services like SendGrid. + +- **Real-Time CRM Updates**: Connect ISN to a CRM platform like Salesforce using Pipedream. When an inspection status updates in ISN, the workflow could trigger an update in Salesforce, keeping sales teams informed in real-time about the inspection process for potential property buyers. diff --git a/components/ispring_learn/actions/enroll-users-in-courses/enroll-users-in-courses.mjs b/components/ispring_learn/actions/enroll-users-in-courses/enroll-users-in-courses.mjs new file mode 100644 index 0000000000000..0e7c2cd6f0ed8 --- /dev/null +++ b/components/ispring_learn/actions/enroll-users-in-courses/enroll-users-in-courses.mjs @@ -0,0 +1,98 @@ +import { parseObject } from "../../common/utils.mjs"; +import ispringLearn from "../../ispring_learn.app.mjs"; + +export default { + key: "ispring_learn-enroll-users-in-courses", + name: "Enroll Users in Courses", + description: "Enrolls users to the specified courses on iSpring Learn.", + version: "0.0.1", + type: "action", + props: { + ispringLearn, + userIds: { + propDefinition: [ + ispringLearn, + "userId", + ], + type: "string[]", + }, + courseIds: { + propDefinition: [ + ispringLearn, + "courseIds", + ], + }, + accessDate: { + type: "string", + label: "Access Date", + description: "The date and time when learners are supposed to start studying the course. If the start date and time aren't indicated, the current date and time will be auto-populated.", + optional: true, + }, + lockAfterDueDate: { + type: "boolean", + label: "Lock After Due Date", + description: "This parameter indicates whether the course will be blocked after the due date.", + optional: true, + }, + dueDateType: { + type: "string", + label: "Due Date Type", + description: "This parameter indicates whether the course has a due date or it isn't time-limited.", + options: [ + { + label: "Unlimited", + value: "unlimited", + }, + { + label: "Default", + value: "default", + }, + { + label: "Due Date", + value: "due_date", + }, + { + label: "Pue Period", + value: "due_period", + }, + ], + default: "default", + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.dueDateType === "due_date") { + props.dueDate = { + type: "string", + label: "Due Date", + description: "The limit date of the enrollment. Format: YYYY-MM-DDTHH:MM:SSZ", + }; + } + if (this.dueDateType === "due_period") { + props.duePeriod = { + type: "string", + label: "Due Period", + description: "The limit period of the enrollment. It is measured in hours.", + }; + } + return props; + }, + async run({ $ }) { + const response = await this.ispringLearn.enrollUser({ + $, + data: { + learnerIds: parseObject(this.userIds), + courseIds: parseObject(this.courseIds), + accessDate: this.accessDate, + lockAfterDueDate: this.lockAfterDueDate, + dueDateType: this.dueDateType, + dueDate: this.dueDate, + duePeriod: this.duePeriod, + }, + }); + + $.export("$summary", "Successfully enrolled users in courses"); + return response; + }, +}; diff --git a/components/ispring_learn/actions/list-enrollments/list-enrollments.mjs b/components/ispring_learn/actions/list-enrollments/list-enrollments.mjs new file mode 100644 index 0000000000000..017d79f7bf32b --- /dev/null +++ b/components/ispring_learn/actions/list-enrollments/list-enrollments.mjs @@ -0,0 +1,39 @@ +import { parseObject } from "../../common/utils.mjs"; +import ispringLearn from "../../ispring_learn.app.mjs"; + +export default { + key: "ispring_learn-list-enrollments", + name: "List Enrollments", + description: "Fetches the list of user enrollments on iSpring Learn. [See the documentation](https://ispringhelpdocs.com/ispring-learn/getting-a-list-of-enrollments-17304245.html)", + version: "0.0.1", + type: "action", + props: { + ispringLearn, + learnerIds: { + propDefinition: [ + ispringLearn, + "userId", + ], + type: "string[]", + optional: true, + }, + courseIds: { + propDefinition: [ + ispringLearn, + "courseIds", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.ispringLearn.listUserEnrollments({ + $, + params: { + learnerIds: parseObject(this.learnerIds), + courseIds: parseObject(this.courseIds), + }, + }); + $.export("$summary", `Successfully fetched user ${response.length} enrollments!`); + return response; + }, +}; diff --git a/components/ispring_learn/actions/update-user/update-user.mjs b/components/ispring_learn/actions/update-user/update-user.mjs new file mode 100644 index 0000000000000..163a773331eb9 --- /dev/null +++ b/components/ispring_learn/actions/update-user/update-user.mjs @@ -0,0 +1,83 @@ +import { parseObject } from "../../common/utils.mjs"; +import ispringLearn from "../../ispring_learn.app.mjs"; + +export default { + key: "ispring_learn-update-user", + name: "Update User", + description: "Allows to modify the properties of a specific user on iSpring Learn.", + version: "0.0.1", + type: "action", + props: { + ispringLearn, + userId: { + propDefinition: [ + ispringLearn, + "userId", + ], + }, + email: { + type: "string", + label: "Email", + description: "The email of the user.", + optional: true, + }, + login: { + type: "string", + label: "Login", + description: "The login of the user.", + optional: true, + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the user.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the user.", + optional: true, + }, + jobTitle: { + type: "string", + label: "Job Title", + description: "The job title of the user.", + optional: true, + }, + departmentId: { + propDefinition: [ + ispringLearn, + "departmentId", + ], + optional: true, + }, + groupIds: { + propDefinition: [ + ispringLearn, + "groupId", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.ispringLearn.updateUser({ + $, + userId: this.userId, + data: { + fields: { + email: this.email, + login: this.login, + first_name: this.firstName, + last_name: this.lastName, + job_title: this.jobTitle, + }, + departmentId: this.departmentId, + groupIds: parseObject(this.groupIds), + }, + }); + + $.export("$summary", `Successfully updated user with ID ${this.userId}`); + return response; + }, +}; diff --git a/components/ispring_learn/common/constants.mjs b/components/ispring_learn/common/constants.mjs new file mode 100644 index 0000000000000..ea830c15a04cb --- /dev/null +++ b/components/ispring_learn/common/constants.mjs @@ -0,0 +1 @@ +export const LIMIT = 100; diff --git a/components/ispring_learn/common/utils.mjs b/components/ispring_learn/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/ispring_learn/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/ispring_learn/ispring_learn.app.mjs b/components/ispring_learn/ispring_learn.app.mjs new file mode 100644 index 0000000000000..10d02264f42ae --- /dev/null +++ b/components/ispring_learn/ispring_learn.app.mjs @@ -0,0 +1,220 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "ispring_learn", + propDefinitions: { + courseIds: { + type: "string[]", + label: "Course ID", + description: "The ID of the course.", + async options({ prevContext }) { + const { + contentItems, nextPageToken, + } = await this.listCourses({ + params: { + pageSize: LIMIT, + pageToken: prevContext.nextPage, + }, + }); + + return { + options: contentItems.map(({ + contentItemId: value, title: label, + }) => ({ + label, + value, + })), + context: { + nextPage: nextPageToken, + }, + }; + }, + }, + userId: { + type: "string", + label: "User ID", + description: "The ID of the user.", + async options({ prevContext }) { + const { + userProfiles, nextPageToken, + } = await this.listUsers({ + params: { + pageSize: LIMIT, + pageToken: prevContext.nextPage, + }, + }); + + return { + options: userProfiles.map(({ + userId: value, fields, + }) => ({ + label: fields.filter(({ name }) => name === "EMAIL")[0].value, + value, + })), + context: { + nextPage: nextPageToken, + }, + }; + }, + }, + departmentId: { + type: "string", + label: "Department Id", + description: "The ID of the department the user belongs to.", + async options({ prevContext }) { + const { + departments, nextPageToken, + } = await this.listDepartments({ + params: { + pageSize: LIMIT, + pageToken: prevContext.nextPage, + }, + }); + + return { + options: departments.map(({ + departmentId: value, name: label, + }) => ({ + label, + value, + })), + context: { + nextPage: nextPageToken, + }, + }; + }, + }, + groupId: { + type: "string[]", + label: "Group Id", + description: "An array with the IDs of the groups the user will be added to.", + async options({ prevContext }) { + const { + groups, nextPageToken, + } = await this.listGroups({ + params: { + pageSize: LIMIT, + pageToken: prevContext.nextPage, + }, + }); + + return { + options: groups.map(({ + groupId: value, name: label, + }) => ({ + label, + value, + })), + context: { + nextPage: nextPageToken, + }, + }; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api-learn.ispringlearn.com"; + }, + _headers() { + return { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "X-Target-Locale": "en-US", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + ...opts, + url: this._baseUrl() + path, + headers: this._headers(), + }); + }, + listCourses(opts = {}) { + return this._makeRequest({ + path: "/contents", + ...opts, + }); + }, + listDepartments(opts = {}) { + return this._makeRequest({ + path: "/departments", + ...opts, + }); + }, + listUserEnrollments(opts = {}) { + return this._makeRequest({ + path: "/enrollment", + ...opts, + }); + }, + listGroups(opts = {}) { + return this._makeRequest({ + path: "/groups", + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users", + ...opts, + }); + }, + updateUser({ + userId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/user/${userId}`, + ...opts, + }); + }, + enrollUser(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/enrollment", + ...opts, + }); + }, + registerSubscriber(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhook/register", + ...opts, + }); + }, + sendConfimationCode(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhook/code/send", + ...opts, + }); + }, + confirmCode(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhook/confirm", + ...opts, + }); + }, + subscribe(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhook/subscribe", + ...opts, + }); + }, + deleteSubscriber(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhook/remove", + ...opts, + }); + }, + }, +}; diff --git a/components/ispring_learn/package.json b/components/ispring_learn/package.json new file mode 100644 index 0000000000000..3b5331a78c635 --- /dev/null +++ b/components/ispring_learn/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/ispring_learn", + "version": "0.1.0", + "description": "Pipedream iSpring Learn Components", + "main": "ispring_learn.app.mjs", + "keywords": [ + "pipedream", + "ispring_learn" + ], + "homepage": "https://pipedream.com/apps/ispring_learn", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} + diff --git a/components/ispring_learn/sources/common/base.mjs b/components/ispring_learn/sources/common/base.mjs new file mode 100644 index 0000000000000..3f5b04419f191 --- /dev/null +++ b/components/ispring_learn/sources/common/base.mjs @@ -0,0 +1,84 @@ +import ispringLearn from "../../ispring_learn.app.mjs"; + +export default { + props: { + ispringLearn, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + webhookName: { + type: "string", + label: "Webhook Name", + description: "The name of the webhook to identify on the iSpring Learn platform.", + }, + }, + methods: { + _setHookId(hookId) { + this.db.set("webhookId", hookId); + }, + _getHookId() { + return this.db.get("webhookId"); + }, + getDocs() { + return this.documentId; + }, + generateMeta(body) { + const ts = Date.now(); + return { + id: `${ts}`, + summary: this.getSummary(body), + ts: ts, + }; + }, + }, + hooks: { + async activate() { + await this.ispringLearn.registerSubscriber({ + data: { + "subscriberName": this.webhookName, + "callbackUrl": this.http.endpoint, + }, + }); + await this.ispringLearn.sendConfimationCode({ + data: { + "subscriberName": this.webhookName, + }, + }); + }, + async deactivate() { + await this.ispringLearn.deleteSubscriber({ + data: { + "subscriberName": this.webhookName, + }, + }); + }, + }, + async run({ body }) { + if (body.type === "CONFIRMATION") { + await this.ispringLearn.confirmCode({ + data: { + subscriberName: this.webhookName, + ...JSON.parse(body.payloads[0]), + }, + }); + await this.ispringLearn.subscribe({ + data: { + subscriberName: this.webhookName, + subscription: { + subscriptionType: this.getSubscriptionType(), + }, + }, + }); + return true; + } + + this.http.respond({ + status: 200, + body: "Success", + }); + + this.$emit(body, this.generateMeta(body)); + }, +}; diff --git a/components/ispring_learn/sources/new-completion-instant/new-completion-instant.mjs b/components/ispring_learn/sources/new-completion-instant/new-completion-instant.mjs new file mode 100644 index 0000000000000..e8b0de0c64d34 --- /dev/null +++ b/components/ispring_learn/sources/new-completion-instant/new-completion-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ispring_learn-new-completion-instant", + name: "New Course or Material Completion (Instant)", + description: "Emit new event when courses or materials in a course are completed successfully.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSubscriptionType() { + return "COURSE_COMPLETED_SUCCESSFULLY"; + }, + getSummary({ payloads }) { + return `${payloads.length} new course completition${payloads.length > 1 + ? "s" + : ""}`; + }, + }, + sampleEmit, +}; diff --git a/components/ispring_learn/sources/new-completion-instant/test-event.mjs b/components/ispring_learn/sources/new-completion-instant/test-event.mjs new file mode 100644 index 0000000000000..ef225b9e96d57 --- /dev/null +++ b/components/ispring_learn/sources/new-completion-instant/test-event.mjs @@ -0,0 +1,23 @@ +export default { + type: "COURSE_COMPLETED_SUCCESSFULLY", + payloads: [ + { + courseId: "", + learnerId: "string uuid>", + enrollmentIds: [ + "", + "" + ], + completionDate: "" + }, + { + courseId: "", + learnerId: "string uuid>", + enrollmentIds: [ + "", + "" + ], + completionDate: "" + } + ] +} \ No newline at end of file diff --git a/components/ispring_learn/sources/new-enrollment-instant/new-enrollment-instant.mjs b/components/ispring_learn/sources/new-enrollment-instant/new-enrollment-instant.mjs new file mode 100644 index 0000000000000..dca4d85a8d0db --- /dev/null +++ b/components/ispring_learn/sources/new-enrollment-instant/new-enrollment-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ispring_learn-new-enrollment-instant", + name: "New Enrollment (Instant)", + description: "Emit new event when learners are enrolled in courses.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSubscriptionType() { + return "LEARNERS_ENROLLED_IN_COURSE"; + }, + getSummary({ payloads }) { + return `${payloads.length} new enrollment${payloads.length > 1 + ? "s" + : ""} successfully created.`; + }, + }, + sampleEmit, +}; diff --git a/components/ispring_learn/sources/new-enrollment-instant/test-event.mjs b/components/ispring_learn/sources/new-enrollment-instant/test-event.mjs new file mode 100644 index 0000000000000..226d34ff10842 --- /dev/null +++ b/components/ispring_learn/sources/new-enrollment-instant/test-event.mjs @@ -0,0 +1,19 @@ +export default { + type: "LEARNERS_ENROLLED_IN_COURSE", + payloads: [ + { + courseId: "", + learnerIds: [ + "string uuid>", + "string uuid>" + ], + }, + { + courseId: "", + learnerIds: [ + "string uuid>", + "string uuid>" + ], + }, + ] +} \ No newline at end of file diff --git a/components/ispring_learn/sources/new-registration-instant/new-registration-instant.mjs b/components/ispring_learn/sources/new-registration-instant/new-registration-instant.mjs new file mode 100644 index 0000000000000..9ea7421c69086 --- /dev/null +++ b/components/ispring_learn/sources/new-registration-instant/new-registration-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ispring_learn-new-registration-instant", + name: "New User Registration (Instant)", + description: "Emit new event when a new user is registered.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSubscriptionType() { + return "USER_REGISTERED"; + }, + getSummary({ payloads }) { + return `${payloads.length} new user${payloads.length > 1 + ? "s" + : ""} successfully registered.`; + }, + }, + sampleEmit, +}; diff --git a/components/ispring_learn/sources/new-registration-instant/test-event.mjs b/components/ispring_learn/sources/new-registration-instant/test-event.mjs new file mode 100644 index 0000000000000..5af81291becd3 --- /dev/null +++ b/components/ispring_learn/sources/new-registration-instant/test-event.mjs @@ -0,0 +1,17 @@ +export default { + type: "USER_REGISTERED", + payloads: [ + { + userId: "", + departmentId: "", + login: ""|null, + email: ""|null + }, + { + userId: "", + departmentId: "", + login: ""|null, + email: ""|null + } + ] +} \ No newline at end of file diff --git a/components/itemize/README.md b/components/itemize/README.md index 6dfa339492059..8cc449377ba60 100644 --- a/components/itemize/README.md +++ b/components/itemize/README.md @@ -1,9 +1,11 @@ # Overview -With Itemize, you can build apps to manage your finances, keep track of your -belongings, or even monitor your home. Here are just a few examples of what you -can build: +The Itemize API enables automated processing and organization of receipts, invoices, and other financial documents. By harnessing machine learning, it extracts pertinent data such as dates, amounts, taxes, and vendor information, converting cluttered paperwork into structured data. This can be a boon for accounting, expense tracking, and financial auditing workflows. -- A budgeting app to help you track your spending and save money -- An inventory app to keep track of your belongings and their value -- A home security app to monitor your home and family's safety +# Example Use Cases + +- **Expense Reporting Automation**: Streamline expense management by connecting Itemize to an HR system like BambooHR. When receipts are uploaded to Itemize, data is extracted and sent to BambooHR to create or update expense reports, cutting down on manual data entry and speeding up reimbursement processes. + +- **Financial Data Syncing**: Keep financial records in real-time sync across platforms. Use Itemize to extract data from invoices, which can then be pushed to QuickBooks or Xero via Pipedream. This automation ensures that your accounting records always reflect the latest transactions without the need for manual updates. + +- **Compliance Auditing Workflow**: Enhance compliance and auditing by integrating Itemize with a document management system such as Box. As financial documents are processed through Itemize, extracted information can be stored in structured formats in Box, with automated alerts set up to flag anomalies or discrepancies for review by the compliance team. diff --git a/components/itemize/package.json b/components/itemize/package.json new file mode 100644 index 0000000000000..6c76f35eaa072 --- /dev/null +++ b/components/itemize/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/itemize", + "version": "0.6.0", + "description": "Pipedream itemize Components", + "main": "itemize.app.mjs", + "keywords": [ + "pipedream", + "itemize" + ], + "homepage": "https://pipedream.com/apps/itemize", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/iterate/README.md b/components/iterate/README.md index e1f3bb88150fe..08640e5b04f33 100644 --- a/components/iterate/README.md +++ b/components/iterate/README.md @@ -1,9 +1,11 @@ # Overview -TheIterate API enables you to quickly and easily build custom applications on -top of theIterate platform.Using theIterate API, you can: +The Iterate API, part of the Planview suite, is tailored for sophisticated task and project management, providing endpoints to manipulate boards, cards, and work items in LeanKit. Leveraging the Iterate API on Pipedream unlocks the potential for creating streamlined, serverless automations that can trigger actions across various platforms, react to changes in project status, and synchronize data between tools, all in real-time. -- Retrieve and update data in yourIterate account -- IntegrateIterate with other applications -- Automate workflows -- Build custom interfaces and applications +# Example Use Cases + +- **Project Status Notifications**: Send a Slack message when a card reaches a specific column on a LeanKit board, keeping your team instantly informed about project progress. This workflow can reduce project status meetings and ensure everyone is aligned. + +- **Task Synchronization with External Systems**: Create or update tasks in external systems like Jira or Asana whenever a new card is created or modified in LeanKit. This keeps your project management synchronized across platforms, providing a consolidated view of all tasks. + +- **Time Tracking Integration**: Start a timer in a time tracking tool like Toggl or Harvest when a card is moved to an 'In Progress' column on LeanKit. This workflow can automate time tracking and provide insights into the time spent on different project phases. diff --git a/components/jeffreyai/README.md b/components/jeffreyai/README.md new file mode 100644 index 0000000000000..3077e0d07b69b --- /dev/null +++ b/components/jeffreyai/README.md @@ -0,0 +1,11 @@ +# Overview + +The JeffreyAI API offers a suite of AI-driven capabilities such as natural language understanding, image recognition, and sentiment analysis. With these tools, you can automate and enhance various aspects of your applications, from analyzing customer feedback to moderating content. Integrating the JeffreyAI API with Pipedream allows you to seamlessly connect its AI features with hundreds of other apps to create powerful workflows. + +# Example Use Cases + +- **Customer Sentiment Analysis**: Connect JeffreyAI with your CRM system on Pipedream to automatically analyze customer feedback. Extract sentiment and key phrases from support tickets to prioritize responses or identify common issues. + +- **Content Moderation Pipeline**: Use JeffreyAI to moderate user-generated content in real-time. Set up a workflow that triggers whenever new content is posted to your platform, then utilize JeffreyAI's image recognition and language understanding to flag or remove inappropriate material. + +- **Marketing Insights Aggregation**: Combine JeffreyAI's natural language processing with social media APIs to gather and analyze public sentiment about your brand. Create a Pipedream workflow that collates mentions across platforms, runs them through JeffreyAI for analysis, and aggregates the results for marketing insights. diff --git a/components/jeffreyai/package.json b/components/jeffreyai/package.json index 6925457be3a94..a22d6c4341763 100644 --- a/components/jeffreyai/package.json +++ b/components/jeffreyai/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/jellyreach/README.md b/components/jellyreach/README.md index bb373656d93f3..dca27db478d7a 100644 --- a/components/jellyreach/README.md +++ b/components/jellyreach/README.md @@ -1,8 +1,11 @@ # Overview -With the Jellyreach API, you can build a number of different things, including: +Jellyreach API lets you automate your marketing campaigns by triggering personalized emails, SMS, and push notifications based on various customer actions or data. Integrating Jellyreach with Pipedream's serverless platform allows seamless connectivity between Jellyreach and other services, enabling sophisticated multi-channel communication strategies that can be tailored on-the-fly to suit an array of business needs. -- A way to search for and find specific types of content -- A way to save and share your favorite content -- A way to get content recommendations -- A way to manage your content preferences +# Example Use Cases + +- **User Onboarding Automation**: Automatically send personalized welcome emails via Jellyreach when a new user signs up on your platform. Detect new sign-ups using a webhook or by polling a user database, then use Pipedream to pass the user's information to Jellyreach's API to dispatch the email. + +- **E-commerce Cart Abandonment Recovery**: Re-engage customers who've left items in their shopping cart. By integrating Jellyreach with an e-commerce platform like Shopify, you can monitor cart abandonment, and trigger targeted emails or SMS messages nudging customers to complete their purchase. + +- **Event-Driven Notification System**: Create an event-based communication system that sends updates through Jellyreach when specific triggers occur. For example, set up a workflow that listens for Stripe payment events and sends a confirmation email or SMS when a payment is successful. diff --git a/components/jibble/README.md b/components/jibble/README.md new file mode 100644 index 0000000000000..da22d44a92da4 --- /dev/null +++ b/components/jibble/README.md @@ -0,0 +1,11 @@ +# Overview + +The Jibble API provides programmatic access to Jibble's time and attendance tracking features, allowing you to manage team timesheets, attendance, and work reports. Integrating the Jibble API with Pipedream opens up possibilities for automating routine tasks, syncing data across platforms, and triggering actions based on time tracking events. You can create workflows that respond to specific triggers like clock-ins and clock-outs, or schedule regular data syncs to maintain up-to-date records in other systems. + +# Example Use Cases + +- **Automate Attendance Reports**: Compile and send daily attendance reports by fetching data from Jibble at a scheduled time each day. The workflow could format the data and send it via email using the Gmail app, Slack for team notifications, or save it to Google Sheets for further analysis. + +- **Synchronize Project Management Tools**: Upon a clock-out event in Jibble, trigger a workflow to log the time spent on a task in a project management app like Trello or Asana. This would ensure that task tracking and time tracking are synced, offering live updates to project timelines and resource allocation. + +- **Trigger Alerts for Overtime or Unusual Activity**: Set up a workflow that monitors for overtime or irregular clock-in patterns. When detected, it triggers notifications to HR or management through SMS (using Twilio), email, or team communication platforms. This helps in maintaining compliance with work policies and addressing potential burnout issues promptly. diff --git a/components/jigsawstack/actions/object-detection/object-detection.mjs b/components/jigsawstack/actions/object-detection/object-detection.mjs new file mode 100644 index 0000000000000..dccd2d1bcdbcc --- /dev/null +++ b/components/jigsawstack/actions/object-detection/object-detection.mjs @@ -0,0 +1,73 @@ +import { ConfigurationError } from "@pipedream/platform"; +import fs from "fs"; +import mime from "mime"; +import { + checkTmp, + throwError, +} from "../../common/utils.mjs"; +import jigsawstack from "../../jigsawstack.app.mjs"; + +export default { + key: "jigsawstack-object-detection", + name: "Object Detection", + description: "Recognize objects within a provided image and retrieve it with great accuracy. [See the documentation](https://docs.jigsawstack.com/api-reference/ai/object-detection)", + version: "0.0.1", + type: "action", + props: { + jigsawstack, + url: { + type: "string", + label: "Image URL", + description: "The URL of the image to process.", + optional: true, + }, + fileStoreKey: { + type: "string", + label: "File Store Key", + description: "The key used to store the image on Jigsawstack file [Storage](https://docs.jigsawstack.com/api-reference/store/file/add).", + optional: true, + }, + imageFile: { + type: "string", + label: "Image File", + description: "The path to a file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp).", + optional: true, + }, + }, + async run({ $ }) { + const { + jigsawstack, + ...data + } = this; + + if (Object.keys(data).length > 1) { + throw new ConfigurationError("You must provide only one option, either the **Image URL**, the **Image File**, or the **File Storage Key**."); + } + + if (data.fileStoreKey) data.file_store_key = data.fileStoreKey; + + if (data.imageFile) { + const filePath = checkTmp(data.imageFile); + const file = fs.readFileSync(filePath); + const { key } = await jigsawstack.uploadFile({ + headers: { + "Content-Type": mime.getType(filePath), + }, + data: file, + }); + data.file_store_key = key; + } + + try { + const response = await jigsawstack.detectObjectsInImage({ + $, + data, + }); + $.export("$summary", "Successfully detected objects in the image"); + return response; + + } catch (e) { + return throwError(e); + } + }, +}; diff --git a/components/jigsawstack/actions/sentiment-analysis/sentiment-analysis.mjs b/components/jigsawstack/actions/sentiment-analysis/sentiment-analysis.mjs new file mode 100644 index 0000000000000..265fe66969bb9 --- /dev/null +++ b/components/jigsawstack/actions/sentiment-analysis/sentiment-analysis.mjs @@ -0,0 +1,33 @@ +import { throwError } from "../../common/utils.mjs"; +import jigsawstack from "../../jigsawstack.app.mjs"; + +export default { + key: "jigsawstack-sentiment-analysis", + name: "Sentiment Analysis", + description: "Assess sentiment of a provided text. Vibes can be positive, negative, or neutral. [See the documentation](https://docs.jigsawstack.com/api-reference/ai/sentiment)", + version: "0.0.1", + type: "action", + props: { + jigsawstack, + text: { + type: "string", + label: "Text", + description: "The text to analyze for sentiment.", + }, + }, + async run({ $ }) { + try { + const response = await this.jigsawstack.analyzeSentiment({ + $, + data: { + text: this.text, + }, + }); + + $.export("$summary", `Successfully analyzed sentiment with emotion: ${response.sentiment.emotion} and sentiment: ${response.sentiment.sentiment}`); + return response; + } catch (e) { + return throwError(e); + } + }, +}; diff --git a/components/jigsawstack/actions/verify-email/verify-email.mjs b/components/jigsawstack/actions/verify-email/verify-email.mjs new file mode 100644 index 0000000000000..4d9eb4e7bba07 --- /dev/null +++ b/components/jigsawstack/actions/verify-email/verify-email.mjs @@ -0,0 +1,33 @@ +import { throwError } from "../../common/utils.mjs"; +import jigsawstack from "../../jigsawstack.app.mjs"; + +export default { + key: "jigsawstack-verify-email", + name: "Verify Email", + description: "Validate any email address and determine deliverability as well as disposable status. [See the documentation](https://docs.jigsawstack.com/api-reference/validate/email)", + version: "0.0.1", + type: "action", + props: { + jigsawstack, + email: { + type: "string", + label: "Email Address", + description: "The email address to validate.", + }, + }, + async run({ $ }) { + try { + const response = await this.jigsawstack.validateEmail({ + $, + params: { + email: this.email, + }, + }); + + $.export("$summary", `Successfully validated email: ${this.email}`); + return response; + } catch (e) { + return throwError(e); + } + }, +}; diff --git a/components/jigsawstack/common/utils.mjs b/components/jigsawstack/common/utils.mjs new file mode 100644 index 0000000000000..c6da433bcc10e --- /dev/null +++ b/components/jigsawstack/common/utils.mjs @@ -0,0 +1,13 @@ +import { ConfigurationError } from "@pipedream/platform"; + +export const throwError = ({ message }) => { + const errorMessage = JSON.parse(message).message; + throw new ConfigurationError(errorMessage); +}; + +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; diff --git a/components/jigsawstack/jigsawstack.app.mjs b/components/jigsawstack/jigsawstack.app.mjs new file mode 100644 index 0000000000000..4a1173aaa5047 --- /dev/null +++ b/components/jigsawstack/jigsawstack.app.mjs @@ -0,0 +1,54 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "jigsawstack", + methods: { + _baseUrl() { + return "https://api.jigsawstack.com/v1"; + }, + _headers(headers = {}) { + return { + "x-api-key": this.$auth.api_key, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(headers), + ...opts, + }); + }, + validateEmail(opts = {}) { + return this._makeRequest({ + method: "GET", + path: "/validate/email", + ...opts, + }); + }, + detectObjectsInImage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/ai/object_detection", + ...opts, + }); + }, + analyzeSentiment(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/ai/sentiment", + ...opts, + }); + }, + uploadFile(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/store/file", + ...opts, + }); + }, + }, +}; diff --git a/components/jigsawstack/package.json b/components/jigsawstack/package.json new file mode 100644 index 0000000000000..f24bbbce6698c --- /dev/null +++ b/components/jigsawstack/package.json @@ -0,0 +1,20 @@ +{ + "name": "@pipedream/jigsawstack", + "version": "0.1.0", + "description": "Pipedream JigsawStack Components", + "main": "jigsawstack.app.mjs", + "keywords": [ + "pipedream", + "jigsawstack" + ], + "homepage": "https://pipedream.com/apps/jigsawstack", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1", + "form-data": "^4.0.0", + "mime": "^4.0.4" + } +} diff --git a/components/jina_reader/actions/convert-to-llm-friendly-input/convert-to-llm-friendly-input.mjs b/components/jina_reader/actions/convert-to-llm-friendly-input/convert-to-llm-friendly-input.mjs new file mode 100644 index 0000000000000..36077cc48a437 --- /dev/null +++ b/components/jina_reader/actions/convert-to-llm-friendly-input/convert-to-llm-friendly-input.mjs @@ -0,0 +1,186 @@ +import fs from "fs"; +import path from "path"; +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../jina_reader.app.mjs"; + +export default { + key: "jina_reader-convert-to-llm-friendly-input", + name: "Convert URL To LLM-Friendly Input", + description: "Converts a provided URL to an LLM-friendly input using Jina Reader. [See the documentation](https://github.com/jina-ai/reader)", + version: "0.0.1", + type: "action", + props: { + app, + url: { + type: "string", + label: "URL", + description: "The URL to convert to an LLM-friendly input.", + optional: true, + }, + contentFormat: { + type: "string", + label: "Content Format", + description: "You can control the level of detail in the response to prevent over-filtering. The default pipeline is optimized for most websites and LLM input.", + optional: true, + options: [ + "markdown", + "html", + "text", + "screenshot", + "pageshot", + ], + }, + timeout: { + type: "integer", + label: "Timeout", + description: "Maximum time to wait for the webpage to load. Note that this is NOT the total time for the whole end-to-end request.", + optional: true, + }, + targetSelector: { + type: "string", + label: "Target Selector", + description: "Provide a list of CSS selector to focus on more specific parts of the page. Useful when your desired content doesn't show under the default settings. E.g., `body, .class, #id`.", + optional: true, + }, + waitForSelector: { + type: "string", + label: "Wait For Selector", + description: "Provide a list of CSS selector to wait for specific elements to appear before returning. Useful when your desired content doesn't show under the default settings. E.g., `body, .class, #id`.", + optional: true, + }, + excludedSelector: { + type: "string", + label: "Excluded Selector", + description: "Provide a list of CSS selector to remove the specified elements of the page. Useful when you want to exclude specific parts of the page like headers, footers, etc. E.g., `header, .class, #id`.", + optional: true, + }, + jsonResponse: { + type: "boolean", + label: "JSON Response", + description: "The response will be in JSON format, containing the URL, title, content, and timestamp (if available). In Search mode, it returns a list of five entries, each following the described JSON structure. Keep in mind **JSON Response** will take piority over **Stream mode** if both are enabled.", + optional: true, + }, + forwardCookie: { + type: "string", + label: "Forward Cookie", + description: "The API server can forward your custom cookie settings when accessing the URL, which is useful for pages requiring extra authentication. Note that requests with cookies will not be cached. E.g., `=, =; domain=`. [Learn more here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie).", + optional: true, + }, + useProxyServer: { + type: "string", + label: "Proxy Server URL", + description: "The API server can utilize your proxy to access URLs, which is helpful for pages accessible only through specific proxies. E.g., `http://your_proxy_server.com`. [Learn more here](https://en.wikipedia.org/wiki/Proxy_server).", + optional: true, + }, + bypassCache: { + type: "boolean", + label: "Bypass Cache", + description: "The API server caches both Read and Search mode contents for a certain amount of time. To bypass this cache, set this header to `true`.", + optional: true, + }, + streamMode: { + type: "boolean", + label: "Stream Mode", + description: "Stream mode is beneficial for large target pages, allowing more time for the page to fully render. If standard mode results in incomplete content, consider using **Stream mode**. [Learn more here](https://github.com/jina-ai/reader?tab=readme-ov-file#streaming-mode). Keep in mind **JSON Response** will take piority over **Stream mode** if both are enabled.", + optional: true, + }, + browserLocale: { + type: "string", + label: "Browser Locale", + description: "Control the browser locale to render the page. eg. `en-US`. [Learn more here](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language).", + optional: true, + }, + iframeContent: { + type: "boolean", + label: "Iframe", + description: "Returning result will also include the content of the iframes on the page.", + optional: true, + }, + shadowDomContent: { + type: "boolean", + label: "Include Shadow DOM Content", + description: "Returning result will also include the content of the shadow DOM on the page.", + optional: true, + }, + pdf: { + type: "string", + label: "PDF File Path", + description: "The path to the pdf file saved to the `/tmp` directory (e.g. `/tmp/example.pdf`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + optional: true, + }, + html: { + type: "string", + label: "HTML File Path", + description: "The path to the html file saved to the `/tmp` directory (e.g. `/tmp/example.html`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + optional: true, + }, + }, + methods: { + async readFileFromTmp(filePath, encoding) { + if (!filePath) { + return; + } + const resolvedPath = path.resolve(filePath); + if (!resolvedPath.startsWith("/tmp/")) { + throw new ConfigurationError(`${filePath} must be located in the '/tmp/' directory`); + } + return await fs.promises.readFile(resolvedPath, encoding); + }, + }, + async run({ $ }) { + const { + app, + readFileFromTmp, + url, + contentFormat, + timeout, + targetSelector, + waitForSelector, + excludedSelector, + jsonResponse, + forwardCookie, + useProxyServer, + bypassCache, + streamMode, + browserLocale, + iframeContent, + shadowDomContent, + pdf, + html, + } = this; + + if (!url && !pdf && !html) { + throw new ConfigurationError("You must provide at least one of **URL**, **PDF File Path**, or **HTML File Path**."); + } + + const response = await app.post({ + $, + headers: { + "X-Return-Format": contentFormat, + "X-Timeout": timeout, + "X-Target-Selector": targetSelector, + "X-Wait-For-Selector": waitForSelector, + "X-Remove-Selector": excludedSelector, + "X-Set-Cookie": forwardCookie, + "X-Proxy-Url": useProxyServer, + "X-No-Cache": bypassCache, + "Accept": jsonResponse + ? "application/json" + : streamMode + ? "text/event-stream" + : undefined, + "X-Locale": browserLocale, + "X-With-Shadow-Dom": shadowDomContent, + "X-Iframe": iframeContent, + }, + data: { + url, + pdf: await readFileFromTmp(pdf, "base64"), + html: await readFileFromTmp(html, "utf-8"), + }, + }); + + $.export("$summary", "Converted URL to LLM-friendly input successfully."); + return response; + }, +}; diff --git a/components/jina_reader/jina_reader.app.mjs b/components/jina_reader/jina_reader.app.mjs new file mode 100644 index 0000000000000..88326348a1639 --- /dev/null +++ b/components/jina_reader/jina_reader.app.mjs @@ -0,0 +1,33 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "jina_reader", + methods: { + getUrl() { + return "https://r.jina.ai/"; + }, + getHeaders(headers) { + return { + ...headers, + "Content-Type": "application/json", + "Authorization": `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + }, +}; diff --git a/components/jina_reader/package.json b/components/jina_reader/package.json new file mode 100644 index 0000000000000..58d8a26a39e73 --- /dev/null +++ b/components/jina_reader/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/jina_reader", + "version": "0.1.0", + "description": "Pipedream Jina Reader Components", + "main": "jina_reader.app.mjs", + "keywords": [ + "pipedream", + "jina_reader" + ], + "homepage": "https://pipedream.com/apps/jina_reader", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" + } +} diff --git a/components/jira/README.md b/components/jira/README.md index 60563b234105e..030a505d41a98 100644 --- a/components/jira/README.md +++ b/components/jira/README.md @@ -1,12 +1,11 @@ # Overview -With the Jira API, you can create custom integrations to automatically track -and manage issues in Jira. For example, you could create a custom integration -that: - -- Automatically creates Jira issues when new items are added to a specified RSS - feed -- Tracks all changes made to a Jira issue in a database -- Sends a notification to a specified Slack channel whenever a Jira issue is - updated -- Creates a new Jira issue when a customer submits a support ticket +The Jira API opens up a world of possibilities for automating project management tasks, syncing with other tools, and enhancing data visibility. With Pipedream's integration, you can streamline issue tracking by automatically creating, updating, and searching for issues in Jira, as well as managing projects, sprints, users, and more. This integration not only saves time but also ensures real-time data flow across various platforms, keeping teams in sync and projects on track. + +# Example Use Cases + +- **Automated Bug Reporting Workflow**: When a new bug is reported through a form on your website or an email, Pipedream can catch the submission and automatically create a new issue in Jira. This ensures that bugs are tracked immediately and can be acted upon quickly by your development team. + +- **Sync Customer Feedback to Product Backlog**: Collect customer feedback from various sources like support tickets, social media, or NPS surveys. Use Pipedream to collate this data and create user stories or feature requests directly in Jira. This allows you to prioritize product development efforts based on real user input systematically. + +- **Cross-Platform Project Updates**: Connect Jira with other project management tools like Trello, Asana, or Monday.com. When a task is marked as completed in one of these platforms, Pipedream can trigger an update in Jira, ensuring project statuses are consistent across all platforms used by your team. diff --git a/components/jira/actions/add-attachment-to-issue/add-attachment-to-issue.mjs b/components/jira/actions/add-attachment-to-issue/add-attachment-to-issue.mjs index b53c9e1b9ca57..473bfbdd01053 100644 --- a/components/jira/actions/add-attachment-to-issue/add-attachment-to-issue.mjs +++ b/components/jira/actions/add-attachment-to-issue/add-attachment-to-issue.mjs @@ -8,7 +8,7 @@ export default { key: "jira-add-attachment-to-issue", name: "Add Attachment To Issue", description: "Adds an attachment to an issue, [See the docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-attachments/#api-rest-api-3-issue-issueidorkey-attachments-post)", - version: "0.2.10", + version: "0.2.11", type: "action", props: { jira, diff --git a/components/jira/actions/add-comment-to-issue/add-comment-to-issue.mjs b/components/jira/actions/add-comment-to-issue/add-comment-to-issue.mjs index 8b329e3fadec7..4077feaa542d6 100644 --- a/components/jira/actions/add-comment-to-issue/add-comment-to-issue.mjs +++ b/components/jira/actions/add-comment-to-issue/add-comment-to-issue.mjs @@ -5,7 +5,7 @@ export default { key: "jira-add-comment-to-issue", name: "Add Comment To Issue", description: "Adds a new comment to an issue, [See the docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-post)", - version: "0.1.8", + version: "0.1.9", type: "action", props: { jira, diff --git a/components/jira/actions/add-multiple-attachments-to-issue/add-multiple-attachments-to-issue.mjs b/components/jira/actions/add-multiple-attachments-to-issue/add-multiple-attachments-to-issue.mjs index 84fe52d74ea64..940407a16f5df 100644 --- a/components/jira/actions/add-multiple-attachments-to-issue/add-multiple-attachments-to-issue.mjs +++ b/components/jira/actions/add-multiple-attachments-to-issue/add-multiple-attachments-to-issue.mjs @@ -8,7 +8,7 @@ export default { key: "jira-add-multiple-attachments-to-issue", name: "Add Multiple Attachments To Issue", description: "Adds multiple attachments to an issue, [See the docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-attachments/#api-rest-api-3-issue-issueidorkey-attachments-post)", - version: "0.0.8", + version: "0.0.9", type: "action", props: { jira, diff --git a/components/jira/actions/add-watcher-to-issue/add-watcher-to-issue.mjs b/components/jira/actions/add-watcher-to-issue/add-watcher-to-issue.mjs index 0bffe8eeac9e4..c16026baeef1c 100644 --- a/components/jira/actions/add-watcher-to-issue/add-watcher-to-issue.mjs +++ b/components/jira/actions/add-watcher-to-issue/add-watcher-to-issue.mjs @@ -3,7 +3,7 @@ import jira from "../../jira.app.mjs"; export default { key: "jira-add-watcher-to-issue", name: "Add Watcher To Issue", - version: "0.0.7", + version: "0.0.8", description: "Adds a user as a watcher of an issue by passing the account ID of the user, For example, `5b10ac8d82e05b22cc7d4ef5`, If no user is specified the calling user is added. [See the docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-watchers/#api-rest-api-3-issue-issueidorkey-watchers-post)", type: "action", props: { diff --git a/components/jira/actions/assign-issue/assign-issue.mjs b/components/jira/actions/assign-issue/assign-issue.mjs index 65a5964e6b6d3..4050b2b4a1f2f 100644 --- a/components/jira/actions/assign-issue/assign-issue.mjs +++ b/components/jira/actions/assign-issue/assign-issue.mjs @@ -3,7 +3,7 @@ import jira from "../../jira.app.mjs"; export default { key: "jira-assign-issue", name: "Assign Issue", - version: "0.0.7", + version: "0.0.8", description: "Assigns an issue to a user. [See the docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-assignee-put)", type: "action", props: { diff --git a/components/jira/actions/common/issue.mjs b/components/jira/actions/common/issue.mjs index be1cc39f0085e..98bcb223f684e 100644 --- a/components/jira/actions/common/issue.mjs +++ b/components/jira/actions/common/issue.mjs @@ -43,56 +43,76 @@ export default { ...args, }); }, - getResourcesInfo(key) { - const { - app, - cloudId, - } = this; + getOptions(key) { switch (key) { case constants.FIELD_KEY.PARENT: - return { - fn: app.getIssues, - args: { + return async ({ prevContext: { startAt = 0 } }) => { + const { + app, + cloudId, + } = this; + const maxResults = 50; + const { issues } = await app.getIssues({ cloudId, params: { - maxResults: 100, + maxResults, + startAt, }, - }, - map: ({ issues }) => - issues?.map(({ + }); + return { + options: issues.map(({ id: value, key: label, }) => ({ value, label, })), + context: { + startAt: startAt + maxResults, + }, + }; }; case constants.FIELD_KEY.LABELS: - return { - fn: app.getLabels, - args: { + return async ({ prevContext: { startAt = 0 } }) => { + const { + app, + cloudId, + } = this; + const maxResults = 50; + const { values } = await app.getLabels({ cloudId, params: { - maxResults: 100, + maxResults, + startAt, }, - }, - map: ({ values }) => values, + }); + return { + options: values, + context: { + startAt: startAt + maxResults, + }, + }; }; case constants.FIELD_KEY.ISSUETYPE: - return { - fn: this.getIssueTypes, - args: { + return async () => { + const { + getIssueTypes, cloudId, - }, - map: (issueTypes) => - issueTypes?.map(({ + } = this; + + const issueTypes = await getIssueTypes({ + cloudId, + }); + return { + options: issueTypes.map(({ id: value, name: label, }) => ({ value, label, })), + }; }; default: - return {}; + return []; } }, async getDynamicFields({ @@ -153,19 +173,11 @@ export default { // Requests by Resource if (keysForResourceRequest.includes(key)) { - const { - fn: resourcesFn, - args: resourcesFnArgs, - map: resourcesFnMap, - } = this.getResourcesInfo(key); - - const response = await resourcesFn(resourcesFnArgs); - return Promise.resolve({ ...reduction, [newKey]: { ...value, - options: resourcesFnMap(response), + options: this.getOptions(key), }, }); } diff --git a/components/jira/actions/create-custom-field-options-context/create-custom-field-options-context.mjs b/components/jira/actions/create-custom-field-options-context/create-custom-field-options-context.mjs new file mode 100644 index 0000000000000..2d5a9561430c0 --- /dev/null +++ b/components/jira/actions/create-custom-field-options-context/create-custom-field-options-context.mjs @@ -0,0 +1,83 @@ +import app from "../../jira.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "jira-create-custom-field-options-context", + name: "Create Custom Field Options (Context)", + description: "Create a context for custom field options. [See the documentation here](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-custom-field-options/#api-rest-api-3-field-fieldid-context-contextid-option-post).", + version: "0.0.1", + type: "action", + props: { + app, + cloudId: { + propDefinition: [ + app, + "cloudId", + ], + }, + fieldId: { + propDefinition: [ + app, + "fieldId", + ({ cloudId }) => ({ + cloudId, + params: { + type: [ + "custom", + ], + }, + }), + ], + }, + contextId: { + propDefinition: [ + app, + "contextId", + ({ + cloudId, fieldId, + }) => ({ + cloudId, + fieldId, + }), + ], + }, + options: { + type: "string[]", + label: "Options", + description: "Options to create. Each option should be a JSON object with the following structure as an example: `{ \"value\": \"Manhattan\", \"optionId\": \"1000\", \"disabled\": true }` where the only required field is `value`.", + }, + }, + methods: { + createCustomFieldOptionsContext({ + fieldId, contextId, ...args + }) { + return this.app._makeRequest({ + method: "POST", + path: `/field/${fieldId}/context/${contextId}/option`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + createCustomFieldOptionsContext, + cloudId, + fieldId, + contextId, + options, + } = this; + + const response = await createCustomFieldOptionsContext({ + $, + cloudId, + fieldId, + contextId, + data: { + options: utils.parseArray(options).map(utils.parseOne), + }, + }); + + $.export("$summary", `Successfully created custom field options for field \`${fieldId}\` and context \`${contextId}\``); + return response; + }, +}; diff --git a/components/jira/actions/create-issue/create-issue.mjs b/components/jira/actions/create-issue/create-issue.mjs index 77e55be454157..6d278a64e9073 100644 --- a/components/jira/actions/create-issue/create-issue.mjs +++ b/components/jira/actions/create-issue/create-issue.mjs @@ -7,7 +7,7 @@ export default { key: "jira-create-issue", name: "Create Issue", description: "Creates an issue or, where the option to create subtasks is enabled in Jira, a subtask, [See the docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-issue-post)", - version: "0.1.16", + version: "0.1.19", type: "action", props: { ...common.props, @@ -112,11 +112,13 @@ export default { cloudId, params, data: { - fields, + fields: { + ...utils.parseObject(additionalProperties), + ...fields, + }, historyMetadata: utils.parseObject(historyMetadata), properties: utils.parse(properties), update: utils.parseObject(update), - ...utils.parseObject(additionalProperties), }, }); diff --git a/components/jira/actions/create-version/create-version.mjs b/components/jira/actions/create-version/create-version.mjs index 90e5dcc135a90..603ddbd2519d4 100644 --- a/components/jira/actions/create-version/create-version.mjs +++ b/components/jira/actions/create-version/create-version.mjs @@ -4,7 +4,7 @@ export default { key: "jira-create-version", name: "Create Jira Version in project", description: "Creates a project version., [See the docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-project-versions/#api-rest-api-3-version-post)", - version: "0.1.8", + version: "0.1.9", type: "action", props: { jira, diff --git a/components/jira/actions/delete-project/delete-project.mjs b/components/jira/actions/delete-project/delete-project.mjs index 87cc46808bbd0..ee9b52b94e2d4 100644 --- a/components/jira/actions/delete-project/delete-project.mjs +++ b/components/jira/actions/delete-project/delete-project.mjs @@ -4,7 +4,7 @@ export default { key: "jira-delete-project", name: "Delete Project", description: "Deletes a project, [See the docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-projects/#api-rest-api-3-project-projectidorkey-delete)", - version: "0.1.8", + version: "0.1.9", type: "action", props: { jira, diff --git a/components/jira/actions/get-all-projects/get-all-projects.mjs b/components/jira/actions/get-all-projects/get-all-projects.mjs index 966bfd1d1c5a1..740ae0bcb85b1 100644 --- a/components/jira/actions/get-all-projects/get-all-projects.mjs +++ b/components/jira/actions/get-all-projects/get-all-projects.mjs @@ -4,7 +4,7 @@ export default { key: "jira-get-all-projects", name: "Get All Projects", description: "Gets metadata on all projects, [See the docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-project-get)", - version: "0.1.9", + version: "0.1.10", type: "action", props: { jira, diff --git a/components/jira/actions/get-issue/get-issue.mjs b/components/jira/actions/get-issue/get-issue.mjs index 7ff8e32388ee1..ec25689806e02 100644 --- a/components/jira/actions/get-issue/get-issue.mjs +++ b/components/jira/actions/get-issue/get-issue.mjs @@ -4,7 +4,7 @@ export default { key: "jira-get-issue", name: "Get Issue", description: "Gets the details for an issue. [See the docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-get)", - version: "0.1.10", + version: "0.1.11", type: "action", props: { jira, diff --git a/components/jira/actions/get-task/get-task.mjs b/components/jira/actions/get-task/get-task.mjs index 831a1836c4717..46f0dd702527d 100644 --- a/components/jira/actions/get-task/get-task.mjs +++ b/components/jira/actions/get-task/get-task.mjs @@ -4,7 +4,7 @@ export default { key: "jira-get-task", name: "Get Task", description: "Gets the status of a long-running asynchronous task, [See the docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-tasks/#api-rest-api-3-task-taskid-get)", - version: "0.1.8", + version: "0.1.9", type: "action", props: { jira, diff --git a/components/jira/actions/get-transitions/get-transitions.mjs b/components/jira/actions/get-transitions/get-transitions.mjs index fbd06aa6ab6a7..2105bda67dd03 100644 --- a/components/jira/actions/get-transitions/get-transitions.mjs +++ b/components/jira/actions/get-transitions/get-transitions.mjs @@ -4,7 +4,7 @@ export default { key: "jira-get-transitions", name: "Get Transitions", description: "Gets either all transitions or a transition that can be performed by the user on an issue, based on the issue's status, [See the docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-transitions-get)", - version: "0.1.8", + version: "0.1.9", type: "action", props: { jira, diff --git a/components/jira/actions/get-user/get-user.mjs b/components/jira/actions/get-user/get-user.mjs index f6debc37b3699..d6dbe3afa9d72 100644 --- a/components/jira/actions/get-user/get-user.mjs +++ b/components/jira/actions/get-user/get-user.mjs @@ -4,7 +4,7 @@ export default { key: "jira-get-user", name: "Get User", description: "Gets details of user, [See the docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-users/#api-rest-api-3-user-get)", - version: "0.1.8", + version: "0.1.9", type: "action", props: { jira, diff --git a/components/jira/actions/get-users/get-users.mjs b/components/jira/actions/get-users/get-users.mjs index 3a826f638d923..e22bf77314658 100644 --- a/components/jira/actions/get-users/get-users.mjs +++ b/components/jira/actions/get-users/get-users.mjs @@ -4,7 +4,7 @@ export default { key: "jira-get-users", name: "Get Users", description: "Gets details of a list of users. [See docs here](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-users/#api-rest-api-3-users-search-get)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { jira, diff --git a/components/jira/actions/list-issue-comments/list-issue-comments.mjs b/components/jira/actions/list-issue-comments/list-issue-comments.mjs index 919e05440a99f..d43366bd5c3e3 100644 --- a/components/jira/actions/list-issue-comments/list-issue-comments.mjs +++ b/components/jira/actions/list-issue-comments/list-issue-comments.mjs @@ -4,7 +4,7 @@ export default { key: "jira-list-issue-comments", name: "List Issue Comments", description: "Lists all comments for an issue, [See the docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-get)", - version: "0.1.8", + version: "0.1.9", type: "action", props: { jira, diff --git a/components/jira/actions/transition-issue/transition-issue.mjs b/components/jira/actions/transition-issue/transition-issue.mjs index 255bce88f3904..28e7f498f7faa 100644 --- a/components/jira/actions/transition-issue/transition-issue.mjs +++ b/components/jira/actions/transition-issue/transition-issue.mjs @@ -5,7 +5,7 @@ export default { key: "jira-transition-issue", name: "Transition Issue", description: "Performs an issue transition and, if the transition has a screen, updates the fields from the transition screen, [See the docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-transitions-post)", - version: "0.1.10", + version: "0.1.11", type: "action", props: { jira, diff --git a/components/jira/actions/update-comment/update-comment.mjs b/components/jira/actions/update-comment/update-comment.mjs index ec782e8e28601..fb4b149a1790c 100644 --- a/components/jira/actions/update-comment/update-comment.mjs +++ b/components/jira/actions/update-comment/update-comment.mjs @@ -5,7 +5,7 @@ export default { key: "jira-update-comment", name: "Update Comment", description: "Updates a comment, [See the docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-id-put)", - version: "0.1.8", + version: "0.1.9", type: "action", props: { jira, diff --git a/components/jira/actions/update-issue/update-issue.mjs b/components/jira/actions/update-issue/update-issue.mjs index 48f0f611fd266..18a7492736145 100644 --- a/components/jira/actions/update-issue/update-issue.mjs +++ b/components/jira/actions/update-issue/update-issue.mjs @@ -8,29 +8,29 @@ export default { key: "jira-update-issue", name: "Update Issue", description: "Updates an issue. A transition may be applied and issue properties updated as part of the edit, [See the docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-put)", - version: "0.2.10", + version: "0.2.12", type: "action", props: { ...common.props, - issueIdOrKey: { - reloadProps: true, + projectId: { propDefinition: [ common.props.app, - "issueIdOrKey", + "projectID", ({ cloudId }) => ({ cloudId, }), ], + optional: true, }, - projectId: { + issueIdOrKey: { + reloadProps: true, propDefinition: [ common.props.app, - "projectID", + "issueIdOrKey", ({ cloudId }) => ({ cloudId, }), ], - optional: true, }, issueTypeId: { reloadProps: true, diff --git a/components/jira/common/constants.mjs b/components/jira/common/constants.mjs index c227606cccb0e..e9d12c30fa197 100644 --- a/components/jira/common/constants.mjs +++ b/components/jira/common/constants.mjs @@ -34,9 +34,12 @@ const SCHEMA = { }, }; +const DEFAULT_LIMIT = 50; + export default { TYPE, FIELD_KEY, FIELD_TYPE, SCHEMA, + DEFAULT_LIMIT, }; diff --git a/components/jira/common/utils.mjs b/components/jira/common/utils.mjs index 9752e1b44b080..6e52c8570349c 100644 --- a/components/jira/common/utils.mjs +++ b/components/jira/common/utils.mjs @@ -1,3 +1,5 @@ +import { ConfigurationError } from "@pipedream/platform"; + function addProperty({ src, predicate, addition, }) { @@ -20,6 +22,28 @@ export default { console.log(`Error when trying to parse: ${str}`, err); } }, + parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } + }, parseOne(obj) { let parsed; try { diff --git a/components/jira/jira.app.mjs b/components/jira/jira.app.mjs index c31a35d25a157..6e5205770d50e 100644 --- a/components/jira/jira.app.mjs +++ b/components/jira/jira.app.mjs @@ -1,6 +1,7 @@ import { ConfigurationError, axios, } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; export default { type: "app", @@ -213,8 +214,102 @@ export default { description: "List of issue screen fields to update, specifying the sub-field to update and its value for each field. This field provides a straightforward option when setting a sub-field. When multiple sub-fields or other operations are required, use `update`. Fields included in here cannot be included in `update`. (.i.e for Fields \"fields\": {\"summary\":\"Completed orders still displaying in pending\",\"customfield_10010\":1,}) [see doc](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-put)", optional: true, }, - }, + fieldId: { + type: "string", + label: "Field ID", + description: "The ID of the field.", + useQuery: true, + async options({ + query, + prevContext: { + hasMore, + startAt = 0, + }, + cloudId, + params = { + type: [ + "custom", + "system", + ], + }, + }) { + if (hasMore === false) { + return []; + } + + const { + isLast, + values, + } = await this.getFieldsPaginated({ + cloudId, + params: { + ...params, + query, + maxResults: constants.DEFAULT_LIMIT, + startAt, + }, + }); + + return { + options: values.map(({ + id: value, name: label, + }) => ({ + label, + value, + })), + context: { + hasMore: !isLast, + startAt: startAt + constants.DEFAULT_LIMIT, + }, + }; + }, + }, + contextId: { + type: "string", + label: "Context ID", + description: "The ID of the context.", + async options({ + prevContext: { + hasMore, + startAt = 0, + }, + cloudId, + fieldId, + params = { + isAnyIssueType: true, + }, + }) { + if (hasMore === false) { + return []; + } + const { + isLast, + values, + } = await this.getCustomFieldContexts({ + cloudId, + fieldId, + params: { + ...params, + startAt, + maxResults: constants.DEFAULT_LIMIT, + }, + }); + return { + options: values.map(({ + id: value, name: label, + }) => ({ + label, + value, + })), + context: { + hasMore: !isLast, + startAt: startAt + constants.DEFAULT_LIMIT, + }, + }; + }, + }, + }, methods: { _getHeaders(headers = {}) { return { @@ -474,6 +569,20 @@ export default { ...args, }); }, + getFieldsPaginated(args = {}) { + return this._makeRequest({ + path: "/field/search", + ...args, + }); + }, + getCustomFieldContexts({ + fieldId, ...args + } = {}) { + return this._makeRequest({ + path: `/field/${fieldId}/context`, + ...args, + }); + }, async *getResourcesStream({ cloudId, resourceFn, diff --git a/components/jira/package.json b/components/jira/package.json index 4503615813d9a..a40a0d335e23c 100644 --- a/components/jira/package.json +++ b/components/jira/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/jira", - "version": "0.0.16", + "version": "0.1.3", "description": "Pipedream Jira Components", "main": "jira.app.mjs", "keywords": [ @@ -10,7 +10,7 @@ "homepage": "https://pipedream.com/apps/jira", "author": "Pipedream (https://pipedream.com/)", "dependencies": { - "@pipedream/platform": "^0.10.0", + "@pipedream/platform": "^3.0.1", "form-data": "^4.0.0" }, "publishConfig": { diff --git a/components/jira/sources/common/common.mjs b/components/jira/sources/common/common.mjs index 0532476f7f768..d25fc992239e4 100644 --- a/components/jira/sources/common/common.mjs +++ b/components/jira/sources/common/common.mjs @@ -21,6 +21,13 @@ export default { ], description: "The JQL filter that specifies which issues the webhook is sent for, only a subset of JQL can be used, e.g. `project = P1` [See supported JQL filters](https://developer.atlassian.com/cloud/jira/service-desk/webhooks/#supported-jql-queries)", }, + overrideExistingWebhooks: { + type: "boolean", + label: "Override Existing Webhooks", + description: "Override existing webhooks with this new Pipedream source's webhook. Recommend to set this to `true` if you have an existing Jira webhook that you no longer use and want to override with the new Pipedream source.", + default: false, + optional: true, + }, }, methods: { _getHookID() { @@ -91,9 +98,28 @@ export default { ts, }; }, + async deleteExistingWebhooks() { + const resourcesStream = await this.jira.getResourcesStream({ + cloudId: this.cloudId, + resourceFn: this.jira.getWebhook, + resourceFnArgs: { + params: {}, + }, + resourceFiltererFn: (resource) => resource.values, + }); + for await (const webhook of resourcesStream) { + await this.jira.deleteHook({ + hookId: webhook.id, + cloudId: this.cloudId, + }); + } + }, }, hooks: { async activate() { + if (this.overrideExistingWebhooks) { + await this.deleteExistingWebhooks(); + } const { hookId } = await this.jira.createHook({ url: this.http.endpoint, events: this.getEvents(), diff --git a/components/jira/sources/events/events.mjs b/components/jira/sources/events/events.mjs index ed5807c1a7cc7..1329faa50e701 100644 --- a/components/jira/sources/events/events.mjs +++ b/components/jira/sources/events/events.mjs @@ -5,7 +5,7 @@ export default { key: "jira-events", name: "New Event", description: "Emit new event when an event with subscribed event source triggered, [See the docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-webhooks/#api-rest-api-3-webhook-post)", - version: "0.0.8", + version: "0.0.10", type: "source", dedupe: "unique", ...common, diff --git a/components/jira/sources/issue-created/issue-created.mjs b/components/jira/sources/issue-created/issue-created.mjs index 09c86c2784a0a..1699d666fa308 100644 --- a/components/jira/sources/issue-created/issue-created.mjs +++ b/components/jira/sources/issue-created/issue-created.mjs @@ -2,9 +2,9 @@ import common from "../common/common.mjs"; export default { key: "jira-issue-created", - name: "New Issue Created Event", + name: "New Issue Created Event (Instant)", description: "Emit new event when an issue is created. Note that Jira supports only one webhook, if more sources are needed please use `New Event` source and select multiple events.", - version: "0.0.8", + version: "0.0.10", type: "source", dedupe: "unique", ...common, diff --git a/components/jira/sources/issue-deleted/issue-deleted.mjs b/components/jira/sources/issue-deleted/issue-deleted.mjs index 9dc774691ad5d..90e34de11b911 100644 --- a/components/jira/sources/issue-deleted/issue-deleted.mjs +++ b/components/jira/sources/issue-deleted/issue-deleted.mjs @@ -2,9 +2,9 @@ import common from "../common/common.mjs"; export default { key: "jira-issue-deleted", - name: "New Issue Deleted Event", + name: "New Issue Deleted Event (Instant)", description: "Emit new event when an issue is deleted. Note that Jira supports only one webhook, if more sources are needed please use `New Event` source and select multiple events.", - version: "0.0.8", + version: "0.0.10", type: "source", dedupe: "unique", ...common, diff --git a/components/jira/sources/issue-updated/issue-updated.mjs b/components/jira/sources/issue-updated/issue-updated.mjs index 10857d60f4359..71e7e82a74069 100644 --- a/components/jira/sources/issue-updated/issue-updated.mjs +++ b/components/jira/sources/issue-updated/issue-updated.mjs @@ -2,9 +2,9 @@ import common from "../common/common.mjs"; export default { key: "jira-issue-updated", - name: "New Issue Updated Event", + name: "New Issue Updated Event (Instant)", description: "Emit new event when an issue is updated. Note that Jira supports only one webhook, if more sources are needed please use `New Event` source and select multiple events.", - version: "0.0.8", + version: "0.0.10", type: "source", dedupe: "unique", ...common, diff --git a/components/jira_data_center/jira_data_center.app.mjs b/components/jira_data_center/jira_data_center.app.mjs new file mode 100644 index 0000000000000..514daaaea5896 --- /dev/null +++ b/components/jira_data_center/jira_data_center.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "jira_data_center", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/jira_data_center/package.json b/components/jira_data_center/package.json new file mode 100644 index 0000000000000..68e7983304ec7 --- /dev/null +++ b/components/jira_data_center/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/jira_data_center", + "version": "0.0.1", + "description": "Pipedream Jira Data Center Components", + "main": "jira_data_center.app.mjs", + "keywords": [ + "pipedream", + "jira_data_center" + ], + "homepage": "https://pipedream.com/apps/jira_data_center", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/components/jira_service_desk/README.md b/components/jira_service_desk/README.md new file mode 100644 index 0000000000000..fd93c3f7e85da --- /dev/null +++ b/components/jira_service_desk/README.md @@ -0,0 +1,11 @@ +# Overview + +The Jira Service Desk API allows you to interact programmatically with your support ticket system, enabling the creation, updating, and management of tickets, customers, and service desk configurations. With Pipedream's integration, you can automate workflows by connecting Jira Service Desk to hundreds of other apps, listen to webhooks for real-time triggers, and execute custom logic. + +# Example Use Cases + +- **Automate Ticket Creation from Emails**: Create Jira Service Desk tickets automatically when an email is received in a connected mailbox service like Gmail. Use this to ensure requests are logged and actioned quickly. + +- **Sync Support Tickets with a CRM**: Maintain alignment between your customer support and sales by syncing new Jira Service Desk tickets with CRM tools like Salesforce. This can help provide sales with visibility into customer issues. + +- **Slack Notifications for High Priority Tickets**: Get instant alerts in a designated Slack channel when high priority tickets are logged in Jira Service Desk. This helps your support team to respond rapidly to critical issues. diff --git a/components/jobber/README.md b/components/jobber/README.md new file mode 100644 index 0000000000000..05c2029aa4d5f --- /dev/null +++ b/components/jobber/README.md @@ -0,0 +1,11 @@ +# Overview + +The Jobber API allows for the automation of service business operations, such as scheduling jobs, managing clients, and invoicing. By pairing it with Pipedream, you can craft powerful serverless workflows that react to events in Jobber or integrate with other services to streamline your business processes. With Pipedream’s ability to connect to hundreds of apps, you can create custom automations without writing a lot of code, handling everything from data transformations to complex logic. + +# Example Use Cases + +- **Job Completion to Invoice Generation**: Once a job is marked as completed in Jobber, a workflow in Pipedream automatically generates an invoice for that job, attaches a report if needed, and sends it to the customer, streamlining the billing process. + +- **Client Onboarding Automation**: When a new client is added to Jobber, Pipedream triggers a workflow that sends a welcome email using SendGrid, creates a new contact in HubSpot, and schedules an initial consultation call on Google Calendar, ensuring no step in the onboarding process is missed. + +- **Real-time Job Updates to Slack**: Keep your team informed with real-time updates by using Pipedream to send a notification to a designated Slack channel whenever the status of a job in Jobber changes. This ensures everyone is up to speed with the current status without having to check Jobber directly. diff --git a/components/jobber/actions/create-client/create-client.mjs b/components/jobber/actions/create-client/create-client.mjs index 266353ab47fe5..21eb20db2aff1 100644 --- a/components/jobber/actions/create-client/create-client.mjs +++ b/components/jobber/actions/create-client/create-client.mjs @@ -5,7 +5,7 @@ export default { key: "jobber-create-client", name: "Create Client", description: "Generates a new client within Jobber. [See the documentation](https://developer.getjobber.com/docs)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { jobber, diff --git a/components/jobber/actions/create-quote/create-quote.mjs b/components/jobber/actions/create-quote/create-quote.mjs index 36af7b914de8f..99ff4a7e216d0 100644 --- a/components/jobber/actions/create-quote/create-quote.mjs +++ b/components/jobber/actions/create-quote/create-quote.mjs @@ -3,8 +3,8 @@ import jobber from "../../jobber.app.mjs"; export default { key: "jobber-create-quote", name: "Create Quote", - description: "Generates a new quote for the client's first property in Jobber. [See the documentation](https://developer.getjobber.com/docs/)", - version: "0.0.1", + description: "Generates a new quote for a client's property in Jobber. [See the documentation](https://developer.getjobber.com/docs/)", + version: "0.0.2", type: "action", props: { jobber, @@ -23,6 +23,9 @@ export default { propDefinition: [ jobber, "propertyId", + (c) => ({ + clientId: c.clientId, + }), ], }, message: { diff --git a/components/jobber/actions/create-request/create-request.mjs b/components/jobber/actions/create-request/create-request.mjs index f2dac782c51f7..77bda5eef6a19 100644 --- a/components/jobber/actions/create-request/create-request.mjs +++ b/components/jobber/actions/create-request/create-request.mjs @@ -6,7 +6,7 @@ export default { key: "jobber-create-request", name: "Create Service Request", description: "Creates a new service request for a client's first property within Jobber. [See the documentation](https://developer.getjobber.com/docs/)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { jobber, diff --git a/components/jobber/jobber.app.mjs b/components/jobber/jobber.app.mjs index 8297edc2ccbdf..7d2c54c05aeea 100644 --- a/components/jobber/jobber.app.mjs +++ b/components/jobber/jobber.app.mjs @@ -35,11 +35,14 @@ export default { type: "string", label: "Property ID", description: "The ID of a property", - async options() { + async options({ clientId }) { + const filter = clientId + ? `(filter: { clientId: "${clientId}" })` + : ""; const { data: { properties: { nodes } } } = await this.post({ data: { query: `query GetProperties { - properties { + properties${filter} { nodes { id address { @@ -74,7 +77,7 @@ export default { url: `${this._baseUrl()}${path}`, headers: { "Authorization": `Bearer ${this.$auth.oauth_access_token}`, - "X-JOBBER-GRAPHQL-VERSION": "2023-11-15", + "X-JOBBER-GRAPHQL-VERSION": "2025-01-20", }, }); }, diff --git a/components/jobber/package.json b/components/jobber/package.json index 55d015955bc1f..c78370bd67c8d 100644 --- a/components/jobber/package.json +++ b/components/jobber/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/jobber", - "version": "0.1.0", + "version": "0.1.1", "description": "Pipedream Jobber Components", "main": "jobber.app.mjs", "keywords": [ @@ -13,7 +13,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.6.0", + "@pipedream/platform": "^3.0.3", "moment-timezone": "^0.5.45" } } diff --git a/components/jobber_developer_app/README.md b/components/jobber_developer_app/README.md new file mode 100644 index 0000000000000..358c5718e75a8 --- /dev/null +++ b/components/jobber_developer_app/README.md @@ -0,0 +1,11 @@ +# Overview + +The Jobber (Developer App) API enables the automation of tasks within Jobber, a platform designed for home service businesses. It allows for the management and syncing of client information, job scheduling, invoicing, and more through programmatic means. On Pipedream, you can leverage this API to create serverless workflows that integrate Jobber with other apps and services, streamlining operations and improving efficiency. + +# Example Use Cases + +- **Client Sync Workflow**: Create a workflow that triggers whenever a new client is added in Jobber. The client's details are automatically synced to a CRM like Salesforce or HubSpot, ensuring all customer information is consistent and up-to-date across platforms. + +- **Automated Invoice Reminders**: Set up a workflow that listens for job completion events in Jobber, then automatically generates and sends an invoice to the client via email. If the invoice remains unpaid after a certain period, trigger follow-up reminders to encourage prompt payment. + +- **Job Scheduling Alerts**: Implement a workflow where new jobs scheduled in Jobber trigger notifications to staff members via Slack or SMS. This helps keep the team informed and ready for upcoming tasks, improving responsiveness and coordination. diff --git a/components/jobber_developer_app/jobber_developer_app.app.mjs b/components/jobber_developer_app/jobber_developer_app.app.mjs new file mode 100644 index 0000000000000..afd9bc8e0de13 --- /dev/null +++ b/components/jobber_developer_app/jobber_developer_app.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "jobber_developer_app", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/jobber_developer_app/package.json b/components/jobber_developer_app/package.json new file mode 100644 index 0000000000000..14ed06b9a269f --- /dev/null +++ b/components/jobber_developer_app/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/jobber_developer_app", + "version": "0.0.1", + "description": "Pipedream Jobber (Developer App) Components", + "main": "jobber_developer_app.app.mjs", + "keywords": [ + "pipedream", + "jobber_developer_app" + ], + "homepage": "https://pipedream.com/apps/jobber_developer_app", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/jobnimbus/README.md b/components/jobnimbus/README.md index 4006f9d1d3398..9dcf435c448a0 100644 --- a/components/jobnimbus/README.md +++ b/components/jobnimbus/README.md @@ -1,12 +1,11 @@ # Overview -Jobnimbus API can be used to create custom applications for managing job -information. +The Jobnimbus API allows for the creation of complex workflows revolving around job and customer management within the construction and contracting industries. With this API, users can automate tasks such as updating job statuses, managing contacts, and creating tasks or appointments. This streamlines the process of moving a job from lead to completion, ensuring communication and record-keeping are seamless. -Some examples of what you can build using the Jobnimbus API include: +# Example Use Cases -- A custom job board -- A job search engine -- A job management system -- A CRM for job-related information -- A job-related data analytics platform +- **Automated Job Status Updates**: Trigger a Pipedream workflow whenever a job status is updated in Jobnimbus to send a custom notification via SMS or email through Twilio or SendGrid. This keeps team members in the loop in real-time about job progress. + +- **Dynamic Contact Import**: When a new contact is added to a Google Sheet, use Pipedream to automatically create a corresponding contact in Jobnimbus. This ensures your customer database is always synchronized and up-to-date without manual entry. + +- **Task Management with Calendar Integration**: On creation of a new task in Jobnimbus, trigger a Pipedream workflow to add an event to a Google Calendar, inviting all relevant team members. This automation helps in scheduling and making sure everyone is aware of their assignments. diff --git a/components/join/README.md b/components/join/README.md new file mode 100644 index 0000000000000..bcc096ba40caa --- /dev/null +++ b/components/join/README.md @@ -0,0 +1,11 @@ +# Overview + +The Join API allows for pushing notifications, URLs, files, and commands between devices connected to a user's Google account. By leveraging Join with Pipedream, you can automate cross-device messaging and data sharing. It's a bridge that connects your devices, enabling them to work in tandem through automated workflows. With Pipedream's capacity for integration, you can trigger events from a plethora of apps to send data to your devices via Join or vice versa. + +# Example Use Cases + +- **Notification Forwarding**: Send notifications from any of your apps or services directly to your Android device. For instance, trigger a Pipedream workflow with a GitHub webhook when a new issue is created, and use Join to push a notification to your phone alerting you to the new issue. + +- **URL Sharing for Quick Access**: Automatically send URLs from a monitored email address to your browser. Use Pipedream to watch for incoming emails with Zapier Email Parser, extract URLs, and then push them to your devices with Join, so you can quickly open webpages on the go. + +- **File Transfers Between Devices**: Set up a workflow to move files from cloud storage services like Dropbox to your device. Whenever a new file is added to a specified Dropbox folder, Pipedream can trigger a workflow that sends the file to your Android tablet or phone via Join, keeping all your devices in sync. diff --git a/components/join/package.json b/components/join/package.json index 2d4040d9c4795..fe50cf6f66b24 100644 --- a/components/join/package.json +++ b/components/join/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/joomla/README.md b/components/joomla/README.md index 2c5d09818d9f8..adc02aaa47ad1 100644 --- a/components/joomla/README.md +++ b/components/joomla/README.md @@ -1,14 +1,11 @@ # Overview -As one of the most popular content management systems, Joomla! offers a wide -range of features and capabilities. With the help of the Joomla! API, you can -extend the functionality of the CMS even further. - -Here are some examples of what you can build using the Joomla! API: - -- Custom components -- Modules -- Plugins -- Templates -- Language packs -- Editions +The Joomla! API provides a powerful way to interact with the Joomla! content management system programmatically. With this API, you can automate content creation, user management, and site maintenance tasks. Pipedream’s serverless platform enhances these capabilities, allowing you to create workflows that trigger on specific events, process data, and integrate with countless other services to extend the functionality of your Joomla! site. + +# Example Use Cases + +- **Automate Content Publishing**: Trigger a workflow in Pipedream when a draft is ready for review in Google Docs. Use the Joomla! API to programmatically publish the content to your site once it’s approved, ensuring a seamless content pipeline from draft to live post. + +- **Sync User Data with CRM**: Sync new Joomla! user registrations to a CRM like Salesforce. When a new user registers on your site, automatically create or update their record in your CRM, keeping your sales and marketing teams up to date with the latest subscriber information. + +- **Site Backup and Maintenance**: Schedule nightly backups of your Joomla! site and store them in a cloud storage service like Google Drive or Dropbox. Use Pipedream to trigger the backup process, compress the site data, and securely transfer it to your chosen storage solution for safekeeping. diff --git a/components/jooto/actions/create-task/create-task.mjs b/components/jooto/actions/create-task/create-task.mjs new file mode 100644 index 0000000000000..85d59cd0dc26c --- /dev/null +++ b/components/jooto/actions/create-task/create-task.mjs @@ -0,0 +1,61 @@ +import app from "../../jooto.app.mjs"; + +export default { + key: "jooto-create-task", + name: "Create Task", + description: "Create a new task in the selected project. [See the documentation](https://www.jooto.com/api/reference/request/#/default/post-boards-id-tasks)", + version: "0.0.1", + type: "action", + props: { + app, + projectId: { + propDefinition: [ + app, + "projectId", + ], + }, + userId: { + propDefinition: [ + app, + "userId", + ], + }, + listId: { + propDefinition: [ + app, + "listId", + (c) => ({ + projectId: c.projectId, + }), + ], + }, + taskName: { + propDefinition: [ + app, + "taskName", + ], + }, + description: { + propDefinition: [ + app, + "description", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createTask({ + $, + id: this.projectId, + data: { + name: this.taskName, + description: this.description, + list_id: this.listId, + assigned_user_ids: this.userId, + }, + }); + + $.export("$summary", `Successfully created task with ID: '${response.id}'`); + + return response; + }, +}; diff --git a/components/jooto/actions/get-projects/get-projects.mjs b/components/jooto/actions/get-projects/get-projects.mjs new file mode 100644 index 0000000000000..5a51df283bdfb --- /dev/null +++ b/components/jooto/actions/get-projects/get-projects.mjs @@ -0,0 +1,21 @@ +import app from "../../jooto.app.mjs"; + +export default { + key: "jooto-get-projects", + name: "Get Projects", + description: "Get a list of projects in your organization. [See the documentation](https://www.jooto.com/api/reference/request/#/default/get-projects)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.getProjects({ + $, + }); + + $.export("$summary", `Successfully retrieved '${response.boards.length}' projects`); + + return response; + }, +}; diff --git a/components/jooto/actions/update-task/update-task.mjs b/components/jooto/actions/update-task/update-task.mjs new file mode 100644 index 0000000000000..50ab3a22d3c82 --- /dev/null +++ b/components/jooto/actions/update-task/update-task.mjs @@ -0,0 +1,71 @@ +import app from "../../jooto.app.mjs"; + +export default { + key: "jooto-update-task", + name: "Update Task", + description: "Update a new task in the selected project. [See the documentation](https://www.jooto.com/api/reference/request/#/default/patch-boards-id-tasks-task_id)", + version: "0.0.1", + type: "action", + props: { + app, + projectId: { + propDefinition: [ + app, + "projectId", + ], + }, + taskId: { + propDefinition: [ + app, + "taskId", + (c) => ({ + projectId: c.projectId, + }), + ], + }, + userId: { + propDefinition: [ + app, + "userId", + ], + }, + listId: { + propDefinition: [ + app, + "listId", + (c) => ({ + projectId: c.projectId, + }), + ], + }, + taskName: { + propDefinition: [ + app, + "taskName", + ], + }, + description: { + propDefinition: [ + app, + "description", + ], + }, + }, + async run({ $ }) { + const response = await this.app.updateTask({ + $, + id: this.projectId, + task_id: this.taskId, + data: { + name: this.taskName, + description: this.description, + list_id: this.listId, + assigned_user_ids: this.userId, + }, + }); + + $.export("$summary", `Successfully updated task with ID: '${response.id}'`); + + return response; + }, +}; diff --git a/components/jooto/jooto.app.mjs b/components/jooto/jooto.app.mjs new file mode 100644 index 0000000000000..7b9ef737db26b --- /dev/null +++ b/components/jooto/jooto.app.mjs @@ -0,0 +1,149 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "jooto", + propDefinitions: { + projectId: { + type: "string", + label: "Project ID", + description: "ID of the Project", + async options() { + const response = await this.getProjects({}); + const projectsIds = response.boards; + return projectsIds.map(({ + id, title, + }) => ({ + value: id, + label: title, + })); + }, + }, + taskId: { + type: "string", + label: "Task ID", + description: "ID of the Task", + async options({ projectId }) { + const response = await this.getTasks({ + id: projectId, + }); + const tasksIds = response.tasks; + return tasksIds.map(({ + id, name, + }) => ({ + value: id, + label: name, + })); + }, + }, + listId: { + type: "string", + label: "List ID", + description: "ID of the List", + async options({ projectId }) { + const response = await this.getLists({ + id: projectId, + }); + const listsIds = response.lists; + return listsIds.map(({ + id, name, + }) => ({ + value: id, + label: name, + })); + }, + }, + userId: { + type: "string[]", + label: "Assigned Users IDs", + description: "The ID of the assigned users", + async options() { + const response = await this.getUsers(); + const usersIds = response.users; + return usersIds.map(({ + id, name, + }) => ({ + value: id, + label: name, + })); + }, + }, + taskName: { + type: "string", + label: "Task Name", + description: "Name of the Task", + }, + description: { + type: "string", + label: "Description", + description: "Description of the Task", + }, + }, + methods: { + _baseUrl() { + return "https://api.jooto.com/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "X-Jooto-Api-Key": `${this.$auth.api_key}`, + }, + }); + }, + async createTask({ + id, ...args + }) { + return this._makeRequest({ + method: "post", + path: `/boards/${id}/tasks`, + ...args, + }); + }, + async getProjects(args = {}) { + return this._makeRequest({ + path: "/boards", + ...args, + }); + }, + async updateTask({ + id, task_id, ...args + }) { + return this._makeRequest({ + method: "patch", + path: `/boards/${id}/tasks/${task_id}`, + ...args, + }); + }, + async getTasks({ + id, ...args + }) { + return this._makeRequest({ + path: `/boards/${id}/tasks`, + ...args, + }); + }, + async getLists({ + id, ...args + }) { + return this._makeRequest({ + path: `/boards/${id}/lists`, + ...args, + }); + }, + async getUsers(args = {}) { + return this._makeRequest({ + path: "/users", + ...args, + }); + }, + }, +}; diff --git a/components/jooto/package.json b/components/jooto/package.json new file mode 100644 index 0000000000000..b5dae71993b0e --- /dev/null +++ b/components/jooto/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/jooto", + "version": "0.1.0", + "description": "Pipedream Jooto Components", + "main": "jooto.app.mjs", + "keywords": [ + "pipedream", + "jooto" + ], + "homepage": "https://pipedream.com/apps/jooto", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/joplin/README.md b/components/joplin/README.md new file mode 100644 index 0000000000000..0eaefe121e251 --- /dev/null +++ b/components/joplin/README.md @@ -0,0 +1,11 @@ +# Overview + +The Joplin API enables you to create, read, update, and delete notes, notebooks, tags, and resources, as well as manage note revisions and searching within Joplin. With Pipedream, you can leverage these API capabilities to automate workflows involving note-taking, organization, and synchronization with other apps. For example, you can build workflows that trigger when you create a new note, append content to an existing note from external sources, or synchronize notes across different platforms. + +# Example Use Cases + +- **Automatically Create Joplin Notes from Emails**: Set up a workflow that listens for incoming emails using an app like Gmail on Pipedream, parses the email content, and creates a new note in a specified Joplin notebook. This helps you keep track of important emails as part of your note-taking routine. + +- **Sync Notes with a Calendar App**: Create a workflow that triggers when a new note with a specific tag is added in Joplin. The workflow can then format the note content and create an event in a calendar app like Google Calendar, with the note details included in the event description. This allows for an integrated planning system that connects notes with scheduled activities. + +- **Backup Joplin Notes to Cloud Storage**: Build a workflow that triggers at regular intervals, retrieves notes from Joplin, and then uploads them to a cloud storage service such as Dropbox. This can be used to maintain a backup of your notes or to share them with other users or devices outside of Joplin's native sync functionality. diff --git a/components/joplin/joplin.app.mjs b/components/joplin/joplin.app.mjs new file mode 100644 index 0000000000000..7e172e4863f60 --- /dev/null +++ b/components/joplin/joplin.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "joplin", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/joplin/package.json b/components/joplin/package.json new file mode 100644 index 0000000000000..56afbdbc57d45 --- /dev/null +++ b/components/joplin/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/joplin", + "version": "0.0.1", + "description": "Pipedream Joplin Components", + "main": "joplin.app.mjs", + "keywords": [ + "pipedream", + "joplin" + ], + "homepage": "https://pipedream.com/apps/joplin", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/jotform/README.md b/components/jotform/README.md index ebe2ecf08b5af..28bc2b8d87b50 100644 --- a/components/jotform/README.md +++ b/components/jotform/README.md @@ -1,13 +1,11 @@ # Overview -Using the Jotform API, you can easily add forms to your website or application. -You can also use the Jotform API to manage your form submissions, gather -insights from your form data, and more. +Jotform’s API is a powerhouse for automating form and survey data management. With Pipedream, harness this API to trigger workflows from new form submissions, manipulate and analyze your form data, and sync it across various platforms. Think streamlined data entry to CRMs, instant notifications for new leads or feedback, and timely data backups to cloud storage. -Here are some examples of what you can build using the Jotform API: +# Example Use Cases -- A contact form for your website -- A survey form to collect data from your users -- A registration form for your events or courses -- A payment form for your products or services -- A lead capture form to collect leads from your website visitors +- **Automated Lead Capture to CRM**: When a new form submission comes through Jotform, Pipedream can kick off a workflow that parses the submission, extracting key information like names, emails, and preferences. This data can then be sent directly to a CRM like Salesforce, keeping your lead database fresh and up-to-date without manual input. + +- **Survey Response Analysis and Reporting**: Jotform collects survey data, but what next? Pipedream can take each submission and run it through analytical processes, perhaps using sentiment analysis or adding the data to a Google Sheet for further review. Then, compile a report with tools like Tableau or Google Data Studio and even email the report to stakeholders directly from the workflow. + +- **Real-Time Notifications for High-Priority Submissions**: Set criteria for flagging high-priority form submissions, such as support requests marked ‘urgent’. Pipedream can monitor your Jotform submissions and when it spots a high-priority one, it can immediately send a message via Slack, SMS, or even create a task in a project management tool like Asana to prompt swift action. diff --git a/components/jotform/actions/get-form-submissions/get-form-submissions.mjs b/components/jotform/actions/get-form-submissions/get-form-submissions.mjs index da5c94f92ef1d..769186ca23ceb 100644 --- a/components/jotform/actions/get-form-submissions/get-form-submissions.mjs +++ b/components/jotform/actions/get-form-submissions/get-form-submissions.mjs @@ -5,49 +5,42 @@ export default { key: "jotform-get-form-submissions", name: "Get Form Submissions", description: "Gets a list of form responses [See the docs here](https://api.jotform.com/docs/#form-id-submissions)", - version: "0.1.0", + version: "0.1.3", type: "action", props: { ...common.props, - formId: { + teamId: { propDefinition: [ common.props.jotform, - "formId", + "teamId", ], }, - max: { + formId: { propDefinition: [ common.props.jotform, - "max", + "formId", + (c) => ({ + teamId: c.teamId, + }), ], }, - encrypted: { + max: { propDefinition: [ common.props.jotform, - "encrypted", + "max", ], - reloadProps: true, }, }, - async additionalProps() { - const props = {}; - if (this.encrypted) { - props.privateKey = common.props.jotform.propDefinitions.privateKey; - } - return props; - }, async run({ $ }) { const params = { $, max: this.max, formId: this.formId, + teamId: this.teamId, }; const submissions = await this.paginate(this.jotform.getFormSubmissions.bind(this), params); const results = []; - for await (let submission of submissions) { - if (this.encrypted) { - submission = this.jotform.decryptSubmission(submission, this.privateKey); - } + for await (const submission of submissions) { results.push(submission); } $.export("$summary", `Successfully retrieved ${results.length} form submission${results.length === 1 diff --git a/components/jotform/actions/get-monthly-user-usage/get-monthly-user-usage.mjs b/components/jotform/actions/get-monthly-user-usage/get-monthly-user-usage.mjs index b6c44ad3ad55d..1925da2d9ebe2 100644 --- a/components/jotform/actions/get-monthly-user-usage/get-monthly-user-usage.mjs +++ b/components/jotform/actions/get-monthly-user-usage/get-monthly-user-usage.mjs @@ -5,7 +5,7 @@ export default { key: "jotform-get-monthly-user-usage", name: "Get Monthly User Usage", description: "Gets number of form submissions received this month. Also, get number of SSL form submissions, payment form submissions and upload space used by user [See the docs here](https://api.jotform.com/docs/#user-usage)", - version: "0.0.3", + version: "0.0.6", type: "action", props: { ...common.props, diff --git a/components/jotform/actions/get-user-submissions/get-user-submissions.mjs b/components/jotform/actions/get-user-submissions/get-user-submissions.mjs index f20e8a368b7c3..17c47dec8576c 100644 --- a/components/jotform/actions/get-user-submissions/get-user-submissions.mjs +++ b/components/jotform/actions/get-user-submissions/get-user-submissions.mjs @@ -5,7 +5,7 @@ export default { key: "jotform-get-user-submissions", name: "Get User Submissions", description: "Gets a list of all submissions for all forms on the account [See the docs here](https://api.jotform.com/docs/#user-submissions)", - version: "0.1.0", + version: "0.1.3", type: "action", props: { ...common.props, @@ -15,13 +15,6 @@ export default { "max", ], }, - encrypted: { - propDefinition: [ - common.props.jotform, - "encrypted", - ], - reloadProps: true, - }, }, async additionalProps() { const props = {}; @@ -37,10 +30,7 @@ export default { }; const submissions = await this.paginate(this.jotform.getUserSubmissions.bind(this), params); const results = []; - for await (let submission of submissions) { - if (this.encrypted) { - submission = this.jotform.decryptSubmission(submission, this.privateKey); - } + for await (const submission of submissions) { results.push(submission); } $.export("$summary", `Successfully retrieved ${results.length} form submission${results.length === 1 diff --git a/components/jotform/jotform.app.mjs b/components/jotform/jotform.app.mjs index 91c5dc1d95e97..c791cb44a635c 100644 --- a/components/jotform/jotform.app.mjs +++ b/components/jotform/jotform.app.mjs @@ -1,7 +1,5 @@ import { axios } from "@pipedream/platform"; import querystring from "query-string"; -import crypto from "crypto"; -import constants from "./common/constants.mjs"; export default { type: "app", @@ -11,19 +9,44 @@ export default { type: "string", label: "Form", description: "The form to watch for new submissions", - async options({ page = 0 }) { + async options({ + page = 0, teamId, excludeDeleted = false, + }) { const limit = 20; const offset = page * limit; - const forms = await this.getForms({ + let { content: forms } = await this.getForms({ offset, limit, - }); - return forms.content.map((form) => ({ + }, teamId); + if (excludeDeleted) { + forms = forms.filter(({ status }) => status !== "DELETED"); + } + return forms.map((form) => ({ label: form.title, value: form.id, })); }, }, + teamId: { + type: "string", + label: "Team", + description: "The identifier of a team. Note: Teams is a Jotform Enterprise feature.", + optional: true, + async options({ page = 0 }) { + const limit = 20; + const offset = page * limit; + const { content } = await this.listTeams({ + offset, + limit, + }); + return content.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, formTitle: { type: "string", label: "Form Title", @@ -74,36 +97,40 @@ export default { description: "Maximum number of items to return", default: 20, }, - encrypted: { - type: "boolean", - label: "Encrypted", - description: "Are the form responses encrypted? Set to `true` to decrypt responses.", - optional: true, - }, - privateKey: { - type: "string", - label: "Private Key", - description: "The private key provided/created when setting the form as encrypted. Starts with `-----BEGIN RSA PRIVATE KEY-----` and ends with `-----END RSA PRIVATE KEY-----`", - secret: true, - }, }, methods: { _getBaseUrl() { - return `https://${this.$auth.region}.jotform.com/`; + let baseUrl = `https://${this.$auth.region}.jotform.com/`; + const standardSubdomains = [ + "api", + "eu-api", + "hipaa-api", + ]; + if (!standardSubdomains.includes(this.$auth.region)) { + baseUrl += "API/"; + } + return baseUrl; }, _ensureTrailingSlash(str) { return (str.endsWith("/")) ? str : `${str}/`; }, + _headers(teamId) { + const headers = { + "APIKEY": this.$auth.api_key, + }; + if (teamId) { + headers["jf-team-id"] = teamId; + } + return headers; + }, async _makeRequest({ - $, endpoint, method = "GET", params = null, + $, endpoint, method = "GET", params = null, teamId, }) { const config = { url: `${this._getBaseUrl()}${endpoint}`, - headers: { - "APIKEY": this.$auth.api_key, - }, + headers: this._headers(teamId), method, }; if (params) { @@ -120,6 +147,7 @@ export default { const { formId, endpoint, + teamId, } = opts; return this._makeRequest({ endpoint: `form/${encodeURIComponent(formId)}/webhooks`, @@ -127,15 +155,18 @@ export default { params: { webhookURL: this._ensureTrailingSlash(endpoint), }, + teamId, }); }, async deleteHook(opts = {}) { const { formId, endpoint, + teamId, } = opts; const result = await this.getWebhooks({ formId, + teamId, }); let webhooks = Object.values(result && result.content || {}); let webhookIdx = -1; @@ -152,34 +183,39 @@ export default { return this._makeRequest({ endpoint: `form/${encodeURIComponent(formId)}/webhooks/${encodeURIComponent(webhookIdx)}`, method: "DELETE", + teamId, }); }, - async getForm(formId) { + async getForm(formId, teamId) { return this._makeRequest({ endpoint: `form/${formId}`, + teamId, }); }, - async getForms(params) { + async getForms(params, teamId) { return this._makeRequest({ endpoint: "user/forms", params, + teamId, }); }, getFormSubmission({ - $, submissionId, + $, submissionId, teamId, }) { return this._makeRequest({ $, endpoint: `submission/${submissionId}`, + teamId, }); }, async getFormSubmissions({ - $, formId, params = null, + $, formId, teamId, params = null, }) { return this._makeRequest({ $, endpoint: `form/${formId}/submissions`, params, + teamId, }); }, async getUserSubmissions({ $ }) { @@ -195,44 +231,19 @@ export default { }); }, async getWebhooks(opts = {}) { - const { formId } = opts; + const { + formId, teamId, + } = opts; return this._makeRequest({ endpoint: `form/${encodeURIComponent(formId)}/webhooks`, + teamId, }); }, - decryptSubmission(submission, privateKey) { - const { answers } = submission; - if (!answers) { - return submission; - } - - if (!privateKey.includes(`${constants.KEY_HEADER}\n`)) { - privateKey = privateKey.replace(constants.KEY_HEADER, `${constants.KEY_HEADER}\n`); - } - if (!privateKey.includes(`\n${constants.KEY_FOOTER}`)) { - privateKey = privateKey.replace(constants.KEY_FOOTER, `\n${constants.KEY_FOOTER}`); - } - - for (const answer of Object.keys(answers)) { - if (!answers[answer]["answer"]) { - continue; - } - for (const question of Object.keys(answers[answer]["answer"])) { - const q = answers[answer]["answer"][question]; - try { - const decrypted = crypto.privateDecrypt({ - key: privateKey, - passphrase: "", - padding: crypto.constants.RSA_PKCS1_PADDING, - }, Buffer.from(q, "base64")); - answers[answer]["answer"][question] = decrypted.toString("utf8"); - } catch (e) { - // not a base64 string - continue; - } - } - } - return submission; + listTeams(opts = {}) { + return this._makeRequest({ + endpoint: "/team/user/me", + ...opts, + }); }, }, }; diff --git a/components/jotform/package.json b/components/jotform/package.json index 72abd6c74edfd..a7337a7fb48ac 100644 --- a/components/jotform/package.json +++ b/components/jotform/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/jotform", - "version": "0.4.9", + "version": "0.4.12", "description": "Pipedream Jotform Components", "main": "jotform.app.mjs", "keywords": [ diff --git a/components/jotform/sources/new-submission/new-submission.mjs b/components/jotform/sources/new-submission/new-submission.mjs index deb76e522242d..dc4db08159066 100644 --- a/components/jotform/sources/new-submission/new-submission.mjs +++ b/components/jotform/sources/new-submission/new-submission.mjs @@ -5,47 +5,41 @@ export default { key: "jotform-new-submission", name: "New Submission (Instant)", description: "Emit new event when a form is submitted", - version: "0.1.2", + version: "0.1.5", type: "source", dedupe: "unique", props: { jotform, http: "$.interface.http", - formId: { + teamId: { propDefinition: [ jotform, - "formId", + "teamId", ], }, - encrypted: { + formId: { propDefinition: [ jotform, - "encrypted", + "formId", + (c) => ({ + teamId: c.teamId, + excludeDeleted: true, + }), ], - reloadProps: true, }, }, - async additionalProps() { - const props = {}; - if (this.encrypted) { - props.privateKey = jotform.propDefinitions.privateKey; - } - return props; - }, hooks: { async deploy() { - const { content: form } = await this.jotform.getForm(this.formId); + const { content: form } = await this.jotform.getForm(this.formId, this.teamId); const { content: submissions } = await this.jotform.getFormSubmissions({ formId: this.formId, + teamId: this.teamId, params: { limit: 25, orderby: "created_at", }, }); for (let submission of submissions.reverse()) { - if (this.encrypted) { - submission = this.jotform.decryptSubmission(submission, this.privateKey); - } const meta = { id: submission.id, summary: form.title, @@ -58,12 +52,14 @@ export default { return (await this.jotform.createHook({ endpoint: this.http.endpoint, formId: this.formId, + teamId: this.teamId, })); }, async deactivate() { return (await this.jotform.deleteHook({ endpoint: this.http.endpoint, formId: this.formId, + teamId: this.teamId, })); }, }, @@ -71,6 +67,7 @@ export default { const { body } = event; let { content: submission } = await this.jotform.getFormSubmission({ submissionId: body.submissionID, + teamId: this.teamId, }); // insert answers from the webhook event @@ -83,10 +80,6 @@ export default { } } - if (this.encrypted) { - submission = this.jotform.decryptSubmission(submission, this.privateKey); - } - this.$emit(submission, { summary: body.formTitle || JSON.stringify(body), id: body.submissionID, diff --git a/components/jp_funda/README.md b/components/jp_funda/README.md new file mode 100644 index 0000000000000..af79655bc666a --- /dev/null +++ b/components/jp_funda/README.md @@ -0,0 +1,11 @@ +# Overview + +The JP Funda API offers access to a wealth of financial data, including stock prices, company fundamentals, and market trends. On Pipedream, you can leverage this API to create powerful serverless workflows that automate financial analysis, track investment opportunities, and notify you of important market changes. By integrating JP Funda with other apps, you can enrich financial datasets, streamline reporting, and make data-driven decisions with ease. + +# Example Use Cases + +- **Market Movement Alerts**: Set up a workflow that monitors stock prices via the JP Funda API. When a specified stock hits a trigger price, send an alert through Slack or email, allowing you to act swiftly on market movements. + +- **Daily Portfolio Summary**: Create a daily routine that fetches your investment portfolio's performance data from JP Funda and compiles it into a digest. Use Pipedream's built-in CRON scheduler to send this summary to your preferred communication platform, like Discord or Microsoft Teams. + +- **Automated Financial Reporting**: Combine JP Funda financial data with Google Sheets to generate automated reports. Use Pipedream to pull key financial metrics periodically and append them to a Google Sheet for trend analysis or presentation to stakeholders. diff --git a/components/judge_me/README.md b/components/judge_me/README.md new file mode 100644 index 0000000000000..effb3ff37fc1b --- /dev/null +++ b/components/judge_me/README.md @@ -0,0 +1,11 @@ +# Overview + +The Judge.me API lets you tap into a rich repository of product review data, enabling you to automate the gathering, moderation, and publication of reviews on your store. You can also trigger actions based on review events, like thanking a customer after they leave a positive review or addressing negative feedback promptly. With Pipedream's serverless platform, you can craft workflows to interact with other apps, streamline processes, and react to review-related activities in real-time, without writing any backend code. + +# Example Use Cases + +- **Sync Reviews to a Google Sheet**: Automatically push new reviews from Judge.me into a Google Sheet for easy monitoring and analysis. This workflow can help with aggregating data for reporting purposes or sharing with your team. + +- **Send Review Thank-You Emails**: Trigger an automated thank-you email to customers who've left a review, using an email service like SendGrid. Personalize the message based on the review content and rating, enhancing customer relationships. + +- **Create Support Tickets from Negative Reviews**: Whenever a review below a certain rating threshold is received, create a support ticket in a tool like Zendesk. This allows your support team to follow up quickly and address any concerns raised by the customer. diff --git a/components/jumpcloud/jumpcloud.app.mjs b/components/jumpcloud/jumpcloud.app.mjs new file mode 100644 index 0000000000000..68043ea783b3c --- /dev/null +++ b/components/jumpcloud/jumpcloud.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "jumpcloud", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/jumpcloud/package.json b/components/jumpcloud/package.json new file mode 100644 index 0000000000000..efec4247f6673 --- /dev/null +++ b/components/jumpcloud/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/jumpcloud", + "version": "0.0.1", + "description": "Pipedream JumpCloud Components", + "main": "jumpcloud.app.mjs", + "keywords": [ + "pipedream", + "jumpcloud" + ], + "homepage": "https://pipedream.com/apps/jumpcloud", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/jumpseller/README.md b/components/jumpseller/README.md new file mode 100644 index 0000000000000..a41eff5ddf19f --- /dev/null +++ b/components/jumpseller/README.md @@ -0,0 +1,11 @@ +# Overview + +The Jumpseller API lets you automate and integrate your e-commerce operations, ranging from managing products, orders, and customers to updating inventory levels. With Pipedream, you can harness this functionality in serverless workflows, enabling you to connect Jumpseller with other apps and services. Craft workflows to simplify tasks such as syncing order info to accounting software, triggering custom alerts for new sales, or maintaining a real-time customer database. + +# Example Use Cases + +- **Sync Orders with Google Sheets**: Automatically update a Google Sheet with new order details when they're placed on Jumpseller. This can help maintain organized records for accounting or inventory management purposes. + +- **Send Custom Alerts on Slack for New Orders**: Set up a workflow that posts a message to a designated Slack channel whenever a new order is created on Jumpseller. This can keep your team in the loop and promote timely responses to new sales. + +- **Automatically Update Inventory**: When a product's stock changes on Jumpseller, trigger a workflow to reflect these changes across other platforms or your internal systems, ensuring accurate inventory levels are maintained at all times. diff --git a/components/junip/README.md b/components/junip/README.md index 395cd0de18daf..64d91a297f988 100644 --- a/components/junip/README.md +++ b/components/junip/README.md @@ -1,11 +1,11 @@ # Overview -With Junip, you can build awesome things like: - -- A to-do list -- A notes app -- A task tracker -- A budgeting tool -- An inventory management system -- A CRM -- A project management tool +The Junip API provides a powerful way to integrate customer reviews and testimonials into your business workflow. By leveraging the Junip API on Pipedream, you can automate the collection, moderation, and publication of customer feedback, ensuring your potential clients see the most relevant and influential reviews. It's a boon for businesses looking to enhance their social proof and build trust with their audience through automated, real-time review management. + +# Example Use Cases + +- **Automated Review Request Post-Purchase**: After a customer completes a purchase on your e-commerce platform (e.g., Shopify), use Pipedream to trigger an automated email through Junip, requesting a product review. As reviews are collected, they can be automatically moderated and published to your website to enhance product credibility. + +- **Review Analysis for Product Insights**: When new reviews come in via Junip, funnel them into a sentiment analysis tool (like MonkeyLearn) on Pipedream. Analyze the sentiment of the text to gain insights into how customers feel about your products. This data can be invaluable for product development and customer service training. + +- **Real-Time Notifications for High-Risk Reviews**: Set up a Pipedream workflow that monitors Junip for new reviews and uses a set of criteria to determine high-risk feedback (e.g., 1 or 2 stars). When such reviews are detected, send instant notifications to your customer support team through Slack or email, enabling them to take swift action to address the customer's concerns. diff --git a/components/justcall/README.md b/components/justcall/README.md index 6d60f9c5514bf..4ac7d2c4a1308 100644 --- a/components/justcall/README.md +++ b/components/justcall/README.md @@ -1,2 +1,11 @@ # Overview -The JustCall API allows developers to integrate JustCall’s cloud phone and SMS features into their own applications. With the API, developers can manage calls, make and receive texts, create contacts, and much more. \ No newline at end of file + +The JustCall API allows for the automation and integration of telephony services into diverse workflows, enabling users to effectively manage calls and SMS within their business processes. By leveraging Pipedream's capabilities, one can create custom event-driven automation that responds to various triggers from JustCall, such as incoming/outgoing call events or new SMS messages, and connects them with over 300+ apps available on the Pipedream platform. This enables seamless data flow and interaction between JustCall and CRM systems, helpdesk software, marketing automation tools, and more, optimizing communication strategies and improving customer engagement. + +# Example Use Cases + +- **Sync JustCall Contacts with CRM**: Automatically update CRM contacts when new contacts are added in JustCall. For instance, create a workflow that adds a new contact to Salesforce whenever a new contact is created in JustCall, ensuring that your sales team always has the latest information at their fingertips. + +- **Log JustCall Call Records**: Integrate JustCall with Google Sheets to log call details. Each time a call is made or received in JustCall, log the call information, including timestamp, caller ID, and call duration, into a Google Sheet. This can be invaluable for sales teams to track customer interactions and for customer support to maintain detailed records. + +- **Automate SMS Campaigns**: Connect JustCall with marketing platforms like Mailchimp to trigger SMS campaigns. When a subscriber status changes in Mailchimp, use Pipedream to send a personalized SMS via JustCall, enhancing subscriber engagement with timely and relevant messages based on their interactions with your emails. diff --git a/components/jw_player/README.md b/components/jw_player/README.md new file mode 100644 index 0000000000000..c162567313d15 --- /dev/null +++ b/components/jw_player/README.md @@ -0,0 +1,11 @@ +# Overview + +The JW Player API offers a way to manage and deliver video content programmatically. On Pipedream, you can leverage this API to automate video publishing workflows, analyze viewer data, and integrate with other services. Creating, updating, and managing video metadata can be automated, as well as handling video transcoding jobs and analyzing performance with custom metrics. By tapping into Pipedream's serverless platform, you can build powerful automations without managing infrastructure. + +# Example Use Cases + +- **Automated Video Publishing Workflow**: Create a workflow on Pipedream that listens for new videos uploaded to a cloud storage bucket like AWS S3. Once a new video is detected, the workflow uses the JW Player API to upload the video to your JW Player account, set metadata, and publish it to your specified channels or playlists. + +- **Video Performance Dashboard Sync**: Construct a Pipedream workflow that periodically fetches video analytics from JW Player API and sends this data to Google Sheets. This allows you to maintain a real-time dashboard of video performance metrics, like views and engagement, which can be shared with your team or used for reporting purposes. + +- **Social Media Integration**: Build a Pipedream workflow that triggers when a new video is added to JW Player. The workflow could then craft a social media post with the video's details and automatically share it on platforms like Twitter or Facebook using their respective APIs. This helps in promoting new content instantly and driving audience engagement. diff --git a/components/kadoa/README.md b/components/kadoa/README.md new file mode 100644 index 0000000000000..b5f7cdae62f18 --- /dev/null +++ b/components/kadoa/README.md @@ -0,0 +1,11 @@ +# Overview + +The Kadoa API enables automation and integration of Kadoa's time tracking and project management features. With it, you can manage projects, tasks, time entries, and extract reports programmatically. When combined with Pipedream's ability to connect to hundreds of other services and create complex workflows, the potential for increased efficiency and data connectivity is significant. You can trigger workflows on Pipedream with HTTP requests, schedule them, or even run them in response to emails, among other methods. + +# Example Use Cases + +- **Automated Project Time Tracking**: Sync time entries from Kadoa to a Google Sheets document for real-time project time tracking. Every time a new time entry is created in Kadoa, a Pipedream workflow is triggered, appending the time entry details to a designated Google Sheet. + +- **Task Creation Notifications**: Automate notifications via Slack whenever a new task is created in Kadoa. A Pipedream workflow listens for a new task event from Kadoa and sends a formatted message to a specific Slack channel, alerting team members about the new task and its details. + +- **Sync Projects with Calendar**: Integrate Kadoa projects with Google Calendar, creating calendar events automatically when a new project is added in Kadoa. This Pipedream workflow creates a detailed event in Google Calendar for the project's timeline, helping teams keep track of project due dates and milestones. diff --git a/components/kadoa/actions/start-workflow/start-workflow.mjs b/components/kadoa/actions/start-workflow/start-workflow.mjs new file mode 100644 index 0000000000000..1bce79c7a20a8 --- /dev/null +++ b/components/kadoa/actions/start-workflow/start-workflow.mjs @@ -0,0 +1,26 @@ +import kadoa from "../../kadoa.app.mjs"; + +export default { + key: "kadoa-start-workflow", + name: "Start Kadoa Workflow", + description: "Triggers a Kadoa workflow using Pipedream. [See the documentation](https://api.kadoa.com/api-docs/)", + version: "0.0.1", + type: "action", + props: { + kadoa, + workflowId: { + propDefinition: [ + kadoa, + "workflowId", + ], + }, + }, + async run({ $ }) { + const response = await this.kadoa.triggerWorkflow({ + $, + workflowId: this.workflowId, + }); + $.export("$summary", `Successfully triggered workflow ${this.workflowId}`); + return response; + }, +}; diff --git a/components/kadoa/kadoa.app.mjs b/components/kadoa/kadoa.app.mjs index 53db443630156..0ac410ed71b8c 100644 --- a/components/kadoa/kadoa.app.mjs +++ b/components/kadoa/kadoa.app.mjs @@ -1,11 +1,71 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "kadoa", - propDefinitions: {}, + propDefinitions: { + workflowId: { + type: "string", + label: "Workflow ID", + description: "The ID of the workflow to be triggered. The workflow must be approved.", + async options() { + const data = await this.getWorkflowsOverview(); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.kadoa.com/v2"; + }, + _headers() { + return { + "x-api-key": `${this.$auth.api_key}`, + "Accept": "application/json", + }; + }, + _makeRequest({ + $ = this, path, ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: this._headers(), + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks/subscribe", + ...opts, + }); + }, + deleteWebhook(hookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/unsubscribe/${hookId}`, + }); + }, + triggerWorkflow({ + workflowId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/controller/run/${workflowId}`, + ...opts, + }); + }, + getWorkflowsOverview() { + return this._makeRequest({ + method: "GET", + path: "/controller/overview", + }); }, }, }; diff --git a/components/kadoa/package.json b/components/kadoa/package.json index 2841b219a1902..c1cf86b3fc480 100644 --- a/components/kadoa/package.json +++ b/components/kadoa/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/kadoa", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Kadoa Components", "main": "kadoa.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" } -} \ No newline at end of file +} + diff --git a/components/kadoa/sources/new-workflow-finished-instant/new-workflow-finished-instant.mjs b/components/kadoa/sources/new-workflow-finished-instant/new-workflow-finished-instant.mjs new file mode 100644 index 0000000000000..7615509c22ec0 --- /dev/null +++ b/components/kadoa/sources/new-workflow-finished-instant/new-workflow-finished-instant.mjs @@ -0,0 +1,51 @@ +import kadoa from "../../kadoa.app.mjs"; + +export default { + key: "kadoa-new-workflow-finished-instant", + name: "New Workflow Finished (Instant)", + description: "Emit new event when a Kadoa workflow finishes.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + kadoa, + db: "$.service.db", + http: "$.interface.http", + }, + hooks: { + async activate() { + const { id } = await this.kadoa.createWebhook({ + data: { + webhookUrl: this.http.endpoint, + webhookHttpMethod: "GET", + events: [ + "workflow_finished", + ], + }, + }); + this._setHookId(id); + }, + async deactivate() { + const hookId = this._getHookId(); + if (hookId) { + await this.kadoa.deleteWebhook(hookId); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + }, + async run(event) { + const { body } = event; + this.$emit(body, { + id: body.jobId, + summary: `Workflow ${body.workflowId} run finished`, + ts: Date.parse(body.finishedAt) || +new Date(), + }); + }, +}; diff --git a/components/kafka/kafka.app.mjs b/components/kafka/kafka.app.mjs new file mode 100644 index 0000000000000..069e2bee904ce --- /dev/null +++ b/components/kafka/kafka.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "kafka", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/kafka/package.json b/components/kafka/package.json new file mode 100644 index 0000000000000..078352ebf5c85 --- /dev/null +++ b/components/kafka/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/kafka", + "version": "0.0.1", + "description": "Pipedream Kafka Components", + "main": "kafka.app.mjs", + "keywords": [ + "pipedream", + "kafka" + ], + "homepage": "https://pipedream.com/apps/kafka", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/kaggle/README.md b/components/kaggle/README.md index 76f71018580b0..11c45cad4392e 100644 --- a/components/kaggle/README.md +++ b/components/kaggle/README.md @@ -1,11 +1,11 @@ # Overview -With the Kaggle API, you can: - -- Download datasets -- Upload datasets -- Create new datasets -- Join competitions -- View competition leaderboards -- View user profiles -- And more! +The Kaggle API allows you to harness a trove of data science and machine learning resources. With this API, you can download datasets, make competition submissions, and interact with Kaggle forums directly. By leveraging Pipedream's capabilities, you can automate repetitive tasks, integrate Kaggle with other applications, and create custom workflows to augment your data science projects. + +# Example Use Cases + +- **Automated Dataset Download and ETL**: Set up a workflow on Pipedream that triggers at scheduled intervals to download the latest version of a Kaggle dataset. Once the dataset is downloaded, the workflow can clean and transform the data before loading it into a database like PostgreSQL, enabling continuous data refreshes for your analytics. + +- **Competition Submission Reminder**: Create a workflow that monitors Kaggle for new competitions and sends notifications via Slack or email. This keeps you informed about new challenges without having to manually check the Kaggle site, ensuring that you never miss an opportunity to compete. + +- **Forum Interaction Digest**: Implement a workflow that collects the latest posts from specific Kaggle forums and compiles them into a daily or weekly digest. This digest can then be sent to your preferred communication app, such as Discord or Microsoft Teams, keeping you up to date with the community without manual browsing. diff --git a/components/kaggle/package.json b/components/kaggle/package.json new file mode 100644 index 0000000000000..236b893587274 --- /dev/null +++ b/components/kaggle/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/kaggle", + "version": "0.6.0", + "description": "Pipedream kaggle Components", + "main": "kaggle.app.mjs", + "keywords": [ + "pipedream", + "kaggle" + ], + "homepage": "https://pipedream.com/apps/kaggle", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/kajabi/README.md b/components/kajabi/README.md new file mode 100644 index 0000000000000..fc6af000c4b44 --- /dev/null +++ b/components/kajabi/README.md @@ -0,0 +1,11 @@ +# Overview + +Kajabi is a platform that allows creators to build and manage online courses, membership sites, and other digital products. The Kajabi API enables developers to programmatically interact with their Kajabi account, allowing for the automation of tasks such as managing users, products, offers, and more. By leveraging the Kajabi API on Pipedream, you can create powerful workflows that connect Kajabi to other apps and services, automate repetitive tasks, and streamline your digital content business. + +# Example Use Cases + +- **Automate New Member Onboarding**: Trigger an email sequence in Mailchimp whenever a new member signs up for your course or membership site on Kajabi. This workflow can include personalized welcome emails, instructions on how to get started, and follow-up messages to keep engagement high. + +- **Sync Kajabi with a CRM**: Keep your CRM, like Salesforce, updated in real-time by creating a workflow that adds or updates contacts whenever someone purchases a product or signs up for an offer on Kajabi. This ensures your sales team has the latest information for outreach and customer support. + +- **Aggregate Course Metrics**: Pull data from Kajabi on course completions and quiz scores, then send this data to a Google Sheet for analysis. This automation can help you track student progress, identify popular courses, and find opportunities for improvement within your content. diff --git a/components/kajabi/package.json b/components/kajabi/package.json index 6cc0e03f5429e..113f31de82f2b 100644 --- a/components/kajabi/package.json +++ b/components/kajabi/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/kakao/README.md b/components/kakao/README.md new file mode 100644 index 0000000000000..22b30022a5a9e --- /dev/null +++ b/components/kakao/README.md @@ -0,0 +1,12 @@ +# Overview + +The Kakao API offers a variety of functionalities that tie into Kakao's diverse platform services, such as Kakao Talk, Kakao Story, and Kakao Games. With this API integrated into Pipedream, you can automate personalized messaging campaigns, sync user data across platforms, and even implement social login features for your apps. Pipedream's serverless platform allows you to connect Kakao with hundreds of other apps to create customized workflows that trigger actions across multiple services seamlessly. + +# Example Use Cases + +- **Automated Customer Support Messages**: Trigger a Pipedream workflow that sends automated support messages or updates to users on Kakao Talk when a customer submits a query or issue through your helpdesk software like Zendesk. + +- **Social Media Content Sync**: Whenever you post new content on your blog or website, use a Pipedream workflow to share that content automatically on Kakao Story, broadening your reach and engaging with your audience on multiple platforms. + +- **User Authentication and Data Collection**: Leverage Pipedream's ability to handle webhook triggers to implement Kakao's social login on your app. Once a user logs in with Kakao, capture their data and store it in a secure database like Google Sheets for further analysis or personalized marketing. +. diff --git a/components/kaleido/README.md b/components/kaleido/README.md new file mode 100644 index 0000000000000..b088476b3087b --- /dev/null +++ b/components/kaleido/README.md @@ -0,0 +1,11 @@ +# Overview + +Kaleido provides an API for creating and managing blockchain networks and services. By integrating with Pipedream, you can automate various blockchain tasks such as deploying smart contracts, managing consortia, and handling tokens without managing infrastructure. Pipedream's serverless platform enables you to connect Kaleido with hundreds of other apps to create powerful and scalable workflows. + +# Example Use Cases + +- **Automated Smart Contract Deployment**: Deploy smart contracts to your blockchain network whenever a GitHub repository updates. The workflow triggers on a new commit, pulls the contract code, and uses Kaleido's API to deploy it, ensuring your network always runs the latest contract code. + +- **Consortium Management Notifications**: Send Slack notifications when there's a change in your blockchain consortium. This workflow listens for events from Kaleido's API, such as a new member joining, and sends a message to your team's Slack channel to keep everyone informed in real-time. + +- **Token Supply Monitoring**: Monitor and log the supply of your custom tokens by connecting Kaleido to Google Sheets. On a scheduled basis, this workflow retrieves the token balance from Kaleido, then appends the data to a Google Sheet for easy tracking and analysis. diff --git a/components/kaleido/actions/create-contract/create-contract.mjs b/components/kaleido/actions/create-contract/create-contract.mjs new file mode 100644 index 0000000000000..4bdd542295356 --- /dev/null +++ b/components/kaleido/actions/create-contract/create-contract.mjs @@ -0,0 +1,61 @@ +import app from "../../kaleido.app.mjs"; + +export default { + key: "kaleido-create-contract", + name: "Create Contract", + description: "Create a contract in Kaleido. [See the documentation](https://api.kaleido.io/platform.html#tag/Contracts/paths/~1consortia~1%7Bconsortia_id%7D~1contracts/post)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "name", + ], + }, + description: { + propDefinition: [ + app, + "description", + ], + }, + type: { + propDefinition: [ + app, + "type", + ], + }, + consortiaId: { + propDefinition: [ + app, + "consortiaId", + ], + }, + membershipId: { + propDefinition: [ + app, + "membershipId", + (c) => ({ + consortiaId: c.consortiaId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.app.createContract({ + $, + consortiaId: this.consortiaId, + data: { + name: this.name, + description: this.description, + type: this.type, + membership_id: this.membershipId, + }, + }); + + $.export("$summary", `Successfully created contract with ID '${response._id}'`); + + return response; + }, +}; diff --git a/components/kaleido/actions/create-membership/create-membership.mjs b/components/kaleido/actions/create-membership/create-membership.mjs new file mode 100644 index 0000000000000..4ee16b838fbfb --- /dev/null +++ b/components/kaleido/actions/create-membership/create-membership.mjs @@ -0,0 +1,60 @@ +import app from "../../kaleido.app.mjs"; + +export default { + key: "kaleido-create-membership", + name: "Create Membership", + description: "Create a membership for a consortia in Kaleido. [See the documentation](https://api.kaleido.io/platform.html#tag/Memberships/paths/~1consortia~1{consortia_id}~1memberships/post)", + version: "0.0.1", + type: "action", + props: { + app, + consortiaId: { + propDefinition: [ + app, + "consortiaId", + ], + }, + manageEnvs: { + propDefinition: [ + app, + "manageEnvs", + ], + }, + inviteOrgs: { + propDefinition: [ + app, + "inviteOrgs", + ], + }, + createSigners: { + propDefinition: [ + app, + "createSigners", + ], + }, + multipleMembers: { + propDefinition: [ + app, + "multipleMembers", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createMembership({ + $, + consortiaId: this.consortiaId, + data: { + permissions: { + manage_envs: this.manageEnvs, + invite_orgs: this.inviteOrgs, + create_signers: this.createSigners, + multiple_members: this.multipleMembers, + }, + }, + }); + + $.export("$summary", `Successfully created membership with ID '${response._id}'`); + + return response; + }, +}; diff --git a/components/kaleido/actions/delete-contract/delete-contract.mjs b/components/kaleido/actions/delete-contract/delete-contract.mjs new file mode 100644 index 0000000000000..66819903fbb4c --- /dev/null +++ b/components/kaleido/actions/delete-contract/delete-contract.mjs @@ -0,0 +1,38 @@ +import app from "../../kaleido.app.mjs"; + +export default { + key: "kaleido-delete-contract", + name: "Delete Contract", + description: "Delete a contract in Kaleido. [See the documentation](https://api.kaleido.io/platform.html#tag/Contracts/paths/~1consortia~1%7Bconsortia_id%7D~1contracts~1%7Bcontract_id%7D/delete)", + version: "0.0.1", + type: "action", + props: { + app, + consortiaId: { + propDefinition: [ + app, + "consortiaId", + ], + }, + contractId: { + propDefinition: [ + app, + "contractId", + (c) => ({ + consortiaId: c.consortiaId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.app.deleteContract({ + $, + consortiaId: this.consortiaId, + contractId: this.contractId, + }); + + $.export("$summary", `Successfully deleted contract with ID '${this.contractId}'`); + + return response; + }, +}; diff --git a/components/kaleido/common/constants.mjs b/components/kaleido/common/constants.mjs new file mode 100644 index 0000000000000..efa1f2a7dccf9 --- /dev/null +++ b/components/kaleido/common/constants.mjs @@ -0,0 +1,9 @@ +export default { + TYPES: [ + "github", + "precompiled", + "corda_jar", + "fabric_precompiled_go", + "fabric_upload_node", + ], +}; diff --git a/components/kaleido/kaleido.app.mjs b/components/kaleido/kaleido.app.mjs index a2e1bd66f959e..400110e521963 100644 --- a/components/kaleido/kaleido.app.mjs +++ b/components/kaleido/kaleido.app.mjs @@ -1,11 +1,163 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "kaleido", - propDefinitions: {}, + propDefinitions: { + name: { + type: "string", + label: "Name", + description: "Name of the contract project (must be unique across all contracts in the consortium)", + }, + description: { + type: "string", + label: "Description", + description: "Description of the contract", + optional: true, + }, + type: { + type: "string", + label: "Type", + description: "The type of contract project being created", + options: constants.TYPES, + }, + manageEnvs: { + type: "boolean", + label: "Manage Enviroments", + description: "Determines whether the member can create and upgrade environments", + }, + inviteOrgs: { + type: "boolean", + label: "Invite Organizaions", + description: "Determines whether the member can invite other orgs to the consortium", + }, + createSigners: { + type: "boolean", + label: "Create Signers", + description: "Determines whether the member can create signing nodes in environments", + }, + multipleMembers: { + type: "boolean", + label: "Multiple Members", + description: "Determines whether the member can create additional membership for themselves", + }, + consortiaId: { + type: "string", + label: "Consortia ID", + description: "ID of the Consortia", + async options() { + const consoriaIds = await this.getConsortia(); + + return consoriaIds.map(({ + _id, name, + }) => ({ + value: _id, + label: name, + })); + }, + }, + contractId: { + type: "string", + label: "Contract ID", + description: "ID of the Contract", + async options({ consortiaId }) { + const contractsIds = await this.getContracts({ + consortiaId, + }); + + return contractsIds.map(({ + _id, name, + }) => ({ + value: _id, + label: name, + })); + }, + }, + membershipId: { + type: "string", + label: "Membership ID", + description: "Field denoting the membership which owns the Contract", + async options({ consortiaId }) { + const membershipIds = await this.getMemberships({ + consortiaId, + }); + + return membershipIds.map(({ _id }) => ({ + value: _id, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `https://${this.$auth.endpoint}.kaleido.io/api/v1`; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "Content-Type": "application/json", + "Authorization": `Bearer ${this.$auth.api_keys}`, + }, + }); + }, + async createContract({ + consortiaId, ...args + }) { + return this._makeRequest({ + method: "post", + path: `/consortia/${consortiaId}/contracts`, + ...args, + }); + }, + async createMembership({ + consortiaId, ...args + }) { + return this._makeRequest({ + method: "post", + path: `/consortia/${consortiaId}/memberships`, + ...args, + }); + }, + async deleteContract({ + consortiaId, contractId, ...args + }) { + return this._makeRequest({ + method: "delete", + path: `/consortia/${consortiaId}/contracts/${contractId}`, + ...args, + }); + }, + async getConsortia(args = {}) { + return this._makeRequest({ + path: "/consortia", + ...args, + }); + }, + async getContracts({ + consortiaId: consortiaId, ...args + }) { + return this._makeRequest({ + path: `/consortia/${consortiaId}/contracts`, + ...args, + }); + }, + async getMemberships({ + consortiaId: consortiaId, ...args + }) { + return this._makeRequest({ + path: `/consortia/${consortiaId}/memberships`, + ...args, + }); }, }, }; diff --git a/components/kaleido/package.json b/components/kaleido/package.json index 3586945b9b635..fe28678801b8f 100644 --- a/components/kaleido/package.json +++ b/components/kaleido/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/kaleido", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Kaleido Components", "main": "kaleido.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" } -} \ No newline at end of file +} diff --git a/components/kanban_tool/README.md b/components/kanban_tool/README.md index 95b6f7bf5bdc6..815a8f90fbb8c 100644 --- a/components/kanban_tool/README.md +++ b/components/kanban_tool/README.md @@ -1,11 +1,11 @@ # Overview -I can use the Kanban Tool API to: - -- Create newboards -- Add and remove tasks from boards -- Assign tasks to members -- Create new members -- Move tasks between columns -- Edit task details -- And much more! +The Kanban Tool API allows for the seamless integration and manipulation of Kanban boards, tasks, and workflows to optimize project management and team collaboration. By leveraging this API on Pipedream, you can automate task updates, synchronize boards with other data sources, and create custom notifications—thus enhancing productivity and maintaining momentum across projects. + +# Example Use Cases + +- **Task Synchronization Between Kanban Tool and Google Sheets**: Automate the process of syncing tasks from Kanban Tool to a Google Sheet for reporting purposes. When a task status updates to "complete," trigger a Pipedream workflow to append a new row with task details in Google Sheets, maintaining an up-to-date record of project progress. + +- **Slack Notifications for New Kanban Tool Cards**: Set up a workflow that notifies a designated Slack channel whenever a new card is created in Kanban Tool. Use this to keep the entire team informed of new tasks and ensure immediate action on high-priority items, fostering a responsive and agile work environment. + +- **GitHub Issues to Kanban Tool Cards Conversion**: Whenever a new issue is reported on GitHub, automatically create a corresponding card in the appropriate Kanban Tool board. Link the GitHub issue to the card and assign it to the relevant team member, streamlining the bug tracking and feature request process. diff --git a/components/kanbanflow/README.md b/components/kanbanflow/README.md index 7f1ba04803693..b1a5dbf71e4ae 100644 --- a/components/kanbanflow/README.md +++ b/components/kanbanflow/README.md @@ -1,9 +1,11 @@ # Overview -KanbanFlow provides a simple, yet powerful API that lets you build a variety of -applications on top of it. Below are some examples of what you can build: +The KanbanFlow API unlocks the potential for creating customized, automated workflows to enhance productivity and streamline task management. By tapping into this API via Pipedream, developers can craft serverless functions that interact with KanbanFlow boards, tasks, users, and time tracking features. These automations can range from syncing tasks across platforms to triggering notifications based on task updates. -- A simple to-do list application -- A project management application -- A task tracking application -- A kanban board application +# Example Use Cases + +- **Sync KanbanFlow Tasks with Google Calendar**: Automate the creation of Google Calendar events when new tasks are added to a KanbanFlow board. Use this to ensure deadlines are reflected across your scheduling tools, making sure nothing falls through the cracks. + +- **Task Status Updates to Slack**: Send real-time notifications to a designated Slack channel when tasks change status on a KanbanFlow board. Keep your team in the loop with updates as tasks move from 'In Progress' to 'Done'. + +- **Automated Backup of KanbanFlow Boards**: Regularly save snapshots of your KanbanFlow boards to Dropbox for backup and compliance purposes. Set up a cron-triggered Pipedream workflow to export board data and save it securely to Dropbox at defined intervals. diff --git a/components/kanbanize/README.md b/components/kanbanize/README.md index 504feee9ac43a..7b38bf9aef2aa 100644 --- a/components/kanbanize/README.md +++ b/components/kanbanize/README.md @@ -1,9 +1,11 @@ # Overview -Kanbanize is a tool that helps you visualized and manage your work. With -Kanbanize, you can build a variety of things, including: +The Kanbanize API enables the automation of complex project management workflows within Kanban boards. By leveraging the API, you can create, update, and move cards, manage board configurations, and integrate real-time updates with other tools. It's a powerful way to streamline project tracking, automate task assignments, and synchronize data across various project management platforms. -- A board to help you manage your work -- A workspace to help you collaborate with others -- A system to help you track your work -- A tool to help you prioritize your work +# Example Use Cases + +- **Automated Task Assignment**: When a new card reaches a specific column on the Kanbanize board, use Pipedream to automatically assign the task to a team member based on availability or expertise. Notify the assignee via Slack or email. + +- **Project Update Synchronizer**: Sync status updates across multiple project management tools. When a card's status changes in Kanbanize, Pipedream can update the corresponding task in tools like Jira, Asana, or Trello, ensuring all platforms reflect the most current information. + +- **Time Tracking Integration**: Implement time tracking by triggering a time entry in a service like Toggl or Harvest whenever a card moves to a 'In Progress' column on Kanbanize. Conversely, stop the tracker when the card moves to the 'Done' column. diff --git a/components/karbon/README.md b/components/karbon/README.md index c55d737d44cfe..bed2cecba6837 100644 --- a/components/karbon/README.md +++ b/components/karbon/README.md @@ -1,11 +1,11 @@ # Overview -Using the Karbon API, you can build a variety of applications and integrations, -including: - -- A custom Karbon dashboard to track your team's progress -- An integration with your project management tool to automatically create - tasks in Karbon -- A "Bot" to automatically assign tasks to team members based on their skills - and availability -- A custom tool to help you onboard new team members +Karbon is a work management platform designed for accounting firms, centralizing team communication, workflows, and client interactions. With Karbon's API, you can automate routine operations, sync data across multiple systems, and create custom monitoring solutions tailored for your firm's workflow. By leveraging Pipedream's capabilities, you can harness the power of Karbon's API to create dynamic, serverless workflows that connect Karbon with other apps to streamline your accounting practices. + +# Example Use Cases + +- **Task Automation from Email Requests**: Trigger a Pipedream workflow when an email is received in a specific mailbox. The workflow can parse the email for details, create a new task in Karbon, and assign it to the relevant team member. This workflow simplifies task creation, ensuring that client requests are promptly converted into actionable items. + +- **Contact Sync with CRM**: Maintain synchronized contact information between Karbon and a CRM like Salesforce. Whenever a contact is updated in Karbon, the workflow triggers, updating the corresponding entry in Salesforce. This ensures that all team members have access to the most up-to-date client information across all platforms. + +- **Daily Workload Digest**: Generate a daily digest of tasks and priorities for each team member using the Karbon API. The workflow aggregates the day's tasks from Karbon, formats them into a readable report, and sends it via Slack or email. This helps team members start their day with a clear picture of their priorities. diff --git a/components/kartra/README.md b/components/kartra/README.md new file mode 100644 index 0000000000000..19677d534231d --- /dev/null +++ b/components/kartra/README.md @@ -0,0 +1,11 @@ +# Overview + +The Kartra API allows you to interact programmatically with Kartra's marketing automation platform. Through Pipedream, you can leverage this API to create, update, retrieve, and delete various marketing and sales assets managed by Kartra, such as leads, emails, memberships, and more. By integrating Kartra with other apps on Pipedream, you can automate complex workflows, sync data across multiple platforms, and trigger actions based on events within Kartra or external services. + +# Example Use Cases + +- **Synchronize Kartra Leads with a CRM**: Automatically export new leads from Kartra to a CRM like Salesforce or HubSpot. When a new lead is captured in Kartra, the workflow triggers, sending lead data to your CRM, ensuring your sales team has the latest information at their fingertips. + +- **Automate Customer Onboarding**: Once a new user purchases a membership in Kartra, trigger a Pipedream workflow to enroll them in an onboarding course in an external Learning Management System (LMS) like Teachable. Additionally, send a personalized welcome email from a service like SendGrid to the new member. + +- **Dynamic Email Campaigns Based on User Activity**: Monitor user interactions on your website or app (tracked by Kartra), and use this data to trigger targeted email campaigns. For example, if a user views a specific product but doesn't purchase, Pipedream can initiate a follow-up email sequence via an email marketing service like Mailchimp to encourage conversion. diff --git a/components/keen/README.md b/components/keen/README.md index e8f3ed170c9dc..546f872a917ec 100644 --- a/components/keen/README.md +++ b/components/keen/README.md @@ -1,12 +1,11 @@ # Overview -The Keen API lets you easily build custom analytics tools and applications. -With Keen, you can collect data from anywhere, analyze it in minutes, and -visualize it in beautiful charts and dashboards. +Keen API empowers developers to capture, analyze, and embed event data into their applications, creating opportunities for deep analytics and custom visualization capabilities. With Keen, you can stream, store, and query large volumes of events, enabling real-time insights into user behavior, system performance, or any other metric that can be captured via events. On Pipedream, integrating Keen allows you to automate workflows by capturing these events and triggering actions in other apps, based on the data you gather. -Some examples of what you can build with the Keen API: +# Example Use Cases -- A custom analytics tool to track your website's traffic -- A mobile app to track your fitness data -- A dashboard to monitor your company's sales data -- A tool to visualize your Twitter analytics +- **Real-Time Alerting System**: Connect Keen to communication platforms like Slack or Twilio on Pipedream. Monitor your application's performance metrics in Keen, and set up a workflow that sends immediate notifications to your team's Slack channel or sends SMS alerts when performance anomalies are detected. + +- **Dynamic User Segmentation**: Integrate Keen with a CRM like Salesforce using Pipedream. Analyze user activities and behaviors through Keen, and automatically segment users in your CRM based on their event patterns. For example, identifying highly engaged users and flagging them in Salesforce for targeted marketing campaigns. + +- **Customer Success Tracking**: Use Keen with Pipedream to funnel event data into a tool like Google Sheets or Airtable. Track customer milestones or feature usage, and create a system that updates a spreadsheet or database in real time. This can help customer success teams to proactively address user needs and improve retention. diff --git a/components/keen/package.json b/components/keen/package.json new file mode 100644 index 0000000000000..7eadbf4ed9e2f --- /dev/null +++ b/components/keen/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/keen", + "version": "0.6.0", + "description": "Pipedream keen Components", + "main": "keen.app.mjs", + "keywords": [ + "pipedream", + "keen" + ], + "homepage": "https://pipedream.com/apps/keen", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/keen_io/README.md b/components/keen_io/README.md new file mode 100644 index 0000000000000..92bd988f1e65f --- /dev/null +++ b/components/keen_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The Keen.io API provides robust analytics and data collection capabilities. On Pipedream, you can harness this power to create custom event tracking and analysis workflows, automate reporting, and drive data-informed decisions. With Keen.io's ability to capture, analyze, and embed event data, you can set up workflows on Pipedream that react to data streams in real-time, aggregate metrics, or trigger actions in other apps based on insights. + +# Example Use Cases + +- **Real-time Event Tracking and Alerting**: Monitor your application's events through Keen.io and set up a Pipedream workflow that sends real-time alerts via Slack or email when specific event thresholds are reached or anomalies are detected, keeping your team informed and responsive. + +- **Dynamic Dashboard Updates**: Use Keen.io to feed analytics data into a Pipedream workflow that periodically updates a Google Sheets dashboard. This workflow can parse and transform data before updating the dashboard, ensuring stakeholders always have access to the latest insights. + +- **Customer Behavior Analysis with CRM Integration**: Analyze customer interaction events in Keen.io and enrich this data in a Pipedream workflow by integrating with a CRM platform like Salesforce. This provides a comprehensive view of customer engagement and helps personalize marketing and support efforts. diff --git a/components/kenjo/actions/create-attendance-entry/create-attendance-entry.mjs b/components/kenjo/actions/create-attendance-entry/create-attendance-entry.mjs new file mode 100644 index 0000000000000..3d646afcfa717 --- /dev/null +++ b/components/kenjo/actions/create-attendance-entry/create-attendance-entry.mjs @@ -0,0 +1,77 @@ +import kenjo from "../../kenjo.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "kenjo-create-attendance-entry", + name: "Create Attendance Entry", + description: "Creates a new attendance entry for an employee in Kenjo. [See the documentation](https://kenjo.readme.io/reference/post_attendances)", + version: "0.0.1", + type: "action", + props: { + kenjo, + employeeId: { + propDefinition: [ + kenjo, + "employeeId", + ], + }, + date: { + type: "string", + label: "Date", + description: "The date of the entry. Format: `YYYY-MM-DD`", + }, + startTime: { + type: "string", + label: "Start Time", + description: "The start time of the entry. Format: `hh:mm:ss`", + }, + endTime: { + type: "string", + label: "End Time", + description: "The end time of the entry. Format: `hh:mm:ss`", + optional: true, + }, + breakStartTime: { + type: "string", + label: "Break Start Time", + description: "The start time of the break. Format: `hh:mm:ss`", + optional: true, + }, + breakEndTime: { + type: "string", + label: "Break End Time", + description: "The end time of the break. Format: `hh:mm:ss`", + optional: true, + }, + comment: { + type: "string", + label: "Comment", + description: "Comment to describe the attendance record", + optional: true, + }, + }, + async run({ $ }) { + if (this.breakEndTime && !this.breakStartTime) { + throw new ConfigurationError("Break Start Time is required if including a break"); + } + + const response = await this.kenjo.createAttendanceEntry({ + $, + data: { + userId: this.employeeId, + date: this.date, + startTime: this.startTime, + endTime: this.endTime, + breaks: this.breakStartTime && [ + { + start: this.breakStartTime, + end: this.breakEndTime, + }, + ], + comment: this.comment, + }, + }); + $.export("$summary", `Successfully created attendance entry with ID: ${response._id}`); + return response; + }, +}; diff --git a/components/kenjo/actions/create-employee/create-employee.mjs b/components/kenjo/actions/create-employee/create-employee.mjs new file mode 100644 index 0000000000000..316a46fe919b4 --- /dev/null +++ b/components/kenjo/actions/create-employee/create-employee.mjs @@ -0,0 +1,178 @@ +import kenjo from "../../kenjo.app.mjs"; + +export default { + key: "kenjo-create-employee", + name: "Create Employee", + description: "Creates a new employee in Kenjo. [See the documentation](https://kenjo.readme.io/reference/post_employees)", + version: "0.0.1", + type: "action", + props: { + kenjo, + email: { + type: "string", + label: "Email", + description: "The email address of the employee", + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the employee", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the employee", + }, + companyId: { + propDefinition: [ + kenjo, + "companyId", + ], + }, + weeklyHours: { + type: "integer", + label: "Weekly Hours", + description: "The number of weekly hours that an employee works", + }, + officeId: { + propDefinition: [ + kenjo, + "officeId", + (c) => ({ + companyId: c.companyId, + }), + ], + }, + departmentId: { + propDefinition: [ + kenjo, + "departmentId", + ], + }, + language: { + type: "string", + label: "Language", + description: "The employee's language", + options: [ + "en", + "es", + "de", + ], + optional: true, + }, + birthdate: { + type: "string", + label: "Birthdate", + description: "The birthdate of the employee. Format `YYYY-MM-DDThh:mm:ss.000Z`", + optional: true, + }, + jobTitle: { + type: "string", + label: "Job Title", + description: "The job title of the employee", + optional: true, + }, + workPhone: { + type: "string", + label: "Work Phone", + description: "The work phone number of the employee", + optional: true, + }, + personalPhone: { + type: "string", + label: "Personal Phone", + description: "The personal phone number of the employee", + optional: true, + }, + workDays: { + type: "string[]", + label: "Work Days", + description: "The days of the week that the employee works", + options: [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + ], + optional: true, + }, + trackAttendance: { + type: "boolean", + label: "Track Attendance", + description: "Set to `true` to activate attendance tracking for the employee", + optional: true, + }, + street: { + type: "string", + label: "Street", + description: "The street address of the employee", + optional: true, + }, + postalCode: { + type: "string", + label: "Postal Code", + description: "The postal code of the employee", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "The city of the employee", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "The country code in ISO 3166-1 alpha-2. Example: `ES`", + optional: true, + }, + }, + async run({ $ }) { + const workSchedule = { + trackAttendance: this.trackAttendance, + }; + if (this.workDays?.length) { + for (const day of this.workDays) { + workSchedule[`${day}WorkingDay`] = true; + } + } + + const response = await this.kenjo.createEmployee({ + $, + data: { + account: { + email: this.email, + language: this.language, + }, + personal: { + firstName: this.firstName, + lastName: this.lastName, + birthdate: this.birthdate, + }, + work: { + companyId: this.companyId, + officeId: this.officeId, + departmentId: this.departmentId, + weeklyHours: this.weeklyHours, + jobTitle: this.jobTitle, + workPhone: this.workPhone, + }, + workSchedule, + address: (this.street || this.postalCode || this.city || this.country) && { + street: this.street, + postalCode: this.postalCode, + city: this.city, + country: this.country, + }, + home: this.personalPhone && { + personalPhone: this.personalPhone, + }, + }, + }); + $.export("$summary", `Created employee with ID: ${response.account._id}`); + return response; + }, +}; diff --git a/components/kenjo/actions/create-leave-request/create-leave-request.mjs b/components/kenjo/actions/create-leave-request/create-leave-request.mjs new file mode 100644 index 0000000000000..e6e6bdb7ac43a --- /dev/null +++ b/components/kenjo/actions/create-leave-request/create-leave-request.mjs @@ -0,0 +1,76 @@ +import kenjo from "../../kenjo.app.mjs"; + +export default { + key: "kenjo-create-leave-request", + name: "Create Leave Request", + description: "Creates a new leave request in Kenjo. [See the documentation](https://kenjo.readme.io/reference/post_time-off-requests).", + version: "0.0.1", + type: "action", + props: { + kenjo, + employeeId: { + propDefinition: [ + kenjo, + "employeeId", + ], + }, + timeOffTypeId: { + propDefinition: [ + kenjo, + "timeOffTypeId", + ], + }, + from: { + type: "string", + label: "From", + description: "The starting date of the time-off request in format `YYYY-MM-DD`", + }, + to: { + type: "string", + label: "To", + description: "The ending date of the time-off request in format `YYYY-MM-DD`", + }, + partOfDayFrom: { + type: "string", + label: "Part of Day From", + description: "The duration of the from date. 'StartOfDay' means that the from date is the entire day. 'HalfOfDay' means that the request starts to apply in the middle of the from day. If not specified, the default value will be 'StartOfDay'.", + options: [ + "StartOfDay", + "HalfOfDay", + ], + optional: true, + }, + partOfDayTo: { + type: "string", + label: "Part of Day To", + description: "The duration of the to date. 'EndOfDay' means that the to date is the entire day. 'HalfOfDay' means the request starts to apply in the middle of the to day. If not specified, the default value will be 'EndOfDay'.", + options: [ + "HalfOfDay", + "EndOfDay", + ], + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "The description of the time-off request", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.kenjo.createLeaveRequest({ + $, + data: { + _userId: this.employeeId, + _timeOffTypeId: this.timeOffTypeId, + from: this.from, + to: this.to, + partOfDayFrom: this.partOfDayFrom, + partOfDayTo: this.partOfDayTo, + description: this.description, + }, + }); + $.export("$summary", `Successfully created leave request with ID: ${response._id}`); + return response; + }, +}; diff --git a/components/kenjo/kenjo.app.mjs b/components/kenjo/kenjo.app.mjs new file mode 100644 index 0000000000000..ba9af1996d928 --- /dev/null +++ b/components/kenjo/kenjo.app.mjs @@ -0,0 +1,154 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "kenjo", + propDefinitions: { + employeeId: { + type: "string", + label: "Employee ID", + description: "The identifier of an employee", + async options() { + const { data } = await this.listEmployees(); + return data?.filter(({ isActive }) => isActive)?.map(({ + _id: value, email: label, + }) => ({ + value, + label, + })); + }, + }, + companyId: { + type: "string", + label: "Company ID", + description: "The identifier of a company", + async options() { + const companies = await this.listCompanies(); + return companies?.map(({ + _id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + timeOffTypeId: { + type: "string", + label: "Time Off Type ID", + description: "The identifier of a time off request type", + async options() { + const { data } = await this.listTimeOffTypes(); + return data?.map(({ + _id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + officeId: { + type: "string", + label: "Office ID", + description: "The identifier of an office", + optional: true, + async options({ companyId }) { + const offices = await this.listOffices({ + params: { + companyId, + }, + }); + return offices?.map(({ + _id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + departmentId: { + type: "string", + label: "Department ID", + description: "The identifier of a department", + optional: true, + async options() { + const departments = await this.listDepartments(); + return departments?.map(({ + _id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return this.$auth.api_url; + }, + _makeRequest({ + $ = this, + path, + ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `${this.$auth.oauth_access_token}`, + Accept: "application/json", + }, + }); + }, + listCompanies(opts = {}) { + return this._makeRequest({ + path: "/companies", + ...opts, + }); + }, + listOffices(opts = {}) { + return this._makeRequest({ + path: "/offices", + ...opts, + }); + }, + listDepartments(opts = {}) { + return this._makeRequest({ + path: "/departments", + ...opts, + }); + }, + listEmployees(opts = {}) { + return this._makeRequest({ + path: "/employees", + ...opts, + }); + }, + listTimeOffTypes(opts = {}) { + return this._makeRequest({ + path: "/time-off/types", + ...opts, + }); + }, + createEmployee(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/employees", + ...opts, + }); + }, + createLeaveRequest(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/time-off/requests", + ...opts, + }); + }, + createAttendanceEntry(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/attendances", + ...opts, + }); + }, + }, +}; diff --git a/components/kenjo/package.json b/components/kenjo/package.json new file mode 100644 index 0000000000000..fe2b6b1d667c5 --- /dev/null +++ b/components/kenjo/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/kenjo", + "version": "0.1.0", + "description": "Pipedream Kenjo Components", + "main": "kenjo.app.mjs", + "keywords": [ + "pipedream", + "kenjo" + ], + "homepage": "https://pipedream.com/apps/kenjo", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/kenjo/sources/common/base.mjs b/components/kenjo/sources/common/base.mjs new file mode 100644 index 0000000000000..2adcc01a56863 --- /dev/null +++ b/components/kenjo/sources/common/base.mjs @@ -0,0 +1,40 @@ +import kenjo from "../../kenjo.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + kenjo, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + getResourceKey() { + return; + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run() { + const resourceFn = this.getResourceFn(); + const resourceKey = this.getResourceKey(); + + const results = await resourceFn(); + const items = resourceKey + ? results[resourceKey] + : results; + + for (const item of items) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + } + }, +}; diff --git a/components/kenjo/sources/new-company-created/new-company-created.mjs b/components/kenjo/sources/new-company-created/new-company-created.mjs new file mode 100644 index 0000000000000..af14be69af8d4 --- /dev/null +++ b/components/kenjo/sources/new-company-created/new-company-created.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "kenjo-new-company-created", + name: "New Company Created", + description: "Emit new event when a new company is created in Kenjo. [See the documentation](https://kenjo.readme.io/reference/get_companies)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.kenjo.listCompanies; + }, + generateMeta(company) { + return { + id: company._id, + summary: `New Company: ${company.name}`, + ts: Date.now(), + }; + }, + }, +}; diff --git a/components/kenjo/sources/new-employee-created/new-employee-created.mjs b/components/kenjo/sources/new-employee-created/new-employee-created.mjs new file mode 100644 index 0000000000000..230c2b932a9dd --- /dev/null +++ b/components/kenjo/sources/new-employee-created/new-employee-created.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "kenjo-new-employee-created", + name: "New Employee Created", + description: "Emit new event when a new employee is added in Kenjo. [See the documentation](https://kenjo.readme.io/reference/get_employees)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.kenjo.listEmployees; + }, + getResourceKey() { + return "data"; + }, + generateMeta(employee) { + return { + id: employee._id, + summary: `New Employee: ${employee.email}`, + ts: Date.now(), + }; + }, + }, +}; diff --git a/components/keycloak/actions/create-user/create-user.mjs b/components/keycloak/actions/create-user/create-user.mjs new file mode 100644 index 0000000000000..333f1c4506285 --- /dev/null +++ b/components/keycloak/actions/create-user/create-user.mjs @@ -0,0 +1,84 @@ +/* eslint-disable no-unused-vars */ +import app from "../../keycloak.app.mjs"; + +export default { + key: "keycloak-create-user", + name: "Create User", + description: "Create a new user in Keycloak. The username must be unique. [See the documentation](https://www.keycloak.org/docs-api/latest/rest-api/index.html#_users)", + version: "0.0.1", + type: "action", + props: { + app, + realm: { + propDefinition: [ + app, + "realm", + ], + }, + username: { + type: "string", + label: "Username", + description: "The username of the user.", + }, + firstName: { + propDefinition: [ + app, + "firstName", + ], + }, + lastName: { + propDefinition: [ + app, + "lastName", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + emailVerified: { + propDefinition: [ + app, + "emailVerified", + ], + }, + enabled: { + propDefinition: [ + app, + "enabled", + ], + }, + }, + methods: { + createUser({ + realm, ...args + } = {}) { + return this.app.post({ + path: `/admin/realms/${realm}/users`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + app, + createUser, + realm, + ...data + } = this; + + await createUser({ + $, + realm, + data, + }); + + $.export("$summary", "Successfully created user."); + + return { + success: true, + }; + }, +}; diff --git a/components/keycloak/actions/delete-user/delete-user.mjs b/components/keycloak/actions/delete-user/delete-user.mjs new file mode 100644 index 0000000000000..aaec8bb4d0317 --- /dev/null +++ b/components/keycloak/actions/delete-user/delete-user.mjs @@ -0,0 +1,56 @@ +import app from "../../keycloak.app.mjs"; + +export default { + key: "keycloak-delete-user", + name: "Delete User", + description: "Delete a user from Keycloak. [See the documentation](https://www.keycloak.org/docs-api/latest/rest-api/index.html#_users)", + version: "0.0.1", + type: "action", + props: { + app, + realm: { + propDefinition: [ + app, + "realm", + ], + }, + userId: { + propDefinition: [ + app, + "userId", + ({ realm }) => ({ + realm, + }), + ], + }, + }, + methods: { + deleteUser({ + realm, userId, ...args + } = {}) { + return this.app.delete({ + path: `/admin/realms/${realm}/users/${userId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + deleteUser, + realm, + userId, + } = this; + + await deleteUser({ + $, + realm, + userId, + }); + + $.export("$summary", "Successfully deleted user."); + + return { + success: true, + }; + }, +}; diff --git a/components/keycloak/actions/get-user/get-user.mjs b/components/keycloak/actions/get-user/get-user.mjs new file mode 100644 index 0000000000000..6dc789f15c02d --- /dev/null +++ b/components/keycloak/actions/get-user/get-user.mjs @@ -0,0 +1,52 @@ +import app from "../../keycloak.app.mjs"; + +export default { + key: "keycloak-get-user", + name: "Get User", + description: "Retrieve the representation of the user. [See the documentation](https://www.keycloak.org/docs-api/latest/rest-api/index.html#_users)", + version: "0.0.1", + type: "action", + props: { + app, + realm: { + propDefinition: [ + app, + "realm", + ], + }, + userId: { + propDefinition: [ + app, + "userId", + ({ realm }) => ({ + realm, + }), + ], + }, + }, + methods: { + getUser({ + realm, userId, ...args + } = {}) { + return this.app._makeRequest({ + path: `/admin/realms/${realm}/users/${userId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + getUser, + realm, + userId, + } = this; + + const response = await getUser({ + $, + realm, + userId, + }); + $.export("$summary", `Successfully retrieved user with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/keycloak/actions/update-user/update-user.mjs b/components/keycloak/actions/update-user/update-user.mjs new file mode 100644 index 0000000000000..0c04491db6ebe --- /dev/null +++ b/components/keycloak/actions/update-user/update-user.mjs @@ -0,0 +1,90 @@ +/* eslint-disable no-unused-vars */ +import app from "../../keycloak.app.mjs"; + +export default { + key: "keycloak-update-user", + name: "Update User", + description: "Updates a user in Keycloak. [See the documentation](https://www.keycloak.org/docs-api/latest/rest-api/index.html#_users)", + version: "0.0.1", + type: "action", + props: { + app, + realm: { + propDefinition: [ + app, + "realm", + ], + }, + userId: { + propDefinition: [ + app, + "userId", + ({ realm }) => ({ + realm, + }), + ], + }, + firstName: { + propDefinition: [ + app, + "firstName", + ], + }, + lastName: { + propDefinition: [ + app, + "lastName", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + emailVerified: { + propDefinition: [ + app, + "emailVerified", + ], + }, + enabled: { + propDefinition: [ + app, + "enabled", + ], + }, + }, + methods: { + updateUser({ + realm, userId, ...args + } = {}) { + return this.app.put({ + path: `/admin/realms/${realm}/users/${userId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + app, + updateUser, + realm, + userId, + ...data + } = this; + + await updateUser({ + $, + realm, + userId, + data, + }); + + $.export("$summary", "Successfully updated user."); + + return { + success: true, + }; + }, +}; diff --git a/components/keycloak/common/constants.mjs b/components/keycloak/common/constants.mjs new file mode 100644 index 0000000000000..dcf1ec5244f28 --- /dev/null +++ b/components/keycloak/common/constants.mjs @@ -0,0 +1,9 @@ +const DEFAULT_LIMIT = 100; +const DEFAULT_MAX = 600; +const LAST_DATE_FROM = "dateFrom"; + +export default { + DEFAULT_LIMIT, + DEFAULT_MAX, + LAST_DATE_FROM, +}; diff --git a/components/keycloak/common/utils.mjs b/components/keycloak/common/utils.mjs new file mode 100644 index 0000000000000..903b2593ed3c2 --- /dev/null +++ b/components/keycloak/common/utils.mjs @@ -0,0 +1,11 @@ +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +export default { + iterate, +}; diff --git a/components/keycloak/keycloak.app.mjs b/components/keycloak/keycloak.app.mjs new file mode 100644 index 0000000000000..f855b8dce7221 --- /dev/null +++ b/components/keycloak/keycloak.app.mjs @@ -0,0 +1,181 @@ +import { axios } from "@pipedream/platform"; +import utils from "./common/utils.mjs"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "keycloak", + propDefinitions: { + realm: { + type: "string", + label: "Realm", + description: "The realm name (not id) to which the user belongs.", + async options() { + const realms = await this.listRealms({ + params: { + briefRepresentation: true, + }, + }); + return realms.map(({ realm }) => realm); + }, + }, + userId: { + type: "string", + label: "User ID", + description: "The unique identifier of the user.", + async options({ realm }) { + if (!realm) { + return []; + } + const users = await this.listUsers({ + realm, + params: { + briefRepresentation: true, + }, + }); + return users.map(({ + id: value, username: label, + }) => ({ + label, + value, + })); + }, + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the user.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the user.", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "The email of the user.", + optional: true, + }, + emailVerified: { + type: "boolean", + label: "Email Verified", + description: "Whether the user's email is verified.", + optional: true, + }, + enabled: { + type: "boolean", + label: "Enabled", + description: "Whether the user is enabled.", + optional: true, + }, + }, + methods: { + getUrl(path) { + return `${this.$auth.url}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Content-Type": "application/json", + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + put(args = {}) { + return this._makeRequest({ + method: "PUT", + ...args, + }); + }, + listRealms(args = {}) { + return this._makeRequest({ + path: "/admin/realms", + ...args, + }); + }, + listUsers({ + realm, ...args + } = {}) { + return this._makeRequest({ + path: `/admin/realms/${realm}/users`, + ...args, + }); + }, + listEvents({ + realm, ...args + } = {}) { + return this._makeRequest({ + debug: true, + path: `/admin/realms/${realm}/events`, + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, + max = constants.DEFAULT_MAX, + }) { + let first = 0; + let resourcesCount = 0; + + while (true) { + const nextResources = + await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + max: constants.DEFAULT_LIMIT, + first, + }, + }); + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + yield resource; + resourcesCount += 1; + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + if (nextResources.length < constants.DEFAULT_LIMIT) { + console.log("No next page found"); + return; + } + + first += constants.DEFAULT_LIMIT; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); + }, + }, +}; diff --git a/components/keycloak/package.json b/components/keycloak/package.json new file mode 100644 index 0000000000000..fd33b925c320e --- /dev/null +++ b/components/keycloak/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/keycloak", + "version": "0.1.0", + "description": "Pipedream Keycloak Components", + "main": "keycloak.app.mjs", + "keywords": [ + "pipedream", + "keycloak" + ], + "homepage": "https://pipedream.com/apps/keycloak", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} diff --git a/components/keycloak/sources/common/polling.mjs b/components/keycloak/sources/common/polling.mjs new file mode 100644 index 0000000000000..812724c80b19e --- /dev/null +++ b/components/keycloak/sources/common/polling.mjs @@ -0,0 +1,82 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../keycloak.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + realm: { + propDefinition: [ + app, + "realm", + ], + }, + }, + methods: { + setLastDateFrom(value) { + this.db.set(constants.LAST_DATE_FROM, value); + }, + getLastDateFrom() { + return this.db.get(constants.LAST_DATE_FROM); + }, + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + processResources(resources) { + const { + setLastDateFrom, + processResource, + } = this; + + const [ + lastResource, + ] = resources; + + if (lastResource?.time) { + const date = new Date(lastResource.time); + setLastDateFrom(date.toISOString().split("T")[0]); + } + + Array.from(resources) + .reverse() + .forEach(processResource); + }, + }, + async run() { + const { + app, + getResourcesFn, + getResourcesFnArgs, + processResources, + } = this; + + const resources = await app.paginate({ + resourcesFn: getResourcesFn(), + resourcesFnArgs: getResourcesFnArgs(), + }); + + processResources(resources); + }, +}; diff --git a/components/keycloak/sources/new-event-created/new-event-created.mjs b/components/keycloak/sources/new-event-created/new-event-created.mjs new file mode 100644 index 0000000000000..38b289f5007ae --- /dev/null +++ b/components/keycloak/sources/new-event-created/new-event-created.mjs @@ -0,0 +1,36 @@ +import common from "../common/polling.mjs"; + +export default { + ...common, + key: "keycloak-new-event-created", + name: "New Event Created", + description: "Emit new event when a new event is created. [See the documentation](https://www.keycloak.org/docs-api/latest/rest-api/index.html#_realms_admin)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourcesFn() { + return this.app.listEvents; + }, + getResourcesFnArgs() { + const { + realm, + getLastDateFrom, + } = this; + return { + realm, + params: { + dateFrom: getLastDateFrom(), + }, + }; + }, + generateMeta(resource) { + return { + id: resource.time, + summary: `New Event: ${resource.type}`, + ts: resource.time, + }; + }, + }, +}; diff --git a/components/keysender/README.md b/components/keysender/README.md new file mode 100644 index 0000000000000..7759bccde828a --- /dev/null +++ b/components/keysender/README.md @@ -0,0 +1,11 @@ +# Overview + +The Keysender API offers a unique capability to automate the process of sending product keys or digital goods instantly after a payment transaction. By integrating this API with Pipedream, you can craft seamless workflows for e-commerce automation, customer support, and digital product distribution. Pipedream's serverless platform lets you connect Keysender with various apps for triggering actions, processing data, and responding to events in real-time. + +# Example Use Cases + +- **E-commerce Purchase Fulfillment**: Integrate Keysender with an e-commerce platform like Shopify. When a customer makes a purchase, trigger a Pipedream workflow that calls the Keysender API to send the product key or digital asset directly to the customer's email address. + +- **Customer Support Ticket Resolution**: Connect Keysender with a customer support tool like Zendesk. Use a Pipedream workflow to automatically send a replacement product key when a support ticket with a specific tag, such as "lost key", is resolved. + +- **Digital Inventory Management**: Pair Keysender with a database service like Airtable. Create a Pipedream workflow that monitors an Airtable base for low inventory alerts and uses the Keysender API to generate and stock up on product keys, ensuring you never run out of digital goods to sell. diff --git a/components/kickbox/README.md b/components/kickbox/README.md new file mode 100644 index 0000000000000..791ce8055f62d --- /dev/null +++ b/components/kickbox/README.md @@ -0,0 +1,11 @@ +# Overview + +The Kickbox API on Pipedream allows you to verify email addresses to improve deliverability and reduce bounce rates. With this API, you can integrate email validation into your application's signup process, maintain the hygiene of your email lists, and orchestrate workflows involving email verification with various apps supported by Pipedream. By leveraging serverless workflows, you can automate tasks like updating CRM contacts, triggering marketing campaigns, or filtering out invalid emails before sending out newsletters. + +# Example Use Cases + +- **List Hygiene Automation**: Automatically validate new email addresses added to a Google Sheet. When a new row is added, trigger a workflow that uses the Kickbox API to verify the email. If the email is valid, update another column in Google Sheets to reflect this, ensuring your contact list remains clean. + +- **Signup Verification Process**: Enhance your website's signup form by integrating Kickbox to perform real-time email validation. Use an HTTP webhook to trigger a Pipedream workflow whenever a user signs up, then validate the email with Kickbox. Upon successful validation, you could then proceed to create a new user record in your database or send a welcome email. + +- **CRM Clean-Up**: Periodically validate the email addresses in your CRM like Salesforce. Set up a scheduled workflow in Pipedream that fetches contacts from Salesforce, verifies each email with Kickbox, and updates the contact records in Salesforce based on the verification results, helping maintain the integrity of your CRM data. diff --git a/components/kickofflabs/README.md b/components/kickofflabs/README.md new file mode 100644 index 0000000000000..1678b57706fab --- /dev/null +++ b/components/kickofflabs/README.md @@ -0,0 +1,11 @@ +# Overview + +The KickoffLabs API enables you to automate your referral marketing campaigns by integrating with their service. With this API, you can programmatically manage campaigns, leads, and rewards, allowing you to dynamically respond to new sign-ups or reward achievements. By connecting the KickoffLabs API to Pipedream, you can create powerful workflows that react to events in real-time, synchronize data across multiple services, or even trigger custom logic tailored to your marketing strategy. + +# Example Use Cases + +- **Sync New Leads to CRM**: Capture new leads from KickoffLabs in real-time and add them to your CRM. Use Pipedream to detect when a new lead is created and automatically push that data into a CRM like Salesforce or HubSpot. This ensures your sales team gets instant access to new prospects. + +- **Send Custom Email Notifications**: Send personalized email follow-ups to users who sign up through your KickoffLabs campaign. When someone joins your campaign, use Pipedream to trigger an email through SendGrid or Mailgun with tailored content based on the lead's data, enhancing user engagement. + +- **Reward Referral Achievements**: Track and reward successful referrals automatically. Set up a Pipedream workflow to listen for referral achievements on KickoffLabs, then connect to a rewards platform like Tango Card to send gift cards or other digital rewards to users who hit referral milestones. diff --git a/components/kingsumo/README.md b/components/kingsumo/README.md new file mode 100644 index 0000000000000..627c9375a9993 --- /dev/null +++ b/components/kingsumo/README.md @@ -0,0 +1,11 @@ +# Overview + +The KingSumo API enables you to automate your giveaways and contests, simplifying the process of managing entries, winners, and related analytics. With Pipedream, you can build workflows that interact with the KingSumo API to create entries, fetch winners, and integrate these actions with other apps for enhanced functionality such as email notifications, CRM updates, and social media engagement. + +# Example Use Cases + +- **Sync Giveaway Entries to a Google Sheet**: Use the KingSumo API on Pipedream to fetch new giveaway entries and automatically add them to a Google Sheet. This allows for easy tracking and analytics of participant data in a familiar spreadsheet format. + +- **Notify Winners via Email**: After selecting winners through the KingSumo API, use Pipedream to send personalized congratulatory emails via SendGrid or another email service. Automate the process to ensure timely notifications and reduce manual efforts. + +- **Post Giveaway Updates to Social Media**: Leverage Pipedream's integration with social media platforms like Twitter or Facebook to post updates about your KingSumo giveaways. Announce new contests, remind followers of closing dates, or celebrate winners automatically. diff --git a/components/kintone/README.md b/components/kintone/README.md new file mode 100644 index 0000000000000..b6929a72478f7 --- /dev/null +++ b/components/kintone/README.md @@ -0,0 +1,11 @@ +# Overview + +The Kintone API provides programmatic access to your Kintone database, allowing you to manage data such as records, spaces, and apps. With Pipedream, you can harness this API to automate workflows, sync data across different platforms, and trigger actions based on events in Kintone. Whether it's updating records in response to external triggers or pushing data from Kintone to other services, the integrations you can build are versatile and powerful. + +# Example Use Cases + +- **Sync Kintone Records with Google Sheets**: Automate the synchronization of data between Kintone and Google Sheets. Whenever a new record is added or updated in Kintone, the workflow triggers and updates the corresponding row in a Google Sheet. This is ideal for sharing data with team members who prefer viewing information in spreadsheet format. + +- **Automated Backup of Kintone Data to Dropbox**: Set up a workflow that periodically backs up your Kintone records to Dropbox. This can be a scheduled event that retrieves records from Kintone and saves them as a CSV or JSON file in a designated Dropbox folder, ensuring you have regular backups without manual intervention. + +- **Slack Notifications for New Kintone Records**: Create a real-time notification system that alerts a Slack channel whenever a new record is created in Kintone. This keeps your team informed instantly about new entries or requests, fostering quick responses and collaboration. diff --git a/components/kite_suite/README.md b/components/kite_suite/README.md new file mode 100644 index 0000000000000..4fac8640a3f3e --- /dev/null +++ b/components/kite_suite/README.md @@ -0,0 +1,11 @@ +# Overview + +The Kite Suite API enables you to automate and integrate your Kite Suite project management tasks seamlessly into Pipedream workflows. With this API, you can create, update, and manage projects, tasks, and teams, allowing for streamlined communication and coordination within your organization. By leveraging Pipedream's capabilities, you can connect Kite Suite with various other apps and services, triggering actions based on events, scheduling regular updates, or even processing data in real-time. + +# Example Use Cases + +- **Project Status Updates to Slack**: Automatically send updates to a designated Slack channel whenever there's a change in project status within Kite Suite. This keeps the team informed in real-time and encourages prompt responses to project developments. + +- **Task Creation from Email Requests**: Convert incoming client requests via email into Kite Suite tasks. Use Pipedream to parse emails for key information and create corresponding tasks in Kite Suite, ensuring that no request slips through the cracks. + +- **Daily Project Summary Reports**: Generate a daily summary of project activities and send them to team members via email or post on a company wiki. This workflow can aggregate data from Kite Suite at the end of each day, format it, and distribute it accordingly to keep everyone up-to-date. diff --git a/components/kiwihr/kiwihr.app.mjs b/components/kiwihr/kiwihr.app.mjs new file mode 100644 index 0000000000000..e013be46e0b4f --- /dev/null +++ b/components/kiwihr/kiwihr.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "kiwihr", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/kiwihr/package.json b/components/kiwihr/package.json new file mode 100644 index 0000000000000..1bb1546c948e9 --- /dev/null +++ b/components/kiwihr/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/kiwihr", + "version": "0.0.1", + "description": "Pipedream kiwiHR Components", + "main": "kiwihr.app.mjs", + "keywords": [ + "pipedream", + "kiwihr" + ], + "homepage": "https://pipedream.com/apps/kiwihr", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/kizeo_forms/README.md b/components/kizeo_forms/README.md new file mode 100644 index 0000000000000..509650c66939b --- /dev/null +++ b/components/kizeo_forms/README.md @@ -0,0 +1,11 @@ +# Overview + +Kizeo Forms API lets you interact with the Kizeo platform to automate form data retrieval, manage users, and push or pull data to or from forms. On Pipedream, you can harness this API to create serverless workflows that trigger actions based on form submissions, integrate with other apps, or manage form data effectively. Use the API to connect Kizeo Forms with CRMs, project management tools, databases, and more, streamlining data collection and processing activities. + +# Example Use Cases + +- **Automatically Store Form Submissions in Google Sheets**: When a new form submission is received in Kizeo, the workflow triggers and extracts the submitted data, formatting it as needed, then appends it to a specified Google Sheet. This is ideal for keeping track of responses without manual data entry. + +- **Send Email Notifications on Form Submission**: Set up a Pipedream workflow that listens for new Kizeo form submissions and automatically sends a detailed email to a designated recipient – useful for alerting staff when new data is available or when a customer has completed a critical form. + +- **Create Trello Cards from Kizeo Form Entries**: Each time a form is submitted through Kizeo, the Pipedream workflow creates a new Trello card with the form data. This can be used to track tasks, feedback, or issues reported via forms, moving them into a project management flow seamlessly. diff --git a/components/klaviyo/README.md b/components/klaviyo/README.md index 4f456d25a9dc7..912c8bf1a26c2 100644 --- a/components/klaviyo/README.md +++ b/components/klaviyo/README.md @@ -1,11 +1,107 @@ # Overview -With the Klaviyo API, you can build a variety of things, including: - -- A way to track and analyze your website's traffic -- A way to segment your website's visitors -- A way to send targeted emails to your website's visitors -- A way to A/B test your email campaigns -- A way to track your email campaign's performance -- A way to create custom reports about your website's traffic and visitor - behavior +The Klaviyo API grants you the power to automate and personalize your email marketing efforts. With it, you can manage lists, profiles, and campaigns, track event-driven communications, and analyze the results. By leveraging this API on Pipedream, you can create intricate, automated workflows that respond in real-time to your users' behavior, sync data across multiple platforms, and tailor your marketing strategies to improve engagement and conversion rates. + +# Example Use Cases + +- **Synchronize New Shopify Orders to Klaviyo for Personalized Follow-Ups**: When a new order is placed in Shopify, trigger a workflow in Pipedream that captures order details and customer information. This data is then sent to Klaviyo to create or update a customer profile and trigger a personalized post-purchase email sequence. + +- **Automate Lead Scoring Based on User Activity**: Use Pipedream to listen for webhooks from Klaviyo that indicate user actions, like email opens, link clicks, or form submissions. Combine this with data from other sources, like CRMs or analytics tools, to calculate a lead score. Update the contact in Klaviyo with this score to segment audiences and target high-scoring leads with specialized campaigns. + +- **Streamline Event Invitations and Follow-Ups with Zoom Webinar Registrations**: When a user registers for a webinar on Zoom, Pipedream can capture this event and create a corresponding profile in Klaviyo, automatically adding them to an event-specific list. Post-webinar, use Klaviyo to send out tailored content, such as a recording of the event, additional resources, or calls to action, based on their engagement. + +# Getting Started + +To get started using Klaviyo with Pipedream, you’ll need to create a new Klaviyo API key. + +First, log in to your Klaviyo account, then open *Settings* in the bottom left-hand corner: + +![Open the drawer in the bottom left-hand drawer to open your Klaviyo account settings](https://res.cloudinary.com/pipedreamin/image/upload/v1715178061/marketplace/apps/klayvio/CleanShot_2024-05-08_at_10.11.15_2x_cusdgp.png) + +Then, on the next page, click **Create API Key** to begin creating a new private API key. + +![Create a new Klaviyo API key](https://res.cloudinary.com/pipedreamin/image/upload/v1715178067/marketplace/apps/klayvio/CleanShot_2024-05-08_at_10.11.29_2x_uenkfm.png) + +On this page, you can configure your API key settings, such as its name and permissions. We recommend naming this API key `Pipedream` so you can easily track where it’s used. + +Next, you'll need to define the permissions for this API key. You can grant specific permissions, read-only access to all resources, or full read/write access. Don’t worry, you can change these settings later. + +![Choosing between permission levels](https://res.cloudinary.com/pipedreamin/image/upload/v1715178064/marketplace/apps/klayvio/CleanShot_2024-05-08_at_10.12.15_2x_ebxwdq.png) + +Once you have created your Klaviyo private API key, make sure to copy it to your clipboard and save it within Pipedream through either a Klaviyo trigger or action in a workflow, or by opening the dedicated Connected Accounts area in Pipedream. + +![Save the Klaviyo API key to Pipedream](https://res.cloudinary.com/pipedreamin/image/upload/v1715178053/marketplace/apps/klayvio/CleanShot_2024-05-08_at_10.19.51_2x_otps4k.png) + +Ensure you save the API key before closing the Klaviyo window, as this is the only time this private API key will be displayed. + +# Troubleshooting + +Klavyio uses standard HTTP status codes to communicate errors over it’s API. + +## Status Codes + +### 200 OK + +The request completed successfully. + +### 201 Created + +The request succeeded, and a new resource was created as a result. + +### 202 Accepted + +The request has been received but not yet acted upon. We return this status code for requests that were accepted but are processed asynchronously. + +### 204 No Content + +The request succeeded, but the API doesn’t provide a response body. + +### 400 Bad Request + +Request is missing a required parameter or has an invalid parameter. + +### 401 Not Authorized + +Request is lacking required authentication information. + +Please follow the guidance here for more details on authenticating your API requests. + +### 403 Forbidden + +The request contains valid authentication information, but does not have permissions to perform the specified action. + +See API key scopes for more information. + +### 404 Not Found + +The requested resource doesn't exist. + +### 405 Method not Allowed + +The requested resource doesn't support the provided HTTP method, e.g., DELETE. + +### 409 Conflict + +The request conflicts with the current state of the server. + +### 410 Gone + +The requested content has been permanently deleted from Klaviyo’s server. This status code will occur for requested endpoints that no longer exist in our API. + +### 415 Unsupported Media Type + +The Content-Type or Content-Encoding header is set incorrectly. + +### 429 Rate Limit + +You hit the rate limit for this endpoint (different endpoints have different rate limits). + +### 500 Server Error + +Something is wrong with the destination server. This may be on Klaviyo's end. + +### 503 Service Unavailable + +Something is wrong on Klaviyo’s end leading to service unavailability. + +Check Klaviyo’s Status for updates. diff --git a/components/klaviyo/actions/add-member-to-list/add-member-to-list.mjs b/components/klaviyo/actions/add-member-to-list/add-member-to-list.mjs index 8e2a172db7562..62e4e47a01909 100644 --- a/components/klaviyo/actions/add-member-to-list/add-member-to-list.mjs +++ b/components/klaviyo/actions/add-member-to-list/add-member-to-list.mjs @@ -1,10 +1,11 @@ +import { parseObject } from "../../common/utils.mjs"; import klaviyo from "../../klaviyo.app.mjs"; export default { key: "klaviyo-add-member-to-list", name: "Add Member To List", description: "Add member to a specific list. [See the docs here](https://developers.klaviyo.com/en/v1-2/reference/add-members)", - version: "0.0.1", + version: "1.0.0", type: "action", props: { klaviyo, @@ -14,187 +15,23 @@ export default { "list", ], }, - email: { + profileIds: { propDefinition: [ klaviyo, - "email", + "profileIds", ], }, - phone: { - propDefinition: [ - klaviyo, - "phone", - ], - optional: true, - }, - externalId: { - propDefinition: [ - klaviyo, - "externalId", - ], - optional: true, - }, - title: { - propDefinition: [ - klaviyo, - "title", - ], - optional: true, - }, - organization: { - propDefinition: [ - klaviyo, - "organization", - ], - optional: true, - }, - firstName: { - propDefinition: [ - klaviyo, - "firstName", - ], - optional: true, - }, - lastName: { - propDefinition: [ - klaviyo, - "lastName", - ], - optional: true, - }, - image: { - propDefinition: [ - klaviyo, - "image", - ], - optional: true, - }, - address1: { - propDefinition: [ - klaviyo, - "address1", - ], - optional: true, - }, - address2: { - propDefinition: [ - klaviyo, - "address2", - ], - optional: true, - }, - city: { - propDefinition: [ - klaviyo, - "city", - ], - optional: true, - }, - country: { - propDefinition: [ - klaviyo, - "country", - ], - optional: true, - }, - latitude: { - propDefinition: [ - klaviyo, - "latitude", - ], - optional: true, - }, - longitude: { - propDefinition: [ - klaviyo, - "longitude", - ], - optional: true, - }, - region: { - propDefinition: [ - klaviyo, - "region", - ], - optional: true, - }, - zip: { - propDefinition: [ - klaviyo, - "zip", - ], - optional: true, - }, - timezone: { - propDefinition: [ - klaviyo, - "timezone", - ], - optional: true, - }, - }, - methods: { - getSummary() { - return `${this.email || this.phone - || this.externalId} successfully added to "${this.list.label}"!`; - }, }, async run({ $ }) { - const { - list, - email, - phone, - externalId, - title, - organization, - firstName, - lastName, - image, - address1, - address2, - city, - country, - latitude, - longitude, - region, - zip, - timezone, - } = this; - - const opts = { - email, - id: externalId, - title, - organization, - first_name: firstName, - last_name: lastName, - image, - location: { - address1, - address2, - city, - country, - latitude, - longitude, - region, - zip, - timezone, - }, - }; - - if (phone) opts.phone_number = phone; - - const [ - response, - ] = await this.klaviyo.addMemberToList(list.value, { - body: { - profiles: [ - opts, - ], - }, + const { response } = await this.klaviyo.subscribeProfiles({ + listId: this.list.value, + data: parseObject(this.profileIds)?.map(({ value: id }) => ({ + type: "profile", + id, + })), }); - $.export("$summary", response || this.getSummary(response)); - return response; + $.export("$summary", `${this.profileId} successfully added to "${this.list.label}"!`); + return response.data; }, }; diff --git a/components/klaviyo/actions/create-new-list/create-new-list.mjs b/components/klaviyo/actions/create-new-list/create-new-list.mjs index c467c8993073d..6d9d7563b5d64 100644 --- a/components/klaviyo/actions/create-new-list/create-new-list.mjs +++ b/components/klaviyo/actions/create-new-list/create-new-list.mjs @@ -4,7 +4,7 @@ export default { key: "klaviyo-create-new-list", name: "Create New List", description: "Creates a new list in an account. [See the docs here](https://developers.klaviyo.com/en/v1-2/reference/create-list)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { klaviyo, @@ -15,18 +15,17 @@ export default { ], }, }, - methods: { - getSummary() { - return `"${this.listName}" successfully created!`; - }, - }, async run({ $ }) { - const { listName } = this; const response = await this.klaviyo.newList({ - listName, + data: { + type: "list", + attributes: { + name: this.listName, + }, + }, }); - $.export("$summary", response || this.getSummary(response)); - return response; + $.export("$summary", `"${this.listName}" successfully created!`); + return response.body; }, }; diff --git a/components/klaviyo/actions/get-lists/get-lists.mjs b/components/klaviyo/actions/get-lists/get-lists.mjs index 6e33301b090ea..d16a68825e086 100644 --- a/components/klaviyo/actions/get-lists/get-lists.mjs +++ b/components/klaviyo/actions/get-lists/get-lists.mjs @@ -4,20 +4,15 @@ export default { key: "klaviyo-get-lists", name: "Get Lists", description: "Get a listing of all of the lists in an account. [See the docs here](https://developers.klaviyo.com/en/v1-2/reference/get-lists)", - version: "0.2.0", + version: "0.0.3", type: "action", props: { klaviyo, }, - methods: { - getSummary() { - return "List Successfully fetched!"; - }, - }, async run({ $ }) { const response = await this.klaviyo.getLists(); - $.export("$summary", response || this.getSummary(response)); - return response; + $.export("$summary", "List Successfully fetched!"); + return response.body; }, }; diff --git a/components/klaviyo/common/utils.mjs b/components/klaviyo/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/klaviyo/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/klaviyo/klaviyo.app.mjs b/components/klaviyo/klaviyo.app.mjs index 6fa22ff09cb35..044b2d8e0dd7d 100644 --- a/components/klaviyo/klaviyo.app.mjs +++ b/components/klaviyo/klaviyo.app.mjs @@ -1,6 +1,7 @@ import { - ApiClient, ListsSegments, -} from "klaviyo-sdk"; + GlobalApiKeySettings, Lists, + Profiles, +} from "klaviyo-api"; export default { type: "app", @@ -12,125 +13,70 @@ export default { description: "The list which will be affected.", withLabel: true, async options() { - const lists = await this.getLists(); - return lists.map(({ - list_id: value, list_name: label, + const { body: { data } } = await this.getLists(); + + return data.map(({ + id: value, attributes: { name: label }, }) => ({ label, value, })); }, }, + profileIds: { + type: "string[]", + label: "Profile Ids", + description: "An array with profile Ids.", + withLabel: true, + async options({ prevContext }) { + const { + body: { + data, links, + }, + } = await this.listProfiles({ + "page[cursor]": prevContext.nextCursor, + }); + + return { + options: data.map(({ + id: value, attributes: { email: label }, + }) => ({ + label, + value, + })), + context: { + nextCursor: links.next, + }, + }; + }, + }, listName: { type: "string", label: "List Name", description: "The name of the new list.", }, - email: { - type: "string", - label: "Email", - description: "The contact's email.", - }, - phone: { - type: "string", - label: "Phone Number", - description: "The contact's phone number.", - }, - externalId: { - type: "string", - label: "External Id", - description: "A unique external Id which identifies the profile.", - }, - title: { - type: "string", - label: "Title", - description: "The member's title.", - }, - organization: { - type: "string", - label: "Organization", - description: "The member's organization.", - }, - firstName: { - type: "string", - label: "First Name", - description: "The member's first name.", - }, - lastName: { - type: "string", - label: "Last Name", - description: "The member's last name.", - }, - image: { - type: "string", - label: "Image", - description: "A URL image to member's profile.", - }, - address1: { - type: "string", - label: "Address 1", - description: "The member's address.", - }, - address2: { - type: "string", - label: "Address 2", - description: "The member's address 2.", - }, - city: { - type: "string", - label: "City", - description: "The member's city.", - }, - country: { - type: "string", - label: "Country", - description: "The member's country.", - }, - latitude: { - type: "string", - label: "Latitude", - description: "The member's latitude.", - }, - longitude: { - type: "string", - label: "Longitude", - description: "The member's longitude.", - }, - region: { - type: "string", - label: "Region", - description: "The member's region.", - }, - zip: { - type: "string", - label: "Zip", - description: "The member's zip.", - }, - timezone: { - type: "string", - label: "Timezone", - description: "The member's timezone.", - }, }, methods: { sdk() { - // Klaviyo sdk setup - const defaultClient = ApiClient.instance; - // Configure API key authorization: ApiKeyAuth - const ApiKeyAuth = defaultClient.authentications["ApiKeyAuth"]; - ApiKeyAuth.apiKey = this.$auth.api_key; + new GlobalApiKeySettings(this.$auth.api_key); + }, + newList(data) { + this.sdk(); + return Lists.createList(data); }, - async newList(data) { + getLists() { this.sdk(); - return await ListsSegments.createList(data); + return Lists.getLists(); }, - async getLists() { + subscribeProfiles({ + listId, ...opts + }) { this.sdk(); - return await ListsSegments.getLists(); + return Lists.createListRelationships(listId, opts); }, - async addMemberToList(listId, body) { + listProfiles(opts = {}) { this.sdk(); - return await ListsSegments.addMembers(listId, body); + return Profiles.getProfiles(opts); }, }, }; diff --git a/components/klaviyo/package.json b/components/klaviyo/package.json index 9e61b4ba0e317..81e4cc8c1721e 100644 --- a/components/klaviyo/package.json +++ b/components/klaviyo/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/klaviyo", - "version": "0.0.2", + "version": "0.1.1", "description": "Pipedream Klaviyo Components", "main": "klaviyo.app.mjs", "keywords": [ @@ -13,7 +13,7 @@ "access": "public" }, "dependencies": { - "klaviyo-sdk": "^1.0.1", - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0-0", + "klaviyo-api": "^11.0.0" } } diff --git a/components/klaxoon/README.md b/components/klaxoon/README.md new file mode 100644 index 0000000000000..91601981061f0 --- /dev/null +++ b/components/klaxoon/README.md @@ -0,0 +1,11 @@ +# Overview + +Klaxoon offers interactive tools for enhanced team collaboration and engagement. The Klaxoon API enables programmatic interactions, allowing you to create, manage, and track Klaxoon activities like quizzes, surveys, and brainstorming sessions. When integrated into Pipedream workflows, you can automate interactions with Klaxoon, sync data with other services, and tailor collaboration activities to your team's needs. + +# Example Use Cases + +- **Automated Feedback Collection**: Trigger a Klaxoon survey after a meeting event in Google Calendar. Use Pipedream to capture survey responses and automatically store them in a Google Sheet for analysis. + +- **Dynamic Activity Creation**: Generate Klaxoon brainstorming sessions based on new Trello cards. When a card is added to a specific Trello board, a Pipedream workflow can create a corresponding Klaxoon activity, inviting relevant team members. + +- **Real-time Notifications**: Send Slack notifications when a Klaxoon quiz is completed. Pipedream can listen for Klaxoon quiz submissions and post summaries or results to a designated Slack channel. diff --git a/components/klazify/README.md b/components/klazify/README.md new file mode 100644 index 0000000000000..7e93004d65d09 --- /dev/null +++ b/components/klazify/README.md @@ -0,0 +1,11 @@ +# Overview + +The Klazify API offers a way to classify websites into categories, discover company logo URLs, and access social media links from a domain. When integrated into Pipedream, this functionality can expand, allowing you to automate workflows for marketing analysis, content filtering, or business intelligence. You can trigger actions based on website categories, enrich CRM data with company logos, or compile lists of social media profiles for outreach. + +# Example Use Cases + +- **Website Categorization for Content Moderation**: Use Klazify to categorize websites submitted by users in a community platform. Trigger a Pipedream workflow when a new URL is submitted, classify it with Klazify, and automatically flag content for review if it falls into sensitive categories. + +- **Enriching CRM Data with Company Logos**: Connect Klazify with a CRM app like Salesforce on Pipedream. When a new company is added to your CRM, use Klazify to fetch the company's logo and social media links, then update the CRM record with this enriched data. + +- **Automated Social Media Profile Collection**: Set up a Pipedream workflow that uses Klazify to gather social media profiles related to a list of domains. This can be useful for marketing campaigns, competitor analysis, or lead generation, by automatically creating a database of social media profiles for outreach or monitoring. diff --git a/components/klenty/README.md b/components/klenty/README.md new file mode 100644 index 0000000000000..3e17f74f28ca5 --- /dev/null +++ b/components/klenty/README.md @@ -0,0 +1,11 @@ +# Overview + +The Klenty API enables automation of your sales engagement by integrating with Pipedream. With this API, you can sync leads, automate email campaigns, and manage tasks without manual intervention, creating a seamless sales workflow. By leveraging Pipedream's serverless execution model, you can set up custom triggers, actions, and data transformations between Klenty and various other apps. + +# Example Use Cases + +- **Lead Synchronization with CRM**: Automatically sync new leads from a CRM like Salesforce to Klenty. When a new contact is added to Salesforce, trigger a Pipedream workflow that creates or updates the lead in Klenty, ensuring your sales team has the latest data. + +- **Email Campaign Trigger from E-commerce Actions**: Start a Klenty email sequence when a customer makes a purchase on an e-commerce platform like Shopify. Use Pipedream to detect new orders and trigger personalized follow-up emails through Klenty to encourage repeat business or request reviews. + +- **Support Ticket Follow-Up Sequence**: After a support ticket is resolved in a support app like Zendesk, trigger a follow-up email sequence in Klenty to gather feedback or offer additional help. This maintains customer engagement and can provide valuable insights into your support process. diff --git a/components/klipfolio/.gitignore b/components/klipfolio/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/klipfolio/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/klipfolio/README.md b/components/klipfolio/README.md new file mode 100644 index 0000000000000..07ba77c92bfc3 --- /dev/null +++ b/components/klipfolio/README.md @@ -0,0 +1,11 @@ +# Overview + +The Klipfolio API opens a window to managing and automating your Klipfolio dashboards and data sources directly from Pipedream. With this API, you can programmatically create, update, and delete dashboards, Klips (widgets), and data sources. This allows you to integrate Klipfolio with a multitude of other services, triggering updates to your dashboards as data changes in other apps, or even automate the import and transformation of data for your Klipfolio visualizations. + +# Example Use Cases + +- **Automated Dashboard Reporting**: Trigger a workflow on Pipedream that fetches data from various sources like Google Analytics and Salesforce, processes it, and then updates a Klipfolio dashboard. This can be scheduled daily or triggered by certain events, ensuring your dashboard always shows the latest metrics. + +- **Dynamic Data Source Refresh**: Use Pipedream to monitor a database or a webhook for changes. Once a change is detected, the workflow can update the corresponding data source in Klipfolio, ensuring your visualizations reflect real-time data without manual intervention. + +- **Alerts for Metrics Anomalies**: Create a Pipedream workflow that analyzes the data in your Klipfolio data sources at regular intervals. If it detects anomalies or values crossing certain thresholds, it can send alerts via email, SMS, or messaging platforms like Slack, keeping your team informed and ready to act on the insights. diff --git a/components/klipfolio/actions/create-datasource/create-datasource.mjs b/components/klipfolio/actions/create-datasource/create-datasource.mjs new file mode 100644 index 0000000000000..f5d7ba7e86d56 --- /dev/null +++ b/components/klipfolio/actions/create-datasource/create-datasource.mjs @@ -0,0 +1,82 @@ +import app from "../../klipfolio.app.mjs"; + +export default { + key: "klipfolio-create-datasource", + name: "Create Datasource", + description: "Create a data source. [See the documentation](https://apidocs.klipfolio.com/reference/data-sources#post-datasources)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "name", + ], + }, + description: { + propDefinition: [ + app, + "description", + ], + }, + format: { + propDefinition: [ + app, + "format", + ], + }, + connector: { + propDefinition: [ + app, + "connector", + ], + }, + refreshInterval: { + propDefinition: [ + app, + "refreshInterval", + ], + }, + endpointUrl: { + propDefinition: [ + app, + "endpointUrl", + ], + }, + method: { + propDefinition: [ + app, + "method", + ], + }, + additionalProperties: { + propDefinition: [ + app, + "additionalProperties", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.createDatasource({ + $, + data: { + name: this.name, + description: this.description, + format: this.format, + connector: this.connector, + refresh_interval: parseInt(this.refreshInterval, 10), + properties: { + endpoint_url: this.endpointUrl, + method: this.method, + ...this.additionalProperties, + }, + }, + }); + + $.export("$summary", `Successfully created Datasource named '${this.name}'`); + + return response; + }, +}; diff --git a/components/klipfolio/actions/delete-datasource/delete-datasource.mjs b/components/klipfolio/actions/delete-datasource/delete-datasource.mjs new file mode 100644 index 0000000000000..78eb41418f6c9 --- /dev/null +++ b/components/klipfolio/actions/delete-datasource/delete-datasource.mjs @@ -0,0 +1,29 @@ +import app from "../../klipfolio.app.mjs"; + +export default { + key: "klipfolio-delete-datasource", + name: "Delete Datasource", + description: "Delete the data source associated with a specific data source ID. [See the documentation](https://apidocs.klipfolio.com/reference/data-sources#delete-datasourcesid)", + version: "0.0.1", + type: "action", + props: { + app, + datasourceId: { + propDefinition: [ + app, + "datasourceId", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.deleteDatasource({ + $, + datasourceId: this.datasourceId, + }); + + $.export("$summary", `Successfully deleted datasource with ID: ${this.datasourceId}`); + + return response; + }, +}; diff --git a/components/klipfolio/actions/update-datasource/update-datasource.mjs b/components/klipfolio/actions/update-datasource/update-datasource.mjs new file mode 100644 index 0000000000000..3701e97107b36 --- /dev/null +++ b/components/klipfolio/actions/update-datasource/update-datasource.mjs @@ -0,0 +1,52 @@ +import app from "../../klipfolio.app.mjs"; + +export default { + key: "klipfolio-update-datasource", + name: "Update Datasource", + description: "Update the specified data source. [See the documentation](https://apidocs.klipfolio.com/reference/data-sources#put-datasourcesid)", + version: "0.0.1", + type: "action", + props: { + app, + datasourceId: { + propDefinition: [ + app, + "datasourceId", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + description: { + propDefinition: [ + app, + "description", + ], + }, + refreshInterval: { + propDefinition: [ + app, + "refreshInterval", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.updateDatasource({ + $, + datasourceId: this.datasourceId, + data: { + name: this.name, + description: this.description, + refresh_interval: parseInt(this.refreshInterval, 10), + }, + }); + + $.export("$summary", `Successfully updated Datasource named '${this.name}'`); + + return response; + }, +}; diff --git a/components/klipfolio/app/klipfolio.app.ts b/components/klipfolio/app/klipfolio.app.ts deleted file mode 100644 index 688ab9a4688e9..0000000000000 --- a/components/klipfolio/app/klipfolio.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "klipfolio", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/klipfolio/common/constants.mjs b/components/klipfolio/common/constants.mjs new file mode 100644 index 0000000000000..7b2657c279f7c --- /dev/null +++ b/components/klipfolio/common/constants.mjs @@ -0,0 +1,40 @@ +export default { + DATASOURCE_CONNECTORS: [ + "box", + "comscore", + "db", + "dropbox", + "facebook", + "ftp", + "google_adwords", + "google_analytics", + "google_drive", + "google_spreadsheets", + "hubspot", + "iformbuilder", + "marketo", + "omniture", + "radian6", + "salesforce", + "searchMetrics", + "shopify", + "simple_rest", + "survey_monkey", + "versature", + "xero", + "xmla", + ], + REFRESH_INTERVALS: [ + "0", + "60", + "300", + "900", + "1800", + "3600", + "7200", + "10800", + "14400", + "43200", + "86400", + ], +}; diff --git a/components/klipfolio/klipfolio.app.mjs b/components/klipfolio/klipfolio.app.mjs new file mode 100644 index 0000000000000..e7fabc40b3685 --- /dev/null +++ b/components/klipfolio/klipfolio.app.mjs @@ -0,0 +1,119 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "klipfolio", + propDefinitions: { + name: { + type: "string", + label: "Name", + description: "Name of the Datasource", + }, + description: { + type: "string", + label: "Description", + description: "Description of the Datasource", + }, + format: { + type: "string", + label: "Format", + description: "Format of the Datasource, i.e.: `xml`", + }, + connector: { + type: "string", + label: "Connector", + description: "Connector of the Datasource", + options: constants.DATASOURCE_CONNECTORS, + }, + refreshInterval: { + type: "string", + label: "Refresh Interval", + description: "Refresh Interval of the Datasource", + options: constants.REFRESH_INTERVALS, + }, + endpointUrl: { + type: "string", + label: "Endpoint URL", + description: "Endpoint URL of the Datasource, i.e.: `http://test/data/scatter.xml`", + }, + method: { + type: "string", + label: "Method", + description: "Method for the endpoint, i.e.: `GET`", + }, + datasourceId: { + type: "string", + label: "Datasource ID", + description: "ID of the Datasource", + async options() { + const response = await this.getDatasources(); + const datasourceIds = response.data.datasources; + return datasourceIds.map(({ + id, name, + }) => ({ + label: name, + value: id, + })); + }, + }, + additionalProperties: { + type: "object", + label: "Additional Properties", + description: "Data source additional properties. For example, Google Analytics API has the following properties: `oauth_provider_id`, `oauth_user_header`, `oauth_user_token`, `token_id`, `prop:profile_id`.", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://app.klipfolio.com/api/1.0"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "kf-api-key": `${this.$auth.api_key}`, + }, + }); + }, + async createDatasource(args = {}) { + return this._makeRequest({ + path: "/datasources", + method: "post", + ...args, + }); + }, + async updateDatasource({ + datasourceId, ...args + }) { + return this._makeRequest({ + path: `/datasources/${datasourceId}`, + method: "put", + ...args, + }); + }, + async deleteDatasource({ + datasourceId, ...args + }) { + return this._makeRequest({ + path: `/datasources/${datasourceId}`, + method: "delete", + ...args, + }); + }, + async getDatasources(args = {}) { + return this._makeRequest({ + path: "/datasources", + ...args, + }); + }, + }, +}; diff --git a/components/klipfolio/package.json b/components/klipfolio/package.json index cccf7a1e3674a..8256b3ed0f288 100644 --- a/components/klipfolio/package.json +++ b/components/klipfolio/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/klipfolio", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream Klipfolio Components", - "main": "dist/app/klipfolio.app.mjs", + "main": "klipfolio.app.mjs", "keywords": [ "pipedream", "klipfolio" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/klipfolio", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/klipy/klipy.app.mjs b/components/klipy/klipy.app.mjs new file mode 100644 index 0000000000000..c3746e58b17f9 --- /dev/null +++ b/components/klipy/klipy.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "klipy", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/klipy/package.json b/components/klipy/package.json new file mode 100644 index 0000000000000..578cf36bfd906 --- /dev/null +++ b/components/klipy/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/klipy", + "version": "0.0.1", + "description": "Pipedream Klipy Components", + "main": "klipy.app.mjs", + "keywords": [ + "pipedream", + "klipy" + ], + "homepage": "https://pipedream.com/apps/klipy", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/knack/README.md b/components/knack/README.md index 3370dd7d55bca..42b0fd0385e35 100644 --- a/components/knack/README.md +++ b/components/knack/README.md @@ -1,20 +1,11 @@ # Overview -The Knack API allows developers to access and manipulate data stored in Knack -databases. With the API, developers can +The Knack API allows for robust interactions with your Knack database, enabling automation of data entry, retrieval, updates, and deletion. With Pipedream, you can leverage these capabilities to create seamless workflows that react to events in real-time, sync data across platforms, and enhance your database management without manual intervention. Think less about CRUD operations and more about the magic of connecting your Knack data to the digital tools ecosystem. -- Create new records -- Update existing records -- Delete records -- Search for records -- Get a list of all records -- Get detailed information about a record +# Example Use Cases -In addition, the Knack API provides developer with access to Knack's powerful -customization features, such as +- **Automated Contact Syncing**: When a new contact is added to a CRM like Salesforce, trigger a Pipedream workflow that creates or updates the corresponding record in a Knack database, ensuring your contact lists are always in sync. -- Creating and updating custom objects -- Creating and updating custom fields -- Creating and updating views -- Creating and updating forms -- Creating and updating reports +- **Support Ticket Management**: Use Pipedream to watch for new support tickets submitted via a form on your website. Once a ticket is received, automatically create a new record in Knack and send a Slack message to your support team to prompt immediate action. + +- **Inventory Alerts**: Set up a Pipedream workflow to monitor inventory levels in Knack. When stock for a particular item falls below a threshold, automatically send an email alert via SendGrid to your procurement team to reorder, keeping inventory levels optimal. diff --git a/components/knorish/README.md b/components/knorish/README.md new file mode 100644 index 0000000000000..0e768635610b1 --- /dev/null +++ b/components/knorish/README.md @@ -0,0 +1,11 @@ +# Overview + +The Knorish API provides a gateway to manage and automate tasks for online course platforms. With it, you can streamline processes like user enrollment, course content management, and tracking learner progress. When leveraged through Pipedream, Knorish API enables the creation of custom, serverless workflows that can connect with other apps to enrich e-learning experiences, perform administrative tasks, or analyze educational activities. + +# Example Use Cases + +- **Automate User Enrollment and Welcome Emails**: When a new user signs up for a course on Knorish, Pipedream can trigger a workflow that automatically enrolls the user in the course and sends a personalized welcome email using the SendGrid app, ensuring a smooth onboarding process. + +- **Synchronize Course Updates with a CMS**: Whenever a course is updated on Knorish, Pipedream can detect the change and synchronize the updated content with a Content Management System (CMS) like WordPress. This keeps your course listings current across platforms without manual intervention. + +- **Generate Certificates and Update CRM**: On course completion, Pipedream can call Knorish API to issue a certificate and then update a Customer Relationship Management (CRM) system like Salesforce with the user's completion status, enabling better tracking of learner achievements and targeted follow-up marketing. diff --git a/components/knowbe4/README.md b/components/knowbe4/README.md index 45798f40f6f58..4625c57d26fed 100644 --- a/components/knowbe4/README.md +++ b/components/knowbe4/README.md @@ -1,15 +1,11 @@ # Overview -With the KnowBe4 API, you can build a variety of integrations and tools to help -improve your security posture. Some examples include: - -- A tool to automatically onboard new users and ensure they are receiving - security awareness training -- A dashboard to monitor the security awareness training progress of your users -- A reporting tool to show the results of your user's training -- An integration with your ticketing system to automatically create tickets for - users who have not completed their training -- A Slack bot to remind users to complete their training - -There are many more possibilities with the KnowBe4 API, so get creative and -build something that fits your specific needs! +The KnowBe4 API offers a platform for automating and managing the various aspects of a security awareness training program. By tapping into KnowBe4's API on Pipedream, you can streamline user management, phishing simulations, and training campaigns, integrating these into broader IT processes or alerting systems. Tailoring automations can lead to improved response times, comprehensive reporting, and enhanced overall security education within an organization. + +# Example Use Cases + +- **Automated User Provisioning**: Sync new hires from an HR system into KnowBe4 to automatically enroll them in security awareness training. When a new employee is added to your HR system, trigger a Pipedream workflow that adds the user to KnowBe4 and assigns relevant training modules based on their role. + +- **Phishing Simulation Analysis**: Connect KnowBe4 with a data visualization tool like Google Sheets or Tableau. Use Pipedream to collect results from phishing simulation campaigns, process the data, and push insights into a dashboard for easy analysis and monitoring of employee susceptibility trends over time. + +- **Real-Time Alerting for Training Completion**: Set up a monitoring system on Pipedream that watches for training completions in KnowBe4. When an employee finishes their assigned training modules, trigger a notification through Slack, email, or SMS to their manager or the security team, ensuring compliance and keeping track of the organization’s security training progress. diff --git a/components/knowfirst/README.md b/components/knowfirst/README.md new file mode 100644 index 0000000000000..4444f82db74f9 --- /dev/null +++ b/components/knowfirst/README.md @@ -0,0 +1,11 @@ +# Overview + +The KnowFirst API enables users to tap into a wealth of news and information by categorizing articles and providing insights based on content and trends. In Pipedream, you can harness this API to trigger workflows from new content alerts, analyze news data, or integrate with other apps to keep your team informed about relevant industry updates. By creating serverless workflows, you can automate actions based on the info received from KnowFirst, such as sending notifications, updating databases, or generating reports. + +# Example Use Cases + +- **Content Alert System**: Build a workflow that triggers whenever KnowFirst categorizes a new article under a specific topic. Use this to send real-time alerts to Slack, ensuring your team stays updated on the latest news in your field. + +- **Trend Analysis Dashboard**: Create a Pipedream workflow that periodically fetches data from KnowFirst and sends it to a Google Sheets spreadsheet. Use this data to track trends and generate visualizations to inform your content strategy. + +- **Competitor Monitoring**: Set up a Pipedream workflow that uses KnowFirst to monitor news about competitors. When a new article is categorized, the workflow can enrich the information using Clearbit to fetch additional company data, then log it in an Airtable base for strategic analysis. diff --git a/components/koala_ai/README.md b/components/koala_ai/README.md new file mode 100644 index 0000000000000..1fef2680d4089 --- /dev/null +++ b/components/koala_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +The Koala AI API enables intelligent analysis of text and conversation data, offering insights and automation capabilities. With Koala AI API on Pipedream, you can craft serverless workflows that respond to data in real-time. Use it to analyze sentiment, categorize content, extract entities, and more. Build workflows that trigger on new data, process it through Koala AI, and act on the insights gained, like updating a database, sending notifications, or enriching CRM information. + +# Example Use Cases + +- **Sentiment Analysis for Customer Support**: Process incoming support tickets using Koala AI to determine sentiment. If a negative sentiment is detected, escalate the ticket in your support system and notify a manager via Slack. + +- **Content Categorization for Social Media Posts**: Automatically categorize social media posts collected via a webhook. Use Koala AI to tag content based on topics and push the categorized data to a Google Sheet for easy tracking and analysis. + +- **Lead Qualification for Sales Teams**: When a new contact is added to your CRM, trigger a workflow to analyze any associated text or email conversation with Koala AI. Depending on the intent and sentiment detected, score the lead and update the CRM record for prioritized follow-up. diff --git a/components/koala_ai/package.json b/components/koala_ai/package.json index 41c5c3891c72a..a49c267781e77 100644 --- a/components/koala_ai/package.json +++ b/components/koala_ai/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/kodagpt/README.md b/components/kodagpt/README.md new file mode 100644 index 0000000000000..e9682b633e57f --- /dev/null +++ b/components/kodagpt/README.md @@ -0,0 +1,11 @@ +# Overview + +The KodaGPT API offers a versatile platform for integrating advanced language processing into your applications, enabling the automation of text-based tasks and enhancing user interactions through natural language understanding. With its capabilities, developers can create dynamic content, automate responses, and analyze text on-the-fly. Leveraging Pipedream's robust integration platform, users can connect KodaGPT with other services to design complex workflows that simplify operations, improve efficiency, and deliver personalized experiences. + +# Example Use Cases + +- **Customer Support Automation**: Automatically handle common customer inquiries by integrating the KodaGPT API with a customer service platform like Zendesk. Use KodaGPT to understand and respond to customer questions, and if the query becomes too complex, escalate the ticket to a human agent within Zendream. + +- **Content Generation for Social Media**: Connect KodaGPT with Twitter to automate content creation. Set up a workflow where KodaGPT generates daily posts based on trending topics or predefined themes, and use Pipedream to schedule and publish these posts automatically to your social media profiles. + +- **Email Response Drafting**: Integrate KodaGPT with Gmail to help draft responses to emails. When a new email arrives, use Pipedream to trigger the KodaGPT API to generate a draft response based on the email content, which you can then review and personalize before sending. diff --git a/components/kodagpt/actions/semantic-search/semantic-search.mjs b/components/kodagpt/actions/semantic-search/semantic-search.mjs new file mode 100644 index 0000000000000..449a317bb50ba --- /dev/null +++ b/components/kodagpt/actions/semantic-search/semantic-search.mjs @@ -0,0 +1,41 @@ +import app from "../../kodagpt.app.mjs"; + +export default { + key: "kodagpt-semantic-search", + name: "Semantic Search", + description: "Perform a semantic search within chatbot data [See the documentation](https://kodagpt.readme.io/reference/buscas-semanticas)", + version: "0.0.1", + type: "action", + props: { + app, + question: { + propDefinition: [ + app, + "question", + ], + }, + chatbotId: { + propDefinition: [ + app, + "chatbotId", + ], + optional: true, + }, + alert: { + type: "alert", + alertType: "info", + content: "Bot ID is required if you don't specify Bot ID on the connection popup", + }, + }, + async run({ $ }) { + const response = await this.app.semanticSearch({ + $, + question: this.question, + chatbotId: this.chatbotId, + }); + + $.export("$summary", `Successfully generated '${response.length}' answers`); + + return response; + }, +}; diff --git a/components/kodagpt/kodagpt.app.mjs b/components/kodagpt/kodagpt.app.mjs new file mode 100644 index 0000000000000..386ac4cf17cb9 --- /dev/null +++ b/components/kodagpt/kodagpt.app.mjs @@ -0,0 +1,48 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "kodagpt", + propDefinitions: { + question: { + type: "string", + label: "Question", + description: "The question to ask the chatbot", + }, + chatbotId: { + type: "string", + label: "Chatbot ID", + description: "To get your Chatbot ID, sign in, go to \"Integrations\". Copy your Chatbot ID from \"My Chatbots\".", + }, + }, + methods: { + _baseUrl() { + return "https://kodagpt.com.br/api/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "Content-Type": "application/json", + "API_KEY": `${this.$auth.api_key}`, + }, + }); + }, + async semanticSearch({ + question, chatbotId, ...args + }) { + return this._makeRequest({ + path: `/embed/${this.$auth.chatbot_id || chatbotId}/${question}`, + ...args, + }); + }, + }, +}; diff --git a/components/kodagpt/package.json b/components/kodagpt/package.json new file mode 100644 index 0000000000000..3b6371ee61eae --- /dev/null +++ b/components/kodagpt/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/kodagpt", + "version": "0.1.0", + "description": "Pipedream KodaGPT Components", + "main": "kodagpt.app.mjs", + "keywords": [ + "pipedream", + "kodagpt" + ], + "homepage": "https://pipedream.com/apps/kodagpt", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/kommo/kommo.app.mjs b/components/kommo/kommo.app.mjs new file mode 100644 index 0000000000000..c8c72a408dce3 --- /dev/null +++ b/components/kommo/kommo.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "kommo", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/kommo/package.json b/components/kommo/package.json new file mode 100644 index 0000000000000..6979e60fc8ed9 --- /dev/null +++ b/components/kommo/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/kommo", + "version": "0.0.1", + "description": "Pipedream Kommo Components", + "main": "kommo.app.mjs", + "keywords": [ + "pipedream", + "kommo" + ], + "homepage": "https://pipedream.com/apps/kommo", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/konfhub/actions/send-otp/send-otp.mjs b/components/konfhub/actions/send-otp/send-otp.mjs new file mode 100644 index 0000000000000..834151e33222d --- /dev/null +++ b/components/konfhub/actions/send-otp/send-otp.mjs @@ -0,0 +1,57 @@ +import konfhub from "../../konfhub.app.mjs"; + +export default { + key: "konfhub-send-otp", + name: "Send OTP", + description: "Send a one-time password (OTP) to a user. [See the documentation](https://docs.konfhub.com/#api-Referral-Registration-Send_OTP)", + version: "0.0.1", + type: "action", + props: { + konfhub, + eventId: { + propDefinition: [ + konfhub, + "eventId", + ], + }, + eventName: { + type: "string", + label: "Event Name", + description: "Name of the event", + }, + email: { + propDefinition: [ + konfhub, + "email", + ], + }, + userName: { + type: "string", + label: "User Name", + description: "Name of the user", + optional: true, + }, + }, + async run({ $ }) { + const { + konfhub, + eventId, + eventName, + email, + userName, + } = this; + + const response = await konfhub.sendOTP({ + $, + eventId, + params: { + email_id: email, + event_name: eventName, + user_name: userName, + }, + }); + + $.export("$summary", `Successfully sent OTP to "${email}"`); + return response; + }, +}; diff --git a/components/konfhub/actions/validate-registration/validate-registration.mjs b/components/konfhub/actions/validate-registration/validate-registration.mjs new file mode 100644 index 0000000000000..f7514a53b05bf --- /dev/null +++ b/components/konfhub/actions/validate-registration/validate-registration.mjs @@ -0,0 +1,80 @@ +import konfhub from "../../konfhub.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "konfhub-validate-registration", + name: "Validate Registration", + description: "Validate a user's email or phone number for a given event. [See the documentation](https://docs.konfhub.com/#api-Common_APIs-Registration_Validation)", + version: "0.0.1", + type: "action", + props: { + konfhub, + docsAlert: { + type: "alert", + alertType: "info", + content: `[See the documentation](https://docs.konfhub.com/#api-Common_APIs-Registration_Validation) for details on the different verification methods available via this action. +\\ +At least one of \`Email Address\` or \`Phone Number\` is required.`, + }, + email: { + propDefinition: [ + konfhub, + "email", + ], + optional: true, + }, + phone: { + type: "string", + label: "Phone Number", + description: "Phone number of the user (with country code)", + optional: true, + }, + countryCode: { + type: "string", + label: "Country Code", + description: "Country code of the phone number, e.g. `+91` or `91`", + optional: true, + }, + eventId: { + propDefinition: [ + konfhub, + "eventId", + ], + optional: true, + }, + ticketId: { + type: "string", + label: "Ticket ID", + description: "ID of the ticket purchased", + optional: true, + }, + }, + async run({ $ }) { + const { + konfhub, + email, + phone, + countryCode, + eventId, + ticketId, + } = this; + + if (!email && !phone) { + throw new ConfigurationError("At least one of `Email Address` or `Phone Number` is required"); + } + + const response = await konfhub.validateRegistration({ + $, + params: { + email_id: email, + phone_number: phone, + country_code: countryCode, + event_id: eventId, + ticket_id: ticketId, + }, + }); + + $.export("$summary", "Successfully verified registration"); + return response; + }, +}; diff --git a/components/konfhub/konfhub.app.mjs b/components/konfhub/konfhub.app.mjs new file mode 100644 index 0000000000000..bafcb9fb074a9 --- /dev/null +++ b/components/konfhub/konfhub.app.mjs @@ -0,0 +1,49 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "konfhub", + propDefinitions: { + email: { + type: "string", + label: "Email Address", + description: "Email address of the user", + }, + eventId: { + type: "string", + label: "Event ID", + description: "ID of the event. You can obtain this from the Konfhub Dashboard's URL, e.g. if the URL is `https://events.konfhub.com/e/fe8c3fbb-8b7d-4268-b9c2-f3a8fa6b31c6/` the event ID is `fe8c3fbb-8b7d-4268-b9c2-f3a8fa6b31c6`", + }, + }, + methods: { + _baseUrl() { + return "https://api.konfhub.com"; + }, + async _makeRequest({ + $ = this, headers, ...otherOpts + }) { + return axios($, { + ...otherOpts, + baseURL: this._baseUrl(), + headers: { + ...headers, + "x-api-key": `${this.$auth.api_key}`, + }, + }); + }, + validateRegistration(args) { + return this._makeRequest({ + url: "/validate", + ...args, + }); + }, + sendOTP({ + eventId, ...args + }) { + return this._makeRequest({ + url: `/event/${eventId}/referral/otp`, + ...args, + }); + }, + }, +}; diff --git a/components/konfhub/package.json b/components/konfhub/package.json new file mode 100644 index 0000000000000..128fe19463896 --- /dev/null +++ b/components/konfhub/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/konfhub", + "version": "0.1.0", + "description": "Pipedream KonfHub Components", + "main": "konfhub.app.mjs", + "keywords": [ + "pipedream", + "konfhub" + ], + "homepage": "https://pipedream.com/apps/konfhub", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} diff --git a/components/kontent_ai/README.md b/components/kontent_ai/README.md new file mode 100644 index 0000000000000..1145f829b6bda --- /dev/null +++ b/components/kontent_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +The Kontent.ai API offers robust content management capabilities for developers to manage and deliver content across various platforms. With this API, you can automate content creation, update content items, retrieve assets, and sync your content with other services. On Pipedream, you can harness these features to create powerful workflows that trigger on specific events, process data, and connect with other apps to streamline your content operations. + +# Example Use Cases + +- **Content Publish Notification to Slack**: When a new piece of content is published in Kontent.ai, trigger a Pipedream workflow to send a notification to a specific Slack channel. This keeps your team informed about new content available and ensures immediate visibility. + +- **Sync Content Updates to Google Sheets**: Monitor changes in your content items on Kontent.ai and automatically update a Google Sheet with the latest content details. This can be used for content tracking, auditing, or as a simple CMS data backup. + +- **Automated Content Translation Workflow**: Whenever new content is added to Kontent.ai, trigger a workflow that sends the text to a translation service like Google Translate or DeepL, and then updates the translated content back into Kontent.ai under a specific language variant. diff --git a/components/koyeb/koyeb.app.mjs b/components/koyeb/koyeb.app.mjs new file mode 100644 index 0000000000000..fc0d65b729436 --- /dev/null +++ b/components/koyeb/koyeb.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "koyeb", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/koyeb/package.json b/components/koyeb/package.json new file mode 100644 index 0000000000000..ee55e06a8e42e --- /dev/null +++ b/components/koyeb/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/koyeb", + "version": "0.0.1", + "description": "Pipedream Koyeb Components", + "main": "koyeb.app.mjs", + "keywords": [ + "pipedream", + "koyeb" + ], + "homepage": "https://pipedream.com/apps/koyeb", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/components/krispcall/actions/add-contact/add-contact.mjs b/components/krispcall/actions/add-contact/add-contact.mjs new file mode 100644 index 0000000000000..2d1d299d6669a --- /dev/null +++ b/components/krispcall/actions/add-contact/add-contact.mjs @@ -0,0 +1,60 @@ +import krispcall from "../../krispcall.app.mjs"; + +export default { + key: "krispcall-add-contact", + name: "Add Contact", + description: "Creates a new contact. [See the documentation](https://documenter.getpostman.com/view/32476792/2sA3dxFCaL)", + version: "0.0.2", + type: "action", + props: { + krispcall, + number: { + propDefinition: [ + krispcall, + "number", + ], + }, + name: { + propDefinition: [ + krispcall, + "name", + ], + optional: true, + }, + email: { + propDefinition: [ + krispcall, + "email", + ], + optional: true, + }, + company: { + propDefinition: [ + krispcall, + "company", + ], + optional: true, + }, + address: { + propDefinition: [ + krispcall, + "address", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.krispcall.createContact({ + $, + data: { + number: this.number, + name: this.name, + email: this.email, + company: this.company, + address: this.address, + }, + }); + $.export("$summary", `Successfully created contact with ID ${response.id}`); + return response; + }, +}; diff --git a/components/krispcall/actions/delete-contact/delete-contact.mjs b/components/krispcall/actions/delete-contact/delete-contact.mjs new file mode 100644 index 0000000000000..45488f064fe4f --- /dev/null +++ b/components/krispcall/actions/delete-contact/delete-contact.mjs @@ -0,0 +1,30 @@ +import { parseObject } from "../../common/utils.mjs"; +import krispcall from "../../krispcall.app.mjs"; + +export default { + key: "krispcall-delete-contact", + name: "Delete Contact", + description: "Deletes a list of contacts. [See the documentation](https://documenter.getpostman.com/view/32476792/2sA3dxFCaL)", + version: "0.0.2", + type: "action", + props: { + krispcall, + contactIds: { + propDefinition: [ + krispcall, + "contactIds", + ], + }, + }, + async run({ $ }) { + const parsedContacts = parseObject(this.contactIds); + const response = await this.krispcall.deleteContacts({ + $, + data: { + contacts: parsedContacts, + }, + }); + $.export("$summary", `Successfully deleted ${parsedContacts.length} contact(s)`); + return response; + }, +}; diff --git a/components/krispcall/actions/new-mms/new-mms.mjs b/components/krispcall/actions/new-mms/new-mms.mjs new file mode 100644 index 0000000000000..b7b9fd0b0d36c --- /dev/null +++ b/components/krispcall/actions/new-mms/new-mms.mjs @@ -0,0 +1,50 @@ +import krispcall from "../../krispcall.app.mjs"; + +export default { + key: "krispcall-new-mms", + name: "Send New MMS", + description: "Send a new MMS to a contact. [See the documentation](https://documenter.getpostman.com/view/32476792/2sA3dxFCaL)", + version: "0.0.2", + type: "action", + props: { + krispcall, + fromNumber: { + propDefinition: [ + krispcall, + "fromNumber", + ], + }, + toNumber: { + propDefinition: [ + krispcall, + "toNumber", + ], + }, + content: { + propDefinition: [ + krispcall, + "content", + ], + optional: true, + }, + medias: { + propDefinition: [ + krispcall, + "medias", + ], + }, + }, + async run({ $ }) { + const response = await this.krispcall.sendMMS({ + $, + data: { + from_number: this.fromNumber, + to_number: this.toNumber, + content: this.content, + medias: this.medias, + }, + }); + $.export("$summary", `Successfully sent MMS from ${this.fromNumber} to ${this.toNumber}`); + return response; + }, +}; diff --git a/components/krispcall/actions/new-sms/new-sms.mjs b/components/krispcall/actions/new-sms/new-sms.mjs new file mode 100644 index 0000000000000..a7d1c936f62e8 --- /dev/null +++ b/components/krispcall/actions/new-sms/new-sms.mjs @@ -0,0 +1,42 @@ +import krispcall from "../../krispcall.app.mjs"; + +export default { + key: "krispcall-new-sms", + name: "Send New SMS", + description: "Send a new SMS to a number. [See the documentation](https://documenter.getpostman.com/view/32476792/2sA3dxFCaL)", + version: "0.0.2", + type: "action", + props: { + krispcall, + fromNumber: { + propDefinition: [ + krispcall, + "fromNumber", + ], + }, + toNumber: { + propDefinition: [ + krispcall, + "toNumber", + ], + }, + content: { + propDefinition: [ + krispcall, + "content", + ], + }, + }, + async run({ $ }) { + const response = await this.krispcall.sendSMS({ + $, + data: { + from_number: this.fromNumber, + to_number: this.toNumber, + content: this.content, + }, + }); + $.export("$summary", `Successfully sent SMS to ${this.toNumber}`); + return response; + }, +}; diff --git a/components/krispcall/common/utils.mjs b/components/krispcall/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/krispcall/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/krispcall/krispcall.app.mjs b/components/krispcall/krispcall.app.mjs new file mode 100644 index 0000000000000..6c6955f01ff2c --- /dev/null +++ b/components/krispcall/krispcall.app.mjs @@ -0,0 +1,146 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "krispcall", + propDefinitions: { + number: { + type: "string", + label: "Number", + description: "The phone number of the contact", + }, + name: { + type: "string", + label: "Name", + description: "The name of the contact", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "The email of the contact", + optional: true, + }, + company: { + type: "string", + label: "Company", + description: "The company of the contact", + optional: true, + }, + address: { + type: "string", + label: "Address", + description: "The address of the contact", + optional: true, + }, + fromNumber: { + type: "string", + label: "From Number", + description: "The number from which the SMS/MMS is sent", + async options() { + const numbers = await this.listNumbers(); + return numbers.map(({ number }) => number); + }, + }, + toNumber: { + type: "string", + label: "To Number", + description: "The recipient's phone number", + }, + content: { + type: "string", + label: "Content", + description: "The content of the SMS/MMS", + }, + medias: { + type: "string[]", + label: "Medias", + description: "The media files for MMS. It should be a valid url field and size should not be greater than 5mb. Upto 5 media lists are supported. Media url should be starting from https. Media url should be public accessible and content-type should be supported by KrispCall app.", + }, + contactIds: { + type: "string[]", + label: "Contact Numbers", + description: "The phone numbers of the contacts to delete", + async options() { + const contacts = await this.listContacts(); + return contacts.map(({ + name, contact_number: number, + }) => ({ + label: `${name} ${number}`, + value: number, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://automationapi.krispcall.com/api/v1/platform/pipedream"; + }, + _headers() { + return { + "X-API-Key": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listNumbers() { + return this._makeRequest({ + path: "/get-numbers", + }); + }, + listContacts() { + return this._makeRequest({ + path: "/get-contacts", + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/add-contact", + ...opts, + }); + }, + sendSMS(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/send-sms", + ...opts, + }); + }, + sendMMS(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/send-mms", + ...opts, + }); + }, + deleteContacts(opts = {}) { + return this._makeRequest({ + method: "DELETE", + path: "/delete-contacts", + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/subscribe", + ...opts, + }); + }, + deleteWebhook(opts = {}) { + return this._makeRequest({ + method: "DELETE", + path: "/unsubscribe", + ...opts, + }); + }, + }, +}; diff --git a/components/krispcall/package.json b/components/krispcall/package.json new file mode 100644 index 0000000000000..c5445a15121cc --- /dev/null +++ b/components/krispcall/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/krispcall", + "version": "0.1.1", + "description": "Pipedream KrispCall Components", + "main": "krispcall.app.mjs", + "keywords": [ + "pipedream", + "krispcall" + ], + "homepage": "https://pipedream.com/apps/krispcall", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} diff --git a/components/krispcall/sources/common/base.mjs b/components/krispcall/sources/common/base.mjs new file mode 100644 index 0000000000000..13f32ce644f46 --- /dev/null +++ b/components/krispcall/sources/common/base.mjs @@ -0,0 +1,47 @@ +import krispcall from "../../krispcall.app.mjs"; + +export default { + props: { + krispcall, + http: { + type: "$.interface.http", + customResponse: false, + }, + db: "$.service.db", + }, + methods: { + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + _getHookId() { + return this.db.get("hookId"); + }, + }, + hooks: { + async activate() { + const data = await this.krispcall.createWebhook({ + data: { + action: this.getAction(), + hookUrl: this.http.endpoint, + }, + }); + this._setHookId(data.id); + }, + async deactivate() { + const hookId = this._getHookId(); + await this.krispcall.deleteWebhook({ + data: { + hookUrl: hookId, + }, + }); + }, + }, + async run({ body }) { + const ts = Date.parse(new Date()); + this.$emit(body, { + id: `${body.id}-${ts}`, + summary: this.getSummary(body), + ts: ts, + }); + }, +}; diff --git a/components/krispcall/sources/new-contact-instant/new-contact-instant.mjs b/components/krispcall/sources/new-contact-instant/new-contact-instant.mjs new file mode 100644 index 0000000000000..dfeebfe8eb48d --- /dev/null +++ b/components/krispcall/sources/new-contact-instant/new-contact-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "krispcall-new-contact-instant", + name: "New Contact (Instant)", + description: "Emit new event when a new contact is created.", + version: "0.0.2", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getAction() { + return "new_contact"; + }, + getSummary(body) { + return `New contact created: ${body.name} ${body.contact_number}`; + }, + }, + sampleEmit, +}; diff --git a/components/krispcall/sources/new-contact-instant/test-event.mjs b/components/krispcall/sources/new-contact-instant/test-event.mjs new file mode 100644 index 0000000000000..b61b7d7173399 --- /dev/null +++ b/components/krispcall/sources/new-contact-instant/test-event.mjs @@ -0,0 +1,8 @@ +export default { + "id": "c17ac176-2f4c-48de-90d0-553d4d1efbf4", + "email": "contact@email.com", + "company": "Company Name", + "address": "Address 01", + "name": "Contact Name", + "contact_number": "+14844731794" +} \ No newline at end of file diff --git a/components/krispcall/sources/new-sms-or-mms-instant/new-sms-or-mms-instant.mjs b/components/krispcall/sources/new-sms-or-mms-instant/new-sms-or-mms-instant.mjs new file mode 100644 index 0000000000000..ed8e6da7bbebb --- /dev/null +++ b/components/krispcall/sources/new-sms-or-mms-instant/new-sms-or-mms-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "krispcall-new-sms-or-mms-instant", + name: "New SMS or MMS Sent (Instant)", + description: "Emit new event when a new SMS or MMS is sent.", + version: "0.0.2", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getAction() { + return "new_sms_or_mms"; + }, + getSummary(body) { + return `New SMS/MMS sent from ${body.from_number} to ${body.to_number}`; + }, + }, + sampleEmit, +}; diff --git a/components/krispcall/sources/new-sms-or-mms-instant/test-event.mjs b/components/krispcall/sources/new-sms-or-mms-instant/test-event.mjs new file mode 100644 index 0000000000000..521211d8d00e1 --- /dev/null +++ b/components/krispcall/sources/new-sms-or-mms-instant/test-event.mjs @@ -0,0 +1,7 @@ +export default { + "id": "LdpkiDcAdWcorm6Nm9a3DR", + "from_number": "+17345084292", + "to_number": "+12568889109", + "media_link": "https://api.medialink.com/8be6fd004ace587eca250cb17c5af4d", + "content": "" +} \ No newline at end of file diff --git a/components/krispcall/sources/new-voicemail-instant/new-voicemail-instant.mjs b/components/krispcall/sources/new-voicemail-instant/new-voicemail-instant.mjs new file mode 100644 index 0000000000000..6f64c51b47d4d --- /dev/null +++ b/components/krispcall/sources/new-voicemail-instant/new-voicemail-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "krispcall-new-voicemail-instant", + name: "New Voicemail (Instant)", + description: "Emit new event when a new voicemail is sent.", + version: "0.0.2", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getAction() { + return "new_voicemail"; + }, + getSummary() { + return "New Voicemail"; + }, + }, + sampleEmit, +}; diff --git a/components/krispcall/sources/new-voicemail-instant/test-event.mjs b/components/krispcall/sources/new-voicemail-instant/test-event.mjs new file mode 100644 index 0000000000000..3e6bed04147cd --- /dev/null +++ b/components/krispcall/sources/new-voicemail-instant/test-event.mjs @@ -0,0 +1,5 @@ +export default { + "id": "LdpkiDcAdWcorm6Nm9a3DR", + "from_number": "+17345084292", + "to_number": "+12568889109", +} \ No newline at end of file diff --git a/components/kucoin_futures/README.md b/components/kucoin_futures/README.md index 3fab0b19d2820..bb814bdcaaa27 100644 --- a/components/kucoin_futures/README.md +++ b/components/kucoin_futures/README.md @@ -1,14 +1,11 @@ # Overview -The Kucoin Futures API allows developers to programmatically access Kucoin's -futures trading functionality. Using the API, developers can create -applications that can trade futures contracts on Kucoin's platform. - -Here are some examples of what you can build using the Kucoin Futures API: - -- A futures trading bot that automatically trades contracts on Kucoin's - platform -- A mobile app that lets users trade futures contracts on Kucoin -- A web app that shows real-time data for Kucoin's futures markets -- A trading platform that integrated with Kucoin's API to allow for trading - futures contracts +The Kucoin Futures API enables traders to programmatically manage their futures accounts on Kucoin, allowing for real-time data retrieval, order placement, and account management. By leveraging Pipedream's serverless platform, you can create automated workflows that react to market changes, manage risk, and optimize your trading strategy. These workflows can integrate with a multitude of other services and APIs, enabling complex automations that can drive informed decision-making based on market signals, notifications, and more. + +# Example Use Cases + +- **Automated Trading Strategy Execution**: Create a Pipedream workflow that monitors market conditions using the Kucoin Futures API. When specific criteria are met—like a certain price target or volume spike—automatically execute trades, adjust leverage, or close positions, thereby optimizing trading strategies with minimal manual intervention. + +- **Risk Management Alerts**: Utilize Pipedream to set up a system that tracks position sizes and margin levels. When a position reaches a threshold that suggests increased risk—such as approaching a margin call—the workflow can trigger real-time alerts via SMS or email through integrations with Twilio or SendGrid, prompting immediate action. + +- **Portfolio Balance Reporting**: Configure a daily or weekly workflow on Pipedream that aggregates your account data from the Kucoin Futures API, calculating the total balance and performance of your futures portfolio. Integrate this with Google Sheets or Airtable to log the data over time, creating a historical performance record for trend analysis. diff --git a/components/kvdb/README.md b/components/kvdb/README.md index b7f1e702065c7..c94e44ff816e6 100644 --- a/components/kvdb/README.md +++ b/components/kvdb/README.md @@ -1,5 +1,14 @@ # Overview -With the KVdb API, you can build a simple key-value database that can be used -to store data for your application. The API provides a simple, easy-to-use -interface that can be used to add, update, and delete data from your database. +The KVdb API is a key-value store that facilitates simple data storage and retrieval operations. On Pipedream, you can harness this API to build serverless workflows that require quick data access and state management. Whether you're needing to store user preferences, cache data for repeat use, or coordinate distributed processes, KVdb's straightforward RESTful interface can be integrated into Pipedream's workflows to provide persistent storage solutions. + +# Example Use Cases + +- **User Preference Management for a Slack Bot** + In a workflow where a Slack bot interacts with users, use KVdb to remember user preferences or the last interaction state. When a user sends a command to the bot, the workflow retrieves the user's data from KVdb to personalize the interaction, then updates the data based on current interactions for future reference. + +- **Cache Layer for API Call Responses** + Speed up response times by caching API call results in KVdb. When a workflow includes an API call that doesn't change often, like a request for weather data or a list of countries from a third-party API, store the response in KVdb. Subsequent workflow runs first check KVdb to see if the data exists and is fresh before calling the external API again, thus saving on API call quotas and speeding up the overall process. + +- **Coordinating Distributed Cron Jobs** + If you have multiple cron-triggered Pipedream workflows that need to work in tandem without stepping on each other's toes, use KVdb to manage flags or timestamps that indicate when a job was last run. Each workflow checks and sets values in KVdb to ensure they only proceed when needed, effectively coordinating processes and preventing overlap or unnecessary executions. diff --git a/components/kvdb/package.json b/components/kvdb/package.json new file mode 100644 index 0000000000000..53aa4db1e44f5 --- /dev/null +++ b/components/kvdb/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/kvdb", + "version": "0.6.0", + "description": "Pipedream kvdb Components", + "main": "kvdb.app.mjs", + "keywords": [ + "pipedream", + "kvdb" + ], + "homepage": "https://pipedream.com/apps/kvdb", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/kvstore_io/README.md b/components/kvstore_io/README.md new file mode 100644 index 0000000000000..78601ff635bd8 --- /dev/null +++ b/components/kvstore_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The KVstore.io API lets users store and retrieve key-value pairs over a simple REST API, making it an ideal tool for serverless data storage and retrieval. On Pipedream, you can integrate this API into workflows to manage state, cache data, or coordinate information between different steps or services. Its simplicity and ease of use make it a versatile component in creating efficient and scalable serverless applications. + +# Example Use Cases + +- **User Session Management**: Track user sessions across multiple API calls by storing session data in KVstore.io. Update or invalidate sessions directly within a workflow after a user logs in or out. + +- **Feature Flag Implementation**: Store feature flags in KVstore.io and use them within Pipedream workflows to toggle features in an application dynamically. This can allow for A/B testing or gradual feature rollouts. + +- **Aggregating Webhook Data**: Collect data from webhooks and store it in KVstore.io. Use Pipedream workflows to process this data, perform analytics, and then push the results to another service like Slack or a database. diff --git a/components/kvstore_io/kvstore_io.app.mjs b/components/kvstore_io/kvstore_io.app.mjs new file mode 100644 index 0000000000000..690625183af04 --- /dev/null +++ b/components/kvstore_io/kvstore_io.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "kvstore_io", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/kvstore_io/package.json b/components/kvstore_io/package.json new file mode 100644 index 0000000000000..6f69f105a6634 --- /dev/null +++ b/components/kvstore_io/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/kvstore_io", + "version": "0.0.1", + "description": "Pipedream KVstore.io Components", + "main": "kvstore_io.app.mjs", + "keywords": [ + "pipedream", + "kvstore_io" + ], + "homepage": "https://pipedream.com/apps/kvstore_io", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/kwtsms/README.md b/components/kwtsms/README.md new file mode 100644 index 0000000000000..1c70486894d7b --- /dev/null +++ b/components/kwtsms/README.md @@ -0,0 +1,11 @@ +# Overview + +The kwtsms API provides SMS services that allow you to send text messages globally. Integrating this API with Pipedream can make your workflows smarter by incorporating SMS notifications, alerts, and marketing communications. With Pipedream's serverless platform, you can trigger these SMS actions based on a variety of events, such as form submissions, ecommerce transactions, or app notifications, unleashing a host of automation possibilities that save time and enhance engagement. + +# Example Use Cases + +- **Order Confirmation Texts**: Automatically send SMS confirmations when a customer completes an order. Combine kwtsms with Shopify or WooCommerce on Pipedream to send custom messages post-purchase. + +- **Appointment Reminders**: Set up a workflow that sends SMS reminders to clients for upcoming appointments. This can be linked with Google Calendar events in Pipedream, ensuring clients receive timely notifications. + +- **System Outage Alerts**: Create a rapid alert system that texts IT staff when system downtime is detected. Pair kwtsms with monitoring apps like Datadog in Pipedream to notify when there's an issue that needs immediate attention. diff --git a/components/kyvio/README.md b/components/kyvio/README.md new file mode 100644 index 0000000000000..291d2b5095be2 --- /dev/null +++ b/components/kyvio/README.md @@ -0,0 +1,11 @@ +# Overview + +The Kyvio API allows for automation and integration of Kyvio's suite of tools for building online businesses, including smart memberships, funnels, and email marketing. By leveraging the Kyvio API on Pipedream, you can create automated workflows to streamline operations such as updating member details, managing subscriptions, tracking email campaigns, and more. Pipedream's serverless platform enables you to connect Kyvio with hundreds of other apps to further enhance your business processes with event-driven actions. + +# Example Use Cases + +- **Member Lifecycle Management**: Automate the process of user onboarding by syncing new Kyvio members to a Google Sheets document, and triggering welcome emails via SendGrid. This ensures a smooth start to the member experience and keeps your records up to date. + +- **Course Enrollment and Access Control**: Create a workflow that adds newly enrolled students from a platform like Teachable to Kyvio's smart membership program. This can include not only the enrollment but also the management of access levels based on the courses they've signed up for. + +- **Marketing Campaign Analysis**: After sending out a campaign through Kyvio's email system, use Pipedream to connect to Google Analytics and track the effectiveness of your emails. Analyze open rates, click-through rates, and conversions to refine your marketing strategies. diff --git a/components/l2s/actions/create-shortened-url/create-shortened-url.mjs b/components/l2s/actions/create-shortened-url/create-shortened-url.mjs new file mode 100644 index 0000000000000..6b1b0de8aaab8 --- /dev/null +++ b/components/l2s/actions/create-shortened-url/create-shortened-url.mjs @@ -0,0 +1,89 @@ +import { parseObject } from "../../common/utils.mjs"; +import l2s from "../../l2s.app.mjs"; + +export default { + key: "l2s-create-shortened-url", + name: "Create Shortened URL", + description: "Generates a shortened URL utilizing L2S capabilities. [See the documentation](https://docs.l2s.is/)", + version: "0.0.1", + type: "action", + props: { + l2s, + url: { + type: "string", + label: "URL", + description: "The URL to be shortened", + }, + customKey: { + type: "string", + label: "Custom Key", + description: "Custom key for the shortened URL", + optional: true, + }, + utmSource: { + type: "string", + label: "UTM Source", + description: "UTM source parameter", + optional: true, + }, + utmMedium: { + type: "string", + label: "UTM Medium", + description: "UTM medium parameter", + optional: true, + }, + utmCampaign: { + type: "string", + label: "UTM Campaign", + description: "UTM campaign parameter", + optional: true, + }, + utmTerm: { + type: "string", + label: "UTM Term", + description: "UTM term parameter", + optional: true, + }, + utmContent: { + type: "string", + label: "UTM Content", + description: "UTM content parameter", + optional: true, + }, + title: { + type: "string", + label: "Title", + description: "Title for the shortened URL", + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "Tags associated with the URL", + optional: true, + }, + }, + async run({ $ }) { + const { + l2s, + tags, + ...data + } = this; + + if (tags) { + data.tags = parseObject(tags); + } + + const { response: { data: response } } = await l2s.shortenUrl({ + $, + data, + }); + + const shortUrl = `https://l2s.is/${response.key}`; + $.export("$summary", "URL shortened successfully"); + return { + short_url: shortUrl, + ...response, + }; + }, +}; diff --git a/components/l2s/common/utils.mjs b/components/l2s/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/l2s/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/l2s/l2s.app.mjs b/components/l2s/l2s.app.mjs new file mode 100644 index 0000000000000..9bee2e562f036 --- /dev/null +++ b/components/l2s/l2s.app.mjs @@ -0,0 +1,32 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "l2s", + methods: { + _baseUrl() { + return "https://api.l2s.is"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + shortenUrl(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/url", + ...opts, + }); + }, + }, +}; diff --git a/components/l2s/package.json b/components/l2s/package.json new file mode 100644 index 0000000000000..3fdf999b84f21 --- /dev/null +++ b/components/l2s/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/l2s", + "version": "0.1.0", + "description": "Pipedream L2S Components", + "main": "l2s.app.mjs", + "keywords": [ + "pipedream", + "l2s" + ], + "homepage": "https://pipedream.com/apps/l2s", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} diff --git a/components/l3mbda/README.md b/components/l3mbda/README.md new file mode 100644 index 0000000000000..67ed73a91f110 --- /dev/null +++ b/components/l3mbda/README.md @@ -0,0 +1,11 @@ +# Overview + +The l3mbda API provides a platform to run JavaScript functions in the cloud, allowing you to execute code without setting up servers. On Pipedream, you can leverage the l3mbda API to create dynamic, serverless workflows. This enables you to run custom JavaScript functions as part of an automated process, integrate with other APIs, manipulate data, and respond to webhooks. + +# Example Use Cases + +- **Dynamic Content Generation**: Use l3mbda to run custom algorithms that generate personalized content, then pipe the output to a service like SendGrid to craft tailored email campaigns within Pipedream. + +- **Data Processing and Enrichment**: Trigger a Pipedream workflow with a webhook, process the incoming data with l3mbda's custom JavaScript functions, enrich it with third-party APIs like Clearbit for additional insights, and store the results in a Google Sheet. + +- **Automated Moderation Tool**: Create a Pipedream workflow that listens for new user-submitted content from platforms like Slack or Discord, run it through l3mbda to filter out inappropriate material using custom logic, and then post the approved content back to the platform or alert admins for manual review. diff --git a/components/labs64_netlicensing/README.md b/components/labs64_netlicensing/README.md index 43dbbdadb041c..939c770c95bc1 100644 --- a/components/labs64_netlicensing/README.md +++ b/components/labs64_netlicensing/README.md @@ -1,12 +1,11 @@ # Overview -With the Labs64 NetLicensing API you can easily build applications that: +The Labs64 NetLicensing API is a sophisticated software licensing service that enables you to manage product licenses and configurations seamlessly. Using Pipedream, you can tap into this power to automate license creation, validation, and tracking. This enables you to integrate licensing operations into your sales, deployment, and customer support workflows, ensuring consistent and automated management of software licenses across customer lifecycles. -- Manage your products and licenses -- Validate licenses -- Generate license keys -- Register licensees and track their activities +# Example Use Cases -So whether you need to develop a simple licensing solution for your product or -a more complex one with many different features, the NetLicensing API has you -covered. +- **Automated License Provisioning**: Trigger a workflow on Pipedream that automatically generates and assigns licenses to new customers when a sale is made through Stripe. The workflow captures the Stripe payment event, creates a new license in NetLicensing, and sends the license details to the customer via Email by SendGrid. + +- **License Validation for Access Control**: Set up a Pipedream workflow that listens for user login attempts in your application (via a webhook). The workflow checks the user's license validity through NetLicensing API and, based on the result, either grants access or triggers an alert to your Slack channel for a follow-up. + +- **Customer Support Automation**: Integrate a Pipedream workflow with Zendesk that automatically checks customer's license status when they submit a support ticket. If a license issue is detected, the workflow updates the ticket and informs support staff, or it can automatically generate a response to guide the customer through troubleshooting or renewal steps. diff --git a/components/labsmobile/README.md b/components/labsmobile/README.md new file mode 100644 index 0000000000000..7a0e95746f836 --- /dev/null +++ b/components/labsmobile/README.md @@ -0,0 +1,11 @@ +# Overview + +LabsMobile API enables seamless SMS messaging for various applications, perfect for notifications, authentication, or marketing. Through Pipedream, you can integrate LabsMobile with countless services and trigger SMS workflows using HTTP requests, scheduled jobs, or event-based setups. Pipedream's serverless platform simplifies these processes, allowing you to focus on building efficient, scalable messaging pipelines. + +# Example Use Cases + +- **Customer Order Alerts**: Automate SMS notifications to customers when their orders have been processed or shipped. Hook into an e-commerce platform like Shopify, listening for new order events, then use LabsMobile API to send real-time updates to customers. + +- **Two-Factor Authentication (2FA)**: Enhance security with 2FA by sending a unique code via SMS each time a user performs a sensitive action. Trigger LabsMobile API from a user management system like Auth0 when a 2FA request is invoked. + +- **Appointment Reminders**: Connect with a calendar service like Google Calendar to send SMS reminders for upcoming appointments. Use Pipedream's cron scheduling to check for upcoming events daily and leverage LabsMobile API to notify clients timely. diff --git a/components/labsmobile/actions/send-sms/send-sms.mjs b/components/labsmobile/actions/send-sms/send-sms.mjs new file mode 100644 index 0000000000000..ae83d69966b3d --- /dev/null +++ b/components/labsmobile/actions/send-sms/send-sms.mjs @@ -0,0 +1,144 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import labsmobile from "../../labsmobile.app.mjs"; + +export default { + key: "labsmobile-send-sms", + name: "Send SMS", + description: "Sends a new SMS message. [See the documentation](https://apidocs.labsmobile.com/)", + version: "0.0.1", + type: "action", + props: { + labsmobile, + msisdn: { + type: "integer[]", + label: "Msisdn", + description: "Parameter that includes a mobile number recipient. The number must include the country code without '+' ó '00'. Each customer account has a maximum number of msisdn per sending. See the terms of your account to see this limit.", + }, + message: { + type: "string", + label: "Message", + description: "The message to be sent. The maximum message length is 160 characters. Only characters in the GSM 3.38 7bit alphabet, found at the end of this document, are valid. Otherwise you must send `ucs2` parameter.", + }, + tpoa: { + type: "string", + label: "Tpoa", + description: "Sender of the message. May have a numeric (maximum length, 14 digits) or an alphanumeric (maximum capacity, 11 characters) value. The messaging platform assigns a default sender if this parameter is not included. By including the sender's mobile number, the message recipient can easily respond from their mobile phone with the \"Reply\" button. The sender can only be defined in some countries due to the restrictions of the operators. Otherwise the sender is a random numeric value.", + optional: true, + }, + subid: { + type: "string", + label: "Subid", + description: "Message ID included in the ACKs (delivery confirmations). It is a unique delivery ID issued by the API client. It has a maximum length of 20 characters.", + optional: true, + }, + label: { + type: "string", + label: "Label", + description: "Identifies the message for statistical purposes. WebSMS and other applications use this field to organize and record the message. Maximum capacity of 255 characters. Typical information contained in this field: user that has sent the message, application or module, etc. ...", + optional: true, + }, + test: { + type: "boolean", + label: "Test", + description: "If the value is `true`, the message will be considered a test. It will not be sent to the GSM network and, therefore, will not be received on any mobile devices. However, these messages are accessible using online search tools. This parameter is intended to enable performing integration tests without an associated cost. Operator and handset confirmations will not be received.", + optional: true, + }, + ackurl: { + type: "string", + label: "Ackurl", + description: "URL to which the corresponding delivery confirmation notifications will be sent. In the preferences section of WebSMS application you can set the default value for ackurl for all cases without having to send this parameter in each sending.", + optional: true, + }, + shortlink: { + type: "boolean", + label: "Shortlink", + description: "If this field is present in the message and has a value of `true` the first URL would be replace by a short link of LabsMobile (format: http://lm0.es/XXXXX). The statistics of visits can be seen in WebSMS application or can be received in an url with the parameter clickurl.", + optional: true, + }, + clickurl: { + type: "string", + label: "Click URL", + description: "URL to which the corresponding click confirmation notifications will be sent if the parameter shortlink is enabled. In the preferences section of WebSMS application you can set the default value for clickurl for all cases without having to send this parameter in each sending.", + optional: true, + }, + scheduled: { + type: "string", + label: "Scheduled", + description: "The message will be sent at the date and time indicated in this field. If this field has not been specified, the message will be sent immediately. Format: YYYY-MM-DD HH:MM:SS. **IMPORTANT: the value of this field must be expressed using GMT time.**", + optional: true, + }, + long: { + type: "boolean", + label: "Long", + description: "If this field is present in the message and has a value of `true`, the message field may contain up to 459 characters. Each 153 characters will be considered a single message (in terms of charges) and the recipient will receive one, concatenated message. **IMPORTANT: This option is only available in some countries due to the restrictions of the operators.**", + optional: true, + }, + crt: { + type: "string", + label: "CRT", + description: "If this field is present in the message, it will be considered a certified SMS message. An email with the delivery certificate in an attachment will be sent to the address contained in this parameter. **IMPORTANT: This option is only implemented in some countries.**", + optional: true, + }, + crtId: { + type: "string", + label: "CRT ID", + description: "If the message is a certified SMS message this field will be set as the tax identification number of the sender company or organization. You would see this value in the certificate in PDF format.", + optional: true, + }, + crtName: { + type: "string", + label: "CRT Name", + description: "If the message is a certified SMS message this field will be set as the name of the sender company or organization. You would see this value in the certificate in PDF format.", + optional: true, + }, + ucs2: { + type: "boolean", + label: "UCS 2", + description: "If this field is present in the message the message can contain any character in the UCS-2 alphabet. In this case the capacity of the message is 70 characters and can be sent concatenated to a maximum of 500 characters.", + optional: true, + }, + nofilter: { + type: "boolean", + label: "No Filter", + description: "If this field is present the platform won't apply the duplicate filter, so no message will be blocked by this filter.", + optional: true, + }, + parameters: { + type: "object", + label: "Parameters", + description: "This field contains values to replace parameters in the text of the message. The message can contain one or more parameters (with the following format: **%name%**, **%fieldn%**, etc.). It is necessary to specify the value of each parameter for each of the recipients or establish a default value. Example: **{\"parameters\": [{\"name\": {\"msisdn\":\"12015550123\", \"value\":\"John\"}}, {\"name\": {\"msisdn\":\"default\", \"value\":\"Client\"}}]}**", + optional: true, + }, + }, + async run({ $ }) { + const { + labsmobile, + msisdn, + parameters, + crtId, + crtName, + ...data + } = this; + + const response = await labsmobile.sendSMS({ + $, + data: { + recipient: parseObject(msisdn).map((item) => ({ + msisdn: item, + })), + parameters: parameters && parseObject(parameters), + crt_id: crtId, + crt_name: crtName, + ...data, + }, + }); + + if (response.code != "0") { + throw new ConfigurationError(response.message); + } + + $.export("$summary", `Successfully sent SMS with Id: ${response.subid}`); + return response; + }, +}; diff --git a/components/labsmobile/common/utils.mjs b/components/labsmobile/common/utils.mjs new file mode 100644 index 0000000000000..154701004e72a --- /dev/null +++ b/components/labsmobile/common/utils.mjs @@ -0,0 +1,18 @@ +export const parseObject = (obj) => { + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + return JSON.parse(obj); + } + return obj; +}; diff --git a/components/labsmobile/labsmobile.app.mjs b/components/labsmobile/labsmobile.app.mjs new file mode 100644 index 0000000000000..7771e02008956 --- /dev/null +++ b/components/labsmobile/labsmobile.app.mjs @@ -0,0 +1,36 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "labsmobile", + methods: { + _baseUrl() { + return "https://api.labsmobile.com/json"; + }, + _auth() { + return { + username: this.$auth.email, + password: this.$auth.api_token_or_password, + }; + }, + _makeRequest({ + $ = this, path = "", ...otherOpts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: { + "Cache-Control": "no-cache", + }, + auth: this._auth(), + ...otherOpts, + }); + }, + sendSMS(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/send", + ...opts, + }); + }, + }, +}; diff --git a/components/labsmobile/package.json b/components/labsmobile/package.json new file mode 100644 index 0000000000000..69ad5e82eeed9 --- /dev/null +++ b/components/labsmobile/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/labsmobile", + "version": "0.1.0", + "description": "Pipedream LabsMobile Components", + "main": "labsmobile.app.mjs", + "keywords": [ + "pipedream", + "labsmobile" + ], + "homepage": "https://pipedream.com/apps/labsmobile", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" + } +} + diff --git a/components/lagrowthmachine/README.md b/components/lagrowthmachine/README.md index 8b87d257f74d8..4e591ce277b05 100644 --- a/components/lagrowthmachine/README.md +++ b/components/lagrowthmachine/README.md @@ -1,7 +1,11 @@ # Overview -LaGrowthMachine is an API that enables you to build websites and applications -that help you grow your business. With LaGrowthMachine, you can easily add -features like social media integration, email marketing, and ecommerce to your -website or application. You can also use LaGrowthMachine to create custom -applications for your business. +The LaGrowthMachine API on Pipedream allows you to craft powerful sales and marketing automations by leveraging its outreach and follow-up capabilities directly within serverless workflows. With Pipedream's integration, you can trigger actions in LaGrowthMachine based on events from other apps, process data, and automate sequences of tasks to enhance lead generation and engagement strategies. + +# Example Use Cases + +- **Automated Lead Enrichment Workflow**: Integrate LaGrowthMachine with Pipedream's trigger for new spreadsheet rows in Google Sheets. Whenever a new lead is added to your sheet, automatically enrich the data using LaGrowthMachine's API to fetch additional contact information and then trigger personalized outreach sequences based on the enriched data. + +- **Multi-Channel Outreach Campaigns**: Connect LaGrowthMachine with Pipedream's Twitter trigger to listen for specific hashtags or mentions. Use this data to create or update leads in LaGrowthMachine and trigger a multi-channel outreach sequence involving personalized emails, LinkedIn messages, and other communications, ensuring no potential lead slips through the cracks. + +- **Customer Feedback Loop**: Set up a webhook in Pipedream to capture responses from a customer satisfaction survey platform like Typeform. Pass this feedback to LaGrowthMachine to automatically categorize and tag customers based on their sentiments. Then, trigger tailored follow-up sequences to address concerns, ask for reviews, or upsell services based on the customer's experience. diff --git a/components/lagrowthmachine/package.json b/components/lagrowthmachine/package.json new file mode 100644 index 0000000000000..4f0808253e64f --- /dev/null +++ b/components/lagrowthmachine/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/lagrowthmachine", + "version": "0.6.0", + "description": "Pipedream lagrowthmachine Components", + "main": "lagrowthmachine.app.mjs", + "keywords": [ + "pipedream", + "lagrowthmachine" + ], + "homepage": "https://pipedream.com/apps/lagrowthmachine", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/lahar/README.md b/components/lahar/README.md new file mode 100644 index 0000000000000..3bb36cab51f0f --- /dev/null +++ b/components/lahar/README.md @@ -0,0 +1,11 @@ +# Overview + +The Lahar API enables users to automate and integrate their marketing efforts, facilitating lead management, email campaigns, and analytics. On Pipedream, you can leverage these capabilities to build serverless workflows that respond to events in real-time, sync data across various platforms, and create custom marketing automations without managing infrastructure. + +# Example Use Cases + +- **Lead Syncing with CRM**: Automatically sync new leads captured through Lahar to a CRM platform like Salesforce. Each time a lead is created in Lahar, a Pipedream workflow triggers to insert or update the lead's details in Salesforce, ensuring the sales team has immediate access to new prospects. + +- **Email Campaign Trigger from eCommerce Actions**: Connect Lahar with an eCommerce platform like Shopify. Whenever a customer completes a purchase, a Pipedream workflow can trigger an email campaign in Lahar to nurture the customer relationship with personalized follow-ups or product recommendations. + +- **Form Submission to Support Ticket**: Integrate Lahar's form submission feature with a customer support tool like Zendesk. Use Pipedream to convert new form submissions into support tickets, automatically categorizing and prioritizing issues based on the form data provided by the user. diff --git a/components/lambdatest/README.md b/components/lambdatest/README.md new file mode 100644 index 0000000000000..c8e23a9b12e25 --- /dev/null +++ b/components/lambdatest/README.md @@ -0,0 +1,11 @@ +# Overview + +LambdaTest API on Pipedream allows developers to integrate and automate browser testing directly within their development workflows. Using this API, you can create, manage, and execute Selenium, Cypress, and other tests on a scalable cloud grid, facilitating continuous testing with minimal setup. This integration is especially beneficial for continuous integration/continuous deployment (CI/CD) pipelines, enabling teams to ensure that applications perform as expected across multiple browser environments before deployment. + +# Example Use Cases + +- **Automate Selenium Test Execution on Code Commit**: Trigger Selenium tests on LambdaTest whenever new code is committed to a GitHub repository. Use Pipedream's GitHub trigger to detect commits, then automatically initiate a series of browser compatibility tests on LambdaTest. This helps ensure that recent changes pass all browser-based tests. + +- **Scheduled Browser Compatibility Testing**: Use Pipedream’s scheduled workflows to initiate browser compatibility tests on LambdaTest at regular intervals, such as nightly or weekly. This is ideal for ensuring ongoing compatibility during long-term projects without needing manual intervention each time. + +- **Error Reporting Integration**: Combine LambdaTest with Slack using Pipedream. Configure a workflow where, if a test fails on LambdaTest, an automatic notification is sent to a designated Slack channel. This immediate feedback loop enables teams to quickly address issues, improving both development speed and software quality. diff --git a/components/lambdatest/lambdatest.app.mjs b/components/lambdatest/lambdatest.app.mjs new file mode 100644 index 0000000000000..3c10c81899a30 --- /dev/null +++ b/components/lambdatest/lambdatest.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "lambdatest", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/lambdatest/package.json b/components/lambdatest/package.json new file mode 100644 index 0000000000000..c5e4e81b7fbca --- /dev/null +++ b/components/lambdatest/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/lambdatest", + "version": "0.0.1", + "description": "Pipedream LambdaTest Components", + "main": "lambdatest.app.mjs", + "keywords": [ + "pipedream", + "lambdatest" + ], + "homepage": "https://pipedream.com/apps/lambdatest", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/langbase/actions/create-memory/create-memory.mjs b/components/langbase/actions/create-memory/create-memory.mjs new file mode 100644 index 0000000000000..5cf0ca5365b64 --- /dev/null +++ b/components/langbase/actions/create-memory/create-memory.mjs @@ -0,0 +1,38 @@ +import app from "../../langbase.app.mjs"; + +export default { + key: "langbase-create-memory", + name: "Create Memory", + description: "Create a new organization memory by sending the memory data. [See the documentation](https://langbase.com/docs/api-reference/memory/create)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "name", + ], + }, + description: { + propDefinition: [ + app, + "description", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.createMemory({ + $, + data: { + name: this.name, + description: this.description, + }, + }); + + $.export("$summary", `Successfully created memory ${this.name}`); + + return response; + }, +}; diff --git a/components/langbase/actions/delete-memory/delete-memory.mjs b/components/langbase/actions/delete-memory/delete-memory.mjs new file mode 100644 index 0000000000000..cba327837a7db --- /dev/null +++ b/components/langbase/actions/delete-memory/delete-memory.mjs @@ -0,0 +1,29 @@ +import app from "../../langbase.app.mjs"; + +export default { + key: "langbase-delete-memory", + name: "Delete Memory", + description: "Delete an existing memory on Langbase. [See the documentation](https://langbase.com/docs/api-reference/memory/delete)", + version: "0.0.1", + type: "action", + props: { + app, + memoryName: { + propDefinition: [ + app, + "memoryName", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.deleteMemory({ + $, + memoryName: this.memoryName, + }); + + $.export("$summary", `Successfully deleted memory named ${this.memoryName}`); + + return response; + }, +}; diff --git a/components/langbase/actions/list-memories/list-memories.mjs b/components/langbase/actions/list-memories/list-memories.mjs new file mode 100644 index 0000000000000..fc47e02888c14 --- /dev/null +++ b/components/langbase/actions/list-memories/list-memories.mjs @@ -0,0 +1,22 @@ +import app from "../../langbase.app.mjs"; + +export default { + key: "langbase-list-memories", + name: "List Memories", + description: "Get a list of memory sets on Langbase. [See the documentation](https://langbase.com/docs/api-reference/memory/list)", + version: "0.0.1", + type: "action", + props: { + app, + }, + + async run({ $ }) { + const response = await this.app.listMemories({ + $, + }); + + $.export("$summary", `Successfully retrieved ${response.memorySets.length} memorysets`); + + return response; + }, +}; diff --git a/components/langbase/langbase.app.mjs b/components/langbase/langbase.app.mjs new file mode 100644 index 0000000000000..24547f9faf43e --- /dev/null +++ b/components/langbase/langbase.app.mjs @@ -0,0 +1,77 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "langbase", + propDefinitions: { + memoryName: { + type: "string", + label: "Memory Name", + description: "The name of the memory", + async options() { + const response = await this.listMemories(); + const memoryNames = response.memorySets; + return memoryNames.map(({ + name, description, + }) => ({ + label: description, + value: name, + })); + }, + }, + name: { + type: "string", + label: "Name", + description: "Name of the memory", + }, + description: { + type: "string", + label: "Description", + description: "Short description of the memory", + }, + }, + methods: { + _baseUrl() { + return "https://api.langbase.com/beta"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "Authorization": `Bearer ${this.$auth.org_api_key}`, + "Accept": "application/json", + }, + }); + }, + async createMemory(args = {}) { + return this._makeRequest({ + path: `/org/${this.$auth.org}/memorysets`, + method: "post", + ...args, + }); + }, + async deleteMemory({ + memoryName, ...args + }) { + return this._makeRequest({ + path: `/memorysets/${this.$auth.org}/${memoryName}`, + method: "delete", + ...args, + }); + }, + async listMemories(args = {}) { + return this._makeRequest({ + path: `/org/${this.$auth.org}/memorysets`, + ...args, + }); + }, + }, +}; diff --git a/components/langbase/package.json b/components/langbase/package.json new file mode 100644 index 0000000000000..91c547399b30e --- /dev/null +++ b/components/langbase/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/langbase", + "version": "0.1.0", + "description": "Pipedream Langbase Components", + "main": "langbase.app.mjs", + "keywords": [ + "pipedream", + "langbase" + ], + "homepage": "https://pipedream.com/apps/langbase", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/langfuse/langfuse.app.mjs b/components/langfuse/langfuse.app.mjs new file mode 100644 index 0000000000000..083bdbf5ace7f --- /dev/null +++ b/components/langfuse/langfuse.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "langfuse", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/langfuse/package.json b/components/langfuse/package.json new file mode 100644 index 0000000000000..e51adcc6c3690 --- /dev/null +++ b/components/langfuse/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/langfuse", + "version": "0.0.1", + "description": "Pipedream Langfuse Components", + "main": "langfuse.app.mjs", + "keywords": [ + "pipedream", + "langfuse" + ], + "homepage": "https://pipedream.com/apps/langfuse", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/laposta/README.md b/components/laposta/README.md index 3f1f796af9bb5..82106befff2aa 100644 --- a/components/laposta/README.md +++ b/components/laposta/README.md @@ -1,5 +1,11 @@ # Overview -With the Laposta API you can integration Laposta's powerful email marketing -features into your own application. With Laposta you can easily create and send -newsletters, manage subscribers and track statistics. +Laposta is a robust email marketing tool, empowering users to build, send, and analyze email campaigns with ease. Leveraging the Laposta API on Pipedream, you can automate email list management, subscriber data synchronization, and campaign performance tracking. The API enables developers to create dynamic, serverless workflows that react to events across various apps, enrich contact information, or even trigger custom email sequences based on user behavior or other external cues. + +# Example Use Cases + +- **Email List Sync with E-Commerce Platforms**: Automatically add new customers from an e-commerce platform like Shopify to a specific Laposta mailing list. When a new order is placed or a customer account is created, trigger a Pipedream workflow that uses the Laposta API to update your subscriber list. + +- **Survey Responses to Email Segmentation**: After a user completes a survey through platforms like Typeform, use their responses to segment them in Laposta. Pipedream can catch the survey submission webhook, process the data, and use the Laposta API to tag subscribers for targeted campaigns based on their interests or feedback. + +- **Campaign Performance to Analytics Dashboards**: Post campaign performance data from Laposta to a Google Sheets or a BI tool like Tableau. Set up a Pipedream workflow to periodically retrieve campaign analytics from Laposta and log the data, helping you visualize trends and optimize future email marketing strategies. diff --git a/components/larger_io/README.md b/components/larger_io/README.md index 51b25c0563a3f..ceccbc4a833be 100644 --- a/components/larger_io/README.md +++ b/components/larger_io/README.md @@ -1,12 +1,11 @@ # Overview -Larger.io is a powerful API that allows you to build all sorts of applications. -Here are a few examples: - -- A social networking site -- An online marketplace -- A content management system -- A blog platform -- A video sharing site -- An image sharing site -- A news aggregator +The Larger.io API taps into extensive data on technologies used by websites around the globe. With this API, you can detect the web technologies and tools employed by various online domains. This is particularly helpful for competitive analysis, market research, and lead generation. By leveraging Pipedream's serverless platform, you can create automated workflows that respond to specific triggers, analyze data with built-in code steps, and interact with an array of apps to streamline business processes. + +# Example Use Cases + +- **Competitive Technology Alerts**: Set up a Pipedream workflow that monitors specific competitors’ websites for technology stack changes with Larger.io. Use this information to trigger custom alerts (via email, Slack, or SMS) to keep your product and marketing teams informed about competitor movements in real-time. + +- **Lead Generation for Sales Teams**: Craft a workflow where Larger.io identifies websites using a particular technology stack compatible with your services. Then, enrich the lead data with LinkedIn or Clearbit, and automatically import the list into a CRM tool like Salesforce or HubSpot, optimizing your sales pipeline. + +- **Market Trend Analysis**: Construct a Pipedream workflow that periodically fetches technology adoption data across industry domains using Larger.io, feeds it into a Google Sheet or Data Studio dashboard, and performs an analysis to spot market trends, thereby empowering strategic decision-making. diff --git a/components/larger_io/package.json b/components/larger_io/package.json new file mode 100644 index 0000000000000..192686b64e40e --- /dev/null +++ b/components/larger_io/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/larger_io", + "version": "0.6.0", + "description": "Pipedream larger_io Components", + "main": "larger_io.app.mjs", + "keywords": [ + "pipedream", + "larger_io" + ], + "homepage": "https://pipedream.com/apps/larger_io", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/lastpass/README.md b/components/lastpass/README.md new file mode 100644 index 0000000000000..6e612b62c5755 --- /dev/null +++ b/components/lastpass/README.md @@ -0,0 +1,11 @@ +# Overview + +The LastPass Enterprise API lets you automate and manage user access, shared folders, and security settings within your LastPass Enterprise account. With this API integrated in Pipedream, you can create workflows that streamline your password management tasks, enforce security policies, and sync with your organization's user directory. It's a secure way to manage credentials across your company's workforce, without constantly diving into the LastPass dashboard. + +# Example Use Cases + +- **Automated User Provisioning and Deprovisioning**: Configure a workflow that listens for new employee entries in your HR system, like BambooHR or Workday. When a new employee is added, automatically create a LastPass Enterprise user account with pre-defined access. Conversely, when an employee leaves, ensure their LastPass access is revoked to maintain security. + +- **Security Compliance Reporting**: Keep track of who has access to what, and when changes occur. Use the LastPass Enterprise API to fetch logs and access reports, then send this data to a Google Sheet or a database like PostgreSQL on Pipedream. This workflow can be scheduled to run at regular intervals, providing up-to-date compliance information. + +- **Password Rotation Enforcement**: Pair LastPass with a cron job workflow on Pipedream that triggers a password update action for shared accounts. This workflow could interface with your development tools, like GitHub or AWS, to rotate credentials for service accounts or CI/CD pipelines, aligning with best security practices and minimizing risk. diff --git a/components/lattice/README.md b/components/lattice/README.md new file mode 100644 index 0000000000000..b62ab7d12bbb2 --- /dev/null +++ b/components/lattice/README.md @@ -0,0 +1,11 @@ +# Overview + +The Lattice API enables you to tap into a powerful people management platform, ideal for handling performance reviews, goal setting, and employee engagement within your organization. When you combine Lattice with Pipedream, you unlock the potential for automating various HR processes, synchronizing employee data across systems, and generating real-time insights. Pipedream acts as a facilitator, providing you with a serverless platform where you can connect Lattice to a multitude of other apps and create custom workflows tailored to your company's needs. + +# Example Use Cases + +- **Synchronize New Employees to HRIS:** When a new employee is added in Lattice, this workflow can automatically update your Human Resources Information System (HRIS), ensuring that employee records are always synced across your internal systems. + +- **Automate Performance Review Reminders:** Set up a workflow that listens for upcoming performance reviews in Lattice and sends out automated reminders to managers and employees via email or a messaging app like Slack, keeping everyone informed and on schedule. + +- **Aggregate Feedback for Reporting:** Collect feedback and reviews submitted through Lattice, and use Pipedream to aggregate and push this data into a BI tool like Google Sheets or Tableau, allowing for in-depth analysis and easier reporting on employee performance. diff --git a/components/lattice/lattice.app.mjs b/components/lattice/lattice.app.mjs new file mode 100644 index 0000000000000..e9dd60c4f6a65 --- /dev/null +++ b/components/lattice/lattice.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "lattice", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/lattice/package.json b/components/lattice/package.json new file mode 100644 index 0000000000000..67f105bd057a1 --- /dev/null +++ b/components/lattice/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/lattice", + "version": "0.0.1", + "description": "Pipedream Lattice Components", + "main": "lattice.app.mjs", + "keywords": [ + "pipedream", + "lattice" + ], + "homepage": "https://pipedream.com/apps/lattice", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/launch27/README.md b/components/launch27/README.md index 3a05a391db85e..3f5c8f43e8e2f 100644 --- a/components/launch27/README.md +++ b/components/launch27/README.md @@ -1,12 +1,11 @@ # Overview -With the Launch27 API, you can build a wide variety of applications and -integrations. Here are just a few examples: - -- A simple booking form that allows customers to book appointments with your - business -- A complex appointment scheduler that lets customers book appointments with - multiple businesses -- An integration with your existing appointment scheduling software -- A system that allows customers to pay for their appointments online -- A way for customers to cancel or reschedule their appointments +Launch27 is a robust platform aimed at helping businesses manage bookings and appointments seamlessly. Leveraging the Launch27 API on Pipedream, users can automate various business processes by connecting their scheduling system with other apps and services. This can include automations like syncing new bookings with a Google Calendar, triggering SMS or email reminders when an appointment is nearing, or compiling customer feedback collected through Launch27 into a CRM platform. + +# Example Use Cases + +- **Automated Booking Confirmation**: When a new booking is made on Launch27, trigger an automated email or SMS to the customer confirming their appointment, using integration with SendGrid or Twilio on Pipedream. + +- **Dynamic Calendar Management**: Sync new Launch27 bookings to a Google Calendar to keep all appointments up-to-date and avoid double-bookings. If a booking is canceled or rescheduled in Launch27, update the corresponding Google Calendar event automatically. + +- **Customer Feedback Aggregation**: After service completion, use Launch27 to solicit feedback, then collect and log these responses into a Google Sheet or a CRM like HubSpot. This data can be used to tailor services, address concerns, and improve overall customer satisfaction. diff --git a/components/launch27/package.json b/components/launch27/package.json new file mode 100644 index 0000000000000..9b25b61c919d9 --- /dev/null +++ b/components/launch27/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/launch27", + "version": "0.6.0", + "description": "Pipedream launch27 Components", + "main": "launch27.app.mjs", + "keywords": [ + "pipedream", + "launch27" + ], + "homepage": "https://pipedream.com/apps/launch27", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/launch_darkly_oauth/README.md b/components/launch_darkly_oauth/README.md index e25c3848c2b64..f10706d4e7090 100644 --- a/components/launch_darkly_oauth/README.md +++ b/components/launch_darkly_oauth/README.md @@ -1,17 +1,11 @@ # Overview -With Launch Darkly, you can easily and quickly add feature flags to your -applications. This is particularly useful when you want to test out a new -feature with a small group of users first, or when you want to slowly rollout a -new feature to all of your users. +Launch Darkly's API provides the means to automate feature flagging and manage experiments in your software delivery. This power, harnessed within Pipedream's serverless environment, can transform how you handle software deployment strategies, perform A/B testing, and control access to new features. The API's capabilities extend to updating flags, fetching flag statuses, and managing user segments, all of which can be integrated into sophisticated, automated workflows that react to external triggers or scheduled events. -Launch Darkly also has an Oauth API, which allows you to easily add feature -flags to your applications using Oauth. This is particularly useful when you -want to test out a new feature with a small group of users first, or when you -want to slowly rollout a new feature to all of your users. +# Example Use Cases -Some examples of what you can build with the Launch Darkly Oauth API include: +- **Automated Feature Rollout Based on Performance Metrics**: Use Launch Darkly's API to monitor feature performance via an analytics platform like New Relic. If a new feature's performance meets predefined criteria, automatically update the feature flag to roll out the feature to all users. -- A feature flag management system -- A system for easily rolling out new features to a small group of users -- A system for slowly rolling out new features to all users +- **Dynamic User Segment Management**: Sync Launch Darkly user segments with a CRM like Salesforce. When a user's status changes in Salesforce, use a Pipedream workflow to automatically update user segment membership in Launch Darkly, ensuring targeted feature flagging and personalization. + +- **Scheduled Feature Flag Clean-up**: Set up a Pipedream scheduled workflow to regularly call the Launch Darkly API to review feature flags. Flags that are no longer in use, based on a set of conditions like 'last updated' or 'active status', can be programmatically removed or archived to maintain a lean and efficient feature flag ecosystem. diff --git a/components/launch_darkly_oauth/package.json b/components/launch_darkly_oauth/package.json new file mode 100644 index 0000000000000..f60df55b9581d --- /dev/null +++ b/components/launch_darkly_oauth/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/launch_darkly_oauth", + "version": "0.6.0", + "description": "Pipedream launch_darkly_oauth Components", + "main": "launch_darkly_oauth.app.mjs", + "keywords": [ + "pipedream", + "launch_darkly_oauth" + ], + "homepage": "https://pipedream.com/apps/launch_darkly_oauth", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/launchdarkly/README.md b/components/launchdarkly/README.md index ad0e8f5fc9820..279fb40542e68 100644 --- a/components/launchdarkly/README.md +++ b/components/launchdarkly/README.md @@ -1,13 +1,11 @@ # Overview -With the LaunchDarkly API, you can easily create custom user experiences for -your application or website. You can use the API to segment your users based on -criteria such as location, device, or even custom attributes, and then change -the application experience for those segments in real-time. +The LaunchDarkly API provides programmatic control over feature flags and toggle management, allowing for real-time updates across different environments. By leveraging these capabilities with Pipedream, developers can automate feature rollouts, audience targeting, and performance monitoring. Through Pipedream's event-driven architecture, you can orchestrate workflows that react to changes in LaunchDarkly, sync feature flag data with other tools, and manage flag lifecycles with precision. -Here are a few examples of what you can build with the LaunchDarkly API: +# Example Use Cases -- A/B testing of new features -- Targeted content for specific user segments -- Dynamic Experiences based on user interactions -- Real-time analytics of user behavior +- **Sync Feature Flags with a Project Management Tool**: Automatically create a task in a project management app like Asana or JIRA when a new feature flag is created in LaunchDarkly. This ensures that new feature developments are tracked and managed alongside your team's workflow. + +- **Automate Feature Rollouts Based on Performance Metrics**: Connect LaunchDarkly with monitoring tools like Datadog. If performance metrics indicate a feature is performing well below a certain threshold, trigger a workflow to turn off the feature flag, roll back the feature, and alert the engineering team. + +- **Dynamic Audience Targeting with CRM Integration**: Integrate LaunchDarkly with a CRM like Salesforce. When a customer's status changes in the CRM, use Pipedream to update the user targeting in LaunchDarkly, dynamically altering the features available to that user or segment. diff --git a/components/launchdarkly/actions/evaluate-feature-flag/evaluate-feature-flag.mjs b/components/launchdarkly/actions/evaluate-feature-flag/evaluate-feature-flag.mjs new file mode 100644 index 0000000000000..310f7ad6f9c1d --- /dev/null +++ b/components/launchdarkly/actions/evaluate-feature-flag/evaluate-feature-flag.mjs @@ -0,0 +1,105 @@ +import app from "../../launchdarkly.app.mjs"; + +export default { + key: "launchdarkly-evaluate-feature-flag", + name: "Evaluate Feature Flag", + description: "Evaluates an existing feature flag for a specific user or in a general context. [See the documentation](https://apidocs.launchdarkly.com/tag/Contexts#operation/evaluateContextInstance).", + version: "0.0.1", + type: "action", + props: { + app, + projectKey: { + propDefinition: [ + app, + "project", + ], + }, + environmentKey: { + propDefinition: [ + app, + "environment", + ({ projectKey }) => ({ + projectKey, + }), + ], + }, + flagKey: { + propDefinition: [ + app, + "flag", + ({ + projectKey, environmentKey, + }) => ({ + projectKey, + environmentKey, + }), + ], + }, + contextKind: { + propDefinition: [ + app, + "contextKind", + ({ projectKey }) => ({ + projectKey, + }), + ], + }, + contextKey: { + label: "Context Key", + description: "The key of the context to evaluate the feature flag against.", + propDefinition: [ + app, + "context", + ({ + projectKey, environmentKey, flagKey, contextKind, + }) => ({ + projectKey, + environmentKey, + key: flagKey, + kind: contextKind, + }), + ], + }, + otherAttributes: { + type: "object", + label: "Other Attributes", + description: "Additional attributes to include in the context.", + optional: true, + }, + }, + methods: { + evaluateFeatureFlag({ + projectKey, environmentKey, ...args + }) { + return this.app.post({ + path: `/projects/${projectKey}/environments/${environmentKey}/flags/evaluate`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + evaluateFeatureFlag, + projectKey, + environmentKey, + contextKind, + contextKey, + otherAttributes, + } = this; + + const response = await evaluateFeatureFlag({ + $, + projectKey, + environmentKey, + data: { + key: contextKey, + kind: contextKind, + ...otherAttributes, + }, + }); + + $.export("$summary", `Successfully evaluated feature flag with \`${response.items.length}\` item(s).`); + + return response; + }, +}; diff --git a/components/launchdarkly/actions/toggle-feature-flag/toggle-feature-flag.mjs b/components/launchdarkly/actions/toggle-feature-flag/toggle-feature-flag.mjs new file mode 100644 index 0000000000000..890a97f43ecc7 --- /dev/null +++ b/components/launchdarkly/actions/toggle-feature-flag/toggle-feature-flag.mjs @@ -0,0 +1,77 @@ +import app from "../../launchdarkly.app.mjs"; + +export default { + key: "launchdarkly-toggle-feature-flag", + name: "Toggle Feature Flag", + description: "Toggles the status of a feature flag, switching it from active to inactive, or vice versa. [See the documentation](https://apidocs.launchdarkly.com/tag/Feature-flags#operation/patchFeatureFlag)", + version: "0.0.1", + type: "action", + props: { + app, + projectKey: { + propDefinition: [ + app, + "project", + ], + }, + environmentKey: { + propDefinition: [ + app, + "environment", + ({ projectKey }) => ({ + projectKey, + }), + ], + }, + featureFlagKey: { + propDefinition: [ + app, + "flag", + ({ + projectKey, environmentKey, + }) => ({ + projectKey, + environmentKey, + }), + ], + }, + }, + async run({ $ }) { + const { + app, + projectKey, + environmentKey, + featureFlagKey, + } = this; + + const { environments: { [environmentKey]: { on: isOn } } } = + await app.getFeatureFlag({ + $, + projectKey, + featureFlagKey, + }); + + const response = await app.updateFeatureFlag({ + $, + projectKey, + featureFlagKey, + headers: { + "Content-Type": "application/json; domain-model=launchdarkly.semanticpatch", + }, + data: { + environmentKey, + instructions: [ + { + kind: isOn + ? "turnFlagOff" + : "turnFlagOn", + }, + ], + }, + }); + + $.export("$summary", "Successfully toggled the feature flag."); + + return response; + }, +}; diff --git a/components/launchdarkly/actions/update-feature-flag/update-feature-flag.mjs b/components/launchdarkly/actions/update-feature-flag/update-feature-flag.mjs new file mode 100644 index 0000000000000..96ba7203d488f --- /dev/null +++ b/components/launchdarkly/actions/update-feature-flag/update-feature-flag.mjs @@ -0,0 +1,90 @@ +import app from "../../launchdarkly.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "launchdarkly-update-feature-flag", + name: "Update Feature Flag", + description: "Updates an existing feature flag using a JSON object. [See the documentation](https://apidocs.launchdarkly.com/tag/Feature-flags#operation/patchFeatureFlag)", + version: "0.0.1", + type: "action", + props: { + app, + projectKey: { + propDefinition: [ + app, + "project", + ], + }, + environmentKey: { + propDefinition: [ + app, + "environment", + ({ projectKey }) => ({ + projectKey, + }), + ], + }, + featureFlagKey: { + propDefinition: [ + app, + "flag", + ({ + projectKey, environmentKey, + }) => ({ + projectKey, + environmentKey, + }), + ], + }, + patch: { + type: "string[]", + label: "Patch", + description: "An array of JSON patch operations to apply to the feature flag. [See the documentation](https://apidocs.launchdarkly.com/#section/Overview/Updates).", + default: [ + JSON.stringify({ + op: "replace", + path: "/description", + value: "New description for this flag", + }), + ], + }, + ignoreConflicts: { + type: "boolean", + label: "Ignore Conflicts", + description: "If a flag configuration change made through this endpoint would cause a pending scheduled change or approval request to fail, this endpoint will return a 400. You can ignore this check by setting this parameter to `true`.", + optional: true, + }, + comment: { + type: "string", + label: "Comment", + description: "A comment to associate with the flag update.", + optional: true, + }, + }, + async run({ $ }) { + const { + app, + projectKey, + featureFlagKey, + patch, + ignoreConflicts, + comment, + } = this; + + const response = await app.updateFeatureFlag({ + $, + projectKey, + featureFlagKey, + params: { + ignoreConflicts, + }, + data: { + patch: utils.parseArray(patch), + comment, + }, + }); + + $.export("$summary", "Successfully updated feature flag"); + return response; + }, +}; diff --git a/components/launchdarkly/common/constants.mjs b/components/launchdarkly/common/constants.mjs new file mode 100644 index 0000000000000..1b024c162d50e --- /dev/null +++ b/components/launchdarkly/common/constants.mjs @@ -0,0 +1,11 @@ +const BASE_URL = "https://app.launchdarkly.com"; +const VERSION_PATH = "/api/v2"; +const WEBHOOK_ID = "webhookId"; +const SECRET = "secret"; + +export default { + BASE_URL, + VERSION_PATH, + WEBHOOK_ID, + SECRET, +}; diff --git a/components/launchdarkly/common/utils.mjs b/components/launchdarkly/common/utils.mjs new file mode 100644 index 0000000000000..e6ed4fa271055 --- /dev/null +++ b/components/launchdarkly/common/utils.mjs @@ -0,0 +1,50 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function isJson(value) { + try { + JSON.parse(value); + } catch (e) { + return false; + } + + return true; +} + +function valueToObject(value) { + if (typeof(value) === "object") { + return value; + } + + if (!isJson(value)) { + throw new ConfigurationError(`Make sure the custom expression contains a valid JSON object: \`${value}\``); + } + + return JSON.parse(value); +} + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +export default { + parseArray: (value) => parseArray(value).map(valueToObject), +}; diff --git a/components/launchdarkly/launchdarkly.app.mjs b/components/launchdarkly/launchdarkly.app.mjs index 6a11f359dea99..8868132f45f22 100644 --- a/components/launchdarkly/launchdarkly.app.mjs +++ b/components/launchdarkly/launchdarkly.app.mjs @@ -1,11 +1,231 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "launchdarkly", - propDefinitions: {}, + propDefinitions: { + project: { + type: "string", + label: "Project Key", + description: "The project key.", + async options({ + mapper = ({ + key: value, name: label, + }) => ({ + label, + value, + }), + }) { + const { items } = await this.listProjects(); + return items.map(mapper); + }, + }, + environment: { + type: "string", + label: "Environment Key", + description: "The environment key.", + async options({ + projectKey, mapper = ({ + key: value, name: label, + }) => ({ + label, + value, + }), + }) { + if (!projectKey) { + return []; + } + const { items } = await this.listEnvironments({ + projectKey, + }); + return items.map(mapper); + }, + }, + flag: { + type: "string", + label: "Feature Flag Key", + description: "The key of the feature flag to evaluate or update.", + async options({ + projectKey, environmentKey: env, + mapper = ({ + key: value, name: label, + }) => ({ + label, + value, + }), + }) { + if (!projectKey) { + return []; + } + const { items } = await this.listFeatureFlags({ + projectKey, + params: { + env, + }, + }); + return items.map(mapper); + }, + }, + contextKind: { + type: "string", + label: "Context Kind", + description: "The kind of context to evaluate the flag.", + async options({ + projectKey, + mapper = ({ + key: value, name: label, + }) => ({ + label, + value, + }), + }) { + if (!projectKey) { + return []; + } + const { items } = await this.getContextKinds({ + projectKey, + }); + return items.map(mapper); + }, + }, + context: { + type: "string", + label: "Context", + description: "Contextual information for evaluating the flag.", + async options({ + projectKey, environmentKey, + mapper = ({ + key: value, name: label, + }) => ({ + label, + value, + }), + }) { + if (!projectKey || !environmentKey) { + return []; + } + const { items } = await this.searchContexts({ + projectKey, + environmentKey, + }); + return items.map(mapper); + }, + }, + memberId: { + type: "string", + label: "Member ID", + description: "The member ID.", + async options({ + mapper = ({ + _id: value, email: label, + }) => ({ + label, + value, + }), + }) { + const { items } = await this.listAccountMembers(); + return items.map(mapper); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + "Content-Type": "application/json", + ...headers, + "Authorization": this.$auth.access_token, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + patch(args = {}) { + return this._makeRequest({ + method: "PATCH", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + listProjects(args = {}) { + return this._makeRequest({ + path: "/projects", + ...args, + }); + }, + listEnvironments({ + projectKey, ...args + } = {}) { + return this._makeRequest({ + path: `/projects/${projectKey}/environments`, + ...args, + }); + }, + listFeatureFlags({ + projectKey, ...args + } = {}) { + return this._makeRequest({ + path: `/flags/${projectKey}`, + ...args, + }); + }, + getContextKinds({ + projectKey, ...args + } = {}) { + return this._makeRequest({ + path: `/projects/${projectKey}/context-kinds`, + ...args, + }); + }, + searchContexts({ + projectKey, environmentKey, ...args + } = {}) { + return this.post({ + path: `/projects/${projectKey}/environments/${environmentKey}/contexts/search`, + ...args, + }); + }, + updateFeatureFlag({ + projectKey, featureFlagKey, ...args + } = {}) { + return this.patch({ + path: `/flags/${projectKey}/${featureFlagKey}`, + ...args, + }); + }, + getFeatureFlag({ + projectKey, featureFlagKey, ...args + } = {}) { + return this._makeRequest({ + path: `/flags/${projectKey}/${featureFlagKey}`, + ...args, + }); + }, + listAccountMembers(args = {}) { + return this._makeRequest({ + path: "/members", + ...args, + }); }, }, }; diff --git a/components/launchdarkly/package.json b/components/launchdarkly/package.json new file mode 100644 index 0000000000000..4c2789a3025a5 --- /dev/null +++ b/components/launchdarkly/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/launchdarkly", + "version": "0.0.1", + "description": "Pipedream LaunchDarkly Components", + "main": "launchdarkly.app.mjs", + "keywords": [ + "pipedream", + "launchdarkly" + ], + "homepage": "https://pipedream.com/apps/launchdarkly", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" + } +} diff --git a/components/launchdarkly/sources/common/webhook.mjs b/components/launchdarkly/sources/common/webhook.mjs new file mode 100644 index 0000000000000..d8f0ccc968472 --- /dev/null +++ b/components/launchdarkly/sources/common/webhook.mjs @@ -0,0 +1,125 @@ +import { createHmac } from "crypto"; +import { v4 as uuid } from "uuid"; +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../launchdarkly.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + }, + hooks: { + async activate() { + const { + http: { endpoint: url }, + createWebhook, + setWebhookId, + setSecret, + getStatements, + } = this; + + const secret = uuid(); + const response = + await createWebhook({ + data: { + name: "Pipedream Webhook", + url, + statements: getStatements(), + secret, + sign: true, + on: true, + }, + }); + + setWebhookId(response._id); + setSecret(secret); + }, + async deactivate() { + const webhookId = this.getWebhookId(); + if (webhookId) { + await this.deleteWebhook({ + webhookId, + }); + } + }, + }, + methods: { + setWebhookId(value) { + this.db.set(constants.WEBHOOK_ID, value); + }, + getWebhookId() { + return this.db.get(constants.WEBHOOK_ID); + }, + setSecret(value) { + this.db.set(constants.SECRET, value); + }, + getSecret() { + return this.db.get(constants.SECRET); + }, + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getStatements() { + throw new ConfigurationError("getStatements is not implemented"); + }, + processResource(resource) { + this.$emit(resource, this.generateMeta(resource)); + }, + createWebhook(args = {}) { + return this.app.post({ + path: "/webhooks", + ...args, + }); + }, + deleteWebhook({ + webhookId, ...args + } = {}) { + return this.app.delete({ + path: `/webhooks/${webhookId}`, + ...args, + }); + }, + isSignatureValid(signature, bodyRaw) { + if (!signature) { + return false; + } + + const secret = this.getSecret(); + + const computedSignature = + createHmac("sha256", secret) + .update(bodyRaw) + .digest("hex"); + + return signature === computedSignature; + }, + }, + async run({ + headers, body, bodyRaw, + }) { + const { + http, + isSignatureValid, + } = this; + + const signature = headers["x-ld-signature"]; + + if (!isSignatureValid(signature, bodyRaw)) { + console.log("Invalid signature"); + return http.respond({ + status: 401, + }); + } + + http.respond({ + status: 200, + }); + + this.processResource(body); + }, +}; diff --git a/components/launchdarkly/sources/new-access-token-event/new-access-token-event.mjs b/components/launchdarkly/sources/new-access-token-event/new-access-token-event.mjs new file mode 100644 index 0000000000000..1488f57efbcfb --- /dev/null +++ b/components/launchdarkly/sources/new-access-token-event/new-access-token-event.mjs @@ -0,0 +1,47 @@ +import common from "../common/webhook.mjs"; + +export default { + ...common, + key: "launchdarkly-new-access-token-event", + name: "New Access Token Event", + description: "Emit new event when a new access token activity happens. [See the documentation](https://apidocs.launchdarkly.com/tag/Webhooks#operation/postWebhook).", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + memberId: { + propDefinition: [ + common.props.app, + "memberId", + ], + }, + }, + methods: { + ...common.methods, + getStatements() { + return [ + { + resources: [ + `member/${this.memberId}:token/*`, + ], + actions: [ + "*", + ], + effect: "allow", + }, + ]; + }, + generateMeta(resource) { + const { + _id: id, + date: ts, + } = resource; + return { + id, + summary: `New Access Token Event ${id}`, + ts, + }; + }, + }, +}; diff --git a/components/launchdarkly/sources/new-flag-event/new-flag-event.mjs b/components/launchdarkly/sources/new-flag-event/new-flag-event.mjs new file mode 100644 index 0000000000000..3d82355ee93ae --- /dev/null +++ b/components/launchdarkly/sources/new-flag-event/new-flag-event.mjs @@ -0,0 +1,39 @@ +import common from "../common/webhook.mjs"; + +export default { + ...common, + key: "launchdarkly-new-flag-event", + name: "New Flag Event", + description: "Emit new event when flag activity occurs. [See the documentation](https://apidocs.launchdarkly.com/tag/Webhooks#operation/postWebhook).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getStatements() { + return [ + { + resources: [ + "proj/*:env/*:flag/*", + ], + actions: [ + "*", + ], + effect: "allow", + }, + ]; + }, + generateMeta(resource) { + const { + _id: id, + name, + date: ts, + } = resource; + return { + id, + summary: `New Flag ${name}`, + ts, + }; + }, + }, +}; diff --git a/components/launchdarkly/sources/new-user-event/new-user-event.mjs b/components/launchdarkly/sources/new-user-event/new-user-event.mjs new file mode 100644 index 0000000000000..36239f3d33daa --- /dev/null +++ b/components/launchdarkly/sources/new-user-event/new-user-event.mjs @@ -0,0 +1,39 @@ +import common from "../common/webhook.mjs"; + +export default { + ...common, + key: "launchdarkly-new-user-event", + name: "New User Event", + description: "Emit new event when user activity is noted. [See the documentation](https://apidocs.launchdarkly.com/tag/Webhooks#operation/postWebhook).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getStatements() { + return [ + { + resources: [ + "proj/*:env/*:user/*", + ], + actions: [ + "*", + ], + effect: "allow", + }, + ]; + }, + generateMeta(resource) { + const { + _id: id, + name, + date: ts, + } = resource; + return { + id, + summary: `New User ${name}`, + ts, + }; + }, + }, +}; diff --git a/components/launchnotes/actions/create-announcement/create-announcement.mjs b/components/launchnotes/actions/create-announcement/create-announcement.mjs new file mode 100644 index 0000000000000..dc193c8d1e2a5 --- /dev/null +++ b/components/launchnotes/actions/create-announcement/create-announcement.mjs @@ -0,0 +1,95 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import launchnotes from "../../launchnotes.app.mjs"; + +export default { + key: "launchnotes-create-announcement", + name: "Create Announcement", + description: "Generates a draft announcement for the LaunchNotes project. [See the documentation](https://developer.launchnotes.com/index.html)", + version: "0.0.1", + type: "action", + props: { + launchnotes, + clientMutationId: { + propDefinition: [ + launchnotes, + "clientMutationId", + ], + optional: true, + }, + projectId: { + propDefinition: [ + launchnotes, + "projectId", + ], + }, + headline: { + type: "string", + label: "Headline", + description: "The announcement title.", + optional: true, + }, + contentMarkdown: { + type: "string", + label: "Content Markdown", + description: "The Announcement content. Markdown formatting is supported.", + optional: true, + }, + shouldNotifyPageSubscribers: { + type: "boolean", + label: "Should Notify Page Subscribers", + description: "Whether the subscribers will be notified or not.", + optional: true, + }, + categories: { + propDefinition: [ + launchnotes, + "categories", + ({ projectId }) => ({ + projectId, + }), + ], + }, + templateId: { + propDefinition: [ + launchnotes, + "templateId", + ({ projectId }) => ({ + projectId, + }), + ], + optional: true, + }, + }, + async run({ $ }) { + const { + data, errors: responseErrors, + } = await this.launchnotes.createAnnouncement({ + $, + variables: { + input: { + clientMutationId: this.clientMutationId, + announcement: { + projectId: this.projectId, + headline: this.headline, + contentMarkdown: this.contentMarkdown, + shouldNotifyPageSubscribers: this.shouldNotifyPageSubscribers, + categories: parseObject(this.categories)?.map((item) => ({ + id: item, + })), + templateId: this.templateId, + }, + }, + }, + }); + + const errors = responseErrors || data.createAnnouncement.errors; + + if (errors.length) { + throw new ConfigurationError(JSON.stringify(errors[0])); + } + + $.export("$summary", `Successfully created draft announcement with Id: ${data.createAnnouncement.announcement.id}`); + return data; + }, +}; diff --git a/components/launchnotes/actions/create-subscription/create-subscription.mjs b/components/launchnotes/actions/create-subscription/create-subscription.mjs new file mode 100644 index 0000000000000..958133bf066cd --- /dev/null +++ b/components/launchnotes/actions/create-subscription/create-subscription.mjs @@ -0,0 +1,157 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { + SUBSCRIBE_TYPE_OPTIONS, SUBSCRIPTION_TYPE_OPTIONS, +} from "../../common/constants.mjs"; +import launchnotes from "../../launchnotes.app.mjs"; + +export default { + key: "launchnotes-create-subscription", + name: "Create Subscription", + description: "Adds a new subscriber to the current LaunchNotes project. [See the documentation](https://developer.launchnotes.com/index.html)", + version: "0.0.1", + type: "action", + props: { + launchnotes, + clientMutationId: { + propDefinition: [ + launchnotes, + "clientMutationId", + ], + optional: true, + }, + projectId: { + propDefinition: [ + launchnotes, + "projectId", + ], + }, + eventTypes: { + propDefinition: [ + launchnotes, + "eventTypes", + ({ projectId }) => ({ + projectId, + }), + ], + }, + categories: { + propDefinition: [ + launchnotes, + "categories", + ({ projectId }) => ({ + projectId, + }), + ], + }, + subscribeType: { + type: "string", + label: "Subscribe Type", + description: "The type you would like to create a subscription for.", + options: SUBSCRIBE_TYPE_OPTIONS, + optional: true, + reloadProps: true, + }, + subscriptionType: { + type: "string", + label: "Subscription Type", + description: "The type of subscription - eg. SimpleMailApp or SlackApp.", + options: SUBSCRIPTION_TYPE_OPTIONS, + }, + skipOptIn: { + type: "boolean", + label: "Skip OptIn", + description: "If set to true, opt in a newly created subscriber without sending a confirmation email.", + optional: true, + }, + subscriber: { + type: "string", + label: "Subscriber", + description: "The subscriber of the subscription.", + withLabel: true, + async options () { + const { data: { project: { projectUsers } } } = await this.launchnotes.getProject({ + projectId: this.projectId, + }); + + return projectUsers.nodes.map(({ + id: value, email: label, + }) => ({ + value, + label, + })); + }, + }, + }, + async additionalProps() { + const props = {}; + if (this.subscribeType) { + if (this.subscribeType === "WORK_ITEM") { + props.workItemId = { + type: "string", + label: "Work Item Id", + description: "Work Item ID", + options: async () => { + const { data: { project: { workItems } } } = await this.launchnotes.getProject({ + projectId: this.projectId, + }); + + return workItems.nodes.map(({ + id: value, name: label, + }) => ({ + value, + label, + })); + }, + }; + } + } + return props; + }, + async run({ $ }) { + const subscribedObject = { + type: "PROJECT", + id: this.projectId, + }; + + if (this.subscribeType === "WORK_ITEM") { + subscribedObject.type = "WORK_ITEM"; + subscribedObject.id = this.workItemId; + } + + const { + data, errors: responseErrors, + } = await this.launchnotes.createSubscription({ + $, + variables: { + input: { + clientMutationId: this.clientMutationId, + subscription: { + eventTypes: this.eventTypes.map((eventType) => ({ + id: eventType, + })), + categories: this.categories.map((category) => ({ + id: category, + })), + subscribedObject, + subscriber: { + type: "USER", + id: this.subscriber.value, + email: this.subscriber.email, + }, + type: this.subscriptionType, + skipOptIn: this.skipOptIn, + }, + }, + }, + }); + + const errors = responseErrors || data.createSubscription.errors; + + if (errors.length) { + throw new ConfigurationError(JSON.stringify(errors[0])); + } + + $.export("$summary", `Successfully created subscription with Id: ${data.createSubscription.subscription.id}`); + return data; + }, +}; diff --git a/components/launchnotes/common/constants.mjs b/components/launchnotes/common/constants.mjs new file mode 100644 index 0000000000000..5fa97fc040c1a --- /dev/null +++ b/components/launchnotes/common/constants.mjs @@ -0,0 +1,11 @@ +export const LIMIT = 100; + +export const SUBSCRIBE_TYPE_OPTIONS = [ + "PROJECT", + "WORK_ITEM", +]; + +export const SUBSCRIPTION_TYPE_OPTIONS = [ + "SimpleMailApp", + "SlackApp", +]; diff --git a/components/launchnotes/common/mutations.mjs b/components/launchnotes/common/mutations.mjs new file mode 100644 index 0000000000000..bfa1327d4dc45 --- /dev/null +++ b/components/launchnotes/common/mutations.mjs @@ -0,0 +1,58 @@ +export default { + createSubscription: ` + mutation CreateSubscription($input: CreateSubscriptionInput!) { + createSubscription( + input: $input + ) { + clientMutationId + errors { + message + path + } + subscription { + id + } + } + } + `, + createAnnouncement: ` + mutation CreateAnnouncement($input: CreateAnnouncementInput!) { + createAnnouncement( + input: $input + ) { + clientMutationId + errors { + message + path + } + announcement { + id + } + } + } + `, + createWebhook: ` + mutation CreateWebhook( + $name: String!, + $url: String!, + $username: String, + $password: String, + $owner: ID!, + $eventTypes: [ID!]! + ) { + createOutboundWebhook( + input: { + ownerId: $owner, + name: $name, + url: $url, + username: $username, + password: $password, + eventTypes: $eventTypes} + ) { + outboundWebhook { + id + } + } + } + `, +}; diff --git a/components/launchnotes/common/queries.mjs b/components/launchnotes/common/queries.mjs new file mode 100644 index 0000000000000..756b571c5c1b6 --- /dev/null +++ b/components/launchnotes/common/queries.mjs @@ -0,0 +1,194 @@ +export default { + listEventTypes: ` + query Project($projectId: ID!) { + project(id: $projectId) { + eventTypes { + totalCount + nodes { + id + name + } + } + } + } + `, + listProjects: ` + query Session { + session { + projects { + nodes { + id + name + } + } + } + } + `, + getProject: ` + query Project ($projectId: ID!) { + project(id: $projectId) { + categories { + nodes { + id + name + } + } + eventTypes { + nodes { + id + name + } + } + projectUsers { + nodes { + id + email + } + } + subscribers { + nodes { + id + email + } + } + templates { + nodes { + id + name + } + } + workItems { + nodes { + id + name + } + } + } + } + `, + getWorkItem: ` + query WorkItem ($workItemId: ID!) { + workItem(id: $workItemId) { + subscribers { + nodes { + id + email + } + } + } + } + `, + paginateAnnouncements: ` + query Project( + $projectId: ID! + $limit: Int! + $cursor: String + $rules: [Rule!] + ) { + project(id: $projectId) { + announcements( + orderBy: { field: "createdAt", sort: DESC } + where: { + rules: $rules + } + first: $limit + after: $cursor + ) { + edges { + cursor + node { + id + name + createdAt + scheduledAt + publishedAt + state + privatePermalink + publicPermalink + notificationsExcerptOnly + inPublishedDigest + headline + hasPendingContentGenerators + feedbackSadCount + feedbackMehCount + feedbackHappyCount + excerptWithFallback + excerpt + doNotIndex + description + deactivatedAt + contentType + contentPlainText + contentHtml + content + author + archived + authorWithFallback + descriptionWithFallback + scheduledAtTimezone + shouldNotifyPageSubscribers + slackChannelCount + slackChannelCounts + slackMessage + slug + subjectLine + subjectLineWithFallback + title + titleWithFallback + totalSubscriberCounts + updatedAt + } + } + } + } + } + `, + paginateSubscriptions: ` + query Project( + $projectId: ID! + $limit: Int! + $cursor: String + $rules: [Rule!] + ) { + project(id: $projectId) { + subscriptions( + orderBy: { field: "createdAt", sort: DESC } + where: { + rules: $rules + } + first: $limit + after: $cursor + ) { + edges { + cursor + node { + announcementPublished + createdAt + feedbackSubmitted + id + updatedAt + eventTypes { + createdAt + id + name + updatedAt + } + categories { + backgroundColor + color + createdAt + description + id + name + position + slug + textColor + updatedAt + } + } + } + } + } + } + `, +}; diff --git a/components/launchnotes/common/utils.mjs b/components/launchnotes/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/launchnotes/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/launchnotes/launchnotes.app.mjs b/components/launchnotes/launchnotes.app.mjs new file mode 100644 index 0000000000000..83243cf9906b8 --- /dev/null +++ b/components/launchnotes/launchnotes.app.mjs @@ -0,0 +1,207 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; +import mutations from "./common/mutations.mjs"; +import queries from "./common/queries.mjs"; + +export default { + type: "app", + app: "launchnotes", + propDefinitions: { + categories: { + type: "string[]", + label: "Categories", + description: "Associated categories of the announcement.", + async options({ projectId }) { + const { data: { project: { categories: { nodes } } } } = await this.getProject({ + projectId, + }); + + return nodes.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + clientMutationId: { + type: "string", + label: "Client Mutation Id", + description: "A unique identifier for the client performing the mutation.", + }, + eventTypes: { + type: "string[]", + label: "Event Types", + description: "Event types you would like receive notifications on.", + async options({ projectId }) { + const { data: { project: { eventTypes: { nodes } } } } = await this.getProject({ + projectId, + }); + + return nodes.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + projectId: { + type: "string", + label: "Project ID", + description: "The ID of the project", + async options() { + const { data: { session: { projects: { nodes } } } } = await this.listProjects(); + + return nodes.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + subscriber: { + type: "string", + label: "Subscriber", + description: "The subscriber of the subscription.", + withLabel: true, + async options({ + projectId, workItemId, + }) { + let nodes = []; + if (workItemId) { + const { data: { project: { subscribers } } } = await this.getWorkItem({ + workItemId, + }); + nodes = subscribers.nodes; + } else { + const { data: { project: { subscribers } } } = await this.getProject({ + projectId, + }); + nodes = subscribers.nodes; + } + + return nodes.map(({ + id: value, email: label, + }) => ({ + value, + label, + })); + }, + }, + templateId: { + type: "string", + label: "Template Id", + description: "Pre-fill the announcement with a template. If this is provided, all other input is ignored.", + async options({ projectId }) { + const { data: { project: { templates: { nodes } } } } = await this.getProject({ + projectId, + }); + + return nodes.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.launchnotes.io/graphql"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, ...data + }) { + return axios($, { + method: "POST", + url: this._baseUrl(), + headers: this._headers(), + data, + }); + }, + createAnnouncement({ + $, variables, + }) { + return this._makeRequest({ + $, + query: mutations.createAnnouncement, + variables, + }); + }, + createSubscription({ + $, variables, + }) { + return this._makeRequest({ + $, + query: mutations.createSubscription, + variables, + }); + }, + getProject(variables) { + return this._makeRequest({ + query: queries.getProject, + variables, + }); + }, + getWorkItem(variables) { + return this._makeRequest({ + query: queries.getWorkItem, + variables, + }); + }, + listAnnouncements(variables) { + return this._makeRequest({ + query: queries.paginateAnnouncements, + variables, + }); + }, + listProjects() { + return this._makeRequest({ + query: queries.listProjects, + }); + }, + listSubscriptions(variables) { + return this._makeRequest({ + query: queries.paginateSubscriptions, + variables, + }); + }, + async *paginate({ + fn, maxResults = null, type, ...variables + }) { + let hasMore = false; + let count = 0; + let cursor; + + do { + variables.limit = LIMIT; + variables.cursor = cursor; + const { data: { project: { [type]: { edges } } } } = await fn( + variables, + ); + + for (const { node } of edges) { + yield node; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = edges.length; + if (hasMore) { + cursor = edges[0].cursor; + } + + } while (hasMore); + }, + }, +}; diff --git a/components/launchnotes/package.json b/components/launchnotes/package.json new file mode 100644 index 0000000000000..6452755e2323d --- /dev/null +++ b/components/launchnotes/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/launchnotes", + "version": "0.1.0", + "description": "Pipedream LaunchNotes Components", + "main": "launchnotes.app.mjs", + "keywords": [ + "pipedream", + "launchnotes" + ], + "homepage": "https://pipedream.com/apps/launchnotes", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/launchnotes/sources/common/base.mjs b/components/launchnotes/sources/common/base.mjs new file mode 100644 index 0000000000000..0505b765a5a84 --- /dev/null +++ b/components/launchnotes/sources/common/base.mjs @@ -0,0 +1,66 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import launchnotes from "../../launchnotes.app.mjs"; + +export default { + props: { + launchnotes, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + projectId: { + propDefinition: [ + launchnotes, + "projectId", + ], + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01T00:00:00Z"; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const dateField = this.getDateField(); + + const response = this.launchnotes.paginate({ + fn: this.getFunction(), + type: this.getTypes(), + maxResults, + rules: this.getRules(lastDate), + projectId: this.projectId, + }); + + let responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + if (responseArray.length) { + this._setLastDate(responseArray[0][dateField]); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item[dateField]), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/launchnotes/sources/new-announcement-published/new-announcement-published.mjs b/components/launchnotes/sources/new-announcement-published/new-announcement-published.mjs new file mode 100644 index 0000000000000..ab61ba6942ea4 --- /dev/null +++ b/components/launchnotes/sources/new-announcement-published/new-announcement-published.mjs @@ -0,0 +1,42 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "launchnotes-new-announcement-published", + name: "New Announcement Published", + description: "Emit new event when an announcement is published.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.launchnotes.listAnnouncements; + }, + getDateField() { + return "publishedAt"; + }, + getTypes() { + return "announcements"; + }, + getSummary(item) { + return `New announcement published: ${item.name}`; + }, + getRules(lastDate) { + return [ + { + field: this.getDateField(), + expression: "gt", + value: lastDate, + }, + { + field: "state", + expression: "eq", + value: "published", + }, + ]; + }, + }, + sampleEmit, +}; diff --git a/components/launchnotes/sources/new-announcement-published/test-event.mjs b/components/launchnotes/sources/new-announcement-published/test-event.mjs new file mode 100644 index 0000000000000..ba602a3dab359 --- /dev/null +++ b/components/launchnotes/sources/new-announcement-published/test-event.mjs @@ -0,0 +1,42 @@ +export default { + "archived": false, + "author": null, + "authorWithFallback": "The Pipedream team", + "content": "{\"type\":\"doc\",\"content\":[{\"type\":\"paragraph\",\"attrs\":{\"textAlign\":\"left\"},\"content\":[{\"text\":\"content markdown\",\"type\":\"text\"}]}]}", + "contentHtml": "

content markdown

", + "contentPlainText": "content markdown", + "contentType": "tiptap", + "createdAt": "2024-08-22T21:42:14Z", + "deactivatedAt": null, + "description": null, + "descriptionWithFallback": "content markdown", + "doNotIndex": false, + "excerpt": null, + "excerptWithFallback": "content markdown", + "feedbackHappyCount": 0, + "feedbackMehCount": 0, + "feedbackSadCount": 0, + "hasPendingContentGenerators": false, + "headline": "HeadLine Test", + "id": "ann_Y67GCQmyBKwYN", + "inPublishedDigest": false, + "name": "HeadLine Test", + "notificationsExcerptOnly": false, + "privatePermalink": "https://app.launchnotes.com/projects/pro_uHApc6EZ/announcements/ann_Y6yBKwYN/published", + "publicPermalink": "https://pipedream.launchnotes.io/announcements/headline-test", + "publishedAt": null, + "scheduledAt": null, + "scheduledAtTimezone": null, + "shouldNotifyPageSubscribers": false, + "slackChannelCount": 0, + "slackChannelCounts": 0, + "slackMessage": null, + "slug": "headline-test", + "state": "published", + "subjectLine": null, + "subjectLineWithFallback": "HeadLine Test", + "title": null, + "titleWithFallback": "HeadLine Test", + "totalSubscriberCounts": 0, + "updatedAt": "2024-08-22T21:42:15Z" +} \ No newline at end of file diff --git a/components/launchnotes/sources/new-announcement-scheduled/new-announcement-scheduled.mjs b/components/launchnotes/sources/new-announcement-scheduled/new-announcement-scheduled.mjs new file mode 100644 index 0000000000000..3555c360040b5 --- /dev/null +++ b/components/launchnotes/sources/new-announcement-scheduled/new-announcement-scheduled.mjs @@ -0,0 +1,42 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "launchnotes-new-announcement-scheduled", + name: "New Announcement Scheduled", + description: "Emit new event when an announcement is scheduled.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.launchnotes.listAnnouncements; + }, + getDateField() { + return "createdAt"; + }, + getTypes() { + return "announcements"; + }, + getSummary(item) { + return `New announcement scheduled: ${item.name}`; + }, + getRules(lastDate) { + return [ + { + field: this.getDateField(), + expression: "gt", + value: lastDate, + }, + { + field: "state", + expression: "eq", + value: "scheduled", + }, + ]; + }, + }, + sampleEmit, +}; diff --git a/components/launchnotes/sources/new-announcement-scheduled/test-event.mjs b/components/launchnotes/sources/new-announcement-scheduled/test-event.mjs new file mode 100644 index 0000000000000..67c4f5e82717b --- /dev/null +++ b/components/launchnotes/sources/new-announcement-scheduled/test-event.mjs @@ -0,0 +1,42 @@ +export default { + "archived": false, + "author": null, + "authorWithFallback": "The Pipedream team", + "content": "{\"type\":\"doc\",\"content\":[{\"type\":\"paragraph\",\"attrs\":{\"textAlign\":\"left\"},\"content\":[{\"text\":\"content markdown\",\"type\":\"text\"}]}]}", + "contentHtml": "

content markdown

", + "contentPlainText": "content markdown", + "contentType": "tiptap", + "createdAt": "2024-08-22T21:42:14Z", + "deactivatedAt": null, + "description": null, + "descriptionWithFallback": "content markdown", + "doNotIndex": false, + "excerpt": null, + "excerptWithFallback": "content markdown", + "feedbackHappyCount": 0, + "feedbackMehCount": 0, + "feedbackSadCount": 0, + "hasPendingContentGenerators": false, + "headline": "HeadLine Test", + "id": "ann_Y67GCQmyBKwYN", + "inPublishedDigest": false, + "name": "HeadLine Test", + "notificationsExcerptOnly": false, + "privatePermalink": "https://app.launchnotes.com/projects/pro_uHApc6EZ/announcements/ann_Y6yBKwYN/published", + "publicPermalink": "https://pipedream.launchnotes.io/announcements/headline-test", + "publishedAt": null, + "scheduledAt": null, + "scheduledAtTimezone": null, + "shouldNotifyPageSubscribers": false, + "slackChannelCount": 0, + "slackChannelCounts": 0, + "slackMessage": null, + "slug": "headline-test", + "state": "scheduled", + "subjectLine": null, + "subjectLineWithFallback": "HeadLine Test", + "title": null, + "titleWithFallback": "HeadLine Test", + "totalSubscriberCounts": 0, + "updatedAt": "2024-08-22T21:42:15Z" +} \ No newline at end of file diff --git a/components/launchnotes/sources/new-subscription-created/new-subscription-created.mjs b/components/launchnotes/sources/new-subscription-created/new-subscription-created.mjs new file mode 100644 index 0000000000000..7183ca6954d59 --- /dev/null +++ b/components/launchnotes/sources/new-subscription-created/new-subscription-created.mjs @@ -0,0 +1,37 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "launchnotes-new-subscription-created", + name: "New Subscription Created", + description: "Emit new event when a new project subscription is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.launchnotes.listSubscriptions; + }, + getDateField() { + return "createdAt"; + }, + getTypes() { + return "subscriptions"; + }, + getSummary(item) { + return `New subscription created: ${item.id}`; + }, + getRules(lastDate) { + return [ + { + field: this.getDateField(), + expression: "gt", + value: lastDate, + }, + ]; + }, + }, + sampleEmit, +}; diff --git a/components/launchnotes/sources/new-subscription-created/test-event.mjs b/components/launchnotes/sources/new-subscription-created/test-event.mjs new file mode 100644 index 0000000000000..916fa79a061aa --- /dev/null +++ b/components/launchnotes/sources/new-subscription-created/test-event.mjs @@ -0,0 +1,35 @@ +export default { + "announcementPublished": true, + "createdAt": "2024-08-22T13:39:59Z", + "feedbackSubmitted": true, + "id": "sub_HGgvX36uP", + "updatedAt": "2024-08-23T15:06:27Z", + "eventTypes": [ + { + "createdAt": "2020-08-16T05:25:59Z", + "id": "evt_tLXGAed8z", + "name": "project.created", + "updatedAt": "2020-08-16T05:25:59Z" + }, + { + "createdAt": "2021-02-19T17:56:00Z", + "id": "evt_5rrtkvyar", + "name": "announcement.unscheduled", + "updatedAt": "2021-02-19T17:56:00Z" + }, + ], + "categories": [ + { + "backgroundColor": "#216453", + "color": "#216453", + "createdAt": "2024-08-21T20:35:52Z", + "description": null, + "id": "cat_U6XsjA4YJ", + "name": "Category 01", + "position": 0, + "slug": "category-01", + "textColor": "#394967", + "updatedAt": "2024-08-21T20:35:52Z" + } + ] +} \ No newline at end of file diff --git a/components/lawmatics/README.md b/components/lawmatics/README.md index 23c41531c976b..218391466646a 100644 --- a/components/lawmatics/README.md +++ b/components/lawmatics/README.md @@ -1,4 +1,11 @@ # Overview -You can use the Lawmatics API to build legal documents, such as contracts, -leases, and wills. +Lawmatics is a platform tailored for legal firms, providing tools for CRM, marketing automation, and intake management. Its API enables the automation of routine tasks, data syncing across various platforms, and personalized client interactions without manual intervention. With Pipedream's serverless execution model and its capability to connect to hundreds of apps, one can automate workflows like client data synchronization, case status updates, or even trigger custom marketing campaigns based on client interaction. The API’s ability to communicate with Pipedream allows for an efficient and programmable way to handle legal operational processes. + +# Example Use Cases + +- **Client Onboarding Automation**: Trigger a workflow in Pipedream when a new client is added in Lawmatics. Automate the creation of client files in Google Drive, set up tasks in a project management tool like Trello, and schedule a welcome email through SendGrid. + +- **Case Status Update Notifications**: Set up a Pipedream workflow to listen for case updates in Lawmatics. When a case status changes, automatically send a notification via Slack to the responsible attorney, update a shared status dashboard in Google Sheets, and log the update in a database like Airtable for record-keeping. + +- **Automated Follow-Up Sequence**: Initiate a sequence of timed follow-up emails through SendGrid when a milestone is reached in a Lawmatics case. Deploy a Pipedream workflow that listens for milestone completions and schedules emails, creates follow-up tasks in Asana for the legal team, and updates the client’s record in Lawmatics with each interaction. diff --git a/components/lawmatics/package.json b/components/lawmatics/package.json new file mode 100644 index 0000000000000..565cba24ca54a --- /dev/null +++ b/components/lawmatics/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/lawmatics", + "version": "0.6.0", + "description": "Pipedream lawmatics Components", + "main": "lawmatics.app.mjs", + "keywords": [ + "pipedream", + "lawmatics" + ], + "homepage": "https://pipedream.com/apps/lawmatics", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/leadboxer/README.md b/components/leadboxer/README.md new file mode 100644 index 0000000000000..5db8ac0f2c44d --- /dev/null +++ b/components/leadboxer/README.md @@ -0,0 +1,11 @@ +# Overview + +The LeadBoxer API lets you track and score leads using web and email behavior data. With this API on Pipedream, you can automate the collection of lead insights, trigger actions based on lead scores or activities, and integrate this data into your CRM or other business tools. By tapping into webhooks and Pipedream's serverless platform, you can create workflows that react in real-time to lead interactions, enrich lead profiles, or sync information across multiple services. + +# Example Use Cases + +- **Real-time Lead Scoring Alerts**: Use LeadBoxer data to monitor lead scores and set up alerts. When a lead hits a score threshold, trigger a workflow that sends a notification to Slack, alerting your sales team to follow up immediately. + +- **Lead Activity-Based Email Campaigns**: Trigger personalized email campaigns in SendGrid based on specific actions or engagement levels tracked by LeadBoxer. For instance, if a lead views a key page on your site, automatically send them a tailored email sequence to nudge them down the sales funnel. + +- **CRM Lead Sync and Enrichment**: Sync lead data between LeadBoxer and your CRM, like Salesforce. When new leads are captured or existing leads show high engagement, update their records in Salesforce and enrich them with behavior data from LeadBoxer for a complete lead profile. diff --git a/components/leadfeeder/README.md b/components/leadfeeder/README.md index 75783bc39c977..35c08d58e6be6 100644 --- a/components/leadfeeder/README.md +++ b/components/leadfeeder/README.md @@ -1,11 +1,11 @@ # Overview -Leadfeeder is a powerful API that allows you to build a variety of applications -and tools. Here are some examples of what you can build with the Leadfeeder -API: - -- A lead capture tool that allows you to capture leads from a variety of - sources -- A lead management tool that allows you to track and manage your leads -- A reporting tool that allows you to generate reports on your leads -- A CRM integration that allows you to integrate your lead data with your CRM +Leadfeeder is a tool that reveals the companies visiting your website and how they interact with your content. By harnessing the power of the Leadfeeder API on Pipedream, you can automate the process of capturing lead data and integrating it directly into your sales and marketing workflows. This can range from enriching CRM records to triggering custom email campaigns based on visitor behavior, enabling a more dynamic and responsive engagement with potential customers. + +# Example Use Cases + +- **Enrich CRM with Lead Data**: Automatically add new leads identified by Leadfeeder into your CRM, such as Salesforce or HubSpot. When a company visits your website, capture the event and create a new contact or update an existing record with the company’s details, visited pages, and engagement level. + +- **Trigger Automated Outreach**: Set up a workflow where specific website activities detected by Leadfeeder, like visiting a pricing page, initiate a personalized email sequence from a marketing automation platform like Mailchimp or SendGrid. Tailor the messages based on the content the lead interacted with to increase conversion chances. + +- **Slack Notifications for Sales Teams**: Keep your sales team in the loop with instant Slack notifications when a target company visits your site. Use Leadfeeder’s API to monitor visits and set criteria, such as a certain number of visits or specific page views, that will trigger a notification to a designated Slack channel or direct message to a sales rep. diff --git a/components/leadiq/README.md b/components/leadiq/README.md new file mode 100644 index 0000000000000..c9209486d88cc --- /dev/null +++ b/components/leadiq/README.md @@ -0,0 +1,11 @@ +# Overview + +The LeadIQ API enables seamless integration of LeadIQ’s capabilities into various marketing and sales workflows, automating the lead capture and enrichment processes. By harnessing this API on Pipedream, you can automate the extraction of lead data, sync it with CRM systems, and trigger personalized outreach campaigns, all within a serverless environment. This reduces manual tasks, enhances lead quality, and accelerates the sales pipeline. + +# Example Use Cases + +- **Sync Leads to Salesforce Automatically**: When a new lead is captured or updated in LeadIQ, automatically push this data to Salesforce. This keeps your CRM up-to-date and ensures that sales teams have immediate access to the latest lead information. + +- **Email Campaign Trigger on Lead Capture**: Set up an automated workflow where capturing a new lead via LeadIQ triggers a personalized email campaign via Mailchimp or another email marketing service. This can be used to immediately engage new leads with a welcome message or promotional offers, enhancing conversion rates. + +- **Slack Notifications for Sales Teams**: Configure a workflow where any update or new addition to your leads in LeadIQ sends an instant notification to a designated Slack channel. This keeps your sales team informed in real-time, enabling prompt follow-ups that could potentially boost sales conversions. diff --git a/components/leadiq/actions/find-contact/find-contact.mjs b/components/leadiq/actions/find-contact/find-contact.mjs new file mode 100644 index 0000000000000..cb3698b04e284 --- /dev/null +++ b/components/leadiq/actions/find-contact/find-contact.mjs @@ -0,0 +1,88 @@ +import app from "../../leadiq.app.mjs"; +import queries from "../../common/queries.mjs"; + +export default { + key: "leadiq-find-contact", + name: "Find Contact", + description: "Searches for contact information based on user-defined props which may include identifiers such as name, email, or company. Returns the contact data if a match is found within the LeadIQ database. [See the documentation](https://developer.leadiq.com/#query-searchPeople)", + version: "0.0.1", + type: "action", + props: { + app, + fullName: { + type: "string", + label: "Full Name", + description: "The full name of the contact.", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "The email of the contact", + optional: true, + }, + companyName: { + type: "string", + label: "Company Name", + description: "The name of the company.", + optional: true, + }, + searchInPastCompanies: { + type: "boolean", + label: "Search in Past Companies", + description: "If enabled, the search will include past companies.", + optional: true, + }, + }, + methods: { + searchContact({ + data, ...args + } = {}) { + return this.app.post({ + ...args, + data: { + ...data, + query: queries.searchPeople, + }, + }); + }, + }, + async run({ $ }) { + const { + searchContact, + fullName, + email, + companyName, + searchInPastCompanies, + } = this; + + const { data: { searchPeople: { results } } } = await searchContact({ + $, + data: { + variables: { + input: { + limit: 1, + fullName, + email, + ...(companyName && { + company: { + name: companyName, + searchInPastCompanies, + }, + }), + }, + }, + }, + }); + + if (!results.length) { + $.export("$summary", `No contact information found for \`${fullName || email || companyName}\``); + return { + success: false, + }; + } + + $.export("$summary", `Successfully found \`${results.length}\` contact(s)`); + return results; + }, +}; diff --git a/components/leadiq/common/queries.mjs b/components/leadiq/common/queries.mjs new file mode 100644 index 0000000000000..277865f3b2a64 --- /dev/null +++ b/components/leadiq/common/queries.mjs @@ -0,0 +1,59 @@ +export default { + searchPeople: ` + query SearchPeople($input: SearchPeopleInput!) { + searchPeople(input: $input) { + results { + _id + name { + first + fullName + last + middle + } + currentPositions { + companyId + title + updatedAt + emails { + type + status + updatedAt + value + } + companyInfo { + name + alternativeNames + domain + description + emailDomains + type + phones + country + } + } + pastPositions { + companyId + title + updatedAt + emails { + type + status + updatedAt + value + } + companyInfo { + name + alternativeNames + domain + description + emailDomains + type + phones + country + } + } + } + } + } + `, +}; diff --git a/components/leadiq/leadiq.app.mjs b/components/leadiq/leadiq.app.mjs new file mode 100644 index 0000000000000..25afabca1d7fe --- /dev/null +++ b/components/leadiq/leadiq.app.mjs @@ -0,0 +1,36 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "leadiq", + methods: { + _baseUrl() { + return "https://api.leadiq.com/graphql"; + }, + getAuth() { + return { + username: this.$auth.api_key, + password: "", + }; + }, + async _makeRequest({ + $ = this, ...args + } = {}) { + const response = await axios($, { + ...args, + url: this._baseUrl(), + auth: this.getAuth(), + }); + if (response?.errors?.length) { + throw new Error(JSON.stringify(response.errors)); + } + return response; + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + }, +}; diff --git a/components/leadiq/package.json b/components/leadiq/package.json new file mode 100644 index 0000000000000..930b0315c1a69 --- /dev/null +++ b/components/leadiq/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/leadiq", + "version": "0.1.0", + "description": "Pipedream LeadIQ Components", + "main": "leadiq.app.mjs", + "keywords": [ + "pipedream", + "leadiq" + ], + "homepage": "https://pipedream.com/apps/leadiq", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "1.6.5" + } +} diff --git a/components/leadoku/README.md b/components/leadoku/README.md new file mode 100644 index 0000000000000..9a603e47248bf --- /dev/null +++ b/components/leadoku/README.md @@ -0,0 +1,11 @@ +# Overview + +The Leadoku API allows for the automation of lead capture, enrichment, and management processes. By interfacing with Leadoku on Pipedream, users can streamline the integration of lead data into sales, marketing, and CRM systems, enhancing lead qualification and accelerating sales cycles. This API serves well for businesses looking to automate the flow of lead information between platforms and derive insights from lead data with minimal manual intervention. + +# Example Use Cases + +- **Lead Capture and Sync to CRM**: Automatically capture leads from various sources (e.g., web forms, chatbots) using the Leadoku API, and sync them in real-time with CRM systems like Salesforce or HubSpot available on Pipedream. This workflow ensures that sales teams have immediate access to new leads and can act swiftly to engage potential customers. + +- **Lead Enrichment and Email Automation**: Use the Leadoku API to enrich lead data by adding additional information such as company size, role, and industry. Connect this workflow to an email marketing app like Mailchimp on Pipedream to trigger personalized email campaigns based on the enriched lead data, enhancing the relevance and effectiveness of marketing efforts. + +- **Lead Scoring and Instant Notifications**: Implement a lead scoring system using the Leadoku API to evaluate and score leads based on predefined criteria (e.g., engagement level, demographic data). Integrate this with communication tools such as Slack or Microsoft Teams on Pipedream to send instant notifications to sales teams when high-value leads are captured, enabling prompt and prioritized follow-up. diff --git a/components/leadoku/leadoku.app.mjs b/components/leadoku/leadoku.app.mjs new file mode 100644 index 0000000000000..a5a6f7392c221 --- /dev/null +++ b/components/leadoku/leadoku.app.mjs @@ -0,0 +1,44 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "leadoku", + propDefinitions: {}, + methods: { + _baseUrl() { + return "https://api.growth-x.com/growth/apis"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + ...otherOpts + } = opts; + return axios($, { + url: `${this._baseUrl()}${path}`, + params: { + ...params, + analytics_code: `${this.$auth.analytics_code}`, + }, + ...otherOpts, + }); + }, + getNewConnections(opts = {}) { + return this._makeRequest({ + params: { + method: "new_connections", + }, + ...opts, + }); + }, + getNewResponders(opts = {}) { + return this._makeRequest({ + params: { + method: "new_responders", + }, + ...opts, + }); + }, + }, +}; diff --git a/components/leadoku/package.json b/components/leadoku/package.json new file mode 100644 index 0000000000000..c12d5fc011fec --- /dev/null +++ b/components/leadoku/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/leadoku", + "version": "0.1.0", + "description": "Pipedream Leadoku Components", + "main": "leadoku.app.mjs", + "keywords": [ + "pipedream", + "leadoku" + ], + "homepage": "https://pipedream.com/apps/leadoku", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/leadoku/sources/common/base.mjs b/components/leadoku/sources/common/base.mjs new file mode 100644 index 0000000000000..65e640853e21f --- /dev/null +++ b/components/leadoku/sources/common/base.mjs @@ -0,0 +1,55 @@ +import leadoku from "../../leadoku.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + leadoku, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastScannedDate() { + return this.db.get("lastScannedDate") || 0; + }, + _setLastScannedDate(lastScannedDate) { + this.db.set("lastScannedDate", lastScannedDate); + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + getTsField() { + throw new Error("getTsField is not implemented"); + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + generateMeta(item) { + const ts = Date.parse(item[this.getTsField()]); + return { + id: `${item.receiver_id}-${ts}`, + summary: this.getSummary(), + ts, + }; + }, + }, + async run() { + const lastScannedDate = this._getLastScannedDate(); + let maxScannedDate = lastScannedDate; + const resourceFn = this.getResourceFn(); + const { data } = await resourceFn(); + for (const item of data) { + const ts = Date.parse(item[this.getTsField()]); + if (ts > lastScannedDate) { + maxScannedDate = Math.max(maxScannedDate, ts); + const meta = this.generateMeta(item); + this.$emit(item, meta); + } + } + this._setLastScannedDate(maxScannedDate); + }, +}; diff --git a/components/leadoku/sources/new-connection/new-connection.mjs b/components/leadoku/sources/new-connection/new-connection.mjs new file mode 100644 index 0000000000000..bdb74d3ac2850 --- /dev/null +++ b/components/leadoku/sources/new-connection/new-connection.mjs @@ -0,0 +1,23 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "leadoku-new-connection", + name: "New Connection", + description: "Emit new event each time a new connection is made in Leadoku. [See the documentation](https://help.leadoku.io/en/articles/8261580-leadoku-api-for-custom-integrations)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.leadoku.getNewConnections; + }, + getTsField() { + return "connection_date"; + }, + getSummary() { + return "New Connection Made"; + }, + }, +}; diff --git a/components/leadoku/sources/new-responder/new-responder.mjs b/components/leadoku/sources/new-responder/new-responder.mjs new file mode 100644 index 0000000000000..08541131e8aef --- /dev/null +++ b/components/leadoku/sources/new-responder/new-responder.mjs @@ -0,0 +1,23 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "leadoku-new-responder", + name: "New Responder", + description: "Emit new event when there is a new responder in Leadoku. [See the documentation](https://help.leadoku.io/en/articles/8261580-leadoku-api-for-custom-integrations)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.leadoku.getNewResponders; + }, + getTsField() { + return "message_scan_date"; + }, + getSummary() { + return "New Responder"; + }, + }, +}; diff --git a/components/leadpops/README.md b/components/leadpops/README.md index 1776a053e70bb..5ef06ca93e211 100644 --- a/components/leadpops/README.md +++ b/components/leadpops/README.md @@ -1,12 +1,11 @@ # Overview -With the Leadpops API, you can build a variety of tools to help you generate -and manage leads. Here are just a few examples: - -- A lead capture form that automatically adds new leads to your Leadpops - account -- A lead management tool that allows you to view, edit, and delete leads -- A lead enrichment tool that allows you to add additional information to your - leads -- A lead tracking tool that allows you to see which leads have been contacted - and which have not +The Leadpops API provides a toolkit for optimizing lead conversion for real estate and mortgage professionals. By integrating Leadpops with Pipedream, users can automate lead capture processes, streamline follow-up communications, and synchronize lead information across various marketing and CRM platforms. Pipedream’s serverless execution model and easy-to-use interface allow for building complex workflows to act upon data received from Leadpops without delving deep into coding. + +# Example Use Cases + +- **Automated Lead Synchronization with CRM**: Capture leads using Leadpops funnels and automatically add them to a CRM like Salesforce or HubSpot. When a new lead is captured, a workflow can trigger that creates or updates the lead’s record in the CRM, ensuring that follow-up actions are timely and no leads fall through the cracks. + +- **Lead Qualification and Notification Workflow**: Create a workflow that qualifies leads based on custom criteria set in Leadpops. Once a lead meets the qualification standards, use Pipedream to send real-time notifications via email, SMS, or messaging apps like Slack to the appropriate salesperson or team, enabling immediate engagement. + +- **Dynamic Retargeting Campaigns**: Leverage the data from Leadpops to build dynamic retargeting campaigns on platforms like Facebook Ads or Google Ads. When a lead interacts with a Leadpops funnel, a Pipedream workflow can be triggered to add the lead to a custom audience for more personalized and effective advertising efforts. diff --git a/components/leadpops/package.json b/components/leadpops/package.json new file mode 100644 index 0000000000000..86148e35e6ae5 --- /dev/null +++ b/components/leadpops/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/leadpops", + "version": "0.6.0", + "description": "Pipedream leadpops Components", + "main": "leadpops.app.mjs", + "keywords": [ + "pipedream", + "leadpops" + ], + "homepage": "https://pipedream.com/apps/leadpops", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/leadzen_ai/README.md b/components/leadzen_ai/README.md new file mode 100644 index 0000000000000..4d73a01d6ab45 --- /dev/null +++ b/components/leadzen_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +Leadzen.ai is an API designed to enhance lead management and marketing automation. It allows users to dynamically score, segment, and nurture leads based on various criteria and interactions. By integrating Leadzen.ai with Pipedream, users can automate complex workflows, sync data across multiple platforms, and trigger actions based on lead behavior in real-time. This integration enables marketers to refine their engagement strategies, personalize communications more effectively, and drive sales conversions more efficiently. + +# Example Use Cases + +- **Lead Scoring and Segmentation Workflow**: Integrate Leadzen.ai with a CRM like Salesforce on Pipedream to automatically update lead scores and segments based on user activity and engagement. This workflow triggers whenever there is a new interaction or update in Leadzen.ai, automatically updating the corresponding lead profile in Salesforce with new scores and segment tags. + +- **Email Marketing Automation**: Connect Leadzen.ai to an email marketing tool like Mailchimp on Pipedream. Use Leadzen.ai to analyze lead engagement and segment leads into different marketing campaigns in Mailchimp. This workflow automates the process of sending personalized email campaigns to leads based on their behavior and segment, ensuring that the right messages reach the right audience at the right time. + +- **Real-Time Lead Notification System**: Set up a workflow on Pipedream where Leadzen.ai triggers Slack notifications. Whenever a lead reaches a certain score threshold or enters a critical segment, this workflow sends an instant message to a designated Slack channel or directly to sales representatives. This enables immediate follow-up, enhancing the chances of converting high-potential leads into customers. diff --git a/components/leadzen_ai/actions/simple-search/simple-search.mjs b/components/leadzen_ai/actions/simple-search/simple-search.mjs new file mode 100644 index 0000000000000..cdaf1302d836e --- /dev/null +++ b/components/leadzen_ai/actions/simple-search/simple-search.mjs @@ -0,0 +1,55 @@ +import app from "../../leadzen_ai.app.mjs"; + +export default { + key: "leadzen_ai-simple-search", + name: "Simple Search", + description: "Fetches detailed LinkedIn profile information based on the provided URL. [See the documentation](https://api.leadzen.ai/docs#/People/people_search_detailed_api_people_linkedin_url_profile_post)", + version: "0.0.1", + type: "action", + props: { + app, + detailsUrl: { + propDefinition: [ + app, + "url", + ], + }, + }, + methods: { + simpleSearch(args = {}) { + return this.app.post({ + path: "/people/linkedin_url/profile", + ...args, + }); + }, + }, + async run({ $ }) { + const { + app, + simpleSearch, + detailsUrl, + } = this; + + const { id: searchId } = await simpleSearch({ + $, + data: { + details_url: detailsUrl, + }, + }); + + const response = await app.retry({ + $, + fn: app.getDetailedProfile, + delay: 5000, + searchId, + }); + + if (response?.status === "failed") { + $.export("$summary", "Failed to performed simple search"); + return response; + } + + $.export("$summary", `Successfully performed simple search with ID \`${response?.search_result?.id}\`.`); + return response; + }, +}; diff --git a/components/leadzen_ai/actions/unlock-email-details/unlock-email-details.mjs b/components/leadzen_ai/actions/unlock-email-details/unlock-email-details.mjs new file mode 100644 index 0000000000000..7e696f2bac5a9 --- /dev/null +++ b/components/leadzen_ai/actions/unlock-email-details/unlock-email-details.mjs @@ -0,0 +1,55 @@ +import app from "../../leadzen_ai.app.mjs"; + +export default { + key: "leadzen_ai-unlock-email-details", + name: "Unlock Email Details", + description: "Fetches the work email of a LinkedIn profile based on the specified URL. [See the documentation](https://api.leadzen.ai/docs#/People/people_search_work_email_api_people_linkedin_url_work_email_post)", + version: "0.0.1", + type: "action", + props: { + app, + url: { + propDefinition: [ + app, + "url", + ], + }, + }, + methods: { + unlockEmailDetails(args = {}) { + return this.app.post({ + path: "/people/linkedin_url/work_email", + ...args, + }); + }, + }, + async run({ $ }) { + const { + app, + unlockEmailDetails, + url, + } = this; + + const { id: searchId } = await unlockEmailDetails({ + $, + data: { + url, + }, + }); + + const response = await app.retry({ + $, + fn: app.getWorkEmail, + delay: 5000, + searchId, + }); + + if (response?.status === "failed") { + $.export("$summary", "Failed to unlock details for email"); + return response; + } + + $.export("$summary", `Successfully unlocked details for email with ID \`${response?.search_result?.id}\``); + return response; + }, +}; diff --git a/components/leadzen_ai/leadzen_ai.app.mjs b/components/leadzen_ai/leadzen_ai.app.mjs new file mode 100644 index 0000000000000..f5de91055c287 --- /dev/null +++ b/components/leadzen_ai/leadzen_ai.app.mjs @@ -0,0 +1,79 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "leadzen_ai", + propDefinitions: { + url: { + type: "string", + label: "URL", + description: "Details URL.", + }, + }, + methods: { + getUrl(path) { + return `https://api.leadzen.ai/api${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "accept": "application/json", + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + getDetailedProfile({ + searchId, ...args + } = {}) { + return this._makeRequest({ + path: `/people/linkedin_url/profile/${searchId}`, + ...args, + }); + }, + getWorkEmail({ + searchId, ...args + } = {}) { + return this._makeRequest({ + path: `/people/linkedin_url/work_email/${searchId}`, + ...args, + }); + }, + async retry({ + fn, retryCount = 1, maxCount = 3, delay = 2000, ...args + } = {}) { + const response = await fn(args); + + if (retryCount > maxCount) { + return response; + } + + if (response?.status === "running") { + console.log("Search is still running, retrying..."); + await new Promise((resolve) => setTimeout(resolve, delay)); + return this.retry({ + fn, + retryCount: retryCount + 1, + delay, + maxCount, + ...args, + }); + } + + return response; + }, + }, +}; diff --git a/components/leadzen_ai/package.json b/components/leadzen_ai/package.json new file mode 100644 index 0000000000000..76bc37c83fc89 --- /dev/null +++ b/components/leadzen_ai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/leadzen_ai", + "version": "0.1.0", + "description": "Pipedream Leadzen.ai Components", + "main": "leadzen_ai.app.mjs", + "keywords": [ + "pipedream", + "leadzen_ai" + ], + "homepage": "https://pipedream.com/apps/leadzen_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} diff --git a/components/leap/README.md b/components/leap/README.md new file mode 100644 index 0000000000000..74a25a1da722b --- /dev/null +++ b/components/leap/README.md @@ -0,0 +1,11 @@ +# Overview + +The Leap API enables automated interactions with the Leap.ai platform, which focuses on matching users with optimal job opportunities based on skills and preferences. In Pipedream, you can harness this API to create workflows that streamline the job search process, manage and analyze job matching data, or even integrate with other platforms to enhance the job seeking experience. With Pipedream's serverless execution environment, you can trigger these workflows on a schedule, via webhooks, or in response to events from other apps. + +# Example Use Cases + +- **Automated Job Notifications**: Send personalized job alerts to Slack or email when new matches are found. Using the Leap API with Pipedream, set up a workflow that checks for new job opportunities and then uses the Slack API or SMTP to notify the user. + +- **Job Match Data Aggregation**: Collect and store job match data in Google Sheets or Airtable. Create a Pipedream workflow that periodically calls the Leap API to retrieve the latest job matches and logs them in a Google Sheets spreadsheet or an Airtable base, enabling easy tracking and analysis. + +- **Professional Network Sync**: Synchronize job matches with LinkedIn profiles. Design a Pipedream workflow that uses the Leap API to fetch recent job recommendations and then, via the LinkedIn API, updates the user's profile with potential job interests or shares curated job opportunities with their network. diff --git a/components/learndash/README.md b/components/learndash/README.md new file mode 100644 index 0000000000000..640b5d4b7b9c2 --- /dev/null +++ b/components/learndash/README.md @@ -0,0 +1,11 @@ +# Overview + +The LearnDash API enables integration with LearnDash LMS, offering developers a way to automate course management tasks, access user data, and enhance the e-learning experience. Using Pipedream, you can build serverless workflows that react to events in LearnDash, sync data with other services, or automate repetitive tasks, all without managing infrastructure. + +# Example Use Cases + +- **Automated Course Enrollment upon Purchase**: When a user buys a course on a platform like WooCommerce, trigger a workflow in Pipedream that automatically enrolls the user in the corresponding LearnDash course. + +- **Sync Learner Progress with CRM**: Keep a CRM like HubSpot updated with user progress by triggering a Pipedream workflow each time a user completes a lesson or quiz in LearnDash, ensuring sales and support teams have up-to-date user information. + +- **Scheduled Reporting to Slack**: Generate daily or weekly reports on course completion rates or quiz scores, and send them to a Slack channel to keep the team informed on user progress and engagement. diff --git a/components/learnworlds/actions/enroll-user/enroll-user.mjs b/components/learnworlds/actions/enroll-user/enroll-user.mjs new file mode 100644 index 0000000000000..b377bb06ed363 --- /dev/null +++ b/components/learnworlds/actions/enroll-user/enroll-user.mjs @@ -0,0 +1,88 @@ +import { PRODUCT_TYPE_OPTIONS } from "../../common/constants.mjs"; +import learnworlds from "../../learnworlds.app.mjs"; + +export default { + key: "learnworlds-enroll-user", + name: "Enroll User", + description: "Enroll user to product. [See the documentation](https://www.learnworlds.dev/docs/api/3d5e79f96b44a-enroll-user-to-product)", + version: "0.0.1", + type: "action", + props: { + learnworlds, + userId: { + propDefinition: [ + learnworlds, + "userId", + ], + }, + productType: { + type: "string", + label: "Product Type", + description: "Type of the product.", + options: PRODUCT_TYPE_OPTIONS, + reloadProps: true, + }, + justification: { + type: "string", + label: "Justification", + description: "Any justification/note for the enrollment.", + optional: true, + }, + sendEnrollmentEmail: { + type: "boolean", + label: "Send Enrollment Email", + description: "Indication about whether the user should receive the enrollment email; true if she should receive the email, false if she should not.", + optional: true, + }, + }, + methods: { + capitalize(s) { + return String(s[0]).toUpperCase() + String(s).slice(1); + }, + }, + async additionalProps() { + const props = {}; + if (this.productType) { + props.productId = { + type: "string", + label: "Product Id", + description: "Unique Identifier of the product.", + options: async({ page }) => { + const { data } = await this.learnworlds[`list${this.capitalize(this.productType)}s`]({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + id: value, title: label, + }) => ({ + label, + value, + })); + }, + }; + } + return props; + }, + async run({ $ }) { + const product = await this.learnworlds[`get${this.capitalize(this.productType)}`]({ + productId: this.productId, + }); + + const response = await this.learnworlds.enrollUser({ + $, + userId: this.userId, + data: { + productId: this.productId, + productType: this.productType, + justification: this.justification, + price: product.price || product.original_price, + send_enrollment_email: this.sendEnrollmentEmail, + }, + }); + + $.export("$summary", "User successfully enrolled!"); + return response; + }, +}; diff --git a/components/learnworlds/common/constants.mjs b/components/learnworlds/common/constants.mjs new file mode 100644 index 0000000000000..949a4295df231 --- /dev/null +++ b/components/learnworlds/common/constants.mjs @@ -0,0 +1,5 @@ +export const PRODUCT_TYPE_OPTIONS = [ + "course", + "bundle", + "subscription", +]; diff --git a/components/learnworlds/learnworlds.app.mjs b/components/learnworlds/learnworlds.app.mjs new file mode 100644 index 0000000000000..acbab7c4fdc38 --- /dev/null +++ b/components/learnworlds/learnworlds.app.mjs @@ -0,0 +1,95 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "learnworlds", + propDefinitions: { + userId: { + type: "string", + label: "User Id", + description: "Unique identifier of the used.", + async options({ page }) { + const { data } = await this.listUsers({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + id: value, username: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.school_domain}/admin/api/v2`; + }, + _getHeaders() { + return { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "Lw-Client": `${this.$auth.oauth_client_id}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._getHeaders(), + ...opts, + }); + }, + enrollUser({ + userId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/users/${userId}/enrollment`, + ...opts, + }); + }, + getBundle({ productId }) { + return this._makeRequest({ + path: `/bundles/${productId}`, + }); + }, + getCourse({ productId }) { + return this._makeRequest({ + path: `/courses/${productId}`, + }); + }, + getSubscription({ productId }) { + return this._makeRequest({ + path: `/subscription-plans/${productId}`, + }); + }, + listBundles(opts = {}) { + return this._makeRequest({ + path: "/bundles", + ...opts, + }); + }, + listCourses(opts = {}) { + return this._makeRequest({ + path: "/courses", + ...opts, + }); + }, + listSubscriptions(opts = {}) { + return this._makeRequest({ + path: "/subscription-plans", + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users", + ...opts, + }); + }, + }, +}; diff --git a/components/learnworlds/package.json b/components/learnworlds/package.json new file mode 100644 index 0000000000000..ecb3b4557d825 --- /dev/null +++ b/components/learnworlds/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/learnworlds", + "version": "0.1.0", + "description": "Pipedream LearnWorlds Components", + "main": "learnworlds.app.mjs", + "keywords": [ + "pipedream", + "learnworlds" + ], + "homepage": "https://pipedream.com/apps/learnworlds", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/leexi/actions/create-call/create-call.mjs b/components/leexi/actions/create-call/create-call.mjs new file mode 100644 index 0000000000000..e1c68e2119abc --- /dev/null +++ b/components/leexi/actions/create-call/create-call.mjs @@ -0,0 +1,98 @@ +import app from "../../leexi.app.mjs"; + +export default { + key: "leexi-create-call", + name: "Create Call", + description: "Create a new call in Leexi. [See the documentation](https://developer.leexi.ai/)", + version: "0.0.1", + type: "action", + props: { + app, + recordingS3Key: { + type: "string", + label: "Recording S3 Key", + description: "The S3 key returned by the presign_recording_url endpoint.", + }, + externalId: { + type: "string", + label: "External ID", + description: "The ID of the call in your system.", + }, + integrationUserExternalId: { + type: "string", + label: "Integration User External ID", + description: "The external ID of the user making the call on your platform.", + }, + integrationUserName: { + type: "string", + label: "Integration User Name", + description: "The name of the user making the call on your platform.", + }, + direction: { + type: "string", + label: "Call Direction", + description: "The direction of the call (inbound or outbound).", + options: [ + "inbound", + "outbound", + ], + }, + performedAt: { + type: "string", + label: "Performed At", + description: "The start time of the call, in ISO8601 format. Eg. `2024-01-09T17:05:09+01:00`", + }, + description: { + type: "string", + label: "Description", + description: "A description of the call.", + optional: true, + }, + rawPhoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number of the caller.", + optional: true, + }, + }, + methods: { + createCall(args = {}) { + return this.app.post({ + path: "/calls", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createCall, + recordingS3Key, + externalId, + integrationUserExternalId, + integrationUserName, + direction, + performedAt, + description, + rawPhoneNumber, + } = this; + + const response = await createCall({ + $, + data: { + recording_s3_key: recordingS3Key, + external_id: externalId, + integration_user: { + external_id: integrationUserExternalId, + name: integrationUserName, + }, + direction, + performed_at: performedAt, + description, + raw_phone_number: rawPhoneNumber, + }, + }); + + $.export("$summary", "Successfully created a new call."); + return response; + }, +}; diff --git a/components/leexi/actions/create-presign-recording-url/create-presign-recording-url.mjs b/components/leexi/actions/create-presign-recording-url/create-presign-recording-url.mjs new file mode 100644 index 0000000000000..ba602c8bf3499 --- /dev/null +++ b/components/leexi/actions/create-presign-recording-url/create-presign-recording-url.mjs @@ -0,0 +1,78 @@ +import { readFileSync } from "fs"; +import app from "../../leexi.app.mjs"; + +export default { + key: "leexi-create-presign-recording-url", + name: "Create Presigned Recording URL", + description: "Creates a presigned URL for uploading a call recording. [See the documentation](https://developer.leexi.ai/)", + version: "0.0.1", + type: "action", + props: { + app, + extension: { + propDefinition: [ + app, + "extension", + ], + }, + filePath: { + type: "string", + label: "File Path", + description: "The path to the file to upload. Eg. `/tmp/recording.mp3`", + }, + }, + methods: { + createPresignedRecordingUrl(args = {}) { + return this.app.post({ + path: "/calls/presign_recording_url", + ...args, + }); + }, + uploadFile(args = {}) { + return this.app.put({ + ...args, + }); + }, + }, + async run({ $ }) { + const { + uploadFile, + createPresignedRecordingUrl, + extension, + filePath, + } = this; + + const response = await createPresignedRecordingUrl({ + $, + data: { + extension, + }, + }); + + if (!response.success) { + $.export("$error", "Failed to create a presigned URL for recording."); + return response; + } + + const { + data: { + headers, + url, + }, + } = response; + + const path = filePath?.startsWith("/tmp") + ? filePath + : `/tmp/${filePath}`; + + await uploadFile({ + $, + headers, + url, + data: readFileSync(path), + }); + + $.export("$summary", "Successfully created a presigned URL for recording"); + return response; + }, +}; diff --git a/components/leexi/actions/get-call/get-call.mjs b/components/leexi/actions/get-call/get-call.mjs new file mode 100644 index 0000000000000..3e400da5ddd09 --- /dev/null +++ b/components/leexi/actions/get-call/get-call.mjs @@ -0,0 +1,41 @@ +import app from "../../leexi.app.mjs"; + +export default { + key: "leexi-get-call", + name: "Get Call", + description: "Get details of a call by its ID. [See the documentation](https://developer.leexi.ai/)", + version: "0.0.1", + type: "action", + props: { + app, + callId: { + propDefinition: [ + app, + "callId", + ], + }, + }, + methods: { + getCall({ + callId, ...args + } = {}) { + return this.app._makeRequest({ + path: `/calls/${callId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + getCall, + callId, + } = this; + + const response = await getCall({ + $, + callId, + }); + $.export("$summary", `Successfully retrieved details for call ID \`${response.data?.uuid}\`.`); + return response; + }, +}; diff --git a/components/leexi/common/constants.mjs b/components/leexi/common/constants.mjs new file mode 100644 index 0000000000000..4a282e6bceee9 --- /dev/null +++ b/components/leexi/common/constants.mjs @@ -0,0 +1,45 @@ +const BASE_URL = "https://public-api.leexi.ai"; +const VERSION_PATH = "/v1"; +const LAST_CREATED_AT = "lastCreatedAt"; +const DEFAULT_LIMIT = 100; +const DEFAULT_MAX = 300; + +const EXTENSION_OPTIONS = [ + ".mp4", + ".mkv", + ".avi", + ".webm", + ".mov", + ".wmv", + ".mpg", + ".mpeg", + ".mp3", + ".wav", + ".aac", + ".flac", + ".ogg", + ".m4a", + ".wma", + ".opus", + ".aiff", + ".alac", + ".amr", + ".ape", + ".dts", + ".ac3", + ".mid", + ".mp2", + ".mpc", + ".ra", + ".tta", + ".vox", +]; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_LIMIT, + DEFAULT_MAX, + LAST_CREATED_AT, + EXTENSION_OPTIONS, +}; diff --git a/components/leexi/common/utils.mjs b/components/leexi/common/utils.mjs new file mode 100644 index 0000000000000..903b2593ed3c2 --- /dev/null +++ b/components/leexi/common/utils.mjs @@ -0,0 +1,11 @@ +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +export default { + iterate, +}; diff --git a/components/leexi/leexi.app.mjs b/components/leexi/leexi.app.mjs new file mode 100644 index 0000000000000..8594bf98bf31e --- /dev/null +++ b/components/leexi/leexi.app.mjs @@ -0,0 +1,126 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; + +export default { + type: "app", + app: "leexi", + propDefinitions: { + extension: { + type: "string", + label: "Recording File Extension", + description: "The file extension of the recording.", + options: constants.EXTENSION_OPTIONS, + }, + callId: { + type: "string", + label: "Call ID", + description: "The unique identifier of the call.", + async options({ page }) { + const { data } = await this.listCalls({ + params: { + page, + items: constants.DEFAULT_LIMIT, + }, + }); + return data.map(({ + uuid: value, direction, + }) => ({ + label: `${value} (${direction})`, + value, + })); + }, + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getAuth() { + const { + api_key_id: username, + api_key_secret: password, + } = this.$auth; + return { + username, + password, + }; + }, + _makeRequest({ + $ = this, url, path, ...args + } = {}) { + return axios($, { + url: url ?? this.getUrl(path), + ...(!url && { + auth: this.getAuth(), + }), + ...args, + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + put(args = {}) { + return this._makeRequest({ + method: "PUT", + ...args, + }); + }, + listCalls(args = {}) { + return this._makeRequest({ + debug: true, + path: "/calls", + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, resourceName, + max = constants.DEFAULT_MAX, + }) { + let page = 1; + let resourcesCount = 0; + + while (true) { + const response = + await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + page, + items: constants.DEFAULT_LIMIT, + }, + }); + + const nextResources = resourceName && response[resourceName] || response; + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + yield resource; + resourcesCount += 1; + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + if (nextResources.length < constants.DEFAULT_LIMIT) { + console.log("No next page found"); + return; + } + + page += 1; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); + }, + }, +}; diff --git a/components/leexi/package.json b/components/leexi/package.json new file mode 100644 index 0000000000000..60695e94c299a --- /dev/null +++ b/components/leexi/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/leexi", + "version": "0.1.0", + "description": "Pipedream Leexi Components", + "main": "leexi.app.mjs", + "keywords": [ + "pipedream", + "leexi" + ], + "homepage": "https://pipedream.com/apps/leexi", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} diff --git a/components/leexi/sources/common/polling.mjs b/components/leexi/sources/common/polling.mjs new file mode 100644 index 0000000000000..4a5e07ee18c78 --- /dev/null +++ b/components/leexi/sources/common/polling.mjs @@ -0,0 +1,80 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../leexi.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + setLastCreatedAt(value) { + this.db.set(constants.LAST_CREATED_AT, value); + }, + getLastCreatedAt() { + return this.db.get(constants.LAST_CREATED_AT); + }, + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getResourceName() { + throw new ConfigurationError("getResourceName is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + async processResources(resources) { + const { + setLastCreatedAt, + processResource, + } = this; + + const [ + lastResource, + ] = resources; + + if (lastResource?.created_at) { + setLastCreatedAt(lastResource.created_at); + } + + Array.from(resources) + .reverse() + .forEach(processResource); + }, + }, + async run() { + const { + app, + getResourcesFn, + getResourcesFnArgs, + getResourceName, + processResources, + } = this; + + const resources = await app.paginate({ + resourcesFn: getResourcesFn(), + resourcesFnArgs: getResourcesFnArgs(), + resourceName: getResourceName(), + }); + + processResources(resources); + }, +}; diff --git a/components/leexi/sources/new-call-created/new-call-created.mjs b/components/leexi/sources/new-call-created/new-call-created.mjs new file mode 100644 index 0000000000000..64f10f28fab6b --- /dev/null +++ b/components/leexi/sources/new-call-created/new-call-created.mjs @@ -0,0 +1,34 @@ +import common from "../common/polling.mjs"; + +export default { + ...common, + key: "leexi-new-call-created", + name: "New Call Created", + description: "Emit new event when a new call is created. [See the documentation](https://developer.leexi.ai/)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceName() { + return "data"; + }, + getResourcesFn() { + return this.app.listCalls; + }, + getResourcesFnArgs() { + return { + params: { + from: this.getLastCreatedAt(), + }, + }; + }, + generateMeta(resource) { + return { + id: resource.uuid, + summary: `New Call: ${resource.direction}`, + ts: Date.parse(resource.created_at), + }; + }, + }, +}; diff --git a/components/leiga/README.md b/components/leiga/README.md new file mode 100644 index 0000000000000..ba988c1b91540 --- /dev/null +++ b/components/leiga/README.md @@ -0,0 +1,11 @@ +# Overview + +The Leiga API unlocks the potential to automate and streamline your graphic design operations. By integrating with Pipedream, you can trigger workflows using events from other apps, manipulate and store data, or even orchestrate complex operations among various services. Whether you're looking to automate image processing, streamline asset management, or create dynamic marketing campaigns, Pipedream's serverless platform lets you build scalable and efficient workflows with the Leiga API. + +# Example Use Cases + +- **Automated Image Processing Pipeline**: Use the Leiga API to create a workflow that listens for new images uploaded to a cloud storage platform like Dropbox. Once a new image is detected, the workflow can automatically send it to Leiga for processing, and then store the processed image back in Dropbox or another service like Google Drive. + +- **Dynamic Marketing Material Generation**: Combine Leiga with a CRM app like HubSpot in Pipedream. Set up a workflow to generate personalized marketing materials using customer data from HubSpot. When a new lead is added to your CRM, Pipedream can trigger Leiga to produce custom graphics tailored to the lead's interests, significantly enhancing your engagement strategy. + +- **Social Media Content Management**: Integrate Leiga API with social media platforms such as Twitter or Instagram via Pipedream. Construct a workflow where design requests posted to a specific Slack channel are automatically picked up, processed through Leiga to create social media-ready images, and then posted to your chosen social media accounts, all without manual intervention. diff --git a/components/leiga/actions/create-issue/create-issue.mjs b/components/leiga/actions/create-issue/create-issue.mjs new file mode 100644 index 0000000000000..4b9015ab116db --- /dev/null +++ b/components/leiga/actions/create-issue/create-issue.mjs @@ -0,0 +1,108 @@ +import { ConfigurationError } from "@pipedream/platform"; +import leiga from "../../leiga.app.mjs"; + +export default { + key: "leiga-create-issue", + name: "Create Issue", + description: "Creates a new issue within Leiga. [See the documentation](https://apidog.com/apidoc/shared-5a741107-c211-410f-880c-048d1917c984/api-3741813)", + version: "0.0.1", + type: "action", + props: { + leiga, + projectId: { + propDefinition: [ + leiga, + "projectId", + ], + description: "The project ID in which you wish to create issue.", + }, + issueTypeId: { + propDefinition: [ + leiga, + "issueTypeId", + ({ projectId }) => ({ + projectId, + }), + ], + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.issueTypeId) { + const { data: { fields } } = await this.leiga.getIssueSchema({ + params: { + projectId: this.projectId, + issueTypeId: this.issueTypeId, + }, + }); + + for (const field of fields) { + let propType = "string"; + let optionsField = {}; + + if (field.options && field.options.length) { + optionsField = { + options: field.options.map((item) => ({ + label: item.name, + value: `${item.value}`, + })), + }; + } + + if (field.multipleChoice) { + propType += "[]"; + } + + let dateTimeFlag = ""; + if (field.controlCode === "datetime-range") { + dateTimeFlag = "DateTimeFlag"; + } + + props[field.fieldCode + dateTimeFlag] = { + type: propType, + label: field.customFieldName, + description: field.fieldDescription || `The ${field.fieldCode} of the issue.`, + optional: !field.requiredFlag, + ...optionsField, + }; + } + } + return props; + }, + async run({ $ }) { + const { + leiga, + projectId, + issueTypeId, + ...props + } = this; + + const data = {}; + for (let [ + key, + value, + ] of Object.entries(props)) { + if (key.endsWith("DateTimeFlag")) { + key = key.slice(0, - 12); + value = Date.parse(value); + } + data[key] = value; + } + + const response = await leiga.createIssue({ + $, + data: { + projectId, + issueTypeId, + data, + }, + }); + if (response.code != "0") { + throw new ConfigurationError(response.msg); + } + + $.export("$summary", `Successfully created issue with Id: ${response.data.id}`); + return response; + }, +}; diff --git a/components/leiga/leiga.app.mjs b/components/leiga/leiga.app.mjs new file mode 100644 index 0000000000000..007d937ff69e2 --- /dev/null +++ b/components/leiga/leiga.app.mjs @@ -0,0 +1,117 @@ +import { + ConfigurationError, axios, +} from "@pipedream/platform"; + +export default { + type: "app", + app: "leiga", + propDefinitions: { + issueTypeId: { + type: "string", + label: "Issue Type ID", + description: "The Issue Type ID in which you wish to create issue.", + async options({ projectId }) { + const { data } = await this.listIssueTypes({ + params: { + projectId, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + projectId: { + type: "string", + label: "Project ID", + description: "The project ID in which you wish to monitor for new issues.", + async options() { + const { data } = await this.listProjects(); + + return data.map(({ + id: value, pname: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://app.leiga.com/openapi/api"; + }, + _headers() { + return { + "Content-Type": "application/json", + "accessToken": `${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...otherOpts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._headers(), + ...otherOpts, + }); + }, + validadeRequest(response) { + if (response.msg) { + throw new ConfigurationError(response.msg); + } + return response; + }, + getIssueSchema(opts = {}) { + return this._makeRequest({ + path: "/issue/issue-scheme", + ...opts, + }); + }, + createHook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhook/add", + ...opts, + }); + }, + deleteHook(opts = {}) { + return this._makeRequest({ + method: "DELETE", + path: "/webhook/delete", + ...opts, + }); + }, + async listIssueTypes(opts = {}) { + const response = await this._makeRequest({ + path: "/issue/type-list", + ...opts, + }); + return this.validadeRequest(response); + }, + async listProjects() { + const response = await this._makeRequest({ + path: "/project/list", + }); + return this.validadeRequest(response); + }, + async listWebhookEvents(opts = {}) { + const response = await this._makeRequest({ + path: "/webhook/list-events", + ...opts, + }); + return this.validadeRequest(response); + }, + createIssue(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/issue/add", + ...opts, + }); + }, + }, +}; diff --git a/components/leiga/package.json b/components/leiga/package.json new file mode 100644 index 0000000000000..e7a6ac302cbe1 --- /dev/null +++ b/components/leiga/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/leiga", + "version": "0.1.0", + "description": "Pipedream Leiga Components", + "main": "leiga.app.mjs", + "keywords": [ + "pipedream", + "leiga" + ], + "homepage": "https://pipedream.com/apps/leiga", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" + } +} + diff --git a/components/leiga/sources/common/base.mjs b/components/leiga/sources/common/base.mjs new file mode 100644 index 0000000000000..eecc052dc0fad --- /dev/null +++ b/components/leiga/sources/common/base.mjs @@ -0,0 +1,68 @@ +import leiga from "../../leiga.app.mjs"; + +export default { + props: { + leiga, + http: "$.interface.http", + db: "$.service.db", + projectId: { + propDefinition: [ + leiga, + "projectId", + ], + }, + }, + methods: { + emitEvent(body) { + this.$emit(body, { + id: body.data.issue.id, + summary: this.getSummary(body), + ts: body.data.date, + }); + }, + setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getHookId() { + return this.db.get("hookId"); + }, + }, + hooks: { + async activate() { + const { data: events } = await this.leiga.listWebhookEvents({ + params: { + projectId: this.projectId, + }, + }); + const eventName = this.getEventName(); + const [ + { eventId }, + ] = events.filter((event) => (event.typeCode === "issue" && event.eventName === eventName)); + + const { data: { webhookId } } = await this.leiga.createHook({ + data: { + name: `Pipedream webhook - New ${eventName}d issue`, + state: "enabled", + type: "ligaAI", + projectId: this.projectId, + eventIds: [ + eventId, + ], + url: this.http.endpoint, + }, + }); + this.setHookId(webhookId); + }, + async deactivate() { + const hookId = this.getHookId(); + await this.leiga.deleteHook({ + data: { + webhookId: hookId, + }, + }); + }, + }, + async run({ body }) { + this.emitEvent(body); + }, +}; diff --git a/components/leiga/sources/deleted-issue-instant/deleted-issue-instant.mjs b/components/leiga/sources/deleted-issue-instant/deleted-issue-instant.mjs new file mode 100644 index 0000000000000..ad3a51562c931 --- /dev/null +++ b/components/leiga/sources/deleted-issue-instant/deleted-issue-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "leiga-deleted-issue-instant", + name: "New Deleted Issue (Instant)", + description: "Emit new event when an issue is deleted in Leiga.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventName() { + return "delete"; + }, + getSummary(body) { + return `New issue deleted with Id: ${body.data.issue.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/leiga/sources/deleted-issue-instant/test-event.mjs b/components/leiga/sources/deleted-issue-instant/test-event.mjs new file mode 100644 index 0000000000000..13528a281a3ae --- /dev/null +++ b/components/leiga/sources/deleted-issue-instant/test-event.mjs @@ -0,0 +1,61 @@ +export default { + "data": { + "issue": { + "createBy": { + "email": "email@test.com", + "id": 123456, + "name": "Name Test" + }, + "createTime": 123456789, + "description": "description test", + "estimatePoint": { + "autoCalculateFlag": 1 + }, + "id": 123456, + "labour": { + "remainingModified": 0 + }, + "number": 1, + "project": { + "description": "Explore Leiga to streamline your workflow and enhance productivity.", + "id": 123456, + "name": "Sample Project" + }, + "status": { + "category": { + "id": 2, + "name": "Todo" + }, + "code": "bug-pending", + "id": 123456, + "name": "New" + }, + "summary": "sdummary test", + "type": { + "code": "bug", + "id": 123456, + "name": "Bug" + }, + "updateBy": { + "email": "email@test.com", + "id": 123456, + "name": "Name Test" + }, + "updateTime": 123456789, + "url": "https://app.leiga.com/work/table?pid=123456&issueid=123456" + } + }, + "date": 123456789, + "tenant": { + "id": 40580924 + }, + "trigger": { + "date": 123456789, + "user": { + "email": "email@test.com", + "id": 123456, + "name": "Name Test" + } + }, + "type": "Issue.Delete", +} \ No newline at end of file diff --git a/components/leiga/sources/new-issue-instant/new-issue-instant.mjs b/components/leiga/sources/new-issue-instant/new-issue-instant.mjs new file mode 100644 index 0000000000000..5628df54082a3 --- /dev/null +++ b/components/leiga/sources/new-issue-instant/new-issue-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "leiga-new-issue-instant", + name: "New Issue (Instant)", + description: "Emit new event when there is a new issue in Leiga for the specified project.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventName() { + return "create"; + }, + getSummary(body) { + return `New issue created with Id: ${body.data.issue.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/leiga/sources/new-issue-instant/test-event.mjs b/components/leiga/sources/new-issue-instant/test-event.mjs new file mode 100644 index 0000000000000..416218a97035c --- /dev/null +++ b/components/leiga/sources/new-issue-instant/test-event.mjs @@ -0,0 +1,61 @@ +export default { + "data": { + "issue": { + "createBy": { + "email": "email@test.com", + "id": 123456, + "name": "Name Test" + }, + "createTime": 123456789, + "description": "description test", + "estimatePoint": { + "autoCalculateFlag": 1 + }, + "id": 123456, + "labour": { + "remainingModified": 0 + }, + "number": 1, + "project": { + "description": "Explore Leiga to streamline your workflow and enhance productivity.", + "id": 123456, + "name": "Sample Project" + }, + "status": { + "category": { + "id": 2, + "name": "Todo" + }, + "code": "bug-pending", + "id": 123456, + "name": "New" + }, + "summary": "sdummary test", + "type": { + "code": "bug", + "id": 123456, + "name": "Bug" + }, + "updateBy": { + "email": "email@test.com", + "id": 123456, + "name": "Name Test" + }, + "updateTime": 123456789, + "url": "https://app.leiga.com/work/table?pid=123456&issueid=123456" + } + }, + "date": 123456789, + "tenant": { + "id": 40580924 + }, + "trigger": { + "date": 123456789, + "user": { + "email": "email@test.com", + "id": 123456, + "name": "Name Test" + } + }, + "type": "Issue.Create" +} \ No newline at end of file diff --git a/components/leiga/sources/updated-issue-instant/test-event.mjs b/components/leiga/sources/updated-issue-instant/test-event.mjs new file mode 100644 index 0000000000000..7a42ae6992765 --- /dev/null +++ b/components/leiga/sources/updated-issue-instant/test-event.mjs @@ -0,0 +1,61 @@ +export default { + "data": { + "issue": { + "createBy": { + "email": "email@test.com", + "id": 123456, + "name": "Name Test" + }, + "createTime": 123456789, + "description": "description test", + "estimatePoint": { + "autoCalculateFlag": 1 + }, + "id": 123456, + "labour": { + "remainingModified": 0 + }, + "number": 1, + "project": { + "description": "Explore Leiga to streamline your workflow and enhance productivity.", + "id": 123456, + "name": "Sample Project" + }, + "status": { + "category": { + "id": 2, + "name": "Todo" + }, + "code": "bug-pending", + "id": 123456, + "name": "New" + }, + "summary": "sdummary test", + "type": { + "code": "bug", + "id": 123456, + "name": "Bug" + }, + "updateBy": { + "email": "email@test.com", + "id": 123456, + "name": "Name Test" + }, + "updateTime": 123456789, + "url": "https://app.leiga.com/work/table?pid=123456&issueid=123456" + } + }, + "date": 123456789, + "tenant": { + "id": 40580924 + }, + "trigger": { + "date": 123456789, + "user": { + "email": "email@test.com", + "id": 123456, + "name": "Name Test" + } + }, + "type": "Issue.Update" +} \ No newline at end of file diff --git a/components/leiga/sources/updated-issue-instant/updated-issue-instant.mjs b/components/leiga/sources/updated-issue-instant/updated-issue-instant.mjs new file mode 100644 index 0000000000000..54c5df1ba6fb0 --- /dev/null +++ b/components/leiga/sources/updated-issue-instant/updated-issue-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "leiga-updated-issue-instant", + name: "New Updated Issue (Instant)", + description: "Emit new event when an existing issue is updated in Leiga.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventName() { + return "update"; + }, + getSummary(body) { + return `New issue updated with Id: ${body.data.issue.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/lemlist/README.md b/components/lemlist/README.md index 53104bc36d60b..2cf79c494038d 100644 --- a/components/lemlist/README.md +++ b/components/lemlist/README.md @@ -1,10 +1,11 @@ # Overview -With the lemlist API you can easily create campaigns with personalized emails -and track the results. +The lemlist API allows for the automation of personalized email outreach campaigns. With it, users can manage campaigns, leads, and handle email interactions programmatically. When utilized within Pipedream, this API enables the creation of seamless workflows that can bridge the gap between lead generation, email marketing, and follow-up processes, leading to more efficient engagement strategies. -Some things you can build with the lemlist API: +# Example Use Cases -- A system to personalize emails based on information in a database -- A campaign tracking system to see which emails are being opened and clicked -- A way to A/B test different email content to see what works best +- **Lead Management Automation**: When new leads are captured via a form on your website, use a Pipedream workflow to automatically add them to a lemlist campaign. This can ensure timely follow-up emails are sent without manual input, fostering prompt communication. + +- **Email Engagement Tracking**: Trigger a workflow in Pipedream when a lead opens an email or clicks a link. This event can kickstart a series of actions, such as updating the lead status in your CRM, like HubSpot, or sending a notification to a Slack channel to alert your sales team. + +- **Campaign Results Digest**: Schedule a daily or weekly Pipedream workflow to fetch campaign statistics from lemlist. These results can be compiled into a report and sent to Google Sheets for analysis or emailed to your team to review performance and adjust strategies accordingly. diff --git a/components/lemon_squeezy/README.md b/components/lemon_squeezy/README.md new file mode 100644 index 0000000000000..327a62043383f --- /dev/null +++ b/components/lemon_squeezy/README.md @@ -0,0 +1,11 @@ +# Overview + +The Lemon Squeezy API lets you manage and sell digital products with ease. On Pipedream, you can integrate Lemon Squeezy with your sales, marketing, and support tools to automate tasks like customer follow-ups, license management, and sales reporting. With Pipedream's serverless platform, you can create workflows that trigger on Lemon Squeezy events or schedule tasks, process data, and connect to a vast array of other apps and services without writing backend infrastructure. + +# Example Use Cases + +- **Automated Customer Onboarding Emails**: When a new purchase is made in Lemon Squeezy, trigger a workflow in Pipedream to send personalized onboarding emails via SendGrid. This helps in engaging customers right after they make a purchase. + +- **License Key Distribution**: Generate and distribute license keys through Pipedream after a sale. Use Lemon Squeezy to handle the sale, then trigger a Pipedream workflow that creates a license in Lemon Squeezy and emails it to the customer using an email service like Mailgun. + +- **Daily Sales Reports to Slack**: Compile daily sales data from Lemon Squeezy and post a summary report to a Slack channel. This Pipedream workflow can be scheduled to run every day, aggregating sales information and keeping your team informed automatically. diff --git a/components/lessaccounting/README.md b/components/lessaccounting/README.md new file mode 100644 index 0000000000000..d0e88e760e19e --- /dev/null +++ b/components/lessaccounting/README.md @@ -0,0 +1,11 @@ +# Overview + +The LessAccounting API allows you to automate accounting tasks by providing endpoints for managing transactions, invoices, expenses, and contacts. Pipedream’s serverless platform leverages this by enabling you to create workflows that can respond to events, process data, and integrate with other apps. With Pipedream, you can craft custom automation sequences, sync data across applications, and build powerful integrations without managing infrastructure. + +# Example Use Cases + +- **Automate Invoice Creation and Notification**: When a sale is completed in your e-commerce system, use Pipedream to trigger an automation that creates an invoice in LessAccounting. Then, send a notification to the sales team via Slack, and email the invoice to the customer. + +- **Expenses Tracking and Reporting**: Set up a workflow on Pipedream that monitors your bank transactions via webhook or scheduled polling. When a new expense is detected, automatically create an expense entry in LessAccounting and forward a weekly report to Google Sheets for easy visualization and sharing. + +- **Sync Contacts Between CRM and Accounting**: Whenever a new contact is added to your CRM platform, such as Salesforce, a Pipedream workflow can be triggered to create or update the corresponding contact in LessAccounting, ensuring your accounting records always stay in sync with your customer database. diff --git a/components/lessonspace/.gitignore b/components/lessonspace/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/lessonspace/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/lessonspace/README.md b/components/lessonspace/README.md new file mode 100644 index 0000000000000..226702ae963e8 --- /dev/null +++ b/components/lessonspace/README.md @@ -0,0 +1,11 @@ +# Overview + +The Lessonspace API provides the functionality to manage and interact with online learning spaces. By integrating with the Lessonspace API on Pipedream, you can automate processes such as scheduling lessons, managing users, and creating learning spaces. Pipedream's serverless platform allows you to set up workflows triggered by various events, which can interact with the Lessonspace API to streamline educational operations and enhance the learning experience without manual intervention. + +# Example Use Cases + +- **Automated Lesson Scheduling**: Schedule lessons automatically in Lessonspace whenever a new event is created in a Google Calendar. This workflow can monitor your Google Calendar for new events tagged with "lesson" and use the Lessonspace API to create corresponding sessions. + +- **Student Onboarding Automation**: Automatically create user profiles in Lessonspace when a new student fills out a registration form in Typeform. The workflow listens for new Typeform submissions and uses the collected data to set up a new user in Lessonspace, sending welcome emails with session details. + +- **Lesson Reminder System**: Send SMS reminders to students using Twilio before a scheduled lesson begins. This workflow can check the Lessonspace for upcoming lessons and trigger SMS notifications via Twilio to ensure students remember to attend. diff --git a/components/lessonspace/actions/launch-space/launch-space.mjs b/components/lessonspace/actions/launch-space/launch-space.mjs new file mode 100644 index 0000000000000..259e476ece95c --- /dev/null +++ b/components/lessonspace/actions/launch-space/launch-space.mjs @@ -0,0 +1,73 @@ +import app from "../../lessonspace.app.mjs"; + +export default { + key: "lessonspace-launch-space", + name: "Launch Space", + description: "Launch a unified space on Lessonspace. [See the documentation](https://api.thelessonspace.com/v2/docs/#tag/Spaces-greater-Launch)", + version: "0.0.1", + type: "action", + props: { + app, + spaceId: { + propDefinition: [ + app, + "spaceId", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + allowGuests: { + propDefinition: [ + app, + "allowGuests", + ], + }, + recordContent: { + propDefinition: [ + app, + "recordContent", + ], + }, + transcribe: { + propDefinition: [ + app, + "transcribe", + ], + }, + recordAv: { + propDefinition: [ + app, + "recordAv", + ], + }, + userName: { + propDefinition: [ + app, + "userName", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.launchSpace({ + $, + data: { + id: this.spaceId, + name: this.name, + allow_guests: this.allowGuests, + record_content: this.recordContent, + transcribe: this.transcribe, + record_av: this.recordAv, + user: { + name: this.userName, + }, + }, + }); + $.export("$summary", `Successfully launched space named ${this.name}`); + return response; + }, +}; diff --git a/components/lessonspace/app/lessonspace.app.ts b/components/lessonspace/app/lessonspace.app.ts deleted file mode 100644 index 65c584a2ee7d6..0000000000000 --- a/components/lessonspace/app/lessonspace.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "lessonspace", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/lessonspace/lessonspace.app.mjs b/components/lessonspace/lessonspace.app.mjs new file mode 100644 index 0000000000000..35d969cefa456 --- /dev/null +++ b/components/lessonspace/lessonspace.app.mjs @@ -0,0 +1,72 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "lessonspace", + propDefinitions: { + spaceId: { + type: "string", + label: "Space ID", + description: "The ID of the Space", + }, + name: { + type: "string", + label: "Name", + description: "The name of the Space", + }, + allowGuests: { + type: "boolean", + label: "Allow Guests", + description: "Whether to allow guests or not in the Space", + }, + recordContent: { + type: "boolean", + label: "Record Content", + description: "Whether or not the space content will be recorded for this session", + }, + transcribe: { + type: "boolean", + label: "Transcribe", + description: "Whether or not a transcription will be generated for this session", + }, + recordAv: { + type: "boolean", + label: "Record AV", + description: "Whether or not audio and video will be recorded in this session", + }, + userName: { + type: "string", + label: "User Name", + description: "Full name of the person joining this space", + }, + }, + methods: { + _baseUrl() { + return "https://api.thelessonspace.com/v2"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + "Authorization": `Organisation ${this.$auth.api_key}`, + ...headers, + }, + }); + }, + + async launchSpace(args = {}) { + return this._makeRequest({ + path: "/spaces/launch/", + method: "post", + ...args, + }); + }, + }, +}; diff --git a/components/lessonspace/package.json b/components/lessonspace/package.json index 6a2d6c4fd340e..fadf9462cc446 100644 --- a/components/lessonspace/package.json +++ b/components/lessonspace/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/lessonspace", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream Lessonspace Components", - "main": "dist/app/lessonspace.app.mjs", + "main": "lessonspace.app.mjs", "keywords": [ "pipedream", "lessonspace" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/lessonspace", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/let_s_enhance/README.md b/components/let_s_enhance/README.md index df57568a5f399..32b90680e5867 100644 --- a/components/let_s_enhance/README.md +++ b/components/let_s_enhance/README.md @@ -1,10 +1,11 @@ # Overview -Some of the great things you can build using the Let's Enhance API include: - -- A "before and after" photo comparison tool, to show people how your product - can improve their images -- An online photo editor, to offer users a way to improve their photos without - expensive software -- A mobile app for editing photos on the go -- A website or blog where you can offer photo enhancement services +The Let's Enhance API provides a powerful way to upscale and enhance images through AI-driven processes, directly accessible via Pipedream. By using this API, developers can automate the enhancement of images, whether it’s for refining product photos for an e-commerce platform or improving user-generated content for better display across digital mediums. This API's capabilities can be integrated into a myriad of workflows where image quality is paramount. + +# Example Use Cases + +- **E-Commerce Image Enhancement**: Automatically upscale and enhance product images when they are uploaded to an e-commerce platform. Upon detecting a new image upload in the platform (like Shopify), Pipedream can trigger a workflow that sends the image to the Let's Enhance API and replaces the original with the enhanced version. + +- **Real Estate Photo Optimization**: Streamline the process of listing properties by enhancing real estate photos. Whenever a new property is added to a CRM system or a database like Airtable, Pipedream can start a workflow to enhance the property images using Let's Enhance API, ensuring high-quality visuals for potential buyers browsing online. + +- **Social Media Content Quality Control**: Maintain a consistent quality standard for user-generated content on social media or community platforms. Pipedream can monitor submissions, pass the images through the Let's Enhance API, and then publish or store the improved versions, thereby ensuring that the platform's visual content remains sharp and engaging. diff --git a/components/let_s_enhance/package.json b/components/let_s_enhance/package.json new file mode 100644 index 0000000000000..46679b8e12746 --- /dev/null +++ b/components/let_s_enhance/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/let_s_enhance", + "version": "0.6.0", + "description": "Pipedream let_s_enhance Components", + "main": "let_s_enhance.app.mjs", + "keywords": [ + "pipedream", + "let_s_enhance" + ], + "homepage": "https://pipedream.com/apps/let_s_enhance", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/letterdrop/README.md b/components/letterdrop/README.md new file mode 100644 index 0000000000000..68963a9611deb --- /dev/null +++ b/components/letterdrop/README.md @@ -0,0 +1,11 @@ +# Overview + +The Letterdrop API enables automated content marketing workflows, streamlining the process of creating, delivering, and tracking email campaigns and newsletters. By harnessing this API within Pipedream, users can craft data-driven, personalized content distribution strategies, integrate with CRMs for targeted outreach, and monitor engagement metrics to refine future campaigns. In essence, it's a tool that can help you engage your audience efficiently by automating repetitive content operations and aggregating performance data for actionable insights. + +# Example Use Cases + +- **Automated Content Distribution Workflow**: Trigger an automated sequence as soon as a new blog post is published. The workflow would use the RSS feed from a blog to capture new content, then utilize the Letterdrop API to create a newsletter issue and distribute it to a predefined email list. The end result is a hands-off approach to ensure your audience is regularly updated with fresh content. + +- **CRM-Driven Newsletter Personalization**: Leverage customer data from a CRM like Salesforce to segment and personalize newsletter content. When new customer data is updated in Salesforce, a Pipedream workflow can activate, fetching the relevant information and using the Letterdrop API to tailor and send newsletters that resonate with different segments of your audience, enhancing the personal touch of your campaigns. + +- **Performance Tracking to Data Visualization Tools**: Connect Letterdrop to data visualization tools such as Google Sheets or Tableau. After each newsletter campaign, the workflow could extract engagement metrics through the Letterdrop API and append them to a Google Sheet. This sheet could then be used to create reports or dashboards in Tableau, providing a clear view of campaign performance and informing future content strategy. diff --git a/components/letterdrop/actions/add-subscriber/add-subscriber.mjs b/components/letterdrop/actions/add-subscriber/add-subscriber.mjs new file mode 100644 index 0000000000000..6a62c052fc137 --- /dev/null +++ b/components/letterdrop/actions/add-subscriber/add-subscriber.mjs @@ -0,0 +1,76 @@ +import app from "../../letterdrop.app.mjs"; + +export default { + key: "letterdrop-add-subscriber", + name: "Add Subscriber", + description: "Adds a new subscriber to your Letterdrop publication. [See the documentation](https://docs.letterdrop.com/api#add-subscriber)", + version: "0.0.1", + type: "action", + props: { + app, + email: { + propDefinition: [ + app, + "email", + ], + }, + welcomeEmail: { + type: "boolean", + label: "Send Welcome Email", + description: "Whether the subscriber should receive a welcome email", + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "The name of the subscriber", + optional: true, + }, + title: { + type: "string", + label: "Title", + description: "Job title of the subscriber", + optional: true, + }, + }, + methods: { + addSubscriber(args = {}) { + return this.app.post({ + path: "/subscriber/add", + ...args, + }); + }, + }, + async run({ $ }) { + const { + addSubscriber, + email, + welcomeEmail, + name, + title, + } = this; + + const additionalData = { + ...(name && { + name, + }), + ...(title && { + title, + }), + }; + + const response = await addSubscriber({ + $, + data: { + email, + welcomeEmail, + ...(Object.keys(additionalData).length && { + additionalData, + }), + }, + }); + + $.export("$summary", `Successfully added subscriber with email \`${response.email}\``); + return response; + }, +}; diff --git a/components/letterdrop/actions/draft-post/draft-post.mjs b/components/letterdrop/actions/draft-post/draft-post.mjs new file mode 100644 index 0000000000000..a8bd721d7dc3f --- /dev/null +++ b/components/letterdrop/actions/draft-post/draft-post.mjs @@ -0,0 +1,56 @@ +import app from "../../letterdrop.app.mjs"; + +export default { + key: "letterdrop-draft-post", + name: "Draft Blog Post", + description: "Drafts a new blog post in your workspace with the required title and content, and optional images and tags. [See the documentation](https://docs.letterdrop.com/api#draft-post)", + version: "0.0.1", + type: "action", + props: { + app, + title: { + type: "string", + label: "Title", + description: "The title of the blog post", + }, + subtitle: { + type: "string", + label: "Subtitle", + description: "The subtitle of the blog post", + optional: true, + }, + html: { + type: "string", + label: "Content", + description: "The HTML content of the blog post", + }, + }, + methods: { + draftPost(args = {}) { + return this.app.post({ + path: "/post/draft", + ...args, + }); + }, + }, + async run({ $ }) { + const { + draftPost, + title, + subtitle, + html, + } = this; + + const response = await draftPost({ + $, + data: { + title, + subtitle, + html, + }, + }); + + $.export("$summary", `Successfully drafted the post with link \`${response.draftLink}\``); + return response; + }, +}; diff --git a/components/letterdrop/actions/remove-subscriber/remove-subscriber.mjs b/components/letterdrop/actions/remove-subscriber/remove-subscriber.mjs new file mode 100644 index 0000000000000..c51700df02d25 --- /dev/null +++ b/components/letterdrop/actions/remove-subscriber/remove-subscriber.mjs @@ -0,0 +1,42 @@ +import app from "../../letterdrop.app.mjs"; + +export default { + key: "letterdrop-remove-subscriber", + name: "Remove Subscriber", + description: "Removes a subscriber from your publication if the email matches an existing one. [See the documentation](https://docs.letterdrop.com/api#remove-subscriber)", + version: "0.0.1", + type: "action", + props: { + app, + email: { + propDefinition: [ + app, + "email", + ], + }, + }, + methods: { + removeSubscriber(args = {}) { + return this.app.post({ + path: "/subscriber/remove", + ...args, + }); + }, + }, + async run({ $ }) { + const { + removeSubscriber, + email, + } = this; + + const response = await removeSubscriber({ + $, + data: { + email, + }, + }); + + $.export("$summary", `Successfully removed subscriber with email \`${response.email}\``); + return response; + }, +}; diff --git a/components/letterdrop/common/constants.mjs b/components/letterdrop/common/constants.mjs new file mode 100644 index 0000000000000..509422b444469 --- /dev/null +++ b/components/letterdrop/common/constants.mjs @@ -0,0 +1,11 @@ +const BASE_URL = "https://api.letterdrop.com"; +const VERSION_PATH = "/api/v1"; +const DEFAULT_MAX = 600; +const DEFAULT_LIMIT = 100; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_MAX, + DEFAULT_LIMIT, +}; diff --git a/components/letterdrop/letterdrop.app.mjs b/components/letterdrop/letterdrop.app.mjs new file mode 100644 index 0000000000000..9da7195762a4c --- /dev/null +++ b/components/letterdrop/letterdrop.app.mjs @@ -0,0 +1,44 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "letterdrop", + propDefinitions: { + email: { + type: "string", + label: "Email", + description: "The email address of the subscriber", + }, + }, + methods: { + getApiKey() { + return this.$auth.api_key; + }, + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Content-Type": "application/json", + "api-key": this.getApiKey(), + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + }) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + }, +}; diff --git a/components/letterdrop/package.json b/components/letterdrop/package.json new file mode 100644 index 0000000000000..a1b41706e5dbb --- /dev/null +++ b/components/letterdrop/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/letterdrop", + "version": "0.1.0", + "description": "Pipedream Letterdrop Components", + "main": "letterdrop.app.mjs", + "keywords": [ + "pipedream", + "letterdrop" + ], + "homepage": "https://pipedream.com/apps/letterdrop", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/letterdrop/sources/common/webhook.mjs b/components/letterdrop/sources/common/webhook.mjs new file mode 100644 index 0000000000000..984a602bcdeca --- /dev/null +++ b/components/letterdrop/sources/common/webhook.mjs @@ -0,0 +1,31 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../letterdrop.app.mjs"; + +export default { + props: { + app, + http: "$.interface.http", + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + processResource(resource) { + this.$emit(resource, this.generateMeta(resource)); + }, + }, + async run({ + body, headers, + }) { + const { + app, + processResource, + } = this; + + if (app.getApiKey() !== headers["api-key"]) { + return console.log("Invalid API key"); + } + + processResource(body); + }, +}; diff --git a/components/letterdrop/sources/new-post-published-instant/new-post-published-instant.mjs b/components/letterdrop/sources/new-post-published-instant/new-post-published-instant.mjs new file mode 100644 index 0000000000000..c45fe96d33862 --- /dev/null +++ b/components/letterdrop/sources/new-post-published-instant/new-post-published-instant.mjs @@ -0,0 +1,23 @@ +import common from "../common/webhook.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "letterdrop-new-post-published-instant", + name: "New Post Published (Instant)", + description: "Emit new event when a new post gets published on Letterdrop. **After creating the source, access Letterdrop UI, go to `Settings > Integrations > API and Webhooks > Webhook Endpoints > Setup endpoints` and add your source endpoint URL to the respective webhook endpoint**. [See the documentation](https://docs.letterdrop.com/webhooks)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Post: ${resource.title}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/letterdrop/sources/new-post-published-instant/test-event.mjs b/components/letterdrop/sources/new-post-published-instant/test-event.mjs new file mode 100644 index 0000000000000..c1706cacc5415 --- /dev/null +++ b/components/letterdrop/sources/new-post-published-instant/test-event.mjs @@ -0,0 +1,33 @@ +export default { + "id": "6030dc61a21da91db1d593c0", + "title": "Title", + "subtitle": "Subtitle", + "text": "Lorem ipsum dolor sit amet...", + "textPreview": "Lorem ipsum dolor sit amet", + "body": "

Lorem ipsum dolor sit amet,

", + "slug": "my-new-post", + "url": "https://letterdop.com/blog/my-new-post", + "publishedOn": "2020-10-20T12:18:13.095Z", + "coverImage": "https://cdn.letterdrop.co/images/2022/11/24/hir0hq8n.jpeg", + "thumbnail": "https://cdn.letterdrop.co/images/2022/11/24/hir0hq8n.jpeg", + "metaTitle": "Meta Title", + "metaDescription": "This is a post via Letterdrop", + "status": "published", + "tags": ["chocolate", "food", "restaurant"], + "updated": "2020-10-20T12:18:13.095Z", + "publication": "piedpiper", + "readTime": 3, + "wordCount": 1565, + "featured": false, + "markdown": "Lorem ipsum dolor sit amet", + "customFields": [ + { + "name": "Related Posts", + "type": "Multi-Select List", + "value": "", + "allowedValues": [ + "Post 1", "Post 2", + ], + }, + ], +}; diff --git a/components/letterdrop/sources/watch-added-subscribers-instant/test-event.mjs b/components/letterdrop/sources/watch-added-subscribers-instant/test-event.mjs new file mode 100644 index 0000000000000..b3adf2ae07cfb --- /dev/null +++ b/components/letterdrop/sources/watch-added-subscribers-instant/test-event.mjs @@ -0,0 +1,4 @@ +export default { + "email": "support@letterdrop.co", + "signedUpOn": "2020-10-20T12:07:13.629Z" +}; diff --git a/components/letterdrop/sources/watch-added-subscribers-instant/watch-added-subscribers-instant.mjs b/components/letterdrop/sources/watch-added-subscribers-instant/watch-added-subscribers-instant.mjs new file mode 100644 index 0000000000000..4314d5bc97152 --- /dev/null +++ b/components/letterdrop/sources/watch-added-subscribers-instant/watch-added-subscribers-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/webhook.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "letterdrop-watch-added-subscribers-instant", + name: "Watch Added Subscribers (Instant)", + description: "Emit new event when a new subscriber gets added on Letterdrop. **After creating the source, access Letterdrop UI, go to `Settings > Integrations > API and Webhooks > Webhook Endpoints > Setup endpoints` and add your source endpoint URL to the respective webhook endpoint**. [See the documentation](https://docs.letterdrop.com/webhooks)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + generateMeta(resource) { + const ts = Date.parse(resource.signedUpOn); + return { + id: ts, + summary: `New Subscriber: ${resource.email}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/lettria/README.md b/components/lettria/README.md new file mode 100644 index 0000000000000..ed5a6c5353b1f --- /dev/null +++ b/components/lettria/README.md @@ -0,0 +1,11 @@ +# Overview + +The Lettria API offers advanced natural language processing (NLP) capabilities, enabling developers to analyze and extract meaningful information from text data. With Lettria's API, you can perform tasks like sentiment analysis, entity recognition, and semantic analysis. When integrated into Pipedream, these features unlock the potential for automating text-intensive workflows, making it simple to process feedback, categorize customer inquiries, or extract insights from unstructured data. + +# Example Use Cases + +- **Sentiment Analysis on Customer Feedback**: Trigger your Pipedream workflow with new customer feedback from a platform like Zendesk. Use Lettria's sentiment analysis to gauge the tone of the feedback. Automatically tag the feedback in Zendesk based on the sentiment, and route positive or negative responses to appropriate team members or support tiers. + +- **Content Categorization for CMS**: When a new article is added to your CMS, such as WordPress, trigger a workflow that uses Lettria to analyze and extract key topics. Use these insights to automatically categorize the content in your CMS, optimize it for SEO, or even cross-post it to social media channels with relevant hashtags. + +- **Lead Qualification via Email Analysis**: For incoming emails from potential leads, trigger a workflow on Pipedream to analyze the content with Lettria's entity recognition. Extract company names, job titles, and other key information to enrich lead data. Integrate this data with a CRM like Salesforce to automatically update lead scores and prioritize follow-up based on the extracted information. diff --git a/components/levity/README.md b/components/levity/README.md new file mode 100644 index 0000000000000..cbf90df127c57 --- /dev/null +++ b/components/levity/README.md @@ -0,0 +1,11 @@ +# Overview + +The Levity API provides a platform for creating AI-powered workflows that can classify images, text, and data with ease. Using this API in Pipedream, you can automate decision-making processes, enhance data categorization, and streamline content moderation by leveraging machine learning models. It integrates smoothly within Pipedream's serverless environment, allowing you to build complex workflows without spinning up a single server. + +# Example Use Cases + +- **Automated Support Ticket Categorization**: Build a workflow where incoming support emails are classified by Levity's AI, and then routed to the appropriate department or person in your organization using the Gmail or Zendesk Pipedream app. + +- **Real-time Content Moderation for Social Media**: Create a Pipedream workflow that utilizes Levity's API to moderate user-generated content on social platforms. When new posts or comments are detected by a social media trigger, such as the Twitter Pipedream app, Levity can assess them for inappropriate content, and actions are taken based on the classifications, such as flagging, removing, or escalating the content. + +- **Enhanced CRM Data Enrichment**: Use Levity API within a Pipedream workflow to analyze and enrich contact records in your CRM. When a new contact is added to your CRM, such as Salesforce, trigger a workflow that uses Levity to classify the contact based on the interaction history or uploaded documents, optimizing your sales or marketing strategy. diff --git a/components/lexoffice/README.md b/components/lexoffice/README.md new file mode 100644 index 0000000000000..85be4ac9a733c --- /dev/null +++ b/components/lexoffice/README.md @@ -0,0 +1,11 @@ +# Overview + +The Lexoffice API provides a suite of accounting tools that can automate various financial tasks for businesses using Pipedream. With this API, you can create invoices, manage contacts, track expenses, and handle accounting documents programmatically. Pipedream's serverless platform enables you to integrate Lexoffice with hundreds of other apps, allowing for streamlined workflows that can trigger actions in Lexoffice or respond to events from Lexoffice in real-time. + +# Example Use Cases + +- **Invoice Automation Workflow**: Automatically create and send invoices in Lexoffice when a new order is received in an e-commerce platform like Shopify. Once the order data is received, it triggers a Pipedream workflow that formats the data and uses the Lexoffice API to create and send the invoice to the customer. + +- **Expense Tracking Workflow**: Monitor your company's Slack channel for messages about new expenses, parse the relevant information, and create an expense entry in Lexoffice. This workflow can help keep financial records up-to-date without manual data entry, ensuring that all expenses are tracked and accounted for efficiently. + +- **Customer Sync Workflow**: When a new customer is added to a CRM like HubSpot, use Pipedream to automatically sync their details to Lexoffice. This ensures that your customer records are consistent across your business tools and saves time on data entry by automating the process of updating your accounting system with new contact information. diff --git a/components/libraria/README.md b/components/libraria/README.md new file mode 100644 index 0000000000000..db7e71812aeb5 --- /dev/null +++ b/components/libraria/README.md @@ -0,0 +1,11 @@ +# Overview + +The Libraria API allows you to access a vast library of books and their metadata, including titles, authors, publication dates, and genres. With this API on Pipedream, you can craft serverless workflows to automate various tasks around book data management, enrichment, and sharing. Whether you're a librarian seeking to update catalog records, a retailer syncing inventory, or a book lover curating a reading list, Pipedream streamlines these tasks by connecting the Libraria API to other apps and services. + +# Example Use Cases + +- **Automated Catalog Updates**: With Pipedream, you can create a workflow that triggers weekly, automatically updating your library's catalog with new additions from the Libraria API. This can include enriching existing book records with updated metadata or adding new book entries to your database. + +- **Sync Inventory with Sales Platforms**: Retailers can leverage a Pipedream workflow that syncs book inventory with online sales platforms like Shopify. When the Libraria API lists new books or updates stock levels, Pipedream can push these changes to your Shopify store, ensuring your listings stay current. + +- **Curate Personalized Reading Lists**: Build a workflow on Pipedream that connects the Libraria API to communication apps like Slack. Whenever a new book is added to a genre you're interested in, Pipedream can send a notification with the book details to your preferred Slack channel, keeping you and your community informed about the latest titles. diff --git a/components/lifterlms/README.md b/components/lifterlms/README.md new file mode 100644 index 0000000000000..000942a44bd5d --- /dev/null +++ b/components/lifterlms/README.md @@ -0,0 +1,11 @@ +# Overview + +The LifterLMS API allows you to leverage a powerful learning management system within your Pipedream workflows, enabling automation of course-related tasks, user management, and more. With this API, you can create new courses, enroll students, track progress, and trigger actions based on course events. Integrating LifterLMS with Pipedream can streamline operations, enhance student engagement, and provide personalized learning experiences by connecting to various other services like email platforms, CRMs, or analytics tools. + +# Example Workflows + +- **Automated Student Onboarding**: When a new user signs up for a course via LifterLMS, trigger a Pipedream workflow that sends a personalized welcome email through SendGrid, adds the user’s details to a Google Sheet for tracking, and enrolls the student in an introductory course. + +- **Course Completion Certificates**: Once a student completes a course in LifterLMS, use Pipedream to generate a certificate using a PDF generation service, then email the certificate to the student and update their status in a connected CRM like HubSpot, marking them as a certified user. + +- **Real-time Course Analytics**: Monitor student progress and course interactions in real-time by setting up a Pipedream workflow that captures LifterLMS events, analyzes the data using a service like Google BigQuery, and visualizes these insights using a dashboard tool like Google Data Studio for better decision-making. diff --git a/components/lifx/README.md b/components/lifx/README.md index 6557bbb3eb406..b587ef52dcded 100644 --- a/components/lifx/README.md +++ b/components/lifx/README.md @@ -1,13 +1,11 @@ # Overview -Lifx is a platform that allows developers to build lightbulb-connected -applications. With the Lifx API, you can control the color, brightness, and -power of Lifx-connected lightbulbs. You can also create schedules and routines -to automate your lightbulbs. Some examples of things you can build with the -Lifx API include: - -- A lightbulb that turns blue when it rains -- A lightbulb that turns red when your favorite team scores -- A lightbulb that gradually wakes you up in the morning -- A lightbulb that dims when you start watching a movie -- A lightbulb that turns off when you leave the house +With the Lifx API, you have the power to control your Lifx smart lights programmatically. This API lets you change the color, brightness, and power state of your bulbs, and it also allows you to access information on the lights' settings and environment. Pipedream's serverless platform leverages this functionality to create automations that can be triggered by various events. You can set your lights to respond to external factors like the weather, implement them into your smart home routines, or sync them with your productivity apps for visual notifications. + +# Example Use Cases + +- **Smart Weather Alerts**: Combine Lifx with a weather API on Pipedream to change the color of your lights based on the current weather. For instance, if rain is forecasted, your lights could glow blue to remind you to bring an umbrella when you leave the house. + +- **Meeting Reminder System**: Integrate Lifx with Google Calendar using Pipedream. Set your office lights to blink or change to a specific color when you have an upcoming meeting. This can serve as a visual cue that it's time to wrap up what you're doing and prep for the meeting. + +- **Home Security Alerts**: Use motion detection from smart home security cameras or systems, and have Pipedream trigger your Lifx bulbs to flash red when unexpected movement is detected at home. This could potentially scare off intruders and alert you to check your security feeds. diff --git a/components/lifx/package.json b/components/lifx/package.json new file mode 100644 index 0000000000000..ff3e824397b30 --- /dev/null +++ b/components/lifx/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/lifx", + "version": "0.6.0", + "description": "Pipedream lifx Components", + "main": "lifx.app.mjs", + "keywords": [ + "pipedream", + "lifx" + ], + "homepage": "https://pipedream.com/apps/lifx", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/lighthouse/README.md b/components/lighthouse/README.md index 918230d6938b5..ce34e5e9b8ef1 100644 --- a/components/lighthouse/README.md +++ b/components/lighthouse/README.md @@ -1,13 +1,11 @@ # Overview -Lighthouse is a great tool for building web applications. With its easy-to-use -API, you can quickly and easily create web applications that are both powerful -and user friendly. +The Lighthouse API provides a window into the world of SSL/TLS certificates. It lets you monitor and track certificates issued for specific domains, offering invaluable insights for security and compliance. By leveraging this API within Pipedream, you can automate certificate tracking, set up alerts for new certificates, and integrate this data with other services for a comprehensive view of your domain's security posture. -Some examples of what you can build using the Lighthouse API include: +# Example Use Cases -- A web application that allows users to search for and book travel -- An online marketplace for buying and selling goods and services -- A social networking site for connecting with friends and family -- A news and information website -- A blog or personal website +- **SSL Certificate Change Notifications:** Build a workflow that triggers on a schedule to check for new SSL/TLS certificates issued for your domain using Lighthouse API. If new certificates are detected, automatically send a notification to your team via Slack or email to keep everyone informed of important changes or potential security issues. + +- **Compliance Automation:** Create a workflow that uses the Lighthouse API to obtain a list of all certificates for your domain. Connect this to a Google Sheets or Airtable base to log and maintain a history. Use this data to automatically generate compliance reports, ensuring you have a trail of certificate issuance and expiration for audits. + +- **Domain Security Dashboard Integration:** Integrate Lighthouse API with a dashboard tool like Grafana or Google Data Studio. Set up a Pipedream workflow that periodically fetches certificate data for your domains and pushes it to your dashboard, providing a real-time visual representation of your SSL/TLS certificate status for monitoring and quick decision-making. diff --git a/components/lightpanda/lightpanda.app.mjs b/components/lightpanda/lightpanda.app.mjs new file mode 100644 index 0000000000000..95507b0ea6da8 --- /dev/null +++ b/components/lightpanda/lightpanda.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "lightpanda", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/lightpanda/package.json b/components/lightpanda/package.json new file mode 100644 index 0000000000000..f5830081b9272 --- /dev/null +++ b/components/lightpanda/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/lightpanda", + "version": "0.0.1", + "description": "Pipedream Lightpanda Components", + "main": "lightpanda.app.mjs", + "keywords": [ + "pipedream", + "lightpanda" + ], + "homepage": "https://pipedream.com/apps/lightpanda", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/lightspeed_retail_pos/README.md b/components/lightspeed_retail_pos/README.md new file mode 100644 index 0000000000000..a3722ee1cd6da --- /dev/null +++ b/components/lightspeed_retail_pos/README.md @@ -0,0 +1,11 @@ +# Overview + +The Lightspeed Retail POS API provides a rich interface to interact with a retail business's sales, inventory, customer information, and more. Leveraging this API on Pipedream enables you to automate workflows, synchronize data across various platforms, and respond to events in real-time. You can build sophisticated serverless workflows that react to Lightspeed Retail POS events, orchestrate data flows, and connect with countless other services available on Pipedream. By setting up webhooks and using Pipedream's integrated services, you can create custom automations without handling the complexity of infrastructure. + +# Example Use Cases + +- **Sync New Sales to Google Sheets**: Automate the transfer of new sale records from Lightspeed Retail POS to a Google Sheets spreadsheet. Each time a sale is completed in Lightspeed, a Pipedream workflow triggers and appends the sale data to a specified Google Sheets document. This keeps your records up-to-date and accessible. + +- **Email Customers Post-Purchase**: Follow up on recent purchases by sending personalized thank you emails. When a sale is made in Lightspeed Retail POS, use Pipedream to trigger an email through a service like SendGrid or Mailgun to the customer, perhaps including a discount on their next purchase or asking for feedback. + +- **Slack Notification for Low Inventory**: Set up an alert system where your team gets notified on Slack when inventory levels for key products drop below a certain threshold. Pipedream can monitor Lightspeed inventory updates and post a message to a designated Slack channel, ensuring restocking happens promptly. diff --git a/components/lightspeed_vt/README.md b/components/lightspeed_vt/README.md new file mode 100644 index 0000000000000..50d7e986d483f --- /dev/null +++ b/components/lightspeed_vt/README.md @@ -0,0 +1,11 @@ +# Overview + +LightSpeed VT is a learning management system (LMS) that provides a platform for delivering interactive training and e-learning experiences. Through the LightSpeed VT API, you can automate access to course catalogs, manage users, track progress and performance, and integrate this data with other systems. On Pipedream, you can leverage this API to create workflows that respond to events in real-time, synchronize training data with other platforms, and enhance the user learning experience with personalized automations. + +# Example Use Cases + +- **Automated User Enrollment**: Automatically enroll new employees into relevant training courses in LightSpeed VT when they are added to your HR system. For example, when a new hire record is created in BambooHR, a Pipedream workflow can be triggered to add that user to specific courses in LightSpeed VT. + +- **Course Completion Tracking**: Monitor and respond to course completions by sending personalized emails or messages. When a user completes a course in LightSpeed VT, trigger a Pipedream workflow that congratulates them via Gmail and logs this achievement in a Google Sheet for record-keeping. + +- **Real-time Performance Alerts**: Create alerts for management when employees score below a certain threshold on a test or assessment. With Pipedream, you can set up a workflow that listens for low test scores in LightSpeed VT and sends a Slack notification to a designated manager to follow up. diff --git a/components/lime_go/README.md b/components/lime_go/README.md index 34097f3fc2692..9a7f839d66bb9 100644 --- a/components/lime_go/README.md +++ b/components/lime_go/README.md @@ -1,10 +1,11 @@ # Overview -Lime Go API can be used to build a variety of applications, including: - -- Web applications -- Mobile applications -- Desktop applications -- Games -- Utilities -- Libraries +LIME Go is a smart CRM tailored for B2B sales teams, designed to streamline customer relationship management with features like lead generation, deal tracking, and proactive suggestions for best next steps. Using the LIME Go API with Pipedream allows you to automate interactions with your CRM data, sync with other business tools, and tailor your sales process with real-time data flows. Create workflows that trigger actions based on CRM events, enrich contact data, or synchronize sales activities across platforms, all with minimal code. + +# Example Use Cases + +- **Lead Score Update Notification**: When a lead's score changes in LIME Go, Pipedream can catch this event and trigger an email or Slack message to the responsible salesperson. This instant notification helps your team act quickly on hot leads. + +- **Cross-Platform Contact Sync**: Whenever a new contact is added to LIME Go, use Pipedream to automatically add that contact to other apps like Mailchimp, ensuring your marketing campaigns are always targeting the latest prospects. + +- **Deal Progression Tracking**: Set up a Pipedream workflow to monitor deal stages in LIME Go. When a deal moves to a new stage, the workflow can update a Google Sheets spreadsheet, create tasks in project management tools like Trello or Asana, or send a celebratory message to your company's general chat in Microsoft Teams. diff --git a/components/lime_go/package.json b/components/lime_go/package.json new file mode 100644 index 0000000000000..d5cb817d7da2e --- /dev/null +++ b/components/lime_go/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/lime_go", + "version": "0.6.0", + "description": "Pipedream lime_go Components", + "main": "lime_go.app.mjs", + "keywords": [ + "pipedream", + "lime_go" + ], + "homepage": "https://pipedream.com/apps/lime_go", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/line/README.md b/components/line/README.md index 5bdb43c864c3d..2862f3be08d4d 100644 --- a/components/line/README.md +++ b/components/line/README.md @@ -1,12 +1,11 @@ # Overview -The Line API allows developers to create all sorts of applications that -interact with Line's messaging platform. Here are some examples of what you can -build: - -- A bot that responds to messages from users -- A bot that helps users buy tickets or make reservations -- A bot that sends notifications or updates to users -- A bot that helps users track their orders or shipments -- A bot that provides customer support -- A bot that helps users find restaurants or other businesses +The Line API facilitates interaction with Line's messaging platform, enabling developers to send messages, manage user data, and leverage Line's rich social features within their applications. By using Pipedream, an integration platform, you can automate interactions with the Line API, connect to countless other services, and orchestrate complex workflows without managing servers. + +# Example Use Cases + +- **Customer Support Notifications**: Integrate Line with a CRM platform like HubSpot on Pipedream to automatically send personalized Line messages to customers when their support ticket status changes, ensuring they're informed and engaged. + +- **Post Purchase Follow-Up**: After a user completes a purchase on an e-commerce platform like Shopify, trigger a Pipedream workflow to send a thank you message or a discount code for future purchases through Line to enhance customer loyalty. + +- **Event Reminder Service**: Connect Line to a calendar service like Google Calendar using Pipedream workflows. Automatically remind attendees about an upcoming event by sending them a Line message a day before the event occurs. diff --git a/components/linear/README.md b/components/linear/README.md index 5fbb1c4467e0d..d6ca69589fb12 100644 --- a/components/linear/README.md +++ b/components/linear/README.md @@ -1,7 +1,11 @@ # Overview -With the Linear API, you can build a variety of applications that allow users -to track their work and progress over time. For example, you could build a task -manager that allows users to track their to-do lists and progress on each task, -or a project manager that allows users to track their progress on different -projects. +Linear (OAuth) API provides access to Linear's issue tracking and project management capabilities, letting you automate tasks, synchronize data across platforms, and enhance your team's productivity. Whether you're reporting bugs, assigning tasks, or tracking progress, the Linear API on Pipedream allows for real-time, event-driven workflows that can increase efficiency and foster collaboration within teams. + +# Example Use Cases + +- **Issue Sync with GitHub**: Whenever a new issue is created in Linear, automatically create a corresponding GitHub issue. This keeps your project management and code repositories aligned, ensuring no task goes unnoticed by developers who primarily interact with GitHub. + +- **Slack Notifications for Priority Tasks**: Set up a workflow where high-priority issues in Linear trigger instant messages to a designated Slack channel or user. This ensures critical items are immediately visible and can be acted upon swiftly by your team. + +- **Customer Support Ticketing Integration**: Connect Linear with a customer support platform like Zendesk. When a support ticket escalates to a point where development work is needed, automatically create a new Linear issue to track the work, making sure customer problems are addressed in your development workflow. diff --git a/components/linear/actions/create-issue/create-issue.mjs b/components/linear/actions/create-issue/create-issue.mjs index 5666160b4ffdc..bec238de468bf 100644 --- a/components/linear/actions/create-issue/create-issue.mjs +++ b/components/linear/actions/create-issue/create-issue.mjs @@ -1,4 +1,4 @@ -import createIssue from "../../../linear_app/actions/create-issue/create-issue.mjs"; +import createIssue from "@pipedream/linear_app/actions/create-issue/create-issue.mjs"; import utils from "../../common/utils.mjs"; /* eslint-disable pipedream/required-properties-type */ @@ -10,6 +10,6 @@ export default { ...utils.getAppProps(createIssue), key: "linear-create-issue", description: "Create an issue (OAuth). See the docs [here](https://developers.linear.app/docs/graphql/working-with-the-graphql-api#creating-and-editing-issues)", - version: "0.4.5", + version: "0.4.8", }; diff --git a/components/linear/actions/get-issue/get-issue.mjs b/components/linear/actions/get-issue/get-issue.mjs index 70c170a6c6ccd..72f5a6d0ef1b8 100644 --- a/components/linear/actions/get-issue/get-issue.mjs +++ b/components/linear/actions/get-issue/get-issue.mjs @@ -1,4 +1,4 @@ -import getIssue from "../../../linear_app/actions/get-issue/get-issue.mjs"; +import getIssue from "@pipedream/linear_app/actions/get-issue/get-issue.mjs"; import utils from "../../common/utils.mjs"; /* eslint-disable pipedream/required-properties-type */ @@ -10,6 +10,6 @@ export default { ...utils.getAppProps(getIssue), key: "linear-get-issue", description: "Get an issue by ID (OAuth). See the docs [here](https://developers.linear.app/docs/graphql/working-with-the-graphql-api)", - version: "0.1.5", + version: "0.1.8", }; diff --git a/components/linear/actions/get-teams/get-teams.mjs b/components/linear/actions/get-teams/get-teams.mjs index 757152ee5aa2c..e65f61fe61a4c 100644 --- a/components/linear/actions/get-teams/get-teams.mjs +++ b/components/linear/actions/get-teams/get-teams.mjs @@ -1,5 +1,5 @@ import linearApp from "../../linear.app.mjs"; -import getTeams from "../../../linear_app/actions/get-teams/get-teams.mjs"; +import getTeams from "@pipedream/linear_app/actions/get-teams/get-teams.mjs"; /* eslint-disable pipedream/required-properties-type */ /* eslint-disable pipedream/required-properties-name */ @@ -9,7 +9,7 @@ export default { ...getTeams, key: "linear-get-teams", description: "Get all the teams (OAuth). See the docs [here](https://developers.linear.app/docs/graphql/working-with-the-graphql-api#creating-and-editing-issues)", - version: "0.2.5", + version: "0.2.8", props: { linearApp, }, diff --git a/components/linear/actions/search-issues/search-issues.mjs b/components/linear/actions/search-issues/search-issues.mjs index ff8225b762998..93425cb708ec8 100644 --- a/components/linear/actions/search-issues/search-issues.mjs +++ b/components/linear/actions/search-issues/search-issues.mjs @@ -1,4 +1,4 @@ -import searchIssues from "../../../linear_app/actions/search-issues/search-issues.mjs"; +import searchIssues from "@pipedream/linear_app/actions/search-issues/search-issues.mjs"; import utils from "../../common/utils.mjs"; /* eslint-disable pipedream/required-properties-type */ @@ -10,6 +10,6 @@ export default { ...utils.getAppProps(searchIssues), key: "linear-search-issues", description: "Search issues (OAuth). See the docs [here](https://developers.linear.app/docs/graphql/working-with-the-graphql-api)", - version: "0.2.5", + version: "0.2.8", }; diff --git a/components/linear/actions/update-issue/update-issue.mjs b/components/linear/actions/update-issue/update-issue.mjs index a52e3b8a3362c..9efe19561ca80 100644 --- a/components/linear/actions/update-issue/update-issue.mjs +++ b/components/linear/actions/update-issue/update-issue.mjs @@ -1,4 +1,4 @@ -import updateIssue from "../../../linear_app/actions/update-issue/update-issue.mjs"; +import updateIssue from "@pipedream/linear_app/actions/update-issue/update-issue.mjs"; import utils from "../../common/utils.mjs"; /* eslint-disable pipedream/required-properties-type */ @@ -9,6 +9,6 @@ export default { ...utils.getAppProps(updateIssue), key: "linear-update-issue", description: "Update an issue (OAuth). See the docs [here](https://developers.linear.app/docs/graphql/working-with-the-graphql-api#creating-and-editing-issues)", - version: "0.1.5", + version: "0.1.8", }; diff --git a/components/linear/linear.app.mjs b/components/linear/linear.app.mjs index 3ea7772d2f6a1..c706602638eef 100644 --- a/components/linear/linear.app.mjs +++ b/components/linear/linear.app.mjs @@ -1,4 +1,4 @@ -import linearApp from "../linear_app/linear_app.app.mjs"; +import linearApp from "@pipedream/linear_app"; export default { ...linearApp, diff --git a/components/linear/package.json b/components/linear/package.json index 03b803ace5f26..46ca6b2ca5a99 100644 --- a/components/linear/package.json +++ b/components/linear/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/linear", - "version": "0.5.5", + "version": "0.7.0", "description": "Pipedream Linear Components", "main": "linear.app.mjs", "keywords": [ @@ -15,6 +15,7 @@ }, "dependencies": { "@linear/sdk": "^13.0.0", - "@pipedream/platform": "^1.3.0" + "@pipedream/linear_app": "^0.7.0", + "@pipedream/platform": "^3.0.3" } } diff --git a/components/linear/sources/comment-created-instant/comment-created-instant.mjs b/components/linear/sources/comment-created-instant/comment-created-instant.mjs index 801598f2ce034..c75eac4358286 100644 --- a/components/linear/sources/comment-created-instant/comment-created-instant.mjs +++ b/components/linear/sources/comment-created-instant/comment-created-instant.mjs @@ -1,4 +1,4 @@ -import commentCreatedInstant from "../../../linear_app/sources/comment-created-instant/comment-created-instant.mjs"; +import commentCreatedInstant from "@pipedream/linear_app/sources/comment-created-instant/comment-created-instant.mjs"; import utils from "../../common/utils.mjs"; /* eslint-disable pipedream/required-properties-type */ @@ -9,6 +9,6 @@ export default { ...commentCreatedInstant, ...utils.getAppProps(commentCreatedInstant), key: "linear-comment-created-instant", - description: "Emit new event when a new comment is created (OAuth). See the docs [here](https://developers.linear.app/docs/graphql/webhooks)", - version: "0.1.5", + description: "Emit new event when a new comment is created (OAuth). [See the documentation](https://developers.linear.app/docs/graphql/webhooks)", + version: "0.1.10", }; diff --git a/components/linear/sources/issue-created-instant/issue-created-instant.mjs b/components/linear/sources/issue-created-instant/issue-created-instant.mjs index fa125ff7f88c5..aeeb2a41cedba 100644 --- a/components/linear/sources/issue-created-instant/issue-created-instant.mjs +++ b/components/linear/sources/issue-created-instant/issue-created-instant.mjs @@ -1,4 +1,4 @@ -import issueCreatedInstant from "../../../linear_app/sources/issue-created-instant/issue-created-instant.mjs"; +import issueCreatedInstant from "@pipedream/linear_app/sources/issue-created-instant/issue-created-instant.mjs"; import utils from "../../common/utils.mjs"; /* eslint-disable pipedream/required-properties-type */ @@ -9,6 +9,6 @@ export default { ...issueCreatedInstant, ...utils.getAppProps(issueCreatedInstant), key: "linear-issue-created-instant", - description: "Emit new event when a new issue is created (OAuth). See the docs [here](https://developers.linear.app/docs/graphql/webhooks)", - version: "0.3.5", + description: "Emit new event when a new issue is created (OAuth). [See the documentation](https://developers.linear.app/docs/graphql/webhooks)", + version: "0.3.10", }; diff --git a/components/linear/sources/issue-updated-instant/issue-updated-instant.mjs b/components/linear/sources/issue-updated-instant/issue-updated-instant.mjs index 8b23b84c84518..a3425729790f0 100644 --- a/components/linear/sources/issue-updated-instant/issue-updated-instant.mjs +++ b/components/linear/sources/issue-updated-instant/issue-updated-instant.mjs @@ -1,4 +1,4 @@ -import issueUpdatedInstant from "../../../linear_app/sources/issue-updated-instant/issue-updated-instant.mjs"; +import issueUpdatedInstant from "@pipedream/linear_app/sources/issue-updated-instant/issue-updated-instant.mjs"; import utils from "../../common/utils.mjs"; /* eslint-disable pipedream/required-properties-type */ @@ -9,6 +9,6 @@ export default { ...issueUpdatedInstant, ...utils.getAppProps(issueUpdatedInstant), key: "linear-issue-updated-instant", - description: "Emit new event when an issue is updated (OAuth). See the docs [here](https://developers.linear.app/docs/graphql/webhooks)", - version: "0.3.5", + description: "Emit new event when an issue is updated (OAuth). See the documentation](https://developers.linear.app/docs/graphql/webhooks)", + version: "0.3.10", }; diff --git a/components/linear/sources/new-issue-status-updated/new-issue-status-updated.mjs b/components/linear/sources/new-issue-status-updated/new-issue-status-updated.mjs index 7b365bd6a4551..62fc359642764 100644 --- a/components/linear/sources/new-issue-status-updated/new-issue-status-updated.mjs +++ b/components/linear/sources/new-issue-status-updated/new-issue-status-updated.mjs @@ -1,4 +1,4 @@ -import newIssueStatusUpdated from "../../../linear_app/sources/new-issue-status-updated/new-issue-status-updated.mjs"; +import newIssueStatusUpdated from "@pipedream/linear_app/sources/new-issue-status-updated/new-issue-status-updated.mjs"; import utils from "../../common/utils.mjs"; /* eslint-disable pipedream/required-properties-type */ @@ -9,6 +9,6 @@ export default { ...newIssueStatusUpdated, ...utils.getAppProps(newIssueStatusUpdated), key: "linear-new-issue-status-updated", - description: "Emit new event when the status of an issue is updated (OAuth). See the docs [here](https://developers.linear.app/docs/graphql/webhooks)", - version: "0.1.5", + description: "Emit new event when the status of an issue is updated (OAuth). [See the documentation](https://developers.linear.app/docs/graphql/webhooks)", + version: "0.1.10", }; diff --git a/components/linear/sources/new-projectupdate-created/new-projectupdate-created.mjs b/components/linear/sources/new-projectupdate-created/new-projectupdate-created.mjs new file mode 100644 index 0000000000000..afa6edda0ee02 --- /dev/null +++ b/components/linear/sources/new-projectupdate-created/new-projectupdate-created.mjs @@ -0,0 +1,15 @@ +import newProjectUpdateCreated from "@pipedream/linear_app/sources/new-projectupdate-created/new-projectupdate-created.mjs"; + +import utils from "../../common/utils.mjs"; + +/* eslint-disable pipedream/required-properties-type */ +/* eslint-disable pipedream/required-properties-name */ +/* eslint-disable pipedream/required-properties-version */ + +export default { + ...newProjectUpdateCreated, + ...utils.getAppProps(newProjectUpdateCreated), + key: "linear-new-projectupdate-created", + description: "Project updates are short status reports on the health of your projects. Emit new event when a new Project Update is written. [See the documentation](https://developers.linear.app/docs/graphql/webhooks)", + version: "0.0.1", +}; diff --git a/components/linear/sources/project-updated-instant/project-updated-instant.mjs b/components/linear/sources/project-updated-instant/project-updated-instant.mjs new file mode 100644 index 0000000000000..e22b921744506 --- /dev/null +++ b/components/linear/sources/project-updated-instant/project-updated-instant.mjs @@ -0,0 +1,15 @@ +import projectUpdatedInstant from "@pipedream/linear_app/sources/project-updated-instant/project-updated-instant.mjs"; + +import utils from "../../common/utils.mjs"; + +/* eslint-disable pipedream/required-properties-type */ +/* eslint-disable pipedream/required-properties-name */ +/* eslint-disable pipedream/required-properties-version */ + +export default { + ...projectUpdatedInstant, + ...utils.getAppProps(projectUpdatedInstant), + key: "linear-project-updated-instant", + description: "Emit new event when a project is updated (OAuth). [See the documentation](https://developers.linear.app/docs/graphql/webhooks)", + version: "0.0.2", +}; diff --git a/components/linear_app/README.md b/components/linear_app/README.md index 59d1c73bb6d30..200614a654026 100644 --- a/components/linear_app/README.md +++ b/components/linear_app/README.md @@ -1,8 +1,11 @@ # Overview -With the Linear API, you can manage your workflow, track issues, and automate -your development process. Here are some examples of what you can build: +Linear helps streamline software project management, bug tracking, and task coordination. By using the Linear (API key) API with Pipedream, you can automate routine tasks, sync issues across platforms, and trigger custom workflows. Think auto-assignment of tasks based on specific triggers or pushing updates to a Slack channel when an issue's status changes. These automations save time and ensure that your development team stays focused on coding rather than on administrative overhead. -- A custom workflow management tool -- A tool to track issues and feature requests -- A development process automation tool +# Example Use Cases + +- **Sync Issues with Google Sheets**: Use Pipedream to monitor new issues in Linear and automatically add them to a Google Sheets spreadsheet. This can help with reporting, auditing, and providing a high-level overview of tasks without needing to access Linear directly. + +- **Automate Task Assignment Based on Labels**: When a new issue is created in Linear with a specific label (e.g., "urgent"), you can set up a Pipedream workflow to automatically assign it to a designated team member or escalate it by creating a high-priority notification in your team's messaging app, like Slack or Microsoft Teams. + +- **Create GitHub Issues from Linear Tasks**: For development teams using both Linear and GitHub, a workflow can be set up to create a new GitHub issue whenever a Linear task reaches a certain stage or is tagged for development. This ensures that your code repository is always in sync with your project management tool. diff --git a/components/linear_app/actions/create-issue/create-issue.mjs b/components/linear_app/actions/create-issue/create-issue.mjs index e61838831b610..dce521454c01c 100644 --- a/components/linear_app/actions/create-issue/create-issue.mjs +++ b/components/linear_app/actions/create-issue/create-issue.mjs @@ -5,7 +5,7 @@ export default { key: "linear_app-create-issue", name: "Create Issue", description: "Create an issue (API Key). See the docs [here](https://developers.linear.app/docs/graphql/working-with-the-graphql-api#creating-and-editing-issues)", - version: "0.4.5", + version: "0.4.7", props: { linearApp, teamId: { diff --git a/components/linear_app/actions/get-issue/get-issue.mjs b/components/linear_app/actions/get-issue/get-issue.mjs index 84bf48f70564b..0dd7e2b3de2b0 100644 --- a/components/linear_app/actions/get-issue/get-issue.mjs +++ b/components/linear_app/actions/get-issue/get-issue.mjs @@ -4,7 +4,7 @@ export default { key: "linear_app-get-issue", name: "Get Issue", description: "Get an issue by ID (API Key). See the docs [here](https://developers.linear.app/docs/graphql/working-with-the-graphql-api)", - version: "0.1.5", + version: "0.1.7", type: "action", props: { linearApp, diff --git a/components/linear_app/actions/get-teams/get-teams.mjs b/components/linear_app/actions/get-teams/get-teams.mjs index e4dd477064526..f1bbf6efbfd47 100644 --- a/components/linear_app/actions/get-teams/get-teams.mjs +++ b/components/linear_app/actions/get-teams/get-teams.mjs @@ -4,7 +4,7 @@ export default { key: "linear_app-get-teams", name: "Get Teams", description: "Get all the teams (API Key). See the docs [here](https://developers.linear.app/docs/graphql/working-with-the-graphql-api)", - version: "0.2.5", + version: "0.2.7", type: "action", props: { linearApp, diff --git a/components/linear_app/actions/search-issues/search-issues.mjs b/components/linear_app/actions/search-issues/search-issues.mjs index c4b2136ac2493..b0227f121ca96 100644 --- a/components/linear_app/actions/search-issues/search-issues.mjs +++ b/components/linear_app/actions/search-issues/search-issues.mjs @@ -6,7 +6,7 @@ export default { name: "Search Issues", description: "Search issues (API Key). See the docs [here](https://developers.linear.app/docs/graphql/working-with-the-graphql-api)", type: "action", - version: "0.2.5", + version: "0.2.7", props: { linearApp, query: { diff --git a/components/linear_app/actions/update-issue/update-issue.mjs b/components/linear_app/actions/update-issue/update-issue.mjs index a9f9986f4964b..1b4d1e6a517ad 100644 --- a/components/linear_app/actions/update-issue/update-issue.mjs +++ b/components/linear_app/actions/update-issue/update-issue.mjs @@ -5,7 +5,7 @@ export default { name: "Update Issue", description: "Update an issue (API Key). See the docs [here](https://developers.linear.app/docs/graphql/working-with-the-graphql-api#creating-and-editing-issues)", type: "action", - version: "0.1.5", + version: "0.1.7", props: { linearApp, teamId: { diff --git a/components/linear_app/common/constants.mjs b/components/linear_app/common/constants.mjs index f7a555c872fde..30c487543a70a 100644 --- a/components/linear_app/common/constants.mjs +++ b/components/linear_app/common/constants.mjs @@ -13,6 +13,7 @@ const RESOURCE_TYPE = { ISSUE: "Issue", ISSUE_LABEL: "IssueLabel", PROJECT: "Project", + PROJECT_UPDATE: "ProjectUpdate", CYCLE: "Cycle", REACTION: "Reaction", }; diff --git a/components/linear_app/common/fragments.mjs b/components/linear_app/common/fragments.mjs index 29e53665fe39b..a823d0fc97f70 100644 --- a/components/linear_app/common/fragments.mjs +++ b/components/linear_app/common/fragments.mjs @@ -101,4 +101,68 @@ export default { } } `, + project: ` + fragment Project on Project { + id + name + creator { + id + } + lead { + id + } + status { + id + } + color + completedIssueCountHistory + completedScopeHistory + createdAt + description + inProgressScopeHistory + issueCountHistory + name + priority + progress + scope + scopeHistory + slackIssueComments + slackIssueStatuses + slackNewIssue + slugId + sortOrder + state + updatedAt + url + } + `, + projectUpdate: ` + fragment ProjectUpdate on ProjectUpdate { + id + body + health + project { + id + name + lead { + id + name + } + initiatives { + nodes { + name + } + } + } + user { + id + } + createdAt + updatedAt + bodyData + slugId + infoSnapshot + url + } + `, }; diff --git a/components/linear_app/common/queries.mjs b/components/linear_app/common/queries.mjs index fc2070ab1b1f1..086484d1a891c 100644 --- a/components/linear_app/common/queries.mjs +++ b/components/linear_app/common/queries.mjs @@ -48,13 +48,59 @@ export default { ${fragments.comment} `, listProjects: ` - query ListProjects { - projects { + query ListProjects( + $filter: ProjectFilter, + $before: String, + $after: String, + $first: Int, + $last: Int, + $orderBy: PaginationOrderBy + ) { + projects( + filter: $filter, + before: $before, + after: $after, + first: $first, + last: $last, + orderBy: $orderBy + ) { + pageInfo { + ...PageInfo + } nodes { - id - name + ...Project } } } + ${fragments.project} + ${fragments.pageInfo} + `, + listProjectUpdates: ` + query ListProjectUpdates( + $filter: ProjectUpdateFilter, + $before: String, + $after: String, + $first: Int, + $last: Int, + $orderBy: PaginationOrderBy + ) { + projectUpdates( + filter: $filter, + before: $before, + after: $after, + first: $first, + last: $last, + orderBy: $orderBy + ) { + pageInfo { + ...PageInfo + } + nodes { + ...ProjectUpdate + } + } + } + ${fragments.projectUpdate} + ${fragments.pageInfo} `, }; diff --git a/components/linear_app/common/utils.mjs b/components/linear_app/common/utils.mjs index 3c5d57853155d..c5d9c214c8463 100644 --- a/components/linear_app/common/utils.mjs +++ b/components/linear_app/common/utils.mjs @@ -42,7 +42,10 @@ function buildVariables(endCursor, args) { const issueLabels = args.filter.issueLabels ? `, labels: { name: { in: ${JSON.stringify(args.filter.issueLabels)} } }` : ""; - let filter = `${title}${teamId}${projectId}${team}${project}${state}${assigneeId}${issueLabels}`; + const createdAt = args.filter.createdAt + ? `, createdAt: { gte: "${args.filter.createdAt.gte}" }` + : ""; + let filter = `${title}${teamId}${projectId}${team}${project}${state}${assigneeId}${issueLabels}${createdAt}`; if (filter[0] === ",") { filter = filter.substring(2, filter.length); } diff --git a/components/linear_app/linear_app.app.mjs b/components/linear_app/linear_app.app.mjs index 2b446a0f0070f..c6c0986e06f51 100644 --- a/components/linear_app/linear_app.app.mjs +++ b/components/linear_app/linear_app.app.mjs @@ -248,6 +248,9 @@ export default { async getProject(id) { return this.client().project(id); }, + async getProjectUpdate(id) { + return this.client().projectUpdate(id); + }, async getState(id) { return this.client().workflowState(id); }, @@ -266,6 +269,15 @@ export default { }); return projects; }, + async listProjectUpdates(variables) { + const { data: { projectUpdates } } = await this.post({ + data: { + query: queries.listProjectUpdates, + variables, + }, + }); + return projectUpdates; + }, async listUsers(variables = {}) { return this.client().users(variables); }, diff --git a/components/linear_app/package.json b/components/linear_app/package.json index cd607b4a3f0bd..48a839d2c50b9 100644 --- a/components/linear_app/package.json +++ b/components/linear_app/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/linear_app", - "version": "0.5.5", + "version": "0.7.0", "description": "Pipedream Linear_app Components", "main": "linear_app.app.mjs", "keywords": [ @@ -15,6 +15,6 @@ }, "dependencies": { "@linear/sdk": "^13.0.0", - "@pipedream/platform": "^1.3.0" + "@pipedream/platform": "^3.0.3" } } diff --git a/components/linear_app/sources/comment-created-instant/comment-created-instant.mjs b/components/linear_app/sources/comment-created-instant/comment-created-instant.mjs index 7f2fb33dd74c3..5d0af9f2e99c3 100644 --- a/components/linear_app/sources/comment-created-instant/comment-created-instant.mjs +++ b/components/linear_app/sources/comment-created-instant/comment-created-instant.mjs @@ -5,9 +5,9 @@ export default { ...common, key: "linear_app-comment-created-instant", name: "New Created Comment (Instant)", - description: "Emit new event when a new comment is created. See the docs [here](https://developers.linear.app/docs/graphql/webhooks)", + description: "Emit new event when a new comment is created. [See the documentation](https://developers.linear.app/docs/graphql/webhooks)", type: "source", - version: "0.1.5", + version: "0.1.9", dedupe: "unique", methods: { ...common.methods, @@ -50,6 +50,11 @@ export default { }, }; }, + getResource(comment) { + return this.linearApp.getComment({ + commentId: comment.id, + }); + }, getMetadata(resource) { const { delivery, diff --git a/components/linear_app/sources/common/webhook.mjs b/components/linear_app/sources/common/webhook.mjs index 8f9e6da8fab82..0b7fb23767c77 100644 --- a/components/linear_app/sources/common/webhook.mjs +++ b/components/linear_app/sources/common/webhook.mjs @@ -1,6 +1,7 @@ import linearApp from "../../linear_app.app.mjs"; import constants from "../../common/constants.mjs"; import utils from "../../common/utils.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; export default { props: { @@ -12,6 +13,7 @@ export default { linearApp, "teamId", ], + reloadProps: true, }, projectId: { propDefinition: [ @@ -19,9 +21,30 @@ export default { "projectId", ], }, - http: "$.interface.http", db: "$.service.db", }, + async additionalProps() { + const props = {}; + let msg; + if (await this.isAdmin()) { + msg = "Admin role detected. Trigger will be set up as a webhook."; + props.http = "$.interface.http"; + } else { + msg = "No admin role detected. Trigger will set up to use polling."; + props.timer = { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }; + } + props.alert = { + type: "alert", + alertType: "info", + content: `${msg} See the Linear [documentation](https://linear.app/docs/api-and-webhooks#webhooks) for details.`, + }; + return props; + }, methods: { setWebhookId(teamId, id) { this.db.set(`webhook-${teamId}`, id); @@ -38,9 +61,15 @@ export default { isRelevant() { return true; }, + isRelevantPolling() { + return true; + }, useGraphQl() { return true; }, + getResource() { + throw new Error("getResource is not implemented"); + }, getResourceTypes() { throw new Error("getResourceTypes is not implemented"); }, @@ -59,11 +88,20 @@ export default { getLoadedProjectId() { throw new Error("Get loaded project ID not implemented"); }, - }, - hooks: { - async deploy() { - // Retrieve historical events - console.log("Retrieving historical events..."); + async isAdmin() { + const { data } = await this.linearApp.makeAxiosRequest({ + method: "POST", + data: { + "query": `{ + user(id: "me") { + admin + } + }`, + }, + }); + return data?.user?.admin; + }, + async emitPolledResources() { const stream = this.linearApp.paginateResources({ resourcesFn: this.getResourcesFn(), resourcesFnArgs: this.getResourcesFnArgs(), @@ -73,77 +111,94 @@ export default { resources .reverse() + .filter((resource) => this.isRelevantPolling(resource)) .forEach((resource) => { this.$emit(resource, this.getMetadata(resource)); }); }, + }, + hooks: { + async deploy() { + // Retrieve historical events + console.log("Retrieving historical events..."); + await this.emitPolledResources(); + }, async activate() { - const args = { - resourceTypes: this.getResourceTypes(), - url: this.http.endpoint, - label: this.getWebhookLabel(), - }; - if (!this.teamIds && !this.teamId) { - args.allPublicTeams = true; - const { _webhook: webhook } = await this.linearApp.createWebhook(args); - this.setWebhookId("1", webhook.id); - return; - } - const teamIds = this.teamIds || [ - this.teamId, - ]; - for (const teamId of teamIds) { - const { _webhook: webhook } = - await this.linearApp.createWebhook({ - teamId, - ...args, - }); - this.setWebhookId(teamId, webhook.id); + if (await this.isAdmin()) { + const args = { + resourceTypes: this.getResourceTypes(), + url: this.http.endpoint, + label: this.getWebhookLabel(), + }; + if (!this.teamIds && !this.teamId) { + args.allPublicTeams = true; + const { _webhook: webhook } = await this.linearApp.createWebhook(args); + this.setWebhookId("1", webhook.id); + return; + } + const teamIds = this.teamIds || [ + this.teamId, + ]; + for (const teamId of teamIds) { + const { _webhook: webhook } = + await this.linearApp.createWebhook({ + teamId, + ...args, + }); + this.setWebhookId(teamId, webhook.id); + } } }, async deactivate() { - if (!this.teamIds && !this.teamId) { - const webhookId = this.getWebhookId("1"); - if (webhookId) { - await this.linearApp.deleteWebhook(webhookId); + if (await this.isAdmin()) { + if (!this.teamIds && !this.teamId) { + const webhookId = this.getWebhookId("1"); + if (webhookId) { + await this.linearApp.deleteWebhook(webhookId); + } + return; } - return; - } - const teamIds = this.teamIds || [ - this.teamId, - ]; - for (const teamId of teamIds) { - const webhookId = this.getWebhookId(teamId); - if (webhookId) { - await this.linearApp.deleteWebhook(webhookId); + const teamIds = this.teamIds || [ + this.teamId, + ]; + for (const teamId of teamIds) { + const webhookId = this.getWebhookId(teamId); + if (webhookId) { + await this.linearApp.deleteWebhook(webhookId); + } } } }, }, async run(event) { - const { - client_ip: clientIp, - body, - headers, - } = event; + if (!(await this.isAdmin())) { + await this.emitPolledResources(); + } else { + const { + client_ip: clientIp, + body, + headers, + } = event; - const { [constants.LINEAR_DELIVERY_HEADER]: delivery } = headers; + const { [constants.LINEAR_DELIVERY_HEADER]: delivery } = headers; - const resource = { - ...body, - delivery, - }; + const resource = { + ...body, + delivery, + }; - if (!this.isWebhookValid(clientIp)) { - console.log("Webhook is not valid"); - return; - } + if (!this.isWebhookValid(clientIp)) { + console.log("Webhook is not valid"); + return; + } - if (!(await this.isFromProject(body)) || !this.isRelevant(body)) { - return; - } + if (!(await this.isFromProject(body)) || !this.isRelevant(body)) { + return; + } - const meta = this.getMetadata(resource); - this.$emit(body, meta); + const meta = this.getMetadata(resource); + const item = await this.getResource(body.data); + this.$emit(item, meta); + } }, }; diff --git a/components/linear_app/sources/issue-created-instant/issue-created-instant.mjs b/components/linear_app/sources/issue-created-instant/issue-created-instant.mjs index aeb24af23591a..4a92022348116 100644 --- a/components/linear_app/sources/issue-created-instant/issue-created-instant.mjs +++ b/components/linear_app/sources/issue-created-instant/issue-created-instant.mjs @@ -5,9 +5,9 @@ export default { ...common, key: "linear_app-issue-created-instant", name: "New Created Issue (Instant)", - description: "Emit new event when a new issue is created. See the docs [here](https://developers.linear.app/docs/graphql/webhooks)", + description: "Emit new event when a new issue is created. [See the documentation](https://developers.linear.app/docs/graphql/webhooks)", type: "source", - version: "0.3.5", + version: "0.3.9", dedupe: "unique", methods: { ...common.methods, @@ -42,6 +42,11 @@ export default { isRelevant(body) { return body?.action === "create"; }, + getResource(issue) { + return this.linearApp.getIssue({ + issueId: issue.id, + }); + }, getMetadata(resource) { const { delivery, diff --git a/components/linear_app/sources/issue-updated-instant/issue-updated-instant.mjs b/components/linear_app/sources/issue-updated-instant/issue-updated-instant.mjs index 5bc302e9477d7..40d1faefbd43b 100644 --- a/components/linear_app/sources/issue-updated-instant/issue-updated-instant.mjs +++ b/components/linear_app/sources/issue-updated-instant/issue-updated-instant.mjs @@ -5,9 +5,9 @@ export default { ...common, key: "linear_app-issue-updated-instant", name: "New Updated Issue (Instant)", - description: "Emit new event when an issue is updated. See the docs [here](https://developers.linear.app/docs/graphql/webhooks)", + description: "Emit new event when an issue is updated. [See the documentation](https://developers.linear.app/docs/graphql/webhooks)", type: "source", - version: "0.3.5", + version: "0.3.9", dedupe: "unique", methods: { ...common.methods, @@ -39,17 +39,22 @@ export default { }, }; }, + getResource(issue) { + return this.linearApp.getIssue({ + issueId: issue.id, + }); + }, getMetadata(resource) { const { - delivery, title, data, updatedAt, } = resource; + const ts = Date.parse(data?.updatedAt || updatedAt); return { - id: delivery || resource.id, + id: `${resource.id}-${ts}`, summary: `Issue Updated: ${data?.title || title}`, - ts: Date.parse(updatedAt), + ts, }; }, }, diff --git a/components/linear_app/sources/new-issue-status-updated/new-issue-status-updated.mjs b/components/linear_app/sources/new-issue-status-updated/new-issue-status-updated.mjs index 7be699c344559..0583e546a0fea 100644 --- a/components/linear_app/sources/new-issue-status-updated/new-issue-status-updated.mjs +++ b/components/linear_app/sources/new-issue-status-updated/new-issue-status-updated.mjs @@ -1,18 +1,18 @@ import common from "../common/webhook.mjs"; import constants from "../../common/constants.mjs"; +import utils from "../../common/utils.mjs"; export default { ...common, key: "linear_app-new-issue-status-updated", name: "New Issue Status Updated (Instant)", - description: "Emit new event when the status of an issue is updated. See the docs [here](https://developers.linear.app/docs/graphql/webhooks)", + description: "Emit new event when the status of an issue is updated. [See the documentation](https://developers.linear.app/docs/graphql/webhooks)", type: "source", - version: "0.1.5", + version: "0.1.9", dedupe: "unique", props: { linearApp: common.props.linearApp, - http: common.props.http, - db: common.props.db, + db: "$.service.db", teamId: { label: "Team ID", type: "string", @@ -20,7 +20,7 @@ export default { common.props.linearApp, "teamId", ], - optional: true, + reloadProps: true, }, projectId: { propDefinition: [ @@ -41,6 +41,12 @@ export default { }, methods: { ...common.methods, + _getPreviousStatuses() { + return this.db.get("previousStatuses") || {}; + }, + _setPreviousStatuses(previousStatuses) { + this.db.set("previousStatuses", previousStatuses); + }, getResourceTypes() { return [ constants.RESOURCE_TYPE.ISSUE, @@ -79,6 +85,11 @@ export default { isRelevant(body) { return body?.updatedFrom?.stateId && (!this.stateId || body.data.stateId === this.stateId); }, + getResource(issue) { + return this.linearApp.getIssue({ + issueId: issue.id, + }); + }, getMetadata(resource) { const { delivery, @@ -86,11 +97,43 @@ export default { data, updatedAt, } = resource; + const ts = Date.parse(updatedAt); return { - id: delivery || resource.id, + id: delivery || `${resource.id}-${ts}`, summary: `Issue status updated: ${data?.title || title}`, - ts: Date.parse(updatedAt), + ts, }; }, + async emitPolledResources() { + const previousStatuses = this._getPreviousStatuses(); + const newStatuses = {}; + + const stream = this.linearApp.paginateResources({ + resourcesFn: this.getResourcesFn(), + resourcesFnArgs: this.getResourcesFnArgs(), + useGraphQl: this.useGraphQl(), + max: 1000, + }); + const resources = await utils.streamIterator(stream); + + const updatedResources = []; + for (const issue of resources) { + newStatuses[issue.id] = issue.state.id; + if (issue.createdAt === issue.updatedAt) { + continue; + } + if (previousStatuses[issue.id] !== issue.state.id) { + updatedResources.push(issue); + } + } + + this._setPreviousStatuses(newStatuses); + + updatedResources + .reverse() + .forEach((resource) => { + this.$emit(resource, this.getMetadata(resource)); + }); + }, }, }; diff --git a/components/linear_app/sources/new-projectupdate-created/new-projectupdate-created.mjs b/components/linear_app/sources/new-projectupdate-created/new-projectupdate-created.mjs new file mode 100644 index 0000000000000..2f6963dba9110 --- /dev/null +++ b/components/linear_app/sources/new-projectupdate-created/new-projectupdate-created.mjs @@ -0,0 +1,86 @@ +import common from "../common/webhook.mjs"; +import constants from "../../common/constants.mjs"; +import linearApp from "../../linear_app.app.mjs"; + +export default { + ...common, + key: "linear_app-new-projectupdate-created", + name: "New Project Update Written (Instant)", + description: "Project updates are short status reports on the health of your projects. Emit new event when a new Project Update is written. [See the documentation](https://developers.linear.app/docs/graphql/webhooks)", + type: "source", + version: "0.0.1", + dedupe: "unique", + props: { + linearApp, + db: "$.service.db", + teamId: { + label: "Team ID", + type: "string", + propDefinition: [ + common.props.linearApp, + "teamId", + ], + description: "The identifier or key of the team associated with the project", + reloadProps: true, + }, + projectId: { + propDefinition: [ + common.props.linearApp, + "projectId", + (c) => ({ + teamId: c.teamId, + }), + ], + description: "Filter results by project", + }, + }, + methods: { + ...common.methods, + getResourceTypes() { + return [ + constants.RESOURCE_TYPE.PROJECT_UPDATE, + ]; + }, + getWebhookLabel() { + return "Project Update created"; + }, + getResourcesFn() { + return this.linearApp.listProjectUpdates; + }, + getResourcesFnArgs() { + return { + orderBy: "createdAt", + filter: { + createdAt: { + gte: "-P1W", // within the last week + }, + }, + }; + }, + getResource(projectUpdate) { + return this.linearApp.getProjectUpdate(projectUpdate.id); + }, + isRelevant(body) { + const teamIds = body.data.infoSnapshot.teamsInfo.map(({ id }) => id); + return body?.action === "create" && teamIds.includes(this.teamId); + }, + isRelevantPolling(resource) { + const teamIds = resource.infoSnapshot.teamsInfo.map(({ id }) => id); + const projectId = resource.project.id; + return (teamIds.includes(this.teamId)) && (!this.projectId || projectId === this.projectId); + }, + getMetadata(resource) { + const { + data, + createdAt, + } = resource; + const ts = Date.parse(data?.createdAt || createdAt); + const id = data?.id || resource.id; + return { + id, + summary: `New Project Update: ${id}`, + ts, + }; + }, + }, +}; diff --git a/components/linear_app/sources/project-updated-instant/project-updated-instant.mjs b/components/linear_app/sources/project-updated-instant/project-updated-instant.mjs new file mode 100644 index 0000000000000..fa890ec44efa7 --- /dev/null +++ b/components/linear_app/sources/project-updated-instant/project-updated-instant.mjs @@ -0,0 +1,69 @@ +import common from "../common/webhook.mjs"; +import constants from "../../common/constants.mjs"; +import linearApp from "../../linear_app.app.mjs"; + +export default { + ...common, + key: "linear_app-project-updated-instant", + name: "New Updated Project (Instant)", + description: "Emit new event when a project is updated. [See the documentation](https://developers.linear.app/docs/graphql/webhooks)", + type: "source", + version: "0.0.2", + dedupe: "unique", + props: { + linearApp, + teamIds: { + label: "Team IDs", + type: "string[]", + propDefinition: [ + linearApp, + "teamId", + ], + description: "The identifier or key of the team associated with the project", + reloadProps: true, + }, + db: "$.service.db", + }, + methods: { + ...common.methods, + getResourceTypes() { + return [ + constants.RESOURCE_TYPE.PROJECT, + ]; + }, + getWebhookLabel() { + return "Project updated"; + }, + getResourcesFn() { + return this.linearApp.listProjects; + }, + getResourcesFnArgs() { + return { + orderBy: "updatedAt", + filter: { + accessibleTeams: { + id: { + in: this.teamIds, + }, + }, + }, + }; + }, + getResource(project) { + return this.linearApp.getProject(project.id); + }, + getMetadata(resource) { + const { + name, + data, + updatedAt, + } = resource; + const ts = Date.parse(data?.updatedAt || updatedAt); + return { + id: `${resource.id}-${ts}`, + summary: `Project Updated: ${data?.name || name}`, + ts, + }; + }, + }, +}; diff --git a/components/linearb/README.md b/components/linearb/README.md new file mode 100644 index 0000000000000..d657c6693dc34 --- /dev/null +++ b/components/linearb/README.md @@ -0,0 +1,11 @@ +# Overview + +The LinearB API serves as a bridge between LinearB's project management insights and your preferred platforms, offering the ability to automate workflows, generate reports, and trigger actions based on project metrics. With Pipedream, these capabilities can be harnessed to create custom workflows that respond to events within LinearB, such as changes in project status or team performance metrics. The data can be pushed to various services, such as communication tools, databases, or other project management apps to enhance visibility and coordination across your team. + +# Example Use Cases + +- **Sync LinearB Project Updates to Slack**: Automatically post updates in a Slack channel whenever a new project is created or a project's status changes in LinearB. This keeps the entire team aligned and promptly informed about the project's progress. + +- **Create LinearB Metrics Dashboard**: Use the LinearB API to fetch key performance metrics and pipe them into a Google Sheets document on a regular schedule. This allows teams to maintain a live dashboard that tracks project health and team productivity without manual data entry. + +- **Trigger Incident Management on Status Change**: Set up a workflow that monitors project status changes in LinearB, and triggers an incident in PagerDuty when certain thresholds are met (like a project falling behind schedule). This ensures that stakeholders are alerted and can take immediate action to address project risks. diff --git a/components/linguapop/README.md b/components/linguapop/README.md new file mode 100644 index 0000000000000..f10ab083b68ab --- /dev/null +++ b/components/linguapop/README.md @@ -0,0 +1,11 @@ +# Overview + +The Linguapop API offers functionalities for language learning applications, focusing on vocabulary, grammar, and language exercises. Through integrating this API with Pipedream, you can automate language learning processes, personalize educational content delivery, and streamline language proficiency assessments. Pipedream’s ability to connect with numerous other APIs and services enables the development of customized, efficient workflows that enhance the learning experience. + +# Example Use Cases + +- **Automated Language Learning Progress Tracker**: Build a workflow on Pipedream that listens for completion events from Linguapop exercises. After an exercise is completed, the workflow can log this data in Google Sheets or a similar service. This allows educators and learners to track progress over time, identify areas needing improvement, and adjust learning plans accordingly. + +- **Personalized Daily Language Practice Emails**: Use the Linguapop API to fetch personalized language exercises based on a user's past performance and preferences. Set up a daily cron job in Pipedream to send these exercises via email using the SendGrid API. This automates the delivery of daily practice exercises, helping learners stay engaged and improve consistently. + +- **Slack Integration for Language Learning Reminders**: Create a workflow that triggers at scheduled intervals using Pipedream’s cron job capabilities. This workflow fetches new vocabulary or grammar lessons from Linguapop and posts them to a designated Slack channel or directly to users via Slack bots. This is especially useful for teams or study groups looking to integrate language learning into their regular communications. diff --git a/components/linguapop/actions/create-test-invitation/create-test-invitation.mjs b/components/linguapop/actions/create-test-invitation/create-test-invitation.mjs new file mode 100644 index 0000000000000..aa3d60df055c5 --- /dev/null +++ b/components/linguapop/actions/create-test-invitation/create-test-invitation.mjs @@ -0,0 +1,68 @@ +import linguapop from "../../linguapop.app.mjs"; + +export default { + key: "linguapop-create-test-invitation", + name: "Create Test Invitation", + description: "Creates a new placement test invitation. [See the documentation](https://docs.linguapop.eu/api/#sendcreate-an-invitation-to-a-placement-test)", + version: "0.0.1", + type: "action", + props: { + linguapop, + email: { + type: "string", + label: "Email", + description: "The candidate's email address. This can also be used to identify your candidate.", + }, + sendEmail: { + type: "boolean", + label: "Send Email", + description: "This determines whether Linguapop sends an invitation email to your candidate.", + }, + languageCode: { + propDefinition: [ + linguapop, + "languageCode", + ], + }, + externalIdentifier: { + type: "string", + label: "External Identifier", + description: "This is an ID you can generate from your CRM to uniquiely identify your candidate.", + optional: true, + }, + testReading: { + type: "boolean", + label: "Test Reading", + description: "This determines whether the test will include a reading section. If not provided, the default option set in your [Customization](https://docs.linguapop.eu/customization/#additional-skills-tested) will be used.", + optional: true, + }, + testListening: { + type: "boolean", + label: "Test Listening", + description: "This determines whether the test will include a listening section. If not provided, the default option set in your [Customization](https://docs.linguapop.eu/customization/#additional-skills-tested) will be used.", + optional: true, + }, + callbackUrl: { + type: "string", + label: "Callback URL", + description: "This is the [callback URL](https://docs.linguapop.eu/api/#the-callback-url) to which a POST request will be sent with the results of the placement test once the test is completed.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.linguapop.createTestInvitation({ + $, + data: { + email: this.email, + sendEmail: this.sendEmail, + languageCode: this.languageCode, + externalIdentifier: this.externalIdentifier, + testReading: this.testReading, + testListening: this.testListening, + callbackUrl: this.callbackUrl, + }, + }); + $.export("$summary", `Successfully created test invitation for ${this.email}`); + return response; + }, +}; diff --git a/components/linguapop/linguapop.app.mjs b/components/linguapop/linguapop.app.mjs new file mode 100644 index 0000000000000..1da9deb02015b --- /dev/null +++ b/components/linguapop/linguapop.app.mjs @@ -0,0 +1,61 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "linguapop", + propDefinitions: { + languageCode: { + type: "string", + label: "Language Code", + description: "The code of the language you want to test", + async options() { + const languages = await this.listLanguages(); + return languages.map(({ + code: value, name: label, + }) => ({ + value, + label, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://app.linguapop.eu/api/actions"; + }, + _authParams(params) { + return { + ...params, + apiKey: `${this.$auth.api_key}`, + }; + }, + _makeRequest(opts = {}) { + const { + $ = this, path, params, data, ...otherOpts + } = opts; + const config = { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + }; + if (params) { + config.params = this._authParams(params); + } else if (data) { + config.data = this._authParams(data); + } + return axios($, config); + }, + listLanguages(opts = {}) { + return this._makeRequest({ + path: "/getLanguages", + ...opts, + }); + }, + createTestInvitation(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/sendInvitation", + ...opts, + }); + }, + }, +}; diff --git a/components/linguapop/package.json b/components/linguapop/package.json new file mode 100644 index 0000000000000..1de77812d48fa --- /dev/null +++ b/components/linguapop/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/linguapop", + "version": "0.1.0", + "description": "Pipedream Linguapop Components", + "main": "linguapop.app.mjs", + "keywords": [ + "pipedream", + "linguapop" + ], + "homepage": "https://pipedream.com/apps/linguapop", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} diff --git a/components/linguapop/sources/new-placement-test-result-instant/new-placement-test-result-instant.mjs b/components/linguapop/sources/new-placement-test-result-instant/new-placement-test-result-instant.mjs new file mode 100644 index 0000000000000..6cdc7e1da1b6c --- /dev/null +++ b/components/linguapop/sources/new-placement-test-result-instant/new-placement-test-result-instant.mjs @@ -0,0 +1,30 @@ +import linguapop from "../../linguapop.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "linguapop-new-placement-test-result-instant", + name: "New Placement Test Result (Instant)", + description: "Emit new event when a placement test is completed. Must setup the source's URL as the `callbackUrl` when sending the test invitation. [See the documentation](https://docs.linguapop.eu/api/#sendcreate-an-invitation-to-a-placement-test)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + linguapop, + http: "$.interface.http", + }, + methods: { + generateMeta(result) { + return { + id: result.invitationId, + summary: `Test completed by ${result.email}`, + ts: Date.parse(result.completedOn), + }; + }, + }, + async run(event) { + const { body } = event; + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, + sampleEmit, +}; diff --git a/components/linguapop/sources/new-placement-test-result-instant/test-event.mjs b/components/linguapop/sources/new-placement-test-result-instant/test-event.mjs new file mode 100644 index 0000000000000..5a855718b6099 --- /dev/null +++ b/components/linguapop/sources/new-placement-test-result-instant/test-event.mjs @@ -0,0 +1,17 @@ +export default { + "externalIdentifier": "EXTERNAL_IDENTIFIER", + "invitationId": "INVITATION_ID", + "placementTestId": "PLACEMENT_TEST_ID", + "name": "CANDIDATE_NAME", + "email": "CANDIDATE_EMAIL", + "phone": "CANDIDATE_PHONE", + "finalLevel": "B2 High", + "finalLevelCode": "B2H", + "reading": null, + "readingMax": null, + "listening": null, + "listeningMax": null, + "rating": 1652.7278254974933, + "completedOn": "2023-08-14T13:58:06.391139Z", + "publicResultsUrl": "PUBLIC_RESULTS_URL" +} \ No newline at end of file diff --git a/components/linkedin/README.md b/components/linkedin/README.md new file mode 100644 index 0000000000000..b8a98413e6df6 --- /dev/null +++ b/components/linkedin/README.md @@ -0,0 +1,12 @@ +# Overview + +The LinkedIn API on Pipedream allows you to automate interactions with your LinkedIn account, such as posting updates, managing your profile, and engaging with your network. With Pipedream's serverless platform, you can create workflows that trigger on various events, process data, and connect with countless other apps to extend your LinkedIn presence, analyze your network's demographics, automate content sharing, and more. + +# Example Use Cases + +- **Automated Content Sharing**: Share blog posts or articles from your company website to LinkedIn as soon as they're published by connecting the RSS feed to LinkedIn using Pipedream's built-in RSS trigger and LinkedIn actions. + +- **Profile Update Notifications**: Set up a workflow that monitors changes to your LinkedIn profile and sends you or your team an email alert whenever updates are made. This can be useful for maintaining a record of changes or for team transparency about profile edits. + +- **Scheduled Posts and Engagement Analysis**: Create a workflow that schedules and posts content to LinkedIn at optimal times. Include steps that analyze post engagement using Pipedream's Data Store to track likes, comments, and shares over time, which helps in understanding content performance. +. diff --git a/components/linkedin/actions/create-comment/create-comment.mjs b/components/linkedin/actions/create-comment/create-comment.mjs index d3e0b05a6c8ce..d4e90266a0e31 100644 --- a/components/linkedin/actions/create-comment/create-comment.mjs +++ b/components/linkedin/actions/create-comment/create-comment.mjs @@ -4,7 +4,7 @@ export default { key: "linkedin-create-comment", name: "Create Comment", description: "Create a comment on a share or user generated content post. [See the docs here](https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/network-update-social-actions#create-comment)", - version: "0.1.4", + version: "0.1.5", type: "action", props: { linkedin, diff --git a/components/linkedin/actions/create-image-post-organization/create-image-post-organization.mjs b/components/linkedin/actions/create-image-post-organization/create-image-post-organization.mjs index bd9b1238ed4f5..1ddb3239e89b8 100644 --- a/components/linkedin/actions/create-image-post-organization/create-image-post-organization.mjs +++ b/components/linkedin/actions/create-image-post-organization/create-image-post-organization.mjs @@ -6,7 +6,7 @@ export default { key: "linkedin-create-image-post-organization", name: "Create Image Post (Organization)", description: "Create an image post on LinkedIn. [See the docs here](https://learn.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/images-api?view=li-lms-2023-09&tabs=http#uploading-an-image)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { linkedin, diff --git a/components/linkedin/actions/create-image-post-user/create-image-post-user.mjs b/components/linkedin/actions/create-image-post-user/create-image-post-user.mjs index 61ffa5d4c185d..06471fe9384cc 100644 --- a/components/linkedin/actions/create-image-post-user/create-image-post-user.mjs +++ b/components/linkedin/actions/create-image-post-user/create-image-post-user.mjs @@ -6,7 +6,7 @@ export default { key: "linkedin-create-image-post-user", name: "Create Image Post (User)", description: "Create an image post on LinkedIn. [See the docs here](https://learn.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/images-api?view=li-lms-2023-09&tabs=http#uploading-an-image)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { linkedin, diff --git a/components/linkedin/actions/create-like-on-share/create-like-on-share.mjs b/components/linkedin/actions/create-like-on-share/create-like-on-share.mjs index 38ae9457bffe7..90df9492d9cd5 100644 --- a/components/linkedin/actions/create-like-on-share/create-like-on-share.mjs +++ b/components/linkedin/actions/create-like-on-share/create-like-on-share.mjs @@ -4,7 +4,7 @@ export default { key: "linkedin-create-like-on-share", name: "Create Like On Share", description: "Creates a like on a share. [See the docs here](https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/network-update-social-actions#create-a-like-on-a-share)", - version: "0.1.4", + version: "0.1.5", type: "action", props: { linkedin, diff --git a/components/linkedin/actions/create-text-post-organization/create-text-post-organization.mjs b/components/linkedin/actions/create-text-post-organization/create-text-post-organization.mjs index e2af84549748c..ed325d91435bc 100644 --- a/components/linkedin/actions/create-text-post-organization/create-text-post-organization.mjs +++ b/components/linkedin/actions/create-text-post-organization/create-text-post-organization.mjs @@ -4,7 +4,7 @@ export default { key: "linkedin-create-text-post-organization", name: "Create a Simple Post (Organization)", description: "Create post on LinkedIn using text, URL or article. [See the docs](https://learn.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/posts-api?view=li-lms-2022-11&tabs=http#create-organic-posts) for more information", - version: "0.0.4", + version: "0.0.5", type: "action", props: { linkedin, diff --git a/components/linkedin/actions/create-text-post-user/create-text-post-user.mjs b/components/linkedin/actions/create-text-post-user/create-text-post-user.mjs index 5326acee5f761..197081e14da1d 100644 --- a/components/linkedin/actions/create-text-post-user/create-text-post-user.mjs +++ b/components/linkedin/actions/create-text-post-user/create-text-post-user.mjs @@ -4,7 +4,7 @@ export default { key: "linkedin-create-text-post-user", name: "Create a Simple Post (User)", description: "Create post on LinkedIn using text, URL or article. [See the docs](https://learn.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/posts-api?view=li-lms-2022-11&tabs=http#create-organic-posts) for more information", - version: "0.0.4", + version: "0.0.5", type: "action", props: { linkedin, diff --git a/components/linkedin/actions/delete-post/delete-post.mjs b/components/linkedin/actions/delete-post/delete-post.mjs index 86352723d639a..0aca5da5e447b 100644 --- a/components/linkedin/actions/delete-post/delete-post.mjs +++ b/components/linkedin/actions/delete-post/delete-post.mjs @@ -4,7 +4,7 @@ export default { key: "linkedin-delete-post", name: "Delete Post", description: "Removes a post from user's wall. [See the docs](https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/share-api?tabs=http#delete-shares) for more information", - version: "0.0.4", + version: "0.0.5", type: "action", props: { linkedin, diff --git a/components/linkedin/actions/fetch-ad-account/fetch-ad-account.mjs b/components/linkedin/actions/fetch-ad-account/fetch-ad-account.mjs index 717b2f2ee10ca..512f0d3e8de06 100644 --- a/components/linkedin/actions/fetch-ad-account/fetch-ad-account.mjs +++ b/components/linkedin/actions/fetch-ad-account/fetch-ad-account.mjs @@ -4,7 +4,7 @@ export default { key: "linkedin-fetch-ad-account", name: "Fetch Ad Account", description: "Fetches an individual adAccount given its id. [See the docs here](https://docs.microsoft.com/en-us/linkedin/marketing/integrations/ads/account-structure/create-and-manage-accounts#fetch-ad-account)", - version: "0.1.4", + version: "0.1.5", type: "action", props: { linkedin, diff --git a/components/linkedin/actions/get-current-member-profile/get-current-member-profile.mjs b/components/linkedin/actions/get-current-member-profile/get-current-member-profile.mjs index 741c8d2f7dae8..1fb95e0f41697 100644 --- a/components/linkedin/actions/get-current-member-profile/get-current-member-profile.mjs +++ b/components/linkedin/actions/get-current-member-profile/get-current-member-profile.mjs @@ -4,7 +4,7 @@ export default { key: "linkedin-get-current-member-profile", name: "Get Current Member Profile", description: "Gets the profile of the current authenticated member. [See the docs here](https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api#retrieve-current-members-profile)", - version: "0.1.4", + version: "0.1.5", type: "action", props: { linkedin, diff --git a/components/linkedin/actions/get-member-organization-access-control/get-member-organization-access-control.mjs b/components/linkedin/actions/get-member-organization-access-control/get-member-organization-access-control.mjs index 650ad67d1ebd2..5bcb826d9b68e 100644 --- a/components/linkedin/actions/get-member-organization-access-control/get-member-organization-access-control.mjs +++ b/components/linkedin/actions/get-member-organization-access-control/get-member-organization-access-control.mjs @@ -4,7 +4,7 @@ export default { key: "linkedin-get-member-organization-access-control", name: "Get Member's Organization Access Control Information", description: "Gets the organization access control information of the current authenticated member. [See the docs here](https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/organizations/organization-access-control?context=linkedin/compliance/context#find-a-members-organization-access-control-information)", - version: "0.1.4", + version: "0.1.5", type: "action", props: { linkedin, diff --git a/components/linkedin/actions/get-member-profile/get-member-profile.mjs b/components/linkedin/actions/get-member-profile/get-member-profile.mjs index ee3cd1cb40c44..88f3e0d32f4c6 100644 --- a/components/linkedin/actions/get-member-profile/get-member-profile.mjs +++ b/components/linkedin/actions/get-member-profile/get-member-profile.mjs @@ -4,7 +4,7 @@ export default { key: "linkedin-get-member-profile", name: "Get Member Profile", description: "Gets another member's profile, given its person id. [See the docs here](https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api#retrieve-other-members-profile)", - version: "0.1.4", + version: "0.1.5", type: "action", props: { linkedin, diff --git a/components/linkedin/actions/get-multiple-member-profiles/get-multiple-member-profiles.mjs b/components/linkedin/actions/get-multiple-member-profiles/get-multiple-member-profiles.mjs index 62e34d3d8dd11..4d0e936b07a2a 100644 --- a/components/linkedin/actions/get-multiple-member-profiles/get-multiple-member-profiles.mjs +++ b/components/linkedin/actions/get-multiple-member-profiles/get-multiple-member-profiles.mjs @@ -4,7 +4,7 @@ export default { key: "linkedin-get-multiple-member-profiles", name: "Get Multiple Member Profiles", description: "Gets multiple member profiles at once. [See the docs here](https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api#retrieve-other-members-profile)", - version: "0.1.4", + version: "0.1.5", type: "action", props: { linkedin, diff --git a/components/linkedin/actions/get-organization-access-control/get-organization-access-control.mjs b/components/linkedin/actions/get-organization-access-control/get-organization-access-control.mjs index 7657b8742abf2..c4e27df6ca920 100644 --- a/components/linkedin/actions/get-organization-access-control/get-organization-access-control.mjs +++ b/components/linkedin/actions/get-organization-access-control/get-organization-access-control.mjs @@ -4,7 +4,7 @@ export default { key: "linkedin-get-organization-access-control", name: "Gets Organization Access Control", description: "Gets an organization's access control information, given the organization urn. [See the docs here](https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/organizations/organization-access-control?context=linkedin/compliance/context#find-access-control-information)", - version: "0.1.4", + version: "0.1.5", type: "action", props: { linkedin, @@ -21,24 +21,20 @@ export default { }, }, async run({ $ }) { + let start = 0; const count = 50; const results = []; - const params = { - q: "organization", - organization: encodeURI(this.organizationUrn), - start: 0, - count, - }; + const params = `q=organization&organization=${this.organizationUrn.replace(/:/g, "%3A")}&count=${count}`; let done = false; do { - const { elements } = await this.linkedin.getAccessControl({ - $, - params, + const { data: { elements } } = await this.linkedin.getAccessControl({ + params: params + `&start=${start}`, }); + results.push(...elements); - params.start += count; + start += count; if (elements?.length < count) { done = true; } diff --git a/components/linkedin/actions/get-organization-administrators/get-organization-administrators.mjs b/components/linkedin/actions/get-organization-administrators/get-organization-administrators.mjs index 63aefbb626211..d1869d3c75f51 100644 --- a/components/linkedin/actions/get-organization-administrators/get-organization-administrators.mjs +++ b/components/linkedin/actions/get-organization-administrators/get-organization-administrators.mjs @@ -4,7 +4,7 @@ export default { key: "linkedin-get-organization-administrators", name: "Get Organization Administrators", description: "Gets the administator members of an organization, given the organization urn. [See the docs here](https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/organizations/organization-access-control?context=linkedin/compliance/context#find-organization-administrators)", - version: "0.2.4", + version: "0.2.5", type: "action", props: { linkedin, @@ -21,26 +21,20 @@ export default { }, }, async run({ $ }) { + let start = 0; const count = 50; const results = []; - const params = { - q: "organization", - organization: encodeURI(this.organizationUrn), - role: "ADMINISTRATOR", - state: "APPROVED", - start: 0, - count, - }; + const params = `q=organization&organization=${this.organizationUrn.replace(/:/g, "%3A")}&role=ADMINISTRATOR&state=APPROVED&count=${count}`; let done = false; do { - const { elements } = await this.linkedin.getAccessControl({ - $, - params, + const { data: { elements } } = await this.linkedin.getAccessControl({ + params: params + `&start=${start}`, }); + results.push(...elements); - params.start += count; + start += count; if (elements?.length < count) { done = true; } diff --git a/components/linkedin/actions/retrieve-comments-on-comments/retrieve-comments-on-comments.mjs b/components/linkedin/actions/retrieve-comments-on-comments/retrieve-comments-on-comments.mjs index 511f977875973..1275a478c5355 100644 --- a/components/linkedin/actions/retrieve-comments-on-comments/retrieve-comments-on-comments.mjs +++ b/components/linkedin/actions/retrieve-comments-on-comments/retrieve-comments-on-comments.mjs @@ -4,7 +4,7 @@ export default { key: "linkedin-retrieve-comments-on-comments", name: "Retrieves Comments on Comments", description: "Retrieves comments on comments, given the parent comment urn. [See the docs here](https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/network-update-social-actions#retrieve-comments-on-comments)", - version: "0.1.4", + version: "0.1.5", type: "action", props: { linkedin, diff --git a/components/linkedin/actions/retrieve-comments-shares/retrieve-comments-shares.mjs b/components/linkedin/actions/retrieve-comments-shares/retrieve-comments-shares.mjs index 4df7261e9bb37..9bf222cf80e1e 100644 --- a/components/linkedin/actions/retrieve-comments-shares/retrieve-comments-shares.mjs +++ b/components/linkedin/actions/retrieve-comments-shares/retrieve-comments-shares.mjs @@ -4,7 +4,7 @@ export default { key: "linkedin-retrieve-comments-shares", name: "Retrieve Comments On Shares", description: "Retrieve comments on shares given the share urn. [See the docs here](https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/network-update-social-actions#retrieve-comments-on-shares)", - version: "0.1.4", + version: "0.1.5", type: "action", props: { linkedin, diff --git a/components/linkedin/actions/search-organization/search-organization.mjs b/components/linkedin/actions/search-organization/search-organization.mjs index 8a924d0bda13a..f32f128e3ef60 100644 --- a/components/linkedin/actions/search-organization/search-organization.mjs +++ b/components/linkedin/actions/search-organization/search-organization.mjs @@ -4,7 +4,7 @@ export default { key: "linkedin-search-organization", name: "Search Organization", description: "Searches for an organization by vanity name or email domain. [See the docs here](https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/organizations/organization-lookup-api)", - version: "0.1.4", + version: "0.1.5", type: "action", props: { linkedin, diff --git a/components/linkedin/common/constants.mjs b/components/linkedin/common/constants.mjs index 2d6dd75a13e43..a84bbac4cf002 100644 --- a/components/linkedin/common/constants.mjs +++ b/components/linkedin/common/constants.mjs @@ -1,6 +1,6 @@ const VERSION_PATH = "rest"; const BASE_URL = "https://api.linkedin.com/"; -const VERSION_HEADER = "202309"; +const VERSION_HEADER = "202409"; const VISIBILITIES = [ { diff --git a/components/linkedin/linkedin.app.mjs b/components/linkedin/linkedin.app.mjs index 2550f28ab1bf5..620ee795e9762 100644 --- a/components/linkedin/linkedin.app.mjs +++ b/components/linkedin/linkedin.app.mjs @@ -1,4 +1,5 @@ -import { axios } from "@pipedream/platform"; +import { axios as axiosPD } from "@pipedream/platform"; +import axios from "axios"; import constants from "./common/constants.mjs"; export default { @@ -45,7 +46,7 @@ export default { }); return elements?.map((element) => ({ label: element.name, - id: element.id, + value: element.id, })); }, }, @@ -138,13 +139,22 @@ export default { }) { const BASE_URL = constants.BASE_URL; - const config = { + return axiosPD($ || this, { url: url || `${BASE_URL}${constants.VERSION_PATH}${path}`, headers: this._getHeaders(), ...otherConfig, - }; + }); + }, + async _makeRequestAxios({ + url, path, ...otherConfig + }) { + const BASE_URL = constants.BASE_URL; - return axios($ || this, config); + return axios({ + url: url || `${BASE_URL}${constants.VERSION_PATH}${path}`, + headers: this._getHeaders(), + ...otherConfig, + }); }, async createPost({ data, ...args @@ -223,10 +233,9 @@ export default { ...args, }); }, - async getAccessControl(args = {}) { - return this._makeRequest({ - path: "/organizationAcls", - ...args, + async getAccessControl({ params }) { + return this._makeRequestAxios({ + path: `/organizationAcls?${params}`, }); }, async queryAnaltyics(query, args = {} ) { diff --git a/components/linkedin/package.json b/components/linkedin/package.json index 1b03f843ecd9f..e5768a8b56aa5 100644 --- a/components/linkedin/package.json +++ b/components/linkedin/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/linkedin", - "version": "0.1.0", + "version": "0.1.1", "description": "Pipedream Linkedin Components", "main": "linkedin.app.mjs", "keywords": [ diff --git a/components/linkedin_ads/README.md b/components/linkedin_ads/README.md new file mode 100644 index 0000000000000..1645d64c59346 --- /dev/null +++ b/components/linkedin_ads/README.md @@ -0,0 +1,11 @@ +# Overview + +The LinkedIn Ads API on Pipedream enables you to automate and integrate your LinkedIn advertising efforts with other services. Fetch campaign data, manage ad accounts, or automate ad creation and adjustments. With Pipedream, you can trigger workflows with HTTP requests, emails, and on a schedule, and connect to thousands of other apps with minimal effort. + +# Example Use Cases + +- **Sync LinkedIn Ad Campaigns to Google Sheets**: Automate the syncing of LinkedIn Ad campaign data into a Google Sheets spreadsheet. This workflow makes it easy to analyze ad performance, share reports with your team, and maintain records without manual data entry. + +- **LinkedIn Ad Performance Alerts via Slack or Email**: Set up a workflow that monitors your LinkedIn Ad campaigns for key performance metrics, and sends an alert via Slack or Email when your ads meet certain criteria, such as cost per click (CPC) thresholds or impressions goals, ensuring prompt reaction to ad performance. + +- **Automatic Ad Pausing Based on Budget Constraints**: Create a workflow that checks your LinkedIn Ad spend against your set budget and automatically pauses campaigns if spend exceeds your budget. This helps maintain control over your advertising costs without needing to check in manually. diff --git a/components/linkedin_ads/actions/create-report-by-advertiser-account/create-report-by-advertiser-account.mjs b/components/linkedin_ads/actions/create-report-by-advertiser-account/create-report-by-advertiser-account.mjs index 40cc269aacd10..1ab349ff56954 100644 --- a/components/linkedin_ads/actions/create-report-by-advertiser-account/create-report-by-advertiser-account.mjs +++ b/components/linkedin_ads/actions/create-report-by-advertiser-account/create-report-by-advertiser-account.mjs @@ -6,7 +6,7 @@ export default { key: "linkedin_ads-create-report-by-advertiser-account", name: "Create Report By Advertiser Account", description: "Sample query using analytics finder that gets analytics for a particular account for date range starting in a given year. [See the docs here](https://docs.microsoft.com/en-us/linkedin/marketing/integrations/ads-reporting/ads-reporting#sample-request)", - version: "0.0.2", + version: "0.0.5", type: "action", props: { ...common.props, diff --git a/components/linkedin_ads/actions/create-report-by-campaign/create-report-by-campaign.mjs b/components/linkedin_ads/actions/create-report-by-campaign/create-report-by-campaign.mjs index b3e265580acae..61aa86d27cbf1 100644 --- a/components/linkedin_ads/actions/create-report-by-campaign/create-report-by-campaign.mjs +++ b/components/linkedin_ads/actions/create-report-by-campaign/create-report-by-campaign.mjs @@ -6,7 +6,7 @@ export default { key: "linkedin_ads-create-report-by-campaign", name: "Query Analytics Finder Campaign Sample", description: "Sample query using analytics finder that gets analytics for a particular campaign in a date range starting in a given year. [See the docs here](https://docs.microsoft.com/en-us/linkedin/marketing/integrations/ads-reporting/ads-reporting#analytics-finder)", - version: "0.0.2", + version: "0.0.5", type: "action", props: { ...common.props, diff --git a/components/linkedin_ads/actions/create-report/create-report.mjs b/components/linkedin_ads/actions/create-report/create-report.mjs index 3c7d82101013f..84818d71ef934 100644 --- a/components/linkedin_ads/actions/create-report/create-report.mjs +++ b/components/linkedin_ads/actions/create-report/create-report.mjs @@ -6,7 +6,7 @@ export default { key: "linkedin_ads-create-report", name: "Create A Report", description: "Queries the Analytics Finder to get analytics for the specified entity i.e company, account, campaign. [See the docs here](https://docs.microsoft.com/en-us/linkedin/marketing/integrations/ads-reporting/ads-reporting#analytics-finder)", - version: "0.0.2", + version: "0.0.5", type: "action", props: { ...common.props, diff --git a/components/linkedin_ads/actions/send-conversion-event/send-conversion-event.mjs b/components/linkedin_ads/actions/send-conversion-event/send-conversion-event.mjs index aede1470483fa..2e5eb71397281 100644 --- a/components/linkedin_ads/actions/send-conversion-event/send-conversion-event.mjs +++ b/components/linkedin_ads/actions/send-conversion-event/send-conversion-event.mjs @@ -5,7 +5,7 @@ export default { key: "linkedin_ads-send-conversion-event", name: "Send Conversion Event", description: "Sends a conversion event to LinkedIn Ads. [See the documentation](https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads-reporting/conversions-api?view=li-lms-2024-01&tabs=http#streaming-conversion-events)", - version: "0.0.1", + version: "0.0.4", type: "action", props: { app, diff --git a/components/linkedin_ads/linkedin_ads.app.mjs b/components/linkedin_ads/linkedin_ads.app.mjs index eb91f410f1ea2..f43fdf1c20bed 100644 --- a/components/linkedin_ads/linkedin_ads.app.mjs +++ b/components/linkedin_ads/linkedin_ads.app.mjs @@ -1,4 +1,4 @@ -import app from "../linkedin/linkedin.app.mjs"; +import app from "@pipedream/linkedin"; import utils from "./common/utils.mjs"; export default { @@ -123,6 +123,31 @@ export default { })); }, }, + leadFormId: { + type: "string", + label: "Lead Form ID", + description: "Select the lead form to retrieve responses for.", + async options({ + page, adAccountId, + }) { + const count = 20; + const { elements } = await this.searchLeadForms({ + params: { + q: "owner", + owner: `(sponsoredAccount:${this.getSponsoredAccountUrn(adAccountId)})`, + count, + start: page * count, + }, + }); + return elements.map(({ + id, + name: label, + }) => ({ + label, + value: String(id), + })); + }, + }, }, methods: { ...app.methods, @@ -141,6 +166,9 @@ export default { getEventUrn(id) { return `urn:li:event:${id}`; }, + getVersionedLeadGenFormUrn(id, version = 1) { + return `urn:li:versionedLeadGenForm:(urn:li:leadGenForm:${id},${version})`; + }, searchCampaigns({ adAccountId, ...args } = {}) { @@ -154,18 +182,24 @@ export default { "owner", ]; return this._makeRequest({ - debug: true, path: "/leadForms", paramsSerializer: utils.getParamsSerializer(utils.encodeParamKeys(keys)), ...args, }); }, + getLeadForm({ + leadFormId, ...args + } = {}) { + return this._makeRequest({ + path: `/leadForms/${leadFormId}`, + ...args, + }); + }, searchEvents(args = {}) { const keys = [ "organizer", ]; return this._makeRequest({ - debug: true, path: "/events", paramsSerializer: utils.getParamsSerializer(utils.encodeParamKeys(keys)), transformResponse: utils.transformResponse, @@ -174,7 +208,6 @@ export default { }, searchConversions(args = {}) { return this._makeRequest({ - debug: true, path: "/conversions", paramsSerializer: utils.getParamsSerializer(utils.encodeFn), ...args, @@ -187,7 +220,6 @@ export default { "associatedEntity", ]; return this._makeRequest({ - debug: true, path: "/leadFormResponses", paramsSerializer: utils.getParamsSerializer(utils.encodeParamKeys(keys)), ...args, diff --git a/components/linkedin_ads/package.json b/components/linkedin_ads/package.json index 58e6d5f9d9d52..8d4bb20a4e294 100644 --- a/components/linkedin_ads/package.json +++ b/components/linkedin_ads/package.json @@ -1,18 +1,19 @@ { "name": "@pipedream/linkedin_ads", - "version": "0.2.0", + "version": "0.3.2", "description": "Pipedream LinkedIn Ads Components", "main": "linkedin_ads.app.mjs", "keywords": [ "pipedream", "linkedin_ads" ], - "files": [ - "dist" - ], "homepage": "https://pipedream.com/apps/linkedin_ads", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/linkedin": "^0.1.1", + "@pipedream/platform": "^3.0.3" } } diff --git a/components/linkedin_ads/sources/new-event-registration-form-response/new-event-registration-form-response.mjs b/components/linkedin_ads/sources/new-event-registration-form-response/new-event-registration-form-response.mjs index 8cbd314628392..7b1c0947a60a3 100644 --- a/components/linkedin_ads/sources/new-event-registration-form-response/new-event-registration-form-response.mjs +++ b/components/linkedin_ads/sources/new-event-registration-form-response/new-event-registration-form-response.mjs @@ -5,7 +5,7 @@ export default { key: "linkedin_ads-new-event-registration-form-response", name: "New Event Registration Form Response", description: "Emit new event when a fresh response is received on the event registration form. User needs to configure the prop of the specific event. [See the documentation](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/organizations/events?view=li-lms-2024-01&tabs=http)", - version: "0.0.1", + version: "0.0.4", type: "source", dedupe: "unique", props: { diff --git a/components/linkedin_ads/sources/new-lead-gen-form-created/new-lead-gen-form-created.mjs b/components/linkedin_ads/sources/new-lead-gen-form-created/new-lead-gen-form-created.mjs new file mode 100644 index 0000000000000..cb2996ac6238d --- /dev/null +++ b/components/linkedin_ads/sources/new-lead-gen-form-created/new-lead-gen-form-created.mjs @@ -0,0 +1,68 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import app from "../../linkedin_ads.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "linkedin_ads-new-lead-gen-form-created", + name: "New Lead Gen Form Created", + description: "Emit new event when a new lead is captured through a form. [See the documentation](https://learn.microsoft.com/en-us/linkedin/marketing/lead-sync/leadsync?view=li-lms-2023-07&tabs=http#find-lead-form-responses-by-owner)", + version: "0.0.3", + type: "source", + dedupe: "unique", + props: { + app, + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + adAccountId: { + propDefinition: [ + app, + "adAccountId", + ], + }, + leadFormId: { + propDefinition: [ + app, + "leadFormId", + ({ adAccountId }) => ({ + adAccountId, + }), + ], + }, + }, + async run() { + const { + app, + adAccountId, + leadFormId, + } = this; + + const { versionId: leadFormVersionId } = await app.getLeadForm({ + leadFormId, + }); + + const { elements } = await app.searchLeadFormResponses({ + params: { + q: "owner", + owner: `(sponsoredAccount:${app.getSponsoredAccountUrn(adAccountId)})`, + leadType: "(leadType:SPONSORED)", + limitedToTestLeads: false, + versionedLeadGenFormUrn: app.getVersionedLeadGenFormUrn(leadFormId, leadFormVersionId), + }, + }); + + elements + .reverse() + .forEach((event) => { + this.$emit(event, { + id: event.id, + summary: `New Lead Gen: ${event.id}`, + ts: event.submittedAt, + }); + }); + }, + sampleEmit, +}; diff --git a/components/linkedin_ads/sources/new-lead-gen-form-created/test-event.mjs b/components/linkedin_ads/sources/new-lead-gen-form-created/test-event.mjs new file mode 100644 index 0000000000000..265851b94f1f2 --- /dev/null +++ b/components/linkedin_ads/sources/new-lead-gen-form-created/test-event.mjs @@ -0,0 +1,48 @@ +export default { + "owner": { + "sponsoredAccount": "urn:li:sponsoredAccount:1232131" + }, + "submitter": "urn:li:person:0", + "leadType": "SPONSORED", + "leadMetadata": { + "sponsoredLeadMetadata": { + "campaign": "urn:li:sponsoredCampaign:32131231" + } + }, + "versionedLeadGenFormUrn": "urn:li:versionedLeadGenForm:(urn:li:leadGenForm:3213,1)", + "id": "52dcb737-26f9-4af3-b1fc-935b3e93f838-4", + "submittedAt": 1706741502694, + "testLead": true, + "associatedEntity": { + "associatedCreative": "urn:li:sponsoredCreative:369799186" + }, + "formResponse": { + "consentResponses": [], + "answers": [ + { + "answerDetails": { + "textQuestionAnswer": { + "answer": "test1@gmail.com" + } + }, + "questionId": 20502584796 + }, + { + "answerDetails": { + "textQuestionAnswer": { + "answer": "Test 1" + } + }, + "questionId": 20502643516 + }, + { + "answerDetails": { + "textQuestionAnswer": { + "answer": "Test 1" + } + }, + "questionId": 20502667260 + } + ] + } +}; diff --git a/components/linkish/README.md b/components/linkish/README.md index 2d3550081eefa..3718b07f2c41a 100644 --- a/components/linkish/README.md +++ b/components/linkish/README.md @@ -1,12 +1,11 @@ # Overview -Linkish is a versatile API that enables developers to create a wide variety of -applications. Some examples of what can be built using Linkish include: - -- Social networking applications -- Content sharing websites -- File sharing and storage applications -- E-commerce platforms -- Application and website security solutions -- Developers can also use Linkish to create entirely new types of applications - not yet imagined! +Linkish is a versatile API that enhances the way you handle web links. It allows you to organize, preview, and optimize web links with features such as URL shortening, web page metadata retrieval, and link previews with screenshots. With Pipedream, you can create powerful automations by connecting Linkish to other services to streamline content sharing, web research, and digital marketing workflows. + +# Example Use Cases + +- **Content Sharing Automation**: Trigger a Pipedream workflow whenever you publish a new blog post or article. Use Linkish to shorten the URL and fetch metadata, then automatically distribute the link with a preview and description to multiple social media platforms using apps like Twitter, Facebook, and LinkedIn. + +- **Digital Marketing Campaign Tracking**: Set up a workflow that creates shortened links with Linkish for different marketing campaigns. Integrate with Google Sheets to log each unique link along with campaign details. Use Pipedream to track link clicks by connecting to analytics services and update the corresponding Google Sheets rows with real-time performance data. + +- **Research & Data Collection**: Organize a workflow where, upon adding a new URL to a designated project management tool like Trello or Asana, Pipedream triggers Linkish to retrieve the metadata and screenshot of the page. Store the collected data in a centralized database or spreadsheet for a visual and structured research library. diff --git a/components/linkish/package.json b/components/linkish/package.json new file mode 100644 index 0000000000000..e35d0cb8680a2 --- /dev/null +++ b/components/linkish/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/linkish", + "version": "0.6.0", + "description": "Pipedream linkish Components", + "main": "linkish.app.mjs", + "keywords": [ + "pipedream", + "linkish" + ], + "homepage": "https://pipedream.com/apps/linkish", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/linkly/README.md b/components/linkly/README.md new file mode 100644 index 0000000000000..283aa5f9e6e0e --- /dev/null +++ b/components/linkly/README.md @@ -0,0 +1,11 @@ +# Overview + +The Linkly API enables you to create and manage short links, track clicks, and analyze the performance of your links in real-time. It's a tool for marketers, developers, and content creators who need to understand user engagement and optimize their online presence. By using Linkly API within Pipedream, you can integrate URL shortening and tracking capabilities into your workflows, allowing for automated link management and data collection. You can build automated processes that trigger on new link creation, analyze click data, generate reports, or sync with other marketing and analytics services to enhance your strategies. + +# Example Use Cases + +- **Automated Link Creation for Social Media Posts**: Use the Linkly API to automatically create short links for new content as soon as it's published. Connect Linkly to a social media platform like Twitter using Pipedream, so that each new blog post, product, or piece of content you publish is accompanied by a trackable Linkly short link when shared on your social profiles. + +- **Click Data Aggregation and Analysis**: Leverage Pipedream to collect click data from Linkly in real-time. Integrate this data with Google Sheets or a database like PostgreSQL to analyze the performance of various links. Use this analysis to adjust marketing strategies and better understand your audience's preferences and behaviors. + +- **Workflow Optimization for Affiliate Marketing**: Enhance your affiliate marketing by creating Linkly short links for affiliate URLs and track their performance. With Pipedream, you can connect Linkly to email marketing services like Mailchimp, automatically embedding trackable links in newsletters and tracking which products resonate most with your subscribers. diff --git a/components/linkup/README.md b/components/linkup/README.md new file mode 100644 index 0000000000000..690952f35b9ed --- /dev/null +++ b/components/linkup/README.md @@ -0,0 +1,12 @@ +# Overview +The Linkup API allows you to access and retrieve high-quality, real-time content from the internet for various use cases, including search, trend analysis, and contextual data gathering. By integrating the Linkup API with Pipedream, you can automate workflows that leverage Linkup's powerful search capabilities, enabling efficient data retrieval, content processing, and integration with other tools and platforms. + +# Example Use Cases + +- **Automated Content Research and Reporting:** Create a workflow that uses the Linkup API to perform automated searches based on specific queries. The results can be processed and stored in Google Sheets or a database for further analysis. This is particularly useful for market research, trend analysis, or competitive intelligence. + +- **Slack Notifications for Trending Topics:** Use Pipedream to set up a workflow that triggers regular searches on trending topics using the Linkup API. The results can then be sent to a Slack channel, keeping your team informed about the latest developments in your industry. + +- **Dynamic Content Generation for Marketing:** Integrate Linkup with a CMS or email marketing platform. Automatically fetch relevant content or insights from the Linkup API and embed them into newsletters, blog posts, or social media updates, ensuring fresh and engaging content for your audience. + +- **Enhanced Customer Support Knowledge Base:** Use the Linkup API to fetch and update relevant, real-time information from trusted sources to enhance your customer support responses or update your knowledge base dynamically. \ No newline at end of file diff --git a/components/linkup/actions/search/search.mjs b/components/linkup/actions/search/search.mjs new file mode 100644 index 0000000000000..1c57ef0288954 --- /dev/null +++ b/components/linkup/actions/search/search.mjs @@ -0,0 +1,111 @@ +import app from "../../linkup.app.mjs"; + +export default { + name: "Linkup Search", + description: "Search and retrieve insights using the Linkup API. [See the documentation](https://docs.linkup.so/pages/api-reference/endpoint/post-search)", + key: "linkup-search", + version: "0.0.1", + type: "action", + props: { + app, + query: { + type: "string", + label: "Query", + description: "The search query for Linkup.", + }, + depth: { + type: "string", + label: "Search Depth", + description: "Defines the precision of the search. `standard` returns results quickly; `deep` takes longer but yields more complete results.", + options: [ + "standard", + "deep", + ], + }, + outputType: { + type: "string", + label: "Output Type", + description: "The type of output you want to get. Use `structured` for a custom-formatted response defined by `structuredOutputSchema`", + options: [ + { + value: "sourcedAnswer", + label: "Natural language answer and its sources", + }, + { + value: "searchResults", + label: "Raw context", + }, + { + value: "structured", + label: "Json format of the response", + }, + ], + reloadProps: true, + }, + structuredOutputSchema: { + type: "string", + label: "Structured Output Schema", + description: "Schema for structured output (only applicable if Output Type is 'structured'). Provide a JSON schema (as a string) representing the desired response format.", + optional: true, + hidden: true, + }, + includeImages: { + type: "boolean", + label: "Include Images", + description: "Defines whether the API should include images in its results", + optional: true, + }, + }, + additionalProps(props) { + if (this.outputType === "structured") { + props.structuredOutputSchema.optional = false; + props.structuredOutputSchema.hidden = false; + props.structuredOutputSchema.default = `{ + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "year": { + "type": "number" + } + }, + "required": [ + "name", + "year" + ], + "additionalProperties": false + } + } + }, + "required": [ + "results" + ], + "additionalProperties": false + }`; + } + return {}; + }, + async run({ $ }) { + try { + const response = await this.app.search({ + query: this.query, + depth: this.depth, + outputType: this.outputType, + structuredOutputSchema: + this.structuredOutputSchema && JSON.parse(this.structuredOutputSchema), + includeImages: this.includeImages, + }); + $.export("$summary", "Successfully completed search query"); + return response; + } catch (error) { + console.error("Error calling Linkup API:", error); + throw new Error(`Failed to fetch data from Linkup API: ${error.message}`); + } + }, +}; diff --git a/components/linkup/linkup.app.mjs b/components/linkup/linkup.app.mjs new file mode 100644 index 0000000000000..5b009adf0f11f --- /dev/null +++ b/components/linkup/linkup.app.mjs @@ -0,0 +1,18 @@ +import { LinkupClient } from "linkup-sdk"; + +export default { + type: "app", + app: "linkup", + propDefinitions: {}, + methods: { + _getClient() { + return new LinkupClient({ + apiKey: this.$auth.api_key, + }); + }, + search(params) { + const client = this._getClient(); + return client.search(params); + }, + }, +}; diff --git a/components/linkup/package.json b/components/linkup/package.json new file mode 100644 index 0000000000000..52e05175f933a --- /dev/null +++ b/components/linkup/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/linkup", + "version": "0.1.0", + "description": "Pipedream Linkup Components", + "main": "linkup.app.mjs", + "keywords": [ + "pipedream", + "linkup" + ], + "homepage": "https://pipedream.com/apps/linkup", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "linkup-sdk": "^1.0.3" + } +} diff --git a/components/linode/package.json b/components/linode/package.json new file mode 100644 index 0000000000000..5de03aa4a0241 --- /dev/null +++ b/components/linode/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/linode", + "version": "0.6.0", + "description": "Pipedream linode Components", + "main": "linode.app.mjs", + "keywords": [ + "pipedream", + "linode" + ], + "homepage": "https://pipedream.com/apps/linode", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/linqs_cc/README.md b/components/linqs_cc/README.md index 33c3fafcc7571..c593c851367dd 100644 --- a/components/linqs_cc/README.md +++ b/components/linqs_cc/README.md @@ -1,9 +1,11 @@ # Overview -With the LINQs API, you can build a variety of applications that can help you -work with data more efficiently. Here are just a few examples: +Linqs API provides a gateway to create and manage smart, digital business cards that can be shared effortlessly. With Linqs and Pipedream, you can build automation that streamlines networking and data collection. Automate the creation of digital cards for new employees, sync contact information with CRM platforms, or trigger custom actions when someone scans a card—options are diverse and tailored for modern, seamless connection-building. -- An application that can help you query data more efficiently -- An application that can help you update data more efficiently -- An application that can help you delete data more efficiently -- An application that can help you manage data more efficiently +# Example Use Cases + +- **Automated Digital Card Creation for New Employees**: When a new employee is added to your HR system (like BambooHR), trigger a Pipedream workflow that automatically creates a Linqs digital business card for them. The card's details can then be sent to the employee via email using an app like SendGrid, making onboarding smooth and tech-savvy. + +- **CRM Integration and Contact Syncing**: Sync scanned Linqs card details to a CRM (such as Salesforce) using Pipedream. Each time a card is scanned, the workflow is triggered, creating or updating the contact's information in your CRM, ensuring your sales team always has up-to-date leads and contact data. + +- **Custom Notifications for Card Interactions**: Set up a Pipedream workflow that sends custom notifications via apps like Slack or Twilio whenever someone scans a Linqs card. This workflow could notify a sales rep instantly when a prospective client scans their card, enabling immediate follow-up and enhancing engagement opportunities. diff --git a/components/liondesk/README.md b/components/liondesk/README.md new file mode 100644 index 0000000000000..161ca0acc9cea --- /dev/null +++ b/components/liondesk/README.md @@ -0,0 +1,11 @@ +# Overview + +The Liondesk API enables you to automate and integrate your CRM tasks, streamlining your customer relationship workflows. With this API on Pipedream, you can craft workflows that trigger actions based on events, manage contacts, send messages, and much more. Pipedream's serverless platform simplifies the process, allowing you to connect Liondesk with hundreds of other apps to automate complex tasks without writing extensive code. + +# Example Use Cases + +- **Sync New Contacts to Google Sheets**: When a new contact is added to Liondesk, automatically add their details to a Google Sheets spreadsheet. This is perfect for backing up contacts or for further data analysis and sharing with team members who prefer to work within Google Sheets. + +- **Distribute Leads to Slack Channels**: Distribute incoming Liondesk leads to specific Slack channels. For instance, based on the lead's interest, a workflow can auto-post their info to a "Homes for Sale" or "Rentals" Slack channel, ensuring the right team follows up promptly. + +- **Automate Email Follow-Ups**: Set up a workflow that auto-sends personalized follow-up emails via Liondesk when a contact reaches a certain stage in your pipeline or after a set period without engagement. This helps maintain consistent communication without manual intervention. diff --git a/components/listclean/README.md b/components/listclean/README.md new file mode 100644 index 0000000000000..7c9bfc9ca98d0 --- /dev/null +++ b/components/listclean/README.md @@ -0,0 +1,11 @@ +# Overview + +Listclean API provides a straightforward way to validate and clean your email lists. When you tap into this tool via Pipedream, you can automate the process of checking email addresses for deliverability, domain validity, and more. This ensures that you're only sending emails to addresses that are active and valid, improving your campaign's success rate and preserving your sender reputation. + +# Example Use Cases + +- **Validate Emails on User Sign-up**: When a new user signs up on your platform, you can automatically validate their email address with Listclean to ensure it's a legitimate address. This can be paired with a user management platform like Auth0 to streamline account creation. + +- **Clean Marketing Lists Before Campaigns**: Before launching an email marketing campaign, use Listclean in a Pipedream workflow to scrub your email list. This can be effectively combined with marketing tools like Mailchimp or SendGrid to ensure high deliverability and engagement rates. + +- **Periodic Cleaning of Database Emails**: Set up a recurring Pipedream workflow that periodically validates all email addresses stored in your database. This can be integrated with database services like PostgreSQL or MySQL, maintaining the hygiene of your contact data over time. diff --git a/components/listen_notes/.gitignore b/components/listen_notes/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/listen_notes/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/listen_notes/README.md b/components/listen_notes/README.md index 55df314055e7f..35a2214e00b6b 100644 --- a/components/listen_notes/README.md +++ b/components/listen_notes/README.md @@ -1,9 +1,11 @@ # Overview -With the Listen Notes API, you can build applications that: +The Listen Notes API allows you to tap into a vast podcast database, offering search capabilities, detailed information on individual episodes, and curated lists of podcasts. By leveraging the API with Pipedream, you can automate podcast-related workflows, such as monitoring new episodes from favorite shows, analyzing podcast trends, or integrating with other services for content distribution, notifications, or data aggregation. -- Get podcast descriptions and metadata -- Search for podcasts by keyword -- Fetch individual episodes -- Recommend new podcasts to users -- And more! +# Example Use Cases + +- **Podcast Update Notifications**: Create a Pipedream workflow that checks for new episodes of selected podcasts and sends an email or a Slack message whenever a new episode is released. This can keep a team updated on the latest industry news or trends as soon as they're available. + +- **Content Aggregation for Curated Feeds**: Build a Pipedream workflow that uses the Listen Notes API to fetch the latest episodes from a list of podcasts and compiles them into a curated RSS feed or a newsletter. This can be automatically distributed to subscribers via email or uploaded to a website, keeping listeners engaged with hand-picked content. + +- **Analytics and Reporting for Podcast Performance**: Develop a Pipedream workflow that retrieves podcast analytics from Listen Notes and integrates them with Google Sheets or a database. This can be used to track performance metrics and generate regular reports for marketing or content strategy purposes. It can also trigger other workflows based on the performance data, like social media posts or advertising campaigns. diff --git a/components/listen_notes/actions/full-search/full-search.mjs b/components/listen_notes/actions/full-search/full-search.mjs new file mode 100644 index 0000000000000..da79072994096 --- /dev/null +++ b/components/listen_notes/actions/full-search/full-search.mjs @@ -0,0 +1,56 @@ +import app from "../../listen_notes.app.mjs"; + +export default { + key: "listen_notes-full-search", + name: "Full Search", + description: "Full-text search on episodes, podcasts, or curated lists of podcasts. [See the documentation](https://www.listennotes.com/api/docs/#get-api-v2-search)", + version: "0.0.1", + type: "action", + props: { + app, + q: { + propDefinition: [ + app, + "q", + ], + }, + sortByDate: { + propDefinition: [ + app, + "sortByDate", + ], + }, + type: { + propDefinition: [ + app, + "type", + ], + }, + language: { + propDefinition: [ + app, + "language", + ], + }, + offset: { + propDefinition: [ + app, + "offset", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.fullSearch({ + $, + params: { + q: this.q, + sort_by_date: this.sortByDate, + type: this.type, + language: this.language, + }, + }); + $.export("$summary", `Successfully retrieved ${response.results.length} results`); + return response; + }, +}; diff --git a/components/listen_notes/actions/get-episode-details/get-episode-details.mjs b/components/listen_notes/actions/get-episode-details/get-episode-details.mjs new file mode 100644 index 0000000000000..bdcd2ebfea74d --- /dev/null +++ b/components/listen_notes/actions/get-episode-details/get-episode-details.mjs @@ -0,0 +1,60 @@ +import app from "../../listen_notes.app.mjs"; + +export default { + key: "listen_notes-get-episode-details", + name: "Get Episode Details", + description: "Get the details of the selected episode. [See the documentation](https://www.listennotes.com/api/docs/#get-api-v2-episodes-id)", + version: "0.0.1", + type: "action", + props: { + app, + q: { + propDefinition: [ + app, + "q", + ], + optional: true, + }, + language: { + propDefinition: [ + app, + "language", + ], + }, + offset: { + propDefinition: [ + app, + "offset", + ], + }, + id: { + propDefinition: [ + app, + "id", + (c) => ({ + q: c.q, + language: c.language, + offset: c.offset, + }), + ], + }, + showTranscript: { + propDefinition: [ + app, + "showTranscript", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.getEpisodeDetails({ + $, + id: this.id, + params: { + show_transcript: this.showTranscript, + }, + }); + $.export("$summary", `Successfully retrieved details for the episode '${response.title}'`); + return response; + }, +}; diff --git a/components/listen_notes/actions/get-podcast-details/get-podcast-details.mjs b/components/listen_notes/actions/get-podcast-details/get-podcast-details.mjs new file mode 100644 index 0000000000000..75b3209c2102a --- /dev/null +++ b/components/listen_notes/actions/get-podcast-details/get-podcast-details.mjs @@ -0,0 +1,68 @@ +import app from "../../listen_notes.app.mjs"; + +export default { + key: "listen_notes-get-podcast-details", + name: "Get Podcast Details", + description: "Get the details of the selected podcast. [See the documentation](https://www.listennotes.com/api/docs/#get-api-v2-podcasts-id)", + version: "0.0.1", + type: "action", + props: { + app, + q: { + propDefinition: [ + app, + "q", + ], + optional: true, + }, + language: { + propDefinition: [ + app, + "language", + ], + }, + offset: { + propDefinition: [ + app, + "offset", + ], + }, + id: { + propDefinition: [ + app, + "id", + (c) => ({ + q: c.q, + type: "podcast", + language: c.language, + offset: c.offset, + }), + ], + }, + nextEpisodePubDate: { + propDefinition: [ + app, + "nextEpisodePubDate", + ], + }, + sort: { + propDefinition: [ + app, + "sort", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.getPodcastDetails({ + $, + id: this.id, + params: { + next_episode_pub_date: this.nextEpisodePubDate, + sort: this.sort, + }, + }); + $.export("$summary", `Successfully retrieved details for the podcast '${response.title}'`); + return response; + }, +}; diff --git a/components/listen_notes/app/listen_notes.app.ts b/components/listen_notes/app/listen_notes.app.ts deleted file mode 100644 index 75cbf47d7cdbd..0000000000000 --- a/components/listen_notes/app/listen_notes.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "listen_notes", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); diff --git a/components/listen_notes/common/constants.mjs b/components/listen_notes/common/constants.mjs new file mode 100644 index 0000000000000..f253ee5a5475e --- /dev/null +++ b/components/listen_notes/common/constants.mjs @@ -0,0 +1,37 @@ +export default { + SEARCH_SORTING_OPTIONS: [ + { + value: "0", + label: "Sort by Relevance", + }, + { + value: "1", + label: "Sort by Date", + }, + ], + EPISODES_SORTING_OPTIONS: [ + { + value: "recent_first", + label: "Recent First", + }, + { + value: "oldest_first", + label: "Oldest First", + }, + ], + TRANSCRIPT_OPTIONS: [ + { + value: "0", + label: "Don't include transcript", + }, + { + value: "1", + label: "Include transcript", + }, + ], + TYPE_OPTIONS: [ + "episode", + "podcast", + "curated", + ], +}; diff --git a/components/listen_notes/listen_notes.app.mjs b/components/listen_notes/listen_notes.app.mjs new file mode 100644 index 0000000000000..3471f52bd0e2f --- /dev/null +++ b/components/listen_notes/listen_notes.app.mjs @@ -0,0 +1,164 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + type: "app", + app: "listen_notes", + propDefinitions: { + id: { + type: "string", + label: "Podcast or Episode ID", + description: "The ID of the podcast or episode", + async options({ + q, type, language, offset, + }) { + const response = await this.listPodcasts({ + q, + type, + language, + offset, + }); + const ids = response.results; + return ids.map(({ + title_original, id, + }) => ({ + value: id, + label: title_original, + })); + }, + }, + q: { + type: "string", + label: "Query", + description: "Search term, e.g., person, place, topic... You can use double quotes to do verbatim match", + }, + offset: { + type: "string", + label: "Offset", + description: "The offset parameter is used to paginate through search results", + optional: true, + }, + sortByDate: { + type: "string", + label: "Sort By Date", + description: "Sort by date. If 0, then sort by relevance. If 1, then sort by date", + options: constants.SEARCH_SORTING_OPTIONS, + optional: true, + }, + type: { + type: "string", + label: "Type", + description: "What type of contents do you want to search for?", + options: constants.TYPE_OPTIONS, + optional: true, + }, + showTranscript: { + type: "string", + label: "Show Transcript", + description: "To include the transcript of this episode or not?", + options: constants.TRANSCRIPT_OPTIONS, + optional: true, + }, + nextEpisodePubDate: { + type: "string", + label: "Next Episode Pub Date", + description: "For episodes pagination. It's the value of `next_episode_pub_date` from the response of last request. It is an Epoch Unix timestamp in milliseconds, i.e.: `1479154463000`", + optional: true, + }, + sort: { + type: "string", + label: "Sort", + description: "What type of contents do you want to search for?", + options: constants.EPISODES_SORTING_OPTIONS, + optional: true, + }, + language: { + type: "string", + label: "Language", + description: "Limit search results to a specific language. If not specified, it'll be any language", + async options() { + const response = await this.getLanguages(); + const languages = response.languages; + return languages.map((language) => ({ + value: language, + label: language, + })); + }, + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://listen-api.listennotes.com/api/v2"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "X-ListenAPI-Key": `${this.$auth.api_key}`, + }, + }); + }, + async fullSearch(args = {}) { + return this._makeRequest({ + path: "/search", + ...args, + }); + }, + async getPodcastDetails({ + id, + ...args + }) { + return this._makeRequest({ + path: `/podcasts/${id}`, + ...args, + }); + }, + async getEpisodeDetails({ + id, + ...args + }) { + return this._makeRequest({ + path: `/episodes/${id}`, + ...args, + }); + }, + async getLanguages(args = {}) { + return this._makeRequest({ + path: "/languages", + ...args, + }); + }, + async listPodcasts({ + q, + language, + type, + offset, + ...args + }) { + if (!q) { + throw new ConfigurationError("You need to inform a query to list the IDs. Alternatively you can directly inform an ID unsing the custom expression function."); + } + + return this._makeRequest({ + path: "/search", + params: { + q: q, + type: type, + language: language, + offset: offset, + }, + ...args, + }); + }, + }, +}; diff --git a/components/listen_notes/package.json b/components/listen_notes/package.json index 98cda4dae11ac..cf135b7b6549b 100644 --- a/components/listen_notes/package.json +++ b/components/listen_notes/package.json @@ -1,13 +1,12 @@ { "name": "@pipedream/listen_notes", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream Listen Notes Components", - "main": "dist/app/listen_notes.app.mjs", + "main": "listen_notes.app.mjs", "keywords": [ "pipedream", "listen_notes" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/listen_notes", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { diff --git a/components/listmonk/README.md b/components/listmonk/README.md new file mode 100644 index 0000000000000..ea172f4e51453 --- /dev/null +++ b/components/listmonk/README.md @@ -0,0 +1,11 @@ +# Overview + +The listmonk API enables you to interact programmatically with the listmonk platform, which specializes in managing mailing lists and sending out newsletters. Using this API, you can automate subscriber management, campaign dispatching, and performance tracking. When you integrate listmonk with Pipedream, you can create powerful workflows that react to events from other apps, process data, and use that data to trigger actions within listmonk. + +# Example Use Cases + +- **Automated Subscriber Syncing**: Sync new user registrations from your app (like a Shopify store) with listmonk. Whenever a new user signs up, a Pipedream workflow can capture this event and automatically add the user's details to a specified mailing list in listmonk. + +- **Dynamic Campaign Triggers**: React to user behavior from your website tracked by Google Analytics. For instance, when a user visits a specific page or completes an action, Pipedream can trigger a workflow that sends a targeted email campaign through listmonk to that specific user or segment. + +- **Survey Response Follow-Ups**: After a user completes a survey via Typeform, Pipedream can process the response and based on certain criteria, like a low satisfaction score, trigger a personalized follow-up email campaign through listmonk to address any concerns or feedback. diff --git a/components/little_green_light/README.md b/components/little_green_light/README.md new file mode 100644 index 0000000000000..40abb84cb906c --- /dev/null +++ b/components/little_green_light/README.md @@ -0,0 +1,13 @@ +# Overview + +The Little Green Light API provides access to a robust donor management platform, allowing for the creation, retrieval, update, and deletion of donor data. With the API, you can automate tasks related to fundraising, event management, and donor engagement. On Pipedream, you can harness this API to construct serverless workflows that trigger on various events, integrate with other apps, and automate repetitive tasks without managing infrastructure. + +Note: Little Green Light strongly recommends against connecting your LGL account with any AI services like ChatGPT, as that could lead to unintended use of personal data. + +# Example Use Cases + +- **Sync Donor Information to Google Sheets**: Whenever a new donor is added in Little Green Light, automatically add their details to a Google Sheet. This workflow can help maintain an up-to-date backup of donor data and facilitate easy sharing and reporting. + +- **Automate Thank You Emails**: Send a personalized thank you email via SendGrid or a similar email service when a new donation is recorded in Little Green Light. This workflow ensures donors receive immediate appreciation, enhancing their engagement with your organization. + +- **Slack Notifications for Major Donations**: Set up a workflow that sends a notification to a designated Slack channel when a donation above a certain threshold is received. This allows your team to quickly acknowledge and discuss significant contributions. diff --git a/components/liveagent/README.md b/components/liveagent/README.md new file mode 100644 index 0000000000000..3da6468fc57f3 --- /dev/null +++ b/components/liveagent/README.md @@ -0,0 +1,11 @@ +# Overview + +The LiveAgent API provides access to a feature-rich helpdesk platform, allowing you to automate customer support tasks, synchronize data with other services, and streamline communication channels. With Pipedream, you can tap into the LiveAgent API to create custom workflows that enhance support operations, trigger actions based on customer interactions, and connect with a myriad of other apps to enrich the support ecosystem. Pipedream's serverless platform makes it simple to set up these automations with minimal coding, using pre-built actions or scripting your own. + +# Example Use Cases + +- **Ticket Management Automation**: Automatically create tickets in LiveAgent when customer inquiries come in through other channels. For example, connect LiveAgent to a Slack channel where customer messages are flagged for support; Pipedream can listen for these flags and create corresponding tickets in LiveAgent, ensuring that no request is missed. + +- **Synchronize Customer Data**: Keep your customer data in sync across different platforms. With the LiveAgent API on Pipedream, you can design a workflow that updates customer records in a CRM like Salesforce whenever ticket statuses change in LiveAgent, providing a cohesive view of customer interactions and support history. + +- **Customer Feedback Analysis**: Analyze customer feedback and sentiment by connecting LiveAgent to an AI service like Google's Natural Language API. After a ticket is resolved, use Pipedream to send the closing messages to the AI service for sentiment analysis, and then log this data in a data store or a tool like Google Sheets for trend analysis and reporting. diff --git a/components/livechat/README.md b/components/livechat/README.md new file mode 100644 index 0000000000000..cc6d2033e5396 --- /dev/null +++ b/components/livechat/README.md @@ -0,0 +1,11 @@ +# Overview + +LiveChat API unlocks the capability to integrate your customer service system with powerful automation and data processing. Through Pipedream, you can harness this API to create workflows that trigger actions within LiveChat, synchronize data with other services, and analyze customer interactions to improve support experiences. Pipedream's serverless platform allows you to connect LiveChat with hundreds of other apps with minimal setup, enabling your customer support to be as responsive and informed as possible. + +# Example Use Cases + +- **Sync LiveChat Conversations to Google Sheets**: Automatically archive every conversation from LiveChat into a Google Sheets document. This workflow can help with record-keeping, data analysis, and reporting, ensuring that every interaction is logged and can be reviewed or mined for insights at a later time. + +- **Customer Support Ticket Creation**: Create a workflow that listens for specific keywords or phrases in a LiveChat conversation and uses this to trigger the creation of a support ticket in a system like Jira or Zendesk. This can help streamline the process of escalating issues and ensuring that customer inquiries don't go unnoticed. + +- **CRM Integration**: Sync LiveChat conversations and customer data with a CRM like Salesforce or HubSpot. You can set up a workflow where new chats automatically create or update customer profiles, keeping your customer information current and making sure that your team has access to the most relevant data during their conversations. diff --git a/components/livechat/package.json b/components/livechat/package.json index f803b2f84d5dd..9426edda0ae0f 100644 --- a/components/livechat/package.json +++ b/components/livechat/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/livekit/actions/create-ingress-from-url/create-ingress-from-url.mjs b/components/livekit/actions/create-ingress-from-url/create-ingress-from-url.mjs new file mode 100644 index 0000000000000..c8b078b6d3a9a --- /dev/null +++ b/components/livekit/actions/create-ingress-from-url/create-ingress-from-url.mjs @@ -0,0 +1,87 @@ +import { IngressInput } from "livekit-server-sdk"; +import app from "../../livekit.app.mjs"; + +export default { + key: "livekit-create-ingress-from-url", + name: "Create Ingress From URL", + description: "Create a new ingress from url in LiveKit. [See the documentation](https://docs.livekit.io/home/ingress/overview/#url-input-example).", + version: "0.0.1", + type: "action", + props: { + app, + name: { + type: "string", + label: "Ingress Name", + description: "The name of the ingress", + optional: true, + }, + roomName: { + description: "The name of the room to send media to", + propDefinition: [ + app, + "room", + ], + }, + participantIdentity: { + type: "string", + label: "Participant Identity", + description: "Unique identity of the participant", + }, + participantName: { + type: "string", + label: "Participant Name", + description: "Participant display name", + optional: true, + }, + participantMetadata: { + type: "string", + label: "Participant Metadata", + description: "Metadata to attach to the participant", + optional: true, + }, + bypassTranscoding: { + type: "boolean", + label: "Bypass Transcoding", + description: "Whether to skip transcoding and forward the input media directly. Only supported by WHIP", + optional: true, + }, + enableTranscoding: { + type: "boolean", + label: "Enable Transcoding", + description: "Whether to enable transcoding or forward the input media directly. Transcoding is required for all input types except WHIP. For WHIP, the default is to not transcode.", + optional: true, + }, + url: { + type: "string", + label: "URL", + description: "URL of the media to pull for ingresses of type URL", + }, + }, + async run({ $ }) { + const { + app, + name, + roomName, + participantIdentity, + participantName, + participantMetadata, + bypassTranscoding, + enableTranscoding, + url, + } = this; + + const response = await app.createIngress({ + inputType: IngressInput.URL_INPUT, + name, + roomName, + participantIdentity, + participantName, + participantMetadata, + bypassTranscoding, + enableTranscoding, + url, + }); + $.export("$summary", `Successfully created ingress with ID \`${response.ingressId}\`.`); + return response; + }, +}; diff --git a/components/livekit/actions/create-room/create-room.mjs b/components/livekit/actions/create-room/create-room.mjs new file mode 100644 index 0000000000000..26ee1164f629d --- /dev/null +++ b/components/livekit/actions/create-room/create-room.mjs @@ -0,0 +1,85 @@ +import app from "../../livekit.app.mjs"; + +export default { + key: "livekit-create-room", + name: "Create Room", + description: "Create a new room in LiveKit. [See the documentation](https://docs.livekit.io/home/server/managing-rooms/#create-a-room).", + version: "0.0.1", + type: "action", + props: { + app, + name: { + type: "string", + label: "Room Name", + description: "The name of the room", + }, + emptyTimeout: { + type: "integer", + label: "Empty Timeout", + description: "Number of seconds to keep the room open before any participant joins", + optional: true, + }, + departureTimeout: { + type: "integer", + label: "Departure Timeout", + description: "Number of seconds to keep the room open after the last participant leaves", + optional: true, + }, + maxParticipants: { + type: "integer", + label: "Max Participants", + description: "Limit to the number of participants in a room at a time", + optional: true, + }, + metadata: { + type: "string", + label: "Metadata", + description: "Initial room metadata", + optional: true, + }, + minPlayoutDelay: { + type: "integer", + label: "Min Playout Delay", + description: "Minimum playout delay in milliseconds", + optional: true, + }, + maxPlayoutDelay: { + type: "integer", + label: "Max Playout Delay", + description: "Maximum playout delay in milliseconds", + optional: true, + }, + syncStreams: { + type: "boolean", + label: "Sync Streams", + description: "Improves A/V sync when min_playout_delay set to a value larger than 200ms. It will disables transceiver re-use -- this option is not recommended for rooms with frequent subscription changes", + optional: true, + }, + }, + async run({ $ }) { + const { + app, + name, + emptyTimeout, + departureTimeout, + maxParticipants, + metadata, + minPlayoutDelay, + maxPlayoutDelay, + syncStreams, + } = this; + + const response = await app.createRoom({ + name, + emptyTimeout, + departureTimeout, + maxParticipants, + metadata, + minPlayoutDelay, + maxPlayoutDelay, + syncStreams, + }); + $.export("$summary", `Successfully created room with SID \`${response.sid}\`.`); + return response; + }, +}; diff --git a/components/livekit/actions/delete-room/delete-room.mjs b/components/livekit/actions/delete-room/delete-room.mjs new file mode 100644 index 0000000000000..28c9e6604af30 --- /dev/null +++ b/components/livekit/actions/delete-room/delete-room.mjs @@ -0,0 +1,32 @@ +import app from "../../livekit.app.mjs"; + +export default { + key: "livekit-delete-room", + name: "Delete Room", + description: "Delete a room in LiveKit. [See the documentation](https://docs.livekit.io/home/server/managing-rooms/#delete-a-room)", + version: "0.0.1", + type: "action", + props: { + app, + room: { + propDefinition: [ + app, + "room", + ], + }, + }, + async run({ $ }) { + const { + app, + room, + } = this; + + await app.deleteRoom(room); + + $.export("$summary", "Successfully deleted room."); + + return { + success: true, + }; + }, +}; diff --git a/components/livekit/actions/list-rooms/list-rooms.mjs b/components/livekit/actions/list-rooms/list-rooms.mjs new file mode 100644 index 0000000000000..4a36acc58fe48 --- /dev/null +++ b/components/livekit/actions/list-rooms/list-rooms.mjs @@ -0,0 +1,17 @@ +import app from "../../livekit.app.mjs"; + +export default { + key: "livekit-list-rooms", + name: "List Rooms", + description: "List all rooms with LiveKit. [See the documentation](https://docs.livekit.io/home/server/managing-rooms/#list-rooms).", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const rooms = await this.app.listRooms(); + $.export("$summary", `Successfully listed \`${rooms.length}\` room(s).`); + return rooms; + }, +}; diff --git a/components/livekit/common/constants.mjs b/components/livekit/common/constants.mjs new file mode 100644 index 0000000000000..b777b760f5598 --- /dev/null +++ b/components/livekit/common/constants.mjs @@ -0,0 +1,7 @@ +const HTTPS_PREFIX = "https://"; +const HTTP_PREFIX = "http://"; + +export default { + HTTPS_PREFIX, + HTTP_PREFIX, +}; diff --git a/components/livekit/livekit.app.mjs b/components/livekit/livekit.app.mjs new file mode 100644 index 0000000000000..d0458075b1ceb --- /dev/null +++ b/components/livekit/livekit.app.mjs @@ -0,0 +1,62 @@ +import { + RoomServiceClient, + IngressClient, +} from "livekit-server-sdk"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "livekit", + propDefinitions: { + room: { + type: "string", + label: "Room Name", + description: "The name of the room", + async options() { + const rooms = await this.listRooms(); + return rooms.map(({ name }) => name); + }, + }, + }, + methods: { + getHost() { + const { project_url: projectUrl } = this.$auth; + + return projectUrl.startsWith(constants.HTTPS_PREFIX) + ? projectUrl + : projectUrl.startsWith(constants.HTTP_PREFIX) + ? projectUrl.replace(constants.HTTP_PREFIX, constants.HTTPS_PREFIX) + : `${constants.HTTPS_PREFIX}${projectUrl}`; + }, + getKeys() { + const { + api_key: apiKey, + secret_key: secretKey, + } = this.$auth; + return [ + apiKey, + secretKey, + ]; + }, + getRoomClient() { + return new RoomServiceClient(this.getHost(), ...this.getKeys()); + }, + getIngressClient() { + return new IngressClient(this.getHost(), ...this.getKeys()); + }, + createRoom(args) { + return this.getRoomClient().createRoom(args); + }, + listRooms(names) { + return this.getRoomClient().listRooms(names); + }, + deleteRoom(room) { + return this.getRoomClient().deleteRoom(room); + }, + createIngress({ + inputType, ...args + } = {}) { + return this.getIngressClient().createIngress(inputType, args); + }, + }, +}; diff --git a/components/livekit/package.json b/components/livekit/package.json new file mode 100644 index 0000000000000..8067eda5b705b --- /dev/null +++ b/components/livekit/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/livekit", + "version": "0.1.0", + "description": "Pipedream LiveKit Components", + "main": "livekit.app.mjs", + "keywords": [ + "pipedream", + "livekit" + ], + "homepage": "https://pipedream.com/apps/livekit", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "livekit-server-sdk": "^2.8.1" + } +} diff --git a/components/livesession/README.md b/components/livesession/README.md new file mode 100644 index 0000000000000..a90e9efeeae0f --- /dev/null +++ b/components/livesession/README.md @@ -0,0 +1,11 @@ +# Overview + +LiveSession is an app that allows you to understand user behavior through session replays and analytics. With its API, you can retrieve session data, manage users, and more. Integrating LiveSession with Pipedream lets you automate actions based on user interactions, compile analytics, or trigger workflows in other apps based on session insights. This can be immensely valuable for support teams, product managers, and UX designers seeking to improve user experience. + +# Example Use Cases + +- **Trigger Support Follow-Ups**: Automatically generate support tickets or follow-up emails when a user experiences an issue on your site. Connect LiveSession to helpdesk software like Zendesk on Pipedream, so that when a session replay shows a user encountering an error, a support ticket is created with the replay link. + +- **Aggregate UX Metrics**: Compile UX metrics by connecting LiveSession with data visualization tools such as Google Sheets or Data Studio. Set up a Pipedream workflow that fetches session replays tagged with specific issues, calculates the frequency of these issues, and pushes summarized data to a spreadsheet or dashboard for review. + +- **Personalize Marketing Campaigns**: Use session data to trigger personalized marketing campaigns. When a user demonstrates high engagement in a session, use Pipedream to send that data to a marketing platform like Mailchimp, creating a segment for highly engaged users and triggering a targeted email sequence. diff --git a/components/livestorm/README.md b/components/livestorm/README.md new file mode 100644 index 0000000000000..dcc4974dfcdfe --- /dev/null +++ b/components/livestorm/README.md @@ -0,0 +1,11 @@ +# Overview + +The Livestorm API opens up a world of possibilities for event management automation. With this API, you can harness data from your webinars and online events to drive engagement, personalize follow-ups, and streamline event operations. Integrating Livestorm with Pipedream allows you to construct workflows that can react to event triggers, sync data across platforms, and automate repetitive tasks. Whether you're looking to enhance participant engagement, capture leads in your CRM, or trigger personalized email campaigns post-event, Pipedream's serverless platform makes it easy to connect Livestorm with other apps and services. + +# Example Use Cases + +- **Automated Event Follow-Up Emails**: Craft personalized follow-up emails to attendees right after an event ends, using attendee data from Livestorm. Set up a workflow on Pipedream that triggers when an event concludes, fetching attendee details and sending customized emails via SendGrid or another email service. + +- **Sync Webinar Leads to CRM**: Whenever a new participant registers for a webinar, automatically add their information to your CRM. Pipedream can trigger a workflow on new registrations, pushing data to Salesforce, HubSpot, or another CRM platform, helping your sales team to act on fresh leads without delay. + +- **Post-Event Survey Distribution**: Distribute a post-event survey to collect feedback from participants. Set up a workflow that triggers at the end of an event to send out a survey link through Twilio SMS or via email, ensuring timely feedback that can be used to improve future events. diff --git a/components/liveswitch/actions/create-contact/create-contact.mjs b/components/liveswitch/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..b86efd667c144 --- /dev/null +++ b/components/liveswitch/actions/create-contact/create-contact.mjs @@ -0,0 +1,52 @@ +import app from "../../liveswitch.app.mjs"; + +export default { + key: "liveswitch-create-contact", + name: "Create Contact", + description: "Create a contact in LiveSwitch [See the documentation](https://developer.liveswitch.com/reference/post_v1-contacts)", + version: "0.0.1", + type: "action", + props: { + app, + phone: { + propDefinition: [ + app, + "phone", + ], + }, + firstName: { + propDefinition: [ + app, + "firstName", + ], + }, + lastName: { + propDefinition: [ + app, + "lastName", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.createContact({ + $, + data: { + phone: this.phone, + firstName: this.firstName, + lastName: this.lastName, + email: this.email, + }, + }); + + $.export("$summary", `Successfully created Contact with ID: ${response.id}`); + + return response; + }, +}; diff --git a/components/liveswitch/actions/create-conversation/create-conversation.mjs b/components/liveswitch/actions/create-conversation/create-conversation.mjs new file mode 100644 index 0000000000000..da0428947a4e7 --- /dev/null +++ b/components/liveswitch/actions/create-conversation/create-conversation.mjs @@ -0,0 +1,38 @@ +import app from "../../liveswitch.app.mjs"; + +export default { + key: "liveswitch-create-conversation", + name: "Create Conversation", + description: "Create a conversation in LiveSwitch [See the documentation](https://developer.liveswitch.com/reference/post_v1-conversations)", + version: "0.0.1", + type: "action", + props: { + app, + contactId: { + propDefinition: [ + app, + "contactId", + ], + }, + message: { + propDefinition: [ + app, + "message", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.createConversation({ + $, + data: { + contactId: this.contactId, + message: this.message, + }, + }); + + $.export("$summary", `Successfully created Conversation with ID: ${response.id}`); + + return response; + }, +}; diff --git a/components/liveswitch/actions/update-contact/update-contact.mjs b/components/liveswitch/actions/update-contact/update-contact.mjs new file mode 100644 index 0000000000000..593488339b62b --- /dev/null +++ b/components/liveswitch/actions/update-contact/update-contact.mjs @@ -0,0 +1,59 @@ +import app from "../../liveswitch.app.mjs"; + +export default { + key: "liveswitch-update-contact", + name: "Update Contact", + description: "Update a contact in LiveSwitch [See the documentation](https://developer.liveswitch.com/reference/post_v1-contacts)", + version: "0.0.1", + type: "action", + props: { + app, + contactId: { + propDefinition: [ + app, + "contactId", + ], + }, + phone: { + propDefinition: [ + app, + "phone", + ], + }, + firstName: { + propDefinition: [ + app, + "firstName", + ], + }, + lastName: { + propDefinition: [ + app, + "lastName", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.updateContact({ + $, + contactId: this.contactId, + data: { + phone: this.phone, + firstName: this.firstName, + lastName: this.lastName, + email: this.email, + }, + }); + + $.export("$summary", `Successfully updated Contact with ID: ${this.contactId}`); + + return response; + }, +}; diff --git a/components/liveswitch/liveswitch.app.mjs b/components/liveswitch/liveswitch.app.mjs new file mode 100644 index 0000000000000..f1dd1ce333ed6 --- /dev/null +++ b/components/liveswitch/liveswitch.app.mjs @@ -0,0 +1,104 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "liveswitch", + propDefinitions: { + firstName: { + type: "string", + label: "First Name", + description: "Contact's first name", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Contact's last name", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "Contact's email", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Contact's phone number, i.e.: `+1 407-982-1211`", + }, + message: { + type: "string", + label: "Message", + description: "The contents of the text message the user will receive", + }, + contactId: { + type: "string", + label: "Contact ID", + description: "Contact's ID", + async options() { + const response = await this.getContacts(); + return response.map(({ + id, firstName, lastName, + }) => ({ + value: id, + label: `${firstName} ${lastName}`, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://public-api.production.liveswitch.com/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + async createContact(args = {}) { + return this._makeRequest({ + path: "/contacts", + method: "post", + ...args, + }); + }, + async updateContact({ + contactId, ...args + }) { + return this._makeRequest({ + path: `/contacts/${contactId}`, + method: "put", + ...args, + }); + }, + async createConversation(args = {}) { + return this._makeRequest({ + path: "/conversations", + method: "post", + ...args, + }); + }, + async getContacts(args = {}) { + return this._makeRequest({ + path: "/contacts", + params: { + page: 1, + pageSize: 100, + }, + ...args, + }); + }, + }, +}; diff --git a/components/liveswitch/package.json b/components/liveswitch/package.json new file mode 100644 index 0000000000000..ec7cde46285d3 --- /dev/null +++ b/components/liveswitch/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/liveswitch", + "version": "0.1.0", + "description": "Pipedream LiveSwitch Components", + "main": "liveswitch.app.mjs", + "keywords": [ + "pipedream", + "liveswitch" + ], + "homepage": "https://pipedream.com/apps/liveswitch", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/llama_ai/README.md b/components/llama_ai/README.md new file mode 100644 index 0000000000000..e0b769f3f5b76 --- /dev/null +++ b/components/llama_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +The Llama AI API provides powerful machine learning capabilities, enabling users to harness advanced AI for image recognition, natural language processing, and predictive modeling. By leveraging this API on Pipedream, you can automate complex workflows that require AI-driven insights, enhancing data analysis and decision-making processes across various business applications. + +# Example Use Cases + +- **Customer Support Automation**: Automate the categorization and prioritization of incoming customer support tickets using Llama AI's NLP capabilities. Integrate with Zendesk to automatically tag and route tickets based on sentiment analysis and content categorization. + +- **Real-Time Content Moderation**: Use Llama AI to analyze user-generated content in real-time on platforms like Twitter or Facebook. Set up a workflow that scans text for offensive or inappropriate content and automatically removes it, or flags it for human review, maintaining community standards and compliance. + +- **Predictive Analytics for Sales**: Enhance sales forecasting by integrating Llama AI with CRM platforms like Salesforce. Analyze historical sales data to predict future trends and customer behavior, enabling sales teams to better focus their efforts on high-value opportunities and personalized engagements. diff --git a/components/llama_ai/actions/create-chat/create-chat.mjs b/components/llama_ai/actions/create-chat/create-chat.mjs new file mode 100644 index 0000000000000..546d863dd38da --- /dev/null +++ b/components/llama_ai/actions/create-chat/create-chat.mjs @@ -0,0 +1,66 @@ +import utils from "../../common/utils.mjs"; +import app from "../../llama_ai.app.mjs"; + +export default { + key: "llama_ai-create-chat", + name: "Create Chat", + description: "Creates a new chat. [See the documentation](https://docs.llama-api.com/api-reference/endpoint/create)", + version: "0.0.1", + type: "action", + props: { + app, + messages: { + type: "string[]", + label: "Messages", + description: "A collection of messages that form the ongoing conversation. Each message should be a JSON string.", + }, + functions: { + type: "string[]", + label: "Functions", + description: "A list of functions for which the model can generate JSON inputs. Each function should be a JSON string.", + optional: true, + }, + stream: { + type: "boolean", + label: "Stream", + description: "When this option is enabled, the model will send partial message updates, similar to ChatGPT. Tokens will be transmitted as data-only server-sent events as they become available, and the streaming will conclude with a data: [DONE] marker.", + optional: true, + }, + functionCall: { + type: "string", + label: "Function Call", + description: "This parameter governs the model's response to function calls. Choosing \"none\" indicates that the model will not invoke any functions and will respond directly to the end-user.", + optional: true, + }, + }, + methods: { + createChat(args = {}) { + return this.app.post({ + path: "/chat/completions", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createChat, + messages, + functions, + stream, + functionCall, + } = this; + + const response = await createChat({ + $, + data: { + messages: utils.parseArray(messages), + functions: utils.parseArray(functions), + stream, + function_call: functionCall, + }, + }); + + $.export("$summary", `Successfully created a new chat with \`${response.choices.length}\` choice(s)`); + return response; + }, +}; diff --git a/components/llama_ai/common/utils.mjs b/components/llama_ai/common/utils.mjs new file mode 100644 index 0000000000000..e6ed4fa271055 --- /dev/null +++ b/components/llama_ai/common/utils.mjs @@ -0,0 +1,50 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function isJson(value) { + try { + JSON.parse(value); + } catch (e) { + return false; + } + + return true; +} + +function valueToObject(value) { + if (typeof(value) === "object") { + return value; + } + + if (!isJson(value)) { + throw new ConfigurationError(`Make sure the custom expression contains a valid JSON object: \`${value}\``); + } + + return JSON.parse(value); +} + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +export default { + parseArray: (value) => parseArray(value).map(valueToObject), +}; diff --git a/components/llama_ai/llama_ai.app.mjs b/components/llama_ai/llama_ai.app.mjs new file mode 100644 index 0000000000000..9291b795dc1c9 --- /dev/null +++ b/components/llama_ai/llama_ai.app.mjs @@ -0,0 +1,33 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "llama_ai", + methods: { + getUrl(path) { + return `https://api.llama-api.com${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Content-Type": "application/json", + "Authorization": `Bearer ${this.$auth.api_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "post", + ...args, + }); + }, + }, +}; diff --git a/components/llama_ai/package.json b/components/llama_ai/package.json new file mode 100644 index 0000000000000..603e57a5e83b3 --- /dev/null +++ b/components/llama_ai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/llama_ai", + "version": "0.1.0", + "description": "Pipedream Llama AI Components", + "main": "llama_ai.app.mjs", + "keywords": [ + "pipedream", + "llama_ai" + ], + "homepage": "https://pipedream.com/apps/llama_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.4" + } +} diff --git a/components/llamaindex/llamaindex.app.mjs b/components/llamaindex/llamaindex.app.mjs new file mode 100644 index 0000000000000..0ed6da82e4cf7 --- /dev/null +++ b/components/llamaindex/llamaindex.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "llamaindex", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/llamaindex/package.json b/components/llamaindex/package.json new file mode 100644 index 0000000000000..e6bc189da1a6c --- /dev/null +++ b/components/llamaindex/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/llamaindex", + "version": "0.0.1", + "description": "Pipedream LlamaIndex Components", + "main": "llamaindex.app.mjs", + "keywords": [ + "pipedream", + "llamaindex" + ], + "homepage": "https://pipedream.com/apps/llamaindex", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/llmwhisperer/actions/extract-text/extract-text.mjs b/components/llmwhisperer/actions/extract-text/extract-text.mjs new file mode 100644 index 0000000000000..50795dc7e8ce8 --- /dev/null +++ b/components/llmwhisperer/actions/extract-text/extract-text.mjs @@ -0,0 +1,181 @@ +import fs from "fs"; +import app from "../../llmwhisperer.app.mjs"; + +export default { + key: "llmwhisperer-extract-text", + name: "Extract Text", + description: "Convert your PDF/scanned documents to text format which can be used by LLMs. [See the documentation](https://docs.unstract.com/llm_whisperer/apis/llm_whisperer_text_extraction_api)", + version: "0.0.1", + type: "action", + props: { + app, + processingMode: { + type: "string", + label: "Processing Mode", + description: "The processing mode to be used. Choose between `ocr` and `text`.", + options: [ + "ocr", + "text", + ], + }, + outputMode: { + type: "string", + label: "Output Mode", + description: "The output mode to be used. Choose between `line-printer` and `text`.", + options: [ + "line-printer", + "text", + ], + }, + pageSeperator: { + type: "string", + label: "Page Seperator", + description: "The string to be used as a page separator. Eg: `<<<`", + optional: true, + }, + forceTextProcessing: { + type: "boolean", + label: "Force Text Processing", + description: "If set to true, the document will be processed as text only. If set to false, the document will be processed based on LLMWhisperer's chosed stratergy.", + optional: true, + }, + pagesToExtract: { + type: "string", + label: "Pages To Extract", + description: "Define which pages to extract. By default all pages are extracted. You can specify which pages to extract with this parameter. Example `1-5,7,21-` will extract pages **1,2,3,4,5,7,21,22,23,24...** till the last page.", + optional: true, + }, + timeout: { + type: "integer", + label: "Timeout", + description: "The time in seconds after which the request will automatically switch to async mode. If a timeout occurs then the API will return a 202 message along with `whisper-hash` which can be used later to check processing status and retrieve the text. Refer to the async operation documentation for more information", + optional: true, + }, + storeMetadataForHighlighting: { + type: "boolean", + label: "Store Metadata for Highlighting", + description: "If set to true, metadata required for the highlighting is stored. If you do not require highlighting API, set this to false. Note that setting this to true will store your text in our servers", + optional: true, + }, + medianFilterSize: { + type: "integer", + label: "Median Filter Size", + description: "The size of the median filter to be applied to the image. This is used to remove noise from the image. This parameter works only in on-prem version of LLMWhisperer.", + optional: true, + }, + gaussianBlurRadius: { + type: "integer", + label: "Gaussian Blur Radius", + description: "The radius of the gaussian blur to be applied to the image. This is used to remove noise from the image. This parameter works only in on-prem version of LLMWhisperer.", + optional: true, + }, + ocrProvider: { + type: "string", + label: "OCR Provider", + description: "The OCR provider to be used. Choose between `simple` and `advanced`. This parameter works only in on-prem version of LLMWhisperer.", + optional: true, + options: [ + "simple", + "advanced", + ], + }, + lineSplitterTolerance: { + type: "string", + label: "Line Splitter Tolerance", + description: "Factor to decide when to move text to the next line when it is above or below the baseline. The default value of `0.4` signifies 40% of the average character height.", + optional: true, + }, + horizontalStretchFactor: { + type: "string", + label: "Horizontal Stretch Factor", + description: "Factor by which a horizontal stretch has to applied. It defaults to `1.0`. A stretch factor of `1.1` would mean at 10% stretch factor applied. Normally this factor need not be adjusted. You might want to use this parameter when multi column layouts back into each other. For example in a two column layout, the two columns get merged into one.", + optional: true, + }, + urlInPost: { + type: "boolean", + label: "URL In Post", + description: "If set to `true`, the headers will be set to `text/plain`. If set to `false`, the headers will be set to `application/octet-stream`.", + reloadProps: true, + default: true, + }, + }, + additionalProps() { + const { urlInPost } = this; + return { + data: { + type: "string", + label: urlInPost + ? "Document URL" + : "Document Path", + description: urlInPost + ? "The URL of the document to process." + : "Document path of the file previously downloaded in Pipedream E.g. (`/tmp/my-file.txt`). [Download a file to the `/tmp` directory](https://pipedream.com/docs/code/nodejs/http-requests/#download-a-file-to-the-tmp-directory)", + }, + }; + }, + methods: { + getHeaders(urlInPost) { + return { + "Content-Type": urlInPost + ? "text/plain" + : "application/octet-stream", + }; + }, + getData(urlInPost, data) { + return urlInPost + ? data + : fs.readFileSync(data); + }, + extractText(args = {}) { + return this.app.post({ + path: "/whisper", + ...args, + }); + }, + }, + async run({ $ }) { + const { + extractText, + getHeaders, + getData, + urlInPost, + processingMode, + outputMode, + pageSeperator, + forceTextProcessing, + pagesToExtract, + timeout, + storeMetadataForHighlighting, + medianFilterSize, + gaussianBlurRadius, + ocrProvider, + lineSplitterTolerance, + horizontalStretchFactor, + data, + } = this; + + const response = await extractText({ + $, + headers: getHeaders(urlInPost), + params: { + url_in_post: urlInPost, + processing_mode: processingMode, + output_mode: outputMode, + page_seperator: pageSeperator, + force_text_processing: forceTextProcessing, + pages_to_extract: pagesToExtract, + timeout, + store_metadata_for_highlighting: storeMetadataForHighlighting, + median_filter_size: medianFilterSize, + gaussian_blur_radius: gaussianBlurRadius, + ocr_provider: ocrProvider, + line_splitter_tolerance: lineSplitterTolerance, + horizontal_stretch_factor: horizontalStretchFactor, + }, + data: getData(urlInPost, data), + }); + + $.export("$summary", "Successfully extracted text from document."); + return response; + }, +}; diff --git a/components/llmwhisperer/actions/get-status/get-status.mjs b/components/llmwhisperer/actions/get-status/get-status.mjs new file mode 100644 index 0000000000000..0f93240ab2109 --- /dev/null +++ b/components/llmwhisperer/actions/get-status/get-status.mjs @@ -0,0 +1,42 @@ +import app from "../../llmwhisperer.app.mjs"; + +export default { + key: "llmwhisperer-get-status", + name: "Get Status", + description: "Get the status of the whisper process. This can be used to check the status of the conversion process when the conversion is done in async mode. [See the documentation](https://docs.unstract.com/llm_whisperer/apis/llm_whisperer_text_extraction_status_api)", + version: "0.0.1", + type: "action", + props: { + app, + whisperHash: { + propDefinition: [ + app, + "whisperHash", + ], + }, + }, + methods: { + getStatus(args = {}) { + return this.app._makeRequest({ + path: "/whisper-status", + ...args, + }); + }, + }, + async run({ $ }) { + const { + getStatus, + whisperHash, + } = this; + + const response = await getStatus({ + $, + params: { + "whisper-hash": whisperHash, + }, + }); + + $.export("$summary", "Successfully retrieved status."); + return response; + }, +}; diff --git a/components/llmwhisperer/actions/highlight-locations/highlight-locations.mjs b/components/llmwhisperer/actions/highlight-locations/highlight-locations.mjs new file mode 100644 index 0000000000000..1d28472b35b83 --- /dev/null +++ b/components/llmwhisperer/actions/highlight-locations/highlight-locations.mjs @@ -0,0 +1,53 @@ +import app from "../../llmwhisperer.app.mjs"; + +export default { + key: "llmwhisperer-highlight-locations", + name: "Highlight Locations", + description: "Generate highlight locations for a search term in the document. [See the documentation](https://docs.unstract.com/llm_whisperer/apis/llm_whisperer_text_extraction_highlight_api)", + version: "0.0.1", + type: "action", + props: { + app, + whisperHash: { + propDefinition: [ + app, + "whisperHash", + ], + }, + data: { + type: "string", + label: "Search Term", + description: "The search term to highlight in the document.", + }, + }, + methods: { + highlightLocations(args = {}) { + return this.app.post({ + path: "/highlight-data", + ...args, + }); + }, + }, + async run({ $ }) { + const { + highlightLocations, + whisperHash, + data, + } = this; + + const response = await highlightLocations({ + $, + headers: { + "Content-Type": "text/plain", + }, + params: { + "whisper-hash": whisperHash, + }, + data, + }); + + $.export("$summary", `Successfully generated highlight locations for the search term: \`${data}\`.`); + + return response; + }, +}; diff --git a/components/llmwhisperer/actions/retrieve-text/retrieve-text.mjs b/components/llmwhisperer/actions/retrieve-text/retrieve-text.mjs new file mode 100644 index 0000000000000..56b4a4113eaf5 --- /dev/null +++ b/components/llmwhisperer/actions/retrieve-text/retrieve-text.mjs @@ -0,0 +1,42 @@ +import app from "../../llmwhisperer.app.mjs"; + +export default { + key: "llmwhisperer-retrieve-text", + name: "Retrieve Extracted Text", + description: "Retrieve the extracted text executed through the whisper API. This can be used to retrieve the text of the conversion process when the conversion is done in async mode. [See the documentation](https://docs.unstract.com/llm_whisperer/apis/llm_whisperer_text_extraction_retrieve_api)", + version: "0.0.1", + type: "action", + props: { + app, + whisperHash: { + propDefinition: [ + app, + "whisperHash", + ], + }, + }, + methods: { + retrieveText(args = {}) { + return this.app._makeRequest({ + path: "/whisper-retrieve", + ...args, + }); + }, + }, + async run({ $ }) { + const { + retrieveText, + whisperHash, + } = this; + + const response = await retrieveText({ + $, + params: { + "whisper-hash": whisperHash, + }, + }); + + $.export("$summary", "Successfully retrieved extracted text."); + return response; + }, +}; diff --git a/components/llmwhisperer/common/constants.mjs b/components/llmwhisperer/common/constants.mjs new file mode 100644 index 0000000000000..26d8bf9a544a2 --- /dev/null +++ b/components/llmwhisperer/common/constants.mjs @@ -0,0 +1,7 @@ +const BASE_URL = "https://llmwhisperer-api.unstract.com"; +const VERSION_PATH = "/v1"; + +export default { + BASE_URL, + VERSION_PATH, +}; diff --git a/components/llmwhisperer/llmwhisperer.app.mjs b/components/llmwhisperer/llmwhisperer.app.mjs new file mode 100644 index 0000000000000..befc1600248f4 --- /dev/null +++ b/components/llmwhisperer/llmwhisperer.app.mjs @@ -0,0 +1,40 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "llmwhisperer", + propDefinitions: { + whisperHash: { + type: "string", + label: "Whisper Hash", + description: "The whisper hash returned while starting the whisper process.", + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "unstract-key": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + }, +}; diff --git a/components/llmwhisperer/package.json b/components/llmwhisperer/package.json new file mode 100644 index 0000000000000..9d9d06ba6ccd0 --- /dev/null +++ b/components/llmwhisperer/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/llmwhisperer", + "version": "0.1.0", + "description": "Pipedream LLMWhisperer Components", + "main": "llmwhisperer.app.mjs", + "keywords": [ + "pipedream", + "llmwhisperer" + ], + "homepage": "https://pipedream.com/apps/llmwhisperer", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0", + "fs": "^0.0.1-security" + } +} diff --git a/components/lnk_bio/README.md b/components/lnk_bio/README.md new file mode 100644 index 0000000000000..3c9d82d79a089 --- /dev/null +++ b/components/lnk_bio/README.md @@ -0,0 +1,14 @@ +# Overview + +Lnk.Bio is a simple tool for creating micro landing pages that house all of your links in one place, often used for social media profiles like Instagram. With the Lnk.Bio API on Pipedream, you can automate the management of these links, pulling link data into other apps, or triggering actions based on link activity. Imagine updating links dynamically based on inventory levels from an e-commerce platform, or sending notifications when new links are added. + +# Example Use Cases + +- **Automate Link Updates Based on Product Inventory** + Use the Lnk.Bio API to check product inventory levels from a Shopify store and automatically update Lnk.Bio links to show "Out of Stock" or redirect to alternative products when inventory runs low. + +- **Sync New Lnk.Bio Links to a Google Sheet** + Every time a new link is added to your Lnk.Bio page, trigger a Pipedream workflow that adds the link details to a Google Sheet. This could be useful for tracking and analytics purposes, ensuring you keep a live database of all active links. + +- **Send Notifications on Link Engagement** + Monitor your Lnk.Bio link activity and use Pipedream to send a Slack message or an email via SendGrid when a link receives a certain number of clicks, allowing you to gauge the effectiveness of your social media campaigns in real-time. diff --git a/components/lob/README.md b/components/lob/README.md index 8a593814760a5..514b768dbe3fd 100644 --- a/components/lob/README.md +++ b/components/lob/README.md @@ -1,12 +1,11 @@ # Overview -With Lob's API, you can: - -- Create and manage bank accounts -- Verify bank account numbers -- Create and manage checks -- Send checks in the mail -- Create and manage Postcards -- Send Postcards in the mail -- Track the status of your mail -- And much more! +The Lob API provides a suite of automation capabilities focused on direct mail, address verification, and print management. With Lob, you can automate the sending of physical mail programmatically, verify addresses to ensure deliverability, and manage printing logistics for marketing campaigns or operational needs. It's a powerful tool for bridging the digital and physical mailing worlds, particularly useful for marketing, billing, and compliance communications. + +# Example Use Cases + +- **Automated Thank-You Postcards**: After a customer completes a purchase on an e-commerce platform, trigger a workflow in Pipedream that sends a personalized thank-you postcard using the Lob API. This gesture can enhance customer loyalty and provide a memorable brand experience. + +- **Billing Reminders via Mail**: Set up an automated system that sends out physical billing reminders or invoices to customers who have due payments. By integrating the Lob API with a payment processing platform on Pipedream, you can target those who haven't engaged with digital reminders. + +- **Address Cleanup for CRM**: Keep your CRM data pristine by using Lob's address verification. Create a Pipedream workflow that periodically checks and updates customer addresses in your CRM system, ensuring that all physical correspondence reaches its intended destination. diff --git a/components/lobste_rs/README.md b/components/lobste_rs/README.md new file mode 100644 index 0000000000000..456af744d1007 --- /dev/null +++ b/components/lobste_rs/README.md @@ -0,0 +1,11 @@ +# Overview + +The lobste.rs API provides access to a wealth of information from the lobste.rs community, a computing-focused curated link aggregation site. It offers endpoints to fetch stories, comments, and user details, which can be integrated into various workflows on Pipedream. This integration allows you to monitor new content, track topics or user activity, and even collate data for analysis. With Pipedream, you can seamlessly connect lobste.rs with hundreds of other apps to create tailored automations without managing servers or writing extensive code. + +# Example Use Cases + +- **Track New Top Stories**: Trigger a Pipedream workflow whenever a new story reaches the top of lobste.rs. Use this to send notifications via email or Slack, ensuring you, your team, or your community stay updated on trending topics in the tech world. + +- **Analyze Comment Sentiments**: Whenever a new comment is posted, trigger a workflow that evaluates the comment's sentiment using a service like Google Cloud Natural Language API. Store the sentiment scores in a Google Sheet for ongoing analysis of community engagement and tone. + +- **Aggregate User Submissions**: For researchers or community managers, track and accumulate submissions by specific users using Pipedream's built-in cron job feature. Connect your workflow to a database like PostgreSQL to create a historical archive, which you can then query for trends and patterns. diff --git a/components/lobste_rs/lobste_rs.app.mjs b/components/lobste_rs/lobste_rs.app.mjs new file mode 100644 index 0000000000000..c2ed540a53aaf --- /dev/null +++ b/components/lobste_rs/lobste_rs.app.mjs @@ -0,0 +1,111 @@ +import { axios } from "@pipedream/platform"; +import FeedParser from "feedparser"; +import hash from "object-hash"; + +export default { + type: "app", + app: "lobste_rs", + propDefinitions: {}, + methods: { + makeRequest({ + $ = this, ...config + }) { + return axios($, config); + }, + itemTs(item = {}) { + const { + pubdate, pubDate, date_published, + } = item; + const itemPubDate = pubdate ?? pubDate ?? date_published; + if (itemPubDate) { + return +new Date(itemPubDate); + } + return +new Date(); + }, + itemKey(item = {}) { + const { + id, guid, link, title, + } = item; + const itemId = id ?? guid ?? link ?? title; + if (itemId) { + // reduce itemId length for deduping + return itemId.length > 64 + ? itemId.slice(-64) + : itemId; + } + return hash(item); + }, + async fetchFeed(url) { + const res = await axios(this, { + url, + method: "GET", + headers: { + "accept": "text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8, application/json, application/feed+json", + }, + responseType: "stream", // stream is required for feedparser + returnFullResponse: true, + }); + return { + data: res.data, + contentType: res.headers["content-type"], + }; + }, + async parseFeed(stream) { + const feedparser = new FeedParser({ + addmeta: true, + }); + const items = []; + await new Promise((resolve, reject) => { + feedparser.on("error", reject); + feedparser.on("end", resolve); + feedparser.on("readable", function () { + let item = this.read(); + + while (item) { + for (const k in item) { + if (item[`rss:${k}`]) { + delete item[`rss:${k}`]; + continue; + } + const o = item[k]; + if (o == null || (typeof o === "object" && !Object.keys(o).length) || Array.isArray(o) && !o.length) { + delete item[k]; + continue; + } + } + items.push(item); + item = this.read(); + } + }); + stream.pipe(feedparser); + }); + return items; + }, + isJSONFeed(response) { + const acceptedJsonFeedMimes = [ + "application/feed+json", + "application/json", + ]; + return acceptedJsonFeedMimes.includes(response?.contentType?.toLowerCase()); + }, + async parseJSONFeed(stream) { + const buffer = await new Promise((resolve, reject) => { + const _buf = []; + stream.on("data", (chunk) => _buf.push(chunk)); + stream.on("end", () => resolve(Buffer.concat(_buf))); + stream.on("error", (err) => reject(err)); + }); + const contentString = buffer.toString(); + const feed = JSON.parse(contentString); + return feed?.items || []; + }, + async fetchAndParseFeed(url) { + const response = await this.fetchFeed(url); + if (this.isJSONFeed(response)) { + return await this.parseJSONFeed(response.data); + } else { + return await this.parseFeed(response.data); + } + }, + }, +}; diff --git a/components/lobste_rs/package.json b/components/lobste_rs/package.json new file mode 100644 index 0000000000000..5372a3a2fb863 --- /dev/null +++ b/components/lobste_rs/package.json @@ -0,0 +1,20 @@ +{ + "name": "@pipedream/lobste_rs", + "version": "0.1.0", + "description": "Pipedream lobste.rs Components", + "main": "lobste_rs.app.mjs", + "keywords": [ + "pipedream", + "lobste_rs" + ], + "homepage": "https://pipedream.com/apps/lobste_rs", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "feedparser": "^2.2.10", + "object-hash": "^3.0.0" + } +} diff --git a/components/lobste_rs/sources/new-comment-in-thread/new-comment-in-thread.mjs b/components/lobste_rs/sources/new-comment-in-thread/new-comment-in-thread.mjs new file mode 100644 index 0000000000000..e1dcec2a8776c --- /dev/null +++ b/components/lobste_rs/sources/new-comment-in-thread/new-comment-in-thread.mjs @@ -0,0 +1,43 @@ +import lobsters from "../../lobste_rs.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + key: "lobste_rs-new-comment-in-thread", + name: "New Comment in Thread", + description: "Emit new event when a new comment is added to a thread.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + lobsters, + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + url: { + type: "string", + label: "URL", + description: "The URL of the comment thread to retrieve. E.g. `https://lobste.rs/s/yqjtvy/cloud_container_iceberg`", + }, + }, + methods: { + generateMeta(comment) { + return { + id: comment.short_id, + summary: comment.comment_plain.substring(0, 50), + ts: Date.parse(comment.created_at), + }; + }, + }, + async run() { + const { comments } = await this.lobsters.makeRequest({ + url: `${this.url}.json`, + }); + for (const comment of comments.reverse()) { + const meta = this.generateMeta(comment); + this.$emit(comment, meta); + } + }, +}; diff --git a/components/lobste_rs/sources/new-story-by-user/new-story-by-user.mjs b/components/lobste_rs/sources/new-story-by-user/new-story-by-user.mjs new file mode 100644 index 0000000000000..2a4b2228c0577 --- /dev/null +++ b/components/lobste_rs/sources/new-story-by-user/new-story-by-user.mjs @@ -0,0 +1,55 @@ +import lobsters from "../../lobste_rs.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + key: "lobste_rs-new-story-by-user", + name: "New Story by User", + description: "Emit new event when a new story is posted by the specified user.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + lobsters, + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + username: { + type: "string", + label: "Username", + description: "The user to watch for stories from. E.g. `adamgordonbell`", + }, + publishedAfter: { + type: "string", + label: "Published After", + description: "Emit items published after the specified date in ISO 8601 format .e.g `2022-12-07T12:57:10+07:00`", + default: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + }, + }, + methods: { + generateMeta(item) { + return { + id: this.lobsters.itemKey(item), + summary: item.title, + ts: Date.now(), + }; + }, + }, + async run() { + const url = `https://lobste.rs/~${this.username}/stories.rss`; + + const items = await this.lobsters.fetchAndParseFeed(url); + for (const item of items.reverse()) { + const publishedAfter = +new Date(this.publishedAfter); + const ts = this.lobsters.itemTs(item); + if (Number.isNaN(publishedAfter) || publishedAfter > ts) { + continue; + } + + const meta = this.generateMeta(item); + this.$emit(item, meta); + } + }, +}; diff --git a/components/locate/README.md b/components/locate/README.md index e4d3b94817029..fd9d911e85446 100644 --- a/components/locate/README.md +++ b/components/locate/README.md @@ -1,6 +1,11 @@ # Overview -The Locate API is a REST API that enables you to access the Locate inventory -management system from your own applications. With the Locate API, you can -create, update, and delete inventory items, track inventory levels, and manage -orders and invoices. +The Locate API enables precise inventory management by tracking items in real-time. By leveraging this API with Pipedream, one can automate inventory updates, trigger alerts for low stock, and integrate with e-commerce platforms for synchronized stock management. It's a potent tool for businesses that want to streamline their inventory processes and ensure item availability through automated workflows. + +# Example Use Cases + +- **Automated Inventory Replenishment Notification**: Set up a workflow that monitors inventory levels using the Locate API. When stock for a particular item drops below a predefined threshold, it triggers an email or SMS via integrations like SendGrid or Twilio to notify the responsible party to reorder, ensuring you never run out of key products. + +- **E-commerce Integration for Real-Time Stock Updates**: Sync inventory levels between Locate and an e-commerce platform like Shopify or WooCommerce. Whenever a sale is made, the workflow updates the stock count in Locate and reflects these changes on the e-commerce site. This automation prevents overselling and keeps customers informed about product availability. + +- **Asset Tracking and Alerts**: Create a workflow that utilizes the Locate API to track assets across multiple locations. If an asset moves outside a designated area or isn't scanned within a set timeframe, Pipedream triggers an alert using services like Slack or PagerDuty to notify the team, enhancing security and accountability of high-value items. diff --git a/components/locate/package.json b/components/locate/package.json new file mode 100644 index 0000000000000..3566193ef1e0c --- /dev/null +++ b/components/locate/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/locate", + "version": "0.6.0", + "description": "Pipedream locate Components", + "main": "locate.app.mjs", + "keywords": [ + "pipedream", + "locate" + ], + "homepage": "https://pipedream.com/apps/locate", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/lodgify/README.md b/components/lodgify/README.md index 6492c6342ee44..5a69d4e459b0d 100644 --- a/components/lodgify/README.md +++ b/components/lodgify/README.md @@ -1,9 +1,11 @@ # Overview -With the Lodgify API, you can build a variety of applications and services for -vacation rental property owners and managers. Here are a few examples: +Lodgify is a vacation rental software solution that provides tools to create a website, manage reservations, and automate communication with guests. With the Lodgify API, you can sync booking data, manage listings, and automate tasks to ensure a seamless rental experience. By leveraging Pipedream, you can tap into this functionality to create dynamic workflows that interact with other apps, automate communications, synchronize calendars, and streamline property management. -- A vacation rental listing and booking website -- A vacation rental management system -- A vacation rental rate and availability service -- A vacation rental reservations and payments service +# Example Use Cases + +- **Automated Guest Communication**: After a booking is confirmed in Lodgify, trigger an email or SMS via SendGrid or Twilio to provide guests with essential information like check-in instructions or local recommendations. + +- **Synchronization with External Calendars**: When a new reservation is made or cancelled, use Pipedream to sync this information to Google Calendar or Office 365 Calendar to keep all stakeholders updated. + +- **Smart Home Integration for Check-In/Check-Out**: Connect Lodgify to a smart home service like Philips Hue or Nest. Automate the process of preparing the property by adjusting the lighting or temperature based on check-in and check-out events. diff --git a/components/loggly_send_data/README.md b/components/loggly_send_data/README.md index b4cb6ae4722d7..0377c51a1842c 100644 --- a/components/loggly_send_data/README.md +++ b/components/loggly_send_data/README.md @@ -1,9 +1,11 @@ # Overview -Loggly provides an API that lets you send data to Loggly. This data can be used -to build a variety of things, including: +The Loggly (Send Data) API enables you to transmit log data into Loggly, a cloud-based log management and analytics service. With this integration, you can automate the aggregation of logs from various sources, analyze them in real-time, and monitor your applications and systems effectively. By leveraging this API on Pipedream, you can create automated workflows that streamline the process of log collection and correlation, set up alerts based on log patterns, and dynamically respond to system events. -- A dashboard of your logs -- A system to monitor your logs -- A way to visualize your logs -- A system to alert you when something goes wrong with your logs +# Example Use Cases + +- **Error Alerting on Communication Platforms**: Create a workflow that monitors application logs for errors and automatically posts a notification in a team's Slack channel when critical issues are detected. This instant alerting enables quicker response times to resolve issues. + +- **Performance Metrics Aggregation**: Set up a workflow where performance metrics from different servers are sent to Loggly, and then use this data to trigger actions, such as scaling your infrastructure up or down on AWS, based on predefined thresholds. + +- **Customer Support Ticket Creation**: Implement an automated system that watches for specific log events that may indicate user issues and automatically creates a support ticket in Zendesk, giving customer support a head start on addressing the problem. diff --git a/components/loggly_send_data/package.json b/components/loggly_send_data/package.json new file mode 100644 index 0000000000000..e6ee202c46975 --- /dev/null +++ b/components/loggly_send_data/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/loggly_send_data", + "version": "0.6.0", + "description": "Pipedream loggly_send_data Components", + "main": "loggly_send_data.app.mjs", + "keywords": [ + "pipedream", + "loggly_send_data" + ], + "homepage": "https://pipedream.com/apps/loggly_send_data", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/logistia_route_planner/logistia_route_planner.app.mjs b/components/logistia_route_planner/logistia_route_planner.app.mjs new file mode 100644 index 0000000000000..49ad380f78341 --- /dev/null +++ b/components/logistia_route_planner/logistia_route_planner.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "logistia_route_planner", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/logistia_route_planner/package.json b/components/logistia_route_planner/package.json new file mode 100644 index 0000000000000..533b6b23fe5fc --- /dev/null +++ b/components/logistia_route_planner/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/logistia_route_planner", + "version": "0.0.1", + "description": "Pipedream Logistia Route Planner Components", + "main": "logistia_route_planner.app.mjs", + "keywords": [ + "pipedream", + "logistia_route_planner" + ], + "homepage": "https://pipedream.com/apps/logistia_route_planner", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/logo_dev/logo_dev.app.mjs b/components/logo_dev/logo_dev.app.mjs new file mode 100644 index 0000000000000..9857304f5ff13 --- /dev/null +++ b/components/logo_dev/logo_dev.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "logo_dev", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/logo_dev/package.json b/components/logo_dev/package.json new file mode 100644 index 0000000000000..0761d077fb6da --- /dev/null +++ b/components/logo_dev/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/logo_dev", + "version": "0.0.1", + "description": "Pipedream Logo.dev Components", + "main": "logo_dev.app.mjs", + "keywords": [ + "pipedream", + "logo_dev" + ], + "homepage": "https://pipedream.com/apps/logo_dev", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/logoraisr/README.md b/components/logoraisr/README.md index d594a1a844515..0ae4dd9bae6a3 100644 --- a/components/logoraisr/README.md +++ b/components/logoraisr/README.md @@ -1,9 +1,11 @@ # Overview -With the Logoraisr API, you can create amazing applications that help you -gather feedback from your users. Here are some examples of what you can build: +The Logoraisr API enables you to enhance images through advanced processing techniques. With Logoraisr, you can improve the quality of logos and graphics, making them sharper and ready for various use cases like branding, marketing, and design projects. When combined with Pipedream, you can automate image processing workflows, handling bulk image enhancements, integrating with cloud storage solutions, and connecting with other apps to streamline your graphic design operations. -- A feedback form that your users can fill out -- A poll or survey tool to gather feedback on a specific topic -- A way to collect customer feedback on your products or services -- A tool to help you track customer satisfaction over time +# Example Use Cases + +- **Automated Image Enhancement for Social Media Posts**: Use Logoraisr with Pipedream to automatically enhance images uploaded to a Dropbox folder. Set up a workflow where each new Dropbox file triggers the Logoraisr API to upscale and sharpen the image, then auto-post the improved version to social media platforms like Twitter or Facebook, ensuring your brand always looks crisp online. + +- **Dynamic Image Processing for E-commerce**: Connect Logoraisr to an e-commerce platform, such as Shopify, via Pipedream. Automate the workflow to detect new product images and send them to Logoraisr for enhancement. Once processed, the high-quality images are updated on the product listings, providing customers with better visuals, which can lead to increased sales and reduced returns due to clearer product imagery. + +- **Real-time Logo Improvement for Brand Assets**: Integrate Google Drive with Logoraisr on Pipedream to monitor a specific folder for new logo uploads. Once a new logo is detected, it's sent to Logoraisr for a quality boost and then stored in a designated 'Enhanced' folder on Google Drive. This automation ensures that all your brand assets are consistently of high quality and ready for any marketing collateral. diff --git a/components/logsnag/README.md b/components/logsnag/README.md new file mode 100644 index 0000000000000..87cc90f055795 --- /dev/null +++ b/components/logsnag/README.md @@ -0,0 +1,11 @@ +# Overview + +LogSnag is a real-time event tracking API that enables developers to monitor and track events in their applications. With LogSnag, you can create simple, powerful dashboards to watch events as they happen and set up triggers to notify you of important activities. On Pipedream, LogSnag's capabilities can be harnessed to automate workflows, integrating with numerous services to create a blend of operations, such as triggering notifications, logging significant events, and gathering metrics across various platforms. + +# Example Use Cases + +- **Customer Sign-up Notifications**: Track user sign-ups in real-time by sending events to LogSnag whenever a new user registers on your platform. Combine this with sending a welcome email via SendGrid, giving you both a record of new sign-ups and an automated response to your users. + +- **Payment Processing Alerts**: Implement a workflow that monitors payment transactions. Upon successful payment, an event gets sent to LogSnag, while failed transactions trigger an alert to your Slack channel. This provides a dual-layered update system, ensuring that you're immediately informed of any payment issues. + +- **Error Logging for Deployments**: Integrate LogSnag with your CI/CD pipeline, such as GitHub Actions, to log deployment statuses. Have events pushed to LogSnag for successful deployments, and use conditional logic to send alerts to your DevOps team's Discord server on failure, keeping your team abreast of the deployment health. diff --git a/components/lokalise/actions/create-project/create-project.mjs b/components/lokalise/actions/create-project/create-project.mjs new file mode 100644 index 0000000000000..59a040a866872 --- /dev/null +++ b/components/lokalise/actions/create-project/create-project.mjs @@ -0,0 +1,60 @@ +import lokalise from "../../lokalise.app.mjs"; + +export default { + key: "lokalise-create-project", + name: "Create Project", + description: "Initializes an empty project in Lokalise. [See the documentation](https://developers.lokalise.com/reference/create-a-project)", + version: "0.0.1", + type: "action", + props: { + lokalise, + name: { + type: "string", + label: "Name", + description: "Name of the project", + }, + description: { + type: "string", + label: "Description", + description: "Description of the project", + optional: true, + }, + language: { + propDefinition: [ + lokalise, + "language", + ], + optional: true, + }, + projectType: { + type: "string", + label: "Project Type", + description: "The type of project", + options: [ + "localization_files", + "paged_documents", + ], + optional: true, + }, + isSegmentationEnabled: { + type: "boolean", + label: "Is Segmentation Enabled", + description: "Whether to enable Segmentation feature for project", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.lokalise.createProject({ + $, + data: { + name: this.name, + description: this.description, + base_lang_iso: this.language, + project_type: this.projectType, + is_segmentation_enabled: this.isSegmentationEnabled, + }, + }); + $.export("$summary", `Successfully created project with ID: ${response.project_id}`); + return response; + }, +}; diff --git a/components/lokalise/actions/download-files/download-files.mjs b/components/lokalise/actions/download-files/download-files.mjs new file mode 100644 index 0000000000000..cd86bbe73df10 --- /dev/null +++ b/components/lokalise/actions/download-files/download-files.mjs @@ -0,0 +1,35 @@ +import lokalise from "../../lokalise.app.mjs"; + +export default { + key: "lokalise-download-files", + name: "Download Files", + description: "Retrieves and downloads files from a specified Lokalise project. [See the documentation](https://developers.lokalise.com/reference/download-files)", + version: "0.0.1", + type: "action", + props: { + lokalise, + projectId: { + propDefinition: [ + lokalise, + "projectId", + ], + }, + fileFormat: { + type: "string", + label: "File Format", + description: "File format (e.g. json, strings, xml). Must be file extension of any of the [supported file formats](https://docs.lokalise.com/en/collections/2909229-supported-file-formats). May also be `ios_sdk` or `android_sdk` for respective OTA SDK bundles.", + }, + }, + async run({ $ }) { + const response = await this.lokalise.downloadFiles({ + $, + projectId: this.projectId, + data: { + format: this.fileFormat, + }, + }); + + $.export("$summary", `Successfully downloaded files from project ${this.projectId}`); + return response; + }, +}; diff --git a/components/lokalise/actions/upload-file/upload-file.mjs b/components/lokalise/actions/upload-file/upload-file.mjs new file mode 100644 index 0000000000000..510bf37d2734a --- /dev/null +++ b/components/lokalise/actions/upload-file/upload-file.mjs @@ -0,0 +1,53 @@ +import lokalise from "../../lokalise.app.mjs"; +import fs from "fs"; + +export default { + key: "lokalise-upload-file", + name: "Upload File", + description: "Uploads a specified file to a Lokalise project. [See the documentation](https://developers.lokalise.com/reference/upload-a-file)", + version: "0.0.1", + type: "action", + props: { + lokalise, + projectId: { + propDefinition: [ + lokalise, + "projectId", + ], + }, + filePath: { + type: "string", + label: "File Path", + description: "The path to a file of a [supported file format](https://docs.lokalise.com/en/collections/2909229-supported-file-formats) in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp).", + }, + language: { + propDefinition: [ + lokalise, + "language", + ], + }, + filename: { + type: "string", + label: "Filename", + description: "Set the filename. You may optionally use a relative path in the filename", + }, + }, + async run({ $ }) { + const fileData = fs.readFileSync(this.filePath.startsWith("/tmp") + ? this.filePath + : `/tmp/${this.filePath}`, { + encoding: "base64", + }); + const response = await this.lokalise.uploadFile({ + $, + projectId: this.projectId, + data: { + data: fileData, + filename: this.filename, + lang_iso: this.language, + }, + }); + $.export("$summary", "Successfully uploaded file"); + return response; + }, +}; diff --git a/components/lokalise/lokalise.app.mjs b/components/lokalise/lokalise.app.mjs new file mode 100644 index 0000000000000..6739459519378 --- /dev/null +++ b/components/lokalise/lokalise.app.mjs @@ -0,0 +1,118 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "lokalise", + propDefinitions: { + projectId: { + type: "string", + label: "Project ID", + description: "Identifier of a project", + async options({ page }) { + const { projects } = await this.listProjects({ + params: { + page: page + 1, + }, + }); + return projects?.map(({ + project_id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + language: { + type: "string", + label: "Language", + description: "Language/locale code of the project base language", + async options({ page }) { + const { languages } = await this.listLanguages({ + params: { + page: page + 1, + }, + }); + return languages?.map(({ + lang_iso: value, lang_name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.lokalise.com/api2"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + createWebhook({ + projectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/projects/${projectId}/webhooks`, + ...opts, + }); + }, + deleteWebhook({ + projectId, hookId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/projects/${projectId}/webhooks/${hookId}`, + ...opts, + }); + }, + listProjects(opts = {}) { + return this._makeRequest({ + path: "/projects", + ...opts, + }); + }, + listLanguages(opts = {}) { + return this._makeRequest({ + path: "/system/languages", + ...opts, + }); + }, + createProject(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/projects", + ...opts, + }); + }, + uploadFile({ + projectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/projects/${projectId}/files/upload`, + ...opts, + }); + }, + downloadFiles({ + projectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/projects/${projectId}/files/download`, + ...opts, + }); + }, + }, +}; diff --git a/components/lokalise/package.json b/components/lokalise/package.json new file mode 100644 index 0000000000000..c8853ae704b26 --- /dev/null +++ b/components/lokalise/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/lokalise", + "version": "0.1.0", + "description": "Pipedream Lokalise Components", + "main": "lokalise.app.mjs", + "keywords": [ + "pipedream", + "lokalise" + ], + "homepage": "https://pipedream.com/apps/lokalise", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/lokalise/sources/common/base.mjs b/components/lokalise/sources/common/base.mjs new file mode 100644 index 0000000000000..217cc694aa591 --- /dev/null +++ b/components/lokalise/sources/common/base.mjs @@ -0,0 +1,65 @@ +import lokalise from "../../lokalise.app.mjs"; + +export default { + props: { + lokalise, + http: "$.interface.http", + db: "$.service.db", + projectId: { + propDefinition: [ + lokalise, + "projectId", + ], + }, + }, + hooks: { + async activate() { + const { webhook } = await this.lokalise.createWebhook({ + projectId: this.projectId, + data: { + url: this.http.endpoint, + events: this.getEvents(), + }, + }); + this._setHookId(webhook.webhook_id); + }, + async deactivate() { + const hookId = this._getHookId(); + if (hookId) { + await this.lokalise.deleteWebhook({ + projectId: this.projectId, + hookId, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + generateMeta(event) { + return { + id: `${event.event}-${event.created_at_timestamp}`, + summary: this.getSummary(event), + ts: event.created_at_timestamp, + }; + }, + getEvents() { + throw new Error("getEvents is not implemented"); + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + }, + async run(event) { + const { body } = event; + if (!body.event) { + return; + } + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, +}; diff --git a/components/lokalise/sources/new-task-closed-instant/new-task-closed-instant.mjs b/components/lokalise/sources/new-task-closed-instant/new-task-closed-instant.mjs new file mode 100644 index 0000000000000..3b9f209118fd3 --- /dev/null +++ b/components/lokalise/sources/new-task-closed-instant/new-task-closed-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "lokalise-new-task-closed-instant", + name: "New Task Closed (Instant)", + description: "Emit new event when a task is closed in Lokalise", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "project.task.closed", + ]; + }, + getSummary({ task }) { + return `Task Closed with ID: ${task.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/lokalise/sources/new-task-closed-instant/test-event.mjs b/components/lokalise/sources/new-task-closed-instant/test-event.mjs new file mode 100644 index 0000000000000..38e6458d8db97 --- /dev/null +++ b/components/lokalise/sources/new-task-closed-instant/test-event.mjs @@ -0,0 +1,21 @@ +export default { + "event": "project.task.closed", + "task": { + "id": 5022, + "type": "translation", + "title": "Headings translation", + "due_date": "2019-08-01 00:00:00", + "description": "Task description" + }, + "project": { + "id": "138c1ffa0ad94848f01f980e7f2f2af19d1bd553", + "name": "TheApp Project", + "branch": "master" + }, + "user": { + "email": "jdoe@mycompany.com", + "full_name": "John Doe" + }, + "created_at": "2019-07-29 12:18:31", + "created_at_timestamp": 1564395511 +} \ No newline at end of file diff --git a/components/lokalise/sources/new-task-created-instant/new-task-created-instant.mjs b/components/lokalise/sources/new-task-created-instant/new-task-created-instant.mjs new file mode 100644 index 0000000000000..a342548b70132 --- /dev/null +++ b/components/lokalise/sources/new-task-created-instant/new-task-created-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "lokalise-new-task-created-instant", + name: "New Task Created (Instant)", + description: "Emit new event when a new task is created in Lokalise", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "project.task.created", + ]; + }, + getSummary({ task }) { + return `New Task with ID: ${task.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/lokalise/sources/new-task-created-instant/test-event.mjs b/components/lokalise/sources/new-task-created-instant/test-event.mjs new file mode 100644 index 0000000000000..6aea90f65171d --- /dev/null +++ b/components/lokalise/sources/new-task-created-instant/test-event.mjs @@ -0,0 +1,21 @@ +export default { + "event": "project.task.created", + "task": { + "id": 5022, + "type": "translation", + "title": "Headings translation", + "due_date": "2019-08-01 00:00:00", + "description": "Task description" + }, + "project": { + "id": "138c1ffa0ad94848f01f980e7f2f2af19d1bd553", + "name": "TheApp Project", + "branch": "master" + }, + "user": { + "email": "jdoe@mycompany.com", + "full_name": "John Doe" + }, + "created_at": "2019-07-29 12:18:31", + "created_at_timestamp": 1564395511 +} \ No newline at end of file diff --git a/components/lokalise/sources/project-imported-instant/project-imported-instant.mjs b/components/lokalise/sources/project-imported-instant/project-imported-instant.mjs new file mode 100644 index 0000000000000..a61a9b69241ec --- /dev/null +++ b/components/lokalise/sources/project-imported-instant/project-imported-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "lokalise-project-imported-instant", + name: "New Project Imported (Instant)", + description: "Emit new event when data is imported into a project", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "project.imported", + ]; + }, + getSummary() { + return "Data imported to project"; + }, + }, + sampleEmit, +}; diff --git a/components/lokalise/sources/project-imported-instant/test-event.mjs b/components/lokalise/sources/project-imported-instant/test-event.mjs new file mode 100644 index 0000000000000..ed85fc7c05d41 --- /dev/null +++ b/components/lokalise/sources/project-imported-instant/test-event.mjs @@ -0,0 +1,43 @@ +export default { + "event": "project.imported", + "import": { + "filename": "ru.yml", + "format": "yml", + "inserted": 231, + "updated": 0, + "skipped": 0 + }, + "import_options": { + "replace_line_breaks": false, + "convert_placeholders": true, + "replace_modified": false, + "key_tags": [ + "tag1", "tag2" + ], + "tag_keys_inserted": true, + "tag_keys_updated": true, + "tag_keys_skipped": false, + "detect_icu_plurals": true, + "fill_empty_with_keys": false, + "hide_from_contributors": false, + "diff_by_file": false, + "use_tm": false, + "cleanup": false + }, + "project": { + "id": "138c1ffa0ad94848f01f980e7f2f2af19d1bd553", + "name": "TheApp Project", + "branch": "develop" + }, + "language": { + "id": 597, + "iso": "ru", + "name": "Russian" + }, + "user": { + "email": "jdoe@mycompany.com", + "full_name": "John Doe" + }, + "created_at": "2019-07-29 12:18:31", + "created_at_timestamp": 1564395511 +} \ No newline at end of file diff --git a/components/looker/README.md b/components/looker/README.md index e2366fb0350b5..16137d4b43f52 100644 --- a/components/looker/README.md +++ b/components/looker/README.md @@ -1,12 +1,11 @@ # Overview -With Looker, you can easily build performant, dynamic applications that look -great and provide value to your users. Here are some examples of what you can -build using the Looker API: - -- A data exploration tool that lets users quickly and easily find the data they - need -- A reporting application that automatically generates reports based on - user-defined criteria -- An analytics dashboard that gives users insights into their data in real-time -- A data visualization tool that makes it easy to understand complex data sets +The Looker API provides programmatic access to Looker's data exploration functionalities, allowing you to tap into your data analytics and share insights across your organization dynamically. With the Looker API on Pipedream, you can automate reporting, synchronize analytics data with other apps, and trigger data-driven workflows. Whether it's scheduling reports, updating dashboards, or integrating data insights into other systems, the Looker API empowers you to make data-informed decisions efficiently. + +# Example Use Cases + +- **Scheduled Report Dispatch**: Configure a Pipedream workflow that triggers at regular intervals, fetches the latest reports from Looker, and sends them via email or Slack. This automation ensures stakeholders receive timely analytics updates without manual intervention. + +- **Dashboard Activity Monitoring**: Set up a Pipedream workflow that uses the Looker API to monitor changes or views on specific dashboards. Whenever there's a significant change, the workflow could trigger notifications or log the event in a tool like Google Sheets or a project management app like Trello, helping teams stay aligned with the latest data insights. + +- **Data Sync Between Looker and CRM**: Create a Pipedream workflow that regularly pulls data from Looker and updates the records in a Customer Relationship Management (CRM) system like Salesforce. This ensures that sales and marketing teams have access to the latest data analytics for better customer engagement and personalized experiences. diff --git a/components/looker_studio/README.md b/components/looker_studio/README.md new file mode 100644 index 0000000000000..9e18068f70f67 --- /dev/null +++ b/components/looker_studio/README.md @@ -0,0 +1,11 @@ +# Overview + +The Looker Studio API (formerly Google Data Studio) provides programmatic access to manage and create compelling reports and dashboards based on diverse data sources. Integrating Looker Studio with Pipedream allows for automation of reporting tasks, real-time data updates, and enhanced collaboration through streamlined sharing and updating of analytics insights across various platforms and apps. This API can facilitate decision-making processes by providing current data-driven insights directly into the tools your team uses daily. + +# Example Use Cases + +- **Automated Report Updates and Sharing**: Set up a Pipedream workflow to automate the refreshing and sharing of Looker Studio reports. For example, automatically update sales data every hour and share the updated report via Slack or email to keep team members informed in real-time. + +- **Dynamic Data Integration from Multiple Sources**: Utilize a workflow that periodically fetches data from multiple sources like Salesforce, Google Sheets, and SQL databases, merges this data, and updates a Looker Studio dashboard to reflect new insights. This is especially useful for cross-platform performance analysis in marketing or sales operations. + +- **Error Logging and Notification System**: Create a Pipedream workflow that monitors your Looker Studio dashboards for anomalies or errors in the data (like sudden drops in key metrics). If an anomaly is detected, the workflow can trigger notifications to administrators via SMS (using Twilio) or email, prompting immediate attention and action. diff --git a/components/looker_studio/looker_studio.app.mjs b/components/looker_studio/looker_studio.app.mjs new file mode 100644 index 0000000000000..55b845ed25ab7 --- /dev/null +++ b/components/looker_studio/looker_studio.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "looker_studio", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/looker_studio/package.json b/components/looker_studio/package.json new file mode 100644 index 0000000000000..1d32eee97d4f5 --- /dev/null +++ b/components/looker_studio/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/looker_studio", + "version": "0.0.1", + "description": "Pipedream Looker Studio Components", + "main": "looker_studio.app.mjs", + "keywords": [ + "pipedream", + "looker_studio" + ], + "homepage": "https://pipedream.com/apps/looker_studio", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/loomio/README.md b/components/loomio/README.md index e29d57ce555b0..4c7314f4930f6 100644 --- a/components/loomio/README.md +++ b/components/loomio/README.md @@ -1,12 +1,11 @@ # Overview -Loomio is a tool that helps groups make better decisions together. With Loomio, -you can easily and efficiently hold discussions, make decisions, and take -action as a group. +The Loomio API empowers you to automate your collaborative decision-making processes, integrating your group discussions, proposals, and decisions with other apps and services. By leveraging Pipedream's capabilities, you can seamlessly connect Loomio with hundreds of other applications, creating custom workflows that trigger actions based on events in Loomio, such as new discussions, votes, or comments. This enables you to streamline communication, enhance project management, and foster transparent governance. -Here are some examples of what you can build using the Loomio API: +# Example Use Cases -- A tool for group decision-making -- A forum for group discussion -- A decision-making tool for teams -- A tool for collaborative action planning +- **Automated Project Management Updates**: Sync Loomio decisions to a project management tool like Trello or Asana. Whenever a decision is reached in Loomio, a Pipedream workflow automatically updates the status of a task or creates a new task in your project management app, ensuring that your project boards reflect the latest consensus. + +- **Real-time Notification Digests**: Compile and send daily or weekly digests of Loomio activity to Slack or Microsoft Teams. Pipedream can aggregate new discussions, proposals, and outcomes, formatting them into a digest message, so your team stays informed without constant interruptions. + +- **Dynamic Polls and Feedback Loop**: Integrate Loomio with Google Forms to initiate a feedback loop. Create a workflow where a new form response triggers a Loomio poll, and once the poll concludes, the results inform the next set of questions on Google Forms. This creates a dynamic system for gathering and acting on group feedback. diff --git a/components/loomio/package.json b/components/loomio/package.json new file mode 100644 index 0000000000000..0e544ad131af8 --- /dev/null +++ b/components/loomio/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/loomio", + "version": "0.6.0", + "description": "Pipedream loomio Components", + "main": "loomio.app.mjs", + "keywords": [ + "pipedream", + "loomio" + ], + "homepage": "https://pipedream.com/apps/loomio", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/loop_returns/README.md b/components/loop_returns/README.md new file mode 100644 index 0000000000000..75229a825f058 --- /dev/null +++ b/components/loop_returns/README.md @@ -0,0 +1,11 @@ +# Overview + +The Loop Returns API enables merchants to automate and streamline their returns and exchanges process. It offers endpoints that allow you to initiate returns, update return states, and manage return-related data, all programmable to fit into your existing e-commerce and customer service workflows. With Pipedream, you can trigger workflows based on events in Loop, or use actions to call the Loop API directly, automating tasks like syncing return data with customer service platforms, updating inventory management systems, or even issuing refunds. + +# Example Use Cases + +- **Sync Returns with Customer Support Tickets**: When a return is initiated in Loop, a Pipedream workflow can automatically create a support ticket in a service like Zendesk. This helps support teams track returns alongside customer queries, ensuring nothing slips through the cracks. + +- **Automate Refund Processing**: Configure a Pipedream workflow to listen for completed returns in Loop, then use the Stripe app to process refunds. This would cut down manual work for your finance team and speed up the refund process for customers. + +- **Update Inventory in Real-Time**: Use a Pipedream workflow to react to a return event from Loop, updating inventory counts in a platform like Shopify. Keeping inventory levels accurate helps with sales planning and reduces the chance of overselling a product that was recently returned. diff --git a/components/loopify/README.md b/components/loopify/README.md new file mode 100644 index 0000000000000..4180ae2cfd0fe --- /dev/null +++ b/components/loopify/README.md @@ -0,0 +1,11 @@ +# Overview + +Loopify API in Pipedream opens doors to creative marketing automation and communication flows. With its suite of tools, you can craft personalized emails, manage campaigns, and handle customer data. Pipedream's serverless platform empowers you to build workflows that react to events in Loopify or to connect with other services, making tasks more streamlined. Whether you're syncing subscriber lists, triggering email sequences, or analyzing campaign performance, Pipedream makes these integrations seamless and scalable. + +# Example Use Cases + +- **Sync New Subscribers to CRM**: When a new subscriber is added in Loopify, automatically add their details to your CRM platform, like Salesforce. This workflow ensures that your sales team always has the latest leads to work with, without manual data entry. + +- **Trigger Personalized Email Campaigns**: Kick off a personalized email campaign in Loopify based on user activity from your app. If a user signs up, completes a purchase, or reaches a milestone, a Pipedream workflow can send this data to Loopify to dispatch a tailored email, enhancing customer engagement. + +- **Analyze Campaign Performance**: Gather campaign performance data from Loopify and send it to a Google Sheet for analysis. By setting up a workflow in Pipedream that pulls campaign stats and populates them into a spreadsheet, you can observe trends and measure the effectiveness of your marketing strategies over time. diff --git a/components/loopify/actions/add-contact-to-new-entry/add-contact-to-new-entry.mjs b/components/loopify/actions/add-contact-to-new-entry/add-contact-to-new-entry.mjs new file mode 100644 index 0000000000000..3ac3d558adbbd --- /dev/null +++ b/components/loopify/actions/add-contact-to-new-entry/add-contact-to-new-entry.mjs @@ -0,0 +1,54 @@ +import app from "../../loopify.app.mjs"; + +export default { + key: "loopify-add-contact-to-new-entry", + name: "Add Contact To New Entry", + description: "Adds a contact to an **API Entry** block in a Loopify flow. [See the documentation](https://api.loopify.com/docs/index.html#tag/Flows/operation/addContactToNewEntry)", + version: "0.0.1", + type: "action", + props: { + app, + flowId: { + propDefinition: [ + app, + "flowId", + ], + }, + contactId: { + propDefinition: [ + app, + "contactId", + ], + }, + }, + methods: { + addContactToNewEntryBlock({ + flowId, ...args + } = {}) { + return this.app.post({ + path: `/flows/${flowId}/new-entry`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + addContactToNewEntryBlock, + flowId, + contactId, + } = this; + + const response = await addContactToNewEntryBlock({ + $, + flowId, + data: { + contactIds: [ + contactId, + ], + }, + }); + + $.export("$summary", `Successfully added contact to the New Entry Block in the Loopify flow with status \`${response.status}\``); + return response; + }, +}; diff --git a/components/loopify/actions/create-update-contact/create-update-contact.mjs b/components/loopify/actions/create-update-contact/create-update-contact.mjs new file mode 100644 index 0000000000000..1bc4dfaec5a7c --- /dev/null +++ b/components/loopify/actions/create-update-contact/create-update-contact.mjs @@ -0,0 +1,150 @@ +import app from "../../loopify.app.mjs"; + +export default { + key: "loopify-create-update-contact", + name: "Create Or Update Contact", + description: "Creates or updates a contact in Loopify. If the contact exists, it will be updated; otherwise, a new contact will be created. [See the documentation](https://api.loopify.com/docs/index.html#tag/Contacts)", + version: "0.0.1", + type: "action", + props: { + app, + email: { + type: "string", + label: "Email", + description: "The email address of the contact", + }, + externalId: { + type: "string", + label: "External ID", + description: "The external ID of the contact", + optional: true, + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the contact", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the contact", + optional: true, + }, + mobile: { + type: "string", + label: "Mobile", + description: "The mobile number of the contact", + optional: true, + }, + title: { + type: "string", + label: "Title", + description: "The title of the contact", + optional: true, + }, + companyName: { + type: "string", + label: "Company Name", + description: "The company name of the contact", + optional: true, + }, + address: { + type: "string", + label: "Address", + description: "The address of the contact", + optional: true, + }, + zip: { + type: "string", + label: "Zip", + description: "The zip code of the contact", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "The city of the contact", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "The country of the contact", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "The state of the contact", + optional: true, + }, + leadScore: { + type: "integer", + label: "Lead Score", + description: "The lead score of the contact", + optional: true, + }, + }, + methods: { + createContact(args = {}) { + return this.app.post({ + path: "/contacts", + ...args, + }); + }, + updateContact({ + contactId, ...args + } = {}) { + return this.app.put({ + path: `/contacts/${contactId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + app, + createContact, + updateContact, + email, + ...props + } = this; + + const { contacts } = await app.getContacts({ + $, + params: { + search: email, + }, + }); + + const [ + contact, + ] = contacts; + + if (contact) { + const response = await updateContact({ + $, + contactId: contact._id, + data: { + email, + ...props, + }, + }); + + $.export("$summary", `Successfully updated contact with ID \`${contact._id}\``); + return response; + } + + const response = await createContact({ + $, + data: { + email, + ...props, + }, + }); + + $.export("$summary", `Successfully created contact with ID \`${response._id}\``); + return response; + }, +}; diff --git a/components/loopify/loopify.app.mjs b/components/loopify/loopify.app.mjs new file mode 100644 index 0000000000000..7485dbee37f59 --- /dev/null +++ b/components/loopify/loopify.app.mjs @@ -0,0 +1,88 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "loopify", + propDefinitions: { + flowId: { + type: "string", + label: "Flow ID", + description: "The ID of the flow", + useQuery: true, + async options({ query: search }) { + const { documents } = await this.flowsSearch({ + data: { + search, + }, + }); + return documents.map(({ + _id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + contactId: { + type: "string", + label: "Contact ID", + description: "The ID of the contact", + async options({ page }) { + const { contacts } = await this.getContacts({ + params: { + pageSize: 50, + pageIndex: page + 1, + }, + }); + return contacts.map(({ + _id: value, firstName, lastName, email, + }) => ({ + label: `${firstName} ${lastName} ${email}`.trim() || value, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.loopify.com"; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this._baseUrl() + path, + headers: { + ...headers, + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "Content-Type": "application/json", + }, + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + put(args = {}) { + return this._makeRequest({ + method: "PUT", + ...args, + }); + }, + flowsSearch(args = {}) { + return this.post({ + path: "/flows/search", + ...args, + }); + }, + getContacts(args = {}) { + return this._makeRequest({ + path: "/contacts", + ...args, + }); + }, + }, +}; diff --git a/components/loopify/package.json b/components/loopify/package.json new file mode 100644 index 0000000000000..ccae6f8ef3dc8 --- /dev/null +++ b/components/loopify/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/loopify", + "version": "0.1.0", + "description": "Pipedream Loopify Components", + "main": "loopify.app.mjs", + "keywords": [ + "pipedream", + "loopify" + ], + "homepage": "https://pipedream.com/apps/loopify", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/loopmessage/README.md b/components/loopmessage/README.md new file mode 100644 index 0000000000000..7cbb4de007d06 --- /dev/null +++ b/components/loopmessage/README.md @@ -0,0 +1,11 @@ +# Overview + +The LoopMessage API offers you the ability to send, receive, and manage messages within your applications. Through Pipedream's integration, you can harness this API to automate communication processes, organize message flows, and even connect to various data sources or other APIs to create complex messaging workflows. With Pipedream, you can trigger actions based on events, schedule messages, and interact with users in real-time without managing servers or infrastructure. + +# Example Use Cases + +- **Automated Customer Support Tickets**: Automatically create customer support tickets in your helpdesk software when a message is received via LoopMessage. Use this to streamline support queries and ensure no customer message is overlooked. + +- **Scheduled Notifications**: Send out scheduled notifications to users or groups based on certain triggers or timeframes. This could be used for reminders, promotional campaigns, or important updates, integrating with services like Google Calendar for event-driven alerts. + +- **Real-Time Data Alerts**: Combine LoopMessage with a monitoring service like Datadog to send real-time alerts via messages when a specific metric threshold is breached. This can be crucial for uptime monitoring and incident management. diff --git a/components/loops_so/README.md b/components/loops_so/README.md new file mode 100644 index 0000000000000..e004915f5c475 --- /dev/null +++ b/components/loops_so/README.md @@ -0,0 +1,11 @@ +# Overview + +The Loops.so API enables you to automate repetitive tasks, create custom integrations, and manage video campaigns directly from Pipedream. It offers endpoints to manage videos, incorporate user-generated content, and track metrics. On Pipedream, you can connect Loops.so with a multitude of other services for seamless data flow across platforms, enabling efficient automation of content distribution, marketing analytics, and customer engagement workflows. + +# Example Use Cases + +- **Automate Video Content Publishing**: Trigger a workflow on Pipedream when a new video is added to Loops.so to automatically publish the video to your social media platforms like Twitter or Facebook using their respective APIs. + +- **User-Generated Content Moderation**: Integrate Loops.so with a sentiment analysis service. When a new video is submitted, analyze the content for appropriate sentiment or detect any unwanted content, and approve or reject the video submission accordingly. + +- **Engagement and Analytics Reporting**: Connect Loops.so with Google Sheets. For each new video, log metrics such as views, engagement, and interactions into a spreadsheet to generate weekly performance reports and gain insights. diff --git a/components/loops_so/actions/common/common-create-update.mjs b/components/loops_so/actions/common/common-create-update.mjs index 2e591c4883cc3..e1f9a82c497d0 100644 --- a/components/loops_so/actions/common/common-create-update.mjs +++ b/components/loops_so/actions/common/common-create-update.mjs @@ -1,3 +1,6 @@ +/* eslint-disable no-unused-vars */ +import pickBy from "lodash.pickby"; +import { parseObject } from "../../common/utils.mjs"; import loops from "../../loops_so.app.mjs"; export default { @@ -31,12 +34,36 @@ export default { "lastName", ], }, + source: { + propDefinition: [ + loops, + "source", + ], + }, + subscribed: { + propDefinition: [ + loops, + "subscribed", + ], + }, userGroup: { propDefinition: [ loops, "userGroup", ], }, + userId: { + propDefinition: [ + loops, + "userId", + ], + }, + mailingLists: { + propDefinition: [ + loops, + "mailingLists", + ], + }, customFields: { propDefinition: [ loops, @@ -44,4 +71,24 @@ export default { ], }, }, + methods: { + prepareData() { + const { + loops, + customFields, + mailingLists, + ...data + } = this; + + const mailingListObject = {}; + for (const item of (parseObject(mailingLists) || [])) { + mailingListObject[item] = true; + } + + return pickBy({ + mailingLists: mailingListObject, + ...data, + }); + }, + }, }; diff --git a/components/loops_so/actions/create-contact/create-contact.mjs b/components/loops_so/actions/create-contact/create-contact.mjs index b9a009d4ba4f3..d03452435b8f3 100644 --- a/components/loops_so/actions/create-contact/create-contact.mjs +++ b/components/loops_so/actions/create-contact/create-contact.mjs @@ -5,20 +5,11 @@ export default { key: "loops_so-create-contact", name: "Create Contact", description: "Creates a new contact. [See the Documentation](https://loops.so/docs/add-users/api-reference#add)", - version: "0.1.0", + version: "0.2.0", type: "action", async run({ $ }) { - const { // eslint-disable-next-line no-unused-vars - loops, email, firstName, lastName, userGroup, customFields, ...data - } = this; - const response = await loops.createContact({ - data: { - email, - firstName, - lastName, - userGroup, - ...data, - }, + const response = await this.loops.createContact({ + data: this.prepareData(), $, }); diff --git a/components/loops_so/actions/delete-contact/delete-contact.mjs b/components/loops_so/actions/delete-contact/delete-contact.mjs new file mode 100644 index 0000000000000..42e413253270f --- /dev/null +++ b/components/loops_so/actions/delete-contact/delete-contact.mjs @@ -0,0 +1,54 @@ +import { ConfigurationError } from "@pipedream/platform"; +import loops from "../../loops_so.app.mjs"; + +export default { + key: "loops_so-delete-contact", + name: "Delete Contact", + description: "Delete an existing contact. [See the documentation](https://loops.so/docs/api-reference/delete-contact)", + version: "0.0.2", + type: "action", + props: { + loops, + infoAlert: { + type: "alert", + alertType: "info", + content: "You can provide either the contact's email address or user ID.", + }, + email: { + propDefinition: [ + loops, + "email", + ], + optional: true, + }, + userId: { + type: "string", + label: "User ID", + description: "User ID of the contact", + optional: true, + }, + }, + async run({ $ }) { + const { + loops, email, userId, + } = this; + if (!email && !userId) { + throw new ConfigurationError("You must provide either the contact's email address or user ID."); + } + const response = await loops.deleteContact({ + $, + data: { + email, + userId, + }, + }); + + const summary = response?.success + ? "Successfully deleted contact" + : "Failed to delete contact"; + + $.export("$summary", summary); + + return response; + }, +}; diff --git a/components/loops_so/actions/find-contact/find-contact.mjs b/components/loops_so/actions/find-contact/find-contact.mjs index 1bad05b2ddf59..9fba12feeda25 100644 --- a/components/loops_so/actions/find-contact/find-contact.mjs +++ b/components/loops_so/actions/find-contact/find-contact.mjs @@ -4,7 +4,7 @@ export default { key: "loops_so-find-contact", name: "Find Contact", description: "Search for a contact by email address. [See the Documentation](https://loops.so/docs/add-users/api-reference#find)", - version: "0.0.2", + version: "0.0.4", type: "action", props: { loops, diff --git a/components/loops_so/actions/list-custom-fields/list-custom-fields.mjs b/components/loops_so/actions/list-custom-fields/list-custom-fields.mjs new file mode 100644 index 0000000000000..f622635091b91 --- /dev/null +++ b/components/loops_so/actions/list-custom-fields/list-custom-fields.mjs @@ -0,0 +1,21 @@ +import loops from "../../loops_so.app.mjs"; + +export default { + key: "loops_so-list-custom-fields", + name: "List Custom Fields", + description: "List your account's custom contact properties. [See the documentation](https://loops.so/docs/api-reference/list-custom-fields)", + version: "0.0.2", + type: "action", + props: { + loops, + }, + async run({ $ }) { + const response = await this.loops.listCustomFields({ + $, + }); + + $.export("$summary", `Successfully retrieved ${response.length} custom fields`); + + return response; + }, +}; diff --git a/components/loops_so/actions/list-mailing-lists/list-mailing-lists.mjs b/components/loops_so/actions/list-mailing-lists/list-mailing-lists.mjs new file mode 100644 index 0000000000000..48530bddebf2d --- /dev/null +++ b/components/loops_so/actions/list-mailing-lists/list-mailing-lists.mjs @@ -0,0 +1,21 @@ +import loops from "../../loops_so.app.mjs"; + +export default { + key: "loops_so-list-mailing-lists", + name: "List Mailing Lists", + description: "List your account's mailing lists. [See the documentation](https://loops.so/docs/api-reference/list-mailing-lists)", + version: "0.0.2", + type: "action", + props: { + loops, + }, + async run({ $ }) { + const response = await this.loops.listMailingLists({ + $, + }); + + $.export("$summary", `Successfully retrieved ${response.length} mailing lists`); + + return response; + }, +}; diff --git a/components/loops_so/actions/send-event/send-event.mjs b/components/loops_so/actions/send-event/send-event.mjs index ccd8d2e947527..230148ba3a31d 100644 --- a/components/loops_so/actions/send-event/send-event.mjs +++ b/components/loops_so/actions/send-event/send-event.mjs @@ -4,7 +4,7 @@ export default { key: "loops_so-send-event", name: "Send Event", description: "Send an event to an email address. [See the Documentation](https://loops.so/docs/add-users/api-reference#send)", - version: "0.0.2", + version: "0.0.4", type: "action", props: { loops, diff --git a/components/loops_so/actions/send-transactional-email/send-transactional-email.mjs b/components/loops_so/actions/send-transactional-email/send-transactional-email.mjs index a2776e17ba440..d964d899749f3 100644 --- a/components/loops_so/actions/send-transactional-email/send-transactional-email.mjs +++ b/components/loops_so/actions/send-transactional-email/send-transactional-email.mjs @@ -4,7 +4,7 @@ export default { key: "loops_so-send-transactional-email", name: "Send Transactional Email", description: "Send a transactional email. [See the Documentation](https://loops.so/docs/transactional/guide#send-your-email)", - version: "0.0.2", + version: "0.0.4", type: "action", props: { loops, diff --git a/components/loops_so/actions/update-contact/update-contact.mjs b/components/loops_so/actions/update-contact/update-contact.mjs index e4c6187d246ed..fee9d3bd85561 100644 --- a/components/loops_so/actions/update-contact/update-contact.mjs +++ b/components/loops_so/actions/update-contact/update-contact.mjs @@ -1,25 +1,15 @@ import common from "../common/common-create-update.mjs"; -import pickBy from "lodash.pickby"; export default { ...common, key: "loops_so-update-contact", name: "Update Contact", description: "Updates an existing contact by email. If email not found, a new contact will be created. [See the Documentation](https://loops.so/docs/add-users/api-reference#update)", - version: "0.1.0", + version: "0.2.0", type: "action", async run({ $ }) { - const { // eslint-disable-next-line no-unused-vars - loops, email, firstName, lastName, userGroup, customFields, ...data - } = this; - const response = await loops.updateContact({ - data: pickBy({ - email, - firstName, - lastName, - userGroup, - ...data, - }), + const response = await this.loops.updateContact({ + data: this.prepareData(), $, }); diff --git a/components/loops_so/common/utils.mjs b/components/loops_so/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/loops_so/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/loops_so/loops_so.app.mjs b/components/loops_so/loops_so.app.mjs index 844bb80ecc58f..be6e9edf07dd4 100644 --- a/components/loops_so/loops_so.app.mjs +++ b/components/loops_so/loops_so.app.mjs @@ -21,12 +21,46 @@ export default { description: "The last name of the contact", optional: true, }, + source: { + type: "string", + label: "Source", + description: "A custom source value to replace the default **API**. [Read more](https://loops.so/docs/contacts/properties#source).", + optional: true, + }, + subscribed: { + type: "boolean", + label: "Subscribed", + description: "Whether the contact will receive campaign and loops emails. [Read more](https://loops.so/docs/contacts/properties#subscribed).", + optional: true, + }, userGroup: { type: "string", label: "User Group", description: "User group of the contact", optional: true, }, + userId: { + type: "string", + label: "User Id", + description: "A unique user ID (for example, from an external application). [Read more](https://loops.so/docs/contacts/properties#user-id).", + optional: true, + }, + mailingLists: { + type: "string[]", + label: "Mailing Lists", + description: "A list of mailing list IDs", + async options() { + const data = await this.listMailingLists(); + return data.map(({ + id: value, + name: label, + }) => ({ + label, + value, + })); + }, + optional: true, + }, customFields: { type: "string[]", label: "Custom Fields", @@ -85,6 +119,13 @@ export default { ...args, }); }, + deleteContact(args = {}) { + return this._makeRequest({ + path: "/contacts/delete", + method: "POST", + ...args, + }); + }, sendEvent(args = {}) { return this._makeRequest({ path: "/events/send", @@ -105,5 +146,11 @@ export default { ...args, }); }, + listMailingLists(args = {}) { + return this._makeRequest({ + path: "/lists", + ...args, + }); + }, }, }; diff --git a/components/loops_so/package.json b/components/loops_so/package.json index e979764dbc906..8dc67ee930a14 100644 --- a/components/loops_so/package.json +++ b/components/loops_so/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/loops_so", - "version": "0.2.0", + "version": "0.3.1", "description": "Pipedream Loops.so Components", "main": "loops_so.app.mjs", "keywords": [ @@ -13,7 +13,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.5.1", + "@pipedream/platform": "^3.0.3", "lodash.pickby": "^4.6.0" } } diff --git a/components/loqate/README.md b/components/loqate/README.md new file mode 100644 index 0000000000000..29b63273b294f --- /dev/null +++ b/components/loqate/README.md @@ -0,0 +1,11 @@ +# Overview + +The Loqate API offers a robust toolkit for location data services including address verification, geocoding, and autocomplete. When leveraged within Pipedream, users can automate workflows involving data enrichment, validation, and location intelligence. You can seamlessly integrate real-time address validation in your e-commerce checkout process, enrich user profiles with geographic data, or automate the updating of an address database. + +# Example Use Cases + +- **Address Validation for E-Commerce Checkout**: In an e-commerce platform, use the Loqate API to validate customer shipping addresses at checkout. Trigger a workflow on Pipedream when a new order is placed, validate the address with Loqate, and only proceed with the order processing if the address is confirmed as valid. + +- **Enrich User Profiles with Location Data**: When a new user signs up on your platform, automatically enrich their profile with geocoded data. Using a webhook to capture signup events, a Pipedream workflow calls the Loqate API to add precise location information to the user's profile, enhancing personalization and enabling location-based services. + +- **Database Cleanup with Batch Address Verification**: Schedule a Pipedream workflow to periodically run through your database of contact addresses, using the Loqate API for batch verification. Update records with validated addresses and flag those that require manual review, ensuring your mailing lists and contact information are accurate and up-to-date. diff --git a/components/loyaltylion/README.md b/components/loyaltylion/README.md index fa9c014df6432..0c5f32cdb36d6 100644 --- a/components/loyaltylion/README.md +++ b/components/loyaltylion/README.md @@ -1,15 +1,11 @@ # Overview -LoyaltyLion is a platform that enables businesses to create and manage loyalty -programs. With the LoyaltyLion API, businesses can create and manage loyalty -programs, as well as track and analyze customer behavior. - -Some examples of what you can build with the LoyaltyLion API include: - -- A loyalty program for your business -- A customer loyalty program for your website or app -- A rewards program for your customers -- A points program for your customers -- A referral program for your customers -- A VIP program for your customers -- A loyalty program for your employees +The LoyaltyLion API unlocks the potential to create custom loyalty and engagement experiences by integrating with various e-commerce platforms. Harnessing this API through Pipedream allows for automated workflows that can sync customer data, track points and rewards, and personalize marketing efforts based on loyalty insights. With the API, you can automate notifications for point changes, create rules for earning points, and analyze customer behavior to tailor rewards. + +# Example Use Cases + +- **Sync Loyalty Data with Email Marketing Platforms**: Automatically enroll customers in targeted email campaigns based on their loyalty status by syncing LoyaltyLion data with email marketing apps like Mailchimp. When a customer reaches a new loyalty tier, trigger a Pipedream workflow to add them to a specific Mailchimp audience segment, ensuring they receive personalized emails. + +- **Automate Rewards Redemption Process**: Connect LoyaltyLion to Shopify using Pipedream workflows to automate the rewards redemption process. Upon a customer redeeming points for a coupon, Pipedream can detect this event and generate a unique discount code in Shopify, emailing it directly to the customer, providing a seamless rewards experience. + +- **Personalize Customer Service Based on Loyalty Data**: Leverage customer loyalty information by integrating LoyaltyLion with a CRM platform such as Salesforce. When a customer submits a support ticket, a Pipedream workflow can enrich the ticket with loyalty data, allowing customer service reps to tailor their support and prioritize tickets based on customer value. diff --git a/components/loyaltylion/package.json b/components/loyaltylion/package.json new file mode 100644 index 0000000000000..cc546c90ec64a --- /dev/null +++ b/components/loyaltylion/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/loyaltylion", + "version": "0.6.0", + "description": "Pipedream loyaltylion Components", + "main": "loyaltylion.app.mjs", + "keywords": [ + "pipedream", + "loyaltylion" + ], + "homepage": "https://pipedream.com/apps/loyaltylion", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/loyjoy/README.md b/components/loyjoy/README.md new file mode 100644 index 0000000000000..3fe9397b55df4 --- /dev/null +++ b/components/loyjoy/README.md @@ -0,0 +1,11 @@ +# Overview + +The LoyJoy API allows you to engage with customers through conversational marketing. You can orchestrate chats, manage users, and track customer interactions and data in real-time. With Pipedream, you can harness this API to create automated workflows that respond to customer inputs, update CRM records, send personalized notifications, and more, all using event-driven serverless architecture. + +# Example Use Cases + +- **Automated Customer Support Workflow**: Set up a Pipedream workflow that listens for new customer queries through the LoyJoy API. When a new query is detected, leverage AI services like OpenAI to interpret the request and fetch relevant information from a database or Knowledge Base to provide instant responses. + +- **CRM Integration for Lead Tracking**: Create a Pipedream workflow that integrates LoyJoy chats with your CRM platform, such as Salesforce or HubSpot. When a new lead is captured via LoyJoy, automatically add or update their details in your CRM, ensuring your sales team has the latest information. + +- **Personalized Marketing Campaigns**: Use a Pipedream workflow to analyze customer interactions from LoyJoy chats and segment users based on their interests or behavior. Connect with a marketing platform like Mailchimp to send targeted email campaigns or push notifications to these segments, improving engagement and conversion rates. diff --git a/components/loyjoy/package.json b/components/loyjoy/package.json index 48cae7a74a335..51e8b44f50d5b 100644 --- a/components/loyjoy/package.json +++ b/components/loyjoy/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/loyverse/README.md b/components/loyverse/README.md new file mode 100644 index 0000000000000..67485b87d1fb5 --- /dev/null +++ b/components/loyverse/README.md @@ -0,0 +1,11 @@ +# Overview + +The Loyverse API provides a suite of functions to access and manage the data for businesses using the Loyverse platform, which includes point of sale, inventory management, employee management, and analytics. With Pipedream, you can create automated workflows that trigger on new events from Loyverse, like a new sale, or that perform operations in Loyverse, like creating a new item in your inventory. By connecting Loyverse to other apps via Pipedream, you can sync data across platforms, get real-time notifications, and streamline operations. + +# Example Use Cases + +- **Automatic Inventory Replenishment**: When stock levels for items fall below a certain threshold in Loyverse, use Pipedream to automatically reorder stock from suppliers by sending purchase orders via email or directly to a supplier's order management system. + +- **Sales Data Analysis and Visualization**: Send new sale transactions from Loyverse to Google Sheets or a BI tool like Tableau using Pipedream. This allows for advanced analysis of sales data, trends, and can help in creating visual reports for better decision-making. + +- **Real-time Notifications for High-Value Transactions**: Set up a Pipedream workflow that captures transactions over a certain amount from Loyverse and sends instant notifications to a Slack channel or via SMS. This can help in quickly recognizing and rewarding high-value sales or identifying potential errors or fraudulent activity. diff --git a/components/loyverse/actions/create-receipt/create-receipt.mjs b/components/loyverse/actions/create-receipt/create-receipt.mjs new file mode 100644 index 0000000000000..24bd26ac7a3d6 --- /dev/null +++ b/components/loyverse/actions/create-receipt/create-receipt.mjs @@ -0,0 +1,100 @@ +import loyverse from "../../loyverse.app.mjs"; +import { parseAsJSON } from "../../common/utils.mjs"; + +export default { + key: "loyverse-create-receipt", + name: "Create Receipt", + description: "Creates a new receipt for a specific store. [See the documentation](https://developer.loyverse.com/docs/#tag/Receipts/paths/~1receipts/post)", + version: "0.0.1", + type: "action", + props: { + loyverse, + storeId: { + propDefinition: [ + loyverse, + "storeId", + ], + }, + lineItems: { + type: "string[]", + label: "Line Items", + description: "[An array of JSON-stringified objects](https://developer.loyverse.com/docs/#tag/Receipts/paths/~1receipts/post). You can use the **Generate Receipt Items** action to generate these items.", + }, + paymentTypeId: { + propDefinition: [ + loyverse, + "paymentTypeId", + ], + }, + employeeId: { + propDefinition: [ + loyverse, + "employeeId", + ], + }, + order: { + type: "string", + label: "Order", + description: "The order name or number associated with the receipt", + optional: true, + }, + customerId: { + propDefinition: [ + loyverse, + "customerId", + ], + description: "Select a customer or provide a customer ID.", + }, + source: { + type: "string", + label: "Source", + description: "The name of the source this receipt comes from. By default it is the name of the application that created the receipt.", + optional: true, + }, + receiptDate: { + type: "string", + label: "Receipt Date", + description: "A date/time string such as `2022-03-15T18:30:00Z`. By default, this is the date/time the receipt was created.", + optional: true, + }, + totalDiscounts: { + type: "string[]", + label: "Total Discounts", + description: "The list of all discounts applied in the receipt, as JSON-stringified objects. [See the documentation](https://developer.loyverse.com/docs/#tag/Receipts/paths/~1receipts/post) for the expected properties.", + optional: true, + }, + note: { + type: "string", + label: "Note", + description: "The receipt's note", + optional: true, + }, + }, + async run({ $ }) { + const { + lineItems, totalDiscounts, + } = this; + const discounts = totalDiscounts?.map?.(parseAsJSON); + const response = await this.loyverse.createReceipt({ + $, + data: { + store_id: this.storeId, + line_items: lineItems.map?.(parseAsJSON) ?? JSON.parse(lineItems), + payments: [ + { + payment_type_id: this.paymentTypeId, + }, + ], + employee_id: this.employeeId, + order: this.order, + customer_id: this.customerId, + source: this.source, + receipt_date: this.receiptDate, + total_discounts: discounts ?? (totalDiscounts && JSON.parse(totalDiscounts)), + note: this.note, + }, + }); + $.export("$summary", `Successfully created receipt with number ${response.receipt_number}`); + return response; + }, +}; diff --git a/components/loyverse/actions/generate-receipt-items/generate-receipt-items.mjs b/components/loyverse/actions/generate-receipt-items/generate-receipt-items.mjs new file mode 100644 index 0000000000000..68379e5d8e5a6 --- /dev/null +++ b/components/loyverse/actions/generate-receipt-items/generate-receipt-items.mjs @@ -0,0 +1,90 @@ +import loyverse from "../../loyverse.app.mjs"; + +export default { + key: "loyverse-generate-receipt-items", + name: "Generate Receipt Items", + description: "Generates the data for items to use in a receipt. [See the documentation](https://developer.loyverse.com/docs/#tag/Receipts/paths/~1receipts/post)", + version: "0.0.1", + type: "action", + props: { + loyverse, + lineItems: { + type: "string[]", + label: "Line Items", + description: "[An array of JSON-stringified objects](https://developer.loyverse.com/docs/#tag/Receipts/paths/~1receipts/post). You can use the props below to generate each item and copy it into this array.", + }, + storeId: { + propDefinition: [ + loyverse, + "storeId", + ], + reloadProps: true, + }, + itemVariantId: { + propDefinition: [ + loyverse, + "itemVariantId", + ], + reloadProps: true, + }, + quantity: { + type: "integer", + label: "Quantity", + description: "The number of items being purchased", + reloadProps: true, + }, + price: { + type: "integer", + label: "Price", + description: "The price of one item. By default it is equal to the price of the selected item variant.", + optional: true, + reloadProps: true, + }, + cost: { + type: "integer", + label: "Cost", + description: "The cost of one item at the moment of transaction. By default it is equal to the cost of the selected item variant.", + optional: true, + reloadProps: true, + }, + note: { + type: "string", + label: "Note", + description: "The line item note.", + optional: true, + reloadProps: true, + }, + }, + additionalProps() { + const { + storeId, itemVariantId, quantity, price, cost, note, + } = this; + if ([ + storeId, + itemVariantId, + quantity, + ].includes(undefined)) { + return {}; + } + const propStr = [ + `"variant_id": "${itemVariantId}"`, + `"store_id": "${storeId}"`, + `"quantity": ${quantity}`, + `"price": ${price}`, + `"cost": ${cost}`, + `"line_note": "${note}"`, + ].filter((str) => !str.includes("undefined")).join(", "); + return { + output: { + type: "alert", + alertType: "info", + content: `\`{ ${propStr} }\``, + }, + }; + }, + async run({ $ }) { + const { lineItems } = this; + $.export("$summary", `Successfully generated ${lineItems.length} items`); + return lineItems; + }, +}; diff --git a/components/loyverse/actions/get-customer/get-customer.mjs b/components/loyverse/actions/get-customer/get-customer.mjs new file mode 100644 index 0000000000000..8e491d706e70b --- /dev/null +++ b/components/loyverse/actions/get-customer/get-customer.mjs @@ -0,0 +1,47 @@ +import loyverse from "../../loyverse.app.mjs"; + +export default { + key: "loyverse-get-customer", + name: "Get Customer(s)", + description: "Retrieves details of one or more customers. [See the documentation](https://developer.loyverse.com/docs/#tag/Customers/paths/~1customers/get)", + version: "0.0.1", + type: "action", + props: { + loyverse, + customerId: { + propDefinition: [ + loyverse, + "customerId", + ], + }, + customerAmount: { + propDefinition: [ + loyverse, + "customerAmount", + ], + }, + }, + async run({ $ }) { + const { + loyverse, customerId, customerAmount, + } = this; + let response, summary; + if (customerId) { + response = await loyverse.getCustomerDetails({ + $, + customerId, + }); + summary = `Successfully retrieved data for customer ${response.email}`; + } else { + response = await loyverse.listCustomers({ + $, + params: { + limit: customerAmount, + }, + }); + summary = `Successfully retrieved data for ${response.length} customers`; + } + $.export("$summary", summary); + return response; + }, +}; diff --git a/components/loyverse/actions/update-inventory-levels/update-inventory-levels.mjs b/components/loyverse/actions/update-inventory-levels/update-inventory-levels.mjs new file mode 100644 index 0000000000000..323d2605a6b31 --- /dev/null +++ b/components/loyverse/actions/update-inventory-levels/update-inventory-levels.mjs @@ -0,0 +1,74 @@ +import { parseAsJSON } from "../../common/utils.mjs"; +import loyverse from "../../loyverse.app.mjs"; + +export default { + key: "loyverse-update-inventory-levels", + name: "Update Inventory Levels", + description: "Batch updates the inventory levels for specific item variants. [See the documentation](https://developer.loyverse.com/docs/#tag/Inventory/paths/~1inventory/post)", + version: "0.0.1", + type: "action", + props: { + loyverse, + inventoryLevels: { + type: "string[]", + label: "Inventory Levels", + description: "[An array of JSON-stringified objects](https://developer.loyverse.com/docs/#tag/Inventory/paths/~1inventory/post). You can use the props below to generate each item and copy it into this array.", + }, + storeId: { + propDefinition: [ + loyverse, + "storeId", + ], + reloadProps: true, + }, + itemVariantId: { + propDefinition: [ + loyverse, + "itemVariantId", + ], + reloadProps: true, + }, + inStock: { + type: "integer", + label: "In Stock", + description: "The current stock at the specified store", + reloadProps: true, + }, + updatedAt: { + type: "string", + label: "Updated At", + description: "The date/time when the specified stock was calculated", + reloadProps: true, + }, + }, + additionalProps() { + const { + storeId, itemVariantId, inStock, updatedAt, + } = this; + if ([ + storeId, + itemVariantId, + inStock, + updatedAt, + ].includes(undefined)) { + return {}; + } + return { + output: { + type: "alert", + alertType: "info", + content: `\`{ "variant_id": "${itemVariantId}", "store_id": "${storeId}", "stock_after": ${inStock}, "updated_at": "${updatedAt}" }\``, + }, + }; + }, + async run({ $ }) { + const response = await this.loyverse.batchUpdateInventoryLevels({ + $, + data: { + inventory_levels: this.inventoryLevels.map(parseAsJSON), + }, + }); + $.export("$summary", "Successfully updated inventory levels"); + return response; + }, +}; diff --git a/components/loyverse/common/utils.mjs b/components/loyverse/common/utils.mjs new file mode 100644 index 0000000000000..e9e2be11d7285 --- /dev/null +++ b/components/loyverse/common/utils.mjs @@ -0,0 +1,11 @@ +import { ConfigurationError } from "@pipedream/platform"; + +export function parseAsJSON(value) { + try { + return typeof value === "string" + ? JSON.parse(value) + : value; + } catch (error) { + throw new ConfigurationError(`Invalid JSON string: ${error.message}`); + } +} diff --git a/components/loyverse/loyverse.app.mjs b/components/loyverse/loyverse.app.mjs new file mode 100644 index 0000000000000..16070a67d6ca6 --- /dev/null +++ b/components/loyverse/loyverse.app.mjs @@ -0,0 +1,211 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "loyverse", + propDefinitions: { + customerAmount: { + type: "integer", + label: "Max Amount", + description: "The maximum amount of customers you want to retrieve, starting from the most recent customers. If you provide a `Customer ID`, this will be ignored.", + optional: true, + default: 50, + min: 1, + max: 250, + }, + customerId: { + type: "string", + label: "Customer ID", + description: "Select a customer or provide a customer ID to retrieve information for. You can leave this empty to retrieve a list of customers instead.", + optional: true, + async options({ prevContext: { lastCursor } }) { + return this._paginatedOptions({ + fn: "listCustomers", + name: "customers", + map: ({ + id, name, email, + }) => ({ + label: `${name} (${email})`, + value: id, + }), + lastCursor, + }); + }, + }, + paymentTypeId: { + type: "string", + label: "Payment Type ID", + description: "Select a payment type or provide a payment type ID.", + async options({ prevContext: { lastCursor } }) { + return this._paginatedOptions({ + fn: "listPaymentTypes", + name: "payment_types", + map: ({ + id, name, + }) => ({ + label: name, + value: id, + }), + lastCursor, + }); + }, + }, + storeId: { + type: "string", + label: "Store ID", + description: "Select a store or provide a store ID.", + async options({ prevContext: { lastCursor } }) { + return this._paginatedOptions({ + fn: "listStores", + name: "stores", + map: ({ + id, name, + }) => ({ + label: name, + value: id, + }), + lastCursor, + }); + }, + }, + employeeId: { + type: "string", + label: "Employee ID", + description: "Select an employee or provide an employee ID.", + optional: true, + async options({ prevContext: { lastCursor } }) { + return this._paginatedOptions({ + fn: "listEmployees", + name: "employees", + map: ({ + id, name, email, + }) => ({ + label: `${name} (${email})`, + value: id, + }), + lastCursor, + }); + }, + }, + itemVariantId: { + type: "string", + label: "Item Variant ID", + description: "Select an item variant or provide an item variant ID.", + async options({ prevContext: { lastCursor } }) { + return this._paginatedOptions({ + fn: "listItemVariants", + name: "variants", + map: ({ + variant_id: id, sku, + }) => ({ + label: `SKU ${sku}`, + value: id, + }), + lastCursor, + }); + }, + }, + }, + methods: { + async _paginatedOptions({ + fn, name, map, lastCursor, + }) { + const { + [name]: items, cursor, + } = await this[fn]({ + params: { + cursor: lastCursor, + }, + }); + const options = items.map(map); + return { + options, + context: { + lastCursor: cursor, + }, + }; + }, + _baseUrl() { + return "https://api.loyverse.com/v1.0"; + }, + async _makeRequest({ + $ = this, + headers, + ...otherOpts + }) { + return axios($, { + ...otherOpts, + baseURL: this._baseUrl(), + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + async createReceipt(args) { + return this._makeRequest({ + method: "POST", + url: "/receipts", + ...args, + }); + }, + async batchUpdateInventoryLevels(args) { + return this._makeRequest({ + method: "POST", + url: "/inventory", + ...args, + }); + }, + async getCustomerDetails({ + customerId, ...args + }) { + return this._makeRequest({ + url: `/customers/${customerId}`, + ...args, + }); + }, + async listCustomers(args) { + return this._makeRequest({ + url: "/customers", + ...args, + }); + }, + async listStores(args) { + return this._makeRequest({ + url: "/stores", + ...args, + }); + }, + async listEmployees(args) { + return this._makeRequest({ + url: "/employees", + ...args, + }); + }, + async listPaymentTypes(args) { + return this._makeRequest({ + url: "/payment_types", + ...args, + }); + }, + async listItemVariants(args) { + return this._makeRequest({ + url: "/variants", + ...args, + }); + }, + async createWebhook(args) { + return this._makeRequest({ + method: "POST", + url: "/webhooks", + ...args, + }); + }, + async deleteWebhook(id) { + return this._makeRequest({ + method: "DELETE", + url: `/webhooks/${id}`, + }); + }, + }, +}; diff --git a/components/loyverse/package.json b/components/loyverse/package.json new file mode 100644 index 0000000000000..91513ad9b79ac --- /dev/null +++ b/components/loyverse/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/loyverse", + "version": "0.1.0", + "description": "Pipedream Loyverse Components", + "main": "loyverse.app.mjs", + "keywords": [ + "pipedream", + "loyverse" + ], + "homepage": "https://pipedream.com/apps/loyverse", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/loyverse/sources/common/common.mjs b/components/loyverse/sources/common/common.mjs new file mode 100644 index 0000000000000..f25205a409187 --- /dev/null +++ b/components/loyverse/sources/common/common.mjs @@ -0,0 +1,50 @@ +import app from "../../loyverse.app.mjs"; + +export default { + props: { + app, + http: "$.interface.http", + db: "$.service.db", + }, + methods: { + getSummary() { + return "New event"; + }, + _getWebhookId() { + return this.db.get("webhookId"); + }, + _setWebhookId(value) { + this.db.set("webhookId", value); + }, + }, + hooks: { + async activate() { + const data = { + type: this.getHookType(), + url: this.http.endpoint, + status: "ENABLED", + }; + + const { id } = await this.app.createWebhook({ + data, + }); + this._setWebhookId(id); + }, + async deactivate() { + const id = this._getWebhookId(); + if (id) { + await this.app.deleteWebhook(id); + } + }, + }, + async run({ body }) { + if (body) { + const ts = Date.now(); + this.$emit(body, { + id: ts, + summary: this.getSummary(body), + ts, + }); + } + }, +}; diff --git a/components/loyverse/sources/customer-updated-instant/customer-updated-instant.mjs b/components/loyverse/sources/customer-updated-instant/customer-updated-instant.mjs new file mode 100644 index 0000000000000..6ae83b917aae7 --- /dev/null +++ b/components/loyverse/sources/customer-updated-instant/customer-updated-instant.mjs @@ -0,0 +1,25 @@ +import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "loyverse-customer-updated-instant", + name: "Customer Updated (Instant)", + description: "Emit new event when a customer is updated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + sampleEmit, + methods: { + ...common.methods, + getSummary(body) { + const { length } = body.customers; + return `${length} customer${length === 1 + ? "" + : "s"} updated`; + }, + getHookType() { + return "customers.update"; + }, + }, +}; diff --git a/components/loyverse/sources/customer-updated-instant/test-event.mjs b/components/loyverse/sources/customer-updated-instant/test-event.mjs new file mode 100644 index 0000000000000..0916c747cb592 --- /dev/null +++ b/components/loyverse/sources/customer-updated-instant/test-event.mjs @@ -0,0 +1,29 @@ +export default { + merchant_id: "7e2daef3-48ad-4aba-b226-2d180f564f31", + type: "customers.update", + created_at: "2024-03-23T20:14:54.779Z", + customers: [ + { + id: "a4c0912a-bd29-46e4-9015-37e7cdb05344", + name: "customer name", + email: "customer@business.com", + phone_number: null, + address: null, + city: null, + region: null, + postal_code: null, + country_code: null, + note: null, + customer_code: null, + first_visit: null, + last_visit: null, + total_visits: 0, + total_spent: 0, + total_points: 0, + permanent_deletion_at: null, + created_at: "2024-03-23T17:52:46.000Z", + updated_at: "2024-03-23T20:14:50.000Z", + deleted_at: null, + }, + ], +}; diff --git a/components/loyverse/sources/item-updated-instant/item-updated-instant.mjs b/components/loyverse/sources/item-updated-instant/item-updated-instant.mjs new file mode 100644 index 0000000000000..85e64c35271f0 --- /dev/null +++ b/components/loyverse/sources/item-updated-instant/item-updated-instant.mjs @@ -0,0 +1,25 @@ +import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "loyverse-item-updated-instant", + name: "Item Updated (Instant)", + description: "Emit new event when an item is updated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + sampleEmit, + methods: { + ...common.methods, + getSummary(body) { + const { length } = body.items; + return `${length} item${length === 1 + ? "" + : "s"} updated`; + }, + getHookType() { + return "items.update"; + }, + }, +}; diff --git a/components/loyverse/sources/item-updated-instant/test-event.mjs b/components/loyverse/sources/item-updated-instant/test-event.mjs new file mode 100644 index 0000000000000..8069b51f5b92f --- /dev/null +++ b/components/loyverse/sources/item-updated-instant/test-event.mjs @@ -0,0 +1,61 @@ +export default { + merchant_id: "7e2daef3-48ad-4aba-b226-2d180f564f31", + type: "items.update", + created_at: "2024-03-23T20:11:26.946Z", + items: [ + { + id: "1c8404d6-8739-41de-898a-6ca4a321b62c", + handle: "new-item-3", + reference_id: null, + item_name: "new item 3", + description: "

test desc

", + track_stock: true, + sold_by_weight: false, + is_composite: false, + use_production: false, + category_id: "387fa7dc-a1d1-453a-8864-6d9e56e402bd", + components: [], + primary_supplier_id: null, + tax_ids: [], + modifier_ids: [], + form: "SQUARE", + color: "GREY", + image_url: null, + option1_name: null, + option2_name: null, + option3_name: null, + created_at: "2024-03-23T20:08:55.000Z", + updated_at: "2024-03-23T20:11:21.000Z", + deleted_at: null, + variants: [ + { + variant_id: "9b3d76bb-5d6b-414e-b9e5-4a46dc14a66b", + item_id: "1c8404d6-8739-41de-898a-6ca4a321b62c", + sku: "10002", + reference_variant_id: null, + option1_value: null, + option2_value: null, + option3_value: null, + barcode: null, + cost: 0, + purchase_cost: 0, + default_pricing_type: "VARIABLE", + default_price: null, + stores: [ + { + store_id: "c3930770-950c-4f48-aa43-174e021f59ba", + pricing_type: "VARIABLE", + price: null, + available_for_sale: true, + optimal_stock: null, + low_stock: 20, + }, + ], + created_at: "2024-03-23T20:08:55.000Z", + updated_at: "2024-03-23T20:11:21.000Z", + deleted_at: null, + }, + ], + }, + ], +}; diff --git a/components/loyverse/sources/receipt-updated-instant/receipt-updated-instant.mjs b/components/loyverse/sources/receipt-updated-instant/receipt-updated-instant.mjs new file mode 100644 index 0000000000000..a7f567980cfd6 --- /dev/null +++ b/components/loyverse/sources/receipt-updated-instant/receipt-updated-instant.mjs @@ -0,0 +1,25 @@ +import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "loyverse-receipt-updated-instant", + name: "Receipt Updated (Instant)", + description: "Emit new event when a receipt is updated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + sampleEmit, + methods: { + ...common.methods, + getSummary(body) { + const { length } = body.receipts; + return `${length} receipt${length === 1 + ? "" + : "s"} updated`; + }, + getHookType() { + return "receipts.update"; + }, + }, +}; diff --git a/components/loyverse/sources/receipt-updated-instant/test-event.mjs b/components/loyverse/sources/receipt-updated-instant/test-event.mjs new file mode 100644 index 0000000000000..42e6bb183b535 --- /dev/null +++ b/components/loyverse/sources/receipt-updated-instant/test-event.mjs @@ -0,0 +1,65 @@ +export default { + merchant_id: "7e2daef3-48ad-4aba-b226-2d180f564f31", + type: "receipts.update", + created_at: "2024-03-23T20:18:58.546Z", + receipts: [ + { + receipt_number: "1001", + note: null, + receipt_type: "SALE", + refund_for: null, + order: null, + created_at: "2024-03-23T20:18:53.000Z", + updated_at: "2024-03-23T20:18:53.000Z", + source: "Pipedream", + receipt_date: "2024-03-23T20:18:53.000Z", + cancelled_at: null, + total_money: 0, + total_tax: 0, + points_earned: 0, + points_deducted: 0, + points_balance: 0, + customer_id: null, + total_discount: 0, + employee_id: null, + store_id: "c3930770-950c-4f48-aa43-174e021f59ba", + pos_device_id: null, + dining_option: null, + total_discounts: [], + total_taxes: [], + tip: 0, + surcharge: 0, + line_items: [ + { + id: "9553152a-9dad-2666-f6a3-2ab5d913516c", + item_id: "e90d11be-b087-4899-853d-a88546212450", + variant_id: "9de08ad3-8d6d-4b31-9b3e-fb2951803417", + item_name: "name 2", + variant_name: null, + sku: "10001", + quantity: 1, + price: 0, + gross_total_money: 0, + total_money: 0, + cost: 20, + cost_total: 20, + line_note: null, + line_taxes: [], + total_discount: 0, + line_discounts: [], + line_modifiers: [], + }, + ], + payments: [ + { + payment_type_id: "958a8756-9914-433d-8abb-0c571824c5e0", + name: "Card", + type: "NONINTEGRATEDCARD", + money_amount: 0, + paid_at: "2024-03-23T20:18:53.000Z", + payment_details: null, + }, + ], + }, + ], +}; diff --git a/components/lucid/README.md b/components/lucid/README.md index 6e49f595c3aa1..9af65e943d6e5 100644 --- a/components/lucid/README.md +++ b/components/lucid/README.md @@ -1,14 +1,11 @@ # Overview - Lucid is a powerful API that enables developers to create beautiful, - interactive visualizations. With Lucid, developers can create stunning charts, - graphs, and maps to visualize data and tell stories. Lucid’s easy-to-use API - makes it simple to get started, and the flexible platform enables developers - to create customized visualizations to fit their specific needs. - -Here are some examples of what you can build with Lucid: - -- Charts: Bar charts, line charts,pie charts, etc. -- Graphs:Node-link diagrams, force-directed graphs, etc. -- Maps:GeoJSON maps, choropleth maps, etc. -- Data visualizations:Heatmaps, scatterplots, etc. +The Lucid API enables developers to automate visual communication processes by programmatically manipulating Lucidchart and Lucidpress documents. This allows for dynamic creation and updates of diagrams, visual workflows, and marketing materials, ensuring that teams can streamline their design operations with current data. Integrations via Pipedream can enhance this by triggering actions in Lucid based on events in other apps, or vice versa. + +# Example Use Cases + +- **Automated Document Updates from CRM Events**: When a sales deal progresses in a CRM like Salesforce, you can automatically update a Lucidchart sales pipeline diagram. This gives real-time visual feedback to the sales team and management. + +- **Dynamic Marketing Material Generation**: On launching a new product, details from a product management tool like Trello can trigger the generation of new marketing materials in Lucidpress, ensuring alignment with the latest product features and specifications. + +- **Issue Tracking Visualization**: For project management, when an issue is reported in GitHub, a workflow could automatically create or update a Lucidchart diagram to visualize the issue within the context of the entire system’s architecture. diff --git a/components/lucid/package.json b/components/lucid/package.json new file mode 100644 index 0000000000000..5eed6899de239 --- /dev/null +++ b/components/lucid/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/lucid", + "version": "0.6.0", + "description": "Pipedream lucid Components", + "main": "lucid.app.mjs", + "keywords": [ + "pipedream", + "lucid" + ], + "homepage": "https://pipedream.com/apps/lucid", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/luminous/README.md b/components/luminous/README.md new file mode 100644 index 0000000000000..4bd7094a8b301 --- /dev/null +++ b/components/luminous/README.md @@ -0,0 +1,11 @@ +# Overview + +The Aleph Alpha (Luminous) API offers advanced AI capabilities, enabling you to integrate state-of-the-art language models into your applications. With this API, you can create natural language understanding and generation tasks, such as answering questions, summarizing content, and generating human-like text. On Pipedream, you can leverage these features within serverless workflows to automate processes, enrich data, and connect with other apps to create powerful AI-driven solutions. + +# Example Use Cases + +- **Content Summarization and Social Media Posting**: Automate the generation of concise summaries for longer articles or reports and directly post them to social media platforms like Twitter or LinkedIn, using Pipedream's built-in Twitter and LinkedIn app integrations. + +- **Customer Support Auto-Responder**: Implement a workflow that uses incoming customer support emails (via SendGrid or Gmail) as triggers. The Aleph Alpha API can analyze the email's content and generate a draft response, which is then sent back to the customer, streamlining initial support interactions. + +- **Real-time Sentiment Analysis for Brand Monitoring**: Track mentions of a brand across different platforms (with webhooks or the Reddit app), analyze the sentiment using Aleph Alpha API, and store the results in a Google Sheets document for easy tracking and visualization. diff --git a/components/luno/README.md b/components/luno/README.md new file mode 100644 index 0000000000000..a4ce74aa6e859 --- /dev/null +++ b/components/luno/README.md @@ -0,0 +1,11 @@ +# Overview + +The Luno API offers a way to automate interactions with the Luno platform, a prominent cryptocurrency exchange. Through this API, you can programmatically access real-time market data, manage trades, and handle your wallet transactions. Integrating Luno with Pipedream enables you to create custom, serverless workflows that can react to Luno events, automate trading strategies, consolidate financial reports, and more. Pipedream's no-code platform allows you to seamlessly connect Luno to hundreds of other apps and orchestrate complex workflows with minimal effort. + +# Example Use Cases + +- **Automate Crypto Trading**: Set up a workflow where market data from Luno triggers buy or sell orders based on custom logic. For instance, buy Bitcoin whenever it drops below a certain price, and sell when it rises above a set threshold. + +- **Sync Transaction Data to Google Sheets**: Whenever a new transaction is made on Luno, automatically update a Google Sheets spreadsheet. This could be useful for tracking trades, managing portfolio performance, and maintaining financial records without manual data entry. + +- **Send Real-Time Price Alerts via SMS**: Create a workflow that monitors the Luno exchange for price changes in your preferred cryptocurrency. Using Twilio or a similar service, send an SMS alert when the price hits predefined targets, keeping you informed for timely investment decisions. diff --git a/components/luno/luno.app.mjs b/components/luno/luno.app.mjs new file mode 100644 index 0000000000000..b546df0ced9bb --- /dev/null +++ b/components/luno/luno.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "luno", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/luno/package.json b/components/luno/package.json new file mode 100644 index 0000000000000..7f4ea6bf2766d --- /dev/null +++ b/components/luno/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/luno", + "version": "0.0.1", + "description": "Pipedream Luno Components", + "main": "luno.app.mjs", + "keywords": [ + "pipedream", + "luno" + ], + "homepage": "https://pipedream.com/apps/luno", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/lusha/README.md b/components/lusha/README.md index 45baa3ebc2a0c..fe6542f8996a6 100644 --- a/components/lusha/README.md +++ b/components/lusha/README.md @@ -1,11 +1,11 @@ # Overview -With the Lusha API you can build a wide range of applications and integrations -including: - -- A tool to find contact information for a given company -- A search engine to help find specific contacts within a company -- An application to track the contact information of key individuals within a - company -- A system to keep track of employee contact information changes -- An analytics tool to help improve the accuracy of contact information +The Lusha API provides programmatic access to Lusha's rich database, allowing you to retrieve business profiles and contact information. By integrating Lusha with Pipedream, you can automate the enrichment of lead data, sync contact information with your CRM, and enhance your sales or recruitment processes. The API enables you to pull details such as phone numbers, email addresses, and company data, turning them into actionable insights within your business workflows. + +# Example Use Cases + +- **Lead Enrichment in Google Sheets**: Automatically enrich new rows added to a Google Sheets spreadsheet with Lusha. When a new contact is added to the sheet, trigger a Pipedream workflow that uses Lusha to find additional contact details and appends these details to the row. This keeps your lead list rich and up-to-date without manual data entry. + +- **CRM Integration for Sales Automation**: Sync enriched contact data from Lusha directly into your CRM, such as Salesforce. Set up a Pipedream workflow that triggers whenever a new lead is captured via your website or sales platform. Use Lusha to enrich the lead's information and then create or update the lead in Salesforce, ensuring your sales team has all the necessary data at their fingertips. + +- **Real-Time Lead Verification for Email Campaigns**: Before sending out a mass email campaign using a service like SendGrid, validate and enrich email addresses with Lusha. Build a Pipedream workflow that processes each email on your list through Lusha to confirm its validity and enhance it with additional lead information, thereby improving your email deliverability and engagement rates. diff --git a/components/lusha/actions/company-enrich/company-enrich.mjs b/components/lusha/actions/company-enrich/company-enrich.mjs new file mode 100644 index 0000000000000..f3855c03562cb --- /dev/null +++ b/components/lusha/actions/company-enrich/company-enrich.mjs @@ -0,0 +1,37 @@ +import lusha from "../../lusha.app.mjs"; + +export default { + key: "lusha-company-enrich", + name: "Enrich Companies", + description: "Enriches company information based on provided company IDs. [See the documentation](https://www.lusha.com/docs/#company-enrich)", + version: "0.0.2", + type: "action", + props: { + lusha, + requestId: { + propDefinition: [ + lusha, + "requestId", + ], + label: "Company Request ID", + description: "The request ID generated from the company search response.", + }, + companiesIds: { + propDefinition: [ + lusha, + "companyIds", + ], + }, + }, + async run({ $ }) { + const response = await this.lusha.enrichCompanies({ + $, + data: { + requestId: this.requestId, + companiesIds: this.companiesIds, + }, + }); + $.export("$summary", `Successfully enriched ${this.companiesIds.length} companies`); + return response; + }, +}; diff --git a/components/lusha/actions/company-search/company-search.mjs b/components/lusha/actions/company-search/company-search.mjs new file mode 100644 index 0000000000000..c00cd4d8b8efa --- /dev/null +++ b/components/lusha/actions/company-search/company-search.mjs @@ -0,0 +1,105 @@ +import { parseObject } from "../../common/utils.mjs"; +import lusha from "../../lusha.app.mjs"; + +export default { + key: "lusha-company-search", + name: "Search Companies", + description: "Search for companies using various filters. [See the documentation](https://www.lusha.com/docs/#contactcompany-search)", + version: "0.0.1", + type: "action", + props: { + lusha, + names: { + propDefinition: [ + lusha, + "companyNames", + ], + optional: true, + }, + domains: { + propDefinition: [ + lusha, + "domains", + ], + optional: true, + }, + locations: { + propDefinition: [ + lusha, + "locations", + ], + optional: true, + }, + sizes: { + propDefinition: [ + lusha, + "sizes", + ], + optional: true, + }, + revenues: { + propDefinition: [ + lusha, + "revenues", + ], + optional: true, + }, + sicCodes: { + propDefinition: [ + lusha, + "sicCodes", + ], + optional: true, + }, + naicsCodes: { + propDefinition: [ + lusha, + "naicsCodes", + ], + optional: true, + }, + limit: { + type: "string", + label: "Limit", + description: "The maximum number of results to return. **This feature is used to avoid timeouts due to very long returns.**", + }, + }, + async run({ $ }) { + try { + const include = {}; + + if (this.names) include.names = parseObject(this.names); + if (this.domains) include.domains = parseObject(this.domains); + if (this.locations) include.locations = parseObject(this.locations); + if (this.sizes) include.sizes = parseObject(this.sizes); + if (this.revenues) include.revenues = parseObject(this.revenues); + if (this.sicCodes) include.sicCodes = parseObject(this.sicCodes); + if (this.naicsCodes) include.naicsCodes = parseObject(this.naicsCodes); + + const response = this.lusha.paginate({ + $, + maxResults: this.limit, + fn: this.lusha.searchCompanies, + data: { + filters: { + companies: { + include, + }, + }, + }, + }); + + const responseArray = []; + + for await (const item of response) { + responseArray.push(item); + } + + $.export("$summary", `Successfully retrieved ${responseArray.length} companies`); + return responseArray; + } catch (error) { + $.export("$summary", "Failed to search companies"); + throw error; + } + }, +}; diff --git a/components/lusha/actions/contact-enrich/contact-enrich.mjs b/components/lusha/actions/contact-enrich/contact-enrich.mjs new file mode 100644 index 0000000000000..732fa6b1f0ff5 --- /dev/null +++ b/components/lusha/actions/contact-enrich/contact-enrich.mjs @@ -0,0 +1,37 @@ +import lusha from "../../lusha.app.mjs"; + +export default { + key: "lusha-contact-enrich", + name: "Enrich Contacts", + description: "Enriches contacts based on provided IDs. [See the documentation](https://www.lusha.com/docs/#contact-enrich)", + version: "0.0.1", + type: "action", + props: { + lusha, + requestId: { + propDefinition: [ + lusha, + "requestId", + ], + label: "Contact Request ID", + description: "The request ID generated from the contact search response.", + }, + contactIds: { + propDefinition: [ + lusha, + "contactIds", + ], + }, + }, + async run({ $ }) { + const response = await this.lusha.enrichContacts({ + $, + data: { + requestId: this.requestId, + contactIds: this.contactIds, + }, + }); + $.export("$summary", `Successfully enriched ${this.contactIds.length} contacts`); + return response; + }, +}; diff --git a/components/lusha/actions/contact-search/contact-search.mjs b/components/lusha/actions/contact-search/contact-search.mjs new file mode 100644 index 0000000000000..d0470eed5b54a --- /dev/null +++ b/components/lusha/actions/contact-search/contact-search.mjs @@ -0,0 +1,98 @@ +import { parseObject } from "../../common/utils.mjs"; +import lusha from "../../lusha.app.mjs"; + +export default { + key: "lusha-contact-search", + name: "Search Contacts", + description: "Search for contacts using various filters. [See the documentation](https://www.lusha.com/docs/#contactcompany-search)", + version: "0.0.1", + type: "action", + props: { + lusha, + names: { + propDefinition: [ + lusha, + "contactNames", + ], + label: "Contact Names", + description: "Names of contacts to search.", + }, + jobTitles: { + propDefinition: [ + lusha, + "jobTitles", + ], + }, + jobTitlesExactMatch: { + propDefinition: [ + lusha, + "jobTitlesExactMatch", + ], + }, + countries: { + propDefinition: [ + lusha, + "countries", + ], + }, + seniority: { + propDefinition: [ + lusha, + "seniority", + ], + }, + departments: { + propDefinition: [ + lusha, + "departments", + ], + }, + existingDataPoints: { + propDefinition: [ + lusha, + "existingDataPoints", + ], + }, + location: { + propDefinition: [ + lusha, + "location", + ], + }, + }, + async run({ $ }) { + const include = {}; + + if (this.names) include.names = parseObject(this.names); + if (this.jobTitles) include.jobTitles = parseObject(this.jobTitles); + if (this.jobTitlesExactMatch) + include.jobTitlesExactMatch = parseObject(this.jobTitlesExactMatch); + if (this.countries) include.countries = parseObject(this.countries); + if (this.seniority) include.seniority = parseObject(this.seniority); + if (this.departments) include.departments = parseObject(this.departments); + if (this.existingDataPoints) include.existingDataPoints = parseObject(this.existingDataPoints); + if (this.location) include.location = parseObject(this.location); + + const response = this.lusha.paginate({ + $, + maxResults: this.limit, + fn: this.lusha.searchContacts, + data: { + filters: { + contacts: { + include, + }, + }, + }, + }); + + const responseArray = []; + + for await (const item of response) { + responseArray.push(item); + } + + $.export("$summary", `Found ${responseArray.length} contacts`); + return response; + }, +}; diff --git a/components/lusha/actions/find-company/find-company.mjs b/components/lusha/actions/find-company/find-company.mjs deleted file mode 100644 index 63f71f7640b78..0000000000000 --- a/components/lusha/actions/find-company/find-company.mjs +++ /dev/null @@ -1,29 +0,0 @@ -import lusha from "../../lusha.app.mjs"; - -export default { - key: "lusha-find-company", - name: "Find Company", - description: "Search for a company. [See docs here](https://www.lusha.com/docs/#company-api)", - version: "0.0.1", - type: "action", - props: { - lusha, - companyName: { - label: "Company Name", - description: "The name of the company", - type: "string", - }, - }, - async run({ $ }) { - const response = this.lusha.findCompany({ - $, - params: { - company: this.companyName, - }, - }); - - $.export("$summary", "Successfully searched company"); - - return response; - }, -}; diff --git a/components/lusha/actions/find-contact/find-contact.mjs b/components/lusha/actions/find-contact/find-contact.mjs deleted file mode 100644 index a0c73a37049d9..0000000000000 --- a/components/lusha/actions/find-contact/find-contact.mjs +++ /dev/null @@ -1,41 +0,0 @@ -import lusha from "../../lusha.app.mjs"; - -export default { - key: "lusha-find-contact", - name: "Find Contact", - description: "Search for a contact. [See docs here](https://www.lusha.com/docs/#person-api)", - version: "0.0.1", - type: "action", - props: { - lusha, - firstName: { - label: "First Name", - description: "The first name of the person", - type: "string", - }, - lastName: { - label: "Last Name", - description: "The last name of the person", - type: "string", - }, - companyName: { - label: "Company Name", - description: "The name of the company", - type: "string", - }, - }, - async run({ $ }) { - const response = this.lusha.findContact({ - $, - params: { - firstName: this.firstName, - lastName: this.lastName, - company: this.companyName, - }, - }); - - $.export("$summary", "Successfully searched contact"); - - return response; - }, -}; diff --git a/components/lusha/common/utils.mjs b/components/lusha/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/lusha/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/lusha/lusha.app.mjs b/components/lusha/lusha.app.mjs index a41caa4647d32..5ea955061dc1a 100644 --- a/components/lusha/lusha.app.mjs +++ b/components/lusha/lusha.app.mjs @@ -3,38 +3,170 @@ import { axios } from "@pipedream/platform"; export default { type: "app", app: "lusha", - propDefinitions: {}, - methods: { - _accessToken() { - return this.$auth.oauth_access_token; + propDefinitions: { + companyNames: { + type: "string[]", + label: "Company Names", + description: "Names of companies to search.", + }, + domains: { + type: "string[]", + label: "Company Domains", + description: "Domains of companies to search.", + }, + locations: { + type: "string[]", + label: "Company Locations", + description: "Location filters for companies to search.", + }, + sizes: { + type: "string[]", + label: "Company Sizes", + description: "Size ranges of companies to search.", + }, + revenues: { + type: "string[]", + label: "Company Revenues", + description: "Revenue ranges of companies to search.", + }, + sicCodes: { + type: "string[]", + label: "Company SIC Codes", + description: "SIC codes of companies to search.", + }, + naicsCodes: { + type: "string[]", + label: "Company NAICS Codes", + description: "NAICS codes of companies to search.", + }, + contactNames: { + type: "string[]", + label: "Contact Names", + description: "Names of contacts to search.", + }, + jobTitles: { + type: "string[]", + label: "Contact Job Titles", + description: "Job titles of contacts to search.", + }, + jobTitlesExactMatch: { + type: "string[]", + label: "Exact Match Contact Job Titles", + description: "Exact job titles of contacts to search.", + }, + countries: { + type: "string[]", + label: "Contact Countries", + description: "Country codes of contacts to search.", + }, + seniority: { + type: "integer[]", + label: "Contact Seniority Levels", + description: "Seniority levels of contacts to search.", + }, + departments: { + type: "string[]", + label: "Contact Departments", + description: "Departments of contacts to search.", + }, + existingDataPoints: { + type: "string[]", + label: "Existing Data Points", + description: "Existing data points of contacts to filter by.", + }, + location: { + type: "string[]", + label: "Contact Locations", + description: "Location filters for contacts to search (JSON strings).", }, - _apiUrl() { + requestId: { + type: "string", + label: "Enrich Contact Request ID", + description: "The request ID generated from the contact search response.", + }, + contactIds: { + type: "string[]", + label: "Enrich Contact IDs", + description: "Array of contact IDs to enrich.", + }, + companyIds: { + type: "string[]", + label: "Enrich Company IDs", + description: "Array of company IDs to enrich.", + }, + }, + methods: { + _baseUrl() { return "https://api.lusha.com"; }, - async _makeRequest({ - $ = this, path, ...args - } = {}) { + _headers() { + return { + "api_key": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { return axios($, { - url: `${this._apiUrl()}${path}`, - headers: { - Authorization: `Bearer ${this._accessToken()}`, - }, - ...args, + url: `${this._baseUrl()}${path}`, + headers: this._headers(), + ...opts, + }); + }, + searchContacts(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/prospecting/contact/search", + ...opts, }); }, - async findContact(args = {}) { - const { data } = await this._makeRequest({ - path: "/person", - ...args, + searchCompanies(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/prospecting/company/search", + ...opts, }); - return data; }, - async findCompany(args = {}) { - const { data } = await this._makeRequest({ - path: "/company", - ...args, + enrichContacts(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/prospecting/contact/enrich", + ...opts, }); - return data; + }, + enrichCompanies(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/prospecting/company/enrich", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.pages = { + page: ++page, + }; + const { data } = await fn({ + params, + ...opts, + }); + for (const d of data) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = data.length; + + } while (hasMore); }, }, }; diff --git a/components/lusha/package.json b/components/lusha/package.json index c419eb7947c3b..b5ce069ed3fa9 100644 --- a/components/lusha/package.json +++ b/components/lusha/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/lusha", - "version": "0.0.3", + "version": "0.1.1", "description": "Pipedream Lusha Components", "main": "lusha.app.mjs", "keywords": [ @@ -14,6 +14,6 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^0.10.0" + "@pipedream/platform": "^3.0.3" } } diff --git a/components/maestra/README.md b/components/maestra/README.md new file mode 100644 index 0000000000000..b2fe0c176cc1d --- /dev/null +++ b/components/maestra/README.md @@ -0,0 +1,11 @@ +# Overview + +The Maestra API lets you automate the transcription, captioning, and voiceover of videos and audios, crucial for creating accessible and localized content. With Pipedream, you can build workflows that trigger on various events to streamline media processing, integrate with other services, and manage content efficiently. Pipedream's serverless platform offers a code-free way to connect the Maestra API with hundreds of other apps, enabling you to create custom automation without the heavy lifting. + +# Example Use Cases + +- **Automated Video Transcription on Upload**: When a video is uploaded to cloud storage like Amazon S3, trigger a Pipedream workflow that automatically sends the video to Maestra for transcription. Then, store the resulting transcript in a database like Airtable or send it via email with SendGrid for further processing or archiving. + +- **Multi-Language Video Captioning for Social Media**: Combine Maestra with social media platforms like YouTube or Facebook. When a new video is posted, a workflow is triggered that sends the video to Maestra for captioning in multiple languages. Once done, the captions can be automatically uploaded back to the respective social media platform, enhancing viewer engagement and accessibility. + +- **Voiceover Production for Marketing Content**: Integrate Maestra with project management tools like Trello. When a new card is created for a marketing campaign, trigger a Pipedream workflow that takes the provided script and uses Maestra to generate a professional voiceover. Subsequently, the voiceover file can be sent to a Slack channel or an email address for review and approval by the marketing team. diff --git a/components/magnetic/README.md b/components/magnetic/README.md index 3c9612d43c589..8a30705d3b65f 100644 --- a/components/magnetic/README.md +++ b/components/magnetic/README.md @@ -1,9 +1,11 @@ # Overview -With the Magnetic API, you can build a variety of applications and -integrations. Here are a few examples: +Magnetic is a project management and workflow automation platform designed to help agencies stay organized and streamline operations. With the Magnetic API on Pipedream, you can automate routine tasks, sync data across various apps, and create custom workflows to enhance productivity. From task creation and time tracking to financial management and reporting, Pipedream’s serverless execution model lets you connect Magnetic with a plethora of other services without the need for a dedicated server setup. -- A CRM system for managing customer relationships -- An eCommerce platform for selling products and services online -- A project management tool for tracking tasks and deadlines -- An invoicing system for generating and tracking invoices +# Example Use Cases + +- **Automated Task Assignment**: Create a workflow on Pipedream that triggers when a new sales deal is closed in your CRM (like Salesforce). The workflow would automatically create a corresponding project and tasks in Magnetic, assigning them to the appropriate team members based on predefined criteria. + +- **Time Tracking Integration**: Set up a workflow that connects Magnetic to time tracking apps like Toggl or Harvest. Whenever a task is marked as completed in Magnetic, the workflow could log the time spent on the task in the time tracking app, ensuring accurate and effortless time management. + +- **Financial Dashboard Sync**: Configure a Pipedream workflow to sync Magnetic's project financial data with accounting software such as QuickBooks. Any updates to budgets, expenses, or invoices in Magnetic would be reflected in QuickBooks in real time, giving you an up-to-date view of financials for better decision-making. diff --git a/components/mailblaze/actions/add-subscriber/add-subscriber.mjs b/components/mailblaze/actions/add-subscriber/add-subscriber.mjs new file mode 100644 index 0000000000000..7aa46d48372e4 --- /dev/null +++ b/components/mailblaze/actions/add-subscriber/add-subscriber.mjs @@ -0,0 +1,16 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "mailblaze-add-subscriber", + name: "Add Subscriber", + description: "Adds a new subscriber to your mailing list. [See the documentation](https://www.mailblaze.com/support/api-documentation)", + version: "0.0.1", + type: "action", + methods: { + ...common.methods, + getSummary(response) { + return `Successfully added subscriber with Id: ${response.data.record.subscriber_uid}`; + }, + }, +}; diff --git a/components/mailblaze/actions/common/base.mjs b/components/mailblaze/actions/common/base.mjs new file mode 100644 index 0000000000000..8aeca568a1b45 --- /dev/null +++ b/components/mailblaze/actions/common/base.mjs @@ -0,0 +1,80 @@ +import mailblaze from "../../mailblaze.app.mjs"; + +export default { + props: { + mailblaze, + listUid: { + propDefinition: [ + mailblaze, + "listUid", + ], + reloadProps: true, + }, + }, + methods: { + parseOptions(options) { + return Object.keys(options).map((key) => ({ + label: key, + value: options[key], + })); + }, + parseProp(data) { + return Object.keys(data).reduce((res, key) => { + res[key] = Array.isArray(data[key]) + ? data[key].join(",") + : data[key]; + return res; + }, {}); + }, + getAction() { + return "add"; + }, + getAdditionalProp() { + return {}; + }, + }, + async additionalProps() { + const props = {}; + if (this.listUid) { + const { data: { records: fields } } = await this.mailblaze.getFields({ + listUid: this.listUid, + }); + + for (const field of fields) { + props[field.tag] = { + type: (field.type.identifier === "checkbox") + ? "string[]" + : "string", + label: field.label, + optional: field.required === "no", + description: field.help_text, + options: field.options && this.parseOptions(field.options), + }; + } + } + return props; + }, + async run({ $ }) { + const { + mailblaze, + listUid, + ...data + } = this; + + const action = this.getAction(); + + const fn = (action === "add") + ? mailblaze.addSubscriber + : mailblaze.updateSubscriber; + + const response = await fn({ + $, + listUid, + data: this.parseProp(data), + ...this.getAdditionalProp(), + }); + + $.export("$summary", this.getSummary(response)); + return response; + }, +}; diff --git a/components/mailblaze/actions/update-subscriber/update-subscriber.mjs b/components/mailblaze/actions/update-subscriber/update-subscriber.mjs new file mode 100644 index 0000000000000..1506dd3ac9875 --- /dev/null +++ b/components/mailblaze/actions/update-subscriber/update-subscriber.mjs @@ -0,0 +1,37 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "mailblaze-update-subscriber", + name: "Update Subscriber", + description: "Updates information for an existing subscriber in your mailing list. [See the documentation](https://www.mailblaze.com/support/api-documentation)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + subscriberUid: { + propDefinition: [ + common.props.mailblaze, + "subscriberUid", + ({ listUid }) => ({ + listUid, + }), + ], + reloadProps: true, + }, + }, + methods: { + ...common.methods, + getAction() { + return "update"; + }, + getAdditionalProp() { + return { + subscriberUid: this.subscriberUid, + }; + }, + getSummary() { + return `Successfully updated subscriber with Id: ${this.subscriberUid}`; + }, + }, +}; diff --git a/components/mailblaze/mailblaze.app.mjs b/components/mailblaze/mailblaze.app.mjs new file mode 100644 index 0000000000000..12bcbd70866b1 --- /dev/null +++ b/components/mailblaze/mailblaze.app.mjs @@ -0,0 +1,108 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "mailblaze", + propDefinitions: { + listUid: { + type: "string", + label: "List UID", + description: "The UID of the mailing list", + async options({ page }) { + const { data } = await this.listLists({ + params: { + page: page + 1, + }, + }); + + return data.records.map(({ + general: { + list_uid: value, name: label, + }, + }) => ({ + label, + value, + })); + }, + }, + subscriberUid: { + type: "string", + label: "subscriber UID", + description: "The UID of the subscriber", + async options({ + page, listUid, + }) { + const { data } = await this.listSubscribers({ + params: { + page: page + 1, + }, + listUid, + }); + + return data.records.map(({ + subscriber_uid: value, email: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://control.mailblaze.com/api"; + }, + _headers() { + return { + "Authorization": `${this.$auth.api_key}`, + "Content-Type": "application/x-www-form-urlencoded", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + addSubscriber({ + listUid, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/lists/${listUid}/subscribers`, + ...opts, + }); + }, + updateSubscriber({ + listUid, subscriberUid, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/lists/${listUid}/subscribers/${subscriberUid}`, + ...opts, + }); + }, + getFields({ listUid }) { + return this._makeRequest({ + path: `/lists/${listUid}/fields`, + }); + }, + listLists(opts = {}) { + return this._makeRequest({ + path: "/lists", + ...opts, + }); + }, + listSubscribers({ + listUid, ...opts + }) { + return this._makeRequest({ + path: `/lists/${listUid}/subscribers`, + ...opts, + }); + }, + }, +}; diff --git a/components/mailblaze/package.json b/components/mailblaze/package.json new file mode 100644 index 0000000000000..bf40b035ea43d --- /dev/null +++ b/components/mailblaze/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/mailblaze", + "version": "0.1.0", + "description": "Pipedream MailBlaze Components", + "main": "mailblaze.app.mjs", + "keywords": [ + "pipedream", + "mailblaze" + ], + "homepage": "https://pipedream.com/apps/mailblaze", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/mailbluster/README.md b/components/mailbluster/README.md new file mode 100644 index 0000000000000..1a2e05be48ccc --- /dev/null +++ b/components/mailbluster/README.md @@ -0,0 +1,11 @@ +# Overview + +Mailbluster is an API for managing email marketing campaigns, allowing for the creation, sending, and tracking of email newsletters. With Pipedream, you can automate workflows involving Mailbluster to send personalized emails, sync subscriber lists, and analyze campaign performance without the need to write code. By leveraging Pipedream's serverless platform, you can connect Mailbluster with various apps, set up triggers based on certain conditions, and create complex email sequences effortlessly. + +# Example Use Cases + +- **Automated Welcome Emails**: Trigger a Mailbluster campaign to send a custom welcome email as soon as a new user signs up on your platform. This could be connected to user sign-up events from apps like Auth0 or Firebase Authentication. + +- **Subscriber List Sync**: Automatically update Mailbluster mailing lists when a new contact is added to a CRM like Salesforce or HubSpot. Use Pipedream's built-in actions to detect new CRM entries and sync them to the appropriate Mailbluster list. + +- **Campaign Performance Digest**: Gather campaign statistics from Mailbluster and send a daily or weekly performance digest to Slack or Microsoft Teams. This workflow can help teams stay informed on the success of their email marketing efforts. diff --git a/components/mailbox_power/README.md b/components/mailbox_power/README.md index 8e911ec01f552..475d0e7f961e4 100644 --- a/components/mailbox_power/README.md +++ b/components/mailbox_power/README.md @@ -1,11 +1,11 @@ # Overview -The Mailbox Power API can be used to build a number of applications that can -help improve your productivity and organization. Here are a few examples: - -- A to-do list application that can help you keep track of your tasks and - priorities. -- A contacts manager that can help you keep track of your contacts and their - contact information. -- A calendar application that can help you keep track of your schedule and - upcoming events. +The Mailbox Power API enables personalized direct mail campaigns by automating the sending of physical mail, gifts, and greeting cards. This can be orchestrated directly from Pipedream's platform, leveraging the ease of serverless workflows. With this API, one can create targeted, personalized mail pieces on the fly, manage contacts, and track sent items, all programmatically. The integration potential on Pipedream also allows for syncing Mailbox Power with CRM systems, triggering mail sends based on customer actions or dates, and analyzing the impact of direct mail campaigns in conjunction with digital marketing tools. + +# Example Use Cases + +- **CRM Lead Engagement**: When a new lead is added to a CRM like Salesforce, automatically trigger a Mailbox Power workflow to send a personalized welcome postcard. This nurtures the lead with a tangible touchpoint. + +- **Customer Birthdays and Anniversaries**: Connect Mailbox Power with a database like Airtable or Google Sheets, where customer birthdays or anniversaries are stored. Set up a Pipedream workflow that sends a personalized card or gift on these special dates to enhance customer loyalty. + +- **E-commerce Order Follow-up**: After an order status changes to 'Delivered' in an e-commerce platform like Shopify, use Pipedream to trigger Mailbox Power to mail a thank-you note or a discount offer for future purchases, enriching the post-purchase experience. diff --git a/components/mailbox_power/package.json b/components/mailbox_power/package.json new file mode 100644 index 0000000000000..66a7e9b1f0934 --- /dev/null +++ b/components/mailbox_power/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/mailbox_power", + "version": "0.6.0", + "description": "Pipedream mailbox_power Components", + "main": "mailbox_power.app.mjs", + "keywords": [ + "pipedream", + "mailbox_power" + ], + "homepage": "https://pipedream.com/apps/mailbox_power", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/mailboxlayer/mailboxlayer.app.mjs b/components/mailboxlayer/mailboxlayer.app.mjs new file mode 100644 index 0000000000000..b2f9290e7c9d2 --- /dev/null +++ b/components/mailboxlayer/mailboxlayer.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "mailboxlayer", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/mailboxlayer/package.json b/components/mailboxlayer/package.json new file mode 100644 index 0000000000000..214bb852d2c8c --- /dev/null +++ b/components/mailboxlayer/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/mailboxlayer", + "version": "0.0.1", + "description": "Pipedream Mailboxlayer Components", + "main": "mailboxlayer.app.mjs", + "keywords": [ + "pipedream", + "mailboxlayer" + ], + "homepage": "https://pipedream.com/apps/mailboxlayer", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/mailboxvalidator/README.md b/components/mailboxvalidator/README.md index f469a26ded469..fcf1e85ac9284 100644 --- a/components/mailboxvalidator/README.md +++ b/components/mailboxvalidator/README.md @@ -1,13 +1,11 @@ # Overview -With MailboxValidator's API, you can build a wide range of tools and -applications to help you validate email addresses. Here are just a few examples -of what you can build: - -- A standalone email validation tool -- A plugin or extension for an existing email validation tool -- A tool to bulk validate email addresses -- A tool to automatically validate email addresses as they are entered into a - database or form -- An API service to provide email validation as a service to other applications - or websites +MailboxValidator is a precision tool for email verification that checks email lists for bad or invalid addresses. It helps maintain hygiene in your mailing list by ensuring that emails are deliverable, reducing bounce rates significantly. By integrating MailboxValidator with Pipedream, you can automate the cleanse of email lists, validate emails in real-time during sign-up processes, and improve the reliability of email marketing campaigns. Pipedream's platform provides the flexibility to trigger workflows with events, schedule tasks, and connect to numerous other services to enrich data or take actions based on the results of an email validation. + +# Example Use Cases + +- **Real-Time Email Verification on Sign-ups**: Integrate MailboxValidator with a sign-up form on your website. When a new user signs up, trigger a Pipedream workflow to validate the email address. If it's invalid, automatically prevent form submission or signal an error to the user. + +- **Scheduled Email List Cleaning**: Set up a scheduled Pipedream workflow to periodically validate your entire email list stored in Google Sheets. Use MailboxValidator to identify invalid emails, and then programmatically remove them from the sheet, ensuring your email marketing tools are working with a clean list. + +- **Customer Onboarding Email Verification**: In a customer onboarding flow, verify the customer's email address with MailboxValidator. If the email is valid, proceed with sending a welcome email using SendGrid and create a new customer record in your CRM like Salesforce, all orchestrated by a Pipedream workflow. diff --git a/components/mailcheck/README.md b/components/mailcheck/README.md new file mode 100644 index 0000000000000..bb91b060a3eb5 --- /dev/null +++ b/components/mailcheck/README.md @@ -0,0 +1,11 @@ +# Overview + +The Mailcheck API provides verification services for email addresses, ensuring they are valid and deliverable. Integrating this API with Pipedream opens up opportunities for automating email validation processes, enriching user data, and maintaining the hygiene of email lists in real-time. By weaving Mailcheck into serverless workflows on Pipedream, you can trigger actions based on email verification results, such as updating databases, initiating marketing campaigns, or even blocking fake signups. + +# Example Use Cases + +- **Validate Emails on Signup**: Use Mailcheck with Pipedream to validate email addresses during user signups. If an email is invalid, trigger a response to prompt the user for a correct address or block the registration process. + +- **Clean Email Lists for Campaigns**: Before sending out a marketing campaign, run your email list through Mailcheck via Pipedream to remove invalid addresses. This can improve deliverability rates and protect your sender reputation. + +- **Automate Lead Qualification**: Integrate Mailcheck with a CRM app on Pipedream to validate and score leads based on email address validity. Enrich lead data automatically, ensuring your sales team focuses on high-quality prospects. diff --git a/components/mailcheck/actions/create-batch-operation/create-batch-operation.mjs b/components/mailcheck/actions/create-batch-operation/create-batch-operation.mjs new file mode 100644 index 0000000000000..3281e67f0f3aa --- /dev/null +++ b/components/mailcheck/actions/create-batch-operation/create-batch-operation.mjs @@ -0,0 +1,34 @@ +import { parseObject } from "../../common/utils.mjs"; +import mailcheck from "../../mailcheck.app.mjs"; + +export default { + key: "mailcheck-create-batch-operation", + name: "Create Batch Operation", + description: "Create a batch check operation. [See the documentation](https://app.mailcheck.co/docs?from=docs#post-/v1/emails-check)", + version: "0.0.1", + type: "action", + props: { + mailcheck, + emails: { + propDefinition: [ + mailcheck, + "emails", + ], + }, + }, + async run({ $ }) { + try { + const response = await this.mailcheck.createBatch({ + $, + data: { + emails: this.emails && parseObject(this.emails), + }, + }); + + $.export("$summary", `Batch check operation successfully created with name: ${response.name}`); + return response; + } catch (message) { + throw new Error(JSON.parse(message).message); + } + }, +}; diff --git a/components/mailcheck/actions/get-batch-operation-status/get-batch-operation-status.mjs b/components/mailcheck/actions/get-batch-operation-status/get-batch-operation-status.mjs new file mode 100644 index 0000000000000..75571ac99776e --- /dev/null +++ b/components/mailcheck/actions/get-batch-operation-status/get-batch-operation-status.mjs @@ -0,0 +1,28 @@ +import mailcheck from "../../mailcheck.app.mjs"; + +export default { + key: "mailcheck-get-batch-operation-status", + name: "Get Batch Operation Status", + description: "Get batch check operation status. [See the documentation](https://app.mailcheck.co/docs?from=docs#get-/v1/emails/-operation_name-)", + version: "0.0.1", + type: "action", + props: { + mailcheck, + operationName: { + propDefinition: [ + mailcheck, + "operationName", + ], + }, + }, + async run({ $ }) { + + const response = await this.mailcheck.getBatchOperationStatus({ + $, + operationName: this.operationName, + }); + + $.export("$summary", `Successfully fetched data for operation with name: ${this.operationName}`); + return response; + }, +}; diff --git a/components/mailcheck/actions/validate-single-email/validate-single-email.mjs b/components/mailcheck/actions/validate-single-email/validate-single-email.mjs new file mode 100644 index 0000000000000..0f345850a1ea8 --- /dev/null +++ b/components/mailcheck/actions/validate-single-email/validate-single-email.mjs @@ -0,0 +1,30 @@ +import mailcheck from "../../mailcheck.app.mjs"; + +export default { + key: "mailcheck-validate-single-email", + name: "Process Single Email", + description: "Process a single email synchronously. [See the documentation](https://app.mailcheck.co/docs?from=docs#post-/v1/singleEmail-check)", + version: "0.0.1", + type: "action", + props: { + mailcheck, + email: { + propDefinition: [ + mailcheck, + "email", + ], + }, + }, + async run({ $ }) { + + const response = await this.mailcheck.verifyEmail({ + $, + data: { + email: this.email, + }, + }); + + $.export("$summary", `Successfully fetched data for ${this.email}`); + return response; + }, +}; diff --git a/components/mailcheck/common/utils.mjs b/components/mailcheck/common/utils.mjs new file mode 100644 index 0000000000000..154701004e72a --- /dev/null +++ b/components/mailcheck/common/utils.mjs @@ -0,0 +1,18 @@ +export const parseObject = (obj) => { + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + return JSON.parse(obj); + } + return obj; +}; diff --git a/components/mailcheck/mailcheck.app.mjs b/components/mailcheck/mailcheck.app.mjs index 851b0219c02d5..c12dc86b81545 100644 --- a/components/mailcheck/mailcheck.app.mjs +++ b/components/mailcheck/mailcheck.app.mjs @@ -1,11 +1,75 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "mailcheck", - propDefinitions: {}, + propDefinitions: { + email: { + type: "string", + label: "Email Address", + description: "The email address to verify.", + }, + emails: { + type: "string[]", + label: "Email Addresses", + description: "The email addresses to verify.", + }, + operationName: { + type: "string", + label: "Operation Name", + description: "The name returned on a batch operation creation. E.g. **operation/7093f46f-54c5-44b4-96ec-f891ca076082**", + async options() { + const { operations } = await this.listBatches(); + + return operations.map(({ name }) => name); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.mailcheck.co/v1"; + }, + _headers() { + return { + "Authorization": `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: this._headers(), + }); + }, + listBatches(opts = {}) { + return this._makeRequest({ + path: "/emails/operations", + ...opts, + }); + }, + verifyEmail(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/singleEmail:check", + ...opts, + }); + }, + createBatch(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/emails:check", + ...opts, + }); + }, + getBatchOperationStatus({ + operationName, ...opts + }) { + return this._makeRequest({ + path: `/emails/${operationName}`, + ...opts, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/mailcheck/package.json b/components/mailcheck/package.json index d9abf86f10cc0..0ac74c3c78aef 100644 --- a/components/mailcheck/package.json +++ b/components/mailcheck/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/mailcheck", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Mailcheck Components", "main": "mailcheck.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" } -} \ No newline at end of file +} + diff --git a/components/mailchimp/README.md b/components/mailchimp/README.md index 7e2c8849368fc..6c67bf044f61f 100644 --- a/components/mailchimp/README.md +++ b/components/mailchimp/README.md @@ -1,18 +1,11 @@ # Overview -The Mailchimp API is a powerful tool that can be used to build all sorts of -applications and integrations. With it, you can manage subscribers, send -emails, and track engagement. Some examples of what you can build with the -Mailchimp API include: +The Mailchimp API taps into the heart of your email marketing efforts, allowing you to manage subscribers, campaigns, and reports. With the API, you can automate tasks like syncing subscriber lists, segmenting contacts based on behavior, and personalizing email content to match user profiles. It's a powerful tool for marketers looking to fine-tune their email strategy and maximize engagement. -- A desktop application for managing your Mailchimp account -- A mobile app for subscribing to newsletters on the go -- An integration with your CRM system to automatically add subscribers to your - Mailchimp list -- A web app that allows visitors to sign up for your newsletter right from your - website -- An app that sends automatic follow-up emails to subscribers who haven't - engaged recently +# Example Use Cases -These are just a few examples of what you can build with the Mailchimp API. -With a little creativity, the possibilities are endless! +- **Sync New Users to Mailchimp List**: Automatically add new users from your app (e.g., a SaaS platform) to a designated Mailchimp list once they sign up. This keeps your subscriber list up-to-date without manual input. + +- **Dynamic Content Campaigns**: Trigger an email campaign in Mailchimp when a user performs a specific action on your website (like browsing a product page). Using Pipedream's workflow, you can fetch user data and send personalized content that matches their interests. + +- **E-commerce Follow-ups**: For an online store, set up a workflow that sends a follow-up email through Mailchimp after a customer makes a purchase. Integrate with Shopify to pull in the purchase data and tailor the follow-up message, suggesting related products or asking for reviews. diff --git a/components/mailcoach/README.md b/components/mailcoach/README.md new file mode 100644 index 0000000000000..b3280de644200 --- /dev/null +++ b/components/mailcoach/README.md @@ -0,0 +1,11 @@ +# Overview + +The Mailcoach API allows you to manage email campaigns and subscriber lists efficiently. On Pipedream, you can trigger workflows using events from Mailcoach, automate the process of syncing subscriber data, manage campaigns, and integrate these functions with other apps to create a powerful email marketing automation system. By leveraging Pipedream's serverless platform, you can connect the Mailcoach API with other services to extend functionality without writing extensive code, and automate repetitive tasks, saving time and resources. + +# Example Use Cases + +- **Sync New Subscribers to a CRM**: Whenever a new subscriber is added to Mailcoach, use Pipedream to automatically add that subscriber to your Customer Relationship Management (CRM) system like Salesforce or HubSpot. This ensures your sales and marketing teams have the latest information at their fingertips. + +- **Automate Campaign Triggers Based on User Activity**: Use Pipedream to watch for specific user activities in your app via webhooks or direct API calls. When a certain action is taken, like a purchase or sign-up, trigger a Mailcoach campaign to send out a tailored email sequence to the user, improving engagement and retention. + +- **Generate Reports and Analytics**: Regularly fetch campaign data from Mailcoach using Pipedream and send it to a data visualization tool like Google Sheets or Tableau. By doing so, you can create custom reports that provide insights into your email campaign's performance, subscriber growth, and engagement trends. diff --git a/components/mailercloud/README.md b/components/mailercloud/README.md new file mode 100644 index 0000000000000..e658182a10982 --- /dev/null +++ b/components/mailercloud/README.md @@ -0,0 +1,11 @@ +# Overview + +Mailercloud API opens the door to robust email marketing and automation possibilities. Within Pipedream's serverless environment, you can leverage this API to create, send, and track email campaigns, manage subscriber lists, and analyze performance metrics. It fits seamlessly into workflows that require communication with customers or internal teams, growing and nurturing leads, or automating email-based tasks. + +# Example Use Cases + +- **Automated Lead Nurturing Emails**: Trigger a sequence of nurturing emails from Mailercloud when a new lead is added to a CRM like Salesforce. As leads progress through your pipeline, send personalized content based on their interaction with your website or product. + +- **Subscription Confirmation Workflow**: After a user subscribes to your service, use Mailercloud via Pipedream to send a confirmation email and manage follow-up communications. Connect this with Shopify to flag new customers and ensure they're warmly welcomed into your community. + +- **Performance Alert System**: Set up an alert system where Mailercloud sends emails to your team when certain performance metrics are hit or missed. Integrate with Google Sheets or Data Studio to monitor KPIs and automate reporting, keeping everyone informed and proactive. diff --git a/components/mailerlite/README.md b/components/mailerlite/README.md index b8b94dcbd2dd1..d25fd551e47c3 100644 --- a/components/mailerlite/README.md +++ b/components/mailerlite/README.md @@ -1,8 +1,11 @@ # Overview -With Mailerlite's API, you can: +The Mailerlite API offers the ability to extend your email marketing efforts through programmatic access to your Mailerlite account. Use it on Pipedream to automate email campaigns, manage subscribers, analyze the results of your outreach, and integrate with your existing tech stack. By crafting workflows on Pipedream, you can connect Mailerlite to a vast array of services, triggering actions based on subscriber behavior, synchronizing data across platforms, or enriching subscriber profiles for targeted campaigns. -- Build a custom signup form to add subscribers to your mailing list -- Send newsletters and automated emails -- Segment your subscribers into groups -- View statistics about your subscribers and email campaigns +# Example Use Cases + +- **Subscriber Lifecycle Management**: Automatically add new subscribers to Mailerlite from various sources like Shopify purchases, Typeform form submissions, or Google Sheets entries. Set up a Pipedream workflow that triggers when a new entry is added to any of these platforms, and use the Mailerlite API to subscribe them to your list, tag them based on their interests, or even trigger a welcome email sequence. + +- **Campaign Results to Dashboard**: Post the performance metrics of your Mailerlite email campaigns directly to a business analytics dashboard like Google Data Studio or Tableau. Use Pipedream to schedule a workflow that retrieves campaign statistics from Mailerlite and formats the data before sending it to your preferred dashboard tool, ensuring real-time insights into your email marketing effectiveness. + +- **Real-Time Subscriber Engagement**: Set up a Pipedream workflow that listens for updates from Mailerlite, like email opens or link clicks. When a subscriber interacts with your email, use that as a trigger to perform actions in other apps like creating a task in Asana for follow-up, sending a personalized message via Twilio, or logging the event in a CRM like HubSpot for further nurturing. diff --git a/components/mailerlite/actions/create-subscriber/create-subscriber.mjs b/components/mailerlite/actions/create-subscriber/create-subscriber.mjs index dc5496055e11f..116141dce46c7 100644 --- a/components/mailerlite/actions/create-subscriber/create-subscriber.mjs +++ b/components/mailerlite/actions/create-subscriber/create-subscriber.mjs @@ -4,8 +4,8 @@ import mailerlite from "../../mailerlite.app.mjs"; export default { key: "mailerlite-create-subscriber", name: "Create Subscriber", - description: "Create a new subscriber. [See docs](https://developers.mailerlite.com/docs/subscribers.html#create-update-subscriber)", - version: "0.0.3", + description: "Create a new subscriber. [See the documentation](https://developers.mailerlite.com/docs/subscribers.html#create-update-subscriber)", + version: "0.0.4", type: "action", props: { mailerlite, @@ -34,11 +34,16 @@ export default { async run({ $ }) { const data = { email: this.email, - name: this.name, - type: this.type, + fields: { + name: this.name, + }, + status: this.type, }; - const resp = await this.mailerlite.createSubscriber(utils.removeUndefined(data)); + const resp = await this.mailerlite.createSubscriber({ + $, + data: utils.removeUndefined(data), + }); $.export("$summary", "Successfully created subscriber"); return resp; }, diff --git a/components/mailerlite/actions/list-subscribers/list-subscribers.mjs b/components/mailerlite/actions/list-subscribers/list-subscribers.mjs index ce3438e6ab8a1..53b95450f0895 100644 --- a/components/mailerlite/actions/list-subscribers/list-subscribers.mjs +++ b/components/mailerlite/actions/list-subscribers/list-subscribers.mjs @@ -4,8 +4,8 @@ import mailerlite from "../../mailerlite.app.mjs"; export default { key: "mailerlite-list-subscribers", name: "List Subscribers", - description: "Lists all subscribers in a group. [See the docs here](https://developers.mailerlite.com/docs/subscribers.html#list-all-subscribers)", - version: "0.0.3", + description: "Lists all subscribers in a group. [See the documentation](https://developers.mailerlite.com/docs/subscribers.html#list-all-subscribers)", + version: "0.0.4", type: "action", props: { mailerlite, @@ -42,7 +42,11 @@ export default { let resp; do { - resp = await this.mailerlite.listSubscribers(this.group, params); + resp = await this.mailerlite.listSubscribers({ + $, + group: this.group, + params, + }); subscribers.push(...resp); params.offset += params.limit; } while (resp?.length === params.limit); diff --git a/components/mailerlite/actions/remove-subscriber-from-group/remove-subscriber-from-group.mjs b/components/mailerlite/actions/remove-subscriber-from-group/remove-subscriber-from-group.mjs index 8d258e860201c..c0616f5fceb27 100644 --- a/components/mailerlite/actions/remove-subscriber-from-group/remove-subscriber-from-group.mjs +++ b/components/mailerlite/actions/remove-subscriber-from-group/remove-subscriber-from-group.mjs @@ -3,8 +3,8 @@ import mailerlite from "../../mailerlite.app.mjs"; export default { key: "mailerlite-remove-subscriber-from-group", name: "Remove Subscriber From Group", - description: "Removes single subscriber from specified group. [See the docs here](https://developers.mailerlite.com/docs/groups.html#unassign-subscriber-from-a-group)", - version: "0.0.4", + description: "Removes single subscriber from specified group. [See the documentation](https://developers.mailerlite.com/docs/groups.html#unassign-subscriber-from-a-group)", + version: "0.0.5", type: "action", props: { mailerlite, @@ -37,10 +37,11 @@ export default { }, }, async run({ $ }) { - const response = await this.mailerlite.removeSubscriberFromGroup( - this.group, - encodeURIComponent(this.subscriber), - ); + const response = await this.mailerlite.removeSubscriberFromGroup({ + $, + subscriber: this.subscriber, + group: this.group, + }); $.export("$summary", "Removed subscriber from group"); diff --git a/components/mailerlite/actions/subscribe-to-group/subscribe-to-group.mjs b/components/mailerlite/actions/subscribe-to-group/subscribe-to-group.mjs index 384cd9611dfe4..a1917d7bb125d 100644 --- a/components/mailerlite/actions/subscribe-to-group/subscribe-to-group.mjs +++ b/components/mailerlite/actions/subscribe-to-group/subscribe-to-group.mjs @@ -3,8 +3,8 @@ import mailerlite from "../../mailerlite.app.mjs"; export default { key: "mailerlite-subscribe-to-group", name: "Subscribe to MailerLite Group", - description: "Add a subscriber to a group. [See the docs here](https://developers.mailerlite.com/docs/groups.html#assign-subscriber-to-a-group)", - version: "0.3.2", + description: "Add a subscriber to a group. [See the documentation](https://developers.mailerlite.com/docs/groups.html#assign-subscriber-to-a-group)", + version: "0.3.3", type: "action", props: { mailerlite, @@ -14,19 +14,20 @@ export default { "group", ], }, - email: { + subscriber: { propDefinition: [ mailerlite, "subscriber", ], - description: "Email of the active subscriber to add to group", + description: "ID of the active subscriber to add to group", }, }, async run({ $ }) { - const data = { - email: this.email, - }; - const resp = await this.mailerlite.addSubscriberToGroup(data, this.group); + const resp = await this.mailerlite.addSubscriberToGroup({ + $, + subscriber: this.subscriber, + group: this.group, + }); $.export("$summary", "Added subscriber to group"); return resp; }, diff --git a/components/mailerlite/actions/update-subscriber/update-subscriber.mjs b/components/mailerlite/actions/update-subscriber/update-subscriber.mjs index 8fa32ad2ecf28..9adc73d286ed3 100644 --- a/components/mailerlite/actions/update-subscriber/update-subscriber.mjs +++ b/components/mailerlite/actions/update-subscriber/update-subscriber.mjs @@ -3,8 +3,8 @@ import mailerlite from "../../mailerlite.app.mjs"; export default { key: "mailerlite-update-subscriber", name: "Update Subscriber", - description: "Updates single active subscriber. [See docs](https://developers.mailerlite.com/docs/subscribers.html#create-update-subscriber)", - version: "0.0.3", + description: "Updates single active subscriber. [See the documentation](https://developers.mailerlite.com/docs/subscribers.html#create-update-subscriber)", + version: "0.0.4", type: "action", props: { mailerlite, @@ -34,6 +34,9 @@ export default { }, async additionalProps() { const props = {}; + if (!this.fields?.length) { + return props; + } for (const field of this.fields) { props[field.value] = { type: "string", @@ -45,14 +48,20 @@ export default { }, async run({ $ }) { const fields = {}; - for (const field of this.fields) { - fields[field.value] = this[field.value]; + if (this.fields?.length) { + for (const field of this.fields) { + fields[field.value] = this[field.value]; + } } const data = { - type: this.type, + status: this.type, fields, }; - const resp = await this.mailerlite.updateSubscriber(data, this.subscriber); + const resp = await this.mailerlite.updateSubscriber({ + $, + subscriber: this.subscriber, + data, + }); $.export("$summary", "Successfully updated subscriber"); return resp; }, diff --git a/components/mailerlite/mailerlite.app.mjs b/components/mailerlite/mailerlite.app.mjs index 5ef43d639dfac..2aaed0ff56d2d 100644 --- a/components/mailerlite/mailerlite.app.mjs +++ b/components/mailerlite/mailerlite.app.mjs @@ -1,4 +1,4 @@ -import MailerLite from "mailerlite-api-v2-node"; +import { axios } from "@pipedream/platform"; import constants from "./common/constants.mjs"; export default { @@ -54,11 +54,14 @@ export default { limit, offset, }; - const subscribers = await this.listSubscribers(group, params); + const subscribers = await this.listSubscribers({ + group, + params, + }); return { options: subscribers.map((subscriber) => ({ - label: subscriber.name, - value: subscriber.email, + label: subscriber.email, + value: subscriber.id, })), context: { offset: offset + limit, @@ -77,7 +80,9 @@ export default { limit, offset, }; - const groups = await this.listGroups(params); + const groups = await this.listGroups({ + params, + }); return { options: groups.map((group) => ({ label: group.name, @@ -106,67 +111,100 @@ export default { }, }, methods: { - _getApiKey() { - return this.$auth.api_key; - }, - async _getClient(baseAPI) { - let options; - if (baseAPI === "connect") { - options = { - baseURL: "https://connect.mailerlite.com/api", - headers: { - Authorization: `Bearer ${this._getApiKey()}`, - }, - }; - } - const client = MailerLite.default; - return client(this._getApiKey(), options); - }, - async listGroups(params) { - const client = await this._getClient(); - return client.getGroups(params); - }, - async listSubscribers(group, params = {}) { - const client = await this._getClient(); - if (group) { - const { type = "active" } = params; - delete params.type; - return client.getGroupSubscribersByType(group, type, params); - } - // getSubscribers returns active subscribers only - return client.getSubscribers(params); - }, - async listFields() { - const client = await this._getClient(); - return client.getFields(); - }, - async listCampaigns(status = "sent") { - const client = await this._getClient(); - return client.getCampaigns(status); - }, - async createSubscriber(data) { - const client = await this._getClient(); - return client.addSubscriber(data); - }, - async createHook(data) { - const client = await this._getClient("connect"); - return client.createWebhook(data); - }, - async updateSubscriber(data, subscriber) { - const client = await this._getClient(); - return client.updateSubscriber(subscriber, data); - }, - async addSubscriberToGroup(data, group) { - const client = await this._getClient(); - return client.addSubscriberToGroup(group, data); - }, - async removeHook(hookId) { - const client = await this._getClient("connect"); - return client.removeWebhook(hookId); - }, - async removeSubscriberFromGroup(group, subscriber) { - const client = await this._getClient(); - return client.removeGroupSubscriber(group, subscriber); + _baseUrl() { + return "https://connect.mailerlite.com/api"; + }, + _makeRequest({ + $ = this, + path, + ...args + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.api_key}`, + }, + ...args, + }); + }, + async listGroups(opts = {}) { + const { data } = await this._makeRequest({ + path: "/groups", + ...opts, + }); + return data; + }, + async listSubscribers({ + group, params = {}, ...opts + }) { + const { type = "active" } = params; + delete params.type; + params["filter[status]"] = type; + const { data } = await this._makeRequest({ + path: group + ? `/groups/${group}/subscribers` + : "/subscribers", + params, + ...opts, + }); + return data; + }, + async listFields(opts = {}) { + const { data } = await this._makeRequest({ + path: "/fields", + ...opts, + }); + return data; + }, + createSubscriber(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/subscribers", + ...opts, + }); + }, + createHook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + updateSubscriber({ + subscriber, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/subscribers/${subscriber}`, + ...opts, + }); + }, + addSubscriberToGroup({ + subscriber, group, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/subscribers/${subscriber}/groups/${group}`, + ...opts, + }); + }, + removeHook({ + hookId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${hookId}`, + ...opts, + }); + }, + removeSubscriberFromGroup({ + subscriber, group, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/subscribers/${subscriber}/groups/${group}`, + ...opts, + }); }, }, }; diff --git a/components/mailerlite/package.json b/components/mailerlite/package.json index 2981e97495f38..f57022594468f 100644 --- a/components/mailerlite/package.json +++ b/components/mailerlite/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/mailerlite", - "version": "1.1.0", + "version": "1.1.1", "description": "Pipedream Mailerlite Components", "main": "mailerlite.app.mjs", "keywords": [ @@ -14,7 +14,7 @@ "access": "public" }, "dependencies": { - "lodash.pickby": "^4.6.0", - "mailerlite-api-v2-node": "^1.2.0" + "@pipedream/platform": "^3.0.3", + "lodash.pickby": "^4.6.0" } } diff --git a/components/mailerlite/sources/common/base.mjs b/components/mailerlite/sources/common/base.mjs index 631ac092a7ce4..0e001bdf3ec99 100644 --- a/components/mailerlite/sources/common/base.mjs +++ b/components/mailerlite/sources/common/base.mjs @@ -10,15 +10,19 @@ export default { hooks: { async activate() { const { data } = await this.mailerlite.createHook({ - url: this.http.endpoint, - events: this.getEvents(), + data: { + url: this.http.endpoint, + events: this.getEvents(), + }, }); this._setHookId(data.id); }, async deactivate() { - const id = this._getHookId("hookId"); - await this.mailerlite.removeHook(id); + const hookId = this._getHookId("hookId"); + await this.mailerlite.removeHook({ + hookId, + }); }, }, methods: { diff --git a/components/mailerlite/sources/subscriber-added-from-form/subscriber-added-from-form.mjs b/components/mailerlite/sources/subscriber-added-from-form/subscriber-added-from-form.mjs deleted file mode 100644 index 93dcc98fdc2fd..0000000000000 --- a/components/mailerlite/sources/subscriber-added-from-form/subscriber-added-from-form.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import common from "../common/base.mjs"; -import sampleEmit from "./test-event.mjs"; - -export default { - ...common, - key: "mailerlite-subscriber-added-from-form", - name: "New Subscriber Added From Form (Instant)", - description: "Emit new event when a new subscriber is added though a form.", - version: "0.0.1", - type: "source", - props: { - ...common.props, - http: "$.interface.http", - db: "$.service.db", - }, - methods: { - ...common.methods, - getEvents() { - return [ - "subscriber.added_through_form", - ]; - }, - getDataToEmit({ - id, - created_at: createdAt, - }) { - return { - id: id, - summary: `A new subscriber with Id: ${id} has been added though a form!`, - ts: new Date(createdAt), - }; - }, - }, - sampleEmit, -}; diff --git a/components/mailerlite/sources/subscriber-added-from-form/test-event.mjs b/components/mailerlite/sources/subscriber-added-from-form/test-event.mjs deleted file mode 100644 index 7f9bf31e2db25..0000000000000 --- a/components/mailerlite/sources/subscriber-added-from-form/test-event.mjs +++ /dev/null @@ -1,30 +0,0 @@ -export default { - id: '123456789012345678', - email: 'email@test.com', - status: 'active', - source: 'api', - sent: null, - opens_count: null, - clicks_count: null, - open_rate: 0, - click_rate: 0, - ip_address: null, - subscribed_at: '2023-10-25T17:02:54.000000Z', - unsubscribed_at: null, - created_at: '2023-10-25T17:02:54.000000Z', - updated_at: '2023-10-25T17:02:54.000000Z', - deleted_at: null, - forget_at: null, - fields: { - name: null, - last_name: null, - company: null, - country: null, - city: null, - phone: null, - state: null, - z_i_p: null - }, - opted_in_at: null, - optin_ip: null -} \ No newline at end of file diff --git a/components/mailerlite/sources/subscriber-added-to-group/subscriber-added-to-group.mjs b/components/mailerlite/sources/subscriber-added-to-group/subscriber-added-to-group.mjs new file mode 100644 index 0000000000000..67475f65e5b3a --- /dev/null +++ b/components/mailerlite/sources/subscriber-added-to-group/subscriber-added-to-group.mjs @@ -0,0 +1,35 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "mailerlite-subscriber-added-to-group", + name: "New Subscriber Added To Group (Instant)", + description: "Emit new event when a new subscriber is added to a group.", + version: "0.0.2", + type: "source", + props: { + ...common.props, + http: "$.interface.http", + db: "$.service.db", + }, + methods: { + ...common.methods, + getEvents() { + return [ + "subscriber.added_to_group", + ]; + }, + getDataToEmit({ + subscriber, + group, + }) { + return { + id: `${subscriber.id}-${group.id}`, + summary: `A new subscriber with Id: ${subscriber.id} has been added to a group`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/mailerlite/sources/subscriber-added-to-group/test-event.mjs b/components/mailerlite/sources/subscriber-added-to-group/test-event.mjs new file mode 100644 index 0000000000000..e7aba07a49be3 --- /dev/null +++ b/components/mailerlite/sources/subscriber-added-to-group/test-event.mjs @@ -0,0 +1,37 @@ +export default { + "type": "subscriber.added_to_group", + "subscriber": { + "id": "1234567890", + "email": "test@example.com", + "status": "active", + "source": "api", + "sent": 0, + "opens_count": 0, + "clicks_count": 0, + "open_rate": 0, + "click_rate": 0, + "ip_address": null, + "subscribed_at": "2022-05-03T21:07:02.000000Z", + "unsubscribed_at": null, + "created_at": "2022-05-03T21:07:02.000000Z", + "updated_at": "2022-05-03T21:07:02.000000Z", + "deleted_at": null, + "forget_at": null, + "fields": { + "name": null, + "last_name": null, + "company": null, + "country": null, + "city": null, + "phone": null, + "state": null, + "z_i_p": null + }, + "opted_in_at": null, + "optin_ip": null + }, + "group": { + "id": "54201175883383850", + "name": "group" + } +} \ No newline at end of file diff --git a/components/mailerlite/sources/subscriber-created/subscriber-created.mjs b/components/mailerlite/sources/subscriber-created/subscriber-created.mjs index 421bbb64f6333..06804fe3bb93b 100644 --- a/components/mailerlite/sources/subscriber-created/subscriber-created.mjs +++ b/components/mailerlite/sources/subscriber-created/subscriber-created.mjs @@ -6,7 +6,7 @@ export default { key: "mailerlite-subscriber-created", name: "New Subscriber Created (Instant)", description: "Emit new event when a new subscriber is created.", - version: "0.0.1", + version: "0.0.2", type: "source", props: { ...common.props, @@ -25,7 +25,7 @@ export default { created_at: createdAt, }) { return { - id: id, + id, summary: `A new subscriber with Id: ${id} has been created!`, ts: new Date(createdAt), }; diff --git a/components/mailerlite/sources/subscriber-unsubscribed/subscriber-unsubscribed.mjs b/components/mailerlite/sources/subscriber-unsubscribed/subscriber-unsubscribed.mjs index a243d58761db2..58a3f9946d894 100644 --- a/components/mailerlite/sources/subscriber-unsubscribed/subscriber-unsubscribed.mjs +++ b/components/mailerlite/sources/subscriber-unsubscribed/subscriber-unsubscribed.mjs @@ -6,7 +6,7 @@ export default { key: "mailerlite-subscriber-unsubscribed", name: "New Subscriber Unsubscribed (Instant)", description: "Emit new event when a subscriber is unsubscribed.", - version: "0.0.1", + version: "0.0.2", type: "source", props: { ...common.props, @@ -25,7 +25,7 @@ export default { created_at: createdAt, }) { return { - id: id, + id, summary: `The subscriber with Id: ${id} has been unsubscribed!`, ts: new Date(createdAt), }; diff --git a/components/mailersend/README.md b/components/mailersend/README.md index 6dd353e971795..67ab0affc0ccf 100644 --- a/components/mailersend/README.md +++ b/components/mailersend/README.md @@ -1,5 +1,11 @@ # Overview -Mailersend is a great tool for building email marketing campaigns. With -Mailersend, you can easily create beautiful email newsletters, manage your -subscribers, and track the performance of your campaigns. +The MailerSend API integrates with Pipedream to create powerful email automation workflows, enabling you to send transactional emails, create and manage templates, and track email activities like opens or clicks. With these capabilities, you can automate routine communications, personalize mass mailings based on user actions or data, and gain insights into your email campaign performances, all within the context of a serverless Pipedream workflow. + +# Example Use Cases + +- **Transactional Email on New User Sign-Up**: Trigger an email with a welcome message and onboarding instructions using MailerSend whenever a new user signs up on your platform. You can capture the sign-up event from your app's API, a database like MySQL, or a service like Auth0, and use that to trigger the workflow on Pipedream. + +- **Dynamic Content for Cart Abandonment Emails**: Connect MailerSend with an e-commerce platform such as Shopify. Monitor cart status and send customized reminder emails with dynamic content, such as cart items and a checkout link, when a user abandons their shopping cart. + +- **Email Campaign Performance Reporting**: After sending out a campaign via MailerSend, use Pipedream to connect to analytics apps like Google Sheets or Data Studio. Automatically log data on email opens, clicks, and bounces, and generate performance reports to measure and improve your email marketing strategies. diff --git a/components/mailgenius/actions/get-daily-limit/get-daily-limit.mjs b/components/mailgenius/actions/get-daily-limit/get-daily-limit.mjs new file mode 100644 index 0000000000000..be43034f60fb3 --- /dev/null +++ b/components/mailgenius/actions/get-daily-limit/get-daily-limit.mjs @@ -0,0 +1,22 @@ +import app from "../../mailgenius.app.mjs"; + +export default { + key: "mailgenius-get-daily-limit", + name: "Get Daily Limit", + description: "Returns daily limit for api token, how many email tests are used in last 24 hours and how many are still remaining for use. [See the documentation](https://app.mailgenius.com/api-docs/index.html)", + version: "0.0.1", + type: "action", + props: { + app, + }, + + async run({ $ }) { + const response = await this.app.getDailyLimit({ + $, + }); + + $.export("$summary", `Your account has '${response.daily_tests_remaining}' remaining tests today`); + + return response; + }, +}; diff --git a/components/mailgenius/actions/get-email-audit/get-email-audit.mjs b/components/mailgenius/actions/get-email-audit/get-email-audit.mjs new file mode 100644 index 0000000000000..174de39139a19 --- /dev/null +++ b/components/mailgenius/actions/get-email-audit/get-email-audit.mjs @@ -0,0 +1,22 @@ +import app from "../../mailgenius.app.mjs"; + +export default { + key: "mailgenius-get-email-audit", + name: "Get Email Audit", + description: "Returns generated test email, limit exceeded if daily limit is reached. [See the documentation](https://app.mailgenius.com/api-docs/index.html)", + version: "0.0.1", + type: "action", + props: { + app, + }, + + async run({ $ }) { + const response = await this.app.emailAudit({ + $, + }); + + $.export("$summary", `Your test email is: ${response.test_email}. Please send an email to this address using the account you want to test`); + + return response; + }, +}; diff --git a/components/mailgenius/actions/get-email-result/get-email-result.mjs b/components/mailgenius/actions/get-email-result/get-email-result.mjs new file mode 100644 index 0000000000000..9a0635812f1b1 --- /dev/null +++ b/components/mailgenius/actions/get-email-result/get-email-result.mjs @@ -0,0 +1,29 @@ +import app from "../../mailgenius.app.mjs"; + +export default { + key: "mailgenius-get-email-result", + name: "Get Email Results", + description: "Returns the results of the test. [See the documentation](https://app.mailgenius.com/api-docs/index.html)", + version: "0.0.1", + type: "action", + props: { + app, + slug: { + propDefinition: [ + app, + "slug", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.emailResult({ + $, + slug: this.slug, + }); + + $.export("$summary", `The test results are '${response.status}'`); + + return response; + }, +}; diff --git a/components/mailgenius/mailgenius.app.mjs b/components/mailgenius/mailgenius.app.mjs new file mode 100644 index 0000000000000..f851d3c6e8ec7 --- /dev/null +++ b/components/mailgenius/mailgenius.app.mjs @@ -0,0 +1,71 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "mailgenius", + propDefinitions: { + slug: { + type: "string", + label: "Model", + description: "Specifies the model to be used for the request", + async options() { + const response = await this.getEmails(); + const emailsSlugs = response.test_emails; + return emailsSlugs.map(({ + slug, test_email, + }) => ({ + value: slug, + label: test_email, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://app.mailgenius.com"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_token}`, + accept: "*/*", + }, + }); + }, + async emailAudit(args = {}) { + return this._makeRequest({ + path: "/external/api/email-audit", + ...args, + }); + }, + async emailResult({ + slug, ...args + }) { + return this._makeRequest({ + path: `/external/api/email-result/${slug}`, + ...args, + }); + }, + async getDailyLimit(args = {}) { + return this._makeRequest({ + path: "/external/api/daily_limit", + ...args, + }); + }, + async getEmails(args = {}) { + return this._makeRequest({ + path: "/external/api/audits", + ...args, + }); + }, + }, +}; diff --git a/components/mailgenius/package.json b/components/mailgenius/package.json new file mode 100644 index 0000000000000..f9923cd985cd9 --- /dev/null +++ b/components/mailgenius/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/mailgenius", + "version": "0.1.0", + "description": "Pipedream MailGenius Components", + "main": "mailgenius.app.mjs", + "keywords": [ + "pipedream", + "mailgenius" + ], + "homepage": "https://pipedream.com/apps/mailgenius", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/mailgun/README.md b/components/mailgun/README.md index c5d1ca65592a1..4e7dd1d8c00e1 100644 --- a/components/mailgun/README.md +++ b/components/mailgun/README.md @@ -1,11 +1,11 @@ # Overview -Mailgun allows you to send and receive emails using their API. With Mailgun, -you can build a variety of applications and services that can send or receive -emails. Here are a few examples of what you can build with Mailgun: - -- A simple email service that can send or receive emails -- An email marketing service that can send mass emails -- A notification service that can send emails when certain events occur -- A task management service that can send emails when tasks are due -- A customer support service that can send or receive emails from customers +The Mailgun API on Pipedream is a potent tool for automating email operations without the overhead of managing a full-fledged email server. It offers capabilities to send, receive, track, and store emails with ease. With Pipedream's serverless platform, you can trigger workflows using Mailgun events, such as inbound emails or delivery status changes, and connect them to hundreds of other services to streamline communication, marketing, and notification systems within your ecosystem. + +# Example Use Cases + +- **Transactional Email Automation**: Automatically send transactional emails, like order confirmations or password resets, using Mailgun when a new entry is added to a Google Sheets spreadsheet or a new order is placed on an e-commerce platform like Shopify. + +- **Email Campaign Analytics**: Collect and analyze email campaign data by connecting Mailgun with a data visualization tool like Google Data Studio. Each time an email is sent, delivered, or opened, Pipedream can process this data and update your analytics dashboards in real-time. + +- **Customer Support Ticketing System**: Create a support ticket in a service like Zendesk or Trello when Mailgun processes an inbound email. Use Pipedream to parse the email content and automatically categorize and prioritize the ticket based on predefined criteria. diff --git a/components/mailify/README.md b/components/mailify/README.md index 2282bc8efd2b9..651ffb104c5ef 100644 --- a/components/mailify/README.md +++ b/components/mailify/README.md @@ -1,8 +1,11 @@ # Overview -With the Mailify API, you can build applications that allow users to: +Mailify API enables you to harness the power of email marketing by automating your email campaigns, managing contacts, analyzing results, and integrating with other services. With Pipedream, you can create complex workflows connecting Mailify with various apps to respond to events and triggers, thereby streamlining your marketing efforts and saving time. -- Send and receive emails -- Manage their contact lists -- Create and manage email campaigns -- Track the results of their email campaigns +# Example Use Cases + +- **Automated Welcome Email Series**: Use Mailify to send a sequence of welcome emails when a new user signs up on your platform. Trigger a Pipedream workflow with a sign-up event from your app, add the new user to Mailify's contact list, and schedule a series of emails that educate them about your services. + +- **Dynamic Campaigns Based on User Activity**: Tailor your email campaigns based on user interactions. With Pipedream, you can listen for specific actions performed by users, like items added to a cart or a service subscription, and use Mailify to send personalized emails that encourage users to take the next step, such as completing a purchase or upgrading their subscription. + +- **Email Performance Analytics Dashboard**: Collect and analyze your email campaign data with Mailify and send it to a dashboard app like Google Sheets or Tableau through Pipedream. Create a workflow that periodically exports campaign statistics from Mailify and updates your dashboard, giving you real-time insights into your email marketing performance. diff --git a/components/mailjet/README.md b/components/mailjet/README.md index 50f7e7ad1a13c..25cb01ca32611 100644 --- a/components/mailjet/README.md +++ b/components/mailjet/README.md @@ -1,13 +1,11 @@ # Overview -With the Mailjet API, you can build a variety of applications, including: - -- An email marketing application that allows you to send newsletters, - promotional emails, and other types of marketing communications to your - subscribers. -- A customer support application that allows you to manage customer inquiries - and tickets via email. -- A lead generation application that allows you to collect leads via email - forms and track their progress through your sales pipeline. -- An event management application that allows you to send invitations, manage - registrations, and send out event reminders via email. +Mailjet's API offers the power to craft, send, and track emails with finesse. You can wield it to automate email sequences, synchronize email lists with your databases, and send transactional emails with personalized content. It's a playground for creating tailored email campaigns and dissecting campaign performance with rich analytics. + +# Example Use Cases + +- **Transactional Email Automation**: Automate the sending of transactional emails using Mailjet when users complete specific actions on your app, like signing up, making a purchase, or resetting their password. Use Pipedream to watch for these events and trigger the Mailjet API to send a tailored email response. + +- **Synchronize Contacts for Marketing Campaigns**: Keep your Mailjet contact lists up-to-date by syncing them with your CRM system. Whenever a new contact is added or updated in your CRM, a Pipedream workflow can update the corresponding Mailjet contact list, ensuring your marketing campaigns target the right audience. + +- **Email Campaign Performance Tracking**: After sending out campaigns via Mailjet, use a Pipedream workflow to fetch the delivery statistics and store them in a database or a Google Sheet. This enables you to analyze delivery rates, open rates, and click-through rates to refine future email strategies. diff --git a/components/mailmodo/README.md b/components/mailmodo/README.md index a2914362c7641..c5993812efde5 100644 --- a/components/mailmodo/README.md +++ b/components/mailmodo/README.md @@ -1,14 +1,11 @@ # Overview -Mailmodo is a powerful API that allows you to create and send custom email -newsletters. With Mailmodo, you can create beautiful, responsive email -newsletters that look great on any device. Plus, Mailmodo makes it easy to -manage your subscribers and track your results. - -Here are some examples of what you can build with the Mailmodo API: - -- A custom email newsletter for your business -- A weekly email digest of your blog posts -- A monthly email update for your customers -- An email course or tutorial -- A promotional email campaign +Mailmodo's API turns email marketing into a more powerful tool by allowing you to automate and personalize your email campaigns. Through Pipedream, you can trigger workflows based on events from other apps, update customer data in real time, send transactional emails without leaving your app, and analyze the performance of your campaigns. By leveraging Pipedream's serverless platform, you can integrate Mailmodo with countless apps to enhance your marketing strategies, improve customer engagement, and streamline your communication processes. + +# Example Use Cases + +- **Order Confirmation Emails**: Automate the sending of personalized order confirmation emails via Mailmodo when a new order is placed in your e-commerce platform like Shopify or WooCommerce. Include details such as order summary, estimated delivery date, and a thank you message. + +- **Event Registration Follow-ups**: After a participant signs up for an event through an app like Eventbrite, use Mailmodo to send a detailed itinerary, reminders, and personalized content based on the participant's interests or past behavior. + +- **Survey Feedback Integration**: Send a Mailmodo email survey to users who have interacted with your customer service platform like Zendesk. Once they complete the survey, trigger workflows that update their customer profile with feedback scores and comments for future reference and action. diff --git a/components/mailninja/README.md b/components/mailninja/README.md new file mode 100644 index 0000000000000..c8fd53525b876 --- /dev/null +++ b/components/mailninja/README.md @@ -0,0 +1,11 @@ +# Overview + +The MailNinja API enables Pipedream users to execute targeted email marketing campaigns by providing a suite of tools to create, send, and track the performance of emails. With MailNinja on Pipedream, you can automate your email outreach, react to subscriber actions, and integrate email data with other services, enriching your marketing stack and enhancing user engagement strategies. + +# Example Use Cases + +- **Automated Welcome Email Sequence**: Trigger a series of welcome emails using MailNinja when a new user signs up for your product. You can start with a friendly introduction and follow up with more information about your offerings over the next few days. + +- **Subscriber Activity Sync to CRM**: Synchronize subscriber activities and engagement metrics from MailNinja to a CRM like Salesforce or HubSpot. This helps maintain updated contact profiles and enables personalized follow-up actions based on user interactions. + +- **E-commerce Purchase Follow-up**: After a customer makes a purchase on an e-commerce platform like Shopify, use MailNinja to send personalized thank-you emails, ask for reviews, or offer discounts on future purchases to encourage retention. diff --git a/components/mailninja/package.json b/components/mailninja/package.json index 2e0f01339badd..e19eccbe656ef 100644 --- a/components/mailninja/package.json +++ b/components/mailninja/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/mailrefine/README.md b/components/mailrefine/README.md index 46e8d439f19b0..f31beee7d03d1 100644 --- a/components/mailrefine/README.md +++ b/components/mailrefine/README.md @@ -1,9 +1,11 @@ # Overview -With the Mailrefine API you can: +The Mailrefine API provides a robust solution for email validation and list cleaning, allowing you to improve the deliverability and effectiveness of your email marketing campaigns. By using Pipedream, you can automate processes that involve validating bulk email lists, segregating invalid or risky email addresses, and enriching your email data with additional information. This API, when hooked into Pipedream's serverless platform, can quickly become part of a powerful automation chain that enhances data flow between your email marketing tools and other business apps. -- Create a new mailing list from a CSV file -- Add subscribers to an existing mailing list -- Remove subscribers from an existing mailing list -- Update subscriber information -- Send a batch email to a mailing list +# Example Use Cases + +- **Email Validation on New Sign-ups**: Automate the process of validating emails as soon as users sign up through your platform. Set up a Pipedream workflow that triggers when a new user registers, sends their email through the Mailrefine API to check its validity, and then updates the user's status or sends a notification based on the validation results. + +- **Scheduled List Cleaning**: Create a workflow that periodically cleans your email lists. Use a scheduled trigger to initiate the workflow, process the list through Mailrefine to remove invalid emails, and update your email marketing platform, such as Mailchimp, with the cleaned list to maintain a high deliverability rate. + +- **Response Handling for Email Campaigns**: Construct a workflow that manages responses from an email campaign. After sending out emails through a service like SendGrid, use the responses to trigger a Pipedream workflow. Check each responder’s email for validity using Mailrefine, and log valid respondents into a CRM like Salesforce, while tagging invalid ones for further review. diff --git a/components/mails_so/actions/validate-email/validate-email.mjs b/components/mails_so/actions/validate-email/validate-email.mjs new file mode 100644 index 0000000000000..7051d7bdb881c --- /dev/null +++ b/components/mails_so/actions/validate-email/validate-email.mjs @@ -0,0 +1,28 @@ +import app from "../../mails_so.app.mjs"; + +export default { + key: "mails_so-validate-email", + name: "Validate Email", + description: "Send an email address to be validated by the API. [See the documentation](https://www.postman.com/joint-operations-engineer-25774813/workspace/mails-so/request/36028084-aa3b6359-bbdd-451b-8596-548a4d913f38)", + version: "0.0.1", + type: "action", + props: { + app, + email: { + propDefinition: [ + app, + "email", + ], + }, + }, + async run({ $ }) { + const response = await this.app.validateEmail({ + $, + params: { + email: this.email, + }, + }); + $.export("$summary", `Email validation result: '${response.data.result}'`); + return response; + }, +}; diff --git a/components/mails_so/mails_so.app.mjs b/components/mails_so/mails_so.app.mjs new file mode 100644 index 0000000000000..51a40f4ad3517 --- /dev/null +++ b/components/mails_so/mails_so.app.mjs @@ -0,0 +1,41 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "mails_so", + propDefinitions: { + email: { + type: "string", + label: "Email", + description: "Email to validate", + }, + }, + methods: { + _baseUrl() { + return "https://api.mails.so/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "x-mails-api-key": `${this.$auth.api_key}`, + }, + }); + }, + async validateEmail(args = {}) { + return this._makeRequest({ + method: "post", + path: "/validate", + ...args, + }); + }, + }, +}; diff --git a/components/mails_so/package.json b/components/mails_so/package.json new file mode 100644 index 0000000000000..c62ec7130b9df --- /dev/null +++ b/components/mails_so/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/mails_so", + "version": "0.1.0", + "description": "Pipedream mails.so Components", + "main": "mails_so.app.mjs", + "keywords": [ + "pipedream", + "mails_so" + ], + "homepage": "https://pipedream.com/apps/mails_so", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/mailtrap/mailtrap.app.mjs b/components/mailtrap/mailtrap.app.mjs new file mode 100644 index 0000000000000..d61463d23ed7e --- /dev/null +++ b/components/mailtrap/mailtrap.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "mailtrap", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/mailtrap/package.json b/components/mailtrap/package.json new file mode 100644 index 0000000000000..647cc0df091e4 --- /dev/null +++ b/components/mailtrap/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/mailtrap", + "version": "0.0.1", + "description": "Pipedream Mailtrap Components", + "main": "mailtrap.app.mjs", + "keywords": [ + "pipedream", + "mailtrap" + ], + "homepage": "https://pipedream.com/apps/mailtrap", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/mailwizz/README.md b/components/mailwizz/README.md new file mode 100644 index 0000000000000..ece95d089e31f --- /dev/null +++ b/components/mailwizz/README.md @@ -0,0 +1,11 @@ +# Overview + +Mailwizz offers a potent API that enables automation of email marketing tasks, subscriber management, campaign tracking, and more. Using Pipedream, you can harness this API to create custom workflows that trigger on specific events, process data, or synchronize with other apps. Pipedream's serverless architecture allows you to handle workflows at scale without managing infrastructure. + +# Example Use Cases + +- **Automate Subscriber Syncing**: Sync subscribers from Mailwizz to Google Sheets. When a new subscriber is added in Mailwizz, Pipedream can catch the event, process the subscriber's information, and automatically add their details to a Google Sheet, keeping your records up to date without manual entry. + +- **Dynamic Campaign Triggering**: Trigger email campaigns based on customer activity. For instance, if a customer makes a purchase on your Shopify store, Pipedream can initiate a Mailwizz workflow that sends a personalized thank-you email or a follow-up email sequence to encourage repeat business. + +- **Monitor Campaign Performance**: Keep tabs on your email campaigns by setting up a workflow where Pipedream listens for campaign event webhooks from Mailwizz, such as opens or clicks. The data can then be logged into a database like MySQL or sent to a dashboard application like Grafana for real-time monitoring and analysis. diff --git a/components/maintainx/README.md b/components/maintainx/README.md new file mode 100644 index 0000000000000..cdee86683bc70 --- /dev/null +++ b/components/maintainx/README.md @@ -0,0 +1,12 @@ +# Overview + +The MaintainX API lets you automate workflows related to work orders, assets, and maintenance activities within the MaintainX platform. With this API on Pipedream, you can build serverless workflows to react to events, sync data, and connect MaintainX to other apps and services, creating a seamless integration hub for facility and equipment management tasks. + +# Example Use Cases + +- **Automated Work Order Creation**: Trigger a workflow in Pipedream to create a new work order in MaintainX whenever a specific event is logged in your IoT system or when a form is submitted through a platform like Typeform. + +- **Maintenance Alert Notifications**: Configure a Pipedream workflow to listen for status updates on work orders or maintenance requests in MaintainX, and automatically send alert notifications via Slack, SMS (Twilio), or email (SendGrid) to the responsible parties. + +- **Asset Management Sync**: Use Pipedream to create a workflow that syncs asset updates from MaintainX with an external inventory management system like Airtable or Google Sheets, keeping all records up-to-date across different platforms. +. diff --git a/components/mainwp/README.md b/components/mainwp/README.md new file mode 100644 index 0000000000000..be314aec8afa3 --- /dev/null +++ b/components/mainwp/README.md @@ -0,0 +1,11 @@ +# Overview + +MainWP is a WordPress management suite that allows you to control multiple WordPress sites from a single dashboard. With the MainWP API, you can manage updates, backups, security, and more for your child sites programmatically. When integrated with Pipedream, this API enables you to automate various WordPress management tasks, set up custom alerts, or tie in other services to enhance your site management capabilities. + +# Example Use Cases + +- **Automate WordPress Updates Across Multiple Sites**: Use Pipedream to regularly check for WordPress core, plugin, and theme updates across all your MainWP managed sites. Automate the update process, and receive notifications via Slack or email if manual intervention is needed. + +- **Backup Sites Before Making Changes**: Configure a Pipedream workflow that triggers a backup on your child sites with MainWP before applying critical updates or site changes. You could then upload these backups to a cloud storage service like Google Drive or Dropbox for safekeeping. + +- **Monitor and Report Uptime Status**: Set up Pipedream to periodically check the uptime status of your MainWP managed sites and report any downtime immediately through services like PagerDuty or to create a log entry in a Google Sheets spreadsheet. diff --git a/components/mamo_business/README.md b/components/mamo_business/README.md new file mode 100644 index 0000000000000..541b87ee26398 --- /dev/null +++ b/components/mamo_business/README.md @@ -0,0 +1,11 @@ +# Overview + +The Mamo Business API offers capabilities for organizations to manage payments and financial tasks. With the API, you can create and oversee invoices, process payments, and handle transfers among other operations. Integrating Mamo Business with Pipedream allows you to automate workflows involving payment processing, financial monitoring, and syncing transaction data with other business systems. Use it to streamline how money moves into and out of your organization, ensuring accurate financial records while saving time on manual tasks. + +# Example Use Cases + +- **Automated Invoice Processing**: Build a workflow where you automatically create invoices in Mamo Business whenever a new order is placed in an e-commerce platform like Shopify. Once the invoice is paid, trigger an update in your inventory management system. + +- **Payment Notifications**: Set up a Pipedream workflow to listen for new payments on Mamo Business. When a payment is received, you can automate the sending of a custom thank-you email to the customer via SendGrid and log the payment details in a Google Sheet for accounting purposes. + +- **Financial Monitoring Dashboard**: Create a dashboard on Pipedream that pulls in transaction data from Mamo Business at regular intervals. Combine this data with other financial metrics from apps like QuickBooks or Xero for a comprehensive financial overview, which could be used to trigger alerts if certain thresholds or anomalies are detected. diff --git a/components/mandrill/README.md b/components/mandrill/README.md index 91aec42f31c0e..fb8c01e972c38 100644 --- a/components/mandrill/README.md +++ b/components/mandrill/README.md @@ -1,11 +1,11 @@ # Overview -With the Mandrill API, you can integrate Mandrill into your own applications to -send emails, track results, manage subscribers, and more. +Mandrill, a transactional email API for Mailchimp, empowers developers to easily send personalized, one-to-one e-commerce emails, or automated transactional emails. With Pipedream, you can harness Mandrill's capabilities to create powerful automations that trigger on various events, like updating databases on email delivery, syncing with CRM systems when users interact with your emails, or even conducting sentiment analysis on email content for customer support insights. -Here are some examples of what you can build with the Mandrill API: +# Example Use Cases -- An email marketing campaign tool that integrates with Mandrill -- A tool to track email deliverability -- A subscription management tool -- An email template builder +- **User Onboarding Sequence**: Trigger an onboarding email sequence in Mandrill whenever a new user signs up through your app. Use Pipedream to listen for new sign-ups, then employ Mandrill to dispatch a series of welcome emails, tips, and resources over their first few weeks. + +- **Support Ticket Update Notifications**: Integrate Mandrill with a support ticketing system like Zendesk. Whenever a support ticket is updated or closed, use Pipedream to send a summary email through Mandrill, keeping the user informed of their support ticket status in real-time. + +- **E-commerce Order Confirmations**: Combine Mandrill with Shopify to automate order confirmation emails. When a customer completes a purchase, Pipedream can detect the new order event from Shopify and prompt Mandrill to send a tailored confirmation email with order details, shipment tracking, and more. diff --git a/components/mandrill/actions/send-email/send-email.mjs b/components/mandrill/actions/send-email/send-email.mjs index ad498872b734d..48a2e8a66c727 100644 --- a/components/mandrill/actions/send-email/send-email.mjs +++ b/components/mandrill/actions/send-email/send-email.mjs @@ -1,54 +1,58 @@ -// legacy_hash_id: a_njiaQg -import { axios } from "@pipedream/platform"; +import mandrill from "../../mandrill.app.mjs"; export default { key: "mandrill-send-email", name: "Send an Email", - description: "Send an email using Mandrill. See API docs here: https://mandrillapp.com/api/docs/messages.curl.html#method=send", - version: "0.1.1", + description: "Send an email using Mandrill. [See the documentation](https://mandrillapp.com/api/docs/messages.curl.html#method=send)", + version: "0.1.2", type: "action", props: { - mandrill: { - type: "app", - app: "mandrill", - }, + mandrill, html: { type: "string", + label: "HTML", description: "The full HTML content to be sent", optional: true, }, text: { type: "string", + label: "Text", description: "Optional full text content to be sent", optional: true, }, subject: { type: "string", + label: "Subject", description: "The message subject", optional: true, }, from_email: { type: "string", + label: "From Email", description: "The sender email address", optional: true, }, from_name: { type: "string", + label: "From Name", description: "Optional from name to be used", optional: true, }, email: { type: "string", + label: "Recipient Email", description: "The email address of the recipient", }, name: { type: "string", + label: "Recipient Name", description: "Optional display name to use for the recipient", optional: true, }, type: { type: "string", - description: "the header type to use for the recipient, defaults to \"to\" if not provided", + label: "Type", + description: "The header type to use for the recipient, defaults to \"to\" if not provided", optional: true, options: [ "to", @@ -58,25 +62,28 @@ export default { }, }, async run({ $ }) { - return await axios($, { - url: "https://mandrillapp.com/api/1.0/messages/send.json", - data: { - key: this.mandrill.$auth.api_key, - message: { - html: this.html, - text: this.text, - subject: this.subject, - from_email: this.from_email, - from_name: this.from_name, - to: [ - { - email: this.email, - name: this.name, - type: this.type || "to", - }, - ], - }, + const data = { + key: this.mandrill.$auth.api_key, + message: { + html: this.html, + text: this.text, + subject: this.subject, + from_email: this.from_email, + from_name: this.from_name, + to: [ + { + email: this.email, + name: this.name, + type: this.type || "to", + }, + ], }, + }; + const response = await this.mandrill.sendMessage({ + $, + data, }); + $.export("$summary", `Successfully sent email to ${this.email}`); + return response; }, }; diff --git a/components/mandrill/mandrill.app.mjs b/components/mandrill/mandrill.app.mjs index 418e38d4efb6a..3a46379c97e51 100644 --- a/components/mandrill/mandrill.app.mjs +++ b/components/mandrill/mandrill.app.mjs @@ -1,11 +1,34 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "mandrill", propDefinitions: {}, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://mandrillapp.com/api/1.0"; + }, + _makeRequest({ + $ = this, + path, + data, + ...args + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + data: { + ...data, + key: this.$auth.api_key, + }, + ...args, + }); + }, + sendMessage(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/messages/send.json", + ...args, + }); }, }, }; diff --git a/components/mandrill/package.json b/components/mandrill/package.json new file mode 100644 index 0000000000000..6f725bc1a273d --- /dev/null +++ b/components/mandrill/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/mandrill", + "version": "0.0.1", + "description": "Pipedream Mandrill Components", + "main": "mandrill.app.mjs", + "keywords": [ + "pipedream", + "mandrill" + ], + "homepage": "https://pipedream.com/apps/mandrill", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/manifestly_checklists/README.md b/components/manifestly_checklists/README.md index 83f739a8b645f..d55732cd9ab01 100644 --- a/components/manifestly_checklists/README.md +++ b/components/manifestly_checklists/README.md @@ -1,18 +1,11 @@ # Overview -With Manifestly, you can easily create, edit, and track checklist items for any -process or activity. Manifestly provides a simple, yet powerful, interface for -managing checklist items that can be used for any type of activity. +Manifestly Checklists API allows for the creation, management, and tracking of detailed checklists within your workflows. Leveraging this API in Pipedream, you can automate checklist operations, which is indispensable for ensuring consistency in processes like onboarding, maintenance schedules, or content publication. By integrating Manifestly with other apps on Pipedream, you can build seamless automation that triggers actions based on checklist status, responses, or completion. -Here are a few examples of what you can build using the Manifestly Checklists -API: +# Example Use Cases -- A simple to-do list -- A shopping list -- A packing list for a trip -- A task list for a project -- A list of things to do before a big event -- A list of tasks for a team -- A daily checklist for a work process -- A safety checklist for a manufacturing process -- An inspection checklist for a construction site +- **Employee Onboarding Automation**: Automatically create and assign onboarding checklists to new hires when they are added to your HR management platform, such as BambooHR. Once the checklist is completed, trigger a workflow that updates their status in the HR system and sends a welcome email. + +- **Content Publication Workflow**: Connect Manifestly to your CMS, like WordPress. Whenever a new blog post is ready for review, automatically generate a publishing checklist. Once the checklist is completed, the workflow can publish the post on your website and share it on social media platforms. + +- **Maintenance Schedule Coordinator**: Integrate Manifestly with IoT monitoring tools like Ubidots. When a piece of equipment reports a fault or scheduled maintenance is due, automatically create a maintenance checklist and notify the technical team. On checklist completion, log the event in a maintenance tracking system and inform stakeholders. diff --git a/components/manychat/README.md b/components/manychat/README.md new file mode 100644 index 0000000000000..48f15e4332a5f --- /dev/null +++ b/components/manychat/README.md @@ -0,0 +1,11 @@ +# Overview + +The ManyChat API lets you manage and automate conversations in your chatbot, giving you control over user data, tags, and messaging. Integrating ManyChat with Pipedream can supercharge your chatbot workflows, enabling you to connect your ManyChat bot to various apps and services, automate interactions based on triggers, and analyze chatbot data to improve user engagement. + +# Example Use Cases + +- **Sync ManyChat Contacts to CRM**: Automatically add new ManyChat subscribers as contacts in your CRM, like Salesforce or HubSpot. This keeps your sales team in the loop with the latest leads generated from your chatbot conversations. + +- **Broadcast Messages Based on User Actions**: Deploy a workflow that sends a broadcast message to a segment of ManyChat users when they perform a specific action, such as visiting a particular page on your website. This can be done by integrating ManyChat with an analytics tool like Google Analytics to track page visits, triggering a ManyChat broadcast via Pipedream. + +- **Customer Support Ticket Creation**: Create a support ticket in tools like Zendesk or Help Scout whenever a user reports an issue through ManyChat. This workflow can be enhanced with sentiment analysis using a service like MonkeyLearn to prioritize urgent tickets. diff --git a/components/mapbox/.gitignore b/components/mapbox/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/mapbox/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/mapbox/README.md b/components/mapbox/README.md index 69d94b7b9cba4..69f6734818af3 100644 --- a/components/mapbox/README.md +++ b/components/mapbox/README.md @@ -1,12 +1,11 @@ # Overview -Mapbox is a powerful mapping platform that enables developers to create -interactive maps for a variety of purposes. Here are some examples of what you -can build with the Mapbox API: - -- A web map with zoom and pan controls -- A map with markers and popups -- A map with geolocation -- A heatmap -- A choropleth map -- A map with custom tiles +The Mapbox API offers a suite of tools for developers to integrate location-based services into their applications. With Pipedream, you can leverage Mapbox's capabilities to create complex automations and workflows, like real-time mapping, geocoding, and route optimization. Whether you're managing delivery routes, analyzing spatial data, or personalizing user experiences based on location, Mapbox provides the geographical intelligence that, when combined with Pipedream's serverless platform, can turn these ideas into efficient, automated processes. + +# Example Use Cases + +- **Customer Order Tracking**: When a customer places an order, automate the process of sending them real-time updates on the delivery driver's location. Use Mapbox to track the driver and Pipedream's workflow to trigger emails or SMS notifications via Twilio to the customer at key points along the route. + +- **Localized Content Delivery**: Tailor the user experience by serving location-specific content. When a user visits your site, use their IP address to determine their location with Mapbox and serve up local news, weather, or personalized recommendations. This workflow could include a connection to a CMS like WordPress to pull the relevant content. + +- **Efficient Dispatch System**: Optimize dispatching for services like taxi or delivery by using Mapbox's routing capabilities. When a service request comes in, Pipedream can use Mapbox to calculate the nearest available driver and the quickest route to the customer, then communicate the dispatch details through Slack or another messaging service to coordinate the pickup. diff --git a/components/mapbox/actions/create-tileset/create-tileset.mjs b/components/mapbox/actions/create-tileset/create-tileset.mjs new file mode 100644 index 0000000000000..2cc4423f4172f --- /dev/null +++ b/components/mapbox/actions/create-tileset/create-tileset.mjs @@ -0,0 +1,99 @@ +import mapbox from "../../mapbox.app.mjs"; +import fs from "fs"; +import FormData from "form-data"; + +export default { + key: "mapbox-create-tileset", + name: "Create Tileset", + description: "Uploads and creates a new tileset from a data source. [See the documentation](https://docs.mapbox.com/api/maps/mapbox-tiling-service/)", + version: "0.0.1", + type: "action", + props: { + mapbox, + username: { + type: "string", + label: "Username", + description: "The Mapbox username of the account for which to create a tileset source", + }, + tilesetName: { + type: "string", + label: "Tileset Name", + description: "A unique name for the tileset source to be created. Limited to 32 characters. The only allowed special characters are `-` (hyphen) and `_` (underscore)", + }, + filePath: { + type: "string", + label: "File Path", + description: "The path to a tileset source file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + }, + recipe: { + type: "object", + label: "Recipe", + description: "A recipe that describes how the GeoJSON data you uploaded should be transformed into tiles. A tileset source is raw geographic data formatted as [line-delimited GeoJSON](https://en.wikipedia.org/wiki/JSON_streaming#Line-delimited_JSON), or a supported [raster file format](https://docs.mapbox.com/mapbox-tiling-service/raster/supported-file-formats/). For more information on how to create and format recipes, see the [Recipe reference](https://docs.mapbox.com/mapbox-tiling-service/reference/) and [Recipe examples](https://docs.mapbox.com/mapbox-tiling-service/examples/).", + }, + description: { + type: "string", + label: "Description", + description: "A description of the tileset", + optional: true, + }, + private: { + type: "boolean", + label: "Private", + description: "Describes whether the tileset must be used with an access token from your Mapbox account. Default is `true`.", + optional: true, + }, + }, + async run({ $ }) { + const filePath = this.filePath.includes("tmp/") + ? this.filePath + : `/tmp/${this.filePath}`; + + // Create Tileset Source + try { + const fileData = new FormData(); + const content = fs.createReadStream(filePath); + fileData.append("file", content); + + await this.mapbox.createTilesetSource({ + $, + username: this.username, + id: this.tilesetName, + data: fileData, + headers: fileData.getHeaders(), + }); + } catch (e) { + throw new Error(`Error uploading file: \`${filePath}\`. Error: ${e}`); + } + + const recipe = typeof this.recipe === "string" + ? JSON.parse(this.recipe) + : this.recipe; + + // Validate Recipe + const { + valid, errors, + } = await this.mapbox.validateRecipe({ + $, + data: recipe, + }); + + if (!valid) { + throw new Error(`Error validating recipe: ${errors}`); + } + + // Create Tileset + const response = await this.mapbox.createTileset({ + $, + tilesetId: `${this.username}.${this.tilesetName}`, + data: { + recipe, + name: this.tilesetName, + description: this.description, + private: this.private, + }, + }); + + $.export("$summary", `Created tileset ${this.tilesetName}`); + return response; + }, +}; diff --git a/components/mapbox/actions/generate-directions/generate-directions.mjs b/components/mapbox/actions/generate-directions/generate-directions.mjs new file mode 100644 index 0000000000000..c332eed59a202 --- /dev/null +++ b/components/mapbox/actions/generate-directions/generate-directions.mjs @@ -0,0 +1,60 @@ +import mapbox from "../../mapbox.app.mjs"; + +export default { + key: "mapbox-generate-directions", + name: "Generate Directions", + description: "Generates directions between two or more locations using Mapbox API. [See the documentation](https://docs.mapbox.com/api/navigation/directions/).", + version: "0.0.1", + type: "action", + props: { + mapbox, + startCoordinate: { + type: "string", + label: "Start Coordinate", + description: "The starting point in the format `longitude,latitude`, E.g. `-85.244869,37.835819`", + }, + endCoordinate: { + type: "string", + label: "End Coordinate", + description: "The ending point in the format `longitude,latitude`, E.g. `-85.244869,37.835819`", + }, + transportationMode: { + propDefinition: [ + mapbox, + "transportationMode", + ], + }, + steps: { + type: "boolean", + label: "Steps", + description: "Whether to return steps and turn-by-turn instructions (`true`) or not (`false`, default)", + optional: true, + }, + alternatives: { + type: "boolean", + label: "Alternatives", + description: "Whether to try to return alternative routes (`true`) or not (`false`, default). An alternative route is a route that is significantly different from the fastest route, but still close in time.", + optional: true, + }, + exclude: { + propDefinition: [ + mapbox, + "exclude", + ], + }, + }, + async run({ $ }) { + const directions = await this.mapbox.getDirections({ + $, + transportationMode: this.transportationMode, + coordinates: `${this.startCoordinate};${this.endCoordinate}`, + params: { + steps: this.steps, + alternatives: this.alternatives, + exclude: this.exclude && this.exclude.join(","), + }, + }); + $.export("$summary", "Generated directions successfully."); + return directions; + }, +}; diff --git a/components/mapbox/actions/geocode-address/geocode-address.mjs b/components/mapbox/actions/geocode-address/geocode-address.mjs new file mode 100644 index 0000000000000..8fb8d9c07765e --- /dev/null +++ b/components/mapbox/actions/geocode-address/geocode-address.mjs @@ -0,0 +1,41 @@ +import mapbox from "../../mapbox.app.mjs"; + +export default { + key: "mapbox-geocode-address", + name: "Geocode Address", + description: "Retrieves the geocoded location for a given address. [See the documentation](https://docs.mapbox.com/api/search/geocoding/)", + version: "0.0.1", + type: "action", + props: { + mapbox, + address: { + type: "string", + label: "Address", + description: "The address (or partial address) to geocode. This could be an address, a city name, etc. Must consist of at most 20 words and numbers separated by spacing and punctuation, and at most 256 characters.", + }, + boundingBox: { + type: "string", + label: "Bounding Box", + description: "Limit results to only those contained within the supplied bounding box. Bounding boxes should be supplied as four numbers separated by commas, in `minLon,minLat,maxLon,maxLat` order. The bounding box cannot cross the 180th meridian. You can use the [Location Helper](https://labs.mapbox.com/location-helper) to find a bounding box for use with this API.", + optional: true, + }, + proximity: { + type: "string", + label: "Proximity", + description: "Bias the response to favor results that are closer to this location. Provided as two comma-separated coordinates in `longitude,latitude` order.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.mapbox.geocode({ + $, + params: { + q: this.address, + bBox: this.boundingBox, + proximity: this.proximity, + }, + }); + $.export("$summary", `Geocoded location for "${this.address}" retrieved successfully`); + return response; + }, +}; diff --git a/components/mapbox/app/mapbox.app.ts b/components/mapbox/app/mapbox.app.ts deleted file mode 100644 index a778df2269db4..0000000000000 --- a/components/mapbox/app/mapbox.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "mapbox", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); diff --git a/components/mapbox/common/constants.mjs b/components/mapbox/common/constants.mjs new file mode 100644 index 0000000000000..a1101632447cd --- /dev/null +++ b/components/mapbox/common/constants.mjs @@ -0,0 +1,21 @@ +const TRANSPORTATION_MODES = [ + "driving", + "walking", + "cycling", + "driving-traffic", +]; + +const EXCLUDE_OPTIONS = [ + "motorway", + "toll", + "ferry", + "unpaved", + "cash_only_tolls", + "country_border", + "state_border", +]; + +export default { + TRANSPORTATION_MODES, + EXCLUDE_OPTIONS, +}; diff --git a/components/mapbox/mapbox.app.mjs b/components/mapbox/mapbox.app.mjs new file mode 100644 index 0000000000000..e11265eed2e3f --- /dev/null +++ b/components/mapbox/mapbox.app.mjs @@ -0,0 +1,81 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "mapbox", + propDefinitions: { + transportationMode: { + type: "string", + label: "Transportation Mode", + description: "The mode of transportation", + options: constants.TRANSPORTATION_MODES, + }, + exclude: { + type: "string[]", + label: "Exclude", + description: "Exclude certain road types and custom locations from routing", + options: constants.EXCLUDE_OPTIONS, + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.mapbox.com"; + }, + _makeRequest({ + $ = this, + path, + params, + ...otherOpts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + params: { + access_token: this.$auth.access_token, + ...params, + }, + ...otherOpts, + }); + }, + geocode(opts = {}) { + return this._makeRequest({ + path: "/search/geocode/v6/forward", + ...opts, + }); + }, + getDirections({ + transportationMode, coordinates, ...opts + }) { + return this._makeRequest({ + path: `/directions/v5/mapbox/${transportationMode}/${coordinates}`, + ...opts, + }); + }, + createTilesetSource({ + username, id, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/tilesets/v1/sources/${username}/${id}`, + ...opts, + }); + }, + validateRecipe(opts = {}) { + return this._makeRequest({ + method: "PUT", + path: "/tilesets/v1/validateRecipe", + ...opts, + }); + }, + createTileset({ + tilesetId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/tilesets/v1/${tilesetId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/mapbox/package.json b/components/mapbox/package.json index 6adaa9ef9e5f5..680b99cc3f4cb 100644 --- a/components/mapbox/package.json +++ b/components/mapbox/package.json @@ -1,18 +1,19 @@ { "name": "@pipedream/mapbox", - "version": "0.0.3", + "version": "0.1.0", "description": "Pipedream Mapbox Components", - "main": "dist/app/mapbox.app.mjs", + "main": "mapbox.app.mjs", "keywords": [ "pipedream", "mapbox" ], - "files": [ - "dist" - ], "homepage": "https://pipedream.com/apps/mapbox", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "form-data": "^4.0.1" } } diff --git a/components/mapulus/README.md b/components/mapulus/README.md new file mode 100644 index 0000000000000..3d0e9b9979d74 --- /dev/null +++ b/components/mapulus/README.md @@ -0,0 +1,11 @@ +# Overview + +Mapulus API enables you to integrate advanced mapping, routing, and geospatial capabilities into your applications. With Pipedream, you can create powerful workflows that leverage the Mapulus API to automate tasks such as generating optimized routes, geocoding addresses, or analyzing geographic data. By connecting Mapulus to other apps available on Pipedream, you can enrich your business processes, enhance data analytics, or create dynamic location-based services without writing code. + +# Example Use Cases + +- **Dynamic Route Optimization for Deliveries**: Automate the process of optimizing delivery routes by connecting Mapulus with a scheduling app like Google Calendar. Whenever a new delivery is scheduled, a workflow triggers on Pipedream to send the address to Mapulus to calculate the most efficient route and update the Calendar event with the estimated time of arrival. + +- **Geofencing Alerts for Asset Tracking**: Combine Mapulus with a real-time messaging app like Slack to create geofencing alerts. Set up a workflow on Pipedream where Mapulus monitors the location of assets, and when they enter or exit a predefined geofenced area, an alert is automatically sent to a dedicated Slack channel to notify the team. + +- **Customer Location Data Enrichment**: Integrate Mapulus with a CRM platform like Salesforce to enrich customer profiles. Create a Pipedream workflow that triggers when new customer data is added to Salesforce. The workflow sends the customer address to Mapulus for geocoding and then updates the customer record in Salesforce with precise location data, helping with market segmentation and targeted marketing efforts. diff --git a/components/marketing_master_io/README.md b/components/marketing_master_io/README.md index 53be749f8cd46..2fb3acb11f462 100644 --- a/components/marketing_master_io/README.md +++ b/components/marketing_master_io/README.md @@ -1,10 +1,11 @@ # Overview -Marketing Master IO API lets you build amazing marketing tools to help you with -your marketing campaigns. Here are some examples of what you can build with -Marketing Master IO API: - -- A tool to help you create and manage your marketing campaigns -- A tool to help you track your marketing campaigns' performance -- A tool to help you analyze your marketing campaigns' results -- A tool to help you optimize your marketing campaigns +The Marketing Master IO API enables the automation of digital marketing tasks, including email campaign management, audience segmentation, and performance analytics. With its integration on Pipedream, you can craft workflows that streamline your marketing efforts, trigger actions based on customer data, and analyze campaign results in real-time. The API's capabilities, when coupled with Pipedream's serverless platform, allow for the creation of dynamic, responsive marketing strategies without extensive manual oversight. + +# Example Use Cases + +- **Email Campaign Automation**: Trigger a workflow on Pipedream when a new subscriber is added to your Marketing Master IO list. Automatically send a personalized welcome email, then schedule a series of follow-up emails tailored to the subscriber's interests. + +- **Audience Segmentation and Targeting**: Use Marketing Master IO to segment your audience based on their interactions with your emails or website. In Pipedream, create a workflow that syncs these segments with your CRM, such as Salesforce, to enable targeted outreach and personalize communication with your leads and customers. + +- **Marketing Analytics Reporting**: Gather data from Marketing Master IO on the performance of your email campaigns. Set up a Pipedream workflow that aggregates this data, sends it to a Google Sheets spreadsheet, and then generates a weekly performance report email to your marketing team. diff --git a/components/marketo/marketo.app.mjs b/components/marketo/marketo.app.mjs new file mode 100644 index 0000000000000..10cec5cb7edd5 --- /dev/null +++ b/components/marketo/marketo.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "marketo", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/marketo/package.json b/components/marketo/package.json new file mode 100644 index 0000000000000..e8e110af326a7 --- /dev/null +++ b/components/marketo/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/marketo", + "version": "0.0.1", + "description": "Pipedream Marketo Components", + "main": "marketo.app.mjs", + "keywords": [ + "pipedream", + "marketo" + ], + "homepage": "https://pipedream.com/apps/marketo", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/marketplacer/README.md b/components/marketplacer/README.md index 542d1d625e553..7aef62306d971 100644 --- a/components/marketplacer/README.md +++ b/components/marketplacer/README.md @@ -1,10 +1,11 @@ # Overview -Marketplacer API can be used to build various types of applications. Here are -some examples: - -- E-commerce applications -- Marketplace applications -- Classifieds applications -- Auctions applications -- And more! +The Marketplacer API offers a suite of functionalities to automate and streamline operations of marketplace businesses, enabling users to manage products, orders, and vendors programmatically. With the ability to integrate into Pipedream, you can harness this API to create custom workflows that react to various triggers, sync data across different platforms, and automate repetitive tasks. Whether you're looking to update inventory, synchronize customer data, or automate communication with vendors, the Marketplacer API coupled with Pipedream's serverless execution model empowers you to build efficient and scalable e-commerce ecosystems. + +# Example Use Cases + +- **Automated Order Processing**: Upon receiving a new order in Marketplacer, trigger a Pipedream workflow to validate the order details, then automatically generate and send an invoice using a service like QuickBooks or Stripe. Subsequently, notify the relevant vendor and update the order status in Marketplacer, all within seconds. + +- **Real-time Inventory Sync**: Set up a workflow on Pipedream to monitor inventory levels. When stock for a product falls below a specified threshold, automatically reorder from the supplier or publish an alert to Slack or email to prompt manual intervention. This ensures your marketplace always has adequate stock levels to meet demand. + +- **Customer Relationship Management (CRM) Integration**: When a new vendor signs up on your Marketplacer store, trigger a workflow to add their details to a CRM like Salesforce or HubSpot. Likewise, when a customer places an order, capture their information and feed it into your CRM to track customer interactions, purchase history, and drive targeted marketing campaigns. diff --git a/components/marketplacer/package.json b/components/marketplacer/package.json new file mode 100644 index 0000000000000..48b551a184aba --- /dev/null +++ b/components/marketplacer/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/marketplacer", + "version": "0.6.0", + "description": "Pipedream marketplacer Components", + "main": "marketplacer.app.mjs", + "keywords": [ + "pipedream", + "marketplacer" + ], + "homepage": "https://pipedream.com/apps/marketplacer", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/marketstack/README.md b/components/marketstack/README.md new file mode 100644 index 0000000000000..3183085783e7d --- /dev/null +++ b/components/marketstack/README.md @@ -0,0 +1,11 @@ +# Overview + +The Marketstack API provides access to real-time, intraday, and historical stock prices from various global stock exchanges. In Pipedream, you can harness this data to create powerful workflows: trigger actions based on stock price movements, log data for analysis, or integrate with other financial services. It's a tool for financial analysts, investors, and anyone needing stock market data for their applications. + +# Example Use Cases + +- **Automated Stock Alerting System**: Create a workflow on Pipedream that monitors specific stocks using the Marketstack API. When a stock hits a certain threshold, trigger an alert via email, SMS, or push notification using integrated services like SendGrid, Twilio, or Pushover. + +- **Daily Portfolio Summary**: Set up a Pipedream daily cron job that fetches the closing prices of your portfolio stocks from Marketstack. Then, compile a report and send it to your preferred communication app, whether that's Slack, Discord, or Email, to keep track of your investments' performance. + +- **Market Movement Dashboard Sync**: Use Marketstack to fuel a live dashboard that tracks the markets. Connect to Google Sheets or a database service like PostgreSQL on Pipedream to record data points throughout the day. Use this data to visualize trends and inform your trading decisions. diff --git a/components/markettime/README.md b/components/markettime/README.md index 4d57d8466bfd3..1fd3d4443359e 100644 --- a/components/markettime/README.md +++ b/components/markettime/README.md @@ -1,15 +1,11 @@ # Overview -With MarketTime, you can build custom clock and timer applications for your -website or blog. For example, you can display a clock that shows the current -time in your local time zone, or a timer that counts down to a specific event. - -Here are some examples of what you can build with MarketTime: - -- A clock that shows the current time in your local time zone -- A timer that counts down to a specific event -- A clock that shows the current time in multiple time zones -- A clock that displays the current time in your local time zone and in - Coordinated Universal Time (UTC) -- A clock that shows the current time in multiple time zones and in Coordinated - Universal Time (UTC) +The MarketTime API provides a suite of endpoints to access and manage retail operations, such as product listings, ordering, and inventory management. By leveraging this API within Pipedream, you can automate routine tasks, sync data across different platforms, and create custom workflows to enhance retail processes. Pipedream's serverless platform facilitates the integration of MarketTime's capabilities with an array of other apps and services, enabling users to build powerful, automated solutions that can save time and reduce the potential for human error. + +# Example Use Cases + +- **Automated Order Processing**: Trigger a workflow whenever a new order is placed through MarketTime. Automatically send order details to a fulfillment service like ShipStation, update inventory levels in a database, and send a notification to a Slack channel to keep your team informed. + +- **Real-Time Inventory Sync**: Use MarketTime's inventory endpoint to monitor stock levels. Create a Pipedream workflow that triggers on a schedule, checks inventory quantities, and syncs this information with an e-commerce platform like Shopify, ensuring that product availability is always up-to-date across all sales channels. + +- **Product Catalog Management**: Whenever a new product is added to the MarketTime catalog, trigger a Pipedream workflow that adds the product to other platforms such as WooCommerce or Etsy. Include steps to format product details to fit the target platform's requirements and post updates to a marketing team's Trello board for campaign tracking. diff --git a/components/markettime/package.json b/components/markettime/package.json new file mode 100644 index 0000000000000..24e7bf8aefde2 --- /dev/null +++ b/components/markettime/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/markettime", + "version": "0.6.0", + "description": "Pipedream markettime Components", + "main": "markettime.app.mjs", + "keywords": [ + "pipedream", + "markettime" + ], + "homepage": "https://pipedream.com/apps/markettime", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/mastodon/README.md b/components/mastodon/README.md new file mode 100644 index 0000000000000..c9dd077180458 --- /dev/null +++ b/components/mastodon/README.md @@ -0,0 +1,11 @@ +# Overview + +The Mastodon API enables you to interact with the Mastodon social network programmatically. Through Pipedream's integration, you can automate posting, monitor timelines, follow accounts, and more. Powerful workflows can be built around these capabilities, facilitating content distribution, social listening, and audience engagement without manual intervention. + +# Example Use Cases + +- **Automated Content Sharing**: Automatically share new blog posts or news articles on Mastodon whenever a new item is published to your RSS feed. This keeps your Mastodon followers updated with fresh content without manual posting. + +- **Social Listening & Sentiment Analysis**: Monitor mentions of a specific hashtag or keyword on Mastodon and analyze sentiment using a natural language processing service. Gain insights into public perception and react promptly to trends or feedback. + +- **New Follower Outreach**: When you get a new follower on Mastodon, trigger a workflow that sends a personalized welcome message or follows back. This can help to foster community and encourage engagement. diff --git a/components/mattermost/README.md b/components/mattermost/README.md index 59792f2d96cce..aade64076de7b 100644 --- a/components/mattermost/README.md +++ b/components/mattermost/README.md @@ -1,9 +1,11 @@ # Overview -Mattermost is an open source, self-hosted Slack-alternative. With the -Mattermost API, you can: +Mattermost is an open-source platform for secure collaboration across the entire software development lifecycle. With the Mattermost API on Pipedream, you can automate common chat operations, integrate with DevOps tools for real-time alerts, orchestrate workflows based on chat events, and connect with other services to enhance team productivity. For instance, you could trigger actions on issue tracking platforms when discussing bugs or link project management tools to update task statuses directly from your Mattermost channels. -- Create channels -- Invite users -- Add posts -- And more! +# Example Use Cases + +- **Incident Alerting and Management**: Automatically post messages to a designated Mattermost channel when an incident is created or updated in monitoring tools like PagerDuty. This keeps your DevOps teams in the loop and allows for quick collaboration and response to system alerts or outages. + +- **Customer Support Ticket Triage**: Connect Mattermost to a customer support platform like Zendesk. When a new support ticket is created, post a message with ticket details to a support channel, and allow the team to claim tickets by reacting to the message. This streamlines the triage process and improves response times. + +- **Continuous Integration/Deployment (CI/CD) Notifications**: Integrate with CI/CD platforms such as GitHub Actions or CircleCI. Send updates to Mattermost channels about the status of code commits, pull requests, or deployment processes, enabling developers to stay informed about the progress of builds and deployments without leaving the chat environment. diff --git a/components/mattermost/actions/post-message/post-message.ts b/components/mattermost/actions/post-message/post-message.ts index 643c45202351b..8fa10efd8927f 100644 --- a/components/mattermost/actions/post-message/post-message.ts +++ b/components/mattermost/actions/post-message/post-message.ts @@ -9,7 +9,7 @@ export default defineAction({ description: "Create a new post in a channel [See docs here](https://api.mattermost.com/#tag/posts/operation/CreatePost)", key: "mattermost-post-message", - version: "0.0.1", + version: "0.0.3", type: "action", props: { mattermost, diff --git a/components/mattermost/app/mattermost.app.ts b/components/mattermost/app/mattermost.app.ts index e289c56b59a2f..33ea2e9baafca 100644 --- a/components/mattermost/app/mattermost.app.ts +++ b/components/mattermost/app/mattermost.app.ts @@ -2,7 +2,7 @@ import { defineApp } from "@pipedream/types"; import { axios } from "@pipedream/platform"; import { HttpRequestParams, - PostMessageParams, PostMessageResponse, + PostMessageParams, PostMessageResponse, Team, } from "../common/types"; import { Channel } from "../common/types"; @@ -31,6 +31,48 @@ export default defineApp({ }); }, }, + publicChannelId: { + label: "Channel", + description: "A public channel on this team to emit events for.", + type: "string", + async options({ teamId }) { + const channels: Channel[] = await this.listTeamChannels(teamId); + + return channels.map(({ + id, name, display_name, + }) => { + const label = name && display_name && (name !== display_name) + ? `${display_name} (${name})` + : (display_name || name); + + return { + label, + value: id, + }; + }); + }, + }, + teamId: { + label: "Team", + description: "The ID of the team that events will be emitted for.", + type: "string", + async options() { + const teams: Team[] = await this.listTeams(); + + return teams.map(({ + id, name, display_name, + }) => { + const label = name && display_name && (name !== display_name) + ? `${display_name} (${name})` + : (display_name || name); + + return { + label, + value: id, + }; + }); + }, + }, }, methods: { _baseUrl(domain: string) { @@ -70,5 +112,28 @@ export default defineApp({ endpoint: "/channels", }); }, + async listTeamChannels(teamId: string): Promise { + return this._httpRequest({ + endpoint: `/teams/${teamId}/channels`, + }); + }, + async listTeams(): Promise { + return this._httpRequest({ + endpoint: "/teams", + }); + }, + async createWebhook(data: object): Promise { + return this._httpRequest({ + endpoint: "/webhooks/outgoing", + method: "POST", + data, + }); + }, + async deleteWebhook(id: string): Promise { + return this._httpRequest({ + endpoint: `/webhooks/outgoing/${id}`, + method: "DELETE", + }); + }, }, }); diff --git a/components/mattermost/common/types.ts b/components/mattermost/common/types.ts index 8dc6f551cca54..c42372dd33f48 100644 --- a/components/mattermost/common/types.ts +++ b/components/mattermost/common/types.ts @@ -30,3 +30,5 @@ export interface Channel { name: string; display_name: string; } + +export type Team = Channel; diff --git a/components/mattermost/package.json b/components/mattermost/package.json index f1175b51f6f9c..7e73638491dde 100644 --- a/components/mattermost/package.json +++ b/components/mattermost/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/mattermost", - "version": "0.0.2", + "version": "0.1.1", "description": "Pipedream Mattermost Components", "main": "dist/app/mattermost.app.mjs", "keywords": [ @@ -16,7 +16,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.1.1", - "@pipedream/types": "^0.1.4" + "@pipedream/platform": "^1.6.0", + "@pipedream/types": "^0.3.0" } } diff --git a/components/mattermost/sources/new-message-sent-in-channel/new-message-sent-in-channel.ts b/components/mattermost/sources/new-message-sent-in-channel/new-message-sent-in-channel.ts new file mode 100644 index 0000000000000..89b081bc9c99d --- /dev/null +++ b/components/mattermost/sources/new-message-sent-in-channel/new-message-sent-in-channel.ts @@ -0,0 +1,105 @@ +import mattermost from "../../app/mattermost.app"; +import { + SourceHttpRunOptions, defineSource, +} from "@pipedream/types"; + +export default defineSource({ + name: "New Message Sent in Channel (Instant)", + description: + "Emit new event when a message matching the requirements is sent in a channel. [See the documentation](https://api.mattermost.com/#tag/webhooks/operation/CreateOutgoingWebhook)", + key: "mattermost-new-message-sent-in-channel", + version: "0.0.2", + type: "source", + dedupe: "unique", + props: { + mattermost, + db: "$.service.db", + http: "$.interface.http", + teamId: { + propDefinition: [ + mattermost, + "teamId", + ], + }, + channelId: { + propDefinition: [ + mattermost, + "publicChannelId", + ({ teamId }) => ({ + teamId, + }), + ], + }, + displayName: { + type: "string", + label: "Display Name", + description: "The display name of the webhook in Mattermost", + optional: true, + default: "Pipedream source", + }, + triggerWords: { + type: "string[]", + label: "Trigger Words", + description: "List of words that will trigger an event", + }, + triggerWhen: { + type: "integer", + label: "Trigger When", + description: "When to trigger an event", + optional: true, + options: [ + { + label: "When a trigger word is present in the message", + value: 0, + }, + { + label: "If the message starts with a trigger word", + value: 1, + }, + ], + }, + }, + methods: { + _getWebhookId(): string { + return this.db.get("webhookId"); + }, + _setWebhookId(value: string) { + this.db.set("webhookId", value); + }, + }, + hooks: { + async activate() { + const data = { + team_id: this.teamId, + channel_id: this.channelId, + display_name: this.displayName, + description: `Pipedream - New Message Sent in Channel ${this.channelId}`, + trigger_words: this.triggerWords, + trigger_when: this.triggerWhen, + callback_urls: [ + this.http.endpoint, + ], + content_type: "application/json", + }; + + const { id } = await this.mattermost.createWebhook(data); + this._setWebhookId(id); + }, + async deactivate() { + const id = this._getWebhookId(); + await this.mattermost.deleteWebhook(id); + }, + }, + async run({ body }: SourceHttpRunOptions) { + if (body) { + const ts = Date.now(); + this.$emit(body, { + id: typeof body.id === "string" + ? body.id + : ts, + summary: "New message", + ts, + }); + } + }, +}); diff --git a/components/matterport/README.md b/components/matterport/README.md new file mode 100644 index 0000000000000..571c7d962fdf1 --- /dev/null +++ b/components/matterport/README.md @@ -0,0 +1,11 @@ +# Overview + +The Matterport API enables developers to harness the power of 3D spatial data, constructing immersive experiences and automating real estate, retail, and hospitality processes. With APIs allowing access to space details, models, and dimensions, users can integrate Matterport's capabilities into various applications, streamlining workflows such as virtual tour creation, space management, and asset documentation on Pipedream's serverless platform. + +# Example Use Cases + +- **Automated Real Estate Listing Updates**: Integrate Matterport with a real estate platform like Zillow using Pipedream. When a new 3D model is uploaded to Matterport, trigger a workflow to automatically update the property listing on Zillow with the latest virtual tour link and images, ensuring potential buyers receive real-time property views. + +- **Enhanced Customer Support with Slack**: Connect Matterport to Slack through Pipedream. Whenever a customer requests a virtual tour within a support ticket, trigger an automated message to the support team's Slack channel with the relevant Matterport space details and direct links, improving response times and customer engagement. + +- **Scheduled Asset Documentation Backup**: Use Pipedream to pair Matterport with cloud storage services like Google Drive. Set up a workflow that periodically retrieves the latest 3D models and measurements from Matterport and stores them in a designated Google Drive folder, providing a secure and automatic backup solution for digital asset management. diff --git a/components/matterport/matterport.app.mjs b/components/matterport/matterport.app.mjs new file mode 100644 index 0000000000000..570728a4a73b6 --- /dev/null +++ b/components/matterport/matterport.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "matterport", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/matterport/package.json b/components/matterport/package.json new file mode 100644 index 0000000000000..cc7cd079e8921 --- /dev/null +++ b/components/matterport/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/matterport", + "version": "0.0.1", + "description": "Pipedream Matterport Components", + "main": "matterport.app.mjs", + "keywords": [ + "pipedream", + "matterport" + ], + "homepage": "https://pipedream.com/apps/matterport", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/mautic/README.md b/components/mautic/README.md index 0658cf9fd5cc2..63a68fd61f38c 100644 --- a/components/mautic/README.md +++ b/components/mautic/README.md @@ -1,13 +1,11 @@ # Overview -Mautic's API enables you to build custom integrations and applications on top -of Mautic. This means you can connect Mautic to other tools in your tech stack, -automate processes, or even build your own Mautic-powered app. +The Mautic API empowers marketers to automate and integrate a wide range of marketing tasks directly into Pipedream workflows. With Mautic's API, you can manage contacts, campaigns, emails, and reports, and use triggers to automate responses based on user behavior. It's a robust tool to align marketing efforts with real-time data and actions, making it possible to personalize communication, streamline follow-ups, and ultimately drive conversions with less manual effort. -Here are a few examples of what you can build using the Mautic API: +# Example Use Cases -- A CRM integration that syncs contact data between Mautic and your CRM system -- An ecommerce integration that tracks purchase data from your store in Mautic -- A marketing automation workflow that sends automated emails based on contact - activity in Mautic -- A custom app that uses Mautic data to power some unique functionality +- **Lead Scoring Automation**: Automatically update lead scores in Mautic when specific events occur in a connected CRM like Salesforce. For instance, if a contact closes a deal, use Pipedream to listen for the event and then increase the contact's lead score in Mautic. + +- **Dynamic Email Campaigns**: Use Pipedream to monitor customer activity from an e-commerce platform like Shopify. When a customer makes a purchase, trigger a Mautic API call to enroll them in a post-purchase email sequence, providing relevant cross-sells, upsells, or loyalty program details. + +- **Real-Time Analytics Sync**: Integrate Mautic with a tool like Google Sheets using Pipedream. Whenever a new campaign report is generated in Mautic, automatically send the data to Google Sheets for real-time analysis, enabling marketers to make data-driven decisions quickly. diff --git a/components/mav/README.md b/components/mav/README.md new file mode 100644 index 0000000000000..7074b20e44507 --- /dev/null +++ b/components/mav/README.md @@ -0,0 +1,11 @@ +# Overview + +The Mav API allows for automated, conversational marketing via SMS, engaging leads and customers through personalized messaging. On Pipedream, you can tap into this capability to create workflows that trigger actions within Mav based on various events or conditions. For example, you can add new leads into Mav's system, update contacts, or send out messages based on triggers from other apps or scheduled times. + +# Example Use Cases + +- **Lead Qualification Automation**: When a lead fills out a form on your website, use Pipedream to send that data to the Mav API, triggering a personalized SMS conversation to qualify the lead. Combine this with Google Sheets on Pipedream to log interactions for later review. + +- **Customer Feedback Collection**: After a customer purchase is recorded in your CRM, such as Salesforce, use Pipedream to initiate a Mav SMS conversation asking for feedback. This can help quickly capture customer sentiment and identify areas for improvement. + +- **Event Reminder Messages**: Schedule Pipedream to trigger Mav SMS reminders to attendees of an upcoming event, using a calendar event from Google Calendar as the input. Personalize the message based on attendee data to increase engagement. diff --git a/components/maxmind_geoip2/README.md b/components/maxmind_geoip2/README.md new file mode 100644 index 0000000000000..d43a94943bbb3 --- /dev/null +++ b/components/maxmind_geoip2/README.md @@ -0,0 +1,11 @@ +# Overview + +The MaxMind GeoIP2 API enables you to identify the geographical location of your users based on their IP addresses. It offers data such as country, city, postal code, latitude and longitude, and more. On Pipedream, you can leverage this API to create powerful workflows that respond to IP-based events with geo-specific outcomes. Whether for security, personalization, or data analytics, integrating GeoIP2 within Pipedream workflows allows you to automate actions based on user locations. + +# Example Use Cases + +- **Content Personalization**: Tailor content delivery based on user geography. When a user visits your site, use their IP to determine their location with MaxMind GeoIP2, and serve localized content or redirect to a region-specific page using Pipedream's HTTP/S webhook triggers and actions. + +- **Security Monitoring**: Enhance security by tracking the source of traffic or login attempts. Set up a workflow that uses the MaxMind GeoIP2 API to map the IP addresses from your authentication logs. With this info, trigger alerts or actions if the service detects access from unexpected or high-risk locations. + +- **Traffic Analysis and Reporting**: Generate real-time insights by correlating IP addresses with geographic data. Use MaxMind GeoIP2 to enrich event logs or analytics data with location info. Then, send the enriched data to other apps like Google Sheets or a database on Pipedream for easy visualization and reporting. diff --git a/components/maxmind_geoip2/maxmind_geoip2.app.mjs b/components/maxmind_geoip2/maxmind_geoip2.app.mjs new file mode 100644 index 0000000000000..b7c17a0f73d5f --- /dev/null +++ b/components/maxmind_geoip2/maxmind_geoip2.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "maxmind_geoip2", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/maxmind_geoip2/package.json b/components/maxmind_geoip2/package.json new file mode 100644 index 0000000000000..2d74bfbc54612 --- /dev/null +++ b/components/maxmind_geoip2/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/maxmind_geoip2", + "version": "0.0.1", + "description": "Pipedream MaxMind GeoIP2 Components", + "main": "maxmind_geoip2.app.mjs", + "keywords": [ + "pipedream", + "maxmind_geoip2" + ], + "homepage": "https://pipedream.com/apps/maxmind_geoip2", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/maxmind_minfraud/README.md b/components/maxmind_minfraud/README.md new file mode 100644 index 0000000000000..c4b12f9d3e4b9 --- /dev/null +++ b/components/maxmind_minfraud/README.md @@ -0,0 +1,11 @@ +# Overview + +The MaxMind minFraud API provides a robust set of services designed to help businesses prevent fraudulent transactions by scoring online activities, providing insights based on IP risk, email checks, and other data points. Integrating this API into Pipedream workflows allows you to automate fraud checks and make informed decisions dynamically, based on the risk scores and data returned by minFraud. + +# Example Use Cases + +- **Automated Order Screening**: Trigger a workflow whenever a new order is placed in your e-commerce platform. Use the minFraud API to assess the risk level of the transaction based on customer details. If the risk score exceeds a certain threshold, automatically flag the order for review or cancel it to prevent fraud. + +- **User Signup Validation**: Each time a user signs up on your website or app, use the minFraud API within a Pipedream workflow to analyze the risk associated with the user's details. Depending on the risk assessment, you could either allow, deny, or require additional verification for the signup process. + +- **Transaction Monitoring with Slack Alerts**: Monitor transactions in real time by setting up a workflow that sends data to the minFraud API. If a transaction is considered high risk, send an alert to a designated Slack channel for immediate attention by your risk management team. This helps in quick decision-making and keeps your team updated on potential fraud. diff --git a/components/maxmind_minfraud/maxmind_minfraud.app.mjs b/components/maxmind_minfraud/maxmind_minfraud.app.mjs new file mode 100644 index 0000000000000..cd5066f8be6dc --- /dev/null +++ b/components/maxmind_minfraud/maxmind_minfraud.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "maxmind_minfraud", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/maxmind_minfraud/package.json b/components/maxmind_minfraud/package.json new file mode 100644 index 0000000000000..4fc1bdf1cbab2 --- /dev/null +++ b/components/maxmind_minfraud/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/maxmind_minfraud", + "version": "0.0.1", + "description": "Pipedream MaxMind minFraud Components", + "main": "maxmind_minfraud.app.mjs", + "keywords": [ + "pipedream", + "maxmind_minfraud" + ], + "homepage": "https://pipedream.com/apps/maxmind_minfraud", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/mboum/README.md b/components/mboum/README.md new file mode 100644 index 0000000000000..61ea3009797ac --- /dev/null +++ b/components/mboum/README.md @@ -0,0 +1,11 @@ +# Overview + +The Mboum API offers rich financial data, including real-time stock quotes, financial statements, and market analysis. On Pipedream, this translates into the ability to craft workflows that can automate tasks for investors, analysts, and fintech applications. For instance, you can trigger actions based on stock price changes, compile financial reports, or integrate with other services to enrich financial datasets. + +# Example Use Cases + +- **Automated Stock Alerts**: Set up a workflow that monitors specific stock prices through the Mboum API. When a price threshold is crossed, the workflow can trigger a notification to your email or messaging app, keeping you updated in real-time. + +- **Financial Reports Generation**: Create a workflow that periodically fetches financial statements for a set of companies using the Mboum API. Then, it compiles this data into a formatted report and sends it to cloud storage services like Google Drive or Dropbox for easy access and sharing. + +- **Market Sentiment Analysis**: Combine Mboum's market analysis with a natural language processing (NLP) app to gauge sentiment. The workflow can fetch the latest market news and use NLP to analyze sentiment, summarizing the mood of the market in an automated dashboard or report. diff --git a/components/mboum/mboum.app.mjs b/components/mboum/mboum.app.mjs new file mode 100644 index 0000000000000..f383fb7047cc8 --- /dev/null +++ b/components/mboum/mboum.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "mboum", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/mboum/package.json b/components/mboum/package.json new file mode 100644 index 0000000000000..d8f15f90c6c81 --- /dev/null +++ b/components/mboum/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/mboum", + "version": "0.0.1", + "description": "Pipedream Mboum Components", + "main": "mboum.app.mjs", + "keywords": [ + "pipedream", + "mboum" + ], + "homepage": "https://pipedream.com/apps/mboum", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/mctime/README.md b/components/mctime/README.md new file mode 100644 index 0000000000000..88f3b96c921eb --- /dev/null +++ b/components/mctime/README.md @@ -0,0 +1,11 @@ +# Overview + +The McTime API provides functionality for tracking and managing time-related data. With McTime integrated into Pipedream, you can automate processes involving employee scheduling, time tracking, and payroll. Leverage Pipedream's ability to connect with hundreds of apps to link time data with project management tools, accounting software, and communication platforms, streamlining workflow efficiencies and reducing manual timekeeping errors. + +# Example Use Cases + +- **Automated Payroll Processing**: Combine McTime with a payroll service like Gusto via Pipedream. Every time a new timesheet is approved in McTime, trigger a workflow that calculates pay and initiates a payroll run in Gusto, ensuring timely and accurate payment. + +- **Shift Scheduling Notifications**: Link McTime to a communication app like Slack. When a new shift is scheduled or changed in McTime, automatically send a notification to the relevant Slack channel or direct message to the employee, keeping everyone informed in real time. + +- **Project Time Tracking**: Integrate McTime with project management tools like Trello or Asana. After logging time in McTime for a project, a Pipedream workflow can update the corresponding task in Trello or Asana with time spent, improving project tracking and resource allocation. diff --git a/components/meaningcloud/README.md b/components/meaningcloud/README.md new file mode 100644 index 0000000000000..aee647c621e42 --- /dev/null +++ b/components/meaningcloud/README.md @@ -0,0 +1,11 @@ +# Overview + +The MeaningCloud API provides advanced text analysis capabilities leveraging natural language processing (NLP). With it, you can extract insights and meaning from textual content. In Pipedream, you can connect the MeaningCloud API to analyze the sentiment of customer feedback, classify text into categories, extract entities and concepts, and much more. The API's integration into serverless workflows on Pipedream allows for automating complex tasks that involve processing and understanding human language. + +# Example Use Cases + +- **Sentiment Analysis on Social Media Mentions**: Use MeaningCloud's Sentiment Analysis endpoint to assess the vibe of social media posts or comments mentioning your brand. Trigger a Pipedream workflow with new tweets from Twitter, analyze sentiment, and send a summary to Slack for your customer service team to follow up. + +- **Automated Support Ticket Categorization**: Process incoming support tickets by using the Text Classification feature of MeaningCloud to categorize tickets based on their content. Integrate with a platform like Zendesk, categorize each new ticket in Pipedream, and route them to the appropriate department or person. + +- **Content Compliance Checker**: Build a workflow that scans blog posts or articles for compliance with specific regulations before publication. Use MeaningCloud's Language Detection and Text Clustering to understand the content and flag non-compliant material. Integrate with a CMS like WordPress to automatically hold or tag posts needing further review. diff --git a/components/meaningcloud/package.json b/components/meaningcloud/package.json index 1b8807eead20c..433a67cb881c2 100644 --- a/components/meaningcloud/package.json +++ b/components/meaningcloud/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/mediatoolkit/README.md b/components/mediatoolkit/README.md index 508ef48c21976..337d0f6228105 100644 --- a/components/mediatoolkit/README.md +++ b/components/mediatoolkit/README.md @@ -1,15 +1,11 @@ # Overview -mediatoolkit is a powerful API that allows you to build a variety of -applications. Here are some examples of what you can build using mediatoolkit: - -- A social media monitoring tool that tracks mentions of your brand across all - major social networks -- A sentiment analysis tool that analyzes the sentiment of your brand’s social - media mentions -- An influencer marketing tool that identifies and tracks the most influential - people talking about your brand -- A crisis management tool that monitors social media for early signs of a - crisis and helps you respond quickly and effectively -- A competitive intelligence tool that tracks your competitors’ social media - activity and provides insights into their marketing strategies +Mediatoolkit is a powerful media monitoring tool that tracks online mentions of your brand or keywords across various platforms, helping you stay on top of public perception and industry trends. Using the Mediatoolkit API on Pipedream, you can automate reactions to these mentions, aggregate data for analytics, and integrate notifications into other apps to keep your team informed. This enables real-time media monitoring, sentiment analysis, and enhances your ability to respond swiftly to the social sentiments affecting your brand or market. + +# Example Use Cases + +- **Instant Alerting for Brand Mentions**: Set up a workflow on Pipedream that monitors for brand mentions using the Mediatoolkit API. When a mention is detected, it triggers a notification through email, SMS, or Slack, allowing your team to promptly see and react to what's being said about your brand. + +- **Sentiment Analysis and Reporting**: Use Mediatoolkit to fetch mentions and carry out sentiment analysis using an AI service like Google's Natural Language API. Analyze the sentiment of each mention and summarize the data in a weekly report, which can be automatically generated and sent to stakeholders via a tool like Google Sheets or email. + +- **Competitor Monitoring and Analysis**: Configure a Pipedream workflow that uses Mediatoolkit to track mentions of competitors. This data is then filtered and sorted according to relevance or sentiment and pushed into a CRM like Salesforce or a project management tool like Trello, where your marketing team can strategize and take action based on the insights gained. diff --git a/components/medium/package.json b/components/medium/package.json new file mode 100644 index 0000000000000..fa2828b320030 --- /dev/null +++ b/components/medium/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/medium", + "version": "0.6.0", + "description": "Pipedream medium Components", + "main": "medium.app.mjs", + "keywords": [ + "pipedream", + "medium" + ], + "homepage": "https://pipedream.com/apps/medium", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/meetingpulse/README.md b/components/meetingpulse/README.md new file mode 100644 index 0000000000000..31c9833d6bb37 --- /dev/null +++ b/components/meetingpulse/README.md @@ -0,0 +1,11 @@ +# Overview + +MeetingPulse is a platform designed to make meetings and events interactive by enabling real-time polling, Q&A sessions, quizzes, and more. Using the MeetingPulse API, you can integrate these interactive experiences into your processes or applications. With Pipedream, the API's potential multiplies, allowing you to create seamless workflows that react to meeting events, collate feedback, and automate follow-ups. By tapping into the API via Pipedream, you get the flexibility to trigger actions on numerous apps in response to meeting activities. + +# Example Use Cases + +- **Automate Feedback Collection**: After a MeetingPulse event ends, Pipedream can trigger a workflow that collects all feedback and polls responses. It can then format this data and send it to Google Sheets for analysis, or push it to a CRM like Salesforce to track attendee engagement over time. + +- **Real-Time Notifications**: Set up a Pipedream workflow that listens for new questions or comments submitted through MeetingPulse. These can trigger instant notifications in Slack to a designated channel, enabling your team to respond in real-time to audience engagement. + +- **Post-Event Follow-Up**: Use Pipedream to craft a workflow where attendees who participated in a MeetingPulse session receive personalized follow-up emails via SendGrid. The content can be customized based on their interaction, providing resources they showed interest in, or thanking them for their questions. diff --git a/components/meetup/README.md b/components/meetup/README.md index 9c2e506f3eed8..af755cc1aca83 100644 --- a/components/meetup/README.md +++ b/components/meetup/README.md @@ -1,10 +1,14 @@ # Overview -The Meetup API allows developers to create applications that make use of the -vast array of groups and events hosted on the Meetup website. Some examples of -what you can build using the Meetup API include: - -- A widget that displays upcoming events for a particular group -- A map application that shows meetup groups in a particular area -- A social networking application that helps users find groups and events based - on their interests +The Meetup API on Pipedream allows you to interact with the various aspects of the Meetup platform, such as accessing group information, RSVPs, and events. By leveraging this API, you can automate event management tasks, sync Meetup data with other services, and enhance user engagement by tapping into the community-driven features of Meetup. The integration offers possibilities for streamlining event coordination, sending notifications, and analyzing attendee data for better event planning. + +# Example Use Cases + +- **Automated Event Creation and Posting** + Use the Meetup API on Pipedream to automatically create and post new events to your Meetup group from an external event management system or CMS. This can be particularly useful for organizations that run frequent events and want to ensure their Meetup group stays up-to-date without manual intervention. + +- **RSVP Syncing and Communication** + Sync RSVPs from Meetup events to a Google Sheet for easy access and management. Set up automated email or SMS notifications using the Twilio app on Pipedream to thank attendees for registering and provide them with additional information or reminders about the event. + +- **Attendee Data Analysis and Reporting** + Collect data on event attendees and their interactions from the Meetup API and send it to a data analysis tool like Google Data Studio. Use this information to generate insights on event performance, member engagement, and to drive decisions for future event planning. diff --git a/components/megaventory/README.md b/components/megaventory/README.md index ab0ab064c559c..2427c7b333b30 100644 --- a/components/megaventory/README.md +++ b/components/megaventory/README.md @@ -1,12 +1,11 @@ # Overview - Megaventory offers a powerful and flexible API that lets you integrate your - existing systems with our platform. With the Megaventory API, you can: - -- Create and manage inventory items -- Create and manage sales orders -- Create and manage purchase orders -- Create and manage manufacturing orders -- Create and manage assemblies -- View real-time inventory levels -- And much more! +Megaventory is a robust inventory management API allowing for detailed tracking of products, orders, and the supply chain. You can automate the updating of stock levels, manage sales and purchase orders, and generate detailed reports. With Megaventory and Pipedream, you can craft automation workflows to streamline inventory operations, synchronize data across platforms, and trigger actions based on inventory changes. + +# Example Use Cases + +- **Inventory Level Alerts**: When stock levels for certain products fall below a designated threshold, trigger an alert using Pipedream to send notifications to Slack, ensuring timely restocking and avoiding stockouts. + +- **Order Processing Automation**: Automate the processing of new sales orders from Megaventory: when an order is received, use Pipedream to create invoices in QuickBooks, send a confirmation email to the customer via SendGrid, and update CRM records in Salesforce. + +- **Supplier Order Management**: When stock levels are low, automatically generate and send purchase orders to suppliers using Pipedream workflows. Integrate with email services to send the orders and update a Google Sheet with order details for easy tracking. diff --git a/components/meistertask/README.md b/components/meistertask/README.md index 93d28e5cb8233..6814e73ecfb68 100644 --- a/components/meistertask/README.md +++ b/components/meistertask/README.md @@ -1,20 +1,11 @@ # Overview -MeisterTask's API enables developers to build all sorts of integrations, tools -and applications that optimize and automate their workflows. Below are some -examples of what can be built using the MeisterTask API: +MeisterTask's API enables automation of task management processes, offering a programmatic way to interact with your tasks and projects. By leveraging the API with Pipedream, you can create powerful, serverless workflows that react to events in MeisterTask, manipulate tasks and projects, or synchronize data across multiple platforms. It's a robust tool for enhancing productivity and streamlining project collaboration. -- **Workflow Automation Tools:** Automate and optimize your workflows using - MeisterTask's API. For example, you could build a tool that automatically - creates new tasks in MeisterTask when issues are reported in your company's - bug tracker. -- **Time Tracking Applications:** Use MeisterTask's API to build a time - tracking application that integrates with your company's current workflow. -- **Reporting & Data Visualization Tools:** Generate reports and visualize data - from your MeisterTask account using the MeisterTask API. For example, you - could build a tool that generates a weekly report of how much time was spent - on each project. -- **Integrations with Other Applications:** MeisterTask's API enables - developers to build all sorts of integrations with other applications. For - example, you could build an integration that automatically adds new contacts - from your CRM into your MeisterTask account. +# Example Use Cases + +- **Task Synchronization with Google Calendar**: Integrate MeisterTask with Google Calendar to automatically create or update calendar events when tasks are created or modified. This keeps your schedule aligned with your project boards, ensuring you never miss a deadline. + +- **Slack Notifications on Task Updates**: Connect MeisterTask to Slack using Pipedream. Whenever a task is assigned, completed, or commented on in MeisterTask, trigger a notification in a specified Slack channel or direct message to keep your team instantly informed about project progress. + +- **GitHub Issue Integration**: Bridge MeisterTask with GitHub to create tasks in MeisterTask whenever new GitHub issues are opened. Conversely, when tasks are marked as completed in MeisterTask, automatically close related GitHub issues. This creates a seamless workflow for managing and tracking software development tasks. diff --git a/components/melissa_data/README.md b/components/melissa_data/README.md new file mode 100644 index 0000000000000..5a702ec91cf1f --- /dev/null +++ b/components/melissa_data/README.md @@ -0,0 +1,11 @@ +# Overview + +The Melissa Data API provides data quality services that can validate, cleanse, append, and enrich your data. Using Pipedream, you can seamlessly integrate these capabilities into workflows, automating processes that require address verification, email validation, phone number validation, and more. With Pipedream's serverless platform, you can trigger workflows with HTTP requests, schedule them, or even run them in response to events from other apps. + +# Example Use Cases + +- **Email Verification upon User Signup**: When a new user signs up on your platform, automate an email verification process using Melissa Data API. If the email is valid, continue the onboarding process; otherwise, prompt for a correct email. + +- **Scheduled Address Data Cleanse**: Set up a scheduled workflow to routinely cleanse and standardize mailing lists. The Melissa Data API can correct addresses, ensuring they're formatted and accurate for marketing campaigns or shipping processes. + +- **Lead Enrichment in CRM**: On adding a new lead to your CRM, use Melissa Data API within Pipedream to enrich lead details. Append demographic information or firmographics to leads in Salesforce or HubSpot, enabling better sales targeting and personalization. diff --git a/components/melissa_data/melissa_data.app.mjs b/components/melissa_data/melissa_data.app.mjs new file mode 100644 index 0000000000000..cdc1f13697aa1 --- /dev/null +++ b/components/melissa_data/melissa_data.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "melissa_data", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/melissa_data/package.json b/components/melissa_data/package.json new file mode 100644 index 0000000000000..8c59b0fe26f77 --- /dev/null +++ b/components/melissa_data/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/melissa_data", + "version": "0.0.1", + "description": "Pipedream Melissa Data Components", + "main": "melissa_data.app.mjs", + "keywords": [ + "pipedream", + "melissa_data" + ], + "homepage": "https://pipedream.com/apps/melissa_data", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/melo/README.md b/components/melo/README.md new file mode 100644 index 0000000000000..d5b744174c05e --- /dev/null +++ b/components/melo/README.md @@ -0,0 +1,11 @@ +# Overview + +Melo API enables you to manage and automate tasks related to project management and team collaboration. Utilize Melo's endpoints to create tasks, update project details, manage team assignments, and retrieve project metrics. Integrating Melo API with Pipedream allows you to connect your project management workflows with other apps and services, streamlining productivity and providing real-time updates across your tech stack. + +# Example Use Cases + +- **Sync Project Tasks with Google Calendar**: Automate the creation of Google Calendar events based on new tasks in Melo. When a task is created in Melo, Pipedream triggers a workflow that adds a corresponding event to a designated Google Calendar, ensuring that deadlines are visible across platforms. + +- **Slack Notifications for Task Updates**: Set up a workflow where any update to a task in Melo sends a message to a specified Slack channel or direct message. This keeps the team informed in real-time about changes in task statuses, comments, or priorities. + +- **Daily Email Digest of Project Progress**: Configure a Pipedream workflow to compile a daily summary of project updates from Melo and send it via email. This can include completed tasks, new assignments, and upcoming deadlines, providing a snapshot of project health to stakeholders every morning. diff --git a/components/mem/README.md b/components/mem/README.md new file mode 100644 index 0000000000000..ccbd9143666ef --- /dev/null +++ b/components/mem/README.md @@ -0,0 +1,11 @@ +# Overview + +The Mem API lets you interact with a unique knowledge base that adapts to your needs. Using Pipedream, you can automate tasks, sync information across apps, and trigger actions based on events in Mem. Think of pulling in tasks, pushing notifications, or archiving ideas - Pipedream serves as a superhighway for your Mem data to connect with over 800 apps. + +# Example Use Cases + +- **Sync Mem tasks with Google Calendar**: Create events in Google Calendar whenever a new task is added in Mem. Keep your schedule aligned without manual updates. + +- **Archive Mem notes to Dropbox**: Every time you mark a note as 'Completed' in Mem, automatically save a backup to a specified Dropbox folder. Ideal for maintaining records and freeing up space. + +- **Send Slack notifications for Mem reminders**: Set up a workflow that sends a message to your Slack when a reminder is due in Mem. Stay on top of your to-dos without leaving your team's communication hub. diff --git a/components/memberful/README.md b/components/memberful/README.md new file mode 100644 index 0000000000000..164c14733bf0d --- /dev/null +++ b/components/memberful/README.md @@ -0,0 +1,11 @@ +# Overview + +The Memberful API lets you handle memberships and subscriptions with precision. Using Pipedream, you can automate tasks around Memberful's robust features, like syncing member data, managing subscriptions, or triggering events based on membership changes. Pipedream's serverless platform turns these tasks into workflows you can build, run, and observe with ease. + +# Example Use Cases + +- **Sync Memberful Subscriptions with Mailchimp**: Automate the process of syncing new Memberful subscribers to a Mailchimp audience. When a new member subscribes through Memberful, use a Pipedream workflow to add their information to a specified Mailchimp list, ensuring your email marketing campaigns are always targeting the most up-to-date audience. + +- **Post Slack Notifications for Membership Updates**: Keep your team informed by posting notifications to a Slack channel whenever a Memberful membership is updated. Whether it’s a new subscription, cancellation, or a pause, a Pipedream workflow can capture these events and send a message to Slack, keeping everyone in the loop without manual checks. + +- **Generate Invoices Using Stripe for New Members**: When a new member signs up through Memberful, create an automated workflow in Pipedream that generates and sends an invoice via Stripe. This streamlines the billing process, making sure that every new member is promptly billed, and their payment information is stored correctly in Stripe for future transactions. diff --git a/components/membership_io/membership_io.app.mjs b/components/membership_io/membership_io.app.mjs new file mode 100644 index 0000000000000..0ff214f8ca984 --- /dev/null +++ b/components/membership_io/membership_io.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "membership_io", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/membership_io/package.json b/components/membership_io/package.json new file mode 100644 index 0000000000000..cc286d8de626e --- /dev/null +++ b/components/membership_io/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/membership_io", + "version": "0.0.1", + "description": "Pipedream Membership.io Components", + "main": "membership_io.app.mjs", + "keywords": [ + "pipedream", + "membership_io" + ], + "homepage": "https://pipedream.com/apps/membership_io", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/components/memberspot/README.md b/components/memberspot/README.md new file mode 100644 index 0000000000000..9b739dbac4958 --- /dev/null +++ b/components/memberspot/README.md @@ -0,0 +1,11 @@ +# Overview + +The Memberspot.io API lets you automate tasks and manage your online course platform efficiently. By integrating with Pipedream, you can leverage serverless workflows to handle routine tasks, synchronize data across various services, and tailor your course delivery to suit your needs better. Use Memberspot.io to manage users, courses, and access rights, and connect with other apps on Pipedream for a seamless data flow and enhanced functionality. + +# Example Use Cases + +- **Automate Course Enrollment**: When a user purchases a course via an e-commerce platform like Shopify, trigger a workflow in Pipedream to automatically enroll them in the corresponding course on Memberspot.io, ensuring immediate access with no manual intervention. + +- **Synchronize Users with CRM**: Keep your customer relationship management (CRM) system such as HubSpot updated by syncing new user registrations or profile updates from Memberspot.io to your CRM. This helps maintain accurate records for marketing and support purposes. + +- **Streamline Access Management**: Use Pipedream to monitor a Slack channel where team members can request access to specific courses. Upon approval, a workflow automatically grants the requested course access in Memberspot.io, streamlining the process and saving time. diff --git a/components/memberstack/README.md b/components/memberstack/README.md index 1c798c72b52d3..cf801269f8c51 100644 --- a/components/memberstack/README.md +++ b/components/memberstack/README.md @@ -1,6 +1,11 @@ # Overview -Memberstack is an API that enables developers to easily add membership features -to their websites and apps. With Memberstack, developers can add features such -as login/logout, signup/login, payment processing, and more. Memberstack also -makes it easy to manage users and their data. +Memberstack offers a powerful API for managing user memberships, payments, and gated content on websites. The API allows for deep customization and automation of membership-related tasks, which can be leveraged to enhance user experiences, streamline operations, and connect with other tools. Using Pipedream, these capabilities can be harnessed through serverless workflows. This flexibility enables developers to build intricate automations that react to events in Memberstack or to trigger actions in Memberstack based on events from other apps and services. + +# Example Use Cases + +- **Member Onboarding and Welcome Emails**: When a new user signs up via Memberstack, trigger a Pipedream workflow that sends a personalized welcome email using SendGrid. The workflow can also add the member to a Mailchimp list for future marketing campaigns. + +- **Membership Renewal Reminders**: Use Pipedream to schedule and send membership renewal reminders a few days before a user's subscription is due to expire. This workflow can integrate Memberstack with Twilio to send SMS reminders, ensuring users are prompted to renew their membership. + +- **Synchronize Member Data with a CRM**: Whenever a Memberstack profile is updated, a Pipedream workflow can trigger to sync the new data with a CRM like Salesforce. This keeps user records consistent and up-to-date across business platforms, which is crucial for sales and support teams. diff --git a/components/membervault/README.md b/components/membervault/README.md index 7b10f4dca65f9..e2fe0bc4238db 100644 --- a/components/membervault/README.md +++ b/components/membervault/README.md @@ -1,9 +1,11 @@ # Overview -With the Membervault API, you can build a variety of membership-based -applications, including: +MemberVault offers an innovative approach to engaging and understanding your audience through interactive content. With the MemberVault API, you can automate the management of users, products, and engagement data, ensuring seamless integration with your marketing and customer service tools. This capability opens up a world of possibilities for personalized interactions, targeted campaigns, and streamlined content delivery based on user behavior and preferences. -- A membership site with content that is only accessible to paying members -- A course platform where users can purchase and take courses -- A subscription service that delivers content on a regular basis -- An online community with restricted access for members only +# Example Use Cases + +- **Automated Welcome Sequence**: Trigger an email sequence in your favorite marketing app like Mailchimp when a new user signs up in MemberVault. Each email can be tailored based on the specific products the user has shown interest in, creating a personalized onboarding experience. + +- **Dynamic Course Access Control**: Connect MemberVault to a webinar platform like Zoom through Pipedream. Automate the process of granting webinar access to users who have completed a prerequisite module or achieved a certain engagement score, ensuring that learners are at the right stage of their educational journey. + +- **Real-Time Engagement Analytics**: Use Pipedream to pipe MemberVault engagement data into a dashboard tool such as Google Data Studio. This setup allows for the creation of real-time analytics dashboards to visualize user engagement trends, helping you make data-driven decisions to boost course interaction and content effectiveness. diff --git a/components/memix/README.md b/components/memix/README.md deleted file mode 100644 index ac4124d1bcda9..0000000000000 --- a/components/memix/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Overview - -Generate a Memix share URL with a random template using the supplied caption diff --git a/components/mercury/README.md b/components/mercury/README.md index 913a3591011c8..a95fd463e6709 100644 --- a/components/mercury/README.md +++ b/components/mercury/README.md @@ -1,8 +1,11 @@ # Overview -With the Mercury API, you can build: +The Mercury API offers a window into banking tailored for startups. With it, you can automate your financial operations, sync transaction data with your accounting software, and monitor your business financial health programmatically. Creating workflows on Pipedream with Mercury's API allows you to connect your banking data with numerous other apps to streamline payments, reconcile transactions, and keep a pulse on your company's money matters. -- Applications to read and analyze articles -- Content supplements such as summaries, image captions, and video transcripts -- A customizeable newsfeed for your website or application -- Your own news curation and content discovery tool +# Example Use Cases + +- **Automated Expense Tracking**: Integrate the Mercury API with Google Sheets on Pipedream to automatically log each transaction. Whenever a new transaction is posted to your Mercury account, a Pipedream workflow can trigger, appending the transaction details to a Google Sheet. This enables real-time expense tracking without manual data entry. + +- **Alerts for Large Transactions**: Set up a Pipedream workflow that triggers on high-value transactions from your Mercury bank account. Combine this with Twilio or another messaging service to send an SMS or an email alert to specified stakeholders, providing instant notifications on significant financial movements, keeping everyone in the loop for important or unusual activity. + +- **Reconciliation with Accounting Software**: Automatically reconcile your transactions by connecting Mercury to accounting software like QuickBooks or Xero through Pipedream. Create a workflow that watches for new transactions in your Mercury account, then matches and records them in your accounting platform, ensuring your books are always up-to-date with minimal manual intervention. diff --git a/components/merge/actions/create-activity/create-activity.mjs b/components/merge/actions/create-activity/create-activity.mjs new file mode 100644 index 0000000000000..df93ef725b400 --- /dev/null +++ b/components/merge/actions/create-activity/create-activity.mjs @@ -0,0 +1,74 @@ +import app from "../../merge.app.mjs"; + +export default { + key: "merge-create-activity", + name: "Create Activity", + description: "Creates an Activity object with the given values. [See the documentation](https://docs.merge.dev/ats/activities/#activities_create)", + version: "0.0.1", + type: "action", + props: { + app, + user: { + propDefinition: [ + app, + "user", + ], + }, + activityType: { + propDefinition: [ + app, + "activityType", + ], + }, + subject: { + propDefinition: [ + app, + "subject", + ], + }, + body: { + propDefinition: [ + app, + "body", + ], + }, + visibility: { + propDefinition: [ + app, + "visibility", + ], + }, + candidate: { + propDefinition: [ + app, + "candidate", + ], + }, + remoteUserId: { + propDefinition: [ + app, + "remoteUserId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createActivity({ + $, + data: { + model: { + user: this.user, + activity_type: this.activityType, + subject: this.subject, + body: this.body, + visibility: this.visibility, + candidate: this.candidate, + }, + remote_user_id: this.remoteUserId, + }, + }); + + $.export("$summary", `Successfully created the activity with the ID ${response.model.id}`); + + return response; + }, +}; diff --git a/components/merge/actions/create-candidate/create-candidate.mjs b/components/merge/actions/create-candidate/create-candidate.mjs new file mode 100644 index 0000000000000..8f34ebb9873d6 --- /dev/null +++ b/components/merge/actions/create-candidate/create-candidate.mjs @@ -0,0 +1,71 @@ +import app from "../../merge.app.mjs"; + +export default { + key: "merge-create-candidate", + name: "Create Candidate", + description: "Create a new candidate profile. [See the documentation](https://docs.merge.dev/ats/candidates/#candidates_create)", + version: "0.0.1", + type: "action", + props: { + app, + firstName: { + propDefinition: [ + app, + "firstName", + ], + }, + lastName: { + propDefinition: [ + app, + "lastName", + ], + }, + canEmail: { + propDefinition: [ + app, + "canEmail", + ], + }, + emailAddress: { + propDefinition: [ + app, + "emailAddress", + ], + }, + emailType: { + propDefinition: [ + app, + "emailType", + ], + }, + remoteUserId: { + propDefinition: [ + app, + "remoteUserId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createCandidate({ + $, + data: { + model: { + first_name: this.firstName, + last_name: this.lastName, + can_email: this.canEmail, + email_addresses: [ + { + value: this.emailAddress, + email_address_type: this.emailType, + }, + ], + }, + remote_user_id: this.remoteUserId, + }, + }); + + $.export("$summary", `Successfully created the candidate '${this.firstName} ${this.lastName}'`); + + return response; + }, +}; diff --git a/components/merge/actions/update-candidate/update-candidate.mjs b/components/merge/actions/update-candidate/update-candidate.mjs new file mode 100644 index 0000000000000..243411adbe985 --- /dev/null +++ b/components/merge/actions/update-candidate/update-candidate.mjs @@ -0,0 +1,78 @@ +import app from "../../merge.app.mjs"; + +export default { + key: "merge-update-candidate", + name: "Update Candidate", + description: "Update a candidate profile with the specified ID. [See the documentation](https://docs.merge.dev/ats/candidates/#candidates_partial_update)", + version: "0.0.1", + type: "action", + props: { + app, + candidate: { + propDefinition: [ + app, + "candidate", + ], + }, + firstName: { + propDefinition: [ + app, + "firstName", + ], + }, + lastName: { + propDefinition: [ + app, + "lastName", + ], + }, + canEmail: { + propDefinition: [ + app, + "canEmail", + ], + }, + emailAddress: { + propDefinition: [ + app, + "emailAddress", + ], + }, + emailType: { + propDefinition: [ + app, + "emailType", + ], + }, + remoteUserId: { + propDefinition: [ + app, + "remoteUserId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.updateCandidate({ + $, + id: this.candidate, + data: { + model: { + first_name: this.firstName, + last_name: this.lastName, + can_email: this.canEmail, + email_addresses: [ + { + value: this.emailAddress, + email_address_type: this.emailType, + }, + ], + }, + remote_user_id: this.remoteUserId, + }, + }); + + $.export("$summary", `Successfully updated the candidate with ID ${this.candidate}`); + + return response; + }, +}; diff --git a/components/merge/common/constants.mjs b/components/merge/common/constants.mjs new file mode 100644 index 0000000000000..a46e96d5d40c6 --- /dev/null +++ b/components/merge/common/constants.mjs @@ -0,0 +1,16 @@ +export default { + EMAIL_TYPES: [ + "PERSONAL", + "WORK", + "OTHER", + ], + ACTIVITY_TYPES: [ + "NOTE", + "OTHER", + ], + VISIBILITIES: [ + "ADMIN_ONLY", + "PUBLIC", + "PRIVATE", + ], +}; diff --git a/components/merge/merge.app.mjs b/components/merge/merge.app.mjs new file mode 100644 index 0000000000000..10efe68b9589c --- /dev/null +++ b/components/merge/merge.app.mjs @@ -0,0 +1,159 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "merge", + propDefinitions: { + user: { + type: "string", + label: "ID", + description: "ID of the user", + async options() { + const { results } = await this.getUsers(); + + return results.map(({ + id, first_name, last_name, + }) => ({ + value: id, + label: first_name + " " + last_name, + })); + }, + }, + activityType: { + type: "string", + label: "Type", + description: "Type of the activity", + options: constants.ACTIVITY_TYPES, + }, + subject: { + type: "string", + label: "Subject", + description: "Subject of the activity", + }, + body: { + type: "string", + label: "Body", + description: "The activity's body", + }, + visibility: { + type: "string", + label: "Visibility", + description: "Visibility of the activity", + options: constants.VISIBILITIES, + }, + candidate: { + type: "string", + label: "Candidate", + description: "The activity's candidate", + async options() { + const { results } = await this.getCandidates(); + + return results.map(({ + id, first_name, last_name, + }) => ({ + value: id, + label: first_name + " " + last_name, + })); + }, + }, + remoteUserId: { + type: "string", + label: "Remote User ID", + description: "ID of the user that is sending the request", + async options() { + const { results } = await this.getUsers(); + + return results.map(({ + id, first_name, last_name, + }) => ({ + value: id, + label: first_name + " " + last_name, + })); + }, + }, + firstName: { + type: "string", + label: "First Name", + description: "The candidate's first name", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The candidate's last name", + }, + canEmail: { + type: "boolean", + label: "Can Email", + description: "Whether or not the candidate can be emailed", + }, + emailAddress: { + type: "string", + label: "Email Addresses", + description: "The candidate's email address", + }, + emailType: { + type: "string", + label: "Email Type", + description: "The candidate's email address", + options: constants.EMAIL_TYPES, + }, + }, + methods: { + _baseUrl() { + return "https://api.merge.dev/api/ats/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "Authorization": `Bearer ${this.$auth.api_key}`, + "X-Account-Token": this.$auth.account_token, + }, + }); + }, + async createActivity(args = {}) { + return this._makeRequest({ + method: "post", + path: "/activities", + ...args, + }); + }, + async createCandidate(args = {}) { + return this._makeRequest({ + method: "post", + path: "/candidates", + ...args, + }); + }, + async updateCandidate({ + id, ...args + }) { + return this._makeRequest({ + method: "patch", + path: `/candidates/${id}`, + ...args, + }); + }, + async getUsers(args = {}) { + return this._makeRequest({ + path: "/users", + ...args, + }); + }, + async getCandidates(args = {}) { + return this._makeRequest({ + path: "/candidates", + ...args, + }); + }, + }, +}; diff --git a/components/merge/package.json b/components/merge/package.json new file mode 100644 index 0000000000000..2cc519833bc2e --- /dev/null +++ b/components/merge/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/merge", + "version": "0.1.0", + "description": "Pipedream Merge Components", + "main": "merge.app.mjs", + "keywords": [ + "pipedream", + "merge" + ], + "homepage": "https://pipedream.com/apps/merge", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/message_bird/README.md b/components/message_bird/README.md index e78ac49923669..25063f94b826c 100644 --- a/components/message_bird/README.md +++ b/components/message_bird/README.md @@ -1,11 +1,11 @@ # Overview -With the MessageBird API, you can build a variety of applications and -integrations that allow you to send and receive SMS messages. Here are some -examples of what you can build: - -- A messaging app for sending and receiving SMS messages -- An integration with your CRM system that sends SMS messages to customers -- A system that sends out SMS alerts for events or appointments -- A customer support system that allows customers to text in for help -- And more! +Message Bird is a communications platform offering a suite of API solutions for sending messages, making voice calls, and conducting video conferences. With Pipedream, you can harness these capabilities to automate personalized notifications, streamline customer support, and facilitate global communication workflows. Message Bird's API on Pipedream allows you to send real-time alerts, trigger voice messages based on customer actions, and integrate with other services to create powerful, multi-channel communication systems. + +# Example Use Cases + +- **Customer Support Automation**: Use Message Bird to automatically send SMS updates to customers regarding their support tickets. Trigger messages based on ticket status updates in helpdesk apps like Zendesk or Freshdesk. + +- **Appointment Reminders**: Set up automated reminders for appointments or reservations by connecting Google Calendar or Calendly to Message Bird. Send SMS reminders a day before the scheduled event to reduce no-shows. + +- **Real-Time Alert System**: Create a workflow that sends critical alerts via SMS when a monitoring app like Datadog or Uptime Robot detects downtime or performance issues on your website or app. diff --git a/components/message_bird/actions/create-contact/create-contact.mjs b/components/message_bird/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..99c5fb190099d --- /dev/null +++ b/components/message_bird/actions/create-contact/create-contact.mjs @@ -0,0 +1,41 @@ +import messagebird from "../../message_bird.app.mjs"; + +export default { + key: "message_bird-create-contact", + name: "Create Contact", + description: "Creates a new contact. [See the documentation](https://developers.messagebird.com/api/contacts/#create-a-contact)", + version: "0.0.1", + type: "action", + props: { + messagebird, + phone: { + type: "string", + label: "Phone", + description: "The phone number of the contact. Example: `31612345678`", + }, + firstName: { + type: "string", + label: "First Name", + description: "First name of the contact", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the contact", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.messagebird.createContact({ + $, + data: { + msisdn: this.phone, + firstName: this.firstName, + lastName: this.lastName, + }, + }); + $.export("$summary", `Successfully created contact with ID ${response.id}`); + return response; + }, +}; diff --git a/components/message_bird/actions/send-sms/send-sms.mjs b/components/message_bird/actions/send-sms/send-sms.mjs new file mode 100644 index 0000000000000..72cd578d3268e --- /dev/null +++ b/components/message_bird/actions/send-sms/send-sms.mjs @@ -0,0 +1,45 @@ +import messagebird from "../../message_bird.app.mjs"; + +export default { + key: "message_bird-send-sms", + name: "Send SMS", + description: "Sends an SMS message. [See the documentation](https://developers.messagebird.com/api/sms-messaging/#send-outbound-sms)", + version: "0.0.1", + type: "action", + props: { + messagebird, + originator: { + propDefinition: [ + messagebird, + "originator", + ], + }, + body: { + propDefinition: [ + messagebird, + "body", + ], + }, + recipients: { + propDefinition: [ + messagebird, + "recipients", + ], + }, + }, + async run({ $ }) { + const response = await this.messagebird.sendSMS({ + $, + data: { + originator: this.originator, + body: this.body, + recipients: typeof this.recipients === "string" + ? JSON.parse(this.recipients) + : this.recipients, + type: "sms", + }, + }); + $.export("$summary", `Successfully sent SMS with ID ${response.id}`); + return response; + }, +}; diff --git a/components/message_bird/actions/send-voice-message/send-voice-message.mjs b/components/message_bird/actions/send-voice-message/send-voice-message.mjs new file mode 100644 index 0000000000000..e43c9ab314baf --- /dev/null +++ b/components/message_bird/actions/send-voice-message/send-voice-message.mjs @@ -0,0 +1,45 @@ +import messagebird from "../../message_bird.app.mjs"; + +export default { + key: "message_bird-send-voice-message", + name: "Send Voice Message", + description: "Sends a voice message. [See the documentation](https://developers.messagebird.com/api/voice-messaging/#send-a-voice-message)", + version: "0.0.1", + type: "action", + props: { + messagebird, + body: { + propDefinition: [ + messagebird, + "body", + ], + }, + recipients: { + propDefinition: [ + messagebird, + "recipients", + ], + }, + originator: { + propDefinition: [ + messagebird, + "originator", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.messagebird.sendVoiceMessage({ + $, + data: { + body: this.body, + recipients: typeof this.recipients === "string" + ? JSON.parse(this.recipients) + : this.recipients, + originator: this.originator, + }, + }); + $.export("$summary", `Successfully sent voice message with ID ${response.id}`); + return response; + }, +}; diff --git a/components/message_bird/message_bird.app.mjs b/components/message_bird/message_bird.app.mjs index 16d8c68358749..10b2d1036cad2 100644 --- a/components/message_bird/message_bird.app.mjs +++ b/components/message_bird/message_bird.app.mjs @@ -1,11 +1,68 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "message_bird", - propDefinitions: {}, + propDefinitions: { + originator: { + type: "string", + label: "Originator", + description: "The sender of the message. This can be a telephone number (including country code) or an alphanumeric string. In case of an alphanumeric string, the maximum length is 11 characters.", + }, + body: { + type: "string", + label: "Body", + description: "The body of the message", + }, + recipients: { + type: "string[]", + label: "Recipients", + description: "An array of recipients msisdns (phone numbers)", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://rest.messagebird.com"; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `AccessKey ${this.$auth.access_key}`, + }, + ...opts, + }); + }, + listSMSMessages(opts = {}) { + return this._makeRequest({ + path: "/messages", + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contacts", + ...opts, + }); + }, + sendSMS(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/messages", + ...opts, + }); + }, + sendVoiceMessage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/voicemessages", + ...opts, + }); }, }, }; diff --git a/components/message_bird/package.json b/components/message_bird/package.json new file mode 100644 index 0000000000000..65d5b369395af --- /dev/null +++ b/components/message_bird/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/message_bird", + "version": "0.0.1", + "description": "Pipedream MessageBird Components", + "main": "message_bird.app.mjs", + "keywords": [ + "pipedream", + "message_bird" + ], + "homepage": "https://pipedream.com/apps/message_bird", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/message_bird/sources/new-sms-message-received/new-sms-message-received.mjs b/components/message_bird/sources/new-sms-message-received/new-sms-message-received.mjs new file mode 100644 index 0000000000000..24c4ced16e7c7 --- /dev/null +++ b/components/message_bird/sources/new-sms-message-received/new-sms-message-received.mjs @@ -0,0 +1,62 @@ +import messagebird from "../../message_bird.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + key: "message_bird-new-sms-message-received", + name: "New SMS Message Received", + description: "Emit new event when a new SMS message is received. [See the documentation](https://developers.messagebird.com/api/sms-messaging/#list-messages)", + version: "0.0.1", + dedupe: "unique", + type: "source", + props: { + messagebird, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs"); + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + generateMeta(message) { + return { + id: message.id, + summary: `New Message ID: ${message.id}`, + ts: Date.parse(message.createdDatetime), + }; + }, + }, + async run() { + let lastTs = this._getLastTs(); + + const { items } = await this.messagebird.listSMSMessages({ + params: { + direction: "mo", // mt = sent, mo = received + type: "sms", + from: lastTs, + }, + }); + + if (!items?.length) { + return; + } + + for (const message of items) { + const meta = this.generateMeta(message); + this.$emit(message, meta); + + if (!lastTs || Date.parse(message.createdDatetime) > Date.parse(lastTs)) { + lastTs = message.createdDatetime; + } + } + + this._setLastTs(lastTs); + }, +}; diff --git a/components/messagebird/.gitignore b/components/messagebird/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/messagebird/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/messagebird/README.md b/components/messagebird/README.md new file mode 100644 index 0000000000000..3d5c3e44f500c --- /dev/null +++ b/components/messagebird/README.md @@ -0,0 +1,11 @@ +# Overview + +The MessageBird API on Pipedream allows you to automate communications by sending messages, making voice calls, and verifying phone numbers. With Pipedream's serverless platform, you can trigger workflows that integrate MessageBird with hundreds of other apps to engage customers, streamline notifications, and improve verification processes. Create custom alert systems, chatbots, or multi-factor authentication (MFA) mechanisms by leveraging the API's capabilities within Pipedream's seamless integration environment. + +# Example Use Cases + +- **Customer Support Ticket Alert System**: When a new ticket is created in your helpdesk software (e.g., Zendesk), send an SMS to the assigned support agent with the MessageBird API. This ensures immediate notification, leading to faster response times. + +- **E-Commerce Order Confirmation**: After a customer places an order on your website, use the MessageBird API to send an SMS confirmation. Combine it with Stripe to provide payment status updates, ensuring a smooth transaction and enhancing customer experience. + +- **Appointment Reminder Service**: Use Google Calendar events to trigger a Pipedream workflow that sends SMS reminders to clients a day before their appointment using the MessageBird API. Reduce no-shows and improve client relations without manual effort. diff --git a/components/messagebird/actions/create-contact/create-contact.mjs b/components/messagebird/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..603a503d22f73 --- /dev/null +++ b/components/messagebird/actions/create-contact/create-contact.mjs @@ -0,0 +1,56 @@ +import messagebird from "../../messagebird.app.mjs"; + +export default { + key: "messagebird-create-contact", + name: "Create Contact", + description: "Creates a new contact. [See the documentation](https://docs.bird.com/api/contacts-api/api-reference/manage-workspace-contacts/create-a-contact)", + version: "0.0.1", + type: "action", + props: { + messagebird, + workspaceId: { + propDefinition: [ + messagebird, + "workspaceId", + ], + }, + displayName: { + type: "string", + label: "Display Name", + description: "The display name for the contact", + }, + email: { + type: "string", + label: "Email", + description: "The email address of the contact", + optional: true, + }, + listIds: { + propDefinition: [ + messagebird, + "listIds", + (c) => ({ + workspaceId: c.workspaceId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.messagebird.createContact({ + $, + workspaceId: this.workspaceId, + data: { + displayName: this.displayName, + identifiers: this.email && [ + { + key: "emailaddress", + value: this.email, + }, + ], + listIds: this.listIds, + }, + }); + $.export("$summary", `Successfully created contact with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/messagebird/actions/send-sms/send-sms.mjs b/components/messagebird/actions/send-sms/send-sms.mjs new file mode 100644 index 0000000000000..87c83a0c81adc --- /dev/null +++ b/components/messagebird/actions/send-sms/send-sms.mjs @@ -0,0 +1,65 @@ +import messagebird from "../../messagebird.app.mjs"; + +export default { + key: "messagebird-send-sms", + name: "Send SMS", + description: "Sends an SMS message. [See the documentation](https://docs.bird.com/api/channels-api/supported-channels/programmable-sms/sending-sms-messages)", + version: "0.0.1", + type: "action", + props: { + messagebird, + workspaceId: { + propDefinition: [ + messagebird, + "workspaceId", + ], + }, + channelId: { + propDefinition: [ + messagebird, + "channelId", + (c) => ({ + workspaceId: c.workspaceId, + }), + ], + }, + contactId: { + propDefinition: [ + messagebird, + "contactId", + (c) => ({ + workspaceId: c.workspaceId, + }), + ], + }, + message: { + type: "string", + label: "Message", + description: "The message text to send", + }, + }, + async run({ $ }) { + const response = await this.messagebird.sendSmsMessage({ + $, + workspaceId: this.workspaceId, + channelId: this.channelId, + data: { + receiver: { + contacts: [ + { + id: this.contactId, + }, + ], + }, + body: { + type: "text", + text: { + text: this.message, + }, + }, + }, + }); + $.export("$summary", `Successfully sent SMS message with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/messagebird/actions/send-voice-message/send-voice-message.mjs b/components/messagebird/actions/send-voice-message/send-voice-message.mjs new file mode 100644 index 0000000000000..d01ee564985a9 --- /dev/null +++ b/components/messagebird/actions/send-voice-message/send-voice-message.mjs @@ -0,0 +1,60 @@ +import messagebird from "../../messagebird.app.mjs"; + +export default { + key: "messagebird-send-voice-message", + name: "Send Voice Message", + description: "Send a voice message to any phone number globally. [See the documentation](https://docs.bird.com/api/voice-api/voice-calls-api/initiate-an-outbound-call)", + version: "0.0.1", + type: "action", + props: { + messagebird, + workspaceId: { + propDefinition: [ + messagebird, + "workspaceId", + ], + }, + channelId: { + propDefinition: [ + messagebird, + "channelId", + (c) => ({ + workspaceId: c.workspaceId, + }), + ], + }, + recipientNumber: { + type: "string", + label: "Recipient Number", + description: "The phone number to send the message to. Example: `+351000000000`", + }, + message: { + type: "string", + label: "Message", + description: "The message to send as a voice message", + }, + }, + async run({ $ }) { + const response = await this.messagebird.sendVoiceMessage({ + $, + workspaceId: this.workspaceId, + channelId: this.channelId, + data: { + to: this.recipientNumber, + callFlow: [ + { + options: { + text: this.message, + }, + command: "say", + }, + { + command: "hangup", + }, + ], + }, + }); + $.export("$summary", `Successfully sent voice message with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/messagebird/app/messagebird.app.ts b/components/messagebird/app/messagebird.app.ts deleted file mode 100644 index 5d564cb3bc9a5..0000000000000 --- a/components/messagebird/app/messagebird.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "messagebird", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/messagebird/messagebird.app.mjs b/components/messagebird/messagebird.app.mjs new file mode 100644 index 0000000000000..6b4f3e23c7039 --- /dev/null +++ b/components/messagebird/messagebird.app.mjs @@ -0,0 +1,221 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "messagebird", + propDefinitions: { + workspaceId: { + type: "string", + label: "Workspace ID", + description: "The unique identifier of a workspace", + async options({ prevContext }) { + const { next: pageToken } = prevContext; + const { + results, nextPageToken, + } = await this.listWorkspaces({ + organizationId: this._organizationId(), + params: pageToken + ? { + pageToken, + } + : {}, + }); + return { + options: results?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || [], + context: { + next: nextPageToken, + }, + }; + }, + }, + channelId: { + type: "string", + label: "Channel ID", + description: "The unique identifier of a channel", + async options({ + workspaceId, prevContext, + }) { + if (!workspaceId) { + return []; + } + const { next: pageToken } = prevContext; + const { + results, nextPageToken, + } = await this.listChannels({ + workspaceId, + params: pageToken + ? { + pageToken, + } + : {}, + }); + return { + options: results?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || [], + context: { + next: nextPageToken, + }, + }; + }, + }, + contactId: { + type: "string", + label: "Contact ID", + description: "The contact to send the message to", + async options({ + workspaceId, prevContext, + }) { + if (!workspaceId) { + return []; + } + const { next: pageToken } = prevContext; + const { + results, nextPageToken, + } = await this.listContacts({ + workspaceId, + params: pageToken + ? { + pageToken, + } + : {}, + }); + return { + options: results?.map(({ + id: value, computedDisplayName: label, + }) => ({ + value, + label, + })) || [], + context: { + next: nextPageToken, + }, + }; + }, + }, + listIds: { + type: "string[]", + label: "List IDs", + description: "An array of unique list identifiers to add the contact to", + optional: true, + async options({ workspaceId }) { + if (!workspaceId) { + return []; + } + const { results } = await this.listLists({ + workspaceId, + }); + return results?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.bird.com"; + }, + _organizationId() { + return this.$auth.organization_id; + }, + _makeRequest({ + $ = this, + path, + ...otherOpts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `AccessKey ${this.$auth.api_key}`, + }, + ...otherOpts, + }); + }, + createWebhook({ + workspaceId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/organizations/${this._organizationId()}/workspaces/${workspaceId}/webhook-subscriptions`, + ...opts, + }); + }, + deleteWebhook({ + workspaceId, hookId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/organizations/${this._organizationId()}/workspaces/${workspaceId}/webhook-subscriptions/${hookId}`, + ...opts, + }); + }, + listWorkspaces(opts = {}) { + return this._makeRequest({ + path: `/organizations/${this._organizationId()}/workspaces`, + ...opts, + }); + }, + listChannels({ + workspaceId, ...opts + }) { + return this._makeRequest({ + path: `/workspaces/${workspaceId}/channels`, + ...opts, + }); + }, + listLists({ + workspaceId, ...opts + }) { + return this._makeRequest({ + path: `/workspaces/${workspaceId}/lists`, + ...opts, + }); + }, + listContacts({ + workspaceId, ...opts + }) { + return this._makeRequest({ + path: `/workspaces/${workspaceId}/contacts`, + ...opts, + }); + }, + createContact({ + workspaceId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/workspaces/${workspaceId}/contacts`, + ...opts, + }); + }, + sendVoiceMessage({ + workspaceId, channelId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/workspaces/${workspaceId}/channels/${channelId}/calls`, + ...opts, + }); + }, + sendSmsMessage({ + workspaceId, channelId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/workspaces/${workspaceId}/channels/${channelId}/messages`, + ...opts, + }); + }, + }, +}; diff --git a/components/messagebird/package.json b/components/messagebird/package.json index 0d01bddd480fb..2c188834d5cd3 100644 --- a/components/messagebird/package.json +++ b/components/messagebird/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/messagebird", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream MessageBird Components", - "main": "dist/app/messagebird.app.mjs", + "main": "messagebird.app.mjs", "keywords": [ "pipedream", "messagebird" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/messagebird", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/messagebird/sources/new-message-received/new-message-received.mjs b/components/messagebird/sources/new-message-received/new-message-received.mjs new file mode 100644 index 0000000000000..bed0f49addaa7 --- /dev/null +++ b/components/messagebird/sources/new-message-received/new-message-received.mjs @@ -0,0 +1,85 @@ +import messagebird from "../../messagebird.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "messagebird-new-message-received", + name: "New Message Received (Instant)", + description: "Emit new event for each new message received. [See the documentation](https://docs.bird.com/api/notifications-api/api-reference/webhook-subscriptions/create-a-webhook-subscription)", + version: "0.0.1", + dedupe: "unique", + type: "source", + props: { + messagebird, + http: "$.interface.http", + db: "$.service.db", + workspaceId: { + propDefinition: [ + messagebird, + "workspaceId", + ], + }, + platform: { + type: "string", + label: "Platform", + description: "The type of inbound message to watch for", + options: [ + "sms", + "whatsapp", + "email", + "line", + "instagram", + "facebook", + "viber", + "linkedin", + "tiktok", + "telegram", + ], + }, + }, + hooks: { + async activate() { + const { id } = await this.messagebird.createWebhook({ + workspaceId: this.workspaceId, + data: { + service: "channels", + event: `${this.platform}.inbound`, + url: this.http.endpoint, + }, + }); + this._setHookId(id); + }, + async deactivate() { + const hookId = this._getHookId(); + if (hookId) { + await this.messagebird.deleteWebhook({ + workspaceId: this.workspaceId, + hookId, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + generateMeta(event) { + return { + id: event.payload.id, + summary: `New Message ID: ${event.payload.id}`, + ts: Date.parse(event.createdAt), + }; + }, + }, + async run(event) { + const { body } = event; + if (!body) { + return; + } + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, + sampleEmit, +}; diff --git a/components/messagebird/sources/new-message-received/test-event.mjs b/components/messagebird/sources/new-message-received/test-event.mjs new file mode 100644 index 0000000000000..c86ec5b5ee366 --- /dev/null +++ b/components/messagebird/sources/new-message-received/test-event.mjs @@ -0,0 +1,44 @@ +export default { + "service": "channels", + "event": "whatsapp.inbound", + "payload": { + "id": "404544c5-9920-45f1-8990-0855634ab7ac", + "channelId": "52ad151f-f7b4-46f9-a5c6-d3318e650c84", + "sender": { + "contact": { + "id": "d4e8935a-5f48-45a5-95a5-ee7ba1e2c5b4", + "identifierKey": "phonenumber", + "identifierValue": "+3511111111" + } + }, + "receiver": { + "connector": { + "id": "60bf59d6-fc6f-4511-89c4-75d9127d7a7c", + "identifierValue": "" + } + }, + "body": { + "type": "text", + "text": { + "text": "Test" + } + }, + "meta": { + "extraInformation": { + "timestamp": "1702496037" + } + }, + "reference": "", + "parts": [ + { + "platformReference": "wamid.HBgMMzUxOTE0MjYyNTM1FQIAEhggQUMzODQyNUY1MDlDQkU1QjJGNTM3RDlENjg0OTJGMDgA" + } + ], + "status": "delivered", + "reason": "", + "direction": "incoming", + "lastStatusAt": "2023-12-13T19:34:01.858Z", + "createdAt": "2023-12-13T19:34:01.858Z", + "updatedAt": "2023-12-13T19:34:02.063Z" + } +} \ No newline at end of file diff --git a/components/metabase/README.md b/components/metabase/README.md new file mode 100644 index 0000000000000..bf015a48bad25 --- /dev/null +++ b/components/metabase/README.md @@ -0,0 +1,11 @@ +# Overview + +The Metabase API opens a gateway to interact with Metabase programmatically, enabling you to automate reporting, dashboards, and data analysis operations. With Pipedream, you can harness this API to trigger workflows, manipulate data, and integrate with various other apps to create a seamless data ecosystem. Think of syncing Metabase insights with other tools, automating report generation, or reacting to events within your Metabase instance in real-time. + +# Example Use Cases + +- **Automated Reporting**: Use Pipedream to set up scheduled triggers that fetch reports from Metabase and send them via email or Slack. This workflow can help teams stay updated with the latest insights without manual intervention. + +- **Dashboard Sync**: Create a workflow that listens for updates in a specific Metabase dashboard and synchronizes those changes with a Google Sheets document. This allows for easy sharing and collaboration on data insights with stakeholders who prefer working in spreadsheets. + +- **Alerting on Metrics**: Set up a Pipedream workflow that monitors specific metrics within Metabase. If certain thresholds are crossed, actions can be taken automatically, like sending alerts through SMS using Twilio or creating a task in project management tools like Trello. diff --git a/components/metaphor/README.md b/components/metaphor/README.md new file mode 100644 index 0000000000000..931fecbc0b55d --- /dev/null +++ b/components/metaphor/README.md @@ -0,0 +1,11 @@ +# Overview + +The Exa (Formerly Metaphor) API enables you to enrich your applications with machine learning-powered insights and data processing capabilities. With Exa, you can perform advanced data analysis, extract meaningful patterns, and automate decision-making processes within your projects. By integrating this API with Pipedream, you can create seamless workflows that leverage these intelligent features alongside other services to streamline your operations, enhance data intelligence, and drive innovation. + +# Example Use Cases + +- **Automated Content Analysis and Categorization**: Connect the Exa API to a content management system (CMS) on Pipedream. As new content is published—whether it be articles, posts, or reviews—the workflow triggers, sending the content to Exa for analysis. The API categorizes the content and extracts key topics, which you can then use to automatically tag and sort the content within the CMS or recommend related articles. + +- **Customer Feedback Sentiment Analysis**: Integrate the Exa API with a customer support platform like Zendesk. Whenever a new customer ticket is submitted, the workflow kicks off, passing the ticket's text to Exa for sentiment analysis. Based on the sentiment score, the workflow can prioritize tickets, escalate urgent matters, or provide insights to the support team on the customer's emotional tone, helping them tailor their response. + +- **Real-Time Market Trend Monitoring**: Combine the Exa API with a platform like Google Sheets on Pipedream. Set up a workflow that periodically collects social media posts or news articles relevant to your business from various sources. Exa analyzes this data for trends, sentiment, and key phrases, and then the workflow populates a Google Sheet with these insights, providing a real-time dashboard of market trends that can inform strategic decisions. diff --git a/components/metatext_ai_inference_api/README.md b/components/metatext_ai_inference_api/README.md new file mode 100644 index 0000000000000..2d36dfb626814 --- /dev/null +++ b/components/metatext_ai_inference_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Metatext.AI Inference API allows developers to leverage advanced machine learning models for natural language understanding tasks such as classification, sentiment analysis, named entity recognition, and more. In Pipedream, you can easily integrate this API into your serverless workflows to automate text analysis and enrichment across various data sources and applications. + +# Example Use Cases + +- **Content Moderation Pipeline**: Automate the moderation of user-generated content by analyzing text for inappropriate or offensive language. Use the Metatext.AI Inference API to classify comments, and then route them for manual review or automatic approval/blocking based on the analysis results. + +- **Customer Feedback Analysis**: Connect the Metatext.AI Inference API to a form submission or customer support ticket system. Automatically categorize feedback sentiment and topics, which can then be used to prioritize issues, inform product teams, or trigger specific follow-up actions in CRM systems like Salesforce. + +- **Social Media Monitoring**: Create a workflow that streams social media posts from an app like Twitter, then employs the Metatext.AI Inference API to detect and extract relevant entities and sentiments. Use these insights to inform marketing strategies, detect brand mentions, or respond to customers in real-time. diff --git a/components/metatext_ai_pre_build_ai_models_api/README.md b/components/metatext_ai_pre_build_ai_models_api/README.md new file mode 100644 index 0000000000000..57ecea7dbe6ca --- /dev/null +++ b/components/metatext_ai_pre_build_ai_models_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Metatext.AI Pre-built AI Models API offers various artificial intelligence capabilities such as natural language processing, image recognition, and sentiment analysis. This API enables users to add AI features to their applications without the need for extensive machine learning expertise. Utilizing this API in Pipedream workflows allows for automation and integration with other services, making it possible to process and analyze text and images within a serverless environment efficiently. + +# Example Use Cases + +- **Content Moderation Workflow**: Use Metatext.AI's sentiment analysis to filter and moderate user-generated content in real-time. Set up a Pipedream workflow that monitors a database or stream for new entries, passes the text through Metatext.AI for analysis, and flags or removes content based on negativity scores. This can be integrated with a CMS like WordPress for automated content control. + +- **Image Categorization and Tagging**: Automate the process of sorting and tagging images by content. Build a workflow where upon uploading an image to a cloud storage service like Amazon S3, the image is then sent to Metatext.AI for recognition. The resulting tags are added to the image metadata in the storage service, or used to sort images into appropriate folders/categories. + +- **Customer Feedback Analysis**: Analyze customer feedback by connecting Metatext.AI with customer support platforms such as Zendesk. Create a Pipedream workflow that triggers when new support tickets are submitted, applies Metatext.AI's sentiment analysis to determine the urgency or overall sentiment of the ticket, and then prioritizes or routes the ticket accordingly within Zendesk based on the analysis. diff --git a/components/meteomatics_weather_api/actions/get-weather-data/get-weather-data.mjs b/components/meteomatics_weather_api/actions/get-weather-data/get-weather-data.mjs new file mode 100644 index 0000000000000..30f657a0fde78 --- /dev/null +++ b/components/meteomatics_weather_api/actions/get-weather-data/get-weather-data.mjs @@ -0,0 +1,49 @@ +import app from "../../meteomatics_weather_api.app.mjs"; + +export default { + key: "meteomatics_weather_api-get-weather-data", + name: "Get Weather Data", + description: "Retrieve historic, current, and forecast data globally. [See the documentation](https://www.meteomatics.com/en/api/getting-started/)", + version: "0.0.2", + type: "action", + props: { + app, + validDateTime: { + propDefinition: [ + app, + "validDateTime", + ], + }, + parameters: { + propDefinition: [ + app, + "parameters", + ], + }, + locations: { + propDefinition: [ + app, + "locations", + ], + }, + format: { + propDefinition: [ + app, + "format", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getWeatherData({ + $, + validdatetime: this.validDateTime.join("--"), + parameters: this.parameters.join(","), + locations: this.locations, + format: this.format, + }); + + $.export("$summary", "Successfully retrieved weather data"); + + return response; + }, +}; diff --git a/components/meteomatics_weather_api/common/constants.mjs b/components/meteomatics_weather_api/common/constants.mjs new file mode 100644 index 0000000000000..2623969109343 --- /dev/null +++ b/components/meteomatics_weather_api/common/constants.mjs @@ -0,0 +1,26 @@ +export default { + PARAMETERS: [ + { + label: "Instantaneous wind speed at 10m above ground", + value: "wind_speed_10m:ms", + }, + { + label: "Instantaneous temperature at 2m above ground in degrees Celsius", + value: "t_2m:C", + }, + { + label: "Precipitation accumulated over the past 24 hours in millimeter (equivalent to litres per square meter)", + value: "precip_24h:mm", + }, + { + label: "UV index", + value: "uv:idx", + }, + ], + FORMATS: [ + "csv", + "xml", + "json", + "png", + ], +}; diff --git a/components/meteomatics_weather_api/meteomatics_weather_api.app.mjs b/components/meteomatics_weather_api/meteomatics_weather_api.app.mjs new file mode 100644 index 0000000000000..93cd43e318198 --- /dev/null +++ b/components/meteomatics_weather_api/meteomatics_weather_api.app.mjs @@ -0,0 +1,60 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "meteomatics_weather_api", + propDefinitions: { + validDateTime: { + type: "string[]", + label: "Valid Date Time", + description: "A date or date range to retrieve the weather forecast for, i.e. `2017-05-28T13:00:00Z`", + }, + parameters: { + type: "string[]", + label: "Parameters", + description: "One or more parameters to be included in this request", + options: constants.PARAMETERS, + }, + locations: { + type: "string", + label: "Location", + description: "Geo-coordinates (latitude and longitude) in WGS-84 decimal format, i.e. `47.419708,9.358478`", + }, + format: { + type: "string", + label: "Format", + description: "The data format of the output", + options: constants.FORMATS, + }, + }, + methods: { + _baseUrl() { + return "https://api.meteomatics.com"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + params: { + ...params, + access_token: `${this.$auth.oauth_access_token}`, + }, + }); + }, + async getWeatherData({ + validdatetime, parameters, locations, format, ...args + }) { + return this._makeRequest({ + path: `/${validdatetime}/${parameters}/${locations}/${format}`, + ...args, + }); + }, + }, +}; diff --git a/components/meteomatics_weather_api/package.json b/components/meteomatics_weather_api/package.json new file mode 100644 index 0000000000000..05a83fdc1105e --- /dev/null +++ b/components/meteomatics_weather_api/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/meteomatics_weather_api", + "version": "0.1.1", + "description": "Pipedream Meteomatics Weather API Components", + "main": "meteomatics_weather_api.app.mjs", + "keywords": [ + "pipedream", + "meteomatics_weather_api" + ], + "homepage": "https://pipedream.com/apps/meteomatics_weather_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/mews/README.md b/components/mews/README.md index f8ce9bb025fea..9628b7a4b445c 100644 --- a/components/mews/README.md +++ b/components/mews/README.md @@ -1,9 +1,11 @@ # Overview -With the Mews API, you can build software that integrates with the Mews -Platform and automates various tasks, such as: +The Mews API is a gateway for developers to interact with the Mews hospitality platform, allowing for innovative connections and automations between hotel management systems and other software tools. By using Pipedream, you can create custom workflows that automate repetitive tasks, integrate with numerous third-party services, and manipulate data to suit specific needs within the hospitality industry. The combination of Mews' API and Pipedream's serverless execution model paves the way for enhanced guest experiences, optimized operations, and real-time data handling. -- Collecting and processing payments -- Managing reservations -- Generating reports -- Synchronizing data with other systems +# Example Use Cases + +- **Automated Guest Communication Workflow**: Send personalized welcome emails or SMS messages to guests upon check-in using Mews with a service like SendGrid or Twilio. Trigger this action in Pipedream when a new guest is added to the Mews system, ensuring timely and personalized guest engagement. + +- **Dynamic Pricing Adjustment**: Integrate Mews with external pricing analysis tools to automatically update room rates based on demand. Use Pipedream to monitor occupancy data from Mews and trigger price changes that are then reflected back in the Mews system, optimizing revenue management strategies. + +- **Housekeeping Status Updates**: Connect Mews to a team collaboration platform like Slack using Pipedream. Notify housekeeping staff when guests check out or request room cleaning, streamlining communication and improving room turnaround times for incoming guests. diff --git a/components/mews/package.json b/components/mews/package.json new file mode 100644 index 0000000000000..017f397007a40 --- /dev/null +++ b/components/mews/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/mews", + "version": "0.6.0", + "description": "Pipedream mews Components", + "main": "mews.app.mjs", + "keywords": [ + "pipedream", + "mews" + ], + "homepage": "https://pipedream.com/apps/mews", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/mezmo/README.md b/components/mezmo/README.md new file mode 100644 index 0000000000000..55bafa7f56119 --- /dev/null +++ b/components/mezmo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Mezmo API provides a window into your application's log data, allowing for the ingestion, search, and monitoring of logs. Within Pipedream, you can leverage Mezmo to create powerful, serverless workflows that respond to log events in real-time, search and analyze logged data, and automate notifications or actions based on log insights. It's an ideal tool for developers and DevOps teams who need to integrate log management into their wider operational toolkit. + +# Example Use Cases + +- **Automated Error Alerting**: When Mezmo detects an error in your logs, a Pipedream workflow can automatically send alerts to communication platforms like Slack or email. This immediate notification enables swift reaction to potential issues. + +- **Log-Triggered Issue Creation**: A Pipedream workflow could watch for specific log patterns or error codes in Mezmo, and when encountered, automatically create an issue in a project management tool like JIRA or GitHub Issues, ensuring that your team can track and address problems efficiently. + +- **Daily Log Summary Reports**: Compile a daily summary of logs by triggering a Pipedream workflow that sifts through the day's log data in Mezmo. The workflow can then format and send a report to tools like Google Sheets or Airtable, or distribute it via email, providing stakeholders with regular insights. diff --git a/components/mezmo/package.json b/components/mezmo/package.json index 152425dacd7ba..28c735897d07f 100644 --- a/components/mezmo/package.json +++ b/components/mezmo/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/microsoft_365/README.md b/components/microsoft_365/README.md index c0052192bc761..898486378eaa8 100644 --- a/components/microsoft_365/README.md +++ b/components/microsoft_365/README.md @@ -1,13 +1,11 @@ # Overview -With the Microsoft 365 API, you can build a wide variety of applications and -solutions that work with your Office 365 data. Here are some examples of what -you can build: - -- A productivity solution that helps you get more done by integrating with your - Office 365 data -- A customer relationship management (CRM) system that lets you track your - interactions with customers -- A financial management system that gives you insights into your finances -- A marketing automation system that helps you plan and execute campaigns -- A human resources (HR) solution that helps you track employee information +The Microsoft 365 API taps into the productivity power of services like Outlook, OneDrive, Excel, and more. It's a treasure chest for those seeking to automate office tasks, streamline communications, and orchestrate data between applications. With Pipedream, you can create workflows that react to events in the Microsoft ecosystem, manipulate data, or automate repetitive tasks, freeing you to focus on the bigger picture. + +# Example Use Cases + +- **Automated Email Campaigns Based on Spreadsheet Data**: Trigger a workflow whenever a new row is added to an Excel spreadsheet on OneDrive. Each row contains email addresses and personalized data. Pipedream can draft and send personalized emails through Outlook to each address listed, effortlessly running your targeted campaigns. + +- **Calendar Event Management**: Sync your Microsoft 365 Calendar with a third-party app like Google Calendar. When a new event is created in Outlook, Pipedream creates a corresponding event in Google Calendar, ensuring that your schedule is mirrored across platforms without manual entry. + +- **Document Workflow and Notification**: When a document is uploaded to OneDrive, Pipedream can automatically trigger a notification to a Slack channel. The workflow could further analyze the document content, apply text recognition if it's an image, or even start an approval process, seamlessly connecting team communication with content management. diff --git a/components/microsoft_365/package.json b/components/microsoft_365/package.json new file mode 100644 index 0000000000000..528f09116d941 --- /dev/null +++ b/components/microsoft_365/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/microsoft_365", + "version": "0.6.0", + "description": "Pipedream microsoft_365 Components", + "main": "microsoft_365.app.mjs", + "keywords": [ + "pipedream", + "microsoft_365" + ], + "homepage": "https://pipedream.com/apps/microsoft_365", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/microsoft_365_people/README.md b/components/microsoft_365_people/README.md new file mode 100644 index 0000000000000..61586269db9d2 --- /dev/null +++ b/components/microsoft_365_people/README.md @@ -0,0 +1,11 @@ +# Overview + +The Microsoft 365 People API lets you tap into your organization's contact data, providing access to people's info within your Microsoft 365 domain. With this API, you can search for users and contacts, discover relationships, and construct rich profiles that include contact information, organizational hierarchies, and photo URLs. On Pipedream, you can harness this API to automate contact management tasks, enhance CRM data, or trigger actions based on changes in your organization's personnel. + +# Example Use Cases + +- **Sync Contacts to CRM**: Automate the transfer of contacts from Microsoft 365 People to your CRM platform like Salesforce. Whenever new contacts are added or existing ones are updated in Microsoft 365, trigger a workflow to update the records in Salesforce, keeping all customer data in sync. + +- **Onboarding Workflow Automation**: Streamline the onboarding process for HR. When a new employee is added to Microsoft 365, trigger a Pipedream workflow that creates accounts for them in other company tools such as Slack, assigns tasks in project management tools like Trello, and sends a welcome email with all necessary info. + +- **Organization Chart Updates**: Maintain an up-to-date organizational chart. Use the Microsoft 365 People API to detect changes in employee roles or departments and trigger a workflow that updates a visual org chart tool like Lucidchart. Notify team leaders and employees about these updates through channels such as email or team messaging apps. diff --git a/components/microsoft_365_planner/README.md b/components/microsoft_365_planner/README.md new file mode 100644 index 0000000000000..6b30c0fef3129 --- /dev/null +++ b/components/microsoft_365_planner/README.md @@ -0,0 +1,11 @@ +# Overview + +The Microsoft 365 Planner API lets you tap into the robust task management capabilities of Planner within the Microsoft 365 suite. With this API on Pipedream, you can automate tasks, sync Planner data with other services, manage plans, tasks, and buckets programmatically. This opens up possibilities for creating custom workflows to enhance productivity, streamline project tracking, and maintain synchronization across various platforms. + +# Example Use Cases + +- **Task Synchronization Workflow**: Automatically sync new or updated Planner tasks to a different task management tool like Trello or Asana. Whenever a task is created or modified in Planner, Pipedream triggers a workflow that updates or creates a corresponding card/task in the external tool, ensuring alignment across your task management ecosystems. + +- **Meeting Prep Workflow**: Prior to a scheduled meeting, compile a list of incomplete tasks from Planner and send it via email using the Microsoft 365 Outlook service. This workflow helps participants stay informed about pending items and encourages accountability and preparation before meetings. + +- **Resource Management Workflow**: When a new task is assigned to a team member in Planner, trigger a workflow that logs this assignment in a Google Sheets spreadsheet. An additional step could involve checking the team member's current task load, and if it exceeds a certain threshold, send a notification via Slack to the project manager for workload balancing. diff --git a/components/microsoft_365_planner/actions/create-bucket/create-bucket.mjs b/components/microsoft_365_planner/actions/create-bucket/create-bucket.mjs index 92a7e4a08825c..015af17e48be5 100644 --- a/components/microsoft_365_planner/actions/create-bucket/create-bucket.mjs +++ b/components/microsoft_365_planner/actions/create-bucket/create-bucket.mjs @@ -4,7 +4,7 @@ export default { key: "microsoft_365_planner-create-bucket", name: "Create Bucket", description: "Create a new bucket in Microsoft 365 Planner. [See the documentation](https://learn.microsoft.com/en-us/graph/api/planner-post-buckets)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { microsoft365Planner, diff --git a/components/microsoft_365_planner/actions/create-plan/create-plan.mjs b/components/microsoft_365_planner/actions/create-plan/create-plan.mjs index d701d751d6138..3cc81953e63b5 100644 --- a/components/microsoft_365_planner/actions/create-plan/create-plan.mjs +++ b/components/microsoft_365_planner/actions/create-plan/create-plan.mjs @@ -4,7 +4,7 @@ export default { key: "microsoft_365_planner-create-plan", name: "Create Plan", description: "Create a new plan in Microsoft 365 Planner. [See the documentation](https://learn.microsoft.com/en-us/graph/api/planner-post-plans)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { microsoft365Planner, diff --git a/components/microsoft_365_planner/actions/create-task/create-task.mjs b/components/microsoft_365_planner/actions/create-task/create-task.mjs index 5efa9b6f1c531..84bc40bc065cf 100644 --- a/components/microsoft_365_planner/actions/create-task/create-task.mjs +++ b/components/microsoft_365_planner/actions/create-task/create-task.mjs @@ -4,7 +4,7 @@ export default { key: "microsoft_365_planner-create-task", name: "Create Task", description: "Create a new task in Microsoft 365 Planner. [See the documentation](https://learn.microsoft.com/en-us/graph/api/planner-post-tasks)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { microsoft365Planner, diff --git a/components/microsoft_365_planner/actions/list-user-tasks/list-user-tasks.mjs b/components/microsoft_365_planner/actions/list-user-tasks/list-user-tasks.mjs new file mode 100644 index 0000000000000..f1a583f975e3a --- /dev/null +++ b/components/microsoft_365_planner/actions/list-user-tasks/list-user-tasks.mjs @@ -0,0 +1,21 @@ +import app from "../../microsoft_365_planner.app.mjs"; + +export default { + key: "microsoft_365_planner-list-user-tasks", + name: "List User Tasks", + description: "List all user tasks in Microsoft 365 Planner. [See the documentation](https://learn.microsoft.com/en-us/graph/api/planneruser-list-tasks?view=graph-rest-1.0&tabs=http)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.listUserTasks({ + $, + }); + + $.export("$summary", `Successfully retrieved \`${response?.value?.length}\` task(s).`); + + return response; + }, +}; diff --git a/components/microsoft_365_planner/actions/update-task/update-task.mjs b/components/microsoft_365_planner/actions/update-task/update-task.mjs new file mode 100644 index 0000000000000..f4fcf2940dc2f --- /dev/null +++ b/components/microsoft_365_planner/actions/update-task/update-task.mjs @@ -0,0 +1,192 @@ +import app from "../../microsoft_365_planner.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "microsoft_365_planner-update-task", + name: "Update Task", + description: "Updates a task in Microsoft 365 Planner. [See the documentation](https://learn.microsoft.com/en-us/graph/api/plannertask-update?view=graph-rest-1.0&tabs=http)", + version: "0.0.1", + type: "action", + props: { + app, + taskId: { + propDefinition: [ + app, + "userTaskId", + ], + }, + title: { + type: "string", + label: "Title", + description: "Title of the task", + optional: true, + }, + priority: { + type: "integer", + label: "Priority", + description: "Priority of the task. The valid range of values is between `0` and `10`, with the increasing value being lower priority (`0` has the highest priority and `10` has the lowest priority)", + optional: true, + min: 0, + max: 10, + }, + percentComplete: { + type: "integer", + label: "Percent Complete", + description: "Percentage of task completion. When set to `100`, the task is considered completed.", + optional: true, + }, + dueDateTime: { + type: "string", + label: "Due Date Time", + description: "Date and time at which the task is due. The Timestamp type represents date and time information using ISO 8601 format and is always in UTC time. For example, midnight UTC on Jan 1, 2014 is `2014-01-01T00:00:00Z`.", + optional: true, + }, + startDateTime: { + type: "string", + label: "Start Date Time", + description: "Date and time at which the task starts. The Timestamp type represents date and time information using ISO 8601 format and is always in UTC time. For example, midnight UTC on Jan 1, 2014 is `2014-01-01T00:00:00Z`.", + optional: true, + }, + orderHint: { + type: "string", + label: "Order Hint", + description: "Hint used to order items of this type in a list view. The format is defined in [Using order hints in Planner](https://learn.microsoft.com/en-us/graph/api/resources/planner-order-hint-format?view=graph-rest-1.0).", + optional: true, + }, + assigneePriority: { + type: "string", + label: "Assignee Priority", + description: "Hint used to order items of this type in a list view. The format is defined as outlined [here](https://learn.microsoft.com/en-us/graph/api/resources/planner-order-hint-format?view=graph-rest-1.0).", + optional: true, + }, + groupId: { + optional: true, + propDefinition: [ + app, + "groupId", + ], + }, + conversationThreadId: { + propDefinition: [ + app, + "conversationThreadId", + ({ groupId }) => ({ + groupId, + }), + ], + }, + assignmentIds: { + propDefinition: [ + app, + "assigneeIds", + ({ groupId }) => ({ + groupId, + }), + ], + }, + planId: { + optional: true, + propDefinition: [ + app, + "planId", + ({ groupId }) => ({ + groupId, + }), + ], + }, + bucketId: { + propDefinition: [ + app, + "bucketId", + ({ planId }) => ({ + planId, + }), + ], + }, + appliedCategories: { + type: "string[]", + label: "Applied Categories", + description: "The categories to which the task has been applied. See [applied Categories](https://learn.microsoft.com/en-us/graph/api/resources/plannerappliedcategories?view=graph-rest-1.0) for possible values.", + optional: true, + options: Array.from({ + length: 6, + }, (_, idx) => `category${idx + 1}`), + }, + }, + methods: { + getAppliedCategories(appliedCategories = []) { + return utils.parseArray(appliedCategories)?.reduce((acc, category) => ({ + ...acc, + [category]: true, + }), {}); + }, + getAssignments(assignmentIds = []) { + return utils.parseArray(assignmentIds)?.reduce((acc, id) => ({ + ...acc, + [id]: { + "@odata.type": "microsoft.graph.plannerAssignment", + "orderHint": " !", + }, + }), {}); + }, + updateTask({ + taskId, ...args + } = {}) { + return this.app._makeRequest({ + method: "PATCH", + path: `/planner/tasks/${taskId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + app, + getAssignments, + getAppliedCategories, + updateTask, + taskId, + title, + priority, + percentComplete, + startDateTime, + dueDateTime, + assigneePriority, + conversationThreadId, + assignmentIds, + bucketId, + appliedCategories, + } = this; + + const { ["@odata.etag"]: etag } = await app.getTask({ + $, + taskId, + }); + + const response = await updateTask({ + $, + taskId, + headers: { + "Content-Type": "application/json", + "If-Match": etag, + "Prefer": "return=representation", + }, + data: { + title, + priority, + percentComplete, + startDateTime, + dueDateTime, + assigneePriority, + conversationThreadId, + bucketId, + assignments: getAssignments(assignmentIds), + appliedCategories: getAppliedCategories(appliedCategories), + }, + }); + + $.export("$summary", `Successfully updated task with ID \`${response.id}\`.`); + + return response; + }, +}; diff --git a/components/microsoft_365_planner/common/utils.mjs b/components/microsoft_365_planner/common/utils.mjs new file mode 100644 index 0000000000000..d9df0aa935aef --- /dev/null +++ b/components/microsoft_365_planner/common/utils.mjs @@ -0,0 +1,28 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +export default { + parseArray, +}; diff --git a/components/microsoft_365_planner/microsoft_365_planner.app.mjs b/components/microsoft_365_planner/microsoft_365_planner.app.mjs index 18339bff36a62..36bc46e1a2ba9 100644 --- a/components/microsoft_365_planner/microsoft_365_planner.app.mjs +++ b/components/microsoft_365_planner/microsoft_365_planner.app.mjs @@ -156,25 +156,57 @@ export default { }; }, }, + userTaskId: { + type: "string", + label: "User Task ID", + description: "Identifier of a user task", + async options() { + const response = await this.listUserTasks(); + return response.value?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })); + }, + }, + conversationThreadId: { + type: "string", + label: "Conversation Thread ID", + description: "Identifier of the conversation thread associated with the task.", + optional: true, + async options({ groupId }) { + if (!groupId) { + return []; + } + const response = await this.listThreads({ + groupId, + }); + return response.value?.map(({ + id: value, topic: label, + }) => ({ + value, + label, + })); + }, + }, }, methods: { _baseUrl() { return "https://graph.microsoft.com/v1.0"; }, - _headers() { + _headers(headers) { return { + ...headers, Authorization: `Bearer ${this.$auth.oauth_access_token}`, }; }, _makeRequest({ - $ = this, - path, - url, - ...args + $ = this, path, url, headers, ...args }) { return axios($, { url: url || `${this._baseUrl()}${path}`, - headers: this._headers(), + headers: this._headers(headers), ...args, }); }, @@ -237,6 +269,28 @@ export default { ...args, }); }, + getTask({ + taskId, ...args + }) { + return this._makeRequest({ + path: `/planner/tasks/${taskId}`, + ...args, + }); + }, + listUserTasks(args = {}) { + return this._makeRequest({ + path: "/me/planner/tasks", + ...args, + }); + }, + listThreads({ + groupId, ...args + }) { + return this._makeRequest({ + path: `/groups/${groupId}/threads`, + ...args, + }); + }, async *paginate({ fn, args, }) { diff --git a/components/microsoft_365_planner/package.json b/components/microsoft_365_planner/package.json index 696e6a90b9e13..0d4ebb4e144f2 100644 --- a/components/microsoft_365_planner/package.json +++ b/components/microsoft_365_planner/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/microsoft_365_planner", - "version": "0.1.0", + "version": "0.2.0", "description": "Pipedream Microsoft 365 Planner Components", "main": "microsoft_365_planner.app.mjs", "keywords": [ @@ -13,6 +13,6 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.5.1" + "@pipedream/platform": "^1.6.5" } } diff --git a/components/microsoft_365_planner/sources/new-plan-created/new-plan-created.mjs b/components/microsoft_365_planner/sources/new-plan-created/new-plan-created.mjs index 8b31b449d0e9b..87695e7d73751 100644 --- a/components/microsoft_365_planner/sources/new-plan-created/new-plan-created.mjs +++ b/components/microsoft_365_planner/sources/new-plan-created/new-plan-created.mjs @@ -5,7 +5,7 @@ export default { key: "microsoft_365_planner-new-plan-created", name: "New Plan Created", description: "Emit new event when a new Plan is created in Microsoft 365 Planner", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "unique", props: { diff --git a/components/microsoft_365_planner/sources/new-task-created/new-task-created.mjs b/components/microsoft_365_planner/sources/new-task-created/new-task-created.mjs index 43ad5b3cf5cf8..c6cc9cb743802 100644 --- a/components/microsoft_365_planner/sources/new-task-created/new-task-created.mjs +++ b/components/microsoft_365_planner/sources/new-task-created/new-task-created.mjs @@ -5,7 +5,7 @@ export default { key: "microsoft_365_planner-new-task-created", name: "New Task Created", description: "Emit new event when a new Task is created in Microsoft 365 Planner", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "unique", props: { diff --git a/components/microsoft_advertising/README.md b/components/microsoft_advertising/README.md new file mode 100644 index 0000000000000..bb45a1c874364 --- /dev/null +++ b/components/microsoft_advertising/README.md @@ -0,0 +1,11 @@ +# Overview + +The Microsoft Advertising API allows you to programmatically manage your advertising campaigns. Using this API on Pipedream, you can create, read, update, and delete campaigns, ad groups, and ads. You can also retrieve reports to analyze performance. Pipedream's serverless platform lets you connect the Microsoft Advertising API to a plethora of other services to automate workflows, sync data across platforms, and react to events in real time. + +# Example Use Cases + +- **Campaign Performance Reporting**: Generate daily or weekly reports on campaign performance and send them via email or to a Google Sheets document. You can schedule a Pipedream workflow to pull the data from Microsoft Advertising, format it, and push it to your preferred reporting medium. + +- **Keyword Bid Adjustment**: Monitor your ad campaign performance data and adjust keyword bids based on predefined rules. A Pipedream workflow can analyze your campaign metrics, decide if a bid needs to be increased or decreased, and make the adjustment automatically through the API. + +- **Lead Synchronization with CRM**: When a user submits a lead form from a Microsoft ad, you can instantly sync this lead information into a CRM like Salesforce or HubSpot. Pipedream can capture the lead data and create or update the lead records in your CRM, ensuring your sales team has immediate access. diff --git a/components/microsoft_advertising/microsoft_advertising.app.mjs b/components/microsoft_advertising/microsoft_advertising.app.mjs new file mode 100644 index 0000000000000..67d49515b686c --- /dev/null +++ b/components/microsoft_advertising/microsoft_advertising.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "microsoft_advertising", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/microsoft_advertising/package.json b/components/microsoft_advertising/package.json new file mode 100644 index 0000000000000..c0c284169a677 --- /dev/null +++ b/components/microsoft_advertising/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/microsoft_advertising", + "version": "0.0.1", + "description": "Pipedream Microsoft Advertising Components", + "main": "microsoft_advertising.app.mjs", + "keywords": [ + "pipedream", + "microsoft_advertising" + ], + "homepage": "https://pipedream.com/apps/microsoft_advertising", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/microsoft_azure_ai_translator/actions/break-sentence/break-sentence.mjs b/components/microsoft_azure_ai_translator/actions/break-sentence/break-sentence.mjs new file mode 100644 index 0000000000000..e46abc8a76081 --- /dev/null +++ b/components/microsoft_azure_ai_translator/actions/break-sentence/break-sentence.mjs @@ -0,0 +1,31 @@ +import app from "../../microsoft_azure_ai_translator.app.mjs"; + +export default { + key: "microsoft_azure_ai_translator-break-sentence", + name: "Break Sentence", + description: "Identifies the positioning of sentence boundaries in a piece of text. [See the documentation](https://learn.microsoft.com/en-us/azure/ai-services/translator/reference/v3-0-break-sentence)", + version: "0.0.1", + type: "action", + props: { + app, + text: { + propDefinition: [ + app, + "text", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.breakSentence({ + $, + data: [ + { + text: this.text, + }, + ], + }); + $.export("$summary", "Successfully identified the number and length of the provided sentences "); + return response; + }, +}; diff --git a/components/microsoft_azure_ai_translator/actions/detect-language/detect-language.mjs b/components/microsoft_azure_ai_translator/actions/detect-language/detect-language.mjs new file mode 100644 index 0000000000000..6a1b9c478b656 --- /dev/null +++ b/components/microsoft_azure_ai_translator/actions/detect-language/detect-language.mjs @@ -0,0 +1,31 @@ +import app from "../../microsoft_azure_ai_translator.app.mjs"; + +export default { + key: "microsoft_azure_ai_translator-detect-language", + name: "Detect Language", + description: "Identifies the language of a piece of text. [See the documentation](https://learn.microsoft.com/en-us/azure/ai-services/translator/reference/v3-0-detect)", + version: "0.0.1", + type: "action", + props: { + app, + text: { + propDefinition: [ + app, + "text", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.detectLanguage({ + $, + data: [ + { + text: this.text, + }, + ], + }); + $.export("$summary", `Successfully detected language of the provided text: '${response.language}'s`); + return response; + }, +}; diff --git a/components/microsoft_azure_ai_translator/actions/translate-text/translate-text.mjs b/components/microsoft_azure_ai_translator/actions/translate-text/translate-text.mjs new file mode 100644 index 0000000000000..80c3710f79d6c --- /dev/null +++ b/components/microsoft_azure_ai_translator/actions/translate-text/translate-text.mjs @@ -0,0 +1,61 @@ +import app from "../../microsoft_azure_ai_translator.app.mjs"; + +export default { + key: "microsoft_azure_ai_translator-translate-text", + name: "Translate Text", + description: "Translate text into the specified language. [See the documentation](https://learn.microsoft.com/en-us/azure/ai-services/translator/reference/v3-0-translate)", + version: "0.0.1", + type: "action", + props: { + app, + text: { + propDefinition: [ + app, + "text", + ], + }, + to: { + propDefinition: [ + app, + "to", + ], + }, + from: { + propDefinition: [ + app, + "from", + ], + }, + profanityAction: { + propDefinition: [ + app, + "profanityAction", + ], + }, + includeAlignment: { + propDefinition: [ + app, + "includeAlignment", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.translateText({ + $, + data: [ + { + text: this.text, + }, + ], + params: { + from: this.from, + to: this.to, + profanityAction: this.profanityAction, + includeAlignment: this.includeAlignment, + }, + }); + $.export("$summary", "Successfully translated the provided text"); + return response; + }, +}; diff --git a/components/microsoft_azure_ai_translator/common/constants.mjs b/components/microsoft_azure_ai_translator/common/constants.mjs new file mode 100644 index 0000000000000..0a74997f28a9b --- /dev/null +++ b/components/microsoft_azure_ai_translator/common/constants.mjs @@ -0,0 +1,7 @@ +export default { + PROFANITY_ACTIONS: [ + "NoAction", + "Marked", + "Deleted", + ], +}; diff --git a/components/microsoft_azure_ai_translator/microsoft_azure_ai_translator.app.mjs b/components/microsoft_azure_ai_translator/microsoft_azure_ai_translator.app.mjs new file mode 100644 index 0000000000000..c118a2529da70 --- /dev/null +++ b/components/microsoft_azure_ai_translator/microsoft_azure_ai_translator.app.mjs @@ -0,0 +1,112 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "microsoft_azure_ai_translator", + propDefinitions: { + text: { + type: "string", + label: "Text", + description: "String that will be sent to the API", + }, + to: { + type: "string", + label: "Output Language", + description: "Language of the output text", + async options() { + const response = await this.getLanguages(); + return Object.entries(response.translation).map(([ + key, + { name }, + ]) => ({ + label: name, + value: key, + })); + }, + }, + from: { + type: "string", + label: "Input Language", + description: "Language of the input text", + optional: true, + async options() { + const response = await this.getLanguages(); + return Object.entries(response.translation).map(([ + key, + { name }, + ]) => ({ + label: name, + value: key, + })); + }, + }, + profanityAction: { + type: "string", + label: "Profanity Action", + description: "Specifies how profanities should be treated", + options: constants.PROFANITY_ACTIONS, + }, + includeAlignment: { + type: "boolean", + label: "Include Alignment", + description: "Specifies whether to include alignment projection from source text to translated text", + optional: true, + }, + }, + methods: { + _baseUrl() { + return `${this.$auth.endpoint}`; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "Ocp-Apim-Subscription-Key": `${this.$auth.api_key}`, + "Ocp-Apim-Subscription-Region": `${this.$auth.location}`, + "Content-Type": "application/json", + }, + params: { + ...params, + "api-version": "3.0", + }, + }); + }, + async translateText(args = {}) { + return this._makeRequest({ + path: "/translate", + method: "post", + ...args, + }); + }, + async breakSentence(args = {}) { + return this._makeRequest({ + path: "/breaksentence", + method: "post", + ...args, + }); + }, + async detectLanguage(args = {}) { + return this._makeRequest({ + path: "/detect", + method: "post", + ...args, + }); + }, + async getLanguages(args = {}) { + return this._makeRequest({ + path: "/languages", + ...args, + }); + }, + }, +}; diff --git a/components/microsoft_azure_ai_translator/package.json b/components/microsoft_azure_ai_translator/package.json new file mode 100644 index 0000000000000..f0f23b7864329 --- /dev/null +++ b/components/microsoft_azure_ai_translator/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/microsoft_azure_ai_translator", + "version": "0.1.0", + "description": "Pipedream Microsoft Azure AI Translator Components", + "main": "microsoft_azure_ai_translator.app.mjs", + "keywords": [ + "pipedream", + "microsoft_azure_ai_translator" + ], + "homepage": "https://pipedream.com/apps/microsoft_azure_ai_translator", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/microsoft_dynamics_365_sales/actions/create-custom-entity/create-custom-entity.mjs b/components/microsoft_dynamics_365_sales/actions/create-custom-entity/create-custom-entity.mjs new file mode 100644 index 0000000000000..9763a7c75b58e --- /dev/null +++ b/components/microsoft_dynamics_365_sales/actions/create-custom-entity/create-custom-entity.mjs @@ -0,0 +1,157 @@ +import microsoft from "../../microsoft_dynamics_365_sales.app.mjs"; +import languageCodes from "../../common/language-codes.mjs"; +import pluralize from "pluralize"; + +export default { + key: "microsoft_dynamics_365_sales-create-custom-entity", + name: "Create Custom Entity", + description: "Create a custom entity. [See the documentation](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/create-update-entity-definitions-using-web-api)", + version: "0.0.1", + type: "action", + props: { + microsoft, + solutionId: { + propDefinition: [ + microsoft, + "solutionId", + ], + }, + displayName: { + type: "string", + label: "Display Name", + description: "The name of the new entity. E.g. `Bank Account`", + }, + primaryAttribute: { + type: "string", + label: "Primary Attribute", + description: "The primary name attribute of the new entity. E.g. `Account Name`", + }, + languageCode: { + type: "integer", + label: "Language Code", + description: "The language code to use for the entity", + options: languageCodes, + default: 1033, + optional: true, + }, + additionalAttributes: { + type: "object", + label: "Additional Attributes", + description: "An array of objects representing attributes to add to the custom entity. [See the documentation](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/create-update-entity-definitions-using-web-api) for more information about formatting attribute objects", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "A description of the new entity", + optional: true, + }, + hasActivities: { + type: "boolean", + label: "Has Activities", + description: "Set to `true` if the new entity has activities", + default: false, + optional: true, + }, + hasNotes: { + type: "boolean", + label: "Has Notes", + description: "Set to `true` if the new entity has notes", + default: false, + optional: true, + }, + }, + methods: { + parseAttributes() { + return this.additionalAttributes + ? typeof this.additionalAttributes === "string" + ? JSON.parse(this.additionalAttributes) + : this.additionalAttributes + : []; + }, + removeSpaces(str) { + return str.replace(/\s+/g, ""); + }, + buildLocalizedLabelArray(label) { + return [ + { + "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel", + "Label": label, + "LanguageCode": this.languageCode, + }, + ]; + }, + }, + async run({ $ }) { + const solution = await this.microsoft.getSolution({ + $, + solutionId: this.solutionId, + }); + + const { customizationprefix } = await this.microsoft.getPublisher({ + $, + publisherId: solution._publisherid_value, + }); + + const attributes = this.parseAttributes(); + + const { headers } = await this.microsoft.createCustomEntity({ + $, + returnFullResponse: true, + headers: { + "MSCRM.SolutionUniqueName": solution.uniquename, + }, + data: { + "@odata.type": "Microsoft.Dynamics.CRM.EntityMetadata", + "Attributes": [ + { + "@odata.type": "Microsoft.Dynamics.CRM.StringAttributeMetadata", + "DisplayName": { + "@odata.type": "Microsoft.Dynamics.CRM.Label", + "LocalizedLabels": this.buildLocalizedLabelArray(this.primaryAttribute), + }, + "IsPrimaryName": true, + "SchemaName": `${customizationprefix}_${this.removeSpaces(this.primaryAttribute)}`, + "MaxLength": 100, + "FormatName": { + "Value": "Text", + }, + "RequiredLevel": { + "Value": "None", + "CanBeChanged": true, + "ManagedPropertyLogicalName": "canmodifyrequirementlevelsettings", + }, + }, + ...attributes, + ], + "DisplayName": { + "@odata.type": "Microsoft.Dynamics.CRM.Label", + "LocalizedLabels": this.buildLocalizedLabelArray(this.displayName), + }, + "DisplayCollectionName": { + "@odata.type": "Microsoft.Dynamics.CRM.Label", + "LocalizedLabels": this.buildLocalizedLabelArray(pluralize(this.displayName)), + }, + "Description": this.description && { + "@odata.type": "Microsoft.Dynamics.CRM.Label", + "LocalizedLabels": this.buildLocalizedLabelArray(this.description), + }, + "HasActivities": this.hasActivities, + "HasNotes": this.hasNotes, + "SchemaName": `${customizationprefix}_${this.removeSpaces(this.displayName)}`, + "PrimaryNameAttribute": `${customizationprefix}_${this.removeSpaces(this.primaryAttribute)}`, + "OwnershipType": "UserOwned", + }, + }); + + const entityId = headers["odata-entityid"].substring(headers["odata-entityid"].lastIndexOf("/") + 1); + const response = await this.microsoft.getEntity({ + $, + entityId, + }); + + $.export("$summary", `Successfully created custom entity with ID: ${entityId}`); + + return response; + }, +}; diff --git a/components/microsoft_dynamics_365_sales/actions/find-contact/find-contact.mjs b/components/microsoft_dynamics_365_sales/actions/find-contact/find-contact.mjs new file mode 100644 index 0000000000000..a146ac59ea7cf --- /dev/null +++ b/components/microsoft_dynamics_365_sales/actions/find-contact/find-contact.mjs @@ -0,0 +1,59 @@ +import microsoft from "../../microsoft_dynamics_365_sales.app.mjs"; + +export default { + key: "microsoft_dynamics_365_sales-find-contact", + name: "Find Contact", + description: "Search for a contact by id, name, or using a custom filter. [See the documentation](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/query/overview)", + version: "0.0.1", + type: "action", + props: { + microsoft, + contactId: { + propDefinition: [ + microsoft, + "contactId", + ], + }, + name: { + type: "string", + label: "Name", + description: "Find contacts whose full name contains the name entered", + optional: true, + }, + filter: { + type: "string", + label: "Filter", + description: "Enter a custom filter to search contacts. E.g. `lastname eq 'Smith'`. [See the documentation] for more information about [filters](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/query/filter-rows)", + optional: true, + }, + }, + async run({ $ }) { + const filterArray = []; + if (this.contactId) { + filterArray.push(`contactid eq '${this.contactId}'`); + } + if (this.name) { + filterArray.push(`contains(fullname, '${this.name}')`); + } + if (this.filter) { + filterArray.push(`(${this.filter})`); + } + + const filter = filterArray.length + ? filterArray.join(" and ") + : undefined; + + const { value } = await this.microsoft.listContacts({ + $, + params: { + $filter: filter, + }, + }); + + $.export("$summary", `Successfully retrieved ${value.length} contact${value.length === 1 + ? "" + : "s"}`); + + return value; + }, +}; diff --git a/components/microsoft_dynamics_365_sales/common/language-codes.mjs b/components/microsoft_dynamics_365_sales/common/language-codes.mjs new file mode 100644 index 0000000000000..5d7674fb60fba --- /dev/null +++ b/components/microsoft_dynamics_365_sales/common/language-codes.mjs @@ -0,0 +1,434 @@ +export default [ + { + label: "Afrikaans - South Africa", + value: 1078, + }, + { + label: "Albanian - Albania", + value: 1052, + }, + { + label: "Arabic - Algeria", + value: 5121, + }, + { + label: "Arabic - Bahrain", + value: 15361, + }, + { + label: "Arabic - Egypt", + value: 3073, + }, + { + label: "Arabic - Iraq", + value: 2049, + }, + { + label: "Arabic - Jordan", + value: 11265, + }, + { + label: "Arabic - Kuwait", + value: 13313, + }, + { + label: "Arabic - Lebanon", + value: 12289, + }, + { + label: "Arabic - Libya", + value: 4097, + }, + { + label: "Arabic - Morocco", + value: 6145, + }, + { + label: "Arabic - Oman", + value: 8193, + }, + { + label: "Arabic - Qatar", + value: 16385, + }, + { + label: "Arabic - Saudi Arabia", + value: 1025, + }, + { + label: "Arabic - Syria", + value: 10241, + }, + { + label: "Arabic - Tunisia", + value: 7169, + }, + { + label: "Arabic - U.A.E.", + value: 14337, + }, + { + label: "Arabic - Yemen", + value: 9217, + }, + { + label: "Armenian - Armenia", + value: 1067, + }, + { + label: "Azeri (Cyrillic) - Azerbaijan", + value: 2092, + }, + { + label: "Azeri (Latin) - Azerbaijan", + value: 1068, + }, + { + label: "Basque - Spain", + value: 1069, + }, + { + label: "Belarusian - Belarus", + value: 1059, + }, + { + label: "Bulgarian - Bulgaria", + value: 1026, + }, + { + label: "Catalan - Spain", + value: 1027, + }, + { + label: "Chinese - Hong Kong S.A.R.", + value: 3076, + }, + { + label: "Chinese - Macau S.A.R.", + value: 5124, + }, + { + label: "Chinese - People's Republic of China", + value: 2052, + }, + { + label: "Chinese - Singapore", + value: 4100, + }, + { + label: "Chinese - Taiwan", + value: 1028, + }, + { + label: "Croatian - Croatia", + value: 1050, + }, + { + label: "Czech - Czech Republic", + value: 1029, + }, + { + label: "Danish - Denmark", + value: 1030, + }, + { + label: "Divehi - Maldives", + value: 1125, + }, + { + label: "Dutch - Belgium", + value: 2067, + }, + { + label: "Dutch - Netherlands", + value: 1043, + }, + { + label: "English - Australia", + value: 3081, + }, + { + label: "English - Belize", + value: 10249, + }, + { + label: "English - Canada", + value: 4105, + }, + { + label: "English - Caribbean", + value: 9225, + }, + { + label: "English - Ireland", + value: 6153, + }, + { + label: "English - Jamaica", + value: 8201, + }, + { + label: "English - New Zealand", + value: 5129, + }, + { + label: "English - Republic of the Philippines", + value: 13321, + }, + { + label: "English - South Africa", + value: 7177, + }, + { + label: "English - Trinidad and Tobago", + value: 11273, + }, + { + label: "English - United Kingdom", + value: 2057, + }, + { + label: "English - United States", + value: 1033, + }, + { + label: "English - Zimbabwe", + value: 12297, + }, + { + label: "Estonian - Estonia", + value: 1061, + }, + { + label: "Faroese - Faeroe Islands", + value: 1080, + }, + { + label: "Farsi - Iran", + value: 1065, + }, + { + label: "Finnish - Finland", + value: 1035, + }, + { + label: "French - Belgium", + value: 2060, + }, + { + label: "French - Canada", + value: 3084, + }, + { + label: "French - France", + value: 1036, + }, + { + label: "French - Luxembourg", + value: 5132, + }, + { + label: "French - Monaco", + value: 6156, + }, + { + label: "French - Switzerland", + value: 4108, + }, + { + label: "Macedonian - North Macedonia", + value: 1071, + }, + { + label: "Galician - Spain", + value: 1110, + }, + { + label: "Georgian - Georgia", + value: 1079, + }, + { + label: "German - Austria", + value: 3079, + }, + { + label: "German - Germany", + value: 1031, + }, + { + label: "German - Liechtenstein", + value: 5127, + }, + { + label: "German - Luxembourg", + value: 4103, + }, + { + label: "German - Switzerland", + value: 2055, + }, + { + label: "Greek - Greece", + value: 1032, + }, + { + label: "Gujarati - India", + value: 1095, + }, + { + label: "Hebrew - Israel", + value: 1037, + }, + { + label: "Hindi - India", + value: 1081, + }, + { + label: "Hungarian - Hungary", + value: 1038, + }, + { + label: "Icelandic - Iceland", + value: 1039, + }, + { + label: "Indonesian - Indonesia", + value: 1057, + }, + { + label: "Italian - Italy", + value: 1040, + }, + { + label: "Italian - Switzerland", + value: 2064, + }, + { + label: "Japanese - Japan", + value: 1041, + }, + { + label: "Kannada - India", + value: 1099, + }, + { + label: "Kazakh - Kazakhstan", + value: 1087, + }, + { + label: "Korean - Korea", + value: 1042, + }, + { + label: "Latvian - Latvia", + value: 1062, + }, + { + label: "Lithuanian - Lithuania", + value: 1063, + }, + { + label: "Malay - Brunei Darussalam", + value: 2110, + }, + { + label: "Malay - Malaysia", + value: 1086, + }, + { + label: "Marathi - India", + value: 1102, + }, + { + label: "Mongolian - Mongolia", + value: 1104, + }, + { + label: "Norwegian (Bokmål) - Norway", + value: 1044, + }, + { + label: "Norwegian (Nynorsk) - Norway", + value: 2068, + }, + { + label: "Polish - Poland", + value: 1045, + }, + { + label: "Portuguese - Brazil", + value: 1046, + }, + { + label: "Portuguese - Portugal", + value: 2070, + }, + { + label: "Punjabi - India", + value: 1094, + }, + { + label: "Romanian - Romania", + value: 1048, + }, + { + label: "Russian - Russia", + value: 1049, + }, + { + label: "Spanish - Spain", + value: 1034, + }, + { + label: "Spanish - Mexico", + value: 2058, + }, + { + label: "Spanish - Argentina", + value: 11274, + }, + { + label: "Spanish - Chile", + value: 13322, + }, + { + label: "Spanish - Colombia", + value: 9226, + }, + { + label: "Swahili - Kenya", + value: 1089, + }, + { + label: "Swedish - Sweden", + value: 1053, + }, + { + label: "Tamil - India", + value: 1097, + }, + { + label: "Thai - Thailand", + value: 1054, + }, + { + label: "Turkish - Turkey", + value: 1055, + }, + { + label: "Ukrainian - Ukraine", + value: 1058, + }, + { + label: "Urdu - Pakistan", + value: 1056, + }, + { + label: "Vietnamese - Vietnam", + value: 1066, + }, + { + label: "Welsh - United Kingdom", + value: 1106, + }, +]; diff --git a/components/microsoft_dynamics_365_sales/microsoft_dynamics_365_sales.app.mjs b/components/microsoft_dynamics_365_sales/microsoft_dynamics_365_sales.app.mjs new file mode 100644 index 0000000000000..92582ba189642 --- /dev/null +++ b/components/microsoft_dynamics_365_sales/microsoft_dynamics_365_sales.app.mjs @@ -0,0 +1,104 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "microsoft_dynamics_365_sales", + propDefinitions: { + contactId: { + type: "string", + label: "Contact ID", + description: "The identifier of a contact", + optional: true, + async options() { + const { value } = await this.listContacts(); + return value?.map(({ + contactid: value, fullname: label, + }) => ({ + value, + label, + })) || []; + }, + }, + solutionId: { + type: "string", + label: "Solution ID", + description: "Identifier of a solution", + async options() { + const { value } = await this.listSolutions(); + return value?.filter(({ isvisible }) => isvisible)?.map(({ + solutionid: value, uniquename, friendlyname, + }) => ({ + value, + label: friendlyname || uniquename, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.org_domain}.crm.dynamics.com/api/data/v9.2`; + }, + _makeRequest({ + $ = this, + path, + headers, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + ...headers, + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "odata-maxversion": "4.0", + "odata-version": "4.0", + "content-type": "application/json", + "If-None-Match": null, + }, + ...opts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts", + ...opts, + }); + }, + getPublisher({ + publisherId, ...opts + }) { + return this._makeRequest({ + path: `/publishers(${publisherId})`, + ...opts, + }); + }, + getSolution({ + solutionId, ...opts + }) { + return this._makeRequest({ + path: `/solutions(${solutionId})`, + ...opts, + }); + }, + listSolutions(opts = {}) { + return this._makeRequest({ + path: "/solutions", + ...opts, + }); + }, + getEntity({ + entityId, ...opts + }) { + return this._makeRequest({ + path: `/${entityId}`, + ...opts, + }); + }, + createCustomEntity(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/EntityDefinitions", + ...opts, + }); + }, + }, +}; diff --git a/components/microsoft_dynamics_365_sales/package.json b/components/microsoft_dynamics_365_sales/package.json new file mode 100644 index 0000000000000..0c3a5b2660126 --- /dev/null +++ b/components/microsoft_dynamics_365_sales/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/microsoft_dynamics_365_sales", + "version": "0.1.0", + "description": "Pipedream Microsoft Dynamics 365 Sales Components", + "main": "microsoft_dynamics_365_sales.app.mjs", + "keywords": [ + "pipedream", + "microsoft_dynamics_365_sales" + ], + "homepage": "https://pipedream.com/apps/microsoft_dynamics_365_sales", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "pluralize": "^8.0.0" + } +} diff --git a/components/microsoft_entra_id/README.md b/components/microsoft_entra_id/README.md new file mode 100644 index 0000000000000..58f979af793bf --- /dev/null +++ b/components/microsoft_entra_id/README.md @@ -0,0 +1,11 @@ +# Overview + +The Microsoft Entra ID API offers a modern identity and access management solution, enabling developers to automate and integrate a broad range of user and identity-related operations. With Pipedream, you can harness this API to create workflows that manage user identities, automate provisioning and deprovisioning, secure access to applications, and more. Pipedream's serverless platform simplifies the process of connecting the Microsoft Entra ID API with hundreds of other apps to build powerful automations. + +# Example Use Cases + +- **Sync New Users to HR Management System**: Create a workflow on Pipedream that triggers when a new user is added in Entra ID, then syncs this user's details to your HR management system, ensuring your employee records are always up-to-date. + +- **Automate User Offboarding**: Set up a Pipedream workflow that listens for deactivation events in Entra ID, triggering an offboarding process that revokes access, archives user data, and notifies relevant departments, all in a coordinated and timely manner. + +- **Periodic Access Review Reminders**: Build a workflow that periodically checks for users with privileged roles in Entra ID and triggers an access review process by sending reminders via email or messaging apps, helping maintain compliance and minimize security risks. diff --git a/components/microsoft_excel/README.md b/components/microsoft_excel/README.md new file mode 100644 index 0000000000000..83365aee8b242 --- /dev/null +++ b/components/microsoft_excel/README.md @@ -0,0 +1,9 @@ +# Overview + +The Microsoft Excel API on Pipedream allows you to automate spreadsheet tasks, manipulate data, and integrate Excel with other apps and services. Through Pipedream, you can create serverless workflows that react to events, perform operations on Excel files like reading, writing, updating cells, and managing worksheets. This API turns Excel into a powerful tool for data analysis, report generation, and task automation without manual intervention. + +# Example Use Cases + +- **Automate Monthly Reporting**: Generate monthly sales reports by pulling latest sales data from a database, formatting it in Excel, and emailing the report using Gmail. +- **Track Social Media Metrics**: Collect social media analytics from platforms like Twitter or Facebook, log the data in Excel, and create a dashboard that updates in real time. +- **Expense Approval Workflow**: When a new expense submission is added to an Excel file on OneDrive, trigger a Pipedream workflow that notifies a manager on Slack for approval. diff --git a/components/microsoft_excel/actions/add-a-worksheet-tablerow/add-a-worksheet-tablerow.mjs b/components/microsoft_excel/actions/add-a-worksheet-tablerow/add-a-worksheet-tablerow.mjs index d51a3cbe7c595..102486632e8ba 100644 --- a/components/microsoft_excel/actions/add-a-worksheet-tablerow/add-a-worksheet-tablerow.mjs +++ b/components/microsoft_excel/actions/add-a-worksheet-tablerow/add-a-worksheet-tablerow.mjs @@ -4,7 +4,7 @@ import microsoftExcel from "../../microsoft_excel.app.mjs"; export default { key: "microsoft_excel-add-a-worksheet-tablerow", name: "Add A Worksheet Tablerow", - version: "0.0.2", + version: "0.0.4", description: "Adds rows to the end of specific table. [See the documentation](https://learn.microsoft.com/en-us/graph/api/tablerowcollection-add?view=graph-rest-1.0&tabs=http)", type: "action", props: { @@ -23,6 +23,7 @@ export default { folderId, }), ], + reloadProps: true, }, tableId: { propDefinition: [ @@ -32,6 +33,13 @@ export default { itemId, }), ], + hidden: true, + }, + tableName: { + type: "string", + label: "Table Name", + description: "This is set in the **Table Design** tab of the ribbon.", + hidden: true, }, values: { propDefinition: [ @@ -40,11 +48,26 @@ export default { ], }, }, + async additionalProps(props) { + if (this.itemId) { + try { + await this.microsoftExcel.listTables({ + itemId: this.itemId, + }); + } catch { + props.tableName.hidden = false; + return {}; + } + props.tableId.hidden = false; + return {}; + } + }, async run({ $ }) { const { microsoftExcel, itemId, tableId, + tableName, values, } = this; @@ -52,6 +75,7 @@ export default { $, itemId, tableId, + tableName, data: { values: parseObject(values), }, diff --git a/components/microsoft_excel/actions/update-worksheet-tablerow/update-worksheet-tablerow.mjs b/components/microsoft_excel/actions/update-worksheet-tablerow/update-worksheet-tablerow.mjs index 0b56aca87471f..91c321da43468 100644 --- a/components/microsoft_excel/actions/update-worksheet-tablerow/update-worksheet-tablerow.mjs +++ b/components/microsoft_excel/actions/update-worksheet-tablerow/update-worksheet-tablerow.mjs @@ -3,7 +3,7 @@ import microsoftExcel from "../../microsoft_excel.app.mjs"; export default { key: "microsoft_excel-update-worksheet-tablerow", name: "Update Worksheet Tablerow", - version: "0.0.2", + version: "0.0.4", description: "Update the properties of tablerow object. `(Only for work or school account)` [See the documentation](https://learn.microsoft.com/en-us/graph/api/tablerow-update?view=graph-rest-1.0&tabs=http)", type: "action", props: { diff --git a/components/microsoft_excel/microsoft_excel.app.mjs b/components/microsoft_excel/microsoft_excel.app.mjs index d1f8359545e20..4b10d83fdcfcc 100644 --- a/components/microsoft_excel/microsoft_excel.app.mjs +++ b/components/microsoft_excel/microsoft_excel.app.mjs @@ -9,7 +9,7 @@ export default { label: "Folder Id", description: "The ID of the folder where the item is located.", async options() { - const folders = await this.listFolders(); + const folders = await this.listFolderOptions(); return [ "root", ...folders, @@ -99,11 +99,11 @@ export default { return axios($, config); }, addRow({ - itemId, tableId, ...args + itemId, tableId, tableName, ...args }) { return this._makeRequest({ method: "POST", - path: `me/drive/items/${itemId}/workbook/tables/${tableId}/rows`, + path: `me/drive/items/${itemId}/workbook/tables/${tableId || tableName}/rows/add`, ...args, }); }, @@ -120,38 +120,6 @@ export default { path: `subscriptions/${hookId}`, }); }, - async listFolders({ - folderId = null, - prefix = "", ...args - } = {}) { - const foldersArray = []; - const { value: items } = await this._makeRequest({ - path: folderId - ? `/me/drive/items/${folderId}/children` - : "me/drive/root/children", - ...args, - }); - - const folders = items.filter((item) => item.folder); - for (const { - id, name, folder: { childCount = null }, - } of folders) { - foldersArray.push({ - value: id, - label: `${prefix}${name}`, - }); - - if (childCount) { - const children = await this.listFolders({ - folderId: id, - prefix: prefix + "-", - }); - foldersArray.push(...children); - } - } - - return foldersArray; - }, listItems({ folderId, ...args }) { @@ -170,6 +138,7 @@ export default { ...args, }); }, + // List tables endpoint is not supported for personal accounts listTables({ itemId, ...args }) { @@ -196,5 +165,87 @@ export default { ...args, }); }, + batch(args = {}) { + return this._makeRequest({ + method: "POST", + path: "$batch", + ...args, + }); + }, + async listFolderOptions({ + folderId = null, prefix = "", batchLimit = 20, ...args + } = {}) { + const options = []; + const stack = [ + { + folderId, + prefix, + }, + ]; + const history = []; + + try { + while (stack.length) { + const currentBatch = []; + while (stack.length && currentBatch.length < batchLimit) { + const { + folderId, + prefix, + } = stack.shift(); + history.push({ + folderId, + prefix, + }); + currentBatch.push({ + id: folderId || "root", + method: "GET", + url: folderId + ? `/me/drive/items/${folderId}/children?$filter=folder ne null` + : "/me/drive/root/children?$filter=folder ne null", + }); + } + + const { responses: batchResponses } = + await this.batch({ + ...args, + data: { + ...args?.data, + requests: currentBatch, + }, + }); + + batchResponses.forEach(({ + id, status, body, + }) => { + if (status === 200) { + body.value.forEach((item) => { + const { + id, name, folder: { childCount }, parentReference: { id: parentId }, + } = item; + const prefix = history.find(({ folderId }) => folderId === parentId)?.prefix || ""; + const currentLabel = `${prefix}${name}`; + options.push({ + value: id, + label: currentLabel, + }); + + if (childCount) { + stack.push({ + folderId: id, + prefix: `${currentLabel}/`, + }); + } + }); + } else { + console.error(`Error in batch request ${id}:`, JSON.stringify(body)); + } + }); + } + } catch (error) { + console.error("Error listing folders:", error); + } + + return options; + }, }, }; diff --git a/components/microsoft_excel/package.json b/components/microsoft_excel/package.json index 037913b3cf3ce..565385837fc08 100644 --- a/components/microsoft_excel/package.json +++ b/components/microsoft_excel/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/microsoft_excel", - "version": "0.1.1", + "version": "0.1.3", "description": "Pipedream Microsoft Excel Components", "main": "microsoft_excel.app.mjs", "keywords": [ @@ -13,7 +13,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.5.1", + "@pipedream/platform": "^3.0.3", "moment": "^2.29.4" } } diff --git a/components/microsoft_excel/sources/new-item-updated/new-item-updated.mjs b/components/microsoft_excel/sources/new-item-updated/new-item-updated.mjs index 11fb38d1410ac..b0560234f0b72 100644 --- a/components/microsoft_excel/sources/new-item-updated/new-item-updated.mjs +++ b/components/microsoft_excel/sources/new-item-updated/new-item-updated.mjs @@ -7,7 +7,7 @@ export default { key: "microsoft_excel-new-item-updated", name: "New Item Updated (Instant)", description: "Emit new event when an item is updated.", - version: "0.0.2", + version: "0.0.4", type: "source", props: { microsoftExcel, diff --git a/components/microsoft_graph_api/README.md b/components/microsoft_graph_api/README.md index ec0d7f10076a5..bedb1c619985f 100644 --- a/components/microsoft_graph_api/README.md +++ b/components/microsoft_graph_api/README.md @@ -1,15 +1,11 @@ # Overview -The Microsoft Graph API is a powerful tool that allows developers to access all -sorts of data from the Microsoft ecosystem. With the Microsoft Graph API, you -can build a variety of different applications and integrations. - -Here are a few examples of what you can build with the Microsoft Graph API: - -- A web application that allows users to login with their Microsoft account and - access their data -- An integration that allows you to send emails through the Microsoft Graph API -- A tool that allows you to analyze your social media activity with Microsoft - Graph API -- A monitoring system that uses the Microsoft Graph API to keep track of your - Azure resources +The Microsoft Graph API taps into the rich data and insights generated by Microsoft 365 services. It enables developers to build powerful workflows integrated with email, calendars, contacts, documents, directory services, and device updates. With Graph API, you can automate user account management, extract insights from Office 365 usage, manage devices, and leverage AI services to mine data from the Microsoft ecosystem. + +# Example Use Cases + +- **Automate Email Campaigns with Microsoft Graph API and SendGrid**: Create a workflow that triggers a personalized email campaign using SendGrid whenever there's a new event on a user's Outlook calendar. This is perfect for follow-up communications after meetings or events. + +- **Sync Microsoft To-Do Tasks with Trello for Project Management**: Develop an integration that automatically syncs tasks added to Microsoft To-Do with a Trello board. This workflow ensures that project tasks are up-to-date across both platforms, aiding in seamless project management and collaboration. + +- **Streamline User Onboarding with Microsoft Graph API and Slack**: Set up a Pipedream workflow that listens for new users added in Azure Active Directory, and then posts a welcome message in a designated Slack channel. Additionally, it can trigger a series of onboarding tasks like provisioning new accounts in other services or enrolling users in training modules. diff --git a/components/microsoft_graph_api/package.json b/components/microsoft_graph_api/package.json new file mode 100644 index 0000000000000..3bb5548951a07 --- /dev/null +++ b/components/microsoft_graph_api/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/microsoft_graph_api", + "version": "0.6.0", + "description": "Pipedream microsoft_graph_api Components", + "main": "microsoft_graph_api.app.mjs", + "keywords": [ + "pipedream", + "microsoft_graph_api" + ], + "homepage": "https://pipedream.com/apps/microsoft_graph_api", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/microsoft_onedrive/README.md b/components/microsoft_onedrive/README.md index d845548dec533..a89f87f70f35f 100644 --- a/components/microsoft_onedrive/README.md +++ b/components/microsoft_onedrive/README.md @@ -1,15 +1,11 @@ # Overview -Microsoft OneDrive API allows developers to access the OneDrive service, which -enables users to store and share their personal data, such as files, photos, -and OneNote notebooks, across all their devices. - -With the Microsoft OneDrive API, developers can build a variety of applications -and services that take advantage of the following features: - -- Access files and folders stored in OneDrive -- Share files and folders with other users -- Synchronize files across devices -- Access OneDrive for Business -- Application Folder Management -- Programmatically manage trash +The Microsoft OneDrive API taps into the robust file storage and management capabilities of OneDrive, allowing for operations like file uploads, retrievals, sharing, and synchronization. Integrating this API into Pipedream workflows lets you automate tasks involving file management, content collaboration, and data backup processes. With OneDrive's API on Pipedream, you can streamline document workflows, trigger actions based on file changes, and connect your file storage to countless other services for enhanced productivity. + +# Example Use Cases + +- **Automated Backup of New Database Entries**: When new data is added to a database (such as Airtable or Google Sheets), trigger a Pipedream workflow that converts this data into a CSV file and automatically uploads it to a specified folder in OneDrive for safekeeping and archiving. + +- **Document Distribution on File Upload**: Upon uploading a new document to a OneDrive folder, use a Pipedream workflow to automatically distribute the file via email (using SendGrid or Gmail) to a predefined list of recipients, ensuring team members receive the latest versions of important documents instantly. + +- **Social Media Photo Sync**: Sync new photos from a social media platform (like Instagram or Twitter) to a OneDrive album using Pipedream. Whenever a new photo is posted to your social media account, the workflow grabs the image and saves it to OneDrive, creating a backup and making it easy to access and share your social media content. diff --git a/components/microsoft_onedrive/actions/create-folder/create-folder.mjs b/components/microsoft_onedrive/actions/create-folder/create-folder.mjs index bd8f1809cb6ad..370e0eb6a5b43 100644 --- a/components/microsoft_onedrive/actions/create-folder/create-folder.mjs +++ b/components/microsoft_onedrive/actions/create-folder/create-folder.mjs @@ -1,15 +1,25 @@ import onedrive from "../../microsoft_onedrive.app.mjs"; -import httpRequest from "../../common/httpRequest.mjs"; import { ConfigurationError } from "@pipedream/platform"; export default { name: "Create Folder", description: "Create a new folder in a drive. [See the documentation](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_post_children?view=odsp-graph-online)", key: "microsoft_onedrive-create-folder", - version: "0.1.0", + version: "0.1.1", type: "action", props: { onedrive, + parentFolderType: { + type: "string", + label: "Parent Folder Type", + description: "Whether to nest the new folder within a folder in your drive (`default`) or a shared folder (`shared`)", + optional: true, + options: [ + "default", + "shared", + ], + reloadProps: true, + }, parentFolderId: { propDefinition: [ onedrive, @@ -17,22 +27,14 @@ export default { ], label: "Parent Folder ID", description: "The ID of the folder which the the new folder should be created. Use the \"Load More\" button to load subfolders.", - optional: true, + hidden: true, }, sharedFolderReference: { - type: "string", - label: "Shared Folder Reference", - description: "The reference of the shared folder which the the new folder should be created.\n\nE.g. `/drives/{driveId}/items/{folderId}/children`", - optional: true, - async options() { - const { value } = await this.httpRequest({ - url: "/sharedWithMe", - }); - return value.map((shared) => ({ - label: shared.name, - value: `/drives/${shared.remoteItem.parentReference.driveId}/items/${shared.remoteItem.id}/children`, - })); - }, + propDefinition: [ + onedrive, + "sharedFolderReference", + ], + hidden: true, }, folderName: { type: "string", @@ -40,32 +42,16 @@ export default { description: "The name of the new folder to be created. e.g. `New Folder`", }, }, - methods: { - httpRequest, - createFolder({ - folderName, parentFolderId, sharedFolderReference, - }) { - let url = "/root/children"; - if (parentFolderId) { - url = `/items/${parentFolderId}/children`; - } - if (sharedFolderReference) { - url = sharedFolderReference; - } - return this.httpRequest({ - url, - useSharedDrive: !!sharedFolderReference, - headers: { - "Content-Type": "application/json", - }, - data: { - name: folderName, - folder: {}, - ["@microsoft.graph.conflictBehavior"]: "rename", - }, - method: "POST", - }); - }, + async additionalProps(props) { + if (this.parentFolderType === "default") { + props.parentFolderId.hidden = false; + props.sharedFolderReference.hidden = true; + } + if (this.parentFolderType === "shared") { + props.sharedFolderReference.hidden = false; + props.parentFolderId.hidden = true; + } + return {}; }, async run({ $ }) { const { @@ -76,7 +62,7 @@ export default { throw new ConfigurationError("You have to select either a `Parent Folder` or a `Shared Folder`."); } - const response = await this.createFolder({ + const response = await this.onedrive.createFolder({ folderName, parentFolderId, sharedFolderReference, diff --git a/components/microsoft_onedrive/actions/create-link/create-link.mjs b/components/microsoft_onedrive/actions/create-link/create-link.mjs index 698abd9d7bdf8..a897d7a4d6cb8 100644 --- a/components/microsoft_onedrive/actions/create-link/create-link.mjs +++ b/components/microsoft_onedrive/actions/create-link/create-link.mjs @@ -1,73 +1,47 @@ -import app from "../../microsoft_onedrive.app.mjs"; +import onedrive from "../../microsoft_onedrive.app.mjs"; +import constants from "../../common/constants.mjs"; export default { name: "Create Link", - version: "0.0.1", + version: "0.0.2", key: "microsoft_onedrive-create-link", type: "action", description: "Create a sharing link for a DriveItem. [See the documentation](https://docs.microsoft.com/en-us/graph/api/driveitem-createlink?view=graph-rest-1.0&tabs=http)", props: { - app, + onedrive, driveItemId: { - type: "string", + propDefinition: [ + onedrive, + "fileId", + () => ({ + excludeFolders: false, + }), + ], label: "Drive Item ID", - description: "The ID of the DriveItem to create a sharing link for.", - async options() { - const { value: driveItems } = await this.listDriveItems(); - return driveItems.map(({ - name: label, id: value, - }) => ({ - label, - value, - })); - }, + description: "The ID of the DriveItem to create a sharing link for. **Search for the file/folder by name.**", }, type: { type: "string", label: "Type", description: "The type of sharing link to create. Either `view`, `edit`, or `embed`.", - options: [ - "view", - "edit", - "embed", - ], + options: constants.SHARING_LINK_TYPE_OPTIONS, }, scope: { type: "string", label: "Scope", description: "The scope of link to create. Either `anonymous` or `organization`.", - options: [ - "anonymous", - "organization", - ], + options: constants.SHARING_LINK_SCOPE_OPTIONS, optional: true, }, }, - methods: { - listDriveItems(args = {}) { - const client = this.app.client(); - return client - .api("/me/drive/root/children") - .get(args); - }, - createLink({ - driveItemId, ...args - } = {}) { - const client = this.app.client(); - return client - .api(`/me/drive/items/${driveItemId}/createLink`) - .post(args); - }, - }, async run({ $ }) { const { - createLink, driveItemId, type, scope, } = this; - const response = await createLink({ + const response = await this.onedrive.createLink({ driveItemId, type, scope, diff --git a/components/microsoft_onedrive/actions/download-file/download-file.mjs b/components/microsoft_onedrive/actions/download-file/download-file.mjs index f1091d3ccb25d..74b6e44df7e54 100644 --- a/components/microsoft_onedrive/actions/download-file/download-file.mjs +++ b/components/microsoft_onedrive/actions/download-file/download-file.mjs @@ -9,32 +9,16 @@ export default { name: "Download File", description: "Download a file stored in OneDrive. [See the documentation](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get_content?view=odsp-graph-online)", key: "microsoft_onedrive-download-file", - version: "0.0.5", + version: "0.0.6", type: "action", props: { onedrive, fileId: { - type: "string", - label: "File ID", - description: "The file to download. You can either search for the file here, provide a custom *File ID*, or use the `File Path` prop to specify the path directly.", + propDefinition: [ + onedrive, + "fileId", + ], optional: true, - useQuery: true, - async options(context) { - const { query } = context; - if (!query) return []; - const response = await this.httpRequest({ - $: context, - url: `/search(q='${query}')?select=folder,name,id`, - }); - return response.value - .filter(({ folder }) => !folder) - .map(({ - name, id, - }) => ({ - label: name, - value: id, - })); - }, }, filePath: { type: "string", @@ -45,7 +29,7 @@ export default { newFileName: { type: "string", label: "New File Name", - description: "The file name to save the downloaded content as, under the `/tmp` folder.", + description: "The file name to save the downloaded content as, under the `/tmp` folder. Make sure to include the file extension.", }, }, methods: { @@ -63,11 +47,18 @@ export default { const url = fileId ? `items/${fileId}/content` : `/root:/${encodeURI(filePath)}:/content`; - const response = await this.httpRequest({ - $, - url, - responseType: "stream", - }); + let response; + try { + response = await this.httpRequest({ + $, + url, + responseType: "stream", + }); + } catch { + throw new ConfigurationError(`Error accessing file. Please make sure that the ${ fileId + ? "File ID" + : "File Path"} is correct.`); + } const fileName = newFileName.split("/").pop(); const tmpFilePath = `/tmp/${fileName}`; diff --git a/components/microsoft_onedrive/actions/find-file-by-name/find-file-by-name.mjs b/components/microsoft_onedrive/actions/find-file-by-name/find-file-by-name.mjs new file mode 100644 index 0000000000000..891fcaccd1d00 --- /dev/null +++ b/components/microsoft_onedrive/actions/find-file-by-name/find-file-by-name.mjs @@ -0,0 +1,47 @@ +import onedrive from "../../microsoft_onedrive.app.mjs"; +import httpRequest from "../../common/httpRequest.mjs"; + +export default { + key: "microsoft_onedrive-find-file-by-name", + name: "Find File by Name", + description: "Search for a file or folder by name. [See the documentation](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_search)", + version: "0.0.1", + type: "action", + props: { + onedrive, + name: { + type: "string", + label: "File Name", + description: "The name of the file or folder to search for", + }, + excludeFolders: { + propDefinition: [ + onedrive, + "excludeFolders", + ], + }, + }, + methods: { + httpRequest, + }, + async run({ $ }) { + const response = await this.httpRequest({ + url: `/search(q='${this.name}')`, + }); + let values = response.value.filter( + ({ name }) => name.toLowerCase().includes(this.name.toLowerCase()), + ); + if (this.excludeFolders) { + values = values.filter(({ folder }) => !folder); + } + + const plural = values.length === 1 + ? "" + : "s"; + const type = this.excludeFolders + ? `file${plural}` + : `file${plural} and/or folder${plural}`; + $.export("$summary", `Found ${values.length} matching ${type}`); + return values; + }, +}; diff --git a/components/microsoft_onedrive/actions/get-excel-table/get-excel-table.mjs b/components/microsoft_onedrive/actions/get-excel-table/get-excel-table.mjs index 10855e0cff100..e3509db868570 100644 --- a/components/microsoft_onedrive/actions/get-excel-table/get-excel-table.mjs +++ b/components/microsoft_onedrive/actions/get-excel-table/get-excel-table.mjs @@ -1,45 +1,34 @@ -import httpRequest from "../../common/httpRequest.mjs"; import onedrive from "../../microsoft_onedrive.app.mjs"; export default { name: "Get Table", description: "Retrieve a table from an Excel spreadsheet stored in OneDrive [See the documentation](https://learn.microsoft.com/en-us/graph/api/table-range?view=graph-rest-1.0&tabs=http)", key: "microsoft_onedrive-get-excel-table", - version: "0.0.4", + version: "0.0.5", type: "action", props: { onedrive, + alert: { + type: "alert", + alertType: "info", + content: `Note: The table must exist within the Excel spreadsheet. + \nSee Microsoft's documentation on how to [Create and Format a Table](https://support.microsoft.com/en-us/office/create-and-format-tables-e81aa349-b006-4f8a-9806-5af9df0ac664) + `, + }, itemId: { - type: "string", - label: "Spreadsheet", - description: "Search for the file by name, only xlsx files are supported", - useQuery: true, - async options( context ) { - const response = await this.httpRequest({ - $: context, - url: `/search(q='${context?.query ?? ""} .xlsx')?select=name,id`, - }); - return response.value - .filter(({ name }) => name.endsWith(".xlsx")) - .map(({ - name, id, - }) => ({ - label: name, - value: id, - })); - }, + propDefinition: [ + onedrive, + "excelFileId", + ], }, tableName: { - type: "string", - label: "Table name", - description: "This is set in the **Table Design** tab of the ribbon.", - async options( context ) { - const response = await this.httpRequest({ - $: context, - url: `/items/${this.itemId}/workbook/tables?$select=name`, - }); - return response.value.map(({ name }) => name); - }, + propDefinition: [ + onedrive, + "tableName", + (c) => ({ + itemId: c.itemId, + }), + ], }, removeHeaders: { type: "boolean", @@ -57,21 +46,23 @@ export default { description: "Leave blank to return all rows.", }, }, - methods: { - httpRequest, - }, async run({ $ }) { - const range = await this.httpRequest({ + const range = await this.onedrive.getExcelTable({ $, - url: `/items/${this.itemId}/workbook/tables/${this.tableName}/range`, + itemId: this.itemId, + tableName: this.tableName, }); - return this.removeHeaders + const response = this.removeHeaders ? this.numberOfRows <= 0 ? range.text.slice(1) : range.text.slice(1, this.numberOfRows + 1) : this.numberOfRows <= 0 ? range.text : range.text.slice(0, this.numberOfRows + 1); + + $.export("$summary", "Successfully retrieved excel table."); + + return response; }, }; diff --git a/components/microsoft_onedrive/actions/get-file-by-id/get-file-by-id.mjs b/components/microsoft_onedrive/actions/get-file-by-id/get-file-by-id.mjs new file mode 100644 index 0000000000000..764176af965e6 --- /dev/null +++ b/components/microsoft_onedrive/actions/get-file-by-id/get-file-by-id.mjs @@ -0,0 +1,25 @@ +import onedrive from "../../microsoft_onedrive.app.mjs"; + +export default { + key: "microsoft_onedrive-get-file-by-id", + name: "Get File by ID", + description: "Retrieves a file by ID. [See the documentation](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get)", + version: "0.0.1", + type: "action", + props: { + onedrive, + fileId: { + propDefinition: [ + onedrive, + "fileId", + ], + description: "The file to retrieve. You can either search for the file here, provide a custom *File ID*.", + }, + }, + async run({ $ }) { + const response = await this.onedrive.client().api(`/me/drive/items/${this.fileId}`) + .get(); + $.export("$summary", `Successfully retreived file with ID: ${this.fileId}`); + return response; + }, +}; diff --git a/components/microsoft_onedrive/actions/list-files-in-folder/list-files-in-folder.mjs b/components/microsoft_onedrive/actions/list-files-in-folder/list-files-in-folder.mjs new file mode 100644 index 0000000000000..04c74358eadf3 --- /dev/null +++ b/components/microsoft_onedrive/actions/list-files-in-folder/list-files-in-folder.mjs @@ -0,0 +1,47 @@ +import onedrive from "../../microsoft_onedrive.app.mjs"; +import httpRequest from "../../common/httpRequest.mjs"; + +export default { + key: "microsoft_onedrive-list-files-in-folder", + name: "List Files in Folder", + description: "Retrieves a list of the files and/or folders directly within a folder. [See the documentation](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_list_children)", + version: "0.0.1", + type: "action", + props: { + onedrive, + folderId: { + propDefinition: [ + onedrive, + "folder", + ], + label: "Folder ID", + description: "The ID of the folder. Use the \"Load More\" button to load subfolders.", + }, + excludeFolders: { + propDefinition: [ + onedrive, + "excludeFolders", + ], + }, + }, + methods: { + httpRequest, + }, + async run({ $ }) { + const response = await this.httpRequest({ + url: `/items/${this.folderId}/children`, + }); + const values = this.excludeFolders + ? response.value.filter(({ folder }) => !folder) + : response.value; + + const plural = values.length === 1 + ? "" + : "s"; + const type = this.excludeFolders + ? `file${plural}` + : `file${plural} and/or folder${plural}`; + $.export("$summary", `Found ${values.length} ${type}`); + return values; + }, +}; diff --git a/components/microsoft_onedrive/actions/upload-file/upload-file.mjs b/components/microsoft_onedrive/actions/upload-file/upload-file.mjs index 6e86c54f75945..84b59d82bda2c 100644 --- a/components/microsoft_onedrive/actions/upload-file/upload-file.mjs +++ b/components/microsoft_onedrive/actions/upload-file/upload-file.mjs @@ -1,5 +1,4 @@ import onedrive from "../../microsoft_onedrive.app.mjs"; -import httpRequest from "../../common/httpRequest.mjs"; import { ConfigurationError } from "@pipedream/platform"; import fs from "fs"; import { fileTypeFromBuffer } from "file-type"; @@ -8,7 +7,7 @@ export default { name: "Upload File", description: "Upload a file to OneDrive. [See the documentation](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content?view=odsp-graph-online)", key: "microsoft_onedrive-upload-file", - version: "0.1.0", + version: "0.1.1", type: "action", props: { onedrive, @@ -23,7 +22,7 @@ export default { filePath: { type: "string", label: "File Path", - description: "The path to the image file saved to the `/tmp` directory (e.g. `/tmp/image.png`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + description: "The path to the file saved to the `/tmp` directory (e.g. `/tmp/image.png`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", }, filename: { type: "string", @@ -31,22 +30,6 @@ export default { description: "Name of the new uploaded file", }, }, - methods: { - httpRequest, - uploadFile({ - uploadFolderId, name, data, ...args - }) { - return this.httpRequest({ - url: `/items/${uploadFolderId}:/${encodeURI(name)}:/content`, - headers: { - "Content-Type": "application/octet-stream", - }, - data, - method: "PUT", - ...args, - }); - }, - }, async run({ $ }) { const { uploadFolderId, filePath, filename, @@ -64,7 +47,7 @@ export default { name = `${filename}.${extension}`; } - const response = await this.uploadFile({ + const response = await this.onedrive.uploadFile({ uploadFolderId, name, data, diff --git a/components/microsoft_onedrive/common/constants.mjs b/components/microsoft_onedrive/common/constants.mjs new file mode 100644 index 0000000000000..561612a84127a --- /dev/null +++ b/components/microsoft_onedrive/common/constants.mjs @@ -0,0 +1,30 @@ +const SHARING_LINK_TYPE_OPTIONS = [ + { + label: "Create a read-only link to the DriveItem", + value: "view", + }, + { + label: "Create a read-write link to the DriveItem", + value: "edit", + }, + { + label: "Create an embeddable link to the DriveItem. Only available for files in OneDrive personal.", + value: "embed", + }, +]; + +const SHARING_LINK_SCOPE_OPTIONS = [ + { + label: "Anyone with the link has access, without needing to sign in", + value: "anonymous", + }, + { + label: "Anyone signed into your organization can use the link. Only available in OneDrive for Business and SharePoint.", + value: "organization", + }, +]; + +export default { + SHARING_LINK_TYPE_OPTIONS, + SHARING_LINK_SCOPE_OPTIONS, +}; diff --git a/components/microsoft_onedrive/common/httpRequest.mjs b/components/microsoft_onedrive/common/httpRequest.mjs index a157fb0647125..9ad1720e4b88b 100644 --- a/components/microsoft_onedrive/common/httpRequest.mjs +++ b/components/microsoft_onedrive/common/httpRequest.mjs @@ -1,18 +1,19 @@ import { axios } from "@pipedream/platform"; export default function ({ - $, ...args + $ = this, ...args }) { let baseURL = "https://graph.microsoft.com/v1.0/me/drive"; if (args.useSharedDrive) { baseURL = "https://graph.microsoft.com/v1.0"; } + const { oauth_access_token: token } = this.$auth || this.onedrive.$auth; return axios($, { ...args, baseURL, headers: { ...args.headers, - Authorization: `Bearer ${this.onedrive.$auth.oauth_access_token}`, + Authorization: `Bearer ${token}`, }, }); } diff --git a/components/microsoft_onedrive/microsoft_onedrive.app.mjs b/components/microsoft_onedrive/microsoft_onedrive.app.mjs index 3498fb9cc71a5..8a33ce6951763 100644 --- a/components/microsoft_onedrive/microsoft_onedrive.app.mjs +++ b/components/microsoft_onedrive/microsoft_onedrive.app.mjs @@ -2,6 +2,7 @@ import "isomorphic-fetch"; import { Client } from "@microsoft/microsoft-graph-client"; import get from "lodash.get"; import mimeTypes from "./sources/common/mime-types.mjs"; +import httpRequest from "./common/httpRequest.mjs"; export default { type: "app", @@ -72,10 +73,87 @@ export default { type: "string[]", label: "File Types", description: "The types of files to watch for", + optional: true, options: mimeTypes, }, + sharedFolderReference: { + type: "string", + label: "Shared Folder Reference", + description: "The reference of the shared folder which the the new folder should be created.\n\nE.g. `/drives/{driveId}/items/{folderId}/children`", + async options() { + const { value } = await this.httpRequest({ + url: "/sharedWithMe", + }); + return value.map((shared) => ({ + label: shared.name, + value: `/drives/${shared.remoteItem.parentReference.driveId}/items/${shared.remoteItem.id}/children`, + })); + }, + }, + fileId: { + type: "string", + label: "File ID", + description: "The file to download. You can either search for the file here, provide a custom *File ID*, or use the `File Path` prop to specify the path directly.", + useQuery: true, + async options({ + query, excludeFolders = true, + }) { + const response = query + ? await this.httpRequest({ + url: `/search(q='${query}')?select=folder,name,id`, + }) + : await this.listDriveItems(); + const values = excludeFolders + ? response.value.filter(({ folder }) => !folder) + : response.value; + return values + .map(({ + name, id, + }) => ({ + label: name, + value: id, + })); + }, + }, + excelFileId: { + type: "string", + label: "Spreadsheet", + description: "**Search for the file by name.** Only xlsx files are supported.", + useQuery: true, + async options({ query }) { + const response = await this.httpRequest({ + url: `/search(q='${query ?? ""} .xlsx')?select=name,id`, + }); + return response.value + .filter(({ name }) => name.endsWith(".xlsx")) + .map(({ + name, id, + }) => ({ + label: name, + value: id, + })); + }, + }, + tableName: { + type: "string", + label: "Table name", + description: "This is set in the **Table Design** tab of the ribbon.", + async options({ itemId }) { + const response = await this.httpRequest({ + url: `/items/${itemId}/workbook/tables?$select=name`, + }); + return response.value.map(({ name }) => name); + }, + }, + excludeFolders: { + type: "boolean", + label: "Exclude Folders?", + description: "Set to `true` to return only files in the response. Defaults to `false`", + optional: true, + }, }, methods: { + httpRequest, /** * @returns Microsoft Graph Client instance */ @@ -285,5 +363,63 @@ export default { url = nextLink; } }, + createLink({ + driveItemId, ...args + } = {}) { + const client = this.client(); + return client + .api(`/me/drive/items/${driveItemId}/createLink`) + .post(args); + }, + createFolder({ + folderName, parentFolderId, sharedFolderReference, + }) { + let url = "/root/children"; + if (parentFolderId) { + url = `/items/${parentFolderId}/children`; + } + if (sharedFolderReference) { + url = sharedFolderReference; + } + return this.httpRequest({ + url, + useSharedDrive: !!sharedFolderReference, + headers: { + "Content-Type": "application/json", + }, + data: { + name: folderName, + folder: {}, + ["@microsoft.graph.conflictBehavior"]: "rename", + }, + method: "POST", + }); + }, + getExcelTable({ + itemId, tableName, ...args + }) { + return this.httpRequest({ + url: `/items/${itemId}/workbook/tables/${tableName}/range`, + ...args, + }); + }, + uploadFile({ + uploadFolderId, name, ...args + }) { + return this.httpRequest({ + url: `/items/${uploadFolderId}:/${encodeURI(name)}:/content`, + headers: { + "Content-Type": "application/octet-stream", + }, + method: "PUT", + ...args, + }); + }, + listDriveItems(args = {}) { + const client = this.client(); + return client + .api("/me/drive/root/children") + .get(args); + }, }, }; diff --git a/components/microsoft_onedrive/package.json b/components/microsoft_onedrive/package.json index fbf09ddc24e7a..1dd1788bf086f 100644 --- a/components/microsoft_onedrive/package.json +++ b/components/microsoft_onedrive/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/microsoft_onedrive", - "version": "1.5.0", + "version": "1.6.0", "description": "Pipedream Microsoft OneDrive components", "main": "microsoft_onedrive.app.js", "homepage": "https://pipedream.com/apps/microsoft-onedrive", @@ -10,7 +10,8 @@ }, "dependencies": { "@microsoft/microsoft-graph-client": "^3.0.1", - "@pipedream/platform": "^1.1.0", + "@pipedream/platform": "^3.0.3", + "bottleneck": "^2.19.5", "file-type": "^18.7.0", "isomorphic-fetch": "^3.0.0", "lodash.get": "^4.4.2" diff --git a/components/microsoft_onedrive/sources/common/base.mjs b/components/microsoft_onedrive/sources/common/base.mjs index 6e8ebad05eaa3..7dc4aa604eb28 100644 --- a/components/microsoft_onedrive/sources/common/base.mjs +++ b/components/microsoft_onedrive/sources/common/base.mjs @@ -1,5 +1,9 @@ import onedrive from "../../microsoft_onedrive.app.mjs"; -import { toSingleLineString } from "./utils.mjs"; +import Bottleneck from "bottleneck"; +const limiter = new Bottleneck({ + minTime: 100, // 10 requests per second + maxConcurrent: 1, +}); // Defaulting to 15 days. The maximum allowed expiration time is 30 days, // according to their API response message: "Subscription expiration can only @@ -18,14 +22,12 @@ const props = { }, timer: { type: "$.interface.timer", - label: "Webhook subscription renewal schedule", - description: toSingleLineString(` - The OneDrive API requires occasional renewal of webhook notification subscriptions. - **This runs in the background, so you should not need to modify this schedule**. - `), + // The OneDrive API requires occasional renewal of webhook notification subscriptions. + // **This runs in the background, so the user should not need to modify this schedule**. default: { intervalSeconds: WEBHOOK_SUBSCRIPTION_RENEWAL_SECONDS, }, + hidden: true, }, }; @@ -37,16 +39,21 @@ const hooks = { // We skip the first drive item, since it represents the root directory await itemsStream.next(); - let eventsToProcess = 10; - for await (const driveItem of itemsStream) { - if (!this.isItemTypeRelevant(driveItem)) { + let done, value, eventsToProcess = 10; + while (true) { + ({ + done, value, + } = await limiter.schedule(() => itemsStream.next())); + if (value && !this.isItemTypeRelevant(value)) { // If the type of the item being processed is not relevant to the // event source we want to skip it in order to avoid confusion in // terms of the actual payload of the sample events continue; } - await this.processEvent(driveItem); + if (done) break; + + await this.processEvent(value); if (--eventsToProcess <= 0) { break; } @@ -136,9 +143,8 @@ const methods = { let done, value; try { ({ - done, - value, - } = await itemsStream.next()); + done, value, + } = await limiter.schedule(() => itemsStream.next())); } catch (e) { // Users have come upon an error with deltaLink, so we need to reset it by // creating a new subscription. @@ -151,8 +157,6 @@ const methods = { this._setSequentialErrorsCount(errors + 1); } - this._setSequentialErrorsCount(0); - if (done) { // No more items to retrieve from OneDrive. We update the cached Delta // Link and move on. @@ -161,6 +165,7 @@ const methods = { } const shouldSkipItem = ( + !value || !this.isItemTypeRelevant(value) || !this.isItemRelevant(value) ); @@ -174,7 +179,7 @@ const methods = { await this.processEvent(value); } } - + this._setSequentialErrorsCount(0); await this.postProcessEvent(); }, /** @@ -282,10 +287,10 @@ async function run(event) { return this._renewSubscription(); } - // Every HTTP call made by a OneDrive webhook expects a '202 Accepted` + // Every HTTP call made by a OneDrive webhook expects a '200 Accepted` // response, and it should be done as soon as possible. this.http.respond({ - status: 202, + status: 200, }); // Using the last known Delta Link, we retrieve and process the items that diff --git a/components/microsoft_onedrive/sources/new-file-created/new-file-created.mjs b/components/microsoft_onedrive/sources/new-file-created/new-file-created.mjs new file mode 100644 index 0000000000000..a6f6d5d739f01 --- /dev/null +++ b/components/microsoft_onedrive/sources/new-file-created/new-file-created.mjs @@ -0,0 +1,53 @@ +import onedrive from "../../microsoft_onedrive.app.mjs"; +import base from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...base, + type: "source", + key: "microsoft_onedrive-new-file-created", + name: "New File Created (Instant)", + description: "Emit new event when a new file is created in a OneDrive drive", + version: "0.0.1", + dedupe: "unique", + props: { + ...base.props, + folder: { + propDefinition: [ + onedrive, + "folder", + ], + description: "The OneDrive folder to watch for new files (leave empty to watch the entire drive). Use the \"Load More\" button to load subfolders.", + optional: true, + }, + fileTypes: { + propDefinition: [ + onedrive, + "fileTypes", + ], + }, + }, + methods: { + ...base.methods, + getDeltaLinkParams() { + return this.folder + ? { + folderId: this.folder, + } + : {}; + }, + isItemTypeRelevant(driveItem) { + const fileType = driveItem?.file?.mimeType; + return this.fileTypes?.length + ? !!(this.fileTypes.find((type) => fileType?.includes(type))) + : true; + }, + isItemRelevant(driveItem) { + if (!driveItem?.file) { + return false; + } + return !this.folder || driveItem?.parentReference?.id === this.folder; + }, + }, + sampleEmit, +}; diff --git a/components/microsoft_onedrive/sources/new-file-created/test-event.mjs b/components/microsoft_onedrive/sources/new-file-created/test-event.mjs new file mode 100644 index 0000000000000..b42f27b757e75 --- /dev/null +++ b/components/microsoft_onedrive/sources/new-file-created/test-event.mjs @@ -0,0 +1,57 @@ +export default { + "@odata.type": "#microsoft.graph.driveItem", + "@microsoft.graph.downloadUrl": "https://public.ph.files.1drv.com/y4m-N2S08wqTS-wxEZrb5EYCUXP2VFmpY7cprlv53D3tAj3OJ3RlXqaz-SdLkpYVDXMVLEls8bGIs1ABBV85Ps46ZthgDepcJyUXKZgnR_I9RPVnCGb90DDcUCz43Y1FhyAkXwLOakoxSdoLsdx5fLMZwaiG7WdXEMZB_TrU4cFUq4Tt2-SXQcgOqS4zxJovlEETmz9jNh7P4Hzeo02WaqtsgWkzPEL9-L5sxwKgEpct9K0EkSMpSq5QOWZWiet9E2elrK", + "createdDateTime": "2023-10-20T10:16:35.5Z", + "cTag": "aYzn2M4207NkI9QUVCNEY1QzU9ITEySi4yNTD", + "eTag": "aNTI3NzZBVFFQjRGMUM1NCExMjkuNA", + "id": "52776A9ACB4F8C54!134", + "lastModifiedDateTime": "2023-10-20T10:16:35.5Z", + "name": "john-doe-JvVVnUmW2nA-unsplash.jpg", + "size": 1548310, + "webUrl": "https://1drv.ms/i/s!AFQcT-uaandSgJS", + "reactions": { + "commentCount": 3 + }, + "createdBy": { + "application": { + "id": "481790b5" + }, + "user": { + "displayName": "John Doe", + "id": "52776a9aeb8f5c78" + } + }, + "lastModifiedBy": { + "application": { + "id": "481790b5" + }, + "user": { + "displayName": "John Doe", + "id": "52776a9aeb8f5c78" + } + }, + "parentReference": { + "driveId": "52776a9aeb8f5c78", + "driveType": "personal", + "id": "52776A9ACB4F8C54!114", + "name": "Photos", + "path": "/drive/root:/Photos" + }, + "file": { + "mimeType": "image/jpeg", + "hashes": { + "quickXorHash": "14D9oiw9tGevQ76sR2mxUcw6Bb0=", + "sha1Hash": "11BAE9E008Z8F9AE33298517Z723120FC6D89L6F", + "sha256Hash": "90523A68654CF99D23057BD1426BB2F5BBD238308BF89E819SFB522C1373C3CE" + } + }, + "fileSystemInfo": { + "createdDateTime": "2023-10-20T10:16:35.5Z", + "lastModifiedDateTime": "2023-10-20T10:16:35.5Z" + }, + "image": { + "height": 5632, + "width": 3755 + }, + "photo": {} + } \ No newline at end of file diff --git a/components/microsoft_onedrive/sources/new-file-in-folder/new-file-in-folder.mjs b/components/microsoft_onedrive/sources/new-file-in-folder/new-file-in-folder.mjs deleted file mode 100644 index 86f20803c972c..0000000000000 --- a/components/microsoft_onedrive/sources/new-file-in-folder/new-file-in-folder.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import onedrive from "../../microsoft_onedrive.app.mjs"; -import sampleEmit from "./test-event.mjs"; -import base from "../common/base.mjs"; - -export default { - ...base, - type: "source", - key: "microsoft_onedrive-new-file-in-folder", - name: "New File in Folder (Instant)", - description: "Emit an event when a new file is added to a specific directory tree in a OneDrive drive", - version: "0.1.0", - dedupe: "unique", - props: { - ...base.props, - folder: { - propDefinition: [ - onedrive, - "folder", - ], - }, - }, - methods: { - ...base.methods, - getDeltaLinkParams() { - return { - folderId: this.folder, - }; - }, - isItemRelevant(driveItem) { - return !!(driveItem.file); - }, - }, - sampleEmit, -}; diff --git a/components/microsoft_onedrive/sources/new-file-in-folder/test-event.mjs b/components/microsoft_onedrive/sources/new-file-in-folder/test-event.mjs deleted file mode 100644 index 4aabf8416098b..0000000000000 --- a/components/microsoft_onedrive/sources/new-file-in-folder/test-event.mjs +++ /dev/null @@ -1,57 +0,0 @@ -export default { - "@odata.type": "#microsoft.graph.driveItem", - "@microsoft.graph.downloadUrl": "https://public.ph.files.1drv.com/y4m-N2S08wqTS-wxEZrb5EYCUXP2VFmpY7cprlv53D3tAj3OJ3RlXqaz-SdLkpYVDXMVLEls8bGIs1ABBV85Ps46ZthgDepcJyUXKZgnR_I9RPVnCGb90DDcUCz43Y1FhyAkXwLOakoxSdoLsdx5fLMZwaiG7WdXEMZB_TrU4cFUq4Tt2-SXQcgOqS4zxJovlEETmz9jNh7P4Hzeo02WaqtsgWkzPEL9-L5sxwKgEpct9K0EkSMpSq5QOWZWiet9E2elrK", - "createdDateTime": "2023-10-20T10:16:35.5Z", - "cTag": "aYzn2M4207NkI9QUVCNEY1QzU9ITEySi4yNTD", - "eTag": "aNTI3NzZBVFFQjRGMUM1NCExMjkuNA", - "id": "52776A9ACB4F8C54!134", - "lastModifiedDateTime": "2023-10-20T10:16:35.5Z", - "name": "john-doe-JvVVnUmW2nA-unsplash.jpg", - "size": 1548310, - "webUrl": "https://1drv.ms/i/s!AFQcT-uaandSgJS", - "reactions": { - "commentCount": 3 - }, - "createdBy": { - "application": { - "id": "481790b5" - }, - "user": { - "displayName": "John Doe", - "id": "52776a9aeb8f5c78" - } - }, - "lastModifiedBy": { - "application": { - "id": "481790b5" - }, - "user": { - "displayName": "John Doe", - "id": "52776a9aeb8f5c78" - } - }, - "parentReference": { - "driveId": "52776a9aeb8f5c78", - "driveType": "personal", - "id": "52776A9ACB4F8C54!114", - "name": "Photos", - "path": "/drive/root:/Photos" - }, - "file": { - "mimeType": "image/jpeg", - "hashes": { - "quickXorHash": "14D9oiw9tGevQ76sR2mxUcw6Bb0=", - "sha1Hash": "11BAE9E008Z8F9AE33298517Z723120FC6D89L6F", - "sha256Hash": "90523A68654CF99D23057BD1426BB2F5BBD238308BF89E819SFB522C1373C3CE" - } - }, - "fileSystemInfo": { - "createdDateTime": "2023-10-20T10:16:35.5Z", - "lastModifiedDateTime": "2023-10-20T10:16:35.5Z" - }, - "image": { - "height": 5632, - "width": 3755 - }, - "photo": {} - } \ No newline at end of file diff --git a/components/microsoft_onedrive/sources/new-file-of-types-in-folder/new-file-of-types-in-folder.mjs b/components/microsoft_onedrive/sources/new-file-of-types-in-folder/new-file-of-types-in-folder.mjs deleted file mode 100644 index a5946b69d8ce0..0000000000000 --- a/components/microsoft_onedrive/sources/new-file-of-types-in-folder/new-file-of-types-in-folder.mjs +++ /dev/null @@ -1,39 +0,0 @@ -import onedrive from "../../microsoft_onedrive.app.mjs"; -import base from "../common/base.mjs"; - -export default { - ...base, - type: "source", - key: "microsoft_onedrive-new-file-of-types-in-folder", - name: "New File of Types in Folder (Instant)", - description: "Emit an event when a new file of a specific type is created under a directory tree in a OneDrive drive", - version: "0.1.0", - dedupe: "unique", - props: { - ...base.props, - folder: { - propDefinition: [ - onedrive, - "folder", - ], - description: "The OneDrive folder to watch for new files (leave empty to watch the entire drive). Use the \"Load More\" button to load subfolders.", - optional: true, - }, - fileTypes: { - propDefinition: [ - onedrive, - "fileTypes", - ], - }, - }, - methods: { - ...base.methods, - isItemTypeRelevant(driveItem) { - const fileType = driveItem?.file?.mimeType; - return ( - base.methods.isItemTypeRelevant.call(this, driveItem) && - this.fileTypes.includes(fileType) - ); - }, - }, -}; diff --git a/components/microsoft_onedrive/sources/new-file/new-file.mjs b/components/microsoft_onedrive/sources/new-file/new-file.mjs deleted file mode 100644 index ae3c2a02d2731..0000000000000 --- a/components/microsoft_onedrive/sources/new-file/new-file.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import base from "../common/base.mjs"; -import sampleEmit from "./test-event.mjs"; - -export default { - ...base, - type: "source", - key: "microsoft_onedrive-new-file", - name: "New File (Instant)", - description: "Emit new event when a new file is added to a specific drive in OneDrive", - version: "0.0.5", - dedupe: "unique", - methods: { - ...base.methods, - isItemTypeRelevant(driveItem) { - return !driveItem.deleted; - }, - isItemRelevant(driveItem) { - return !!(driveItem.file); - }, - }, - sampleEmit, -}; diff --git a/components/microsoft_onedrive/sources/new-file/test-event.mjs b/components/microsoft_onedrive/sources/new-file/test-event.mjs deleted file mode 100644 index 1ae77eda2c232..0000000000000 --- a/components/microsoft_onedrive/sources/new-file/test-event.mjs +++ /dev/null @@ -1,57 +0,0 @@ -export default { - "@odata.type": "#microsoft.graph.driveItem", - "@microsoft.graph.downloadUrl": "https://public.ab.files.1drv.com/w4mo_aBSo3LhWOU6gUHWKuic32DAkhJ58GTBhS9tSRo85PUvWlvU3MZmiAxrYLygem", - "createdDateTime": "2045-08-07T10:55:51.557Z", - "cTag": "aYzo4Mjc7NkE5QUVCNEYxQzU0ITEzOS4yNTc", - "eTag": "aNTI3Nz6BOUFFQj4GMUM1NCExMzkuMg", - "id": "62776A9AEB4F1C", - "lastModifiedDateTime": "2045-08-07T10:55:51.837Z", - "name": "rose-2535.jpeg", - "size": 45635, - "webUrl": "https://1drv.ms/i/s!AFQcT-uaandSgRs", - "reactions": { - "commentCount": 1 - }, - "createdBy": { - "application": { - "id": "581720a4" - }, - "user": { - "displayName": "Dung Tran", - "id": "62776a9aeb4f1c54" - } - }, - "lastModifiedBy": { - "application": { - "id": "581720a4" - }, - "user": { - "displayName": "Dung Tran", - "id": "62776a9aeb4f1c54" - } - }, - "parentReference": { - "driveId": "62776a9aeb4f1c54", - "driveType": "personal", - "id": "62776A9AEB4F1C54!103", - "name": "root:", - "path": "/drive/root:" - }, - "file": { - "mimeType": "image/jpeg", - "hashes": { - "quickXorHash": "P7nMvhdLnzi1Ipmu337PXj6wdFM=", - "sha1Hash": "2E91802A7865A8B576CB5DE8A1E9FED345C3A0E8", - "sha256Hash": "107535F9FDB56340368439C0BAB9267B8F8812852BF249118D1641D2F548CCBF" - } - }, - "fileSystemInfo": { - "createdDateTime": "2045-08-07T10:55:51.556Z", - "lastModifiedDateTime": "2045-08-07T10:55:51.556Z" - }, - "image": { - "height": 665, - "width": 665 - }, - "photo": {} - } \ No newline at end of file diff --git a/components/microsoft_onedrive/sources/new-folder-created/new-folder-created.mjs b/components/microsoft_onedrive/sources/new-folder-created/new-folder-created.mjs new file mode 100644 index 0000000000000..d19a2cbbadcc2 --- /dev/null +++ b/components/microsoft_onedrive/sources/new-folder-created/new-folder-created.mjs @@ -0,0 +1,37 @@ +import base from "../common/base.mjs"; +import onedrive from "../../microsoft_onedrive.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...base, + type: "source", + key: "microsoft_onedrive-new-folder-created", + name: "New Folder Created (Instant)", + description: "Emit new event when a new folder is created in a OneDrive drive", + version: "0.0.1", + dedupe: "unique", + props: { + ...base.props, + folder: { + propDefinition: [ + onedrive, + "folder", + ], + optional: true, + }, + }, + methods: { + ...base.methods, + getDeltaLinkParams() { + return this.folder + ? { + folderId: this.folder, + } + : {}; + }, + isItemRelevant(driveItem) { + return !!(driveItem.folder); + }, + }, + sampleEmit, +}; diff --git a/components/microsoft_onedrive/sources/new-folder-created/test-event.mjs b/components/microsoft_onedrive/sources/new-folder-created/test-event.mjs new file mode 100644 index 0000000000000..7d81c1db7925e --- /dev/null +++ b/components/microsoft_onedrive/sources/new-folder-created/test-event.mjs @@ -0,0 +1,47 @@ +export default { + "createdDateTime": "2024-08-07T17:50:10.587Z", + "cTag": "adDpDOTgyN0EzRUI3MkE1RjYwITIxMDEuNjM4NTg2NDk4MTA1ODcwMDAw", + "eTag": "aQzk4MjdBM0VCNzJBNUY2MCEyMTAxLjA", + "id": "C9827A3EB72A5F60!2101", + "lastModifiedDateTime": "2024-08-07T17:50:10.587Z", + "name": "newfolder", + "size": 0, + "webUrl": "https://1drv.ms/f/s!AGBfKrc-eoLJkDU", + "createdBy": { + "application": { + "id": "481710a4" + }, + "user": { + "displayName": "John Doe", + "id": "c9827a3eb72a5f60" + } + }, + "lastModifiedBy": { + "application": { + "id": "481710a4" + }, + "user": { + "displayName": "John Doe", + "id": "c9827a3eb72a5f60" + } + }, + "parentReference": { + "driveId": "c9827a3eb72a5f60", + "driveType": "personal", + "id": "C9827A3EB72A5F60!129", + "name": "Documents", + "path": "/drive/root:/Documents" + }, + "fileSystemInfo": { + "createdDateTime": "2024-08-07T17:50:10.586Z", + "lastModifiedDateTime": "2024-08-07T17:50:10.586Z" + }, + "folder": { + "childCount": 0, + "view": { + "viewType": "thumbnails", + "sortBy": "name", + "sortOrder": "ascending" + } + } +} \ No newline at end of file diff --git a/components/microsoft_onedrive/sources/new-folder-in-folder/new-folder-in-folder.mjs b/components/microsoft_onedrive/sources/new-folder-in-folder/new-folder-in-folder.mjs deleted file mode 100644 index 67c9df9004bd9..0000000000000 --- a/components/microsoft_onedrive/sources/new-folder-in-folder/new-folder-in-folder.mjs +++ /dev/null @@ -1,32 +0,0 @@ -import base from "../new-file/new-file.mjs"; -import onedrive from "../../microsoft_onedrive.app.mjs"; - -export default { - ...base, - type: "source", - key: "microsoft_onedrive-new-folder-in-folder", - name: "New Folder in Folder (Instant)", - description: "Emit an event when a new folder is created under a directory tree in a OneDrive drive", - version: "0.1.0", - dedupe: "unique", - props: { - ...base.props, - folder: { - propDefinition: [ - onedrive, - "folder", - ], - }, - }, - methods: { - ...base.methods, - getDeltaLinkParams() { - return { - folderId: this.folder, - }; - }, - isItemRelevant(driveItem) { - return !!(driveItem.folder) && driveItem.parentReference?.path !== "/drive/root:"; - }, - }, -}; diff --git a/components/microsoft_onedrive/sources/new-folder/new-folder.mjs b/components/microsoft_onedrive/sources/new-folder/new-folder.mjs deleted file mode 100644 index 87bb1adc164c2..0000000000000 --- a/components/microsoft_onedrive/sources/new-folder/new-folder.mjs +++ /dev/null @@ -1,17 +0,0 @@ -import base from "../new-file/new-file.mjs"; - -export default { - ...base, - type: "source", - key: "microsoft_onedrive-new-folder", - name: "New Folder (Instant)", - description: "Emit new event when a new folder is created in a OneDrive drive", - version: "0.0.3", - dedupe: "unique", - methods: { - ...base.methods, - isItemRelevant(driveItem) { - return !!(driveItem.folder); - }, - }, -}; diff --git a/components/microsoft_outlook/README.md b/components/microsoft_outlook/README.md index c37ce2538e06a..74c77b03c0fcd 100644 --- a/components/microsoft_outlook/README.md +++ b/components/microsoft_outlook/README.md @@ -1,9 +1,11 @@ # Overview -With the Microsoft Outlook API, you can build a wide range of applications and -services that work with Outlook. Here are some examples: +The Microsoft Outlook API on Pipedream allows you to automate email-related tasks, manage calendars, and handle contacts effortlessly. With the API, you can trigger workflows on new emails, send emails programmatically, and synchronize calendars across platforms, among other functions. Pipedream's serverless platform facilitates the connection between Outlook and a myriad of other apps for efficient automation workflows. -- A service that helps you keep track of your to-do list and get things done -- A service that helps you monitor your email and respond to messages -- A service that helps you find and connect with people in your network -- A service that helps you stay up to date on the latest news and information +# Example Use Cases + +- **Email Event to Slack Notification**: When receiving an email from a specific sender or with certain keywords, you can automatically post a message to a Slack channel. This keeps teams informed of important communications without manual monitoring of inboxes. + +- **Calendar Sync with Google Calendar**: Upon the creation of a new event in Outlook Calendar, sync this event to Google Calendar. This is ideal for individuals who use multiple calendar services and want to ensure consistency across platforms without manual duplication of events. + +- **Automated Email Responses**: Set up a workflow that sends an automated response to emails that meet specific criteria, such as out-of-office replies or acknowledging receipt of customer inquiries. This helps maintain communication with contacts even when you're not actively managing your inbox. diff --git a/components/microsoft_outlook/actions/add-label-to-email/add-label-to-email.mjs b/components/microsoft_outlook/actions/add-label-to-email/add-label-to-email.mjs new file mode 100644 index 0000000000000..f2fe8ce589359 --- /dev/null +++ b/components/microsoft_outlook/actions/add-label-to-email/add-label-to-email.mjs @@ -0,0 +1,54 @@ +import microsoftOutlook from "../../microsoft_outlook.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "microsoft_outlook-add-label-to-email", + name: "Add Label to Email", + description: "Adds a label/category to an email in Microsoft Outlook. [See the documentation](https://learn.microsoft.com/en-us/graph/api/message-update)", + version: "0.0.3", + type: "action", + props: { + microsoftOutlook, + messageId: { + propDefinition: [ + microsoftOutlook, + "messageId", + ], + }, + label: { + propDefinition: [ + microsoftOutlook, + "label", + (c) => ({ + messageId: c.messageId, + excludeMessageLabels: true, + }), + ], + }, + }, + async run({ $ }) { + const message = await this.microsoftOutlook.getMessage({ + $, + messageId: this.messageId, + }); + + const labels = message?.categories; + + if (labels.includes(this.label)) { + throw new ConfigurationError(`Message already contains label "${this.label}".`); + } + + const response = await this.microsoftOutlook.updateMessage({ + $, + messageId: this.messageId, + data: { + categories: [ + ...labels, + this.label, + ], + }, + }); + $.export("$summary", "Successfully added label to message."); + return response; + }, +}; diff --git a/components/microsoft_outlook/actions/approve-workflow/approve-workflow.mjs b/components/microsoft_outlook/actions/approve-workflow/approve-workflow.mjs new file mode 100644 index 0000000000000..61b2771a4d46f --- /dev/null +++ b/components/microsoft_outlook/actions/approve-workflow/approve-workflow.mjs @@ -0,0 +1,47 @@ +import microsoftOutlook from "../../microsoft_outlook.app.mjs"; + +export default { + key: "microsoft_outlook-approve-workflow", + name: "Approve Workflow", + description: "Suspend the workflow until approved by email. [See the documentation](https://pipedream.com/docs/code/nodejs/rerun#flowsuspend)", + version: "0.0.1", + type: "action", + props: { + microsoftOutlook, + recipients: { + propDefinition: [ + microsoftOutlook, + "recipients", + ], + optional: false, + }, + subject: { + propDefinition: [ + microsoftOutlook, + "subject", + ], + optional: false, + }, + }, + async run({ $ }) { + const { + resume_url, cancel_url, + } = $.flow.suspend(); + const opts = { + content: `Click here to approve the workflow: ${resume_url}, \nand cancel here: ${cancel_url}`, + ccRecipients: [], + bccRecipients: [], + ...this, + }; + const response = await this.microsoftOutlook.sendEmail({ + $, + data: { + message: { + ...this.microsoftOutlook.prepareMessageBody(opts), + }, + }, + }); + $.export("$summary", "Email has been sent."); + return response; + }, +}; diff --git a/components/microsoft_outlook/actions/create-contact/create-contact.mjs b/components/microsoft_outlook/actions/create-contact/create-contact.mjs index cad9c28c18fe1..eee6d78089425 100644 --- a/components/microsoft_outlook/actions/create-contact/create-contact.mjs +++ b/components/microsoft_outlook/actions/create-contact/create-contact.mjs @@ -3,7 +3,7 @@ import microsoftOutlook from "../../microsoft_outlook.app.mjs"; export default { type: "action", key: "microsoft_outlook-create-contact", - version: "0.0.7", + version: "0.0.10", name: "Create Contact", description: "Add a contact to the root Contacts folder, [See the docs](https://docs.microsoft.com/en-us/graph/api/user-post-contacts)", props: { diff --git a/components/microsoft_outlook/actions/create-draft-email/create-draft-email.mjs b/components/microsoft_outlook/actions/create-draft-email/create-draft-email.mjs index a0f3bd330aa59..d01b0283ede10 100644 --- a/components/microsoft_outlook/actions/create-draft-email/create-draft-email.mjs +++ b/components/microsoft_outlook/actions/create-draft-email/create-draft-email.mjs @@ -3,7 +3,7 @@ import microsoftOutlook from "../../microsoft_outlook.app.mjs"; export default { type: "action", key: "microsoft_outlook-create-draft-email", - version: "0.0.7", + version: "0.0.10", name: "Create Draft Email", description: "Create a draft email, [See the docs](https://docs.microsoft.com/en-us/graph/api/user-post-messages)", props: { diff --git a/components/microsoft_outlook/actions/find-contacts/find-contacts.mjs b/components/microsoft_outlook/actions/find-contacts/find-contacts.mjs index 92ac9b7020773..63b8aa83f8428 100644 --- a/components/microsoft_outlook/actions/find-contacts/find-contacts.mjs +++ b/components/microsoft_outlook/actions/find-contacts/find-contacts.mjs @@ -3,7 +3,7 @@ import microsoftOutlook from "../../microsoft_outlook.app.mjs"; export default { type: "action", key: "microsoft_outlook-find-contacts", - version: "0.0.7", + version: "0.0.10", name: "Find Contacts", description: "Finds contacts with given search string", props: { diff --git a/components/microsoft_outlook/actions/find-email/find-email.mjs b/components/microsoft_outlook/actions/find-email/find-email.mjs new file mode 100644 index 0000000000000..50d9b821ea507 --- /dev/null +++ b/components/microsoft_outlook/actions/find-email/find-email.mjs @@ -0,0 +1,37 @@ +import microsoftOutlook from "../../microsoft_outlook.app.mjs"; + +export default { + key: "microsoft_outlook-find-email", + name: "Find Email", + description: "Search for an email in Microsoft Outlook. [See the documentation](https://learn.microsoft.com/en-us/graph/api/user-list-messages)", + version: "0.0.1", + type: "action", + props: { + microsoftOutlook, + filter: { + type: "string", + label: "Filter", + description: "Filters results. For example, `contains(subject, 'meet for lunch?')` will include messages whose subject contains ‘meet for lunch?’. [See documentation](https://learn.microsoft.com/en-us/graph/filter-query-parameter) for the full list of operations.", + optional: true, + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "The maximum number of results to return", + optional: true, + }, + }, + async run({ $ }) { + const { value } = await this.microsoftOutlook.listMessages({ + $, + params: { + "$filter": this.filter, + "$top": this.maxResults, + }, + }); + $.export("$summary", `Successfully retrieved ${value.length} message${value.length != 1 + ? "s" + : ""}.`); + return value; + }, +}; diff --git a/components/microsoft_outlook/actions/list-contacts/list-contacts.mjs b/components/microsoft_outlook/actions/list-contacts/list-contacts.mjs index 2d7602b5f7a47..415d6387a04ef 100644 --- a/components/microsoft_outlook/actions/list-contacts/list-contacts.mjs +++ b/components/microsoft_outlook/actions/list-contacts/list-contacts.mjs @@ -3,7 +3,7 @@ import microsoftOutlook from "../../microsoft_outlook.app.mjs"; export default { type: "action", key: "microsoft_outlook-list-contacts", - version: "0.0.7", + version: "0.0.10", name: "List Contacts", description: "Get a contact collection from the default contacts folder, [See the docs](https://docs.microsoft.com/en-us/graph/api/user-list-contacts)", props: { diff --git a/components/microsoft_outlook/actions/list-folders/list-folders.mjs b/components/microsoft_outlook/actions/list-folders/list-folders.mjs new file mode 100644 index 0000000000000..73de1ea228345 --- /dev/null +++ b/components/microsoft_outlook/actions/list-folders/list-folders.mjs @@ -0,0 +1,21 @@ +import microsoftOutlook from "../../microsoft_outlook.app.mjs"; + +export default { + key: "microsoft_outlook-list-folders", + name: "List Folders", + description: "Retrieves a list of all folders in Microsoft Outlook. [See the documentation](https://learn.microsoft.com/en-us/graph/api/user-list-mailfolders)", + version: "0.0.1", + type: "action", + props: { + microsoftOutlook, + }, + async run({ $ }) { + const { value } = await this.microsoftOutlook.listFolders({ + $, + }); + $.export("$summary", `Successfully retrieved ${value.length} folder${value.length != 1 + ? "s" + : ""}.`); + return value; + }, +}; diff --git a/components/microsoft_outlook/actions/list-labels/list-labels.mjs b/components/microsoft_outlook/actions/list-labels/list-labels.mjs new file mode 100644 index 0000000000000..c201c5c2d2571 --- /dev/null +++ b/components/microsoft_outlook/actions/list-labels/list-labels.mjs @@ -0,0 +1,21 @@ +import microsoftOutlook from "../../microsoft_outlook.app.mjs"; + +export default { + key: "microsoft_outlook-list-labels", + name: "List Labels", + description: "Get all the labels/categories that have been defined for a user. [See the documentation](https://learn.microsoft.com/en-us/graph/api/outlookuser-list-mastercategories)", + version: "0.0.3", + type: "action", + props: { + microsoftOutlook, + }, + async run({ $ }) { + const { value } = await this.microsoftOutlook.listLabels({ + $, + }); + $.export("$summary", `Successfully retrieved ${value.length} label${value.length != 1 + ? "s" + : ""}.`); + return value; + }, +}; diff --git a/components/microsoft_outlook/actions/move-email-to-folder/move-email-to-folder.mjs b/components/microsoft_outlook/actions/move-email-to-folder/move-email-to-folder.mjs new file mode 100644 index 0000000000000..b0e036542ec2a --- /dev/null +++ b/components/microsoft_outlook/actions/move-email-to-folder/move-email-to-folder.mjs @@ -0,0 +1,38 @@ +import microsoftOutlook from "../../microsoft_outlook.app.mjs"; + +export default { + key: "microsoft_outlook-move-email-to-folder", + name: "Move Email to Folder", + description: "Moves an email to the specified folder in Microsoft Outlook. [See the documentation](https://learn.microsoft.com/en-us/graph/api/message-move)", + version: "0.0.1", + type: "action", + props: { + microsoftOutlook, + messageId: { + propDefinition: [ + microsoftOutlook, + "messageId", + ], + }, + folderId: { + propDefinition: [ + microsoftOutlook, + "folderIds", + ], + type: "string", + label: "Folder ID", + description: "The identifier of the folder to move the selected message to", + }, + }, + async run({ $ }) { + const response = await this.microsoftOutlook.moveMessage({ + $, + messageId: this.messageId, + data: { + destinationId: this.folderId, + }, + }); + $.export("$summary", `Successfully moved email to folder with ID: ${this.folderId}`); + return response; + }, +}; diff --git a/components/microsoft_outlook/actions/remove-label-from-email/remove-label-from-email.mjs b/components/microsoft_outlook/actions/remove-label-from-email/remove-label-from-email.mjs new file mode 100644 index 0000000000000..4a4c00dd64965 --- /dev/null +++ b/components/microsoft_outlook/actions/remove-label-from-email/remove-label-from-email.mjs @@ -0,0 +1,51 @@ +import microsoftOutlook from "../../microsoft_outlook.app.mjs"; + +export default { + key: "microsoft_outlook-remove-label-from-email", + name: "Remove Label from Email", + description: "Removes a label/category from an email in Microsoft Outlook. [See the documentation](https://learn.microsoft.com/en-us/graph/api/message-update)", + version: "0.0.3", + type: "action", + props: { + microsoftOutlook, + messageId: { + propDefinition: [ + microsoftOutlook, + "messageId", + ], + }, + label: { + propDefinition: [ + microsoftOutlook, + "label", + (c) => ({ + messageId: c.messageId, + onlyMessageLabels: true, + }), + ], + description: "The name of the label/category to remove", + }, + }, + async run({ $ }) { + const message = await this.microsoftOutlook.getMessage({ + $, + messageId: this.messageId, + }); + let labels = message?.categories; + + const index = labels.indexOf(this.label); + if (index > -1) { + labels.splice(index, 1); + } + + const response = await this.microsoftOutlook.updateMessage({ + $, + messageId: this.messageId, + data: { + categories: labels, + }, + }); + $.export("$summary", "Successfully removed label from message."); + return response; + }, +}; diff --git a/components/microsoft_outlook/actions/send-email/send-email.mjs b/components/microsoft_outlook/actions/send-email/send-email.mjs index 59807a5e715ed..db0d421e58ab5 100644 --- a/components/microsoft_outlook/actions/send-email/send-email.mjs +++ b/components/microsoft_outlook/actions/send-email/send-email.mjs @@ -3,7 +3,7 @@ import microsoftOutlook from "../../microsoft_outlook.app.mjs"; export default { type: "action", key: "microsoft_outlook-send-email", - version: "0.0.8", + version: "0.0.11", name: "Send Email", description: "Send an email to one or multiple recipients, [See the docs](https://docs.microsoft.com/en-us/graph/api/user-sendmail)", props: { diff --git a/components/microsoft_outlook/actions/update-contact/update-contact.mjs b/components/microsoft_outlook/actions/update-contact/update-contact.mjs index 0e03b22201b33..3e33b9cfbc94e 100644 --- a/components/microsoft_outlook/actions/update-contact/update-contact.mjs +++ b/components/microsoft_outlook/actions/update-contact/update-contact.mjs @@ -3,7 +3,7 @@ import microsoftOutlook from "../../microsoft_outlook.app.mjs"; export default { type: "action", key: "microsoft_outlook-update-contact", - version: "0.0.7", + version: "0.0.10", name: "Update Contact", description: "Add a contact to the root Contacts folder, [See the docs](https://docs.microsoft.com/en-us/graph/api/user-post-contacts)", props: { diff --git a/components/microsoft_outlook/microsoft_outlook.app.mjs b/components/microsoft_outlook/microsoft_outlook.app.mjs index dddde09063d63..0542d02b3a5f0 100644 --- a/components/microsoft_outlook/microsoft_outlook.app.mjs +++ b/components/microsoft_outlook/microsoft_outlook.app.mjs @@ -100,6 +100,63 @@ export default { type: "object", optional: true, }, + label: { + type: "string", + label: "Label", + description: "The name of the label/category to add", + async options({ + messageId, excludeMessageLabels, onlyMessageLabels, + }) { + const { value } = await this.listLabels(); + let labels = value; + if (messageId) { + const { categories } = await this.getMessage({ + messageId, + }); + labels = excludeMessageLabels + ? labels.filter(({ displayName }) => !categories.includes(displayName)) + : onlyMessageLabels + ? labels.filter(({ displayName }) => categories.includes(displayName)) + : labels; + } + return labels?.map(({ displayName }) => displayName) || []; + }, + }, + messageId: { + type: "string", + label: "Message ID", + description: "The identifier of the message to update", + async options({ page }) { + const limit = 50; + const { value } = await this.listMessages({ + params: { + $top: limit, + $skip: limit * page, + $orderby: "createdDateTime desc", + }, + }); + return value?.map(({ + id: value, subject: label, + }) => ({ + value, + label, + })) || []; + }, + }, + folderIds: { + type: "string[]", + label: "Folder IDs to Monitor", + description: "Specify the folder IDs or names in Outlook that you want to monitor for new emails. Leave empty to monitor all folders (excluding \"Sent Items\" and \"Drafts\").", + async options() { + const { value: folders } = await this.listFolders(); + return folders?.map(({ + id: value, displayName: label, + }) => ({ + value, + label, + })) || []; + }, + }, }, methods: { _getUrl(path) { @@ -286,5 +343,35 @@ export default { ...args, }); }, + listLabels(args = {}) { + return this._makeRequest({ + path: "/me/outlook/masterCategories", + ...args, + }); + }, + listFolders(args = {}) { + return this._makeRequest({ + path: "/me/mailFolders", + ...args, + }); + }, + moveMessage({ + messageId, ...args + }) { + return this._makeRequest({ + method: "POST", + path: `/me/messages/${messageId}/move`, + ...args, + }); + }, + updateMessage({ + messageId, ...args + }) { + return this._makeRequest({ + method: "PATCH", + path: `/me/messages/${messageId}`, + ...args, + }); + }, }, }; diff --git a/components/microsoft_outlook/package.json b/components/microsoft_outlook/package.json index a392c43a2ae96..872354b35d81d 100644 --- a/components/microsoft_outlook/package.json +++ b/components/microsoft_outlook/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/microsoft_outlook", - "version": "1.0.3", + "version": "1.3.0", "description": "Pipedream Microsoft Outlook Components", "main": "microsoft_outlook.app.mjs", "keywords": [ @@ -12,8 +12,10 @@ "homepage": "https://pipedream.com/apps/microsoft_outlook", "author": "Pipedream (https://pipedream.com/)", "dependencies": { + "@pipedream/platform": "^3.0.3", "axios": "^0.21.1", "js-base64": "^3.7.2", + "md5": "^2.3.0", "mime-types": "^2.1.35" }, "publishConfig": { diff --git a/components/microsoft_outlook/sources/new-contact/new-contact.mjs b/components/microsoft_outlook/sources/new-contact/new-contact.mjs index dd9579df69308..d1fe6815e160b 100644 --- a/components/microsoft_outlook/sources/new-contact/new-contact.mjs +++ b/components/microsoft_outlook/sources/new-contact/new-contact.mjs @@ -5,7 +5,7 @@ export default { key: "microsoft_outlook-new-contact", name: "New Contact Event (Instant)", description: "Emit new event when a new Contact is created", - version: "0.0.8", + version: "0.0.11", type: "source", hooks: { ...common.hooks, diff --git a/components/microsoft_outlook/sources/new-email/new-email.mjs b/components/microsoft_outlook/sources/new-email/new-email.mjs index 5b1da7b4c0f64..b5eca7cc7064f 100644 --- a/components/microsoft_outlook/sources/new-email/new-email.mjs +++ b/components/microsoft_outlook/sources/new-email/new-email.mjs @@ -1,19 +1,45 @@ import common from "../common.mjs"; +import md5 from "md5"; import sampleEmit from "./test-event.mjs"; export default { ...common, key: "microsoft_outlook-new-email", name: "New Email Event (Instant)", - description: "Emit new event when an email received", - version: "0.0.9", + description: "Emit new event when an email is received in specified folders.", + version: "0.0.14", type: "source", + dedupe: "unique", + props: { + ...common.props, + folderIds: { + propDefinition: [ + common.props.microsoftOutlook, + "folderIds", + ], + optional: true, + }, + }, hooks: { ...common.hooks, + async deploy() { + this.db.set("sentItemFolderId", await this.getFolderIdByName("Sent Items")); + this.db.set("draftsFolderId", await this.getFolderIdByName("Drafts")); + + const events = await this.getSampleEvents({ + pageSize: 25, + }); + if (!events || events.length == 0) { + return; + } + for (const item of events) { + this.emitEvent(item); + } + }, async activate() { await this.activate({ changeType: "created", - resource: "/me/mailfolders('inbox')/messages", + resource: "/me/messages", }); }, async deactivate() { @@ -22,37 +48,81 @@ export default { }, methods: { ...common.methods, + async getFolderIdByName(name) { + const { value: folders } = await this.microsoftOutlook.listFolders(); + const folder = folders.find(({ displayName }) => displayName === name); + return folder?.id; + }, async getSampleEvents({ pageSize }) { - return this.microsoftOutlook.listMessages({ - params: { - $top: pageSize, - $orderby: "createdDateTime desc", - }, - }); + const folders = this.folderIds?.length + ? this.folderIds.map((id) => `/me/mailFolders/${id}/messages`) + : [ + "/me/messages", + ]; + + const results = []; + for (const folder of folders) { + const { value: messages } = await this.microsoftOutlook.listMessages({ + resource: folder, + params: { + $top: pageSize, + $orderby: "createdDateTime desc", + }, + }); + results.push(...messages); + } + return results; + }, + isRelevant(item) { + if (this.folderIds?.length) { + return this.folderIds.includes(item.parentFolderId); + } + // if no folderIds are specified, filter out items in Sent Items & Drafts + const sentItemFolderId = this.db.get("sentItemFolderId"); + const draftsFolderId = this.db.get("draftsFolderId"); + return item.parentFolderId !== sentItemFolderId && item.parentFolderId !== draftsFolderId; }, emitEvent(item) { - this.$emit({ - email: item, - }, this.generateMeta(item)); + if (this.isRelevant(item)) { + this.$emit( + { + email: item, + }, + this.generateMeta(item), + ); + } }, generateMeta(item) { return { - id: item.id, + id: md5(item.id), // id > 64 characters, so dedupe on hash of id summary: `New email (ID:${item.id})`, ts: Date.parse(item.createdDateTime), }; }, }, async run(event) { - await this.run({ - event, - emitFn: async ({ resourceId } = {}) => { - const item = await this.microsoftOutlook.getMessage({ - messageId: resourceId, - }); - this.emitEvent(item); - }, - }); + const folders = this.folderIds?.length + ? this.folderIds.map((id) => `/me/mailFolders/${id}/messages`) + : [ + "/me/messages", + ]; + + for (const folder of folders) { + await this.run({ + event, + emitFn: async ({ resourceId } = {}) => { + try { + const item = await this.microsoftOutlook.getMessage({ + resource: folder, + messageId: resourceId, + }); + this.emitEvent(item); + } catch { + console.log(`Could not fetch message with ID: ${resourceId}`); + } + }, + }); + } }, sampleEmit, }; diff --git a/components/microsoft_outlook_calendar/README.md b/components/microsoft_outlook_calendar/README.md index 1e13a9f727eaf..60d248a18781c 100644 --- a/components/microsoft_outlook_calendar/README.md +++ b/components/microsoft_outlook_calendar/README.md @@ -1,5 +1,11 @@ # Overview -With the Microsoft Outlook API, you can build a wide range of applications and -services that work with Outlook. The Microsoft Outlook Calendar app helps you -manage your calendar and schedule appointments. +The Microsoft Outlook Calendar API provides programmatic access to a user's calendar events, allowing for the creation, retrieval, update, and deletion of events within Outlook calendars. With Pipedream, you can integrate these calendar operations into workflows that automate tasks involving scheduling, event management, and coordination with other services. Whether it's triggering actions when new events are created, syncing calendar events with other scheduling tools, or managing attendees, Pipedream's serverless platform enables you to build custom automations with minimal overhead. + +# Example Use Cases + +- **Automated Event Reminder Emails**: Trigger a Pipedream workflow whenever a new event is added to an Outlook Calendar. The workflow could send a reminder email via a service like SendGrid or Gmail to all the attendees a day before the event occurs, ensuring everyone is prepped and on time. + +- **Cross-Platform Calendar Sync**: Sync events between Microsoft Outlook Calendar and Google Calendar. When an event is created or updated in Outlook, a Pipedream workflow can create or update a corresponding event in Google Calendar. This ensures that users who operate across different platforms have unified schedules. + +- **Meeting Room Availability Checker**: Monitor your Outlook Calendar for new meetings and automatically check the availability of meeting rooms using a service like Google's G Suite Admin SDK. If a room is available, the workflow reserves it and updates the event with the room's details. If not, it could notify the organizer to select a different room or time. diff --git a/components/microsoft_outlook_calendar/actions/get-schedule/get-schedule.mjs b/components/microsoft_outlook_calendar/actions/get-schedule/get-schedule.mjs new file mode 100644 index 0000000000000..1538441a0298b --- /dev/null +++ b/components/microsoft_outlook_calendar/actions/get-schedule/get-schedule.mjs @@ -0,0 +1,71 @@ +import microsoftOutlook from "../../microsoft_outlook_calendar.app.mjs"; + +export default { + key: "microsoft_outlook_calendar-get-schedule", + name: "Get Free/Busy Schedule", + description: "Get the free/busy availability information for a collection of users, distributions lists, or resources (rooms or equipment) for a specified time period. [See the documentation](https://learn.microsoft.com/en-us/graph/api/calendar-getschedule)", + version: "0.0.2", + type: "action", + props: { + microsoftOutlook, + schedules: { + type: "string[]", + label: "Emails", + description: "A list of emails of users, distribution lists, or resources. For example: `[ \"adelev@contoso.com\" , \"meganb@contoso.com\" ]`", + }, + start: { + propDefinition: [ + microsoftOutlook, + "start", + ], + }, + end: { + propDefinition: [ + microsoftOutlook, + "end", + ], + }, + timeZone: { + propDefinition: [ + microsoftOutlook, + "timeZone", + ], + }, + availabilityViewInterval: { + type: "integer", + label: "Availability View Interval", + description: "Represents the duration of a time slot in minutes in an availabilityView in the response. The default is 30 minutes, minimum is 5, maximum is 1440.", + optional: true, + }, + }, + methods: { + getSchedule(opts = {}) { + return this.microsoftOutlook._makeRequest({ + method: "POST", + path: "/me/calendar/getSchedule", + ...opts, + }); + }, + }, + async run({ $ }) { + const { value } = await this.getSchedule({ + $, + data: { + schedules: this.schedules, + startTime: { + dateTime: this.start, + timeZone: this.timeZone, + }, + endTime: { + dateTime: this.end, + timeZone: this.timeZone, + }, + availabilityViewInterval: this.availabilityViewInterval, + }, + }); + + $.export("$summary", `Successfully retrieved schedules for \`${this.schedules.join("`, `")}\``); + + return value; + }, +}; diff --git a/components/microsoft_outlook_calendar/actions/list-events/list-events.mjs b/components/microsoft_outlook_calendar/actions/list-events/list-events.mjs new file mode 100644 index 0000000000000..6f27f7e43b8b6 --- /dev/null +++ b/components/microsoft_outlook_calendar/actions/list-events/list-events.mjs @@ -0,0 +1,47 @@ +import microsoftOutlook from "../../microsoft_outlook_calendar.app.mjs"; + +export default { + key: "microsoft_outlook_calendar-list-events", + name: "List Events", + description: "Get a list of event objects in the user's mailbox. [See the documentation](https://learn.microsoft.com/en-us/graph/api/user-list-events)", + version: "0.0.2", + type: "action", + props: { + microsoftOutlook, + filter: { + type: "string", + label: "Filter", + description: "Filters results. For example, `contains(subject, 'meet for lunch?')` will include events whose title contains ‘meet for lunch?’. [See documentation](https://learn.microsoft.com/en-us/graph/filter-query-parameter) for the full list of operations.", + optional: true, + }, + orderBy: { + type: "string", + label: "Order By", + description: "Orders results. For example, `displayName desc` will sort the results by Display Name in decending order.", + default: "createdDateTime desc", + optional: true, + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "The maximum number of results to return", + optional: true, + }, + }, + async run({ $ }) { + const { value = [] } = await this.microsoftOutlook.listCalendarEvents({ + $, + params: { + "$orderby": this.orderBy, + "$filter": this.filter, + "$top": this.maxResults, + }, + }); + + $.export("$summary", `Successfully retrieved ${value.length} event${value.length === 1 + ? "" + : "s"}`); + + return value; + }, +}; diff --git a/components/microsoft_outlook_calendar/package.json b/components/microsoft_outlook_calendar/package.json index 063bbda238371..2f8c6d0156821 100644 --- a/components/microsoft_outlook_calendar/package.json +++ b/components/microsoft_outlook_calendar/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/microsoft_outlook_calendar", - "version": "0.2.0", + "version": "0.3.1", "description": "Pipedream Microsoft Outlook Calendar Components", "main": "microsoft_outlook_calendar.app.mjs", "keywords": [ @@ -11,10 +11,10 @@ ], "homepage": "https://pipedream.com/apps/microsoft_outlook_calendar", "author": "Pipedream (https://pipedream.com/)", - "dependencies": { - "@pipedream/platform": "^1.5.1" - }, "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/microsoft_power_bi/README.md b/components/microsoft_power_bi/README.md new file mode 100644 index 0000000000000..7fdc904629850 --- /dev/null +++ b/components/microsoft_power_bi/README.md @@ -0,0 +1,12 @@ +# Overview + +The Microsoft Power BI API allows you to interact with your Power BI assets programmatically. With this API, you can embed your reports and dashboards into applications, manage Power BI datasets, push data into datasets for real-time dashboard updates, and automate your reporting workflows. On Pipedream, you can use this API to create intricate workflows that react to various triggers, like webhooks or schedules, and perform actions like refreshing datasets, posting to datasets, and managing Power BI assets. + +# Example Use Cases + +- **Automated Data Refresh**: Trigger a dataset refresh in Power BI whenever new data is received from a web service or when a file is uploaded to a cloud storage service like Google Drive or Dropbox. This keeps your Power BI dashboards updated with the latest data without manual intervention. + +- **Real-Time Dashboard Updates**: Push data to a Power BI streaming dataset from various sources like IoT devices, payment gateways, or CRMs such as Salesforce. Every time a new event occurs, the workflow pushes the data to Power BI, allowing for real-time dashboard updates and analytics. + +- **Dynamic Report Generation**: Generate and distribute custom Power BI reports automatically based on user actions or time-based triggers. For instance, on the completion of a project, trigger a workflow that fetches relevant data from a project management tool like Asana, generates a report in Power BI, and then emails it to stakeholders. +. diff --git a/components/microsoft_power_bi/actions/add-rows-dataset-table/add-rows-dataset-table.mjs b/components/microsoft_power_bi/actions/add-rows-dataset-table/add-rows-dataset-table.mjs index 2d26ab37089f4..7efb1937b488c 100644 --- a/components/microsoft_power_bi/actions/add-rows-dataset-table/add-rows-dataset-table.mjs +++ b/components/microsoft_power_bi/actions/add-rows-dataset-table/add-rows-dataset-table.mjs @@ -5,7 +5,7 @@ export default { key: "microsoft_power_bi-add-rows-dataset-table", name: "Add Rows to Dataset Table", description: "Adds new data rows to the specified table within the specified dataset from My workspace. [See the documentation](https://learn.microsoft.com/en-us/rest/api/power-bi/push-datasets/datasets-post-rows)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { powerBi, diff --git a/components/microsoft_power_bi/actions/cancel-refresh/cancel-refresh.mjs b/components/microsoft_power_bi/actions/cancel-refresh/cancel-refresh.mjs index ce7cec150a218..5ee13cc4864cc 100644 --- a/components/microsoft_power_bi/actions/cancel-refresh/cancel-refresh.mjs +++ b/components/microsoft_power_bi/actions/cancel-refresh/cancel-refresh.mjs @@ -4,7 +4,7 @@ export default { key: "microsoft_power_bi-cancel-refresh", name: "Cancel Dataset Refresh", description: "Cancels a refresh operation for a specified dataset in Power BI. [See the documentation](https://learn.microsoft.com/en-us/rest/api/power-bi/datasets/cancel-refresh)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { powerBi, diff --git a/components/microsoft_power_bi/actions/create-dataset/create-dataset.mjs b/components/microsoft_power_bi/actions/create-dataset/create-dataset.mjs index 2a171eb096b71..0f44fe11f054c 100644 --- a/components/microsoft_power_bi/actions/create-dataset/create-dataset.mjs +++ b/components/microsoft_power_bi/actions/create-dataset/create-dataset.mjs @@ -5,7 +5,7 @@ export default { key: "microsoft_power_bi-create-dataset", name: "Create Dataset", description: "Creates a new Push Dataset in Power BI. [See the documentation](https://learn.microsoft.com/en-us/rest/api/power-bi/push-datasets/datasets-post-dataset)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { powerBi, diff --git a/components/microsoft_power_bi/actions/get-dataset-refresh/get-dataset-refresh.mjs b/components/microsoft_power_bi/actions/get-dataset-refresh/get-dataset-refresh.mjs new file mode 100644 index 0000000000000..353674b078b46 --- /dev/null +++ b/components/microsoft_power_bi/actions/get-dataset-refresh/get-dataset-refresh.mjs @@ -0,0 +1,54 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../microsoft_power_bi.app.mjs"; + +export default { + key: "microsoft_power_bi-get-dataset-refresh", + name: "Get Dataset Refresh", + description: "Triggers a refresh operation for a specified Power BI dataset. [See the documentation](https://learn.microsoft.com/en-us/rest/api/power-bi/datasets/get-refresh-history)", + version: "0.0.1", + type: "action", + props: { + app, + datasetId: { + propDefinition: [ + app, + "datasetId", + ], + optional: true, + }, + customDatasetId: { + propDefinition: [ + app, + "customDatasetId", + ], + }, + top: { + type: "integer", + label: "Top", + description: "The number of refresh history items to retrieve.", + optional: true, + }, + }, + async run({ $ }) { + const { + datasetId, + customDatasetId, + top, + } = this; + + if (!datasetId && !customDatasetId) { + throw new ConfigurationError("Must enter one of Dataset ID or custom Dataset ID"); + } + + const response = await this.app.getRefreshHistory({ + $, + datasetId: datasetId || customDatasetId, + params: { + ["$top"]: top, + }, + }); + + $.export("$summary", `Successfully retrieved refresh history for dataset (ID \`${datasetId || customDatasetId}\`)`); + return response; + }, +}; diff --git a/components/microsoft_power_bi/actions/refresh-dataset/refresh-dataset.mjs b/components/microsoft_power_bi/actions/refresh-dataset/refresh-dataset.mjs index a2ddd4bd14955..299b84cde0e2b 100644 --- a/components/microsoft_power_bi/actions/refresh-dataset/refresh-dataset.mjs +++ b/components/microsoft_power_bi/actions/refresh-dataset/refresh-dataset.mjs @@ -4,7 +4,7 @@ export default { key: "microsoft_power_bi-refresh-dataset", name: "Refresh Dataset", description: "Triggers a refresh operation for a specified Power BI dataset. [See the documentation](https://learn.microsoft.com/en-us/rest/api/power-bi/datasets/refresh-dataset)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { powerBi, diff --git a/components/microsoft_power_bi/microsoft_power_bi.app.mjs b/components/microsoft_power_bi/microsoft_power_bi.app.mjs index e9f13132e1b93..e843d510f8e35 100644 --- a/components/microsoft_power_bi/microsoft_power_bi.app.mjs +++ b/components/microsoft_power_bi/microsoft_power_bi.app.mjs @@ -60,10 +60,10 @@ export default { })); }, }, - top: { - type: "integer", - label: "Top", - description: "The number of refresh history items to retrieve.", + customDatasetId: { + type: "string", + label: "Custom Dataset ID", + description: "You may enter a Dataset ID directly. Either Dataset ID or Custom Dataset ID must be entered.", optional: true, }, }, @@ -71,7 +71,7 @@ export default { _baseUrl() { return "https://api.powerbi.com/v1.0/myorg"; }, - async _makeRequest({ + _makeRequest({ $ = this, path, headers, ...otherOpts }) { return axios($, { @@ -84,7 +84,7 @@ export default { }, }); }, - async addRowsToTable({ + addRowsToTable({ datasetId, tableName, ...args }) { return this._makeRequest({ @@ -93,7 +93,7 @@ export default { ...args, }); }, - async refreshDataset({ + refreshDataset({ datasetId, ...args }) { return this._makeRequest({ @@ -102,7 +102,7 @@ export default { ...args, }); }, - async cancelRefresh({ + cancelRefresh({ datasetId, refreshId, ...args }) { return this._makeRequest({ @@ -145,11 +145,5 @@ export default { ...args, }); }, - async emitEvent(eventName, data) { - this.$emit(data, { - summary: eventName, - id: data.id, - }); - }, }, }; diff --git a/components/microsoft_power_bi/package.json b/components/microsoft_power_bi/package.json index f07e00758710a..e2f0574bdc92e 100644 --- a/components/microsoft_power_bi/package.json +++ b/components/microsoft_power_bi/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/microsoft_power_bi", - "version": "0.2.1", + "version": "0.3.0", "description": "Pipedream Microsoft Power BI Components", "main": "microsoft_power_bi.app.mjs", "keywords": [ diff --git a/components/microsoft_power_bi/sources/common.mjs b/components/microsoft_power_bi/sources/common.mjs index fec3081eca3ab..2ba1aecef1022 100644 --- a/components/microsoft_power_bi/sources/common.mjs +++ b/components/microsoft_power_bi/sources/common.mjs @@ -15,10 +15,10 @@ export default { optional: true, }, customDatasetId: { - type: "string", - label: "Custom Dataset ID", - description: "You may enter a Dataset ID directly. Either Dataset ID or Custom Dataset ID must be entered.", - optional: true, + propDefinition: [ + powerBiApp, + "customDatasetId", + ], }, timer: { type: "$.interface.timer", diff --git a/components/microsoft_power_bi/sources/dataset-refresh-completed/dataset-refresh-completed.mjs b/components/microsoft_power_bi/sources/dataset-refresh-completed/dataset-refresh-completed.mjs index e3026efcd0702..3b2fb6e203693 100644 --- a/components/microsoft_power_bi/sources/dataset-refresh-completed/dataset-refresh-completed.mjs +++ b/components/microsoft_power_bi/sources/dataset-refresh-completed/dataset-refresh-completed.mjs @@ -4,7 +4,7 @@ export default { key: "microsoft_power_bi-dataset-refresh-completed", name: "Dataset Refresh Completed", description: "Emits a new event when a dataset refresh operation has completed. [See the documentation](https://learn.microsoft.com/en-us/rest/api/power-bi/datasets/get-refresh-history)", - version: "0.0.3", + version: "0.0.4", type: "source", dedupe: "unique", ...common, diff --git a/components/microsoft_power_bi/sources/dataset-refresh-failed/dataset-refresh-failed.mjs b/components/microsoft_power_bi/sources/dataset-refresh-failed/dataset-refresh-failed.mjs index fe6c862fbb454..d215bebdbb08c 100644 --- a/components/microsoft_power_bi/sources/dataset-refresh-failed/dataset-refresh-failed.mjs +++ b/components/microsoft_power_bi/sources/dataset-refresh-failed/dataset-refresh-failed.mjs @@ -4,7 +4,7 @@ export default { key: "microsoft_power_bi-dataset-refresh-failed", name: "Dataset Refresh Failed", description: "Emits an event when a dataset refresh operation has failed in Power BI. [See the documentation](https://learn.microsoft.com/en-us/rest/api/power-bi/datasets/get-refresh-history)", - version: "0.0.3", + version: "0.0.4", type: "source", dedupe: "unique", ...common, diff --git a/components/microsoft_power_bi/sources/new-dataset-refresh-created/new-dataset-refresh-created.mjs b/components/microsoft_power_bi/sources/new-dataset-refresh-created/new-dataset-refresh-created.mjs new file mode 100644 index 0000000000000..9e8222c6697a9 --- /dev/null +++ b/components/microsoft_power_bi/sources/new-dataset-refresh-created/new-dataset-refresh-created.mjs @@ -0,0 +1,20 @@ +import common from "../common.mjs"; + +export default { + key: "microsoft_power_bi-new-dataset-refresh-created", + name: "New Dataset Refresh Created", + description: "Emit new event when a new dataset refresh operation is created. [See the documentation](https://learn.microsoft.com/en-us/rest/api/power-bi/datasets/get-refresh-history)", + version: "0.0.1", + type: "source", + dedupe: "unique", + ...common, + methods: { + ...common.methods, + checkStatus() { + return true; + }, + getSummary() { + return "New Refresh Created"; + }, + }, +}; diff --git a/components/microsoft_sql_server/README.md b/components/microsoft_sql_server/README.md new file mode 100644 index 0000000000000..b32d531684923 --- /dev/null +++ b/components/microsoft_sql_server/README.md @@ -0,0 +1,11 @@ +# Overview + +With the Microsoft SQL Server API on Pipedream, you can automate your database operations and create powerful integrations. This API allows you to execute queries, manage databases, and trigger actions based on data changes. Implementing workflows that react to database events, manipulate data, or synchronize SQL Server data with other services, becomes effortless with Pipedream's serverless platform. + +# Example Use Cases + +- **Automated Data Backup**: Create a workflow that triggers on a schedule to back up your SQL Server databases. Use Pipedream's built-in cron scheduler to run a SQL query that exports data to a secure location, such as an AWS S3 bucket, ensuring your data is regularly archived without manual intervention. + +- **Real-Time Sales Dashboard**: Connect SQL Server to a dashboard app like Geckoboard. Use a workflow that runs SQL queries at regular intervals to fetch the latest sales data, then pushes that data to the dashboard for a real-time display of key sales metrics, giving your team immediate insights to drive decisions. + +- **Email Alerts for Low Inventory**: Pair SQL Server with an email service like SendGrid. Set up a workflow that monitors inventory levels and, when stock for a particular item falls below a threshold, automatically sends an email alert to the procurement team, enabling timely restocking and avoiding stockouts. diff --git a/components/microsoft_sql_server/actions/execute-query/execute-query.mjs b/components/microsoft_sql_server/actions/execute-query/execute-query.mjs index 802403cb61b56..265bcffaa2056 100644 --- a/components/microsoft_sql_server/actions/execute-query/execute-query.mjs +++ b/components/microsoft_sql_server/actions/execute-query/execute-query.mjs @@ -5,7 +5,7 @@ export default { name: "Execute Query", description: "Executes a SQL query and returns the results. [See the documentation](https://learn.microsoft.com/en-us/sql/t-sql/queries/select-transact-sql?view=sql-server-ver16)", type: "action", - version: "0.0.1", + version: "0.0.4", props: { app, query: { @@ -21,18 +21,17 @@ export default { optional: true, }, }, - run({ $: step }) { + async run({ $: step }) { const { app, inputs, query, } = this; - - return app.executeQuery({ - step, + const response = await app.executeQuery({ query, inputs, - summary: () => "Successfully executed query.", }); + step.export("$summary", "Successfully executed query."); + return response; }, }; diff --git a/components/microsoft_sql_server/actions/execute-raw-query/execute-raw-query.mjs b/components/microsoft_sql_server/actions/execute-raw-query/execute-raw-query.mjs new file mode 100644 index 0000000000000..2e2911e9fcf7a --- /dev/null +++ b/components/microsoft_sql_server/actions/execute-raw-query/execute-raw-query.mjs @@ -0,0 +1,26 @@ +import app from "../../microsoft_sql_server.app.mjs"; + +export default { + key: "microsoft_sql_server-execute-raw-query", + name: "Execute SQL Query", + description: "Execute a custom SQL query. See [our docs](https://pipedream.com/docs/databases/working-with-sql) to learn more about working with SQL in Pipedream.", + type: "action", + version: "0.0.3", + props: { + app, + // eslint-disable-next-line pipedream/props-description + sql: { + type: "sql", + auth: { + app: "app", + }, + label: "SQL Query", + }, + }, + async run({ $ }) { + const args = this.app.executeQueryAdapter(this.sql); + const response = await this.app.executeQuery(args); + $.export("$summary", "Successfully executed query."); + return response; + }, +}; diff --git a/components/microsoft_sql_server/actions/insert-row/insert-row.mjs b/components/microsoft_sql_server/actions/insert-row/insert-row.mjs index 0c74bf8099b93..6e7e79bc3cbba 100644 --- a/components/microsoft_sql_server/actions/insert-row/insert-row.mjs +++ b/components/microsoft_sql_server/actions/insert-row/insert-row.mjs @@ -5,7 +5,7 @@ export default { name: "Insert Row", description: "Inserts a new row in a table. [See the documentation](https://learn.microsoft.com/en-us/sql/t-sql/statements/insert-transact-sql?view=sql-server-ver16)", type: "action", - version: "0.0.1", + version: "0.0.4", props: { app, table: { @@ -37,18 +37,18 @@ export default { }, }), {}); }, - run({ $: step }) { + async run({ $: step }) { const { app, table, ...inputs } = this; - return app.insertRow({ - step, + const response = await app.insertRow({ table, inputs, - summary: () => "Successfully inserted row.", }); + step.export("$summary", "Successfully inserted row."); + return response; }, }; diff --git a/components/microsoft_sql_server/microsoft_sql_server.app.mjs b/components/microsoft_sql_server/microsoft_sql_server.app.mjs index 4f65737e2c433..322eb5c113d0c 100644 --- a/components/microsoft_sql_server/microsoft_sql_server.app.mjs +++ b/components/microsoft_sql_server/microsoft_sql_server.app.mjs @@ -1,5 +1,9 @@ import mssql from "mssql"; -import { ConfigurationError } from "@pipedream/platform"; +import { + sqlProxy, + sqlProp, + ConfigurationError, +} from "@pipedream/platform"; import constants from "./common/constants.mjs"; import utils from "./common/utils.mjs"; @@ -29,15 +33,17 @@ export default { }, }, methods: { + ...sqlProxy.methods, + ...sqlProp.methods, exportSummary(step) { if (!step?.export) { - throw new ConfigurationError("The summary method should be bind to the step object aka `$`"); + throw new ConfigurationError("The summary method should be bound to the step object aka `$`"); } return (msg = "") => step.export(constants.SUMMARY_LABEL, msg); }, getConfig() { const { - host, port, username, password, database, encrypt, trustServerCertificate, + host, port, username, password, database, trustServerCertificate, encrypt, } = this.$auth; return { user: username, @@ -56,25 +62,106 @@ export default { getConnection() { return mssql.connect(this.getConfig()); }, - async executeQuery({ - step = this, summary, query = "", inputs = {}, - } = {}) { + /** + * A helper method to get the schema of the database. Used by other features + * (like the `sql` prop) to enrich the code editor and provide the user with + * auto-complete and fields suggestion. + * + * @returns {DbInfo} The schema of the database, which is a + * JSON-serializable object. + */ + async getSchema() { + const sql = ` + SELECT t.TABLE_SCHEMA AS tableSchema, + t.TABLE_NAME AS tableName, + c.COLUMN_NAME AS columnName, + c.DATA_TYPE AS dataType, + c.IS_NULLABLE AS isNullable, + c.COLUMN_DEFAULT AS columnDefault + FROM INFORMATION_SCHEMA.TABLES AS t + JOIN INFORMATION_SCHEMA.COLUMNS AS c ON t.TABLE_NAME = c.TABLE_NAME + AND t.TABLE_SCHEMA = c.TABLE_SCHEMA + WHERE t.TABLE_TYPE = 'BASE TABLE' + ORDER BY t.TABLE_NAME, + c.ORDINAL_POSITION + `; + const { recordset: rows } = await this.executeQuery({ + query: sql, + }); + return rows.reduce((acc, row) => { + const key = `${row.tableSchema}.${row.tableName}`; + acc[key] ??= { + metadata: { + rowCount: row.rowCount, + }, + schema: {}, + }; + acc[key].schema[row.columnName] = { + ...row, + }; + return acc; + }, {}); + }, + /** + * Adapts the arguments to `executeQuery` so that they can be consumed by + * the SQL proxy (when applicable). Note that this method is not intended to + * be used by the component directly. + * @param {object} preparedStatement The prepared statement to be sent to the DB. + * @param {string} preparedStatement.sql The prepared SQL query to be executed. + * @param {string[]} preparedStatement.values The values to replace in the SQL query. + * @returns {object} - The adapted query and parameters. + */ + proxyAdapter(preparedStatement = {}) { + const { query } = preparedStatement; + const inputs = preparedStatement?.inputs || {}; + for (const [ + key, + value, + ] of Object.entries(inputs)) { + query.replaceAll(`@${key}`, value); + } + return { + query, + params: [], + }; + }, + /** + * A method that performs the inverse transformation of `proxyAdapter`. + * + * @param {object} proxyArgs - The output of `proxyAdapter`. + * @param {string} proxyArgs.query - The SQL query to be executed. + * @param {string[]} proxyArgs.params - The values to replace in the SQL + * query. + * @returns {object} - The adapted query and parameters, compatible with + * `executeQuery`. + */ + executeQueryAdapter(proxyArgs = {}) { + let { query } = proxyArgs; + const params = proxyArgs?.params || []; + for (const param of params) { + query = query.replace("?", param); + } + query = query.replaceAll("\n", " "); + return { + query, + inputs: {}, + }; + }, + getClientConfiguration() { + return this.getConfig(); + }, + async executeQuery(preparedStatement = {}) { let connection; - + const { query } = preparedStatement; + const inputs = preparedStatement?.inputs || {}; try { - connection = await this.getConnection(); - + connection = await mssql.connect(this.getClientConfiguration()); const input = Object.entries(inputs) .reduce((req, inputArgs) => req.input(...inputArgs), connection.request()); - const response = await input.query(query); - if (typeof summary === "function") { - this.exportSummary(step)(summary(response)); - } - return response; } catch (error) { diff --git a/components/microsoft_sql_server/package.json b/components/microsoft_sql_server/package.json index 5a66a643121c0..4a36d5070cac6 100644 --- a/components/microsoft_sql_server/package.json +++ b/components/microsoft_sql_server/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/microsoft_sql_server", - "version": "0.1.1", + "version": "0.1.4", "description": "Pipedream Microsoft SQL Server Components", "main": "microsoft_sql_server.app.mjs", "keywords": [ @@ -10,7 +10,7 @@ "homepage": "https://pipedream.com/apps/microsoft_sql_server", "author": "Pipedream (https://pipedream.com/)", "dependencies": { - "@pipedream/platform": "^1.5.1", + "@pipedream/platform": "^3.0.0", "mssql": "^10.0.1" }, "publishConfig": { diff --git a/components/microsoft_sql_server/sources/new-column/new-column.mjs b/components/microsoft_sql_server/sources/new-column/new-column.mjs index a50ef01be77ed..08fd759fbd255 100644 --- a/components/microsoft_sql_server/sources/new-column/new-column.mjs +++ b/components/microsoft_sql_server/sources/new-column/new-column.mjs @@ -8,7 +8,7 @@ export default { name: "New Column", description: "Triggers when a new column is added to a table. [See the documentation](https://learn.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-columns-transact-sql?view=sql-server-ver16)", type: "source", - version: "0.0.2", + version: "0.0.5", dedupe: "unique", props: { ...common.props, diff --git a/components/microsoft_sql_server/sources/new-or-updated-row/new-or-updated-row.mjs b/components/microsoft_sql_server/sources/new-or-updated-row/new-or-updated-row.mjs index 520390d6bac2d..46ac8b44c2528 100644 --- a/components/microsoft_sql_server/sources/new-or-updated-row/new-or-updated-row.mjs +++ b/components/microsoft_sql_server/sources/new-or-updated-row/new-or-updated-row.mjs @@ -7,7 +7,7 @@ export default { name: "New Or Updated Row", description: "Triggers when a new row is added or an existing row is updated. [See the documentation](https://learn.microsoft.com/en-us/sql/t-sql/queries/select-transact-sql?view=sql-server-ver16)", type: "source", - version: "0.0.2", + version: "0.0.5", dedupe: "unique", props: { ...common.props, diff --git a/components/microsoft_teams/README.md b/components/microsoft_teams/README.md index ef15d2f586394..5678b91867e4a 100644 --- a/components/microsoft_teams/README.md +++ b/components/microsoft_teams/README.md @@ -1,7 +1,11 @@ # Overview -With the Microsoft Teams API, you can build a variety of applications and -integrations that work with Teams. For example, you could build a bot that -responds to questions from users, or an app that lets users add information to -a shared Teams document. You could also build an integration that lets users -interact with an external service right from within Teams. +The Microsoft Teams API on Pipedream allows you to automate tasks, streamline communication, and integrate with other services to enhance the functionality of Teams as a collaboration hub. With this API, you can send messages to channels, orchestrate complex workflows based on Teams events, and manage Teams' settings programmatically. + +# Example Use Cases + +- **Automated Daily Stand-up Notifications**: Trigger a workflow that sends a daily message to a designated Teams channel prompting team members to post their stand-up updates. This ensures consistency and regularity in team communication. + +- **New Lead Alert System**: Connect the Teams API with a CRM app on Pipedream. Each time a new lead is captured in the CRM, the workflow automatically posts a summary to a sales channel in Teams, keeping the sales team instantly informed. + +- **Incident Management Updates**: Integrate the Teams API with a monitoring tool like Datadog. When an incident is detected, the workflow triggers, creating a new conversation in the relevant Teams channel and tagging on-call staff to coordinate a rapid response. diff --git a/components/microsoft_teams/actions/create-channel/create-channel.mjs b/components/microsoft_teams/actions/create-channel/create-channel.mjs index 771d57d040af0..844c59d96d475 100644 --- a/components/microsoft_teams/actions/create-channel/create-channel.mjs +++ b/components/microsoft_teams/actions/create-channel/create-channel.mjs @@ -5,7 +5,7 @@ export default { name: "Create Channel", description: "Create a new channel in Microsoft Teams. [See the docs here](https://docs.microsoft.com/en-us/graph/api/channel-post?view=graph-rest-1.0&tabs=http)", type: "action", - version: "0.0.5", + version: "0.0.8", props: { microsoftTeams, teamId: { diff --git a/components/microsoft_teams/actions/list-channels/list-channels.mjs b/components/microsoft_teams/actions/list-channels/list-channels.mjs index 2519664fd3981..3451cb7b2a14b 100644 --- a/components/microsoft_teams/actions/list-channels/list-channels.mjs +++ b/components/microsoft_teams/actions/list-channels/list-channels.mjs @@ -5,7 +5,7 @@ export default { name: "List Channels", description: "Lists all channels in a Microsoft Team. [See the docs here](https://docs.microsoft.com/en-us/graph/api/channel-list?view=graph-rest-1.0&tabs=http)", type: "action", - version: "0.0.5", + version: "0.0.8", props: { microsoftTeams, teamId: { diff --git a/components/microsoft_teams/actions/list-shifts/list-shifts.mjs b/components/microsoft_teams/actions/list-shifts/list-shifts.mjs index e2531dc0cd02d..2af018585a8ac 100644 --- a/components/microsoft_teams/actions/list-shifts/list-shifts.mjs +++ b/components/microsoft_teams/actions/list-shifts/list-shifts.mjs @@ -5,7 +5,7 @@ export default { name: "List Shifts", description: "Get the list of shift instances for a team. [See the documentation](https://learn.microsoft.com/en-us/graph/api/schedule-list-shifts?view=graph-rest-1.0&tabs=http)", type: "action", - version: "0.0.2", + version: "0.0.5", props: { microsoftTeams, teamId: { diff --git a/components/microsoft_teams/actions/send-channel-message/send-channel-message.mjs b/components/microsoft_teams/actions/send-channel-message/send-channel-message.mjs index 476b8b3a6b133..9ed69e54b1190 100644 --- a/components/microsoft_teams/actions/send-channel-message/send-channel-message.mjs +++ b/components/microsoft_teams/actions/send-channel-message/send-channel-message.mjs @@ -5,7 +5,7 @@ export default { name: "Send Channel Message", description: "Send a message to a team's channel. [See the docs here](https://docs.microsoft.com/en-us/graph/api/channel-post-messages?view=graph-rest-1.0&tabs=http)", type: "action", - version: "0.0.5", + version: "0.0.8", props: { microsoftTeams, teamId: { @@ -29,12 +29,19 @@ export default { "message", ], }, + contentType: { + propDefinition: [ + microsoftTeams, + "contentType", + ], + }, }, async run({ $ }) { const { teamId, channelId, message, + contentType, } = this; const response = @@ -44,6 +51,7 @@ export default { content: { body: { content: message, + contentType, }, }, }); diff --git a/components/microsoft_teams/actions/send-chat-message/send-chat-message.mjs b/components/microsoft_teams/actions/send-chat-message/send-chat-message.mjs index fbc854f75e1a0..265adb34126a2 100644 --- a/components/microsoft_teams/actions/send-chat-message/send-chat-message.mjs +++ b/components/microsoft_teams/actions/send-chat-message/send-chat-message.mjs @@ -5,7 +5,7 @@ export default { name: "Send Chat Message", description: "Send a message to a team's chat. [See the docs here](https://docs.microsoft.com/en-us/graph/api/chat-post-messages?view=graph-rest-1.0&tabs=http)", type: "action", - version: "0.0.5", + version: "0.0.8", props: { microsoftTeams, chatId: { @@ -20,11 +20,18 @@ export default { "message", ], }, + contentType: { + propDefinition: [ + microsoftTeams, + "contentType", + ], + }, }, async run({ $ }) { const { chatId, message, + contentType, } = this; const response = @@ -33,6 +40,7 @@ export default { content: { body: { content: message, + contentType, }, }, }); diff --git a/components/microsoft_teams/microsoft_teams.app.mjs b/components/microsoft_teams/microsoft_teams.app.mjs index 256d6c3fef589..12edcb2f43997 100644 --- a/components/microsoft_teams/microsoft_teams.app.mjs +++ b/components/microsoft_teams/microsoft_teams.app.mjs @@ -34,7 +34,8 @@ export default { label: "Channel", description: "Team Channel", async options({ - teamId, prevContext, + teamId, + prevContext, }) { const response = prevContext.nextLink ? await this.makeRequest({ @@ -58,21 +59,69 @@ export default { chat: { type: "string", label: "Chat", - description: "Team Chat within the organization (No external Contacts)", - async options({ prevContext }) { - const response = prevContext.nextLink + description: "Select a chat (type to search by participant names)", + async options({ + prevContext, query, + }) { + let path = "/chats?$expand=members"; + path += "&$top=20"; + + if (query) { + path += `&$search="${query}"`; + } + + const response = prevContext?.nextLink ? await this.makeRequest({ path: prevContext.nextLink, }) - : await this.listChats(); + : await this.makeRequest({ + path, + }); + + this._userCache = this._userCache || new Map(); const options = []; + for (const chat of response.value) { - const members = chat.members.map((member) => member.displayName); + let members = chat.members.map((member) => ({ + displayName: member.displayName, + wasNull: !member.displayName, + userId: member.userId, + email: member.email, + })); + + if (members.some((member) => !member.displayName)) { + try { + const messages = await this.makeRequest({ + path: `/chats/${chat.id}/messages?$top=10&$orderby=createdDateTime desc`, + }); + + const nameMap = new Map(); + messages.value.forEach((msg) => { + if (msg.from?.user?.id && msg.from?.user?.displayName) { + nameMap.set(msg.from.user.id, msg.from.user.displayName); + } + }); + + members = members.map((member) => ({ + ...member, + displayName: member.displayName || nameMap.get(member.userId) || member.email || "Unknown User", + })); + } catch (err) { + console.error(`Failed to fetch messages for chat ${chat.id}:`, err); + } + } + + const memberNames = members.map((member) => + member.wasNull + ? `${member.displayName} (External)` + : member.displayName); + options.push({ - label: members.join(", "), + label: memberNames.join(", "), value: chat.id, }); } + return { options, context: { @@ -80,6 +129,7 @@ export default { }, }; }, + useQuery: true, }, channelDisplayName: { type: "string", @@ -96,6 +146,17 @@ export default { label: "Message", description: "Message to be sent", }, + contentType: { + type: "string", + label: "Content Type", + description: "Text message or HTML message", + optional: true, + default: "text", + options: [ + "text", + "html", + ], + }, }, methods: { _accessToken() { @@ -109,7 +170,10 @@ export default { }); }, async makeRequest({ - method, path, params = {}, content, + method, + path, + params = {}, + content, }) { const api = this.client().api(path); @@ -156,7 +220,8 @@ export default { }); }, async createChannel({ - teamId, content, + teamId, + content, }) { return this.makeRequest({ method: "post", @@ -165,7 +230,9 @@ export default { }); }, async sendChannelMessage({ - teamId, channelId, content, + teamId, + channelId, + content, }) { return this.makeRequest({ method: "post", @@ -174,7 +241,8 @@ export default { }); }, async sendChatMessage({ - chatId, content, + chatId, + content, }) { return this.makeRequest({ method: "post", @@ -204,7 +272,8 @@ export default { .get(); }, async listChannelMessages({ - teamId, channelId, + teamId, + channelId, }) { return this.makeRequest({ path: `/teams/${teamId}/channels/${channelId}/messages/delta`, diff --git a/components/microsoft_teams/package.json b/components/microsoft_teams/package.json index 2aca9178d862a..c6550f058d7b2 100644 --- a/components/microsoft_teams/package.json +++ b/components/microsoft_teams/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/microsoft_teams", - "version": "0.1.1", + "version": "0.1.4", "description": "Pipedream Microsoft Teams Components", "main": "microsoft_teams.app.mjs", "keywords": [ diff --git a/components/microsoft_teams/sources/new-channel-message/new-channel-message.mjs b/components/microsoft_teams/sources/new-channel-message/new-channel-message.mjs index bb2ab0783d81b..37898dae07a8b 100644 --- a/components/microsoft_teams/sources/new-channel-message/new-channel-message.mjs +++ b/components/microsoft_teams/sources/new-channel-message/new-channel-message.mjs @@ -5,7 +5,7 @@ export default { key: "microsoft_teams-new-channel-message", name: "New Channel Message", description: "Emit new event when a new message is posted in a channel", - version: "0.0.6", + version: "0.0.9", type: "source", dedupe: "unique", props: { diff --git a/components/microsoft_teams/sources/new-channel/new-channel.mjs b/components/microsoft_teams/sources/new-channel/new-channel.mjs index dffdb511a74b7..89cf6fd8a23f2 100644 --- a/components/microsoft_teams/sources/new-channel/new-channel.mjs +++ b/components/microsoft_teams/sources/new-channel/new-channel.mjs @@ -5,7 +5,7 @@ export default { key: "microsoft_teams-new-channel", name: "New Channel", description: "Emit new event when a new channel is created within a team", - version: "0.0.6", + version: "0.0.9", type: "source", dedupe: "unique", props: { diff --git a/components/microsoft_teams/sources/new-chat-message/new-chat-message.mjs b/components/microsoft_teams/sources/new-chat-message/new-chat-message.mjs index 0be5d54a7b943..1f0e8925cc3b9 100644 --- a/components/microsoft_teams/sources/new-chat-message/new-chat-message.mjs +++ b/components/microsoft_teams/sources/new-chat-message/new-chat-message.mjs @@ -5,7 +5,7 @@ export default { key: "microsoft_teams-new-chat-message", name: "New Chat Message", description: "Emit new event when a new message is received in a chat", - version: "0.0.6", + version: "0.0.9", type: "source", dedupe: "unique", props: { diff --git a/components/microsoft_teams/sources/new-chat/new-chat.mjs b/components/microsoft_teams/sources/new-chat/new-chat.mjs index 37917bab9340a..e105b9374ab18 100644 --- a/components/microsoft_teams/sources/new-chat/new-chat.mjs +++ b/components/microsoft_teams/sources/new-chat/new-chat.mjs @@ -5,7 +5,7 @@ export default { key: "microsoft_teams-new-chat", name: "New Chat", description: "Emit new event when a new chat is created", - version: "0.0.6", + version: "0.0.9", type: "source", dedupe: "unique", methods: { diff --git a/components/microsoft_teams/sources/new-team-member/new-team-member.mjs b/components/microsoft_teams/sources/new-team-member/new-team-member.mjs index f04dc2ddbf7be..2011638c4ed11 100644 --- a/components/microsoft_teams/sources/new-team-member/new-team-member.mjs +++ b/components/microsoft_teams/sources/new-team-member/new-team-member.mjs @@ -5,7 +5,7 @@ export default { key: "microsoft_teams-new-team-member", name: "New Team Member", description: "Emit new event when a new member is added to a team", - version: "0.0.6", + version: "0.0.9", type: "source", dedupe: "unique", props: { diff --git a/components/microsoft_teams/sources/new-team/new-team.mjs b/components/microsoft_teams/sources/new-team/new-team.mjs index be9528d1ed439..3c9799c1e54f8 100644 --- a/components/microsoft_teams/sources/new-team/new-team.mjs +++ b/components/microsoft_teams/sources/new-team/new-team.mjs @@ -5,7 +5,7 @@ export default { key: "microsoft_teams-new-team", name: "New Team", description: "Emit new event when a new team is joined by the authenticated user", - version: "0.0.6", + version: "0.0.9", type: "source", dedupe: "unique", methods: { diff --git a/components/microsoft_teams_admin/README.md b/components/microsoft_teams_admin/README.md index bdc9f10dc7b89..07f3964641d47 100644 --- a/components/microsoft_teams_admin/README.md +++ b/components/microsoft_teams_admin/README.md @@ -1,11 +1,11 @@ # Overview -The Microsoft Teams Admin API allows you to build a variety of tools and -applications to manage and automate your Teams environment. Here are some -examples of what you can build: - -- A tool to provision and manage teams and team members -- A tool to manage team settings and configurations -- A tool to monitor team activity and usage -- A tool to migrate teams and team data to and from other collaboration - platforms +The Microsoft Teams Admin API allows you to manage Teams environments programmatically. With it, you can automate tasks like creating and managing teams, channels, and policies, as well as controlling membership and settings. Leveraging Pipedream's serverless execution model, you can create workflows that react to specific triggers and perform a sequence of actions across multiple services, enhancing your organization's productivity and governance within Microsoft Teams. + +# Example Use Cases + +- **Automated Team Provisioning**: When a new employee is onboarded and added to your HR system, a Pipedream workflow can be triggered to create a new team in Microsoft Teams, complete with predefined channels and settings, ensuring they have the resources they need from day one. + +- **Policy Compliance Monitoring**: Set up a Pipedream workflow to regularly audit team settings and memberships against compliance policies. If it detects a policy violation, such as a public team containing sensitive information, the workflow can automatically revert changes and notify administrators. + +- **Cross-Platform Collaboration**: Integrate Microsoft Teams Admin with Slack using Pipedream. Whenever a new Slack channel is created in your company's workspace, a corresponding team or channel can be created in Teams, ensuring cross-platform collaboration is streamlined and no team is siloed. diff --git a/components/microsoft_teams_admin/package.json b/components/microsoft_teams_admin/package.json new file mode 100644 index 0000000000000..f8b54811fc724 --- /dev/null +++ b/components/microsoft_teams_admin/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/microsoft_teams_admin", + "version": "0.6.0", + "description": "Pipedream microsoft_teams_admin Components", + "main": "microsoft_teams_admin.app.mjs", + "keywords": [ + "pipedream", + "microsoft_teams_admin" + ], + "homepage": "https://pipedream.com/apps/microsoft_teams_admin", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/microsoft_teams_bot/README.md b/components/microsoft_teams_bot/README.md new file mode 100644 index 0000000000000..ee8824eb68b20 --- /dev/null +++ b/components/microsoft_teams_bot/README.md @@ -0,0 +1,127 @@ +# Overview + +By connecting a Microsoft Teams Bot to Pipedream, you can build interactive chat experiences and automate messaging workflows using any of the thousands of apps available on Pipedream. + +# Getting Started + +To connect your Teams Bot to Pipedream, you'll need to create a bot in Azure, set up a webhook in Pipedream, and configure the Teams app manifest. Follow the detailed instructions below to get started. + +## Configuring a Teams Bot in Azure Portal + +To get started, you'll need the following: +- An Azure account +- A Microsoft Teams account +- Basic familiarity with Azure Portal + +## Quickstart + +1. Create a bot in the Azure Portal +2. Set up a Pipedream webhook +3. Configure the bot's messaging endpoint +4. Create and upload the Teams app manifest +5. Install the bot in Teams +6. Configure your Pipedream workflow + +For detailed instructions, follow the steps below. + +## Detailed Setup Instructions + +### 1. Create a Bot in Azure + +1. Sign in to the [Azure Portal](https://portal.azure.com) +2. Create a new "Azure Bot" resource +3. During creation: + - Select "Create new" Microsoft App ID + - Note down the generated App ID (client_id) +4. Navigate to the "Configuration" section +5. Generate a new client secret +6. Note down the secret value immediately (you won't be able to see it again) + +### 2. Set Up Pipedream Webhook to receive bot messages + +1. Go to Pipedream +2. Create a new workflow starting with an HTTP trigger +3. Copy the generated webhook URL (format: https://xxx.m.pipedream.net) + - This will be your bot messaging endpoint + +### 3. Configure Bot's Messaging Endpoint + +1. Return to your Azure Bot resource +2. Under "Configuration" +3. Set Messaging endpoint to your Pipedream webhook URL + +### 4. Create Teams App Manifest + +1. Create a new file called `manifest.json` with the following template: + +```json +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.14/MicrosoftTeams.schema.json", + "manifestVersion": "1.14", + "version": "1.0.0", + "id": "", + "packageName": "com.yourcompany.bot", + "developer": { + "name": "Your Company", + "websiteUrl": "https://your-website.com", + "privacyUrl": "https://your-website.com/privacy", + "termsOfUseUrl": "https://your-website.com/terms" + }, + "name": { + "short": "Your Bot Name", + "full": "Your Bot Full Name" + }, + "description": { + "short": "A brief description", + "full": "A full description of your bot" + }, + "icons": { + "outline": "outline.png", + "color": "color.png" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "", + "scopes": [ + "personal", + "team", + "groupchat" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "permissions": [ + "messageTeamMembers" + ] +} +``` + +2. Create two icon files: + - `outline.png` (32x32 pixels) + - `color.png` (192x192 pixels) +3. Zip these three files together + +### 5. Install Bot in Teams + +1. Open Microsoft Teams +2. Go to Apps > Upload a custom app +3. Upload your zip file +4. Follow installation prompts, and your bot will now be added to Microsoft Teams! + +### 6. Configure Pipedream Workflow + +1. Add "Microsoft Teams Bot" app in Pipedream +2. Enter your Bot's App ID and client secret +3. Configure your workflow to process and respond to messages + +# Troubleshooting + +- **Authentication Issues**: Verify your App ID and client secret are correct +- **Messaging Endpoint Errors**: Ensure your Pipedream webhook URL is properly configured in Azure +- **Teams Installation Problems**: Check that your manifest.json and icon files meet all requirements + +For more details, please see: +* [Azure Bot Service Documentation](https://docs.microsoft.com/en-us/azure/bot-service/) +* [Teams App Manifest Documentation](https://docs.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) diff --git a/components/microsoft_teams_bot/microsoft_teams_bot.app.mjs b/components/microsoft_teams_bot/microsoft_teams_bot.app.mjs new file mode 100644 index 0000000000000..960d05d94d580 --- /dev/null +++ b/components/microsoft_teams_bot/microsoft_teams_bot.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "microsoft_teams_bot", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/microsoft_teams_bot/package.json b/components/microsoft_teams_bot/package.json new file mode 100644 index 0000000000000..7ca448c892c6c --- /dev/null +++ b/components/microsoft_teams_bot/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/microsoft_teams_bot", + "version": "0.0.1", + "description": "Pipedream Microsoft Teams Bot Components", + "main": "microsoft_teams_bot.app.mjs", + "keywords": [ + "pipedream", + "microsoft_teams_bot" + ], + "homepage": "https://pipedream.com/apps/microsoft_teams_bot", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/microsoft_text_translate/microsoft_text_translate.app.mjs b/components/microsoft_text_translate/microsoft_text_translate.app.mjs new file mode 100644 index 0000000000000..5bd3c6fe11a9f --- /dev/null +++ b/components/microsoft_text_translate/microsoft_text_translate.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "microsoft_text_translate", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/microsoft_text_translate/package.json b/components/microsoft_text_translate/package.json new file mode 100644 index 0000000000000..4c4dd3c822caa --- /dev/null +++ b/components/microsoft_text_translate/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/microsoft_text_translate", + "version": "0.0.1", + "description": "Pipedream Microsoft Text Translate Components", + "main": "microsoft_text_translate.app.mjs", + "keywords": [ + "pipedream", + "microsoft_text_translate" + ], + "homepage": "https://pipedream.com/apps/microsoft_text_translate", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/microsofttodo/README.md b/components/microsofttodo/README.md new file mode 100644 index 0000000000000..5e55fa1caf46f --- /dev/null +++ b/components/microsofttodo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Microsoft To Do API on Pipedream allows you to seamlessly integrate your tasks and to-do lists with other services and automate your personal or team workflows. By harnessing this API, you can create, read, update, and delete tasks, manage lists, and sync with your calendar or mail. This opens up possibilities for automated task management, deadline reminders, or syncing across multiple platforms for enhanced productivity. + +# Example Use Cases + +- **Sync Tasks with Google Calendar:** Automatically create or update events in Google Calendar when a new task is added or modified in Microsoft To Do. This ensures that all your deadlines and to-dos are visible on your calendar, which helps keep you on top of your schedule. + +- **Email Digest of Daily Tasks:** Design a workflow that sends you a daily email digest with your tasks for the day. This can be achieved by integrating Microsoft To Do with an email service like Gmail or SendGrid on Pipedream, providing a clear overview of your day's priorities directly in your inbox. + +- **Task Creation from Slack Messages:** Set up a Pipedream workflow where you can create tasks in Microsoft To Do directly from Slack messages. Utilize a slash command or a message action in Slack to instantly turn a message into a to-do item, allowing for quick task capture while communicating with your team. diff --git a/components/middesk/README.md b/components/middesk/README.md index 021ba294b36f6..81d3f15eef500 100644 --- a/components/middesk/README.md +++ b/components/middesk/README.md @@ -1,12 +1,11 @@ # Overview -Middesk makes it easy to build custom applications on top of its robust API. -With Middesk, you can easily create custom applications to automate your -business processes. Here are some examples of what you can build using the -Middesk API: - -- A custom CRM application to track your customer data -- An automated invoicing system to generate and send invoices automatically -- A custom ticketing system to manage customer support requests -- An inventory management system to keep track of your stock levels -- A forecasting tool to predict future trends in your industry +Middesk API is a game-changer for businesses looking to automate their background checks and company verifications. It's the bridge between your company and critical data on businesses you plan to engage with. Through the Middesk API on Pipedream, you can seamlessly conduct due diligence, validate company information, and ensure regulatory compliance. From automating onboarding workflows to real-time monitoring of business status changes, the integration possibilities with other apps and services are both robust and precise. + +# Example Use Cases + +- **Automated Vendor Onboarding**: As soon as a new vendor is added to your CRM, you can trigger a workflow that uses Middesk to perform thorough background checks and business verifications, ensuring they meet your compliance standards before onboarding. + +- **Real-time Business Status Tracking**: Connect Middesk to a database monitoring service on Pipedream. Monitor for updates or changes in the status of businesses you work with and trigger alerts or update records in your internal systems to reflect the new status. + +- **Compliance Report Generation**: Schedule a regular workflow that pulls the latest verification data from Middesk for all your active vendors and compiles a compliance report, which can be formatted and sent to your compliance team or stored in your document management system. diff --git a/components/middesk/package.json b/components/middesk/package.json new file mode 100644 index 0000000000000..f72143dace86c --- /dev/null +++ b/components/middesk/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/middesk", + "version": "0.6.0", + "description": "Pipedream middesk Components", + "main": "middesk.app.mjs", + "keywords": [ + "pipedream", + "middesk" + ], + "homepage": "https://pipedream.com/apps/middesk", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/miestro/README.md b/components/miestro/README.md index dfe2d877364f7..0cfc659ec224f 100644 --- a/components/miestro/README.md +++ b/components/miestro/README.md @@ -1,10 +1,11 @@ # Overview -The Miestro API can be used to create a variety of applications, including: - -- Location-based applications -- Messaging applications -- Social networking applications -- Gaming applications -- Augmented reality applications -- And more! +Miestro is a robust platform designed for creating and managing online courses. Leveraging the Miestro API on Pipedream allows you to automate interactions with your online courses, such as enrolling students, tracking progress, and analyzing engagement. With the automation workflows on Pipedream, you can streamline your educational platform's operations, react to student activity in real time, and integrate with other services to enhance the e-learning experience. + +# Example Use Cases + +- **Automate New Student Enrollment**: When a new user signs up on your website, automatically enroll them in a Miestro course via API. Use Pipedream to listen for signup events from your website and trigger the enrollment process, removing manual entry and ensuring a seamless onboarding experience. + +- **Student Progress Report Generator**: Create a workflow that fetches student progress from Miestro and compiles it into a report. Connect with an email service like SendGrid on Pipedream to automatically send weekly progress reports to students, keeping them informed and engaged with their courses. + +- **Course Engagement Analytics**: Set up a Pipedream workflow that tracks course completion rates and quiz scores from Miestro. Integrate with a data visualization tool like Google Sheets to analyze and visualize this data, enabling you to make informed decisions about course content and structure. diff --git a/components/mindbody/mindbody.app.mjs b/components/mindbody/mindbody.app.mjs new file mode 100644 index 0000000000000..bf4239a0d5323 --- /dev/null +++ b/components/mindbody/mindbody.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "mindbody", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/mindbody/package.json b/components/mindbody/package.json new file mode 100644 index 0000000000000..f382b695a0132 --- /dev/null +++ b/components/mindbody/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/mindbody", + "version": "0.0.1", + "description": "Pipedream Mindbody Components", + "main": "mindbody.app.mjs", + "keywords": [ + "pipedream", + "mindbody" + ], + "homepage": "https://pipedream.com/apps/mindbody", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/mindmeister/README.md b/components/mindmeister/README.md new file mode 100644 index 0000000000000..7ef5d7c7d0f8d --- /dev/null +++ b/components/mindmeister/README.md @@ -0,0 +1,11 @@ +# Overview + +The MindMeister API lets you integrate real-time mind mapping into your projects. By connecting MindMeister to Pipedream, you can automate mind map creation, updates, and sharing processes. It's ideal for brainstorming sessions, project planning, and knowledge management. With Pipedream's serverless platform, you can trigger workflows with HTTP requests, schedules, or app events, and connect MindMeister with hundreds of other apps to streamline your tasks. + +# Example Use Cases + +- **Automated Mind Map Creation from New Tasks**: When new tasks are added to a project management tool like Trello or Asana, automatically create a corresponding mind map in MindMeister. + +- **Sync Mind Maps with Calendar Events**: Before a scheduled meeting in Google Calendar, trigger a workflow that creates or updates a MindMeister mind map with the agenda or discussion points, and share the link with all participants. + +- **Content Brainstorming Pipeline**: Collect ideas from a form submission via Typeform or Google Forms, and automatically populate them into a MindMeister mind map for content brainstorming and organization. diff --git a/components/minio/minio.app.mjs b/components/minio/minio.app.mjs new file mode 100644 index 0000000000000..8c53102b20120 --- /dev/null +++ b/components/minio/minio.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "minio", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/minio/package.json b/components/minio/package.json new file mode 100644 index 0000000000000..c34b33b921df8 --- /dev/null +++ b/components/minio/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/minio", + "version": "0.0.1", + "description": "Pipedream MinIO Components", + "main": "minio.app.mjs", + "keywords": [ + "pipedream", + "minio" + ], + "homepage": "https://pipedream.com/apps/minio", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/miro_custom_app/README.md b/components/miro_custom_app/README.md new file mode 100644 index 0000000000000..475abf1054279 --- /dev/null +++ b/components/miro_custom_app/README.md @@ -0,0 +1,11 @@ +# Overview + +The Miro Developer App API lets you extend the functionality of Miro, an online collaborative whiteboarding platform. On Pipedream, you can automate interactions with Miro boards, such as creating or updating content, and extracting data for reporting or integration purposes. This can streamline collaboration and project management by automating repetitive tasks and connecting Miro with other tools and services. + +# Example Use Cases + +- **Automated Meeting Prep**: Set up a workflow that creates a new board in Miro with predefined templates before each scheduled meeting in Google Calendar. This can ensure that every meeting starts with the necessary structure and materials. + +- **Real-time Collaboration Sync**: Whenever a team member adds a sticky note to a Miro board, trigger a workflow that posts the content to a Slack channel. This keeps the entire team updated on brainstorming sessions without switching contexts. + +- **Task Tracking Integration**: Combine the power of Miro and Trello by automating the creation of Trello cards for each new frame added to a Miro board. This helps in turning brainstorming sessions into actionable tasks seamlessly. diff --git a/components/mission_mobile/mission_mobile.app.mjs b/components/mission_mobile/mission_mobile.app.mjs new file mode 100644 index 0000000000000..f60710d7f9249 --- /dev/null +++ b/components/mission_mobile/mission_mobile.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "mission_mobile", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/mission_mobile/package.json b/components/mission_mobile/package.json new file mode 100644 index 0000000000000..ee42ed9f20f27 --- /dev/null +++ b/components/mission_mobile/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/mission_mobile", + "version": "0.0.1", + "description": "Pipedream Mission Mobile Components", + "main": "mission_mobile.app.mjs", + "keywords": [ + "pipedream", + "mission_mobile" + ], + "homepage": "https://pipedream.com/apps/mission_mobile", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/missive/README.md b/components/missive/README.md new file mode 100644 index 0000000000000..954e83a259dd8 --- /dev/null +++ b/components/missive/README.md @@ -0,0 +1,11 @@ +# Overview + +The Missive API allows users to streamline their email and team communication within the Pipedream platform. It offers access to manage conversations, collaborate on emails, and automate workflows involving your communication stack. With Pipedream, you can harness this API to integrate Missive with various other apps, enhance your team's productivity, and create custom automation based on triggers like new emails or conversation status changes. + +# Example Use Cases + +- **Automate Customer Support Ticket Creation**: When a conversation is tagged with "support" in Missive, trigger a workflow that creates a ticket in a customer support platform like Zendesk. This ensures that your support team immediately knows about the issue and can track it through to resolution. + +- **Sync Missive Conversations to CRM**: Each time a conversation is marked as 'closed' in Missive, automatically sync relevant details to a CRM tool like Salesforce. This can include the conversation summary, customer contact info, and any follow-up tasks, keeping your sales team informed and your CRM up to date. + +- **Email Campaign Analytics Reporting**: After sending out a marketing campaign, use Missive to tag conversations that come back with specific responses. Then, set up a Pipedream workflow that collects these conversations and sends a daily or weekly digest to your marketing team's Slack channel for quick review and analysis. diff --git a/components/mitra/mitra.app.mjs b/components/mitra/mitra.app.mjs new file mode 100644 index 0000000000000..dca7dd079de66 --- /dev/null +++ b/components/mitra/mitra.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "mitra", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/mitra/package.json b/components/mitra/package.json new file mode 100644 index 0000000000000..d0952be684728 --- /dev/null +++ b/components/mitra/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/mitra", + "version": "0.0.1", + "description": "Pipedream Mitra Components", + "main": "mitra.app.mjs", + "keywords": [ + "pipedream", + "mitra" + ], + "homepage": "https://pipedream.com/apps/mitra", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/mixmax/README.md b/components/mixmax/README.md index d99453083fb19..0936dc8f36d88 100644 --- a/components/mixmax/README.md +++ b/components/mixmax/README.md @@ -1,9 +1,11 @@ # Overview -MixMax is a powerful email platform that lets you build rich email -applications. With the MixMax API, you can: +The MixMax API allows you to automate and enhance email productivity tasks within your workflows. Utilizing MixMax with Pipedream, you can create dynamic automation scenarios such as syncing calendar events, triggering email sequences based on customer actions, and analyzing email engagement data. By harnessing the power of Pipedream's serverless platform, you can integrate MixMax with numerous other apps to streamline communication processes, improve response times, and personalize your outreach efforts. -- Build rich email applications that take advantage of MixMax's powerful - features -- Automate email tasks with ease -- Integrate MixMax with your existing applications +# Example Use Cases + +- **Automated Follow-Up Emails After Meetings**: After a meeting scheduled through MixMax, a Pipedream workflow can be triggered to send a personalized follow-up email. It could include meeting notes, action items, or a thank-you message. + +- **Lead Qualification Emails Based on Website Activity**: When a potential lead performs a specific action on your website (tracked via a tool like Google Analytics), Pipedream can trigger MixMax to send targeted email sequences to nurture the lead further down the sales funnel. + +- **Email Engagement Data to CRM**: Use Pipedream to send email engagement data from MixMax, such as opens or clicks, to your CRM platform, like Salesforce. This data can then be used to score leads or tailor future communications. diff --git a/components/mixpanel/README.md b/components/mixpanel/README.md index d47f77731df81..2cd44aeb2da63 100644 --- a/components/mixpanel/README.md +++ b/components/mixpanel/README.md @@ -1,12 +1,11 @@ # Overview -Mixpanel is a powerful analytics API that allows you to track, analyze, and -engage with your users. With Mixpanel, you can build a variety of applications -and services, including: - -- A user analytics platform, for tracking user behavior and engagement -- A marketing platform, for segmenting users and delivering targeted messages -- A customer support platform, for tracking customer issues and resolving them - efficiently -- A product management platform, for tracking product usage and understanding - how users interact with your product +Mixpanel's API allows you to track user interactions with your web and mobile applications, providing insights through data analysis. You can capture events, update user profiles, and segment data based on user actions. Integrating Mixpanel with Pipedream enables you to automate responses to these events, sync data across services, and trigger workflows that can enhance user engagement and improve retention. + +# Example Use Cases + +- **User Segmentation for Email Campaigns**: When Mixpanel identifies a user segment that performs a specific action, say, completing a tutorial, you can use Pipedream to trigger an automated email through SendGrid or Mailgun. This email could provide additional resources or incentives, encouraging further engagement. + +- **Slack Alerts for High-Value Features**: Set up a Pipedream workflow that listens for Mixpanel events related to high-value features. Whenever usage spikes or a VIP customer interacts with the feature, send a notification to a designated Slack channel, keeping your team informed in real-time. + +- **Automated User Onboarding**: Combine Mixpanel's data with Pipedream to create a tailored onboarding experience. When a new user signs up, use Pipedream to enroll them in an onboarding sequence in your CRM, such as Salesforce, and track their progress with Mixpanel events, personalizing the journey based on their behavior. diff --git a/components/mixpanel/package.json b/components/mixpanel/package.json new file mode 100644 index 0000000000000..12d75129f4f0d --- /dev/null +++ b/components/mixpanel/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/mixpanel", + "version": "0.6.0", + "description": "Pipedream mixpanel Components", + "main": "mixpanel.app.mjs", + "keywords": [ + "pipedream", + "mixpanel" + ], + "homepage": "https://pipedream.com/apps/mixpanel", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/moaform/common/constants.mjs b/components/moaform/common/constants.mjs new file mode 100644 index 0000000000000..dbce28a0a61e4 --- /dev/null +++ b/components/moaform/common/constants.mjs @@ -0,0 +1,30 @@ +export const RETENTION_DAYS_OPTIONS = [ + { + label: "1", + value: 1, + }, + { + label: "3", + value: 3, + }, + { + label: "5", + value: 5, + }, + { + label: "7", + value: 7, + }, + { + label: "10", + value: 10, + }, + { + label: "15", + value: 15, + }, + { + label: "30", + value: 30, + }, +]; diff --git a/components/moaform/moaform.app.mjs b/components/moaform/moaform.app.mjs new file mode 100644 index 0000000000000..09007f770c539 --- /dev/null +++ b/components/moaform/moaform.app.mjs @@ -0,0 +1,68 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "moaform", + propDefinitions: { + formId: { + type: "string", + label: "Form ID", + description: "The ID of the form to monitor for new submissions", + async options({ page }) { + const { items } = await this.getForms({ + params: { + page: page + 1, + }, + }); + return items.map(({ + id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.moaform.com/v1"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + getForms(opts = {}) { + return this._makeRequest({ + ...opts, + path: "/forms", + }); + }, + createWebhook({ + formId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/forms/${formId}/webhooks`, + ...opts, + }); + }, + deleteWebhook({ + formId, webhookId, + }) { + return this._makeRequest({ + method: "DELETE", + path: `/forms/${formId}/webhooks/${webhookId}`, + }); + }, + }, +}; diff --git a/components/moaform/package.json b/components/moaform/package.json new file mode 100644 index 0000000000000..209996644a6c8 --- /dev/null +++ b/components/moaform/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/moaform", + "version": "0.1.0", + "description": "Pipedream Moaform Components", + "main": "moaform.app.mjs", + "keywords": [ + "pipedream", + "moaform" + ], + "homepage": "https://pipedream.com/apps/moaform", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/moaform/sources/new-submission-instant/new-submission-instant.mjs b/components/moaform/sources/new-submission-instant/new-submission-instant.mjs new file mode 100644 index 0000000000000..85668ca7d3524 --- /dev/null +++ b/components/moaform/sources/new-submission-instant/new-submission-instant.mjs @@ -0,0 +1,100 @@ +import crypto from "crypto"; +import { RETENTION_DAYS_OPTIONS } from "../../common/constants.mjs"; +import moaform from "../../moaform.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "moaform-new-submission-instant", + name: "New Submission (Instant)", + description: "Emit new event every time a new form submission is received.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + moaform, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + formId: { + propDefinition: [ + moaform, + "formId", + ], + }, + retentionDays: { + type: "integer", + label: "Retention Days", + description: "Resend restriction days", + options: RETENTION_DAYS_OPTIONS, + optional: true, + }, + secret: { + type: "string", + label: "Secret Code", + description: "This code is used to verify that the data received at the specified endpoint has indeed been sent from Moaform and has not been tampered with.", + secret: true, + optional: true, + }, + }, + methods: { + _getWebhookId() { + return this.db.get("webhookId"); + }, + _setWebhookId(id) { + this.db.set("webhookId", id); + }, + }, + hooks: { + async activate() { + const response = await this.moaform.createWebhook({ + formId: this.formId, + data: { + endpoint: this.http.endpoint, + enabled: true, + secret: this.secret, + verify_ssl: true, + retention_days: this.retentionDays, + }, + }); + this._setWebhookId(response.id); + }, + async deactivate() { + const webhookId = this.db.get("webhookId"); + await this.moaform.deleteWebhook({ + formId: this.formId, + webhookId, + }); + }, + }, + async run({ + bodyRaw, body, headers, + }) { + + if (this.secret) { + const signature = headers["moaform-signature"]; + const receivedSig = signature.split("sha256=")[1]; + + const calculatedSig = crypto + .createHmac("sha256", this.secret) + .update(bodyRaw) + .digest("base64"); + + if (receivedSig !== calculatedSig) { + return this.http.respond({ + status: 401, + body: "Unauthorized", + }); + } + } + + const ts = Date.parse(body.submitted_at); + this.$emit(body, { + id: body.event_id, + summary: `New submission received for form ${this.formId}`, + ts: ts, + }); + }, + sampleEmit, +}; diff --git a/components/moaform/sources/new-submission-instant/test-event.mjs b/components/moaform/sources/new-submission-instant/test-event.mjs new file mode 100644 index 0000000000000..6fc8f83f5c9f2 --- /dev/null +++ b/components/moaform/sources/new-submission-instant/test-event.mjs @@ -0,0 +1,18 @@ +export default { + "event_id": "105fa72f-9e6e-4667-8ead-22b0a3ed254a", + "event_type": "response_completed", + "hidden": {}, + "response_id": "test#123-45-67890", + "submitted_at": "2024-09-27T21:00:55Z", + "form": { + "id": "MG7eAk", + "title": "form title", + "report_url": "https://www.moaform.com/reports/q945EegynYD8G2xM", + "answer_url": "https://moaform.com/q/not-started-collecting" + }, + "answers": [], + "thankyou": { + "id": "cm1l7axyf2t9x0fqrly27vdn7", + "url": "https://answer.moaform.com/answers/MG7eAk/thankyou/Wo2b1Z09wx5" + } +} \ No newline at end of file diff --git a/components/mobile_text_alerts/README.md b/components/mobile_text_alerts/README.md new file mode 100644 index 0000000000000..5e77145feabd3 --- /dev/null +++ b/components/mobile_text_alerts/README.md @@ -0,0 +1,11 @@ +# Overview + +The Mobile Text Alerts API allows for the automation of SMS messaging processes, enabling you to send alerts, reminders, and notifications directly to people's phones. With Pipedream's serverless platform, you can integrate these text messaging capabilities into complex workflows with various triggers and actions from a multitude of services. + +# Example Use Cases + +- **Automated Event Reminders**: Send SMS reminders to attendees for upcoming events by integrating with a calendar service. Trigger a workflow on Pipedream whenever a new event is nearing, and use the Mobile Text Alerts API to notify registered participants. + +- **E-commerce Order Updates**: Keep customers informed about their order status. Connect an e-commerce platform with the Mobile Text Alerts API on Pipedream to send texts when an order is placed, shipped, or delivered. + +- **System Outage Alerts**: Set up a monitoring service to trigger an alert on Pipedream when your system is down. Use the Mobile Text Alerts API to immediately inform IT staff or a broader user base about the outage for quick response times. diff --git a/components/mobilemonkey/README.md b/components/mobilemonkey/README.md new file mode 100644 index 0000000000000..1be54f1075dcf --- /dev/null +++ b/components/mobilemonkey/README.md @@ -0,0 +1,11 @@ +# Overview + +The MobileMonkey API allows you to automate interactions with your MobileMonkey-powered chatbots. By leveraging this API on Pipedream, you can orchestrate complex workflows that react to events in MobileMonkey, send messages, update contacts, and more, thus enriching your chatbot's capabilities with Pipedream's robust integration platform. You can trigger workflows on Pipedream using webhooks, schedules, or other app events, and incorporate logic to interact with a wide range of services to enhance customer engagement, streamline processes, and gather insights from chatbot interactions. + +# Example Use Cases + +- **Customer Support Ticket Creation**: When a user reports an issue via the MobileMonkey chatbot, a Pipedream workflow can be triggered, creating a support ticket in a tool like Zendesk, and then notifying the relevant support team via a Slack message. + +- **Lead Qualification and CRM Integration**: A Pipedream workflow can listen for specific keywords or questions in chatbot conversations to qualify leads. Once a lead is qualified, the workflow can automatically add or update the lead's information in a CRM like Salesforce, and then send a personalized follow-up message via MobileMonkey. + +- **E-commerce Order Updates**: After a customer places an order through your e-commerce platform, a Pipedream workflow can capture the order details and send a confirmation message through MobileMonkey. It can also monitor the order status and send customers real-time updates on their shipping status directly through the chatbot. diff --git a/components/mobivate/README.md b/components/mobivate/README.md new file mode 100644 index 0000000000000..e549199b6ce1c --- /dev/null +++ b/components/mobivate/README.md @@ -0,0 +1,11 @@ +# Overview + +The Mobivate API on Pipedream allows you to engage with SMS services for sending messages, checking delivery status, and managing contacts. It's a powerful tool for automating communication-based workflows, such as notifications, marketing campaigns, and customer engagement. With Pipedream, you can rapidly integrate Mobivate into serverless workflows with other apps and services, leveraging its capabilities without needing to manage infrastructure. + +# Example Use Cases + +- **Automated Customer Support Notifications**: Send automated SMS messages to customers using Mobivate when a ticket is updated in a support system like Zendesk. Combine customer data from your CRM to personalize messages, improving the customer experience. + +- **Marketing Campaign Management**: Create a workflow that triggers a series of targeted SMS messages through Mobivate in response to a subscriber's action, such as signing up for a newsletter or downloading a whitepaper. Integrate with apps like Mailchimp for seamless marketing automation. + +- **Order Confirmation and Delivery Updates**: Use Mobivate within Pipedream to send confirmation texts once an order is placed on your e-commerce platform, such as Shopify. Follow up with delivery updates when the status changes, keeping customers informed and satisfied. diff --git a/components/mobivate/mobivate.app.mjs b/components/mobivate/mobivate.app.mjs index 93117aa297b5f..2904c748b5282 100644 --- a/components/mobivate/mobivate.app.mjs +++ b/components/mobivate/mobivate.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/mobivate/package.json b/components/mobivate/package.json index f369d8f55ff3b..3643c923bf255 100644 --- a/components/mobivate/package.json +++ b/components/mobivate/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/mobygames/README.md b/components/mobygames/README.md new file mode 100644 index 0000000000000..bac4b85362c7e --- /dev/null +++ b/components/mobygames/README.md @@ -0,0 +1,11 @@ +# Overview + +The MobyGames API provides access to a vast database of video game information, including titles, platforms, release dates, and more. In Pipedream, you can leverage this API to create powerful integrations and automated workflows. You might fetch game data to curate personalized game recommendations, sync release information with your calendar, or compile game statistics for analysis. + +# Example Use Cases + +- **Game Release Calendar Integration**: Pipedream can automate the process of adding upcoming game releases to your Google Calendar. By fetching new release data from MobyGames and using the Google Calendar API, you can keep your calendar updated with the latest game launch dates. + +- **Personalized Game Recommendation Engine**: Build a workflow that takes user preferences from a Typeform survey, queries the MobyGames API for matching games, and then emails the recommendations via SendGrid. This can be a personalized weekly gaming newsletter based on their interests. + +- **Game Launch Notification Service**: Use Pipedream to monitor MobyGames for new game releases and send notifications through Slack when a game matching certain criteria (like genre or platform) is released. This keeps a gaming community informed about the latest titles relevant to their interests. diff --git a/components/mocean_api/README.md b/components/mocean_api/README.md new file mode 100644 index 0000000000000..05f12659ee9b2 --- /dev/null +++ b/components/mocean_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Mocean API provides tools for sending SMS and making voice calls, enabling you to integrate communication features into your applications or workflows seamlessly. With MoceanAPI on Pipedream, you can automate sending notifications, create alert systems, or even implement two-factor authentication (2FA). It interacts with other apps and services to craft powerful, event-driven automation. + +# Example Use Cases + +- **SMS Notifications for E-Commerce**: Automate order confirmation and dispatch notification via SMS to customers when their orders are processed in Shopify. Trigger a Mocean API action to send a text message directly from your Pipedream workflow whenever there's a new order or shipment in Shopify. + +- **Voice Alert System for Monitoring Services**: Set up a server monitoring workflow that makes automated calls using Mocean API when a server's health check fails. This could be tied to incoming HTTP requests or scheduled checks in Pipedream, ensuring that you immediately know about any critical service disruptions. + +- **Two-Factor Authentication for User Security**: Strengthen your app's security by adding 2FA. After a user login attempt, trigger a workflow that sends a one-time passcode via SMS through Mocean API. Integrate with Auth0 or another authentication service on Pipedream to verify the passcode and complete the authentication process. diff --git a/components/moco/README.md b/components/moco/README.md new file mode 100644 index 0000000000000..dc7a34fa1004c --- /dev/null +++ b/components/moco/README.md @@ -0,0 +1,11 @@ +# Overview + +The MOCO API provides programmatic access to functions within the MOCO project management and accounting platform. With Pipedream, you can harness this API to create automated workflows that interact with project data, time entries, invoices, and more. Whether you're syncing project updates to other platforms, automating billing processes, or compiling reports, Pipedream's serverless platform enables you to connect MOCO with various apps and services to streamline your project management and accounting tasks. + +# Example Use Cases + +- **Project Time Tracking to Google Sheets**: Sync time tracking entries from MOCO to a Google Sheets spreadsheet for consolidated reporting. Whenever a new time entry is logged in MOCO, a Pipedream workflow triggers and appends the entry details to a designated Google Sheets document. This helps in maintaining up-to-date time tracking records for analysis and client billing. + +- **Invoicing Notifications through Slack**: Send notifications to a Slack channel when a new invoice is created in MOCO. Set up a Pipedream workflow that monitors for new invoices in MOCO and then posts a message with the invoice details to Slack, keeping your team instantly informed about billing updates and ensuring prompt follow-up with clients. + +- **Sync Projects with Trello for Task Management**: Create Trello cards for new projects added in MOCO. With Pipedream, connect MOCO to Trello and automatically generate corresponding cards when a project is initiated. This allows for a smooth transition from project planning to execution, ensuring that task tracking is in sync across both platforms. diff --git a/components/mode/README.md b/components/mode/README.md new file mode 100644 index 0000000000000..c22be3250978d --- /dev/null +++ b/components/mode/README.md @@ -0,0 +1,11 @@ +# Overview + +The Mode API provides programmatic access to Mode's analytics platform, allowing you to automate interactions with your Mode workspace. Using Mode with Pipedream, you can create workflows that interact with reports, queries, and spaces, or even manage members and permissions. This can include tasks like triggering a report run, fetching query results, or syncing users from other systems into Mode. By leveraging Pipedream's serverless platform, you can build robust, event-driven automations that integrate Mode with hundreds of other apps without the need for dedicated infrastructure. + +# Example Use Cases + +- **Automated Report Generation and Distribution**: Schedule and trigger Mode report runs, then fetch the results and distribute them through email or messaging platforms like Slack or Microsoft Teams. This workflow can ensure stakeholders receive timely analytics updates without manual intervention. + +- **Synchronize Mode Spaces with External Data Sources**: Maintain a sync between Mode spaces and external databases or apps like Salesforce or Google Sheets. Whenever new data is added or updated in the external source, the workflow can update or create corresponding reports in Mode, keeping your data analysis up-to-date. + +- **User Management Automation**: Streamline the process of managing Mode workspace members by automating user provisioning and deprovisioning based on events from HR systems like Workday or BambooHR. When a user's status changes in the HR system, it can trigger a workflow to update their permissions or remove them from your Mode workspace accordingly. diff --git a/components/modeck/README.md b/components/modeck/README.md new file mode 100644 index 0000000000000..54c2b1e89fde1 --- /dev/null +++ b/components/modeck/README.md @@ -0,0 +1,11 @@ +# Overview + +The MoDeck API offers an interface for managing playlists and videos within their platform, providing endpoints for various operations like retrieving video details, updating playlists, or managing users. Integrating MoDeck with Pipedream allows you to automate interactions with your MoDeck data, such as syncing playlists, updating video statuses, or triggering actions based on video analytics. With Pipedream's serverless platform, you can build powerful workflows that react to events in real-time, schedule tasks, and connect MoDeck with hundreds of other services. + +# Example Use Cases + +- **Sync MoDeck Playlist with a CMS**: Automate the synchronization of MoDeck playlists with your content management system. Whenever a new video is added to a playlist in MoDeck, Pipedream can trigger a workflow that updates the corresponding content in your CMS, ensuring that your website or app displays the latest videos without manual intervention. + +- **Update Video Status on Social Media**: Create a workflow that listens for updates to video statuses in MoDeck, such as a change to 'published'. When a video is published, Pipedream can automatically post an update to social media platforms like Twitter or Facebook, sharing the new content with your audience immediately. + +- **Aggregate Video Analytics**: Collect and analyze video performance data from MoDeck. Set up a scheduled workflow on Pipedream to retrieve video analytics from MoDeck on a regular basis. You can then process this data and send it to a Google Sheet or a data visualization tool like Tableau for reporting and further analysis. diff --git a/components/modelry/modelry.app.mjs b/components/modelry/modelry.app.mjs new file mode 100644 index 0000000000000..4ffe3be2c6abc --- /dev/null +++ b/components/modelry/modelry.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "modelry", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/modelry/package.json b/components/modelry/package.json new file mode 100644 index 0000000000000..25c76568ef418 --- /dev/null +++ b/components/modelry/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/modelry", + "version": "0.0.1", + "description": "Pipedream Modelry Components", + "main": "modelry.app.mjs", + "keywords": [ + "pipedream", + "modelry" + ], + "homepage": "https://pipedream.com/apps/modelry", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/modern_treasury/README.md b/components/modern_treasury/README.md index 958d42c2c19c7..2ab66fd67002f 100644 --- a/components/modern_treasury/README.md +++ b/components/modern_treasury/README.md @@ -1,9 +1,11 @@ # Overview -With the Modern Treasury API, you can build a variety of financial -applications. Here are a few examples: +Modern Treasury is a powerful platform for payment operations, allowing businesses to manage payments, banking, and reconciliation easily. Through its API, you can initiate transfers, update transactions, reconcile accounts, and automate workflows related to financial transactions. Pipedream, as a serverless integration and compute platform, empowers you to connect Modern Treasury with countless other apps to create custom automation workflows, trigger actions based on payment events, and synchronize financial data across your business systems. -- A payments processing platform -- A invoicing and receivables platform -- A lending and borrowing platform -- A financial analytics platform +# Example Use Cases + +- **Automated Payment Reconciliation**: When payments are processed via Modern Treasury, trigger a workflow on Pipedream that cross-references the payment data with your accounting software (like QuickBooks or Xero). If the payment matches an invoice, mark it as paid automatically, and if not, send an alert to your finance team. + +- **Customer Refund Process**: Set up a Pipedream workflow that listens for refund requests from a customer support platform (like Zendesk or Intercom). Upon receiving a request, automatically process the refund through Modern Treasury and log the refund details in a Google Sheet or your CRM for record-keeping and further analysis. + +- **Real-time Financial Reporting**: Configure a Pipedream workflow that triggers on a scheduled basis or in response to specific events, such as a large transaction. The workflow retrieves transaction data from Modern Treasury, aggregates and formats the data, and then pushes it to a business intelligence tool like Tableau or Google Data Studio for real-time financial reporting and visualization. diff --git a/components/modern_treasury/package.json b/components/modern_treasury/package.json new file mode 100644 index 0000000000000..07f20a08516bb --- /dev/null +++ b/components/modern_treasury/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/modern_treasury", + "version": "0.6.0", + "description": "Pipedream modern_treasury Components", + "main": "modern_treasury.app.mjs", + "keywords": [ + "pipedream", + "modern_treasury" + ], + "homepage": "https://pipedream.com/apps/modern_treasury", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/mojo_helpdesk/README.md b/components/mojo_helpdesk/README.md index 3c5cec3b7e575..9675aa840d352 100644 --- a/components/mojo_helpdesk/README.md +++ b/components/mojo_helpdesk/README.md @@ -1,13 +1,11 @@ # Overview -The Mojo Helpdesk API is a powerful tool that allows developers to create a -wide variety of applications. Here are just a few examples of what you can -build: - -- A help desk application to track and resolve customer issues -- A ticketing system for managing customer support requests -- A customer service chatbot -- A live chat system for customer support -- A CRM system for tracking customer interactions -- A system for tracking customer satisfaction levels -- An analytics platform for customer data +Mojo Helpdesk API lets you seamlessly integrate your customer support system into your business workflow. With this API, you can automate ticket creation, update statuses, track ticket progress, manage user accounts, and extract ticket data for analysis. Take advantage of Pipedream's capabilities to connect Mojo Helpdesk with various services, triggering actions based on customer interactions, automating responses, and synchronizing data across platforms to streamline your customer support and enhance the user experience. + +# Example Use Cases + +- **Automated Ticket Management**: Create a workflow where new customer emails to a specific address automatically generate tickets in Mojo Helpdesk. Use Pipedream's email trigger to start the workflow and the Mojo Helpdesk API to create the tickets. Automatically tag and prioritize these based on keywords within the email content. + +- **Slack Integration for Support Alerts**: Set up a workflow that notifies a designated Slack channel when a high-priority ticket is created in Mojo Helpdesk. Use the Mojo Helpdesk API to monitor ticket statuses and Pipedream's Slack app to dispatch instant alerts, ensuring rapid response times for critical issues. + +- **Customer Feedback Loop**: After a ticket is resolved in Mojo Helpdesk, trigger a workflow that sends a follow-up survey using a service like Typeform. Collect customer feedback and store it in a Google Sheet via Pipedream's Google Sheets app. Use this data to analyze customer satisfaction and identify areas for support improvement. diff --git a/components/mojotxt/mojotxt.app.mjs b/components/mojotxt/mojotxt.app.mjs new file mode 100644 index 0000000000000..1ed58cdbd97ab --- /dev/null +++ b/components/mojotxt/mojotxt.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "mojotxt", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/mojotxt/package.json b/components/mojotxt/package.json new file mode 100644 index 0000000000000..bd990fcf0ce2e --- /dev/null +++ b/components/mojotxt/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/mojotxt", + "version": "0.0.1", + "description": "Pipedream MojoTxt Components", + "main": "mojotxt.app.mjs", + "keywords": [ + "pipedream", + "mojotxt" + ], + "homepage": "https://pipedream.com/apps/mojotxt", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/monday/README.md b/components/monday/README.md index 8e587160f3b6c..7d4d8a3189170 100644 --- a/components/monday/README.md +++ b/components/monday/README.md @@ -1,10 +1,11 @@ # Overview -With the monday.com API you can build a variety of applications and -integrations. Some examples include: - -- A monitoring and analytics tool to track the performance of your monday.com - account -- A mobile app to access your monday.com account on the go -- A tool to help you migrate your data from another platform onto monday.com -- An integration with your existing workflow or CRM system +The monday.com API unlocks the potential to automate workflows, sync data across applications, and create dynamic project management solutions. With this API on Pipedream, you can craft custom integrations that respond to events in monday.com, manipulate boards, items, and columns, and harmonize project data with third-party services to streamline operations, reduce manual workload, and ensure consistent information flow within your business ecosystem. + +# Example Use Cases + +- **Project Progress Tracker**: Trigger a workflow on Pipedream when a status column in monday.com updates, signaling a task's progression. This workflow can then post a message to a Slack channel to notify team members of the update, fostering real-time communication and keeping everyone aligned on project status. + +- **Issue Reporting to Ticketing System**: When a new item is created in a specific monday.com board, representing a reported issue or bug, use Pipedream to automatically create a corresponding ticket in Jira. This ensures that your development team can triage and address issues efficiently without manual data transfer between systems. + +- **Lead Management Automation**: Connect monday.com to a CRM platform like Salesforce using Pipedream. Automate the creation of new leads in Salesforce when a form submission on monday.com occurs. This can aid in ensuring that leads are promptly and accurately entered into your sales pipeline, reducing the risk of lost opportunities. diff --git a/components/monday/actions/common/column-values.mjs b/components/monday/actions/common/column-values.mjs index 68f299015d115..68e40ca492e47 100644 --- a/components/monday/actions/common/column-values.mjs +++ b/components/monday/actions/common/column-values.mjs @@ -1,4 +1,5 @@ import monday from "../../monday.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; export default { props: { @@ -15,6 +16,9 @@ export default { const columns = await this.monday.listColumns({ boardId: +boardId, }); + if (!columns) { + throw new ConfigurationError(`No columns found for board ${boardId}`); + } return columns.filter(({ id }) => id !== "name"); }, formatColumnValues(items) { diff --git a/components/monday/actions/common/common-create-item.mjs b/components/monday/actions/common/common-create-item.mjs index 05b41c7f2a7c6..ae99103a97049 100644 --- a/components/monday/actions/common/common-create-item.mjs +++ b/components/monday/actions/common/common-create-item.mjs @@ -1,3 +1,6 @@ +import { + capitalizeWord, getColumnOptions, +} from "../../common/utils.mjs"; import monday from "../../monday.app.mjs"; export default { @@ -11,7 +14,7 @@ export default { }), ], type: "string[]", - description: "Select columns to fill", + description: "Select which item columns to set values for", reloadProps: true, }, }, @@ -20,26 +23,32 @@ export default { if (!this.columns) { return props; } + const columnData = await this.monday.listColumns({ + boardId: +this.boardId, + }); for (const column of this.columns) { - let description; - if (column === "status") { - description = "Value for status. [Status Index Value Map](https://view.monday.com/1073554546-ad9f20a427a16e67ded630108994c11b?r=use1)"; - } else if (column === "person") { - description = "The ID of the person/user to add to item"; + let description, options; + options = getColumnOptions(columnData, column); + if (column === "person") { + description = "The ID of a person/user"; } else if (column === "date4") { - description = "Enter date of item in YYYY-MM-DD format. Eg. `2022-09-02`"; + description = "A date string in `YYYY-MM-DD` format, e.g. `2022-09-02`"; + } else if (options) { + description = `Select a value from the list for column "${column}".`; } else { - description = `Value for column ${column}. See the [Column Type Reference](https://developer.monday.com/api-reference/docs/column-types-reference) to learn more about entering column type values.`; + description = `Value for column "${column}". See the [Column Type Reference](https://developer.monday.com/api-reference/reference/column-types-reference) to learn more about entering column type values.`; } props[column] = { type: "string", - label: column, + label: capitalizeWord(column), description, + options, }; } return props; }, methods: { + capitalizeWord, getEmailValue(value) { let email = value; if (typeof value === "string") { diff --git a/components/monday/actions/create-board/create-board.mjs b/components/monday/actions/create-board/create-board.mjs index 1a817c453f76f..50a48b120e580 100644 --- a/components/monday/actions/create-board/create-board.mjs +++ b/components/monday/actions/create-board/create-board.mjs @@ -4,9 +4,9 @@ import monday from "../../monday.app.mjs"; export default { key: "monday-create-board", name: "Create Board", - description: "Creates a new board. [See the documentation](https://api.developer.monday.com/docs/boards#create-a-board)", + description: "Creates a new board. [See the documentation](https://developer.monday.com/api-reference/reference/boards#create-a-board)", type: "action", - version: "0.0.6", + version: "0.0.8", props: { monday, boardName: { diff --git a/components/monday/actions/create-column/create-column.mjs b/components/monday/actions/create-column/create-column.mjs index 424c7521bfb4d..9cd9b0d395d79 100644 --- a/components/monday/actions/create-column/create-column.mjs +++ b/components/monday/actions/create-column/create-column.mjs @@ -1,12 +1,13 @@ import constants from "../../common/constants.mjs"; import monday from "../../monday.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; export default { key: "monday-create-column", name: "Create Column", - description: "Creates a column. [See the documentation](https://developer.monday.com/api-reference/docs/columns-queries-1)", + description: "Creates a column. [See the documentation](https://developer.monday.com/api-reference/reference/columns#create-a-column)", type: "action", - version: "0.0.6", + version: "0.1.0", props: { monday, boardId: { @@ -18,28 +19,63 @@ export default { title: { type: "string", label: "Title", - description: "The new column's title.", + description: "The title of the new column", }, columnType: { type: "string", label: "Column Type", - description: "The new column's title", + description: "The type of the new column", options: constants.COLUMN_TYPE_OPTIONS, - }, - defaults: { - type: "object", - label: "Defaults", - description: "The new column's defaults.", - optional: true, + reloadProps: true, }, description: { type: "string", label: "Description", - description: "The column's description.", + description: "The description of the new column", optional: true, }, }, + async additionalProps() { + const props = {}; + if ([ + "status", + "dropdown", + ].includes(this.columnType)) { + props.defaults = { + type: "string", + label: "Custom Labels (Defaults)", + description: "The new column's custom labels (defaults). For use with column types `status` or `dropdown`. Should be an object in the format `{ \"1\": \"Technology\", \"2\": \"Marketing\" }` where each key is the label ID and each value is the label text. [See the documentation](https://developer.monday.com/api-reference/reference/columns#create-a-status-or-dropdown-column-with-custom-labels) for more information.", + optional: true, + }; + } + return props; + }, async run({ $ }) { + let { defaults } = this; + if (defaults) { + try { + if (this.columnType === "status") { + defaults = JSON.stringify({ + labels: JSON.parse(defaults), + }); + } else if (this.columnType === "dropdown") { + const obj = JSON.parse(defaults); + defaults = JSON.stringify({ + settings: { + labels: Object.entries(obj).map(([ + id, + name, + ]) => ({ + id: Number(id), + name, + })), + }, + }); + } + } catch (err) { + throw new ConfigurationError(`Error parsing \`Custom Labels\` as JSON: "${err}"`); + } + } const { data, errors, @@ -49,7 +85,7 @@ export default { boardId: +this.boardId, title: this.title, columnType: this.columnType, - defaults: this.defaults, + defaults, description: this.description, }); diff --git a/components/monday/actions/create-group/create-group.mjs b/components/monday/actions/create-group/create-group.mjs index 8a9c6d11c2cb1..f4a4d690fb29b 100644 --- a/components/monday/actions/create-group/create-group.mjs +++ b/components/monday/actions/create-group/create-group.mjs @@ -3,9 +3,9 @@ import monday from "../../monday.app.mjs"; export default { key: "monday-create-group", name: "Create Group", - description: "Creates a new group in a specific board. [See the documentation](https://api.developer.monday.com/docs/groups-queries#create-a-group)", + description: "Creates a new group in a specific board. [See the documentation](https://developer.monday.com/api-reference/reference/groups#create-a-group)", type: "action", - version: "0.0.7", + version: "0.0.9", props: { monday, boardId: { diff --git a/components/monday/actions/create-item/create-item.mjs b/components/monday/actions/create-item/create-item.mjs index 048af1878fbfe..057ae0ff8c54a 100644 --- a/components/monday/actions/create-item/create-item.mjs +++ b/components/monday/actions/create-item/create-item.mjs @@ -6,9 +6,9 @@ export default { ...commonCreateItem, key: "monday-create-item", name: "Create Item", - description: "Creates an item. [See the documentation](https://api.developer.monday.com/docs/items-queries#create-an-item)", + description: "Creates an item. [See the documentation](https://developer.monday.com/api-reference/reference/items#create-an-item)", type: "action", - version: "0.0.9", + version: "0.1.0", props: { monday, boardId: { diff --git a/components/monday/actions/create-subitem/create-subitem.mjs b/components/monday/actions/create-subitem/create-subitem.mjs index b1ff84b45fdb7..6f4d9e47c6690 100644 --- a/components/monday/actions/create-subitem/create-subitem.mjs +++ b/components/monday/actions/create-subitem/create-subitem.mjs @@ -6,9 +6,9 @@ export default { ...commonCreateItem, key: "monday-create-subitem", name: "Create Subitem", - description: "Creates a subitem. [See the documentation](https://developer.monday.com/api-reference/docs/introduction-to-graphql#mondaycom-schema)", + description: "Creates a subitem. [See the documentation](https://developer.monday.com/api-reference/reference/subitems#create-a-subitem)", type: "action", - version: "0.0.1", + version: "0.1.0", props: { monday, boardId: { @@ -26,7 +26,7 @@ export default { }), ], optional: false, - description: "The parent item's unique identifier", + description: "Select a parent item or provide an item ID", }, itemName: { propDefinition: [ @@ -35,12 +35,6 @@ export default { ], description: "The new subitem's name", }, - createLabels: { - propDefinition: [ - monday, - "itemCreateLabels", - ], - }, ...commonCreateItem.props, }, methods: { @@ -50,7 +44,6 @@ export default { parentItemId: utils.emptyStrToUndefined(this.parentItemId), itemName: utils.emptyStrToUndefined(this.itemName), columnValues: utils.strinfied(columnValues), - createLabels: utils.emptyStrToUndefined(this.createLabels), }); }, getItemId(data) { diff --git a/components/monday/actions/create-update/create-update.mjs b/components/monday/actions/create-update/create-update.mjs index fe5d70a13a4e2..ed2a7a6f372be 100644 --- a/components/monday/actions/create-update/create-update.mjs +++ b/components/monday/actions/create-update/create-update.mjs @@ -4,9 +4,9 @@ import monday from "../../monday.app.mjs"; export default { key: "monday-create-update", name: "Create an Update", - description: "Creates a new update. [See the documentation](https://api.developer.monday.com/docs/updates-queries#create-an-update)", + description: "Creates a new update. [See the documentation](https://developer.monday.com/api-reference/reference/updates#create-an-update)", type: "action", - version: "0.0.8", + version: "0.0.11", props: { monday, updateBody: { @@ -33,7 +33,7 @@ export default { }, parentId: { label: "Parent Update ID", - description: "The parent post identifier", + description: "Select a parent update or provide an update ID", propDefinition: [ monday, "updateId", diff --git a/components/monday/actions/get-column-values/get-column-values.mjs b/components/monday/actions/get-column-values/get-column-values.mjs index 585ccd56ee3bb..287942c53eca2 100644 --- a/components/monday/actions/get-column-values/get-column-values.mjs +++ b/components/monday/actions/get-column-values/get-column-values.mjs @@ -4,8 +4,8 @@ export default { ...common, key: "monday-get-column-values", name: "Get Column Values", - description: "Return values of a specific column or columns for a board item. [See the documentation](https://developer.monday.com/api-reference/docs/column-values-v2)", - version: "0.0.3", + description: "Return values of specific column(s) for a board item. [See the documentation](https://developer.monday.com/api-reference/reference/column-values-v2)", + version: "0.0.6", type: "action", props: { ...common.props, @@ -29,7 +29,7 @@ export default { ], type: "string[]", label: "Columns", - description: "Return data from the specified column(s)", + description: "Select the column(s) to return data from", optional: true, }, }, @@ -49,7 +49,7 @@ export default { throw new Error(response.errors[0].message); } - $.export("$summary", `Successfully retrieved column values for item with ID ${this.itemId}.`); + $.export("$summary", `Successfully retrieved column values for item with ID ${this.itemId}`); return this.formatColumnValues(response.data.items); }, diff --git a/components/monday/actions/get-items-by-column-value/get-items-by-column-value.mjs b/components/monday/actions/get-items-by-column-value/get-items-by-column-value.mjs index 1d081edcf28b5..10777b3195060 100644 --- a/components/monday/actions/get-items-by-column-value/get-items-by-column-value.mjs +++ b/components/monday/actions/get-items-by-column-value/get-items-by-column-value.mjs @@ -1,11 +1,12 @@ +import { getColumnOptions } from "../../common/utils.mjs"; import common from "../common/column-values.mjs"; export default { ...common, key: "monday-get-items-by-column-value", name: "Get Items By Column Value", - description: "Searches a column for items matching a value. [See the documentation](https://developer.monday.com/api-reference/docs/items-page-by-column-values)", - version: "0.0.3", + description: "Searches a column for items matching a value. [See the documentation](https://developer.monday.com/api-reference/reference/items-page-by-column-values)", + version: "0.1.0", type: "action", props: { ...common.props, @@ -18,12 +19,26 @@ export default { }), ], description: "The column to search", + reloadProps: true, }, - value: { - type: "string", - label: "Value", - description: "The value to serach for. [See documentation](https://developer.monday.com/api-reference/docs/items-by-column-values#supported-limited-support-and-unsupported-columns) for additional information about column values.", - }, + }, + async additionalProps() { + const columnData = await this.monday.listColumns({ + boardId: +this.boardId, + }); + + const options = getColumnOptions(columnData, this.columnId, true); + + return { + value: { + type: "string", + label: "Value", + description: `The value to search for.${options + ? "" + : " [See the documentation](https://developer.monday.com/api-reference/reference/items-page-by-column-values#supported-and-unsupported-columns) for additional information about column values"} `, + options, + }, + }; }, async run({ $ }) { const response = await this.monday.getItemsByColumnValue({ diff --git a/components/monday/actions/update-column-values/update-column-values.mjs b/components/monday/actions/update-column-values/update-column-values.mjs index 6efe984bc454e..8c725ebc8f87c 100644 --- a/components/monday/actions/update-column-values/update-column-values.mjs +++ b/components/monday/actions/update-column-values/update-column-values.mjs @@ -1,17 +1,25 @@ import common from "../common/column-values.mjs"; +import { axios } from "@pipedream/platform"; +import fs from "fs"; +import FormData from "form-data"; +import { getColumnOptions } from "../../common/utils.mjs"; export default { ...common, key: "monday-update-column-values", name: "Update Column Values", - description: "Update multiple column values of an item. [See the documentation](https://developer.monday.com/api-reference/docs/columns#change-multiple-column-values)", - version: "0.0.3", + description: "Update multiple column values of an item. [See the documentation](https://developer.monday.com/api-reference/reference/columns#change-multiple-column-values)", + version: "0.1.0", type: "action", props: { ...common.props, + updateInfoBox: { + type: "alert", + alertType: "info", + content: "See the [Column types reference](https://developer.monday.com/api-reference/reference/column-types-reference) to find the proper data structures for supported column types", + }, boardId: { ...common.props.boardId, - description: "The board's unique identifier. See the [Column types reference](https://developer.monday.com/api-reference/docs/column-types-reference) to find the proper data structures for supported column types.", reloadProps: true, }, itemId: { @@ -27,24 +35,66 @@ export default { }, async additionalProps() { const props = {}; - if (this.boardId) { - const columns = await this.getColumns(this.boardId); + const { boardId } = this; + if (boardId) { + const columns = await this.monday.listColumns({ + boardId: +boardId, + }); for (const column of columns) { - props[column.id] = { + const id = column.id; + props[id] = { type: "string", label: column.title, - description: `The value for column ${column.title}`, + description: `The value for the "${column.title}" column (\`${id}\`)`, optional: true, + options: getColumnOptions(columns, id), }; + if (column.type === "file") { + props[column.id].description += ". The path to a file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)"; + } } } return props; }, + methods: { + ...common.methods, + async uploadFile({ + $, itemId, column, filePath, + }) { + const query = `mutation ($file: File!) { add_file_to_column (file: $file, item_id: ${itemId}, column_id: "${column.id}") { id } }`; + const content = fs.createReadStream(filePath.includes("tmp/") + ? filePath + : `/tmp/${filePath}`); + + const formData = new FormData(); + formData.append("query", query); + formData.append("variables[file]", content); + + return axios($, { + method: "POST", + url: "https://api.monday.com/v2/file", + headers: { + "Content-Type": `multipart/form-data; boundary=${formData._boundary}`, + "Authorization": this.monday.$auth.api_key, + }, + data: formData, + }); + }, + }, async run({ $ }) { const columns = await this.getColumns(this.boardId); const columnValues = {}; for (const column of columns) { if (this[column.id]) { + if (column.type === "file") { + await this.uploadFile({ + $, + itemId: this.itemId, + column, + filePath: this[column.id], + }); + continue; + } columnValues[column.id] = this[column.id]; } } diff --git a/components/monday/actions/update-item-name/update-item-name.mjs b/components/monday/actions/update-item-name/update-item-name.mjs index 6d308926f0b47..f0888f3bc06e2 100644 --- a/components/monday/actions/update-item-name/update-item-name.mjs +++ b/components/monday/actions/update-item-name/update-item-name.mjs @@ -3,9 +3,9 @@ import monday from "../../monday.app.mjs"; export default { key: "monday-update-item-name", name: "Update Item Name", - description: "Update an item's name. [See the documentation](https://api.developer.monday.com/docs/item-name)", + description: "Update an item's name. [See the documentation](https://developer.monday.com/api-reference/reference/columns#change-multiple-column-values)", type: "action", - version: "0.0.8", + version: "0.0.10", props: { monday, boardId: { diff --git a/components/monday/common/queries.mjs b/components/monday/common/queries.mjs index 49d0ce466ef04..0792bf8738012 100644 --- a/components/monday/common/queries.mjs +++ b/components/monday/common/queries.mjs @@ -95,6 +95,18 @@ export default { `, listColumns: ` query listColumns ($boardId: ID!) { + boards (ids: [$boardId]) { + columns { + id + settings_str + title + type + } + } + } + `, + listColumnOptions: ` + query listColumnOptions ($boardId: ID!) { boards (ids: [$boardId]) { columns { id diff --git a/components/monday/common/utils.mjs b/components/monday/common/utils.mjs index 77174847f66ff..eb8f0f2f2a168 100644 --- a/components/monday/common/utils.mjs +++ b/components/monday/common/utils.mjs @@ -1,12 +1,12 @@ function emptyStrToUndefined(value) { - const trimmed = typeof(value) === "string" && value.trim(); + const trimmed = typeof value === "string" && value.trim(); return trimmed === "" ? undefined : value; } function strinfied(value) { - return typeof(value) === "object" + return typeof value === "object" ? JSON.stringify(value) : emptyStrToUndefined(value); } @@ -18,11 +18,50 @@ function strNumber(value) { } function toNumber(value) { - return typeof(value) === "number" + return typeof value === "number" ? value : strNumber(value); } +export function capitalizeWord(str) { + return str.slice(0, 1).toUpperCase() + str.slice(1); +} + +export function getColumnOptions(allColumnData, columnId, useLabels = false) { + const columnOptions = allColumnData.find( + ({ id }) => id === columnId, + )?.settings_str; + if (columnOptions) { + try { + const labels = JSON.parse(columnOptions).labels; + return (Array.isArray(labels) + ? labels.map(({ + id, name, + }) => useLabels + ? name + : ({ + label: name, + value: id.toString(), + })) + : Object.entries(labels).map( + ([ + value, + label, + ]) => useLabels + ? label + : ({ + label: label !== "" + ? label + : value, + value, + }), + )).filter((str) => str); + } catch (err) { + console.log(`Error parsing options for column "${columnId}": ${err}`); + } + } +} + export default { emptyStrToUndefined, strinfied, diff --git a/components/monday/monday.app.mjs b/components/monday/monday.app.mjs index 51d66e26702e4..da43d4db51f6d 100644 --- a/components/monday/monday.app.mjs +++ b/components/monday/monday.app.mjs @@ -13,7 +13,7 @@ export default { boardId: { type: "string", label: "Board ID", - description: "The board's unique identifier", + description: "Select a board, or provide a board ID", async options({ page }) { return this.listBoardsOptions({ page: page + 1, @@ -23,18 +23,18 @@ export default { boardName: { type: "string", label: "Board Name", - description: "The board's name", + description: "The new board's name", }, boardKind: { type: "string", label: "Board Kind", - description: "The board's kind (`public` / `private` / `share`)", + description: "The new board's kind (`public` / `private` / `share`)", options: constants.BOARD_KIND_OPTIONS, }, folderId: { type: "integer", label: "Folder ID", - description: "Board folder ID", + description: "Optionally select a folder to create the board in, or provide a folder ID", optional: true, async options({ workspaceId }) { return this.listFolderOptions({ @@ -45,7 +45,7 @@ export default { workspaceId: { type: "integer", label: "Workspace ID", - description: "Board workspace ID. If you don't specify this field, the board will be created in the **Main Workspace**", + description: "Select a workspace to create the board in, or provide a workspace ID. If not specified, the **Main Workspace** will be used", optional: true, async options() { return this.listWorkspacesOptions(); @@ -54,7 +54,7 @@ export default { templateId: { type: "integer", label: "Board Template ID", - description: "Board ID saved as a custom template. The ID can be obteined from the url in browser selecting the corresponding board (e.g. `https://{subdomain}.monday.com/boards/2419687965`) where `2419687965` is the ID of the template", + description: "The board's template ID. You can obtain it from the URL when selecting the desired board (e.g. `https://{subdomain}.monday.com/boards/2419687965`) where `2419687965` is the template ID. [See the documentation](https://developer.monday.com/api-reference/reference/boards#create-a-board) for more information", optional: true, }, groupName: { @@ -65,7 +65,7 @@ export default { groupId: { type: "string", label: "Group ID", - description: "The group's unique identifier", + description: "Select a group or provide a group ID", optional: true, async options({ boardId }) { return this.listGroupsOptions({ @@ -76,7 +76,7 @@ export default { itemName: { type: "string", label: "Item Name", - description: "The item's name", + description: "The new item's name", }, itemColumnValues: { type: "object", @@ -98,7 +98,7 @@ export default { itemId: { type: "string", label: "Item ID", - description: "The item's unique identifier", + description: "Select an item or provide an item ID", optional: true, async options({ boardId, prevContext, @@ -112,7 +112,7 @@ export default { updateId: { type: "string", label: "Update ID", - description: "The update's unique identifier", + description: "Select an update or provide an update ID", optional: true, async options({ page, boardId, @@ -126,17 +126,17 @@ export default { column: { type: "string", label: "Column", - description: "Column to watch for changes", + description: "Select a column to watch for changes", async options({ boardId }) { - const columns = await this.listColumns({ + const columns = await this.listColumnOptions({ boardId: +boardId, }); return columns - .filter((column) => column.id !== "name") + ?.filter((column) => column.id !== "name") .map((column) => ({ label: column.title, value: column.id, - })); + })) ?? []; }, }, }, @@ -307,6 +307,15 @@ export default { }, }); }, + async listColumnOptions(variables) { + const { data } = await this.makeRequest({ + query: queries.listColumnOptions, + options: { + variables, + }, + }); + return data?.boards[0]?.columns; + }, async listColumns(variables) { const { data } = await this.makeRequest({ query: queries.listColumns, @@ -376,13 +385,13 @@ export default { const { boards } = data; return boards - .filter(({ type }) => type !== constants.BOARD_TYPE.SUB_ITEMS_BOARD) + ?.filter(({ type }) => type !== constants.BOARD_TYPE.SUB_ITEMS_BOARD) .map(({ id, name, }) => ({ label: name, value: id, - })); + })) ?? []; }, async listFolderOptions(variables) { const { diff --git a/components/monday/package.json b/components/monday/package.json index 6e6958297b7ee..ce48c1bb44f7c 100644 --- a/components/monday/package.json +++ b/components/monday/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/monday", - "version": "0.6.0", + "version": "0.7.0", "description": "Pipedream Monday Components", "main": "monday.app.mjs", "keywords": [ @@ -14,7 +14,8 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.6.0", + "@pipedream/platform": "^3.0.3", + "form-data": "^4.0.0", "lodash.flatmap": "^4.5.0", "lodash.map": "^4.6.0", "lodash.uniqby": "^4.7.0", diff --git a/components/monday/sources/column-value-updated/column-value-updated.mjs b/components/monday/sources/column-value-updated/column-value-updated.mjs index 0ac502b624e0d..b9c24655cc029 100644 --- a/components/monday/sources/column-value-updated/column-value-updated.mjs +++ b/components/monday/sources/column-value-updated/column-value-updated.mjs @@ -3,10 +3,10 @@ import common from "../common/common-webhook.mjs"; export default { ...common, key: "monday-column-value-updated", - name: "New Column Value Updated (Instant)", - description: "Emit new event when a column value is updated on a board in Monday. For changes to Name, use the Name Updated Trigger.", + name: "Column Value Updated (Instant)", + description: "Emit new event when a column value is updated on a board. [See the documentation](https://developer.monday.com/api-reference/reference/webhooks#sample-payload-for-webhook-events)", type: "source", - version: "0.0.6", + version: "0.0.8", dedupe: "unique", hooks: { ...common.hooks, @@ -14,6 +14,14 @@ export default { await this.commonDeploy(); }, }, + props: { + ...common.props, + alertBox: { + type: "alert", + alertType: "warning", + content: "For changes to `Name`, use the **Name Updated** trigger", + }, + }, methods: { ...common.methods, getWebhookArgs() { diff --git a/components/monday/sources/name-updated/name-updated.mjs b/components/monday/sources/name-updated/name-updated.mjs index 12944443dfa45..bf276e44f5172 100644 --- a/components/monday/sources/name-updated/name-updated.mjs +++ b/components/monday/sources/name-updated/name-updated.mjs @@ -3,10 +3,10 @@ import common from "../common/common-webhook.mjs"; export default { ...common, key: "monday-name-updated", - name: "New Name Updated (Instant)", - description: "Emit new event when an item's Name is updated on a board in Monday.", + name: "Name Updated (Instant)", + description: "Emit new event when an item's name is updated. [See the documentation](https://developer.monday.com/api-reference/reference/webhooks#sample-payload-for-webhook-events)", type: "source", - version: "0.0.6", + version: "0.0.8", dedupe: "unique", hooks: { ...common.hooks, diff --git a/components/monday/sources/new-board/new-board.mjs b/components/monday/sources/new-board/new-board.mjs index 6288268152d11..1b6b6ffc9a904 100644 --- a/components/monday/sources/new-board/new-board.mjs +++ b/components/monday/sources/new-board/new-board.mjs @@ -3,10 +3,10 @@ import common from "../common/common-polling.mjs"; export default { ...common, key: "monday-new-board", - name: "New Board", - description: "Emit new event when a new board is created in Monday.", + name: "New Board Created", + description: "Emit new event when a board is created in Monday. [See the documentation](https://developer.monday.com/api-reference/reference/webhooks#sample-payload-for-webhook-events)", type: "source", - version: "0.0.7", + version: "0.0.9", dedupe: "unique", props: { ...common.props, @@ -14,7 +14,7 @@ export default { type: "integer", min: 1, label: "Max API Requests per Execution", - description: "The maximum number of API requests to make per execution (e.g., multiple requests are required to retrieve paginated results)", + description: "The maximum number of API requests to make per execution (multiple requests are required to retrieve paginated results)", optional: true, default: 1, }, diff --git a/components/monday/sources/new-item/new-item.mjs b/components/monday/sources/new-item/new-item.mjs index 1e2e3a50baeec..03e1b00613ca1 100644 --- a/components/monday/sources/new-item/new-item.mjs +++ b/components/monday/sources/new-item/new-item.mjs @@ -3,10 +3,10 @@ import common from "../common/common-webhook.mjs"; export default { ...common, key: "monday-new-item", - name: "New Item (Instant)", - description: "Emit new event when a new item is added to a board in Monday.", + name: "New Item Created (Instant)", + description: "Emit new event when a new item is added to a board. [See the documentation](https://developer.monday.com/api-reference/reference/webhooks#sample-payload-for-webhook-events)", type: "source", - version: "0.0.6", + version: "0.0.8", dedupe: "unique", hooks: { ...common.hooks, diff --git a/components/monday/sources/new-subitem-update/new-subitem-update.mjs b/components/monday/sources/new-subitem-update/new-subitem-update.mjs index 67d60f8a0dd95..9fa17bc4c06c1 100644 --- a/components/monday/sources/new-subitem-update/new-subitem-update.mjs +++ b/components/monday/sources/new-subitem-update/new-subitem-update.mjs @@ -4,12 +4,17 @@ export default { ...common, key: "monday-new-subitem-update", name: "New Sub-Item Update (Instant)", - description: "Emit new event when an update is posted in sub-items. To create this trigger, you need to have at least one subitem previously created on your board.", + description: "Emit new event when an update is posted in sub-items. [See the documentation](https://developer.monday.com/api-reference/reference/webhooks#sample-payload-for-webhook-events)", type: "source", - version: "0.0.5", + version: "0.0.7", dedupe: "unique", props: { ...common.props, + alertBox: { + type: "alert", + alertType: "warning", + content: "To create this trigger, you need to have at least one subitem previously created on your board", + }, boardId: { propDefinition: [ common.props.monday, @@ -20,7 +25,7 @@ export default { methods: { ...common.methods, getWebhookCreationError() { - return "Failed to establish webhook. To create this trigger, you need to have at least one subitem previously created on your board."; + return "Failed to establish webhook. To create this trigger, you need to have at least one subitem previously created on your board"; }, getWebhookArgs() { return { diff --git a/components/monday/sources/new-subitem/new-subitem.mjs b/components/monday/sources/new-subitem/new-subitem.mjs index 0708f874c16ce..26942653260b0 100644 --- a/components/monday/sources/new-subitem/new-subitem.mjs +++ b/components/monday/sources/new-subitem/new-subitem.mjs @@ -3,13 +3,18 @@ import common from "../common/common-webhook.mjs"; export default { ...common, key: "monday-new-subitem", - name: "New Sub-Item (Instant)", - description: "Emit new event when a sub-item is created. To create this trigger, you need to have at least one subitem previously created on your board.", + name: "New Sub-Item Created (Instant)", + description: "Emit new event when a sub-item is created. [See the documentation](https://developer.monday.com/api-reference/reference/webhooks#sample-payload-for-webhook-events)", type: "source", - version: "0.0.5", + version: "0.0.7", dedupe: "unique", props: { ...common.props, + alertBox: { + type: "alert", + alertType: "warning", + content: "To create this trigger, you need to have at least one subitem previously created on your board", + }, boardId: { propDefinition: [ common.props.monday, @@ -20,7 +25,7 @@ export default { methods: { ...common.methods, getWebhookCreationError() { - return "Failed to establish webhook. To create this trigger, you need to have at least one subitem previously created on your board."; + return "Failed to establish webhook. To create this trigger, you need to have at least one subitem previously created on your board"; }, getWebhookArgs() { return { diff --git a/components/monday/sources/new-user/new-user.mjs b/components/monday/sources/new-user/new-user.mjs index d24db18babd18..146b70e3e112f 100644 --- a/components/monday/sources/new-user/new-user.mjs +++ b/components/monday/sources/new-user/new-user.mjs @@ -3,10 +3,10 @@ import common from "../common/common-polling.mjs"; export default { ...common, key: "monday-new-user", - name: "New User", - description: "Emit new event when a new user is created in Monday.", + name: "New User Created", + description: "Emit new event when a new user is created in Monday. [See the documentation](https://developer.monday.com/api-reference/reference/webhooks#sample-payload-for-webhook-events)", type: "source", - version: "0.0.7", + version: "0.0.9", dedupe: "unique", methods: { ...common.methods, diff --git a/components/monday/sources/specific-column-updated/specific-column-updated.mjs b/components/monday/sources/specific-column-updated/specific-column-updated.mjs index 7ff193eab245c..da0616a946429 100644 --- a/components/monday/sources/specific-column-updated/specific-column-updated.mjs +++ b/components/monday/sources/specific-column-updated/specific-column-updated.mjs @@ -3,10 +3,10 @@ import common from "../common/common-webhook.mjs"; export default { ...common, key: "monday-specific-column-updated", - name: "New Specific Column Updated (Instant)", - description: "Emit new event when a value in the specified column is updated on a board in Monday. For changes to Name, use the Name Updated Trigger.", + name: "Specific Column Updated (Instant)", + description: "Emit new event when a value in the specified column is updated. [See the documentation](https://developer.monday.com/api-reference/reference/webhooks#sample-payload-for-webhook-events)", type: "source", - version: "0.0.6", + version: "0.0.8", dedupe: "unique", hooks: { ...common.hooks, @@ -16,6 +16,11 @@ export default { }, props: { ...common.props, + alertBox: { + type: "alert", + alertType: "warning", + content: "For changes to `Name`, use the **Name Updated** trigger", + }, column: { propDefinition: [ common.props.monday, diff --git a/components/monday/sources/subitem-column-value-updated/subitem-column-value-updated.mjs b/components/monday/sources/subitem-column-value-updated/subitem-column-value-updated.mjs index 8c56f5fe2e16d..34c6191b80e34 100644 --- a/components/monday/sources/subitem-column-value-updated/subitem-column-value-updated.mjs +++ b/components/monday/sources/subitem-column-value-updated/subitem-column-value-updated.mjs @@ -3,13 +3,18 @@ import common from "../common/common-webhook.mjs"; export default { ...common, key: "monday-subitem-column-value-updated", - name: "New Sub-Item Column Value Updated (Instant)", - description: "Emit new event when any sub-item column changes. To create this trigger, you need to have at least one subitem previously created on your board.", + name: "Sub-Item Column Value Updated (Instant)", + description: "Emit new event when any sub-item column changes. [See the documentation](https://developer.monday.com/api-reference/reference/webhooks#sample-payload-for-webhook-events)", type: "source", - version: "0.0.6", + version: "0.0.8", dedupe: "unique", props: { ...common.props, + alertBox: { + type: "alert", + alertType: "warning", + content: "To create this trigger, you need to have at least one subitem previously created on your board", + }, boardId: { propDefinition: [ common.props.monday, @@ -20,7 +25,7 @@ export default { methods: { ...common.methods, getWebhookCreationError() { - return "Failed to establish webhook. To create this trigger, you need to have at least one subitem previously created on your board."; + return "Failed to establish webhook. To create this trigger, you need to have at least one subitem previously created on your board"; }, getWebhookArgs() { return { diff --git a/components/monday/sources/subitem-name-updated/subitem-name-updated.mjs b/components/monday/sources/subitem-name-updated/subitem-name-updated.mjs index 21d5747e9f250..39dc57d4ac159 100644 --- a/components/monday/sources/subitem-name-updated/subitem-name-updated.mjs +++ b/components/monday/sources/subitem-name-updated/subitem-name-updated.mjs @@ -3,13 +3,18 @@ import common from "../common/common-webhook.mjs"; export default { ...common, key: "monday-subitem-name-updated", - name: "New Sub-Item Name Updated (Instant)", - description: "Emit new event when a sub-item name changes. To create this trigger, you need to have at least one subitem previously created on your board.", + name: "Sub-Item Name Updated (Instant)", + description: "Emit new event when a sub-item name changes. [See the documentation](https://developer.monday.com/api-reference/reference/webhooks#sample-payload-for-webhook-events)", type: "source", - version: "0.0.5", + version: "0.0.7", dedupe: "unique", props: { ...common.props, + alertBox: { + type: "alert", + alertType: "warning", + content: "To create this trigger, you need to have at least one subitem previously created on your board", + }, boardId: { propDefinition: [ common.props.monday, @@ -20,7 +25,7 @@ export default { methods: { ...common.methods, getWebhookCreationError() { - return "Failed to establish webhook. To create this trigger, you need to have at least one subitem previously created on your board."; + return "Failed to establish webhook. To create this trigger, you need to have at least one subitem previously created on your board"; }, getWebhookArgs() { return { diff --git a/components/monday_oauth/README.md b/components/monday_oauth/README.md new file mode 100644 index 0000000000000..3b76f0f3b51e2 --- /dev/null +++ b/components/monday_oauth/README.md @@ -0,0 +1,16 @@ +# Overview + +The monday.com API enables developers to programmatically interact with their monday.com workspace, allowing for automated workflows, data management, and integrations with other applications. Using Pipedream's serverless platform, you can leverage this API to create custom workflows that respond to board updates, manage items automatically, and connect monday.com with hundreds of other apps. + +You can connect your monday.com account in a few different ways: +1. Within the workflow builder, from a trigger or action step. Search for the monday.com app, and connect your account. +2. From the Pipedream Accounts [page](https://pipedream.com/accounts): Go to Accounts > Connect an app > Search for "monday.com". +3. Run the example Node JS code below by clicking "Connect monday.com and run", and choose a project and create an example workflow to interact with the monday.com API. + +# Example Use Cases + +- **Task Management Automation**: Automatically create and update tasks in monday.com when tickets are created in your support system. When a new support ticket comes in, Pipedream can capture the details and create a corresponding item in monday.com, keeping your team's task board current. + +- **Project Status Updates**: Sync project statuses between monday.com and other project management tools. When a status changes in monday.com, Pipedream can update the corresponding project in tools like Jira or Asana, ensuring all systems remain synchronized. + +- **Team Communication Integration**: Connect monday.com updates to your team communication platforms. When important changes occur in monday.com boards, Pipedream can send notifications to Slack or Microsoft Teams, keeping your team informed of critical updates. \ No newline at end of file diff --git a/components/monday_oauth/actions/create-board/create-board.mjs b/components/monday_oauth/actions/create-board/create-board.mjs new file mode 100644 index 0000000000000..c8b1692c4e1d8 --- /dev/null +++ b/components/monday_oauth/actions/create-board/create-board.mjs @@ -0,0 +1,22 @@ +import app from "../../monday_oauth.app.mjs"; +import common from "@pipedream/monday/actions/create-board/create-board.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "monday_oauth-create-board", + version: "0.0.3", + name, + description, + type, + props: { + monday: app, + ...props, + }, +}; diff --git a/components/monday_oauth/actions/create-column/create-column.mjs b/components/monday_oauth/actions/create-column/create-column.mjs new file mode 100644 index 0000000000000..f4e25d996468c --- /dev/null +++ b/components/monday_oauth/actions/create-column/create-column.mjs @@ -0,0 +1,22 @@ +import app from "../../monday_oauth.app.mjs"; +import common from "@pipedream/monday/actions/create-column/create-column.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "monday_oauth-create-column", + version: "0.1.0", + name, + description, + type, + props: { + monday: app, + ...props, + }, +}; diff --git a/components/monday_oauth/actions/create-group/create-group.mjs b/components/monday_oauth/actions/create-group/create-group.mjs new file mode 100644 index 0000000000000..fb75b275ad5ba --- /dev/null +++ b/components/monday_oauth/actions/create-group/create-group.mjs @@ -0,0 +1,22 @@ +import app from "../../monday_oauth.app.mjs"; +import common from "@pipedream/monday/actions/create-group/create-group.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "monday_oauth-create-group", + version: "0.0.3", + name, + description, + type, + props: { + monday: app, + ...props, + }, +}; diff --git a/components/monday_oauth/actions/create-item/create-item.mjs b/components/monday_oauth/actions/create-item/create-item.mjs new file mode 100644 index 0000000000000..26406367c6efd --- /dev/null +++ b/components/monday_oauth/actions/create-item/create-item.mjs @@ -0,0 +1,22 @@ +import app from "../../monday_oauth.app.mjs"; +import common from "@pipedream/monday/actions/create-item/create-item.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "monday_oauth-create-item", + version: "0.1.0", + name, + description, + type, + props: { + monday: app, + ...props, + }, +}; diff --git a/components/monday_oauth/actions/create-subitem/create-subitem.mjs b/components/monday_oauth/actions/create-subitem/create-subitem.mjs new file mode 100644 index 0000000000000..c2e1e4b7cae3b --- /dev/null +++ b/components/monday_oauth/actions/create-subitem/create-subitem.mjs @@ -0,0 +1,22 @@ +import app from "../../monday_oauth.app.mjs"; +import common from "@pipedream/monday/actions/create-subitem/create-subitem.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "monday_oauth-create-subitem", + version: "0.1.0", + name, + description, + type, + props: { + monday: app, + ...props, + }, +}; diff --git a/components/monday_oauth/actions/create-update/create-update.mjs b/components/monday_oauth/actions/create-update/create-update.mjs new file mode 100644 index 0000000000000..2533a2f5fd958 --- /dev/null +++ b/components/monday_oauth/actions/create-update/create-update.mjs @@ -0,0 +1,22 @@ +import app from "../../monday_oauth.app.mjs"; +import common from "@pipedream/monday/actions/create-update/create-update.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "monday_oauth-create-update", + version: "0.0.3", + name, + description, + type, + props: { + monday: app, + ...props, + }, +}; diff --git a/components/monday_oauth/actions/get-column-values/get-column-values.mjs b/components/monday_oauth/actions/get-column-values/get-column-values.mjs new file mode 100644 index 0000000000000..4b6ca502c99bb --- /dev/null +++ b/components/monday_oauth/actions/get-column-values/get-column-values.mjs @@ -0,0 +1,22 @@ +import app from "../../monday_oauth.app.mjs"; +import common from "@pipedream/monday/actions/get-column-values/get-column-values.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "monday_oauth-get-column-values", + version: "0.0.3", + name, + description, + type, + props: { + monday: app, + ...props, + }, +}; diff --git a/components/monday_oauth/actions/get-items-by-column-value/get-items-by-column-value.mjs b/components/monday_oauth/actions/get-items-by-column-value/get-items-by-column-value.mjs new file mode 100644 index 0000000000000..b0cd786cf74e5 --- /dev/null +++ b/components/monday_oauth/actions/get-items-by-column-value/get-items-by-column-value.mjs @@ -0,0 +1,22 @@ +import app from "../../monday_oauth.app.mjs"; +import common from "@pipedream/monday/actions/get-items-by-column-value/get-items-by-column-value.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "monday_oauth-get-items-by-column-value", + version: "0.1.0", + name, + description, + type, + props: { + monday: app, + ...props, + }, +}; diff --git a/components/monday_oauth/actions/update-column-values/update-column-values.mjs b/components/monday_oauth/actions/update-column-values/update-column-values.mjs new file mode 100644 index 0000000000000..33af2d0b6eb97 --- /dev/null +++ b/components/monday_oauth/actions/update-column-values/update-column-values.mjs @@ -0,0 +1,22 @@ +import app from "../../monday_oauth.app.mjs"; +import common from "@pipedream/monday/actions/update-column-values/update-column-values.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "monday_oauth-update-column-values", + version: "0.1.0", + name, + description, + type, + props: { + monday: app, + ...props, + }, +}; diff --git a/components/monday_oauth/actions/update-item-name/update-item-name.mjs b/components/monday_oauth/actions/update-item-name/update-item-name.mjs new file mode 100644 index 0000000000000..75e0f295ee53d --- /dev/null +++ b/components/monday_oauth/actions/update-item-name/update-item-name.mjs @@ -0,0 +1,22 @@ +import app from "../../monday_oauth.app.mjs"; +import common from "@pipedream/monday/actions/update-item-name/update-item-name.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "monday_oauth-update-item-name", + version: "0.0.3", + name, + description, + type, + props: { + monday: app, + ...props, + }, +}; diff --git a/components/monday_oauth/common/utils.mjs b/components/monday_oauth/common/utils.mjs new file mode 100644 index 0000000000000..d42df055ddeb0 --- /dev/null +++ b/components/monday_oauth/common/utils.mjs @@ -0,0 +1,40 @@ +export function adjustPropDefinitions(props, app) { + return Object.fromEntries( + Object.entries(props).map(([ + key, + prop, + ]) => { + if (typeof prop === "string") return [ + key, + prop, + ]; + const { + propDefinition, ...otherValues + } = prop; + if (propDefinition) { + const [ + , ...otherDefs + ] = propDefinition; + return [ + key, + { + propDefinition: [ + app, + ...otherDefs, + ], + ...otherValues, + }, + ]; + } + return [ + key, + otherValues.type === "app" + ? null + : otherValues, + ]; + }) + .filter(([ + , value, + ]) => value), + ); +} diff --git a/components/monday_oauth/monday_oauth.app.mjs b/components/monday_oauth/monday_oauth.app.mjs new file mode 100644 index 0000000000000..64c60d27825c2 --- /dev/null +++ b/components/monday_oauth/monday_oauth.app.mjs @@ -0,0 +1,17 @@ +import common from "@pipedream/monday"; +import mondaySdk from "monday-sdk-js"; + +export default { + ...common, + app: "monday_oauth", + methods: { + ...common.methods, + async makeRequest({ + query, options, + }) { + const monday = mondaySdk(); + monday.setToken(this.$auth.oauth_access_token); + return monday.api(query, options); + }, + }, +}; diff --git a/components/monday_oauth/package.json b/components/monday_oauth/package.json new file mode 100644 index 0000000000000..d147722a3e147 --- /dev/null +++ b/components/monday_oauth/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/monday_oauth", + "version": "0.2.0", + "description": "Pipedream monday.com (OAuth) Components", + "main": "monday_oauth.app.mjs", + "keywords": [ + "pipedream", + "monday_oauth" + ], + "homepage": "https://pipedream.com/apps/monday_oauth", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/monday": "^0.7.0", + "monday-sdk-js": "^0.5.5" + } +} diff --git a/components/monday_oauth/sources/column-value-updated/column-value-updated.mjs b/components/monday_oauth/sources/column-value-updated/column-value-updated.mjs new file mode 100644 index 0000000000000..c0da666b6e103 --- /dev/null +++ b/components/monday_oauth/sources/column-value-updated/column-value-updated.mjs @@ -0,0 +1,22 @@ +import app from "../../monday_oauth.app.mjs"; +import common from "@pipedream/monday/sources/column-value-updated/column-value-updated.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "monday_oauth-column-value-updated", + version: "0.0.3", + name, + description, + type, + props: { + monday: app, + ...props, + }, +}; diff --git a/components/monday_oauth/sources/name-updated/name-updated.mjs b/components/monday_oauth/sources/name-updated/name-updated.mjs new file mode 100644 index 0000000000000..81347b49ef2ee --- /dev/null +++ b/components/monday_oauth/sources/name-updated/name-updated.mjs @@ -0,0 +1,22 @@ +import app from "../../monday_oauth.app.mjs"; +import common from "@pipedream/monday/sources/name-updated/name-updated.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "monday_oauth-name-updated", + version: "0.0.3", + name, + description, + type, + props: { + monday: app, + ...props, + }, +}; diff --git a/components/monday_oauth/sources/new-board/new-board.mjs b/components/monday_oauth/sources/new-board/new-board.mjs new file mode 100644 index 0000000000000..e638adcb996b6 --- /dev/null +++ b/components/monday_oauth/sources/new-board/new-board.mjs @@ -0,0 +1,22 @@ +import app from "../../monday_oauth.app.mjs"; +import common from "@pipedream/monday/sources/new-board/new-board.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "monday_oauth-new-board", + version: "0.0.3", + name, + description, + type, + props: { + monday: app, + ...props, + }, +}; diff --git a/components/monday_oauth/sources/new-item/new-item.mjs b/components/monday_oauth/sources/new-item/new-item.mjs new file mode 100644 index 0000000000000..ffab036923101 --- /dev/null +++ b/components/monday_oauth/sources/new-item/new-item.mjs @@ -0,0 +1,22 @@ +import app from "../../monday_oauth.app.mjs"; +import common from "@pipedream/monday/sources/new-item/new-item.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "monday_oauth-new-item", + version: "0.0.3", + name, + description, + type, + props: { + monday: app, + ...props, + }, +}; diff --git a/components/monday_oauth/sources/new-subitem-update/new-subitem-update.mjs b/components/monday_oauth/sources/new-subitem-update/new-subitem-update.mjs new file mode 100644 index 0000000000000..c344b0f024bed --- /dev/null +++ b/components/monday_oauth/sources/new-subitem-update/new-subitem-update.mjs @@ -0,0 +1,22 @@ +import app from "../../monday_oauth.app.mjs"; +import common from "@pipedream/monday/sources/new-subitem-update/new-subitem-update.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "monday_oauth-new-subitem-update", + version: "0.0.3", + name, + description, + type, + props: { + monday: app, + ...props, + }, +}; diff --git a/components/monday_oauth/sources/new-subitem/new-subitem.mjs b/components/monday_oauth/sources/new-subitem/new-subitem.mjs new file mode 100644 index 0000000000000..706c7ac12f85b --- /dev/null +++ b/components/monday_oauth/sources/new-subitem/new-subitem.mjs @@ -0,0 +1,22 @@ +import app from "../../monday_oauth.app.mjs"; +import common from "@pipedream/monday/sources/new-subitem/new-subitem.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "monday_oauth-new-subitem", + version: "0.0.3", + name, + description, + type, + props: { + monday: app, + ...props, + }, +}; diff --git a/components/monday_oauth/sources/subitem-column-value-updated/subitem-column-value-updated.mjs b/components/monday_oauth/sources/subitem-column-value-updated/subitem-column-value-updated.mjs new file mode 100644 index 0000000000000..3bcaa1c34e393 --- /dev/null +++ b/components/monday_oauth/sources/subitem-column-value-updated/subitem-column-value-updated.mjs @@ -0,0 +1,22 @@ +import app from "../../monday_oauth.app.mjs"; +import common from "@pipedream/monday/sources/subitem-column-value-updated/subitem-column-value-updated.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "monday_oauth-subitem-column-value-updated", + version: "0.0.3", + name, + description, + type, + props: { + monday: app, + ...props, + }, +}; diff --git a/components/monday_oauth/sources/subitem-name-updated/subitem-name-updated.mjs b/components/monday_oauth/sources/subitem-name-updated/subitem-name-updated.mjs new file mode 100644 index 0000000000000..b8b896698bb93 --- /dev/null +++ b/components/monday_oauth/sources/subitem-name-updated/subitem-name-updated.mjs @@ -0,0 +1,22 @@ +import app from "../../monday_oauth.app.mjs"; +import common from "@pipedream/monday/sources/subitem-name-updated/subitem-name-updated.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "monday_oauth-subitem-name-updated", + version: "0.0.3", + name, + description, + type, + props: { + monday: app, + ...props, + }, +}; diff --git a/components/moneybird/README.md b/components/moneybird/README.md index c72debd67ec9d..55f283d7d1f2e 100644 --- a/components/moneybird/README.md +++ b/components/moneybird/README.md @@ -1,9 +1,11 @@ # Overview -With the Moneybird API, you can build applications that can: +The Moneybird API allows you to automate your finance and accounting tasks by enabling programmatic access to your Moneybird account. With the API, you can create invoices, manage contacts, record payments, and fetch financial reports. This integration with Pipedream allows you to streamline your financial operations by connecting Moneybird with other apps and services to automate workflows, sync data, and trigger actions based on financial events. -- Create and manage invoices -- Create and manage contacts -- Create and manage sales invoices -- Manage products and services -- And much more! +# Example Use Cases + +- **Automated Invoice Creation and Delivery**: Using the Moneybird API on Pipedream, you can monitor new orders from your e-commerce platform (like Shopify) and automatically generate and send invoices in Moneybird. Once an order is detected, a Pipedream workflow can create an invoice and email it to the customer, without manual intervention. + +- **Expense Tracking and Reporting**: Integrate Moneybird with a time-tracking app like Toggl to automate the recording of billable hours as expenses in Moneybird. As time entries are logged in Toggl, a Pipedream workflow can translate these into expenses in Moneybird, and at the end of the month, compile these expenses into a report that's sent to your email. + +- **Payment Status Webhooks and Notifications**: With Pipedream, set up a workflow that listens for webhook events from Moneybird indicating payment status changes. Upon receiving a notification that a payment has been made, the workflow can trigger a Slack message to a designated channel, notifying your team in real time of the updated payment status. diff --git a/components/mongodb/README.md b/components/mongodb/README.md index 55d152d878fa4..4a478f9738bf3 100644 --- a/components/mongodb/README.md +++ b/components/mongodb/README.md @@ -1,12 +1,75 @@ # Overview -Assuming you want a few paragraph about the MongoDB API: - -MongoDB offers a powerful API that allows developers to interact with their -databases in a variety of ways. The API provides methods for performing -standard CRUD (create, read, update, delete) operations, as well as more -specialized methods like bulk write operations and aggregate queries. MongoDB -also offers a rich set of indexing options that allow developers to optimize -their queries for maximum performance. With the MongoDB API, developers can -build a wide variety of applications, from simple data-driven apps to complex -event-driven systems. +The MongoDB API provides powerful capabilities to interact with a MongoDB database, allowing you to perform CRUD (Create, Read, Update, Delete) operations, manage databases, and execute sophisticated queries. With Pipedream, you can harness these abilities to automate tasks, sync data across various apps, and react to events in real-time. It’s a combo that’s particularly potent for managing data workflows, syncing application states, or triggering actions based on changes to your data. + +# Example Use Cases + +- **Real-time Data Syncing with Google Sheets**: Sync new MongoDB document entries to a Google Sheet for easy sharing and reporting. Whenever a new customer is added to the MongoDB database, a Pipedream workflow triggers and appends the customer data to a Google Sheet, keeping your sales team updated in real-time. + +- **Automated Backup Notifications via Email**: Set up a daily database backup and use Pipedream to monitor the completion status of the backup operation. Once a backup is successfully completed, trigger an SMTP action to send an email notification to the system admin, ensuring peace of mind with regular backup status updates. + +- **Slack Alerts for High-Value Transactions**: Monitor your MongoDB for transactions exceeding a certain value and send alerts to a Slack channel when such transactions occur. Finance teams can stay on top of significant movements without manual database checks, improving response time and financial oversight. + +# Getting Started + +To get started, you need to set up a user to connect to your MongoDB cluster. This guide assumes your MongoDB cluster is hosted on Mongo Atlas. + +First, [log in to Mongo Atlas](https://account.mongodb.com/account/login) to complete the next steps. + +## Create the MongoDB User + +First, create a dedicated user for connecting MongoDB to Pipedream. + +Select the MongoDB project where your cluster resides, then select the *Database Access* tab. Here, you'll see a list of users who can connect to your MongoDB cluster. + +Click *Create new database user* to get started. + +![Open the MongoDB project from within MongoDB Atlas and select the Database Access panel, then create the user](https://res.cloudinary.com/pipedreamin/image/upload/v1715025845/marketplace/apps/mongodb/CleanShot_2024-05-06_at_16.02.42_apamii.png) + +In the new pop-up, choose the *Password* authentication method first. Name the user `pipedream` so it’s easy to remember which service this user account is used for. + +Generate a password and select a pre-built or custom role. Pre-built roles offer options like read-only or read and write access to all databases in the cluster. For more fine-tuned control, like restricting Pipedream to reading a single database, use a custom role. + +Finally click *Add user* to create this new user account. + +![Filling out the details of the new user account in MongoDB Atlas for connecting this new user to Pipedream](https://res.cloudinary.com/pipedreamin/image/upload/v1715026023/marketplace/apps/mongodb/CleanShot_2024-05-06_at_16.06.22_rfz4ur.png) + +## Provide the Credentials + +Now, with a dedicated MongoDB user, connect to Pipedream. Return to Pipedream to set up a new MongoDB action in a workflow or to add your MongoDB account in the Connected Accounts area. + +Copy and paste the *Username* and *Password* fields from the previous step, when you created the `pipedream` user in Mongo Atlas. + +To find your database hostname, go to the **Databases** view in Mongo Atlas and click the **Connect** button for your chosen database. + +Select the **Compass or Node.js** option. Then you’ll see the connection URI string. The hostname is the latter portion of the string, as shown below: + +![Finding the MongoDB database host name in MongoDB Atlas by using the Connect button to show the connection URI](https://res.cloudinary.com/pipedreamin/image/upload/v1715026375/marketplace/apps/mongodb/CleanShot_2024-05-06_at_16.12.19_sgr7wd.png) + +Lastly, provide the **Database** name to connect to. + +## Allowing Pipedream Connections + +By default, Pipedream workflows can start anywhere within the `us-east-1` AWS IP range. You'll need to expose your MongoDB Atlas Cluster to the internet or use a Pipedream VPC to assign a specific outbound IP address to your workflows. + +### Allowing any IP Address Range + +First, open the **Network Access** section of your MongoDB Atlas project. Then click **Add new IP address** in the top right-hand corner: + +![Add a new IP address to the allowed list of IP addresses within MongoDB Atlas](https://res.cloudinary.com/pipedreamin/image/upload/v1715026727/marketplace/apps/mongodb/CleanShot_2024-05-06_at_16.18.24_a3u6y0.png) + +Then enter `0.0.0.0/0` in the IP address field to allow connections from any IP address. In the description field, we recommend adding a note about this requirement for Pipedream. + +![Allowing any IP address to connect to your MongoDB Atlas Database Cluster. Necessary for services like Pipedream workflows that don’t have a static IP address unless you’re using Pipedream VPCs.](https://res.cloudinary.com/pipedreamin/image/upload/v1715026860/marketplace/apps/mongodb/CleanShot_2024-05-06_at_16.20.51_ixxmoa.png) + +Finally, click **Confirm** + +### Using a Pipedream VPC + +Pipedream VPCs allow you to define a single static IP address for your workflows and allow you to run these workflows in a separate network. + +[Follow this guide to create a VPC and assign the IP address to your workflow.](https://pipedream.com/docs/workflows/vpc) + +Then create a new allowed IP address in MongoDB by opening the **Network Access** section in Atlas: + +![Add a new IP address to the allowed list of IP addresses within MongoDB Atlas](https://res.cloudinary.com/pipedreamin/image/upload diff --git a/components/mongodb/actions/execute-aggregation/execute-aggregation.mjs b/components/mongodb/actions/execute-aggregation/execute-aggregation.mjs new file mode 100644 index 0000000000000..47c731cb8266b --- /dev/null +++ b/components/mongodb/actions/execute-aggregation/execute-aggregation.mjs @@ -0,0 +1,64 @@ +import app from "../../mongodb.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "mongodb-execute-aggregation", + name: "Execute Aggregation", + description: "Execute an aggregation pipeline on a MongoDB collection. [See the documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/aggregation/)", + version: "0.0.1", + type: "action", + props: { + app, + database: { + propDefinition: [ + app, + "database", + ], + }, + collectionName: { + propDefinition: [ + app, + "collection", + ({ database }) => ({ + database, + }), + ], + }, + pipeline: { + type: "string[]", + label: "Pipeline", + description: "The aggregation pipeline to execute where each row represents a stage as a JSON string. Eg. `[ { \"$match\": { \"categories\": \"Bakery\" } }, { \"$group\": { \"_id\": \"$stars\", \"count\": { \"$sum\": 1 } } } ]`", + }, + }, + methods: { + async excecuteAggregation({ + database, collectionName, pipeline, + } = {}) { + const { app } = this; + const client = await app.getClient(); + const collection = app.getCollection(client, database, collectionName); + const cursor = collection.aggregate(pipeline); + const result = await utils.iterate(cursor); + await client.close(); + return result; + }, + }, + async run({ $ }) { + const { + excecuteAggregation, + database, + collectionName, + pipeline, + } = this; + + const response = await excecuteAggregation({ + database, + collectionName, + pipeline: utils.parseArray(pipeline), + }); + + $.export("$summary", `Successfully executed aggregation pipeline on collection with \`${response.length}\` document(s).`); + + return response; + }, +}; diff --git a/components/mongodb/actions/find-document/find-document.mjs b/components/mongodb/actions/find-document/find-document.mjs new file mode 100644 index 0000000000000..3286b349f851b --- /dev/null +++ b/components/mongodb/actions/find-document/find-document.mjs @@ -0,0 +1,75 @@ +import app from "../../mongodb.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "mongodb-find-document", + name: "Find Document", + description: "Finds a document by a query filter. [See the documentation](https://docs.mongodb.com/manual/reference/method/db.collection.find/)", + version: "0.0.1", + type: "action", + props: { + app, + database: { + propDefinition: [ + app, + "database", + ], + }, + collectionName: { + propDefinition: [ + app, + "collection", + ({ database }) => ({ + database, + }), + ], + }, + filter: { + type: "string", + label: "Filter", + description: "JSON string to use as a filter. Eg. `{ \"name\": \"Twitter\" }`", + }, + options: { + type: "string", + label: "Options", + description: "JSON string to use as options. Eg. `{ \"projection\": { \"_id\": 0, \"title\": 1 } }`", + optional: true, + }, + }, + methods: { + async findDocument({ + database, collectionName, filter, options, + } = {}) { + const { app } = this; + const client = await app.getClient(); + const collection = app.getCollection(client, database, collectionName); + const result = await collection.findOne(filter, options); + await client.close(); + return result; + }, + }, + async run({ $ }) { + const { + findDocument, + database, + collectionName, + filter, + options, + } = this; + + const response = await findDocument({ + database, + collectionName, + filter: utils.valueToObject(filter), + options: utils.valueToObject(options), + }); + + if (!response) { + $.export("$summary", "Document not found in the collection."); + return; + } + + $.export("$summary", "Successfully found a document in the collection."); + return response; + }, +}; diff --git a/components/mongodb/actions/update-documents/update-documents.mjs b/components/mongodb/actions/update-documents/update-documents.mjs new file mode 100644 index 0000000000000..5ad3bec2acaf6 --- /dev/null +++ b/components/mongodb/actions/update-documents/update-documents.mjs @@ -0,0 +1,69 @@ +import app from "../../mongodb.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "mongodb-update-documents", + name: "Update Documents", + description: "Updates many documents by query filter. [See the documentation](https://www.mongodb.com/docs/drivers/node/current/usage-examples/updateMany/)", + version: "0.0.1", + type: "action", + props: { + app, + database: { + propDefinition: [ + app, + "database", + ], + }, + collectionName: { + propDefinition: [ + app, + "collection", + ({ database }) => ({ + database, + }), + ], + }, + filter: { + type: "string", + label: "Filter", + description: "JSON string to use as a filter. Eg. `{ \"rated\": \"G\" }`", + }, + data: { + label: "Data To Update", + type: "string", + description: "JSON data to use as the update. Eg. `{ \"$set\": { \"rating\": \"Everyone\" } }`", + }, + }, + methods: { + async updateDocuments({ + database, collectionName, filter, data, + } = {}) { + const { app } = this; + const client = await app.getClient(); + const collection = app.getCollection(client, database, collectionName); + const result = await collection.updateMany(filter, data); + await client.close(); + return result; + }, + }, + async run({ $ }) { + const { + updateDocuments, + database, + collectionName, + filter, + data, + } = this; + + const response = await updateDocuments({ + database, + collectionName, + filter: utils.valueToObject(filter), + data: utils.valueToObject(data), + }); + + $.export("$summary", `Successfully updated \`${response.modifiedCount}\` document(s) in the collection.`); + return response; + }, +}; diff --git a/components/mongodb/common/utils.mjs b/components/mongodb/common/utils.mjs new file mode 100644 index 0000000000000..16af2678bc5b2 --- /dev/null +++ b/components/mongodb/common/utils.mjs @@ -0,0 +1,63 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function isJson(value) { + if (typeof(value) !== "string") { + return false; + } + + try { + JSON.parse(value); + } catch (e) { + return false; + } + + return true; +} + +function valueToObject(value) { + if (value === undefined || typeof(value) === "object") { + return value; + } + + if (!isJson(value)) { + throw new ConfigurationError(`Make sure the value contains a valid JSON string: ${value}`); + } + return JSON.parse(value); +} + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the value contains a valid JSON array"); + } +} + +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +export default { + valueToObject, + parseArray: (value) => parseArray(value).map(valueToObject), + iterate, +}; diff --git a/components/mongodb/package.json b/components/mongodb/package.json index 7152f670adbcf..a2de271d2c20c 100644 --- a/components/mongodb/package.json +++ b/components/mongodb/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/mongodb", - "version": "0.0.8", + "version": "0.1.1", "description": "Pipedream MongoDB Components", "main": "mongodb.app.mjs", "keywords": [ @@ -14,7 +14,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.2.0", + "@pipedream/platform": "^3.0.1", "mongodb": "^4.6.0" } } diff --git a/components/mongodb/sources/new-document/new-document.mjs b/components/mongodb/sources/new-document/new-document.mjs index bccb7a5dfe973..c81ce914ce97c 100644 --- a/components/mongodb/sources/new-document/new-document.mjs +++ b/components/mongodb/sources/new-document/new-document.mjs @@ -1,11 +1,12 @@ import common from "../common/base.mjs"; +import { Timestamp } from "mongodb"; export default { ...common, key: "mongodb-new-document", name: "New Document", description: "Emit new an event when a new document is added to a collection", - version: "0.0.8", + version: "0.0.9", type: "source", dedupe: "unique", props: { @@ -53,6 +54,15 @@ export default { return doc[this.timestampField]; } }, + convertToTimestamp(timestampStr) { + const bigIntValue = BigInt(timestampStr); + const seconds = Number(bigIntValue >> 32n); + const increment = Number(bigIntValue & 0xFFFFFFFFn); + return new Timestamp({ + t: seconds, + i: increment, + }); + }, async processEvent(client, eventTs, max) { const lastTs = this._getLastTs() || 0; let maxTs = lastTs; @@ -61,7 +71,12 @@ export default { const sort = { [this.timestampField]: -1, }; - const documents = await collection.find().sort(sort) + const query = { + [this.timestampField]: { + $gt: this.convertToTimestamp(lastTs), + }, + }; + const documents = await collection.find(query).sort(sort) .toArray(); const docs = []; for (const doc of documents) { diff --git a/components/monkeylearn/README.md b/components/monkeylearn/README.md index 2fb6fe591d874..56d6f17ed4b59 100644 --- a/components/monkeylearn/README.md +++ b/components/monkeylearn/README.md @@ -1,12 +1,11 @@ # Overview -With the MonkeyLearn API, you can build a number of different applications that -can analyze text data. Some examples include: +MonkeyLearn is a text analysis platform that employs machine learning to extract and process data from chunks of text. By leveraging the MonkeyLearn API on Pipedream, you can automate the categorization of text, extract specific data, analyze sentiment, and more, all in real-time. This enables the development of powerful custom workflows that can analyze customer feedback, automate email processing, or provide insightful analytics on textual data from various sources. -- Sentiment analysis -- Keyword extraction -- Language detection -- Text classification +# Example Use Cases -For more information on the MonkeyLearn API, please visit their website at -[monkeylearn.com](https://monkeylearn.com) +- **Customer Feedback Analysis**: Automatically pipe customer feedback from a support platform like Zendesk into MonkeyLearn to determine sentiment and categorize the feedback into topics. This can be especially useful for prioritizing support tickets and understanding common customer issues. + +- **Email Processing Automation**: Use the MonkeyLearn API to scan incoming emails for specific keywords or phrases, categorize them, and then triage to the appropriate department or individual within your organization. This could be coupled with Gmail or another email service integrated through Pipedream. + +- **Social Media Monitoring**: Create a real-time social media monitoring tool by connecting MonkeyLearn to Twitter's API via Pipedream. Analyze tweets mentioning your brand to gauge public sentiment and identify key influencers or detractors, providing valuable marketing insights. diff --git a/components/moonclerk/README.md b/components/moonclerk/README.md index 9240b0ed4c85d..10b7ffd6d8707 100644 --- a/components/moonclerk/README.md +++ b/components/moonclerk/README.md @@ -1,6 +1,11 @@ # Overview -You can use the MoonClerk API to create and manage online payments for your -business. With MoonClerk, you can accept payments from customers via credit -card, bank account, or PayPal. You can also use MoonClerk to set up recurring -payments, so customers can make automatic payments on a schedule you specify. +The MoonClerk API offers a streamlined way to manage and automate payments and billing processes directly within your applications. With Pipedream, you can harness this API to create custom workflows that trigger actions based on payment events, update customer subscriptions, sync with accounting software, and more. It's a powerful tool for businesses looking to automate their financial operations and integrate their payment system with other digital tools. + +# Example Use Cases + +- **Automated Receipt Delivery**: After a payment is processed through MoonClerk, trigger a workflow in Pipedream that sends a customized receipt to the customer via email. Use the SendGrid app to format and dispatch the email, ensuring your customers get timely, branded communications about their transactions. + +- **Subscription Status Sync**: Whenever a subscription is updated in MoonClerk, use a Pipedream workflow to synchronize this information with a CRM like HubSpot. This keeps your sales and support teams informed about customer subscription statuses, enhancing customer service and enabling targeted marketing campaigns. + +- **Financial Reporting Integration**: Connect MoonClerk to an accounting platform such as QuickBooks. Set up a Pipedream workflow to export new transactions from MoonClerk to QuickBooks automatically. This ensures that your financial records are accurate and up-to-date, streamlining your accounting processes and saving valuable time. diff --git a/components/moonclerk/package.json b/components/moonclerk/package.json new file mode 100644 index 0000000000000..93b4e85e3c7dc --- /dev/null +++ b/components/moonclerk/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/moonclerk", + "version": "0.6.0", + "description": "Pipedream moonclerk Components", + "main": "moonclerk.app.mjs", + "keywords": [ + "pipedream", + "moonclerk" + ], + "homepage": "https://pipedream.com/apps/moonclerk", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/moonmail/README.md b/components/moonmail/README.md new file mode 100644 index 0000000000000..8ce446642adc9 --- /dev/null +++ b/components/moonmail/README.md @@ -0,0 +1,11 @@ +# Overview + +MoonMail is a powerful email marketing platform that allows you to create, send, and track email campaigns. When integrated with Pipedream, you can automate workflows for managing subscriber lists, triggering email campaigns based on specific events, and analyzing campaign performance. Pipedream's serverless platform facilitates the creation of complex workflows without the need to manage infrastructure, making it simple to connect MoonMail with other services to enhance your email marketing automation. + +# Example Use Cases + +- **Automated Subscriber Segmentation**: Add new subscribers to specific lists in MoonMail based on their actions in your application. For instance, when a user signs up for a premium account on your platform, a Pipedream workflow can automatically add them to a "Premium Users" list in MoonMail for targeted campaigns. + +- **E-commerce Cart Abandonment Emails**: Integrate MoonMail with an e-commerce platform like Shopify. Monitor for abandoned carts and use Pipedream to trigger a follow-up email campaign in MoonMail, reminding customers to complete their purchases. + +- **Event-Driven Campaign Trigger**: Combine MoonMail with a CRM like HubSpot. When a new deal reaches a certain stage in your sales pipeline, a Pipedream workflow can trigger a MoonMail campaign tailored to nudge the deal toward closing, ensuring timely and relevant communication. diff --git a/components/moosend/README.md b/components/moosend/README.md index 540ff75bffb86..62844dd74d0c4 100644 --- a/components/moosend/README.md +++ b/components/moosend/README.md @@ -1,12 +1,11 @@ # Overview -With the Moosend API, you can build a variety of applications and integrations -to help you manage your email marketing campaigns. Here are some examples of -what you can build: - -- A system to automatically send emails based on certain criteria (e.g. when a - user subscribes to a service, or when a product is back in stock) -- A tool to import or export data from your email marketing campaigns -- An integration with your CRM system to keep track of customer interactions -- A system to automatically generate reports on your email marketing campaigns -- A system to track who opens your emails and clicks on links inside them +The Moosend API offers a robust platform for email marketing automation, enabling you to manage subscribers, email campaigns, and mailing lists. With its integration on Pipedream, you can automate workflows that respond to events in real-time, like new subscriber additions or campaign status updates. This seamless connection allows for the automation of tasks such as syncing subscriber data across platforms, triggering personalized email sequences, or generating detailed analytics reports by combining data from multiple sources. + +# Example Use Cases + +- **Automated Subscriber Syncing Between Moosend and CRM**: When a new subscriber is added in Moosend, automatically add that subscriber to a specific CRM platform such as Salesforce or HubSpot. This ensures your subscriber lists are consistent and up-to-date across your marketing and sales platforms. + +- **Dynamic Email Campaigns Triggered by E-Commerce Activity**: Trigger email campaigns in Moosend based on customer behavior on your e-commerce platform, like Shopify. For instance, set up a workflow that sends a personalized email sequence when a customer abandons their shopping cart or completes a purchase. + +- **Real-time Analytics Reporting**: Generate real-time analytics reports by combining data from Moosend with other analytics tools like Google Sheets or Tableau. Whenever a campaign is sent, track its performance and populate a report with open rates, click-through rates, and other relevant metrics to monitor the effectiveness of your email marketing efforts. diff --git a/components/more_trees_/README.md b/components/more_trees_/README.md new file mode 100644 index 0000000000000..9aee4fed52165 --- /dev/null +++ b/components/more_trees_/README.md @@ -0,0 +1,11 @@ +# Overview + +The More Trees API enables users to integrate tree planting into their apps and workflows. It offers a way to automate the process of planting trees, allowing users to contribute to reforestation efforts globally through their business practices or personal projects. With this API on Pipedream, you can create serverless workflows that respond to various triggers (like eCommerce transactions or milestones) and automatically initiate tree planting tasks. This not only greenifies your operations but also provides a tangible way to showcase corporate social responsibility. + +# Example Use Cases + +- **E-Commerce Green Initiative**: Automatically plant a tree for every product sold on your eCommerce platform. When a new order is placed, trigger a Pipedream workflow that sends a request to More Trees API to plant a tree, then notify the customer of this contribution via email. + +- **Sustainable User Onboarding**: For each new user signup on your app, you could use Pipedream to call the More Trees API to plant a tree in their honor. This workflow can be further enhanced by integrating with a messaging app like Slack or Microsoft Teams to announce each new signup and the corresponding eco-friendly action. + +- **Social Media Milestone Celebration**: Create a workflow that monitors your company's social media for specific milestone achievements (like reaching a certain number of followers). Use Pipedream to trigger the More Trees API to plant a set number of trees for each milestone, and then auto-generate a social media post to share the good news. diff --git a/components/morgen/morgen.app.mjs b/components/morgen/morgen.app.mjs new file mode 100644 index 0000000000000..f01fa0f4a62c0 --- /dev/null +++ b/components/morgen/morgen.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "morgen", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/morgen/package.json b/components/morgen/package.json new file mode 100644 index 0000000000000..a4ead804f3f26 --- /dev/null +++ b/components/morgen/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/morgen", + "version": "0.0.1", + "description": "Pipedream Morgen Components", + "main": "morgen.app.mjs", + "keywords": [ + "pipedream", + "morgen" + ], + "homepage": "https://pipedream.com/apps/morgen", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/morningmate/README.md b/components/morningmate/README.md new file mode 100644 index 0000000000000..3b2172cf3bbee --- /dev/null +++ b/components/morningmate/README.md @@ -0,0 +1,11 @@ +# Overview + +The MorningMate API enables users to automate morning routines and information retrieval, providing a personalized start to the day. It can fetch weather forecasts, news updates, calendar events, and more. With Pipedream, you can harness this data to create custom workflows that trigger on schedules or events, integrate with other services, and streamline morning updates into a single, automated process. + +# Example Use Cases + +- **Weather Alert to Slack**: Set up a Pipedream workflow to fetch the daily weather forecast from MorningMate and post it to a Slack channel every morning. This helps teams plan their day, especially for those with outdoor work or commute considerations. + +- **Morning Digest Email**: Combine MorningMate's news and calendar events to create a personalized morning digest. Using Pipedream, you can format this information and send it as a daily email, ensuring you never miss out on important updates or meetings. + +- **Smart Home Integration**: Use MorningMate API to trigger smart home actions through Pipedream. For example, if the temperature drops below a certain point, a signal could be sent to your smart thermostat to adjust the heating, ensuring a warm and cozy environment when you wake up. diff --git a/components/moskit/README.md b/components/moskit/README.md index 887cb698be84f..3222c523a807d 100644 --- a/components/moskit/README.md +++ b/components/moskit/README.md @@ -1,11 +1,11 @@ # Overview -Looking for an API to help you build a CRM? Look no further than the Moskit -API. With this API, you can easily create, manage, and track customer -relationships. Plus, you can use the Moskit API to automate your workflow, -making your life easier. Here are some examples of what you can build using the -Moskit API: - -- A CRM system for keeping track of your customers and sales -- A system for tracking your inventory and customers -- A Customer Relationship Management (CRM) system +The Moskit API, integrated with Pipedream, offers a robust platform for automating CRM-related activities. With this API, you can orchestrate data synchronization, streamline customer engagement processes, and enhance sales workflows. By leveraging the Moskit API through Pipedream, you can create custom serverless workflows that react to various CRM events, manipulate data, and connect with countless other services for a seamless automation experience. + +# Example Use Cases + +- **Lead Qualification Automation**: Automatically qualify leads in Moskit by setting up a workflow on Pipedream that triggers when a new lead is added. The workflow could enrich lead data using Clearbit, score the lead based on predefined criteria, and then update the lead's status in Moskit accordingly. This process ensures that sales teams focus on high-potential prospects, improving conversion rates. + +- **Customer Onboarding Sequence**: Create a customer onboarding sequence by triggering a Pipedream workflow when a deal is marked as won in Moskit. The workflow could send a welcome email via SendGrid, create a new customer record in QuickBooks for invoicing, and set up a series of tasks in Trello for onboarding the new customer. This automation provides a consistent onboarding experience and reduces manual tasks for sales and operations teams. + +- **Real-time Sales Notifications**: Set up a Pipedream workflow to send real-time notifications to a Slack channel whenever a deal approaches its closing date or moves to a new stage in Moskit. This keeps the sales team informed and ensures timely follow-up with prospects, potentially increasing the chances of closing deals. diff --git a/components/moskit/package.json b/components/moskit/package.json new file mode 100644 index 0000000000000..4c4624827c12a --- /dev/null +++ b/components/moskit/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/moskit", + "version": "0.6.0", + "description": "Pipedream moskit Components", + "main": "moskit.app.mjs", + "keywords": [ + "pipedream", + "moskit" + ], + "homepage": "https://pipedream.com/apps/moskit", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/motion/README.md b/components/motion/README.md new file mode 100644 index 0000000000000..5bd26d5cc546f --- /dev/null +++ b/components/motion/README.md @@ -0,0 +1,11 @@ +# Overview + +The Motion API empowers users to streamline project management and productivity tasks. Within Pipedream's environment, you can leverage this API to automate actions based on project updates, task completions, and team collaborations. It's a toolset that sails smoothly with Pipedream's knack for creating swift integrations and workflows, making it possible to connect Motion with other apps to optimize project tracking, notifications, and data synchronization. + +# Example Use Cases + +- **Task Completion Notifications**: Send real-time alerts via Slack or email when tasks in Motion are marked as complete. This ensures that team members stay informed about project progress without constantly checking the project management tool. + +- **Sync Tasks with Google Calendar**: Automatically add Motion tasks with due dates as events in Google Calendar. This integration can help you visualize your workload and deadlines in a calendar format, making time management more intuitive. + +- **Aggregate Project Updates**: Compile updates from Motion projects into a weekly digest, which can then be sent to team members or stakeholders. This workflow can be set to pull the latest project changes and format them into a neat report, thus keeping everyone in the loop with minimal manual effort. diff --git a/components/motion/actions/get-schedules/get-schedules.mjs b/components/motion/actions/get-schedules/get-schedules.mjs new file mode 100644 index 0000000000000..acb237a8b737d --- /dev/null +++ b/components/motion/actions/get-schedules/get-schedules.mjs @@ -0,0 +1,25 @@ +import motion from "../../motion.app.mjs"; + +export default { + key: "motion-get-schedules", + name: "Retrieve Schedules", + version: "0.0.1", + description: "Get a list of schedules for your user. [See the documentation](https://docs.usemotion.com/docs/motion-rest-api/f9fec7bb61c6f-get-schedules)", + type: "action", + props: { + motion, + }, + methods: { + getSchedules($) { + return this.motion._makeRequest({ + $, + path: "schedules", + }); + }, + }, + async run({ $ }) { + const response = await this.getSchedules($); + $.export("$summary", `Successfully fetched ${response?.length} schedules`); + return response; + }, +}; diff --git a/components/motion/package.json b/components/motion/package.json index 7eaedfc5ddd1d..e0b19c52c9866 100644 --- a/components/motion/package.json +++ b/components/motion/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/motion", - "version": "0.1.0", + "version": "0.2.0", "description": "Pipedream Motion Components", "main": "motion.app.mjs", "keywords": [ @@ -13,6 +13,6 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.5.1" + "@pipedream/platform": "^3.0.3" } } diff --git a/components/motive/actions/find-user-details/find-user-details.mjs b/components/motive/actions/find-user-details/find-user-details.mjs new file mode 100644 index 0000000000000..5df74bf9319df --- /dev/null +++ b/components/motive/actions/find-user-details/find-user-details.mjs @@ -0,0 +1,75 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { clearObj } from "../../common/util.mjs"; +import motive from "../../motive.app.mjs"; + +export default { + key: "motive-find-user-details", + name: "Find User Details", + description: "Retrieve user details based on specific criteria. [See the documentation](https://developer.gomotive.com/reference/users-lookup)", + version: "0.0.1", + type: "action", + props: { + motive, + alert: { + type: "alert", + alertType: "info", + content: "If you provide more than one prop, only one will be considered, the others will be ignored.", + }, + username: { + type: "string", + label: "Username", + description: "Username to retrieve user details", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "Email to retrieve user details", + optional: true, + }, + driverCompanyId: { + propDefinition: [ + motive, + "driverCompanyId", + ], + optional: true, + }, + }, + async run({ $ }) { + let response; + const criteria = []; + + const { + username, email, driverCompanyId, + } = this; + if (!username && !email && !driverCompanyId) { + throw new Error("At least one of 'Username', 'Email', 'Driver Company Id' must be provided."); + } + + if (username) criteria.push(`username: ${username}`); + if (email) criteria.push(`email: ${email}`); + if (driverCompanyId) criteria.push(`driverCompanyId: ${driverCompanyId}`); + + try { + response = await this.motive.retrieveUserDetails({ + $, + params: clearObj({ + username, + email, + driver_company_id: driverCompanyId, + }), + }); + + } catch ({ response: { data } }) { + if (data.error_message === "user not found") { + response = { + user: null, + }; + } else { + throw new ConfigurationError(data.error_message); + } + } + $.export("$summary", `Successfully retrieved user details for ${criteria.join(", ")}`); + return response; + }, +}; diff --git a/components/motive/common/util.mjs b/components/motive/common/util.mjs new file mode 100644 index 0000000000000..090fad73a48d6 --- /dev/null +++ b/components/motive/common/util.mjs @@ -0,0 +1,16 @@ +export const clearObj = (obj) => { + return Object.entries(obj) + .filter(([ + _, + v, + ]) => (v != null && v != "" && _ != "$emit")) + .reduce((acc, [ + k, + v, + ]) => ({ + ...acc, + [k]: (!Array.isArray(v) && v === Object(v)) + ? clearObj(v) + : v, + }), {}); +}; diff --git a/components/motive/motive.app.mjs b/components/motive/motive.app.mjs new file mode 100644 index 0000000000000..2bf660dbee1d5 --- /dev/null +++ b/components/motive/motive.app.mjs @@ -0,0 +1,124 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "motive", + propDefinitions: { + driverId: { + type: "string", + label: "Driver ID", + description: "Filter to a specific driver", + }, + safetyCategory: { + type: "string", + label: "Safety Category", + description: "Filter to a specific safety category", + }, + driverCompanyId: { + type: "string", + label: "Driver Company ID", + description: "Driver company ID to retrieve user details", + async options({ page }) { + const { users } = await this.listUsers({ + params: { + page_no: page + 1, + role: "driver", + }, + }); + + return users.map(({ user }) => ({ + label: user.email || `${user.first_name} ${user.last_name}`, + value: user.driver_company_id, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.gomotive.com/v1"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listHosViolations(opts = {}) { + return this._makeRequest({ + method: "GET", + path: "/hos_violations", + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users", + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/company_webhooks", + ...opts, + }); + }, + updateWebhook({ + webhookId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/company_webhooks/${webhookId}`, + ...opts, + }); + }, + listDriverPerformanceEvents(opts = {}) { + return this._makeRequest({ + method: "GET", + path: "/driver_performance_events", + ...opts, + }); + }, + retrieveUserDetails(opts = {}) { + return this._makeRequest({ + method: "GET", + path: "/users/lookup", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, fieldName, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const data = await fn({ + params, + ...opts, + }); + const items = data[fieldName]; + for (const d of items) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = items.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/motive/package.json b/components/motive/package.json new file mode 100644 index 0000000000000..7b17921c1c4a0 --- /dev/null +++ b/components/motive/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/motive", + "version": "0.1.0", + "description": "Pipedream Motive Components", + "main": "motive.app.mjs", + "keywords": [ + "pipedream", + "motive" + ], + "homepage": "https://pipedream.com/apps/motive", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "uuid": "^11.0.4" + } +} diff --git a/components/motive/sources/common/base.mjs b/components/motive/sources/common/base.mjs new file mode 100644 index 0000000000000..b82a39f4587f0 --- /dev/null +++ b/components/motive/sources/common/base.mjs @@ -0,0 +1,63 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import motive from "../../motive.app.mjs"; + +export default { + props: { + motive, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + async emitEvent(maxResults = false) { + const lastId = this._getLastId(); + const fieldName = this.getFieldName(); + + const response = this.motive.paginate({ + fn: this.getFunction(), + fieldName: `${fieldName}s`, + }); + + let responseArray = []; + for await (const item of response) { + const data = item[fieldName]; + if (data.id <= lastId) break; + responseArray.push(data); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastId(responseArray[0].id); + } + + for (const item of responseArray.reverse()) { + const data = item[fieldName]; + this.$emit(data, { + id: data.id, + summary: this.getSummary(data), + ts: Date.parse(data.start_time || new Date()), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/motive/sources/new-hours-of-service-violation/new-hours-of-service-violation.mjs b/components/motive/sources/new-hours-of-service-violation/new-hours-of-service-violation.mjs new file mode 100644 index 0000000000000..7ab61c8b44436 --- /dev/null +++ b/components/motive/sources/new-hours-of-service-violation/new-hours-of-service-violation.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "motive-new-hours-of-service-violation", + name: "New Hours of Service Violation", + description: "Emit new event when a hos is emited.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.motive.listHosViolations; + }, + getFieldName() { + return "hos_violation"; + }, + getSummary(item) { + return `New HOS Violation: ${item.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/motive/sources/new-hours-of-service-violation/test-event.mjs b/components/motive/sources/new-hours-of-service-violation/test-event.mjs new file mode 100644 index 0000000000000..274d17697702a --- /dev/null +++ b/components/motive/sources/new-hours-of-service-violation/test-event.mjs @@ -0,0 +1,17 @@ +export default { + "id": 97, + "type": "ca_duty_15", + "start_time": "2016-03-12T19:00:00Z", + "end_time": null, + "name": "15 Hour On Duty Limit", + "user": { + "id": 156, + "first_name": "Harold", + "last_name": "Hoeger", + "username": "hobart", + "email": null, + "driver_company_id": null, + "status": "deactivated", + "role": "driver" + } +} \ No newline at end of file diff --git a/components/motive/sources/new-safety-event/new-safety-event.mjs b/components/motive/sources/new-safety-event/new-safety-event.mjs new file mode 100644 index 0000000000000..ecdad4a7b42fd --- /dev/null +++ b/components/motive/sources/new-safety-event/new-safety-event.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "motive-new-safety-event", + name: "New Safety Event", + description: "Emit new safety-related events like harsh braking or acceleration.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.motive.listDriverPerformanceEvents; + }, + getFieldName() { + return "driver_performance_event"; + }, + getSummary(event) { + return `New Safety Event: ${event.type}`; + }, + }, + sampleEmit, +}; diff --git a/components/motive/sources/new-safety-event/test-event.mjs b/components/motive/sources/new-safety-event/test-event.mjs new file mode 100644 index 0000000000000..9ab6ca3096d39 --- /dev/null +++ b/components/motive/sources/new-safety-event/test-event.mjs @@ -0,0 +1,123 @@ +export default { + "id": 828645972, + "acceleration": 0.292558613659531, + "duration": 2, + "end_bearing": 270.36, + "end_speed": 30.924, + "end_time": "2023-03-16T00:22:53Z", + "lat": 43.0361387, + "lon": -78.8207746, + "m_gps_heading": [ + 269.82, + 269.3, + 269.06, + 268.91, + 268.93, + 269.42, + 269.69, + 269.23, + 270.36, + 278.3, + 323.29, + 342.05, + 344.37, + 344.19, + 343.28, + 341.11 + ], + "m_gps_lat": [ + 43.0361491, + 43.0361477, + 43.0361451, + 43.0361428, + 43.0361407, + 43.0361387, + 43.0361387, + 43.0361381, + 43.0361383, + 43.0361438, + 43.0362017, + 43.0362433, + 43.0362853, + 43.0363294, + 43.0363748, + 43.0364192 + ], + "m_gps_lon": [ + -78.8195014, + -78.8197336, + -78.8199641, + -78.8201898, + -78.8204033, + -78.8206003, + -78.8207746, + -78.8209258, + -78.8210443, + -78.8211361, + -78.8212513, + -78.8212781, + -78.8212971, + -78.8213152, + -78.8213349, + -78.8213535 + ], + "m_veh_odo": null, + "m_veh_spd": [ + 68, + 68, + 66, + 64, + 60, + 54, + 48, + 40, + 22, + 20.5, + 19, + 18, + 17, + 18, + 19, + 19 + ], + "start_bearing": 269.69, + "start_speed": 48.816, + "start_time": "2023-03-16T00:22:51Z", + "type": "hard_brake", + "driver": null, + "vehicle": { + "id": 421006, + "number": "Camann_Pilot2", + "year": "2012", + "make": "Honda", + "model": "Pilot", + "vin": "", + "metric_units": false + }, + "eld_device": { + "id": 698247, + "identifier": "AABL36GB270042", + "model": "lbb-3.6ca" + }, + "m_gps_spd": [ + 68.148, + 68.112, + 66.852, + 64.764, + 61.128, + 55.872, + 48.816, + 41.256, + 30.924, + 23.292, + 19.656, + 17.928, + 18.252, + 18.072, + 19.548, + 18.108 + ], + "location": "3.5 km E of North Tonawanda, NY", + "intensity": "-10.3 kph/s", + "coaching_status": "pending_review" +} \ No newline at end of file diff --git a/components/moxie/README.md b/components/moxie/README.md new file mode 100644 index 0000000000000..5ca9039b4dc14 --- /dev/null +++ b/components/moxie/README.md @@ -0,0 +1,11 @@ +# Overview + +The Moxie API provides tools to facilitate customer engagement through chat and messaging services. With this API, you can integrate real-time chat capabilities, manage conversations, and leverage customer data to provide personalized support. On Pipedream, you can use the Moxie API to create automated workflows that respond to events in Moxie, such as new messages or chat engagements, and connect them with other services to enhance customer support, automate responses, and streamline communication processes. + +# Example Use Cases + +- **Automated Customer Support Ticket Creation**: When a new chat starts on Moxie, trigger a Pipedream workflow to create a support ticket in a tool like Zendesk or Help Scout. This allows customer support teams to track and follow up on issues efficiently, ensuring that no customer query goes unanswered. + +- **Real-time Chat Analysis and Reporting**: Analyze the content of chats in real-time by connecting Moxie to a sentiment analysis API like ParallelDots. Each time a chat concludes, the workflow can send the transcript to the sentiment analysis service and log the results in a Google Sheet or Data Store in Pipedream. This can help gauge customer satisfaction and improve service quality. + +- **Customer CRM Integration**: Sync chat interactions to a CRM like Salesforce or HubSpot. Each time a chat session ends, the workflow can extract relevant customer information and update their CRM record. This ensures that sales and support teams have the latest information on customer preferences and history. diff --git a/components/mozilla_observatory/README.md b/components/mozilla_observatory/README.md new file mode 100644 index 0000000000000..b8966d30a69ad --- /dev/null +++ b/components/mozilla_observatory/README.md @@ -0,0 +1,11 @@ +# Overview + +The Mozilla Observatory API empowers developers to assess and improve the security of their websites. It offers a suite of analysis tools that evaluate website security headers, SSL configurations, and other security-related features. By integrating this API into Pipedream, you can automate security checks, receive real-time alerts, and combine data from your site audits with other services for enhanced monitoring and reporting. + +# Example Use Cases + +- **Automated Security Audit Workflow**: Set up a scheduled workflow in Pipedream that triggers weekly security scans of your website using the Mozilla Observatory API. On receiving the results, format them and store them in a Google Sheet for trend analysis and historical reference. + +- **Real-time Slack Alerts**: Create a workflow that listens for Pipedream HTTP webhook events whenever you push updates to your website. Use the Mozilla Observatory API to perform a quick scan and, if any security issues are detected, send a notification with details to a designated Slack channel. + +- **Incident Response with PagerDuty**: Develop a security incident response workflow that, upon detecting critical security flaws via the Mozilla Observatory API, automatically creates an incident in PagerDuty to ensure immediate attention from your operations team. diff --git a/components/msg91/README.md b/components/msg91/README.md index 9496511ec7eec..84ba7d9ee5159 100644 --- a/components/msg91/README.md +++ b/components/msg91/README.md @@ -1,12 +1,11 @@ # Overview -With MSG91, you can build a variety of features into your app or website, -including: - -- Sending and receiving SMS messages -- Setting up phone number verification -- Creating and managing groups of contacts -- Sending bulk SMS messages -- Tracking the delivery status of messages -- Scheduling messages to be sent at a later time -- And more! +MSG91 API offers a powerful platform for integrating SMS solutions into a variety of applications, enabling automated and personalized communication. With Pipedream, MSG91's API can be woven into workflows to send notifications, verify users with OTPs, and engage customers with marketing campaigns. Pipedream's no-code connectors and serverless execution model simplify the process of setting up MSG91-powered automations that can interact with countless other services. + +# Example Use Cases + +- **User Verification Automation**: Trigger a workflow on Pipedream when a new user signs up on your platform. Use MSG91 to send a one-time password (OTP) for user verification. Connect to a database like MySQL to store the verification status. + +- **E-commerce Order Updates**: Set up a workflow that responds to order status changes from an e-commerce platform like Shopify. Use the MSG91 API to send personalized SMS updates to customers about their order's shipping status or delivery confirmation. + +- **Scheduled Marketing Campaigns**: Integrate a CRM platform like HubSpot as a trigger for Pipedream workflows. Schedule and send bulk SMS messages through MSG91 for promotions, product launches, or event announcements to a filtered list of contacts based on their interaction history or lead status. diff --git a/components/msg91/package.json b/components/msg91/package.json new file mode 100644 index 0000000000000..c126b46932001 --- /dev/null +++ b/components/msg91/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/msg91", + "version": "0.6.0", + "description": "Pipedream msg91 Components", + "main": "msg91.app.mjs", + "keywords": [ + "pipedream", + "msg91" + ], + "homepage": "https://pipedream.com/apps/msg91", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/mslm_cloud/mslm_cloud.app.mjs b/components/mslm_cloud/mslm_cloud.app.mjs new file mode 100644 index 0000000000000..8704385dfcbc6 --- /dev/null +++ b/components/mslm_cloud/mslm_cloud.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "mslm_cloud", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/mslm_cloud/package.json b/components/mslm_cloud/package.json new file mode 100644 index 0000000000000..68dfd7ee440b0 --- /dev/null +++ b/components/mslm_cloud/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/mslm_cloud", + "version": "0.0.1", + "description": "Pipedream Mslm Cloud Components", + "main": "mslm_cloud.app.mjs", + "keywords": [ + "pipedream", + "mslm_cloud" + ], + "homepage": "https://pipedream.com/apps/mslm_cloud", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/mumara/README.md b/components/mumara/README.md new file mode 100644 index 0000000000000..75a38a4a35f14 --- /dev/null +++ b/components/mumara/README.md @@ -0,0 +1,11 @@ +# Overview + +The Mumara API provides programmatic access to powerful email marketing and SMS campaign tools, allowing for the automation of contact list management, campaign creation, and performance tracking. In Pipedream, you can leverage these capabilities to create custom workflows that integrate Mumara with other services, automate repetitive tasks, and react to events in real-time. By connecting Mumara's API with Pipedream's serverless execution platform, you can create data-driven workflows that streamline your marketing efforts. + +# Example Use Cases + +- **Sync New Shopify Customers to Mumara Contacts**: Automatically add new customers from your Shopify store to a Mumara contact list. Whenever a new customer is created in Shopify, a Pipedream workflow triggers, adding the customer's details to your specified Mumara list, keeping your marketing campaigns up-to-date with minimal effort. + +- **Send Campaign Results to Slack**: Keep your team informed by sending Mumara campaign performance reports directly to a Slack channel. After a campaign's completion, a Pipedream workflow activates, fetching the metrics from Mumara and formatting a message with key results, which is then posted to your team's Slack channel, providing quick insights for decision making. + +- **Create Mumara Campaigns from Google Sheets**: Manage your email campaigns directly from a Google Sheet. Set up a Pipedream workflow that monitors changes in a designated Google Sheets document. When campaign details like subject, content, and recipient list are updated, the workflow creates a new email campaign in Mumara with the provided details, turning your Google Sheet into a campaign management tool. diff --git a/components/mumara/package.json b/components/mumara/package.json index bdb856b2a32bc..fb021f8b4d5ea 100644 --- a/components/mumara/package.json +++ b/components/mumara/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/mural/actions/create-mural/create-mural.mjs b/components/mural/actions/create-mural/create-mural.mjs new file mode 100644 index 0000000000000..f61afb50a29b2 --- /dev/null +++ b/components/mural/actions/create-mural/create-mural.mjs @@ -0,0 +1,95 @@ +import mural from "../../mural.app.mjs"; + +export default { + key: "mural-create-mural", + name: "Create Mural", + description: "Create a new mural within a specified workspace. [See the documentation](https://developers.mural.co/public/reference/createmural)", + version: "0.0.1", + type: "action", + props: { + mural, + workspaceId: { + propDefinition: [ + mural, + "workspaceId", + ], + }, + roomId: { + propDefinition: [ + mural, + "roomId", + (c) => ({ + workspaceId: c.workspaceId, + }), + ], + }, + title: { + type: "string", + label: "Title", + description: "The title of the Mural.", + }, + backgroundColor: { + type: "string", + label: "Background Color", + description: "The background color of the mural. Example: `#FAFAFAFF`", + optional: true, + }, + height: { + type: "integer", + label: "Height", + description: "The height of the mural in px", + optional: true, + }, + width: { + type: "integer", + label: "Width", + description: "The width of the mural in px", + optional: true, + }, + infinite: { + type: "boolean", + label: "Infinite", + description: "When `true`, this indicates that the mural canvas is borderless and grows as you add widgets to it.", + optional: true, + }, + timerSoundTheme: { + type: "string", + label: "Timer Sound Theme", + description: "The timer sound theme for the mural", + options: [ + "airplane", + "cello", + "cuckoo", + ], + optional: true, + }, + visitorAvatarTheme: { + type: "string", + label: "Visitor Avatar Theme", + description: "The visitor avatar theme for the mural", + options: [ + "animals", + "music", + "travel", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.mural.createMural({ + $, + data: { + roomId: this.roomId, + title: this.title, + backgroundColor: this.backgroundColor, + height: this.height, + width: this.width, + infinite: this.infinite, + timerSoundTheme: this.timerSoundTheme, + visitorAvatarTheme: this.visitorAvatarTheme, + }, + }); + $.export("$summary", `Successfully created mural "${this.title}"`); + return response; + }, +}; diff --git a/components/mural/actions/create-sticky/create-sticky.mjs b/components/mural/actions/create-sticky/create-sticky.mjs new file mode 100644 index 0000000000000..ae273cb02d290 --- /dev/null +++ b/components/mural/actions/create-sticky/create-sticky.mjs @@ -0,0 +1,117 @@ +import mural from "../../mural.app.mjs"; + +export default { + key: "mural-create-sticky", + name: "Create Sticky", + description: "Create a new sticky note within a given mural. [See the documentation](https://developers.mural.co/public/reference/createstickynote)", + version: "0.0.1", + type: "action", + props: { + mural, + workspaceId: { + propDefinition: [ + mural, + "workspaceId", + ], + }, + muralId: { + propDefinition: [ + mural, + "muralId", + (c) => ({ + workspaceId: c.workspaceId, + }), + ], + }, + shape: { + type: "string", + label: "Shape", + description: "The shape of the sticky note widget", + options: [ + "circle", + "rectangle", + ], + }, + xPosition: { + type: "integer", + label: "X Position", + description: "The horizontal position of the widget in px. This is the distance from the left of the parent widget, such as an area. If the widget has no parent widget, this is the distance from the left of the mural.", + }, + yPosition: { + type: "integer", + label: "Y Position", + description: "The vertical position of the widget in px. This is the distance from the top of the parent widget, such as an area. If the widget has no parent widget, this is the distance from the top of the mural.", + }, + text: { + type: "string", + label: "Text", + description: "The text in the widget", + }, + title: { + type: "string", + label: "Title", + description: "The title of the widget in the outline", + optional: true, + }, + height: { + type: "integer", + label: "Height", + description: "The height of the widget in px", + optional: true, + }, + width: { + type: "integer", + label: "Width", + description: "The width of the widget in px", + optional: true, + }, + hidden: { + type: "boolean", + label: "Hidden", + description: "If `true`, the widget is hidden from non-facilitators. Applies only when the widget is in the outline", + optional: true, + }, + tagIds: { + propDefinition: [ + mural, + "tagIds", + (c) => ({ + muralId: c.muralId, + }), + ], + }, + parentId: { + propDefinition: [ + mural, + "widgetId", + (c) => ({ + muralId: c.muralId, + type: "areas", + }), + ], + label: "Parent ID", + description: "The ID of the area widget that contains the widget", + }, + }, + async run({ $ }) { + const response = await this.mural.createSticky({ + $, + muralId: this.muralId, + data: [ + { + shape: this.shape, + x: this.xPosition, + y: this.yPosition, + text: this.text, + title: this.title, + height: this.height, + width: this.width, + hidden: this.hidden, + parentId: this.parentId, + }, + ], + }); + $.export("$summary", `Successfully created sticky note with ID: ${response.value[0].id}`); + return response; + }, +}; diff --git a/components/mural/mural.app.mjs b/components/mural/mural.app.mjs new file mode 100644 index 0000000000000..79bfc8c0a77e6 --- /dev/null +++ b/components/mural/mural.app.mjs @@ -0,0 +1,247 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "mural", + propDefinitions: { + workspaceId: { + type: "string", + label: "Workspace ID", + description: "The ID of the Workspace.", + async options({ prevContext }) { + const { + value, next: nextToken, + } = await this.listWorkspaces({ + params: { + next: prevContext?.next, + }, + }); + return { + options: value?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || [], + context: { + next: nextToken, + }, + }; + }, + }, + muralId: { + type: "string", + label: "Mural ID", + description: "The ID of the Mural.", + async options({ + workspaceId, prevContext, + }) { + const { + value, next: nextToken, + } = await this.listMurals({ + workspaceId, + params: { + next: prevContext?.next, + }, + }); + return { + options: value?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })) || [], + context: { + next: nextToken, + }, + }; + }, + }, + roomId: { + type: "string", + label: "Room ID", + description: "The ID of the Room.", + async options({ + workspaceId, prevContext, + }) { + const { + value, next: nextToken, + } = await this.listRooms({ + workspaceId, + params: { + next: prevContext?.next, + }, + }); + return { + options: value?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || [], + context: { + next: nextToken, + }, + }; + }, + }, + tagIds: { + type: "string[]", + label: "Tag IDs", + description: "Unique identifiers of the tags in the widget", + optional: true, + async options({ + muralId, prevContext, + }) { + const { + value, next: nextToken, + } = await this.listTags({ + muralId, + params: { + next: prevContext?.next, + }, + }); + return { + options: value?.map(({ + id: value, text: label, + }) => ({ + value, + label, + })) || [], + context: { + next: nextToken, + }, + }; + }, + }, + widgetId: { + type: "string", + label: "Widget ID", + description: "Unique identifiers of the widget", + optional: true, + async options({ + muralId, type, prevContext, + }) { + const { + value, next: nextToken, + } = await this.listWidgets({ + muralId, + params: { + type, + next: prevContext?.next, + }, + }); + return { + options: value?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })) || [], + context: { + next: nextToken, + }, + }; + }, + }, + }, + methods: { + _baseUrl() { + return "https://app.mural.co/api/public/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + listWorkspaces(opts = {}) { + return this._makeRequest({ + path: "/workspaces", + ...opts, + }); + }, + listMurals({ + workspaceId, ...opts + }) { + return this._makeRequest({ + path: `/workspaces/${workspaceId}/murals`, + ...opts, + }); + }, + listRooms({ + workspaceId, ...opts + }) { + return this._makeRequest({ + path: `/workspaces/${workspaceId}/rooms`, + ...opts, + }); + }, + listTags({ + muralId, ...opts + }) { + return this._makeRequest({ + path: `/murals/${muralId}/tags`, + ...opts, + }); + }, + listWidgets({ + muralId, ...opts + }) { + return this._makeRequest({ + path: `/murals/${muralId}/widgets`, + ...opts, + }); + }, + createMural(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/murals", + ...opts, + }); + }, + createSticky({ + muralId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/murals/${muralId}/widgets/sticky-note`, + ...opts, + }); + }, + async *paginate({ + fn, + args, + max, + }) { + args = { + ...args, + params: { + ...args?.params, + limit: 100, + }, + }; + let count = 0; + do { + const { + value, next, + } = await fn(args); + for (const item of value) { + yield item; + if (max && ++count >= max) { + return; + } + } + args.params.next = next; + } while (args.params.next); + }, + }, +}; diff --git a/components/mural/package.json b/components/mural/package.json new file mode 100644 index 0000000000000..227fca215ed5d --- /dev/null +++ b/components/mural/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/mural", + "version": "0.1.0", + "description": "Pipedream Mural Components", + "main": "mural.app.mjs", + "keywords": [ + "pipedream", + "mural" + ], + "homepage": "https://pipedream.com/apps/mural", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/mural/sources/common/base.mjs b/components/mural/sources/common/base.mjs new file mode 100644 index 0000000000000..7fada52e4d214 --- /dev/null +++ b/components/mural/sources/common/base.mjs @@ -0,0 +1,85 @@ +import mural from "../../mural.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + mural, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + workspaceId: { + propDefinition: [ + mural, + "workspaceId", + ], + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + getArgs() { + return {}; + }, + getTsField() { + return "createdOn"; + }, + generateMeta(item) { + return { + id: item.id, + summary: this.getSummary(item), + ts: item[this.getTsField()], + }; + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + const fn = this.getResourceFn(); + const args = this.getArgs(); + const tsField = this.getTsField(); + + const results = this.mural.paginate({ + fn, + args, + max, + }); + + const items = []; + for await (const item of results) { + const ts = item[tsField]; + if (ts > lastTs) { + items.push(item); + maxTs = Math.max(ts, maxTs); + } + } + + this._setLastTs(maxTs); + + items.forEach((item) => { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/mural/sources/new-area/new-area.mjs b/components/mural/sources/new-area/new-area.mjs new file mode 100644 index 0000000000000..d97417079f6f4 --- /dev/null +++ b/components/mural/sources/new-area/new-area.mjs @@ -0,0 +1,40 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "mural-new-area", + name: "New Area Created", + description: "Emit new event when a new area is created in the user's mural", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + muralId: { + propDefinition: [ + common.props.mural, + "muralId", + (c) => ({ + workspaceId: c.workspaceId, + }), + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.mural.listWidgets; + }, + getArgs() { + return { + muralId: this.muralId, + params: { + type: "areas", + }, + }; + }, + getSummary(item) { + return `New Area Widget ID: ${item.id}`; + }, + }, +}; diff --git a/components/mural/sources/new-mural/new-mural.mjs b/components/mural/sources/new-mural/new-mural.mjs new file mode 100644 index 0000000000000..49abc7fd03fda --- /dev/null +++ b/components/mural/sources/new-mural/new-mural.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "mural-new-mural", + name: "New Mural Created", + description: "Emit new event when a new mural is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.mural.listMurals; + }, + getArgs() { + return { + workspaceId: this.workspaceId, + params: { + sortBy: "lastCreated", + }, + }; + }, + getSummary(item) { + return `New Mural ID: ${item.id}`; + }, + }, +}; diff --git a/components/mural/sources/new-sticky/new-sticky.mjs b/components/mural/sources/new-sticky/new-sticky.mjs new file mode 100644 index 0000000000000..6316a2dc51261 --- /dev/null +++ b/components/mural/sources/new-sticky/new-sticky.mjs @@ -0,0 +1,40 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "mural-new-sticky", + name: "New Sticky Note Created", + description: "Emit new event each time a new sticky note is created in a specified mural", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + muralId: { + propDefinition: [ + common.props.mural, + "muralId", + (c) => ({ + workspaceId: c.workspaceId, + }), + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.mural.listWidgets; + }, + getArgs() { + return { + muralId: this.muralId, + params: { + type: "sticky notes", + }, + }; + }, + getSummary(item) { + return `New Sticky Note ID: ${item.id}`; + }, + }, +}; diff --git a/components/murlist/README.md b/components/murlist/README.md index 451a9a9631659..3164a783de277 100644 --- a/components/murlist/README.md +++ b/components/murlist/README.md @@ -1,5 +1,11 @@ # Overview -With the MurList API, you can build applications that manage lists of tasks, -to-dos, or anything else that can be represented as a list. You can also add, -remove, and update items in the list, as well as track the status of each item. +MurList API offers the ability to manage and automate email lists and campaigns, streamlining the processes of subscriber management, email sending, and performance tracking. With MurList, you can create, update, and delete subscriber data, design personalized email campaigns, and analyze metrics to enhance your marketing strategies. Pipedream, as a serverless integration and compute platform, supercharges these capabilities by enabling you to connect MurList with an array of other apps and services, creating sophisticated workflows that can run on triggers or schedules. + +# Example Use Cases + +- **Subscriber Segmentation and Automated Campaigns**: Automate the segmentation of subscribers based on their engagement or behavior by using MurList with a CRM like Salesforce. When a subscriber's status updates to "Engaged" in Salesforce, trigger a Pipedream workflow to add that user to a specific MurList segment and kick off a targeted email campaign. + +- **Performance-Driven Content Creation**: Integrate MurList with a content management system like WordPress. Use MurList's performance data to determine the most popular content topics, and trigger a Pipedream workflow that creates a new content draft in WordPress based on those topics, helping to drive subscriber engagement through relevant content. + +- **E-commerce Customer Retention**: Connect MurList with Shopify to build a workflow that reacts to customer purchase history. Set up a Pipedream workflow that, upon a new purchase in Shopify, checks the customer's purchase frequency and sends a personalized thank-you email via MurList. For frequent buyers, automatically enroll them in a loyalty program campaign, enhancing customer retention. diff --git a/components/mux/README.md b/components/mux/README.md index 169ff60a483d7..e3b78c63bd1dd 100644 --- a/components/mux/README.md +++ b/components/mux/README.md @@ -1,9 +1,11 @@ # Overview -With Mux, you can build a variety of video-related applications and services, -including: +Mux is a powerful API that simplifies the process of working with video and audio data. By leveraging Mux on Pipedream, you unlock the potential to automate video and audio streaming workflows, analyze media performance, and integrate seamlessly with other services to enrich your media content strategy. With Mux's ability to manage video assets, including uploading, encoding, and delivering content across devices, combined with Pipedream's serverless execution model, you can create dynamic, scalable, and highly customized media operations. -- Video streaming and encoding -- Video player and recorder -- Video analytics and transcoding -- Video content management and distribution +# Example Use Cases + +- **Automated Video Content Pipeline**: Use Mux to automate the ingestion of user-generated content. When a video is uploaded to a storage platform like S3, trigger a Pipedream workflow to encode and stream the video with Mux, then post the video's details to a CMS like WordPress. + +- **Real-time Media Performance Monitoring**: Monitor your Mux video analytics in real-time by connecting Mux with a Pipedream workflow. Set up alerts in Slack or via email when key performance indicators, such as buffering rates or video startup times, reach certain thresholds, keeping your team informed and proactive. + +- **Enhanced Video Engagement Tracking**: Enhance user engagement through video interactions by combining Mux with Pipedream. When a user performs an action like pausing a video or changing the resolution, trigger a workflow that logs these interactions into a CRM like Salesforce or a database, enabling detailed analysis of user preferences and behavior. diff --git a/components/mux/actions/create-asset-track/create-asset-track.mjs b/components/mux/actions/create-asset-track/create-asset-track.mjs index 7e35772b48812..6b42d8a0a6686 100644 --- a/components/mux/actions/create-asset-track/create-asset-track.mjs +++ b/components/mux/actions/create-asset-track/create-asset-track.mjs @@ -4,7 +4,7 @@ export default { key: "mux-create-asset-track", name: "Create Asset Track", description: "Adds an asset track (for example, subtitles) to an asset. [See the documentation](https://docs.mux.com/api-reference#video/operation/create-asset-track)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { mux, diff --git a/components/mux/actions/create-asset/create-asset.mjs b/components/mux/actions/create-asset/create-asset.mjs index 514d89619a28d..771d5a20e8943 100644 --- a/components/mux/actions/create-asset/create-asset.mjs +++ b/components/mux/actions/create-asset/create-asset.mjs @@ -4,7 +4,7 @@ export default { key: "mux-create-asset", name: "Create Asset", description: "Create a new asset with a track. [See the documentation](https://docs.mux.com/api-reference#video/operation/create-asset)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { mux, diff --git a/components/mux/actions/get-asset-or-livestream-id/get-asset-or-livestream-id.mjs b/components/mux/actions/get-asset-or-livestream-id/get-asset-or-livestream-id.mjs new file mode 100644 index 0000000000000..ed354bdd4fd5e --- /dev/null +++ b/components/mux/actions/get-asset-or-livestream-id/get-asset-or-livestream-id.mjs @@ -0,0 +1,38 @@ +import mux from "../../mux.app.mjs"; + +export default { + key: "mux-get-asset-or-livestream-id", + name: "Get Asset or Livestream ID", + description: "Returns an Asset or Livestream ID from a Playback ID. [See the documentation](https://docs.mux.com/api-reference#video/operation/get-asset-or-livestream-id)", + version: "0.0.1", + type: "action", + props: { + mux, + assetId: { + propDefinition: [ + mux, + "assetId", + ], + optional: true, + }, + playbackId: { + propDefinition: [ + mux, + "playbackId", + (c) => ({ + assetId: c.assetId, + }), + ], + }, + }, + async run({ $ }) { + const { data } = await this.mux.getAssetOrLivestreamId({ + $, + playbackId: this.playbackId, + }); + if (data?.object) { + $.export("$summary", `Successfully retrieved asset or livestream ID: ${data.object.id}`); + } + return data; + }, +}; diff --git a/components/mux/actions/get-asset/get-asset.mjs b/components/mux/actions/get-asset/get-asset.mjs new file mode 100644 index 0000000000000..e0958d7846463 --- /dev/null +++ b/components/mux/actions/get-asset/get-asset.mjs @@ -0,0 +1,28 @@ +import mux from "../../mux.app.mjs"; + +export default { + key: "mux-get-asset", + name: "Get Asset", + description: "Retrieves an asset. [See the documentation](https://docs.mux.com/api-reference#video/operation/get-asset)", + version: "0.0.2", + type: "action", + props: { + mux, + assetId: { + propDefinition: [ + mux, + "assetId", + ], + }, + }, + async run({ $ }) { + const { data } = await this.mux.getAsset({ + $, + assetId: this.assetId, + }); + if (data?.tracks) { + $.export("$summary", `Successfully retrieved asset: ${data.id}`); + } + return data; + }, +}; diff --git a/components/mux/mux.app.mjs b/components/mux/mux.app.mjs index f5adf9071e203..0199e03376e1e 100644 --- a/components/mux/mux.app.mjs +++ b/components/mux/mux.app.mjs @@ -18,6 +18,32 @@ export default { return data?.map(({ id }) => id) || []; }, }, + playbackId: { + type: "string", + label: "Playback ID", + description: "A live stream's playback ID", + async options({ + assetId, page, + }) { + if (assetId) { + const { data } = await this.getAsset({ + assetId, + }); + return data.playback_ids?.map(({ id }) => id ) || []; + } + const { data } = await this.listAssets({ + params: { + page: page + 1, + }, + }); + const playbackIds = []; + for (const asset of data) { + const assetPlaybackIds = asset.playback_ids?.map(({ id }) => id); + playbackIds.push(...assetPlaybackIds); + } + return playbackIds; + }, + }, playbackPolicy: { type: "string", label: "Playback Policy", @@ -91,6 +117,22 @@ export default { ...args, }); }, + getAsset({ + assetId, ...args + }) { + return this._makeRequest({ + path: `/assets/${assetId}`, + ...args, + }); + }, + getAssetOrLivestreamId({ + playbackId, ...args + }) { + return this._makeRequest({ + path: `/playback-ids/${playbackId}`, + ...args, + }); + }, listAssets(args = {}) { return this._makeRequest({ path: "/assets", diff --git a/components/mux/package.json b/components/mux/package.json index 5573c424a237e..0085dac524f12 100644 --- a/components/mux/package.json +++ b/components/mux/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/mux", - "version": "0.1.0", + "version": "0.1.3", "description": "Pipedream Mux Components", "main": "mux.app.mjs", "keywords": [ diff --git a/components/mux/sources/live-stream-active/live-stream-active.mjs b/components/mux/sources/live-stream-active/live-stream-active.mjs index 20486dc6c9265..55d18c189ffc6 100644 --- a/components/mux/sources/live-stream-active/live-stream-active.mjs +++ b/components/mux/sources/live-stream-active/live-stream-active.mjs @@ -6,7 +6,7 @@ export default { key: "mux-live-stream-active", name: "New Live Stream Active", description: "Emit new event when a live-stream is set to active status.", - version: "0.0.1", + version: "0.0.2", type: "source", props: { mux, diff --git a/components/mux/sources/video-asset-ready/video-asset-ready.mjs b/components/mux/sources/video-asset-ready/video-asset-ready.mjs index 19433df0fb6a2..61d2ec030d37c 100644 --- a/components/mux/sources/video-asset-ready/video-asset-ready.mjs +++ b/components/mux/sources/video-asset-ready/video-asset-ready.mjs @@ -6,7 +6,7 @@ export default { key: "mux-video-asset-ready", name: "New Video Asset Ready", description: "Emit new event when a video asset is set to ready status.", - version: "0.0.1", + version: "0.0.2", type: "source", props: { mux, diff --git a/components/mx_technologies/README.md b/components/mx_technologies/README.md new file mode 100644 index 0000000000000..af1931fe6ed2b --- /dev/null +++ b/components/mx_technologies/README.md @@ -0,0 +1,11 @@ +# Overview + +The MX Technologies API provides a range of financial data solutions, enabling users to obtain insights into personal finances, conduct risk analysis, and offer personalized financial advice. Within Pipedream, you can harness the power of the MX API to automate financial data aggregation, customer profiling, and trigger custom workflows based on financial events or changes in user data. + +# Example Use Cases + +- **Aggregate User Financial Data**: Use the MX Technologies API on Pipedream to consolidate user financial data from various institutions. Trigger a workflow whenever a new account is linked or updated, and store this data in a secure data store for analysis or reporting. + +- **Personalized Financial Alerts**: Create a workflow that monitors transaction data from the MX API for specific events, such as large deposits or unusual spending patterns. When detected, automatically send personalized alerts or advice to the user via email or SMS through integrated communication platforms like Twilio or SendGrid. + +- **Financial Health Dashboard Updates**: Set up a Pipedream workflow that periodically fetches financial data using the MX API, processes the data to assess financial health metrics, and then updates a user's financial dashboard in an app like Google Sheets or a custom web application. diff --git a/components/mx_toolbox/README.md b/components/mx_toolbox/README.md new file mode 100644 index 0000000000000..dc7732a717f9a --- /dev/null +++ b/components/mx_toolbox/README.md @@ -0,0 +1,11 @@ +# Overview + +The Mx Toolbox API offers a suite of network diagnostic and lookup tools useful for IT professionals and system administrators. With this API on Pipedream, you can automate checks for domain health, DNS records, blacklisting, and more, integrating these insights into your broader IT infrastructure. Pipedream's serverless platform allows you to create workflows that react to various triggers and pass data between the Mx Toolbox API and numerous other apps and services. + +# Example Use Cases + +- **Automated Domain Health Monitoring**: Trigger a Pipedream workflow daily to check the health of your domain using Mx Toolbox's domain health API. If issues are detected, the workflow could alert your team via Slack, create a ticket in a service like Jira, or log the incident for further review. + +- **Blacklist Status Notification**: Set up a workflow that regularly checks your domain's IP against various blacklists using Mx Toolbox. On detection of a listing, the workflow can notify your security team through Email by integrating with an email service like SendGrid, enabling quick response to potential reputation issues. + +- **DNS Change Management**: Monitor for changes in DNS records by scheduling DNS lookups through Mx Toolbox. When a change is identified, the workflow can update a Google Sheet with the new records, send a notification to a Discord channel, or trigger a webhook to inform other systems of the change. diff --git a/components/myotp_app/actions/send-otp/send-otp.mjs b/components/myotp_app/actions/send-otp/send-otp.mjs new file mode 100644 index 0000000000000..646baeae061d7 --- /dev/null +++ b/components/myotp_app/actions/send-otp/send-otp.mjs @@ -0,0 +1,38 @@ +import app from "../../myotp_app.app.mjs"; + +export default { + key: "myotp_app-send-otp", + name: "Send OTP", + description: "Generate a One Time Password (OTP) and send it to the specified phone number. [See the documentation](https://api.myotp.app/swagger-ui/#/default/generate_otp_endpoint)", + version: "0.0.1", + type: "action", + props: { + app, + phoneNumber: { + propDefinition: [ + app, + "phoneNumber", + ], + }, + otpValidity: { + propDefinition: [ + app, + "otpValidity", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.app.sendOTP({ + $, + data: { + phone_number: this.phoneNumber, + otp_validity: this.otpValidity, + }, + }); + + $.export("$summary", `Successfully sent OTP. Valid until '${response.expires_at}'`); + + return response; + }, +}; diff --git a/components/myotp_app/actions/verify-otp/verify-otp.mjs b/components/myotp_app/actions/verify-otp/verify-otp.mjs new file mode 100644 index 0000000000000..02a0f1e3f9b83 --- /dev/null +++ b/components/myotp_app/actions/verify-otp/verify-otp.mjs @@ -0,0 +1,44 @@ +import app from "../../myotp_app.app.mjs"; + +export default { + key: "myotp_app-verify-otp", + name: "Verify OTP", + description: "Validate the OTP for successful verification. [See the documentation](https://api.myotp.app/swagger-ui/#/default/verify_otp_endpoint)", + version: "0.0.1", + type: "action", + props: { + app, + phoneNumber: { + propDefinition: [ + app, + "phoneNumber", + ], + }, + messageID: { + propDefinition: [ + app, + "messageID", + ], + }, + otp: { + propDefinition: [ + app, + "otp", + ], + }, + }, + async run({ $ }) { + const response = await this.app.verifyOTP({ + $, + data: { + phone_number: this.phoneNumber, + message_id: this.messageID, + otp: this.otp, + }, + }); + + $.export("$summary", `Successfully verified OTP. Status: ${response.status}`); + + return response; + }, +}; diff --git a/components/myotp_app/myotp_app.app.mjs b/components/myotp_app/myotp_app.app.mjs new file mode 100644 index 0000000000000..6cdcdeca41687 --- /dev/null +++ b/components/myotp_app/myotp_app.app.mjs @@ -0,0 +1,80 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "myotp_app", + propDefinitions: { + otpValidity: { + type: "integer", + label: "OTP Validity", + description: "Validity of the OTP", + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "Phone number to which the OTP will be sent", + }, + messageID: { + type: "string", + label: "Message ID", + description: "ID of the OTP message", + async options() { + const response = await this.getReport({}); + const messagesIDs = response.transactions; + return messagesIDs.map(({ + message_id, phone_number, + }) => ({ + value: message_id, + label: phone_number, + })); + }, + }, + otp: { + type: "string", + label: "OTP", + description: "One-time password sent to the phone number", + }, + }, + methods: { + _baseUrl() { + return "https://api.myotp.app"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "X-API-Key": `${this.$auth.api_key}`, + }, + }); + }, + async sendOTP(args = {}) { + return this._makeRequest({ + method: "post", + path: "/generate_otp", + ...args, + }); + }, + async verifyOTP(args = {}) { + return this._makeRequest({ + method: "post", + path: "/verify_otp", + ...args, + }); + }, + async getReport(args = {}) { + return this._makeRequest({ + method: "post", + path: "/report", + ...args, + }); + }, + }, +}; diff --git a/components/myotp_app/package.json b/components/myotp_app/package.json new file mode 100644 index 0000000000000..d1d76b00924fe --- /dev/null +++ b/components/myotp_app/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/myotp_app", + "version": "0.1.0", + "description": "Pipedream MyOTP.App Components", + "main": "myotp_app.app.mjs", + "keywords": [ + "pipedream", + "myotp_app" + ], + "homepage": "https://pipedream.com/apps/myotp_app", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/myphoner/README.md b/components/myphoner/README.md index 8b3cbd1495022..d3dda9041a4f4 100644 --- a/components/myphoner/README.md +++ b/components/myphoner/README.md @@ -1,11 +1,11 @@ # Overview -Myphoner provides an API that can be used to create phone applications. This -API can be used to create a wide variety of phone applications, including: - -- VoIP applications -- Messaging applications -- SMS applications -- Voice recognition applications -- Call forwarding applications -- And more! +Myphoner API offers a streamlined approach to managing your lead tracking and cold calling workflow. By connecting Myphoner with Pipedream, you can automate lead distribution, follow-ups, and integrate with other CRMs or communication tools. Pipedream's serverless platform enables you to create custom workflows without managing infrastructure, letting you focus on improving sales processes, enhancing lead qualification, and nurturing customer relationships through automated, data-driven actions. + +# Example Use Cases + +- **Lead Auto-Assignment and Notification**: Automatically assign new leads to sales reps based on custom criteria such as geographic location or expertise. Once assigned, trigger a notification to the assigned rep via Slack or email, ensuring they can act on the lead promptly. + +- **Scheduled Follow-Up Reminders**: Configure a workflow to schedule follow-ups by creating tasks in project management tools like Trello or Asana when a lead moves to a specific stage in Myphoner. This helps in maintaining timely communication and keeping the sales pipeline active. + +- **Lead Qualification and CRM Integration**: Use Pipedream to listen for new leads or changes in lead status on Myphoner, evaluate their quality based on predefined metrics, and push the qualified leads to a CRM platform like Salesforce or HubSpot. This ensures that high-potential leads are nurtured with the appropriate resources. diff --git a/components/myphoner/package.json b/components/myphoner/package.json new file mode 100644 index 0000000000000..904c1913731f0 --- /dev/null +++ b/components/myphoner/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/myphoner", + "version": "0.6.0", + "description": "Pipedream myphoner Components", + "main": "myphoner.app.mjs", + "keywords": [ + "pipedream", + "myphoner" + ], + "homepage": "https://pipedream.com/apps/myphoner", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/mysql/README.md b/components/mysql/README.md index 354576b409838..99dbbbc14a587 100644 --- a/components/mysql/README.md +++ b/components/mysql/README.md @@ -1,12 +1,11 @@ # Overview -MySQL is a powerful database management system used by some of the largest -organizations in the world, including Facebook, Google, and Amazon. MySQL is an -open-source relational database management system (RDBMS), as well as a popular -choice for web applications used by millions of websites. +The MySQL application on Pipedream enables direct interaction with your MySQL databases, allowing you to perform CRUD operations—create, read, update, delete—on your data with ease. You can leverage these capabilities to automate data synchronization, report generation, and event-based triggers that kick off workflows in other apps. With Pipedream's serverless platform, you can connect MySQL to hundreds of other services without managing infrastructure, crafting complex code, or handling authentication. -Some applications that can be built using the MySQL API include: +# Example Use Cases -- A web application that stores and retrieves data from a MySQL database -- A desktop application that uses a MySQL database for data storage -- A mobile application that interacts with a MySQL database +- **Automated Data Backup Workflow**: Automatically back up specific tables or datasets from MySQL to cloud storage services like Google Drive or Amazon S3 at regular intervals. This flow can be set up to trigger on a schedule, ensuring your data is consistently backed up without manual intervention. + +- **Real-time Customer Data Sync**: Sync new customer information from a CRM platform like Salesforce or HubSpot to your MySQL database in real time. Whenever a new contact is added to the CRM, the workflow triggers and inserts or updates the corresponding record in MySQL, keeping your customer data aligned across systems. + +- **Email Alerts on Database Changes**: Monitor specific tables in your MySQL database for changes, such as new entries or updates to existing records. Use this trigger to send email notifications through SendGrid or another email service whenever there's a significant database modification, keeping relevant stakeholders informed. diff --git a/components/mysql/actions/create-row/create-row.mjs b/components/mysql/actions/create-row/create-row.mjs index 397d17bef8ab2..e2674c7061ec8 100644 --- a/components/mysql/actions/create-row/create-row.mjs +++ b/components/mysql/actions/create-row/create-row.mjs @@ -6,7 +6,7 @@ export default { name: "Create Row", description: "Adds a new row. [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/insert.html)", type: "action", - version: "0.0.6", + version: "2.0.5", props: { mysql, table: { diff --git a/components/mysql/actions/delete-row/delete-row.mjs b/components/mysql/actions/delete-row/delete-row.mjs index 4dffb12c778a0..27d7bea08773c 100644 --- a/components/mysql/actions/delete-row/delete-row.mjs +++ b/components/mysql/actions/delete-row/delete-row.mjs @@ -5,7 +5,7 @@ export default { name: "Delete Row", description: "Delete an existing row. [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/delete.html)", type: "action", - version: "0.0.5", + version: "2.0.5", props: { mysql, table: { diff --git a/components/mysql/actions/execute-query/execute-query.mjs b/components/mysql/actions/execute-query/execute-query.mjs index 3df90bcf7ab56..b0f998f5cefe3 100644 --- a/components/mysql/actions/execute-query/execute-query.mjs +++ b/components/mysql/actions/execute-query/execute-query.mjs @@ -5,7 +5,7 @@ export default { name: "Execute Query", description: "Find row(s) via a custom query. [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/select.html)", type: "action", - version: "0.0.5", + version: "2.0.5", props: { mysql, table: { diff --git a/components/mysql/actions/execute-raw-query/execute-raw-query.mjs b/components/mysql/actions/execute-raw-query/execute-raw-query.mjs new file mode 100644 index 0000000000000..432179dd1460f --- /dev/null +++ b/components/mysql/actions/execute-raw-query/execute-raw-query.mjs @@ -0,0 +1,28 @@ +import mysql from "../../mysql.app.mjs"; + +export default { + key: "mysql-execute-raw-query", + name: "Execute SQL Query", + description: "Execute a custom MySQL query. See [our docs](https://pipedream.com/docs/databases/working-with-sql) to learn more about working with SQL in Pipedream.", + type: "action", + version: "2.0.1", + props: { + mysql, + // eslint-disable-next-line pipedream/props-description + sql: { + type: "sql", + auth: { + app: "mysql", + }, + label: "SQL Query", + }, + }, + async run({ $ }) { + const args = this.mysql.executeQueryAdapter(this.sql); + const data = await this.mysql.executeQuery(args); + $.export("$summary", `Returned ${data.length} ${data.length === 1 + ? "row" + : "rows"}`); + return data; + }, +}; diff --git a/components/mysql/actions/execute-stored-procedure/execute-stored-procedure.mjs b/components/mysql/actions/execute-stored-procedure/execute-stored-procedure.mjs index 0b715aab30d2d..f10506eed0ab2 100644 --- a/components/mysql/actions/execute-stored-procedure/execute-stored-procedure.mjs +++ b/components/mysql/actions/execute-stored-procedure/execute-stored-procedure.mjs @@ -5,7 +5,7 @@ export default { name: "Execute Stored Procedure", description: "Execute Stored Procedure. [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/stored-programs-defining.html)", type: "action", - version: "0.1.2", + version: "2.0.5", props: { mysql, storedProcedure: { diff --git a/components/mysql/actions/find-row/find-row.mjs b/components/mysql/actions/find-row/find-row.mjs index d7901440ee9ec..4827f3831de71 100644 --- a/components/mysql/actions/find-row/find-row.mjs +++ b/components/mysql/actions/find-row/find-row.mjs @@ -5,7 +5,7 @@ export default { name: "Find Row", description: "Finds a row in a table via a lookup column. [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/select.html)", type: "action", - version: "0.0.5", + version: "2.0.5", props: { mysql, table: { diff --git a/components/mysql/actions/update-row/update-row.mjs b/components/mysql/actions/update-row/update-row.mjs index 7263aca55da65..36477bd617647 100644 --- a/components/mysql/actions/update-row/update-row.mjs +++ b/components/mysql/actions/update-row/update-row.mjs @@ -6,7 +6,7 @@ export default { name: "Update Row", description: "Updates an existing row. [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/update.html)", type: "action", - version: "0.0.6", + version: "2.0.5", props: { mysql, table: { diff --git a/components/mysql/common/constants.mjs b/components/mysql/common/constants.mjs new file mode 100644 index 0000000000000..bb494448d5ed4 --- /dev/null +++ b/components/mysql/common/constants.mjs @@ -0,0 +1,16 @@ +const SSL_CONFIG = { + verify_identity: { + rejectUnauthorized: true, + verifyIdentity: true, + }, + verify_ca: { + rejectUnauthorized: true, + }, + skip_verification: { + rejectUnauthorized: false, + }, +}; + +export default { + SSL_CONFIG, +}; diff --git a/components/mysql/mysql.app.mjs b/components/mysql/mysql.app.mjs index 0074fa78299a4..fb4d1dcfc471c 100644 --- a/components/mysql/mysql.app.mjs +++ b/components/mysql/mysql.app.mjs @@ -1,4 +1,9 @@ import mysqlClient from "mysql2/promise"; +import { + sqlProxy, + sqlProp, +} from "@pipedream/platform"; +import constants from "./common/constants.mjs"; export default { type: "app", @@ -95,33 +100,117 @@ export default { }, }, methods: { - getConfig() { + ...sqlProxy.methods, + ...sqlProp.methods, + _getSslConfig() { + const { + ca, + key, + cert, + ssl_verification_mode: mode, + } = this.$auth; + + const defaultConfig = { + ...(ca && { + rejectUnauthorized: true, + verifyIdentity: true, + }), + }; + + const sslConfg = constants.SSL_CONFIG[mode] || defaultConfig; + + const ssl = { + ...(ca && { + ca, + }), + ...(key && { + key, + }), + ...(cert && { + cert, + }), + ...sslConfg, + }; + + return Object.keys(ssl).length > 0 + ? ssl + : undefined; + }, + /** + * A helper method to get the configuration object that's directly fed to + * the MySQL client constructor. Used by other features (like the SQL proxy) + * to initialize their clients in an identical way. + * + * @returns {object} - Configuration object for the MySQL client + */ + getClientConfiguration() { const { host, port, - username, + username: user, password, database, } = this.$auth; + return { host, port, - user: username, + user, password, database, - ssl: { - rejectUnauthorized: false, - }, + ssl: this._getSslConfig(), }; }, - async closeConnection(connection) { - await connection.end(); - return new Promise((resolve) => { - connection.connection.stream.on("close", resolve); - }); + /** + * Adapts the arguments to `executeQuery` so that they can be consumed by + * the SQL proxy (when applicable). Note that this method is not intended to + * be used by the component directly. + * @param {object} preparedStatement The prepared statement to be sent to the DB. + * @param {string} preparedStatement.sql The prepared SQL query to be executed. + * @param {string[]} preparedStatement.values The values to replace in the SQL query. + * @returns {object} - The adapted query and parameters. + */ + proxyAdapter(preparedStatement = {}) { + const { + sql: query = "", + values: params = [], + } = preparedStatement; + return { + query, + params, + }; + }, + /** + * A method that performs the inverse transformation of `proxyAdapter`. + * + * @param {object} proxyArgs - The output of `proxyAdapter`. + * @param {string} proxyArgs.query - The SQL query to be executed. + * @param {string[]} proxyArgs.params - The values to replace in the SQL + * query. + * @returns {object} - The adapted query and parameters, compatible with + * `executeQuery`. + */ + executeQueryAdapter(proxyArgs = {}) { + const { + query: sql = "", + params: values = [], + } = proxyArgs; + return { + sql, + values, + }; }, + /** + * Executes a query against the MySQL database. This method takes care of + * connecting to the database, executing the query, and closing the + * connection. + * @param {object} preparedStatement - The prepared statement to be sent to the DB. + * @param {string} preparedStatement.sql - The prepared SQL query to be executed. + * @param {string[]} preparedStatement.values - The values to replace in the SQL query. + * @returns {object[]} - The rows returned by the DB as a result of the query. + */ async executeQuery(preparedStatement = {}) { - const config = this.getConfig(); + const config = this.getClientConfiguration(); let connection; try { connection = await mysqlClient.createConnection(config); @@ -136,10 +225,59 @@ export default { } finally { if (connection) { - await this.closeConnection(connection); + await this._closeConnection(connection); } } }, + /** + * A helper method to get the schema of the database. Used by other features + * (like the `sql` prop) to enrich the code editor and provide the user with + * auto-complete and fields suggestion. + * + * @returns {DbInfo} The schema of the database, which is a + * JSON-serializable object. + */ + async getSchema() { + const sql = ` + SELECT t.table_schema AS tableSchema, + t.table_name AS tableName, + CAST(t.table_rows AS UNSIGNED) AS rowCount, + c.column_name AS columnName, + c.data_type AS dataType, + c.is_nullable AS isNullable, + c.column_default AS columnDefault + FROM information_schema.tables AS t + JOIN information_schema.columns AS c ON t.table_name = c.table_name + AND t.table_schema = c.table_schema + WHERE t.table_schema = ? + ORDER BY t.table_name, + c.ordinal_position + `; + const rows = await this.executeQuery({ + sql, + values: [ + this.$auth.database, + ], + }); + return rows.reduce((acc, row) => { + acc[row.tableName] ??= { + metadata: { + rowCount: row.rowCount, + }, + schema: {}, + }; + acc[row.tableName].schema[row.columnName] = { + ...row, + }; + return acc; + }, {}); + }, + async _closeConnection(connection) { + await connection.end(); + return new Promise((resolve) => { + connection.connection.stream.on("close", resolve); + }); + }, async listStoredProcedures(args = {}) { const procedures = await this.executeQuery({ sql: "SHOW PROCEDURE STATUS", @@ -163,11 +301,12 @@ export default { }); }, listBaseTables({ - lastResult, ...args + lastResult, + ...args } = {}) { return this.executeQuery({ sql: ` - SELECT * FROM INFORMATION_SCHEMA.TABLES + SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND CREATE_TIME > ? ORDER BY CREATE_TIME DESC @@ -179,11 +318,12 @@ export default { }); }, listTopTables({ - maxCount = 10, ...args + maxCount = 10, + ...args } = {}) { return this.executeQuery({ sql: ` - SELECT * FROM INFORMATION_SCHEMA.TABLES + SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' ORDER BY CREATE_TIME DESC LIMIT ${maxCount} @@ -192,7 +332,8 @@ export default { }); }, listColumns({ - table, ...args + table, + ...args } = {}) { return this.executeQuery({ sql: `SHOW COLUMNS FROM \`${table}\``, @@ -200,7 +341,9 @@ export default { }); }, listNewColumns({ - table, previousColumns, ...args + table, + previousColumns, + ...args } = {}) { return this.executeQuery({ sql: ` @@ -222,12 +365,15 @@ export default { * that has been previously returned. */ listRows({ - table, column, lastResult, ...args + table, + column, + lastResult, + ...args } = {}) { return this.executeQuery({ sql: ` SELECT * FROM \`${table}\` - WHERE \`${column}\` > ? + WHERE \`${column}\` > ? ORDER BY \`${column}\` DESC `, values: [ @@ -246,7 +392,10 @@ export default { * @param {number} maxCount - Maximum number of results to return. */ listMaxRows({ - table, column, maxCount = 10, ...args + table, + column, + maxCount = 10, + ...args } = {}) { return this.executeQuery({ sql: ` @@ -258,7 +407,8 @@ export default { }); }, getPrimaryKey({ - table, ...args + table, + ...args } = {}) { return this.executeQuery({ sql: "SHOW KEYS FROM ? WHERE Key_name = 'PRIMARY'", @@ -269,7 +419,8 @@ export default { }); }, async listColumnNames({ - table, ...args + table, + ...args } = {}) { const columns = await this.listColumns({ table, @@ -278,7 +429,10 @@ export default { return columns.map((column) => column.Field); }, findRows({ - table, condition, values = [], ...args + table, + condition, + values = [], + ...args } = {}) { return this.executeQuery({ sql: `SELECT * FROM \`${table}\` WHERE ${condition}`, @@ -287,7 +441,10 @@ export default { }); }, deleteRows({ - table, condition, values = [], ...args + table, + condition, + values = [], + ...args } = {}) { return this.executeQuery({ sql: `DELETE FROM \`${table}\` WHERE ${condition}`, @@ -296,7 +453,10 @@ export default { }); }, insertRow({ - table, columns = [], values = [], ...args + table, + columns = [], + values = [], + ...args } = {}) { const placeholder = values.map(() => "?").join(","); return this.executeQuery({ @@ -309,8 +469,11 @@ export default { }); }, updateRow({ - table, condition, - conditionValues = [], columnsToUpdate = [], valuesToUpdate = [], + table, + condition, + conditionValues = [], + columnsToUpdate = [], + valuesToUpdate = [], ...args } = {}) { const updates = @@ -330,7 +493,9 @@ export default { }); }, executeStoredProcedure({ - storedProcedure, values = [], ...args + storedProcedure, + values = [], + ...args } = {}) { return this.executeQuery({ sql: `CALL ${storedProcedure}(${values.map(() => "?")})`, diff --git a/components/mysql/package.json b/components/mysql/package.json index 273333aceb79f..3685c0885872a 100644 --- a/components/mysql/package.json +++ b/components/mysql/package.json @@ -1,7 +1,7 @@ { "name": "@pipedream/mysql", - "version": "0.0.11", - "description": "Pipedream Mysql Components", + "version": "2.3.1", + "description": "Pipedream MySQL Components", "main": "mysql.app.mjs", "keywords": [ "pipedream", @@ -12,7 +12,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.2.0", + "@pipedream/platform": "^2.0.0", "mysql2": "^3.9.2", "mysql2-promise": "^0.1.4", "uuid": "^8.3.2" diff --git a/components/mysql/sources/new-column/new-column.mjs b/components/mysql/sources/new-column/new-column.mjs index 1b70cce5f61dc..07739f2278f49 100644 --- a/components/mysql/sources/new-column/new-column.mjs +++ b/components/mysql/sources/new-column/new-column.mjs @@ -6,7 +6,7 @@ export default { name: "New Column", description: "Emit new event when you add a new column to a table. [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/show-columns.html)", type: "source", - version: "0.0.7", + version: "2.0.5", dedupe: "unique", props: { ...common.props, diff --git a/components/mysql/sources/new-or-updated-row/new-or-updated-row.mjs b/components/mysql/sources/new-or-updated-row/new-or-updated-row.mjs index 6cfff72dca782..9debffc7bd531 100644 --- a/components/mysql/sources/new-or-updated-row/new-or-updated-row.mjs +++ b/components/mysql/sources/new-or-updated-row/new-or-updated-row.mjs @@ -9,7 +9,7 @@ export default { name: "New or Updated Row", description: "Emit new event when you add or modify a new row in a table. [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/select.html)", type: "source", - version: "0.0.8", + version: "2.0.5", dedupe: "unique", props: { ...common.props, diff --git a/components/mysql/sources/new-row-custom-query/new-row-custom-query.mjs b/components/mysql/sources/new-row-custom-query/new-row-custom-query.mjs index f1a2c5d7eb60e..2589defb96fc1 100644 --- a/components/mysql/sources/new-row-custom-query/new-row-custom-query.mjs +++ b/components/mysql/sources/new-row-custom-query/new-row-custom-query.mjs @@ -8,7 +8,7 @@ export default { key: "mysql-new-row-custom-query", name: "New Row (Custom Query)", description: "Emit new event when new rows are returned from a custom query. [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/select.html)", - version: "0.0.7", + version: "2.0.5", type: "source", dedupe: "unique", props: { diff --git a/components/mysql/sources/new-row/new-row.mjs b/components/mysql/sources/new-row/new-row.mjs index 42a14902e7f58..73def448fd1e4 100644 --- a/components/mysql/sources/new-row/new-row.mjs +++ b/components/mysql/sources/new-row/new-row.mjs @@ -8,7 +8,7 @@ export default { name: "New Row", description: "Emit new event when you add a new row to a table. [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/select.html)", type: "source", - version: "0.0.7", + version: "2.0.5", dedupe: "unique", props: { ...common.props, diff --git a/components/mysql/sources/new-table/new-table.mjs b/components/mysql/sources/new-table/new-table.mjs index 4a5a19aa8723b..312f41ccc50c8 100644 --- a/components/mysql/sources/new-table/new-table.mjs +++ b/components/mysql/sources/new-table/new-table.mjs @@ -6,7 +6,7 @@ export default { name: "New Table", description: "Emit new event when a new table is added to a database. [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/select.html)", type: "source", - version: "0.0.7", + version: "2.0.5", dedupe: "unique", props: { ...common.props, diff --git a/components/mysql_ssl/actions/create-row/create-row.mjs b/components/mysql_ssl/actions/create-row/create-row.mjs deleted file mode 100644 index 2b80e61dec0d3..0000000000000 --- a/components/mysql_ssl/actions/create-row/create-row.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import createRow from "../../../mysql/actions/create-row/create-row.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...createRow, - ...utils.getAppProps(createRow), - key: "mysql_ssl-create-row", - description: "Adds a new row (SSL). [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/insert.html)", - version: "1.0.2", -}; diff --git a/components/mysql_ssl/actions/delete-row/delete-row.mjs b/components/mysql_ssl/actions/delete-row/delete-row.mjs deleted file mode 100644 index 61177f74d37ac..0000000000000 --- a/components/mysql_ssl/actions/delete-row/delete-row.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import deleteRow from "../../../mysql/actions/delete-row/delete-row.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...deleteRow, - ...utils.getAppProps(deleteRow), - key: "mysql_ssl-delete-row", - description: "Delete an existing row (SSL). [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/delete.html)", - version: "1.0.2", -}; diff --git a/components/mysql_ssl/actions/execute-query/execute-query.mjs b/components/mysql_ssl/actions/execute-query/execute-query.mjs deleted file mode 100644 index 25936178c0355..0000000000000 --- a/components/mysql_ssl/actions/execute-query/execute-query.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import executeQuery from "../../../mysql/actions/execute-query/execute-query.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...executeQuery, - ...utils.getAppProps(executeQuery), - key: "mysql_ssl-execute-query", - description: "Find row(s) via a custom query (SSL). [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/select.html)", - version: "1.0.2", -}; diff --git a/components/mysql_ssl/actions/execute-stored-procedure/execute-stored-procedure.mjs b/components/mysql_ssl/actions/execute-stored-procedure/execute-stored-procedure.mjs deleted file mode 100644 index 8048c8eee30f6..0000000000000 --- a/components/mysql_ssl/actions/execute-stored-procedure/execute-stored-procedure.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import executeStoredProcedure from "../../../mysql/actions/execute-stored-procedure/execute-stored-procedure.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...executeStoredProcedure, - ...utils.getAppProps(executeStoredProcedure), - key: "mysql_ssl-execute-stored-procedure", - description: "Execute Stored Procedure (SSL). [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/stored-programs-defining.html)", - version: "1.0.2", -}; diff --git a/components/mysql_ssl/actions/find-row/find-row.mjs b/components/mysql_ssl/actions/find-row/find-row.mjs deleted file mode 100644 index 8a07baf5c9665..0000000000000 --- a/components/mysql_ssl/actions/find-row/find-row.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import findRow from "../../../mysql/actions/find-row/find-row.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...findRow, - ...utils.getAppProps(findRow), - key: "mysql_ssl-find-row", - description: "Finds a row in a table via a lookup column (SSL). [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/select.html)", - version: "1.0.2", -}; diff --git a/components/mysql_ssl/actions/update-row/update-row.mjs b/components/mysql_ssl/actions/update-row/update-row.mjs deleted file mode 100644 index ae1b032aa560d..0000000000000 --- a/components/mysql_ssl/actions/update-row/update-row.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import updateRow from "../../../mysql/actions/update-row/update-row.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...updateRow, - ...utils.getAppProps(updateRow), - key: "mysql_ssl-update-row", - description: "Updates an existing row (SSL). [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/update.html)", - version: "1.0.2", -}; diff --git a/components/mysql_ssl/common/utils.mjs b/components/mysql_ssl/common/utils.mjs deleted file mode 100644 index 0b1aafcf3188e..0000000000000 --- a/components/mysql_ssl/common/utils.mjs +++ /dev/null @@ -1,55 +0,0 @@ -import mysqlSslApp from "../mysql_ssl.app.mjs"; - -function buildPropDefinitions({ - app = {}, props = {}, -}) { - return Object.entries(props) - .reduce((newProps, [ - key, - prop, - ]) => { - if (!prop.propDefinition) { - return { - ...newProps, - [key]: prop, - }; - } - - const [ - , ...propDefinitionItems - ] = prop.propDefinition; - - return { - ...newProps, - [key]: { - ...prop, - propDefinition: [ - app, - ...propDefinitionItems, - ], - }, - }; - }, {}); -} - -function getAppProps(component = {}) { - const { - // eslint-disable-next-line no-unused-vars - mysql, - ...otherProps - } = component.props; - return { - props: { - mysql: mysqlSslApp, - ...buildPropDefinitions({ - app: mysqlSslApp, - props: otherProps, - }), - }, - }; -} - -export default { - buildPropDefinitions, - getAppProps, -}; diff --git a/components/mysql_ssl/mysql_ssl.app.mjs b/components/mysql_ssl/mysql_ssl.app.mjs deleted file mode 100644 index 85c379679ca35..0000000000000 --- a/components/mysql_ssl/mysql_ssl.app.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import mysqlApp from "../mysql/mysql.app.mjs"; - -export default { - ...mysqlApp, - app: "mysql_ssl", - methods: { - ...mysqlApp.methods, - getConfig() { - const { - host, - port, - username, - password, - database, - ca, - key, - cert, - rejectUnauthorized, - } = this.$auth; - return { - host, - port, - user: username, - password, - database, - ssl: { - rejectUnauthorized: rejectUnauthorized === "true", - ca, - key, - cert, - }, - }; - }, - }, -}; diff --git a/components/mysql_ssl/package.json b/components/mysql_ssl/package.json deleted file mode 100644 index b5b17b27dce86..0000000000000 --- a/components/mysql_ssl/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@pipedream/mysql_ssl", - "version": "1.0.2", - "description": "Pipedream MySQL (SSL) Components", - "main": "mysql_ssl.app.mjs", - "keywords": [ - "pipedream", - "mysql_ssl" - ], - "homepage": "https://pipedream.com/apps/mysql_ssl", - "publishConfig": { - "access": "public" - }, - "dependencies": { - "@pipedream/platform": "^1.2.0", - "mysql2-promise": "^0.1.4", - "uuid": "^8.3.2" - } -} diff --git a/components/mysql_ssl/sources/new-column/new-column.mjs b/components/mysql_ssl/sources/new-column/new-column.mjs deleted file mode 100644 index 5f7f025f6da88..0000000000000 --- a/components/mysql_ssl/sources/new-column/new-column.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import newColumn from "../../../mysql/sources/new-column/new-column.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...newColumn, - ...utils.getAppProps(newColumn), - key: "mysql_ssl-new-column", - description: "Emit new event when you add a new column to a table (SSL). [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/show-columns.html)", - version: "1.0.2", -}; diff --git a/components/mysql_ssl/sources/new-or-updated-row/new-or-updated-row.mjs b/components/mysql_ssl/sources/new-or-updated-row/new-or-updated-row.mjs deleted file mode 100644 index 591abdd219c35..0000000000000 --- a/components/mysql_ssl/sources/new-or-updated-row/new-or-updated-row.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import newOrUpdatedRow from "../../../mysql/sources/new-or-updated-row/new-or-updated-row.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...newOrUpdatedRow, - ...utils.getAppProps(newOrUpdatedRow), - key: "mysql_ssl-new-or-updated-row", - description: "Emit new event when you add or modify a new row in a table (SSL). [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/select.html)", - version: "1.0.2", -}; diff --git a/components/mysql_ssl/sources/new-row-custom-query/new-row-custom-query.mjs b/components/mysql_ssl/sources/new-row-custom-query/new-row-custom-query.mjs deleted file mode 100644 index 49b8c54091fad..0000000000000 --- a/components/mysql_ssl/sources/new-row-custom-query/new-row-custom-query.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import newRowCustomQuery from "../../../mysql/sources/new-row-custom-query/new-row-custom-query.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...newRowCustomQuery, - ...utils.getAppProps(newRowCustomQuery), - key: "mysql_ssl-new-row-custom-query", - description: "Emit new event when new rows are returned from a custom query (SSL). [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/select.html)", - version: "1.0.2", -}; diff --git a/components/mysql_ssl/sources/new-row/new-row.mjs b/components/mysql_ssl/sources/new-row/new-row.mjs deleted file mode 100644 index 4ea60d369838e..0000000000000 --- a/components/mysql_ssl/sources/new-row/new-row.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import newRow from "../../../mysql/sources/new-row/new-row.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...newRow, - ...utils.getAppProps(newRow), - key: "mysql_ssl-new-row", - description: "Emit new event when you add a new row to a table (SSL). [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/select.html)", - version: "1.0.2", -}; diff --git a/components/mysql_ssl/sources/new-table/new-table.mjs b/components/mysql_ssl/sources/new-table/new-table.mjs deleted file mode 100644 index ef6da9056a0e6..0000000000000 --- a/components/mysql_ssl/sources/new-table/new-table.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import newTable from "../../../mysql/sources/new-table/new-table.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...newTable, - ...utils.getAppProps(newTable), - key: "mysql_ssl-new-table", - description: "Emit new event when a new table is added to a database (SSL). [See the docs here](https://dev.mysql.com/doc/refman/8.0/en/select.html)", - version: "1.0.2", -}; diff --git a/components/n8n_io/README.md b/components/n8n_io/README.md new file mode 100644 index 0000000000000..ec83740cbe684 --- /dev/null +++ b/components/n8n_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The n8n.io API provides a platform for automating workflows in a node-based structure that allows for integrations across various services and apps. Leveraging this API within Pipedream enables you to orchestrate complex operations, connect disparate systems, and trigger actions conditionally, transforming and passing data between services without writing extensive code. + +# Example Use Cases + +- **Automated Data Sync Between n8n and Google Sheets**: Use the n8n.io API to fetch data from a specific workflow run and sync it to a Google Sheet for easy monitoring and reporting. This can be triggered at regular intervals or upon specific workflow events. + +- **Conditional Alerting with n8n and Slack**: Set up a Pipedream workflow that calls the n8n.io API to monitor the status of workflows. If a workflow fails or produces an unexpected result, send an alert with details to a designated Slack channel, ensuring immediate team response. + +- **Dynamic Workflow Trigger Based on GitHub Events**: Create a Pipedream workflow that listens for GitHub events, such as a new push or pull request. Once an event is detected, use the n8n.io API to trigger specific n8n workflows that perform tasks like code linting or automated testing. diff --git a/components/namecheap/README.md b/components/namecheap/README.md new file mode 100644 index 0000000000000..eb0e0a776c1b0 --- /dev/null +++ b/components/namecheap/README.md @@ -0,0 +1,11 @@ +# Overview + +The Namecheap API provides a suite of functions to automate domain management tasks, enabling you to register, configure, and manage domains programmatically. With Pipedream's integration, you can create workflows that react to specific events or schedule domain-related operations, streamlining processes like domain registration, DNS management, and domain renewal reminders. By leveraging Pipedream's serverless platform, you can connect the Namecheap API with other services, set up cron jobs, and handle webhooks to create custom, automated workflows. + +# Example Use Cases + +- **Automated Domain Registration**: Automate the process of searching for available domains and registering them through Namecheap as soon as they become available. Combine this with a Slack alert to notify your team in real-time when new domains are registered. + +- **DNS Management Automation**: Simplify DNS updates by creating a workflow that listens for webhooks from your application or another service, then automatically applies those changes to your Namecheap DNS records, ensuring your domain's DNS settings are always in sync with your current infrastructure. + +- **Domain Renewal Reminder System**: Set up a scheduled workflow to check the expiry dates of your domains and send reminders via email or SMS using Twilio. This way, you never miss out on renewing important domains and can keep your online presence uninterrupted. diff --git a/components/namecheap/namecheap.app.mjs b/components/namecheap/namecheap.app.mjs new file mode 100644 index 0000000000000..972bba57736a5 --- /dev/null +++ b/components/namecheap/namecheap.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "namecheap", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/namecheap/package.json b/components/namecheap/package.json new file mode 100644 index 0000000000000..ec88346daf9d6 --- /dev/null +++ b/components/namecheap/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/namecheap", + "version": "0.0.1", + "description": "Pipedream Namecheap Components", + "main": "namecheap.app.mjs", + "keywords": [ + "pipedream", + "namecheap" + ], + "homepage": "https://pipedream.com/apps/namecheap", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/namely/README.md b/components/namely/README.md index 9ef77e687775c..55adb8d615c8a 100644 --- a/components/namely/README.md +++ b/components/namely/README.md @@ -1,18 +1,11 @@ # Overview -The Namely API provides a set of tools that you can use to build applications -that interact with Namely data. With the Namely API, you can: +Namely is a Human Resources Information System (HRIS) designed to simplify HR, payroll, benefits, and talent management. By leveraging the Namely API on Pipedream, you can automate employee data synchronization across various platforms, streamline HR operations by triggering workflows based on employee lifecycle events, and even analyze workforce data to gain insights. It's a potent tool for HR professionals looking to integrate their HR tech stack and automate repetitive tasks. -- Retrieve information about Namely accounts, users, and jobs -- Create and update Namely data -- Search for Namely data -- Authenticate Namely users +# Example Use Cases -Here are some examples of what you can build with the Namely API: +- **Employee Onboarding Automations**: When a new employee record is created in Namely, trigger a Pipedream workflow to set up accounts in tools like Slack, G Suite, and GitHub. This flow could also assign tasks in project management software like Trello or Asana, ensuring new hires have a structured onboarding process. -- A web application that allows Namely users to view and update their account - information -- A mobile application that allows Namely users to search for jobs and view job - descriptions -- An application that allows Namely administrators to manage account data and - user permissions +- **Payroll Change Notifications**: Monitor changes in employee payroll details on Namely and use Pipedream to notify finance and the affected employee via email through SendGrid or Slack. This workflow ensures transparency and quick updates on critical payroll adjustments. + +- **Time-off Sync to Calendar**: Sync employee time-off requests from Namely to a company-wide calendar like Google Calendar or Outlook. When a time-off request is approved, Pipedream automatically creates an event on the calendar, keeping everyone informed and aiding in resource planning. diff --git a/components/nango/README.md b/components/nango/README.md new file mode 100644 index 0000000000000..83af990442618 --- /dev/null +++ b/components/nango/README.md @@ -0,0 +1,11 @@ +# Overview + +The Nango API is a bridge for integrating with SaaS apps, smoothing out authentication and API call complexities. On Pipedream, you can harness Nango to automate interactions between various services without getting bogged down by the nuances of OAuth flows or API intricacies. Use Nango to authenticate once, and let Pipedream workflows handle the rest, executing tasks, syncing data, and triggering actions across apps efficiently. + +# Example Use Cases + +- **Automate Social Media Postings**: Automate the scheduling of social media posts across platforms. When a new blog post goes live, use Nango to authenticate and post updates to Twitter, LinkedIn, and Facebook via their respective APIs. + +- **Sync CRM and Support Tickets**: Keep your CRM up-to-date by syncing new support tickets from a customer service platform like Zendesk. As new tickets arrive, Nango can handle the authentication to add or update the corresponding customer records in a CRM like Salesforce. + +- **Manage Email Marketing Campaigns**: Coordinate an email marketing campaign across multiple platforms. When a new subscriber is added to a platform like Mailchimp, use Nango to authenticate and replicate the subscriber data to other marketing tools such as SendGrid or ConvertKit for broader reach. diff --git a/components/nano_nets/README.md b/components/nano_nets/README.md new file mode 100644 index 0000000000000..0b763afd8be35 --- /dev/null +++ b/components/nano_nets/README.md @@ -0,0 +1,11 @@ +# Overview + +The Nano Nets API offers machine learning capabilities to classify images, extract data, and automate processes with custom models. Through Pipedream's serverless platform, you can trigger workflows from various events, manipulate and route data from the Nano Nets API, and connect it to hundreds of other apps to automate complex tasks. Pipedream's built-in code steps also allow you to transform data, make HTTP requests, and handle logic right inside your workflows. + +# Example Use Cases + +- **Automated Content Moderation**: Use the Nano Nets API to analyze user-uploaded images on your platform. Set up a workflow on Pipedream that triggers when a new image is uploaded, sends it to Nano Nets for analysis, and if the content is flagged as inappropriate, automatically remove the image and notify admins via Slack. + +- **Intelligent Invoice Processing**: Create a Pipedream workflow that triggers when new invoice images are added to a Dropbox folder. Use the Nano Nets API to extract structured data from these invoices, and then store the data in a Google Sheets spreadsheet for easy tracking and analysis. + +- **Real-time Inventory Management**: Implement a workflow that starts when an inventory count is done via image capture. The images are processed by the Nano Nets API to identify and count items, with the results sent to an Airtable base to update inventory levels, and a notification sent through email using SendGrid if stock for any item is low. diff --git a/components/nasa/README.md b/components/nasa/README.md new file mode 100644 index 0000000000000..1eb1a75f4319f --- /dev/null +++ b/components/nasa/README.md @@ -0,0 +1,11 @@ +# Overview + +The NASA API provides a wealth of data related to space, including imagery, Mars weather reports, and information about asteroids near Earth. With Pipedream, you can harness this data to create automated workflows. Whether you're sending daily space photos as a Slack message, saving Mars weather reports to a spreadsheet, or triggering alerts when new asteroids approach, Pipedream serves as a bridge between NASA's data and your applications. + +# Example Use Cases + +- **Daily Astronomy Picture to Slack**: Fetch the Astronomy Picture of the Day from the NASA API and send it to a Slack channel, including the image, title, and explanation. You can schedule this as a daily inspirational or educational snippet for your team. + +- **Mars Weather Report to Google Sheets**: Gather the latest weather report from the Mars InSight lander using the NASA API. Then, format the data and append it to a Google Sheets document for ongoing analysis or public sharing. This is a perfect way to keep track of Mars weather trends over time. + +- **Asteroid Watch Twitter Bot**: Use the NASA API to check for near-Earth objects, and then tweet interesting findings from a dedicated Twitter account. You can add filters to only tweet about asteroids of certain sizes or those that come exceptionally close to Earth, keeping your followers informed and engaged. diff --git a/components/nasa/nasa.app.mjs b/components/nasa/nasa.app.mjs new file mode 100644 index 0000000000000..f851bd4a7ddf8 --- /dev/null +++ b/components/nasa/nasa.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "nasa", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/nasa/package.json b/components/nasa/package.json new file mode 100644 index 0000000000000..00ad9215350d6 --- /dev/null +++ b/components/nasa/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/nasa", + "version": "0.0.1", + "description": "Pipedream Nasa Components", + "main": "nasa.app.mjs", + "keywords": [ + "pipedream", + "nasa" + ], + "homepage": "https://pipedream.com/apps/nasa", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/nasdaq_data_link_time_series_and_table_data_/README.md b/components/nasdaq_data_link_time_series_and_table_data_/README.md new file mode 100644 index 0000000000000..379a07941e636 --- /dev/null +++ b/components/nasdaq_data_link_time_series_and_table_data_/README.md @@ -0,0 +1,11 @@ +# Overview + +The Nasdaq Data Link API provides access to financial, economic, and alternative data that powers investment decisions, research, and more. Within Pipedream, you can use this API to automate workflows involving time series and table data. This might include fetching stock prices, economic indicators, or data for quantitative analysis. By creating workflows that trigger on schedules or events, you can efficiently process and act upon this data. Combine it with other apps to gain insights, notify stakeholders, or integrate with your databases. + +# Example Use Cases + +- **Automate Financial Reporting**: Create a workflow that triggers weekly to fetch the latest stock prices and compile a report. This could then be sent via email, Slack, or stored in a Google Sheet for easy access and sharing among team members. + +- **Real-time Investment Alerts**: Set up a workflow that checks for specific criteria in the market data, such as sudden drops or spikes in a stock's price. When detected, it can trigger alerts through SMS or messaging apps to inform a rapid investment decision. + +- **Data Analysis and Visualization**: Compile time series data for multiple stocks, then process it within Pipedream to calculate statistical metrics. Push this information to data visualization tools like Tableau or Google Data Studio for real-time dashboard updates. diff --git a/components/nationbuilder/README.md b/components/nationbuilder/README.md new file mode 100644 index 0000000000000..7d8cc17eab5ce --- /dev/null +++ b/components/nationbuilder/README.md @@ -0,0 +1,11 @@ +# Overview + +NationBuilder is a comprehensive platform for community organizing, offering tools for website creation, donor management, and communication with the community. The NationBuilder API unlocks potent opportunities for automation and data management. On Pipedream, you can leverage this API to streamline interactions between NationBuilder and other services, automate contacts synchronization, trigger communications based on user activities, and more, all while enjoying the benefits of serverless workflows. + +# Example Use Cases + +- **Automate Contact Sync Between NationBuilder and a CRM**: Sync new contacts from a CRM like Salesforce to NationBuilder or vice versa. When a contact is added or updated in Salesforce, the workflow can be triggered, automatically updating or creating a matching profile in NationBuilder. + +- **Send Custom Email Campaigns Based on User Actions**: Monitor user activities on NationBuilder, such as donations or signups, and use this data to trigger personalized email campaigns in an email service like SendGrid. For example, thank supporters who've recently donated or send follow-up information to new community members. + +- **Create Real-Time Alerts for Key Activities**: Set up a workflow that sends real-time alerts via Slack whenever key activities occur in NationBuilder, like significant donations or new membership signups. This keeps your team immediately informed and ready to act on new opportunities or show timely appreciation. diff --git a/components/navigatr/navigatr.app.mjs b/components/navigatr/navigatr.app.mjs new file mode 100644 index 0000000000000..ed8cb8e9e2815 --- /dev/null +++ b/components/navigatr/navigatr.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "navigatr", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/navigatr/package.json b/components/navigatr/package.json new file mode 100644 index 0000000000000..531e4bc412fd6 --- /dev/null +++ b/components/navigatr/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/navigatr", + "version": "0.0.1", + "description": "Pipedream Navigatr Components", + "main": "navigatr.app.mjs", + "keywords": [ + "pipedream", + "navigatr" + ], + "homepage": "https://pipedream.com/apps/navigatr", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/ncscale/README.md b/components/ncscale/README.md new file mode 100644 index 0000000000000..811584ea9413c --- /dev/null +++ b/components/ncscale/README.md @@ -0,0 +1,11 @@ +# Overview + +The ncScale API offers scalable solutions for handling complex data transformations, computations, and integrations. By leveraging this API on Pipedream, you can automate and streamline processes like real-time data analysis, dynamic resource allocation, and system monitoring. Pipedream's serverless platform allows you to trigger workflows with HTTP requests, emails, and over 800+ integrations, making it highly versatile for incorporating ncScale's capabilities into a variety of systems. + +# Example Use Cases + +- **Real-time Data Processing**: Connect the ncScale API to a stream of IoT sensor data. Whenever Pipedream receives data from the sensors, it can trigger a workflow that passes this information to ncScale for immediate processing. The processed data can then be sent to a database like PostgreSQL for storage or analysis. + +- **Dynamic Resource Scaling**: Use Pipedream to monitor application metrics from a service like AWS CloudWatch. When a metric crosses a certain threshold, trigger a workflow that calls the ncScale API to adjust the resources allocated to your application, ensuring optimal performance without manual intervention. + +- **Automated Reporting**: Set up a Pipedream workflow that collects data from various sources, such as Google Analytics and Salesforce. This data can be fed into the ncScale API for aggregation and analysis. The results can then be formatted into a report and sent automatically to stakeholders via email or Slack at regular intervals. diff --git a/components/nectar_crm/README.md b/components/nectar_crm/README.md index 1be06d19da41f..ee1871211dba1 100644 --- a/components/nectar_crm/README.md +++ b/components/nectar_crm/README.md @@ -1,14 +1,11 @@ # Overview -The Nectar CRM API can be used to build a wide variety of applications, -including: - -- Customer Relationship Management (CRM) applications -- Sales and marketing applications -- Customer service and support applications -- E-commerce applications -- Accounting and financial applications -- Human resources applications -- Project management applications -- Inventory management applications -- And much more! +The Nectar CRM API enables users to streamline customer relationship management by automating tasks and syncing data across various platforms. With this API on Pipedream, you can perform actions like updating contact details, managing deals, tracking customer interactions, and generating reports. These capabilities can be leveraged to create custom, automated workflows that save time and keep your sales processes efficient. + +# Example Use Cases + +- **Lead Score Update to Email Marketing Platform**: Automatically update the lead score in your email marketing tool when it changes in Nectar CRM. For example, when a contact's engagement increases, bump their lead score in SendGrid, triggering a tailored email campaign to nurture the hot lead. + +- **Support Ticket Creation from Deal Feedback**: Create a support ticket in a tool like Zendesk whenever negative feedback is received for a deal in Nectar CRM. This ensures prompt follow-up to improve customer satisfaction and service recovery. + +- **Sync New Contacts to Google Sheets**: When a new contact is added to Nectar CRM, seamlessly add their details to a Google Sheet. This can be used for additional data analysis or backup, keeping all teams up-to-date with the latest contact information. diff --git a/components/neetoinvoice/README.md b/components/neetoinvoice/README.md new file mode 100644 index 0000000000000..83a4c6ddd99a9 --- /dev/null +++ b/components/neetoinvoice/README.md @@ -0,0 +1,11 @@ +# Overview + +The neetoinvoice API facilitates the creation and management of invoices, allowing users to seamlessly generate, send, and track invoices directly through their API. Integrating neetoinvoice with Pipedream opens up a realm of possibilities for automating your invoice-related workflows. With Pipedream, you can connect neetoinvoice to a multitude of apps to create custom, serverless workflows that can handle events like invoice creation, payment updates, and sending reminders, all managed with a user-friendly interface. + +# Example Use Cases + +- **Create Invoices on New Orders**: Automate the creation of invoices in neetoinvoice when new orders are placed on an e-commerce platform like Shopify. When Pipedream detects a new order event, it can trigger a workflow that generates a corresponding invoice through neetoinvoice's API. + +- **Update CRM on Invoice Payment**: Keep your CRM, such as Salesforce, up-to-date by pushing payment confirmation details whenever an invoice is paid. Set up a Pipedream workflow that listens for the payment confirmation webhook from neetoinvoice and then updates the relevant contact or account record in Salesforce. + +- **Send Payment Reminders via Email or SMS**: Implement a workflow that periodically checks for overdue invoices using neetoinvoice API and then uses SendGrid or Twilio to send out payment reminders. With Pipedream's scheduled tasks, you can automate this process to run at regular intervals, ensuring timely reminders are sent without manual intervention. diff --git a/components/neetokb/README.md b/components/neetokb/README.md new file mode 100644 index 0000000000000..c0c33db10e7ec --- /dev/null +++ b/components/neetokb/README.md @@ -0,0 +1,11 @@ +# Overview + +The neetoKB API enables integration with neetoKB, a knowledge base management service. It allows you to automate the creation, retrieval, updating, and deletion of articles, categories, and sections within your knowledge base. On Pipedream, you can leverage this API to create workflows that streamline content management, collaborate with team members, and synchronize data across multiple platforms. Pipedream's serverless platform also offers real-time processing, event-driven triggers, and connections to hundreds of other apps for extended functionality. + +# Example Use Cases + +- **Automated Content Updates**: When a Google Doc is updated, use Pipedream to trigger a workflow that updates an associated article in neetoKB. This ensures that your knowledge base remains up-to-date with the most recent document changes. + +- **Customer Support Ticket Resolution**: Integrate neetoKB with a customer support platform like Zendesk. Whenever a new support ticket is resolved, automatically create or update an article in neetoKB with the solution details, enriching your knowledge base for future reference. + +- **Content Approval Workflow**: Set up a workflow that triggers when a draft article is ready for review in neetoKB. Notify team members on Slack to review the content. After approval, the workflow can publish the article to the knowledge base and notify subscribers via email. diff --git a/components/neetokb/package.json b/components/neetokb/package.json index 4f32b697bd870..3b7e0e8adb967 100644 --- a/components/neetokb/package.json +++ b/components/neetokb/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/neon_api_keys/README.md b/components/neon_api_keys/README.md new file mode 100644 index 0000000000000..5d244b2684110 --- /dev/null +++ b/components/neon_api_keys/README.md @@ -0,0 +1,11 @@ +# Overview + +The Neon API provides powerful interaction with Neon's infrastructure, which includes secure handling of API keys and other sensitive data. On Pipedream, you can leverage these capabilities to automate workflows around key management, data security, and infrastructure scaling. By connecting the Neon API with other services and apps, you can create automatic processes for issuing, rotating, and monitoring API keys, ensuring your applications maintain robust security and compliance without manual oversight. + +# Example Use Cases + +- **Automated API Key Rotation**: Generate new API keys and deactivate old ones periodically to maintain security. Use Pipedream's built-in cron job feature to schedule these rotations, and automatically update the keys in other services like AWS or Stripe by integrating with their respective APIs on Pipedream. + +- **Usage Monitoring and Alerting**: Keep track of how and when your API keys are used by setting up a workflow that logs key usage. Connect to a logging service or database on Pipedream to store this information and set alerts for any unusual activity, such as unexpected spikes in usage or access from unrecognized locations. + +- **Provisioning Keys for New Services**: When setting up a new service that requires an API key, automate the process of generating and provisioning the necessary keys. With Pipedream, you can integrate the Neon API with configuration management tools like Ansible or Terraform to streamline the deployment and setup of new services. diff --git a/components/nerv/nerv.app.mjs b/components/nerv/nerv.app.mjs new file mode 100644 index 0000000000000..029b58080db69 --- /dev/null +++ b/components/nerv/nerv.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "nerv", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/nerv/package.json b/components/nerv/package.json new file mode 100644 index 0000000000000..a9807993a3af3 --- /dev/null +++ b/components/nerv/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/nerv", + "version": "0.0.1", + "description": "Pipedream Nerv Components", + "main": "nerv.app.mjs", + "keywords": [ + "pipedream", + "nerv" + ], + "homepage": "https://pipedream.com/apps/nerv", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/netatmo/README.md b/components/netatmo/README.md new file mode 100644 index 0000000000000..3b3940d2dec5d --- /dev/null +++ b/components/netatmo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Netatmo API gives you access to data from your Netatmo weather station and connected devices, letting you track weather and air quality, monitor home security, and automate smart home reactions. Using Pipedream, these capabilities open up a world of possibilities for creating workflows that harness real-time environmental data, trigger actions based on weather events, and integrate with a myriad of other services available on Pipedream’s platform. + +# Example Use Cases + +- **Weather-Triggered Notifications**: Use the Netatmo API to monitor weather conditions and set up a Pipedream workflow that sends alerts via email or SMS when specific weather criteria are met, such as high temperature or heavy rain. + +- **Air Quality-Based Device Control**: Create a workflow that uses real-time air quality data from Netatmo to control smart home devices connected to Pipedream, like smart plugs or air purifiers, turning them on or off based on the pollution level. + +- **Home Security Automation**: Combine Netatmo security device data with Pipedream workflows to automate responses to events detected by Netatmo cameras or sensors. For instance, turn on lights using smart home integrations or send a notification to a Slack channel when unexpected motion is detected. diff --git a/components/netatmo/package.json b/components/netatmo/package.json index 6747443aa01f9..da95f9ed4669a 100644 --- a/components/netatmo/package.json +++ b/components/netatmo/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/nethunt_crm/README.md b/components/nethunt_crm/README.md index bd15f3ca25113..8ce545a2ffddc 100644 --- a/components/nethunt_crm/README.md +++ b/components/nethunt_crm/README.md @@ -1,19 +1,11 @@ # Overview -NetHunt CRM API is a powerful tool that lets you build custom applications on -top of the NetHunt CRM platform. With the API, you can access data stored in -NetHunt CRM, including contacts, tasks, notes, deals, and more. You can also -use the API to create custom applications that interact with NetHunt CRM data. +The NetHunt CRM API provides powerful access to manage customer relationship data programmatically. With this API and Pipedream, you can automate tasks, synchronize customer data across platforms, trigger communications based on customer interactions, and much more. Pipedream's serverless execution model and vast library of app integrations open up a myriad of possibilities for streamlining sales processes and enhancing customer engagement by automating the flow of information between NetHunt CRM and other business tools. -The NetHunt CRM API is a REST API, so it can be used with any programming -language that supports making HTTP requests. In addition, the NetHunt CRM API -is fully documented, so you can easily find the information you need to get -started. +# Example Use Cases -Here are some examples of what you can build with the NetHunt CRM API: +- **Lead Qualification Automation**: Automatically qualify and score leads in NetHunt CRM based on data from web forms submitted through Typeform. When a prospect fills out a contact form, the workflow can enrich the lead data with additional information, score the lead based on predefined criteria, and update the lead status in NetHunt CRM for immediate follow-up by the sales team. -- A custom contact manager that displays data from NetHunt CRM in a custom way -- A task manager that lets you view and manage tasks from NetHunt CRM -- A deal tracker that lets you see which deals are close to being won -- A custom report that displays data from NetHunt CRM in a custom way -- A custom application that interacts with NetHunt CRM data in some way +- **Customer Support Ticket Creation**: Create a support ticket in Zendesk whenever a new conversation is started by a customer in NetHunt CRM. This workflow can also include fetching relevant customer data from NetHunt CRM to provide context within the Zendesk ticket, ensuring that support agents have all the information they need to assist the customer efficiently. + +- **Automated Data Sync Between CRM and Newsletter Service**: Sync contact updates from NetHunt CRM to a Mailchimp list, ensuring that email marketing campaigns are always targeting the most current customer information. This workflow can be designed to trigger whenever a contact is updated or added in NetHunt CRM, automatically updating subscriber details or adding new subscribers to designated Mailchimp audience lists. diff --git a/components/netlify/README.md b/components/netlify/README.md index e3d27b5f413b7..43a460c6f2f86 100644 --- a/components/netlify/README.md +++ b/components/netlify/README.md @@ -1,10 +1,14 @@ # Overview -With the Netlify API, you can build a number of things, including: - -- A platform for hosting static websites -- A Continuous Deployment platform for your web applications -- A serverless functions platform -- A headless CMS -- A static site generator -- A image processing service +Harness the power of the Netlify API on Pipedream to automate your web development workflows, streamline site deployments, manage DNS settings, and more. With Pipedream's serverless platform, you can orchestrate Netlify's capabilities in concert with numerous other services to enhance productivity, monitor your deployments, and react to events in real-time. Create custom CI/CD pipelines, synchronize your site's content with third-party systems, or automate responses to form submissions, all with the seamless integration of the Netlify API within Pipedream workflows. + +# Example Use Cases + +- **Automated Deployment After Content Update** + Listen for updates in a CMS like Contentful or WordPress, and trigger a Netlify build to redeploy your site whenever new content is published or updated. + +- **Dynamic DNS Management** + Combine Netlify with DNS services like Cloudflare or Google Cloud DNS in Pipedream. Automatically update DNS records when deploying new features, or manage subdomains for multi-tenant applications dynamically. + +- **Form Submission Handling** + Capture form submissions on your Netlify-hosted site and process them in Pipedream. Integrate with email services like SendGrid to send auto-responses, or with Slack to notify your team about new entries. diff --git a/components/neuronwriter/actions/create-new-query/create-new-query.mjs b/components/neuronwriter/actions/create-new-query/create-new-query.mjs new file mode 100644 index 0000000000000..1cb8c475864c7 --- /dev/null +++ b/components/neuronwriter/actions/create-new-query/create-new-query.mjs @@ -0,0 +1,46 @@ +import { + ENGINE_OPTIONS, LANGUAGE_OPTIONS, +} from "../../common/constants.mjs"; +import neuronwriter from "../../neuronwriter.app.mjs"; + +export default { + key: "neuronwriter-create-new-query", + name: "Create New Query", + description: "Launches a new query based on provided keyword, search engine, and language. [See the documentation](https://contadu.crisp.help/en/article/neuronwriter-api-how-to-use-2ds6hx/#3-new-query)", + version: "0.0.1", + type: "action", + props: { + neuronwriter, + keyword: { + type: "string", + label: "Keyword", + description: "The keyword to generate a query and recommendations for.", + }, + searchEngine: { + type: "string", + label: "Search Engine", + description: "Preferred search engine.", + options: ENGINE_OPTIONS, + }, + language: { + type: "string", + label: "Language", + description: "Content language.", + options: LANGUAGE_OPTIONS, + }, + }, + async run({ $ }) { + const response = await this.neuronwriter.createNewQuery({ + $, + data: { + project: this.neuronwriter.$auth.project_id, + keyword: this.keyword, + engine: this.searchEngine, + language: this.language, + }, + }); + + $.export("$summary", `Successfully created new query with Id: ${response.query}`); + return response; + }, +}; diff --git a/components/neuronwriter/actions/get-content-saved/get-content-saved.mjs b/components/neuronwriter/actions/get-content-saved/get-content-saved.mjs new file mode 100644 index 0000000000000..10c0635566262 --- /dev/null +++ b/components/neuronwriter/actions/get-content-saved/get-content-saved.mjs @@ -0,0 +1,28 @@ +import neuronwriter from "../../neuronwriter.app.mjs"; + +export default { + key: "neuronwriter-get-content-saved", + name: "Get Content Saved", + description: "Pulls the most recent revision of the content saved for a specific query. [See the documentation](https://contadu.crisp.help/en/article/neuronwriter-api-how-to-use-2ds6hx/#3-get-content)", + version: "0.0.1", + type: "action", + props: { + neuronwriter, + queryId: { + propDefinition: [ + neuronwriter, + "queryId", + ], + }, + }, + async run({ $ }) { + const response = await this.neuronwriter.getContent({ + $, + data: { + query: this.queryId, + }, + }); + $.export("$summary", `Successfully retrieved the content for query ID ${this.queryId}`); + return response; + }, +}; diff --git a/components/neuronwriter/actions/get-query-details/get-query-details.mjs b/components/neuronwriter/actions/get-query-details/get-query-details.mjs new file mode 100644 index 0000000000000..173f0f7916cf9 --- /dev/null +++ b/components/neuronwriter/actions/get-query-details/get-query-details.mjs @@ -0,0 +1,33 @@ +import neuronwriter from "../../neuronwriter.app.mjs"; + +export default { + key: "neuronwriter-get-query-details", + name: "Get Query Details", + description: "Fetches the data related to a pre-defined query. [See the documentation](https://contadu.crisp.help/en/article/neuronwriter-api-how-to-use-2ds6hx/#3-get-query)", + version: "0.0.1", + type: "action", + props: { + neuronwriter, + queryId: { + propDefinition: [ + neuronwriter, + "queryId", + ], + }, + }, + async run({ $ }) { + const response = await this.neuronwriter.getQueryResults({ + data: { + query: this.queryId, + }, + }); + + let summary = `Successfully fetched query details for Query ID: ${this.queryId}`; + if (response.status != "ready") { + summary = `Query is not ready. Current status: ${response.status}`; + } + + $.export("$summary", summary); + return response; + }, +}; diff --git a/components/neuronwriter/common/constants.mjs b/components/neuronwriter/common/constants.mjs new file mode 100644 index 0000000000000..8b666876bedb6 --- /dev/null +++ b/components/neuronwriter/common/constants.mjs @@ -0,0 +1,935 @@ +export const LANGUAGE_OPTIONS = [ + "Abkhazian", + "Afar", + "Afrikaans", + "Akan", + "Albanian", + "Amharic", + "Arabic", + "Aragonese", + "Armenian", + "Assamese", + "Avaric", + "Aymara", + "Azerbaijani", + "Bambara", + "Bashkir", + "Basque", + "Belarusian", + "Bengali", + "Bihari", + "Bislama", + "Bosnian", + "Breton", + "Bulgarian", + "Burmese", + "Catalan", + "Chamorro", + "Chechen", + "Chinese", + "Chuvash", + "Corsican", + "Cree", + "Croatian", + "Czech", + "Danish", + "Dhivehi", + "Dutch", + "Dzongkha", + "English", + "Esperanto", + "Estonian", + "Ewe", + "Faroese", + "Fijian", + "Finnish", + "French", + "Fulah", + "Galician", + "Ganda", + "Georgian", + "German", + "Greek", + "Guarani", + "Gujarati", + "Haitian", + "Hausa", + "Hebrew", + "Herero", + "Hindi", + "Hiri Motu", + "Hungarian", + "Icelandic", + "Igbo", + "Indonesian", + "Inuktitut", + "Inupiaq", + "Irish", + "Italian", + "Japanese", + "Javanese", + "Kalaallisut", + "Kannada", + "Kanuri", + "Kashmiri", + "Kazakh", + "Khmer", + "Kikuyu", + "Kinyarwanda", + "Kirghiz", + "Komi", + "Kongo", + "Korean", + "Kuanyama", + "Kurdish", + "Lao", + "Latvian", + "Limburgan", + "Lingala", + "Lithuanian", + "Luba-Katanga", + "Luxembourgish", + "Macedonian", + "Malagasy", + "Malay", + "Malayalam", + "Maltese", + "Maori", + "Marathi", + "Marshallese", + "Moldavian", + "Mongolian", + "Nauru", + "Navajo", + "Ndonga", + "Nepali", + "North Ndebele", + "Northern Sami", + "Norwegian", + "Nyanja", + "Occitan", + "Ojibwa", + "Oriya", + "Oromo", + "Ossetian", + "Panjabi", + "Persian", + "Polish", + "Portuguese (Brazil)", + "Portuguese", + "Pushto", + "Quechua", + "Romanian", + "Romansh", + "Rundi", + "Russian", + "Samoan", + "Sango", + "Sanskrit", + "Sardinian", + "Gaelic", + "Serbian", + "Shona", + "Sichuan Yi", + "Sindhi", + "Sinhala", + "Slovak", + "Slovenian", + "Somali", + "South Ndebele", + "Southern Sotho", + "Spanish", + "Sundanese", + "Swahili", + "Swati", + "Swedish", + "Tagalog", + "Tahitian", + "Tajik", + "Tamil", + "Tatar", + "Telugu", + "Thai", + "Tibetan", + "Tigrinya", + "Tonga", + "Tsonga", + "Tswana", + "Turkish", + "Turkmen", + "Twi", + "Uighur", + "Ukrainian", + "Urdu", + "Uzbek", + "Venda", + "Vietnamese", + "Walloon", + "Welsh", + "Western Frisian", + "Wolof", + "Xhosa", + "Yiddish", + "Yoruba", + "Zhuang", + "Zulu", +]; + +export const ENGINE_OPTIONS = [ + { + label: "ALBANIA | google.AL", + value: "google.al", + }, + { + label: "ANDORRA | google.AD", + value: "google.ad", + }, + { + label: "AUSTRIA | google.AT", + value: "google.at", + }, + { + label: "BELARUS | google.BY", + value: "google.by", + }, + { + label: "BELGIUM | google.BE", + value: "google.be", + }, + { + label: "BOSNIA AND HERZEGOVINA | google.BA", + value: "google.ba", + }, + { + label: "BULGARIA | google.BG", + value: "google.bg", + }, + { + label: "CROATIA | google.HR", + value: "google.hr", + }, + { + label: "CZECH REPUBLIC | google.CZ", + value: "google.cz", + }, + { + label: "DENMARK | google.DK", + value: "google.dk", + }, + { + label: "ESTONIA | google.EE", + value: "google.ee", + }, + { + label: "FINLAND | google.FI", + value: "google.fi", + }, + { + label: "FRANCE | google.FR", + value: "google.fr", + }, + { + label: "GERMANY | google.DE", + value: "google.de", + }, + { + label: "GIBRALTAR | google.com.GI", + value: "google.com.gi", + }, + { + label: "GREECE | google.GR", + value: "google.gr", + }, + { + label: "GUERNSEY | google.GG", + value: "google.gg", + }, + { + label: "HUNGARY | google.HU", + value: "google.hu", + }, + { + label: "ICELAND | google.IS", + value: "google.is", + }, + { + label: "IRELAND | google.IE", + value: "google.ie", + }, + { + label: "ISLE OF MAN | google.IM", + value: "google.im", + }, + { + label: "ITALY | google.IT", + value: "google.it", + }, + { + label: "JERSEY | google.JE", + value: "google.je", + }, + { + label: "LATVIA | google.LV", + value: "google.lv", + }, + { + label: "LIECHTENSTEIN | google.LI", + value: "google.li", + }, + { + label: "LITHUANIA | google.LT", + value: "google.lt", + }, + { + label: "LUXEMBOURG | google.LU", + value: "google.lu", + }, + { + label: "MACEDONIA | google.MK", + value: "google.mk", + }, + { + label: "MALTA | google.com.MT", + value: "google.com.mt", + }, + { + label: "MOLDOVA | google.MD", + value: "google.md", + }, + { + label: "MONTENEGRO | google.ME", + value: "google.me", + }, + { + label: "NETHERLANDS | google.NL", + value: "google.nl", + }, + { + label: "NORWAY | google.NO", + value: "google.no", + }, + { + label: "POLAND | google.PL", + value: "google.pl", + }, + { + label: "PORTUGAL | google.PT", + value: "google.pt", + }, + { + label: "ROMANIA | google.RO", + value: "google.ro", + }, + { + label: "RUSSIA | google.RU", + value: "google.ru", + }, + { + label: "SAN MARINO | google.SM", + value: "google.sm", + }, + { + label: "SERBIA | google.RS", + value: "google.rs", + }, + { + label: "SLOVAKIA | google.SK", + value: "google.sk", + }, + { + label: "SLOVENIA | google.SI", + value: "google.si", + }, + { + label: "SPAIN | google.ES", + value: "google.es", + }, + { + label: "SWEDEN | google.SE", + value: "google.se", + }, + { + label: "SWITZERLAND | google.CH", + value: "google.ch", + }, + { + label: "TURKEY | google.com.TR", + value: "google.com.tr", + }, + { + label: "UKRAINE | google.com.UA", + value: "google.com.ua", + }, + { + label: "UNITED KINGDOM | google.co.UK", + value: "google.co.uk", + }, + { + label: "ANTIGUA AND BARBUDA | google.com.AG", + value: "google.com.ag", + }, + { + label: "BAHAMAS | google.BS", + value: "google.bs", + }, + { + label: "BELIZE | google.com.BZ", + value: "google.com.bz", + }, + { + label: "BRITISH VIRGIN ISLANDS | google.VG", + value: "google.vg", + }, + { + label: "CANADA | google.CA", + value: "google.ca", + }, + { + label: "COSTA RICA | google.co.CR", + value: "google.co.cr", + }, + { + label: "CUBA | google.com.CU", + value: "google.com.cu", + }, + { + label: "DOMINICA | google.DM", + value: "google.dm", + }, + { + label: "DOMINICAN REPUBLIC | google.com.DO", + value: "google.com.do", + }, + { + label: "EL SALVADOR | google.com.SV", + value: "google.com.sv", + }, + { + label: "GREENLAND | google.GL", + value: "google.gl", + }, + { + label: "GUATEMALA | google.com.GT", + value: "google.com.gt", + }, + { + label: "HAITI | google.HT", + value: "google.ht", + }, + { + label: "HONDURAS | google.HN", + value: "google.hn", + }, + { + label: "JAMAICA | google.com.JM", + value: "google.com.jm", + }, + { + label: "MEXICO | google.com.MX", + value: "google.com.mx", + }, + { + label: "NICARAGUA | google.com.NI", + value: "google.com.ni", + }, + { + label: "PANAMA | google.com.PA", + value: "google.com.pa", + }, + { + label: "PUERTO RICO | google.com.PR", + value: "google.com.pr", + }, + { + label: "SAINT VINCENT AND THE GRENADINES | google.com.VC", + value: "google.com.vc", + }, + { + label: "TRINIDAD AND TOBAGO | google.TT", + value: "google.tt", + }, + { + label: "UNITED STATES (USA) | google.COM", + value: "google.com", + }, + { + label: "VIRGIN ISLANDS | google.co.VI", + value: "google.co.vi", + }, + { + label: "ARGENTINA | google.com.AR", + value: "google.com.ar", + }, + { + label: "BOLIVIA | google.com.BO", + value: "google.com.bo", + }, + { + label: "BRAZIL | google.com.BR", + value: "google.com.br", + }, + { + label: "CHILE | google.CL", + value: "google.cl", + }, + { + label: "COLOMBIA | google.com.CO", + value: "google.com.co", + }, + { + label: "ECUADOR | google.com.EC", + value: "google.com.ec", + }, + { + label: "GUYANA | google.GY", + value: "google.gy", + }, + { + label: "PARAGUAY | google.com.PY", + value: "google.com.py", + }, + { + label: "PERU | google.com.PE", + value: "google.com.pe", + }, + { + label: "SURINAME | google.SR", + value: "google.sr", + }, + { + label: "URUGUAY | google.com.UY", + value: "google.com.uy", + }, + { + label: "VENEZUELA | google.co.VE", + value: "google.co.ve", + }, + { + label: "AFGHANISTAN | google.com.AF", + value: "google.com.af", + }, + { + label: "ARMENIA | google.AM", + value: "google.am", + }, + { + label: "AZERBAIJAN | google.AZ", + value: "google.az", + }, + { + label: "BAHRAIN | google.com.BH", + value: "google.com.bh", + }, + { + label: "BANGLADESH | google.com.BD", + value: "google.com.bd", + }, + { + label: "BHUTAN | google.BT", + value: "google.bt", + }, + { + label: "BRUNEI | google.com.BN", + value: "google.com.bn", + }, + { + label: "CAMBODIA | google.com.KH", + value: "google.com.kh", + }, + { + label: "CHINA | google.CN", + value: "google.cn", + }, + { + label: "CYPRUS | google.com.CY", + value: "google.com.cy", + }, + { + label: "GEORGIA | google.GE", + value: "google.ge", + }, + { + label: "HONG KONG | google.com.HK", + value: "google.com.hk", + }, + { + label: "INDIA | google.co.IN", + value: "google.co.in", + }, + { + label: "INDONESIA | google.co.ID", + value: "google.co.id", + }, + { + label: "IRAQ | google.IQ", + value: "google.iq", + }, + { + label: "ISRAEL | google.co.IL", + value: "google.co.il", + }, + { + label: "JAPAN | google.co.JP", + value: "google.co.jp", + }, + { + label: "JORDAN | google.JO", + value: "google.jo", + }, + { + label: "KAZAKHSTAN | google.KZ", + value: "google.kz", + }, + { + label: "KUWAIT | google.com.KW", + value: "google.com.kw", + }, + { + label: "KYRGYZSTAN | google.KG", + value: "google.kg", + }, + { + label: "LAOS | google.LA", + value: "google.la", + }, + { + label: "LEBANON | google.com.LB", + value: "google.com.lb", + }, + { + label: "MALAYSIA | google.com.MY", + value: "google.com.my", + }, + { + label: "MALDIVES | google.MV", + value: "google.mv", + }, + { + label: "MONGOLIA | google.MN", + value: "google.mn", + }, + { + label: "MYANMAR | google.com.MM", + value: "google.com.mm", + }, + { + label: "NEPAL | google.com.NP", + value: "google.com.np", + }, + { + label: "OMAN | google.com.OM", + value: "google.com.om", + }, + { + label: "PAKISTAN | google.com.PK", + value: "google.com.pk", + }, + { + label: "PALESTINE | google.PS", + value: "google.ps", + }, + { + label: "PHILIPPINES | google.com.PH", + value: "google.com.ph", + }, + { + label: "QATAR | google.com.QA", + value: "google.com.qa", + }, + { + label: "SAUDI ARABIA | google.com.SA", + value: "google.com.sa", + }, + { + label: "SINGAPORE | google.com.SG", + value: "google.com.sg", + }, + { + label: "SOUTH KOREA | google.co.KR", + value: "google.co.kr", + }, + { + label: "SRI LANKA | google.LK", + value: "google.lk", + }, + { + label: "TAIWAN | google.com.TW", + value: "google.com.tw", + }, + { + label: "TAJIKISTAN | google.com.TJ", + value: "google.com.tj", + }, + { + label: "THAILAND | google.co.TH", + value: "google.co.th", + }, + { + label: "TIMOR-LESTE | google.TL", + value: "google.tl", + }, + { + label: "TURKMENISTAN | google.TM", + value: "google.tm", + }, + { + label: "UNITED ARAB EMIRATES | google.AE", + value: "google.ae", + }, + { + label: "UZBEKISTAN | google.co.UZ", + value: "google.co.uz", + }, + { + label: "VIETNAM | google.com.VN", + value: "google.com.vn", + }, + { + label: "ALGERIA | google.DZ", + value: "google.dz", + }, + { + label: "ANGOLA | google.co.AO", + value: "google.co.ao", + }, + { + label: "BENIN | google.BJ", + value: "google.bj", + }, + { + label: "BOTSWANA | google.co.BW", + value: "google.co.bw", + }, + { + label: "BURKINA FASO | google.BF", + value: "google.bf", + }, + { + label: "BURUNDI | google.BI", + value: "google.bi", + }, + { + label: "CAMEROON | google.CM", + value: "google.cm", + }, + { + label: "CAPE VERDE | google.CV", + value: "google.cv", + }, + { + label: "CENTRAL AFRICAN REPUBLIC | google.CF", + value: "google.cf", + }, + { + label: "CHAD | google.TD", + value: "google.td", + }, + { + label: "DEMOCRATIC REPUBLIC OF THE CONGO | google.CD", + value: "google.cd", + }, + { + label: "DJIBOUTI | google.DJ", + value: "google.dj", + }, + { + label: "EGYPT | google.com.EG", + value: "google.com.eg", + }, + { + label: "ETHIOPIA | google.com.ET", + value: "google.com.et", + }, + { + label: "GABON | google.GA", + value: "google.ga", + }, + { + label: "GAMBIA | google.GM", + value: "google.gm", + }, + { + label: "GHANA | google.com.GH", + value: "google.com.gh", + }, + { + label: "IVORY COAST | google.CI", + value: "google.ci", + }, + { + label: "KENYA | google.co.KE", + value: "google.co.ke", + }, + { + label: "LESOTHO | google.co.LS", + value: "google.co.ls", + }, + { + label: "LIBYA | google.com.LY", + value: "google.com.ly", + }, + { + label: "MADAGASCAR | google.MG", + value: "google.mg", + }, + { + label: "MALAWI | google.MW", + value: "google.mw", + }, + { + label: "MALI | google.ML", + value: "google.ml", + }, + { + label: "MAURITIUS | google.MU", + value: "google.mu", + }, + { + label: "MOROCCO | google.co.MA", + value: "google.co.ma", + }, + { + label: "MOZAMBIQUE | google.co.MZ", + value: "google.co.mz", + }, + { + label: "NAMIBIA | google.com.NA", + value: "google.com.na", + }, + { + label: "NIGER | google.NE", + value: "google.ne", + }, + { + label: "NIGERIA | google.com.NG", + value: "google.com.ng", + }, + { + label: "REPUBLIC OF THE CONGO | google.CG", + value: "google.cg", + }, + { + label: "RWANDA | google.RW", + value: "google.rw", + }, + { + label: "SAINT HELENA | google.SH", + value: "google.sh", + }, + { + label: "SAO TOMÉ AND PRÍNCIPE | google.ST", + value: "google.st", + }, + { + label: "SENEGAL | google.SN", + value: "google.sn", + }, + { + label: "SEYCHELLES | google.SC", + value: "google.sc", + }, + { + label: "SIERRA LEONE | google.com.SL", + value: "google.com.sl", + }, + { + label: "SOMALIA | google.SO", + value: "google.so", + }, + { + label: "SOUTH AFRICA | google.co.ZA", + value: "google.co.za", + }, + { + label: "TANZANIA | google.co.TZ", + value: "google.co.tz", + }, + { + label: "TOGO | google.TG", + value: "google.tg", + }, + { + label: "TUNISIA | google.TN", + value: "google.tn", + }, + { + label: "UGANDA | google.co.UG", + value: "google.co.ug", + }, + { + label: "ZAMBIA | google.co.ZM", + value: "google.co.zm", + }, + { + label: "ZIMBABWE | google.co.ZW", + value: "google.co.zw", + }, + { + label: "AMERICAN SAMOA | google.AS", + value: "google.as", + }, + { + label: "ANGUILLA | google.com.AI", + value: "google.com.ai", + }, + { + label: "AUSTRALIA | google.com.AU", + value: "google.com.au", + }, + { + label: "COOK ISLANDS | google.co.CK", + value: "google.co.ck", + }, + { + label: "FIJI | google.com.FJ", + value: "google.com.fj", + }, + { + label: "KIRIBATI | google.KI", + value: "google.ki", + }, + { + label: "MICRONESIA | google.FM", + value: "google.fm", + }, + { + label: "MONTSERRAT | google.MS", + value: "google.ms", + }, + { + label: "NAURU | google.NR", + value: "google.nr", + }, + { + label: "NEW ZEALAND | google.co.NZ", + value: "google.co.nz", + }, + { + label: "NIUE | google.NU", + value: "google.nu", + }, + { + label: "PAPUA NEW GUINEA | google.com.PG", + value: "google.com.pg", + }, + { + label: "PITCAIRN | google.PN", + value: "google.pn", + }, + { + label: "SOLOMON ISLANDS | google.com.SB", + value: "google.com.sb", + }, + { + label: "TONGA | google.TO", + value: "google.to", + }, + { + label: "VANUATU | google.VU", + value: "google.vu", + }, + { + label: "WESTERN SAMOA | google.WS", + value: "google.ws", + }, +]; diff --git a/components/neuronwriter/neuronwriter.app.mjs b/components/neuronwriter/neuronwriter.app.mjs new file mode 100644 index 0000000000000..e5707ce4519ab --- /dev/null +++ b/components/neuronwriter/neuronwriter.app.mjs @@ -0,0 +1,76 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "neuronwriter", + propDefinitions: { + queryId: { + type: "string", + label: "Query ID", + description: "The ID of the query to fetch or manipulate.", + async options() { + const data = await this.listQueries({ + data: { + project: this.$auth.project_id, + }, + }); + + return data.map(({ + query: value, source, + }) => ({ + label: `${value} - Source: ${source}`, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://app.neuronwriter.com/neuron-api/0.5/writer"; + }, + _headers() { + return { + "X-API-KEY": this.$auth.api_key, + "Accept": "application/json", + "Content-Type": "application/json", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createNewQuery(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/new-query", + ...opts, + }); + }, + getQueryResults(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/get-query", + ...opts, + }); + }, + getContent(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/get-content", + ...opts, + }); + }, + listQueries(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/list-queries", + ...opts, + }); + }, + }, +}; diff --git a/components/neuronwriter/package.json b/components/neuronwriter/package.json new file mode 100644 index 0000000000000..ea412d7e25507 --- /dev/null +++ b/components/neuronwriter/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/neuronwriter", + "version": "0.1.0", + "description": "Pipedream NEURONWriter Components", + "main": "neuronwriter.app.mjs", + "keywords": [ + "pipedream", + "neuronwriter" + ], + "homepage": "https://pipedream.com/apps/neuronwriter", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} + diff --git a/components/neuronwriter/sources/common/base.mjs b/components/neuronwriter/sources/common/base.mjs new file mode 100644 index 0000000000000..e9684e947f1a5 --- /dev/null +++ b/components/neuronwriter/sources/common/base.mjs @@ -0,0 +1,66 @@ +import { + ConfigurationError, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import neuronwriter from "../../neuronwriter.app.mjs"; + +export default { + props: { + neuronwriter, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01"; + }, + _setLastDate(created) { + this.db.set("lastDate", created); + }, + generateMeta(item) { + return { + id: item.query, + summary: this.getSummary(item), + ts: item.updated, + }; + }, + getFilter() { + throw new ConfigurationError("getFilter not implemented yet"); + }, + async startEvent(maxResults = 0) { + const lastDate = this._getLastDate(); + const response = await this.neuronwriter.listQueries({ + data: { + project: this.neuronwriter.$auth.project_id, + updated: lastDate, + ...this.getFilter(), + }, + }); + + const responseArray = []; + let count = 0; + for await (const item of response) { + if (maxResults && (++count > maxResults)) break; + responseArray.push(item); + } + + if (responseArray.length) this._setLastDate(responseArray[0].updated); + + for (const item of responseArray.reverse()) { + this.$emit(item, this.generateMeta(item)); + } + }, + }, + hooks: { + async deploy() { + await this.startEvent(25); + }, + }, + async run() { + await this.startEvent(); + }, +}; diff --git a/components/neuronwriter/sources/new-done-query/new-done-query.mjs b/components/neuronwriter/sources/new-done-query/new-done-query.mjs new file mode 100644 index 0000000000000..ada25050b0817 --- /dev/null +++ b/components/neuronwriter/sources/new-done-query/new-done-query.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "neuronwriter-new-done-query", + name: "New Done Query", + description: "Emit new event when a query is marked as 'done', indicating content is ready for publication.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFilter() { + return { + tags: [ + "Done", + ], + }; + }, + getSummary(item) { + return `Query ID: ${item.query} marked as 'done'`; + }, + }, + sampleEmit, +}; diff --git a/components/neuronwriter/sources/new-done-query/test-event.mjs b/components/neuronwriter/sources/new-done-query/test-event.mjs new file mode 100644 index 0000000000000..ebfca27443a85 --- /dev/null +++ b/components/neuronwriter/sources/new-done-query/test-event.mjs @@ -0,0 +1,10 @@ +export default { + "id": "38026513a7fbb549", + "query": "38026513a7fbb549", + "created": "2024-05-29T18:56:34+00:00", + "updated": "2024-05-29T18:58:21+00:00", + "source": "neuron-api", + "tags": [ + "Done" + ] +} \ No newline at end of file diff --git a/components/neuronwriter/sources/new-query-processed/new-query-processed.mjs b/components/neuronwriter/sources/new-query-processed/new-query-processed.mjs new file mode 100644 index 0000000000000..9568643c531ec --- /dev/null +++ b/components/neuronwriter/sources/new-query-processed/new-query-processed.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "neuronwriter-new-query-processed", + name: "New Query Processed", + description: "Emit new event when a query is processed by NeuronWriter API and results are ready.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFilter() { + return { + status: "ready", + }; + }, + getSummary(item) { + return `Query ${item.query} is ready.`; + }, + }, + sampleEmit, +}; diff --git a/components/neuronwriter/sources/new-query-processed/test-event.mjs b/components/neuronwriter/sources/new-query-processed/test-event.mjs new file mode 100644 index 0000000000000..c0056eb497933 --- /dev/null +++ b/components/neuronwriter/sources/new-query-processed/test-event.mjs @@ -0,0 +1,8 @@ +export default { + "id": "38026513a7fbb549", + "query": "38026513a7fbb549", + "created": "2024-05-29T18:56:34+00:00", + "updated": "2024-05-29T18:58:21+00:00", + "source": "neuron-api", + "tags": [] +} \ No newline at end of file diff --git a/components/neutrino/README.md b/components/neutrino/README.md index ab6e79f8453d4..442dfdbf21812 100644 --- a/components/neutrino/README.md +++ b/components/neutrino/README.md @@ -1,14 +1,11 @@ # Overview -With the Neutrino API, you can build many different types of applications and -services, including: - -- Location-based applications -- Geolocation services -- Travel and mapping applications -- Weather applications -- Social media applications -- Messaging and chat applications -- ECommerce applications -- Financial applications -- Politics and news applications +The Neutrino API provides a set of tools for performing various utility functions over the web. Leveraging this API within Pipedream workflows enables you to automate tasks like email validation, bad word filtering, IP blocklisting, and data conversion. It's a Swiss Army knife for developers needing to ensure data integrity, security, and streamlined data processing within their applications. + +# Example Use Cases + +- **Email Verification Workflow**: Automatically validate email addresses in real-time as they are collected from lead forms on your website. When a new submission is received, Pipedream can trigger a workflow that uses Neutrino to verify the email's validity and then update your CRM, like Salesforce, only with the verified leads, ensuring your contact lists are clean and bounce rates are minimized. + +- **Content Moderation System**: Maintain the quality of user-generated content on your platform by setting up an automated content moderation system. Use Neutrino's bad word filter with Pipedream's webhook to scan and filter out profanity or inappropriate content from comments, reviews, or forum posts. If bad content is detected, the workflow could notify moderators via Slack or remove the content automatically from your database. + +- **Enhanced Security Monitoring**: Create a security workflow that uses Neutrino's IP Blocklist and Threat Intelligence to monitor access logs. When a new IP hits your service, trigger a Pipedream workflow that checks the IP against Neutrino's database. If it's a known threat, the workflow could alert your security team on Microsoft Teams and automatically block the IP on your firewall or cloud infrastructure like AWS. diff --git a/components/neverbounce/README.md b/components/neverbounce/README.md index aa51217a6ed29..74c945e0be548 100644 --- a/components/neverbounce/README.md +++ b/components/neverbounce/README.md @@ -1,8 +1,11 @@ # Overview -The NeverBounce API can be used to build a variety of applications, including: +Neverbounce is a nifty email verification tool that ensures your email lists are clean and your messages reach real people. By leveraging the Neverbounce API on Pipedream, you can automate the process of validating emails in real-time or in batches, directly within your workflows. This means you can maintain high deliverability rates, decrease bounce rates, and preserve your sender reputation without breaking a sweat. -- A way to verify the validity of an email address -- A tool to check if an email address is associated with a real person -- A service to clean up a list of email addresses -- A component of a larger application that need to verify email addresses +# Example Use Cases + +- **Email List Hygiene Automation**: Automatically clean email lists by connecting Neverbounce to a CRM like HubSpot. When a new contact is added, trigger a workflow that uses Neverbounce to validate the email address. If it's valid, update the contact in HubSpot; if not, flag it for review or removal. + +- **Real-Time Signup Verification**: Integrate Neverbounce with your sign-up forms to validate email addresses as they're entered. Use Pipedream to create a workflow where each new entry triggers an email check. Upon verification, proceed with the signup process or prompt the user to provide a valid email. + +- **Automated Outreach Quality Control**: Before sending out a marketing campaign using a platform like SendGrid, use Neverbounce within a Pipedream workflow to verify all recipient email addresses. This helps ensure your campaign reaches the intended audience and improves overall engagement metrics. diff --git a/components/new_relic/README.md b/components/new_relic/README.md index ec83b917c5bba..77789f0bb3492 100644 --- a/components/new_relic/README.md +++ b/components/new_relic/README.md @@ -1,15 +1,11 @@ # Overview -New Relic's API enables you to do the following: +The New Relic API offers powerful capabilities for monitoring, alerting, and analyzing the performance of your web applications and infrastructure. By using the API within Pipedream, you can automate and orchestrate a vast array of operations that revolve around your application's health and data insights. This can range from triggering workflows based on New Relic alerts, to syncing performance data with other tools, or even automating responses to specific incidents. -- Automate your New Relic account and data -- Retrieve data from New Relic for use in other tools -- Build New Relic-powered tools and applications +# Example Use Cases -Here are some examples of what you can build using the New Relic API: +- **Automated Incident Response**: When New Relic detects an anomaly or a performance issue, it can trigger a Pipedream workflow that automatically posts a message to a Slack channel, alerting the relevant team. The workflow could also create a ticket in Jira, ensuring that the incident is tracked and managed properly. -- A custom dashboard to display the data that matters most to you -- An alerting system that notifies you when your app is experiencing problems -- A tool to help you track your New Relic bill and usage -- An application that integrates with New Relic to provide additional data or - functionality +- **Performance Metrics to Data Warehouse**: Set up a Pipedream workflow that periodically fetches performance metrics from New Relic and inserts this data into a Google BigQuery data warehouse. This allows for advanced analysis alongside other business metrics, giving a more comprehensive view of how application performance impacts the overall business. + +- **Dynamic Configuration Updates**: If New Relic reports that a service is experiencing high traffic, a Pipedream workflow can interact with other APIs, such as a feature flagging service like LaunchDarkly, to dynamically adjust application features or throttle user access to maintain service stability. diff --git a/components/new_sloth/README.md b/components/new_sloth/README.md new file mode 100644 index 0000000000000..5f917d67f63f7 --- /dev/null +++ b/components/new_sloth/README.md @@ -0,0 +1,11 @@ +# Overview + +The New Sloth API offers data on slow-moving trends and behaviors, useful for analytics in various domains such as marketing, finance, and social media. Within Pipedream, you can use this API to automate the collection and analysis of trend data, create custom alerts, and integrate with other apps for enhanced decision-making processes. + +# Example Use Cases + +- **Trend Alert System**: Create a workflow that monitors specific trends from the New Sloth API. When a trend reaches a particular threshold, trigger an alert via email or messaging apps like Slack or Discord, ensuring you're always updated on crucial shifts. + +- **Market Analysis Automation**: Analyze financial market trends by setting up a Pipedream workflow that fetches data from the New Sloth API daily. Process this data to gain insights, and integrate with Google Sheets or Airtable to maintain an organized record of market behavior over time. + +- **Social Media Strategy**: Leverage the slow-moving trends in social media by constructing a Pipedream workflow where New Sloth API data guides your content strategy. Sync with social media platforms such as Twitter or Instagram to optimize posting schedules and content themes based on trending data. diff --git a/components/new_sloth/new_sloth.app.mjs b/components/new_sloth/new_sloth.app.mjs new file mode 100644 index 0000000000000..7227ccb370425 --- /dev/null +++ b/components/new_sloth/new_sloth.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "new_sloth", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/new_sloth/package.json b/components/new_sloth/package.json new file mode 100644 index 0000000000000..a59e19c2af399 --- /dev/null +++ b/components/new_sloth/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/new_sloth", + "version": "0.0.1", + "description": "Pipedream New Sloth Components", + "main": "new_sloth.app.mjs", + "keywords": [ + "pipedream", + "new_sloth" + ], + "homepage": "https://pipedream.com/apps/new_sloth", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/new_york_times/README.md b/components/new_york_times/README.md new file mode 100644 index 0000000000000..4b4132c1223a0 --- /dev/null +++ b/components/new_york_times/README.md @@ -0,0 +1,11 @@ +# Overview + +The New York Times API offers a variety of endpoints to access different types of news and information. With Pipedream, you can tap into this wealth of data to automate tasks, enrich applications with the latest news, or even analyze trends. Workflows on Pipedream triggered by NYT's API can range from sending daily news briefings to your communication platform, to analyzing article metadata for SEO research, or even keeping track of the most popular stories for content curation. + +# Example Use Cases + +- **Daily News Digest to Slack**: Automatically fetch the top stories from New York Times every day and post them to a Slack channel, keeping your team informed on the latest news without leaving their workspace. + +- **SEO Trend Analysis**: Analyze article metadata and trending topics from NYT to identify popular keywords and subjects, providing valuable insights for content creation and SEO strategy. + +- **Content Curation for Social Media**: Monitor specific sections or topics in the NYT, curate a list of articles, and share them on social media platforms like Twitter, helping to position yourself or your brand as a thought leader in your chosen field. diff --git a/components/new_york_times/new_york_times.app.mjs b/components/new_york_times/new_york_times.app.mjs index cb6baea75b507..64e78719987aa 100644 --- a/components/new_york_times/new_york_times.app.mjs +++ b/components/new_york_times/new_york_times.app.mjs @@ -1,11 +1,57 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "new_york_times", - propDefinitions: {}, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.nytimes.com/svc"; + }, + _params(params = {}) { + return { + ...params, + "api-key": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, params, ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + params: this._params(params), + }); + }, + searchArticles(opts = {}) { + return this._makeRequest({ + path: "/search/v2/articlesearch.json", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = page++; + const { response: { docs } } = await fn({ + params, + ...opts, + }); + for (const d of docs) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = docs.length; + + } while (hasMore); }, }, -}; \ No newline at end of file +}; diff --git a/components/new_york_times/package.json b/components/new_york_times/package.json index df0450fa17b6c..8b8e7264f0865 100644 --- a/components/new_york_times/package.json +++ b/components/new_york_times/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/new_york_times", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream New York Times Components", "main": "new_york_times.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1", + "moment": "^2.30.1" } -} \ No newline at end of file +} diff --git a/components/new_york_times/sources/new-article/new-article.mjs b/components/new_york_times/sources/new-article/new-article.mjs new file mode 100644 index 0000000000000..5a48a6c12970e --- /dev/null +++ b/components/new_york_times/sources/new-article/new-article.mjs @@ -0,0 +1,130 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import moment from "moment"; +import app from "../../new_york_times.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "new_york_times-new-article", + name: "New Article Published", + description: "Emit new event when an article is published.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + facet: { + type: "boolean", + label: "Facet", + description: "Whether to show facet counts.", + optional: true, + }, + facetFields: { + type: "string[]", + label: "Facet Fields", + description: "Use facets to view the relative importance of certain fields to a search term, and gain insight about how to best refine your queries and filter your search results.", + options: [ + "day_of_week", + "document_type", + "ingredients", + "news_desk", + "pub_month", + "pub_year", + "section_name", + "source", + "subsection_name", + "type_of_material", + ], + optional: true, + }, + facetFilter: { + type: "boolean", + label: "Facet Filter", + description: "Have facet counts use filters.", + optional: true, + }, + fl: { + type: "string[]", + label: "Field List", + description: "The list of fields to return in the result set.", + optional: true, + }, + fq: { + type: "string", + label: "Filtered Query", + description: "A query to filter the search results.", + optional: true, + }, + q: { + type: "string", + label: "Query", + description: "Search query term. Search is performed on the article body, headline, and byline.", + optional: true, + }, + alert: { + type: "alert", + alertType: "info", + content: "To learn more about filters you can download the [New York Times specification here](https://developer.nytimes.com/portals/api/sites/nyt-devportal/liveportal/apis/articlesearch-product/download_spec).", + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || moment().subtract(1, "day"); + }, + _setLastDate(created) { + this.db.set("lastDate", created); + }, + generateMeta(item) { + return { + id: item._id, + summary: `A new article was just published with Id: ${item._id}.`, + ts: item.pub_date, + }; + }, + async startEvent(maxResults = 0) { + const lastDate = this._getLastDate(); + + const data = this.app.paginate({ + fn: this.app.searchArticles, + maxResults, + params: { + begin_date: moment(lastDate).format("YYYYMMDD"), + facet: this.facet, + facet_fields: this.facetFields && this.facetFields.join(), + facet_filter: this.facetFilter, + fl: this.fl && this.fl.join(), + fq: this.fq, + q: this.q, + sort: "newest", + }, + }); + + const responseArray = []; + for await (const item of data) { + if (Date.parse(item.pub_date) <= Date.parse(lastDate)) break; + responseArray.push(item); + } + + if (responseArray.length) this._setLastDate(responseArray[0].pub_date); + + for (const item of responseArray.reverse()) { + this.$emit(item, this.generateMeta(item)); + } + }, + }, + hooks: { + async deploy() { + await this.startEvent(25); + }, + }, + async run() { + await this.startEvent(); + }, + sampleEmit, +}; diff --git a/components/new_york_times/sources/new-article/test-event.mjs b/components/new_york_times/sources/new-article/test-event.mjs new file mode 100644 index 0000000000000..af917392a9e34 --- /dev/null +++ b/components/new_york_times/sources/new-article/test-event.mjs @@ -0,0 +1,101 @@ +export default { + "abstract": "President Biden called the United States and Japan “the closest of friends” during a welcoming ceremony for Prime Minister Fumio Kishida.", + "web_url": "https://www.nytimes.com/video/us/politics/100000009407179/biden-kishida-us-japan.html", + "snippet": "President Biden called the United States and Japan “the closest of friends” during a welcoming ceremony for Prime Minister Fumio Kishida.", + "lead_paragraph": "President Biden called the United States and Japan “the closest of friends” during a welcoming ceremony for Prime Minister Fumio Kishida.", + "source": "The New York Times", + "multimedia": [ + { + "rank": 0, + "subtype": "xlarge", + "caption": null, + "credit": null, + "type": "image", + "url": "images/2024/04/10/multimedia/10vid-biden-kishida-welcome-COVER-whqf/10vid-biden-kishida-welcome-COVER-whqf-articleLarge.jpg", + "height": 426, + "width": 600, + "legacy": { + "xlarge": "images/2024/04/10/multimedia/10vid-biden-kishida-welcome-COVER-whqf/10vid-biden-kishida-welcome-COVER-whqf-articleLarge.jpg", + "xlargewidth": 600, + "xlargeheight": 426 + }, + "subType": "xlarge", + "crop_name": "articleLarge" + }, + { + "rank": 0, + "subtype": "popup", + "caption": null, + "credit": null, + "type": "image", + "url": "images/2024/04/10/multimedia/10vid-biden-kishida-welcome-COVER-whqf/10vid-biden-kishida-welcome-COVER-whqf-popup.jpg", + "height": 461, + "width": 650, + "legacy": {}, + "subType": "popup", + "crop_name": "popup" + }, + { + "rank": 0, + "subtype": "blog480", + "caption": null, + "credit": null, + "type": "image", + "url": "images/2024/04/10/multimedia/10vid-biden-kishida-welcome-COVER-whqf/10vid-biden-kishida-welcome-COVER-whqf-blog480.jpg", + "height": 341, + "width": 480, + "legacy": {}, + "subType": "blog480", + "crop_name": "blog480" + }, + ], + "headline": { + "main": "Biden Hosts Japan’s Prime Minister at the White House", + "kicker": null, + "content_kicker": null, + "print_headline": null, + "name": null, + "seo": null, + "sub": null + }, + "keywords": [ + { + "name": "persons", + "value": "Biden, Joseph R Jr", + "rank": 1, + "major": "N" + }, + { + "name": "persons", + "value": "Kishida, Fumio", + "rank": 2, + "major": "N" + }, + { + "name": "glocations", + "value": "Japan", + "rank": 3, + "major": "N" + }, + { + "name": "glocations", + "value": "United States", + "rank": 4, + "major": "N" + } + ], + "pub_date": "2024-04-10T16:43:13+0000", + "document_type": "multimedia", + "news_desk": "", + "section_name": "U.S.", + "subsection_name": "Politics", + "byline": { + "original": "By The New York Times", + "person": [], + "organization": "The New York Times" + }, + "type_of_material": "Video", + "_id": "nyt://video/6e4c5474-7ef4-5f08-8218-fec7666cbe0e", + "word_count": 0, + "uri": "nyt://video/6e4c5474-7ef4-5f08-8218-fec7666cbe0e" +} \ No newline at end of file diff --git a/components/news_api/actions/search-everything/search-everything.mjs b/components/news_api/actions/search-everything/search-everything.mjs new file mode 100644 index 0000000000000..49e77517b6383 --- /dev/null +++ b/components/news_api/actions/search-everything/search-everything.mjs @@ -0,0 +1,100 @@ +import newsapi from "../../news_api.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "news_api-search-everything", + name: "Search Everything", + description: "Search through millions of articles from over 150,000 large and small news sources and blogs. [See the documentation](https://newsapi.org/docs/endpoints/everything)", + version: "0.0.1", + type: "action", + props: { + newsapi, + q: { + propDefinition: [ + newsapi, + "q", + ], + }, + searchin: { + propDefinition: [ + newsapi, + "searchin", + ], + }, + sourceIds: { + propDefinition: [ + newsapi, + "sourceIds", + ], + }, + domains: { + type: "string[]", + label: "Domains", + description: "An array of domains to restrict the search to", + optional: true, + }, + excludeDomains: { + type: "string[]", + label: "Exclude Domains", + description: "An array of domains to remove from the results", + optional: true, + }, + from: { + type: "string", + label: "From", + description: "A date and optional time for the oldest article allowed. This should be in ISO 8601 format (e.g. `2024-11-01` or `2024-11-01T17:27:47`)", + optional: true, + }, + to: { + type: "string", + label: "To", + description: "A date and optional time for the newest article allowed. This should be in ISO 8601 format (e.g. `2024-11-01` or `2024-11-01T17:27:47`)", + optional: true, + }, + language: { + propDefinition: [ + newsapi, + "language", + ], + }, + sortBy: { + propDefinition: [ + newsapi, + "sortBy", + ], + }, + maxResults: { + propDefinition: [ + newsapi, + "maxResults", + ], + }, + }, + async run({ $ }) { + const { + status, articles, + } = await this.newsapi.searchEverything({ + $, + params: { + q: this.q, + searchin: utils.joinArray(this.searchin), + sources: utils.joinArray(this.sourceIds), + domains: utils.joinArray(this.domains), + excludeDomains: utils.joinArray(this.excludeDomains), + from: this.from, + to: this.to, + language: this.language, + sortBy: this.sortBy, + pageSize: this.maxResults, + }, + }); + + if (status === "ok") { + $.export("$summary", `Successfully retrieved ${articles.length} article${articles.length === 1 + ? "" + : "s"}`); + } + + return articles; + }, +}; diff --git a/components/news_api/actions/search-top-headlines/search-top-headlines.mjs b/components/news_api/actions/search-top-headlines/search-top-headlines.mjs new file mode 100644 index 0000000000000..e520a2bd23a2d --- /dev/null +++ b/components/news_api/actions/search-top-headlines/search-top-headlines.mjs @@ -0,0 +1,73 @@ +import newsapi from "../../news_api.app.mjs"; +import utils from "../../common/utils.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "news_api-search-top-headlines", + name: "Search Top Headlines", + description: "Retrieve live top and breaking headlines for a category, single source, multiple sources, or keywords. [See the documentation](https://newsapi.org/docs/endpoints/top-headlines)", + version: "0.0.1", + type: "action", + props: { + newsapi, + q: { + propDefinition: [ + newsapi, + "q", + ], + optional: true, + }, + category: { + type: "string", + label: "Category", + description: "The category you want to get headlines for. Possible options: `business` `entertainment` `general` `health` `science` `sports` `technology`. Note: you can't mix this param with the `sources` param.", + optional: true, + }, + sourceIds: { + propDefinition: [ + newsapi, + "sourceIds", + ], + }, + maxResults: { + propDefinition: [ + newsapi, + "maxResults", + ], + }, + }, + async run({ $ }) { + if (this.category && this.sourceIds) { + throw new ConfigurationError("Please specify only one of `Category` or `SourceIds`"); + } + + const params = { + q: this.q, + category: this.category, + sources: utils.joinArray(this.sourceIds), + pageSize: this.maxResults, + }; + + // The only available country is "us", but it can't be specified along with category or sources. + // At least one of q, category, sources, or country must be entered, so adding in country if + // none of the others are specified. + if (!this.q && !this.category && !this.sourceIds) { + params.country = "us"; + } + + const { + status, articles, + } = await this.newsapi.searchTopHeadlines({ + $, + params, + }); + + if (status === "ok") { + $.export("$summary", `Successfully retrieved ${articles.length} article${articles.length === 1 + ? "" + : "s"}`); + } + + return articles; + }, +}; diff --git a/components/news_api/common/constants.mjs b/components/news_api/common/constants.mjs new file mode 100644 index 0000000000000..527f5bb44c4f4 --- /dev/null +++ b/components/news_api/common/constants.mjs @@ -0,0 +1,76 @@ +const SEARCH_IN_OPTIONS = [ + "title", + "description", + "content", +]; + +const SORT_OPTIONS = [ + "relevancy", + "popularity", + "publishedAt", +]; + +const LANGUAGES = [ + { + value: "ar", + label: "Arabic", + }, + { + value: "de", + label: "German", + }, + { + value: "en", + label: "English", + }, + { + value: "es", + label: "Spanish", + }, + { + value: "fr", + label: "French", + }, + { + value: "he", + label: "Hebrew", + }, + { + value: "it", + label: "Italian", + }, + { + value: "nl", + label: "Dutch", + }, + { + value: "no", + label: "Norwegian", + }, + { + value: "pt", + label: "Portuguese", + }, + { + value: "ru", + label: "Russian", + }, + { + value: "sv", + label: "Swedish", + }, + { + value: "ud", + label: "Urdu", + }, + { + value: "zh", + label: "Chinese", + }, +]; + +export default { + SEARCH_IN_OPTIONS, + SORT_OPTIONS, + LANGUAGES, +}; diff --git a/components/news_api/common/utils.mjs b/components/news_api/common/utils.mjs new file mode 100644 index 0000000000000..1c5c52546a575 --- /dev/null +++ b/components/news_api/common/utils.mjs @@ -0,0 +1,10 @@ +function joinArray(arr) { + if (!arr) { + return undefined; + } + return arr.join(); +} + +export default { + joinArray, +}; diff --git a/components/news_api/news_api.app.mjs b/components/news_api/news_api.app.mjs new file mode 100644 index 0000000000000..138238ac9dd18 --- /dev/null +++ b/components/news_api/news_api.app.mjs @@ -0,0 +1,92 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "news_api", + propDefinitions: { + sourceIds: { + type: "string[]", + label: "Source IDs", + description: "An array of source identifiers (maximum 20) for the news sources or blogs you want headlines from", + optional: true, + async options() { + const { sources } = await this.listSources(); + return sources.map(({ + id: value, name: label, + }) => ({ + value, + label, + })); + }, + }, + searchin: { + type: "string[]", + label: "Search In", + description: "The fields to restrict your q search to. Default: all fields are searched", + options: constants.SEARCH_IN_OPTIONS, + optional: true, + }, + q: { + type: "string", + label: "Query", + description: "Keywords or phrases to search for", + }, + language: { + type: "string", + label: "Language", + description: "The 2-letter ISO-639-1 code of the language you want to get headlines for", + options: constants.LANGUAGES, + optional: true, + }, + sortBy: { + type: "string", + label: "Sort By", + description: "The order to sort the articles in. Default: `publishedAt`", + options: constants.SORT_OPTIONS, + optional: true, + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "The maximum number of results to return. Must be between 1 and 100.", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://newsapi.org/v2"; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "X-Api-Key": `${this.$auth.api_key}`, + }, + ...opts, + }); + }, + listSources(opts = {}) { + return this._makeRequest({ + path: "/top-headlines/sources", + ...opts, + }); + }, + searchEverything(opts = {}) { + return this._makeRequest({ + path: "/everything", + ...opts, + }); + }, + searchTopHeadlines(opts = {}) { + return this._makeRequest({ + path: "/top-headlines", + ...opts, + }); + }, + }, +}; diff --git a/components/news_api/package.json b/components/news_api/package.json new file mode 100644 index 0000000000000..542a42c5868a2 --- /dev/null +++ b/components/news_api/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/news_api", + "version": "0.1.0", + "description": "Pipedream News API Components", + "main": "news_api.app.mjs", + "keywords": [ + "pipedream", + "news_api" + ], + "homepage": "https://pipedream.com/apps/news_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/newsapi/README.md b/components/newsapi/README.md index f00b9b13c0cd9..dab586ab281d5 100644 --- a/components/newsapi/README.md +++ b/components/newsapi/README.md @@ -1,8 +1,11 @@ # Overview -With the NewsAPI API, you can build apps that: +NewsAPI is a powerful tool for accessing live and historic news articles from worldwide sources. With its simple RESTful interface, you can harness the flow of information for content aggregation, media monitoring, or data analytics. Pipedream's serverless platform takes it further by allowing you to automate interactions with NewsAPI, letting you integrate news data into diverse workflows, trigger actions based on news events, or blend news with other data sources for insightful analysis. -- Retrieve news stories from over 70 different sources -- Filter stories by keyword, topic, or location -- Display story headlines, descriptions, and images -- Stay up-to-date with the latest news stories as they happen +# Example Use Cases + +- **Content Curation and Social Media Automation**: Use NewsAPI to fetch the latest articles on specific topics, then filter and post the most relevant content to social media platforms like Twitter or Facebook automatically. This keeps your social media feeds fresh and engaging without manual oversight. + +- **Sentiment Analysis and Trend Tracking**: Combine NewsAPI with a sentiment analysis service like Google's Natural Language API. Automatically analyze the sentiment of news content to gain insights into public opinion and emerging trends, which can inform marketing strategies or product development. + +- **Email Digests for Targeted News Updates**: Set up a workflow that collects top news stories related to your industry from NewsAPI, formats them into a digest, and sends out an email through SendGrid or a similar email service. Keep your team informed with the latest developments directly in their inbox. diff --git a/components/newsapi/package.json b/components/newsapi/package.json new file mode 100644 index 0000000000000..21446e94db3fc --- /dev/null +++ b/components/newsapi/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/newsapi", + "version": "0.6.0", + "description": "Pipedream newsapi Components", + "main": "newsapi.app.mjs", + "keywords": [ + "pipedream", + "newsapi" + ], + "homepage": "https://pipedream.com/apps/newsapi", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/newscatcher/README.md b/components/newscatcher/README.md new file mode 100644 index 0000000000000..eee9dbd770005 --- /dev/null +++ b/components/newscatcher/README.md @@ -0,0 +1,11 @@ +# Overview + +The NewsCatcher API lets you tap into a wealth of news articles from various sources around the globe. By using this API within Pipedream, you can automate the retrieval of the latest news based on keywords, topics, or languages, and integrate this data into numerous applications or workflows. Use it to keep tabs on industry trends, monitor brand mentions, or aggregate news for specific research topics. + +# Example Use Cases + +- **Automated News Alerts to Slack**: Use the NewsCatcher API to monitor news for specific keywords. When an article of interest is published, Pipedream can automatically send a message with the article details to a designated Slack channel. This keeps teams informed in real-time about relevant news updates. + +- **Sentiment Analysis of News Articles**: Combine NewsCatcher with a sentiment analysis tool like the sentiment npm package available in Pipedream. Fetch articles on a particular subject, analyze the sentiment of the content, and store the results. This can help in market research or understanding public perception on certain topics. + +- **Curated Newsletters with SendGrid**: Leverage the NewsCatcher API to pull the latest articles for your niche audience. Use Pipedream to format this information and send it through SendGrid as part of a regular newsletter, providing subscribers with a digest of the most pertinent news tailored to their interests. diff --git a/components/newscatcher/newscatcher.app.mjs b/components/newscatcher/newscatcher.app.mjs new file mode 100644 index 0000000000000..16c9493889b44 --- /dev/null +++ b/components/newscatcher/newscatcher.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "newscatcher", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/newscatcher/package.json b/components/newscatcher/package.json new file mode 100644 index 0000000000000..1d4ebcf680890 --- /dev/null +++ b/components/newscatcher/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/newscatcher", + "version": "0.0.1", + "description": "Pipedream NewsCatcher Components", + "main": "newscatcher.app.mjs", + "keywords": [ + "pipedream", + "newscatcher" + ], + "homepage": "https://pipedream.com/apps/newscatcher", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/newsletter/README.md b/components/newsletter/README.md new file mode 100644 index 0000000000000..9929b95d76f30 --- /dev/null +++ b/components/newsletter/README.md @@ -0,0 +1,11 @@ +# Overview + +The Newsletter API provides a way to integrate and automate email campaign management directly within Pipedream. With this API, you can manage subscribers, send out newsletters, and analyze the performance of your email campaigns. It's a powerful tool for website owners, bloggers, and marketers who want to engage their audience through regular email updates. + +# Example Use Cases + +- **Automated Subscriber Sync**: Automatically add new subscribers from other platforms to your Newsletter mailing list by connecting the Newsletter API with Pipedream's trigger for new rows in a Google Sheets spreadsheet. + +- **Scheduled Newsletter Campaigns**: Use Pipedream’s scheduled triggers to send out newsletters at predetermined times. Combine this with dynamic content from your website's RSS feed to keep the newsletters fresh and relevant. + +- **Performance Reporting to Slack**: After sending out a newsletter, use Pipedream to call the Newsletter API for performance data and construct a report. Then, send this report to a Slack channel where your team can quickly analyze the campaign's effectiveness. diff --git a/components/newslit/README.md b/components/newslit/README.md new file mode 100644 index 0000000000000..c297eb1cfb2cf --- /dev/null +++ b/components/newslit/README.md @@ -0,0 +1,11 @@ +# Overview + +The Newslit API offers access to a robust news database, allowing you to pull in news articles, blog posts, and journalistic content from a variety of sources. Integrated with Pipedream, you can automate workflows to track brand mentions, monitor industry updates, or aggregate news for data analysis. The API's capabilities to filter by keyword, date, language, and more, combined with Pipedream's ability to connect to numerous other services, open up a wide range of potential automations. + +# Example Use Cases + +- **Content Monitoring and Alerting**: Use Newslit to monitor news articles for specific keywords relevant to your business or interests. When a new article is found, trigger a Pipedream workflow to send alerts through email, Slack, or another communication app of your choice. + +- **Data Aggregation for Analysis**: Aggregate news data on Pipedream for competitive analysis or trend tracking. Collect articles over time, filter them by certain criteria using Newslit, and store them in a database like PostgreSQL or Google Sheets for further analysis. + +- **Social Media Sharing**: Combine Newslit with social media platforms like Twitter or Facebook. When the API detects a newsworthy article related to your field, Pipedream can automatically share the article on your social media profiles, keeping your followers engaged with industry-relevant content. diff --git a/components/newsman/README.md b/components/newsman/README.md new file mode 100644 index 0000000000000..0db47506a7043 --- /dev/null +++ b/components/newsman/README.md @@ -0,0 +1,11 @@ +# Overview + +The Newsman API lets you automate and integrate email marketing efforts with ease. You can manage subscribers, send campaigns, and analyze your email marketing performance directly within Pipedream. This API is a gateway to create workflows that engage your audience, segment lists, and track results, combining the power of email marketing with other tools to create rich, automated tasks. + +# Example Use Cases + +- **Subscriber Sync Workflow**: Automatically sync new subscribers from your CRM, like Salesforce, to your Newsman subscriber lists. When a new contact is added to Salesforce, Pipedream triggers a workflow that adds the contact to a specified Newsman list, ensuring your audience is always up-to-date. + +- **Campaign Trigger Workflow**: Launch a Newsman email campaign based on customer behavior tracked in an e-commerce platform like Shopify. For instance, if a customer makes a purchase, Pipedream can kick off a workflow that sends a thank-you email or a series of follow-up emails via Newsman to keep engagement high. + +- **Performance Metrics Workflow**: Collect and analyze your Newsman campaign metrics by sending data to Google Sheets. After an email campaign is sent, use Pipedream to retrieve the campaign performance data from Newsman and log it in a spreadsheet, allowing for easy tracking and visualization of your marketing efforts. diff --git a/components/newsman/package.json b/components/newsman/package.json index 2aac8011bd050..4e6d52949e055 100644 --- a/components/newsman/package.json +++ b/components/newsman/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/nextcloud/README.md b/components/nextcloud/README.md new file mode 100644 index 0000000000000..8a01f495d4cef --- /dev/null +++ b/components/nextcloud/README.md @@ -0,0 +1,11 @@ +# Overview + +Nextcloud's API lets you tap into your self-hosted cloud storage and collaboration platform. With Pipedream, you can automate tasks like file management, user account operations, and accessing calendar and contacts. It's a treasure trove for creating workflows that sync, share, and manage data programmatically between Nextcloud and other apps. + +# Example Use Cases + +- **Automated File Synchronization:** Create a workflow that monitors a specific folder in Dropbox (or any other cloud storage service) for new files and automatically uploads them to a designated folder in Nextcloud. + +- **Contacts Sync Between Platforms:** Whenever a new contact is added to your Google Contacts, have a Pipedream workflow that pushes that contact into your Nextcloud Contacts, keeping your address books in sync across platforms. + +- **Calendar Event Alert System:** Set up a Pipedream workflow where, if a new event is added to your Nextcloud Calendar, a message with event details is sent out via Slack to keep your team informed and aligned. diff --git a/components/nextcloud/actions/create-share/create-share.mjs b/components/nextcloud/actions/create-share/create-share.mjs new file mode 100644 index 0000000000000..f5fa44a30e014 --- /dev/null +++ b/components/nextcloud/actions/create-share/create-share.mjs @@ -0,0 +1,30 @@ +import nextcloud from "../../nextcloud.app.mjs"; + +export default { + key: "nextcloud-create-share", + name: "Create Share", + description: "Creates a new share link from the specified path in Nextcloud. [See the documentation](https://docs.nextcloud.com/server/latest/developer_manual/_static/openapi.html#/operations/files_sharing-shareapi-create-share)", + version: "0.0.1", + type: "action", + props: { + nextcloud, + path: { + propDefinition: [ + nextcloud, + "path", + ], + description: "The path of the file or folder to share", + }, + }, + async run({ $ }) { + const { ocs: { data } } = await this.nextcloud.createShare({ + $, + data: { + path: this.path, + shareType: 3, + }, + }); + $.export("$summary", `Successfully created share link: ${data.url}`); + return data; + }, +}; diff --git a/components/nextcloud/actions/delete-share/delete-share.mjs b/components/nextcloud/actions/delete-share/delete-share.mjs new file mode 100644 index 0000000000000..a9e0188ae914c --- /dev/null +++ b/components/nextcloud/actions/delete-share/delete-share.mjs @@ -0,0 +1,26 @@ +import nextcloud from "../../nextcloud.app.mjs"; + +export default { + key: "nextcloud-delete-share", + name: "Delete Share", + description: "Deletes a specific share in Nextcloud. [See the documenation](https://docs.nextcloud.com/server/latest/developer_manual/_static/openapi.html#/operations/files_sharing-shareapi-delete-share)", + version: "0.0.1", + type: "action", + props: { + nextcloud, + shareId: { + propDefinition: [ + nextcloud, + "shareId", + ], + }, + }, + async run({ $ }) { + const response = await this.nextcloud.deleteShare({ + $, + shareId: this.shareId, + }); + $.export("$summary", `Share with ID ${this.shareId} successfully deleted`); + return response; + }, +}; diff --git a/components/nextcloud/actions/get-shares/get-shares.mjs b/components/nextcloud/actions/get-shares/get-shares.mjs new file mode 100644 index 0000000000000..1d45f3c2273b0 --- /dev/null +++ b/components/nextcloud/actions/get-shares/get-shares.mjs @@ -0,0 +1,31 @@ +import nextcloud from "../../nextcloud.app.mjs"; + +export default { + key: "nextcloud-get-shares", + name: "Get Shares", + description: "Retrieves a list of shares based on the specified criteria in Nextcloud. [See the documentation](https://docs.nextcloud.com/server/latest/developer_manual/_static/openapi.html#/operations/files_sharing-shareapi-get-shares)", + version: "0.0.1", + type: "action", + props: { + nextcloud, + path: { + propDefinition: [ + nextcloud, + "path", + ], + optional: true, + }, + }, + async run({ $ }) { + const { ocs: { data } } = await this.nextcloud.listShares({ + $, + params: { + path: this.path, + }, + }); + $.export("$summary", `Successfully retrieved ${data.length} share${data.length === 1 + ? "" + : "s"}`); + return data; + }, +}; diff --git a/components/nextcloud/nextcloud.app.mjs b/components/nextcloud/nextcloud.app.mjs new file mode 100644 index 0000000000000..72b48a7817cc9 --- /dev/null +++ b/components/nextcloud/nextcloud.app.mjs @@ -0,0 +1,73 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "nextcloud", + propDefinitions: { + shareId: { + type: "string", + label: "Share ID", + description: "The identifier of a share", + async options() { + const { ocs: { data } } = await this.listShares(); + return data?.map(({ + id: value, path: label, + }) => ({ + value, + label, + })) || []; + }, + }, + path: { + type: "string", + label: "Path", + description: "Get shares for a specific path", + }, + }, + methods: { + _baseUrl() { + return `${this.$auth.url}/ocs/v2.php/apps/files_sharing/api/v1`; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + + url: `${this._baseUrl()}${path}`, + headers: { + "OCS-APIRequest": "true", + }, + auth: { + username: `${this.$auth.username}`, + password: `${this.$auth.password}`, + }, + }); + }, + listShares(opts = {}) { + return this._makeRequest({ + path: "/shares", + ...opts, + }); + }, + createShare(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/shares", + ...opts, + }); + }, + deleteShare({ + shareId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/shares/${shareId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/nextcloud/package.json b/components/nextcloud/package.json new file mode 100644 index 0000000000000..59f55746bfbd9 --- /dev/null +++ b/components/nextcloud/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/nextcloud", + "version": "0.1.0", + "description": "Pipedream Nextcloud Components", + "main": "nextcloud.app.mjs", + "keywords": [ + "pipedream", + "nextcloud" + ], + "homepage": "https://pipedream.com/apps/nextcloud", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/nextcloud/sources/new-share-created/new-share-created.mjs b/components/nextcloud/sources/new-share-created/new-share-created.mjs new file mode 100644 index 0000000000000..58773d2203637 --- /dev/null +++ b/components/nextcloud/sources/new-share-created/new-share-created.mjs @@ -0,0 +1,67 @@ +import nextcloud from "../../nextcloud.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "nextcloud-new-share-created", + name: "New Share Created", + description: "Emit new event whenever a share is created in Nextcloud. [See the documentation](https://docs.nextcloud.com/server/latest/developer_manual/_static/openapi.html#/operations/files_sharing-shareapi-get-shares)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + nextcloud, + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + path: { + propDefinition: [ + nextcloud, + "path", + ], + optional: true, + }, + itemType: { + type: "string", + label: "Item Type", + description: "Filter results to only files or folders", + optional: true, + options: [ + "file", + "folder", + ], + }, + }, + methods: { + isRelevant(share) { + return !this.itemType || share.item_type === this.itemType; + }, + generateMeta(share) { + return { + id: share.id, + summary: `New Share created: ${share.path}`, + ts: share.stime, + }; + }, + }, + async run() { + const params = this.path + ? { + path: this.path, + } + : {}; + const { ocs: { data } } = await this.nextcloud.listShares({ + params, + }); + for (const item of data) { + if (this.isRelevant(item)) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + } + } + }, + sampleEmit, +}; diff --git a/components/nextcloud/sources/new-share-created/test-event.mjs b/components/nextcloud/sources/new-share-created/test-event.mjs new file mode 100644 index 0000000000000..041c498dcd42b --- /dev/null +++ b/components/nextcloud/sources/new-share-created/test-event.mjs @@ -0,0 +1,38 @@ +export default { + "id": "358", + "share_type": 3, + "uid_owner": "test@test.com", + "displayname_owner": "test@test.com", + "permissions": 17, + "can_edit": true, + "can_delete": true, + "stime": 1711420687, + "parent": null, + "expiration": null, + "token": "xLoyTbxpaR6fP8J", + "uid_file_owner": "test@test.com", + "note": "", + "label": "", + "displayname_file_owner": "test@test.com", + "path": "/Nextcloud intro.mp4", + "item_type": "file", + "item_permissions": 27, + "mimetype": "video/mp4", + "has_preview": false, + "storage_id": "home::test@test.com", + "storage": 1069, + "item_source": 457256, + "file_source": 457256, + "file_parent": 457235, + "file_target": "/Nextcloud intro.mp4", + "item_size": 3963036, + "item_mtime": 1711418296, + "share_with": null, + "share_with_displayname": "(Shared link)", + "password": null, + "send_password_by_talk": false, + "url": "https://kim.nl.tab.digital/s/xLoyTbxpaR6fP8J", + "mail_send": 0, + "hide_download": 0, + "attributes": null +} \ No newline at end of file diff --git a/components/nextdns/README.md b/components/nextdns/README.md index d261655c5e491..0115e456c64c7 100644 --- a/components/nextdns/README.md +++ b/components/nextdns/README.md @@ -1,13 +1,11 @@ # Overview -NextDNS provides an API that developers can use to programmatically manage DNS -records for their domains. This provides a way to automate tasks such as adding -new records when a website is created, or removing old records when a website -is taken down. +NextDNS API on Pipedream allows you to automate DNS management and enrich cybersecurity measures. This API offers control over DNS queries and filtering settings, making it possible to automate domain blocking, security rule updates, and log analysis. Utilizing Pipedream's serverless platform, you can integrate NextDNS with a wide range of services, without the need for a dedicated backend, to enhance network management and threat mitigation. -Here are some examples of what you can build using the NextDNS API: +# Example Use Cases -- A tool for automatically creating DNS records when a new website is created -- A tool for automatically removing DNS records when a website is taken down -- A tool for managing DNS records for multiple domains in one place -- A tool for monitoring DNS changes across multiple domains +- **Automate Security Updates**: When a new threat is identified by a threat intelligence platform (e.g., VirusTotal), an automated workflow can trigger NextDNS to immediately add the domain to a blacklist, ensuring network protection against emerging threats. + +- **Enhanced Reporting**: Combine NextDNS logs with a data visualization tool like Google Sheets or Data Studio. Set up a scheduled workflow to extract DNS query logs, parse them, and push the data into reports for a clear view of network traffic and blocked queries. + +- **IoT Device Management**: For a network with numerous IoT devices, use NextDNS API to manage device-specific DNS configurations. Integrate this with device management platforms to dynamically adjust DNS settings based on device status or location, optimizing performance and security. diff --git a/components/nextdoor/nextdoor.app.mjs b/components/nextdoor/nextdoor.app.mjs new file mode 100644 index 0000000000000..24e8246661805 --- /dev/null +++ b/components/nextdoor/nextdoor.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "nextdoor", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/nextdoor/package.json b/components/nextdoor/package.json new file mode 100644 index 0000000000000..8616aa3642a07 --- /dev/null +++ b/components/nextdoor/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/nextdoor", + "version": "0.0.1", + "description": "Pipedream Nextdoor Ads Components", + "main": "nextdoor.app.mjs", + "keywords": [ + "pipedream", + "nextdoor" + ], + "homepage": "https://pipedream.com/apps/nextdoor", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/nexudus/README.md b/components/nexudus/README.md new file mode 100644 index 0000000000000..de2ab86605f69 --- /dev/null +++ b/components/nexudus/README.md @@ -0,0 +1,11 @@ +# Overview + +The Nexudus API is a powerful interface for managing coworking spaces, enabling you to automate tasks related to billing, booking, customer management, and more. With Pipedream, you can connect the Nexudus API to hundreds of other apps and create custom automation workflows. Whether you need to sync member data, automate billing processes, or manage space availability, Pipedream provides the tools to streamline operations and integrate with other services seamlessly. + +# Example Use Cases + +- **Sync New Members to CRM**: When a new member is added in Nexudus, this workflow triggers and adds the member's details to your preferred CRM system, keeping your sales and marketing teams updated in real-time. + +- **Automate Billing Alerts**: Set up a workflow where whenever a payment fails or is overdue in Nexudus, Pipedream sends an alert to Slack, email, or SMS, notifying the relevant team to take immediate action. + +- **Manage Event Bookings**: Automate event management by triggering a workflow that posts new Nexudus event bookings to a Google Calendar, and sends a confirmation email to the attendee using a service like SendGrid. diff --git a/components/nexudus/nexudus.app.mjs b/components/nexudus/nexudus.app.mjs new file mode 100644 index 0000000000000..a5738ab3cab51 --- /dev/null +++ b/components/nexudus/nexudus.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "nexudus", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/nexudus/package.json b/components/nexudus/package.json new file mode 100644 index 0000000000000..14397ede4f266 --- /dev/null +++ b/components/nexudus/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/nexudus", + "version": "0.0.1", + "description": "Pipedream Nexudus Components", + "main": "nexudus.app.mjs", + "keywords": [ + "pipedream", + "nexudus" + ], + "homepage": "https://pipedream.com/apps/nexudus", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/nexweave/README.md b/components/nexweave/README.md new file mode 100644 index 0000000000000..0ef8d3d535b0d --- /dev/null +++ b/components/nexweave/README.md @@ -0,0 +1,11 @@ +# Overview + +The Nexweave API lets you create personalized images and videos at scale, transforming static designs into dynamic templates. With it, you can automate the generation of customized content for email campaigns, social media, or advertising, by integrating user-specific data to create a more engaging and personalized experience. On Pipedream, you can harness this power to craft serverless workflows that trigger Nexweave's capabilities in response to various events and integrate them with other apps and services. + +# Example Use Cases + +- **Personalized Email Campaigns**: Integrate Nexweave with an email service like SendGrid on Pipedream. When a new contact is added to your CRM, automatically generate a personalized image or video using Nexweave and send it via email through SendGrid, enhancing your email marketing with bespoke content. + +- **Dynamic Social Media Content**: Combine Nexweave with Twitter's API on Pipedream to create and post personalized images or videos. Set up a workflow where a new follower on Twitter receives a welcome message with a customized image, increasing engagement and personal connection on social media. + +- **Custom Advertisements for E-commerce**: Use Nexweave together with Shopify's API on Pipedream to automatically create personalized product videos or images whenever a new product is added to your Shopify store. These can then be used in your advertising campaigns to showcase products in a way that resonates with individual customers. diff --git a/components/ngrok/actions/create-https-edge/create-https-edge.mjs b/components/ngrok/actions/create-https-edge/create-https-edge.mjs new file mode 100644 index 0000000000000..cef5483f17643 --- /dev/null +++ b/components/ngrok/actions/create-https-edge/create-https-edge.mjs @@ -0,0 +1,57 @@ +import app from "../../ngrok.app.mjs"; + +export default { + key: "ngrok-create-https-edge", + name: "Create HTTPS Edge", + description: "Create an HTTPS Edge. [See the documentation](https://ngrok.com/docs/api/resources/edges-https/#create-https-edge).", + version: "0.0.1", + type: "action", + props: { + app, + description: { + propDefinition: [ + app, + "description", + ], + }, + hostports: { + propDefinition: [ + app, + "hostports", + ], + }, + metadata: { + propDefinition: [ + app, + "metadata", + ], + }, + }, + methods: { + createHTTPSEdge(args = {}) { + return this.app.post({ + path: "/edges/https", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createHTTPSEdge, + description, + hostports, + metadata, + } = this; + + const response = await createHTTPSEdge({ + $, + data: { + description, + hostports, + metadata: metadata && JSON.stringify(metadata), + }, + }); + $.export("$summary", `Successfully created new HTTPS edge with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/ngrok/actions/delete-https-edge/delete-https-edge.mjs b/components/ngrok/actions/delete-https-edge/delete-https-edge.mjs new file mode 100644 index 0000000000000..d5ef8c4adafb4 --- /dev/null +++ b/components/ngrok/actions/delete-https-edge/delete-https-edge.mjs @@ -0,0 +1,42 @@ +import app from "../../ngrok.app.mjs"; + +export default { + key: "ngrok-delete-https-edge", + name: "Delete HTTPS Edge", + description: "Delete an HTTPS Edge. [See the documentation](https://ngrok.com/docs/api/resources/edges-https/#delete-https-edge).", + version: "0.0.1", + type: "action", + props: { + app, + edgeId: { + propDefinition: [ + app, + "edgeId", + ], + }, + }, + methods: { + deleteHTTPSEdge({ + edgeId, ...args + } = {}) { + return this.app.delete({ + path: `/edges/https/${edgeId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + deleteHTTPSEdge, + edgeId, + } = this; + await deleteHTTPSEdge({ + $, + edgeId, + }); + $.export("$summary", "Successfully deleted HTTPS edge."); + return { + success: true, + }; + }, +}; diff --git a/components/ngrok/actions/get-https-edge/get-https-edge.mjs b/components/ngrok/actions/get-https-edge/get-https-edge.mjs new file mode 100644 index 0000000000000..522d1622682a2 --- /dev/null +++ b/components/ngrok/actions/get-https-edge/get-https-edge.mjs @@ -0,0 +1,40 @@ +import app from "../../ngrok.app.mjs"; + +export default { + key: "ngrok-get-https-edge", + name: "Get HTTPS Edge", + description: "Get the details of an HTTPS Edge. [See the documentation](https://ngrok.com/docs/api/resources/edges-https/#get-https-edge).", + version: "0.0.1", + type: "action", + props: { + app, + edgeId: { + propDefinition: [ + app, + "edgeId", + ], + }, + }, + methods: { + getHTTPSEdge({ + edgeId, ...args + } = {}) { + return this.app._makeRequest({ + path: `/edges/https/${edgeId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + getHTTPSEdge, + edgeId, + } = this; + const response = await getHTTPSEdge({ + $, + edgeId, + }); + $.export("$summary", `Successfully retrieved HTTPS edge with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/ngrok/actions/update-https-edge/update-https-edge.mjs b/components/ngrok/actions/update-https-edge/update-https-edge.mjs new file mode 100644 index 0000000000000..744fb9348f12a --- /dev/null +++ b/components/ngrok/actions/update-https-edge/update-https-edge.mjs @@ -0,0 +1,67 @@ +import app from "../../ngrok.app.mjs"; + +export default { + key: "ngrok-update-https-edge", + name: "Update HTTPS Edge", + description: "Updates an HTTPS Edge. [See the documentation](https://ngrok.com/docs/api/resources/edges-https/#update-https-edge).", + version: "0.0.1", + type: "action", + props: { + app, + edgeId: { + propDefinition: [ + app, + "edgeId", + ], + }, + description: { + propDefinition: [ + app, + "description", + ], + }, + hostports: { + propDefinition: [ + app, + "hostports", + ], + }, + metadata: { + propDefinition: [ + app, + "metadata", + ], + }, + }, + methods: { + updateHTTPSEdge({ + edgeId, ...args + } = {}) { + return this.app.patch({ + path: `/edges/https/${edgeId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + updateHTTPSEdge, + edgeId, + description, + hostports, + metadata, + } = this; + + const response = await updateHTTPSEdge({ + $, + edgeId, + data: { + description, + hostports, + metadata: metadata && JSON.stringify(metadata), + }, + }); + $.export("$summary", `Successfully updated Agent Ingress ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/ngrok/ngrok.app.mjs b/components/ngrok/ngrok.app.mjs new file mode 100644 index 0000000000000..d032d2f569bf4 --- /dev/null +++ b/components/ngrok/ngrok.app.mjs @@ -0,0 +1,102 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "ngrok", + propDefinitions: { + description: { + type: "string", + label: "Description", + description: "What this edge will be used for.", + }, + hostports: { + type: "string[]", + label: "Host Ports", + description: "Hostports served by this edge. Eg: `example.com:443`", + optional: true, + }, + metadata: { + type: "object", + label: "Metadata", + description: "The metadata of the agent ingress. Arbitrary user-defined machine-readable data.", + optional: true, + }, + edgeId: { + type: "string", + label: "Edge ID", + description: "The ID of the edge to update.", + async options({ prevContext: { url } }) { + if (url === null) { + return []; + } + const { + next_page_uri: nextPageUri, + https_edges: edges, + } = await this.listHTTPSEdges({ + url, + params: { + limit: 10, + }, + }); + const options = edges.map(({ + id: value, description: label, + }) => ({ + label, + value, + })); + return { + options, + context: { + url: nextPageUri, + }, + }; + }, + }, + }, + methods: { + getUrl(path) { + return `https://api.ngrok.com${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Authorization": `Bearer ${this.$auth.api_key}`, + "Content-Type": "application/json", + "Ngrok-Version": "2", + }; + }, + _makeRequest({ + $ = this, url, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: url || this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + patch(args = {}) { + return this._makeRequest({ + method: "PATCH", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + listHTTPSEdges(args = {}) { + return this._makeRequest({ + path: "/edges/https", + ...args, + }); + }, + }, +}; diff --git a/components/ngrok/package.json b/components/ngrok/package.json new file mode 100644 index 0000000000000..cb986e652731f --- /dev/null +++ b/components/ngrok/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/ngrok", + "version": "0.1.0", + "description": "Pipedream ngrok Components", + "main": "ngrok.app.mjs", + "keywords": [ + "pipedream", + "ngrok" + ], + "homepage": "https://pipedream.com/apps/ngrok", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" + } +} diff --git a/components/niceboard/.gitignore b/components/niceboard/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/niceboard/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/niceboard/README.md b/components/niceboard/README.md index 12f6ef4c1c38b..e4ac02a531aab 100644 --- a/components/niceboard/README.md +++ b/components/niceboard/README.md @@ -1,24 +1,11 @@ # Overview -The Niceboard API is a way for developers to integrate their applications with -Niceboard, a marketplace for digital teams. The API enables developers to query -and manipulate data, as well as observe changes in the data in real-time. The -API is designed for developers, offering a streamlined and intuitive way to -integrate with Niceboard. +The Niceboard API allows for the seamless integration and automation of job board functionalities. With this API, you can programmatically create job listings, manage applications, and handle user interactions on your Niceboard-powered job platform. Leveraging Pipedream's capabilities, you can connect Niceboard to a myriad of other apps and services to streamline job board operations, enhance user engagement, and automate repetitive tasks. -With the Niceboard API, developers can build virtual whiteboard-style -applications for digital teams. These applications can be used to create tasks, -collaborate on projects, share files, and see the progress of their work. -Additionally, Niceboard’s API allows developers to access user data, store -settings and preferences, and track activity. +# Example Use Cases -Here are some examples of what you can build with the Niceboard API: +- **Dynamic Job Posting via Google Sheets**: Automate the creation of job listings on Niceboard directly from a Google Sheets spreadsheet. Every time a new row is added to the spreadsheet, a Pipedream workflow triggers, creating a new job listing with the details specified in the row. This helps HR teams manage job postings in a familiar interface without manual entry. -- Collaborative task management applications -- Real-time project tracking applications -- Dashboards to visualize data -- Chatbot applications -- Documents and file sharing apps -- Custom integrations with other services -- Scheduling and calendar applications -- Data analysis tools +- **Slack Notification for New Applications**: Set up a workflow where Pipedream listens for new job applications on Niceboard and automatically sends a notification to a designated Slack channel. This ensures that the hiring team is immediately informed about new candidates and can respond quicker to promising applicants. + +- **Automated Candidate Follow-Up Emails**: Configure a Pipedream workflow that triggers an automated email response when a candidate applies for a job. Connect Niceboard to a service like SendGrid or another email provider. This can provide applicants with immediate confirmation that their application has been received, and supply further details about next steps or additional required documentation. diff --git a/components/niceboard/actions/create-category/create-category.mjs b/components/niceboard/actions/create-category/create-category.mjs new file mode 100644 index 0000000000000..52ab575b5e428 --- /dev/null +++ b/components/niceboard/actions/create-category/create-category.mjs @@ -0,0 +1,34 @@ +import niceboard from "../../niceboard.app.mjs"; + +export default { + key: "niceboard-create-category", + name: "Create Category", + description: "Creates a new job category within Niceboard.", + version: "0.0.1", + type: "action", + props: { + niceboard, + niceboardUrl: { + propDefinition: [ + niceboard, + "niceboardUrl", + ], + }, + name: { + type: "string", + label: "Category Name", + description: "The name of the job category to be created", + }, + }, + async run({ $ }) { + const response = await this.niceboard.createCategory({ + $, + niceboardUrl: this.niceboardUrl, + data: { + name: this.name, + }, + }); + $.export("$summary", `Successfully created category with name "${this.name}"`); + return response; + }, +}; diff --git a/components/niceboard/actions/create-job/create-job.mjs b/components/niceboard/actions/create-job/create-job.mjs new file mode 100644 index 0000000000000..9b9d7d8a90889 --- /dev/null +++ b/components/niceboard/actions/create-job/create-job.mjs @@ -0,0 +1,114 @@ +import niceboard from "../../niceboard.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "niceboard-create-job", + name: "Create Job", + description: "Creates a new job posting within the Niceboard app.", + version: "0.0.1", + type: "action", + props: { + niceboard, + niceboardUrl: { + propDefinition: [ + niceboard, + "niceboardUrl", + ], + }, + title: { + propDefinition: [ + niceboard, + "title", + ], + }, + description: { + propDefinition: [ + niceboard, + "description", + ], + }, + companyId: { + propDefinition: [ + niceboard, + "companyId", + (c) => ({ + niceboardUrl: c.niceboardUrl, + }), + ], + }, + jobTypeId: { + propDefinition: [ + niceboard, + "jobTypeId", + (c) => ({ + niceboardUrl: c.niceboardUrl, + }), + ], + }, + categoryId: { + propDefinition: [ + niceboard, + "categoryId", + (c) => ({ + niceboardUrl: c.niceboardUrl, + }), + ], + optional: true, + }, + locationId: { + propDefinition: [ + niceboard, + "locationId", + (c) => ({ + niceboardUrl: c.niceboardUrl, + }), + ], + optional: true, + }, + minSalary: { + propDefinition: [ + niceboard, + "minSalary", + ], + }, + maxSalary: { + propDefinition: [ + niceboard, + "maxSalary", + ], + }, + salaryTimeframe: { + propDefinition: [ + niceboard, + "salaryTimeframe", + ], + }, + }, + async run({ $ }) { + if ((this.minSalary || this.maxSalary) && !this.salaryTimeframe) { + throw new ConfigurationError("Salary Timeframe is required if Minimum Salary or Maximum Salary is entered"); + } + + const response = await this.niceboard.createJob({ + $, + niceboardUrl: this.niceboardUrl, + data: { + title: this.title, + description_html: this.description, + company_id: this.companyId, + jobtype_id: this.jobTypeId, + category_id: this.categoryId, + location_id: this.locationId, + salary_min: this.minSalary, + salary_max: this.maxSalary, + salary_timeframe: this.salaryTimeframe, + apply_by_form: true, + }, + }); + + if (response?.job?.id) { + $.export("$summary", `Successfully created job with ID: ${response.job.id}`); + } + return response; + }, +}; diff --git a/components/niceboard/actions/update-job/update-job.mjs b/components/niceboard/actions/update-job/update-job.mjs new file mode 100644 index 0000000000000..d64c15a3dffbb --- /dev/null +++ b/components/niceboard/actions/update-job/update-job.mjs @@ -0,0 +1,122 @@ +import niceboard from "../../niceboard.app.mjs"; + +export default { + key: "niceboard-update-job", + name: "Update Job", + description: "Updates an existing job posting within the Niceboard app.", + version: "0.0.1", + type: "action", + props: { + niceboard, + niceboardUrl: { + propDefinition: [ + niceboard, + "niceboardUrl", + ], + }, + jobId: { + propDefinition: [ + niceboard, + "jobId", + (c) => ({ + niceboardUrl: c.niceboardUrl, + }), + ], + }, + title: { + propDefinition: [ + niceboard, + "title", + ], + optional: true, + }, + description: { + propDefinition: [ + niceboard, + "description", + ], + optional: true, + }, + companyId: { + propDefinition: [ + niceboard, + "companyId", + (c) => ({ + niceboardUrl: c.niceboardUrl, + }), + ], + optional: true, + }, + jobTypeId: { + propDefinition: [ + niceboard, + "jobTypeId", + (c) => ({ + niceboardUrl: c.niceboardUrl, + }), + ], + optional: true, + }, + categoryId: { + propDefinition: [ + niceboard, + "categoryId", + (c) => ({ + niceboardUrl: c.niceboardUrl, + }), + ], + optional: true, + }, + locationId: { + propDefinition: [ + niceboard, + "locationId", + (c) => ({ + niceboardUrl: c.niceboardUrl, + }), + ], + optional: true, + }, + minSalary: { + propDefinition: [ + niceboard, + "minSalary", + ], + }, + maxSalary: { + propDefinition: [ + niceboard, + "maxSalary", + ], + }, + salaryTimeframe: { + propDefinition: [ + niceboard, + "salaryTimeframe", + ], + }, + }, + async run({ $ }) { + const response = await this.niceboard.updateJob({ + $, + niceboardUrl: this.niceboardUrl, + jobId: this.jobId, + data: { + title: this.title, + description_html: this.description, + company_id: this.companyId, + jobtype_id: this.jobTypeId, + category_id: this.categoryId, + location_id: this.locationId, + salary_min: this.minSalary, + salary_max: this.maxSalary, + salary_timeframe: this.salaryTimeframe, + }, + }); + + if (response?.job?.id) { + $.export("$summary", `Successfully updated job with ID: ${response.job.id}`); + } + return response; + }, +}; diff --git a/components/niceboard/app/niceboard.app.ts b/components/niceboard/app/niceboard.app.ts deleted file mode 100644 index 8c032b44f16a7..0000000000000 --- a/components/niceboard/app/niceboard.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "niceboard", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); diff --git a/components/niceboard/niceboard.app.mjs b/components/niceboard/niceboard.app.mjs new file mode 100644 index 0000000000000..5d292f4ba0e0c --- /dev/null +++ b/components/niceboard/niceboard.app.mjs @@ -0,0 +1,219 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "niceboard", + propDefinitions: { + niceboardUrl: { + type: "string", + label: "Niceboard URL", + description: "If your Niceboard account uses a custom domain or subdomain, enter it here. Otherwise, enter your Niceboard URL (Manage Board -> General -> Base Settings), and include the full domain. Example: `myboard.niceboard.co`", + }, + companyId: { + type: "string", + label: "Company ID", + description: "Identifier of the company of the job posting", + async options({ niceboardUrl }) { + const { results: { companies } } = await this.listCompanies({ + niceboardUrl, + }); + return companies?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + jobTypeId: { + type: "string", + label: "Job Type ID", + description: "Identifier of the type of the job posting", + async options({ niceboardUrl }) { + const { results: { jobtypes } } = await this.listJobTypes({ + niceboardUrl, + }); + return jobtypes?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + categoryId: { + type: "string", + label: "Category ID", + description: "Identifier of the category of the job posting", + async options({ niceboardUrl }) { + const { results: { categories } } = await this.listCategories({ + niceboardUrl, + }); + return categories?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + locationId: { + type: "string", + label: "Location ID", + description: "Identifier of the location of the job posting", + async options({ niceboardUrl }) { + const { results: { locations } } = await this.listLocations({ + niceboardUrl, + }); + return locations?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + jobId: { + type: "string", + label: "Job ID", + description: "The ID of the job", + async options({ + niceboardUrl, page, + }) { + const { results: { jobs } } = await this.listJobs({ + niceboardUrl, + params: { + page: page + 1, + }, + }); + return jobs?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })) || []; + }, + }, + title: { + type: "string", + label: "Title", + description: "The title of the job posting", + }, + description: { + type: "string", + label: "Description", + description: "The description of the job posting", + }, + minSalary: { + type: "integer", + label: "Minimum Salary", + description: "Minimum salary value", + optional: true, + }, + maxSalary: { + type: "integer", + label: "Maximum Salary", + description: "Maximum salary value", + optional: true, + }, + salaryTimeframe: { + type: "string", + label: "Salary Timeframe", + description: "Required if minimum or maximum salary values submitted", + options: [ + "annually", + "monthly", + "hourly", + "weekly", + ], + optional: true, + }, + }, + methods: { + _baseUrl(niceboardUrl) { + return `https://${niceboardUrl}/api/v1`; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + niceboardUrl, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl(niceboardUrl)}${path}`, + params: { + ...params, + key: this.$auth.secret_key, + }, + }); + }, + listCompanies(opts = {}) { + return this._makeRequest({ + path: "/companies", + ...opts, + }); + }, + listJobTypes(opts = {}) { + return this._makeRequest({ + path: "/jobtypes", + ...opts, + }); + }, + listCategories(opts = {}) { + return this._makeRequest({ + path: "/categories", + ...opts, + }); + }, + listLocations(opts = {}) { + return this._makeRequest({ + path: "/locations", + ...opts, + }); + }, + listJobs(opts = {}) { + return this._makeRequest({ + path: "/jobs", + ...opts, + }); + }, + listJobAlerts(opts = {}) { + return this._makeRequest({ + path: "/jobalerts", + ...opts, + }); + }, + listJobSeekers(opts = {}) { + return this._makeRequest({ + path: "/jobseekers", + ...opts, + }); + }, + createJob(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/jobs", + ...opts, + }); + }, + createCategory(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/categories", + ...opts, + }); + }, + updateJob({ + jobId, ...opts + }) { + return this._makeRequest({ + method: "PATCH", + path: `/jobs/${jobId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/niceboard/package.json b/components/niceboard/package.json index ea46c229ddded..a1e9895f9e2e9 100644 --- a/components/niceboard/package.json +++ b/components/niceboard/package.json @@ -1,18 +1,18 @@ { "name": "@pipedream/niceboard", - "version": "0.0.3", + "version": "0.1.0", "description": "Pipedream Niceboard Components", - "main": "dist/app/niceboard.app.mjs", + "main": "niceboard.app.mjs", "keywords": [ "pipedream", "niceboard" ], - "files": [ - "dist" - ], "homepage": "https://pipedream.com/apps/niceboard", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/niceboard/sources/common/base.mjs b/components/niceboard/sources/common/base.mjs new file mode 100644 index 0000000000000..1d8f1f5e49ea8 --- /dev/null +++ b/components/niceboard/sources/common/base.mjs @@ -0,0 +1,85 @@ +import niceboard from "../../niceboard.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + niceboard, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + niceboardUrl: { + propDefinition: [ + niceboard, + "niceboardUrl", + ], + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + async *paginate({ + fn, max, + }) { + const lastId = this._getLastId(); + const params = { + page: 1, + }; + let hasMore, count = 0; + do { + const response = await fn({ + niceboardUrl: this.niceboardUrl, + params, + }); + const results = this.getResults(response); + const totalPages = this.getTotalPages(response); + for (const item of results) { + if (+item.id > lastId) { + yield item; + if (max && ++count >= max) { + return; + } + } + } + hasMore = params.pages < totalPages; + params.page++; + } while (hasMore); + }, + getMaxResults(results, max) { + if (max && results.length > max) { + return results.slice(-1 * max); + } + return results; + }, + emitEvents(events) { + events.forEach((event) => { + const meta = this.generateMeta(event); + this.$emit(event, meta); + }); + }, + getResults() { + throw new Error("getResults is not implemented"); + }, + getTotalPages() { + throw new Error("getTotalPages is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/niceboard/sources/new-job-alert/new-job-alert.mjs b/components/niceboard/sources/new-job-alert/new-job-alert.mjs new file mode 100644 index 0000000000000..8668f95d0dd50 --- /dev/null +++ b/components/niceboard/sources/new-job-alert/new-job-alert.mjs @@ -0,0 +1,39 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "niceboard-new-job-alert", + name: "New Job Alert", + description: "Emit new event when a new job alert email subscription is created", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + generateMeta(jobalert) { + return { + id: jobalert.id, + summary: `New Job Alert ID: ${jobalert.id}`, + ts: Date.now(), + }; + }, + async processEvent(max) { + let lastId = this._getLastId(); + const { results } = await this.niceboard.listJobAlerts({ + niceboardUrl: this.niceboardUrl, + }); + let jobAlerts = []; + for (const jobAlert of results) { + if (+jobAlert.id > lastId) { + jobAlerts.push(jobAlert); + } + } + if (!jobAlerts?.length) { + return; + } + jobAlerts = this.getMaxResults(jobAlerts, max); + this._setLastId(+jobAlerts[jobAlerts.length - 1].id); + this.emitEvents(jobAlerts); + }, + }, +}; diff --git a/components/niceboard/sources/new-job-seeker/new-job-seeker.mjs b/components/niceboard/sources/new-job-seeker/new-job-seeker.mjs new file mode 100644 index 0000000000000..271b8e79b0e65 --- /dev/null +++ b/components/niceboard/sources/new-job-seeker/new-job-seeker.mjs @@ -0,0 +1,42 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "niceboard-new-job-seeker", + name: "New Job Seeker", + description: "Emit new event when a new job seeker account is registered", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + async processEvent(max) { + let jobSeekers = []; + const newJobSeekers = this.paginate({ + fn: this.niceboard.listJobSeekers, + }); + for await (const jobseeker of newJobSeekers) { + jobSeekers.push(jobseeker); + } + if (!jobSeekers?.length) { + return; + } + jobSeekers = this.getMaxResults(jobSeekers, max); + this._setLastId(+jobSeekers[jobSeekers.length - 1].id); + this.emitEvents(jobSeekers); + }, + getResults(response) { + return response.jobs; + }, + getTotalPages(response) { + return response.total_pages; + }, + generateMeta(jobseeker) { + return { + id: jobseeker.id, + summary: `New Job Seeker: ${jobseeker.first_name} ${jobseeker.last_name}`, + ts: Date.parse(jobseeker.last_indexed_at), + }; + }, + }, +}; diff --git a/components/niceboard/sources/new-job/new-job.mjs b/components/niceboard/sources/new-job/new-job.mjs new file mode 100644 index 0000000000000..b4a1afc1158f9 --- /dev/null +++ b/components/niceboard/sources/new-job/new-job.mjs @@ -0,0 +1,42 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "niceboard-new-job", + name: "New Job Published", + description: "Emit new event each time a new job is published in Niceboard", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + async processEvent(max) { + const jobs = []; + const newJobs = this.paginate({ + fn: this.niceboard.listJobs, + max, + }); + for await (const job of newJobs) { + jobs.push(job); + } + if (!jobs?.length) { + return; + } + this._setLastId(+jobs[0].id); + this.emitEvents(jobs); + }, + getResults({ results }) { + return results.jobs; + }, + getTotalPages({ results }) { + return results.pages.total; + }, + generateMeta(job) { + return { + id: job.id, + summary: `New Job: ${job.title}`, + ts: Date.parse(job.published_at), + }; + }, + }, +}; diff --git a/components/nicereply/README.md b/components/nicereply/README.md index ca9ac29288b36..a5590194cebc4 100644 --- a/components/nicereply/README.md +++ b/components/nicereply/README.md @@ -1,20 +1,11 @@ # Overview -With Nicereply API, you can create powerful customer feedback solutions and -measure customer satisfaction scores in different ways. +The Nicereply API enables the integration of customer satisfaction data into various business workflows. With its API, you can automate the collection of customer feedback, analyze customer sentiment, track agent performance, and generate actionable insights. Leveraging Pipedream’s capabilities, you can create powerful automations that respond to Nicereply events, synchronize data across platforms, and trigger actions in other applications, enriching CRM data or streamlining support workflows. -Whether you're a developer, marketer, customer service agent, or product -manager, the Nicereply API makes it easy to set up and manage your customer -feedback solutions. +# Example Use Cases -Here are some examples of what you can do with Nicereply API: +- **Customer Feedback to Slack Alerts**: - Trigger a Slack notification when a new rating is received in Nicereply. This allows teams to respond promptly to customer feedback, particularly negative ratings, and take immediate action to resolve any issues. -- Automatically send surveys to customers and collect customer feedback. -- Display customer satisfaction scores in various ways for different teams. -- Track customer support metrics and trends over time. -- Generate reports on customer satisfaction and support efficiency. -- Integrate with third-party applications to extend your customer feedback - solutions. -- Design custom interaction forms and data fields. -- Automate customer follow-ups and notifications. -- Monitor customer sentiment across multiple channels. +- **Sync Ratings with CRM**: - Ingest customer satisfaction ratings into a CRM like Salesforce or HubSpot. Use this data to enrich customer profiles, allowing sales or support teams to tailor their interactions based on customer sentiment. + +- **Automated Follow-Up Emails**: - Set up an automated workflow that sends a follow-up email through an email platform like SendGrid when a customer leaves a rating below a certain threshold. This can help address customer concerns quickly and improve overall satisfaction. diff --git a/components/nifty/README.md b/components/nifty/README.md new file mode 100644 index 0000000000000..23b1828cc7541 --- /dev/null +++ b/components/nifty/README.md @@ -0,0 +1,11 @@ +# Overview + +The Nifty API allows for streamlined project management, facilitating task automation and data synchronization within Pipedream. By integrating with the Nifty API, you can automate workflows involving project tracking, task assignments, and progress monitoring, effectively reducing manual overhead. On Pipedream, the API's endpoints can trigger actions, respond to changes, and update records across different services, bringing cohesion to your project management ecosystem. + +# Example Use Cases + +- **Automate Task Creation for New Deals**: When a new deal is closed in a CRM like Salesforce, automatically create a corresponding project and tasks in Nifty to kickstart the workflow without manual entry. + +- **Sync Project Updates with Communication Platforms**: Whenever a Nifty project status updates, post a message to a Slack channel to keep the team informed in real-time, ensuring everyone is aligned with the latest project developments. + +- **Consolidate Timesheet Entries**: At the end of each day, collect time tracking data from Nifty and compile it into a Google Sheets document for easy review and reporting, streamlining the timesheet management process. diff --git a/components/niftyimages/README.md b/components/niftyimages/README.md index 8f75dffab944a..11e2c7072fa7a 100644 --- a/components/niftyimages/README.md +++ b/components/niftyimages/README.md @@ -1,19 +1,11 @@ # Overview -NiftyImages is an easy-to-use API for building custom images, GIFs, and videos -for all your online needs. You can create, manage, and personalize your media -for any platform, from emails and advertising campaigns to webpages and emails. -With just a few lines of code, you can create stunning and unique visuals for -your project or product. +The NiftyImages API allows for the dynamic creation and customization of images for email marketing campaigns. By automating image personalization, users can create unique, engaging visuals that resonate with each recipient. It's possible to insert text into images, generate countdown timers, and manipulate image URLs on the fly, enhancing the visual appeal and relevance of marketing messages. -Here are some examples of what you can build using the NiftyImages API: +# Example Use Cases -- Custom branded imagery with your company logo and colors -- Ad campaigns featuring eye-catching visuals -- Animated GIFs to add more dynamic features to your site -- Custom memes and videos to reach a larger audience and keep them engaged -- Interactive images to bring a fresh, modern look to your page -- Interactive polls to increase engagement and increase customer feedback -- Personalized videos and images to help tell a story in a more interactive way -- Showcasing product features with interactive GIFs and videos -- Add fun and interactive elements to increase the lifespan of your content +- **Personalized Email Campaigns**: Use Pipedream to trigger a workflow whenever a new subscriber is added to your Mailchimp list. The workflow fetches the subscriber's information, generates a personalized image using NiftyImages, and sends a welcoming email with the customized image through SendGrid. + +- **Event Countdowns**: Set up a Pipedream workflow that integrates with Eventbrite. Whenever a new event is created, generate a countdown image with NiftyImages and post it automatically to your company's Twitter account, creating buzz and urgency. + +- **Abandoned Cart Reminders**: Connect NiftyImages to an e-commerce platform like Shopify via Pipedream. When a cart is abandoned, create a personalized reminder image featuring the products left in the cart and email it to the customer to encourage completion of the purchase. diff --git a/components/niftykit/README.md b/components/niftykit/README.md new file mode 100644 index 0000000000000..9148372716fe1 --- /dev/null +++ b/components/niftykit/README.md @@ -0,0 +1,11 @@ +# Overview + +The NiftyKit API offers a platform to create, sell, and manage digital collectibles on the blockchain. Users can mint non-fungible tokens (NFTs), set up drop campaigns, and handle various aspects of the NFT lifecycle. Integrating NiftyKit with Pipedream enables users to automate workflows related to NFTs, like minting in response to events, updating listings based on market conditions, or synchronizing NFT ownership data with other databases. + +# Example Use Cases + +- **Automated NFT Minting on Sale**: When a sale is confirmed via a payment gateway like Stripe, Pipedream can listen for this event and trigger an automated NFT minting process through NiftyKit, assigning the digital collectible to the buyer's wallet address. + +- **Dynamic Pricing Based on Market Conditions**: Connect to a cryptocurrency price feed API to monitor fluctuations in the market. Use Pipedream to adjust the price of NFTs on NiftyKit in real-time, ensuring your pricing strategies remain competitive and responsive. + +- **NFT Ownership Verification for Exclusive Content**: Leverage NiftyKit's API in Pipedream to verify NFT ownership before granting access to exclusive content. When a user requests access, Pipedream can check their wallet against the ledger on NiftyKit to confirm they own a specific NFT before unlocking content. diff --git a/components/nightfall_ai/README.md b/components/nightfall_ai/README.md new file mode 100644 index 0000000000000..f0033a2eef225 --- /dev/null +++ b/components/nightfall_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +The Nightfall.ai API excels in identifying and managing sensitive information exposed in text and file payloads. With Pipedream's serverless integration platform, you can wire Nightfall.ai into a variety of workflows to automate data protection and compliance processes. By tapping into Nightfall's machine learning capabilities, you can build workflows that scan and react to data in motion, ensuring sensitive info like credit card numbers, passwords, or Personally Identifiable Information (PII) is caught and handled securely. + +# Example Use Cases + +- **Content Compliance on Social Media**: Use Nightfall.ai with Twitter's API to monitor and filter out sensitive content from user posts. When Pipedream detects a tweet containing PII, Nightfall.ai can assess it, and if needed, trigger a moderation action or alert. + +- **Data Leakage Prevention in Cloud Storage**: Integrate Nightfall.ai with Google Drive to scan documents for sensitive data. When a new file is uploaded to Drive, Pipedream triggers Nightfall.ai to analyze it, and if it contains sensitive info, the workflow can automatically move the file to a secure location and notify administrators. + +- **Email Filtering and Alerting**: Combine Nightfall.ai with the email service, like Gmail, to safeguard against sensitive information leaks in emails. When inbound emails are received, Pipedream can invoke Nightfall.ai to scan the content. If high-risk information is found, Pipedream can flag the email and send an alert to the security team for review. diff --git a/components/nile_database/actions/create-user/create-user.mjs b/components/nile_database/actions/create-user/create-user.mjs new file mode 100644 index 0000000000000..18920f39208d2 --- /dev/null +++ b/components/nile_database/actions/create-user/create-user.mjs @@ -0,0 +1,57 @@ +import nile from "../../nile_database.app.mjs"; + +export default { + key: "nile_database-create-user", + name: "Create User", + description: "Create a new database user by providing an email address and password. [See the documentation](https://www.thenile.dev/docs/reference/api-reference/users/create-user)", + version: "0.0.2", + type: "action", + props: { + nile, + workspace: { + propDefinition: [ + nile, + "workspace", + ], + }, + database: { + propDefinition: [ + nile, + "database", + (c) => ({ + workspace: c.workspace, + }), + ], + }, + email: { + type: "string", + label: "Email", + description: "Email address of the user", + }, + password: { + type: "string", + label: "Password", + description: "Password for the user", + }, + preferredName: { + type: "string", + label: "Preferred Name", + description: "The preferred name of the user", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.nile.createUser({ + $, + workspace: this.workspace, + database: this.database, + data: { + email: this.email, + password: this.password, + preferredName: this.preferredName, + }, + }); + $.export("$summary", `Successfully created user with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/nile_database/actions/execute-query/execute-query.mjs b/components/nile_database/actions/execute-query/execute-query.mjs new file mode 100644 index 0000000000000..68c13f60a0d1b --- /dev/null +++ b/components/nile_database/actions/execute-query/execute-query.mjs @@ -0,0 +1,59 @@ +import nile from "../../nile_database.app.mjs"; + +export default { + name: "Execute Query", + key: "nile_database-execute-query", + description: "Execute a custom PostgreSQL query.", + version: "0.0.2", + type: "action", + props: { + nile, + workspace: { + propDefinition: [ + nile, + "workspace", + ], + }, + database: { + propDefinition: [ + nile, + "database", + (c) => ({ + workspace: c.workspace, + }), + ], + }, + user: { + type: "string", + label: "Username", + description: "The username or userId of the database user. Note: Credentials are generated in the Nile Dashboard under Settings -> Credentials", + }, + password: { + type: "string", + label: "Password", + description: "The password of the database user", + }, + query: { + type: "string", + label: "Query", + description: "The PostgreSQL query to execute", + }, + }, + async run({ $ }) { + const config = { + user: this.user, + password: this.password, + host: await this.nile.getHost({ + workspace: this.workspace, + database: this.database, + }), + port: "5432", + database: this.database, + }; + const data = await this.nile.executeQuery(config, this.query); + $.export("$summary", `Returned ${data.length} ${data.length === 1 + ? "row" + : "rows"}`); + return data; + }, +}; diff --git a/components/nile_database/nile_database.app.mjs b/components/nile_database/nile_database.app.mjs new file mode 100644 index 0000000000000..0e58af09fada9 --- /dev/null +++ b/components/nile_database/nile_database.app.mjs @@ -0,0 +1,150 @@ +import { axios } from "@pipedream/platform"; +import pg from "pg"; + +export default { + type: "app", + app: "nile_database", + propDefinitions: { + workspace: { + type: "string", + label: "Workspace", + description: "Your workspace slug", + async options() { + const { workspaces } = await this.getAuthenticatedUser(); + return workspaces?.map(({ slug }) => slug) || []; + }, + }, + database: { + type: "string", + label: "Database", + description: "The database name", + async options({ workspace }) { + if (!workspace) { + return []; + } + const databases = await this.listDatabases({ + workspace, + }); + return databases?.map(({ name }) => name) || []; + }, + }, + }, + methods: { + _globalBaseUrl() { + return "https://global.thenile.dev"; + }, + async _getBaseUrl({ + workspace, database, ...opts + }) { + const { apiHost } = await this.getDatabase({ + workspace, + database, + ...opts, + }); + return apiHost; + }, + async _makeRequest({ + $ = this, + workspace, + database, + url, + path, + ...opts + }) { + return axios($, { + url: url || `${await this._getBaseUrl({ + workspace, + database, + $, + })}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + ...opts, + }); + }, + getDatabase({ + workspace, database, ...opts + }) { + return this._makeRequest({ + url: `${this._globalBaseUrl()}/workspaces/${workspace}/databases/${database}`, + workspace, + database, + ...opts, + }); + }, + listDatabases({ + workspace, ...opts + }) { + return this._makeRequest({ + url: `${this._globalBaseUrl()}/workspaces/${workspace}/databases`, + ...opts, + }); + }, + getAuthenticatedUser(opts = {}) { + return this._makeRequest({ + url: `${this._globalBaseUrl()}/developers/me/full`, + ...opts, + }); + }, + async getHost({ + workspace, database, ...opts + }) { + const { dbHost } = await this.getDatabase({ + workspace, + database, + ...opts, + }); + const host = dbHost.match(/postgres:\/\/([^/]+)\//); + return host[1]; + }, + listUsers({ + workspace, database, ...opts + }) { + return this._makeRequest({ + path: "/users", + workspace, + database, + ...opts, + }); + }, + listTenants({ + workspace, database, ...opts + }) { + return this._makeRequest({ + path: "/tenants", + workspace, + database, + ...opts, + }); + }, + createUser({ + workspace, database, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: "/users", + workspace, + database, + ...opts, + }); + }, + async _getClient(config) { + const pool = new pg.Pool(config); + const client = await pool.connect(); + return client; + }, + async _endClient(client) { + return client.release(); + }, + async executeQuery(config, query) { + const client = await this._getClient(config); + try { + const { rows } = await client.query(query); + return rows; + } finally { + await this._endClient(client); + } + }, + }, +}; diff --git a/components/nile_database/package.json b/components/nile_database/package.json new file mode 100644 index 0000000000000..b948c4735f286 --- /dev/null +++ b/components/nile_database/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/nile_database", + "version": "0.1.1", + "description": "Pipedream Nile Database Components", + "main": "nile_database.app.mjs", + "keywords": [ + "pipedream", + "nile_database" + ], + "homepage": "https://pipedream.com/apps/nile_database", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "pg": "^8.13.0" + } +} diff --git a/components/nile_database/sources/common/base.mjs b/components/nile_database/sources/common/base.mjs new file mode 100644 index 0000000000000..3a787d79281f2 --- /dev/null +++ b/components/nile_database/sources/common/base.mjs @@ -0,0 +1,65 @@ +import nile from "../../nile_database.app.mjs"; +import { + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, ConfigurationError, +} from "@pipedream/platform"; + +export default { + props: { + nile, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + workspace: { + propDefinition: [ + nile, + "workspace", + ], + }, + database: { + propDefinition: [ + nile, + "database", + (c) => ({ + workspace: c.workspace, + }), + ], + }, + }, + hooks: { + async deploy() { + try { + await this.nile.getDatabase({ + workspace: this.workspace, + database: this.database, + }); + } catch { + throw new ConfigurationError(`Database "${this.database}" with workspace "${this.workspace}" not found`); + } + }, + }, + methods: { + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run() { + const resourceFn = this.getResourceFn(); + + const results = await resourceFn({ + workspace: this.workspace, + database: this.database, + }); + + for (const item of results) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + } + }, +}; diff --git a/components/nile_database/sources/new-tenant-created/new-tenant-created.mjs b/components/nile_database/sources/new-tenant-created/new-tenant-created.mjs new file mode 100644 index 0000000000000..067cc870d5926 --- /dev/null +++ b/components/nile_database/sources/new-tenant-created/new-tenant-created.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "nile_database-new-tenant-created", + name: "New Tenant Created", + description: "Emit new event when a new tenant is added to a Nile Database", + version: "0.0.2", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.nile.listTenants; + }, + generateMeta(tenant) { + return { + id: tenant.id, + summary: `New Tenant ID: ${tenant.id}`, + ts: Date.now(), + }; + }, + }, +}; diff --git a/components/nile_database/sources/new-user-created/new-user-created.mjs b/components/nile_database/sources/new-user-created/new-user-created.mjs new file mode 100644 index 0000000000000..b37d2fcfe8b27 --- /dev/null +++ b/components/nile_database/sources/new-user-created/new-user-created.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "nile_database-new-user-created", + name: "New User Created", + description: "Emit new event when a new user is added in a Nile Database", + version: "0.0.2", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.nile.listUsers; + }, + generateMeta(user) { + return { + id: user.id, + summary: `New User ID: ${user.id}`, + ts: Date.parse(user.created), + }; + }, + }, +}; diff --git a/components/nimble/README.md b/components/nimble/README.md new file mode 100644 index 0000000000000..cc1ace32406cd --- /dev/null +++ b/components/nimble/README.md @@ -0,0 +1,11 @@ +# Overview + +The Nimble API enables you to manage contacts, deals, activities, and more within your Nimble CRM. With Pipedream's serverless platform, you can create workflows that trigger on events in Nimble or other apps, process data, and perform actions back in Nimble or across various other services. By utilizing Pipedream's capabilities, you can automate tasks such as syncing contacts to other platforms, triggering custom actions based on deal updates, or aggregating customer interactions for analysis. + +# Example Use Cases + +- **Contact Sync to Google Sheets**: Automatically sync new Nimble contacts to a Google Sheet. Each time a contact is added in Nimble, a Pipedream workflow is triggered, appending the contact details to a Google Sheet. This is useful for keeping a backup or for performing bulk operations on contact data. + +- **Deal Alerts via Slack**: Get real-time Slack notifications when a deal's status changes in Nimble. Set up a Pipedream workflow that listens for updates to deals in Nimble and sends a message to a designated Slack channel with details about the deal. This helps teams stay informed and react quickly to important sales updates. + +- **Task Creation on Project Management Tools**: Create tasks in project management tools like Trello or Asana when a specific event occurs in Nimble, such as a deal entering a new stage. A Pipedream workflow can monitor deal stages and automatically create corresponding tasks in the chosen project management app, ensuring that actionable items are tracked and managed effectively. diff --git a/components/ninjaone/ninjaone.app.mjs b/components/ninjaone/ninjaone.app.mjs new file mode 100644 index 0000000000000..0945dafedfbfa --- /dev/null +++ b/components/ninjaone/ninjaone.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "ninjaone", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/ninjaone/package.json b/components/ninjaone/package.json new file mode 100644 index 0000000000000..968834403f082 --- /dev/null +++ b/components/ninjaone/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/ninjaone", + "version": "0.0.1", + "description": "Pipedream NinjaOne Components", + "main": "ninjaone.app.mjs", + "keywords": [ + "pipedream", + "ninjaone" + ], + "homepage": "https://pipedream.com/apps/ninjaone", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/ninox/README.md b/components/ninox/README.md index fc08deb9da868..0b309937f8e47 100644 --- a/components/ninox/README.md +++ b/components/ninox/README.md @@ -1,22 +1,11 @@ # Overview -The [Ninox API](https://ninox.com) provides a powerful way to build and manage -web applications. With Ninox, web developers can create and customize -applications to fit their exact needs. +The Ninox API allows you to interact programmatically with the Ninox database platform. By harnessing this API via Pipedream, you can automate data entry, synchronize databases with other apps, and trigger actions based on specific events within your Ninox tables. It facilitates bidirectional communication, ensuring your data flows seamlessly between Ninox and other services in your tech stack. From sending notifications based on Ninox triggers to processing and analyzing data from other sources within Ninox, the possibilities are extensive. -Ninox's API makes it easy to build any kind of web application. Here are some -of the applications you can create using Ninox: +# Example Use Cases -- Web-based CRM: Use Ninox to create powerful customer relationship management - (CRM) applications. Store customer data and track customer interactions. -- Surveys and Forms: Make it easy for customers to provide feedback and answer - surveys with an easy-to-use form builder. -- Dashboard: Create visualizations to quickly get a full view of your business. -- Scheduling: Allow customers to quickly book appointments and more with an - interactive calendar. -- Website Builder: Create stunning websites with custom layouts, dynamic - content, and easy editing. -- Shopping Cart: Create intuitive online shopping experiences and integrate - them into your website. -- Automation: Set up automated workflows and triggered events to streamline - your business processes. +- **Automated Invoice Generation**: When a new order is placed in your e-commerce platform (like Shopify), Pipedream can detect this event, and create a corresponding invoice record in Ninox, ensuring that your financial records are always up-to-date. + +- **Customer Support Ticketing Integration**: Sync customer queries from a support ticketing platform (such as Zendesk) into Ninox. Every time a support ticket is created, Pipedream creates a new record in Ninox, allowing for easy tracking and management of customer issues. + +- **Event-Driven Email Campaigns**: Utilize customer data in Ninox to drive targeted email campaigns. When a new contact is added to a Ninox table, Pipedream can trigger an automated email sequence via a mailing service like Mailchimp, personalizing the content based on the data points available in Ninox. diff --git a/components/nioleads/actions/find-email/find-email.mjs b/components/nioleads/actions/find-email/find-email.mjs new file mode 100644 index 0000000000000..ca3bb6b2e3a0d --- /dev/null +++ b/components/nioleads/actions/find-email/find-email.mjs @@ -0,0 +1,36 @@ +import nioleads from "../../nioleads.app.mjs"; + +export default { + key: "nioleads-find-email", + name: "Find Email", + description: "Finds a business email address using a full name and a website domain. [See the documentation](https://apidoc.nioleads.com/?_gl=1*1288vdg*_ga*MTY1NzE1MjMzOC4xNzI1OTM5Njk1*_ga_ZVT2YHDDZG*MTcyNTk0Mzk5NC4yLjAuMTcyNTk0NDAyMy4wLjAuMA..#email-finder)", + version: "0.0.1", + type: "action", + props: { + nioleads, + name: { + type: "string", + label: "Name", + description: "Full name of the person", + }, + domain: { + type: "string", + label: "Domain", + description: "The company domain name. (e.g. `example.com`)", + }, + }, + async run({ $ }) { + const response = await this.nioleads.findEmail({ + $, + data: { + name: this.name, + domain: this.domain, + }, + }); + if (response?.code) { + throw new Error(`${response.msg}`); + } + $.export("$summary", `Found email: ${response.email}`); + return response; + }, +}; diff --git a/components/nioleads/actions/verify-email/verify-email.mjs b/components/nioleads/actions/verify-email/verify-email.mjs new file mode 100644 index 0000000000000..fe8e7d95991fc --- /dev/null +++ b/components/nioleads/actions/verify-email/verify-email.mjs @@ -0,0 +1,30 @@ +import nioleads from "../../nioleads.app.mjs"; + +export default { + key: "nioleads-verify-email", + name: "Verify Email", + description: "Checks the deliverability of a specified email address. [See the documentation](https://apidoc.nioleads.com/?_gl=1*1288vdg*_ga*MTY1NzE1MjMzOC4xNzI1OTM5Njk1*_ga_ZVT2YHDDZG*MTcyNTk0Mzk5NC4yLjAuMTcyNTk0NDAyMy4wLjAuMA..#email-verifier)", + version: "0.0.1", + type: "action", + props: { + nioleads, + email: { + type: "string", + label: "Email", + description: "The email address to verify", + }, + }, + async run({ $ }) { + const response = await this.nioleads.verifyEmail({ + $, + data: { + email: this.email, + }, + }); + if (response?.code) { + throw new Error(`${response.msg}`); + } + $.export("$summary", `Verified email ${this.email}`); + return response; + }, +}; diff --git a/components/nioleads/nioleads.app.mjs b/components/nioleads/nioleads.app.mjs new file mode 100644 index 0000000000000..1cb59c5a80826 --- /dev/null +++ b/components/nioleads/nioleads.app.mjs @@ -0,0 +1,48 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "nioleads", + propDefinitions: {}, + methods: { + _baseUrl() { + return "https://api.nioleads.com/v1/openapi"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `Bearer ${this.$auth.api_key}`, + "Content-Type": "application/json", + "Accept": "application/json", + }, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/new_contacts", + ...opts, + }); + }, + verifyEmail(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/verify_email", + ...opts, + }); + }, + findEmail(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/find_email", + ...opts, + }); + }, + }, +}; diff --git a/components/nioleads/package.json b/components/nioleads/package.json new file mode 100644 index 0000000000000..6478ec2d01c2b --- /dev/null +++ b/components/nioleads/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/nioleads", + "version": "0.1.0", + "description": "Pipedream NioLeads Components", + "main": "nioleads.app.mjs", + "keywords": [ + "pipedream", + "nioleads" + ], + "homepage": "https://pipedream.com/apps/nioleads", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} diff --git a/components/nioleads/sources/new-contact-added/new-contact-added.mjs b/components/nioleads/sources/new-contact-added/new-contact-added.mjs new file mode 100644 index 0000000000000..d28de5f58d550 --- /dev/null +++ b/components/nioleads/sources/new-contact-added/new-contact-added.mjs @@ -0,0 +1,62 @@ +import nioleads from "../../nioleads.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "nioleads-new-contact-added", + name: "New Contact Added", + description: "Emit new event when a new contact is added. [See the documentation](https://apidoc.nioleads.com/?_gl=1*1288vdg*_ga*MTY1NzE1MjMzOC4xNzI1OTM5Njk1*_ga_ZVT2YHDDZG*MTcyNTk0Mzk5NC4yLjAuMTcyNTk0NDAyMy4wLjAuMA..#contacts)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + nioleads, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + generateMeta(contact) { + return { + id: contact.id, + summary: `New Contact: ${contact.name}`, + ts: Date.now(), + }; + }, + async processEvent(limit) { + const lastId = this._getLastId(); + const contacts = await this.nioleads.listContacts(); + if (!contacts?.length) { + return; + } + const newContacts = contacts.filter(({ id }) => id > lastId); + if (limit && newContacts.length > limit) { + newContacts.length = limit; + } + this._setLastId(newContacts[0].id); + newContacts.reverse().forEach((contact) => { + const meta = this.generateMeta(contact); + this.$emit(contact, meta); + }); + }, + }, + async run() { + await this.processEvent(); + }, + sampleEmit, +}; diff --git a/components/nioleads/sources/new-contact-added/test-event.mjs b/components/nioleads/sources/new-contact-added/test-event.mjs new file mode 100644 index 0000000000000..be75f936ad620 --- /dev/null +++ b/components/nioleads/sources/new-contact-added/test-event.mjs @@ -0,0 +1,20 @@ +export default { + "id": 1, + "name": "Tom ********", + "company": "All We Have Is Now", + "domain": "allwehaveisnow.co", + "job_title": "Keynote Speaker", + "email": "********@allwehaveisnow.co", + "linkedin_url": "http://www.linkedin.com/in/tomfgoodwin", + "first_name": "Tom", + "last_name": "********", + "city": "New York", + "region": "New York", + "country": "United States", + "industry": "management consulting", + "company_city": "New York", + "company_region": "New York", + "company_country": "United States", + "company_linkedin_url": "http://www.linkedin.com/company/all-we-have-is-now", + "list": "Default list" +} \ No newline at end of file diff --git a/components/nmbrs/nmbrs.app.mjs b/components/nmbrs/nmbrs.app.mjs new file mode 100644 index 0000000000000..04e608c1cc6fa --- /dev/null +++ b/components/nmbrs/nmbrs.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "nmbrs", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/nmbrs/package.json b/components/nmbrs/package.json new file mode 100644 index 0000000000000..0c24fdd5d9afc --- /dev/null +++ b/components/nmbrs/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/nmbrs", + "version": "0.0.1", + "description": "Pipedream Nmbrs Components", + "main": "nmbrs.app.mjs", + "keywords": [ + "pipedream", + "nmbrs" + ], + "homepage": "https://pipedream.com/apps/nmbrs", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/nocodb/README.md b/components/nocodb/README.md index 5b35561d713ca..cff5d3f20153a 100644 --- a/components/nocodb/README.md +++ b/components/nocodb/README.md @@ -1,22 +1,11 @@ # Overview -NoCodeDB offers a powerful API that allows you to build compelling web and -mobile applications without writing code. With this API, you can create robust -applications that interact with millions of users, securely store data, and -build integrations with other services. Here’s a short list of some of the -things you can do with NoCodeDB: +The NoCodeB (NocoDB) API transforms your databases into a smart Excel sheet, providing a platform for managing your data with the ease of a spreadsheet interface. When combined with Pipedream's capabilities, this API allows you to automate data operations, sync with other databases or apps, and streamline workflows without writing complex code. Using Pipedream, you can trigger events based on changes in your NocoDB, process and manipulate the data, and connect to countless apps to extend functionality, such as sending notifications, updating CRMs, or generating reports. -- Create real-time web applications by connecting to your users via WebSockets -- Securely store and manage data for user accounts and settings -- Create custom APIs to extend your applications -- Create integrations with third-party services such as Stripe, Salesforce, - Google Drive, and Dropbox -- Deploy on serverless infrastructure to ensure fast and reliable results -- Create custom data models to define relationships between data -- Create custom queries to access data in powerful ways -- Automate tasks with serverless functions -- Monitor application performance and security with detailed analysis -- Extend existing applications with custom data and logic -- Utilize modern data practices such as GraphQL and cloud storage -- Create secure and fully compliant serverless environments -- And more! +# Example Use Cases + +- **Automated Data Sync Between NocoDB and Google Sheets**: Use the NocoDB API to watch for new records or updates in your database tables. When a change is detected, a Pipedream workflow can be triggered to reflect these changes in a corresponding Google Sheets document, ensuring both NocoDB and Google Sheets have synchronized data. + +- **Dynamic CRM Updates from NocoDB**: When new contacts or deals are added to your NocoDB tables, a Pipedream workflow can automatically push this data to your CRM platform, such as Salesforce or HubSpot. This ensures that your sales team has the latest information without manual entry, saving time and reducing errors. + +- **Email Notifications on Specific Triggers**: Configure Pipedream to monitor specific columns or triggers within NocoDB—for instance, when a project status changes to "Complete." Upon this trigger, Pipedream can craft and send a customized email notification to team members or stakeholders via SendGrid or Mailgun, keeping everyone in the loop. diff --git a/components/nocodb/actions/add-record/add-record.mjs b/components/nocodb/actions/add-record/add-record.mjs index 366922345ff49..adbe8a8ab03fc 100644 --- a/components/nocodb/actions/add-record/add-record.mjs +++ b/components/nocodb/actions/add-record/add-record.mjs @@ -4,8 +4,8 @@ export default { ...common, key: "nocodb-add-record", name: "Add Record", - description: "This action adds a record in a table. [See the docs here](https://all-apis.nocodb.com/#tag/DB-table-row/operation/db-table-row-create)", - version: "0.0.2", + description: "This action adds a record in a table. [See the documentation](https://data-apis-v2.nocodb.com/#tag/Table-Records/operation/db-data-table-row-create)", + version: "0.0.5", type: "action", props: { ...common.props, @@ -17,20 +17,18 @@ export default { }, }, methods: { - async processEvent() { - const { - projectId, - tableName, - data, - } = this; + async processEvent($) { + const data = typeof this.data === "string" + ? JSON.parse(this.data) + : this.data; return this.nocodb.createTableRow({ - projectId, - tableName: tableName.value, + tableId: this.tableId.value, data, + $, }); }, getSummary() { - return `Record Successfully added in ${this.tableName.label} table!`; + return `Record Successfully added in ${this.tableId.label} table!`; }, }, }; diff --git a/components/nocodb/actions/common/base.mjs b/components/nocodb/actions/common/base.mjs index 536119902e591..0984ed08e926e 100644 --- a/components/nocodb/actions/common/base.mjs +++ b/components/nocodb/actions/common/base.mjs @@ -4,16 +4,25 @@ export default { type: "action", props: { nocodb, + workspaceId: { + propDefinition: [ + nocodb, + "workspaceId", + ], + }, projectId: { propDefinition: [ nocodb, "projectId", + (c) => ({ + workspaceId: c.workspaceId, + }), ], }, - tableName: { + tableId: { propDefinition: [ nocodb, - "tableName", + "tableId", (c) => ({ projectId: c.projectId, }), @@ -21,7 +30,7 @@ export default { }, }, async run({ $ }) { - const response = await this.processEvent(); + const response = await this.processEvent($); $.export("$summary", this.getSummary(response)); return response; }, diff --git a/components/nocodb/actions/delete-record/delete-record.mjs b/components/nocodb/actions/delete-record/delete-record.mjs index d41dcec2e65f1..d9591b3c96ada 100644 --- a/components/nocodb/actions/delete-record/delete-record.mjs +++ b/components/nocodb/actions/delete-record/delete-record.mjs @@ -4,8 +4,8 @@ export default { ...common, key: "nocodb-delete-record", name: "Delete Record", - description: "This action deletes a row in a table. [See the docs here](https://all-apis.nocodb.com/#tag/DB-table-row/operation/db-table-row-delete)", - version: "0.0.2", + description: "This action deletes a row in a table. [See the documentation](https://data-apis-v2.nocodb.com/#tag/Table-Records/operation/db-data-table-row-delete)", + version: "0.0.5", type: "action", props: { ...common.props, @@ -13,24 +13,24 @@ export default { propDefinition: [ common.props.nocodb, "rowId", + (c) => ({ + tableId: c.tableId.value, + }), ], }, }, methods: { - async processEvent() { - const { - projectId, - tableName, - rowId, - } = this; + async processEvent($) { return this.nocodb.deleteTableRow({ - projectId, - tableName: tableName.value, - rowId, + tableId: this.tableId.value, + data: { + Id: this.rowId, + }, + $, }); }, getSummary() { - return `Record Successfully deleted in ${this.tableName.label} table!`; + return `Record Successfully deleted in ${this.tableId.label} table!`; }, }, }; diff --git a/components/nocodb/actions/get-record/get-record.mjs b/components/nocodb/actions/get-record/get-record.mjs index ec6a5a7cc40b1..5ae8549178421 100644 --- a/components/nocodb/actions/get-record/get-record.mjs +++ b/components/nocodb/actions/get-record/get-record.mjs @@ -4,8 +4,8 @@ export default { ...common, key: "nocodb-get-record", name: "Get Record (from row number)", - description: "This action gets a row by row Id. [See the docs here](https://all-apis.nocodb.com/#tag/DB-table-row/operation/db-table-row-read)", - version: "0.0.2", + description: "This action gets a row by row Id. [See the documentation](https://data-apis-v2.nocodb.com/#tag/Table-Records/operation/db-data-table-row-read)", + version: "0.0.5", type: "action", props: { ...common.props, @@ -13,24 +13,22 @@ export default { propDefinition: [ common.props.nocodb, "rowId", + (c) => ({ + tableId: c.tableId.value, + }), ], }, }, methods: { - async processEvent() { - const { - projectId, - tableName, - rowId, - } = this; + async processEvent($) { return this.nocodb.getTableRow({ - projectId, - tableName: tableName.value, - rowId, + tableId: this.tableId.value, + rowId: this.rowId, + $, }); }, getSummary() { - return `Record Successfully fetched from ${this.tableName.label} table!`; + return `Record Successfully fetched from ${this.tableId.label} table!`; }, }, }; diff --git a/components/nocodb/actions/list-records-matching-criteria/list-records-matching-criteria.mjs b/components/nocodb/actions/list-records-matching-criteria/list-records-matching-criteria.mjs index 1b71a1f5b346a..14aabf15b1afa 100644 --- a/components/nocodb/actions/list-records-matching-criteria/list-records-matching-criteria.mjs +++ b/components/nocodb/actions/list-records-matching-criteria/list-records-matching-criteria.mjs @@ -4,8 +4,8 @@ export default { ...common, key: "nocodb-list-records-matching-criteria", name: "List Records in Table Matching Criteria", - description: "This action lists all rows in a table. [See the docs here](https://all-apis.nocodb.com/#tag/DB-table-row/operation/db-table-row-list)", - version: "0.0.3", + description: "This action lists all rows in a table. [See the documentation](https://data-apis-v2.nocodb.com/#tag/Table-Records/operation/db-data-table-row-list)", + version: "0.0.6", type: "action", props: { ...common.props, @@ -14,7 +14,7 @@ export default { common.props.nocodb, "fields", (c) => ({ - tableId: c.tableName.value, + tableId: c.tableId.value, }), ], optional: true, @@ -24,7 +24,7 @@ export default { common.props.nocodb, "sort", (c) => ({ - tableId: c.tableName.value, + tableId: c.tableId.value, }), ], optional: true, @@ -45,31 +45,20 @@ export default { }, }, methods: { - async processEvent() { - const { - projectId, - tableName, - fields, - sort, - where, - limit, - } = this; - - const params = { - projectId, - tableName: tableName.value, - query: { - fields, - sort, - where, - limit, - }, - }; - + async processEvent($) { const rows = []; const paginator = this.nocodb.paginate({ fn: this.nocodb.listTableRow, - params, + args: { + tableId: this.tableId.value, + params: { + fields: this.fields, + sort: this.sort, + where: this.where, + }, + }, + max: this.limit, + $, }); for await (const row of paginator) { rows.push(row); @@ -81,7 +70,7 @@ export default { ? "" : "s"; - return `Returned ${response.length} row${suffix} from ${this.tableName.label} table`; + return `Returned ${response.length} row${suffix} from ${this.tableId.label} table`; }, }, }; diff --git a/components/nocodb/actions/update-record/update-record.mjs b/components/nocodb/actions/update-record/update-record.mjs index 6a985932a53ac..8756f4f0c64d7 100644 --- a/components/nocodb/actions/update-record/update-record.mjs +++ b/components/nocodb/actions/update-record/update-record.mjs @@ -4,8 +4,8 @@ export default { ...common, key: "nocodb-update-record", name: "Update Record", - description: "This action updates a record in a table. [See the docs here](https://all-apis.nocodb.com/#tag/DB-table-row/operation/db-table-row-update)", - version: "0.0.2", + description: "This action updates a record in a table. [See the documentation](https://data-apis-v2.nocodb.com/#tag/Table-Records/operation/db-data-table-row-update)", + version: "0.0.5", type: "action", props: { ...common.props, @@ -13,6 +13,9 @@ export default { propDefinition: [ common.props.nocodb, "rowId", + (c) => ({ + tableId: c.tableId.value, + }), ], }, data: { @@ -23,22 +26,21 @@ export default { }, }, methods: { - async processEvent() { - const { - projectId, - tableName, - rowId, - data, - } = this; + async processEvent($) { + const data = typeof this.data === "string" + ? JSON.parse(this.data) + : this.data; return this.nocodb.updateTableRow({ - projectId, - tableName: tableName.value, - rowId, - data, + tableId: this.tableId.value, + data: { + Id: this.rowId, + ...data, + }, + $, }); }, getSummary() { - return `Successfully updated row in ${this.tableName.label} table`; + return `Successfully updated row in ${this.tableId.label} table`; }, }, }; diff --git a/components/nocodb/nocodb.app.mjs b/components/nocodb/nocodb.app.mjs index 039d29864b378..bc4326cafbb25 100644 --- a/components/nocodb/nocodb.app.mjs +++ b/components/nocodb/nocodb.app.mjs @@ -1,4 +1,5 @@ -import { Api } from "nocodb-sdk"; +import { axios } from "@pipedream/platform"; +const DEFAULT_LIMIT = 50; export default { type: "app", @@ -6,20 +7,22 @@ export default { propDefinitions: { projectId: { type: "string", - label: "Project Id", - description: "The id of the project", - async options() { - const { list } = await this.listProjects(); + label: "Project ID", + description: "The ID of the project", + async options({ workspaceId }) { + const { list } = await this.listProjects({ + workspaceId, + }); return list.map((project) => ({ label: project.title, value: project.id, })); }, }, - tableName: { + tableId: { type: "string", - label: "Table Name", - description: "The name of the table", + label: "Table ID", + description: "The ID of the table", withLabel: true, async options({ projectId }) { const { list } = await this.listTables({ @@ -31,10 +34,61 @@ export default { })); }, }, + workspaceId: { + type: "string", + label: "Workspace ID", + description: "The ID of a workspace", + async options() { + const { list } = await this.listWorkspaces(); + return list.map(({ + id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, rowId: { type: "string", label: "Row Id", description: "Id of the row", + async options({ + tableId, page, + }) { + const { list } = await this.listTableRow({ + tableId, + params: { + offset: page * DEFAULT_LIMIT, + }, + }); + const rows = Array.isArray(list) + ? list + : [ + list, + ]; + return rows?.map(({ Id }) => `${Id}` ) || []; + }, + }, + viewId: { + type: "string", + label: "View ID", + description: "The ID of a view", + async options({ + tableId, page, + }) { + const { list } = await this.listViews({ + tableId, + params: { + offset: page * DEFAULT_LIMIT, + }, + }); + return list?.map(({ + id: value, title: label, + }) => ({ + label, + value, + })) || []; + }, }, data: { type: "any", @@ -69,52 +123,75 @@ export default { }, }, methods: { - sdk() { - return new Api({ - baseURL: `https://${this.$auth.domain}`, + _makeRequest({ + $ = this, + path, + apiVersion = "v2", + ...opts + }) { + return axios($, { + url: `https://${this.$auth.domain}/api/${apiVersion}${path}`, headers: { "xc-token": this.$auth.api_key, }, + ...opts, }); }, async *paginate({ - fn, params = {}, offset = 0, + fn, args = {}, max, }) { - let lastPage = false; - const { query } = params; - const limit = query.limit || null; + let hasMore = false; let count = 0; - + args.params = { + ...args.params, + offset: 0, + limit: 1000, + }; do { - params.query.offset = offset; const { - list, - pageInfo: { - pageSize, - page, - isLastPage, - }, - } = await fn(params); - for (const d of list) { - yield d; - - if (limit && ++count === limit) { - return count; + list, pageInfo, + } = await fn(args); + for (const item of list) { + args.params.offset++; + yield item; + if (max && ++count === max) { + return; } } - - offset = pageSize * page; - lastPage = !isLastPage; - } while (lastPage); + hasMore = !pageInfo.isLastPage; + } while (hasMore); }, - async listProjects() { - return this.sdk().project.list(); + listWorkspaces(opts = {}) { + return this._makeRequest({ + path: "/workspaces", + apiVersion: "v1", + ...opts, + }); }, - async listTables({ projectId }) { - return this.sdk().dbTable.list(projectId); + listProjects({ + workspaceId, ...opts + }) { + return this._makeRequest({ + path: `/workspaces/${workspaceId}/bases`, + apiVersion: "v1", + ...opts, + }); }, - async readTable({ tableId }) { - return this.sdk().dbTable.read(tableId); + listTables({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/meta/bases/${projectId}/tables`, + ...opts, + }); + }, + readTable({ + tableId, ...opts + }) { + return this._makeRequest({ + path: `/meta/tables/${tableId}`, + ...opts, + }); }, async listTableFields(tableId) { const { columns } = await this.readTable({ @@ -134,41 +211,56 @@ export default { ]); }, []); }, - async createTableRow({ - projectId, - tableName, - data, + createTableRow({ + tableId, ...opts }) { - return this.sdk().dbTableRow.create("v1", projectId, tableName, data); + return this._makeRequest({ + method: "POST", + path: `/tables/${tableId}/records`, + ...opts, + }); }, - async updateTableRow({ - projectId, - tableName, - rowId, - data, + updateTableRow({ + tableId, ...opts }) { - return this.sdk().dbTableRow.update("v1", projectId, tableName, rowId, data); + return this._makeRequest({ + method: "PATCH", + path: `/tables/${tableId}/records`, + ...opts, + }); }, - async deleteTableRow({ - projectId, - tableName, - rowId, + deleteTableRow({ + tableId, ...opts }) { - return this.sdk().dbTableRow.delete("v1", projectId, tableName, rowId); + return this._makeRequest({ + method: "DELETE", + path: `/tables/${tableId}/records`, + ...opts, + }); }, - async getTableRow({ - projectId, - tableName, - rowId, + getTableRow({ + tableId, rowId, ...opts }) { - return this.sdk().dbTableRow.read("v1", projectId, tableName, rowId); + return this._makeRequest({ + path: `/tables/${tableId}/records/${rowId}`, + ...opts, + }); }, - async listTableRow({ - projectId, - tableName, - query, + listTableRow({ + tableId, ...opts }) { - return this.sdk().dbTableRow.list("v1", projectId, tableName, query); + return this._makeRequest({ + path: `/tables/${tableId}/records`, + ...opts, + }); + }, + listViews({ + tableId, ...opts + }) { + return this._makeRequest({ + path: `/meta/tables/${tableId}/views`, + ...opts, + }); }, }, }; diff --git a/components/nocodb/package.json b/components/nocodb/package.json index 5ce3f853c32a3..fd91c3e67f090 100644 --- a/components/nocodb/package.json +++ b/components/nocodb/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/nocodb", - "version": "0.0.5", + "version": "0.1.0", "description": "Pipedream Nocodb Components", "main": "nocodb.app.mjs", "keywords": [ @@ -9,12 +9,10 @@ ], "homepage": "https://pipedream.com/apps/nocodb", "author": "Pipedream (https://pipedream.com/)", - "dependencies": { - "@pipedream/platform": "^1.2.0", - "moment": "^2.29.4", - "nocodb-sdk": "^0.92.4" - }, "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/nocodb/sources/common/base.mjs b/components/nocodb/sources/common/base.mjs index 85b5c55ae9019..20b3f807d9d29 100644 --- a/components/nocodb/sources/common/base.mjs +++ b/components/nocodb/sources/common/base.mjs @@ -1,20 +1,28 @@ -import moment from "moment"; import nocodb from "../../nocodb.app.mjs"; import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; export default { props: { nocodb, + workspaceId: { + propDefinition: [ + nocodb, + "workspaceId", + ], + }, projectId: { propDefinition: [ nocodb, "projectId", + (c) => ({ + workspaceId: c.workspaceId, + }), ], }, - tableName: { + tableId: { propDefinition: [ nocodb, - "tableName", + "tableId", (c) => ({ projectId: c.projectId, }), @@ -37,66 +45,53 @@ export default { _setLastTime(lastTime) { this.db.set("lastTime", lastTime); }, - async processEvent({ - params, lastTime, - }) { + getParams(timeField) { + return { + sort: `-${timeField}`, + }; + }, + async getRows(records, timeField, lastTime) { + const rows = []; + for await (const row of records) { + if (!lastTime || Date.parse(row[timeField]) >= Date.parse(lastTime)) { + rows.push(row); + } else { + break; + } + } + return rows.reverse(); + }, + async processEvent(max) { const timeField = this.getTimeField(); + const lastTime = this._getLastTime(); const records = this.nocodb.paginate({ fn: this.nocodb.listTableRow, - params, + args: { + tableId: this.tableId.value, + params: this.getParams(timeField), + }, + max, }); - for await (const record of records) { - if (moment(record[timeField]).isAfter(lastTime)) this._setLastTime(record[timeField]); - this.$emit(record, this.getDataToEmit(record)); + const rows = await this.getRows(records, timeField, lastTime); + + if (!rows.length) { + return; } + + this._setLastTime(rows[rows.length - 1][timeField]); + + rows.forEach((row) => this.$emit(row, this.getDataToEmit(row))); }, }, hooks: { - async activate() { - const timeField = this.getTimeField(); - const lastTime = this._getLastTime(); - const { list } = await this.nocodb.listTableRow({ - projectId: this.projectId, - tableName: this.tableName.value, - query: { - sort: `-${timeField}`, - }, - }); - - list.reverse(); - - for (const row of list) { - if (!lastTime || moment(lastTime).isAfter(row[timeField])) { - this._setLastTime(row[timeField]); - } - this.$emit(row, this.getDataToEmit(row)); - } + async deploy() { + await this.processEvent(25); }, }, async run() { - const { - projectId, - tableName, - } = this; - - const timeField = this.getTimeField(); - const lastTime = this._getLastTime(); - const params = { - query: { - sort: timeField, - }, - projectId, - tableName: tableName.value, - }; - // moment is necessary because nocodb query doesn't filter equal datetime in 'greater than' - if (lastTime) params.query.where = `(${timeField},gte,${moment(lastTime).add(1, "ms") - .toISOString()})`; - return this.processEvent({ - params, - lastTime, - }); + await this.processEvent(); }, }; diff --git a/components/nocodb/sources/new-record-in-view/new-record-in-view.mjs b/components/nocodb/sources/new-record-in-view/new-record-in-view.mjs new file mode 100644 index 0000000000000..b3f67c758ed62 --- /dev/null +++ b/components/nocodb/sources/new-record-in-view/new-record-in-view.mjs @@ -0,0 +1,51 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "nocodb-new-record-in-view", + name: "New Record in View", + description: "Emit new event for each new record in a view. [See the documentation](https://data-apis-v2.nocodb.com/#tag/Table-Records/operation/db-data-table-row-list)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + viewId: { + propDefinition: [ + common.props.nocodb, + "viewId", + (c) => ({ + tableId: c.tableId.value || c.tableId, + }), + ], + }, + }, + methods: { + ...common.methods, + getDataToEmit(record) { + return { + id: record.id, + summary: `New record created (${record.id})`, + ts: Date.parse(record[this.getTimeField()]), + }; + }, + getTimeField() { + return "created_at"; + }, + getParams(timeField) { + return { + viewId: this.viewId, + fields: timeField, + }; + }, + async getRows(records, timeField, lastTime) { + const rows = []; + for await (const row of records) { + if (!lastTime || Date.parse(row[timeField]) >= Date.parse(lastTime)) { + rows.push(row); + } + } + return rows; + }, + }, +}; diff --git a/components/nocodb/sources/new-record/new-record.mjs b/components/nocodb/sources/new-record/new-record.mjs index 1aa35201c8d45..64d836fcfe7e1 100644 --- a/components/nocodb/sources/new-record/new-record.mjs +++ b/components/nocodb/sources/new-record/new-record.mjs @@ -5,8 +5,8 @@ export default { type: "source", name: "New Record in Table", key: "nocodb-new-record", - description: "Emit new event for each new record in table. [See docs here](https://all-apis.nocodb.com/#tag/DB-table-row/operation/db-table-row-list)", - version: "0.0.3", + description: "Emit new event for each new record in table. [See the documentation](https://data-apis-v2.nocodb.com/#tag/Table-Records/operation/db-data-table-row-list)", + version: "0.0.6", dedupe: "unique", props: { ...common.props, diff --git a/components/nocodb/sources/updated-record/updated-record.mjs b/components/nocodb/sources/updated-record/updated-record.mjs index 56c6f0ddc0db0..18633ea71ead5 100644 --- a/components/nocodb/sources/updated-record/updated-record.mjs +++ b/components/nocodb/sources/updated-record/updated-record.mjs @@ -5,8 +5,8 @@ export default { type: "source", name: "New Update in Table", key: "nocodb-updated-record", - description: "Emit new event for each update in table. [See docs here](https://all-apis.nocodb.com/#tag/DB-table-row/operation/db-table-row-list)", - version: "0.0.3", + description: "Emit new event for each update in table. [See the documentation](https://data-apis-v2.nocodb.com/#tag/Table-Records/operation/db-data-table-row-list)", + version: "0.0.6", dedupe: "unique", props: { ...common.props, diff --git a/components/nocrm_io/README.md b/components/nocrm_io/README.md index a75206b898449..b5cd72a52964e 100644 --- a/components/nocrm_io/README.md +++ b/components/nocrm_io/README.md @@ -1,19 +1,11 @@ # Overview -NoCRM.io is a CRM platform that offers a powerful API that developers can use -to power custom applications and integrations. With it, you can build -applications to help organize customers, products, marketing campaigns, sales -processes, and much more. +noCRM.io API offers the ability to streamline and automate the sales process, focusing on lead management. With the noCRM.io API, you can create, update, delete, and retrieve leads, manage lead statuses, and trigger actions based on lead activity. Pipedream's serverless platform enhances these capabilities by allowing you to integrate noCRM.io with a plethora of other apps, setting up complex workflows without writing extensive code. -Here are some examples of what you can create with the NoCRM.io API: +# Example Use Cases -- Create customer profiles with contact information, notes, and rich media - attachments -- Integrate with existing applications like marketing automation, customer - service platforms, and eCommerce stores -- Automate sales processes with customizable workflows -- Create powerful dashboards that visualize customer data in real-time -- Develop custom reports and analytics to monitor sales performance -- Track customer activity with real-time notifications -- Set up notifications for when important business milestones are met -- Create lead qualification rules based on specific criteria +- **Lead Capture Automation**: Capture leads from various sources like web forms, emails, or chat systems and automatically create new leads in noCRM.io. For instance, set up a workflow where every new subscriber from a Mailchimp campaign is added as a lead in noCRM.io, with appropriate tags and status. + +- **Lead Activity Notifications**: Send real-time notifications via Slack or email whenever a lead status changes in noCRM.io. This workflow can keep sales teams informed about hot leads or follow-ups needed, ensuring timely engagement with potential clients. + +- **Task and Calendar Sync**: Synchronize noCRM.io tasks with Google Calendar. Whenever a new task is created in noCRM.io, a corresponding event can be added to a Google Calendar, helping sales reps manage their follow-ups and meetings efficiently. diff --git a/components/node/README.md b/components/node/README.md new file mode 100644 index 0000000000000..63c0b931ae42a --- /dev/null +++ b/components/node/README.md @@ -0,0 +1,116 @@ +# Overview + +Develop, run and deploy your Node.js code in Pipedream workflows, using it between no-code steps, with [connected accounts](https://pipedream.com/docs/code/nodejs/auth), or integrate [Data Stores](https://pipedream.com/docs/data-stores) and [File Stores](https://pipedream.com/docs/projects/file-stores). + +This includes [installing NPM packages](https://pipedream.com/docs/code/nodejs#using-npm-packages), within your code without having to manage a `package.json` file or running `npm install`. + +Below is an example of installing the `axios` package in a Pipedream Node.js code step. Pipedream imports the **`axios`** package, performs the API request, and shares the response with subsequent workflow steps: + +# Getting Started + +To add a Node.js code step, open a new workflow and include a step. + +1. Select the **Node** app: + +![Node.js app being chosen in Pipedream's new step selector screen.](https://res.cloudinary.com/pipedreamin/image/upload/v1713366571/marketplace/apps/node.js/CleanShot_2024-04-17_at_11.07.40_nrfq20.png) + +2. Then select the **Run Node Code** action: + +![Selecting the 'Run Node Code' action from the Node app in the Pipedream interface.”](https://res.cloudinary.com/pipedreamin/image/upload/v1713366838/marketplace/apps/node.js/CleanShot_2024-04-17_at_11.12.26_rtj2qi.png) + +Now you’re ready to write some code! + +On the right, you'll see the default code provided by Pipedream: + +```jsx +// To use any npm package, just import it +// import axios from "axios" + +export default defineComponent({ + async run({ steps, $ }) { + // Reference previous step data using the steps object and return data to use it in future steps + return steps.trigger.event + }, +}) +``` + +You can write your custom code within the `run` function. `run` is called when this step executes in your workflow. + +When you click **Test** on the Node.js code step, it will display the event data from your trigger step. For instance, if your trigger is an HTTP request, then the HTTP request data will be returned. + +This step can execute any Node.js code. However, the **`run`** function, a special Pipedream callback, must be set up correctly to return data. Otherwise, you can run arbitrary code that: + +- [Consumes or shares data with other steps](https://pipedream.com/docs/code/nodejs#sharing-data-between-steps) +- [Send HTTP requests](https://pipedream.com/docs/code/nodejs#making-http-requests-from-your-workflow) +- [Return an HTTP response](https://pipedream.com/docs/code/nodejs#returning-http-responses) +- [End the entire workflow](https://pipedream.com/docs/code/nodejs#ending-a-workflow-early) +- [Use your connected accounts to make authenticated HTTP requests](https://pipedream.com/docs/code/nodejs/auth) +- [Reference environment variables](https://pipedream.com/docs/code/nodejs#using-secrets-in-code) +- [Display props for entering in dynamic or static data](https://pipedream.com/docs/code/nodejs#passing-props-to-code-steps) +- [Retrieve or update data within Data Stores](https://pipedream.com/docs/code/nodejs/using-data-stores) +- [Download, upload and manipulate files](https://pipedream.com/docs/code/nodejs/working-with-files) +- [Pausing, resuming and reruning Node.js code steps](https://pipedream.com/docs/code/nodejs/rerun) +- [Browser automation with Puppeteer or Playwright](https://pipedream.com/docs/code/nodejs/browser-automation) + +## Developing pre-built actions and triggers + +Pipedream’s pre-built actions and triggers (a.k.a. Sources) are built using Node.js. [These components](https://pipedream.com/docs/components) allow you to quickly build workflows by reusing logic with different inputs (also known as [props](https://pipedream.com/docs/workflows/using-props)). + +These components are open source. [You can fork the Pipedream repository](https://github.com/PipedreamHQ/pipedream) and modify these components to your needs. You can even publish private actions and sources to your workspace for you and your team’s own use. + +You can also [contribute actions and triggers to Pipedream’s Registry](https://pipedream.com/docs/apps/contributing) which are published on [Pipedream’s App Marketplace](https://pipedream.com/apps). Publishing your app on Pipedream integrates it for use with code steps as well as streamlined integrations with your own pre-built actions and triggers. + +Components can be deployed to your workspace by using the Pipedream CLI, or within your workflows Node.js code steps directly. + +To get started: + +- Learn about Pipedream Node.js powered [Components](https://pipedream.com/docs/components) + - [Build a custom action](https://pipedream.com/docs/components/actions-quickstart) + - [Build a custom trigger](https://pipedream.com/docs/components/sources-quickstart) +- [Integrate your app into Pipedream](https://pipedream.com/docs/apps/contributing#contribution-process) + +## AI code generation + +Generate Pipedream compatible Node.js code within your workflow with human language. + +To get started, open a Node.js step and select the **Edit with AI** button. + +![Initiating an AI code generation session from a Node.js code step in the Pipedream workflow editor.](https://res.cloudinary.com/pipedreamin/image/upload/v1713367770/marketplace/apps/node.js/CleanShot_2024-04-17_at_11.28.34_ld1ymo.png) + +Then, enter your prompt to generate code. Selecting a specific app generates the necessary integration code for API requests. + +For example, select the Slack app and use the prompt: + +``` +Send a message to a public channel +``` + +This prompt will generate the corresponding code to send an API request to Slack and perform the action using your connected Pipedream account. + +# Troubleshooting + +Pipedream will show your error traces within your individual steps, under the **Logs** section. + +Traces across all of your workflows are also available within the [Event History](https://pipedream.com/docs/event-history) in your Pipedream workspace. This gives a global view of all failed executions, and gives you the tools to filter by workflow, time occurred and more. + +## Pipedream Specific Errors + +If there’s an issue with a specific Pipedream level construct, you’ll see specific error messages within the step itself. + +### Configuration Error + +A `ConfigurationError` can occur if the props for the code step are invalid. For example, if you’re attempting to update a Google Calendar event, but the `end_date` is *before* the `start_date`, then the action may throw a specific `ConfigurationError` to signal this action isn’t possible. + +As a Pipedream action or source developer, you can also leverage `ConfigurationErrors` to guide users of your action or source to pass appropriate data to props in order for the component to function properly. + +For more details, visit the [Configuration Error documentation](https://pipedream.com/docs/code/nodejs#configuration-error). + +### Active Handle Error + +If your code is performing an asynchronous action that is not properly awaited, you may see an error. + +This error means that *control* has moved onto the next step, but there were unresolved promises within your Node.js code. + +To keep Pipedream from moving onto the next step, use `await` on your promises to hold execution until the promise finishes. + +[Read our docs](https://pipedream.com/docs/code/nodejs/async#the-problem) on implementing asynchronous code in Node.js code steps. diff --git a/components/node/package.json b/components/node/package.json new file mode 100644 index 0000000000000..9f28cbece1f5a --- /dev/null +++ b/components/node/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/node", + "version": "0.6.0", + "description": "Pipedream node Components", + "main": "node.app.mjs", + "keywords": [ + "pipedream", + "node" + ], + "homepage": "https://pipedream.com/apps/node", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/noor/actions/send-text-message/send-text-message.mjs b/components/noor/actions/send-text-message/send-text-message.mjs new file mode 100644 index 0000000000000..7024688a62b01 --- /dev/null +++ b/components/noor/actions/send-text-message/send-text-message.mjs @@ -0,0 +1,46 @@ +import app from "../../noor.app.mjs"; + +export default { + key: "noor-send-text-message", + name: "Send Text Message", + description: "Send a message in a thread. [See the documentation](https://usenoor.notion.site/v0-e812ae5e5976420f81232fa1c0316e84)", + version: "0.0.1", + type: "action", + props: { + app, + spaceId: { + propDefinition: [ + app, + "spaceId", + ], + }, + thread: { + propDefinition: [ + app, + "thread", + ], + }, + text: { + propDefinition: [ + app, + "text", + ], + }, + }, + async run({ $ }) { + const response = await this.app.sendMessage({ + $, + data: { + spaceId: this.spaceId, + thread: this.thread, + text: this.text, + }, + }); + + if (response.error) throw new Error(response?.error); + + $.export("$summary", `Successfully sent message to '${this.thread}' thread`); + + return response; + }, +}; diff --git a/components/noor/noor.app.mjs b/components/noor/noor.app.mjs new file mode 100644 index 0000000000000..bd32273dc1158 --- /dev/null +++ b/components/noor/noor.app.mjs @@ -0,0 +1,52 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "noor", + propDefinitions: { + thread: { + type: "string", + label: "Thread", + description: "Thread to where the message will be sent", + }, + text: { + type: "string", + label: "Text", + description: "Text of the message", + }, + spaceId: { + type: "string", + label: "Space ID", + description: "ID of the space, can be viewed in your space settings", + }, + }, + methods: { + _baseUrl() { + return "https://sun.noor.to/api/v0"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "Authorization": `Bearer ${this.$auth.api_key}`, + "Content-Type": "application/json", + }, + }); + }, + async sendMessage(args = {}) { + return this._makeRequest({ + method: "post", + path: "/sendMessage", + ...args, + }); + }, + }, +}; diff --git a/components/noor/package.json b/components/noor/package.json new file mode 100644 index 0000000000000..42ad01d436733 --- /dev/null +++ b/components/noor/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/noor", + "version": "0.1.0", + "description": "Pipedream Noor Components", + "main": "noor.app.mjs", + "keywords": [ + "pipedream", + "noor" + ], + "homepage": "https://pipedream.com/apps/noor", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/nordigen/README.md b/components/nordigen/README.md index 057568815ccec..3dd8954c9ea1f 100644 --- a/components/nordigen/README.md +++ b/components/nordigen/README.md @@ -1,16 +1,11 @@ # Overview -What You Can Build with the Nordigen API +The GoCardless Bank Account Data API furnishes you with the ability to access enriched banking data, enabling the creation of financial insights and streamlining customer verification processes. By integrating this API with Pipedream, you can automate various tasks involving bank account transactions and customer data analysis. For instance, you might set up workflows to monitor transactions for accounting purposes, verify customer account details for KYC compliance, or aggregate financial data to provide personalized financial advice. -The Nordigen API allows developers to easily access financial data and use it -to build the solutions their customers need. With the Nordigen API, you can -build innovative solutions such as: +# Example Use Cases -- Financial products that rate and compare banking services -- Money management tools that generate personalised budgeting plans -- Automated personal finance solutions -- Robo advisors for financial investment advice -- Predictive analytics for cash flow forecasting -- Digital banking solutions for customers -- Complex risk profiling solutions -- Customer segmentation analytics +- **Transaction Monitoring for Accounting Software**: Connect GoCardless Bank Account Data with accounting software like QuickBooks. Automatically detect new transactions, categorize them, and log them into the accounting ledger within QuickBooks. This ensures real-time bookkeeping and a consistent overview of financial flows. + +- **KYC & Customer Verification**: Pair the GoCardless Bank Account Data API with a CRM platform such as Salesforce. Use Pipedream workflows to validate new customer bank details and automatically update their verification status in the CRM. This simplifies the onboarding process and enhances customer relationship management. + +- **Personalized Financial Advice**: Integrate GoCardless API with a machine learning platform or a service like Google Sheets. Collect client's financial data, analyze spending habits, and use this data to offer personalized financial advice or create tailored investment strategies that are automatically shared with clients via email or a web portal. diff --git a/components/northflank/README.md b/components/northflank/README.md new file mode 100644 index 0000000000000..5fb20cf93b115 --- /dev/null +++ b/components/northflank/README.md @@ -0,0 +1,11 @@ +# Overview + +The Northflank API offers a range of functionality for managing and automating deployment workflows, handling various aspects from creating and managing services to scaling and monitoring applications. On Pipedream, you can use this API to create seamless automation that connects your deployment processes with other services and tools, crafting custom continuous deployment pipelines, generating real-time alerts, or automating service scaling based on specific triggers. + +# Example Use Cases + +- **Continuous Deployment Pipeline**: Create a workflow on Pipedream that listens to GitHub push events. When new code is pushed to your repository, trigger a Northflank API call to build and deploy the latest version of your application. Connect this with Slack to send deployment notifications to a team channel. + +- **Automated Backup on Deployment**: Whenever a new deployment is successful via Northflank, use Pipedream to trigger a backup process. The workflow can connect to a cloud storage service like Google Drive or Dropbox, uploading the current state of the database or specific data files, ensuring you have regular backups coinciding with your deployment schedule. + +- **Dynamic Resource Scaling**: Leverage metrics from a monitoring service like Datadog to inform your Northflank services about when to scale up or down. Set up a Pipedream workflow that receives CPU or memory usage alerts, and based on predefined thresholds, makes API calls to Northflank to adjust resource allocation accordingly, maintaining optimal application performance. diff --git a/components/notarize/README.md b/components/notarize/README.md index 1207eb41b1d4c..3374443e70470 100644 --- a/components/notarize/README.md +++ b/components/notarize/README.md @@ -1,25 +1,11 @@ # Overview -Notarize is an API that allows you to conduct secure and legally binding -eNotary transactions. This API enables you to easily integrate with your -existing applications to quickly and reliably create, notarize, and store -digital documents, products, or services around the world. +The Notarize API allows for seamless integration of remote electronic notarization into your digital workflow, enabling documents to be legally notarized online. By leveraging Pipedream's serverless platform, you can automate the notarization process, sync it with your business applications, and respond to notarization events in real-time. This could range from auto-submitting documents for notarization, updating customer records, to sending notifications upon completion of notarizations. -Notarize provides users with the ability to digitally notarize documents within -minutes. You can effectively sanitize your processes, eliminate manual and -paper-based steps, and verify your signer's identity all without leaving your -home or office. +# Example Use Cases -Using Notarize's API, you can quickly and securely create a variety of services -and products, including but not limited to: +- **Automated Document Submission for Notarization**: Trigger a workflow in Pipedream when a document is ready for notarization in your system (e.g., a CRM). The workflow uploads the document to Notarize, submits it for notarization, and then captures the notarization status, updating the CRM record with the new status. -- Digitally signed legal contracts -- Online loans and rental agreements -- Notarized wills and trusts -- Digital passports and visas -- Interstate marriages -- Real estate transactions -- Notarized medical documents -- Charity and campaign donations -- Financing and equity documents -- IPO and M&A documents +- **Real-Time Notarization Updates to Slack**: Set up a Pipedream workflow that listens for webhook events from Notarize indicating the status of document notarization. On receiving a completed notarization event, the workflow sends a notification to a designated Slack channel, providing the team with instant updates about the notarization process. + +- **Automated Follow-Up Emails Post-Notarization**: Create a Pipedream workflow that, upon successful notarization of a document, triggers an automated email to the relevant parties. The email could contain the notarized document or a link to download it, and integrates with an email service like SendGrid for email delivery, ensuring participants receive the necessary documents without delay. diff --git a/components/notarize/package.json b/components/notarize/package.json new file mode 100644 index 0000000000000..76d6cf25184cf --- /dev/null +++ b/components/notarize/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/notarize", + "version": "0.6.0", + "description": "Pipedream notarize Components", + "main": "notarize.app.mjs", + "keywords": [ + "pipedream", + "notarize" + ], + "homepage": "https://pipedream.com/apps/notarize", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/noticeable/README.md b/components/noticeable/README.md index 7dc427fce2be2..e89ad39668259 100644 --- a/components/noticeable/README.md +++ b/components/noticeable/README.md @@ -1,30 +1,11 @@ # Overview -What you can build using the Noticeable API +The Noticeable API on Pipedream allows you to automate the management of your project's news and updates efficiently. By integrating Noticeable with Pipedream, you can trigger workflows based on new publications, update timelines, and synchronize content across various platforms. This can help keep your users or team informed about the latest changes, releases or announcements with minimal manual intervention. -Noticeable is an API designed to make it easier for developers to build -applications and services for healthcare, elderly care, home automation and -beyond. Using the Noticeable API, developers can create and manage applications -that automate everyday tasks and enable social interaction, increase senior -safety, and enable health care professionals to remote monitor their patients. +# Example Use Cases -The Noticeable API lets developers create applications for: +- **Automated Release Notes Distribution**: When a new notice is published on Noticeable, trigger a Pipedream workflow to distribute the update across multiple channels. The workflow could post the release notes to Slack for internal team notification, send an email to subscribers via SendGrid, and tweet the update on Twitter for broader reach. -- Motion Detection & Notification: Create motion detection systems, connected - doorbells and environmental monitors. -- Remote Video Surveillance: Create surveillance systems powered by wireless - cameras. -- Automation & Robotics: Create robotic cleaners and automated systems for - senior safety. -- Location & Tracking Solutions: Develop location-based tracking and location - sharing applications. -- Health & Wellness Monitoring: Monitor health metrics, create health - dashboards, and gain better insights into the health of the elderly and - patients. -- Social Interactions: Enable seniors and patients to communicate more easily - with theircaregivers and connect with other users remotely. -- Home & Hospital Care: Develop applications and services to assist in home - care and hospital care, including prescription delivery and medication - reminders. -- Automated Home Care: Automate care for family members that need special - attention, such as tips for energy efficiency and protection against theft. +- **User Feedback Gathering**: After an update is announced on Noticeable, use Pipedream to automate the collection of user feedback. Set up a workflow that sends a Typeform survey to gather user impressions and automatically store the responses in a Google Sheet for easy analysis and follow-up. + +- **Project Management Sync**: Integrate Noticeable with project management tools like Trello or Asana. Every time a new update is posted on Noticeable, a Pipedream workflow can create a new card or task in your project management tool, ensuring that your development team is always aligned with the latest updates and priorities. diff --git a/components/notion/README.md b/components/notion/README.md index de3c8cf3dfc64..8917a45a49490 100644 --- a/components/notion/README.md +++ b/components/notion/README.md @@ -1,5 +1,81 @@ +# Overview + +Notion's API allows for the creation, reading, updating, and deleting of pages, databases, and their contents within Notion. Using Pipedream's platform, you can build workflows that connect Notion with various other services to automate tasks such as content management, task tracking, and data synchronization. With Pipedream's serverless execution, you can trigger these workflows on a schedule, or by external events from other services, without managing any infrastructure. + +# Example Use Cases + +- **Content Sync Between Notion and a CMS**: Automatically push new blog posts from Notion to WordPress, ensuring a seamless content flow from drafting to publishing. + +- **Task Management with Todoist Integration**: When a new task is added to a Notion database, create a corresponding task in Todoist, and vice versa, keeping task lists synced across platforms. + +- **Daily Sales Report Generation**: Gather sales data from a tool like Shopify at the end of each day, sum up total sales, and create a page in Notion with the day's sales summary for easy reporting. + +# Getting Started + +To get started, first log in to or create your [Pipedream account](https://pipedream.com) and start a new workflow. + +Add a Notion action or trigger to your workflow, then click **Select a Notion account** to open a Notion connection window: + +![Selecting your Notion API account](https://res.cloudinary.com/pipedreamin/image/upload/v1715267108/marketplace/apps/notion/CleanShot_2024-05-09_at_11.04.34_pdb2rx.png) + +From within this window, select pages you'd like Pipedream to access: + +![Select the Notion pages that you'd like Pipedream to have access to](https://res.cloudinary.com/pipedreamin/image/upload/v1715267109/marketplace/apps/notion/CleanShot_2024-05-09_at_11.04.44_awccry.png) + +Click **Accept** to connect your Notion account to Pipedream. + # Troubleshooting -Note: After creating a new database, please reconnect your account and select it to enable access to Pipedream. +## Unable to find a new database + +After creating a new database, reconnect your account and select it to enable Pipedream access. + +## HTTP errors + +### 400 "invalid_json" +Ensure the request body is formatted as valid JSON. + +### 400 "invalid_request_url" +The URL in your request is incorrect. Ensure the correct format and parameters. + +### 400 "invalid_request" +The request type isn't supported. Ensure the correct format and parameters. + +### 400 "invalid_grant" +Your authorization grant or refresh token is invalid, expired, or mismatched. Refer to OAuth 2.0 documentation. + +### 400 "validation_error" +The request body doesn't match the expected parameters schema. You're most likely missing a key parameter, or a parameter is malformed. + +### 400 "missing_version" +If your request lacks the required `Notion-Version` header, try adding a new Notion step in Pipedream, as it should handle this automatically. + +### 401 "unauthorized" +If your API token is invalid, reconnect your Notion account to Pipedream. + +### 403 "restricted_resource" +Your API token doesn't have permission to perform this operation. Try reconnecting your Notion account to Pipedream. + +### 404 "object_not_found" +The requested resource doesn't exist or isn't shared with your API token. Double check that the block, database or page exists. + +### 409 "conflict_error" +A conflict occurred, possibly due to outdated parameters. Try updating your parameters. + +### 429 "rate_limited" +You've exceeded the number of allowed requests. Slow down your request rate. + +### 500 "internal_server_error" +An unexpected error has occurred. Contact Notion support if it persists. + +### 502 "bad_gateway" +Notion had a problem processing your request, possibly due to an upstream server issue. + +### 503 "service_unavailable" +Notion is temporarily unavailable, possibly due to a long response time. Try again later. + +### 503 "database_connection_unavailable" +Notion's database cannot be queried at the moment. Try again later. -Please [reach out](https://pipedream.com/support/) to the Pipedream team with any technical issues or questions about the Notion integration. We're happy to help! +### 504 "gateway_timeout" +Your request to Notion timed out. Try resending it after a while. diff --git a/components/notion/actions/append-block/append-block.mjs b/components/notion/actions/append-block/append-block.mjs index 0f64ccacec0a5..dbc581d1453e4 100644 --- a/components/notion/actions/append-block/append-block.mjs +++ b/components/notion/actions/append-block/append-block.mjs @@ -6,7 +6,7 @@ export default { key: "notion-append-block", name: "Append Block to Parent", description: "Creates and appends blocks to the specified parent. [See the documentation](https://developers.notion.com/reference/patch-block-children)", - version: "0.2.13", + version: "0.2.17", type: "action", props: { notion, @@ -47,6 +47,16 @@ export default { optional: true, }, }, + methods: { + ...base.methods, + chunkArray(array, chunkSize = 100) { + const chunks = []; + for (let i = 0; i < array.length; i += chunkSize) { + chunks.push(array.slice(i, i + chunkSize)); + } + return chunks; + }, + }, async run({ $ }) { const children = []; // add blocks from blockObjects @@ -97,8 +107,17 @@ export default { return; } - const { results } = await this.notion.appendBlock(this.pageId, children); - $.export("$summary", `Appended ${results.length} block(s) successfully`); - return results; + const results = []; + const chunks = this.chunkArray(children); + + for (const chunk of chunks) { + const { results: payload } = await this.notion.appendBlock(this.pageId, chunk); + results.push(payload); + } + + const totalAppended = results.reduce((sum, res) => sum + res.length, 0); + + $.export("$summary", `Appended ${totalAppended} block(s) successfully`); + return results.flat(); }, }; diff --git a/components/notion/actions/common/base-page-builder.mjs b/components/notion/actions/common/base-page-builder.mjs index 31c3a8c99cc32..af11d746910ac 100644 --- a/components/notion/actions/common/base-page-builder.mjs +++ b/components/notion/actions/common/base-page-builder.mjs @@ -79,11 +79,12 @@ export default { */ _filterProps(properties = {}) { return Object.keys(properties) - .filter((property) => this[property] != null) + .filter((property) => this[property] != null + || (this.properties && this.properties[property])) .map((property) => ({ type: properties[property]?.type ?? property, label: property, - value: this[property], + value: this[property] || this.properties[property], })); }, /** diff --git a/components/notion/actions/create-comment/create-comment.mjs b/components/notion/actions/create-comment/create-comment.mjs new file mode 100644 index 0000000000000..8471473e20d62 --- /dev/null +++ b/components/notion/actions/create-comment/create-comment.mjs @@ -0,0 +1,53 @@ +import notion from "../../notion.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "notion-create-comment", + name: "Create Comment", + description: "Creates a comment in a page or existing discussion thread. [See the documentation](https://developers.notion.com/reference/create-a-comment)", + version: "0.0.1", + type: "action", + props: { + notion, + pageId: { + propDefinition: [ + notion, + "pageId", + ], + description: "Unique identifier of a page. Either this or a Discussion ID is required (not both)", + optional: true, + }, + discussionId: { + type: "string", + label: "Discussion ID", + description: "A UUID identifier for a discussion thread. Either this or a Page ID is required (not both)", + optional: true, + }, + comment: { + type: "string", + label: "Comment", + description: "The comment text", + }, + }, + async run({ $ }) { + if ((this.pageId && this.discussionId) || (!this.pageId && !this.discussionId)) { + throw new ConfigurationError("Either a Page ID or a Discussion ID is required (not both)"); + } + + const response = await this.notion._getNotionClient().comments.create({ + parent: this.pageId && { + page_id: this.pageId, + }, + discussion_id: this.discussionId, + rich_text: [ + { + text: { + content: this.comment, + }, + }, + ], + }); + $.export("$summary", `Successfully added comment with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/notion/actions/create-page-from-database/create-page-from-database.mjs b/components/notion/actions/create-page-from-database/create-page-from-database.mjs index 0eff58a08f1f5..971c77ec95102 100644 --- a/components/notion/actions/create-page-from-database/create-page-from-database.mjs +++ b/components/notion/actions/create-page-from-database/create-page-from-database.mjs @@ -6,8 +6,8 @@ export default { ...base, key: "notion-create-page-from-database", name: "Create Page from Database", - description: "Creates a page from a database. [See the docs](https://developers.notion.com/reference/post-page)", - version: "0.1.12", + description: "Creates a page from a database. [See the documentation](https://developers.notion.com/reference/post-page)", + version: "0.1.15", type: "action", props: { notion, @@ -37,10 +37,16 @@ export default { ], reloadProps: true, }, + alert: { + type: "alert", + alertType: "info", + content: "This action will create an empty page by default. To add content, use the `pageContent` prop below.", + }, pageContent: { type: "string", label: "Page Content", description: "Content of the page. You can use Markdown syntax [See docs](https://www.notion.so/help/writing-and-editing-basics#markdown-&-shortcuts)", + optional: true, }, }, async additionalProps() { diff --git a/components/notion/actions/create-page/create-page.mjs b/components/notion/actions/create-page/create-page.mjs index 6188c0a867fa5..b818d7c3a92db 100644 --- a/components/notion/actions/create-page/create-page.mjs +++ b/components/notion/actions/create-page/create-page.mjs @@ -7,7 +7,7 @@ export default { key: "notion-create-page", name: "Create Page", description: "Creates a page from a parent page. The only valid property is *title*. [See the documentation](https://developers.notion.com/reference/post-page)", - version: "0.2.10", + version: "0.2.13", type: "action", props: { notion, @@ -36,6 +36,7 @@ export default { type: "string", label: "Page Content", description: "Content of the page. You can use Markdown syntax [See docs](https://www.notion.so/help/writing-and-editing-basics#markdown-&-shortcuts)", + optional: true, }, }, async additionalProps() { diff --git a/components/notion/actions/duplicate-page/duplicate-page.mjs b/components/notion/actions/duplicate-page/duplicate-page.mjs index 2bdf1b2626561..6d21c41091280 100644 --- a/components/notion/actions/duplicate-page/duplicate-page.mjs +++ b/components/notion/actions/duplicate-page/duplicate-page.mjs @@ -6,8 +6,8 @@ export default { ...base, key: "notion-duplicate-page", name: "Duplicate Page", - description: "Creates a new page copied from an existing page block. [See the docs](https://developers.notion.com/reference/post-page)", - version: "0.0.7", + description: "Creates a new page copied from an existing page block. [See the documentation](https://developers.notion.com/reference/post-page)", + version: "0.0.9", type: "action", props: { notion, diff --git a/components/notion/actions/update-page/update-page.mjs b/components/notion/actions/update-page/update-page.mjs index e418140a5ecfe..b1fcdd6a7fa8b 100644 --- a/components/notion/actions/update-page/update-page.mjs +++ b/components/notion/actions/update-page/update-page.mjs @@ -6,8 +6,8 @@ export default { ...base, key: "notion-update-page", name: "Update Page", - description: "Updates page property values for the specified page. Properties that are not set will remain unchanged. To append page content, use the *append block* action. [See the docs](https://developers.notion.com/reference/patch-page)", - version: "1.1.1", + description: "Updates page property values for the specified page. Properties that are not set will remain unchanged. To append page content, use the *append block* action. [See the documentation](https://developers.notion.com/reference/patch-page)", + version: "1.1.3", type: "action", props: { notion, @@ -54,13 +54,23 @@ export default { }, }, async additionalProps() { - const { properties } = await this.notion.retrieveDatabase(this.parent); - const selectedProperties = pick(properties, this.propertyTypes); + try { + const { properties } = await this.notion.retrieveDatabase(this.parent); + const selectedProperties = pick(properties, this.propertyTypes); - return this.buildAdditionalProps({ - properties: selectedProperties, - meta: this.metaTypes, - }); + return this.buildAdditionalProps({ + properties: selectedProperties, + meta: this.metaTypes, + }); + } catch { + return { + properties: { + type: "object", + label: "Properties", + description: "The property values to update for the page. The keys are the names or IDs of the property and the values are property values. If a page property ID is not included, then it is not changed. Example: `{ \"Name\": \"Tuscan Kale\", \"Description\": \"A dark green leafy vegetable\" }`", + }, + }; + } }, methods: { ...base.methods, @@ -77,16 +87,44 @@ export default { properties, }; }, + parseProperties(properties) { + if (!properties) { + return undefined; + } + if (typeof properties === "string") { + try { + return JSON.parse(properties); + } catch { + throw new Error("Could not parse properties as JSON object"); + } + } + const parsedProperties = {}; + for (const [ + key, + value, + ] of Object.entries(properties)) { + try { + parsedProperties[key] = typeof value === "string" + ? JSON.parse(value) + : value; + } catch { + parsedProperties[key] = value; + } + } + return parsedProperties; + }, }, async run({ $ }) { try { + this.properties = this.parseProperties(this.properties); + const currentPage = await this.notion.retrievePage(this.pageId); const page = this.buildPage(currentPage); const response = await this.notion.updatePage(this.pageId, page); $.export("$summary", "Updated page successfully"); return response; } catch (error) { - throw new Error(error.body); + throw new Error(error.body || error); } }, }; diff --git a/components/notion/common/notion-page-properties.mjs b/components/notion/common/notion-page-properties.mjs index b12dc2cf4df85..621e304c3e352 100644 --- a/components/notion/common/notion-page-properties.mjs +++ b/components/notion/common/notion-page-properties.mjs @@ -64,12 +64,16 @@ const NOTION_PAGE_PROPERTIES = { }, date: { type: "string", - example: "2022-05-15T18:47:00.000Z", + example: "2022-05-15T18:47:00.000Z or { \"start\": \"2022-05-15T18:47:00.000Z\", \"end\": \"2022-06-15T18:47:00.000Z\" }", options: () => undefined, convertToNotion: (property) => ({ - date: { - start: property.value, - }, + date: !(typeof (property.value) === "string") + ? property.value + : property.value.trim().startsWith("{") + ? JSON.parse(property.value) + : { + start: property.value, + }, }), }, people: { diff --git a/components/notion/package.json b/components/notion/package.json index 5129322416405..c06202a516975 100644 --- a/components/notion/package.json +++ b/components/notion/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/notion", - "version": "0.1.17", + "version": "0.3.0", "description": "Pipedream Notion Components", "main": "notion.app.mjs", "keywords": [ @@ -11,12 +11,14 @@ "author": "Pipedream (https://pipedream.com/)", "dependencies": { "@notionhq/client": "^2.2.3", + "@pipedream/platform": "^3.0.3", "@tryfabric/martian": "^1.2.4", "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "delayed-stream": "^1.0.0", "form-data": "^3.0.1", "lodash-es": "^4.17.21", + "md5": "^2.3.0", "mime-db": "^1.52.0", "mime-types": "^2.1.35", "node-fetch": "^2.6.7", diff --git a/components/notion/sources/new-comment-created/new-comment-created.mjs b/components/notion/sources/new-comment-created/new-comment-created.mjs new file mode 100644 index 0000000000000..b8b04ae1ebb5a --- /dev/null +++ b/components/notion/sources/new-comment-created/new-comment-created.mjs @@ -0,0 +1,59 @@ +import base from "../common/base.mjs"; + +export default { + ...base, + key: "notion-new-comment-created", + name: "New Comment Created", + description: "Emit new event when a new comment is created in a page or block. [See the documentation](https://developers.notion.com/reference/retrieve-a-comment)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...base.props, + pageId: { + propDefinition: [ + base.props.notion, + "pageId", + ], + description: "Unique identifier of a page or block", + }, + }, + methods: { + ...base.methods, + generateMeta(comment) { + return { + id: comment.id, + summary: `New Comment ID: ${comment.id}`, + ts: comment.created_time, + }; + }, + }, + async run() { + const lastTs = this.getLastCreatedTimestamp(); + let maxTs = lastTs; + let cursor; + + do { + const { + results, next_cursor: next, + } = await this.notion._getNotionClient().comments.list({ + block_id: this.pageId, + start_cursor: cursor, + page_size: 100, + }); + if (!results?.length) { + break; + } + for (const comment of results) { + const ts = Date.parse(comment.created_time); + if (ts >= lastTs) { + maxTs = Math.max(ts, maxTs); + this.$emit(comment, this.generateMeta(comment)); + } + } + cursor = next; + } while (cursor); + + this.setLastCreatedTimestamp(maxTs); + }, +}; diff --git a/components/notion/sources/updated-page/updated-page.mjs b/components/notion/sources/updated-page/updated-page.mjs index 95eac90ba100b..8df7f1e15855b 100644 --- a/components/notion/sources/updated-page/updated-page.mjs +++ b/components/notion/sources/updated-page/updated-page.mjs @@ -2,13 +2,14 @@ import notion from "../../notion.app.mjs"; import sampleEmit from "./test-event.mjs"; import base from "../common/base.mjs"; import constants from "../common/constants.mjs"; +import zlib from "zlib"; export default { ...base, key: "notion-updated-page", name: "Updated Page in Database", /* eslint-disable-line pipedream/source-name */ description: "Emit new event when a page in a database is updated. To select a specific page, use `Updated Page ID` instead", - version: "0.0.15", + version: "0.1.4", type: "source", dedupe: "unique", props: { @@ -19,6 +20,12 @@ export default { "databaseId", ], }, + includeNewPages: { + type: "boolean", + label: "Include New Pages", + description: "Set to `true` to emit events when pages are created. Set to `false` to ignore new pages.", + default: true, + }, properties: { propDefinition: [ notion, @@ -31,105 +38,183 @@ export default { description: "Only emit events when one or more of the selected properties have changed", optional: true, }, - includeNewPages: { - type: "boolean", - label: "Include New Pages", - description: "Emit events when pages are created or updated. Set to `true` to include newly created pages. Set to `false` to only emit updated pages. Defaults to `false`.", - default: false, + alert: { + type: "alert", + alertType: "info", + content: "Source not saving? Your database might be too large. If deployment takes longer than one minute, an error will occur.", }, }, hooks: { - async deploy() { - if (!this.properties?.length) { - return; - } + async activate() { + console.log("Activating: fetching pages and properties"); + this._setLastUpdatedTimestamp(Date.now()); const propertyValues = {}; - const pagesStream = this.notion.getPages(this.databaseId); + const propertiesToCheck = await this._getPropertiesToCheck(); + const params = this.lastUpdatedSortParam(); + const pagesStream = this.notion.getPages(this.databaseId, params); for await (const page of pagesStream) { - propertyValues[page.id] = {}; - for (const property of this.properties) { - propertyValues[page.id][property] = JSON.stringify(page.properties[property]); + for (const propertyName of propertiesToCheck) { + const currentValue = this._maybeRemoveFileSubItems(page.properties[propertyName]); + propertyValues[page.id] = { + ...propertyValues[page.id], + [propertyName]: currentValue, + }; } } this._setPropertyValues(propertyValues); }, + async deactivate() { + console.log("Deactivating: clearing states"); + this._setLastUpdatedTimestamp(null); + }, }, methods: { ...base.methods, + _getLastUpdatedTimestamp() { + return this.db.get(constants.timestamps.LAST_EDITED_TIME); + }, + _setLastUpdatedTimestamp(ts) { + this.db.set(constants.timestamps.LAST_EDITED_TIME, ts); + }, _getPropertyValues() { - return this.db.get("propertyValues"); + const compressed = this.db.get("propertyValues"); + const buffer = Buffer.from(compressed, "base64"); + const decompressed = zlib.inflateSync(buffer).toString(); + return JSON.parse(decompressed); }, _setPropertyValues(propertyValues) { - this.db.set("propertyValues", propertyValues); + const string = JSON.stringify(propertyValues); + const compressed = zlib.deflateSync(string).toString("base64"); + this.db.set("propertyValues", compressed); + }, + async _getPropertiesToCheck() { + if (this.properties?.length) { + return this.properties; + } + const { properties } = await this.notion.retrieveDatabase(this.databaseId); + return Object.keys(properties); + }, + _maybeRemoveFileSubItems(property) { + // Files & Media type: + // `url` and `expiry_time` are constantly updated by Notion, so ignore these fields + if (property.type === "files") { + const modified = structuredClone(property); + for (const file of modified.files) { + if (file.type === "file") { + delete file.file; + } + } + return modified; + } + return property; + }, + _generateMeta(obj, summary) { + const { id } = obj; + const title = this.notion.extractPageTitle(obj); + const ts = Date.now(); + return { + id: `${id}-${ts}`, + summary: `${summary}: ${title}`, + ts, + }; + }, + _emitEvent(page, changes = [], isNewPage = true) { + const meta = isNewPage + ? this._generateMeta(page, constants.summaries.PAGE_ADDED) + : this._generateMeta(page, constants.summaries.PAGE_UPDATED); + const event = { + page, + changes, + }; + this.$emit(event, meta); }, }, async run() { - const lastCheckedTimestamp = this.getLastUpdatedTimestamp(); - const lastCheckedTimestampDate = new Date(lastCheckedTimestamp); - const lastCheckedTimestampISO = lastCheckedTimestampDate.toISOString(); + const lastCheckedTimestamp = this._getLastUpdatedTimestamp(); const propertyValues = this._getPropertyValues(); - // Add a filter so that we only receive pages that have been updated since the last call. + if (!lastCheckedTimestamp) { + // recently updated (deactivated / activated), skip execution + console.log("Awaiting restart completion: skipping execution"); + return; + } + const params = { ...this.lastUpdatedSortParam(), filter: { timestamp: "last_edited_time", last_edited_time: { - after: lastCheckedTimestampISO, + on_or_after: new Date(lastCheckedTimestamp).toISOString(), }, }, }; let newLastUpdatedTimestamp = lastCheckedTimestamp; - + const propertiesToCheck = await this._getPropertiesToCheck(); const pagesStream = this.notion.getPages(this.databaseId, params); for await (const page of pagesStream) { - if (!this.isResultNew(page.last_edited_time, lastCheckedTimestamp)) { - // The call to getPages() includes a descending sort by last_edited_time. - // As soon as one page !isResultNew(), all of the following ones will also. - // NOTE: the last_edited_filter means this will never be called, - // but it's worth keeping as future-proofing. - break; - } + const changes = []; + let isNewPage = false; + let propertyHasChanged = false; newLastUpdatedTimestamp = Math.max( newLastUpdatedTimestamp, - Date.parse(page?.last_edited_time), + Date.parse(page.last_edited_time), ); - if (this.properties?.length) { - let propertyChangeFound = false; - for (const property of this.properties) { - const currentProperty = JSON.stringify(page.properties[property]); - if (!propertyValues[page.id] || currentProperty !== propertyValues[page.id][property]) { - propertyChangeFound = true; - propertyValues[page.id] = { - ...propertyValues[page.id], - [property]: currentProperty, - }; - } + if (lastCheckedTimestamp > Date.parse(page.last_edited_time)) { + break; + } + + for (const propertyName of propertiesToCheck) { + const previousValue = structuredClone(propertyValues[page.id]?.[propertyName]); + // value used to compare and to save to this.db + const currentValueToSave = this._maybeRemoveFileSubItems(page.properties[propertyName]); + // (unmodified) value that should be emitted + const currentValueToEmit = page.properties[propertyName]; + + const pageExistsInDB = propertyValues[page.id] != null; + const propertyChanged = + JSON.stringify(previousValue) !== JSON.stringify(currentValueToSave); + + if (pageExistsInDB && propertyChanged) { + propertyHasChanged = true; + propertyValues[page.id] = { + ...propertyValues[page.id], + [propertyName]: currentValueToSave, + }; + changes.push({ + property: propertyName, + previousValue, + currentValue: currentValueToEmit, + }); } - if (!propertyChangeFound) { - continue; + + if (!pageExistsInDB) { + isNewPage = true; + propertyHasChanged = true; + propertyValues[page.id] = { + [propertyName]: currentValueToSave, + }; + changes.push({ + property: propertyName, + previousValue, + currentValue: currentValueToEmit, + }); } } - if (!this.includeNewPages && page?.last_edited_time === page?.created_time) { + if (isNewPage && !this.includeNewPages) { + console.log(`Ignoring new page: ${page.id}`); continue; } - const meta = this.generateMeta( - page, - constants.types.PAGE, - constants.timestamps.LAST_EDITED_TIME, - constants.summaries.PAGE_UPDATED, - true, - ); - - this.$emit(page, meta); + if (propertyHasChanged) { + this._emitEvent(page, changes, isNewPage); + } } - this.setLastUpdatedTimestamp(newLastUpdatedTimestamp); + this._setLastUpdatedTimestamp(newLastUpdatedTimestamp); this._setPropertyValues(propertyValues); }, sampleEmit, diff --git a/components/nozbe_teams/README.md b/components/nozbe_teams/README.md index 8064037121495..979edc1d4dbcd 100644 --- a/components/nozbe_teams/README.md +++ b/components/nozbe_teams/README.md @@ -1,25 +1,11 @@ # Overview -Nozbe Teams API is an API that enables developers to create and manage team -workspaces without having to write a single line of code. With this API, an -organization can easily create and manage team workspaces to improve -collaboration and productivity. This includes setting up projects for teams, -assigning tasks and organizing workloads among teams. +The Nozbe Teams API enables automation of task and project management within the Nozbe Teams app. With this API on Pipedream, you can create, read, update, and delete tasks, projects, and comments, as well as manage team members and their activities. By tapping into this functionality, teams can streamline their workflows, synchronize project activities with other business tools, and enhance overall productivity with custom automation. -The Nozbe Teams API offers a number of advantages for businesses, such as: +# Example Use Cases -- Real-time synchronization -- Easy access to task information -- Automated workflow -- Integrated email reminders -- Personalized task assignments +- **Task Management Sync**: Sync tasks between Nozbe Teams and Google Calendar. When a new task is added to a specific project on Nozbe Teams, create an event in Google Calendar with the due date and details. This ensures that deadlines are visible across all platforms used by team members. -Here are some of the things you can build with Nozbe Teams API: +- **Project Activity Digest**: Generate a daily or weekly digest of project activity. Using the Nozbe Teams API, aggregate the completed tasks and comments within a specified period, and send this summary through email using the SendGrid app on Pipedream. Team leaders can stay informed of project progress without having to manually check each project on Nozbe Teams. -- Collaboration tools to make it easier to work together -- Real-time chat systems to keep your team connected -- Project management apps that help keep track of tasks -- Task-tracking systems to help your team stay organized -- Resource scheduling and tracking to maximize resources -- Automated assignment of tasks to team members -- Email reminders for tasks that need to be completed +- **Slack Task Notifications**: Integrate Nozbe Teams with Slack for real-time task updates. When a task is marked as completed in Nozbe Teams, send a notification to a designated Slack channel. This keeps the whole team updated on task completions and reinforces a culture of recognition and transparency in task management. diff --git a/components/npm/common/constants.mjs b/components/npm/common/constants.mjs new file mode 100644 index 0000000000000..296b1a61c57bd --- /dev/null +++ b/components/npm/common/constants.mjs @@ -0,0 +1,19 @@ +const WEBHOOK_ID = "webhookId"; +const SECRET = "secret"; + +const API = { + DEFAULT: { + BASE_URL: "https://api.npmjs.org", + VERSION_PATH: "", + }, + REGISTRY: { + BASE_URL: "https://registry.npmjs.org", + VERSION_PATH: "/-/npm/v1", + }, +}; + +export default { + API, + WEBHOOK_ID, + SECRET, +}; diff --git a/components/npm/npm.app.js b/components/npm/npm.app.js deleted file mode 100644 index 135c4ffc3f6f0..0000000000000 --- a/components/npm/npm.app.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - type: "app", - app: "npm", -}; diff --git a/components/npm/npm.app.mjs b/components/npm/npm.app.mjs new file mode 100644 index 0000000000000..d10043864d0bc --- /dev/null +++ b/components/npm/npm.app.mjs @@ -0,0 +1,32 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "npm", + methods: { + getUrl(path, api = constants.API.DEFAULT, withVersion = false) { + return `${api.BASE_URL}${withVersion && api.VERSION_PATH || ""}${path}`; + }, + makeRequest({ + $ = this, path, api, withVersion, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path, api, withVersion), + }); + }, + getPackageMetadata({ + packageName, ...args + } = {}) { + return this.makeRequest({ + api: constants.API.REGISTRY, + path: `/${packageName}`, + headers: { + "Accept": "application/vnd.npm.install-v1+json", + }, + ...args, + }); + }, + }, +}; diff --git a/components/npm/package.json b/components/npm/package.json index 9bb5a9032dc90..3d7976d3a461a 100644 --- a/components/npm/package.json +++ b/components/npm/package.json @@ -1,8 +1,8 @@ { "name": "@pipedream/npm", - "version": "0.3.6", + "version": "0.4.0", "description": "Pipedream Npm Components", - "main": "npm.app.js", + "main": "npm.app.mjs", "keywords": [ "pipedream", "npm" @@ -14,6 +14,6 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.2.0" + "@pipedream/platform": "^3.0.0" } } diff --git a/components/npm/sources/download-counts/download-counts.js b/components/npm/sources/download-counts/download-counts.js deleted file mode 100644 index 1e11440b1f86c..0000000000000 --- a/components/npm/sources/download-counts/download-counts.js +++ /dev/null @@ -1,49 +0,0 @@ -const npm = require("../../npm.app.js"); -const { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } = require("@pipedream/platform"); -const axios = require("axios"); - -module.exports = { - key: "npm-download-counts", - name: "npm Download Counts", - description: "Emit an event with the latest count of downloads for an npm package", - version: "0.0.2", - type: "source", - props: { - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - period: { - type: "string", - label: "Period", - description: "Select last-day, last-week or last-month.", - optional: false, - default: "last-day", - options: [ - "last-day", - "last-week", - "last-month", - ], - }, - package: { - type: "string", - label: "Package", - description: "Enter an npm package name. Leave blank for all", - optional: true, - default: "@pipedreamhq/platform", - }, - npm, - }, - async run() { - const npm_event = (await axios({ - method: "get", - url: `https://api.npmjs.org/downloads/point/${encodeURIComponent(this.period)}/${encodeURIComponent(this.package)}`, - })).data; - this.$emit(npm_event, { - summary: "" + npm_event.downloads, - }); - }, -}; diff --git a/components/npm/sources/download-counts/download-counts.mjs b/components/npm/sources/download-counts/download-counts.mjs new file mode 100644 index 0000000000000..84232ccafb1be --- /dev/null +++ b/components/npm/sources/download-counts/download-counts.mjs @@ -0,0 +1,69 @@ +import app from "../../npm.app.mjs"; + +export default { + key: "npm-download-counts", + name: "New Download Counts", + description: "Emit new event with the latest count of downloads for an npm package. [See the documentation](https://github.com/npm/registry/blob/main/docs/download-counts.md).", + version: "0.1.0", + type: "source", + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + description: "One day interval time is recommended because NPM only update metrics once a day. [See the documentation](https://github.com/npm/registry/blob/main/docs/download-counts.md#data-source).", + default: { + intervalSeconds: 60 * 60 * 24, + }, + }, + period: { + type: "string", + label: "Period", + description: "Select last-day, last-week or last-month.", + optional: false, + default: "last-day", + options: [ + "last-day", + "last-week", + "last-month", + ], + }, + packageName: { + type: "string", + label: "Package", + description: "Enter an npm package name. Leave blank for all", + optional: true, + }, + }, + methods: { + getDownloadCounts({ + period, packageName, ...args + } = {}) { + const basePath = `/downloads/point/${encodeURIComponent(period)}`; + return this.app.makeRequest({ + path: packageName + ? `${basePath}/${encodeURIComponent(packageName)}` + : basePath, + ...args, + }); + }, + }, + async run({ timestamp: ts }) { + const { + getDownloadCounts, + period, + packageName, + } = this; + + const response = await getDownloadCounts({ + period, + packageName, + }); + + this.$emit(response, { + id: ts, + summary: `New Download Count ${response.downloads}`, + ts, + }); + }, +}; diff --git a/components/npm/sources/new-package-version/new-package-version.mjs b/components/npm/sources/new-package-version/new-package-version.mjs new file mode 100644 index 0000000000000..cb11b00186d80 --- /dev/null +++ b/components/npm/sources/new-package-version/new-package-version.mjs @@ -0,0 +1,46 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import app from "../../npm.app.mjs"; + +export default { + key: "npm-new-package-version", + name: "New Package Version", + description: "Emit new event when a new version of an npm package is published. [See the documentation](https://github.com/npm/registry/blob/main/docs/responses/package-metadata.md)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + packageName: { + type: "string", + label: "Package", + description: "Enter an npm package name. Leave blank for all", + default: "@pipedream/platform", + }, + }, + async run() { + const { + app, + packageName, + } = this; + + const response = await app.getPackageMetadata({ + debug: true, + packageName, + }); + + const { "dist-tags": { latest: latestVersion } } = response; + + this.$emit(response, { + id: latestVersion, + summary: `New Package Version ${latestVersion}`, + ts: Date.parse(response.modified), + }); + }, +}; diff --git a/components/ntfy/actions/send-notification/send-notification.mjs b/components/ntfy/actions/send-notification/send-notification.mjs new file mode 100644 index 0000000000000..4c23e89c5249e --- /dev/null +++ b/components/ntfy/actions/send-notification/send-notification.mjs @@ -0,0 +1,56 @@ +import app from "../../ntfy.app.mjs"; + +export default { + key: "ntfy-send-notification", + name: "Send Notification", + description: "Send a notification using Ntfy. [See the documentation](https://docs.ntfy.sh/publish/).", + version: "0.0.1", + type: "action", + props: { + app, + topic: { + type: "string", + label: "Topic", + description: "The topic to which the notification will be sent", + }, + data: { + type: "string", + label: "Message", + description: "The message content of the notification", + }, + headers: { + type: "object", + label: "Headers", + description: "Optional headers to include in the request. [See the documentation](https://docs.ntfy.sh/publish/).", + optional: true, + }, + }, + methods: { + sendNotification({ + topic, ...args + } = {}) { + return this.app.post({ + path: `/${topic}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + sendNotification, + topic, + data, + headers, + } = this; + + const response = await sendNotification({ + $, + headers, + topic, + data, + }); + + $.export("$summary", `Successfully sent notification with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/ntfy/ntfy.app.mjs b/components/ntfy/ntfy.app.mjs new file mode 100644 index 0000000000000..7933474f09d0d --- /dev/null +++ b/components/ntfy/ntfy.app.mjs @@ -0,0 +1,25 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "ntfy", + methods: { + getUrl(path) { + return `${this.$auth.server}${path}`; + }, + _makeRequest({ + $ = this, path, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + }, +}; diff --git a/components/ntfy/package.json b/components/ntfy/package.json new file mode 100644 index 0000000000000..9b18e22fd40e3 --- /dev/null +++ b/components/ntfy/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/ntfy", + "version": "0.1.0", + "description": "Pipedream ntfy Components", + "main": "ntfy.app.mjs", + "keywords": [ + "pipedream", + "ntfy" + ], + "homepage": "https://pipedream.com/apps/ntfy", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" + } +} diff --git a/components/nudgify/README.md b/components/nudgify/README.md new file mode 100644 index 0000000000000..4fd10a94f8ee0 --- /dev/null +++ b/components/nudgify/README.md @@ -0,0 +1,11 @@ +# Overview + +The Nudgify API lets you create and manage social proof notifications, aimed at increasing user engagement and conversions on your website. By leveraging this API on Pipedream, you can automate the interaction with Nudgify notifications and integrate them into your site's workflow. This could include creating notifications based on user behavior, updating them in real-time as data changes, or deleting them once they're no longer needed. The Pipedream platform simplifies the process of working with the Nudgify API by handling authentication, providing a code-free interface for setting up workflows, and enabling connections with numerous other apps for extended functionality. + +# Example Use Cases + +- **Dynamic Social Proof Creation**: Trigger a Nudgify notification on your website when a new order is placed. Use Pipedream's trigger for a new row in a Google Sheets spreadsheet, where the sheet is updated with order details, to send data to Nudgify for a "recent purchase" notification. + +- **Real-time Stock Update Alerts**: Set up a notification for low stock alerts on your e-commerce platform. When the inventory for a popular item dips below a certain threshold in your database (like AWS DynamoDB), it triggers a Pipedream workflow that sends a 'low stock' nudge via Nudgify, prompting users to act fast. + +- **User Signup Acknowledgment**: Acknowledge new user signups by sending a "Welcome" nudge. When a new user registers via your app (e.g., through Auth0), trigger a Pipedream workflow that calls the Nudgify API to display a personalized welcome message on your site, enhancing user onboarding. diff --git a/components/nudgify/package.json b/components/nudgify/package.json index 3220a7a064cc1..f85d4a69d3648 100644 --- a/components/nudgify/package.json +++ b/components/nudgify/package.json @@ -15,4 +15,4 @@ "dependencies": { "@pipedream/platform": "^1.5.1" } -} \ No newline at end of file +} diff --git a/components/numverify/actions/validate-phone/validate-phone.mjs b/components/numverify/actions/validate-phone/validate-phone.mjs new file mode 100644 index 0000000000000..d234d288d745d --- /dev/null +++ b/components/numverify/actions/validate-phone/validate-phone.mjs @@ -0,0 +1,34 @@ +import numverify from "../../numverify.app.mjs"; + +export default { + key: "numverify-validate-phone", + name: "Validate Phone", + description: "Validates a phone number. [See the documentation](https://numverify.com/documentation)", + version: "0.0.1", + type: "action", + props: { + numverify, + number: { + type: "string", + label: "Number", + description: "It is most efficient to provide phone numbers in a strictly numeric format (e.g. `441179287870`), but NumVerify is also capable of processing numbers containing special characters (e.g. `+44 (0) 117 928 7870`).", + }, + countryCode: { + type: "string", + label: "Country Code", + description: "Specify only if working with national (local) phone numbers. 2-digit country code, such as `US`", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.numverify.validatePhone({ + $, + params: { + number: this.number, + country_code: this.countryCode, + }, + }); + $.export("$summary", `Successfully validated phone number ${this.number}`); + return response; + }, +}; diff --git a/components/numverify/numverify.app.mjs b/components/numverify/numverify.app.mjs new file mode 100644 index 0000000000000..52b975782b01c --- /dev/null +++ b/components/numverify/numverify.app.mjs @@ -0,0 +1,26 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "numverify", + methods: { + async _makeRequest({ + $ = this, params, ...args + }) { + return axios($, { + baseURL: "http://apilayer.net/api", + params: { + ...params, + access_key: `${this.$auth.api_key}`, + }, + ...args, + }); + }, + async validatePhone(args) { + return this._makeRequest({ + url: "/validate", + ...args, + }); + }, + }, +}; diff --git a/components/numverify/package.json b/components/numverify/package.json new file mode 100644 index 0000000000000..5639cdb1accc7 --- /dev/null +++ b/components/numverify/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/numverify", + "version": "0.1.0", + "description": "Pipedream numverify Components", + "main": "numverify.app.mjs", + "keywords": [ + "pipedream", + "numverify" + ], + "homepage": "https://pipedream.com/apps/numverify", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/nusii_proposals/README.md b/components/nusii_proposals/README.md index 70ac201b448da..bc109f8b886f0 100644 --- a/components/nusii_proposals/README.md +++ b/components/nusii_proposals/README.md @@ -1,18 +1,11 @@ # Overview -[Nusii Proposals API](https://nusii.com/) provides an easy and straightforward -way to automate your proposal creation and sending needs. With this API you can -build custom applications to create, manage, and send proposals to clients -right from within your own app or website. +The Nusii Proposals API enables users to automate and integrate their proposal workflow with other tools and services. With this API, you can create, send, and manage proposals, keep track of their status, and handle clients efficiently. Leveraging Pipedream's capabilities, you can construct workflows that respond to events in Nusii, like a new proposal acceptance, or trigger actions in Nusii based on activities in other apps. -Using the Nusii Proposals API, you can create a variety of proposal-related -solutions to suit your business needs. Here are some of the potential uses for -this API: +# Example Use Cases -- Digital document and proposal creation systems -- Add proposal requests to existing customer relationship management (CRM) - applications -- Integration with eCommerce solutions to add proposals to online orders -- Automated proposal generation and email delivery solutions -- Custom solutions to manage proposal reviews from multiple stakeholders -- Collaboration tools to easily share and work on proposals with colleagues +- **Automated Proposal Follow-ups**: When a proposal is sent and remains unopened for a set period, trigger a follow-up email sequence or a reminder task in your CRM to ensure timely engagement. + +- **Proposal Acceptance to Project Kick-off**: On proposal acceptance, automatically create a project in your project management tool, like Asana or Trello, and notify team members in Slack or Microsoft Teams to commence work. + +- **Client Onboarding Workflow**: After a proposal gets accepted, trigger a client onboarding sequence, which could include adding client details to a CRM like Salesforce, scheduling a kick-off call via Calendly, and creating an invoice in QuickBooks or Xero. diff --git a/components/nutshell/README.md b/components/nutshell/README.md new file mode 100644 index 0000000000000..8bd7c1488367c --- /dev/null +++ b/components/nutshell/README.md @@ -0,0 +1,11 @@ +# Overview + +The Nutshell API allows you to access and manage data within the Nutshell CRM platform, enabling you to automate tasks, sync information across apps, and build custom integrations. With Pipedream, these capabilities are harnessed through serverless workflows that can trigger actions within Nutshell or react to events, all with minimal setup. You can create leads, update contacts, manage sales processes, and more, all in a scalable and efficient manner. + +# Example Use Cases + +- **Sync Nutshell Leads to Google Sheets**: Automatically push new leads from Nutshell into a Google Sheet for further analysis or record keeping. This workflow can help sales teams keep track of leads without manual data entry. + +- **Send Slack Notifications for New Deals**: Set up a workflow where a Slack notification is sent to a specific channel whenever a new deal is created in Nutshell. This keeps the team informed in real-time and can prompt immediate action. + +- **Create Nutshell Tasks from Email**: When you receive an email that requires follow-up, have a Pipedream workflow parse the email and create a task in Nutshell. This ensures that nothing falls through the cracks and your CRM tasks are always up-to-date. diff --git a/components/nyckel/README.md b/components/nyckel/README.md new file mode 100644 index 0000000000000..695eba6c5ea61 --- /dev/null +++ b/components/nyckel/README.md @@ -0,0 +1,11 @@ +# Overview + +The Nyckel API offers machine learning capabilities, enabling you to add custom image and text classification to your applications without needing a data science background. With Nyckel, you can train models, make predictions, and refine your model iteratively as new data comes in. On Pipedream, you can integrate Nyckel to automate various tasks such as processing images uploaded to cloud storage, categorizing customer support tickets, or augmenting content moderation workflows. By harnessing the power of serverless on Pipedream, you can create efficient pipelines that respond in real-time to events, without managing infrastructure. + +# Example Use Cases + +- **Automated Image Sorting**: Trigger a Pipedream workflow whenever new images are uploaded to an S3 bucket. Use the Nyckel API to classify the images and then route them to different folders based on the prediction results. This can streamline the organization of visual content and support automated image-based processes. + +- **Content Moderation Automation**: Integrate Nyckel with social media platforms like Twitter. Whenever a new post is detected, evaluate the content using Nyckel’s text classification to detect potentially harmful or inappropriate content. Use this output to automatically flag or remove posts, ensuring a safer online environment. + +- **Customer Inquiry Categorization**: Connect your email platform to Pipedream and use Nyckel API to analyze and categorize incoming customer support emails. Based on the classification, you can automatically route messages to the appropriate department or flag high-priority issues, enhancing customer service efficiency. diff --git a/components/nyckel/actions/classify-image/classify-image.mjs b/components/nyckel/actions/classify-image/classify-image.mjs new file mode 100644 index 0000000000000..c7b3f712e8657 --- /dev/null +++ b/components/nyckel/actions/classify-image/classify-image.mjs @@ -0,0 +1,61 @@ +import nyckel from "../../nyckel.app.mjs"; +import commonImage from "../../common/common-image.mjs"; + +export default { + ...commonImage, + key: "nyckel-classify-image", + name: "Classify Image", + description: "Classifies image data based on pre-trained classifiers in Nyckel. [See the documentation](https://www.nyckel.com/docs#invoke-image)", + version: "0.0.1", + type: "action", + props: { + nyckel, + functionId: { + propDefinition: [ + nyckel, + "functionId", + ], + }, + ...commonImage.props, + labelCount: { + propDefinition: [ + nyckel, + "labelCount", + ], + }, + includeMetadata: { + propDefinition: [ + nyckel, + "includeMetadata", + ], + }, + capture: { + propDefinition: [ + nyckel, + "capture", + ], + }, + externalId: { + propDefinition: [ + nyckel, + "externalId", + ], + }, + }, + async run({ $ }) { + const response = await this.nyckel.invokeFunction({ + $, + functionId: this.functionId, + ...this.getImageData(), + params: { + labelCount: this.labelCount, + includeMetadata: this.includeMetadata, + capture: this.capture, + externalId: this.externalId, + }, + }); + + $.export("$summary", "Image classification completed successfully"); + return response; + }, +}; diff --git a/components/nyckel/actions/classify-text/classify-text.mjs b/components/nyckel/actions/classify-text/classify-text.mjs new file mode 100644 index 0000000000000..f00f582f88900 --- /dev/null +++ b/components/nyckel/actions/classify-text/classify-text.mjs @@ -0,0 +1,65 @@ +import nyckel from "../../nyckel.app.mjs"; + +export default { + key: "nyckel-classify-text", + name: "Classify Text", + description: "Classifies text data based on pre-trained classifiers in Nyckel. [See the documentation](https://www.nyckel.com/docs#invoke-text)", + version: "0.0.1", + type: "action", + props: { + nyckel, + functionId: { + propDefinition: [ + nyckel, + "functionId", + ], + }, + textInput: { + type: "string", + label: "Text Input", + description: "The text input to classify", + }, + labelCount: { + propDefinition: [ + nyckel, + "labelCount", + ], + }, + includeMetadata: { + propDefinition: [ + nyckel, + "includeMetadata", + ], + }, + capture: { + propDefinition: [ + nyckel, + "capture", + ], + }, + externalId: { + propDefinition: [ + nyckel, + "externalId", + ], + }, + }, + async run({ $ }) { + const response = await this.nyckel.invokeFunction({ + $, + functionId: this.functionId, + data: { + data: this.textInput, + }, + params: { + labelCount: this.labelCount, + includeMetadata: this.includeMetadata, + capture: this.capture, + externalId: this.externalId, + }, + }); + + $.export("$summary", "Successfully classified text data"); + return response; + }, +}; diff --git a/components/nyckel/actions/extract-text-from-image/extract-text-from-image.mjs b/components/nyckel/actions/extract-text-from-image/extract-text-from-image.mjs new file mode 100644 index 0000000000000..289b0514f4103 --- /dev/null +++ b/components/nyckel/actions/extract-text-from-image/extract-text-from-image.mjs @@ -0,0 +1,40 @@ +import nyckel from "../../nyckel.app.mjs"; +import commonImage from "../../common/common-image.mjs"; + +export default { + ...commonImage, + key: "nyckel-extract-text-from-image", + name: "Extract Text from Image", + description: "Extracts text from an image URL. [See the documentation](https://www.nyckel.com/docs#ocr-image)", + version: "0.0.1", + type: "action", + props: { + nyckel, + functionId: { + propDefinition: [ + nyckel, + "functionId", + ], + }, + ...commonImage.props, + includeRegions: { + propDefinition: [ + nyckel, + "includeRegions", + ], + }, + }, + async run({ $ }) { + const response = await this.nyckel.extractTextFromImageUrl({ + $, + functionId: this.functionId, + ...this.getImageData(), + params: { + includeRegions: this.includeRegions, + }, + }); + + $.export("$summary", "Successfully extracted text from image"); + return response; + }, +}; diff --git a/components/nyckel/common/common-image.mjs b/components/nyckel/common/common-image.mjs new file mode 100644 index 0000000000000..a18f1fad59693 --- /dev/null +++ b/components/nyckel/common/common-image.mjs @@ -0,0 +1,42 @@ +import nyckel from "../nyckel.app.mjs"; +import FormData from "form-data"; +import fs from "fs"; + +export default { + props: { + imageOrUrl: { + propDefinition: [ + nyckel, + "imageOrUrl", + ], + }, + }, + methods: { + getImageData() { + const { imageOrUrl } = this; + const isUrl = imageOrUrl.startsWith("http"); + if (isUrl) { + return { + data: { + data: imageOrUrl, + }, + headers: { + "Content-Type": "application/json", + }, + }; + } + + const data = new FormData(); + data.append("data", fs.createReadStream( + imageOrUrl.includes("tmp/") + ? imageOrUrl + : `/tmp/${imageOrUrl}`, + )); + + return { + data, + headers: data.getHeaders(), + }; + }, + }, +}; diff --git a/components/nyckel/nyckel.app.mjs b/components/nyckel/nyckel.app.mjs new file mode 100644 index 0000000000000..23dedcf06617d --- /dev/null +++ b/components/nyckel/nyckel.app.mjs @@ -0,0 +1,109 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "nyckel", + propDefinitions: { + functionId: { + type: "string", + label: "Function ID", + description: "Select a function or provide a custom function ID.", + async options({ prevContext: { cursor } }) { + const items = await this.listFunctions({ + params: { + cursor, + }, + }); + return { + context: { + cursor: items[items.length - 1].id, + }, + options: items.map(({ + id, name, + }) => ({ + label: name, + value: id, + })), + }; + }, + }, + imageOrUrl: { + type: "string", + label: "Image Path or URL", + description: "The path to an image file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp). Alternatively, you can pass the direct URL to a file.", + }, + includeRegions: { + type: "boolean", + label: "Include Regions", + description: "When set to true, return the regions of the image that contained text", + optional: true, + }, + labelCount: { + type: "integer", + label: "Label Count", + description: "The number of labels to return, along with their confidences. When not specified, only the highest confidence result is returned.", + optional: true, + }, + includeMetadata: { + type: "boolean", + label: "Include Metadata", + description: "Whether to include the label metadata in the response.", + optional: true, + default: false, + }, + capture: { + type: "boolean", + label: "Capture", + description: "Whether to enable invoke capture for this invoke. Invoke capture saves informative samples captured from your invokes for future annotation.", + optional: true, + default: true, + }, + externalId: { + type: "string", + label: "External ID", + description: "Your unique identifier for this sample. This will be used if this invoke is saved through Invoke Capture.", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://www.nyckel.com"; + }, + async _makeRequest({ + $ = this, headers, ...otherOpts + }) { + return axios($, { + ...otherOpts, + baseURL: this._baseUrl(), + headers: { + ...headers, + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + async listFunctions(args) { + return this._makeRequest({ + url: "/v1/functions", + ...args, + }); + }, + async extractTextFromImageUrl({ + functionId, ...args + }) { + return this._makeRequest({ + method: "POST", + url: `/v0.9/functions/${functionId}/ocr`, + ...args, + }); + }, + async invokeFunction({ + functionId, ...args + }) { + return this._makeRequest({ + method: "POST", + url: `/v1/functions/${functionId}/invoke`, + ...args, + }); + }, + }, +}; diff --git a/components/nyckel/package.json b/components/nyckel/package.json new file mode 100644 index 0000000000000..41b880a1a4950 --- /dev/null +++ b/components/nyckel/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/nyckel", + "version": "0.1.0", + "description": "Pipedream Nyckel Components", + "main": "nyckel.app.mjs", + "keywords": [ + "pipedream", + "nyckel" + ], + "homepage": "https://pipedream.com/apps/nyckel", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2", + "form-data": "^4.0.0" + } +} diff --git a/components/oanda/README.md b/components/oanda/README.md index a9b33f0e7267d..bd8204c0d7a40 100644 --- a/components/oanda/README.md +++ b/components/oanda/README.md @@ -1,26 +1,11 @@ # Overview -What You Can Build With the OANDA API +The OANDA API enables you to tap into the world of forex trading, providing real-time currency exchange rates, and the ability to automate trading strategies, manage accounts, access market data, and more. On Pipedream, you can craft workflows that capitalize on OANDA's capabilities, such as reacting to market changes, automating trades based on custom logic, and syncing forex data with other business applications for analysis and decision-making. -The OANDA API allows developers to build a wide range of financial trading -applications. It provides APIs for Spot, FX, and CFD Trading, Historical -Currency Conversion, and Exchange Rates. With the tools and services offered by -OANDA, you can create the following types of programs and applications: +# Example Use Cases -- Automated Trading System: With OANDA's API, you can create rules-based - trading systems which are capable of automatically executing trades based on - user-defined parameters. -- Currency Conversion: OANDA's API enables you to access and convert currency - exchange rates on demand. -- Market Data and Analysis: OANDA's APIs allow developers to access market data - and perform technical analysis to form trading decisions. -- Portfolio Management: OANDA's APIs enable developers to monitor and manage - portfolios of different financial instruments. -- Risk Management and Fraud Protection: OANDA's APIs help developers to control - risks and detect fraudulent activities in their trading applications. -- Regulatory Compliance: With OANDA's APIs, developers can ensure that their - investment applications comply with the necessary regulations. -- Multi-Currency Support: OANDA's APIs enable developers to easily add support - for multiple currencies to their applications. -- Algorithmic Trading: OANDA's APIs can be used to develop sophisticated - algorithms for trading various financial instruments. +- **Automated Trading Strategy**: Trigger a Pipedream workflow when specific forex signals are detected in OANDA. Use conditional logic to place trades automatically back on OANDA when certain technical indicators or price thresholds are met, ensuring timely execution of your trading strategy. + +- **Forex Data Sync for Analytics**: Schedule a recurring Pipedream workflow that fetches the latest exchange rates from OANDA and pushes them to a Google Sheets document. Leverage this for ongoing analysis or to fuel business intelligence dashboards that help inform company financial decisions. + +- **Market Alert Notifications**: Create a Pipedream workflow that monitors OANDA for significant market events or price movements. When specified conditions are detected, automatically send out alert notifications via email, SMS (Twilio), or messaging apps like Slack to keep you or your team instantly informed about critical market changes. diff --git a/components/oanda/package.json b/components/oanda/package.json new file mode 100644 index 0000000000000..20ec6ae68b4a1 --- /dev/null +++ b/components/oanda/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/oanda", + "version": "0.6.0", + "description": "Pipedream oanda Components", + "main": "oanda.app.mjs", + "keywords": [ + "pipedream", + "oanda" + ], + "homepage": "https://pipedream.com/apps/oanda", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/occasion/README.md b/components/occasion/README.md index 01e6d3ea03c2d..34b2d842a8d26 100644 --- a/components/occasion/README.md +++ b/components/occasion/README.md @@ -1,20 +1,11 @@ # Overview -The Occasion API enables you to create powerful web and mobile applications -that allow your customers to experience events more efficiently. With this API, -you can build applications that fulfill a variety of functions and needs. Here -are a few examples of what you can build: +The Occasion API provides a suite of tools to manage and automate event booking and scheduling. Through Pipedream, you can harness this API to create powerful automations by triggering workflows based on booking activity, syncing event data with other services, managing customer information, and more. Utilizing Pipedream's capability to connect with hundreds of apps, you can extend the functionality of the Occasion API to streamline operations, enhance customer experiences, and leverage event data in innovative ways. -- Online event registration applications that are secure and give users an easy - way to save and purchase tickets -- SEO-friendly websites for each event that present details, announcements, and - imagery -- Geolocation-based applications that allow customers to explore events near - them -- Mobile applications that enable users to keep track of events, purchases, and - schedules -- Social media applications that help users to spread the word about events -- Event analytics applications that measure the success of events and identify - best practices -- Personalized experiences for customers such as e-commerce, personalized - agendas, and more +# Example Use Cases + +- **Automated Event Confirmation and Follow-up Emails**: Use Occasion API to trigger a workflow on Pipedream when a new booking is made. Connect with an email service like SendGrid to automatically send confirmation emails to customers. Include a follow-up email sequence to ensure high engagement. + +- **Sync Event Bookings to Google Calendar**: After a booking is created via Occasion, automatically sync this information to a Google Calendar using Pipedream's Google Calendar integration. This keeps your schedule up-to-date and allows for seamless sharing of event details with your team. + +- **Customer Feedback Collection Post-Event**: Trigger a Pipedream workflow with the Occasion API once an event concludes. Connect to a survey platform like Typeform to send a feedback form to attendees. Gather responses and store them in a Google Sheet for easy analysis and future reference. diff --git a/components/occasion/package.json b/components/occasion/package.json new file mode 100644 index 0000000000000..f19596a32a9c1 --- /dev/null +++ b/components/occasion/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/occasion", + "version": "0.6.0", + "description": "Pipedream occasion Components", + "main": "occasion.app.mjs", + "keywords": [ + "pipedream", + "occasion" + ], + "homepage": "https://pipedream.com/apps/occasion", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/ocr_web_service/README.md b/components/ocr_web_service/README.md new file mode 100644 index 0000000000000..3a973d321cd64 --- /dev/null +++ b/components/ocr_web_service/README.md @@ -0,0 +1,11 @@ +# Overview + +The OCR Web Service API on Pipedream allows users to convert scanned documents, images, and PDFs into editable and searchable text formats. Leveraging OCR (Optical Character Recognition) technology, this API is powerful for extracting text data efficiently. On Pipedream, you can integrate OCR Web Service with various other platforms to automate workflows like document management, data entry, and content archiving, enhancing productivity and reducing manual errors across diverse business processes. + +# Example Use Cases + +- **Automated Invoice Processing**: Use the OCR Web Service to scan and extract data from uploaded invoices, then integrate with accounting software like QuickBooks to automatically update financial records and trigger payment processes. + +- **Digital Document Management System**: Build a workflow where documents uploaded to a cloud storage service like Google Drive are automatically sent through OCR Web Service to extract text. The extracted data can then be used to categorize and index documents for easy search and retrieval. + +- **Real-Time Content Moderation in User-Generated Content**: Implement a system where images or PDFs uploaded by users on a platform are processed through the OCR Web Service. Extracted text can be analyzed for compliance with content policies using a text moderation API, and appropriate actions can be taken based on the analysis. diff --git a/components/ocr_web_service/ocr_web_service.app.mjs b/components/ocr_web_service/ocr_web_service.app.mjs new file mode 100644 index 0000000000000..cbb7bb7867ddd --- /dev/null +++ b/components/ocr_web_service/ocr_web_service.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "ocr_web_service", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/ocr_web_service/package.json b/components/ocr_web_service/package.json new file mode 100644 index 0000000000000..0570dda942fe2 --- /dev/null +++ b/components/ocr_web_service/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/ocr_web_service", + "version": "0.0.1", + "description": "Pipedream OCR Web Service Components", + "main": "ocr_web_service.app.mjs", + "keywords": [ + "pipedream", + "ocr_web_service" + ], + "homepage": "https://pipedream.com/apps/ocr_web_service", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/ocrspace/README.md b/components/ocrspace/README.md index 19863de9c12e5..9e3bcbbabff98 100644 --- a/components/ocrspace/README.md +++ b/components/ocrspace/README.md @@ -1,13 +1,11 @@ # Overview -The OCRSpace API provides an efficient way to convert texts from images into -text formats with an accuracy of up to 90%. With the OCRSpace API, you can -build applications that are able to extract text from images and turn them into -a usable text format. Here are some examples of what you can build with the -OCRSpace API: - -- Automated data entry applications -- Document digitization projects -- Automated document indexing systems -- Text recognition apps -- Intelligent search engine applications +The OCRSpace API enables text extraction from images and PDFs, converting various image formats into editable and searchable data. This service is a game-changer for automating document handling processes, where the need to digitize content is crucial. Pipedream's platform allows for seamless integration of OCRSpace's capabilities with various apps to streamline workflows, such as organizing documents, populating databases, or even triggering event-driven actions based on the extracted content. + +# Example Use Cases + +- **Automated Invoice Processing**: Scan and extract data from uploaded invoices using OCRSpace, then use Pipedream to send the extracted data to accounting software like QuickBooks for reconciliation and archiving. This workflow can reduce manual data entry and speed up the accounting process. + +- **Digital Document Archival System**: Convert paper documents into searchable text with OCRSpace. With Pipedream, you can then store the extracted text in a database like Airtable or Google Sheets, and even initiate a backup to cloud storage solutions such as Dropbox or Google Drive, creating an organized, searchable archive. + +- **Event Registration Via Business Card Scans**: Use OCRSpace to gather contact information from scanned business cards at events. Pipedream can then automatically populate the data into a CRM like HubSpot or Salesforce, and trigger a follow-up email sequence via an email marketing platform like Mailchimp, enhancing lead management and engagement. diff --git a/components/ocrspace/actions/common/process-base.mjs b/components/ocrspace/actions/common/process-base.mjs new file mode 100644 index 0000000000000..2a8fbc60500d1 --- /dev/null +++ b/components/ocrspace/actions/common/process-base.mjs @@ -0,0 +1,76 @@ +import { ConfigurationError } from "@pipedream/platform"; +import FormData from "form-data"; +import { getUrlOrFile } from "../../common/utils.mjs"; +import ocrspace from "../../ocrspace.app.mjs"; + +export default { + props: { + ocrspace, + language: { + propDefinition: [ + ocrspace, + "language", + ], + }, + isOverlayRequired: { + propDefinition: [ + ocrspace, + "isOverlayRequired", + ], + }, + detectOrientation: { + propDefinition: [ + ocrspace, + "detectOrientation", + ], + }, + scale: { + propDefinition: [ + ocrspace, + "scale", + ], + }, + isTable: { + propDefinition: [ + ocrspace, + "isTable", + ], + }, + ocrEngine: { + propDefinition: [ + ocrspace, + "ocrEngine", + ], + }, + }, + async run({ $ }) { + const data = new FormData(); + const { + url, file, + } = getUrlOrFile(this.file); + + if (url) data.append("url", url); + if (file) data.append("base64Image", file); + if (this.imageLanguage) data.append("language", this.imageLanguage); + if (this.isOverlayRequired) data.append("isOverlayRequired", `${this.isOverlayRequired}`); + if (this.filetype) data.append("filetype", this.filetype); + if (this.detectOrientation) data.append("detectOrientation", `${this.detectOrientation}`); + if (this.scale) data.append("scale", `${this.scale}`); + if (this.isTable) data.append("isTable", `${this.isTable}`); + if (this.ocrEngine) data.append("OCREngine", this.ocrEngine); + + const response = await this.ocrspace.processImage({ + $, + data, + headers: data.getHeaders(), + }); + + $.export("$summary", this.getSummary()); + + if (response.ErrorMessage) { + throw new ConfigurationError(response.ErrorMessage[0]); + } + + return response; + }, +}; diff --git a/components/ocrspace/actions/process-image/process-image.mjs b/components/ocrspace/actions/process-image/process-image.mjs new file mode 100644 index 0000000000000..dace35e1e2ca1 --- /dev/null +++ b/components/ocrspace/actions/process-image/process-image.mjs @@ -0,0 +1,30 @@ +import common from "../common/process-base.mjs"; + +export default { + ...common, + key: "ocrspace-process-image", + name: "Process Image", + description: "Submits an image file for OCR processing using OCR.space. [See the documentation](https://ocr.space/ocrapi)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + file: { + propDefinition: [ + common.props.ocrspace, + "file", + ], + }, + filetype: { + propDefinition: [ + common.props.ocrspace, + "filetype", + ], + }, + }, + methods: { + getSummary() { + return "Image submitted for OCR processing."; + }, + }, +}; diff --git a/components/ocrspace/actions/process-pdf/process-pdf.mjs b/components/ocrspace/actions/process-pdf/process-pdf.mjs new file mode 100644 index 0000000000000..cda0790f5c620 --- /dev/null +++ b/components/ocrspace/actions/process-pdf/process-pdf.mjs @@ -0,0 +1,26 @@ +import common from "../common/process-base.mjs"; + +export default { + ...common, + key: "ocrspace-process-pdf", + name: "Process PDF for OCR", + description: "Submit a PDF for OCR processing. [See the documentation](https://ocr.space/ocrapi)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + file: { + propDefinition: [ + common.props.ocrspace, + "file", + ], + label: "PDF File", + description: "The URL of the PDF file or the path to the file saved to the `/tmp` directory (e.g. `/tmp/example.pdf`) to process. [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + }, + }, + methods: { + getSummary() { + return "Submitted PDF for OCR processing."; + }, + }, +}; diff --git a/components/ocrspace/common/constants.mjs b/components/ocrspace/common/constants.mjs new file mode 100644 index 0000000000000..ea51ce5ad0337 --- /dev/null +++ b/components/ocrspace/common/constants.mjs @@ -0,0 +1,117 @@ +export const LANGUAGE_OPTIONS = [ + { + label: "Arabic", + value: "ara", + }, + { + label: "Bulgarian", + value: "bul", + }, + { + label: "Chinese (Simplified)", + value: "chs", + }, + { + label: "Chinese (Traditional)", + value: "cht", + }, + { + label: "Croatian", + value: "hrv", + }, + { + label: "Czech", + value: "cze", + }, + { + label: "Danish", + value: "dan", + }, + { + label: "Dutch", + value: "dut", + }, + { + label: "English", + value: "eng", + }, + { + label: "Finnish", + value: "fin", + }, + { + label: "French", + value: "fre", + }, + { + label: "German", + value: "ger", + }, + { + label: "Greek", + value: "gre", + }, + { + label: "Hungarian", + value: "hun", + }, + { + label: "Korean", + value: "kor", + }, + { + label: "Italian", + value: "ita", + }, + { + label: "Japanese", + value: "jpn", + }, + { + label: "Polish", + value: "pol", + }, + { + label: "Portuguese", + value: "por", + }, + { + label: "Russian", + value: "rus", + }, + { + label: "Slovenian", + value: "slv", + }, + { + label: "Spanish", + value: "spa", + }, + { + label: "Swedish", + value: "swe", + }, + { + label: "Turkish", + value: "tur", + }, +]; + +export const IMAGE_FILETYPE_OPTIONS = [ + "GIF", + "PNG", + "JPG", + "TIF", + "BMP", +]; + +export const OCR_ENGINE_OPTIONS = [ + { + label: "OCR Engine 1", + value: "1", + }, + { + label: "OCR Engine 2", + value: "2", + }, +]; diff --git a/components/ocrspace/common/utils.mjs b/components/ocrspace/common/utils.mjs new file mode 100644 index 0000000000000..3384dcc350320 --- /dev/null +++ b/components/ocrspace/common/utils.mjs @@ -0,0 +1,34 @@ +import fs from "fs"; +import mime from "mime"; + +export const isValidUrl = (urlString) => { + var urlPattern = new RegExp("^(https?:\\/\\/)?" + // validate protocol +"((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // validate domain name +"((\\d{1,3}\\.){3}\\d{1,3}))" + // validate OR ip (v4) address +"(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // validate port and path +"(\\?[;&a-z\\d%_.~+=-]*)?" + // validate query string +"(\\#[-a-z\\d_]*)?$", "i"); // validate fragment locator + return !!urlPattern.test(urlString); +}; + +export const checkTmp = (filename) => { + if (filename.indexOf("/tmp") === -1) { + return `/tmp/${filename}`; + } + return filename; +}; + +export const getUrlOrFile = (url) => { + if (!isValidUrl(url)) { + const filePath = checkTmp(url); + const data = fs.readFileSync(filePath); + const mimeType = mime.getType(filePath); + const base64Image = Buffer.from(data, "binary").toString("base64"); + return { + file: `data:${mimeType};base64,${base64Image}`, + }; + } + return { + url, + }; +}; diff --git a/components/ocrspace/ocrspace.app.mjs b/components/ocrspace/ocrspace.app.mjs index 915aa5782b482..ae96d7db00db7 100644 --- a/components/ocrspace/ocrspace.app.mjs +++ b/components/ocrspace/ocrspace.app.mjs @@ -1,11 +1,90 @@ +import { axios } from "@pipedream/platform"; +import { + IMAGE_FILETYPE_OPTIONS, + LANGUAGE_OPTIONS, + OCR_ENGINE_OPTIONS, +} from "./common/constants.mjs"; + export default { type: "app", app: "ocrspace", - propDefinitions: {}, + propDefinitions: { + file: { + type: "string", + label: "Image", + description: "The URL of the image or the path to the file saved to the `/tmp` directory (e.g. `/tmp/example.jpg`) to process. [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + }, + language: { + type: "string", + label: "Language", + description: "Language setting for image OCR processing.", + options: LANGUAGE_OPTIONS, + optional: true, + }, + isOverlayRequired: { + type: "boolean", + label: "Is Overlay Required", + description: "If true, returns the coordinates of the bounding boxes for each word. If false, the OCR'ed text is returned only as a text block (this makes the JSON reponse smaller). Overlay data can be used, for example, to show [text over the image](https://ocr.space/english).", + optional: true, + }, + filetype: { + type: "string", + label: "File Type", + description: "Overwrites the automatic file type detection based on content-type. Supported image file formats are png, jpg (jpeg), gif, tif (tiff) and bmp. For document ocr, the api supports the Adobe PDF format. Multi-page TIFF files are supported.", + options: IMAGE_FILETYPE_OPTIONS, + optional: true, + }, + detectOrientation: { + type: "boolean", + label: "Detect Orientation", + description: "If set to true, the api autorotates the image correctly and sets the TextOrientation parameter in the JSON response. If the image is not rotated, then TextOrientation=0, otherwise it is the degree of the rotation, e. g. \"270\".", + optional: true, + }, + scale: { + type: "boolean", + label: "Scale", + description: "If set to true, the api does some internal upscaling. This can improve the OCR result significantly, especially for low-resolution PDF scans. Note that the front page demo uses scale=true, but the API uses scale=false by default. See also this OCR forum post.", + optional: true, + }, + isTable: { + type: "boolean", + label: "Is Table", + description: "If set to true, the OCR logic makes sure that the parsed text result is always returned line by line. This switch is recommended for [table OCR](https://ocr.space/tablerecognition), [receipt OCR](https://ocr.space/receiptscanning), invoice processing and all other type of input documents that have a table like structure.", + optional: true, + }, + ocrEngine: { + type: "string", + label: "OCR Engine", + description: "Engine 1 is default. [See OCR Engines](https://ocr.space/OCRAPI#ocrengine).", + options: OCR_ENGINE_OPTIONS, + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.ocr.space"; + }, + _headers(headers = {}) { + return { + "apikey": this.$auth.apikey, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(headers), + ...opts, + }); + }, + processImage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/parse/image", + ...opts, + }); }, }, }; diff --git a/components/ocrspace/package.json b/components/ocrspace/package.json new file mode 100644 index 0000000000000..a0ab1ae330c10 --- /dev/null +++ b/components/ocrspace/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/ocrspace", + "version": "0.1.0", + "description": "Pipedream OCRSpace Components", + "main": "ocrspace.app.mjs", + "keywords": [ + "pipedream", + "ocrspace" + ], + "homepage": "https://pipedream.com/apps/ocrspace", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "mime": "^4.0.6" + } +} diff --git a/components/octoparse/README.md b/components/octoparse/README.md new file mode 100644 index 0000000000000..50b041d980acd --- /dev/null +++ b/components/octoparse/README.md @@ -0,0 +1,11 @@ +# Overview + +The Octoparse API allows you to automate the extraction of web data without coding, making it a powerful tool for data-driven workflows. With this API, you can control your scraping tasks, retrieve extracted data, and manage your account programmatically. When combined with Pipedream's serverless execution environment, you can build custom workflows to process, store, or act upon the data fetched by Octoparse. This integration can be a cornerstone for solutions in market research, competitor analysis, price monitoring, or lead generation. + +# Example Use Cases + +- **Automated Data Collection to Google Sheets**: Trigger a scraping task in Octoparse and, upon completion, send the extracted data to Google Sheets. This workflow is ideal for users needing regular updates for market research or competitive analysis without manual intervention. + +- **Lead Generation with CRM Integration**: Use Octoparse to scrape contact information from target websites and automatically add these leads to a CRM platform like Salesforce. This workflow streamlines the process of expanding your sales funnel with fresh prospects. + +- **Price Monitoring and Alerts**: Set Octoparse to track product prices on e-commerce websites and, using Pipedream, create a workflow that sends alerts via email or Slack if prices drop below a specified threshold. This can be an asset for businesses monitoring pricing strategies or for consumers looking for deals. diff --git a/components/octoparse/octoparse.app.mjs b/components/octoparse/octoparse.app.mjs index 8e9bfd810c838..9fdc702a5e1b1 100644 --- a/components/octoparse/octoparse.app.mjs +++ b/components/octoparse/octoparse.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/octoparse/package.json b/components/octoparse/package.json index 5e2aeeed73f22..289eb9afecc6f 100644 --- a/components/octoparse/package.json +++ b/components/octoparse/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/octopus_deploy/README.md b/components/octopus_deploy/README.md new file mode 100644 index 0000000000000..f7bab9126b616 --- /dev/null +++ b/components/octopus_deploy/README.md @@ -0,0 +1,11 @@ +# Overview + +The Octopus Deploy API offers the ability to automate, integrate, and extend your deployment processes. With Pipedream, you can harness this API to create customized workflows that trigger actions within Octopus Deploy or respond to events from other apps. Imagine setting up deployments, creating releases, or managing your infrastructure programmatically, helping you to streamline your DevOps practices. + +# Example Use Cases + +- **Automate Deployment Triggers**: Create a workflow that listens for code pushes to GitHub, then automatically triggers a deployment in Octopus Deploy. Combine GitHub and Octopus Deploy on Pipedream to ensure your team's latest commits are deployed to the right environments without manual intervention. + +- **Synchronize Release Information**: After creating a new release in Octopus Deploy, you can automatically post the release notes to Slack. This keeps your entire team informed about new versions and deployment statuses, enhancing communication and transparency. + +- **Incident Management Integration**: Set up a workflow where, if a deployment fails, an incident is automatically created in PagerDuty. This ensures that your on-call engineers are immediately alerted to deployment issues, allowing them to react and resolve problems swiftly. diff --git a/components/octopush_sms/README.md b/components/octopush_sms/README.md new file mode 100644 index 0000000000000..93cf3b7d9f4ce --- /dev/null +++ b/components/octopush_sms/README.md @@ -0,0 +1,11 @@ +# Overview + +The Octopush SMS API lets you programmatically send text messages, providing a powerful way to reach out to customers directly on their phones. With Pipedream, you can create workflows that trigger SMS sending via Octopush based on various events, streamlining communication processes. You could use this API to send alerts, reminders, verification codes, or marketing messages, and integrate with other apps to respond to emails, form submissions, or updates in a CRM. + +# Example Use Cases + +- **SMS Alerts for Critical System Outages**: When a monitoring system like Datadog detects an outage or critical system issue, it can trigger a Pipedream workflow to send an immediate alert via Octopush SMS to your on-call engineers, reducing response times. + +- **Appointment Reminders from Google Calendar**: Set up a workflow where upcoming Google Calendar events trigger SMS reminders through Octopush. This keeps clients or patients informed about their appointments, potentially cutting down on no-shows. + +- **E-commerce Order Confirmations**: Whenever a new order is placed in an e-commerce platform like Shopify, a Pipedream workflow can automatically send an order confirmation and updates via SMS through Octopush, enhancing customer experience. diff --git a/components/odoo/odoo.app.mjs b/components/odoo/odoo.app.mjs new file mode 100644 index 0000000000000..8d77eafc9cdbc --- /dev/null +++ b/components/odoo/odoo.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "odoo", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/odoo/package.json b/components/odoo/package.json new file mode 100644 index 0000000000000..3a7bf3965dc43 --- /dev/null +++ b/components/odoo/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/odoo", + "version": "0.0.1", + "description": "Pipedream Odoo Components", + "main": "odoo.app.mjs", + "keywords": [ + "pipedream", + "odoo" + ], + "homepage": "https://pipedream.com/apps/odoo", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/office_365_management/office_365_management.app.mjs b/components/office_365_management/office_365_management.app.mjs new file mode 100644 index 0000000000000..ba141060dfdec --- /dev/null +++ b/components/office_365_management/office_365_management.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "office_365_management", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/office_365_management/package.json b/components/office_365_management/package.json new file mode 100644 index 0000000000000..b9e433f856095 --- /dev/null +++ b/components/office_365_management/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/office_365_management", + "version": "0.0.1", + "description": "Pipedream Office 365 Management Components", + "main": "office_365_management.app.mjs", + "keywords": [ + "pipedream", + "office_365_management" + ], + "homepage": "https://pipedream.com/apps/office_365_management", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/offlight/actions/create-task/create-task.mjs b/components/offlight/actions/create-task/create-task.mjs new file mode 100644 index 0000000000000..1f4a29194345e --- /dev/null +++ b/components/offlight/actions/create-task/create-task.mjs @@ -0,0 +1,63 @@ +import offlight from "../../offlight.app.mjs"; + +export default { + key: "offlight-create-task", + name: "Create Task", + description: "Initiates the creation of a new task in Offlight. [See the documentation](https://www.offlight.work/docs/zapeir-api)", + version: "0.0.1", + type: "action", + props: { + offlight, + taskName: { + type: "string", + label: "Task Name", + description: "The name of the task.", + }, + taskNote: { + type: "string", + label: "Task Note", + description: "A note about the task.", + optional: true, + }, + taskDeadline: { + type: "string", + label: "Task Deadline", + description: "The deadline of the task. **In ISO 8601 format (YYYY-MM-DD)**.", + optional: true, + }, + identifier: { + type: "string", + label: "Identifier", + description: "A unique identifier for the task.", + optional: true, + }, + sourceName: { + type: "string", + label: "Source Name", + description: "The source name of the task.", + optional: true, + }, + sourceLink: { + type: "string", + label: "Source Link", + description: "The source link of the task.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.offlight.createTask({ + $, + data: { + task_name: this.taskName, + task_note: this.taskNote, + task_deadline: this.taskDeadline, + identifier: this.identifier, + source_name: this.sourceName, + source_link: this.sourceLink, + }, + }); + + $.export("$summary", `Task successfully created with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/offlight/offlight.app.mjs b/components/offlight/offlight.app.mjs new file mode 100644 index 0000000000000..f6ee422a7df42 --- /dev/null +++ b/components/offlight/offlight.app.mjs @@ -0,0 +1,53 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "offlight", + methods: { + _baseUrl() { + return "https://api.offlight.work"; + }, + _headers() { + return { + "x-api-key": `${this.$auth.api_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createTask(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/zapier/task", + ...opts, + }); + }, + getDoneTasks(opts = {}) { + return this._makeRequest({ + ...opts, + method: "GET", + path: "/zapier/doneTasks", + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/zapier/webhook", + ...opts, + }); + }, + deleteWebhook(opts = {}) { + return this._makeRequest({ + method: "DELETE", + path: "/zapier/webhook", + ...opts, + }); + }, + }, +}; diff --git a/components/offlight/package.json b/components/offlight/package.json new file mode 100644 index 0000000000000..ee27f074772a4 --- /dev/null +++ b/components/offlight/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/offlight", + "version": "0.1.0", + "description": "Pipedream OFFLIGHT Components", + "main": "offlight.app.mjs", + "keywords": [ + "pipedream", + "offlight" + ], + "homepage": "https://pipedream.com/apps/offlight", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} + diff --git a/components/offlight/sources/new-task-done-instant/new-task-done-instant.mjs b/components/offlight/sources/new-task-done-instant/new-task-done-instant.mjs new file mode 100644 index 0000000000000..043a5a02a30e8 --- /dev/null +++ b/components/offlight/sources/new-task-done-instant/new-task-done-instant.mjs @@ -0,0 +1,55 @@ +import offlight from "../../offlight.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "offlight-new-task-done-instant", + name: "New Task Done (Instant)", + description: "Emit new event when a task is marked as complete.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + offlight, + http: { + type: "$.interface.http", + }, + db: "$.service.db", + }, + methods: { + emitTask(task) { + this.$emit(task, { + id: task.id, + summary: `Task ${task.name} marked as done`, + ts: Date.parse(task.doneAt), + }); + }, + }, + hooks: { + async deploy() { + const tasks = await this.offlight.getDoneTasks(); + for (const task of tasks) { + this.emitTask(task); + } + }, + async activate() { + await this.offlight.createWebhook({ + data: { + purpose: "doneTask", + hookUrl: this.http.endpoint, + }, + }); + }, + async deactivate() { + await this.offlight.deleteWebhook({ + data: { + purpose: "doneTask", + hookUrl: this.http.endpoint, + }, + }); + }, + }, + async run({ body }) { + this.emitTask(body); + }, + sampleEmit, +}; diff --git a/components/offlight/sources/new-task-done-instant/test-event.mjs b/components/offlight/sources/new-task-done-instant/test-event.mjs new file mode 100644 index 0000000000000..324942504b1cc --- /dev/null +++ b/components/offlight/sources/new-task-done-instant/test-event.mjs @@ -0,0 +1,18 @@ +export default { + "createdAt": "2024-09-23T19:18:54.995Z", + "updatedAt": "2024-09-23T20:10:26.000Z", + "plannedDate": null, + "doneAt": "2024-09-23T20:10:25.136Z", + "deletedAt": null, + "id": "4e73c28d-0e2f-4352-9c86-62048255e1b8", + "name": "🤵 Welcome to OFFLIGHT", + "note": null, + "status": "done", + "deadline": null, + "goal": null, + "source": "admin", + "identifier": null, + "sourceName": null, + "link": null, + "creator": null +} \ No newline at end of file diff --git a/components/oksign/README.md b/components/oksign/README.md index 07f2032b191e3..76bd047a38cef 100644 --- a/components/oksign/README.md +++ b/components/oksign/README.md @@ -1,19 +1,11 @@ # Overview -The OKSign API allows you to build legally compliant signing and authentication -experiences, so you can create sustainable digital processes and trust within -your user base. No matter what your signature process or business workflow -requires, OKSign has you covered. +The OKSign API unlocks the potential to automate electronic signature workflows, streamlining the process of collecting legally binding signatures on documents. With this API, you can create signature requests, send documents to signees, and track the status of these documents, all programmatically. When leveraged through Pipedream, OKSign can be integrated with a vast array of apps to trigger actions, handle document flow, and ensure seamless end-to-end document management within your business processes. -What types of things can you build with OKSign? +# Example Use Cases -- Document signing/verification/generating -- Secure confirmations and acknowledgements -- Digital identification processes -- Electronic signature capture -- Cross-border forms and contracts -- Automated contracts management -- Electronic signatures for documents in multiple languages -- Biometric authentication systems -- Secure online payments -- Onboarding for secure digital services +- **Contract Automation for Sales**: Integrate OKSign with a CRM like Salesforce on Pipedream. Automate the sending of sales contracts when a new opportunity reaches a certain stage. Set up a workflow that listens for stage changes, then triggers a signature request for the related contract template in OKSign, and sends it to the customer for signing. + +- **Employee Onboarding Documents**: Connect OKSign with an HR platform like BambooHR. Automate the distribution of onboarding paperwork to new hires. When a new employee record is created, a Pipedream workflow can fetch the necessary documents and send them through OKSign for e-signature, then store the signed documents back in the HR system. + +- **Legal Document Approval Process**: Use OKSign with email services like Gmail on Pipedream. When a legal document is ready for review, a workflow can dispatch the document from a designated email to the relevant parties via OKSign. Once signed, the workflow can send a confirmation email to the involved parties and update any project management tools like Trello with the document's completion status. diff --git a/components/okta/actions/create-user/create-user.mjs b/components/okta/actions/create-user/create-user.mjs new file mode 100644 index 0000000000000..6371cab5ef8a2 --- /dev/null +++ b/components/okta/actions/create-user/create-user.mjs @@ -0,0 +1,93 @@ +import { ConfigurationError } from "@pipedream/platform"; +import okta from "../../okta.app.mjs"; + +export default { + key: "okta-create-user", + name: "Create User", + description: "Creates a new user in the Okta system. [See the documentation](https://developer.okta.com/docs/api/openapi/okta-management/management/tag/User/#tag/User/operation/createUser)", + version: "0.0.1", + type: "action", + props: { + okta, + activate: { + type: "boolean", + label: "Activate", + description: "Executes activation lifecycle operation when creating the user. Defaults to true.", + optional: true, + }, + firstName: { + propDefinition: [ + okta, + "firstName", + ], + }, + lastName: { + propDefinition: [ + okta, + "lastName", + ], + }, + email: { + propDefinition: [ + okta, + "email", + ], + }, + login: { + propDefinition: [ + okta, + "login", + ], + }, + mobilePhone: { + propDefinition: [ + okta, + "mobilePhone", + ], + optional: true, + }, + typeId: { + propDefinition: [ + okta, + "typeId", + ], + optional: true, + }, + }, + async run({ $ }) { + try { + const { + okta, + activate, + typeId, + ...profile + } = this; + + const data = { + profile, + }; + + if (typeId) { + data.type = { + id: this.typeId, + }; + } + + const response = await okta.createUser({ + $, + data, + params: { + activate, + }, + }); + + $.export("$summary", `Successfully created user with ID ${response.id}`); + return response; + } catch ({ message }) { + const messageError = JSON.parse(message); + throw new ConfigurationError(messageError.errorCauses.length + ? messageError.errorCauses[0].errorSummary + : messageError.errorSummary); + } + }, +}; diff --git a/components/okta/actions/get-user/get-user.mjs b/components/okta/actions/get-user/get-user.mjs new file mode 100644 index 0000000000000..60c4980185f6d --- /dev/null +++ b/components/okta/actions/get-user/get-user.mjs @@ -0,0 +1,26 @@ +import okta from "../../okta.app.mjs"; + +export default { + key: "okta-get-user", + name: "Get User", + description: "Fetches the information of a specific user from the Okta system. [See the documentation](https://developer.okta.com/docs/api/openapi/okta-management/management/tag/User/#tag/User/operation/getUser)", + version: "0.0.1", + type: "action", + props: { + okta, + userId: { + propDefinition: [ + okta, + "userId", + ], + }, + }, + async run({ $ }) { + const response = await this.okta.getUser({ + $, + userId: this.userId, + }); + $.export("$summary", `Successfully fetched user details for user ID ${this.userId}`); + return response; + }, +}; diff --git a/components/okta/actions/update-user/update-user.mjs b/components/okta/actions/update-user/update-user.mjs new file mode 100644 index 0000000000000..d2a111198e51f --- /dev/null +++ b/components/okta/actions/update-user/update-user.mjs @@ -0,0 +1,93 @@ +import okta from "../../okta.app.mjs"; + +export default { + key: "okta-update-user", + name: "Update User", + description: "Updates the profile of a specific user in the Okta system. [See the documentation](https://developer.okta.com/docs/api/openapi/okta-management/management/tag/User/#tag/User/operation/updateUser)", + version: "0.0.1", + type: "action", + props: { + okta, + userId: { + propDefinition: [ + okta, + "userId", + ], + }, + firstName: { + propDefinition: [ + okta, + "firstName", + ], + optional: true, + }, + lastName: { + propDefinition: [ + okta, + "lastName", + ], + optional: true, + }, + email: { + propDefinition: [ + okta, + "email", + ], + optional: true, + }, + login: { + propDefinition: [ + okta, + "login", + ], + optional: true, + }, + mobilePhone: { + propDefinition: [ + okta, + "mobilePhone", + ], + optional: true, + }, + typeId: { + propDefinition: [ + okta, + "typeId", + ], + optional: true, + }, + }, + async run({ $ }) { + const { + okta, + userId, + typeId, + ...profile + } = this; + + const { profile: userProfile } = await okta.getUser({ + userId, + }); + + const response = await okta.updateUser({ + $, + userId, + data: { + profile: { + ...userProfile, + ...profile, + }, + ...(typeId + ? { + type: { + id: this.typeId, + }, + } : + {}), + }, + }); + + $.export("$summary", `Successfully updated user with ID ${userId}`); + return response; + }, +}; diff --git a/components/okta/common/constants.mjs b/components/okta/common/constants.mjs new file mode 100644 index 0000000000000..e67b995b66aa4 --- /dev/null +++ b/components/okta/common/constants.mjs @@ -0,0 +1 @@ +export const LIMIT = 1000; diff --git a/components/okta/okta.app.mjs b/components/okta/okta.app.mjs new file mode 100644 index 0000000000000..e45b6ee93ba64 --- /dev/null +++ b/components/okta/okta.app.mjs @@ -0,0 +1,209 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "okta", + propDefinitions: { + typeId: { + type: "string", + label: "Type Id", + description: "The ID of the user type. Add this value if you want to create a user with a non-default user type. The user type determines which schema applies to that user. After a user has been created, the user can only be assigned a different user type by an administrator through a full replacement (PUT) operation.", + async options() { + const data = await this.listUserTypes(); + + return data.map(({ + id: value, displayName: label, + }) => ({ + label, + value, + })); + }, + }, + groupIds: { + type: "string[]", + label: "Group Ids", + description: "An array of group Ids.", + async options({ prevContext = {} }) { + let after = ""; + if (prevContext.nextToken) { + after = prevContext.nextToken; + } + const data = await this.listGroups({ + params: { + limit: LIMIT, + after, + }, + }); + + return { + options: data.map(({ + id: value, profile: { name: label }, + }) => ({ + label, + value, + })), + context: { + nextToken: data[data.length - 1].id, + }, + }; + }, + }, + userId: { + type: "string", + label: "User ID", + description: "The unique identifier of the user.", + async options({ prevContext }) { + let after = ""; + if (prevContext.nextToken) { + after = prevContext.nextToken; + } + const data = await this.listUsers({ + params: { + limit: LIMIT, + after, + }, + }); + + return { + options: data.map(({ + id: value, profile: { email: label }, + }) => ({ + label, + value, + })), + context: { + nextToken: data[data.length - 1].id, + }, + }; + }, + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the user.", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the user.", + }, + email: { + type: "string", + label: "Email", + description: "The email address of the user.", + }, + login: { + type: "string", + label: "Login", + description: "The login for the user.", + }, + mobilePhone: { + type: "string", + label: "Mobile Phone", + description: "The mobile phone number of the user.", + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.subdomain}.okta.com/api/v1`; + }, + _headers() { + return { + "Authorization": `SSWS ${this.$auth.api_token}`, + "Content-Type": "application/json", + "Accept": "application/json; okta-version=1.0.0", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + getUser({ + userId, ...opts + }) { + return this._makeRequest({ + path: `/users/${userId}`, + ...opts, + }); + }, + listUserTypes() { + return this._makeRequest({ + path: "/meta/types/user", + }); + }, + listGroups(opts = {}) { + return this._makeRequest({ + path: "/groups", + ...opts, + }); + }, + listLogs(opts = {}) { + return this._makeRequest({ + path: "/logs", + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users", + ...opts, + }); + }, + createUser(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/users", + ...opts, + }); + }, + updateUser({ + userId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/users/${userId}`, + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let after = null; + + do { + params.after = after; + const { + headers: { link }, data, + } = await fn({ + returnFullResponse: true, + params, + ...opts, + }); + + for (const d of data) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + const nextToken = link.match(/after=(\w+)/); + if (nextToken) { + after = nextToken[1]; + } + + hasMore = nextToken; + + } while (hasMore); + }, + }, +}; diff --git a/components/okta/package.json b/components/okta/package.json new file mode 100644 index 0000000000000..cfa8023ced5fe --- /dev/null +++ b/components/okta/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/okta", + "version": "0.1.0", + "description": "Pipedream Okta Components", + "main": "okta.app.mjs", + "keywords": [ + "pipedream", + "okta" + ], + "homepage": "https://pipedream.com/apps/okta", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} + diff --git a/components/okta/sources/watch-new-events/test-event.mjs b/components/okta/sources/watch-new-events/test-event.mjs new file mode 100644 index 0000000000000..fab9bba1a44d6 --- /dev/null +++ b/components/okta/sources/watch-new-events/test-event.mjs @@ -0,0 +1,118 @@ +export default { + "actor": { + "alternateId": "string", + "detailEntry": { + "property1": {}, + "property2": {} + }, + "displayName": "string", + "id": "string", + "type": "string" + }, + "authenticationContext": { + "authenticationProvider": "ACTIVE_DIRECTORY", + "authenticationStep": 0, + "credentialProvider": "DUO", + "credentialType": "ASSERTION", + "externalSessionId": "string", + "interface": "string", + "issuer": { + "id": "string", + "type": "string" + } + }, + "client": { + "device": "string", + "geographicalContext": { + "city": "string", + "country": "string", + "geolocation": { + "lat": 0, + "lon": 0 + }, + "postalCode": "string", + "state": "string" + }, + "id": "string", + "ipAddress": "string", + "userAgent": { + "browser": "string", + "os": "string", + "rawUserAgent": "string" + }, + "zone": "string" + }, + "debugContext": { + "debugData": { + "property1": {}, + "property2": {} + } + }, + "displayMessage": "string", + "eventType": "string", + "legacyEventType": "string", + "outcome": { + "reason": "string", + "result": "string" + }, + "published": "2019-08-24T14:15:22Z", + "request": { + "ipChain": [ + { + "geographicalContext": { + "city": "string", + "country": "string", + "geolocation": { + "lat": 0, + "lon": 0 + }, + "postalCode": "string", + "state": "string" + }, + "ip": "string", + "source": "string", + "version": "string" + } + ] + }, + "securityContext": { + "asNumber": 0, + "asOrg": "string", + "domain": "string", + "isp": "string", + "isProxy": true + }, + "severity": "DEBUG", + "target": [ + { + "alternateId": "string", + "changeDetails": { + "from": { + "property1": {}, + "property2": {} + }, + "to": { + "property1": {}, + "property2": {} + } + }, + "detailEntry": { + "property1": {}, + "property2": {} + }, + "displayName": "string", + "id": "string", + "type": "string" + } + ], + "transaction": { + "detail": { + "property1": {}, + "property2": {} + }, + "id": "string", + "type": "string" + }, + "uuid": "string", + "version": "string" +} \ No newline at end of file diff --git a/components/okta/sources/watch-new-events/watch-new-events.mjs b/components/okta/sources/watch-new-events/watch-new-events.mjs new file mode 100644 index 0000000000000..9c49dff84aa63 --- /dev/null +++ b/components/okta/sources/watch-new-events/watch-new-events.mjs @@ -0,0 +1,69 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import okta from "../../okta.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "okta-watch-new-events", + name: "New Okta Event", + description: "Emit new event when the system observes a new event.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + okta, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01T00:00:00Z"; + }, + _setLastDate(created) { + this.db.set("lastDate", created); + }, + generateMeta(event) { + return { + id: event.uuid, + summary: `New Okta Event: ${event.eventType}`, + ts: Date.parse(event.published), + }; + }, + async startEvent(maxResults = 0) { + const lastDate = this._getLastDate(); + const response = this.okta.paginate({ + fn: this.okta.listLogs, + maxResults, + params: { + since: lastDate, + sortOrder: "DESCENDING", + }, + }); + + let responseArray = []; + + for await (const item of response) { + responseArray.push(item); + } + + if (responseArray.length) this._setLastDate(responseArray[0].published); + + for (const event of responseArray.reverse()) { + this.$emit(event, this.generateMeta(event)); + } + }, + }, + hooks: { + async deploy() { + await this.startEvent(25); + }, + }, + async run() { + await this.startEvent(); + }, + sampleEmit, +}; diff --git a/components/ollama/actions/copy-model/copy-model.mjs b/components/ollama/actions/copy-model/copy-model.mjs new file mode 100644 index 0000000000000..895c39a745e2f --- /dev/null +++ b/components/ollama/actions/copy-model/copy-model.mjs @@ -0,0 +1,50 @@ +import app from "../../ollama.app.mjs"; + +export default { + key: "ollama-copy-model", + name: "Copy Model", + description: "Copies a model, creating a model with another name from an existing model. [See the documentation](https://github.com/ollama/ollama/blob/main/docs/api.md#copy-a-model).", + version: "0.0.1", + type: "action", + props: { + app, + source: { + propDefinition: [ + app, + "model", + ], + }, + destination: { + type: "string", + label: "New Model Name", + description: "The new name for the copied model.", + }, + }, + methods: { + copyModel(args = {}) { + return this.app.post({ + path: "/copy", + ...args, + }); + }, + }, + async run({ $ }) { + const { + copyModel, + source, + destination, + } = this; + + await copyModel({ + $, + data: { + source, + destination, + }, + }); + $.export("$summary", "Successfully copied model."); + return { + success: true, + }; + }, +}; diff --git a/components/ollama/actions/create-model/create-model.mjs b/components/ollama/actions/create-model/create-model.mjs new file mode 100644 index 0000000000000..367625a53b44e --- /dev/null +++ b/components/ollama/actions/create-model/create-model.mjs @@ -0,0 +1,55 @@ +import app from "../../ollama.app.mjs"; + +export default { + key: "ollama-create-model", + name: "Create Model", + description: "Create a model from a modelfile. [See the documentation](https://github.com/ollama/ollama/blob/main/docs/api.md#create-a-model).", + version: "0.0.1", + type: "action", + props: { + app, + name: { + type: "string", + label: "Name", + description: "The name of the model.", + }, + modelfile: { + type: "string", + label: "Model File", + description: "Contents of the Modelfile. Eg. `FROM llama3 SYSTEM You are mario from Super Mario Bros`", + }, + stream: { + propDefinition: [ + app, + "stream", + ], + }, + }, + methods: { + createModel(args = {}) { + return this.app.post({ + path: "/create", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createModel, + name, + modelfile, + stream, + } = this; + + const response = await createModel({ + $, + data: { + name, + modelfile, + stream, + }, + }); + $.export("$summary", "Successfully created model."); + return response; + }, +}; diff --git a/components/ollama/actions/delete-model/delete-model.mjs b/components/ollama/actions/delete-model/delete-model.mjs new file mode 100644 index 0000000000000..f475c1391113e --- /dev/null +++ b/components/ollama/actions/delete-model/delete-model.mjs @@ -0,0 +1,43 @@ +import app from "../../ollama.app.mjs"; + +export default { + key: "ollama-delete-model", + name: "Delete Model", + description: "Delete a model and its data. [See the documentation](https://github.com/ollama/ollama/blob/main/docs/api.md#delete-a-model)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "model", + ], + }, + }, + methods: { + deleteModel(args = {}) { + return this.app.delete({ + path: "/delete", + ...args, + }); + }, + }, + async run({ $ }) { + const { + deleteModel, + name, + } = this; + + await deleteModel({ + $, + data: { + name, + }, + }); + $.export("$summary", "Successfully deleted model."); + return { + success: true, + }; + }, +}; diff --git a/components/ollama/actions/generate-chat-completion/generate-chat-completion.mjs b/components/ollama/actions/generate-chat-completion/generate-chat-completion.mjs new file mode 100644 index 0000000000000..3fb01de635b74 --- /dev/null +++ b/components/ollama/actions/generate-chat-completion/generate-chat-completion.mjs @@ -0,0 +1,82 @@ +import app from "../../ollama.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "ollama-generate-chat-completion", + name: "Generate Chat Completion", + description: "Generates the next message in a chat with a provided model. [See the documentation](https://github.com/ollama/ollama/blob/main/docs/api.md#generate-a-chat-completion).", + version: "0.0.1", + type: "action", + props: { + app, + model: { + propDefinition: [ + app, + "model", + ], + }, + messages: { + type: "string[]", + label: "Messages", + description: "The messages of the chat, this can be used to keep a chat memory. Each row should be set as a JSON format string. Eg. `{\"role\": \"user\", \"content\": \"Hello\"}`. The message object has the following fields:\n- `role`: the role of the message, either `system`, `user`, `assistant`, or `tool`.\n- `content`: The content of the message.\n- `images` (optional): a list of images to include in the message (for multimodal models such as `llava`).\n- `tool_calls`(optional): a list of tools the model wants to use.", + }, + tools: { + type: "string[]", + label: "Tools", + description: "A list of tools the model can use. Each row should be set as a JSON format string.", + optional: true, + }, + options: { + propDefinition: [ + app, + "options", + ], + }, + stream: { + propDefinition: [ + app, + "stream", + ], + }, + keepAlive: { + propDefinition: [ + app, + "keepAlive", + ], + }, + }, + methods: { + generateChatCompletion(args = {}) { + return this.app.post({ + path: "/chat", + ...args, + }); + }, + }, + async run({ $ }) { + const { + generateChatCompletion, + model, + messages, + tools, + options, + stream, + keepAlive, + } = this; + + const response = await generateChatCompletion({ + $, + data: { + model, + messages: utils.parseArray(messages), + tools: utils.parseArray(tools), + options: utils.parseOptions(options), + stream, + keep_alive: keepAlive, + }, + }); + + $.export("$summary", "Successfully generated chat completion."); + return response; + }, +}; diff --git a/components/ollama/actions/generate-completion/generate-completion.mjs b/components/ollama/actions/generate-completion/generate-completion.mjs new file mode 100644 index 0000000000000..28d90fe1f3a9a --- /dev/null +++ b/components/ollama/actions/generate-completion/generate-completion.mjs @@ -0,0 +1,90 @@ +import utils from "../../common/utils.mjs"; +import app from "../../ollama.app.mjs"; + +export default { + key: "ollama-generate-completion", + name: "Generate Completion", + description: "Generates a response for a given prompt with a provided model. [See the documentation](https://github.com/ollama/ollama/blob/main/docs/api.md#generate-a-completion).", + version: "0.0.1", + type: "action", + props: { + app, + model: { + propDefinition: [ + app, + "model", + ], + }, + prompt: { + propDefinition: [ + app, + "prompt", + ], + }, + suffix: { + propDefinition: [ + app, + "suffix", + ], + }, + images: { + propDefinition: [ + app, + "images", + ], + }, + options: { + propDefinition: [ + app, + "options", + ], + }, + stream: { + propDefinition: [ + app, + "stream", + ], + }, + keepAlive: { + propDefinition: [ + app, + "keepAlive", + ], + }, + }, + methods: { + generateCompletion(args = {}) { + return this.app.post({ + path: "/generate", + ...args, + }); + }, + }, + async run({ $ }) { + const { + generateCompletion, + model, + prompt, + suffix, + images, + options, + stream, + keepAlive, + } = this; + + const response = await generateCompletion({ + $, + data: { + model, + prompt, + suffix, + images, + options: utils.parseOptions(options), + stream, + keep_alive: keepAlive, + }, + }); + $.export("$summary", "Successfully generated completion."); + return response; + }, +}; diff --git a/components/ollama/actions/generate-embeddings/generate-embeddings.mjs b/components/ollama/actions/generate-embeddings/generate-embeddings.mjs new file mode 100644 index 0000000000000..954f11f29f062 --- /dev/null +++ b/components/ollama/actions/generate-embeddings/generate-embeddings.mjs @@ -0,0 +1,73 @@ +import utils from "../../common/utils.mjs"; +import app from "../../ollama.app.mjs"; + +export default { + key: "ollama-generate-embeddings", + name: "Generate Embeddings", + description: "Generate embeddings from a model. [See the documentation](https://github.com/ollama/ollama/blob/main/docs/api.md#generate-embeddings).", + version: "0.0.1", + type: "action", + props: { + app, + model: { + propDefinition: [ + app, + "model", + ], + }, + input: { + type: "string[]", + label: "Input", + description: "The list of texts to generate embeddings for.", + }, + truncate: { + type: "boolean", + label: "Truncate", + description: "Truncates the end of each input to fit within context length. Returns error if `false` and context length is exceeded. Defaults to `true`.", + optional: true, + }, + options: { + propDefinition: [ + app, + "options", + ], + }, + keepAlive: { + propDefinition: [ + app, + "keepAlive", + ], + }, + }, + methods: { + generateEmbeddings(args = {}) { + return this.app.post({ + path: "/embed", + ...args, + }); + }, + }, + async run({ $ }) { + const { + generateEmbeddings, + model, + input, + truncate, + options, + keepAlive, + } = this; + + const response = await generateEmbeddings({ + $, + data: { + model, + input, + truncate, + options: utils.parseOptions(options), + keep_alive: keepAlive, + }, + }); + $.export("$summary", "Successfully generated embeddings."); + return response; + }, +}; diff --git a/components/ollama/actions/list-local-models/list-local-models.mjs b/components/ollama/actions/list-local-models/list-local-models.mjs new file mode 100644 index 0000000000000..250a910f6dd74 --- /dev/null +++ b/components/ollama/actions/list-local-models/list-local-models.mjs @@ -0,0 +1,19 @@ +import app from "../../ollama.app.mjs"; + +export default { + key: "ollama-list-local-models", + name: "List Local Models", + description: "List models that are available locally. [See the documentation](https://github.com/ollama/ollama/blob/main/docs/api.md#list-running-models).", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.listLocalModels({ + $, + }); + $.export("$summary", "Successfully listed local models."); + return response; + }, +}; diff --git a/components/ollama/actions/pull-model/pull-model.mjs b/components/ollama/actions/pull-model/pull-model.mjs new file mode 100644 index 0000000000000..2121c18bf972e --- /dev/null +++ b/components/ollama/actions/pull-model/pull-model.mjs @@ -0,0 +1,56 @@ +import app from "../../ollama.app.mjs"; + +export default { + key: "ollama-pull-model", + name: "Pull Model", + description: "Download a model from the ollama library. Cancelled pulls are resumed from where they left off, and multiple calls will share the same download progress. [See the documentation](https://github.com/ollama/ollama/blob/main/docs/api.md#pull-a-model).", + version: "0.0.1", + type: "action", + props: { + app, + name: { + type: "string", + label: "Model Name", + description: "The name of the model to pull.", + }, + insecure: { + propDefinition: [ + app, + "insecure", + ], + }, + stream: { + propDefinition: [ + app, + "stream", + ], + }, + }, + methods: { + pullModel(args = {}) { + return this.app.post({ + path: "/pull", + ...args, + }); + }, + }, + async run({ $ }) { + const { + pullModel, + name, + insecure, + stream, + } = this; + + const response = await pullModel({ + $, + data: { + name, + insecure, + stream, + }, + }); + $.export("$summary", "Successfully pulled model."); + return response; + }, +}; diff --git a/components/ollama/actions/push-model/push-model.mjs b/components/ollama/actions/push-model/push-model.mjs new file mode 100644 index 0000000000000..fa7b474b5351f --- /dev/null +++ b/components/ollama/actions/push-model/push-model.mjs @@ -0,0 +1,59 @@ +import app from "../../ollama.app.mjs"; + +export default { + key: "ollama-push-model", + name: "Push Model to Library", + description: "Upload a model to a model library. Requires registering for ollama.ai and adding a public key first. [See the documentation](https://github.com/ollama/ollama/blob/main/docs/api.md#push-a-model).", + version: "0.0.1", + type: "action", + props: { + app, + name: { + description: "Name of the model to push in the form of `/:`. Please make sure you follow [the instructions in this issue](https://github.com/ollama/ollama/issues/1140#issuecomment-1814823949) in order to push a model to your own library in [ollama.com](https://ollama.com/).", + propDefinition: [ + app, + "model", + ], + }, + insecure: { + propDefinition: [ + app, + "insecure", + ], + }, + stream: { + propDefinition: [ + app, + "stream", + ], + }, + }, + methods: { + pushModel(args = {}) { + return this.app.post({ + path: "/push", + ...args, + }); + }, + }, + async run({ $ }) { + const { + pushModel, + name, + insecure, + stream, + } = this; + + const response = await pushModel({ + $, + data: { + name, + insecure, + stream, + }, + }); + + $.export("$summary", "Successfully pushed model."); + return response; + }, +}; diff --git a/components/ollama/actions/show-model-information/show-model-information.mjs b/components/ollama/actions/show-model-information/show-model-information.mjs new file mode 100644 index 0000000000000..bbb82666f7c82 --- /dev/null +++ b/components/ollama/actions/show-model-information/show-model-information.mjs @@ -0,0 +1,51 @@ +import app from "../../ollama.app.mjs"; + +export default { + key: "ollama-show-model-information", + name: "Show Model Information", + description: "Show information about a model including details, modelfile, template, parameters, license, and system prompt. [See the documentation](https://github.com/ollama/ollama/blob/main/docs/api.md#show-model-information).", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "model", + ], + }, + verbose: { + type: "boolean", + label: "Verbose", + description: "Show verbose output.", + optional: true, + }, + }, + methods: { + getModelInfo(args = {}) { + return this.app.post({ + path: "/show", + ...args, + }); + }, + }, + async run({ $ }) { + const { + getModelInfo, + name, + verbose, + } = this; + + const response = await getModelInfo({ + $, + data: { + name, + verbose, + }, + }); + + $.export("$summary", "Successfully retrieved model information."); + + return response; + }, +}; diff --git a/components/ollama/common/utils.mjs b/components/ollama/common/utils.mjs new file mode 100644 index 0000000000000..ac8fa4db58f74 --- /dev/null +++ b/components/ollama/common/utils.mjs @@ -0,0 +1,87 @@ +import { ConfigurationError } from "@pipedream/platform"; + +const parseJson = (input) => { + const parse = (value) => { + if (typeof(value) === "string") { + try { + return parseJson(JSON.parse(value)); + } catch (e) { + return value; + } + } else if (typeof(value) === "object" && value !== null) { + return Object.entries(value) + .reduce((acc, [ + key, + val, + ]) => Object.assign(acc, { + [key]: parse(val), + }), {}); + } + return value; + }; + + return parse(input); +}; + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +function isJson(value) { + try { + JSON.parse(value); + } catch (e) { + return false; + } + return true; +} + +function parseOptions(options) { + if (!options) { + return; + } + return Object.fromEntries( + Object.entries(options) + .map(([ + key, + value, + ]) => { + let parsedValue = isNaN(value) + ? value + : Number(value); + + parsedValue = isJson(value) + ? JSON.parse(value) + : parsedValue; + + return [ + key, + parsedValue, + ]; + }), + ); +} + +export default { + parseArray: (value) => parseArray(value).map(parseJson), + parseOptions, +}; diff --git a/components/ollama/ollama.app.mjs b/components/ollama/ollama.app.mjs new file mode 100644 index 0000000000000..3066adcd87d0e --- /dev/null +++ b/components/ollama/ollama.app.mjs @@ -0,0 +1,100 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "ollama", + propDefinitions: { + model: { + type: "string", + label: "Model Name", + description: "Model names follow a `model:tag` format, where model can have an optional namespace such as `example/model`. Some examples are `orca-mini:3b-q4_1` and `llama3:70b`. The tag is optional and, if not provided, will default to latest. The tag is used to identify a specific version.", + async options() { + const { models } = await this.listLocalModels(); + return models.map(({ name }) => name); + }, + }, + prompt: { + type: "string", + label: "Prompt", + description: "The prompt to generate a response for.", + }, + suffix: { + type: "string", + label: "Suffix", + description: "The text after the model response.", + optional: true, + }, + images: { + type: "string[]", + label: "Images", + description: "A list of base64-encoded images (for multimodal models such as `llava`).", + optional: true, + }, + options: { + type: "object", + label: "Advanced Options", + description: "Additional model parameters listed in the documentation for the [Modelfile](https://github.com/ollama/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values) such as `temperature`", + optional: true, + }, + insecure: { + type: "boolean", + label: "Insecure", + description: "Allow insecure connections to the library. Only use this if you are pulling from your own library during development.", + optional: true, + }, + stream: { + type: "boolean", + label: "Stream", + description: "If `false` the response will be returned as a single response object, rather than a stream of objects.", + optional: true, + default: false, + }, + keepAlive: { + type: "string", + label: "Keep Alive", + description: "Controls how long the model will stay loaded into memory following the request (default: 5m).", + optional: true, + }, + }, + methods: { + getUrl(path) { + return `${this.$auth.url}/api${path}`; + }, + getHeaders(headers) { + const { apiKey } = this.$auth; + return { + ...headers, + ...(apiKey && { + Authorization: `Bearer ${apiKey}`, + }), + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + listLocalModels(args = {}) { + return this._makeRequest({ + path: "/tags", + ...args, + }); + }, + }, +}; diff --git a/components/ollama/package.json b/components/ollama/package.json new file mode 100644 index 0000000000000..00f24ba705318 --- /dev/null +++ b/components/ollama/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/ollama", + "version": "0.1.0", + "description": "Pipedream Ollama Components", + "main": "ollama.app.mjs", + "keywords": [ + "pipedream", + "ollama" + ], + "homepage": "https://pipedream.com/apps/ollama", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" + } +} diff --git a/components/ometria/README.md b/components/ometria/README.md index da0125c4d6046..f0055a35233c3 100644 --- a/components/ometria/README.md +++ b/components/ometria/README.md @@ -1,20 +1,11 @@ # Overview -What can I build with the Ometria API? +The Ometria API is a data-driven tool designed to empower marketers to efficiently orchestrate personalized customer experiences across multiple channels. With this API, you can automate the management of customer data, track customer behaviors, and trigger personalized marketing campaigns. By leveraging the power of Pipedream's integration capabilities, you can seamlessly connect Ometria with other services, sync data in real-time, and create custom, scalable automations to enhance your marketing efforts. -The Ometria API provides a comprehensive set of tools to help you create robust -and comprehensive customer segmentation and analytics for your eCommerce store. -With access to Ometria’s full suite of products, you can build the following: +# Example Use Cases -- Real-time customer behaviour tracking and reporting -- Create better targeted email campaigns -- Create automated segmentation rules to segment customers into different - groups -- Create advanced personalisation ideas for each customer -- Create requests for audience-specific product recommendations -- Create marketing automation and loyalty programs -- Create automated recommendations based on customer behaviour -- Automatically score customers’ buying habits and preferences -- Generate powerful insights into business performance -- Monitor customer feedback, returns and reviews -- Automatically trigger re-engagement campaigns +- **Sync Customer Data with CRM**: Automate the process of syncing new customer data from Ometria to your CRM platform, like Salesforce or HubSpot. When a new customer is added in Ometria, trigger a workflow that enriches and syncs this data to the CRM, ensuring your sales team has the latest information at their fingertips. + +- **Personalized Email Campaigns Trigger**: Set up a workflow to monitor customer activity via the Ometria API, such as recent purchases or cart abandonment. Use this data to trigger personalized email campaigns through an email marketing service like Mailchimp, sending targeted offers or reminders to nudge customers towards conversion. + +- **Real-Time Customer Segmentation and Reporting**: Create a workflow that uses customer interaction data from Ometria to segment customers in real-time based on their behavior. Connect to a visualization tool like Google Data Studio to generate dynamic reports that help tailor marketing strategies and track the performance of different customer segments. diff --git a/components/ometria/package.json b/components/ometria/package.json new file mode 100644 index 0000000000000..552c894fed624 --- /dev/null +++ b/components/ometria/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/ometria", + "version": "0.6.0", + "description": "Pipedream ometria Components", + "main": "ometria.app.mjs", + "keywords": [ + "pipedream", + "ometria" + ], + "homepage": "https://pipedream.com/apps/ometria", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/omise/README.md b/components/omise/README.md new file mode 100644 index 0000000000000..ba58ff1f615de --- /dev/null +++ b/components/omise/README.md @@ -0,0 +1,11 @@ +# Overview + +The OPN API, previously known as Omise, provides a powerful suite of features for handling online payments, from processing transactions to managing payment methods. With Pipedream's integration capabilities, you can automate various aspects of payment operations, link payment events to other services, and streamline financial workflows without the need for extensive coding. + +# Example Use Cases + +- **Automated Payment Alerts**: Trigger a workflow in Pipedream whenever a new payment is made via OPN. Use this to send real-time notifications to Slack, keeping your finance or sales team instantly informed about new transactions. + +- **Customer Follow-Up After Payment**: After a successful payment is processed, use the OPN API to kick off a customer follow-up sequence. Connect with SendGrid to send a personalized 'Thank You' email, and add the customer details to a Google Sheet for future reference and marketing efforts. + +- **Monthly Sales Reporting**: Set up a scheduled workflow in Pipedream that summarizes monthly sales data from OPN. Compile this information and generate a report, then distribute it through email or a messaging app such as Microsoft Teams to provide stakeholders with regular updates. diff --git a/components/omniconvert/README.md b/components/omniconvert/README.md index 3d34699cddf70..8aaa5d2fa80f4 100644 --- a/components/omniconvert/README.md +++ b/components/omniconvert/README.md @@ -1,22 +1,11 @@ # Overview -The Omniconvert API is a powerful tool that allows you to quickly build -automated optimization experiments and reports. With Omniconvert, you can -implement A/B tests, segmentation analysis, segment analytics, behavioral data, -and more. +Omniconvert is a versatile API that enables businesses to optimize, personalize, and survey their e-commerce platforms. With it, you can automate A/B testing, personalize customer experiences based on a variety of metrics, and generate insights from customer feedback through surveys. Pipedream, as a serverless integration and automation platform, takes this a step further by allowing you to create complex workflows that trigger actions in other apps based on events in Omniconvert. -Using Omniconvert, you can build and deploy a variety of optimizations for your -site. Here are some examples: +# Example Use Cases -- Implement A/B testing to compare different versions of a page to determine - the best design for maximum conversions. -- Analyze user behavior and segmentation by designing custom reports to track - user interactions or drop offs. -- Utilize segmentation analytics to understand user behaviour and take action - from the results. -- Create automated optimization experiments to monitor, adjust, and optimize - the performance of pages. -- Gather real-time user data, such as for promotions or abandoned carts, to - boost conversion rates. -- Leverage behavioral data to create audiences for further optimization and - targeting. +- **Personalized Customer Follow-Up Emails**: Connect Omniconvert to an email marketing platform like Mailchimp via Pipedream. When Omniconvert completes a survey, trigger an automated, personalized follow-up email based on the customer's responses, enhancing the customer relationship and increasing engagement. + +- **Slack Notifications for A/B Test Results**: Create a Pipedream workflow that sends notifications to a Slack channel when an A/B test from Omniconvert reaches a statistical significance. This keeps the team instantly informed about which variations are performing better, allowing for quick and informed decisions. + +- **Automated Coupon Distribution Based on Survey Results**: Use Omniconvert to segment customers based on survey responses. Then, through a Pipedream workflow, integrate with eCommerce platforms like Shopify to automatically send tailored discount codes to those segments, encouraging repeat purchases and increasing customer loyalty. diff --git a/components/omniconvert/package.json b/components/omniconvert/package.json new file mode 100644 index 0000000000000..0d009b15e194b --- /dev/null +++ b/components/omniconvert/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/omniconvert", + "version": "0.6.0", + "description": "Pipedream omniconvert Components", + "main": "omniconvert.app.mjs", + "keywords": [ + "pipedream", + "omniconvert" + ], + "homepage": "https://pipedream.com/apps/omniconvert", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/omnisend/README.md b/components/omnisend/README.md new file mode 100644 index 0000000000000..d850eae64ac3d --- /dev/null +++ b/components/omnisend/README.md @@ -0,0 +1,11 @@ +# Overview + +Omnisend's API lets you automate email marketing by integrating with your e-commerce platform. You can sync subscriber lists, trigger personalized emails based on customer behavior, and track results. With Pipedream, you can seamlessly connect Omnisend to a plethora of other services, creating custom workflows. This means you can automate follow-ups, update databases, or kick off multi-step sequences with ease. + +# Example Use Cases + +- **Sync New Shopify Orders to Omnisend**: Whenever a new order is placed on Shopify, use Pipedream to catch the event and add the customer's details to a specific Omnisend email segment. This can trigger an automated follow-up email campaign for post-purchase engagement or feedback. + +- **Update Contacts from Google Sheets**: If you manage customer information in Google Sheets, Pipedream can watch for changes in your sheet and automatically update your Omnisend contact list. Ideal for ensuring your marketing lists are always in sync with the latest data. + +- **Aggregate Campaign Metrics to a Dashboard**: Use Pipedream to gather campaign performance data from Omnisend, such as open rates or click-through rates, and send that data to a business intelligence tool like Google Data Studio. This workflow helps you visualize your marketing campaign's success and make data-driven decisions. diff --git a/components/omnivore/README.md b/components/omnivore/README.md new file mode 100644 index 0000000000000..17b21e26a30a9 --- /dev/null +++ b/components/omnivore/README.md @@ -0,0 +1,11 @@ +# Overview + +The Omnivore API offers a platform to integrate with various Point of Sale (POS) systems, streamlining data access across multiple restaurant locations and systems. With Pipedream, you can harness this API to automate tasks, sync data, and connect with other services like CRMs, messaging apps, or analytics tools. By setting up triggers and actions, workflows can process transactions, update menus, gather sales reports, and more, all in real time. + +# Example Use Cases + +- **Sync Sales Data to Google Sheets**: Use Omnivore to trigger a workflow in Pipedream when new sales data is available. Then, append this data to a Google Sheets spreadsheet for easy access and analysis. This can help with accounting, performance tracking, and reporting. + +- **Send Slack Notifications for Large Orders**: Configure a workflow that listens for new orders via Omnivore. If an order exceeds a certain amount, automatically send a notification to a Slack channel to alert the team. This can be useful for real-time updates on significant sales or to prompt quick follow-up actions. + +- **Automate Menu Updates Across Platforms**: When you update your restaurant's menu in your POS system, use Omnivore to trigger a Pipedream workflow that automatically reflects these changes on your website and other platforms such as a food delivery service. This ensures consistency and saves time. diff --git a/components/onbee_app/actions/create-employee/create-employee.mjs b/components/onbee_app/actions/create-employee/create-employee.mjs new file mode 100644 index 0000000000000..e2c9f269661c4 --- /dev/null +++ b/components/onbee_app/actions/create-employee/create-employee.mjs @@ -0,0 +1,59 @@ +import app from "../../onbee_app.app.mjs"; + +export default { + key: "onbee_app-create-employee", + name: "Create Employee", + description: "Adds an employee to the system. [See the documentation](https://docs.onboardee.io/api/#tag/Employees/paths/~1employees~1add/post)", + version: "0.0.1", + type: "action", + props: { + app, + firstname: { + propDefinition: [ + app, + "firstname", + ], + }, + surname: { + propDefinition: [ + app, + "surname", + ], + }, + privateEmail: { + propDefinition: [ + app, + "privateEmail", + ], + }, + workEmail: { + propDefinition: [ + app, + "workEmail", + ], + }, + dateOfBirth: { + propDefinition: [ + app, + "dateOfBirth", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.createEmployee({ + $, + data: { + firstname: this.firstname, + surname: this.surname, + privateEmail: this.privateEmail, + workEmail: this.workEmail, + dateOfBirth: this.dateOfBirth, + }, + }); + + $.export("$summary", `Successfully created Employee with ID '${response.payload._id}'`); + + return response; + }, +}; diff --git a/components/onbee_app/actions/delete-employee/delete-employee.mjs b/components/onbee_app/actions/delete-employee/delete-employee.mjs new file mode 100644 index 0000000000000..d9fe18aaf377c --- /dev/null +++ b/components/onbee_app/actions/delete-employee/delete-employee.mjs @@ -0,0 +1,29 @@ +import app from "../../onbee_app.app.mjs"; + +export default { + key: "onbee_app-delete-employee", + name: "Delete Employee", + description: "Delete an employee with the specified ID. [See the documentation](https://docs.onboardee.io/api/#tag/Employees/paths/~1employees~1edit~1{id}/post)", + version: "0.0.1", + type: "action", + props: { + app, + employeeId: { + propDefinition: [ + app, + "employeeId", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.deleteEmployee({ + $, + employeeId: this.employeeId, + }); + + $.export("$summary", `Successfully deleted Employee with ID '${this.employeeId}'`); + + return response; + }, +}; diff --git a/components/onbee_app/actions/update-employee/update-employee.mjs b/components/onbee_app/actions/update-employee/update-employee.mjs new file mode 100644 index 0000000000000..8aef737fdc8c2 --- /dev/null +++ b/components/onbee_app/actions/update-employee/update-employee.mjs @@ -0,0 +1,68 @@ +import app from "../../onbee_app.app.mjs"; + +export default { + key: "onbee_app-update-employee", + name: "Update Employee", + description: "Update an employee with the specified ID. [See the documentation](https://docs.onboardee.io/api/#tag/Employees/paths/~1employees~1edit~1{id}/post)", + version: "0.0.1", + type: "action", + props: { + app, + employeeId: { + propDefinition: [ + app, + "employeeId", + ], + }, + firstname: { + propDefinition: [ + app, + "firstname", + ], + optional: true, + }, + surname: { + propDefinition: [ + app, + "surname", + ], + optional: true, + }, + privateEmail: { + propDefinition: [ + app, + "privateEmail", + ], + }, + workEmail: { + propDefinition: [ + app, + "workEmail", + ], + }, + dateOfBirth: { + propDefinition: [ + app, + "dateOfBirth", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.updateEmployee({ + $, + employeeId: this.employeeId, + data: { + firstname: this.firstname, + surname: this.surname, + privateEmail: this.privateEmail, + workEmail: this.workEmail, + dateOfBirth: this.dateOfBirth, + }, + }); + + $.export("$summary", `Successfully updated Employee with ID '${response.payload._id}'`); + + return response; + }, +}; diff --git a/components/onbee_app/onbee_app.app.mjs b/components/onbee_app/onbee_app.app.mjs new file mode 100644 index 0000000000000..4f58d0fc2204e --- /dev/null +++ b/components/onbee_app/onbee_app.app.mjs @@ -0,0 +1,104 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "onbee_app", + propDefinitions: { + employeeId: { + type: "string", + label: "Employee ID", + description: "ID of the Employee", + async options() { + const response = await this.getEmployees(); + + const employeeIds = response.payload; + + return employeeIds.map(({ + _id, firstname, surname, + }) => ({ + value: _id, + label: `${firstname} ${surname}`, + })); + }, + }, + firstname: { + type: "string", + label: "First Name", + description: "First name of the employee", + }, + surname: { + type: "string", + label: "Last Name", + description: "Last name of the employee", + }, + privateEmail: { + type: "string", + label: "Private Email", + description: "Private email of the employee", + optional: true, + }, + workEmail: { + type: "string", + label: "Work Email", + description: "Work email of the employee", + optional: true, + }, + dateOfBirth: { + type: "string", + label: "Date of Birth", + description: "Date of birth of the employee, i.e.: `1997-12-03`", + optional: true, + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.workspace_name}.onbee.app/api`; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_token}`, + }, + }); + }, + async createEmployee(args = {}) { + return this._makeRequest({ + path: "/employees/add", + method: "post", + ...args, + }); + }, + async updateEmployee({ + employeeId, ...args + }) { + return this._makeRequest({ + path: `/employees/edit/${employeeId}`, + method: "post", + ...args, + }); + }, + async deleteEmployee({ + employeeId, ...args + }) { + return this._makeRequest({ + path: `/employee-delete/${employeeId}`, + ...args, + }); + }, + async getEmployees(args = {}) { + return this._makeRequest({ + path: "/employee-list/", + ...args, + }); + }, + }, +}; diff --git a/components/onbee_app/package.json b/components/onbee_app/package.json new file mode 100644 index 0000000000000..3563b8bb603bc --- /dev/null +++ b/components/onbee_app/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/onbee_app", + "version": "0.1.0", + "description": "Pipedream Onbee.app Components", + "main": "onbee_app.app.mjs", + "keywords": [ + "pipedream", + "onbee_app" + ], + "homepage": "https://pipedream.com/apps/onbee_app", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/oncehub/README.md b/components/oncehub/README.md index d879ae5b06fe7..6a6f2f851a754 100644 --- a/components/oncehub/README.md +++ b/components/oncehub/README.md @@ -1,24 +1,17 @@ # Overview -OnceHub API is an efficient and reliable way to build automated, -programmatically managed appointment scheduling for your organization. With the -OnceHub API, you can build a wide range of applications like booking systems, -appointment and event scheduling, automated marketing campaigns, and much more. - -Some of the specific applications that you can create with OnceHub API include: - -- Automated appointment booking systems -- Event scheduling and management tools -- Automated marketing campaigns -- Automated task scheduling and tracking -- Automated customer relationship management (CRM) tools -- Automated customer feedback systems -- Automated customer segmentation and targeting -- Automated sales and support team organization -- Custom-built third-party integrations - -By leveraging OnceHub API, businesses can create more efficient and -cost-effective appointment management processes, helping to reduce manual labor -and free up time to focus on other areas of their business. Moreover, with the -ability to manage booking and scheduling across multiple time zones, businesses -can better optimize their resources for better performance. +The OnceHub API is a powerful tool for automating the scheduling of meetings and appointments. With this API, you can dynamically manage bookings, sync calendars, personalize notifications, and streamline the entire process of setting up and following up on appointments. When used on Pipedream, you can harness the serverless power of the platform to trigger workflows based on events in OnceHub, or to act upon data from other services, making the maintenance of your calendar and scheduling needs a breeze. + +# Example Use Cases + +**Automated Meeting Follow-Up Emails** + +- After a meeting is scheduled via OnceHub, a Pipedream workflow automatically triggers, sending a personalized follow-up email through a service like SendGrid or Gmail. This email can include meeting notes, action items, or a link to a feedback survey to enhance client engagement. + +**Slack Notifications for New Bookings** + +- When a new booking is made in OnceHub, Pipedream detects this event and posts a message to a designated Slack channel. This immediate notification enables a team to swiftly acknowledge new appointments and prepare accordingly, keeping everyone in the loop. + +**Sync Appointments to Google Calendar** + +- As soon as an appointment is booked through OnceHub, Pipedream triggers a workflow that creates an event in Google Calendar. This ensures that all appointments made through OnceHub are automatically reflected on your primary calendar, keeping your schedule up-to-date across platforms. diff --git a/components/one_ai/README.md b/components/one_ai/README.md new file mode 100644 index 0000000000000..61d5bee209e30 --- /dev/null +++ b/components/one_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +One AI API enables users to leverage advanced language AI to analyze and process text. On Pipedream, you can integrate One AI into serverless workflows to automate tasks like content summarization, emotion detection, or keyword extraction. By tapping into One AI's capabilities via Pipedream, you can enrich data, glean insights, and streamline content-centric operations with ease and efficiency. + +# Example Use Cases + +- **Content Summarization**: Create a workflow that triggers whenever a new article is published to your CMS. The One AI API analyzes the article and provides a summary that you can automatically post to your social media channels. + +- **Customer Feedback Analysis**: Set up a Pipedream workflow to collect customer feedback from various sources, use One AI to detect emotions and sentiments, and then categorize the feedback for product improvement or route to the appropriate team members. + +- **Keyword Alerts**: Build a workflow that scans news articles or social media posts using One AI to extract keywords. Combine this with Slack or email to send real-time alerts about relevant topics related to your business or industry. diff --git a/components/one_ai/package.json b/components/one_ai/package.json index 8d96c46352029..cb812d68a0a00 100644 --- a/components/one_ai/package.json +++ b/components/one_ai/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/onedesk/README.md b/components/onedesk/README.md index 55fd56290f12e..4ebe8e5b4fd99 100644 --- a/components/onedesk/README.md +++ b/components/onedesk/README.md @@ -1,33 +1,11 @@ # Overview -The [OneDesk API](https://www.onedesk.com/) allows developers to easily access -and integrate several of OneDesk's core services into their own applications. -With the OneDesk API, developers are able to customize and combine multiple -services to create completely unique and versatile applications that their -users can enjoy. +The OneDesk API allows for the automation and integration of project management and customer support tasks. Through Pipedream, users can leverage this API to create custom workflows that streamline processes, sync data across platforms, and react to events in real-time. By using OneDesk's capabilities in task and ticket management within Pipedream's serverless platform, users can create powerful automations that enhance productivity and coordination between teams. -The OneDesk API provides developers access to: +# Example Use Cases -- Project Management: Create, edit and delete projects and tasks, and - collaborate with team members -- Knowledge Management: Create, edit and share documents and FAQs -- Ideas and Feedback Management: Manage customer feedback and ideas -- Resource Scheduling: Define resource profiles and calendars for project - scheduling -- Time Tracking: Track time and cost allocations to tasks +- **Automated Task Creation from Emails**: When an email labeled "Support" arrives in Gmail, a Pipedream workflow triggers the creation of a new support ticket in OneDesk. This ensures no customer query is missed and support tickets are organized and actionable within OneDesk. -Using these and other core services from OneDesk, developers can create -powerful and engaging applications that cover a variety of use cases. Here are -some examples of applications you can build with the OneDesk API: +- **Real-time Project Updates to Slack**: Whenever a task is updated or completed in OneDesk, Pipedream sends a notification to a designated Slack channel. This keeps the team instantly informed about project progress, fostering a culture of transparency and up-to-date communication. -- Corporate Project Planning App - A mobile and web-based application that - allows teams to collaborate on planning and organizing projects -- Knowledge Base App - A web-based application that lets teams store, share and - update knowledge resources across multiple devices -- Time Tracking App - A mobile and web-based application that provides detailed - time tracking, resource scheduling and cost allocation capabilities -- Customer Support App - A web-based application that uses knowledge management - and feedback management capabilities to manage customer interactions and - inquiries quickly and easily -- Collaboration App - A mobile and web-based application that allows teams to - easily collaborate with each other on multiple projects and tasks +- **Syncing OneDesk Tickets with Trello for Visual Project Management**: Use Pipedream to sync OneDesk tickets to a Trello board automatically. When a new ticket is created in OneDesk, a corresponding card is created in Trello. This allows for visual management of tasks and can help teams that utilize both platforms for different aspects of project tracking. diff --git a/components/onedesk/actions/create-item/create-item.mjs b/components/onedesk/actions/create-item/create-item.mjs index 22aa48ba697e6..e635583807301 100644 --- a/components/onedesk/actions/create-item/create-item.mjs +++ b/components/onedesk/actions/create-item/create-item.mjs @@ -1,57 +1,75 @@ -import onedesk from "../../onedesk.app.mjs"; +import app from "../../onedesk.app.mjs"; export default { key: "onedesk-create-item", name: "Create Item", - description: "Creates a new item. [See the docs](https://www.onedesk.com/developers/#_create_work_item)", - version: "0.0.1", + description: "Creates a new item. [See the documentation](https://www.onedesk.com/dev/).", + version: "0.0.2", type: "action", props: { - onedesk, + app, + name: { + propDefinition: [ + app, + "itemName", + ], + }, type: { propDefinition: [ - onedesk, + app, "itemType", ], }, - name: { - type: "string", - label: "Name", - description: "Name of the item", - }, description: { - type: "string", - label: "Description", - description: "Description of the item", - optional: true, + propDefinition: [ + app, + "itemDescription", + ], }, - spaceId: { + projectExternalId: { propDefinition: [ - onedesk, - "spaceId", + app, + "projectId", ], }, priority: { propDefinition: [ - onedesk, + app, "priority", ], }, }, + methods: { + createItem(args = {}) { + return this.app.post({ + path: "/items/", + ...args, + }); + }, + }, async run({ $ }) { - const { data } = await this.onedesk.createItem({ + const { + createItem, + name, + type, + description, + projectExternalId, + priority, + } = this; + + const response = await createItem({ + $, data: { - type: this.type, - name: this.name, - description: this.description, - spaceId: this.spaceId, - priority: this.priority, + name, + type, + description, + projectExternalId, + priority, }, - $, }); - $.export("$summary", `Successfully created item with ID ${data.id}.`); + $.export("$summary", `Successfully created item with ID \`${response.data}\`.`); - return data; + return response; }, }; diff --git a/components/onedesk/actions/create-message/create-message.mjs b/components/onedesk/actions/create-message/create-message.mjs index 729e38b3fb764..981e5fcc908bc 100644 --- a/components/onedesk/actions/create-message/create-message.mjs +++ b/components/onedesk/actions/create-message/create-message.mjs @@ -1,44 +1,50 @@ -import onedesk from "../../onedesk.app.mjs"; +import app from "../../onedesk.app.mjs"; export default { key: "onedesk-create-message", name: "Create Message", - description: "Creates a message or comment. [See the docs](https://www.onedesk.com/developers/#_create_comment)", - version: "0.0.1", + description: "Creates a message or comment. [See the documentation](https://www.onedesk.com/dev/).", + version: "0.0.2", type: "action", props: { - onedesk, - itemId: { + app, + conversationExternalId: { propDefinition: [ - onedesk, - "itemId", + app, + "conversationExternalId", ], }, - description: { + content: { type: "string", - label: "Description", - description: "Content of the message", + label: "Content", + description: "Content of the conversation message", }, - postType: { - propDefinition: [ - onedesk, - "postType", - ], + }, + methods: { + createMessage(args = {}) { + return this.app.post({ + path: "/conversation-messages/", + ...args, + }); }, }, async run({ $ }) { - const { data } = await this.onedesk.createComment({ + const { + createMessage, + conversationExternalId, + content, + } = this; + + const response = await createMessage({ + $, data: { - itemId: this.itemId, - description: this.description, - postType: this.postType, - token: this.onedesk._authToken(), + conversationExternalId, + content, }, - $, }); - $.export("$summary", `Successfully created comment with ID ${data}.`); + $.export("$summary", `Successfully created message with ID \`${response.data}\`.`); - return data; + return response; }, }; diff --git a/components/onedesk/actions/create-project/create-project.mjs b/components/onedesk/actions/create-project/create-project.mjs index acfd7b1cbe199..4524c6e00588b 100644 --- a/components/onedesk/actions/create-project/create-project.mjs +++ b/components/onedesk/actions/create-project/create-project.mjs @@ -1,50 +1,68 @@ -import onedesk from "../../onedesk.app.mjs"; +import app from "../../onedesk.app.mjs"; export default { key: "onedesk-create-project", name: "Create Project", - description: "Creates a project/space. [See the docs](https://www.onedesk.com/developers/#_create_space)", - version: "0.0.1", + description: "Creates a project/space. [See the documentation](https://www.onedesk.com/dev/).", + version: "0.0.2", type: "action", props: { - onedesk, + app, + name: { + type: "string", + label: "Name", + description: "Name of the project.", + }, type: { + label: "Project Type", + description: "Type of the project.", propDefinition: [ - onedesk, + app, "containerType", ], }, - name: { - type: "string", - label: "Name", - description: "Name of the project/space", - }, description: { type: "string", label: "Description", - description: "Description of the project/space", + description: "Description of the project", optional: true, }, - parentIds: { + parentPortfolioExternalIds: { propDefinition: [ - onedesk, - "parentIds", + app, + "parentPortfolioExternalIds", ], }, }, + methods: { + createProject(args = {}) { + return this.app.post({ + path: "/projects/", + ...args, + }); + }, + }, async run({ $ }) { - const { data } = await this.onedesk.createSpace({ + const { + createProject, + type, + name, + description, + parentPortfolioExternalIds, + } = this; + + const response = await createProject({ + $, data: { - containerType: this.type, - name: this.name, - description: this.description, - parentIds: this.parentIds, + type, + name, + description, + parentPortfolioExternalIds, }, - $, }); - $.export("$summary", `Successfully created project with ID ${data.id}.`); + $.export("$summary", `Successfully created project with ID \`${response.data}\`.`); - return data; + return response; }, }; diff --git a/components/onedesk/actions/create-user/create-user.mjs b/components/onedesk/actions/create-user/create-user.mjs index a2e1bc79ad521..d76e553cc5c90 100644 --- a/components/onedesk/actions/create-user/create-user.mjs +++ b/components/onedesk/actions/create-user/create-user.mjs @@ -1,62 +1,84 @@ -import onedesk from "../../onedesk.app.mjs"; +import app from "../../onedesk.app.mjs"; export default { key: "onedesk-create-user", name: "Create User", - description: "Creates a user or a customer. [See the docs](https://www.onedesk.com/developers/#_create_user)", - version: "0.0.1", + description: "Creates a user or a customer. [See the documentation](https://www.onedesk.com/dev/).", + version: "0.0.2", type: "action", props: { - onedesk, + app, + email: { + type: "string", + label: "Email", + description: "The new user email", + }, firstName: { type: "string", label: "First Name", description: "The first name of the new user", + optional: true, }, lastName: { type: "string", label: "Last Name", description: "The last name of the new user", - }, - email: { - type: "string", - label: "Email", - description: "The new user email", + optional: true, }, type: { propDefinition: [ - onedesk, + app, "userType", ], }, - teamId: { + teams: { + type: "string[]", + label: "Team IDs", propDefinition: [ - onedesk, + app, "teamId", ], }, - isAdministrator: { + isAdmin: { type: "boolean", label: "Is Administrator", description: "Set to `true` if the new user should be an administrator", optional: true, }, }, + methods: { + createUser(args = {}) { + return this.app.post({ + path: "/users/", + ...args, + }); + }, + }, async run({ $ }) { - const { data } = await this.onedesk.createUser({ + const { + createUser, + email, + firstName, + lastName, + type, + teams, + isAdmin, + } = this; + + const response = await createUser({ + $, data: { - firstName: this.firstName, - lastName: this.lastName, - email: this.email, - type: this.type, - teamId: this.teamId, - isAdministrator: this.isAdministrator, + email, + firstName, + lastName, + type, + teams, + isAdmin, }, - $, }); - $.export("$summary", `Successfully created user with ID ${data.id}.`); + $.export("$summary", `Successfully created user with ID \`${response.data}\`.`); - return data; + return response; }, }; diff --git a/components/onedesk/actions/find-item/find-item.mjs b/components/onedesk/actions/find-item/find-item.mjs index eea117d637349..2916018a81f47 100644 --- a/components/onedesk/actions/find-item/find-item.mjs +++ b/components/onedesk/actions/find-item/find-item.mjs @@ -1,97 +1,90 @@ -import onedesk from "../../onedesk.app.mjs"; -import { ConfigurationError } from "@pipedream/platform"; +import app from "../../onedesk.app.mjs"; +import constants from "../../common/constants.mjs"; export default { key: "onedesk-find-item", name: "Find Item", - description: "Search for an existing item. [See the docs](https://www.onedesk.com/developers/#_search_items)", - version: "0.0.1", + description: "Search for an existing item. [See the documentation](https://www.onedesk.com/dev/).", + version: "0.0.2", type: "action", props: { - onedesk, - itemId: { + app, + itemType: { + optional: false, propDefinition: [ - onedesk, - "itemId", + app, + "itemType", ], + }, + creationTimeValue: { + type: "string", + label: "Creation Time Value", + description: "Item creation date. Eg. `2024-01-28`", optional: true, }, - spaceName: { - propDefinition: [ - onedesk, - "spaceName", + creationTimeOperator: { + type: "string", + label: "Creation Time Operator", + description: "Item creation date operator", + optional: true, + options: [ + { + label: "Equal", + value: "EQ", + }, + constants.DATE_OPERATOR.GT, + constants.DATE_OPERATOR.LT, ], }, name: { type: "string", label: "Item Name", description: "Name of the item", - optional: true, - }, - exactMatch: { - type: "boolean", - label: "Exact Match", - description: "The item name search should be an exact match", - optional: true, - default: false, }, }, async run({ $ }) { const { - itemId, - spaceName, + app, + itemType, + creationTimeValue, + creationTimeOperator, name, - exactMatch, } = this; - if (!itemId && !spaceName && !name) { - throw new ConfigurationError("Must enter `itemId`, `spaceName` or `name`."); - } - - const data = { - filters: [], - }; + const properties = [ + { + property: "name", + operation: "CONTAINS", + value: name, + }, + { + property: "creationTime", + operation: creationTimeOperator || constants.DATE_OPERATOR.LT.value, + value: creationTimeValue, + }, + ]; - if (itemId) { - data.itemIds = [ - itemId, - ]; - } - - if (spaceName) { - data.filters.push({ - propertyName: "spaceName", - operation: "EQ", - value: spaceName, - }); - } - - const { data: { items } } = await this.onedesk.searchItems({ - data, - $, - }); - - let results = []; - for (const workItemId of items) { - const { data: itemWithData } = await this.onedesk.getItem({ - params: { - workItemId, - }, + const results = await app.paginate({ + resourcesFn: app.filterItemDetails, + resourcesFnArgs: { $, - }); - results.push(itemWithData); - } + data: { + properties: properties.filter(({ value }) => value), + isAsc: false, + itemType: [ + itemType, + ], + }, + }, + resourceName: "data", + }); - if (name) { - const lowerCaseName = name.toLowerCase(); - results = results.filter((item) => - exactMatch - ? item.name === name - : item.name.toLowerCase().includes(lowerCaseName)); + if (!results.length) { + $.export("$summary", "No items found."); + } else { + $.export("$summary", `Found \`${results.length}\` matching item(s).`); } - $.export("$summary", `Found ${results.length} matching item(s).`); - return results; }, }; diff --git a/components/onedesk/actions/find-project/find-project.mjs b/components/onedesk/actions/find-project/find-project.mjs index d662a8b54686a..d6d7bd612dc97 100644 --- a/components/onedesk/actions/find-project/find-project.mjs +++ b/components/onedesk/actions/find-project/find-project.mjs @@ -1,87 +1,94 @@ -import onedesk from "../../onedesk.app.mjs"; -import { ConfigurationError } from "@pipedream/platform"; +import app from "../../onedesk.app.mjs"; +import constants from "../../common/constants.mjs"; export default { key: "onedesk-find-project", name: "Find Project", - description: "Search for a project/space by name or ID. [See the docs](https://www.onedesk.com/developers/#_search_spaces)", - version: "0.0.1", + description: "Search for a project/space by name or ID. [See the documentation](https://www.onedesk.com/dev/).", + version: "0.0.2", type: "action", props: { - onedesk, - spaceId: { + app, + creationTimeValue: { + type: "string", + label: "Creation Time Value", + description: "Project creation date. Eg. `2024-01-28`", + }, + creationTimeOperator: { + type: "string", + label: "Creation Time Operator", + description: "Project creation date operator", + optional: true, + options: Object.values(constants.DATE_OPERATOR), + }, + invoiceType: { + label: "Invoice Type", + description: "Type of invoice. Eg. `flat fee`", propDefinition: [ - onedesk, - "spaceId", + app, + "invoice", ], }, - name: { + dueDateValue: { type: "string", - label: "Name", - description: "Name of the project/space", + label: "Due Date Value", + description: "Due date of project. Eg. `2024-01-28`", optional: true, }, - exactMatch: { - type: "boolean", - label: "Exact Match", - description: "The project name search should be an exact match", + dueDateOperator: { + type: "string", + label: "Due Date Operator", + description: "Due date of project operator", optional: true, - default: false, + options: Object.values(constants.DATE_OPERATOR), }, }, async run({ $ }) { const { - spaceId, - name, - exactMatch, + app, + creationTimeValue, + creationTimeOperator, + invoiceType, + dueDateValue, + dueDateOperator, } = this; - if (!spaceId && !name) { - throw new ConfigurationError("Must enter `spaceId` or `name`."); - } - - const data = {}; - - if (spaceId) { - data.itemIds = [ - spaceId, - ]; - } - - if (name && exactMatch) { - data.filters = [ - { - propertyName: "name", - operation: "EQ", - value: name, - }, - ]; - } - - const { data: { items: projects } } = await this.onedesk.searchSpaces({ - data, - $, - }); + const properties = [ + { + property: "creationTime", + operation: creationTimeOperator || constants.DATE_OPERATOR.LT.value, + value: creationTimeValue, + }, + { + property: "invoiceType", + operation: "EQ", + value: invoiceType, + }, + { + property: "dueDate", + operation: dueDateOperator || constants.DATE_OPERATOR.LT.value, + value: dueDateValue, + }, + ]; - let results = []; - for (const projectId of projects) { - const { data: projectWithData } = await this.onedesk.getSpace({ + const results = await app.paginate({ + resourcesFn: app.filterProjectDetails, + resourcesFnArgs: { + $, data: { - id: projectId, + properties: properties.filter(({ value }) => value), + isAsc: false, }, - $, - }); - results.push(projectWithData); - } + }, + resourceName: "data", + }); - if (name && !exactMatch) { - const lowerCaseName = name.toLowerCase(); - results = results.filter((project) => - project.name.toLowerCase().includes(lowerCaseName)); + if (!results.length) { + $.export("$summary", "No projects found."); + } else { + $.export("$summary", `Found \`${results.length}\` matching project(s).`); } - $.export("$summary", `Found ${results.length} matching project(s).`); - return results; }, }; diff --git a/components/onedesk/actions/update-item/update-item.mjs b/components/onedesk/actions/update-item/update-item.mjs index ba2362f1dee42..2e03833727725 100644 --- a/components/onedesk/actions/update-item/update-item.mjs +++ b/components/onedesk/actions/update-item/update-item.mjs @@ -1,62 +1,89 @@ -import onedesk from "../../onedesk.app.mjs"; -import pickBy from "lodash.pickby"; +import app from "../../onedesk.app.mjs"; export default { key: "onedesk-update-item", name: "Update Item", - description: "Updates an existing item. [See the docs](https://www.onedesk.com/developers/#_update_work_item)", - version: "0.0.1", + description: "Updates an existing item. [See the documentation](https://www.onedesk.com/dev/).", + version: "0.0.2", type: "action", props: { - onedesk, + app, + itemType: { + optional: false, + propDefinition: [ + app, + "itemType", + ], + }, itemId: { propDefinition: [ - onedesk, + app, "itemId", + ({ itemType }) => ({ + itemType, + }), ], - description: "", }, name: { - type: "string", - label: "Name", - description: "Name of the item", optional: true, + propDefinition: [ + app, + "itemName", + ], }, description: { - type: "string", - label: "Description", - description: "Description of the item", - optional: true, + propDefinition: [ + app, + "itemDescription", + ], }, priority: { propDefinition: [ - onedesk, + app, "priority", ], - optional: true, }, - progress: { + percentComplete: { type: "integer", label: "Progress", - description: "Progress of the item between 1 and 100", + description: "Progress of the item between `1` and `100`", max: 100, optional: true, }, }, + methods: { + updateItem({ + itemId, ...args + } = {}) { + return this.app.post({ + path: `/items/id/${itemId}`, + ...args, + }); + }, + }, async run({ $ }) { - const { data } = await this.onedesk.updateItem({ - data: pickBy({ - itemId: this.itemId, - name: this.name, - spaceId: this.spaceId, - priority: this.priority, - progress: this.progress, - }), + const { + updateItem, + itemId, + name, + description, + priority, + percentComplete, + } = this; + + const response = await updateItem({ $, + itemId, + data: { + name, + description, + priority, + percentComplete, + }, }); - $.export("$summary", `Successfully updated item with ID ${data.id}.`); + $.export("$summary", `Successfully updated item with code \`${response}\`.`); - return data; + return response; }, }; diff --git a/components/onedesk/common/constants.mjs b/components/onedesk/common/constants.mjs index c76a8f8e2a629..44d42a43f1aae 100644 --- a/components/onedesk/common/constants.mjs +++ b/components/onedesk/common/constants.mjs @@ -25,13 +25,34 @@ const PRIORITY_OPTIONS = [ }, ]; -const POST_TYPES = [ - "OLD_REQUESTER_ONLY_POST", - "PUBLIC_DISCUSSION_POST", - "PRIVATE_DISCUSSION_POST", -]; +const DEFAULT_LIMIT = 20; +const DEFAULT_MAX = 100; + +const DATE_OPERATOR = { + LE: { + label: "Less Than or Equal", + value: "LE", + }, + LT: { + label: "Less Than", + value: "LT", + }, + GT: { + label: "Greater Than", + value: "GT", + }, + GE: { + label: "Greater Than or Equal", + value: "GE", + }, +}; + +const LAST_CREATION_DATE = "lastCreationDate"; export default { PRIORITY_OPTIONS, - POST_TYPES, + DEFAULT_LIMIT, + DEFAULT_MAX, + DATE_OPERATOR, + LAST_CREATION_DATE, }; diff --git a/components/onedesk/common/utils.mjs b/components/onedesk/common/utils.mjs new file mode 100644 index 0000000000000..fa76a8907af05 --- /dev/null +++ b/components/onedesk/common/utils.mjs @@ -0,0 +1,26 @@ +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +function getDateOnly(date) { + const dateStr = date instanceof Date + ? date.toISOString() + : date; + return dateStr.split("T")[0]; +} + +function getDateAfterToday(days = 1) { + const dateAfterToday = new Date(); + dateAfterToday.setDate(dateAfterToday.getDate() + days); + return getDateOnly(dateAfterToday); +} + +export default { + iterate, + getDateOnly, + getDateAfterToday, +}; diff --git a/components/onedesk/onedesk.app.mjs b/components/onedesk/onedesk.app.mjs index 61b7af6896aea..6b921511bf8bc 100644 --- a/components/onedesk/onedesk.app.mjs +++ b/components/onedesk/onedesk.app.mjs @@ -1,138 +1,221 @@ import { axios } from "@pipedream/platform"; import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; export default { type: "app", app: "onedesk", propDefinitions: { - userType: { + containerType: { type: "string", - label: "User Type", - description: "Type of user or customer", + label: "Container Type", + description: "Type of the container", optional: true, - async options() { - const { data: { userTypes } } = await this.getOrgInfo(); - return userTypes.map((type) => ({ - label: type.name, - value: type.userType, - })); + async options({ + filter = ({ visible }) => visible, + mapper = ({ label }) => label, + }) { + const { data: containerTypes } = await this.getContainerTypes(); + return containerTypes + .filter(filter) + .map(mapper); }, }, - itemType: { - type: "string", - label: "Item Type", - description: "Type of the item", - async options() { - const { data: { itemTypes } } = await this.getOrgInfo(); - return itemTypes.map((type) => ({ - label: type.name, - value: type.itemType, - })); + parentPortfolioExternalIds: { + type: "string[]", + label: "Parent Portfolio External IDs", + description: "Array of parent portfolio external IDs. Type the name of the parent portfolio to search for it.", + useQuery: true, + optional: true, + async options({ + query, prevContext: { offset = 0 }, + }) { + const { data } = await this.filterPortfolioDetails({ + data: { + properties: [ + { + property: "name", + operation: "CONTAINS", + value: query, + }, + ], + isAsc: false, + limit: constants.DEFAULT_LIMIT, + offset, + }, + }); + + return { + options: data.map(({ + name: label, + externalId: value, + }) => ({ + label, + value, + })), + context: { + offset: offset + constants.DEFAULT_LIMIT, + }, + }; }, }, - containerType: { + userType: { type: "string", - label: "Container Type", - description: "Type of the space", + label: "User Type", + description: "Type of user or customer", + optional: true, async options() { - const { data: { containerTypes } } = await this.getOrgInfo(); - return containerTypes.map((type) => ({ - label: type.name, - value: type.containerType, - })); + const { data: userTypes } = await this.getUserTypes(); + return userTypes.map(({ label }) => label); }, }, teamId: { type: "string", label: "Team ID", description: "The ID of the team", + useQuery: true, optional: true, - async options() { - const { data } = await this.listTeams(); - return data.map((team) => ({ - label: team.name, - value: team.id, - })); + async options({ + query, prevContext: { offset = 0 }, + }) { + const { data } = await this.filterTeamDetails({ + data: { + properties: [ + { + property: "name", + operation: "CONTAINS", + value: query, + }, + ], + isAsc: false, + limit: constants.DEFAULT_LIMIT, + offset, + }, + }); + + return { + options: data.map(({ + name: label, + externalId: value, + }) => ({ + label, + value, + })), + context: { + offset: offset + constants.DEFAULT_LIMIT, + }, + }; }, }, - itemId: { + invoice: { type: "string", - label: "Item ID", - description: "Id of the work item to post a comment on", - async options() { - const { data: { items } } = await this.searchItems(); - const { data: { items: workspaceItems } } = await this.getItemsByIds({ + label: "Invoice", + description: "The invoice to search for", + optional: true, + async options({ + prevContext: { offset = 0 }, + mapper = ({ type }) => type, + }) { + const { data } = await this.filterInvoiceDetails({ data: { - workItemIds: items, + properties: [ + { + property: "creationTime", + operation: constants.DATE_OPERATOR.LT.value, + value: utils.getDateAfterToday(), + }, + ], + isAsc: false, + limit: constants.DEFAULT_LIMIT, + offset, }, }); - return workspaceItems.map((workspaceItem) => ({ - label: workspaceItem.name, - value: workspaceItem.id, - })); + + return { + options: data.map(mapper), + context: { + offset: offset + constants.DEFAULT_LIMIT, + }, + }; }, }, - spaceId: { + conversationExternalId: { type: "string", - label: "Space ID", - description: "The ID of the space or project", - optional: true, - async options() { - const spaces = []; - const { data: { items } } = await this.searchSpaces(); - for (const item of items) { - const { data: space } = await this.getSpace({ - data: { - id: item, - }, - }); - spaces.push({ - label: space.name, - value: space.id, - }); - } - return spaces; + label: "Conversation External ID", + description: "The external ID of the conversation", + async options({ prevContext: { offset = 0 } }) { + const { data } = await this.filterConversationDetails({ + data: { + properties: [ + { + property: "createdTime", + operation: constants.DATE_OPERATOR.LT.value, + value: utils.getDateAfterToday(), + }, + ], + isAsc: false, + limit: constants.DEFAULT_LIMIT, + offset, + }, + }); + + return { + options: data.map(({ + conversationId: value, + content: label, + }) => ({ + label, + value, + })), + context: { + offset: offset + constants.DEFAULT_LIMIT, + }, + }; }, }, - spaceName: { + itemType: { type: "string", - label: "Space Name", - description: "The name of the space or project", + label: "Item Type", + description: "Type of the item", optional: true, async options() { - const spaces = []; - const { data: { items } } = await this.searchSpaces(); - for (const item of items) { - const { data: space } = await this.getSpace({ - data: { - id: item, - }, - }); - spaces.push(space.name); - } - return spaces; + const { data: itemTypes } = await this.getItemTypes(); + return itemTypes.map(({ label }) => label); }, }, - parentIds: { - type: "string[]", - label: "Parent IDs", - description: "Array of parent/portfolio IDs", + projectId: { + type: "string", + label: "Project ID", + description: "The ID of the project", optional: true, - async options() { - const portfolios = []; - const { data: { items } } = await this.searchPortfolios(); - for (const item of items) { - const { data: portfolio } = await this.getPortfolio({ - data: { - id: item, - }, - }); - portfolios.push({ - label: portfolio.name, - value: portfolio.id, - }); - } - return portfolios; + async options({ prevContext: { offset = 0 } }) { + const { data } = await this.filterProjectDetails({ + data: { + properties: [ + { + property: "creationTime", + operation: constants.DATE_OPERATOR.LT.value, + value: utils.getDateAfterToday(), + }, + ], + isAsc: false, + limit: constants.DEFAULT_LIMIT, + offset, + }, + }); + + return { + options: data.map(({ + name: label, + externalId: value, + }) => ({ + label, + value, + })), + context: { + offset: offset + constants.DEFAULT_LIMIT, + }, + }; }, }, priority: { @@ -142,154 +225,209 @@ export default { optional: true, options: constants.PRIORITY_OPTIONS, }, - postType: { + itemId: { + type: "integer", + label: "Item ID", + description: "Id of the item", + useQuery: true, + async options({ + query, prevContext: { offset = 0 }, itemType, + }) { + const properties = [ + { + property: "creationTime", + operation: constants.DATE_OPERATOR.LT.value, + value: utils.getDateAfterToday(), + }, + { + property: "name", + operation: "CONTAINS", + value: query, + }, + ]; + + const { data } = await this.filterItemDetails({ + data: { + properties: properties.filter(({ value }) => value), + isAsc: false, + limit: constants.DEFAULT_LIMIT, + offset, + itemType: [ + itemType, + ], + }, + }); + + return { + options: data.map(({ + name: label, + id: value, + }) => ({ + label, + value, + })), + context: { + offset: offset + constants.DEFAULT_LIMIT, + }, + }; + }, + }, + itemName: { type: "string", - label: "Post Type", - description: "Type of comment", - options: constants.POST_TYPES, + label: "Name", + description: "Name of the item", + }, + itemDescription: { + type: "string", + label: "Description", + description: "Description of the item", + optional: true, }, }, methods: { _baseUrl() { - return "https://app.onedesk.com/rest/2.0"; + return "https://app.onedesk.com/rest/public"; }, _authToken() { return this.$auth.oauth_access_token; }, - _buildAuth(method, data, params) { - if (method === "POST") { - return { - data: { - ...data, - authenticationToken: this._authToken(), - }, - }; - } + getHeaders(headers) { return { - params: { - ...params, - token: this._authToken(), - }, + ...headers, + "OD-Public-API-Key": `${this.$auth.api_key}`, }; }, async _makeRequest({ - $ = this, - path, - method = "GET", - data, - params, - ...args - }) { - const config = { + $ = this, path, headers, ...args + } = {}) { + const response = await axios($, { url: `${this._baseUrl()}${path}`, - method, - ...this._buildAuth(method, data, params), - ...args, - }; - return axios($, config); - }, - getItemUpdates(args = {}) { - return this._makeRequest({ - path: "/history/getItemUpdates", - method: "POST", - ...args, - }); - }, - getOrgInfo(args = {}) { - return this._makeRequest({ - path: "/organization/getOrgInfo", + headers: this.getHeaders(headers), ...args, }); + + if (response?.data === null) { + throw new Error(`No data returned from the API ${JSON.stringify(response, null, 2)}`); + } + + if (response?.status === 429) { + throw new Error("API rate limit exceeded"); + } + + return response; }, - getSpace(args = {}) { + post(args = {}) { return this._makeRequest({ - path: "/space/getItemById", method: "POST", ...args, }); }, - getItem(args = {}) { + getContainerTypes(args = {}) { return this._makeRequest({ - path: "/workitem/getItemDetails", + path: "/organization/containerTypes", ...args, }); }, - getPortfolio(args = {}) { + getUserTypes(args = {}) { return this._makeRequest({ - path: "/portfolio/getItemById", - method: "POST", + path: "/organization/userTypes", ...args, }); }, - getItemsByIds(args = {}) { + getItemTypes(args = {}) { return this._makeRequest({ - path: "/workitem/getItems", - method: "POST", + path: "/organization/itemTypes", ...args, }); }, - listTeams(args = {}) { - return this._makeRequest({ - path: "/userteam/get", - method: "POST", + filterPortfolioDetails(args = {}) { + return this.post({ + path: "/portfolios/filter/details", ...args, }); }, - createUser(args = {}) { - return this._makeRequest({ - path: "/user/create", - method: "POST", + filterTeamDetails(args = {}) { + return this.post({ + path: "/teams/filter/details", ...args, }); }, - createItem(args = {}) { - return this._makeRequest({ - path: "/workitem/createWorkItem", - method: "POST", + filterInvoiceDetails(args = {}) { + return this.post({ + path: "/invoices/filter/details", ...args, }); }, - createSpace(args = {}) { - return this._makeRequest({ - path: "/space/create", - method: "POST", + filterProjectDetails(args = {}) { + return this.post({ + path: "/projects/filter/details", ...args, }); }, - createComment(args = {}) { - return this._makeRequest({ - path: "/workitem/createComment", - method: "POST", + filterConversationDetails(args = {}) { + return this.post({ + path: "/conversation-messages/filter/details", ...args, }); }, - searchSpaces(args = {}) { - return this._makeRequest({ - path: "/space/search", - method: "POST", + filterItemDetails(args = {}) { + return this.post({ + path: "/items/filter/details", ...args, }); }, - searchPortfolios(args = {}) { - return this._makeRequest({ - path: "/portfolio/search", - method: "POST", + filterActivityDetails(args = {}) { + return this.post({ + debug: true, + path: "/activities/filter/details", ...args, }); }, - searchItems(args = {}) { - return this._makeRequest({ - path: "/workitem/searchItems", - method: "POST", - ...args, - }); + async *getIterations({ + resourcesFn, resourcesFnArgs, resourceName, + max = constants.DEFAULT_MAX, + }) { + let offset = 0; + let resourcesCount = 0; + + while (true) { + const response = + await resourcesFn({ + ...resourcesFnArgs, + data: { + ...resourcesFnArgs?.data, + limit: constants.DEFAULT_LIMIT, + offset, + }, + }); + + const nextResources = resourceName && response[resourceName] || response; + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + yield resource; + resourcesCount += 1; + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + if (nextResources.length < constants.DEFAULT_LIMIT) { + console.log("No next page found"); + return; + } + + offset += constants.DEFAULT_LIMIT; + } }, - updateItem(args = {}) { - return this._makeRequest({ - path: "/workitem/updateWorkItem", - method: "POST", - ...args, - }); + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); }, }, }; diff --git a/components/onedesk/package.json b/components/onedesk/package.json index e54e8bba32810..325bf2f0c3cf4 100644 --- a/components/onedesk/package.json +++ b/components/onedesk/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/onedesk", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream OneDesk Components", "main": "onedesk.app.mjs", "keywords": [ @@ -13,8 +13,6 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.3.0", - "lodash.pickby": "^4.6.0", - "uuid": "^9.0.0" + "@pipedream/platform": "^3.0.0" } } diff --git a/components/onedesk/sources/common/activity-types.mjs b/components/onedesk/sources/common/activity-types.mjs new file mode 100644 index 0000000000000..49a406a20a556 --- /dev/null +++ b/components/onedesk/sources/common/activity-types.mjs @@ -0,0 +1,85 @@ +// The following are the activity types that are available in OneDesk according to the Support team. +export default { + ADDED_CUSTOMER_REPLY: "added a Customer Reply", + REMOVED_CUSTOMER_REPLY: "removed a Customer Reply", + CHANGED_CUSTOMER_REPLY: "changed a Customer Reply", + REMOVED_CUSTOM_FIELD: "removed the custom field", + PROJECT_ARCHIVED: "project archived", + UPDATE_ORGANIZATION_FINANCIAL: "update organization financial", + ADDED_ATTACHMENT: "added an attachment", + REMOVED_ATTACHMENT: "attachment(s) removed", + ADDED_ASSIGNEE: "added an assignee", + REMOVED_ASSIGNEE: "removed an assignee", + ADDED_TEAM_ASSIGNMENT: "added a team assignment", + REMOVED_TEAM_ASSIGNMENT: "removed a team assignment", + ADDED_LINK_TO_ITEM: "added a link to an item", + REMOVED_LINK_TO_ITEM: "removed a link to an item", + SENT_EMAIL: "sent email", + EMAIL_SENT: "email sent", + LOGGED_IN: "logged in", + LOGGED_OUT: "logged out", + FIRST_NAME_UPDATED: "first name updated", + LAST_NAME_UPDATED: "last name updated", + ADDED_FOLLOWER: "added follower", + REMOVED_FOLLOWER: "removed follower", + CREATED_WORK_ITEM: "created work item", + DELETED_WORK_ITEM: "deleted work item", + CREATED_PROJECT: "created project", + DELETED_PROJECT: "deleted project", + CREATED_PORTFOLIO: "created portfolio", + DELETED_PORTFOLIO: "deleted portfolio", + CREATED_MARKER: "created marker", + DELETED_MARKER: "deleted marker", + USER_JOINED_PROJECT: "user joined project", + USER_LEFT_PROJECT: "user left project", + CREATED_USER: "created user", + DEACTIVATED_USER: "deactivated user", + REACTIVATED_USER: "reactivated user", + ASSIGNED_TO_SLA: "assigned to SLA", + CHANGED_SLA: "changed the SLA", + ADDED_PROJECT_FOLLOWER: "added project follower", + REMOVED_PROJECT_FOLLOWER: "removed project follower", + CREATE_INVOICE: "create invoice", + UPDATE_INVOICE: "update invoice", + NUMBER_OF_USER_LICENSES_UPDATED: "number of user licenses updated", + COMPLETE_UPDATED: "complete updated", + ACTUAL_COST_UPDATED: "actual cost updated", + ACTUAL_FINISH_DATE_UPDATED: "actual finish date updated", + ACTUAL_START_DATE_UPDATED: "actual start date updated", + // This was given by OneDesk Support team however it returns 500 when used so the correct + // one right now is `changed the actual work` + // ACTUAL_WORK_UPDATED: "actual work updated", + ACTUAL_WORK_UPDATED: "changed the actual work", + ADDED_INTERNAL_MESSAGE: "added a Internal message", + AGILE_POINTS_CAPACITY_UPDATED: "agile points capacity updated", + AGILE_POINTS_UPDATED: "agile points updated", + CHANGED_INTERNAL_MESSAGE: "changed a Internal message", + CUSTOM_FIELD_UPDATED: "custom field updated", + DELETED_USER: "deleted user", + DESCRIPTION_UPDATED: "description updated", + ESTIMATION_TYPE_UPDATED: "estimation type updated", + LIFECYCLE_STATUS_UPDATED: "lifecycle status updated", + NAME_UPDATED: "name updated", + ONE_TIME_ACCESS_GRANTED: "one-time access granted", + ONE_TIME_ACCESS_REVOKED: "one-time access revoked", + PLANNED_CONSTRAINT_DATE_UPDATED: "planned constraint date updated", + PLANNED_CONSTRAINT_TYPE_UPDATED: "planned constraint type updated", + PLANNED_COST_UPDATED: "planned cost updated", + PLANNED_DURATION_AMOUNT_UPDATED: "planned duration amount updated", + PLANNED_DURATION_UNIT_UPDATED: "planned duration unit updated", + PLANNED_ESTIMATED_REVENUE_UPDATED: "planned estimated revenue updated", + PLANNED_FINISH_DATE_UPDATED: "planned finish date updated", + PLANNED_START_DATE_UPDATED: "planned start date updated", + PLANNED_WORK_AMOUNT_UPDATED: "planned work amount updated", + PLANNED_WORK_UNIT_UPDATED: "planned work unit updated", + PRIORITY_UPDATED: "priority updated", + PROJECT_PLANNED_START_DATE_UPDATED: "project planned start date updated", + PROJECT_PRIVACY_UPDATED: "project privacy updated", + PROJECT_UPDATED: "project updated", + PUBLISHED_STATE_UPDATED: "published state updated", + REMOVED_INTERNAL_MESSAGE: "removed a Internal message", + SCHEDULED_FOR_DELETION: "scheduled for deletion", + SUPPORT_TEAM_LOGGED_IN: "support team logged in", + TYPE_UPDATED: "type updated", + UPDATED_REMAINING_PREPAID_HOURS: "updated remaining prepaid hours", +}; diff --git a/components/onedesk/sources/common/common.mjs b/components/onedesk/sources/common/common.mjs deleted file mode 100644 index 42110a5ab9cd5..0000000000000 --- a/components/onedesk/sources/common/common.mjs +++ /dev/null @@ -1,52 +0,0 @@ -import onedesk from "../../onedesk.app.mjs"; -import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; -import { v4 as uuidv4 } from "uuid"; - -export default { - props: { - onedesk, - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - }, - hooks: { - async deploy() { - await this.resetApplicationId(); - }, - }, - methods: { - getApplicationId() { - return this.db.get("applicationId"); - }, - setApplicationId(applicationId) { - this.db.set("applicationId", applicationId); - }, - async resetApplicationId() { - const applicationId = uuidv4(); - await this.getUpdates(applicationId); - this.setApplicationId(applicationId); - }, - getUpdates() { - throw new Error("getUpdates is not implemented"); - }, - generateMeta() { - throw new Error("generateMeta is not implemented"); - }, - }, - async run() { - const applicationId = this.getApplicationId(); - - const results = await this.getUpdates(applicationId); - - await this.resetApplicationId(); - - for (const result of results) { - const meta = this.generateMeta(result); - this.$emit(result, meta); - } - }, -}; diff --git a/components/onedesk/sources/common/polling.mjs b/components/onedesk/sources/common/polling.mjs new file mode 100644 index 0000000000000..31a25e615494e --- /dev/null +++ b/components/onedesk/sources/common/polling.mjs @@ -0,0 +1,104 @@ +import { + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + ConfigurationError, +} from "@pipedream/platform"; +import app from "../../onedesk.app.mjs"; +import constants from "../../common/constants.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + setLastCreationDate(value) { + this.db.set(constants.LAST_CREATION_DATE, value); + }, + getLastCreationDate() { + return this.db.get(constants.LAST_CREATION_DATE); + }, + getActivityTypeProperties() { + throw new ConfigurationError("getActivityTypeProperties is not implemented"); + }, + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getResourceName() { + return "data"; + }, + getResourcesFn() { + return this.app.filterActivityDetails; + }, + getResourcesFnArgs() { + const lastCreationDate = this.getLastCreationDate(); + + const creationDateFilter = lastCreationDate + ? { + property: "createdDate", + operation: "GE", + value: utils.getDateOnly(lastCreationDate), + } + : { + property: "createdDate", + operation: "LT", + value: utils.getDateAfterToday(), + }; + + return { + data: { + properties: [ + creationDateFilter, + ...this.getActivityTypeProperties(), + ], + isAsc: false, + }, + }; + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + async processResources(resources) { + const { + setLastCreationDate, + processResource, + } = this; + + const [ + lastResource, + ] = resources; + + if (lastResource?.timestamp) { + setLastCreationDate(lastResource.timestamp); + } + + Array.from(resources) + .reverse() + .forEach(processResource); + }, + }, + async run() { + const { + app, + getResourcesFn, + getResourcesFnArgs, + getResourceName, + processResources, + } = this; + + const resources = await app.paginate({ + resourcesFn: getResourcesFn(), + resourcesFnArgs: getResourcesFnArgs(), + resourceName: getResourceName(), + }); + + processResources(resources); + }, +}; diff --git a/components/onedesk/sources/new-item-created/new-item-created.mjs b/components/onedesk/sources/new-item-created/new-item-created.mjs index 1b6db28366577..add5c8f359500 100644 --- a/components/onedesk/sources/new-item-created/new-item-created.mjs +++ b/components/onedesk/sources/new-item-created/new-item-created.mjs @@ -1,35 +1,33 @@ -import common from "../common/common.mjs"; +import common from "../common/polling.mjs"; +import activityTypes from "../common/activity-types.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "onedesk-new-item-created", name: "New Item Created", - description: "Emit new event when a new item is created. [See the docs](https://www.onedesk.com/developers/#_get_item_updates)", - version: "0.0.1", + description: "Emit new event when a new item is created. [See the documentation](https://www.onedesk.com/dev/).", + version: "0.0.2", type: "source", dedupe: "unique", methods: { ...common.methods, - async getUpdates(applicationId) { - const { data } = await this.onedesk.getItemUpdates({ - data: { - applicationId, - itemTypes: [ - "ProjectTask", - ], - operations: [ - "CREATE", - ], + getActivityTypeProperties() { + return [ + { + property: "types", + operation: "EQ", + value: activityTypes.CREATED_WORK_ITEM, }, - }); - return data; + ]; }, - generateMeta(item) { + generateMeta(resource) { return { - id: item.itemId, - summary: item.itemName || `New Item ID ${item.itemId}`, - ts: Date.parse(item.collectedTimestamp), + id: resource.itemExternalId, + summary: `New Item Created: ${resource.itemName || resource.itemExternalId}`, + ts: Date.parse(resource.timestamp), }; }, }, + sampleEmit, }; diff --git a/components/onedesk/sources/new-item-created/test-event.mjs b/components/onedesk/sources/new-item-created/test-event.mjs new file mode 100644 index 0000000000000..cb1bbb5c3e44a --- /dev/null +++ b/components/onedesk/sources/new-item-created/test-event.mjs @@ -0,0 +1,11 @@ +export default { + "projectExternalId": "91d899c0-e3c4-46c3-9cc2-97b6ea2a708b", + "authorExternalId": "d5ef5e78-a82b-477f-be9c-a17b04283648", + "itemExternalId": "26ecb43a-69b1-4550-962f-616bbdc5476c", + "activityType": "created work item", + "automationTypeName": null, + "timestamp": "2024-06-12T22:20:06.598+00:00", + "itemType": "Item", + "itemName": null, + "newValue": null +} diff --git a/components/onedesk/sources/new-item-updated/new-item-updated.mjs b/components/onedesk/sources/new-item-updated/new-item-updated.mjs deleted file mode 100644 index 2afa94c7caa9f..0000000000000 --- a/components/onedesk/sources/new-item-updated/new-item-updated.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - key: "onedesk-new-item-updated", - name: "New Item Updated", - description: "Emit new event when an item is updated. [See the docs](https://www.onedesk.com/developers/#_get_item_updates)", - version: "0.0.1", - type: "source", - dedupe: "unique", - methods: { - ...common.methods, - async getUpdates(applicationId) { - const { data } = await this.onedesk.getItemUpdates({ - data: { - applicationId, - itemTypes: [ - "ProjectTask", - ], - operations: [ - "PROPERTY_UPDATE", - ], - }, - }); - return data; - }, - generateMeta(item) { - return { - id: item.itemId, - summary: item.itemName || `New Item ID ${item.itemId}`, - ts: Date.parse(item.collectedTimestamp), - }; - }, - }, -}; diff --git a/components/onedesk/sources/new-project-created/new-project-created.mjs b/components/onedesk/sources/new-project-created/new-project-created.mjs index 29d818f5c6fc5..ec5493dad0f74 100644 --- a/components/onedesk/sources/new-project-created/new-project-created.mjs +++ b/components/onedesk/sources/new-project-created/new-project-created.mjs @@ -1,35 +1,33 @@ -import common from "../common/common.mjs"; +import common from "../common/polling.mjs"; +import activityTypes from "../common/activity-types.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "onedesk-new-project-created", name: "New Project Created", - description: "Emit new event when a new project is created. [See the docs](https://www.onedesk.com/developers/#_get_item_updates)", - version: "0.0.1", + description: "Emit new event when a new project is created. [See the documentation](https://www.onedesk.com/dev/).", + version: "0.0.2", type: "source", dedupe: "unique", methods: { ...common.methods, - async getUpdates(applicationId) { - const { data } = await this.onedesk.getItemUpdates({ - data: { - applicationId, - itemTypes: [ - "ProjectVariables", - ], - operations: [ - "CREATE", - ], + getActivityTypeProperties() { + return [ + { + property: "types", + operation: "EQ", + value: activityTypes.CREATED_PROJECT, }, - }); - return data; + ]; }, - generateMeta(project) { + generateMeta(resource) { return { - id: project.itemId, - summary: project.itemName, - ts: Date.parse(project.collectedTimestamp), + id: resource.itemExternalId, + summary: `New Project Created: ${resource.itemName}`, + ts: Date.parse(resource.timestamp), }; }, }, + sampleEmit, }; diff --git a/components/onedesk/sources/new-project-created/test-event.mjs b/components/onedesk/sources/new-project-created/test-event.mjs new file mode 100644 index 0000000000000..7e5928602e7c1 --- /dev/null +++ b/components/onedesk/sources/new-project-created/test-event.mjs @@ -0,0 +1,11 @@ +export default { + "projectExternalId": "99f6ea5e-da59-48c1-8a61-15cde4a11cf3", + "authorExternalId": "d5ef5e78-a82b-477f-be9c-a17b04283648", + "itemExternalId": "99f6ea5e-da59-48c1-8a61-15cde4a11cf3", + "activityType": "created project", + "automationTypeName": null, + "timestamp": "2024-06-12T20:43:10.770+00:00", + "itemType": "Project", + "itemName": "Test 100", + "newValue": null +} diff --git a/components/onedesk/sources/new-project-updated/new-project-updated.mjs b/components/onedesk/sources/new-project-updated/new-project-updated.mjs deleted file mode 100644 index 074083a0b0e6a..0000000000000 --- a/components/onedesk/sources/new-project-updated/new-project-updated.mjs +++ /dev/null @@ -1,36 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - key: "onedesk-new-project-updated", - name: "New Project Updated", - description: "Emit new event when a project is updated. [See the docs](https://www.onedesk.com/developers/#_get_item_updates)", - version: "0.0.1", - type: "source", - dedupe: "unique", - methods: { - ...common.methods, - async getUpdates(applicationId) { - const { data } = await this.onedesk.getItemUpdates({ - data: { - applicationId, - itemTypes: [ - "Space", - ], - operations: [ - "PROPERTY_UPDATE", - ], - }, - }); - return data; - }, - generateMeta(project) { - const ts = Date.parse(project.collectedTimestamp); - return { - id: `${project.itemId}${ts}`, - summary: project.itemName, - ts, - }; - }, - }, -}; diff --git a/components/onedesk/sources/new-public-message-created/new-public-message-created.mjs b/components/onedesk/sources/new-public-message-created/new-public-message-created.mjs deleted file mode 100644 index 81354fe6d4088..0000000000000 --- a/components/onedesk/sources/new-public-message-created/new-public-message-created.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import common from "../common/common.mjs"; - -export default { - ...common, - key: "onedesk-new-public-message-created", - name: "New Public Message Created", - description: "Emit new event when a new public message is created. [See the docs](https://www.onedesk.com/developers/#_get_item_updates)", - version: "0.0.1", - type: "source", - dedupe: "unique", - methods: { - ...common.methods, - async getUpdates(applicationId) { - const { data } = await this.onedesk.getItemUpdates({ - data: { - applicationId, - itemTypes: [ - "ConversationMessageActivity", - ], - operations: [ - "CREATE", - ], - }, - }); - return data; - }, - generateMeta(item) { - return { - id: item.itemId, - summary: `New Message Activity ID ${item.itemId}`, - ts: Date.parse(item.collectedTimestamp), - }; - }, - }, -}; diff --git a/components/onedesk/sources/new-timesheet-created/new-timesheet-created.mjs b/components/onedesk/sources/new-timesheet-created/new-timesheet-created.mjs index 78fc22b66d2f9..abdf902fa2819 100644 --- a/components/onedesk/sources/new-timesheet-created/new-timesheet-created.mjs +++ b/components/onedesk/sources/new-timesheet-created/new-timesheet-created.mjs @@ -1,35 +1,34 @@ -import common from "../common/common.mjs"; +import common from "../common/polling.mjs"; +import activityTypes from "../common/activity-types.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "onedesk-new-timesheet-created", name: "New Timesheet Created", - description: "Emit new event when a new timesheet is created. [See the docs](https://www.onedesk.com/developers/#_get_item_updates)", - version: "0.0.1", + description: "Emit new event when a new timesheet is created. [See the documentation](https://www.onedesk.com/dev/).", + version: "0.0.2", type: "source", dedupe: "unique", methods: { ...common.methods, - async getUpdates(applicationId) { - const { data } = await this.onedesk.getItemUpdates({ - data: { - applicationId, - itemTypes: [ - "ProjectTaskTimesheet", - ], - operations: [ - "CREATE", - ], + getActivityTypeProperties() { + return [ + { + property: "types", + operation: "EQ", + value: activityTypes.ACTUAL_WORK_UPDATED, }, - }); - return data; + ]; }, - generateMeta(item) { + generateMeta(resource) { + const ts = Date.parse(resource.timestamp); return { - id: item.itemId, - summary: `New Timesheet ID ${item.itemId}`, - ts: Date.parse(item.collectedTimestamp), + id: `${resource.itemExternalId}-${ts}`, + summary: `New Timesheet: ${resource.itemName || resource.itemExternalId}`, + ts, }; }, }, + sampleEmit, }; diff --git a/components/onedesk/sources/new-timesheet-created/test-event.mjs b/components/onedesk/sources/new-timesheet-created/test-event.mjs new file mode 100644 index 0000000000000..2664e0b4d32fc --- /dev/null +++ b/components/onedesk/sources/new-timesheet-created/test-event.mjs @@ -0,0 +1,11 @@ +export default { + "projectExternalId": "91d899c0-e3c4-46c3-9cc2-97b6ea2a708b", + "authorExternalId": "471cb72b-c56a-4413-8a51-abb2ffac0a2f", + "itemExternalId": "20bf477b-5aa4-45aa-b08d-e403e17c4c75", + "activityType": "changed the actual work", + "automationTypeName": null, + "timestamp": "2024-06-17T15:43:27.201+00:00", + "itemType": "Item", + "itemName": "Sample TICKET in Project", + "newValue": null +}; diff --git a/components/onedesk/sources/new-user-created/new-user-created.mjs b/components/onedesk/sources/new-user-created/new-user-created.mjs index ef3464de0618d..e4b0a4e692e9d 100644 --- a/components/onedesk/sources/new-user-created/new-user-created.mjs +++ b/components/onedesk/sources/new-user-created/new-user-created.mjs @@ -1,35 +1,33 @@ -import common from "../common/common.mjs"; +import common from "../common/polling.mjs"; +import activityTypes from "../common/activity-types.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "onedesk-new-user-created", name: "New User Created", - description: "Emit new event when a new user is created. [See the docs](https://www.onedesk.com/developers/#_get_item_updates)", - version: "0.0.1", + description: "Emit new event when a new user is created. [See the documentation](https://www.onedesk.com/dev/).", + version: "0.0.2", type: "source", dedupe: "unique", methods: { ...common.methods, - async getUpdates(applicationId) { - const { data } = await this.onedesk.getItemUpdates({ - data: { - applicationId, - itemTypes: [ - "User", - ], - operations: [ - "CREATE", - ], + getActivityTypeProperties() { + return [ + { + property: "types", + operation: "EQ", + value: activityTypes.CREATED_USER, }, - }); - return data; + ]; }, - generateMeta(item) { + generateMeta(resource) { return { - id: item.itemId, - summary: item.itemName, - ts: Date.parse(item.collectedTimestamp), + id: resource.itemExternalId, + summary: `New User Created: ${resource.itemName}`, + ts: Date.parse(resource.timestamp), }; }, }, + sampleEmit, }; diff --git a/components/onedesk/sources/new-user-created/test-event.mjs b/components/onedesk/sources/new-user-created/test-event.mjs new file mode 100644 index 0000000000000..f5f69421ba1a6 --- /dev/null +++ b/components/onedesk/sources/new-user-created/test-event.mjs @@ -0,0 +1,11 @@ +export default { + "projectExternalId": null, + "authorExternalId": "d5ef5e78-a82b-477f-be9c-a17b04283648", + "itemExternalId": "0fd310cb-2eda-4a22-899a-2830990fabfc", + "activityType": "created user", + "automationTypeName": null, + "timestamp": "2024-06-12T20:45:19.623+00:00", + "itemType": "User", + "itemName": "Test 10 Test 10", + "newValue": null +}; diff --git a/components/onelogin/onelogin.app.mjs b/components/onelogin/onelogin.app.mjs new file mode 100644 index 0000000000000..bc625b9d09792 --- /dev/null +++ b/components/onelogin/onelogin.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "onelogin", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/onelogin/package.json b/components/onelogin/package.json new file mode 100644 index 0000000000000..0945197d47fa2 --- /dev/null +++ b/components/onelogin/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/onelogin", + "version": "0.0.1", + "description": "Pipedream OneLogin Components", + "main": "onelogin.app.mjs", + "keywords": [ + "pipedream", + "onelogin" + ], + "homepage": "https://pipedream.com/apps/onelogin", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/onenote/README.md b/components/onenote/README.md new file mode 100644 index 0000000000000..61905933df295 --- /dev/null +++ b/components/onenote/README.md @@ -0,0 +1,11 @@ +# Overview + +The Microsoft OneNote API lets you create, read, and update notes and notebooks. Integrating this API with Pipedream opens doors to automate note-taking, content management, and data syncing tasks. With Pipedream's serverless platform, you can set up workflows that trigger actions in OneNote or respond to events in OneNote, streamlining your digital note organization and enhancing productivity with automated processes. + +# Example Use Cases + +- **Automated Meeting Minutes**: Sync meeting notes to a OneNote notebook after a calendar event. After a Google Calendar event ends, a Pipedream workflow extracts the minutes from a document and creates a new note in a specified OneNote section. + +- **Task Management Sync**: Create OneNote tasks from new Trello cards. Whenever a new card is added to a Trello board, a Pipedream workflow triggers, adding a corresponding task in OneNote, ensuring tasks are noted across platforms. + +- **Content Backup**: Back up new OneNote pages to Dropbox. Each time a new page is added to a OneNote section, Pipedream detects the change and automatically saves a copy of the page to a designated Dropbox folder, providing an extra layer of redundancy. diff --git a/components/onepage/package.json b/components/onepage/package.json index 1169791ffba88..def2f9f7e9e10 100644 --- a/components/onepage/package.json +++ b/components/onepage/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/onepagecrm/README.md b/components/onepagecrm/README.md new file mode 100644 index 0000000000000..760d1a5740608 --- /dev/null +++ b/components/onepagecrm/README.md @@ -0,0 +1,11 @@ +# Overview + +The OnePageCRM API enables interaction with your OnePageCRM account, automating your contact and sales management. With it, you can create, update, and delete contacts, deals, and notes, or trigger actions based on sales activities. When combined with Pipedream's serverless platform, this API allows you to connect OnePageCRM with hundreds of other apps to streamline your sales process, enhance lead generation, and improve follow-ups. + +# Example Use Cases + +- **Sync New Contacts to a Google Sheet**: Automatically add new contacts created in OnePageCRM to a Google Sheet, allowing for easy sharing and analysis. Each time a contact is added in OnePageCRM, a Pipedream workflow triggers and appends their details to a designated Google Sheet. + +- **Create Contacts from Inbound Emails**: Convert incoming emails to OnePageCRM contact entries. Using Pipedream's Email Trigger, whenever you receive a new email to a specific address, parse the sender's details and create a new contact in OnePageCRM, ensuring you never lose a lead. + +- **Post Slack Notifications for Won Deals**: Keep your team instantly informed when deals are won. Set up a Pipedream workflow that monitors deal statuses in OnePageCRM; when a deal is marked as won, trigger a notification to a specified Slack channel celebrating the success with your team. diff --git a/components/ones2u/README.md b/components/ones2u/README.md new file mode 100644 index 0000000000000..9e82b986f56a6 --- /dev/null +++ b/components/ones2u/README.md @@ -0,0 +1,11 @@ +# Overview + +The 1S2U API provides SMS messaging functionality, enabling you to send texts globally from your Pipedream workflows. With this integration, you can automate notifications, alerts, and communication with users or staff, streamlining your outreach efforts. Pipedream’s serverless platform allows you to create complex workflows, triggering SMS sends based on events from various apps or custom logic. + +# Example Use Cases + +- **Order Confirmation Texts**: When an order is placed through your e-commerce platform, use the 1S2U API in Pipedream to send a confirmation SMS to the customer, ensuring they receive immediate order details and peace of mind. + +- **Appointment Reminders**: Connect your scheduling app to Pipedream and set up a workflow that automatically sends reminder SMS messages to clients a day before their appointment, reducing no-shows and improving customer experience. + +- **Two-Factor Authentication (2FA)**: For added security, integrate 1S2U with your authentication flow in Pipedream to send a one-time password (OTP) via SMS when a user attempts to log in. diff --git a/components/onesaas/README.md b/components/onesaas/README.md index 6a412dbd53000..01452414769c0 100644 --- a/components/onesaas/README.md +++ b/components/onesaas/README.md @@ -1,24 +1,11 @@ # Overview -Using the 1SaaS API, you can build a wide range of cloud applications for -automating various types of activities. The API can be used for developing -programs and applications to increase efficiency and productivity in areas such -as accounting, customer relationship management, stock management, invoice -management and more. +The 1SaaS API offers a unified interface to integrate with multiple SaaS applications, streamlining data exchange and automating workflows across various platforms. With Pipedream, developers can create serverless workflows that tap into 1SaaS's capabilities, such as syncing data between apps, triggering actions based on events, or managing cross-application processes without writing boilerplate code. This powerful combination enables quick deployment of complex automation scenarios, tailored to unique business needs. -The API is specifically designed to enable developers to easily integrate their -systems with the 1SaaS platform, allowing them to connect and communicate with -their customers as well as with other companies. The API has been developed -with an emphasis on flexibilty, scalability, and security, giving developers -the ability to build highly customized solutions for businesses of all sizes. +# Example Use Cases -Here are some examples of what you can build with the 1SaaS API: +- **Automated Multi-Channel Customer Support Ticketing**: Create a workflow where a new ticket submission in Zendesk triggers an automated notification in Slack. Use the 1SaaS API to log the ticket details in a Google Sheet, enabling a cross-functional team to track and handle customer support across platforms efficiently. -- Automated receipts and payments applications. -- Invoice management applications. -- Payment processing applications. -- Stock management applications. -- Customer relationship management applications. -- Accounting applications. -- Integration services. -- CRM applications. +- **E-commerce Order Processing and Inventory Management**: Set up a workflow that detects new orders from Shopify using the 1SaaS API. Automatically create an invoice in QuickBooks, update inventory levels, and send a personalized confirmation email to the customer via SendGrid, streamlining the order-to-fulfillment process. + +- **Marketing Campaign Lead Synchronization**: Build a workflow where new leads captured via a Facebook Ads campaign are instantly added to a Mailchimp list using the 1SaaS API, and simultaneously logged into a CRM like Salesforce. This ensures real-time lead nurturing and accurate tracking of campaign performance. diff --git a/components/onesec_mail/README.md b/components/onesec_mail/README.md new file mode 100644 index 0000000000000..09287f318ab16 --- /dev/null +++ b/components/onesec_mail/README.md @@ -0,0 +1,11 @@ +# Overview + +The 1sec Mail API lets you interact with temporary email accounts, providing functionalities like creating new emails, fetching messages, and downloading attachments. It's a handy tool for testing email-related functionalities without spamming your inbox or when privacy is a concern. Within Pipedream, you can leverage this API to build automated workflows that can handle email verification, temporary account creation for testing, and more, all without cluttering your main email account. + +# Example Use Cases + +- **Email Verification Automation**: Automatically create a temporary email address with 1sec Mail, use it to sign up for a service, and then use Pipedream to check the inbox for a verification email and extract the verification link. + +- **Attachment Download & Processing**: Set up a Pipedream workflow where you create a 1sec Mail address, receive emails with attachments, and have Pipedream download and send the attachments to another app like Dropbox for storage or to a service like VirusTotal for scanning. + +- **Privacy-focused Newsletter Signup**: Use a 1sec Mail address to sign up for newsletters or offers, then use Pipedream to route any useful content to your permanent email or to apps like Slack for easy reading, keeping your real email address private and your inbox clean. diff --git a/components/onesignal_rest_api/README.md b/components/onesignal_rest_api/README.md index c4ac4189e09f7..b621d77f23516 100644 --- a/components/onesignal_rest_api/README.md +++ b/components/onesignal_rest_api/README.md @@ -1,23 +1,11 @@ # Overview -With the OneSignal (REST API) API, you can create powerful tools to connect -with your users and build an engaged audience. The API enables you to create, -read, update, and delete push notifications, set up segmentation, and deliver -messages to players. +OneSignal's REST API enables developers to automate the delivery of push notifications, manage users and segments, and gather analytics to refine communication strategies. By integrating OneSignal with Pipedream, you can orchestrate complex workflows that react to events or schedules, synchronize user data across platforms, and personalize user engagement with cross-channel marketing tools. -The API also enables you to: +# Example Use Cases -- Monitor and analyze performance with analytics dashboards and reporting tools -- Receive updates on user engagement with real-time events -- Automate message sending using scheduled notifications -- Personalize messages using data gathered on each user +- **User Activity-Based Notifications**: Trigger personalized push notifications via OneSignal when a user performs a specific action in your app, such as completing a purchase. Use Pipedream to listen for the event, process the user's activity, and call the OneSignal API to send a tailored message. -These are just some of the resources available to you when you use OneSignal -API: +- **Scheduled Digest Notifications**: Compile and send daily or weekly digest notifications to users by collecting updates or content from various sources (like RSS feeds, website updates, or database records). Pipedream can automate the aggregation of content and scheduling of notification dispatches with OneSignal at predefined intervals. -- Create targeted notifications for user-specific behaviors -- Trigger personalized messages via segmentation -- Maintain continuity with automated, scheduled messages -- Develop group messages and alert campaigns -- Analyze user engagement with reporting tools -- Mark individual push notifications as delivered or failed +- **Multi-Channel User Engagement**: Enhance user retention by connecting OneSignal with email marketing services (e.g., Mailchimp) using Pipedream. Detect inactive users via OneSignal analytics, then automatically enroll them in re-engagement email campaigns, harmonizing push notifications with email outreach. diff --git a/components/onesignal_user_auth/README.md b/components/onesignal_user_auth/README.md index 7a9b8f8fef20e..f9a2c459c6662 100644 --- a/components/onesignal_user_auth/README.md +++ b/components/onesignal_user_auth/README.md @@ -1,26 +1,11 @@ # Overview -OneSignal is the premier user identity, authentication, and engagement platform -for mobile and web applications. The OneSignal API lets developers build a -range of applications that provide user authentication, data storage and -improved customer engagement. With its extensive feature set, the OneSignal API -can be used to create amazing web and mobile products that drive user -engagement. Here are just a few things you can build using the OneSignal (User -Auth) API: +The OneSignal (User Auth) API on Pipedream empowers developers to streamline communication by automating notifications and messaging. With it, you can handle user data, send targeted notifications to specific user segments, and track message engagement. By leveraging Pipedream's serverless platform, you can create intricate workflows to interact with the API, reacting to events across various apps or triggering actions based on user behavior. -- Login and signup forms: Create seamless and secure signup and login forms - with the User Auth API. -- User profiles: Create custom user profiles that securely store user - information and profile data. -- 2FA authentication: Enable two-factor authentication for added security with - the OneSignal's Push, SMS and Email authentication integrations. -- Push notifications: Leverage the OneSignal Push Notification Service to send - personalized notifications and messages to your users. -- Feature flippin: Easily and securely allow users to flip features on and off - with the Feature Flippin integrations. -- Loyalty programs: Create custom loyalty programs for your app users with the - Loyalty Program Builder. -- Messaging and chatbots: Create powerful messaging and chatbot experiences - with the OneSignal Messaging Platform. -- Tracking and analytics: Track and measure user engagement with the OneSignal - Analytics suite. +# Example Use Cases + +- **User Onboarding Notifications**: Automate the sending of personalized welcome emails or push notifications to new users when they sign up for your service. Connect OneSignal (User Auth) to a database like Firebase to monitor new user sign-ups and trigger welcome messages. + +- **Behavioral Triggered Messaging**: Send notifications or in-app messages based on user activities. For example, if a user abandons a shopping cart, set up a workflow where OneSignal (User Auth) communicates with an e-commerce platform like Shopify to trigger a reminder or discount offer to encourage completion of the purchase. + +- **Real-time User Engagement Analysis**: Integrate OneSignal (User Auth) with analytics tools like Google Analytics to monitor the effectiveness of sent notifications. Create a workflow that captures notification interaction data and feeds it into an analytics pipeline, enabling you to make data-driven decisions on notification strategies. diff --git a/components/onesignal_user_auth/package.json b/components/onesignal_user_auth/package.json new file mode 100644 index 0000000000000..bd4c8bc2af503 --- /dev/null +++ b/components/onesignal_user_auth/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/onesignal_user_auth", + "version": "0.6.0", + "description": "Pipedream onesignal_user_auth Components", + "main": "onesignal_user_auth.app.mjs", + "keywords": [ + "pipedream", + "onesignal_user_auth" + ], + "homepage": "https://pipedream.com/apps/onesignal_user_auth", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/onethread/README.md b/components/onethread/README.md new file mode 100644 index 0000000000000..d14358cbe5378 --- /dev/null +++ b/components/onethread/README.md @@ -0,0 +1,11 @@ +# Overview + +The Onethread API allows you to tap into a project management platform designed to streamline teamwork and improve project execution. By integrating the Onethread API with Pipedream, you can automate repetitive tasks, sync data with other tools, and create custom notifications and reports. This enables you to craft workflows that connect Onethread data with hundreds of other apps, leading to a more efficient project management environment. + +# Example Use Cases + +- **Task Automation:** Trigger a Pipedream workflow with new tasks created in Onethread. Automate the creation of related tasks in other systems like Trello or Asana, ensuring project alignment across multiple platforms. + +- **Custom Notifications:** Send custom alerts or summary emails using the Onethread API whenever a project hits a milestone or a task's status changes. Integrate with services like Slack or Gmail to keep your team informed in real-time. + +- **Report Generation:** Compile a weekly report of project progress and task completion rates using Onethread data. Utilize Pipedream's scheduled triggers to automatically fetch this data and send it to Google Sheets or another reporting tool of your choice. diff --git a/components/onethread/onethread.app.mjs b/components/onethread/onethread.app.mjs new file mode 100644 index 0000000000000..d46bcf5b41605 --- /dev/null +++ b/components/onethread/onethread.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "onethread", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/onethread/package.json b/components/onethread/package.json new file mode 100644 index 0000000000000..ab95ff39d7f46 --- /dev/null +++ b/components/onethread/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/onethread", + "version": "0.0.1", + "description": "Pipedream Onethread Components", + "main": "onethread.app.mjs", + "keywords": [ + "pipedream", + "onethread" + ], + "homepage": "https://pipedream.com/apps/onethread", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/oneuptime/README.md b/components/oneuptime/README.md new file mode 100644 index 0000000000000..31d55c6fc4596 --- /dev/null +++ b/components/oneuptime/README.md @@ -0,0 +1,11 @@ +# Overview + +OneUptime's API provides the tools to monitor applications and servers, track incidents, and manage uptime from a developer's point of view. With Pipedream, you can leverage this API to create automated workflows that respond to uptime events, synchronize incident reports with other tools, and aggregate performance data for analysis. It's about connecting OneUptime's monitoring prowess with your existing infrastructure to automate responses, notifications, and reporting. + +# Example Use Cases + +- **Incident Response Notifications**: Trigger a workflow on Pipedream when OneUptime detects an incident. Automatically notify team members via Slack, send an SMS via Twilio, or create an incident ticket in Jira. This ensures quick attention and resolution to issues affecting your services. + +- **Performance Metrics Dashboard Update**: Collect and send performance metrics from OneUptime to a Google Sheet or a data visualization tool like Grafana whenever a monitoring check is completed. Use this data to maintain an up-to-date dashboard for real-time performance analysis, helping you make informed decisions. + +- **Automated Status Page Updates**: When OneUptime identifies an incident, use a Pipedream workflow to update the company status page hosted on a platform like Statuspage.io. Include the incident details and expected resolution times to keep users informed without manual intervention. diff --git a/components/onfleet/README.md b/components/onfleet/README.md index 56c12e0dc0ce5..5c368a0cbc0c8 100644 --- a/components/onfleet/README.md +++ b/components/onfleet/README.md @@ -1,18 +1,11 @@ # Overview -Tired of manually managing your local delivery tasks and execution? Onfleet API -provides a way to automate and streamline your local delivery operations. With -Onfleet API, you can build powerful applications that can dramatically increase -the efficiency of your delivery business. +The Onfleet API integrates with Pipedream to power logistics and delivery operations, offering real-time tracking, optimized routes, automatic dispatch, analytics, and communication with drivers. With Pipedream's serverless platform, one can automate complex workflows involving Onfleet and other apps, triggering actions based on events, scheduling tasks, or processing data to enhance delivery services. -Here are some examples of what you can build with Onfleet API: +# Example Use Cases -- Automated routing for fleets of drivers -- Automated billing and invoicing -- Integration with existing business software -- Assets and package tracking -- Multi-lingual support -- Analyzing delivery data to learn and optimize routes -- Real-time insights into delivery performance -- Automated after-delivery surveys -- Scheduling services for special deliveries and pickups +- **Automated Delivery Alerts**: When a delivery is marked as completed in Onfleet, trigger a workflow on Pipedream that sends a customizable confirmation email to the customer through SendGrid, thanking them for their business and encouraging feedback. + +- **Dynamic Route Optimization**: As new orders come in through an e-commerce platform like Shopify, use Pipedream to automatically create delivery tasks in Onfleet, with the added capability to dynamically adjust routes as more orders are received to optimize for time and fuel efficiency. + +- **Customer Support Ticket Creation**: If a delivery encounters an issue, such as being marked as failed or delayed in Onfleet, trigger a Pipedream workflow that creates a support ticket in a tool like Zendesk, ensuring the customer service team promptly addresses and resolves the delivery issues. diff --git a/components/ongage/README.md b/components/ongage/README.md index e022bb9b77e3c..33633412735aa 100644 --- a/components/ongage/README.md +++ b/components/ongage/README.md @@ -1,27 +1,13 @@ # Overview -With the Ongage API, you can build powerful, data-driven marketing automation -tools that make complex campaigns easy to manage, saving both time and money. -Here are some examples of what you can build by leveraging the Ongage API: +The Ongage API provides a robust platform for email marketing campaign management, allowing for the creation, management, and analysis of email campaigns. With Pipedream's capabilities, you can automate tasks like syncing subscriber lists, triggering email campaigns based on specific events, and analyzing campaign performance directly within your workflow. -- Create dynamic segments of customers and prospects based on a variety of - criteria. -- Create and schedule email campaigns, automated messages, and SMS messages. -- Monitor the performance of campaigns and optimize ad impressions for more - effective results. -- Integrate with third-party services such as Salesforce, HubSpot, and Marketo - to easily store and track customer and prospect data for better marketing - decision-making. -- Automate marketing processes like onboarding and retention, providing a - seamless experience for customers and prospects. -- Track results from multiple campaigns and measure ROI using detailed - analytics and reporting. -- Build custom dashboards and reporting based on the data collected from - campaigns. -- Automate segmentation and targeting to ensure campaigns are always sent to - the right audience. -- Create automated triggered campaigns that respond in real-time to customer - and prospect behavior. -- Easily sync data between multiple platforms to keep customer and prospect - data up-to-date. -- And much more! +# Example Use Cases + +- **List Synchronization Workflow**: Synchronize contact lists between Ongage and a CRM like Salesforce. As contacts are added or updated in Salesforce, the workflow updates the corresponding list in Ongage, ensuring your email campaigns target the right audience without manual intervention. + +- **Behavior-Triggered Email Campaign**: Launch email campaigns in Ongage based on user behavior tracked by analytics tools such as Google Analytics. For example, if a user visits a specific product page multiple times but doesn't make a purchase, trigger a personalized discount offer via email through Ongage to encourage conversion. + +- **Campaign Performance Dashboard**: Aggregate email campaign performance data from Ongage into a BI tool like Google Data Studio. With each campaign sent, the workflow fetches open rates, click-through rates, and other relevant metrics, providing a real-time dashboard to analyze and optimize your email marketing strategy. + +Note: The link provided for the Ongage API appears to be incorrect and points to the invoiced.com website. If further information is required for Ongage, please provide the correct URL. diff --git a/components/online_live_support/README.md b/components/online_live_support/README.md index 060647a569afc..932422710bfda 100644 --- a/components/online_live_support/README.md +++ b/components/online_live_support/README.md @@ -1,23 +1,11 @@ # Overview -With the WhatsApp by Online Live Support API, you can easily and quickly -integrate WhatsApp as part of your business process. This API provides -optimized API for Whatsapp and allows your customers to communicate with you -through the world's most popular messaging app. This API not only ensures -customers can conveniently make contact but also increases your customer -service quality and efficiency. +The WhatsApp by Online Live Support API on Pipedream lets you automate interactions with WhatsApp, enabling you to send messages, create groups, and manage chats within a workflow. By leveraging this API with Pipedream's capabilities, you can craft event-driven automations that trigger actions in WhatsApp based on external events or conditions defined in other apps, such as receiving a customer support ticket, triggering a follow-up after a certain period, or coordinating team alerts. -Some examples of what you can build with this API include: +# Example Use Cases -- Chatbots: Leverage AI and automation to respond to customer inquiries quickly - and accurately. -- Automated Broadcast Messages: Send messages automatically to all of your - customers or a targeted segment of your contacts. -- Contact Routing: Route customer messages to the right specialized operators - or departments based on their inquiry. -- Automate Sales Funnells: Pre-define a chat path and trigger automated - messages with targeted content at the right time. -- Integrate WhatsApp with Your Business: Link WhatsApp to your CRM and other - systems to move customers through the entire sales funnel. -- Customize APIs: Customize the API to fit your needs, including creating - private custom extensions. +- **Customer Support Ticket Resolution**: When a new support ticket is created in Zendesk, use Pipedream to trigger an automated WhatsApp message to the customer, acknowledging the issue and providing an estimated time for resolution. This creates a prompt and personal line of communication. + +- **E-commerce Order Updates**: Combine WooCommerce events with WhatsApp messaging to inform customers of their order status. After an order is placed, Pipedream can send a confirmation message via WhatsApp. It can also notify customers when their order is shipped or delivered for a seamless shopping experience. + +- **Team Coordination Alerts**: In a Slack-driven team environment, use Pipedream to monitor for specific messages or keywords. Once detected, trigger a WhatsApp message to alert off-Slack team members. This is especially useful for urgent issues requiring immediate attention from various team members who might not be active on Slack. diff --git a/components/online_live_support/package.json b/components/online_live_support/package.json new file mode 100644 index 0000000000000..a79830e297dd9 --- /dev/null +++ b/components/online_live_support/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/online_live_support", + "version": "0.6.0", + "description": "Pipedream online_live_support Components", + "main": "online_live_support.app.mjs", + "keywords": [ + "pipedream", + "online_live_support" + ], + "homepage": "https://pipedream.com/apps/online_live_support", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/onlinecheckwriter/actions/create-check/create-check.mjs b/components/onlinecheckwriter/actions/create-check/create-check.mjs new file mode 100644 index 0000000000000..a2cbff182fcfa --- /dev/null +++ b/components/onlinecheckwriter/actions/create-check/create-check.mjs @@ -0,0 +1,134 @@ +import app from "../../onlinecheckwriter.app.mjs"; + +export default { + key: "onlinecheckwriter-create-check", + name: "Create Check", + description: "Creates a new check. [See the documentation](https://apiv3.onlinecheckwriter.com/#211cb6e4-bda7-46de-9e84-a5e8b2709206)", + version: "0.0.1", + type: "action", + props: { + app, + bankAccountId: { + propDefinition: [ + app, + "bankAccountId", + ], + }, + payeeId: { + propDefinition: [ + app, + "payeeId", + ], + }, + categoryId: { + propDefinition: [ + app, + "categoryId", + ], + }, + issueDate: { + propDefinition: [ + app, + "issueDate", + ], + }, + amount: { + propDefinition: [ + app, + "amount", + ], + }, + memo: { + propDefinition: [ + app, + "memo", + ], + }, + note: { + propDefinition: [ + app, + "note", + ], + }, + invoiceNumber: { + propDefinition: [ + app, + "invoiceNumber", + ], + }, + noSign: { + propDefinition: [ + app, + "noSign", + ], + }, + noAmount: { + propDefinition: [ + app, + "noAmount", + ], + }, + noDate: { + propDefinition: [ + app, + "noDate", + ], + }, + noPayee: { + propDefinition: [ + app, + "noPayee", + ], + }, + }, + methods: { + createChecks(args = {}) { + return this.app.post({ + path: "/checks", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createChecks, + bankAccountId, + payeeId, + categoryId, + issueDate, + amount, + memo, + note, + invoiceNumber, + noSign, + noAmount, + noDate, + noPayee, + } = this; + + const response = await createChecks({ + $, + data: { + checks: [ + { + bankAccountId, + payeeId, + categoryId, + issueDate, + amount, + memo, + note, + invoiceNumber, + noSign, + noAmount, + noDate, + noPayee, + }, + ], + }, + }); + + $.export("$summary", `Successfully created a new check with ID \`${response.data.checks[0].checkId}\`.`); + return response; + }, +}; diff --git a/components/onlinecheckwriter/actions/create-email-check/create-email-check.mjs b/components/onlinecheckwriter/actions/create-email-check/create-email-check.mjs new file mode 100644 index 0000000000000..b4d8fd366b83b --- /dev/null +++ b/components/onlinecheckwriter/actions/create-email-check/create-email-check.mjs @@ -0,0 +1,82 @@ +import app from "../../onlinecheckwriter.app.mjs"; + +export default { + key: "onlinecheckwriter-create-email-check", + name: "Create Email Check", + description: "Create an email check for a payee. [See the documentation](https://apiv3.onlinecheckwriter.com/#211cb6e4-bda7-46de-9e84-a5e8b2709206).", + version: "0.0.1", + type: "action", + props: { + app, + checkId: { + propDefinition: [ + app, + "checkId", + ], + }, + payeeEmail: { + optional: false, + propDefinition: [ + app, + "payeeEmail", + ], + }, + enableSmsInform: { + propDefinition: [ + app, + "enableSmsInform", + ], + }, + payeePhone: { + optional: false, + description: "Required if **Enable SMS Inform** is `true` and not exist any phone on associated payee,", + propDefinition: [ + app, + "payeePhone", + ], + }, + sendAttachment: { + type: "boolean", + label: "Send Attachment", + description: "This will send added attachments along with the check.", + optional: true, + }, + }, + methods: { + createEmailChecks(args = {}) { + return this.app.post({ + path: "/emailchecks", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createEmailChecks, + checkId, + payeeEmail, + enableSmsInform, + payeePhone, + sendAttachment, + } = this; + + const response = await createEmailChecks({ + $, + data: { + emailChecks: [ + { + checkId, + payeeEmail, + enableSmsInform, + payeePhone, + ...(sendAttachment !== undefined && { + sendAttachment: +sendAttachment, + }), + }, + ], + }, + }); + $.export("$summary", "Successfully created email checks."); + return response; + }, +}; diff --git a/components/onlinecheckwriter/actions/create-mail-check/create-mail-check.mjs b/components/onlinecheckwriter/actions/create-mail-check/create-mail-check.mjs new file mode 100644 index 0000000000000..18a56a3e17fc6 --- /dev/null +++ b/components/onlinecheckwriter/actions/create-mail-check/create-mail-check.mjs @@ -0,0 +1,145 @@ +import app from "../../onlinecheckwriter.app.mjs"; + +export default { + key: "onlinecheckwriter-create-mail-check", + name: "Create Mail Check", + description: "Creates a mail check. [See the documentation](https://apiv3.onlinecheckwriter.com/#f4562b65-70e8-4c4d-8444-8898e61ab7f0).", + version: "0.0.1", + type: "action", + props: { + app, + isShippingToCustomAddress: { + type: "boolean", + label: "Shipping To Custom Address?", + description: "Value is `true` if sending mail to custom address. Default `false`", + optional: true, + }, + customFromAddressId: { + description: "Must be a custom from address ID. Required if **Shipping To Custom Address?** is `true`.", + propDefinition: [ + app, + "customFromAddressId", + ], + }, + customToAddressId: { + description: "Must be a custom to address ID. Required if **Shipping To Custom Address?** is `true`.", + propDefinition: [ + app, + "customToAddressId", + ], + }, + customShippingTypeId: { + label: "Custom Shipping Type ID", + description: "Must be a valid custom shipping type ID. Required if **Shipping To Custom Address?** is `true`.", + propDefinition: [ + app, + "shippingTypeId", + ], + }, + checkId: { + propDefinition: [ + app, + "checkId", + ], + }, + shippingTypeId: { + optional: false, + description: "The shipping type ID of the check.", + propDefinition: [ + app, + "shippingTypeId", + ], + }, + paperTypeId: { + propDefinition: [ + app, + "paperTypeId", + ], + }, + informTypeId: { + propDefinition: [ + app, + "informTypeId", + ], + }, + enableSmsInform: { + propDefinition: [ + app, + "enableSmsInform", + ], + }, + enableEmailInform: { + type: "boolean", + label: "Enable Email Inform", + description: "Value is `true` if email inform is enabled. Default `false`.", + optional: true, + }, + payeeEmail: { + description: "Required if **Enable Email Inform** is `true` and there is no email on the associated payee.", + propDefinition: [ + app, + "payeeEmail", + ], + }, + payeePhone: { + description: "Required if **Enable SMS Inform** is `true` and there is no phone on the associated payee.", + propDefinition: [ + app, + "payeePhone", + ], + }, + }, + methods: { + createMailChecks(args = {}) { + return this.app.post({ + path: "/mailchecks", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createMailChecks, + isShippingToCustomAddress, + customFromAddressId, + customToAddressId, + customShippingTypeId, + checkId, + shippingTypeId, + paperTypeId, + informTypeId, + enableSmsInform, + enableEmailInform, + payeeEmail, + payeePhone, + } = this; + + const response = await createMailChecks({ + $, + data: { + isShippingToCustomAddress, + customFromAddressId, + customToAddressId, + customShippingTypeId, + mailChecks: [ + { + checkId, + shippingTypeId, + paperTypeId, + informTypeId, + ...(enableSmsInform !== undefined && { + enableSmsInform: +enableSmsInform, + }), + ...(enableEmailInform !== undefined && { + enableEmailInform: +enableEmailInform, + }), + payeeEmail, + payeePhone, + }, + ], + }, + }); + $.export("$summary", "Successfully created and mailed checks."); + return response; + }, +}; diff --git a/components/onlinecheckwriter/actions/create-payee/create-payee.mjs b/components/onlinecheckwriter/actions/create-payee/create-payee.mjs new file mode 100644 index 0000000000000..eed49f25a70a7 --- /dev/null +++ b/components/onlinecheckwriter/actions/create-payee/create-payee.mjs @@ -0,0 +1,117 @@ +import app from "../../onlinecheckwriter.app.mjs"; + +export default { + key: "onlinecheckwriter-create-payee", + name: "Create Payee", + description: "Registers a new payee within the system.[See the documentation](https://apiv3.onlinecheckwriter.com/#38a31300-bf13-4da1-ac97-dd81525b57b3)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + type: "string", + label: "Name", + description: "The name of the payee.", + }, + company: { + type: "string", + label: "Company", + description: "The company of the payee.", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "The email of the payee.", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The phone number of the payee.", + optional: true, + }, + address1: { + type: "string", + label: "Address 1", + description: "The first line of the address of the payee.", + optional: true, + }, + address2: { + type: "string", + label: "Address 2", + description: "The second line of the address of the payee.", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "The country of the payee.", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "The state of the payee.", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "The city of the payee.", + optional: true, + }, + zip: { + type: "string", + label: "Zip", + description: "The zip code of the payee.", + optional: true, + }, + }, + methods: { + createPayees(args = {}) { + return this.app.post({ + path: "/payees", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createPayees, + name, + company, + email, + phone, + address1, + address2, + country, + state, + city, + zip, + } = this; + + const response = await createPayees({ + $, + data: { + payees: [ + { + name, + company, + email, + phone, + address1, + address2, + country, + state, + city, + zip, + }, + ], + }, + }); + + $.export("$summary", `Successfully created payee with ID \`${response.data.payees[0].payeeId}\`.`); + return response; + }, +}; diff --git a/components/onlinecheckwriter/actions/mail-pdf-document/mail-pdf-document.mjs b/components/onlinecheckwriter/actions/mail-pdf-document/mail-pdf-document.mjs new file mode 100644 index 0000000000000..daa5cb15e5fff --- /dev/null +++ b/components/onlinecheckwriter/actions/mail-pdf-document/mail-pdf-document.mjs @@ -0,0 +1,206 @@ +import fs from "fs"; +import FormData from "form-data"; +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../onlinecheckwriter.app.mjs"; + +export default { + key: "onlinecheckwriter-mail-pdf-document", + name: "Mail PDF Document", + description: "Mails a PDF document to a destination. [See the documentation](https://apiv3.onlinecheckwriter.com/#878daf05-e36e-44a2-bce8-15f24d72f82e).", + version: "0.0.1", + type: "action", + props: { + app, + documentTitle: { + type: "string", + label: "Document Title", + description: "The title of the document.", + optional: true, + }, + filePath: { + type: "string", + label: "File Path", + description: "The path to the pdf file saved to the `/tmp` directory (e.g. `/tmp/example.pdf`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + }, + shippingTypeId: { + optional: false, + propDefinition: [ + app, + "shippingTypeId", + ], + }, + senderName: { + type: "string", + label: "Sender Name", + description: "The name of the sender.", + optional: true, + }, + senderCompany: { + type: "string", + label: "Sender Company", + description: "The company of the sender.", + optional: true, + }, + senderAddress1: { + type: "string", + label: "Sender Address 1", + description: "The first line of the sender's address.", + optional: true, + }, + senderAddress2: { + type: "string", + label: "Sender Address 2", + description: "The second line of the sender's address.", + optional: true, + }, + senderCity: { + type: "string", + label: "Sender City", + description: "The city of the sender.", + optional: true, + }, + senderState: { + type: "string", + label: "Sender State", + description: "The state of the sender.", + optional: true, + }, + senderZip: { + type: "string", + label: "Sender Zip", + description: "The zip code of the sender.", + optional: true, + }, + senderPhone: { + type: "string", + label: "Sender Phone", + description: "The phone number of the sender.", + optional: true, + }, + senderEmail: { + type: "string", + label: "Sender Email", + description: "The email address of the sender.", + optional: true, + }, + destinationName: { + type: "string", + label: "Destination Name", + description: "The name of the recipient.", + }, + destinationCompany: { + type: "string", + label: "Destination Company", + description: "The company of the recipient.", + optional: true, + }, + destinationAddress1: { + type: "string", + label: "Destination Address 1", + description: "The first line of the recipient's address.", + }, + destinationAddress2: { + type: "string", + label: "Destination Address 2", + description: "The second line of the recipient's address.", + optional: true, + }, + destinationCity: { + type: "string", + label: "Destination City", + description: "The city of the recipient.", + }, + destinationState: { + type: "string", + label: "Destination State", + description: "The state of the recipient. Use 2 characters Eg. `Tx` instead of `Texas` for example.", + }, + destinationZip: { + type: "string", + label: "Destination Zip", + description: "The zip code of the recipient.", + }, + destinationPhone: { + type: "string", + label: "Destination Phone", + description: "The phone number of the recipient.", + optional: true, + }, + destinationEmail: { + type: "string", + label: "Destination Email", + description: "The email address of the recipient.", + optional: true, + }, + }, + methods: { + mailPdfDocument(args = {}) { + return this.app.post({ + path: "/documentmailing/mail", + ...args, + }); + }, + }, + async run({ $ }) { + const { + mailPdfDocument, + documentTitle, + filePath, + shippingTypeId, + senderName, + senderCompany, + senderAddress1, + senderAddress2, + senderCity, + senderState, + senderZip, + senderPhone, + senderEmail, + destinationName, + destinationCompany, + destinationAddress1, + destinationAddress2, + destinationCity, + destinationState, + destinationZip, + destinationPhone, + destinationEmail, + } = this; + + if (!filePath?.startsWith("/tmp/")) { + throw new ConfigurationError("The file path must start with `/tmp/`."); + } + + const data = new FormData(); + const file = fs.createReadStream(filePath); + data.append("document_details[file]", file); + data.append("document_details[title]", documentTitle || ""); + data.append("shipping[shippingTypeId]", shippingTypeId || ""); + data.append("destination[name]", destinationName || ""); + data.append("destination[company]", destinationCompany || ""); + data.append("destination[address1]", destinationAddress1 || ""); + data.append("destination[address2]", destinationAddress2 || ""); + data.append("destination[city]", destinationCity || ""); + data.append("destination[state]", destinationState || ""); + data.append("destination[zip]", destinationZip || ""); + data.append("destination[phone]", destinationPhone || ""); + data.append("destination[email]", destinationEmail || ""); + data.append("from_address[name]", senderName || ""); + data.append("from_address[company]", senderCompany || ""); + data.append("from_address[address1]", senderAddress1 || ""); + data.append("from_address[address2]", senderAddress2 || ""); + data.append("from_address[city]", senderCity || ""); + data.append("from_address[state]", senderState || ""); + data.append("from_address[zip]", senderZip || ""); + data.append("from_address[phone]", senderPhone || ""); + data.append("from_address[email]", senderEmail || ""); + const response = await mailPdfDocument({ + $, + data, + headers: data.getHeaders(), + }); + + $.export("$summary", "Successfully generated and mailed PDF document"); + return response; + }, +}; diff --git a/components/onlinecheckwriter/onlinecheckwriter.app.mjs b/components/onlinecheckwriter/onlinecheckwriter.app.mjs new file mode 100644 index 0000000000000..c344ed01f5735 --- /dev/null +++ b/components/onlinecheckwriter/onlinecheckwriter.app.mjs @@ -0,0 +1,329 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "onlinecheckwriter", + propDefinitions: { + bankAccountId: { + type: "string", + label: "Bank Account ID", + description: "Must be a bank account ID.", + async options({ page }) { + const { data: { bankAccounts } } = await this.listBankAccounts({ + params: { + page, + }, + }); + return bankAccounts.map(({ + bankAccountId: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + payeeId: { + type: "string", + label: "Payee ID", + description: "Must be a payee ID.", + async options({ page }) { + const { data: { payees } } = await this.listPayees({ + params: { + page, + }, + }); + return payees.map(({ + payeeId: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + categoryId: { + type: "string", + label: "Category ID", + description: "Must be a category ID.", + optional: true, + async options({ page }) { + const { data: { categories } } = await this.listCategories({ + params: { + page, + }, + }); + return categories.map(({ + categoryId: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + issueDate: { + type: "string", + label: "Issue Date", + description: "By default it will be check creation date, If an **Issue Date** is passed, that will override any default. Eg. `2021-12-31`.", + optional: true, + }, + amount: { + type: "string", + label: "Amount", + description: "The money value of the check.", + }, + memo: { + type: "string", + label: "Memo", + description: "Max of 150 characters to be included on the memo line of the check.", + optional: true, + }, + note: { + type: "string", + label: "Note", + description: "An internal note that identifies this check. Must be no longer than 150 characters.", + optional: true, + }, + invoiceNumber: { + type: "string", + label: "Invoice Number", + description: "Any invoice number associated with this check", + optional: true, + }, + noSign: { + type: "boolean", + label: "No Sign", + description: "if `true`, the check will be created without signature. Default `false`.", + optional: true, + }, + noAmount: { + type: "boolean", + label: "No Amount", + description: "if `true`, the check will be created without amount. Default `false`", + optional: true, + }, + noDate: { + type: "boolean", + label: "No Date", + description: "if `true`, the check will be created without date. Default `false`", + optional: true, + }, + noPayee: { + type: "boolean", + label: "No Payee", + description: "if `true`, the check will be created without payee. Default `false`", + optional: true, + }, + customFromAddressId: { + type: "string", + label: "Custom From Address ID", + description: "Must be a custom from address ID.", + optional: true, + async options({ page }) { + const { data: { addresses } } = await this.listCustomFromAddresses({ + params: { + page, + }, + }); + return addresses.map(({ + customFromAddressId: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + customToAddressId: { + type: "string", + label: "Custom To Address ID", + description: "Must be a custom to address ID.", + optional: true, + async options({ page }) { + const { data: { addresses } } = await this.listCustomToAddresses({ + params: { + page, + }, + }); + return addresses.map(({ + customToAddressId: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + shippingTypeId: { + type: "string", + label: "Shipping Type ID", + description: "Must be a valid shipping type ID.", + optional: true, + options: [ + { + label: "First Class", + value: "1", + }, + { + label: "First Class with Tracking", + value: "3", + }, + { + label: "Priority Mail", + value: "4", + }, + { + label: "Express Mail", + value: "5", + }, + { + label: "Standard Overnight -Fedex-By 3pm the next business day", + value: "12", + }, + { + label: "Fedex Saturday Delivery", + value: "20", + }, + ], + }, + checkId: { + type: "string", + label: "Check ID", + description: "Must be a check ID.", + async options({ + page, params, + }) { + const { data: { checks } } = await this.listChecks({ + params: { + ...params, + page, + }, + }); + return checks.map(({ + checkId: value, payee: { + name, payeeId, + }, amount, + }) => ({ + label: `${name || payeeId} - $${amount}`, + value, + })); + }, + }, + paperTypeId: { + type: "string", + label: "Paper Type ID", + description: "Must be a valid paper type ID.", + options: [ + { + label: "Regular Check Paper", + value: "7", + }, + { + label: "Hollogram Check Paper", + value: "8", + }, + { + label: "Ultra Hollogram Check Paper", + value: "9", + }, + ], + }, + informTypeId: { + type: "string", + label: "Inform Type ID", + description: "Must be a valid inform type ID.", + optional: true, + options: [ + { + label: "Notify Receiver by Sms", + value: "10", + }, + ], + }, + enableSmsInform: { + type: "boolean", + label: "Enable SMS Inform", + description: "Value is `true` if SMS inform is enabled. Default `false`", + optional: true, + }, + payeeEmail: { + type: "string", + label: "Payee Email", + description: "The email address of the payee.", + optional: true, + }, + payeePhone: { + type: "string", + label: "Payee Phone", + description: "The phone number of the payee.", + optional: true, + }, + }, + methods: { + getUrl(path) { + const { environment } = this.$auth; + return `${environment}/api/v3${path}`; + }, + getHeaders(headers) { + return { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": `Bearer ${this.$auth.api_key}`, + ...headers, + }; + }, + async _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + const response = await axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + + if (response.success === false) { + throw new Error(JSON.stringify(response)); + } + + return response; + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + listBankAccounts(args = {}) { + return this._makeRequest({ + path: "/bankAccounts", + ...args, + }); + }, + listPayees(args = {}) { + return this._makeRequest({ + path: "/payees", + ...args, + }); + }, + listCategories(args = {}) { + return this._makeRequest({ + path: "/categories", + ...args, + }); + }, + listCustomFromAddresses(args = {}) { + return this._makeRequest({ + path: "/customFromAddresses", + ...args, + }); + }, + listCustomToAddresses(args = {}) { + return this._makeRequest({ + path: "/customToAddresses", + ...args, + }); + }, + listChecks(args = {}) { + return this._makeRequest({ + path: "/checks", + ...args, + }); + }, + }, +}; diff --git a/components/onlinecheckwriter/package.json b/components/onlinecheckwriter/package.json new file mode 100644 index 0000000000000..b3ad9e2566d5a --- /dev/null +++ b/components/onlinecheckwriter/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/onlinecheckwriter", + "version": "0.1.0", + "description": "Pipedream OnlineCheckWriter Components", + "main": "onlinecheckwriter.app.mjs", + "keywords": [ + "pipedream", + "onlinecheckwriter" + ], + "homepage": "https://pipedream.com/apps/onlinecheckwriter", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3", + "form-data": "^4.0.0" + } +} diff --git a/components/onlyoffice_docspace/actions/create-room/create-room.mjs b/components/onlyoffice_docspace/actions/create-room/create-room.mjs new file mode 100644 index 0000000000000..c516e4ee180ef --- /dev/null +++ b/components/onlyoffice_docspace/actions/create-room/create-room.mjs @@ -0,0 +1,77 @@ +import app from "../../onlyoffice_docspace.app.mjs"; + +export default { + key: "onlyoffice_docspace-create-room", + name: "Create Room", + description: "Creates a new room. [See the documentation](https://api.onlyoffice.com/docspace/method/files/post/api/2.0/files/rooms)", + version: "0.0.1", + type: "action", + props: { + app, + title: { + type: "string", + label: "Title", + description: "The name of the room to be created.", + }, + roomType: { + type: "integer", + label: "Room Type", + description: "The type of the room.", + options: [ + { + label: "Editing Room", + value: 2, + }, + { + label: "Custom Room", + value: 5, + }, + { + label: "Public Room", + value: 6, + }, + ], + }, + notify: { + type: "boolean", + label: "Notify", + description: "Whether to notify the user about the room creation.", + optional: true, + }, + sharingMessage: { + type: "string", + label: "Sharing Message", + description: "Message to send when notifying about the shared room", + optional: true, + }, + }, + methods: { + createRoom(args = {}) { + return this.app.post({ + path: "/files/rooms", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createRoom, + title, + roomType, + notify, + sharingMessage, + } = this; + + const response = await createRoom({ + $, + data: { + Title: title, + RoomType: roomType, + Notify: notify, + SharingMessage: sharingMessage, + }, + }); + $.export("$summary", `Successfully created room with ID \`${response.response.id}\`.`); + return response; + }, +}; diff --git a/components/onlyoffice_docspace/actions/invite-user/invite-user.mjs b/components/onlyoffice_docspace/actions/invite-user/invite-user.mjs new file mode 100644 index 0000000000000..2fed95ae39ff1 --- /dev/null +++ b/components/onlyoffice_docspace/actions/invite-user/invite-user.mjs @@ -0,0 +1,45 @@ +import app from "../../onlyoffice_docspace.app.mjs"; + +export default { + key: "onlyoffice_docspace-invite-user", + name: "Invite User", + description: "Invites a new user to the portal. [See the documentation](https://api.onlyoffice.com/docspace/method/people/post/api/2.0/people/invite)", + version: "0.0.1", + type: "action", + props: { + app, + email: { + type: "string", + label: "User Email", + description: "The email of the user to invite.", + }, + }, + methods: { + inviteUser(args = {}) { + return this.app.post({ + path: "/people/invite", + ...args, + }); + }, + }, + async run({ $ }) { + const { + inviteUser, + email, + } = this; + + const response = await inviteUser({ + $, + data: { + Invitations: [ + { + email, + }, + ], + }, + }); + + $.export("$summary", `Successfully invited user with email \`${email}\``); + return response; + }, +}; diff --git a/components/onlyoffice_docspace/actions/upload-file/upload-file.mjs b/components/onlyoffice_docspace/actions/upload-file/upload-file.mjs new file mode 100644 index 0000000000000..e1dd3512f196f --- /dev/null +++ b/components/onlyoffice_docspace/actions/upload-file/upload-file.mjs @@ -0,0 +1,55 @@ +import app from "../../onlyoffice_docspace.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "onlyoffice_docspace-upload-file", + name: "Upload File", + description: "Uploads a file to the specified room. [See the documentation](https://api.onlyoffice.com/docspace/method/files/post/api/2.0/files/%7bfolderid%7d/upload)", + version: "0.0.1", + type: "action", + props: { + app, + folderId: { + label: "Folder ID", + description: "The ID of the folder where you want the file to be uploaded.", + propDefinition: [ + app, + "file", + ], + }, + file: { + type: "string", + label: "File", + description: "File path of a file previously downloaded in Pipedream E.g. (`/tmp/my-file.txt`). [Download a file to the `/tmp` directory](https://pipedream.com/docs/code/nodejs/http-requests/#download-a-file-to-the-tmp-directory)", + }, + }, + methods: { + uploadFile({ + folderId, ...args + } = {}) { + return this.app.post({ + path: `/files/${folderId}/upload`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + uploadFile, + folderId, + file, + } = this; + + const response = await uploadFile({ + $, + folderId, + headers: constants.MULTIPART_FORM_DATA_HEADERS, + data: { + File: file, + }, + }); + + $.export("$summary", `Successfully uploaded file with ID \`${response.response.id}\`.`); + return response; + }, +}; diff --git a/components/onlyoffice_docspace/common/constants.mjs b/components/onlyoffice_docspace/common/constants.mjs new file mode 100644 index 0000000000000..6ace84c5f9959 --- /dev/null +++ b/components/onlyoffice_docspace/common/constants.mjs @@ -0,0 +1,49 @@ +const SUBDOMAIN_PLACEHOLDER = "{subdomain}"; +const VERSION_PATH = "/api/2.0"; +const BASE_URL = `https://${SUBDOMAIN_PLACEHOLDER}.onlyoffice.com${VERSION_PATH}`; + +const FILE_PROP_NAMES = [ + "File", +]; + +const CONTENT_TYPE_KEY_HEADER = "Content-Type"; +const MULTIPART_FORM_DATA_VALUE_HEADER = "multipart/form-data"; +const MULTIPART_FORM_DATA_HEADERS = { + [CONTENT_TYPE_KEY_HEADER]: MULTIPART_FORM_DATA_VALUE_HEADER, +}; + +const RESOURCE_NAME = { + FILES: "files", + FOLDERS: "folders", +}; + +const FILTER_TYPE = { + NONE: "None", + FILES_ONLY: "FilesOnly", + FOLDERS_ONLY: "FoldersOnly", + DOCUMENTS_ONLY: "DocumentsOnly", + PRESENTATIONS_ONLY: "PresentationsOnly", + SPREADSHEETS_ONLY: "SpreadsheetsOnly", + IMAGES_ONLY: "ImagesOnly", + BY_USER: "ByUser", + BY_DEPARTMENT: "ByDepartment", + ARCHIVE_ONLY: "ArchiveOnly", + BY_EXTENSION: "ByExtension", + MEDIA_ONLY: "MediaOnly", + EDITING_ROOMS: "EditingRooms", + CUSTOM_ROOMS: "CustomRooms", + OFORM_TEMPLATE_ONLY: "OFormTemplateOnly", + OFORM_ONLY: "OFormOnly", +}; + +export default { + SUBDOMAIN_PLACEHOLDER, + BASE_URL, + VERSION_PATH, + RESOURCE_NAME, + FILTER_TYPE, + FILE_PROP_NAMES, + MULTIPART_FORM_DATA_VALUE_HEADER, + MULTIPART_FORM_DATA_HEADERS, + CONTENT_TYPE_KEY_HEADER, +}; diff --git a/components/onlyoffice_docspace/common/utils.mjs b/components/onlyoffice_docspace/common/utils.mjs new file mode 100644 index 0000000000000..a4710a240ed9f --- /dev/null +++ b/components/onlyoffice_docspace/common/utils.mjs @@ -0,0 +1,40 @@ +import { createReadStream } from "fs"; +import FormData from "form-data"; +import constants from "./constants.mjs"; + +function buildFormData(formData, data, parentKey) { + if (data && typeof(data) === "object") { + Object.keys(data) + .forEach((key) => { + buildFormData(formData, data[key], parentKey && `${parentKey}[${key}]` || key); + }); + + } else if (data && constants.FILE_PROP_NAMES.some((prop) => parentKey.includes(prop))) { + formData.append(parentKey, createReadStream(data)); + + } else if (data) { + formData.append(parentKey, (data).toString()); + } +} + +function getFormData(data) { + try { + const formData = new FormData(); + buildFormData(formData, data); + return formData; + } catch (error) { + console.log("FormData Error", error); + throw error; + } +} + +function hasMultipartHeader(headers) { + return headers + && headers[constants.CONTENT_TYPE_KEY_HEADER]?. + includes(constants.MULTIPART_FORM_DATA_VALUE_HEADER); +} + +export default { + getFormData, + hasMultipartHeader, +}; diff --git a/components/onlyoffice_docspace/onlyoffice_docspace.app.mjs b/components/onlyoffice_docspace/onlyoffice_docspace.app.mjs new file mode 100644 index 0000000000000..a2380211af035 --- /dev/null +++ b/components/onlyoffice_docspace/onlyoffice_docspace.app.mjs @@ -0,0 +1,97 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; + +export default { + type: "app", + app: "onlyoffice_docspace", + propDefinitions: { + file: { + type: "string", + label: "File", + description: "The file or folder ID.", + async options({ + resourcesName = constants.RESOURCE_NAME.FOLDERS, + mapper = ({ + id, + title: label, + }) => ({ + value: String(id), + label, + }), + params = { + filterType: constants.FILTER_TYPE.FOLDERS_ONLY, + withsubfolders: true, + }, + }) { + const { response: { [resourcesName]: resources } } = + await this.listMyFilesAndFolders({ + params, + }); + return resources.map(mapper); + }, + }, + }, + methods: { + getUrl(path) { + const baseUrl = constants.BASE_URL + .replace(constants.SUBDOMAIN_PLACEHOLDER, this.$auth.subdomain); + return `${baseUrl}${path}`; + }, + getHeaders(headers) { + return { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": this.$auth.oauth_access_token, + ...headers, + }; + }, + getConfig({ + headers, data: preData, ...args + } = {}) { + const contentType = constants.CONTENT_TYPE_KEY_HEADER; + const hasMultipartHeader = utils.hasMultipartHeader(headers); + const data = hasMultipartHeader && utils.getFormData(preData) || preData; + const currentHeaders = this.getHeaders(headers); + + return { + headers: hasMultipartHeader + ? { + ...currentHeaders, + [contentType]: data.getHeaders()[contentType.toLowerCase()], + } + : currentHeaders, + data, + ...args, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + const config = this.getConfig({ + url: this.getUrl(path), + headers: this.getHeaders(headers), + ...args, + }); + return axios($, config); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + listMyFilesAndFolders(args = {}) { + return this._makeRequest({ + path: "/files/@my", + ...args, + }); + }, + searchUsersByExtendedFilter(args = {}) { + return this._makeRequest({ + path: "/people/simple/filter", + ...args, + }); + }, + }, +}; diff --git a/components/onlyoffice_docspace/package.json b/components/onlyoffice_docspace/package.json new file mode 100644 index 0000000000000..cab0756882afd --- /dev/null +++ b/components/onlyoffice_docspace/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/onlyoffice_docspace", + "version": "0.1.0", + "description": "Pipedream ONLYOFFICE DocSpace Components", + "main": "onlyoffice_docspace.app.mjs", + "keywords": [ + "pipedream", + "onlyoffice_docspace" + ], + "homepage": "https://pipedream.com/apps/onlyoffice_docspace", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0", + "form-data": "^4.0.0" + } +} diff --git a/components/onlyoffice_docspace/sources/common/polling.mjs b/components/onlyoffice_docspace/sources/common/polling.mjs new file mode 100644 index 0000000000000..916900823eac9 --- /dev/null +++ b/components/onlyoffice_docspace/sources/common/polling.mjs @@ -0,0 +1,72 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../onlyoffice_docspace.app.mjs"; + +export default { + props: { + app, + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + sortFn() { + return; + }, + getResourcesName() { + return; + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + async processResources(resources) { + const { + sortFn, + processResource, + } = this; + + Array.from(resources) + .sort(sortFn) + .forEach(processResource); + }, + }, + async run() { + const { + getResourcesFn, + getResourcesFnArgs, + getResourcesName, + processResources, + } = this; + + const resourcesName = getResourcesName(); + const resourcesFn = getResourcesFn(); + const metadata = await resourcesFn(getResourcesFnArgs()); + + let resources; + + if (resourcesName) { + ({ response: { [resourcesName]: resources } } = metadata); + } else { + ({ response: resources } = metadata); + } + + processResources(resources); + }, +}; diff --git a/components/onlyoffice_docspace/sources/new-file/new-file.mjs b/components/onlyoffice_docspace/sources/new-file/new-file.mjs new file mode 100644 index 0000000000000..66deec4dc805a --- /dev/null +++ b/components/onlyoffice_docspace/sources/new-file/new-file.mjs @@ -0,0 +1,40 @@ +import constants from "../../common/constants.mjs"; +import common from "../common/polling.mjs"; + +export default { + ...common, + key: "onlyoffice_docspace-new-file", + name: "New File Created", + description: "Emit new event when a new file is created. [See the documentation](https://api.onlyoffice.com/docspace/method/files/get/api/2.0/files/%40root).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + sortFn(a, b) { + return new Date(b.created) - new Date(a.created); + }, + getResourcesName() { + return constants.RESOURCE_NAME.FILES; + }, + getResourcesFn() { + return this.app.listMyFilesAndFolders; + }, + getResourcesFnArgs() { + return { + debug: true, + params: { + filterType: constants.FILTER_TYPE.FILES_ONLY, + withsubfolders: true, + }, + }; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New File: ${resource.title}`, + ts: Date.parse(resource.created), + }; + }, + }, +}; diff --git a/components/onlyoffice_docspace/sources/new-folder/new-folder.mjs b/components/onlyoffice_docspace/sources/new-folder/new-folder.mjs new file mode 100644 index 0000000000000..d03ab41d05fd4 --- /dev/null +++ b/components/onlyoffice_docspace/sources/new-folder/new-folder.mjs @@ -0,0 +1,40 @@ +import constants from "../../common/constants.mjs"; +import common from "../common/polling.mjs"; + +export default { + ...common, + key: "onlyoffice_docspace-new-folder", + name: "New Folder Created", + description: "Emit new event when a new folder is createdr. [See the documentation](https://api.onlyoffice.com/docspace/method/files/get/api/2.0/files/%40root).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + sortFn(a, b) { + return new Date(b.created) - new Date(a.created); + }, + getResourcesName() { + return constants.RESOURCE_NAME.FOLDERS; + }, + getResourcesFn() { + return this.app.listMyFilesAndFolders; + }, + getResourcesFnArgs() { + return { + debug: true, + params: { + filterType: constants.FILTER_TYPE.FOLDERS_ONLY, + withsubfolders: true, + }, + }; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Folder: ${resource.title}`, + ts: Date.parse(resource.created), + }; + }, + }, +}; diff --git a/components/onlyoffice_docspace/sources/new-user-added/new-user-added.mjs b/components/onlyoffice_docspace/sources/new-user-added/new-user-added.mjs new file mode 100644 index 0000000000000..38bb100b994fa --- /dev/null +++ b/components/onlyoffice_docspace/sources/new-user-added/new-user-added.mjs @@ -0,0 +1,32 @@ +import common from "../common/polling.mjs"; + +export default { + ...common, + key: "onlyoffice_docspace-new-user-added", + name: "New User Added", + description: "Emit new event when a new user is added. [See the documentation](https://api.onlyoffice.com/docspace/method/people/get/api/2.0/people/simple/filter).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourcesFn() { + return this.app.searchUsersByExtendedFilter; + }, + getResourcesFnArgs() { + return { + debug: true, + params: { + activationStatus: "Activated", + }, + }; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New User: ${resource.displayName}`, + ts: Date.now(), + }; + }, + }, +}; diff --git a/components/onstrategy/README.md b/components/onstrategy/README.md new file mode 100644 index 0000000000000..4bd58af8dcaf7 --- /dev/null +++ b/components/onstrategy/README.md @@ -0,0 +1,11 @@ +# Overview + +The OnStrategy API lets you automate strategic planning and tracking within your organization. You can create, update, and retrieve data on goals, actions, and progress, making it simpler to integrate strategic management into daily workflows. Pipedream's serverless platform eases the process by allowing you to construct workflows that link OnStrategy with other apps, trigger actions based on events, and handle data programmatically without managing servers. + +# Example Use Cases + +- **Automated Strategy Reporting**: Build a workflow that fetches strategic goal data from OnStrategy and sends a formatted report to Slack or email on a weekly basis, ensuring team members are always informed about progress and updates. + +- **Strategy Progress Tracker**: Create a Pipedream workflow that listens for updates in project management tools like Asana or Trello. Whenever a relevant task is completed, it triggers an update in OnStrategy to reflect the progress made towards a strategic goal. + +- **Strategic Decision Making**: Use Pipedream to set up a workflow where key performance indicators (KPIs) from various sources like Google Analytics or Salesforce are collected and sent to OnStrategy, enabling data-driven decision making based on real-time analytics. diff --git a/components/onstrategy/onstrategy.app.mjs b/components/onstrategy/onstrategy.app.mjs index 40e8d79a756c1..ff3ac1c579d53 100644 --- a/components/onstrategy/onstrategy.app.mjs +++ b/components/onstrategy/onstrategy.app.mjs @@ -1,11 +1,33 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "onstrategy", - propDefinitions: {}, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.onstrategyhq.com/api"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + params: { + ...params, + key: `${this.$auth.api_key}`, + }, + }); + }, + listGoals(opts = {}) { + return this._makeRequest({ + path: "/goals.json", + ...opts, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/onstrategy/package.json b/components/onstrategy/package.json index 0336cdc6d2fdb..e7889b742c389 100644 --- a/components/onstrategy/package.json +++ b/components/onstrategy/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/onstrategy", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream OnStrategy Components", "main": "onstrategy.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" } -} \ No newline at end of file +} diff --git a/components/onstrategy/sources/new-goal-added/new-goal-added.mjs b/components/onstrategy/sources/new-goal-added/new-goal-added.mjs new file mode 100644 index 0000000000000..fab435e1fa55a --- /dev/null +++ b/components/onstrategy/sources/new-goal-added/new-goal-added.mjs @@ -0,0 +1,62 @@ +import onstrategy from "../../onstrategy.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "onstrategy-new-goal-added", + name: "New Goal Added", + description: "Emit new event when a new goal is created within the OnStrategy app. [See the documentation](https://developer.onstrategyhq.com/)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + onstrategy, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + generateMeta(goal) { + return { + id: goal.id, + summary: `New Goal: ${goal.item}`, + ts: Date.now(), + }; + }, + async processEvent(limit) { + const lastId = this._getLastId(); + let maxId = lastId; + const { data } = await this.onstrategy.listGoals(); + const goals = limit + ? data.slice(-1 * limit) + : data; + for (const goal of goals) { + if (goal.id > lastId) { + maxId = Math.max(goal.id, maxId); + const meta = this.generateMeta(goal); + this.$emit(goal, meta); + } + } + this._setLastId(maxId); + }, + }, + async run() { + await this.processEvent(); + }, + sampleEmit, +}; diff --git a/components/onstrategy/sources/new-goal-added/test-event.mjs b/components/onstrategy/sources/new-goal-added/test-event.mjs new file mode 100644 index 0000000000000..7fa694f713b2e --- /dev/null +++ b/components/onstrategy/sources/new-goal-added/test-event.mjs @@ -0,0 +1,33 @@ +export default { + "id": "921840", + "item": "More customers", + "description": null, + "number": "1.4", + "level": "1", + "when_updated": "0000-00-00 00:00:00", + "can_edit": true, + "can_add": true, + "start_date": "2024-01-01", + "end_date": "2024-12-31", + "measure": "Percent Complete", + "start_value": "0.00", + "target": { + "fiscal_year": "2024", + "target": "100.00" + }, + "is_key": "0", + "tracking_frequency": "Monthly", + "target_direction": "Up", + "target_type": "Percent", + "function": "Current", + "priority": "", + "status_icon": "not_started", + "parent_id": "921820", + "comments": "", + "owner": [], + "latest_comment": [], + "all_comments": [], + "rollup_type": "0", + "auto_status": "1", + "ytd": null +} \ No newline at end of file diff --git a/components/ontraport/README.md b/components/ontraport/README.md index 74c1269753ef1..a3fb64b4ad331 100644 --- a/components/ontraport/README.md +++ b/components/ontraport/README.md @@ -1,27 +1,11 @@ # Overview -The Ontraport API enables developers to create powerful applications by -integrating Ontraport’s suite of CRM and marketing automation tools. It allows -developers to access information and control most aspects of the Ontraport -platform, such as contacts, campaigns, products, orders, and more. With the -power of the Ontraport API, developers can create applications that can: +Ontraport's API offers robust capabilities for automating business processes, managing customer relationships, and streamlining marketing efforts. Use it to create dynamic, serverless workflows on Pipedream that react to events in real-time, sync data between services, or enhance customer interactions. With this API, you can automate tasks such as updating contact information, tracking customer behaviors, and sending targeted communications, all customizable to your specific business needs. -- Automate contact and customer segmentation -- Create email marketing campaigns and send automated emails -- Generate custom reports -- Update customer information -- Create and manage accounts, users, and access -- Capture leads and process payments -- Track customer interactions -- Monitor leads and sales funnels +# Example Use Cases -Examples of Applications You Can Build Using the Ontraport API: +- **Contact Segmentation and Email Campaigns**: Automatically tag and segment contacts in Ontraport based on their activity or data points from other apps. Trigger personalized email campaigns in Ontraport for each segment, enhancing marketing precision and engagement. -- Customer Relationship Management (CRM) solutions -- Online marketing solutions -- eCommerce solutions -- Lead generation solutions -- Automated sales tracking solutions -- Email marketing solutions -- Affiliate management solutions -- Reporting solutions +- **Lead Scoring and Salesforce Integration**: Implement a lead scoring system using Ontraport to assign points for various customer actions. Sync high-scoring leads to Salesforce, ensuring your sales team focuses on the most promising prospects. + +- **Customer Feedback and Support Ticket Creation**: Capture customer feedback through surveys or web forms. Use Ontraport to analyze sentiments and, based on defined criteria, create support tickets in apps like Zendesk for follow-up, ensuring customer issues are promptly addressed. diff --git a/components/open_exchange_rates/README.md b/components/open_exchange_rates/README.md new file mode 100644 index 0000000000000..b46262f599fd5 --- /dev/null +++ b/components/open_exchange_rates/README.md @@ -0,0 +1,11 @@ +# Overview + +The Open Exchange Rates API provides real-time and historical exchange rates for currencies worldwide, which enables you to retrieve the latest currency data for any purpose like financial reporting, pricing products in multiple currencies, or converting currency for international transactions. In Pipedream, you can tap into this API to create workflows that automate currency-related tasks, enrich data with current exchange rates, and trigger actions based on currency fluctuations. + +# Example Use Cases + +- **Automated Currency Conversion for E-Commerce**: Sync currency rates daily to your e-commerce platform using Pipedream's scheduled triggers. Convert product prices from your base currency to multiple currencies and update the pricing on your website accordingly. + +- **Financial Reporting**: Use Pipedream to gather and store historical exchange rates data in a data warehouse like Snowflake or Google BigQuery. Combine this with your financial data to generate accurate multi-currency financial reports. + +- **Alert System for Currency Fluctuations**: Set up a Pipedream workflow that monitors the Open Exchange Rates API for significant currency fluctuations. When a threshold is crossed, send an alert via email, Slack, or SMS to notify stakeholders of the change. diff --git a/components/openai/README.md b/components/openai/README.md index 999bb1e5332d4..f78066e4bbcfd 100644 --- a/components/openai/README.md +++ b/components/openai/README.md @@ -1,22 +1,91 @@ # Overview -The OpenAI API is a powerful tool that provides access to a range of -high-powered machine learning models. With the OpenAI API, developers can -create products, services, and tools that enable humanizing AI experiences, and -build applications that extend the power of human language. - -Using the OpenAI API, developers can create language-driven applications such -as: - -- Natural language understanding and sentiment analysis -- Text-based search -- Question answering -- Dialogue systems and conversation agents -- Intelligent text completion -- Text summarization -- Text classification -- Machine translation -- Language generation -- Multi-factor authentication -- Anomaly detection -- Text analysis +OpenAI provides a suite of powerful AI models through its API, enabling developers to integrate advanced natural language processing and generative capabilities into their applications. Here’s an overview of the services offered by OpenAI's API: + +- [Text generation](https://platform.openai.com/docs/guides/text-generation) +- [Embeddings](https://platform.openai.com/docs/guides/embeddings) +- [Fine-tuning](https://platform.openai.com/docs/guides/fine-tuning) +- [Image Generation](https://platform.openai.com/docs/guides/images?context=node) +- [Vision](https://platform.openai.com/docs/guides/vision) +- [Text-to-Speech](https://platform.openai.com/docs/guides/text-to-speech) +- [Speech-to-Text](https://platform.openai.com/docs/guides/speech-to-text) + +Use Python or Node.js code to make fully authenticated API requests with your OpenAI account: + +# Example Use Cases + +The OpenAI API can be leveraged in a wide range of business contexts to drive efficiency, enhance customer experiences, and innovate product offerings. Here are some specific business use cases for utilizing the OpenAI API: + +### **Customer Support Automation** + +Significantly reduce response times and free up human agents to tackle more complex issues by automating customer support ticket responses. + +### **Content Creation and Management** + +Utilize AI to generate high-quality content for blogs, articles, product descriptions, and marketing material. + +### **Personalized Marketing and Advertising** + +Optimize advertising copy and layouts based on user interaction data with a trained model via the Fine Tuning API. + +### **Language Translation and Localization** + +Use ChatGPT to translate and localize content across multiple languages, expanding your market reach without the need for extensive translation teams. + +# Getting Started + +First, sign up for an OpenAI account, then in a new workflow step open the OpenAI app: + +![Screenshot highlighting the OpenAI (ChatGPT) app selected in the Pipedream workflow automation interface, with a red arrow pointing to the OpenAI option in the sidebar app list.](https://res.cloudinary.com/pipedreamin/image/upload/v1713464578/marketplace/apps/openai/CleanShot_2024-04-18_at_14.22.30_guc5ri.png) + +Then select one of the Pre-built actions, or choose to use Node.js or Python: + +![Interface showing a list of OpenAI (ChatGPT) actions available in Pipedream, including options for building API requests, using the API in Node.js and Python, and pre-built actions like Chat, Summarize Text, and Create Image (DALL-E).](https://res.cloudinary.com/pipedreamin/image/upload/v1713464768/marketplace/apps/openai/CleanShot_2024-04-18_at_14.25.46_akse9e.png) + +Then connect your OpenAI account to Pipedream. Open the [API keys section](https://platform.openai.com/api-keys) in the OpenAI dashboard. + +Then select **Create a new secret key:** + +![Screenshot of the OpenAI API keys management page with a red arrow pointing to the 'Create new secret key' button for generating new API keys.](https://res.cloudinary.com/pipedreamin/image/upload/v1713464913/marketplace/apps/openai/CleanShot_2024-04-18_at_14.28.03_lw0pbw.png) + +Name the key `Pipedream` and then save the API key within Pipedream. Now you’re all set to use pre-built actions like `Chat` or use your OpenAI API key directly in Node.js or Python code. + +# Troubleshooting + +## 401 - Invalid Authentication + +--- + +Ensure the correct [API key](https://platform.openai.com/account/api-keys) and requesting organization are being used. + +## 401 - Incorrect API key provided + +--- + +Ensure the API key used is correct or [generate a new one](https://platform.openai.com/account/api-keys) and then reconnect it to Pipedream. + +## 401 - You must be a member of an organization to use the API + +Contact OpenAI to get added to a new organization or ask your organization manager to [invite you to an organization](https://platform.openai.com/account/team). + +## 403 - Country, region, or territory not supported + +You are accessing the API from an unsupported country, region, or territory. + +## 429 - Rate limit reached for requests + +--- + +You are sending requests too quickly. Pace your requests. Read the OpenAI [Rate limit guide](https://platform.openai.com/docs/guides/rate-limits). Use [Pipedream Concurrency and Throttling](https://pipedream.com/docs/workflows/concurrency-and-throttling) settings to control the frequency of API calls to OpenAI. + +## 429 - You exceeded your current quota, please check your plan and billing details + +You have run out of OpenAI credits or hit your maximum monthly spend. [Buy more OpenAI credits](https://platform.openai.com/account/billing) or learn how to [increase your OpenAI account limits](https://platform.openai.com/account/limits). + +## 500 - The server had an error while processing your request + +Retry your request after a brief wait and contact us if the issue persists. Check the [OpenAI status page](https://status.openai.com/). + +## 503 - The engine is currently overloaded, please try again later + +OpenAI’s servers are experiencing high amounts of traffic. Please retry your requests after a brief wait. diff --git a/components/openai/actions/analyze-image-content/analyze-image-content.mjs b/components/openai/actions/analyze-image-content/analyze-image-content.mjs new file mode 100644 index 0000000000000..f656b37f7da1e --- /dev/null +++ b/components/openai/actions/analyze-image-content/analyze-image-content.mjs @@ -0,0 +1,131 @@ +import openai from "../../openai.app.mjs"; +import common from "../common/common-assistants.mjs"; +import FormData from "form-data"; +import fs from "fs"; + +export default { + ...common, + key: "openai-analyze-image-content", + name: "Analyze Image Content", + description: "Send a message or question about an image and receive a response. [See the documentation](https://platform.openai.com/docs/api-reference/runs/createThreadAndRun)", + version: "0.1.4", + type: "action", + props: { + openai, + message: { + type: "string", + label: "Message", + description: "The message or question to send", + }, + imageUrl: { + type: "string", + label: "Image URL", + description: "The URL of the image to analyze. Must be a supported image types: jpeg, jpg, png, gif, webp", + optional: true, + }, + imageFileId: { + propDefinition: [ + openai, + "fileId", + () => ({ + purpose: "vision", + }), + ], + optional: true, + }, + filePath: { + type: "string", + label: "File Path", + description: "The path to a file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + optional: true, + }, + }, + async run({ $ }) { + const { id: assistantId } = await this.openai.createAssistant({ + $, + data: { + model: "gpt-4o", // replaced from "gpt-4-vision-preview" - see https://platform.openai.com/docs/deprecations + }, + }); + + const data = { + assistant_id: assistantId, + thread: { + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: this.message, + }, + ], + }, + ], + }, + model: this.model, + }; + if (this.imageUrl) { + data.thread.messages[0].content.push({ + type: "image_url", + image_url: { + url: this.imageUrl, + }, + }); + } + if (this.imageFileId) { + data.thread.messages[0].content.push({ + type: "image_file", + image_file: { + file_id: this.imageFileId, + }, + }); + } + if (this.filePath) { + const fileData = new FormData(); + const content = fs.createReadStream(this.filePath.includes("tmp/") + ? this.filePath + : `/tmp/${this.filePath}`); + fileData.append("purpose", "vision"); + fileData.append("file", content); + + const { id } = await this.openai.uploadFile({ + $, + data: fileData, + headers: fileData.getHeaders(), + }); + + data.thread.messages[0].content.push({ + type: "image_file", + image_file: { + file_id: id, + }, + }); + } + + let run; + run = await this.openai.createThreadAndRun({ + $, + data, + }); + const runId = run.id; + const threadId = run.thread_id; + + run = await this.pollRunUntilCompleted(run, threadId, runId, $); + + // get response; + const { data: messages } = await this.openai.listMessages({ + $, + threadId, + params: { + order: "desc", + }, + }); + const response = messages[0].content[0].text.value; + return { + response, + messages, + run, + }; + }, +}; diff --git a/components/openai/actions/cancel-run/cancel-run.mjs b/components/openai/actions/cancel-run/cancel-run.mjs index ca408ea8a17e0..54050850537d6 100644 --- a/components/openai/actions/cancel-run/cancel-run.mjs +++ b/components/openai/actions/cancel-run/cancel-run.mjs @@ -4,7 +4,7 @@ export default { key: "openai-cancel-run", name: "Cancel Run (Assistants)", description: "Cancels a run that is in progress. [See the documentation](https://platform.openai.com/docs/api-reference/runs/cancelRun)", - version: "0.0.6", + version: "0.0.13", type: "action", props: { openai, @@ -26,6 +26,7 @@ export default { }, async run({ $ }) { const response = await this.openai.cancelRun({ + $, threadId: this.threadId, runId: this.runId, }); diff --git a/components/openai/actions/chat-with-assistant/chat-with-assistant.mjs b/components/openai/actions/chat-with-assistant/chat-with-assistant.mjs new file mode 100644 index 0000000000000..1cd63b709a694 --- /dev/null +++ b/components/openai/actions/chat-with-assistant/chat-with-assistant.mjs @@ -0,0 +1,135 @@ +import openai from "../../openai.app.mjs"; +import common from "../common/common-assistants.mjs"; + +export default { + ...common, + key: "openai-chat-with-assistant", + name: "Chat with Assistant", + description: "Sends a message and generates a response, storing the message history for a continuous conversation. [See the documentation](https://platform.openai.com/docs/api-reference/runs/createThreadAndRun)", + version: "0.0.9", + type: "action", + props: { + openai, + message: { + type: "string", + label: "Message", + description: "The message to send", + }, + assistantId: { + propDefinition: [ + openai, + "assistant", + ], + description: "The assistant to use. Leave blank to create a new assistant", + optional: true, + }, + name: { + propDefinition: [ + openai, + "name", + ], + }, + instructions: { + propDefinition: [ + openai, + "instructions", + ], + }, + model: { + propDefinition: [ + openai, + "assistantModel", + ], + optional: true, + default: "gpt-3.5-turbo", + }, + threadId: { + propDefinition: [ + openai, + "threadId", + ], + description: "The unique identifier for the thread. Example: `thread_abc123`. Leave blank to create a new thread. To locate the thread ID, make sure your OpenAI Threads setting (Settings -> Organization/Personal -> General -> Features and capabilities -> Threads) is set to \"Visible to organization owners\" or \"Visible to everyone\". You can then access the list of threads and click on individual threads to reveal their IDs", + optional: true, + }, + ...common.props, + }, + async run({ $ }) { + // create assistant if not provided + let assistantId = this.assistantId; + if (!assistantId) { + const { id: newAssistantId } = await this.openai.createAssistant({ + $, + data: { + model: this.model, + name: this.name, + instructions: this.instructions, + tools: this.buildTools(), + tool_resources: this.buildToolResources(), + }, + }); + assistantId = newAssistantId; + } + + // create and run thread if no thread is provided + let threadId = this.threadId; + let run; + if (!threadId) { + run = await this.openai.createThreadAndRun({ + $, + data: { + assistant_id: assistantId, + thread: { + messages: [ + { + role: "user", + content: this.message, + }, + ], + }, + model: this.model, + instructions: this.instructions, + tools: this.buildTools(), + tool_resources: this.buildToolResources(), + }, + }); + threadId = run.thread_id; + } else { + // create run for thread if threadId is provided + run = await this.openai.createRun({ + $, + threadId: this.threadId, + data: { + assistant_id: assistantId, + model: this.model, + instructions: this.instructions, + tools: this.buildTools(), + tool_resources: this.buildToolResources(), + additional_messages: [ + { + role: "user", + content: this.message, + }, + ], + }, + }); + } + const runId = run.id; + + run = await this.pollRunUntilCompleted(run, threadId, runId, $); + + // get response; + const { data: messages } = await this.openai.listMessages({ + $, + threadId, + params: { + order: "desc", + }, + }); + const response = messages[0].content[0].text.value; + return { + response, + messages, + run, + }; + }, +}; diff --git a/components/openai/actions/chat/chat.mjs b/components/openai/actions/chat/chat.mjs index 235eec1228cb7..7d6ec0786aa3a 100644 --- a/components/openai/actions/chat/chat.mjs +++ b/components/openai/actions/chat/chat.mjs @@ -1,12 +1,14 @@ import openai from "../../openai.app.mjs"; import common from "../common/common.mjs"; +import constants from "../../common/constants.mjs"; +import { ConfigurationError } from "@pipedream/platform"; export default { ...common, name: "Chat", - version: "0.1.9", + version: "0.2.5", key: "openai-chat", - description: "The Chat API, using the `gpt-3.5-turbo` or `gpt-4` model. [See docs here](https://platform.openai.com/docs/api-reference/chat)", + description: "The Chat API, using the `gpt-3.5-turbo` or `gpt-4` model. [See the documentation](https://platform.openai.com/docs/api-reference/chat)", type: "action", props: { openai, @@ -21,6 +23,7 @@ export default { type: "string", description: "The user messages provide instructions to the assistant. They can be generated by the end users of an application, or set by a developer as an instruction.", }, + ...common.props, systemInstructions: { label: "System Instructions", type: "string", @@ -36,28 +39,119 @@ export default { images: { label: "Images", type: "string[]", - description: "Provide one or more images to [OpenAI's vision model](https://platform.openai.com/docs/guides/vision). Accepts URLs or base64 encoded strings. Compatible with the `gpt4-vision-preview model`", + description: "Provide one or more images to [OpenAI's vision model](https://platform.openai.com/docs/guides/vision). Accepts URLs or base64 encoded strings. Compatible with the `gpt4-vision-preview` model", + optional: true, + }, + audio: { + type: "string", + label: "Audio", + description: "Provide the file path to an audio file in the `/tmp` directory. For use with the `gpt-4o-audio-preview` model. Currently supports `wav` and `mp3` files.", optional: true, }, responseFormat: { type: "string", label: "Response Format", - description: "Specify the format that the model must output. [Setting to `json_object` guarantees the message the model generates is valid JSON](https://platform.openai.com/docs/api-reference/chat/create#chat-create-response_format). Defaults to `text`", - options: [ - "text", - "json_object", - ], + description: "Specify the format that the model must output. \n- **Text** (default): Returns unstructured text output.\n- **JSON Object**: Ensures the model's output is a valid JSON object.\n- **JSON Schema** (GPT-4o and later): Enables you to define a specific structure for the model's output using a JSON schema. Supported with models `gpt-4o-2024-08-06` and later, and `gpt-4o-mini-2024-07-18` and later.", + options: Object.values(constants.CHAT_RESPONSE_FORMAT), + default: constants.CHAT_RESPONSE_FORMAT.TEXT.value, optional: true, - default: "text", + reloadProps: true, + }, + toolTypes: { + type: "string[]", + label: "Tool Types", + description: "The types of tools to enable on the assistant", + options: constants.TOOL_TYPES.filter((toolType) => toolType === "function"), + optional: true, + reloadProps: true, + }, + }, + additionalProps() { + const { + responseFormat, + toolTypes, + numberOfFunctions, + } = this; + const props = {}; + + if (responseFormat === constants.CHAT_RESPONSE_FORMAT.JSON_SCHEMA.value) { + props.jsonSchema = { + type: "string", + label: "JSON Schema", + description: "Define the schema that the model's output must adhere to. [See the documentation here](https://platform.openai.com/docs/guides/structured-outputs/supported-schemas).", + }; + } + + if (toolTypes?.includes("function")) { + props.numberOfFunctions = { + type: "integer", + label: "Number of Functions", + description: "The number of functions to define", + optional: true, + reloadProps: true, + default: 1, + }; + + for (let i = 0; i < (numberOfFunctions || 1); i++) { + props[`functionName_${i}`] = { + type: "string", + label: `Function Name ${i + 1}`, + description: "The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64.", + }; + props[`functionDescription_${i}`] = { + type: "string", + label: `Function Description ${i + 1}`, + description: "A description of what the function does, used by the model to choose when and how to call the function.", + optional: true, + }; + props[`functionParameters_${i}`] = { + type: "object", + label: `Function Parameters ${i + 1}`, + description: "The parameters the functions accepts, described as a JSON Schema object. See the [guide](https://platform.openai.com/docs/guides/text-generation/function-calling) for examples, and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format.", + optional: true, + }; + } + } + + return props; + }, + methods: { + ...common.methods, + _buildTools() { + const tools = this.toolTypes?.filter((toolType) => toolType !== "function")?.map((toolType) => ({ + type: toolType, + })) || []; + if (this.toolTypes?.includes("function")) { + const numberOfFunctions = this.numberOfFunctions || 1; + for (let i = 0; i < numberOfFunctions; i++) { + tools.push({ + type: "function", + function: { + name: this[`functionName_${i}`], + description: this[`functionDescription_${i}`], + parameters: this[`functionParameters_${i}`], + }, + }); + } + } + return tools.length + ? tools + : undefined; }, - ...common.props, }, async run({ $ }) { + if (this.audio && !this.modelId.includes("gpt-4o-audio-preview")) { + throw new ConfigurationError("Use of audio files requires using the `gpt-4o-audio-preview` model."); + } + const args = this._getChatArgs(); const response = await this.openai.createChatCompletion({ $, - args, + data: { + ...args, + tools: this._buildTools(), + }, }); if (response) { diff --git a/components/openai/actions/classify-items-into-categories/classify-items-into-categories.mjs b/components/openai/actions/classify-items-into-categories/classify-items-into-categories.mjs index dde3dbc752e10..b1edf09015f78 100644 --- a/components/openai/actions/classify-items-into-categories/classify-items-into-categories.mjs +++ b/components/openai/actions/classify-items-into-categories/classify-items-into-categories.mjs @@ -3,12 +3,22 @@ import common from "../common/common-helper.mjs"; export default { ...common, name: "Classify Items into Categories", - version: "0.0.10", + version: "0.1.4", key: "openai-classify-items-into-categories", - description: "Classify items into specific categories using the Chat API", + description: "Classify items into specific categories using the Chat API. [See the documentation](https://platform.openai.com/docs/api-reference/chat)", type: "action", props: { ...common.props, + info: { + type: "alert", + alertType: "info", + content: `Provide a list of **items** and a list of **categories**. The output will contain an array of objects, each with properties \`item\` and \`category\` + \nExample: + \nIf **Categories** is \`["people", "pets"]\`, and **Items** is \`["dog", "George Washington"]\` + \n The output will contain the following categorizations: + \n \`[{"item":"dog","category":"pets"},{"item":"George Washington","category":"people"}]\` + `, + }, items: { label: "Items", description: "Items to categorize", @@ -40,17 +50,24 @@ export default { if (!messages || !response) { throw new Error("Invalid API output, please reach out to https://pipedream.com/support"); } - const assistantResponse = response.choices?.[0]?.message?.content; - let categorizations = assistantResponse; - try { - categorizations = JSON.parse(assistantResponse); - } catch (err) { - console.log("Failed to parse output, assistant returned malformed JSON"); + const responses = response.choices?.map(({ message }) => message.content); + const categorizations = []; + for (const response of responses) { + try { + categorizations.push(JSON.parse(response)); + } catch (err) { + console.log("Failed to parse output, assistant returned malformed JSON"); + } } - return { - categorizations, + const output = { messages, }; + if (this.n > 1) { + output.categorizations = categorizations; + } else { + output.categorizations = categorizations[0]; + } + return output; }, }, }; diff --git a/components/openai/actions/common/common-assistants.mjs b/components/openai/actions/common/common-assistants.mjs new file mode 100644 index 0000000000000..f6c1e26684f22 --- /dev/null +++ b/components/openai/actions/common/common-assistants.mjs @@ -0,0 +1,147 @@ +import constants from "../../common/constants.mjs"; + +export default { + props: { + toolTypes: { + type: "string[]", + label: "Tool Types", + description: "The types of tools to enable on the assistant", + options: constants.TOOL_TYPES, + optional: true, + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (!this.toolTypes?.length) { + return props; + } + if (this.toolTypes.includes("function")) { + props.numberOfFunctions = { + type: "integer", + label: "Number of Functions", + description: "The number of functions to define.", + optional: true, + reloadProps: true, + default: 1, + }; + } + return { + ...props, + ...(await this.getToolProps()), + }; + }, + methods: { + async getToolProps() { + const props = {}; + if (this.toolTypes.includes("code_interpreter")) { + props.fileIds = { + type: "string[]", + label: "File IDs", + description: "List of file IDs to attach to the message", + optional: true, + options: async () => { + const { data } = await this.openai.listFiles({ + purpose: "assistants", + }); + return data?.map(({ + filename, id, + }) => ({ + label: filename, + value: id, + })) || []; + }, + }; + } + if (this.toolTypes.includes("file_search")) { + props.vectorStoreIds = { + type: "string[]", + label: "Vector Store IDs", + description: "The ID of the vector store to attach to this assistant", + optional: true, + options: async () => { + const { data } = await this.openai.listVectorStores(); + return data?.map(({ + name, id, + }) => ({ + label: name, + value: id, + })) || []; + }, + }; + } + if (this.toolTypes.includes("function")) { + const numberOfFunctions = this.numberOfFunctions || 1; + for (let i = 0; i < numberOfFunctions; i++) { + props[`functionName_${i}`] = { + type: "string", + label: `Function Name ${i + 1}`, + description: "The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64.", + }; + props[`functionDescription_${i}`] = { + type: "string", + label: `Function Description ${i + 1}`, + description: "A description of what the function does, used by the model to choose when and how to call the function.", + optional: true, + }; + props[`functionParameters_${i}`] = { + type: "object", + label: `Function Parameters ${i + 1}`, + description: "The parameters the functions accepts, described as a JSON Schema object. See the [guide](https://platform.openai.com/docs/guides/text-generation/function-calling) for examples, and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format.", + optional: true, + }; + } + } + return props; + }, + buildTools() { + const tools = this.toolTypes?.filter((toolType) => toolType !== "function")?.map((toolType) => ({ + type: toolType, + })) || []; + if (this.toolTypes?.includes("function")) { + const numberOfFunctions = this.numberOfFunctions || 1; + for (let i = 0; i < numberOfFunctions; i++) { + tools.push({ + type: "function", + function: { + name: this[`functionName_${i}`], + description: this[`functionDescription_${i}`], + parameters: this[`functionParameters_${i}`], + }, + }); + } + } + return tools.length + ? tools + : undefined; + }, + buildToolResources() { + const toolResources = {}; + if (this.toolTypes?.includes("code_interpreter") && this.fileIds?.length) { + toolResources.code_interpreter = { + file_ids: this.fileIds, + }; + } + if (this.toolTypes?.includes("file_search") && this.vectorStoreIds?.length) { + toolResources.file_search = { + vector_store_ids: this.vectorStoreIds, + }; + } + return Object.keys(toolResources).length + ? toolResources + : undefined; + }, + async pollRunUntilCompleted(run, threadId, runId, $ = this) { + const timer = (ms) => new Promise((res) => setTimeout(res, ms)); + while (run.status === "queued" || run.status === "in_progress") { + run = await this.openai.retrieveRun({ + $, + threadId, + runId, + }); + await timer(3000); + } + return run; + }, + }, +}; diff --git a/components/openai/actions/common/common-helper.mjs b/components/openai/actions/common/common-helper.mjs index 6cd0571b4541e..f9aae3e7a7e3e 100644 --- a/components/openai/actions/common/common-helper.mjs +++ b/components/openai/actions/common/common-helper.mjs @@ -5,6 +5,13 @@ export default { ...common, props: { openai, + modelId: { + propDefinition: [ + openai, + "chatCompletionModelId", + ], + description: "The ID of the model to use for chat completions", + }, ...common.props, }, methods: { @@ -33,14 +40,14 @@ export default { content: this.userMessage(), }, ]; - const args = { + const data = { ...this._getCommonArgs(), - model: "gpt-3.5-turbo", + model: this.modelId, messages, }; const response = await this.openai.createChatCompletion({ $, - args, + data, }); if (this.summary() && response) { diff --git a/components/openai/actions/common/common.mjs b/components/openai/actions/common/common.mjs index e4d232eb50e4e..41474b809d1d6 100644 --- a/components/openai/actions/common/common.mjs +++ b/components/openai/actions/common/common.mjs @@ -1,4 +1,7 @@ import { ConfigurationError } from "@pipedream/platform"; +import constants from "../../common/constants.mjs"; +import { parse } from "../../common/helpers.mjs"; +import fs from "fs"; const CHAT_DOCS_MESSAGE_FORMAT_URL = "https://platform.openai.com/docs/guides/chat/introduction"; @@ -90,6 +93,20 @@ export default { } } + if (this.audio) { + const fileContent = fs.readFileSync(this.audio.includes("tmp/") + ? this.audio + : `/tmp/${this.audio}`).toString("base64"); + const extension = this.audio.match(/\.(\w+)$/)?.[1]; + content.push({ + type: "input_audio", + input_audio: { + data: fileContent, + format: extension, + }, + }); + } + content.push({ "type": "text", "text": this.userMessage, @@ -148,9 +165,17 @@ export default { const responseFormat = {}; + const jsonSchemaObj = + this.responseFormat === constants.CHAT_RESPONSE_FORMAT.JSON_SCHEMA.value + ? { + json_schema: parse(this.jsonSchema), + } + : {}; + if (this.modelId != "gpt-4-vision-preview") { responseFormat["response_format"] = { type: this.responseFormat, + ...jsonSchemaObj, }; } diff --git a/components/openai/actions/common/constants.mjs b/components/openai/actions/common/constants.mjs deleted file mode 100644 index 36c8176131712..0000000000000 --- a/components/openai/actions/common/constants.mjs +++ /dev/null @@ -1,14 +0,0 @@ -export default { - IMAGE_SIZES: [ - "256x256", - "512x512", - "1024x1024", - ], - TRANSCRIPTION_FORMATS: [ - "json", - "text", - "srt", - "verbose_json", - "vtt", - ], -}; diff --git a/components/openai/actions/convert-text-to-speech/convert-text-to-speech.mjs b/components/openai/actions/convert-text-to-speech/convert-text-to-speech.mjs new file mode 100644 index 0000000000000..376e3e3c13cd7 --- /dev/null +++ b/components/openai/actions/convert-text-to-speech/convert-text-to-speech.mjs @@ -0,0 +1,73 @@ +import fs from "fs"; +import openai from "../../openai.app.mjs"; + +export default { + key: "openai-convert-text-to-speech", + name: "Convert Text to Speech (TTS)", + description: "Generates audio from the input text. [See the documentation](https://platform.openai.com/docs/api-reference/audio/createSpeech)", + version: "0.0.12", + type: "action", + props: { + openai, + model: { + propDefinition: [ + openai, + "ttsModel", + ], + }, + input: { + propDefinition: [ + openai, + "input", + ], + }, + voice: { + propDefinition: [ + openai, + "voice", + ], + }, + responseFormat: { + propDefinition: [ + openai, + "responseFormat", + ], + }, + speed: { + propDefinition: [ + openai, + "speed", + ], + }, + outputFile: { + type: "string", + label: "Output Filename", + description: "The filename of the output audio file that will be written to the `/tmp` folder, e.g. `/tmp/myFile.mp3`", + }, + }, + async run({ $ }) { + const response = await this.openai.createSpeech({ + $, + data: { + model: this.model, + input: this.input, + voice: this.voice, + response_format: this.responseFormat, + speed: Number(this.speed), + }, + responseType: "arraybuffer", + }); + + const outputFilePath = this.outputFile.includes("tmp/") + ? this.outputFile + : `/tmp/${this.outputFile}`; + + await fs.promises.writeFile(outputFilePath, Buffer.from(response)); + + $.export("$summary", "Generated audio successfully"); + return { + outputFilePath, + response, + }; + }, +}; diff --git a/components/openai/actions/create-assistant/create-assistant.mjs b/components/openai/actions/create-assistant/create-assistant.mjs index b1ab7dcd1f650..b3d2227a95f56 100644 --- a/components/openai/actions/create-assistant/create-assistant.mjs +++ b/components/openai/actions/create-assistant/create-assistant.mjs @@ -1,11 +1,12 @@ -import { parseToolsArray } from "../../common/helpers.mjs"; import openai from "../../openai.app.mjs"; +import common from "../common/common-assistants.mjs"; export default { + ...common, key: "openai-create-assistant", name: "Create Assistant", - description: "Creates an assistant with a model and instructions. [See the docs here](https://platform.openai.com/docs/api-reference/assistants/createAssistant)", - version: "0.1.3", + description: "Creates an assistant with a model and instructions. [See the documentation](https://platform.openai.com/docs/api-reference/assistants/createAssistant)", + version: "0.1.11", type: "action", props: { openai, @@ -33,35 +34,26 @@ export default { "instructions", ], }, - tools: { - propDefinition: [ - openai, - "tools", - ], - }, - file_ids: { - propDefinition: [ - openai, - "file_ids", - ], - }, metadata: { propDefinition: [ openai, "metadata", ], }, + ...common.props, }, async run({ $ }) { const response = await this.openai.createAssistant({ $, - model: this.model, - name: this.name, - description: this.description, - instructions: this.instructions, - tools: parseToolsArray(this.tools), - file_ids: this.file_ids, - metadata: this.metadata, + data: { + model: this.model, + name: this.name, + description: this.description, + instructions: this.instructions, + tools: this.buildTools(), + tool_resources: this.buildToolResources(), + metadata: this.metadata, + }, }); $.export("$summary", `Successfully created an assistant with ID: ${response.id}`); diff --git a/components/openai/actions/create-batch/create-batch.mjs b/components/openai/actions/create-batch/create-batch.mjs new file mode 100644 index 0000000000000..6277dfca0ed35 --- /dev/null +++ b/components/openai/actions/create-batch/create-batch.mjs @@ -0,0 +1,78 @@ +import openai from "../../openai.app.mjs"; +import constants from "../../common/constants.mjs"; +import { ConfigurationError } from "@pipedream/platform"; +import FormData from "form-data"; +import fs from "fs"; + +export default { + key: "openai-create-batch", + name: "Create Batch", + description: "Creates and executes a batch from an uploaded file of requests. [See the documentation](https://platform.openai.com/docs/api-reference/batch/create)", + version: "0.0.7", + type: "action", + props: { + openai, + endpoint: { + type: "string", + label: "Endpoint", + description: "The endpoint to be used for all requests in the batch", + options: constants.BATCH_ENDPOINTS, + }, + fileId: { + propDefinition: [ + openai, + "fileId", + () => ({ + purpose: "batch", + }), + ], + optional: true, + }, + filePath: { + type: "string", + label: "File Path", + description: "The path to a .jsonl file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + optional: true, + }, + metadata: { + propDefinition: [ + openai, + "metadata", + ], + }, + }, + async run({ $ }) { + if (!this.fileId && !this.filePath) { + throw new ConfigurationError("Must provide one of File ID or File Path"); + } + + let fileId = this.fileId; + if (this.filePath) { + const fileData = new FormData(); + const content = fs.createReadStream(this.filePath.includes("tmp/") + ? this.filePath + : `/tmp/${this.filePath}`); + fileData.append("purpose", "batch"); + fileData.append("file", content); + + const { id } = await this.openai.uploadFile({ + $, + data: fileData, + headers: fileData.getHeaders(), + }); + fileId = id; + } + + const response = await this.openai.createBatch({ + $, + data: { + input_file_id: fileId, + endpoint: this.endpoint, + completion_window: "24h", + metadata: this.metadata, + }, + }); + $.export("$summary", `Successfully created batch with ID ${response.id}`); + return response; + }, +}; diff --git a/components/openai/actions/create-embeddings/create-embeddings.mjs b/components/openai/actions/create-embeddings/create-embeddings.mjs index e6465c75d05b3..fdc3e30305f76 100644 --- a/components/openai/actions/create-embeddings/create-embeddings.mjs +++ b/components/openai/actions/create-embeddings/create-embeddings.mjs @@ -4,9 +4,9 @@ import common from "../common/common.mjs"; export default { name: "Create Embeddings", - version: "0.0.8", + version: "0.0.16", key: "openai-create-embeddings", - description: "Get a vector representation of a given input that can be easily consumed by machine learning models and algorithms. [See the docs here](https://platform.openai.com/docs/api-reference/embeddings)", + description: "Get a vector representation of a given input that can be easily consumed by machine learning models and algorithms. [See the documentation](https://platform.openai.com/docs/api-reference/embeddings)", type: "action", props: { openai, @@ -36,7 +36,7 @@ export default { const response = await this.openai.createEmbeddings({ $, - args: { + data: { model: this.modelId, input: this.input, }, diff --git a/components/openai/actions/create-fine-tuning-job/create-fine-tuning-job.mjs b/components/openai/actions/create-fine-tuning-job/create-fine-tuning-job.mjs index 54430317479f2..d80be648e8aee 100644 --- a/components/openai/actions/create-fine-tuning-job/create-fine-tuning-job.mjs +++ b/components/openai/actions/create-fine-tuning-job/create-fine-tuning-job.mjs @@ -4,7 +4,7 @@ export default { key: "openai-create-fine-tuning-job", name: "Create Fine Tuning Job", description: "Creates a job that fine-tunes a specified model from a given dataset. [See the documentation](https://platform.openai.com/docs/api-reference/fine-tuning/create)", - version: "0.0.5", + version: "0.0.12", type: "action", props: { openai, @@ -17,8 +17,13 @@ export default { trainingFile: { propDefinition: [ openai, - "trainingFile", + "fileId", + () => ({ + purpose: "fine-tune", + }), ], + label: "Training File", + description: "The ID of an uploaded file that contains training data. You can use the **Upload File** action and reference the returned ID here.", }, hyperParameters: { type: "object", @@ -33,7 +38,13 @@ export default { optional: true, }, validationFile: { - type: "string", + propDefinition: [ + openai, + "fileId", + () => ({ + purpose: "fine-tune", + }), + ], label: "Validation File", description: "The ID of an uploaded file that contains validation data. [See details in the documentation](https://platform.openai.com/docs/api-reference/fine-tuning/create#fine-tuning-create-validation_file).", optional: true, diff --git a/components/openai/actions/create-image/create-image.mjs b/components/openai/actions/create-image/create-image.mjs index 507d63836b6ee..9e875bea571f6 100644 --- a/components/openai/actions/create-image/create-image.mjs +++ b/components/openai/actions/create-image/create-image.mjs @@ -1,10 +1,12 @@ import openai from "../../openai.app.mjs"; +import constants from "../../common/constants.mjs"; +import fs from "fs"; export default { name: "Create Image (Dall-E)", - version: "0.1.13", + version: "0.1.20", key: "openai-create-image", - description: "Creates an image given a prompt returning a URL to the image. [See docs here](https://platform.openai.com/docs/api-reference/images)", + description: "Creates an image given a prompt returning a URL to the image. [See the documentation](https://platform.openai.com/docs/api-reference/images)", type: "action", props: { openai, @@ -12,127 +14,114 @@ export default { label: "Model", description: "Choose the DALL·E models to generate image(s) with.", type: "string", - options: [ - { - label: "dall-e-2", - value: "dall-e-2", - }, - { - label: "dall-e-3", - value: "dall-e-3", - }, - ], - default: "dall-e-3", + options: constants.IMAGE_MODELS, + reloadProps: true, }, prompt: { label: "Prompt", - description: "A text description of the desired image(s). The maximum length is 1000 characters.", + description: "A text description of the desired image(s). The maximum length is 1000 characters for dall-e-2 and 4000 characters for dall-e-3.", type: "string", }, - n: { - label: "N", - description: "The number of images to generate. Must be between 1 and 10.", - type: "integer", - optional: true, - default: 1, - }, - quality: { - label: "Quality", - description: "The quality of the image", - type: "string", - optional: true, - options: [ - { - label: "Standard", - value: "standard", - }, - { - label: "HD", - value: "hd", - }, - ], - default: "standard", - }, - style: { - label: "Style", - description: "The style of the image", - type: "string", - optional: true, - options: [ - { - label: "Natural", - value: "natural", - }, - { - label: "Vivid", - value: "vivid", - }, - ], - default: "natural", - }, responseFormat: { label: "Response Format", description: "The format in which the generated images are returned.", type: "string", optional: true, - options: [ - { - label: "URL", - value: "url", - }, - { - label: "Base64 JSON", - value: "b64_json", - }, - ], + options: constants.IMAGE_RESPONSE_FORMATS, default: "url", + reloadProps: true, }, size: { label: "Size", description: "The size of the generated images.", type: "string", optional: true, - options: [ - { - label: "256x256", - value: "256x256", - }, - { - label: "512x512", - value: "512x512", - }, - { - label: "1024x1024", - value: "1024x1024", - }, - { - label: "1792x1024", - value: "1792x1024", - }, - { - label: "1024x1792", - value: "1024x1792", - }, - ], + options: constants.IMAGE_SIZES, default: "1024x1024", }, }, + async additionalProps() { + const props = {}; + if (!this.model && !this.responseFormat) { + return props; + } + if (this.responseFormat === "tmp") { + props.filename = { + type: "string", + label: "Filename", + description: "Filename of the new file in `/tmp` directory. Make sure to include the extension.", + }; + } + if (this.model && this.model !== "dall-e-3") { + props.n = { + type: "integer", + label: "N", + description: "The number of images to generate. Must be between 1 and 10.", + optional: true, + default: 1, + }; + } else { + props.quality = { + type: "string", + label: "Quality", + description: "The quality of the image", + options: constants.IMAGE_QUALITIES, + optional: true, + default: "standard", + }; + props.style = { + type: "string", + label: "Style", + description: "The style of the image", + options: constants.IMAGE_STYLES, + optional: true, + default: "natural", + }; + } + return props; + }, async run({ $ }) { const response = await this.openai.createImage({ $, - args: { + data: { prompt: this.prompt, n: this.n, size: this.size, - response_format: this.responseFormat, + response_format: this.responseFormat === "url" + ? this.responseFormat + : "b64_json", model: this.model, quality: this.quality, style: this.style, }, }); + if (this.responseFormat === "tmp") { + const n = this.n || 1; + const fileData = []; + for (let i = 0; i < n; i++) { + // if N > 0, name subsequent files "filename_1.ext", "filename_2.ext", etc. + const filename = i === 0 + ? this.filename + : this.filename.replace(/(\.[^/.]+)$/, `_${i}$1`); + const outputFilePath = filename.includes("tmp/") + ? filename + : `/tmp/${filename}`; + await fs.writeFileSync(outputFilePath, Buffer.from(response.data[0].b64_json.toString(), "base64")); + fileData.push({ + tmp: [ + filename, + outputFilePath, + ], + }); + } + response.data = fileData; + } + if (response.data.length) { - $.export("$summary", `Successfully created ${response.data.length} images`); + $.export("$summary", `Successfully created ${response.data.length} image${response.data.length === 1 + ? "" + : "s"}`); } return response; diff --git a/components/openai/actions/create-message/create-message.mjs b/components/openai/actions/create-message/create-message.mjs deleted file mode 100644 index b179dadaf3e8d..0000000000000 --- a/components/openai/actions/create-message/create-message.mjs +++ /dev/null @@ -1,64 +0,0 @@ -import openai from "../../openai.app.mjs"; - -export default { - key: "openai-create-message", - name: "Create Message (Assistants)", - description: "Create a message in a thread. [See the documentation](https://platform.openai.com/docs/api-reference/messages/createMessage)", - version: "0.0.7", - type: "action", - props: { - openai, - threadId: { - propDefinition: [ - openai, - "threadId", - ], - }, - content: { - propDefinition: [ - openai, - "content", - ], - }, - role: { - propDefinition: [ - openai, - "role", - ], - default: "user", - }, - fileIds: { - propDefinition: [ - openai, - "fileIds", - ], - optional: true, - }, - metadata: { - propDefinition: [ - openai, - "metadata", - ], - optional: true, - }, - }, - async run({ $ }) { - const fileIdsArray = this.fileIds - ? this.fileIds.map((fileId) => fileId.trim()) - : undefined; - const metadataObject = this.metadata - ? JSON.parse(this.metadata) - : undefined; - - const response = await this.openai.createMessage({ - threadId: this.threadId, - content: this.content, - role: this.role, - fileIds: fileIdsArray, - metadata: metadataObject, - }); - - $.export("$summary", `Successfully created a message in thread ${this.threadId}`); - return response; - }, -}; diff --git a/components/openai/actions/create-moderation/create-moderation.mjs b/components/openai/actions/create-moderation/create-moderation.mjs new file mode 100644 index 0000000000000..8b09cea1ff4ce --- /dev/null +++ b/components/openai/actions/create-moderation/create-moderation.mjs @@ -0,0 +1,35 @@ +import openai from "../../openai.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "openai-create-moderation", + name: "Create Moderation", + description: "Classifies if text is potentially harmful. [See the documentation](https://platform.openai.com/docs/api-reference/moderations/create)", + version: "0.0.7", + type: "action", + props: { + openai, + input: { + type: "string", + label: "Input", + description: "The input text to classify", + }, + model: { + type: "string", + label: "Model", + description: "The model to use", + options: constants.MODERATION_MODELS, + }, + }, + async run({ $ }) { + const response = await this.openai.createModeration({ + $, + data: { + input: this.input, + model: this.model, + }, + }); + $.export("$summary", `Successfully created moderation with ID ${response.id}`); + return response; + }, +}; diff --git a/components/openai/actions/create-run/create-run.mjs b/components/openai/actions/create-run/create-run.mjs deleted file mode 100644 index 92579ea3be2dc..0000000000000 --- a/components/openai/actions/create-run/create-run.mjs +++ /dev/null @@ -1,65 +0,0 @@ -import { parseToolsArray } from "../../common/helpers.mjs"; -import openai from "../../openai.app.mjs"; - -export default { - key: "openai-create-run", - name: "Create Run (Assistants)", - description: "Creates a run given a thread ID and assistant ID. [See the documentation](https://platform.openai.com/docs/api-reference/runs/create)", - version: "0.1.3", - type: "action", - props: { - openai, - threadId: { - propDefinition: [ - openai, - "threadId", - ], - }, - assistantId: { - propDefinition: [ - openai, - "assistantId", - ], - }, - model: { - propDefinition: [ - openai, - "model", - ], - }, - instructions: { - propDefinition: [ - openai, - "instructions", - ], - }, - tools: { - propDefinition: [ - openai, - "tools", - ], - }, - metadata: { - propDefinition: [ - openai, - "metadata", - ], - }, - }, - async run({ $ }) { - const response = await this.openai.createRun({ - threadId: this.threadId, - assistantId: this.assistantId, - model: this.model, - instructions: this.instructions, - tools: parseToolsArray(this.tools), - metadata: this.metadata, - }); - - const summary = response.id - ? `Successfully created a run with ID: ${response.id}` - : `Successfully created a run in thread ${this.threadId}`; - $.export("$summary", summary); - return response; - }, -}; diff --git a/components/openai/actions/create-speech/create-speech.mjs b/components/openai/actions/create-speech/create-speech.mjs deleted file mode 100644 index 175b3cf25c530..0000000000000 --- a/components/openai/actions/create-speech/create-speech.mjs +++ /dev/null @@ -1,73 +0,0 @@ -import fs from "fs"; -import openai from "../../openai.app.mjs"; - -export default { - key: "openai-create-speech", - name: "Create Speech (TTS)", - description: "Generates audio from the input text. [See the documentation](https://platform.openai.com/docs/api-reference/audio/createSpeech)", - version: "0.0.5", - type: "action", - props: { - openai, - model: { - propDefinition: [ - openai, - "ttsModel", - ], - }, - input: { - propDefinition: [ - openai, - "input", - ], - }, - voice: { - propDefinition: [ - openai, - "voice", - ], - }, - responseFormat: { - propDefinition: [ - openai, - "responseFormat", - ], - }, - speed: { - propDefinition: [ - openai, - "speed", - ], - }, - outputFile: { - type: "string", - label: "Output Filename", - description: "The filename of the output audio file that will be written to the `/tmp` folder, e.g. `/tmp/myFile.mp3`", - }, - }, - async run({ $ }) { - const response = await this.openai.createSpeech({ - $, - data: { - model: this.model, - input: this.input, - voice: this.voice, - response_format: this.responseFormat, - speed: Number(this.speed), - }, - responseType: "arraybuffer", - }); - - const outputFilePath = this.outputFile.includes("tmp/") - ? this.outputFile - : `/tmp/${this.outputFile}`; - - await fs.promises.writeFile(outputFilePath, Buffer.from(response)); - - $.export("$summary", "Generated audio successfully"); - return { - outputFilePath, - response, - }; - }, -}; diff --git a/components/openai/actions/create-thread-and-run/create-thread-and-run.mjs b/components/openai/actions/create-thread-and-run/create-thread-and-run.mjs deleted file mode 100644 index f06d9e8358d1b..0000000000000 --- a/components/openai/actions/create-thread-and-run/create-thread-and-run.mjs +++ /dev/null @@ -1,62 +0,0 @@ -import { parseToolsArray } from "../../common/helpers.mjs"; -import openai from "../../openai.app.mjs"; - -export default { - key: "openai-create-thread-and-run", - name: "Create Thread and Run (Assistants)", - description: "Create a thread and run it in one request using the specified assistant ID and optional parameters. [See the documentation](https://platform.openai.com/docs/api-reference)", - version: "0.1.3", - type: "action", - props: { - openai, - assistantId: { - propDefinition: [ - openai, - "assistantId", - ], - }, - thread: { - type: "object", - label: "Thread", - description: "The thread object containing messages and other optional properties.", - optional: true, - }, - model: { - propDefinition: [ - openai, - "model", - ], - }, - instructions: { - propDefinition: [ - openai, - "instructions", - ], - }, - tools: { - propDefinition: [ - openai, - "tools", - ], - }, - metadata: { - propDefinition: [ - openai, - "metadata", - ], - }, - }, - async run({ $ }) { - const response = await this.openai.createThreadAndRun({ - assistant_id: this.assistantId, - thread: this.thread, - model: this.model, - instructions: this.instructions, - tools: parseToolsArray(this.tools), - metadata: this.metadata, - }); - - $.export("$summary", `Successfully created thread and initiated run with ID: ${response.id}`); - return response; - }, -}; diff --git a/components/openai/actions/create-thread/create-thread.mjs b/components/openai/actions/create-thread/create-thread.mjs index efe6c71def2b2..2e43d3ba1b1ba 100644 --- a/components/openai/actions/create-thread/create-thread.mjs +++ b/components/openai/actions/create-thread/create-thread.mjs @@ -1,10 +1,12 @@ import openai from "../../openai.app.mjs"; +import common from "../common/common-assistants.mjs"; +import constants from "../../common/constants.mjs"; export default { key: "openai-create-thread", name: "Create Thread (Assistants)", - description: "Creates a thread with optional messages and metadata. [See the documentation](https://platform.openai.com/docs/api-reference/threads/createThread)", - version: "0.0.5", + description: "Creates a thread with optional messages and metadata, and optionally runs the thread using the specified assistant. [See the documentation](https://platform.openai.com/docs/api-reference/threads/createThread)", + version: "0.0.13", type: "action", props: { openai, @@ -22,15 +24,115 @@ export default { ], optional: true, }, + runThread: { + type: "boolean", + label: "Run Thread", + description: "Set to `true` to run the thread after creation", + optional: true, + reloadProps: true, + }, + toolTypes: { + type: "string[]", + label: "Tool Types", + description: "The types of tools to enable on the assistant", + options: constants.TOOL_TYPES.filter((type) => type !== "function"), + optional: true, + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.runThread) { + props.assistantId = { + type: "string", + label: "Assistant ID", + description: "The unique identifier for the assistant.", + options: async () => { return this.getAssistantPropOptions(); }, + }; + props.model = { + type: "string", + label: "Model", + description: "The ID of the model to use for the assistant", + options: async () => { return this.getAssistantModelPropOptions(); }, + }; + props.instructions = { + type: "string", + label: "Instructions", + description: "The system instructions that the assistant uses.", + optional: true, + }; + props.waitForCompletion = { + type: "boolean", + label: "Wait For Completion", + description: "Set to `true` to poll the API in 3-second intervals until the run is completed", + optional: true, + }; + } + const toolProps = this.toolTypes?.length + ? await this.getToolProps() + : {}; + return { + ...props, + ...toolProps, + }; + }, + methods: { + ...common.methods, + async getAssistantPropOptions() { + const { data } = await this.openai.listAssistants(); + return data.map(({ + name, id, + }) => ({ + label: name || id, + value: id, + })); + }, + async getAssistantModelPropOptions() { + const models = (await this.openai.models({})).filter(({ id }) => (id.includes("gpt-3.5-turbo") || id.includes("gpt-4-turbo")) && (id !== "gpt-3.5-turbo-0301")); + return models.map(({ id }) => id); + }, }, async run({ $ }) { - const response = await this.openai.createThread({ - $, - messages: this.messages, - metadata: this.metadata, - }); + const messages = this.messages?.length + ? this.messages.map((message) => ({ + role: "user", + content: message, + })) + : undefined; + let response = !this.runThread + ? await this.openai.createThread({ + $, + data: { + messages, + metadata: this.metadata, + tool_resources: this.buildToolResources(), + }, + }) + : await this.openai.createThreadAndRun({ + $, + data: { + assistant_id: this.assistantId, + thread: { + messages, + metadata: this.metadata, + }, + model: this.model, + instructions: this.instructions, + tools: this.buildTools(), + tool_resources: this.buildToolResources(), + metadata: this.metadata, + }, + }); + + if (this.waitForCompletion) { + const runId = response.id; + const threadId = response.thread_id; + response = await this.pollRunUntilCompleted(response, threadId, runId, $); + } - $.export("$summary", `Successfully created a thread with ID: ${response.id}`); + $.export("$summary", `Successfully created a thread ${this.runThread + ? "and run" + : ""} with ID: ${response.id}`); return response; }, }; diff --git a/components/openai/actions/create-transcription/create-transcription.mjs b/components/openai/actions/create-transcription/create-transcription.mjs deleted file mode 100644 index d0c37a7135c19..0000000000000 --- a/components/openai/actions/create-transcription/create-transcription.mjs +++ /dev/null @@ -1,264 +0,0 @@ -import ffmpegInstaller from "@ffmpeg-installer/ffmpeg"; -import { ConfigurationError } from "@pipedream/platform"; -import axios from "axios"; -import Bottleneck from "bottleneck"; -import { exec } from "child_process"; -import FormData from "form-data"; -import fs from "fs"; -import { - extname, - join, -} from "path"; -import stream from "stream"; -import { promisify } from "util"; -import openai from "../../openai.app.mjs"; -import common from "../common/common.mjs"; -import constants from "../common/constants.mjs"; -import lang from "../common/lang.mjs"; - -const COMMON_AUDIO_FORMATS_TEXT = "Your audio file must be in one of these formats: mp3, mp4, mpeg, mpga, m4a, wav, or webm."; -const CHUNK_SIZE_MB = 20; - -const execAsync = promisify(exec); -const pipelineAsync = promisify(stream.pipeline); - -export default { - name: "Create Transcription (Whisper)", - version: "0.1.8", - key: "openai-create-transcription", - description: "Transcribes audio into the input language. [See docs here](https://platform.openai.com/docs/api-reference/audio/create).", - type: "action", - props: { - openai, - uploadType: { - label: "Audio Upload Type", - description: "Are you uploading an audio file from [your workflow's `/tmp` directory](https://pipedream.com/docs/code/nodejs/working-with-files/#the-tmp-directory), or providing a URL to the file?", - type: "string", - options: [ - "File", - "URL", - ], - reloadProps: true, - }, - language: { - label: "Language", - description: "**Optional**. The language of the input audio. Supplying the input language will improve accuracy and latency.", - type: "string", - optional: true, - options: lang.LANGUAGES.map((l) => ({ - label: l.label, - value: l.value, - })), - }, - }, - async additionalProps() { - const props = {}; - switch (this.uploadType) { - case "File": - props.path = { - type: "string", - label: "File Path", - description: `A path to your audio file to transcribe, e.g. \`/tmp/audio.mp3\`. ${COMMON_AUDIO_FORMATS_TEXT} Add the appropriate extension (mp3, mp4, etc.) on your filename — OpenAI uses the extension to determine the file type. [See the Pipedream docs on saving files to \`/tmp\`](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)`, - }; - break; - case "URL": - props.url = { - type: "string", - label: "URL", - description: `A public URL to the audio file to transcribe. This URL must point directly to the audio file, not a webpage that links to the audio file. ${COMMON_AUDIO_FORMATS_TEXT}`, - }; - break; - default: - throw new ConfigurationError("Invalid upload type specified. Please provide 'File' or 'URL'."); - } - // Because we need to display the file or URL above, and not below, these optional props - // TODO: Will be fixed when we render optional props correctly when used with additionalProps - props.prompt = { - label: "Prompt", - description: "**Optional** text to guide the model's style or continue a previous audio segment. The [prompt](https://platform.openai.com/docs/guides/speech-to-text/prompting) should match the audio language.", - type: "string", - optional: true, - }; - props.responseFormat = { - label: "Response Format", - description: "**Optional**. The format of the response. The default is `json`.", - type: "string", - default: "json", - optional: true, - options: constants.TRANSCRIPTION_FORMATS, - }; - props.temperature = common.props.temperature; - - return props; - }, - methods: { - createForm({ - file, outputDir, - }) { - const form = new FormData(); - form.append("model", "whisper-1"); - if (this.prompt) form.append("prompt", this.prompt); - if (this.temperature) form.append("temperature", this.temperature); - if (this.language) form.append("language", this.language); - if (this.responseFormat) form.append("response_format", this.responseFormat); - const readStream = fs.createReadStream(join(outputDir, file)); - form.append("file", readStream); - return form; - }, - async splitLargeChunks(files, outputDir) { - for (const file of files) { - if (fs.statSync(`${outputDir}/${file}`).size / (1024 * 1024) > CHUNK_SIZE_MB) { - await this.chunkFile({ - file: `${outputDir}/${file}`, - outputDir, - index: file.slice(6, 9), - }); - await execAsync(`rm -f "${outputDir}/${file}"`); - } - } - }, - async chunkFileAndTranscribe({ - file, $, - }) { - const outputDir = join("/tmp", "chunks"); - await execAsync(`mkdir -p "${outputDir}"`); - await execAsync(`rm -f "${outputDir}/*"`); - - await this.chunkFile({ - file, - outputDir, - }); - - let files = await fs.promises.readdir(outputDir); - // ffmpeg will sometimes return chunks larger than the allowed size, - // so we need to identify large chunks and break them down further - await this.splitLargeChunks(files, outputDir); - files = await fs.promises.readdir(outputDir); - - return this.transcribeFiles({ - files, - outputDir, - $, - }); - }, - async chunkFile({ - file, outputDir, index, - }) { - const ffmpegPath = ffmpegInstaller.path; - const ext = extname(file); - - const fileSizeInMB = fs.statSync(file).size / (1024 * 1024); - // We're limited to 26MB per request. Because of how ffmpeg splits files, - // we need to be conservative in the number of chunks we create - const conservativeChunkSizeMB = CHUNK_SIZE_MB; - const numberOfChunks = !index - ? Math.ceil(fileSizeInMB / conservativeChunkSizeMB) - : 2; - - if (numberOfChunks === 1) { - await execAsync(`cp "${file}" "${outputDir}/chunk-000${ext}"`); - return; - } - - const { stdout } = await execAsync(`${ffmpegPath} -i "${file}" 2>&1 | grep "Duration"`); - const duration = stdout.match(/\d{2}:\d{2}:\d{2}\.\d{2}/s)[0]; - const [ - hours, - minutes, - seconds, - ] = duration.split(":").map(parseFloat); - - const totalSeconds = (hours * 60 * 60) + (minutes * 60) + seconds; - const segmentTime = Math.ceil(totalSeconds / numberOfChunks); - - const command = `${ffmpegPath} -i "${file}" -f segment -segment_time ${segmentTime} -c copy "${outputDir}/chunk-${index - ? `${index}-` - : ""}%03d${ext}"`; - await execAsync(command); - }, - transcribeFiles({ - files, outputDir, $, - }) { - const limiter = new Bottleneck({ - maxConcurrent: 1, - minTime: 1000 / 59, - }); - - return Promise.all(files.map((file) => { - return limiter.schedule(() => this.transcribe({ - file, - outputDir, - $, - })); - })); - }, - transcribe({ - file, outputDir, $, - }) { - const form = this.createForm({ - file, - outputDir, - }); - return this.openai.createTranscription({ - $, - form, - }); - }, - getFullText(transcriptions = []) { - return transcriptions.map((t) => t.text || t).join(" "); - }, - }, - async run({ $ }) { - const { - url, - path, - } = this; - - if (!url && !path) { - throw new ConfigurationError("Must specify either File URL or File Path"); - } - - let file; - - if (path) { - if (!fs.existsSync(path)) { - throw new ConfigurationError(`${path} does not exist`); - } - - file = path; - } else if (url) { - const ext = extname(url); - - const response = await axios({ - method: "GET", - url, - responseType: "stream", - timeout: 250000, - }); - - const bufferStream = new stream.PassThrough(); - response.data.pipe(bufferStream); - - const downloadPath = join("/tmp", `audio${ext}`); - const writeStream = fs.createWriteStream(downloadPath); - - await pipelineAsync(bufferStream, writeStream); - - file = downloadPath; - } - - const transcriptions = await this.chunkFileAndTranscribe({ - file, - $, - }); - - if (transcriptions.length) { - $.export("$summary", "Successfully created transcription"); - } - - return { - transcription: this.getFullText(transcriptions), - transcriptions, - }; - }, -}; diff --git a/components/openai/actions/create-vector-store-file/create-vector-store-file.mjs b/components/openai/actions/create-vector-store-file/create-vector-store-file.mjs new file mode 100644 index 0000000000000..b6b1d4ee79a93 --- /dev/null +++ b/components/openai/actions/create-vector-store-file/create-vector-store-file.mjs @@ -0,0 +1,77 @@ +import openai from "../../openai.app.mjs"; + +export default { + key: "openai-create-vector-store-file", + name: "Create Vector Store File", + description: "Create a vector store file. [See the documentation](https://platform.openai.com/docs/api-reference/vector-stores-files/createFile)", + version: "0.0.3", + type: "action", + props: { + openai, + vectorStoreId: { + propDefinition: [ + openai, + "vectorStoreId", + ], + }, + fileId: { + propDefinition: [ + openai, + "fileId", + ], + }, + chunkingStrategy: { + type: "string", + label: "Chunking Strategy", + description: "The chunking strategy used to chunk the file", + options: [ + "auto", + "static", + ], + optional: true, + reloadProps: true, + }, + }, + additionalProps() { + const props = {}; + + if (this?.chunkingStrategy === "static") { + props.maxChunkSizeTokens = { + type: "integer", + label: "Max Chunk Size Tokens", + description: "The maximum number of tokens in each chunk. The default value is `800`. The minimum value is `100` and the maximum value is `4096`.", + default: 800, + optional: true, + }; + props.chunkOverlapTokens = { + type: "integer", + label: "Chunk Overlap Tokens", + description: "The number of tokens that overlap between chunks. The default value is `400`. Note that the overlap must not exceed half of max_chunk_size_tokens.", + default: 400, + optional: true, + }; + } + + return props; + }, + async run({ $ }) { + const response = await this.openai.createVectorStoreFile({ + $, + vectorStoreId: this.vectorStoreId, + data: { + file_id: this.fileId, + chunking_strategy: this.chunkingStrategy && { + type: this.chunkingStrategy, + static: this.chunkingStrategy === "static" + ? { + max_chunk_size_tokens: this.maxChunkSizeTokens, + chunk_overlap_tokens: this.chunkOverlapTokens, + } + : undefined, + }, + }, + }); + $.export("$summary", `Successfully created vector store file with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/openai/actions/create-vector-store/create-vector-store.mjs b/components/openai/actions/create-vector-store/create-vector-store.mjs new file mode 100644 index 0000000000000..5310772519f77 --- /dev/null +++ b/components/openai/actions/create-vector-store/create-vector-store.mjs @@ -0,0 +1,103 @@ +import openai from "../../openai.app.mjs"; + +export default { + key: "openai-create-vector-store", + name: "Create Vector Store", + description: "Create a vector store. [See the documentation](https://platform.openai.com/docs/api-reference/vector-stores/create)", + version: "0.0.3", + type: "action", + props: { + openai, + fileIds: { + propDefinition: [ + openai, + "fileId", + ], + type: "string[]", + label: "File IDs", + description: "An array of File IDs that the vector store should use", + optional: true, + reloadProps: true, + }, + name: { + type: "string", + label: "Name", + description: "Name of the vector store", + optional: true, + }, + expiryDays: { + type: "integer", + label: "Expiration Days", + description: "The number of days after the `last_active_at` time that the vector store will expire", + optional: true, + }, + metadata: { + type: "object", + label: "Metadata", + description: "Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format", + optional: true, + }, + }, + additionalProps() { + const props = {}; + if (!this.fileIds?.length) { + return props; + } + + props.chunkingStrategy = { + type: "string", + label: "Chunking Strategy", + description: "The chunking strategy used to chunk the file(s)", + options: [ + "auto", + "static", + ], + optional: true, + reloadProps: true, + }; + + if (this?.chunkingStrategy === "static") { + props.maxChunkSizeTokens = { + type: "integer", + label: "Max Chunk Size Tokens", + description: "The maximum number of tokens in each chunk. The default value is `800`. The minimum value is `100` and the maximum value is `4096`.", + default: 800, + optional: true, + }; + props.chunkOverlapTokens = { + type: "integer", + label: "Chunk Overlap Tokens", + description: "The number of tokens that overlap between chunks. The default value is `400`. Note that the overlap must not exceed half of max_chunk_size_tokens.", + default: 400, + optional: true, + }; + } + + return props; + }, + async run({ $ }) { + const response = await this.openai.createVectorStore({ + $, + data: { + file_ids: this.fileIds, + name: this.name, + expires_after: this.expiryDays && { + anchor: "last_active_at", + days: this.expiryDays, + }, + chunking_strategy: this.chunkingStrategy && { + type: this.chunkingStrategy, + static: this.chunkingStrategy === "static" + ? { + max_chunk_size_tokens: this.maxChunkSizeTokens, + chunk_overlap_tokens: this.chunkOverlapTokens, + } + : undefined, + }, + metadata: this.metadata, + }, + }); + $.export("$summary", `Successfully created vector store with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/openai/actions/delete-file/delete-file.mjs b/components/openai/actions/delete-file/delete-file.mjs index 870a84c8429d8..5aadc79614b40 100644 --- a/components/openai/actions/delete-file/delete-file.mjs +++ b/components/openai/actions/delete-file/delete-file.mjs @@ -4,23 +4,24 @@ export default { key: "openai-delete-file", name: "Delete File", description: "Deletes a specified file from OpenAI. [See the documentation](https://platform.openai.com/docs/api-reference/files/delete)", - version: "0.0.6", + version: "0.0.13", type: "action", props: { openai, - file_id: { + fileId: { propDefinition: [ openai, - "file_id", + "fileId", ], }, }, async run({ $ }) { const response = await this.openai.deleteFile({ - file_id: this.file_id, + $, + file_id: this.fileId, }); - $.export("$summary", `Successfully deleted file with ID: ${this.file_id}`); + $.export("$summary", `Successfully deleted file with ID: ${this.fileId}`); return response; }, }; diff --git a/components/openai/actions/delete-vector-store-file/delete-vector-store-file.mjs b/components/openai/actions/delete-vector-store-file/delete-vector-store-file.mjs new file mode 100644 index 0000000000000..44ee5a7ba7d45 --- /dev/null +++ b/components/openai/actions/delete-vector-store-file/delete-vector-store-file.mjs @@ -0,0 +1,36 @@ +import openai from "../../openai.app.mjs"; + +export default { + key: "openai-delete-vector-store-file", + name: "Delete Vector Store File", + description: "Deletes a vector store file. [See the documentation](https://platform.openai.com/docs/api-reference/vector-stores-files/deleteFile)", + version: "0.0.3", + type: "action", + props: { + openai, + vectorStoreId: { + propDefinition: [ + openai, + "vectorStoreId", + ], + }, + vectorStoreFileId: { + propDefinition: [ + openai, + "vectorStoreFileId", + (c) => ({ + vectorStoreId: c.vectorStoreId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.openai.deleteVectorStoreFile({ + $, + vectorStoreId: this.vectorStoreId, + vectorStoreFileId: this.vectorStoreFileId, + }); + $.export("$summary", `Successfully deleted vector store file with ID: ${this.vectorStoreFileId}`); + return response; + }, +}; diff --git a/components/openai/actions/delete-vector-store/delete-vector-store.mjs b/components/openai/actions/delete-vector-store/delete-vector-store.mjs new file mode 100644 index 0000000000000..6e2183928501f --- /dev/null +++ b/components/openai/actions/delete-vector-store/delete-vector-store.mjs @@ -0,0 +1,26 @@ +import openai from "../../openai.app.mjs"; + +export default { + key: "openai-delete-vector-store", + name: "Delete Vector Store", + description: "Delete a vector store. [See the documentation](https://platform.openai.com/docs/api-reference/vector-stores/delete)", + version: "0.0.3", + type: "action", + props: { + openai, + vectorStoreId: { + propDefinition: [ + openai, + "vectorStoreId", + ], + }, + }, + async run({ $ }) { + const response = await this.openai.deleteVectorStore({ + $, + vectorStoreId: this.vectorStoreId, + }); + $.export("$summary", `Successfully deleted vector store with ID: ${this.vectorStoreId}`); + return response; + }, +}; diff --git a/components/openai/actions/list-files/list-files.mjs b/components/openai/actions/list-files/list-files.mjs index 55bf1c02b3fc5..07b8b74ca954d 100644 --- a/components/openai/actions/list-files/list-files.mjs +++ b/components/openai/actions/list-files/list-files.mjs @@ -4,7 +4,7 @@ export default { key: "openai-list-files", name: "List Files", description: "Returns a list of files that belong to the user's organization. [See the documentation](https://platform.openai.com/docs/api-reference/files/list)", - version: "0.0.6", + version: "0.0.13", type: "action", props: { openai, @@ -18,9 +18,10 @@ export default { }, async run({ $ }) { const response = await this.openai.listFiles({ + $, purpose: this.purpose, }); - const summary = `Successfully listed ${response.length} files`; + const summary = `Successfully listed ${response.data.length} files`; $.export("$summary", summary); return response; }, diff --git a/components/openai/actions/list-messages/list-messages.mjs b/components/openai/actions/list-messages/list-messages.mjs index fb1098b8465f8..6e8ce277246fa 100644 --- a/components/openai/actions/list-messages/list-messages.mjs +++ b/components/openai/actions/list-messages/list-messages.mjs @@ -4,7 +4,7 @@ export default { key: "openai-list-messages", name: "List Messages (Assistants)", description: "Lists the messages for a given thread. [See the documentation](https://platform.openai.com/docs/api-reference/messages/listMessages)", - version: "0.0.7", + version: "0.0.14", type: "action", props: { openai, @@ -26,29 +26,26 @@ export default { "order", ], }, - after: { - propDefinition: [ - openai, - "after", - ], - }, - before: { - propDefinition: [ - openai, - "before", - ], - }, }, async run({ $ }) { - const response = await this.openai.listMessages({ - threadId: this.threadId, - limit: this.limit, - order: this.order, - after: this.after, - before: this.before, + const response = this.openai.paginate({ + resourceFn: this.openai.listMessages, + args: { + $, + threadId: this.threadId, + params: { + order: this.order, + }, + }, + max: this.limit, }); - $.export("$summary", `Successfully listed ${response.data.length} messages for thread ID ${this.threadId}`); - return response.data; + const messages = []; + for await (const message of response) { + messages.push(message); + } + + $.export("$summary", `Successfully listed ${messages.length} messages for thread ID ${this.threadId}`); + return messages; }, }; diff --git a/components/openai/actions/list-run-steps/list-run-steps.mjs b/components/openai/actions/list-run-steps/list-run-steps.mjs index 8a3d67b1ab7eb..604de4c8b692f 100644 --- a/components/openai/actions/list-run-steps/list-run-steps.mjs +++ b/components/openai/actions/list-run-steps/list-run-steps.mjs @@ -4,7 +4,7 @@ export default { key: "openai-list-run-steps", name: "List Run Steps (Assistants)", description: "Returns a list of run steps belonging to a run. [See the documentation](https://platform.openai.com/docs/api-reference/runs/list-run-steps)", - version: "0.0.6", + version: "0.0.13", type: "action", props: { openai, @@ -28,41 +28,34 @@ export default { openai, "limit", ], - optional: true, }, order: { propDefinition: [ openai, "order", ], - optional: true, - }, - after: { - propDefinition: [ - openai, - "after", - ], - optional: true, - }, - before: { - propDefinition: [ - openai, - "before", - ], - optional: true, }, }, async run({ $ }) { - const response = await this.openai.listRunSteps({ - threadId: this.threadId, - runId: this.runId, - limit: this.limit, - order: this.order, - after: this.after, - before: this.before, + const response = this.openai.paginate({ + resourceFn: this.openai.listRunSteps, + args: { + $, + threadId: this.threadId, + runId: this.runId, + params: { + order: this.order, + }, + }, + max: this.limit, }); + const runSteps = []; + for await (const runStep of response) { + runSteps.push(runStep); + } + $.export("$summary", `Successfully listed run steps for run ${this.runId}`); - return response; + return runSteps; }, }; diff --git a/components/openai/actions/list-runs/list-runs.mjs b/components/openai/actions/list-runs/list-runs.mjs index f535e4c83e210..1ed17dd1860b3 100644 --- a/components/openai/actions/list-runs/list-runs.mjs +++ b/components/openai/actions/list-runs/list-runs.mjs @@ -4,7 +4,7 @@ export default { key: "openai-list-runs", name: "List Runs (Assistants)", description: "Returns a list of runs belonging to a thread. [See the documentation](https://platform.openai.com/docs/api-reference/runs/list)", - version: "0.0.7", + version: "0.0.14", type: "action", props: { openai, @@ -19,50 +19,33 @@ export default { openai, "limit", ], - optional: true, }, order: { propDefinition: [ openai, "order", ], - optional: true, - }, - after: { - propDefinition: [ - openai, - "after", - ], - }, - before: { - propDefinition: [ - openai, - "before", - ], }, }, async run({ $ }) { - const params = { - ...(this.limit && { - limit: this.limit, - }), - ...(this.order && { - order: this.order, - }), - ...(this.after && { - after: this.after, - }), - ...(this.before && { - before: this.before, - }), - }; - - const response = await this.openai.listRuns({ - threadId: this.threadId, - ...params, + const response = this.openai.paginate({ + resourceFn: this.openai.listRuns, + args: { + $, + threadId: this.threadId, + params: { + order: this.order, + }, + }, + max: this.limit, }); + const runs = []; + for await (const run of response) { + runs.push(run); + } + $.export("$summary", `Successfully retrieved runs for thread ${this.threadId}`); - return response; + return runs; }, }; diff --git a/components/openai/actions/list-vector-store-files/list-vector-store-files.mjs b/components/openai/actions/list-vector-store-files/list-vector-store-files.mjs new file mode 100644 index 0000000000000..daff6a616cba9 --- /dev/null +++ b/components/openai/actions/list-vector-store-files/list-vector-store-files.mjs @@ -0,0 +1,53 @@ +import openai from "../../openai.app.mjs"; + +export default { + key: "openai-list-vector-store-files", + name: "List Vector Store Files", + description: "Returns a list of vector store file. [See the documentation](https://platform.openai.com/docs/api-reference/vector-stores-files/listFiles)", + version: "0.0.3", + type: "action", + props: { + openai, + vectorStoreId: { + propDefinition: [ + openai, + "vectorStoreId", + ], + }, + limit: { + propDefinition: [ + openai, + "limit", + ], + }, + order: { + propDefinition: [ + openai, + "order", + ], + }, + }, + async run({ $ }) { + const response = this.openai.paginate({ + resourceFn: this.openai.listVectorStoreFiles, + args: { + $, + vectorStoreId: this.vectorStoreId, + params: { + order: this.order, + }, + }, + max: this.limit, + }); + + const vectorStoreFiles = []; + for await (const vectorStoreFile of response) { + vectorStoreFiles.push(vectorStoreFile); + } + + $.export("$summary", `Successfully retrieved ${vectorStoreFiles.length} vector store file${vectorStoreFiles.length === 1 + ? "" + : "s"}`); + return vectorStoreFiles; + }, +}; diff --git a/components/openai/actions/list-vector-stores/list-vector-stores.mjs b/components/openai/actions/list-vector-stores/list-vector-stores.mjs new file mode 100644 index 0000000000000..27295b2eca338 --- /dev/null +++ b/components/openai/actions/list-vector-stores/list-vector-stores.mjs @@ -0,0 +1,46 @@ +import openai from "../../openai.app.mjs"; + +export default { + key: "openai-list-vector-stores", + name: "List Vector Stores", + description: "Returns a list of vector stores. [See the documentation](https://platform.openai.com/docs/api-reference/vector-stores/list)", + version: "0.0.3", + type: "action", + props: { + openai, + limit: { + propDefinition: [ + openai, + "limit", + ], + }, + order: { + propDefinition: [ + openai, + "order", + ], + }, + }, + async run({ $ }) { + const response = this.openai.paginate({ + resourceFn: this.openai.listVectorStores, + args: { + $, + params: { + order: this.order, + }, + }, + max: this.limit, + }); + + const vectorStores = []; + for await (const vectorStore of response) { + vectorStores.push(vectorStore); + } + + $.export("$summary", `Successfully retrieved ${vectorStores.length} vector store${vectorStores.length === 1 + ? "" + : "s"}`); + return vectorStores; + }, +}; diff --git a/components/openai/actions/modify-assistant/modify-assistant.mjs b/components/openai/actions/modify-assistant/modify-assistant.mjs index 66ca7a5837551..07f0f30415e63 100644 --- a/components/openai/actions/modify-assistant/modify-assistant.mjs +++ b/components/openai/actions/modify-assistant/modify-assistant.mjs @@ -1,11 +1,12 @@ -import { parseToolsArray } from "../../common/helpers.mjs"; import openai from "../../openai.app.mjs"; +import common from "../common/common-assistants.mjs"; export default { + ...common, key: "openai-modify-assistant", name: "Modify an Assistant", description: "Modifies an existing OpenAI assistant. [See the documentation](https://platform.openai.com/docs/api-reference/assistants/modifyAssistant)", - version: "0.1.3", + version: "0.1.11", type: "action", props: { openai, @@ -43,20 +44,6 @@ export default { ], optional: true, }, - tools: { - propDefinition: [ - openai, - "tools", - ], - optional: true, - }, - file_ids: { - propDefinition: [ - openai, - "file_ids", - ], - optional: true, - }, metadata: { propDefinition: [ openai, @@ -64,18 +51,21 @@ export default { ], optional: true, }, + ...common.props, }, async run({ $ }) { const response = await this.openai.modifyAssistant({ $, assistant: this.assistant, - model: this.model, - name: this.name, - description: this.description, - instructions: this.instructions, - tools: parseToolsArray(this.tools), - file_ids: this.file_ids, - metadata: this.metadata, + data: { + model: this.model, + name: this.name, + description: this.description, + instructions: this.instructions, + tools: this.buildTools(), + tool_resources: this.buildToolResources(), + metadata: this.metadata, + }, }); $.export("$summary", `Successfully modified assistant ${this.assistant}`); return response; diff --git a/components/openai/actions/modify-message/modify-message.mjs b/components/openai/actions/modify-message/modify-message.mjs deleted file mode 100644 index 4ba9d7768ea44..0000000000000 --- a/components/openai/actions/modify-message/modify-message.mjs +++ /dev/null @@ -1,42 +0,0 @@ -import openai from "../../openai.app.mjs"; - -export default { - key: "openai-modify-message", - name: "Modify Message (Assistants)", - description: "Modifies an existing message in a thread. [See the documentation](https://platform.openai.com/docs/api-reference/messages/modifyMessage)", - version: "0.0.7", - type: "action", - props: { - openai, - threadId: { - propDefinition: [ - openai, - "threadId", - ], - }, - messageId: { - propDefinition: [ - openai, - "messageId", - ], - }, - metadata: { - type: "string", - label: "Metadata", - description: "Metadata for the message in JSON string format", - optional: true, - }, - }, - async run({ $ }) { - const response = await this.openai.modifyMessage({ - threadId: this.threadId, - messageId: this.messageId, - metadata: this.metadata - ? JSON.parse(this.metadata) - : undefined, - }); - - $.export("$summary", `Successfully modified message ${this.messageId} in thread ${this.threadId}`); - return response; - }, -}; diff --git a/components/openai/actions/modify-run/modify-run.mjs b/components/openai/actions/modify-run/modify-run.mjs deleted file mode 100644 index be3892f1a0531..0000000000000 --- a/components/openai/actions/modify-run/modify-run.mjs +++ /dev/null @@ -1,45 +0,0 @@ -import openai from "../../openai.app.mjs"; - -export default { - key: "openai-modify-run", - name: "Modify Run (Assistants)", - description: "Modifies an existing run. [See the documentation](https://platform.openai.com/docs/api-reference/runs/modifyRun)", - version: "0.0.6", - type: "action", - props: { - openai, - threadId: { - propDefinition: [ - openai, - "threadId", - ], - }, - runId: { - propDefinition: [ - openai, - "runId", - ({ threadId }) => ({ - threadId, - }), - ], - }, - metadata: { - propDefinition: [ - openai, - "metadata", - ], - }, - }, - async run({ $ }) { - const response = await this.openai.modifyRun({ - threadId: this.threadId, - runId: this.runId, - ...(this.metadata && { - metadata: this.metadata, - }), - }); - - $.export("$summary", `Successfully modified run with ID: ${this.runId}`); - return response; - }, -}; diff --git a/components/openai/actions/retrieve-file-content/retrieve-file-content.mjs b/components/openai/actions/retrieve-file-content/retrieve-file-content.mjs index 207ae889eeab9..35adcf4d256bc 100644 --- a/components/openai/actions/retrieve-file-content/retrieve-file-content.mjs +++ b/components/openai/actions/retrieve-file-content/retrieve-file-content.mjs @@ -1,25 +1,48 @@ import openai from "../../openai.app.mjs"; +import fs from "fs"; export default { key: "openai-retrieve-file-content", name: "Retrieve File Content", description: "Retrieves the contents of the specified file. [See the documentation](https://platform.openai.com/docs/api-reference/files/retrieve-content)", - version: "0.0.6", + version: "0.0.13", type: "action", props: { openai, - file_id: { + fileId: { propDefinition: [ openai, - "file_id", + "fileId", ], }, + fileName: { + type: "string", + label: "Filename", + description: "Optionally, download the file to the `/tmp` directory using the given filename", + optional: true, + }, }, async run({ $ }) { const response = await this.openai.retrieveFileContent({ - file_id: this.file_id, + $, + file_id: this.fileId, + responseType: "arraybuffer", }); - $.export("$summary", `Successfully retrieved file content with ID ${this.file_id}`); - return response; + + if (!this.fileName) { + $.export("$summary", `Successfully retrieved file content with ID ${this.fileId}`); + return response; + } + + const outputFilePath = this.fileName.includes("tmp/") + ? this.fileName + : `/tmp/${this.fileName}`; + await fs.promises.writeFile(outputFilePath, Buffer.from(response)); + const filedata = [ + this.fileName, + outputFilePath, + ]; + $.export("$summary", `Successfully retrieved and downloaded file content with ID ${this.fileId}`); + return filedata; }, }; diff --git a/components/openai/actions/retrieve-file/retrieve-file.mjs b/components/openai/actions/retrieve-file/retrieve-file.mjs index 9aaa174ae9b0d..f88994a6994a9 100644 --- a/components/openai/actions/retrieve-file/retrieve-file.mjs +++ b/components/openai/actions/retrieve-file/retrieve-file.mjs @@ -4,23 +4,24 @@ export default { key: "openai-retrieve-file", name: "Retrieve File", description: "Retrieves a specific file from OpenAI. [See the documentation](https://platform.openai.com/docs/api-reference/files/retrieve)", - version: "0.0.6", + version: "0.0.13", type: "action", props: { openai, - file_id: { + fileId: { propDefinition: [ openai, - "file_id", + "fileId", ], }, }, async run({ $ }) { const response = await this.openai.retrieveFile({ - file_id: this.file_id, + $, + file_id: this.fileId, }); - $.export("$summary", `Successfully retrieved file with ID ${this.file_id}`); + $.export("$summary", `Successfully retrieved file with ID ${this.fileId}`); return response; }, }; diff --git a/components/openai/actions/retrieve-run-step/retrieve-run-step.mjs b/components/openai/actions/retrieve-run-step/retrieve-run-step.mjs index c0b3a379fb3c0..3397c3f080218 100644 --- a/components/openai/actions/retrieve-run-step/retrieve-run-step.mjs +++ b/components/openai/actions/retrieve-run-step/retrieve-run-step.mjs @@ -4,7 +4,7 @@ export default { key: "openai-retrieve-run-step", name: "Retrieve Run Step (Assistants)", description: "Retrieve a specific run step in a thread. [See the documentation](https://platform.openai.com/docs/api-reference/runs/getRunStep)", - version: "0.0.6", + version: "0.0.13", type: "action", props: { openai, @@ -27,11 +27,18 @@ export default { propDefinition: [ openai, "stepId", + ({ + threadId, runId, + }) => ({ + threadId, + runId, + }), ], }, }, async run({ $ }) { const response = await this.openai.retrieveRunStep({ + $, threadId: this.threadId, runId: this.runId, stepId: this.stepId, diff --git a/components/openai/actions/retrieve-run/retrieve-run.mjs b/components/openai/actions/retrieve-run/retrieve-run.mjs index 079d971c9ec33..4f26c51a18876 100644 --- a/components/openai/actions/retrieve-run/retrieve-run.mjs +++ b/components/openai/actions/retrieve-run/retrieve-run.mjs @@ -4,7 +4,7 @@ export default { key: "openai-retrieve-run", name: "Retrieve Run (Assistants)", description: "Retrieves a specific run within a thread. [See the documentation](https://platform.openai.com/docs/api-reference/runs/getRun)", - version: "0.0.6", + version: "0.0.13", type: "action", props: { openai, @@ -26,6 +26,7 @@ export default { }, async run({ $ }) { const response = await this.openai.retrieveRun({ + $, threadId: this.threadId, runId: this.runId, }); diff --git a/components/openai/actions/retrieve-vector-store-file/retrieve-vector-store-file.mjs b/components/openai/actions/retrieve-vector-store-file/retrieve-vector-store-file.mjs new file mode 100644 index 0000000000000..d9b0ee491f0f2 --- /dev/null +++ b/components/openai/actions/retrieve-vector-store-file/retrieve-vector-store-file.mjs @@ -0,0 +1,36 @@ +import openai from "../../openai.app.mjs"; + +export default { + key: "openai-retrieve-vector-store-file", + name: "Retrieve Vector Store File", + description: "Retrieve a vector store file. [See the documentation](https://platform.openai.com/docs/api-reference/vector-stores-files/getFile)", + version: "0.0.3", + type: "action", + props: { + openai, + vectorStoreId: { + propDefinition: [ + openai, + "vectorStoreId", + ], + }, + vectorStoreFileId: { + propDefinition: [ + openai, + "vectorStoreFileId", + (c) => ({ + vectorStoreId: c.vectorStoreId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.openai.getVectorStoreFile({ + $, + vectorStoreId: this.vectorStoreId, + vectorStoreFileId: this.vectorStoreFileId, + }); + $.export("$summary", `Successfully retrieved vector store file with ID: ${this.vectorStoreFileId}`); + return response; + }, +}; diff --git a/components/openai/actions/retrieve-vector-store/retrieve-vector-store.mjs b/components/openai/actions/retrieve-vector-store/retrieve-vector-store.mjs new file mode 100644 index 0000000000000..3330dd9242812 --- /dev/null +++ b/components/openai/actions/retrieve-vector-store/retrieve-vector-store.mjs @@ -0,0 +1,26 @@ +import openai from "../../openai.app.mjs"; + +export default { + key: "openai-retrieve-vector-store", + name: "Retrieve Vector Store", + description: "Retrieve a vector store. [See the documentation](https://platform.openai.com/docs/api-reference/vector-stores/retrieve)", + version: "0.0.3", + type: "action", + props: { + openai, + vectorStoreId: { + propDefinition: [ + openai, + "vectorStoreId", + ], + }, + }, + async run({ $ }) { + const response = await this.openai.getVectorStore({ + $, + vectorStoreId: this.vectorStoreId, + }); + $.export("$summary", `Successfully retrieved vector store with ID: ${this.vectorStoreId}`); + return response; + }, +}; diff --git a/components/openai/actions/send-prompt/send-prompt.mjs b/components/openai/actions/send-prompt/send-prompt.mjs index 63aca46c0cc25..561a2b1a594e6 100644 --- a/components/openai/actions/send-prompt/send-prompt.mjs +++ b/components/openai/actions/send-prompt/send-prompt.mjs @@ -4,12 +4,17 @@ import common from "../common/common.mjs"; export default { ...common, name: "Create Completion (Send Prompt)", - version: "0.1.7", + version: "0.1.15", key: "openai-send-prompt", - description: "OpenAI recommends using the **Chat** action for the latest `gpt-3.5-turbo` API, since it's faster and 10x cheaper. This action creates a completion for the provided prompt and parameters using the older `/completions` API. [See docs here](https://beta.openai.com/docs/api-reference/completions/create)", + description: "OpenAI recommends using the **Chat** action for the latest `gpt-3.5-turbo` API, since it's faster and 10x cheaper. This action creates a completion for the provided prompt and parameters using the older `/completions` API. [See the documentation](https://beta.openai.com/docs/api-reference/completions/create)", type: "action", props: { openai, + alert: { + type: "alert", + alertType: "warning", + content: "We recommend using the Pipedream **Chat** action instead of this one. It supports the latest `gpt-3.5-turbo` API, which is faster and 10x cheaper. This action, **Create Completion (Send Prompt)**, creates a completion for the provided prompt and parameters using the older `/completions` API.", + }, modelId: { propDefinition: [ openai, @@ -38,7 +43,7 @@ export default { async run({ $ }) { const response = await this.openai.createCompletion({ $, - args: this._getCommonArgs(), + data: this._getCommonArgs(), }); if (response) { diff --git a/components/openai/actions/submit-tool-outputs-to-run/submit-tool-outputs-to-run.mjs b/components/openai/actions/submit-tool-outputs-to-run/submit-tool-outputs-to-run.mjs index 3c01d75366c87..88d5fa38085fb 100644 --- a/components/openai/actions/submit-tool-outputs-to-run/submit-tool-outputs-to-run.mjs +++ b/components/openai/actions/submit-tool-outputs-to-run/submit-tool-outputs-to-run.mjs @@ -1,10 +1,11 @@ import openai from "../../openai.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; export default { key: "openai-submit-tool-outputs-to-run", name: "Submit Tool Outputs to Run (Assistants)", description: "Submits tool outputs to a run that requires action. [See the documentation](https://platform.openai.com/docs/api-reference/runs/submitToolOutputs)", - version: "0.0.6", + version: "0.0.13", type: "action", props: { openai, @@ -31,11 +32,25 @@ export default { }, }, async run({ $ }) { - const parsedToolOutputs = this.toolOutputs.map(JSON.parse); + const parsedToolOutputs = typeof this.toolOutputs === "string" + ? JSON.parse(this.toolOutputs) + : Array.isArray(this.toolOutputs) + ? this.toolOutputs.map((output) => typeof output === "string" + ? JSON.parse(output) + : output) + : null; + + if (!parsedToolOutputs) { + throw new ConfigurationError("Could not parse tool outputs as JSON."); + } + const response = await this.openai.submitToolOutputs({ + $, threadId: this.threadId, runId: this.runId, - toolOutputs: parsedToolOutputs, + data: { + tool_outputs: parsedToolOutputs, + }, }); $.export("$summary", `Successfully submitted tool outputs to run ${this.runId}`); diff --git a/components/openai/actions/summarize/summarize.mjs b/components/openai/actions/summarize/summarize.mjs index cf1c19f65629c..7b27e33e278e2 100644 --- a/components/openai/actions/summarize/summarize.mjs +++ b/components/openai/actions/summarize/summarize.mjs @@ -1,11 +1,12 @@ import common from "../common/common-helper.mjs"; +import constants from "../../common/constants.mjs"; export default { ...common, name: "Summarize Text", - version: "0.0.10", + version: "0.1.4", key: "openai-summarize", - description: "Summarizes text using the Chat API", + description: "Summarizes text using the Chat API. [See the documentation](https://platform.openai.com/docs/api-reference/chat)", type: "action", props: { ...common.props, @@ -19,12 +20,7 @@ export default { description: "The length of the summary", type: "string", optional: true, - options: [ - "word", - "sentence", - "paragraph", - "page", - ], + options: constants.SUMMARIZE_LENGTH, }, }, methods: { @@ -46,10 +42,15 @@ export default { if (!messages || !response) { throw new Error("Invalid API output, please reach out to https://pipedream.com/support"); } - return { - summary: response.choices?.[0]?.message?.content, + const output = { messages, }; + if (this.n > 1) { + output.summaries = response.choices?.map(({ message }) => message.content); + } else { + output.summary = response.choices?.[0]?.message?.content; + } + return output; }, }, }; diff --git a/components/openai/actions/translate-text/translate-text.mjs b/components/openai/actions/translate-text/translate-text.mjs index 9357c0d26c106..406d8901d777e 100644 --- a/components/openai/actions/translate-text/translate-text.mjs +++ b/components/openai/actions/translate-text/translate-text.mjs @@ -9,9 +9,9 @@ const langOptions = lang.LANGUAGES.map((l) => ({ export default { ...common, name: "Translate Text (Whisper)", - version: "0.0.12", + version: "0.1.4", key: "openai-translate-text", - description: "Translate text from one language to another using the Chat API", + description: "Translate text from one language to another using the Chat API. [See the documentation](https://platform.openai.com/docs/api-reference/chat)", type: "action", props: { ...common.props, @@ -50,13 +50,17 @@ export default { if (!messages || !response) { throw new Error("Invalid API output, please reach out to https://pipedream.com/support"); } - - return { - translation: response.choices?.[0]?.message?.content, + const output = { source_lang: this.sourceLang, target_lang: this.targetLang, messages, }; + if (this.n > 1) { + output.translations = response.choices?.map(({ message }) => message.content); + } else { + output.translation = response.choices?.[0]?.message?.content; + } + return output; }, }, }; diff --git a/components/openai/actions/upload-file/upload-file.mjs b/components/openai/actions/upload-file/upload-file.mjs index 9c2d3e8de9e39..fc7d68bdd7b0d 100644 --- a/components/openai/actions/upload-file/upload-file.mjs +++ b/components/openai/actions/upload-file/upload-file.mjs @@ -6,7 +6,7 @@ export default { key: "openai-upload-file", name: "Upload File", description: "Upload a file that can be used across various endpoints/features. The size of individual files can be a maximum of 512mb. [See the documentation](https://platform.openai.com/docs/api-reference/files/create)", - version: "0.0.9", + version: "0.0.16", type: "action", props: { openai, diff --git a/components/openai/common/constants.mjs b/components/openai/common/constants.mjs index 45797657298ab..5a016f6ab5532 100644 --- a/components/openai/common/constants.mjs +++ b/components/openai/common/constants.mjs @@ -1,4 +1,4 @@ -export const FINE_TUNING_MODEL_OPTIONS = [ +const FINE_TUNING_MODEL_OPTIONS = [ { label: "gpt-3.5-turbo-1106 (recommended)", value: "gpt-3.5-turbo-1106", @@ -16,8 +16,173 @@ export const FINE_TUNING_MODEL_OPTIONS = [ value: "davinci-002", }, { - label: - "gpt-4-0613 (experimental — eligible users will be presented with an option to request access in the fine-tuning UI)", + label: "gpt-4-0613 (experimental — eligible users will be presented with an option to request access in the fine-tuning UI)", value: "gpt-4-0613", }, ]; + +const TTS_MODELS = [ + "tts-1", + "tts-1-hd", +]; + +const IMAGE_MODELS = [ + "dall-e-2", + "dall-e-3", +]; + +const MODERATION_MODELS = [ + "text-moderation-stable", + "text-moderation-latest", +]; + +const AUDIO_RESPONSE_FORMATS = [ + "mp3", + "opus", + "aac", + "flac", + "wav", + "pcm", +]; + +const CHAT_RESPONSE_FORMAT = { + TEXT: { + label: "Text", + value: "text", + }, + JSON_OBJECT: { + label: "JSON Object", + value: "json_object", + }, + JSON_SCHEMA: { + label: "JSON Schema", + value: "json_schema", + }, +}; + +const IMAGE_RESPONSE_FORMATS = [ + { + label: "URL", + value: "url", + }, + { + label: "Write file to /tmp directory", + value: "tmp", + }, + { + label: "Base64 JSON", + value: "b64_json", + }, +]; + +const USER_OPTIONS = [ + { + label: "User", + value: "user", + }, +]; + +const ORDER_OPTIONS = [ + { + label: "Ascending", + value: "asc", + }, + { + label: "Descending", + value: "desc", + }, +]; + +const TRANSCRIPTION_FORMATS = [ + "json", + "text", + "srt", + "verbose_json", + "vtt", +]; + +const PURPOSES = [ + "fine-tune", + "assistants", + "vision", + "batch", +]; + +const VOICES = [ + "alloy", + "echo", + "fable", + "onyx", + "nova", + "shimmer", +]; + +const IMAGE_QUALITIES = [ + { + label: "Standard", + value: "standard", + }, + { + label: "HD", + value: "hd", + }, +]; + +const IMAGE_STYLES = [ + { + label: "Natural", + value: "natural", + }, + { + label: "Vivid", + value: "vivid", + }, +]; + +const IMAGE_SIZES = [ + "256x256", + "512x512", + "1024x1024", + "1792x1024", + "1024x1792", +]; + +const SUMMARIZE_LENGTH = [ + "word", + "sentence", + "paragraph", + "page", +]; + +const TOOL_TYPES = [ + "code_interpreter", + "file_search", + "function", +]; + +const BATCH_ENDPOINTS = [ + "/v1/chat/completions", + "/v1/embeddings", + "/v1/completions", +]; + +export default { + FINE_TUNING_MODEL_OPTIONS, + TTS_MODELS, + IMAGE_MODELS, + MODERATION_MODELS, + AUDIO_RESPONSE_FORMATS, + CHAT_RESPONSE_FORMAT, + IMAGE_RESPONSE_FORMATS, + USER_OPTIONS, + ORDER_OPTIONS, + TRANSCRIPTION_FORMATS, + PURPOSES, + VOICES, + IMAGE_QUALITIES, + IMAGE_STYLES, + IMAGE_SIZES, + SUMMARIZE_LENGTH, + TOOL_TYPES, + BATCH_ENDPOINTS, +}; diff --git a/components/openai/common/helpers.mjs b/components/openai/common/helpers.mjs index 56fd683e05fc6..0a984c0024aa5 100644 --- a/components/openai/common/helpers.mjs +++ b/components/openai/common/helpers.mjs @@ -1,8 +1,10 @@ +import { ConfigurationError } from "@pipedream/platform"; + export function parseToolsArray(arr) { if (!arr) return undefined; return arr.map((value) => { if ([ - "retrieval", + "file_search", "code_interpreter", ].includes(value)) { return { @@ -16,3 +18,22 @@ export function parseToolsArray(arr) { } }); } + +function emptyStrToUndefined(value) { + const trimmed = typeof(value) === "string" && value.trim(); + return trimmed === "" + ? undefined + : value; +} + +export function parse(value) { + const valueToParse = emptyStrToUndefined(value); + if (typeof(valueToParse) === "object" || valueToParse === undefined) { + return valueToParse; + } + try { + return JSON.parse(valueToParse); + } catch (e) { + throw new ConfigurationError("Make sure the schema contains a valid JSON object."); + } +} diff --git a/components/openai/openai.app.mjs b/components/openai/openai.app.mjs index 3c8bf93af21ad..c7d6c16ed4488 100644 --- a/components/openai/openai.app.mjs +++ b/components/openai/openai.app.mjs @@ -1,5 +1,5 @@ import { axios } from "@pipedream/platform"; -import { FINE_TUNING_MODEL_OPTIONS } from "./common/constants.mjs"; +import constants from "./common/constants.mjs"; export default { type: "app", @@ -12,7 +12,7 @@ export default { async options() { return (await this.getCompletionModels({})).map((model) => model.id); }, - default: "text-davinci-003", + default: "davinci-002", }, chatCompletionModelId: { label: "Model", @@ -21,7 +21,7 @@ export default { async options() { return (await this.getChatCompletionModels({})).map((model) => model.id); }, - default: "gpt-3.5-turbo", + default: "gpt-4o-mini", }, embeddingsModelId: { label: "Model", @@ -30,30 +30,93 @@ export default { async options() { return (await this.getEmbeddingsModels({})).map((model) => model.id); }, - default: "text-embedding-ada-002", + default: "text-embedding-3-small", }, assistantModel: { type: "string", label: "Model", description: "The ID of the model to use for the assistant", async options() { - const models = await this.models({}); - return models.map((model) => ({ - label: model.id, - value: model.id, - })); + const models = (await this.models({})).filter(({ id }) => (id.includes("gpt-3.5-turbo") || id.includes("gpt-4-turbo") || id.includes("gpt-4o")) && (id !== "gpt-3.5-turbo-0301")); + return models.map(({ id }) => id); }, }, assistant: { type: "string", label: "Assistant", description: "Select an assistant to modify", - async options() { - const assistants = await this.listAssistants({}); - return assistants.map((assistant) => ({ - label: assistant.name || assistant.id, - value: assistant.id, - })); + async options({ prevContext }) { + const params = prevContext?.after + ? { + after: prevContext.after, + } + : {}; + const { + data: assistants, last_id: after, + } = await this.listAssistants({ + params, + }); + return { + options: assistants.map((assistant) => ({ + label: assistant.name || assistant.id, + value: assistant.id, + })), + context: { + after, + }, + }; + }, + }, + vectorStoreId: { + type: "string", + label: "Vector Store ID", + description: "The identifier of a vector store", + async options({ prevContext }) { + const params = prevContext?.after + ? { + after: prevContext.after, + } + : {}; + const { + data: vectorStores, last_id: after, + } = await this.listVectorStores({ + params, + }); + return { + options: vectorStores.map((vectorStore) => ({ + label: vectorStore.name || vectorStore.id, + value: vectorStore.id, + })), + context: { + after, + }, + }; + }, + }, + vectorStoreFileId: { + type: "string", + label: "Vector Store File ID", + description: "The identifier of a vector store file", + async options({ + vectorStoreId, prevContext, + }) { + const params = prevContext?.after + ? { + after: prevContext.after, + } + : {}; + const { + data: vectorStoreFiles, last_id: after, + } = await this.listVectorStoreFiles({ + vectorStoreId, + params, + }); + return { + options: vectorStoreFiles.map((vectorStoreFile) => vectorStoreFile.id), + context: { + after, + }, + }; }, }, name: { @@ -66,41 +129,68 @@ export default { type: "string", label: "Description", description: "The description of the assistant.", + optional: true, }, threadId: { type: "string", label: "Thread ID", - description: "The unique identifier for the thread.", + description: "The unique identifier for the thread. Example: `thread_abc123`. To locate the thread ID, make sure your OpenAI Threads setting (Settings -> Organization/Personal -> General -> Features and capabilities -> Threads) is set to \"Visible to organization owners\" or \"Visible to everyone\". You can then access the list of threads and click on individual threads to reveal their IDs", }, runId: { type: "string", label: "Run ID", description: "The unique identifier for the run.", - async options({ threadId }) { + async options({ + threadId, prevContext, + }) { if (!threadId) { return []; } - const { data: runs } = await this.listRuns({ + const params = prevContext?.after + ? { + after: prevContext.after, + } + : {}; + const { + data: runs, last_id: after, + } = await this.listRuns({ threadId, + params, }); - return runs.map(({ id }) => id); + return { + options: runs.map(({ id }) => id), + context: { + after, + }, + }; }, }, stepId: { type: "string", label: "Step ID", description: "The unique identifier for the step.", - }, - assistantId: { - type: "string", - label: "Assistant ID", - description: "The unique identifier for the assistant.", - }, - model: { - type: "string", - label: "Model", - description: "The ID of the model to use.", - optional: true, + async options({ + threadId, runId, prevContext, + }) { + const params = prevContext?.after + ? { + after: prevContext.after, + } + : {}; + const { + data, last_id: after, + } = await this.listRunSteps({ + threadId, + runId, + params, + }); + return { + options: data?.map(({ id }) => id) || [], + context: { + after, + }, + }; + }, }, instructions: { type: "string", @@ -108,18 +198,6 @@ export default { description: "The system instructions that the assistant uses.", optional: true, }, - tools: { - type: "string[]", - label: "Tools", - description: "Each tool should be a valid JSON object. [See the documentation](https://platform.openai.com/docs/api-reference/assistants/createAssistant#assistants-createassistant-tools) for more information. Examples of function tools [can be found here](https://cookbook.openai.com/examples/how_to_call_functions_with_chat_models#basic-concepts).", - optional: true, - }, - file_ids: { - type: "string[]", - label: "File IDs", - description: "A list of [file](https://platform.openai.com/docs/api-reference/files) IDs attached to this assistant.", - optional: true, - }, metadata: { type: "object", label: "Metadata", @@ -131,11 +209,6 @@ export default { label: "Messages", description: "An array of messages to start the thread with.", }, - messageId: { - type: "string", - label: "Message ID", - description: "The ID of the message to modify", - }, content: { type: "string", label: "Content", @@ -145,24 +218,13 @@ export default { type: "string", label: "Role", description: "The role of the entity creating the message", - options: [ - { - label: "User", - value: "user", - }, - ], + options: constants.USER_OPTIONS, default: "user", }, - fileIds: { - type: "string[]", - label: "File IDs", - description: "List of file IDs to attach to the message", - optional: true, - }, toolOutputs: { type: "string[]", label: "Tool Outputs", - description: "The outputs from the tool calls.", + description: "The outputs from the tool calls. Each object in the array should contain properties `tool_call_id` and `output`.", }, limit: { type: "integer", @@ -174,44 +236,21 @@ export default { type: "string", label: "Order", description: "Sort order by the created_at timestamp of the objects.", - options: [ - { - label: "Ascending", - value: "asc", - }, - { - label: "Descending", - value: "desc", - }, - ], - optional: true, - }, - after: { - type: "string", - label: "After", - description: "A cursor for use in pagination to fetch the next set of items.", + options: constants.ORDER_OPTIONS, optional: true, }, - before: { - type: "string", - label: "Before", - description: "A cursor for use in pagination, identifying the message ID to end the list before", - optional: true, - }, - file_id: { + fileId: { type: "string", label: "File ID", description: "The ID of the file to use for this request.", - async options({ prevContext }) { - const files = await this.listFiles({ - purpose: prevContext - ? prevContext.purpose - : undefined, + async options({ purpose }) { + const { data: files } = await this.listFiles({ + purpose: purpose || undefined, }); - return files.map((file) => ({ + return files?.map((file) => ({ label: file.filename, value: file.id, - })); + })) || []; }, }, file: { @@ -223,25 +262,19 @@ export default { type: "string", label: "Purpose", description: "The intended purpose of the uploaded file. Use 'fine-tune' for fine-tuning and 'assistants' for assistants and messages.", - options: [ - "fine-tune", - "assistants", - ], + options: constants.PURPOSES, }, ttsModel: { type: "string", label: "Model", description: "One of the available [TTS models](https://platform.openai.com/docs/models/tts). `tts-1` is optimized for speed, while `tts-1-hd` is optimized for quality.", - options: [ - "tts-1", - "tts-1-hd", - ], + options: constants.TTS_MODELS, }, fineTuningModel: { type: "string", label: "Fine Tuning Model", description: "The name of the model to fine-tune. [See the supported models](https://platform.openai.com/docs/guides/fine-tuning/what-models-can-be-fine-tuned).", - options: FINE_TUNING_MODEL_OPTIONS, + options: constants.FINE_TUNING_MODEL_OPTIONS, }, input: { type: "string", @@ -252,25 +285,13 @@ export default { type: "string", label: "Voice", description: "The voice to use when generating the audio.", - options: [ - "alloy", - "echo", - "fable", - "onyx", - "nova", - "shimmer", - ], + options: constants.VOICES, }, responseFormat: { type: "string", label: "Response Format", - description: "The format to audio in.", - options: [ - "mp3", - "opus", - "aac", - "flac", - ], + description: "The format to generate audio in. Supported formats are mp3, opus, aac, flac, wav, and pcm.", + options: constants.AUDIO_RESPONSE_FORMATS, optional: true, }, speed: { @@ -280,11 +301,6 @@ export default { default: "1.0", optional: true, }, - trainingFile: { - type: "string", - label: "Training File", - description: "The ID of an uploaded file that contains training data. You can use the **Upload File** action and reference the returned ID here.", - }, }, methods: { _apiKey() { @@ -300,22 +316,23 @@ export default { "User-Agent": "@PipedreamHQ/pipedream v1.0", }; }, - _betaHeaders() { + _betaHeaders(version = "v2") { return { ...this._commonHeaders(), - "OpenAI-Beta": "assistants=v1", + "OpenAI-Beta": `assistants=${version}`, }; }, - async _makeRequest({ + _makeRequest({ $ = this, path, + headers, ...args } = {}) { return axios($, { ...args, url: `${this._baseApiUrl()}${path}`, headers: { - ...args.headers, + ...headers, ...this._commonHeaders(), }, maxBodyLength: Infinity, @@ -332,7 +349,7 @@ export default { const models = await this.models({ $, }); - return models.filter((model) => model.id.match(/turbo|gpt/gi)); + return models.filter((model) => model.id.match(/4o|o[1-9]/gi)); }, async getCompletionModels({ $ }) { const models = await this.models({ @@ -352,18 +369,17 @@ export default { return models.filter((model) => { const { id } = model; return ( - id.match(/^(text-embedding-ada-002|.*-(davinci|curie|babbage|ada)-.*-001)$/gm) + id.match(/^(text-embedding-ada-002|text-embedding-3.*|.*-(davinci|curie|babbage|ada)-.*-001)$/gm) ); }); }, async _makeCompletion({ - $, path, args, + path, ...args }) { const data = await this._makeRequest({ - $, path, method: "POST", - data: args, + ...args, }); // For completions, return the text of the first choice at the top-level @@ -385,45 +401,33 @@ export default { ...data, }; }, - async createCompletion({ - $, args, - }) { + createCompletion(args = {}) { return this._makeCompletion({ - $, path: "/completions", - args, + ...args, }); }, - async createChatCompletion({ - $, args, - }) { + createChatCompletion(args = {}) { return this._makeCompletion({ - $, path: "/chat/completions", - args, + ...args, }); }, - async createImage({ - $, args, - }) { + createImage(args = {}) { return this._makeRequest({ - $, path: "/images/generations", - data: args, method: "POST", + ...args, }); }, - async createEmbeddings({ - $, args, - }) { + createEmbeddings(args = {}) { return this._makeRequest({ - $, path: "/embeddings", - data: args, method: "POST", + ...args, }); }, - async createTranscription({ + createTranscription({ $, form, }) { return this._makeRequest({ @@ -437,85 +441,41 @@ export default { data: form, }); }, - async listAssistants({ $ }) { - const { data: assistants } = await this._makeRequest({ - $, + listAssistants(args = {}) { + return this._makeRequest({ path: "/assistants", headers: this._betaHeaders(), + ...args, }); - return assistants; - }, - async createAssistant({ - $, - model, - name, - description, - instructions, - tools, - file_ids, - metadata, - }) { + }, + createAssistant(args = {}) { return this._makeRequest({ - $, method: "POST", path: "/assistants", - headers: this._betaHeaders(), - data: { - model, - name, - description, - instructions, - tools, - file_ids, - metadata, - }, + headers: this._betaHeaders("v2"), + ...args, }); }, - async modifyAssistant({ - $, - assistant, - model, - name, - description, - instructions, - tools, - file_ids, - metadata, + modifyAssistant({ + assistant, ...args }) { return this._makeRequest({ - $, method: "POST", path: `/assistants/${assistant}`, - headers: this._betaHeaders(), - data: { - model, - name, - description, - instructions, - tools, - file_ids, - metadata, - }, + headers: this._betaHeaders("v2"), + ...args, }); }, - async createThread({ - $, - messages, - metadata, - }) { + createThread(args = {}) { return this._makeRequest({ - $, method: "POST", path: "/threads", - headers: this._betaHeaders(), - data: { - messages, - metadata, - }, + headers: this._betaHeaders("v2"), + ...args, }); }, - async createMessage({ - threadId, content, role, fileIds, metadata, + createMessage({ + threadId, metadata, ...args }) { const parsedMetadata = metadata ? JSON.parse(metadata) @@ -525,29 +485,23 @@ export default { path: `/threads/${threadId}/messages`, headers: this._betaHeaders(), data: { - role, - content, - file_ids: fileIds, + ...args.data, metadata: parsedMetadata, }, + ...args, }); }, - async listMessages({ - threadId, limit, order, after, before, + listMessages({ + threadId, ...args }) { return this._makeRequest({ path: `/threads/${threadId}/messages`, - headers: this._betaHeaders(), - params: { - limit, - order, - after, - before, - }, + headers: this._betaHeaders("v2"), + ...args, }); }, - async modifyMessage({ - threadId, messageId, metadata, + modifyMessage({ + threadId, messageId, metadata, ...args }) { const parsedMetadata = metadata ? JSON.parse(metadata) @@ -559,111 +513,107 @@ export default { data: { metadata: parsedMetadata, }, + ...args, }); }, - async createRun({ - threadId, assistantId, ...opts + createRun({ + threadId, ...args }) { return this._makeRequest({ path: `/threads/${threadId}/runs`, method: "POST", - headers: this._betaHeaders(), - data: { - assistant_id: assistantId, - ...opts, - }, + headers: this._betaHeaders("v2"), + ...args, }); }, - async retrieveRun({ - threadId, runId, + retrieveRun({ + threadId, runId, ...args }) { return this._makeRequest({ - headers: this._betaHeaders(), + headers: this._betaHeaders("v2"), path: `/threads/${threadId}/runs/${runId}`, + ...args, }); }, - async modifyRun({ - threadId, runId, ...opts + modifyRun({ + threadId, runId, data, ...args }) { return this._makeRequest({ path: `/threads/${threadId}/runs/${runId}`, headers: this._betaHeaders(), method: "POST", - data: opts, + data, + ...args, }); }, - async listRuns({ - threadId, ...opts + listRuns({ + threadId, ...args }) { return this._makeRequest({ path: `/threads/${threadId}/runs`, headers: this._betaHeaders(), - params: opts, + ...args, }); }, - async submitToolOutputs({ - threadId, runId, toolOutputs, + submitToolOutputs({ + threadId, runId, ...args }) { - // Assuming toolOutputs should be parsed as JSON objects - const parsedToolOutputs = toolOutputs.map(JSON.parse); return this._makeRequest({ path: `/threads/${threadId}/runs/${runId}/submit_tool_outputs`, headers: this._betaHeaders(), method: "POST", - data: { - tool_outputs: parsedToolOutputs, - }, + ...args, }); }, - async cancelRun({ - threadId, runId, + cancelRun({ + threadId, runId, ...args }) { return this._makeRequest({ path: `/threads/${threadId}/runs/${runId}/cancel`, - headers: this._betaHeaders(), + headers: this._betaHeaders("v2"), method: "POST", + ...args, }); }, - async createThreadAndRun({ - assistantId, ...opts - }) { + createThreadAndRun(args = {}) { return this._makeRequest({ path: "/threads/runs", - headers: this._betaHeaders(), + headers: this._betaHeaders("v2"), method: "POST", - data: { - assistant_id: assistantId, - ...opts, - }, + ...args, }); }, - async retrieveRunStep({ - threadId, runId, stepId, + retrieveRunStep({ + threadId, runId, stepId, ...args }) { return this._makeRequest({ path: `/threads/${threadId}/runs/${runId}/steps/${stepId}`, headers: this._betaHeaders(), + ...args, }); }, - async listRunSteps({ - threadId, runId, ...opts + listRunSteps({ + threadId, runId, ...args }) { return this._makeRequest({ path: `/threads/${threadId}/runs/${runId}/steps`, headers: this._betaHeaders(), - params: opts, + ...args, }); }, - async listFiles({ purpose } = {}) { + listFiles({ + purpose, ...args + } = {}) { return this._makeRequest({ path: "/files", headers: this._betaHeaders(), params: { purpose, }, + ...args, }); }, - async uploadFile(args) { + uploadFile(args) { return this._makeRequest({ method: "POST", path: "/files", @@ -674,44 +624,172 @@ export default { }, }); }, - async deleteFile({ file_id }) { + deleteFile({ + file_id, ...args + }) { return this._makeRequest({ method: "DELETE", headers: this._betaHeaders(), path: `/files/${file_id}`, + ...args, }); }, - async retrieveFile({ file_id }) { + retrieveFile({ + file_id, ...args + }) { return this._makeRequest({ headers: this._betaHeaders(), path: `/files/${file_id}`, + ...args, }); }, - async retrieveFileContent({ file_id }) { + retrieveFileContent({ + file_id, ...args + }) { return this._makeRequest({ - headers: this._betaHeaders(), + headers: this._betaHeaders("v2"), path: `/files/${file_id}/content`, + ...args, }); }, - async listFineTuningJobs(args) { + listFineTuningJobs(args = {}) { return this._makeRequest({ path: "/fine_tuning/jobs", ...args, }); }, - async createSpeech(args) { + createSpeech(args = {}) { return this._makeRequest({ path: "/audio/speech", method: "POST", ...args, }); }, - async createFineTuningJob(args) { + createFineTuningJob(args = {}) { return this._makeRequest({ path: "/fine_tuning/jobs", method: "POST", ...args, }); }, + getVectorStore({ + vectorStoreId, ...args + }) { + return this._makeRequest({ + path: `/vector_stores/${vectorStoreId}`, + headers: this._betaHeaders("v2"), + ...args, + }); + }, + listVectorStores(args = {}) { + return this._makeRequest({ + path: "/vector_stores", + headers: this._betaHeaders("v2"), + ...args, + }); + }, + createVectorStore(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/vector_stores", + headers: this._betaHeaders("v2"), + ...args, + }); + }, + deleteVectorStore({ + vectorStoreId, ...args + }) { + return this._makeRequest({ + method: "DELETE", + path: `/vector_stores/${vectorStoreId}`, + headers: this._betaHeaders("v2"), + ...args, + }); + }, + getVectorStoreFile({ + vectorStoreId, vectorStoreFileId, ...args + }) { + return this._makeRequest({ + path: `/vector_stores/${vectorStoreId}/files/${vectorStoreFileId}`, + headers: this._betaHeaders("v2"), + ...args, + }); + }, + listVectorStoreFiles({ + vectorStoreId, ...args + }) { + return this._makeRequest({ + path: `/vector_stores/${vectorStoreId}/files`, + headers: this._betaHeaders("v2"), + ...args, + }); + }, + createVectorStoreFile({ + vectorStoreId, ...args + }) { + return this._makeRequest({ + method: "POST", + path: `/vector_stores/${vectorStoreId}/files`, + headers: this._betaHeaders("v2"), + ...args, + }); + }, + deleteVectorStoreFile({ + vectorStoreId, vectorStoreFileId, ...args + }) { + return this._makeRequest({ + method: "DELETE", + path: `/vector_stores/${vectorStoreId}/files/${vectorStoreFileId}`, + headers: this._betaHeaders("v2"), + ...args, + }); + }, + createModeration(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/moderations", + ...args, + }); + }, + createBatch(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/batches", + ...args, + }); + }, + listBatches(args = {}) { + return this._makeRequest({ + path: "/batches", + ...args, + }); + }, + async *paginate({ + resourceFn, + args = {}, + max, + }) { + args = { + ...args, + params: { + ...args.params, + }, + }; + let hasMore, count = 0; + do { + const { + data, last_id: after, + } = await resourceFn(args); + for (const item of data) { + yield item; + count++; + if (max && count >= max) { + return; + } + } + hasMore = data?.length; + args.params.after = after; + } while (hasMore); + }, }, }; diff --git a/components/openai/package.json b/components/openai/package.json index daeab49b41e6a..12c47625d4832 100644 --- a/components/openai/package.json +++ b/components/openai/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/openai", - "version": "0.5.1", + "version": "0.8.1", "description": "Pipedream OpenAI Components", "main": "openai.app.mjs", "keywords": [ @@ -14,14 +14,13 @@ "access": "public" }, "dependencies": { - "@ffmpeg-installer/ffmpeg": "^1.1.0", - "@pipedream/platform": "^1.2.1", + "@pipedream/platform": "^3.0.3", "@pipedream/types": "^0.1.4", "axios": "^1.6.2", "bottleneck": "^2.19.5", "form-data": "^4.0.0", "got": "^12.6.0", - "openai": "^3.2.1" + "openai": "^4.77.0" }, "devDependencies": { "@types/node": "^17.0.45" diff --git a/components/openai/sources/common.mjs b/components/openai/sources/common.mjs deleted file mode 100644 index 18227121c5040..0000000000000 --- a/components/openai/sources/common.mjs +++ /dev/null @@ -1,51 +0,0 @@ -import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; -import openai from "../openai.app.mjs"; - -export default { - props: { - openai, - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - }, - methods: { - _getSavedItems() { - return this.db.get("savedItems") ?? []; - }, - _setSavedItems(value) { - this.db.set("savedItems", value); - }, - getMeta() { - throw new Error("No item metadata implemented!"); - }, - async getData() { - throw new Error("No item fetching implemented!"); - }, - async getAndProcessItems(maxEvents) { - const savedItems = this._getSavedItems(); - const { data } = await this.getData(); - data - ?.filter(({ id }) => !savedItems.includes(id)) - .reverse() - .forEach((item, index) => { - if (!maxEvents || index < maxEvents) { - this.$emit(item, this.getMeta(item)); - } - savedItems.push(item.id); - }); - this._setSavedItems(savedItems); - }, - }, - hooks: { - async deploy() { - await this.getAndProcessItems(10); - }, - }, - async run() { - await this.getAndProcessItems(); - }, -}; diff --git a/components/openai/sources/common/common.mjs b/components/openai/sources/common/common.mjs new file mode 100644 index 0000000000000..32b45c8d88d89 --- /dev/null +++ b/components/openai/sources/common/common.mjs @@ -0,0 +1,53 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import openai from "../../openai.app.mjs"; + +export default { + props: { + openai, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastCreated() { + return this.db.get("lastCreated") || 0; + }, + _setLastCreated(lastCreated) { + this.db.set("lastCreated", lastCreated); + }, + getMeta() { + throw new Error("No item metadata implemented!"); + }, + async getData() { + throw new Error("No item fetching implemented!"); + }, + async getAndProcessItems(maxEvents) { + const lastCreated = this._getLastCreated(); + const { data } = await this.getData(); + if (!data?.length) { + return; + } + this._setLastCreated(data[0].created_at); + data + ?.filter(({ created_at }) => created_at >= lastCreated) + .reverse() + .forEach((item, index) => { + if (!maxEvents || index < maxEvents) { + this.$emit(item, this.getMeta(item)); + } + }); + }, + }, + hooks: { + async deploy() { + await this.getAndProcessItems(10); + }, + }, + async run() { + await this.getAndProcessItems(); + }, +}; diff --git a/components/openai/sources/new-batch-completed/new-batch-completed.mjs b/components/openai/sources/new-batch-completed/new-batch-completed.mjs new file mode 100644 index 0000000000000..b644f4e6d9678 --- /dev/null +++ b/components/openai/sources/new-batch-completed/new-batch-completed.mjs @@ -0,0 +1,46 @@ +import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "openai-new-batch-completed", + name: "New Batch Completed", + description: "Emit new event when a new batch is completed in OpenAI. [See the documentation](https://platform.openai.com/docs/api-reference/batch/list)", + version: "0.0.7", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + _getLastCompleted() { + return this.db.get("lastCompleted") || 0; + }, + _setLastCompleted(lastCompleted) { + this.db.set("lastCompleted", lastCompleted); + }, + getMeta(batch) { + return { + id: batch.id, + summary: `Batch Completed with ID ${batch.id}`, + ts: batch.completed_at * 1000, + }; + }, + async getAndProcessItems(max) { + const lastCompleted = this._getLastCompleted(); + let maxCompleted = lastCompleted; + const results = this.openai.paginate({ + resourceFn: this.openai.listBatches, + max, + }); + const batches = []; + for await (const batch of results) { + if (batch.completed_at && batch.completed_at > lastCompleted) { + batches.push(batch); + maxCompleted = Math.max(batch.completed_at, maxCompleted); + } + } + this._setLastCompleted(maxCompleted); + batches.reverse().forEach((item) => this.$emit(item, this.getMeta(item))); + }, + }, + sampleEmit, +}; diff --git a/components/openai/sources/new-batch-completed/test-event.mjs b/components/openai/sources/new-batch-completed/test-event.mjs new file mode 100644 index 0000000000000..78dd34e0b1cb7 --- /dev/null +++ b/components/openai/sources/new-batch-completed/test-event.mjs @@ -0,0 +1,29 @@ +export default { + "id": "batch_abc123", + "object": "batch", + "endpoint": "/v1/completions", + "errors": null, + "input_file_id": "file-abc123", + "completion_window": "24h", + "status": "completed", + "output_file_id": "file-cvaTdG", + "error_file_id": "file-HOWS94", + "created_at": 1711471533, + "in_progress_at": 1711471538, + "expires_at": 1711557933, + "finalizing_at": 1711493133, + "completed_at": 1711493163, + "failed_at": null, + "expired_at": null, + "cancelling_at": null, + "cancelled_at": null, + "request_counts": { + "total": 100, + "completed": 95, + "failed": 5 + }, + "metadata": { + "customer_id": "user_123456789", + "batch_description": "Nightly eval job", + } +} \ No newline at end of file diff --git a/components/openai/sources/new-file-created/new-file-created.mjs b/components/openai/sources/new-file-created/new-file-created.mjs index df1fa895ea706..069f8409e265e 100644 --- a/components/openai/sources/new-file-created/new-file-created.mjs +++ b/components/openai/sources/new-file-created/new-file-created.mjs @@ -1,11 +1,12 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "openai-new-file-created", name: "New File Created", description: "Emit new event when a new file is created in OpenAI. [See the documentation](https://platform.openai.com/docs/api-reference/files/list)", - version: "0.0.5", + version: "0.0.12", type: "source", dedupe: "unique", props: { @@ -21,7 +22,7 @@ export default { }, methods: { ...common.methods, - async getData() { + getData() { return this.openai.listFiles({ purpose: this.purpose, }); @@ -34,4 +35,5 @@ export default { }; }, }, + sampleEmit, }; diff --git a/components/openai/sources/new-file-created/test-event.mjs b/components/openai/sources/new-file-created/test-event.mjs new file mode 100644 index 0000000000000..53c7049ae5a66 --- /dev/null +++ b/components/openai/sources/new-file-created/test-event.mjs @@ -0,0 +1,10 @@ +export default { + "id": "file-abc123", + "object": "file", + "bytes": 140, + "created_at": 1613779121, + "filename": "puppy.jsonl", + "purpose": "fine-tune", + "status": "processed", + "status_details": null +} \ No newline at end of file diff --git a/components/openai/sources/new-fine-tuning-job-created/new-fine-tuning-job-created.mjs b/components/openai/sources/new-fine-tuning-job-created/new-fine-tuning-job-created.mjs index 6ac38a4989e79..8c0649f1a5729 100644 --- a/components/openai/sources/new-fine-tuning-job-created/new-fine-tuning-job-created.mjs +++ b/components/openai/sources/new-fine-tuning-job-created/new-fine-tuning-job-created.mjs @@ -1,16 +1,17 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "openai-new-fine-tuning-job-created", name: "New Fine Tuning Job Created", description: "Emit new event when a new fine-tuning job is created in OpenAI. [See the documentation](https://platform.openai.com/docs/api-reference/fine-tuning/list)", - version: "0.0.5", + version: "0.0.12", type: "source", dedupe: "unique", methods: { ...common.methods, - async getData() { + getData() { return this.openai.listFineTuningJobs(); }, getMeta(item) { @@ -21,4 +22,5 @@ export default { }; }, }, + sampleEmit, }; diff --git a/components/openai/sources/new-fine-tuning-job-created/test-event.mjs b/components/openai/sources/new-fine-tuning-job-created/test-event.mjs new file mode 100644 index 0000000000000..a4816b7e2aaf3 --- /dev/null +++ b/components/openai/sources/new-fine-tuning-job-created/test-event.mjs @@ -0,0 +1,19 @@ +export default { + "object": "fine_tuning.job", + "id": "ftjob-vbCfZSaivshuV18SjpCQwu2I", + "model": "gpt-3.5-turbo-0125", + "created_at": 1715284148, + "finished_at": null, + "fine_tuned_model": null, + "organization_id": "org-IQsNYSuAzzwvzXiKFGa1BeR4", + "result_files": [], + "status": "failed", + "validation_file": null, + "training_file": "file-D1svkZhYUtI4E8IpYsmIs3qY", + "hyperparameters": null, + "trained_tokens": null, + "user_provided_suffix": null, + "seed": 66573047, + "estimated_finish": null, + "integrations": [] +} \ No newline at end of file diff --git a/components/openai/sources/new-run-state-changed/new-run-state-changed.mjs b/components/openai/sources/new-run-state-changed/new-run-state-changed.mjs index f1cd98852d8db..add3270a5b8b2 100644 --- a/components/openai/sources/new-run-state-changed/new-run-state-changed.mjs +++ b/components/openai/sources/new-run-state-changed/new-run-state-changed.mjs @@ -1,11 +1,12 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "openai-new-run-state-changed", name: "New Run State Changed", description: "Emit new event every time a run changes its status. [See the documentation](https://platform.openai.com/docs/api-reference/runs/listRuns)", - version: "0.0.1", + version: "0.0.8", type: "source", dedupe: "unique", props: { @@ -81,4 +82,5 @@ export default { setStatusItems(buildStatusItems(data)); }, }, + sampleEmit, }; diff --git a/components/openai/sources/new-run-state-changed/test-event.mjs b/components/openai/sources/new-run-state-changed/test-event.mjs new file mode 100644 index 0000000000000..64c87a5e0766b --- /dev/null +++ b/components/openai/sources/new-run-state-changed/test-event.mjs @@ -0,0 +1,36 @@ +export default { + "id": "run_RW7H8ICHslGIXBCBpgXn4OJy", + "object": "thread.run", + "created_at": 1715284963, + "assistant_id": "asst_aHkRGOagtHiTJJF7hNqLqcrv", + "thread_id": "thread_IqZAvwcU3SVsLeFeJ7fkx6LN", + "status": "completed", + "started_at": 1715284963, + "expires_at": null, + "cancelled_at": null, + "failed_at": null, + "completed_at": 1715284966, + "required_action": null, + "last_error": null, + "model": "gpt-4-turbo", + "instructions": "", + "tools": [], + "file_ids": [], + "metadata": {}, + "temperature": 1, + "top_p": 1, + "max_completion_tokens": null, + "max_prompt_tokens": null, + "truncation_strategy": { + "type": "auto", + "last_messages": null + }, + "incomplete_details": null, + "usage": { + "prompt_tokens": 25, + "completion_tokens": 25, + "total_tokens": 50 + }, + "response_format": "auto", + "tool_choice": "auto" +} \ No newline at end of file diff --git a/components/opencage/README.md b/components/opencage/README.md index 5a1c1fc16283d..78f60d154a59a 100644 --- a/components/opencage/README.md +++ b/components/opencage/README.md @@ -1,17 +1,11 @@ # Overview -You can build a variety of different applications using the OpenCage API. With -it, you can develop geocoding, time zone, reverse geocoding, and structured -data lookup applications. It offers an easy-to-use API, with a variety of -language bindings, that makes it easy to work with geographic data. +The OpenCage API provides geocoding services, converting coordinates to readable addresses (reverse geocoding) and vice versa (forward geocoding). It's valuable for apps requiring geolocation data, such as mapping, logistics, or location-based services. In Pipedream, it can be used to create workflows that react to location-based events or enrich datasets with geographical information. -Some possible applications that can be built with the OpenCage API include: +# Example Use Cases -- An address lookup system -- A mapping application -- A location picker for mobile applications -- A search engine for address data -- An application for finding nearby businesses -- A system to map areas by zip code -- An application to find local points of interest -- A system to determine the time zone of a specific location +- **Event-Based Location Logging**: Trigger a Pipedream workflow with a webhook from an IoT device. Use OpenCage to convert the latitude and longitude to a human-readable address. Log the address and device information to a Google Sheets spreadsheet for easy tracking and visualization. + +- **User Address Verification**: When a user signs up on your platform and provides their address, use a Pipedream workflow to verify it by converting it to coordinates with OpenCage. Compare the results with the provided information to help with fraud detection or data consistency checks. + +- **Dynamic Content Personalization**: Customize content based on user location. Use OpenCage to reverse geocode a user's IP address captured from a website visit. Based on the location, tailor email marketing campaigns in Pipedream by integrating with SendGrid, providing relevant offers or regional news to the user. diff --git a/components/opencage/package.json b/components/opencage/package.json new file mode 100644 index 0000000000000..df1a7357d1602 --- /dev/null +++ b/components/opencage/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/opencage", + "version": "0.6.0", + "description": "Pipedream opencage Components", + "main": "opencage.app.mjs", + "keywords": [ + "pipedream", + "opencage" + ], + "homepage": "https://pipedream.com/apps/opencage", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/opengraph_io/README.md b/components/opengraph_io/README.md new file mode 100644 index 0000000000000..25c3ea4999801 --- /dev/null +++ b/components/opengraph_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The OpenGraph.io API enables you to fetch Open Graph data from websites, which is useful for previewing content like images, titles, and descriptions the way social platforms do. Integrating OpenGraph.io with Pipedream allows you to automate the retrieval of this data in response to various triggers and to use it in workflows that involve other apps and services. + +# Example Use Cases + +- **Content Preview on Social Media Posts**: Automatically fetch Open Graph data from URLs within a dataset or a stream of user-generated content. Then, use this data to create rich social media posts on platforms like Twitter or Facebook that include images, titles, and descriptions for enhanced engagement. + +- **Enhanced Bookmarking for Read-It-Later Services**: In a workflow where users save URLs to read later, utilize OpenGraph.io to retrieve metadata and create a more informative and visually appealing list of saved articles. This could be combined with data stores in Pipedream to keep track of user-saved URLs and their metadata. + +- **SEO Analysis and Reporting**: Automate the collection of Open Graph data for a set of URLs as part of an SEO tool or service. You can feed this data into a report generator or an analysis tool to help understand how well a site's content is optimized for social sharing, potentially coupling it with Google Sheets for data storage and visualization. diff --git a/components/openperplex/actions/get-website-screenshot/get-website-screenshot.mjs b/components/openperplex/actions/get-website-screenshot/get-website-screenshot.mjs new file mode 100644 index 0000000000000..3ce5f6fc8037b --- /dev/null +++ b/components/openperplex/actions/get-website-screenshot/get-website-screenshot.mjs @@ -0,0 +1,27 @@ +import openperplex from "../../openperplex.app.mjs"; + +export default { + key: "openperplex-get-website-screenshot", + name: "Get Website Screenshot", + description: "Get a screenshot of a website using Openperplex. [See the documentation](https://docs.openperplex.com/api-reference/endpoint/get-website-screenshot)", + version: "0.0.1", + type: "action", + props: { + openperplex, + url: { + type: "string", + label: "URL", + description: "The URL to create a screenshot of", + }, + }, + async run({ $ }) { + const response = await this.openperplex.getWebsiteScreenshot({ + $, + params: { + url: this.url, + }, + }); + $.export("$summary", "Successfully created screenshot"); + return response; + }, +}; diff --git a/components/openperplex/actions/query-from-url/query-from-url.mjs b/components/openperplex/actions/query-from-url/query-from-url.mjs new file mode 100644 index 0000000000000..8be0487f26c9d --- /dev/null +++ b/components/openperplex/actions/query-from-url/query-from-url.mjs @@ -0,0 +1,48 @@ +import openperplex from "../../openperplex.app.mjs"; + +export default { + key: "openperplex-query-from-url", + name: "Query From URL", + description: "Queries content from a specific URL using Openperplex. [See the documentation](https://docs.openperplex.com/api-reference/endpoint/query-from-url)", + version: "0.0.1", + type: "action", + props: { + openperplex, + url: { + type: "string", + label: "URL", + description: "The URL to search", + }, + query: { + propDefinition: [ + openperplex, + "query", + ], + }, + responseLanguage: { + propDefinition: [ + openperplex, + "responseLanguage", + ], + }, + answerType: { + propDefinition: [ + openperplex, + "answerType", + ], + }, + }, + async run({ $ }) { + const response = await this.openperplex.queryFromUrl({ + $, + params: { + url: this.url, + query: this.query, + response_language: this.responseLanguage, + answer_type: this.answerType, + }, + }); + $.export("$summary", "Successfully performed search"); + return response; + }, +}; diff --git a/components/openperplex/actions/simple-search/simple-search.mjs b/components/openperplex/actions/simple-search/simple-search.mjs new file mode 100644 index 0000000000000..ed71f0adea3fb --- /dev/null +++ b/components/openperplex/actions/simple-search/simple-search.mjs @@ -0,0 +1,56 @@ +import openperplex from "../../openperplex.app.mjs"; + +export default { + key: "openperplex-simple-search", + name: "Simple Search", + description: "Perform a simple search using Openperplex. [See the documentation](https://docs.openperplex.com/api-reference/endpoint/search-simple)", + version: "0.0.1", + type: "action", + props: { + openperplex, + query: { + propDefinition: [ + openperplex, + "query", + ], + }, + dateContext: { + type: "string", + label: "Date Context", + description: "A date for context. Format: `YYYY-MM-DD` or `YYYY-MM-DD HH:MM AM/PM`. If not provided, the API uses the current date.", + optional: true, + }, + location: { + propDefinition: [ + openperplex, + "location", + ], + }, + responseLanguage: { + propDefinition: [ + openperplex, + "responseLanguage", + ], + }, + answerType: { + propDefinition: [ + openperplex, + "answerType", + ], + }, + }, + async run({ $ }) { + const response = await this.openperplex.simpleSearch({ + $, + params: { + query: this.query, + date_context: this.dateContext, + location: this.location, + response_language: this.responseLanguage, + answer_type: this.answerType, + }, + }); + $.export("$summary", "Successfully performed search"); + return response; + }, +}; diff --git a/components/openperplex/common/constants.mjs b/components/openperplex/common/constants.mjs new file mode 100644 index 0000000000000..f36c8f93b2ac1 --- /dev/null +++ b/components/openperplex/common/constants.mjs @@ -0,0 +1,241 @@ +const ANSWER_TYPES = [ + "text", + "markdown", + "html", +]; + +const COUNTRIES = [ + { + label: "United States", + value: "us", + }, + { + label: "Canada", + value: "ca", + }, + { + label: "United Kingdom", + value: "uk", + }, + { + label: "Mexico", + value: "mx", + }, + { + label: "Spain", + value: "es", + }, + { + label: "Germany", + value: "de", + }, + { + label: "France", + value: "fr", + }, + { + label: "Belgium", + value: "be", + }, + { + label: "Portugal", + value: "pt", + }, + { + label: "Netherlands", + value: "nl", + }, + { + label: "Turkey", + value: "tr", + }, + { + label: "Italy", + value: "it", + }, + { + label: "Poland", + value: "pl", + }, + { + label: "Russia", + value: "ru", + }, + { + label: "South Africa", + value: "za", + }, + { + label: "United Arab Emirates", + value: "ae", + }, + { + label: "Saudi Arabia", + value: "sa", + }, + { + label: "Argentina", + value: "ar", + }, + { + label: "Brazil", + value: "br", + }, + { + label: "Australia", + value: "au", + }, + { + label: "China", + value: "cn", + }, + { + label: "Korea", + value: "kr", + }, + { + label: "Japan", + value: "jp", + }, + { + label: "India", + value: "in", + }, + { + label: "Palestine", + value: "ps", + }, + { + label: "Kuwait", + value: "kw", + }, + { + label: "Oman", + value: "om", + }, + { + label: "Qatar", + value: "qa", + }, + { + label: "Israel", + value: "il", + }, + { + label: "Morocco", + value: "ma", + }, + { + label: "Egypt", + value: "eg", + }, + { + label: "Iran", + value: "ir", + }, + { + label: "Libya", + value: "ly", + }, + { + label: "Yemen", + value: "ye", + }, + { + label: "Indonesia", + value: "id", + }, + { + label: "Pakistan", + value: "pk", + }, + { + label: "Bangladesh", + value: "bd", + }, + { + label: "Malaysia", + value: "my", + }, + { + label: "Philippines", + value: "ph", + }, + { + label: "Thailand", + value: "th", + }, + { + label: "Vietnam", + value: "vn", + }, +]; + +const LANGUAGES = [ + { + label: "English", + value: "en", + }, + { + label: "French", + value: "fr", + }, + { + label: "Spanish", + value: "es", + }, + { + label: "German", + value: "de", + }, + { + label: "Italian", + value: "it", + }, + { + label: "Portuguese", + value: "pt", + }, + { + label: "Dutch", + value: "nl", + }, + { + label: "Japanese", + value: "ja", + }, + { + label: "Korean", + value: "ko", + }, + { + label: "Chinese", + value: "zh", + }, + { + label: "Arabic", + value: "ar", + }, + { + label: "Russian", + value: "ru", + }, + { + label: "Turkish", + value: "tr", + }, + { + label: "Hindi", + value: "hi", + }, + { + label: "Auto-detect", + value: "auto", + }, +]; + +export default { + ANSWER_TYPES, + COUNTRIES, + LANGUAGES, +}; diff --git a/components/openperplex/openperplex.app.mjs b/components/openperplex/openperplex.app.mjs new file mode 100644 index 0000000000000..e42b2c4d4688f --- /dev/null +++ b/components/openperplex/openperplex.app.mjs @@ -0,0 +1,75 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "openperplex", + propDefinitions: { + query: { + type: "string", + label: "Query", + description: "The search query or question you want to ask", + }, + location: { + type: "string", + label: "Location", + description: "Country code for search context. This helps in providing localized results.", + options: constants.COUNTRIES, + optional: true, + }, + responseLanguage: { + type: "string", + label: "Response Language", + description: "Language code for the response. `auto` will auto-detect based on the query.", + default: "auto", + options: constants.LANGUAGES, + optional: true, + }, + answerType: { + type: "string", + label: "Answer Type", + description: "Format of the answer", + options: constants.ANSWER_TYPES, + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://5e70fd93-e9b8-4b9c-b7d9-eea4580f330c.app.bhs.ai.cloud.ovh.net"; + }, + _headers() { + return { + "X-API-Key": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, + path, + ...args + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._headers(), + ...args, + }); + }, + simpleSearch(opts = {}) { + return this._makeRequest({ + path: "/search_simple", + ...opts, + }); + }, + getWebsiteScreenshot(opts = {}) { + return this._makeRequest({ + path: "/get_website_screenshot", + ...opts, + }); + }, + queryFromUrl(opts = {}) { + return this._makeRequest({ + path: "/query_from_url", + ...opts, + }); + }, + }, +}; diff --git a/components/openperplex/package.json b/components/openperplex/package.json new file mode 100644 index 0000000000000..202a6d8153816 --- /dev/null +++ b/components/openperplex/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/openperplex", + "version": "0.1.0", + "description": "Pipedream OpenPerplex Components", + "main": "openperplex.app.mjs", + "keywords": [ + "pipedream", + "openperplex" + ], + "homepage": "https://pipedream.com/apps/openperplex", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} diff --git a/components/openphone/actions/create-contact/create-contact.mjs b/components/openphone/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..be3f6626c2ad4 --- /dev/null +++ b/components/openphone/actions/create-contact/create-contact.mjs @@ -0,0 +1,80 @@ +import { parseObject } from "../../common/utils.mjs"; +import openphone from "../../openphone.app.mjs"; + +export default { + key: "openphone-create-contact", + name: "Create Contact", + description: "Create a new contact in OpenPhone. [See the documentation](https://www.openphone.com/docs/api-reference/contacts/create-a-contact)", + version: "0.0.1", + type: "action", + props: { + openphone, + firstName: { + propDefinition: [ + openphone, + "firstName", + ], + }, + lastName: { + propDefinition: [ + openphone, + "lastName", + ], + optional: true, + }, + company: { + propDefinition: [ + openphone, + "company", + ], + optional: true, + }, + role: { + propDefinition: [ + openphone, + "role", + ], + optional: true, + }, + emails: { + propDefinition: [ + openphone, + "emails", + ], + optional: true, + }, + phoneNumbers: { + propDefinition: [ + openphone, + "phoneNumbers", + ], + optional: true, + }, + customFields: { + propDefinition: [ + openphone, + "customFields", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.openphone.createContact({ + $, + data: { + defaultFields: { + firstName: this.firstName, + lastName: this.lastName, + company: this.company, + role: this.role, + emails: parseObject(this.emails), + phoneNumbers: parseObject(this.phoneNumbers), + }, + customFields: parseObject(this.customFields), + }, + }); + + $.export("$summary", `Successfully created contact with ID: ${response.data.id}`); + return response; + }, +}; diff --git a/components/openphone/actions/send-message/send-message.mjs b/components/openphone/actions/send-message/send-message.mjs new file mode 100644 index 0000000000000..c44ccd8ea68ae --- /dev/null +++ b/components/openphone/actions/send-message/send-message.mjs @@ -0,0 +1,57 @@ +import { ConfigurationError } from "@pipedream/platform"; +import openphone from "../../openphone.app.mjs"; + +export default { + key: "openphone-send-message", + name: "Send a Text Message via OpenPhone", + description: "Send a text message from your OpenPhone number to a recipient. [See the documentation](https://www.openphone.com/docs/api-reference/messages/send-a-text-message)", + version: "0.0.1", + type: "action", + props: { + openphone, + from: { + propDefinition: [ + openphone, + "from", + ], + }, + to: { + type: "string", + label: "To", + description: "Recipient phone number in E.164 format.", + }, + content: { + type: "string", + label: "Content", + description: "The text content of the message to be sent.", + }, + }, + async run({ $ }) { + try { + const response = await this.openphone.sendTextMessage({ + $, + data: { + content: this.content, + from: this.from, + to: [ + this.to, + ], + setInboxStatus: "done", + }, + }); + $.export("$summary", `Successfully sent message to ${this.to}`); + return response; + + } catch ({ response }) { + let errorMessage = ""; + + if (response.data.errors) { + errorMessage = `Prop: ${response.data.errors[0].path} - ${response.data.errors[0].message}`; + } else { + errorMessage = response.data.message; + } + + throw new ConfigurationError(errorMessage); + } + }, +}; diff --git a/components/openphone/actions/update-contact/update-contact.mjs b/components/openphone/actions/update-contact/update-contact.mjs new file mode 100644 index 0000000000000..d685eb793b0d9 --- /dev/null +++ b/components/openphone/actions/update-contact/update-contact.mjs @@ -0,0 +1,87 @@ +import { parseObject } from "../../common/utils.mjs"; +import openphone from "../../openphone.app.mjs"; + +export default { + key: "openphone-update-contact", + name: "Update Contact", + description: "Update an existing contact on OpenPhone. [See the documentation](https://www.openphone.com/docs/api-reference/contacts/update-a-contact-by-id)", + version: "0.0.1", + type: "action", + props: { + openphone, + contactId: { + type: "string", + label: "Contact ID", + description: "The unique identifier of the contact.", + }, + firstName: { + propDefinition: [ + openphone, + "firstName", + ], + optional: true, + }, + lastName: { + propDefinition: [ + openphone, + "lastName", + ], + optional: true, + }, + company: { + propDefinition: [ + openphone, + "company", + ], + optional: true, + }, + role: { + propDefinition: [ + openphone, + "role", + ], + optional: true, + }, + emails: { + propDefinition: [ + openphone, + "emails", + ], + optional: true, + }, + phoneNumbers: { + propDefinition: [ + openphone, + "phoneNumbers", + ], + optional: true, + }, + customFields: { + propDefinition: [ + openphone, + "customFields", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.openphone.updateContact({ + $, + contactId: this.contactId, + data: { + defaultFields: { + firstName: this.firstName, + lastName: this.lastName, + company: this.company, + role: this.role, + emails: parseObject(this.emails), + phoneNumbers: parseObject(this.phoneNumbers), + }, + customFields: parseObject(this.customFields), + }, + }); + + $.export("$summary", `Successfully updated contact with ID ${this.contactId}`); + return response; + }, +}; diff --git a/components/openphone/common/utils.mjs b/components/openphone/common/utils.mjs new file mode 100644 index 0000000000000..e7c267cec0d5e --- /dev/null +++ b/components/openphone/common/utils.mjs @@ -0,0 +1,26 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + let parsedObj = obj; + if (typeof obj === "string") { + try { + parsedObj = JSON.parse(obj); + } catch (e) { + return obj; + } + } + + if (Array.isArray(parsedObj)) { + return parsedObj.map((item) => parseObject(item)); + } + if (typeof parsedObj === "object") { + for (const [ + key, + value, + ] of Object.entries(parsedObj)) { + parsedObj[key] = parseObject(value); + } + } + + return parsedObj; +}; diff --git a/components/openphone/openphone.app.mjs b/components/openphone/openphone.app.mjs new file mode 100644 index 0000000000000..7d126dd084736 --- /dev/null +++ b/components/openphone/openphone.app.mjs @@ -0,0 +1,121 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "openphone", + propDefinitions: { + from: { + type: "string", + label: "From", + description: "The sender's phone number. Can be either your OpenPhone phone number ID or the full phone number in E.164 format.", + async options() { + const { data } = await this.listPhoneNumbers(); + return data.map(({ + id: value, name, formattedNumber, + }) => ({ + label: `${name} - ${formattedNumber}`, + value, + })); + }, + }, + firstName: { + type: "string", + label: "First Name", + description: "The contact's first name.", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The contact's last name.", + optional: true, + }, + company: { + type: "string", + label: "Company", + description: "The contact's company name.", + optional: true, + }, + role: { + type: "string", + label: "Role", + description: "The contact's role.", + optional: true, + }, + emails: { + type: "string[]", + label: "Emails", + description: "Array of objects of contact's emails. **Example:** `{\"name\": \"Company Email\", \"value\": \"abc@example.com\"}`.", + }, + phoneNumbers: { + type: "string[]", + label: "Phone Numbers", + description: "Array of objects of contact's phone numbers. **Example:** `{\"name\": \"Company Phone\", \"value\": \"+12345678901\"}`.", + }, + customFields: { + type: "string[]", + label: "Custom Fields", + description: "Array of objects of custom fields for the contact. **Example:** `{\"key\": \"inbound-lead\", \"value\": \"[\"option1\", \"option2\"]\"}`.", + }, + }, + methods: { + _baseUrl() { + return "https://api.openphone.com/v1"; + }, + _headers() { + return { + Authorization: `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listPhoneNumbers(opts = {}) { + return this._makeRequest({ + path: "/phone-numbers", + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks/calls", + ...opts, + }); + }, + deleteWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + }); + }, + sendTextMessage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/messages", + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contacts", + ...opts, + }); + }, + updateContact({ + contactId, ...opts + }) { + return this._makeRequest({ + method: "PATCH", + path: `/contacts/${contactId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/openphone/package.json b/components/openphone/package.json new file mode 100644 index 0000000000000..e7fae1c54b357 --- /dev/null +++ b/components/openphone/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/openphone", + "version": "0.1.0", + "description": "Pipedream OpenPhone Components", + "main": "openphone.app.mjs", + "keywords": [ + "pipedream", + "openphone" + ], + "homepage": "https://pipedream.com/apps/openphone", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/openphone/sources/common/base.mjs b/components/openphone/sources/common/base.mjs new file mode 100644 index 0000000000000..75921c5b6df7d --- /dev/null +++ b/components/openphone/sources/common/base.mjs @@ -0,0 +1,62 @@ +import openphone from "../../openphone.app.mjs"; + +export default { + props: { + openphone, + http: "$.interface.http", + db: "$.service.db", + resourceIds: { + propDefinition: [ + openphone, + "from", + ], + type: "string[]", + label: "Resource IDs", + description: "The unique identifiers of phone numbers associated with the webhook.", + optional: true, + }, + label: { + type: "string", + label: "Label", + description: "Webhook's label", + optional: true, + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getEventFilter() { + return true; + }, + }, + hooks: { + async activate() { + const response = await this.openphone.createWebhook({ + data: { + url: this.http.endpoint, + events: this.getEvent(), + resourceIds: this.resourceIds, + label: this.label, + }, + }); + this._setHookId(response.data.id); + }, + async deactivate() { + const webhookId = this._getHookId(); + await this.openphone.deleteWebhook(webhookId); + }, + }, + async run({ body }) { + if (this.getEventFilter(body)) { + this.$emit(body, { + id: body.id, + summary: this.getSummary(body), + ts: Date.parse(body.data.object.completedAt), + }); + } + }, +}; diff --git a/components/openphone/sources/new-call-recording-completed-instant/new-call-recording-completed-instant.mjs b/components/openphone/sources/new-call-recording-completed-instant/new-call-recording-completed-instant.mjs new file mode 100644 index 0000000000000..9a0098f0fbe39 --- /dev/null +++ b/components/openphone/sources/new-call-recording-completed-instant/new-call-recording-completed-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "openphone-new-call-recording-completed-instant", + name: "New Call Recording Completed", + description: "Emit new event when a call recording has finished.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return [ + "call.recording.completed", + ]; + }, + getEmit(body) { + return `New call recording completed for call ID: ${body.data.object.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/openphone/sources/new-call-recording-completed-instant/test-event.mjs b/components/openphone/sources/new-call-recording-completed-instant/test-event.mjs new file mode 100644 index 0000000000000..bd109bda4fb0f --- /dev/null +++ b/components/openphone/sources/new-call-recording-completed-instant/test-event.mjs @@ -0,0 +1,30 @@ +export default { + "apiVersion": "v3", + "createdAt": "2022-01-24T19:30:55.400Z", + "data": { + "object": { + "answeredAt": "2022-01-24T19:30:38.000Z", + "completedAt": "2022-01-24T19:30:48.000Z", + "conversationId": "CN78ba0373683c48fd8fd96bc836c51f79", + "createdAt": "2022-01-24T19:30:34.675Z", + "direction": "incoming", + "from": "+18005550100", + "media": [ + { + "duration":7, + "type": "audio/mpeg", + "url": "https://storage.googleapis.com/opstatics-dev/a5f839bc72a24b33a7fc032f78777146.mp3" + } + ], + "object": "call", + "phoneNumberId": "PNtoDbDhuz", + "status": "completed", + "to": "+18885550101", + "userId": "USu5AsEHuQ", + "voicemail":null + } + }, + "id": "EVda6e196255814311aaac1983005fa2d9", + "object": "event", + "type": "call.recording.completed" +} \ No newline at end of file diff --git a/components/openphone/sources/new-incoming-call-completed-instant/new-incoming-call-completed-instant.mjs b/components/openphone/sources/new-incoming-call-completed-instant/new-incoming-call-completed-instant.mjs new file mode 100644 index 0000000000000..dade14952b2a6 --- /dev/null +++ b/components/openphone/sources/new-incoming-call-completed-instant/new-incoming-call-completed-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "openphone-new-incoming-call-completed-instant", + name: "New Incoming Call Completed (Instant)", + description: "Emit new event when an incoming call is completed, including calls not picked up or voicemails left.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return [ + "call.completed", + ]; + }, + getSummary() { + return "New Incoming Call Completed"; + }, + getEventFilter(body) { + return body.data.object.direction === "incoming"; + }, + }, + sampleEmit, +}; diff --git a/components/openphone/sources/new-incoming-call-completed-instant/test-event.mjs b/components/openphone/sources/new-incoming-call-completed-instant/test-event.mjs new file mode 100644 index 0000000000000..350b4db7cad36 --- /dev/null +++ b/components/openphone/sources/new-incoming-call-completed-instant/test-event.mjs @@ -0,0 +1,28 @@ +export default { + "apiVersion": "v3", + "createdAt": "2022-01-24T19:22:25.427Z", + "data": { + "object": { + "answeredAt": null, + "completedAt": "2022-01-24T19:22:19.000Z", + "conversationId": "CN78ba0373683c48fd8fd96bc836c51f79", + "createdAt": "2022-01-24T19:21:59.545Z", + "direction": "incoming", + "from": "+18005550100", + "media": [], + "object": "call", + "phoneNumberId": "PNtoDbDhuz", + "status": "completed", + "to": "+18885550101", + "userId": "USu5AsEHuQ", + "voicemail": { + "duration": 7, + "type": "audio/mpeg", + "url": "https://m.openph.one/static/15ad4740be6048e4a80efb268d347482.mp3" + } + } + }, + "id": "EVd39d3c8d6f244d21a9131de4fc9350d0", + "object": "event", + "type": "call.completed" +} \ No newline at end of file diff --git a/components/openphone/sources/new-outgoing-call-completed-instant/new-outgoing-call-completed-instant.mjs b/components/openphone/sources/new-outgoing-call-completed-instant/new-outgoing-call-completed-instant.mjs new file mode 100644 index 0000000000000..cdd01cfa2a1cd --- /dev/null +++ b/components/openphone/sources/new-outgoing-call-completed-instant/new-outgoing-call-completed-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "openphone-new-outgoing-call-completed-instant", + name: "New Outgoing Call Completed (Instant)", + description: "Emit new event when an outgoing call has ended.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return [ + "call.completed", + ]; + }, + getSummary() { + return "New Outgoing Call Completed"; + }, + getEventFilter(body) { + return body.data.object.direction === "outgoing"; + }, + }, + sampleEmit, +}; diff --git a/components/openphone/sources/new-outgoing-call-completed-instant/test-event.mjs b/components/openphone/sources/new-outgoing-call-completed-instant/test-event.mjs new file mode 100644 index 0000000000000..3451381272c21 --- /dev/null +++ b/components/openphone/sources/new-outgoing-call-completed-instant/test-event.mjs @@ -0,0 +1,28 @@ +export default { + "apiVersion": "v3", + "createdAt": "2022-01-24T19:22:25.427Z", + "data": { + "object": { + "answeredAt": null, + "completedAt": "2022-01-24T19:22:19.000Z", + "conversationId": "CN78ba0373683c48fd8fd96bc836c51f79", + "createdAt": "2022-01-24T19:21:59.545Z", + "direction": "outgoing", + "from": "+18005550100", + "media": [], + "object": "call", + "phoneNumberId": "PNtoDbDhuz", + "status": "completed", + "to": "+18885550101", + "userId": "USu5AsEHuQ", + "voicemail": { + "duration": 7, + "type": "audio/mpeg", + "url": "https://m.openph.one/static/15ad4740be6048e4a80efb268d347482.mp3" + } + } + }, + "id": "EVd39d3c8d6f244d21a9131de4fc9350d0", + "object": "event", + "type": "call.completed" +} \ No newline at end of file diff --git a/components/openrouter/openrouter.app.mjs b/components/openrouter/openrouter.app.mjs new file mode 100644 index 0000000000000..65fe721b9dc29 --- /dev/null +++ b/components/openrouter/openrouter.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "openrouter", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/openrouter/package.json b/components/openrouter/package.json new file mode 100644 index 0000000000000..002d31f9fe829 --- /dev/null +++ b/components/openrouter/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/openrouter", + "version": "0.0.1", + "description": "Pipedream OpenRouter Components", + "main": "openrouter.app.mjs", + "keywords": [ + "pipedream", + "openrouter" + ], + "homepage": "https://pipedream.com/apps/openrouter", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/opensea/README.md b/components/opensea/README.md index 68ece1358bafa..09ab255e83730 100644 --- a/components/opensea/README.md +++ b/components/opensea/README.md @@ -1,32 +1,11 @@ # Overview -What Is OpenSea API? OpenSea API is a platform that provides access to -Web3-enabled cryptocurrency applications, allowing developers to build secure, -trustless applications. OpenSea API provides a suite of tools, including a -secure and distributed marketplace, digital asset management, and decentralized -trading. Through OpenSea API, developers can build secure, trustless -applications to tokenize, trade, and manage digital assets as well as create a -marketplace for digital goods and services. +The OpenSea API provides a plethora of endpoints that lets you tap into the rich world of NFTs. From fetching detailed asset information to looking up user-specific items, the possibilities for crafting custom Pipedream workflows are vast. This allows you to automate alerts, synchronize NFT collections with external databases, or even analyze market trends programmatically. -OpenSea API can be used to create a variety of applications, including: +# Example Use Cases -- Digital Asset Marketplaces: Create tokenized products, which can be traded or - exchanged on an open marketplace. -- Token Management Interfaces: Compile or manage digital assets, such as - collectibles, using an intuitive user interface. -- Decentralized Exchanges: Create automated trading systems with secure, - distributed marketplaces. -- Collectible Games: Create digital collectibles that can be bought or sold on - the OpenSea API platform. -- Security Tokens: Issue tokens backed by real-world assets, such as stocks, - real estate, or commodities. -- Digital Identity Management Products: Create digital identities using the - OpenSea API platform. +- **NFT Price Alert System**: Construct a workflow where Pipedream monitors specific NFTs on OpenSea for price changes. Once a price drops or rises above a certain threshold, Pipedream can trigger notifications through email, SMS, or messaging apps like Slack to alert the subscriber. -With the OpenSea API, developers can create an array of custom solutions to -trade, manage, and monetize digital assets. Developers can build applications -that enable users to buy, sell, and manage digital assets in a secure, -trustless environment. Applications built on OpenSea API can facilitate both -peer-to-peer trading and investment management services. OpenSea API can also -be used to create digital collectibles, gaming solutions, and digital asset -marketplaces for buying and selling digital assets. +- **Automated Collection Sync**: Develop a system that uses the OpenSea API to track newly minted NFTs within a collection. Pipedream can then automatically update an external database or a Google Sheet, keeping your catalog in sync with OpenSea's marketplace in near real-time. + +- **Market Trend Analysis**: Create a workflow that aggregates sales data from OpenSea for specific NFT collections or overall market segments. Pipedream can process this data and push insights into a BI tool like Google Data Studio or Tableau, enabling deeper analysis on NFT market trends and performance metrics. diff --git a/components/opensea/opensea.app.mjs b/components/opensea/opensea.app.mjs index 5c0b65d7b062c..2be62f0ec6313 100644 --- a/components/opensea/opensea.app.mjs +++ b/components/opensea/opensea.app.mjs @@ -1,26 +1,38 @@ import { axios } from "@pipedream/platform"; +import Bottleneck from "bottleneck"; +const limiter = new Bottleneck({ + minTime: 200, // 5 requests per second + maxConcurrent: 1, +}); +const axiosRateLimiter = limiter.wrap(axios); export default { type: "app", app: "opensea", propDefinitions: {}, methods: { - async retrieveEvents({ - $, contract, eventType, occurredAfter, cursor, + _baseUrl() { + return "https://api.opensea.io/api/v2"; + }, + _makeRequest({ + $ = this, + path, + ...otherOpts }) { - const apiKey = this.$auth.api_key; - return axios($ ?? this, { - url: "https://api.opensea.io/api/v1/events", - params: { - only_opensea: false, - asset_contract_address: contract, - event_type: eventType, - occurred_after: occurredAfter, - cursor, - }, + return axiosRateLimiter($, { + url: `${this._baseUrl()}${path}`, headers: { - "X-API-KEY": apiKey, + "X-API-KEY": this.$auth.api_key, }, + ...otherOpts, + }); + }, + retrieveEvents({ + collectionSlug, ...opts + }) { + return this._makeRequest({ + path: `/events/collection/${collectionSlug}`, + ...opts, }); }, }, diff --git a/components/opensea/package.json b/components/opensea/package.json index e4385afa2338d..f289b6d70aea7 100644 --- a/components/opensea/package.json +++ b/components/opensea/package.json @@ -1,21 +1,20 @@ { "name": "@pipedream/opensea", - "version": "0.0.3", + "version": "0.0.4", "description": "Pipedream Opensea Components", "main": "opensea.app.mjs", "keywords": [ "pipedream", "opensea" ], - "files": [ - "dist" - ], "homepage": "https://pipedream.com/apps/opensea", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.2.0" + "@pipedream/platform": "^3.0.3", + "bottleneck": "^2.19.5", + "md5": "^2.3.0" } } diff --git a/components/opensea/sources/new-collection-events/new-collection-events.mjs b/components/opensea/sources/new-collection-events/new-collection-events.mjs index dde3a4bb3bdab..519a22619bde1 100644 --- a/components/opensea/sources/new-collection-events/new-collection-events.mjs +++ b/components/opensea/sources/new-collection-events/new-collection-events.mjs @@ -1,13 +1,13 @@ import opensea from "../../opensea.app.mjs"; import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import md5 from "md5"; export default { - name: "New Collection Events", - version: "0.0.3", key: "opensea-new-collection-events", - description: - "Emit new filtered events. [See docs](https://docs.opensea.io/reference/retrieving-asset-events)", - dedupe: "greatest", + name: "New Collection Events", + description: "Emit new filtered events for a collection. [See the documentation](https://docs.opensea.io/reference/list_events_by_collection)", + version: "0.0.4", + dedupe: "unique", type: "source", props: { opensea, @@ -18,54 +18,81 @@ export default { intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, }, }, - contractAddress: { + collectionSlug: { type: "string", - label: "Contract Address", - description: "Collection contract address", + label: "Collection Slug", + description: "Unique string to identify a collection on OpenSea. This can be found by visiting the collection on the OpenSea website and noting the last path parameter.", }, eventType: { type: "string", options: [ - "sales", - "listings", + "all", + "cancel", + "listing", + "offer", + "order", + "redemption", + "sale", + "transfer", ], label: "Event Type", - description: "OpenSea event type", + description: "The type of event to filter by", + default: "all", + optional: true, }, }, methods: { - getLastTimestamp() { + _getLastTimestamp() { return this.db.get("lastTimestamp"); }, - setLastTimestamp(ts) { + _setLastTimestamp(ts) { this.db.set("lastTimestamp", ts); }, - }, - async run() { - const eventType = this.eventType === "sales" - ? "successful" - : "created"; - const lastTimestamp = this.getLastTimestamp(); - let cursor = null; - do { - const resp = await this.opensea.retrieveEvents({ - contract: this.contractAddress, - eventType, - occurredAfter: lastTimestamp, - cursor, - }); - resp.asset_events.forEach((event) => { - this.$emit(event, { - id: event.id, - summary: `${event.asset.name} ${this.eventType} event`, - ts: +new Date(event.created_date), + generateMeta(event) { + return { + id: md5(JSON.stringify(event)), + summary: `New ${event.event_type} event`, + ts: event.event_timestamp, + }; + }, + async processEvent(max) { + const lastTimestamp = this._getLastTimestamp(); + let next = null; + let events = []; + do { + const resp = await this.opensea.retrieveEvents({ + collectionSlug: this.collectionSlug, + params: { + event_type: this.eventType, + after: lastTimestamp, + next, + }, }); - }); - if (!cursor && resp.asset_events.length > 0) { - const ts = Math.floor(new Date(resp.asset_events[0].created_date).getTime() / 1000); - this.setLastTimestamp(ts); + if (!resp?.asset_events) { + break; + } + events.push(...resp.asset_events); + next = resp.next; + } while (lastTimestamp && next); + + if (!events.length) { + return; } - cursor = resp.next; - } while (lastTimestamp && cursor); + this._setLastTimestamp(events[0].event_timestamp); + if (max) { + events = events.slice(0, max); + } + events.reverse().forEach((event) => { + this.$emit(event, this.generateMeta(event)); + }); + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + async run() { + await this.processEvent(); }, }; diff --git a/components/opensrs/actions/initiate-domain-transfer/initiate-domain-transfer.mjs b/components/opensrs/actions/initiate-domain-transfer/initiate-domain-transfer.mjs new file mode 100644 index 0000000000000..fc615c8dc6127 --- /dev/null +++ b/components/opensrs/actions/initiate-domain-transfer/initiate-domain-transfer.mjs @@ -0,0 +1,157 @@ +import app from "../../opensrs.app.mjs"; +import utils from "../../common/utils.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "opensrs-initiate-domain-transfer", + name: "Initiate Domain Transfer", + description: "Initiate a domain transfer to OpenSRS. [See the documentation](https://domains.opensrs.guide/docs/trade_domain).", + version: "0.0.1", + type: "action", + props: { + app, + domain: { + propDefinition: [ + app, + "domain", + ], + }, + email: { + type: "string", + label: "Email", + description: "The email address of the new owner of the domain.", + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the new owner of the domain.", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the new owner of the domain.", + }, + orgName: { + type: "string", + label: "Organization Name", + description: "The organization name of the new owner of the domain.", + }, + phone: { + type: "string", + label: "Phone", + description: "The phone number of the new owner of the domain. Required for all except `.BE`.", + optional: true, + }, + address1: { + type: "string", + label: "Address 1", + description: "The first line of the address of the new owner of the domain. Required for all except `.BE`.", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "The city of the new owner of the domain. Required for all except `.BE`.", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "The country of the new owner of the domain. Required for all except `.BE`.", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "The state of the new owner of the domain. Required for all except `.BE`.", + optional: true, + }, + postalCode: { + type: "string", + label: "Postal Code", + description: "The postal code of the new owner of the domain. Required for all except `.BE`.", + optional: true, + }, + domainAuthInfo: { + type: "string", + label: "Domain Authorization Info", + description: "The authorization code required for domain transfer. Required for `.BE`.", + optional: true, + }, + jsonOutput: { + type: "boolean", + label: "JSON Output", + description: "Whether to output the response in JSON format.", + optional: true, + default: true, + }, + }, + methods: { + getJsonBody() { + const { + domain, + email, + firstName, + lastName, + orgName, + phone, + address1, + city, + country, + state, + postalCode, + domainAuthInfo, + } = this; + + return { + data_block: { + dt_assoc: { + item: [ + ...utils.addItem("protocol", constants.PROTOCOL.XCP), + ...utils.addItem("object", constants.OBJECT_TYPE.DOMAIN), + ...utils.addItem("action", constants.ACTION_TYPE.TRADE_DOMAIN), + { + "@_key": "attributes", + "dt_assoc": { + item: [ + ...utils.addItem("domain", domain), + ...utils.addItem("email", email), + ...utils.addItem("first_name", firstName), + ...utils.addItem("last_name", lastName), + ...utils.addItem("org_name", orgName), + ...utils.addItem("phone", phone), + ...utils.addItem("address1", address1), + ...utils.addItem("city", city), + ...utils.addItem("country", country), + ...utils.addItem("state", state), + ...utils.addItem("postal_code", postalCode), + ...utils.addItem("domain_auth_info", domainAuthInfo), + ], + }, + }, + ], + }, + }, + }; + }, + initiateDomainTransfer(args = {}) { + return this.app.post(args); + }, + }, + async run({ $ }) { + const { + initiateDomainTransfer, + getJsonBody, + jsonOutput, + } = this; + + const response = await initiateDomainTransfer({ + $, + jsonBody: getJsonBody(), + jsonOutput, + }); + + $.export("$summary", "Successfully initiated domain transfer."); + return response; + }, +}; diff --git a/components/opensrs/actions/register-domain/register-domain.mjs b/components/opensrs/actions/register-domain/register-domain.mjs new file mode 100644 index 0000000000000..c37a85ee722eb --- /dev/null +++ b/components/opensrs/actions/register-domain/register-domain.mjs @@ -0,0 +1,530 @@ +import app from "../../opensrs.app.mjs"; +import constants from "../../common/constants.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "opensrs-register-domain", + name: "Register Domain", + description: "Register a new domain. [See the documentation](https://domains.opensrs.guide/docs/sw_register-domain-or-trust_service-).", + version: "0.0.1", + type: "action", + props: { + app, + domain: { + propDefinition: [ + app, + "domain", + ], + }, + regUsername: { + type: "string", + label: "Reseller Username", + description: "Usernames must be 3-20 characters in length. You can use any of the following alphanumeric characters: `A-Z`, `a-z`, `0-9`.", + }, + regPassword: { + type: "string", + label: "Reseller Password", + description: "Passwords must be 10-20 characters in length. You can use any of the following alphanumeric characters and symbols: `A-Z`, `a-z`, `0-9`, `! @$^,.~|=-+_{}#`.", + }, + ownerFirstName: { + type: "string", + label: "Owner First Name", + description: "The first name of the owner of the domain.", + }, + ownerLastName: { + type: "string", + label: "Owner Last Name", + description: "The last name of the owner of the domain.", + }, + ownerEmail: { + type: "string", + label: "Owner Email", + description: "The email of the owner of the domain.", + }, + ownerOrgName: { + type: "string", + label: "Owner Organization Name", + description: "The organization name of the owner of the domain.", + optional: true, + }, + ownerPhone: { + type: "string", + label: "Owner Phone", + description: "The phone number of the owner of the domain.", + }, + ownerAddress1: { + type: "string", + label: "Owner Address 1", + description: "The first line of the address of the owner of the domain.", + }, + ownerCity: { + type: "string", + label: "Owner City", + description: "The city of the owner of the domain.", + }, + ownerCountry: { + type: "string", + label: "Owner Country", + description: "The country of the owner of the domain.", + }, + ownerState: { + type: "string", + label: "Owner State", + description: "The state of the owner of the domain.", + }, + ownerPostalCode: { + type: "string", + label: "Owner Postal Code", + description: "The postal code of the owner of the domain.", + }, + adminFirstName: { + type: "string", + label: "Admin First Name", + description: "The first name of the admin of the domain.", + }, + adminLastName: { + type: "string", + label: "Admin Last Name", + description: "The last name of the admin of the domain.", + }, + adminEmail: { + type: "string", + label: "Admin Email", + description: "The email of the admin of the domain.", + }, + adminOrgName: { + type: "string", + label: "Admin Organization Name", + description: "The organization name of the admin of the domain.", + optional: true, + }, + adminPhone: { + type: "string", + label: "Admin Phone", + description: "The phone number of the admin of the domain.", + }, + adminAddress1: { + type: "string", + label: "Admin Address 1", + description: "The first line of the address of the admin of the domain.", + }, + adminCity: { + type: "string", + label: "Admin City", + description: "The city of the admin of the domain.", + }, + adminCountry: { + type: "string", + label: "Admin Country", + description: "The country of the admin of the domain.", + }, + adminState: { + type: "string", + label: "Admin State", + description: "The state of the admin of the domain.", + }, + adminPostalCode: { + type: "string", + label: "Admin Postal Code", + description: "The postal code of the admin of the domain.", + }, + billingFirstName: { + type: "string", + label: "Billing First Name", + description: "The first name of the billing contact of the domain.", + }, + billingLastName: { + type: "string", + label: "Billing Last Name", + description: "The last name of the billing contact of the domain.", + }, + billingEmail: { + type: "string", + label: "Billing Email", + description: "The email of the billing contact of the domain.", + }, + billingOrgName: { + type: "string", + label: "Billing Organization Name", + description: "The organization name of the billing contact of the domain.", + optional: true, + }, + billingPhone: { + type: "string", + label: "Billing Phone", + description: "The phone number of the billing contact of the domain.", + }, + billingAddress1: { + type: "string", + label: "Billing Address 1", + description: "The first line of the address of the billing contact of the domain.", + }, + billingCity: { + type: "string", + label: "Billing City", + description: "The city of the billing contact of the domain.", + }, + billingCountry: { + type: "string", + label: "Billing Country", + description: "The country of the billing contact of the domain.", + }, + billingState: { + type: "string", + label: "Billing State", + description: "The state of the billing contact of the domain.", + }, + billingPostalCode: { + type: "string", + label: "Billing Postal Code", + description: "The postal code of the billing contact of the domain.", + }, + techFirstName: { + type: "string", + label: "Tech First Name", + description: "The first name of the tech contact of the domain.", + }, + techLastName: { + type: "string", + label: "Tech Last Name", + description: "The last name of the tech contact of the domain.", + }, + techEmail: { + type: "string", + label: "Tech Email", + description: "The email of the tech contact of the domain.", + }, + techOrgName: { + type: "string", + label: "Tech Organization Name", + description: "The organization name of the tech contact of the domain.", + optional: true, + }, + techPhone: { + type: "string", + label: "Tech Phone", + description: "The phone number of the tech contact of the domain.", + }, + techAddress1: { + type: "string", + label: "Tech Address 1", + description: "The first line of the address of the tech contact of the domain.", + }, + techCity: { + type: "string", + label: "Tech City", + description: "The city of the tech contact of the domain.", + }, + techCountry: { + type: "string", + label: "Tech Country", + description: "The country of the tech contact of the domain.", + }, + techState: { + type: "string", + label: "Tech State", + description: "The state of the tech contact of the domain.", + }, + techPostalCode: { + type: "string", + label: "Tech Postal Code", + description: "The postal code of the tech contact of the domain.", + }, + customTechContact: { + type: "string", + label: "Custom Tech Contact", + description: "An indication of whether to use the RSP's tech contact info, or the tech contact info provided n the request.", + default: "0", + options: [ + { + label: "Use reseller's tech contact info.", + value: "0", + }, + { + label: "Use tech contact info provided in request.", + value: "1", + }, + ], + }, + autoRenew: { + type: "string", + label: "Auto Renew", + description: "Whether to automatically renew the domain.", + optional: true, + options: [ + { + label: "Do not auto-renew", + value: "0", + }, + { + label: "Auto-renew", + value: "1", + }, + ], + }, + period: { + type: "string", + label: "Period", + description: "The length of the registration period. Allowed values are `1-10`, depending on the TLD, that is, not all registries allow for a 1-year registration. The default is `2`, which is valid for all TLDs.", + default: "2", + options: [ + { + label: "1 Year", + value: "1", + }, + { + label: "2 Years", + value: "2", + }, + { + label: "3 Years", + value: "3", + }, + { + label: "4 Years", + value: "4", + }, + { + label: "5 Years", + value: "5", + }, + { + label: "6 Years", + value: "6", + }, + { + label: "7 Years", + value: "7", + }, + { + label: "8 Years", + value: "8", + }, + { + label: "9 Years", + value: "9", + }, + { + label: "10 Years", + value: "10", + }, + ], + }, + customNameservers: { + type: "string", + label: "Custom Nameservers", + description: "Custom nameservers for the domain.", + reloadProps: true, + options: [ + { + label: "Use reseller's default nameservers", + value: "0", + }, + { + label: "Use nameservers provided in request", + value: "1", + }, + ], + }, + jsonOutput: { + type: "boolean", + label: "JSON Output", + description: "Whether to output the response in JSON format.", + optional: true, + default: true, + }, + }, + additionalProps() { + const { customNameservers } = this; + if (customNameservers === "1") { + return { + nameserverList: { + type: "string[]", + label: "Nameserver List", + description: "List of nameservers for the domain. Eg. `ns1.opensrs.net`, `ns2.opensrs.net`.", + }, + }; + } + return {}; + }, + methods: { + getJsonBody() { + const { + domain, + regUsername, + regPassword, + ownerFirstName, + ownerLastName, + ownerEmail, + ownerOrgName, + ownerPhone, + ownerAddress1, + ownerCity, + ownerCountry, + ownerState, + ownerPostalCode, + adminFirstName, + adminLastName, + adminEmail, + adminOrgName, + adminPhone, + adminAddress1, + adminCity, + adminCountry, + adminState, + adminPostalCode, + billingFirstName, + billingLastName, + billingEmail, + billingOrgName, + billingPhone, + billingAddress1, + billingCity, + billingCountry, + billingState, + billingPostalCode, + techFirstName, + techLastName, + techEmail, + techOrgName, + techPhone, + techAddress1, + techCity, + techCountry, + techState, + techPostalCode, + customTechContact, + autoRenew, + customNameservers, + nameserverList, + period, + } = this; + return { + data_block: { + dt_assoc: { + item: [ + ...utils.addItem("protocol", constants.PROTOCOL.XCP), + ...utils.addItem("object", constants.OBJECT_TYPE.DOMAIN), + ...utils.addItem("action", constants.ACTION_TYPE.SW_REGISTER), + { + "@_key": "attributes", + "dt_assoc": { + item: [ + ...utils.addItem("domain", domain), + ...utils.addItem("reg_username", regUsername), + ...utils.addItem("reg_password", regPassword), + ...utils.addItem("reg_type", constants.REGISTRY_TYPE.NEW), + ...utils.addItem("custom_nameservers", customNameservers), + ...utils.addItem("period", period), + ...utils.addItem("auto_renew", autoRenew), + ...utils.addItemList( + "nameserver_list", + customNameservers === "1" + ? nameserverList || [] + : constants.DEFAULT_NAMESERVER_LIST, + ), + ...utils.addItem("custom_tech_contact", customTechContact), + { + "@_key": "contact_set", + "dt_assoc": { + item: [ + { + "@_key": "owner", + "dt_assoc": { + item: [ + ...utils.addItem("first_name", ownerFirstName), + ...utils.addItem("last_name", ownerLastName), + ...utils.addItem("phone", ownerPhone), + ...utils.addItem("email", ownerEmail), + ...utils.addItem("address1", ownerAddress1), + ...utils.addItem("city", ownerCity), + ...utils.addItem("state", ownerState), + ...utils.addItem("country", ownerCountry), + ...utils.addItem("postal_code", ownerPostalCode), + ...utils.addItem("org_name", ownerOrgName), + ], + }, + }, + { + "@_key": "admin", + "dt_assoc": { + item: [ + ...utils.addItem("first_name", adminFirstName), + ...utils.addItem("last_name", adminLastName), + ...utils.addItem("phone", adminPhone), + ...utils.addItem("email", adminEmail), + ...utils.addItem("address1", adminAddress1), + ...utils.addItem("city", adminCity), + ...utils.addItem("state", adminState), + ...utils.addItem("country", adminCountry), + ...utils.addItem("postal_code", adminPostalCode), + ...utils.addItem("org_name", adminOrgName), + ], + }, + }, + { + "@_key": "billing", + "dt_assoc": { + item: [ + ...utils.addItem("first_name", billingFirstName), + ...utils.addItem("last_name", billingLastName), + ...utils.addItem("phone", billingPhone), + ...utils.addItem("email", billingEmail), + ...utils.addItem("address1", billingAddress1), + ...utils.addItem("city", billingCity), + ...utils.addItem("state", billingState), + ...utils.addItem("country", billingCountry), + ...utils.addItem("postal_code", billingPostalCode), + ...utils.addItem("org_name", billingOrgName), + ], + }, + }, + { + "@_key": "tech", + "dt_assoc": { + item: [ + ...utils.addItem("first_name", techFirstName), + ...utils.addItem("last_name", techLastName), + ...utils.addItem("phone", techPhone), + ...utils.addItem("email", techEmail), + ...utils.addItem("address1", techAddress1), + ...utils.addItem("city", techCity), + ...utils.addItem("state", techState), + ...utils.addItem("country", techCountry), + ...utils.addItem("postal_code", techPostalCode), + ...utils.addItem("org_name", techOrgName), + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }; + }, + registerDomain(args = {}) { + return this.app.post(args); + }, + }, + async run({ $ }) { + const { + registerDomain, + getJsonBody, + jsonOutput, + } = this; + + const response = await registerDomain({ + $, + jsonBody: getJsonBody(), + jsonOutput, + }); + + $.export("$summary", "Successfully registered domain."); + return response; + }, +}; diff --git a/components/opensrs/actions/update-dns-records/update-dns-records.mjs b/components/opensrs/actions/update-dns-records/update-dns-records.mjs new file mode 100644 index 0000000000000..b84a7175eeefe --- /dev/null +++ b/components/opensrs/actions/update-dns-records/update-dns-records.mjs @@ -0,0 +1,396 @@ +import app from "../../opensrs.app.mjs"; +import utils from "../../common/utils.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "opensrs-update-dns-records", + name: "Update DNS Records", + description: "Update DNS records for a specified domain. [See the documentation](https://domains.opensrs.guide/docs/set_dns_zone-).", + version: "0.0.1", + type: "action", + props: { + app, + domain: { + propDefinition: [ + app, + "domain", + ], + }, + nameserversOk: { + type: "string", + label: "Nameservers OK", + description: "Indicates whether the domain is set up to use the OpenSRS nameservers.", + default: "0", + options: [ + { + label: "Domain is not set up to use the OpenSRS nameservers", + value: "0", + }, + { + label: "Domain is set up to use the OpenSRS nameservers", + value: "1", + }, + ], + }, + numberOfARecords: { + type: "integer", + label: "Number of A Records", + description: "Number of A records to update", + default: 0, + reloadProps: true, + }, + numberOfAAAARecords: { + type: "integer", + label: "Number of AAAA Records", + description: "Number of AAAA records to update", + default: 0, + reloadProps: true, + }, + numberOfCNAMERecords: { + type: "integer", + label: "Number of CNAME Records", + description: "Number of CNAME records to update", + default: 0, + reloadProps: true, + }, + numberOfMXRecords: { + type: "integer", + label: "Number of MX Records", + description: "Number of MX records to update", + default: 0, + reloadProps: true, + }, + numberOfTXTRecords: { + type: "integer", + label: "Number of TXT Records", + description: "Number of TXT records to update", + default: 0, + reloadProps: true, + }, + jsonOutput: { + type: "boolean", + label: "JSON Output", + description: "Whether to output the response in JSON format.", + optional: true, + default: true, + }, + }, + additionalProps() { + const { + numberOfARecords, + numberOfAAAARecords, + numberOfCNAMERecords, + numberOfMXRecords, + numberOfTXTRecords, + getARecordPropDefinitions, + getAAAARecordPropDefinitions, + getCNAMERecordPropDefinitions, + getMXRecordPropDefinitions, + getTXTRecordPropDefinitions, + } = this; + + const aRecords = utils.getAdditionalProps({ + numberOfFields: numberOfARecords, + fieldName: "a", + getPropDefinitions: getARecordPropDefinitions, + }); + + const aaaaRecords = utils.getAdditionalProps({ + numberOfFields: numberOfAAAARecords, + fieldName: "aaaa", + getPropDefinitions: getAAAARecordPropDefinitions, + }); + + const cnameRecords = utils.getAdditionalProps({ + numberOfFields: numberOfCNAMERecords, + fieldName: "cname", + getPropDefinitions: getCNAMERecordPropDefinitions, + }); + + const mxRecords = utils.getAdditionalProps({ + numberOfFields: numberOfMXRecords, + fieldName: "mx", + getPropDefinitions: getMXRecordPropDefinitions, + }); + + const txtRecords = utils.getAdditionalProps({ + numberOfFields: numberOfTXTRecords, + fieldName: "txt", + getPropDefinitions: getTXTRecordPropDefinitions, + }); + + return Object.assign({}, aRecords, aaaaRecords, cnameRecords, mxRecords, txtRecords); + }, + methods: { + getARecordPropDefinitions({ + prefix, label, + } = {}) { + return { + [`${prefix}ipAddress`]: { + type: "string", + label: `${label} - IP Address`, + description: "The IPv4 address for the A record. A numeric address that computers recognize eg. `123.45.54.123`.", + }, + [`${prefix}subdomain`]: { + type: "string", + label: `${label} - Subdomain`, + description: "The subdomain for the A record. The third level of the domain name, such as `www` or `ftp`.", + optional: true, + }, + }; + }, + getAAAARecordPropDefinitions({ + prefix, label, + } = {}) { + return { + [`${prefix}ipv6Address`]: { + type: "string", + label: `${label} - IPv6 Address`, + description: "The IPv6 address for the AAAA record. A numeric address that computers recognize eg. `2001:0db8:85a3:0000:0000:8a2e:0370:7334`.", + }, + [`${prefix}subdomain`]: { + type: "string", + label: `${label} - Subdomain`, + description: "The subdomain for the AAAA record. The third level of the domain name, such as `www` or `ftp`.", + }, + }; + }, + getCNAMERecordPropDefinitions({ + prefix, label, + } = {}) { + return { + [`${prefix}hostname`]: { + type: "string", + label: `${label} - Hostname`, + description: "The FQDN of the domain that you want to access.", + }, + [`${prefix}subdomain`]: { + type: "string", + label: `${label} - Subdomain`, + description: "The third level of the domain name, such as `www` or `ftp`.", + }, + }; + }, + getMXRecordPropDefinitions({ + prefix, label, + } = {}) { + return { + [`${prefix}priority`]: { + type: "string", + label: `${label} - Priority`, + description: "The priority of the target host, lower value means more preferred.", + optional: true, + }, + [`${prefix}hostname`]: { + type: "string", + label: `${label} - Hostname`, + description: "The FQDN of the domain that you want to access.", + }, + [`${prefix}subdomain`]: { + type: "string", + label: `${label} - Subdomain`, + description: "The third level of the domain name, such as `www` or `ftp`.", + }, + }; + }, + getTXTRecordPropDefinitions({ + prefix, label, + } = {}) { + return { + [`${prefix}subdomain`]: { + type: "string", + label: `${label} - Subdomain`, + description: "The third level of the domain name, such as `www` or `ftp`.", + }, + [`${prefix}text`]: { + type: "string", + label: `${label} - Text`, + description: "The text content for the TXT record.", + }, + }; + }, + aRecordPropsMapper(prefix) { + const { + [`${prefix}ipAddress`]: ipAddress, + [`${prefix}subdomain`]: subdomain, + } = this; + return { + dt_assoc: { + item: [ + ...utils.addItem("subdomain", subdomain), + ...utils.addItem("ip_address", ipAddress), + ], + }, + }; + }, + aaaaRecordPropsMapper(prefix) { + const { + [`${prefix}ipv6Address`]: ipv6Address, + [`${prefix}subdomain`]: subdomain, + } = this; + return { + dt_assoc: { + item: [ + ...utils.addItem("subdomain", subdomain), + ...utils.addItem("ipv6_address", ipv6Address), + ], + }, + }; + }, + cnameRecordPropsMapper(prefix) { + const { + [`${prefix}hostname`]: hostname, + [`${prefix}subdomain`]: subdomain, + } = this; + return { + dt_assoc: { + item: [ + ...utils.addItem("subdomain", subdomain), + ...utils.addItem("hostname", hostname), + ], + }, + }; + }, + mxRecordPropsMapper(prefix) { + const { + [`${prefix}priority`]: priority, + [`${prefix}hostname`]: hostname, + [`${prefix}subdomain`]: subdomain, + } = this; + return { + dt_assoc: { + item: [ + ...utils.addItem("subdomain", subdomain), + ...utils.addItem("hostname", hostname), + ...utils.addItem("priority", priority), + ], + }, + }; + }, + txtRecordPropsMapper(prefix) { + const { + [`${prefix}subdomain`]: subdomain, + [`${prefix}text`]: text, + } = this; + return { + dt_assoc: { + item: [ + ...utils.addItem("subdomain", subdomain), + ...utils.addItem("text", text), + ], + }, + }; + }, + getJsonBody() { + const { + domain, + nameserversOk, + numberOfARecords, + numberOfAAAARecords, + numberOfCNAMERecords, + numberOfMXRecords, + numberOfTXTRecords, + aRecordPropsMapper, + aaaaRecordPropsMapper, + cnameRecordPropsMapper, + mxRecordPropsMapper, + txtRecordPropsMapper, + } = this; + + const aRecords = utils.getFieldsProps({ + numberOfFields: numberOfARecords, + fieldName: "a", + propsMapper: aRecordPropsMapper, + }); + + const aaaaRecords = utils.getFieldsProps({ + numberOfFields: numberOfAAAARecords, + fieldName: "aaaa", + propsMapper: aaaaRecordPropsMapper, + }); + + const cnameRecords = utils.getFieldsProps({ + numberOfFields: numberOfCNAMERecords, + fieldName: "cname", + propsMapper: cnameRecordPropsMapper, + }); + + const mxRecords = utils.getFieldsProps({ + numberOfFields: numberOfMXRecords, + fieldName: "mx", + propsMapper: mxRecordPropsMapper, + }); + + const txtRecords = utils.getFieldsProps({ + numberOfFields: numberOfTXTRecords, + fieldName: "txt", + propsMapper: txtRecordPropsMapper, + }); + + const records = [ + ...aRecords, + ...aaaaRecords, + ...cnameRecords, + ...mxRecords, + ...txtRecords, + ]; + + return { + data_block: { + dt_assoc: { + item: [ + ...utils.addItem("protocol", constants.PROTOCOL.XCP), + ...utils.addItem("object", constants.OBJECT_TYPE.DOMAIN), + ...utils.addItem("action", constants.ACTION_TYPE.SET_DNS_ZONE), + { + "@_key": "attributes", + "dt_assoc": { + item: [ + ...utils.addItem("domain", domain), + ...(records.length + ? [ + ...utils.addItem("nameservers_ok", nameserversOk), + { + "@_key": "records", + "dt_assoc": { + item: [ + ...utils.addDnsRecord("A", aRecords), + ...utils.addDnsRecord("AAAA", aaaaRecords), + ...utils.addDnsRecord("CNAME", cnameRecords), + ...utils.addDnsRecord("MX", mxRecords), + ...utils.addDnsRecord("TXT", txtRecords), + ], + }, + }, + ] + : [] + ), + ], + }, + }, + ], + }, + }, + }; + }, + updateDnsRecords(args) { + return this.app.post(args); + }, + }, + async run({ $ }) { + const { + updateDnsRecords, + getJsonBody, + jsonOutput, + } = this; + + const response = await updateDnsRecords({ + $, + jsonBody: getJsonBody(), + jsonOutput, + }); + + $.export("$summary", "Successfully updated DNS records."); + return response; + }, +}; diff --git a/components/opensrs/common/constants.mjs b/components/opensrs/common/constants.mjs new file mode 100644 index 0000000000000..d852bb88fa808 --- /dev/null +++ b/components/opensrs/common/constants.mjs @@ -0,0 +1,43 @@ +const DEFAULT_LIMIT = 100; +const SEP = "_"; + +const REGISTRY_TYPE = { + NEW: "new", + TRANSFER: "transfer", + SUNRISE: "sunrise", +}; + +const PROTOCOL = { + XCP: "XCP", +}; + +const OBJECT_TYPE = { + DOMAIN: "DOMAIN", + EVENT: "EVENT", + ORDER: "ORDER", + TRANSFER: "TRANSFER", +}; + +const ACTION_TYPE = { + SW_REGISTER: "SW_REGISTER", + SET_DNS_ZONE: "SET_DNS_ZONE", + TRADE_DOMAIN: "TRADE_DOMAIN", + POLL: "POLL", + ACKNOWLEDGE: "ACK", +}; + +const DEFAULT_NAMESERVER_LIST = [ + "ns1.systemdns.com", + "ns2.systemdns.com", + "ns3.systemdns.com", +]; + +export default { + DEFAULT_LIMIT, + SEP, + REGISTRY_TYPE, + PROTOCOL, + OBJECT_TYPE, + ACTION_TYPE, + DEFAULT_NAMESERVER_LIST, +}; diff --git a/components/opensrs/common/utils.mjs b/components/opensrs/common/utils.mjs new file mode 100644 index 0000000000000..610b09c16f156 --- /dev/null +++ b/components/opensrs/common/utils.mjs @@ -0,0 +1,177 @@ +import { XMLBuilder } from "fast-xml-parser"; +import constants from "./constants.mjs"; + +const builder = new XMLBuilder({ + ignoreAttributes: false, + suppressBooleanAttributes: false, + arrayNodeName: "item", + textNodeName: "value", +}); + +function toPascalCase(str) { + return str.replace(/(\w)(\w*)/g, (_, group1, group2) => + group1.toUpperCase() + group2.toLowerCase()); +} + +function toUpperCase(str) { + return str.toUpperCase(); +} + +function getMetadataProp({ + index, fieldName, prefix, label, labelTransform, +} = {}) { + const fieldIdx = index + 1; + const key = `${fieldName}${fieldIdx}`; + return { + prefix: prefix + ? `${prefix}${key}${constants.SEP}` + : `${key}${constants.SEP}`, + label: label + ? `${label} - ${labelTransform(fieldName)} ${fieldIdx}` + : `${labelTransform(fieldName)} ${fieldIdx}`, + }; +} + +function getFieldProps({ + index, fieldName, prefix, labelTransform = toUpperCase, + propsMapper = function propsMapper(prefix) { + const { [`${prefix}name`]: name } = this; + return { + name, + }; + }, +} = {}) { + const { prefix: metaPrefix } = getMetadataProp({ + index, + fieldName, + prefix, + labelTransform, + }); + return propsMapper(metaPrefix); +} + +function getFieldsProps({ + numberOfFields, fieldName, propsMapper, prefix, +} = {}) { + return Array.from({ + length: numberOfFields, + }).map((_, index) => getFieldProps({ + index, + fieldName, + prefix, + propsMapper, + })); +} + +function getAdditionalProps({ + numberOfFields, fieldName, prefix, label, labelTransform = toUpperCase, + getPropDefinitions = ({ + prefix, label, + }) => ({ + [`${prefix}name`]: { + type: "string", + label, + description: "The name of the field.", + optional: true, + }, + }), +} = {}) { + return Array.from({ + length: numberOfFields, + }).reduce((acc, _, index) => { + const { + prefix: metaPrefix, + label: metaLabel, + } = getMetadataProp({ + index, + fieldName, + prefix, + label, + labelTransform, + }); + + return Object.assign({}, acc, getPropDefinitions({ + prefix: metaPrefix, + label: metaLabel, + })); + }, {}); +} + +function getXml(body) { + return { + "?xml": { + "@_version": "1.0", + "@_encoding": "UTF-8", + "@_standalone": "no", + }, + "OPS_envelope": { + header: { + version: "0.9", + }, + body, + }, + }; +} + +function addItem(key, value) { + return value + ? [ + { + "@_key": key, + value, + }, + ] + : []; +} + +function addItemList(key, array) { + return array?.length + ? [ + { + "@_key": key, + "dt_array": { + item: array.map((value, index) => ({ + "@_key": index.toString(), + "dt_assoc": { + item: [ + ...addItem("name", value), + ...addItem("sortorder", index + 1), + ], + }, + })), + }, + }, + ] + : []; +} + +function addDnsRecord(key, records) { + return records.length + ? [ + { + "@_key": key, + "dt_array": { + item: records.map((record, index) => ({ + "@_key": index.toString(), + ...record, + })), + }, + }, + ] + : []; +} + +function buildXmlData(data) { + return builder.build(getXml(data)); +} + +export default { + buildXmlData, + addItem, + addItemList, + addDnsRecord, + getAdditionalProps, + getFieldsProps, + toUpperCase, + toPascalCase, +}; diff --git a/components/opensrs/opensrs.app.mjs b/components/opensrs/opensrs.app.mjs new file mode 100644 index 0000000000000..9cfe7fae96be7 --- /dev/null +++ b/components/opensrs/opensrs.app.mjs @@ -0,0 +1,88 @@ +import { createHash } from "crypto"; +import { XMLParser } from "fast-xml-parser"; +import { axios } from "@pipedream/platform"; +import utils from "./common/utils.mjs"; + +const parser = new XMLParser({ + ignoreAttributes: false, + arrayMode: true, + textNodeName: "value", +}); + +export default { + type: "app", + app: "opensrs", + propDefinitions: { + domain: { + type: "string", + label: "Domain", + description: "The domain name to register, update, or transfer.", + }, + }, + methods: { + getUrl() { + const { api_host_port: url } = this.$auth; + return url; + }, + generateSignature(data) { + const { api_key: apiKey } = this.$auth; + const signature = createHash("md5") + .update(data + apiKey) + .digest("hex"); + return createHash("md5") + .update(signature + apiKey) + .digest("hex"); + }, + getHeaders(data, headers) { + const { reseller_username: username } = this.$auth; + return { + ...headers, + "Content-Type": "text/xml", + "X-Username": username, + "X-Signature": this.generateSignature(data), + }; + }, + throwIfError(jsonResponse) { + const { item: items } = jsonResponse?.OPS_envelope?.body?.data_block?.dt_assoc || {}; + const attributes = items?.find((item) => item["@_key"] === "attributes"); + const { item: errorItems } = attributes?.dt_assoc || {}; + const errorItem = errorItems?.find((item) => item["@_key"] === "error"); + if (errorItem) { + throw new Error(errorItem.value); + } + + const isSuccessItem = items?.find((item) => item["@_key"] === "is_success"); + const responseTextItem = items?.find((item) => item["@_key"] === "response_text"); + + if (isSuccessItem?.value === 0 && responseTextItem) { + console.log("response", JSON.stringify(jsonResponse, null, 2)); + throw new Error(responseTextItem.value); + } + }, + async _makeRequest({ + $ = this, jsonBody, headers, jsonOutput = true, ...args + } = {}) { + const data = utils.buildXmlData(jsonBody); + + const response = await axios($, { + ...args, + url: this.getUrl(), + headers: this.getHeaders(data, headers), + data, + }); + + const jsonResponse = parser.parse(response); + this.throwIfError(jsonResponse); + + return jsonOutput + ? jsonResponse + : response; + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + }, +}; diff --git a/components/opensrs/package.json b/components/opensrs/package.json new file mode 100644 index 0000000000000..85ca2961ff30e --- /dev/null +++ b/components/opensrs/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/opensrs", + "version": "0.1.0", + "description": "Pipedream OpenSRS Components", + "main": "opensrs.app.mjs", + "keywords": [ + "pipedream", + "opensrs" + ], + "homepage": "https://pipedream.com/apps/opensrs", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "fast-xml-parser": "^4.3.2" + } +} diff --git a/components/opensrs/sources/common/events.mjs b/components/opensrs/sources/common/events.mjs new file mode 100644 index 0000000000000..0c3b8161ece03 --- /dev/null +++ b/components/opensrs/sources/common/events.mjs @@ -0,0 +1,13 @@ +export default { + CREATED: "CREATED", + REGISTERED: "REGISTERED", + RENEWED: "RENEWED", + EXPIRED: "EXPIRED", + DELETED: "DELETED", + NAMESERVER_UPDATE: "NAMESERVER_UPDATE", + REGISTRANT_VERIFICATION_STATUS_CHANGE: "REGISTRANT_VERIFICATION_STATUS_CHANGE", + ZONE_CHECK_STATUS_CHANGE: "ZONE_CHECK_STATUS_CHANGE", + MESSAGE_STATUS_CHANGE: "MESSAGE_STATUS_CHANGE", + CLAIM_STATUS_CHANGE: "CLAIM_STATUS_CHANGE", + STATUS_CHANGE: "STATUS_CHANGE", +}; diff --git a/components/opensrs/sources/common/polling.mjs b/components/opensrs/sources/common/polling.mjs new file mode 100644 index 0000000000000..610eee2759cdd --- /dev/null +++ b/components/opensrs/sources/common/polling.mjs @@ -0,0 +1,103 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../opensrs.app.mjs"; +import utils from "../../common/utils.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + sortFn() { + return; + }, + isResourceRelevant() { + return true; + }, + getResourceName() { + throw new ConfigurationError("getResourceName is not implemented"); + }, + getJsonBody() { + return { + data_block: { + dt_assoc: { + item: [ + ...utils.addItem("protocol", constants.PROTOCOL.XCP), + ...utils.addItem("object", constants.OBJECT_TYPE.EVENT), + ...utils.addItem("action", constants.ACTION_TYPE.POLL), + { + "@_key": "attributes", + "dt_assoc": { + item: [ + ...utils.addItem("limit", constants.DEFAULT_LIMIT), + ], + }, + }, + ], + }, + }, + }; + }, + getResourcesFnArgs() { + return { + debug: true, + jsonBody: this.getJsonBody(), + }; + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + }, + async run() { + const { + app, + isResourceRelevant, + getResourcesFnArgs, + processResource, + } = this; + + const { OPS_envelope: envelope } = await app.post(getResourcesFnArgs()); + + const { item: items } = envelope?.body?.data_block?.dt_assoc || {}; + const attributes = items?.find((item) => item["@_key"] === "attributes"); + + const { item: metadataItems } = attributes?.dt_assoc || {}; + const eventsMetadata = metadataItems?.find((item) => item["@_key"] === "events"); + + const { item: eventItems } = eventsMetadata?.dt_array || {}; + + const resources = eventItems.map(({ dt_assoc: { item: resourceItems } }) => { + const data = resourceItems.reduce((acc, item) => + Object.assign(acc, { + [item["@_key"]]: item.value, + }), {}); + + const objectData = resourceItems.find((item) => item["@_key"] === "object_data"); + const objectDataItems = objectData?.dt_assoc?.item.reduce((acc, item) => + Object.assign(acc, { + [item["@_key"]]: item.value, + }), {}); + return { + ...data, + objectData: objectDataItems, + }; + }); + + Array.from(resources) + .reverse() + .filter(isResourceRelevant) + .forEach(processResource); + }, +}; diff --git a/components/opensrs/sources/new-dns-zone-change/new-dns-zone-change.mjs b/components/opensrs/sources/new-dns-zone-change/new-dns-zone-change.mjs new file mode 100644 index 0000000000000..e0d21ce94ce57 --- /dev/null +++ b/components/opensrs/sources/new-dns-zone-change/new-dns-zone-change.mjs @@ -0,0 +1,29 @@ +import common from "../common/polling.mjs"; +import events from "../common/events.mjs"; +import constants from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "opensrs-new-dns-zone-change", + name: "New DNS Zone Change", + description: "Emit new event when the DNS/ZONE check has passed or failed at the registry. [See the documentation](https://domains.opensrs.guide/docs/zone_check_status_change-domain).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + isResourceRelevant(resource) { + return resource.object === constants.OBJECT_TYPE.DOMAIN + && resource.event === events.ZONE_CHECK_STATUS_CHANGE; + }, + generateMeta(resource) { + return { + id: resource.event_id, + summary: `New DNS Zone Change: ${resource.objectData.domain_name}`, + ts: Date.parse(resource.event_date), + }; + }, + }, + sampleEmit, +}; diff --git a/components/opensrs/sources/new-dns-zone-change/test-event.mjs b/components/opensrs/sources/new-dns-zone-change/test-event.mjs new file mode 100644 index 0000000000000..8642493bd5828 --- /dev/null +++ b/components/opensrs/sources/new-dns-zone-change/test-event.mjs @@ -0,0 +1,11 @@ +export default { + object: "DOMAIN", + event_id: "bf7b2cf6d8e835324241a4eae7be4ee5", + event: "ZONE_CHECK_STATUS_CHANGE", + event_date: "2014-05-27T19:29:25Z", + objectData: { + domain_name: "abc-fictional.de", + domain_id: "801743", + zone_check_status: "invalid", + }, +}; diff --git a/components/opensrs/sources/new-domain-registration/new-domain-registration.mjs b/components/opensrs/sources/new-domain-registration/new-domain-registration.mjs new file mode 100644 index 0000000000000..46534711eb24f --- /dev/null +++ b/components/opensrs/sources/new-domain-registration/new-domain-registration.mjs @@ -0,0 +1,29 @@ +import common from "../common/polling.mjs"; +import events from "../common/events.mjs"; +import constants from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "opensrs-new-domain-registration", + name: "New Domain Registration", + description: "Emit new event for each new domain registration. [See the documentation](https://domains.opensrs.guide/docs/registered-domain).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + isResourceRelevant(resource) { + return resource.object === constants.OBJECT_TYPE.DOMAIN + && resource.event === events.REGISTERED; + }, + generateMeta(resource) { + return { + id: resource.event_id, + summary: `New domain: ${resource.objectData.domain_name}`, + ts: Date.parse(resource.event_date), + }; + }, + }, + sampleEmit, +}; diff --git a/components/opensrs/sources/new-domain-registration/test-event.mjs b/components/opensrs/sources/new-domain-registration/test-event.mjs new file mode 100644 index 0000000000000..d952995d8c5d4 --- /dev/null +++ b/components/opensrs/sources/new-domain-registration/test-event.mjs @@ -0,0 +1,12 @@ +export default { + object: "DOMAIN", + event_id: "6887c7d3838dcaec7517c531df527bb7", + event: "REGISTERED", + event_date: "2014-05-27T19:29:25Z", + objectData: { + domain_name: "abc-fictional.de", + period: "1", + domain_id: "801743", + expiration_date: "2015-05-27T19:29:25Z", + }, +}; diff --git a/components/opensrs/sources/new-transfer-status/new-transfer-status.mjs b/components/opensrs/sources/new-transfer-status/new-transfer-status.mjs new file mode 100644 index 0000000000000..b0486da5573ed --- /dev/null +++ b/components/opensrs/sources/new-transfer-status/new-transfer-status.mjs @@ -0,0 +1,29 @@ +import common from "../common/polling.mjs"; +import events from "../common/events.mjs"; +import constants from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "opensrs-new-transfer-status", + name: "New Transfer Status", + description: "Emit new event when the status of a domain transfer changes. [See the documentation](https://domains.opensrs.guide/docs/status_change-transfer).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + isResourceRelevant(resource) { + return resource.object === constants.OBJECT_TYPE.TRANSFER + && resource.event === events.STATUS_CHANGE; + }, + generateMeta(resource) { + return { + id: resource.event_id, + summary: `New Transfer Status: ${resource.objectData.domain_name}`, + ts: Date.parse(resource.event_date), + }; + }, + }, + sampleEmit, +}; diff --git a/components/opensrs/sources/new-transfer-status/test-event.mjs b/components/opensrs/sources/new-transfer-status/test-event.mjs new file mode 100644 index 0000000000000..f0aa4961e3ce4 --- /dev/null +++ b/components/opensrs/sources/new-transfer-status/test-event.mjs @@ -0,0 +1,12 @@ +export default { + object: "TRANSFER", + event_id: "21bd704aba7804b839f87aee292fcf72", + event: "STATUS_CHANGE", + event_date: "2014-05-27T19:29:25Z", + objectData: { + domain_name: "abc-fictional.de", + transfer_id: "95094", + order_id: "701988", + transfer_status: "pending_owner", + }, +}; diff --git a/components/openweather_api/README.md b/components/openweather_api/README.md index 9dcee430e6101..88a69018e5e0d 100644 --- a/components/openweather_api/README.md +++ b/components/openweather_api/README.md @@ -1,28 +1,11 @@ # Overview -The OpenWeather API is one of the most powerful and comprehensive weather APIs -available today, allowing developers to build a wide range of applications and -services from simple weather forecasting to complex weather analysis. With -access to global weather data from a wide range of sources, including -professional and amateur weather stations, users can gain insight into climate -change, predict storms and floods, better manage existing and developing -weather hazards, and more. +The OpenWeather API offers real-time weather data, forecasts, and historical information, enabling you to integrate weather conditions into your applications or workflows. With Pipedream, you can harness this data to automate tasks like sending weather updates, triggering actions based on specific weather conditions, or combining it with other data sources to inform decision-making processes. -The OpenWeather API is designed to make life easier for developers, by allowing -users to easily interface with detailed and reliable weather data. Here are a -few ways developers can use the OpenWeather API: +# Example Use Cases -- Weather Forecasting: Forecast the temperatures and weather conditions of a - given location in the near and distant future. -- Real-time Weather Observation: Observe the current weather conditions at a - given location using real-time data from local and international sources. -- Rain and Snow Prediction: Predict the amount and intensity of rainfall and - snowfall in a certain area. -- Air Pollution Monitoring: Track air pollution levels in a given area. -- Historical Data Analyses: Analyze past weather and climate data to identify - patterns and trends. -- Risk Assessment: Use historical weather data to determine potential for - extreme weather events and provide risk management solutions. -- Emergency Preparedness: Prepare for hazardous weather conditions and plan for - worst-case scenarios by analyzing data from professional and amateur weather - stations. +- **Weather-Dependent Content Delivery**: Use OpenWeather API on Pipedream to tailor website or app content based on the user's local weather. For example, an e-commerce store can feature raincoats on rainy days or sunglasses on sunny days by dynamically updating its homepage based on the current weather data. + +- **Smart Home Automation**: Connect OpenWeather API to smart home devices via Pipedream to automate home environments. Set up a workflow where if the API forecasts rain, the system could close windows or adjust the home thermostat to maintain a comfortable temperature, saving energy and enhancing comfort. + +- **Agricultural Monitoring**: Create a Pipedream workflow that aids farmers by using OpenWeather API to monitor weather conditions. If the API predicts adverse weather, the workflow could alert farmers via SMS or email, allowing them to take preemptive actions to protect crops or adjust irrigation systems accordingly. diff --git a/components/opnform/opnform.app.mjs b/components/opnform/opnform.app.mjs new file mode 100644 index 0000000000000..5d7a7fb15c1ce --- /dev/null +++ b/components/opnform/opnform.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "opnform", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/opnform/package.json b/components/opnform/package.json new file mode 100644 index 0000000000000..5aa0ca20c8e7e --- /dev/null +++ b/components/opnform/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/opnform", + "version": "0.0.1", + "description": "Pipedream OpnForm Components", + "main": "opnform.app.mjs", + "keywords": [ + "pipedream", + "opnform" + ], + "homepage": "https://pipedream.com/apps/opnform", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/opsgenie/.gitignore b/components/opsgenie/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/opsgenie/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/opsgenie/README.md b/components/opsgenie/README.md new file mode 100644 index 0000000000000..e064f0293c085 --- /dev/null +++ b/components/opsgenie/README.md @@ -0,0 +1,11 @@ +# Overview + +The Opsgenie API enables automated incident management and alerting to streamline response actions. It allows you to create, update, and manage alerts, as well as configure and query users, schedules, and on-call rotations. Integrating Opsgenie with Pipedream opens up possibilities for orchestrating complex workflows, such as incident triggering from various sources, auto-update of tickets, synchronization with other tools, and much more, all in a serverless environment. + +# Example Use Cases + +- **Incident Alerting from Monitoring Tools**: Trigger an Opsgenie alert in Pipedream when a monitoring tool like Datadog or Prometheus detects an anomaly. This workflow can automatically escalate and notify the right teams based on the alert severity. + +- **Scheduled On-Call Rotations Sync**: Use Pipedream to sync Opsgenie's on-call schedules with Google Calendar. This ensures that any changes in Opsgenie are reflected on the team's calendars, keeping everyone informed of their on-call periods without manual updates. + +- **Auto-Resolution of Incidents**: Configure a workflow in Pipedream to watch for status updates from a linked ticketing system, such as Jira. When an incident is marked as resolved in Jira, automatically close the corresponding alert in Opsgenie. diff --git a/components/opsgenie/actions/add-note-alert/add-note-alert.mjs b/components/opsgenie/actions/add-note-alert/add-note-alert.mjs new file mode 100644 index 0000000000000..fc01dc48f01f4 --- /dev/null +++ b/components/opsgenie/actions/add-note-alert/add-note-alert.mjs @@ -0,0 +1,34 @@ +import opsgenie from "../../opsgenie.app.mjs"; + +export default { + key: "opsgenie-add-note-alert", + name: "Add Note to Alert", + description: "Adds a note to an existing alert in Opsgenie. [See the documentation](https://docs.opsgenie.com/docs/alert-api#add-note-to-alert)", + version: "0.0.1", + type: "action", + props: { + opsgenie, + alertId: { + propDefinition: [ + opsgenie, + "alertId", + ], + }, + note: { + type: "string", + label: "Note", + description: "Alert note to add", + }, + }, + async run({ $ }) { + const response = await this.opsgenie.addNoteToAlert({ + $, + alertId: this.alertId, + data: { + note: this.note, + }, + }); + $.export("$summary", `Successfully added note to alert with ID: ${this.alertId}`); + return response; + }, +}; diff --git a/components/opsgenie/actions/create-alert/create-alert.mjs b/components/opsgenie/actions/create-alert/create-alert.mjs new file mode 100644 index 0000000000000..8929af43cacc8 --- /dev/null +++ b/components/opsgenie/actions/create-alert/create-alert.mjs @@ -0,0 +1,65 @@ +import app from "../../opsgenie.app.mjs"; + +export default { + key: "opsgenie-create-alert", + name: "Create Alert", + description: "Send a new Alert for processing. [See the documentation](https://www.postman.com/api-evangelist/opsgenie/request/zuj17nj/create-alert)", + version: "0.0.2", + type: "action", + props: { + app, + user: { + propDefinition: [ + app, + "user", + ], + }, + message: { + propDefinition: [ + app, + "message", + ], + }, + note: { + propDefinition: [ + app, + "note", + ], + }, + description: { + propDefinition: [ + app, + "description", + ], + }, + tags: { + propDefinition: [ + app, + "tags", + ], + }, + priority: { + propDefinition: [ + app, + "priority", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createAlert({ + $, + data: { + message: this.message, + user: this.user, + note: this.note, + description: this.description, + tags: this.tags, + priority: this.priority, + }, + }); + + $.export("$summary", `Successfully sent a new alert for processing. The alert ID can be used to check the request status: '${response.requestId}'`); + + return response; + }, +}; diff --git a/components/opsgenie/actions/delete-alert/delete-alert.mjs b/components/opsgenie/actions/delete-alert/delete-alert.mjs new file mode 100644 index 0000000000000..33fe270945c8d --- /dev/null +++ b/components/opsgenie/actions/delete-alert/delete-alert.mjs @@ -0,0 +1,26 @@ +import opsgenie from "../../opsgenie.app.mjs"; + +export default { + key: "opsgenie-delete-alert", + name: "Delete Alert", + description: "Removes an existing alert from Opsgenie. [See the documentation](https://docs.opsgenie.com/docs/alert-api#delete-alert)", + version: "0.0.1", + type: "action", + props: { + opsgenie, + alertId: { + propDefinition: [ + opsgenie, + "alertId", + ], + }, + }, + async run({ $ }) { + const response = await this.opsgenie.deleteAlert({ + $, + alertId: this.alertId, + }); + $.export("$summary", `Successfully deleted alert with ID: ${this.alertId}`); + return response; + }, +}; diff --git a/components/opsgenie/actions/get-alert-status/get-alert-status.mjs b/components/opsgenie/actions/get-alert-status/get-alert-status.mjs new file mode 100644 index 0000000000000..192bac3cf86c3 --- /dev/null +++ b/components/opsgenie/actions/get-alert-status/get-alert-status.mjs @@ -0,0 +1,28 @@ +import app from "../../opsgenie.app.mjs"; + +export default { + key: "opsgenie-get-alert-status", + name: "Get Alert Status", + description: "Get the status of the alert with the specified ID. [See the documentation](https://www.postman.com/api-evangelist/opsgenie/request/03tcghu/get-request-status-of-alert)", + version: "0.0.2", + type: "action", + props: { + app, + requestId: { + propDefinition: [ + app, + "requestId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getAlertStatus({ + $, + requestId: this.requestId, + }); + + $.export("$summary", `Request processing status: '${response.data.status}'`); + + return response; + }, +}; diff --git a/components/opsgenie/app/opsgenie.app.ts b/components/opsgenie/app/opsgenie.app.ts deleted file mode 100644 index e7a1f2b97e51e..0000000000000 --- a/components/opsgenie/app/opsgenie.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "opsgenie", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/opsgenie/common/constants.mjs b/components/opsgenie/common/constants.mjs new file mode 100644 index 0000000000000..15af37ddffac1 --- /dev/null +++ b/components/opsgenie/common/constants.mjs @@ -0,0 +1,10 @@ +export default { + DEFAULT_LIMIT: 20, + PRIORITY_OPTIONS: [ + "P1", + "P2", + "P3", + "P4", + "P5", + ], +}; diff --git a/components/opsgenie/opsgenie.app.mjs b/components/opsgenie/opsgenie.app.mjs new file mode 100644 index 0000000000000..a7b455d1330e0 --- /dev/null +++ b/components/opsgenie/opsgenie.app.mjs @@ -0,0 +1,148 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "opsgenie", + propDefinitions: { + alertId: { + type: "string", + label: "Alert ID", + description: "ID of the alert", + async options({ page }) { + const limit = constants.DEFAULT_LIMIT; + const { data } = await this.listAlerts({ + params: { + limit, + offset: page * limit, + }, + }); + return data?.map(({ + id: value, message: label, + }) => ({ + value, + label, + })) || []; + }, + }, + user: { + type: "string", + label: "User ID", + description: "ID of the User", + async options() { + const response = await this.listUsers(); + const usersIds = response.data; + return usersIds.map(({ + id, fullName, + }) => ({ + value: id, + label: fullName, + })); + }, + }, + message: { + type: "string", + label: "Message", + description: "The message of the alert", + }, + note: { + type: "string", + label: "Note", + description: "Note of the alert", + }, + description: { + type: "string", + label: "Description", + description: "Description of the alert", + }, + tags: { + type: "string[]", + label: "Tags", + description: "Tags of the alert", + }, + priority: { + type: "string", + label: "Priority", + description: "Priority of the alert", + options: constants.PRIORITY_OPTIONS, + }, + requestId: { + type: "string", + label: "Request ID", + description: "ID of the alert request to be checked", + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.instance_region}.opsgenie.com/v2`; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + APIKey, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + ...headers, + Authorization: `GenieKey ${APIKey}`, + }, + }); + }, + listAlerts(opts = {}) { + return this._makeRequest({ + path: "/alerts", + APIKey: this.$auth.team_api_key, + ...opts, + }); + }, + createAlert(args = {}) { + return this._makeRequest({ + method: "post", + path: "/alerts", + APIKey: this.$auth.team_api_key, + ...args, + }); + }, + getAlertStatus({ + requestId, ...args + }) { + return this._makeRequest({ + path: `/alerts/requests/${requestId}`, + APIKey: this.$auth.team_api_key, + ...args, + }); + }, + listUsers(args = {}) { + return this._makeRequest({ + path: "/users", + APIKey: this.$auth.api_key, + ...args, + }); + }, + addNoteToAlert({ + alertId, ...args + }) { + return this._makeRequest({ + method: "POST", + path: `/alerts/${alertId}/notes`, + APIKey: this.$auth.team_api_key, + ...args, + }); + }, + deleteAlert({ + alertId, ...args + }) { + return this._makeRequest({ + method: "DELETE", + path: `/alerts/${alertId}`, + APIKey: this.$auth.team_api_key, + ...args, + }); + }, + }, +}; diff --git a/components/opsgenie/package.json b/components/opsgenie/package.json index 823a199c4e7bb..a18bfdf3a4315 100644 --- a/components/opsgenie/package.json +++ b/components/opsgenie/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/opsgenie", - "version": "0.0.2", + "version": "0.2.0", "description": "Pipedream Opsgenie Components", - "main": "dist/app/opsgenie.app.mjs", + "main": "opsgenie.app.mjs", "keywords": [ "pipedream", "opsgenie" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/opsgenie", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" } } diff --git a/components/optimoroute/README.md b/components/optimoroute/README.md new file mode 100644 index 0000000000000..a9ea4a20126ce --- /dev/null +++ b/components/optimoroute/README.md @@ -0,0 +1,11 @@ +# Overview + +The OptimoRoute API offers a range of functionalities for route optimization and workforce management. By tapping into this API via Pipedream, you can automate complex logistics and delivery operations, scheduling, and real-time order tracking. Leverage the power of serverless workflows to integrate OptimoRoute with other apps, streamlining your dispatch, improving delivery times, and maximizing efficiency. Whether you're planning routes for a fleet of vehicles or scheduling service appointments, the OptimoRoute API provides the data you need to make decisions on-the-fly. + +# Example Use Cases + +- **Dynamic Route Optimization**: Create a workflow that listens for new delivery orders from your e-commerce platform (like Shopify). When a new order is placed, the workflow can automatically send the details to OptimoRoute to optimize the delivery route and then update the delivery status back in Shopify. + +- **Real-Time Driver Dispatch**: Pair OptimoRoute with a communication platform like Twilio. Use Pipedream to monitor route progress and, if there's a delay or change, automatically notify the customer via SMS. This keeps customers informed and improves the customer experience. + +- **Scheduled Route Planning**: Integrate OptimoRoute with Google Calendar using Pipedream. Automatically populate the calendar with optimized routes for the day. This helps drivers start their day with a clear schedule, which they can access from any device. diff --git a/components/optimoroute/optimoroute.app.mjs b/components/optimoroute/optimoroute.app.mjs new file mode 100644 index 0000000000000..e109a82c48617 --- /dev/null +++ b/components/optimoroute/optimoroute.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "optimoroute", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/optimoroute/package.json b/components/optimoroute/package.json new file mode 100644 index 0000000000000..a42536d6e63ea --- /dev/null +++ b/components/optimoroute/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/optimoroute", + "version": "0.0.1", + "description": "Pipedream OptimoRoute Components", + "main": "optimoroute.app.mjs", + "keywords": [ + "pipedream", + "optimoroute" + ], + "homepage": "https://pipedream.com/apps/optimoroute", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/orbisx/README.md b/components/orbisx/README.md new file mode 100644 index 0000000000000..4e114a2117ac6 --- /dev/null +++ b/components/orbisx/README.md @@ -0,0 +1,11 @@ +# Overview + +The OrbisX API offers a platform for detailing businesses to manage customer relationships, appointments, services, and more. In Pipedream, you can harness this API to automate various tasks, streamline business workflows, and integrate with other services to enhance operational efficiency. Using Pipedream's serverless platform, you can set up event-driven workflows that respond to data from OrbisX in real-time, or on a schedule, without the need to manage infrastructure. + +# Example Use Cases + +- **Appointment Confirmation and Follow-Up**: Trigger a workflow on Pipedream when a new appointment is scheduled in OrbisX. Send an SMS via Twilio to confirm the appointment with the customer and follow up with an email through SendGrid post-service to solicit feedback or offer a discount on their next visit. + +- **Customer Data Synchronization**: Sync customer data between OrbisX and other CRM platforms like Salesforce or HubSpot. Whenever a new customer is added or updated in OrbisX, replicate the changes in the other CRM to maintain consistent and up-to-date records across platforms. + +- **Service Analysis and Reporting**: Collect data on services rendered from OrbisX and feed it into a Google Sheets spreadsheet for analysis. Use this data to gain insights on the most popular services, peak times, and customer preferences, which can inform your marketing and operational strategies. diff --git a/components/orbit/README.md b/components/orbit/README.md index 2b054cb163d42..5ce776e6e10a1 100644 --- a/components/orbit/README.md +++ b/components/orbit/README.md @@ -1,22 +1,11 @@ # Overview -The Orbit API is an open source toolkit for building real-time synchronization -and communication applications in the browser. It allows developers to easily -and efficiently build user experiences that are multi-user, real-time, and -fully secure. With the Orbit API, you can easily create applications that are -multi-user and real-time, with no servers needed. Here are some examples of -applications that you can build with the Orbit API: +The Orbit API enables you to monitor and manage the uptime and performance of your websites and internet services. Through automation and integration on Pipedream, you can streamline incident management, aggregate performance data, and enhance communication across your tech stack. By leveraging real-time data from Orbit, you can trigger workflows based on site status changes, get aggregated insights, and automate responses to performance issues. -- Multiplayer Games - Create real-time gaming experiences with multiple players - interacting in the same environment. -- Collaboration Apps - Create multi-user applications where multiple people can - share data, edit documents, and collaborate in the same environment. -- Chat - Create an application where multiple people can chat in real-time. -- Real-Time Dashboards - Connect multiple users to the same data source, and - display information in real-time on a dashboard. -- Trading Platforms - Create advanced real-time trading platforms where users - can view pricing information in real-time and respond with trades. -- Tracking Apps - Create applications for tracking objects and viewing them in - real-time. -- Virtual Reality - Create a virtual reality experience with users interacting - in the same environment. +# Example Use Cases + +- **Incident Response Automation**: When Orbit detects a website down event, trigger an automated workflow on Pipedream that sends an alert to Slack, creates an incident in PagerDuty, and sends an email to the support team. This ensures that the right people are notified instantly for a quick response. + +- **Performance Monitoring Dashboard**: Collect and aggregate data from Orbit on site performance and uptime, then use Pipedream to send this data to a Google Sheets document. Create a live dashboard that provides an at-a-glance view of the health of all monitored services, making it easier for the team to spot issues and trends over time. + +- **Scheduled Uptime Reports**: Set up a Pipedream workflow that periodically retrieves uptime statistics from Orbit and compiles a comprehensive report. Email this report to stakeholders on a weekly or monthly basis, or post it to a team channel in Microsoft Teams, to maintain transparency and keep everyone informed about site reliability. diff --git a/components/orca_scan/README.md b/components/orca_scan/README.md new file mode 100644 index 0000000000000..de3c2d9db366c --- /dev/null +++ b/components/orca_scan/README.md @@ -0,0 +1,11 @@ +# Overview + +The Orca Scan API is a gateway to integrate barcode scanning and inventory management capabilities into various workflows. By leveraging the API, you can automate data collection, streamline inventory tracking, and sync your barcode scanning data with other systems. When used on Pipedream, you can merge Orca Scan functionalities with countless other apps, creating custom automation rules, processing data, and managing inventory in real-time. Ideal for inventory management, asset tracking, and data collection automation, the Orca Scan API is a tool that can adapt to a myriad of business needs. + +# Example Use Cases + +- **Automate Inventory Updates**: Use the Orca Scan API to update inventory counts in a Google Sheet every time a new item is scanned. This workflow ensures real-time inventory tracking without manual data entry. + +- **Sync with CRM Systems**: After scanning items using Orca Scan, trigger a workflow on Pipedream to create or update records in a CRM like Salesforce or HubSpot. This keeps customer-related inventory data up-to-date. + +- **Alerts for Low Stock**: Set up a workflow that monitors inventory levels using the Orca Scan API. When stock for certain items drops below a specified threshold, trigger an alert via email or a messaging app like Slack to prompt restocking. diff --git a/components/order_desk/README.md b/components/order_desk/README.md new file mode 100644 index 0000000000000..18c621e93d59d --- /dev/null +++ b/components/order_desk/README.md @@ -0,0 +1,11 @@ +# Overview + +The Order Desk API allows you to automate and streamline order management processes within the Order Desk platform. With this API, you have the power to create, update, and get detailed information on orders, inventory, and shipments. Pipedream's serverless platform enables you to connect the Order Desk API with numerous other apps and services to build dynamic workflows. Whether you're syncing order data, managing inventory, or connecting to fulfillment services, Pipedream can help you craft custom automations that save time and reduce manual errors. + +# Example Use Cases + +- **Order Sync to Google Sheets**: Sync new orders from Order Desk into a Google Sheets spreadsheet. Each time an order is created or updated, a Pipedream workflow is triggered, appending the order details to a sheet. This allows for easy sharing and reporting on order data. + +- **Slack Notifications for High-Value Orders**: Send a Slack message for every order over a certain amount. When a high-value order is detected in Order Desk, Pipedream triggers a workflow that posts a notification with the order details to a designated Slack channel, keeping your team immediately informed. + +- **Shopify Order Import**: Automatically import new orders from Shopify to Order Desk. Set up a Pipedream workflow that listens to Shopify order webhooks and creates corresponding orders in Order Desk, providing a seamless connection between your storefront and order management system. diff --git a/components/originality_ai/README.md b/components/originality_ai/README.md new file mode 100644 index 0000000000000..5627c453d6ade --- /dev/null +++ b/components/originality_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +The Originality.ai API provides functionality to check content for potential plagiarism and AI-generated text. Within Pipedream's serverless environment, you can construct workflows that automate the process of evaluating originality in large volumes of text, integrate plagiarism checks into content pipelines, and flag content for further review. By leveraging this API in Pipedream, you can harness event-driven, scalable workflows to ensure content integrity across various platforms. + +# Example Use Cases + +- **Content Monitoring Pipeline**: Build a workflow that monitors content submissions on a platform like WordPress or Shopify. When new content is detected, use Originality.ai to assess its originality and flag potential issues for editorial review. + +- **Automated Plagiarism Checker for Document Uploads**: Create a system where documents uploaded to Google Drive or Dropbox are automatically sent to Originality.ai for plagiarism detection. The results could then be logged into a Google Sheet for record-keeping or trigger a notification if a certain plagiarism threshold is crossed. + +- **Quality Assurance for User-Generated Content**: Develop an automation that checks forum posts, product reviews, or comments (from platforms like Discourse or Reddit) using Originality.ai. This workflow could help maintain quality and authenticity by highlighting suspicious content for moderation. diff --git a/components/orimon/actions/send-message/send-message.mjs b/components/orimon/actions/send-message/send-message.mjs new file mode 100644 index 0000000000000..085726cf5f592 --- /dev/null +++ b/components/orimon/actions/send-message/send-message.mjs @@ -0,0 +1,43 @@ +import orimon from "../../orimon.app.mjs"; + +export default { + key: "orimon-send-message", + name: "Send Message to Orimon", + description: "Sends a direct message to Orimon. [See the documentation](https://orimon.gitbook.io/docs/developer-api/message-api)", + version: "0.0.1", + type: "action", + props: { + orimon, + text: { + type: "string", + label: "Message Text", + description: "The text message you want Orimon to receive.", + }, + }, + async run({ $ }) { + const tenantId = this.orimon.$auth.tenant_id; + + const response = await this.orimon.sendMessage({ + $, + data: { + type: "message", + info: { + psid: `${Date.parse(new Date())}_${tenantId}`, + sender: "user", + tenantId: tenantId, + platformName: "web", + }, + message: { + id: tenantId, + type: "text", + payload: { + text: this.text, + }, + }, + }, + }); + + $.export("$summary", "Message sent successfully to Orimon!"); + return response; + }, +}; diff --git a/components/orimon/orimon.app.mjs b/components/orimon/orimon.app.mjs new file mode 100644 index 0000000000000..ef3284d6ef2a6 --- /dev/null +++ b/components/orimon/orimon.app.mjs @@ -0,0 +1,32 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "orimon", + methods: { + _baseUrl() { + return "https://channel-connector.orimon.ai/orimon/v1"; + }, + _headers() { + return { + "Authorization": `apiKey ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path = "/", ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + sendMessage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/conversation/api/message", + ...opts, + }); + }, + }, +}; diff --git a/components/orimon/package.json b/components/orimon/package.json new file mode 100644 index 0000000000000..84c6209c9806c --- /dev/null +++ b/components/orimon/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/orimon", + "version": "0.1.0", + "description": "Pipedream Orimon Components", + "main": "orimon.app.mjs", + "keywords": [ + "pipedream", + "orimon" + ], + "homepage": "https://pipedream.com/apps/orimon", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} + diff --git a/components/ortto/.gitignore b/components/ortto/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/ortto/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/ortto/README.md b/components/ortto/README.md index 3734050fbdb15..554c08461864a 100644 --- a/components/ortto/README.md +++ b/components/ortto/README.md @@ -1,19 +1,11 @@ # Overview -Ortto is a web-based API that provides developers with turnkey functionality -for creating and managing websites, mobile apps, and other digital projects. -With an Ortto account, developers can build high-quality applications quickly -and efficiently. +The Ortto API unleashes the power to streamline customer relationship management by automating personalized customer journeys, tracking user actions, and integrating with a variety of services to enhance marketing and sales strategies. With Ortto on Pipedream, you can create intricate workflows that respond dynamically to customer interactions, update CRM records, send targeted communications, and much more, building a rich, automated system that nurtures leads and cultivates customer loyalty. -The Ortto API offers a range of features and tools, such as user -authentication, data storage, and analytics, so developers can create -comprehensive digital projects. Here are some of the projects developers can -create with Ortto: +# Example Use Cases -- Fully featured websites, including eCommerce stores -- Mobile applications with user authentication -- Apps for interacting with data stored in the cloud -- Custom web-based dashboards -- Interactive tools for collecting and visualizing user data -- APIs for connecting apps with third-party services -- Chatbots for automating tasks and customer support +- **Automated Lead Scoring and Segmentation**: Capture leads from multiple sources, like web forms or chatbots, use Ortto to score these leads based on predefined criteria, and segment them within Ortto for targeted campaigning. Pair with Pipedream's ability to connect to databases, enabling you to enrich lead data and fine-tune your scoring algorithms over time. + +- **Customer Journey Optimization**: Track customer interactions across various touchpoints, analyze behavior with Ortto, and trigger personalized workflows on Pipedream that deliver custom content, product recommendations, or support resources at critical moments in the customer journey, enhancing experiences and boosting conversion rates. + +- **Event-Driven Campaign Management**: Utilize webhooks from other apps to inform Ortto of specific events, like cart abandonment or app engagement. Create a workflow on Pipedream that triggers a series of actions, such as sending a follow-up email sequence or an SMS reminder, to reel customers back in and drive sales. diff --git a/components/ortto/actions/create-custom-activity/create-custom-activity.mjs b/components/ortto/actions/create-custom-activity/create-custom-activity.mjs new file mode 100644 index 0000000000000..fe7753fa909ee --- /dev/null +++ b/components/ortto/actions/create-custom-activity/create-custom-activity.mjs @@ -0,0 +1,51 @@ +import { parseObject } from "../../common/utils.mjs"; +import ortto from "../../ortto.app.mjs"; + +export default { + key: "ortto-create-custom-activity", + name: "Create Custom Activity", + description: "Creates a unique activity for a person. Can optionally initialize a new record beforehand. [See the documentation](https://help.ortto.com/a-271-create-a-custom-activity-event-create)", + version: "0.0.1", + type: "action", + props: { + ortto, + activityId: { + propDefinition: [ + ortto, + "activityId", + ], + }, + attributes: { + propDefinition: [ + ortto, + "attributes", + ], + }, + fields: { + propDefinition: [ + ortto, + "fields", + ], + }, + }, + async run({ $ }) { + const response = await this.ortto.createCustomActivity({ + $, + data: { + "activities": [ + { + activity_id: this.activityId, + attributes: parseObject(this.attributes), + fields: parseObject(this.fields), + }, + ], + "merge_by": [ + "str::email", + ], + }, + }); + + $.export("$summary", "Successfully created activity"); + return response; + }, +}; diff --git a/components/ortto/actions/create-person/create-person.mjs b/components/ortto/actions/create-person/create-person.mjs new file mode 100644 index 0000000000000..e9962f3402959 --- /dev/null +++ b/components/ortto/actions/create-person/create-person.mjs @@ -0,0 +1,108 @@ +import { clearObj } from "../../common/utils.mjs"; +import ortto from "../../ortto.app.mjs"; + +export default { + key: "ortto-create-person", + name: "Create or Update a Person", + description: "Create or update a preexisting person in the Ortto account. [See the documentation](https://help.ortto.com/a-250-api-reference)", + version: "0.0.1", + type: "action", + props: { + ortto, + firstName: { + type: "string", + label: "First Name", + description: "The person's first name.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "The person's last name.", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The person's phone number.", + }, + email: { + type: "string", + label: "Email", + description: "The person's email address.", + }, + city: { + type: "string", + label: "City", + description: "The person's address city.", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "The person's address country.", + optional: true, + }, + region: { + type: "string", + label: "Region", + description: "The person's address region.", + optional: true, + }, + birthday: { + type: "string", + label: "Birthday", + description: "The person's birth date.", + optional: true, + }, + }, + async run({ $ }) { + const { + ortto, + ...props + } = this; + + const birthday = {}; + if (props.birthday) { + const date = new Date(props.birthday); + birthday.day = date.getDate(); + birthday.month = date.getMonth() + 1; + birthday.year = date.getFullYear(); + } + + const response = await ortto.createPerson({ + data: { + people: [ + { + fields: clearObj({ + "str::first": props.firstName, + "str::last": props.lastName, + "phn::phone": { + "phone": props.phone, + "parse_with_country_code": true, + }, + "str::email": props.email, + "geo::city": { + name: props.city, + }, + "geo::country": { + name: props.country, + }, + "geo::region": { + name: props.region, + }, + "dtz::b": birthday, + }), + }, + ], + async: false, + merge_by: [ + "str::email", + ], + find_strategy: 0, + }, + }); + $.export("$summary", "Person successfully initialized or updated!"); + return response; + }, +}; diff --git a/components/ortto/actions/opt-out-sms/opt-out-sms.mjs b/components/ortto/actions/opt-out-sms/opt-out-sms.mjs new file mode 100644 index 0000000000000..23493003267db --- /dev/null +++ b/components/ortto/actions/opt-out-sms/opt-out-sms.mjs @@ -0,0 +1,39 @@ +import ortto from "../../ortto.app.mjs"; + +export default { + key: "ortto-opt-out-sms", + name: "Opt Out of SMS", + description: "Allows a user to opt-out from all SMS communications. [See the documentation](https://help.ortto.com/a-250-api-reference)", + version: "0.0.1", + type: "action", + props: { + ortto, + userEmail: { + propDefinition: [ + ortto, + "userEmail", + ], + }, + }, + async run({ $ }) { + const response = await this.ortto.updatePerson({ + data: { + people: [ + { + fields: { + "str::email": this.userEmail, + "bol::sp": false, + }, + }, + ], + async: false, + merge_by: [ + "str::email", + ], + }, + }); + + $.export("$summary", `Successfully opted out SMS for User ID: ${this.userEmail}`); + return response; + }, +}; diff --git a/components/ortto/app/ortto.app.ts b/components/ortto/app/ortto.app.ts deleted file mode 100644 index 3cf734f0e1e4c..0000000000000 --- a/components/ortto/app/ortto.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "ortto", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/ortto/common/constants.mjs b/components/ortto/common/constants.mjs new file mode 100644 index 0000000000000..ea830c15a04cb --- /dev/null +++ b/components/ortto/common/constants.mjs @@ -0,0 +1 @@ +export const LIMIT = 100; diff --git a/components/ortto/common/utils.mjs b/components/ortto/common/utils.mjs new file mode 100644 index 0000000000000..adf012089ff85 --- /dev/null +++ b/components/ortto/common/utils.mjs @@ -0,0 +1,41 @@ +export const clearObj = (obj) => { + return Object.entries(obj) + .filter(([ + , + v, + ]) => (v != null && v != "" && JSON.stringify(v) != "{}")) + .reduce((acc, [ + k, + v, + ]) => ({ + ...acc, + [k]: (!Array.isArray(v) && v === Object(v)) + ? clearObj(v) + : v, + }), {}); +}; + +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/ortto/ortto.app.mjs b/components/ortto/ortto.app.mjs new file mode 100644 index 0000000000000..fb899b14a3dd4 --- /dev/null +++ b/components/ortto/ortto.app.mjs @@ -0,0 +1,115 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "ortto", + propDefinitions: { + userEmail: { + type: "string", + label: "User Email", + description: "Specify the user email to opt out from all SMS communications.", + async options({ page }) { + const { contacts } = await this.listPeople({ + data: { + limit: LIMIT, + offset: LIMIT * page, + fields: [ + "str::first", + "str::last", + "str::email", + ], + }, + }); + + return contacts.map(({ fields }) => ({ + label: `${fields["str::first"]} ${fields["str::last"]} (${fields["str::email"]})`, + value: fields["str::email"], + })); + }, + }, + activityId: { + type: "string", + label: "Activity Id", + description: "The Id of the activity definition. To find Activity ID, login into your Ortto app > CDP > Activities > Select an activity, then you can find the Activity ID in the browser URL as `https://ortto.app/{Org}/activities/{Activity_ID}/overview`. For example, if your Activity URL is `https://ortto.app/pipedreamtest/activities/act::s/overview`, then your Activity ID is `act::s`", + }, + fields: { + type: "object", + label: "Fields", + description: "The object containing the fields for a person associated with the event.", + }, + attributes: { + type: "object", + label: "Attributes", + description: "An object with the attributes. To find Activity attributes, login into your Ortto app > CDP > Activities > Select an activity > Developer.", + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.region}/v1`; + }, + _headers() { + return { + "X-Api-Key": `${this.$auth.api_key}`, + "Content-Type": "application/json", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + method: "POST", + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listPeople(opts = {}) { + return this._makeRequest({ + path: "/person/get", + ...opts, + }); + }, + updatePerson(opts = {}) { + return this._makeRequest({ + path: "/person/merge", + ...opts, + }); + }, + createPerson(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/person/merge", + ...opts, + }); + }, + createCustomActivity(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/activities/create", + ...opts, + }); + }, + async *paginate({ + fn, data = {}, fieldName, ...opts + }) { + let hasMore = false; + let page = 0; + + do { + data.limit = LIMIT; + data.offset = LIMIT * page++; + const response = await fn({ + data, + ...opts, + }); + for (const d of response[fieldName]) { + yield d; + } + + hasMore = response.has_more; + + } while (hasMore); + }, + }, +}; diff --git a/components/ortto/package.json b/components/ortto/package.json index 791493cd88960..3411ff2897a21 100644 --- a/components/ortto/package.json +++ b/components/ortto/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/ortto", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream Ortto Components", - "main": "dist/app/ortto.app.mjs", + "main": "ortto.app.mjs", "keywords": [ "pipedream", "ortto" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/ortto", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/ortto/sources/common/base.mjs b/components/ortto/sources/common/base.mjs new file mode 100644 index 0000000000000..0479c45dd01af --- /dev/null +++ b/components/ortto/sources/common/base.mjs @@ -0,0 +1,65 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import ortto from "../../ortto.app.mjs"; + +export default { + props: { + ortto, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + + const response = this.ortto.paginate({ + fn: this.getFunction(), + fieldName: this.getFieldName(), + data: { + "fields": this.getFields(), + "sort_by_field_id": "c", + "sort_order": "desc", + }, + }); + + let responseArray = []; + for await (const item of response) { + if (Date.parse(item.fields.c) <= lastDate) break; + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastDate(responseArray[0].id); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item.fields.c || new Date()), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/ortto/sources/new-contact-created/new-contact-created.mjs b/components/ortto/sources/new-contact-created/new-contact-created.mjs new file mode 100644 index 0000000000000..e4d5235c9f673 --- /dev/null +++ b/components/ortto/sources/new-contact-created/new-contact-created.mjs @@ -0,0 +1,48 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ortto-new-contact-created", + name: "New Contact Created", + description: "Emit new event when a contact is created in your Ortto account.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.ortto.listPeople; + }, + getFieldName() { + return "contacts"; + }, + getFields() { + return [ + "str::first", + "str::last", + "phn::phone", + "str::email", + "geo::city", + "geo::country", + "dtz::b", + "geo::region", + "str::postal", + "tags", + "u4s::t", + "bol::gdpr", + "str::ei", + "bol::p", + "str::u-ctx", + "str::s-ctx", + "bol::sp", + "str::soi-ctx", + "str::soo-ctx", + ]; + }, + getSummary(item) { + return `New Contact: ${item.fields["str::first"] || ""} ${item.fields["str::last"] || ""} ${item.fields["str::email"] || ""} `; + }, + }, + sampleEmit, +}; diff --git a/components/ortto/sources/new-contact-created/test-event.mjs b/components/ortto/sources/new-contact-created/test-event.mjs new file mode 100644 index 0000000000000..c57c01771ccb2 --- /dev/null +++ b/components/ortto/sources/new-contact-created/test-event.mjs @@ -0,0 +1,8 @@ +export default { + "id":"0061b02b24f9b6f85dcb1e00", + "fields":{ + "str::ei":"c533532fe5d16c7d4fa4c7f0", + "str::email":"alex@example.com", + "str::first":"Alex" + } +} \ No newline at end of file diff --git a/components/ory/ory.app.mjs b/components/ory/ory.app.mjs new file mode 100644 index 0000000000000..655c485a005c8 --- /dev/null +++ b/components/ory/ory.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "ory", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/ory/package.json b/components/ory/package.json new file mode 100644 index 0000000000000..f45b4fe52a578 --- /dev/null +++ b/components/ory/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/ory", + "version": "0.0.1", + "description": "Pipedream Ory Components", + "main": "ory.app.mjs", + "keywords": [ + "pipedream", + "ory" + ], + "homepage": "https://pipedream.com/apps/ory", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/osu/osu.app.mjs b/components/osu/osu.app.mjs new file mode 100644 index 0000000000000..657629379c64b --- /dev/null +++ b/components/osu/osu.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "osu", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/osu/package.json b/components/osu/package.json new file mode 100644 index 0000000000000..d5d2e91734c5a --- /dev/null +++ b/components/osu/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/osu", + "version": "0.0.1", + "description": "Pipedream Osu! Components", + "main": "osu.app.mjs", + "keywords": [ + "pipedream", + "osu" + ], + "homepage": "https://pipedream.com/apps/osu", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/otter_waiver/otter_waiver.app.mjs b/components/otter_waiver/otter_waiver.app.mjs new file mode 100644 index 0000000000000..e9c0c767f5658 --- /dev/null +++ b/components/otter_waiver/otter_waiver.app.mjs @@ -0,0 +1,51 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "otter_waiver", + methods: { + _baseUrl() { + return "https://api.otterwaiver.com"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + async _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + async createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhook/subscribe", + ...opts, + }); + }, + async deleteWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhook/unsubscribe", + ...opts, + }); + }, + async getLatestCheckIns() { + return this._makeRequest({ + method: "GET", + path: "/participants/latest/checkins", + }); + }, + async getLatestParticipants() { + return this._makeRequest({ + method: "GET", + path: "/participants/latest", + }); + }, + }, +}; diff --git a/components/otter_waiver/package.json b/components/otter_waiver/package.json new file mode 100644 index 0000000000000..a939ee695cbad --- /dev/null +++ b/components/otter_waiver/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/otter_waiver", + "version": "0.1.0", + "description": "Pipedream Otter Waiver Components", + "main": "otter_waiver.app.mjs", + "keywords": [ + "pipedream", + "otter_waiver" + ], + "homepage": "https://pipedream.com/apps/otter_waiver", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/otter_waiver/sources/common/base.mjs b/components/otter_waiver/sources/common/base.mjs new file mode 100644 index 0000000000000..bd3cfb45247e0 --- /dev/null +++ b/components/otter_waiver/sources/common/base.mjs @@ -0,0 +1,59 @@ +// import crypto from "crypto"; +import otterWaiver from "../../otter_waiver.app.mjs"; + +export default { + type: "source", + dedupe: "unique", + props: { + otterWaiver, + http: { + type: "$.interface.http", + customResponse: true, + }, + }, + methods: { + emitEvent(event) { + this.$emit(event, { + id: event.id, + summary: this.getSummary(event), + ts: Date.now(), + }); + }, + }, + hooks: { + async deploy() { + const fn = this.getFunction(); + const response = await fn(); + if (response.length > 25) response.length = 25; + + for (const item of response) { + this.emitEvent(item); + } + }, + async activate() { + await this.otterWaiver.createWebhook({ + data: { + trigger: this.getEvent(), + webhook: this.http.endpoint, + }, + }); + + }, + async deactivate() { + await this.otterWaiver.deleteWebhook({ + data: { + trigger: this.getEvent(), + webhook: this.http.endpoint, + }, + }); + }, + }, + async run({ body }) { + this.http.respond({ + status: 200, + body: "OK", + }); + + this.emitEvent(body); + }, +}; diff --git a/components/otter_waiver/sources/new-check-in-instant/new-check-in-instant.mjs b/components/otter_waiver/sources/new-check-in-instant/new-check-in-instant.mjs new file mode 100644 index 0000000000000..5b37fc31b38f2 --- /dev/null +++ b/components/otter_waiver/sources/new-check-in-instant/new-check-in-instant.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "otter_waiver-new-check-in-instant", + name: "New Check-In (Instant)", + description: "Emit new event when a participant checks into an event.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return "checkin"; + }, + getFunction() { + return this.otterWaiver.getLatestCheckIns; + }, + getSummary(checkIn) { + return `New check-in: ${checkIn.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/otter_waiver/sources/new-check-in-instant/test-event.mjs b/components/otter_waiver/sources/new-check-in-instant/test-event.mjs new file mode 100644 index 0000000000000..2a69df62f5a42 --- /dev/null +++ b/components/otter_waiver/sources/new-check-in-instant/test-event.mjs @@ -0,0 +1,78 @@ +export default { + "id": "12345678-1234-1234-1234-123456789012", + "waiverId": "12345678-1234-1234-1234-123456789012", + "type": "adult", + "signee": null, + "emergencyContact": null, + "additionalFieldsData": [], + "hasAdditionalFields": false, + "source": "web", + "documentUrl": null, + "certificateUrl": null, + "combinedCertificateUrl": "https://media.otterwaiver.com/waivers/participantsCertificate/12345678-1234-1234-1234-123456789012-123456789012.pdf", + "timeStamps": { + "signed": "2024-07-24T19:37:10.117Z", + "viewed": "2024-07-24T19:36:57.667Z", + "timeZone": "-03:00", + "initialTime": "2024-07-24T19:36:22.327Z", + "rightToSignForMinor": "2024-07-24T19:37:12.195Z", + "confirmedAdultConsent": "2024-07-24T19:37:12.195Z", + "electronicRecordAndSignatureDisclosure": "2024-07-24T19:37:12.195Z", + "rightToUseElectronicSignatureDisclosure": "2024-07-24T19:37:12.195Z", + "checkedIn": null, + "timeTrackStart": null, + "timeTrackEnd": null + }, + "createdAt": "2024-07-24T19:37:14.702Z", + "teamId": "12345678-1234-1234-1234-123456789012", + "documentId": null, + "isAdult": null, + "device": { + "isIOS": false, + "osName": "Windows", + "isTablet": false, + "isAndroid": false, + "isBrowser": true, + "isConsole": false, + "isSmartTV": false, + "osVersion": "10", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", + "deviceType": "browser", + "engineName": "Blink", + "isWearable": false, + "isWinPhone": false, + "browserName": "Chrome", + "isMobileOnly": false, + "engineVersion": "126.0.0.0", + "browserFullVersion": "126.0.0.0" + }, + "participantType": "adult", + "publicKey": null, + "smsOptIn": true, + "emailOptIn": true, + "minorId": null, + "adultId": "12345678-1234-1234-1234-123456789012", + "companyName": "Pipedream", + "identification": null, + "localAddress": null, + "headgear": null, + "wetsuit": null, + "rentalInsurance": null, + "shootingSportsExperienceLevel": null, + "accidentInsurance": null, + "tripDate": null, + "hearAboutUs": null, + "questions": null, + "shooting": null, + "imageUrl": null, + "adult": { + "firstName": "First name", + "lastName": "Last Name", + "age": 24, + "gender": null, + "email": "email@test.com", + "phone": "+123456789", + "dob": "12/12/1999" + }, + "address": null +} \ No newline at end of file diff --git a/components/otter_waiver/sources/new-signee-instant/new-signee-instant.mjs b/components/otter_waiver/sources/new-signee-instant/new-signee-instant.mjs new file mode 100644 index 0000000000000..3e82b5aec766b --- /dev/null +++ b/components/otter_waiver/sources/new-signee-instant/new-signee-instant.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "otter_waiver-new-signee-instant", + name: "New Signee (Instant)", + description: "Emit new event when a new signee is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return "new_signee"; + }, + getFunction() { + return this.otterWaiver.getLatestParticipants; + }, + getSummary(checkIn) { + return `New signee: ${checkIn.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/otter_waiver/sources/new-signee-instant/test-event.mjs b/components/otter_waiver/sources/new-signee-instant/test-event.mjs new file mode 100644 index 0000000000000..2a69df62f5a42 --- /dev/null +++ b/components/otter_waiver/sources/new-signee-instant/test-event.mjs @@ -0,0 +1,78 @@ +export default { + "id": "12345678-1234-1234-1234-123456789012", + "waiverId": "12345678-1234-1234-1234-123456789012", + "type": "adult", + "signee": null, + "emergencyContact": null, + "additionalFieldsData": [], + "hasAdditionalFields": false, + "source": "web", + "documentUrl": null, + "certificateUrl": null, + "combinedCertificateUrl": "https://media.otterwaiver.com/waivers/participantsCertificate/12345678-1234-1234-1234-123456789012-123456789012.pdf", + "timeStamps": { + "signed": "2024-07-24T19:37:10.117Z", + "viewed": "2024-07-24T19:36:57.667Z", + "timeZone": "-03:00", + "initialTime": "2024-07-24T19:36:22.327Z", + "rightToSignForMinor": "2024-07-24T19:37:12.195Z", + "confirmedAdultConsent": "2024-07-24T19:37:12.195Z", + "electronicRecordAndSignatureDisclosure": "2024-07-24T19:37:12.195Z", + "rightToUseElectronicSignatureDisclosure": "2024-07-24T19:37:12.195Z", + "checkedIn": null, + "timeTrackStart": null, + "timeTrackEnd": null + }, + "createdAt": "2024-07-24T19:37:14.702Z", + "teamId": "12345678-1234-1234-1234-123456789012", + "documentId": null, + "isAdult": null, + "device": { + "isIOS": false, + "osName": "Windows", + "isTablet": false, + "isAndroid": false, + "isBrowser": true, + "isConsole": false, + "isSmartTV": false, + "osVersion": "10", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", + "deviceType": "browser", + "engineName": "Blink", + "isWearable": false, + "isWinPhone": false, + "browserName": "Chrome", + "isMobileOnly": false, + "engineVersion": "126.0.0.0", + "browserFullVersion": "126.0.0.0" + }, + "participantType": "adult", + "publicKey": null, + "smsOptIn": true, + "emailOptIn": true, + "minorId": null, + "adultId": "12345678-1234-1234-1234-123456789012", + "companyName": "Pipedream", + "identification": null, + "localAddress": null, + "headgear": null, + "wetsuit": null, + "rentalInsurance": null, + "shootingSportsExperienceLevel": null, + "accidentInsurance": null, + "tripDate": null, + "hearAboutUs": null, + "questions": null, + "shooting": null, + "imageUrl": null, + "adult": { + "firstName": "First name", + "lastName": "Last Name", + "age": 24, + "gender": null, + "email": "email@test.com", + "phone": "+123456789", + "dob": "12/12/1999" + }, + "address": null +} \ No newline at end of file diff --git a/components/ottertext/ottertext.app.mjs b/components/ottertext/ottertext.app.mjs new file mode 100644 index 0000000000000..e0b6f7fa9c38c --- /dev/null +++ b/components/ottertext/ottertext.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "ottertext", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/ottertext/package.json b/components/ottertext/package.json new file mode 100644 index 0000000000000..b836fb9ffd0ca --- /dev/null +++ b/components/ottertext/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/ottertext", + "version": "0.0.1", + "description": "Pipedream OtterText Components", + "main": "ottertext.app.mjs", + "keywords": [ + "pipedream", + "ottertext" + ], + "homepage": "https://pipedream.com/apps/ottertext", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/outgrow/README.md b/components/outgrow/README.md index d7221eec00789..8106dd6f8f969 100644 --- a/components/outgrow/README.md +++ b/components/outgrow/README.md @@ -1,33 +1,11 @@ # Overview -The Outgrow API allows you to maximize the user engagement and conversions for -your website by embedding highly interactive calculators, assessments, quizzes, -polls and more. The powerful API gives you the freedom to customize and deliver -your content however you choose. +The Outgrow API offers a toolkit to integrate interactive content like quizzes, polls, and calculators with your apps and workflows. Using Pipedream, you can automate actions based on events in Outgrow, such as new lead captures or quiz completions. This enables seamless data flows between Outgrow and CRM platforms, marketing tools, databases, and more. With Pipedream's serverless platform, you can trigger workflows with Outgrow events, manipulate the data, and connect to thousands of other apps without writing complex code. -Here are some of the creative experiences you can easily build with the Outgrow -API: +# Example Use Cases -- Financial Services Calculators: Utilize the to design financial services - calculators for your customers. With Outgrow, you can create personalized - user experiences based upon inputs. -- Employee Engagement Programs: Create company-wide engagement and recognition - programs utilizing Outgrow’s programs and assessments. -- Product Recommendations: Let Outgrow’s powerful recommendation engines - suggest products and services that are tailored to your customers’ needs. -- Quizzes and Surveys: Easily build fully branded quizzes and surveys to - capture leads, drive traffic and build loyalty. -- Interactive Lead Qualification Forms: Use Outgrow to create forms designed to - qualify leads that drive conversions and ROI. -- Personalized User Experiences: Customize the user experiences generated with - the Outgrow’s powerful APIs and algorithms, allowing customers to get the - most out of their content. -- Gamified User Experiences: Increase user loyalty and engagement with - Outgrow’s gamified experiences. Create game-like assessments and quizzes with - leaderboards and badges. -- Interactive Shopping Experiences: Let Outgrow’s powerful algorithms create - customized shopping experiences for your customers. -- Assessment Features: Create assessments for users to evaluate their - knowledge, skills and abilities. +- **Lead Nurturing Automation**: When a user completes a quiz and turns into a lead, automagically add their details to a CRM like Salesforce. Then, trigger a personalized email sequence in a tool like Mailchimp to nurture the lead through the sales funnel. -These are just some of the awesome things you can do with the Outgrow API! +- **Real-Time Notifications**: Set up instant Slack or SMS notifications to your sales or customer service team whenever a high-value lead fills out an Outgrow calculator. This enables quick follow-up, potentially increasing conversion rates. + +- **Data Synchronization**: After a user engages with a poll or survey, sync their responses to a Google Sheet or Airtable base for deeper analysis. Use this data to inform product development or content creation, ensuring your offerings align with customer preferences. diff --git a/components/outreach/README.md b/components/outreach/README.md index b0be84b54e5cc..313a5bd13c2ea 100644 --- a/components/outreach/README.md +++ b/components/outreach/README.md @@ -1,26 +1,14 @@ # Overview -The Outreach API is a powerful way to harness the power of Outreach, the -leading platform for sales engagement and orchestration. With the Outreach API, -you can unlock the full potential of Outreach, allowing you to build and -integrate custom apps, automate workflows, and even extend your sales -activities beyond the core platform. With Outreach, you'll have the ability to -develop the exact tools that your organization needs to achieve the best -results from your sales engagements. +The Outreach API offers extensive capabilities to automate sales engagement processes and integrate with other tools seamlessly on Pipedream. With this API, you can sync lead and prospect data, automate outreach campaigns, and trigger actions based on sales interactions. The API provides endpoints to manage prospects, accounts, opportunities, and more, offering a rich set of data for custom workflows. -Below are just a few of the examples of what you can do with the Outreach API: +# Example Use Cases -- Automatically sync data between your CRM and Outreach -- Build custom solutions to manage and monitor outreach sequences -- Import and send large sets of templates and emails -- Set up automated workflows to streamline actions like follow-ups and feedback - loops -- Create and upload account-based marketing campaigns -- Customize emails with dynamic, personalized content -- Monitor sales performance with detailed analytics -- Segment target accounts and build hyper-targeted outreach campaigns -- Create custom reports to capture key performance insights -- Monitor sentiment across your outreach and sales channels -- Integrate outreach with communication channels like Slack -- Automate activities to increase sales efficiency -- Create custom API endpoints to extend the functionality of Outreach +- **Lead Qualification and Scoring Automation** + Automate the process of qualifying and scoring leads by integrating Outreach with a CRM like Salesforce on Pipedream. Retrieve new leads from Salesforce, score them based on predefined criteria using Outreach sequences, and update the lead status in Salesforce based on engagement levels. + +- **Prospect Re-engagement Campaigns** + Create a workflow that identifies inactive prospects and automates re-engagement. Use the Outreach API to fetch prospects who haven't responded to previous emails, and trigger a personalized follow-up sequence via Pipedream. Integrate with a service like SendGrid to monitor email deliverability and open rates. + +- **Automated Sales Reporting** + Generate real-time sales reports by connecting Outreach to a data visualization tool like Google Sheets or Tableau on Pipedream. Automatically extract data related to prospect interactions, sequence stats, and sales activity from Outreach, and populate reports in your chosen platform for up-to-date insights. diff --git a/components/outreach/actions/add-prospect-sequence/add-prospect-sequence.mjs b/components/outreach/actions/add-prospect-sequence/add-prospect-sequence.mjs new file mode 100644 index 0000000000000..a6dd6f7db27fe --- /dev/null +++ b/components/outreach/actions/add-prospect-sequence/add-prospect-sequence.mjs @@ -0,0 +1,68 @@ +import { ConfigurationError } from "@pipedream/platform"; +import outreach from "../../outreach.app.mjs"; + +export default { + key: "outreach-add-prospect-sequence", + name: "Add Prospect to Sequence", + description: "Adds an existing prospect to a specific sequence in Outreach. [See the documentation](https://developers.outreach.io/api/reference/tag/prospect/)", + version: "0.0.1", + type: "action", + props: { + outreach, + prospectId: { + propDefinition: [ + outreach, + "prospectId", + ], + }, + sequenceId: { + propDefinition: [ + outreach, + "sequenceId", + ], + }, + mailboxId: { + propDefinition: [ + outreach, + "mailboxId", + ], + }, + }, + async run({ $ }) { + try { + const response = await this.outreach.addProspectToSequence({ + $, + data: { + data: { + type: "sequenceState", + relationships: { + prospect: { + data: { + type: "prospect", + id: this.prospectId, + }, + }, + sequence: { + data: { + type: "sequence", + id: this.sequenceId, + }, + }, + mailbox: { + data: { + type: "mailbox", + id: this.mailboxId, + }, + }, + }, + }, + }, + }); + + $.export("$summary", `Successfully added prospect ${this.prospectId} to sequence ${this.sequenceId}`); + return response; + } catch ({ message }) { + throw new ConfigurationError(JSON.parse(message).errors[0].detail); + } + }, +}; diff --git a/components/outreach/actions/create-account/create-account.mjs b/components/outreach/actions/create-account/create-account.mjs new file mode 100644 index 0000000000000..2d4ea01eabd98 --- /dev/null +++ b/components/outreach/actions/create-account/create-account.mjs @@ -0,0 +1,154 @@ +import { customProps } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import outreach from "../../outreach.app.mjs"; + +export default { + key: "outreach-create-account", + name: "Create Account", + description: "Creates an account within Outreach. [See the documentation](https://developers.outreach.io/api/reference/tag/Account/#tag/Account/paths/~1accounts/post)", + version: "0.0.1", + type: "action", + props: { + outreach, + buyerIntentScore: { + type: "string", + label: "Buyer Intent Score", + description: "A custom score given to measure the quality of the account.", + optional: true, + }, + companyType: { + type: "string", + label: "Company Type", + description: "A description of the company's type (e.g. \"Public Company\").", + optional: true, + }, + customId: { + type: "string", + label: "Custom Id", + description: "A custom ID for the account, often referencing an ID in an external system.", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "A custom description of the account.", + optional: true, + }, + domain: { + type: "string", + label: "Domain", + description: "The company's website domain (e.g. \"www.acme.com\").", + optional: true, + }, + externalSource: { + type: "string", + label: "External Source", + description: "The source of the resource's creation (e.g. \"outreach-api\").", + optional: true, + }, + followers: { + type: "integer", + label: "Followers", + description: "The number of followers the company has listed on social media.", + optional: true, + }, + foundedAt: { + type: "string", + label: "Founded At", + description: "The founding date of the company. Format: 'YYYY-MM-DDTHH:MM:SS.UUUZ'", + optional: true, + }, + industry: { + type: "string", + label: "Industry", + description: "A description of the company's industry (e.g. \"Manufacturing\").", + optional: true, + }, + linkedInEmployees: { + type: "integer", + label: "Linked In Employees", + description: "The number of employees listed on the company's LinkedIn URL.", + optional: true, + }, + linkedInUrl: { + type: "string", + label: "LinkedIn URL", + description: "The company's LinkedIn URL.", + optional: true, + }, + locality: { + type: "string", + label: "Locality", + description: "The company's primary geographic region (e.g. \"Eastern USA\").", + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "The name of the company (e.g. \"Acme Corporation\").", + }, + named: { + type: "boolean", + label: "Named", + description: "A boolean value determining whether this is a \"named\" account or not. Only named accounts will show up on the collection index.", + optional: true, + }, + naturalName: { + type: "string", + label: "Natural Name", + description: "The natural name of the company (e.g. \"Acme\").", + optional: true, + }, + numberOfEmployees: { + type: "integer", + label: "Number Of Employees", + description: "The number of employees working at the company.", + optional: true, + }, + sharingTeamId: { + propDefinition: [ + outreach, + "sharingTeamId", + ], + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "A list of tag values associated with the account (e.g. [\"Enterprise\", \"Tier 1\"]).", + optional: true, + }, + websiteUrl: { + type: "string", + label: "Website URL", + description: "The company's website URL (e.g. \"https://www.acme.com/contact\").", + optional: true, + }, + ...customProps, + }, + async run({ $ }) { + const { + outreach, + buyerIntentScore, + tags, + ...data + } = this; + + const response = await outreach.createAccount({ + $, + data: { + data: { + attributes: { + buyerIntentScore: buyerIntentScore && parseFloat(buyerIntentScore), + tags: tags && parseObject(tags), + ...data, + }, + type: "account", + }, + }, + }); + + $.export("$summary", `Successfully created account with Id: ${response.data.id}`); + return response; + }, +}; diff --git a/components/outreach/actions/create-prospect/create-prospect.mjs b/components/outreach/actions/create-prospect/create-prospect.mjs new file mode 100644 index 0000000000000..21dbad9f6bbb4 --- /dev/null +++ b/components/outreach/actions/create-prospect/create-prospect.mjs @@ -0,0 +1,407 @@ +import { customProps } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import outreach from "../../outreach.app.mjs"; + +export default { + key: "outreach-create-prospect", + name: "Create Prospect", + description: "Creates a new prospect in Outreach. [See the documentation](https://developers.outreach.io/api/reference/tag/Prospect/#tag/Prospect/paths/~1prospects/post)", + version: "0.0.1", + type: "action", + props: { + outreach, + addedAt: { + type: "string", + label: "Added At", + description: "The date and time the prospect was added to any system. Format: 'YYYY-MM-DDTHH:MM:SS.UUUZ'", + optional: true, + }, + addressCity: { + type: "string", + label: "Address City", + description: "The prospect's city (e.g. \"Seattle\").", + optional: true, + }, + addressCountry: { + type: "string", + label: "Address Country", + description: "The prospect's country (e.g. \"USA\").", + optional: true, + }, + addressState: { + type: "string", + label: "Address State", + description: "The prospect's state (e.g. \"Washington\").", + optional: true, + }, + addressStreet: { + type: "string", + label: "Address Street", + description: "The prospect's street address (e.g. \"1441 N 34th St\").", + optional: true, + }, + addressStreet2: { + type: "string", + label: "Address Street 2", + description: "The prospect's second street address, if applicable.", + optional: true, + }, + addressZip: { + type: "string", + label: "Address Zip", + description: "The prospect's postal code (e.g. \"98103\").", + optional: true, + }, + angelListUrl: { + type: "string", + label: "Angel List URL", + description: "The prospect's AngelList URL.", + optional: true, + }, + availableAt: { + type: "string", + label: "Available At", + description: "The date and time the prospect is available to contact again. Format: 'YYYY-MM-DDTHH:MM:SS.UUUZ'", + optional: true, + }, + campaignName: { + type: "string", + label: "Campaign Name", + description: "The name of the campaign the prospect is associated with.", + optional: true, + }, + company: { + type: "string", + label: "Company", + description: "The name of the company the prospect works at. If associated with an account, this is the name of the account. (e.g. Acme International).", + optional: true, + }, + dateOfBirth: { + type: "string", + label: "Date Of Birth", + description: "The date the prospect was born. Format YYYY-MM-DD", + optional: true, + }, + degree: { + type: "string", + label: "Degree", + description: "The degree(s) the prospect has received.", + optional: true, + }, + emails: { + type: "string[]", + label: "Emails", + description: "A list of email addresses associated with the prospect.", + optional: true, + }, + eventName: { + type: "string", + label: "Event Name", + description: "The name of the event the prospect was met at.", + optional: true, + }, + externalId: { + type: "string", + label: "External Id", + description: "A custom ID for the prospect, often referencing an ID in an external system.", + optional: true, + }, + externalOwner: { + type: "string", + label: "External Owner", + description: "A custom owner for the prospect, often referencing an ownering in an external system.", + optional: true, + }, + externalSource: { + type: "string", + label: "External Source", + description: "The source of the resource's creation (e.g. \"outreach-api\").", + optional: true, + }, + facebookUrl: { + type: "string", + label: "Facebook URL", + description: "The prospect's Facebook URL.", + optional: true, + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the prospect.", + optional: true, + }, + gender: { + type: "string", + label: "Gender", + description: "The gender of the prospect.", + optional: true, + }, + githubUrl: { + type: "string", + label: "GitHub URL", + description: "The prospect's GitHub URL.", + optional: true, + }, + githubUsername: { + type: "string", + label: "GitHub Username", + description: "The prospect's GitHub username.", + optional: true, + }, + googlePlusUrl: { + type: "string", + label: "Google Plus URL", + description: "The prospect's Google+ URL.", + optional: true, + }, + graduationDate: { + type: "string", + label: "Graduation Date", + description: "The graduation date of the prospect.", + optional: true, + }, + homePhones: { + type: "string[]", + label: "Home Phones", + description: "A list of home phone numbers associated with the prospect.", + optional: true, + }, + jobStartDate: { + type: "string", + label: "Job Start Date", + description: "The starting date of the prospect's current job.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the prospect.", + optional: true, + }, + linkedInConnections: { + type: "integer", + label: "LinkedIn Connections", + description: "The number of connections on the prospect's LinkedIn profile.", + optional: true, + }, + linkedInId: { + type: "string", + label: "LinkedIn Id", + description: "The prospect's LinkedIn ID.", + optional: true, + }, + linkedInUrl: { + type: "string", + label: "LinkedIn URL", + description: "The prospect's LinkedIn URL.", + optional: true, + }, + middleName: { + type: "string", + label: "Middle Name", + description: "The middle name of the prospect.", + optional: true, + }, + mobilePhones: { + type: "string[]", + label: "Mobile Phones", + description: "A list of mobile phone numbers associated with the prospect.", + optional: true, + }, + nickname: { + type: "string", + label: "Nickname", + description: "The nickname of the prospect.", + optional: true, + }, + occupation: { + type: "string", + label: "Occupation", + description: "The occupation of the prospect (e.g. \"Purchasing Manager\").", + optional: true, + }, + optedOut: { + type: "boolean", + label: "Opted Out", + description: "A boolean value representing whether this prospect is currently opted out of all mailings. Set this value to true to opt out the prospect; the 'opted_out' timestamp will be updated to the time of the request. Set this value to false to revert the opt at and clear the opted out timestamp.", + optional: true, + }, + otherPhones: { + type: "string[]", + label: "Other Phones", + description: "A list of other phone numbers associated with the prospect.", + optional: true, + }, + personalNote1: { + type: "string", + label: "Personal Note 1", + description: "A custom note field related to the prospect.", + optional: true, + }, + personalNote2: { + type: "string", + label: "Personal Note 2", + description: "A second note field related to the prospect.", + optional: true, + }, + preferredContact: { + type: "string", + label: "Preferred Contact", + description: "The preferred contact method for the prospect.", + optional: true, + }, + quoraUrl: { + type: "string", + label: "Quora URL", + description: "The prospect's Quora URL.", + optional: true, + }, + region: { + type: "string", + label: "Region", + description: "The primary geographic region of the prospect.", + optional: true, + }, + school: { + type: "string", + label: "School", + description: "The school(s) the prospect has attended.", + optional: true, + }, + score: { + type: "string", + label: "Score", + description: "A custom score given to measure the quality of the lead.", + optional: true, + }, + sharingTeamId: { + propDefinition: [ + outreach, + "sharingTeamId", + ], + optional: true, + }, + source: { + type: "string", + label: "Source", + description: "A custom source representing where the lead was first acquired.", + optional: true, + }, + specialties: { + type: "string", + label: "Specialties", + description: "A description of the prospect's specialties.", + optional: true, + }, + stackOverflowId: { + type: "string", + label: "Stack Overflow Id", + description: "The prospect's StackOverflow ID.", + optional: true, + }, + stackOverflowUrl: { + type: "string", + label: "StackOverflow URL", + description: "The prospect's StackOverflow URL.", + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "A list of tag values associated with the account (e.g. [\"Interested\", \"2017 Expo\"]).", + optional: true, + }, + timeZone: { + type: "string", + label: "TimeZone", + description: "The prospect's current timezone, preferably in the IANA format (e.g. \"America/LosAngeles\").", + optional: true, + }, + title: { + type: "string", + label: "Title", + description: "The title of the prospect.", + optional: true, + }, + twitterUrl: { + type: "string", + label: "Twitter URL", + description: "The prospect's Twitter URL.", + optional: true, + }, + twitterUsername: { + type: "string", + label: "Twitter Username", + description: "The prospect's Twitter username.", + optional: true, + }, + voipPhones: { + type: "string[]", + label: "Voip Phones", + description: "A list of voip phone numbers associated with the prospect.", + optional: true, + }, + websiteUrl1: { + type: "string", + label: "Website URL 1", + description: "The URL of the prospect's website.", + optional: true, + }, + websiteUrl2: { + type: "string", + label: "Website URL 2", + description: "The value of the prospect's second website URL field.", + optional: true, + }, + websiteUrl3: { + type: "string", + label: "Website URL 3", + description: "The value of the prospect's third website URL field.", + optional: true, + }, + workPhones: { + type: "string[]", + label: "Work Phones", + description: "A list of work phone numbers associated with the prospect.", + optional: true, + }, + ...customProps, + }, + async run({ $ }) { + const { + outreach, + emails, + homePhones, + mobilePhones, + otherPhones, + score, + tags, + voipPhones, + workPhones, + ...data + } = this; + + const response = await outreach.createProspect({ + $, + data: { + data: { + attributes: { + emails: emails && parseObject(emails), + homePhones: homePhones && parseObject(homePhones), + mobilePhones: mobilePhones && parseObject(mobilePhones), + otherPhones: otherPhones && parseObject(otherPhones), + score: score && parseFloat(score), + tags: tags && parseObject(tags), + voipPhones: voipPhones && parseObject(voipPhones), + workPhones: workPhones && parseObject(workPhones), + ...data, + }, + type: "prospect", + }, + }, + }); + + $.export("$summary", `Successfully created prospect with Id: ${response.data.id}`); + return response; + }, +}; diff --git a/components/outreach/common/constants.mjs b/components/outreach/common/constants.mjs new file mode 100644 index 0000000000000..be77ad704d38c --- /dev/null +++ b/components/outreach/common/constants.mjs @@ -0,0 +1,123 @@ +export const LIMIT = 50; +export const customProps = { + custom1: { + type: "string", + label: "Custom 1", + description: "The value of the first custom field.", + optional: true, + }, + custom2: { + type: "string", + label: "Custom 2", + description: "The value of the second custom field.", + optional: true, + }, + custom3: { + type: "string", + label: "Custom 3", + description: "The value of the third custom field.", + optional: true, + }, + custom4: { + type: "string", + label: "Custom 4", + description: "The value of the fourth custom field.", + optional: true, + }, + custom5: { + type: "string", + label: "Custom 5", + description: "The value of the fifth custom field.", + optional: true, + }, + custom6: { + type: "string", + label: "Custom 6", + description: "The value of the sixth custom field.", + optional: true, + }, + custom7: { + type: "string", + label: "Custom 7", + description: "The value of the seventh custom field.", + optional: true, + }, + custom8: { + type: "string", + label: "Custom 8", + description: "The value of the eight custom field.", + optional: true, + }, + custom9: { + type: "string", + label: "Custom 9", + description: "The value of the ninth custom field.", + optional: true, + }, + custom10: { + type: "string", + label: "Custom 10", + description: "The value of the 10th custom field.", + optional: true, + }, + custom11: { + type: "string", + label: "Custom 11", + description: "The value of the 11th custom field.", + optional: true, + }, + custom12: { + type: "string", + label: "Custom 12", + description: "The value of the 12th custom field.", + optional: true, + }, + custom13: { + type: "string", + label: "Custom 13", + description: "The value of the 13th custom field.", + optional: true, + }, + custom14: { + type: "string", + label: "Custom 14", + description: "The value of the 14th custom field.", + optional: true, + }, + custom15: { + type: "string", + label: "Custom 15", + description: "The value of the 15th custom field.", + optional: true, + }, + custom16: { + type: "string", + label: "Custom 16", + description: "The value of the 16th custom field.", + optional: true, + }, + custom17: { + type: "string", + label: "Custom 17", + description: "The value of the 17th custom field.", + optional: true, + }, + custom18: { + type: "string", + label: "Custom 18", + description: "The value of the 18th custom field.", + optional: true, + }, + custom19: { + type: "string", + label: "Custom 19", + description: "The value of the 19th custom field.", + optional: true, + }, + custom20: { + type: "string", + label: "Custom 20", + description: "The value of the 20th custom field.", + optional: true, + }, +}; diff --git a/components/outreach/common/utils.mjs b/components/outreach/common/utils.mjs new file mode 100644 index 0000000000000..f35e0c55fc61e --- /dev/null +++ b/components/outreach/common/utils.mjs @@ -0,0 +1,6 @@ +export const parseObject = (obj) => { + if (typeof obj === "string") { + return JSON.parse(obj); + } + return obj; +}; diff --git a/components/outreach/outreach.app.mjs b/components/outreach/outreach.app.mjs index f6ce1a3558703..406f20bd3bee6 100644 --- a/components/outreach/outreach.app.mjs +++ b/components/outreach/outreach.app.mjs @@ -1,11 +1,185 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + export default { type: "app", app: "outreach", - propDefinitions: {}, + propDefinitions: { + mailboxId: { + type: "string", + label: "Mailbox Id", + description: "The ID of the mailboxId.", + async options({ prevContext: { after } }) { + if (after) { + const params = new URLSearchParams(after); + after = params.get("page[after]"); + } + + const { + data, links, + } = await this.listMailboxes({ + params: { + "count": false, + "page[size]": LIMIT, + "page[after]": after, + }, + }); + + return { + options: data.map(({ + id: value, attributes: { email: label }, + }) => ({ + label, + value, + })), + context: { + after: links.next, + }, + }; + }, + }, + prospectId: { + type: "string", + label: "Prospect ID", + description: "The ID of the prospect.", + async options({ prevContext: { after } }) { + if (after) { + const params = new URLSearchParams(after); + after = params.get("page[after]"); + } + + const { + data, links, + } = await this.listPropspects({ + params: { + "count": false, + "page[size]": LIMIT, + "page[after]": after, + }, + }); + + return { + options: data.map(({ + id: value, attributes: { name: label }, + }) => ({ + label, + value, + })), + context: { + after: links.next, + }, + }; + }, + }, + sequenceId: { + type: "string", + label: "Sequence ID", + description: "The ID of the sequence.", + async options({ prevContext: { after } }) { + if (after) { + const params = new URLSearchParams(after); + after = params.get("page[after]"); + } + + const { + data, links, + } = await this.listSequences({ + params: { + "count": false, + "page[size]": LIMIT, + "page[after]": after, + }, + }); + + return { + options: data.map(({ + id: value, attributes: { name: label }, + }) => ({ + label, + value, + })), + context: { + after: links.next, + }, + }; + }, + }, + sharingTeamId: { + type: "string", + label: "Sharing Team Id", + description: "The ID of the sharing team associated with this object. Access is currently in beta.", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.outreach.io/api/v2"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...otherOpts + }) { + const config = { + ...otherOpts, + url: this._baseUrl() + path, + headers: this._headers(), + }; + return axios($, config); + }, + createAccount(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/accounts", + ...opts, + }); + }, + createProspect(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/prospects", + ...opts, + }); + }, + createHook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteHook(hookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${hookId}`, + }); + }, + listMailboxes(opts = {}) { + return this._makeRequest({ + path: "/mailboxes", + ...opts, + }); + }, + listPropspects(opts = {}) { + return this._makeRequest({ + path: "/prospects", + ...opts, + }); + }, + listSequences(opts = {}) { + return this._makeRequest({ + path: "/sequences", + ...opts, + }); + }, + addProspectToSequence(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/sequenceStates", + ...opts, + }); }, }, }; diff --git a/components/outreach/package.json b/components/outreach/package.json new file mode 100644 index 0000000000000..1c355dcc0402c --- /dev/null +++ b/components/outreach/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/outreach", + "version": "0.1.0", + "description": "Pipedream Outreach Components", + "main": "outreach.app.mjs", + "keywords": [ + "pipedream", + "outreach" + ], + "homepage": "https://pipedream.com/apps/outreach", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/outreach/sources/common/base.mjs b/components/outreach/sources/common/base.mjs new file mode 100644 index 0000000000000..9d2e383ba682b --- /dev/null +++ b/components/outreach/sources/common/base.mjs @@ -0,0 +1,58 @@ +import outreach from "../../outreach.app.mjs"; + +export default { + props: { + outreach, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + }, + methods: { + emitEvent(body) { + this.$emit(body, { + id: body.meta.jobId, + summary: `New ${body.meta.eventName.split(".").join(" ")} with Id: ${body.data.id}`, + ts: body.meta.deliveredAt, + }); + }, + setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getHookId() { + return this.db.get("hookId"); + }, + }, + hooks: { + async activate() { + const { data } = await this.outreach.createHook({ + data: { + data: { + attributes: { + action: "*", + active: true, + resource: this.getResource(), + url: this.http.endpoint, + }, + type: "webhook", + }, + }, + }); + this.setHookId(data.id); + }, + async deactivate() { + const hookId = this.getHookId(); + await this.outreach.deleteHook(hookId); + }, + }, + async run(event) { + const { body } = event; + + this.http.respond({ + status: 200, + }); + + this.emitEvent(body); + }, +}; diff --git a/components/outreach/sources/new-call-instant/new-call-instant.mjs b/components/outreach/sources/new-call-instant/new-call-instant.mjs new file mode 100644 index 0000000000000..083618254810c --- /dev/null +++ b/components/outreach/sources/new-call-instant/new-call-instant.mjs @@ -0,0 +1,17 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "outreach-new-call-instant", + name: "New Call Instant", + description: "Emit new event when a call is created, updated, or deleted.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResource() { + return "call"; + }, + }, +}; diff --git a/components/outreach/sources/new-email-instant/new-email-instant.mjs b/components/outreach/sources/new-email-instant/new-email-instant.mjs new file mode 100644 index 0000000000000..0228df9e265ea --- /dev/null +++ b/components/outreach/sources/new-email-instant/new-email-instant.mjs @@ -0,0 +1,17 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "outreach-new-email-instant", + name: "New Email Event (Instant)", + description: "Emit new event when an email is created, updated, destroyed, bounced, delivered, opened or replied.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResource() { + return "mailing"; + }, + }, +}; diff --git a/components/outreach/sources/new-task-instant/new-task-instant.mjs b/components/outreach/sources/new-task-instant/new-task-instant.mjs new file mode 100644 index 0000000000000..1b188a98a02b4 --- /dev/null +++ b/components/outreach/sources/new-task-instant/new-task-instant.mjs @@ -0,0 +1,17 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "outreach-new-task-instant", + name: "New Task Event (Instant)", + description: "Emit new event when a task is created, updated, destroyed or completed.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResource() { + return "task"; + }, + }, +}; diff --git a/components/outscraper/README.md b/components/outscraper/README.md new file mode 100644 index 0000000000000..dc023f1213d51 --- /dev/null +++ b/components/outscraper/README.md @@ -0,0 +1,11 @@ +# Overview + +Outscraper provides a powerful API to scrape public data from Google services like Google Maps, Google Search, and Google Ads. The API can extract rich datasets including business listings, reviews, ads data, and SERP. When leveraged with Pipedream, you can automate workflows integrating Outscraper's capabilities with other apps, conduct data analysis, monitor brand mentions, or enrich CRM data, all in real-time and without the need for manual intervention. + +# Example Use Cases + +- **Business Data Enrichment**: Automate the enhancement of your CRM records by pulling detailed business information from Google Maps using Outscraper and syncing it with your CRM software on Pipedream, such as Salesforce or HubSpot, to keep your records up-to-date with the latest data. + +- **Review Monitoring and Alerts**: Use Outscraper to monitor reviews of your business across Google Maps. Set up a Pipedream workflow that sends notifications via email, Slack, or SMS whenever a new review is posted. This can help you quickly respond to customer feedback and manage your online reputation effectively. + +- **Market Research Automation**: Conduct automated market research by scraping search results for specific queries using Outscraper. Analyze the data with Pipedream's built-in code steps or send it to data visualization tools like Google Sheets or Tableau to uncover insights and trends in your industry. diff --git a/components/outscraper/actions/reverse-geocoding/reverse-geocoding.mjs b/components/outscraper/actions/reverse-geocoding/reverse-geocoding.mjs new file mode 100644 index 0000000000000..724418d2b6c68 --- /dev/null +++ b/components/outscraper/actions/reverse-geocoding/reverse-geocoding.mjs @@ -0,0 +1,29 @@ +import outscraper from "../../outscraper.app.mjs"; + +export default { + key: "outscraper-reverse-geocoding", + name: "Reverse Geocoding", + description: "Translates geographic locations into human-readable addresses. [See the documentation](https://app.outscraper.com/api-docs#tag/Other-Services/paths/~1reverse-geocoding/get)", + version: "0.0.1", + type: "action", + props: { + outscraper, + coordinates: { + propDefinition: [ + outscraper, + "coordinates", + ], + }, + }, + async run({ $ }) { + const query = this.coordinates; + const response = await this.outscraper.translateLocation({ + $, + params: { + query, + }, + }); + $.export("$summary", `Successfully obtained location for ${query}`); + return response; + }, +}; diff --git a/components/outscraper/actions/scrape-emails-contacts/scrape-emails-contacts.mjs b/components/outscraper/actions/scrape-emails-contacts/scrape-emails-contacts.mjs new file mode 100644 index 0000000000000..bd1e3e840f5bb --- /dev/null +++ b/components/outscraper/actions/scrape-emails-contacts/scrape-emails-contacts.mjs @@ -0,0 +1,39 @@ +import outscraper from "../../outscraper.app.mjs"; +import preferredContacts from "../../common/constants/preferredContacts.mjs"; + +export default { + key: "outscraper-scrape-emails-contacts", + name: "Scrape Emails and Contacts", + description: "Finds email addresses, social links, and phone numbers from given domains. [See the documentation](https://app.outscraper.com/api-docs#tag/Email-Related/paths/~1emails-and-contacts/get)", + version: "0.0.1", + type: "action", + props: { + outscraper, + query: { + type: "string", + label: "Domain", + description: "Domain or link, e.g. `pipedream.com`", + }, + preferredContacts: { + type: "string[]", + label: "Preferred Contacts", + description: "The preferred contacts to find.", + optional: true, + options: preferredContacts, + }, + }, + async run({ $ }) { + const { + outscraper, ...params + } = this; + const response = await outscraper.findDomainData({ + $, + params, + paramsSerializer: { + indexes: null, + }, + }); + $.export("$summary", `Successfully retrieved data for domain ${this.domain}`); + return response; + }, +}; diff --git a/components/outscraper/actions/search-places/search-places.mjs b/components/outscraper/actions/search-places/search-places.mjs new file mode 100644 index 0000000000000..f88f0f03ba940 --- /dev/null +++ b/components/outscraper/actions/search-places/search-places.mjs @@ -0,0 +1,62 @@ +import languages from "../../common/constants/languages.mjs"; +import regions from "../../common/constants/regions.mjs"; +import outscraper from "../../outscraper.app.mjs"; + +export default { + key: "outscraper-search-places", + name: "Search Places on Google Maps", + description: "Searches for places on Google Maps using queries. [See the documentation](https://app.outscraper.com/api-docs#tag/Businesses-and-POI/paths/~1maps~1search-v3/get)", + version: "0.0.1", + type: "action", + props: { + outscraper, + query: { + type: "string", + label: "Query", + description: "You can use anything that you would use on a regular Google Maps site. Additionally, you can use `google_id` (feature_id), `place_id`, or `CID`. Examples of valid queries: `Real estate agency, Rome, Italy`, `The NoMad Restaurant, NY, USA`, `restaurants, Brooklyn 11203`, `0x886916e8bc273979:0x5141fcb11460b226`, `ChIJrZhup4lZwokRUr_5sLoFlDw`, etc.", + }, + limit: { + type: "integer", + label: "Limit", + description: "The limit of organizations to take from one query search.", + optional: true, + min: 1, + max: 500, + default: 500, + }, + coordinates: { + propDefinition: [ + outscraper, + "coordinates", + ], + description: "The latitude and longitude of the location where you want your query to be applied, e.g. `37.427074,-122.1439166`", + optional: true, + }, + language: { + type: "string", + label: "Language", + description: "The language to use.", + optional: true, + default: "en", + options: languages, + }, + region: { + type: "string", + label: "Language", + description: "The country to use, recommended for a better search experience.", + optional: true, + options: regions, + }, + }, + async run({ $ }) { + const { + outscraper, ...params + } = this; + const response = await outscraper.searchPlaces({ + $, + params, + }); + $.export("$summary", "Successfully searched for places"); + return response; + }, +}; diff --git a/components/outscraper/common/constants/languages.mjs b/components/outscraper/common/constants/languages.mjs new file mode 100644 index 0000000000000..3b6a832c9a73b --- /dev/null +++ b/components/outscraper/common/constants/languages.mjs @@ -0,0 +1,152 @@ +export default [ + "en", + "de", + "es", + "es-419", + "fr", + "hr", + "it", + "nl", + "pl", + "pt-BR", + "pt-PT", + "vi", + "tr", + "ru", + "ar", + "th", + "ko", + "zh-CN", + "zh-TW", + "ja", + "ach", + "af", + "ak", + "ig", + "az", + "ban", + "ceb", + "xx-bork", + "bs", + "br", + "ca", + "cs", + "sn", + "co", + "cy", + "da", + "yo", + "et", + "xx-elmer", + "eo", + "eu", + "ee", + "tl", + "fil", + "fo", + "fy", + "gaa", + "ga", + "gd", + "gl", + "gn", + "xx-hacker", + "ht", + "ha", + "haw", + "bem", + "rn", + "id", + "ia", + "xh", + "zu", + "is", + "jw", + "rw", + "sw", + "tlh", + "kg", + "mfe", + "kri", + "la", + "lv", + "to", + "lt", + "ln", + "loz", + "lua", + "lg", + "hu", + "mg", + "mt", + "mi", + "ms", + "pcm", + "no", + "nso", + "ny", + "nn", + "uz", + "oc", + "om", + "xx-pirate", + "ro", + "rm", + "qu", + "nyn", + "crs", + "sq", + "sk", + "sl", + "so", + "st", + "sr-ME", + "sr-Latn", + "su", + "fi", + "sv", + "tn", + "tum", + "tk", + "tw", + "wo", + "el", + "be", + "bg", + "ky", + "kk", + "mk", + "mn", + "sr", + "tt", + "tg", + "uk", + "ka", + "hy", + "yi", + "iw", + "ug", + "ur", + "ps", + "sd", + "fa", + "ckb", + "ti", + "am", + "ne", + "mr", + "hi", + "bn", + "pa", + "gu", + "or", + "ta", + "te", + "kn", + "ml", + "si", + "lo", + "my", + "km", + "chr", +]; diff --git a/components/outscraper/common/constants/preferredContacts.mjs b/components/outscraper/common/constants/preferredContacts.mjs new file mode 100644 index 0000000000000..28fb7b19b8721 --- /dev/null +++ b/components/outscraper/common/constants/preferredContacts.mjs @@ -0,0 +1,15 @@ +export default [ + "decision makers", + "influencers", + "procurement/purchasing", + "technical", + "finance", + "operations", + "marketing", + "sales", + "maintenance", + "human resources", + "legal and compliance", + "supply chain/logistics", + "education/training", +]; diff --git a/components/outscraper/common/constants/regions.mjs b/components/outscraper/common/constants/regions.mjs new file mode 100644 index 0000000000000..e43aee0e0b38f --- /dev/null +++ b/components/outscraper/common/constants/regions.mjs @@ -0,0 +1,192 @@ +export default [ + "AF", + "AL", + "DZ", + "AS", + "AD", + "AO", + "AI", + "AG", + "AR", + "AM", + "AU", + "AT", + "AZ", + "BS", + "BH", + "BD", + "BY", + "BE", + "BZ", + "BJ", + "BT", + "BO", + "BA", + "BW", + "BR", + "VG", + "BN", + "BG", + "BF", + "BI", + "KH", + "CM", + "CA", + "CV", + "CF", + "TD", + "CL", + "CN", + "CO", + "CG", + "CD", + "CK", + "CR", + "CI", + "HR", + "CU", + "CY", + "CZ", + "DK", + "DJ", + "DM", + "DO", + "EC", + "EG", + "SV", + "EE", + "ET", + "FJ", + "FI", + "FR", + "GA", + "GM", + "GE", + "DE", + "GH", + "GI", + "GR", + "GL", + "GT", + "GG", + "GY", + "HT", + "HN", + "HK", + "HU", + "IS", + "IN", + "ID", + "IQ", + "IE", + "IM", + "IL", + "IT", + "JM", + "JP", + "JE", + "JO", + "KZ", + "KE", + "KI", + "KW", + "KG", + "LA", + "LV", + "LB", + "LS", + "LY", + "LI", + "LT", + "LU", + "MG", + "MW", + "MY", + "MV", + "ML", + "MT", + "MU", + "MX", + "FM", + "MD", + "MN", + "ME", + "MS", + "MA", + "MQ", + "MZ", + "MM", + "NA", + "NR", + "NP", + "NL", + "NZ", + "NI", + "NE", + "NG", + "NU", + "MK", + "NO", + "OM", + "PK", + "PS", + "PA", + "PG", + "PY", + "PE", + "PH", + "PN", + "PL", + "PT", + "PR", + "QA", + "RO", + "RU", + "RW", + "WS", + "SM", + "ST", + "SA", + "SN", + "RS", + "SC", + "SL", + "SG", + "SK", + "SI", + "SB", + "SO", + "ZA", + "KR", + "ES", + "LK", + "SH", + "VC", + "SR", + "SE", + "CH", + "TW", + "TJ", + "TZ", + "TH", + "TL", + "TG", + "TO", + "TT", + "TN", + "TR", + "TM", + "VI", + "UG", + "UA", + "AE", + "GB", + "US", + "UY", + "UZ", + "VU", + "VE", + "VN", + "ZM", + "ZW", +]; diff --git a/components/outscraper/outscraper.app.mjs b/components/outscraper/outscraper.app.mjs new file mode 100644 index 0000000000000..84c60fadb744f --- /dev/null +++ b/components/outscraper/outscraper.app.mjs @@ -0,0 +1,97 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "outscraper", + propDefinitions: { + taskId: { + type: "string", + label: "Task ID", + description: "The ID of the task you want to track.", + }, + timeInterval: { + type: "integer", + label: "Time Interval", + description: "The interval at which the platform will check the task status (in seconds).", + optional: true, + default: 60, + }, + query: { + type: "string", + label: "Query", + description: "The search query, comprising of category, city/zip, and country.", + }, + links: { + type: "boolean", + label: "Include Links", + description: "Whether to include links in the search results.", + optional: true, + }, + placeIds: { + type: "boolean", + label: "Include Place IDs", + description: "Whether to include Place IDs in the search results.", + optional: true, + }, + domain: { + type: "string", + label: "Domain", + description: "The domain to find email addresses, social links, and phone numbers for.", + }, + coordinates: { + type: "string", + label: "Coordinates", + description: "The latitude and longitude of the location to translate into a human-readable address, e.g. `37.427074,-122.1439166`", + }, + }, + methods: { + _baseUrl() { + return "https://api.app.outscraper.com"; + }, + async _makeRequest({ + $ = this, + headers, + ...otherOpts + }) { + return axios($, { + ...otherOpts, + baseURL: this._baseUrl(), + headers: { + ...headers, + "X-API-KEY": this.$auth.api_token, + }, + }); + }, + async searchPlaces(args) { + return this._makeRequest({ + url: "/maps/search-v3", + ...args, + }); + }, + async findDomainData(args) { + return this._makeRequest({ + url: "/emails-and-contacts", + ...args, + }); + }, + async translateLocation(args) { + return this._makeRequest({ + url: "/reverse-geocoding", + ...args, + }); + }, + async getRequests() { + return this._makeRequest({ + url: "/requests", + params: { + type: "finished", + }, + }); + }, + async getRequestData(requestId) { + return this._makeRequest({ + url: `/requests/${requestId}`, + }); + }, + }, +}; diff --git a/components/outscraper/package.json b/components/outscraper/package.json new file mode 100644 index 0000000000000..f5bd80987002e --- /dev/null +++ b/components/outscraper/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/outscraper", + "version": "0.1.0", + "description": "Pipedream Outscraper Components", + "main": "outscraper.app.mjs", + "keywords": [ + "pipedream", + "outscraper" + ], + "homepage": "https://pipedream.com/apps/outscraper", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/outscraper/sources/new-task-finished/new-task-finished.mjs b/components/outscraper/sources/new-task-finished/new-task-finished.mjs new file mode 100644 index 0000000000000..3cfc33c13d088 --- /dev/null +++ b/components/outscraper/sources/new-task-finished/new-task-finished.mjs @@ -0,0 +1,59 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import outscraper from "../../outscraper.app.mjs"; + +export default { + key: "outscraper-new-task-finished", + name: "New Task Finished", + description: "Emit new event when a task is finished on Outscraper.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + outscraper, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _setSavedItems(value) { + this.db.set("items", value); + }, + _getSavedItems() { + return this.db.get("items") ?? []; + }, + async getAndProcessItems(emit = true) { + const savedItems = this._getSavedItems(); + const items = await this.outscraper.getRequests(); + + const idsToEmit = items.map(({ id }) => id).filter((id) => !savedItems.includes(id)); + + const ts = Date.now(); + const promises = idsToEmit.map((id) => async () => { + if (emit) { + const { data } = await this.outscraper.getRequestData(id); + this.$emit(data, { + id, + summary: `New task: ${id}`, + ts, + }); + } + savedItems.push(id); + }); + + await Promise.allSettled(promises); + this._setSavedItems(savedItems); + }, + }, + hooks: { + async deploy() { + await this.getAndProcessItems(false); + }, + }, + async run() { + await this.getAndProcessItems(); + }, +}; diff --git a/components/outseta/README.md b/components/outseta/README.md new file mode 100644 index 0000000000000..aec8125491011 --- /dev/null +++ b/components/outseta/README.md @@ -0,0 +1,11 @@ +# Overview + +The Outseta API provides a versatile set of endpoints for interacting with a company's CRM, subscription billing, customer support, and email marketing functionalities. With this API on Pipedream, you can automate tasks, synchronize data across platforms, and build powerful workflows that cater to various aspects of SaaS business operations. Pipedream's serverless execution model enables integrating Outseta's services with numerous other apps without managing infrastructure, focusing on logic and efficiency. + +# Example Use Cases + +- **User Onboarding Automation**: Once a new user signs up via Outseta, trigger a Pipedream workflow that adds the user to a welcome email sequence in MailChimp, assigns a task to the sales team in Trello for personal follow-up, and logs the event in a Google Sheet for tracking. + +- **Subscription Lifecycle Management**: Harness the Outseta API to monitor subscription changes; this could include upgrades, downgrades, and cancellations. Pipedream can catch these events and perform actions such as updating a Slack channel, sending personalized emails through SendGrid, or adjusting user permissions in your application accordingly. + +- **Support Ticket Integration**: Create a Pipedream workflow that listens for new support requests submitted through Outseta. Upon detection, it can automatically generate a ticket in Zendesk, post a message with the ticket details to a specific Discord channel, and update a shared Airtable base for cross-department visibility. diff --git a/components/overledger/README.md b/components/overledger/README.md new file mode 100644 index 0000000000000..48a3966747e7a --- /dev/null +++ b/components/overledger/README.md @@ -0,0 +1,11 @@ +# Overview + +The Overledger API by Quant Network allows developers to interact with multiple blockchain networks. It facilitates the creation, reading, and execution of smart contracts and transactions across different blockchains. This API simplifies the complex nature of blockchain interoperability, enabling multi-chain applications (MApps) through a single gateway. + +# Example Use Cases + +- **Automated Multi-chain Financial Reporting**: Build a workflow on Pipedream that triggers monthly, pulling transaction data from various blockchains via the Overledger API. Integrate this data with Google Sheets or a similar app on Pipedream to generate consolidated financial reports. + +- **Cross-Blockchain Event Monitoring**: Set up a Pipedream workflow to monitor specific events (like transactions or smart contract interactions) across multiple blockchains using Overledger. This can trigger notifications via Slack or email when predefined conditions are met, such as unusual transaction volumes or large transfers. + +- **Decentralized Application (DApp) Management**: Develop a workflow in Pipedream that utilizes Overledger to deploy and manage smart contracts across different blockchains. The workflow could, for instance, automatically update or verify contracts on Ethereum and Binance Smart Chain when a GitHub repository is updated, using a combination of Overledger and GitHub triggers on Pipedream. diff --git a/components/overledger/actions/execute-signed-transaction/execute-signed-transaction.mjs b/components/overledger/actions/execute-signed-transaction/execute-signed-transaction.mjs new file mode 100644 index 0000000000000..7e21ea44c664c --- /dev/null +++ b/components/overledger/actions/execute-signed-transaction/execute-signed-transaction.mjs @@ -0,0 +1,45 @@ +import overledger from "../../overledger.app.mjs"; + +export default { + key: "overledger-execute-signed-transaction", + name: "Execute Signed Transaction", + description: "Executes a signed transaction by sending it to a blockchain node via Overledger. [See the documentation](https://developers.quant.network/reference/executesignedrequest)", + version: "0.0.3", + type: "action", + props: { + overledger, + environment: { + propDefinition: [ + overledger, + "environment", + ], + }, + requestId: { + type: "string", + label: "Request ID", + description: "The Overledger identifier assigned to the related transaction preparation request. This should be set to the requestId parameter found in the response object of the 'Prepare a Smart Contract Transaction' Overledger action.", + }, + signedTransaction: { + type: "string", + label: "Signed Transaction", + description: "The raw transaction bytecode after signing. This should be set to the signed parameter found in the response object of the 'Sign a Transaction' Overledger action.", + optional: true, + }, + }, + async run({ $ }) { + + const requestBody = { + requestId: this.requestId, + signedTransaction: this.signedTransaction, + }; + + const response = await this.overledger.executeSignedTransaction({ + $, + environment: this.environment, + data: requestBody, + }); + + $.export("$summary", `Successfully executed signed transaction with Request ID ${this.requestId}`); + return response; + }, +}; diff --git a/components/overledger/actions/prepare-smart-contract-transaction/prepare-smart-contract-transaction.mjs b/components/overledger/actions/prepare-smart-contract-transaction/prepare-smart-contract-transaction.mjs new file mode 100644 index 0000000000000..5dedda399ff12 --- /dev/null +++ b/components/overledger/actions/prepare-smart-contract-transaction/prepare-smart-contract-transaction.mjs @@ -0,0 +1,83 @@ +import { + NETWORK_OPTIONS, TECHNOLOGY_OPTIONS, +} from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import overledger from "../../overledger.app.mjs"; + +export default { + key: "overledger-prepare-smart-contract-transaction", + name: "Prepare Smart Contract Transaction", + description: "Prepares a smart contract transaction for signing on the Overledger platform. [See the documentation](https://developers.quant.network/reference/preparesmartcontractwrite)", + version: "0.0.3", + type: "action", + props: { + overledger, + environment: { + propDefinition: [ + overledger, + "environment", + ], + }, + locationTechnology: { + type: "string", + label: "Location Technology", + description: "The technology of the blockchain that the transaction will be submitted to", + options: TECHNOLOGY_OPTIONS, + reloadProps: true, + }, + signingAccountId: { + type: "string", + label: "Signing Account ID", + description: "The blockchain account ID/address that will be sending this transaction", + }, + smartContractId: { + type: "string", + label: "Smart Contract ID", + description: "The ID/address of the smart contract to interact with.", + + }, + functionName: { + type: "string", + label: "Function Name", + description: "The name of the function to call on the smart contract.", + }, + inputParameters: { + type: "string[]", + label: "Input Parameters", + description: "The input parameters for the smart contract function, in JSON format.", + optional: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.locationTechnology) { + props.locationNetwork = { + type: "string", + label: "Location Network", + description: "The blockchain network the transaction will be submitted to.", + options: NETWORK_OPTIONS[this.locationTechnology], + }; + } + return props; + }, + async run({ $ }) { + const requestBody = { + location: { + technology: this.locationTechnology, + network: this.locationNetwork, + }, + signingAccountId: this.signingAccountId, + functionName: this.functionName, + smartContractId: this.smartContractId, + inputParameters: parseObject(this.inputParameters), //parse these values using the parseObject function at this shouls turn the JSON string into JSON objects to used in the request body. + }; + + const response = await this.overledger.prepareSmartContractTransaction({ + $, + environment: this.environment, + data: requestBody, + }); + $.export("$summary", "Smart contract transaction prepared successfully"); + return response; + }, +}; diff --git a/components/overledger/actions/read-from-a-smart-contract/read-from-a-smart-contract.mjs b/components/overledger/actions/read-from-a-smart-contract/read-from-a-smart-contract.mjs new file mode 100644 index 0000000000000..d05977fb19136 --- /dev/null +++ b/components/overledger/actions/read-from-a-smart-contract/read-from-a-smart-contract.mjs @@ -0,0 +1,86 @@ +import { + NETWORK_OPTIONS, TECHNOLOGY_OPTIONS, +} from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import overledger from "../../overledger.app.mjs"; + +export default { + key: "overledger-read-from-a-smart-contract", + name: "Read from a smart contract", + description: "Reads data from a specified smart contract on the Overledger network.", + version: "0.0.2", + type: "action", + props: { + overledger, + environment: { + propDefinition: [ + overledger, + "environment", + ], + }, + locationTechnology: { + type: "string", + label: "Location Technology", + description: "The technology of the blockchain that the transaction will be submitted to", + options: TECHNOLOGY_OPTIONS, + reloadProps: true, + }, + functionName: { + type: "string", + label: "Function Name", + description: "The name of the function to call on the smart contract.", + }, + inputParameters: { + type: "string[]", + label: "Input Parameters", + description: "The input parameters for the smart contract function, provide both type and value in object format. Example: {\"type\":\"uint256\",\"value\":\"5\"} or {\"type\":\"address\",\"value\":\"0x3....ed8\"}", + optional: true, + default: [], + }, + smartContractId: { + type: "string", + label: "Smart Contract ID", + description: "The ID/address of the smart contract to interact with.", + }, + outputParameters: { + type: "string[]", + label: "Output Parameters", + description: "Each output parameter expected, provide just the type in object format. Example - 1) function returns one uint256 value: {\"type\": \"uint256\"} or 2) function returns two address values: {\"type\": \"address\"},{\"type\": \"address\"}", + optional: true, + default: [], + }, + }, + async additionalProps() { + const props = {}; + if (this.locationTechnology) { + props.locationNetwork = { + type: "string", + label: "Location Network", + description: "The blockchain network the transaction will be submitted to.", + options: NETWORK_OPTIONS[this.locationTechnology], + }; + } + return props; + }, + async run({ $ }) { + + const requestBody = { + location: { + technology: this.locationTechnology, + network: this.locationNetwork, + }, + functionName: this.functionName, + inputParameters: parseObject(this.inputParameters), //parse these values using the parseObject function at this shouls turn the JSON string into JSON objects to used in the request body. + smartContractId: this.smartContractId, + outputParameters: parseObject(this.outputParameters), + }; + // Make the API call to Overledger + const response = await this.overledger.readFromSmartContract({ + $, + environment: this.environment, + data: requestBody, + }); + $.export("$summary", `Successfully read from contract: ${this.smartContractId}`); + return response; + }, +}; diff --git a/components/overledger/actions/sign-a-transaction/sign-a-transaction.mjs b/components/overledger/actions/sign-a-transaction/sign-a-transaction.mjs new file mode 100644 index 0000000000000..f24ac3c535d25 --- /dev/null +++ b/components/overledger/actions/sign-a-transaction/sign-a-transaction.mjs @@ -0,0 +1,77 @@ +import overledger from "../../overledger.app.mjs"; +import { + TECHNOLOGY_OPTIONS, UNIT_OPTIONS, +} from "../../common/constants.mjs"; + +export default { + key: "overledger-sign-a-transaction", + name: "Sign a transaction", + description: "Sign a transaction using Overledger - Part 2 of [Overledger Pattern](https://developers.quant.network/reference/overledger-pattern). [See documentation](https://developers.quant.network/reference/sandboxsigning)", + version: "0.0.3", + type: "action", + props: { + overledger, + environment: { + propDefinition: [ + overledger, + "environment", + ], + }, + locationTechnology: { + type: "string", + label: "Location Technology", + description: "The blockchain technology used for this transaction, e.g., ethereum, substrate - required in order to set the dltfee", + options: TECHNOLOGY_OPTIONS, + reloadProps: true, + }, + keyId: { + type: "string", + label: "Signing Account ID", + description: "The ID/address of the blockchain account that will sign the transaction.", + }, + requestId: { + type: "string", + label: "Request ID", + description: "The Request ID assigned to a preparation request in Overledger. This should be set to the requestId parameter found in the response object of the 'Prepare Transaction' Overledger action.", + }, + transactionSigningResponderName: { + type: "string", + label: "Transaction Signing Responder Name", + description: "The name of the Transaction Signing Responder you would like to use. The CTA Transaction Signing Responder is the Quant-provided signer for testnet accounts.", + }, + nativeData: { + type: "object", + label: "Native Data", + description: "An object representing the transaction required to be signed - This should be set to the nativeData object of the 'Prepare Transaction' Overledger action.", + }, + }, + async run({ $ }) { + //default values of gatewayFee and dltfee hard coded into params. + const gatewayFee = { + amount: "0", + unit: "QNT", + }; + // Define DLT Fee and dynamically set the 'unit/symbol' from UNIT_OPTIONS + const dltFee = { + amount: "0.000019897764079968", + unit: UNIT_OPTIONS[this.locationTechnology] || "ETH", // Use default if not found + }; + // Sign the transaction + const requestBody = { + keyId: this.keyId, + gatewayFee: gatewayFee, + requestId: this.requestId, + dltFee: dltFee, + nativeData: this.nativeData, + transactionSigningResponderName: this.transactionSigningResponderName, + }; + + const response = await this.overledger.signTransaction({ + $, + environment: this.environment, + data: requestBody, + }); + $.export("$summary", "Transaction signed successfully"); + return response; + }, +}; diff --git a/components/overledger/common/constants.mjs b/components/overledger/common/constants.mjs new file mode 100644 index 0000000000000..58d8d0c621972 --- /dev/null +++ b/components/overledger/common/constants.mjs @@ -0,0 +1,69 @@ +export const TECHNOLOGY_OPTIONS = [ + { + label: "Ethereum", + value: "ethereum", + }, + { + label: "Substrate", + value: "substrate", + }, + { + label: "XRP Ledger", + value: "xrp ledger", + }, + { + label: "Bitcoin", + value: "bitcoin", + }, + { + label: "Hyperledger Fabric", + value: "hyperledger fabric", + }, +]; + +export const NETWORK_OPTIONS = { + "ethereum": [ + "ethereum sepolia testnet", + "ethereum mainnet", + "polygon amoy testnet", + "polygon mainnet", + "avalanche fuji testnet", + "avalanche c-chain mainnet", + "xdc apothem testnet", + "xdc network mainnet", + ], + "substrate": [ + "polkadot westend", + "polkadot mainnet", + ], + "xrp ledger": [ + "testnet", + "mainnet", + ], + "bitcoin": [ + "testnet", + "mainnet", + ], + "hyperledger fabric": [ + "Sandbox", + ], +}; +//Overledger environment to be used - Test or Live +export const OVERLEDGER_INSTANCE = [ + { + label: "Sandbox", + value: "sandbox", + }, + { + label: "Overledger", + value: "overledger", + }, +]; +///unit options to allow for the correct selection based on the location network +export const UNIT_OPTIONS = { + "ethereum": "ETH", // Ethereum's token symbol is ETH + "substrate": "DOT", // Polkadot's token symbol is DOT + "xrp ledger": "XRP", // XRP Ledger's token symbol is XRP + "bitcoin": "BTC", // Bitcoin's token symbol is BTC + "hyperledger fabric": "FAB", // Placeholder for Hyperledger Fabric's token symbol +}; diff --git a/components/overledger/common/utils.mjs b/components/overledger/common/utils.mjs new file mode 100644 index 0000000000000..154701004e72a --- /dev/null +++ b/components/overledger/common/utils.mjs @@ -0,0 +1,18 @@ +export const parseObject = (obj) => { + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + return JSON.parse(obj); + } + return obj; +}; diff --git a/components/overledger/overledger.app.mjs b/components/overledger/overledger.app.mjs new file mode 100644 index 0000000000000..794e845ed370a --- /dev/null +++ b/components/overledger/overledger.app.mjs @@ -0,0 +1,89 @@ +import { axios } from "@pipedream/platform"; +import { OVERLEDGER_INSTANCE } from "./common/constants.mjs"; + +export default { + type: "app", + app: "overledger", + propDefinitions: { + smartContractId: { + type: "string", + label: "Smart Contract ID", + description: "The ID of the smart contract to interact with.", + }, + environment: { + type: "string", + label: "Overledger Instance", + description: "Select the Overledger instance to be used", + options: OVERLEDGER_INSTANCE, + }, + }, + methods: { + _headers() { + return { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "Content-Type": "application/json", + "API-Version": "3.0.0", + }; + }, + _getBaseUrl(environment) { //conditional for environment url selection. + return environment === "sandbox" + ? "https://api.sandbox.overledger.dev" + : "https://api.overledger.dev"; + }, + _makeRequest({ + $ = this, environment, path, ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: this._getBaseUrl(environment) + path, + headers: this._headers(), + }); + }, + prepareSmartContractTransaction(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/api/preparations/transactions/smart-contracts/write", + ...opts, + }); + }, + readFromSmartContract(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/api/smart-contracts/read", + ...opts, + }); + }, + signTransaction(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/api/transaction-signing-sandbox", + ...opts, + }); + }, + executeSignedTransaction(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/api/executions/transactions", + ...opts, + }); + }, + createHook({ + path, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/api/webhooks/${path}`, + ...opts, + }); + }, + deleteHook({ + path, webhookId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/api/webhooks/${path}/${webhookId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/overledger/package.json b/components/overledger/package.json new file mode 100644 index 0000000000000..e6ff12a98bcf5 --- /dev/null +++ b/components/overledger/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/overledger", + "version": "1.0.0", + "description": "Pipedream Overledger Components", + "main": "overledger.app.mjs", + "keywords": [ + "pipedream", + "overledger" + ], + "homepage": "https://pipedream.com/apps/overledger", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" + } +} diff --git a/components/overledger/sources/common/base.mjs b/components/overledger/sources/common/base.mjs new file mode 100644 index 0000000000000..c6a6ae816976f --- /dev/null +++ b/components/overledger/sources/common/base.mjs @@ -0,0 +1,74 @@ +import { + NETWORK_OPTIONS, TECHNOLOGY_OPTIONS, +} from "../../common/constants.mjs"; +import overledger from "../../overledger.app.mjs"; + +export default { + props: { + overledger, + environment: { + propDefinition: [ + overledger, + "environment", + ], + }, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + locationTechnology: { + type: "string", + label: "Location Technology", + description: "The technology of the blockchain that the transaction will be submitted to", + options: TECHNOLOGY_OPTIONS, + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.locationTechnology) { + props.locationNetwork = { + type: "string", + label: "Location Network", + description: "The blockchain network the transaction will be submitted to.", + options: NETWORK_OPTIONS[this.locationTechnology], + }; + } + return props; + }, + hooks: { + async activate() { + const response = await this.overledger.createHook({ + path: this.getPath(), + environment: this.environment, + data: { + location: { + technology: this.locationTechnology, + network: this.locationNetwork, + }, + callbackUrl: this.http.endpoint, + ...this.additionalData(), + }, + }); + this.db.set("webhookId", response.webhookId); + }, + async deactivate() { + const webhookId = this.db.get("webhookId"); + await this.overledger.deleteHook({ + path: this.getPath(), + webhookId, + environment: this.environment, + }); + }, + }, + async run(event) { + const { body } = event; + + this.$emit(body, { + id: body.transactionId || body?.smartContractEventUpdateDetails?.nativeData?.transactionHash, + summary: this.getSummary(body), + ts: Date.now(), + }); + }, +}; diff --git a/components/overledger/sources/new-contract-event-instant/new-contract-event-instant.mjs b/components/overledger/sources/new-contract-event-instant/new-contract-event-instant.mjs new file mode 100644 index 0000000000000..32609a09817bc --- /dev/null +++ b/components/overledger/sources/new-contract-event-instant/new-contract-event-instant.mjs @@ -0,0 +1,35 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "overledger-new-contract-event-instant", + name: "New Smart Contract Event (Instant)", + description: "Emit new event when a smart contract releases a new event.", + version: "0.0.3", + type: "source", + dedupe: "unique", + props: { + ...common.props, + smartContractId: { + propDefinition: [ + common.props.overledger, + "smartContractId", + ], + }, + }, + methods: { + getPath() { + return "smart-contract-events"; + }, + additionalData() { + return { + smartContractId: this.smartContractId, + }; + }, + getSummary(body) { + return `New contract event with transaction Hash: ${body?.smartContractEventUpdateDetails?.nativeData?.transactionHash}`; + }, + }, + sampleEmit, +}; diff --git a/components/overledger/sources/new-contract-event-instant/test-event.mjs b/components/overledger/sources/new-contract-event-instant/test-event.mjs new file mode 100644 index 0000000000000..3ea7cf117d01a --- /dev/null +++ b/components/overledger/sources/new-contract-event-instant/test-event.mjs @@ -0,0 +1,24 @@ +export default { + "type": "smartContractEvent", + "webhookId": "6c964253-4b10-43a5-a812-9b30e92275a3", + "location": { + "technology": "ethereum", + "network": "polygon mumbai testnet" + }, + "smartContractEventUpdateDetails": { + "smartContractId": "0x8590d37d55049de2555f0f9541325e7fe6b19b17", + "nativeData": { + "removed": false, + "logIndex": 0, + "transactionIndex": 0, + "transactionHash": "0xde2bbf9704d726ff395eb78217e9acb226b0ecb0b609021cccd7e67d99763db2", + "blockHash": "0x6d1a13d15fd0a90f361d3e368830bbe45c628382c4533d10f1a23ae699c781cc", + "blockNumber": "0x28b2471", + "address": "0x8590d37d55049de2555f0f9541325e7fe6b19b17", + "data": "0x0000000000000000000000001789d90438333751fdcca0d03d8952168b99ef02", + "topics": [ + "0x2d1801d4e6df986759c8582affebc974bcf0cacfd5d2ab120eb776efa53dffa2" + ] + } + } +} \ No newline at end of file diff --git a/components/overledger/sources/watch-new-account-event-instant/test-event.mjs b/components/overledger/sources/watch-new-account-event-instant/test-event.mjs new file mode 100644 index 0000000000000..ebd81d7c59ca9 --- /dev/null +++ b/components/overledger/sources/watch-new-account-event-instant/test-event.mjs @@ -0,0 +1,10 @@ +export default { + "type": "account", + "webhookId": "20f6ce08-04b4-4ce4-94eb-e2304a0737af", + "accountId": "0x282f70d5af34aedaac479b12a08e189bbee83066", + "location": { + "technology": "ethereum", + "network": "polygon mumbai testnet" + }, + "transactionId": "0xd65c16e476a5ad5fa89ffe14512bd7984e575a4370ac86ccfb88202da20f9262" +} \ No newline at end of file diff --git a/components/overledger/sources/watch-new-account-event-instant/watch-new-account-event-instant.mjs b/components/overledger/sources/watch-new-account-event-instant/watch-new-account-event-instant.mjs new file mode 100644 index 0000000000000..75a43e58b8a01 --- /dev/null +++ b/components/overledger/sources/watch-new-account-event-instant/watch-new-account-event-instant.mjs @@ -0,0 +1,34 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "overledger-watch-new-account-event-instant", + name: "New Account Event (Instant)", + description: "Emit new event for transactions to/from a specific account.", + version: "0.0.3", + type: "source", + dedupe: "unique", + props: { + ...common.props, + accountId: { + type: "string", + label: "Account Id", + description: "The blockchain account that will be monitored for transaction updates.", + }, + }, + methods: { + getPath() { + return "accounts"; + }, + additionalData() { + return { + accountId: this.accountId, + }; + }, + getSummary(body) { + return `New account event with transaction Id: ${body.transactionId}`; + }, + }, + sampleEmit, +}; diff --git a/components/overloop/README.md b/components/overloop/README.md index 2bbd3d56b47b4..ffee2bf998abd 100644 --- a/components/overloop/README.md +++ b/components/overloop/README.md @@ -1,23 +1,11 @@ # Overview -With the Overloop API, you can build powerful applications that can simplify -your workflow. Whether it be content-rich websites, data-driven applications, -or mobile-ready apps, Overloop allows you to make your project vision a -reality. Here are some examples of what you can build with the Overloop API: +The Overloop API is a powerhouse for sales automation, enabling users to streamline their sales pipeline by automating repetitive tasks, syncing data across platforms, and enhancing lead management. With Pipedream, you can leverage Overloop's capabilities to create detailed, event-driven workflows that respond in real-time to changes in your sales environment. This integration can help maintain a healthy sales funnel, ensure timely follow-ups, and personalize communication at scale. -- Custom user interfaces which can be fully tailored to meet the needs of any - organization or user. -- Highly interactive and optimized digital products like e-commerce websites - and digital magazines. -- Mobile applications that can be easily integrated with external systems and - services. -- Real-time dashboard visualizations such as status updates, analytics, and - more. -- High-volume content management platforms to manage and deliver digital - content. -- Automated natural language processing and text-mining tools. -- Collaboration platforms for teams to easily share files and collaborate. -- Interactive customer support bots for customer service and help desk - assistance. -- Business intelligence applications to quickly gather and analyze data. -- Data visualization tools to better understand and make sense of data. +# Example Use Cases + +- **Sales Lead Auto-Tagging**: Create a workflow that listens for new leads in Overloop and automatically tags them based on predefined criteria such as industry, company size, or location. Connect this with Slack to send notifications to corresponding sales reps instantly. + +- **Email Sequence Triggering Based on Website Activity**: Design a workflow where Overloop triggers an email sequence when a lead visits certain pages on your website. Combine this with Google Analytics to refine follow-ups based on user engagement and page interactions. + +- **Real-time CRM Synchronization**: Set up a real-time sync between Overloop and your CRM, such as Salesforce. Whenever a lead's status updates in Overloop, the workflow could update the corresponding record in Salesforce, ensuring that sales data remains consistent across both platforms. diff --git a/components/owen_wilson/package.json b/components/owen_wilson/package.json new file mode 100644 index 0000000000000..116c4b4839a82 --- /dev/null +++ b/components/owen_wilson/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/owen_wilson", + "version": "0.6.0", + "description": "Pipedream owen_wilson Components", + "main": "owen_wilson.app.mjs", + "keywords": [ + "pipedream", + "owen_wilson" + ], + "homepage": "https://pipedream.com/apps/owen_wilson", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/owl_protocol/actions/deploy-collection/deploy-collection.mjs b/components/owl_protocol/actions/deploy-collection/deploy-collection.mjs new file mode 100644 index 0000000000000..47e15d7c4e36a --- /dev/null +++ b/components/owl_protocol/actions/deploy-collection/deploy-collection.mjs @@ -0,0 +1,52 @@ +import app from "../../owl_protocol.app.mjs"; + +export default { + key: "owl_protocol-deploy-collection", + name: "Deploy Collection", + description: "Deploy digital asset collection. [See the documentation](https://docs-api.owlprotocol.xyz/reference/collection-deploy)", + version: "0.0.1", + type: "action", + props: { + app, + chainId: { + propDefinition: [ + app, + "chainId", + ], + }, + collectionName: { + propDefinition: [ + app, + "collectionName", + ], + }, + symbol: { + propDefinition: [ + app, + "symbol", + ], + }, + description: { + propDefinition: [ + app, + "description", + ], + }, + + }, + async run({ $ }) { + const response = await this.app.deployCollection({ + $, + data: { + chainId: this.chainId, + name: this.collectionName, + symbol: this.symbol, + description: this.description, + }, + }); + + $.export("$summary", `Successfully deployed the collection '${this.collectionName}' with the contract Address: '${response.contractAddress}'`); + + return response; + }, +}; diff --git a/components/owl_protocol/actions/get-network/get-network.mjs b/components/owl_protocol/actions/get-network/get-network.mjs new file mode 100644 index 0000000000000..130b974d30c29 --- /dev/null +++ b/components/owl_protocol/actions/get-network/get-network.mjs @@ -0,0 +1,28 @@ +import app from "../../owl_protocol.app.mjs"; + +export default { + key: "owl_protocol-get-network", + name: "Get Network", + description: "Get network details by the ID. [See the documentation](https://docs-api.owlprotocol.xyz/reference/network-get)", + version: "0.0.1", + type: "action", + props: { + app, + chainId: { + propDefinition: [ + app, + "chainId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getNetwork({ + $, + chainId: this.chainId, + }); + + $.export("$summary", `Successfully retrieved data of Network: '${response.name}'`); + + return response; + }, +}; diff --git a/components/owl_protocol/actions/mint-asset/mint-asset.mjs b/components/owl_protocol/actions/mint-asset/mint-asset.mjs new file mode 100644 index 0000000000000..b46a4575c2848 --- /dev/null +++ b/components/owl_protocol/actions/mint-asset/mint-asset.mjs @@ -0,0 +1,44 @@ +import app from "../../owl_protocol.app.mjs"; + +export default { + key: "owl_protocol-mint-asset", + name: "Mint Asset", + description: "Mint digital assets for collection. [See the documentation](https://docs-api.owlprotocol.xyz/reference/collection-erc721autoid-mint)", + version: "0.0.1", + type: "action", + props: { + app, + chainId: { + propDefinition: [ + app, + "chainId", + ], + }, + address: { + propDefinition: [ + app, + "address", + ], + }, + mintTo: { + propDefinition: [ + app, + "mintTo", + ], + }, + }, + async run({ $ }) { + const response = await this.app.mintAsset({ + $, + chainId: this.chainId, + address: this.address, + data: { + to: this.mintTo, + }, + }); + + $.export("$summary", `Successfully minted asset to '${this.mintTo}'`); + + return response; + }, +}; diff --git a/components/owl_protocol/owl_protocol.app.mjs b/components/owl_protocol/owl_protocol.app.mjs new file mode 100644 index 0000000000000..bbd2a15ec5733 --- /dev/null +++ b/components/owl_protocol/owl_protocol.app.mjs @@ -0,0 +1,96 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "owl_protocol", + propDefinitions: { + chainId: { + type: "integer", + label: "Chain ID", + description: "Network Chain ID", + async options() { + const networksIds = await this.listNetworks(); + return networksIds.map(({ + chainId, name, + }) => ({ + value: chainId, + label: name, + })); + }, + }, + collectionName: { + type: "string", + label: "Collection Name", + description: "Name of the collection", + }, + symbol: { + type: "string", + label: "Collection Symbol", + description: "Ticker symbol of collection, Must be 3-4 characters long", + }, + description: { + type: "string", + label: "Collection Description", + description: "Description of the collection", + }, + address: { + type: "string", + label: "Address", + description: "An ethereum address", + }, + mintTo: { + type: "string", + label: "Mint To", + description: "Email, userId or address to mint to", + }, + }, + methods: { + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this.$auth.api_url + path, + headers: { + ...headers, + "accept": "application/json", + "x-api-key": `${this.$auth.api_key}`, + }, + }); + }, + async getNetwork({ + chainId, ...args + }) { + return this._makeRequest({ + path: `/network/${chainId}`, + ...args, + }); + }, + async mintAsset({ + chainId, address, ...args + }) { + return this._makeRequest({ + method: "post", + path: `/project/collection/${chainId}/${address}/mint/erc721AutoId`, + ...args, + }); + }, + async deployCollection(args = {}) { + return this._makeRequest({ + method: "post", + path: "/project/collection/deploy", + ...args, + }); + }, + async listNetworks(args = {}) { + return this._makeRequest({ + path: "/networks", + ...args, + }); + }, + }, +}; diff --git a/components/owl_protocol/package.json b/components/owl_protocol/package.json new file mode 100644 index 0000000000000..49add93c57b4e --- /dev/null +++ b/components/owl_protocol/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/owl_protocol", + "version": "0.1.0", + "description": "Pipedream Owl Protocol Components", + "main": "owl_protocol.app.mjs", + "keywords": [ + "pipedream", + "owl_protocol" + ], + "homepage": "https://pipedream.com/apps/owl_protocol", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/oxford_dictionaries/README.md b/components/oxford_dictionaries/README.md new file mode 100644 index 0000000000000..921ebce2b07e1 --- /dev/null +++ b/components/oxford_dictionaries/README.md @@ -0,0 +1,11 @@ +# Overview + +The Oxford Dictionaries API offers a treasure trove of linguistic data, from definitions to pronunciations, and can be a goldmine for language-related apps. Use it on Pipedream to create workflows that leverage its expansive vocab database. Automate word info retrieval, integrate with quizzes, or enrich language learning platforms— the API is your oyster. + +# Example Use Cases + +- **Word of the Day Bot**: Create a workflow that fetches the "word of the day" from Oxford Dictionaries and posts it to Slack or Discord, complete with meaning, example usage, and pronunciation. Great for educational groups or those looking to expand their vocabulary. + +- **Vocabulary Quiz App**: Develop a Pipedream workflow that interacts with the Oxford Dictionaries API to pull random words and their definitions to use in a vocabulary quiz. Integrate it with Google Sheets or Airtable to manage your question bank and score tracking. + +- **Multilingual Content Creation**: Construct a workflow that utilizes the Oxford Dictionaries API to translate keywords for international content creators on platforms like WordPress or Shopify. It can automatically pull synonyms or translations to help create rich, multilingual content. diff --git a/components/oxylabs/README.md b/components/oxylabs/README.md new file mode 100644 index 0000000000000..e0c939bfb751e --- /dev/null +++ b/components/oxylabs/README.md @@ -0,0 +1,11 @@ +# Overview + +The Oxylabs API provides robust tools for web data extraction, enabling you to gather vast amounts of data efficiently from various sources across the web. With Pipedream's serverless platform, you can harness this power to create automated workflows that trigger on specific events, process the data, and connect to other services. Whether you're monitoring brand sentiment, tracking prices, or conducting market research, integrating Oxylabs with Pipedream can significantly streamline your data collection and analysis tasks. + +# Example Use Cases + +- **Brand Monitoring Automation**: Retrieve web data on brand mentions across different sites and social media platforms using the Oxylabs API. Dispatch this data to a sentiment analysis service like Google Cloud Natural Language API, and save the sentiment scores in a Google Sheet for real-time brand sentiment tracking. + +- **Price Comparison Engine**: Schedule regular extractions of pricing data from e-commerce websites via the Oxylabs API. Use this data to compare your product prices against competitors. Pipedream can then update a dashboard in a tool like Geckoboard or send a Slack notification if a competitor changes their price. + +- **Market Research Workflow**: Collect large-scale data sets from various industry sites using Oxylabs, and feed this data into a Pipedream workflow. Perform data transformation using Pipedream's built-in Node.js code steps, then store the results in a database like PostgreSQL or send it to a BI tool like Tableau for analysis and visualization. diff --git a/components/oyster/README.md b/components/oyster/README.md new file mode 100644 index 0000000000000..782e4becfc009 --- /dev/null +++ b/components/oyster/README.md @@ -0,0 +1,14 @@ +# Overview + +The Oyster API allows you to streamline your global employment processes by providing programmatic access to their HR platform. With it, you can automate tasks like managing employee details, handling payroll, and tracking time off. Pipedream, as a serverless integration platform, makes it simple to create workflows that connect the Oyster API with hundreds of other apps. This can help in automating HR operations, syncing employee data across systems, or even sending notifications when certain HR events occur. + +# Example Use Cases + +- **Employee Onboarding Automation** +When a new employee is added in Oyster, trigger a Pipedream workflow that creates accounts for them in other tools like Slack, Google Workspace, or GitHub, assigns to-dos in a project management tool like Trello or Asana, and sends a welcome email via SendGrid or Mailgun. + +- **Payroll Processing Notification** +Set up a workflow that listens for payroll processing events in Oyster. When payroll is processed, the workflow can notify finance or the employee's manager in Slack or Microsoft Teams, log the event in a Google Sheet for record-keeping, and even trigger an email summary via Amazon SES or another email service. + +- **Time Off Sync and Alert** +Create a pipeline that monitors time-off requests in Oyster. Once approved, the workflow can block out time on the employee’s calendar in Google Calendar or Outlook, send a message to their team’s Slack channel to inform of the absence, and update any relevant task deadlines in project management tools like Asana or Jira. diff --git a/components/paddle/README.md b/components/paddle/README.md new file mode 100644 index 0000000000000..9269092279879 --- /dev/null +++ b/components/paddle/README.md @@ -0,0 +1,11 @@ +# Overview + +The Paddle API on Pipedream allows you to integrate your Paddle account to automate billing, subscription, and user management processes. You can react to events like new purchases or subscription cancellations, sync customer data to other platforms, and even automate financial reporting. This API hooks into Pipedream's capabilities of managing complex workflows with multiple steps, without the need for manual coding or server management. + +# Example Use Cases + +- **Automate Customer Onboarding**: When a new user purchases a product through Paddle, trigger a Pipedream workflow to send a personalized welcome email through SendGrid and add their contact information to a Mailchimp list for future marketing. + +- **Sync Purchase Data to a Database**: Configure a workflow to capture new Paddle transactions and store them in a PostgreSQL database. This can help maintain a record of purchases and assist with financial analysis or inventory management. + +- **Handle Subscription Changes**: Set up a listener for subscription update events from Paddle. When a user upgrades, downgrades, or cancels their subscription, use this information to update their permissions in a third-party app like Intercom or Zendesk for customer support tracking. diff --git a/components/paddle/package.json b/components/paddle/package.json new file mode 100644 index 0000000000000..3010d442e47fd --- /dev/null +++ b/components/paddle/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/paddle", + "version": "0.0.1", + "description": "Pipedream Paddle Components", + "main": "paddle.app.mjs", + "keywords": [ + "pipedream", + "paddle" + ], + "homepage": "https://pipedream.com/apps/paddle", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/paddle/paddle.app.mjs b/components/paddle/paddle.app.mjs new file mode 100644 index 0000000000000..98474e3cabdc2 --- /dev/null +++ b/components/paddle/paddle.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "paddle", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/page_x/actions/add-new-lead/add-new-lead.mjs b/components/page_x/actions/add-new-lead/add-new-lead.mjs new file mode 100644 index 0000000000000..e1065dc877dc1 --- /dev/null +++ b/components/page_x/actions/add-new-lead/add-new-lead.mjs @@ -0,0 +1,49 @@ +import pageX from "../../page_x.app.mjs"; + +export default { + key: "page_x-add-new-lead", + name: "Add New Lead", + description: "Create a new lead on PageX CRM. [See the documentation](https://rapidapi.com/thunderhurt/api/pagexcrm)", + version: "0.0.1", + type: "action", + props: { + pageX, + customerId: { + type: "string", + label: "Customer ID", + description: "Unique customer ID if exists", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "Email address of the lead", + }, + phone: { + type: "string", + label: "Phone", + description: "Phone number of the lead", + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "Full name of the lead", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.pageX.createLead({ + $, + data: { + customer_id: this.customerId, + email: this.email, + phone: this.phone, + name: this.name, + }, + }); + + $.export("$summary", `Successfully created lead with email: ${this.email}`); + return response; + }, +}; diff --git a/components/page_x/package.json b/components/page_x/package.json new file mode 100644 index 0000000000000..a2ab234c9f25e --- /dev/null +++ b/components/page_x/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/page_x", + "version": "0.1.0", + "description": "Pipedream PAGE X Components", + "main": "page_x.app.mjs", + "keywords": [ + "pipedream", + "page_x" + ], + "homepage": "https://pipedream.com/apps/page_x", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} diff --git a/components/page_x/page_x.app.mjs b/components/page_x/page_x.app.mjs new file mode 100644 index 0000000000000..49220ceef47d1 --- /dev/null +++ b/components/page_x/page_x.app.mjs @@ -0,0 +1,41 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "page_x", + methods: { + _apiKey() { + return `${this.$auth.api_key}`; + }, + _baseUrl() { + return "https://pagexcrm.p.rapidapi.com/api"; + }, + _headers(data = {}) { + return { + "Content-Type": `multipart/form-data; boundary=${data._boundary}`, + "x-rapidapi-host": "pagexcrm.p.rapidapi.com", + "x-rapidapi-key": `${this.$auth.rapidapi_key}`, + }; + }, + _makeRequest({ + $ = this, path, data, ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._headers(data), + data: { + ...data, + api_key: this._apiKey(), + }, + ...opts, + }); + }, + createLead(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/lead", + ...opts, + }); + }, + }, +}; diff --git a/components/pagerduty/README.md b/components/pagerduty/README.md index d2c41c6261074..b9b1c7f3c9e06 100644 --- a/components/pagerduty/README.md +++ b/components/pagerduty/README.md @@ -1,16 +1,11 @@ # Overview -PagerDuty's APIs allow you to integrate the powerful incident response -capabilities of PagerDuty with your own custom applications. With PagerDuty's -APIs, you can build applications to optimize your incident response process, -collaborate with teams more effectively, and analyze data from your incident -response process. Here are some examples of what you can do with PagerDuty's -APIs: +The PagerDuty API offers a powerful interface to automate your digital operations management. By leveraging its capabilities on Pipedream, you can create workflows that respond to incidents, automate alerts, and synchronize incident data across various platforms. PagerDuty's API enables you to manage services, teams, and incidents, ensuring that your systems remain operational and that the right people are notified at the right time. -- Create and manage automated incident response workflows -- Automatically trigger actions based on incident response events -- Enable communication between PagerDuty and other third party applications -- Pull data from your previous incident response processes to identify best - practices -- Create custom reporting tools and insights into incident response processes -- Monitor multiple teams and services at once +# Example Use Cases + +- **Incident Response Coordination**: Trigger a workflow on Pipedream when a new incident is reported in PagerDuty. Automatically notify team members via Slack, create a Zoom meeting for immediate response, and log the incident details in a Google Sheet for record keeping. + +- **Scheduled On-Call Reminders**: Use Pipedream to schedule and send on-call reminders to team members. The workflow could check the PagerDuty on-call schedule and send an SMS via Twilio to the on-call engineer the day before their shift starts, ensuring they are aware and prepared. + +- **Automated Incident Escalation**: Create a Pipedream workflow that listens for incidents that haven't been acknowledged within a set time frame. Automatically escalate the issue by creating a Jira ticket, posting a message to a specific Microsoft Teams channel, and calling the secondary on-call person via Twilio. diff --git a/components/paigo/README.md b/components/paigo/README.md new file mode 100644 index 0000000000000..b6a22b5d909ef --- /dev/null +++ b/components/paigo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Paigo API offers a suite of financial tools for expense tracking, budgeting, and financial planning. In Pipedream, you can utilize these capabilities to automate personal finance management, analyze spending habits, and receive real-time notifications on financial events. Paigo's API can be integrated with various apps on Pipedream to streamline financial operations, from syncing transactions with accounting software to triggering alerts when budgets are nearing their limits. + +# Example Use Cases + +- **Expense Tracking Automation**: Create a Pipedream workflow that monitors your bank transactions via the Paigo API. Whenever a new transaction occurs, the workflow can classify the expense and log it into a Google Sheet. This helps in maintaining an up-to-date overview of your finances without manual data entry. + +- **Budget Threshold Alerts**: Set up a workflow that uses the Paigo API to regularly check your spending against predefined budget categories. If spending approaches the budget limit, the workflow can trigger an SMS or email alert using Twilio or SendGrid. This proactive approach keeps you informed and helps avoid overspending. + +- **Financial Dashboard Updates**: Build a dashboard in a tool like Geckoboard to visualize your financial data. With a workflow on Pipedream, you can fetch the latest financial data from Paigo API at regular intervals and push updates to your dashboard, ensuring you always have access to real-time insights into your financial health. diff --git a/components/paigo/actions/add-credits/add-credits.mjs b/components/paigo/actions/add-credits/add-credits.mjs new file mode 100644 index 0000000000000..6e5a68270e646 --- /dev/null +++ b/components/paigo/actions/add-credits/add-credits.mjs @@ -0,0 +1,34 @@ +import paigo from "../../paigo.app.mjs"; + +export default { + key: "paigo-add-credits", + name: "Add Credits", + description: "Increments the credit balance of a specific customer. [See the documentation](http://www.api.docs.paigo.tech/#tag/Customers/operation/Create%20a%20wallet%20transaction)", + version: "0.0.1", + type: "action", + props: { + paigo, + customerId: { + propDefinition: [ + paigo, + "customerId", + ], + }, + creditAmount: { + type: "string", + label: "Credit Amount", + description: "The amount to credit the customer. Can be positive or negative. Customers cannot have a negative balance set via the API.", + }, + }, + async run({ $ }) { + const response = await this.paigo.incrementCreditBalance({ + $, + customerId: this.customerId, + data: { + transactionAmount: this.creditAmount, + }, + }); + $.export("$summary", `Successfully added ${this.creditAmount} credits to customer ${this.customerId}`); + return response; + }, +}; diff --git a/components/paigo/actions/create-dimension/create-dimension.mjs b/components/paigo/actions/create-dimension/create-dimension.mjs new file mode 100644 index 0000000000000..7899f6d973cd2 --- /dev/null +++ b/components/paigo/actions/create-dimension/create-dimension.mjs @@ -0,0 +1,79 @@ +import paigo from "../../paigo.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "paigo-create-dimension", + name: "Create Dimension", + description: "Creates a new dimension inside the Paigo platform. [See the documentation](http://www.api.docs.paigo.tech/#tag/Dimensions/operation/Create%20a%20dimension)", + version: "0.0.1", + type: "action", + props: { + paigo, + dimensionName: { + type: "string", + label: "Dimension Name", + description: "A friendly, human-readable name for the dimension", + }, + consumptionUnitType: { + type: "string", + label: "Consumption Unit Type", + description: "Consumption unit type of the dimension", + options: Object.keys(constants.CONSUMPTION_UNITS), + }, + consumptionUnit: { + type: "string", + label: "Consumption Unit", + description: "Consumption unit type of the dimension", + options() { + return constants.CONSUMPTION_UNITS[this.consumptionUnitType] || []; + }, + }, + usageIncrement: { + type: "string", + label: "Usage Increment", + description: "The minimum increment for usage amount. As an example, if usage increment is 1 Hour job execution time, then 1 Hour and 5 Minutes execution time may be calculated as 1 Hour or 2 Hours, depending on the rounding algorithm field of the dimension.", + }, + rounding: { + type: "string", + label: "Rounding", + description: "The rounding algorithm that is used to calculate the amount of usage increment. Ceiling algorithm rounds up, floor algorithm rounds down, the round algrogrithm rounds to the nearest whole integer rounding half away from zero.", + options: constants.USAGE_ROUNDING, + }, + usageEntitlement: { + type: "string", + label: "Usage Entitlement", + description: "Used with Subscription Tier Offering type. SaaS customers subscribed to a subscription tier are entitled to use the amount of product with regard to the dimension up to the value specified in this field. For example, a subscription tier may entitle subscribers to make up to 1,000,000 API requests.", + default: "inf", + optional: true, + }, + overageAllowed: { + type: "string", + label: "Overage Allowed", + description: "Used with Subscription Tier Offering type. When the usage entitlement is specified, this field decides if allowing SaaS customers to use more than entitled amount of the product dimension.", + default: "true", + optional: true, + options: [ + "true", + "false", + ], + }, + }, + async run({ $ }) { + const response = await this.paigo.createDimension({ + $, + data: { + dimensionName: this.dimensionName, + consumptionUnit: { + type: this.consumptionUnitType, + unit: this.consumptionUnit, + }, + usageIncrement: this.usageIncrement, + rounding: this.rounding, + usageEntitlement: this.usageEntitlement, + overageAllowed: this.overageAllowed, + }, + }); + $.export("$summary", `Successfully created dimension with ID: ${response.dimensionId}`); + return response; + }, +}; diff --git a/components/paigo/actions/create-offering/create-offering.mjs b/components/paigo/actions/create-offering/create-offering.mjs new file mode 100644 index 0000000000000..6625b046624e6 --- /dev/null +++ b/components/paigo/actions/create-offering/create-offering.mjs @@ -0,0 +1,78 @@ +import paigo from "../../paigo.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "paigo-create-offering", + name: "Create Offering", + description: "Creates a new offering in the Paigo platform. [See the documentation](http://www.api.docs.paigo.tech/#tag/Offerings/operation/Create%20an%20offering)", + version: "0.0.1", + type: "action", + props: { + paigo, + offeringName: { + type: "string", + label: "Offering Name", + description: "A friendly, human-readable name for the offering.", + }, + dimensionIds: { + propDefinition: [ + paigo, + "dimensionId", + ], + type: "string[]", + label: "Dimension IDs", + description: "Array of the identifier of the dimensions that this offering contains. Dimensions specify the type of usage that is being billed for.", + optional: true, + }, + offeringVisibility: { + type: "string", + label: "Offering Visibility", + description: "The visibility of the offering, specifically if its private or public. Public offerings are designed to be shared among customers. Private offerings are typically used for enterprise deals which contain discounts or prepaid credits.", + options: constants.OFFERING_VISIBILITY, + default: "public", + optional: true, + }, + offeringType: { + type: "string", + label: "Offering Type", + description: "The type of offering", + options: constants.OFFERING_TYPE, + optional: true, + }, + billingCycle: { + type: "string", + label: "Billing Cycle", + description: "The time frame when an automatic bill should be sent. Leave empty for no automated billing", + options: constants.BILLING_CYCLE, + optional: true, + }, + subscriptionPrice: { + type: "string", + label: "SubscriptionPrice", + description: "The price of the subscription. Only positive number string is allowed. Only required if `offeringType` is `subscription`.", + optional: true, + }, + freeTrialLength: { + type: "string", + label: "Free Trial Length", + description: "The length of time for a free trial. This is a number of days. Only positive number string is allowed.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.paigo.createOffering({ + $, + data: { + offeringName: this.offeringName, + dimensionIds: this.dimensionIds || [], + offeringVisibility: this.offeringVisibility, + offeringType: this.offeringType, + billingCycle: this.billingCycle, + subscriptionPrice: this.subscriptionPrice, + freeTrialLength: this.freeTrialLength, + }, + }); + $.export("$summary", `Successfully created offering with ID: ${response.offeringId}`); + return response; + }, +}; diff --git a/components/paigo/actions/get-invoice/get-invoice.mjs b/components/paigo/actions/get-invoice/get-invoice.mjs new file mode 100644 index 0000000000000..408edf8e52313 --- /dev/null +++ b/components/paigo/actions/get-invoice/get-invoice.mjs @@ -0,0 +1,36 @@ +import paigo from "../../paigo.app.mjs"; + +export default { + key: "paigo-get-invoice", + name: "Get Invoice", + description: "Fetches detailed information about a specific invoice. [See the documentation](http://www.api.docs.paigo.tech/#tag/Invoices/operation/Get%20an%20Invoice%20by%20ID)", + version: "0.0.1", + type: "action", + props: { + paigo, + customerId: { + propDefinition: [ + paigo, + "customerId", + ], + optional: true, + }, + invoiceId: { + propDefinition: [ + paigo, + "invoiceId", + (c) => ({ + customerId: c.customerId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.paigo.getInvoice({ + $, + invoiceId: this.invoiceId, + }); + $.export("$summary", `Successfully fetched details for invoice with ID: ${this.invoiceId}`); + return response; + }, +}; diff --git a/components/paigo/actions/measure-usage/measure-usage.mjs b/components/paigo/actions/measure-usage/measure-usage.mjs new file mode 100644 index 0000000000000..093743cd0f4ee --- /dev/null +++ b/components/paigo/actions/measure-usage/measure-usage.mjs @@ -0,0 +1,51 @@ +import paigo from "../../paigo.app.mjs"; + +export default { + key: "paigo-measure-usage", + name: "Measure Usage", + description: "Records the amount of a specific usage type linked with a customer. [See the documentation](http://www.api.docs.paigo.tech/#tag/Usage/operation/Collect%20usage%20data)", + version: "0.0.1", + type: "action", + props: { + paigo, + customerId: { + propDefinition: [ + paigo, + "customerId", + ], + }, + dimensionId: { + propDefinition: [ + paigo, + "dimensionId", + ], + }, + usageAmount: { + type: "string", + label: "Usage Amount", + description: "The amount of the usage on this record. Numerical values are represented as strings to avoid precision loss.", + }, + timestamp: { + type: "string", + label: "Timestamp", + description: "The timestamp of usage record in RFC3339 format with a 4-digit year. This is the time the usage occurred, or the end of the usage period. Defaults to the current time.", + default: new Date().toISOString() + .slice(0, 19) + .replace("T", " ") + "Z", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.paigo.recordUsage({ + $, + data: { + customerId: this.customerId, + dimensionId: this.dimensionId, + recordValue: this.usageAmount, + timestamp: this.timestamp, + }, + }); + $.export("$summary", `Recorded usage for customer: ${this.customerId}`); + return response; + }, +}; diff --git a/components/paigo/actions/onboard-customer/onboard-customer.mjs b/components/paigo/actions/onboard-customer/onboard-customer.mjs new file mode 100644 index 0000000000000..93968f2484bad --- /dev/null +++ b/components/paigo/actions/onboard-customer/onboard-customer.mjs @@ -0,0 +1,49 @@ +import paigo from "../../paigo.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "paigo-onboard-customer", + name: "Onboard Customer", + description: "Creates a new customer and assigns them an offering in Paigo. [See the documentation](http://www.api.docs.paigo.tech/#tag/Customers/operation/Create%20a%20customer)", + version: "0.0.1", + type: "action", + props: { + paigo, + customerName: { + type: "string", + label: "Customer Name", + description: "The friendly, human-readable name for the customer profile", + }, + email: { + type: "string", + label: "Email", + description: "Customer email address", + }, + paymentChannel: { + type: "string", + label: "Payment Channel", + description: "The payment channel associated with a customer", + options: constants.PAYMENT_CHANNELS, + }, + offeringId: { + propDefinition: [ + paigo, + "offeringId", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.paigo.createCustomer({ + $, + data: { + customerName: this.customerName, + email: this.email, + paymentChannel: this.paymentChannel, + offeringId: this.offeringId, + }, + }); + $.export("$summary", `Successfully onboarded customer with ID ${response.customerId}`); + return response; + }, +}; diff --git a/components/paigo/common/constants.mjs b/components/paigo/common/constants.mjs new file mode 100644 index 0000000000000..e20f9d41f09a4 --- /dev/null +++ b/components/paigo/common/constants.mjs @@ -0,0 +1,58 @@ +const CONSUMPTION_UNITS = { + "count": [ + "count-based", + ], + "time-based": [ + "second", + "minute", + "hour", + "day", + ], + "data-based": [ + "byte", + "kilobyte", + "megabyte", + "gigabyte", + ], +}; + +const USAGE_ROUNDING = [ + "round", + "floor", + "ceiling", +]; + +const OFFERING_VISIBILITY = [ + "private", + "public", +]; + +const OFFERING_TYPE = [ + "usage-based", + "subscription", +]; + +const BILLING_CYCLE = [ + "monthly", + "annualToDate", +]; + +const PAYMENT_CHANNELS = [ + "Stripe", + "manual", +]; + +const ENVIRONMENTS = [ + "production", + "sandbox", +]; + +export default { + CONSUMPTION_UNITS, + USAGE_ROUNDING, + OFFERING_VISIBILITY, + OFFERING_TYPE, + BILLING_CYCLE, + PAYMENT_CHANNELS, + ENVIRONMENTS, +}; diff --git a/components/paigo/package.json b/components/paigo/package.json new file mode 100644 index 0000000000000..12a3223127064 --- /dev/null +++ b/components/paigo/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/paigo", + "version": "0.1.0", + "description": "Pipedream Paigo Components", + "main": "paigo.app.mjs", + "keywords": [ + "pipedream", + "paigo" + ], + "homepage": "https://pipedream.com/apps/paigo", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/paigo/paigo.app.mjs b/components/paigo/paigo.app.mjs new file mode 100644 index 0000000000000..4a4565331d398 --- /dev/null +++ b/components/paigo/paigo.app.mjs @@ -0,0 +1,179 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "paigo", + propDefinitions: { + offeringId: { + type: "string", + label: "Offering ID", + description: "The unique identifier for the offering", + async options() { + const { data } = await this.listOfferings(); + return data?.map(({ + offeringId: value, offeringName: label, + }) => ({ + value, + label, + })) || []; + }, + }, + customerId: { + type: "string", + label: "Customer ID", + description: "The unique identifier for the customer", + async options() { + const { data } = await this.listCustomers(); + return data?.map(({ + customerId: value, customerName: label, + }) => ({ + value, + label, + })) || []; + }, + }, + invoiceId: { + type: "string", + label: "Invoice ID", + description: "The unique identifier for the invoice", + async options({ customerId }) { + if (!customerId) { + return []; + } + const { data } = await this.getCustomer({ + customerId, + }); + if (!data?.length) { + return []; + } + const { invoices } = data[0]; + return invoices?.map(({ + invoiceId: value, invoiceDate, currency, amountPaid, + }) => ({ + value, + label: `${invoiceDate} - ${amountPaid} ${currency}`, + })) || []; + }, + }, + dimensionId: { + type: "string", + label: "Dimension ID", + description: "The unique identifier for the dimension", + async options() { + const { data } = await this.listDimensions(); + return data?.map(({ + dimensionId: value, dimensionName: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.prod.paigo.tech"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks/subscribe", + ...opts, + }); + }, + deleteWebhook({ + hookId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${hookId}`, + ...opts, + }); + }, + getInvoice({ + invoiceId, ...opts + }) { + return this._makeRequest({ + path: `/invoices/${invoiceId}`, + ...opts, + }); + }, + getCustomer({ + customerId, ...opts + }) { + return this._makeRequest({ + path: `/customers/${customerId}`, + ...opts, + }); + }, + listOfferings(opts = {}) { + return this._makeRequest({ + path: "/offerings", + ...opts, + }); + }, + listCustomers(opts = {}) { + return this._makeRequest({ + path: "/customers", + ...opts, + }); + }, + listDimensions(opts = {}) { + return this._makeRequest({ + path: "/dimensions", + ...opts, + }); + }, + recordUsage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/usage", + ...opts, + }); + }, + createDimension(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/dimensions", + ...opts, + }); + }, + incrementCreditBalance({ + customerId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/customers/${customerId}/transactions`, + ...opts, + }); + }, + createOffering(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/offerings", + ...opts, + }); + }, + createCustomer(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/customers", + ...opts, + }); + }, + }, +}; diff --git a/components/paigo/sources/common/base.mjs b/components/paigo/sources/common/base.mjs new file mode 100644 index 0000000000000..92474b9b84564 --- /dev/null +++ b/components/paigo/sources/common/base.mjs @@ -0,0 +1,61 @@ +import paigo from "../../paigo.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + paigo, + db: "$.service.db", + http: "$.interface.http", + environment: { + type: "string", + label: "Environment", + description: "The environment the webhook is for. This is used to differentiate between sandbox and production. Will default to `production` if not provided.", + options: constants.ENVIRONMENTS, + optional: true, + }, + }, + hooks: { + async activate() { + const data = { + hookUrl: this.http.endpoint, + webhookType: this.getWebhookType(), + }; + if (this.offeringId) { + data.offeringId = this.offeringId; + } + if (this.environment) { + data.environment = this.environment; + } + const { webhookId } = await this.paigo.createWebhook({ + data, + }); + this._setHookId(webhookId); + }, + async deactivate() { + const hookId = this._getHookId(); + if (hookId) { + await this.paigo.deleteWebhook({ + hookId, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getWebhookType() { + throw new Error("getWebhookType is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run({ body }) { + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, +}; diff --git a/components/paigo/sources/entitlement-threshold-reached-instant/entitlement-threshold-reached-instant.mjs b/components/paigo/sources/entitlement-threshold-reached-instant/entitlement-threshold-reached-instant.mjs new file mode 100644 index 0000000000000..2946fbd1592e3 --- /dev/null +++ b/components/paigo/sources/entitlement-threshold-reached-instant/entitlement-threshold-reached-instant.mjs @@ -0,0 +1,36 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "paigo-entitlement-threshold-reached-instant", + name: "Entitlement Threshold Reached (Instant)", + description: "Emit new event when customers' usage reaches a threshold of 80% or 100% of their offerings. [See the documentation](http://www.api.docs.paigo.tech/#tag/Webhooks/operation/Subscribe%20a%20Webhook)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + offeringId: { + propDefinition: [ + common.props.paigo, + "offeringId", + ], + }, + }, + methods: { + ...common.methods, + getWebhookType() { + return "ENTITLEMENT"; + }, + generateMeta(event) { + const ts = Date.parse(event.timestamp); + return { + id: `${event.customerId}-${ts}`, + summary: `Entitlement threshold reached for customer ${event.customerId}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/paigo/sources/entitlement-threshold-reached-instant/test-event.mjs b/components/paigo/sources/entitlement-threshold-reached-instant/test-event.mjs new file mode 100644 index 0000000000000..8f04ed0ce4ead --- /dev/null +++ b/components/paigo/sources/entitlement-threshold-reached-instant/test-event.mjs @@ -0,0 +1,11 @@ +export default { + "timestamp": "2024-04-01T20:10:25.406Z", + "dimensionId": "030ff7c6-be56-405f-a54b-eccea7e0f905", + "customerId": "b793d484-746e-4ecf-b137-ace9741a1dee", + "entitlementLimit": "100", + "email": "noreply@paigo.tech", + "eventType": "usageEntitlementLimitReached", + "currentUsageTotal": "100", + "customerName": "Test Customer", + "webhookType": "ENTITLEMENT" +} \ No newline at end of file diff --git a/components/paigo/sources/new-customer-instant/new-customer-instant.mjs b/components/paigo/sources/new-customer-instant/new-customer-instant.mjs new file mode 100644 index 0000000000000..12152f1560bd0 --- /dev/null +++ b/components/paigo/sources/new-customer-instant/new-customer-instant.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "paigo-new-customer-instant", + name: "New Customer (Instant)", + description: "Emit new event when a customer account is created. [See the documentation](http://www.api.docs.paigo.tech/#tag/Webhooks/operation/Subscribe%20a%20Webhook)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getWebhookType() { + return "CUSTOMER_CREATED"; + }, + generateMeta(customer) { + return { + id: customer.customerId, + summary: `New Customer ${customer.customerName}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/paigo/sources/new-customer-instant/test-event.mjs b/components/paigo/sources/new-customer-instant/test-event.mjs new file mode 100644 index 0000000000000..41f08501c394a --- /dev/null +++ b/components/paigo/sources/new-customer-instant/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "customerId": "7663f0bf-a162-4064-a829-6c86ec790323", + "customerName": "Test Customer", + "paymentChannel": "manual", + "email": "test@paigo.tech", + "paymentChannelOptions": { + "stripeCustomerId": "" + }, + "customerVatId": "", + "taxExempt": "none", + "offeringId": null, + "currency": "USD", + "invoices": null, + "children": [] +} \ No newline at end of file diff --git a/components/paigo/sources/new-invoice-instant/new-invoice-instant.mjs b/components/paigo/sources/new-invoice-instant/new-invoice-instant.mjs new file mode 100644 index 0000000000000..fc8554d413c5c --- /dev/null +++ b/components/paigo/sources/new-invoice-instant/new-invoice-instant.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "paigo-new-invoice-instant", + name: "New Invoice (Instant)", + description: "Emit new event whenever a new invoice is generated. [See the documentation](http://www.api.docs.paigo.tech/#tag/Webhooks/operation/Subscribe%20a%20Webhook)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getWebhookType() { + return "INVOICE_CREATED"; + }, + generateMeta(invoice) { + return { + id: invoice.invoiceId, + summary: `${invoice.message} ${invoice.invoiceId}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/paigo/sources/new-invoice-instant/test-event.mjs b/components/paigo/sources/new-invoice-instant/test-event.mjs new file mode 100644 index 0000000000000..5b35fea2d009e --- /dev/null +++ b/components/paigo/sources/new-invoice-instant/test-event.mjs @@ -0,0 +1,5 @@ +export default { + "invoiceId": "b6cfd3dd-d30e-456c-bf83-3a1711bf78a5", + "message": "Generated invoice", + "customerId": "7663f0bf-a162-4064-a829-6c86ec790323" +} \ No newline at end of file diff --git a/components/pandadoc/README.md b/components/pandadoc/README.md index df02dfec1cb04..5a47a527169df 100644 --- a/components/pandadoc/README.md +++ b/components/pandadoc/README.md @@ -1,22 +1,11 @@ # Overview -The PandaDoc API is a powerful tool that can be used to create and customize -documents with ease. With the PandaDoc API, you can connect to the PandaDoc -cloud Platform and create automated document workflows, streamline document -creation and editing, and more. +The PandaDoc API opens up a realm of possibilities for automating document workflows, creating a seamless bridge between document management and various business processes. With it, you can programmatically create, send, and track documents, streamline electronic signatures, and manage templates, among others. Integrations through Pipedream can harness these capabilities, enabling you to trigger actions in PandaDoc based on events from other apps, or vice versa. -Using the PandaDoc API, you can: +# Example Use Cases -- Automate and streamline document creation and editing -- Create custom document workflows -- Securely store documents and access document data -- Track document activity and analyze document data -- Automate document approvals and sign-off -- Integrate PandaDoc with third-party applications -- Personalize documents with dynamic data fields -- Generate intelligent document insights -- View, sign, and approve documents online -- Automatically store documents in cloud-based file storage -- Automatically send documents directly to customers -- Customize integration with your organization's branding -- Create reports and dashboard to monitor document performance +- **Sales Pipeline Automation**: When a new deal is marked as "won" in a CRM like Salesforce, trigger a PandaDoc workflow to automatically generate a contract from a template, fill it with the deal details, and send it out for e-signature. + +- **HR Onboarding Processes**: Sync PandaDoc with an HR platform like BambooHR. Once a new employee is added to the HR system, kick off a workflow that auto-populates and sends out onboarding paperwork through PandaDoc. + +- **Real-time Notifications**: Combine PandaDoc with a messaging platform like Slack. Set up a workflow that sends a message to a designated channel or direct message when a document is completed or a specific action is taken on a document in PandaDoc. diff --git a/components/pandadoc/actions/create-document-attachment/create-document-attachment.mjs b/components/pandadoc/actions/create-document-attachment/create-document-attachment.mjs index 204a6ab7ed24c..ce7caf8c31f87 100644 --- a/components/pandadoc/actions/create-document-attachment/create-document-attachment.mjs +++ b/components/pandadoc/actions/create-document-attachment/create-document-attachment.mjs @@ -7,9 +7,9 @@ import FormData from "form-data"; export default { key: "pandadoc-create-document-attachment", name: "Create Document Attachment", - description: "Adds an attachment to a document. [See the docs here](https://developers.pandadoc.com/reference/create-document-attachment)", + description: "Adds an attachment to a document. [See the documentation here](https://developers.pandadoc.com/reference/create-document-attachment)", type: "action", - version: "0.0.6", + version: "0.0.7", props: { app, documentId: { diff --git a/components/pandadoc/actions/create-document-from-file/create-document-from-file.mjs b/components/pandadoc/actions/create-document-from-file/create-document-from-file.mjs index af5ab4a2fed63..dcd5bc0d454f2 100644 --- a/components/pandadoc/actions/create-document-from-file/create-document-from-file.mjs +++ b/components/pandadoc/actions/create-document-from-file/create-document-from-file.mjs @@ -5,9 +5,9 @@ import createDocumentAttachment from "../create-document-attachment/create-docum export default { key: "pandadoc-create-document-from-file", name: "Create Document From File", - description: "Create a document from a file or public file URL. [See the docs here](https://developers.pandadoc.com/reference/create-document-from-pdf)", + description: "Create a document from a file or public file URL. [See the documentation here](https://developers.pandadoc.com/reference/create-document-from-pdf)", type: "action", - version: "0.0.7", + version: "0.0.8", props: { app, name: { diff --git a/components/pandadoc/actions/create-document-from-template/create-document-from-template.mjs b/components/pandadoc/actions/create-document-from-template/create-document-from-template.mjs index b522d86cb7ed6..54a508d562b12 100644 --- a/components/pandadoc/actions/create-document-from-template/create-document-from-template.mjs +++ b/components/pandadoc/actions/create-document-from-template/create-document-from-template.mjs @@ -3,9 +3,9 @@ import app from "../../pandadoc.app.mjs"; export default { key: "pandadoc-create-document-from-template", name: "Create Document From Template", - description: "Create Document from PandaDoc Template. [See the docs here](https://developers.pandadoc.com/reference/create-document-from-pandadoc-template)", + description: "Create a Document from a PandaDoc Template. [See the documentation here](https://developers.pandadoc.com/reference/create-document-from-pandadoc-template)", type: "action", - version: "0.0.6", + version: "0.0.7", props: { app, name: { diff --git a/components/pandadoc/actions/create-folder/create-folder.mjs b/components/pandadoc/actions/create-folder/create-folder.mjs index fa88f3d0f8708..b02c8fbe3dac3 100644 --- a/components/pandadoc/actions/create-folder/create-folder.mjs +++ b/components/pandadoc/actions/create-folder/create-folder.mjs @@ -3,9 +3,9 @@ import app from "../../pandadoc.app.mjs"; export default { key: "pandadoc-create-folder", name: "Create Folder", - description: "Create a new folder to store your documents. [See the docs here](https://developers.pandadoc.com/reference/create-documents-folder)", + description: "Create a new folder to store your documents. [See the documentation here](https://developers.pandadoc.com/reference/create-documents-folder)", type: "action", - version: "0.0.5", + version: "0.0.6", props: { app, name: { diff --git a/components/pandadoc/actions/create-or-update-contact/create-or-update-contact.mjs b/components/pandadoc/actions/create-or-update-contact/create-or-update-contact.mjs index 12c02ce0f4e2a..3447e49061723 100644 --- a/components/pandadoc/actions/create-or-update-contact/create-or-update-contact.mjs +++ b/components/pandadoc/actions/create-or-update-contact/create-or-update-contact.mjs @@ -3,9 +3,9 @@ import app from "../../pandadoc.app.mjs"; export default { key: "pandadoc-create-or-update-contact", name: "Create or Update Contact", - description: "This method adds or updates a contact using the email as index. [See the docs here](https://developers.pandadoc.com/reference/create-contact)", + description: "This method adds or updates a contact using the email as index. [See the documentation here](https://developers.pandadoc.com/reference/create-contact)", type: "action", - version: "0.0.6", + version: "0.0.7", props: { app, email: { diff --git a/components/pandadoc/actions/document-details/document-details.mjs b/components/pandadoc/actions/document-details/document-details.mjs index 8520e47a63ad7..082b060c8bec7 100644 --- a/components/pandadoc/actions/document-details/document-details.mjs +++ b/components/pandadoc/actions/document-details/document-details.mjs @@ -2,10 +2,10 @@ import app from "../../pandadoc.app.mjs"; export default { key: "pandadoc-document-details", - name: "Document Details", - description: "Return detailed data about a document. [See the docs here](https://developers.pandadoc.com/reference/document-details)", + name: "Get Document Details", + description: "Return detailed data about a document. [See the documentation here](https://developers.pandadoc.com/reference/document-details)", type: "action", - version: "0.0.6", + version: "0.0.7", props: { app, id: { diff --git a/components/pandadoc/actions/download-document/download-document.mjs b/components/pandadoc/actions/download-document/download-document.mjs new file mode 100644 index 0000000000000..901291a4047c2 --- /dev/null +++ b/components/pandadoc/actions/download-document/download-document.mjs @@ -0,0 +1,92 @@ +import app from "../../pandadoc.app.mjs"; +import fs from "fs"; + +export default { + key: "pandadoc-download-document", + name: "Download Document", + description: + "Download a document as PDF. [See documentation here](https://developers.pandadoc.com/reference/download-document)", + type: "action", + version: "0.0.1", + props: { + app, + id: { + propDefinition: [ + app, + "documentId", + ], + }, + outputFilename: { + propDefinition: [ + app, + "outputFilename", + ], + }, + showWatermark: { + type: "boolean", + label: "Apply Watermark", + description: "Set to true to show available watermark props", + reloadProps: true, + }, + separateFiles: { + propDefinition: [ + app, + "separateFiles", + ], + }, + }, + additionalProps() { + return this.showWatermark + ? { + watermarkText: { + type: "string", + label: "Watermark Text", + description: "Specify watermark text", + optional: true, + }, + watermarkColor: { + type: "string", + label: "Watermark Color", + description: "Should be a HEX code `#RRGGBB`", + optional: true, + }, + watermarkFontSize: { + type: "integer", + label: "Watermark Font Size", + description: "Font size of the watermark - positive integer", + optional: true, + }, + watermarkOpacity: { + type: "string", + label: "Watermark Opacity", + description: "Should be in range 0.0 - 1.0", + optional: true, + }, + } + : {}; + }, + async run({ $ }) { + const { + outputFilename, id, + } = this; + const data = await this.app.downloadDocument({ + $, + id, + params: { + separate_files: this.separateFiles, + watermark_text: this.watermarkText, + watermark_color: this.watermarkColor, + watermark_font_size: this.watermarkFontSize, + watermark_opacity: this.watermarkOpacity, + }, + }); + + const filePath = `/tmp/${outputFilename}`; + fs.writeFileSync(filePath, data); + $.export("$summary", `Successfully downloaded document "${outputFilename}"`); + + return { + filePath, + }; + }, +}; diff --git a/components/pandadoc/actions/download-protected-document/download-protected-document.mjs b/components/pandadoc/actions/download-protected-document/download-protected-document.mjs new file mode 100644 index 0000000000000..5ca0d96754b51 --- /dev/null +++ b/components/pandadoc/actions/download-protected-document/download-protected-document.mjs @@ -0,0 +1,52 @@ +import app from "../../pandadoc.app.mjs"; +import fs from "fs"; + +export default { + key: "pandadoc-download-protected-document", + name: "Download Protected Document", + description: + "Download a completed document as a verifiable PDF. [See documentation here](https://developers.pandadoc.com/reference/download-protected-document)", + type: "action", + version: "0.0.1", + props: { + app, + id: { + propDefinition: [ + app, + "completedDocumentId", + ], + }, + outputFilename: { + propDefinition: [ + app, + "outputFilename", + ], + }, + separateFiles: { + propDefinition: [ + app, + "separateFiles", + ], + }, + }, + async run({ $ }) { + const { + outputFilename, id, + } = this; + const data = await this.app.downloadProtectedDocument({ + $, + id, + params: { + separate_files: this.separateFiles, + }, + }); + + const filePath = `/tmp/${outputFilename}`; + fs.writeFileSync(filePath, data); + $.export("$summary", `Successfully downloaded protected document "${outputFilename}"`); + + return { + filePath, + }; + }, +}; diff --git a/components/pandadoc/actions/get-document-status/get-document-status.mjs b/components/pandadoc/actions/get-document-status/get-document-status.mjs index 60b8ea37b3008..6228c26298bf2 100644 --- a/components/pandadoc/actions/get-document-status/get-document-status.mjs +++ b/components/pandadoc/actions/get-document-status/get-document-status.mjs @@ -6,7 +6,7 @@ export default { description: "Get basic status info about a document. [See documentation here](https://developers.pandadoc.com/reference/document-status)", type: "action", - version: "0.0.2", + version: "0.0.3", props: { app, id: { diff --git a/components/pandadoc/actions/list-contacts/list-contacts.mjs b/components/pandadoc/actions/list-contacts/list-contacts.mjs index c1a01fe59105b..ebcd60a738056 100644 --- a/components/pandadoc/actions/list-contacts/list-contacts.mjs +++ b/components/pandadoc/actions/list-contacts/list-contacts.mjs @@ -3,9 +3,9 @@ import app from "../../pandadoc.app.mjs"; export default { key: "pandadoc-list-contacts", name: "List Contacts", - description: "This method lists all contacts within an account. [See the docs here](https://developers.pandadoc.com/reference/list-contacts)", + description: "List all contacts within an account. [See the documentation here](https://developers.pandadoc.com/reference/list-contacts)", type: "action", - version: "0.0.6", + version: "0.0.7", props: { app, }, diff --git a/components/pandadoc/actions/list-document-attachments/list-document-attachments.mjs b/components/pandadoc/actions/list-document-attachments/list-document-attachments.mjs index 95851f77686c2..455281a76810a 100644 --- a/components/pandadoc/actions/list-document-attachments/list-document-attachments.mjs +++ b/components/pandadoc/actions/list-document-attachments/list-document-attachments.mjs @@ -2,10 +2,10 @@ import app from "../../pandadoc.app.mjs"; export default { key: "pandadoc-list-document-attachments", - name: "List Document Attachment", - description: "Returns a list of attachments associated with a specified document. [See the docs here](https://developers.pandadoc.com/reference/list-attachment)", + name: "List Document Attachments", + description: "Returns a list of attachments associated with a specified document. [See the documentation here](https://developers.pandadoc.com/reference/list-attachment)", type: "action", - version: "0.0.6", + version: "0.0.7", props: { app, documentId: { diff --git a/components/pandadoc/actions/list-documents/list-documents.mjs b/components/pandadoc/actions/list-documents/list-documents.mjs index 98c31359af24e..4462d85e7f90a 100644 --- a/components/pandadoc/actions/list-documents/list-documents.mjs +++ b/components/pandadoc/actions/list-documents/list-documents.mjs @@ -4,9 +4,9 @@ import constants from "../common/constants.mjs"; export default { key: "pandadoc-list-documents", name: "List Documents", - description: "List documents optionally filter by a search query or tags. [See the docs here](https://developers.pandadoc.com/reference/list-documents)", + description: "List documents, optionally filtering by a search query or tags. [See the documentation here](https://developers.pandadoc.com/reference/list-documents)", type: "action", - version: "0.0.6", + version: "0.0.7", props: { app, query: { diff --git a/components/pandadoc/actions/list-folders/list-folders.mjs b/components/pandadoc/actions/list-folders/list-folders.mjs index 739e43d3c365f..3d694024bf41e 100644 --- a/components/pandadoc/actions/list-folders/list-folders.mjs +++ b/components/pandadoc/actions/list-folders/list-folders.mjs @@ -3,9 +3,9 @@ import app from "../../pandadoc.app.mjs"; export default { key: "pandadoc-list-folders", name: "List Folders", - description: "List folders which contain Documents [See the docs here](https://developers.pandadoc.com/reference/list-documents-folders)", + description: "List folders which contain Documents. [See the documentation here](https://developers.pandadoc.com/reference/list-documents-folders)", type: "action", - version: "0.0.5", + version: "0.0.6", props: { app, parentFolderId: { diff --git a/components/pandadoc/actions/send-document/send-document.mjs b/components/pandadoc/actions/send-document/send-document.mjs index 4304211412a69..0931d1d224d29 100644 --- a/components/pandadoc/actions/send-document/send-document.mjs +++ b/components/pandadoc/actions/send-document/send-document.mjs @@ -5,7 +5,7 @@ export default { name: "Send Document", description: "Move a document to sent status and send an optional email. [See the documentation](https://developers.pandadoc.com/reference/send-document)", type: "action", - version: "0.0.6", + version: "0.0.7", props: { app, documentId: { diff --git a/components/pandadoc/common/constants.mjs b/components/pandadoc/common/constants.mjs new file mode 100644 index 0000000000000..2da0e9e81584b --- /dev/null +++ b/components/pandadoc/common/constants.mjs @@ -0,0 +1 @@ +export const DOCUMENT_STATUS_COMPLETED = 2; diff --git a/components/pandadoc/package.json b/components/pandadoc/package.json index cad274c9c86f4..e1acc2088c9ea 100644 --- a/components/pandadoc/package.json +++ b/components/pandadoc/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/pandadoc", - "version": "0.2.6", + "version": "0.3.0", "description": "Pipedream PandaDoc Components", "main": "pandadoc.app.mjs", "keywords": [ diff --git a/components/pandadoc/pandadoc.app.mjs b/components/pandadoc/pandadoc.app.mjs index b77b998cddd44..fd4de74e7b696 100644 --- a/components/pandadoc/pandadoc.app.mjs +++ b/components/pandadoc/pandadoc.app.mjs @@ -1,4 +1,5 @@ import { axios } from "@pipedream/platform"; +import { DOCUMENT_STATUS_COMPLETED } from "./common/constants.mjs"; export default { type: "app", @@ -8,10 +9,32 @@ export default { type: "string", label: "Document Id", description: "Specify document's ID", - async options() { + async options({ page }) { const response = await this.listDocuments({ params: { deleted: false, + page: page || undefined, + }, + }); + return response?.results?.map(({ + id, + name, + }) => ({ + label: name, + value: id, + })) || []; + }, + }, + completedDocumentId: { + type: "string", + label: "Completed Document Id", + description: "Select a completed document or provide its ID", + async options({ page }) { + const response = await this.listDocuments({ + params: { + deleted: false, + status: DOCUMENT_STATUS_COMPLETED, + page: page || undefined, }, }); return response?.results?.map(({ @@ -27,10 +50,11 @@ export default { type: "string", label: "Template Id", description: "Specify template's ID", - async options() { + async options({ page }) { const response = await this.listTemplates({ params: { deleted: false, + page: page || undefined, }, }); return response?.results?.map(({ @@ -47,8 +71,12 @@ export default { label: "Document Folder Id", description: "Specify the document folder ID", optional: true, - async options() { - const response = await this.listDocumentFolders(); + async options({ page }) { + const response = await this.listDocumentFolders({ + params: { + page: page || undefined, + }, + }); return response?.results?.map(({ uuid, name, @@ -78,6 +106,17 @@ export default { if none exists with the given email; or 2. gets the existing contact with the given email that already exists. \n\nE.g. \`{ "email": "john.doe@pipedream.com", "first_name": "John", "last_name": "Doe", "role": "user" }\``, }, + separateFiles: { + type: "boolean", + label: "Separate Files", + description: "Download document bundle as a zip-archive of separate PDFs (1 file per section)", + optional: true, + }, + outputFilename: { + type: "string", + label: "Output Filename", + description: "The filename of the downloaded file in the `tmp` folder.", + }, }, methods: { getUrl(path) { @@ -204,5 +243,23 @@ export default { ...args, }); }, + downloadDocument({ + id, ...args + }) { + return this.makeRequest({ + path: `/documents/${id}/download`, + responseType: "arraybuffer", + ...args, + }); + }, + downloadProtectedDocument({ + id, ...args + }) { + return this.makeRequest({ + path: `/documents/${id}/download-protected`, + responseType: "arraybuffer", + ...args, + }); + }, }, }; diff --git a/components/pandadoc/sources/document-creation-failed/document-creation-failed.mjs b/components/pandadoc/sources/document-creation-failed/document-creation-failed.mjs index b186d73e3b828..e9d67645071a2 100644 --- a/components/pandadoc/sources/document-creation-failed/document-creation-failed.mjs +++ b/components/pandadoc/sources/document-creation-failed/document-creation-failed.mjs @@ -6,9 +6,9 @@ export default { ...common, name: "Document Creation Failed (Instant)", description: - `Emit new event when a document failed to be created [See docs here](${DOCS_LINK})`, + `Emit new event when a document failed to be created. [See the documentation here](${DOCS_LINK})`, key: "pandadoc-document-creation-failed", - version: "0.0.4", + version: "0.0.5", type: "source", methods: { ...common.methods, diff --git a/components/pandadoc/sources/document-deleted/document-deleted.mjs b/components/pandadoc/sources/document-deleted/document-deleted.mjs index 054686645c218..fcb11fc6f16dc 100644 --- a/components/pandadoc/sources/document-deleted/document-deleted.mjs +++ b/components/pandadoc/sources/document-deleted/document-deleted.mjs @@ -6,9 +6,9 @@ export default { ...common, name: "Document Deleted (Instant)", description: - `Emit new event when a document is deleted [See docs here](${DOCS_LINK})`, + `Emit new event when a document is deleted. [See the documentation here](${DOCS_LINK})`, key: "pandadoc-document-deleted", - version: "0.0.4", + version: "0.0.5", type: "source", methods: { ...common.methods, diff --git a/components/pandadoc/sources/document-state-changed/document-state-changed.mjs b/components/pandadoc/sources/document-state-changed/document-state-changed.mjs index 9d861a63775b2..5ca9647514d7b 100644 --- a/components/pandadoc/sources/document-state-changed/document-state-changed.mjs +++ b/components/pandadoc/sources/document-state-changed/document-state-changed.mjs @@ -6,9 +6,9 @@ export default { ...common, name: "Document State Changed (Instant)", description: - `Emit new event when a document's state is changed [See docs here](${DOCS_LINK})`, + `Emit new event when a document's state is changed. [See the documentation here](${DOCS_LINK})`, key: "pandadoc-document-state-changed", - version: "0.0.4", + version: "0.0.5", type: "source", methods: { ...common.methods, diff --git a/components/pandadoc/sources/document-updated/document-updated.mjs b/components/pandadoc/sources/document-updated/document-updated.mjs index f26dd621239ea..480d0c73917ea 100644 --- a/components/pandadoc/sources/document-updated/document-updated.mjs +++ b/components/pandadoc/sources/document-updated/document-updated.mjs @@ -6,9 +6,9 @@ export default { ...common, name: "Document Updated (Instant)", description: - `Emit new event when a document is updated [See docs here](${DOCS_LINK})`, + `Emit new event when a document is updated. [See the documentation here](${DOCS_LINK})`, key: "pandadoc-document-updated", - version: "0.0.4", + version: "0.0.5", type: "source", methods: { ...common.methods, diff --git a/components/pandadoc/sources/recipient-completed/recipient-completed.mjs b/components/pandadoc/sources/recipient-completed/recipient-completed.mjs index 56dedafd36722..522aae963ef62 100644 --- a/components/pandadoc/sources/recipient-completed/recipient-completed.mjs +++ b/components/pandadoc/sources/recipient-completed/recipient-completed.mjs @@ -6,9 +6,9 @@ export default { ...common, name: "Recipient Completed (Instant)", description: - `Emit new event when a recipient completes a document [See docs here](${DOCS_LINK})`, + `Emit new event when a recipient completes a document. [See the documentation here](${DOCS_LINK})`, key: "pandadoc-recipient-completed", - version: "0.0.4", + version: "0.0.5", type: "source", methods: { ...common.methods, diff --git a/components/paperform/.gitignore b/components/paperform/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/paperform/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/paperform/README.md b/components/paperform/README.md index 7021bdc07a9c3..4654263a5d003 100644 --- a/components/paperform/README.md +++ b/components/paperform/README.md @@ -1,22 +1,11 @@ # Overview -PaperForm is a powerful, easy to use form builder that provides powerful tools -to quickly build forms and collect data. The PaperForm API enables developers -to customize the forms they create, set up notifications and triggers, and -access data collected from forms in real time. With the PaperForm API, -developers are able to build robust, efficient web applications to meet their -needs. +The PaperForm API offers a robust means to interact programmatically with PaperForm's features, allowing you to automate form submissions, retrieve form data, and integrate with various other services. With Pipedream's serverless platform, you can construct workflows that respond to events from PaperForm forms—like new submissions—by triggering actions in other apps, or perform operations on the submissions themselves. -Some examples of what can be built with the PaperForm API include: +# Example Use Cases -- Online forms for gathering customer feedback, surveys, and registration -- A secure portal for customers to access and make payments -- A tool for collecting and securely storing applicants’ resumes and interview - documents -- A secure database of contact information for customers and prospects -- Automated reminders for appointments or events -- Integrations to other third-party applications, like Salesforce or Mailchimp -- Secure online booking and scheduling solutions -- Automated thank you messages to customers after form completion -- Customized form validation rules based on specific data sets or user input -- Automated process flows and notifications after form submission +- **Automated Lead Capture and CRM Integration**: When a potential customer submits a form on PaperForm, trigger a workflow that captures the submission data and feeds it directly into a CRM such as Salesforce or HubSpot. This can help ensure immediate follow-up and maintain an organized lead pipeline. + +- **Feedback Collection and Analysis Workflow**: After receiving a new submission from a feedback form, use Pipedream to parse the data, send it to a Google Sheet for aggregation, and then use sentiment analysis tools to gauge customer satisfaction. You could even trigger an automated email response thanking them for their input. + +- **Event Registration and Calendar Synchronization**: For new event registrations via PaperForm, trigger a Pipedream workflow that creates an event in a Google Calendar, sends a personalized confirmation email with event details to the registrant, and posts a message in a Slack channel to notify the team. diff --git a/components/paperform/app/paperform.app.ts b/components/paperform/app/paperform.app.ts deleted file mode 100644 index 6afce6d351aff..0000000000000 --- a/components/paperform/app/paperform.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "paperform", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); diff --git a/components/paperform/package.json b/components/paperform/package.json index a37b1da222c9f..25d4783c2b434 100644 --- a/components/paperform/package.json +++ b/components/paperform/package.json @@ -1,18 +1,18 @@ { "name": "@pipedream/paperform", - "version": "0.0.3", + "version": "0.1.0", "description": "Pipedream PaperForm Components", - "main": "dist/app/paperform.app.mjs", + "main": "paperform.app.mjs", "keywords": [ "pipedream", "paperform" ], - "files": [ - "dist" - ], "homepage": "https://pipedream.com/apps/paperform", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/paperform/paperform.app.mjs b/components/paperform/paperform.app.mjs new file mode 100644 index 0000000000000..fe4ba95739c90 --- /dev/null +++ b/components/paperform/paperform.app.mjs @@ -0,0 +1,88 @@ +import { axios } from "@pipedream/platform"; +const DEFAULT_LIMIT = 100; + +export default { + type: "app", + app: "paperform", + propDefinitions: { + formId: { + type: "string", + label: "Form ID", + description: "The ID of the form to monitor", + async options({ page }) { + const { results: { forms } } = await this.listForms({ + params: { + limit: DEFAULT_LIMIT, + skip: page * DEFAULT_LIMIT, + }, + }); + return forms?.map(({ + id: value, title: label, + }) => ({ + label, + value, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.paperform.co/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + listForms(opts = {}) { + return this._makeRequest({ + path: "/forms", + ...opts, + }); + }, + listSubmissions({ + formId, ...opts + }) { + return this._makeRequest({ + path: `/forms/${formId}/submissions`, + ...opts, + }); + }, + async *paginate({ + fn, args, resourceKey, max, + }) { + args = { + ...args, + params: { + ...args?.params, + limit: DEFAULT_LIMIT, + skip: 0, + }, + }; + let hasMore, count = 0; + do { + const { + results, has_more: more, + } = await fn(args); + const items = results[resourceKey]; + for (const item of items) { + yield item; + if (max && ++count >= max) { + return; + } + } + hasMore = more; + args.params.skip += args.params.limit; + } while (hasMore); + }, + }, +}; diff --git a/components/paperform/sources/new-submission/new-submission.mjs b/components/paperform/sources/new-submission/new-submission.mjs new file mode 100644 index 0000000000000..2ce46137114c6 --- /dev/null +++ b/components/paperform/sources/new-submission/new-submission.mjs @@ -0,0 +1,83 @@ +import paperform from "../../paperform.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + key: "paperform-new-submission", + name: "New Submission", + description: "Emit new event when a new submission is made on the specified form in Paperform. [See the documentation](https://paperform.readme.io/reference/listformsubmissions)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + paperform, + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + db: "$.service.db", + formId: { + propDefinition: [ + paperform, + "formId", + ], + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + generateMeta(submission) { + return { + id: submission.id, + summary: `New Submission ID: ${submission.id}`, + ts: Date.parse(submission.created_at), + }; + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + + const items = this.paperform.paginate({ + fn: this.paperform.listSubmissions, + args: { + formId: this.formId, + }, + resourceKey: "submissions", + max, + }); + + const submissions = []; + for await (const item of items) { + const ts = Date.parse(item.created_at); + if (ts >= lastTs) { + submissions.push(item); + } else { + break; + } + } + + if (!submissions?.length) { + return; + } + + this._setLastTs(Date.parse(submissions[0].created_at)); + + submissions.forEach((submission) => { + const meta = this.generateMeta(submission); + this.$emit(submission, meta); + }); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/papersign/actions/copy-document/copy-document.mjs b/components/papersign/actions/copy-document/copy-document.mjs new file mode 100644 index 0000000000000..19202f76c34a5 --- /dev/null +++ b/components/papersign/actions/copy-document/copy-document.mjs @@ -0,0 +1,65 @@ +import papersign from "../../papersign.app.mjs"; + +export default { + key: "papersign-copy-document", + name: "Copy Document", + description: "Duplicates a given document. [See the documentation](https://paperform.readme.io/reference/papersigncopydocument)", + version: "0.0.1", + type: "action", + props: { + papersign, + documentId: { + propDefinition: [ + papersign, + "documentId", + ], + }, + name: { + type: "string", + label: "Name", + description: "The new name of the copied document", + optional: true, + }, + spaceId: { + propDefinition: [ + papersign, + "spaceId", + ], + optional: true, + }, + path: { + type: "string", + label: "Path", + description: "The path to copy the document to. Maximum depth is 4 levels. Any missing folders will be created.", + optional: true, + }, + folderId: { + propDefinition: [ + papersign, + "folderId", + ], + optional: true, + }, + }, + async run({ $ }) { + const data = {}; + if (this.folderId) { + data.folder_id = this.folderId; + } else { + data.space_id = this.spaceId; + data.path = this.path; + } + + const response = await this.papersign.duplicateDocument({ + $, + documentId: this.documentId, + data: { + ...data, + name: this.name, + }, + }); + + $.export("$summary", `Successfully copied document: ${response.results.document.name}`); + return response; + }, +}; diff --git a/components/papersign/actions/get-document/get-document.mjs b/components/papersign/actions/get-document/get-document.mjs new file mode 100644 index 0000000000000..d566fdd390518 --- /dev/null +++ b/components/papersign/actions/get-document/get-document.mjs @@ -0,0 +1,27 @@ +import papersign from "../../papersign.app.mjs"; + +export default { + key: "papersign-get-document", + name: "Get Document", + description: "Retrieve a document using a specified ID. [See the documentation](https://paperform.readme.io/reference/getpapersigndocument)", + version: "0.0.1", + type: "action", + props: { + papersign, + documentId: { + propDefinition: [ + papersign, + "documentId", + ], + }, + }, + async run({ $ }) { + const response = await this.papersign.getDocument({ + $, + documentId: this.documentId, + }); + + $.export("$summary", `Successfully retrieved document with ID: ${this.documentId}`); + return response; + }, +}; diff --git a/components/papersign/actions/send-document/send-document.mjs b/components/papersign/actions/send-document/send-document.mjs new file mode 100644 index 0000000000000..1847319e6a907 --- /dev/null +++ b/components/papersign/actions/send-document/send-document.mjs @@ -0,0 +1,115 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import papersign from "../../papersign.app.mjs"; + +export default { + key: "papersign-send-document", + name: "Send Document", + description: "Dispatches a document to a specified recipient. [See the documentation](https://paperform.readme.io/reference/papersignsenddocument)", + version: "0.0.1", + type: "action", + props: { + papersign, + documentId: { + propDefinition: [ + papersign, + "documentId", + ], + }, + expiration: { + type: "string", + label: "Expiration", + description: "The expiration date of the document. Must be at least 30 minutes in the future. **Format: YYYY-MM-DDTHH:MM:SS.SSSZ**", + optional: true, + }, + inviteMessage: { + type: "string", + label: "Invite Message", + description: "The message to include in the invitation email, up to 1000 characters.", + optional: true, + }, + fromUserEmail: { + type: "string", + label: "From User Email", + description: "The email address of a User on your team's account to send the document from.", + optional: true, + }, + documentRecipientEmails: { + type: "string[]", + label: "Document Recipient Emails", + description: "An array of recipient emails for the document.", + optional: true, + }, + firstAfterDays: { + type: "integer", + label: "Automatic Reminder - First After Days", + description: "The number of days after the document is sent to send the reminder.", + optional: true, + }, + followUpEveryDays: { + type: "integer", + label: "Automatic Reminder - Follow Up Every Days", + description: "The number of days to wait between reminders.", + optional: true, + }, + signers: { + type: "string[]", + label: "Signers", + description: "An array of objects of signers. **Object format: {\"key\": \"123\",\"name\": \"Jack Smith\",\"email\": \"signer@example.com\",\"phone\": \"123 456 7899\",\"job_title\": \"Account Manager\",\"company\": \"Explosive Startup\",\"custom_attributes\": [{\"key\": \"Relationship\",\"label\": \"Relationship to the company\",\"value\": \"CEO\"}]}**", + optional: true, + }, + variables: { + type: "object", + label: "Variables", + description: "The key: value of the document variables.", + optional: true, + }, + copy: { + type: "boolean", + label: "Copy", + description: "Whether to copy before sending.", + optional: true, + }, + }, + async run({ $ }) { + if ( + (this.firstAfterDays && !this.followUpEveryDays) || + (!this.firstAfterDays && this.followUpEveryDays) + ) { + throw new ConfigurationError("You must fill in the fields 'First After Days' and 'Follow Up Every Days' or none of them"); + } + + const automaticReminders = {}; + if (this.firstAfterDays) { + automaticReminders.first_after_days = this.firstAfterDays; + automaticReminders.follow_up_every_days = this.followUpEveryDays; + } + + const variables = []; + if (this.variables) { + for (const key of Object.keys(parseObject(this.variables))) { + variables.push({ + key, + value: this.variables[key], + }); + } + } + + const response = await this.papersign.sendDocument({ + $, + documentId: this.documentId, + data: { + expiration: this.expiration, + inviteMessage: this.inviteMessage, + fromUserEmail: this.fromUserEmail, + documentRecipientEmails: parseObject(this.documentRecipientEmails), + automaticReminders, + signers: parseObject(this.signers), + variables, + copy: this.copy, + }, + }); + $.export("$summary", `Document sent successfully with ID ${this.documentId}`); + return response; + }, +}; diff --git a/components/papersign/common/constants.mjs b/components/papersign/common/constants.mjs new file mode 100644 index 0000000000000..28a6656d208ad --- /dev/null +++ b/components/papersign/common/constants.mjs @@ -0,0 +1,12 @@ +export const LIMIT = 100; + +export const SCOPE_OPTIONS = [ + { + label: "Direct Children", + value: "folder.direct_children", + }, + { + label: "All Descendants", + value: "folder.all_descendants", + }, +]; diff --git a/components/papersign/common/utils.mjs b/components/papersign/common/utils.mjs new file mode 100644 index 0000000000000..9050833bcda0b --- /dev/null +++ b/components/papersign/common/utils.mjs @@ -0,0 +1,32 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return parseObject(item); + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + if (typeof obj === "object") { + for (const [ + key, + value, + ] of Object.entries(obj)) { + obj[key] = parseObject(value); + } + } + return obj; +}; diff --git a/components/papersign/package.json b/components/papersign/package.json new file mode 100644 index 0000000000000..656e518ce4d24 --- /dev/null +++ b/components/papersign/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/papersign", + "version": "0.1.0", + "description": "Pipedream Papersign Components", + "main": "papersign.app.mjs", + "keywords": [ + "pipedream", + "papersign" + ], + "homepage": "https://pipedream.com/apps/papersign", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/papersign/papersign.app.mjs b/components/papersign/papersign.app.mjs new file mode 100644 index 0000000000000..2f3e59c9ec6f2 --- /dev/null +++ b/components/papersign/papersign.app.mjs @@ -0,0 +1,122 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "papersign", + propDefinitions: { + documentId: { + type: "string", + label: "Document ID", + description: "Enter the ID of the Papersign document", + async options({ page }) { + return await this.list({ + module: "documents", + page, + }); + }, + }, + spaceId: { + type: "string", + label: "Space ID", + description: "Enter the ID of the Papersign space", + async options({ page }) { + return await this.list({ + module: "spaces", + page, + }); + }, + }, + folderId: { + type: "string", + label: "Folder ID", + description: "Enter the ID of the Papersign folder. `If folder id is present, Space Id and Path will be ignored`.", + async options({ page }) { + return await this.list({ + module: "folders", + page, + }); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.paperform.co/v1/papersign"; + }, + _headers() { + return { + "Authorization": `Bearer ${this.$auth.access_token}`, + "accept": "application/json", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + async list({ + module, page, ...opts + }) { + const { results } = await this._makeRequest({ + path: `/${module}`, + params: { + limit: LIMIT, + skip: LIMIT * page, + }, + ...opts, + }); + + return results[module].map(({ + id: value, name: label, + }) => ({ + value, + label, + })); + }, + duplicateDocument({ + documentId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/documents/${documentId}/copy`, + ...opts, + }); + }, + getDocument({ + documentId, ...opts + }) { + return this._makeRequest({ + path: `/documents/${documentId}`, + ...opts, + }); + }, + sendDocument({ + documentId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/documents/${documentId}/send`, + ...opts, + }); + }, + createWebhook({ + folderId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/folders/${folderId}/webhooks`, + ...opts, + }); + }, + deleteWebhook(hookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${hookId}`, + }); + }, + }, +}; diff --git a/components/papersign/sources/common/base.mjs b/components/papersign/sources/common/base.mjs new file mode 100644 index 0000000000000..3fcd824eb6ed5 --- /dev/null +++ b/components/papersign/sources/common/base.mjs @@ -0,0 +1,64 @@ +import { SCOPE_OPTIONS } from "../../common/constants.mjs"; +import papersign from "../../papersign.app.mjs"; + +export default { + props: { + papersign, + http: { + type: "$.interface.http", + customResponse: false, + }, + db: "$.service.db", + folderId: { + propDefinition: [ + papersign, + "folderId", + ], + }, + name: { + type: "string", + label: "Name", + description: "The name of the webhook.", + optional: true, + }, + scope: { + type: "string", + label: "Scope", + description: "The scope of the webhook", + options: SCOPE_OPTIONS, + }, + }, + methods: { + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + _getHookId() { + return this.db.get("hookId"); + }, + }, + hooks: { + async activate() { + const { results: { webhook } } = await this.papersign.createWebhook({ + folderId: this.folderId, + data: { + name: this.name, + target_url: this.http.endpoint, + scope: this.scope, + triggers: this.getTriggers(), + }, + }); + this._setHookId(webhook.id); + }, + async deactivate() { + const webhookId = this._getHookId(); + await this.papersign.deleteWebhook(webhookId); + }, + }, + async run({ body }) { + this.$emit(body, { + id: body.id, + summary: this.getSummary(body), + ts: Date.parse(body.created_at), + }); + }, +}; diff --git a/components/papersign/sources/new-document-completed-instant/new-document-completed-instant.mjs b/components/papersign/sources/new-document-completed-instant/new-document-completed-instant.mjs new file mode 100644 index 0000000000000..40175d61ff4f1 --- /dev/null +++ b/components/papersign/sources/new-document-completed-instant/new-document-completed-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "papersign-new-document-completed-instant", + name: "New Document Completed (Instant)", + description: "Emit new event when a document is completed, i.e., all signers have signed.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTriggers() { + return [ + "document.completed", + ]; + }, + getSummary(body) { + return `Document '${body.document_name}' completed`; + }, + }, + sampleEmit, +}; diff --git a/components/papersign/sources/new-document-completed-instant/test-event.mjs b/components/papersign/sources/new-document-completed-instant/test-event.mjs new file mode 100644 index 0000000000000..568316a81e779 --- /dev/null +++ b/components/papersign/sources/new-document-completed-instant/test-event.mjs @@ -0,0 +1,10 @@ +export default { + "id": "671ba9030af55bff3b7a8c00", + "document_id": "671ba9030af55bff3b7a8c00", + "document_name": "Document Name", + "type": "document.completed", + "data": { + "document_url": "https://sign.papersign.com/sign/671ba9030af55bff3b7a8c00/completed-document?expires=1737659429&signature=671ba9030af55bff3b7a8c00671ba9030af55bff3b7a8c00" + }, + "created_at": "2024-10-25T19:10:29+00:00" +} \ No newline at end of file diff --git a/components/papersign/sources/new-event-instant/new-event-instant.mjs b/components/papersign/sources/new-event-instant/new-event-instant.mjs new file mode 100644 index 0000000000000..229f94dbabcf7 --- /dev/null +++ b/components/papersign/sources/new-event-instant/new-event-instant.mjs @@ -0,0 +1,33 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "papersign-new-event-instant", + name: "New Event in Papersign (Instant)", + description: "Emit new event when any document or signer action occurs.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTriggers() { + return [ + "document.sent", + "document.completed", + "document.cancelled", + "document.rejected", + "document.expired", + "signer.notified", + "signer.viewed", + "signer.consent_accepted", + "signer.nominated", + "signer.signed", + ]; + }, + getSummary({ data }) { + return `New event: ${data.type}`; + }, + }, + sampleEmit, +}; diff --git a/components/papersign/sources/new-event-instant/test-event.mjs b/components/papersign/sources/new-event-instant/test-event.mjs new file mode 100644 index 0000000000000..568316a81e779 --- /dev/null +++ b/components/papersign/sources/new-event-instant/test-event.mjs @@ -0,0 +1,10 @@ +export default { + "id": "671ba9030af55bff3b7a8c00", + "document_id": "671ba9030af55bff3b7a8c00", + "document_name": "Document Name", + "type": "document.completed", + "data": { + "document_url": "https://sign.papersign.com/sign/671ba9030af55bff3b7a8c00/completed-document?expires=1737659429&signature=671ba9030af55bff3b7a8c00671ba9030af55bff3b7a8c00" + }, + "created_at": "2024-10-25T19:10:29+00:00" +} \ No newline at end of file diff --git a/components/papersign/sources/new-signer-signed-instant/new-signer-signed-instant.mjs b/components/papersign/sources/new-signer-signed-instant/new-signer-signed-instant.mjs new file mode 100644 index 0000000000000..16b08fa730384 --- /dev/null +++ b/components/papersign/sources/new-signer-signed-instant/new-signer-signed-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "papersign-new-signer-signed-instant", + name: "New Signer Signed (Instant)", + description: "Emit new event when a signer signs a document.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTriggers() { + return [ + "signer.signed", + ]; + }, + getSummary({ data }) { + return `Document signed by signer ${data.by.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/papersign/sources/new-signer-signed-instant/test-event.mjs b/components/papersign/sources/new-signer-signed-instant/test-event.mjs new file mode 100644 index 0000000000000..912a48198cf59 --- /dev/null +++ b/components/papersign/sources/new-signer-signed-instant/test-event.mjs @@ -0,0 +1,7 @@ +export default { + "project": "project_slug", + "translated": 100, + "resource": "resource_slug", + "event": "translation_completed", + "language": "lang_code" +} \ No newline at end of file diff --git a/components/papertrail/README.md b/components/papertrail/README.md new file mode 100644 index 0000000000000..2228b2319b045 --- /dev/null +++ b/components/papertrail/README.md @@ -0,0 +1,11 @@ +# Overview + +The Papertrail API provides access to its log management and analysis service, enabling you to programmatically query logs, retrieve system events, and manage log destinations. Integrating Papertrail with Pipedream allows you to automate reactions to log events, analyze log data, and connect log insights with other services for notifications, data storage, or further processing. By leveraging Papertrail's API, you can craft tailored serverless workflows on Pipedream that respond in real-time to your logging needs. + +# Example Use Cases + +- **Real-time Alerting on Specific Log Events**: Trigger a workflow on Pipedream when a particular log event is detected in Papertrail. The workflow can filter log messages based on severity or content and send instant alerts to communication platforms like Slack, email, or SMS for immediate action. + +- **Automated Log Archiving**: Set up a scheduled workflow on Pipedream to periodically fetch logs from Papertrail and archive them to cloud storage services like Amazon S3 or Google Cloud Storage. This helps in maintaining compliance and ensures long-term storage of logs for auditing or historical analysis. + +- **Log Analysis and Visualization**: Collect logs via Papertrail's API and feed them into a data processing workflow on Pipedream. Perform custom analysis, and then push the results to visualization tools like Google Sheets or data platforms like Snowflake, enabling you to monitor patterns or anomalies within your system's activities. diff --git a/components/papertrail/package.json b/components/papertrail/package.json index 62fa5769d2891..6ffcdfebbd340 100644 --- a/components/papertrail/package.json +++ b/components/papertrail/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/papyrs/README.md b/components/papyrs/README.md new file mode 100644 index 0000000000000..9943a353fb7f5 --- /dev/null +++ b/components/papyrs/README.md @@ -0,0 +1,11 @@ +# Overview + +The Papyrs API facilitates the creation and manipulation of rich text documents in your Papyrs intranet. With it, you can automate the handling of text-documents, forms, and pages within your organization's Papyrs account. When combined with Pipedream's capacity to connect to various APIs, the Papyrs API can be a powerful tool for document-driven workflow automation. You can trigger actions in Papyrs based on events from other apps, process Papyrs data for insights, or synchronize Papyrs content with external systems, databases, or file storage solutions. + +# Example Use Cases + +- **Automated Employee Onboarding Documents**: Create an onboarding page in Papyrs for each new employee automatically when they are added to your HR management system (e.g., BambooHR). Store necessary paperwork and guides relevant to their role, and share them with the new hire. + +- **Customer Feedback Compilation**: Collect customer feedback via a form on your website. When a new submission is received, use the Papyrs API to add this feedback to a centralized Papyrs document, making it easy for your team to review and act on. + +- **Meeting Notes Archival**: After a meeting ends in your calendar app (e.g., Google Calendar), trigger a Pipedream workflow to create a new page in Papyrs with the meeting's name and date, then append transcribed notes or summaries for future reference. diff --git a/components/paradym/actions/common/create-issuance-offer.mjs b/components/paradym/actions/common/create-issuance-offer.mjs new file mode 100644 index 0000000000000..7d24cb747b10c --- /dev/null +++ b/components/paradym/actions/common/create-issuance-offer.mjs @@ -0,0 +1,107 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../paradym.app.mjs"; + +export default { + props: { + app, + projectId: { + propDefinition: [ + app, + "projectId", + ], + }, + credentialTemplateId: { + reloadProps: true, + propDefinition: [ + app, + "sdJWTVCCredentialTemplateId", + ({ projectId }) => ({ + projectId, + }), + ], + }, + }, + async additionalProps() { + const { + projectId, + credentialTemplateId, + getCredentialTemplateFunction, + getDynamicProps, + } = this; + + const getCredentialTemplate = getCredentialTemplateFunction(); + + const credentialTemplate = + credentialTemplateId && await getCredentialTemplate({ + projectId, + credentialTemplateId, + }); + + return Object.entries(credentialTemplate?.attributes) + .reduce((acc, [ + key, + attr, + ]) => getDynamicProps({ + acc, + key, + attr, + }), {}); + }, + methods: { + getCredentialTemplateFunction() { + throw new ConfigurationError("getCredentialTemplateFunction is not implemented."); + }, + getSummary() { + throw new ConfigurationError("getSummary is not implemented."); + }, + getCreateIssuanceOfferFunction() { + throw new ConfigurationError("getCreateIssuanceOfferFunction is not implemented."); + }, + getCreateIssuanceOfferFunctionArgs() { + throw new ConfigurationError("getCreateIssuanceOfferFunctionArgs is not implemented."); + }, + getDynamicProps() { + throw new ConfigurationError("getDynamicProps is not implemented."); + }, + getCredential(propSuffix) { + const { + credentialTemplateId, + ...attrs + } = this; + + const attributes = Object.entries(attrs) + .filter(([ + key, + ]) => key.startsWith(propSuffix)) + .reduce((acc, [ + key, + value, + ]) => ({ + ...acc, + [key.replace(propSuffix, "")]: value, + }), {}); + + return { + credentialTemplateId, + attributes, + }; + }, + }, + async run({ $ }) { + const { + getSummary, + getCreateIssuanceOfferFunction, + getCreateIssuanceOfferFunctionArgs, + } = this; + const createIssuanceOfferFunction = getCreateIssuanceOfferFunction(); + + const response = await createIssuanceOfferFunction({ + $, + ...getCreateIssuanceOfferFunctionArgs(), + }); + + $.export("$summary", getSummary()); + + return response; + }, +}; diff --git a/components/paradym/actions/create-didcomm-issuance-offer/create-didcomm-issuance-offer.mjs b/components/paradym/actions/create-didcomm-issuance-offer/create-didcomm-issuance-offer.mjs new file mode 100644 index 0000000000000..581b1c8a66537 --- /dev/null +++ b/components/paradym/actions/create-didcomm-issuance-offer/create-didcomm-issuance-offer.mjs @@ -0,0 +1,69 @@ +import constants from "../../common/constants.mjs"; +import common from "../common/create-issuance-offer.mjs"; + +export default { + ...common, + key: "paradym-create-didcomm-issuance-offer", + name: "Create DIDComm Issuance Offer", + description: "Create a DIDComm issuance offer for the selected credentials. [See the documentation](https://paradym.id/reference?full#tag/didcomm-issuance/post/v1/projects/{projectId}/didcomm/issuance/offer)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + credentialTemplateId: { + reloadProps: true, + propDefinition: [ + common.props.app, + "anoncredsCredentialTemplateId", + ({ projectId }) => ({ + projectId, + }), + ], + }, + }, + methods: { + ...common.methods, + createDIDCommCredentialOffer({ + projectId, ...args + } = {}) { + return this.app.post({ + path: `/projects/${projectId}/didcomm/issuance/offer`, + ...args, + }); + }, + getSummary() { + return "Successfully created DIDComm credential offer."; + }, + getCreateIssuanceOfferFunction() { + return this.createDIDCommCredentialOffer; + }, + getCredentialTemplateFunction() { + return this.app.getAnonCredsCredentialTemplate; + }, + getCreateIssuanceOfferFunctionArgs() { + const { + projectId, + getCredential, + } = this; + return { + projectId, + data: { + credential: getCredential(constants.TEMPLATE_SUFFIX.ANONYMOUS), + }, + }; + }, + getDynamicProps({ + acc, key, attr, + }) { + return { + ...acc, + [`${constants.TEMPLATE_SUFFIX.ANONYMOUS}${key}`]: { + type: constants.PROP_TYPE[attr.type], + label: `Anonymous Attribute (${attr.name})`, + description: `The ${key} attribute of the Anonymous credential template.`, + optional: !attr.required, + }, + }; + }, + }, +}; diff --git a/components/paradym/actions/create-openid4vc-credential-offer/create-openid4vc-credential-offer.mjs b/components/paradym/actions/create-openid4vc-credential-offer/create-openid4vc-credential-offer.mjs new file mode 100644 index 0000000000000..5d1248a146d60 --- /dev/null +++ b/components/paradym/actions/create-openid4vc-credential-offer/create-openid4vc-credential-offer.mjs @@ -0,0 +1,58 @@ +import constants from "../../common/constants.mjs"; +import common from "../common/create-issuance-offer.mjs"; + +export default { + ...common, + key: "paradym-create-openid4vc-credential-offer", + name: "Create OpenID4VC Credential Offer", + description: "Create a OpenID4VC issuance offer for the selected credentials. [See the documentation](https://paradym.id/reference?full#tag/openid4vc-issuance/post/v1/projects/{projectId}/openid4vc/issuance/offer)", + version: "0.0.1", + type: "action", + methods: { + ...common.methods, + createOpenID4VCCredentialOffer({ + projectId, ...args + } = {}) { + return this.app.post({ + path: `/projects/${projectId}/openid4vc/issuance/offer`, + ...args, + }); + }, + getSummary() { + return "Successfully created OpenID4VC credential offer."; + }, + getCredentialTemplateFunction() { + return this.app.getSDJWTVCCredentialTemplate; + }, + getCreateIssuanceOfferFunction() { + return this.createOpenID4VCCredentialOffer; + }, + getCreateIssuanceOfferFunctionArgs() { + const { + projectId, + getCredential, + } = this; + return { + projectId, + data: { + credentials: [ + getCredential(constants.TEMPLATE_SUFFIX.SDJWTVC), + ].filter(Boolean), + }, + }; + }, + getDynamicProps({ + acc, key, attr, + }) { + return { + ...acc, + [`${constants.TEMPLATE_SUFFIX.SDJWTVC}${key}`]: { + type: constants.PROP_TYPE[attr.type], + label: `SD-JWT-VC Attribute (${attr.name})`, + description: `The ${key} attribute of the SD-JWT-VC credential template.`, + optional: !attr.required, + }, + }; + }, + }, +}; diff --git a/components/paradym/actions/create-openid4vc-verification-request/create-openid4vc-verification-request.mjs b/components/paradym/actions/create-openid4vc-verification-request/create-openid4vc-verification-request.mjs new file mode 100644 index 0000000000000..fe44f8744e27f --- /dev/null +++ b/components/paradym/actions/create-openid4vc-verification-request/create-openid4vc-verification-request.mjs @@ -0,0 +1,54 @@ +import app from "../../paradym.app.mjs"; + +export default { + key: "paradym-create-openid4vc-verification-request", + name: "Create OpenID4VC Verification Request", + description: "Initiates a verification request based on the selected template. [See the documentation](https://paradym.id/reference?full#tag/openid4vc-verification/post/v1/projects/{projectId}/openid4vc/verification/request)", + version: "0.0.1", + type: "action", + props: { + app, + projectId: { + propDefinition: [ + app, + "projectId", + ], + }, + presentationTemplateId: { + propDefinition: [ + app, + "presentationTemplateId", + ({ projectId }) => ({ + projectId, + }), + ], + }, + }, + methods: { + createOpenID4VCVerificationRequest({ + projectId, ...args + } = {}) { + return this.app.post({ + path: `/projects/${projectId}/openid4vc/verification/request`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + createOpenID4VCVerificationRequest, + projectId, + presentationTemplateId, + } = this; + + const response = await createOpenID4VCVerificationRequest({ + $, + projectId, + data: { + presentationTemplateId, + }, + }); + $.export("$summary", `Successfully created OpenID4VC verification request with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/paradym/actions/get-openid4vc-verification-session/get-openid4vc-verification-session.mjs b/components/paradym/actions/get-openid4vc-verification-session/get-openid4vc-verification-session.mjs new file mode 100644 index 0000000000000..650e66fa06bb5 --- /dev/null +++ b/components/paradym/actions/get-openid4vc-verification-session/get-openid4vc-verification-session.mjs @@ -0,0 +1,54 @@ +import app from "../../paradym.app.mjs"; + +export default { + key: "paradym-get-openid4vc-verification-session", + name: "Get OpenID4VC Verification Session", + description: "Fetches the verification session data for the specified session ID. [See the documentation](https://paradym.id/reference?full#tag/openid4vc-verification/get/v1/projects/{projectId}/openid4vc/verification/{openId4VcVerificationId})", + version: "0.0.1", + type: "action", + props: { + app, + projectId: { + propDefinition: [ + app, + "projectId", + ], + }, + openId4VcVerificationId: { + propDefinition: [ + app, + "openId4VcVerificationId", + ({ projectId }) => ({ + projectId, + }), + ], + }, + }, + methods: { + getOpenId4VcVerificationSessionUrl({ + projectId, openId4VcVerificationId, ...args + } = {}) { + return this.app._makeRequest({ + path: `/projects/${projectId}/openid4vc/verification/${openId4VcVerificationId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + getOpenId4VcVerificationSessionUrl, + projectId, + openId4VcVerificationId, + } = this; + + const response = await getOpenId4VcVerificationSessionUrl({ + $, + projectId, + openId4VcVerificationId, + }); + + $.export("$summary", "Successfully fetched the verification session data for the specified session ID."); + + return response; + }, +}; diff --git a/components/paradym/common/constants.mjs b/components/paradym/common/constants.mjs new file mode 100644 index 0000000000000..25a217e73c8b9 --- /dev/null +++ b/components/paradym/common/constants.mjs @@ -0,0 +1,29 @@ +const BASE_URL = "https://api.paradym.id"; +const VERSION_PATH = "/v1"; +const LAST_CREATED_AT = "lastCreatedAt"; +const DEFAULT_LIMIT = 20; +const WEBHOOK_ID = "webhookId"; +const SIGNATURE_SECRET = "signatureSecret"; + +const PROP_TYPE = { + string: "string", + number: "string", + boolean: "boolean", + date: "string", +}; + +const TEMPLATE_SUFFIX = { + SDJWTVC: "sdJWTVCCredentialTemplate-", + ANONYMOUS: "anoncredsCredentialTemplate-", +}; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_LIMIT, + WEBHOOK_ID, + SIGNATURE_SECRET, + LAST_CREATED_AT, + PROP_TYPE, + TEMPLATE_SUFFIX, +}; diff --git a/components/paradym/package.json b/components/paradym/package.json new file mode 100644 index 0000000000000..218253e67b8db --- /dev/null +++ b/components/paradym/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/paradym", + "version": "0.1.0", + "description": "Pipedream Paradym Components", + "main": "paradym.app.mjs", + "keywords": [ + "pipedream", + "paradym" + ], + "homepage": "https://pipedream.com/apps/paradym", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0", + "crypto": "^1.0.1" + } +} diff --git a/components/paradym/paradym.app.mjs b/components/paradym/paradym.app.mjs new file mode 100644 index 0000000000000..f186c62d93f71 --- /dev/null +++ b/components/paradym/paradym.app.mjs @@ -0,0 +1,265 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "paradym", + propDefinitions: { + projectId: { + type: "string", + label: "Project ID", + description: "The ID of the project.", + async options({ prevContext: { pageBeforeId } }) { + if (pageBeforeId === null) { + return []; + } + const { data } = await this.listProjects({ + params: { + "sort": "-updatedAt", + "page[size]": constants.DEFAULT_LIMIT, + "page[before]": pageBeforeId, + }, + }); + const options = data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + return { + options, + context: { + pageBeforeId: + data.length + ? data[data.length - 1].id + : null, + }, + }; + }, + }, + sdJWTVCCredentialTemplateId: { + type: "string", + label: "SD-JWT-VC Credential Template ID", + description: "The ID of the SD-JWT-VC credential template.", + async options({ + projectId, + prevContext: { pageBeforeId }, + }) { + if (pageBeforeId === null) { + return []; + } + const { data } = await this.listSDJWTVCCredentialTemplates({ + projectId, + params: { + "sort": "-updatedAt", + "page[size]": constants.DEFAULT_LIMIT, + "page[before]": pageBeforeId, + }, + }); + const options = data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + return { + options, + context: { + pageBeforeId: + data.length + ? data[data.length - 1].id + : null, + }, + }; + }, + }, + anoncredsCredentialTemplateId: { + type: "string", + label: "Anoncreds Credential Template ID", + description: "The ID of the anoncreds credential template.", + async options({ + projectId, + prevContext: { pageBeforeId }, + }) { + if (pageBeforeId === null) { + return []; + } + const { data } = await this.listAnonCredsCredentialTemplates({ + projectId, + params: { + "sort": "-updatedAt", + "page[size]": constants.DEFAULT_LIMIT, + "page[before]": pageBeforeId, + }, + }); + const options = data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + return { + options, + context: { + pageBeforeId: + data.length + ? data[data.length - 1].id + : null, + }, + }; + }, + }, + presentationTemplateId: { + type: "string", + label: "Presentation Template ID", + description: "The ID of the presentation template.", + async options({ + projectId, + prevContext: { pageBeforeId }, + }) { + if (pageBeforeId === null) { + return []; + } + const { data } = await this.listPresentationTemplates({ + projectId, + params: { + "sort": "-updatedAt", + "page[size]": constants.DEFAULT_LIMIT, + "page[before]": pageBeforeId, + }, + }); + const options = data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + return { + options, + context: { + pageBeforeId: + data.length + ? data[data.length - 1].id + : null, + }, + }; + }, + }, + openId4VcVerificationId: { + type: "string", + label: "OpenID4VC Verification ID", + description: "The ID of the OpenID4VC verification.", + async options({ + projectId, + prevContext: { pageBeforeId }, + }) { + if (pageBeforeId === null) { + return []; + } + const { data } = await this.listOpenId4VcVerifications({ + projectId, + params: { + "sort": "-updatedAt", + "page[size]": constants.DEFAULT_LIMIT, + "page[before]": pageBeforeId, + }, + }); + return { + options: data.map(({ id }) => id), + context: { + pageBeforeId: + data.length + ? data[data.length - 1].id + : null, + }, + }; + }, + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "X-Access-Token": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + listProjects(args = {}) { + return this._makeRequest({ + path: "/projects", + ...args, + }); + }, + listSDJWTVCCredentialTemplates({ + projectId, ...args + } = {}) { + return this._makeRequest({ + path: `/projects/${projectId}/templates/credentials/sd-jwt-vc`, + ...args, + }); + }, + listAnonCredsCredentialTemplates({ + projectId, ...args + } = {}) { + return this._makeRequest({ + path: `/projects/${projectId}/templates/credentials/anoncreds`, + ...args, + }); + }, + getSDJWTVCCredentialTemplate({ + projectId, credentialTemplateId, ...args + } = {}) { + return this._makeRequest({ + path: `/projects/${projectId}/templates/credentials/sd-jwt-vc/${credentialTemplateId}`, + ...args, + }); + }, + getAnonCredsCredentialTemplate({ + projectId, credentialTemplateId, ...args + } = {}) { + return this._makeRequest({ + path: `/projects/${projectId}/templates/credentials/anoncreds/${credentialTemplateId}`, + ...args, + }); + }, + listPresentationTemplates({ + projectId, ...args + } = {}) { + return this._makeRequest({ + path: `/projects/${projectId}/templates/presentations`, + ...args, + }); + }, + listOpenId4VcVerifications({ + projectId, ...args + } = {}) { + return this._makeRequest({ + path: `/projects/${projectId}/openid4vc/verification`, + ...args, + }); + }, + }, +}; diff --git a/components/paradym/sources/common/events.mjs b/components/paradym/sources/common/events.mjs new file mode 100644 index 0000000000000..dbaecff31f78a --- /dev/null +++ b/components/paradym/sources/common/events.mjs @@ -0,0 +1,25 @@ +export default { + WORKFLOWEXECUTION_STARTED: "workflowExecution.started", + WORKFLOWEXECUTION_COMPLETED: "workflowExecution.completed", + WORKFLOWEXECUTION_WAITINGFORTRIGGER: "workflowExecution.waitingForTrigger", + WORKFLOWEXECUTION_ACTIONEXECUTED: "workflowExecution.actionExecuted", + WORKFLOWEXECUTION_CANCELED: "workflowExecution.canceled", + WORKFLOWEXECUTION_FAILED: "workflowExecution.failed", + OPENID4VC_ISSUANCE_OFFERED: "openid4vc.issuance.offered", + OPENID4VC_ISSUANCE_COMPLETED: "openid4vc.issuance.completed", + OPENID4VC_ISSUANCE_FAILED: "openid4vc.issuance.failed", + OPENID4VC_ISSUANCE_PARTIALLYISSUED: "openid4vc.issuance.partiallyIssued", + OPENID4VC_VERIFICATION_FAILED: "openid4vc.verification.failed", + OPENID4VC_VERIFICATION_REQUESTED: "openid4vc.verification.requested", + OPENID4VC_VERIFICATION_VERIFIED: "openid4vc.verification.verified", + DIDCOMM_CONNECTION_CREATED: "didcomm.connection.created", + DIDCOMM_CONNECTION_REUSED: "didcomm.connection.reused", + DIDCOMM_MESSAGING_BASIC_RECEIVED: "didcomm.messaging.basic.received", + DIDCOMM_MESSAGING_CUSTOM_RECEIVED: "didcomm.messaging.custom.received", + DIDCOMM_ISSUANCE_OFFERED: "didcomm.issuance.offered", + DIDCOMM_ISSUANCE_COMPLETED: "didcomm.issuance.completed", + DIDCOMM_ISSUANCE_FAILED: "didcomm.issuance.failed", + DIDCOMM_VERIFICATION_FAILED: "didcomm.verification.failed", + DIDCOMM_VERIFICATION_REQUESTED: "didcomm.verification.requested", + DIDCOMM_VERIFICATION_VERIFIED: "didcomm.verification.verified", +}; diff --git a/components/paradym/sources/common/webhook.mjs b/components/paradym/sources/common/webhook.mjs new file mode 100644 index 0000000000000..08898b5f776fe --- /dev/null +++ b/components/paradym/sources/common/webhook.mjs @@ -0,0 +1,139 @@ +import { createHmac } from "crypto"; +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../paradym.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + projectId: { + propDefinition: [ + app, + "projectId", + ], + }, + name: { + type: "string", + label: "Name", + description: "The name of the webhook.", + }, + }, + hooks: { + async activate() { + const { + http: { endpoint: url }, + setWebhookId, + setSignatureSecret, + createWebhook, + projectId, + name, + getEventTypes, + } = this; + const response = + await createWebhook({ + projectId, + data: { + name, + url, + eventTypes: getEventTypes(), + }, + }); + + setWebhookId(response.id); + setSignatureSecret(response.signatureSecret); + }, + async deactivate() { + const { + getWebhookId, + deleteWebhook, + projectId, + } = this; + const webhookId = getWebhookId(); + if (webhookId) { + await deleteWebhook({ + projectId, + webhookId, + }); + } + }, + }, + methods: { + setSignatureSecret(value) { + this.db.set(constants.SIGNATURE_SECRET, value); + }, + getSignatureSecret() { + return this.db.get(constants.SIGNATURE_SECRET); + }, + setWebhookId(value) { + this.db.set(constants.WEBHOOK_ID, value); + }, + getWebhookId() { + return this.db.get(constants.WEBHOOK_ID); + }, + getEventTypes() { + throw new ConfigurationError("getEventTypes is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + createWebhook({ + projectId, ...args + } = {}) { + return this.app.post({ + debug: true, + path: `/projects/${projectId}/webhooks`, + ...args, + }); + }, + deleteWebhook({ + projectId, webhookId, ...args + } = {}) { + return this.app.delete({ + debug: true, + path: `/projects/${projectId}/webhooks/${webhookId}`, + ...args, + }); + }, + processResource(resource) { + this.$emit(resource, this.generateMeta(resource)); + }, + verifySignature(bodyRaw, signature) { + const signatureSecret = this.getSignatureSecret(); + const hash = createHmac("sha256", signatureSecret) + .update(bodyRaw) + .digest("hex"); + return hash === signature; + }, + }, + async run({ + body, headers: { ["x-paradym-hmac-sha-256"]: signature }, bodyRaw, + }) { + const { + http, + verifySignature, + processResource, + } = this; + + const validSignature = verifySignature(bodyRaw, signature); + + if (!validSignature) { + return http.respond({ + status: 401, + }); + } + + http.respond({ + status: 200, + }); + + processResource(body); + }, +}; diff --git a/components/paradym/sources/new-verification-response-instant/new-verification-response-instant.mjs b/components/paradym/sources/new-verification-response-instant/new-verification-response-instant.mjs new file mode 100644 index 0000000000000..79609389e1d99 --- /dev/null +++ b/components/paradym/sources/new-verification-response-instant/new-verification-response-instant.mjs @@ -0,0 +1,31 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "paradym-new-verification-response-instant", + name: "New Verification Response (Instant)", + description: "Emit new event when a verification results in a confirmed response from the user.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventTypes() { + return [ + events.OPENID4VC_VERIFICATION_VERIFIED, + events.DIDCOMM_VERIFICATION_VERIFIED, + ]; + }, + generateMeta(resource) { + const ts = Date.parse(resource.webhookPublishedAt); + return { + id: ts, + summary: "New Verification Response", + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/paradym/sources/new-verification-response-instant/test-event.mjs b/components/paradym/sources/new-verification-response-instant/test-event.mjs new file mode 100644 index 0000000000000..01b70c1735315 --- /dev/null +++ b/components/paradym/sources/new-verification-response-instant/test-event.mjs @@ -0,0 +1,9 @@ +export default { + "webhookId": "879aba63-6a15-4630-b1f1-18e1780eaeb1", + "webhookPublishedAt": "2024-08-09T17:03:10.725Z", + "projectId": "clz947pji000301s66fz38671", + "eventType": "openid4vc.verification.verified", + "payload": { + "openId4VcVerificationId": "clzmygrdy00nobmwgpzbpn2ea" + } +}; diff --git a/components/parallel/package.json b/components/parallel/package.json new file mode 100644 index 0000000000000..cbadee371a9f6 --- /dev/null +++ b/components/parallel/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/parallel", + "version": "0.0.1", + "description": "Pipedream Parallel Components", + "main": "parallel.app.mjs", + "keywords": [ + "pipedream", + "parallel" + ], + "homepage": "https://pipedream.com/apps/parallel", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/parallel/parallel.app.mjs b/components/parallel/parallel.app.mjs new file mode 100644 index 0000000000000..ddcc94b46e628 --- /dev/null +++ b/components/parallel/parallel.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "parallel", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/parma/actions/create-note/create-note.mjs b/components/parma/actions/create-note/create-note.mjs new file mode 100644 index 0000000000000..50ec799c03391 --- /dev/null +++ b/components/parma/actions/create-note/create-note.mjs @@ -0,0 +1,42 @@ +import parma from "../../parma.app.mjs"; + +export default { + key: "parma-create-note", + name: "Create Note", + description: "Adds a new note in Parma. [See the documentation](https://developers.parma.ai/api-docs/index.html)", + version: "0.0.1", + type: "action", + props: { + parma, + relationshipId: { + propDefinition: [ + parma, + "relationshipId", + ], + }, + body: { + type: "string", + label: "Body", + description: "The body of the note.", + }, + datetime: { + type: "string", + label: "Date Time", + description: "The datetime of the note in ISO-8601 format. Example: `2024-06-19T23:26:32.693Z`", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.parma.addNote({ + $, + data: { + relationship_ids: this.relationshipId, + body: this.body, + datetime: this.datetime, + }, + }); + + $.export("$summary", `Successfully created note with Id: ${response.data.id}`); + return response; + }, +}; diff --git a/components/parma/actions/create-relationship/create-relationship.mjs b/components/parma/actions/create-relationship/create-relationship.mjs new file mode 100644 index 0000000000000..06164e9c951ee --- /dev/null +++ b/components/parma/actions/create-relationship/create-relationship.mjs @@ -0,0 +1,51 @@ +import parma from "../../parma.app.mjs"; + +export default { + key: "parma-create-relationship", + name: "Create Relationship", + description: "Creates a new relationship in Parma. [See the documentation](https://developers.parma.ai/api-docs/index.html)", + version: "0.0.1", + type: "action", + props: { + parma, + name: { + propDefinition: [ + parma, + "name", + ], + }, + type: { + propDefinition: [ + parma, + "relationshipType", + ], + optional: true, + }, + groupIds: { + propDefinition: [ + parma, + "groupIds", + ], + }, + about: { + type: "string", + label: "About", + description: "The context of the relationship.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.parma.createRelationship({ + $, + data: { + name: this.name, + type: this.type, + group_ids: this.groupIds, + about: this.about, + }, + }); + + $.export("$summary", `Successfully created relationship with Id: ${response.data.id}`); + return response; + }, +}; diff --git a/components/parma/actions/find-relationship/find-relationship.mjs b/components/parma/actions/find-relationship/find-relationship.mjs new file mode 100644 index 0000000000000..c1a14abd677e2 --- /dev/null +++ b/components/parma/actions/find-relationship/find-relationship.mjs @@ -0,0 +1,102 @@ +import parma from "../../parma.app.mjs"; + +export default { + key: "parma-find-relationship", + name: "Find Relationship", + description: "Searches for an existing relationship in Parma. [See the documentation](https://developers.parma.ai/api-docs/index.html)", + version: "0.0.1", + type: "action", + props: { + parma, + name: { + propDefinition: [ + parma, + "name", + ], + optional: true, + }, + type: { + propDefinition: [ + parma, + "relationshipType", + ], + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "The email of the relationship.", + optional: true, + }, + whatsapp: { + type: "string", + label: "Whatsapp", + description: "The whatsapp of the relationship.", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The phone of the relationship.", + optional: true, + }, + sms: { + type: "string", + label: "SMS", + description: "The SMS of the relationship.", + optional: true, + }, + telegram: { + type: "string", + label: "Telegram", + description: "The telegram of the relationship.", + optional: true, + }, + twitter: { + type: "string", + label: "Twitter", + description: "The twitter of the relationship.", + optional: true, + }, + instagram: { + type: "string", + label: "Instagram", + description: "The instagram of the relationship.", + optional: true, + }, + linkedin: { + type: "string", + label: "LinkedIn", + description: "The linkedin of the relationship.", + optional: true, + }, + schedulinglink: { + type: "string", + label: "Scheduling Link", + description: "The scheduling link of the relationship.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.parma.listRelationships({ + $, + data: { + name: this.name, + type: this.type, + email: this.email, + whatsapp: this.whatsapp, + phone: this.phone, + sms: this.sms, + telegram: this.telegram, + twitter: this.twitter, + instagram: this.instagram, + linkedin: this.linkedin, + schedulinglink: this.schedulinglink, + }, + }); + $.export("$summary", `Successfully fetched ${response.data.length} relationship${response.data.length > 1 + ? "s" + : ""}`); + return response; + }, +}; diff --git a/components/parma/package.json b/components/parma/package.json new file mode 100644 index 0000000000000..6a1aca0a3a9f4 --- /dev/null +++ b/components/parma/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/parma", + "version": "0.1.0", + "description": "Pipedream Parma Components", + "main": "parma.app.mjs", + "keywords": [ + "pipedream", + "parma" + ], + "homepage": "https://pipedream.com/apps/parma", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/parma/parma.app.mjs b/components/parma/parma.app.mjs new file mode 100644 index 0000000000000..2ea0ae134c2fc --- /dev/null +++ b/components/parma/parma.app.mjs @@ -0,0 +1,128 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "parma", + propDefinitions: { + groupIds: { + type: "string[]", + label: "Group Ids", + description: "An array of group Ids.", + async options() { + const { data } = await this.listGroups(); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + relationshipType: { + type: "string", + label: "Type", + description: "The type of relationship.", + options: [ + "person", + "company", + ], + }, + name: { + type: "string", + label: "Name", + description: "The name of the relationship.", + }, + relationshipId: { + type: "string[]", + label: "Relationship ID", + description: "The ID of the relationship.", + async options({ page }) { + const { data } = await this.listRelationships({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://app.parma.ai/api/v1"; + }, + _headers() { + return { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "Content-Type": "application/json", + "Accept": "application/json", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + ...opts, + url: this._baseUrl() + path, + headers: this._headers(), + }); + }, + createRelationship(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/relationships", + ...opts, + }); + }, + listGroups(opts = {}) { + return this._makeRequest({ + path: "/groups", + ...opts, + }); + }, + listRelationships(opts = {}) { + return this._makeRequest({ + path: "/relationships", + ...opts, + }); + }, + addNote(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/notes", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const { data } = await fn({ + params, + ...opts, + }); + for (const d of data) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = data.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/parma/sources/new-relationship/new-relationship.mjs b/components/parma/sources/new-relationship/new-relationship.mjs new file mode 100644 index 0000000000000..b65e91b4ba829 --- /dev/null +++ b/components/parma/sources/new-relationship/new-relationship.mjs @@ -0,0 +1,62 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import parma from "../../parma.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "parma-new-relationship", + name: "New Relationship Created", + description: "Emit new event when a new relationship is created.", + version: "0.0.1", + type: "source", + props: { + parma, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + async startEvent(maxResults = 0) { + const lastId = this._getLastId(); + const response = this.parma.paginate({ + fn: this.parma.listRelationships, + maxResults, + }); + + let responseArray = []; + + for await (const item of response) { + if (item.id <= lastId) break; + responseArray.push(item); + } + + if (responseArray.length) this._setLastId(responseArray[0].id); + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: `New Relationship: ${item.name}`, + ts: Date.parse(new Date()), + }); + } + }, + }, + hooks: { + async deploy() { + await this.startEvent(25); + }, + }, + async run() { + await this.startEvent(); + }, + sampleEmit, +}; diff --git a/components/parma/sources/new-relationship/test-event.mjs b/components/parma/sources/new-relationship/test-event.mjs new file mode 100644 index 0000000000000..a26fa0718106d --- /dev/null +++ b/components/parma/sources/new-relationship/test-event.mjs @@ -0,0 +1,13 @@ +export default { + "id": 123456, + "name": "Relationship Name", + "type": "person", + "about": "About", + "company_id": null, + "group_ids": [ + 1234, + 4321 + ], + "contact_details": [], + "properties": [] +} \ No newline at end of file diff --git a/components/parsehub/README.md b/components/parsehub/README.md new file mode 100644 index 0000000000000..fc39d450f8266 --- /dev/null +++ b/components/parsehub/README.md @@ -0,0 +1,11 @@ +# Overview + +The ParseHub API allows you to leverage the power of web scraping directly within Pipedream. By integrating ParseHub, you can automate the collection of data from web pages, manipulate and transform it with Pipedream’s built-in code steps or pre-built actions, and connect it to hundreds of other apps. You can extract structured data from any website, run scraping jobs, retrieve results and integrate with other services for data processing, visualization, or storage. + +# Example Use Cases + +- **Automated Data Extraction and Database Insertion**: Use ParseHub to scrape product details from an e-commerce site and insert the data into an SQL database using Pipedream's SQL service integrations. This can be set on a schedule to keep the database updated with the latest information. + +- **Lead Generation and CRM Integration**: Scrape contact information from industry forums or directories with ParseHub, then use Pipedream to automatically add those leads to a CRM like Salesforce, ensuring your sales team always has fresh prospects to engage with. + +- **Market Research and Analysis**: Collect data on market trends from various sources using ParseHub, then pipe the results into a data analysis tool like Google Sheets or Tableau with Pipedream. Analyze the data for insights and automatically generate reports for your team. diff --git a/components/parsehub/actions/get-data-run/get-data-run.mjs b/components/parsehub/actions/get-data-run/get-data-run.mjs new file mode 100644 index 0000000000000..f5c924242e770 --- /dev/null +++ b/components/parsehub/actions/get-data-run/get-data-run.mjs @@ -0,0 +1,28 @@ +import app from "../../parsehub.app.mjs"; + +export default { + key: "parsehub-get-data-run", + name: "Get Data for a Run", + description: "Returns the data extracted by a specified run. [See the documentation](https://www.parsehub.com/docs/ref/api/v2/#get-data-for-a-run)", + version: "0.0.2", + type: "action", + props: { + app, + runToken: { + propDefinition: [ + app, + "runToken", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getRunData({ + $, + runToken: this.runToken, + }); + + $.export("$summary", `Successfully retrieved data for run token: ${this.runToken}`); + + return response; + }, +}; diff --git a/components/parsehub/actions/get-project/get-project.mjs b/components/parsehub/actions/get-project/get-project.mjs new file mode 100644 index 0000000000000..71d9a0c0b301b --- /dev/null +++ b/components/parsehub/actions/get-project/get-project.mjs @@ -0,0 +1,28 @@ +import app from "../../parsehub.app.mjs"; + +export default { + key: "parsehub-get-project", + name: "Get Project Details", + description: "Retrieves the details of a specified project within the user's account. [See the documentation](https://www.parsehub.com/docs/ref/api/v2/#get-a-project)", + version: "0.0.2", + type: "action", + props: { + app, + projectId: { + propDefinition: [ + app, + "projectId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getProjectDetails({ + $, + projectId: this.projectId, + }); + + $.export("$summary", `Retrieved details for project ID: ${this.projectId}`); + + return response; + }, +}; diff --git a/components/parsehub/actions/run-project/run-project.mjs b/components/parsehub/actions/run-project/run-project.mjs new file mode 100644 index 0000000000000..a2cf015dda28e --- /dev/null +++ b/components/parsehub/actions/run-project/run-project.mjs @@ -0,0 +1,43 @@ +import app from "../../parsehub.app.mjs"; + +export default { + key: "parsehub-run-project", + name: "Run Parsehub Project", + description: "Initiates an instance of a specified project on the Parsehub cloud. [See the documentation](https://www.parsehub.com/docs/ref/api/v2/#run-a-project)", + version: "0.0.2", + type: "action", + props: { + app, + startUrl: { + propDefinition: [ + app, + "startUrl", + ], + }, + projectId: { + propDefinition: [ + app, + "projectId", + ], + }, + sendEmail: { + type: "boolean", + label: "Send Email", + description: "Whether Parsehub should send an email when the run finishes", + }, + }, + async run({ $ }) { + const response = await this.app.runProject({ + $, + projectId: this.projectId, + data: { + sendEmail: this.sendEmail, + start_url: this.startUrl, + }, + }); + + $.export("$summary", `Successfully initiated project run for project ID: ${this.projectId}`); + + return response; + }, +}; diff --git a/components/parsehub/package.json b/components/parsehub/package.json index 73c6882095c3e..c83d188417880 100644 --- a/components/parsehub/package.json +++ b/components/parsehub/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/parsehub", - "version": "0.0.1", + "version": "0.1.1", "description": "Pipedream ParseHub Components", "main": "parsehub.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" } -} \ No newline at end of file +} diff --git a/components/parsehub/parsehub.app.mjs b/components/parsehub/parsehub.app.mjs index 1c6596f2c3b3e..f3de672b6b67e 100644 --- a/components/parsehub/parsehub.app.mjs +++ b/components/parsehub/parsehub.app.mjs @@ -1,11 +1,85 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "parsehub", - propDefinitions: {}, + propDefinitions: { + projectId: { + type: "string", + label: "Project ID", + description: "The unique identifier of the project", + async options() { + const response = await this.listProjects({}); + const projectsIDs = response.projects; + return projectsIDs.map(({ + token, title, + }) => ({ + value: token, + label: title, + })); + }, + }, + runToken: { + type: "string", + label: "Run Token", + description: "The token of the run to retrieve data for", + }, + startUrl: { + type: "string", + label: "Start URL", + description: "The url to start running on", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://www.parsehub.com/api/v2"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + params: { + ...params, + api_key: `${this.$auth.api_key}`, + }, + }); + }, + getProjectDetails({ + projectId, ...args + }) { + return this._makeRequest({ + path: `/projects/${projectId}`, + ...args, + }); + }, + async runProject({ + projectId, ...args + }) { + return this._makeRequest({ + method: "POST", + path: `/projects/${projectId}/run`, + ...args, + }); + }, + async getRunData({ + runToken, ...args + }) { + return this._makeRequest({ + path: `/runs/${runToken}/data`, + ...args, + }); + }, + async listProjects(args = {}) { + return this._makeRequest({ + path: "/projects", + ...args, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/parser_expert/package.json b/components/parser_expert/package.json new file mode 100644 index 0000000000000..0954b42a8d80e --- /dev/null +++ b/components/parser_expert/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/parser_expert", + "version": "0.0.1", + "description": "Pipedream Parser Expert Components", + "main": "parser_expert.app.mjs", + "keywords": [ + "pipedream", + "parser_expert" + ], + "homepage": "https://pipedream.com/apps/parser_expert", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/parser_expert/parser_expert.app.mjs b/components/parser_expert/parser_expert.app.mjs new file mode 100644 index 0000000000000..e652f879a4e0b --- /dev/null +++ b/components/parser_expert/parser_expert.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "parser_expert", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/parsera/actions/extract/extract.mjs b/components/parsera/actions/extract/extract.mjs new file mode 100644 index 0000000000000..38bc26825ad6d --- /dev/null +++ b/components/parsera/actions/extract/extract.mjs @@ -0,0 +1,56 @@ +import app from "../../parsera.app.mjs"; + +export default { + key: "parsera-extract", + name: "Extract", + description: "Extract data from a given URL. [See the documentation](https://docs.parsera.org/api/getting-started/)", + version: "0.0.1", + type: "action", + props: { + app, + url: { + type: "string", + label: "URL", + description: "The URL to extract information from.", + }, + attributes: { + propDefinition: [ + app, + "attributes", + ], + }, + proxyCountry: { + propDefinition: [ + app, + "proxyCountry", + ], + }, + }, + methods: { + extract(args = {}) { + return this.app.post({ + path: "/extract", + ...args, + }); + }, + }, + async run({ $ }) { + const { + extract, + url, + attributes, + proxyCountry, + } = this; + + const response = await extract({ + $, + data: { + url, + attributes: Array.isArray(attributes) && attributes.map(JSON.parse), + proxy_country: proxyCountry, + }, + }); + $.export("$summary", "Successfully extracted data from url."); + return response; + }, +}; diff --git a/components/parsera/actions/parse/parse.mjs b/components/parsera/actions/parse/parse.mjs new file mode 100644 index 0000000000000..eac4d2eebd9bd --- /dev/null +++ b/components/parsera/actions/parse/parse.mjs @@ -0,0 +1,50 @@ +import app from "../../parsera.app.mjs"; + +export default { + key: "parsera-parse", + name: "Parse", + description: "Parse data using pre-defined attributes. [See the documentation](https://docs.parsera.org/api/getting-started/)", + version: "0.0.1", + type: "action", + props: { + app, + content: { + type: "string", + label: "Content", + description: "The content to parse. HTML or text content. Eg. `

Hello World

`.", + }, + attributes: { + propDefinition: [ + app, + "attributes", + ], + }, + }, + methods: { + parse(args = {}) { + return this.app.post({ + path: "/parse", + ...args, + }); + }, + }, + async run({ $ }) { + const { + parse, + content, + attributes, + } = this; + + const response = await parse({ + $, + data: { + content, + attributes: Array.isArray(attributes) && attributes.map(JSON.parse), + }, + }); + + $.export("$summary", "Successfully parsed content."); + + return response; + }, +}; diff --git a/components/parsera/package.json b/components/parsera/package.json new file mode 100644 index 0000000000000..4689243199bd4 --- /dev/null +++ b/components/parsera/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/parsera", + "version": "0.1.0", + "description": "Pipedream Parsera Components", + "main": "parsera.app.mjs", + "keywords": [ + "pipedream", + "parsera" + ], + "homepage": "https://pipedream.com/apps/parsera", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" + } +} diff --git a/components/parsera/parsera.app.mjs b/components/parsera/parsera.app.mjs new file mode 100644 index 0000000000000..a75cdb2f0584a --- /dev/null +++ b/components/parsera/parsera.app.mjs @@ -0,0 +1,63 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "parsera", + propDefinitions: { + proxyCountry: { + type: "string", + label: "Proxy Country", + description: "Set a specific proxy country for the request.", + optional: true, + async options() { + const response = await this.listProxyCountries(); + return Object.entries(response) + .map(([ + value, + label, + ]) => ({ + label, + value, + })); + }, + }, + attributes: { + type: "string[]", + label: "Attributes", + description: "List of attributes to extract or parse from the content. Format each attribute as a JSON string. Eg. `{\"name\": \"Title\",\"description\": \"News title\"}`.", + }, + }, + methods: { + getUrl(path) { + return `https://api.parsera.org/v1${path}`; + }, + getHeaders(headers) { + return { + "Content-Type": "application/json", + ...headers, + "X-API-KEY": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + listProxyCountries(args = {}) { + return this._makeRequest({ + path: "/proxy-countries", + ...args, + }); + }, + }, +}; diff --git a/components/parseur/README.md b/components/parseur/README.md index d2d6034c8cc24..d2c86e42178e6 100644 --- a/components/parseur/README.md +++ b/components/parseur/README.md @@ -1,16 +1,14 @@ # Overview -Parseur is an AI-powered email parsing and extraction API that allows you to -easily extract data from incoming emails. This powerful tool can be used to -build applications and software that automates a wide variety of processes. -Here are some examples of what you can build with the Parseur API: +Parseur is a powerful email parsing tool that automates the extraction of data from emails and documents. With its API, you can unlock the data trapped in emails and documents and transform it into structured data. On Pipedream, you can use Parseur to trigger workflows from parsed email data, connecting it to hundreds of other services for endless automation possibilities. This can streamline business processes like lead management, invoice processing, and data entry by automating the extraction and flow of critical information. -- Automatically create invoices from emails -- Automatically create leads or contacts from emails -- Automatically store and backup emails in databases -- Automatically send emails in response to incoming messages -- Automatically pass extracted data to your existing software or applications -- Automatically filter and extract information from emails -- Automatically remove duplicate addresses from contact databases -- Automatically transfer parsed data to the desired format for integration with - third-party applications +# Example Use Cases + +- **Email Lead Capture to CRM**: + Automatically extract lead information from incoming emails using Parseur, and then add new leads to your CRM system like Salesforce or HubSpot. This ensures that potential clients are immediately captured in your sales pipeline without manual data entry. + +- **Invoice Processing to Accounting Software**: + Parse invoice data from email attachments with Parseur, and then forward the structured data to accounting software like QuickBooks or Xero. This workflow can help with quick updates of accounts payable and receivable, improving financial operations. + +- **Support Ticket Creation from Customer Emails**: + Convert support requests received via email into structured data using Parseur, and then create tickets in a helpdesk system like Zendesk or Jira Service Desk. This automates the ticketing process, ensuring timely and organized responses to customer inquiries. diff --git a/components/parsio_io/README.md b/components/parsio_io/README.md new file mode 100644 index 0000000000000..40299802e280e --- /dev/null +++ b/components/parsio_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The Parsio.io API lets you extract valuable data from inbound emails. This means you can pull information like order details, shipping notifications, or contact info from automated emails without manual data entry. In Pipedream, you can use Parsio.io to trigger automated workflows based on the extracted data. + +# Example Use Cases + +- **Order Processing Automation**: When Parsio.io extracts new order details from incoming emails, Pipedream can automatically create an order in your eCommerce platform, such as Shopify or WooCommerce. + +- **Customer Support Ticket Creation**: Extract customer inquiries from emails with Parsio.io and use Pipedream to create support tickets in platforms like Zendesk or Freshdesk, enriching each ticket with the provided contact information and query details. + +- **Event Registration Confirmation**: Use Parsio.io to parse registration details from event sign-up confirmation emails. Pipedream can then add these details to a Google Sheets spreadsheet and send personalized confirmation emails via SendGrid or another email service provider. diff --git a/components/parsio_io/package.json b/components/parsio_io/package.json new file mode 100644 index 0000000000000..b18326c78ada9 --- /dev/null +++ b/components/parsio_io/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/parsio_io", + "version": "0.0.1", + "description": "Pipedream Parsio.io Components", + "main": "parsio_io.app.mjs", + "keywords": [ + "pipedream", + "parsio_io" + ], + "homepage": "https://pipedream.com/apps/parsio_io", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/parsio_io/parsio_io.app.mjs b/components/parsio_io/parsio_io.app.mjs new file mode 100644 index 0000000000000..d6e5a8b4ab3ed --- /dev/null +++ b/components/parsio_io/parsio_io.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "parsio_io", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/partnerize/README.md b/components/partnerize/README.md new file mode 100644 index 0000000000000..d244be81aec18 --- /dev/null +++ b/components/partnerize/README.md @@ -0,0 +1,11 @@ +# Overview + +The Partnerize API enables you to interact programmatically with the Partnerize platform, which specializes in partner marketing and affiliate management. With this API, you can automate tasks related to tracking, commissioning, and reporting on marketing campaigns. In Pipedream, you can leverage these capabilities to create automated workflows that respond to various triggers (like webhooks), manipulate data, and integrate with a myriad of other services without having to manage servers or complex infrastructure. + +# Example Use Cases + +- **Commission Reporting to Google Sheets**: Automate the collection of commission data from Partnerize and append it to a Google Sheet. This workflow can be scheduled to run at regular intervals, ensuring you always have up-to-date commission information for analysis. + +- **Affiliate Signup Notifications**: Send a Slack notification or an email via SendGrid whenever a new affiliate signs up through Partnerize. This can help you stay on top of your growing affiliate network and respond promptly to new partners. + +- **Campaign Performance Dashboarding**: Use Partnerize API to fetch the latest performance metrics for your marketing campaigns and feed them into a Grafana dashboard. This allows for real-time visualization and monitoring of campaign effectiveness. diff --git a/components/partnerize/package.json b/components/partnerize/package.json new file mode 100644 index 0000000000000..73b09f49a2cd0 --- /dev/null +++ b/components/partnerize/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/partnerize", + "version": "0.0.1", + "description": "Pipedream Partnerize Components", + "main": "partnerize.app.mjs", + "keywords": [ + "pipedream", + "partnerize" + ], + "homepage": "https://pipedream.com/apps/partnerize", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/partnerize/partnerize.app.mjs b/components/partnerize/partnerize.app.mjs new file mode 100644 index 0000000000000..40eec91d1d441 --- /dev/null +++ b/components/partnerize/partnerize.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "partnerize", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/partnerstack/README.md b/components/partnerstack/README.md index 9a0b3eaf03d18..63dc1cc1f55f6 100644 --- a/components/partnerstack/README.md +++ b/components/partnerstack/README.md @@ -1,22 +1,11 @@ # Overview -## What Can You Build with PartnerStack (Partner API)? +The PartnerStack (Partner API) API empowers you to automate and streamline your partner, affiliate, and reseller networks. Using Pipedream, you can connect this API to orchestrate workflows that synchronize partner data, track referrals and commissions, and manage payouts. This enables you to maintain a robust partner ecosystem while cutting down on manual work. By tapping into events and data from PartnerStack, you can ensure partners are rewarded promptly, gain insights into the most effective channels, and foster stronger relationships with your network. -PartnerStack provides an API that allows developers to easily integrate the -power of partnerships into their products. With the Partner API, developers can -create various kinds of integrations to help drive products, strengthen -partnerships, and create comprehensive marketing campaigns. Below are a few -examples of what you can build with the Partner API. +# Example Use Cases -## Examples +- **Synchronize Partner Data with CRM**: Automate the process of keeping your CRM up-to-date with the latest partner information. When a new partner joins through PartnerStack, trigger a workflow on Pipedream that creates or updates their record in your CRM platform (e.g., Salesforce or HubSpot), ensuring sales and marketing teams have the latest data at their fingertips. -- Create targeted referral programs -- Automate partners onboarding -- Create a partner portal to manage partnerships -- Construct loyalty programs to reward your customers and partners -- Develop cross-sell campaigns -- Develop integrated promotion campaigns -- Provide a network of leads to increase customer acquisition -- Track, reward and leverage influencers -- Analyze partnership performance and ROI -- Integrate partners using API's and Webhooks +- **Referral Reward Notifications**: Create an instant notification system for rewarding successful referrals. When a partner generates a new sale via PartnerStack, use Pipedream to send a notification through Slack or email to your team. This workflow could also update a dashboard (e.g., Google Sheets or Geckoboard) that tracks referral success in real-time, keeping everyone informed and motivated. + +- **Automated Commission Payouts**: Streamline your commission payment process. When PartnerStack records a commission for a partner, trigger a workflow on Pipedream to process the payment through your accounting software (e.g., QuickBooks or Xero). This eliminates the need for manual entry, reduces errors, and ensures that partners receive their payments swiftly and accurately. \ No newline at end of file diff --git a/components/partnerstack/package.json b/components/partnerstack/package.json new file mode 100644 index 0000000000000..3e025391c92bc --- /dev/null +++ b/components/partnerstack/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/partnerstack", + "version": "0.6.0", + "description": "Pipedream partnerstack Components", + "main": "partnerstack.app.mjs", + "keywords": [ + "pipedream", + "partnerstack" + ], + "homepage": "https://pipedream.com/apps/partnerstack", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/passcreator/README.md b/components/passcreator/README.md new file mode 100644 index 0000000000000..dc22fd388c4e4 --- /dev/null +++ b/components/passcreator/README.md @@ -0,0 +1,11 @@ +# Overview + +The Passcreator API lets you automate the creation, management, and distribution of mobile wallet content, such as coupons, membership cards, and event tickets. Within Pipedream, this API becomes a powerful tool to connect with other services, streamlining your marketing and customer engagement efforts. By blending the API's functionality with Pipedream's ability to integrate with countless services, you can create dynamic, personalized wallet items and distribute them based on user behavior, events, or data from other apps. + +# Example Use Cases + +- **Customer Reward System Automation**: Trigger a Passcreator wallet item (like a loyalty card) issuance whenever a new customer is added to your CRM system, like Salesforce or HubSpot. Monitor customer engagement and send personalized follow-up offers. + +- **Event Registration and Ticketing**: When someone signs up for an event using an app like Eventbrite, automatically generate a Passcreator event ticket and send it via email or SMS. Sync attendee information to your database or a tool like Google Sheets for easy management. + +- **Personalized Coupon Distribution**: Connect Passcreator with e-commerce platforms like Shopify or WooCommerce. When a customer completes a purchase, use their buying history and preferences to create a personalized Passcreator coupon and deliver it instantly, encouraging repeat business. diff --git a/components/passslot/README.md b/components/passslot/README.md new file mode 100644 index 0000000000000..799084f2b938c --- /dev/null +++ b/components/passslot/README.md @@ -0,0 +1,11 @@ +# Overview + +The PassSlot API enables you to create and manage passes for Apple Wallet, such as coupons, loyalty cards, and event tickets. Integrating PassSlot with Pipedream allows you to automate the distribution and update of passes based on various triggers and events. You can dynamically create passes based on user actions, update them when certain conditions are met, and track their usage without manual intervention. This simplifies the process of engaging with customers through their mobile wallets. + +# Example Use Cases + +- **Customer Loyalty Program Automation**: Automatically generate a loyalty card pass for Apple Wallet when a new user signs up on your platform. Trigger this Pipedream workflow by connecting to your user management system, like Auth0 or Firebase. + +- **Event Ticket Distribution**: Send personalized event tickets as passes when a customer purchases a ticket. Use Pipedream to listen for new Stripe payments or Shopify orders, generate a pass through PassSlot and email it directly to the customer. + +- **Coupon Code Update**: Update and invalidate a coupon pass in Apple Wallet when a promotional campaign ends. Set up a Pipedream workflow that listens for a webhook from your marketing platform, such as Mailchimp, and then updates the pass accordingly via PassSlot. diff --git a/components/path_of_exile/README.md b/components/path_of_exile/README.md index a7c6d6a324d49..6ebf6bad60d4a 100644 --- a/components/path_of_exile/README.md +++ b/components/path_of_exile/README.md @@ -1,28 +1,11 @@ # Overview -Path of Exile (PoE) is a popular online action role-playing game developed and -published by Grinding Gear Games. The game provides an extensive and robust API -which can be used to build and optimize a variety of tools and applications. +The Path of Exile API offers a trove of in-game information, from player stats to item data. Using Pipedream, you can automate interactions with the API to monitor game updates, track item prices, automate notifications for trades, and integrate with other services for analytics or enhanced gameplay experiences. -The PoE API offers a comprehensive set of features, including the ability to -access character information, league information, item information, and more. -It also provides game components such as crafting, trading, and searching, as -well as the ability to build a custom character build planner. The API also -provides the ability to create automated bots and tools for managing crafted -items. +# Example Use Cases -Below are just a few of the many tools and applications that can be built using -the PoE API: +- **Automated Trade Offers Notifications**: Set up a Pipedream workflow that polls the Path of Exile trade API for items you're interested in. When a new listing meets your criteria, automatically send a notification via Discord or Slack, ensuring you never miss out on a critical trade. -- Automatic build planner: A tool that allows players to easily plan out their - builds and optimize their characters. -- Recipe search tool: A tool that allows players to find recipes quickly and - easily. -- Item search tool: A tool that lets players search for items they’re looking - for without having to go through the in-game menus or search manually. -- Automated currency converter: A tool that automatically converts currency - between different leagues. -- Trading tool: A tool that can help players find the best deals as they trade - with other PoE players. -- Character stats tracker: A tool that tracks characters’ stats over time, - helping players optimize and improve their builds. +- **League Stat Tracking**: Create a Pipedream workflow that retrieves your character's league stats at regular intervals. Log this data into Google Sheets or Airtable to analyze your gameplay over time or share your progress with friends or your gaming community. + +- **Price Watcher for Rare Items**: Monitor the market for rare items with a Pipedream workflow. When the price drops below a threshold you set, trigger an alert through SMS or email. Connect this workflow with a database like MongoDB to track historical price data for deeper market insights. diff --git a/components/path_of_exile/package.json b/components/path_of_exile/package.json new file mode 100644 index 0000000000000..96609d114c2c9 --- /dev/null +++ b/components/path_of_exile/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/path_of_exile", + "version": "0.6.0", + "description": "Pipedream path_of_exile Components", + "main": "path_of_exile.app.mjs", + "keywords": [ + "pipedream", + "path_of_exile" + ], + "homepage": "https://pipedream.com/apps/path_of_exile", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/patreon/README.md b/components/patreon/README.md new file mode 100644 index 0000000000000..c8d53a85fc0b3 --- /dev/null +++ b/components/patreon/README.md @@ -0,0 +1,11 @@ +# Overview + +Pipedream's integration with the Patreon API enables creators to automate interactions with their patrons, manage memberships, and streamline content delivery. By leveraging Pipedream's ability to connect with hundreds of apps, creators can set up custom workflows that trigger actions based on Patreon events, like new pledges, or that perform regular tasks, like sending messages to patrons. + +# Example Use Cases + +- **Auto-Thank New Patrons**: Trigger a workflow when a new patron joins and automatically send a personalized thank you email via SendGrid or another email service provider connected to Pipedream. + +- **Patron Milestone Celebrations**: Set up a workflow that tracks the number of patrons and posts celebratory messages on social media platforms like Twitter when certain milestones are reached. + +- **Exclusive Content Delivery**: Create a system where new posts on Patreon trigger a workflow that sends exclusive content links to qualifying patrons through platforms like Slack or Discord. diff --git a/components/paved/common/constants.mjs b/components/paved/common/constants.mjs new file mode 100644 index 0000000000000..626f2e55be4c6 --- /dev/null +++ b/components/paved/common/constants.mjs @@ -0,0 +1,17 @@ +const BASE_URL = "https://api.paved.com"; +const LAST_FROM_DATE = "lastFromDate"; +const DEFAULT_LIMIT = 100; +const DEFAULT_MAX = 600; + +const API = { + PUBLISHER_PATH: "/publisher/v1", + ADVERTISER_PATH: "/advertiser/v1", +}; + +export default { + BASE_URL, + API, + DEFAULT_LIMIT, + DEFAULT_MAX, + LAST_FROM_DATE, +}; diff --git a/components/paved/common/utils.mjs b/components/paved/common/utils.mjs new file mode 100644 index 0000000000000..903b2593ed3c2 --- /dev/null +++ b/components/paved/common/utils.mjs @@ -0,0 +1,11 @@ +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +export default { + iterate, +}; diff --git a/components/paved/package.json b/components/paved/package.json new file mode 100644 index 0000000000000..044b7e31455f3 --- /dev/null +++ b/components/paved/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/paved", + "version": "0.1.0", + "description": "Pipedream Paved Components", + "main": "paved.app.mjs", + "keywords": [ + "pipedream", + "paved" + ], + "homepage": "https://pipedream.com/apps/paved", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/paved/paved.app.mjs b/components/paved/paved.app.mjs new file mode 100644 index 0000000000000..fa8f075e443b8 --- /dev/null +++ b/components/paved/paved.app.mjs @@ -0,0 +1,102 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; + +export default { + type: "app", + app: "paved", + propDefinitions: { + slug: { + type: "string", + label: "Newsletter Slug", + description: "Your newsletter slug.", + async options() { + const newsletters = await this.listNewsletters(); + return newsletters.map(({ + name: label, slug: value, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + getUrl(path, versionPath = constants.API.PUBLISHER_PATH) { + return `${constants.BASE_URL}${versionPath}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + Authorization: `Token ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + listNewsletters(args = {}) { + return this._makeRequest({ + path: "/newsletters", + ...args, + }); + }, + listSponsorships({ + slug, ...args + } = {}) { + return this._makeRequest({ + path: `/newsletters/${slug}/sponsorships`, + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, + fromDate, + max = constants.DEFAULT_MAX, + }) { + let resourcesCount = 0; + + while (true) { + const nextResources = + await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + limit: constants.DEFAULT_LIMIT, + from_date: fromDate, + }, + }); + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + yield resource; + resourcesCount += 1; + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + if (nextResources.length < constants.DEFAULT_LIMIT) { + console.log("No next page found"); + return; + } + + fromDate = nextResources[nextResources.length - 1].run_date; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); + }, + }, +}; diff --git a/components/paved/sources/common/polling.mjs b/components/paved/sources/common/polling.mjs new file mode 100644 index 0000000000000..aaf709400d743 --- /dev/null +++ b/components/paved/sources/common/polling.mjs @@ -0,0 +1,81 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../paved.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + slug: { + propDefinition: [ + app, + "slug", + ], + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + setLastFromDate(value) { + this.db.set(constants.LAST_FROM_DATE, value); + }, + getLastFromDate() { + return this.db.get(constants.LAST_FROM_DATE); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + async processResources(resources) { + const { + setLastFromDate, + processResource, + } = this; + + const [ + lastResource, + ] = Array.from(resources).reverse(); + + if (lastResource?.run_date) { + setLastFromDate(lastResource.run_date); + } + + Array.from(resources) + .forEach(processResource); + }, + }, + async run() { + const { + app, + getResourcesFn, + getResourcesFnArgs, + processResources, + } = this; + + const resources = await app.paginate({ + resourcesFn: getResourcesFn(), + resourcesFnArgs: getResourcesFnArgs(), + fromDate: this.getLastFromDate(), + }); + + processResources(resources); + }, +}; diff --git a/components/paved/sources/new-sponsorship-detected/new-sponsorship-detected.mjs b/components/paved/sources/new-sponsorship-detected/new-sponsorship-detected.mjs new file mode 100644 index 0000000000000..e1b98d8244ebd --- /dev/null +++ b/components/paved/sources/new-sponsorship-detected/new-sponsorship-detected.mjs @@ -0,0 +1,30 @@ +import common from "../common/polling.mjs"; + +export default { + ...common, + key: "paved-new-sponsorship-detected", + name: "New Sponsorship Detected", + description: "Emit new event when a sponsorship is detected on a newsletter similar to yours. [See the documentation](https://docs.paved.com/list-sponsorships)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourcesFn() { + return this.app.listSponsorships; + }, + getResourcesFnArgs() { + return { + debug: true, + slug: this.slug, + }; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Sponsorthip: ${resource.advertiser_name}`, + ts: Date.parse(resource.run_date), + }; + }, + }, +}; diff --git a/components/payhere/README.md b/components/payhere/README.md index a2f619df11c5b..3f898930bf323 100644 --- a/components/payhere/README.md +++ b/components/payhere/README.md @@ -1,28 +1,11 @@ # Overview -With the Payhere API, you can build a variety of powerful and user-friendly -payment solutions. Whether you are creating an online store, accepting -donations, taking payments for services, or creating a membership system, the -Payhere API allows you to securely handle payment solutions without having to -worry about any of the technical complexities that come with implementing -payments. +Payhere, powered by Xendit, offers a powerful API that allows you to craft seamless payment experiences within your apps and websites. By leveraging this API on Pipedream, you can automate payment processing, synchronize transaction data with accounting systems, or trigger communication based on payment events. Pipedream's serverless platform enables the orchestration of Payhere with an array of other services to streamline financial operations, enhance customer engagement, and maintain robust financial records. -Payhere provides a host of useful features, like: +# Example Use Cases -- Securely handle payments from most major credit cards and debit cards -- Accept payments from bank transfers, PayPal, mobile money, and more -- Stay up-to-date on the status of payments and customers with real-time - notifications -- Automate sorting and reconciling payments using the Payhere features +- **Automated Receipt Generation**: When a payment is successfully processed via Payhere, trigger a workflow on Pipedream to generate a digital receipt using a service like PDF.co. The workflow can then email the receipt to the customer automatically using an email service like SendGrid. -Below are a few examples of what you can build using the Payhere API: +- **Real-Time Sales Dashboard Update**: Create a Pipedream workflow that listens for new transactions from Payhere and updates a real-time dashboard on Google Sheets or Geckoboard. This allows for instantaneous monitoring of sales data, which can be particularly useful for flash sales or promotions. -- Online stores with product and inventory management -- Donation portals -- Online ticketing systems -- Billing platforms for services -- Subscription systems for membership sites -- Peer-to-peer (P2P) payments within an application -- Marketplace payment solutions -- Ecommerce payment processing -- Mobile payment options for on-the-go payment options +- **Customer Support Ticket Creation**: Use Pipedream to detect failed or disputed transactions from Payhere, automatically creating a support ticket in a customer service platform like Zendesk or Freshdesk. This workflow can help prioritize and streamline customer support responses to payment issues. diff --git a/components/payhip/README.md b/components/payhip/README.md new file mode 100644 index 0000000000000..f03b36d496239 --- /dev/null +++ b/components/payhip/README.md @@ -0,0 +1,11 @@ +# Overview + +The Payhip API offers a suite of e-commerce capabilities, enabling automation for selling digital products, memberships, and physical goods. With Pipedream, you can leverage these capabilities to create customized workflows that respond to various Payhip events, such as new sales or product updates. These workflows can handle data manipulation, integrate with other services, or notify you or your team of important events. + +# Example Use Cases + +- **Automate Customer Follow-Ups:** After a sale on Payhip, use Pipedream to send a personalized thank you email to the customer via SendGrid. Then, schedule a follow-up email asking for feedback or offering a discount on future purchases. + +- **Sync Sales Data with Google Sheets:** Capture new Payhip sales data in real-time and log it into a Google Sheet. This workflow helps maintain an up-to-date sales record, making it easier to analyze trends and generate reports without manual data entry. + +- **Manage Inventory with Airtable:** When a product's stock changes on Payhip, use Pipedream to update the corresponding records in an Airtable base. This ensures that your inventory management is accurate and reflects current stock levels, helping to prevent overselling. diff --git a/components/paykickstart/README.md b/components/paykickstart/README.md index 778ec8f4d1d78..8c37f00dd1d91 100644 --- a/components/paykickstart/README.md +++ b/components/paykickstart/README.md @@ -1,28 +1,14 @@ # Overview -PayKickStart is an all-in-one ecommerce, subscription and membership platform -offering robust analytics and reporting tools. With their powerful and -comprehensive API, users can build powerful integrations and automated -workflows to optimise their business. +The PayKickstart API enables you to automate your sales funnel and manage transactions, subscriptions, and customers in your PayKickstart account. With Pipedream, you can harness this capability to create custom workflows that react to events, synchronize data across platforms, or trigger actions based on customer behaviors. Whether you're looking to streamline your e-commerce operations or integrate PayKickstart into your broader tech stack, Pipedream makes it feasible without writing extensive code. -The key features of the PayKickStart API include: +# Example Use Cases -- Automated workflows -- Ecommerce capabilities -- Analytics and reporting -- Ability to create subscriptions and memberships -- Ability to build custom payment forms +- **Automated Customer Follow-up** + Set up a workflow that listens for new sales in PayKickstart and then triggers an email sequence in an email marketing service like Mailchimp. This could be a thank-you message, a series of onboarding emails, or a request for product feedback. -Using the PayKickStart API, businesses have the power to build powerful and -scalable ecommerce experiences. Here are some examples of what you can build -using the PayKickStart API: +- **Synchronized Subscription Status** + Whenever a subscription is updated or canceled in PayKickstart, use Pipedream to automatically reflect these changes in your CRM, like Salesforce. This ensures your sales team has the most current information on customer subscriptions when they make contact. -- Create automated payment forms with custom flows -- Automatically create subscriptions and memberships -- Generate customized invoices and statements -- Analyze customer data and usage -- Create user fields for collecting detailed customer information -- Integrate with other 3rd party systems and applications -- Track transactions and capture detailed payment information -- Create custom product recommendations based on customer purchase history or - behaviour +- **Real-time Analytics Dashboard** + Use Pipedream to send PayKickstart data to Google Sheets or a BI tool like Tableau every time a sale occurs. This allows you to maintain an up-to-date analytics dashboard for tracking revenue, customer acquisition cost, or other key performance indicators. diff --git a/components/paykickstart/package.json b/components/paykickstart/package.json new file mode 100644 index 0000000000000..81ed3aa08b004 --- /dev/null +++ b/components/paykickstart/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/paykickstart", + "version": "0.6.0", + "description": "Pipedream paykickstart Components", + "main": "paykickstart.app.mjs", + "keywords": [ + "pipedream", + "paykickstart" + ], + "homepage": "https://pipedream.com/apps/paykickstart", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/paylocity/package.json b/components/paylocity/package.json new file mode 100644 index 0000000000000..b9f2d8f1f5cd6 --- /dev/null +++ b/components/paylocity/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/paylocity", + "version": "0.0.1", + "description": "Pipedream Paylocity Components", + "main": "paylocity.app.mjs", + "keywords": [ + "pipedream", + "paylocity" + ], + "homepage": "https://pipedream.com/apps/paylocity", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/paylocity/paylocity.app.mjs b/components/paylocity/paylocity.app.mjs new file mode 100644 index 0000000000000..adf5df1613c22 --- /dev/null +++ b/components/paylocity/paylocity.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "paylocity", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/paymo/README.md b/components/paymo/README.md index ed5d47694d54e..875462c0ebc1f 100644 --- a/components/paymo/README.md +++ b/components/paymo/README.md @@ -1,24 +1,11 @@ # Overview -Using the Paymo API, you can build a range of applications that help your -business run smoother, faster, and more cost-effectively. With the Paymo API, -you can: +The Paymo API offers a broad spectrum of project management functionalities, including time tracking, task management, and invoicing. By leveraging Pipedream's serverless platform, you can automate workflows spanning across these capabilities, such as syncing project updates, streamlining time entries, and managing invoices with other apps. Pipedream's no-code connectors and trigger/action mechanics enable customized integration solutions, making tedious tasks disappear. -- Create custom time tracking and reporting systems -- Integrate Payment solutions with other platforms -- Create project management systems -- Generate invoices -- Automate notifications and reminders +# Example Use Cases -Examples of applications you can build with the Paymo API include: +- **Automated Time Tracking Reports**: Automatically generate weekly time tracking reports in Paymo and send them to a Slack channel. This keeps teams informed about project hours without manual data compilation. -- Time tracking and reporting systems -- Solutions for online payments and billing -- Project management applications -- Custom notifications for quick and easy updates -- Automated invoicing solutions -- Automated reminders for due dates and other deadlines -- Invoicing integration solutions -- Custom productivity solutions -- Reporting and analytics tools -- Task management systems +- **Sync Tasks with Google Calendar**: When a new task is created in Paymo, add it as an event in Google Calendar. This ensures your schedule is always up-to-date with your project’s task deadlines. + +- **Invoice Management with QuickBooks**: Create an invoice in Paymo and automatically copy the details to QuickBooks for accounting purposes. This eliminates the need to manually enter the same data across different platforms. diff --git a/components/paypal/README.md b/components/paypal/README.md index 5337b081ef3c0..f36355b9b4afd 100644 --- a/components/paypal/README.md +++ b/components/paypal/README.md @@ -1,24 +1,15 @@ # Overview -The PayPal API is an incredibly powerful tool for developers and businesses to -manage payments, subscriptions, and money transfers securely without having to -write lengthy integration code. With the PayPal API, you can rapidly integrate -payment, billing, and money transfer operations into any website or app, -enabling businesses to accept payments from customers around the world. - -As such, you can use the PayPal API to build many different types of -applications, such as: - -- eCommerce payment processing -- Billing and invoicing tools -- Online marketplace integrations -- Subscription management -- Peer-to-peer money transfers -- Automated clearance of payments -- Crowdfunding platforms -- Multi-currency transaction support -- Payment fraud detection and prevention systems -- Mobile app monetization +The PayPal API on Pipedream allows you to automate payment processing, manage transactions, and streamline financial operations within your applications. By leveraging Pipedream's serverless platform, you can craft customized workflows that trigger on specific PayPal events, such as successful payments or disputes. Harness the power of automations to synchronize transaction data across diverse systems, send notifications, or even analyze financial trends. + +# Example Use Cases + +- **Automated Invoice Creation and Payment Tracking**: When a customer completes a payment, use the PayPal API to capture the transaction details. Then, create an invoice in an accounting app like QuickBooks, and track the payment status. Notify your sales team or customer via email or messaging platforms like Slack with the transaction and invoice details. + +- **Dispute Resolution Workflow**: Set up a Pipedream workflow that triggers when a dispute is filed on PayPal. Automatically gather relevant transaction details and customer information. Use this data to create a ticket in a customer service platform like Zendesk or ServiceNow, and assign it to the appropriate team for expedited dispute resolution. + +- **Sales Monitoring and Analytics**: Monitor your PayPal transactions to gain insights into sales trends. With each sale, trigger a Pipedream workflow to insert transaction data into a Google Sheet or send it to a BI tool like Tableau for analysis. Set up alerts for large transactions and generate daily or weekly sales reports to keep the team informed. + # Troubleshooting **Why can't I connect my PayPal account in Pipedream?** diff --git a/components/paypro/README.md b/components/paypro/README.md index 613e6b70df057..fa473eee5caaa 100644 --- a/components/paypro/README.md +++ b/components/paypro/README.md @@ -1,19 +1,11 @@ # Overview -With the PayPro API, you can build powerful applications that provide secure, -real-time access to payment and banking services. From secure payments and -banking services to money management and budgeting tools, the possibilities are -endless. +The PayPro API allows for seamless integration of payment processing capabilities with your web applications or e-commerce platforms. With this API, you can initiate payments, verify transactions, and manage refunds. Automating payment workflows can significantly streamline business operations, reduce the workload on finance teams, and provide real-time financial data synchronization. -Here are some examples of what you can build with the PayPro API: +# Example Use Cases -- Online Payment Gateways -- Money Management Apps -- Credit Card Processing -- Automated Billing System -- Automated Banking & Cash Management Tools -- Budget & Spending Management Tools -- Insurance & Risk Management Solutions -- Online Shopping Apps -- Loyalty & Rewards Programs -- And more! +- **Automated Invoice Processing**: Automatically create and send invoices using PayPro when a new order is received in your e-commerce platform. Trigger an action in Pipedream when a purchase is made, generate an invoice through PayPro, and deliver the invoice to the customer's email. + +- **Real-Time Payment Alerts**: Set up a notification system that alerts you via Slack, email, or SMS whenever a payment is processed or fails. In Pipedream, listen for a webhook from PayPro indicating a transaction update, and then forward the alert to the preferred communication channel. + +- **Refund Automation**: Simplify the refund process by integrating PayPro with a customer support platform like Zendesk. When a refund request is approved in Zendesk, trigger a Pipedream workflow that instructs PayPro to process the refund, reducing manual intervention and ensuring prompt customer service. diff --git a/components/paystack/README.md b/components/paystack/README.md new file mode 100644 index 0000000000000..cc56b43dcd25e --- /dev/null +++ b/components/paystack/README.md @@ -0,0 +1,11 @@ +# Overview + +Paystack is a payment gateway that allows businesses to accept payments online from customers. With the Paystack API, you can automate invoicing, verify transactions, manage customers, and more. On Pipedream, you can integrate Paystack with a multitude of other apps to build powerful workflows, such as syncing payment data with accounting software, triggering events based on transaction status, or automating customer outreach. + +# Example Use Cases + +- **Automated Invoice Generation and Payment Tracking**: When a customer completes an order on your e-commerce platform, use Pipedream to automatically generate an invoice via the Paystack API and send it to the customer. Once payment is confirmed, update the order status in your database and notify the customer with a thank-you message. + +- **Subscription Payment Verification and Access Control**: Integrate Paystack with a membership platform to handle subscription payments. Use Pipedream to check for successful subscription payments and, upon verification, grant or extend access to premium content. Conversely, automate access revocation for failed renewals. + +- **Real-time Financial Dashboard Updates**: Connect Paystack to a dashboard app like Geckoboard on Pipedream. Set up a workflow that listens for new transactions on Paystack, processes the data, and then pushes real-time updates to your financial dashboard, allowing for immediate insight into sales trends and revenue. diff --git a/components/paystack/actions/charge-authorization/charge-authorization.mjs b/components/paystack/actions/charge-authorization/charge-authorization.mjs new file mode 100644 index 0000000000000..fa550ef9d9474 --- /dev/null +++ b/components/paystack/actions/charge-authorization/charge-authorization.mjs @@ -0,0 +1,47 @@ +import paystack from "../../paystack.app.mjs"; + +export default { + key: "paystack-charge-authorization", + name: "Charge Authorization", + description: "Charge a reusable authorization. [See the documentation](https://paystack.com/docs/api/transaction/#charge-authorization)", + version: "0.0.2", + type: "action", + props: { + paystack, + email: { + propDefinition: [ + paystack, + "email", + ], + }, + amount: { + propDefinition: [ + paystack, + "amount", + ], + }, + authorization_code: { + propDefinition: [ + paystack, + "authorization_code", + (configuredProps) => ({ + customer: configuredProps.email, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.paystack.chargeAuthorization({ + $, + data: { + email: this.email, + amount: this.amount, + authorization_code: this.authorization_code, + }, + + }); + + $.export("$summary", "Authorization charged"); + return response; + }, +}; diff --git a/components/paystack/actions/fetch-transaction/fetch-transaction.mjs b/components/paystack/actions/fetch-transaction/fetch-transaction.mjs index b6e0a2e026b65..0a16d0b0812d2 100644 --- a/components/paystack/actions/fetch-transaction/fetch-transaction.mjs +++ b/components/paystack/actions/fetch-transaction/fetch-transaction.mjs @@ -4,7 +4,7 @@ export default { key: "paystack-fetch-transaction", name: "Fetch Transaction", description: "Fetch a single transaction. [See the documentation](https://paystack.com/docs/api/transaction/#fetch)", - version: "0.0.1", + version: "0.0.3", type: "action", props: { paystack, diff --git a/components/paystack/actions/initialize-transaction/initialize-transaction.mjs b/components/paystack/actions/initialize-transaction/initialize-transaction.mjs index 847e23503d578..3e0c2a6ed9819 100644 --- a/components/paystack/actions/initialize-transaction/initialize-transaction.mjs +++ b/components/paystack/actions/initialize-transaction/initialize-transaction.mjs @@ -4,7 +4,7 @@ export default { key: "paystack-initialize-transaction", name: "Initialize Transaction", description: "Initializes a new transaction on Paystack. [See the documentation](https://paystack.com/docs/api/transaction/#initialize)", - version: "0.0.2", + version: "0.1.1", type: "action", props: { paystack, diff --git a/components/paystack/actions/list-transactions/list-transactions.mjs b/components/paystack/actions/list-transactions/list-transactions.mjs index 51e6bf86f3e8d..d06d2a0cf163f 100644 --- a/components/paystack/actions/list-transactions/list-transactions.mjs +++ b/components/paystack/actions/list-transactions/list-transactions.mjs @@ -4,7 +4,7 @@ export default { key: "paystack-list-transactions", name: "List Transactions", description: "List transactions carried out on your integration. [See the documentation](https://paystack.com/docs/api/transaction/#list)", - version: "0.0.1", + version: "0.0.3", type: "action", props: { paystack, diff --git a/components/paystack/actions/verify-transaction/verify-transaction.mjs b/components/paystack/actions/verify-transaction/verify-transaction.mjs index 3fbb019ffd5d8..dab38b293d3a1 100644 --- a/components/paystack/actions/verify-transaction/verify-transaction.mjs +++ b/components/paystack/actions/verify-transaction/verify-transaction.mjs @@ -4,7 +4,7 @@ export default { key: "paystack-verify-transaction", name: "Verify Transaction", description: "Confirm the status of a transaction. [See the documentation](https://paystack.com/docs/api/transaction/#verify)", - version: "0.0.1", + version: "0.0.3", type: "action", props: { paystack, diff --git a/components/paystack/package.json b/components/paystack/package.json index 42c33650a4b66..338ca51435613 100644 --- a/components/paystack/package.json +++ b/components/paystack/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/paystack", - "version": "0.1.0", + "version": "0.2.1", "description": "Pipedream Paystack Components", "main": "paystack.app.mjs", "keywords": [ diff --git a/components/paystack/paystack.app.mjs b/components/paystack/paystack.app.mjs index 4fbf7ab4eee8e..b35caf89e0d8f 100644 --- a/components/paystack/paystack.app.mjs +++ b/components/paystack/paystack.app.mjs @@ -9,6 +9,25 @@ export default { type: "string", label: "Email", description: "Email address of the customer to charge", + async options({ + page, query, + }) { + const { data } = await this.listCustomers({ + params: { + page: page + 1, + email: query, + }, + }); + return ( + data?.map(({ + email: value, email: label, + }) => ({ + label, + value, + })) || [] + ); + }, + useQuery: true, }, amount: { type: "string", @@ -18,39 +37,46 @@ export default { currency: { type: "string", label: "Currency", - description: "Currency to use for the charge. Defaults to your integration currency", + description: + "Currency to use for the charge. Defaults to your integration currency", options: constants.CURRENCIES, }, reference: { type: "string", label: "Reference", - description: "Unique transaction reference. Only alphanumeric characters and `-`, `.`, `=` are allowed", + description: + "Unique transaction reference. Only alphanumeric characters and `-`, `.`, `=` are allowed", async options({ page }) { const { data } = await this.listTransactions({ params: { page: page + 1, }, }); - return data?.map(({ reference }) => ({ - value: reference, - label: `${reference}`, - })) || []; + return ( + data?.map(({ reference }) => ({ + value: reference, + label: `${reference}`, + })) || [] + ); }, }, callbackUrl: { type: "string", label: "Callback URL", - description: "URL to redirect customers to after a successful transaction. Setting this overrides the callback URL set on the dashboard", + description: + "URL to redirect customers to after a successful transaction. Setting this overrides the callback URL set on the dashboard", }, metadata: { type: "object", label: "Metadata", - description: "Stringified JSON object of custom data. Check the [Metadata docs](https://paystack.com/docs/payments/metadata/) for more information", + description: + "Stringified JSON object of custom data. Check the [Metadata docs](https://paystack.com/docs/payments/metadata/) for more information", }, status: { type: "string", label: "Status", - description: "Status of a transaction. Possible values are success, failed, and abandoned.", + description: + "Status of a transaction. Possible values are success, failed, and abandoned.", options: constants.STATUS, }, transactionID: { @@ -63,10 +89,12 @@ export default { page: page + 1, }, }); - return data?.map(({ id }) => ({ - value: id, - label: `${id}`, - })) || []; + return ( + data?.map(({ id }) => ({ + value: id, + label: `${id}`, + })) || [] + ); }, }, customerID: { @@ -78,13 +106,51 @@ export default { params: { page: page + 1, }, - }); console.log(data); - return data?.map(({ - id: value, email: label, - }) => ({ - label, - value, - })) || []; + }); + return ( + data?.map(({ + id: value, email: label, + }) => ({ + label, + value, + })) || [] + ); + }, + }, + authorization_code: { + type: "string", + label: "Authorization Code", + description: + "Authorization code to charge. This is created whenever a customer makes a payment on your integration", + async options({ customer }) { + const { data } = await this.fetchCustomer({ + customer, + }); + const authorizations = data?.authorizations || []; + return authorizations + .filter(({ reusable }) => reusable) + .map((authorization) => { + const { + bank, + channel, + card_type, + last4, + exp_month, + exp_year, + authorization_code, + } = authorization; + if (channel === "card") { + return { + label: ` ${bank} ${card_type} card ending in ${last4} (expires ${exp_month}/${exp_year})`, + value: authorization_code, + }; + } else if (channel === "direct_debit") { + return { + label: `${bank} account ending in (${last4})`, + value: authorization_code, + }; + } + }); }, }, from: { @@ -159,6 +225,21 @@ export default { ...args, }); }, + fetchCustomer({ + customer, ...args + }) { + return this._makeRequest({ + path: `/customer/${customer}`, + args, + }); + }, + chargeAuthorization(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/transaction/charge_authorization", + ...args, + }); + }, async *paginate({ resourceFn, args, max, }) { @@ -170,7 +251,8 @@ export default { page: 1, }, }; - let total, count = 0; + let total, + count = 0; do { const { data } = await resourceFn(args); for (const item of data) { diff --git a/components/paytrace/README.md b/components/paytrace/README.md index 047eff3c8e345..3f2ff7f568e98 100644 --- a/components/paytrace/README.md +++ b/components/paytrace/README.md @@ -1,31 +1,11 @@ # Overview -## Introduction to PayTrace API +The PayTrace API offers a robust suite of payment processing tools for developers aiming to build seamless payment integrations. With it, you can automate credit card transactions, manage customer profiles, and generate detailed reports, among other functionalities. Leveraging the PayTrace API on Pipedream allows you to create workflows that interact with other services like CRMs, accounting software, and ecommerce platforms, facilitating tasks like sales automation, financial reconciliation, and data synchronization. -PayTrace is an API designed to help small and mid-sized businesses process -credit card payments securely and efficiently. PayTrace offers a variety of -services in order to help businesses process payments with ease, such as online -payment processing and recurring billing. +# Example Use Cases -With the PayTrace API, users can easily link their applications to the PayTrace -service and make use of the payment processing and security features. Now, -let’s take a look at some of the things businesses can build with the API. +- **Real-time Sales Notification**: Send a Slack message or an email via SendGrid whenever a new transaction is processed through PayTrace. This keeps sales teams informed of new deals in real time. -## Examples of Things to Build +- **Customer Onboarding**: After a successful transaction, automatically create a customer profile in HubSpot or Salesforce, ensuring your CRM records are always up-to-date with the latest transaction data. -- Point of Sale Solutions - Create a fully integrated point of sale system that - will allow customers to make payments quickly and securely. -- Secure eCommerce websites - Build a secure and reliable way for customers to - purchase products from an online store. -- Invoice Automation Systems - Create an automated system for generating, - sending, and managing invoices for businesses. -- Payment Gateways - Create payment gateway solutions for businesses to - securely process payments on their websites. -- Recurring Payments - Allow businesses to easily setup and manage recurring - payments for their customers. -- Security Features - Implement various security features to keep customer’s - information safe and secure. -- Reporting Tools - Create tools to analyze and generate reports of payment - information for businesses. -- Mobile Payment Solutions - Create mobile payment solutions for businesses to - accept payments on the go. +- **Ecommerce Synchronization**: On a new payment confirmation from PayTrace, trigger an inventory update in a platform like Shopify or WooCommerce to maintain accurate stock levels, and simultaneously log the transaction in QuickBooks for financial tracking. diff --git a/components/pcloud/README.md b/components/pcloud/README.md index 4448693daf6ca..5b0c7948e859e 100644 --- a/components/pcloud/README.md +++ b/components/pcloud/README.md @@ -1,24 +1,11 @@ # Overview -The pCloud API allows you to build a variety of products and applications that -integrate with the pCloud service. With the pCloud API, you can get access to a -wide range of features, including: +The pCloud API allows for direct interaction with your pCloud account, providing access to files and folders within your cloud storage. With Pipedream, you can automate file management tasks such as uploading, downloading, and synchronizing files. Additionally, you can create workflows to organize your cloud storage, share files with team members, or back up important data from various sources. -- Storage for cloud-based file sharing -- Automated synchronization of content -- Content and account management -- Automation of user-related tasks -- Programmatic access to all pCloud services +# Example Use Cases -With pCloud, you can easily build: +- **Automated Backup from GitHub to pCloud**: Upon a new GitHub release, Pipedream can trigger a workflow that archives the repository and uploads the zip file to a designated pCloud folder. This ensures your codebase is backed up at every significant milestone. -- Web and mobile applications -- Cloud-based backup & storage services -- Content management solutions -- User authentication & identities -- File collaboration & sharing -- Automated media streaming -- Photo & video sharing services -- Online Photo & Video Editor -- Business process automation -- Business intelligence & analytics +- **Image Sync with Social Media**: When a new photo is posted to your Instagram account, Pipedream can automatically save a copy to a specific pCloud album. This workflow keeps a cloud backup of your social media imagery without manual intervention. + +- **Receipt Collection from Email**: Pipedream can monitor your email inbox for messages containing receipts and save the attachments directly to a pCloud folder. This can be particularly useful for expense tracking and reporting. diff --git a/components/pdf_api_io/package.json b/components/pdf_api_io/package.json new file mode 100644 index 0000000000000..72ee72b163005 --- /dev/null +++ b/components/pdf_api_io/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/pdf_api_io", + "version": "0.0.1", + "description": "Pipedream PDF-API.io Components", + "main": "pdf_api_io.app.mjs", + "keywords": [ + "pipedream", + "pdf_api_io" + ], + "homepage": "https://pipedream.com/apps/pdf_api_io", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/pdf_api_io/pdf_api_io.app.mjs b/components/pdf_api_io/pdf_api_io.app.mjs new file mode 100644 index 0000000000000..12797b170f90a --- /dev/null +++ b/components/pdf_api_io/pdf_api_io.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "pdf_api_io", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/pdf_app_net/actions/compress-pdf/compress-pdf.mjs b/components/pdf_app_net/actions/compress-pdf/compress-pdf.mjs new file mode 100644 index 0000000000000..6b129f6511fba --- /dev/null +++ b/components/pdf_app_net/actions/compress-pdf/compress-pdf.mjs @@ -0,0 +1,35 @@ +import pdfApp from "../../pdf_app_net.app.mjs"; + +export default { + key: "pdf_app_net-compress-pdf", + name: "Compress PDF", + description: "Compress a PDF File with PDF-app.net. [See the documentation](https://pdf-app.net/apidocumentation)", + version: "0.0.1", + type: "action", + props: { + pdfApp, + fileUrl: { + type: "string", + label: "File URL", + description: "The URL of a .pdf file to compress", + }, + fileName: { + propDefinition: [ + pdfApp, + "fileName", + ], + }, + }, + async run({ $ }) { + const response = await this.pdfApp.compressPdf({ + $, + data: { + fileUrl: this.fileUrl, + async: false, + fileName: this.fileName, + }, + }); + $.export("$summary", "Successfully compressed PDF File"); + return response; + }, +}; diff --git a/components/pdf_app_net/actions/image-to-pdf/image-to-pdf.mjs b/components/pdf_app_net/actions/image-to-pdf/image-to-pdf.mjs new file mode 100644 index 0000000000000..6dc818dea2371 --- /dev/null +++ b/components/pdf_app_net/actions/image-to-pdf/image-to-pdf.mjs @@ -0,0 +1,37 @@ +import pdfApp from "../../pdf_app_net.app.mjs"; + +export default { + key: "pdf_app_net-image-to-pdf", + name: "Image to PDF", + description: "Convert an image from a URL to a PDF File with PDF-app.net. [See the documentation](https://pdf-app.net/apidocumentation)", + version: "0.0.1", + type: "action", + props: { + pdfApp, + imageUrl: { + type: "string", + label: "Image URL", + description: "The URL of an image file to convert", + }, + fileName: { + propDefinition: [ + pdfApp, + "fileName", + ], + }, + }, + async run({ $ }) { + const response = await this.pdfApp.imageToPdf({ + $, + data: { + imageUrls: [ + this.imageUrl, + ], + async: false, + fileName: this.fileName, + }, + }); + $.export("$summary", "Successfully converted image to PDF File"); + return response; + }, +}; diff --git a/components/pdf_app_net/actions/split-pdf/split-pdf.mjs b/components/pdf_app_net/actions/split-pdf/split-pdf.mjs new file mode 100644 index 0000000000000..765f73f8bb154 --- /dev/null +++ b/components/pdf_app_net/actions/split-pdf/split-pdf.mjs @@ -0,0 +1,41 @@ +import pdfApp from "../../pdf_app_net.app.mjs"; + +export default { + key: "pdf_app_net-split-pdf", + name: "Split PDF", + description: "Split a PDF into multiple PDFs containing the specified number of pages. [See the documentation](https://pdf-app.net/apidocumentation)", + version: "0.0.1", + type: "action", + props: { + pdfApp, + fileUrl: { + type: "string", + label: "File URL", + description: "The URL of a .pdf file to split", + }, + pagesPerSplit: { + type: "integer", + label: "Pages", + description: "Each PDF will contain the specified number of pages. For instance, if the original PDF contains 6 pages, and `Pages` is `2`, the result will be 3 files, each containing 2 pages.", + }, + fileName: { + propDefinition: [ + pdfApp, + "fileName", + ], + }, + }, + async run({ $ }) { + const response = await this.pdfApp.splitPdf({ + $, + data: { + fileUrl: this.fileUrl, + pagesPerSplit: this.pagesPerSplit, + async: false, + fileName: this.fileName, + }, + }); + $.export("$summary", "Successfully split PDF File"); + return response; + }, +}; diff --git a/components/pdf_app_net/package.json b/components/pdf_app_net/package.json new file mode 100644 index 0000000000000..a36082cf96c5a --- /dev/null +++ b/components/pdf_app_net/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/pdf_app_net", + "version": "0.1.0", + "description": "Pipedream PDF-app.net Components", + "main": "pdf_app_net.app.mjs", + "keywords": [ + "pipedream", + "pdf_app_net" + ], + "homepage": "https://pipedream.com/apps/pdf_app_net", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/pdf_app_net/pdf_app_net.app.mjs b/components/pdf_app_net/pdf_app_net.app.mjs new file mode 100644 index 0000000000000..0fd7c75e8ed56 --- /dev/null +++ b/components/pdf_app_net/pdf_app_net.app.mjs @@ -0,0 +1,51 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "pdf_app_net", + propDefinitions: { + fileName: { + type: "string", + label: "File Name", + description: "The name of the file", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.pdf-app.net"; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + method: "POST", + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `${this.$auth.api_key}`, + }, + ...opts, + }); + }, + compressPdf(opts = {}) { + return this._makeRequest({ + path: "/compress_PDF", + ...opts, + }); + }, + imageToPdf(opts = {}) { + return this._makeRequest({ + path: "/image_to_pdf", + ...opts, + }); + }, + splitPdf(opts = {}) { + return this._makeRequest({ + path: "/splitt_PDF", + ...opts, + }); + }, + }, +}; diff --git a/components/pdf_charts/README.md b/components/pdf_charts/README.md new file mode 100644 index 0000000000000..9c36c16ec8654 --- /dev/null +++ b/components/pdf_charts/README.md @@ -0,0 +1,11 @@ +# Overview + +The PDF Charts API allows users to create high-quality, customizable PDF documents featuring various chart types directly from data. This API is especially valuable in automated reporting systems, data visualization for analytics, and dynamically generated financial or operational reports. By integrating this API with Pipedream, you can automate the generation and distribution of these documents based on triggers from numerous other apps and data sources, streamlining processes like monthly sales reporting, customer analytics, or resource management. + +# Example Use Cases + +- **Automated Monthly Sales Reports**: Automatically generate a PDF sales report at the end of each month using data from Salesforce. The PDF Charts API can be used to create visualizations of sales trends, revenue by product, and other key metrics, which are then compiled into a PDF and emailed to stakeholders via SendGrid. + +- **Real-Time Analytics Dashboard PDF Generation**: Trigger a PDF generation whenever a Google Sheets spreadsheet is updated with new data. Use the PDF Charts API to convert these updates into a series of charts, providing stakeholders with a visual, real-time analytics dashboard they can view offline. + +- **Resource Management for Project Teams**: On a weekly basis, automatically pull data from a project management tool like Asana or Monday.com, use the PDF Charts API to create visual representations of resource allocation and project progress, and distribute the report via Slack to keep the entire team informed. diff --git a/components/pdf_charts/actions/create-pdf/create-pdf.mjs b/components/pdf_charts/actions/create-pdf/create-pdf.mjs new file mode 100644 index 0000000000000..8fd28231c3e0d --- /dev/null +++ b/components/pdf_charts/actions/create-pdf/create-pdf.mjs @@ -0,0 +1,43 @@ +import pdfCharts from "../../pdf_charts.app.mjs"; + +export default { + key: "pdf_charts-create-pdf", + name: "Create PDF", + description: "Create a PDF document using PDF Charts. [See the documentation](https://www.pdf-charts.com/playground)", + version: "0.0.1", + type: "action", + props: { + pdfCharts, + documentId: { + type: "string", + label: "Document ID", + description: "ID of the document for PDF creation", + }, + data: { + type: "object", + label: "Data", + description: "Values for elements in the document. See the PDF Charts [Playground](https://www.pdf-charts.com/playground) for example data.", + }, + alert: { + type: "alert", + alertType: "info", + content: `Data Example: + \n\`{"day":"Monday","item1713508000534":{"title":{"x":"center","text":"Sample Bar Chart"},"xAxis":{"data":["Category 1","Category 2","Category 3","Category 4","Category 5"],"type":"category","axisTick":{"show":false} },"yAxis":{"type":"value"},"series":[{"data":[{"value":20},{"value":200},{"value":150},{"value":80},{"value":70}],"name":"Data","type":"bar"}]} }\` + \n\n Note: To find the correct element id(s) (ex. item1713508000534), select the document in PDF Charts and click the edit icon.`, + }, + }, + async run({ $ }) { + const response = await this.pdfCharts.createPdf({ + $, + data: { + documentId: this.documentId, + data: typeof this.data === "string" + ? JSON.parse(this.data) + : this.data, + output: "json", + }, + }); + $.export("$summary", `Successfully created PDF: ${response.signedUrl}`); + return response; + }, +}; diff --git a/components/pdf_charts/package.json b/components/pdf_charts/package.json new file mode 100644 index 0000000000000..cb5979953aabf --- /dev/null +++ b/components/pdf_charts/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/pdf_charts", + "version": "0.1.0", + "description": "Pipedream PDF Charts Components", + "main": "pdf_charts.app.mjs", + "keywords": [ + "pipedream", + "pdf_charts" + ], + "homepage": "https://pipedream.com/apps/pdf_charts", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.3" + } +} diff --git a/components/pdf_charts/pdf_charts.app.mjs b/components/pdf_charts/pdf_charts.app.mjs new file mode 100644 index 0000000000000..d4d3ecd1b97cf --- /dev/null +++ b/components/pdf_charts/pdf_charts.app.mjs @@ -0,0 +1,40 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "pdf_charts", + propDefinitions: {}, + methods: { + _baseUrl() { + return "https://api.pdf-charts.com"; + }, + _auth(data) { + return { + ...data, + apiKey: this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, + path, + data, + ...args + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "Content-Type": "application/json", + }, + data: this._auth(data), + ...args, + }); + }, + createPdf(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/make/sync", + ...args, + }); + }, + }, +}; diff --git a/components/pdf_co/README.md b/components/pdf_co/README.md index 0e6143ed96d6c..1aaeb507db277 100644 --- a/components/pdf_co/README.md +++ b/components/pdf_co/README.md @@ -1,18 +1,11 @@ # Overview -PDF.co is an API that allows you to build applications using PDFs in a variety -of ways. With PDF.co, you can create, edit, store, and analyze PDFs for every -purpose. +PDF.co API on Pipedream opens up opportunities for automating document handling tasks. You can create PDFs from scratch, merge multiple documents, extract text or data, convert PDFs to different formats, and even perform complex operations like filling out forms programmatically. With Pipedream's serverless platform, these capabilities can be integrated into workflows that respond to events, schedule tasks, or trigger on specific conditions, streamlining processes that involve PDF manipulation. -Here are just a few of the things you can create with PDF.co: +# Example Use Cases -- Create e-signature workflows -- Convert Word and Excel documents to PDF -- Create PDF forms -- Split and combine PDF pages -- Extract data from PDF documents -- Automate PDF processing tasks -- Generate barcodes -- Annotate pages -- Merge PDFs -- Create searchable PDFs +- **Data Extraction Workflow**: Extract text, tables, and form data from PDFs and feed them into a database like PostgreSQL. When a PDF is uploaded to Dropbox, the workflow triggers, processes the PDF using PDF.co to extract data, and then inserts the data into PostgreSQL for further analysis or reporting. + +- **Invoice Processing Workflow**: Automate invoice generation by triggering a workflow whenever a new sale is recorded in Shopify. The workflow uses PDF.co to generate a PDF invoice, which is then emailed to the customer using the SendGrid Pipedream component, ensuring a seamless billing process. + +- **Document Conversion and Archive Workflow**: Convert a batch of documents from Google Drive from various formats to PDFs, then compress and archive them in Amazon S3. A workflow monitors Google Drive for new files, uses PDF.co for conversion and compression, and then uploads the optimized files to S3, making it easier to manage document storage efficiently. diff --git a/components/pdffiller/actions/create-distributable-form-generate-link/create-distributable-form-generate-link.mjs b/components/pdffiller/actions/create-distributable-form-generate-link/create-distributable-form-generate-link.mjs new file mode 100644 index 0000000000000..1a1ee13b52228 --- /dev/null +++ b/components/pdffiller/actions/create-distributable-form-generate-link/create-distributable-form-generate-link.mjs @@ -0,0 +1,105 @@ +import { + ACCESS_OPTIONS, STATUS_OPTIONS, +} from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import pdffiller from "../../pdffiller.app.mjs"; + +export default { + key: "pdffiller-create-distributable-form-generate-link", + name: "Create Distributable Form and Generate Link", + description: "Transforms a document into a fillable form and generates a shareable link for the form. [See the documentation](https://docs.pdffiller.com/docs/pdffiller/9d3a06696db96-create-fillable-document-converts-a-downloaded-document-to-a-link-to-fill-form)", + version: "0.0.2", + type: "action", + props: { + pdffiller, + documentId: { + propDefinition: [ + pdffiller, + "documentId", + ], + }, + access: { + type: "string", + label: "Access", + description: "Access level for the fill request document.", + options: ACCESS_OPTIONS, + }, + status: { + type: "string", + label: "Status", + description: "Document access permission.", + options: STATUS_OPTIONS, + }, + nameRequired: { + type: "boolean", + label: "Name Required", + description: "Name required", + default: false, + }, + emailRequired: { + type: "boolean", + label: "Email Required", + description: "Email required", + default: false, + }, + enforceRequiredFields: { + type: "boolean", + label: "Enforce Required Fields", + description: "Prevent closing document before filling all fields.", + default: false, + }, + allowDownloads: { + type: "boolean", + label: "Allow Downloads", + description: "Allow to download", + default: false, + }, + redirectUrl: { + type: "string", + label: "Redirect URL", + description: "Redirect to URL after complete.", + optional: true, + }, + customMessage: { + type: "string", + label: "Custom Message", + description: "LinkToFill custom message.", + optional: true, + }, + notificationEmails: { + type: "string[]", + label: "Notification Emails", + description: "LinkToFill notification emails.", + optional: true, + }, + additionalDocuments: { + type: "string[]", + label: "Additional Documents", + description: "Additional documents required after filling the document. Max count 5.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.pdffiller.createFillableDocument({ + $, + data: { + document_id: this.documentId, + access: this.access, + status: this.status, + name_required: this.nameRequired, + email_required: this.emailRequired, + enforce_required_fields: this.enforceRequiredFields, + allow_downloads: this.allowDownloads, + redirect_url: this.redirectUrl, + notification_emails: parseObject(this.notificationEmails)?.map((email) => ({ + email, + })), + additional_documents: this.additionalDocuments, + custom_message: this.customMessage, + }, + }); + + $.export("$summary", `Successfully created a fillable form and generated a shareable link for document ID ${this.fillableFormId}`); + return response; + }, +}; diff --git a/components/pdffiller/actions/find-document/find-document.mjs b/components/pdffiller/actions/find-document/find-document.mjs new file mode 100644 index 0000000000000..388ac03c310ec --- /dev/null +++ b/components/pdffiller/actions/find-document/find-document.mjs @@ -0,0 +1,47 @@ +import pdffiller from "../../pdffiller.app.mjs"; + +export default { + key: "pdffiller-find-document", + name: "Find Document", + description: "Enables searching capabilities for documents by name. [See the documentation](https://pdffiller.readme.io/reference/get_v2-templates)", + version: "0.0.2", + type: "action", + props: { + pdffiller, + alert: { + type: "alert", + alertType: "info", + content: "You can use exact name of a file or with asterisks. For example, file*.doc will match file1.doc and file_test.pdf.", + }, + name: { + propDefinition: [ + pdffiller, + "name", + ], + }, + folderId: { + propDefinition: [ + pdffiller, + "folderId", + ], + optional: true, + }, + }, + async run({ $ }) { + const responseTemp = this.pdffiller.paginate({ + fn: this.pdffiller.listDocuments, + params: { + folder_id: this.folderId, + name: this.name, + }, + }); + + const responseArray = []; + for await (const item of responseTemp) { + responseArray.push(item); + } + + $.export("$summary", `Found ${responseArray.length} document(s) with the name "${this.name}"`); + return responseArray; + }, +}; diff --git a/components/pdffiller/actions/upload-document/upload-document.mjs b/components/pdffiller/actions/upload-document/upload-document.mjs new file mode 100644 index 0000000000000..c86f0521cc229 --- /dev/null +++ b/components/pdffiller/actions/upload-document/upload-document.mjs @@ -0,0 +1,45 @@ +import FormData from "form-data"; +import fs from "fs"; +import { checkTmp } from "../../common/utils.mjs"; +import pdffiller from "../../pdffiller.app.mjs"; + +export default { + key: "pdffiller-upload-document", + name: "Upload Document", + description: "Uploads a chosen file to PDFfiller. [See the documentation](https://docs.pdffiller.com/docs/pdffiller/992d9d79fec32-creates-a-new-document-template-by-uploading-file-from-multipart)", + version: "0.0.2", + type: "action", + props: { + pdffiller, + file: { + propDefinition: [ + pdffiller, + "file", + ], + }, + folderId: { + propDefinition: [ + pdffiller, + "folderId", + ], + optional: true, + }, + }, + async run({ $ }) { + const fileStream = fs.createReadStream(checkTmp(this.file)); + const data = new FormData(); + data.append("file", fileStream); + + if (this.folderId) { + data.append("folder_id", this.folderId); + } + + const response = await this.pdffiller.uploadFile({ + $, + data, + headers: data.getHeaders(), + }); + $.export("$summary", `Successfully uploaded document with ID ${response.id}`); + return response; + }, +}; diff --git a/components/pdffiller/common/constants.mjs b/components/pdffiller/common/constants.mjs new file mode 100644 index 0000000000000..5b02f8f0aa364 --- /dev/null +++ b/components/pdffiller/common/constants.mjs @@ -0,0 +1,15 @@ +export const ACCESS_OPTIONS = [ + { + label: "Full - read|write|sign.", + value: "full", + }, + { + label: "Signature - read|sign.", + value: "signature", + }, +]; + +export const STATUS_OPTIONS = [ + "public", + "private", +]; diff --git a/components/pdffiller/common/utils.mjs b/components/pdffiller/common/utils.mjs new file mode 100644 index 0000000000000..0cd1a12b6a4ba --- /dev/null +++ b/components/pdffiller/common/utils.mjs @@ -0,0 +1,31 @@ +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; + +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/pdffiller/package.json b/components/pdffiller/package.json new file mode 100644 index 0000000000000..5c0d1928b31d7 --- /dev/null +++ b/components/pdffiller/package.json @@ -0,0 +1,20 @@ +{ + "name": "@pipedream/pdffiller", + "version": "0.1.1", + "description": "Pipedream pdfFiller Components", + "main": "pdffiller.app.mjs", + "keywords": [ + "pipedream", + "pdffiller" + ], + "homepage": "https://pipedream.com/apps/pdffiller", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0", + "form-data": "^4.0.0", + "fs": "^0.0.1-security" + } +} diff --git a/components/pdffiller/pdffiller.app.mjs b/components/pdffiller/pdffiller.app.mjs new file mode 100644 index 0000000000000..45defe2ce0b61 --- /dev/null +++ b/components/pdffiller/pdffiller.app.mjs @@ -0,0 +1,170 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "pdffiller", + propDefinitions: { + documentId: { + type: "string", + label: "Document ID", + description: "The ID of the document template.", + async options({ page }) { + const { items } = await this.listDocuments({ + params: { + page: page + 1, + }, + }); + + return items.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + fillableFormId: { + type: "string", + label: "Fillable Form ID", + description: "The ID of the fillable form.", + async options({ page }) { + const { items } = await this.listFillableForms({ + params: { + page: page + 1, + }, + }); + + return items.map(({ + fillable_form_id: value, document_name: label, + }) => ({ + label, + value, + })); + }, + }, + file: { + type: "string", + label: "File", + description: "The path to the file saved to the `/tmp` directory (e.g. `/tmp/example.json`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + }, + folderId: { + type: "string", + label: "Folder Id", + description: "The id of the folder.", + async options({ page }) { + const { items } = await this.listFolders({ + params: { + page: page + 1, + }, + }); + + return items.map(({ + folder_id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + name: { + type: "string", + label: "Name", + description: "The name of the document to be searched", + }, + }, + methods: { + _baseUrl() { + return "https://api.pdffiller.com/v2"; + }, + _headers(headers = {}) { + return { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(headers), + ...opts, + }); + }, + listDocuments(opts = {}) { + return this._makeRequest({ + method: "GET", + path: "/templates", + ...opts, + }); + }, + listFillableForms(opts = {}) { + return this._makeRequest({ + method: "GET", + path: "/fillable_forms", + ...opts, + }); + }, + listFolders(opts = {}) { + return this._makeRequest({ + path: "/folders", + ...opts, + }); + }, + createFillableDocument(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/fillable_forms", + ...opts, + }); + }, + uploadFile(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/templates", + ...opts, + }); + }, + createCallback(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/callbacks", + ...opts, + }); + }, + deleteCallback(callbackId) { + return this._makeRequest({ + method: "DELETE", + path: `/callbacks/${callbackId}`, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const { + items, + next_page_url, + } = await fn({ + params, + ...opts, + }); + for (const d of items) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = next_page_url; + + } while (hasMore); + }, + }, +}; diff --git a/components/pdffiller/sources/common/base-polling.mjs b/components/pdffiller/sources/common/base-polling.mjs new file mode 100644 index 0000000000000..ea9b5f10c2c9b --- /dev/null +++ b/components/pdffiller/sources/common/base-polling.mjs @@ -0,0 +1,60 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import pdffiller from "../../pdffiller.app.mjs"; + +export default { + props: { + pdffiller, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const response = this.pdffiller.paginate({ + fn: this.getFunction(), + maxResults, + params: { + order: "desc", + order_by: "id", + }, + }); + + let responseArray = []; + for await (const item of response) { + if (item.created <= lastDate) break; + responseArray.push(item); + } + + if (responseArray.length) { + this._setLastDate(responseArray[0].created); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: item.created, + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/pdffiller/sources/common/base-webhook.mjs b/components/pdffiller/sources/common/base-webhook.mjs new file mode 100644 index 0000000000000..fd3b7edb5a932 --- /dev/null +++ b/components/pdffiller/sources/common/base-webhook.mjs @@ -0,0 +1,50 @@ +import pdffiller from "../../pdffiller.app.mjs"; + +export default { + props: { + pdffiller, + http: { + type: "$.interface.http", + customResponse: false, + }, + db: "$.service.db", + fillableFormId: { + propDefinition: [ + pdffiller, + "fillableFormId", + ], + }, + }, + methods: { + _setCallbackId(callbackId) { + this.db.set("callbackId", callbackId); + }, + _getCallbackId() { + return this.db.get("callbackId"); + }, + }, + hooks: { + async activate() { + const data = await this.pdffiller.createCallback({ + data: { + document_id: this.fillableFormId, + event_id: this.getEventId(), + callback_url: this.http.endpoint, + }, + }); + this._setCallbackId(data.id); + }, + async deactivate() { + const callbackId = this._getCallbackId(); + await this.pdffiller.deleteCallback(callbackId); + }, + }, + async run({ body }) { + const ts = Date.parse(new Date()); + this.$emit(body, { + id: `${body.resource}-${ts}`, + summary: this.getSummary(body), + ts: ts, + }); + }, +}; diff --git a/components/pdffiller/sources/new-document/new-document.mjs b/components/pdffiller/sources/new-document/new-document.mjs new file mode 100644 index 0000000000000..c432bb958d81e --- /dev/null +++ b/components/pdffiller/sources/new-document/new-document.mjs @@ -0,0 +1,22 @@ +import common from "../common/base-polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "pdffiller-new-document", + name: "New Document Created", + description: "Emit new event when a new document is uploaded or created.", + version: "0.0.2", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.pdffiller.listDocuments; + }, + getSummary(item) { + return `New Document: ${item.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/pdffiller/sources/new-document/test-event.mjs b/components/pdffiller/sources/new-document/test-event.mjs new file mode 100644 index 0000000000000..664d9d42a6268 --- /dev/null +++ b/components/pdffiller/sources/new-document/test-event.mjs @@ -0,0 +1,13 @@ +export default { + "id": 1234567890, + "name": "file.pdf", + "type": "pdf", + "status": "FINISHED", + "created": 1722351361, + "updated": 1722351377, + "fillable": false, + "folder": { + "folder_id": 0, + "name": "Unsorted" + } +} \ No newline at end of file diff --git a/components/pdffiller/sources/new-filled-form-instant/new-filled-form-instant.mjs b/components/pdffiller/sources/new-filled-form-instant/new-filled-form-instant.mjs new file mode 100644 index 0000000000000..8764089e136a7 --- /dev/null +++ b/components/pdffiller/sources/new-filled-form-instant/new-filled-form-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base-webhook.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "pdffiller-new-filled-form-instant", + name: "New Filled Form (Instant)", + description: "Emit new event when a form is filled out.", + version: "0.0.2", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventId() { + return "fill_request.done"; + }, + getSummary(body) { + return `New filled form with ID: ${body["event_data[fill_request][filled_form][id]"]}`; + }, + }, + sampleEmit, +}; diff --git a/components/pdffiller/sources/new-filled-form-instant/test-event.mjs b/components/pdffiller/sources/new-filled-form-instant/test-event.mjs new file mode 100644 index 0000000000000..0db246f5d35b8 --- /dev/null +++ b/components/pdffiller/sources/new-filled-form-instant/test-event.mjs @@ -0,0 +1,6 @@ +export default { + "event": "fill_request.done", + "document_id": "1234567890", + "event_data[fill_request][filled_form][id]": "123456", + "event_data[fill_request][filled_form][document_id]": "1234567890" +} \ No newline at end of file diff --git a/components/pdfless/README.md b/components/pdfless/README.md new file mode 100644 index 0000000000000..81f21501aa6b8 --- /dev/null +++ b/components/pdfless/README.md @@ -0,0 +1,11 @@ +# Overview + +The Pdfless API provides a suite of tools for creating and manipulating PDF documents, enabling users to generate PDFs from HTML, merge multiple PDFs into one, split a PDF into separate files, and convert PDFs to other formats. By incorporating the Pdfless API into Pipedream workflows, you can automate document processing tasks, streamline data extraction, and integrate with other services to manage documents as part of larger business processes. + +# Example Use Cases + +- **Generate Invoices from E-commerce Orders**: Automate the creation of invoices by triggering a workflow whenever a new order is placed in an e-commerce platform like Shopify. Use Pdfless to convert HTML invoice templates into PDFs, populated with order details fetched from Shopify, and then email the invoice directly to the customer. + +- **Merge Monthly Reports into a Single PDF**: Collect monthly reports from various sources like Google Sheets or Dropbox. Use Pdfless to combine these individual documents into a single PDF file, which can then be stored back in Dropbox or sent via email to stakeholders for review. + +- **Convert Submitted Forms to Editable Formats**: When users submit forms through a web service like Typeform, trigger a Pipedream workflow to process the data. Leverage Pdfless to convert the submitted information into a PDF, and then to a format like DOCX, making it easy for team members to review and edit the content as needed. diff --git a/components/pdfmonkey/.gitignore b/components/pdfmonkey/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/pdfmonkey/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/pdfmonkey/README.md b/components/pdfmonkey/README.md index ddb5ad36dd3e2..d84c555e4fc1f 100644 --- a/components/pdfmonkey/README.md +++ b/components/pdfmonkey/README.md @@ -1,22 +1,11 @@ # Overview -PDFMonkey is an API that enables users to build powerful applications that -generate and manipulate PDF files. The API allows developers to generate and -manipulate documents such as modifying texts, adding images, creating form -fields and content, as well as signing and securing documents. +The PDFMonkey API on Pipedream allows you to automate the creation of PDF documents from dynamic data sources. You can generate invoices, reports, tickets, or any customized document based on templates you define. With Pipedream’s serverless platform, you can trigger PDF generation from a multitude of events, such as form submissions, scheduled times, or changes in a database, and then perform actions like sending these PDFs via email, storing them in cloud storage, or updating records in a CRM. -Below are some of the great products you can build using the PDFMonkey API: +# Example Use Cases -- Create custom invoices -- Create contact sheets -- Generate PDFs from HTML documents -- Convert HTML to PDF -- Print web pages -- Merge multiple PDF documents into one -- Extract text from PDFs -- Redact confidential information from PDFs -- Generate dynamic PDFs for web applications -- Sign PDF documents -- Verify a digital signature -- Crop PDF pages -- Extract images from PDF documents +- **Invoice Generation Workflow**: Automate the creation of invoices by triggering a workflow on Pipedream whenever a new order is placed in an eCommerce platform like Shopify. The workflow can generate a PDF invoice using PDFMonkey, and then send it to the customer through an email service like SendGrid, all without manual intervention. + +- **Monthly Reports Automation**: Set up a scheduled workflow in Pipedream to generate monthly performance reports. Connect to a data source like Google Sheets or a SQL database to retrieve the data, use PDFMonkey to create a polished PDF report, and then distribute it to stakeholders through Dropbox or email, ensuring consistent and timely reporting. + +- **Event Ticketing System**: Whenever a user registers for an event through a platform like Eventbrite, trigger a workflow on Pipedream that uses PDFMonkey to generate a personalized event ticket. The ticket can then be sent automatically to the registrant via an email delivery service, or stored in a database for later retrieval. diff --git a/components/pdfmonkey/actions/delete-document/delete-document.mjs b/components/pdfmonkey/actions/delete-document/delete-document.mjs new file mode 100644 index 0000000000000..413d3b55f2ad1 --- /dev/null +++ b/components/pdfmonkey/actions/delete-document/delete-document.mjs @@ -0,0 +1,26 @@ +import pdfmonkey from "../../pdfmonkey.app.mjs"; + +export default { + key: "pdfmonkey-delete-document", + name: "Delete Document", + description: "Deletes a specific document using its ID. [See the documentation](https://docs.pdfmonkey.io/references/api/documents)", + version: "0.0.1", + type: "action", + props: { + pdfmonkey, + documentId: { + propDefinition: [ + pdfmonkey, + "documentId", + ], + }, + }, + async run({ $ }) { + const response = await this.pdfmonkey.deleteDocument({ + $, + documentId: this.documentId, + }); + $.export("$summary", `Deleted document with ID ${this.documentId}`); + return response; + }, +}; diff --git a/components/pdfmonkey/actions/find-document/find-document.mjs b/components/pdfmonkey/actions/find-document/find-document.mjs new file mode 100644 index 0000000000000..7034927aaaea2 --- /dev/null +++ b/components/pdfmonkey/actions/find-document/find-document.mjs @@ -0,0 +1,26 @@ +import pdfmonkey from "../../pdfmonkey.app.mjs"; + +export default { + key: "pdfmonkey-find-document", + name: "Find Document", + description: "Find a document within PDFMonkey. [See the documentation](https://docs.pdfmonkey.io/references/api/documents)", + version: "0.0.1", + type: "action", + props: { + pdfmonkey, + documentId: { + propDefinition: [ + pdfmonkey, + "documentId", + ], + }, + }, + async run({ $ }) { + const document = await this.pdfmonkey.getDocument({ + $, + documentId: this.documentId, + }); + $.export("$summary", `Successfully found document with ID ${this.documentId}`); + return document; + }, +}; diff --git a/components/pdfmonkey/actions/generate-document/generate-document.mjs b/components/pdfmonkey/actions/generate-document/generate-document.mjs new file mode 100644 index 0000000000000..d4a17729926f6 --- /dev/null +++ b/components/pdfmonkey/actions/generate-document/generate-document.mjs @@ -0,0 +1,54 @@ +import { STATUS_OPTIONS } from "../../common/constants.mjs"; +import pdfmonkey from "../../pdfmonkey.app.mjs"; + +export default { + key: "pdfmonkey-generate-document", + name: "Generate Document", + description: "Generates a new document using a specified template. [See the documentation](https://docs.pdfmonkey.io/references/api/documents)", + version: "0.0.1", + type: "action", + props: { + pdfmonkey, + templateId: { + propDefinition: [ + pdfmonkey, + "templateId", + ], + }, + payload: { + type: "object", + label: "Payload", + description: "Data to use for the Document generation.", + optional: true, + }, + meta: { + type: "object", + label: "Meta", + description: "Meta-Data to attach to the Document.", + optional: true, + }, + status: { + type: "string", + label: "Status", + description: "The status of the document", + options: STATUS_OPTIONS, + optional: true, + }, + }, + async run({ $ }) { + const response = await this.pdfmonkey.createDocument({ + $, + data: { + document: { + document_template_id: this.templateId, + payload: this.payload, + meta: this.meta, + status: this.status, + }, + }, + }); + + $.export("$summary", `Successfully generated document with ID ${response.document.id}`); + return response; + }, +}; diff --git a/components/pdfmonkey/app/pdfmonkey.app.ts b/components/pdfmonkey/app/pdfmonkey.app.ts deleted file mode 100644 index 013cfeb73cc1a..0000000000000 --- a/components/pdfmonkey/app/pdfmonkey.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "pdfmonkey", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); diff --git a/components/pdfmonkey/common/constants.mjs b/components/pdfmonkey/common/constants.mjs new file mode 100644 index 0000000000000..b8df6991c7e70 --- /dev/null +++ b/components/pdfmonkey/common/constants.mjs @@ -0,0 +1,10 @@ +export const STATUS_OPTIONS = [ + { + label: "Draft (Default)", + value: "draft", + }, + { + label: "Pending (To trigger generation)", + value: "pending", + }, +]; diff --git a/components/pdfmonkey/package.json b/components/pdfmonkey/package.json index a11e79998f246..b447e4ef5188e 100644 --- a/components/pdfmonkey/package.json +++ b/components/pdfmonkey/package.json @@ -1,18 +1,18 @@ { "name": "@pipedream/pdfmonkey", - "version": "0.0.3", + "version": "0.1.0", "description": "Pipedream PDFMonkey Components", - "main": "dist/app/pdfmonkey.app.mjs", + "main": "pdfmonkey.app.mjs", "keywords": [ "pipedream", "pdfmonkey" ], - "files": [ - "dist" - ], "homepage": "https://pipedream.com/apps/pdfmonkey", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/pdfmonkey/pdfmonkey.app.mjs b/components/pdfmonkey/pdfmonkey.app.mjs new file mode 100644 index 0000000000000..10d52ef699c98 --- /dev/null +++ b/components/pdfmonkey/pdfmonkey.app.mjs @@ -0,0 +1,135 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "pdfmonkey", + propDefinitions: { + templateId: { + type: "string", + label: "Template ID", + description: "The unique identifier of the document template.", + async options({ page }) { + const { document_template_cards: data } = await this.listTemplates({ + params: { + "page[number]": page, + }, + }); + + return data.map(({ + id: value, identifier: label, + }) => ({ + label, + value, + })); + }, + }, + documentId: { + type: "string", + label: "Document ID", + description: "The unique identifier of the document.", + async options({ page }) { + const { document_cards: data } = await this.listDocuments({ + params: { + "page[number]": page, + }, + }); + + return data.map(({ + id: value, filename, + }) => ({ + label: `${value}${filename + ? ` - ${filename}` + : ""}`, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.pdfmonkey.io/api/v1"; + }, + _headers() { + return { + "Authorization": `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createDocument(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/documents", + ...opts, + }); + }, + deleteDocument({ + documentId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/documents/${documentId}`, + ...opts, + }); + }, + getDocument({ + documentId, ...opts + }) { + return this._makeRequest({ + path: `/documents/${documentId}`, + ...opts, + }); + }, + listDocuments(opts = {}) { + return this._makeRequest({ + path: "/document_cards", + ...opts, + }); + }, + listTemplates(opts = {}) { + return this._makeRequest({ + path: "/document_template_cards", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params["page[number]"] = ++page; + + const { + document_cards: data, + meta: { + current_page, total_pages, + }, + } = await fn({ + params, + ...opts, + }); + + for (const d of data) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = current_page < total_pages; + + } while (hasMore); + }, + }, +}; diff --git a/components/pdfmonkey/sources/new-document-generated/new-document-generated.mjs b/components/pdfmonkey/sources/new-document-generated/new-document-generated.mjs new file mode 100644 index 0000000000000..609f808db29b2 --- /dev/null +++ b/components/pdfmonkey/sources/new-document-generated/new-document-generated.mjs @@ -0,0 +1,67 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import pdfmonkey from "../../pdfmonkey.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "pdfmonkey-new-document-generated", + name: "New Document Generated", + description: "Emit new event when a document's generation is completed.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + pdfmonkey, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const response = this.pdfmonkey.paginate({ + fn: this.pdfmonkey.listDocuments, + maxResults, + params: { + "q[status]": "success", + "q[updated_since]": lastDate, + }, + }); + + let responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + if (responseArray.length) { + this._setLastDate(Date.parse(responseArray[0].created_at)); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: `Document ${item.filename || item.id} Generation Completed`, + ts: Date.parse(item.created_at), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, + sampleEmit, +}; diff --git a/components/pdfmonkey/sources/new-document-generated/test-event.mjs b/components/pdfmonkey/sources/new-document-generated/test-event.mjs new file mode 100644 index 0000000000000..050ab7bb9b2ef --- /dev/null +++ b/components/pdfmonkey/sources/new-document-generated/test-event.mjs @@ -0,0 +1,14 @@ +export default { + "id": "11475e57-0334-4ad5-8896-9462a2243957", + "app_id": "c2b67b84-4aac-49ea-bed8-69a15d7a65d3", + "created_at": "2022-04-07T11:01:38.201+02:00", + "document_template_id": "96611e9e-ab03-4ac3-8551-1b485210c892", + "document_template_identifier": "My Awesome Template", + "download_url": "https://pdfmonkey.s3.eu-west-1.amazonaws.com/production/backend/document/11475e57-0334-4ad5-8896-9462a2243957/my-test-document.pdf?response-content-disposition=attachment&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJ2ZTKW4HMOLK63IQ%2F20220406%2Feu-west-1%2Fs3%2Faws4_request&X-Amz-Date=20220407T204150Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=24e3a8c0801ad8d1efd6aaa22d946ee70f5c8d5b55c586f346a094afa5046c77", + "failure_cause": null, + "filename": "my-test-document.pdf", + "meta": "{ \"_filename\":\"my-test-document.pdf\" }", + "public_share_link": "https://files.pdfmonkey.io/share/5CEA8C37-D130-4C19-9E11-72BE2293C82B/my-test-document.pdf", + "status": "success", + "updated_at": "2022-04-03T11:12:56.023+02:00" +} \ No newline at end of file diff --git a/components/peach/actions/create-contact/create-contact.mjs b/components/peach/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..eee0da3abb1aa --- /dev/null +++ b/components/peach/actions/create-contact/create-contact.mjs @@ -0,0 +1,46 @@ +import { clearObj } from "../../common/utils.mjs"; +import peach from "../../peach.app.mjs"; + +export default { + key: "peach-create-contact", + name: "Create Contact", + description: "Creates a contact in your account. [See the documentation](https://peach.apidocumentation.com/reference#tag/contacts/post/subscribers)", + version: "0.0.1", + type: "action", + props: { + peach, + phoneNumber: { + propDefinition: [ + peach, + "phoneNumber", + ], + }, + contactName: { + propDefinition: [ + peach, + "contactName", + ], + optional: true, + }, + contactEmail: { + propDefinition: [ + peach, + "contactEmail", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.peach.createContact({ + $, + data: clearObj({ + name: this.contactName, + email: this.contactEmail, + phone_number: this.phoneNumber, + }), + }); + + $.export("$summary", `Created the contact successfully with waId: ${this.phoneNumber}`); + return response; + }, +}; diff --git a/components/peach/actions/send-template-message/send-template-message.mjs b/components/peach/actions/send-template-message/send-template-message.mjs new file mode 100644 index 0000000000000..b7f4ec0dbd664 --- /dev/null +++ b/components/peach/actions/send-template-message/send-template-message.mjs @@ -0,0 +1,66 @@ +import { clearObj } from "../../common/utils.mjs"; +import peach from "../../peach.app.mjs"; + +export default { + key: "peach-send-template-message", + name: "Send Template Message", + description: "Send a predefined message to a contact within the Peach app. [See the documentation](https://peach.apidocumentation.com/reference#tag/messaging/post/transactional_messages)", + version: "0.0.1", + type: "action", + props: { + peach, + phoneNumber: { + propDefinition: [ + peach, + "phoneNumber", + ], + }, + templateName: { + type: "string", + label: "Template Name", + description: "WhatsApp approved utility template name", + }, + contactName: { + propDefinition: [ + peach, + "contactName", + ], + optional: true, + }, + contactEmail: { + propDefinition: [ + peach, + "contactEmail", + ], + optional: true, + }, + arguments: { + type: "object", + label: "Arguments", + description: "Arguments for the template", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.peach.sendTransactionalMessage({ + $, + data: clearObj({ + template_name: this.templateName, + to: { + name: this.contactName, + email: this.contactEmail, + phone_number: this.phoneNumber, + }, + arguments: this.arguments + ? Object.keys(this.arguments).map((key) => ({ + key, + value: this.arguments[key], + })) + : {}, + }), + }); + + $.export("$summary", `Message sent successfully to ${this.phoneNumber}`); + return response; + }, +}; diff --git a/components/peach/common/utils.mjs b/components/peach/common/utils.mjs new file mode 100644 index 0000000000000..816afd11fddb2 --- /dev/null +++ b/components/peach/common/utils.mjs @@ -0,0 +1,16 @@ +export const clearObj = (obj) => { + return Object.entries(obj) + .filter(([ + , + v, + ]) => (v != null && v != "" && JSON.stringify(v) != "{}")) + .reduce((acc, [ + k, + v, + ]) => ({ + ...acc, + [k]: (!Array.isArray(v) && v === Object(v)) + ? clearObj(v) + : v, + }), {}); +}; diff --git a/components/peach/package.json b/components/peach/package.json new file mode 100644 index 0000000000000..b44a64755861e --- /dev/null +++ b/components/peach/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/peach", + "version": "0.1.0", + "description": "Pipedream Peach Components", + "main": "peach.app.mjs", + "keywords": [ + "pipedream", + "peach" + ], + "homepage": "https://pipedream.com/apps/peach", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/peach/peach.app.mjs b/components/peach/peach.app.mjs new file mode 100644 index 0000000000000..df54b05c31088 --- /dev/null +++ b/components/peach/peach.app.mjs @@ -0,0 +1,57 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "peach", + propDefinitions: { + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number of the contact to send the message to", + }, + contactName: { + type: "string", + label: "Contact Name", + description: "The name of the contact", + }, + contactEmail: { + type: "string", + label: "Contact Email", + description: "The email of the contact", + }, + }, + methods: { + _baseUrl() { + return "https://app.trypeach.io/api/v1"; + }, + _headers() { + return { + "Authorization": `${this.$auth.api_token}`, + "Content-Type": "application/json", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + sendTransactionalMessage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/transactional_messages", + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contacts", + ...opts, + }); + }, + }, +}; diff --git a/components/peaka/actions/run-query/run-query.mjs b/components/peaka/actions/run-query/run-query.mjs new file mode 100644 index 0000000000000..7460e43ae1dce --- /dev/null +++ b/components/peaka/actions/run-query/run-query.mjs @@ -0,0 +1,49 @@ +import utils from "../../common/utils.mjs"; +import app from "../../peaka.app.mjs"; + +export default { + key: "peaka-run-query", + name: "Run Query", + description: "Runs a specific query in Peaka and returns the result as line items. [See the documentation](https://docs.peaka.com/api-reference/data-%3E-query/execute-a-query).", + version: "0.0.1", + type: "action", + props: { + app, + projectId: { + propDefinition: [ + app, + "projectId", + ], + }, + data: { + type: "string", + label: "JSON Query String", + description: "JSON structure as a query. Eg: `{ \"columns\": [ { \"column\": { \"catalogId\": \"2\", \"tableName\": \"mongo_postgresql\", \"columnName\": \"suburb\", \"schemaName\": \"query\" } } ], \"from\": [ { \"catalogId\": \"2\", \"tableName\": \"mongo_postgresql\", \"schemaName\": \"query\" } ], \"limit\": 50, \"offset\": 0 }` where `catalogId` and `schemaName` are always fixed values.", + }, + }, + methods: { + executeQuery({ + projectId, ...args + } = {}) { + return this.app.post({ + path: `/data/projects/${projectId}/queries/execute`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + executeQuery, + projectId, + data, + } = this; + + const response = await executeQuery({ + projectId, + data: utils.valueToObject(data), + }); + + $.export("$summary", `Successfully ran the query in project ID ${projectId}`); + return response; + }, +}; diff --git a/components/peaka/actions/search-query/search-query.mjs b/components/peaka/actions/search-query/search-query.mjs new file mode 100644 index 0000000000000..a9bf3cc1bc264 --- /dev/null +++ b/components/peaka/actions/search-query/search-query.mjs @@ -0,0 +1,62 @@ +import app from "../../peaka.app.mjs"; + +export default { + key: "peaka-search-query", + name: "Search Query", + description: "Performs a search for a specific query in Peaka and returns the matches as rows. [See the documentation](https://docs.peaka.com/api-reference/data-%3E-query/list-queries).", + version: "0.0.1", + type: "action", + props: { + app, + projectId: { + propDefinition: [ + app, + "projectId", + ], + }, + term: { + type: "string", + label: "Query Term", + description: "The term to search for in queries.", + }, + }, + methods: { + listQueries({ + projectId, ...args + } = {}) { + return this.app._makeRequest({ + path: `/data/projects/${projectId}/queries`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + listQueries, + projectId, + term, + } = this; + + const response = await listQueries({ + $, + projectId, + }); + + const matches = response?.filter(({ + name, displayName, queryType, inputQuery, + }) => { + return name.includes(term) + || displayName.includes(term) + || queryType.includes(term) + || inputQuery.includes(term); + }); + + if (!matches.length) { + $.export("$summary", "No queries found matching the term."); + return []; + } + + $.export("$summary", `Successfully found ${matches.length} queries matching the query term.`); + return matches; + }, +}; diff --git a/components/peaka/common/utils.mjs b/components/peaka/common/utils.mjs new file mode 100644 index 0000000000000..81f89c50e782e --- /dev/null +++ b/components/peaka/common/utils.mjs @@ -0,0 +1,26 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function isJson(value) { + try { + JSON.parse(value); + } catch (e) { + return false; + } + return true; +} + +function valueToObject(value) { + if (typeof(value) === "object") { + return value; + } + + if (!isJson(value)) { + throw new ConfigurationError(`Make sure the custom expression contains a valid JSON object: \`${value}\``); + } + + return JSON.parse(value); +} + +export default { + valueToObject, +}; diff --git a/components/peaka/package.json b/components/peaka/package.json new file mode 100644 index 0000000000000..d507938b482fa --- /dev/null +++ b/components/peaka/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/peaka", + "version": "0.1.0", + "description": "Pipedream Peaka Components", + "main": "peaka.app.mjs", + "keywords": [ + "pipedream", + "peaka" + ], + "homepage": "https://pipedream.com/apps/peaka", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} diff --git a/components/peaka/peaka.app.mjs b/components/peaka/peaka.app.mjs new file mode 100644 index 0000000000000..ce947bb6156d5 --- /dev/null +++ b/components/peaka/peaka.app.mjs @@ -0,0 +1,52 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "peaka", + propDefinitions: { + projectId: { + type: "string", + label: "Project ID", + description: "The Project ID to execute the query against.", + async options() { + const response = await this.listProjects(); + return response.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://partner.peaka.studio/api/v1"; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + debug: true, + url: this._baseUrl() + path, + headers: { + ...headers, + "Authorization": `Bearer ${this.$auth.api_key}`, + }, + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + listProjects(args = {}) { + return this._makeRequest({ + path: "/projects", + ...args, + }); + }, + }, +}; diff --git a/components/pendo/README.md b/components/pendo/README.md new file mode 100644 index 0000000000000..beedefa9fdeff --- /dev/null +++ b/components/pendo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Pendo API provides a suite of endpoints that allow you to tap into user insights and product data collected by Pendo. With this API, you can automate the retrieval of visitor analytics, track feature usage, and manage guides and feedback within your application. When used within Pipedream, you can craft workflows that respond to this data in real-time, connect with other apps, and streamline your user-centric operations. + +# Example Use Cases + +- **Sync Pendo Visitor Data to CRM**: Automate syncing of visitor analytics and metadata from Pendo to your CRM platform such as Salesforce. Keep sales and customer success teams informed about user engagement levels and product interactions for better support and upselling opportunities. + +- **Trigger Email Campaigns Based on Feature Usage**: Use Pendo feature events to trigger personalized email campaigns via an email platform like SendGrid. When a user engages heavily with a new feature, send them tips, tutorials, or related promotional content to enhance their experience and retention. + +- **Aggregate Feedback for Product Roadmap**: Collect user feedback submitted through Pendo and aggregate it in a tool like Airtable. This workflow enables product teams to analyze feedback trends, prioritize feature requests, and inform the product roadmap with real user insights. diff --git a/components/pendo/package.json b/components/pendo/package.json index bda2fe409dcbe..8a08eb00de242 100644 --- a/components/pendo/package.json +++ b/components/pendo/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/pennylane/actions/create-billing-subscription/create-billing-subscription.mjs b/components/pennylane/actions/create-billing-subscription/create-billing-subscription.mjs new file mode 100644 index 0000000000000..4835da3aa8a83 --- /dev/null +++ b/components/pennylane/actions/create-billing-subscription/create-billing-subscription.mjs @@ -0,0 +1,162 @@ +import { + DAY_OF_WEEK_OPTIONS, + MODE_OPTIONS, + PAYMENT_CONDITIONS_OPTIONS, + PAYMENT_METHOD_OPTIONS, + RECURRING_RULE_TYPE, +} from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import pennylane from "../../pennylane.app.mjs"; + +export default { + key: "pennylane-create-billing-subscription", + name: "Create Billing Subscription", + description: "Creates a billing subscription for a customer. [See the documentation](https://pennylane.readme.io/reference/billing_subscriptions-post-1).", + version: "0.0.1", + type: "action", + props: { + pennylane, + currency: { + type: "string", + label: "Currency", + description: "Invoice Currency (ISO 4217). Default is EUR.", + optional: true, + }, + mode: { + type: "string", + label: "Mode", + description: "Mode in which the new invoices will be created.", + options: MODE_OPTIONS, + }, + start: { + type: "string", + label: "Start", + description: "Start date (ISO 8601)", + }, + paymentConditions: { + type: "string", + label: "Payment Conditions", + description: "Customer payment conditions", + options: PAYMENT_CONDITIONS_OPTIONS, + }, + paymentMethod: { + type: "string", + label: "Payment Method", + description: "PaymentMethod", + options: PAYMENT_METHOD_OPTIONS, + }, + recurringRuleType: { + type: "string", + label: "Recurring Rule Type", + description: "Type of the billing subscription's recurrence", + options: RECURRING_RULE_TYPE, + reloadProps: true, + }, + dayOfMonth: { + type: "integer", + label: "Day Of Month", + description: "The day of occurrences of the recurring rule", + hidden: true, + }, + dayOfWeek: { + type: "string", + label: "Day Of Week", + description: "The day of occurrences of the recurring rule", + options: DAY_OF_WEEK_OPTIONS, + hidden: true, + }, + interval: { + type: "string", + label: "Interval", + description: "The interval of occurrences of the recurring rule", + optional: true, + }, + count: { + type: "integer", + label: "Count", + description: "Number of occurrences of the recurring rule", + optional: true, + }, + specialMention: { + type: "string", + label: "Special Mention", + description: "Additional details", + optional: true, + }, + discount: { + type: "integer", + label: "Discount", + description: "Invoice discount (in percent)", + optional: true, + }, + customerId: { + propDefinition: [ + pennylane, + "customerId", + ], + }, + lineItemsSectionsAttributes: { + propDefinition: [ + pennylane, + "lineItemsSectionsAttributes", + ], + optional: true, + }, + invoiceLines: { + propDefinition: [ + pennylane, + "lineItems", + ], + label: "Invoice Lines", + }, + }, + async additionalProps(props) { + switch (this.recurringRuleType) { + case "monthly": + props.dayOfMonth.hidden = false; + props.dayOfWeek.hidden = true; + break; + case "weekly": + props.dayOfMonth.hidden = true; + props.dayOfWeek.hidden = false; + break; + case "yearly": + props.dayOfMonth.hidden = true; + props.dayOfWeek.hidden = true; + break; + } + return {}; + }, + async run({ $ }) { + const response = await this.pennylane.createBillingSubscription({ + $, + data: { + create_customer: false, + create_products: false, + billing_subscription: { + currency: this.currency, + mode: this.mode, + start: this.start, + payment_conditions: this.paymentConditions, + payment_method: this.paymentMethod, + recurring_rule: { + type: this.recurringRuleType, + day_of_month: this.dayOfMonth, + day_of_week: this.dayOfWeek, + interval: this.interval, + count: this.count, + }, + special_mention: this.specialMention, + discount: this.discount, + customer: { + source_id: this.customerId, + }, + line_items_sections_attributes: parseObject(this.lineItemsSectionsAttributes), + invoice_lines: parseObject(this.invoiceLines), + }, + }, + }); + $.export("$summary", `Created billing subscription with ID ${response.billing_subscription.id}`); + return response; + }, +}; diff --git a/components/pennylane/actions/create-customer-invoice/create-customer-invoice.mjs b/components/pennylane/actions/create-customer-invoice/create-customer-invoice.mjs new file mode 100644 index 0000000000000..1e568b8339f73 --- /dev/null +++ b/components/pennylane/actions/create-customer-invoice/create-customer-invoice.mjs @@ -0,0 +1,182 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { + BANKING_PROVIDER_OPTIONS, + LANGUAGE_OPTIONS, + PROVIDER_FIELD_NAMES_OPTIONS, +} from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import pennylane from "../../pennylane.app.mjs"; + +export default { + key: "pennylane-create-customer-invoice", + name: "Create Customer Invoice", + description: "Generates a new invoice for a customer using Pennylane. [See the documentation](https://pennylane.readme.io/reference/customer_invoices-post-1)", + version: "0.0.1", + type: "action", + props: { + pennylane, + date: { + type: "string", + label: "Date", + description: "Invoice date (ISO 8601)", + }, + deadline: { + type: "string", + label: "Deadline", + description: "Invoice payment deadline (ISO 8601)", + }, + externalId: { + type: "string", + label: "External Id", + description: "An id you can use to refer to the invoice from outside of Pennylane", + optional: true, + }, + pdfInvoiceFreeText: { + type: "string", + label: "PDF Invoice Free Text", + description: "For example, the contact details of the person to contact", + optional: true, + }, + pdfInvoiceSubject: { + type: "string", + label: "PDF Invoice Subject", + description: "Invoice title", + optional: true, + }, + draft: { + type: "boolean", + label: "Draft", + description: "Do you wish to create a draft invoice (otherwise it is a finalized invoice)? Reminder, once created, a finalized invoice cannot be edited!", + }, + currency: { + type: "string", + label: "Currency", + description: "Invoice Currency (ISO 4217). Default is EUR.", + optional: true, + }, + specialMention: { + type: "string", + label: "Special Mention", + description: "Additional details", + optional: true, + }, + discount: { + type: "integer", + label: "Discount", + description: "Invoice discount (in percent)", + optional: true, + }, + language: { + type: "string", + label: "Language", + description: "invoice pdf language", + options: LANGUAGE_OPTIONS, + optional: true, + }, + bankingProvider: { + type: "string", + label: "Banking Provider", + description: "The banking provider for the transaction", + options: BANKING_PROVIDER_OPTIONS, + reloadProps: true, + }, + providerFieldName: { + type: "string", + label: "Provider Field Name", + description: "Name of the field that you want to match", + options: PROVIDER_FIELD_NAMES_OPTIONS, + hidden: true, + }, + providerFieldValue: { + type: "string", + label: "Provider Field Value", + description: "Value that you want to match", + }, + customerId: { + propDefinition: [ + pennylane, + "customerId", + ], + }, + lineItemsSectionsAttributes: { + propDefinition: [ + pennylane, + "lineItemsSectionsAttributes", + ], + optional: true, + }, + lineItems: { + propDefinition: [ + pennylane, + "lineItems", + ], + }, + categories: { + type: "string[]", + label: "Categories", + description: "A list of objects of categories", + optional: true, + }, + startDate: { + type: "string", + label: "Start Date", + description: "Start date of the imputation period (ISO 8601)", + }, + endDate: { + type: "string", + label: "End Date", + description: "End date of the imputation period (ISO 8601)", + }, + }, + async additionalProps(props) { + if (this.bankingProvider === "stripe") { + props.providerFieldName.hidden = false; + } + return {}; + }, + async run({ $ }) { + try { + const invoice = await this.pennylane.createInvoice({ + $, + data: { + create_customer: false, + create_products: false, + invoice: { + date: this.date, + deadline: this.deadline, + external_id: this.externalId, + pdf_invoice_free_text: this.pdfInvoiceFreeText, + pdf_invoice_subject: this.pdfInvoiceSubject, + draft: this.draft, + currency: this.currency, + special_mention: this.specialMention, + discount: this.discount, + language: this.language, + transactions_reference: { + banking_provider: this.bankingProvider, + provider_field_name: (this.bankingProvider === "gocardless") + ? "payment_id" + : this.providerFieldName, + provider_field_value: this.providerFieldValue, + }, + customer: { + source_id: this.customerId, + }, + line_items_sections_attributes: parseObject(this.lineItemsSectionsAttributes), + line_items: parseObject(this.lineItems), + categories: parseObject(this.categories), + imputation_dates: { + start_date: this.startDate, + end_date: this.endDate, + }, + }, + }, + }); + + $.export("$summary", `Created invoice with ID ${invoice.invoice.id}`); + return invoice; + } catch ({ response }) { + throw new ConfigurationError(response?.data?.message || response?.data?.error); + } + }, +}; diff --git a/components/pennylane/actions/create-customer/create-customer.mjs b/components/pennylane/actions/create-customer/create-customer.mjs new file mode 100644 index 0000000000000..38054afd4905c --- /dev/null +++ b/components/pennylane/actions/create-customer/create-customer.mjs @@ -0,0 +1,229 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { + CUSTOMER_GENDER_OPTIONS, + CUSTOMER_TYPE_OPTIONS, PAYMENT_CONDITIONS_OPTIONS, +} from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import pennylane from "../../pennylane.app.mjs"; + +export default { + key: "pennylane-create-customer", + name: "Create Customer", + description: "Creates a new customer in Pennylane. [See the documentation](https://pennylane.readme.io/reference/customers-post-1)", + version: "0.0.1", + type: "action", + props: { + pennylane, + customerType: { + type: "string", + label: "Customer Type", + description: "The type of the customer you want to create.", + options: CUSTOMER_TYPE_OPTIONS, + reloadProps: true, + }, + firstName: { + type: "string", + label: "First Name", + description: "Customer first name.", + hidden: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Customer last name.", + hidden: true, + }, + gender: { + type: "string", + label: "Gender", + description: "Customer Gender", + options: CUSTOMER_GENDER_OPTIONS, + hidden: true, + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "The name of the company.", + hidden: true, + }, + regNo: { + type: "string", + label: "Reg No", + description: "Customer registration number (SIREN).", + hidden: true, + optional: true, + }, + address: { + type: "string", + label: "Address", + description: "Customer address (billing address).", + }, + postalCode: { + type: "string", + label: "Postal Code", + description: "Postal code (billing address).", + }, + city: { + type: "string", + label: "City", + description: "City (billing address).", + }, + country: { + type: "string", + label: "Country", + description: "Any ISO 3166 Alpha-2 country code (billing address).", + }, + recipient: { + type: "string", + label: "Recipient", + description: "Recipient displayed in the invoice.", + hidden: true, + optional: true, + }, + vatNumber: { + type: "string", + label: "VAT Number", + description: "Customer's VAT number.", + hidden: true, + optional: true, + }, + sourceId: { + type: "string", + label: "Source Id", + description: "You can use your own id when creating the customer. If not provided, Pennylane will pick one for you. Id must be unique.", + optional: true, + }, + emails: { + type: "string[]", + label: "Emails", + description: "A list of customer emails.", + optional: true, + }, + billingIban: { + type: "string", + label: "Billing IBAN", + description: "The billing IBAN of the customer. This is the iban on which you wish to receive payment from this customer.", + optional: true, + }, + deliveryAddress: { + type: "string", + label: "Delivery Address", + description: "Address (shipping address).", + optional: true, + }, + deliveryPostalCode: { + type: "string", + label: "Delivery Postal Code", + description: "Postal code (shipping address).", + optional: true, + }, + deliveryCity: { + type: "string", + label: "Delivery City", + description: "City (shipping address).", + optional: true, + }, + deliveryCountry: { + type: "string", + label: "Delivery Country", + description: "Any ISO 3166 Alpha-2 country code (shipping address).", + optional: true, + }, + paymentConditions: { + type: "string", + label: "Payment Conditions", + description: "Customer payment conditions", + options: PAYMENT_CONDITIONS_OPTIONS, + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Customer phone number.", + optional: true, + }, + reference: { + type: "string", + label: "Reference", + description: "This reference doesn't appear on the invoice.", + optional: true, + }, + notes: { + type: "string", + label: "Notes", + description: "Notes about the customer.", + optional: true, + }, + mandate: { + type: "object", + label: "Mandate", + description: "The mandate object. **Example: {\"provider\": \"gocardless\",\"source_id\": \"MD001H23WP8E7XN\"}**.", + optional: true, + }, + planItem: { + type: "object", + label: "Plan Item", + description: "The plan item object. **Example: {\"enabled\": true,\"number\": \"123\",\"label\": \"label\",\"vat_rate\": \"123\",\"country_alpha2\": \"US\",\"description\": \"description\"}**.", + optional: true, + }, + }, + async additionalProps(props) { + const typeCompany = (this.customerType === "company"); + props.name.hidden = !typeCompany; + props.regNo.hidden = !typeCompany; + props.recipient.hidden = !typeCompany; + props.vatNumber.hidden = !typeCompany; + props.firstName.hidden = typeCompany; + props.lastName.hidden = typeCompany; + props.gender.hidden = typeCompany; + return {}; + }, + async run({ $ }) { + try { + const additionalData = this.customerType === "company" + ? { + name: this.name, + reg_no: this.regNo, + vat_number: this.vatNumber, + recipient: this.recipient, + } + : { + first_name: this.firstName, + last_name: this.lastName, + gender: this.gender, + }; + + const response = await this.pennylane.createCustomer({ + $, + data: { + customer: { + customer_type: this.customerType, + address: this.address, + postal_code: this.postalCode, + city: this.city, + country_alpha2: this.country, + source_id: this.sourceId, + emails: parseObject(this.emails), + billing_iban: this.billingIban, + delivery_address: this.deliveryAddress, + delivery_postal_code: this.deliveryPostalCode, + delivery_city: this.deliveryCity, + delivery_country_alpha2: this.deliveryCountry, + payment_conditions: this.paymentConditions, + phone: this.phone, + reference: this.reference, + notes: this.notes, + mandate: parseObject(this.mandate), + plan_item: parseObject(this.planItem), + ...additionalData, + }, + }, + }); + $.export("$summary", `Successfully created customer with Id: ${response.customer.source_id}`); + return response; + } catch ({ response }) { + throw new ConfigurationError(response?.data?.message || response?.data?.error); + } + }, +}; diff --git a/components/pennylane/common/constants.mjs b/components/pennylane/common/constants.mjs new file mode 100644 index 0000000000000..3cdf83a14f219 --- /dev/null +++ b/components/pennylane/common/constants.mjs @@ -0,0 +1,75 @@ +export const CUSTOMER_TYPE_OPTIONS = [ + "company", + "individual", +]; + +export const PAYMENT_CONDITIONS_OPTIONS = [ + "upon_receipt", + "custom", + "15_days", + "30_days", + "45_days", + "60_days", +]; + +export const CUSTOMER_GENDER_OPTIONS = [ + "mister", + "madam", +]; + +export const LANGUAGE_OPTIONS = [ + "en_GB", + "fr_FR", +]; + +export const BANKING_PROVIDER_OPTIONS = [ + "gocardless", + "stripe", +]; + +export const PROVIDER_FIELD_NAMES_OPTIONS = [ + "payment_id", + "charge_id", +]; + +export const RECURRING_RULE_TYPE = [ + "monthly", + "weekly", + "yearly", +]; + +export const MODE_OPTIONS = [ + { + label: "Draft invoices will be created", + value: "awaiting_validation", + }, + { + label: "Finalized invoices will be created", + value: "finalized", + }, + { + label: "Finalized invoices will be created and autimatically sent to the client at each new occurrence", + value: "email", + }, +]; + +export const PAYMENT_METHOD_OPTIONS = [ + { + label: "Offline - The subscription is not linked to a payment method", + value: "offline", + }, + { + label: "Gocardless Direct Debit - At each new occurrence the client will be automatically debited thanks to GoCardless", + value: "gocardless_direct_debit", + }, +]; + +export const DAY_OF_WEEK_OPTIONS = [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", +]; diff --git a/components/pennylane/common/utils.mjs b/components/pennylane/common/utils.mjs new file mode 100644 index 0000000000000..9050833bcda0b --- /dev/null +++ b/components/pennylane/common/utils.mjs @@ -0,0 +1,32 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return parseObject(item); + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + if (typeof obj === "object") { + for (const [ + key, + value, + ] of Object.entries(obj)) { + obj[key] = parseObject(value); + } + } + return obj; +}; diff --git a/components/pennylane/package.json b/components/pennylane/package.json new file mode 100644 index 0000000000000..0acf42d2418cb --- /dev/null +++ b/components/pennylane/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/pennylane", + "version": "0.1.0", + "description": "Pipedream Pennylane Components", + "main": "pennylane.app.mjs", + "keywords": [ + "pipedream", + "pennylane" + ], + "homepage": "https://pipedream.com/apps/pennylane", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/pennylane/pennylane.app.mjs b/components/pennylane/pennylane.app.mjs new file mode 100644 index 0000000000000..a163a81424d8b --- /dev/null +++ b/components/pennylane/pennylane.app.mjs @@ -0,0 +1,123 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "pennylane", + propDefinitions: { + customerId: { + type: "string", + label: "Customer Id", + description: "Existing customer identifier (source_id)", + async options({ page }) { + const { customers } = await this.listCustomers({ + params: { + page: page + 1, + }, + }); + + return customers.map(({ + source_id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + lineItemsSectionsAttributes: { + type: "string[]", + label: "Line Items Sections Attributes", + description: "A list of objects of items sections to be listed on the invoice. **Example: [{\"title\": \"Title\",\"description\": \"description\",\"rank\": 1}]** [See the documentation](https://pennylane.readme.io/reference/customer_invoices-post-1) from further information.", + }, + lineItems: { + type: "string[]", + label: "Line Items", + description: "A list of objects of items to be listed on the invoice. **Example: [{\"product\": {\"source_id\": \"0e67fc3c-c632-4feb-ad34-e18ed5fbf66a\",\"unit\": \"20\",\"price\": \"10.00\",\"label\": \"Product label\",\"vat_rate\": \"FR_09\",\"currency\": \"EUR\"},\"quantity\": 2,\"label\": \"Line Item Label\"}]** [See the documentation](https://pennylane.readme.io/reference/customer_invoices-post-1) from further information.", + }, + }, + methods: { + _baseUrl() { + return "https://app.pennylane.com/api/external/v1"; + }, + _headers() { + return { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "Content-Type": "application/json", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createCustomer(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/customers", + ...opts, + }); + }, + createInvoice(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/customer_invoices", + ...opts, + }); + }, + listCustomers(opts = {}) { + return this._makeRequest({ + path: "/customers", + ...opts, + }); + }, + createBillingSubscription(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/billing_subscriptions", + ...opts, + }); + }, + listBillingSubscriptions(opts = {}) { + return this._makeRequest({ + path: "/billing_subscriptions", + params: opts.params, + }); + }, + listInvoices(opts = {}) { + return this._makeRequest({ + method: "GET", + path: "/customer_invoices", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, fieldName, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const data = await fn({ + params, + ...opts, + }); + const items = data[fieldName]; + for (const d of items) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = items.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/pennylane/sources/common/base.mjs b/components/pennylane/sources/common/base.mjs new file mode 100644 index 0000000000000..43611e79f381e --- /dev/null +++ b/components/pennylane/sources/common/base.mjs @@ -0,0 +1,60 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import pennylane from "../../pennylane.app.mjs"; + +export default { + props: { + pennylane, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + + const response = this.pennylane.paginate({ + fn: this.getFunction(), + fieldName: this.getFieldName(), + }); + + let responseArray = []; + for await (const item of response) { + if (Date.parse(item.updated_at) <= lastDate) break; + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastDate(responseArray[0].updated_at); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.source_id || item.id, + summary: this.getSummary(item), + ts: Date.parse(item.updated_at || item.activated_at || new Date()), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/pennylane/sources/new-billing-subscription/new-billing-subscription.mjs b/components/pennylane/sources/new-billing-subscription/new-billing-subscription.mjs new file mode 100644 index 0000000000000..7b331da212156 --- /dev/null +++ b/components/pennylane/sources/new-billing-subscription/new-billing-subscription.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "pennylane-new-billing-subscription", + name: "New Billing Subscription Created", + description: "Emit new event when a billing subscription is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.pennylane.listBillingSubscriptions; + }, + getFieldName() { + return "billing_subscriptions"; + }, + getSummary(item) { + return `New Billing Subscription: ${item.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/pennylane/sources/new-billing-subscription/test-event.mjs b/components/pennylane/sources/new-billing-subscription/test-event.mjs new file mode 100644 index 0000000000000..a9dd205c14c45 --- /dev/null +++ b/components/pennylane/sources/new-billing-subscription/test-event.mjs @@ -0,0 +1,106 @@ +export default { + "id": 0, + "next_occurrence": "string", + "prev_occurrence": "string", + "stopped_at": "2023-08-30T10:08:08.146343Z", + "start": "2023-01-01", + "finish": "2023-12-31", + "status": "string", + "mode": "string", + "email_settings": {}, + "activated_at": "2023-08-30T10:08:08.146343Z", + "payment_method": "string", + "recurring_rule": { + "day_of_month": [ + 0 + ], + "week_start": 0, + "day": [ + 0 + ], + "rule_type": "weekly", + "interval": 0, + "count": 12, + "until": "string" + }, + "customer": { + "first_name": "John", + "last_name": "Doe", + "gender": "mister", + "name": "Pennylane", + "reg_no": "XXXXXXXXX", + "vat_number": "FR12345678910", + "updated_at": "2023-08-30T10:08:08.146343Z", + "source_id": "38a1f19a-256d-4692-a8fe-0a16403f59ff", + "emails": [ + "hello@example.org" + ], + "billing_iban": "FRXX XXXX XXXX XXXX XXXX XXXX XXX", + "customer_type": "company", + "address": "4 rue du faubourg saint martin", + "postal_code": "75010", + "city": "Paris", + "country_alpha2": "FR", + "recipient": "string", + "billing_address": { + "address": "string", + "postal_code": "string", + "city": "string", + "country_alpha2": "string" + }, + "delivery_address": { + "address": "105 Rue Mondenard", + "postal_code": "33100", + "city": "Bordeaux", + "country_alpha2": "FR" + }, + "payment_conditions": "upon_receipt", + "phone": "+33123232323", + "reference": "This is a custom reference", + "notes": "This is a note", + "v2_id": 1234 + }, + "invoice_template": { + "label": "Invoice label", + "currency": "EUR", + "amount": "230.32", + "currency_amount": "230.32", + "currency_amount_before_tax": "196.32", + "exchange_rate": "1.0", + "payment_condition": "15_days", + "currency_tax": "34.0", + "language": "fr_FR", + "discount": "50.1", + "discount_type": "relative", + "special_mention": "Additional details", + "updated_at": "2023-08-30T10:08:08.146343Z", + "line_items_sections_attributes": [ + { + "title": "Line items section title", + "description": "Description of the line items section", + "rank": 1 + } + ], + "line_items": [ + { + "id": 444, + "label": "Demo label", + "unit": "piece", + "quantity": 12, + "amount": "50.4", + "currency_amount": "50.4", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor", + "product_id": "9bca2815-217a-4210-bed8-73139977b9a8", + "product_v2_id": 1234, + "vat_rate": "FR_200", + "currency_price_before_tax": "30", + "currency_tax": "10", + "discount": "25", + "discount_type": "relative", + "section_rank": 1, + "v2_id": 1234 + } + ] + }, + "v2_id": 1234 +} \ No newline at end of file diff --git a/components/pennylane/sources/new-customer-invoice/new-customer-invoice.mjs b/components/pennylane/sources/new-customer-invoice/new-customer-invoice.mjs new file mode 100644 index 0000000000000..0bb509b1f85c4 --- /dev/null +++ b/components/pennylane/sources/new-customer-invoice/new-customer-invoice.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "pennylane-new-customer-invoice", + name: "New Customer Invoice Created or Imported", + description: "Emit new event when a new invoice is created or imported.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.pennylane.listInvoices; + }, + getFieldName() { + return "invoices"; + }, + getSummary(item) { + return `New Invoice: ${item.invoice_number || item.label}`; + }, + }, + sampleEmit, +}; diff --git a/components/pennylane/sources/new-customer-invoice/test-event.mjs b/components/pennylane/sources/new-customer-invoice/test-event.mjs new file mode 100644 index 0000000000000..a12654d228b09 --- /dev/null +++ b/components/pennylane/sources/new-customer-invoice/test-event.mjs @@ -0,0 +1,114 @@ +export default { + "id": "wMoOACctiA", + "label": "Invoice label", + "invoice_number": "Invoice number", + "quote_group_uuid": "b50b8fe6-48ce-4380-b9b3-52eec688fc04", + "is_draft": true, + "is_estimate": true, + "currency": "EUR", + "amount": "230.32", + "currency_amount": "230.32", + "currency_amount_before_tax": "196.32", + "exchange_rate": 1, + "date": "2023-08-30", + "deadline": "2020-09-02", + "currency_tax": "34.0", + "language": "fr_FR", + "paid": false, + "status": "upcoming", + "discount": "50.1", + "discount_type": "relative", + "public_url": "https://app.pennylane.com/...", + "file_url": "https://app.pennylane.com/.../file.pdf", + "filename": "my_file.pdf", + "remaining_amount": "20.0", + "source": "email", + "special_mention": "Additional details", + "updated_at": "2023-08-30T10:08:08.146343Z", + "imputation_dates": { + "start_date": "2020-06-30", + "end_date": "2021-06-30" + }, + "line_items_sections_attributes": [ + { + "title": "Line items section title", + "description": "Description of the line items section", + "rank": 1 + } + ], + "line_items": [ + { + "id": 444, + "label": "Demo label", + "unit": "piece", + "quantity": 12, + "amount": "50.4", + "currency_amount": "50.4", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor", + "product_id": "9bca2815-217a-4210-bed8-73139977b9a8", + "product_v2_id": 1234, + "vat_rate": "FR_200", + "currency_price_before_tax": "30", + "currency_tax": "10", + "raw_currency_unit_price": "5", + "discount": "25", + "discount_type": "relative", + "section_rank": 1, + "v2_id": 1234 + } + ], + "categories": [ + { + "source_id": "38a1f19a-256d-4692-a8fe-0a16403f59ff", + "weight": 0.8, + "label": "Alimentaire", + "direction": "cash_in", + "created_at": "2023-08-30T10:08:08.146343Z", + "updated_at": "2023-08-30T10:08:08.146343Z", + "v2_id": 1234 + } + ], + "transactions_reference": { + "banking_provider": "bank", + "provider_field_name": "label", + "provider_field_value": "invoice_number" + }, + "payments": [ + { + "label": "string", + "created_at": "2023-08-30", + "currency_amount": "string" + } + ], + "matched_transactions": [ + { + "label": "string", + "amount": "string", + "group_uuid": "string", + "date": "2023-08-30", + "fee": "string", + "currency": "string" + } + ], + "pdf_invoice_free_text": "string", + "pdf_invoice_subject": "string", + "billing_subscription": { + "id": 0, + "v2_id": 1234 + }, + "credit_notes": [ + { + "id": "BCVPZQJ17V", + "amount": "230.32", + "tax": "34.0", + "currency": "EUR", + "currency_amount": "230.32", + "currency_tax": "230.32", + "currency_price_before_tax": "196.32", + "invoice_number": "Credit note number", + "draft": false, + "v2_id": 1234 + } + ], + "v2_id": 1234 +} \ No newline at end of file diff --git a/components/pennylane/sources/new-customer/new-customer.mjs b/components/pennylane/sources/new-customer/new-customer.mjs new file mode 100644 index 0000000000000..bd3dc384edbed --- /dev/null +++ b/components/pennylane/sources/new-customer/new-customer.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "pennylane-new-customer", + name: "New Customer Created", + description: "Emit new event when a customer is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.pennylane.listCustomers; + }, + getFieldName() { + return "customers"; + }, + getSummary(item) { + return `New Customer: ${item.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/pennylane/sources/new-customer/test-event.mjs b/components/pennylane/sources/new-customer/test-event.mjs new file mode 100644 index 0000000000000..89b75ac2b2e00 --- /dev/null +++ b/components/pennylane/sources/new-customer/test-event.mjs @@ -0,0 +1,38 @@ +export default { + "name": "Pennylane", + "reg_no": "XXXXXXXXX", + "vat_number": "FR12345678910", + "updated_at": "2021-06-30T07:44:37.545Z", + "source_id": "38a1f19a-256d-4692-a8fe-0a16403f59ff", + "emails": [ + "hello@example.org" + ], + "billing_iban": "FRXX XXXX XXXX XXXX XXXX XXXX XXX", + "customer_type": "company", + "recipient": "On the behalf of John", + "billing_address": { + "address": "33 rue du mail", + "postal_code": "75010", + "city": "Paris", + "country_alpha2": "FR" + }, + "delivery_address": { + "address": "33 rue du mail", + "postal_code": "75010", + "city": "Paris", + "country_alpha2": "FR" + }, + "payment_conditions": "upon_receipt", + "phone": "+33123232323", + "reference": "This is a custom reference", + "notes": "This is a note", + "plan_item": { + "number": "string", + "label": "string", + "enabled": true, + "vat_rate": "string", + "country_alpha2": "string", + "description": "string" + }, + "v2_id": 1234 +} \ No newline at end of file diff --git a/components/people_data_labs/README.md b/components/people_data_labs/README.md index f8f71cf169687..d2405272f5d72 100644 --- a/components/people_data_labs/README.md +++ b/components/people_data_labs/README.md @@ -1,22 +1,11 @@ # Overview -People Data Labs API provides reliable access to massive and up-to-date -worldwide datasets, enabling businesses to get the data they need to make more -informed decisions. With People Data Labs' easy-to-use API, you can explore and -access a variety of datasets to build innovative solutions. Here are a few -examples of what you can build using the People Data Labs API: +The People Data Labs API provides access to a rich database of profiles, offering granular data on individuals such as employment history, education, skills, and more. Leveraging Pipedream's capabilities, you can automate personalized outreach, enrich contact lists, validate professional information, and sync data across platforms in real-time. These automations can power recruitment, sales, and networking efforts by providing up-to-date and comprehensive data points. -- Build comprehensive people profiles using data from a variety of sources, - including public records, job history, digital identities, and more. -- Create dynamic user segmentation tools to better target and engage with - customers. -- Provide sophisticated background checks for due diligence and fraud - detection. -- Create predictive models for developing valuable insights on consumer - behavior. -- Generate powerful analytics by blending different datasets to uncover - patterns and trends. -- Create decision-making tools to improve recruitment and onboarding processes. -- Develop powerful lead generation & retargeting tools to drive more sales. -- Develop apps that automatically surface the most relevant personal - information for tasks. +# Example Use Cases + +- **Lead Enrichment for Sales Teams**: Automate the enrichment of lead information as new prospects are added to a CRM like Salesforce. Trigger a workflow in Pipedream when a new lead is created, fetch detailed profile data from People Data Labs, and update the lead record in Salesforce with enriched data for enhanced lead scoring and personalized follow-up. + +- **Recruitment Automation**: Streamline candidate sourcing by triggering a Pipedream workflow when a new job is posted to your ATS, like Greenhouse. Use People Data Labs to search for candidates matching the job criteria, and automatically populate a Google Sheet with candidate profiles to accelerate the recruitment process. + +- **Event Attendee Networking**: Post-event engagement can be critical. After an event, import attendee details into Pipedream, use People Data Labs to enrich these contacts with social profiles and work history, and automatically create personalized LinkedIn connection requests or email follow-ups to foster professional relationships. diff --git a/components/people_data_labs/actions/enrich-company/enrich-company.mjs b/components/people_data_labs/actions/enrich-company/enrich-company.mjs index 782fac8432e75..4ea5390b5d1c9 100644 --- a/components/people_data_labs/actions/enrich-company/enrich-company.mjs +++ b/components/people_data_labs/actions/enrich-company/enrich-company.mjs @@ -4,7 +4,7 @@ export default { key: "people_data_labs-enrich-company", name: "Enrich a company", description: "The Company Enrichment API provides a one-to-one match, retrieving up-to-date information on a unique company. [See the docs here](https://docs.peopledatalabs.com/docs/reference-company-enrichment-api)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { app, @@ -104,7 +104,10 @@ export default { min_likelihood: this.minLikelihood, }; - const res = await this.app.enrichCompany(params); + const res = await this.app.enrichCompany({ + $, + params, + }); if (!res) { $.export("$summary", "No results found"); } else { diff --git a/components/people_data_labs/actions/enrich-person/enrich-person.mjs b/components/people_data_labs/actions/enrich-person/enrich-person.mjs index 95baf6965268d..2faadb96ecb13 100644 --- a/components/people_data_labs/actions/enrich-person/enrich-person.mjs +++ b/components/people_data_labs/actions/enrich-person/enrich-person.mjs @@ -4,7 +4,7 @@ export default { key: "people_data_labs-enrich-person", name: "Enrich a person", description: "The Person Enrichment API provides a one-to-one match, retrieving up-to-date information on a unique individual. [See the docs here](https://docs.peopledatalabs.com/docs/reference-person-enrichment-api)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { app, @@ -160,7 +160,10 @@ export default { pretty: this.pretty || true, }; - const res = await this.app.enrichPerson(params); + const res = await this.app.enrichPerson({ + $, + params, + }); if (!res) { $.export("$summary", "No results found"); } else { diff --git a/components/people_data_labs/actions/search-people/search-people.mjs b/components/people_data_labs/actions/search-people/search-people.mjs new file mode 100644 index 0000000000000..fa6507ce6a18b --- /dev/null +++ b/components/people_data_labs/actions/search-people/search-people.mjs @@ -0,0 +1,69 @@ +import { QUERY_TYPE_OPTIONS } from "../../common/constants.mjs"; +import app from "../../people_data_labs.app.mjs"; + +export default { + key: "people_data_labs-search-people", + name: "Search People", + description: "Find specific segments of people that you need to power your projects and products. [See the docs here](https://docs.peopledatalabs.com/docs/reference-person-search-api)", + version: "0.0.1", + type: "action", + props: { + app, + query: { + label: "Query", + type: "string", + description: "The query to perform (can be an [Elasticsearch](https://docs.peopledatalabs.com/docs/input-parameters-person-search-api#query) or [SQL query](https://docs.peopledatalabs.com/docs/input-parameters-person-search-api#sql)). Click the preferred query type for the documentation.", + }, + queryType: { + label: "Query Type", + type: "string", + description: "Which type of query is being used.", + options: QUERY_TYPE_OPTIONS, + }, + size: { + label: "Size", + type: "integer", + description: "The batch size or the maximum number of matched records to return for this query if they exist", + default: 1, + min: 1, + max: 100, + optional: true, + }, + datasets: { + label: "Datasets", + type: "string[]", + description: "Specifies which [dataset(s)](https://docs.peopledatalabs.com/docs/datasets) the API should search against.", + optional: true, + }, + titlecase: { + type: "boolean", + label: "Title Case", + description: "By default, all text in the response data returns as lowercase. Setting to `true` will titlecase any records returned.", + optional: true, + }, + pretty: { + propDefinition: [ + app, + "pretty", + ], + }, + }, + async run({ $ }) { + const { + app, query, queryType, ...data + } = this; + const params = { + [queryType]: query, + ...data, + }; + + const res = await app.searchPeople({ + $, + params, + }); + $.export("$summary", res?.status === 200 + ? `Found ${res?.data?.length} records` + : "No records found"); + return res; + }, +}; diff --git a/components/people_data_labs/common/constants.mjs b/components/people_data_labs/common/constants.mjs new file mode 100644 index 0000000000000..44574e5f9e23f --- /dev/null +++ b/components/people_data_labs/common/constants.mjs @@ -0,0 +1,45 @@ +export const QUERY_TYPE_OPTIONS = [ + { + label: "Elasticsearch query", + value: "query", + }, + { + label: "SQL query", + value: "sql", + }, +]; + +export const DATASET_OPTIONS = [ + { + label: "All", + value: "all", + }, + { + label: "Resume", + value: "resume", + }, + { + label: "Email", + value: "email", + }, + { + label: "Phone", + value: "phone", + }, + { + label: "Mobile Phone", + value: "mobile_phone", + }, + { + label: "Street Address", + value: "street_address", + }, + { + label: "Consumer Social", + value: "consumer_social", + }, + { + label: "Developer", + value: "developer", + }, +]; diff --git a/components/people_data_labs/package.json b/components/people_data_labs/package.json new file mode 100644 index 0000000000000..1faced4a6dc58 --- /dev/null +++ b/components/people_data_labs/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/people_data_labs", + "version": "0.1.0", + "description": "Pipedream People Data Labs Components", + "main": "people_data_labs.app.mjs", + "keywords": [ + "pipedream", + "people_data_labs" + ], + "homepage": "https://pipedream.com/apps/people_data_labs", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/people_data_labs/people_data_labs.app.mjs b/components/people_data_labs/people_data_labs.app.mjs index 80e3ceb9fe8bc..a0d4237f8bab6 100644 --- a/components/people_data_labs/people_data_labs.app.mjs +++ b/components/people_data_labs/people_data_labs.app.mjs @@ -39,10 +39,12 @@ export default { delete res.path; return res; }, - async _makeRequest(opts = {}) { + async _makeRequest({ + $ = this, ...opts + }) { try { const axiosParams = this._getAxiosParams(opts); - return await axios(this, axiosParams); + return await axios($, axiosParams); } catch (err) { if (err?.response?.data?.error) { if (err.response.data.error.status == 404) { @@ -54,16 +56,22 @@ export default { } }, - async enrichPerson(params) { + async enrichPerson(args) { return this._makeRequest({ path: "/person/enrich", - params, + ...args, }); }, - async enrichCompany(params) { + async enrichCompany(args) { return this._makeRequest({ path: "/company/enrich", - params, + ...args, + }); + }, + async searchPeople(args) { + return this._makeRequest({ + path: "/person/search", + ...args, }); }, }, diff --git a/components/perigon/README.md b/components/perigon/README.md index 94c2df368c1ea..a7617301285ef 100644 --- a/components/perigon/README.md +++ b/components/perigon/README.md @@ -1,35 +1,11 @@ # Overview -Perigon is a leading application platform and enables developers to build -powerful applications for their customers. Perigon provides all the necessary -tools and infrastructure to create custom integrated user experiences and -web-based applications. With Perigon, you can quickly and easily build -sophisticated applications and services that are secure, reliable, and -effortlessly scale to meet your users’ needs. +The Perigon API enables the creation and management of complex data relationships, allowing for the automation of data-driven processes. With this API, you can streamline data synchronization, manage master data, and enhance business intelligence by connecting Perigon's data capabilities with various apps and services. Automating workflows with the Perigon API on Pipedream can lead to increased efficiency by reducing manual data handling, ensuring data accuracy, and providing actionable insights through connected systems. -There are a variety of applications that can be built using the Perigon -application platform. The following are a few examples: +# Example Use Cases -- Content and Digital Library Apps – Create apps that store and organize - digital media such as images, music, videos, and documents. -- Customer Engagement Apps – Create apps that facilitate communication between - customer and business, such as customer feedback forms, surveys, customer - support services, and more. -- Database and Data Apps – Create apps that store and manage various types of - data, such as customer and product information, user settings, and analytics. -- Cloud-Based Collaboration Apps – Create apps that enable collaboration - between multiple users, such as task management, project management, - workflow, document sharing, and more. -- AI and Augmented Reality Apps – Create apps that utilize artificial - intelligence, augmented reality, machine learning, and natural language - processing to enhance user experiences. -- Online Payments and Shopping Apps – Create apps that enable users to securely - pay for goods and services online, as well as shop for products in app - stores. -- Mobile Apps – Create apps that are optimized for mobile devices and available - in the Apple App Store and Google Play Store. +- **Customer Data Synchronization**: Automatically sync customer data from a CRM like Salesforce to Perigon. Whenever a new contact is added or updated in Salesforce, trigger a Pipedream workflow that updates the corresponding record in Perigon, ensuring data consistency across platforms. -These are just a few of the possibilities using the Perigon application -platform. As you can see, with Perigon, the possibilities are endless and you -can create powerful, feature-rich applications that will engage your customers -and grow your business. +- **Inventory Management**: Integrate Perigon with an e-commerce platform like Shopify. Set up a Pipedream workflow to monitor stock levels in Perigon and, when stock falls below a certain threshold, send a replenishment request to your supplier and update inventory quantities on Shopify, keeping your sales and supply chain in sync. + +- **Data Analysis and Reporting**: Connect Perigon to a business intelligence tool like Tableau. Use Pipedream to schedule and trigger data export from Perigon, transform and prepare the data as needed, and upload it to Tableau for advanced analysis and visualization. This allows for regular, automated reporting that aids in strategic decision-making. diff --git a/components/perigon/package.json b/components/perigon/package.json new file mode 100644 index 0000000000000..44ca7fb57eac3 --- /dev/null +++ b/components/perigon/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/perigon", + "version": "0.6.0", + "description": "Pipedream perigon Components", + "main": "perigon.app.mjs", + "keywords": [ + "pipedream", + "perigon" + ], + "homepage": "https://pipedream.com/apps/perigon", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/perplexity/README.md b/components/perplexity/README.md new file mode 100644 index 0000000000000..6be2fba8fd8c7 --- /dev/null +++ b/components/perplexity/README.md @@ -0,0 +1,11 @@ +# Overview + +The Perplexity API offers advanced natural language processing capabilities, enabling users to generate answers, summaries, and insights from texts. Leveraging this API on Pipedream allows for the automation of content analysis, intelligent alert systems, and dynamic data enrichment, integrating seamlessly with various data sources and services for real-time processing. + +# Example Use Cases + +- **Customer Support Automation**: Automatically handle incoming customer queries by setting up a workflow where support emails trigger the Perplexity API to analyze and generate responses. Connect this with a customer relationship management (CRM) app on Pipedream to log interactions and follow-ups. + +- **Content Summarization for News Articles**: Streamline the process of summarizing news articles by creating a workflow where new articles fetched from RSS feeds are passed to the Perplexity API for summarization. The summaries can then be automatically posted to a company’s internal communication tool like Slack. + +- **Real-time Social Media Monitoring**: Develop a workflow to monitor social media for brand mentions. Use the Perplexity API to assess sentiment and extract key phrases from posts. This data can be piped into a data visualization tool on Pipedream, like Google Sheets, for analysis and reporting. diff --git a/components/perplexity/actions/chat-completions/chat-completions.mjs b/components/perplexity/actions/chat-completions/chat-completions.mjs new file mode 100644 index 0000000000000..9fa863f9c2f7c --- /dev/null +++ b/components/perplexity/actions/chat-completions/chat-completions.mjs @@ -0,0 +1,48 @@ +import app from "../../perplexity.app.mjs"; + +export default { + key: "perplexity-chat-completions", + name: "Chat Completions", + description: "Generates a model's response for the given chat conversation. [See the documentation](https://docs.perplexity.ai/reference/post_chat_completions)", + version: "0.0.5", + type: "action", + props: { + app, + model: { + propDefinition: [ + app, + "model", + ], + }, + content: { + propDefinition: [ + app, + "content", + ], + }, + role: { + propDefinition: [ + app, + "role", + ], + }, + }, + async run({ $ }) { + const response = await this.app.chatCompletions({ + $, + data: { + model: this.model, + messages: [ + { + role: this.role, + content: this.content, + }, + ], + }, + }); + + $.export("$summary", "Successfully generated a response from the selected model"); + + return response; + }, +}; diff --git a/components/perplexity/common/constants.mjs b/components/perplexity/common/constants.mjs new file mode 100644 index 0000000000000..a19d9a2df58c5 --- /dev/null +++ b/components/perplexity/common/constants.mjs @@ -0,0 +1,13 @@ +export default { + MODELS: [ + "sonar", + "sonar-pro", + "sonar-reasoning", + "sonar-reasoning-pro", + ], + ROLES: [ + "system", + "user", + "assistant", + ], +}; diff --git a/components/perplexity/package.json b/components/perplexity/package.json new file mode 100644 index 0000000000000..4b680ddcf5819 --- /dev/null +++ b/components/perplexity/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/perplexity", + "version": "0.1.4", + "description": "Pipedream Perplexity Components", + "main": "perplexity.app.mjs", + "keywords": [ + "pipedream", + "perplexity" + ], + "homepage": "https://pipedream.com/apps/perplexity", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} diff --git a/components/perplexity/perplexity.app.mjs b/components/perplexity/perplexity.app.mjs new file mode 100644 index 0000000000000..b7cba4a78ee95 --- /dev/null +++ b/components/perplexity/perplexity.app.mjs @@ -0,0 +1,54 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "perplexity", + propDefinitions: { + model: { + type: "string", + label: "Model", + description: "The name of the model that will complete your prompt", + options: constants.MODELS, + }, + content: { + type: "string", + label: "Content", + description: "The contents of the message in this turn of conversation", + }, + role: { + type: "string", + label: "Role", + description: "The role of the speaker in this turn of conversation. After the (optional) system message, user and assistant roles should alternate with 'user' then 'assistant', ending in 'user'.", + options: constants.ROLES, + }, + }, + methods: { + _baseUrl() { + return "https://api.perplexity.ai"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + async chatCompletions(args = {}) { + return this._makeRequest({ + method: "post", + path: "/chat/completions", + ...args, + }); + }, + }, +}; diff --git a/components/persistiq/README.md b/components/persistiq/README.md index 201fdb5a267e1..3cabd40d87ae7 100644 --- a/components/persistiq/README.md +++ b/components/persistiq/README.md @@ -1,31 +1,11 @@ # Overview -The PersistIQ API is a powerful tool that can be used to build a variety of -applications and services related to sales outreach automation and email -marketing. With the PersistIQ API, developers and businesses can build -applications that automate and streamline sales processes, manage contact data, -and optimize email campaigns. +PersistIQ is a sales automation platform that streamlines outbound sales processes with personalized email campaigns and task management. By leveraging the PersistIQ API on Pipedream, you can craft intricate automation workflows that enhance lead management, align sales communications, and improve team productivity. This API interaction enables real-time synchronization of lead data, automated campaign actions, and analytics integration, fostering a seamless sales operation. -Below are just a few examples of the types of applications and services that -can be created using the PersistIQ API: +# Example Use Cases -- Contact and Leads Management Apps: Create apps that track leads, manage - contact data and segment contacts into targeted lists. -- Lead and Contact Activity Tracking: Monitor lead and contact activities to - better understand your customers, evaluate marketing efforts and track - conversions. -- Bulk Email Campaigns: Automate bulk email campaigns and utilize automated - email templates to streamline outreach processes. -- Autoresponders And A/B Testing: Create autoresponders and A/B test campaigns - to evaluate the effectiveness of different emails and boost conversions. -- Email Personalization And Content Management: Automatically customize emails - using contact data and manage email content to improve open rates. -- Trigger-Based Email Campaigns: Automatically trigger email campaigns based on - customer activities, behaviors and preferences. -- Sales Outreach Automation: Automate sales outreach processes and use machine - learning algorithms to personalize your outreach efforts. +- **Lead Syncing Across Platforms**: Automatically sync new leads from a CRM like Salesforce into PersistIQ to ensure sales campaigns always target the latest prospects. When a new lead is added in Salesforce, trigger a Pipedream workflow to create or update that lead in PersistIQ. -These are just a few of the many possibilities available with the PersistIQ -API. With the right tools and resources, developers and businesses of all sizes -can create amazing solutions for enhancing their sales outreach efforts and -email campaigns. +- **Campaign Trigger Based on Activity**: Initiate outbound campaigns in PersistIQ when a lead performs a specific action, such as visiting a particular webpage or engaging with prior email content. Use Pipedream to monitor webhooks or activity logs from analytics tools like Google Analytics, and trigger personalized campaigns in PersistIQ for high-intent leads. + +- **Consolidated Reporting for Sales Activities**: Aggregate campaign performance data from PersistIQ with other sales-related metrics from platforms such as HubSpot or Zendesk Sell. Use Pipedream to fetch campaign results from PersistIQ and compile comprehensive reports, giving a 360-degree view of sales efforts and outcomes. diff --git a/components/persona/README.md b/components/persona/README.md new file mode 100644 index 0000000000000..b013e332a8b1b --- /dev/null +++ b/components/persona/README.md @@ -0,0 +1,11 @@ +# Overview + +The Persona API lets you automate identity verification processes, tailor user onboarding, and ensure compliance with ease. Within Pipedream, you can use this API to create robust serverless workflows that trigger actions based on verification status, update CRM records, or even flag high-risk activities. Combining Persona with Pipedream's vast suite of integrated apps means you can streamline operations, enhance user trust, and maintain security without the heavy lifting of manual intervention. + +# Example Use Cases + +- **Automate User Verification Status Updates**: When a user's identity verification status changes in Persona, a Pipedream workflow can automatically update their record in a connected CRM system like Salesforce or HubSpot. This ensures that your sales or customer service team always has the latest info on user verification. + +- **Trigger Custom Communications Post-Verification**: Once a verification is completed in Persona, you could use Pipedream to send a personalized email via SendGrid or a direct message using Slack to the user. This can improve user engagement and provide immediate feedback on their verification process. + +- **Enhance Security with Real-Time Alerts**: Implement a Pipedream workflow that listens for flags or anomalies reported by Persona. Integrate with incident management systems such as PagerDuty to alert your security team in real-time, ensuring swift action to mitigate any potential risks. diff --git a/components/persona/package.json b/components/persona/package.json new file mode 100644 index 0000000000000..681eddc641c2f --- /dev/null +++ b/components/persona/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/persona", + "version": "0.0.1", + "description": "Pipedream Persona Components", + "main": "persona.app.mjs", + "keywords": [ + "pipedream", + "persona" + ], + "homepage": "https://pipedream.com/apps/persona", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/persona/persona.app.mjs b/components/persona/persona.app.mjs new file mode 100644 index 0000000000000..861fa463b81b9 --- /dev/null +++ b/components/persona/persona.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "persona", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/personio/README.md b/components/personio/README.md new file mode 100644 index 0000000000000..a7dd21577ab97 --- /dev/null +++ b/components/personio/README.md @@ -0,0 +1,11 @@ +# Overview + +The Personio API provides programmatic access to HR management functions, allowing you to manage employee data, time tracking, payroll, and recruitment processes. With Pipedream, you can leverage this API to create automations and workflows that can streamline HR operations, react to events in Personio in real-time, and connect HR data to other systems seamlessly. + +# Example Use Cases + +- **Employee Onboarding Automation**: When a new hire is added to Personio, this workflow automatically sends a welcome email, creates accounts on necessary services like Slack or GitHub, and schedules onboarding meetings in your calendar app. It ensures new employees have a smooth start with all the resources they need. + +- **Leave Request Notification System**: Set up a workflow that listens for leave requests submitted in Personio, then automatically notifies team leads via email or Slack for approval. Once approved, it updates the Personio system, blocks out the time on the company's shared calendar, and informs teammates of the absence to plan accordingly. + +- **Recruitment Pipeline Tracker**: Whenever a candidate's status updates in Personio's recruitment module, this workflow triggers actions such as updating a Kanban board in Trello with the candidate's progress, sending status update emails to the hiring team, and archiving or activating candidate profiles based on their interview outcomes. diff --git a/components/pexels/README.md b/components/pexels/README.md index 47f097fd1c372..b7ea87cdae69f 100644 --- a/components/pexels/README.md +++ b/components/pexels/README.md @@ -1,28 +1,11 @@ # Overview -The Pexels API is a powerful tool for developers who want to create -applications and services with stock photo and video content. It provides -access to millions of professional-grade, high-quality images and videos from -over 60,000 contributing photographers and videographers. With the API, you can -quickly access Pexels' database of images and videos, search for the right -content to suit your specific project needs, and add the media to your own -application. +The Pexels API provides access to a well-curated library of high-quality photos and videos, all offered under the Pexels license which allows for a broad range of uses. On Pipedream, this API becomes a treasure trove for automations that require dynamic, attractive visuals. Whether for social media posts, content curation, or website updates, you can fetch and utilize rich media programmatically to enhance your digital projects. -The Pexels API provides a great opportunity to create projects that include -visuals, and here are some examples of what you can do with this API: +# Example Use Cases -- Build websites and applications with a more engaging look and feel that - appeal to your visitors. -- Create educational content such as tutorials, webinars, online courses and - more. -- Create ad campaigns and graphics for social media. -- Create eye-catching presentations. -- Create custom artwork for blogs, magazine covers and other publications. -- Create digital products such as video games, e-commerce sites and theme - parks. -- Create movie effects and animations. -- Create dynamic film and animation projects. -- And many more! +- **Dynamic Social Media Content**: Automatically select and post daily featured images or videos from Pexels to your social media platforms like Twitter or Instagram via Pipedream's platform. This can keep your feed fresh and engaging without manual intervention. -No matter what kind of digital creation you’re looking to build, the Pexels API -is there to support your project! +- **Website Content Refresh**: Use Pipedream to listen for a webhook that triggers when your website content needs an update. Fetch a new batch of images from Pexels and upload them directly to your CMS like WordPress, ensuring your site always has the latest visual trends. + +- **Email Marketing Campaigns**: Integrate the Pexels API with an email service provider like SendGrid on Pipedream. Curate and send personalized, visually appealing email campaigns by attaching images that resonate with the email's theme or intended audience. diff --git a/components/pexels/package.json b/components/pexels/package.json new file mode 100644 index 0000000000000..67455c9c44563 --- /dev/null +++ b/components/pexels/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/pexels", + "version": "0.6.0", + "description": "Pipedream pexels Components", + "main": "pexels.app.mjs", + "keywords": [ + "pipedream", + "pexels" + ], + "homepage": "https://pipedream.com/apps/pexels", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/phantombuster/README.md b/components/phantombuster/README.md index 6cf04c2cb6b0f..db25123d478df 100644 --- a/components/phantombuster/README.md +++ b/components/phantombuster/README.md @@ -1,28 +1,11 @@ # Overview -Phantombuster is a powerful API that enables users to build efficient web -automation solutions. It provides a pool of services and tools to help users -quickly, easily and securely interact with multiple websites at once. +Phantombuster is a tool that lets you automate interactions with web platforms to extract data, manage social media campaigns, and automate web actions. Using its API on Pipedream, you can seamlessly integrate these automation capabilities into complex workflows. Imagine orchestrating a symphony where Phantombuster plays the strings—pulling data from LinkedIn, sending tailored tweets, or automating lead generation—while Pipedream conducts, integrating these actions with other apps and services for a harmonious data-driven performance. -With Phantombuster, users can create custom automated solutions to perform -various tasks such as data extraction, lead generation, marketing automation, -and web scraping. Here are a few of the things users can build using -Phantombuster: +# Example Use Cases -- Data extraction - Phantombuster's API allows users to quickly and securely - extract data from multiple sites and APIs, allowing users to access large - amounts of data at once and extract only what they need. -- Lead generation - Phantombuster's API connects to multiple social networks - and websites, allowing users to quickly and accurately gather leads or - potential contacts and store them into a database. -- Automated marketing campaigns - Phantombuster's API enables users to create - and launch automated marketing campaigns, automating the entire process and - saving time and money. -- Web scraping - Phantombuster's API allows users to scrape entire webpages or - just parts of it, enabling streamlined data years. -- Robot monitoring - Phantombuster's API enables users to monitor robots, - allowing them to keep an eye on their tasks and ensuring their bots are - running as efficiently as possible. -- Data analytics - Phantombuster's API allows users to easily analyze and - visualize their gathered data, allowing them to quickly make decisions based - on their collected data. +- **LinkedIn Lead Generation and Outreach**: Trigger a Phantombuster automation to scrape LinkedIn profiles based on specified criteria. Use Pipedream to process this data, filter leads, and send personalized messages through LinkedIn or sync with a CRM like Salesforce to follow up. + +- **Twitter Engagement Booster**: Set up a workflow where Phantombuster automatically likes, retweets, or follows users based on specific hashtags or keywords. Pipedream can then store these interactions in a Google Sheet for analysis and further engage with these users by scheduling follow-up tweets or direct messages. + +- **Instagram Content Planner**: Use Phantombuster to pull popular posts and hashtags for given topics on Instagram. With Pipedream, analyze the popularity trends and automatically schedule your posts through Buffer or Hootsuite to maximize engagement, ensuring your content hits the mark every time. diff --git a/components/phaxio/README.md b/components/phaxio/README.md index 48a8408f8d4b8..d76d7e6555216 100644 --- a/components/phaxio/README.md +++ b/components/phaxio/README.md @@ -1,24 +1,11 @@ # Overview -Using the Phaxio API, you can quickly and easily build integrations that allow -your users to send payments, verify user information, securely sign documents, -and much more. +The Phaxio API turns the cumbersome fax communication into a streamlined digital process. By leveraging Phaxio with Pipedream, you can automate sending and receiving faxes, manage fax statuses, and integrate fax capabilities into your existing digital workflows without dealing with traditional fax machines. This can be particularly useful for industries like healthcare, legal, or finance where faxing documents is still prevalent. -For example, you can: +# Example Use Cases -- Request payments: Allow your users to make payments via secure phone-based - two-factor authentication. -- Verify user information: Confirm identity with automated two-tier voice call - verification. -- Sign documents: Allow users to sign documents securely and remotely with your - own custom web-based or app-based workflow. -- Send multimedia messages: Push updates, reminders, and documents to your - customers with multimedia messages that can be sent as email, SMS, and/or - voice. -- Build custom business intelligence applications: Gather data from phone - conversations to gain insights into customer conversations, call trends, and - other interesting correlations. -- Create confirmation-based notifications: Allow your users to receive updates, - reminders, and confirmations via voice, email, or SMS. -- Integrate into existing systems: Leverage existing databases, applications, - and platforms such as CRMs, billing systems, notification services, and more. +- **Automated Invoice Submission**: Use Phaxio to fax invoices to clients automatically whenever a new sale is recorded in your e-commerce platform. Trigger a Pipedream workflow with a new order event, then send the invoice as a fax via Phaxio. + +- **Fax to Email Bridge**: Set up a Phaxio-Pipedream workflow to receive faxes and forward them as email attachments. This can streamline document intake for businesses that receive orders or documents via fax. + +- **Multi-step Approval Process**: Integrate Phaxio into an approval workflow where a document is faxed out, and then, upon receiving a confirmation, triggers follow-up tasks such as updating a CRM or notifying team members via apps like Slack. diff --git a/components/philips_hue/README.md b/components/philips_hue/README.md index f0721f3f2d687..7787a80494fea 100644 --- a/components/philips_hue/README.md +++ b/components/philips_hue/README.md @@ -1,20 +1,11 @@ # Overview -Philips Hue offers a full suite of APIs that allow developers to use their -lights to create amazing applications and experiences. The Philips Hue API -allows developers to control an array of features and settings that make the -lights in a room act as if they are alive. Here are some of the things you can -build with the Philip Hue API: +The Philips Hue API offers rich control over Philips Hue smart lighting systems. Via Pipedream, you can automate your lighting based on various triggers – think syncing lights with the sun's cycle, changing color to indicate new emails, or even integrating with motion sensors for a smart-home experience. With Pipedream's capabilities to connect to countless services, the possibilities of creating personalized lighting scenarios are vast, limited only by your imagination and the availability of triggers or events from other services and devices. -- Color changing effects like fading between different colors, flashing between - colors and creating various moods throughout the room -- Scheduling lights to turn on and off based on time -- Automatically dimming lights based on light levels outside -- Automatically adjusting entire color pallets for a more natural look -- Providing a more immersive experience with your music and movies -- Creating virtual assistants that you can control using spoken commands -- Smart home automation for controlling other devices using voice commands and - creating specific rules for the lights -- Creating immersive gaming experiences with the lights -- Developing applications to monitor the amount of energy your lights are using - and to optimize your energy consumption +# Example Use Cases + +- **Sunset/Sunrise Lighting**: Adjust your Philips Hue lights to gradually turn on as the sun sets and dim as the sun rises by integrating with a weather API on Pipedream. This automation can mimic natural lighting and provide a gentle transition into and out of the day. + +- **Email Notification Lights**: Get visual notifications for new important emails by connecting your Philips Hue lights to your email service on Pipedream. Set up a workflow where lights blink or change color when an email from a VIP list comes in, allowing you to stay informed without constantly checking your inbox. + +- **Smart Home Security**: Enhance your home security system by linking Philips Hue with motion sensors through Pipedream. Program your lights to turn on or change color if unexpected motion is detected after certain hours, potentially deterring unwelcome visitors and alerting you to check your security cameras. diff --git a/components/philips_hue/package.json b/components/philips_hue/package.json new file mode 100644 index 0000000000000..f5e455c19a54b --- /dev/null +++ b/components/philips_hue/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/philips_hue", + "version": "0.6.0", + "description": "Pipedream philips_hue Components", + "main": "philips_hue.app.mjs", + "keywords": [ + "pipedream", + "philips_hue" + ], + "homepage": "https://pipedream.com/apps/philips_hue", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/phone_com/README.md b/components/phone_com/README.md index eb0a5ed4816f1..39ee4e1f776e8 100644 --- a/components/phone_com/README.md +++ b/components/phone_com/README.md @@ -1,21 +1,11 @@ # Overview -Phone.com offers a powerful API that makes it easy for developers to build -advanced communications tools for their customers. With the API you can create -custom voice, messaging and caller ID applications, as well as advanced call -routing and custom voicemail capabilities. The API also enables developers to -integrate customer databases and contact lists, allowing users to quickly and -easily access contacts, information and services. +Phone.com's API offers a programmable way to interact with its VoIP services, allowing you to streamline communication tasks, manage call settings, send SMS messages, and retrieve call logs programmatically. By leveraging Phone.com's functionalities within Pipedream, you can automate a variety of telephony workflows, integrate with other services for enhanced communication experiences, and analyze call data for actionable insights. -Using the Phone.com API, developers can create a number of applications to -strengthen customer communications, including: +# Example Use Cases -- Automated calling and scheduling systems -- Voicebots and conversational AI -- Real-time call routing -- Interactive voice response (IVR) applications -- Conference and video calls -- Voicemail and caller ID applications -- Customized outbound call lists -- Custom call rules -- Text messaging applications +- **Automated SMS Notifications for Calendar Events**: Integrate Phone.com with Google Calendar using Pipedream to send SMS reminders for upcoming events. When a new event is added to a specific Google Calendar, trigger a workflow that automatically sends a text message to the participants through Phone.com's SMS feature, ensuring everyone is notified timely. + +- **Dynamic Call Routing Based on CRM Data**: Combine Phone.com with a CRM platform like Salesforce within Pipedream. Create a workflow that checks incoming phone numbers against CRM contacts. If the caller is identified as a high-value customer, the call is routed to a senior team member or their account manager, enhancing customer service and prioritization. + +- **Voice Transcription Analysis and Logging**: Use Phone.com's voice-to-text capabilities alongside cloud storage services such as Google Drive in Pipedream. Implement a workflow where calls are transcribed by Phone.com, and the transcriptions are then automatically saved and analyzed in Google Sheets for keyword spotting, sentiment analysis, or compliance purposes. diff --git a/components/phoneburner/README.md b/components/phoneburner/README.md new file mode 100644 index 0000000000000..6d79960d6e847 --- /dev/null +++ b/components/phoneburner/README.md @@ -0,0 +1,11 @@ +# Overview + +The PhoneBurner API provides programmatic access to call center functionality, allowing you to manage contacts, handle dial sessions, and interact with call logs. With Pipedream, you can automate workflows that respond to events in PhoneBurner or orchestrate PhoneBurner actions based on triggers from other apps. This melds the power of cloud telephony with broad integration possibilities, unlocking productivity gains and streamlined operations. + +# Example Use Cases + +- **Sync Contacts from CRM to PhoneBurner**: Create a workflow in Pipedream that listens for new or updated contacts in your CRM system (like Salesforce or HubSpot). When a change is detected, the workflow can automatically update or add the contact's details to PhoneBurner, keeping your call lists current without manual entry. + +- **Automate Follow-Up Emails After Calls**: After a call is completed in PhoneBurner, trigger a Pipedream workflow that sends a personalized follow-up email via a service like SendGrid or Gmail. This workflow can pull details from the call log to tailor the message, ensuring timely and relevant communication with the prospect or customer. + +- **Log Calls to a Database or Google Sheets**: Use Pipedream to catch webhook events from PhoneBurner after calls are made. The workflow can then log call details, such as duration, outcome, and notes, into a database like PostgreSQL or a Google Sheets spreadsheet for record-keeping and advanced analysis. diff --git a/components/php_point_of_sale/README.md b/components/php_point_of_sale/README.md new file mode 100644 index 0000000000000..51a4324513427 --- /dev/null +++ b/components/php_point_of_sale/README.md @@ -0,0 +1,11 @@ +# Overview + +The PHP Point of Sale API offers a robust set of features to interact with your sales data, inventory, and customer information, allowing for seamless integration into various business processes. Using Pipedream, you can automate actions based on events in PHP Point of Sale, such as new sales or inventory changes, and connect to countless other services to streamline your retail operations. Pipedream provides a platform to create serverless workflows that can listen for webhooks, run code, and use pre-built actions to interact with the PHP Point of Sale API, all while enabling you to orchestrate data between multiple apps effortlessly. + +# Example Use Cases + +- **Sales Data Analysis**: Integrate PHP Point of Sale with Google Sheets on Pipedream to automatically export new sales data. Analyze trends, create visual reports, and share up-to-date information with your team in real-time. + +- **Inventory Management Notifications**: Set up a workflow that monitors inventory levels in PHP Point of Sale. When stock for a popular item falls below a certain threshold, automatically send restock requests to suppliers or alert staff via Slack or email. + +- **Customer Engagement Improvement**: Enhance customer relationships by connecting PHP Point of Sale to a CRM like HubSpot. When a sale is completed, add or update customer details in your CRM, and trigger personalized follow-up emails or SMS messages to encourage repeat business. diff --git a/components/php_point_of_sale/actions/create-register/create-register.mjs b/components/php_point_of_sale/actions/create-register/create-register.mjs new file mode 100644 index 0000000000000..d3071e231b1b9 --- /dev/null +++ b/components/php_point_of_sale/actions/create-register/create-register.mjs @@ -0,0 +1,44 @@ +import app from "../../php_point_of_sale.app.mjs"; + +export default { + key: "php_point_of_sale-create-register", + name: "Create Register", + description: "Creates a new register in the store. [See the documentation](https://phppointofsale.com/api.php#/registers/addregister)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "name", + ], + }, + iptranDeviceId: { + propDefinition: [ + app, + "iptranDeviceId", + ], + }, + emvTerminalId: { + propDefinition: [ + app, + "emvTerminalId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createRegister({ + $, + data: { + name: this.name, + iptran_device_id: this.iptranDeviceId, + emv_terminal_id: this.emvTerminalId, + }, + }); + + $.export("$summary", "Successfully created a new register"); + + return response; + }, +}; diff --git a/components/php_point_of_sale/actions/delete-register/delete-register.mjs b/components/php_point_of_sale/actions/delete-register/delete-register.mjs new file mode 100644 index 0000000000000..74755c0264f59 --- /dev/null +++ b/components/php_point_of_sale/actions/delete-register/delete-register.mjs @@ -0,0 +1,28 @@ +import app from "../../php_point_of_sale.app.mjs"; + +export default { + key: "php_point_of_sale-delete-register", + name: "Delete a Register", + description: "Deletes a register from PHP Point of Sale. [See the documentation](https://phppointofsale.com/api.php#/registers/deleteregister)", + version: "0.0.1", + type: "action", + props: { + app, + registerId: { + propDefinition: [ + app, + "registerId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.deleteRegister({ + $, + register_id: this.registerId, + }); + + $.export("$summary", `Successfully deleted register with ID: ${this.registerId}`); + + return response; + }, +}; diff --git a/components/php_point_of_sale/actions/get-registers/get-registers.mjs b/components/php_point_of_sale/actions/get-registers/get-registers.mjs new file mode 100644 index 0000000000000..73dfe65414258 --- /dev/null +++ b/components/php_point_of_sale/actions/get-registers/get-registers.mjs @@ -0,0 +1,30 @@ +import app from "../../php_point_of_sale.app.mjs"; + +export default { + key: "php_point_of_sale-get-registers", + name: "Get Registers", + description: "Search for registers in PHP Point Of Sale. [See the documentation](https://phppointofsale.com/api.php#/registers/searchregisters)", + version: "0.0.1", + type: "action", + props: { + app, + search: { + propDefinition: [ + app, + "search", + ], + }, + }, + async run({ $ }) { + const results = await this.app.searchRegisters({ + $, + params: { + search: this.search, + }, + }); + + $.export("$summary", "Successfully retrieved registers"); + + return results; + }, +}; diff --git a/components/php_point_of_sale/package.json b/components/php_point_of_sale/package.json index c05b70a947a7d..2aea235271b57 100644 --- a/components/php_point_of_sale/package.json +++ b/components/php_point_of_sale/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/php_point_of_sale", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream PHP Point of Sale Components", "main": "php_point_of_sale.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" } -} \ No newline at end of file +} diff --git a/components/php_point_of_sale/php_point_of_sale.app.mjs b/components/php_point_of_sale/php_point_of_sale.app.mjs index 47b3a53eeaf77..4c4875b311d74 100644 --- a/components/php_point_of_sale/php_point_of_sale.app.mjs +++ b/components/php_point_of_sale/php_point_of_sale.app.mjs @@ -1,11 +1,96 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "php_point_of_sale", - propDefinitions: {}, + propDefinitions: { + registerId: { + type: "string", + label: "Register ID", + description: "The ID of the register to delete or update", + async options({ page }) { + const resources = await this.searchRegisters({ + params: { + offset: page * 100, + limit: 100, + }, + }); + + return resources.map(({ + register_id, name, + }) => ({ + value: register_id, + label: name, + })); + }, + }, + name: { + type: "string", + label: "Name", + description: "The name of the register", + }, + iptranDeviceId: { + type: "string", + label: "IP Tran Device ID", + description: "The ID of the IP Tran Device", + optional: true, + }, + emvTerminalId: { + type: "string", + label: "EMV Terminal ID", + description: "The ID of the EMV Terminal", + optional: true, + }, + search: { + type: "string", + label: "Search Term", + description: "The term to search within the registers", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `https://${this.$auth.domain}/index.php/api/v1`; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "x-api-key": `${this.$auth.api_key}`, + }, + params, + }); + }, + async createRegister(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/registers", + ...args, + }); + }, + async searchRegisters(args = {}) { + return this._makeRequest({ + method: "GET", + path: "/registers", + ...args, + }); + }, + async deleteRegister({ + register_id, ...args + }) { + return this._makeRequest({ + method: "DELETE", + path: `/registers/${register_id}`, + ...args, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/phrase/README.md b/components/phrase/README.md index 71a4c23a31ac8..1f632c2304eb2 100644 --- a/components/phrase/README.md +++ b/components/phrase/README.md @@ -1,20 +1,11 @@ # Overview -Using the Phrase API, you can build a range of applications for local and -international businesses. With the Phrase API, you can do things like: +The Phrase API provides a suite of tools for CRM (Customer Relationship Management) to manage and nurture customer interactions and data throughout the customer lifecycle. Using Pipedream, developers can create automated workflows that tap into the power of the Phrase API to streamline communication, manage leads, and enhance customer relationships. With Pipedream's no-code platform, these workflows can trigger on events, process data, and connect to hundreds of other services for a wide array of use cases. -- Create powerful multilingual websites, apps and content -- Speed up the development of new software, websites and apps -- Dynamic internationalization of websites, apps and content -- Simplify localization workflow and processes -- Automate delivery of translated pages and content +# Example Use Cases -Here are a few examples of what you can build with the Phrase API: +- **Lead Qualification Automation**: Automatically qualify leads in Phrase by setting up a workflow that triggers when a new lead is captured through a web form. Integrate with a third-party service that enriches lead data, score those leads based on the enriched data and predefined criteria, and update the leads' status in Phrase accordingly. -- Software localization platforms -- Translated websites -- Custom multilingual content services -- Machine-translated content delivery networks -- Localized mobile apps -- Globalized translation workflows -- Internationalizing data entry forms +- **Customer Support Ticketing**: Create a seamless support experience by automating the creation of support tickets in Phrase from incoming emails or form submissions. Use Pipedream to parse the emails, extract relevant information, and create a new ticket in Phrase. Connect with Slack to send a notification to the support team whenever a new ticket is created for immediate action. + +- **Marketing Campaign Feedback Loop**: Enhance marketing efforts by setting up a workflow that captures campaign responses from various platforms, aggregates the data, and updates contact records in Phrase. Analyze the impact of the campaign on customer engagement and generate reports for better decision-making on future campaigns. Pipedream's integration with apps like Google Sheets or Airtable can store and organize this data for easy analysis. diff --git a/components/picdefense/actions/check-credits/check-credits.mjs b/components/picdefense/actions/check-credits/check-credits.mjs new file mode 100644 index 0000000000000..1f22c7a9b7cc9 --- /dev/null +++ b/components/picdefense/actions/check-credits/check-credits.mjs @@ -0,0 +1,21 @@ +import app from "../../picdefense.app.mjs"; + +export default { + key: "picdefense-check-credits", + name: "Check Credits", + description: "Check how many credits the associated account have. [See the documentation](https://app.picdefense.io/apidocs/)", + version: "0.0.2", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.checkCredits({ + $, + }); + + $.export("$summary", `The associated account have: '${response.data.credits}' credits`); + + return response; + }, +}; diff --git a/components/picdefense/actions/check-image-risk/check-image-risk.mjs b/components/picdefense/actions/check-image-risk/check-image-risk.mjs new file mode 100644 index 0000000000000..2884c02858df2 --- /dev/null +++ b/components/picdefense/actions/check-image-risk/check-image-risk.mjs @@ -0,0 +1,30 @@ +import app from "../../picdefense.app.mjs"; + +export default { + key: "picdefense-check-image-risk", + name: "Check Image Risk", + description: "Check the risk of the image on the provided URL. [See the documentation](https://app.picdefense.io/apidocs/)", + version: "0.0.2", + type: "action", + props: { + app, + url: { + propDefinition: [ + app, + "url", + ], + }, + }, + async run({ $ }) { + const response = await this.app.checkImageRisk({ + $, + data: { + url: this.url, + }, + }); + + $.export("$summary", `Image check: ${response.message}`); + + return response; + }, +}; diff --git a/components/picdefense/package.json b/components/picdefense/package.json new file mode 100644 index 0000000000000..75cfcc6ec7456 --- /dev/null +++ b/components/picdefense/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/picdefense", + "version": "0.1.1", + "description": "Pipedream PicDefense Components", + "main": "picdefense.app.mjs", + "keywords": [ + "pipedream", + "picdefense" + ], + "homepage": "https://pipedream.com/apps/picdefense", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/picdefense/picdefense.app.mjs b/components/picdefense/picdefense.app.mjs new file mode 100644 index 0000000000000..4a0d0bdd8b6a4 --- /dev/null +++ b/components/picdefense/picdefense.app.mjs @@ -0,0 +1,47 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "picdefense", + propDefinitions: { + url: { + type: "string", + label: "URL", + description: "URL of the image that is going to be checked", + }, + }, + methods: { + _baseUrl() { + return "https://app.picdefense.io/api/v2"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "X-API-TOKEN": `${this.$auth.user_id}:${this.$auth.api_key}`, + }, + }); + }, + async checkImageRisk(args = {}) { + return this._makeRequest({ + method: "post", + path: "/checkImageRisk", + ...args, + }); + }, + async checkCredits(args = {}) { + return this._makeRequest({ + path: "/credits", + ...args, + }); + }, + }, +}; diff --git a/components/picky_assist/README.md b/components/picky_assist/README.md index 2a28acc1dbdf8..987d28003c4fd 100644 --- a/components/picky_assist/README.md +++ b/components/picky_assist/README.md @@ -1,25 +1,11 @@ # Overview -Picky Assist is a powerful, intuitive API that can help you build great digital -experiences, ranging from e-commerce websites to customer relationship -management software. With Picky Assist, you can quickly and easily customize -and add new features to your existing web applications and programs, allowing -you to maximize your use of technology and provide an improved online -experience for your customers and users. +Picky Assist bridges your communication channels with smart automation, letting you weave SMS, WhatsApp, and other messaging services into your business workflows. By integrating with Pipedream, you can automate interactions with customers, streamline notifications, or orchestrate multi-platform messaging campaigns. The API enables you to trigger messages based on events, sync conversations to CRMs, or even dispatch alerts from monitoring systems. -Here are just some of the things you can build with the Picky Assist API: +# Example Use Cases -- Robust customer relationship management tools. -- Secure payment gateways and merchant accounts. -- Interactive e-commerce shopping carts. -- Shopping carts with real-time discounts and promotions. -- Customizable customer feedback forms. -- Web-based appointment scheduling systems. -- Registration, licensing, and other forms of online customer authentication. -- Professional online forms for data capture and reporting. -- Flexible customer messaging and reporting portals. -- Customized front- And back-end customer surveys. -- Automated email follow-up campaigns. -- Integrated collaboration and task management tools. -- Comprehensive analytics dashboards. -- Product/service tracking and order management tools. +- **Customer Support Ticket Creation**: When a customer sends a message via WhatsApp, Picky Assist can receive that message and use Pipedream to create a new support ticket in Zendesk or another customer service platform, ensuring no request is left unattended. + +- **Real-Time Order Updates**: Connect Picky Assist with an e-commerce platform like Shopify on Pipedream. Automate the process of sending personalized order confirmation and shipping updates via SMS to customers when their order status changes. + +- **Event-Triggered Alerts**: Combine Picky Assist with monitoring tools like Datadog. Set up a workflow on Pipedream that triggers an SMS to the on-call team if there's an incident or performance anomaly detected, providing timely response capabilities. diff --git a/components/pidj/README.md b/components/pidj/README.md new file mode 100644 index 0000000000000..4f5719745c8d7 --- /dev/null +++ b/components/pidj/README.md @@ -0,0 +1,11 @@ +# Overview + +The Pidj API allows users to manage and automate SMS communication directly through their platform. Integrating Pidj with Pipedream offers a powerful way to streamline communications, synchronize data across apps, and trigger SMS actions based on specific conditions. With Pipedream, you can effortlessly connect Pidj to other apps, creating sophisticated, multi-step workflows that react in real-time to events across your software stack. + +# Example Use Cases + +- **Customer Support Ticket Alert**: Trigger an SMS alert via Pidj whenever a high-priority support ticket is created in Zendesk. The workflow listens for new Zendesk tickets, filters for priority level, and sends an SMS to the assigned support agent, ensuring immediate attention. + +- **E-commerce Order Confirmation**: Send an order confirmation SMS through Pidj when a customer completes a purchase on Shopify. This workflow activates on a new Shopify order event, extracts customer details, and sends a personalized SMS confirmation, enhancing the customer experience. + +- **Appointment Reminder System**: Automatically send appointment reminders to clients a day before their scheduled time, using Pidj to dispatch the messages. This workflow can be triggered by calendar events from Google Calendar, fetching appointment details and sending timely SMS reminders to minimize no-shows. diff --git a/components/pidj/actions/create-contact/create-contact.mjs b/components/pidj/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..7977572e4dab8 --- /dev/null +++ b/components/pidj/actions/create-contact/create-contact.mjs @@ -0,0 +1,172 @@ +import { TIMEZONE_OPTIONS } from "../../common/constants.mjs"; +import pidj from "../../pidj.app.mjs"; + +export default { + key: "pidj-create-contact", + name: "Create Contact", + description: "Initiates a process to add a new contact to your Pidj account. [See the documentation](https://pidj.co/wp-content/uploads/2023/06/Pidj-API-Technical-Document-v3-1.pdf).", + version: "0.0.1", + type: "action", + props: { + pidj, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "Phone number for the contact. This must be in E.164 format. **e.g., +18885552222**", + }, + emailAddress: { + type: "string", + label: "Email Address", + description: "The contact's email address.", + optional: true, + }, + firstName: { + type: "string", + label: "First Name", + description: "The contact's first name.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "The contact's last name.", + optional: true, + }, + businessName: { + type: "string", + label: "Business Name", + description: "The contact's business name.", + optional: true, + }, + displayName: { + type: "string", + label: "Display Name", + description: "The contact's display name, if present, and different from first and last name.", + optional: true, + }, + groupId: { + propDefinition: [ + pidj, + "groupId", + ], + optional: true, + }, + timezone: { + type: "string", + label: "Timezone", + description: "Timezone identifier for the contact. If omitted, this will default to the account value.", + options: TIMEZONE_OPTIONS, + optional: true, + }, + externalId: { + type: "string", + label: "External Id", + description: "The external ID of the contact from your own system. This may be used in any enabled integrations to cross-identify the contact record.", + optional: true, + }, + homeAddressStreet1: { + type: "string", + label: "Home Street 1", + description: "The home street address 1.", + optional: true, + }, + homeAddressStreet2: { + type: "string", + label: "Home Street 2", + description: "The home street address 2.", + optional: true, + }, + homeAddressCity: { + type: "string", + label: "Home City", + description: "The home city address.", + optional: true, + }, + homeAddressState: { + type: "string", + label: "Home State", + description: "The home state address. Must be the standard 2 character abbreviation (**ANSI2-letter** or **USPS**). Any other values will be discarded. [See further information](https://en.wikipedia.org/wiki/List_of_U.S._state_abbreviations).", + optional: true, + }, + homeAddressPostal: { + type: "string", + label: "Home Postal", + description: "Valid 5 or 10 digit zipcode (US addresses). British, Canadian, and Australian postal codes also supported.", + optional: true, + }, + homeAddressCountry: { + type: "string", + label: "Home Country", + description: "Must be either **ISO 3166-1 alpha-2** or **ISO 3166-1 alpha-3**. Any other values will be discarded. [See further information](https://en.wikipedia.org/wiki/ISO_3166-1).", + optional: true, + }, + businessAddressStreet1: { + type: "string", + label: "Business Street 1", + description: "The business street address 1.", + optional: true, + }, + businessAddressStreet2: { + type: "string", + label: "Business Street 2", + description: "The business street address 2.", + optional: true, + }, + businessAddressCity: { + type: "string", + label: "Business City", + description: "The business city address.", + optional: true, + }, + businessAddressState: { + type: "string", + label: "Business State", + description: "The business state address. Must be the standard 2 character abbreviation (**ANSI2-letter** or **USPS**). Any other values will be discarded. [See further information](https://en.wikipedia.org/wiki/List_of_U.S._state_abbreviations).", + optional: true, + }, + businessAddressPostal: { + type: "string", + label: "Business Postal", + description: "Valid 5 or 10 digit zipcode (US addresses). British, Canadian, and Australian postal codes also supported.", + optional: true, + }, + businessAddressCountry: { + type: "string", + label: "Business Country", + description: "Must be either **ISO 3166-1 alpha-2** or **ISO 3166-1 alpha-3**. Any other values will be discarded. [See further information](https://en.wikipedia.org/wiki/ISO_3166-1).", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.pidj.addContact({ + $, + data: { + phone_number: this.phoneNumber, + email_address: this.emailAddress, + first_name: this.firstName, + last_name: this.lastName, + business_name: this.businessName, + display_name: this.displayName, + group: this.groupId, + timezone: this.timezone, + external_id: this.externalId, + addresses: { + home_street_1: this.homeAddressStreet1, + home_street_2: this.homeAddressStreet2, + home_city: this.homeAddressCity, + home_state: this.homeAddressState, + home_postal: this.homeAddressPostal, + home_country: this.homeAddressCountry, + business_street_1: this.businessAddressStreet1, + business_street_2: this.businessAddressStreet2, + business_city: this.businessAddressCity, + business_state: this.businessAddressState, + business_postal: this.businessAddressPostal, + business_country: this.businessAddressCountry, + }, + }, + }); + $.export("$summary", `Successfully added a new contact with Id: ${response.id}`); + return response; + }, +}; diff --git a/components/pidj/actions/initiate-survey/initiate-survey.mjs b/components/pidj/actions/initiate-survey/initiate-survey.mjs new file mode 100644 index 0000000000000..d05a665b2f4ee --- /dev/null +++ b/components/pidj/actions/initiate-survey/initiate-survey.mjs @@ -0,0 +1,57 @@ +import { ConfigurationError } from "@pipedream/platform"; +import pidj from "../../pidj.app.mjs"; + +export default { + key: "pidj-initiate-survey", + name: "Initiate Survey", + description: "Triggers a pre-configured text survey to a specific contact. Requires the contact's information and the survey ID. [See the documentation](https://pidj.co/wp-content/uploads/2023/06/Pidj-API-Technical-Document-v3-1.pdf).", + version: "0.0.1", + type: "action", + props: { + pidj, + surveyId: { + propDefinition: [ + pidj, + "surveyId", + ], + }, + contactId: { + propDefinition: [ + pidj, + "contactId", + ], + }, + fromNumber: { + propDefinition: [ + pidj, + "fromNumber", + ], + optional: true, + }, + groupId: { + propDefinition: [ + pidj, + "groupId", + ], + optional: true, + }, + }, + async run({ $ }) { + if ((!this.fromNumber && !this.groupId) || (this.fromNumber && this.groupId)) { + throw new ConfigurationError("You must provide either From Number or Group Id."); + } + + const response = await this.pidj.triggerSurvey({ + $, + data: { + survey_id: this.surveyId, + contact_id: this.contactId, + from_number: this.fromNumber, + group_id: this.groupId, + }, + }); + + $.export("$summary", `Successfully initiated survey for contact ${this.contactId}`); + return response; + }, +}; diff --git a/components/pidj/actions/send-message/send-message.mjs b/components/pidj/actions/send-message/send-message.mjs new file mode 100644 index 0000000000000..a3b6a6ce8606d --- /dev/null +++ b/components/pidj/actions/send-message/send-message.mjs @@ -0,0 +1,44 @@ +import { ConfigurationError } from "@pipedream/platform"; +import pidj from "../../pidj.app.mjs"; + +export default { + key: "pidj-send-message", + name: "Send Message", + description: "Sends a text message to a specified phone number from your pidj account. [See the documentation](https://pidj.co/wp-content/uploads/2023/06/Pidj-API-Technical-Document-v3-1.pdf).", + version: "0.0.1", + type: "action", + props: { + pidj, + groupId: { + propDefinition: [ + pidj, + "groupId", + ], + }, + toNumber: { + propDefinition: [ + pidj, + "toNumber", + ], + }, + textBody: { + type: "string", + label: "Text Body", + description: "The text to send. Message lengths greater than 1,600 characters will be truncated. Messages with emoji and special characters may have a smaller limit due to encoding requirements. [See Pidj FAQ for details](https://pidjco.gopidj.com/faq).", + }, + }, + async run({ $ }) { + const response = await this.pidj.sendMessage({ + $, + data: { + group_id: this.groupId, + to_number: this.toNumber, + text_body: this.textBody, + }, + }); + if (response.status != "success") throw new ConfigurationError(response.message); + + $.export("$summary", `Message successfully sent to ${this.toNumber}`); + return response; + }, +}; diff --git a/components/pidj/common/constants.mjs b/components/pidj/common/constants.mjs new file mode 100644 index 0000000000000..7e1205e5046a7 --- /dev/null +++ b/components/pidj/common/constants.mjs @@ -0,0 +1,599 @@ +export const TIMEZONE_OPTIONS = [ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmara", + "Africa/Asmera", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Timbuktu", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/ComodRivadavia", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Atikokan", + "America/Atka", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Buenos_Aires", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Catamarca", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Ciudad_Juarez", + "America/Coral_Harbour", + "America/Cordoba", + "America/Costa_Rica", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Ensenada", + "America/Fort_Nelson", + "America/Fort_Wayne", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indianapolis", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Jujuy", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Knox_IN", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Louisville", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Mendoza", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montreal", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Nuuk", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Acre", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Rosario", + "America/Santa_Isabel", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Shiprock", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Virgin", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/South_Pole", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Ashkhabad", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Calcutta", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Chongqing", + "Asia/Chungking", + "Asia/Colombo", + "Asia/Dacca", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Harbin", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Istanbul", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kashgar", + "Asia/Kathmandu", + "Asia/Katmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macao", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qostanay", + "Asia/Qyzylorda", + "Asia/Rangoon", + "Asia/Riyadh", + "Asia/Saigon", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Tel_Aviv", + "Asia/Thimbu", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ujung_Pandang", + "Asia/Ulaanbaatar", + "Asia/Ulan_Bator", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faeroe", + "Atlantic/Faroe", + "Atlantic/Jan_Mayen", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/ACT", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Canberra", + "Australia/Currie", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/LHI", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/North", + "Australia/NSW", + "Australia/Perth", + "Australia/Queensland", + "Australia/South", + "Australia/Sydney", + "Australia/Tasmania", + "Australia/Victoria", + "Australia/West", + "Australia/Yancowinna", + "Brazil/Acre", + "Brazil/DeNoronha", + "Brazil/East", + "Brazil/West", + "Canada/Atlantic", + "Canada/Central", + "Canada/Eastern", + "Canada/Mountain", + "Canada/Newfoundland", + "Canada/Pacific", + "Canada/Saskatchewan", + "Canada/Yukon", + "CET", + "Chile/Continental", + "Chile/EasterIsland", + "CST6CDT", + "Cuba", + "EET", + "Egypt", + "Eire", + "EST", + "EST5EDT", + "Etc/GMT", + "Etc/GMT+0", + "Etc/GMT+1", + "Etc/GMT+10", + "Etc/GMT+11", + "Etc/GMT+12", + "Etc/GMT+2", + "Etc/GMT+3", + "Etc/GMT+4", + "Etc/GMT+5", + "Etc/GMT+6", + "Etc/GMT+7", + "Etc/GMT+8", + "Etc/GMT+9", + "Etc/GMT-0", + "Etc/GMT-1", + "Etc/GMT-10", + "Etc/GMT-11", + "Etc/GMT-12", + "Etc/GMT-13", + "Etc/GMT-14", + "Etc/GMT-2", + "Etc/GMT-3", + "Etc/GMT-4", + "Etc/GMT-5", + "Etc/GMT-6", + "Etc/GMT-7", + "Etc/GMT-8", + "Etc/GMT-9", + "Etc/GMT0", + "Etc/Greenwich", + "Etc/UCT", + "Etc/Universal", + "Etc/UTC", + "Etc/Zulu", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belfast", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Kyiv", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Nicosia", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Tiraspol", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zaporozhye", + "Europe/Zurich", + "Factory", + "GB", + "GB-Eire", + "GMT", + "GMT+0", + "GMT-0", + "GMT0", + "Greenwich", + "Hongkong", + "HST", + "Iceland", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Iran", + "Israel", + "Jamaica", + "Japan", + "Kwajalein", + "Libya", + "MET", + "Mexico/BajaNorte", + "Mexico/BajaSur", + "Mexico/General", + "MST", + "MST7MDT", + "Navajo", + "NZ", + "NZ-CHAT", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Johnston", + "Pacific/Kanton", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Ponape", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Samoa", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Truk", + "Pacific/Wake", + "Pacific/Wallis", + "Pacific/Yap", + "Poland", + "Portugal", + "PRC", + "PST8PDT", + "ROC", + "ROK", + "Singapore", + "Turkey", + "UCT", + "Universal", + "US/Alaska", + "US/Aleutian", + "US/Arizona", + "US/Central", + "US/East-Indiana", + "US/Eastern", + "US/Hawaii", + "US/Indiana-Starke", + "US/Michigan", + "US/Mountain", + "US/Pacific", + "US/Samoa", + "UTC", + "W-SU", + "WET", + "Zulu", +]; diff --git a/components/pidj/package.json b/components/pidj/package.json new file mode 100644 index 0000000000000..a088d58551a78 --- /dev/null +++ b/components/pidj/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/pidj", + "version": "0.1.0", + "description": "Pipedream Pidj Components", + "main": "pidj.app.mjs", + "keywords": [ + "pipedream", + "pidj" + ], + "homepage": "https://pipedream.com/apps/pidj", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/pidj/pidj.app.mjs b/components/pidj/pidj.app.mjs new file mode 100644 index 0000000000000..30bfed651a7eb --- /dev/null +++ b/components/pidj/pidj.app.mjs @@ -0,0 +1,125 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "pidj", + propDefinitions: { + contactId: { + type: "string", + label: "Contact ID", + description: "The ID of the contact.", + async options() { + const { contacts } = await this.listContacts(); + + return contacts.map(({ + id: value, first_name, last_name, display_name, phone_number, + }) => ({ + label: (first_name && last_name) + ? `${first_name} ${last_name}` + : `${display_name || phone_number}`, + value, + })); + }, + }, + groupId: { + type: "string", + label: "Group Id", + description: "The Id of the group.", + async options() { + const { groups } = await this.listGroups(); + + return groups.map(({ + group_id: value, name: label, + }) => ({ + label, + value, + })); + }, + optional: true, + }, + surveyId: { + type: "string", + label: "Survey ID", + description: "The ID of the survey to be triggered.", + async options() { + const { surveys } = await this.listSurveys(); + + return surveys.map(({ + survey_id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + fromNumber: { + type: "string", + label: "From Number", + description: "Phone number to send from; this will determine which group the message sends from, and must be an active number from one of your groups. This must be in E.164 format, e.g., +18885552222", + }, + toNumber: { + type: "string", + label: "To Number", + description: "Phone number to send to. This must be in E.164 format, e.g., +18885553333.", + }, + }, + methods: { + _baseUrl() { + return "https://api.gopidj.com/api/2020"; + }, + _auth() { + return { + username: `${this.$auth.account_key}`, + password: `${this.$auth.token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + auth: this._auth(), + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + ...opts, + }); + }, + addContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contact", + ...opts, + }); + }, + listGroups() { + return this._makeRequest({ + path: "/groups", + }); + }, + listSurveys() { + return this._makeRequest({ + path: "/surveys", + }); + }, + listContacts() { + return this._makeRequest({ + path: "/contacts", + }); + }, + triggerSurvey(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/survey/initiate", + ...opts, + }); + }, + sendMessage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/send", + ...opts, + }); + }, + }, +}; diff --git a/components/piggy/README.md b/components/piggy/README.md new file mode 100644 index 0000000000000..e6ed34bdfa8ce --- /dev/null +++ b/components/piggy/README.md @@ -0,0 +1,11 @@ +# Overview + +The Piggy API provides a suite of loyalty and reward services allowing businesses to manage customer loyalty programs, offer discounts, and track customer engagement. With Pipedream, you can harness this powerful API to automate various aspects of your loyalty program. You might sync loyalty data with other business systems, trigger emails or notifications based on customer actions, or generate reports on the effectiveness of your loyalty campaigns. + +# Example Use Cases + +- **Sync Loyalty Points with CRM**: Automatically update a customer's loyalty points in your CRM whenever they earn or redeem points through Piggy. This ensures all customer touchpoints have the most current information, enhancing personalization and customer service. + +- **Rewards Notification System**: Create a system that notifies customers via email or SMS when they're close to a reward, have earned a new reward, or when their reward is about to expire. You could connect Piggy to an email service like SendGrid or a messaging service like Twilio to automate these communications. + +- **Automate Monthly Loyalty Reports**: Set up a scheduled workflow on Pipedream to generate monthly reports on loyalty program metrics. This could involve aggregating data from the Piggy API and sending it to Google Sheets, Slack, or an email address, providing regular, actionable insights to the marketing team. diff --git a/components/piggy/actions/find-or-create-contact/find-or-create-contact.mjs b/components/piggy/actions/find-or-create-contact/find-or-create-contact.mjs index 16dfd568f2d80..49e0f63f8484f 100644 --- a/components/piggy/actions/find-or-create-contact/find-or-create-contact.mjs +++ b/components/piggy/actions/find-or-create-contact/find-or-create-contact.mjs @@ -2,7 +2,7 @@ import app from "../../piggy.app.mjs"; export default { name: "Find Or Create Contact", - version: "0.0.1", + version: "0.0.2", key: "piggy-find-or-create-contact", description: "Find or create a contact. [See the documentation](https://docs.piggy.eu/v3/oauth/contacts#:~:text=Possible%20errors-,Find%20or%20Create%20Contact,-Find%20Contact%20by)", type: "action", diff --git a/components/piggy/package.json b/components/piggy/package.json index b0636b1fabe7c..227b0c6fe2963 100644 --- a/components/piggy/package.json +++ b/components/piggy/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/piggy", - "version": "0.1.0", + "version": "0.1.1", "description": "Pipedream Piggy Components", "main": "piggy.app.mjs", "keywords": [ diff --git a/components/pikaso/README.md b/components/pikaso/README.md index 9fb806476386f..ea76fb1cb03c4 100644 --- a/components/pikaso/README.md +++ b/components/pikaso/README.md @@ -1,21 +1,11 @@ # Overview -Build powerful and user-friendly applications with the Pikaso API. Pikaso API -is a REST API that allows developers to integrate data from multiple sources -into their applications and deliver them in one centralized platform. +The Pikaso API lets you capture and customize screenshots of tweets with defined styles, sizes, and themes. Harness this power within Pipedream to automate the process of capturing tweet images for reporting, archiving, or sharing across various platforms. Think of it as a bridge between Twitter's textual content and visually-oriented platforms or documents. -Pikaso API helps to access data from a wide range of sources such as Github, -Twitter, Google Calendar and many more. It also provides all the necessary -tools needed to build, test and maintain APIs and applications. With the Pikaso -API, developers can save time and reduce effort in managing data from multiple -sources. +# Example Use Cases -Here are some of the things developers can build with the Pikaso API: +- **Automated Tweet Reporting**: Set up a Pipedream workflow that listens for tweets from specific accounts or containing certain hashtags using Twitter's API. Whenever a relevant tweet appears, the workflow triggers the Pikaso API to capture a screenshot and then sends that image to a Google Drive folder for organized reporting. -- Custom Mobile Applications -- Web Applications -- Data Aggregators -- Access Controls -- Workflows and Automation -- IoT Solutions -- Cloud and Server Solutions +- **Social Media Management**: Incorporate Pikaso into a Pipedream workflow that helps manage your social media. After crafting and posting a tweet, use Pikaso to capture the tweet's image. Then, programmatically post the screenshot to other social media platforms like Instagram or Facebook, repurposing content across channels. + +- **Tweet Archive Building**: Create an automated archival system on Pipedream where every tweet from your company's Twitter account is captured by Pikaso and then stored in Airtable or a similar database. This can serve as a visual archive for reference, marketing materials, or compliance purposes. diff --git a/components/piloterr/README.md b/components/piloterr/README.md new file mode 100644 index 0000000000000..1eac054965997 --- /dev/null +++ b/components/piloterr/README.md @@ -0,0 +1,11 @@ +# Overview + +Piloterr API allows you to integrate and automate various aspects of project management and team collaboration. Within Pipedream's serverless platform, you can leverage this API to create custom workflows, connecting Piloterr's capabilities with other apps to streamline operations, trigger actions based on project updates, and synchronize data across different tools. + +# Example Use Cases + +- **Automate Task Creation from Emails**: Create a workflow that listens for incoming emails on a designated address. Use this to automatically generate tasks in Piloterr, ensuring that requests sent via email are promptly converted into actionable items within your project management system. + +- **Sync Project Updates to Slack**: Set up a workflow that triggers whenever there's an update in Piloterr, such as task completion or status changes. Use this trigger to send notifications to a Slack channel, keeping your team informed about project progress in real-time. + +- **Aggregate Time Tracking Data**: Design a workflow that periodically retrieves time tracking data from Piloterr. Connect this data with a reporting app like Google Sheets or Data Studio to analyze productivity trends and optimize team performance. diff --git a/components/pilvio/README.md b/components/pilvio/README.md index f25170fe25044..cde6dc2d3b0db 100644 --- a/components/pilvio/README.md +++ b/components/pilvio/README.md @@ -1,23 +1,11 @@ # Overview -The [Pilvio API](https://pilvio.com/) allows developers to rapidly build -interactive web, mobile and desktop applications with efficient, fast and -secure cloud hosting. This can be done on the Pilvio platform, which is highly -extensible and enables users to seamlessly integrate cloud-based applications. +The Pilvio API is a powerful tool that allows users to manage virtual phone systems. With it, you can create and manage phone numbers, handle inbound and outbound calls, send and receive SMS messages, and implement IVR (Interactive Voice Response) systems. When combined with Pipedream's capabilities, the Pilvio API can automate various aspects of communication workflows, effectively integrating telephony features into your business processes or applications. -The Pilvio API can handle almost any kind of project quickly, ranging from -simple static websites to complex and highly interactive multi-tier -applications. It also increases the speed and security of development and -hosting with its built-in security layer and wide range of configuration -options. +# Example Use Cases -The Pilvio API allows developers to create: +- **Customer Support Automation**: Use the Pilvio API to build a workflow that automatically routes incoming customer support calls to the appropriate department. When a call is received, the workflow can assess the caller's input in an IVR menu and forward the call, log the interaction in a CRM like Salesforce, and send a follow-up SMS to the caller with a ticket number. -- Interactive web applications -- Mobile applications -- Desktop applications -- Highly secure cloud hosting solutions -- Secure and fast web-based applications -- Multi-tier applications -- Storage and caching solutions -- Scalable and extensible platforms +- **Marketing Campaign Tracking**: Create a workflow that assigns unique phone numbers to different marketing campaigns using the Pilvio API. Whenever someone calls or texts one of these numbers, the workflow logs the interaction in Google Sheets or a marketing platform like HubSpot for real-time campaign performance tracking. It can also trigger automated SMS responses thanking the caller for their interest. + +- **Appointment Reminders and Confirmation**: Implement a system that sends out automated SMS appointment reminders to clients. The Pilvio API can be used to schedule and send these messages. When a client responds with a confirmation or rescheduling request, the workflow can update the appointment in a calendar app like Google Calendar and notify the service provider via Slack or email. diff --git a/components/pinboard/README.md b/components/pinboard/README.md index efb6eaa28b459..7c4cda54ca65d 100644 --- a/components/pinboard/README.md +++ b/components/pinboard/README.md @@ -1,22 +1,11 @@ # Overview -Building projects with the Pinboard API can open up a variety of creative and -functional ways of utilizing the power of Pinboard's bookmarking and -organization service. Here are some of the things developers can create with -the the Pinboard API: +The Pinboard API on Pipedream allows for the automation of social media content curation and sharing by interacting with a user's Pinboard account. This API provides the ability to programmatically add, update, and retrieve bookmarks, enabling users to streamline their bookmark management. Leveraging Pipedream's capabilities, developers can create sophisticated workflows that react to various triggers, manipulate data, and connect to numerous other services to enhance productivity and data management. -- A custom bookmark application that allows people to browse and organize their - bookmarks and tags in an interface tailored to their needs. -- A tool that simplifies adding multiple bookmarks to Pinboard in one step. -- A plugin that adds bookmarking functionality to websites and applications. -- A mobile app that enables users to search, categorize, add, and share their - bookmarks on the go. -- A search engine-style interface that allows users to quickly search their - bookmarks. -- A tool to easily share bookmarks with others or link to them in social media - posts. -- A timeline-style interface that allows users to visualize their bookmarked - sites and discover patterns over time. -- A timeline-style interface that allows users to visualize the popular - bookmarks within their network of contacts. -- An editor to manage and edit bookmark titles, content, and tags. +# Example Use Cases + +- **Automated Bookmark Archiving**: Create a workflow that triggers daily, searching for bookmarks tagged with "archive" in your Pinboard account, and automatically backing them up to a Google Drive folder. This ensures a secure and accessible copy of your essential bookmarks. + +- **Content Sharing Pipeline**: Develop a system where any new bookmark tagged "share" on Pinboard triggers a Pipedream workflow, which then formats the content and posts it to your Twitter and LinkedIn profiles, helping maintain an active social media presence with relevant links. + +- **Email Digest of New Bookmarks**: Set up a Pipedream workflow that compiles a weekly email digest of all new bookmarks added to Pinboard with a specific tag, such as "read-later", and sends this curated list to your preferred email address, keeping you informed about your bookmarked content without manual checking. diff --git a/components/pinboard/package.json b/components/pinboard/package.json new file mode 100644 index 0000000000000..8433ac226050e --- /dev/null +++ b/components/pinboard/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/pinboard", + "version": "0.6.0", + "description": "Pipedream pinboard Components", + "main": "pinboard.app.mjs", + "keywords": [ + "pipedream", + "pinboard" + ], + "homepage": "https://pipedream.com/apps/pinboard", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/pinecone/README.md b/components/pinecone/README.md new file mode 100644 index 0000000000000..1f7798a37dfc7 --- /dev/null +++ b/components/pinecone/README.md @@ -0,0 +1,11 @@ +# Overview + +The Pinecone API enables you to work with vector databases, which are essential for building and scaling applications with AI features like recommendation systems, image recognition, and natural language processing. On Pipedream, you can create serverless workflows integrating Pinecone with other apps, automate data ingestion, query vector databases in response to events, and orchestrate complex data processing pipelines that leverage Pinecone's similarity search. + +# Example Use Cases + +- **Customer Support Ticket Routing**: Automate the categorization and routing of customer support tickets by integrating Pinecone with a customer support platform like Zendesk. Use machine learning models to encode ticket text into vectors and query Pinecone to find similar past tickets for quick resolution. + +- **Real-time Product Recommendations**: Build a workflow that triggers when users view products on an e-commerce platform like Shopify. Capture viewing events, process product descriptions through ML models to obtain vectors, and query Pinecone to generate real-time personalized product recommendations for users. + +- **Content Discovery and Matching**: Connect Pinecone with a content management system like WordPress. When new content is published, convert it to a vector representation and upload it to Pinecone. Set up a workflow that responds to user queries by searching Pinecone for the most relevant content vectors, enhancing content discoverability. diff --git a/components/pinecone/actions/delete-vectors/delete-vectors.mjs b/components/pinecone/actions/delete-vectors/delete-vectors.mjs index 9855c6e3b85b3..0e38590a69832 100644 --- a/components/pinecone/actions/delete-vectors/delete-vectors.mjs +++ b/components/pinecone/actions/delete-vectors/delete-vectors.mjs @@ -4,9 +4,9 @@ import utils from "../../common/utils.mjs"; export default { key: "pinecone-delete-vectors", name: "Delete Vectors", - description: "Deletes one or more vectors by ID, from a single namespace. [See the docs](https://docs.pinecone.io/reference/delete_post).", + description: "Deletes one or more vectors by ID, from a single namespace. [See the documentation](https://docs.pinecone.io/reference/api/data-plane/delete).", type: "action", - version: "0.0.1", + version: "0.0.2", props: { app, indexName: { @@ -15,10 +15,16 @@ export default { "indexName", ], }, - projectId: { + prefix: { propDefinition: [ app, - "projectId", + "prefix", + ], + }, + namespace: { + propDefinition: [ + app, + "namespace", ], }, ids: { @@ -28,24 +34,12 @@ export default { propDefinition: [ app, "vectorId", - ({ - indexName, projectId, - }) => ({ - indexName, - projectId, - }), - ], - }, - namespace: { - propDefinition: [ - app, - "namespace", ], }, }, methods: { deleteVector(args = {}) { - return this.app.create({ + return this.app.post({ path: "/vectors/delete", ...args, }); @@ -53,14 +47,14 @@ export default { }, async run({ $: step }) { const { + deleteVector, indexName, - projectId, ids, namespace, } = this; - await this.deleteVector({ - projectId, + await deleteVector({ + step, indexName, data: { ids: utils.parseArray(ids), @@ -69,5 +63,8 @@ export default { }); step.export("$summary", "Successfully deleted vectors"); + return { + success: true, + }; }, }; diff --git a/components/pinecone/actions/fetch-vectors/fetch-vectors.mjs b/components/pinecone/actions/fetch-vectors/fetch-vectors.mjs index 54d1a9b50f3c7..bdd62171b2c79 100644 --- a/components/pinecone/actions/fetch-vectors/fetch-vectors.mjs +++ b/components/pinecone/actions/fetch-vectors/fetch-vectors.mjs @@ -4,9 +4,9 @@ import app from "../../pinecone.app.mjs"; export default { key: "pinecone-fetch-vectors", name: "Fetch Vectors", - description: "Looks up and returns vectors by ID, from a single namespace.. [See the docs](https://docs.pinecone.io/reference/fetch).", + description: "Looks up and returns vectors by ID, from a single namespace.. [See the documentation](https://docs.pinecone.io/reference/api/data-plane/fetch).", type: "action", - version: "0.0.1", + version: "0.0.2", props: { app, indexName: { @@ -15,10 +15,10 @@ export default { "indexName", ], }, - projectId: { + namespace: { propDefinition: [ app, - "projectId", + "namespace", ], }, ids: { @@ -28,31 +28,27 @@ export default { propDefinition: [ app, "vectorId", - ({ - indexName, projectId, - }) => ({ - indexName, - projectId, - }), ], }, - namespace: { - propDefinition: [ - app, - "namespace", - ], + }, + methods: { + fetchVectors(args = {}) { + return this.app.makeRequest({ + path: "/vectors/fetch", + ...args, + }); }, }, async run({ $: step }) { const { + fetchVectors, indexName, - projectId, ids, namespace, } = this; - const response = await this.app.fetchVectors({ - projectId, + const response = await fetchVectors({ + step, indexName, params: { ids: utils.parseArray(ids), @@ -60,7 +56,7 @@ export default { }, }); - step.export("$summary", `Successfully fetched ${Object.keys(response?.vectors).length} vectors.`); + step.export("$summary", `Successfully fetched \`${Object.keys(response.vectors).length}\` vector(s).`); return response; }, diff --git a/components/pinecone/actions/query-ids/query-ids.mjs b/components/pinecone/actions/query-ids/query-ids.mjs index 5f99a0a86e449..ab4267a8a658c 100644 --- a/components/pinecone/actions/query-ids/query-ids.mjs +++ b/components/pinecone/actions/query-ids/query-ids.mjs @@ -4,9 +4,9 @@ import utils from "../../common/utils.mjs"; export default { key: "pinecone-query-ids", name: "Query IDs", - description: "Searches a namespace, using a query vector. It retrieves the ids of the most similar items in a namespace, along with their similarity scores. [See the docs](https://docs.pinecone.io/reference/query).", + description: "Searches a namespace, using a query vector. It retrieves the ids of the most similar items in a namespace, along with their similarity scores. [See the documentation](https://docs.pinecone.io/reference/api/data-plane/query).", type: "action", - version: "0.0.1", + version: "0.0.2", props: { app, indexName: { @@ -15,12 +15,6 @@ export default { "indexName", ], }, - projectId: { - propDefinition: [ - app, - "projectId", - ], - }, topK: { type: "integer", label: "Top K", @@ -32,7 +26,7 @@ export default { filter: { type: "object", label: "Filter", - description: "The filter to apply. You can use vector metadata to limit your search. [See the docs](https://www.pinecone.io/docs/metadata-filtering/).", + description: "The filter to apply. You can use vector metadata to limit your search. For guidance and examples, see [filtering-with-metadata](https://docs.pinecone.io/guides/data/filtering-with-metadata).", optional: true, }, includeValues: { @@ -59,16 +53,9 @@ export default { propDefinition: [ app, "vectorId", - ({ - indexName, projectId, - }) => ({ - indexName, - projectId, - }), ], }, vector: { - optional: true, description: `${app.propDefinitions.vectorValues.description} Each request can contain only one of the parameters either **Vector Values** or **Vector ID**.`, propDefinition: [ app, @@ -76,10 +63,18 @@ export default { ], }, }, + methods: { + query(args = {}) { + return this.app.post({ + path: "/query", + ...args, + }); + }, + }, async run({ $: step }) { const { + query, indexName, - projectId, id, vector, topK, @@ -89,12 +84,16 @@ export default { namespace, } = this; - const response = await this.app.query({ - projectId, + const vectorParsed = utils.parseArray(vector); + + const response = await query({ + step, indexName, data: { id, - vector: utils.parseArray(vector), + ...(vectorParsed.length && { + vector: vectorParsed, + }), topK, filter: utils.parse(filter), includeValues, diff --git a/components/pinecone/actions/update-vector/update-vector.mjs b/components/pinecone/actions/update-vector/update-vector.mjs index 688c6b332819e..f8ed46d514702 100644 --- a/components/pinecone/actions/update-vector/update-vector.mjs +++ b/components/pinecone/actions/update-vector/update-vector.mjs @@ -4,9 +4,9 @@ import app from "../../pinecone.app.mjs"; export default { key: "pinecone-update-vector", name: "Update Vector", - description: "Updates vector in a namespace. If a value is included, it will overwrite the previous value. [See the docs](https://docs.pinecone.io/reference/update).", + description: "Updates vector in a namespace. If a value is included, it will overwrite the previous value. [See the documentation](https://docs.pinecone.io/reference/api/data-plane/update).", type: "action", - version: "0.0.1", + version: "0.0.2", props: { app, indexName: { @@ -15,22 +15,10 @@ export default { "indexName", ], }, - projectId: { - propDefinition: [ - app, - "projectId", - ], - }, id: { propDefinition: [ app, "vectorId", - ({ - indexName, projectId, - }) => ({ - indexName, - projectId, - }), ], }, values: { @@ -54,7 +42,7 @@ export default { }, methods: { updateVector(args = {}) { - return this.app.create({ + return this.app.post({ path: "/vectors/update", ...args, }); @@ -62,25 +50,28 @@ export default { }, async run({ $: step }) { const { + updateVector, indexName, - projectId, id, values, metadata, namespace, } = this; - await this.updateVector({ - projectId, + await updateVector({ + step, indexName, data: { id, - values, + values: utils.parseArray(values), setMetadata: utils.parse(metadata), namespace, }, }); step.export("$summary", `Successfully updated vector with ID ${id}.`); + return { + success: true, + }; }, }; diff --git a/components/pinecone/actions/upsert-vector/upsert-vector.mjs b/components/pinecone/actions/upsert-vector/upsert-vector.mjs index 8e8a8e883f1fb..48663bb8b1afc 100644 --- a/components/pinecone/actions/upsert-vector/upsert-vector.mjs +++ b/components/pinecone/actions/upsert-vector/upsert-vector.mjs @@ -4,9 +4,9 @@ import utils from "../../common/utils.mjs"; export default { key: "pinecone-upsert-vector", name: "Upsert Vector", - description: "Writes vectors into a namespace. If a new value is upserted for an existing vector ID, it will overwrite the previous value. [See the docs](https://docs.pinecone.io/reference/upsert).", + description: "Writes vectors into a namespace. If a new value is upserted for an existing vector ID, it will overwrite the previous value. [See the documentation](https://docs.pinecone.io/reference/api/data-plane/upsert).", type: "action", - version: "0.0.1", + version: "0.0.2", props: { app, indexName: { @@ -15,25 +15,14 @@ export default { "indexName", ], }, - projectId: { - propDefinition: [ - app, - "projectId", - ], - }, id: { propDefinition: [ app, "vectorId", - ({ - indexName, projectId, - }) => ({ - indexName, - projectId, - }), ], }, values: { + optional: false, propDefinition: [ app, "vectorValues", @@ -54,7 +43,7 @@ export default { }, methods: { upsertVector(args = {}) { - return this.app.create({ + return this.app.post({ path: "/vectors/upsert", ...args, }); @@ -62,22 +51,22 @@ export default { }, async run({ $: step }) { const { + upsertVector, indexName, - projectId, id, values, metadata, namespace, } = this; - const response = await this.upsertVector({ - projectId, + const response = await upsertVector({ + step, indexName, data: { vectors: [ { id, - values, + values: utils.parseArray(values), metadata: utils.parse(metadata), }, ], diff --git a/components/pinecone/common/constants.mjs b/components/pinecone/common/constants.mjs index 3ddf0a85a967a..25613cf3f06ce 100644 --- a/components/pinecone/common/constants.mjs +++ b/components/pinecone/common/constants.mjs @@ -1,23 +1,8 @@ const INDEX_NAME_PLACEHOLDER = ""; -const PROJECT_ID_PLACEHOLDER = ""; -const ENV_PLACEHOLDER = ""; -const BASE_URL = `https://${INDEX_NAME_PLACEHOLDER}-${PROJECT_ID_PLACEHOLDER}.svc.${ENV_PLACEHOLDER}.pinecone.io`; -const CONTROLLER_URL = `https://controller.${ENV_PLACEHOLDER}.pinecone.io`; -const LAST_CREATED_AT = "lastCreatedAt"; +const BASE_URL = `https://${INDEX_NAME_PLACEHOLDER}`; +const CONTROLLER_URL = "https://api.pinecone.io"; const DEFAULT_MAX = 600; -const FILE_PROP_NAMES = [ - "attachment", - "uploaddoc", - "upload_file", -]; - -const CONTENT_TYPE_KEY_HEADER = "Content-Type"; -const MULTIPART_FORM_DATA_VALUE_HEADER = "multipart/form-data"; -const MULTIPART_FORM_DATA_HEADERS = { - [CONTENT_TYPE_KEY_HEADER]: MULTIPART_FORM_DATA_VALUE_HEADER, -}; - const API = { DEFAULT: "default", CONTROLLER: "controller", @@ -25,15 +10,8 @@ const API = { export default { API, - ENV_PLACEHOLDER, - PROJECT_ID_PLACEHOLDER, INDEX_NAME_PLACEHOLDER, BASE_URL, CONTROLLER_URL, DEFAULT_MAX, - LAST_CREATED_AT, - FILE_PROP_NAMES, - CONTENT_TYPE_KEY_HEADER, - MULTIPART_FORM_DATA_VALUE_HEADER, - MULTIPART_FORM_DATA_HEADERS, }; diff --git a/components/pinecone/package.json b/components/pinecone/package.json index 2698522ba74ae..a1141179b4f8f 100644 --- a/components/pinecone/package.json +++ b/components/pinecone/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/pinecone", - "version": "0.0.3", + "version": "0.0.4", "description": "Pipedream Pinecone Components", "main": "pinecone.app.mjs", "keywords": [ diff --git a/components/pinecone/pinecone.app.mjs b/components/pinecone/pinecone.app.mjs index 292adf8610a69..a74e8c2e75e6a 100644 --- a/components/pinecone/pinecone.app.mjs +++ b/components/pinecone/pinecone.app.mjs @@ -9,44 +9,22 @@ export default { indexName: { type: "string", label: "Index Name", - description: "The name of the index. E.g. `my-index`", - options() { - return this.listIndexes(); + description: "The name of the index. E.g. `my-index.pinecone.io`", + async options() { + const { indexes } = await this.listIndexes(); + return indexes.map(({ + name: label, host: value, + }) => ({ + label, + value, + })); }, }, - projectId: { + prefix: { type: "string", - label: "Project ID", - description: "The ID of the project. You can get the project from the Pinecone URL, for example https://app.pinecone.io/organizations/abcde/projects/us-east-1-aws:611e7f9/indexes, then the Project ID is `611e7f9`", - }, - vectorId: { - type: "string", - label: "Vector ID", - description: "The ID of the vector.", - async options({ - indexName, projectId, - }) { - if (!indexName || !projectId) { - return []; - } - - const { database: { dimension } } = await this.describeIndex({ - indexName, - }); - - const { matches } = await this.query({ - projectId, - indexName, - data: { - topK: dimension, - vector: Array.from({ - length: dimension, - }).map(() => 0), - }, - }); - - return matches?.map(({ id }) => id); - }, + label: "Prefix", + description: "The vector IDs to fetch. Does not accept values containing spaces. E.g. `my-prefix`", + optional: true, }, namespace: { type: "string", @@ -54,10 +32,16 @@ export default { description: "The namespace to use.", optional: true, }, + vectorId: { + type: "string", + label: "Vector ID", + description: "The ID of the vector.", + }, vectorValues: { type: "string[]", label: "Vector Values", description: "The values of the vector.", + optional: true, }, vectorMetadata: { type: "object", @@ -68,25 +52,17 @@ export default { }, methods: { getBaseUrl({ - api, projectId, indexName, + api, indexName, } = {}) { - const { environment } = this.$auth; return api === constants.API.CONTROLLER ? constants.CONTROLLER_URL - .replace(constants.ENV_PLACEHOLDER, environment) : constants.BASE_URL - .replace(constants.INDEX_NAME_PLACEHOLDER, indexName) - .replace(constants.PROJECT_ID_PLACEHOLDER, projectId) - .replace(constants.ENV_PLACEHOLDER, environment); + .replace(constants.INDEX_NAME_PLACEHOLDER, indexName); }, getUrl({ - api, path, projectId, indexName, + path, ...args } = {}) { - const baseUrl = this.getBaseUrl({ - api, - projectId, - indexName, - }); + const baseUrl = this.getBaseUrl(args); return `${baseUrl}${path}`; }, getHeaders(headers) { @@ -102,118 +78,32 @@ export default { }); }, makeRequest({ - step = this, api, headers, projectId, indexName, path, ...args + step = this, api, headers, indexName, path, ...args } = {}) { - const config = { + ...args, paramsSerializer: this.paramsSerializer, headers: this.getHeaders(headers), url: this.getUrl({ api, indexName, - projectId, path, }), - ...args, }; - return axios(step, config); }, - create(args = {}) { + post(args = {}) { return this.makeRequest({ method: "post", ...args, }); }, - update(args = {}) { - return this.makeRequest({ - method: "put", - ...args, - }); - }, - delete(args = {}) { - return this.makeRequest({ - method: "delete", - ...args, - }); - }, - patch(args = {}) { - return this.makeRequest({ - method: "patch", - ...args, - }); - }, - query(args = {}) { - return this.create({ - path: "/query", - ...args, - }); - }, - describeIndex({ - indexName, ...args - } = {}) { + listIndexes(args = {}) { return this.makeRequest({ api: constants.API.CONTROLLER, - path: `/databases/${indexName}`, + path: "/indexes", ...args, }); }, - listIndexes() { - return this.makeRequest({ - api: constants.API.CONTROLLER, - path: "/databases", - }); - }, - fetchVectors(args = {}) { - return this.makeRequest({ - path: "/vectors/fetch", - ...args, - }); - }, - async *getResourcesStream({ - resourceFn, - resourceFnArgs, - resourceName, - lastCreatedAt, - max = constants.DEFAULT_MAX, - }) { - let page = 1; - let resourcesCount = 0; - - while (true) { - const response = - await resourceFn({ - ...resourceFnArgs, - params: { - ...resourceFnArgs.params, - page, - }, - }); - - const nextResources = resourceName && response[resourceName] || response; - - if (!nextResources?.length) { - console.log("No more resources found"); - return; - } - - for (const resource of nextResources) { - const dateFilter = - lastCreatedAt - && Date.parse(resource.created_at) > Date.parse(lastCreatedAt); - - if (!lastCreatedAt || dateFilter) { - yield resource; - resourcesCount += 1; - } - - if (resourcesCount >= max) { - return; - } - } - - page += 1; - } - }, }, }; diff --git a/components/pingbell/README.md b/components/pingbell/README.md new file mode 100644 index 0000000000000..60d9ff6a03d59 --- /dev/null +++ b/components/pingbell/README.md @@ -0,0 +1,11 @@ +# Overview + +The PingBell API is a versatile tool for automating interactions with your physical PingBell device. Pipedream, a serverless integration and compute platform, helps you build workflows around the PingBell API with ease. You can craft automations that trigger when someone rings your PingBell, check the device's battery level, and much more, all within Pipedream's simplified workflow environment. By using Pipedream's capabilities, you can integrate with various apps, send notifications through different channels, log data, and implement complex logic to respond to PingBell events. + +# Example Use Cases + +- **Visitor Notification Workflow**: Create a Pipedream workflow that sends a Slack message to your home office channel whenever someone presses your PingBell. This ensures you're promptly notified of guests while working. + +- **Battery Level Monitor**: Set up a workflow that periodically checks the battery level of your PingBell and sends you an email alert when the battery falls below a certain threshold, so you'll know when it's time to replace it. + +- **Visitor Log with Google Sheets**: Design a workflow that logs every PingBell ring to a Google Sheet, including the time of the ring. This can be useful for tracking the frequency and timing of deliveries or visitors. diff --git a/components/pingdom/README.md b/components/pingdom/README.md new file mode 100644 index 0000000000000..9ebcfb211af44 --- /dev/null +++ b/components/pingdom/README.md @@ -0,0 +1,11 @@ +# Overview + +The Pingdom API offers a suite of endpoints to monitor the uptime, performance, and interactions for websites and web applications. With this API, you can automate the retrieval of monitor statuses, alerts, and performance data. Integrating Pingdom with Pipedream allows developers to create custom workflows that can notify teams of incidents, track performance metrics, or even trigger actions based on website status changes. + +# Example Use Cases + +- **Incident Response Notifications**: Create a workflow that listens for Pingdom alerts and automatically sends notifications to Slack, alerting your team immediately when a website goes down or experiences performance issues. + +- **Performance Metrics Reporting**: Build a workflow where you gather daily performance data from Pingdom and compile a report in Google Sheets. This can help track the uptime trends and response times of your websites over time, providing valuable insights into your web infrastructure's health. + +- **Auto-Remediation Triggers**: Set up a workflow that, upon receiving a down alert from Pingdom, triggers a series of diagnostic or remediation actions on AWS, like checking server statuses or rebooting instances, to attempt an automatic fix before manual intervention is needed. diff --git a/components/pingone/package.json b/components/pingone/package.json new file mode 100644 index 0000000000000..25f2650b23000 --- /dev/null +++ b/components/pingone/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/pingone", + "version": "0.0.1", + "description": "Pipedream PingOne Components", + "main": "pingone.app.mjs", + "keywords": [ + "pipedream", + "pingone" + ], + "homepage": "https://pipedream.com/apps/pingone", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/pingone/pingone.app.mjs b/components/pingone/pingone.app.mjs new file mode 100644 index 0000000000000..a10b29800fbb4 --- /dev/null +++ b/components/pingone/pingone.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "pingone", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/pingrabbit/package.json b/components/pingrabbit/package.json new file mode 100644 index 0000000000000..743db0f05cf55 --- /dev/null +++ b/components/pingrabbit/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/pingrabbit", + "version": "0.0.1", + "description": "Pipedream PingRabbit Components", + "main": "pingrabbit.app.mjs", + "keywords": [ + "pipedream", + "pingrabbit" + ], + "homepage": "https://pipedream.com/apps/pingrabbit", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/pingrabbit/pingrabbit.app.mjs b/components/pingrabbit/pingrabbit.app.mjs new file mode 100644 index 0000000000000..f9e84da13d456 --- /dev/null +++ b/components/pingrabbit/pingrabbit.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "pingrabbit", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/pinterest/README.md b/components/pinterest/README.md index 772f68f6434f1..585a399f7d32a 100644 --- a/components/pinterest/README.md +++ b/components/pinterest/README.md @@ -1,23 +1,11 @@ # Overview -The Pinterest API is an open service that allows developers to access the -Pinterest platform and build amazing experiences for Pinterest users. It -provides a wide range of capabilities for creating amazing applications that -allow users to view, save, and share beautiful images and content. With the -Pinterest API, you can build apps that let users browse, search and create -collections of images from around the web. You can also build apps that allow -users to easily organize and manage their Pins, Boards and Likes. +The Pinterest API opens a portal to interact programmatically with Pinterest's rich data, including boards, pins, and user information. By leveraging this API on Pipedream, you can automate actions like posting new pins, extracting pin data for analysis, and synchronizing Pinterest content with other platforms. The potential extends to marketing optimization, content management, and audience engagement, all automated and integrated within the Pipedream ecosystem. -Some of the great things you can build with the Pinterest API include: +# Example Use Cases -- An application that lets users search Pins across multiple sources -- A visual discovery tool that organizes pins into categories -- A creative showcase platform that lets users easily view and share the best - content -- A social network that allows users to interact with and follow other pins -- A personalized shopping experience that displays tailored product - recommendations based on user interests -- An app that lets users save their own customized collections of images and - content -- A messaging platform that lets users connect with brands, stores and friends - through group conversations +- **Content Synchronization Between E-commerce and Pinterest**: When a new product is added to an e-commerce site like Shopify, automatically create a pin for that product on a specified Pinterest board. Sync product updates or availability to Pinterest in real-time to engage users with the latest offerings. + +- **Social Media Analytics Dashboard**: Collect data from Pinterest such as pin performance, board statistics, and audience insights. Send this data to Google Sheets or a data visualization tool like Tableau to create a comprehensive analytics dashboard, enabling data-driven marketing strategies. + +- **Automated Inspirational Content Posting**: Set up a workflow where inspirational quotes, sourced from a platform like Quotes API, are periodically posted as pins to designated inspiration boards on Pinterest. This can keep your boards active and engaging without manual intervention. diff --git a/components/pipedream/README.md b/components/pipedream/README.md deleted file mode 100644 index a8ee4ff78156b..0000000000000 --- a/components/pipedream/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Overview - -Pipedream is an API that allows you to build applications that can connect to -various data sources and processes them in real-time. You can use Pipedream to -create applications that can perform ETL (Extract, Transform, and Load) tasks, -as well as to create data-driven workflows. - -Some examples of applications you can build using the Pipedream API include: - -- An application that can extract data from a database, transform it, and then - load it into another database. -- An application that can monitor a data source for changes, and then trigger a - workflow in response to those changes. -- An application that can poll an API for new data, and then process that data - in real-time. diff --git a/components/pipedream_connect/package.json b/components/pipedream_connect/package.json new file mode 100644 index 0000000000000..29c5e18e02052 --- /dev/null +++ b/components/pipedream_connect/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/pipedream_connect", + "version": "0.0.1", + "description": "Pipedream Pipedream Connect Components", + "main": "pipedream_connect.app.mjs", + "keywords": [ + "pipedream", + "pipedream_connect" + ], + "homepage": "https://pipedream.com/apps/pipedream_connect", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/pipedream_connect/pipedream_connect.app.mjs b/components/pipedream_connect/pipedream_connect.app.mjs new file mode 100644 index 0000000000000..ffcf591317aa9 --- /dev/null +++ b/components/pipedream_connect/pipedream_connect.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "pipedream_connect", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/pipedrive/README.md b/components/pipedrive/README.md index 187da1c76f174..e34e1121a8123 100644 --- a/components/pipedrive/README.md +++ b/components/pipedrive/README.md @@ -1,16 +1,11 @@ # Overview -# What can be Built with the Pipedrive API? +Pipedrive API on Pipedream allows you to create powerful sales automation and data management workflows. With access to Pipedrive's CRM capabilities, you can automate deal updates, contact management, and sales reporting. Whether you're syncing customer information across platforms or triggering actions based on deal stages, Pipedream makes these integrations seamless. -The Pipedrive API is a powerful source of data that allows developers to build custom applications and integrations that can "plug-and-play" into the back-end of Pipedrive. With a few lines of code, developers can create amazing ways to use Pipedrive's data for productivity and success. Here are a few examples of what can be done with the Pipedrive API: +# Example Use Cases -1. Automate the addition of leads into the back-end of Pipedrive from other sources such as Salesforce, or other 3rd party applications. -2. Customize new views and filters for managing deals and activities. -3. Integrate with other platforms to build custom reports and insights. -4. Develop custom modules such as invoice generation and payment processing. -5. Create custom tasks, reminders, and notifications that sync with other tools and platforms. -6. Automate the creation of tasks and activities based on arbitrary criteria. -7. Monitor and track changes that happen within Pipedrive with webhooks. -8. Create dynamic logic-driven processes with automated decision making. -9. Create custom dashboards and data visualizations to get an overview of the business performance. -10. Automatically export data to other cloud-based services such as Google Drive, Dropbox, or other cloud-based applications. +- **Lead Scoring and Distribution**: Automatically score leads based on activity and distribute them to the appropriate sales reps. When a new deal is added to Pipedrive, an automated workflow can assign a score based on predefined criteria and assign the lead to a rep in your sales team. + +- **Email Campaign Follow-Up**: After an email campaign, use Pipedrive's API to track responses and engagement. Create workflows to update deal status or move deals to the next stage when a prospect interacts with an email, ensuring timely follow-ups. + +- **Syncing with Marketing Platforms**: Keep your sales and marketing teams aligned by syncing Pipedrive contacts with a platform like Mailchimp. When a contact is updated in Pipedrive, a workflow can trigger an update in Mailchimp, ensuring both platforms have the most current information. diff --git a/components/pipedrive/actions/add-activity/add-activity.mjs b/components/pipedrive/actions/add-activity/add-activity.mjs index 22c683c3515df..d611e311fce78 100644 --- a/components/pipedrive/actions/add-activity/add-activity.mjs +++ b/components/pipedrive/actions/add-activity/add-activity.mjs @@ -1,12 +1,13 @@ +import { ConfigurationError } from "@pipedream/platform"; import constants from "../../common/constants.mjs"; -import utils from "../../common/utils.mjs"; +import { parseObject } from "../../common/utils.mjs"; import pipedriveApp from "../../pipedrive.app.mjs"; export default { key: "pipedrive-add-activity", name: "Add Activity", description: "Adds a new activity. Includes `more_activities_scheduled_in_context` property in response's `additional_data` which indicates whether there are more undone activities scheduled with the same deal, person or organization (depending on the supplied data). See the Pipedrive API docs for Activities [here](https://developers.pipedrive.com/docs/api/v1/#!/Activities). For info on [adding an activity in Pipedrive](https://developers.pipedrive.com/docs/api/v1/Activities#addActivity)", - version: "0.1.5", + version: "0.1.7", type: "action", props: { pipedriveApp, @@ -15,22 +16,6 @@ export default { label: "Subject", description: "Subject of the activity", }, - done: { - type: "string", - label: "Done", - description: "Whether the activity is done or not. 0 = Not done, 1 = Done", - optional: true, - options: [ - { - label: "Not done", - value: "0", - }, - { - label: "Done", - value: "1", - }, - ], - }, type: { type: "string", label: "Type", @@ -45,25 +30,7 @@ export default { })); }, }, - dueDate: { - type: "string", - label: "Due Date", - description: "Due date of the activity. Format: `YYYY-MM-DD`", - optional: true, - }, - dueTime: { - type: "string", - label: "Due Time", - description: "Due time of the activity in UTC. Format: `HH:MM`", - optional: true, - }, - duration: { - type: "string", - label: "Duration", - description: "Duration of the activity. Format: `HH:MM`", - optional: true, - }, - userId: { + ownerId: { propDefinition: [ pipedriveApp, "userId", @@ -76,148 +43,159 @@ export default { "dealId", ], }, - personId: { + leadId: { propDefinition: [ pipedriveApp, - "personId", + "leadId", ], - description: "ID of the person this activity will be associated with", }, - participants: { - type: "string[]", - label: "Participants", - description: "List of multiple persons (participants) this activity will be associated with. If omitted, single participant from `person_id` field is used. It requires a structure as follows: `[{\"person_id\":1,\"primary_flag\":true}]`", - optional: true, + orgId: { propDefinition: [ pipedriveApp, - "personId", + "organizationId", ], + description: "ID of the organization this activity will be associated with", }, - organizationId: { + projectId: { propDefinition: [ pipedriveApp, - "organizationId", + "projectId", ], - description: "ID of the organization this activity will be associated with", + description: "ID of the project this activity will be associated with", }, - note: { + dueDate: { type: "string", - label: "Note", - description: "Note of the activity (HTML format)", + label: "Due Date", + description: "Due date of the activity. Format: `YYYY-MM-DD`", optional: true, }, - location: { + dueTime: { type: "string", - label: "Location", - description: "The address of the activity. Pipedrive will automatically check if the location matches a geo-location on Google maps.", + label: "Due Time", + description: "Due time of the activity in UTC. Format: `HH:MM`", optional: true, }, - publicDescription: { + duration: { type: "string", - label: "Public Description", - description: "Additional details about the activity that will be synced to your external calendar. Unlike the note added to the activity, the description will be publicly visible to any guests added to the activity.", + label: "Duration", + description: "Duration of the activity. Format: `HH:MM`", optional: true, }, - busyFlag: { + busy: { type: "boolean", - label: "Busy Flag", + label: "Busy", description: "Set the activity as 'Busy' or 'Free'. If the flag is set to true, your customers will not be able to book that time slot through any Scheduler links", optional: true, }, + done: { + type: "boolean", + label: "Done", + description: "Whether the activity is done or not.", + optional: true, + }, + location: { + type: "object", + label: "Location", + description: "The address of the activity. Pipedrive will automatically check if the location matches a geo-location on Google maps.", + optional: true, + }, + participants: { + type: "string[]", + label: "Participants", + description: "List of multiple persons (participants) this activity will be associated with. If omitted, single participant from `person_id` field is used. It requires a structure as follows: `[{\"person_id\":1,\"primary\":true}]`", + optional: true, + propDefinition: [ + pipedriveApp, + "personId", + ], + }, attendees: { type: "string[]", label: "Attendees", - description: "Attendees of the activity. This can be either your existing Pipedrive contacts or an external email address. It requires a structure as follows: `[{\"email_address\":\"mail@example.org\"}]` or `[{\"person_id\":1, \"email_address\":\"mail@example.org\"}]`", + description: "Attendees of the activity. This can be either your existing Pipedrive contacts or an external email address. It requires a structure as follows: `[{\"email\":\"mail@example.org\"}]`", optional: true, async options({ prevContext }) { - const { - moreItemsInCollection, - start, - } = prevContext; - - if (moreItemsInCollection === false) { + if (prevContext?.cursor === false) { return []; } - const { data: persons, additional_data: additionalData, } = await this.pipedriveApp.getPersons({ - start, + cursor: prevContext.cursor, limit: constants.DEFAULT_PAGE_LIMIT, }); - const options = - persons?.flatMap(({ - name, email, - }) => email?.map(({ value }) => ({ + return { + options: persons?.flatMap(({ + name, emails, + }) => emails?.map(({ value }) => ({ label: name, value, - })).filter((option) => option?.value)); - - return { - options, + })).filter((option) => option?.value)), context: { - moreItemsInCollection: additionalData.pagination.more_items_in_collection, - start: additionalData.pagination.next_start, + cursor: additionalData.next_cursor, }, }; }, }, + publicDescription: { + type: "string", + label: "Public Description", + description: "Additional details about the activity that will be synced to your external calendar. Unlike the note added to the activity, the description will be publicly visible to any guests added to the activity.", + optional: true, + }, + note: { + type: "string", + label: "Note", + description: "Note of the activity (HTML format)", + optional: true, + }, }, async run({ $ }) { const { - subject, - type, + pipedriveApp, dueDate, dueTime, - duration, - userId, + ownerId, dealId, - personId, - organizationId, - note, + leadId, + orgId, + projectId, location, + participants, + attendees, publicDescription, - busyFlag, + ...data } = this; - const participants = utils.parseOrUndefined(this.participants); - const attendees = utils.parseOrUndefined(this.attendees); - const done = utils.parseOrUndefined(this.done); - try { const resp = - await this.pipedriveApp.addActivity({ - subject, - done, - type, + await pipedriveApp.addActivity({ due_date: dueDate, due_time: dueTime, - duration, - user_id: userId, + owner_id: ownerId, deal_id: dealId, - person_id: personId, - participants: participants?.map((value, idx) => ({ + lead_id: leadId, + participants: parseObject(participants)?.map((value, idx) => ({ person_id: value, - primary_flag: !idx, + primary: !idx, })), - org_id: organizationId, - note, - location, + org_id: orgId, + project_id: projectId, + location: parseObject(location), public_description: publicDescription, - busy_flag: busyFlag, - attendees: attendees?.map((value) => ({ - email_address: value, + attendees: parseObject(attendees)?.map((value) => ({ + email: value, })), + ...data, }); $.export("$summary", "Successfully added activity"); return resp; - } catch (error) { - console.error(error.context?.body || error); - throw error.context?.body?.error || "Failed to add activity"; + } catch ({ error }) { + throw new ConfigurationError(error); } }, }; diff --git a/components/pipedrive/actions/add-deal/add-deal.mjs b/components/pipedrive/actions/add-deal/add-deal.mjs index a0dcf042ab4f4..aa309c5c8a6e0 100644 --- a/components/pipedrive/actions/add-deal/add-deal.mjs +++ b/components/pipedrive/actions/add-deal/add-deal.mjs @@ -1,10 +1,11 @@ +import { ConfigurationError } from "@pipedream/platform"; import pipedriveApp from "../../pipedrive.app.mjs"; export default { key: "pipedrive-add-deal", name: "Add Deal", description: "Adds a new deal. See the Pipedrive API docs for Deals [here](https://developers.pipedrive.com/docs/api/v1/Deals#addDeal)", - version: "0.1.5", + version: "0.1.7", type: "action", props: { pipedriveApp, @@ -14,40 +15,47 @@ export default { "dealTitle", ], }, - value: { + ownerId: { propDefinition: [ pipedriveApp, - "dealValue", + "userId", ], }, - currency: { + personId: { propDefinition: [ pipedriveApp, - "dealCurrency", + "personId", ], }, - userId: { + orgId: { propDefinition: [ pipedriveApp, - "userId", + "organizationId", ], }, - personId: { + pipelineId: { propDefinition: [ pipedriveApp, - "personId", + "pipelineId", ], + optional: true, }, - organizationId: { + stageId: { propDefinition: [ pipedriveApp, - "organizationId", + "stageId", ], }, - stageId: { + value: { propDefinition: [ pipedriveApp, - "stageId", + "dealValue", + ], + }, + currency: { + propDefinition: [ + pipedriveApp, + "dealCurrency", ], }, status: { @@ -74,52 +82,30 @@ export default { "visibleTo", ], }, - addTime: { - propDefinition: [ - pipedriveApp, - "addTime", - ], - }, }, async run({ $ }) { - const { - title, - value, - currency, - userId, - personId, - organizationId, - stageId, - status, - probability, - lostReason, - visibleTo, - addTime, - } = this; - try { const resp = await this.pipedriveApp.addDeal({ - title, - value, - currency, - user_id: userId, - person_id: personId, - org_id: organizationId, - stage_id: stageId, - status, - probability, - lost_reason: lostReason, - visible_to: visibleTo, - add_time: addTime, + title: this.title, + owner_id: this.ownerId, + person_id: this.personId, + org_id: this.orgId, + pipeline_id: this.pipelineId, + stage_id: this.stageId, + value: this.value, + currency: this.currency, + status: this.status, + probability: this.probability, + lost_reason: this.lostReason, + visible_to: this.visibleTo, }); $.export("$summary", "Successfully added deal"); return resp; - } catch (error) { - console.error(error.context?.body || error); - throw error.context?.body?.error || "Failed to add deal"; + } catch ({ error }) { + throw new ConfigurationError(error); } }, }; diff --git a/components/pipedrive/actions/add-note/add-note.mjs b/components/pipedrive/actions/add-note/add-note.mjs index 4f22533aae34e..1d75bbaccc846 100644 --- a/components/pipedrive/actions/add-note/add-note.mjs +++ b/components/pipedrive/actions/add-note/add-note.mjs @@ -1,18 +1,25 @@ +import { ConfigurationError } from "@pipedream/platform"; import pipedriveApp from "../../pipedrive.app.mjs"; export default { key: "pipedrive-add-note", name: "Add Note", description: "Adds a new note. For info on [adding an note in Pipedrive](https://developers.pipedrive.com/docs/api/v1/Notes#addNote)", - version: "0.0.3", + version: "0.0.5", type: "action", props: { pipedriveApp, content: { + type: "string", + label: "Content", + description: "The content of the note in HTML format. Subject to sanitization on the back-end.", + }, + leadId: { propDefinition: [ pipedriveApp, - "content", + "leadId", ], + description: "The ID of the lead the note will be attached to. This property is required unless one of (deal_id/person_id/org_id) is specified.", }, dealId: { propDefinition: [ @@ -49,62 +56,56 @@ export default { ], }, pinnedToDealFlag: { - propDefinition: [ - pipedriveApp, - "pinnedToDealFlag", - ], + type: "boolean", + label: "Pinned To Deal Flag", + description: "If set, the results are filtered by note to deal pinning state (deal_id is also required)", + default: false, + optional: true, + }, + pinnedToLeadFlag: { + type: "boolean", + label: "Pinned To Lead Flag", + description: "If set, the results are filtered by note to lead pinning state (lead_id is also required)", + default: false, optional: true, }, pinnedToOrgFlag: { - propDefinition: [ - pipedriveApp, - "pinnedToOrgFlag", - ], + type: "boolean", + label: "Pinned To Organization Flag", + description: "If set, the results are filtered by note to organization pinning state (org_id is also required)", + default: false, optional: true, }, pinnedToPersonFlag: { - propDefinition: [ - pipedriveApp, - "pinnedToPersonFlag", - ], + type: "boolean", + label: "Pinned To Person Flag", + description: "If set, the results are filtered by note to person pinning state (person_id is also required)", + default: false, optional: true, }, }, async run({ $ }) { - const { - content, - dealId, - personId, - organizationId, - userId, - addTime, - pinnedToLeadFlag, - pinnedToDealFlag, - pinnedToOrgFlag, - pinnedToPersonFlag, - } = this; - try { const resp = await this.pipedriveApp.addNote({ - content, - deal_id: dealId, - person_id: personId, - org_id: organizationId, - user_id: userId, - add_time: addTime, - pinned_to_lead_flag: pinnedToLeadFlag, - pinned_to_deal_flag: pinnedToDealFlag, - pinned_to_organization_flag: pinnedToOrgFlag, - pinned_to_person_flag: pinnedToPersonFlag, + content: this.content, + lead_id: this.leadId, + deal_id: this.dealId, + person_id: this.personId, + org_id: this.organizationId, + user_id: this.userId, + add_time: this.addTime, + pinned_to_lead_flag: this.pinnedToLeadFlag, + pinned_to_deal_flag: this.pinnedToDealFlag, + pinned_to_organization_flag: this.pinnedToOrgFlag, + pinned_to_person_flag: this.pinnedToPersonFlag, }); $.export("$summary", "Successfully added note"); return resp; - } catch (error) { - console.error(error.context?.body || error); - throw error; + } catch ({ error }) { + throw new ConfigurationError(error); } }, }; diff --git a/components/pipedrive/actions/add-organization/add-organization.mjs b/components/pipedrive/actions/add-organization/add-organization.mjs index babea37f9fd4d..e2775daa867a5 100644 --- a/components/pipedrive/actions/add-organization/add-organization.mjs +++ b/components/pipedrive/actions/add-organization/add-organization.mjs @@ -1,10 +1,11 @@ +import { ConfigurationError } from "@pipedream/platform"; import pipedriveApp from "../../pipedrive.app.mjs"; export default { key: "pipedrive-add-organization", name: "Add Organization", description: "Adds a new organization. See the Pipedrive API docs for Organizations [here](https://developers.pipedrive.com/docs/api/v1/Organizations#addOrganization)", - version: "0.1.5", + version: "0.1.7", type: "action", props: { pipedriveApp, @@ -28,37 +29,20 @@ export default { ], description: "Visibility of the organization. If omitted, visibility will be set to the default visibility setting of this item type for the authorized user.", }, - addTime: { - propDefinition: [ - pipedriveApp, - "addTime", - ], - description: "Optional creation date & time of the organization in UTC. Requires admin user API token. Format: `YYYY-MM-DD HH:MM:SS`", - }, }, async run({ $ }) { - const { - name, - ownerId, - visibleTo, - addTime, - } = this; - try { - const resp = - await this.pipedriveApp.addOrganization({ - name, - owner_id: ownerId, - visible_to: visibleTo, - add_time: addTime, - }); + const resp = await this.pipedriveApp.addOrganization({ + name: this.name, + owner_id: this.ownerId, + visible_to: this.visibleTo, + }); $.export("$summary", "Successfully added organization"); return resp; - } catch (error) { - console.error(error.context?.body || error); - throw error.context?.body?.error || "Failed to add organization"; + } catch ({ error }) { + throw new ConfigurationError(error); } }, }; diff --git a/components/pipedrive/actions/add-person/add-person.mjs b/components/pipedrive/actions/add-person/add-person.mjs index ec9d0a2c7eef0..e14be3b0aff84 100644 --- a/components/pipedrive/actions/add-person/add-person.mjs +++ b/components/pipedrive/actions/add-person/add-person.mjs @@ -1,10 +1,12 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; import pipedriveApp from "../../pipedrive.app.mjs"; export default { key: "pipedrive-add-person", name: "Add Person", description: "Adds a new person. See the Pipedrive API docs for People [here](https://developers.pipedrive.com/docs/api/v1/Persons#addPerson)", - version: "0.1.5", + version: "0.1.7", type: "action", props: { pipedriveApp, @@ -28,16 +30,16 @@ export default { ], description: "ID of the organization this person will belong to.", }, - email: { - type: "any", - label: "Email", - description: "Email addresses (one or more) associated with the person, presented in the same manner as received by GET request of a person.", + emails: { + type: "string[]", + label: "Emails", + description: "Email addresses (one or more) associated with the person, presented in the same manner as received by GET request of a person. **Example: {\"value\":\"email1@email.com\", \"primary\":true, \"label\":\"work\"}**", optional: true, }, - phone: { - type: "any", - label: "Phone", - description: "Phone numbers (one or more) associated with the person, presented in the same manner as received by GET request of a person.", + phones: { + type: "string[]", + label: "Phones", + description: "Phone numbers (one or more) associated with the person, presented in the same manner as received by GET request of a person. **Example: {\"value\":\"12345\", \"primary\":true, \"label\":\"work\"}**", optional: true, }, visibleTo: { @@ -47,44 +49,25 @@ export default { ], description: "Visibility of the person. If omitted, visibility will be set to the default visibility setting of this item type for the authorized user.", }, - addTime: { - propDefinition: [ - pipedriveApp, - "addTime", - ], - description: "Optional creation date & time of the person in UTC. Requires admin user API token. Format: `YYYY-MM-DD HH:MM:SS`", - }, }, async run({ $ }) { - const { - name, - ownerId, - organizationId, - email, - phone, - visibleTo, - addTime, - } = this; - try { const resp = await this.pipedriveApp.addPerson({ - name, - owner_id: ownerId, - org_id: organizationId, - email, - phone, - visible_to: visibleTo, - add_time: addTime, + name: this.name, + owner_id: this.ownerId, + org_id: this.organizationId, + emails: parseObject(this.emails), + phones: parseObject(this.phones), + visible_to: this.visibleTo, }); $.export("$summary", "Successfully added person"); return resp; - } catch (error) { - console.error(error.context?.body || error); - throw error.context?.body?.error || "Failed to add person"; + } catch ({ error }) { + throw new ConfigurationError(error); } }, }; diff --git a/components/pipedrive/actions/search-persons/search-persons.mjs b/components/pipedrive/actions/search-persons/search-persons.mjs index 508038b07a8c5..459c87f16ac45 100644 --- a/components/pipedrive/actions/search-persons/search-persons.mjs +++ b/components/pipedrive/actions/search-persons/search-persons.mjs @@ -1,11 +1,13 @@ +import { ConfigurationError } from "@pipedream/platform"; import constants from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; import pipedriveApp from "../../pipedrive.app.mjs"; export default { key: "pipedrive-search-persons", name: "Search persons", description: "Searches all Persons by `name`, `email`, `phone`, `notes` and/or custom fields. This endpoint is a wrapper of `/v1/itemSearch` with a narrower OAuth scope. Found Persons can be filtered by Organization ID. See the Pipedrive API docs [here](https://developers.pipedrive.com/docs/api/v1/Persons#searchPersons)", - version: "0.1.5", + version: "0.1.7", type: "action", props: { pipedriveApp, @@ -15,9 +17,9 @@ export default { description: "The search term to look for. Minimum 2 characters (or 1 if using exact_match).", }, fields: { - type: "string", + type: "string[]", label: "Search fields", - description: "A comma-separated string array. The fields to perform the search from. Defaults to all of them.", + description: "An array containing fields to perform the search from. Defaults to all of them.", optional: true, options: constants.FIELD_OPTIONS, }, @@ -42,48 +44,37 @@ export default { options: constants.INCLUDE_FIELDS_OPTIONS, }, start: { - propDefinition: [ - pipedriveApp, - "start", - ], + type: "integer", + label: "Pagination start", + description: "Pagination start. Note that the pagination is based on main results and does not include related items when using `search_for_related_items` parameter.", + optional: true, }, limit: { - propDefinition: [ - pipedriveApp, - "limit", - ], + type: "integer", + label: "Limit", + description: "Items shown per page.", + optional: true, }, }, async run({ $ }) { - const { - term, - fields, - exactMatch, - organizationId, - includeFields, - start, - limit, - } = this; - try { const resp = await this.pipedriveApp.searchPersons({ - term, - fields, - exact_match: exactMatch, - org_id: organizationId, - include_fields: includeFields, - start, - limit, + term: this.term, + fields: parseObject(this.fields), + exact_match: this.exactMatch, + org_id: this.organizationId, + include_fields: this.includeFields, + start: this.start, + limit: this.limit, }); $.export("$summary", `Successfully found ${resp.data?.items.length || 0} persons`); return resp; - } catch (error) { - console.error(error.context?.body || error); - throw error.context?.body?.error || "Failed to search persons"; + } catch ({ error }) { + throw new ConfigurationError(error); } }, }; diff --git a/components/pipedrive/actions/update-deal/update-deal.mjs b/components/pipedrive/actions/update-deal/update-deal.mjs index e75b06f103d6d..af698080f1aeb 100644 --- a/components/pipedrive/actions/update-deal/update-deal.mjs +++ b/components/pipedrive/actions/update-deal/update-deal.mjs @@ -1,20 +1,21 @@ +import { ConfigurationError } from "@pipedream/platform"; import pipedriveApp from "../../pipedrive.app.mjs"; export default { key: "pipedrive-update-deal", name: "Update Deal", description: "Updates the properties of a deal. See the Pipedrive API docs for Deals [here](https://developers.pipedrive.com/docs/api/v1/Deals#updateDeal)", - version: "0.1.6", + version: "0.1.8", type: "action", props: { pipedriveApp, dealId: { - description: "ID of the deal", - optional: false, propDefinition: [ pipedriveApp, "dealId", ], + optional: false, + description: "ID of the deal", }, title: { propDefinition: [ @@ -23,40 +24,47 @@ export default { ], optional: true, }, - value: { + ownerId: { propDefinition: [ pipedriveApp, - "dealValue", + "userId", ], }, - currency: { + personId: { propDefinition: [ pipedriveApp, - "dealCurrency", + "personId", ], }, - userId: { + orgId: { propDefinition: [ pipedriveApp, - "userId", + "organizationId", ], }, - personId: { + pipelineId: { propDefinition: [ pipedriveApp, - "personId", + "pipelineId", ], + optional: true, }, - organizationId: { + stageId: { propDefinition: [ pipedriveApp, - "organizationId", + "stageId", ], }, - stageId: { + value: { propDefinition: [ pipedriveApp, - "stageId", + "dealValue", + ], + }, + currency: { + propDefinition: [ + pipedriveApp, + "dealCurrency", ], }, status: { @@ -85,45 +93,29 @@ export default { }, }, async run({ $ }) { - const { - dealId, - title, - value, - currency, - userId, - personId, - organizationId, - stageId, - status, - probability, - lostReason, - visibleTo, - } = this; - try { - const resp = - await this.pipedriveApp.updateDeal({ - dealId, - title, - value, - currency, - user_id: userId, - person_id: personId, - org_id: organizationId, - stage_id: stageId, - status, - probability, - lost_reason: lostReason, - visible_to: visibleTo, - }); + const resp = await this.pipedriveApp.updateDeal({ + dealId: this.dealId, + title: this.title, + owner_id: this.ownerId, + person_id: this.personId, + org_id: this.orgId, + pipeline_id: this.pipelineId, + stage_id: this.stageId, + value: this.value, + currency: this.currency, + status: this.status, + probability: this.probability, + lost_reason: this.lostReason, + visible_to: this.visibleTo, + }); $.export("$summary", "Successfully updated deal"); return resp; - } catch (error) { - console.error(error.context?.body || error); - throw error.context?.body?.error || "Failed to update deal"; + } catch ({ error }) { + throw new ConfigurationError(error); } }, }; diff --git a/components/pipedrive/common/constants.mjs b/components/pipedrive/common/constants.mjs index 263ac9b090560..4af0c17a543cd 100644 --- a/components/pipedrive/common/constants.mjs +++ b/components/pipedrive/common/constants.mjs @@ -1,8 +1,4 @@ -const LAST_RESOURCE_PROPERTY = "lastResourceProperty"; -const FILTER_ID = "filterId"; -const FIELD_ID = "fieldId"; const DEFAULT_PAGE_LIMIT = 20; // max is 500 per page -const DEFAULT_MAX_ITEMS = DEFAULT_PAGE_LIMIT * 4; const STATUS_OPTIONS = [ "open", @@ -22,11 +18,11 @@ const FIELD_OPTIONS = [ const VISIBLE_TO_OPTIONS = [ { label: "Owner & followers (private)", - value: "1", + value: 1, }, { label: "Entire company (shared)", - value: "3", + value: 3, }, ]; @@ -34,156 +30,6 @@ const INCLUDE_FIELDS_OPTIONS = [ "person.picture", ]; -const EVENT_OBJECT = { - ACTIVITY: "activity", - ACTIVITY_TYPE: "activityType", - DEAL: "deal", - NOTE: "note", - ORGANIZATION: "organization", - PERSON: "person", - PIPELINE: "pipeline", - PRODUCT: "product", - STAGE: "stage", - USER: "user", -}; - -const FILTER_TYPE = { - DEALS: "deals", - LEADS: "leads", - ORG: "org", - PEOPLE: "people", - PRODUCTS: "products", - ACTIVITY: "activity", -}; - -const EVENT_ACTION = { - ADDED: "added", - UPDATED: "updated", - MERGED: "merged", - DELETED: "deleted", -}; - -const API = { - WEBHOOKS: [ - "WebhooksApi", - "AddWebhookRequest", - ], - USERS: [ - "UsersApi", - ], - USER_SETTINGS: [ - "UserSettingsApi", - ], - USER_CONNECTIONS: [ - "UserConnectionsApi", - ], - TEAMS: [ - "TeamsApi", - ], - SUBSCRIPTIONS: [ - "SubscriptionsApi", - ], - STAGES: [ - "StagesApi", - ], - ROLES: [ - "RolesApi", - ], - RECENTS: [ - "RecentsApi", - ], - PRODUCTS: [ - "ProductsApi", - ], - PRODUCT_FIELDS: [ - "ProductFieldsApi", - ], - PIPELINES: [ - "PipelinesApi", - ], - PERSONS: [ - "PersonsApi", - "NewPerson", - "BasicPerson", - ], - PERSON_FIELDS: [ - "PersonFieldsApi", - ], - PERMISSION_SETS: [ - "PermissionSetsApi", - ], - ORGANIZATIONS: [ - "OrganizationsApi", - "NewOrganization", - ], - ORGANIZATION_RELATIONSHIPS: [ - "OrganizationRelationshipsApi", - ], - ORGANIZATION_FIELDS: [ - "OrganizationFieldsApi", - ], - NOTES: [ - "NotesApi", - ], - NOTE_FIELDS: [ - "NoteFieldsApi", - ], - MAILBOX: [ - "MailboxApi", - ], - LEADS: [ - "LeadsApi", - ], - LEAD_SOURCES: [ - "LeadSourcesApi", - ], - LEAD_LABELS: [ - "LeadLabelsApi", - ], - ITEM_SEARCH: [ - "ItemSearchApi", - ], - GOALS: [ - "GoalsApi", - ], - GLOBAL_MESSAGES: [ - "GlobalMessagesApi", - ], - FILTERS: [ - "FiltersApi", - "AddFilterRequest", - "UpdateFilterRequest", - ], - FILES: [ - "FilesApi", - ], - DEALS: [ - "DealsApi", - "NewDeal", - "UpdateDealRequest", - ], - DEAL_FIELDS: [ - "DealFieldsApi", - ], - CURRENCIES: [ - "CurrenciesApi", - ], - CALL_LOGS: [ - "CallLogsApi", - ], - ACTIVITY_TYPES: [ - "ActivityTypesApi", - ], - ACTIVITY_FIELDS: [ - "ActivityFieldsApi", - ], - ACTIVITIES: [ - "ActivitiesApi", - "ActivityPostObject", - "ActivityPutObject", - ], -}; - const FIELD = { ADD_TIME: "add_time", UPDATE_TIME: "update_time", @@ -194,14 +40,6 @@ export default { FIELD_OPTIONS, VISIBLE_TO_OPTIONS, INCLUDE_FIELDS_OPTIONS, - LAST_RESOURCE_PROPERTY, - FILTER_ID, - FIELD_ID, - EVENT_OBJECT, - EVENT_ACTION, - API, DEFAULT_PAGE_LIMIT, - DEFAULT_MAX_ITEMS, FIELD, - FILTER_TYPE, }; diff --git a/components/pipedrive/common/utils.mjs b/components/pipedrive/common/utils.mjs index cc17a9478cfe9..dcc9cc61f6f41 100644 --- a/components/pipedrive/common/utils.mjs +++ b/components/pipedrive/common/utils.mjs @@ -1,17 +1,24 @@ -export default { - parseOrUndefined(value) { - if (value === undefined) { - return undefined; +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; } - return typeof(value) === "object" - ? value - : JSON.parse(value); - }, - async streamIterator(stream) { - const resources = []; - for await (const resource of stream) { - resources.push(resource); - } - return resources; - }, + } + return obj; }; diff --git a/components/pipedrive/package.json b/components/pipedrive/package.json index ac42f13453d8d..e1e5470a64072 100644 --- a/components/pipedrive/package.json +++ b/components/pipedrive/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/pipedrive", - "version": "0.3.9", + "version": "0.3.11", "description": "Pipedream Pipedrive Components", "main": "pipedrive.app.mjs", "keywords": [ @@ -14,7 +14,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.2.0", - "pipedrive": "^13.2.7" + "@pipedream/platform": "^3.0.3", + "pipedrive": "^24.1.1" } } diff --git a/components/pipedrive/pipedrive.app.mjs b/components/pipedrive/pipedrive.app.mjs index e1edb9fdedaea..51a85b4acc6ea 100644 --- a/components/pipedrive/pipedrive.app.mjs +++ b/components/pipedrive/pipedrive.app.mjs @@ -1,15 +1,10 @@ -import pipedrive from "pipedrive"; +import pd from "pipedrive"; import constants from "./common/constants.mjs"; export default { type: "app", app: "pipedrive", propDefinitions: { - content: { - type: "string", - label: "Content", - description: "The content of the note in HTML format. Subject to sanitization on the back-end.", - }, userId: { type: "integer", label: "User ID", @@ -31,35 +26,26 @@ export default { description: "ID of the person this deal will be associated with", optional: true, async options({ prevContext }) { - const { - moreItemsInCollection, - start, - } = prevContext; - - if (moreItemsInCollection === false) { + if (prevContext?.cursor === false) { return []; } - const { data: persons, additional_data: additionalData, } = await this.getPersons({ - start, + cursor: prevContext.cursor, limit: constants.DEFAULT_PAGE_LIMIT, }); - const options = persons.map(({ - id, name, - }) => ({ - label: name, - value: id, - })); - return { - options, + options: persons.map(({ + id, name, + }) => ({ + label: name, + value: id, + })), context: { - moreItemsInCollection: additionalData.pagination.more_items_in_collection, - start: additionalData.pagination.next_start, + cursor: additionalData.next_cursor, }, }; }, @@ -70,57 +56,29 @@ export default { description: "ID of the organization this deal will be associated with", optional: true, async options({ prevContext }) { - const { - moreItemsInCollection, - start, - } = prevContext; - - if (moreItemsInCollection === false) { + if (prevContext?.cursor === false) { return []; } - const { data: organizations, additional_data: additionalData, } = await this.getOrganizations({ - start, + cursor: prevContext.cursor, limit: constants.DEFAULT_PAGE_LIMIT, }); - - const options = organizations.map(({ - id, name, - }) => ({ - label: name, - value: id, - })); - return { - options, + options: organizations.map(({ + id, name, + }) => ({ + label: name, + value: id, + })), context: { - moreItemsInCollection: additionalData.pagination.more_items_in_collection, - start: additionalData.pagination.next_start, + cursor: additionalData.next_cursor || false, }, }; }, }, - pinnedToDealFlag: { - type: "boolean", - label: "Pinned To Deal Flag", - description: "If set, the results are filtered by note to deal pinning state (deal_id is also required)", - default: false, - }, - pinnedToOrgFlag: { - type: "boolean", - label: "Pinned To Organization Flag", - description: "If set, the results are filtered by note to organization pinning state (org_id is also required)", - default: false, - }, - pinnedToPersonFlag: { - type: "boolean", - label: "Pinned To Person Flag", - description: "If set, the results are filtered by note to person pinning state (person_id is also required)", - default: false, - }, probability: { type: "integer", label: "Probability", @@ -134,7 +92,7 @@ export default { optional: true, }, visibleTo: { - type: "string", + type: "integer", label: "Visible To", description: "Visibility of the deal. If omitted, visibility will be set to the default visibility setting of this item type for the authorized user.", optional: true, @@ -143,7 +101,7 @@ export default { addTime: { type: "string", label: "Add Time", - description: "Optional creation date & time of the deal in UTC. Requires admin user API token. Format: `YYYY-MM-DD HH:MM:SS`", + description: "The creation date and time of the deal. Requires admin user API token. Format: `YYYY-MM-DDTHH:MM:SSZ`", optional: true, }, dealTitle: { @@ -168,14 +126,29 @@ export default { label: "Stage ID", description: "ID of the stage this deal will be placed in a pipeline (note that you can't supply the ID of the pipeline as this will be assigned automatically based on `stage_id`). If omitted, the deal will be placed in the first stage of the default pipeline. Get the `stage_id` from [here](https://developers.pipedrive.com/docs/api/v1/#!/Stages/get_stages).", optional: true, - async options() { - const { data: stages } = await this.getStages(); - return stages.map(({ - id, name, - }) => ({ - label: name, - value: id, - })); + async options({ prevContext }) { + if (prevContext.cursor === false) { + return []; + } + const { + data: stages, + additional_data: additionalData, + } = await this.getStages({ + cursor: prevContext.cursor, + limit: constants.DEFAULT_PAGE_LIMIT, + }); + + return { + options: stages?.map(({ + id, name, + }) => ({ + label: name, + value: id, + })), + context: { + cursor: additionalData.next_cursor, + }, + }; }, }, status: { @@ -185,245 +158,225 @@ export default { optional: true, options: constants.STATUS_OPTIONS, }, - start: { - type: "integer", - label: "Pagination start", - description: "Pagination start. Note that the pagination is based on main results and does not include related items when using `search_for_related_items` parameter.", - optional: true, - }, - limit: { - type: "integer", - label: "Limit", - description: "Items shown per page.", - optional: true, - }, dealId: { type: "string", label: "Deal ID", description: "ID of the deal this activity will be associated with", optional: true, async options({ prevContext }) { + if (prevContext?.cursor === false) { + return []; + } const { - moreItemsInCollection, - start, - } = prevContext; + data: deals, + additional_data: additionalData, + } = await this.getDeals({ + cursor: prevContext.cursor, + limit: constants.DEFAULT_PAGE_LIMIT, + }); - if (moreItemsInCollection === false) { + return { + options: deals?.map(({ + id, title, + }) => ({ + label: title, + value: id, + })), + context: { + cursor: additionalData.next_cursor, + }, + }; + }, + }, + pipelineId: { + type: "string", + label: "Pipeline ID", + description: "ID of the pipeline this activity will be associated with", + optional: true, + async options({ prevContext }) { + if (prevContext?.cursor === false) { return []; } + const { + data: pipelines, + additional_data: additionalData, + } = await this.getPipelines({ + cursor: prevContext.cursor, + limit: constants.DEFAULT_PAGE_LIMIT, + }); + return { + options: pipelines?.map(({ + id, title, + }) => ({ + label: title, + value: id, + })), + context: { + cursor: additionalData.next_cursor, + }, + }; + }, + }, + leadId: { + type: "string", + label: "Lead ID", + description: "ID of the lead this activity will be associated with", + optional: true, + async options({ prevContext }) { + if (prevContext?.nextStart === false) { + return []; + } const { - data: deals, + data: leads, additional_data: additionalData, - } = await this.getDeals({ - start, + } = await this.getLeads({ + start: prevContext.nextStart, limit: constants.DEFAULT_PAGE_LIMIT, }); - const options = deals?.map(({ - id, title, - }) => ({ - label: title, - value: id, - })); + return { + options: leads?.map(({ + id, title, + }) => ({ + label: title, + value: id, + })), + context: { + nextStart: additionalData.next_start || false, + }, + }; + }, + }, + projectId: { + type: "string", + label: "Project ID", + description: "ID of the project this activity will be associated with", + optional: true, + async options({ prevContext }) { + if (prevContext?.nextStart === false) { + return []; + } + const { + data: projects, + additional_data: additionalData, + } = await this.getProjects({ + start: prevContext.nextStart, + limit: constants.DEFAULT_PAGE_LIMIT, + }); return { - options, + options: projects?.map(({ + id, title, + }) => ({ + label: title, + value: id, + })), context: { - moreItemsInCollection: additionalData.pagination.more_items_in_collection, - start: additionalData.pagination.next_start, + nextStart: additionalData.next_start || false, }, }; }, }, }, methods: { - setupToken() { - const client = new pipedrive.ApiClient(); - const oauth2 = client.authentications.oauth2; - oauth2.accessToken = this.$auth.oauth_access_token; - oauth2.refreshToken = this.$auth.oauth_refresh_token; - oauth2.clientId = this.$auth.oauth_client_id; - return client; - }, - api(className) { - const client = this.setupToken(); - return new pipedrive[className](client); - }, - buildOpts(property, opts) { - return pipedrive[property].constructFromObject(opts); - }, - async addActivity(opts = {}) { - const [ - className, - addProperty, - ] = constants.API.ACTIVITIES; - try { - return this.api(className).addActivity(this.buildOpts(addProperty, opts)); - } catch (error) { - console.error(error); - throw new Error(error); - } - }, - addDeal(opts = {}) { - const [ - className, - addProperty, - ] = constants.API.DEALS; - return this.api(className).addDeal(this.buildOpts(addProperty, opts)); - }, - updateDeal(opts = {}) { - const { - dealId, - ...otherOpts - } = opts; - const [ - className,, - updateProperty, - ] = constants.API.DEALS; - return this.api(className).updateDeal(dealId, this.buildOpts(updateProperty, otherOpts)); - }, - async addNote(opts = {}) { - const [ - className, - ] = constants.API.NOTES; - return this.api(className).addNote(opts); - }, - async addOrganization(opts = {}) { - const [ - className, - addProperty, - ] = constants.API.ORGANIZATIONS; - return this.api(className).addOrganization(this.buildOpts(addProperty, opts)); + api(model, version = "v1") { + const config = new pd[version].Configuration({ + accessToken: this.$auth.oauth_access_token, + basePath: `${this.$auth.api_domain}/api/${version}`, + }); + return new pd[version][model](config); }, - addPerson(opts = {}) { - const [ - className, - ] = constants.API.PERSONS; - return this.api(className).addPerson(opts); - }, - searchPersons(opts = {}) { - const { - term, - ...otherOpts - } = opts; - const [ - className, - ] = constants.API.PERSONS; - return this.api(className).searchPersons(term, otherOpts); - }, - addFilter(opts = {}) { - const [ - className, - addProperty, - ] = constants.API.FILTERS; - return this.api(className).addFilter(this.buildOpts(addProperty, opts)); - }, - updateFilter(opts = {}) { - const { - filterId, - ...otherOpts - } = opts; - const [ - className,, - updateProperty, - ] = constants.API.FILTERS; - return this.api(className).updateFilter(filterId, this.buildOpts(updateProperty, otherOpts)); - }, - deleteFilter(filterId) { - const [ - className, - ] = constants.API.FILTERS; - return this.api(className).deleteFilter(filterId); + getActivityTypes(opts) { + const activityTypesApi = this.api("ActivityTypesApi"); + return activityTypesApi.getActivityTypes(opts); }, getDeals(opts = {}) { - const [ - className, - ] = constants.API.DEALS; - return this.api(className).getDeals(opts); + const dealApi = this.api("DealsApi", "v2"); + return dealApi.getDeals(opts); }, - getPersons(opts = {}) { - const [ - className, - ] = constants.API.PERSONS; - return this.api(className).getPersons(opts); - }, - async getLeads(opts) { - const [ - className, - ] = constants.API.LEADS; - return this.api(className).getLeads(opts); - }, - async getUsers(opts) { - const [ - className, - ] = constants.API.USERS; - return this.api(className).getUsers(opts); + getPipelines(opts = {}) { + const pipelineApi = this.api("PipelinesApi", "v2"); + return pipelineApi.getPipelines(opts); }, - getActivityTypes(opts) { - const [ - className, - ] = constants.API.ACTIVITY_TYPES; - return this.api(className).getActivityTypes(opts); + getLeads(opts = {}) { + const leadApi = this.api("LeadsApi"); + return leadApi.getLeads(opts); + }, + getProjects(opts = {}) { + const projectApi = this.api("ProjectsApi"); + return projectApi.getProjects(opts); }, getOrganizations(opts = {}) { - const [ - className, - ] = constants.API.ORGANIZATIONS; - return this.api(className).getOrganizations(opts); + const organizationApi = this.api("OrganizationsApi", "v2"); + return organizationApi.getOrganizations(opts); + }, + getPersons(opts = {}) { + const personApi = this.api("PersonsApi", "v2"); + return personApi.getPersons(opts); + }, + getUsers(opts) { + const UsersApi = this.api("UsersApi"); + return UsersApi.getUsers(opts); }, getStages(opts) { - const [ - className, - ] = constants.API.STAGES; - return this.api(className).getStages(opts); - }, - getDealFields(opts = {}) { - const [ - className, - ] = constants.API.DEAL_FIELDS; - return this.api(className).getDealFields(opts); - }, - getPersonFields(opts = {}) { - const [ - className, - ] = constants.API.PERSON_FIELDS; - return this.api(className).getPersonFields(opts); - }, - async *getResourcesStream({ - resourceFn, - resourceFnArgs, - limit = constants.DEFAULT_PAGE_LIMIT, - max = constants.DEFAULT_MAX_ITEMS, + const stagesApi = this.api("StagesApi", "v2"); + return stagesApi.getStages(opts); + }, + addActivity(opts = {}) { + const activityApi = this.api("ActivitiesApi", "v2"); + return activityApi.addActivity({ + AddActivityRequest: opts, + }); + }, + addNote(opts = {}) { + const noteApi = this.api("NotesApi"); + return noteApi.addNote({ + AddNoteRequest: opts, + }); + }, + addDeal(opts = {}) { + const dealsApi = this.api("DealsApi", "v2"); + return dealsApi.addDeal({ + AddDealRequest: opts, + }); + }, + addOrganization(opts = {}) { + const organizationApi = this.api("OrganizationsApi", "v2"); + return organizationApi.addOrganization({ + AddOrganizationRequest: opts, + }); + }, + addPerson(opts = {}) { + const personsApi = this.api("PersonsApi", "v2"); + return personsApi.addPerson({ + AddPersonRequest: opts, + }); + }, + addWebhook(opts = {}) { + const webhooksApi = this.api("WebhooksApi"); + return webhooksApi.addWebhook({ + AddWebhookRequest: opts, + }); + }, + deleteWebhook(webhookId) { + const webhooksApi = this.api("WebhooksApi"); + return webhooksApi.deleteWebhook({ + id: webhookId, + }); + }, + searchPersons(opts = {}) { + const personsApi = this.api("PersonsApi"); + return personsApi.searchPersons(opts); + }, + updateDeal({ + dealId, ...opts }) { - let start = 0; - let resourcesCount = 0; - - while (true) { - const response = - await resourceFn({ - ...resourceFnArgs, - limit, - start, - }); - - const nextResources = response?.data || []; - start = response?.additional_data?.pagination?.next_start; - - if (!nextResources.length) { - console.log("No more records to fetch."); - return; - } - - for (const resource of nextResources) { - resourcesCount += 1; - yield resource; - } - - if (!start || resourcesCount >= max) { - return; - } - } + const dealsApi = this.api("DealsApi", "v2"); + return dealsApi.updateDeal({ + id: dealId, + UpdateDealRequest: opts, + }); }, }, }; diff --git a/components/pipedrive/sources/common/base.mjs b/components/pipedrive/sources/common/base.mjs index d111a47e7b8da..66b57c3b0089e 100644 --- a/components/pipedrive/sources/common/base.mjs +++ b/components/pipedrive/sources/common/base.mjs @@ -1,162 +1,44 @@ -import app from "../../pipedrive.app.mjs"; -import constants from "../../common/constants.mjs"; -import utils from "../../common/utils.mjs"; -import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import pipedrive from "../../pipedrive.app.mjs"; export default { props: { - app, + pipedrive, + http: "$.interface.http", db: "$.service.db", - timer: { - type: "$.interface.timer", - label: "Polling schedule", - description: "How often to poll the Pipedrive API", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - }, - hooks: { - async deploy() { - const fieldId = await this.fetchFieldId(); - const args = await this.getFilterArgs({ - fieldId, - }); - const { data: { id: filterId } } = await this.app.addFilter(args); - this.setFilterId(filterId); - }, - async deactivate() { - await this.app.deleteFilter(this.getFilterId()); - }, }, methods: { - getFilterId() { - return this.db.get(constants.FILTER_ID); - }, - setFilterId(value) { - this.db.set(constants.FILTER_ID, value); - }, - getFieldId() { - return this.db.get(constants.FIELD_ID); - }, - setFieldId(value) { - this.db.set(constants.FIELD_ID, value); - }, - getFieldKey() { - throw new Error("getFieldKey not implemented"); - }, - getEventObject() { - throw new Error("getEventObject not implemented"); - }, - getEventAction() { - throw new Error("getEventAction not implemented"); - }, - getResourceFn() { - throw new Error("getResourceFn not implemented"); - }, - getResourceFnArgs() { - throw new Error("getResourceFnArgs not implemented"); - }, - getTimestamp() { - throw new Error("getTimestamp not implemented"); - }, - getFilterArgs() { - throw new Error("getFilterArgs not implemented"); + _getHookId() { + return this.db.get("hookId"); }, - getFieldsFn() { - throw new Error("getFieldsFn not implemented"); + _setHookId(hookId) { + this.db.set("hookId", hookId); }, - getMetaId(resource) { - return resource.id; + getExtraData() { + return {}; }, - getConditions({ - fieldId, value, - } = {}) { - return { - glue: "and", - conditions: [ - { - glue: "or", - conditions: [], - }, - { - glue: "and", - conditions: [ - { - object: this.getEventObject(), - field_id: fieldId, - operator: ">", - value, - extra_value: null, - }, - ], - }, - ], - }; - }, - async fetchFieldId() { - const fieldId = this.getFieldId(); - - if (fieldId) { - return fieldId; - } - const getFields = this.getFieldsFn(); - const { data: fields } = await getFields(); - const field = fields.find(({ key }) => key === this.getFieldKey()); - - this.setFieldId(field.id); + }, + hooks: { + async activate() { + const response = await this.pipedrive.addWebhook({ + subscription_url: this.http.endpoint, + ...this.getExtraData(), + }); + console.log("response: ", response); - return field.id; - }, - generateMeta(resource) { - return { - id: this.getMetaId(resource), - summary: `${this.getEventObject()} ${resource.id} was ${this.getEventAction()}`, - ts: this.getTimestamp(resource), - }; + this._setHookId(response.data.id); }, - processEvent(resource) { - const meta = this.generateMeta(resource); - this.$emit(resource, meta); - }, - async processStreamEvents(stream) { - const resources = await utils.streamIterator(stream); - - if (resources.length === 0) { - console.log("No new events detected. Skipping..."); - return; - } - - const [ - lastResource, - ] = resources; - - resources.reverse().forEach(this.processEvent); - - if (lastResource) { - const lastDateTimeStr = lastResource[this.getFieldKey()]; - - const fieldId = await this.fetchFieldId(); - - const lastDate = new Date(lastDateTimeStr); - const args = this.getFilterArgs({ - fieldId, - value: lastDate.toISOString().split("T")[0], - }); - await this.app.updateFilter({ - filterId: this.getFilterId(), - ...args, - }); - } + async deactivate() { + const webhookId = this._getHookId(); + await this.pipedrive.deleteWebhook(webhookId); }, }, - async run() { - const stream = - await this.app.getResourcesStream({ - resourceFn: this.getResourceFn(), - resourceFnArgs: this.getResourceFnArgs(), - }); - - await this.processStreamEvents(stream); + async run({ body }) { + const ts = Date.parse(body.current.update_time); + + this.$emit(body, { + id: `${body.current.id}-${ts}`, + summary: this.getSummary(body), + ts, + }); }, }; diff --git a/components/pipedrive/sources/new-deal-instant/new-deal-instant.mjs b/components/pipedrive/sources/new-deal-instant/new-deal-instant.mjs new file mode 100644 index 0000000000000..25be591244a65 --- /dev/null +++ b/components/pipedrive/sources/new-deal-instant/new-deal-instant.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "pipedrive-new-deal-instant", + name: "New Deal (Instant)", + description: "Emit new event when a new deal is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getExtraData() { + return { + event_action: "added", + event_object: "deal", + }; + }, + getSummary(body) { + return `New Deal successfully created: ${body.current.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/pipedrive/sources/new-deal-instant/test-event.mjs b/components/pipedrive/sources/new-deal-instant/test-event.mjs new file mode 100644 index 0000000000000..b6459df9c5370 --- /dev/null +++ b/components/pipedrive/sources/new-deal-instant/test-event.mjs @@ -0,0 +1,106 @@ +export default { + "v": 1, + "matches_filters": { + "current": [] + }, + "meta": { + "action": "added", + "change_source": "api", + "company_id": 1234567890, + "host": "subdomain.pipedrive.com", + "id": 10, + "is_bulk_update": false, + "matches_filters": { + "current": [] + }, + "object": "deal", + "permitted_user_ids": [ + 1234567890 + ], + "pipedrive_service_name": false, + "timestamp": 1739826584, + "timestamp_micro": 1739826584443168, + "prepublish_timestamp": 1739826584525, + "trans_pending": false, + "user_id": 1234567890, + "v": 1, + "webhook_id": "1234567890" + }, + "current": { + "email_messages_count": 0, + "cc_email": "subomain+deal11@pipedrivemail.com", + "channel": 8, + "products_count": 0, + "archive_time": null, + "acv_currency": null, + "next_activity_date": null, + "acv": null, + "next_activity_type": null, + "local_close_date": null, + "next_activity_duration": null, + "id": 11, + "person_id": null, + "creator_user_id": 1234567890, + "expected_close_date": null, + "owner_name": "Owner Name", + "arr_currency": null, + "participants_count": 0, + "stage_id": 1, + "probability": null, + "undone_activities_count": 0, + "active": true, + "local_lost_date": null, + "person_name": null, + "last_activity_date": null, + "close_time": null, + "org_hidden": false, + "next_activity_id": null, + "weighted_value_currency": "USD", + "local_won_date": null, + "stage_order_nr": 0, + "next_activity_subject": null, + "rotten_time": null, + "is_archived": false, + "user_id": 1234567890, + "visible_to": "3", + "org_id": null, + "notes_count": 0, + "next_activity_time": null, + "channel_id": "Pipedream", + "formatted_value": "$ 0", + "status": "open", + "formatted_weighted_value": "$ 0", + "mrr_currency": null, + "first_won_time": null, + "origin": "Marketplace", + "last_outgoing_mail_time": null, + "origin_id": "5d80274eae050b58", + "title": "Deal Title", + "last_activity_id": null, + "update_time": "2025-02-17 21:09:44", + "activities_count": 0, + "pipeline_id": 1, + "lost_time": null, + "currency": "USD", + "weighted_value": 0, + "org_name": null, + "value": 0, + "person_hidden": false, + "next_activity_note": null, + "arr": null, + "files_count": 0, + "last_incoming_mail_time": null, + "label": null, + "mrr": null, + "lost_reason": null, + "deleted": false, + "won_time": null, + "followers_count": 0, + "stage_change_time": null, + "add_time": "2025-02-17 21:09:44", + "done_activities_count": 0 + }, + "previous": null, + "retry": 0, + "event": "added.deal" +} \ No newline at end of file diff --git a/components/pipedrive/sources/new-deal/new-deal.mjs b/components/pipedrive/sources/new-deal/new-deal.mjs deleted file mode 100644 index 6f83da7f0882c..0000000000000 --- a/components/pipedrive/sources/new-deal/new-deal.mjs +++ /dev/null @@ -1,51 +0,0 @@ -import constants from "../../common/constants.mjs"; -import common from "../common/base.mjs"; - -export default { - ...common, - key: "pipedrive-new-deal", - name: "New Deal", - description: "Emit new event when a new deal is created.", - version: "0.0.5", - type: "source", - dedupe: "unique", - methods: { - ...common.methods, - getFieldsFn() { - return this.app.getDealFields; - }, - getResourceFn() { - return this.app.getDeals; - }, - getResourceFnArgs() { - return { - filter_id: this.getFilterId(), - sort: `${this.getFieldKey()} DESC, id DESC`, - }; - }, - getFieldKey() { - return constants.FIELD.ADD_TIME; - }, - getEventObject() { - return constants.EVENT_OBJECT.DEAL; - }, - getEventAction() { - return constants.EVENT_ACTION.ADDED; - }, - getTimestamp(resource) { - return Date.parse(resource.add_time); - }, - getFilterArgs({ - fieldId, value = "3_months_ago", - } = {}) { - return { - type: constants.FILTER_TYPE.DEALS, - name: "Pipedream: Deals created later than specific value", - conditions: this.getConditions({ - fieldId, - value, - }), - }; - }, - }, -}; diff --git a/components/pipedrive/sources/new-person-instant/new-person-instant.mjs b/components/pipedrive/sources/new-person-instant/new-person-instant.mjs new file mode 100644 index 0000000000000..76400aa10c134 --- /dev/null +++ b/components/pipedrive/sources/new-person-instant/new-person-instant.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "pipedrive-new-person-instant", + name: "New Person (Instant)", + description: "Emit new event when a new person is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getExtraData() { + return { + event_action: "added", + event_object: "person", + }; + }, + getSummary(body) { + return `New Person successfully created: ${body.current.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/pipedrive/sources/new-person-instant/test-event.mjs b/components/pipedrive/sources/new-person-instant/test-event.mjs new file mode 100644 index 0000000000000..8f56cc935301b --- /dev/null +++ b/components/pipedrive/sources/new-person-instant/test-event.mjs @@ -0,0 +1,110 @@ +export default { + "v": 1, + "matches_filters": { + "current": [] + }, + "meta": { + "action": "added", + "change_source": "api", + "company_id": 1234567890, + "host": "subdomain.pipedrive.com", + "id": 10, + "is_bulk_update": false, + "matches_filters": { + "current": [] + }, + "object": "person", + "permitted_user_ids": [ + 1234567890 + ], + "pipedrive_service_name": false, + "timestamp": 1739826941, + "timestamp_micro": 1739826941509060, + "prepublish_timestamp": 1739826941605, + "trans_pending": false, + "user_id": 1234567890, + "v": 1, + "webhook_id": "1234567890" + }, + "current": { + "related_closed_deals_count": 0, + "notes": null, + "email_messages_count": 0, + "cc_email": "subdomain@pipedrivemail.com", + "owner_id": 1234567890, + "open_deals_count": 0, + "postal_address_subpremise": null, + "next_activity_date": null, + "participant_open_deals_count": 0, + "postal_address_locality": null, + "id": 10, + "job_title": null, + "postal_address": null, + "owner_name": "Owner Name", + "im": [ + { + "value": "", + "primary": true + } + ], + "related_won_deals_count": 0, + "label_ids": [], + "undone_activities_count": 0, + "last_activity_date": null, + "next_activity_id": null, + "postal_address_country": null, + "phone": [ + { + "value": "", + "primary": true + } + ], + "visible_to": "3", + "org_id": null, + "notes_count": 0, + "name": "Person Name", + "participant_closed_deals_count": 0, + "lost_deals_count": 0, + "next_activity_time": null, + "birthday": null, + "delete_time": null, + "postal_address_postal_code": null, + "last_outgoing_mail_time": null, + "active_flag": true, + "picture_id": null, + "last_activity_id": null, + "update_time": "2025-02-17 21:15:41", + "activities_count": 0, + "postal_address_route": null, + "org_name": null, + "first_name": "Person Name", + "email": [ + { + "value": "", + "primary": true + } + ], + "postal_address_street_number": null, + "won_deals_count": 0, + "files_count": 0, + "company_id": 1234567890, + "last_incoming_mail_time": null, + "first_char": "p", + "closed_deals_count": 0, + "last_name": "02", + "label": null, + "related_lost_deals_count": 0, + "related_open_deals_count": 0, + "postal_address_formatted_address": null, + "postal_address_sublocality": null, + "postal_address_admin_area_level_2": null, + "postal_address_admin_area_level_1": null, + "followers_count": 0, + "primary_email": null, + "add_time": "2025-02-17 21:15:41", + "done_activities_count": 0 + }, + "previous": null, + "retry": 0, + "event": "added.person" +} \ No newline at end of file diff --git a/components/pipedrive/sources/new-person/new-person.mjs b/components/pipedrive/sources/new-person/new-person.mjs deleted file mode 100644 index 871224ee526b7..0000000000000 --- a/components/pipedrive/sources/new-person/new-person.mjs +++ /dev/null @@ -1,51 +0,0 @@ -import constants from "../../common/constants.mjs"; -import common from "../common/base.mjs"; - -export default { - ...common, - key: "pipedrive-new-person", - name: "New Person", - description: "Emit new event when a new person is created.", - version: "0.0.5", - type: "source", - dedupe: "unique", - methods: { - ...common.methods, - getFieldsFn() { - return this.app.getPersonFields; - }, - getResourceFn() { - return this.app.getPersons; - }, - getResourceFnArgs() { - return { - filter_id: this.getFilterId(), - sort: `${this.getFieldKey()} DESC, id DESC`, - }; - }, - getFieldKey() { - return constants.FIELD.ADD_TIME; - }, - getEventObject() { - return constants.EVENT_OBJECT.PERSON; - }, - getEventAction() { - return constants.EVENT_ACTION.ADDED; - }, - getTimestamp(resource) { - return Date.parse(resource.add_time); - }, - getFilterArgs({ - fieldId, value = "3_months_ago", - } = {}) { - return { - type: constants.FILTER_TYPE.PEOPLE, - name: "Pipedream: Persons created later than specific value", - conditions: this.getConditions({ - fieldId, - value, - }), - }; - }, - }, -}; diff --git a/components/pipedrive/sources/updated-deal-instant/test-event.mjs b/components/pipedrive/sources/updated-deal-instant/test-event.mjs new file mode 100644 index 0000000000000..5ec55c7f65796 --- /dev/null +++ b/components/pipedrive/sources/updated-deal-instant/test-event.mjs @@ -0,0 +1,179 @@ +export default { + "v": 1, + "matches_filters": { + "current": [] + }, + "meta": { + "action": "updated", + "change_source": "api", + "company_id": 1234567890, + "host": "subdomain.pipedrive.com", + "id": 9, + "is_bulk_update": false, + "matches_filters": { + "current": [] + }, + "object": "deal", + "permitted_user_ids": [ + 1234567890 + ], + "pipedrive_service_name": false, + "timestamp": 1739827361, + "timestamp_micro": 1739827361322662, + "prepublish_timestamp": 1739827361337, + "trans_pending": false, + "user_id": 1234567890, + "v": 1, + "webhook_id": "1234567890" + }, + "current": { + "email_messages_count": 0, + "cc_email": "subdomain+deal9@pipedrivemail.com", + "channel": 8, + "products_count": 0, + "archive_time": null, + "acv_currency": null, + "next_activity_date": null, + "acv": null, + "next_activity_type": null, + "local_close_date": null, + "next_activity_duration": null, + "id": 9, + "person_id": null, + "creator_user_id": 1234567890, + "expected_close_date": null, + "owner_name": "Owner Name", + "arr_currency": null, + "participants_count": 0, + "stage_id": 1, + "probability": null, + "undone_activities_count": 0, + "active": true, + "local_lost_date": null, + "person_name": null, + "last_activity_date": null, + "close_time": null, + "org_hidden": false, + "next_activity_id": null, + "weighted_value_currency": "USD", + "local_won_date": null, + "stage_order_nr": 0, + "next_activity_subject": null, + "rotten_time": null, + "is_archived": false, + "user_id": 1234567890, + "visible_to": "3", + "org_id": null, + "notes_count": 0, + "next_activity_time": null, + "channel_id": "Channel", + "formatted_value": "$0", + "status": "open", + "formatted_weighted_value": "$0", + "mrr_currency": null, + "first_won_time": null, + "origin": "Marketplace", + "last_outgoing_mail_time": null, + "origin_id": "5d80274eae050b58", + "title": "Updated Deal Title", + "last_activity_id": null, + "update_time": "2025-02-17 21:22:41", + "activities_count": 0, + "pipeline_id": 1, + "lost_time": null, + "currency": "USD", + "weighted_value": 0, + "org_name": null, + "value": 0, + "person_hidden": false, + "next_activity_note": null, + "arr": null, + "files_count": 0, + "last_incoming_mail_time": null, + "label": null, + "mrr": null, + "lost_reason": null, + "deleted": false, + "won_time": null, + "followers_count": 1, + "stage_change_time": null, + "add_time": "2025-02-17 18:06:03", + "done_activities_count": 0 + }, + "previous": { + "email_messages_count": 0, + "cc_email": "subdomain+deal9@pipedrivemail.com", + "channel": 8, + "products_count": 0, + "archive_time": null, + "acv_currency": null, + "next_activity_date": null, + "acv": null, + "next_activity_type": null, + "local_close_date": null, + "next_activity_duration": null, + "id": 9, + "person_id": null, + "creator_user_id": 1234567890, + "expected_close_date": null, + "owner_name": "Owner Name", + "arr_currency": null, + "participants_count": 0, + "stage_id": 1, + "probability": null, + "undone_activities_count": 0, + "active": true, + "local_lost_date": null, + "person_name": null, + "last_activity_date": null, + "close_time": null, + "org_hidden": false, + "next_activity_id": null, + "weighted_value_currency": "USD", + "local_won_date": null, + "stage_order_nr": 0, + "next_activity_subject": null, + "rotten_time": null, + "is_archived": false, + "user_id": 1234567890, + "visible_to": "3", + "org_id": null, + "notes_count": 0, + "next_activity_time": null, + "channel_id": "Channel", + "formatted_value": "$0", + "status": "open", + "formatted_weighted_value": "$0", + "mrr_currency": null, + "first_won_time": null, + "origin": "Marketplace", + "last_outgoing_mail_time": null, + "origin_id": "5d80274eae050b58", + "title": "Updated Deal Title", + "last_activity_id": null, + "update_time": "2025-02-17 19:46:52", + "activities_count": 0, + "pipeline_id": 1, + "lost_time": null, + "currency": "USD", + "weighted_value": 0, + "org_name": null, + "value": 0, + "person_hidden": false, + "next_activity_note": null, + "arr": null, + "files_count": 0, + "last_incoming_mail_time": null, + "label": null, + "mrr": null, + "lost_reason": null, + "deleted": false, + "won_time": null, + "followers_count": 1, + "stage_change_time": null, + "add_time": "2025-02-17 18:06:03", + "done_activities_count": 0 + }, + "retry": 0, + "event": "updated.deal" +} \ No newline at end of file diff --git a/components/pipedrive/sources/updated-deal-instant/updated-deal-instant.mjs b/components/pipedrive/sources/updated-deal-instant/updated-deal-instant.mjs new file mode 100644 index 0000000000000..d1a759e95ac53 --- /dev/null +++ b/components/pipedrive/sources/updated-deal-instant/updated-deal-instant.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "pipedrive-updated-deal-instant", + name: "New Deal Update (Instant)", + description: "Emit new event when a deal is updated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getExtraData() { + return { + event_action: "updated", + event_object: "deal", + }; + }, + getSummary(body) { + return `Deal successfully updated: ${body.current.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/pipedrive/sources/updated-deal/updated-deal.mjs b/components/pipedrive/sources/updated-deal/updated-deal.mjs deleted file mode 100644 index fdf33ff9ceeed..0000000000000 --- a/components/pipedrive/sources/updated-deal/updated-deal.mjs +++ /dev/null @@ -1,54 +0,0 @@ -import constants from "../../common/constants.mjs"; -import common from "../common/base.mjs"; - -export default { - ...common, - key: "pipedrive-updated-deal", - name: "Updated Deal", - description: "Emit new event when a deal is updated.", - version: "0.0.5", - type: "source", - dedupe: "greatest", - methods: { - ...common.methods, - getFieldsFn() { - return this.app.getDealFields; - }, - getResourceFn() { - return this.app.getDeals; - }, - getResourceFnArgs() { - return { - filter_id: this.getFilterId(), - sort: `${this.getFieldKey()} DESC, id DESC`, - }; - }, - getFieldKey() { - return constants.FIELD.UPDATE_TIME; - }, - getEventObject() { - return constants.EVENT_OBJECT.DEAL; - }, - getEventAction() { - return constants.EVENT_ACTION.UPDATED; - }, - getMetaId(resource) { - return this.getTimestamp(resource); - }, - getTimestamp(resource) { - return Date.parse(resource.update_time); - }, - getFilterArgs({ - fieldId, value = "3_months_ago", - } = {}) { - return { - type: constants.FILTER_TYPE.DEALS, - name: "Pipedream: Deals updated later than specific value", - conditions: this.getConditions({ - fieldId, - value, - }), - }; - }, - }, -}; diff --git a/components/pipedrive/sources/updated-person-instant/test-event.mjs b/components/pipedrive/sources/updated-person-instant/test-event.mjs new file mode 100644 index 0000000000000..98912c6596d7e --- /dev/null +++ b/components/pipedrive/sources/updated-person-instant/test-event.mjs @@ -0,0 +1,189 @@ +export default { + "v": 1, + "matches_filters": { + "current": [] + }, + "meta": { + "action": "updated", + "change_source": "app", + "company_id": 1234567890, + "host": "subdomain.pipedrive.com", + "id": 11, + "is_bulk_update": false, + "matches_filters": { + "current": [] + }, + "object": "person", + "permitted_user_ids": [ + 1234567890 + ], + "pipedrive_service_name": false, + "timestamp": 1739827618, + "timestamp_micro": 1739827618595189, + "prepublish_timestamp": 1739827618608, + "trans_pending": false, + "user_id": 1234567890, + "v": 1, + "webhook_id": "1234567890" + }, + "current": { + "related_closed_deals_count": 0, + "notes": null, + "email_messages_count": 0, + "cc_email": "subdomain@pipedrivemail.com", + "owner_id": 1234567890, + "open_deals_count": 0, + "postal_address_subpremise": null, + "next_activity_date": null, + "participant_open_deals_count": 0, + "postal_address_locality": null, + "id": 11, + "job_title": null, + "postal_address": null, + "owner_name": "Owner Name", + "im": [ + { + "value": "", + "primary": true + } + ], + "related_won_deals_count": 0, + "label_ids": [ + 14 + ], + "undone_activities_count": 0, + "picture_128_url": null, + "last_activity_date": null, + "next_activity_id": null, + "postal_address_country": null, + "phone": [ + { + "value": "", + "primary": true + } + ], + "visible_to": "3", + "org_id": null, + "notes_count": 0, + "name": "Person Name", + "participant_closed_deals_count": 0, + "lost_deals_count": 0, + "next_activity_time": null, + "birthday": null, + "delete_time": null, + "postal_address_postal_code": null, + "last_outgoing_mail_time": null, + "active_flag": true, + "picture_id": null, + "last_activity_id": null, + "update_time": "2025-02-17 21:26:58", + "activities_count": 0, + "postal_address_route": null, + "org_name": null, + "first_name": "Person Name", + "email": [ + { + "value": "", + "primary": true + } + ], + "postal_address_street_number": null, + "won_deals_count": 0, + "files_count": 0, + "company_id": 1234567890, + "last_incoming_mail_time": null, + "first_char": "p", + "closed_deals_count": 0, + "last_name": "Surname", + "label": 14, + "related_lost_deals_count": 0, + "related_open_deals_count": 0, + "postal_address_formatted_address": null, + "postal_address_sublocality": null, + "postal_address_admin_area_level_2": null, + "postal_address_admin_area_level_1": null, + "followers_count": 1, + "add_time": "2025-02-17 21:15:41", + "done_activities_count": 0 + }, + "previous": { + "related_closed_deals_count": 0, + "notes": null, + "email_messages_count": 0, + "cc_email": "subdomain@pipedrivemail.com", + "owner_id": 1234567890, + "open_deals_count": 0, + "postal_address_subpremise": null, + "next_activity_date": null, + "participant_open_deals_count": 0, + "postal_address_locality": null, + "id": 11, + "job_title": null, + "postal_address": null, + "owner_name": "Owner Name", + "im": [ + { + "value": "", + "primary": true + } + ], + "related_won_deals_count": 0, + "label_ids": [], + "undone_activities_count": 0, + "picture_128_url": null, + "last_activity_date": null, + "next_activity_id": null, + "postal_address_country": null, + "phone": [ + { + "value": "", + "primary": true + } + ], + "visible_to": "3", + "org_id": null, + "notes_count": 0, + "name": "Person Name", + "participant_closed_deals_count": 0, + "lost_deals_count": 0, + "next_activity_time": null, + "birthday": null, + "delete_time": null, + "postal_address_postal_code": null, + "last_outgoing_mail_time": null, + "active_flag": true, + "picture_id": null, + "last_activity_id": null, + "update_time": "2025-02-17 21:15:41", + "activities_count": 0, + "postal_address_route": null, + "org_name": null, + "first_name": "Person Name", + "email": [ + { + "value": "", + "primary": true + } + ], + "postal_address_street_number": null, + "won_deals_count": 0, + "files_count": 0, + "company_id": 1234567890, + "last_incoming_mail_time": null, + "first_char": "p", + "closed_deals_count": 0, + "last_name": "Surname", + "label": null, + "related_lost_deals_count": 0, + "related_open_deals_count": 0, + "postal_address_formatted_address": null, + "postal_address_sublocality": null, + "postal_address_admin_area_level_2": null, + "postal_address_admin_area_level_1": null, + "followers_count": 1, + "add_time": "2025-02-17 21:15:41", + "done_activities_count": 0 + }, + "retry": 0, + "event": "updated.person" +} \ No newline at end of file diff --git a/components/pipedrive/sources/updated-person-instant/updated-person-instant.mjs b/components/pipedrive/sources/updated-person-instant/updated-person-instant.mjs new file mode 100644 index 0000000000000..73e4527455a93 --- /dev/null +++ b/components/pipedrive/sources/updated-person-instant/updated-person-instant.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "pipedrive-updated-person-instant", + name: "New Person Update (Instant)", + description: "Emit new event when a person is updated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getExtraData() { + return { + event_action: "updated", + event_object: "person", + }; + }, + getSummary(body) { + return `Person successfully updated: ${body.current.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/pipedrive/sources/updated-person/updated-person.mjs b/components/pipedrive/sources/updated-person/updated-person.mjs deleted file mode 100644 index d4e3602262b0a..0000000000000 --- a/components/pipedrive/sources/updated-person/updated-person.mjs +++ /dev/null @@ -1,54 +0,0 @@ -import constants from "../../common/constants.mjs"; -import common from "../common/base.mjs"; - -export default { - ...common, - key: "pipedrive-updated-person", - name: "Updated Person", - description: "Emit new event when a person is updated.", - version: "0.0.5", - type: "source", - dedupe: "greatest", - methods: { - ...common.methods, - getFieldsFn() { - return this.app.getPersonFields; - }, - getResourceFn() { - return this.app.getPersons; - }, - getResourceFnArgs() { - return { - filter_id: this.getFilterId(), - sort: `${this.getFieldKey()} DESC, id DESC`, - }; - }, - getFieldKey() { - return constants.FIELD.UPDATE_TIME; - }, - getEventObject() { - return constants.EVENT_OBJECT.PERSON; - }, - getEventAction() { - return constants.EVENT_ACTION.UPDATED; - }, - getMetaId(resource) { - return this.getTimestamp(resource); - }, - getTimestamp(resource) { - return Date.parse(resource.update_time); - }, - getFilterArgs({ - fieldId, value = "3_months_ago", - } = {}) { - return { - type: constants.FILTER_TYPE.PEOPLE, - name: "Pipedream: Persons updated later than specific value", - conditions: this.getConditions({ - fieldId, - value, - }), - }; - }, - }, -}; diff --git a/components/pipefy/README.md b/components/pipefy/README.md index c07d31e490d2d..ef14fd622c556 100644 --- a/components/pipefy/README.md +++ b/components/pipefy/README.md @@ -1,19 +1,11 @@ # Overview -Using the Pipefy API, it is possible to build powerful workflows that help to streamline the entire process of managing any given task or process. The API provides access to all of Pipefy’s features, allowing developers to create custom-built workflows that are tailored for their specific needs. +Pipefy is a platform that empowers users to streamline complex processes and workflows without the need for technical skills. With the Pipefy API, you can automate various aspects of your Pipefy environment, such as creating cards, updating fields, and managing pipelines programmatically. Use Pipedream to connect Pipefy with hundreds of other apps and orchestrate workflows that can save time, reduce errors, and enhance efficiency. -With Pipefy’s API, developers can take advantage of the following: +# Example Use Cases -- Create and manage customized pipelines -- Automatically trigger events between different phases -- Track progress and milestones -- Establish custom notifications -- Manage resources +- **Automated Client Onboarding:** Trigger a Pipedream workflow when a new client fills out a form on your website. Use the Pipefy API to create a new card in your Client Onboarding pipe, and then send a welcome email to the client using the Gmail app. This ensures a smooth and systematic onboarding experience for each new client. -Examples of what you can build with the Pipefy API: +- **Issue Tracking Integration:** Connect Pipefy to your customer support platform, like Zendesk. When a support ticket is marked as a bug, automatically create a card in your Pipefy Bug Tracking pipe and assign it to the development team. This creates a seamless bridge between customer support and the dev team, ensuring quick response to critical issues. -- A workflow to oversee the recruitment process, from applicant submission to onboarding. -- A workflow to organize the marketing team’s daily, weekly and monthly tasks. -- A workflow to oversee the development of software products, from planning to the release. -- An automated system for processing customer requests and complaints. -- A workflow to manage product releases from inception to launch. +- **Sales Lead Management:** Start a workflow whenever a new lead is captured via LinkedIn Lead Gen Forms. Add the lead as a card in your Pipefy Sales Pipeline, and simultaneously update your CRM, such as Salesforce, with the new lead's information. This cuts down manual entry and keeps your sales team informed about fresh opportunities in real-time. diff --git a/components/pipeline/README.md b/components/pipeline/README.md index b6bf7eeb8be12..29d42118cad73 100644 --- a/components/pipeline/README.md +++ b/components/pipeline/README.md @@ -1,16 +1,11 @@ # Overview -The Pipeline API () allows users to create and manage their sales pipelines, track leads, manage contacts, and streamline their customer updates. With the Pipeline API, you can build a powerful CRM system that can help your business grow and increase productivity. +The Pipeline API on Pipedream enables you to design robust automations that streamline your sales and engagement processes. By leveraging this API, you can programmatically manage your sales pipeline, track email interactions, and book meetings automatically. Create triggers based on email events, update deals based on customer actions, or sync your sales data with other platforms for deeper analysis. With Pipedream's serverless architecture, you can create workflows that respond in real-time, ensuring your sales team has the freshest information at their fingertips. -Here are some examples of what you can build using the Pipeline API: +# Example Use Cases -- A custom sales pipeline with automated scheduling and task management -- Lead tracking and management tools -- Integrations with existing systems like Google Sheets and Salesforce -- Contact management and segmentation capabilities -- Automated nurture campaigns and email blasts -- Automated customer updates via SMS and other channels -- Custom integrations with third-party software and databases -- Automated thumbnails and email signatures -- Real-time reporting and analytics tools -- A custom chatbot to handle customer inquiries +- **Sales Lead Scoring Automation**: Automatically score leads based on their engagement with emails and web content. Use the Pipeline API to monitor interactions, such as email opens or link clicks, and feed this data into a scoring algorithm. High-scoring leads can be escalated to the sales team via Slack using Pipedream's Slack integration. + +- **Automatic Meeting Scheduler**: Set up a workflow that listens for specific phrases in emails, like "schedule a call," and use the Pipeline API to trigger an automatic meeting booking. Connect with Google Calendar using Pipedream's Google Calendar integration to find available slots and send calendar invites without manual intervention. + +- **Real-time Deal Updates**: Create a system that updates deal stages in your CRM whenever a lead interacts with an email or completes a specific action on your website. Use the Pipeline API to adjust the deal status and then sync these updates with other tools like Salesforce, HubSpot, or a custom database using Pipedream's integrations for seamless data flow. diff --git a/components/pipeliner_crm/README.md b/components/pipeliner_crm/README.md index d92e09a2c4c03..c04e1268d6831 100644 --- a/components/pipeliner_crm/README.md +++ b/components/pipeliner_crm/README.md @@ -1,15 +1,11 @@ # Overview -What Can I Build with the Pipeliner CRM API? - -The Pipeliner CRM API offers a wide range of capabilities to build apps and services that interact with Pipeliner CRM. From full-fledged integrations to simple automation of data synchronization, you can use the API for an array of possibilities. Here are just a few of the things you can build with the Pipeliner CRM API: - -1. Create custom integrations between Pipeliner CRM and external services and applications. -2. Automate data synchronization and enable two-way synchronization between Pipeliner CRM and other systems. -3. Auto-generate reports and dashboard to quickly assess performance and trends. -4. Automate business processes, such as creating leads, tasks, notes, and others. -5. Customize different aspects of Pipeliner CRM, such as user interface, custom fields, and data types. -6. Access user records, accounts, contacts, activities, and more. -7. Create personalized email campaigns for specific accounts. -8. Create personalized reminders for events, appointments, and other tasks. -9. Create custom filters for accounts and contacts to query and retrieve specific data. +The Pipeliner CRM API unlocks a world of possibilities for managing sales pipelines and customer relationships. With Pipedream's serverless integration platform, you can automate routine tasks, sync data across different systems, and trigger bespoke workflows. You can create new leads, update opportunities, fetch account details, and more. By leveraging Pipedream's capabilities, you can seamlessly connect Pipeliner CRM with numerous other apps and services to streamline sales processes, enhance customer engagement, and drive efficiency. + +# Example Use Cases + +- **Lead Score Automation**: As soon as a lead's score reaches a predefined threshold in Pipeliner CRM, use Pipedream to trigger a personalized email from SendGrid, suggesting a follow-up by the sales team. This ensures leads are engaged at just the right time to maximize conversion chances. + +- **Deal Closure Celebration**: When a deal is marked as won in Pipeliner CRM, trigger a message in Slack to celebrate the success with the team. This could also kick off a workflow that prepares onboarding documents in Google Drive, ensuring a smooth transition from sales to account management. + +- **Support Ticket Creation**: If a customer issue is detected within Pipeliner CRM, you could use Pipedream to automatically create a ticket in Zendesk. This keeps customer support in the loop and ensures that any problems are addressed promptly, contributing to better customer satisfaction and retention. diff --git a/components/pipeliner_crm/package.json b/components/pipeliner_crm/package.json new file mode 100644 index 0000000000000..7bd6942d5b806 --- /dev/null +++ b/components/pipeliner_crm/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/pipeliner_crm", + "version": "0.6.0", + "description": "Pipedream pipeliner_crm Components", + "main": "pipeliner_crm.app.mjs", + "keywords": [ + "pipedream", + "pipeliner_crm" + ], + "homepage": "https://pipedream.com/apps/pipeliner_crm", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/pirate_weather/README.md b/components/pirate_weather/README.md new file mode 100644 index 0000000000000..cf851a335b0cf --- /dev/null +++ b/components/pirate_weather/README.md @@ -0,0 +1,11 @@ +# Overview + +The Pirate Weather API delivers accurate weather forecasts, leveraging the same data model as top-tier weather services. Within Pipedream, you can craft workflows that tap into this forecast data to trigger events, power notifications, or feed into data analytics tools. The serverless nature of Pipedream simplifies the process of setting up these workflows, allowing for easy integration with various services for a myriad of applications ranging from personal alerts to data driven decision-making in business. + +# Example Use Cases + +- **Personalized Daily Weather Report to Email**: Use the Pirate Weather API to fetch the daily forecast and send a customized weather report to your email each morning. This could be further integrated with calendar apps to suggest the best times for outdoor activities based on weather conditions. + +- **Smart Home Automation Based on Weather Conditions**: Trigger smart home devices like thermostats or blinds in response to real-time weather updates. For example, adjust your home's heating or cooling systems according to the forecasted temperatures for energy efficiency. + +- **Weather-Dependent Content Publishing**: Automate content posting on social media or websites based on specific weather scenarios. For instance, publish pre-written articles or promotions for rain gear on retail platforms when the forecast predicts rainfall. diff --git a/components/pitchlane/README.md b/components/pitchlane/README.md new file mode 100644 index 0000000000000..d46d0d0f7a0ef --- /dev/null +++ b/components/pitchlane/README.md @@ -0,0 +1,11 @@ +# Overview + +The Pitchlane API enables users to automate and integrate media planning and buying processes. This API can streamline the creation, management, and optimization of advertising campaigns across various channels. By utilizing Pitchlane within Pipedream, users can connect their media planning tools with other business systems (like CRM, analytics, or communication platforms), automate repetitive tasks, and orchestrate complex workflows that respond to real-time data changes. + +# Example Use Cases + +- **Campaign Performance Sync to Google Sheets**: Automate the process of syncing detailed campaign performance data from Pitchlane to a Google Sheets document. This workflow can be set to trigger every time a campaign's data is updated, ensuring that stakeholders have access to the most current information without manual intervention. Useful for real-time reporting and data aggregation for meetings or audits. + +- **Slack Notifications for Campaign Alerts**: Set up an automated alert system where any critical updates or metrics (like budget overruns or KPI achievements) are instantly sent as notifications to a designated Slack channel. This ensures that the marketing team stays informed about important developments in real-time, promoting swift decision-making and immediate responses to potential issues. + +- **Automated Email Reports with SendGrid**: Design a workflow where weekly or monthly email summaries of campaign performances are automatically generated and sent through SendGrid. This can include key performance indicators, budget usage, and other relevant data tailored to the needs of different stakeholders. This eliminates the need for manual report preparation and ensures consistent updates are delivered to executives or clients. diff --git a/components/pitchlane/package.json b/components/pitchlane/package.json new file mode 100644 index 0000000000000..9b0c2253d35ed --- /dev/null +++ b/components/pitchlane/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/pitchlane", + "version": "0.0.1", + "description": "Pipedream Pitchlane Components", + "main": "pitchlane.app.mjs", + "keywords": [ + "pipedream", + "pitchlane" + ], + "homepage": "https://pipedream.com/apps/pitchlane", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/pitchlane/pitchlane.app.mjs b/components/pitchlane/pitchlane.app.mjs new file mode 100644 index 0000000000000..a445ecf903825 --- /dev/null +++ b/components/pitchlane/pitchlane.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "pitchlane", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/pivotal_tracker/README.md b/components/pivotal_tracker/README.md index 4847771d36e42..0b769e75ee760 100644 --- a/components/pivotal_tracker/README.md +++ b/components/pivotal_tracker/README.md @@ -1,12 +1,11 @@ # Overview - Using the Pivotal Tracker API, you can create powerful applications and integrations that bring the features of Pivotal Tracker to your own applications. Here are a few examples: - -1. Create your own tracking and reporting tools for team performance on projects. -2. Automate updates to Pivotal Tracker from your own applications. -3. Create applications that display interactive and real-time project timelines. -4. Generate accurate reports from project data. -5. Automate the project creation process. -6. Create notifications and alerts for project updates. -7. Monitor multiple projects from a single application. -8. Utilize the API to integrate with third-party applications like Office 365, Slack, and Salesforce. +Pipedream's integration with the Pivotal Tracker API turns your project management into a powerhouse of automation and connectivity. It allows you to orchestrate workflows that can respond to changes in your projects, stories, and tasks in real-time. For instance, you can sync project updates with other tools, extract metrics for reporting, or even streamline notification systems to keep your entire team in the loop effortlessly. + +# Example Use Cases + +- **Automated Task Distribution**: When a new story is created in Pivotal Tracker, Pipedream triggers a workflow that automatically assigns tasks to developers based on their current workload or expertise, which is tracked in a separate database or app like Airtable. + +- **Slack Project Updates**: Keep your team informed with real-time alerts. Set up a workflow where any status changes in Pivotal Tracker stories push notifications to a dedicated Slack channel, ensuring that everyone is aware of project progress without needing to leave their communication platform. + +- **Sprint Report Generation**: At the end of each sprint, Pipedream can collate completed stories and tasks from Pivotal Tracker, format them into a comprehensive report, and then send this report via email or store it in a shared Google Drive folder for stakeholders' review. diff --git a/components/pixelbin/actions/create-folder/create-folder.mjs b/components/pixelbin/actions/create-folder/create-folder.mjs new file mode 100644 index 0000000000000..d5dd69f65dfbb --- /dev/null +++ b/components/pixelbin/actions/create-folder/create-folder.mjs @@ -0,0 +1,52 @@ +import app from "../../pixelbin.app.mjs"; + +export default { + key: "pixelbin-create-folder", + name: "Create Folder", + description: "Creates a new folder in Pixelbin. [See the documentation](https://www.pixelbin.io/docs/api-docs/)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + optional: false, + description: "Name of the folder.", + propDefinition: [ + app, + "name", + ], + }, + path: { + description: "Path of the containing folder. Eg. `folder1/folder2`.", + propDefinition: [ + app, + "path", + ], + }, + }, + methods: { + createFolder(args = {}) { + return this.app.post({ + path: "/folders", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createFolder, + name, + path, + } = this; + + const response = await createFolder({ + $, + data: { + name, + path, + }, + }); + $.export("$summary", `Successfully created folder with ID \`${response._id}\`.`); + return response; + }, +}; diff --git a/components/pixelbin/actions/delete-file/delete-file.mjs b/components/pixelbin/actions/delete-file/delete-file.mjs new file mode 100644 index 0000000000000..fec09fa142b06 --- /dev/null +++ b/components/pixelbin/actions/delete-file/delete-file.mjs @@ -0,0 +1,47 @@ +import app from "../../pixelbin.app.mjs"; + +export default { + key: "pixelbin-delete-file", + name: "Delete File", + description: "Deletes a file from Pixelbin. [See the documentation](https://www.pixelbin.io/docs/api-docs/)", + version: "0.0.1", + type: "action", + props: { + app, + path: { + optional: false, + propDefinition: [ + app, + "path", + () => ({ + params: { + onlyFolders: false, + onlyFiles: true, + }, + }), + ], + }, + }, + methods: { + deleteFile({ + path, ...args + }) { + return this.app.delete({ + path: `/files/${path}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + deleteFile, + path, + } = this; + const response = await deleteFile({ + $, + path, + }); + $.export("$summary", `Successfully deleted file with ID \`${response._id}\`.`); + return response; + }, +}; diff --git a/components/pixelbin/actions/list-files/list-files.mjs b/components/pixelbin/actions/list-files/list-files.mjs new file mode 100644 index 0000000000000..cc800b992bea5 --- /dev/null +++ b/components/pixelbin/actions/list-files/list-files.mjs @@ -0,0 +1,65 @@ +import app from "../../pixelbin.app.mjs"; + +export default { + key: "pixelbin-list-files", + name: "List Files", + description: "List all files. [See the documentation](https://www.pixelbin.io/docs/api-docs/)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + description: "Find items with matching name.", + propDefinition: [ + app, + "name", + ], + }, + path: { + description: "Find items with matching path.", + propDefinition: [ + app, + "path", + ], + }, + format: { + type: "string", + label: "Format", + description: "Find items with matching format.", + optional: true, + }, + tags: { + description: "Find items containing these tags", + propDefinition: [ + app, + "tags", + ], + }, + }, + async run({ $ }) { + const { + app, + name, + path, + format, + tags, + } = this; + + const result = await app.paginate({ + resourcesFn: app.listFiles, + resourcesFnArgs: { + $, + params: { + name, + path, + format, + tags, + onlyFiles: true, + }, + }, + resourceName: "items", + }); + $.export("$summary", `Successfully listed \`${result.length}\` file(s).`); + return result; + }, +}; diff --git a/components/pixelbin/actions/upload-asset-url/upload-asset-url.mjs b/components/pixelbin/actions/upload-asset-url/upload-asset-url.mjs new file mode 100644 index 0000000000000..3a6111f2aaf95 --- /dev/null +++ b/components/pixelbin/actions/upload-asset-url/upload-asset-url.mjs @@ -0,0 +1,97 @@ +import app from "../../pixelbin.app.mjs"; + +export default { + key: "pixelbin-upload-asset-url", + name: "Upload Asset From URL", + description: "Uploads an asset to Pixelbin from a given URL. [See the documentation](https://www.pixelbin.io/docs/api-docs/)", + version: "0.0.1", + type: "action", + props: { + app, + url: { + type: "string", + label: "URL", + description: "URL of the asset you want to upload.", + }, + path: { + propDefinition: [ + app, + "path", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + access: { + propDefinition: [ + app, + "access", + ], + }, + tags: { + propDefinition: [ + app, + "tags", + ], + }, + metadata: { + propDefinition: [ + app, + "metadata", + ], + }, + overwrite: { + propDefinition: [ + app, + "overwrite", + ], + }, + filenameOverride: { + propDefinition: [ + app, + "filenameOverride", + ], + }, + }, + methods: { + uploadAssetFromUrl(args = {}) { + return this.app.post({ + path: "/upload/url", + ...args, + }); + }, + }, + async run({ $ }) { + const { + uploadAssetFromUrl, + url, + path, + name, + access, + tags, + metadata, + overwrite, + filenameOverride, + } = this; + + const response = await uploadAssetFromUrl({ + $, + data: { + url, + path, + name, + access, + tags, + metadata, + overwrite, + filenameOverride, + }, + }); + + $.export("$summary", `Successfully uploaded asset with ID: \`${response._id}\`.`); + return response; + }, +}; diff --git a/components/pixelbin/actions/upload-file/upload-file.mjs b/components/pixelbin/actions/upload-file/upload-file.mjs new file mode 100644 index 0000000000000..c0ed332d74b24 --- /dev/null +++ b/components/pixelbin/actions/upload-file/upload-file.mjs @@ -0,0 +1,112 @@ +import fs from "fs"; +import FormData from "form-data"; +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../pixelbin.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "pixelbin-upload-file", + name: "Upload File", + description: "Upload a file to Pixelbin. [See the documentation](https://www.pixelbin.io/docs/api-docs/)", + version: "0.0.1", + type: "action", + props: { + app, + filePath: { + type: "string", + label: "File Path", + description: "Assete file path. The path to the file saved to the `/tmp` directory (e.g. `/tmp/example.pdf`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + }, + path: { + propDefinition: [ + app, + "path", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + access: { + propDefinition: [ + app, + "access", + ], + }, + tags: { + propDefinition: [ + app, + "tags", + ], + }, + metadata: { + propDefinition: [ + app, + "metadata", + ], + }, + overwrite: { + propDefinition: [ + app, + "overwrite", + ], + }, + filenameOverride: { + propDefinition: [ + app, + "filenameOverride", + ], + }, + }, + methods: { + uploadFile(args = {}) { + return this.app.post({ + path: "/upload/direct", + headers: { + "Content-Type": "multipart/form-data", + }, + ...args, + }); + }, + }, + async run({ $ }) { + const { + uploadFile, + filePath, + path, + name, + access, + tags, + metadata, + overwrite, + filenameOverride, + } = this; + + if (!filePath.startsWith("/tmp/")) { + throw new ConfigurationError("File must be located in `/tmp` directory."); + } + + const data = new FormData(); + data.append("file", fs.createReadStream(filePath)); + + utils.appendPropsToFormData(data, { + path, + name, + access, + tags, + metadata, + overwrite, + filenameOverride, + }); + + const response = await uploadFile({ + $, + data, + }); + + $.export("$summary", `Successfully uploaded file with ID \`${response._id}\`.`); + return response; + }, +}; diff --git a/components/pixelbin/common/constants.mjs b/components/pixelbin/common/constants.mjs new file mode 100644 index 0000000000000..749b834db7fc9 --- /dev/null +++ b/components/pixelbin/common/constants.mjs @@ -0,0 +1,27 @@ +const VERSION_PLACEHOLDER = "{version}"; +const BASE_URL = "https://api.pixelbin.io/service/platform"; +const DEFAULT_MAX = 600; +const DEFAULT_LIMIT = 100; + +const API = { + ASSETS: { + PATH: `/assets/${VERSION_PLACEHOLDER}`, + VERSION10: "v1.0", + VERSION20: "v2.0", + }, + ORGANIZATION: { + PATH: `/organization/${VERSION_PLACEHOLDER}`, + VERSION10: "v1.0", + }, + TRANSFORMATION: { + PATH: "/transformation", + }, +}; + +export default { + VERSION_PLACEHOLDER, + BASE_URL, + API, + DEFAULT_LIMIT, + DEFAULT_MAX, +}; diff --git a/components/pixelbin/common/utils.mjs b/components/pixelbin/common/utils.mjs new file mode 100644 index 0000000000000..c23f8402b1bda --- /dev/null +++ b/components/pixelbin/common/utils.mjs @@ -0,0 +1,39 @@ +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +function appendPropsToFormData(form, props) { + Object.entries(props) + .forEach(([ + key, + value, + ]) => { + if (value === undefined) { + return; + } + if (Array.isArray(value)) { + value.forEach((tag, idx) => { + form.append(`${key}[${idx}]`, String(tag)); + }); + } else if (typeof value === "object" && value !== null) { + form.append(key, JSON.stringify(value)); + } else { + form.append(key, String(value)); + } + }); +} + +function getNestedProperty(obj, propertyString) { + const properties = propertyString.split("."); + return properties.reduce((prev, curr) => prev?.[curr], obj); +} + +export default { + iterate, + appendPropsToFormData, + getNestedProperty, +}; diff --git a/components/pixelbin/package.json b/components/pixelbin/package.json new file mode 100644 index 0000000000000..c6d65d6eb0032 --- /dev/null +++ b/components/pixelbin/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/pixelbin", + "version": "0.1.0", + "description": "Pipedream Pixelbin Components", + "main": "pixelbin.app.mjs", + "keywords": [ + "pipedream", + "pixelbin" + ], + "homepage": "https://pipedream.com/apps/pixelbin", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3", + "form-data": "^4.0.0" + } +} diff --git a/components/pixelbin/pixelbin.app.mjs b/components/pixelbin/pixelbin.app.mjs new file mode 100644 index 0000000000000..bc15f6ee72e30 --- /dev/null +++ b/components/pixelbin/pixelbin.app.mjs @@ -0,0 +1,187 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; + +export default { + type: "app", + app: "pixelbin", + propDefinitions: { + path: { + type: "string", + label: "Path", + description: "Path where you want to store the asset. Path of containing folder. Eg. `folder1/folder2`.", + optional: true, + async options({ + page: pageNo, params, prevContext: { hasNext }, + mapper = ({ + name, path, + }) => path + ? `${path}/${name}` + : name, + }) { + if (hasNext === false) { + return []; + } + const { + page, + items, + } = await this.listFiles({ + params: { + onlyFolders: true, + ...params, + pageNo: pageNo + 1, + }, + }); + return { + options: items.map(mapper), + context: { + hasNext: page.hasNext, + }, + }; + }, + }, + name: { + type: "string", + label: "Name", + description: "Name of the asset, if not provided name of the file will be used. Note - The provided name will be slugified to make it URL safe.", + optional: true, + }, + access: { + type: "string", + label: "Access", + description: "Access level of the asset.", + options: [ + "public-read", + "private", + ], + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "Tags for the asset.", + optional: true, + }, + metadata: { + type: "object", + label: "Metadata", + description: "Metadata for the asset.", + optional: true, + }, + overwrite: { + type: "boolean", + label: "Overwrite", + description: "Overwrite flag. If set to `true` will overwrite any file that exists with same path, name and type. Defaults to `false`.", + optional: true, + }, + filenameOverride: { + type: "boolean", + label: "Filename Override", + description: "If set to `true` will add unique characters to name if asset with given name already exists. If overwrite flag is set to `true`, preference will be given to overwrite flag. If both are set to `false` an error will be raised.", + optional: true, + }, + }, + methods: { + getUrl(path, api = constants.API.ASSETS, version = constants.API.ASSETS.VERSION10) { + const versionPath = version + ? api.PATH.replace(constants.VERSION_PLACEHOLDER, version) + : api.PATH; + return `${constants.BASE_URL}${versionPath}${path}`; + }, + getHeaders(headers) { + const { api_token: apiToken } = this.$auth; + const apiKey = Buffer.from(apiToken).toString("base64"); + return { + "Content-Type": "application/json", + ...headers, + "Accept": "application/json", + "Authorization": `Bearer ${apiKey}`, + }; + }, + _makeRequest({ + $ = this, path, api, version, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path, api, version), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + ...args, + method: "POST", + }); + }, + delete(args = {}) { + return this._makeRequest({ + ...args, + method: "DELETE", + }); + }, + listFiles(args = {}) { + return this._makeRequest({ + path: "/listFiles", + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, resourceName, + lastDateAt, dateField, + max = constants.DEFAULT_MAX, + }) { + let page = 1; + let resourcesCount = 0; + + while (true) { + const response = + await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + pageNo: page, + pageSize: constants.DEFAULT_LIMIT, + }, + }); + + const nextResources = utils.getNestedProperty(response, resourceName); + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + const isDateGreater = + lastDateAt + && Date.parse(resource[dateField]) >= Date.parse(lastDateAt); + + if (!lastDateAt || isDateGreater) { + yield resource; + resourcesCount += 1; + } + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + if (nextResources.length < constants.DEFAULT_LIMIT) { + console.log("No next page found"); + return; + } + + if (response.page.hasNext === false) { + console.log("hasNext is false"); + return; + } + + page += 1; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); + }, + }, +}; diff --git a/components/pixiebrix/README.md b/components/pixiebrix/README.md new file mode 100644 index 0000000000000..414484a8c0b6d --- /dev/null +++ b/components/pixiebrix/README.md @@ -0,0 +1,11 @@ +# Overview + +PixieBrix is an extension framework for creating and sharing browser enhancements that streamline and enrich web interactions. It makes the customization of webpages simple, allowing users to tailor their web experience and automate tasks without needing to write code. In Pipedream, you can harness the PixieBrix API to trigger workflows from web actions, scrape data, and integrate with countless other services to automate tasks, enhance analytics, or sync data across platforms. + +# Example Use Cases + +- **Trigger Actions From Web Events**: Create a Pipedream workflow that listens for specific actions taken in a browser, like form submissions or button clicks, and uses PixieBrix to trigger an automated response, such as sending an email via SendGrid or creating a task in Asana. + +- **Scrape and Process Web Data**: Set up a workflow where PixieBrix scrapes data from a web page, perhaps product information or articles, and passes it to Pipedream, which then processes and stores the data in Google Sheets or sends it to a database like MongoDB for further analysis. + +- **Custom Notifications and Alerts**: Develop a workflow where PixieBrix monitors certain webpages for changes or specific conditions, then utilizes Pipedream to send custom notifications through Slack, SMS via Twilio, or even to update a status in a project management tool like Trello. diff --git a/components/pixiebrix/actions/add-group-memberships/add-group-memberships.mjs b/components/pixiebrix/actions/add-group-memberships/add-group-memberships.mjs new file mode 100644 index 0000000000000..868283d972d5c --- /dev/null +++ b/components/pixiebrix/actions/add-group-memberships/add-group-memberships.mjs @@ -0,0 +1,83 @@ +import utils from "../../common/utils.mjs"; +import app from "../../pixiebrix.app.mjs"; + +export default { + key: "pixiebrix-add-group-memberships", + name: "Add Group Memberships", + description: "Adds user memberships to a group in PixieBrix. [See the documentation](https://docs.pixiebrix.com/developer-api/team-management-apis#add-group-memberships)", + version: "0.0.1", + type: "action", + props: { + app, + organizationId: { + propDefinition: [ + app, + "organizationId", + ], + }, + groupId: { + propDefinition: [ + app, + "groupId", + ({ organizationId }) => ({ + organizationId, + }), + ], + }, + userIds: { + type: "string[]", + label: "User IDs", + description: "The IDs or emails of the users to add to the group", + propDefinition: [ + app, + "userId", + ({ organizationId }) => ({ + organizationId, + }), + ], + }, + }, + methods: { + addGroupMemberships({ + groupId, ...args + } = {}) { + return this.app.post({ + headers: { + "Accept": "application/json; version=1.0", + }, + path: `/groups/${groupId}/memberships/`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + addGroupMemberships, + groupId, + userIds, + } = this; + + await addGroupMemberships({ + $, + groupId, + data: utils.parseArray(userIds) + ?.map((value) => { + const isEmail = utils.isEmail(value); + return { + ...(isEmail && { + email: value, + }), + ...(!isEmail && { + user_id: value, + }), + }; + }), + }); + + $.export("$summary", `Successfully added memberships to group ID \`${groupId}\``); + + return { + success: true, + }; + }, +}; diff --git a/components/pixiebrix/actions/delete-group-membership/delete-group-membership.mjs b/components/pixiebrix/actions/delete-group-membership/delete-group-membership.mjs new file mode 100644 index 0000000000000..851d562ec64b3 --- /dev/null +++ b/components/pixiebrix/actions/delete-group-membership/delete-group-membership.mjs @@ -0,0 +1,68 @@ +import app from "../../pixiebrix.app.mjs"; + +export default { + key: "pixiebrix-delete-group-membership", + name: "Delete Group Membership", + description: "Deletes a single group membership. [See the documentation](https://docs.pixiebrix.com/developer-api/team-management-apis#delete-group-membership)", + version: "0.0.1", + type: "action", + props: { + app, + organizationId: { + propDefinition: [ + app, + "organizationId", + ], + }, + groupId: { + propDefinition: [ + app, + "groupId", + ({ organizationId }) => ({ + organizationId, + }), + ], + }, + membershipId: { + propDefinition: [ + app, + "membershipId", + ({ groupId }) => ({ + groupId, + }), + ], + }, + }, + methods: { + deleteGroupMembership({ + groupId, membershipId, ...args + } = {}) { + return this.app.delete({ + headers: { + "Accept": "application/json; version=1.0", + }, + path: `/groups/${groupId}/memberships/${membershipId}/`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + deleteGroupMembership, + groupId, + membershipId, + } = this; + + await deleteGroupMembership({ + $, + groupId, + membershipId, + }); + + $.export("$summary", `Successfully deleted membership with ID: \`${membershipId}\``); + + return { + success: true, + }; + }, +}; diff --git a/components/pixiebrix/actions/list-group-memberships/list-group-memberships.mjs b/components/pixiebrix/actions/list-group-memberships/list-group-memberships.mjs new file mode 100644 index 0000000000000..fcf343ef8f988 --- /dev/null +++ b/components/pixiebrix/actions/list-group-memberships/list-group-memberships.mjs @@ -0,0 +1,42 @@ +import app from "../../pixiebrix.app.mjs"; + +export default { + key: "pixiebrix-list-group-memberships", + name: "List Group Memberships", + description: "Gets the current memberships of a group. [See the PixieBrix API documentation](https://docs.pixiebrix.com/developer-api/team-management-apis#list-group-memberships)", + version: "0.0.1", + type: "action", + props: { + app, + organizationId: { + propDefinition: [ + app, + "organizationId", + ], + }, + groupId: { + propDefinition: [ + app, + "groupId", + ({ organizationId }) => ({ + organizationId, + }), + ], + }, + }, + async run({ $ }) { + const { + app, + groupId, + } = this; + + const response = await app.listGroupMemberships({ + $, + groupId, + }); + + $.export("$summary", `Successfully listed \`${response?.length || 0}\` group membership(s)`); + + return response || []; + }, +}; diff --git a/components/pixiebrix/actions/update-group-membership/update-group-membership.mjs b/components/pixiebrix/actions/update-group-membership/update-group-membership.mjs new file mode 100644 index 0000000000000..0987099dd40bb --- /dev/null +++ b/components/pixiebrix/actions/update-group-membership/update-group-membership.mjs @@ -0,0 +1,86 @@ +import app from "../../pixiebrix.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "pixiebrix-update-group-membership", + name: "Update Group Membership", + description: "Updates the memberships of a group in PixieBrix. [See the documentation](https://docs.pixiebrix.com/developer-api/team-management-apis#update-group-membership)", + version: "0.0.1", + type: "action", + props: { + app, + organizationId: { + propDefinition: [ + app, + "organizationId", + ], + }, + groupId: { + propDefinition: [ + app, + "groupId", + ({ organizationId }) => ({ + organizationId, + }), + ], + }, + userIds: { + type: "string[]", + label: "User IDs", + description: "The IDs or emails of the users to update in the group", + propDefinition: [ + app, + "userId", + ({ organizationId }) => ({ + organizationId, + mapper: ({ + user: { + email: value, name, + }, + }) => ({ + label: [ + name, + value, + ].join(" ").trim(), + value, + }), + }), + ], + }, + }, + methods: { + updateGroupMemberships({ + groupId, ...args + }) { + return this.app.put({ + headers: { + "Accept": "application/json; version=1.0", + }, + path: `/groups/${groupId}/memberships/`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + updateGroupMemberships, + groupId, + userIds, + } = this; + + await updateGroupMemberships({ + $, + groupId, + data: utils.parseArray(userIds) + ?.map((value) => ({ + email: value, + })), + }); + + $.export("$summary", `Successfully updated memberships of group ID \`${groupId}\``); + + return { + success: true, + }; + }, +}; diff --git a/components/pixiebrix/common/constants.mjs b/components/pixiebrix/common/constants.mjs new file mode 100644 index 0000000000000..b05224647847b --- /dev/null +++ b/components/pixiebrix/common/constants.mjs @@ -0,0 +1,9 @@ +const BASE_URL = "https://app.pixiebrix.com"; +const VERSION_PATH = "/api"; +const DEFAULT_LIMIT = 100; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_LIMIT, +}; diff --git a/components/pixiebrix/common/utils.mjs b/components/pixiebrix/common/utils.mjs new file mode 100644 index 0000000000000..d1a7cbf4ac5d0 --- /dev/null +++ b/components/pixiebrix/common/utils.mjs @@ -0,0 +1,33 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +function isEmail(value) { + return /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(value); +} + +export default { + parseArray, + isEmail, +}; diff --git a/components/pixiebrix/package.json b/components/pixiebrix/package.json new file mode 100644 index 0000000000000..24d31049ac29d --- /dev/null +++ b/components/pixiebrix/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/pixiebrix", + "version": "0.1.0", + "description": "Pipedream PixieBrix Components", + "main": "pixiebrix.app.mjs", + "keywords": [ + "pipedream", + "pixiebrix" + ], + "homepage": "https://pipedream.com/apps/pixiebrix", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/pixiebrix/pixiebrix.app.mjs b/components/pixiebrix/pixiebrix.app.mjs new file mode 100644 index 0000000000000..de4ce3b52e74d --- /dev/null +++ b/components/pixiebrix/pixiebrix.app.mjs @@ -0,0 +1,185 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "pixiebrix", + propDefinitions: { + organizationId: { + type: "string", + label: "Organization ID", + description: "The ID of the organization", + async options({ page }) { + const orgs = await this.listOrganizations({ + params: { + page: page + 1, + page_size: constants.DEFAULT_LIMIT, + }, + }); + return orgs?.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + groupId: { + type: "string", + label: "Group ID", + description: "The ID of the group", + async options({ + organizationId, page, + }) { + const groups = await this.listOrganizationGroups({ + organizationId, + params: { + page: page + 1, + page_size: constants.DEFAULT_LIMIT, + }, + }); + return groups?.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + userId: { + type: "string", + label: "User ID", + description: "The ID of the user", + async options({ + organizationId, page, + mapper = ({ + user: { + id: value, name, email, + }, + }) => ({ + label: [ + name, + email, + ].join(" ").trim(), + value, + }), + }) { + const users = await this.listOrganizationMemberships({ + organizationId, + params: { + page: page + 1, + page_size: constants.DEFAULT_LIMIT, + }, + }); + return users?.map(mapper); + }, + }, + membershipId: { + type: "string", + label: "Membership ID", + description: "The ID of the group membership", + async options({ + groupId, page, + }) { + const memberships = await this.listGroupMemberships({ + groupId, + params: { + page: page + 1, + page_size: constants.DEFAULT_LIMIT, + }, + }); + return memberships?.map(({ + id: value, first_name: name, email, + }) => ({ + label: [ + name, + email, + ].join(" ").trim(), + value, + })); + }, + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + "Authorization": `Token ${this.$auth.auth_token}`, + "Accept": "application/json; version=2.0", + "Content-Type": "application/json; version=2.0", + ...headers, + }; + }, + async _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + try { + const response = await axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + return response; + + } catch (error) { + if (error.response?.status === 404) { + return; + } + throw error; + } + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + put(args = {}) { + return this._makeRequest({ + method: "PUT", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + listOrganizations(args = {}) { + return this._makeRequest({ + path: "/organizations/", + ...args, + }); + }, + listOrganizationGroups({ + organizationId, ...args + } = {}) { + return this._makeRequest({ + path: `/organizations/${organizationId}/groups/`, + ...args, + }); + }, + listOrganizationMemberships({ + organizationId, ...args + } = {}) { + return this._makeRequest({ + path: `/organizations/${organizationId}/memberships/`, + ...args, + }); + }, + listGroupMemberships({ + groupId, ...args + } = {}) { + return this._makeRequest({ + headers: { + "Accept": "application/json; version=1.0", + }, + path: `/groups/${groupId}/memberships/`, + ...args, + }); + }, + }, +}; diff --git a/components/pizzly/package.json b/components/pizzly/package.json new file mode 100644 index 0000000000000..bb93b725c8c06 --- /dev/null +++ b/components/pizzly/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/pizzly", + "version": "0.6.0", + "description": "Pipedream pizzly Components", + "main": "pizzly.app.mjs", + "keywords": [ + "pipedream", + "pizzly" + ], + "homepage": "https://pipedream.com/apps/pizzly", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/placekey/README.md b/components/placekey/README.md index c26acf5624b81..5100de1b17fcc 100644 --- a/components/placekey/README.md +++ b/components/placekey/README.md @@ -1,15 +1,11 @@ # Overview - What can you build with Placekey API? - -Placekey API is a powerful, open source tool that allows developers to create powerful applications in the location data space. With this API, you can create applications that can do a variety of geospatial tasks, such as building and querying maps, providing real-time data analysis and visualization, mobile application development, and more. Here are some examples of what you can do with the Placekey API: - -- Build maps for any type of application with comprehensive geospatial capabilities such as geolocation, reverse geocoding, and more -- Query and filter large datasets quickly and easily with Placekey-backed search and filtering capabilities -- Generate real-time insights and analytics within your application with Placekey-enabled visualization and analytic capabilities -- Create powerful mobile applications and in-app location features with Placekey’s comprehensive mobile SDK and integrations -- Create smooth and efficient routing and navigation experiences with Placekey’s easy-to-use routing functionality -- Aggregate, search, and filter data from multiple data sources with Placekey-powered data aggregation and filtering capabilities -- Analyze the relationship between places, activity, and transportation -- Generate accurate and precise distance measures with Placekey-enabled geospatial distance calculation capabilities -- Leverage Placekey-backed advanced analytics to draw valuable insights from your data +The Placekey API is a powerful tool for standardizing and enriching location data. With Placekey, you can translate addresses or points of interest into a unique, universal location identifier, making it easier to integrate and compare data across different databases or platforms. On Pipedream, you can leverage this API to automate tasks that require precise location matching, enrichment, and deduplication, thus enhancing data analysis, business intelligence, marketing campaigns, and logistic operations. + +# Example Use Cases + +- **Real Estate Portfolio Management**: Automate the enrichment of property listings by converting addresses to Placekeys and appending these identifiers to your real estate database on Google Sheets using Pipedream's Google Sheets integration. This allows for seamless integration and analysis of properties across various datasets. + +- **Retail Chain Expansion Analysis**: Use Placekey with Pipedream to match potential new store locations against existing ones in a database like Airtable, cutting the risk of cannibalization by ensuring new locations are strategically placed. Placekey's unique identifiers make it simple to identify and compare catchment areas or neighborhood characteristics. + +- **Event Management Coordination**: Streamline the coordination of multiple event venues by transforming venue addresses into Placekeys and syncing them to a CRM platform like Salesforce. This can help in visualizing event locations on a map for logistics, tracking attendee demographics, and conducting post-event analysis to measure the success of various locations. diff --git a/components/placekey/package.json b/components/placekey/package.json new file mode 100644 index 0000000000000..ef734894841a6 --- /dev/null +++ b/components/placekey/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/placekey", + "version": "0.6.0", + "description": "Pipedream placekey Components", + "main": "placekey.app.mjs", + "keywords": [ + "pipedream", + "placekey" + ], + "homepage": "https://pipedream.com/apps/placekey", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/placetel/README.md b/components/placetel/README.md new file mode 100644 index 0000000000000..95239cfb2b7b5 --- /dev/null +++ b/components/placetel/README.md @@ -0,0 +1,11 @@ +# Overview + +The Placetel API lets you integrate a variety of telephony functions directly into Pipedream workflows. By connecting Placetel to Pipedream, you can automate processes like sending SMS messages, managing calls, or updating contact information programmatically. Pipedream's serverless platform enables these integrations to run on triggers such as incoming webhooks, scheduled times, or actions from other apps, orchestrating complex tasks in a simplified manner. + +# Example Use Cases + +- **Automated Customer Support Ticket Creation**: When a call is missed or a voicemail is received on Placetel, trigger a workflow in Pipedream to create a support ticket in a tool like Zendesk. This ensures that every customer query is logged and assigned to a support agent promptly. + +- **SMS Notification for Calendar Events**: Link Placetel with Google Calendar in Pipedream. When an event is starting soon, automatically send an SMS reminder to the attendees using the Placetel API, ensuring everyone is on time. + +- **Sync Contacts Between CRM and Placetel**: Maintain a consistent contact list by building a workflow that syncs new contacts from a CRM platform like Salesforce to Placetel. Each time a contact is added to Salesforce, add that contact to Placetel, keeping your communication channels up to date. diff --git a/components/placetel/package.json b/components/placetel/package.json index b58ffa0c02a50..561ed666b53de 100644 --- a/components/placetel/package.json +++ b/components/placetel/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/placid/README.md b/components/placid/README.md new file mode 100644 index 0000000000000..4855f8ce4f4d4 --- /dev/null +++ b/components/placid/README.md @@ -0,0 +1,11 @@ +# Overview + +The Placid API lets you automate the creation of visual content such as images and videos. With Pipedream, you can trigger workflows using various events, process data, and use the Placid API to dynamically generate marketing materials, social media graphics, personalized images for email campaigns, and more. Leveraging Pipedream's capabilities, you can integrate Placid with other apps to create robust, multi-step automations without writing extensive code. + +# Example Use Cases + +- **Dynamic Social Media Content Creation**: When a new blog post is published on your CMS (like WordPress), use Pipedream to detect the event, extract the post's title and featured image, and send this data to Placid. Placid generates a customized image for social media, which Pipedream then posts to platforms like Twitter or LinkedIn. + +- **Personalized Email Campaign Images**: Kick off a workflow whenever a user signs up for your service. Collect user-specific data and feed it into the Placid API to create a unique image, such as a welcome message or a membership card. Pipedream can then send the personalized image via an email service like SendGrid to the new user. + +- **E-commerce Product Updates**: Monitor changes to your e-commerce platform's product listings using Pipedream. When a product detail is updated or a new product is added, the workflow triggers the Placid API to generate updated product images or promotional banners. These can be automatically distributed across your social channels or used in targeted advertising campaigns. diff --git a/components/plaid/README.md b/components/plaid/README.md index 8f78518c7570f..45c2387ea7a68 100644 --- a/components/plaid/README.md +++ b/components/plaid/README.md @@ -1,18 +1,11 @@ # Overview - What can I do with the Plaid API? +The Plaid API offers a multitude of financial data operations, enabling developers to manage and interact with user bank accounts, transactions, identity info, and more, all within a secure and compliant ecosystem. Integrating Plaid with Pipedream can unlock powerful automations, like syncing transaction data to accounting software, verifying user identities for KYC compliance, and automating financial alerts or reporting. -The Plaid API enables developers to access financial data from a powerful network of over 11,000 banks, credit cards, investment firms, and more. It helps developers build applications and products that can securely connect with financial institutions, while addressing financial crimes and privacy issues. +# Example Use Cases -The Plaid API can be used to: +- **Transaction Sync to Google Sheets**: Automatically pull recent transactions from a Plaid-connected bank account and append them to a Google Sheets document. This is ideal for personal finance tracking or small business accounting, providing a real-time view of expenditures and income. -- Build a financial management web or mobile application that lets users monitor their accounts, get their credit score, and analyze their spending patterns -- Create an ACH payments gateway that streamlines transactions between customers and businesses -- Develop a personal finance application that helps users compare rates, compare deals and save money -- Link up users' financial accounts within financial management platforms, providing a more comprehensive overview -- Connect customer bank accounts for businesses looking to offer a cash back rewards program -- Create a fraud detection and prevention system -- Integrate with banking and credit card processing systems -- Build a budgeting application -- Generate a financial matching service so users can find the right financial products for their needs -- Enable money transfers, allowing users to move money between accounts securely and easily +- **KYC Verification Process**: Use Plaid to verify the identity of a user and then trigger a workflow on Pipedream to update the user's status in your database. This could link Plaid with a CRM like Salesforce or a database like Airtable, automating the Know Your Customer (KYC) process for finance or fintech apps. + +- **Real-Time Fraud Detection Alerts**: Set up a workflow on Pipedream that uses Plaid's API to monitor account balances and transactions. When a transaction that meets certain criteria (e.g., unusually large amount, foreign location) is detected, an alert can be sent via email or SMS through integrations with services like SendGrid or Twilio, helping to quickly identify and respond to potential fraudulent activities. diff --git a/components/plaid/package.json b/components/plaid/package.json new file mode 100644 index 0000000000000..7f32adbc649c1 --- /dev/null +++ b/components/plaid/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/plaid", + "version": "0.6.0", + "description": "Pipedream plaid Components", + "main": "plaid.app.mjs", + "keywords": [ + "pipedream", + "plaid" + ], + "homepage": "https://pipedream.com/apps/plaid", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/plain/README.md b/components/plain/README.md new file mode 100644 index 0000000000000..14082fe2554ce --- /dev/null +++ b/components/plain/README.md @@ -0,0 +1,11 @@ +# Overview + +The Plain API provides functionality for automating customer support tasks, such as managing conversations, customers, and team inboxes. Within Pipedream, you can leverage these features to create custom workflows that respond to events from your app or external services, triggering actions in Plain. By integrating the Plain API with Pipedream, you can enhance customer support efficiency, streamline communication, and keep customer data synchronized across platforms. + +# Example Use Cases + +- **Support Ticket Routing**: Automatically assign incoming support messages from different channels to the appropriate team inbox based on the content or customer data. This can help ensure that tickets are handled by the right team member quickly. + +- **Customer Feedback Collection**: After resolving support tickets, trigger a feedback request workflow. Connect Plain with a survey tool to send personalized surveys, and then store responses back in Plain to keep all customer interaction history in one place. + +- **Real-time Customer Support Notifications**: Create a workflow that listens to new messages or updates in Plain, and then sends real-time notifications to Slack or another chat app. This ensures that your support team is instantly informed about customer issues and can react promptly. diff --git a/components/planhat/package.json b/components/planhat/package.json new file mode 100644 index 0000000000000..3cc6e2059c992 --- /dev/null +++ b/components/planhat/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/planhat", + "version": "0.0.1", + "description": "Pipedream Planhat Components", + "main": "planhat.app.mjs", + "keywords": [ + "pipedream", + "planhat" + ], + "homepage": "https://pipedream.com/apps/planhat", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/planhat/planhat.app.mjs b/components/planhat/planhat.app.mjs new file mode 100644 index 0000000000000..801bda0325eca --- /dev/null +++ b/components/planhat/planhat.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "planhat", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/planly/package.json b/components/planly/package.json new file mode 100644 index 0000000000000..a87ba342a3737 --- /dev/null +++ b/components/planly/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/planly", + "version": "0.0.1", + "description": "Pipedream Planly Components", + "main": "planly.app.mjs", + "keywords": [ + "pipedream", + "planly" + ], + "homepage": "https://pipedream.com/apps/planly", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/planly/planly.app.mjs b/components/planly/planly.app.mjs new file mode 100644 index 0000000000000..7b8e5d58299b3 --- /dev/null +++ b/components/planly/planly.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "planly", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/planning_center/README.md b/components/planning_center/README.md index 4e7db5ebeddd4..276772b7b68ec 100644 --- a/components/planning_center/README.md +++ b/components/planning_center/README.md @@ -1,17 +1,11 @@ # Overview -What You Can Build with the Planning Center API +The Planning Center API offers a suite of church management features, allowing for the seamless coordination of events, services, and resources. With Pipedream, you can automate tasks, sync data across different platforms, and create dynamic workflows that save time and reduce errors. This can include managing attendees, coordinating service schedules, tracking donations, and more. Pipedream's serverless platform connects with the Planning Center API to create workflows that trigger on new data, process and transform data, and automate communication and organizational tasks. -The Planning Center API provides an expansive suite of tools that developers can use to build powerful, automated applications and integrations to serve their organization's needs. From coordinating staff schedules to interfacing with web-based tools and services, the Planning Center API can help organizations create more time and reach more people with their mission. +# Example Use Cases -Here are some examples of what you can use the Planning Center API to create: +- **Automated Attendee Follow-up**: Trigger a workflow when a new attendee checks in at a service via Planning Center. Use Pipedream to send a personalized email or SMS via SendGrid or Twilio, thanking them for attending and providing next steps or resources. -- Automated team scheduling and coordination -- Follow-up automation for newly added contact info -- Automated task reminders and notifications -- Push notifications for upcoming events -- Integration with customer relationship management (CRM) services -- Automated list segmentation for marketing campaigns -- Automated donation tracking and secure payment processing -- Dashboards to visualize key church-wide metrics -- Sending automated emails based on updated stats and insights +- **Resource Management and Scheduling**: Sync Planning Center resource bookings with Google Calendar. When a new resource booking is added in Planning Center, a workflow can automatically create a corresponding event in a designated Google Calendar, ensuring that all team members stay informed about resource availability and event times. + +- **Donation Tracking & Accounting Integration**: When new donations are recorded in Planning Center, trigger a workflow that logs this information into an accounting app like QuickBooks or sends a thank you message to the donor. This not only saves manual data entry but also helps in maintaining accurate financial records and nurturing donor relationships. diff --git a/components/planso_forms/README.md b/components/planso_forms/README.md new file mode 100644 index 0000000000000..a3c16fa99538c --- /dev/null +++ b/components/planso_forms/README.md @@ -0,0 +1,11 @@ +# Overview + +The PlanSo Forms API allows for the automation of form-related tasks and the seamless integration of form data into various applications or workflows. On Pipedream, this translates to crafting serverless workflows that trigger upon form submissions, manipulating and routing the data to other services for analysis, storage, or further action. With this API, you can trigger events, process form inputs, and integrate with countless other apps in the Pipedream ecosystem to streamline processes and enhance data management. + +# Example Use Cases + +- **Auto-Respond to Form Submissions**: When a new form submission is detected by PlanSo Forms, use Pipedream to send an automated email response to the submitter. Integrate with email services like SendGrid or Gmail to personalize and deliver the message promptly. + +- **Sync Form Data with a CRM**: Integrate PlanSo Forms with a CRM service like Salesforce or HubSpot. When a form is submitted, Pipedream can parse the data and create or update contact records in the CRM, ensuring your sales or customer service teams have the latest information. + +- **Analyze and Visualize Form Data**: On form submission, collect the data and feed it into a Google Sheets spreadsheet. Use this as a basis for creating dashboards or reports in data visualization tools like Google Data Studio, helping you gain insights into your form data trends over time. diff --git a/components/planview_leankit/README.md b/components/planview_leankit/README.md index a0102f301a9b0..43c66e5cf3b1a 100644 --- a/components/planview_leankit/README.md +++ b/components/planview_leankit/README.md @@ -1,17 +1,11 @@ # Overview -What is Planview Leankit? +Planview LeanKit API empowers users to automate and integrate their Kanban workflow management. With this API, you can programmatically access boards, cards, and related data to sync with other systems, trigger actions based on board updates, and extract analytics for reporting purposes. -Planview Leankit is an agile project management software solution that helps organizations make agile processes simpler. It provides a comprehensive suite of features and tools that allow users to easily visualize and create tasks, track progress, and get insights into project health and performance. The associated API enables developers to integrate Planview Leankit into their own applications. +# Example Use Cases -The Planview Leankit API allows developers to access and manipulate data stored within the Leankit application. This includes tasks, topics, users, projects, and custom fields. The API also supports searching and sorting, pushing notifications, and batch requests. +- **Automated Task Syncing with External Project Management Tools:** Create workflows that sync Planview LeanKit tasks with other project management apps like Asana or Trello. Whenever a card is updated in LeanKit, the corresponding task in the other tool is automatically updated to reflect the change. -With the Planview Leankit API, developers can easily create powerful applications that offer great functionality and are tailored to the requirements of the user. Here are some examples of what you can build using the API: +- **Enhanced Reporting with Data Analysis Tools:** Set up a pipeline to continuously export card data to a data warehouse like Google BigQuery, enabling advanced analytics and custom dashboard creation in BI tools such as Tableau or Google Data Studio. -- Robust project management systems and dashboards. -- Automated workflows for task assignment and progress tracking. -- Automated recurring reports sent directly to executives. -- Real-time task updates and notifications sent to users. -- Custom tools for categorizing, filtering and analyzing project data. -- Automated project status and performance tracking. -- Custom integrations with existing systems and software. +- **Real-time Notifications of Board Updates:** Implement a system that listens for changes in LeanKit boards and sends real-time notifications to team communication platforms like Slack or Microsoft Teams, keeping everyone informed about the latest project developments. diff --git a/components/planyo_online_booking/README.md b/components/planyo_online_booking/README.md index fa62b0e7d7948..c0ad367db984f 100644 --- a/components/planyo_online_booking/README.md +++ b/components/planyo_online_booking/README.md @@ -1,20 +1,11 @@ # Overview -Using Planyo Online Booking API, you can create custom booking systems for any type of business or service. The API provides all the tools you need to build a powerful, modern booking system tailored to your specific needs. +The Planyo Online Booking API facilitates interaction with a versatile reservation system, allowing for booking management and integration with external calendars, payment systems, and customer relationship tools. With Pipedream's serverless platform, you can wield this API to construct dynamic, event-driven workflows. Automate notifications, synchronize bookings with other services, or generate detailed reports on reservation trends. Streamline scheduling tasks, minimize manual data entry, and enhance the customer booking experience with customized automations. -Planyo's API tools make it easy for developers to implement a wide variety of features. You can quickly add booking forms and interfaces, as well as offer advanced payment and customer management tools. You can also create custom dynamic reservations or cart-based checkout experiences which are reliable and secure. +# Example Use Cases -Here are some examples of what you can do with Planyo's API: +- **Automated Reservation Confirmation Emails**: Trigger a workflow in Pipedream when a new booking is made via Planyo. Automatically send a personalized confirmation email to the customer using SendGrid, including details like date, time, and any necessary preparation steps. -- Create interactive booking forms and interfaces -- Offer numerous payment gateways and payment methods -- Create automated customer communication and follow-up emails -- Create a dynamic reservation system with customizable search filters -- Offer dynamic promotions, discounts and coupons -- Create custom cart-based checkout experiences -- Integrate a loyalty program -- Create automated invoicing and payment tracking -- Customize customer profiles and access options -- Utilize dynamic real-time availability -- Generate integrated calendar views -- Integrate with external content systems via REST API or JavaScript calls +- **Synchronization with Google Calendar**: Whenever a booking is modified on Planyo, use Pipedream to catch this event and update an associated Google Calendar event. This ensures that any changes in the booking schedule are reflected across all staff calendars in real-time, preventing double bookings and scheduling conflicts. + +- **Slack Notifications for New Bookings**: Set up a Pipedream workflow that listens for new reservations on Planyo and sends a message to a designated Slack channel. This keeps your team instantly informed about new bookings, allowing for swift planning and resource allocation. diff --git a/components/plasmic/README.md b/components/plasmic/README.md new file mode 100644 index 0000000000000..d45bd9eda8922 --- /dev/null +++ b/components/plasmic/README.md @@ -0,0 +1,11 @@ +# Overview + +The Plasmic API enables you to tap into the powerful visual design features of Plasmic within Pipedream workflows. With it, you can automate the fetching, updating, and publishing of Plasmic projects and their components. This opens up possibilities for dynamic content management, design collaboration automation, and streamlined deployment processes. You can use the Plasmic API to integrate with various services, trigger updates across platforms, or synchronize design changes in real-time. + +# Example Use Cases + +- **Content Update Notifications**: Automate the process of notifying team members when a Plasmic project is updated. When a design change is pushed, use the Plasmic API to trigger a Pipedream workflow that sends a message with the update details to a Slack channel, keeping everyone in sync. + +- **Automated Deployment on Design Publish**: Set up a workflow where, upon publishing a design in Plasmic, Pipedream triggers a deployment process through a CI/CD platform like GitHub Actions. This ensures that your live site or application always reflects the latest design changes without manual intervention. + +- **Sync Design Assets with a CMS**: Whenever new assets are added to a Plasmic project, use a Pipedream workflow to automatically upload these assets to a Content Management System (CMS) like Contentful. This keeps your CMS media library up-to-date with the latest design assets, ready for content creation. diff --git a/components/platerecognizer/README.md b/components/platerecognizer/README.md new file mode 100644 index 0000000000000..1d02ff6d86325 --- /dev/null +++ b/components/platerecognizer/README.md @@ -0,0 +1,11 @@ +# Overview + +The Plate Recognizer API provides robust tools for converting images of vehicle license plates into text data. Using Pipedream, you can harness this capability to automate various tasks involving vehicle identification and monitoring. This integration is particularly useful in scenarios involving security, parking management, and logistics optimization, where automated plate recognition can streamline operations significantly. + +# Example Use Cases + +- **Automated Parking Access**: Set up a workflow where a camera at a parking entrance snaps pictures of incoming vehicles. Use the Plate Recognizer API to decode the license plate and check against a database of authorized vehicles (hosted on a service like Airtable). If the vehicle is authorized, trigger an IoT device to open the gate. + +- **Security Alert System**: Create a workflow that monitors a feed from security cameras. Use the Plate Recognizer API to identify license plates of vehicles entering a restricted area. If a plate matches a list of flagged vehicles (maintained in a Google Sheets file), send an alert message via Slack or email to security personnel. + +- **Logistics Tracking**: Develop a system to track the arrival and departure of trucks at a warehouse. Capture license plate images, decode them through the Plate Recognizer API, and log the timestamps in a database. Use this data to analyze logistics efficiency and notify managers via SMS (using Twilio) if there are notable delays or early arrivals. diff --git a/components/platerecognizer/actions/run-recognition/run-recognition.mjs b/components/platerecognizer/actions/run-recognition/run-recognition.mjs new file mode 100644 index 0000000000000..e424d3541f8bf --- /dev/null +++ b/components/platerecognizer/actions/run-recognition/run-recognition.mjs @@ -0,0 +1,67 @@ +import fs from "fs"; +import { checkTmp } from "../../common/utils.mjs"; +import platerecognizer from "../../platerecognizer.app.mjs"; + +export default { + key: "platerecognizer-run-recognition", + name: "Run Recognition", + description: "Triggers a recognition process using the Plate Recognizer SDK.", + version: "0.0.1", + type: "action", + props: { + platerecognizer, + imageFileOrUrl: { + type: "string", + label: "Image File or URL", + description: "The image file or URL to be recognized.", + }, + regions: { + type: "string[]", + label: "Regions", + description: "Regions to select specific license plate patterns. [See further details here](https://guides.platerecognizer.com/docs/other/country-codes/#country-codes)", + optional: true, + }, + cameraId: { + type: "string", + label: "Camera ID", + description: "The ID of the camera that took the image.", + optional: true, + }, + mmc: { + type: "boolean", + label: "MMC", + description: "Whether to detect vehicle make, model, and color.", + optional: true, + }, + config: { + type: "object", + label: "Config", + description: "Additional configuration. [See further details here](https://guides.platerecognizer.com/docs/snapshot/api-reference/#engine-configuration)", + optional: true, + }, + }, + async run({ $ }) { + const fileObj = {}; + + if (this.imageFileOrUrl.startsWith("http")) { + fileObj.upload_url = this.imageFileOrUrl; + } else { + const file = fs.readFileSync(checkTmp(this.imageFileOrUrl)); + fileObj.upload = Buffer(file).toString("base64"); + } + + const response = await this.platerecognizer.runRecognition({ + $, + data: { + ...fileObj, + regions: this.regions, + camera_id: this.cameraId, + mmc: this.mmc, + config: this.config, + }, + }); + + $.export("$summary", "Recognition process triggered successfully"); + return response; + }, +}; diff --git a/components/platerecognizer/common/utils.mjs b/components/platerecognizer/common/utils.mjs new file mode 100644 index 0000000000000..1a5e36f32a603 --- /dev/null +++ b/components/platerecognizer/common/utils.mjs @@ -0,0 +1,6 @@ +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; diff --git a/components/platerecognizer/package.json b/components/platerecognizer/package.json new file mode 100644 index 0000000000000..763385296dc5f --- /dev/null +++ b/components/platerecognizer/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/platerecognizer", + "version": "0.1.0", + "description": "Pipedream Plate Recognizer Components", + "main": "platerecognizer.app.mjs", + "keywords": [ + "pipedream", + "platerecognizer" + ], + "homepage": "https://pipedream.com/apps/platerecognizer", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} + diff --git a/components/platerecognizer/platerecognizer.app.mjs b/components/platerecognizer/platerecognizer.app.mjs new file mode 100644 index 0000000000000..5c83ad58b5f5e --- /dev/null +++ b/components/platerecognizer/platerecognizer.app.mjs @@ -0,0 +1,33 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "platerecognizer", + methods: { + _baseUrl() { + return "https://api.platerecognizer.com/v1"; + }, + _headers(headers) { + return { + "Authorization": `Token ${this.$auth.api_token}`, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers = {}, ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._headers(headers), + ...opts, + }); + }, + runRecognition(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/plate-reader/", + ...opts, + }); + }, + }, +}; diff --git a/components/platform_ly/README.md b/components/platform_ly/README.md new file mode 100644 index 0000000000000..b8ead7e7cd8b2 --- /dev/null +++ b/components/platform_ly/README.md @@ -0,0 +1,11 @@ +# Overview + +The Platform.ly API allows for deep integration with your marketing automation, CRM, and business intelligence data. Within Pipedream, you can leverage this API to automate workflows, sync data with other services, trigger actions based on customer interactions, and analyze business metrics. The seamless connection with Pipedream opens up possibilities for enriching customer profiles, managing campaigns, and driving analytics-driven decisions. + +# Example Use Cases + +- **Automated Contact Tagging Based on Activity**: When a user completes an action on your website, like downloading a white paper, you can use the Platform.ly API to tag that contact automatically. Connect this action to a webhook in Pipedream to listen for the event and then call the Platform.ly API to update the contact's tags. + +- **Sync Contacts with a Third-Party Service**: If you're using a third-party service like Mailchimp in tandem with Platform.ly, you can sync contacts between the two. Set up a workflow in Pipedream that triggers periodically, fetches contacts from Platform.ly using their API, and updates or adds those contacts to a Mailchimp audience. + +- **Lead Scoring Based on Engagement**: Create a Pipedream workflow that scores leads based on their engagement with your emails or website. Use the Platform.ly API to fetch interaction data and apply your scoring logic. Based on the score, update the lead's status in Platform.ly, or trigger further actions like sending a personalized email or notification to your sales team. diff --git a/components/plecto/README.md b/components/plecto/README.md new file mode 100644 index 0000000000000..4dbf3dd10fe3d --- /dev/null +++ b/components/plecto/README.md @@ -0,0 +1,11 @@ +# Overview + +The Plecto API allows you to automate the process of data visualization and performance tracking. With Pipedream, you can harness this API to create real-time dashboards, generate reports, and update KPIs automatically. Pipedream's serverless platform makes it simple to connect Plecto with other services, trigger events, process data, and more. Whether you're syncing sales data, updating leaderboards, or notifying teams of their performance, Pipedream offers a robust and flexible way to streamline these workflows. + +# Example Use Cases + +- **Sales Leaderboard Automation**: Integrate Plecto with a CRM like Salesforce on Pipedream. Whenever a new sale is logged in Salesforce, trigger an event that updates a Plecto leaderboard in real time. This keeps sales teams informed and motivated with the latest performance metrics. + +- **Performance Alerts**: Use Pipedream to monitor performance thresholds in Plecto. Set up an automation that sends a notification via Slack or email when a team member hits or misses a key performance indicator (KPI). Keep the team engaged and responsive by providing instant recognition or support. + +- **Data Sync and Visualization**: Connect Plecto to a database like MySQL using Pipedream. Automatically sync new and updated data to Plecto, allowing for up-to-the-minute visualizations in your dashboards. This ensures that decision-makers always have access to the latest data insights. diff --git a/components/plisio/README.md b/components/plisio/README.md new file mode 100644 index 0000000000000..bc3d6e6922fc1 --- /dev/null +++ b/components/plisio/README.md @@ -0,0 +1,11 @@ +# Overview + +The Plisio API enables automated cryptocurrency payment processing, offering capabilities for creating and managing invoices, tracking transactions, and handling crypto payments. With Pipedream, you can leverage the Plisio API to create serverless workflows that integrate with numerous other apps and services. This empowers developers to automate payment flows, synchronize transaction data across platforms, and trigger actions based on payment statuses, without writing complex server-side code. + +# Example Use Cases + +- **Automated Invoice Creation and Email Delivery**: When a new order is placed on your e-commerce platform, use Pipedream to automatically create a cryptocurrency invoice via the Plisio API and send it to the customer's email using a service like SendGrid or Mailgun. + +- **Transaction Status Monitoring and Database Update**: Monitor your Plisio wallet transactions. When a payment status changes to confirmed, trigger a Pipedream workflow that updates the order status in a connected database like Airtable or Google Sheets, keeping your records synchronized. + +- **Real-time Notifications on Payment Status**: Set up a Pipedream workflow to listen for webhook events from Plisio. When a transaction is completed, send real-time notifications to your Slack channel or Discord server to keep your team informed about successful payments. diff --git a/components/plivo/README.md b/components/plivo/README.md index 2203f20708041..eb6203e4c364e 100644 --- a/components/plivo/README.md +++ b/components/plivo/README.md @@ -1,15 +1,11 @@ # Overview -The Plivo API provides a powerful platform for developers to create powerful and innovative telecommunications applications. With the API, developers can build anything from a simple IVR to voice bots and SIP Trunks. Whether you want to create a cloud-based VoIP solution or a custom PBX, the Plivo API makes it possible. - -Some of the top applications that you can build with the Plivo API are: - -- IVR (Interative Voice Response): Create a voice interface for customers to access information on demand. -- Voice Bots: Build AI-powered voice bots to interact with users on automated calls. -- SIP Trunks: Create virtual telephone lines to connect with existing PBX systems. -- Cloud Telephony Platform: Build a cloud-based VoIP solution for use with web and mobile apps. -- Event-Driven Telephony: Create powerful apps that respond to events on the call. -- Multi-channel Messaging: Send and receive text messages, voice broadcasts, and other multimedia messages. -- Call Analytics and Recording: Monitor the performance of your calls and record conversations for compliant purposes. -- Call Flow Builder: Design custom call flows using an easy-to-use drag-and-drop interface. -- Number Management: Manage multiple phone numbers and toll-free numbers across a range of countries. +The Plivo API taps into the power of cloud communication, allowing you to programmatically send SMS messages, make voice calls, and manage user verifications among other telephony functions. With Pipedream's serverless platform, you can easily integrate Plivo into workflows that automate these communication tasks, react to incoming messages or calls, and connect with numerous other apps for comprehensive automation solutions. + +# Example Use Cases + +- **Customer Support Ticket Creation via SMS**: When a customer sends a support query via SMS to your Plivo number, a Pipedream workflow can intercept that message, extract the content, and automatically create a support ticket in a service like Zendesk. This streamlines the support process and ensures timely responses. + +- **Appointment Reminder Calls**: Use Plivo to automate voice call reminders for appointments. A Pipedream workflow can schedule calls based on your calendar events in Google Calendar, extract the necessary details, and trigger Plivo to call clients with personalized reminders, reducing no-shows and improving client relations. + +- **Two-Factor Authentication (2FA) with One-Time Passwords (OTP)**: Enhance security by implementing 2FA using SMS OTPs sent through Plivo. A workflow on Pipedream could listen for authentication requests from your app, generate a secure OTP, and leverage Plivo to send this password to the user's phone. The workflow can then verify the submitted OTP against the generated one, providing an extra layer of security to user logins. diff --git a/components/pobuca_connect/README.md b/components/pobuca_connect/README.md new file mode 100644 index 0000000000000..c095a8d3254a3 --- /dev/null +++ b/components/pobuca_connect/README.md @@ -0,0 +1,11 @@ +# Overview + +Pobuca Connect API offers a suite of functionalities to manage contacts within an organization, streamline communication, and enhance collaboration. Integrating this API with Pipedream enables you to automate tasks involving contact management, such as syncing new contacts to other platforms, triggering notifications upon contact updates, and creating custom workflows to maintain your organization's contact database efficiently. Harness the API's capabilities to build robust, serverless workflows on Pipedream that trigger on events or on a schedule, and interact with an array of other apps and services. + +# Example Use Cases + +- **Sync New Contacts to Google Sheets**: Whenever a new contact is added in Pobuca Connect, automatically add their details to a Google Sheets spreadsheet. This workflow helps maintain an up-to-date backup of contacts and allows for easy sharing and analysis within teams. + +- **Slack Notifications for Contact Updates**: Set up a Pipedream workflow that listens for updates to contacts in Pobuca Connect and sends a notification to a Slack channel. This keeps the team informed about changes in the contact list and ensures that everyone has the latest information. + +- **Automated Email Campaigns with SendGrid**: Create a workflow that triggers an automated email campaign using SendGrid whenever a specific tag is added to a contact in Pobuca Connect. This enables personalized marketing efforts and streamlined communication with clients or stakeholders. diff --git a/components/pocket/README.md b/components/pocket/README.md index 529d6f014df65..3a42738dc00df 100644 --- a/components/pocket/README.md +++ b/components/pocket/README.md @@ -1,15 +1,11 @@ # Overview -The Pocket API provides an incredible opportunity to develop custom integrations and build powerful applications. With the Pocket API, you can access Pocket articles and data, allowing you to build a wide variety of applications and integrations that leverage the power of personal content curation and organization. - -Whether you're a developer, designer, or media creator, the Pocket API makes it easy to create powerful applications and content experiences. Here are a few examples of what you can build using the Pocket API: - -- Innovative content feed readers -- Personalizable content recommendation engines -- Automated content curation tools -- Data-driven content analytics platforms -- Relevant content discovery widgets -- Customizable reading lists -- Smart content tagging tools -- End-user content dashboards -- Custom content scheduling tools +Pocket's API on Pipedream allows for the automation of content curation workflows. Users can add, retrieve, and organize articles, videos, or other content they want to view later. By leveraging the API, you can create systems for tagging and sorting saved items, integrating them with other services for further processing or sharing. This could be useful for content creators, researchers, or anyone needing to manage a large influx of information efficiently. + +# Example Use Cases + +- **Content Digest Email Automation**: Trigger a weekly Pipedream workflow that fetches items saved in Pocket over the past week. Use the retrieved list to generate an HTML email digest through the SendGrid app and send it to a curated list of subscribers, keeping them updated with your latest finds. + +- **Social Media Sharing Scheduler**: Create a workflow where new items saved to Pocket with a specific tag (e.g., 'share') automatically get scheduled for posting on social media platforms like Twitter or LinkedIn. Use the Twitter app within Pipedream to handle the posting, adding custom messages or hashtags for each item. + +- **Research Resource Aggregator**: Set up a workflow that watches for new Pocket items tagged with specific research-related keywords and automatically adds them to a Google Sheets document. This workflow facilitates collaborative research by providing a live, shared resource list that team members can access and update. diff --git a/components/podio/README.md b/components/podio/README.md index d6aeb64f8d255..ecb6248785e3b 100644 --- a/components/podio/README.md +++ b/components/podio/README.md @@ -1,14 +1,11 @@ # Overview -The Podio API (Application Programming Interface) is an open and powerful tool for developers to create sophisticated apps and services that can interact with the data stored inside Podio. Using the Podio API, developers can create a wide variety of customized applications that will allow users to stay connected to their data on the cloud, while allowing collaboration and data sharing. - -Here are a few examples of what you can do with the Podio API: - -- Create custom reports and dashboards -- Automate tasks like data import/export and synchronization -- Create mobile and web apps that can interact with Podio data -- Build integrations with other software, such as CRM systems -- Build custom workflows to manage processes and tasks -- Build applications to track sales, customer feedback and more -- Automate notifications and reminders for important events and deadlines -- Create custom business apps that facilitate collaboration, data sharing and communication +The Podio API opens a world of possibilities for managing tasks, projects, and team collaboration with ease. By harnessing the API through Pipedream, you can automate routine operations, synchronize data across different platforms, and craft custom workflows that facilitate real-time project management and enhance productivity. Whether it's updating leads in a CRM, managing a content calendar, or automating project status reports, the Podio API paired with Pipedream's serverless execution model allows for seamless integration with a vast array of services to streamline your work processes. + +# Example Use Cases + +- **Task Automation upon Project Completion**: Trigger a Pipedream workflow when a project is marked complete in Podio. Automatically notify the team via Slack, archive the project, and generate an invoice using QuickBooks. + +- **Dynamic CRM Update**: Sync contacts between Podio and Salesforce. When a new contact is added or updated in Podio, use Pipedream to mirror those changes in Salesforce, ensuring your sales team always has the latest information. + +- **Content Calendar Management**: Integrate Podio with Google Calendar via Pipedream. Whenever a new content piece is scheduled in Podio, create a corresponding event in Google Calendar, and send a summary email to the marketing team using SendGrid. diff --git a/components/podio_custom_app/README.md b/components/podio_custom_app/README.md index 72e59ed5b0100..b5edb6eea4bb0 100644 --- a/components/podio_custom_app/README.md +++ b/components/podio_custom_app/README.md @@ -1,20 +1,11 @@ # Overview -Podio's Custom App API allows you to build custom tools and applications to meet your specific needs. It provides a comprehensive suite of features and functions to help you customize and automate your workflows across teams, departments, locations, and ecosystems. With the API, you can do the following and more: +Podio's Custom App API unleashes the power to mold the Podio platform to fit your project and workflow needs. It grants the ability to create, modify, and orchestrate data across various Podio apps. By leveraging Pipedream's serverless platform, you can connect Podio with hundreds of other services for automation. This means you can trigger actions in Podio or react to events in Podio with custom logic, transform data, and pass it between your apps in real-time, without the need to manage infrastructure. -- Automate data processes and enforce permission policies -- Create custom forms, reports, and dashboards -- Create custom complex workflows -- Embed Podio data in external websites -- Automate notifications and alerts -- Create workflows across multiple teams and departments +# Example Use Cases -Here are a few examples of what you can build using Podio's Custom App API: +- **Sync Contacts Between Podio and a CRM**: Automatically sync contact details created or updated in a Podio Custom App with a CRM system like Salesforce. Each new contact in Podio could be added to Salesforce, and updates in Podio could trigger updates in Salesforce, keeping all contact information consistent across platforms. -- A custom CRM that you can use to manage customer inquiries, orders, follow-ups, and records. -- A custom order management system to allow your customers to place and track orders. -- An automated task tracker that sends notifications and reminders to keep your team and processes organized. -- A custom budget tracking and reporting system to help manage and monitor spending against budgeted amounts. -- A knowledgebase to store, retrieve, and share documents, knowledge, and resources. -- A ticketing system to view, respond to, and manage customer inquiries. -- A messaging system to communicate with colleagues or to broadcast notices. +- **Project Management Automation**: When a new project is added to a Podio Custom App, trigger the creation of a corresponding Trello board or Asana project. Integrate time-tracking tools like Toggl to automatically log time as tasks progress, or connect with Slack to notify team members of new tasks and updates within Podio. + +- **Lead Qualification and Distribution**: On receiving a new lead in a Podio Custom App, use Pipedream to qualify the lead based on custom logic (e.g., source, industry, size). Then, distribute qualified leads to specific sales team members in tools like HubSpot or via direct email. Additionally, log all interactions in a Podio app for centralized lead management. diff --git a/components/podio_custom_app/package.json b/components/podio_custom_app/package.json new file mode 100644 index 0000000000000..753afa2be659e --- /dev/null +++ b/components/podio_custom_app/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/podio_custom_app", + "version": "0.6.0", + "description": "Pipedream podio_custom_app Components", + "main": "podio_custom_app.app.mjs", + "keywords": [ + "pipedream", + "podio_custom_app" + ], + "homepage": "https://pipedream.com/apps/podio_custom_app", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/pointagram/README.md b/components/pointagram/README.md index 961192e3e7bfe..953b47c0055e0 100644 --- a/components/pointagram/README.md +++ b/components/pointagram/README.md @@ -1,15 +1,11 @@ # Overview -What can you create with the Pointagram API? - -The Pointagram API is a powerful tool for building location-aware apps that can be used for a variety of purposes. With it, you can create custom maps, location-based services, and more. Here are a few examples of what you can do with the Pointagram API: - -- Create maps with colored pins and/or labels to track points-of-interest -- Build an app that can detect nearby locations and send notifications -- Generate real-time updates on events, news, and weather in any area -- Create a geo-fencing feature to draw virtual boundaries around certain areas -- Track records and analytics of store visits, user locations, and more -- Create virtual tours of historical sites and other attractions -- Visualize asset tracking and inventory data -- Gather data on people's movements and preferences -- Develop location-based games and augmented reality experiences +The Pointagram API offers a suite of functions for gamifying performance tracking. It's a tool to motivate teams and individuals by turning targets and achievements into point-based games and competitions. By leveraging this API in Pipedream, you can automate the tracking of activities, update leaderboards, award badges, and integrate Pointagram with other business tools to streamline productivity and engagement. + +# Example Use Cases + +- **Automated Points Allocation**: Set up a workflow where sales or customer service activities, tracked in a CRM like Salesforce, automatically grant points to your team members in Pointagram. Each new record (like a closed sale or resolved support ticket) can trigger a Pipedream workflow that uses the Pointagram API to update the respective member's points. + +- **Progress Tracking and Notifications**: Create a workflow that listens for updated scores and leaderboards in Pointagram. When certain thresholds are reached or someone tops the leaderboard, trigger an automated notification through Slack or email, using Pipedream's integration with messaging apps, to celebrate the achievement and encourage continuous performance. + +- **Reward Redemption Process**: Implement a workflow where Pointagram points can be exchanged for rewards. When a team member requests a reward in Pointagram, use Pipedream to capture this event, verify the points balance via the API, and then process the reward claim, potentially by creating a task in a project management tool like Asana or sending a notification to the admin team. diff --git a/components/pointerpro/README.md b/components/pointerpro/README.md index 58f91c334d139..2b71bf6c9168f 100644 --- a/components/pointerpro/README.md +++ b/components/pointerpro/README.md @@ -1,16 +1,11 @@ # Overview -The Pointerpro API lets you develop interactive, visually stunning tools and applications. With the Pointerpro API, you can harness the power of the web to build apps that move beyond the browser and into the physical world. +The Pointerpro API enables automated interactions with Pointerpro's survey platform, allowing developers to create, manage, and analyze surveys without manual intervention. This can streamline feedback collection and data analysis, making it easier to integrate customer insights into business processes. With Pipedream, you can leverage the Pointerpro API to build custom workflows that trigger actions in other apps, based on survey responses or other events, making your data work for you in real-time. -Here are some examples of what you can create using the Pointerpro API: +# Example Use Cases -- Apps that allow users to explore physical spaces, such as retail stores or warehouses. -- Interactive web applications that allow users to interact with objects in augmented reality. -- Services for creating virtual and blended reality travel experiences. -- Tools for data visualization, such as charts, graphs, and business intelligence. -- Real-time 3D game development platforms. -- Automated, AI-driven digital marketing campaigns. -- Augmented reality apps for decision-making and analytics. -- Cloud-based inventory management systems. -- Interactive guides for learning about new products and services. -- Smart home automation devices. +- **Survey Response to CRM Update**: When a survey is completed, Pipedream can capture the response and update a contact or lead record in a CRM like Salesforce or HubSpot with the new data. This keeps customer profiles up-to-date with their latest feedback and preferences. + +- **Net Promoter Score (NPS) Alerting**: Create a workflow where Pipedream listens for new NPS survey submissions. If a score falls below a certain threshold, trigger an alert in Slack or send an email to the customer success team, prompting immediate follow-up with the dissatisfied customer. + +- **Automated Survey Result Analysis**: Use Pipedream to send completed survey responses to a data analysis tool like Google Sheets or Tableau. Apply formulas or visualizations to identify trends and insights, which can then be shared with stakeholders through automated reports. diff --git a/components/pokeapi/README.md b/components/pokeapi/README.md index 4149826bbf9bd..fb859c0cc3b1e 100644 --- a/components/pokeapi/README.md +++ b/components/pokeapi/README.md @@ -1,18 +1,11 @@ # Overview -PokéAPI is a great resource for any Pokémon enthusiast. With this API, you can access over 800 Pokémon, as well as all of their associated stats, abilities and moves. With these resources, you can build exciting applications, such as: +The PokéAPI API offers a treasure trove of data for Pokémon enthusiasts, providing detailed information on Pokémon species, abilities, moves, and much more. Leveraging this API within Pipedream allows you to craft creative automations that can engage fans, enhance gaming experiences, and organize Pokémon data efficiently. With the serverless platform that Pipedream provides, you can set up event-driven workflows, connect to a myriad of other services, and automate tasks that respond in real-time to changes in the PokéAPI data. -- A Pokédex - Allowing you to look up Pokémon based on type and ability; -- A battle simulator - Where two Pokémon fight each other using their moves and stats; -- A breeding simulator - Where two Pokémon can mate with specified stats; -- An exploration game - Where you explore various areas to find and capture Pokémon. +# Example Use Cases -You can also use PokéAPI to build informative websites that help Pokémon trainers understand their Pokémon better. Here are some examples of what you can build: +- **Discord Bot Integration for Pokémon Trivia**: Integrate PokéAPI with Discord to create an interactive trivia bot. Set up a Pipedream workflow that listens for specific commands in a Discord channel, fetches random Pokémon facts, and posts them to engage users in a fun trivia game. -- A database of Pokémon stats, moves and abilities; -- A matchup database between different types of Pokémon; -- A database of all the items available in the game; -- A chart showing the progress of Pokémon evolutions; -- A timeline of game releases showing which Pokémon were added when. +- **Pokémon Go Event Notifier**: Use PokéAPI to track updates on Pokémon availability or events, and connect with Twilio on Pipedream to send SMS notifications to subscribers when rare Pokémon appear or when special events are about to start in Pokémon Go. -All in all, the possibilities with PokéAPI are endless. With the resources it offers you can create extremely robust applications, or informative websites that cater to a wide range of Pokémon fans. Whether you're a budding developer, or an experienced player, PokéAPI has something for you! +- **Pokémon Data Aggregation and Analysis**: Build a workflow that periodically pulls data from PokéAPI and sends it to Google Sheets. Use this for compiling comprehensive databases, comparing Pokémon stats, and creating visualizations or reports for analysis or sharing with a community. diff --git a/components/pokeapi/package.json b/components/pokeapi/package.json new file mode 100644 index 0000000000000..78de88dbd0a3e --- /dev/null +++ b/components/pokeapi/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/pokeapi", + "version": "0.6.0", + "description": "Pipedream pokeapi Components", + "main": "pokeapi.app.mjs", + "keywords": [ + "pipedream", + "pokeapi" + ], + "homepage": "https://pipedream.com/apps/pokeapi", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/polly/README.md b/components/polly/README.md index 916c096165594..a83fb4872a50d 100644 --- a/components/polly/README.md +++ b/components/polly/README.md @@ -1,16 +1,11 @@ # Overview -Polly AI is an AI-powered natural language processing (NLP) platform that can take any text and turn it into a human-like voice. With Polly, you can easily deploy natural language applications that comprehend and respond to user interactions using natural language processing (NLP). In addition, you can build sophisticated text-to-speech applications that are capable of transforming written words into life-like conversational speech. +The Polly API allows you to automate and personalize the creation of images for marketing campaigns, emails, and web content. It provides the capability to dynamically generate images with custom text, fonts, and other variables at scale. Specifically in Pipedream, you can harness this API to craft on-the-fly marketing assets that are tailored to individual recipients or audience segments, integrate with email services to deliver personalized images within newsletters, or trigger image creation based on specific events or actions taken by your users. -Here are a few examples of applications you can build with the Polly API: +# Example Use Cases -- Voice-activated virtual assistant applications -- Text-to-speech applications -- Speech-driven video and gaming applications -- Text-to-audio conversions -- Speech recognition systems and applications -- Natural language understanding (NLU) solutions and applications -- Automated customer service solutions -- Automated audio transcription solutions -- Voice biometrics and authentication solutions -- Text analytics solutions +- **Personalized Email Campaigns:** Integrate Polly with SendGrid on Pipedream to create and send personalized images within email campaigns. When a new subscriber is added to a mailing list, trigger an event that uses Polly to generate a custom welcome image, then send it via SendGrid to the new subscriber's email address. + +- **Event-Driven Social Media Posts:** Link Polly with Twitter via Pipedream. Set up a workflow where event registrations from Eventbrite trigger image creations with Polly, including event details and personalized attendee names. Then automatically post these images to a Twitter account to engage attendees and promote the event. + +- **E-commerce Product Personalization:** Connect Polly with Shopify on Pipedream to offer personalized product images. When a customer orders a customizable product, use their details to create a unique image preview of the product with their chosen text or design, then email it to the customer for approval before shipment. diff --git a/components/polly/package.json b/components/polly/package.json new file mode 100644 index 0000000000000..6d15adc8b7a47 --- /dev/null +++ b/components/polly/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/polly", + "version": "0.6.0", + "description": "Pipedream polly Components", + "main": "polly.app.mjs", + "keywords": [ + "pipedream", + "polly" + ], + "homepage": "https://pipedream.com/apps/polly", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/polygon/README.md b/components/polygon/README.md new file mode 100644 index 0000000000000..1e090a3a5e464 --- /dev/null +++ b/components/polygon/README.md @@ -0,0 +1,11 @@ +# Overview + +The Polygon API grants access to a wealth of financial data, including real-time and historical stock prices, trade data, and more. Within Pipedream's serverless integration platform, you can tap into Polygon to create powerful workflows. With Pipedream, you can easily connect Polygon to hundreds of other apps, automate financial data processing, send stock alerts, or synchronize market data with databases or spreadsheets, all in real-time. + +# Example Use Cases + +- **Real-Time Stock Dashboard Updates**: Trigger a workflow whenever new tick data is available on Polygon for specific stocks. Pipedream can process this data and update a Google Sheets dashboard, giving you a live view of stock performance. + +- **Automated Trading Alerts**: Set up a Pipedream workflow that listens for significant price changes or trade volumes reported by the Polygon API. When certain conditions are met, Pipedream sends alerts via Slack or email, keeping traders informed for quick decision-making. + +- **Market Analysis and Reporting**: Leverage Pipedream's scheduled workflows to fetch daily market summaries from Polygon and compile a report. This report could be enriched with additional data from other financial services, formatted and then delivered to Dropbox or sent via email to a list of subscribers. diff --git a/components/polygon/actions/get-company-financials/get-company-financials.mjs b/components/polygon/actions/get-company-financials/get-company-financials.mjs new file mode 100644 index 0000000000000..8db7b40ad0e2c --- /dev/null +++ b/components/polygon/actions/get-company-financials/get-company-financials.mjs @@ -0,0 +1,80 @@ +import { + SORT_HISTORICAL_OPTIONS, + SORT_OPTIONS, TIMEFRAME_OPTIONS, +} from "../../common/constants.mjs"; +import polygon from "../../polygon.app.mjs"; + +export default { + key: "polygon-get-company-financials", + name: "Get Company Financials", + description: "Retrieves financial details for a specific company by stock ticker. [See the documentation](https://polygon.io/docs/stocks/get_vx_reference_financials).", + version: "0.0.1", + type: "action", + props: { + polygon, + stockTicker: { + propDefinition: [ + polygon, + "stockTicker", + ], + }, + filingDate: { + type: "string", + label: "Filing Date", + description: "Query by the date when the filing with financials data was filed in **YYYY-MM-DD** format.", + optional: true, + }, + periodOfReportDate: { + type: "string", + label: "Period Of Report Date", + description: "The period of report for the filing with financials data in **YYYY-MM-DD** format.", + optional: true, + }, + timeframe: { + type: "string", + label: "Timeframe", + description: "Query by timeframe. Annual financials originate from 10-K filings, and quarterly financials originate from 10-Q filings. Note: Most companies do not file quarterly reports for Q4 and instead include those financials in their annual report, so some companies my not return quarterly financials for Q4", + options: TIMEFRAME_OPTIONS, + optional: true, + }, + order: { + type: "string", + label: "Order", + description: "Order results based on the `Sort` field.", + options: SORT_OPTIONS, + optional: true, + }, + limit: { + type: "integer", + label: "Limit", + description: "Limit the number of results returned.", + optional: true, + min: 1, + max: 100, + default: 10, + }, + sort: { + type: "string", + label: "Sort", + description: "Sort field used for ordering", + options: SORT_HISTORICAL_OPTIONS, + optional: true, + }, + }, + async run({ $ }) { + const financialDetails = await this.polygon.getFinancialDetails({ + $, + params: { + ticker: this.stockTicker, + filing_date: this.filingDate, + period_of_report_date: this.periodOfReportDate, + timeframe: this.timeframe, + order: this.order, + limit: this.limit, + sort: this.sort, + }, + }); + $.export("$summary", `Successfully retrieved financial details for ${this.stockTicker}`); + return financialDetails; + }, +}; diff --git a/components/polygon/actions/get-historical-prices/get-historical-prices.mjs b/components/polygon/actions/get-historical-prices/get-historical-prices.mjs new file mode 100644 index 0000000000000..a65c1d7cc79cf --- /dev/null +++ b/components/polygon/actions/get-historical-prices/get-historical-prices.mjs @@ -0,0 +1,84 @@ +import { + SORT_OPTIONS, + TIMESPAN_OPTIONS, +} from "../../common/constants.mjs"; +import polygon from "../../polygon.app.mjs"; + +export default { + key: "polygon-get-historical-prices", + name: "Get Historical Prices", + description: "Fetches historical price data for a specified stock ticker within a date range. [See the documentation](https://polygon.io/docs/stocks/get_v2_aggs_ticker__stocksticker__range__multiplier___timespan___from___to)", + version: "0.0.1", + type: "action", + props: { + polygon, + stockTicker: { + propDefinition: [ + polygon, + "stockTicker", + ], + }, + multiplier: { + type: "integer", + label: "Multiplier", + description: "The size of the timespan multiplier.", + default: 1, + }, + timespan: { + type: "string", + label: "Timespan", + description: "The size of the time window.", + options: TIMESPAN_OPTIONS, + }, + from: { + type: "string", + label: "From Date", + description: "The start of the aggregate time window. Either a date with the format **YYYY-MM-DD** or a millisecond timestamp.", + }, + to: { + type: "string", + label: "To Date", + description: "The end of the aggregate time window. Either a date with the format **YYYY-MM-DD** or a millisecond timestamp.", + }, + adjusted: { + propDefinition: [ + polygon, + "adjusted", + ], + optional: true, + }, + sort: { + type: "string", + label: "Sort", + description: "Sort the results by timestamp. asc will return results in ascending order (oldest at the top), desc will return results in descending order (newest at the top).", + options: SORT_OPTIONS, + optional: true, + }, + limit: { + type: "integer", + label: "Limit", + description: "Limits the number of base aggregates queried to create the aggregate results. Max 50000 and Default 5000. Read more about how limit is used to calculate aggregate results in our article on [Aggregate Data API Improvements](https://polygon.io/blog/aggs-api-updates).", + optional: true, + min: 1, + max: 50000, + default: 5000, + }, + }, + async run({ $ }) { + const response = await this.polygon.getHistoricalPriceData({ + $, + stockTicker: this.stockTicker, + multiplier: this.multiplier, + timespan: this.timespan, + from: this.from, + to: this.to, + params: { + adjusted: this.adjusted, + sort: this.sort, + limit: this.limit, + }, + }); + $.export("$summary", `Fetched historical prices for ${this.stockTicker} from ${this.from} to ${this.to}.`); + return response; + }, +}; diff --git a/components/polygon/actions/get-stock-price/get-stock-price.mjs b/components/polygon/actions/get-stock-price/get-stock-price.mjs new file mode 100644 index 0000000000000..f4c92f5df8439 --- /dev/null +++ b/components/polygon/actions/get-stock-price/get-stock-price.mjs @@ -0,0 +1,51 @@ +import polygon from "../../polygon.app.mjs"; + +export default { + key: "polygon-get-stock-price", + name: "Get Stock Price", + description: "Get the open, close and afterhours prices of a stock symbol on a certain date. [See the documentation](https://polygon.io/docs/stocks/get_v1_open-close__stocksticker___date)", + version: "0.0.1", + type: "action", + props: { + polygon, + stockTicker: { + propDefinition: [ + polygon, + "stockTicker", + ], + }, + date: { + type: "string", + label: "Date", + description: "The date of the requested open/close in the format YYYY-MM-DD.", + }, + adjusted: { + propDefinition: [ + polygon, + "adjusted", + ], + optional: true, + }, + }, + async run({ $ }) { + try { + + const response = await this.polygon.getCurrentPrice({ + $, + date: this.date, + stockTicker: this.stockTicker, + params: { + adjusted: this.adjusted, + }, + }); + + $.export("$summary", `Successfully fetched the price of ${this.stockTicker}`); + return response; + } catch ({ response }) { + if (response.status === 404) { + $.export("$summary", `No data found for ${this.stockTicker}`); + return {}; + } + } + }, +}; diff --git a/components/polygon/common/constants.mjs b/components/polygon/common/constants.mjs new file mode 100644 index 0000000000000..552ef3b23e526 --- /dev/null +++ b/components/polygon/common/constants.mjs @@ -0,0 +1,26 @@ +export const TIMESPAN_OPTIONS = [ + "second", + "minute", + "hour", + "day", + "week", + "month", + "quarter", + "year", +]; + +export const TIMEFRAME_OPTIONS = [ + "annual", + "quarterly", + "ttm", +]; + +export const SORT_OPTIONS = [ + "asc", + "desc", +]; + +export const SORT_HISTORICAL_OPTIONS = [ + "filing_date", + "period_of_report_date", +]; diff --git a/components/polygon/common/utils.mjs b/components/polygon/common/utils.mjs new file mode 100644 index 0000000000000..3354dda7c3efc --- /dev/null +++ b/components/polygon/common/utils.mjs @@ -0,0 +1,7 @@ +export const parseNextPage = (nextUrl = null) => { + if (nextUrl) { + const url = new URL(nextUrl); + nextUrl = url.searchParams.get("cursor"); + } + return nextUrl; +}; diff --git a/components/polygon/package.json b/components/polygon/package.json new file mode 100644 index 0000000000000..249c9ac98ce11 --- /dev/null +++ b/components/polygon/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/polygon", + "version": "0.7.0", + "description": "Pipedream polygon Components", + "main": "polygon.app.mjs", + "keywords": [ + "pipedream", + "polygon" + ], + "homepage": "https://pipedream.com/apps/polygon", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/polygon/polygon.app.mjs b/components/polygon/polygon.app.mjs index 4ff0d5c269030..419e4852e9cf9 100644 --- a/components/polygon/polygon.app.mjs +++ b/components/polygon/polygon.app.mjs @@ -1,11 +1,143 @@ +import { axios } from "@pipedream/platform"; +import { parseNextPage } from "./common/utils.mjs"; + export default { type: "app", app: "polygon", - propDefinitions: {}, + propDefinitions: { + stockTicker: { + type: "string", + label: "Stock Ticker", + description: "Specify a case-sensitive ticker symbol. For example, AAPL represents Apple Inc.", + async options({ + page, prevContext, + }) { + const parsedPage = parseNextPage(prevContext.nextPage); + + const { + results, next_url: next, + } = await this.listStockTickers({ + params: { + page, + cursor: parsedPage, + }, + }); + + return { + options: results.map(({ + ticker: value, name, + }) => ({ + label: `${name} (${value})`, + value, + })), + context: { + nextPage: next, + }, + }; + }, + }, + adjusted: { + type: "boolean", + label: "Adjusted", + description: "Whether or not the results are adjusted for splits. By default, results are adjusted. Set this to false to get results that are NOT adjusted for splits.", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.polygon.io"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + getCurrentPrice({ + stockTicker, date, ...opts + }) { + return this._makeRequest({ + path: `/v1/open-close/${stockTicker}/${date}`, + ...opts, + }); + }, + getHistoricalPriceData({ + stockTicker, multiplier, timespan, from, to, ...opts + }) { + return this._makeRequest({ + path: `/v2/aggs/ticker/${stockTicker}/range/${multiplier}/${timespan}/${from}/${to}`, + ...opts, + }); + }, + getPreviousClose({ + stockTicker, ...opts + }) { + return this._makeRequest({ + path: `/v2/aggs/ticker/${stockTicker}/prev`, + ...opts, + }); + }, + listStockTickers(opts = {}) { + return this._makeRequest({ + path: "/v3/reference/tickers", + ...opts, + }); + }, + getFinancialDetails(opts = {}) { + return this._makeRequest({ + path: "/vX/reference/financials", + ...opts, + }); + }, + getNewsArticles(opts = {}) { + return this._makeRequest({ + path: "/v2/reference/news", + ...opts, + }); + }, + getTradeEvents({ + stockTicker, ...opts + }) { + return this._makeRequest({ + path: `/v3/trades/${stockTicker}`, + ...opts, + }); + }, + async *paginate({ + fn, params = {}, parseDataFn, maxResults = null, ...opts + }) { + let next; + let count = 0; + + do { + params.cursor = next; + const data = await fn({ + params, + ...opts, + }); + + const { + parsedData, nextPage, + } = parseDataFn(data); + + for (const d of parsedData) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + next = nextPage; + + } while (next); }, }, }; diff --git a/components/polygon/sources/common/base.mjs b/components/polygon/sources/common/base.mjs new file mode 100644 index 0000000000000..fee7bc2df3d7e --- /dev/null +++ b/components/polygon/sources/common/base.mjs @@ -0,0 +1,66 @@ +import { + ConfigurationError, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../polygon.app.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + try { + const lastDate = this._getLastDate(); + + const response = this.app.paginate({ + fn: this.getFunction(), + parseDataFn: this.parseData, + ...this.getData(lastDate), + maxResults, + }); + + let responseArray = []; + for await (const item of response) { + if (Date.parse(item.date) <= Date.parse(lastDate)) break; + responseArray.push(item); + } + + if (responseArray.length) { + this._setLastDate(responseArray[0].published_utc); + } + + for (const item of responseArray.reverse()) { + const ts = Date.parse(item.published_utc || item.sip_timestamp || new Date()); + this.$emit(item, { + id: item.id || ts, + summary: this.getSummary(item), + ts, + }); + } + } catch ({ response }) { + throw new ConfigurationError(response.data.message); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/polygon/sources/new-market-news/new-market-news.mjs b/components/polygon/sources/new-market-news/new-market-news.mjs new file mode 100644 index 0000000000000..5516761bc3532 --- /dev/null +++ b/components/polygon/sources/new-market-news/new-market-news.mjs @@ -0,0 +1,43 @@ +import { parseNextPage } from "../../common/utils.mjs"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "polygon-new-market-news", + name: "New Market News", + description: "Emit new events when a news article related to the stock market is published.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.app.getNewsArticles; + }, + getSummary(item) { + return `New Market News: ${item.title}`; + }, + getData(lastDate) { + return { + params: { + "limit": 1000, + "sort": "published_utc", + "order": "desc", + "published_utc.gt": lastDate, + }, + }; + }, + parseData({ + results, next_url: next, + }) { + const parsedPage = parseNextPage(next); + + return { + parsedData: results, + nextPage: parsedPage, + }; + }, + }, + sampleEmit, +}; diff --git a/components/polygon/sources/new-market-news/test-event.mjs b/components/polygon/sources/new-market-news/test-event.mjs new file mode 100644 index 0000000000000..dc74e73e01e3e --- /dev/null +++ b/components/polygon/sources/new-market-news/test-event.mjs @@ -0,0 +1,31 @@ +export default { + "amp_url": "https://m.uk.investing.com/news/stock-market-news/markets-are-underestimating-fed-cuts-ubs-3559968?ampMode=1", + "article_url": "https://uk.investing.com/news/stock-market-news/markets-are-underestimating-fed-cuts-ubs-3559968", + "author": "Sam Boughedda", + "description": "UBS analysts warn that markets are underestimating the extent of future interest rate cuts by the Federal Reserve, as the weakening economy is likely to justify more cuts than currently anticipated.", + "id": "8ec638777ca03b553ae516761c2a22ba2fdd2f37befae3ab6fdab74e9e5193eb", + "image_url": "https://i-invdn-com.investing.com/news/LYNXNPEC4I0AL_L.jpg", + "insights": [ + { + "sentiment": "positive", + "sentiment_reasoning": "UBS analysts are providing a bullish outlook on the extent of future Federal Reserve rate cuts, suggesting that markets are underestimating the number of cuts that will occur.", + "ticker": "UBS" + } + ], + "keywords": [ + "Federal Reserve", + "interest rates", + "economic data" + ], + "published_utc": "2024-06-24T18:33:53Z", + "publisher": { + "favicon_url": "https://s3.polygon.io/public/assets/news/favicons/investing.ico", + "homepage_url": "https://www.investing.com/", + "logo_url": "https://s3.polygon.io/public/assets/news/logos/investing.png", + "name": "Investing.com" + }, + "tickers": [ + "UBS" + ], + "title": "Markets are underestimating Fed cuts: UBS By Investing.com - Investing.com UK" +} \ No newline at end of file diff --git a/components/polygon/sources/new-stock-price-summary/new-stock-price-summary.mjs b/components/polygon/sources/new-stock-price-summary/new-stock-price-summary.mjs new file mode 100644 index 0000000000000..19444274cece9 --- /dev/null +++ b/components/polygon/sources/new-stock-price-summary/new-stock-price-summary.mjs @@ -0,0 +1,54 @@ +import { parseNextPage } from "../../common/utils.mjs"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "polygon-new-stock-price-summary", + name: "New Stock Price Summary", + description: "Emit new event when the daily price summary (open, high, low, close) for a specified stock ticker is available.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + stockTicker: { + propDefinition: [ + common.props.app, + "stockTicker", + ], + }, + }, + methods: { + ...common.methods, + getFunction() { + return this.app.getTradeEvents; + }, + getSummary() { + return `Daily price summary for ${this.stockTicker}`; + }, + getData(lastDate) { + return { + stockTicker: this.stockTicker, + params: { + "limit": 5000, + "sort": "timestamp", + "order": "desc", + "timestamp.gt": lastDate, + }, + }; + }, + parseData({ + results, next_url: next, + }) { + const parsedPage = parseNextPage(next); + + return { + parsedData: results, + nextPage: parsedPage, + }; + }, + }, + sampleEmit, +}; + diff --git a/components/polygon/sources/new-stock-price-summary/test-event.mjs b/components/polygon/sources/new-stock-price-summary/test-event.mjs new file mode 100644 index 0000000000000..b3ee9e530df63 --- /dev/null +++ b/components/polygon/sources/new-stock-price-summary/test-event.mjs @@ -0,0 +1,10 @@ +export default { + "T": "AAPL", + "c": 115.97, + "h": 117.59, + "l": 114.13, + "o": 115.55, + "t": 1605042000000, + "v": 131704427, + "vw": 116.3058 +} \ No newline at end of file diff --git a/components/polygon/sources/new-stock-trade/new-stock-trade.mjs b/components/polygon/sources/new-stock-trade/new-stock-trade.mjs new file mode 100644 index 0000000000000..35bc8b292c268 --- /dev/null +++ b/components/polygon/sources/new-stock-trade/new-stock-trade.mjs @@ -0,0 +1,50 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "polygon-new-stock-trade", + name: "New Stock Trade", + description: "Emit new event when a trade occurs for a specified stock ticker.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + stockTicker: { + propDefinition: [ + common.props.app, + "stockTicker", + ], + }, + adjusted: { + propDefinition: [ + common.props.app, + "adjusted", + ], + optional: true, + }, + }, + methods: { + ...common.methods, + getFunction() { + return this.app.getPreviousClose; + }, + getSummary() { + return `New trade for ${this.stockTicker}`; + }, + getData() { + return { + stockTicker: this.stockTicker, + adjusted: this.adjusted, + }; + }, + parseData({ results }) { + return { + parsedData: results, + nextPage: null, + }; + }, + }, + sampleEmit, +}; diff --git a/components/polygon/sources/new-stock-trade/test-event.mjs b/components/polygon/sources/new-stock-trade/test-event.mjs new file mode 100644 index 0000000000000..2f4ceb47942e1 --- /dev/null +++ b/components/polygon/sources/new-stock-trade/test-event.mjs @@ -0,0 +1,14 @@ +export default { + "conditions": [ + 12, + 41 + ], + "exchange": 11, + "id": "1", + "participant_timestamp": 1517562000015577000, + "price": 171.55, + "sequence_number": 1063, + "sip_timestamp": 1517562000016036600, + "size": 100, + "tape": 3 +} \ No newline at end of file diff --git a/components/polygon_io/README.md b/components/polygon_io/README.md new file mode 100644 index 0000000000000..03042c17e70a1 --- /dev/null +++ b/components/polygon_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The Polygon.io API provides access to an extensive array of financial data including stocks, forex, and cryptocurrency information. In Pipedream, you can harness this data to create robust, serverless workflows that react to market changes, automate reporting, or integrate with other financial tools. Workflows can range from simple data retrieval to complex trading strategies. + +# Example Use Cases + +- **Real-time Stock Alerting**: Set up a Pipedream workflow that uses the Polygon.io API to monitor stock prices and volumes. When a stock hits certain thresholds, send alerts via email or SMS using integrations like SendGrid or Twilio. + +- **Automated Reporting**: Build a workflow that aggregates daily financial data from Polygon.io and compiles it into a comprehensive report. Use Pipedream's built-in cron scheduler to run this workflow daily, and send the report to Google Sheets or via email. + +- **Trading Strategy Execution**: Create a workflow that uses Polygon.io to fetch real-time market data, evaluates it against your trading criteria using Pipedream's code steps, and places trades via a broker's API like Alpaca. Log all trades and results to a data store for performance analysis. diff --git a/components/polygon_io/package.json b/components/polygon_io/package.json new file mode 100644 index 0000000000000..fbf270384090b --- /dev/null +++ b/components/polygon_io/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/polygon_io", + "version": "0.0.1", + "description": "Pipedream Polygon.io Components", + "main": "polygon_io.app.mjs", + "keywords": [ + "pipedream", + "polygon_io" + ], + "homepage": "https://pipedream.com/apps/polygon_io", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/polygon_io/polygon_io.app.mjs b/components/polygon_io/polygon_io.app.mjs new file mode 100644 index 0000000000000..e31a125c57211 --- /dev/null +++ b/components/polygon_io/polygon_io.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "polygon_io", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/polygonscan/README.md b/components/polygonscan/README.md new file mode 100644 index 0000000000000..ce6550ea92435 --- /dev/null +++ b/components/polygonscan/README.md @@ -0,0 +1,11 @@ +# Overview + +The PolygonScan API provides access to blockchain data from the Polygon network. It allows for querying of blocks, transactions, and wallet addresses, among other data points. With Pipedream, you can integrate this API to automate monitoring, alerting, and data analysis tasks. You can leverage Pipedream's serverless platform to create workflows that react to events on the Polygon network in real-time, without having to manage infrastructure or write complex backend code. + +# Example Use Cases + +- **Automated Transaction Alerts**: Create a workflow that listens for transactions to a specific wallet address on the Polygon network. Use Pipedream's built-in actions to send a notification via email, SMS, or a messaging app like Slack whenever a transaction occurs. + +- **Periodic Portfolio Value Checker**: Schedule a Pipedream workflow to periodically fetch the balance of your Polygon wallet addresses and calculate the current value based on the latest token prices. Connect to a service like CoinGecko for price data and store the results in a Google Sheet for analysis. + +- **Smart Contract Event Logger**: Watch for specific smart contract events on the Polygon network. Each time the event is emitted, the workflow captures the data and logs it to a database like PostgreSQL or sends it to a data visualization tool like Google Data Studio for monitoring and reporting. diff --git a/components/polymer_co/package.json b/components/polymer_co/package.json new file mode 100644 index 0000000000000..e1b5c95e276b5 --- /dev/null +++ b/components/polymer_co/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/polymer_co", + "version": "0.0.1", + "description": "Pipedream Polymer.co Components", + "main": "polymer_co.app.mjs", + "keywords": [ + "pipedream", + "polymer_co" + ], + "homepage": "https://pipedream.com/apps/polymer_co", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/polymer_co/polymer_co.app.mjs b/components/polymer_co/polymer_co.app.mjs new file mode 100644 index 0000000000000..619862a459949 --- /dev/null +++ b/components/polymer_co/polymer_co.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "polymer_co", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/poof/README.md b/components/poof/README.md new file mode 100644 index 0000000000000..88953b97fb062 --- /dev/null +++ b/components/poof/README.md @@ -0,0 +1,11 @@ +# Overview + +The Poof API lets you schedule and automate emails and SMS messages, making it a powerful tool for communication-based workflows. With Pipedream, you can connect the Poof API to hundreds of other services to craft custom automations. Trigger events, process data, and design complex interactions without managing a server infrastructure. + +# Example Use Cases + +- **Automated Customer Follow-Up**: Initiate a Poof API call to send follow-up emails or SMS messages after a customer completes a purchase on your e-commerce platform. This can involve thanking them, requesting feedback, or offering post-purchase support. + +- **Event Reminder System**: Use Pipedream to schedule reminders for events. When a user registers for an event on your website, trigger a workflow that sends out confirmation and reminder messages via Poof API as the event date approaches. + +- **Marketing Campaigns Coordination**: With Poof API on Pipedream, set up a workflow where marketing campaign leads captured through a form submission app, like Typeform, are automatically added to a Poof mailing list for a scheduled promotional campaign. diff --git a/components/poper/package.json b/components/poper/package.json new file mode 100644 index 0000000000000..ef2ebcffcb8e4 --- /dev/null +++ b/components/poper/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/poper", + "version": "0.1.0", + "description": "Pipedream Poper Components", + "main": "poper.app.mjs", + "keywords": [ + "pipedream", + "poper" + ], + "homepage": "https://pipedream.com/apps/poper", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/poper/poper.app.mjs b/components/poper/poper.app.mjs new file mode 100644 index 0000000000000..b6798dc2cf74b --- /dev/null +++ b/components/poper/poper.app.mjs @@ -0,0 +1,57 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "poper", + propDefinitions: { + poperId: { + type: "string", + label: "Poper ID", + description: "The ID of the Poper popup", + async options() { + const { popups } = await this.listPopups(); + return popups.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.poper.ai/general/v1"; + }, + _data(data = {}) { + return { + ...data, + api_key: `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, data, ...opts + }) { + return axios($, { + method: "POST", + url: this._baseUrl() + path, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + data: this._data(data), + ...opts, + }); + }, + listPopups() { + return this._makeRequest({ + path: "/popup/list", + }); + }, + listPoperResponses(opts = {}) { + return this._makeRequest({ + path: "/popup/responses", + ...opts, + }); + }, + }, +}; diff --git a/components/poper/sources/new-lead/new-lead.mjs b/components/poper/sources/new-lead/new-lead.mjs new file mode 100644 index 0000000000000..cd92423cb9d1b --- /dev/null +++ b/components/poper/sources/new-lead/new-lead.mjs @@ -0,0 +1,74 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import poper from "../../poper.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "poper-new-lead", + name: "New Lead from Poper Popup", + description: "Emit new event when a new lead is obtained from Poper popups.", + version: "0.0.1", + type: "source", + props: { + poper, + db: "$.service.db", + timer: { + label: "Polling interval", + description: "Pipedream will poll the Poper API on this schedule", + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + poperId: { + propDefinition: [ + poper, + "poperId", + ], + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + getParams() { + return {}; + }, + async startEvent(maxResults = 0) { + const lastId = this._getLastId(); + const { responses } = await this.poper.listPoperResponses({ + data: { + popup_id: this.poperId, + }, + }); + + const filteredResponse = responses.filter((item) => item.id > lastId); + + if (filteredResponse.length) { + if (maxResults && filteredResponse.length > maxResults) { + filteredResponse.length = maxResults; + } + this._setLastId(filteredResponse[0].id); + } + + for (const item of filteredResponse.reverse()) { + this.$emit( item, { + id: item.id, + summary: `New lead with Id: ${item.id}`, + ts: Date.parse(item.time_stamp), + }); + } + }, + }, + hooks: { + async deploy() { + await this.startEvent(25); + }, + }, + async run() { + await this.startEvent(); + }, + sampleEmit, +}; diff --git a/components/poper/sources/new-lead/test-event.mjs b/components/poper/sources/new-lead/test-event.mjs new file mode 100644 index 0000000000000..4f695596adefd --- /dev/null +++ b/components/poper/sources/new-lead/test-event.mjs @@ -0,0 +1,24 @@ +export default { + "id": 6625, + "popup_id": 1233, + "time_stamp": "2024-07-05T16:22:23.830Z", + "response": { + "email": "email@test.com" + }, + "system": { + "page": "/", + "referrer": "https://site.com.br/wp-admin/admin.php?page=poper-settings", + "language": "pt-BR", + "os": "Windows", + "browser": "Chrome", + "ip": "123.456.78.90", + "city": "São Paulo", + "country": "BR", + "continent": "SA", + "latitude": "-22.123421", + "longitude": "-49.657476", + "postal_code": "05040001", + "region": "São Paulo", + "region_code": "SP" + } +} \ No newline at end of file diff --git a/components/popupsmart/README.md b/components/popupsmart/README.md new file mode 100644 index 0000000000000..cd83698487eb8 --- /dev/null +++ b/components/popupsmart/README.md @@ -0,0 +1,11 @@ +# Overview + +The Popupsmart API allows you to manage and analyze the performance of popups on your website from within Pipedream. By leveraging this API, you can automate popup creation, update settings, fetch analytics, and synchronize these actions with other services to streamline your marketing and lead generation efforts. + +# Example Use Cases + +- **Integrate Popupsmart with CRM systems**: Trigger a workflow on Pipedream when a new lead is captured through a Popupsmart popup. Automatically add this lead to your CRM system, such as Salesforce or HubSpot, to maintain an up-to-date leads database. + +- **Sync Popupsmart data with Google Sheets**: Set up an automated process to extract analytics data from Popupsmart and append it to a Google Sheet. This workflow provides a simple way to create custom reports or share popup performance data with team members who prefer working within spreadsheets. + +- **Automate email follow-ups**: Connect Popupsmart to an email platform like SendGrid or Mailchimp on Pipedream. Whenever a user signs up via a popup, automatically send a personalized welcome email or a drip sequence to nurture these potential customers. diff --git a/components/popupsmart/package.json b/components/popupsmart/package.json index c27fa77ccb176..fcfd4f61bea3f 100644 --- a/components/popupsmart/package.json +++ b/components/popupsmart/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/portabilling/package.json b/components/portabilling/package.json new file mode 100644 index 0000000000000..f77ad1662f7ed --- /dev/null +++ b/components/portabilling/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/portabilling", + "version": "0.0.1", + "description": "Pipedream PortaBilling Components", + "main": "portabilling.app.mjs", + "keywords": [ + "pipedream", + "portabilling" + ], + "homepage": "https://pipedream.com/apps/portabilling", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/components/portabilling/portabilling.app.mjs b/components/portabilling/portabilling.app.mjs new file mode 100644 index 0000000000000..9d2b0ae23fadc --- /dev/null +++ b/components/portabilling/portabilling.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "portabilling", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/portfolio_optimizer/README.md b/components/portfolio_optimizer/README.md new file mode 100644 index 0000000000000..bd934677a3bc5 --- /dev/null +++ b/components/portfolio_optimizer/README.md @@ -0,0 +1,11 @@ +# Overview + +The Portfolio Optimizer API allows you to build advanced financial models and investment strategies directly within Pipedream. With this API, you gain access to portfolio analysis tools that can optimize asset allocations, calculate efficient frontiers, and perform risk assessments. Leverage the power of Pipedream's serverless platform to automate these tasks, integrate with other financial data sources, and much more, facilitating smarter investment decisions and real-time portfolio management. + +# Example Use Cases + +- **Automated Portfolio Rebalancing**: Trigger a workflow on a schedule to analyze your investment portfolio using the Portfolio Optimizer API. This workflow could fetch current asset prices from a finance app like Yahoo Finance, calculate the optimal asset mix, and if necessary, send orders to a brokerage service or send alerts for manual rebalancing. + +- **Risk Analysis Reporting**: Connect the Portfolio Optimizer API to your data warehouse where your financial data is stored. Periodically, trigger a Pipedream workflow to pull the latest data, use the API to assess portfolio risk, and generate a report. Email this risk analysis to stakeholders or save it to cloud storage such as Google Drive or Dropbox for easy access. + +- **Investment Strategy Backtesting**: Combine historical stock data from apps like Alpha Vantage with the Portfolio Optimizer API within a Pipedream workflow. Test different investment strategies by backtesting against historical data to find the most efficient portfolio model, and then automatically publish the results to a Google Sheets spreadsheet or a Slack channel dedicated to investment strategy discussions. diff --git a/components/postalytics/README.md b/components/postalytics/README.md new file mode 100644 index 0000000000000..562bda7653a96 --- /dev/null +++ b/components/postalytics/README.md @@ -0,0 +1,11 @@ +# Overview + +The Postalytics API lets you automate direct mail marketing campaigns, manage contacts, and track results. By integrating Postalytics with Pipedream, you can build serverless workflows that leverage your marketing stack, sync data across platforms, and personalize your outreach efforts. You can trigger direct mail pieces based on customer behavior, update your CRM with mailing results, or create custom analytics dashboards. + +# Example Use Cases + +- **Trigger Direct Mail from E-Commerce Events**: Send personalized postcards or letters when a customer makes a significant purchase on your e-commerce platform. Use triggers from Shopify or WooCommerce to initiate these mailings through Postalytics within a Pipedream workflow. + +- **Sync Mailing Results with CRM**: After Postalytics sends a mail and tracks the response, update your CRM records automatically. Connect Postalytics to Salesforce or HubSpot within Pipedream to ensure your sales team has the latest engagement data. + +- **Automated Follow-Up Campaigns**: Combine Postalytics with email marketing solutions like Mailchimp. When a direct mail recipient engages with your material, trigger a follow-up email campaign, creating a multi-touch marketing strategy without manual intervention. diff --git a/components/postgresql/README.md b/components/postgresql/README.md index 1df6bdbe4470b..fddccf5113d4b 100644 --- a/components/postgresql/README.md +++ b/components/postgresql/README.md @@ -1,12 +1,11 @@ # Overview -Assuming you want a few paragraphs on what you can do with the PostgreSQL API: +On Pipedream, you can leverage the PostgreSQL app to create workflows that automate database operations, synchronize data across platforms, and react to database events in real-time. Think handling new row entries, updating records from webhooks, or even compiling reports on a set schedule. Pipedream's serverless platform provides a powerful way to connect PostgreSQL with a variety of apps, enabling you to create tailored automation that fits your specific needs. -The following examples demonstrate some of the things that can be done with the -PostgreSQL API: +# Example Use Cases -- Developing a custom storage engine for PostgreSQL -- Adding a new data type to PostgreSQL -- Creating a new function for PostgreSQL -- Building a graphical user interface for PostgreSQL -- And much more! +- **Real-time Data Sync**: Keep your PostgreSQL database in sync with another data store, like Google Sheets. Each time a new row is added to a PostgreSQL table, a Pipedream workflow can insert the corresponding data into a Google Sheet, ensuring that your team has access to the latest information without manual updates. + +- **Automated Backups**: Set up a workflow that triggers on a schedule to perform database backups. The workflow could export data from selected PostgreSQL tables and save them to a cloud storage platform like Dropbox or Google Drive, providing regular and reliable backups without manual intervention. + +- **Event-Driven Notifications**: Create a Pipedream workflow that watches for specific changes in your PostgreSQL database, such as a new customer sign-up or reaching a stock threshold. Upon detecting the change, it can send a notification through platforms like Slack or email, keeping relevant stakeholders informed instantly and facilitating timely responses. diff --git a/components/postgresql/actions/delete-rows/delete-rows.mjs b/components/postgresql/actions/delete-rows/delete-rows.mjs index 6a9e8bddb63dc..df8d4200bb552 100644 --- a/components/postgresql/actions/delete-rows/delete-rows.mjs +++ b/components/postgresql/actions/delete-rows/delete-rows.mjs @@ -3,24 +3,15 @@ import postgresql from "../../postgresql.app.mjs"; export default { name: "Delete Row(s)", key: "postgresql-delete-rows", - description: "Deletes a row or rows from a table. [See Docs](https://node-postgres.com/features/queries)", - version: "0.0.10", + description: "Deletes a row or rows from a table. [See the documentation](https://node-postgres.com/features/queries)", + version: "2.0.6", type: "action", props: { postgresql, - rejectUnauthorized: { - propDefinition: [ - postgresql, - "rejectUnauthorized", - ], - }, schema: { propDefinition: [ postgresql, "schema", - (c) => ({ - rejectUnauthorized: c.rejectUnauthorized, - }), ], }, table: { @@ -29,7 +20,6 @@ export default { "table", (c) => ({ schema: c.schema, - rejectUnauthorized: c.rejectUnauthorized, }), ], }, @@ -40,7 +30,6 @@ export default { (c) => ({ table: c.table, schema: c.schema, - rejectUnauthorized: c.rejectUnauthorized, }), ], label: "Lookup Column", @@ -54,7 +43,6 @@ export default { table: c.table, column: c.column, schema: c.schema, - rejectUnauthorized: c.rejectUnauthorized, }), ], }, @@ -65,7 +53,6 @@ export default { schema, column, value, - rejectUnauthorized, } = this; try { @@ -74,15 +61,15 @@ export default { table, column, value, - rejectUnauthorized, ); $.export("$summary", `Deleted ${rows.length} rows from ${table}`); return rows; } catch (error) { - throw new Error(` - Row not deleted due to an error. ${error}. - This could be because SSL verification failed, consider changing the Reject Unauthorized prop and try again. - `); + let errorMsg = "Row not deleted due to an error. "; + errorMsg += `${error}`.includes("SSL verification failed") + ? "This could be because SSL verification failed. To resolve this, reconnect your account and set SSL Verification Mode: Skip Verification, and try again." + : `${error}`; + throw new Error(errorMsg); } }, }; diff --git a/components/postgresql/actions/execute-custom-query/execute-custom-query.mjs b/components/postgresql/actions/execute-custom-query/execute-custom-query.mjs index f27f66282d25b..7af9f9eee88fb 100644 --- a/components/postgresql/actions/execute-custom-query/execute-custom-query.mjs +++ b/components/postgresql/actions/execute-custom-query/execute-custom-query.mjs @@ -1,60 +1,28 @@ import postgresql from "../../postgresql.app.mjs"; export default { - name: "Execute Custom Query", + name: "Execute SQL Query", key: "postgresql-execute-custom-query", - description: "Executes a custom query you provide. [See Docs](https://node-postgres.com/features/queries)", - version: "0.0.9", + description: "Execute a custom PostgreSQL query. See [our docs](https://pipedream.com/docs/databases/working-with-sql) to learn more about working with SQL in Pipedream.", + version: "3.0.1", type: "action", props: { postgresql, - query: { - propDefinition: [ - postgresql, - "query", - ], - }, - values: { - propDefinition: [ - postgresql, - "values", - ], - }, - rejectUnauthorized: { - propDefinition: [ - postgresql, - "rejectUnauthorized", - ], + // eslint-disable-next-line pipedream/props-description + sql: { + type: "sql", + auth: { + app: "postgresql", + }, + label: "PostreSQL Query", }, }, async run({ $ }) { - const { - query, - values = [], - rejectUnauthorized, - } = this; - - if (!Array.isArray(values)) { - throw new Error("No valid values provided. The values property must be an array."); - } - - const numberOfValues = query?.match(/\$/g)?.length || 0; - if (values.length !== numberOfValues) { - throw new Error("The number of values provided does not match the number of values in the query."); - } - - try { - const res = await this.postgresql.executeQuery({ - text: query, - values, - }, rejectUnauthorized); - $.export("$summary", "Successfully executed query"); - return res; - } catch (error) { - throw new Error(` - Query not executed due to an error. ${error}. - This could be because SSL verification failed, consider changing the Reject Unauthorized prop and try again. - `); - } + const args = this.postgresql.executeQueryAdapter(this.sql); + const data = await this.postgresql.executeQuery(args); + $.export("$summary", `Returned ${data.length} ${data.length === 1 + ? "row" + : "rows"}`); + return data; }, }; diff --git a/components/postgresql/actions/find-row-custom-query/find-row-custom-query.mjs b/components/postgresql/actions/find-row-custom-query/find-row-custom-query.mjs index ee69b12894344..a41a5396ff76e 100644 --- a/components/postgresql/actions/find-row-custom-query/find-row-custom-query.mjs +++ b/components/postgresql/actions/find-row-custom-query/find-row-custom-query.mjs @@ -3,8 +3,8 @@ import postgresql from "../../postgresql.app.mjs"; export default { name: "Find Row With Custom Query", key: "postgresql-find-row-custom-query", - description: "Finds a row in a table via a custom query. [See Docs](https://node-postgres.com/features/queries)", - version: "0.0.7", + description: "Finds a row in a table via a custom query. [See the documentation](https://node-postgres.com/features/queries)", + version: "2.0.6", type: "action", props: { postgresql, @@ -20,18 +20,11 @@ export default { "values", ], }, - rejectUnauthorized: { - propDefinition: [ - postgresql, - "rejectUnauthorized", - ], - }, }, async run({ $ }) { const { query, values = [], - rejectUnauthorized, } = this; if (!Array.isArray(values)) { @@ -53,14 +46,15 @@ export default { const res = await this.postgresql.executeQuery({ text: query, values, - }, rejectUnauthorized); + }); $.export("$summary", "Successfully executed query"); return res; } catch (error) { - throw new Error(` - Query not executed due to an error. ${error}. - This could be because SSL verification failed, consider changing the Reject Unauthorized prop and try again. - `); + let errorMsg = "Query not executed due to an error. "; + errorMsg += `${error}`.includes("SSL verification failed") + ? "This could be because SSL verification failed. To resolve this, reconnect your account and set SSL Verification Mode: Skip Verification, and try again." + : `${error}`; + throw new Error(errorMsg); } }, }; diff --git a/components/postgresql/actions/find-row/find-row.mjs b/components/postgresql/actions/find-row/find-row.mjs index 0bc161e134aa5..a455451e48d14 100644 --- a/components/postgresql/actions/find-row/find-row.mjs +++ b/components/postgresql/actions/find-row/find-row.mjs @@ -3,24 +3,15 @@ import postgresql from "../../postgresql.app.mjs"; export default { name: "Find Row", key: "postgresql-find-row", - description: "Finds a row in a table via a lookup column. [See Docs](https://node-postgres.com/features/queries)", - version: "0.0.10", + description: "Finds a row in a table via a lookup column. [See the documentation](https://node-postgres.com/features/queries)", + version: "2.0.6", type: "action", props: { postgresql, - rejectUnauthorized: { - propDefinition: [ - postgresql, - "rejectUnauthorized", - ], - }, schema: { propDefinition: [ postgresql, "schema", - (c) => ({ - rejectUnauthorized: c.rejectUnauthorized, - }), ], }, table: { @@ -29,7 +20,6 @@ export default { "table", (c) => ({ schema: c.schema, - rejectUnauthorized: c.rejectUnauthorized, }), ], }, @@ -40,7 +30,6 @@ export default { (c) => ({ table: c.table, schema: c.schema, - rejectUnauthorized: c.rejectUnauthorized, }), ], label: "Lookup Column", @@ -54,7 +43,6 @@ export default { table: c.table, column: c.column, schema: c.schema, - rejectUnauthorized: c.rejectUnauthorized, }), ], }, @@ -65,7 +53,6 @@ export default { table, column, value, - rejectUnauthorized, } = this; try { const res = await this.postgresql.findRowByValue( @@ -73,7 +60,6 @@ export default { table, column, value, - rejectUnauthorized, ); const summary = res ? "Row found" @@ -81,10 +67,11 @@ export default { $.export("$summary", summary); return res; } catch (error) { - throw new Error(` - Row not retrieved due to an error. ${error}. - This could be because SSL verification failed, consider changing the Reject Unauthorized prop and try again. - `); + let errorMsg = "Row not retrieved due to an error. "; + errorMsg += `${error}`.includes("SSL verification failed") + ? "This could be because SSL verification failed. To resolve this, reconnect your account and set SSL Verification Mode: Skip Verification, and try again." + : `${error}`; + throw new Error(errorMsg); } }, }; diff --git a/components/postgresql/actions/insert-row/insert-row.mjs b/components/postgresql/actions/insert-row/insert-row.mjs index e137c55982ea9..42a1feb182b41 100644 --- a/components/postgresql/actions/insert-row/insert-row.mjs +++ b/components/postgresql/actions/insert-row/insert-row.mjs @@ -3,24 +3,15 @@ import postgresql from "../../postgresql.app.mjs"; export default { name: "Insert Row", key: "postgresql-insert-row", - description: "Adds a new row. [See Docs](https://node-postgres.com/features/queries)", - version: "0.0.7", + description: "Adds a new row. [See the documentation](https://node-postgres.com/features/queries)", + version: "2.0.6", type: "action", props: { postgresql, - rejectUnauthorized: { - propDefinition: [ - postgresql, - "rejectUnauthorized", - ], - }, schema: { propDefinition: [ postgresql, "schema", - (c) => ({ - rejectUnauthorized: c.rejectUnauthorized, - }), ], }, table: { @@ -29,7 +20,6 @@ export default { "table", (c) => ({ schema: c.schema, - rejectUnauthorized: c.rejectUnauthorized, }), ], }, @@ -45,7 +35,6 @@ export default { schema, table, rowValues, - rejectUnauthorized, } = this; const columns = Object.keys(rowValues); const values = Object.values(rowValues); @@ -55,15 +44,15 @@ export default { table, columns, values, - rejectUnauthorized, ); $.export("$summary", "New row inserted"); return res; } catch (error) { - throw new Error(` - New row not inserted due to an error. ${error}. - This could be because SSL verification failed, consider changing the Reject Unauthorized prop and try again. - `); + let errorMsg = "New row not inserted due to an error. "; + errorMsg += `${error}`.includes("SSL verification failed") + ? "This could be because SSL verification failed. To resolve this, reconnect your account and set SSL Verification Mode: Skip Verification, and try again." + : `${error}`; + throw new Error(errorMsg); } }, }; diff --git a/components/postgresql/actions/update-row/update-row.mjs b/components/postgresql/actions/update-row/update-row.mjs index 6ea0e7e19e10a..a2964051d2b9c 100644 --- a/components/postgresql/actions/update-row/update-row.mjs +++ b/components/postgresql/actions/update-row/update-row.mjs @@ -3,24 +3,15 @@ import postgresql from "../../postgresql.app.mjs"; export default { name: "Update Row", key: "postgresql-update-row", - description: "Updates an existing row. [See Docs](https://node-postgres.com/features/queries)", - version: "0.0.10", + description: "Updates an existing row. [See the documentation](https://node-postgres.com/features/queries)", + version: "2.0.6", type: "action", props: { postgresql, - rejectUnauthorized: { - propDefinition: [ - postgresql, - "rejectUnauthorized", - ], - }, schema: { propDefinition: [ postgresql, "schema", - (c) => ({ - rejectUnauthorized: c.rejectUnauthorized, - }), ], }, table: { @@ -29,7 +20,6 @@ export default { "table", (c) => ({ schema: c.schema, - rejectUnauthorized: c.rejectUnauthorized, }), ], }, @@ -40,7 +30,6 @@ export default { (c) => ({ table: c.table, schema: c.schema, - rejectUnauthorized: c.rejectUnauthorized, }), ], label: "Lookup Column", @@ -54,7 +43,6 @@ export default { table: c.table, column: c.column, schema: c.schema, - rejectUnauthorized: c.rejectUnauthorized, }), ], }, @@ -72,7 +60,6 @@ export default { column, value, rowValues, - rejectUnauthorized, } = this; try { const res = await this.postgresql.updateRow( @@ -81,7 +68,6 @@ export default { column, value, rowValues, - rejectUnauthorized, ); const summary = res ? "Row updated" @@ -89,10 +75,11 @@ export default { $.export("$summary", summary); return res; } catch (error) { - throw new Error(` - Row not updated due to an error. ${error}. - This could be because SSL verification failed, consider changing the Reject Unauthorized prop and try again. - `); + let errorMsg = "Row not updated due to an error. "; + errorMsg += `${error}`.includes("SSL verification failed") + ? "This could be because SSL verification failed. To resolve this, reconnect your account and set SSL Verification Mode: Skip Verification, and try again." + : `${error}`; + throw new Error(errorMsg); } }, }; diff --git a/components/postgresql/actions/upsert-row/upsert-row.mjs b/components/postgresql/actions/upsert-row/upsert-row.mjs index c0fa4fa67b39a..5bb5042508505 100644 --- a/components/postgresql/actions/upsert-row/upsert-row.mjs +++ b/components/postgresql/actions/upsert-row/upsert-row.mjs @@ -4,24 +4,15 @@ import format from "pg-format"; export default { name: "Upsert Row", key: "postgresql-upsert-row", - description: "Adds a new row or updates an existing row. [See Docs](https://node-postgres.com/features/queries)", - version: "0.0.1", + description: "Adds a new row or updates an existing row. [See the documentation](https://node-postgres.com/features/queries)", + version: "2.0.6", type: "action", props: { postgresql, - rejectUnauthorized: { - propDefinition: [ - postgresql, - "rejectUnauthorized", - ], - }, schema: { propDefinition: [ postgresql, "schema", - (c) => ({ - rejectUnauthorized: c.rejectUnauthorized, - }), ], }, table: { @@ -30,7 +21,6 @@ export default { "table", (c) => ({ schema: c.schema, - rejectUnauthorized: c.rejectUnauthorized, }), ], }, @@ -41,7 +31,6 @@ export default { (c) => ({ table: c.table, schema: c.schema, - rejectUnauthorized: c.rejectUnauthorized, }), ], label: "Conflict Target", @@ -63,22 +52,19 @@ export default { * @param {Array} args.columns - The columns in which to insert values. * @param {Array} args.values - The values to insert. * @param {string} args.conflictTarget - The column to use as the conflict target. - * @param {boolean} args.rejectUnauthorized - If false, allows - * self-signed and invalid SSL certificates. * @returns {Promise} A promise that resolves with the result of the query. * @throws {Error} Will throw an error if the query fails. */ upsertRow({ - schema, table, columns, values, conflictTarget = "id", rejectUnauthorized, + schema, table, columns, values, conflictTarget = "id", } = {}) { const placeholders = this.postgresql.getPlaceholders({ values, }); - const updates = - columns - .filter((column) => column !== conflictTarget) - .map((column) => `${column}=EXCLUDED.${column}`); + const updates = columns + .filter((column) => column !== conflictTarget) + .map((column) => `${column}=EXCLUDED.${column}`); const query = ` INSERT INTO ${schema}.${table} (${columns}) @@ -91,7 +77,7 @@ export default { return this.postgresql.executeQuery({ text: format(query, schema, table), values, - }, rejectUnauthorized); + }); }, }, async run({ $ }) { @@ -116,10 +102,11 @@ export default { $.export("$summary", summary); return res; } catch (error) { - throw new Error(` - Row not upserted due to an error. ${error}. - This could be because SSL verification failed, consider changing the Reject Unauthorized prop and try again. - `); + let errorMsg = "Row not upserted due to an error. "; + errorMsg += `${error}`.includes("SSL verification failed") + ? "This could be because SSL verification failed. To resolve this, reconnect your account and set SSL Verification Mode: Skip Verification, and try again." + : `${error}`; + throw new Error(errorMsg); } }, }; diff --git a/components/postgresql/package.json b/components/postgresql/package.json index 6a9954d917c4d..9f59ef9723092 100644 --- a/components/postgresql/package.json +++ b/components/postgresql/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/postgresql", - "version": "0.1.0", + "version": "2.2.1", "description": "Pipedream PostgreSQL Components", "main": "postgresql.app.mjs", "keywords": [ @@ -10,9 +10,9 @@ "homepage": "https://pipedream.com/apps/postgresql", "author": "Pipedream (https://pipedream.com/)", "dependencies": { + "@pipedream/platform": "^2.0.0", "pg": "^8.7.1", - "pg-format": "^1.0.4", - "typescript": "^4.6.4" + "pg-format": "^1.0.4" }, "publishConfig": { "access": "public" diff --git a/components/postgresql/postgresql.app.mjs b/components/postgresql/postgresql.app.mjs index 1a71826211cac..4acf0f0b7c41c 100644 --- a/components/postgresql/postgresql.app.mjs +++ b/components/postgresql/postgresql.app.mjs @@ -1,5 +1,10 @@ import pg from "pg"; import format from "pg-format"; +import { + sqlProp, + sqlProxy, + ConfigurationError, +} from "@pipedream/platform"; export default { type: "app", @@ -9,18 +14,16 @@ export default { type: "string", label: "Schema", description: "Database schema", - async options({ rejectUnauthorized }) { - return this.getSchemas(rejectUnauthorized); + async options() { + return this.getSchemas(); }, }, table: { type: "string", label: "Table", description: "Database table", - async options({ - schema, rejectUnauthorized, - }) { - return this.getTables(this.getNormalizedSchema(schema), rejectUnauthorized); + async options({ schema }) { + return this.getTables(this._getNormalizedSchema(schema)); }, }, column: { @@ -28,11 +31,9 @@ export default { label: "Column", description: "The name of a column in the table to use for deduplication. Defaults to the table's primary key", async options({ - table, - schema, - rejectUnauthorized, + table, schema, }) { - return this.getColumns(table, this.getNormalizedSchema(schema), rejectUnauthorized); + return this.getColumns(table, this._getNormalizedSchema(schema)); }, }, query: { @@ -51,14 +52,14 @@ export default { label: "Lookup Value", description: "Value to search for", async options({ - table, column, prevContext, schema, rejectUnauthorized, + table, column, prevContext, schema, }) { const limit = 20; - const normalizedSchema = this.getNormalizedSchema(schema); + const normalizedSchema = this._getNormalizedSchema(schema); const { offset = 0 } = prevContext; return { options: await this - .getColumnValues(table, column, limit, offset, normalizedSchema, rejectUnauthorized), + .getColumnValues(table, column, limit, offset, normalizedSchema), context: { offset: limit + offset, }, @@ -70,72 +71,189 @@ export default { label: "Row Values", description: "Enter the column names and respective values as key/value pairs, or with structured mode off as `{{{columnName:\"columnValue\"}}}`", }, - rejectUnauthorized: { - type: "boolean", - label: "Reject Unauthorized", - description: "If `true`, the server certificate is verified against the list of supplied CAs. If you encounter the error `Connection terminated unexpectedly`, try setting this prop to `false`.", - default: true, - }, }, methods: { - async getClient(rejectUnauthorized = true) { - const { Client } = pg; + ...sqlProp.methods, + ...sqlProxy.methods, + _getSslConfig() { + const { + ca, + key, + cert, + ssl_verification_mode: mode, + } = this.$auth; + + const ssl = { + ...(ca && { + ca, + }), + ...(key && { + key, + }), + ...(cert && { + cert, + }), + rejectUnauthorized: mode !== "skip_verification", + }; + + return Object.keys(ssl).length > 0 + ? ssl + : undefined; + }, + + /** + * A helper method to get the configuration object that's directly fed to + * the PostgreSQL client constructor. Used by other features (like SQL + * proxy) to initialize their client in an identical way. + * @returns {object} - Configuration object for the PostgreSQL client + */ + getClientConfiguration() { const { + host, + port, user, password, + database, + } = this.$auth; + + return { host, port, + user, + password, database, - } = this.$auth; - const config = { - connectionString: `postgresql://${user}:${password}@${host}:${port}/${database}`, + ssl: this._getSslConfig(), }; - if (rejectUnauthorized === false) { - config.ssl = { - rejectUnauthorized, - }; + }, + async _getClient() { + const config = this.getClientConfiguration(); + const client = new pg.Client(config); + try { + await client.connect(); + } catch (err) { + if (err.code === "SELF_SIGNED_CERT_IN_CHAIN") { + throw new ConfigurationError(`SSL verification failed: \`${err}\`. To resolve this, reconnect your account and set SSL Verification Mode: Skip Verification, and try again.`); + } + console.error("Connection error", err.stack); + throw err; } - const client = new Client(config); - await client - .connect() - .catch((err) => console.error("Connection error", err.stack)); return client; }, - async endClient(client) { + async _endClient(client) { return client.end(); }, /** - * Executes SQL query and returns the resulting rows - * @param {string} query - SQL query to execute - * @param {boolean} rejectUnauthorized - if false, allow self-signed certificates - * @returns Array of rows returned from the given SQL query + * Adapts the arguments to `executeQuery` so that they can be consumed by + * the SQL proxy (when applicable). Note that this method is not intended to + * be used by the component directly. + * @param {object|string} query The query string or object to be sent to the DB. + * @param {string} query.text The prepared SQL query to be executed. + * @param {string[]} query.values The values to replace in the SQL query. + * @returns {object} - The adapted query and parameters. */ - async executeQuery(query, rejectUnauthorized = true) { - const client = await this.getClient(rejectUnauthorized); + proxyAdapter(query) { + if (typeof query === "string") { + return { + query, + }; + } + + return { + query: query.text, + params: query.values, + }; + }, + /** + * A method that performs the inverse transformation of `proxyAdapter`. + * + * @param {object} proxyArgs - The output of `proxyAdapter`. + * @param {string} proxyArgs.query - The SQL query to be executed. + * @param {string[]} proxyArgs.params - The values to replace in the SQL + * query. + * @returns {object} - The adapted query and parameters, compatible with + * `executeQuery`. + */ + executeQueryAdapter(proxyArgs = {}) { + const { + query: text = "", + params: values = [], + } = proxyArgs; + return { + text, + values, + }; + }, + /** + * Executes a query against the PostgreSQL database. This method takes care + * of connecting to the database, executing the query, and closing the + * connection. + * @param {object|string} query The query string or object to be sent to the DB. + * @param {string} query.text The prepared SQL query to be executed. + * @param {string[]} query.values The values to replace in the SQL query. + * SQL query. + * @returns {object[]} - The rows returned by the DB as a result of the + * query. + */ + async executeQuery(query) { + const client = await this._getClient(); try { const { rows } = await client.query(query); return rows; } finally { - await this.endClient(client); + await this._endClient(client); } }, + /** + * A helper method to get the schema of the database. Used by other features + * (like the `sql` prop) to enrich the code editor and provide the user with + * auto-complete and fields suggestion. + * + * @returns {DbInfo} The schema of the database, which is a + * JSON-serializable object. + */ + async getSchema() { + const text = ` + SELECT table_name AS "tableName", + column_name AS "columnName", + is_nullable AS "isNullable", + data_type AS "dataType", + column_default AS "columnDefault" + FROM information_schema.columns + WHERE table_schema NOT IN ('pg_catalog', 'information_schema') + ORDER BY table_name, + ordinal_position + `; + const rows = await this.executeQuery({ + text, + }); + return rows.reduce((acc, row) => { + acc[row.tableName] ??= { + metadata: {}, + schema: {}, + }; + acc[row.tableName].schema[row.columnName] = { + ...row, + }; + return acc; + }, {}); + }, /** * Gets an array of table names in a database * @returns Array of table names */ - async getTables(schema = "public", rejectUnauthorized = true) { + async getTables(schema = "public") { const query = format("SELECT table_name FROM information_schema.tables WHERE table_schema = %L", schema); - const rows = await this.executeQuery(query, rejectUnauthorized); + const rows = await this.executeQuery(query); return rows.map((row) => row.table_name); }, /** * Gets an array of schemas in a database * @returns Array of schemas */ - async getSchemas(rejectUnauthorized = true) { + async getSchemas() { const query = format("select schema_name FROM information_schema.schemata"); - const rows = await this.executeQuery(query, rejectUnauthorized); + const rows = await this.executeQuery(query); return rows.map((row) => row.schema_name); }, /** @@ -143,12 +261,17 @@ export default { * @param {string} table - Name of the table to get columns in * @returns Array of column names */ - async getColumns(table, schema = "public", rejectUnauthorized = true) { + async getColumns(table, schema = "public") { if (!table) { return []; } - const query = format("SELECT column_name FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %L AND TABLE_NAME = %L", schema, table); - const rows = await this.executeQuery(query, rejectUnauthorized); + + const query = format(` + SELECT column_name + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = %L AND TABLE_NAME = %L + `, schema, table); + const rows = await this.executeQuery(query); return rows.map((row) => row.column_name); }, /** @@ -156,7 +279,7 @@ export default { * @param {string} table - Name of the table to get the primary key for * @returns Name of the primary key column */ - async getPrimaryKey(table, schema = "public", rejectUnauthorize = true) { + async getPrimaryKey(table, schema = "public") { const rows = await this.executeQuery({ text: format(` SELECT c.column_name, c.ordinal_position @@ -165,7 +288,7 @@ export default { ON t.constraint_name = c.constraint_name WHERE t.table_name = %L and t.table_schema = %L AND t.constraint_type = 'PRIMARY KEY'; `, table, schema), - }, rejectUnauthorize); + }); return rows[0]?.column_name; }, /** @@ -176,7 +299,7 @@ export default { * last time the table was queried. * @returns Array of rows returned from the query */ - async getRows(table, column, lastResult = null, rejectUnauthorize = true, schema = "public") { + async getRows(table, column, lastResult = null, schema = "public") { const select = "SELECT * FROM %I.%I"; const where = "WHERE %I > $1"; const orderby = "ORDER BY %I DESC"; @@ -188,23 +311,22 @@ export default { ], } : format(`${select} ${orderby}`, schema, table, column); - return this.executeQuery(query, rejectUnauthorize); + return this.executeQuery(query); }, /** * Gets a single row from a table based on a lookup column & value * @param {string} table - Name of database table to query * @param {string} column - Column to filter by value * @param {string} value - A column value to search for - * @param {boolean} rejectUnauthorized - if false, allow self-signed certificates * @returns A single database row */ - async findRowByValue(schema, table, column, value, rejectUnauthorized) { + async findRowByValue(schema, table, column, value) { const rows = await this.executeQuery({ text: format("SELECT * FROM %I.%I WHERE %I = $1", schema, table, column), values: [ value, ], - }, rejectUnauthorized); + }); return rows[0]; }, /** @@ -212,25 +334,23 @@ export default { * @param {string} table - Name of database table to delete from * @param {string} column - Column used to find the row(s) to delete * @param {string} value - A column value. Used to find the row(s) to delete - * @param {boolean} rejectUnauthorized - if false, allow self-signed certificates */ - async deleteRows(schema, table, column, value, rejectUnauthorized) { + async deleteRows(schema, table, column, value) { return this.executeQuery({ text: format("DELETE FROM %I.%I WHERE %I = $1 RETURNING *", schema, table, column), values: [ value, ], - }, rejectUnauthorized); + }); }, /** * Inserts a row in a table * @param {string} table - Name of database table to insert row into * @param {array} columns - Array of column names * @param {array} values - Array of values corresponding to the column names provided - * @param {boolean} rejectUnauthorized - if false, allow self-signed certificates * @returns The newly created row */ - async insertRow(schema, table, columns, values, rejectUnauthorized) { + async insertRow(schema, table, columns, values) { const placeholders = this.getPlaceholders({ values, }); @@ -241,11 +361,12 @@ export default { RETURNING * `, schema, table), values, - }, rejectUnauthorized); + }); }, getPlaceholders({ - values = [], fromIndex = 1, - }) { + values = [], + fromIndex = 1, + } = {}) { return values.map((_, index) => `$${index + fromIndex}`); }, /** @@ -255,11 +376,10 @@ export default { * @param {string} lookupValue - A column value. Used to find row to update * @param {object} rowValues - An object with keys representing column names * and values representing new column values - * @param {boolean} rejectUnauthorized - if false, allow self-signed certificates * @returns The newly updated row */ - async updateRow(schema, table, lookupColumn, lookupValue, rowValues, rejectUnauthorized) { - const columnsPlaceholders = this.getColumnsPlaceholders({ + async updateRow(schema, table, lookupColumn, lookupValue, rowValues) { + const columnsPlaceholders = this._getColumnsPlaceholders({ rowValues, fromIndex: 2, }); @@ -276,38 +396,41 @@ export default { lookupValue, ...Object.values(rowValues), ], - }, rejectUnauthorized); + }); return response[0]; }, - getColumnsPlaceholders({ - rowValues = {}, fromIndex = 1, - }) { - return Object.keys(rowValues) + _getColumnsPlaceholders({ + rowValues = {}, + fromIndex = 1, + } = {}) { + return Object + .keys(rowValues) .map((key, index) => `${key} = $${index + fromIndex}`); }, /** * Gets all of the values for a single column in a table * @param {string} table - Name of database table to query * @param {string} column - Name of column to get values from - * @params {string} [limit] - maximum number of rows to return - * @params {integer} [offset] - number of rows to skip, used for pagination + * @param {string} [limit] - maximum number of rows to return + * @param {integer} [offset] - number of rows to skip, used for pagination * @returns Array of column values */ - async getColumnValues(table, column, limit = "ALL", offset = 0, schema = "public", rejectUnauthorized = true) { + async getColumnValues(table, column, limit = "ALL", offset = 0, schema = "public") { const rows = await this.executeQuery({ text: format(` - SELECT %I FROM %I.%I - LIMIT $1::numeric OFFSET $2::numeric + SELECT %I + FROM %I.%I + LIMIT $1::numeric OFFSET $2::numeric `, column, schema, table), values: [ limit, offset, ], - }, rejectUnauthorized); + }); const values = rows.map((row) => row[column]?.toString()); return values.filter((row) => row); }, - async getInitialRows(schema, table, column, limitNum = 10, rejectUnauthorize = true) { + async getInitialRows(schema, table, column, limitNum = 10) { const select = "SELECT * FROM %I.%I"; const orderby = "ORDER BY %I DESC"; const limit = "LIMIT $1"; @@ -317,14 +440,11 @@ export default { limitNum, ], }; - return this.executeQuery(query, rejectUnauthorize); + return this.executeQuery(query); }, /**Normalize Schema**/ - getNormalizedSchema(schema) { - if (!schema) { - return "public"; - } - return schema; + _getNormalizedSchema(schema) { + return schema ?? "public"; }, }, }; diff --git a/components/postgresql/sources/common.mjs b/components/postgresql/sources/common.mjs index f0aeb5b568151..e4e7a52954e18 100644 --- a/components/postgresql/sources/common.mjs +++ b/components/postgresql/sources/common.mjs @@ -14,12 +14,6 @@ export default { label: "Polling Interval", description: "Pipedream will poll the API on this schedule", }, - rejectUnauthorized: { - propDefinition: [ - postgresql, - "rejectUnauthorized", - ], - }, }, methods: { _getPreviousValues() { @@ -63,14 +57,12 @@ export default { table, column, lastResult, - this.rejectUnauthorized, schema, ); this.emitRows(rows, column); }, - async initialRows(schema, table, column, limit, rejectUnauthorized) { - const rows = await this.postgresql - .getInitialRows(schema, table, column, limit, rejectUnauthorized); + async initialRows(schema, table, column, limit) { + const rows = await this.postgresql.getInitialRows(schema, table, column, limit); this.emitRows(rows, column); }, emitRows(rows, column) { @@ -82,7 +74,7 @@ export default { }, async isColumnUnique(schema, table, column) { const query = format("select count(*) <> count(distinct %I) as duplicate_flag from %I.%I", column, schema, table); - const hasDuplicates = await this.postgresql.executeQuery(query, this.rejectUnauthorized); + const hasDuplicates = await this.postgresql.executeQuery(query); const { duplicate_flag: duplicateFlag } = hasDuplicates[0]; return !duplicateFlag; }, diff --git a/components/postgresql/sources/new-column/new-column.mjs b/components/postgresql/sources/new-column/new-column.mjs index 0abbab030fd41..c09dfe325506e 100644 --- a/components/postgresql/sources/new-column/new-column.mjs +++ b/components/postgresql/sources/new-column/new-column.mjs @@ -4,8 +4,8 @@ export default { ...common, name: "New Column", key: "postgresql-new-column", - description: "Emit new event when a new column is added to a table. [See Docs](https://node-postgres.com/features/queries)", - version: "0.0.9", + description: "Emit new event when a new column is added to a table. [See the documentation](https://node-postgres.com/features/queries)", + version: "2.0.6", type: "source", props: { ...common.props, @@ -13,9 +13,6 @@ export default { propDefinition: [ common.props.postgresql, "schema", - (c) => ({ - rejectUnauthorized: c.rejectUnauthorized, - }), ], }, table: { @@ -24,7 +21,6 @@ export default { "table", (c) => ({ schema: c.schema, - rejectUnauthorized: c.rejectUnauthorized, }), ], }, @@ -32,8 +28,7 @@ export default { async run() { const previousColumns = this._getPreviousValues() || []; - const columns = await this.postgresql - .getColumns(this.table, this.schema, this.rejectUnauthorized); + const columns = await this.postgresql.getColumns(this.table, this.schema); const newColumns = columns.filter((column) => !previousColumns.includes(column)); for (const column of newColumns) { diff --git a/components/postgresql/sources/new-or-updated-row/new-or-updated-row.mjs b/components/postgresql/sources/new-or-updated-row/new-or-updated-row.mjs index 275354da2fd6a..ec64ec58c66a9 100644 --- a/components/postgresql/sources/new-or-updated-row/new-or-updated-row.mjs +++ b/components/postgresql/sources/new-or-updated-row/new-or-updated-row.mjs @@ -4,8 +4,8 @@ export default { ...common, name: "New or Updated Row", key: "postgresql-new-or-updated-row", - description: "Emit new event when a row is added or modified. [See Docs](https://node-postgres.com/features/queries)", - version: "0.0.9", + description: "Emit new event when a row is added or modified. [See the documentation](https://node-postgres.com/features/queries)", + version: "2.0.6", type: "source", dedupe: "unique", props: { @@ -14,9 +14,6 @@ export default { propDefinition: [ common.props.postgresql, "schema", - (c) => ({ - rejectUnauthorized: c.rejectUnauthorized, - }), ], }, table: { @@ -25,7 +22,6 @@ export default { "table", (c) => ({ schema: c.schema, - rejectUnauthorized: c.rejectUnauthorized, }), ], }, @@ -37,7 +33,6 @@ export default { (c) => ({ table: c.table, schema: c.schema, - rejectUnauthorized: c.rejectUnauthorized, }), ], description: "The column to identify an unique row, commonly it's `id` or `uuid`.", @@ -50,7 +45,6 @@ export default { (c) => ({ table: c.table, schema: c.schema, - rejectUnauthorized: c.rejectUnauthorized, }), ], description: "A datetime column, such as 'date_updated' or 'last_modified' that is set to the current datetime when a row is updated.", @@ -58,11 +52,12 @@ export default { }, hooks: { async deploy() { - await this.initialRows(this.schema, + await this.initialRows( + this.schema, this.table, this.timestampColumn, this.limit, - this.rejectUnauthorized); + ); }, }, methods: { diff --git a/components/postgresql/sources/new-row-custom-query/new-row-custom-query.mjs b/components/postgresql/sources/new-row-custom-query/new-row-custom-query.mjs index 97fb33a45b30b..e0859460167ea 100644 --- a/components/postgresql/sources/new-row-custom-query/new-row-custom-query.mjs +++ b/components/postgresql/sources/new-row-custom-query/new-row-custom-query.mjs @@ -4,8 +4,8 @@ export default { ...common, name: "New Row Custom Query", key: "postgresql-new-row-custom-query", - description: "Emit new event when new rows are returned from a custom query that you provide. [See Docs](https://node-postgres.com/features/queries)", - version: "0.0.10", + description: "Emit new event when new rows are returned from a custom query that you provide. [See the documentation](https://node-postgres.com/features/queries)", + version: "2.0.6", type: "source", dedupe: "unique", props: { @@ -14,9 +14,6 @@ export default { propDefinition: [ common.props.postgresql, "schema", - (c) => ({ - rejectUnauthorized: c.rejectUnauthorized, - }), ], }, table: { @@ -25,7 +22,6 @@ export default { "table", (c) => ({ schema: c.schema, - rejectUnauthorized: c.rejectUnauthorized, }), ], }, @@ -36,7 +32,6 @@ export default { (c) => ({ table: c.table, schema: c.schema, - rejectUnauthorized: c.rejectUnauthorized, }), ], }, @@ -89,7 +84,7 @@ export default { const rows = await this.postgresql.executeQuery({ text: query, values, - }, this.rejectUnauthorized); + }); for (const row of rows) { const meta = this.generateMeta(row, column); this.$emit(row, meta); diff --git a/components/postgresql/sources/new-row/new-row.mjs b/components/postgresql/sources/new-row/new-row.mjs index 9293c7a784806..c51deb369a41f 100644 --- a/components/postgresql/sources/new-row/new-row.mjs +++ b/components/postgresql/sources/new-row/new-row.mjs @@ -4,8 +4,8 @@ export default { ...common, name: "New Row", key: "postgresql-new-row", - description: "Emit new event when a new row is added to a table. [See Docs](https://node-postgres.com/features/queries)", - version: "1.0.7", + description: "Emit new event when a new row is added to a table. [See the documentation](https://node-postgres.com/features/queries)", + version: "3.0.6", type: "source", dedupe: "unique", props: { @@ -14,9 +14,6 @@ export default { propDefinition: [ common.props.postgresql, "schema", - (c) => ({ - rejectUnauthorized: c.rejectUnauthorized, - }), ], }, table: { @@ -25,7 +22,6 @@ export default { "table", (c) => ({ schema: c.schema, - rejectUnauthorized: c.rejectUnauthorized, }), ], }, @@ -36,7 +32,6 @@ export default { (c) => ({ schema: c.schema, table: c.table, - rejectUnauthorized: c.rejectUnauthorized, }), ], description: "An ID or timestamp column where new rows will always contain larger values than the previous row. Defaults to the table's primary key.", @@ -49,7 +44,7 @@ export default { async deploy() { const column = this.column ? this.column - : await this.postgresql.getPrimaryKey(this.table, this.schema, this.rejectUnauthorized); + : await this.postgresql.getPrimaryKey(this.table, this.schema); this._setColumn(column); await this.initialRows(this.schema, this.table, column); diff --git a/components/postgresql/sources/new-table/new-table.mjs b/components/postgresql/sources/new-table/new-table.mjs index e6a58a17ed003..d8863e0cfbe20 100644 --- a/components/postgresql/sources/new-table/new-table.mjs +++ b/components/postgresql/sources/new-table/new-table.mjs @@ -4,8 +4,8 @@ export default { ...common, name: "New Table", key: "postgresql-new-table", - description: "Emit new event when a new table is added to the database. [See Docs](https://node-postgres.com/features/queries)", - version: "0.0.10", + description: "Emit new event when a new table is added to the database. [See the documentation](https://node-postgres.com/features/queries)", + version: "2.0.6", type: "source", props: { ...common.props, @@ -13,16 +13,13 @@ export default { propDefinition: [ common.props.postgresql, "schema", - (c) => ({ - rejectUnauthorized: c.rejectUnauthorized, - }), ], }, }, async run() { const previousTables = this._getPreviousValues() || []; - const tables = await this.postgresql.getTables(this.schema, this.rejectUnauthorized); + const tables = await this.postgresql.getTables(this.schema); const newTables = tables.filter((table) => !previousTables.includes(table)); for (const table of newTables) { diff --git a/components/postgrid/README.md b/components/postgrid/README.md new file mode 100644 index 0000000000000..6c5b0125c85b9 --- /dev/null +++ b/components/postgrid/README.md @@ -0,0 +1,11 @@ +# Overview + +The PostGrid Print & Mail API offers seamless print and mail solutions, enabling the automation of sending personalized letters, postcards, checks, and more. With this API, you can systematize direct mail campaigns, transactional mailings, or on-demand dispatch without managing printing infrastructure. In Pipedream, you can harness this capability to interlace PostGrid with an array of other services and triggers, creating workflows that respond to events such as form submissions, scheduled campaigns, or customer actions. + +# Example Use Cases + +- **Automated Thank You Letters**: After a customer makes a purchase in your Shopify store, use Pipedream to trigger the sending of a personalized thank you letter via PostGrid, enhancing customer engagement and loyalty. + +- **Scheduled Direct Mail Campaigns**: Combine PostGrid with cron job triggers in Pipedream to dispatch monthly promotional postcards to a curated list of contacts, driving recurring marketing efforts with minimal manual intervention. + +- **Dynamic Invoice Printing**: When a payment is recorded in your Stripe account, Pipedream can automate the creation and mailing of a physical invoice using PostGrid, ensuring timely and professional communication with your clients. diff --git a/components/postgrid_verify/README.md b/components/postgrid_verify/README.md new file mode 100644 index 0000000000000..5282575d371d5 --- /dev/null +++ b/components/postgrid_verify/README.md @@ -0,0 +1,11 @@ +# Overview + +The PostGrid Verify API offers a precise method to validate and standardize postal addresses. By integrating with this API on Pipedream, you can automate the process of scrubbing address data within your apps, ensuring accuracy and deliverability. This could be critical for businesses that depend on reliable mailing operations, CRM data accuracy, or e-commerce checkout processes. Using Pipedream, you can create serverless workflows that respond to events, verify addresses on-the-fly, and connect with countless other services for enhanced data management. + +# Example Use Cases + +- **CRM Address Validation:** Automate the verification of new addresses as they are added to your CRM. When a contact is created or updated in Salesforce, trigger a workflow that uses PostGrid Verify to validate the address. If the address is invalid, update the CRM record with a flag or send an alert to the responsible account manager. + +- **E-commerce Order Validation:** Improve your e-commerce platform's accuracy by validating shipping addresses during checkout. Set up a Pipedream workflow triggered by a new order in Shopify. Use PostGrid Verify to ensure the address is correct, reducing shipping errors, and potentially add corrected addresses back into the Shopify order details. + +- **Bulk Address Cleanup:** Periodically clean up the addresses in your database. Trigger a Pipedream workflow on a schedule to pull addresses from a Google Sheets document, validate each one with PostGrid Verify, and then update the sheet with the validation results. This helps in maintaining a clean, organized, and deliverable address database. diff --git a/components/posthog/.gitignore b/components/posthog/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/posthog/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/posthog/README.md b/components/posthog/README.md new file mode 100644 index 0000000000000..f2eac45124917 --- /dev/null +++ b/components/posthog/README.md @@ -0,0 +1,11 @@ +# Overview + +The PostHog API lets you track events, update user properties, and extract analytics to understand user behavior within your applications. Integrating this API into Pipedream workflows allows you to automate actions based on event data, sync user properties across platforms, and trigger alerts or notifications. + +# Example Use Cases + +- **Automated Feature Flags Update**: Use PostHog to monitor application usage and automatically update feature flags based on user interaction data. When a specific event is tracked, like reaching a milestone in-app, a workflow can trigger a change in feature access for the user. + +- **Slack Notifications for User Milestones**: Set up a workflow that listens for PostHog events indicating a user has achieved a particular milestone. When the event is captured, an automated notification can be sent to a Slack channel, keeping the team informed of user achievements in real-time. + +- **Sync User Data with CRM**: When PostHog identifies changes in user properties, such as updates to contact information or user preferences, trigger a workflow that syncs this new data with a CRM like Salesforce. This ensures that your user data is consistent and up-to-date across all business platforms. diff --git a/components/posthog/actions/capture-event/capture-event.mjs b/components/posthog/actions/capture-event/capture-event.mjs new file mode 100644 index 0000000000000..78fb1ec60e861 --- /dev/null +++ b/components/posthog/actions/capture-event/capture-event.mjs @@ -0,0 +1,68 @@ +import posthog from "../../posthog.app.mjs"; + +export default { + key: "posthog-capture-event", + name: "Capture Event", + description: "Captures a given event within the PostHog system. [See the documentation](https://posthog.com/docs/api/post-only-endpoints#single-event)", + version: "0.0.1", + type: "action", + props: { + posthog, + projectApiKey: { + type: "string", + label: "Project API Key", + description: "The Project API Key in your Project Settings. This key can only create new events. It can't read events or any of your other data stored with PostHog, so it's safe to use in public apps.", + }, + organizationId: { + propDefinition: [ + posthog, + "organizationId", + ], + }, + projectId: { + propDefinition: [ + posthog, + "projectId", + (c) => ({ + organizationId: c.organizationId, + }), + ], + }, + event: { + propDefinition: [ + posthog, + "event", + (c) => ({ + projectId: c.projectId, + }), + ], + }, + properties: { + type: "object", + label: "Properties", + description: "The properties to include in the event", + optional: true, + }, + }, + async run({ $ }) { + const properties = typeof this.properties === "string" + ? JSON.parse(this.properties) + : this.properties; + const { distinct_id: distinctId } = await this.posthog.getUser({ + $, + }); + const response = await this.posthog.captureEvent({ + $, + data: { + event: this.event, + api_key: this.projectApiKey, + properties: { + ...properties, + distinct_id: distinctId, + }, + }, + }); + $.export("$summary", `Successfully captured event ${this.event}`); + return response; + }, +}; diff --git a/components/posthog/app/posthog.app.ts b/components/posthog/app/posthog.app.ts deleted file mode 100644 index f1bca3bf9e386..0000000000000 --- a/components/posthog/app/posthog.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "posthog", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/posthog/common/constants.mjs b/components/posthog/common/constants.mjs new file mode 100644 index 0000000000000..6414d992bb568 --- /dev/null +++ b/components/posthog/common/constants.mjs @@ -0,0 +1,5 @@ +const DEFAULT_LIMIT = 50; + +export default { + DEFAULT_LIMIT, +}; diff --git a/components/posthog/package.json b/components/posthog/package.json index 225f87621a0e6..6d476e362c3d6 100644 --- a/components/posthog/package.json +++ b/components/posthog/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/posthog", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream PostHog Components", - "main": "dist/app/posthog.app.mjs", + "main": "posthog.app.mjs", "keywords": [ "pipedream", "posthog" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/posthog", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.4" } } diff --git a/components/posthog/posthog.app.mjs b/components/posthog/posthog.app.mjs new file mode 100644 index 0000000000000..c52abc4e6696c --- /dev/null +++ b/components/posthog/posthog.app.mjs @@ -0,0 +1,121 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "posthog", + propDefinitions: { + organizationId: { + type: "string", + label: "Organization ID", + description: "Identifier of an organization", + async options() { + const { organizations } = await this.getUser(); + return organizations?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + projectId: { + type: "string", + label: "Project ID", + description: "Identifier of a project", + async options({ + organizationId, page, + }) { + const limit = constants.DEFAULT_LIMIT; + const { results } = await this.listProjects({ + organizationId, + params: { + limit, + offset: page * limit, + }, + }); + return results?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + event: { + type: "string", + label: "Event", + description: "The event type to capture", + async options({ + projectId, page, + }) { + const limit = constants.DEFAULT_LIMIT; + const { results } = await this.listEvents({ + projectId, + params: { + limit, + offset: page * limit, + }, + }); + return results?.map(({ name }) => name ) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://app.posthog.com"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + getUser(opts = {}) { + return this._makeRequest({ + path: "/api/users/@me", + ...opts, + }); + }, + listProjects({ + organizationId, ...opts + }) { + return this._makeRequest({ + path: `/api/organizations/${organizationId}/projects`, + ...opts, + }); + }, + listEvents({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/api/projects/${projectId}/event_definitions`, + ...opts, + }); + }, + createQuery({ + projectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/api/projects/${projectId}/query`, + ...opts, + }); + }, + captureEvent(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/capture", + ...opts, + }); + }, + }, +}; diff --git a/components/posthog/sources/new-action-performed/new-action-performed.mjs b/components/posthog/sources/new-action-performed/new-action-performed.mjs new file mode 100644 index 0000000000000..4944a99c71e73 --- /dev/null +++ b/components/posthog/sources/new-action-performed/new-action-performed.mjs @@ -0,0 +1,103 @@ +import posthog from "../../posthog.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + key: "posthog-new-action-performed", + name: "New Action Performed", + description: "Emit new event when an action is performed in a project. [See the documentation](https://posthog.com/docs/api/query#post-api-projects-project_id-query)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + posthog, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + organizationId: { + propDefinition: [ + posthog, + "organizationId", + ], + }, + projectId: { + propDefinition: [ + posthog, + "projectId", + (c) => ({ + organizationId: c.organizationId, + }), + ], + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs"); + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + emitEvent(event) { + const meta = this.generateMeta(event); + this.$emit(event, meta); + }, + generateMeta(event) { + return { + id: event.uuid, + summary: `New ${event.event} event`, + ts: Date.parse(event.timestamp), + }; + }, + buildQuery(limit) { + const lastTs = this._getLastTs(); + let query = "SELECT * FROM events"; + if (lastTs) { + query += ` WHERE timestamp > '${lastTs}'`; + } + query += " ORDER BY timestamp DESC"; + if (limit) { + query += ` LIMIT ${limit}`; + } + return query; + }, + formatEvents(columns, results) { + return results.map((result) => { + const values = {}; + for (let i = 0; i < columns.length; i++) { + values[columns[i]] = result[i]; + } + return values; + }); + }, + async processEvent(limit) { + const { + columns, results, + } = await this.posthog.createQuery({ + projectId: this.projectId, + data: { + query: { + kind: "HogQLQuery", + query: this.buildQuery(limit), + }, + }, + }); + const events = this.formatEvents(columns, results); + if (!events?.length) { + return; + } + this._setLastTs(events[0].timestamp.slice(0, 19)); + events.reverse().forEach((event) => this.emitEvent(event)); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/postman/README.md b/components/postman/README.md new file mode 100644 index 0000000000000..fb7c6c5e580bf --- /dev/null +++ b/components/postman/README.md @@ -0,0 +1,11 @@ +# Overview + +The Postman API enables you to automate tasks within your Postman collections, such as running collections, fetching and updating environments, and integrating your API development workflow into your CI/CD pipeline. Using Pipedream, you can harness this functionality to create custom workflows that trigger on various events, process data, and connect with other apps, extending the capabilities of your API testing and development processes. + +# Example Use Cases + +- **Scheduled Collection Runs**: Use Pipedream to schedule and run Postman collections at regular intervals. You can analyze the results, send alerts if a test fails, or even trigger other workflows based on those results. + +- **Dynamic Environment Updates**: Automatically update Postman environment variables when changes occur in your apps. For instance, sync new user data from a CRM platform to Postman's environment, ensuring your API tests always use the latest data. + +- **CI/CD Integration**: Connect your Postman tests with your CI/CD pipeline. After every code commit, use Pipedream to trigger a workflow that runs a specific Postman collection and reports the status back to your CI/CD tool, such as GitHub Actions, ensuring that only tested code gets deployed. diff --git a/components/postmark/README.md b/components/postmark/README.md index 043151700aeaf..d0f0a7af310b7 100644 --- a/components/postmark/README.md +++ b/components/postmark/README.md @@ -1,19 +1,11 @@ # Overview -What Is the Postmark API? - The Postmark API enables developers to easily integrate programmatic emailing into their applications. With the API, developers can create, send, and track transactional emails that are designed to enhance user experience and user engagement. The API also provides developers with access to powerful analytics and insights, such as open/click rates, spam complaints, bounces, and unsubscribes. -### What Can You Build With the Postmark API? +# Example Use Cases + +- **Customer Support Ticket Acknowledgement**: When a new support ticket is created in your helpdesk software (e.g., Zendesk), trigger a workflow in Pipedream to send a confirmation email via Postmark, ensuring the customer receives an immediate and reliable acknowledgement of their query. -The Postmark API allows developers to easily incorporate automated email notifications into their applications. Some examples of what you can build using the API are: +- **Transactional Email for E-commerce**: After a new order is placed in an e-commerce platform like Shopify, use Pipedream to trigger an order confirmation email through Postmark. Include details such as order summary, shipment tracking, and expected delivery date to enhance customer experience. -- Welcome emails -- Password reset emails -- Email verification reminders -- Event notification emails -- Automated transactional emails -- Billing and invoicing emails -- Receipt emails -- Email newsletters -- Auto-responders and more! +- **User Behavior-Triggered Emails**: Combine Postmark with a CRM platform like HubSpot on Pipedream to send personalized emails based on user behavior. For instance, if a user signs up for a webinar or downloads a whitepaper, automatically send them a curated follow-up email sequence, improving engagement and conversion. diff --git a/components/postmark/actions/create-domain/create-domain.mjs b/components/postmark/actions/create-domain/create-domain.mjs index ad7d316f477c1..ea3154682769c 100644 --- a/components/postmark/actions/create-domain/create-domain.mjs +++ b/components/postmark/actions/create-domain/create-domain.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-create-domain", name: "Create Domain", description: "Create a new domain. [See the documentation](https://postmarkapp.com/developer/api/domains-api#create-domain)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/create-signature/create-signature.mjs b/components/postmark/actions/create-signature/create-signature.mjs index 09ee97ba63fde..8dff1b93f6aeb 100644 --- a/components/postmark/actions/create-signature/create-signature.mjs +++ b/components/postmark/actions/create-signature/create-signature.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-create-signature", name: "Create Sender Signature", description: "Create a new sender signature. [See the documentation](https://postmarkapp.com/developer/api/signatures-api#create-signature)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/delete-domain/delete-domain.mjs b/components/postmark/actions/delete-domain/delete-domain.mjs index e99d214e61c68..f87f4adf08402 100644 --- a/components/postmark/actions/delete-domain/delete-domain.mjs +++ b/components/postmark/actions/delete-domain/delete-domain.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-delete-domain", name: "Delete Domain", description: "Delete a specific domain. [See the documentation](https://postmarkapp.com/developer/api/domains-api#delete-domain)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/delete-signature/delete-signature.mjs b/components/postmark/actions/delete-signature/delete-signature.mjs index 67d161666170a..d0e0f54bd4d43 100644 --- a/components/postmark/actions/delete-signature/delete-signature.mjs +++ b/components/postmark/actions/delete-signature/delete-signature.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-delete-signature", name: "Delete Sender Signature", description: "Delete a specific sender signature. [See the documentation](https://postmarkapp.com/developer/api/signatures-api#delete-signature)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/get-bounce-counts/get-bounce-counts.mjs b/components/postmark/actions/get-bounce-counts/get-bounce-counts.mjs index b54f2f2369915..94eaef54442a7 100644 --- a/components/postmark/actions/get-bounce-counts/get-bounce-counts.mjs +++ b/components/postmark/actions/get-bounce-counts/get-bounce-counts.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-get-bounce-counts", name: "Get Bounce Counts", description: "Gets total counts of emails you've sent out that have been returned as bounced. [See the documentation](https://postmarkapp.com/developer/api/stats-api#bounce-counts)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/get-browser-usage/get-browser-usage.mjs b/components/postmark/actions/get-browser-usage/get-browser-usage.mjs index 0f4f4f60de42e..b4d985c736d15 100644 --- a/components/postmark/actions/get-browser-usage/get-browser-usage.mjs +++ b/components/postmark/actions/get-browser-usage/get-browser-usage.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-get-browser-usage", name: "Get Browser Usage", description: "Gets an overview of the browsers used to open links in your emails. This is only recorded when Link Tracking is enabled for that email. [See the documentation](https://postmarkapp.com/developer/api/stats-api#browser-usage)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/get-click-counts/get-click-counts.mjs b/components/postmark/actions/get-click-counts/get-click-counts.mjs index 4f60637812f44..85addbe853f41 100644 --- a/components/postmark/actions/get-click-counts/get-click-counts.mjs +++ b/components/postmark/actions/get-click-counts/get-click-counts.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-get-click-counts", name: "Get Click Counts", description: "Gets total counts of unique links that were clicked. [See the documentation](https://postmarkapp.com/developer/api/stats-api#click-counts)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/get-domain/get-domain.mjs b/components/postmark/actions/get-domain/get-domain.mjs index de3f09b42c2b4..c027b0a6b89fd 100644 --- a/components/postmark/actions/get-domain/get-domain.mjs +++ b/components/postmark/actions/get-domain/get-domain.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-get-domain", name: "Get Domain", description: "Gets all the details for a specific domain. [See the documentation](https://postmarkapp.com/developer/api/domains-api#domain)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/get-email-open-counts/get-email-open-counts.mjs b/components/postmark/actions/get-email-open-counts/get-email-open-counts.mjs index cb02b5d460988..7e35f2fd92211 100644 --- a/components/postmark/actions/get-email-open-counts/get-email-open-counts.mjs +++ b/components/postmark/actions/get-email-open-counts/get-email-open-counts.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-get-email-open-counts", name: "Get Email Open Counts", description: "Gets total counts of recipients who opened your emails. This is only recorded when open tracking is enabled for that email. [See the documentation](https://postmarkapp.com/developer/api/stats-api#email-open-counts)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/get-email-platform-usage/get-email-platform-usage.mjs b/components/postmark/actions/get-email-platform-usage/get-email-platform-usage.mjs index 955f43f0ae536..33bc07d98075e 100644 --- a/components/postmark/actions/get-email-platform-usage/get-email-platform-usage.mjs +++ b/components/postmark/actions/get-email-platform-usage/get-email-platform-usage.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-get-email-platform-usage", name: "Get Email Platform Usage", description: "Gets an overview of the platforms used to open your emails. This is only recorded when open tracking is enabled for that email. [See the documentation](https://postmarkapp.com/developer/api/stats-api#email-platform-usage)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/get-outbound-overview/get-outbound-overview.mjs b/components/postmark/actions/get-outbound-overview/get-outbound-overview.mjs index 99d090b8ded8f..bc1ba33be61f1 100644 --- a/components/postmark/actions/get-outbound-overview/get-outbound-overview.mjs +++ b/components/postmark/actions/get-outbound-overview/get-outbound-overview.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-get-outbound-overview", name: "Get Outbound Overview", description: "Gets a brief overview of statistics for all of your outbound email. [See the documentation](https://postmarkapp.com/developer/api/stats-api#overview)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/get-sent-counts/get-sent-counts.mjs b/components/postmark/actions/get-sent-counts/get-sent-counts.mjs index 52667f693a26b..70ea58fd6891c 100644 --- a/components/postmark/actions/get-sent-counts/get-sent-counts.mjs +++ b/components/postmark/actions/get-sent-counts/get-sent-counts.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-get-sent-counts", name: "Get Sent Counts", description: "Gets a total count of emails you've sent out. [See the documentation](https://postmarkapp.com/developer/api/stats-api#sent-counts)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/get-spam-complaints/get-spam-complaints.mjs b/components/postmark/actions/get-spam-complaints/get-spam-complaints.mjs index 31d02fe0f3242..8d6e7a5be8e0e 100644 --- a/components/postmark/actions/get-spam-complaints/get-spam-complaints.mjs +++ b/components/postmark/actions/get-spam-complaints/get-spam-complaints.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-get-spam-complaints", name: "Get Spam Complaints", description: "Gets a total count of recipients who have marked your email as spam. [See the documentation](https://postmarkapp.com/developer/api/stats-api#spam-complaints)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/get-tracked-email-counts/get-tracked-email-counts.mjs b/components/postmark/actions/get-tracked-email-counts/get-tracked-email-counts.mjs index 3098cff545f03..6dc3f8320bd1b 100644 --- a/components/postmark/actions/get-tracked-email-counts/get-tracked-email-counts.mjs +++ b/components/postmark/actions/get-tracked-email-counts/get-tracked-email-counts.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-get-tracked-email-counts", name: "Get Tracked Email Counts", description: "Gets a total count of emails you've sent with open tracking or link tracking enabled. [See the documentation](https://postmarkapp.com/developer/api/stats-api#email-tracked-count)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/list-domains/list-domains.mjs b/components/postmark/actions/list-domains/list-domains.mjs index 96fee5c5e1f55..85d64fe76ef95 100644 --- a/components/postmark/actions/list-domains/list-domains.mjs +++ b/components/postmark/actions/list-domains/list-domains.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-list-domains", name: "List Domains", description: "Gets a list of domains containing an overview of the domain and authentication status. [See the documentation](https://postmarkapp.com/developer/api/domains-api#list-domains)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/list-sender-signatures/list-sender-signatures.mjs b/components/postmark/actions/list-sender-signatures/list-sender-signatures.mjs index 12f8e28a0e3ac..7f57496261f0b 100644 --- a/components/postmark/actions/list-sender-signatures/list-sender-signatures.mjs +++ b/components/postmark/actions/list-sender-signatures/list-sender-signatures.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-list-sender-signatures", name: "List Sender Signatures", description: "Gets a list of sender signatures containing brief details associated with your account. [See the documentation](https://postmarkapp.com/developer/api/signatures-api#list-sender-signatures)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/resend-confirmation/resend-confirmation.mjs b/components/postmark/actions/resend-confirmation/resend-confirmation.mjs index b4c5eefffead7..291daa7e555e5 100644 --- a/components/postmark/actions/resend-confirmation/resend-confirmation.mjs +++ b/components/postmark/actions/resend-confirmation/resend-confirmation.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-resend-confirmation", name: "Resend Confirmation", description: "Resend a confirmation email for a specific sender signature. [See the documentation](https://postmarkapp.com/developer/api/signatures-api#resend-confirmation)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/rotate-dkim-keys/rotate-dkim-keys.mjs b/components/postmark/actions/rotate-dkim-keys/rotate-dkim-keys.mjs index 2f1e6e9dfb7f6..0b898ea6af166 100644 --- a/components/postmark/actions/rotate-dkim-keys/rotate-dkim-keys.mjs +++ b/components/postmark/actions/rotate-dkim-keys/rotate-dkim-keys.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-rotate-dkim-keys", name: "Rotate DKIM Keys", description: "Creates a new DKIM key to replace your current key. Until the new DNS entries are confirmed, the pending values will be in DKIMPendingHost and DKIMPendingTextValue fields. After the new DKIM value is verified in DNS, the pending values will migrate to DKIMTextValue and DKIMPendingTextValue and Postmark will begin to sign emails with the new DKIM key. [See the documentation](https://postmarkapp.com/developer/api/domains-api#rotate-dkim)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/send-batch-with-templates/send-batch-with-templates.mjs b/components/postmark/actions/send-batch-with-templates/send-batch-with-templates.mjs index 6320e6e611ee3..c760873756963 100644 --- a/components/postmark/actions/send-batch-with-templates/send-batch-with-templates.mjs +++ b/components/postmark/actions/send-batch-with-templates/send-batch-with-templates.mjs @@ -9,7 +9,7 @@ export default { key: "postmark-send-batch-with-templates", name: "Send Batch With Templates", description: "Send a batch of emails using a template [See the documentation](https://postmarkapp.com/developer/api/templates-api#send-batch-with-templates)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { postmark, diff --git a/components/postmark/actions/send-email-with-template/send-email-with-template.mjs b/components/postmark/actions/send-email-with-template/send-email-with-template.mjs index 3df083349cf19..f35b212131368 100644 --- a/components/postmark/actions/send-email-with-template/send-email-with-template.mjs +++ b/components/postmark/actions/send-email-with-template/send-email-with-template.mjs @@ -1,5 +1,4 @@ import common from "../common/common.mjs"; -import templateProps from "../common/templateProps.mjs"; const { postmark, ...props @@ -10,11 +9,37 @@ export default { key: "postmark-send-email-with-template", name: "Send Email With Template", description: "Send a single email with Postmark using a template [See the documentation](https://postmarkapp.com/developer/api/templates-api#email-with-template)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { postmark, - ...templateProps, + templateAlias: { + type: "string", + label: "Template", + description: "The template to use for this email.", + async options ({ page }) { + const data = await this.postmark.listTemplates(page); + const options = data.Templates?.map((obj) => { + return { + label: obj.Name, + value: obj.Alias, + }; + }) ?? []; + + return { + options, + context: { + page, + }, + }; + }, + }, + templateModel: { + type: "object", + label: "Template Model", + description: + "The model to be applied to the specified template to generate the email body and subject.", + }, ...props, inlineCss: { type: "boolean", diff --git a/components/postmark/actions/send-single-email/send-single-email.mjs b/components/postmark/actions/send-single-email/send-single-email.mjs index b631b5f033f95..2b6fcd4d07880 100644 --- a/components/postmark/actions/send-single-email/send-single-email.mjs +++ b/components/postmark/actions/send-single-email/send-single-email.mjs @@ -9,7 +9,7 @@ export default { key: "postmark-send-single-email", name: "Send Single Email", description: "Send a single email with Postmark [See the documentation](https://postmarkapp.com/developer/api/email-api#send-a-single-email)", - version: "0.2.2", + version: "0.2.3", type: "action", props: { postmark, diff --git a/components/postmark/actions/update-signature/update-signature.mjs b/components/postmark/actions/update-signature/update-signature.mjs index 44f67016b1b24..3b7d3df8409b9 100644 --- a/components/postmark/actions/update-signature/update-signature.mjs +++ b/components/postmark/actions/update-signature/update-signature.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-update-signature", name: "Update Sender Signature", description: "Create a new sender signature. [See the documentation](https://postmarkapp.com/developer/api/signatures-api#create-signature)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/verify-dkim/verify-dkim.mjs b/components/postmark/actions/verify-dkim/verify-dkim.mjs index 83d8aceac3a60..0f4a660d8107a 100644 --- a/components/postmark/actions/verify-dkim/verify-dkim.mjs +++ b/components/postmark/actions/verify-dkim/verify-dkim.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-verify-dkim", name: "Verify DKIM", description: "Verify DKIM keys for the specified domain. [See the documentation](https://postmarkapp.com/developer/api/domains-api#domains-verify-dkim)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/actions/verify-return-path/verify-return-path.mjs b/components/postmark/actions/verify-return-path/verify-return-path.mjs index aa04fe618ee12..3e36054d45ac9 100644 --- a/components/postmark/actions/verify-return-path/verify-return-path.mjs +++ b/components/postmark/actions/verify-return-path/verify-return-path.mjs @@ -4,7 +4,7 @@ export default { key: "postmark-verify-return-path", name: "Verify Return-Path DNS", description: "Verify Return-Path DNS record for the specified domain. [See the documentation](https://postmarkapp.com/developer/api/domains-api#domains-verify-return-path)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { postmark, diff --git a/components/postmark/package.json b/components/postmark/package.json index d754609505078..d43e0f39f7422 100644 --- a/components/postmark/package.json +++ b/components/postmark/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/postmark", - "version": "0.1.1", + "version": "0.1.2", "description": "Pipedream Postmark Components", "main": "postmark.app.mjs", "keywords": [ diff --git a/components/postmark/postmark.app.mjs b/components/postmark/postmark.app.mjs index d1202ced85874..2974e7afbab02 100644 --- a/components/postmark/postmark.app.mjs +++ b/components/postmark/postmark.app.mjs @@ -27,6 +27,28 @@ export default { }); }, }, + serverId: { + type: "string", + label: "Server Id", + description: "The identifier of the server.", + async options({ page }) { + const { Servers = [] } = await this.listServers({ + params: { + count: LIMIT, + offset: LIMIT * page, + }, + }); + + return Servers.map(({ + Name: label, ID: value, + }) => { + return { + label, + value, + }; + }); + }, + }, signatureId: { type: "string", label: "Signature Id", @@ -155,11 +177,14 @@ export default { ...opts, }); }, - async setServerInfo(data) { - return this.sharedRequest(this, { - endpoint: "server", - method: "put", - data, + setServerInfo({ + serverId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + tokenType: "account", + path: `servers/${serverId}`, + ...opts, }); }, createDataRemoval(opts = {}) { @@ -305,6 +330,13 @@ export default { ...opts, }); }, + listServers(opts = {}) { + return this._makeRequest({ + tokenType: "account", + path: "servers", + ...opts, + }); + }, resendConfirmation({ signatureId, ...opts }) { diff --git a/components/postmark/sources/new-domain/new-domain.mjs b/components/postmark/sources/new-domain/new-domain.mjs index 76c315d571daa..cdfb5a4900066 100644 --- a/components/postmark/sources/new-domain/new-domain.mjs +++ b/components/postmark/sources/new-domain/new-domain.mjs @@ -5,7 +5,7 @@ export default { key: "postmark-new-domain", name: "New Domain", description: "Emit new event when a new domain is created.", - version: "0.0.1", + version: "0.0.2", type: "source", methods: { ...common.methods, diff --git a/components/postmark/sources/new-email-opened/new-email-opened.mjs b/components/postmark/sources/new-email-opened/new-email-opened.mjs index d62b3564465ad..fae6417f1baa3 100644 --- a/components/postmark/sources/new-email-opened/new-email-opened.mjs +++ b/components/postmark/sources/new-email-opened/new-email-opened.mjs @@ -6,7 +6,7 @@ export default { name: "New Email Opened", description: "Emit new event when an email is opened by a recipient [(See docs here)](https://postmarkapp.com/developer/webhooks/open-tracking-webhook)", - version: "0.0.3", + version: "0.0.4", type: "source", props: { ...common.props, diff --git a/components/postmark/sources/new-inbound-email-received/new-inbound-email-received.mjs b/components/postmark/sources/new-inbound-email-received/new-inbound-email-received.mjs index 0d99fa05c7447..647980b22d7c4 100644 --- a/components/postmark/sources/new-inbound-email-received/new-inbound-email-received.mjs +++ b/components/postmark/sources/new-inbound-email-received/new-inbound-email-received.mjs @@ -4,10 +4,18 @@ export default { ...common, key: "postmark-new-inbound-email-received", name: "New Inbound Email Received", - description: - "Emit new event when an email is received by the Postmark server [(See docs here)](https://postmarkapp.com/developer/webhooks/inbound-webhook)", - version: "0.0.3", + description: "Emit new event when an email is received by the Postmark server. This source updates the server's inbound URL. You cannot create multiple inbound sources for the same server. [See the documentation](https://postmarkapp.com/developer/webhooks/inbound-webhook#set-the-webhook-url)", + version: "1.0.0", type: "source", + props: { + ...common.props, + serverId: { + propDefinition: [ + common.props.postmark, + "serverId", + ], + }, + }, methods: { ...common.methods, getWebhookProps() { @@ -19,4 +27,17 @@ export default { return `New email received! MessageID - ${body.MessageID}`; }, }, + hooks: { + async activate() { + await this.postmark.setServerInfo({ + serverId: this.serverId, + data: { + InboundHookUrl: this.http.endpoint, + }, + }); + }, + async deactivate() { + return true; + }, + }, }; diff --git a/components/postmark/sources/new-sender-signature/new-sender-signature.mjs b/components/postmark/sources/new-sender-signature/new-sender-signature.mjs index 6b70f4a396a69..983cb6fc71b01 100644 --- a/components/postmark/sources/new-sender-signature/new-sender-signature.mjs +++ b/components/postmark/sources/new-sender-signature/new-sender-signature.mjs @@ -5,7 +5,7 @@ export default { key: "postmark-new-sender-signature", name: "New Sender Signature", description: "Emit new event when a new sender signature is created.", - version: "0.0.1", + version: "0.0.2", type: "source", methods: { ...common.methods, diff --git a/components/power_automate/package.json b/components/power_automate/package.json new file mode 100644 index 0000000000000..1471d2b9c2427 --- /dev/null +++ b/components/power_automate/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/power_automate", + "version": "0.0.1", + "description": "Pipedream Power Automate Components", + "main": "power_automate.app.mjs", + "keywords": [ + "pipedream", + "power_automate" + ], + "homepage": "https://pipedream.com/apps/power_automate", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/power_automate/power_automate.app.mjs b/components/power_automate/power_automate.app.mjs new file mode 100644 index 0000000000000..fdb0db26fa7ed --- /dev/null +++ b/components/power_automate/power_automate.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "power_automate", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/practitest/README.md b/components/practitest/README.md index 9969bde5734e2..cd36470b06c9b 100644 --- a/components/practitest/README.md +++ b/components/practitest/README.md @@ -1,21 +1,11 @@ # Overview -The PractiTest API is an incredibly powerful tool that allows developers and businesses to quickly and easily create and customize software testing solutions. With the PractiTest API, you can add automation, management, and customization features to your existing development process, creating a more efficient and effective software testing platform. +PractiTest is a test management tool that facilitates the organization, tracking, and planning of software testing processes by integrating with Pipedream's automation capabilities. With the PractiTest API, you're able to manipulate test cases, sets, runs, and report on results programmatically. This powerful combination opens the door to automated workflows that enhance testing processes, improve bug tracking, and provide real-time feedback to your team. -By using the PractiTest API, the possibilities are endless. Here are a few examples of what you can build using the PractiTest API: +# Example Use Cases -- Automated test execution: Use the API to programmatically trigger test scripts, then process the outcomes and send results back to the PractiTest for reporting and further analysis. +- **Automated Test Case Creation**: Upon a new feature being tracked in your project management tool (like Jira), automatically create associated test cases in PractiTest. This ensures that nothing slips through the cracks and all features have corresponding test plans. -- Customizable dashboards: Create interactive dashboards from test results and displays that are customized to your needs. +- **Real-Time Bug Reporting**: Configure a workflow that listens for failed test instances in PractiTest. When a failure occurs, it automatically creates a detailed bug report in your issue tracker (like GitHub Issues), including test steps, environment details, and failure logs. -- Cross-platform functionality: With the API, you can integrate applications from different software platforms and compile test results into a single report. - -- Automated issue tracking: The API enables issue tracking, allowing you to track bug reports and feature requests from various sources. - -- Real-time collaboration: Communicate and collaborate in real-time with your team, setting up tasks and sharing data securely. - -- Customizable test plans: Design custom tests plans and ensure that they conform to specific requirements. - -- Security: Create secure authentication protocols and ensure that only authorized personnel can access the data. - -By using the PractiTest API, you can build customized and efficient software testing solutions that will help you increase quality assurance and reduce development time. +- **Test Results Broadcast**: After a test run is completed in PractiTest, trigger a workflow that sends the results to a Slack channel, keeping the whole team informed. Enhance communication and quickly address any critical issues discovered during testing. diff --git a/components/precisefp/README.md b/components/precisefp/README.md new file mode 100644 index 0000000000000..3c38f7af5a4a9 --- /dev/null +++ b/components/precisefp/README.md @@ -0,0 +1,11 @@ +# Overview + +The PreciseFP API integrates with Pipedream to automate workflows involving client data collection and management for financial professionals. By leveraging this API, you can streamline the process of gathering and updating client information, create trigger-based actions for client engagement, and sync data with other financial management tools. With Pipedream's serverless platform, these automations can run without manual intervention, connecting to countless other apps and services to create a seamless data flow. + +# Example Use Cases + +- **Sync Client Data with CRM Systems**: Automatically update client profiles in your Customer Relationship Management (CRM) system whenever there's a change in PreciseFP. This ensures that your sales and support teams always have the latest information without manual data entry. + +- **Client Onboarding and Workflow Automation**: Trigger a series of welcome emails and educational content to new clients when they complete a data form on PreciseFP. This can help in keeping clients engaged and informed right from the start of their journey with your services. + +- **Financial Data Consolidation**: Collect and merge financial data from PreciseFP with investment platforms or accounting software. Create a comprehensive view of client financial status, which can be used for reporting, analysis, or advising. diff --git a/components/prerender/README.md b/components/prerender/README.md index 4bd9987f88255..cf07567f4df78 100644 --- a/components/prerender/README.md +++ b/components/prerender/README.md @@ -1,18 +1,11 @@ # Overview - Prerender.io allows organizations to serve static content to bots, crawlers, and web search results. It enables users to build applications that can support SEO, analytics, and also provides a better user experience. With the Prerender.io API, developers can build: +Prerender.io is an API that enhances SEO by allowing servers to return fully rendered HTML pages to search engines and social media crawlers, ensuring that these services can index and display web content efficiently. Utilizing Prerender.io with Pipedream, developers can automate the caching and serving of rendered pages, monitor and manage the performance of their prerendered content, and integrate SEO enhancement processes into broader application workflows. -1. SEO-optimized webpages -2. Paginated Ajax sites -3. eCommerce applications -4. Single Page Applications (SPAs) -5. Rich content sites -6. Digital magazines -7. Interactive media sites -8. Social media sites -9. Content management systems -10. Portals -11. Intranets -12. Private web servers -13. Public web servers -14. And more! +# Example Use Cases + +- **Automated Page Prerendering for New Content**: When new content is published on a CMS like WordPress or Contentful, a Pipedream automation triggers Prerender.io to prerender the new page. This ensures that the latest content is immediately ready for search engine indexing, maintaining SEO freshness. + +- **SEO Monitoring and Alerting**: Schedule a Pipedream workflow that periodically uses Prerender.io to check the rendering status of key pages. Combine with Pipedream's email or Slack integrations to send alerts if a page isn't rendering correctly, enabling quick fixes to maintain SEO performance. + +- **Social Media Sharing Optimization**: In response to new blog posts or product listings, trigger Prerender.io via Pipedream to generate and cache the prerendered pages. Then, use Pipedream to automatically post the URLs to social platforms like Twitter or Facebook, ensuring that link previews are populated with the correct images and descriptions. diff --git a/components/prerender/package.json b/components/prerender/package.json new file mode 100644 index 0000000000000..57e3c16f45092 --- /dev/null +++ b/components/prerender/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/prerender", + "version": "0.6.0", + "description": "Pipedream prerender Components", + "main": "prerender.app.mjs", + "keywords": [ + "pipedream", + "prerender" + ], + "homepage": "https://pipedream.com/apps/prerender", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/pretix/.gitignore b/components/pretix/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/pretix/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/pretix/README.md b/components/pretix/README.md index 01cb5231e69a5..6613ad96ba023 100644 --- a/components/pretix/README.md +++ b/components/pretix/README.md @@ -1,16 +1,11 @@ # Overview -Welcome to Pretix, a powerful API enabling you to integrate ticketing into any application or service with ease. The Pretix API lets you access data from your ticket shop, such as orders, attendees, or events, and use the data to build powerful apps, custom integrations, analytics, and more. +The pretix API enables seamless integration of your event management tasks by automating ticket sales, attendee management, and event analytics. With Pipedream's capabilities, you can build custom workflows that respond to various pretix events, synchronize attendee data with other services, or analyze sales patterns for actionable insights. -With the Pretix API, you can build: +# Example Use Cases -1. Custom ticket checkout experiences -2. Automated delivery of tickets to customers -3. Custom analytics dashboards -4. Third-party app integrations -5. Automated marketing campaigns -6. Real-time ticket scanning and attendance tracking solutions -7. Event management tools -8. Custom invoicing and cash flow solutions -9. Automated customer support solutions -10. Advanced fraud detection and prevention features +- **Synchronize Attendee Data with CRM Systems**: Automatically update contact lists in your CRM whenever new attendees register for an event on pretix. This can ensure that your sales team has the latest information and can follow up with personalized communication. + +- **Real-time Notification on Ticket Sales**: Set up notifications to be sent via Slack, email, or SMS when a certain number of tickets are sold or when a ticket for a high-demand event is purchased. This helps you stay informed about sales milestones and inventory. + +- **Post-Event Feedback Collection**: After an event concludes, trigger a workflow to send out surveys via email or another platform to gather feedback from attendees. Use the responses to improve future event planning and attendee satisfaction. diff --git a/components/pretix/actions/get-order-details/get-order-details.mjs b/components/pretix/actions/get-order-details/get-order-details.mjs new file mode 100644 index 0000000000000..5d43b9a7108ac --- /dev/null +++ b/components/pretix/actions/get-order-details/get-order-details.mjs @@ -0,0 +1,57 @@ +import pretix from "../../pretix.app.mjs"; + +export default { + key: "pretix-get-order-details", + name: "Get Order Details", + version: "0.0.1", + description: "Returns information on one order, identified by its order code. [See the documentation](https://docs.pretix.eu/en/latest/api/resources/orders.html#fetching-individual-orders)", + type: "action", + props: { + pretix, + organizerSlug: { + propDefinition: [ + pretix, + "organizerSlug", + ], + }, + eventSlug: { + propDefinition: [ + pretix, + "eventSlug", + ({ organizerSlug }) => ({ + organizerSlug, + }), + ], + }, + orderCode: { + propDefinition: [ + pretix, + "orderCode", + ({ + organizerSlug, eventSlug, + }) => ({ + organizerSlug, + eventSlug, + }), + ], + }, + }, + async run({ $ }) { + const { + pretix, + organizerSlug, + eventSlug, + orderCode, + } = this; + + const response = await pretix.getOrderDetails({ + $, + organizerSlug, + eventSlug, + orderCode, + }); + + $.export("$summary", `The order with code: ${orderCode} was successfully fetched!`); + return response; + }, +}; diff --git a/components/pretix/actions/update-event/update-event.mjs b/components/pretix/actions/update-event/update-event.mjs new file mode 100644 index 0000000000000..0631488fbb052 --- /dev/null +++ b/components/pretix/actions/update-event/update-event.mjs @@ -0,0 +1,164 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import pretix from "../../pretix.app.mjs"; + +export default { + key: "pretix-update-event", + name: "Update Event", + version: "0.0.1", + description: "Updates a specific event. [See the documentation](https://docs.pretix.eu/en/latest/api/resources/events.html#patch--api-v1-organizers-(organizer)-events-(event)-)", + type: "action", + props: { + pretix, + organizerSlug: { + propDefinition: [ + pretix, + "organizerSlug", + ], + }, + eventSlug: { + propDefinition: [ + pretix, + "eventSlug", + ({ organizerSlug }) => ({ + organizerSlug, + }), + ], + }, + name: { + type: "string", + label: "Name", + description: "The event's full name", + optional: true, + }, + live: { + type: "boolean", + label: "Live", + description: "If **true**, the event ticket shop is publicly available.", + optional: true, + }, + testmode: { + type: "boolean", + label: "Test Mode", + description: "If **true**, the ticket shop is in test mode.", + optional: true, + }, + currency: { + type: "string", + label: "Currency", + description: "The currency this event is handled in.", + optional: true, + }, + dateFrom: { + type: "string", + label: "Date From", + description: "The event's start date.", + optional: true, + }, + dateTo: { + type: "string", + label: "Date To", + description: "The event's end date.", + optional: true, + }, + dateAdmission: { + type: "string", + label: "Date Admission", + description: "The event's admission date.", + optional: true, + }, + isPublic: { + type: "boolean", + label: "Is Public", + description: "If **true**, the event shows up in places like the organizer's public list of events", + optional: true, + }, + presaleStart: { + type: "string", + label: "Presale Start", + description: "The date at which the ticket shop opens.", + optional: true, + }, + presaleEnd: { + type: "string", + label: "Presale End", + description: "The date at which the ticket shop closes.", + optional: true, + }, + location: { + type: "string", + label: "Location", + description: "The event location.", + optional: true, + }, + getLat: { + type: "string", + label: "Geo Lat", + description: "Latitude of the location.", + optional: true, + }, + getLon: { + type: "string", + label: "Geo Lon", + description: "Longitude of the location.", + optional: true, + }, + timezone: { + type: "string", + label: "Timezone", + description: "Event timezone name.", + optional: true, + }, + itemMetaProperties: { + type: "object", + label: "Item Meta Properties", + description: "Item-specific meta data parameters and default values.", + optional: true, + }, + }, + async run({ $ }) { + const { + pretix, + organizerSlug, + eventSlug, + dateFrom, + dateTo, + dateAdmission, + isPublic, + presaleStart, + presaleEnd, + getLat, + getLon, + metaData, + itemMetaProperties, + ...data + } = this; + + try { + const response = await pretix.updateEvent({ + $, + organizerSlug, + eventSlug, + data: { + date_from: dateFrom, + date_to: dateTo, + date_admission: dateAdmission, + is_public: isPublic, + presale_start: presaleStart, + presale_end: presaleEnd, + geo_lat: getLat, + geo_lon: getLon, + meta_data: metaData && parseObject(metaData), + item_meta_properties: itemMetaProperties && parseObject(itemMetaProperties), + ...data, + }, + }); + + $.export("$summary", `The event with slug: ${response.slug} was successfully updated!`); + return response; + } catch (e) { + const message = Object.values(e.response.data)[0][0]; + throw new ConfigurationError(message.replace("a href=\"", "a href=\"https://pretix.eu" )); + } + }, +}; diff --git a/components/pretix/app/pretix.app.ts b/components/pretix/app/pretix.app.ts deleted file mode 100644 index 3be2c1ffd8e97..0000000000000 --- a/components/pretix/app/pretix.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "pretix", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); diff --git a/components/pretix/common/utils.mjs b/components/pretix/common/utils.mjs new file mode 100644 index 0000000000000..154701004e72a --- /dev/null +++ b/components/pretix/common/utils.mjs @@ -0,0 +1,18 @@ +export const parseObject = (obj) => { + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + return JSON.parse(obj); + } + return obj; +}; diff --git a/components/pretix/package.json b/components/pretix/package.json index 793d1b7cab23d..8fe42c82aa50c 100644 --- a/components/pretix/package.json +++ b/components/pretix/package.json @@ -1,18 +1,19 @@ { "name": "@pipedream/pretix", - "version": "0.0.3", + "version": "0.1.0", "description": "Pipedream pretix Components", - "main": "dist/app/pretix.app.mjs", + "main": "pretix.app.mjs", "keywords": [ "pipedream", "pretix" ], - "files": [ - "dist" - ], "homepage": "https://pipedream.com/apps/pretix", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" } } + diff --git a/components/pretix/pretix.app.mjs b/components/pretix/pretix.app.mjs new file mode 100644 index 0000000000000..5c2d7489af911 --- /dev/null +++ b/components/pretix/pretix.app.mjs @@ -0,0 +1,130 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "pretix", + propDefinitions: { + organizerSlug: { + type: "string", + label: "Organizer Slug", + description: "The slug of the organizer.", + async options({ page }) { + const { results } = await this.listOrganizers({ + params: { + page: page + 1, + }, + }); + + return results.map(({ + slug: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + eventSlug: { + type: "string", + label: "Event Slug", + description: "The slug of the event.", + async options({ + page, organizerSlug, + }) { + const { results } = await this.listEvents({ + organizerSlug, + params: { + page: page + 1, + }, + }); + + return results.map(({ + slug: value, name, + }) => ({ + label: Object.values(name)[0], + value, + })); + }, + }, + orderCode: { + type: "string", + label: "Order Code", + description: "The code of the order.", + async options({ + page, organizerSlug, eventSlug, + }) { + const { results } = await this.listOrders({ + organizerSlug, + eventSlug, + params: { + page: page + 1, + }, + }); + + return results.map(({ + code: value, email: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _apiUrl() { + return "https://pretix.eu/api/v1"; + }, + _getHeaders() { + return { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: `${this._apiUrl()}/${path}`, + headers: this._getHeaders(), + ...opts, + }); + }, + listOrganizers(opts = {}) { + return this._makeRequest({ + path: "organizers", + ...opts, + }); + }, + listEvents({ + organizerSlug, ...opts + }) { + return this._makeRequest({ + path: `organizers/${organizerSlug}/events`, + ...opts, + }); + }, + listOrders({ + organizerSlug, eventSlug, ...opts + }) { + return this._makeRequest({ + path: `organizers/${organizerSlug}/events/${eventSlug}/orders`, + ...opts, + }); + }, + getOrderDetails({ + organizerSlug, eventSlug, orderCode, ...opts + }) { + return this._makeRequest({ + path: `organizers/${organizerSlug}/events/${eventSlug}/orders/${orderCode}`, + ...opts, + }); + }, + updateEvent({ + organizerSlug, eventSlug, ...opts + }) { + return this._makeRequest({ + method: "PATCH", + path: `organizers/${organizerSlug}/events/${eventSlug}/`, + ...opts, + }); + }, + }, +}; diff --git a/components/printautopilot/README.md b/components/printautopilot/README.md new file mode 100644 index 0000000000000..16c204fe10eed --- /dev/null +++ b/components/printautopilot/README.md @@ -0,0 +1,11 @@ +# Overview + +The PrintAutopilot API facilitates the automation of print-on-demand services, allowing you to seamlessly create, manage, and track custom print orders right from within your Pipedream workflows. With this API, you can trigger printing and shipping of products like t-shirts, mugs, and posters as soon as a customer places an order, or batch process orders to optimize fulfillment. In Pipedream, harness these capabilities to build integrated workflows that connect PrintAutopilot with eCommerce platforms, CRM systems, or customer support tools to streamline your print-on-demand business. + +# Example Use Cases + +- **Order Processing Automation**: Trigger a workflow in Pipedream when a new order is received on Shopify. Validate the order details, then use the PrintAutopilot API to submit the order for printing. Finally, update the order status in Shopify and notify the customer via email. + +- **Customer Support Ticket Printing**: On receiving a support ticket in Zendesk that requires printed material as a resolution, automatically process the request using the PrintAutopilot API. Once the printing job is completed and shipped, update the ticket in Zendesk with the tracking information. + +- **Inventory and Sales Reporting**: Combine data from PrintAutopilot and Stripe to generate a daily sales and inventory report. Set up a Pipedream workflow that fetches new transactions from Stripe, checks product availability via PrintAutopilot, and compiles a report that's emailed to your team daily. diff --git a/components/printavo/README.md b/components/printavo/README.md index 9a463534a606b..8fed32156fd6c 100644 --- a/components/printavo/README.md +++ b/components/printavo/README.md @@ -1,20 +1,11 @@ # Overview -What Can You Create With the Printavo API? +The Printavo API allows for the automation of print shop management tasks, enabling users to streamline their operations by integrating their Printavo account with various other apps and services. With Pipedream's serverless platform, you can create workflows that trigger actions in Printavo or respond to events from Printavo, such as updating order statuses, syncing customer data, or automating notifications and reminders, all without writing complex code. -The Printavo API provides a powerful and feature-rich set of tools to integrate with any software platform or service. With the API, you can create custom integrations that can help automate workflow, track and manage orders, generate customer invoices, and much more. +# Example Use Cases -The possibilities for what you can create using the Printavo API are endless, and a few of the potential uses include: +- **Automated Order Processing Workflow**: When a new order is placed on your e-commerce platform, a Pipedream workflow can trigger, automatically creating a corresponding job in Printavo, assigning it to the appropriate staff member, and sending an order confirmation email to the customer using the SendGrid app. -- Automated customer order processing. -- Creation of custom checkout flows that link directly to Printavo. -- Management of orders, status updates, and customer communications. -- Integration of inventory management systems. -- Automated invoice generation. -- Ability to access and update customer artwork. -- Tracking of customer usage data. -- Automatic triggering of order acknowledgements. -- Back-end integration with existing print services. -- And even more. +- **Customer Feedback Collection**: After an order status is updated to 'Completed' in Printavo, a Pipedream workflow can send a follow-up email through Mailchimp to the customer asking for feedback or a review, helping to enhance customer engagement and gather valuable insights. -By using the Printavo API, you can create unique and useful integrations to simplify your workflow and better satisfy your customer's needs. +- **Inventory Reconciliation**: Schedule a regular Pipedream workflow that checks current inventory levels in Printavo and compares them with Shopify inventory. If discrepancies are found, the workflow can adjust Shopify inventory counts to match Printavo's records, ensuring accurate stock levels and preventing over-selling. diff --git a/components/printful/README.md b/components/printful/README.md index 67cdfe9941ea9..1bdae033c4ab5 100644 --- a/components/printful/README.md +++ b/components/printful/README.md @@ -1,18 +1,11 @@ # Overview -Using the Printful API, you can build a wide variety of applications that allow you to customize and create products. With the Printful API, you can securely and easily integrate a powerful, automated product fulfillment service into your existing website or application. Here are some examples of what you can build with the Printful API: +The Printful API unlocks the ability to automate custom print-on-demand operations within Pipedream's serverless platform. With this API, you can streamline the process of managing products, submitting orders, and syncing inventory. Building workflows around the Printful API on Pipedream can significantly reduce manual intervention, connect your e-commerce data with other services, and enhance customer experiences through timely fulfillment and updates. -- Design and deliver custom apparel and merchandise such as t-shirts, hats, mugs, and phone cases. -- Create personalized products with your designs or photos, providing a unique experience for customers. -- Generate 3D product previews so customers can see what their product will look like before ordering. -- Automate the entire product design, printing, shipping, and tracking process. -- Integrate with popular ecommerce platforms like Shopify, WooCommerce, and BigCommerce. -- Reprint, retag, and re-brand merchandise. -- Generate product mockups to enhance your marketing campaigns. -- Automatically sync products, inventories and orders. -- Keep better track of orders with real-time tracking updates. -- Integrate with popular payment providers such as Stripe, PayPal and Square. -- Easily access reports for data analysis. -- View and export store order history and more. +# Example Use Cases -With the Printful API, you can create all sorts of amazing products and services that give customers the best quality experience. From custom apparel and merchandise to personalized products to 3D previews, you can use the Printful API to create amazing products that customers will love. +- **Order Management Workflow**: Sync orders from your e-commerce platform to Printful automatically. Whenever a new order is placed on your Shopify store, trigger a Pipedream workflow that submits the order details to Printful for fulfillment. This ensures seamless order processing without manual data entry. + +- **Inventory Syncing Automation**: Keep your store inventory up-to-date by integrating Printful with your inventory management system. Set up a Pipedream workflow that periodically checks Printful stock levels and updates the inventory counts on platforms like WooCommerce, preventing overselling and maintaining accurate stock information. + +- **Customer Notification System**: Enhance customer service by notifying customers about the status of their orders. Create a Pipedream workflow that listens for status updates from Printful, such as shipped or delivered, and then sends a personalized email or SMS via SendGrid or Twilio to the customer, keeping them informed in real-time. diff --git a/components/printful/actions/calculate-shipping-rates/calculate-shipping-rates.mjs b/components/printful/actions/calculate-shipping-rates/calculate-shipping-rates.mjs new file mode 100644 index 0000000000000..a2aaeb7dce023 --- /dev/null +++ b/components/printful/actions/calculate-shipping-rates/calculate-shipping-rates.mjs @@ -0,0 +1,92 @@ +import { LOCALE_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import printful from "../../printful.app.mjs"; + +export default { + key: "printful-calculate-shipping-rates", + name: "Calculate Shipping Rates", + description: "Fetches available shipping rates for a given destination. [See the documentation](https://developers.printful.com/docs/#tag/Shipping-Rate-API/operation/calculateShippingRates)", + version: "0.0.1", + type: "action", + props: { + printful, + storeId: { + propDefinition: [ + printful, + "storeId", + ], + }, + address1: { + label: "Address 1", + description: "The address 1", + type: "string", + }, + city: { + label: "City", + description: "The city", + type: "string", + }, + stateCode: { + label: "State Code", + description: "The state code. E.g. `SC`", + type: "string", + }, + countryCode: { + label: "Country Code", + description: "The country code. E.g. `BR`", + type: "string", + }, + zip: { + label: "ZIP/Postal Code", + description: "The ZIP/postal code. E.g. `89221525`", + type: "string", + }, + phone: { + label: "Phone", + description: "The phone number", + type: "string", + optional: true, + }, + items: { + type: "string[]", + label: "Items", + description: "A list of items in JSON format. **Example: [{\"variant_id\": \"123456\", \"external_variant_id\": \"123456\", \"quantity\": 123, \"value\": \"12345\" }]**", + }, + currency: { + type: "string", + label: "Currency", + description: "3 letter currency code (optional), required if the rates need to be converted to another currency instead of store default currency", + optional: true, + }, + locale: { + type: "string", + label: "Locale", + description: "Locale in which shipping rate names will be returned", + options: LOCALE_OPTIONS, + optional: true, + }, + }, + async run({ $ }) { + const shippingRates = await this.printful.fetchShippingRates({ + $, + headers: { + "X-PF-Store-Id": this.storeId, + }, + data: { + recipient: { + address1: this.address1, + city: this.city, + state_code: this.stateCode, + country_code: this.countryCode, + zip: this.zip, + phone: this.phone, + }, + items: parseObject(this.items), + currency: this.currency, + locale: this.locale, + }, + }); + $.export("$summary", "Fetched shipping rates successfully"); + return shippingRates; + }, +}; diff --git a/components/printful/actions/create-order/create-order.mjs b/components/printful/actions/create-order/create-order.mjs new file mode 100644 index 0000000000000..4338776e08d03 --- /dev/null +++ b/components/printful/actions/create-order/create-order.mjs @@ -0,0 +1,144 @@ +import { parseObject } from "../../common/utils.mjs"; +import printful from "../../printful.app.mjs"; + +export default { + name: "Create Order", + version: "0.0.1", + key: "printful-create-order", + description: "Creates a new order in your Printful account. [See the documentaion](https://developers.printful.com/docs/#operation/createOrder)", + type: "action", + props: { + printful, + storeId: { + propDefinition: [ + printful, + "storeId", + ], + }, + externalId: { + label: "External ID", + description: "Order ID from the external system", + type: "string", + optional: true, + }, + recipientName: { + label: "Recipient Name", + description: "The recipient full name", + type: "string", + }, + recipientCompanyName: { + label: "Recipient Company Name", + description: "The recipient company name", + type: "string", + optional: true, + }, + address1: { + label: "Address 1", + description: "The address 1", + type: "string", + }, + address2: { + label: "Address 2", + description: "The address 2", + type: "string", + optional: true, + }, + city: { + label: "City", + description: "The city", + type: "string", + }, + stateCode: { + label: "State Code", + description: "The state code. E.g. `SC`", + type: "string", + }, + countryCode: { + label: "Country Code", + description: "The country code. E.g. `BR`", + type: "string", + reloadProps: true, + }, + taxNumber: { + label: "Tax Number", + description: "TAX number (`optional`, but in case of Brazil country this field becomes `required` and will be used as CPF/CNPJ number)", + type: "string", + optional: true, + }, + zip: { + label: "ZIP/Postal Code", + description: "The ZIP/postal code. E.g. `89221525`", + type: "string", + reloadProps: true, + }, + phone: { + label: "Phone", + description: "The phone number", + type: "string", + optional: true, + }, + email: { + label: "Email", + description: "The email", + type: "string", + optional: true, + }, + giftSubject: { + label: "Gift Subject", + description: "Gift message title", + type: "string", + optional: true, + }, + giftMessage: { + label: "Gift Message", + description: "Gift message text", + type: "string", + optional: true, + }, + items: { + label: "Items", + description: "Array of items in the order. E.g. `[ { \"id\": 1, \"variant_id\": 2, \"quantity\": 3, \"price\": \"13.60\", \"retail_price\": \"9.90\", \"name\": \"Beauty Poster\", \"product\": { \"product_id\": 301, \"variant_id\": 500, \"name\": \"Red T-Shirt\" }, \"files\": [{\"url\": \"https://file.com/060b204a37f.png\"}] } ]`", + type: "string[]", + }, + }, + additionalProps(props) { + props.taxNumber.optional = this.countryCode != "BR"; + return {}; + }, + async run({ $ }) { + const response = await this.printful.createOrder({ + $, + headers: { + "X-PF-Store-Id": this.storeId, + }, + data: { + external_id: this.externalId, + shipping: "STANDARD", + recipient: { + name: this.recipientName, + company: this.recipientCompanyName, + address1: this.address1, + address2: this.address2, + city: this.city, + state_code: this.stateCode, + country_code: this.countryCode, + zip: this.zip, + phone: this.phone, + email: this.email, + tax_number: this.taxNumber, + }, + gift: { + subject: this.giftSubject, + message: this.giftMessage, + }, + items: parseObject(this.items), + }, + }); + + if (response) { + $.export("$summary", `Successfully created order with id ${response.result.id}`); + } + + return response; + }, +}; diff --git a/components/printful/actions/update-product/update-product.mjs b/components/printful/actions/update-product/update-product.mjs new file mode 100644 index 0000000000000..31843cc4f5f36 --- /dev/null +++ b/components/printful/actions/update-product/update-product.mjs @@ -0,0 +1,63 @@ +import { parseObject } from "../../common/utils.mjs"; +import printful from "../../printful.app.mjs"; + +export default { + key: "printful-update-product", + name: "Update Product", + description: "Updates an existing product in your Printful store. [See the documentation](https://developers.printful.com/docs/#tag/Products-API/operation/updateSyncProduct)", + version: "0.0.1", + type: "action", + props: { + printful, + storeId: { + propDefinition: [ + printful, + "storeId", + ], + }, + productId: { + propDefinition: [ + printful, + "productId", + ({ storeId }) => ({ + storeId, + }), + ], + }, + alert: { + type: "alert", + alertType: "warning", + content: `Please note that in the request body you only need to specify the fields that need to be changed. + \nFurthermore, if you want to update existing sync variants, then in the sync variants array you must specify the IDs of all existing sync variants. + \nAll omitted existing sync variants will be deleted. All new sync variants without an ID will be created.`, + }, + syncProduct: { + type: "object", + label: "Sync Product", + description: "Information about the SyncProduct. **Example: {\"external_id\": \"4235234213\", \"name\": \"T-shirt\", \"thumbnail\": \"http://your-domain.com/path/to/thumbnail.png\", \"is_ignored\": true}**", + optional: true, + }, + syncVariants: { + type: "string[]", + label: "Sync Variants", + description: "Information about the Sync Variants. **Example: [{\"external_id\": \"12312414\", \"variant_id\": 3001, \"retail_price\": \"29.99\", \"is_ignored\": true, \"sku\": \"SKU1234\", \"files\": [{ \"type\": \"default\", \"url\": \"​https://www.example.com/files/tshirts/example.png\", \"options\": [{ \"id\": \"template_type\", \"value\": \"native\" }], \"filename\": \"shirt1.png\", \"visible\": true }], \"options\": [{ \"id\": \"embroidery_type\", \"value\": \"flat\" }], \"availability_status\": \"active\" }]**", + optional: true, + }, + }, + async run({ $ }) { + + const response = await this.printful.updateProduct({ + $, + headers: { + "X-PF-Store-Id": this.storeId, + }, + productId: this.productId, + data: { + sync_product: parseObject(this.syncProduct), + sync_variants: parseObject(this.syncVariants), + }, + }); + $.export("$summary", `Updated product ${this.productId}`); + return response; + }, +}; diff --git a/components/printful/common/constants.mjs b/components/printful/common/constants.mjs new file mode 100644 index 0000000000000..2591c9bc759fb --- /dev/null +++ b/components/printful/common/constants.mjs @@ -0,0 +1,11 @@ +export const LIMIT = 100; +export const LOCALE_OPTIONS = [ + { + label: "English (US)", + value: "en_US", + }, + { + label: "Spanish (Spain)", + value: "es_ES", + }, +]; diff --git a/components/printful/common/utils.mjs b/components/printful/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/printful/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/printful/package.json b/components/printful/package.json new file mode 100644 index 0000000000000..f4b8d85c67412 --- /dev/null +++ b/components/printful/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/printful", + "version": "0.1.0", + "description": "Pipedream Printful Components", + "main": "printful.app.mjs", + "keywords": [ + "pipedream", + "printful" + ], + "homepage": "https://pipedream.com/apps/printful", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/printful/printful.app.mjs b/components/printful/printful.app.mjs index fa42a5dcdfc21..0566f37c3a737 100644 --- a/components/printful/printful.app.mjs +++ b/components/printful/printful.app.mjs @@ -1,11 +1,123 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + export default { type: "app", app: "printful", - propDefinitions: {}, + propDefinitions: { + storeId: { + type: "string", + label: "Store ID", + description: "Select a store or provide s store ID to use for the request", + async options({ page }) { + const { result } = await this.listStores({ + params: { + offset: LIMIT * page, + limit: LIMIT, + }, + }); + + return result.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + productId: { + type: "string", + label: "Product ID", + description: "Sync Product ID (integer) or External ID (if prefixed with @)", + async options({ + storeId, page, + }) { + const { result } = await this.listProducts({ + headers: { + "X-PF-Store-Id": storeId, + }, + params: { + offset: LIMIT * page, + limit: LIMIT, + }, + }); + + return result.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.printful.com"; + }, + _headers(headers = {}) { + return { + ...headers, + authorization: `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(headers), + ...opts, + }); + }, + createOrder(opts = {}) { + return this._makeRequest({ + path: "/orders", + method: "post", + ...opts, + }); + }, + listStores(opts = {}) { + return this._makeRequest({ + path: "/stores", + ...opts, + }); + }, + listProducts(opts = {}) { + return this._makeRequest({ + path: "/store/products", + ...opts, + }); + }, + fetchShippingRates(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/shipping/rates", + ...opts, + }); + }, + updateProduct({ + productId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/store/products/${productId}`, + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteWebhook(opts = {}) { + return this._makeRequest({ + method: "DELETE", + path: "/webhooks", + ...opts, + }); }, }, }; diff --git a/components/printful/sources/common/base.mjs b/components/printful/sources/common/base.mjs new file mode 100644 index 0000000000000..9b3904c1a623d --- /dev/null +++ b/components/printful/sources/common/base.mjs @@ -0,0 +1,52 @@ +import printful from "../../printful.app.mjs"; + +export default { + props: { + printful, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + alert: { + type: "alert", + alertType: "warning", + content: "**Note** that only one webhook URL can be active for a store, so all existing webhook configuration will be disabled upon creating this source.", + }, + storeId: { + propDefinition: [ + printful, + "storeId", + ], + }, + }, + hooks: { + async activate() { + await this.printful.createWebhook({ + headers: { + "X-PF-Store-Id": this.storeId, + }, + data: { + url: this.http.endpoint, + types: this.getEventType(), + }, + }); + }, + async deactivate() { + await this.printful.deleteWebhook({ + headers: { + "X-PF-Store-Id": this.storeId, + }, + }); + }, + }, + async run({ body }) { + const modelField = this.getModelField(); + const ts = body.created; + this.$emit(body, { + id: `${body.data[modelField].id}-${ts}`, + summary: this.getSummary(body), + ts, + }); + }, +}; diff --git a/components/printful/sources/new-order-instant/new-order-instant.mjs b/components/printful/sources/new-order-instant/new-order-instant.mjs new file mode 100644 index 0000000000000..0ad9abd5714cf --- /dev/null +++ b/components/printful/sources/new-order-instant/new-order-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "printful-new-order-instant", + name: "New Order Created (Instant)", + description: "Emit new event when a new order is created in your Printful account.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return [ + "order_created", + ]; + }, + getModelField() { + return "order"; + }, + getSummary(body) { + return `New order: ${body.data.order.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/printful/sources/new-order-instant/test-event.mjs b/components/printful/sources/new-order-instant/test-event.mjs new file mode 100644 index 0000000000000..3d06351d72e66 --- /dev/null +++ b/components/printful/sources/new-order-instant/test-event.mjs @@ -0,0 +1,217 @@ +export default { + "type": "order_created", + "created": 1622456737, + "retries": 2, + "store": 12, + "data": { + "order": { + "id": 13, + "external_id": "4235234213", + "store": 10, + "status": "draft", + "shipping": "STANDARD", + "shipping_service_name": "Flat Rate (3-4 business days after fulfillment)", + "created": 1602607640, + "updated": 1602607640, + "recipient": { + "name": "John Smith", + "company": "John Smith Inc", + "address1": "19749 Dearborn St", + "address2": "string", + "city": "Chatsworth", + "state_code": "CA", + "state_name": "California", + "country_code": "US", + "country_name": "United States", + "zip": "91311", + "phone": "2312322334", + "email": "firstname.secondname@domain.com", + "tax_number": "123.456.789-10" + }, + "items": [ + { + "id": 1, + "external_id": "item-1", + "variant_id": 1, + "sync_variant_id": 1, + "external_variant_id": "variant-1", + "warehouse_product_variant_id": 1, + "product_template_id": 1, + "quantity": 1, + "price": "13.00", + "retail_price": "13.00", + "name": "Enhanced Matte Paper Poster 18×24", + "product": { + "variant_id": 3001, + "product_id": 301, + "image": "https://files.cdn.printful.com/products/71/5309_1581412541.jpg", + "name": "Bella + Canvas 3001 Unisex Short Sleeve Jersey T-Shirt with Tear Away Label (White / 4XL)" + }, + "files": [ + { + "type": "default", + "id": 10, + "url": "​https://www.example.com/files/tshirts/example.png", + "options": [ + { + "id": "template_type", + "value": "native" + } + ], + "hash": "ea44330b887dfec278dbc4626a759547", + "filename": "shirt1.png", + "mime_type": "image/png", + "size": 45582633, + "width": 1000, + "height": 1000, + "dpi": 300, + "status": "ok", + "created": 1590051937, + "thumbnail_url": "https://files.cdn.printful.com/files/ea4/ea44330b887dfec278dbc4626a759547_thumb.png", + "preview_url": "https://files.cdn.printful.com/files/ea4/ea44330b887dfec278dbc4626a759547_thumb.png", + "visible": true, + "is_temporary": false + } + ], + "options": [ + { + "id": "OptionKey", + "value": "OptionValue" + } + ], + "sku": null, + "discontinued": true, + "out_of_stock": true + } + ], + "branding_items": [ + { + "id": 1, + "external_id": "item-1", + "variant_id": 1, + "sync_variant_id": 1, + "external_variant_id": "variant-1", + "warehouse_product_variant_id": 1, + "product_template_id": 1, + "quantity": 1, + "price": "13.00", + "retail_price": "13.00", + "name": "Enhanced Matte Paper Poster 18×24", + "product": { + "variant_id": 3001, + "product_id": 301, + "image": "https://files.cdn.printful.com/products/71/5309_1581412541.jpg", + "name": "Bella + Canvas 3001 Unisex Short Sleeve Jersey T-Shirt with Tear Away Label (White / 4XL)" + }, + "files": [ + { + "type": "default", + "id": 10, + "url": "https://www.example.com/files/tshirts/example.png", + "options": [ + { + "id": "template_type", + "value": "native" + } + ], + "hash": "ea44330b887dfec278dbc4626a759547", + "filename": "shirt1.png", + "mime_type": "image/png", + "size": 45582633, + "width": 1000, + "height": 1000, + "dpi": 300, + "status": "ok", + "created": 1590051937, + "thumbnail_url": "https://files.cdn.printful.com/files/ea4/ea44330b887dfec278dbc4626a759547_thumb.png", + "preview_url": "https://files.cdn.printful.com/files/ea4/ea44330b887dfec278dbc4626a759547_thumb.png", + "visible": true, + "is_temporary": false + } + ], + "options": [ + { + "id": "OptionKey", + "value": "OptionValue" + } + ], + "sku": null, + "discontinued": true, + "out_of_stock": true + } + ], + "incomplete_items": [ + { + "name": "Red T-Shirt", + "quantity": 1, + "sync_variant_id": 70, + "external_variant_id": "external-id", + "external_line_item_id": "external-line-item-id" + } + ], + "costs": { + "currency": "USD", + "subtotal": "10.00", + "discount": "0.00", + "shipping": "5.00", + "digitization": "0.00", + "additional_fee": "0.00", + "fulfillment_fee": "0.00", + "retail_delivery_fee": "0.00", + "tax": "0.00", + "vat": "0.00", + "total": "15.00" + }, + "retail_costs": { + "currency": "USD", + "subtotal": "10.00", + "discount": "0.00", + "shipping": "5.00", + "tax": "0.00", + "vat": "0.00", + "total": "15.00" + }, + "pricing_breakdown": [ + { + "customer_pays": "3.75", + "printful_price": "6", + "profit": "-2.25", + "currency_symbol": "USD" + } + ], + "shipments": [ + { + "id": 10, + "carrier": "FEDEX", + "service": "FedEx SmartPost", + "tracking_number": 0, + "tracking_url": "https://www.fedex.com/fedextrack/?tracknumbers=0000000000", + "created": 1588716060, + "ship_date": "2020-05-05", + "shipped_at": 1588716060, + "reshipment": false, + "items": [ + { + "item_id": 1, + "quantity": 1, + "picked": 1, + "printed": 1 + } + ] + } + ], + "gift": { + "subject": "To John", + "message": "Have a nice day" + }, + "packing_slip": { + "email": "your-name@your-domain.com", + "phone": "+371 28888888", + "message": "Message on packing slip", + "logo_url": "http://www.your-domain.com/packing-logo.png", + "store_name": "Your store name", + "custom_order_id": "kkk2344lm" + } + } + } +} \ No newline at end of file diff --git a/components/printful/sources/new-product-added-instant/new-product-added-instant.mjs b/components/printful/sources/new-product-added-instant/new-product-added-instant.mjs new file mode 100644 index 0000000000000..d20d4b27d22e3 --- /dev/null +++ b/components/printful/sources/new-product-added-instant/new-product-added-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "printful-new-product-added-instant", + name: "New Product Added (Instant)", + description: "Emit new event when a new product is added to your Printful store catalog.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return [ + "product_synced", + ]; + }, + getModelField() { + return "sync_product"; + }, + getSummary(body) { + return `New product added: ${body.data.sync_product.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/printful/sources/new-product-added-instant/test-event.mjs b/components/printful/sources/new-product-added-instant/test-event.mjs new file mode 100644 index 0000000000000..09206b7df0277 --- /dev/null +++ b/components/printful/sources/new-product-added-instant/test-event.mjs @@ -0,0 +1,17 @@ +export default { + "type": "product_synced", + "created": 1622456737, + "retries": 2, + "store": 12, + "data": { + "sync_product": { + "id": 13, + "external_id": "4235234213", + "name": "T-shirt", + "variants": 10, + "synced": 10, + "thumbnail_url": "https://your-domain.com/path/to/image.png", + "is_ignored": true + } + } +} \ No newline at end of file diff --git a/components/printful/sources/order-status-updated-instant/order-status-updated-instant.mjs b/components/printful/sources/order-status-updated-instant/order-status-updated-instant.mjs new file mode 100644 index 0000000000000..e5d0ebb8f11a3 --- /dev/null +++ b/components/printful/sources/order-status-updated-instant/order-status-updated-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "printful-order-status-updated-instant", + name: "Order Status Updated (Instant)", + description: "Emit new event when the status of an existing Printful order is updated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return [ + "order_updated", + ]; + }, + getModelField() { + return "order"; + }, + getSummary(body) { + return `Order ${body.data.order.id} status updated to ${body.data.order.status}`; + }, + }, + sampleEmit, +}; diff --git a/components/printful/sources/order-status-updated-instant/test-event.mjs b/components/printful/sources/order-status-updated-instant/test-event.mjs new file mode 100644 index 0000000000000..3cfc9a3320bab --- /dev/null +++ b/components/printful/sources/order-status-updated-instant/test-event.mjs @@ -0,0 +1,217 @@ +export default { + "type": "order_updated", + "created": 1622456737, + "retries": 2, + "store": 12, + "data": { + "order": { + "id": 13, + "external_id": "4235234213", + "store": 10, + "status": "draft", + "shipping": "STANDARD", + "shipping_service_name": "Flat Rate (3-4 business days after fulfillment)", + "created": 1602607640, + "updated": 1602607640, + "recipient": { + "name": "John Smith", + "company": "John Smith Inc", + "address1": "19749 Dearborn St", + "address2": "string", + "city": "Chatsworth", + "state_code": "CA", + "state_name": "California", + "country_code": "US", + "country_name": "United States", + "zip": "91311", + "phone": "2312322334", + "email": "firstname.secondname@domain.com", + "tax_number": "123.456.789-10" + }, + "items": [ + { + "id": 1, + "external_id": "item-1", + "variant_id": 1, + "sync_variant_id": 1, + "external_variant_id": "variant-1", + "warehouse_product_variant_id": 1, + "product_template_id": 1, + "quantity": 1, + "price": "13.00", + "retail_price": "13.00", + "name": "Enhanced Matte Paper Poster 18×24", + "product": { + "variant_id": 3001, + "product_id": 301, + "image": "https://files.cdn.printful.com/products/71/5309_1581412541.jpg", + "name": "Bella + Canvas 3001 Unisex Short Sleeve Jersey T-Shirt with Tear Away Label (White / 4XL)" + }, + "files": [ + { + "type": "default", + "id": 10, + "url": "https://www.example.com/files/tshirts/example.png", + "options": [ + { + "id": "template_type", + "value": "native" + } + ], + "hash": "ea44330b887dfec278dbc4626a759547", + "filename": "shirt1.png", + "mime_type": "image/png", + "size": 45582633, + "width": 1000, + "height": 1000, + "dpi": 300, + "status": "ok", + "created": 1590051937, + "thumbnail_url": "https://files.cdn.printful.com/files/ea4/ea44330b887dfec278dbc4626a759547_thumb.png", + "preview_url": "https://files.cdn.printful.com/files/ea4/ea44330b887dfec278dbc4626a759547_thumb.png", + "visible": true, + "is_temporary": false + } + ], + "options": [ + { + "id": "OptionKey", + "value": "OptionValue" + } + ], + "sku": null, + "discontinued": true, + "out_of_stock": true + } + ], + "branding_items": [ + { + "id": 1, + "external_id": "item-1", + "variant_id": 1, + "sync_variant_id": 1, + "external_variant_id": "variant-1", + "warehouse_product_variant_id": 1, + "product_template_id": 1, + "quantity": 1, + "price": "13.00", + "retail_price": "13.00", + "name": "Enhanced Matte Paper Poster 18×24", + "product": { + "variant_id": 3001, + "product_id": 301, + "image": "https://files.cdn.printful.com/products/71/5309_1581412541.jpg", + "name": "Bella + Canvas 3001 Unisex Short Sleeve Jersey T-Shirt with Tear Away Label (White / 4XL)" + }, + "files": [ + { + "type": "default", + "id": 10, + "url": "https://www.example.com/files/tshirts/example.png", + "options": [ + { + "id": "template_type", + "value": "native" + } + ], + "hash": "ea44330b887dfec278dbc4626a759547", + "filename": "shirt1.png", + "mime_type": "image/png", + "size": 45582633, + "width": 1000, + "height": 1000, + "dpi": 300, + "status": "ok", + "created": 1590051937, + "thumbnail_url": "https://files.cdn.printful.com/files/ea4/ea44330b887dfec278dbc4626a759547_thumb.png", + "preview_url": "https://files.cdn.printful.com/files/ea4/ea44330b887dfec278dbc4626a759547_thumb.png", + "visible": true, + "is_temporary": false + } + ], + "options": [ + { + "id": "OptionKey", + "value": "OptionValue" + } + ], + "sku": null, + "discontinued": true, + "out_of_stock": true + } + ], + "incomplete_items": [ + { + "name": "Red T-Shirt", + "quantity": 1, + "sync_variant_id": 70, + "external_variant_id": "external-id", + "external_line_item_id": "external-line-item-id" + } + ], + "costs": { + "currency": "USD", + "subtotal": "10.00", + "discount": "0.00", + "shipping": "5.00", + "digitization": "0.00", + "additional_fee": "0.00", + "fulfillment_fee": "0.00", + "retail_delivery_fee": "0.00", + "tax": "0.00", + "vat": "0.00", + "total": "15.00" + }, + "retail_costs": { + "currency": "USD", + "subtotal": "10.00", + "discount": "0.00", + "shipping": "5.00", + "tax": "0.00", + "vat": "0.00", + "total": "15.00" + }, + "pricing_breakdown": [ + { + "customer_pays": "3.75", + "printful_price": "6", + "profit": "-2.25", + "currency_symbol": "USD" + } + ], + "shipments": [ + { + "id": 10, + "carrier": "FEDEX", + "service": "FedEx SmartPost", + "tracking_number": 0, + "tracking_url": "https://www.fedex.com/fedextrack/?tracknumbers=0000000000", + "created": 1588716060, + "ship_date": "2020-05-05", + "shipped_at": 1588716060, + "reshipment": false, + "items": [ + { + "item_id": 1, + "quantity": 1, + "picked": 1, + "printed": 1 + } + ] + } + ], + "gift": { + "subject": "To John", + "message": "Have a nice day" + }, + "packing_slip": { + "email": "your-name@your-domain.com", + "phone": "+371 28888888", + "message": "Message on packing slip", + "logo_url": "http://www.your-domain.com/packing-logo.png", + "store_name": "Your store name", + "custom_order_id": "kkk2344lm" + } + } + } +} \ No newline at end of file diff --git a/components/printful_oauth/README.md b/components/printful_oauth/README.md index 99d1965b77743..d7dbae6444005 100644 --- a/components/printful_oauth/README.md +++ b/components/printful_oauth/README.md @@ -1,19 +1,11 @@ # Overview -The Printful (Oauth) API is a powerful and customizable tool that can be used to build a wide variety of applications related to eCommerce and on-demand printing. With the Printful API, you can easily access the full range of Printful services, including order management, product catalogs, order status and tracking, as well as direct product integration. With the Printful API, developers have the ability to create a wide variety of eCommerce applications such as online stores, automated fulfillment applications, and even custom storefronts. +The Printful (OAuth) API on Pipedream allows you to automate and integrate Printful's on-demand printing and warehousing services into your custom workflows. From syncing e-commerce orders to generating custom notifications on order status updates, you can tailor the Printful API to fit seamlessly into your business processes. This integration is ideal for e-commerce businesses looking to streamline their operations, especially for those who want to synchronize their storefronts with Printful's production pipeline without extensive manual oversight. -Here are just a few of the ways you can use the Printful API to build your own applications: +# Example Use Cases -1. Create custom online stores: You can use Printful's API to integrate your custom store with Printful to offer your customers premium on-demand printing services. +- **Sync Orders between E-commerce Platform and Printful**: Automatically create Printful orders whenever new orders are received on your e-commerce platform (like Shopify or WooCommerce). This workflow listens for new orders and uses the Printful API to create corresponding orders, ensuring that fulfillment begins without delay. -2. Automated fulfillment applications: Printful's API can be used to build sales and fulfillment applications that automatically process orders, route them for printing and shipping, and track orders for customers. +- **Inventory Level Syncing**: Set up a workflow to monitor and sync inventory levels between Printful and your online store. When inventory levels change in Printful, the workflow updates the inventory count on your storefront to prevent overselling items that are out of stock. -3. Full-fledge eCommerce solution: Printful's API can also be used to create a full-fledge eCommerce solution that can manage product catalogs, order management, customer support and more. - -4. Create custom storefronts: You can use Printful's API to create custom storefronts for customers that offer integrated Printful products. It can be used to customize storefronts for specific products or for exclusive rewards for customers. - -5. Manage product catalogs and inventories: Printful's API can be used to manage product catalogs and inventories, allowing you to easily create and manage new products as they are added to your store. - -6. Track orders and shipping status: You can also use Printful's API to track orders and shipping status, allowing customers to easily see when their orders have been shipped. - -Clearly, the possibilities with Printful's API are nearly limitless, and it is a powerful tool for any developer looking to create top-of-the-line eCommerce applications. With the full range of features and customization available to you with the Printful API, you have everything you need to create the ultimate online store or application. +- **Order Status Notifications to Customers**: Implement a workflow to send custom email or SMS notifications to customers when their Printful order status changes. For instance, when an order is fulfilled or shipped, trigger an automated message via SendGrid or Twilio to keep the customer informed throughout the fulfillment process. diff --git a/components/printify/README.md b/components/printify/README.md new file mode 100644 index 0000000000000..bd93cc408fb07 --- /dev/null +++ b/components/printify/README.md @@ -0,0 +1,12 @@ +# Overview + +The Printify API, accessible within Pipedream's platform, offers a suite of operations to streamline your print-on-demand business. It allows you to create products, manage orders, sync inventory, and handle a variety of other e-commerce functions programmatically. With Pipedream's serverless execution environment, you can tap into the Printify API to automate workflows, integrate with other apps, and manipulate data without the need to manage infrastructure. + +# Example Use Cases + +- **Automated Order Fulfillment Workflow**: Triggered by a new sale on your platform, this workflow automates the order creation process in Printify. When a customer purchases an item, Pipedream can receive the sale details, create an order in Printify, and send an order confirmation to the customer, all without manual intervention. + +- **Product Catalog Sync**: Maintain an up-to-date product catalog across platforms. Whenever a new product is added to your Printify account, Pipedream can capture the event and update your other sales channels, like Shopify or WooCommerce, ensuring consistent product information and pricing across your ecosystem. + +- **Inventory Level Monitoring**: Keep tabs on your inventory levels by setting up a Pipedream workflow that checks Printify stock quantities at regular intervals. If stock for a popular item dips below a certain threshold, Pipedream can notify you via email or Slack, and it could even automatically pause ad campaigns for that item on Google Ads to avoid overselling. +. diff --git a/components/printnode/README.md b/components/printnode/README.md new file mode 100644 index 0000000000000..8ede6278b7113 --- /dev/null +++ b/components/printnode/README.md @@ -0,0 +1,11 @@ +# Overview + +The PrintNode API on Pipedream allows you to integrate cloud printing capabilities into workflows. It supports automating print jobs, managing printers, and querying printer status. With Pipedream's ability to connect to hundreds of apps, you can trigger print jobs from emails, forms, databases, or custom events. The API's functions can be weaved into broader business processes to streamline document handling. + +# Example Use Cases + +- **Print Shipping Labels on New Orders**: Automatically print shipping labels when new orders come in from an eCommerce platform like Shopify. When an order is placed, Pipedream triggers a workflow that sends the order's shipping label to a designated printer via PrintNode. + +- **Print Invoices on Payment Confirmation**: Set up a workflow where invoices are automatically printed when payment is confirmed through payment processors like Stripe or PayPal. Once the payment succeeds, a PDF invoice is generated and sent to PrintNode for printing. + +- **Daily Printout of Sales Reports**: Schedule a daily workflow that queries your sales database, generates a report, and prints it using PrintNode. This could be connected to apps like Google Sheets or Airtable where your sales data is updated. diff --git a/components/prismic/README.md b/components/prismic/README.md new file mode 100644 index 0000000000000..a2d705fb0ebee --- /dev/null +++ b/components/prismic/README.md @@ -0,0 +1,11 @@ +# Overview + +The Prismic API enables you to query and manage your content repository. With Pipedream's serverless integration, you can automate content updates, sync data across multiple platforms, and trigger custom workflows. For instance, you can connect Prismic with your static site generator to update your website's content on-the-fly, or sync Prismic content with a CMS like WordPress for better content management. Pipedream's no-code platform simplifies the process of setting up these integrations and workflows, making it accessible to users of varying technical levels. + +# Example Use Cases + +- **Automated Content Publishing**: Trigger a workflow in Pipedream whenever a new document is published in Prismic. This workflow can deploy your static site through services like Netlify or Vercel, ensuring your website displays the most up-to-date content. + +- **Content Sync with CMS**: Sync Prismic content to a WordPress site by triggering a Pipedream workflow whenever a Prismic document is updated. The workflow would use the WordPress API to update or create a corresponding post in WordPress, keeping both platforms seamlessly aligned. + +- **Email Notification on Content Changes**: Set up a workflow that sends an email alert through a service like SendGrid whenever a specific type of content is updated in Prismic. This can keep your editorial team informed about content changes, enabling quick reviews and updates. diff --git a/components/prismic/package.json b/components/prismic/package.json new file mode 100644 index 0000000000000..59c7e02988b1d --- /dev/null +++ b/components/prismic/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/prismic", + "version": "0.0.1", + "description": "Pipedream Prismic Components", + "main": "prismic.app.mjs", + "keywords": [ + "pipedream", + "prismic" + ], + "homepage": "https://pipedream.com/apps/prismic", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/prismic/prismic.app.mjs b/components/prismic/prismic.app.mjs new file mode 100644 index 0000000000000..a60e29a38cbb2 --- /dev/null +++ b/components/prismic/prismic.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "prismic", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/pro_ledger/actions/create-record/create-record.mjs b/components/pro_ledger/actions/create-record/create-record.mjs new file mode 100644 index 0000000000000..058d64ccff5b1 --- /dev/null +++ b/components/pro_ledger/actions/create-record/create-record.mjs @@ -0,0 +1,83 @@ +import app from "../../pro_ledger.app.mjs"; + +export default { + key: "pro_ledger-create-record", + name: "Create Record", + description: "Create a new record in the Pro Ledger platform. [See the documentation](https://api.pro-ledger.com/redoc#tag/record/operation/create_new_record_api_v1_record_create_new_record_post)", + version: "0.0.1", + type: "action", + props: { + app, + rec: { + propDefinition: [ + app, + "rec", + ], + }, + class: { + propDefinition: [ + app, + "class", + ], + }, + category: { + propDefinition: [ + app, + "category", + ], + }, + account: { + propDefinition: [ + app, + "account", + ], + }, + source: { + propDefinition: [ + app, + "source", + ], + }, + total: { + propDefinition: [ + app, + "total", + ], + }, + taxIncluded: { + propDefinition: [ + app, + "taxIncluded", + ], + }, + transactionType: { + propDefinition: [ + app, + "transactionType", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.app.createRecord({ + $, + params: { + transactionType: this.transactionType, + }, + data: { + job_id: this.jobID, + rec: this.rec, + class: this.class, + category: JSON.stringify(this.category), + account: this.account, + source: this.source, + total: this.total, + taxIncluded: this.taxIncluded, + }, + }); + + $.export("$summary", `Successfully created record with ID: '${response.data.id}'`); + + return response; + }, +}; diff --git a/components/pro_ledger/actions/get-accounts/get-accounts.mjs b/components/pro_ledger/actions/get-accounts/get-accounts.mjs new file mode 100644 index 0000000000000..5b93a6f3deeee --- /dev/null +++ b/components/pro_ledger/actions/get-accounts/get-accounts.mjs @@ -0,0 +1,21 @@ +import app from "../../pro_ledger.app.mjs"; + +export default { + key: "pro_ledger-get-accounts", + name: "Get Accounts", + description: "Get accounts setup information. [See the documentation](https://api.pro-ledger.com/redoc#tag/record/operation/get_accounts_api_v1_record_get_accounts_get)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.getAccounts({ + $, + }); + + $.export("$summary", "Successfully retrieved accounts"); + + return response; + }, +}; diff --git a/components/pro_ledger/actions/get-categories/get-categories.mjs b/components/pro_ledger/actions/get-categories/get-categories.mjs new file mode 100644 index 0000000000000..54b53cfe483d5 --- /dev/null +++ b/components/pro_ledger/actions/get-categories/get-categories.mjs @@ -0,0 +1,31 @@ +import app from "../../pro_ledger.app.mjs"; + +export default { + key: "pro_ledger-get-categories", + name: "Get Categories", + description: "Get categories setup information. [See the documentation](https://api.pro-ledger.com/redoc#tag/record/operation/get_categories_api_v1_record_get_categories_get)", + version: "0.0.1", + type: "action", + props: { + app, + transactionType: { + propDefinition: [ + app, + "transactionType", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.app.getCategories({ + $, + params: { + transactionType: this.transactionType, + }, + }); + + $.export("$summary", "Successfully retrieved categories"); + + return response; + }, +}; diff --git a/components/pro_ledger/actions/get-classes/get-classes.mjs b/components/pro_ledger/actions/get-classes/get-classes.mjs new file mode 100644 index 0000000000000..77fced59bfb00 --- /dev/null +++ b/components/pro_ledger/actions/get-classes/get-classes.mjs @@ -0,0 +1,31 @@ +import app from "../../pro_ledger.app.mjs"; + +export default { + key: "pro_ledger-get-classes", + name: "Get Classes", + description: "Get classes setup information. [See the documentation](https://api.pro-ledger.com/redoc#tag/record/operation/get_classes_api_v1_record_get_classes_get)", + version: "0.0.{{ts}}", + type: "action", + props: { + app, + transactionType: { + propDefinition: [ + app, + "transactionType", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.app.getClasses({ + $, + params: { + transactionType: this.transactionType, + }, + }); + + $.export("$summary", "Successfully retrieved classes"); + + return response; + }, +}; diff --git a/components/pro_ledger/common/constants.mjs b/components/pro_ledger/common/constants.mjs new file mode 100644 index 0000000000000..09e184e4c635a --- /dev/null +++ b/components/pro_ledger/common/constants.mjs @@ -0,0 +1,18 @@ +export default { + RECS: [ + "Select", + "Done", + "No Receipt", + "Foreign $$", + ], + TAX_INCLUDED: [ + "Incl. Tax", + "Excl. Tax", + ], + TRANSACTION_TYPES: [ + "Income", + "Expense", + "Transfer", + "Cost of Goods Sold", + ], +}; diff --git a/components/pro_ledger/package.json b/components/pro_ledger/package.json new file mode 100644 index 0000000000000..7fb29e0be51a0 --- /dev/null +++ b/components/pro_ledger/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/pro_ledger", + "version": "0.1.0", + "description": "Pipedream Pro-Ledger Components", + "main": "pro_ledger.app.mjs", + "keywords": [ + "pipedream", + "pro_ledger" + ], + "homepage": "https://pipedream.com/apps/pro_ledger", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} diff --git a/components/pro_ledger/pro_ledger.app.mjs b/components/pro_ledger/pro_ledger.app.mjs new file mode 100644 index 0000000000000..425941dfdb82d --- /dev/null +++ b/components/pro_ledger/pro_ledger.app.mjs @@ -0,0 +1,128 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "pro_ledger", + propDefinitions: { + class: { + type: "string", + label: "Class", + description: "Record's class", + async options() { + const resources = await this.getClasses(); + + return resources.map(({ + id, label, + }) => ({ + value: id, + label: label, + })); + }, + }, + category: { + type: "string", + label: "category", + description: "Record's category", + async options() { + const resources = await this.getCategories(); + + return resources.map(({ + id, label, + }) => ({ + value: id, + label: label, + })); + }, + }, + account: { + type: "string", + label: "Account", + description: "Reccord's account", + async options() { + const resources = await this.getAccounts(); + + return resources.map(({ + id, label, + }) => ({ + value: id, + label: label, + })); + }, + }, + rec: { + type: "string", + label: "rec", + description: "", + options: constants.RECS, + }, + taxIncluded: { + type: "string", + label: "Tax Included", + description: "If taxes are already included", + options: constants.TAX_INCLUDED, + }, + transactionType: { + type: "string", + label: "Transaction Type", + description: "The transaction type", + options: constants.TRANSACTION_TYPES, + }, + source: { + type: "string", + label: "source", + description: "Record's source", + }, + total: { + type: "integer", + label: "Total", + description: "Record's total", + }, + }, + methods: { + _baseUrl() { + return "https://api.pro-ledger.com/api/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "X-API-Key": `${this.$auth.api_key}`, + }, + }); + }, + async createRecord(args = {}) { + return this._makeRequest({ + method: "post", + path: "/record/create_new_record", + ...args, + }); + }, + async getClasses(args = {}) { + return this._makeRequest({ + path: "/record/get_classes", + ...args, + }); + }, + async getCategories(args = {}) { + return this._makeRequest({ + path: "/record/get_categories", + ...args, + }); + }, + async getAccounts(args = {}) { + return this._makeRequest({ + path: "/record/get_accounts", + ...args, + }); + }, + }, +}; diff --git a/components/proabono/README.md b/components/proabono/README.md new file mode 100644 index 0000000000000..3138feffd96ec --- /dev/null +++ b/components/proabono/README.md @@ -0,0 +1,11 @@ +# Overview + +ProAbono is a service built to manage subscription billing with fine-grained control over pricing, features, customers, and usage. With the ProAbono API, Pipedream can be your automation partner to streamline subscription operations. Use cases include synchronizing customer data, updating subscription details, and managing billing events. Pipedream's serverless platform allows you to trigger workflows on schedule, by webhook, or via other app events, making it ideal for integrating with ProAbono to handle complex subscription logic. + +# Example Use Cases + +- **Automate Customer Onboarding**: When a new customer signs up via your platform, Pipedream can automate the process of creating a new customer and subscription in ProAbono. Pair this with Slack to notify the team of new signups. + +- **Sync Subscription Updates to CRM**: Maintain a single source of truth by updating customer subscription details in your CRM whenever a change occurs in ProAbono. Integrate with Salesforce or Hubspot to ensure sales and service teams are up-to-date. + +- **Handle Failed Payments**: Set up a workflow to listen for failed payment events from ProAbono. When a payment fails, Pipedream can trigger an email campaign via SendGrid to alert the customer and provide instructions for updating payment information. diff --git a/components/process_street/README.md b/components/process_street/README.md index 950fa7dbdc3e7..e551b406425c1 100644 --- a/components/process_street/README.md +++ b/components/process_street/README.md @@ -1,11 +1,11 @@ # Overview -Process Street is an API-ready, cloud-based task-management platform that enables users to create and manage automated workflows. The API allows you to easily customize and extend the functionality of your Process Street processes. Here are some of the things you can build using the API: - -1. Custom integrations with third-party apps and services, allowing for seamless data flow between them and Process Street. -2. Automated reports and notifications to help you stay on top of your processes. -3. Automated checks to ensure workflow accuracy, compliance and consistency. -4. Smart triggers that launch actions in response to changes in your data. -5. Ability to combine processes from multiple sources. -6. Custom logic and workflows tailored to specific processes. -7. An API-ready, easily extensible and customizable platform. +The Process Street API empowers you to automate and integrate your checklists and workflows with other apps to streamline business operations. With the API, you can create workflows, manage tasks, trigger actions based on checklist completion, and synchronize data across tools. This paves the way for endless possibilities in terms of project management, compliance tracking, onboarding processes, and routine audits, all managed within the familiar interface of Process Street but supercharged with Pipedream's connectivity. + +# Example Use Cases + +- **Workflow Management Automation**: Automatically create Process Street checklists from a project management tool like Asana or Trello. When a new task is marked as complete in Asana, trigger a Pipedream workflow to create a corresponding checklist in Process Street to ensure all subtasks and compliance requirements are assessed. + +- **Dynamic Checklist Deployment**: Use Pipedream to deploy checklists in Process Street based on customer actions. For example, once a customer signs up on your platform, a Pipedream workflow can trigger the creation of an onboarding checklist in Process Street, ensuring consistent onboarding experiences. + +- **Data Synchronization Across Tools**: Keep your CRM, like Salesforce, in sync with Process Street. When a sales deal is marked as won in Salesforce, a Pipedream workflow can update or create a checklist in Process Street for the account management team, ensuring a seamless transition and kickoff for account setup and management. diff --git a/components/processplan/README.md b/components/processplan/README.md new file mode 100644 index 0000000000000..4321e11bbd6b6 --- /dev/null +++ b/components/processplan/README.md @@ -0,0 +1,11 @@ +# Overview + +ProcessPlan is a process management tool with an API that lets you automate and track workflows. On Pipedream, you can use this API to create intricate automations, which can involve triggering workflows in ProcessPlan, updating tasks, pulling data from process templates, or integrating with other services to streamline operations. + +# Example Use Cases + +- **Automate Task Creation on Schedule**: Use Pipedream's scheduled triggers to automatically create tasks in ProcessPlan at regular intervals, ensuring recurring processes are not overlooked and are initiated on time. + +- **Sync ProcessPlan with Project Management Tools**: Connect ProcessPlan to project management apps like Trello or Asana on Pipedream. When a process reaches a specific stage, it could create a card in Trello or a task in Asana, aligning cross-platform project tracking. + +- **Email Notifications for Process Updates**: Set up a Pipedream workflow that listens for status updates on tasks in ProcessPlan and automatically sends out detailed email notifications using SendGrid or a similar email service app whenever a process status changes. diff --git a/components/procfu/package.json b/components/procfu/package.json new file mode 100644 index 0000000000000..132429779b7ec --- /dev/null +++ b/components/procfu/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/procfu", + "version": "0.0.1", + "description": "Pipedream ProcFu Components", + "main": "procfu.app.mjs", + "keywords": [ + "pipedream", + "procfu" + ], + "homepage": "https://pipedream.com/apps/procfu", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/procfu/procfu.app.mjs b/components/procfu/procfu.app.mjs new file mode 100644 index 0000000000000..efdf734f728a9 --- /dev/null +++ b/components/procfu/procfu.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "procfu", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/procore/README.md b/components/procore/README.md index 42180125aa06a..2d31f45cc2005 100644 --- a/components/procore/README.md +++ b/components/procore/README.md @@ -1,14 +1,11 @@ # Overview - Procore's API enables developers to build powerful applications for industries with complex workflows, such as construction. With the Procore API, you can extend and customize Procore's existing features, integrate with external platforms and applications, and create innovative solutions for your organization. - -Here are some examples of what you can build with the Procore API: - -- Automatically sync project documents with external file storage systems -- Streamline transition from bids to projects -- Create custom reports on key project metrics -- Generate custom invoices with custom language and branding -- Automatically monitor and respond to changes in project scheduling -- Develop dashboards to set up tracking for subcontractors and suppliers -- Automate communication between stakeholders -- Integrate Procore user data with third-party applications +The Procore API empowers developers to interact with its construction management software programmatically, enabling automation of tasks, data synchronization, and enhanced reporting. With APIs covering project management, quality and safety, construction financials, and field productivity, you can craft workflows that streamline operations, reduce manual entry, and provide real-time insights. On Pipedream, these capabilities can be harnessed to create workflows that react to events in Procore or orchestrate actions between Procore and other apps, optimizing construction project workflows. + +# Example Use Cases + +- **Automate Incident Reporting**: Trigger a workflow on Pipedream when a safety incident is reported in Procore. Use this trigger to immediately send an email notification to safety managers, log the incident in a Google Sheet for record-keeping, and create a follow-up task in a project management tool like Asana. + +- **Sync Project Financials with Accounting Software**: Set up a scheduled Pipedream workflow that retrieves updated project budget and cost data from Procore and syncs it with an accounting platform like QuickBooks. Ensure financial records are always up to date and allow for real-time budget analysis and reporting. + +- **Streamline RFIs and Submittals Management**: When a new Request for Information (RFI) or Submittal is created in Procore, trigger a Pipedream workflow to cross-post the details to a Slack channel dedicated to project communications. This keeps the team informed and facilitates quick responses, ensuring project timelines are met with fewer delays. diff --git a/components/procore_sandbox/package.json b/components/procore_sandbox/package.json new file mode 100644 index 0000000000000..9959a0d543bee --- /dev/null +++ b/components/procore_sandbox/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/procore_sandbox", + "version": "0.0.1", + "description": "Pipedream Procore Sandbox Components", + "main": "procore_sandbox.app.mjs", + "keywords": [ + "pipedream", + "procore_sandbox" + ], + "homepage": "https://pipedream.com/apps/procore_sandbox", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/procore_sandbox/procore_sandbox.app.mjs b/components/procore_sandbox/procore_sandbox.app.mjs new file mode 100644 index 0000000000000..45ee90e282c69 --- /dev/null +++ b/components/procore_sandbox/procore_sandbox.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "procore_sandbox", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/prodpad/README.md b/components/prodpad/README.md index b58992e2fdde0..6450fd91a4df2 100644 --- a/components/prodpad/README.md +++ b/components/prodpad/README.md @@ -1,28 +1,18 @@ # Overview -What can you build with ProdPad's API? The possibilities are endless! +The ProdPad API taps into the core functionalities of product management, allowing you to automate interactions with your product backlog, roadmaps, and idea pools. With Pipedream's serverless execution environment, you can trigger workflows based on events in ProdPad, sync data across multiple platforms, and create custom automations to streamline your product management processes. ProdPad's API lets you seamlessly integrate with other tools to keep your team aligned, informed, and productive. -ProdPad is an idea and innovation management platform – with our API, you’re able to turn ideas into actionable tasks with minimal effort. ProdPad’s API enables you to manage product planning and roadmapping, such as: +# Example Use Cases -- Retrieve ideas, including customer feedback and suggestions -- Automatically creates tasks for your team to feature develop and utilize -- Track and analyze the progress of product-related tasks across your different products -- Generate reports on product performance -- Monitor customer feedback with customizable filters -- Integrate your product roadmap with third-party systems and tools +- **Idea Submission Automation**: When a new idea is submitted to ProdPad, use Pipedream to trigger a workflow that automatically categorizes the idea based on predefined rules, assigns it to the appropriate team, and sends notifications via Slack or email to relevant stakeholders. This ensures that new ideas are quickly processed and evaluated. -The ProdPad API allows you to build a range of custom projects that can streamline and automate parts of your product planning process. Here are some of the things you can do: +- **Feedback Loop Enhancement**: Integrate ProdPad with customer support tools like Zendesk or Intercom. Whenever feedback is received, it can trigger a Pipedream workflow that creates or updates ideas in ProdPad, linking them back to the customer tickets. This ensures valuable user feedback is directly tied to potential feature development in the product roadmap. -- Track customer feedback and manage product roadmaps -- Keep track of team tasks and milestones -- Analyze product performance and utilization -- Visualize your product roadmap -- Aggregate customer feedback into reports -- Integrate ProdPad into your existing systems and tools -- Create custom development projects -- Synchronize data between Prodpad and third-party applications +- **Product Roadmap Synchronization**: Keep your product roadmap in sync with project management tools such as Jira or Trello. When changes are made to the roadmap in ProdPad, Pipedream can catch these events and update corresponding tasks, stories, or epics in your project management tool, ensuring all teams are working from the latest plan. -# Webhooks +# Getting Started + +## Webhooks Some triggers support webhooks for ProdPad. These include: diff --git a/components/product_fruits/README.md b/components/product_fruits/README.md new file mode 100644 index 0000000000000..649907c5601c8 --- /dev/null +++ b/components/product_fruits/README.md @@ -0,0 +1,11 @@ +# Overview + +The Product Fruits API offers tools for enhancing user experience with in-app guidance like tooltips, walkthroughs, and announcements. By leveraging this API within Pipedream, you can automate the creation, update, and tracking of these UX elements based on user behavior or feedback, enrich product engagement, and drive feature adoption. Combining Product Fruits with Pipedream's capabilities opens a realm of possibilities, from syncing user data to triggering in-app messages based on external events. + +# Example Use Cases + +- **User Onboarding Automation**: Trigger personalized onboarding flows in your application for new users by listening to signup events from your authentication platform (e.g., Auth0) on Pipedream. Create a workflow that activates specific Product Fruits guides based on the user's role or interests captured during the signup process. + +- **Feature Announcement Broadcasting**: When you release a new feature, use Product Fruits to announce it to users. Set up a Pipedream workflow that monitors your CI/CD pipeline (e.g., GitHub Actions) for deployment of new releases and automatically triggers an in-app announcement via Product Fruits to inform users of the update. + +- **Feedback-Driven Product Education**: Connect your customer support tool (e.g., Zendesk) with Product Fruits through Pipedream. Whenever a user submits a ticket related to a misunderstanding of a feature, automatically trigger a Product Fruits guide explaining that feature the next time they log in to your app. diff --git a/components/product_fruits/package.json b/components/product_fruits/package.json index 3bfa1b0dc3445..16d820096ca3c 100644 --- a/components/product_fruits/package.json +++ b/components/product_fruits/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/product_fruits/product_fruits.app.mjs b/components/product_fruits/product_fruits.app.mjs index 3ed19040555cc..86775aac20a73 100644 --- a/components/product_fruits/product_fruits.app.mjs +++ b/components/product_fruits/product_fruits.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/product_hunt/README.md b/components/product_hunt/README.md index e80856e55b297..866b17fae59fe 100644 --- a/components/product_hunt/README.md +++ b/components/product_hunt/README.md @@ -1,19 +1,11 @@ # Overview -What Can You Build With The Product Hunt API? +The Product Hunt API taps into a vibrant community of tech enthusiasts and makers, allowing you to interact with their platform programmatically. With it, you can retrieve details on the latest trending products, post comments, and gather user data. Automating these interactions can keep you informed on tech trends, engage with the community, and analyze market interests. -Product Hunt is a great platform for discovering new and noteworthy products. With the Product Hunt API, you can build custom tools and integrate Product Hunt into your existing applications. Below are some examples of what you can build with the Product Hunt API. +# Example Use Cases -1. Search and filter for trending products - Find the most popular products on Product Hunt by searching or filtering by category or date, and get detailed information about each product. +- **Track New Product Launches**: Automate the monitoring of new products on Product Hunt. Whenever a product launches in your category of interest, use Pipedream to trigger a notification workflow that sends alerts through email, Slack, or SMS, keeping your team instantly informed. -2. Create custom leaderboards - Create custom leaderboards sorted by total votes, comments, or status. +- **Auto-Post Comments**: Develop a workflow that automatically posts predefined comments on new products that match certain criteria. For instance, you can congratulate makers on their launches or ask pertinent questions, thus maintaining a steady presence on the platform without manual intervention. -3. Measure users' Product Hunt activity - Track your followers' Product Hunt activity and get insights on what's popular and what's not. - -4. Build a recommendation engine - Create personalized product recommendations for users by analyzing the product data and scouring the user's activity. - -5. Enhance existing applications - Integrate Product Hunt data into existing applications, such as integrating product information into a directory site or adding product ratings to an ecommerce store. - -6. Monitor competitor activity - Use the API to keep an eye on the latest products from your competitors so you can stay one step ahead. - -7. Build custom tools - Take data from Product Hunt and make it instantly actionable with custom tools, such as a real-time product analyzer or trends explorer. +- **Analyze Market Trends**: Set up a Pipedream workflow to collect data on products, such as upvotes and comments, to analyze market trends. Connect with Google Sheets or another data visualization tool to create reports, helping you make informed decisions based on user engagement and interest. diff --git a/components/productboard/README.md b/components/productboard/README.md index c4ecf55c638fb..0b8ed4d26e805 100644 --- a/components/productboard/README.md +++ b/components/productboard/README.md @@ -1,15 +1,11 @@ # Overview -Using Productboard API, you can create powerful solutions to help better manage your product development needs. With the API, you can access and manipulate data stored in the Productboard platform, including project management, customer relationship management (CRM) tasks, and product development tracking. Here are just a few examples of what can be built with Productboard API: +The Productboard API offers a way to extend the capabilities of Productboard, a product management platform that helps teams organize and prioritize user feedback and feature requests. By leveraging this API on Pipedream, you can automate repetitive tasks, sync data across various apps, and create custom integrations to streamline your product management process. This can include automating feedback collection, updating roadmaps, or coordinating cross-functional team efforts. -- Automate process: With a custom API integration, businesses can quickly automate routine processes and instead focus more on other creative endeavors. +# Example Use Cases -- Build better product roadmaps: With the API, teams can access and update product roadmap data in real-time to help craft the vision for the future of their product line. +- **Feature Request Syncing**: Automatically sync new feature requests from Productboard to a project management tool like JIRA or Trello. Once a new feature is noted in Productboard, a Pipedream workflow can create a corresponding ticket or card in your project tracking app, ensuring no request goes unnoticed by the development team. -- Generate project reports: The API enables product teams to quickly retrieve project data to compile reports that analyze a product development's performance. +- **User Feedback Aggregation**: Collect user feedback from multiple channels, such as support tickets or social media mentions, and funnel this information into Productboard. Use Pipedream to monitor these channels, parse the relevant data, and create new insights or notes in Productboard to inform your product's direction. -- Collect product feedback: After implementing an API-powered feedback form, teams can inform future product decisions with customer feedback. - -- Craft effective customer onboarding: API solutions can help streamline customer onboarding processes to ensure quick adoption and long-term customer retention. - -- Manage customer support tickets: With API access to customer support tickets data, teams can reduce their response times and resolve customer issues faster. +- **Automated Roadmap Updates**: Integrate Productboard with your internal communication tools like Slack. Whenever there's a status update on a feature in Productboard, Pipedream can trigger a notification in a dedicated Slack channel, keeping the entire team informed about what's next on the product roadmap. diff --git a/components/productive_io/README.md b/components/productive_io/README.md new file mode 100644 index 0000000000000..9a3fc03e69986 --- /dev/null +++ b/components/productive_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The Productive.io API enables you to automate and integrate your business operations managed within Productive.io. Whether it's syncing project data, managing resources, or tracking finances, the API allows you to streamline workflows by connecting with other services. On Pipedream, you can craft serverless workflows that react to events in Productive.io, or schedule tasks to manage your operations efficiently without leaving the platform. + +# Example Use Cases + +- **Project Management Automation**: Whenever a new task is created in Productive.io, trigger a Pipedream workflow that automatically posts a message with task details to a Slack channel, keeping your team instantly informed. + +- **Time Tracking Insights**: After a time entry is logged in Productive.io, run a Pipedream workflow that compiles time tracking data and sends a weekly report via email using SendGrid, offering insights into where your team's efforts are being spent. + +- **Financial Health Checks**: Set up a Pipedream workflow that triggers monthly to fetch financial data from Productive.io and creates a visual report using Google Sheets, which is then shared with stakeholders to provide a snapshot of the company's financial status. diff --git a/components/productlane/README.md b/components/productlane/README.md new file mode 100644 index 0000000000000..1a2add0902fd6 --- /dev/null +++ b/components/productlane/README.md @@ -0,0 +1,11 @@ +# Overview + +The Productlane API on Pipedream allows you to integrate and automate your product feedback loop. You can craft workflows to manage feature requests, prioritize product development, and engage with your user base more effectively. It's useful for product managers who want to streamline user feedback into actionable insights, and for development teams aiming to align their work with customer needs. + +# Example Use Cases + +- **Syncing Feature Requests to Project Management Tools**: Automatically create tasks in project management apps like Trello or Asana when new feature requests are submitted via Productlane. + +- **User Feedback Aggregation**: Gather and sort user feedback from Productlane, sending a daily or weekly digest to your team's Slack channel or email to keep everyone in the loop. + +- **Feature Launch Announcements**: When a feature moves to 'launched' status in Productlane, trigger a workflow to announce the update across multiple platforms, such as Twitter, company blog, or newsletter. diff --git a/components/profitwell/README.md b/components/profitwell/README.md index f81f74ac18465..2efef03d76268 100644 --- a/components/profitwell/README.md +++ b/components/profitwell/README.md @@ -1,14 +1,11 @@ # Overview -The ProfitWell API enables you to build powerful subscription analytics and optimization solutions. With it, you can access key insights from your subscription data that allows you to make smarter decisions, boost customer retention, and increase revenue. - -Here are some of the key features you can create with the ProfitWell API: - -- Access subscription insights and analytics -- Bandwidth meter within your app that updates in real-time -- Create custom dashboards that display key metrics -- Automatically split test price points and promotions -- Turn on personalized emails and notifications to customers -- Launch product-specific upgrades and downgrades -- Offer promotions for churning customers -- Create upsell paths with simple-to-configure workflows +The ProfitWell API provides programmatic access to metrics and data related to subscription-based businesses. By integrating with Pipedream, you can automate complex workflows involving your financial metrics, customer insights, and growth tracking. Whether it's syncing subscriber data with marketing platforms, triggering alerts based on revenue milestones, or creating custom analytics dashboards, Pipedream's serverless platform enables you to connect ProfitWell with a plethora of other services without writing extensive code. + +# Example Use Cases + +- **Alerting on Key Financial Events**: Connect ProfitWell to Slack using Pipedream to trigger notifications. Set up a workflow that sends a message to a finance channel whenever your MRR crosses a certain threshold or you get a new churned customer, enabling real-time team updates. + +- **Customer Lifecycle Email Campaigns**: Use ProfitWell's API to feed customer subscription data into Mailchimp via Pipedream. Automate a sequence of emails that target customers at different stages of their lifecycle, such as onboarding sequences for new signups or win-back campaigns for those who are at risk of churning. + +- **Cross-Platform Revenue Reporting**: Combine ProfitWell with Google Sheets on Pipedream. Automate the extraction of financial reports from ProfitWell and append them to a Google Sheet for easy sharing and analysis, ensuring stakeholders always have the latest subscription data at their fingertips. diff --git a/components/project_broadcast/README.md b/components/project_broadcast/README.md new file mode 100644 index 0000000000000..2ce18fa824309 --- /dev/null +++ b/components/project_broadcast/README.md @@ -0,0 +1,11 @@ +# Overview + +The Project Broadcast API facilitates communication through automated text messaging, providing features to manage contacts, send messages, and track campaigns. Leveraging this API on Pipedream allows you to create intricate workflows that can automate your texting campaigns, sync contact lists, and engage with customers by integrating with numerous other services for CRM, marketing, and other functions. With Pipedream's serverless platform, these integrations become seamless, triggering actions in real-time or on a schedule without the need for complex infrastructure. + +# Example Use Cases + +- **Automated Follow-Up Messages**: Build a workflow that listens for new customer sign-ups from your website's API. When a new sign-up is detected, automatically add the customer's contact information to Project Broadcast and send a personalized welcome text message. + +- **SMS Campaign Metrics to Google Sheets**: Create a workflow that triggers periodically to retrieve campaign performance metrics from Project Broadcast. Then, parse the data and append it to a Google Sheets document for easy tracking and visualization of campaign effectiveness. + +- **Customer Feedback Collection**: Set up a workflow that sends a text message to customers after a purchase, asking for feedback. Use a webhook to collect responses, store them in a Pipedream data store, and then sync the data with your CRM, such as Salesforce, for further customer engagement strategies. diff --git a/components/project_bubble/README.md b/components/project_bubble/README.md index ed6c928590ae1..9d808a1f69887 100644 --- a/components/project_bubble/README.md +++ b/components/project_bubble/README.md @@ -1,23 +1,11 @@ # Overview -The ProProfs Project API allows developers to build sophisticated project management applications. With its straightforward integration and comprehensive toolkits, the ProProfs Project API can easily be used to build powerful websites and applications. With its comprehensive endpoints, you can access and manipulate project workspaces, tasks, assignments, resources, events, projects, and more. Here are a few examples of what you can build with the ProProfs Project API: +The Project Bubble (ProProfs Project) API offers a suite of project management tools that can help streamline workflows, improve collaboration, and track progress on tasks and milestones within a project. Using Pipedream, you can automate interactions between Project Bubble and other apps, create custom events based on project changes, and manage your projects efficiently. From syncing tasks with calendar apps to triggering notifications when project milestones are reached, the possibilities are vast for enhancing productivity and project oversight. -1. Task Management Applications - Easily create and manage tasks, as well as assign tasks to people or groups. +# Example Use Cases -2. Resource Management - Track resources and manage their availability. +- **Project Updates to Slack Notifications**: Automate the process of informing your team by sending notifications to a Slack channel whenever there is an update in Project Bubble, such as a new task creation, task completion, or a change in project status. This keeps the team aligned and promptly informed. -3. Task Scheduling - Automatically create calendar entries and assign tasks based on availability. +- **Task Synchronization with Google Calendar**: Keep your schedules in sync by creating events in Google Calendar whenever a new task with a due date is added in Project Bubble. This ensures that deadlines are visible across different platforms and helps with time management. -4. Timelogging - Track everyone’s time spent on tasks to ensure everyone is efficient. - -5. Project Dashboards - Create high-level overviews of project progress. - -6. Milestone Tracking - Monitor progress towards project milestones. - -7. Team Collaboration - Foster collaboration among project teams with built-in tools. - -8. Project Analytics - Track project success and monitor performance metrics. - -9. Project Planning & Forecasting - Understand project dependencies and forecast outcomes. - -10. Notification & Alerts - Automatically notify project managers and team members of important tasks and events. +- **Project Metrics to Google Sheets**: Compile project data, like time tracking and task completion metrics, into a Google Sheet automatically. This can serve for reporting purposes, performance reviews, or resource allocation analysis, providing a clear view of project health and team productivity. diff --git a/components/project_bubble/package.json b/components/project_bubble/package.json new file mode 100644 index 0000000000000..d02c50afe63d5 --- /dev/null +++ b/components/project_bubble/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/project_bubble", + "version": "0.6.0", + "description": "Pipedream project_bubble Components", + "main": "project_bubble.app.mjs", + "keywords": [ + "pipedream", + "project_bubble" + ], + "homepage": "https://pipedream.com/apps/project_bubble", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/promptmate_io/package.json b/components/promptmate_io/package.json new file mode 100644 index 0000000000000..c2e45a45356fe --- /dev/null +++ b/components/promptmate_io/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/promptmate_io", + "version": "0.0.1", + "description": "Pipedream promptmate.io Components", + "main": "promptmate_io.app.mjs", + "keywords": [ + "pipedream", + "promptmate_io" + ], + "homepage": "https://pipedream.com/apps/promptmate_io", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/promptmate_io/promptmate_io.app.mjs b/components/promptmate_io/promptmate_io.app.mjs new file mode 100644 index 0000000000000..3c4c05ef66a09 --- /dev/null +++ b/components/promptmate_io/promptmate_io.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "promptmate_io", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/proofly/README.md b/components/proofly/README.md new file mode 100644 index 0000000000000..19cbbc769c835 --- /dev/null +++ b/components/proofly/README.md @@ -0,0 +1,11 @@ +# Overview + +The Proofly API enables you to create, manage, and deploy social proof notifications on your website to boost conversions. It provides various endpoints that allow you to automate the process of updating notifications based on specific triggers or events. Using Pipedream, you can create workflows that integrate with the Proofly API to dynamically control these notifications, respond to analytics, and sync with your marketing or sales tools to optimize user engagement. + +# Example Use Cases + +- **Automate Notification Updates Based on User Behavior**: Using Pipedream, you can design a workflow that listens for webhooks from your analytics platform, then triggers Proofly API to update notifications when a user performs a specific action like signing up or completing a purchase. + +- **Sync Proofly Campaigns with Email Marketing**: Craft a workflow that ties Proofly notifications with your email campaigns. When a new email subscriber is added to your Mailchimp list, for instance, trigger a Proofly notification on your site to welcome the new subscriber and incentivize further engagement. + +- **Aggregate Feedback for A/B Testing**: Assemble a workflow that utilizes Proofly API to switch between different notification designs or messages based on user interaction data piped in from your A/B testing platform. Use this data to determine which notifications are most effective at converting visitors. diff --git a/components/proofly/actions/get-notification-data/get-notification-data.mjs b/components/proofly/actions/get-notification-data/get-notification-data.mjs new file mode 100644 index 0000000000000..5fa1fb552d854 --- /dev/null +++ b/components/proofly/actions/get-notification-data/get-notification-data.mjs @@ -0,0 +1,41 @@ +import app from "../../proofly.app.mjs"; + +export default { + key: "proofly-get-notification-data", + name: "Get Notification Data", + description: "Get data for a notification. [See the documentation here](https://proofly.io/developers)", + version: "0.0.1", + type: "action", + props: { + app, + campaignId: { + propDefinition: [ + app, + "campaignId", + ], + }, + notificationId: { + propDefinition: [ + app, + "notificationId", + ({ campaignId }) => ({ + campaignId, + }), + ], + }, + }, + async run({ $ }) { + const { + app, + notificationId, + } = this; + + const response = await app.listData({ + $, + notificationId, + }); + + $.export("$summary", `Successfully retrieved notification with status \`${response.status}\``); + return response; + }, +}; diff --git a/components/proofly/actions/toggle-campaign/toggle-campaign.mjs b/components/proofly/actions/toggle-campaign/toggle-campaign.mjs new file mode 100644 index 0000000000000..0d0cd72be663b --- /dev/null +++ b/components/proofly/actions/toggle-campaign/toggle-campaign.mjs @@ -0,0 +1,43 @@ +import app from "../../proofly.app.mjs"; + +export default { + key: "proofly-toggle-campaign", + name: "Toggle Campaign Status", + description: "Switch a campaign's status between active and inactive. [See the documentation](https://proofly.io/developers)", + version: "0.0.1", + type: "action", + props: { + app, + campaignId: { + propDefinition: [ + app, + "campaignId", + ], + }, + }, + methods: { + switchCampaignStatus({ + campaignId, ...args + }) { + return this.app.put({ + path: `/campaign/${campaignId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + campaignId, + switchCampaignStatus, + } = this; + + const response = await switchCampaignStatus({ + $, + campaignId, + }); + + $.export("$summary", `Successfully toggled campaign with status \`${response.status}\` and message \`${response.data}\``); + + return response; + }, +}; diff --git a/components/proofly/common/constants.mjs b/components/proofly/common/constants.mjs new file mode 100644 index 0000000000000..65767187fe85a --- /dev/null +++ b/components/proofly/common/constants.mjs @@ -0,0 +1,7 @@ +const BASE_URL = "https://proofly.io"; +const VERSION_PATH = "/api"; + +export default { + BASE_URL, + VERSION_PATH, +}; diff --git a/components/proofly/package.json b/components/proofly/package.json index 41a5d1e8a379f..766fb3c18933e 100644 --- a/components/proofly/package.json +++ b/components/proofly/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/proofly", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Proofly Components", "main": "proofly.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" } -} \ No newline at end of file +} diff --git a/components/proofly/proofly.app.mjs b/components/proofly/proofly.app.mjs index d4ed85a19db61..8fbc659f424e1 100644 --- a/components/proofly/proofly.app.mjs +++ b/components/proofly/proofly.app.mjs @@ -1,11 +1,95 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "proofly", - propDefinitions: {}, + propDefinitions: { + campaignId: { + type: "string", + label: "Campaign ID", + description: "The unique identifier for the campaign", + async options() { + const { data } = await this.listCampaigns(); + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + notificationId: { + type: "string", + label: "Notification ID", + description: "The unique identifier for the notification", + async options({ campaignId }) { + const { data } = await this.listNotifications({ + campaignId, + }); + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "X-Api-Key": this.$auth.api_key, + }; + }, + async _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + const config = { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }; + + const response = await axios($, config); + + if (response?.ok !== true) { + throw new Error(`Response Error: ${JSON.stringify(response)}`); + } + + return response; + }, + put(args = {}) { + return this._makeRequest({ + method: "put", + ...args, + }); + }, + listCampaigns(args = {}) { + return this._makeRequest({ + path: "/campaigns", + ...args, + }); + }, + listNotifications({ + campaignId, ...args + }) { + return this._makeRequest({ + path: `/campaign/${campaignId}`, + ...args, + }); + }, + listData({ + notificationId, ...args + }) { + return this._makeRequest({ + path: `/data/${notificationId}`, + ...args, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/proofly/sources/common/polling.mjs b/components/proofly/sources/common/polling.mjs new file mode 100644 index 0000000000000..97e1ca75b5882 --- /dev/null +++ b/components/proofly/sources/common/polling.mjs @@ -0,0 +1,53 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../proofly.app.mjs"; + +export default { + props: { + app, + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getResourceName() { + throw new ConfigurationError("getResourceName is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + }, + async run() { + const { + getResourcesFn, + getResourceName, + getResourcesFnArgs, + processResource, + } = this; + + const resourcesFn = getResourcesFn(); + const { [getResourceName()]: resources } = + await resourcesFn(getResourcesFnArgs()); + + Array.from(resources) + .reverse() + .forEach(processResource); + }, +}; diff --git a/components/proofly/sources/new-notification-data-created/new-notification-data-created.mjs b/components/proofly/sources/new-notification-data-created/new-notification-data-created.mjs new file mode 100644 index 0000000000000..45762fa59cca6 --- /dev/null +++ b/components/proofly/sources/new-notification-data-created/new-notification-data-created.mjs @@ -0,0 +1,52 @@ +import common from "../common/polling.mjs"; + +export default { + ...common, + key: "proofly-new-notification-data-created", + name: "New Notification Data Created", + description: "Emit new event when notification data is received. [See the documentation](https://proofly.io/developers)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + campaignId: { + propDefinition: [ + common.props.app, + "campaignId", + ], + }, + notificationId: { + propDefinition: [ + common.props.app, + "notificationId", + ({ campaignId }) => ({ + campaignId, + }), + ], + }, + }, + methods: { + ...common.methods, + getResourceName() { + return "data"; + }, + getResourcesFn() { + return this.app.listData; + }, + getResourcesFnArgs() { + return { + debug: true, + notificationId: this.notificationId, + }; + }, + generateMeta(resource) { + const ts = Date.parse(resource.date); + return { + id: ts, + summary: "New Notification Data", + ts, + }; + }, + }, +}; diff --git a/components/propelauth/README.md b/components/propelauth/README.md index 185973d0376f2..07e39c7e7568a 100644 --- a/components/propelauth/README.md +++ b/components/propelauth/README.md @@ -1,20 +1,11 @@ # Overview -Using the PropelAuth API, you can build a wide variety of authentication -options for your applications or websites. No matter your needs, the API lets -you quickly and easily incorporate secure authentication into your project, -allowing you to protect user data and keep your system secure. Here are some -examples of the types of authentication you can build with the PropelAuth API: +PropelAuth provides a robust API for managing authentication and user management in your applications. By leveraging the PropelAuth API on Pipedream, you can seamlessly integrate authentication flows, user data synchronization, and access management with your existing services and workflows. This allows for the creation of secure, scalable, and efficient automations that can react to user events, update permissions, and maintain user data across various platforms. -- Password-based authentication - Create custom login forms and securely store - hashed passwords in your system. -- Social logins - Enable your users to sign in via popular social network - accounts like Facebook, Twitter, and more. -- OAuth authentication - Allow users to securely log in with identity providers - like Google, Microsoft, and more. -- Multi-factor authentication - Implement additional layers of security, such - as SMS verification, biometric authentication, and more. -- Access control - Create custom policies to control user access to specific - resources and services. -- Self-service password reset - Allow users to securely reset their passwords - without jeopardizing the security of your system. +# Example Use Cases + +- **User Onboarding Automation**: Automate the onboarding process by using PropelAuth to create new user accounts. Once a user is created, trigger a Pipedream workflow to send a personalized welcome email using the SendGrid app, add the user's details to a Google Sheets spreadsheet for record-keeping, and assign initial roles or permissions within your application. + +- **Real-time Access Revocation**: Monitor user activity or subscription status using PropelAuth, and set up a Pipedream workflow that instantly revokes access or deactivates accounts when certain conditions are met. Connect this workflow to your Slack app to notify your admin team whenever an account is deactivated for immediate follow-up or auditing. + +- **Synchronized User Profiles**: Keep user profiles in sync across multiple platforms. When a user updates their profile information in PropelAuth, trigger a Pipedream workflow to update that information in other systems such as CRM tools like Salesforce or marketing platforms like Mailchimp to maintain data consistency and enable personalized marketing campaigns. diff --git a/components/propeller/README.md b/components/propeller/README.md index 9452dc24764d1..9b67b163ddd1c 100644 --- a/components/propeller/README.md +++ b/components/propeller/README.md @@ -1,31 +1,11 @@ # Overview -Welcome to the Propeller API, the world's most comprehensive platform for -retail development! With Propeller, you have the capability to build powerful -and innovative solutions that meet the specific needs of your online business. -With Propeller's robust suite of APIs, you can create everything from ecommerce -sites and marketplaces, to mobile applications and progressive web -applications. +Propeller API offers a powerful suite of e-commerce tools enabling businesses to streamline their online sales processes. With the Propeller API, you can manage products, orders, customers, and inventory in real-time. Automating these e-commerce operations through Pipedream allows for seamless integration with other services, such as CRMs, email marketing platforms, and accounting software, to create a cohesive ecosystem for your online business. -Propeller's APIs are designed to provide access to platform functionality and -data in an easy-to-use and consistent manner. With Propeller, you can create: +# Example Use Cases -- Ecommerce sites -- Marketplaces -- Mobile applications -- Progressive web applications -- Admin dashboards -- Analytics and reporting dashboards -- Mobile payment and checkout solutions -- Social logins and user profiles -- Content management solutions -- Shopping and payments integrations -- Search and filter solutions -- Shopping cart and checkout solutions -- Promotion and discount solutions -- Shipping and tax integrations -- Customer segments and loyalty programs -- Recommendation solutions -- Site optimization solutions -- Content delivery solutions -- Virtual reality solutions +- **Automated Order Fulfillment Workflow**: When a new order is placed via Propeller, trigger a Pipedream workflow to capture the order details. The workflow can then notify the warehouse to ship the product, update the inventory, and send an order confirmation email to the customer using a service like SendGrid. + +- **Customer Data Synchronization**: Whenever a new customer is added in Propeller, use Pipedream to synchronize this data with a CRM platform like Salesforce. The workflow would transfer customer details such as name, contact information, and purchase history to maintain updated records across both systems. + +- **Real-time Inventory Management**: Set up a Pipedream workflow to monitor changes in inventory levels on Propeller. Connect this with Slack or another messaging app to send instant alerts when stock for a popular item is low, ensuring timely restocking and preventing lost sales due to out-of-stock items. diff --git a/components/proposify/package.json b/components/proposify/package.json new file mode 100644 index 0000000000000..f3290d0a78298 --- /dev/null +++ b/components/proposify/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/proposify", + "version": "0.0.1", + "description": "Pipedream Proposify Components", + "main": "proposify.app.mjs", + "keywords": [ + "pipedream", + "proposify" + ], + "homepage": "https://pipedream.com/apps/proposify", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/proposify/proposify.app.mjs b/components/proposify/proposify.app.mjs new file mode 100644 index 0000000000000..40756cfa94ffb --- /dev/null +++ b/components/proposify/proposify.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "proposify", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/proprofs_quiz_maker/README.md b/components/proprofs_quiz_maker/README.md new file mode 100644 index 0000000000000..52d99dca515d3 --- /dev/null +++ b/components/proprofs_quiz_maker/README.md @@ -0,0 +1,11 @@ +# Overview + +The ProProfs Quiz Maker API lets you manage quizzes, questions, and results programmatically. With it, you can automate the creation of quizzes, extract quiz data, and integrate quiz results with other systems. On Pipedream, you can create workflows that trigger on a specific event, such as a new quiz completion, and connect with hundreds of other apps to streamline your processes or enhance your data analysis. + +# Example Use Cases + +- **Sync Quiz Results to Google Sheets**: Every time a quiz is completed, the workflow triggers, fetching the results and appending them to a Google Sheets spreadsheet. This allows you to analyze quiz data with the powerful tools available in Google Sheets. + +- **Send Quiz Completion Notifications via Email**: Set up a workflow to send customized email notifications using an email service like SendGrid whenever a participant finishes a quiz. This could be useful for notifying teachers, managers, or team leaders about the performance of the quiz taker. + +- **Create a Quiz Dashboard with Retool**: Combine quiz results with other data sources by sending the data to Retool. You can create an interactive dashboard that displays real-time quiz analytics, helping educators or trainers to make informed decisions based on the performance statistics. diff --git a/components/proprofs_quiz_maker/package.json b/components/proprofs_quiz_maker/package.json new file mode 100644 index 0000000000000..74c3b7a13900b --- /dev/null +++ b/components/proprofs_quiz_maker/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/proprofs_quiz_maker", + "version": "0.0.1", + "description": "Pipedream ProProfs Quiz Maker Components", + "main": "proprofs_quiz_maker.app.mjs", + "keywords": [ + "pipedream", + "proprofs_quiz_maker" + ], + "homepage": "https://pipedream.com/apps/proprofs_quiz_maker", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/proprofs_quiz_maker/proprofs_quiz_maker.app.mjs b/components/proprofs_quiz_maker/proprofs_quiz_maker.app.mjs new file mode 100644 index 0000000000000..81a91c952fbc8 --- /dev/null +++ b/components/proprofs_quiz_maker/proprofs_quiz_maker.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "proprofs_quiz_maker", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/proworkflow/README.md b/components/proworkflow/README.md index 79b8f6d46b519..a38d75fba4a10 100644 --- a/components/proworkflow/README.md +++ b/components/proworkflow/README.md @@ -1,22 +1,11 @@ # Overview -The ProWorkflow API allows developers to easily access and make use of your -ProWorkflow data and functions in many ways. With ProWorkflow's extensive API -capabilities, developers can create highly customized and tailored applications -to perform powerful project management operations. Here are just a few examples -of what can be built with the ProWorkflow API: +The ProWorkflow API offers a powerhouse of project management and workflow automation capabilities. With Pipedream, you can leverage these abilities to create custom integrations that boost productivity and streamline operations. By tapping into ProWorkflow's endpoints, you can automate project creation, task assignments, time tracking, and report generation. This API dovetails neatly with other apps on Pipedream, allowing you to create multifaceted workflows that respond in real-time to external triggers, such as emails, form submissions, and calendar events. -- Create custom reports that give detailed insights from ProWorkflow data -- Create automated project processes based on triggers and events -- Create real-time synchronizations between ProWorkflow and other third-party - applications -- Leverage ProWorkflow's processing power to deploy specialized project - functions -- Develop custom mobile applications to use ProWorkflow on the go -- Develop integrations between ProWorkflow and other online tools and - applications -- Integrate ProWorkflow into your existing infrastructure and business - processes -- Create project-level dashboards that are personalized to each user -- Create custom workflow rules that determine when tasks will start and be - completed +# Example Use Cases + +- **Project Creation from CRM Deals**: When a new deal is marked as 'won' in a CRM like Salesforce, automatically create a corresponding project in ProWorkflow. Set up task assignments based on the deal specs, ensuring your team can kickstart work without delay. + +- **Task Assignment via Slack**: Automate the distribution of tasks in ProWorkflow by monitoring a dedicated Slack channel. When someone posts a new task request, parse the message and create a task in ProWorkflow, tagging the relevant team members and setting deadlines based on predefined criteria. + +- **Time Tracking for Invoicing**: Connect ProWorkflow to a time tracking tool like Toggl. When a project reaches completion, trigger a workflow that compiles time tracking data and generates an invoice draft in an accounting app like QuickBooks, thus ensuring billing is accurate and timely. diff --git a/components/proxiedmail/package.json b/components/proxiedmail/package.json new file mode 100644 index 0000000000000..a423cf87d5069 --- /dev/null +++ b/components/proxiedmail/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/proxiedmail", + "version": "0.0.1", + "description": "Pipedream ProxiedMail Components", + "main": "proxiedmail.app.mjs", + "keywords": [ + "pipedream", + "proxiedmail" + ], + "homepage": "https://pipedream.com/apps/proxiedmail", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/proxiedmail/proxiedmail.app.mjs b/components/proxiedmail/proxiedmail.app.mjs new file mode 100644 index 0000000000000..e7b0c4274bc79 --- /dev/null +++ b/components/proxiedmail/proxiedmail.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "proxiedmail", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/proxy_spider/README.md b/components/proxy_spider/README.md new file mode 100644 index 0000000000000..4f25e4579e7da --- /dev/null +++ b/components/proxy_spider/README.md @@ -0,0 +1,11 @@ +# Overview + +The Proxy Spider API lets you scrape and gather data from the web without the usual hassle of IP blocks or CAPTCHAs. By leveraging Pipedream's integration capabilities, you can automate the extraction of web data and manage proxy pools seamlessly. This means you can focus on what to do with the data you gather, rather than worrying about the technicalities of acquiring it. Within Pipedream's serverless platform, you could set up workflows that trigger based on a variety of events and use the Proxy Spider API to fetch data as needed. + +# Example Use Cases + +- **Real-time Price Monitoring**: Keep track of product prices across different e-commerce platforms. When a Pipedream schedule triggers the workflow, it can use Proxy Spider to scrape pricing information anonymously, and then save this data to a Google Sheet or send a notification if prices drop below a certain threshold. + +- **Content Change Detection**: Monitor changes to the content of a critical webpage. Use Proxy Spider to scrape the page at regular intervals. If the content changes, trigger a workflow that alerts you via email or Slack. This can be vital for tracking updates on competitor websites or monitoring legal pages for compliance. + +- **SEO Analysis Automation**: Automate the process of gathering SEO-relevant data from multiple websites. Combine Proxy Spider with an SEO analysis tool available on Pipedream, like Moz or Ahrefs. This workflow can periodically scrape required data and pass it to the SEO tool for a comprehensive report, helping you keep tabs on keyword rankings or backlink profiles. diff --git a/components/proxy_spider/package.json b/components/proxy_spider/package.json new file mode 100644 index 0000000000000..96c455ce1c02b --- /dev/null +++ b/components/proxy_spider/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/proxy_spider", + "version": "0.0.1", + "description": "Pipedream Proxy Spider Components", + "main": "proxy_spider.app.mjs", + "keywords": [ + "pipedream", + "proxy_spider" + ], + "homepage": "https://pipedream.com/apps/proxy_spider", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/proxy_spider/proxy_spider.app.mjs b/components/proxy_spider/proxy_spider.app.mjs new file mode 100644 index 0000000000000..1a35ac2dca937 --- /dev/null +++ b/components/proxy_spider/proxy_spider.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "proxy_spider", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/proxycurl/README.md b/components/proxycurl/README.md new file mode 100644 index 0000000000000..2e08d450fbb57 --- /dev/null +++ b/components/proxycurl/README.md @@ -0,0 +1,11 @@ +# Overview + +The Proxycurl API is a tool for scraping LinkedIn data, enabling users to extract professional information from LinkedIn profiles and company pages. When integrated with Pipedream's serverless platform, Proxycurl can automate the collection of LinkedIn data, which can be used for lead generation, market research, or recruitment. Pipedream's capabilities allow users to create workflows that respond to various triggers, process the data with Proxycurl, and then output it to desired destinations or further manipulate it with other apps and services. + +# Example Use Cases + +- **Enrich CRM with LinkedIn Data**: Automatically enrich contact records in your CRM when new leads are added. Proxycurl can fetch detailed professional information, which can then be sent to the CRM to update contact profiles. + +- **LinkedIn Lead Generation**: Trigger a workflow when a new form is submitted on your website. Use Proxycurl to find LinkedIn profiles matching the submission info and create a list of potential leads to be added to an email marketing tool like Mailchimp for outreach campaigns. + +- **Automated Recruitment Pipeline**: Set up a workflow to monitor job application submissions. Use Proxycurl to pull LinkedIn profiles for applicants and filter qualified candidates based on their work history and skills. Candidates can then be moved to an ATS like Greenhouse or sent to a Google Sheet for review. diff --git a/components/publisherkit/README.md b/components/publisherkit/README.md new file mode 100644 index 0000000000000..5983da16f5e67 --- /dev/null +++ b/components/publisherkit/README.md @@ -0,0 +1,11 @@ +# Overview + +The PublisherKit API offers a suite of tools for digital content management, including the ability to manage and distribute various multimedia content like articles, videos, and podcasts. Integrating this API with Pipedream allows you to automate content workflows, sync data across platforms, and react to events in real-time. For instance, you could automatically publish scheduled content, analyze content performance, or update cross-platform content statuses. + +# Example Use Cases + +- **Content Publishing Automation**: Automate the process of content publication. When a new piece of content is marked as 'ready for publication' in your CMS, trigger a Pipedream workflow that uses the PublisherKit API to publish the content across multiple platforms. + +- **Real-time Content Performance Tracking**: Use Pipedream to create a workflow that subscribes to PublisherKit's events for content views or shares. Then, feed this data into an analytics service like Google Analytics or a custom dashboard powered by apps like Geckoboard or Datadog for real-time performance tracking. + +- **Cross-platform Content Synchronization**: Build a workflow that listens for updates to content in PublisherKit and synchronizes those changes with other content management platforms like WordPress or Drupal. This ensures a uniform content experience across all your digital properties. diff --git a/components/pulsetic/README.md b/components/pulsetic/README.md new file mode 100644 index 0000000000000..cea6cb78d57e2 --- /dev/null +++ b/components/pulsetic/README.md @@ -0,0 +1,11 @@ +# Overview + +Pulsetic API lets you monitor your websites and servers, sending notifications when downtime occurs or performance issues arise. With Pipedream, you can leverage this API to create powerful, automated workflows that respond to Pulsetic events in real-time. For instance, you can automate incident management by connecting Pulsetic to ticketing systems, trigger alerts in communication channels, or log performance data for analysis. Pipedream's zero-management data stores also allow you to keep track of incident history without needing a separate database. + +# Example Use Cases + +- **Automated Incident Reporting**: When Pulsetic detects an outage, this workflow automatically creates a ticket in a service like Jira or Zendesk. It ensures that support teams are immediately aware of issues, streamlining the response process. + +- **Performance Alerting in Slack**: Set up notifications to a Slack channel when Pulsetic reports a slow response time or downtime. This keeps your team in the loop, enabling quick action to resolve the issue and maintain service quality. + +- **Downtime Log with Google Sheets**: Every time Pulsetic reports downtime, the workflow appends a new row to a Google Sheet. This provides a historical log of incidents, aiding in analyzing patterns and improving future uptime. diff --git a/components/pulsetic/package.json b/components/pulsetic/package.json index d2e85cdf946a7..e607a435f64a5 100644 --- a/components/pulsetic/package.json +++ b/components/pulsetic/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/pulsetic", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Pulsetic Components", "main": "pulsetic.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" } -} \ No newline at end of file +} + diff --git a/components/pulsetic/pulsetic.app.mjs b/components/pulsetic/pulsetic.app.mjs index f74aa98c12d05..15d3fef635e6a 100644 --- a/components/pulsetic/pulsetic.app.mjs +++ b/components/pulsetic/pulsetic.app.mjs @@ -1,11 +1,56 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "pulsetic", - propDefinitions: {}, + propDefinitions: { + monitorId: { + type: "string", + label: "Monitor ID", + description: "The ID of the monitor to retrieve events for", + async options() { + const data = await this.listMonitors(); + + return data.map(({ + id: value, name, url, + }) => ({ + label: name || url, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.pulsetic.com/api/public"; + }, + _headers() { + return { + "Authorization": `${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listEvents({ + monitorId, ...opts + }) { + return this._makeRequest({ + path: `/${monitorId}/events`, + ...opts, + }); + }, + listMonitors(opts = {}) { + return this._makeRequest({ + path: "/monitors", + ...opts, + }); }, }, }; diff --git a/components/pulsetic/sources/new-monitor-event/new-monitor-event.mjs b/components/pulsetic/sources/new-monitor-event/new-monitor-event.mjs new file mode 100644 index 0000000000000..5b3569cfad0e2 --- /dev/null +++ b/components/pulsetic/sources/new-monitor-event/new-monitor-event.mjs @@ -0,0 +1,82 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import pulsetic from "../../pulsetic.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "pulsetic-new-monitor-event", + name: "New Monitor Event", + description: "Emit new event when a monitor event occurs.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + pulsetic, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + monitorId: { + propDefinition: [ + pulsetic, + "monitorId", + ], + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01T00:00:00Z"; + }, + _setLastDate(createdAt) { + this.db.set("lastDate", createdAt); + }, + generateMeta(event) { + const ts = Date.parse(event.created_at); + return { + id: ts, + summary: `New event published with type: ${event.type}`, + ts: ts, + }; + }, + getParams() { + return {}; + }, + async startEvent(maxResults = 0) { + const lastDate = this._getLastDate(); + + const data = await this.pulsetic.listEvents({ + monitorId: this.monitorId, + params: { + start_dt: lastDate, + end_dt: new Date(), + }, + }); + + const responseArray = []; + for await (const item of data) { + if (Date.parse(item.created_at) <= Date.parse(lastDate)) break; + responseArray.push(item); + } + + if (responseArray.length) this._setLastDate(responseArray[0].created_at); + if (maxResults && responseArray.length > maxResults) { + responseArray.length = maxResults; + } + + for (const item of responseArray.reverse()) { + this.$emit(item, this.generateMeta(item)); + } + }, + }, + hooks: { + async deploy() { + await this.startEvent(25); + }, + }, + async run() { + await this.startEvent(); + }, + sampleEmit, +}; diff --git a/components/pulsetic/sources/new-monitor-event/test-event.mjs b/components/pulsetic/sources/new-monitor-event/test-event.mjs new file mode 100644 index 0000000000000..1898375114246 --- /dev/null +++ b/components/pulsetic/sources/new-monitor-event/test-event.mjs @@ -0,0 +1,6 @@ +export default { + "monitor_id": 1, + "type": "MonitorOffline", + "created_at": "2023-04-12T16:36:32.000000Z", + "updated_at": "2023-04-12T16:36:32.000000Z" +} \ No newline at end of file diff --git a/components/pumble/actions/create-channel/create-channel.mjs b/components/pumble/actions/create-channel/create-channel.mjs new file mode 100644 index 0000000000000..c126ca9864740 --- /dev/null +++ b/components/pumble/actions/create-channel/create-channel.mjs @@ -0,0 +1,46 @@ +import pumble from "../../pumble.app.mjs"; + +export default { + key: "pumble-create-channel", + name: "Create Channel", + description: "Create a new channel in Pumble. [See the documentation](https://pumble.com/help/integrations/add-pumble-apps/api-keys-integration/#create-channel)", + version: "0.0.1", + type: "action", + props: { + pumble, + name: { + type: "string", + label: "Channel Name", + description: "Name of the new channel", + }, + type: { + type: "string", + label: "Type", + description: "Whether the new channel is public or private. Defaults to `public`", + options: [ + "PUBLIC", + "PRIVATE", + ], + default: "PUBLIC", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "Description of the new channel", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.pumble.createChannel({ + $, + data: { + name: this.name, + type: this.type, + description: this.description, + }, + }); + $.export("$summary", `Successfully created channel with ID ${response.id}`); + return response; + }, +}; diff --git a/components/pumble/actions/list-messages/list-messages.mjs b/components/pumble/actions/list-messages/list-messages.mjs new file mode 100644 index 0000000000000..c20f51fa00e18 --- /dev/null +++ b/components/pumble/actions/list-messages/list-messages.mjs @@ -0,0 +1,30 @@ +import pumble from "../../pumble.app.mjs"; + +export default { + key: "pumble-list-messages", + name: "List Messages", + description: "List messages in a channel. [See the documentation](https://pumble.com/help/integrations/add-pumble-apps/api-keys-integration/#list-messages-in-a-channel)", + version: "0.0.1", + type: "action", + props: { + pumble, + channel: { + propDefinition: [ + pumble, + "channel", + ], + }, + }, + async run({ $ }) { + const { messages } = await this.pumble.listMessages({ + $, + params: { + channel: this.channel, + }, + }); + $.export("$summary", `Successfully retrieved ${messages.length} message${messages.length === 1 + ? "" + : "s"}`); + return messages; + }, +}; diff --git a/components/pumble/actions/send-message/send-message.mjs b/components/pumble/actions/send-message/send-message.mjs new file mode 100644 index 0000000000000..034be02d08e5a --- /dev/null +++ b/components/pumble/actions/send-message/send-message.mjs @@ -0,0 +1,42 @@ +import pumble from "../../pumble.app.mjs"; + +export default { + key: "pumble-send-message", + name: "Send Message", + description: "Send a message to a channel in Pumble. [See the documentation](https://pumble.com/help/integrations/add-pumble-apps/api-keys-integration/#send-messages)", + version: "0.0.1", + type: "action", + props: { + pumble, + channel: { + propDefinition: [ + pumble, + "channel", + ], + }, + text: { + type: "string", + label: "Text", + description: "The message to send", + }, + asBot: { + type: "boolean", + label: "As Bot", + description: "Whether to send the message from your personal account or as a bot. Defaults to `false`", + default: false, + optional: true, + }, + }, + async run({ $ }) { + const response = await this.pumble.sendMessage({ + $, + data: { + channel: this.channel, + text: this.text, + asBot: this.asBot, + }, + }); + $.export("$summary", `Successfully sent message with ID ${response.id}`); + return response; + }, +}; diff --git a/components/pumble/package.json b/components/pumble/package.json new file mode 100644 index 0000000000000..5d079b0ddd618 --- /dev/null +++ b/components/pumble/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/pumble", + "version": "0.1.0", + "description": "Pipedream Pumble Components", + "main": "pumble.app.mjs", + "keywords": [ + "pipedream", + "pumble" + ], + "homepage": "https://pipedream.com/apps/pumble", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} diff --git a/components/pumble/pumble.app.mjs b/components/pumble/pumble.app.mjs new file mode 100644 index 0000000000000..d2ff3054c8c5b --- /dev/null +++ b/components/pumble/pumble.app.mjs @@ -0,0 +1,66 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "pumble", + propDefinitions: { + channel: { + type: "string", + label: "Channel", + description: "The name of the channel", + async options() { + const channels = await this.listChannels(); + return channels + ?.filter(({ channel }) => channel.name) + ?.map(({ channel }) => channel.name) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://pumble-api-keys.addons.marketplace.cake.com"; + }, + _headers() { + return { + "Api-Key": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._headers(), + ...opts, + }); + }, + listChannels(opts = {}) { + return this._makeRequest({ + path: "/listChannels", + ...opts, + }); + }, + listMessages(opts = {}) { + return this._makeRequest({ + path: "/listMessages", + ...opts, + }); + }, + createChannel(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/createChannel", + ...opts, + }); + }, + sendMessage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/sendMessage", + ...opts, + }); + }, + }, +}; diff --git a/components/pumble/sources/common/base.mjs b/components/pumble/sources/common/base.mjs new file mode 100644 index 0000000000000..58d51a9f018eb --- /dev/null +++ b/components/pumble/sources/common/base.mjs @@ -0,0 +1,24 @@ +import pumble from "../../pumble.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + pumble, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + emitEvent(item) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, +}; diff --git a/components/pumble/sources/new-channel-created/new-channel-created.mjs b/components/pumble/sources/new-channel-created/new-channel-created.mjs new file mode 100644 index 0000000000000..5aa90a147d8b9 --- /dev/null +++ b/components/pumble/sources/new-channel-created/new-channel-created.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "pumble-new-channel-created", + name: "New Channel Created", + description: "Emit new event when a new channel is created in Pumble", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + generateMeta(channel) { + return { + id: channel.id, + summary: `New Channel: ${channel.name}`, + ts: Date.now(), + }; + }, + }, + async run() { + const channels = await this.pumble.listChannels(); + channels + .filter(({ channel }) => channel.name) + .forEach(({ channel }) => this.emitEvent(channel)); + }, + sampleEmit, +}; diff --git a/components/pumble/sources/new-channel-created/test-event.mjs b/components/pumble/sources/new-channel-created/test-event.mjs new file mode 100644 index 0000000000000..47dd44dd37732 --- /dev/null +++ b/components/pumble/sources/new-channel-created/test-event.mjs @@ -0,0 +1,28 @@ +export default { + "id": "66575d32d3c8847e4c85e00f", + "workspaceId": "66575d31d3c8847e4c85e009", + "channelType": "PUBLIC", + "name": "general", + "description": "", + "isMember": true, + "isMuted": false, + "isHidden": false, + "isArchived": false, + "isPumbleBot": false, + "isAddonBot": false, + "lastMarkTimestamp": "2024-05-29T17:25:50.805Z", + "lastMarkTimestampMilli": 1717003550805, + "isMain": true, + "isInitial": true, + "sectionId": "", + "postingPermissions": { + "allowThreads": true, + "allowMentions": true, + "postingPermissionsGroup": "EVERYONE", + "workspaceUserIds": [] + }, + "desktopNotificationPreferences": null, + "mobileNotificationPreferences": null, + "notifyAboutRepliesInThreads": false, + "addedById": "66575d31d3c8847e4c85e00a" +} \ No newline at end of file diff --git a/components/pumble/sources/new-message-in-channel/new-message-in-channel.mjs b/components/pumble/sources/new-message-in-channel/new-message-in-channel.mjs new file mode 100644 index 0000000000000..dadca1e90ff7f --- /dev/null +++ b/components/pumble/sources/new-message-in-channel/new-message-in-channel.mjs @@ -0,0 +1,57 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "pumble-new-message-in-channel", + name: "New Message in Channel", + description: "Emit new event when a message is posted in the specified channel in Pumble", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + channel: { + propDefinition: [ + common.props.pumble, + "channel", + ], + }, + }, + methods: { + ...common.methods, + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + generateMeta(message) { + return { + id: message.id, + summary: `New Message: ${message.text}`, + ts: Date.parse(message.timestamp), + }; + }, + }, + async run() { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + + const { messages } = await this.pumble.listMessages({ + params: { + channel: this.channel, + }, + }); + for (const message of messages) { + const ts = Date.parse(message.timestamp); + if (ts >= lastTs) { + this.emitEvent(message); + maxTs = Math.max(ts, maxTs); + } + } + + this._setLastTs(maxTs); + }, + sampleEmit, +}; diff --git a/components/pumble/sources/new-message-in-channel/test-event.mjs b/components/pumble/sources/new-message-in-channel/test-event.mjs new file mode 100644 index 0000000000000..40fc5a2a13175 --- /dev/null +++ b/components/pumble/sources/new-message-in-channel/test-event.mjs @@ -0,0 +1,40 @@ +export default { + "id": "6657650a3ca2f542afff7151", + "workspaceId": "66575d31d3c8847e4c85e009", + "channelId": "66575d32d3c8847e4c85e00f", + "author": "66575d31d3c8847e4c85e00a", + "text": "hello world", + "timestamp": "2024-05-29T17:25:30Z", + "timestampMilli": 1717003530000, + "subtype": "", + "reactions": [], + "linkPreviews": [], + "isFollowing": true, + "threadRootInfo": null, + "threadReplyInfo": null, + "files": [], + "deleted": false, + "edited": false, + "localId": "665765820b689728fcc64b52", + "attachments": [], + "sharedMessage": null, + "savedTimestampMilli": 0, + "blocks": [ + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "text", + "text": "hello world" + } + ] + } + ] + } + ], + "meta": null, + "authorAppId": null +} \ No newline at end of file diff --git a/components/puppeteer/actions/common/common.mjs b/components/puppeteer/actions/common/common.mjs new file mode 100644 index 0000000000000..b42ee037a62ff --- /dev/null +++ b/components/puppeteer/actions/common/common.mjs @@ -0,0 +1,19 @@ +export default { + props: { + url: { + type: "string", + label: "URL", + description: + "The URL of the page to scrape. For example, `https://example.com`.", + }, + }, + methods: { + normalizeUrl() { + let url = this.url; + if (!url.startsWith("http")) { + url = `https://${url}`; + } + return url; + }, + }, +}; diff --git a/components/puppeteer/actions/get-html/get-html.mjs b/components/puppeteer/actions/get-html/get-html.mjs index ffcbc2a7ec38d..b4af0c99cc5de 100644 --- a/components/puppeteer/actions/get-html/get-html.mjs +++ b/components/puppeteer/actions/get-html/get-html.mjs @@ -1,28 +1,28 @@ import puppeteer from "../../puppeteer.app.mjs"; +import common from "../common/common.mjs"; export default { + ...common, key: "puppeteer-get-html", name: "Get HTML", - description: "Get the HTML of a webpage using Puppeteer. [See the documentation](https://pptr.dev/api/puppeteer.page.content)", - version: "1.0.1", + description: + "Get the HTML of a webpage using Puppeteer. [See the documentation](https://pptr.dev/api/puppeteer.page.content) for details.", + version: "1.0.2", type: "action", props: { puppeteer, - url: { - type: "string", - label: "URL", - description: "The URL of the page to scrape.", - }, + ...common.props, }, async run({ $ }) { + const url = this.normalizeUrl(); const browser = await this.puppeteer.launch(); const page = await browser.newPage(); - await page.goto(this.url); + await page.goto(url); const html = await page.content(); await browser.close(); if (html) { - $.export("$summary", "Successfully retrieved HTML from page."); + $.export("$summary", `Successfully retrieved HTML from ${url}`); } return html; diff --git a/components/puppeteer/actions/get-page-title/get-page-title.mjs b/components/puppeteer/actions/get-page-title/get-page-title.mjs index 386a76c5216ed..b85ab74337be7 100644 --- a/components/puppeteer/actions/get-page-title/get-page-title.mjs +++ b/components/puppeteer/actions/get-page-title/get-page-title.mjs @@ -1,28 +1,28 @@ import puppeteer from "../../puppeteer.app.mjs"; +import common from "../common/common.mjs"; export default { + ...common, key: "puppeteer-get-page-title", name: "Get Page Title", - description: "Get the title of a webpage using Puppeteer. [See the documentation](https://pptr.dev/api/puppeteer.page.title)", - version: "1.0.1", + description: + "Get the title of a webpage using Puppeteer. [See the documentation](https://pptr.dev/api/puppeteer.page.title)", + version: "1.0.2", type: "action", props: { puppeteer, - url: { - type: "string", - label: "URL", - description: "The URL of the webpage to get the title from.", - }, + ...common.props, }, async run({ $ }) { + const url = this.normalizeUrl(); const browser = await this.puppeteer.launch(); const page = await browser.newPage(); - await page.goto(this.url); + await page.goto(url); const title = await page.title(); await browser.close(); if (title) { - $.export("$summary", `Successfully retrieved title ${title}.`); + $.export("$summary", `Successfully retrieved the title from ${url}.`); } return title; diff --git a/components/puppeteer/actions/get-pdf/get-pdf.mjs b/components/puppeteer/actions/get-pdf/get-pdf.mjs index d7a6d3d3a52cb..683e1a281314a 100644 --- a/components/puppeteer/actions/get-pdf/get-pdf.mjs +++ b/components/puppeteer/actions/get-pdf/get-pdf.mjs @@ -1,24 +1,24 @@ import puppeteer from "../../puppeteer.app.mjs"; import constants from "../../common/constants.mjs"; +import common from "../common/common.mjs"; import fs from "fs"; export default { + ...common, key: "puppeteer-get-pdf", name: "Get PDF", - description: "Generate a PDF of a page using Puppeteer. [See the documentation](https://pptr.dev/api/puppeteer.page.pdf)", - version: "1.0.1", + description: + "Generate a PDF of a page using Puppeteer. [See the documentation](https://pptr.dev/api/puppeteer.page.pdf)", + version: "1.0.2", type: "action", props: { puppeteer, - url: { - type: "string", - label: "URL", - description: "The URL of the page to scrape.", - }, + ...common.props, downloadPath: { type: "string", label: "Download Path", - description: "Download the PDF to the `/tmp` directory with the specified filename", + description: + "Download the PDF to the `/tmp` directory with the specified filename", optional: true, }, displayHeaderFooter: { @@ -44,13 +44,15 @@ export default { headerTemplate: { type: "string", label: "Header Template", - description: "HTML template for the print header. Should be valid HTML with the following classes used to inject values into them: `date` - formatted print date, `title` - document title, `url` - document location, `pageNumber` - current page number, `totalPages` - total pages in the document.", + description: + "HTML template for the print header. Should be valid HTML with the following classes used to inject values into them: `date` - formatted print date, `title` - document title, `url` - document location, `pageNumber` - current page number, `totalPages` - total pages in the document.", optional: true, }, height: { type: "string", label: "Height", - description: "Sets the height of paper. You can pass in a number or a string with a unit.", + description: + "Sets the height of paper. You can pass in a number or a string with a unit.", optional: true, }, landscape: { @@ -87,7 +89,8 @@ export default { omitBackground: { type: "boolean", label: "Omit Background", - description: "Hides default white background and allows generating pdfs with transparency.", + description: + "Hides default white background and allows generating pdfs with transparency.", optional: true, default: false, }, @@ -100,7 +103,8 @@ export default { preferCSSPageSize: { type: "boolean", label: "Prefer CSS Page Size", - description: "Give any CSS @page size declared in the page priority over what is declared in the width or height or format option.", + description: + "Give any CSS @page size declared in the page priority over what is declared in the width or height or format option.", optional: true, default: false, }, @@ -114,7 +118,8 @@ export default { scale: { type: "string", label: "Scale", - description: "Scales the rendering of the web page. Amount must be between 0.1 and 2.", + description: + "Scales the rendering of the web page. Amount must be between 0.1 and 2.", optional: true, }, timeout: { @@ -127,7 +132,8 @@ export default { width: { type: "string", label: "Width", - description: "Sets the width of paper. You can pass in a number or a string with a unit.", + description: + "Sets the width of paper. You can pass in a number or a string with a unit.", optional: true, }, }, @@ -165,18 +171,20 @@ export default { width: this.width, }; + const url = this.normalizeUrl(); const browser = await this.puppeteer.launch(); const page = await browser.newPage(); - await page.goto(this.url); + await page.goto(url); const pdf = await page.pdf(options); await browser.close(); - const filePath = pdf && this.downloadPath - ? await this.downloadToTMP(pdf) - : undefined; + const filePath = + pdf && this.downloadPath + ? await this.downloadToTMP(pdf) + : undefined; if (pdf) { - $.export("$summary", "Successfully generated PDF from page."); + $.export("$summary", `Successfully generated PDF from ${url}`); } return filePath diff --git a/components/puppeteer/actions/screenshot-page/screenshot-page.mjs b/components/puppeteer/actions/screenshot-page/screenshot-page.mjs index 6af498373160a..8951065064bf2 100644 --- a/components/puppeteer/actions/screenshot-page/screenshot-page.mjs +++ b/components/puppeteer/actions/screenshot-page/screenshot-page.mjs @@ -1,25 +1,25 @@ import puppeteer from "../../puppeteer.app.mjs"; import constants from "../../common/constants.mjs"; +import common from "../common/common.mjs"; import fs from "fs"; import { ConfigurationError } from "@pipedream/platform"; export default { + ...common, key: "puppeteer-screenshot-page", name: "Screenshot a Page", - description: "Captures a screenshot of a page using Puppeteer. [See the documentation](https://pptr.dev/api/puppeteer.page.screenshot)", - version: "1.0.1", + description: + "Captures a screenshot of a page using Puppeteer. [See the documentation](https://pptr.dev/api/puppeteer.page.screenshot)", + version: "1.0.2", type: "action", props: { puppeteer, - url: { - type: "string", - label: "URL", - description: "The URL of the page to scrape.", - }, + ...common.props, downloadPath: { type: "string", label: "Download Path", - description: "Download the screenshot to the `/tmp` directory with the specified filename", + description: + "Download the screenshot to the `/tmp` directory with the specified filename", optional: true, }, captureBeyondViewport: { @@ -71,7 +71,8 @@ export default { fromSurface: { type: "boolean", label: "From Surface", - description: "Capture the screenshot from the surface, rather than the view.", + description: + "Capture the screenshot from the surface, rather than the view.", optional: true, default: false, }, @@ -85,7 +86,8 @@ export default { omitBackground: { type: "boolean", label: "Omit Background", - description: "Hides default white background and allows capturing screenshots with transparency.", + description: + "Hides default white background and allows capturing screenshots with transparency.", optional: true, default: false, }, @@ -99,7 +101,8 @@ export default { quality: { type: "integer", label: "Quality", - description: "Quality of the image, between 0-100. Not applicable to png images.", + description: + "Quality of the image, between 0-100. Not applicable to png images.", optional: true, }, type: { @@ -121,20 +124,25 @@ export default { }, }, async run({ $ }) { - if ((this.clipHeight || this.clipWidth || this.clipX || this.clipY) - && !(this.clipHeight && this.clipWidth && this.clipX && this.clipY)) { - throw new ConfigurationError("Clip height, width, X, and Y must be specified to create clip."); + if ( + (this.clipHeight || this.clipWidth || this.clipX || this.clipY) && + !(this.clipHeight && this.clipWidth && this.clipX && this.clipY) + ) { + throw new ConfigurationError( + "Clip height, width, X, and Y must be specified to create clip.", + ); } - const clip = this.clipHeight || this.clipWidth || this.clipX || this.clipY - ? { - height: parseFloat(this.clipHeight), - scale: parseFloat(this.clipScale), - width: parseFloat(this.clipWidth), - x: parseFloat(this.clipX), - y: parseFloat(this.clipY), - } - : undefined; + const clip = + this.clipHeight || this.clipWidth || this.clipX || this.clipY + ? { + height: parseFloat(this.clipHeight), + scale: parseFloat(this.clipScale), + width: parseFloat(this.clipWidth), + x: parseFloat(this.clipX), + y: parseFloat(this.clipY), + } + : undefined; const options = { captureBeyondViewport: this.captureBeyondViewport, @@ -148,18 +156,20 @@ export default { type: this.type, }; + const url = this.normalizeUrl(); const browser = await this.puppeteer.launch(); const page = await browser.newPage(); - await page.goto(this.url); + await page.goto(url); const screenshot = await page.screenshot(options); await browser.close(); - const filePath = screenshot && this.downloadPath - ? await this.downloadToTMP(screenshot) - : undefined; + const filePath = + screenshot && this.downloadPath + ? await this.downloadToTMP(screenshot) + : undefined; if (screenshot) { - $.export("$summary", "Successfully captured screenshot from page."); + $.export("$summary", `Successfully captured screenshot from ${url}`); } return filePath diff --git a/components/puppeteer/package.json b/components/puppeteer/package.json index 8b6eed6959edf..e38f1b6c7e38c 100644 --- a/components/puppeteer/package.json +++ b/components/puppeteer/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/puppeteer", - "version": "1.0.1", + "version": "1.0.2", "description": "Pipedream Puppeteer Components", "main": "puppeteer.app.mjs", "keywords": [ diff --git a/components/push_by_techulus/README.md b/components/push_by_techulus/README.md new file mode 100644 index 0000000000000..9a36118dab415 --- /dev/null +++ b/components/push_by_techulus/README.md @@ -0,0 +1,11 @@ +# Overview + +The Push by Techulus API offers a straightforward way to send notifications directly to your devices. With this functionality within Pipedream, you can craft workflows that alert you or someone else when certain events happen in your apps or in your code. Think of it as wiring a custom doorbell for the digital events that matter to you. Whether you're tracking sales on your e-commerce platform, monitoring website uptime, or just want to get pinged when your long-running script finishes, Push by Techulus nudges you in real time. + +# Example Use Cases + +- **Sales Alert**: Set up a pipeline that monitors your e-commerce platform for new sales. Each time a sale goes through, trigger a Push notification to your phone with the sale details. You can use Pipedream's built-in support for Shopify or WooCommerce to easily detect new orders. + +- **Website Uptime Monitor**: Combine Push by Techulus with a Pipedream scheduled workflow that pings your website at regular intervals. If your site goes down or the response time exceeds a threshold, send an instant Push notification, allowing for rapid response to outages. + +- **CI/CD Completion Notification**: After a build or deployment process completes in your CI/CD pipeline, use the Push by Techulus API to notify your development team. This can be integrated with GitHub actions running on Pipedream to notify when a build passes or fails, keeping everyone in the loop. diff --git a/components/pushbullet/README.md b/components/pushbullet/README.md index 9770880fd2cc7..c365decce9f39 100644 --- a/components/pushbullet/README.md +++ b/components/pushbullet/README.md @@ -1,21 +1,11 @@ # Overview -The Pushbullet API is a great tool for building applications that can do a wide -variety of tasks. From pushing notifications to sending messages, the -Pushbullet API is a powerful resource for developers. Here are a few examples -of what you can build using the Pushbullet API: +Pushbullet acts as a bridge, connecting your devices with push notifications and file sharing capabilities. By leveraging the Pushbullet API on Pipedream, you can automate notifications for a host of events, enabling real-time alerts on your phone, browser, or desktop. Whether you're monitoring server uptimes, tracking sales, or simply staying on top of to-do lists, integrating Pushbullet with other services via Pipedream workflows can streamline your notification management and data sharing tasks. -- Mobile Apps - Using the Pushbullet API, you can create mobile apps that use - Pushbullet's notifications and messaging to send messages and information to - users. -- Chatbots - Chatbots can be created using the Pushbullet API to interact with - users. -- Real-time Alerts - With the Pushbullet API, you can create applications that - send real-time alerts to users, such as notifications about weather or - important news. -- Reminders & Tasks - Pushbullet's API can be used to create applications that - allow users to set reminders and tasks by sending them messages and - notifications. -- Automation - Automation applications can be built using the Pushbullet API to - automate tasks, such as setting up daily reminders or sending out messages - and notifications on a regular basis. +# Example Use Cases + +- **Real-Time Error Logging Notifications**: If your web application encounters a critical error, you can use the Pushbullet API to send an immediate notification. Set up a Pipedream workflow where an HTTP request triggers an event in your system that, upon error detection, pushes a notification through Pushbullet with error details. For enhanced efficiency, connect this with logging tools like Sentry or Loggly. + +- **E-commerce Sale Alert**: Stay informed about new orders on your e-commerce platform. Create a Pipedream workflow that listens for new Stripe charges. When a customer completes a purchase, Pipedream sends a message via Pushbullet with the sale details. This can help you track sales in real-time, providing instant gratification and allowing for quick response to high-value orders. + +- **IoT Device Monitoring**: Keep tabs on your IoT devices with Pushbullet notifications. Set up a Pipedream workflow where a Raspberry Pi publishes a message to an MQTT topic about its status, which Pipedream subscribes to. When specific conditions are met, like temperature thresholds being exceeded, Pipedream triggers a Pushbullet notification to alert you immediately, keeping you in the loop for timely interventions. diff --git a/components/pushbullet/package.json b/components/pushbullet/package.json new file mode 100644 index 0000000000000..fdc328eddaa1b --- /dev/null +++ b/components/pushbullet/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/pushbullet", + "version": "0.6.0", + "description": "Pipedream pushbullet Components", + "main": "pushbullet.app.mjs", + "keywords": [ + "pipedream", + "pushbullet" + ], + "homepage": "https://pipedream.com/apps/pushbullet", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/pushcut/README.md b/components/pushcut/README.md new file mode 100644 index 0000000000000..c8a4342f88231 --- /dev/null +++ b/components/pushcut/README.md @@ -0,0 +1,11 @@ +# Overview + +The Pushcut API enables the automation of iOS notifications with custom actions, triggering events based on various conditions. On Pipedream, you can harness this functionality to create intricate workflows, combining Pushcut notifications with a multitude of services to act based on data from APIs, schedules, or other apps. Think of Pushcut as a bridge between the real world and your digital tasks, letting you know when to act and offering shortcuts to execute specific automations directly from your iOS devices. + +# Example Use Cases + +- **Dynamic Notification for Calendar Events**: Send custom notifications for upcoming calendar events. By integrating with Google Calendar on Pipedream, you can schedule a workflow that fetches events and uses Pushcut to notify you with action buttons to check details or postpone events. + +- **Smart Home Alerts**: Combine Pushcut with IoT platforms like SmartThings. Set up a Pipedream workflow that listens for sensor triggers in your home—like motion or door openings—and then sends a Pushcut notification to your device with options to toggle devices or set modes. + +- **GitHub Repo Updates**: Stay on top of changes to your GitHub repositories. With a Pipedream workflow, get Pushcut notifications whenever there's a new commit, issue, or pull request. You can add action buttons to open the GitHub app, directly view the changes, or even run a CI/CD pipeline. diff --git a/components/pusher/README.md b/components/pusher/README.md index 223b10c4da38e..1977f8df264b0 100644 --- a/components/pusher/README.md +++ b/components/pusher/README.md @@ -1,21 +1,11 @@ # Overview -Pusher is an easy-to-use technology that makes it easy for developers to add -realtime features to their apps. With the Pusher API, developers can build many -different types of applications and services, from realtime voting to -collaborative games. +The Pusher API offers real-time communication capabilities for apps, enabling instant data delivery. With Pipedream, you can harness these features to create dynamic, real-time workflows that react to events, update clients immediately, and synchronize data across users and systems. It's perfect for powering live dashboards, instant notifications, chat applications, and any scenario where you need to push updates quickly and efficiently. Pipedream's serverless platform empowers you to build and run workflows that leverage Pusher's APIs without managing any infrastructure. -Here are some examples of what you can build using the Pusher API: +# Example Use Cases -- Chat Applications: Create multi-room and private conversations. -- Notification Services: Automatically send new events to subscribed users. -- Private Group Messaging: Host private conversations between multiple members. -- Realtime Data Visualization: Create live data updates and insights. -- Collaborative Editing: Allow people to collaborate on text documents in - real-time. -- Location Tracking: Monitor and track users’ locations in real-time. -- Presence Management: Notify users when individuals enter or leave particular - areas. -- Instant Payments: Handle real-time payments and transactions. -- Automated Workflows: Enable automated processes and tasks. -- IoT Solutions: Connect mobile, web and device for improved control and usage. +- **Real-Time Data Sync Across Services:** Integrate Pusher with a database like PostgreSQL on Pipedream. When a new record is added to the database, trigger a Pipedream workflow that publishes this update to a Pusher channel, instantly updating all connected clients. + +- **E-Commerce Order Notifications:** Connect Pusher with Shopify on Pipedream. Set up a workflow that listens for new orders and pushes instant confirmations or updates to customers through a Pusher-powered frontend, enhancing the shopping experience with real-time notifications. + +- **IoT Device State Monitoring:** Pair Pusher with IoT platforms such as Particle. Create a Pipedream workflow that subscribes to IoT device state changes, and use Pusher to broadcast these updates to a network of subscriber devices or dashboards, maintaining real-time device status awareness. diff --git a/components/pusher/package.json b/components/pusher/package.json new file mode 100644 index 0000000000000..2ebe1d0e6cd7f --- /dev/null +++ b/components/pusher/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/pusher", + "version": "0.6.0", + "description": "Pipedream pusher Components", + "main": "pusher.app.mjs", + "keywords": [ + "pipedream", + "pusher" + ], + "homepage": "https://pipedream.com/apps/pusher", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/pushover/README.md b/components/pushover/README.md index 1c3d49569b287..325219f86bf6b 100644 --- a/components/pushover/README.md +++ b/components/pushover/README.md @@ -1,22 +1,11 @@ # Overview -Using the Pushover API, you can design applications that can send notifications -to Android, iOS, and Deskop devices. Pushover provides a simple, yet powerful -API that makes it easy to send notifications to any device in real-time. With -the Pushover API, you have the ability to quickly send notifications, create -interactive conversations, and create targeted messages. +Pushover makes it easy to send real-time notifications to your Android and iOS devices. By leveraging the Pushover API on Pipedream, you can craft customized messages triggered by various events and send them to users instantly. This integration can be especially useful for monitoring systems, to-do list reminders, or even to keep tabs on your online services and workflows. -The following are examples of applications you can build with Pushover: +# Example Use Cases -- Mobile and Web Applications: Pushover is perfect for any mobile or web - application that requires notifications to users. -- Critical Alerts: Pushover is used by many businesses and organizations to - send critical alerts to mobile users as well as desktops. -- Notification Systems: Pushover can be used to send notifications to any - destination, such as SMS, email, and webhooks. -- Instant Messaging System: Pushover can be used to create an instant messaging - system for any mobile or desktop user. -- Automatic Notifications: Pushover can be used to automatically send - notifications to users based on triggers or timers. -- Event Tracking: Pushover can be used to track events and send notifications - when those events occur. +- **System Monitoring Alerts**: Set up a Pipedream workflow that monitors server health using a cron job. If a check fails (e.g., server down, high CPU usage), automatically send an alert via Pushover to inform the IT team promptly. + +- **E-Commerce Order Notifications**: Connect Pushover to an e-commerce platform like Shopify. Create a workflow on Pipedream that triggers a Pushover notification whenever a new order is placed, providing real-time sale updates to the business owner. + +- **Social Media Mention Alerts**: Pair Pushover with Twitter's API on Pipedream. Send notifications to your device when your brand is mentioned on Twitter, allowing for rapid engagement or issue resolution. diff --git a/components/pushsafer/README.md b/components/pushsafer/README.md index faa6ac43d9b27..0ddc7f0619b87 100644 --- a/components/pushsafer/README.md +++ b/components/pushsafer/README.md @@ -1,23 +1,11 @@ # Overview -Pushsafer is an API that provides a comprehensive suite of tools for creating -and sending push notifications to mobile and desktop devices. With Pushsafer, -you can build a wide range of applications. Here are just some of the -possibilities: +Pushsafer is an API that enables real-time notifications to your devices. With Pushsafer, you can send customized messages to Android, iOS, and Windows devices, quickly and efficiently. When integrated with Pipedream, this API can serve as the linchpin for a vast array of automation workflows, allowing for immediate alerts based on triggers from countless services and applications. Imagine getting notifications for new sales leads, system downtimes, or even receiving reminders for upcoming events - All of this becomes seamless with Pushsafer on Pipedream. -- Customize and send push notifications for your apps, websites, and other - services. -- Manage push notifications with Pushsafer's built-in scheduling and targeting - capabilities. -- Use Pushsafer to set up automatic updates, reminders, or alerts. -- Create personalized message presentations for each user or recipient. -- Design interactive notifications with interactive button links and images. -- Measure and track the performance of your push notifications over time. -- Integrate Pushsafer with webhooks, Zapier, and IFTTT to create powerful - automated workflows. -- Utilitize Pushsafer's data security measures, including encryption and - two-factor authentication. -- And much more! +# Example Use Cases -Pushsafer is an incredibly powerful tool for creating and sending push -notifications, with endless possibilities for customization and usage. +- **E-Commerce Order Alerts**: When a new order is placed on an e-commerce platform, trigger a workflow that captures order details and sends an instant notification via Pushsafer to inform the sales team or the business owner. + +- **Website Downtime Notification**: Combine Pushsafer with a website monitoring app. If the app detects downtime, it triggers a Pipedream workflow that sends an alert through Pushsafer to the IT support team, prompting an immediate response. + +- **Scheduled Report Reminders**: Use Pipedream's scheduled triggers to send daily or weekly summaries of key metrics from a database or a service like Google Analytics. The workflow compiles the report and delivers it through Pushsafer to keep stakeholders informed. diff --git a/components/pushsafer/package.json b/components/pushsafer/package.json new file mode 100644 index 0000000000000..ab74b8e992fb3 --- /dev/null +++ b/components/pushsafer/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/pushsafer", + "version": "0.6.0", + "description": "Pipedream pushsafer Components", + "main": "pushsafer.app.mjs", + "keywords": [ + "pipedream", + "pushsafer" + ], + "homepage": "https://pipedream.com/apps/pushsafer", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/pushshift_reddit_search/actions/search-reddit-comments/search-reddit-comments.mjs b/components/pushshift_reddit_search/actions/search-reddit-comments/search-reddit-comments.mjs deleted file mode 100644 index ce0bde27ec963..0000000000000 --- a/components/pushshift_reddit_search/actions/search-reddit-comments/search-reddit-comments.mjs +++ /dev/null @@ -1,117 +0,0 @@ -import pushshift from "../../pushshift_reddit_search.app.mjs"; -import utils from "../../common/utils.mjs"; - -export default { - key: "pushshift_reddit_search-search-reddit-comments", - name: "Search Reddit Comments", - description: "Search Reddit comments using the Pushshift.io API. [See the docs here](https://github.com/pushshift/api)", - version: "0.1.2", - type: "action", - props: { - pushshift, - q: { - propDefinition: [ - pushshift, - "q", - ], - }, - ids: { - propDefinition: [ - pushshift, - "ids", - ], - }, - size: { - propDefinition: [ - pushshift, - "size", - ], - }, - fields: { - propDefinition: [ - pushshift, - "fields", - ], - }, - sort: { - propDefinition: [ - pushshift, - "sort", - ], - }, - sortType: { - propDefinition: [ - pushshift, - "sortType", - ], - }, - aggs: { - propDefinition: [ - pushshift, - "aggs", - ], - }, - author: { - propDefinition: [ - pushshift, - "author", - ], - }, - subreddit: { - propDefinition: [ - pushshift, - "subreddit", - ], - }, - after: { - propDefinition: [ - pushshift, - "after", - ], - }, - before: { - propDefinition: [ - pushshift, - "before", - ], - }, - frequency: { - propDefinition: [ - pushshift, - "frequency", - ], - }, - metadata: { - propDefinition: [ - pushshift, - "metadata", - ], - }, - }, - async run({ $ }) { - const params = utils.omitEmptyStringValues({ - q: this.q, - ids: this.ids, - size: this.size, - fields: this.fields, - sort: this.sort, - sort_type: this.sortType, - aggs: this.aggs, - author: this.author, - subreddit: this.subreddit, - after: this.after, - before: this.before, - frequency: this.frequency, - metadata: this.metadata, - }); - - const comments = await this.pushshift.searchComments({ - params, - $, - }); - - $.export("$summary", `Found ${comments.length} comment(s)`); - - return comments; - }, -}; diff --git a/components/pushshift_reddit_search/package.json b/components/pushshift_reddit_search/package.json index 49c59a7b1a395..46d4e81c1331c 100644 --- a/components/pushshift_reddit_search/package.json +++ b/components/pushshift_reddit_search/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/pushshift_reddit_search", - "version": "0.0.5", + "version": "0.0.6", "description": "Pipedream Pushshift Reddit Search Components", "main": "pushshift_reddit_search.app.mjs", "keywords": [ diff --git a/components/python/README.md b/components/python/README.md index 99f8defff4cce..1479b6d9cf362 100644 --- a/components/python/README.md +++ b/components/python/README.md @@ -1,25 +1,67 @@ # Overview -Python API on Pipedream offers developers to build or automate a variety of -tasks from their web and cloud apps. With the Python API, users are able to -create comprehensive and flexible scripts, compose and manage environment -variables, and configure resources to perform a range of functions. - -By using Pipedream, you can easily: - -- Create automated workflows that run on a specific schedule -- Compose workflows across various apps and services -- React to events in cloud services or form data -- Automatically create content and notifications -- Construct classifications and predictions -- Analyze and react to sentiment, sentiment analysis and sentiment score -- Connect backends to the frontend with serverless functions -- Work with files and databases -- Perform web requests and fetch data -- Integrate third-party APIs into your apps -- Orchestrate data processing tasks and pipelines -- Create powerful application APIs with authentication and authorization -- Design CI/CD pipelines and Continuous Delivery services -- Connect databases like MongoDB and MySQL -- Monitor connections and events -- Generate alerts and notifications for corresponding events +Develop, run and deploy your Python code in Pipedream workflows. Integrate seamlessly between no-code steps, with [connected accounts]([https://pipedream.com/docs/code/python/auth](https://pipedream.com/docs/code/nodejs/auth)), or integrate [Data Stores](https://pipedream.com/docs/data-stores) and [manipulate files within a workflow](https://pipedream.com/docs/code/python/working-with-files). + +This includes [installing PyPI packages](https://pipedream.com/docs/code/python#using-third-party-packages), within your code without having to manage a `requirements.txt` file or running `pip`. + +Below is an example of using Python to access data from the trigger of the workflow, and sharing it with subsequent workflow steps: + +# Example Use Cases + +Here are three practical uses for incorporating Python code in Pipedream workflows: + +1. **Automated Data Processing**: + - Python can be utilized within Pipedream to automate the processing of incoming data from various sources such as webhooks, APIs, or even scheduled events. You can transform, sanitize, and aggregate this data before passing it to subsequent steps or storing it in databases or data stores provided by Pipedream. This is particularly useful for workflows involving data analytics, where Python’s extensive library ecosystem (e.g., [Pandas](https://pandas.pydata.org/docs/user_guide/index.html) for data manipulation) can be leveraged. +2. **Integration and API Interactions**: + - Use Python to orchestrate complex API interactions that require custom logic beyond simple HTTP requests. This can include handling authentication flows, error processing, or managing pagination. Python’s robust support for network and protocol management makes it ideal for integrating disparate systems, performing API health checks, or even creating composite APIs that aggregate data from multiple sources into a unified response. +3. **Machine Learning and AI**: + - Implement machine learning models directly within your workflows to perform real-time predictions, analyses, or automated decision-making based on the incoming data streams. Python’s compatibility with machine learning frameworks like [TensorFlow](https://www.tensorflow.org/learn) or [scikit-learn](https://scikit-learn.org/stable/) allows you to import pre-trained models or train them on-the-fly using workflow data. This can be applied in scenarios such as image recognition, predictive maintenance, or customer sentiment analysis. + +These applications of Python in Pipedream workflows enable sophisticated data operations, extend functionality with external APIs, and incorporate advanced analytics and machine learning directly into your automated processes. + +# Getting Started + +To add a Python code step, open a new workflow and include a step. + +1. Select the Python app: + +![Python app being chosen in Pipedream's new step selector screen.](https://res.cloudinary.com/pipedreamin/image/upload/v1713462237/marketplace/apps/python/CleanShot_2024-04-18_at_13.43.34_fin0ru.png) + +2. Then select the **Run Python Code** action: + +![Selecting the 'Run Python Code' action from the Python app in the Pipedream interface.](https://res.cloudinary.com/pipedreamin/image/upload/v1713462283/marketplace/apps/python/CleanShot_2024-04-18_at_13.44.34_jt15cq.png) + +Now you’re ready to write some code! + +On the right, you'll see the default code provided by Pipedream: + +```python +def handler(pd: "pipedream"): + # Reference data from previous steps + print(pd.steps["trigger"]["context"]["id"]) + # Return data for use in future steps + return {"foo": {"test": True}} +``` + +You can write your custom code within the `handler` function. `handler` is called when this step executes in your workflow. The **`pd`** argument contains the workflow's context and data. + +When you click **Test** on the Python code step, it will display the event data from your trigger step. For instance, if your trigger is an HTTP request, then the HTTP request data will be returned. + +This step can execute any Python code. However, the `handler` function, a special Pipedream callback, must be set up correctly to return data. Otherwise, you can run arbitrary code that: + +* [Consume or share data with other steps](https://pipedream.com/docs/code/python#sharing-data-between-steps) + +- [Send HTTP requests]([https://pipedream.com/docs/code/python#making-http-requests-from-your-workflow](https://pipedream.com/docs/code/nodejs#making-http-requests-from-your-workflow)) +- [Return an HTTP response]([https://pipedream.com/docs/code/python#returning-http-responses](https://pipedream.com/docs/code/nodejs#returning-http-responses)) +- [End the entire workflow]([https://pipedream.com/docs/code/python#ending-a-workflow-early](https://pipedream.com/docs/code/nodejs#ending-a-workflow-early)) +- [Use your connected accounts to make authenticated HTTP requests]([https://pipedream.com/docs/code/python/auth](https://pipedream.com/docs/code/nodejs/auth)) +- [Reference environment variables]([https://pipedream.com/docs/code/python#using-secrets-in-code](https://pipedream.com/docs/code/nodejs#using-secrets-in-code)) +- [Retrieve or update data within Data Stores]([https://pipedream.com/docs/code/python/using-data-stores](https://pipedream.com/docs/code/nodejs/using-data-stores)) +- [Download, upload and manipulate files]([https://pipedream.com/docs/code/python/working-with-files](https://pipedream.com/docs/code/nodejs/working-with-files)) +- [Pausing, resuming and rerunning Python code steps]([https://pipedream.com/docs/code/python/rerun](https://pipedream.com/docs/code/nodejs/rerun)) + +# Troubleshooting + +Pipedream will show your error traces within your individual steps, under the **Logs** section. + +Traces across all of your workflows are also available within the [Event History](https://pipedream.com/docs/event-history) in your Pipedream workspace. This gives a global view of all failed executions, and gives you the tools to filter by workflow, time occurred and more. diff --git a/components/python/package.json b/components/python/package.json new file mode 100644 index 0000000000000..d7bcf0b18427b --- /dev/null +++ b/components/python/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/python", + "version": "0.6.0", + "description": "Pipedream python Components", + "main": "python.app.mjs", + "keywords": [ + "pipedream", + "python" + ], + "homepage": "https://pipedream.com/apps/python", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/qdrant/README.md b/components/qdrant/README.md new file mode 100644 index 0000000000000..3430205aa665a --- /dev/null +++ b/components/qdrant/README.md @@ -0,0 +1,3 @@ +# Overview + +Qdrant - is a high-performance vector search-engine/database. diff --git a/components/qdrant/actions/delete-points/delete-points.mjs b/components/qdrant/actions/delete-points/delete-points.mjs new file mode 100644 index 0000000000000..867cde14dd61b --- /dev/null +++ b/components/qdrant/actions/delete-points/delete-points.mjs @@ -0,0 +1,62 @@ +import app from "../../qdrant.app.mjs"; +import utils from "../../common/utils.mjs"; +import { QdrantClient } from "@qdrant/js-client-rest"; + +export default { + key: "qdrant-delete-points", + name: "Delete Points", + description: "Deletes one or more points by ID/filter in a Qdrant collection.", + type: "action", + version: "0.0.1", + props: { + app, + collectionName: { + propDefinition: [ + app, + "collectionName", + ], + }, + ids: { + type: "string[]", + label: "Point IDs", + description: "The IDs of the points. E.g. `[\"94058ad0-c3c0-4bd9-a085-b7e45f009070\", 32342]`", + propDefinition: [ + app, + "pointId", + ], + optional: true, + }, + filter: { + propDefinition: [ + app, + "filter", + ], + }, + }, + methods: { + async deletePoints(collectionName, ids, filter) { + const client = new QdrantClient(this.app.getCredentials()); + + return await client.delete(collectionName, { + points: ids, + filter, + wait: true, + }); + }, + }, + async run({ $: step }) { + const { + collectionName, ids, filter, + } = this; + + const response = await this.deletePoints( + collectionName, + utils.parsePointIds(ids), + utils.parse(filter), + ); + + step.export("$summary", "Successfully deleted points"); + + return response; + }, +}; diff --git a/components/qdrant/actions/get-points/get-points.mjs b/components/qdrant/actions/get-points/get-points.mjs new file mode 100644 index 0000000000000..888de33eccc5d --- /dev/null +++ b/components/qdrant/actions/get-points/get-points.mjs @@ -0,0 +1,72 @@ +import app from "../../qdrant.app.mjs"; +import utils from "../../common/utils.mjs"; +import { QdrantClient } from "@qdrant/js-client-rest"; + +export default { + key: "qdrant-get-points", + name: "Get Points", + description: "Get points by IDs from a collection.", + type: "action", + version: "0.0.1", + props: { + app, + collectionName: { + propDefinition: [ + app, + "collectionName", + ], + }, + ids: { + type: "string[]", + label: "Point IDs", + description: "The IDs of the points. E.g. `[\"94058ad0-c3c0-4bd9-a085-b7e45f009070\", 32342]`", + propDefinition: [ + app, + "pointId", + ], + }, + withPayload: { + propDefinition: [ + app, + "withPayload", + ], + }, + withVector: { + propDefinition: [ + app, + "withVector", + ], + }, + }, + methods: { + async getPoints(collectionName, { + ids, withPayload, withVector, + }) { + const client = new QdrantClient(this.app.getCredentials()); + + return await client.retrieve(collectionName, { + ids, + with_payload: withPayload, + with_vector: withVector, + }); + }, + }, + async run({ $: step }) { + const { + collectionName, ids, withPayload, withVector, getPoints, + } = this; + + const response = await getPoints( + collectionName, + { + ids: utils.parsePointIds(ids), + withPayload, + withVector, + }, + ); + + step.export("$summary", "Successfully retrieved points"); + + return response; + }, +}; diff --git a/components/qdrant/actions/search-points/search-points.mjs b/components/qdrant/actions/search-points/search-points.mjs new file mode 100644 index 0000000000000..a303c1c2319fd --- /dev/null +++ b/components/qdrant/actions/search-points/search-points.mjs @@ -0,0 +1,89 @@ +import app from "../../qdrant.app.mjs"; +import utils from "../../common/utils.mjs"; +import { QdrantClient } from "@qdrant/js-client-rest"; + +export default { + key: "qdrant-search-points", + name: "Search Points", + description: "Performs a semantic search on the Qdrant collection.", + type: "action", + version: "0.0.2", + props: { + app, + collectionName: { + propDefinition: [ + app, + "collectionName", + ], + }, + vector: { + optional: false, + propDefinition: [ + app, + "vector", + ], + }, + filter: { + propDefinition: [ + app, + "filter", + ], + }, + limit: { + type: "integer", + label: "Search Limit", + description: "The maximum number of points to return.", + optional: true, + default: 10, + }, + withPayload: { + propDefinition: [ + app, + "withPayload", + ], + }, + withVector: { + propDefinition: [ + app, + "withVector", + ], + }, + }, + methods: { + searchPoints(collectionName, { + vector, filter, limit, withPayload, withVector, + }) { + + const client = new QdrantClient(this.app.getCredentials()); + + return client.query(collectionName, { + filter, + limit, + query: vector, + with_payload: withPayload, + with_vector: withVector, + }); + + }, + }, + async run({ $: step }) { + const { + collectionName, vector, filter, withPayload, withVector, limit, searchPoints, + } = this; + + const response = await searchPoints( + collectionName, + { + vector: utils.parseVector(vector), + filter: utils.parse(filter), + limit: parseInt(limit), + withPayload, + withVector, + }, + ); + + step.export("$summary", "Successfully searched the collection"); + + return response.points; + }, +}; diff --git a/components/qdrant/actions/upsert-point/upsert-point.mjs b/components/qdrant/actions/upsert-point/upsert-point.mjs new file mode 100644 index 0000000000000..e9f18920f72c5 --- /dev/null +++ b/components/qdrant/actions/upsert-point/upsert-point.mjs @@ -0,0 +1,73 @@ +import app from "../../qdrant.app.mjs"; +import utils from "../../common/utils.mjs"; +import { QdrantClient } from "@qdrant/js-client-rest"; + +export default { + key: "qdrant-upsert-point", + name: "Upsert Point", + description: "Adds a point to the Qdrant collection.", + type: "action", + version: "0.0.1", + props: { + app, + collectionName: { + propDefinition: [ + app, + "collectionName", + ], + }, + id: { + propDefinition: [ + app, + "pointId", + ], + }, + vector: { + optional: false, + propDefinition: [ + app, + "vector", + ], + }, + payload: { + propDefinition: [ + app, + "payload", + ], + }, + }, + methods: { + upsertPoint(collectionName, { + id, vector, payload, + }) { + + const client = new QdrantClient(this.app.getCredentials()); + + return client.upsert(collectionName, { + points: [ + { + id, + vector, + payload, + }, + ], + }); + + }, + }, + async run({ $: step }) { + const { + collectionName, id, vector, payload, upsertPoint, + } = this; + + const response = await upsertPoint(collectionName, { + id: utils.parsePointId(id), + vector: utils.parseVector(vector), + payload: utils.parse(payload), + }); + + step.export("$summary", "Successfully added the point"); + + return response; + }, +}; diff --git a/components/qdrant/common/utils.mjs b/components/qdrant/common/utils.mjs new file mode 100644 index 0000000000000..79e7a87316dd6 --- /dev/null +++ b/components/qdrant/common/utils.mjs @@ -0,0 +1,67 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function parsePointIds(values) { + if (!values) { + return undefined; + } + + return values.map(parsePointId); +} + +// Qdrant point IDs can either be unsigned integers or strings(UUIDs) +// https://qdrant.tech/documentation/concepts/points/#point-ids +function parsePointId(value) { + if (String(value).length && !isNaN(value)) { + const number = parseInt(value); + if (number >= 0) { + return number; + } else { + throw new ConfigurationError(`${value} is not an unsigned integer to be used as a point ID.`); + } + } else { + return String(value); + } +} + +function parseVector(values) { + if (!values) { + return []; + } + + return values.map(parseFloatValue); +} + +function parseFloatValue(value) { + if (String(value).length && !isNaN(value)) { + return parseFloat(value); + } + + throw new ConfigurationError("Make sure vector values are valid floats"); +} + +function emptyStrToUndefined(value) { + const trimmed = typeof (value) === "string" && value.trim(); + return trimmed === "" + ? undefined + : value; +} + +function parse(value) { + const valueToParse = emptyStrToUndefined(value); + if (typeof (valueToParse) === "object" || valueToParse === undefined) { + return valueToParse; + } + try { + return JSON.parse(valueToParse); + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid object"); + } +} + +export default { + emptyStrToUndefined, + parse, + parseVector, + parsePointId, + parsePointIds, +}; diff --git a/components/qdrant/package.json b/components/qdrant/package.json new file mode 100644 index 0000000000000..5ae89f52de02d --- /dev/null +++ b/components/qdrant/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/qdrant", + "version": "0.1.1", + "description": "Pipedream Qdrant Components", + "main": "qdrant.app.mjs", + "keywords": [ + "pipedream", + "qdrant" + ], + "homepage": "https://pipedream.com/apps/qdrant", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@qdrant/js-client-rest": "^1.11.0" + } +} diff --git a/components/qdrant/qdrant.app.mjs b/components/qdrant/qdrant.app.mjs new file mode 100644 index 0000000000000..9965e18dd7455 --- /dev/null +++ b/components/qdrant/qdrant.app.mjs @@ -0,0 +1,70 @@ +import { QdrantClient } from "@qdrant/js-client-rest"; + +export default { + type: "app", + app: "qdrant", + propDefinitions: { + collectionName: { + type: "string", + label: "Collection Name", + description: "The name of the Qdrant collection.", + async options() { + return await this.listCollections(); + }, + }, + pointId: { + type: "string", + label: "Point ID", + description: "The ID of the point in the collection. [Documentation](https://qdrant.tech/documentation/concepts/points/#point-ids)", + }, + payload: { + type: "object", + label: "Payload", + description: "The payload associated with a point. [Documentation](https://qdrant.tech/documentation/concepts/payload/)", + optional: true, + default: {}, + }, + vector: { + type: "string[]", + label: "Vector", + description: "The vector embeddings.", + optional: true, + }, + filter: { + type: "object", + label: "Points Filter", + description: "Filter to apply in queries. [Documentation](https://qdrant.tech/documentation/concepts/filtering/)`", + optional: true, + default: {}, + }, + withPayload: { + type: "boolean", + label: "With Payload", + description: "Whether to include the payload in the response.", + optional: true, + default: false, + }, + withVector: { + type: "boolean", + label: "With Vector", + description: "Whether to include the vector in the response.", + optional: true, + default: false, + }, + }, + methods: { + getCredentials() { + return { + url: this.$auth.url, + apiKey: this.$auth.api_key, + }; + }, + async listCollections() { + const client = new QdrantClient(this.getCredentials()); + + const collections = (await client.getCollections()).collections; + + return collections.map((collection) => collection.name); + }, + }, +}; diff --git a/components/qntrl/README.md b/components/qntrl/README.md new file mode 100644 index 0000000000000..405285077e0ff --- /dev/null +++ b/components/qntrl/README.md @@ -0,0 +1,11 @@ +# Overview + +The Qntrl API offers a powerful way to manage and automate your organization's workflows and processes directly within Pipedream. With this API, you can create, update, retrieve, and manage cards, orchestrate workflows, and synchronize data across different systems. Harness the power of Pipedream to integrate Qntrl with various apps and services, enabling seamless data flow and enhanced productivity. + +# Example Use Cases + +- **Automated Task Assignment**: Use Qntrl's API to create cards for new tasks and automatically assign them to team members based on predefined criteria. Trigger this workflow when a new row is added to a Google Sheets spreadsheet tracking task requests. + +- **Workflow Progress Webhooks**: Set up a Pipedream workflow that listens to Qntrl webhook events to get real-time updates on card statuses. When a card moves to a certain stage, send a notification via Slack to the relevant team or team member. + +- **Synchronized CRM Updates**: Whenever a deal progresses in Qntrl, use the API to update the corresponding record in Salesforce. This ensures your CRM reflects the most current status of each deal, maintaining a single source of truth. diff --git a/components/qntrl/actions/create-job/create-job.mjs b/components/qntrl/actions/create-job/create-job.mjs new file mode 100644 index 0000000000000..d7b7a1fc90831 --- /dev/null +++ b/components/qntrl/actions/create-job/create-job.mjs @@ -0,0 +1,126 @@ +import { ConfigurationError } from "@pipedream/platform"; +import qntrl from "../../qntrl.app.mjs"; + +export default { + key: "qntrl-create-job", + name: "Create Job", + description: + "Creates a new job (card) in Qntrl. [See the documentation](https://core.qntrl.com/apidoc.html?type=reference&module=jobs&action=CreateJob)", + version: "0.0.1", + type: "action", + props: { + qntrl, + orgId: { + propDefinition: [ + qntrl, + "orgId", + ], + }, + formId: { + propDefinition: [ + qntrl, + "formId", + ({ orgId }) => ({ + orgId, + }), + ], + reloadProps: true, + }, + title: { + type: "string", + label: "Title", + description: "Title of the job.", + }, + description: { + type: "string", + label: "Description", + description: "Description of the job.", + optional: true, + }, + dueDate: { + type: "string", + label: "Due Date", + description: + "The due date of the job as a valid date string, e.g. `2016-02-29T12:12:12+0530` or `2016-02-29`.", + optional: true, + }, + additionalOptions: { + type: "object", + label: "Additional Options", + description: + "Additional parameters to send in this request. [See the documentation](https://core.qntrl.com/apidoc.html?type=reference&module=jobs&action=CreateJob) for all available parameters.", + optional: true, + }, + }, + methods: { + fieldToProp(field) { + const result = {}; + /* eslint-disable no-fallthrough */ + switch (field.field_type_value) { + case "PICKLIST": + result.options = field.picklist_details?.map?.((option) => ({ + label: option.value, + value: option.picklist_id, + })); + case "TEXT": + case "MLTEXT": + case "DATETIME": + default: + result.type = "string"; + break; + } + return result; + }, + }, + async additionalProps() { + const result = {}; + const { + orgId, formId, + } = this; + const formDetails = await this.qntrl.getFormDetails({ + orgId, + formId, + }); + formDetails?.section_details?.forEach?.((section) => { + section.sectionfieldmap_details?.forEach?.((fieldmap) => { + fieldmap.customfield_details?.forEach?.((field) => { + const columnName = field.column_name; + if (columnName.startsWith("customfield")) { + result[columnName] = { + label: field.alias_name, + description: `${field.alias_name} (\`${columnName}\`)`, + optional: !fieldmap.is_mandatory, + ...this.fieldToProp(field), + }; + } + }); + }); + }); + return result; + }, + async run({ $ }) { + const { // eslint-disable-next-line no-unused-vars + qntrl, fieldToProp, orgId, formId, dueDate, additionalOptions, ...data + } = this; + const dateObj = new Date(dueDate); + const isInvalidDate = isNaN(dateObj.valueOf()); + if (dueDate && isInvalidDate) { + throw new ConfigurationError("Invalid date string for `Due Date`"); + } + + const response = await qntrl.createJob({ + $, + orgId, + data: { + layout_id: formId, + duedate: isInvalidDate + ? undefined + : (dateObj?.toISOString().slice(0, -5) + "+0000"), + ...data, + ...additionalOptions, + }, + }); + $.export("$summary", `Successfully created job (ID: ${response.id})`); + return response; + }, +}; diff --git a/components/qntrl/actions/post-comment/post-comment.mjs b/components/qntrl/actions/post-comment/post-comment.mjs new file mode 100644 index 0000000000000..454351bbe8bb6 --- /dev/null +++ b/components/qntrl/actions/post-comment/post-comment.mjs @@ -0,0 +1,48 @@ +import qntrl from "../../qntrl.app.mjs"; + +export default { + key: "qntrl-post-comment", + name: "Post Comment", + description: "Posts a new comment under a specific job. [See the documentation](https://core.qntrl.com/apidoc.html?type=reference&module=jobs&action=PostComment)", + version: "0.0.1", + type: "action", + props: { + qntrl, + orgId: { + propDefinition: [ + qntrl, + "orgId", + ], + }, + jobId: { + propDefinition: [ + qntrl, + "jobId", + (({ orgId }) => ({ + orgId, + })), + ], + }, + content: { + type: "string", + label: "Content", + description: "Comment to be posted.", + }, + }, + async run({ $ }) { + const { + qntrl, orgId, jobId, content, + } = this; + const response = await qntrl.postComment({ + $, + orgId, + jobId, + data: { + content, + }, + }); + + $.export("$summary", `Successfully posted a comment under job ID ${this.jobId}`); + return response; + }, +}; diff --git a/components/qntrl/package.json b/components/qntrl/package.json new file mode 100644 index 0000000000000..ffe063c421e02 --- /dev/null +++ b/components/qntrl/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/qntrl", + "version": "0.1.0", + "description": "Pipedream Qntrl Components", + "main": "qntrl.app.mjs", + "keywords": [ + "pipedream", + "qntrl" + ], + "homepage": "https://pipedream.com/apps/qntrl", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/qntrl/qntrl.app.mjs b/components/qntrl/qntrl.app.mjs new file mode 100644 index 0000000000000..e8483c95ae944 --- /dev/null +++ b/components/qntrl/qntrl.app.mjs @@ -0,0 +1,118 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "qntrl", + propDefinitions: { + orgId: { + type: "string", + label: "Organization ID", + description: "Select an organization, or provide a custom organization ID.", + async options() { + const orgs = await this.listOrganizations(); + return orgs?.map((org) => ({ + label: org.org_domain, + value: org.org_id, + })); + }, + }, + formId: { + type: "string", + label: "Form ID", + description: "Select a form (layout), or provide a custom form ID.", + async options({ orgId }) { + const forms = await this.listForms(orgId); + return forms?.map((form) => ({ + label: form.layout_name, + value: form.layout_id, + })); + }, + }, + jobId: { + type: "string", + label: "Job ID", + description: "Select a job (card), or provide a custom job ID.", + async options({ orgId }) { + const jobs = await this.listJobs(orgId); + return jobs?.map((job) => ({ + label: job.title, + value: job.id, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://coreapi.qntrl.com/blueprint/api"; + }, + async _makeRequest({ + $ = this, + headers, + ...otherOpts + }) { + return axios($, { + ...otherOpts, + baseURL: this._baseUrl(), + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + async listOrganizations() { + return this._makeRequest({ + url: "/org", + }); + }, + async listForms(orgId) { + return this._makeRequest({ + url: `/${orgId}/layout`, + }); + }, + async listJobs(orgId) { + const response = await this._makeRequest({ + url: `/${orgId}/job`, + }); + return response.job_list; + }, + async listJobComments({ + orgId, jobId, + }) { + const { data } = await this._makeRequest({ + url: `/${orgId}/job/${jobId}/comment`, + }); + return data; + }, + async getFormDetails({ + orgId, formId, + }) { + return this._makeRequest({ + url: `/${orgId}/layout/${formId}`, + }); + }, + async createJob({ + orgId, ...args + }) { + return this._makeRequest({ + method: "POST", + url: `/${orgId}/job`, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + ...args, + }); + }, + async postComment({ + orgId, jobId, ...args + }) { + return this._makeRequest({ + method: "POST", + url: `/${orgId}/job/${jobId}/comment`, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + ...args, + }); + }, + }, +}; diff --git a/components/qntrl/sources/common/common.mjs b/components/qntrl/sources/common/common.mjs new file mode 100644 index 0000000000000..080091694fbd8 --- /dev/null +++ b/components/qntrl/sources/common/common.mjs @@ -0,0 +1,58 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import app from "../../qntrl.app.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + orgId: { + propDefinition: [ + app, + "orgId", + ], + }, + }, + hooks: { + async deploy() { + await this.getAndProcessData(false); + }, + }, + methods: { + _getSavedIds() { + return this.db.get("savedIds") ?? []; + }, + _setSavedIds(value) { + this.db.set("savedIds", value); + }, + getItemId(item) { + return item.id; + }, + async getAndProcessData(emit = true) { + const savedIds = this._getSavedIds(); + const items = await this.getItems(); + const ts = Date.now(); + items?.filter((item) => !savedIds.includes(this.getItemId(item))).sort(this.sortItems) + .forEach((item) => { + const id = this.getItemId(item); + if (emit) { + this.$emit(item, { + id, + summary: this.getSummary(item), + ts, + }); + } + savedIds.push(id); + }); + this._setSavedIds(savedIds); + }, + }, + async run() { + await this.getAndProcessData(); + }, +}; diff --git a/components/qntrl/sources/new-job-comment/new-job-comment.mjs b/components/qntrl/sources/new-job-comment/new-job-comment.mjs new file mode 100644 index 0000000000000..856bc2884ce2b --- /dev/null +++ b/components/qntrl/sources/new-job-comment/new-job-comment.mjs @@ -0,0 +1,50 @@ +import qntrl from "../../qntrl.app.mjs"; +import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "qntrl-new-job-comment", + name: "New Comment Posted", + description: "Emit new event when a comment is posted to a job. [See the documentation](https://core.qntrl.com/apidoc.html?type=reference&module=jobs&action=GetAllComments)", + version: "0.0.1", + type: "source", + dedupe: "unique", + sampleEmit, + props: { + ...common.props, + jobId: { + propDefinition: [ + qntrl, + "jobId", + (({ orgId }) => ({ + orgId, + })), + ], + }, + }, + methods: { + ...common.methods, + getSummary({ comment }) { + return `New Comment: "${comment.length > 40 + ? `${comment.slice(0, 40)}...` + : comment}"`; + }, + sortItems(a, b) { + // eslint-disable-next-line max-len + return new Date(Number(a.created_time)).valueOf() - new Date(Number(b.created_time)).valueOf(); + }, + getItems() { + const { + orgId, jobId, + } = this; + return this.app.listJobComments({ + orgId, + jobId, + }); + }, + getItemId(item) { + return item.comment_id; + }, + }, +}; diff --git a/components/qntrl/sources/new-job-comment/test-event.mjs b/components/qntrl/sources/new-job-comment/test-event.mjs new file mode 100644 index 0000000000000..34f14c30a92e8 --- /dev/null +++ b/components/qntrl/sources/new-job-comment/test-event.mjs @@ -0,0 +1,16 @@ +export default { + "author_name": "Author Name", + "created_time": "1710445199944", + "attachments": [], + "comment_id": "37908000000050437", + "created_date_hours": "03-14-2024 04:39 PM", + "modified_date": "03-14-2024", + "created_by": "37908000000000005", + "modified_date_hours": "03-14-2024 04:39 PM", + "zuid": "822203861", + "modified_time": "1710445199944", + "job_id": "37908000000050401", + "modified_by": "37908000000000005", + "comment": "
Comment text body
", + "created_date": "03-14-2024" +} \ No newline at end of file diff --git a/components/qntrl/sources/new-job-created/new-job-created.mjs b/components/qntrl/sources/new-job-created/new-job-created.mjs new file mode 100644 index 0000000000000..25cf81499447c --- /dev/null +++ b/components/qntrl/sources/new-job-created/new-job-created.mjs @@ -0,0 +1,25 @@ +import common from "../common/common.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "qntrl-new-job-created", + name: "New Job Created", + description: "Emit new event when a job is created. [See the documentation](https://core.qntrl.com/apidoc.html?type=reference&module=jobs&action=getAllJobs)", + version: "0.0.1", + type: "source", + dedupe: "unique", + sampleEmit, + methods: { + ...common.methods, + getSummary(job) { + return `New Job: "${job.title}"`; + }, + sortItems(a, b) { + return new Date(a.created_date_utc).valueOf() - new Date(b.created_date_utc).valueOf(); + }, + getItems() { + return this.app.listJobs(this.orgId); + }, + }, +}; diff --git a/components/qntrl/sources/new-job-created/test-event.mjs b/components/qntrl/sources/new-job-created/test-event.mjs new file mode 100644 index 0000000000000..bfede80071c16 --- /dev/null +++ b/components/qntrl/sources/new-job-created/test-event.mjs @@ -0,0 +1,68 @@ +export default { + no: "2", + closed_date: null, + layout_id: "37908000000050191", + closed_time_long: null, + record_owner_name: null, + job_link: + "https://core.qntrl.com/blueprint/api/pipedreamintegrationtesting/job/37908000000050417", + requestor_id: "37908000000000005", + prefix: "NM", + closed_date_hours: null, + modified_date_utc: "2024-03-14T16:34:48-0300", + description: null, + modified_time_long: "1710444888963", + team_id: null, + title: "Card Title", + created_date_hours: "03-14-2024 04:34 PM", + blueprint_id: "37908000000050199", + isadmin: true, + added_via: "web", + due_date_hours: null, + requestor_name: "Requestor Name", + stage_name: "Service Request Received", + due_date_utc: null, + id: "37908000000050417", + lock_type: { + is_detail_editable: true, + is_attachments_editable: true, + is_comments_editable: true, + is_fields_editable: true, + }, + assigned_time: null, + currentuser_zuid: 822203861, + is_closed: false, + created_time_long: "1710444888927", + record_owner_id: null, + escalation_time: null, + layout_name: "Network Management", + due_time_long: null, + stage_id: "37908000000050258", + model_type: "Job", + due_date: null, + blueprint_name: "Network Management_process_1", + priority: "37908000000050245", + modified_date: "03-14-2024", + team_name: null, + modified_date_hours: "03-14-2024 04:34 PM", + is_escalated: 0, + created_date_utc: "2024-03-14T16:34:48-0300", + created_date: "03-14-2024", + fields: { + customfield_shorttext2: null, + customfield_shorttext1: "Sector", + customfield_dropdown1: "37908000000050248", + customfield_dropdown2: null, + customfield_date1_utc: null, + customfield_userdropdown1: null, + customfield_userdropdown2: null, + customfield_shorttext6: null, + customfield_date2: null, + customfield_shorttext5: null, + customfield_date2_utc: null, + customfield_shorttext4: null, + customfield_shorttext3: null, + customfield_date1: null, + }, + closed_date_utc: null, +}; diff --git a/components/qryptal/README.md b/components/qryptal/README.md new file mode 100644 index 0000000000000..0ac06d219df6a --- /dev/null +++ b/components/qryptal/README.md @@ -0,0 +1,11 @@ +# Overview + +Qryptal API offers robust solutions for generating and validating secure document QR codes that can be integrated seamlessly into documents for authenticity verification. This is particularly crucial in sectors like education, finance, and government, where document integrity is paramount. Using Pipedream, the API can be connected to a variety of other services to create complete automation workflows, enhancing processes like document management, compliance tracking, and secure exchanges. + +# Example Use Cases + +- **Document Verification Automation**: Automatically verify the authenticity of documents as they are uploaded to cloud storage solutions like Google Drive or Dropbox. When a new document is uploaded, trigger a workflow that uses the Qryptal API to validate the QR code on the document, ensuring its legitimacy before it's processed or stored permanently. + +- **Secure Document Generation for Registrations**: In registration processes where certificates or confirmation documents are issued (e.g., course completions, event registrations), integrate Qryptal with a registration platform like Eventbrite. Automatically generate a secure QR code for each registration confirmation document, ensuring that each document can be verified for authenticity. + +- **Compliance Document Handling in Finance**: For financial institutions handling sensitive compliance documents, automate the generation and verification process with Qryptal API. Set up a workflow where every new compliance document is automatically equipped with a QR code before it's saved to a secure cloud service like AWS S3. This ensures all documents can be quickly verified for compliance audits or regulatory reviews. diff --git a/components/qryptal/package.json b/components/qryptal/package.json new file mode 100644 index 0000000000000..96080aa6063ba --- /dev/null +++ b/components/qryptal/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/qryptal", + "version": "0.0.1", + "description": "Pipedream Qryptal Components", + "main": "qryptal.app.mjs", + "keywords": [ + "pipedream", + "qryptal" + ], + "homepage": "https://pipedream.com/apps/qryptal", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/qryptal/qryptal.app.mjs b/components/qryptal/qryptal.app.mjs new file mode 100644 index 0000000000000..c8c413bcb8862 --- /dev/null +++ b/components/qryptal/qryptal.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "qryptal", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/qstash/README.md b/components/qstash/README.md index 68d2a72497808..33599b4d677f8 100644 --- a/components/qstash/README.md +++ b/components/qstash/README.md @@ -1,31 +1,11 @@ # Overview -QStash offers an API that enables developers to access and manipulate data from -their cloud storage service. With the QStash API, developers can build a wide -range of applications and services that work with large datasets for data -retrieval and analysis. Here are some examples of what can be built using the -QStash API: +QStash API offers a secure, scalable, and simple way to manage message queues and defer tasks. Using this API, you can enqueue messages, schedule tasks to run after a delay, and ensure that tasks are executed exactly once, leveraging the power of serverless architecture. With Pipedream's ability to connect to a multitude of services, you can build complex workflows that trigger actions in other apps based on events in QStash, allowing you to automate cross-application business processes with ease. -- Online Event Storage & Management Platforms - Using the QStash API, - developers can build online event platform applications which allow users to - store their event data securely in the cloud, and then access it remotely via - their web or mobile applications. -- Image Processing & Retrieval Platforms - With the QStash API, developers can - build an application that will allow users to easily and securely store and - access large digital images in the cloud. The application can be used to - process and retrieve images based on various criteria, such as size, - location, etc. -- Cloud-Based Data Analysis Platforms - With the QStash API, developers can - create cloud-based data analysis platforms that allow users to store large - datasets like transaction logs, customer behavior data, etc. in the cloud and - analyze them from a secure remote environment. -- Data Management Systems - Developers can also use the QStash API to create - cloud-based data management systems which allow users to store and manage - large datasets in the cloud, and access and analyze them from any location. -- Big Data Applications - Big data applications can also be built using the - QStash API which allow users to store and process large datasets at scale. +# Example Use Cases -These are just a few examples of what can be built using the QStash API. With -the myriad of possibilities, developers are assured of having the ability to -create all sorts of applications and services that can harness, store, and -process large amounts of data. +- **Delayed Order Processing**: Automate the handling of orders that require a delayed processing step. For instance, after an order is received, you can use QStash to delay notification for manual review, and after the set delay, trigger a workflow in Pipedream that checks the order for fraud, and then proceeds with shipping confirmation. + +- **Scheduled Content Publishing**: Perfect for managing social media or blog content schedules. When a content piece is ready, push it to QStash with a scheduled release time. Pipedream can then listen for the message to be dequeued and automatically publish the content to platforms like Twitter or WordPress. + +- **Distributed Task Coordination**: In a microservices architecture, coordinating tasks across services can be challenging. Use QStash to queue tasks that are dependent on the completion of other services' tasks. Pipedream workflows can then act on task completion events, such as updating a database in Airtable or triggering a new deployment in Heroku, ensuring smooth inter-service operation. diff --git a/components/quaderno/README.md b/components/quaderno/README.md new file mode 100644 index 0000000000000..9703802e614a6 --- /dev/null +++ b/components/quaderno/README.md @@ -0,0 +1,11 @@ +# Overview + +The Quaderno API provides robust capabilities for handling sales tax, VAT, and GST compliance. It allows you to automate tax calculations, create and send invoices, and manage transactions and reports with ease. Integrating the Quaderno API on Pipedream opens up opportunities to streamline your finance operations by connecting to various other services like CRMs, payment gateways, and e-commerce platforms, all while leveraging Pipedream's serverless platform to execute custom logic without managing infrastructure. + +# Example Use Cases + +- **Automated Tax Compliance for E-Commerce Sales**: Set up a workflow that triggers whenever a new order is placed in your e-commerce platform (like Shopify). The workflow calculates the appropriate taxes using Quaderno, creates an invoice, and then stores the transaction details in a Google Sheet for record-keeping. + +- **Invoice Generation on Subscription Renewal**: Each time a subscription renews in a payment service like Stripe, trigger a Pipedream workflow that uses Quaderno to generate an invoice with the correct tax rates and sends it to the customer via email, simplifying the recurring billing process. + +- **Expense Tracking and Reporting**: Create a workflow that listens for new expenses entered in an accounting app like QuickBooks. When a new expense is detected, the workflow adds the expense to Quaderno, calculates the tax implications, and then updates a dashboard in a BI tool like Tableau, providing real-time expense tracking and insights. diff --git a/components/qualaroo/README.md b/components/qualaroo/README.md index 0f3101403fa09..7d62b5770440a 100644 --- a/components/qualaroo/README.md +++ b/components/qualaroo/README.md @@ -1,18 +1,11 @@ # Overview -Using the Qualaroo API, you can build comprehensive customer feedback systems -for your business or website. Qualaroo helps you discover the information you -need from your customers, capture every detail and make changes to improve the -customer experience. With Qualaroo, you can easily build surveys, polls, and -other forms of customer feedback. +Qualaroo is an advanced tool for collecting user feedback through surveys and analyzing those insights. With the Qualaroo API, you can programmatically manage your surveys, retrieve survey responses, and integrate user feedback into your data pipeline to enhance decision-making. On Pipedream, you can harness this API to automate the aggregation of survey data, trigger actions based on feedback, and sync insights across various platforms. -Examples of Things You Can Build Using the Qualaroo API: +# Example Use Cases -- Surveys to measure customer satisfaction -- Polls to quickly collect opinions -- Gather in-depth customer insights -- Create targeted questions for different customers -- Capture customer feedback and analyze it in real-time -- Customize text and coding to create surveys and polls -- Deliver survey results to your development team -- Integrate surveys into your website or mobile app +- **Automated Feedback Analysis Workflow**: Retrieve Qualaroo survey responses in real-time via Pipedream, analyze them using natural language processing (NLP) with a service like Google Cloud Natural Language, and store the sentiment analysis results in a Google Sheets spreadsheet for easy visualization and team collaboration. + +- **Customer Support Enhancement Workflow**: When a user submits negative feedback through a Qualaroo survey, trigger a customer support ticket in a CRM like Zendesk or HubSpot. This immediate response can help address user concerns quickly, potentially improving user satisfaction and loyalty. + +- **Product Feature Request Aggregator Workflow**: Collect and categorize feature requests from Qualaroo surveys, push them into a prioritization tool like Trello or JIRA, and automatically notify the product team via Slack or email. This streamlines the process of gathering user input and turning it into actionable development tasks. diff --git a/components/qualetics/README.md b/components/qualetics/README.md new file mode 100644 index 0000000000000..96de864403eaf --- /dev/null +++ b/components/qualetics/README.md @@ -0,0 +1,11 @@ +# Overview + +The Qualetics API provides data intelligence as a service, allowing you to transform your data into actionable insights. With this API, you can perform advanced data analysis, predict trends, and generate reports to make informed decisions swiftly. On Pipedream, Qualetics can be integrated into workflows to automate data analysis, connect with other services for enriched data operations, and trigger actions based on the insights derived. + +# Example Use Cases + +- **Automated Data Analysis and Reporting**: Schedule a Pipedream workflow to pull data from your apps, send it to the Qualetics API for analysis, and compile the results into a report. This could be automatically emailed to stakeholders using a service like SendGrid or saved to Google Sheets for easy access and sharing. + +- **Real-time Data Monitoring and Alerts**: Create a Pipedream workflow that listens to signals from your product or service, sends this data to Qualetics for analysis, and uses the results to trigger alerts. For instance, when key performance indicators drop below a certain threshold, an alert could be sent via Slack to your team. + +- **Enhanced Customer Experience with Predictive Analytics**: Leverage the Qualetics API to predict customer behavior by analyzing historical data. Integrate this with a CRM tool like Salesforce on Pipedream to flag high-value customers or potential churn risks, enabling proactive engagement or tailored marketing campaigns. diff --git a/components/qualiobee/package.json b/components/qualiobee/package.json new file mode 100644 index 0000000000000..9d0856b3ec6e6 --- /dev/null +++ b/components/qualiobee/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/qualiobee", + "version": "0.0.1", + "description": "Pipedream Qualiobee Components", + "main": "qualiobee.app.mjs", + "keywords": [ + "pipedream", + "qualiobee" + ], + "homepage": "https://pipedream.com/apps/qualiobee", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/qualiobee/qualiobee.app.mjs b/components/qualiobee/qualiobee.app.mjs new file mode 100644 index 0000000000000..7d6853dbb8e76 --- /dev/null +++ b/components/qualiobee/qualiobee.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "qualiobee", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/quentn/README.md b/components/quentn/README.md index b28aca48775ba..b60ef13d2846d 100644 --- a/components/quentn/README.md +++ b/components/quentn/README.md @@ -1,28 +1,11 @@ # Overview -The Quentn API offers a powerful platform to help you build custom applications -and websites by using artificial intelligence (AI). The AI-based technology -makes it easy to create automated processes and optimize customer engagement. -With the Quentn API, you can develop applications that can automate customer -support, provide personalized customer experiences, and create intelligent -customer journeys. In addition, the Quentn API offers data analytics to help -you measure and optimize the performance of your applications. +Quentn is a marketing automation platform that enables users to streamline communication with their leads and customers. With the Quentn API on Pipedream, you can automate tasks like managing contacts, sending personalized emails, and triggering actions based on customer behaviors. It allows for a more targeted approach to marketing by engaging with your audience at critical points in the customer journey, all while saving time with automated workflows. -The following is a list of some examples of applications you can build using -the Quentn API: +# Example Use Cases -- Chatbots: Automate customer support, provide personalized product - recommendations, and manage customer inquiries. -- Personalization Engines: Tailor customer engagement by delivering relevant - content, videos, product recommendations, and more. -- Autoresponders: Set up automatic responses for specific customer inquiries or - requests. -- Data Analytics: Analyze customer behavior and preferences to optimize user - experience and improve product performance. -- Journeys: Automate customer journeys by integrating the Quentn API with - third-party tools and services. -- Custom Applications: Build custom applications that use Quentn's AI-based - features. -- Website Optimization: Optimize the design and performance of your website. -- Machine Learning: Utilize machine learning to make decisions about customer - engagement. +- **Automated Lead Scoring and Segmentation**: Use Quentn to track customer interactions and score leads based on activity. Combine this data with Pipedream's functionality to segment and transfer leads to different lists or CRMs, ensuring tailored follow-ups. + +- **Targeted Email Campaigns Triggered by User Behavior**: Set up a Pipedream workflow that listens for specific actions taken by users, like visiting a pricing page or downloading a white paper. Leverage Quentn's API to trigger an email sequence offering further information, discounts, or a personal consultation, pushing them further down the funnel. + +- **Customer Feedback and Reactivation Campaigns**: Implement a Pipedream workflow to monitor customer activity. Use Quentn to reach out with a feedback request survey after a purchase or to re-engage users who haven't interacted with your brand in a while with a special offer. diff --git a/components/questionpro/README.md b/components/questionpro/README.md new file mode 100644 index 0000000000000..48f5628d29e75 --- /dev/null +++ b/components/questionpro/README.md @@ -0,0 +1,11 @@ +# Overview + +QuestionPro provides a suite of tools to create, distribute, and analyze surveys. Utilizing its API within Pipedream allows you to automate workflows around survey distribution, response collection, and data analysis. You can trigger actions based on new survey responses, generate reports, and integrate survey data with other apps to enrich customer profiles, support decision-making processes, or measure satisfaction levels across various services. + +# Example Use Cases + +- **Automated Survey Response Processing**: Trigger a workflow in Pipedream whenever a new response is submitted to a QuestionPro survey. Use this to update a CRM like Salesforce with customer feedback, or to push the data into a Google Sheet for further analysis. + +- **Scheduled Survey Distribution**: Set up a timed workflow in Pipedream that sends out surveys through QuestionPro at regular intervals – daily, weekly, or monthly. Combine this with a mail service like SendGrid to follow up with non-respondents, ensuring a higher response rate. + +- **Real-time Feedback Analysis**: Implement a Pipedream workflow that captures new QuestionPro survey responses, analyzes the sentiment using an AI service like MonkeyLearn, and posts a summary to a Slack channel. This can help teams quickly gauge customer sentiment and react promptly to feedback. diff --git a/components/quickbase/README.md b/components/quickbase/README.md index 2d4f1ebef191a..8a5ea0e661d64 100644 --- a/components/quickbase/README.md +++ b/components/quickbase/README.md @@ -1,21 +1,11 @@ # Overview -The Quickbase API enables developers to build business solutions quickly and -efficiently. With it, you can quickly create custom databases and applications -that support your organization. The API supports all the core features -Quickbase has to offer, like records and fields, table-level authentication, -and views and reports. All of these add up to the ability to build powerful -solutions that solve real-world problems. +Quickbase is a low-code platform for building operational applications and automations that can manage data, workflows, and reporting. Using Pipedream's serverless platform, you can tap into the robust Quickbase API to weave complex workflows that respond to events, synchronize data across multiple systems, and streamline business processes. With Pipedream, you can trigger on events in Quickbase, or from hundreds of other apps, and write Node.js code to integrate with the Quickbase API directly, enabling a broad range of automation possibilities. -Below are some examples of what developers can build using the Quickbase API: +# Example Use Cases -- Automated reports and dashboards that present data from different sources in - different ways. -- Customized applications that store and display data from Quickbase and other - external sources. -- Workflows and business processes that leverage internal and external data to - streamline operations. -- Integrations with external systems and websites that pull in data from - Quickbase and keep it up-to-date. -- A platform for developing mobile applications that access and display data - stored in Quickbase. +- **Project Management Automation**: Trigger a workflow when a new project is added to Quickbase, then send a customized email to all stakeholders using the SendGrid app on Pipedream. This workflow ensures that each stakeholder is aware of the new project and can take relevant actions promptly. + +- **Customer Support Ticket Routing**: Detect when a new support ticket is created in Quickbase. Use Pipedream to analyze the ticket's content, perhaps with a natural language processing service like Algorithmia, and then based on the analysis, assign the ticket to the correct department or individual in your customer support tool, such as Zendesk. + +- **Real-time Reporting and Alerts**: Set up a Pipedream workflow to monitor changes in critical Quickbase reports, like sales data or inventory levels. When a predefined condition is met, such as low stock on a popular item, trigger an alert and post a message to a Slack channel, so your team can act immediately to restock and avoid missed sales opportunities. diff --git a/components/quickbase/actions/common/record.mjs b/components/quickbase/actions/common/record.mjs new file mode 100644 index 0000000000000..958967465eacc --- /dev/null +++ b/components/quickbase/actions/common/record.mjs @@ -0,0 +1,75 @@ +import quickbase from "../../quickbase.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + quickbase, + appId: { + propDefinition: [ + quickbase, + "appId", + ], + }, + }, + methods: { + async getKeyFieldId() { + const { keyFieldId } = await this.quickbase.getTable({ + tableId: this.tableId, + params: { + appId: this.appId, + }, + }); + return keyFieldId; + }, + async getFieldProps() { + const props = {}; + if (!this.tableId) { + return props; + } + const fields = await this.quickbase.listFields({ + params: { + tableId: this.tableId, + }, + }); + for (const field of fields) { + const type = constants.FIELD_TYPES[field.fieldType]; + if (!type) { + continue; + } + props[field.id] = { + type, + label: field.label, + description: `Value for field "${field.label}". See the [field-types page](https://developer.quickbase.com/fieldInfo) for more information about field types.`, + optional: !field.required, + }; + } + return props; + }, + async buildRecordData(keyFieldId, data) { + const fields = await this.quickbase.listFields({ + params: { + tableId: this.tableId, + }, + }); + const fieldsToReturn = [ + keyFieldId, + ]; + for (const field of fields) { + fieldsToReturn.push(field.id); + if (this[field.id]) { + data[0][field.id] = { + value: constants.NUMERIC_FIELD_TYPES.includes(field.fieldType) + ? +this[field.id] + : constants.OBJECT_FIELD_TYPES.includes(field.fieldType) + ? JSON.parse(this[field.id]) + : this[field.id], + }; + } + } + return { + fieldsToReturn, + data, + }; + }, + }, +}; diff --git a/components/quickbase/actions/create-record/create-record.mjs b/components/quickbase/actions/create-record/create-record.mjs new file mode 100644 index 0000000000000..1eba5ceb64e17 --- /dev/null +++ b/components/quickbase/actions/create-record/create-record.mjs @@ -0,0 +1,48 @@ +import common from "../common/record.mjs"; + +export default { + ...common, + key: "quickbase-create-record", + name: "Create Record", + description: "Creates a new record in a Quick Base table. [See the documentation](https://developer.quickbase.com/operation/upsert)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + tableId: { + propDefinition: [ + common.props.quickbase, + "tableId", + (c) => ({ + appId: c.appId, + }), + ], + reloadProps: true, + }, + }, + async additionalProps() { + return await this.getFieldProps(); + }, + async run({ $ }) { + const fieldData = [ + {}, + ]; + const keyFieldId = await this.getKeyFieldId(); + const { + fieldsToReturn, data, + } = await this.buildRecordData(keyFieldId, fieldData); + const response = await this.quickbase.createOrUpdateRecord({ + $, + data: { + to: this.tableId, + data, + fieldsToReturn, + }, + }); + if (response.metadata.lineErrors) { + throw new Error(JSON.stringify(response.metadata.lineErrors)); + } + $.export("$summary", `Successfully created record in table ${this.tableId}`); + return response; + }, +}; diff --git a/components/quickbase/actions/delete-record/delete-record.mjs b/components/quickbase/actions/delete-record/delete-record.mjs new file mode 100644 index 0000000000000..d1cfab03e9124 --- /dev/null +++ b/components/quickbase/actions/delete-record/delete-record.mjs @@ -0,0 +1,44 @@ +import common from "../common/record.mjs"; + +export default { + ...common, + key: "quickbase-delete-record", + name: "Delete Record", + description: "Deletes a record in a Quick Base table. [See the documentation](https://developer.quickbase.com/operation/deleteRecords)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + tableId: { + propDefinition: [ + common.props.quickbase, + "tableId", + (c) => ({ + appId: c.appId, + }), + ], + }, + recordId: { + propDefinition: [ + common.props.quickbase, + "recordId", + (c) => ({ + appId: c.appId, + tableId: c.tableId, + }), + ], + }, + }, + async run({ $ }) { + const keyFieldId = await this.getKeyFieldId(this.appId, this.tableId); + const response = await this.quickbase.deleteRecord({ + $, + data: { + from: this.tableId, + where: `{${keyFieldId}.EX.${this.recordId}}`, + }, + }); + $.export("$summary", `Successfully deleted record ${this.recordId}`); + return response; + }, +}; diff --git a/components/quickbase/actions/update-record/update-record.mjs b/components/quickbase/actions/update-record/update-record.mjs new file mode 100644 index 0000000000000..b6b2f20259f5c --- /dev/null +++ b/components/quickbase/actions/update-record/update-record.mjs @@ -0,0 +1,59 @@ +import common from "../common/record.mjs"; + +export default { + ...common, + key: "quickbase-update-record", + name: "Update Record", + description: "Updates an existing record in a Quick Base table. [See the documentation](https://developer.quickbase.com/operation/upsert)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + tableId: { + propDefinition: [ + common.props.quickbase, + "tableId", + (c) => ({ + appId: c.appId, + }), + ], + reloadProps: true, + }, + recordId: { + propDefinition: [ + common.props.quickbase, + "recordId", + (c) => ({ + appId: c.appId, + tableId: c.tableId, + }), + ], + }, + }, + async additionalProps() { + return await this.getFieldProps(); + }, + async run({ $ }) { + const keyFieldId = await this.getKeyFieldId(); + const fieldData = [ + { + [keyFieldId]: { + value: this.recordId, + }, + }, + ]; + const { + fieldsToReturn, data, + } = await this.buildRecordData(keyFieldId, fieldData); + const response = await this.quickbase.createOrUpdateRecord({ + $, + data: { + to: this.tableId, + data, + fieldsToReturn, + }, + }); + $.export("$summary", `Successfully created record in table ${this.tableId}`); + return response; + }, +}; diff --git a/components/quickbase/common/constants.mjs b/components/quickbase/common/constants.mjs new file mode 100644 index 0000000000000..a60eef647db63 --- /dev/null +++ b/components/quickbase/common/constants.mjs @@ -0,0 +1,46 @@ +const DEFAULT_LIMIT = 50; + +const FIELD_TYPES = { + "text": "string", + "rich-text": "string", + "text-multi-line": "string", + "text-multiple-choice": "string", + "multitext": "string[]", + "address": "string", + "email": "string", + "url": "string", + "numeric": "string", + "percent": "string", + "rating": "string", + "currency": "string", + "duration": "string", + "date": "string", + "datetime": "string", + "timeofday": "string", + "checkbox": "boolean", + "phone": "string", + "user": "string", + "multiuser": "string", + "file": "string", +}; + +const NUMERIC_FIELD_TYPES = [ + "numeric", + "percent", + "rating", + "currency", + "duration", +]; + +const OBJECT_FIELD_TYPES = [ + "user", + "multiuser", + "file", +]; + +export default { + DEFAULT_LIMIT, + FIELD_TYPES, + NUMERIC_FIELD_TYPES, + OBJECT_FIELD_TYPES, +}; diff --git a/components/quickbase/package.json b/components/quickbase/package.json new file mode 100644 index 0000000000000..67c0f612ad677 --- /dev/null +++ b/components/quickbase/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/quickbase", + "version": "0.0.1", + "description": "Pipedream Quickbase Components", + "main": "quickbase.app.mjs", + "keywords": [ + "pipedream", + "quickbase" + ], + "homepage": "https://pipedream.com/apps/qualetics", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/quickbase/quickbase.app.mjs b/components/quickbase/quickbase.app.mjs index 3a4b65e224163..cec36fe02730e 100644 --- a/components/quickbase/quickbase.app.mjs +++ b/components/quickbase/quickbase.app.mjs @@ -1,11 +1,155 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "quickbase", - propDefinitions: {}, + propDefinitions: { + appId: { + type: "string", + label: "App ID", + description: "The ID of an app. If the URL from your app dashboard starts with \"https://domain.quickbase.com/db/123456\", then the app ID is `123456`", + }, + tableId: { + type: "string", + label: "Table ID", + description: "The ID of the table", + async options({ appId }) { + const tables = await this.listTables({ + params: { + appId, + }, + }); + return tables?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + recordId: { + type: "string", + label: "Record ID", + description: "The ID of the record to update or delete", + async options({ + appId, tableId, page, + }) { + const { keyFieldId } = await this.getTable({ + tableId, + params: { + appId, + }, + }); + const { data } = await this.listRecords({ + data: { + from: tableId, + select: [ + keyFieldId, + ], + options: { + top: constants.DEFAULT_LIMIT, + skip: page * constants.DEFAULT_LIMIT, + }, + }, + }); + return data?.map((record) => record[keyFieldId]) || []; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.quickbase.com/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "QB-Realm-Hostname": `${this.$auth.hostname}`, + "User-Agent": "@PipedreamHQ/pipedream v0.1", + "Authorization": `QB-USER-TOKEN ${this.$auth.user_token}`, + "Content-Type": "application/json", + }, + }); + }, + getTable({ + tableId, ...opts + }) { + return this._makeRequest({ + path: `/tables/${tableId}`, + ...opts, + }); + }, + listTables(opts = {}) { + return this._makeRequest({ + path: "/tables", + ...opts, + }); + }, + listFields(opts = {}) { + return this._makeRequest({ + path: "/fields", + ...opts, + }); + }, + listRecords(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/records/query", + ...opts, + }); + }, + createOrUpdateRecord(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/records", + ...opts, + }); + }, + deleteRecord(opts = {}) { + return this._makeRequest({ + method: "DELETE", + path: "/records", + ...opts, + }); + }, + async *paginate({ + resourceFn, + args, + max, + }) { + let total, count = 0; + args = { + ...args, + data: { + ...args.data, + options: { + top: constants.DEFAULT_LIMIT, + skip: 0, + }, + }, + }; + do { + const { data } = await resourceFn(args); + if (!data) { + return; + } + for (const item of data) { + yield item; + count++; + if (max && count >= max) { + return; + } + } + total = data?.length; + args.data.options.skip += args.data.options.top; + } while (total === args.data.options.top); }, }, }; diff --git a/components/quickbase/sources/new-record/new-record.mjs b/components/quickbase/sources/new-record/new-record.mjs new file mode 100644 index 0000000000000..1b9533d074039 --- /dev/null +++ b/components/quickbase/sources/new-record/new-record.mjs @@ -0,0 +1,110 @@ +import quickbase from "../../quickbase.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + key: "quickbase-new-record", + name: "New Record", + description: "Emit new event each time a new record is created in a specified table in Quickbase.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + quickbase, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + appId: { + propDefinition: [ + quickbase, + "appId", + ], + }, + tableId: { + propDefinition: [ + quickbase, + "tableId", + (c) => ({ + appId: c.appId, + }), + ], + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + emitEvent(record, keyFieldId) { + const meta = this.generateMeta(record, keyFieldId); + this.$emit(record, meta); + }, + generateMeta(record, keyFieldId) { + const id = record[keyFieldId].value; + return { + id, + summary: `New Record ${id}`, + ts: Date.now(), + }; + }, + async processEvent(max) { + const lastId = this._getLastId(); + const { keyFieldId } = await this.quickbase.getTable({ + tableId: this.tableId, + params: { + appId: this.appId, + }, + }); + const fields = await this.quickbase.listFields({ + params: { + tableId: this.tableId, + }, + }); + const select = fields.map(({ id }) => id); + select.push(keyFieldId); + + const records = this.quickbase.paginate({ + resourceFn: this.quickbase.listRecords, + args: { + data: { + from: this.tableId, + select, + where: `{${keyFieldId}.GT.${lastId}}`, + sortBy: [ + { + fieldId: keyFieldId, + order: "DESC", + }, + ], + }, + }, + max, + }); + + const newRecords = []; + for await (const record of records) { + newRecords.push(record); + } + + if (!newRecords.length) { + return; + } + + this._setLastId(newRecords[0][keyFieldId].value); + newRecords.reverse().forEach((record) => this.emitEvent(record, keyFieldId)); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/quickbase/sources/new-record/test-event.mjs b/components/quickbase/sources/new-record/test-event.mjs new file mode 100644 index 0000000000000..15725e1d09f4b --- /dev/null +++ b/components/quickbase/sources/new-record/test-event.mjs @@ -0,0 +1,59 @@ +export default { + "1": { + "value": "2024-03-22T16:13:34Z" + }, + "2": { + "value": "2024-03-22T16:13:32Z" + }, + "3": { + "value": 1 + }, + "4": { + "value": { + "id": "66206175.dh63", + "name": "Jane Doe" + } + }, + "5": { + "value": { + "id": "66206175.dh63", + "name": "Jane Doe" + } + }, + "6": { + "value": "John Doe" + }, + "7": { + "value": "johndoe@example.com" + }, + "8": { + "value": "(123) 456-7890" + }, + "9": { + "value": "10th St. & Constitution Ave. NW, Washington, DC 20560 United States" + }, + "10": { + "value": "10th St. & Constitution Ave. NW" + }, + "11": { + "value": "" + }, + "12": { + "value": "Washington" + }, + "13": { + "value": "DC" + }, + "14": { + "value": "20560" + }, + "15": { + "value": "United States" + }, + "16": { + "value": "" + }, + "17": { + "value": "https://domain.quickbase.com/db/bt3vraz6x?a=API_GenAddRecordForm&_fid_21=1&z=" + } +} \ No newline at end of file diff --git a/components/quickbooks/README.md b/components/quickbooks/README.md index c493df10bcac6..4e883df25ca5e 100644 --- a/components/quickbooks/README.md +++ b/components/quickbooks/README.md @@ -1,21 +1,11 @@ # Overview -The Quickbooks API is an incredibly powerful tool for businesses of all kinds -that need help managing their finances. With Quickbooks, businesses can easily -process payments, keep track of billing, create invoices and so much more. -Through the Quickbooks API, businesses can customize the platform to fit their -individual needs and create custom, automated transactions that can streamline -processes. Here are a few examples of what you can build using the Quickbooks -API: +The QuickBooks API allows for streamlined financial management within Pipedream's ecosystem, enabling automated accounting and data syncing across various platforms. With this API, you can manipulate invoices, manage sales receipts, handle expenses, and synchronize customer data. It's a robust tool for financial oversight and automation that can save time and reduce errors for businesses of all sizes. -- Automated payment processing -- Customer invoices -- Customized billing cycles -- Automated expense tracking -- Client account monitoring -- Financial analytics and insights -- Automated reminders -- Transaction histories and insights -- Global payment processing -- Accounting integration -- Sales tax calculation and compliance +# Example Use Cases + +- **Automated Invoice Generation**: Trigger the creation of invoices in QuickBooks when a new order is placed in an e-commerce platform like Shopify. Once the order is confirmed, a workflow can capture the details and generate an invoice, ensuring accurate billing without manual intervention. + +- **Expense Tracking and Approval**: Connect QuickBooks to email or messaging apps like Slack to automate expense submission and approval processes. When an employee submits an expense, the workflow can parse the receipt, create an expense entry in QuickBooks, and post a message to a Slack channel for managerial approval. + +- **Synchronized Customer Data**: Maintain up-to-date customer records by syncing QuickBooks with a CRM like Salesforce. When customer information is updated in Salesforce, a Pipedream workflow can automatically reflect those changes in QuickBooks, keeping financial records aligned with sales data. diff --git a/components/quickbooks/actions/create-ap-aging-report/create-ap-aging-report.mjs b/components/quickbooks/actions/create-ap-aging-report/create-ap-aging-report.mjs new file mode 100644 index 0000000000000..74bfef08d4dc7 --- /dev/null +++ b/components/quickbooks/actions/create-ap-aging-report/create-ap-aging-report.mjs @@ -0,0 +1,102 @@ +import quickbooks from "../../quickbooks.app.mjs"; +import { AP_AGING_REPORT_COLUMNS } from "../../common/constants.mjs"; + +export default { + key: "quickbooks-create-ap-aging-report", + name: "Create AP Aging Detail Report", + description: "Creates an AP aging report in Quickbooks Online. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/apagingdetail#query-a-report)", + version: "0.0.3", + type: "action", + props: { + quickbooks, + shipvia: { + type: "string", + label: "Ship Via", + description: "Filter by the shipping method", + optional: true, + }, + termIds: { + propDefinition: [ + quickbooks, + "termIds", + ], + }, + startDueDate: { + type: "string", + label: "Start Due Date", + description: "The range of dates over which receivables are due, in the format `YYYY-MM-DD`. `start_duedate` must be less than `end_duedate`. If not specified, all data is returned.", + optional: true, + }, + endDueDate: { + type: "string", + label: "End Due Date", + description: "The range of dates over which receivables are due, in the format `YYYY-MM-DD`. `start_duedate` must be less than `end_duedate`. If not specified, all data is returned.", + optional: true, + }, + accountingMethod: { + propDefinition: [ + quickbooks, + "accountingMethod", + ], + }, + reportDate: { + type: "string", + label: "Report Date", + description: "Start date to use for the report, in the format `YYYY-MM-DD`", + optional: true, + }, + numPeriods: { + type: "integer", + label: "Num Periods", + description: "The number of periods to be shown in the report", + optional: true, + }, + vendorIds: { + propDefinition: [ + quickbooks, + "vendorIds", + ], + }, + pastDue: { + type: "integer", + label: "Past Due", + description: "Filters report contents based on minimum days past due", + optional: true, + }, + agingPeriod: { + type: "integer", + label: "Aging Period", + description: "The number of days in the aging period", + optional: true, + }, + columns: { + propDefinition: [ + quickbooks, + "columns", + ], + options: AP_AGING_REPORT_COLUMNS, + }, + }, + async run({ $ }) { + const response = await this.quickbooks.getApAgingReport({ + $, + params: { + shipvia: this.shipvia, + term: this.termIds, + start_duedate: this.startDueDate, + end_duedate: this.endDueDate, + accounting_method: this.accountingMethod, + report_date: this.reportDate, + num_periods: this.numPeriods, + vendor: this.vendorIds, + past_due: this.pastDue, + aging_period: this.agingPeriod, + columns: this.columns, + }, + }); + if (response) { + $.export("summary", "Successfully created AP Aging Detail Report"); + } + return response; + }, +}; diff --git a/components/quickbooks/actions/create-bill/create-bill.mjs b/components/quickbooks/actions/create-bill/create-bill.mjs index 7532271182193..67ff7d2a72224 100644 --- a/components/quickbooks/actions/create-bill/create-bill.mjs +++ b/components/quickbooks/actions/create-bill/create-bill.mjs @@ -1,84 +1,129 @@ import { ConfigurationError } from "@pipedream/platform"; import quickbooks from "../../quickbooks.app.mjs"; +import { parseLineItems } from "../../common/utils.mjs"; export default { key: "quickbooks-create-bill", name: "Create Bill", - description: "Creates a bill. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/bill#create-a-bill)", - version: "0.1.3", + description: "Creates a bill. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/bill#create-a-bill)", + version: "0.1.8", type: "action", props: { quickbooks, vendorRefValue: { - label: "Vendor Ref Value", - type: "string", - description: "Reference to the vendor for this transaction. Query the Vendor name list resource to determine the appropriate Vendor object for this reference. Use `Vendor.Id` from that object for `VendorRef.value`.", - }, - lineItems: { - description: "Individual line items of a transaction. Valid Line types include: `ItemBasedExpenseLine` and `AccountBasedExpenseLine`. One minimum line item required for the request to succeed. E.g `[ { \"DetailType\": \"AccountBasedExpenseLineDetail\", \"Amount\": 200.0, \"AccountBasedExpenseLineDetail\": { \"AccountRef\": { \"value\": \"1\" } } } ]`", propDefinition: [ quickbooks, - "lineItems", + "vendorIds", ], - }, - vendorRefName: { - label: "Vendor Reference Name", type: "string", - description: "Reference to the vendor for this transaction. Query the Vendor name list resource to determine the appropriate Vendor object for this reference. Use `Vendor.Name` from that object for `VendorRef.name`.", - optional: true, + label: "Vendor ID", + description: "Reference to the vendor for this transaction", + optional: false, }, currencyRefValue: { propDefinition: [ quickbooks, - "currencyRefValue", + "currency", ], }, - currencyRefName: { + lineItemsAsObjects: { propDefinition: [ quickbooks, - "currencyRefName", + "lineItemsAsObjects", ], + reloadProps: true, }, - minorVersion: { - propDefinition: [ - quickbooks, - "minorVersion", - ], + }, + async additionalProps() { + const props = {}; + if (this.lineItemsAsObjects) { + props.lineItems = { + type: "string[]", + label: "Line Items", + description: "Line items of a bill. Set DetailType to `AccountBasedExpenseLineDetail`. Example: `{ \"DetailType\": \"AccountBasedExpenseLineDetail\", \"Amount\": 100.0, \"AccountBasedExpenseLineDetail\": { \"AccountRef\": { \"name\": \"Advertising\", \"value\": \"1\" } } }`", + }; + return props; + } + props.numLineItems = { + type: "integer", + label: "Number of Line Items", + description: "The number of line items to enter", + reloadProps: true, + }; + if (!this.numLineItems) { + return props; + } + for (let i = 1; i <= this.numLineItems; i++) { + props[`account_${i}`] = { + type: "string", + label: `Line ${i} - Account ID`, + options: async ({ page }) => { + return this.quickbooks.getPropOptions({ + page, + resource: "Account", + mapper: ({ + Id: value, Name: label, + }) => ({ + value, + label, + }), + }); + }, + }; + props[`amount_${i}`] = { + type: "string", + label: `Line ${i} - Amount`, + }; + } + return props; + }, + methods: { + buildLineItems() { + const lineItems = []; + for (let i = 1; i <= this.numLineItems; i++) { + lineItems.push({ + DetailType: "AccountBasedExpenseLineDetail", + Amount: this[`amount_${i}`], + AccountBasedExpenseLineDetail: { + AccountRef: { + value: this[`account_${i}`], + }, + }, + }); + } + return lineItems; }, }, async run({ $ }) { - if (!this.vendorRefValue || !this.lineItems) { + if (!this.vendorRefValue || (!this.numLineItems && !this.lineItemsAsObjects)) { throw new ConfigurationError("Must provide vendorRefValue, and lineItems parameters."); } - try { - this.lineItems = this.lineItems.map((lineItem) => typeof lineItem === "string" - ? JSON.parse(lineItem) - : lineItem); - } catch (error) { - throw new ConfigurationError(`We got an error trying to parse the LineItems. Error: ${error}`); - } + const lines = this.lineItemsAsObjects + ? parseLineItems(this.lineItems) + : this.buildLineItems(); + + lines.forEach((line) => { + if (line.DetailType !== "AccountBasedExpenseLineDetail") { + throw new ConfigurationError("Line Item DetailType must be `AccountBasedExpenseLineDetail`"); + } + }); const response = await this.quickbooks.createBill({ $, data: { VendorRef: { value: this.vendorRefValue, - name: this.vendorRefName, }, - Line: this.lineItems, + Line: lines, CurrencyRef: { value: this.currencyRefValue, - name: this.currencyRefName, }, }, - params: { - minorversion: this.minorVersion, - }, }); if (response) { - $.export("summary", `Successfully created bill with id ${response.Bill.Id}`); + $.export("summary", `Successfully created bill with ID ${response.Bill.Id}`); } return response; diff --git a/components/quickbooks/actions/create-customer/create-customer.mjs b/components/quickbooks/actions/create-customer/create-customer.mjs index 0f56870eb41de..d2128293bb329 100644 --- a/components/quickbooks/actions/create-customer/create-customer.mjs +++ b/components/quickbooks/actions/create-customer/create-customer.mjs @@ -4,8 +4,8 @@ import quickbooks from "../../quickbooks.app.mjs"; export default { key: "quickbooks-create-customer", name: "Create Customer", - description: "Creates a customer. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/customer#create-a-customer)", - version: "0.1.3", + description: "Creates a customer. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/customer#create-a-customer)", + version: "0.1.8", type: "action", props: { quickbooks, @@ -45,12 +45,6 @@ export default { "suffix", ], }, - minorVersion: { - propDefinition: [ - quickbooks, - "minorVersion", - ], - }, }, async run({ $ }) { if ( @@ -70,13 +64,10 @@ export default { FamilyName: this.familyName, GivenName: this.givenName, }, - params: { - minorversion: this.minorVersion, - }, }); if (response) { - $.export("summary", `Successfully created customer with id ${response.Customer.Id}`); + $.export("summary", `Successfully created customer with ID ${response.Customer.Id}`); } return response; diff --git a/components/quickbooks/actions/create-invoice/create-invoice.mjs b/components/quickbooks/actions/create-invoice/create-invoice.mjs index 9771634259b9c..9728c9c817fde 100644 --- a/components/quickbooks/actions/create-invoice/create-invoice.mjs +++ b/components/quickbooks/actions/create-invoice/create-invoice.mjs @@ -1,83 +1,125 @@ import { ConfigurationError } from "@pipedream/platform"; import quickbooks from "../../quickbooks.app.mjs"; +import { parseLineItems } from "../../common/utils.mjs"; export default { key: "quickbooks-create-invoice", name: "Create Invoice", - description: "Creates an invoice. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#create-an-invoice)", - version: "0.1.3", + description: "Creates an invoice. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#create-an-invoice)", + version: "0.1.8", type: "action", props: { quickbooks, - lineItems: { - propDefinition: [ - quickbooks, - "lineItems", - ], - }, customerRefValue: { - label: "Customer Reference Value", - type: "string", - description: "Reference to a customer or job. Query the Customer name list resource to determine the appropriate Customer object for this reference. Use `Customer.Id` from that object for `CustomerRef.value`.", - }, - customerRefName: { propDefinition: [ quickbooks, - "customerRefName", + "customer", ], }, currencyRefValue: { propDefinition: [ quickbooks, - "currencyRefValue", + "currency", ], }, - currencyRefName: { + lineItemsAsObjects: { propDefinition: [ quickbooks, - "currencyRefName", + "lineItemsAsObjects", ], + reloadProps: true, }, - minorVersion: { - propDefinition: [ - quickbooks, - "minorVersion", - ], + }, + async additionalProps() { + const props = {}; + if (this.lineItemsAsObjects) { + props.lineItems = { + type: "string[]", + label: "Line Items", + description: "Line items of an invoice. Set DetailType to `SalesItemLineDetail`, `GroupLineDetail`, or `DescriptionOnly`. Example: `{ \"DetailType\": \"SalesItemLineDetail\", \"Amount\": 100.0, \"SalesItemLineDetail\": { \"ItemRef\": { \"name\": \"Services\", \"value\": \"1\" } } }`", + }; + return props; + } + props.numLineItems = { + type: "integer", + label: "Number of Line Items", + description: "The number of line items to enter", + reloadProps: true, + }; + if (!this.numLineItems) { + return props; + } + for (let i = 1; i <= this.numLineItems; i++) { + props[`item_${i}`] = { + type: "string", + label: `Line ${i} - Item ID`, + options: async ({ page }) => { + return this.quickbooks.getPropOptions({ + page, + resource: "Item", + mapper: ({ + Id: value, Name: label, + }) => ({ + value, + label, + }), + }); + }, + }; + props[`amount_${i}`] = { + type: "string", + label: `Line ${i} - Amount`, + }; + } + return props; + }, + methods: { + buildLineItems() { + const lineItems = []; + for (let i = 1; i <= this.numLineItems; i++) { + lineItems.push({ + DetailType: "SalesItemLineDetail", + Amount: this[`amount_${i}`], + SalesItemLineDetail: { + ItemRef: { + value: this[`item_${i}`], + }, + }, + }); + } + return lineItems; }, }, async run({ $ }) { - if (!this.lineItems || !this.customerRefValue) { + if ((!this.numLineItems && !this.lineItemsAsObjects) || !this.customerRefValue) { throw new ConfigurationError("Must provide lineItems, and customerRefValue parameters."); } - try { - this.lineItems = this.lineItems.map((lineItem) => typeof lineItem === "string" - ? JSON.parse(lineItem) - : lineItem); - } catch (error) { - throw new ConfigurationError(`We got an error trying to parse the LineItems. Error: ${error}`); - } + const lines = this.lineItemsAsObjects + ? parseLineItems(this.lineItems) + : this.buildLineItems(); + + lines.forEach((line) => { + if (line.DetailType !== "SalesItemLineDetail" && line.DetailType !== "GroupLineDetail" && line.DetailType !== "DescriptionOnly") { + throw new ConfigurationError("Line Item DetailType must be `SalesItemLineDetail`, `GroupLineDetail`, or `DescriptionOnly`"); + } + }); const response = await this.quickbooks.createInvoice({ $, data: { - Line: this.lineItems, + Line: lines, CustomerRef: { value: this.customerRefValue, - name: this.customerRefName, }, CurrencyRef: { value: this.currencyRefValue, - name: this.currencyRefName, }, }, - params: { - minorversion: this.minorVersion, - }, }); if (response) { - $.export("summary", `Successfully created invoice with id ${response.Invoice.Id}`); + $.export("summary", `Successfully created invoice with ID ${response.Invoice.Id}`); } return response; diff --git a/components/quickbooks/actions/create-payment/create-payment.mjs b/components/quickbooks/actions/create-payment/create-payment.mjs index eceb3b06d4f38..569ea05d946d1 100644 --- a/components/quickbooks/actions/create-payment/create-payment.mjs +++ b/components/quickbooks/actions/create-payment/create-payment.mjs @@ -3,8 +3,8 @@ import quickbooks from "../../quickbooks.app.mjs"; export default { key: "quickbooks-create-payment", name: "Create Payment", - description: "Creates a payment. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/payment#create-a-payment)", - version: "0.0.2", + description: "Creates a payment. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/payment#create-a-payment)", + version: "0.0.7", type: "action", props: { quickbooks, @@ -14,26 +14,15 @@ export default { type: "string", }, customerRefValue: { - label: "Customer Reference Value", - description: "Reference to a customer or job. Query the Customer name list resource to determine the appropriate Customer object for this reference. Use `Customer.Id` from that object for `CustomerRef.value`.", - type: "string", - }, - customerRefName: { propDefinition: [ quickbooks, - "customerRefName", + "customer", ], }, currencyRefValue: { propDefinition: [ quickbooks, - "currencyRefValue", - ], - }, - currencyRefName: { - propDefinition: [ - quickbooks, - "currencyRefName", + "currency", ], }, }, @@ -44,17 +33,15 @@ export default { TotalAmt: this.totalAmount, CustomerRef: { value: this.customerRefValue, - name: this.customerRefName, }, CurrencyRef: { value: this.currencyRefValue, - name: this.currencyRefName, }, }, }); if (response) { - $.export("summary", `Successfully created payment with id ${response.Payment.Id}`); + $.export("summary", `Successfully created payment with ID ${response.Payment.Id}`); } return response; diff --git a/components/quickbooks/actions/create-pl-report/create-pl-report.mjs b/components/quickbooks/actions/create-pl-report/create-pl-report.mjs new file mode 100644 index 0000000000000..87dbf1c088a17 --- /dev/null +++ b/components/quickbooks/actions/create-pl-report/create-pl-report.mjs @@ -0,0 +1,144 @@ +import quickbooks from "../../quickbooks.app.mjs"; +import { + DATE_MACRO_OPTIONS, PAYMENT_METHOD_OPTIONS, ACCOUNT_TYPE_OPTIONS, PROFIT_LOSS_REPORT_COLUMNS, +} from "../../common/constants.mjs"; + +export default { + key: "quickbooks-create-pl-report", + name: "Create Profit and Loss Detail Report", + description: "Creates a profit and loss report in Quickbooks Online. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/profitandloss#query-a-report)", + version: "0.0.3", + type: "action", + props: { + quickbooks, + customerIds: { + propDefinition: [ + quickbooks, + "customer", + ], + type: "string[]", + label: "Customer Ids", + description: "Filters report contents to include information for specified customers", + optional: true, + }, + accountIds: { + propDefinition: [ + quickbooks, + "accountIds", + ], + }, + accountingMethod: { + propDefinition: [ + quickbooks, + "accountingMethod", + ], + }, + dateMacro: { + type: "string", + label: "Date Macro", + description: "Predefined date range. Use if you want the report to cover a standard report date range; otherwise, use the `start_date` and `end_date` to cover an explicit report date range", + options: DATE_MACRO_OPTIONS, + optional: true, + }, + startDate: { + type: "string", + label: "Start Date", + description: "The start date of the report, in the format `YYYY-MM-DD`. `start_date` must be less than `end_date`", + optional: true, + }, + endDate: { + type: "string", + label: "End Date", + description: "The end date of the report, in the format `YYYY-MM-DD`. `start_date` must be less than `end_date`", + optional: true, + }, + adjustedGainLoss: { + type: "string", + label: "Adjusted Gain Loss", + description: "Specifies whether unrealized gain and losses are included in the report", + options: [ + "true", + "false", + ], + optional: true, + }, + classIds: { + propDefinition: [ + quickbooks, + "classIds", + ], + }, + paymentMethod: { + type: "string", + label: "Payment Method", + description: "Filters report contents based on payment method", + options: PAYMENT_METHOD_OPTIONS, + optional: true, + }, + employeeIds: { + propDefinition: [ + quickbooks, + "employeeIds", + ], + }, + departmentIds: { + propDefinition: [ + quickbooks, + "departmentIds", + ], + }, + vendorIds: { + propDefinition: [ + quickbooks, + "vendorIds", + ], + }, + accountType: { + type: "string", + label: "Account Type", + description: "Account type from which transactions are included in the report", + options: ACCOUNT_TYPE_OPTIONS, + optional: true, + }, + sortBy: { + type: "string", + label: "Sort By", + description: "The column type used in sorting report rows", + options: PROFIT_LOSS_REPORT_COLUMNS, + optional: true, + }, + columns: { + propDefinition: [ + quickbooks, + "columns", + ], + options: PROFIT_LOSS_REPORT_COLUMNS, + }, + }, + async run({ $ }) { + const response = await this.quickbooks.getProfitLossReport({ + $, + params: { + customer: this.customerIds, + account: this.accountIds, + accounting_method: this.accountingMethod, + date_macro: this.dateMacro, + start_date: this.startDate, + end_date: this.endDate, + adjusted_gain_loss: this.adjustedGainLoss, + class: this.classIds, + payment_method: this.paymentMethod, + employee: this.employeeIds, + department: this.departmentIds, + vendor: this.vendorIds, + account_type: this.accountType, + sort_by: this.sortBy, + columns: this.columns, + }, + }); + if (response) { + $.export("summary", "Successfully created Profit and Loss Detail Report"); + } + return response; + }, +}; diff --git a/components/quickbooks/actions/create-purchase/create-purchase.mjs b/components/quickbooks/actions/create-purchase/create-purchase.mjs index 212a49137c244..7fb08e7855cfe 100644 --- a/components/quickbooks/actions/create-purchase/create-purchase.mjs +++ b/components/quickbooks/actions/create-purchase/create-purchase.mjs @@ -1,18 +1,24 @@ -import { parseOne } from "../../common/utils.mjs"; import quickbooks from "../../quickbooks.app.mjs"; +import { parseLineItems } from "../../common/utils.mjs"; +import { ConfigurationError } from "@pipedream/platform"; export default { key: "quickbooks-create-purchase", name: "Create Purchase", - description: "Creates a new purchase. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/purchase#create-a-purchase)", - version: "0.0.1", + description: "Creates a new purchase. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/purchase#create-a-purchase)", + version: "0.0.6", type: "action", props: { quickbooks, accountRefValue: { - label: "Account Reference Value", + propDefinition: [ + quickbooks, + "accountIds", + ], type: "string", - description: "Specifies the id of the account reference. Check must specify bank account, CreditCard must specify credit card account. Validation Rules:Valid and Active Account Reference of an appropriate type.", + label: "Account Reference Value", + description: "Specifies the ID of the account reference. Check must specify bank account, CreditCard must specify credit card account. Validation Rules:Valid and Active Account Reference of an appropriate type.", + optional: false, }, paymentType: { label: "Payment Type", @@ -24,64 +30,106 @@ export default { "CreditCard", ], }, - lineItems: { + currencyRefValue: { propDefinition: [ quickbooks, - "lineItems", + "currency", ], }, - accountRefName: { - label: "Account Reference Name", - type: "string", - description: "Specifies the name of the account reference. Check must specify bank account, CreditCard must specify credit card account. Validation Rules:Valid and Active Account Reference of an appropriate type.", - optional: true, - }, - currencyRefValue: { - label: "Currency Reference Value", - type: "string", - description: "A three letter string representing the ISO 4217 code for the currency. For example, `USD`, `AUD`, `EUR`, and so on. This must be defined if multicurrency is enabled for the company.\nMulticurrency is enabled for the company if `Preferences.MultiCurrencyEnabled` is set to `true`. Read more about multicurrency support [here](https://developer.intuit.com/docs?RedirectID=MultCurrencies). Required if multicurrency is enabled for the company.", - optional: true, - }, - currencyRefName: { - label: "Currency Reference Name", - type: "object", - description: "The full name of the currency.", - optional: true, + lineItemsAsObjects: { + propDefinition: [ + quickbooks, + "lineItemsAsObjects", + ], + reloadProps: true, }, - minorversion: { - label: "Minor Version", - type: "string", - description: "Use the minorversion query parameter in REST API requests to access a version of the API other than the generally available version. For example, to invoke minor version 1 of the JournalEntry entity, issue the following request:\n`https://quickbooks.api.intuit.com/v3/company//journalentry/entityId?minorversion=1`", - optional: true, + }, + async additionalProps() { + const props = {}; + if (this.lineItemsAsObjects) { + props.lineItems = { + type: "string[]", + label: "Line Items", + description: "Line items of a purchase. Set DetailType to `AccountBasedExpenseLineDetail`. Example: `{ \"DetailType\": \"AccountBasedExpenseLineDetail\", \"Amount\": 100.0, \"AccountBasedExpenseLineDetail\": { \"AccountRef\": { \"name\": \"Advertising\", \"value\": \"1\" } } }`", + }; + return props; + } + props.numLineItems = { + type: "integer", + label: "Number of Line Items", + description: "The number of line items to enter", + reloadProps: true, + }; + if (!this.numLineItems) { + return props; + } + for (let i = 1; i <= this.numLineItems; i++) { + props[`account_${i}`] = { + type: "string", + label: `Line ${i} - Account ID`, + options: async ({ page }) => { + return this.quickbooks.getPropOptions({ + page, + resource: "Account", + mapper: ({ + Id: value, Name: label, + }) => ({ + value, + label, + }), + }); + }, + }; + props[`amount_${i}`] = { + type: "string", + label: `Line ${i} - Amount`, + }; + } + return props; + }, + methods: { + buildLineItems() { + const lineItems = []; + for (let i = 1; i <= this.numLineItems; i++) { + lineItems.push({ + DetailType: "AccountBasedExpenseLineDetail", + Amount: this[`amount_${i}`], + AccountBasedExpenseLineDetail: { + AccountRef: { + value: this[`account_${i}`], + }, + }, + }); + } + return lineItems; }, }, async run({ $ }) { + const lines = this.lineItemsAsObjects + ? parseLineItems(this.lineItems) + : this.buildLineItems(); - let parsedLineItems = parseOne(this.lineItems); - + lines.forEach((line) => { + if (line.DetailType !== "AccountBasedExpenseLineDetail") { + throw new ConfigurationError("Line Item DetailType must be `AccountBasedExpenseLineDetail`"); + } + }); const response = await this.quickbooks.createPurchase({ $, data: { PaymentType: this.paymentType, AccountRef: { value: this.accountRefValue, - name: this.accountRefName, }, - Line: parsedLineItems.length - ? parsedLineItems.map((item) => parseOne(item)) - : [], + Line: lines, CurrencyRef: { value: this.currencyRefValue, - name: this.currencyRefName, }, }, - params: { - minorversion: this.minorVersion, - }, }); if (response) { - $.export("summary", `Successfully created purchase with id ${response.Purchase.Id}`); + $.export("summary", `Successfully created purchase with ID ${response.Purchase.Id}`); } return response; diff --git a/components/quickbooks/actions/create-sales-receipt/create-sales-receipt.mjs b/components/quickbooks/actions/create-sales-receipt/create-sales-receipt.mjs index 29685f43c02f8..ab80a8995bf2d 100644 --- a/components/quickbooks/actions/create-sales-receipt/create-sales-receipt.mjs +++ b/components/quickbooks/actions/create-sales-receipt/create-sales-receipt.mjs @@ -1,71 +1,113 @@ -import { ConfigurationError } from "@pipedream/platform"; import quickbooks from "../../quickbooks.app.mjs"; +import { parseLineItems } from "../../common/utils.mjs"; +import { ConfigurationError } from "@pipedream/platform"; export default { key: "quickbooks-create-sales-receipt", name: "Create Sales Receipt", description: "Creates a sales receipt. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/salesreceipt#create-a-salesreceipt)", - version: "0.0.2", + version: "0.0.7", type: "action", props: { quickbooks, - lineItems: { + lineItemsAsObjects: { propDefinition: [ quickbooks, - "lineItems", + "lineItemsAsObjects", ], + reloadProps: true, }, currencyRefValue: { propDefinition: [ quickbooks, - "currencyRefValue", - ], - optional: true, - }, - currencyRefName: { - propDefinition: [ - quickbooks, - "currencyRefName", + "currency", ], optional: true, }, }, + async additionalProps() { + const props = {}; + if (this.lineItemsAsObjects) { + props.lineItems = { + type: "string[]", + label: "Line Items", + description: "Line items of a sales receipt. Set DetailType to `SalesItemLineDetail` or `GroupLineDetail`. Example: `{ \"DetailType\": \"SalesItemLineDetail\", \"Amount\": 100.0, \"SalesItemLineDetail\": { \"ItemRef\": { \"name\": \"Services\", \"value\": \"1\" } } }`", + }; + return props; + } + props.numLineItems = { + type: "integer", + label: "Number of Line Items", + description: "The number of line items to enter", + reloadProps: true, + }; + if (!this.numLineItems) { + return props; + } + for (let i = 1; i <= this.numLineItems; i++) { + props[`item_${i}`] = { + type: "string", + label: `Line ${i} - Item ID`, + options: async ({ page }) => { + return this.quickbooks.getPropOptions({ + page, + resource: "Item", + mapper: ({ + Id: value, Name: label, + }) => ({ + value, + label, + }), + }); + }, + }; + props[`amount_${i}`] = { + type: "string", + label: `Line ${i} - Amount`, + }; + } + return props; + }, methods: { - async createSalesReceipt({ - $, data, - }) { - return this.quickbooks._makeRequest(`company/${this.quickbooks._companyId()}/salesreceipt`, { - method: "post", - data, - }, $); + buildLineItems() { + const lineItems = []; + for (let i = 1; i <= this.numLineItems; i++) { + lineItems.push({ + DetailType: "SalesItemLineDetail", + Amount: this[`amount_${i}`], + SalesItemLineDetail: { + ItemRef: { + value: this[`item_${i}`], + }, + }, + }); + } + return lineItems; }, }, async run({ $ }) { - try { - if (typeof (this.lineItems) === "string") { - this.lineItems = JSON.parse(this.lineItems); - } else { - this.lineItems = this.lineItems.map((lineItem) => typeof lineItem === "string" - ? JSON.parse(lineItem) - : lineItem); + const lines = this.lineItemsAsObjects + ? parseLineItems(this.lineItems) + : this.buildLineItems(); + + lines.forEach((line) => { + if (line.DetailType !== "SalesItemLineDetail" && line.DetailType !== "GroupLineDetail") { + throw new ConfigurationError("Line Item DetailType must be `SalesItemLineDetail` or `GroupLineDetail`"); } - } catch (error) { - throw new ConfigurationError(`An error occurred while trying to parse the LineItems. Error: ${error}`); - } + }); - const response = await this.createSalesReceipt({ + const response = await this.quickbooks.createSalesReceipt({ $, data: { - Line: this.lineItems, + Line: lines, CurrencyRef: this.currencyRefValue && { value: this.currencyRefValue, - name: this.currencyRefName, }, }, }); if (response) { - $.export("summary", `Successfully created sales receipt with id ${response.SalesReceipt.Id}`); + $.export("summary", `Successfully created sales receipt with ID ${response.SalesReceipt.Id}`); } return response; diff --git a/components/quickbooks/actions/delete-purchase/delete-purchase.mjs b/components/quickbooks/actions/delete-purchase/delete-purchase.mjs index bf73ab919f981..7d5cdfdd0e90a 100644 --- a/components/quickbooks/actions/delete-purchase/delete-purchase.mjs +++ b/components/quickbooks/actions/delete-purchase/delete-purchase.mjs @@ -3,8 +3,8 @@ import quickbooks from "../../quickbooks.app.mjs"; export default { key: "quickbooks-delete-purchase", name: "Delete Purchase", - description: "Delete a specific purchase. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/purchase#delete-a-purchase)", - version: "0.0.1", + description: "Delete a specific purchase. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/purchase#delete-a-purchase)", + version: "0.0.6", type: "action", props: { quickbooks, @@ -14,39 +14,28 @@ export default { "purchaseId", ], }, - minorversion: { - label: "Minor Version", - type: "string", - description: "Use the minorversion query parameter in REST API requests to access a version of the API other than the generally available version. For example, to invoke minor version 1 of the JournalEntry entity, issue the following request:\n`https://quickbooks.api.intuit.com/v3/company//journalentry/entityId?minorversion=1`", - optional: true, - }, }, async run({ $ }) { - const { - quickbooks, - purchaseId, - minorversion, - } = this; + const purchaseId = this.purchaseId.value || this.purchaseId; - const [ - Id, - SyncToken, - ] = purchaseId.value.split("|"); + const { Purchase: { SyncToken: syncToken } } = await this.quickbooks.getPurchase({ + $, + purchaseId, + }); - const response = await quickbooks.deletePurchase({ + const response = await this.quickbooks.deletePurchase({ $, data: { - Id, - SyncToken, + Id: purchaseId, + SyncToken: syncToken, }, params: { - minorversion, operation: "delete", }, }); if (response) { - $.export("summary", `Successfully deleted purchase with id ${Id}`); + $.export("summary", `Successfully deleted purchase with ID ${purchaseId}`); } return response; diff --git a/components/quickbooks/actions/get-bill/get-bill.mjs b/components/quickbooks/actions/get-bill/get-bill.mjs index f3eaa3a1428ce..66ed1327a727d 100644 --- a/components/quickbooks/actions/get-bill/get-bill.mjs +++ b/components/quickbooks/actions/get-bill/get-bill.mjs @@ -4,20 +4,15 @@ import quickbooks from "../../quickbooks.app.mjs"; export default { key: "quickbooks-get-bill", name: "Get Bill", - description: "Returns info about a bill. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/bill#read-a-bill)", - version: "0.1.3", + description: "Returns info about a bill. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/bill#read-a-bill)", + version: "0.1.8", type: "action", props: { quickbooks, billId: { - label: "Bill ID", - type: "string", - description: "Id of the bill to get details of.", - }, - minorVersion: { propDefinition: [ quickbooks, - "minorVersion", + "billId", ], }, }, @@ -29,13 +24,10 @@ export default { const response = await this.quickbooks.getBill({ $, billId: this.billId, - params: { - minorversion: this.minorVersion, - }, }); if (response) { - $.export("summary", `Successfully retrieved bill with id ${response.Bill.Id}`); + $.export("summary", `Successfully retrieved bill with ID ${response.Bill.Id}`); } return response; diff --git a/components/quickbooks/actions/get-customer/get-customer.mjs b/components/quickbooks/actions/get-customer/get-customer.mjs index 62ec29dda7997..5b4dff74892ec 100644 --- a/components/quickbooks/actions/get-customer/get-customer.mjs +++ b/components/quickbooks/actions/get-customer/get-customer.mjs @@ -4,20 +4,15 @@ import quickbooks from "../../quickbooks.app.mjs"; export default { key: "quickbooks-get-customer", name: "Get Customer", - description: "Returns info about a customer. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/customer#read-a-customer)", - version: "0.3.3", + description: "Returns info about a customer. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/customer#read-a-customer)", + version: "0.3.8", type: "action", props: { quickbooks, customerId: { - label: "Customer ID", - type: "string", - description: "Id of the account to get details of.", - }, - minorVersion: { propDefinition: [ quickbooks, - "minorVersion", + "customer", ], }, }, @@ -29,13 +24,10 @@ export default { const response = await this.quickbooks.getCustomer({ $, customerId: this.customerId, - params: { - minorversion: this.minorVersion, - }, }); if (response) { - $.export("summary", `Successfully retrieved customer with id ${response.Customer.Id}`); + $.export("summary", `Successfully retrieved customer with ID ${response.Customer.Id}`); } return response; diff --git a/components/quickbooks/actions/get-invoice/get-invoice.mjs b/components/quickbooks/actions/get-invoice/get-invoice.mjs index 2539713c6572a..5b1dc82329dd1 100644 --- a/components/quickbooks/actions/get-invoice/get-invoice.mjs +++ b/components/quickbooks/actions/get-invoice/get-invoice.mjs @@ -4,21 +4,15 @@ import quickbooks from "../../quickbooks.app.mjs"; export default { key: "quickbooks-get-invoice", name: "Get Invoice", - description: "Returns info about an invoice. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#read-an-invoice)", - version: "0.2.4", + description: "Returns info about an invoice. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#read-an-invoice)", + version: "0.2.9", type: "action", props: { quickbooks, invoiceId: { - label: "Invoice ID", - type: "string", - description: "Id of the invoice to get details of.", - optional: true, - }, - minorVersion: { propDefinition: [ quickbooks, - "minorVersion", + "invoiceId", ], }, }, @@ -30,13 +24,10 @@ export default { const response = await this.quickbooks.getInvoice({ $, invoiceId: this.invoiceId, - params: { - minorversion: this.minorVersion, - }, }); if (response) { - $.export("summary", `Successfully retrieved invoice with id ${response.Invoice.Id}`); + $.export("summary", `Successfully retrieved invoice with ID ${response.Invoice.Id}`); } return response; diff --git a/components/quickbooks/actions/get-my-company/get-my-company.mjs b/components/quickbooks/actions/get-my-company/get-my-company.mjs index 98cf3870d3ae7..88526024ebe70 100644 --- a/components/quickbooks/actions/get-my-company/get-my-company.mjs +++ b/components/quickbooks/actions/get-my-company/get-my-company.mjs @@ -3,20 +3,19 @@ import quickbooks from "../../quickbooks.app.mjs"; export default { key: "quickbooks-get-my-company", name: "Get My Company", - description: "Gets info about a company. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/companyinfo)", - version: "0.1.3", + description: "Gets info about a company. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/companyinfo)", + version: "0.1.8", type: "action", props: { quickbooks, }, async run({ $ }) { - const response = await this.quickbooks.getMyCompany({ $, }); if (response) { - $.export("summary", `Successfully retrieved company with id ${response.CompanyInfo.Id}`); + $.export("summary", `Successfully retrieved company with ID ${response.CompanyInfo.Id}`); } return response; diff --git a/components/quickbooks/actions/get-payment/get-payment.mjs b/components/quickbooks/actions/get-payment/get-payment.mjs new file mode 100644 index 0000000000000..18adb32660e28 --- /dev/null +++ b/components/quickbooks/actions/get-payment/get-payment.mjs @@ -0,0 +1,35 @@ +import { ConfigurationError } from "@pipedream/platform"; +import quickbooks from "../../quickbooks.app.mjs"; + +export default { + key: "quickbooks-get-payment", + name: "Get Payment", + description: "Returns info about a payment. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/payment#read-a-payment)", + version: "0.0.2", + type: "action", + props: { + quickbooks, + paymentId: { + propDefinition: [ + quickbooks, + "paymentId", + ], + }, + }, + async run({ $ }) { + if (!this.paymentId) { + throw new ConfigurationError("Must provide paymentId parameter."); + } + + const response = await this.quickbooks.getPayment({ + $, + paymentId: this.paymentId, + }); + + if (response) { + $.export("summary", `Successfully retrieved payment with ID ${response.Payment.Id}`); + } + + return response; + }, +}; diff --git a/components/quickbooks/actions/get-purchase-order/get-purchase-order.mjs b/components/quickbooks/actions/get-purchase-order/get-purchase-order.mjs index 9ecb52a7781b4..643ef5046dbee 100644 --- a/components/quickbooks/actions/get-purchase-order/get-purchase-order.mjs +++ b/components/quickbooks/actions/get-purchase-order/get-purchase-order.mjs @@ -4,20 +4,15 @@ import quickbooks from "../../quickbooks.app.mjs"; export default { key: "quickbooks-get-purchase-order", name: "Get Purchase Order", - description: "Returns details about a purchase order. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/purchaseorder#read-a-purchase-order)", - version: "0.1.3", + description: "Returns details about a purchase order. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/purchaseorder#read-a-purchase-order)", + version: "0.1.8", type: "action", props: { quickbooks, purchaseOrderId: { - label: "Purchase Order ID", - type: "string", - description: "Id of the purchase order to get details of.", - }, - minorVersion: { propDefinition: [ quickbooks, - "minorVersion", + "purchaseOrderId", ], }, }, @@ -29,13 +24,11 @@ export default { const response = await this.quickbooks.getPurchaseOrder({ $, purchaseOrderId: this.purchaseOrderId, - params: { - minorversion: this.minorVersion, - }, + params: {}, }); if (response) { - $.export("summary", `Successfully retrieved purchase order with id ${response.PurchaseOrder.Id}`); + $.export("summary", `Successfully retrieved purchase order with ID ${response.PurchaseOrder.Id}`); } return response; diff --git a/components/quickbooks/actions/get-purchase/get-purchase.mjs b/components/quickbooks/actions/get-purchase/get-purchase.mjs index 63c3787cdedbc..eb618dfb0d26d 100644 --- a/components/quickbooks/actions/get-purchase/get-purchase.mjs +++ b/components/quickbooks/actions/get-purchase/get-purchase.mjs @@ -4,20 +4,15 @@ import quickbooks from "../../quickbooks.app.mjs"; export default { key: "quickbooks-get-purchase", name: "Get Purchase", - description: "Returns info about a purchase. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/purchase#read-a-purchase)", - version: "0.1.3", + description: "Returns info about a purchase. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/purchase#read-a-purchase)", + version: "0.1.8", type: "action", props: { quickbooks, purchaseId: { - label: "Purchase ID", - type: "string", - description: "Id of the purchase to get details of.", - }, - minorVersion: { propDefinition: [ quickbooks, - "minorVersion", + "purchaseId", ], }, }, @@ -28,14 +23,11 @@ export default { const response = await this.quickbooks.getPurchase({ $, - purchaseId: this.purchaseId, - params: { - minorversion: this.minorVersion, - }, + purchaseId: this.purchaseId?.value ?? this.purchaseId, }); if (response) { - $.export("summary", `Successfully retrieved purchase with id ${response.Purchase.Id}`); + $.export("summary", `Successfully retrieved purchase with ID ${response.Purchase.Id}`); } return response; diff --git a/components/quickbooks/actions/get-sales-receipt/get-sales-receipt.mjs b/components/quickbooks/actions/get-sales-receipt/get-sales-receipt.mjs index 0c9b52660dffd..04f9cfa0a7587 100644 --- a/components/quickbooks/actions/get-sales-receipt/get-sales-receipt.mjs +++ b/components/quickbooks/actions/get-sales-receipt/get-sales-receipt.mjs @@ -4,20 +4,15 @@ import quickbooks from "../../quickbooks.app.mjs"; export default { key: "quickbooks-get-sales-receipt", name: "Get Sales Receipt", - description: "Returns details about a sales receipt. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/salesreceipt#read-a-salesreceipt)", - version: "0.1.3", + description: "Returns details about a sales receipt. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/salesreceipt#read-a-salesreceipt)", + version: "0.1.8", type: "action", props: { quickbooks, salesReceiptId: { - label: "sales Receipt ID", - type: "string", - description: "Id of the sales receipt to get details of.", - }, - minorVersion: { propDefinition: [ quickbooks, - "minorVersion", + "salesReceiptId", ], }, }, @@ -29,13 +24,10 @@ export default { const response = await this.quickbooks.getSalesReceipt({ $, salesReceiptId: this.salesReceiptId, - params: { - minorversion: this.minorVersion, - }, }); if (response) { - $.export("summary", `Successfully retrieved sales receipt with id ${response.SalesReceipt.Id}`); + $.export("summary", `Successfully retrieved sales receipt with ID ${response.SalesReceipt.Id}`); } return response; diff --git a/components/quickbooks/actions/get-time-activity/get-time-activity.mjs b/components/quickbooks/actions/get-time-activity/get-time-activity.mjs index a9a4c46eb9631..42b0d364a0365 100644 --- a/components/quickbooks/actions/get-time-activity/get-time-activity.mjs +++ b/components/quickbooks/actions/get-time-activity/get-time-activity.mjs @@ -4,20 +4,15 @@ import quickbooks from "../../quickbooks.app.mjs"; export default { key: "quickbooks-get-time-activity", name: "Get Time Activity", - description: "Returns info about an activity. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/timeactivity#read-a-timeactivity-object)", - version: "0.1.3", + description: "Returns info about an activity. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/timeactivity#read-a-timeactivity-object)", + version: "0.1.8", type: "action", props: { quickbooks, timeActivityId: { - label: "Time Activity ID", - type: "string", - description: "Id of the time activity object to get details of.", - }, - minorVersion: { propDefinition: [ quickbooks, - "minorVersion", + "timeActivityId", ], }, }, @@ -29,13 +24,11 @@ export default { const response = await this.quickbooks.getTimeActivity({ $, timeActivityId: this.timeActivityId, - params: { - minorversion: this.minorVersion, - }, + params: {}, }); if (response) { - $.export("summary", `Successfully retrieved time activity with id ${response.TimeActivity.Id}`); + $.export("summary", `Successfully retrieved time activity with ID ${response.TimeActivity.Id}`); } return response; diff --git a/components/quickbooks/actions/sandbox-search-invoices/sandbox-search-invoices.mjs b/components/quickbooks/actions/sandbox-search-invoices/sandbox-search-invoices.mjs deleted file mode 100644 index 6926bbcff1334..0000000000000 --- a/components/quickbooks/actions/sandbox-search-invoices/sandbox-search-invoices.mjs +++ /dev/null @@ -1,90 +0,0 @@ -import { ConfigurationError } from "@pipedream/platform"; -import quickbooks from "../../quickbooks.app.mjs"; - -export default { - key: "quickbooks-sandbox-search-invoices", - name: "Search Invoices", - description: "Searches for invoices. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#query-an-invoice)", - version: "0.1.3", - type: "action", - props: { - quickbooks, - includeClause: { - propDefinition: [ - quickbooks, - "includeClause", - ], - optional: false, - }, - maxResults: { - propDefinition: [ - quickbooks, - "maxResults", - ], - }, - orderClause: { - propDefinition: [ - quickbooks, - "orderClause", - ], - }, - startPosition: { - description: "The starting count of the response for pagination.", - label: "Start Position", - optional: true, - type: "string", - }, - whereClause: { - propDefinition: [ - quickbooks, - "whereClause", - ], - optional: false, - }, - minorVersion: { - propDefinition: [ - quickbooks, - "minorVersion", - ], - }, - }, - async run({ $ }) { - if (!this.includeClause || !this.whereClause) { - throw new ConfigurationError("Must provide includeClause, whereClause parameters."); - } - - //Prepares OrderBy clause,start position, max results - // parameters to be used in the request's query parameter. - var orderClause = ""; - if (this.orderClause) { - orderClause = ` ORDERBY ${this.orderClause}`; - } - - var startPosition = ""; - if (this.startPosition) { - startPosition = ` STARTPOSITION ${this.startPosition}`; - } - - var maxResults = ""; - if (this.maxResults) { - maxResults = ` MAXRESULTS ${this.maxResults}` || ""; - } - - //Prepares the request's query parameter - const query = `select ${this.includeClause} from Invoice where ${this.whereClause}${orderClause}${startPosition}${maxResults}`; - - const response = await this.quickbooks.query({ - $, - params: { - minorversion: this.minorVersion, - query, - }, - }); - - if (response) { - $.export("summary", "Successfully retrieved invoices"); - } - - return response; - }, -}; diff --git a/components/quickbooks/actions/search-accounts/search-accounts.mjs b/components/quickbooks/actions/search-accounts/search-accounts.mjs index 8ea78ef309b7d..6fa0ab6f8acbd 100644 --- a/components/quickbooks/actions/search-accounts/search-accounts.mjs +++ b/components/quickbooks/actions/search-accounts/search-accounts.mjs @@ -4,18 +4,11 @@ import quickbooks from "../../quickbooks.app.mjs"; export default { key: "quickbooks-search-accounts", name: "Search Accounts", - description: "Search for accounts. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account#query-an-account)", - version: "0.2.3", + description: "Search for accounts. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account#query-an-account)", + version: "0.2.8", type: "action", props: { quickbooks, - includeClause: { - propDefinition: [ - quickbooks, - "includeClause", - ], - optional: false, - }, maxResults: { propDefinition: [ quickbooks, @@ -41,40 +34,32 @@ export default { ], optional: false, }, - minorVersion: { - propDefinition: [ - quickbooks, - "minorVersion", - ], - }, }, async run({ $ }) { - if (!this.includeClause || !this.whereClause) { - throw new ConfigurationError("Must provide includeClause, whereClause parameters."); + if (!this.whereClause) { + throw new ConfigurationError("Must provide whereClause parameter."); } - var orderClause = ""; + let orderClause = ""; if (this.orderClause) { orderClause = ` ORDERBY ${this.orderClause}`; } - var startPosition = ""; + let startPosition = ""; if (this.startPosition) { startPosition = ` STARTPOSITION ${this.startPosition}`; } - var maxResults = ""; + let maxResults = ""; if (this.maxResults) { - maxResults = ` MAXRESULTS ${this.maxResults}` || ""; + maxResults = ` MAXRESULTS ${this.maxResults}`; } - //Prepares the request's query parameter - const query = `select ${this.includeClause} from Account where ${this.whereClause}${orderClause}${startPosition}${maxResults}`; + const query = `select * from Account where ${this.whereClause}${orderClause}${startPosition}${maxResults}`; const response = await this.quickbooks.query({ $, params: { - minorversion: this.minorVersion, query, }, }); diff --git a/components/quickbooks/actions/search-customers/search-customers.mjs b/components/quickbooks/actions/search-customers/search-customers.mjs index 2c74873099925..282572de8d85d 100644 --- a/components/quickbooks/actions/search-customers/search-customers.mjs +++ b/components/quickbooks/actions/search-customers/search-customers.mjs @@ -4,18 +4,11 @@ import quickbooks from "../../quickbooks.app.mjs"; export default { key: "quickbooks-search-customers", name: "Search Customers", - description: "Searches for customers. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/customer#query-a-customer)", - version: "0.1.3", + description: "Searches for customers. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/customer#query-a-customer)", + version: "0.1.8", type: "action", props: { quickbooks, - includeClause: { - propDefinition: [ - quickbooks, - "includeClause", - ], - optional: false, - }, maxResults: { propDefinition: [ quickbooks, @@ -41,40 +34,32 @@ export default { ], optional: false, }, - minorVersion: { - propDefinition: [ - quickbooks, - "minorVersion", - ], - }, }, async run({ $ }) { - if (!this.includeClause || !this.whereClause) { - throw new ConfigurationError("Must provide includeClause, whereClause parameters."); + if (!this.whereClause) { + throw new ConfigurationError("Must provide whereClause parameter."); } - var orderClause = ""; + let orderClause = ""; if (this.orderClause) { orderClause = ` ORDERBY ${this.orderClause}`; } - var startPosition = ""; + let startPosition = ""; if (this.startPosition) { startPosition = ` STARTPOSITION ${this.startPosition}`; } - var maxResults = ""; + let maxResults = ""; if (this.maxResults) { - maxResults = ` MAXRESULTS ${this.maxResults}` || ""; + maxResults = ` MAXRESULTS ${this.maxResults}`; } - //Prepares the request's query parameter - const query = `select ${this.includeClause} from Customer where ${this.whereClause}${orderClause}${startPosition}${maxResults}`; + const query = `select * from Customer where ${this.whereClause}${orderClause}${startPosition}${maxResults}`; const response = await this.quickbooks.query({ $, params: { - minorversion: this.minorVersion, query, }, }); diff --git a/components/quickbooks/actions/search-invoices/search-invoices.mjs b/components/quickbooks/actions/search-invoices/search-invoices.mjs new file mode 100644 index 0000000000000..1f15fe471dd60 --- /dev/null +++ b/components/quickbooks/actions/search-invoices/search-invoices.mjs @@ -0,0 +1,70 @@ +import { ConfigurationError } from "@pipedream/platform"; +import quickbooks from "../../quickbooks.app.mjs"; + +export default { + key: "quickbooks-search-invoices", + name: "Search Invoices", + description: "Searches for invoices. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#query-an-invoice)", + version: "0.0.2", + type: "action", + props: { + quickbooks, + maxResults: { + propDefinition: [ + quickbooks, + "maxResults", + ], + }, + orderClause: { + propDefinition: [ + quickbooks, + "orderClause", + ], + }, + startPosition: { + description: "The starting count of the response for pagination.", + label: "Start Position", + optional: true, + type: "string", + }, + whereClause: { + propDefinition: [ + quickbooks, + "whereClause", + ], + optional: false, + }, + }, + async run({ $ }) { + if (!this.whereClause) { + throw new ConfigurationError("Must provide whereClause parameter."); + } + + const orderClause = this.orderClause + ? ` ORDERBY ${this.orderClause}` + : ""; + + const startPosition = this.startPosition + ? ` STARTPOSITION ${this.startPosition}` + : ""; + + const maxResults = this.maxResults + ? ` MAXRESULTS ${this.maxResults}` + : ""; + + const query = `select * from Invoice where ${this.whereClause}${orderClause}${startPosition}${maxResults}`; + + const response = await this.quickbooks.query({ + $, + params: { + query, + }, + }); + + if (response) { + $.export("summary", "Successfully retrieved invoices"); + } + + return response; + }, +}; diff --git a/components/quickbooks/actions/search-items/search-items.mjs b/components/quickbooks/actions/search-items/search-items.mjs index 8ee7f13e938e7..c4e4a02f6deb6 100644 --- a/components/quickbooks/actions/search-items/search-items.mjs +++ b/components/quickbooks/actions/search-items/search-items.mjs @@ -4,18 +4,11 @@ import quickbooks from "../../quickbooks.app.mjs"; export default { key: "quickbooks-search-items", name: "Search Items", - description: "Searches for items. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#query-an-item)", - version: "0.1.3", + description: "Searches for items. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#query-an-item)", + version: "0.1.8", type: "action", props: { quickbooks, - includeClause: { - propDefinition: [ - quickbooks, - "includeClause", - ], - optional: false, - }, maxResults: { propDefinition: [ quickbooks, @@ -41,40 +34,29 @@ export default { ], optional: false, }, - minorVersion: { - propDefinition: [ - quickbooks, - "minorVersion", - ], - }, }, async run({ $ }) { - if (!this.includeClause || !this.whereClause) { - throw new ConfigurationError("Must provide includeClause, whereClause parameters."); + if (!this.whereClause) { + throw new ConfigurationError("Must provide whereClause parameter."); } - var orderClause = ""; - if (this.orderClause) { - orderClause = ` ORDERBY ${this.orderClause}`; - } + const orderClause = this.orderClause + ? ` ORDERBY ${this.orderClause}` + : ""; - var startPosition = ""; - if (this.startPosition) { - startPosition = ` STARTPOSITION ${this.startPosition}`; - } + const startPosition = this.startPosition + ? ` STARTPOSITION ${this.startPosition}` + : ""; - var maxResults = ""; - if (this.maxResults) { - maxResults = ` MAXRESULTS ${this.maxResults}` || ""; - } + const maxResults = this.maxResults + ? ` MAXRESULTS ${this.maxResults}` + : ""; - //Prepares the request's query parameter - const query = `select ${this.includeClause} from Item where ${this.whereClause}${orderClause}${startPosition}${maxResults}`; + const query = `select * from Item where ${this.whereClause}${orderClause}${startPosition}${maxResults}`; const response = await this.quickbooks.query({ $, params: { - minorversion: this.minorVersion, query, }, }); diff --git a/components/quickbooks/actions/search-products/search-products.mjs b/components/quickbooks/actions/search-products/search-products.mjs index 45a394510948e..cdb26ce45509f 100644 --- a/components/quickbooks/actions/search-products/search-products.mjs +++ b/components/quickbooks/actions/search-products/search-products.mjs @@ -4,18 +4,11 @@ import quickbooks from "../../quickbooks.app.mjs"; export default { key: "quickbooks-search-products", name: "Search Products", - description: "Search for products. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#query-an-item)", - version: "0.1.3", + description: "Search for products. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#query-an-item)", + version: "0.1.8", type: "action", props: { quickbooks, - includeClause: { - propDefinition: [ - quickbooks, - "includeClause", - ], - optional: false, - }, maxResults: { propDefinition: [ quickbooks, @@ -41,40 +34,29 @@ export default { ], optional: false, }, - minorVersion: { - propDefinition: [ - quickbooks, - "minorVersion", - ], - }, }, async run({ $ }) { - if (!this.includeClause || !this.whereClause) { - throw new ConfigurationError("Must provide includeClause, whereClause parameters."); + if (!this.whereClause) { + throw new ConfigurationError("Must provide whereClause parameter."); } - var orderClause = ""; - if (this.orderClause) { - orderClause = ` ORDERBY ${this.orderClause}`; - } + const orderClause = this.orderClause + ? ` ORDERBY ${this.orderClause}` + : ""; - var startPosition = ""; - if (this.startPosition) { - startPosition = ` STARTPOSITION ${this.startPosition}`; - } + const startPosition = this.startPosition + ? ` STARTPOSITION ${this.startPosition}` + : ""; - var maxResults = ""; - if (this.maxResults) { - maxResults = ` MAXRESULTS ${this.maxResults}` || ""; - } + const maxResults = this.maxResults + ? ` MAXRESULTS ${this.maxResults}` + : ""; - //Prepares the request's query parameter - const query = `select ${this.includeClause} from Item where Type = 'Inventory' and ${this.whereClause}${orderClause}${startPosition}${maxResults}`; + const query = `select * from Item where Type = 'Inventory' and ${this.whereClause}${orderClause}${startPosition}${maxResults}`; const response = await this.quickbooks.query({ $, params: { - minorversion: this.minorVersion, query, }, }); diff --git a/components/quickbooks/actions/search-purchases/search-purchases.mjs b/components/quickbooks/actions/search-purchases/search-purchases.mjs index 4ba962a8287b8..e6d8a645d03f8 100644 --- a/components/quickbooks/actions/search-purchases/search-purchases.mjs +++ b/components/quickbooks/actions/search-purchases/search-purchases.mjs @@ -4,7 +4,7 @@ export default { key: "quickbooks-search-purchases", name: "Search Purchases", description: "Searches for purchases. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/purchase#query-a-purchase)", - version: "0.0.1", + version: "0.0.6", type: "action", props: { quickbooks, @@ -33,40 +33,29 @@ export default { ], optional: true, }, - minorVersion: { - propDefinition: [ - quickbooks, - "minorVersion", - ], - }, }, async run({ $ }) { - let whereClause = ""; - if (this.whereClause) { - whereClause = ` WHERE ${this.whereClause}`; - } + const whereClause = this.whereClause + ? ` WHERE ${this.whereClause}` + : ""; - var orderClause = ""; - if (this.orderClause) { - orderClause = ` ORDERBY ${this.orderClause}`; - } + const orderClause = this.orderClause + ? ` ORDERBY ${this.orderClause}` + : ""; - var startPosition = ""; - if (this.startPosition) { - startPosition = ` STARTPOSITION ${this.startPosition}`; - } + const startPosition = this.startPosition + ? ` STARTPOSITION ${this.startPosition}` + : ""; + + const maxResults = this.maxResults + ? ` MAXRESULTS ${this.maxResults}` + : ""; - var maxResults = ""; - if (this.maxResults) { - maxResults = ` MAXRESULTS ${this.maxResults}` || ""; - } - //Prepares the request's query parameter const query = `select * from Purchase ${whereClause}${orderClause}${startPosition}${maxResults}`; const response = await this.quickbooks.query({ $, params: { - minorversion: this.minorVersion, query, }, }); diff --git a/components/quickbooks/actions/search-query/search-query.mjs b/components/quickbooks/actions/search-query/search-query.mjs index 3cdea5ae78456..f111541f82b2d 100644 --- a/components/quickbooks/actions/search-query/search-query.mjs +++ b/components/quickbooks/actions/search-query/search-query.mjs @@ -4,8 +4,8 @@ import quickbooks from "../../quickbooks.app.mjs"; export default { key: "quickbooks-search-query", name: "Search Query", - description: "Performs a search query against a Quickbooks entity. [See docs here](https://developer.intuit.com/app/develophttps://developer.intuit.com/app/developer/qbo/docs/develop/explore-the-quickbooks-online-api/data-queries)", - version: "0.1.3", + description: "Performs a search query against a Quickbooks entity. [See the documentation](https://developer.intuit.com/app/develophttps://developer.intuit.com/app/developer/qbo/docs/develop/explore-the-quickbooks-online-api/data-queries)", + version: "0.1.8", type: "action", props: { quickbooks, @@ -14,12 +14,6 @@ export default { type: "string", description: "A search query against a Quickbooks entity. See query language syntax, limitations, and other specifications on [Data queries](https://developer.intuit.com/app/developer/qbo/docs/develop/explore-the-quickbooks-online-api/data-queries)", }, - minorVersion: { - propDefinition: [ - quickbooks, - "minorVersion", - ], - }, }, async run({ $ }) { if (!this.searchQuery) { @@ -29,7 +23,6 @@ export default { const response = await this.quickbooks.query({ $, params: { - minorversion: this.minorVersion, query: this.searchQuery, }, }); @@ -37,5 +30,6 @@ export default { if (response) { $.export("summary", "Successfully retrieved query results"); } + return response; }, }; diff --git a/components/quickbooks/actions/search-services/search-services.mjs b/components/quickbooks/actions/search-services/search-services.mjs new file mode 100644 index 0000000000000..aeb790b9ff8dc --- /dev/null +++ b/components/quickbooks/actions/search-services/search-services.mjs @@ -0,0 +1,70 @@ +import quickbooks from "../../quickbooks.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "quickbooks-search-services", + name: "Search Services", + description: "Search for services. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#query-an-item)", + version: "0.0.2", + type: "action", + props: { + quickbooks, + whereClause: { + propDefinition: [ + quickbooks, + "whereClause", + ], + optional: false, + }, + orderClause: { + propDefinition: [ + quickbooks, + "orderClause", + ], + }, + startPosition: { + description: "The starting count of the response for pagination.", + label: "Start Position", + optional: true, + type: "string", + }, + maxResults: { + propDefinition: [ + quickbooks, + "maxResults", + ], + }, + }, + async run({ $ }) { + if (!this.whereClause) { + throw new ConfigurationError("Must provide whereClause parameter."); + } + + const orderClause = this.orderClause + ? ` ORDERBY ${this.orderClause}` + : ""; + + const startPosition = this.startPosition + ? ` STARTPOSITION ${this.startPosition}` + : ""; + + const maxResults = this.maxResults + ? ` MAXRESULTS ${this.maxResults}` + : ""; + + const query = `select * from Item where Type = 'Service' and ${this.whereClause}${orderClause}${startPosition}${maxResults}`; + + const response = await this.quickbooks.query({ + $, + params: { + query, + }, + }); + + if (response) { + $.export("summary", "Successfully retrieved services"); + } + + return response; + }, +}; diff --git a/components/quickbooks/actions/search-time-activities/search-time-activities.mjs b/components/quickbooks/actions/search-time-activities/search-time-activities.mjs index 781dbbdf77ac6..206fdbac32fa4 100644 --- a/components/quickbooks/actions/search-time-activities/search-time-activities.mjs +++ b/components/quickbooks/actions/search-time-activities/search-time-activities.mjs @@ -4,18 +4,11 @@ import quickbooks from "../../quickbooks.app.mjs"; export default { key: "quickbooks-search-time-activities", name: "Search Time Activities", - description: "Searches for time activities. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/timeactivity#query-a-timeactivity-object)", - version: "0.1.3", + description: "Searches for time activities. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/timeactivity#query-a-timeactivity-object)", + version: "0.1.8", type: "action", props: { quickbooks, - includeClause: { - propDefinition: [ - quickbooks, - "includeClause", - ], - optional: false, - }, maxResults: { propDefinition: [ quickbooks, @@ -41,40 +34,29 @@ export default { ], optional: false, }, - minorVersion: { - propDefinition: [ - quickbooks, - "minorVersion", - ], - }, }, async run({ $ }) { - if (!this.includeClause || !this.whereClause) { - throw new ConfigurationError("Must provide includeClause, whereClause parameters."); + if (!this.whereClause) { + throw new ConfigurationError("Must provide whereClause parameter."); } - var orderClause = ""; - if (this.orderClause) { - orderClause = ` ORDERBY ${this.orderClause}`; - } + const orderClause = this.orderClause + ? ` ORDERBY ${this.orderClause}` + : ""; - var startPosition = ""; - if (this.startPosition) { - startPosition = ` STARTPOSITION ${this.startPosition}`; - } + const startPosition = this.startPosition + ? ` STARTPOSITION ${this.startPosition}` + : ""; - var maxResults = ""; - if (this.maxResults) { - maxResults = ` MAXRESULTS ${this.maxResults}` || ""; - } + const maxResults = this.maxResults + ? ` MAXRESULTS ${this.maxResults}` + : ""; - //Prepares the request's query parameter - const query = `select ${this.includeClause} from TimeActivity where ${this.whereClause}${orderClause}${startPosition}${maxResults}`; + const query = `select * from TimeActivity where ${this.whereClause}${orderClause}${startPosition}${maxResults}`; const response = await this.quickbooks.query({ $, params: { - minorversion: this.minorVersion, query, }, }); diff --git a/components/quickbooks/actions/search-vendors/search-vendors.mjs b/components/quickbooks/actions/search-vendors/search-vendors.mjs index 2955c6e355754..4e682be2f4ebd 100644 --- a/components/quickbooks/actions/search-vendors/search-vendors.mjs +++ b/components/quickbooks/actions/search-vendors/search-vendors.mjs @@ -4,18 +4,11 @@ import quickbooks from "../../quickbooks.app.mjs"; export default { key: "quickbooks-search-vendors", name: "Search Vendors", - description: "Searches for vendors. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/vendor#query-a-vendor)", - version: "0.1.3", + description: "Searches for vendors. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/vendor#query-a-vendor)", + version: "0.1.8", type: "action", props: { quickbooks, - includeClause: { - propDefinition: [ - quickbooks, - "includeClause", - ], - optional: false, - }, maxResults: { propDefinition: [ quickbooks, @@ -41,40 +34,29 @@ export default { ], optional: false, }, - minorVersion: { - propDefinition: [ - quickbooks, - "minorVersion", - ], - }, }, async run({ $ }) { - if (!this.includeClause || !this.whereClause) { + if (!this.whereClause) { throw new ConfigurationError("Must provide includeClause, whereClause parameters."); } - var orderClause = ""; - if (this.orderClause) { - orderClause = ` ORDERBY ${this.orderClause}`; - } + const orderClause = this.orderClause + ? ` ORDERBY ${this.orderClause}` + : ""; - var startPosition = ""; - if (this.startPosition) { - startPosition = ` STARTPOSITION ${this.startPosition}`; - } + const startPosition = this.startPosition + ? ` STARTPOSITION ${this.startPosition}` + : ""; - var maxResults = ""; - if (this.maxResults) { - maxResults = ` MAXRESULTS ${this.maxResults}` || ""; - } + const maxResults = this.maxResults + ? ` MAXRESULTS ${this.maxResults}` + : ""; - //Prepares the request's query parameter - const query = `select ${this.includeClause} from Vendor where ${this.whereClause}${orderClause}${startPosition}${maxResults}`; + const query = `select * from Vendor where ${this.whereClause}${orderClause}${startPosition}${maxResults}`; const response = await this.quickbooks.query({ $, params: { - minorversion: this.minorVersion, query, }, }); diff --git a/components/quickbooks/actions/sparse-update-invoice/sparse-update-invoice.mjs b/components/quickbooks/actions/sparse-update-invoice/sparse-update-invoice.mjs index eb22d8e55578d..c4f5372e8d13c 100644 --- a/components/quickbooks/actions/sparse-update-invoice/sparse-update-invoice.mjs +++ b/components/quickbooks/actions/sparse-update-invoice/sparse-update-invoice.mjs @@ -1,12 +1,12 @@ -import { ConfigurationError } from "@pipedream/platform"; -import { parseOne } from "../../common/utils.mjs"; +import { parseLineItems } from "../../common/utils.mjs"; import quickbooks from "../../quickbooks.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; export default { key: "quickbooks-sparse-update-invoice", name: "Sparse Update Invoice", - description: "Sparse updating provides the ability to update a subset of properties for a given object; only elements specified in the request are updated. Missing elements are left untouched. The ID of the object to update is specified in the request body.​ [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#sparse-update-an-invoice)", - version: "0.1.1", + description: "Sparse updating provides the ability to update a subset of properties for a given object; only elements specified in the request are updated. Missing elements are left untouched. The ID of the object to update is specified in the request body.​ [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#sparse-update-an-invoice)", + version: "0.1.6", type: "action", props: { quickbooks, @@ -16,12 +16,6 @@ export default { "invoiceId", ], }, - lineItems: { - propDefinition: [ - quickbooks, - "lineItems", - ], - }, customer: { propDefinition: [ quickbooks, @@ -34,42 +28,110 @@ export default { "currency", ], }, - minorVersion: { + lineItemsAsObjects: { propDefinition: [ quickbooks, - "minorVersion", + "lineItemsAsObjects", ], + reloadProps: true, }, }, - async run({ $ }) { - try { - this.lineItems = this.lineItems.map((lineItem) => typeof lineItem === "string" - ? JSON.parse(lineItem) - : lineItem); - } catch (error) { - throw new ConfigurationError(`We got an error trying to parse the Line Items prop. Error: ${error}`); + async additionalProps() { + const props = {}; + if (this.lineItemsAsObjects) { + props.lineItems = { + type: "string[]", + label: "Line Items", + description: "Line items of an invoice. Set DetailType to `SalesItemLineDetail`, `GroupLineDetail`, `DescriptionOnly`, `DiscountLineDetail`, or `SubTotalLineDetail`. Example: `{ \"DetailType\": \"SalesItemLineDetail\", \"Amount\": 100.0, \"SalesItemLineDetail\": { \"ItemRef\": { \"name\": \"Services\", \"value\": \"1\" } } }`", + }; + return props; + } + props.numLineItems = { + type: "integer", + label: "Number of Line Items", + description: "The number of line items to enter", + reloadProps: true, + }; + if (!this.numLineItems) { + return props; } + for (let i = 1; i <= this.numLineItems; i++) { + props[`item_${i}`] = { + type: "string", + label: `Line ${i} - Item ID`, + options: async ({ page }) => { + return this.quickbooks.getPropOptions({ + page, + resource: "Item", + mapper: ({ + Id: value, Name: label, + }) => ({ + value, + label, + }), + }); + }, + }; + props[`amount_${i}`] = { + type: "string", + label: `Line ${i} - Amount`, + }; + } + return props; + }, + methods: { + buildLineItems() { + const lineItems = []; + for (let i = 1; i <= this.numLineItems; i++) { + lineItems.push({ + DetailType: "SalesItemLineDetail", + Amount: this[`amount_${i}`], + SalesItemLineDetail: { + ItemRef: { + value: this[`item_${i}`], + }, + }, + }); + } + return lineItems; + }, + }, + async run({ $ }) { + const lines = this.lineItemsAsObjects + ? parseLineItems(this.lineItems) + : this.buildLineItems(); + + lines.forEach((line) => { + if (line.DetailType !== "SalesItemLineDetail" && line.DetailType !== "GroupLineDetail" && line.DetailType !== "DescriptionOnly" && line.DetailType !== "DiscountLineDetail" && line.DetailType !== "SubTotalLineDetail") { + throw new ConfigurationError("Line Item DetailType must be `SalesItemLineDetail`, `GroupLineDetail`, `DescriptionOnly`, `DiscountLineDetail`, or `SubTotalLineDetail`"); + } + }); - const { Invoice } = await this.quickbooks.getInvoice({ + const { Invoice: invoice } = await this.quickbooks.getInvoice({ $, invoiceId: this.invoiceId, }); - if (this.lineItems.length) Invoice.Line?.push(...this.lineItems); - - Invoice.CurrencyRef = parseOne(this.currency); - Invoice.CustomerRef = parseOne(this.customer); + if (lines?.length) invoice.Line?.push(...lines); const response = await this.quickbooks.sparseUpdateInvoice({ $, - data: Invoice, - params: { - minorversion: this.minorVersion, + data: { + sparse: true, + Id: this.invoiceId, + SyncToken: invoice.SyncToken, + CurrencyRef: this.currencyRefValue && { + value: this.currencyRefValue, + }, + CustomerRef: { + value: this.customer, + }, + Line: invoice.Line, }, }); if (response) { - $.export("summary", `Successfully updated invoice with id ${response.Invoice.Id}`); + $.export("summary", `Successfully updated invoice with ID ${response.Invoice.Id}`); } return response; diff --git a/components/quickbooks/actions/update-customer/update-customer.mjs b/components/quickbooks/actions/update-customer/update-customer.mjs index cf4d2d5fc5352..aa2c4572fcc1b 100644 --- a/components/quickbooks/actions/update-customer/update-customer.mjs +++ b/components/quickbooks/actions/update-customer/update-customer.mjs @@ -4,11 +4,17 @@ import quickbooks from "../../quickbooks.app.mjs"; export default { key: "quickbooks-update-customer", name: "Update Customer", - description: "Updates a customer. [See docs here](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/customer#full-update-a-customer)", - version: "0.1.3", + description: "Updates a customer. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/customer#full-update-a-customer)", + version: "0.1.8", type: "action", props: { quickbooks, + customerId: { + propDefinition: [ + quickbooks, + "customer", + ], + }, displayName: { propDefinition: [ quickbooks, @@ -48,20 +54,14 @@ export default { currencyRefValue: { propDefinition: [ quickbooks, - "currencyRefValue", - ], - }, - currencyRefName: { - propDefinition: [ - quickbooks, - "currencyRefName", + "currency", ], }, active: { description: "If true, this entity is currently enabled for use by QuickBooks. If there is an amount in `Customer.Balance` when setting this Customer object to inactive through the QuickBooks UI, a CreditMemo balancing transaction is created for the amount.", label: "Active", optional: true, - type: "string", + type: "boolean", }, alternatePhoneFreeFormNumber: { description: "Specifies the alternate phone number in free form.", @@ -69,29 +69,14 @@ export default { optional: true, type: "string", }, - arAccountRefName: { - description: "Name of the accounts receivable account to be used for this customer. Each customer must have his own AR account. Applicable for France companies, only. Available when endpoint is evoked with the minorversion=3 query parameter. Query the Account name list resource to determine the appropriate Account object for this reference, where Account.AccountType=Accounts Receivable. Use `Account.Name` from that object for `ARAccountRef.name`.", - label: "Ar Account Ref Name", - optional: true, - type: "string", - }, arAccountRefValue: { - description: "Id of the accounts receivable account to be used for this customer. Each customer must have his own AR account. Applicable for France companies, only. Available when endpoint is evoked with the minorversion=3 query parameter. Query the Account name list resource to determine the appropriate Account object for this reference, where Account.AccountType=Accounts Receivable. Use `Account.Id` from that object for `ARAccountRef.value`.", - label: "Ar Account Ref Value", - optional: true, - type: "string", - }, - balance: { - description: "Specifies the open balance amount or the amount unpaid by the customer. For the create operation, this represents the opening balance for the customer. When returned in response to the query request it represents the current open balance (unpaid amount) for that customer. Write-on-create.", - label: "Balance", - optional: true, - type: "string", - }, - balanceWithJob: { - description: "Cumulative open balance amount for the Customer (or Job) and all its sub-jobs. Cannot be written to QuickBooks.", - label: "Balance With Job", - optional: true, + description: "ID of the accounts receivable account to be used for this customer. Each customer must have his own AR account. Applicable for France companies, only. Available when endpoint is evoked with the minorversion=3 query parameter. Query the Account name list resource to determine the appropriate Account object for this reference, where Account.AccountType=Accounts Receivable. Use `Account.Id` from that object for `ARAccountRef.value`.", + label: "AR Account Ref Value", type: "string", + propDefinition: [ + quickbooks, + "accountIds", + ], }, billAddrCity: { description: "City name for the billing address.", @@ -111,12 +96,6 @@ export default { optional: true, type: "string", }, - billAddrId: { - description: "Unique identifier of the QuickBooks object for the billing address, used for modifying the address. \nThe BillAddr object represents the default billing address. If a physical address is updated from within the transaction object, the QuickBooks Online API flows individual address components differently into the Line elements of the transaction response then when the transaction was first created:\n* `Line1` and `Line2` elements are populated with the customer name and company name.\n* Original `Line1` through `Line5` contents, `City`, `SubDivisionCode`, and `PostalCode` flow into `Line3` through `Line5` as a free format strings.", - label: "Bill Addr Id", - optional: true, - type: "string", - }, billAddrLate: { description: "Latitude coordinate of Geocode (Geospacial Entity Object Code). `INVALID` is returned for invalid addresses.", label: "Bill Addr Late", @@ -183,25 +162,14 @@ export default { optional: true, type: "string", }, - customerId: { - description: "Unique identifier for the customer object to be updated.", - label: "Customer Id", - type: "string", - }, customerTypeRefValue: { - description: "Id referencing to the customer type assigned to a customer. This field is only returned if the customer is assigned a customer type.", - label: "Customer Type Ref Value", - optional: true, - type: "string", - }, - defaultTaxCodeName: { - description: "Id of the default tax code associated with this Customer object. Reference is valid if `Customer.Taxable` is set to true; otherwise, it is ignored. If automated sales tax is enabled (`Preferences.TaxPrefs.PartnerTaxEnabled` is set to true) the default tax code is set by the system and can not be overridden. Query the `TaxCode` name list resource to determine the appropriate `TaxCode` object for this reference. Use `TaxCode.Name` from that object for `DefaultTaxCodeRef.name`.", - label: "Default Tax Code Name", - optional: true, - type: "string", + propDefinition: [ + quickbooks, + "customerType", + ], }, defaultTaxCodeValue: { - description: "Id of the default tax code associated with this Customer object. Reference is valid if `Customer.Taxable` is set to true; otherwise, it is ignored. If automated sales tax is enabled (`Preferences.TaxPrefs.PartnerTaxEnabled` is set to true) the default tax code is set by the system and can not be overridden. Query the `TaxCode` name list resource to determine the appropriate `TaxCode` object for this reference. Use `TaxCode.Id` from that object for `DefaultTaxCodeRef.value`.", + description: "ID of the default tax code associated with this Customer object. Reference is valid if `Customer.Taxable` is set to true; otherwise, it is ignored. If automated sales tax is enabled (`Preferences.TaxPrefs.PartnerTaxEnabled` is set to true) the default tax code is set by the system and can not be overridden. Query the `TaxCode` name list resource to determine the appropriate `TaxCode` object for this reference. Use `TaxCode.Id` from that object for `DefaultTaxCodeRef.value`.", label: "Default Tax Code Value", optional: true, type: "string", @@ -233,12 +201,6 @@ export default { ], type: "string", }, - isProject: { - description: "If true, indicates this is a Project.", - label: "Is Project", - optional: true, - type: "boolean", - }, job: { description: "If true, this is a Job or sub-customer. If false or null, this is a top level customer, not a Job or sub-customer.", label: "Job", @@ -257,35 +219,11 @@ export default { optional: true, type: "string", }, - openBalanceDate: { - description: "Date of the Open Balance for the create operation. Write-on-create.", - label: "Open Balance Date", - optional: true, - type: "string", - }, - parentRefName: { - description: "Name referencing to a Customer object that is the immediate parent of the Sub-Customer/Job in the hierarchical Customer:Job list. Required for the create operation if this object is a sub-customer or Job. Query the Customer name list resource to determine the appropriate Customer object for this reference. Use `Customer.Name` from that object for `ParentRef.name`.", - label: "Parent Ref Name", - optional: true, - type: "string", - }, - parentRefValue: { - description: "Id referencing to a Customer object that is the immediate parent of the Sub-Customer/Job in the hierarchical Customer:Job list. Required for the create operation if this object is a sub-customer or Job. Query the Customer name list resource to determine the appropriate Customer object for this reference. Use `Customer.Id` from that object for `ParentRef.value`.", - label: "Parent Ref Value", - optional: true, - type: "string", - }, - paymentMethodRefName: { - description: "Id referencing a PaymentMethod object associated with this Customer object. Query the PaymentMethod name list resource to determine the appropriate PaymentMethod object for this reference. Use `PaymentMethod.Name` from that object for `PaymentMethodRef.name`.", - label: "Payment Method Ref Name", - optional: true, - type: "string", - }, paymentMethodRefValue: { - description: "Id referencing a PaymentMethod object associated with this Customer object. Query the PaymentMethod name list resource to determine the appropriate PaymentMethod object for this reference. Use `PaymentMethod.Id` from that object for `PaymentMethodRef.value`.", - label: "Payment Method Ref Value", - optional: true, - type: "string", + propDefinition: [ + quickbooks, + "paymentMethod", + ], }, preferredDeliveryMethod: { description: "Preferred delivery method. Values are `Print`, `Email`, or `None`.", @@ -328,17 +266,13 @@ export default { optional: true, type: "string", }, - saleTermRefName: { - description: "Name of a SalesTerm object associated with this Customer object. Query the Term name list resource to determine the appropriate Term object for this reference. Use `Term.Name` from that object for `SalesTermRef.name`.", - label: "Sale Term Ref Name", - optional: true, - type: "string", - }, saleTermRefValue: { - description: "Id of a SalesTerm object associated with this Customer object. Query the Term name list resource to determine the appropriate Term object for this reference. Use `Term.Id` from that object for `SalesTermRef.value`.", - label: "Sale Term Ref Value", - optional: true, + description: "ID of a SalesTerm object associated with this Customer object. Query the Term name list resource to determine the appropriate Term object for this reference. Use `Term.Id` from that object for `SalesTermRef.value`.", type: "string", + propDefinition: [ + quickbooks, + "termIds", + ], }, secondaryTaxIdentifier: { description: "Also called UTR No. in ( UK ) , CST Reg No. ( IN ) also represents the tax registration number of the Person or Organization. This value is masked in responses, exposing only last five characters. For example, the ID of `123-45-6789` is returned as `XXXXXX56789`", @@ -418,21 +352,11 @@ export default { optional: true, type: "string", }, - sparseUpdate: { - description: "When set to `true`, sparse updating provides the ability to update a subset of properties for a given object; only elements specified in the request are updated. Missing elements are left untouched.", - label: "Sparse Update", - type: "string", - }, - syncToken: { - description: "Version number of the object. It is used to lock an object for use by one app at a time. As soon as an application modifies an object, its SyncToken is incremented. Attempts to modify an object specifying an older SyncToken fails. Only the latest version of the object is maintained by QuickBooks Online.", - label: "Sync Token", - type: "string", - }, taxable: { description: "If true, transactions for this customer are taxable. Default behavior with minor version 10 and above: true, if `DefaultTaxCodeRef` is defined or false if `TaxExemptionReasonId` is set.", label: "Taxable", optional: true, - type: "string", + type: "boolean", }, taxExemptionReasonId: { label: "Tax Exemption Reason Id", @@ -463,28 +387,54 @@ export default { optional: true, type: "string", }, - minorVersion: { - propDefinition: [ - quickbooks, - "minorVersion", - ], + }, + methods: { + async getSyncToken($) { + const { Customer: customer } = await this.quickbooks.getCustomer({ + $, + customerId: this.customerId, + }); + return customer.SyncToken; }, }, async run({ $ }) { - //See Quickbooks API docs at: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/customer#full-update-a-customer - if ( !this.displayName && (!this.title && !this.givenName && !this.middleName && !this.familyName && !this.suffix) || - !this.customerId || !this.syncToken || this.sparseUpdate === undefined + !this.customerId ) { - throw new ConfigurationError("Must provide displayName or at least one of title, givenName, middleName, familyName, or suffix, and customerId, syncToken parameters."); + throw new ConfigurationError("Must provide displayName or at least one of title, givenName, middleName, familyName, or suffix, and customerId parameters."); } + const hasBillingAddress = this.billAddrPostalCode + || this.billAddrCity + || this.billAddrCountry + || this.billAddrLine5 + || this.billAddrLine4 + || this.billAddrLine3 + || this.billAddrLine2 + || this.billAddrLine1 + || this.billAddrLate + || this.billAddrLong + || this.billAddrCountrySubDivisionCode; + + const hasShippingAddress = this.shipAddrId + || this.shipAddrPostalCode + || this.shipAddrCity + || this.shipAddrCountry + || this.shipAddrLine5 + || this.shipAddrLine4 + || this.shipAddrLine3 + || this.shipAddrLine2 + || this.shipAddrLine1 + || this.shipAddrLate + || this.shipAddrLong + || this.shipAddrCountrySubDivisionCode; + const response = await this.quickbooks.updateCustomer({ $, data: { - sparse: this.sparseUpdate, + sparse: true, Id: this.customerId, DisplayName: this.displayName, Suffix: this.suffix, @@ -492,58 +442,49 @@ export default { MiddleName: this.middleName, FamilyName: this.familyName, GivenName: this.givenName, - SyncToken: this.syncToken, - PrimaryEmailAddr: this.primaryEmailAddr, + SyncToken: await this.getSyncToken($), + PrimaryEmailAddr: this.primaryEmailAddr && { + Address: this.primaryEmailAddr, + }, ResaleNum: this.resaleNum, SecondaryTaxIdentifier: this.secondaryTaxIdentifier, - ARAccountRef: { + ARAccountRef: this.arAccountRefValue && { value: this.arAccountRefValue, - name: this.arAccountRefName, }, - DefaultTaxCodeRef: { + DefaultTaxCodeRef: this.defaultTaxCodeValue && { value: this.defaultTaxCodeValue, - name: this.defaultTaxCodeName, }, PreferredDeliveryMethod: this.preferredDeliveryMethod, GSTIN: this.GSTIN, - SalesTermRef: { + SalesTermRef: this.saleTermRefValue && { value: this.saleTermRefValue, - name: this.saleRermRefName, }, - CustomerTypeRef: { - value: this.customerRypeRefValue, + CustomerTypeRef: this.customerTypeRefValue && { + value: this.customerTypeRefValue, }, - Fax: { + Fax: this.faxFreeFormNumber && { FreeFormNumber: this.faxFreeFormNumber, }, BusinessNumber: this.businessNumber, BillWithParent: this.billWithParent, - CurrencyRef: { + CurrencyRef: this.currencyRefValue && { value: this.currencyRefValue, - name: this.currencyRefName, }, - Mobile: { + Mobile: this.mobileFreeFormNumber && { FreeFormNumber: this.mobileFreeFormNumber, }, Job: this.job, - BalanceWithJobs: this.balanceWithJob, - PrimaryPhone: { + PrimaryPhone: this.primaryPhoneFreeFormNumber && { FreeFormNumber: this.primaryPhoneFreeFormNumber, }, - OpenBalanceDate: this.openBalanceDate, Taxable: this.taxable, - AlternatePhone: { + AlternatePhone: this.alternatePhoneFreeFormNumber && { FreeFormNumber: this.alternatePhoneFreeFormNumber, }, - ParentRef: { - value: this.parentRefValue, - name: this.parentRefName, - }, Notes: this.notes, WebAddr: this.webAddr, Active: this.active, - Balance: this.balance, - ShipAddr: { + ShipAddr: hasShippingAddress && { Id: this.shipAddrId, PostalCode: this.shipAddrPostalCode, City: this.shipAddrCity, @@ -557,17 +498,14 @@ export default { Long: this.shipAddrLong, CountrySubDivisionCode: this.shipAddrCountrySubDivisionCode, }, - PaymentMethodRef: { + PaymentMethodRef: this.paymentMethodRefValue && { value: this.paymentMethodRefValue, - name: this.paymentMethodRefName, }, - IsProject: this.isProject, CompanyName: this.companyName, PrimaryTaxIdentifier: this.primaryTaxIdentifier, GSTRegistrationType: this.gstRegistrationType, PrintOnCheckName: this.printOnCheckName, - BillAddr: { - Id: this.billAddrId, + BillAddr: hasBillingAddress && { PostalCode: this.billAddrPostalCode, City: this.billAddrCity, Country: this.billAddrCountry, @@ -582,13 +520,10 @@ export default { }, TaxExemptionReasonId: this.taxExemptionReasonId, }, - params: { - minorversion: this.minorVersion, - }, }); if (response) { - $.export("summary", `Successfully updated customer with id ${this.customerId}`); + $.export("summary", `Successfully updated customer with ID ${this.customerId}`); } return response; diff --git a/components/quickbooks/actions/update-item/update-item.mjs b/components/quickbooks/actions/update-item/update-item.mjs new file mode 100644 index 0000000000000..6aeb093cd91f0 --- /dev/null +++ b/components/quickbooks/actions/update-item/update-item.mjs @@ -0,0 +1,297 @@ +import quickbooks from "../../quickbooks.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "quickbooks-update-item", + name: "Update Item", + description: "Updates an item. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#full-update-an-item)", + version: "0.0.2", + type: "action", + props: { + quickbooks, + itemId: { + propDefinition: [ + quickbooks, + "itemId", + ], + }, + name: { + type: "string", + label: "Name", + description: "Name of the item. This value must be unique.", + }, + trackQtyOnHand: { + type: "boolean", + label: "Track Quantity on Hand", + description: "True if there is quantity on hand to be tracked. Once this value is true, it cannot be updated to false. Applicable for items of type `Inventory`. Not applicable for `Service` or `NonInventory` item types.", + optional: true, + }, + qtyOnHand: { + type: "string", + label: "Quantity on Hand", + description: "Current quantity of the `Inventory` items available for sale. Not used for `Service` or `NonInventory` type items. Required for `Inventory` type items.", + optional: true, + }, + incomeAccountRefValue: { + type: "string", + label: "Income Account Ref Value", + description: "Reference to the posting account, that is, the account that records the proceeds from the sale of this item. Must be an account with account type of `Sales of Product Income`. Query the Account name list resource to determine the appropriate Account object for this reference. Use `Account.Id` from that object for `IncomeAccountRef.value`. See specifications for the IncomeAccountRef parameters in the [Create an item page](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#create-an-item).", + propDefinition: [ + quickbooks, + "accountIds", + ], + }, + type: { + type: "string", + label: "Type", + description: "Classification that specifies the use of this item. See the description at the top of the [Item entity page](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item) for details about supported item types. See specifications for the type parameter in the [Create an item page](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#create-an-item).", + optional: true, + options: [ + "Inventory", + "Group", + "Service", + "NonInventory", + ], + }, + assetAccountRefValue: { + type: "string", + label: "Asset Account Ref Value", + description: "Required for Inventory item types. Reference to the Inventory Asset account that tracks the current value of the inventory. If the same account is used for all inventory items, the current balance of this account will represent the current total value of the inventory. Must be an account with account type of `Other Current Asset`. Query the Account name list resource to determine the appropriate Account object for this reference. Use `Account.Id` from that object for `AssetAccountRef.value`.", + propDefinition: [ + quickbooks, + "accountIds", + ], + }, + invStartDate: { + type: "string", + label: "Inventory Start Date", + description: "Date of opening balance for the inventory transaction. Required when creating an `Item.Type=Inventory`. Required for `Inventory` item types.", + optional: true, + }, + expenseAccountRefValue: { + type: "string", + label: "Expense Account Ref Value", + description: "Reference to the expense account used to pay the vendor for this item. Must be an account with account type of `Cost of Goods Sold`. Query the Account name list resource to determine the appropriate Account object for this reference. Use `Account.Id` from that object for `ExpenseAccountRef.value`. See specifications for the ExpenseAccountRef parameters in the [Create an item page](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#create-an-item).", + propDefinition: [ + quickbooks, + "accountIds", + ], + }, + sku: { + type: "string", + label: "Sku", + description: "The stock keeping unit (SKU) for this Item. This is a company-defined identifier for an item or product used in tracking inventory.", + optional: true, + }, + salesTaxIncluded: { + type: "boolean", + label: "Sales Tax Included", + description: "True if the sales tax is included in the item amount, and therefore is not calculated for the transaction.", + optional: true, + }, + salesTaxCodeRefValue: { + label: "Sales Tax Code Ref Value", + description: "ID of the referenced sales tax code for the Sales item. Applicable to Service and Sales item types only. Query the TaxCode name list resource to determine the appropriate TaxCode object for this reference. Use `TaxCode.Id` from that object for `SalesTaxCodeRef.value`.", + propDefinition: [ + quickbooks, + "taxCodeId", + ], + }, + purchaseTaxIncluded: { + type: "boolean", + label: "Purchase Tax Included", + description: "True if the purchase tax is included in the item amount, and therefore is not calculated for the transaction.", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "Description of the item.", + optional: true, + }, + abatementRate: { + type: "string", + label: "Abatement Rate", + description: "Sales tax abatement rate for India locales.", + optional: true, + }, + reverseChargeRate: { + type: "string", + label: "Reverse Charge Rate", + description: "Sales tax reverse charge rate for India locales.", + optional: true, + }, + subItem: { + type: "boolean", + label: "Sub Item", + description: "If true, this is a sub item. If false or null, this is a top-level item. Creating inventory hierarchies with traditional inventory items is being phased out in lieu of using categories and sub categories.", + optional: true, + }, + taxable: { + type: "boolean", + label: "Taxable", + description: "If true, transactions for this item are taxable. Applicable to US companies, only.", + optional: true, + }, + UqcDisplayText: { + type: "string", + label: "UQC Display Text", + description: "Text to be displayed on customer's invoice to denote the Unit of Measure (instead of the standard code).", + optional: true, + }, + reorderPoint: { + type: "string", + label: "Reorder Point", + description: "The minimum quantity of a particular inventory item that you need to restock at any given time. The ReorderPoint value cannot be set to null for sparse updates(sparse=true). It can be set to null only for full updates.", + optional: true, + }, + purchaseDesc: { + type: "string", + label: "Purchase Description", + description: "Purchase description for the item.", + optional: true, + }, + prefVendorRefValue: { + type: "string", + label: "Pref Vendor Ref Value", + description: "Pref vendor ref value", + propDefinition: [ + quickbooks, + "vendorIds", + ], + }, + active: { + type: "boolean", + label: "Active", + description: "If true, the object is currently enabled for use by QuickBooks.", + optional: true, + }, + UqcId: { + type: "string", + label: "UQC ID", + description: "ID of Standard Unit of Measure (UQC:Unique Quantity Code) of the item according to GST rule.", + optional: true, + }, + purchaseTaxCodeRefValue: { + label: "Purchase Tax Code Ref Value", + description: "The ID for the referenced purchase tax code object as found in the Id field of the object payload. \n\nReference to the purchase tax code for the item. Applicable to Service, Other Charge, and Product (Non-Inventory) item types. Query the TaxCode name list resource to determine the appropriate TaxCode object for this reference. Use `TaxCode.Id` from that object for `PurchaseTaxCodeRef.value`.", + propDefinition: [ + quickbooks, + "taxCodeId", + ], + }, + serviceType: { + type: "string", + label: "Service Type", + description: "Sales tax service type for India locales.", + optional: true, + }, + purchaseCost: { + type: "string", + label: "Purchase Cost", + description: "Amount paid when buying or ordering the item, as expressed in the home currency.", + optional: true, + }, + unitPrice: { + type: "string", + label: "Unit Price", + description: "Corresponds to the Price/Rate column on the QuickBooks Online UI to specify either unit price, a discount, or a tax rate for item. If used for unit price, the monetary value of the service or product, as expressed in the home currency. If used for a discount or tax rate, express the percentage as a fraction. For example, specify `0.4` for 40% tax", + optional: true, + }, + taxClassificationRefValue: { + description: "The ID for the referenced Tax classification object as found in the Id field of the object payload.\n\nTax classification segregates different items into different classifications and the tax classification is one of the key parameters to determine appropriate tax on transactions involving items. Tax classifications are sourced by either tax governing authorities as in India/Malaysia or externally like Exactor. 'Fuel', 'Garments' and 'Soft drinks' are a few examples of tax classification in layman terms. User can choose a specific tax classification for an item while creating it. A level 1 tax classification cannot be associated to an Item", + propDefinition: [ + quickbooks, + "taxClassificationId", + ], + }, + parentRefValue: { + label: "Parent Ref Value", + description: "The ID for the referenced parent item object as found in the Id field of the object payload. \n\nThe immediate parent of the sub item in the hierarchical Category:Sub-category list. If SubItem is true, then ParenRef is required. Query the Item name list resource to determine the appropriate object for this reference. Use `Item.Id` from that object for `ParentRef.value`.", + optional: true, + propDefinition: [ + quickbooks, + "itemId", + ], + }, + }, + methods: { + async getSyncToken($) { + const { Item: item } = await this.quickbooks.getItem({ + $, + itemId: this.itemId, + }); + return item.SyncToken; + }, + }, + async run({ $ }) { + if (!this.itemId || !this.name) { + throw new ConfigurationError("Must provide itemId and name parameters."); + } + + const data = { + sparse: true, + Id: this.itemId, + Name: this.name, + QtyOnHand: this.qtyOnHand, + SyncToken: await this.getSyncToken($), + IncomeAccountRef: this.incomeAccountRefValue && { + value: this.incomeAccountRefValue, + }, + Type: this.type, + AssetAccountRef: this.assetAccountRefValue && { + value: this.assetAccountRefValue, + }, + InvStartDate: this.invStartDate, + ExpenseAccountRef: this.expenseAccountRefValue && { + value: this.expenseAccountRefValue, + }, + Sku: this.sku, + SalesTaxIncluded: this.salesTaxIncluded, + TrackQtyOnHand: this.trackQtyOnHand, + SalesTaxCodeRef: this.salesTaxCodeRefValue && { + value: this.salesTaxCodeRefValue, + }, + PurchaseTaxIncluded: this.purchaseTaxIncluded, + Description: this.description, + AbatementRate: this.abatementRate, + ReverseChargeRate: this.reverseChargeRate, + SubItem: this.subItem, + Taxable: this.taxable, + UQCDisplayText: this.UqcDisplayText, + ReorderPoint: this.reorderPoint, + PurchaseDesc: this.purchaseDesc, + PrefVendorRef: this.prefVendorRefValue && { + value: this.prefVendorRefValue, + }, + Active: this.active, + UQCId: this.UqcId, + PurchaseTaxCodeRef: this.purchaseTaxCodeRefValue && { + value: this.purchaseTaxCodeRefValue, + }, + ServiceType: this.serviceType, + PurchaseCost: this.purchaseCost, + UnitPrice: this.unitPrice, + TaxClassificationRef: this.taxClassificationRefValue && { + value: this.taxClassificationRefValue, + }, + }; + + if (this.parentRefValue || this.parentRefName) { + data["ParentRef"] = { + value: this.parentRefValue, + }; + } + + const response = await this.quickbooks.updateItem({ + $, + data, + }); + + if (response) { + $.export("summary", `Successfully updated item with ID ${this.itemId}`); + } + + return response; + }, +}; diff --git a/components/quickbooks/common/constants.mjs b/components/quickbooks/common/constants.mjs new file mode 100644 index 0000000000000..bf001cba36632 --- /dev/null +++ b/components/quickbooks/common/constants.mjs @@ -0,0 +1,125 @@ +export const LIMIT = 100; +export const MAX_RETRIES = 5; +export const INITIAL_BACKOFF_MILLISECONDS = 1500; + +export const AP_AGING_REPORT_COLUMNS = [ + "create_by", + "create_date", + "doc_num", + "due_date", + "last_mod_by", + "last_mod_date", + "memo", + "past_due", + "term_name", + "tx_date", + "txn_type", + "vend_bill_addr", + "vend_comp_name", + "vend_name", + "vend_pri_cont", + "vend_pri_email", + "vend_pri_tel", + "dept_name", + "currency", + "exch_rate", + "subt_neg_open_bal", + "subt_neg_amount", + "neg_foreign_open_bal", + "subt_neg_home_open_bal", + "neg_foreign_amount", + "subt_neg_home_amount", +]; + +export const PROFIT_LOSS_REPORT_COLUMNS = [ + "create_by", + "create_date", + "doc_num", + "last_mod_by", + "last_mod_date", + "memo", + "name", + "pmt_mthd", + "split_acc", + "tx_date", + "txn_type", + "tax_code", + "klass_name", + "dept_name", + "debt_amt", + "credit_amt", + "nat_open_bal", + "subt_nat_amount", + "subt_nat_amount_nt", + "rbal_nat_amount", + "rbal_nat_amount_nt", + "tax_amount", + "net_amount", + "debt_home_amt", + "credit_home_amt", + "currency", + "exch_rate", + "nat_home_open_bal", + "nat_foreign_open_bal", + "subt_nat_home_amount", + "subt_nat_amount_home_nt", + "rbal_nat_home_amount", + "rbal_nat_amount_home_nt", + "home_tax_amount", + "home_net_amount", +]; + +export const DATE_MACRO_OPTIONS = [ + "Today", + "Yesterday", + "This Week", + "Last Week", + "This Week-to-date", + "Last Week-to-date", + "Next Week", + "Next 4 Weeks", + "This Month", + "Last Month", + "This Month-to-date", + "Last Month-to-date", + "Next Month", + "This Fiscal Quarter", + "Last Fiscal Quarter", + "This Fiscal Quarter-to-date", + "Last Fiscal Quarter-to-date", + "Next Fiscal Quarter", + "This Fiscal Year", + "Last Fiscal Year", + "This Fiscal Year-to-date", + "Last Fiscal Year-to-date", + "Next Fiscal Year", +]; + +export const PAYMENT_METHOD_OPTIONS = [ + "Cash", + "Check", + "Dinners Club", + "American Express", + "Discover", + "MasterCard", + "Visa", +]; + +export const ACCOUNT_TYPE_OPTIONS = [ + "AccountsPayable", + "AccountsReceivable", + "Bank", + "CostOfGoodsSold", + "CreditCard", + "Equity", + "Expense", + "FixedAsset", + "Income", + "LongTermLiability", + "NonPosting", + "OtherAsset", + "OtherCurrentAsset", + "OtherCurrentLiability", + "OtherExpense", + "OtherIncome", +]; diff --git a/components/quickbooks/common/utils.mjs b/components/quickbooks/common/utils.mjs index 1d579b8da0b66..fb0d909f06b08 100644 --- a/components/quickbooks/common/utils.mjs +++ b/components/quickbooks/common/utils.mjs @@ -1,3 +1,9 @@ +import { + MAX_RETRIES, + INITIAL_BACKOFF_MILLISECONDS, +} from "./constants.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + export const parseOne = (obj) => { let parsed; try { @@ -8,3 +14,33 @@ export const parseOne = (obj) => { return parsed; }; +export async function retryWithExponentialBackoff( + requestFn, retries = MAX_RETRIES, backoff = INITIAL_BACKOFF_MILLISECONDS, +) { + try { + return await requestFn(); + } catch (error) { + if (retries > 0 && error.response?.status === 429) { + console.warn(`Rate limit exceeded. Retrying in ${backoff}ms...`); + await new Promise((resolve) => setTimeout(resolve, backoff)); + return retryWithExponentialBackoff(requestFn, retries - 1, backoff * 2); + } + throw error; + } +} + +export function parseLineItems(arr) { + if (!arr) { + return undefined; + } + try { + if (typeof arr === "string") { + return JSON.parse(arr); + } + return arr.map((lineItem) => typeof lineItem === "string" + ? JSON.parse(lineItem) + : lineItem); + } catch (error) { + throw new ConfigurationError(`We got an error trying to parse the LineItems. Error: ${error}`); + } +} diff --git a/components/quickbooks/package-lock.json b/components/quickbooks/package-lock.json index 6e03670e82062..0729857583216 100644 --- a/components/quickbooks/package-lock.json +++ b/components/quickbooks/package-lock.json @@ -57,9 +57,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -176,9 +176,9 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" }, "form-data": { "version": "4.0.0", diff --git a/components/quickbooks/package.json b/components/quickbooks/package.json index d1a1bccafb886..0e25d35f339be 100644 --- a/components/quickbooks/package.json +++ b/components/quickbooks/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/quickbooks", - "version": "0.1.0", + "version": "0.5.1", "description": "Pipedream Quickbooks Components", "main": "quickbooks.app.mjs", "keywords": [ @@ -14,6 +14,6 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.0.0" + "@pipedream/platform": "^3.0.0" } } diff --git a/components/quickbooks/quickbooks.app.mjs b/components/quickbooks/quickbooks.app.mjs index dce002c60af14..96307d7354d84 100644 --- a/components/quickbooks/quickbooks.app.mjs +++ b/components/quickbooks/quickbooks.app.mjs @@ -1,4 +1,6 @@ import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; +import { retryWithExponentialBackoff } from "./common/utils.mjs"; export default { type: "app", @@ -7,109 +9,358 @@ export default { invoiceId: { label: "Invoice ID", type: "string", - description: "Id of the invoice to get details of.", + description: "Id of the invoice to get details of", async options({ page }) { - const position = 1 + (page * 10); - const { QueryResponse: { Invoice: records } } = await this.query({ - params: { - query: `select * from invoice maxresults 10${page - ? `startposition ${position}` - : ""} `, - }, + return this.getPropOptions({ + page, + resource: "Invoice", + mapper: ({ + Id: value, DocNumber: docNumber, CustomerRef: customerRef, + }) => ({ + label: `(${docNumber}) ${customerRef.name}`, + value, + }), }); - - return records?.map(({ - Id: value, DocNumber: docNumber, CustomerRef: customerRef, - }) => ({ - label: `(${docNumber}) ${customerRef.name}`, - value, - })) || []; }, }, - minorVersion: { - label: "Minor Version", - type: "string", - description: "Use the `minorversion` query parameter in REST API requests to access a version of the API other than the generally available version. For example, to invoke minor version 1 of the JournalEntry entity, issue the following request:\n`https://quickbooks.api.intuit.com/v3/company//journalentry/entityId?minorversion=1`", - optional: true, - }, - lineItems: { - label: "Line Items", - type: "string[]", - description: "Individual line items of a transaction. Valid Line types include: `ItemBasedExpenseLine` and `AccountBasedExpenseLine`. One minimum line item required for the request to succeed. E.g `[ { \"DetailType\": \"AccountBasedExpenseLineDetail\", \"Amount\": 100.0, \"AccountBasedExpenseLineDetail\": { \"AccountRef\": { \"name\": \"Meals and Entertainment\", \"value\": \"10\" } } } ]`", - }, customer: { label: "Customer Reference", type: "string", - description: "Reference to a customer or job. Query the Customer name list resource to determine the appropriate Customer object for this reference.", + description: "Reference to a customer or job", async options({ page }) { - const position = 1 + (page * 10); - const { QueryResponse: { Customer: records } } = await this.query({ - params: { - query: `select * from Customer maxresults 10${page - ? `startposition ${position}` - : ""} `, - }, + return this.getPropOptions({ + page, + resource: "Customer", + mapper: ({ + Id: id, DisplayName: label, + }) => ({ + label, + value: id, + }), }); - - return records?.map(({ - Id: id, DisplayName: label, - }) => ({ - label, - value: JSON.stringify({ + }, + }, + customerType: { + label: "Customer Type", + type: "string", + description: "ID referencing the customer type assigned to a customer", + optional: true, + async options({ page }) { + return this.getPropOptions({ + page, + resource: "CustomerType", + mapper: ({ + Id: id, Name: label, + }) => ({ + label, value: id, - name: label, }), - })) || []; + }); }, }, - customerRefName: { - label: "Customer Reference Name", + paymentMethod: { + label: "Payment Method", type: "string", - description: "Reference to a customer or job. Query the Customer name list resource to determine the appropriate Customer object for this reference. Use `Customer.DisplayName ` from that object for `CustomerRef.name`.", + description: "ID referencing a PaymentMethod object associated with this Customer object", optional: true, + async options({ page }) { + return this.getPropOptions({ + page, + resource: "Payment Method", + mapper: ({ + Id: id, Name: label, + }) => ({ + label, + value: id, + }), + }); + }, }, currency: { - label: "Currency Reference", + label: "Currency Code", type: "string", - description: "This must be defined if multicurrency is enabled for the company.\nMulticurrency is enabled for the company if `Preferences.MultiCurrencyEnabled` is set to `true`. Read more about multicurrency support [here](https://developer.intuit.com/docs?RedirectID=MultCurrencies). Required if multicurrency is enabled for the company.", + description: "A three letter string representing the ISO 4217 code for the currency. For example, `USD`, `AUD`, `EUR`, and so on. This must be defined if multicurrency is enabled for the company.\nMulticurrency is enabled for the company if `Preferences.MultiCurrencyEnabled` is set to `true`. Read more about multicurrency support [here](https://developer.intuit.com/docs?RedirectID=MultCurrencies). Required if multicurrency is enabled for the company.", optional: true, + default: "USD", async options({ page }) { - const position = 1 + (page * 10); - const { QueryResponse: { CompanyCurrency: records } } = await this.query({ - params: { - query: `select * from companycurrency maxresults 10${page - ? `startposition ${position}` - : ""} `, - }, - }); - - return records?.map(({ - Code: code, Name: name, - }) => ({ - label: `${code} - ${name}`, - value: JSON.stringify({ - value: code, - name: name, + return this.getPropOptions({ + page, + resource: "CompanyCurrency", + mapper: ({ + Code: code, Name: name, + }) => ({ + label: `${code} - ${name}`, + value: JSON.stringify({ + value: code, + name: name, + }), }), - })) || []; + }); }, }, - currencyRefValue: { - label: "Currency Reference Value", + purchaseId: { + label: "Purchase ID", type: "string", - description: "A three letter string representing the ISO 4217 code for the currency. For example, `USD`, `AUD`, `EUR`, and so on. This must be defined if multicurrency is enabled for the company.\nMulticurrency is enabled for the company if `Preferences.MultiCurrencyEnabled` is set to `true`. Read more about multicurrency support [here](https://developer.intuit.com/docs?RedirectID=MultCurrencies). Required if multicurrency is enabled for the company.", + description: "ID of the purchase.", + withLabel: true, + async options({ page }) { + return this.getPropOptions({ + page, + resource: "Purchase", + mapper: ({ + Id, PaymentType, TxnDate, + }) => ({ + label: `${Id} - ${PaymentType} - ${TxnDate}`, + value: Id, + }), + }); + }, + }, + termIds: { + type: "string[]", + label: "Term IDs", + description: "Filters report contents based on term or terms supplied", optional: true, + async options({ page }) { + return this.getPropOptions({ + page, + resource: "Term", + mapper: ({ + Id: value, Name: label, + }) => ({ + value, + label, + }), + }); + }, }, - currencyRefName: { - label: "Currency Reference Name", + vendorIds: { + type: "string[]", + label: "Vendor IDs", + description: "Filters report contents to include information for specified vendors", + optional: true, + async options({ page }) { + return this.getPropOptions({ + page, + resource: "Vendor", + mapper: ({ + Id: value, DisplayName: label, + }) => ({ + value, + label, + }), + }); + }, + }, + accountIds: { + type: "string[]", + label: "Account IDs", + description: "Filters report contents to include information for specified accounts", + optional: true, + async options({ page }) { + return this.getPropOptions({ + page, + resource: "Account", + mapper: ({ + Id: value, Name: label, + }) => ({ + value, + label, + }), + }); + }, + }, + classIds: { + type: "string[]", + label: "Class IDs", + description: "Filters report contents to include information for specified classes if so configured in the company file", + optional: true, + async options({ page }) { + return this.getPropOptions({ + page, + resource: "Class", + mapper: ({ + Id: value, Name: label, + }) => ({ + value, + label, + }), + }); + }, + }, + employeeIds: { + type: "string[]", + label: "Employee IDs", + description: "Filters report contents to include information for specified employees", + optional: true, + async options({ page }) { + return this.getPropOptions({ + page, + resource: "Employee", + mapper: ({ + Id: value, DisplayName: label, + }) => ({ + value, + label, + }), + }); + }, + }, + departmentIds: { + type: "string[]", + label: "Department IDs", + description: "Filters report contents to include information for specified departments if so configured in the company file", + optional: true, + async options({ page }) { + return this.getPropOptions({ + page, + resource: "Department", + mapper: ({ + Id: value, Name: label, + }) => ({ + value, + label, + }), + }); + }, + }, + itemId: { + type: "string", + label: "Item ID", + description: "The identifier of an item", + async options({ page }) { + return this.getPropOptions({ + page, + resource: "Item", + mapper: ({ + Id: value, Name: label, + }) => ({ + value, + label, + }), + }); + }, + }, + billId: { + type: "string", + label: "Bill ID", + description: "The identifier of a bill", + async options({ page }) { + return this.getPropOptions({ + page, + resource: "Bill", + mapper: ({ Id: id }) => id, + }); + }, + }, + purchaseOrderId: { + type: "string", + label: "Purchase Order ID", + description: "The identifier of a purchase order", + async options({ page }) { + return this.getPropOptions({ + page, + resource: "PurchaseOrder", + mapper: ({ + Id: value, DocNumber, + }) => ({ + value, + label: DocNumber ?? value, + }), + }); + }, + }, + salesReceiptId: { type: "string", - description: "The full name of the currency.", + label: "Sales Receipt ID", + description: "The identifier of a sales receipt", + async options({ page }) { + return this.getPropOptions({ + page, + resource: "SalesReceipt", + mapper: ({ + Id: value, DocNumber, + }) => ({ + value, + label: DocNumber ?? value, + }), + }); + }, + }, + timeActivityId: { + type: "string", + label: "Time Activity ID", + description: "The identifier of a time activity", + async options({ page }) { + return this.getPropOptions({ + page, + resource: "TimeActivity", + mapper: ({ + Id: value, NameOf: nameOf, Description: description, + }) => ({ + value, + label: `${nameOf} ${description}`, + }), + }); + }, + }, + paymentId: { + type: "string", + label: "Payment ID", + description: "The identifier of a payment", + async options({ page }) { + return this.getPropOptions({ + page, + resource: "Payment", + mapper: ({ + Id: value, CustomerRef: customerRef, TotalAmt: totalAmt, TxnDate: txnDate, + }) => ({ + value, + label: `${customerRef.name} - Amount: ${totalAmt} - ${txnDate}`, + }), + }); + }, + }, + taxCodeId: { + type: "string", + label: "Tax Code ID", + description: "The identifier of a tax code", optional: true, + async options({ page }) { + return this.getPropOptions({ + page, + resource: "TaxCode", + mapper: ({ + Id: value, Name: label, + }) => ({ + value, + label, + }), + }); + }, }, - includeClause: { - description: "Fields to use in the select clause of the data query. See query language syntax, limitations, and other specifications on [Data queries](https://developer.intuit.com/app/developer/qbo/docs/develop/explore-the-quickbooks-online-api/data-queries)", - label: "Include Clause", + taxClassificationId: { type: "string", + label: "Tax Classification ID", + description: "The identifier of a tax classification", + optional: true, + async options({ page }) { + return this.getPropOptions({ + page, + resource: "TaxClassification", + mapper: ({ + id: value, name: label, + }) => ({ + value, + label, + }), + }); + }, + }, + lineItemsAsObjects: { + type: "boolean", + label: "Enter Line Items as Objects", + description: "Enter line items as an array of objects", }, maxResults: { description: "The number of entity elements in the response.", @@ -164,35 +415,28 @@ export default { description: "Family name or the last name of the person. The `DisplayName` attribute or at least one of `Title`, `GivenName`, `MiddleName`, `FamilyName`, or `Suffix` attributes is required for object create.", optional: true, }, - purchaseId: { - label: "purchase Id", - type: "string", - description: "Id of the purchase.", - withLabel: true, - async options({ page }) { - const position = 1 + (page * 10); - const { QueryResponse: { Purchase: records } } = await this.query({ - params: { - query: `select * from Purchase maxresults 10 ${page - ? `startposition ${position}` - : ""} `, - }, - }); - - return records?.map(({ - Id, PaymentType, SyncToken, - }) => ({ - label: `${Id} - ${PaymentType}`, - value: `${Id}|${SyncToken}`, - })) || []; - }, - }, suffix: { label: "Suffix", type: "string", description: "Suffix of the name. For example, `Jr`. The `DisplayName` attribute or at least one of `Title`, `GivenName`, `MiddleName`, `FamilyName`, or `Suffix` attributes is required for object create.", optional: true, }, + accountingMethod: { + type: "string", + label: "Accounting Method", + description: "The accounting method used in the report", + options: [ + "Cash", + "Accrual", + ], + optional: true, + }, + columns: { + type: "string[]", + label: "Columns", + description: "Column types to be shown in the report", + optional: true, + }, }, methods: { _companyId() { @@ -204,150 +448,247 @@ export default { _apiUrl() { return "https://quickbooks.api.intuit.com/v3"; }, - async _makeRequest(path, options = {}, $ = this) { - return axios($, { - url: `${this._apiUrl()}/${path}`, - headers: { - Authorization: `Bearer ${this._accessToken()}`, - accept: "application/json", + async _makeRequest({ + $ = this, + path, + params, + ...opts + }) { + const requestFn = async () => { + return await axios($, { + url: `${this._apiUrl()}/${path}`, + headers: { + Authorization: `Bearer ${this._accessToken()}`, + accept: "application/json", + }, + params: { + ...params, + minorversion: 75, + }, + ...opts, + }); + }; + return await retryWithExponentialBackoff(requestFn); + }, + async getPropOptions({ + page = 0, resource, mapper, + }) { + const position = 1 + (page * 10); + const { QueryResponse: queryResponse } = await this.query({ + params: { + query: `select * from ${resource} maxresults 10${page + ? `startposition ${position}` + : ""} `, }, - ...options, }); + const items = queryResponse[resource]; + return items?.map(mapper) || []; }, - async createPayment({ - $, data, - }) { - return this._makeRequest(`company/${this._companyId()}/payment`, { + createPayment(opts = {}) { + return this._makeRequest({ + path: `company/${this._companyId()}/payment`, method: "post", - data, - }, $); + ...opts, + }); }, - async createBill({ - $, data, params, - }) { - return this._makeRequest(`company/${this._companyId()}/bill`, { + createBill(opts = {}) { + return this._makeRequest({ + path: `company/${this._companyId()}/bill`, method: "post", - data, - params, - }, $); + ...opts, + }); }, - async createCustomer({ - $, data, params, - }) { - return this._makeRequest(`company/${this._companyId()}/customer`, { + createCustomer(opts = {}) { + return this._makeRequest({ + path: `company/${this._companyId()}/customer`, method: "post", - data, - params, - }, $); + ...opts, + }); }, - createPurchase({ - $, ...args - }) { - return this._makeRequest(`company/${this._companyId()}/purchase`, { - method: "POST", - ...args, - }, $); + createPurchase(opts = {}) { + return this._makeRequest({ + path: `company/${this._companyId()}/purchase`, + method: "post", + ...opts, + }); }, - async createInvoice({ - $, data, params, - }) { - return this._makeRequest(`company/${this._companyId()}/invoice`, { + createInvoice(opts = {}) { + return this._makeRequest({ + path: `company/${this._companyId()}/invoice`, method: "post", - data, - params, - }, $); + ...opts, + }); }, - async deletePurchase({ - $, ...args - }) { - return this._makeRequest(`company/${this._companyId()}/purchase`, { - method: "POST", - ...args, - }, $); + createSalesReceipt(opts = {}) { + return this._makeRequest({ + path: `company/${this._companyId()}/salesreceipt`, + method: "post", + ...opts, + }); }, - async sparseUpdateInvoice({ - $, data, params, - }) { - return this._makeRequest(`company/${this._companyId()}/invoice`, { + deletePurchase(opts = {}) { + return this._makeRequest({ + path: `company/${this._companyId()}/purchase`, method: "post", - data, - params, - }, $); + ...opts, + }); }, - async getBill({ - $, billId, params, - }) { - return this._makeRequest(`company/${this._companyId()}/bill/${billId}`, { - params, - }, $); + sparseUpdateInvoice(opts = {}) { + return this._makeRequest({ + path: `company/${this._companyId()}/invoice`, + method: "post", + ...opts, + }); }, - async getCustomer({ - $, customerId, params, + getBill({ + billId, ...opts }) { - return this._makeRequest(`company/${this._companyId()}/customer/${customerId}`, { - params, - }, $); + return this._makeRequest({ + path: `company/${this._companyId()}/bill/${billId}`, + ...opts, + }); }, - async getInvoice({ - $, invoiceId, params, + getCustomer({ + customerId, ...opts }) { - return this._makeRequest(`company/${this._companyId()}/invoice/${invoiceId}`, { - params, - }, $); + return this._makeRequest({ + path: `company/${this._companyId()}/customer/${customerId}`, + ...opts, + }); }, - async getInvoices({ - $, params, + getInvoice({ + invoiceId, ...opts }) { - return this._makeRequest(`company/${this._companyId()}/query`, { - params, - }, $); + return this._makeRequest({ + path: `company/${this._companyId()}/invoice/${invoiceId}`, + ...opts, + }); + }, + getInvoices(opts = {}) { + return this._makeRequest({ + path: `company/${this._companyId()}/query`, + ...opts, + }); }, - async getMyCompany({ $ } = {}) { - return this._makeRequest(`company/${this._companyId()}/companyinfo/${this._companyId()}`, {}, $); + getMyCompany(opts = {}) { + return this._makeRequest({ + path: `company/${this._companyId()}/companyinfo/${this._companyId()}`, + ...opts, + }); }, - async getPurchase({ - $, purchaseId, params, + getPurchase({ + purchaseId, ...opts }) { - return this._makeRequest(`company/${this._companyId()}/purchase/${purchaseId}`, { - params, - }, $); + return this._makeRequest({ + path: `company/${this._companyId()}/purchase/${purchaseId}`, + ...opts, + }); }, - async getPurchaseOrder({ - $, purchaseOrderId, params, + getPurchaseOrder({ + purchaseOrderId, ...opts }) { - return this._makeRequest(`company/${this._companyId()}/purchaseorder/${purchaseOrderId}`, { - params, - }, $); + return this._makeRequest({ + path: `company/${this._companyId()}/purchaseorder/${purchaseOrderId}`, + ...opts, + }); }, - async getSalesReceipt({ - $, salesReceiptId, params, + getSalesReceipt({ + salesReceiptId, ...opts }) { - return this._makeRequest(`company/${this._companyId()}/salesreceipt/${salesReceiptId}`, { - params, - }, $); + return this._makeRequest({ + path: `company/${this._companyId()}/salesreceipt/${salesReceiptId}`, + ...opts, + }); }, - async getTimeActivity({ - $, timeActivityId, params, + getTimeActivity({ + timeActivityId, ...opts }) { - return this._makeRequest(`company/${this._companyId()}/timeactivity/${timeActivityId}`, { - params, - }, $); + return this._makeRequest({ + path: `company/${this._companyId()}/timeactivity/${timeActivityId}`, + ...opts, + }); }, - async query({ - $, params, + getPayment({ + paymentId, ...opts }) { - return this._makeRequest(`company/${this._companyId()}/query`, { - params, - }, $); + return this._makeRequest({ + path: `company/${this._companyId()}/payment/${paymentId}`, + ...opts, + }); }, - async updateCustomer({ - $, data, params, + getItem({ + itemId, ...opts }) { - return this._makeRequest(`company/${this._companyId()}/customer`, { + return this._makeRequest({ + path: `company/${this._companyId()}/item/${itemId}`, + ...opts, + }); + }, + query(opts = {}) { + return this._makeRequest({ + path: `company/${this._companyId()}/query`, + ...opts, + }); + }, + updateCustomer(opts = {}) { + return this._makeRequest({ + path: `company/${this._companyId()}/customer`, + method: "post", + ...opts, + }); + }, + updateItem(opts = {}) { + return this._makeRequest({ + path: `company/${this._companyId()}/item`, method: "post", - data, - params, - }, $); + ...opts, + }); + }, + getApAgingReport(opts = {}) { + return this._makeRequest({ + path: `company/${this._companyId()}/reports/AgedPayableDetail`, + ...opts, + }); + }, + getProfitLossReport(opts = {}) { + return this._makeRequest({ + path: `company/${this._companyId()}/reports/ProfitAndLoss`, + ...opts, + }); + }, + async *paginate({ + fn, params = {}, fieldList, query, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + const position = 1 + (page * LIMIT); + params.query = query + ` maxresults ${LIMIT} ${page + ? `startposition ${position}` + : ""} `; + page++; + const { QueryResponse: queryResponse } = await fn({ + params, + ...opts, + }); + + const items = queryResponse[fieldList]; + if (!items) { + return false; + } + for (const d of items) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = items.length; + + } while (hasMore); }, }, }; diff --git a/components/quickbooks/sources/common/base.mjs b/components/quickbooks/sources/common/base.mjs new file mode 100644 index 0000000000000..4e1eec220dfbe --- /dev/null +++ b/components/quickbooks/sources/common/base.mjs @@ -0,0 +1,71 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import quickbooks from "../../quickbooks.app.mjs"; + +export default { + props: { + quickbooks, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01"; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const fieldDate = this.getFieldDate(); + + const response = await this.quickbooks.paginate({ + fn: this.quickbooks.query, + maxResults, + query: this.getQuery(this._getLastDate()), + fieldList: this.getFieldList(), + }); + + let responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + if (responseArray.length) { + this._setLastDate(responseArray[0].MetaData[fieldDate]); + } + + for (const item of responseArray.reverse()) { + const ts = Date.parse(item.MetaData[fieldDate]); + this.$emit(item, { + id: `${item.Id}-${ts}`, + summary: this.getSummary(item), + ts, + }); + } + }, + getFieldDate() { + return "CreateTime"; + }, + getQuery() { + throw new Error("getQuery is not implemented"); + }, + getFieldList() { + throw new Error("getFieldList is not implemented"); + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/quickbooks/sources/new-customer-created/new-customer-created.mjs b/components/quickbooks/sources/new-customer-created/new-customer-created.mjs new file mode 100644 index 0000000000000..037f81e135afa --- /dev/null +++ b/components/quickbooks/sources/new-customer-created/new-customer-created.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "quickbooks-new-customer-created", + name: "New Customer Created", + description: "Emit new event when a new customer is created.", + version: "0.0.5", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getQuery(lastDate) { + return `select * from Customer Where Metadata.CreateTime >= '${lastDate}' orderby Metadata.CreateTime desc`; + }, + getFieldList() { + return "Customer"; + }, + getSummary(item) { + return `New Customer: ${item.DisplayName}`; + }, + }, + sampleEmit, +}; diff --git a/components/quickbooks/sources/new-customer-created/test-event.mjs b/components/quickbooks/sources/new-customer-created/test-event.mjs new file mode 100644 index 0000000000000..8352ecc26fa0c --- /dev/null +++ b/components/quickbooks/sources/new-customer-created/test-event.mjs @@ -0,0 +1,52 @@ +export default { + "domain": "QBO", + "FamilyName": "Lauterbach", + "DisplayName": "Amy's Bird Sanctuary", + "DefaultTaxCodeRef": { + "value": "2" + }, + "PrimaryEmailAddr": { + "Address": "Birds@Intuit.com" + }, + "PreferredDeliveryMethod": "Print", + "GivenName": "Amy", + "FullyQualifiedName": "Amy's Bird Sanctuary", + "BillWithParent": false, + "Job": false, + "BalanceWithJobs": 274.0, + "PrimaryPhone": { + "FreeFormNumber": "(650) 555-3311" + }, + "Active": true, + "MetaData": { + "CreateTime": "2014-09-11T16:48:43-07:00", + "LastUpdatedTime": "2015-07-01T10:14:15-07:00" + }, + "BillAddr": { + "City": "Bayshore", + "Line1": "4581 Finch St.", + "PostalCode": "94326", + "Lat": "INVALID", + "Long": "INVALID", + "CountrySubDivisionCode": "CA", + "Id": "2" + }, + "MiddleName": "Michelle", + "Notes": "Note added via Update operation.", + "Taxable": true, + "Balance": 274.0, + "SyncToken": "5", + "CompanyName": "Amy's Bird Sanctuary", + "ShipAddr": { + "City": "Bayshore", + "Line1": "4581 Finch St.", + "PostalCode": "94326", + "Lat": "INVALID", + "Long": "INVALID", + "CountrySubDivisionCode": "CA", + "Id": "109" + }, + "PrintOnCheckName": "Amy's Bird Sanctuary", + "sparse": false, + "Id": "1" +} \ No newline at end of file diff --git a/components/quickbooks/sources/new-customer-updated/new-customer-updated.mjs b/components/quickbooks/sources/new-customer-updated/new-customer-updated.mjs new file mode 100644 index 0000000000000..5f7ec1477b9d7 --- /dev/null +++ b/components/quickbooks/sources/new-customer-updated/new-customer-updated.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "quickbooks-new-customer-updated", + name: "New Customer Updated", + description: "Emit new event when a customer is updated.", + version: "0.0.5", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getQuery(lastDate) { + return `select * from Customer Where Metadata.LastUpdatedTime >= '${lastDate}' orderby Metadata.LastUpdatedTime desc`; + }, + getFieldList() { + return "Customer"; + }, + getFieldDate() { + return "LastUpdatedTime"; + }, + getSummary(item) { + return `New Customer Updated: ${item.DisplayName}`; + }, + }, + sampleEmit, +}; diff --git a/components/quickbooks/sources/new-customer-updated/test-event.mjs b/components/quickbooks/sources/new-customer-updated/test-event.mjs new file mode 100644 index 0000000000000..8352ecc26fa0c --- /dev/null +++ b/components/quickbooks/sources/new-customer-updated/test-event.mjs @@ -0,0 +1,52 @@ +export default { + "domain": "QBO", + "FamilyName": "Lauterbach", + "DisplayName": "Amy's Bird Sanctuary", + "DefaultTaxCodeRef": { + "value": "2" + }, + "PrimaryEmailAddr": { + "Address": "Birds@Intuit.com" + }, + "PreferredDeliveryMethod": "Print", + "GivenName": "Amy", + "FullyQualifiedName": "Amy's Bird Sanctuary", + "BillWithParent": false, + "Job": false, + "BalanceWithJobs": 274.0, + "PrimaryPhone": { + "FreeFormNumber": "(650) 555-3311" + }, + "Active": true, + "MetaData": { + "CreateTime": "2014-09-11T16:48:43-07:00", + "LastUpdatedTime": "2015-07-01T10:14:15-07:00" + }, + "BillAddr": { + "City": "Bayshore", + "Line1": "4581 Finch St.", + "PostalCode": "94326", + "Lat": "INVALID", + "Long": "INVALID", + "CountrySubDivisionCode": "CA", + "Id": "2" + }, + "MiddleName": "Michelle", + "Notes": "Note added via Update operation.", + "Taxable": true, + "Balance": 274.0, + "SyncToken": "5", + "CompanyName": "Amy's Bird Sanctuary", + "ShipAddr": { + "City": "Bayshore", + "Line1": "4581 Finch St.", + "PostalCode": "94326", + "Lat": "INVALID", + "Long": "INVALID", + "CountrySubDivisionCode": "CA", + "Id": "109" + }, + "PrintOnCheckName": "Amy's Bird Sanctuary", + "sparse": false, + "Id": "1" +} \ No newline at end of file diff --git a/components/quickbooks/sources/new-employee-created/new-employee-created.mjs b/components/quickbooks/sources/new-employee-created/new-employee-created.mjs new file mode 100644 index 0000000000000..4a7b938a7de96 --- /dev/null +++ b/components/quickbooks/sources/new-employee-created/new-employee-created.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "quickbooks-new-employee-created", + name: "New Employee Created", + description: "Emit new event when a new employee is created.", + version: "0.0.3", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getQuery(lastDate) { + return `select * from Employee Where Metadata.CreateTime >= '${lastDate}' orderby Metadata.CreateTime desc`; + }, + getFieldList() { + return "Employee"; + }, + getFieldDate() { + return "CreateTime"; + }, + getSummary(item) { + return `New Employee: ${item.Id}`; + }, + }, + sampleEmit, +}; diff --git a/components/quickbooks/sources/new-employee-created/test-event.mjs b/components/quickbooks/sources/new-employee-created/test-event.mjs new file mode 100644 index 0000000000000..95c34d34de479 --- /dev/null +++ b/components/quickbooks/sources/new-employee-created/test-event.mjs @@ -0,0 +1,16 @@ +export default { + "BillableTime": false, + "domain": "QBO", + "sparse": false, + "Id": "55", + "SyncToken": "0", + "MetaData": { + "CreateTime": "2021-08-30T11:21:48-07:00", + "LastUpdatedTime": "2021-08-30T11:21:48-07:00" + }, + "GivenName": "Emily", + "FamilyName": "Platt", + "DisplayName": "Emily Platt", + "PrintOnCheckName": "Emily Platt", + "Active": true +} \ No newline at end of file diff --git a/components/quickbooks/sources/new-employee-updated/new-employee-updated.mjs b/components/quickbooks/sources/new-employee-updated/new-employee-updated.mjs new file mode 100644 index 0000000000000..4de774c06a07f --- /dev/null +++ b/components/quickbooks/sources/new-employee-updated/new-employee-updated.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "quickbooks-new-employee-updated", + name: "New Employee Updated", + description: "Emit new event when an employee is updated.", + version: "0.0.3", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getQuery(lastDate) { + return `select * from Employee Where Metadata.LastUpdatedTime >= '${lastDate}' orderby Metadata.LastUpdatedTime desc`; + }, + getFieldList() { + return "Employee"; + }, + getFieldDate() { + return "LastUpdatedTime"; + }, + getSummary(item) { + return `Employee Updated: ${item.Id}`; + }, + }, + sampleEmit, +}; diff --git a/components/quickbooks/sources/new-employee-updated/test-event.mjs b/components/quickbooks/sources/new-employee-updated/test-event.mjs new file mode 100644 index 0000000000000..95c34d34de479 --- /dev/null +++ b/components/quickbooks/sources/new-employee-updated/test-event.mjs @@ -0,0 +1,16 @@ +export default { + "BillableTime": false, + "domain": "QBO", + "sparse": false, + "Id": "55", + "SyncToken": "0", + "MetaData": { + "CreateTime": "2021-08-30T11:21:48-07:00", + "LastUpdatedTime": "2021-08-30T11:21:48-07:00" + }, + "GivenName": "Emily", + "FamilyName": "Platt", + "DisplayName": "Emily Platt", + "PrintOnCheckName": "Emily Platt", + "Active": true +} \ No newline at end of file diff --git a/components/quickbooks/sources/new-invoice-created/new-invoice-created.mjs b/components/quickbooks/sources/new-invoice-created/new-invoice-created.mjs new file mode 100644 index 0000000000000..44205dabe3a07 --- /dev/null +++ b/components/quickbooks/sources/new-invoice-created/new-invoice-created.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "quickbooks-new-invoice-created", + name: "New Invoice Created", + description: "Emit new event when a new invoice is created.", + version: "0.0.5", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getQuery(lastDate) { + return `select * from Invoice Where Metadata.CreateTime >= '${lastDate}' orderby Metadata.CreateTime desc`; + }, + getFieldList() { + return "Invoice"; + }, + getSummary(item) { + return `New Invoice: ${item.Id}`; + }, + }, + sampleEmit, +}; diff --git a/components/quickbooks/sources/new-invoice-created/test-event.mjs b/components/quickbooks/sources/new-invoice-created/test-event.mjs new file mode 100644 index 0000000000000..5fd4a0a67d551 --- /dev/null +++ b/components/quickbooks/sources/new-invoice-created/test-event.mjs @@ -0,0 +1,77 @@ +export default { + "TxnDate": "2015-07-24", + "domain": "QBO", + "PrintStatus": "NeedToPrint", + "TotalAmt": 150.0, + "Line": [ + { + "LineNum": 1, + "Amount": 150.0, + "SalesItemLineDetail": { + "TaxCodeRef": { + "value": "NON" + }, + "ItemRef": { + "name": "Services", + "value": "1" + } + }, + "Id": "1", + "DetailType": "SalesItemLineDetail" + }, + { + "DetailType": "SubTotalLineDetail", + "Amount": 150.0, + "SubTotalLineDetail": {} + } + ], + "DueDate": "2015-08-23", + "ApplyTaxAfterDiscount": false, + "DocNumber": "1070", + "sparse": false, + "ProjectRef": { + "value": "39298034" + }, + "Deposit": 0, + "Balance": 150.0, + "CustomerRef": { + "name": "Amy's Bird Sanctuary", + "value": "1" + }, + "TxnTaxDetail": { + "TotalTax": 0 + }, + "SyncToken": "0", + "LinkedTxn": [], + "ShipAddr": { + "City": "Bayshore", + "Line1": "4581 Finch St.", + "PostalCode": "94326", + "Lat": "INVALID", + "Long": "INVALID", + "CountrySubDivisionCode": "CA", + "Id": "109" + }, + "EmailStatus": "NotSet", + "BillAddr": { + "City": "Bayshore", + "Line1": "4581 Finch St.", + "PostalCode": "94326", + "Lat": "INVALID", + "Long": "INVALID", + "CountrySubDivisionCode": "CA", + "Id": "2" + }, + "MetaData": { + "CreateTime": "2015-07-24T10:35:08-07:00", + "LastUpdatedTime": "2015-07-24T10:35:08-07:00" + }, + "CustomField": [ + { + "DefinitionId": "1", + "Type": "StringType", + "Name": "Crew #" + } + ], + "Id": "239" +} \ No newline at end of file diff --git a/components/quickbooks/sources/new-invoice-updated/new-invoice-updated.mjs b/components/quickbooks/sources/new-invoice-updated/new-invoice-updated.mjs new file mode 100644 index 0000000000000..8e584ee018a40 --- /dev/null +++ b/components/quickbooks/sources/new-invoice-updated/new-invoice-updated.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "quickbooks-new-invoice-updated", + name: "New Invoice Updated", + description: "Emit new event when an invoice is updated.", + version: "0.0.5", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getQuery(lastDate) { + return `select * from Invoice Where Metadata.LastUpdatedTime >= '${lastDate}' orderby Metadata.LastUpdatedTime desc`; + }, + getFieldList() { + return "Invoice"; + }, + getFieldDate() { + return "LastUpdatedTime"; + }, + getSummary(item) { + return `New Invoice Updated: ${item.Id}`; + }, + }, + sampleEmit, +}; diff --git a/components/quickbooks/sources/new-invoice-updated/test-event.mjs b/components/quickbooks/sources/new-invoice-updated/test-event.mjs new file mode 100644 index 0000000000000..5fd4a0a67d551 --- /dev/null +++ b/components/quickbooks/sources/new-invoice-updated/test-event.mjs @@ -0,0 +1,77 @@ +export default { + "TxnDate": "2015-07-24", + "domain": "QBO", + "PrintStatus": "NeedToPrint", + "TotalAmt": 150.0, + "Line": [ + { + "LineNum": 1, + "Amount": 150.0, + "SalesItemLineDetail": { + "TaxCodeRef": { + "value": "NON" + }, + "ItemRef": { + "name": "Services", + "value": "1" + } + }, + "Id": "1", + "DetailType": "SalesItemLineDetail" + }, + { + "DetailType": "SubTotalLineDetail", + "Amount": 150.0, + "SubTotalLineDetail": {} + } + ], + "DueDate": "2015-08-23", + "ApplyTaxAfterDiscount": false, + "DocNumber": "1070", + "sparse": false, + "ProjectRef": { + "value": "39298034" + }, + "Deposit": 0, + "Balance": 150.0, + "CustomerRef": { + "name": "Amy's Bird Sanctuary", + "value": "1" + }, + "TxnTaxDetail": { + "TotalTax": 0 + }, + "SyncToken": "0", + "LinkedTxn": [], + "ShipAddr": { + "City": "Bayshore", + "Line1": "4581 Finch St.", + "PostalCode": "94326", + "Lat": "INVALID", + "Long": "INVALID", + "CountrySubDivisionCode": "CA", + "Id": "109" + }, + "EmailStatus": "NotSet", + "BillAddr": { + "City": "Bayshore", + "Line1": "4581 Finch St.", + "PostalCode": "94326", + "Lat": "INVALID", + "Long": "INVALID", + "CountrySubDivisionCode": "CA", + "Id": "2" + }, + "MetaData": { + "CreateTime": "2015-07-24T10:35:08-07:00", + "LastUpdatedTime": "2015-07-24T10:35:08-07:00" + }, + "CustomField": [ + { + "DefinitionId": "1", + "Type": "StringType", + "Name": "Crew #" + } + ], + "Id": "239" +} \ No newline at end of file diff --git a/components/quickbooks/sources/new-item-created/new-item-created.mjs b/components/quickbooks/sources/new-item-created/new-item-created.mjs new file mode 100644 index 0000000000000..0436825e02d80 --- /dev/null +++ b/components/quickbooks/sources/new-item-created/new-item-created.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "quickbooks-new-item-created", + name: "New Item Created", + description: "Emit new event when a new item is created.", + version: "0.0.5", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getQuery(lastDate) { + return `select * from Item Where Metadata.CreateTime >= '${lastDate}' orderby Metadata.CreateTime desc`; + }, + getFieldList() { + return "Item"; + }, + getSummary(item) { + return `New Item: ${item.Id}`; + }, + }, + sampleEmit, +}; diff --git a/components/quickbooks/sources/new-item-created/test-event.mjs b/components/quickbooks/sources/new-item-created/test-event.mjs new file mode 100644 index 0000000000000..57fbb68de17d7 --- /dev/null +++ b/components/quickbooks/sources/new-item-created/test-event.mjs @@ -0,0 +1,23 @@ +export default { + "FullyQualifiedName": "Concrete", + "domain": "QBO", + "Name": "Concrete", + "TrackQtyOnHand": false, + "Type": "Service", + "PurchaseCost": 0, + "IncomeAccountRef": { + "name": "Landscaping Services:Job Materials:Fountains and Garden Lighting", + "value": "48" + }, + "Taxable": true, + "MetaData": { + "CreateTime": "2014-09-16T10:36:03-07:00", + "LastUpdatedTime": "2014-09-19T12:47:47-07:00" + }, + "sparse": false, + "Active": true, + "SyncToken": "1", + "UnitPrice": 0, + "Id": "3", + "Description": "Concrete for fountain installation" +} \ No newline at end of file diff --git a/components/quickbooks/sources/new-item-updated/new-item-updated.mjs b/components/quickbooks/sources/new-item-updated/new-item-updated.mjs new file mode 100644 index 0000000000000..dff39f0ec7208 --- /dev/null +++ b/components/quickbooks/sources/new-item-updated/new-item-updated.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "quickbooks-new-item-updated", + name: "New Item Updated", + description: "Emit new event when an item is updated.", + version: "0.0.5", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getQuery(lastDate) { + return `select * from Item Where Metadata.LastUpdatedTime >= '${lastDate}' orderby Metadata.LastUpdatedTime desc`; + }, + getFieldList() { + return "Item"; + }, + getFieldDate() { + return "LastUpdatedTime"; + }, + getSummary(item) { + return `New Item Updated: ${item.Id}`; + }, + }, + sampleEmit, +}; diff --git a/components/quickbooks/sources/new-item-updated/test-event.mjs b/components/quickbooks/sources/new-item-updated/test-event.mjs new file mode 100644 index 0000000000000..57fbb68de17d7 --- /dev/null +++ b/components/quickbooks/sources/new-item-updated/test-event.mjs @@ -0,0 +1,23 @@ +export default { + "FullyQualifiedName": "Concrete", + "domain": "QBO", + "Name": "Concrete", + "TrackQtyOnHand": false, + "Type": "Service", + "PurchaseCost": 0, + "IncomeAccountRef": { + "name": "Landscaping Services:Job Materials:Fountains and Garden Lighting", + "value": "48" + }, + "Taxable": true, + "MetaData": { + "CreateTime": "2014-09-16T10:36:03-07:00", + "LastUpdatedTime": "2014-09-19T12:47:47-07:00" + }, + "sparse": false, + "Active": true, + "SyncToken": "1", + "UnitPrice": 0, + "Id": "3", + "Description": "Concrete for fountain installation" +} \ No newline at end of file diff --git a/components/quickbooks_sandbox/README.md b/components/quickbooks_sandbox/README.md index 23d2c73485cbd..f9da6d7d94dff 100644 --- a/components/quickbooks_sandbox/README.md +++ b/components/quickbooks_sandbox/README.md @@ -1,24 +1,11 @@ # Overview -QuickBooks Sandbox API provides a powerful and reliable platform to develop, -customize, and test applications related to accounting and finance. The API -allows developers to access a variety of features, such as: +QuickBooks Sandbox API provides a robust platform for developers to test QuickBooks Online integrations without affecting live data. With Pipedream, you can harness this API to automate various accounting tasks, simulate financial scenarios, or validate app behavior pre-deployment. Imagine syncing invoice statuses with your CRM, triggering alerts based on financial thresholds, or reconciling payments programmatically—all in a safe, isolated environment before going live. -- Create, read, update and delete financial data. -- Create and manage Invoices, Estimates, Orders, Payments and other objects. -- Log in and manage customers, vendors, and suppliers. -- Record and manage expenses, taxes, banking transactions and other financial - transactions. +# Example Use Cases -With the Quickbooks Sandbox API, you can build the following applications: +- **Automated Invoice Syncing with CRM**: Craft a workflow that listens for new invoices created in QuickBooks Sandbox, then automatically pushes these invoices to a CRM like Salesforce. This ensures that sales teams always have the latest billing information at their fingertips without manual data entry. -- A financial record-keeping and reporting app. -- An expense manager and budget tracker. -- A customer, vendor & supplier management system. -- A payments & receipts processing application. -- An invoicing & estimate management system. -- An accounts receivable & payable system. -- A multi-currency payment processing system. -- A tax calculation and reporting tool. -- A virtual banking application. -- A dynamic accounting dashboard. +- **Expense Report Alerts**: Design a system where expense reports submitted through QuickBooks Sandbox trigger real-time alerts via Slack or email. This could help managers promptly review and approve expenses, improving the efficiency of financial operations. + +- **Payment Reconciliation Automation**: Implement a workflow that reconciles payments received in a platform like Stripe with corresponding invoices in QuickBooks Sandbox. This would vastly reduce the manual workload of ensuring that payments are accurately reflected in the business's financial records. diff --git a/components/quickbooks_sandbox/actions/create-ap-aging-report/create-ap-aging-report.mjs b/components/quickbooks_sandbox/actions/create-ap-aging-report/create-ap-aging-report.mjs new file mode 100644 index 0000000000000..15f60380a021f --- /dev/null +++ b/components/quickbooks_sandbox/actions/create-ap-aging-report/create-ap-aging-report.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/create-ap-aging-report/create-ap-aging-report.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-create-ap-aging-report", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/create-bill/create-bill.mjs b/components/quickbooks_sandbox/actions/create-bill/create-bill.mjs new file mode 100644 index 0000000000000..dc43d92f195a9 --- /dev/null +++ b/components/quickbooks_sandbox/actions/create-bill/create-bill.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/create-bill/create-bill.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-create-bill", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/create-customer/create-customer.mjs b/components/quickbooks_sandbox/actions/create-customer/create-customer.mjs index 4779837b795b2..6d8141b7636b5 100644 --- a/components/quickbooks_sandbox/actions/create-customer/create-customer.mjs +++ b/components/quickbooks_sandbox/actions/create-customer/create-customer.mjs @@ -1,79 +1,22 @@ -// legacy_hash_id: a_EVi7zr -import { axios } from "@pipedream/platform"; +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/create-customer/create-customer.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); export default { + ...others, key: "quickbooks_sandbox-create-customer", - name: "Create Customer", - description: "Creates a customer.", - version: "0.1.1", - type: "action", + version: "0.1.4", + name, + description, + type, props: { - quickbooks_sandbox: { - type: "app", - app: "quickbooks_sandbox", - }, - display_name: { - type: "string", - description: "The name of the person or organization as displayed. Must be unique across all Customer, Vendor, and Employee objects. Cannot be removed with sparse update. If not supplied, the system generates DisplayName by concatenating customer name components supplied in the request from the following list: `Title`, `GivenName`, `MiddleName`, `FamilyName`, and `Suffix`.", - optional: true, - }, - title: { - type: "string", - description: "Title of the person. This tag supports i18n, all locales. The `DisplayName` attribute or at least one of `Title`, `GivenName`, `MiddleName`, `FamilyName`, `Suffix`, or `FullyQualifiedName` attributes are required during create.", - optional: true, - }, - given_name: { - type: "string", - description: "Given name or first name of a person. The `DisplayName` attribute or at least one of `Title`, `GivenName`, `MiddleName`, `FamilyName`, or `Suffix` attributes is required for object create.", - optional: true, - }, - middle_name: { - type: "string", - description: "Middle name of the person. The person can have zero or more middle names. The `DisplayName` attribute or at least one of `Title`, `GivenName`, `MiddleName`, `FamilyName`, or `Suffix` attributes is required for object create.", - optional: true, - }, - family_name: { - type: "string", - description: "Family name or the last name of the person. The `DisplayName` attribute or at least one of `Title`, `GivenName`, `MiddleName`, `FamilyName`, or `Suffix` attributes is required for object create.", - optional: true, - }, - suffix: { - type: "string", - description: "Suffix of the name. For example, `Jr`. The `DisplayName` attribute or at least one of `Title`, `GivenName`, `MiddleName`, `FamilyName`, or `Suffix` attributes is required for object create.", - optional: true, - }, - minorversion: { - type: "string", - description: "Use the minorversion query parameter in REST API requests to access a version of the API other than the generally available version. For example, to invoke minor version 1 of the JournalEntry entity, issue the following request:\n`https://quickbooks.api.intuit.com/v3/company//journalentry/entityId?minorversion=1`", - optional: true, - }, - }, - async run({ $ }) { - //See Quickbooks API docs at: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/customer#create-a-customer - - if (!this.display_name && (!this.title && !this.given_name && !this.middle_name && !this.family_name && !this.suffix)) { - throw new Error("Must provide display_name or at least one of title, given_name, middle_name, family_name, or suffix parameters."); - } - - return await axios($, { - method: "post", - url: `https://sandbox-quickbooks.api.intuit.com/v3/company/${this.quickbooks_sandbox.$auth.company_id}/customer`, - headers: { - "Authorization": `Bearer ${this.quickbooks_sandbox.$auth.oauth_access_token}`, - "accept": "application/json", - "content-type": "application/json", - }, - data: { - DisplayName: this.display_name, - Suffix: this.suffix, - Title: this.title, - MiddleName: this.middle_name, - FamilyName: this.family_name, - GivenName: this.given_name, - }, - params: { - minorversion: this.minorversion, - }, - }); + quickbooks: app, + ...props, }, }; diff --git a/components/quickbooks_sandbox/actions/create-invoice/create-invoice.mjs b/components/quickbooks_sandbox/actions/create-invoice/create-invoice.mjs index 8cb1799149c6c..20505197cd8fd 100644 --- a/components/quickbooks_sandbox/actions/create-invoice/create-invoice.mjs +++ b/components/quickbooks_sandbox/actions/create-invoice/create-invoice.mjs @@ -1,75 +1,22 @@ -// legacy_hash_id: a_gniWe7 -import { axios } from "@pipedream/platform"; +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/create-invoice/create-invoice.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); export default { + ...others, key: "quickbooks_sandbox-create-invoice", - name: "Create Invoice", - description: "Creates an invoice.", - version: "0.1.1", - type: "action", + version: "0.1.4", + name, + description, + type, props: { - quickbooks_sandbox: { - type: "app", - app: "quickbooks_sandbox", - }, - line_items: { - type: "any", - description: "The minimum line item required for the request is one of the following: \n* `SalesItemLine` type\n* `GroupLine` type\nand Inline subtotal using `DescriptionOnlyLine`", - }, - customer_ref_value: { - type: "string", - description: "Reference to a customer or job. Query the Customer name list resource to determine the appropriate Customer object for this reference. Use `Customer.Id` from that object for `CustomerRef.value`.", - }, - customer_ref_name: { - type: "string", - description: "Reference to a customer or job. Query the Customer name list resource to determine the appropriate Customer object for this reference. Use `Customer.DisplayName ` from that object for `CustomerRef.name`.", - optional: true, - }, - currency_ref_value: { - type: "string", - description: "A three letter string representing the ISO 4217 code for the currency. For example, `USD`, `AUD`, `EUR`, and so on. This must be defined if multicurrency is enabled for the company.\nMulticurrency is enabled for the company if `Preferences.MultiCurrencyEnabled` is set to `true`. Read more about multicurrency support [here](https://developer.intuit.com/docs?RedirectID=MultCurrencies). Required if multicurrency is enabled for the company.", - optional: true, - }, - currency_ref_name: { - type: "object", - description: "The full name of the currency.", - optional: true, - }, - minorversion: { - type: "string", - description: "Use the minorversion query parameter in REST API requests to access a version of the API other than the generally available version. For example, to invoke minor version 1 of the JournalEntry entity, issue the following request:\n`https://quickbooks.api.intuit.com/v3/company//journalentry/entityId?minorversion=1`", - optional: true, - }, - }, - async run({ $ }) { - // See Quickbooks API docs at: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#create-an-invoice - - if (!this.line_items || !this.customer_ref_value) { - throw new Error("Must provide line_items, and customer_ref_value parameters."); - } - - return await axios($, { - method: "post", - url: `https://sandbox-quickbooks.api.intuit.com/v3/company/${this.quickbooks_sandbox.$auth.company_id}/invoice`, - headers: { - "Authorization": `Bearer ${this.quickbooks_sandbox.$auth.oauth_access_token}`, - "accept": "application/json", - "content-type": "application/json", - }, - data: { - Line: this.line_items, - CustomerRef: { - value: this.customer_ref_value, - name: this.customer_ref_name, - }, - CurrencyRef: { - value: this.currency_ref_value, - name: this.currency_ref_name, - }, - }, - params: { - minorversion: this.minorversion, - }, - }); + quickbooks: app, + ...props, }, }; diff --git a/components/quickbooks_sandbox/actions/create-payment/create-payment.mjs b/components/quickbooks_sandbox/actions/create-payment/create-payment.mjs index 0f646fd99888c..df52c9df75a4b 100644 --- a/components/quickbooks_sandbox/actions/create-payment/create-payment.mjs +++ b/components/quickbooks_sandbox/actions/create-payment/create-payment.mjs @@ -1,121 +1,22 @@ -// legacy_hash_id: a_l0i8d0 -import { axios } from "@pipedream/platform"; +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/create-payment/create-payment.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); export default { + ...others, key: "quickbooks_sandbox-create-payment", - name: "Create Bill Payment", - description: "Creates a bill payment.", - version: "0.1.1", - type: "action", + version: "0.1.4", + name, + description, + type, props: { - quickbooks_sandbox: { - type: "app", - app: "quickbooks_sandbox", - }, - total_amt: { - type: "string", - description: "Indicates the total amount of the associated with this payment. This includes the total of all the payments from the BillPayment Details.", - }, - vendor_ref_value: { - type: "string", - description: "The id of the vendor reference for this transaction.", - }, - line: { - type: "any", - description: "Individual line items representing zero or more `Bill`, `VendorCredit`, and `JournalEntry` objects linked to this BillPayment object.. Valid Line type for create: `LinkedTxnLine`.", - }, - pay_type: { - type: "string", - description: "The payment type. Valid values include: `Check`, `CreditCard`", - options: [ - "Check", - "CreditCard", - ], - }, - vendor_ref_name: { - type: "string", - description: "The name of the vendor reference for this transaction.", - optional: true, - }, - currency_ref_value: { - type: "string", - description: "A three letter string representing the ISO 4217 code for the currency. For example, `USD`, `AUD`, `EUR`, and so on. This must be defined if multicurrency is enabled for the company.\nMulticurrency is enabled for the company if `Preferences.MultiCurrencyEnabled` is set to `true`. Read more about multicurrency support [here](https://developer.intuit.com/docs?RedirectID=MultCurrencies). Required if multicurrency is enabled for the company.", - optional: true, - }, - currency_ref_name: { - type: "object", - description: "The full name of the currency.", - optional: true, - }, - cc_account_ref_value: { - type: "string", - description: "The id of the credit card account reference. Required when `PayType` is `CreditCard`. Query the Account name list resource to determine the appropriate Account object for this reference. Use `Account.Id` from that object for `CCAccountRef.value`. The specified account must have `Account.AccountType` set to `Credit Card` and `Account.AccountSubType` set to `CreditCard`. Inject with data only if the payment was transacted through Intuit Payments API.", - optional: true, - }, - cc_account_ref_name: { - type: "string", - description: "The name of the credit card account reference. Query the Account name list resource to determine the appropriate Account object for this reference. Use `Account.Name` from that object for `CCAccountRef.name`. The specified account must have `Account.AccountType` set to `Credit Card` and `Account.AccountSubType` set to `CreditCard`. Inject with data only if the payment was transacted through Intuit Payments API.", - optional: true, - }, - bank_account_ref_value: { - type: "string", - description: "The id of the bank account reference. Required when `PayType` is `Check`. Query the Account name list resource to determine the appropriate Account object for this reference. Use `Account.Id` from that object for `APAccountRef.value`. The specified account must have `Account.AccountType` set to `Bank` and `Account.AccountSubType` set to `Checking`.", - optional: true, - }, - bank_account_ref_name: { - type: "string", - description: "The name of the bank account reference. Query the Account name list resource to determine the appropriate Account object for this reference. Use `Account.Name` from that object for `APAccountRef.name`. The specified account must have `Account.AccountType` set to `Bank` and `Account.AccountSubType` set to `Checking`.", - optional: true, - }, - minorversion: { - type: "string", - description: "Use the minorversion query parameter in REST API requests to access a version of the API other than the generally available version. For example, to invoke minor version 1 of the JournalEntry entity, issue the following request:\n`https://quickbooks.api.intuit.com/v3/company//journalentry/entityId?minorversion=1`", - optional: true, - }, - }, - async run({ $ }) { - // See Quickbooks API docs at: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/billpayment#create-a-billpayment - - if (!this.total_amt || !this.vendor_ref_value || !this.line || !this.pay_type) { - throw new Error("Must provide total_amt, and vendor_ref_value, line, and pay_type parameters."); - } - - return await axios($, { - method: "post", - url: `https://sandbox-quickbooks.api.intuit.com/v3/company/${this.quickbooks_sandbox.$auth.company_id}/billpayment`, - headers: { - "Authorization": `Bearer ${this.quickbooks_sandbox.$auth.oauth_access_token}`, - "accept": "application/json", - "content-type": "application/json", - }, - data: { - TotalAmt: this.total_amt, - VendorRef: { - value: this.vendor_ref_value, - name: this.vendor_ref_name, - }, - Line: this.line, - PayType: this.pay_type, - CurrencyRef: { - value: this.currency_ref_value, - name: this.currency_ref_name, - }, - CreditCardPayment: { - CCAccountRef: { - value: this.cc_account_ref_value, - name: this.cc_account_ref_name, - }, - }, - CheckPayment: { - BankAccountRef: { - value: this.bank_account_ref_value, - name: this.bank_account_ref_name, - }, - }, - }, - params: { - minorversion: this.minorversion, - }, - }); + quickbooks: app, + ...props, }, }; diff --git a/components/quickbooks_sandbox/actions/create-pl-report/create-pl-report.mjs b/components/quickbooks_sandbox/actions/create-pl-report/create-pl-report.mjs new file mode 100644 index 0000000000000..94f8f158c3c7b --- /dev/null +++ b/components/quickbooks_sandbox/actions/create-pl-report/create-pl-report.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/create-pl-report/create-pl-report.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-create-pl-report", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/create-purchase/create-purchase.mjs b/components/quickbooks_sandbox/actions/create-purchase/create-purchase.mjs index 4f13c5ace47bb..fa6d2fd771527 100644 --- a/components/quickbooks_sandbox/actions/create-purchase/create-purchase.mjs +++ b/components/quickbooks_sandbox/actions/create-purchase/create-purchase.mjs @@ -1,85 +1,22 @@ -// legacy_hash_id: a_G1iL4w -import { axios } from "@pipedream/platform"; +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/create-purchase/create-purchase.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); export default { + ...others, key: "quickbooks_sandbox-create-purchase", - name: "Create Purchase", - description: "Creates a purchase.", - version: "0.1.1", - type: "action", + version: "0.1.4", + name, + description, + type, props: { - quickbooks_sandbox: { - type: "app", - app: "quickbooks_sandbox", - }, - line_items: { - type: "any", - description: "Individual line items of a transaction. Valid `Line` type for create: `AccountBasedExpenseLine`", - }, - account_ref_value: { - type: "string", - description: "Specifies the id of the account reference. Check must specify bank account, CreditCard must specify credit card account. Validation Rules:Valid and Active Account Reference of an appropriate type.", - }, - payment_type: { - type: "string", - description: "Payment Type can be: `Cash`, `Check`, or `CreditCard`.", - options: [ - "Cash", - "Check", - "CreditCard", - ], - }, - account_ref_name: { - type: "string", - description: "Specifies the name of the account reference. Check must specify bank account, CreditCard must specify credit card account. Validation Rules:Valid and Active Account Reference of an appropriate type.", - optional: true, - }, - currency_ref_value: { - type: "string", - description: "A three letter string representing the ISO 4217 code for the currency. For example, `USD`, `AUD`, `EUR`, and so on. This must be defined if multicurrency is enabled for the company.\nMulticurrency is enabled for the company if `Preferences.MultiCurrencyEnabled` is set to `true`. Read more about multicurrency support [here](https://developer.intuit.com/docs?RedirectID=MultCurrencies). Required if multicurrency is enabled for the company.", - optional: true, - }, - currency_ref_name: { - type: "object", - description: "The full name of the currency.", - optional: true, - }, - minorversion: { - type: "string", - description: "Use the minorversion query parameter in REST API requests to access a version of the API other than the generally available version. For example, to invoke minor version 1 of the JournalEntry entity, issue the following request:\n`https://quickbooks.api.intuit.com/v3/company//journalentry/entityId?minorversion=1`", - optional: true, - }, - }, - async run({ $ }) { - // See Quickbooks API docs at: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#create-an-invoice - - if (!this.line_items || !this.account_ref_value) { - throw new Error("Must provide line_items, and account_ref_value parameters."); - } - - return await axios($, { - method: "post", - url: `https://sandbox-quickbooks.api.intuit.com/v3/company/${this.quickbooks_sandbox.$auth.company_id}/purchase`, - headers: { - "Authorization": `Bearer ${this.quickbooks_sandbox.$auth.oauth_access_token}`, - "accept": "application/json", - "content-type": "application/json", - }, - data: { - PaymentType: this.payment_type, - AccountRef: { - value: this.account_ref_value, - name: this.account_ref_name, - }, - Line: this.line_items, - CurrencyRef: { - value: this.currency_ref_value, - name: this.currency_ref_name, - }, - }, - params: { - minorversion: this.minorversion, - }, - }); + quickbooks: app, + ...props, }, }; diff --git a/components/quickbooks_sandbox/actions/create-sales-receipt/create-sales-receipt.mjs b/components/quickbooks_sandbox/actions/create-sales-receipt/create-sales-receipt.mjs new file mode 100644 index 0000000000000..78c41804ed3c3 --- /dev/null +++ b/components/quickbooks_sandbox/actions/create-sales-receipt/create-sales-receipt.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/create-sales-receipt/create-sales-receipt.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-create-sales-receipt", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/delete-purchase/delete-purchase.mjs b/components/quickbooks_sandbox/actions/delete-purchase/delete-purchase.mjs new file mode 100644 index 0000000000000..dd09a75fa89ca --- /dev/null +++ b/components/quickbooks_sandbox/actions/delete-purchase/delete-purchase.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/delete-purchase/delete-purchase.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-delete-purchase", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/get-bill/get-bill.mjs b/components/quickbooks_sandbox/actions/get-bill/get-bill.mjs new file mode 100644 index 0000000000000..ff5b9e0f25ac2 --- /dev/null +++ b/components/quickbooks_sandbox/actions/get-bill/get-bill.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/get-bill/get-bill.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-get-bill", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/get-customer/get-customer.mjs b/components/quickbooks_sandbox/actions/get-customer/get-customer.mjs index 2ddb78c66ed88..b3a83d7979a9a 100644 --- a/components/quickbooks_sandbox/actions/get-customer/get-customer.mjs +++ b/components/quickbooks_sandbox/actions/get-customer/get-customer.mjs @@ -1,43 +1,22 @@ -// legacy_hash_id: a_vgidgN -import { axios } from "@pipedream/platform"; +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/get-customer/get-customer.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); export default { + ...others, key: "quickbooks_sandbox-get-customer", - name: "Get Customer", - description: "Returns info about a customer.", - version: "0.1.1", - type: "action", + version: "0.1.4", + name, + description, + type, props: { - quickbooks_sandbox: { - type: "app", - app: "quickbooks_sandbox", - }, - customer_id: { - type: "string", - description: "Id of the customer to get details of.", - }, - minorversion: { - type: "string", - description: "Use the `minorversion` query parameter in REST API requests to access a version of the API other than the generally available version. For example, to invoke minor version 1 of the JournalEntry entity, issue the following request:\n`https://quickbooks.api.intuit.com/v3/company//journalentry/entityId?minorversion=1`", - optional: true, - }, - }, - async run({ $ }) { - // See Quickbooks API docs at: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/customer#read-a-customer - - if (!this.customer_id) { - throw new Error("Must provide customer_id parameter."); - } - - return await axios($, { - url: `https://sandbox-quickbooks.api.intuit.com/v3/company/${this.quickbooks_sandbox.$auth.company_id}/customer/${this.customer_id}`, - headers: { - "Authorization": `Bearer ${this.quickbooks_sandbox.$auth.oauth_access_token}`, - "content-type": "application/json", - }, - params: { - minorversion: this.minorversion, - }, - }); + quickbooks: app, + ...props, }, }; diff --git a/components/quickbooks_sandbox/actions/get-invoice/get-invoice.mjs b/components/quickbooks_sandbox/actions/get-invoice/get-invoice.mjs index 8cfca67a443da..13142eecefa8f 100644 --- a/components/quickbooks_sandbox/actions/get-invoice/get-invoice.mjs +++ b/components/quickbooks_sandbox/actions/get-invoice/get-invoice.mjs @@ -1,44 +1,22 @@ -// legacy_hash_id: a_Q3ix1Q -import { axios } from "@pipedream/platform"; +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/get-invoice/get-invoice.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); export default { + ...others, key: "quickbooks_sandbox-get-invoice", - name: "Get Invoice", - description: "Returns info about an invoice.", - version: "0.1.1", - type: "action", + version: "0.1.4", + name, + description, + type, props: { - quickbooks_sandbox: { - type: "app", - app: "quickbooks_sandbox", - }, - invoice_id: { - type: "string", - description: "Id of the invoice to get details of.", - }, - minorversion: { - type: "string", - description: "Use the `minorversion` query parameter in REST API requests to access a version of the API other than the generally available version. For example, to invoke minor version 1 of the JournalEntry entity, issue the following request:\n`https://quickbooks.api.intuit.com/v3/company//journalentry/entityId?minorversion=1`", - optional: true, - }, - }, - async run({ $ }) { - // See Quickbooks API docs at: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#read-an-invoice - - if (!this.invoice_id) { - throw new Error("Must provide invoice_id parameter."); - } - - return await axios($, { - url: `https://sandbox-quickbooks.api.intuit.com/v3/company/${this.quickbooks_sandbox.$auth.company_id}/invoice/${this.invoice_id}`, - headers: { - "Authorization": `Bearer ${this.quickbooks_sandbox.$auth.oauth_access_token}`, - "accept": "application/json", - "content-type": "application/json", - }, - params: { - minorversion: this.minorversion, - }, - }); + quickbooks: app, + ...props, }, }; diff --git a/components/quickbooks_sandbox/actions/get-my-company/get-my-company.mjs b/components/quickbooks_sandbox/actions/get-my-company/get-my-company.mjs new file mode 100644 index 0000000000000..34fad046eb73e --- /dev/null +++ b/components/quickbooks_sandbox/actions/get-my-company/get-my-company.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/get-my-company/get-my-company.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-get-my-company", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/get-payment/get-payment.mjs b/components/quickbooks_sandbox/actions/get-payment/get-payment.mjs index a57162263dff3..78845c7d4fd84 100644 --- a/components/quickbooks_sandbox/actions/get-payment/get-payment.mjs +++ b/components/quickbooks_sandbox/actions/get-payment/get-payment.mjs @@ -1,43 +1,22 @@ -// legacy_hash_id: a_0Mi7LY -import { axios } from "@pipedream/platform"; +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/get-payment/get-payment.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); export default { + ...others, key: "quickbooks_sandbox-get-payment", - name: "Get Payment", - description: "Returns info about a payment.", - version: "0.1.1", - type: "action", + version: "0.1.4", + name, + description, + type, props: { - quickbooks_sandbox: { - type: "app", - app: "quickbooks_sandbox", - }, - payment_id: { - type: "string", - description: "Id of the item to get details of.", - }, - minorversion: { - type: "string", - description: "Use the `minorversion` query parameter in REST API requests to access a version of the API other than the generally available version. For example, to invoke minor version 1 of the JournalEntry entity, issue the following request:\n`https://quickbooks.api.intuit.com/v3/company//journalentry/entityId?minorversion=1`", - optional: true, - }, - }, - async run({ $ }) { - //See Quickbooks API docs at: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/payment#read-a-payment - - if (!this.payment_id) { - throw new Error("Must provide payment_id parameter."); - } - - return await axios($, { - url: `https://sandbox-quickbooks.api.intuit.com/v3/company/${this.quickbooks_sandbox.$auth.company_id}/payment/${this.payment_id}`, - headers: { - "Authorization": `Bearer ${this.quickbooks_sandbox.$auth.oauth_access_token}`, - "content-type": "application/json", - }, - params: { - minorversion: this.minorversion, - }, - }); + quickbooks: app, + ...props, }, }; diff --git a/components/quickbooks_sandbox/actions/get-purchase-order/get-purchase-order.mjs b/components/quickbooks_sandbox/actions/get-purchase-order/get-purchase-order.mjs new file mode 100644 index 0000000000000..b467ec743d2eb --- /dev/null +++ b/components/quickbooks_sandbox/actions/get-purchase-order/get-purchase-order.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/get-purchase-order/get-purchase-order.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-get-purchase-order", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/get-purchase/get-purchase.mjs b/components/quickbooks_sandbox/actions/get-purchase/get-purchase.mjs new file mode 100644 index 0000000000000..4c0f9cad9d416 --- /dev/null +++ b/components/quickbooks_sandbox/actions/get-purchase/get-purchase.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/get-purchase/get-purchase.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-get-purchase", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/get-sales-receipt/get-sales-receipt.mjs b/components/quickbooks_sandbox/actions/get-sales-receipt/get-sales-receipt.mjs new file mode 100644 index 0000000000000..e570e9c2b6808 --- /dev/null +++ b/components/quickbooks_sandbox/actions/get-sales-receipt/get-sales-receipt.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/get-sales-receipt/get-sales-receipt.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-get-sales-receipt", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/get-time-activity/get-time-activity.mjs b/components/quickbooks_sandbox/actions/get-time-activity/get-time-activity.mjs new file mode 100644 index 0000000000000..fe9bda76f8d1b --- /dev/null +++ b/components/quickbooks_sandbox/actions/get-time-activity/get-time-activity.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/get-time-activity/get-time-activity.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-get-time-activity", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/search-account/search-account.mjs b/components/quickbooks_sandbox/actions/search-account/search-account.mjs deleted file mode 100644 index fcad08115820f..0000000000000 --- a/components/quickbooks_sandbox/actions/search-account/search-account.mjs +++ /dev/null @@ -1,84 +0,0 @@ -// legacy_hash_id: a_vgid15 -import { axios } from "@pipedream/platform"; - -export default { - key: "quickbooks_sandbox-search-account", - name: "Search Account", - description: "Searches for accounts.", - version: "0.1.1", - type: "action", - props: { - quickbooks_sandbox: { - type: "app", - app: "quickbooks_sandbox", - }, - include_clause: { - type: "string", - description: "Fields to use in the include clause of the Account data query. See query language syntax, limitations, and other specifications on [Data queries](https://developer.intuit.com/app/developer/qbo/docs/develop/explore-the-quickbooks-online-api/data-queries)", - }, - where_clause: { - type: "string", - description: "Filters to use in the where clause of the Account data query. Note: Multiple clauses (filters) are AND'd. The OR operation is not supported.", - }, - order_clause: { - type: "string", - description: "The `order_clause` is for sorting the result. Include the Account property to sort by. The default sort order is ascending; to indicate descending sort order, include DESC, for example: `Name DESC`", - optional: true, - }, - start_position: { - type: "string", - description: "The starting count of the response for pagination.", - optional: true, - }, - max_results: { - type: "string", - description: "The number of Account entity elements in the response.", - optional: true, - }, - minorversion: { - type: "string", - description: "Use the `minorversion` query parameter in REST API requests to access a version of the API other than the generally available version. For example, to invoke minor version 1 of the JournalEntry entity, issue the following request:\n`https://quickbooks.api.intuit.com/v3/company//journalentry/entityId?minorversion=1`", - optional: true, - }, - }, - async run({ $ }) { - //See Quickbooks API docs at: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account#query-an-account - - if (!this.include_clause || !this.where_clause) { - throw new Error("Must provide include_clause, where_clause parameters."); - } - - //Prepares OrderBy clause,start position, max results parameters to be used in the request's query parameter. - var orderClause = ""; - if (this.order_clause) { - orderClause = ` ORDERBY ${this.order_clause}`; - } - - var startPosition = ""; - if (this.start_position) { - startPosition = ` STARTPOSITION ${this.start_position}`; - } - - var maxResults = ""; - if (this.max_results) { - maxResults = ` MAXRESULTS ${this.max_results}` || ""; - } - - //Prepares the request's query parameter - const query = `select ${this.include_clause} from Account where ${this.where_clause}${orderClause}${startPosition}${maxResults}`; - - //Sends the request against Quickbooks API - return await axios($, { - url: `https://sandbox-quickbooks.api.intuit.com/v3/company/${this.quickbooks_sandbox.$auth.company_id}/query`, - headers: { - "Authorization": `Bearer ${this.quickbooks_sandbox.$auth.oauth_access_token}`, - "accept": "application/json", - "content-type": "application/octet-stream", - }, - params: { - minorversion: this.minorversion, - query, - }, - }); - }, -}; diff --git a/components/quickbooks_sandbox/actions/search-accounts/search-accounts.mjs b/components/quickbooks_sandbox/actions/search-accounts/search-accounts.mjs new file mode 100644 index 0000000000000..2c630b08dc129 --- /dev/null +++ b/components/quickbooks_sandbox/actions/search-accounts/search-accounts.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/search-accounts/search-accounts.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-search-accounts", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/search-customers/search-customers.mjs b/components/quickbooks_sandbox/actions/search-customers/search-customers.mjs index b4d284cbbe780..d2cf04e949937 100644 --- a/components/quickbooks_sandbox/actions/search-customers/search-customers.mjs +++ b/components/quickbooks_sandbox/actions/search-customers/search-customers.mjs @@ -1,84 +1,22 @@ -// legacy_hash_id: a_Xzi4P0 -import { axios } from "@pipedream/platform"; +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/search-customers/search-customers.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); export default { + ...others, key: "quickbooks_sandbox-search-customers", - name: "Search Customers", - description: "Searches for customers.", - version: "0.1.1", - type: "action", + version: "0.1.4", + name, + description, + type, props: { - quickbooks_sandbox: { - type: "app", - app: "quickbooks_sandbox", - }, - include_clause: { - type: "string", - description: "Fields to use in the select clause of the Customer data query. See query language syntax, limitations, and other specifications on [Data queries](https://developer.intuit.com/app/developer/qbo/docs/develop/explore-the-quickbooks-online-api/data-queries)", - }, - where_clause: { - type: "string", - description: "Filters to use in the where clause of the Customer data query. Note: Multiple clauses (filters) are AND'd. The OR operation is not supported.", - }, - order_clause: { - type: "string", - description: "The `order_clause` is for sorting the result. Include the Customer property to sort by. The default sort order is ascending; to indicate descending sort order, include DESC, for example: `Name DESC`", - optional: true, - }, - start_position: { - type: "string", - description: "The starting count of the response for pagination.", - optional: true, - }, - max_results: { - type: "string", - description: "The number of Customer entity elements in the response.", - optional: true, - }, - minorversion: { - type: "string", - description: "Use the `minorversion` query parameter in REST API requests to access a version of the API other than the generally available version. For example, to invoke minor version 1 of the JournalEntry entity, issue the following request:\n`https://quickbooks.api.intuit.com/v3/company//journalentry/entityId?minorversion=1`", - optional: true, - }, - }, - async run({ $ }) { - //See Quickbooks API docs at: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/customer#query-a-customer - - if (!this.include_clause || !this.where_clause) { - throw new Error("Must provide include_clause, where_clause parameters."); - } - - //Prepares OrderBy clause,start position, max results parameters to be used in the request's query parameter. - var orderClause = ""; - if (this.order_clause) { - orderClause = ` ORDERBY ${this.order_clause}`; - } - - var startPosition = ""; - if (this.start_position) { - startPosition = ` STARTPOSITION ${this.start_position}`; - } - - var maxResults = ""; - if (this.max_results) { - maxResults = ` MAXRESULTS ${this.max_results}` || ""; - } - - //Prepares the request's query parameter - const query = `select ${this.include_clause} from Customer where ${this.where_clause}${orderClause}${startPosition}${maxResults}`; - - //Sends the request against Quickbooks API - return await axios($, { - url: `https://sandbox-quickbooks.api.intuit.com/v3/company/${this.quickbooks_sandbox.$auth.company_id}/query`, - headers: { - "Authorization": `Bearer ${this.quickbooks_sandbox.$auth.oauth_access_token}`, - "accept": "application/json", - "content-type": "application/octet-stream", - }, - params: { - minorversion: this.minorversion, - query, - }, - }); + quickbooks: app, + ...props, }, }; diff --git a/components/quickbooks_sandbox/actions/search-invoices/search-invoices.mjs b/components/quickbooks_sandbox/actions/search-invoices/search-invoices.mjs index 80e92dc14c889..a855674fe7a0b 100644 --- a/components/quickbooks_sandbox/actions/search-invoices/search-invoices.mjs +++ b/components/quickbooks_sandbox/actions/search-invoices/search-invoices.mjs @@ -1,84 +1,22 @@ -// legacy_hash_id: a_1Wi7KO -import { axios } from "@pipedream/platform"; +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/search-invoices/search-invoices.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); export default { + ...others, key: "quickbooks_sandbox-search-invoices", - name: "Search Invoices", - description: "Searches for invoices.", - version: "0.1.1", - type: "action", + version: "0.1.4", + name, + description, + type, props: { - quickbooks_sandbox: { - type: "app", - app: "quickbooks_sandbox", - }, - include_clause: { - type: "string", - description: "Fields to use in the select clause of the Invoice data query. See query language syntax, limitations, and other specifications on [Data queries](https://developer.intuit.com/app/developer/qbo/docs/develop/explore-the-quickbooks-online-api/data-queries)", - }, - where_clause: { - type: "string", - description: "Filters to use in the where clause of the Invoice data query. Note: Multiple clauses (filters) are AND'd. The OR operation is not supported.", - }, - order_clause: { - type: "string", - description: "The `order_clause` is for sorting the result. Include the Invoice property to sort by. The default sort order is ascending; to indicate descending sort order, include DESC, for example: `Name DESC`", - optional: true, - }, - start_position: { - type: "string", - description: "The starting count of the response for pagination.", - optional: true, - }, - max_results: { - type: "string", - description: "The number of Invoice entity elements in the response.", - optional: true, - }, - minorversion: { - type: "string", - description: "Use the `minorversion` query parameter in REST API requests to access a version of the API other than the generally available version. For example, to invoke minor version 1 of the JournalEntry entity, issue the following request:\n`https://quickbooks.api.intuit.com/v3/company//journalentry/entityId?minorversion=1`", - optional: true, - }, - }, - async run({ $ }) { - //See Quickbooks API docs at: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#query-an-invoice - - if (!this.include_clause || !this.where_clause) { - throw new Error("Must provide include_clause, where_clause parameters."); - } - - //Prepares OrderBy clause,start position, max results parameters to be used in the request's query parameter. - var orderClause = ""; - if (this.order_clause) { - orderClause = ` ORDERBY ${this.order_clause}`; - } - - var startPosition = ""; - if (this.start_position) { - startPosition = ` STARTPOSITION ${this.start_position}`; - } - - var maxResults = ""; - if (this.max_results) { - maxResults = ` MAXRESULTS ${this.max_results}` || ""; - } - - //Prepares the request's query parameter - const query = `select ${this.include_clause} from Invoice where ${this.where_clause}${orderClause}${startPosition}${maxResults}`; - - //Sends the request against Quickbooks API - return await axios($, { - url: `https://sandbox-quickbooks.api.intuit.com/v3/company/${this.quickbooks_sandbox.$auth.company_id}/query`, - headers: { - "Authorization": `Bearer ${this.quickbooks_sandbox.$auth.oauth_access_token}`, - "accept": "application/json", - "content-type": "application/octet-stream", - }, - params: { - minorversion: this.minorversion, - query, - }, - }); + quickbooks: app, + ...props, }, }; diff --git a/components/quickbooks_sandbox/actions/search-items/search-items.mjs b/components/quickbooks_sandbox/actions/search-items/search-items.mjs new file mode 100644 index 0000000000000..9b76f89f85b7f --- /dev/null +++ b/components/quickbooks_sandbox/actions/search-items/search-items.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/search-items/search-items.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-search-items", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/search-products/search-products.mjs b/components/quickbooks_sandbox/actions/search-products/search-products.mjs new file mode 100644 index 0000000000000..c2ed30be53bfc --- /dev/null +++ b/components/quickbooks_sandbox/actions/search-products/search-products.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/search-products/search-products.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-search-products", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/search-purchases/search-purchases.mjs b/components/quickbooks_sandbox/actions/search-purchases/search-purchases.mjs new file mode 100644 index 0000000000000..7f917886ce2fa --- /dev/null +++ b/components/quickbooks_sandbox/actions/search-purchases/search-purchases.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/search-purchases/search-purchases.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-search-purchases", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/search-query/search-query.mjs b/components/quickbooks_sandbox/actions/search-query/search-query.mjs new file mode 100644 index 0000000000000..4eba4f59e5ede --- /dev/null +++ b/components/quickbooks_sandbox/actions/search-query/search-query.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/search-query/search-query.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-search-query", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/search-services/search-services.mjs b/components/quickbooks_sandbox/actions/search-services/search-services.mjs index e5d765df8f20e..1a8ef097e5758 100644 --- a/components/quickbooks_sandbox/actions/search-services/search-services.mjs +++ b/components/quickbooks_sandbox/actions/search-services/search-services.mjs @@ -1,84 +1,22 @@ -// legacy_hash_id: a_B0i8lr -import { axios } from "@pipedream/platform"; +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/search-services/search-services.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); export default { + ...others, key: "quickbooks_sandbox-search-services", - name: "Search Services", - description: "Search for services.", - version: "0.1.1", - type: "action", + version: "0.1.4", + name, + description, + type, props: { - quickbooks_sandbox: { - type: "app", - app: "quickbooks_sandbox", - }, - include_clause: { - type: "string", - description: "Fields to use in the select clause of the Item data query. See query language syntax, limitations, and other specifications on [Data queries](https://developer.intuit.com/app/developer/qbo/docs/develop/explore-the-quickbooks-online-api/data-queries)", - }, - where_clause: { - type: "string", - description: "Filters to use in the where clause of the Item data query. Note: Multiple clauses (filters) are AND'd. The OR operation is not supported.", - }, - order_clause: { - type: "string", - description: "The `order_clause` is for sorting the result. Include the Item property to sort by. The default sort order is ascending; to indicate descending sort order, include DESC, for example: `Name DESC`", - optional: true, - }, - start_position: { - type: "string", - description: "The starting count of the response for pagination.", - optional: true, - }, - max_results: { - type: "string", - description: "The number of Item entity elements in the response.", - optional: true, - }, - minorversion: { - type: "string", - description: "Use the `minorversion` query parameter in REST API requests to access a version of the API other than the generally available version. For example, to invoke minor version 1 of the JournalEntry entity, issue the following request:\n`https://quickbooks.api.intuit.com/v3/company//journalentry/entityId?minorversion=1`", - optional: true, - }, - }, - async run({ $ }) { - //See Quickbooks API docs at: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#query-an-item - - if (!this.include_clause || !this.where_clause) { - throw new Error("Must provide include_clause, where_clause parameters."); - } - - //Prepares OrderBy clause,start position, max results parameters to be used in the request's query parameter. - var orderClause = ""; - if (this.order_clause) { - orderClause = ` ORDERBY ${this.order_clause}`; - } - - var startPosition = ""; - if (this.start_position) { - startPosition = ` STARTPOSITION ${this.start_position}`; - } - - var maxResults = ""; - if (this.max_results) { - maxResults = ` MAXRESULTS ${this.max_results}` || ""; - } - - //Prepares the request's query parameter - const query = `select ${this.include_clause} from Item where Type = 'Service' and ${this.where_clause}${orderClause}${startPosition}${maxResults}`; - - //Sends the request against Quickbooks API - return await axios($, { - url: `https://sandbox-quickbooks.api.intuit.com/v3/company/${this.quickbooks_sandbox.$auth.company_id}/query`, - headers: { - "Authorization": `Bearer ${this.quickbooks_sandbox.$auth.oauth_access_token}`, - "accept": "application/json", - "content-type": "application/octet-stream", - }, - params: { - minorversion: this.minorversion, - query, - }, - }); + quickbooks: app, + ...props, }, }; diff --git a/components/quickbooks_sandbox/actions/search-time-activities/search-time-activities.mjs b/components/quickbooks_sandbox/actions/search-time-activities/search-time-activities.mjs new file mode 100644 index 0000000000000..69f116c932602 --- /dev/null +++ b/components/quickbooks_sandbox/actions/search-time-activities/search-time-activities.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/search-time-activities/search-time-activities.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-search-time-activities", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/search-vendors/search-vendors.mjs b/components/quickbooks_sandbox/actions/search-vendors/search-vendors.mjs new file mode 100644 index 0000000000000..c5aff097cdef3 --- /dev/null +++ b/components/quickbooks_sandbox/actions/search-vendors/search-vendors.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/search-vendors/search-vendors.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-search-vendors", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/sparse-update-invoice/sparse-update-invoice.mjs b/components/quickbooks_sandbox/actions/sparse-update-invoice/sparse-update-invoice.mjs new file mode 100644 index 0000000000000..e2abd4bc427f0 --- /dev/null +++ b/components/quickbooks_sandbox/actions/sparse-update-invoice/sparse-update-invoice.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/sparse-update-invoice/sparse-update-invoice.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-sparse-update-invoice", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/update-customer/update-customer.mjs b/components/quickbooks_sandbox/actions/update-customer/update-customer.mjs new file mode 100644 index 0000000000000..1a94dd7c33d2b --- /dev/null +++ b/components/quickbooks_sandbox/actions/update-customer/update-customer.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/update-customer/update-customer.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-update-customer", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/actions/update-item/update-item.mjs b/components/quickbooks_sandbox/actions/update-item/update-item.mjs index 20ed4528da3aa..9c4c458871aeb 100644 --- a/components/quickbooks_sandbox/actions/update-item/update-item.mjs +++ b/components/quickbooks_sandbox/actions/update-item/update-item.mjs @@ -1,319 +1,22 @@ -// legacy_hash_id: a_bKil4n -import { axios } from "@pipedream/platform"; +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/actions/update-item/update-item.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); export default { + ...others, key: "quickbooks_sandbox-update-item", - name: "Update Item", - description: "Updates an item.", - version: "0.2.1", - type: "action", + version: "0.2.4", + name, + description, + type, props: { - quickbooks_sandbox: { - type: "app", - app: "quickbooks_sandbox", - }, - item_id: { - type: "string", - description: "Id of the item to update.", - }, - name: { - type: "string", - description: "Name of the item. This value must be unique.", - }, - sync_token: { - type: "string", - description: "Version number of the entity. Required for the update operation.", - }, - track_qty_on_hand: { - type: "boolean", - description: "True if there is quantity on hand to be tracked. Once this value is true, it cannot be updated to false. Applicable for items of type `Inventory`. Not applicable for `Service` or `NonInventory` item types.", - }, - sparse_update: { - type: "string", - description: "When set to `true`, sparse updating provides the ability to update a subset of properties for a given object; only elements specified in the request are updated. Missing elements are left untouched.", - }, - qty_on_hand: { - type: "string", - description: "Current quantity of the `Inventory` items available for sale. Not used for `Service` or `NonInventory` type items. Required for `Inventory` type items.", - optional: true, - }, - income_account_ref_value: { - type: "string", - description: "Reference to the posting account, that is, the account that records the proceeds from the sale of this item. Must be an account with account type of `Sales of Product Income`. Query the Account name list resource to determine the appropriate Account object for this reference. Use `Account.Id` from that object for `IncomeAccountRef.value`. See specifications for the IncomeAccountRef parameters in the [Create an item page](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#create-an-item).", - optional: true, - }, - income_account_ref_name: { - type: "string", - description: "Reference to the posting account, that is, the account that records the proceeds from the sale of this item. Must be an account with account type of `Sales of Product Income`. Query the Account name list resource to determine the appropriate Account object for this reference. Use `Account.Name` from that object for `IncomeAccountRef.name`. See specifications for the IncomeAccountRef parameters in the [Create an item page](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#create-an-item).", - optional: true, - }, - type: { - type: "string", - description: "Classification that specifies the use of this item. See the description at the top of the [Item entity page](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item) for details about supported item types. See specifications for the type parameter in the [Create an item page](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#create-an-item).", - optional: true, - options: [ - "Inventory", - "Group", - "Service", - "NonInventory", - ], - }, - asset_account_ref_value: { - type: "string", - description: "Required for Inventory item types. Reference to the Inventory Asset account that tracks the current value of the inventory. If the same account is used for all inventory items, the current balance of this account will represent the current total value of the inventory. Must be an account with account type of `Other Current Asset`. Query the Account name list resource to determine the appropriate Account object for this reference. Use `Account.Id` from that object for `AssetAccountRef.value`.", - optional: true, - }, - asset_account_ref_name: { - type: "string", - description: "Required for Inventory item types. Reference to the Inventory Asset account that tracks the current value of the inventory. If the same account is used for all inventory items, the current balance of this account will represent the current total value of the inventory. Must be an account with account type of `Other Current Asset`. Query the Account name list resource to determine the appropriate Account object for this reference. Use `Account.Name` from that object for `AssetAccountRef.name`.", - optional: true, - }, - inv_start_date: { - type: "string", - description: "Date of opening balance for the inventory transaction. Required when creating an `Item.Type=Inventory`. Required for `Inventory` item types.", - optional: true, - }, - expense_account_ref_value: { - type: "string", - description: "Reference to the expense account used to pay the vendor for this item. Must be an account with account type of `Cost of Goods Sold`. Query the Account name list resource to determine the appropriate Account object for this reference. Use `Account.Id` from that object for `ExpenseAccountRef.value`. See specifications for the ExpenseAccountRef parameters in the [Create an item page](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#create-an-item).", - optional: true, - }, - expense_account_ref_name: { - type: "string", - description: "Reference to the expense account used to pay the vendor for this item. Must be an account with account type of `Cost of Goods Sold`. Query the Account name list resource to determine the appropriate Account object for this reference. Use `Account.Name` from that object for `ExpenseAccountRef.name`. See specifications for the ExpenseAccountRef parameters in the [Create an item page](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#create-an-item).", - optional: true, - }, - Sku: { - type: "string", - description: "The stock keeping unit (SKU) for this Item. This is a company-defined identifier for an item or product used in tracking inventory.", - optional: true, - }, - sales_tax_included: { - type: "string", - description: "True if the sales tax is included in the item amount, and therefore is not calculated for the transaction.", - optional: true, - }, - sales_tax_code_ref_value: { - type: "string", - description: "Id of the referenced sales tax code for the Sales item. Applicable to Service and Sales item types only. Query the TaxCode name list resource to determine the appropriate TaxCode object for this reference. Use `TaxCode.Id` from that object for `SalesTaxCodeRef.value`.", - optional: true, - }, - sales_tax_code_ref_name: { - type: "string", - description: "Name of the referenced sales tax code for the Sales item. Applicable to Service and Sales item types only. Query the TaxCode name list resource to determine the appropriate TaxCode object for this reference. Use `TaxCode.Name` from that object for `SalesTaxCodeRef.name`.", - optional: true, - }, - class_ref_value: { - type: "string", - description: "Id of the referenced Class for the item. Query the Class name list resource to determine the appropriate object for this reference. Use `Class.Id` from that object for `ClassRef.value`.", - optional: true, - }, - class_ref_name: { - type: "string", - description: "Name of the referenced Class for the item. Query the Class name list resource to determine the appropriate object for this reference. Use `Class.Name` from that object for `ClassRef.name`.", - optional: true, - }, - purchase_tax_tncluded: { - type: "boolean", - description: "True if the purchase tax is included in the item amount, and therefore is not calculated for the transaction.", - optional: true, - }, - description: { - type: "string", - description: "Description of the item.", - optional: true, - }, - abatement_rate: { - type: "string", - description: "Sales tax abatement rate for India locales.", - optional: true, - }, - reverse_charge_rate: { - type: "string", - description: "Sales tax reverse charge rate for India locales.", - optional: true, - }, - sub_item: { - type: "boolean", - description: "If true, this is a sub item. If false or null, this is a top-level item. Creating inventory hierarchies with traditional inventory items is being phased out in lieu of using categories and sub categories.", - optional: true, - }, - taxable: { - type: "boolean", - description: "If true, transactions for this item are taxable. Applicable to US companies, only.", - optional: true, - }, - UQC_display_text: { - type: "string", - description: "Text to be displayed on customer's invoice to denote the Unit of Measure (instead of the standard code).", - optional: true, - }, - reorder_point: { - type: "string", - description: "The minimum quantity of a particular inventory item that you need to restock at any given time. The ReorderPoint value cannot be set to null for sparse updates(sparse=true). It can be set to null only for full updates.", - optional: true, - }, - purchase_desc: { - type: "string", - description: "Purchase description for the item.", - optional: true, - }, - pref_vendor_ref_value: { - type: "string", - optional: true, - }, - pref_vendor_ref_name: { - type: "string", - optional: true, - }, - active: { - type: "boolean", - description: "If true, the object is currently enabled for use by QuickBooks.", - optional: true, - }, - UQC_id: { - type: "string", - description: "Id of Standard Unit of Measure (UQC:Unique Quantity Code) of the item according to GST rule.", - optional: true, - }, - purchase_tax_code_ref_value: { - type: "string", - description: "The ID for the referenced purchase tax code object as found in the Id field of the object payload. \n\nReference to the purchase tax code for the item. Applicable to Service, Other Charge, and Product (Non-Inventory) item types. Query the TaxCode name list resource to determine the appropriate TaxCode object for this reference. Use `TaxCode.Id` from that object for `PurchaseTaxCodeRef.value`.", - optional: true, - }, - purchase_tax_code_ref_name: { - type: "string", - description: "An identifying name for the purchase tax code object being referenced by value and is derived from the field that holds the common name of that object. \n\nReference to the purchase tax code for the item. Applicable to Service, Other Charge, and Product (Non-Inventory) item types. Query the TaxCode name list resource to determine the appropriate TaxCode object for this reference. Use `TaxCode.Name` from that object for `PurchaseTaxCodeRef.name`.", - optional: true, - }, - service_type: { - type: "string", - description: "Sales tax service type for India locales.", - optional: true, - }, - purchase_cost: { - type: "string", - description: "Amount paid when buying or ordering the item, as expressed in the home currency.", - optional: true, - }, - unit_price: { - type: "string", - description: "Corresponds to the Price/Rate column on the QuickBooks Online UI to specify either unit price, a discount, or a tax rate for item. If used for unit price, the monetary value of the service or product, as expressed in the home currency. If used for a discount or tax rate, express the percentage as a fraction. For example, specify `0.4` for 40% tax", - optional: true, - }, - tax_classification_ref_value: { - type: "string", - description: "The ID for the referenced Tax classification object as found in the Id field of the object payload.\n\nTax classification segregates different items into different classifications and the tax classification is one of the key parameters to determine appropriate tax on transactions involving items. Tax classifications are sourced by either tax governing authorities as in India/Malaysia or externally like Exactor. 'Fuel', 'Garments' and 'Soft drinks' are a few examples of tax classification in layman terms. User can choose a specific tax classification for an item while creating it. A level 1 tax classification cannot be associated to an Item", - optional: true, - }, - tax_classification_ref_name: { - type: "string", - description: "An identifying name for the Tax classification object being referenced by value and is derived from the field that holds the common name of that object.", - optional: true, - }, - parent_ref_name: { - type: "string", - description: "An identifying name for the parent item object being referenced by `value` and is derived from the field that holds the common name of that object.\n\nThe immediate parent of the sub item in the hierarchical Category:Sub-category list. If SubItem is true, then ParenRef is required. Query the Item name list resource to determine the appropriate object for this reference. Use `Item.Id` from that object for `ParentRef.value`.", - optional: true, - }, - parent_ref_value: { - type: "string", - description: "The ID for the referenced parent item object as found in the Id field of the object payload. \n\nThe immediate parent of the sub item in the hierarchical Category:Sub-category list. If SubItem is true, then ParenRef is required. Query the Item name list resource to determine the appropriate object for this reference. Use `Item.Id` from that object for `ParentRef.value`.", - optional: true, - }, - minorversion: { - type: "string", - description: "Use the minorversion query parameter in REST API requests to access a version of the API other than the generally available version. For example, to invoke minor version 1 of the JournalEntry entity, issue the following request:\n`https://quickbooks.api.intuit.com/v3/company//journalentry/entityId?minorversion=1`", - optional: true, - }, - }, - async run({ $ }) { - //See Quickbooks API docs at: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#full-update-an-item - - if (!this.item_id || !this.name || !this.sync_token || this.track_qty_on_hand === undefined || this.sparse_update === undefined) { - throw new Error("Must provide item_id, name, sync_token, and track_qty_on_hand parameters."); - } - - //Prepares the request body - var data = { - sparse: this.sparse_update, - Id: this.item_id, - Name: this.name, - QtyOnHand: this.qty_on_hand, - SyncToken: this.sync_token, - IncomeAccountRef: { - value: this.income_account_ref_value, - name: this.income_account_ref_name, - }, - Type: this.type, - AssetAccountRef: { - value: this.asset_account_ref_value, - name: this.asset_account_ref_name, - }, - InvStartDate: this.inv_start_date, - ExpenseAccountRef: { - value: this.expense_account_ref_value, - name: this.expense_account_ref_name, - }, - Sku: this.Sku, - SalesTaxIncluded: this.sales_tax_included, - TrackQtyOnHand: this.track_qty_on_hand, - SalesTaxCodeRef: { - value: this.sales_tax_code_ref_value, - name: this.sales_tax_code_ref_name, - }, - ClassRef: { - value: this.class_ref_value, - name: this.class_ref_name, - }, - PurchaseTaxIncluded: this.purchase_tax_tncluded, - Description: this.description, - AbatementRate: this.abatement_rate, - ReverseChargeRate: this.reverse_charge_rate, - SubItem: this.sub_item, - Taxable: this.taxable, - UQCDisplayText: this.UQC_display_text, - ReorderPoint: this.reorder_point, - PurchaseDesc: this.purchase_desc, - PrefVendorRef: { - value: this.pref_vendor_ref_value, - name: this.pref_vendor_ref_name, - }, - Active: this.active, - UQCId: this.UQC_id, - PurchaseTaxCodeRef: { - value: this.purchase_tax_code_ref_value, - name: this.purchase_tax_code_ref_name, - }, - ServiceType: this.service_type, - PurchaseCost: this.purchase_cost, - UnitPrice: this.unit_price, - TaxClassificationRef: { - value: this.tax_classification_ref_value, - name: this.tax_classification_ref_name, - }, - }; - - if (this.pref_vendor_ref_value || this.parent_ref_name) { - data["ParentRef"] = { - value: this.parent_ref_value, - name: this.parent_ref_name, - }; - } - - //Sends the request against Quickbooks Sandbox API - return await axios($, { - method: "post", - url: `https://sandbox-quickbooks.api.intuit.com/v3/company/${this.quickbooks_sandbox.$auth.company_id}/item`, - headers: { - "Authorization": `Bearer ${this.quickbooks_sandbox.$auth.oauth_access_token}`, - "accept": "application/json", - "content-type": "application/json", - }, - data, - params: { - minorversion: this.minorversion, - }, - }); + quickbooks: app, + ...props, }, }; diff --git a/components/quickbooks_sandbox/common/utils.mjs b/components/quickbooks_sandbox/common/utils.mjs new file mode 100644 index 0000000000000..d42df055ddeb0 --- /dev/null +++ b/components/quickbooks_sandbox/common/utils.mjs @@ -0,0 +1,40 @@ +export function adjustPropDefinitions(props, app) { + return Object.fromEntries( + Object.entries(props).map(([ + key, + prop, + ]) => { + if (typeof prop === "string") return [ + key, + prop, + ]; + const { + propDefinition, ...otherValues + } = prop; + if (propDefinition) { + const [ + , ...otherDefs + ] = propDefinition; + return [ + key, + { + propDefinition: [ + app, + ...otherDefs, + ], + ...otherValues, + }, + ]; + } + return [ + key, + otherValues.type === "app" + ? null + : otherValues, + ]; + }) + .filter(([ + , value, + ]) => value), + ); +} diff --git a/components/quickbooks_sandbox/package.json b/components/quickbooks_sandbox/package.json new file mode 100644 index 0000000000000..cd384f3c8652b --- /dev/null +++ b/components/quickbooks_sandbox/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/quickbooks_sandbox", + "version": "0.1.2", + "description": "Pipedream Quickbooks Sandbox Components", + "main": "quickbooks_sandbox.app.mjs", + "keywords": [ + "pipedream", + "quickbooks_sandbox" + ], + "homepage": "https://pipedream.com/apps/quickbooks_sandbox", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/quickbooks": "^0.5.1" + } +} diff --git a/components/quickbooks_sandbox/quickbooks_sandbox.app.mjs b/components/quickbooks_sandbox/quickbooks_sandbox.app.mjs index 8fc6bc9dc64db..a8811530c1e50 100644 --- a/components/quickbooks_sandbox/quickbooks_sandbox.app.mjs +++ b/components/quickbooks_sandbox/quickbooks_sandbox.app.mjs @@ -1,11 +1,13 @@ +import common from "@pipedream/quickbooks"; + export default { + ...common, type: "app", app: "quickbooks_sandbox", - propDefinitions: {}, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + ...common.methods, + _apiUrl() { + return "https://sandbox-quickbooks.api.intuit.com/v3"; }, }, }; diff --git a/components/quickbooks_sandbox/sources/new-customer-created/new-customer-created.mjs b/components/quickbooks_sandbox/sources/new-customer-created/new-customer-created.mjs new file mode 100644 index 0000000000000..ed1fba5222c6c --- /dev/null +++ b/components/quickbooks_sandbox/sources/new-customer-created/new-customer-created.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/sources/new-customer-created/new-customer-created.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-new-customer-created", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/sources/new-customer-updated/new-customer-updated.mjs b/components/quickbooks_sandbox/sources/new-customer-updated/new-customer-updated.mjs new file mode 100644 index 0000000000000..5c6b74c97fd63 --- /dev/null +++ b/components/quickbooks_sandbox/sources/new-customer-updated/new-customer-updated.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/sources/new-customer-updated/new-customer-updated.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-new-customer-updated", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/sources/new-employee-created/new-employee-created.mjs b/components/quickbooks_sandbox/sources/new-employee-created/new-employee-created.mjs new file mode 100644 index 0000000000000..2faa77fc0640c --- /dev/null +++ b/components/quickbooks_sandbox/sources/new-employee-created/new-employee-created.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/sources/new-employee-created/new-employee-created.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-new-employee-created", + version: "0.0.4", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/sources/new-employee-updated/new-employee-updated.mjs b/components/quickbooks_sandbox/sources/new-employee-updated/new-employee-updated.mjs new file mode 100644 index 0000000000000..d1dad91782015 --- /dev/null +++ b/components/quickbooks_sandbox/sources/new-employee-updated/new-employee-updated.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/sources/new-employee-updated/new-employee-updated.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-new-employee-updated", + version: "0.0.4", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/sources/new-invoice-created/new-invoice-created.mjs b/components/quickbooks_sandbox/sources/new-invoice-created/new-invoice-created.mjs new file mode 100644 index 0000000000000..02d4fbbe0744a --- /dev/null +++ b/components/quickbooks_sandbox/sources/new-invoice-created/new-invoice-created.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/sources/new-invoice-created/new-invoice-created.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-new-invoice-created", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/sources/new-invoice-updated/new-invoice-updated.mjs b/components/quickbooks_sandbox/sources/new-invoice-updated/new-invoice-updated.mjs new file mode 100644 index 0000000000000..9baa413fe6691 --- /dev/null +++ b/components/quickbooks_sandbox/sources/new-invoice-updated/new-invoice-updated.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/sources/new-invoice-updated/new-invoice-updated.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-new-invoice-updated", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/sources/new-item-created/new-item-created.mjs b/components/quickbooks_sandbox/sources/new-item-created/new-item-created.mjs new file mode 100644 index 0000000000000..da21f57e9fa86 --- /dev/null +++ b/components/quickbooks_sandbox/sources/new-item-created/new-item-created.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/sources/new-item-created/new-item-created.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-new-item-created", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickbooks_sandbox/sources/new-item-updated/new-item-updated.mjs b/components/quickbooks_sandbox/sources/new-item-updated/new-item-updated.mjs new file mode 100644 index 0000000000000..cfe26cdfd329b --- /dev/null +++ b/components/quickbooks_sandbox/sources/new-item-updated/new-item-updated.mjs @@ -0,0 +1,22 @@ +import app from "../../quickbooks_sandbox.app.mjs"; +import common from "@pipedream/quickbooks/sources/new-item-updated/new-item-updated.mjs"; + +import { adjustPropDefinitions } from "../../common/utils.mjs"; + +const { + name, description, type, ...others +} = common; +const props = adjustPropDefinitions(others.props, app); + +export default { + ...others, + key: "quickbooks_sandbox-new-item-updated", + version: "0.0.3", + name, + description, + type, + props: { + quickbooks: app, + ...props, + }, +}; diff --git a/components/quickemailverification/README.md b/components/quickemailverification/README.md index 952ea10eefae5..9b8d1d1ee9321 100644 --- a/components/quickemailverification/README.md +++ b/components/quickemailverification/README.md @@ -1,27 +1,11 @@ # Overview -QuickEmailVerification (QEV) is a powerful API that enables businesses to -quickly and easily verify the accuracy of email addresses. The API reduces the -risk of bouncebacks caused by mistyped or incorrect email addresses, and -ensures that only correct emails are sent out. With QEV, you can build tools -that authenticate email addresses with confidence and accuracy. +The QuickEmailVerification API offers a robust solution for validating email addresses in real-time, ensuring they're formatted correctly, deliverable, and free from common issues like typos or disposable domains. By integrating this API with Pipedream, you can automate the process of cleaning your email lists, enhancing email deliverability, and maintaining the integrity of your communication channels. This seamless verification process can be a critical component to any customer outreach, marketing campaign, or user sign-up flow. -QEV is great for any organization that needs to send emails to customers and -prospects. Here are just a few of the ways that you can use the QEV API to -build powerful and useful applications: +# Example Use Cases -- Email Address Validation: Quickly and accurately validate email addresses - entered by users, or stored in your customer database, to eliminate bad - addresses and reduce bouncebacks. -- Email Domain Verification: Verify the domain names or extensions of email - addresses, such as .com, .net, .org, etc., to ensure that they are genuine. -- Email Authentication: Perform SMTP checks to authenticate incoming emails and - ensure that they actually come from who they say they do. -- Real-Time Validation: Validate email addresses in real-time, to ensure that - no bad addresses sneak through. -- Bulk Email Verification: Verify a list of email addresses all at once to save - time and increase accuracy. -- Email List Cleaning: Detect and remove incorrect email addresses from your - customer database, drastically cutting down on bouncebacks. -- Risk Analysis: Analyze the risk of online fraud associated with certain email - addresses, helping you to protect your business and customers. +- **User Sign-Up Validation**: Immediately validate email addresses during the user registration process. Connect QuickEmailVerification to your app's sign-up form via Pipedream. This prevents fake or typo-ridden emails from entering your system, thereby improving the quality of your user base and reducing bounce rates. + +- **Email Campaign Cleaning**: Prior to launching an email marketing campaign, run your mailing list through QuickEmailVerification using a Pipedream workflow. Link it with email marketing platforms like Mailchimp or SendGrid to ensure that only verified emails receive your campaigns, which can boost open rates and sender reputation. + +- **Automated CRM Hygiene**: Keep your CRM data clean by setting up a Pipedream workflow that periodically checks and updates the email addresses of your contacts. Integrate with popular CRM systems like Salesforce or HubSpot to verify email data, reducing the risk of emailing invalid addresses and increasing the efficacy of sales and marketing efforts. diff --git a/components/quickmail_io/README.md b/components/quickmail_io/README.md new file mode 100644 index 0000000000000..cba7fa630d269 --- /dev/null +++ b/components/quickmail_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The QuickMail.io API allows you to automate email outreach and follow-up sequences. With this API, you can manage contacts, create and send new campaigns, and track the performance of your emails. When integrated with Pipedream, you can set up workflows that react to various triggers and perform actions such as adding new leads to a campaign, updating contact information, or triggering a follow-up email based on customer interactions. + +# Example Use Cases + +- **Sync New Users to QuickMail Campaigns**: Automatically add new users from your app (tracked in a platform like Segment) to a specific QuickMail campaign, initiating a welcome or onboarding sequence as soon as they sign up. + +- **Trigger Follow-ups Based on Customer Activity**: Use Pipedream to listen for specific events from your product analytics or a CRM like Salesforce, and trigger personalized follow-up emails via QuickMail when a user performs a key action or reaches a certain milestone. + +- **Automate Email Reporting with Google Sheets**: Create a workflow that compiles campaign performance statistics from QuickMail and appends them to a Google Sheet. Use this data for regular reporting or to gain insights for optimizing your email strategies. diff --git a/components/quickmail_io/package.json b/components/quickmail_io/package.json index 35f36be17a7f9..f94af06c469d0 100644 --- a/components/quickmail_io/package.json +++ b/components/quickmail_io/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/quipu/README.md b/components/quipu/README.md index d47eb36f3e118..37b99a7b83ad2 100644 --- a/components/quipu/README.md +++ b/components/quipu/README.md @@ -1,27 +1,11 @@ # Overview -With the Quipu API you can create powerful, feature-rich and fully-adaptable -invoicing applications for your business. +Quipu is an API designed to simplify invoicing and accounting for freelancers and small businesses. By integrating Quipu with Pipedream, you unlock the potential of automating financial tasks, syncing data across platforms, and streamlining invoicing operations. Pipedream's serverless platform provides the flexibility to connect Quipu with a myriad of other apps and services, enabling you to craft custom workflows that fit your exact business needs. Whether it's triggering actions based on invoice statuses, syncing contacts between platforms, or generating financial reports, Quipu's API and Pipedream's orchestration can together build a robust financial automation system. -Examples of you can create with the Quipu API: +# Example Use Cases -- Invoice generators -- Invoice tracking systems -- Document and customer management solutions -- Customizable invoice layout solutions -- Automated customer onboarding -- Document signing solutions -- Automated payroll solutions -- Customizable G/L integrations -- Automated notifications and email reminders -- Tax management tools -- Real-time reporting dashboards -- Automated document conversion solutions -- Automated payment solutions +- **Automated Invoice Creation and Delivery**: When a new sale is registered in your e-commerce platform (like Shopify), Pipedream triggers a Quipu workflow that generates an invoice and sends it to the customer. This ensures timely billing without manual intervention. -The Quipu API is used by small and large businesses of all kinds, anywhere in -the world. Whether you're a freelancer, online merchant, or a large enterprise, -Quipu API has the tools to get the job done quickly and efficiently. +- **Expense Tracking and Reporting**: Capture expenses from a company credit card or a platform like Expensify into Quipu. Set up a Pipedream workflow to automatically add these transactions as expenses in Quipu, categorize them, and generate monthly expense reports for easy reconciliation. -So think no more, let Quipu API do the work for you and save you time and -money! +- **Sync Contacts Between CRM and Quipu**: Whenever a new contact is added to your CRM (like Salesforce), a Pipedream workflow is triggered to create or update the contact details in Quipu. This keeps your invoicing system in sync with your customer relationship management, saving time and reducing errors. diff --git a/components/quriiri/package.json b/components/quriiri/package.json new file mode 100644 index 0000000000000..44f53f2b6ee74 --- /dev/null +++ b/components/quriiri/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/quriiri", + "version": "0.0.1", + "description": "Pipedream Quriiri Components", + "main": "quriiri.app.mjs", + "keywords": [ + "pipedream", + "quriiri" + ], + "homepage": "https://pipedream.com/apps/quriiri", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/quriiri/quriiri.app.mjs b/components/quriiri/quriiri.app.mjs new file mode 100644 index 0000000000000..2fdfc4c9390e8 --- /dev/null +++ b/components/quriiri/quriiri.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "quriiri", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/qwilr/README.md b/components/qwilr/README.md index a91dabbc4115d..84af4a22295b6 100644 --- a/components/qwilr/README.md +++ b/components/qwilr/README.md @@ -1,32 +1,11 @@ # Overview -Introducing the Qwilr API - It enables you to create and deliver stunning, -interactive documents like proposals, quotes, briefs, presentations and more. -With Qwilr API, you can build an array of document types that give your -business the professional edge. +The Qwilr API offers a programmable way to generate and manage proposals, quotes, and web-based documents. Using Pipedream, you can automate document lifecycle events, streamline proposal approvals, and synchronize Qwilr data with CRM platforms or other databases. Leverage the power of serverless workflows to react to new document events, update records in real-time, and create notifications or follow-ups that enhance collaboration and efficiency. -Using the Qwilr API, you can quickly create stunning and engaging documents in -a few easy steps: +# Example Use Cases -1. Choose a stunning template -2. Add content -3. Share your interactive document with potential customers +- **Automated Proposal Follow-Up**: When a Qwilr document is viewed by a client, trigger a workflow that logs this event in a CRM like Salesforce, and automatically sends a follow-up email to the client using a service like SendGrid, asking if they have any questions or require further assistance. -The Qwilr API offers a range of document types which you can instantly create -and share with your customers: +- **New Document Alert & Assignment**: On creation of a new Qwilr document, trigger a Pipedream workflow that posts a notification in a Slack channel and assigns a team member to review the document. Use Slack's API to mention the assigned team member directly, ensuring the immediate attention and handling of the new document. -- Proposals: Create and track winning proposals with Qwilr's templates, giving - customers an immersive viewing experience -- Quotes: Design and send beautiful, customised quotes with real-time data and - pricing -- Presentations: Create interactive, immersive presentations that are engaging - and memorable -- Briefs: Design and share attractive briefs with custom elements like - interactive mockups and video -- Legal Agreements: Streamline the process of creating and sharing legal - documents to customers in an easy and effective manner -- eSign: Send documents for easy, secure eSignatures - -With the Qwilr API, you can give your business the professional edge while -creating beautiful, interactive documents that engage your customers from start -to finish. Try it today! +- **Document Analytics to Dashboard**: Capture Qwilr document interaction analytics, such as views and time spent on the document, and push this data to a business intelligence tool like Google Sheets or a data visualization app like Tableau. Use this data to refine sales strategies and monitor engagement trends. diff --git a/components/radar/README.md b/components/radar/README.md index 08200cb46452d..d0f142877d0e4 100644 --- a/components/radar/README.md +++ b/components/radar/README.md @@ -1,27 +1,11 @@ # Overview -The Radar API enables users to develop powerful and exciting applications with -location services and payment solutions. It provides a secure, reliable -platform for managing and tracking user location data and payments, enabling -businesses to make informed decisions about their customers, services, and -products. +Radar API offers developer-friendly tools for location data and geofencing. This API lets you track user locations, generate location-based events, and create geofences to engage users based on their real-world movements. On Pipedream, you can harness this location intelligence to trigger workflows that empower your apps with contextual user insights, automate location-based marketing, or enhance operational efficiency with geospatial data. -By leveraging the Radar API, you can incorporate location services and payment -features into your application. Here are some examples of applications you can -build using the Radar API: +# Example Use Cases -- Customer Loyalty Program: Create a customized loyalty program to reward your - customers for frequent visits. -- Geofencing: Create virtual boundaries to target users when they enter a store - or restaurant. -- Location-Based Advertising: Enable advertisers to more accurately target - customers using location data. -- Delivery/Pickup: Allow customers to order goods online and have them - delivered quickly and reliably. -- Location Data Analytics: Analyze customer behaviors and movements to better - understand user preferences and better tailor services to them. -- Payment Services: Integrate payments into your application using Radar’s - secure payment platform. -- Time-Sensitive Tasks: Use geofencing and custom geotriggering to set up - tasks, such as garden maintenance, that can be triggered when users enter a - certain area. +- **Geo-Based User Engagement**: Enhance customer experience by sending personalized offers or content when a user enters a specific geofenced area. For instance, connect Radar to a CRM like Salesforce on Pipedream to trigger a workflow that sends a discount coupon via email or SMS when a customer is near a retail store. + +- **Automated Alerts for Asset Tracking**: Keep tabs on valuable assets or fleet vehicles by setting up workflows that send alerts to Slack or email when an asset enters or exits a geofenced zone. This real-time information flow can be crucial for logistics and security operations, ensuring timely responses to asset movements. + +- **Localized Content Delivery**: Deliver content that resonates with your audience by triggering actions based on user location. Connect Radar to a CMS like WordPress on Pipedream to automatically update website banners, language settings, or promote region-specific events as a user's location changes. diff --git a/components/rafflys/README.md b/components/rafflys/README.md new file mode 100644 index 0000000000000..2f5cb971f28e8 --- /dev/null +++ b/components/rafflys/README.md @@ -0,0 +1,11 @@ +# Overview + +Rafflys API provides tools to create and manage online raffles and contests, paving the way for engagement and promotional activities. Within Pipedream, you can leverage this API to build automations that enhance the way you run contests, connect with your audience, and analyze engagement data. By integrating Rafflys with other apps on Pipedream, you can craft workflows that streamline contest operations, notify winners, or even sync data with marketing platforms. + +# Example Use Cases + +- **Automated Contest Creation and Notification**: Create a periodic workflow in Pipedream that automatically sets up new raffles in Rafflys at scheduled intervals. Use the Slack or Discord app to announce the new contest to your community instantly. + +- **Dynamically Sync Contest Entries with Google Sheets**: Each time a new entry is submitted to a Rafflys contest, trigger a workflow that adds the entrant's details to a Google Sheet. This facilitates real-time tracking and analysis of participant data. + +- **Winner Announcement Workflow**: After a Rafflys contest concludes, use a workflow to pick a winner and post the results to your social media platforms, such as Twitter or Facebook, to increase transparency and engagement. diff --git a/components/ragic/README.md b/components/ragic/README.md index dda43753d5972..030ae9af1a111 100644 --- a/components/ragic/README.md +++ b/components/ragic/README.md @@ -1,4 +1,18 @@ +# Overview + +The Ragic API offers a robust way to interact with your Ragic databases, enabling you to create, read, update, and delete records programmatically. With its API, you can automate data entry, synchronize data across platforms, and trigger custom workflows. Pipedream amplifies these capabilities with a serverless platform where you can deploy these automations rapidly, reacting to events in Ragic or orchestrating actions across multiple apps. + +# Getting Started ## Webhooks To connect Pipedream Webhooks with Ragic!, first create and add the source to a workflow, then copy the generated URL and follow the instructions at [Webhooks on Ragic](https://www.ragic.com/intl/en/doc-api/33/Webhook-on-Ragic). + +# Example Use Cases + +- **Automated Data Backup**: Schedule a daily Pipedream workflow to fetch records from Ragic and save them to Google Sheets, ensuring your data is backed up and easily accessible for reporting. + +- **Lead Management**: When a new lead is entered into Ragic, trigger a Pipedream workflow to enrich the lead data using Clearbit, then push the enhanced data to Salesforce and notify your sales team via Slack. + +- **Inventory Tracking**: Set up a Pipedream workflow that detects when inventory levels in Ragic fall below a threshold, automatically reordering stock from suppliers through an email API and updating the Ragic inventory records accordingly. + diff --git a/components/ragie/actions/create-document/create-document.mjs b/components/ragie/actions/create-document/create-document.mjs new file mode 100644 index 0000000000000..70fada4cf4a38 --- /dev/null +++ b/components/ragie/actions/create-document/create-document.mjs @@ -0,0 +1,71 @@ +import FormData from "form-data"; +import fs from "fs"; +import { checkTmp } from "../../common/utils.mjs"; +import ragie from "../../ragie.app.mjs"; + +export default { + key: "ragie-create-document", + name: "Create Document", + description: "Creates a new document in Ragie. [See the documentation](https://docs.ragie.ai/reference/createdocument)", + version: "0.0.1", + type: "action", + props: { + ragie, + file: { + propDefinition: [ + ragie, + "documentFile", + ], + }, + mode: { + propDefinition: [ + ragie, + "documentMode", + ], + optional: true, + }, + metadata: { + type: "object", + label: "Metadata", + description: "Metadata for the document. Keys must be strings. Values may be strings, numbers, booleans, or lists of strings.", + optional: true, + }, + externalId: { + type: "string", + label: "External ID", + description: "An optional identifier for the document. A common value might be an ID in an external system or the URL where the source file may be found.", + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "An optional name for the document. If set, the document will have this name. Otherwise, it will default to the file's name.", + optional: true, + }, + partition: { + propDefinition: [ + ragie, + "partition", + ], + optional: true, + }, + }, + async run({ $ }) { + const data = new FormData(); + data.append("file", fs.createReadStream(checkTmp(this.file))); + if (this.mode) data.append("mode", this.mode); + if (this.metadata) data.append("metadata", JSON.stringify(this.metadata)); + if (this.externalId) data.append("external_id", this.externalId); + if (this.name) data.append("name", this.name); + if (this.partition) data.append("partition", this.partition); + + const response = await this.ragie.createDocument({ + $, + data, + headers: data.getHeaders(), + }); + + $.export("$summary", `Created document: ${response.name} (ID: ${response.id})`); + return response; + }, +}; diff --git a/components/ragie/actions/update-document-file/update-document-file.mjs b/components/ragie/actions/update-document-file/update-document-file.mjs new file mode 100644 index 0000000000000..e62730e0a6034 --- /dev/null +++ b/components/ragie/actions/update-document-file/update-document-file.mjs @@ -0,0 +1,48 @@ +import FormData from "form-data"; +import fs from "fs"; +import { checkTmp } from "../../common/utils.mjs"; +import ragie from "../../ragie.app.mjs"; + +export default { + key: "ragie-update-document-file", + name: "Update Document File", + description: "Updates an existing document file in Ragie. [See the documentation](https://docs.ragie.ai/reference/updatedocumentfile).", + version: "0.0.1", + type: "action", + props: { + ragie, + documentId: { + propDefinition: [ + ragie, + "documentId", + ], + }, + mode: { + propDefinition: [ + ragie, + "documentMode", + ], + optional: true, + }, + file: { + propDefinition: [ + ragie, + "documentFile", + ], + }, + }, + async run({ $ }) { + const data = new FormData(); + data.append("file", fs.createReadStream(checkTmp(this.file))); + if (this.mode) data.append("mode", this.mode); + + const response = await this.ragie.updateDocumentFile({ + $, + documentId: this.documentId, + data, + headers: data.getHeaders(), + }); + $.export("$summary", `Successfully updated document file with ID: ${this.documentId}`); + return response; + }, +}; diff --git a/components/ragie/common/constants.mjs b/components/ragie/common/constants.mjs new file mode 100644 index 0000000000000..d681c66f145c1 --- /dev/null +++ b/components/ragie/common/constants.mjs @@ -0,0 +1,15 @@ +export const SCOPE_OPTIONS = [ + { + label: "Document", + value: "document", + }, + { + label: "Chunk", + value: "chunk", + }, +]; + +export const DOCUMENT_MODE_OPTIONS = [ + "hi_res", + "fast", +]; diff --git a/components/ragie/common/utils.mjs b/components/ragie/common/utils.mjs new file mode 100644 index 0000000000000..0cd1a12b6a4ba --- /dev/null +++ b/components/ragie/common/utils.mjs @@ -0,0 +1,31 @@ +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; + +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/ragie/package.json b/components/ragie/package.json new file mode 100644 index 0000000000000..b331423650937 --- /dev/null +++ b/components/ragie/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/ragie", + "version": "0.1.0", + "description": "Pipedream Ragie Components", + "main": "ragie.app.mjs", + "keywords": [ + "pipedream", + "ragie" + ], + "homepage": "https://pipedream.com/apps/ragie", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/ragie/ragie.app.mjs b/components/ragie/ragie.app.mjs new file mode 100644 index 0000000000000..ea3c95b014d66 --- /dev/null +++ b/components/ragie/ragie.app.mjs @@ -0,0 +1,127 @@ +import { axios } from "@pipedream/platform"; +import { DOCUMENT_MODE_OPTIONS } from "./common/constants.mjs"; + +export default { + type: "app", + app: "ragie", + propDefinitions: { + documentFile: { + type: "string", + label: "File", + description: "The path to the file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#the-tmp-directory).", + }, + documentMode: { + type: "string", + label: "Mode", + description: "Partition strategy for the document. Options are 'hi_res' or 'fast'.", + optional: true, + options: DOCUMENT_MODE_OPTIONS, + }, + partition: { + type: "string", + label: "Partition", + description: "An optional partition identifier. Partitions must be lowercase alphanumeric and may only include the special characters '_' and '-'.", + }, + documentId: { + type: "string", + label: "Document ID", + description: "The ID of the document to update.", + async options({ prevContext }) { + const { + documents, pagination, + } = await this.listDocuments({ + params: { + cursor: prevContext.cursor, + }, + }); + return { + options: documents.map(({ + id: value, name: label, + }) => ({ + label, + value, + })), + context: { + cursor: pagination.next_cursor, + }, + }; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.ragie.ai"; + }, + _headers(headers = {}) { + return { + ...headers, + "Authorization": `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(headers), + ...opts, + }); + }, + createDocument(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/documents", + ...opts, + }); + }, + listDocuments(opts = {}) { + return this._makeRequest({ + path: "/documents", + params: opts, + }); + }, + updateDocumentFile({ + documentId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/documents/${documentId}/file`, + ...opts, + }); + }, + listConnections(opts = {}) { + return this._makeRequest({ + path: "/connections", + params: opts, + }); + }, + async *paginate({ + fn, params = {}, fieldName, maxResults = null, ...opts + }) { + let count = 0; + let nextCursor; + + do { + params.cursor = nextCursor; + const { + pagination, + ...data + } = await fn({ + params, + ...opts, + }); + const items = data[fieldName]; + + for (const d of items) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + nextCursor = pagination?.next_cursor; + } while (nextCursor); + }, + }, +}; diff --git a/components/ragie/sources/common/base.mjs b/components/ragie/sources/common/base.mjs new file mode 100644 index 0000000000000..d09ddc09671a2 --- /dev/null +++ b/components/ragie/sources/common/base.mjs @@ -0,0 +1,60 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import ragie from "../../ragie.app.mjs"; + +export default { + props: { + ragie, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + + const response = this.ragie.paginate({ + fn: this.getFunction(), + fieldName: this.getFieldName(), + }); + + let responseArray = []; + for await (const item of response) { + if (Date.parse(item.created_at) <= lastDate) break; + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastDate(Date.parse(responseArray[0].created_at)); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item.created_at || new Date()), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/ragie/sources/new-connection/new-connection.mjs b/components/ragie/sources/new-connection/new-connection.mjs new file mode 100644 index 0000000000000..43a70dc255e20 --- /dev/null +++ b/components/ragie/sources/new-connection/new-connection.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ragie-new-connection", + name: "New Ragie Connection Created", + description: "Emit new event whenever a new connection is created in Ragie. [See the documentation](https://docs.ragie.ai/reference/list_connections_connections_get)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.ragie.listConnections; + }, + getFieldName() { + return "connections"; + }, + getSummary({ + name, type, + }) { + return `New Ragie Connection: ${name} (${type})`; + }, + }, + sampleEmit, +}; diff --git a/components/ragie/sources/new-connection/test-event.mjs b/components/ragie/sources/new-connection/test-event.mjs new file mode 100644 index 0000000000000..27c452a389fe9 --- /dev/null +++ b/components/ragie/sources/new-connection/test-event.mjs @@ -0,0 +1,11 @@ +export default { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "created_at": "2025-01-16T18:44:13.901Z", + "updated_at": "2025-01-16T18:44:13.901Z", + "metadata": {}, + "type": "string", + "name": "string", + "enabled": true, + "last_synced_at": "2025-01-16T18:44:13.901Z", + "syncing": true +} \ No newline at end of file diff --git a/components/ragie/sources/new-document/new-document.mjs b/components/ragie/sources/new-document/new-document.mjs new file mode 100644 index 0000000000000..95995cfbc1df0 --- /dev/null +++ b/components/ragie/sources/new-document/new-document.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ragie-new-document", + name: "New Ragie Document Created", + description: "Emit new event whenever a new document is created in Ragie. [See the documentation](https://docs.ragie.ai/reference/listdocuments)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.ragie.listDocuments; + }, + getFieldName() { + return "documents"; + }, + getSummary(document) { + return `New Ragie Document: ${document.name || document.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/ragie/sources/new-document/test-event.mjs b/components/ragie/sources/new-document/test-event.mjs new file mode 100644 index 0000000000000..e86b0ff192662 --- /dev/null +++ b/components/ragie/sources/new-document/test-event.mjs @@ -0,0 +1,13 @@ +export default { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "created_at": "2025-01-16T18:44:13.901Z", + "updated_at": "2025-01-16T18:44:13.901Z", + "status": "string", + "name": "string", + "metadata": { + "additionalProp": "string" + }, + "partition": "string", + "chunk_count": 0, + "external_id": "string" +} \ No newline at end of file diff --git a/components/railsr/README.md b/components/railsr/README.md new file mode 100644 index 0000000000000..1acbfc3d57899 --- /dev/null +++ b/components/railsr/README.md @@ -0,0 +1,11 @@ +# Overview + +The Railsr API provides robust tools for building and managing financial services within applications. It offers features like creating and managing accounts, transferring money, and handling different currencies, making it a powerful ally for fintech developers. Integrating the Railsr API with Pipedream allows you to automate these financial operations and connect them with hundreds of other services to create dynamic, cross-functional workflows that enhance productivity and streamline financial processes. + +# Example Use Cases + +- **Automated Customer Onboarding**: - When a new customer signs up via your app, trigger a workflow in Pipedream that uses the Railsr API to create a new customer account. Connect this workflow with Slack to send a notification to your sales team, informing them about the new sign-up and customer details. + +- **Real-Time Fraud Detection Alerts**: - Set up a Pipedream workflow that monitors transaction activities on customer accounts via the Railsr API. Use machine learning tools like AWS SageMaker, integrated within the workflow, to analyze transactions for potential fraud. If suspicious activity is detected, automatically trigger alerts to your security team via email or SMS. + +- **Daily Financial Reporting**: - Configure a daily scheduled workflow in Pipedream that gathers financial data from Railsr API, such as daily transactions and account balances. Summarize this data and send a comprehensive financial report to your finance team through email or a Google Sheets document for easy access and analysis. diff --git a/components/railsr/package.json b/components/railsr/package.json new file mode 100644 index 0000000000000..6662e847c72b3 --- /dev/null +++ b/components/railsr/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/railsr", + "version": "0.0.1", + "description": "Pipedream Railsr Components", + "main": "railsr.app.mjs", + "keywords": [ + "pipedream", + "railsr" + ], + "homepage": "https://pipedream.com/apps/railsr", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/railsr/railsr.app.mjs b/components/railsr/railsr.app.mjs new file mode 100644 index 0000000000000..cdd025fe4bb96 --- /dev/null +++ b/components/railsr/railsr.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "railsr", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/raindrop/README.md b/components/raindrop/README.md index 52096dc5db793..5a8f832497f28 100644 --- a/components/raindrop/README.md +++ b/components/raindrop/README.md @@ -1,20 +1,11 @@ # Overview -The Raindrop API provides users with powerful tools to create sophisticated web -and mobile applications. With Raindrop, users have the ability to build and -deploy a wide range of applications, feature-rich applications that can meet -the needs and requirements of any business. +Raindrop.io is a versatile bookmarking service that lets users save, organize, and share web content. With its API, you can automate the management of bookmarks, collections, and tags, thus streamlining the way you handle internet resources. Automating these tasks with the Raindrop API on Pipedream opens the door to personalized content curation, enhanced collaboration, and efficient information retrieval workflows. Think syncing saved articles across platforms, generating reports on saved links, or even triggering actions based on new content. -Here are just some of the many things you can build with the Raindrop API: +# Example Use Cases -- Create and manage dynamic user databases. -- Create marketing campaigns and loyalty programs. -- Create web and mobile applications. -- Create integrated payment systems. -- Create targeted data-driven newsletters. -- Monitor user activity with analytics. -- Build and deliver customized experiences. -- Build and integrate with third-party APIs. -- Create and manage multi-tier role-based user hierarchies. -- Create virtual currencies. -- Set up custom forms for collecting user data. +- **Content Syncing Across Platforms**: Integrate Raindrop with Slack to share newly added bookmarks with a team channel. When a new bookmark is added to a certain collection in Raindrop, Pipedream can automatically post a message to Slack, keeping everyone in the loop. + +- **Read-it-Later Email Digest**: Connect Raindrop with an email service like SendGrid to create a weekly digest. Use Pipedream to aggregate bookmarks added to a "Read Later" collection and email them as a compiled list, turning Raindrop collections into a personalized newsletter. + +- **Resource Management Dashboard**: Fuse Raindrop with Google Sheets to build a dynamic bookmark dashboard. With Pipedream, every new bookmark saved to Raindrop can be added as a new row in a Google Sheet, allowing for a powerful overview of your resources and easy sharing capabilities. diff --git a/components/raindrop/actions/create-collection/create-collection.mjs b/components/raindrop/actions/create-collection/create-collection.mjs index b44cbf9f444fb..c843112120e46 100644 --- a/components/raindrop/actions/create-collection/create-collection.mjs +++ b/components/raindrop/actions/create-collection/create-collection.mjs @@ -4,7 +4,7 @@ export default { key: "raindrop-create-collection", name: "Create New Collection", description: "Creates an additional collection. [See the docs here](https://developer.raindrop.io/v1/collections/methods#create-collection)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { raindrop, diff --git a/components/raindrop/actions/delete-bookmark/delete-bookmark.mjs b/components/raindrop/actions/delete-bookmark/delete-bookmark.mjs index 2cb2e7a70866a..a6c7844cbcff8 100644 --- a/components/raindrop/actions/delete-bookmark/delete-bookmark.mjs +++ b/components/raindrop/actions/delete-bookmark/delete-bookmark.mjs @@ -4,7 +4,7 @@ export default { key: "raindrop-delete-bookmark", name: "Delete Bookmark", description: "Delete a bookmark. [See the docs here](https://developer.raindrop.io/v1/raindrops/single#remove-raindrop)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { raindrop, diff --git a/components/raindrop/actions/delete-collection/delete-collection.mjs b/components/raindrop/actions/delete-collection/delete-collection.mjs index 9f1897030110b..4a0f479aef63e 100644 --- a/components/raindrop/actions/delete-collection/delete-collection.mjs +++ b/components/raindrop/actions/delete-collection/delete-collection.mjs @@ -4,7 +4,7 @@ export default { key: "raindrop-delete-collection", name: "Delete Collection", description: "Delete a collection. [See the docs here](https://developer.raindrop.io/v1/collections/methods#remove-collection)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { raindrop, diff --git a/components/raindrop/actions/get-bookmark/get-bookmark.mjs b/components/raindrop/actions/get-bookmark/get-bookmark.mjs index 5b512f397c7ea..e27d2cb504abd 100644 --- a/components/raindrop/actions/get-bookmark/get-bookmark.mjs +++ b/components/raindrop/actions/get-bookmark/get-bookmark.mjs @@ -4,7 +4,7 @@ export default { key: "raindrop-get-bookmark", name: "Get Bookmark", description: "Retrieve bookmark detailed information by given ID. [See the docs here](https://developer.raindrop.io/v1/raindrops/single#get-raindrop)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { raindrop, diff --git a/components/raindrop/actions/get-collection/get-collection.mjs b/components/raindrop/actions/get-collection/get-collection.mjs index 7a4dce6fbe55c..2c75858fa0061 100644 --- a/components/raindrop/actions/get-collection/get-collection.mjs +++ b/components/raindrop/actions/get-collection/get-collection.mjs @@ -4,7 +4,7 @@ export default { key: "raindrop-get-collection", name: "Get Collection", description: "Get collection. [See the docs here](https://developer.raindrop.io/v1/collections/methods#get-collection)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { raindrop, diff --git a/components/raindrop/actions/list-collections/list-collections.mjs b/components/raindrop/actions/list-collections/list-collections.mjs index 59fe1ff866879..63cf1cec2635c 100644 --- a/components/raindrop/actions/list-collections/list-collections.mjs +++ b/components/raindrop/actions/list-collections/list-collections.mjs @@ -4,7 +4,7 @@ export default { key: "raindrop-list-collections", name: "Retrieve All Collections", description: "Retrieves all collections. [See the docs here](https://developer.raindrop.io/v1/collections/methods#get-root-collections)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { raindrop, diff --git a/components/raindrop/actions/parse-bookmark-file/parse-bookmark-file.mjs b/components/raindrop/actions/parse-bookmark-file/parse-bookmark-file.mjs index 438f2502d8b6e..d3ad6cd329141 100644 --- a/components/raindrop/actions/parse-bookmark-file/parse-bookmark-file.mjs +++ b/components/raindrop/actions/parse-bookmark-file/parse-bookmark-file.mjs @@ -9,7 +9,7 @@ export default { key: "raindrop-parse-bookmark-file", name: "Parse HTML Bookmark File", description: "Convert HTML bookmark file to JSON. Support Nestcape, Pocket and Instapaper file formats. [See the docs here](https://developer.raindrop.io/v1/import#parse-html-import-file)", - version: "0.0.6", + version: "0.0.7", type: "action", props: { raindrop, diff --git a/components/raindrop/actions/retrieve-bookmarks/retrieve-bookmarks.mjs b/components/raindrop/actions/retrieve-bookmarks/retrieve-bookmarks.mjs index bf2beb2f2e9f8..f2e82cf699464 100644 --- a/components/raindrop/actions/retrieve-bookmarks/retrieve-bookmarks.mjs +++ b/components/raindrop/actions/retrieve-bookmarks/retrieve-bookmarks.mjs @@ -5,7 +5,7 @@ export default { key: "raindrop-retrieve-bookmarks", name: "Retrieve Bookmarks from Collection", description: "Retrieves all bookmarks from the specified collection. [See docs](https://developer.raindrop.io/v1/raindrops/multiple)", - version: "0.2.1", + version: "0.2.2", type: "action", props: { raindrop, diff --git a/components/raindrop/actions/save/save.mjs b/components/raindrop/actions/save/save.mjs index 4a018294f3d30..5de001bf0af64 100644 --- a/components/raindrop/actions/save/save.mjs +++ b/components/raindrop/actions/save/save.mjs @@ -4,7 +4,7 @@ export default { key: "raindrop-save", name: "Save to Raindrop Collection", description: "Receive a link and save it into a specified collection. [See docs](https://developer.raindrop.io/v1/raindrops/single)", - version: "1.2.1", + version: "1.2.2", type: "action", props: { raindrop, diff --git a/components/raindrop/actions/update-bookmark/update-bookmark.mjs b/components/raindrop/actions/update-bookmark/update-bookmark.mjs index 272bb337e26fc..eae3988ccc4ec 100644 --- a/components/raindrop/actions/update-bookmark/update-bookmark.mjs +++ b/components/raindrop/actions/update-bookmark/update-bookmark.mjs @@ -4,7 +4,7 @@ export default { key: "raindrop-update-bookmark", name: "Update Bookmark", description: "Update an existing bookmark. [See the docs here](https://developer.raindrop.io/v1/raindrops/single#update-raindrop)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { raindrop, diff --git a/components/raindrop/actions/update-collection/update-collection.mjs b/components/raindrop/actions/update-collection/update-collection.mjs index 8bb0e6ab462e7..5aad481a4a4d0 100644 --- a/components/raindrop/actions/update-collection/update-collection.mjs +++ b/components/raindrop/actions/update-collection/update-collection.mjs @@ -4,7 +4,7 @@ export default { key: "raindrop-update-collection", name: "Update Collection", description: "Update an existing collection. [See the docs here](https://developer.raindrop.io/v1/collections/methods#update-collection)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { raindrop, diff --git a/components/raindrop/package.json b/components/raindrop/package.json index fb4153861f070..0fa3f505bd212 100644 --- a/components/raindrop/package.json +++ b/components/raindrop/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/raindrop", - "version": "0.3.11", + "version": "0.3.13", "description": "Pipedream Raindrop.io Components", "main": "raindrop.app.js", "keywords": [ diff --git a/components/raindrop/sources/new-bookmark/new-bookmark.mjs b/components/raindrop/sources/new-bookmark/new-bookmark.mjs index 9b5525308e948..7872f6382e822 100644 --- a/components/raindrop/sources/new-bookmark/new-bookmark.mjs +++ b/components/raindrop/sources/new-bookmark/new-bookmark.mjs @@ -7,7 +7,7 @@ export default { name: "New Bookmark", description: "Emit new event when a bookmark is added", type: "source", - version: "0.0.4", + version: "0.0.6", dedupe: "unique", props: { raindrop, @@ -48,11 +48,14 @@ export default { }, async run() { let page = this._getPage(); + const createdDate = `created:>${new Date().toISOString() + .slice(0, 7)}`; while (true) { const { items: bookmarks } = await this.raindrop.getRaindrops(this, this.collectionId, { page, perpage: constants.DEFAULT_PER_PAGE, + search: createdDate, }); this.emitEvents(bookmarks); diff --git a/components/raisely/README.md b/components/raisely/README.md new file mode 100644 index 0000000000000..b3713fbdff0b5 --- /dev/null +++ b/components/raisely/README.md @@ -0,0 +1,11 @@ +# Overview + +The Raisely API lets you manage and automate interactions with your fundraising campaigns. It can be used for retrieving campaign data, updating donor records, or triggering communications. When paired with Pipedream, these capabilities grow exponentially, allowing for integration with countless other services for enhanced automation and data flow. For example, syncing donor information with a CRM, sending custom thank you emails, or posting updates to social media platforms. + +# Example Use Cases + +- **Sync Donors with CRM**: Integrate Raisely with a CRM like Salesforce. When a new donation is recorded in Raisely, use the API to create or update the donor's details in Salesforce, ensuring your donor records are always current. + +- **Distribute Custom Thank You Emails**: After a donation, trigger an automated, personalized thank you email using a service like SendGrid. Combine Raisely's data with SendGrid's email sending capabilities to make each donor feel appreciated with a custom message. + +- **Social Media Engagement**: Share donor milestones or campaign updates to social platforms such as Twitter. When a campaign hits a milestone, use Raisely API to fetch the latest data and post a celebratory message or thank donors publicly for their support. diff --git a/components/ramp/README.md b/components/ramp/README.md index ebc056e88c500..4fa947f9b11fc 100644 --- a/components/ramp/README.md +++ b/components/ramp/README.md @@ -1,29 +1,11 @@ # Overview -The Ramp API allows developers to quickly and securely integrate payments into -their applications. Ramp uses tokenization, encryption and audit trails & -compliance to keep data safe, so developers can securely create custom payment -experience. With the Ramp API: +The Ramp API provides programmatic access to financial and accounting functionalities, allowing for the management of company cards, transactions, and reporting. By leveraging the Ramp API within Pipedream, you can automate intricate finance operations, synchronize transaction data with accounting software, trigger notifications based on spending activities, and analyze financial data in real-time, integrating with other services for enhanced financial workflows. -- You can easily add custom payment forms to any online shopping cart on your - site. -- You can create a payment gateway to accept credit and debit card payments - from customers - Directly from within your website or application. -- You can add a subscription service that allows your customers to easily and - securely pay for recurring purchases. -- You can accept payment across mobile devices, with or without an internet - connection -- You can use biometric authentication and fraud prevention features to secure - transactions. +# Example Use Cases -Here are a few examples of what you can do with the Ramp API: +- **Automated Expense Reporting**: Automatically export new Ramp transactions into a Google Sheet or Airtable base for real-time expense tracking. This workflow can be set to trigger whenever a new transaction is detected, populating the sheet or base with transaction details, amounts, merchant names, and categories for easy reporting and analysis. -- Accept payments from customers in multiple currencies -- Integrate secure payment processing with a variety of third party vendors -- Enable customers to pay securely with any major credit or debit card -- Develop custom payment experiences (e.g. dashboard payment history, loyalty - discounts, flexible payment plans etc.) -- Set up subscription-based payments -- Automate payment reconciliation and facilitate easy refunds -- Build web and mobile commerce solutions using secure payment APIs -- Utilize biometric authentication and fraud protection tools +- **Receipt Matching and Reconciliation**: Use the Ramp API to fetch transaction data and match receipts uploaded to a cloud storage platform like Dropbox or Google Drive. This can involve extracting receipt information through OCR (Optical Character Recognition) services and comparing it with transaction records to streamline the reconciliation process without manual data entry. + +- **Real-time Alerts and Approvals**: Create a workflow where you receive instant Slack notifications or emails when high-value transactions occur or when spend thresholds are approached. Further, implement approval workflows where managers receive a prompt to approve or reject proposed transactions via a Slack message or a custom-built approval interface on Pipedream. diff --git a/components/ramp/actions/create-user-invite/create-user-invite.mjs b/components/ramp/actions/create-user-invite/create-user-invite.mjs new file mode 100644 index 0000000000000..c74966e90cf97 --- /dev/null +++ b/components/ramp/actions/create-user-invite/create-user-invite.mjs @@ -0,0 +1,72 @@ +import ramp from "../../ramp.app.mjs"; +import { v4 as uuidv4 } from "uuid"; + +export default { + key: "ramp-create-user-invite", + name: "Create User Invite", + description: "Sends out an invite for a new user. [See the documentation](https://docs.ramp.com/developer-api/v1/reference/rest/users#post-developer-v1-users-deferred)", + version: "0.0.2", + type: "action", + props: { + ramp, + email: { + type: "string", + label: "Email", + description: "The employee's email address", + }, + firstName: { + type: "string", + label: "First Name", + description: "First name of the employee", + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the employee", + }, + role: { + propDefinition: [ + ramp, + "role", + ], + }, + departmentId: { + propDefinition: [ + ramp, + "departmentId", + ], + }, + directManagerId: { + propDefinition: [ + ramp, + "userId", + ], + label: "Direct Manager ID", + description: "Unique identifier of the employee's direct manager", + optional: true, + }, + locationId: { + propDefinition: [ + ramp, + "locationId", + ], + }, + }, + async run({ $ }) { + const response = await this.ramp.createUserInvite({ + $, + data: { + email: this.email, + first_name: this.firstName, + last_name: this.lastName, + role: this.role, + department_id: this.departmentId, + direct_manager_id: this.directManagerId, + location_id: this.locationId, + idempotency_key: uuidv4(), + }, + }); + $.export("$summary", `Invite sent successfully to new user ${this.firstName} ${this.lastName}`); + return response; + }, +}; diff --git a/components/ramp/actions/issue-virtual-card/issue-virtual-card.mjs b/components/ramp/actions/issue-virtual-card/issue-virtual-card.mjs new file mode 100644 index 0000000000000..5734fdb1871d7 --- /dev/null +++ b/components/ramp/actions/issue-virtual-card/issue-virtual-card.mjs @@ -0,0 +1,150 @@ +import ramp from "../../ramp.app.mjs"; +import { v4 as uuidv4 } from "uuid"; + +export default { + key: "ramp-issue-virtual-card", + name: "Issue Virtual Card", + description: "Creates a new virtual card for a given user. [See the documentation](https://docs.ramp.com/developer-api/v1/reference/rest/limits#post-developer-v1-limits-deferred)", + version: "0.0.3", + type: "action", + props: { + ramp, + displayName: { + type: "string", + label: "Virtual Card Name", + description: "The name of the virtual card", + }, + userId: { + propDefinition: [ + ramp, + "userId", + ], + }, + linkToSpendProgram: { + type: "boolean", + label: "Link to Existing Spend Program", + description: "Whether to link the card to an existing spend program", + reloadProps: true, + }, + spendProgramId: { + propDefinition: [ + ramp, + "spendProgramId", + ], + hidden: true, + }, + allowedCategories: { + propDefinition: [ + ramp, + "allowedCategories", + ], + hidden: true, + }, + blockedCategories: { + propDefinition: [ + ramp, + "blockedCategories", + ], + hidden: true, + }, + }, + async additionalProps(props) { + const newProps = {}; + if (this.linkToSpendProgram === undefined) { + return newProps; + } + if (this.linkToSpendProgram) { + props.spendProgramId.hidden = false; + } else if (this.linkToSpendProgram === false) { + props.spendProgramId.hidden = true; + props.allowedCategories.hidden = false; + props.blockedCategories.hidden = false; + newProps.primaryCardEnabled = { + type: "boolean", + label: "Primary Card Enabled", + description: "Dictates whether the user's physical card can be linked to this limit", + }; + newProps.reimbursementsEnabled = { + type: "boolean", + label: "Reimbursements Enabled", + description: "Dictates whether reimbursements can be submitted against this limit", + }; + newProps.isShareable = { + type: "boolean", + label: "Is Shareable", + description: "Dictates whether the spend limit is shareable among multiple users", + optional: true, + }; + } + newProps.limit = { + type: "string", + label: "Total Limit per Interval (USD)", + description: "Total amount limit per interval in USD", + }; + newProps.interval = { + type: "string", + label: "Interval", + description: "Time interval to apply limit to", + options: [ + "ANNUAL", + "DAILY", + "MONTHLY", + "QUARTERLY", + "TERTIARY", + "TOTAL", + "WEEKLY", + "YEARLY", + ], + }; + newProps.transactionAmountLimit = { + type: "string", + label: "Maximum Spend per Transaction (USD)", + description: "Max amount per transaction in USD", + optional: true, + }; + return newProps; + }, + methods: { + formatUSD(amount) { + if (!amount) { + return undefined; + } + return +(amount.split("$").pop()) * 100; + }, + }, + async run({ $ }) { + const response = await this.ramp.createLimit({ + $, + data: { + idempotency_key: uuidv4(), + display_name: this.displayName, + user_id: this.userId, + spend_program_id: this.spendProgramId, + permitted_spend_types: !this.linkToSpendProgram + ? { + primary_card_enabled: this.primaryCardEnabled, + reimbursements_enabled: this.reimbursementsEnabled, + } + : undefined, + spending_restrictions: !this.spendProgramId + ? { + limit: { + amount: this.formatUSD(this.limit), + }, + interval: this.interval, + transaction_amount_limit: this.transactionAmountLimit + ? { + amount: this.formatUSD(this.transactionAmountLimit), + } + : undefined, + allowed_categories: this.allowedCategories, + blocked_categories: this.blockedCategories, + } + : undefined, + is_shareable: this.isShareable, + }, + }); + $.export("$summary", `Successfully created new limit with ID ${response.id}`); + return response; + }, +}; diff --git a/components/ramp/actions/upload-receipt/upload-receipt.mjs b/components/ramp/actions/upload-receipt/upload-receipt.mjs new file mode 100644 index 0000000000000..80c366684a260 --- /dev/null +++ b/components/ramp/actions/upload-receipt/upload-receipt.mjs @@ -0,0 +1,68 @@ +import ramp from "../../ramp.app.mjs"; +import { v4 as uuidv4 } from "uuid"; +import fs from "fs"; + +export default { + key: "ramp-upload-receipt", + name: "Upload Receipt", + description: "Uploads a receipt for a given transaction and user. [See the documentation](https://docs.ramp.com/developer-api/v1/reference/rest/receipts#post-developer-v1-receipts)", + version: "0.0.2", + type: "action", + props: { + ramp, + transactionId: { + propDefinition: [ + ramp, + "transactionId", + ], + }, + userId: { + propDefinition: [ + ramp, + "userId", + ], + }, + filePath: { + type: "string", + label: "File Path", + description: "The path to a file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + }, + }, + async run({ $ }) { + const boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"; + const form = `--${boundary}\r\n` + + "Content-Disposition: form-data; name=\"idempotency_key\"\r\n\r\n" + + `${uuidv4()}\r\n` + + `--${boundary}\r\n` + + "Content-Disposition: form-data; name=\"transaction_id\"\r\n\r\n" + + `${this.transactionId}\r\n` + + `--${boundary}\r\n` + + "Content-Disposition: form-data; name=\"user_id\"\r\n\r\n" + + `${this.userId}\r\n` + + `--${boundary}\r\n` + + `Content-Disposition: attachment; name="receipt"; filename="${this.filePath.split("/").pop()}"\r\n\r\n`; + + const fileContent = fs.readFileSync(this.filePath.includes("tmp/") + ? this.filePath + : `/tmp/${this.filePath}`); + + const formEnd = `\r\n--${boundary}--`; + + const data = Buffer.concat([ + Buffer.from(form, "utf8"), + fileContent, + Buffer.from(formEnd, "utf8"), + ]); + + const response = await this.ramp.uploadReceipt({ + $, + data, + headers: { + "Content-Type": `multipart/form-data; boundary=${boundary}`, + "Content-Length": data.length, + }, + }); + $.export("$summary", `Successfully uploaded receipt for transaction ID: ${this.transactionId}`); + return response; + }, +}; diff --git a/components/ramp/common/constants.mjs b/components/ramp/common/constants.mjs new file mode 100644 index 0000000000000..59a9bffc3fc07 --- /dev/null +++ b/components/ramp/common/constants.mjs @@ -0,0 +1,222 @@ +const CATEGORY_CODES = [ + { + value: 1, + label: "Pet", + }, + { + value: 2, + label: "Other", + }, + { + value: 3, + label: "Office", + }, + { + value: 4, + label: "Airlines", + }, + { + value: 5, + label: "Car rental", + }, + { + value: 6, + label: "Lodging", + }, + { + value: 7, + label: "Travel misc", + }, + { + value: 8, + label: "Taxi and rideshare", + }, + { + value: 9, + label: "Freight, moving and delivery services", + }, + { + value: 10, + label: "Shipping", + }, + { + value: 11, + label: "Utilities", + }, + { + value: 12, + label: "Office supplies and cleaning", + }, + { + value: 13, + label: "General merchandise", + }, + { + value: 14, + label: "Electronics", + }, + { + value: 15, + label: "Clothing", + }, + { + value: 16, + label: "Books and newspapers", + }, + { + value: 17, + label: "Supermarkets and grocery stores", + }, + { + value: 18, + label: "Fuel and gas", + }, + { + value: 19, + label: "Restaurants", + }, + { + value: 20, + label: "Alcohol and bars", + }, + { + value: 21, + label: "Medical", + }, + { + value: 23, + label: "Fees and financial institutions", + }, + { + value: 24, + label: "Entertainment", + }, + { + value: 25, + label: "Professional services", + }, + { + value: 26, + label: "Taxes and tax preparation", + }, + { + value: 27, + label: "Advertising", + }, + { + value: 28, + label: "Parking", + }, + { + value: 29, + label: "Car services", + }, + { + value: 30, + label: "Gambling", + }, + { + value: 31, + label: "Clubs and memberships", + }, + { + value: 32, + label: "Legal", + }, + { + value: 33, + label: "Education", + }, + { + value: 34, + label: "Charitable donations", + }, + { + value: 35, + label: "Political organizations", + }, + { + value: 36, + label: "Religious organizations", + }, + { + value: 37, + label: "Fines", + }, + { + value: 38, + label: "Government services", + }, + { + value: 39, + label: "Intra-company purchases", + }, + { + value: 40, + label: "SaaS / Software", + }, + { + value: 41, + label: "Cloud computing", + }, + { + value: 42, + label: "Streaming services", + }, + { + value: 43, + label: "Internet and phone", + }, + { + value: 44, + label: "Insurance", + }, +]; + +const ROLES = [ + "AUDITOR", + "BUSINESS_ADMIN", + "BUSINESS_BOOKKEEPER", + "BUSINESS_OWNER", + "BUSINESS_USER", + "DEVELOPER_ADMIN", + "GUEST_USER", + "IT_ADMIN", + "VENDOR_NETWORK_ADMIN", +]; + +const TRANSACTION_STATES = [ + "CLEARED", + "COMPLETION", + "DECLINED", + "ERROR", + "PENDING", + "PENDING_INITIATION", +]; + +const TRANSFER_STATUSES = [ + "ACH_CONFIRMED", + "CANCELED", + "COMPLETED", + "ERROR", + "INITIATED", + "NOT_ACKED", + "NOT_ENOUGH_FUNDS", + "PROCESSING_BY_ODFI", + "REJECTED_BY_ODFI", + "RETURNED_BY_RDFI", + "SUBMITTED_TO_FED", + "SUBMITTED_TO_RDFI", + "UNNECESSARY", + "UPLOADED", +]; + +const DEFAULT_PAGE_SIZE = 1000; + +export default { + CATEGORY_CODES, + ROLES, + DEFAULT_PAGE_SIZE, + TRANSACTION_STATES, + TRANSFER_STATUSES, +}; diff --git a/components/ramp/package.json b/components/ramp/package.json index 1a39850dd52c1..f3d2e10293486 100644 --- a/components/ramp/package.json +++ b/components/ramp/package.json @@ -1,21 +1,19 @@ { "name": "@pipedream/ramp", - "version": "0.0.3", + "version": "0.1.2", "description": "Pipedream Ramp Components", "main": "ramp.app.mjs", "keywords": [ "pipedream", "ramp" ], - "files": [ - "dist" - ], "homepage": "https://pipedream.com/apps/ramp", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.2.0" + "@pipedream/platform": "^3.0.0", + "uuid": "^10.0.0" } } diff --git a/components/ramp/ramp.app.mjs b/components/ramp/ramp.app.mjs index 28eae6e966b78..ea85829eb7e65 100644 --- a/components/ramp/ramp.app.mjs +++ b/components/ramp/ramp.app.mjs @@ -1,41 +1,261 @@ import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; export default { type: "app", app: "ramp", - propDefinitions: {}, + propDefinitions: { + userId: { + type: "string", + label: "User ID", + description: "The ID of the user", + async options({ prevContext }) { + return this.getPropOptions({ + prevContext, + resourceFn: this.listUsers, + mapper: ({ + id: value, first_name: firstName, last_name: lastName, + }) => ({ + value, + label: (`${firstName} ${lastName}`).trim(), + }), + }); + }, + }, + spendProgramId: { + type: "string", + label: "Spend Program ID", + description: "The ID of the spend program", + optional: true, + async options({ prevContext }) { + return this.getPropOptions({ + prevContext, + resourceFn: this.listSpendPrograms, + mapper: ({ + id: value, display_name: label, + }) => ({ + value, + label, + }), + }); + }, + }, + departmentId: { + type: "string", + label: "Department ID", + description: "Unique identifier of the employee's department", + optional: true, + async options({ prevContext }) { + return this.getPropOptions({ + prevContext, + resourceFn: this.listDepartments, + mapper: ({ + id: value, name: label, + }) => ({ + value, + label, + }), + }); + }, + }, + locationId: { + type: "string", + label: "Location ID", + description: "Unique identifier of the employee's location", + optional: true, + async options({ prevContext }) { + return this.getPropOptions({ + prevContext, + resourceFn: this.listLocations, + mapper: ({ + id: value, name: label, + }) => ({ + value, + label, + }), + }); + }, + }, + transactionId: { + type: "string", + label: "Transaction ID", + description: "The ID of a transaction", + async options({ prevContext }) { + return this.getPropOptions({ + prevContext, + resourceFn: this.listTransactions, + mapper: ({ + id: value, + merchant_name: merchantName, + amount, + currency_code: currencyCode, + user_transaction_time: userTransactionTime, + }) => ({ + value, + label: `${merchantName} - ${amount} ${currencyCode} - ${userTransactionTime}`, + }), + }); + }, + }, + allowedCategories: { + type: "integer[]", + label: "Allowed Categories", + description: "List of Ramp category codes allowed for the limit", + options: constants.CATEGORY_CODES, + optional: true, + }, + blockedCategories: { + type: "integer[]", + label: "Blocked Categories", + description: "List of Ramp category codes blocked for the limit", + options: constants.CATEGORY_CODES, + optional: true, + }, + role: { + type: "string", + label: "Role", + description: "The employee's role", + options: constants.ROLES, + }, + transactionState: { + type: "string", + label: "State", + description: "Filter transactions by the current state", + optional: true, + options: constants.TRANSACTION_STATE_OPTIONS, + }, + transferStatus: { + type: "string", + label: "Status", + description: "Filter transfers by the current status", + optional: true, + options: constants.TRANSFER_STATUSES, + }, + }, methods: { - _getBaseUrl() { + _baseUrl() { return "https://api.ramp.com/developer/v1"; }, - _getHeaders() { + _getHeaders(headers) { return { - "Content-Type": "application/json", - "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, }; }, - _getAxiosParams(opts = {}) { - const res = { - ...opts, - url: this._getBaseUrl() + opts.path, - headers: this._getHeaders(), - }; - return res; + async _makeRequest({ + $ = this, + path, + headers, + ...opts + }) { + try { + return await axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._getHeaders(headers), + ...opts, + }); + } catch (e) { + throw new Error(JSON.parse(e.message).error_v2.message); + } }, - async listTransactions(ctx = this, state, defaultPageSize, latestTransactionId) { - const params = { - order_by_date_asc: true, - page_size: defaultPageSize, - state, + async getPropOptions({ + prevContext, + resourceFn, + mapper, + }) { + const args = prevContext?.next + ? { + url: prevContext.next, + } + : {}; + + const { + data, page, + } = await resourceFn(args); + + return { + options: data.map(mapper), + context: { + next: page.next, + }, }; - if (latestTransactionId) { - params.start = latestTransactionId; - } - return axios(ctx, this._getAxiosParams({ + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users", + ...opts, + }); + }, + listSpendPrograms(opts = {}) { + return this._makeRequest({ + path: "/spend-programs", + ...opts, + }); + }, + listDepartments(opts = {}) { + return this._makeRequest({ + path: "/departments", + ...opts, + }); + }, + listLocations(opts = {}) { + return this._makeRequest({ + path: "/locations", + ...opts, + }); + }, + listTransactions(opts = {}) { + return this._makeRequest({ path: "/transactions", - method: "GET", - params, - })); + ...opts, + }); + }, + listTransfers(opts = {}) { + return this._makeRequest({ + path: "/transfers", + ...opts, + }); + }, + createLimit(opts = {}) { + return this._makeRequest({ + path: "/limits/deferred", + method: "POST", + ...opts, + }); + }, + createUserInvite(opts = {}) { + return this._makeRequest({ + path: "/users/deferred", + method: "POST", + ...opts, + }); + }, + uploadReceipt(opts = {}) { + return this._makeRequest({ + path: "/receipts", + method: "POST", + ...opts, + }); + }, + async *paginate({ + resourceFn, + params = {}, + }) { + const args = { + params: { + ...params, + page_size: constants.DEFAULT_PAGE_SIZE, + }, + }; + do { + const { + data, page, + } = await resourceFn(args); + for (const item of data) { + yield item; + } + args.url = page.next; + } while (args.url); }, }, }; diff --git a/components/ramp/sources/common/base.mjs b/components/ramp/sources/common/base.mjs new file mode 100644 index 0000000000000..72923213858d1 --- /dev/null +++ b/components/ramp/sources/common/base.mjs @@ -0,0 +1,58 @@ +import ramp from "../../ramp.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + ramp, + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + db: "$.service.db", + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getPreviousStatuses() { + return this.db.get("previousStatuses") || {}; + }, + _setPreviousStatuses(previousStatuses) { + this.db.set("previousStatuses", previousStatuses); + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + emitResults() { + throw new Error("emitResults is not implemented"); + }, + getParams() { + return {}; + }, + async processEvent(max) { + const resourceFn = this.getResourceFn(); + const params = this.getParams(); + + const items = this.ramp.paginate({ + resourceFn, + params, + }); + + const results = []; + for await (const item of items) { + results.push(item); + } + if (!results.length) { + return; + } + await this.emitResults(results, max); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/ramp/sources/common/common.mjs b/components/ramp/sources/common/common.mjs deleted file mode 100644 index d8a3e2cb562ea..0000000000000 --- a/components/ramp/sources/common/common.mjs +++ /dev/null @@ -1,8 +0,0 @@ -export default { - stateOptions: [ - "CLEARED", - "PENDING", - "DECLINED", - ], - defaultPageSize: 1000, -}; diff --git a/components/ramp/sources/new-transaction-created/new-transaction-created.mjs b/components/ramp/sources/new-transaction-created/new-transaction-created.mjs new file mode 100644 index 0000000000000..cb6750365a0d1 --- /dev/null +++ b/components/ramp/sources/new-transaction-created/new-transaction-created.mjs @@ -0,0 +1,69 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ramp-new-transaction-created", + name: "New Transaction Created", + description: "Emit new event for each new transaction created in Ramp.", + version: "0.0.2", + type: "source", + dedupe: "unique", + props: { + ...common.props, + departmentId: { + propDefinition: [ + common.props.ramp, + "departmentId", + ], + }, + locationId: { + propDefinition: [ + common.props.ramp, + "locationId", + ], + }, + }, + methods: { + ...common.methods, + _getLatestTransactionId() { + return this.db.get("latestTransactionId"); + }, + _setLatestTransactionId(id) { + this.db.set("latestTransactionId", id); + }, + getResourceFn() { + return this.ramp.listTransactions; + }, + getParams() { + const latestTransactionId = this._getLatestTransactionId(); + const params = { + order_by_date_asc: true, + department_id: this.departmentId, + location_id: this.locationId, + }; + if (latestTransactionId) { + params.start = latestTransactionId; + } + return params; + }, + generateMeta(transaction) { + return { + id: transaction.id, + summary: `New Transaction ID: ${transaction.id}`, + ts: Date.parse(transaction.user_transaction_time), + }; + }, + emitResults(results, max) { + if (max && results.length > max) { + results = results.slice(-1 * max); + } + this._setLatestTransactionId(results[results.length - 1].id); + results.reverse().forEach((transaction) => { + const meta = this.generateMeta(transaction); + this.$emit(transaction, meta); + }); + }, + }, + sampleEmit, +}; diff --git a/components/ramp/sources/new-transaction-created/test-event.mjs b/components/ramp/sources/new-transaction-created/test-event.mjs new file mode 100644 index 0000000000000..4719ae31659d2 --- /dev/null +++ b/components/ramp/sources/new-transaction-created/test-event.mjs @@ -0,0 +1,47 @@ +export default { + "settlement_date": "2021-02-07T13:26:48+00:00", + "state": "CLEARED", + "id": "c74326d3-a6b3-4a88-9a0c-4b61850784cd", + "merchant_name": "Facebook Ads", + "policy_violations": [], + "merchant_descriptor": "FACEBOOK ADS", + "line_items": [ + { + "amount": { + "amount": 13500, + "currency_code": "USD" + }, + "accounting_field_selections": [] + } + ], + "disputes": [], + "card_holder": { + "last_name": "Alhasawi", + "location_id": "b503bdcd-bddb-4382-be59-e5543a451e54", + "location_name": "Boston", + "department_name": "Implementations", + "first_name": "Dana", + "department_id": "1e10e754-512a-4d1f-8400-9d7023f4ee55", + "user_id": "002e077c-6d7e-4613-b03d-27709e64f8e0" + }, + "merchant_category_code": null, + "original_transaction_amount": { + "amount": 13500, + "currency_code": "USD" + }, + "sk_category_name": "Advertising", + "amount": 135, + "receipts": [], + "memo": "advertising budget", + "statement_id": "dacf769c-0991-4fae-8f16-0dbb190ebe1d", + "synced_at": null, + "currency_code": "USD", + "card_id": "4002ca56-5d98-44f7-8f01-db8ae013ecfb", + "merchant_category_code_description": null, + "accounting_categories": [], + "user_transaction_time": "2024-07-03T20:40:05+00:00", + "accounting_field_selections": [], + "entity_id": "dff9389a-2819-468e-a220-28e059b23f8e", + "merchant_id": "04aa535d-7699-4709-a98a-0278a73338fb", + "sk_category_id": 27 +} \ No newline at end of file diff --git a/components/ramp/sources/new-transactions/new-transactions.mjs b/components/ramp/sources/new-transactions/new-transactions.mjs deleted file mode 100644 index 543d00d543d94..0000000000000 --- a/components/ramp/sources/new-transactions/new-transactions.mjs +++ /dev/null @@ -1,53 +0,0 @@ -import ramp from "../../ramp.app.mjs"; -import common from "../common/common.mjs"; -import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; - -export default { - key: "ramp-new-transactions", - name: "New Transactions", - description: "Emit new transaction by retrieving all transactions of the business. [See Docs](https://docs.ramp.com/reference/rest/transactions)", - version: "0.0.3", - type: "source", - dedupe: "unique", - props: { - ramp, - state: { - type: "string", - label: "State", - description: "Filter transactions by its current state", - options: common.stateOptions, - }, - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - db: "$.service.db", - }, - methods: { - setLatestTransactionId(id) { - this.db.set("latest-transaction-id", id); - }, - getLatestTransactionId() { - return this.db.get("latest-transaction-id"); - }, - }, - async run({ $ }) { - const latestTransactionId = this.getLatestTransactionId(); - const transactions = await this.ramp.listTransactions( - $, - this.state, - common.defaultPageSize, - latestTransactionId, - ); - transactions.data.forEach((transaction) => { - this.$emit(transaction, { - id: transaction.id, - summary: `Transaction of $${transaction.amount} on ${transaction.merchant_name}`, - ts: transaction.user_transaction_time && +new Date(transaction.user_transaction_time), - }); - this.setLatestTransactionId(transaction.id); - }); - }, -}; diff --git a/components/ramp/sources/transaction-status-updated/test-event.mjs b/components/ramp/sources/transaction-status-updated/test-event.mjs new file mode 100644 index 0000000000000..4719ae31659d2 --- /dev/null +++ b/components/ramp/sources/transaction-status-updated/test-event.mjs @@ -0,0 +1,47 @@ +export default { + "settlement_date": "2021-02-07T13:26:48+00:00", + "state": "CLEARED", + "id": "c74326d3-a6b3-4a88-9a0c-4b61850784cd", + "merchant_name": "Facebook Ads", + "policy_violations": [], + "merchant_descriptor": "FACEBOOK ADS", + "line_items": [ + { + "amount": { + "amount": 13500, + "currency_code": "USD" + }, + "accounting_field_selections": [] + } + ], + "disputes": [], + "card_holder": { + "last_name": "Alhasawi", + "location_id": "b503bdcd-bddb-4382-be59-e5543a451e54", + "location_name": "Boston", + "department_name": "Implementations", + "first_name": "Dana", + "department_id": "1e10e754-512a-4d1f-8400-9d7023f4ee55", + "user_id": "002e077c-6d7e-4613-b03d-27709e64f8e0" + }, + "merchant_category_code": null, + "original_transaction_amount": { + "amount": 13500, + "currency_code": "USD" + }, + "sk_category_name": "Advertising", + "amount": 135, + "receipts": [], + "memo": "advertising budget", + "statement_id": "dacf769c-0991-4fae-8f16-0dbb190ebe1d", + "synced_at": null, + "currency_code": "USD", + "card_id": "4002ca56-5d98-44f7-8f01-db8ae013ecfb", + "merchant_category_code_description": null, + "accounting_categories": [], + "user_transaction_time": "2024-07-03T20:40:05+00:00", + "accounting_field_selections": [], + "entity_id": "dff9389a-2819-468e-a220-28e059b23f8e", + "merchant_id": "04aa535d-7699-4709-a98a-0278a73338fb", + "sk_category_id": 27 +} \ No newline at end of file diff --git a/components/ramp/sources/transaction-status-updated/transaction-status-updated.mjs b/components/ramp/sources/transaction-status-updated/transaction-status-updated.mjs new file mode 100644 index 0000000000000..0addeaa2f14b3 --- /dev/null +++ b/components/ramp/sources/transaction-status-updated/transaction-status-updated.mjs @@ -0,0 +1,71 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ramp-transaction-status-updated", + name: "Transaction Status Updated", + description: "Emit new event when there is a change in transaction status.", + version: "0.0.2", + type: "source", + dedupe: "unique", + props: { + ...common.props, + departmentId: { + propDefinition: [ + common.props.ramp, + "departmentId", + ], + }, + locationId: { + propDefinition: [ + common.props.ramp, + "locationId", + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.ramp.listTransactions; + }, + getParams() { + return { + order_by_date_asc: true, + department_id: this.departmentId, + location_id: this.locationId, + }; + }, + generateMeta(transaction) { + const ts = Date.now(); + return { + id: `${transaction.id}-${ts}`, + summary: `Status updated for transaction ID: ${transaction.id}`, + ts, + }; + }, + emitResults(results, max) { + const previousStatuses = this._getPreviousStatuses(); + let transactions = []; + for (const transaction of results) { + if (previousStatuses[transaction.id] + && previousStatuses[transaction.id] !== transaction.state) { + transactions.push(transaction); + } + previousStatuses[transaction.id] = transaction.state; + } + this._setPreviousStatuses(previousStatuses); + if (!transactions.length) { + return; + } + if (max && transactions.length > max) { + transactions = transactions.slice(-1 * max); + } + transactions.reverse().forEach((transaction) => { + const meta = this.generateMeta(transaction); + this.$emit(transaction, meta); + }); + }, + }, + sampleEmit, +}; diff --git a/components/ramp/sources/transfer-payment-updated/test-event.mjs b/components/ramp/sources/transfer-payment-updated/test-event.mjs new file mode 100644 index 0000000000000..1dc9052d8d053 --- /dev/null +++ b/components/ramp/sources/transfer-payment-updated/test-event.mjs @@ -0,0 +1,10 @@ +export default { + "status":"COMPLETED", + "amount": { + "currency_code":"USD", + "amount":435015 + }, + "entity_id":"dff9389a-2819-468e-a220-28e059b23f8e", + "id":"c219c973-d35b-4fa3-b7d2-fe620e5682d2", + "created_at":"2024-07-03T20:37:23+00:00", +} \ No newline at end of file diff --git a/components/ramp/sources/transfer-payment-updated/transfer-payment-updated.mjs b/components/ramp/sources/transfer-payment-updated/transfer-payment-updated.mjs new file mode 100644 index 0000000000000..a878fa2e15f71 --- /dev/null +++ b/components/ramp/sources/transfer-payment-updated/transfer-payment-updated.mjs @@ -0,0 +1,62 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ramp-transfer-payment-updated", + name: "Transfer Payment Updated", + description: "Emit new event when the status of a transfer payment changes", + version: "0.0.2", + type: "source", + dedupe: "unique", + props: { + ...common.props, + transferStatus: { + propDefinition: [ + common.props.ramp, + "transferStatus", + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.ramp.listTransfers; + }, + getParams() { + return { + status: this.transferStatus, + }; + }, + generateMeta(transfer) { + const ts = Date.now(); + return { + id: `${transfer.id}-${ts}`, + summary: `Status updated for transfer ID: ${transfer.id}`, + ts, + }; + }, + emitResults(results, max) { + const previousStatuses = this._getPreviousStatuses(); + let transfers = []; + for (const transfer of results) { + if (previousStatuses[transfer.id] && previousStatuses[transfer.id] !== transfer.status) { + transfers.push(transfer); + } + previousStatuses[transfer.id] = transfer.status; + } + this._setPreviousStatuses(previousStatuses); + if (!transfers.length) { + return; + } + if (max && transfers.length > max) { + transfers = transfers.slice(-1 * max); + } + transfers.reverse().forEach((transfer) => { + const meta = this.generateMeta(transfer); + this.$emit(transfer, meta); + }); + }, + }, + sampleEmit, +}; diff --git a/components/ramp_sandbox/README.md b/components/ramp_sandbox/README.md new file mode 100644 index 0000000000000..0dcbf383374e2 --- /dev/null +++ b/components/ramp_sandbox/README.md @@ -0,0 +1,14 @@ +# Overview + +The Ramp (Sandbox) API provides programmatic access to manage corporate cards and spending in a controlled environment, ideal for testing and development. With this API on Pipedream, developers can automate financial operations, synchronize accounting data, and streamline expense management workflows. Leveraging Pipedream's capabilities, users can integrate Ramp with other services to enhance financial visibility, automate reconciliation processes, and trigger actions based on spending activities. + +# Example Use Cases + +- **Automated Expense Reporting** + Connect the Ramp (Sandbox) API to Google Sheets on Pipedream to automatically update a spreadsheet whenever a new transaction is recorded. This workflow can help finance teams maintain real-time visibility of expenses without manual data entry. + +- **Real-Time Budget Alerts** + Use the Ramp (Sandbox) API with Slack on Pipedream to send instant notifications to a designated channel whenever a transaction exceeds a predefined threshold. This is crucial for teams that want to keep tight control over their budgets and prevent overspending. + +- **Synchronized Financial Records** + Integrate Ramp (Sandbox) with QuickBooks on Pipedream to automatically sync transaction details and receipts for seamless accounting. This workflow automates the reconciliation process, reducing errors and saving time for accounting departments. diff --git a/components/ramp_sandbox/actions/create-user-invite/create-user-invite.mjs b/components/ramp_sandbox/actions/create-user-invite/create-user-invite.mjs new file mode 100644 index 0000000000000..24d203ef1a7e5 --- /dev/null +++ b/components/ramp_sandbox/actions/create-user-invite/create-user-invite.mjs @@ -0,0 +1,56 @@ +import ramp from "../../ramp_sandbox.app.mjs"; +import createUserInvite from "../../../ramp/actions/create-user-invite/create-user-invite.mjs"; + +export default { + ...createUserInvite, + key: "ramp_sandbox-create-user-invite", + name: "Create User Invite", + description: "Sends out an invite for a new user. [See the documentation](https://docs.ramp.com/developer-api/v1/reference/rest/users#post-developer-v1-users-deferred)", + version: "0.0.2", + type: "action", + props: { + ramp, + email: { + type: "string", + label: "Email", + description: "The employee's email address", + }, + firstName: { + type: "string", + label: "First Name", + description: "First name of the employee", + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the employee", + }, + role: { + propDefinition: [ + ramp, + "role", + ], + }, + departmentId: { + propDefinition: [ + ramp, + "departmentId", + ], + }, + directManagerId: { + propDefinition: [ + ramp, + "userId", + ], + label: "Direct Manager ID", + description: "Unique identifier of the employee's direct manager", + optional: true, + }, + locationId: { + propDefinition: [ + ramp, + "locationId", + ], + }, + }, +}; diff --git a/components/ramp_sandbox/actions/issue-virtual-card/issue-virtual-card.mjs b/components/ramp_sandbox/actions/issue-virtual-card/issue-virtual-card.mjs new file mode 100644 index 0000000000000..21451639ceabe --- /dev/null +++ b/components/ramp_sandbox/actions/issue-virtual-card/issue-virtual-card.mjs @@ -0,0 +1,52 @@ +import ramp from "../../ramp_sandbox.app.mjs"; +import issueVirtualCard from "../../../ramp/actions/issue-virtual-card/issue-virtual-card.mjs"; + +export default { + ...issueVirtualCard, + key: "ramp_sandbox-issue-virtual-card", + name: "Issue Virtual Card", + description: "Creates a new virtual card for a given user. [See the documentation](https://docs.ramp.com/developer-api/v1/reference/rest/limits#post-developer-v1-limits-deferred)", + version: "0.0.3", + type: "action", + props: { + ramp, + displayName: { + type: "string", + label: "Virtual Card Name", + description: "The name of the virtual card", + }, + userId: { + propDefinition: [ + ramp, + "userId", + ], + }, + linkToSpendProgram: { + type: "boolean", + label: "Link to Existing Spend Program", + description: "Whether to link the card to an existing spend program", + reloadProps: true, + }, + spendProgramId: { + propDefinition: [ + ramp, + "spendProgramId", + ], + hidden: true, + }, + allowedCategories: { + propDefinition: [ + ramp, + "allowedCategories", + ], + hidden: true, + }, + blockedCategories: { + propDefinition: [ + ramp, + "blockedCategories", + ], + hidden: true, + }, + }, +}; diff --git a/components/ramp_sandbox/actions/upload-receipt/upload-receipt.mjs b/components/ramp_sandbox/actions/upload-receipt/upload-receipt.mjs new file mode 100644 index 0000000000000..330653ad1116b --- /dev/null +++ b/components/ramp_sandbox/actions/upload-receipt/upload-receipt.mjs @@ -0,0 +1,31 @@ +import ramp from "../../ramp_sandbox.app.mjs"; +import uploadReceipt from "../../../ramp/actions/upload-receipt/upload-receipt.mjs"; + +export default { + ...uploadReceipt, + key: "ramp_sandbox-upload-receipt", + name: "Upload Receipt", + description: "Uploads a receipt for a given transaction and user. [See the documentation](https://docs.ramp.com/developer-api/v1/reference/rest/receipts#post-developer-v1-receipts)", + version: "0.0.2", + type: "action", + props: { + ramp, + transactionId: { + propDefinition: [ + ramp, + "transactionId", + ], + }, + userId: { + propDefinition: [ + ramp, + "userId", + ], + }, + filePath: { + type: "string", + label: "File Path", + description: "The path to a file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + }, + }, +}; diff --git a/components/ramp_sandbox/package.json b/components/ramp_sandbox/package.json new file mode 100644 index 0000000000000..cb839997ef866 --- /dev/null +++ b/components/ramp_sandbox/package.json @@ -0,0 +1,20 @@ +{ + "name": "@pipedream/ramp_sandbox", + "version": "0.1.2", + "description": "Pipedream Ramp (Sandbox) Components", + "main": "ramp_sandbox.app.mjs", + "keywords": [ + "pipedream", + "ramp_sandbox" + ], + "homepage": "https://pipedream.com/apps/ramp_sandbox", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0", + "@pipedream/ramp": "^0.1.2", + "uuid": "^10.0.0" + } +} diff --git a/components/ramp_sandbox/ramp_sandbox.app.mjs b/components/ramp_sandbox/ramp_sandbox.app.mjs new file mode 100644 index 0000000000000..0482f8b371e3f --- /dev/null +++ b/components/ramp_sandbox/ramp_sandbox.app.mjs @@ -0,0 +1,21 @@ +import ramp from "@pipedream/ramp"; + +export default { + type: "app", + app: "ramp_sandbox", + propDefinitions: { + ...ramp.propDefinitions, + }, + methods: { + ...ramp.methods, + _baseUrl() { + return "https://demo-api.ramp.com/developer/v1"; + }, + _getHeaders(headers) { + return { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + }, +}; diff --git a/components/ramp_sandbox/sources/common/base.mjs b/components/ramp_sandbox/sources/common/base.mjs new file mode 100644 index 0000000000000..790d257a5f09b --- /dev/null +++ b/components/ramp_sandbox/sources/common/base.mjs @@ -0,0 +1,17 @@ +import ramp from "../../ramp_sandbox.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import common from "@pipedream/ramp/sources/common/base.mjs"; + +export default { + ...common, + props: { + ramp, + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + db: "$.service.db", + }, +}; diff --git a/components/ramp_sandbox/sources/new-transaction-created/new-transaction-created.mjs b/components/ramp_sandbox/sources/new-transaction-created/new-transaction-created.mjs new file mode 100644 index 0000000000000..ea459f0d727cf --- /dev/null +++ b/components/ramp_sandbox/sources/new-transaction-created/new-transaction-created.mjs @@ -0,0 +1,33 @@ +import common from "../common/base.mjs"; +import newTransactionCreated from "@pipedream/ramp/sources/new-transaction-created/new-transaction-created.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ramp_sandbox-new-transaction-created", + name: "New Transaction Created", + description: "Emit new event for each new transaction created in Ramp.", + version: "0.0.2", + type: "source", + dedupe: "unique", + props: { + ...common.props, + departmentId: { + propDefinition: [ + common.props.ramp, + "departmentId", + ], + }, + locationId: { + propDefinition: [ + common.props.ramp, + "locationId", + ], + }, + }, + methods: { + ...common.methods, + ...newTransactionCreated.methods, + }, + sampleEmit, +}; diff --git a/components/ramp_sandbox/sources/new-transaction-created/test-event.mjs b/components/ramp_sandbox/sources/new-transaction-created/test-event.mjs new file mode 100644 index 0000000000000..4719ae31659d2 --- /dev/null +++ b/components/ramp_sandbox/sources/new-transaction-created/test-event.mjs @@ -0,0 +1,47 @@ +export default { + "settlement_date": "2021-02-07T13:26:48+00:00", + "state": "CLEARED", + "id": "c74326d3-a6b3-4a88-9a0c-4b61850784cd", + "merchant_name": "Facebook Ads", + "policy_violations": [], + "merchant_descriptor": "FACEBOOK ADS", + "line_items": [ + { + "amount": { + "amount": 13500, + "currency_code": "USD" + }, + "accounting_field_selections": [] + } + ], + "disputes": [], + "card_holder": { + "last_name": "Alhasawi", + "location_id": "b503bdcd-bddb-4382-be59-e5543a451e54", + "location_name": "Boston", + "department_name": "Implementations", + "first_name": "Dana", + "department_id": "1e10e754-512a-4d1f-8400-9d7023f4ee55", + "user_id": "002e077c-6d7e-4613-b03d-27709e64f8e0" + }, + "merchant_category_code": null, + "original_transaction_amount": { + "amount": 13500, + "currency_code": "USD" + }, + "sk_category_name": "Advertising", + "amount": 135, + "receipts": [], + "memo": "advertising budget", + "statement_id": "dacf769c-0991-4fae-8f16-0dbb190ebe1d", + "synced_at": null, + "currency_code": "USD", + "card_id": "4002ca56-5d98-44f7-8f01-db8ae013ecfb", + "merchant_category_code_description": null, + "accounting_categories": [], + "user_transaction_time": "2024-07-03T20:40:05+00:00", + "accounting_field_selections": [], + "entity_id": "dff9389a-2819-468e-a220-28e059b23f8e", + "merchant_id": "04aa535d-7699-4709-a98a-0278a73338fb", + "sk_category_id": 27 +} \ No newline at end of file diff --git a/components/ramp_sandbox/sources/transaction-status-updated/test-event.mjs b/components/ramp_sandbox/sources/transaction-status-updated/test-event.mjs new file mode 100644 index 0000000000000..4719ae31659d2 --- /dev/null +++ b/components/ramp_sandbox/sources/transaction-status-updated/test-event.mjs @@ -0,0 +1,47 @@ +export default { + "settlement_date": "2021-02-07T13:26:48+00:00", + "state": "CLEARED", + "id": "c74326d3-a6b3-4a88-9a0c-4b61850784cd", + "merchant_name": "Facebook Ads", + "policy_violations": [], + "merchant_descriptor": "FACEBOOK ADS", + "line_items": [ + { + "amount": { + "amount": 13500, + "currency_code": "USD" + }, + "accounting_field_selections": [] + } + ], + "disputes": [], + "card_holder": { + "last_name": "Alhasawi", + "location_id": "b503bdcd-bddb-4382-be59-e5543a451e54", + "location_name": "Boston", + "department_name": "Implementations", + "first_name": "Dana", + "department_id": "1e10e754-512a-4d1f-8400-9d7023f4ee55", + "user_id": "002e077c-6d7e-4613-b03d-27709e64f8e0" + }, + "merchant_category_code": null, + "original_transaction_amount": { + "amount": 13500, + "currency_code": "USD" + }, + "sk_category_name": "Advertising", + "amount": 135, + "receipts": [], + "memo": "advertising budget", + "statement_id": "dacf769c-0991-4fae-8f16-0dbb190ebe1d", + "synced_at": null, + "currency_code": "USD", + "card_id": "4002ca56-5d98-44f7-8f01-db8ae013ecfb", + "merchant_category_code_description": null, + "accounting_categories": [], + "user_transaction_time": "2024-07-03T20:40:05+00:00", + "accounting_field_selections": [], + "entity_id": "dff9389a-2819-468e-a220-28e059b23f8e", + "merchant_id": "04aa535d-7699-4709-a98a-0278a73338fb", + "sk_category_id": 27 +} \ No newline at end of file diff --git a/components/ramp_sandbox/sources/transaction-status-updated/transaction-status-updated.mjs b/components/ramp_sandbox/sources/transaction-status-updated/transaction-status-updated.mjs new file mode 100644 index 0000000000000..1278401903651 --- /dev/null +++ b/components/ramp_sandbox/sources/transaction-status-updated/transaction-status-updated.mjs @@ -0,0 +1,33 @@ +import common from "../common/base.mjs"; +import transactionStatusUpdated from "@pipedream/ramp/sources/transaction-status-updated/transaction-status-updated.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ramp_sandbox-transaction-status-updated", + name: "Transaction Status Updated", + description: "Emit new event when there is a change in transaction status.", + version: "0.0.2", + type: "source", + dedupe: "unique", + props: { + ...common.props, + departmentId: { + propDefinition: [ + common.props.ramp, + "departmentId", + ], + }, + locationId: { + propDefinition: [ + common.props.ramp, + "locationId", + ], + }, + }, + methods: { + ...common.methods, + ...transactionStatusUpdated.methods, + }, + sampleEmit, +}; diff --git a/components/ramp_sandbox/sources/transfer-payment-updated/test-event.mjs b/components/ramp_sandbox/sources/transfer-payment-updated/test-event.mjs new file mode 100644 index 0000000000000..1dc9052d8d053 --- /dev/null +++ b/components/ramp_sandbox/sources/transfer-payment-updated/test-event.mjs @@ -0,0 +1,10 @@ +export default { + "status":"COMPLETED", + "amount": { + "currency_code":"USD", + "amount":435015 + }, + "entity_id":"dff9389a-2819-468e-a220-28e059b23f8e", + "id":"c219c973-d35b-4fa3-b7d2-fe620e5682d2", + "created_at":"2024-07-03T20:37:23+00:00", +} \ No newline at end of file diff --git a/components/ramp_sandbox/sources/transfer-payment-updated/transfer-payment-updated.mjs b/components/ramp_sandbox/sources/transfer-payment-updated/transfer-payment-updated.mjs new file mode 100644 index 0000000000000..12192b442a323 --- /dev/null +++ b/components/ramp_sandbox/sources/transfer-payment-updated/transfer-payment-updated.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import transferPaymentUpdated from "@pipedream/ramp/sources/transfer-payment-updated/transfer-payment-updated.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ramp_sandbox-transfer-payment-updated", + name: "Transfer Payment Updated", + description: "Emit new event when the status of a transfer payment changes", + version: "0.0.2", + type: "source", + dedupe: "unique", + props: { + ...common.props, + transferStatus: { + propDefinition: [ + common.props.ramp, + "transferStatus", + ], + }, + }, + methods: { + ...common.methods, + ...transferPaymentUpdated.methods, + }, + sampleEmit, +}; diff --git a/components/range/README.md b/components/range/README.md index 450964c2b6e36..488057317905f 100644 --- a/components/range/README.md +++ b/components/range/README.md @@ -1,24 +1,11 @@ # Overview -Range can help you build great products by making it easy to store, version -control, and collaborate on code and designs. The [Range API](https://range.co) -is a powerful tool that you can use to build amazing products. Here are some -examples of what you can build using Range's API: +The Range API provides a platform for managing print shop operations, including job status tracking, quoting, invoicing, and scheduling. Leveraging Pipedream, users can automate workflows between Range and other apps, creating a seamless integration that boosts productivity. Whether it's syncing order details with a CRM, triggering notifications based on job status updates, or automating invoice creation, Pipedream acts as a conduit for connecting Range's print shop management capabilities with a myriad of other services. -- A Visual Design Editor - You can use the Range API to build a graphical - editor for visual design. With the API, you can easily create and manipulate - complex layouts, fonts, colors, and more. -- Version Control - The Range API makes it easy to store different versions of - an application, store code changes in multiple branches, and collaborate on - code. -- Collaborative Workspaces - Create collaborative workspaces with the Range API - that can be used to share and collaborate on code, designs, and more. -- A Knowledge Base - Easily create and manage a knowledge base where developers - and designers can go to answer questions and learn new skills. -- Performance Monitoring - With the Range API, you can track the performance of - applications and services in real time. -- Automation - Automate tedious development and design processes with the Range - API. You can create automated flows between core development and design - processes, such as code review and deployment. -- Machine Learning - Use the Range API to build smarter applications by - integrating machine learning into the development and design processes. +# Example Use Cases + +- **Automated Job Status Updates to Slack**: Use Pipedream to monitor changes in job statuses on Range and automatically send custom notifications to a Slack channel. This keeps teams informed in real-time about production flow without manual checks. + +- **Dynamic Invoice Generation with QuickBooks**: Configure a workflow where Pipedream listens for new completed jobs in Range and generates invoices in QuickBooks. This seamless connection ensures that accounting is updated without the need for manual data entry. + +- **CRM Synchronization with HubSpot**: Establish a Pipedream workflow that syncs new customer data from Range to HubSpot. Whenever a new order is placed, the customer's details are updated in the CRM, ensuring the sales team has the latest information for follow-ups and relationship management. diff --git a/components/rapid7_insight_platform/README.md b/components/rapid7_insight_platform/README.md new file mode 100644 index 0000000000000..d22e207891cbc --- /dev/null +++ b/components/rapid7_insight_platform/README.md @@ -0,0 +1,11 @@ +# Overview + +The Rapid7 Insight Platform API allows users to automate and integrate their security data with other applications. By harnessing this API on Pipedream, you can create custom workflows to streamline security operations, such as automating threat detection, managing vulnerabilities, or orchestrating incident response. This means you can connect the Insight Platform's capabilities with numerous other apps to trigger actions, process data, and get real-time insights, all within Pipedream's low-code environment. + +# Example Use Cases + +- **Automated Threat Detection and Notification**: Use Rapid7 Insight Platform to continuously monitor for threats. When a threat is detected, use Pipedream to automate the process of sending an alert through communication platforms like Slack or Email, ensuring your team is informed instantaneously. + +- **Vulnerability Management Workflow**: Integrate Rapid7's vulnerability data into a workflow that automatically creates tickets in a project management tool like Jira. This workflow could assess the severity of vulnerabilities, prioritize them, and assign them to the appropriate team for remediation within your organization's operational processes. + +- **Incident Response Coordination**: Combine the Rapid7 Insight Platform with incident management tools like PagerDuty. When an incident is flagged by Rapid7, trigger a workflow in Pipedream to create an incident in PagerDuty, page the on-call engineer, and post incident details to a dedicated Microsoft Teams or Slack channel for cross-team visibility. diff --git a/components/rapid_url_indexer/actions/download-project-report/download-project-report.mjs b/components/rapid_url_indexer/actions/download-project-report/download-project-report.mjs new file mode 100644 index 0000000000000..25e91261cc729 --- /dev/null +++ b/components/rapid_url_indexer/actions/download-project-report/download-project-report.mjs @@ -0,0 +1,39 @@ +import rapidUrlIndexer from "../../rapid_url_indexer.app.mjs"; +import fs from "fs"; + +export default { + key: "rapid_url_indexer-download-project-report", + name: "Download Project Report", + description: "Download the report for a specific project. [See the documentation](https://rapidurlindexer.com/indexing-api/).", + type: "action", + version: "0.0.1", + props: { + rapidUrlIndexer, + projectId: { + propDefinition: [ + rapidUrlIndexer, + "projectId", + ], + }, + filename: { + type: "string", + label: "Filename", + description: "A filename to save the report as in the `/tmp` directory. Include the `.csv` extension", + }, + }, + async run({ $ }) { + const response = await this.rapidUrlIndexer.downloadProjectReport({ + $, + projectId: this.projectId, + }); + + const filepath = this.filename.includes("/tmp") + ? this.filename + : `/tmp/${this.filename}`; + + fs.writeFileSync(filepath, response); + + $.export("$summary", `Successfully downloaded report for Project with ID ${this.projectId}`); + return filepath; + }, +}; diff --git a/components/rapid_url_indexer/actions/get-project-status/get-project-status.mjs b/components/rapid_url_indexer/actions/get-project-status/get-project-status.mjs new file mode 100644 index 0000000000000..0dce39b75f554 --- /dev/null +++ b/components/rapid_url_indexer/actions/get-project-status/get-project-status.mjs @@ -0,0 +1,28 @@ +import rapidUrlIndexer from "../../rapid_url_indexer.app.mjs"; + +export default { + key: "rapid_url_indexer-get-project-status", + name: "Get Project Status", + description: "Get the status of a specific project. [See the documentation](https://rapidurlindexer.com/indexing-api/).", + type: "action", + version: "0.0.1", + props: { + rapidUrlIndexer, + projectId: { + propDefinition: [ + rapidUrlIndexer, + "projectId", + ], + }, + }, + async run({ $ }) { + const response = await this.rapidUrlIndexer.getProjectStatus({ + $, + projectId: this.projectId, + }); + if (response.id) { + $.export("$summary", `Successfully retrieved status for Project with ID ${response.id}`); + } + return response; + }, +}; diff --git a/components/rapid_url_indexer/actions/submit-project/submit-project.mjs b/components/rapid_url_indexer/actions/submit-project/submit-project.mjs new file mode 100644 index 0000000000000..8ef0cf06d6a4c --- /dev/null +++ b/components/rapid_url_indexer/actions/submit-project/submit-project.mjs @@ -0,0 +1,40 @@ +import rapidUrlIndexer from "../../rapid_url_indexer.app.mjs"; + +export default { + key: "rapid_url_indexer-submit-project", + name: "Submit Project", + description: "Submit a new project for indexing. [See the documentation](https://rapidurlindexer.com/indexing-api/).", + type: "action", + version: "0.0.1", + props: { + rapidUrlIndexer, + name: { + type: "string", + label: "Project Name", + description: "The name of the project", + }, + urls: { + type: "string[]", + label: "URLs", + description: "An array of URLs to index. URLs must start with either “http://” or “https://”.", + }, + notifyOnStatusChange: { + type: "boolean", + label: "Notify on Status Change", + description: "If set to `true`, you will receive email notifications when the project status changes", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.rapidUrlIndexer.submitProject({ + $, + data: { + project_name: this.name, + urls: this.urls, + notify_on_status_change: this.notifyOnStatusChange, + }, + }); + $.export("$summary", `${response.message}`); + return response; + }, +}; diff --git a/components/rapid_url_indexer/package.json b/components/rapid_url_indexer/package.json new file mode 100644 index 0000000000000..d3d9f342f3855 --- /dev/null +++ b/components/rapid_url_indexer/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/rapid_url_indexer", + "version": "0.1.0", + "description": "Pipedream Rapid URL Indexer Components", + "main": "rapid_url_indexer.app.mjs", + "keywords": [ + "pipedream", + "rapid_url_indexer" + ], + "homepage": "https://pipedream.com/apps/rapid_url_indexer", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/rapid_url_indexer/rapid_url_indexer.app.mjs b/components/rapid_url_indexer/rapid_url_indexer.app.mjs new file mode 100644 index 0000000000000..d5cda1868589d --- /dev/null +++ b/components/rapid_url_indexer/rapid_url_indexer.app.mjs @@ -0,0 +1,54 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "rapid_url_indexer", + propDefinitions: { + projectId: { + type: "string", + label: "Project ID", + description: "The identifier of a project", + }, + }, + methods: { + _baseUrl() { + return "https://rapidurlindexer.com/wp-json/api/v1"; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "X-API-Key": `${this.$auth.api_key}`, + }, + ...opts, + }); + }, + getProjectStatus({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/projects/${projectId}`, + ...opts, + }); + }, + downloadProjectReport({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/projects/${projectId}/report`, + ...opts, + }); + }, + submitProject(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/projects", + ...opts, + }); + }, + }, +}; diff --git a/components/rapidapi/README.md b/components/rapidapi/README.md index cc86fc787451e..859e8dbd4a5c0 100644 --- a/components/rapidapi/README.md +++ b/components/rapidapi/README.md @@ -1,23 +1,11 @@ # Overview -RapidAPI is an API marketplace that enables developers to quickly access & test -thousands of API’s in one platform. With RapidAPI, developers can browse, -connect, and access API's from a wide array of external sources and can quickly -develop API integrations for their applications. +RapidAPI is a platform that lets you connect to thousands of APIs with ease. By using RapidAPI with Pipedream, you can create powerful serverless workflows that automate API interactions, streamline data processing, and integrate various services seamlessly. Imagine stitching together APIs for machine learning, data enrichment, communication, and more, without managing infrastructure. You can trigger workflows with HTTP requests, emails, or on a schedule, and process the data with built-in Pipedream steps or custom Node.js code. -Here are some of the things you can build using the RapidAPI API: +# Example Use Cases -- Chatbots: Create using Natural Language Processing API to create - sophisticated chatbots. -- Weather applications: Gather live weather data from global organizations - using RapidAPI. -- Games: Integrate 3D graphics and gaming models using RapidAPI. -- Dynamic Maps: Use Location APIs to build intuitive, interactive maps. -- Corporate applications: Use RapidAPI to build financial and administrative - applications. -- Social networks: Create social networks with RapidAPI powered content APIs. -- Music players: Integrate streaming audio and net audio services with - RapidAPI. -- Automation: Create automated systems that can connect to external APIs. -- Machine Learning Applications: Create applications that leverage predictive - and insights through ML models. +- **Automated Sentiment Analysis**: Combine RapidAPI's text analysis APIs with Pipedream to monitor social media. Whenever a new post mentioning your brand is found, analyze the sentiment and alert your team via Slack if the sentiment is negative. + +- **Stock Market Alert System**: Use RapidAPI to fetch stock prices and financial data, then set up Pipedream to check the data against your thresholds. If certain criteria are met, automatically notify you via email or SMS, helping you make timely investment decisions. + +- **Content Aggregator and Distributor**: Pull content from various sources via RapidAPI, such as news, blogs, or video platforms. Pipedream can process and aggregate the data, then republish it to your website, app, or social media channels, keeping your content fresh and engaging. diff --git a/components/rapidapi/package.json b/components/rapidapi/package.json new file mode 100644 index 0000000000000..6dc62ca6e421f --- /dev/null +++ b/components/rapidapi/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/rapidapi", + "version": "0.6.0", + "description": "Pipedream rapidapi Components", + "main": "rapidapi.app.mjs", + "keywords": [ + "pipedream", + "rapidapi" + ], + "homepage": "https://pipedream.com/apps/rapidapi", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/rasa/README.md b/components/rasa/README.md index 9aeab1893406a..2d3fd350126a1 100644 --- a/components/rasa/README.md +++ b/components/rasa/README.md @@ -1,17 +1,11 @@ # Overview -Rasa is an open source artificial intelligence platform for building contextual -chatbots and assistants. With Rasa's open source API, you can build a variety -of conversational AI applications quickly and easily. +Rasa is an open-source platform for building conversational AI applications, including chatbots and voice assistants. It offers robust API endpoints for training models, managing conversations, and interpreting user messages, thus enabling the development of sophisticated AI-driven communication tools. When used with Pipedream, Rasa can automate dialogue flow, extract insights from conversation data, or trigger actions in other apps based on conversational cues. -Rasa has a wide range of capabilities that can be used to create intelligent, -scalable, and secure conversational AI applications. Using the Rasa API, you -can design and create: +# Example Use Cases -- Chatbots for customer service -- Personal assistants for home automation -- Voice-controlled bots for hands-free interaction -- AI-driven customer feedback systems -- Context-aware marketing bots -- Enterprise-level dialogue management systems -- Multi-lingual assistants for international users +- **Customer Support Automation**: Connect Rasa to a CRM tool like Salesforce on Pipedream. Every time Rasa processes a customer's question and recognizes an issue, a new case is created in Salesforce, tagged with the conversation details and assigned to the appropriate support team. + +- **Survey Collection Bot**: Use Rasa to deploy a chatbot that conducts surveys. On Pipedream, set up a workflow where each completed survey interaction triggers a Google Sheets API to log responses. This automates data collection and centralization for analysis, saving time and ensuring organized data management. + +- **Event-Driven Notifications**: Pair Rasa with the Slack API on Pipedream to send notifications. When Rasa's NLU engine detects specific entities or intents like 'urgent' during a conversation, trigger a workflow that sends a Slack message to a designated channel or user, ensuring fast response times for critical issues. diff --git a/components/rasa/package.json b/components/rasa/package.json new file mode 100644 index 0000000000000..bb249fac6adb9 --- /dev/null +++ b/components/rasa/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/rasa", + "version": "0.6.0", + "description": "Pipedream rasa Components", + "main": "rasa.app.mjs", + "keywords": [ + "pipedream", + "rasa" + ], + "homepage": "https://pipedream.com/apps/rasa", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/rat_genome_database/README.md b/components/rat_genome_database/README.md index 0c746ec3b4f5f..a53c135cd3925 100644 --- a/components/rat_genome_database/README.md +++ b/components/rat_genome_database/README.md @@ -1,30 +1,11 @@ # Overview -The Rat Genome Database (RGD) is a public, online resource that provides -genetic information about the rat. It is a powerful tool for researchers and -developers, enabling them to access and use data archived in the RGD database. -The RGD API has many uses, from research to data integration, and can help you -create useful applications for rat-related data. +The Rat Genome Database (RGD) API provides access to a wealth of genetic and genomic data related to rats, a key model organism in medical research. Via this API, researchers can query for gene information, phenotypic data, and genomic sequences, creating a treasure trove for geneticists, bioinformaticians, and medical researchers seeking to understand disease pathways and potential treatments. On Pipedream, this can be leveraged to automate data retrieval, sync genetic information with other databases, or trigger workflows based on specific genomic updates or criteria. -Here are some examples of what you can build using the RGD API: +# Example Use Cases -- Create an application that displays the gene expression profiles of rat - genes. -- Develop a program that uses the search capabilities provided by the RGD API - to search rat genetic data. -- Develop a web-based program that provides genetic analysis tools for use in - research. -- Write a program that uses the RGD API to access sequence information for - specific rat genes. -- Use the RGD API to build an application that can compare genetic codes - between species. -- Create a research project that searches through rat data stored in the RGD - database. -- Design a web application that enables integration of rat gene data into - existing databases. -- Use the RGD API to assemble data on disease models and other research topics. +- **Automated Genetic Data Sync**: Sync rat genetic data from RGD to a research database like MongoDB on a regular basis. When new genes or phenotypes are added to RGD, a Pipedream workflow can be triggered, automatically fetching and inserting this data into the MongoDB database, ensuring researchers always have access to the latest data without manual intervention. -With the RGD API you can quickly build applications and projects that access, -use and analyse rat genetic data. So if you are a researcher looking for a tool -to help you tackle a rat-related research project, the RGD API is a great place -to start. +- **Disease Research Notification System**: Set up a Pipedream workflow that monitors RGD for updates on specific genes or phenotypic data related to a particular disease. When updates are detected, the workflow sends out email notifications using the SendGrid app to a list of subscribed researchers, keeping them informed of the latest findings and potentially accelerating the pace of medical research. + +- **Genomic Data Analysis Pipeline**: Trigger a data analysis pipeline whenever new genomic sequences are released on RGD. A Pipedream workflow could invoke AWS Lambda functions to process and analyze the data, comparing it with human genome datasets from apps like Ensembl or NCBI. The results could then be stored in AWS S3 and a summary report sent to the research team, providing valuable insights into comparative genomics and cross-species disease models. diff --git a/components/ratecard/README.md b/components/ratecard/README.md new file mode 100644 index 0000000000000..a877bc1ac593c --- /dev/null +++ b/components/ratecard/README.md @@ -0,0 +1,11 @@ +# Overview + +The Ratecard API allows you to automate the feedback collection and reputation management process. With its endpoints, you can send out surveys, collect responses, and analyze customer satisfaction metrics. Integrating Ratecard with Pipedream opens up possibilities to trigger workflows based on events, sync customer data with your CRM, or even escalate issues based on feedback scores, all in real time and without managing servers. + +# Example Use Cases + +- **Automated Feedback Request Post-Transaction**: After a purchase is marked as complete in an e-commerce platform like Shopify, you can trigger a Pipedream workflow to automatically send out a Ratecard survey to the customer, asking for feedback on their shopping experience. + +- **Customer Satisfaction Monitoring**: Connect Ratecard to a data visualization tool like Google Sheets using Pipedream. Whenever a new survey response comes in, append the data to a Google Sheet for real-time monitoring and analysis of customer satisfaction trends. + +- **Support Ticket Escalation**: Integrate Ratecard with a customer support tool like Zendesk. If a survey response indicates a dissatisfied customer, trigger a workflow that creates a high-priority support ticket in Zendesk to ensure immediate follow-up. diff --git a/components/ratecard/package.json b/components/ratecard/package.json index 94ee4c7a1f73d..7782d1cd5c710 100644 --- a/components/ratecard/package.json +++ b/components/ratecard/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/raven_tools/README.md b/components/raven_tools/README.md index 9f38db6cb7348..71d8218b7bf5c 100644 --- a/components/raven_tools/README.md +++ b/components/raven_tools/README.md @@ -1,28 +1,11 @@ # Overview -The Raven Tools API is an all-in-one platform that enables developers to create -powerful applications that improve their online presence and enterprise -marketing efforts. With Raven Tools, developers can create custom tools and -utilize Raven's powerful analytics and data solutions to optimize and enhance -their applications. +Raven Tools is a comprehensive suite of SEO tools that helps marketers to conduct audits, track search engine rankings, and analyze competitors' strategies. With the Raven Tools API, one can programmatically access this wealth of SEO data and generate reports, manage link building campaigns, and monitor keyword rankings. Leveraging the Pipedream platform, you can create automated workflows that react to various triggers and connect Raven Tools with other services to streamline SEO processes, enhance data analysis, and improve decision-making. -Raven Tools provides developers with a comprehensive set of tools that allow -them to access and manage data across numerous sources. With Raven’s powerful -reporting functions and data-driven insights, developers can create custom -dynamic applications and processes that deliver results. This makes Raven Tools -an essential tool for developing professional and efficient applications for -businesses, marketers, and entrepreneurs. +# Example Use Cases -Using Raven Tools, developers can build applications that: +- **SEO Monitoring and Alerting**: Create a workflow that periodically checks keyword rankings and website performance metrics via the Raven Tools API. If there's a significant change, like a drop in rankings or a spike in page load time, Pipedream can trigger an alert and send notifications via email, Slack, or SMS, ensuring you can react promptly to maintain your site's SEO health. -- Analyze and track website performance and rankings -- Monitor social media accounts and automatically generate reports -- Track affiliate programs and measure conversions and ROI -- Automate email campaigns and optimize content for search -- Create robust and interactive customer surveys -- Analyze website traffic and understand user behavior -- Track user engagement and improve website design and layout -- Automate search engine optimization and digital marketing strategies -- Integrate various data sources and build custom reporting functions -- Create powerful analytics dashboards and monitor marketing efforts in real - time +- **Content Performance Dashboard**: Build a workflow that fetches data from Raven Tools about organic traffic, backlinks, and keyword positions. Then, use Pipedream to send this data to Google Sheets or a BI tool like Tableau. This setup can serve as an automated content performance dashboard, giving you and your team real-time insights into which content is driving results and where improvements are needed. + +- **Competitor Analysis Automation**: Set up a Pipedream workflow that uses the Raven Tools API to track competitors' domain changes, backlink profiles, and keyword strategies on a scheduled basis. Integrate this with a CRM like Salesforce or a project management tool like Trello to automatically create tasks or opportunities based on insights gained from the competitor data, keeping your marketing strategies agile and informed. diff --git a/components/rawg_video_games_database/README.md b/components/rawg_video_games_database/README.md index 8bdea8e0b5389..4bd3093e00dd6 100644 --- a/components/rawg_video_games_database/README.md +++ b/components/rawg_video_games_database/README.md @@ -1,22 +1,11 @@ # Overview -The RAWG Video Games Database API is an award-winning service that provides -comprehensive access to gaming data. This open-source API allows developers to -access a broad range of gaming-related data such as game listings, developers, -and genres. An overview of what you can build using the RAWG Video Games -Database API includes: +The RAWG Video Games Database API is a treasure trove for gaming enthusiasts and developers, offering access to a massive collection of video game data. With it, you can pull information such as game titles, ratings, platforms, and release dates. This data can power up apps or websites with rich content or be used to analyze gaming trends. Through Pipedream, you can automate data collection, trigger events based on new or updated game releases, and integrate with other services for notifications or data enrichment. -- Create a game registry where users can add the video games they own or are - currently playing. -- Create a game search engine with added filters to help users find their - favorite games. -- Design a game recommendation engine that suggests games based on the user's - preferences. -- Design a game ratings system based on individual user ratings and comments. -- Develop a game encyclopedia page with snippets from game reviews from major - websites. -- Construct an interactive game calendar that tracks the release dates of - upcoming games. -- Develop a game report app that analyzes the user’s gaming habits and trends. -- Create an app that tracks and maps the popular video game franchises of all - time. +# Example Use Cases + +- **Game Release Tracker**: Set up a workflow that monitors the RAWG API for new game releases and automatically posts updates to a Discord server or Slack channel dedicated to gaming news. This keeps your community informed about the latest games. + +- **User Preferences Alert**: Create a system that sends personalized emails or SMS messages to users when games matching their preferences (like genre or platform) are added to the RAWG database. This can be achieved by integrating with SendGrid for email notifications or Twilio for SMS. + +- **Trend Analysis Dashboard**: Implement a workflow that aggregates data from RAWG to visualize gaming trends over time. Use this data to feed into a Google Sheets or a Tableau dashboard, allowing for in-depth analysis of game popularity, platform distribution, or genre success. diff --git a/components/razorpay/README.md b/components/razorpay/README.md index fc62ccbe17e07..7c09afac38c1e 100644 --- a/components/razorpay/README.md +++ b/components/razorpay/README.md @@ -1,17 +1,11 @@ # Overview -The Razorpay API provides a wealth of possibilities for developers to build -payment solutions for a vast range of use-cases. With the API, you can -implement payment functionalities for Mobile and Web applications, simplify -digital payments and allow customers to securely store digital wallets on your -platform. Here are some of the things you can do using Razorpay's API: +The Razorpay API provides a robust platform for handling payments and financial transactions, allowing you to integrate payment processing into your websites or apps. This versatile API enables automated payment capture, refunds, transfers, and the management of subscriptions. With Pipedream, you can weave Razorpay's capabilities into workflows that streamline payment operations, send real-time notifications, synchronize data across platforms, and more. -- Create an online store with a modern checkout process -- Enable customers to pay online with a QR code -- Integrate banking systems to process payments -- Allow customers to pay with their preferred payment mode -- Collect recurring payments securely -- Send and receive payments with ease -- Collect payments on behalf of other merchants -- Automate payment flows and subscription management -- Monitor all transactions on your platform +# Example Use Cases + +- **Automated Invoice Processing**: Trigger a workflow on Pipedream when a payment is successful in Razorpay. Generate an invoice with all transaction details and email it to the customer using a service like SendGrid. Update your CRM, like Salesforce, to reflect the payment. + +- **Real-time Payment Alerts**: Set up a Pipedream workflow that listens for Razorpay payment events. Once a payment is captured, send a notification to a Slack channel or via SMS using Twilio to keep your team instantly informed about sales and cash flow. + +- **Subscription Management Automation**: Use Pipedream to manage subscriptions. When a new subscription is created in Razorpay, automatically enroll the user in your platform and sync their membership details to a database like Airtable. Handle subscription renewals and cancellations by updating user permissions and access rights accordingly. diff --git a/components/rd_station_crm/README.md b/components/rd_station_crm/README.md index bc3b86575a8b6..1687414067afa 100644 --- a/components/rd_station_crm/README.md +++ b/components/rd_station_crm/README.md @@ -1,20 +1,11 @@ # Overview -The RD Station CRM API allows companies to access and manage their customer -relationships and use their data to better understand and engage their -customers. With the RD Station CRM API you can create, read, update and delete -customer information from within your own application. This can enable a range -of innovative solutions tailored to optimize customer relationships and -increase sales opportunities. +RD Station CRM API offers a suite of powerful tools for managing customer relationships and sales pipelines. With this API, you can automate the syncing of lead information, update records, and manage sales activities. By leveraging Pipedream's capabilities, you can create intricate workflows that react to events in RD Station CRM, synchronize data across multiple platforms, and trigger actions that streamline your sales and marketing processes. -Here are some examples of what you can do, using the RD Station CRM API: +# Example Use Cases -- Create an automated account creation process for new customers -- Retrieve up-to-date customer records -- Create and update contact and lead list filters -- Implement lead scoring rules to track engagement levels -- Automate customer segmentation -- Enable customer onboarding and personalization -- Create tailored customer journeys and opportunity tracking -- Automate customer communications and notifications -- Integrate with other popular CRM and marketing automation solutions +- **Lead Qualification and Distribution Workflow**: Monitor new leads in RD Station CRM and use Pipedream to qualify them based on custom criteria, such as lead score or engagement level. Once qualified, automatically assign leads to the appropriate sales reps in other tools like Salesforce or HubSpot, ensuring a timely and organized follow-up. + +- **Customer Success Outreach Automation**: Trigger a workflow when a deal reaches a certain stage in RD Station CRM. Connect with email platforms such as SendGrid to send personalized outreach or onboarding emails to the customer, fostering a relationship and encouraging retention. + +- **Event-Driven Follow-Up Tasks**: Create a Pipedream workflow that listens for specific events in RD Station CRM, like a deal closing or a new contact being added. Use this trigger to create follow-up tasks in project management tools such as Asana or Trello, keeping the team aligned and responsive to customer actions. diff --git a/components/re_amaze/README.md b/components/re_amaze/README.md index 2ce086c6c0dc1..d12cc2d59bfe7 100644 --- a/components/re_amaze/README.md +++ b/components/re_amaze/README.md @@ -1,24 +1,11 @@ # Overview -Re:amaze is an API that allows developers to build customer service solutions. -Its simple and intuitive interface makes it easy to build custom applications -related to customer service. With Re:amaze, you can create automated customer -support applications, live chat and ticketing services, customer feedback -surveys, and more. +Re:amaze is a customer service and engagement platform that combines email, social media, mobile, and live chat communication with customers into a single, integrated dashboard. Leveraging the Re:amaze API on Pipedream allows automating routine support tasks, syncing customer data across platforms, triggering event-based communications, and more. This API's functionality can streamline customer interactions, ensure timely responses, and enhance overall customer experience management. -Re:amaze offers developers a wide range of tools and features that enable -faster development and more comprehensive customer service solutions. Its -flexible SDK and extensive libraries allow for rapid deployment, custom -workflows, and integrations. +# Example Use Cases -Below are just a few examples of what you can build using the Re:amaze API: +- **Auto-responder for Off-Hours**: Use the Re:amaze API to detect when a new support message is received outside business hours. Automatically send a customized message to let the customer know their query has been received and will be addressed first thing in the morning. -- Automated customer support applications -- Live chat and ticketing services -- Customer feedback surveys -- Automatically filled forms -- Integration with third-party applications -- Reports and analytics -- Predefined customer service flows -- AI-powered chatbots -- Self-service customer support portals +- **Synchronize Support Tickets with a CRM**: Whenever a new ticket is created in Re:amaze, trigger a workflow that creates or updates the customer's record in a CRM like Salesforce. This ensures that the sales and support teams have up-to-date customer interactions data. + +- **Automated Feedback Collection Post-Resolution**: After a ticket is marked as resolved in Re:amaze, trigger an automated email or survey request for feedback on the support experience. Integrate with a platform like Typeform for collecting responses and analyze customer satisfaction over time. diff --git a/components/re_amaze/package.json b/components/re_amaze/package.json new file mode 100644 index 0000000000000..84edcb46d2374 --- /dev/null +++ b/components/re_amaze/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/re_amaze", + "version": "0.6.0", + "description": "Pipedream re_amaze Components", + "main": "re_amaze.app.mjs", + "keywords": [ + "pipedream", + "re_amaze" + ], + "homepage": "https://pipedream.com/apps/re_amaze", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/reachmail/README.md b/components/reachmail/README.md new file mode 100644 index 0000000000000..74d0e9ea3773a --- /dev/null +++ b/components/reachmail/README.md @@ -0,0 +1,11 @@ +# Overview + +The ReachMail API lets you seamlessly integrate email marketing capabilities into your workflows on Pipedream. With it, you can automate email list management, campaign creation, and performance tracking. This flexibility opens up possibilities for custom email marketing automation, audience segmentation, and targeted outreach based on user actions or data from other apps. By harnessing the power of Pipedream's serverless platform, you can build workflows that trigger from various events and connect with numerous other services to create a sophisticated, automated email marketing machine. + +# Example Use Cases + +- **Automated Campaign Creation from Webhooks**: Trigger an email campaign in ReachMail when a specific event occurs in your app. For instance, you could set up a Pipedream workflow where a new sign-up on your platform via a webhook triggers a welcome email sequence in ReachMail. + +- **Dynamic Audience Segmentation with Google Sheets**: Maintain a Google Sheet with contact information and preferences. Use a Pipedream workflow to periodically sync this data to ReachMail, automatically updating email lists for targeted campaigns based on the latest data. + +- **Performance-Driven Emails with ReachMail and Slack**: Monitor your email campaign performance with ReachMail, and use Pipedream to send summary reports or alerts to a Slack channel when key metrics hit certain thresholds, ensuring that your team stays informed about your marketing efforts in real time. diff --git a/components/reachmail/actions/opt-out-recipient-from-list/opt-out-recipient-from-list.mjs b/components/reachmail/actions/opt-out-recipient-from-list/opt-out-recipient-from-list.mjs new file mode 100644 index 0000000000000..a05e74119f40e --- /dev/null +++ b/components/reachmail/actions/opt-out-recipient-from-list/opt-out-recipient-from-list.mjs @@ -0,0 +1,36 @@ +import reachmail from "../../reachmail.app.mjs"; + +export default { + key: "reachmail-opt-out-recipient-from-list", + name: "Opt Out Recipient From List", + description: "The action will remove the recipient from the specified list. [See the documentation](https://services.reachmail.net/)", + version: "0.0.1", + type: "action", + props: { + reachmail, + recipient: { + type: "string", + label: "Recipient Email", + description: "The email address of the recipient.", + }, + listId: { + propDefinition: [ + reachmail, + "listId", + ], + type: "string", + description: "The ID of the list from which the recipient should be opted out.", + }, + }, + async run({ $ }) { + const response = await this.reachmail.optOutRecipient({ + $, + listId: this.listId, + data: { + Email: this.recipient, + }, + }); + $.export("$summary", `Successfully opted out ${this.recipient} from list ${this.listId}`); + return response; + }, +}; diff --git a/components/reachmail/package.json b/components/reachmail/package.json index 81223bb3475ff..d0a915b25ae31 100644 --- a/components/reachmail/package.json +++ b/components/reachmail/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/reachmail", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream ReachMail Components", "main": "reachmail.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" } -} \ No newline at end of file +} + diff --git a/components/reachmail/reachmail.app.mjs b/components/reachmail/reachmail.app.mjs index 70bdff55d96cc..cd15284b1da9a 100644 --- a/components/reachmail/reachmail.app.mjs +++ b/components/reachmail/reachmail.app.mjs @@ -1,11 +1,80 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "reachmail", - propDefinitions: {}, + propDefinitions: { + listId: { + type: "string[]", + label: "List ID", + description: "The list ID related to the new campaign.", + async options() { + const data = await this.listLists(); + + return data.map(({ + Id: value, Name: label, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://services.reachmail.net"; + }, + _headers() { + return { + "Authorization": `Bearer ${this.$auth.api_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createWebhook({ + action, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/Webhooks/${action}`, + ...opts, + }); + }, + deleteWebhook(action) { + return this._makeRequest({ + method: "DELETE", + path: `/Webhooks/${action}`, + }); + }, + updateWebhook({ + hookId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/Webhooks/${hookId}`, + ...opts, + }); + }, + listLists() { + return this._makeRequest({ + path: "/Lists", + }); + }, + optOutRecipient({ + listId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/Lists/OptOut/${listId}`, + ...opts, + }); }, }, }; diff --git a/components/reachmail/sources/common/base.mjs b/components/reachmail/sources/common/base.mjs new file mode 100644 index 0000000000000..482905f961d61 --- /dev/null +++ b/components/reachmail/sources/common/base.mjs @@ -0,0 +1,49 @@ +import reachmail from "../../reachmail.app.mjs"; + +export default { + props: { + reachmail, + http: "$.interface.http", + db: "$.service.db", + }, + methods: { + _setHookId(Id) { + this.db.set("hookId", Id); + }, + _getHookId() { + return this.db.get("hookId"); + }, + emitData(body) { + this.$emit(body, { + id: body.Signature, + summary: this.getSummary(body), + ts: Date.parse(body.Timestamp), + }); + }, + }, + hooks: { + async activate() { + const response = await this.reachmail.createWebhook({ + action: this.getAction(), + data: { + Url: this.http.endpoint, + }, + }); + this._setHookId(response.Id); + }, + async deactivate() { + const hookId = this._getHookId(); + + await this.reachmail.updateWebhook({ + hookId, + data: { + Url: this.http.endpoint, + Active: false, + }, + }); + }, + }, + async run({ body }) { + this.emitData(body); + }, +}; diff --git a/components/reachmail/sources/new-bounce-instant/new-bounce-instant.mjs b/components/reachmail/sources/new-bounce-instant/new-bounce-instant.mjs new file mode 100644 index 0000000000000..9141ba377342f --- /dev/null +++ b/components/reachmail/sources/new-bounce-instant/new-bounce-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "reachmail-new-bounce-instant", + name: "New Bounce Instant", + description: "Emit new event when a recipient's email address bounces.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getAction() { + return "Bounce"; + }, + getSummary(body) { + return `New bounce: ${body.Email}`; + }, + }, + sampleEmit, +}; diff --git a/components/reachmail/sources/new-bounce-instant/test-event.mjs b/components/reachmail/sources/new-bounce-instant/test-event.mjs new file mode 100644 index 0000000000000..d1f217b55c9bc --- /dev/null +++ b/components/reachmail/sources/new-bounce-instant/test-event.mjs @@ -0,0 +1,39 @@ +export default { + "body": { + "Device": { + "ClientName": null, + "ClientOs": "Unknown", + "ClientType": "Other", + "DeviceType": "Desktop" + }, + "Email": "webhook@reachmail.net", + "EventId": "9d2360cd-e094-4586-9125-ffcc9c713f58", + "EventType": "bounce", + "IpAddress": "123.11.32.1", + "Location": { + "City": "Chicago", + "CountryCode": "US", + "Region": "IL" + }, + "RecipientId": "20b64129-c0a1-42bc-9453-209a28f7bd51", + "Signature": "816fb208ea0cb491f81b171d6d5d91d0de850b37156e1322fabccd2e2ed5cd64", + "Timestamp": 1710169299, + "Token": "xeeggtwycyqgcpemxduirulpdpmjbksejmkocvkjffwjefpnph", + "Url": null, + "UserAgent": "Mozilla/firefox", + "XCampaign": "Campaign Name" + }, + "client_ip": "123.11.32.1", + "headers": { + "accept": "text/html, application/xhtml+xml, application/xml; q=0.9, image/webp, */*; q=0.8", + "accept-encoding": "gzip, deflate, sdch, br", + "content-length": "569", + "content-type": "application/json; charset=utf-8", + "host": "urltest.com", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15" + }, + "method": "POST", + "path": "/", + "query": {}, + "url": "https://urltest.com/" +} \ No newline at end of file diff --git a/components/reachmail/sources/new-click-instant/new-click-instant.mjs b/components/reachmail/sources/new-click-instant/new-click-instant.mjs new file mode 100644 index 0000000000000..16bb0492690c1 --- /dev/null +++ b/components/reachmail/sources/new-click-instant/new-click-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "reachmail-new-click-instant", + name: "New Click Instant", + description: "Emit new event when a recipient clicks on an email.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getAction() { + return "Click"; + }, + getSummary(body) { + return `New click from ${body.Email}`; + }, + }, + sampleEmit, +}; diff --git a/components/reachmail/sources/new-click-instant/test-event.mjs b/components/reachmail/sources/new-click-instant/test-event.mjs new file mode 100644 index 0000000000000..d282ad3285170 --- /dev/null +++ b/components/reachmail/sources/new-click-instant/test-event.mjs @@ -0,0 +1,39 @@ +export default { + "body": { + "Device": { + "ClientName": null, + "ClientOs": "Unknown", + "ClientType": "Other", + "DeviceType": "Desktop" + }, + "Email": "webhook@reachmail.net", + "EventId": "9d2360cd-e094-4586-9125-ffcc9c713f58", + "EventType": "click", + "IpAddress": "123.11.32.1", + "Location": { + "City": "Chicago", + "CountryCode": "US", + "Region": "IL" + }, + "RecipientId": "20b64129-c0a1-42bc-9453-209a28f7bd51", + "Signature": "816fb208ea0cb491f81b171d6d5d91d0de850b37156e1322fabccd2e2ed5cd64", + "Timestamp": 1710169299, + "Token": "xeeggtwycyqgcpemxduirulpdpmjbksejmkocvkjffwjefpnph", + "Url": null, + "UserAgent": "Mozilla/firefox", + "XCampaign": "Campaign Name" + }, + "client_ip": "123.11.32.1", + "headers": { + "accept": "text/html, application/xhtml+xml, application/xml; q=0.9, image/webp, */*; q=0.8", + "accept-encoding": "gzip, deflate, sdch, br", + "content-length": "569", + "content-type": "application/json; charset=utf-8", + "host": "urltest.com", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15" + }, + "method": "POST", + "path": "/", + "query": {}, + "url": "https://urltest.com/" +} \ No newline at end of file diff --git a/components/reachmail/sources/new-open-instant/new-open-instant.mjs b/components/reachmail/sources/new-open-instant/new-open-instant.mjs new file mode 100644 index 0000000000000..c497c1adf61af --- /dev/null +++ b/components/reachmail/sources/new-open-instant/new-open-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "reachmail-new-open-instant", + name: "New Email Open Instant", + description: "Emit new event when a recipient opens an email.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getAction() { + return "Open"; + }, + getSummary(body) { + return `Email opened by ${body.Email}`; + }, + }, + sampleEmit, +}; diff --git a/components/reachmail/sources/new-open-instant/test-event.mjs b/components/reachmail/sources/new-open-instant/test-event.mjs new file mode 100644 index 0000000000000..ed76387486338 --- /dev/null +++ b/components/reachmail/sources/new-open-instant/test-event.mjs @@ -0,0 +1,39 @@ +export default { + "body": { + "Device": { + "ClientName": null, + "ClientOs": "Unknown", + "ClientType": "Other", + "DeviceType": "Desktop" + }, + "Email": "webhook@reachmail.net", + "EventId": "9d2360cd-e094-4586-9125-ffcc9c713f58", + "EventType": "open", + "IpAddress": "123.11.32.1", + "Location": { + "City": "Chicago", + "CountryCode": "US", + "Region": "IL" + }, + "RecipientId": "20b64129-c0a1-42bc-9453-209a28f7bd51", + "Signature": "816fb208ea0cb491f81b171d6d5d91d0de850b37156e1322fabccd2e2ed5cd64", + "Timestamp": 1710169299, + "Token": "xeeggtwycyqgcpemxduirulpdpmjbksejmkocvkjffwjefpnph", + "Url": null, + "UserAgent": "Mozilla/firefox", + "XCampaign": "Campaign Name" + }, + "client_ip": "123.11.32.1", + "headers": { + "accept": "text/html, application/xhtml+xml, application/xml; q=0.9, image/webp, */*; q=0.8", + "accept-encoding": "gzip, deflate, sdch, br", + "content-length": "569", + "content-type": "application/json; charset=utf-8", + "host": "urltest.com", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15" + }, + "method": "POST", + "path": "/", + "query": {}, + "url": "https://urltest.com/" +} \ No newline at end of file diff --git a/components/readwise/README.md b/components/readwise/README.md index 315d66138995f..602102df342bb 100644 --- a/components/readwise/README.md +++ b/components/readwise/README.md @@ -1,17 +1,11 @@ # Overview -The Readwise API is an amazing resource for building powerful applications that -use Readwise's unique data and insights. Using the Readwise API, you can build -applications that: +The Readwise API allows you to access and manipulate your Readwise data, which includes highlights, notes, and books from your reading list. With this API, you can automate the retrieval of your reading highlights, synchronize them across various platforms, or trigger custom actions based on new highlights added. Pipedream, as a serverless integration and compute platform, enables you to create workflows that leverage the Readwise API to build powerful automations, connecting your reading insights to countless other apps and services to enrich productivity and data management. -- Retrieve annotations and highlights from any book or article you’ve read - using Readwise. -- Analyze and visualize insights from your annotations and highlights. -- Perfoerm sentiment analysis on the data to identify topics. -- Create interactive books or articles to help you better understand what - you’ve read. -- Permalink any highlight or annotation you’ve saved through Readwise. -- Create reading lists and summaries. -- Send custom alerts and notifications when certain topics or keywords appear - in your highlights and annotations. -- Create summary and progress reports for books or articles you’re reading. +# Example Use Cases + +- **Sync Highlights to Notion Database**: Automatically add new Readwise highlights to a Notion database. Upon detecting a new highlight via the Readwise API, Pipedream can trigger a workflow that creates or updates a corresponding page in Notion, allowing for seamless management and review of reading notes. + +- **Create Weekly Reading Digest Email**: Compile a weekly digest of your Readwise highlights and send it via email. Pipedream schedules a weekly workflow that fetches the latest highlights and uses an email service, such as SendGrid, to craft and send a personalized reading digest. + +- **Trigger Slack Notifications for New Highlights**: Receive immediate notifications in Slack whenever a new highlight is added to your Readwise account. Pipedream listens for new Readwise highlights and sends a formatted message to a specified Slack channel, keeping you and your team informed about new insights. diff --git a/components/real_id/actions/create-id-check/create-id-check.mjs b/components/real_id/actions/create-id-check/create-id-check.mjs new file mode 100644 index 0000000000000..2f272c8eb78fb --- /dev/null +++ b/components/real_id/actions/create-id-check/create-id-check.mjs @@ -0,0 +1,123 @@ +import app from "../../real_id.app.mjs"; + +export default { + key: "real_id-create-id-check", + name: "Create ID Check", + description: "Create a new ID check for a user. [See the documentation](https://getverdict.com/help/docs/api/checks#create-an-id-check).", + version: "0.0.1", + type: "action", + props: { + app, + // eslint-disable-next-line pipedream/props-label, pipedream/props-description + info: { + type: "alert", + alertType: "info", + content: "Either the **Email** or the **Phone** number is required to create an ID check.", + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + phone: { + propDefinition: [ + app, + "phone", + ], + }, + firstName: { + type: "string", + label: "First Name", + description: "First name of the customer", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the customer", + optional: true, + }, + wooCommerceCustomerId: { + type: "string", + label: "WooCommerce Customer ID", + description: "Unique identifier for the customer in WooCommerce", + optional: true, + }, + wooCommerceOrderId: { + type: "string", + label: "WooCommerce Order ID", + description: "Unique identifier for the order in WooCommerce", + optional: true, + }, + shopifyAdminGraphqlId: { + type: "string", + label: "Shopify Admin GraphQL ID", + description: "Unique identifier for the customer in Shopify", + optional: true, + }, + shopifyAdminGraphqlOrderId: { + type: "string", + label: "Shopify Admin GraphQL Order ID", + description: "Unique identifier for the order in Shopify", + optional: true, + }, + shopifyAdminGraphqlOrderName: { + type: "string", + label: "Shopify Admin GraphQL Order Name", + description: "Name of the order in Shopify", + optional: true, + }, + additionalParameters: { + type: "object", + label: "Additional Parameters", + description: "Additional parameters to include in the request", + optional: true, + }, + }, + methods: { + createIdCheck(args = {}) { + return this.app.post({ + path: "/checks", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createIdCheck, + email, + phone, + firstName, + lastName, + wooCommerceCustomerId, + wooCommerceOrderId, + shopifyAdminGraphqlId, + shopifyAdminGraphqlOrderId, + shopifyAdminGraphqlOrderName, + additionalParameters, + } = this; + + const response = await createIdCheck({ + $, + data: { + customer: { + first_name: firstName, + last_name: lastName, + email, + phone, + wc_id: wooCommerceCustomerId, + shopify_admin_graphql_id: shopifyAdminGraphqlId, + }, + order: { + wc_id: wooCommerceOrderId, + shopify_admin_graphql_id: shopifyAdminGraphqlOrderId, + name: shopifyAdminGraphqlOrderName, + }, + ...additionalParameters, + }, + }); + $.export("$summary", "Successfully created ID check."); + return response; + }, +}; diff --git a/components/real_id/actions/delete-id-check/delete-id-check.mjs b/components/real_id/actions/delete-id-check/delete-id-check.mjs new file mode 100644 index 0000000000000..20ebe86e82426 --- /dev/null +++ b/components/real_id/actions/delete-id-check/delete-id-check.mjs @@ -0,0 +1,41 @@ +import app from "../../real_id.app.mjs"; + +export default { + key: "real_id-delete-id-check", + name: "Delete ID Check", + description: "Permanently delete all data associated with a specific ID check. [See the documentation](https://getverdict.com/help/docs/api/checks#delete-id-check-data).", + version: "0.0.1", + type: "action", + props: { + app, + checkId: { + propDefinition: [ + app, + "checkId", + ], + }, + }, + methods: { + deleteIdCheck({ + checkId, ...args + } = {}) { + return this.app.delete({ + path: `/checks/${checkId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + deleteIdCheck, + checkId, + } = this; + + const response = await deleteIdCheck({ + $, + checkId, + }); + $.export("$summary", `Successfully deleted ID check \`${response.check.id}\`.`); + return response; + }, +}; diff --git a/components/real_id/actions/get-id-check/get-id-check.mjs b/components/real_id/actions/get-id-check/get-id-check.mjs new file mode 100644 index 0000000000000..ab863ae7bcba3 --- /dev/null +++ b/components/real_id/actions/get-id-check/get-id-check.mjs @@ -0,0 +1,52 @@ +import app from "../../real_id.app.mjs"; + +export default { + key: "real_id-get-id-check", + name: "Get ID Check", + description: "Retrieve an ID check. [See the documentation](https://getverdict.com/help/docs/api/checks#retrieve-an-id-check).", + version: "0.0.1", + type: "action", + props: { + app, + checkId: { + propDefinition: [ + app, + "checkId", + ], + }, + withPhotos: { + type: "boolean", + label: "Include Photos", + description: "All photos will be provided as short lived URLs in the API response under the `photos` key.", + optional: true, + }, + }, + methods: { + retrieveIdCheck({ + checkId, ...args + } = {}) { + return this.app._makeRequest({ + path: `/checks/${checkId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + retrieveIdCheck, + checkId, + withPhotos, + } = this; + + const response = await retrieveIdCheck({ + $, + checkId, + params: { + withPhotos, + }, + }); + + $.export("$summary", "Successfully retrieved ID check."); + return response; + }, +}; diff --git a/components/real_id/package.json b/components/real_id/package.json new file mode 100644 index 0000000000000..9d65f5d460b56 --- /dev/null +++ b/components/real_id/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/real_id", + "version": "0.1.0", + "description": "Pipedream Real ID Components", + "main": "real_id.app.mjs", + "keywords": [ + "pipedream", + "real_id" + ], + "homepage": "https://pipedream.com/apps/real_id", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/real_id/real_id.app.mjs b/components/real_id/real_id.app.mjs new file mode 100644 index 0000000000000..ac1f236fbd1f9 --- /dev/null +++ b/components/real_id/real_id.app.mjs @@ -0,0 +1,58 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "real_id", + propDefinitions: { + email: { + type: "string", + label: "Email", + description: "Email address for initiating the ID check", + optional: true, + }, + phone: { + type: "string", + label: "Phone Number", + description: "Phone number for initiating the ID check", + optional: true, + }, + checkId: { + type: "string", + label: "ID Check Identifier", + description: "The unique identifier for the ID check", + }, + }, + methods: { + getUrl(path) { + return `https://real-id.getverdict.com/api/v1${path}`; + }, + getHeaders(headers) { + return { + "Authorization": `Bearer ${this.$auth.api_key}`, + "Content-Type": "application/json", + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + }, +}; diff --git a/components/realgeeks/README.md b/components/realgeeks/README.md new file mode 100644 index 0000000000000..943459d2b5c77 --- /dev/null +++ b/components/realgeeks/README.md @@ -0,0 +1,11 @@ +# Overview + +The RealGeeks API offers a gateway to RealGeeks' real estate platform, allowing you to manage leads, properties, and agents programmatically. With the RealGeeks API on Pipedream, you can automate interactions with RealGeeks, such as syncing leads with other platforms, triggering actions based on property updates, and managing CRM activities. Pipedream's serverless platform enables you to create these workflows with minimal setup, using pre-built actions or custom Node.js code steps. + +# Example Use Cases + +- **Syncing Leads to a CRM**: Automatically transfer new leads from RealGeeks to a CRM like Salesforce. When a lead is created in RealGeeks, a workflow on Pipedream can capture that event and push the lead data into Salesforce, ensuring your sales team has immediate access to new opportunities. + +- **Real-Time Notifications**: Get notified instantly when there's a new lead or a property status changes. Pipedream can listen for RealGeeks events and send alerts through email, SMS via Twilio, or a Slack message, keeping you or your team informed in real-time. + +- **Email Campaign Trigger**: Initiate an email campaign with an email marketing service like Mailchimp when a new property is listed. With Pipedream, you can watch for new property listings in RealGeeks and automatically add that information to a Mailchimp audience, triggering a targeted email campaign. diff --git a/components/realphonevalidation/README.md b/components/realphonevalidation/README.md index 2e0531c9ef2f4..c04380abf21aa 100644 --- a/components/realphonevalidation/README.md +++ b/components/realphonevalidation/README.md @@ -1,29 +1,11 @@ # Overview -RealPhoneValidation is an API based tool that allows users to check the -accuracy of phone numbers located around the world. The RealPhoneValidation API -offers a variety of services and features that can help you keep your data up -to date and accurate. +With the RealPhoneValidation API, users can verify and validate phone numbers to ensure their accuracy and activity. This is crucial for businesses that rely on phone communication to connect with customers, as it helps to reduce wasted resources on invalid numbers and improves the efficiency of outreach campaigns. By integrating this API into Pipedream, you can automate phone number validation within your workflows and seamlessly combine this process with other actions and data from a wide variety of apps to enhance lead validation, customer onboarding, and user authentication processes. -RealPhoneValidation offers a number of features and services that can help you -maintain the accuracy of your data, and make sure that information is up to -date and correct. With RealPhoneValidation's advanced technology, you can -quickly determine the accuracy of phone number data from multiple sources. +# Example Use Cases -RealPhoneValidation also offers a range of reporting and analytics tools to -help you gain insight into customer data. This can help you develop better -strategies for reaching out to customers in the most efficient manner. +- **Lead Verification Workflow**: Create a Pipedream workflow that triggers every time a new lead is captured in your CRM (like Salesforce). The workflow would use the RealPhoneValidation API to verify the phone number provided, updating the lead score or status based on the validation result. This ensures sales teams focus on leads with valid contact information. -Here are some examples of what you can build using the RealPhoneValidation API: +- **User Signup Validation**: For platforms requiring phone verification upon user signup, a Pipedream workflow can be triggered upon the creation of a new user account. The workflow would use RealPhoneValidation to confirm the phone number's legitimacy and, if validated, send a welcome message or notification through Twilio, or alternatively, flag the account for follow-up if the number is invalid. -- Email List Verification: Validate customer email addresses in real time and - identify the accuracy of addresses. -- Phone Number Lookup: Quickly determine the accuracy of any phone number in - any country. -- Spam Detection: Identify potential spam messages and associated phone numbers -- Geolocation Services: Generate accurate geolocation information for any given - address or phone number. -- Data Hygiene Solutions: Automatically and accurately clean customer data to - make sure it is up to date. -- Fraud Detection: Track and monitor mobile device and IP addresses to - recognize suspicious activity. +- **E-commerce Order Confirmation**: Automate order processing in an e-commerce store by setting up a Pipedream workflow that triggers after an order is placed. Before confirming the order, use RealPhoneValidation to ensure the phone number associated with the order is active. If active, proceed with order confirmation and fulfillment; if not, send an alert or email via SendGrid to request updated contact information from the customer. diff --git a/components/reapit_foundations/README.md b/components/reapit_foundations/README.md new file mode 100644 index 0000000000000..45bdd4483ee5e --- /dev/null +++ b/components/reapit_foundations/README.md @@ -0,0 +1,11 @@ +# Overview + +The Reapit Foundations API provides a suite of tools for real estate businesses, enabling them to manage properties, customers, offers, and appointments among other things. With its comprehensive data endpoints, you can streamline various real estate operations by integrating with Pipedream. Pipedream serves as a powerful workflow automation platform where you can connect the Reapit Foundations API to hundreds of other services to automate tasks, sync data, and create dynamic business processes without the need for dedicated backend infrastructure. + +# Example Use Cases + +- **Automate Lead Follow-up**: Trigger a workflow in Pipedream when new leads are added to Reapit. Send personalized follow-up emails using an email service like SendGrid or log the lead in a CRM like Salesforce for further action. + +- **Property Listing Sync**: Whenever a new property is listed in Reapit, automatically publish the listing to social media platforms like Twitter or Facebook to increase visibility and engage potential clients. + +- **Appointment Reminder System**: Set up a reminder system that sends SMS messages through Twilio to both agents and clients about their upcoming appointments, reducing the no-show rate and ensuring better time management. diff --git a/components/reapit_foundations/package.json b/components/reapit_foundations/package.json index e4936f9e0e010..a1c788d4db26a 100644 --- a/components/reapit_foundations/package.json +++ b/components/reapit_foundations/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/rebrandly/README.md b/components/rebrandly/README.md index 66310f334a809..a6b9ba46dc79d 100644 --- a/components/rebrandly/README.md +++ b/components/rebrandly/README.md @@ -1,18 +1,11 @@ # Overview -Using the Rebrandly API you can easily build a variety of useful link -management solutions. The API enables developers to quickly and easily create, -manage, and track branded short URLs through their platform. Here are some -examples of what you can build: +The Rebrandly API enables the creation, updating, and management of branded short links. Within Pipedream, you can leverage this API to automate custom URL shortening workflows, analyze link performance, and seamlessly integrate URL management into your business processes or applications. -- Automatically shorten and brand link URLs when they are posted to social - media. -- Create new short links in bulk from an existing list of URLs. -- Generate and detect ‘vanity’ links from custom words and phrases. -- Link attribution and tracking across various platforms and networks. -- Automatically redirect users to specific pages based on a combination of - location, language, and device. -- Create branded links for YouTube videos, introducing a level of - sophistication to your YouTube marketing campaigns. -- Create and leverage QRCodes for mobile content discovery. -- Generate dynamic link UTM parameters based on a wide range of criteria. +# Example Use Cases + +- **Content Publication Alert Shortener**: Automate short link generation for new blog posts or articles by triggering a Pipedream workflow whenever a new piece of content is published. Connect Rebrandly with a CMS like WordPress to create memorable, branded short links for each post, which can then be shared across social media platforms via a Pipedream integration with Twitter, LinkedIn, or Facebook. + +- **E-Commerce Promotion Tracker**: Generate unique, trackable short links for product promotions using Rebrandly within a Pipedream workflow. Link this to an e-commerce platform such as Shopify to disseminate promotion-specific URLs through marketing emails or SMS campaigns, while tracking click-through rates to gauge the effectiveness of your campaigns. + +- **Event Registration Coordinator**: Simplify event sign-up processes by integrating Rebrandly with event management tools like Eventbrite in a Pipedream workflow. Create branded short links for event registrations and automatically distribute them to potential attendees through email marketing tools like Mailchimp, streamlining the registration process and improving the user experience. diff --git a/components/recharge/README.md b/components/recharge/README.md index 46ebcf4a7865b..d2759881729bb 100644 --- a/components/recharge/README.md +++ b/components/recharge/README.md @@ -1,19 +1,11 @@ # Overview -The ReCharge API is a powerful tool for integrating subscription billing -services into existing applications and building out new ones. By connecting -your application directly to ReCharge, you can leverage the functionality of -ReCharge to create a powerful back-end solution for managing the subscription -payment lifecycle. With ReCharge, merchants can design and automate complex -billing workflows to eliminate manual processes and deliver superior customer -experiences. +The ReCharge API offers a programmatic entrance to the ReCharge ecosystem, a place where subscription-based e-commerce is managed with ease. Whether you're looking to synchronize customer data, manage subscriptions, or automate notifications, the API's endpoints enable you to seamlessly integrate subscription data into your business processes. With Pipedream's serverless execution model, you can craft workflows that respond to events in ReCharge, manipulate and route data, and trigger actions across your entire software stack. This opens doors to personalized customer experiences, streamlined operations, and data-driven decision-making. -Below are some examples of what you can do with the ReCharge API: +# Example Use Cases -- Manage customers and their subscription information -- Support recurring and one-time payments -- Generate invoices and accept payments -- Automate the subscription management process -- Create and manage promotions -- Leverage the API for analytics and reporting -- Integrate with payment and fraud protection solutions +- **Sync ReCharge Customers with a CRM**: Keep your Customer Relationship Management (CRM) platform like Salesforce or HubSpot up-to-date by syncing new and updated customer data from ReCharge. Automatically update contact details, track subscription statuses, and personalize marketing efforts based on customer subscription data. + +- **Automate Subscription Lifecycle Emails**: Connect ReCharge to an email platform like SendGrid or Mailchimp. Create workflows that send welcome emails to new subscribers, renewal reminders before a subscription renews, and win-back messages to customers whose subscriptions have lapsed. + +- **Slack Alerts for High-Value Actions**: Set up a workflow to monitor ReCharge events for high-value customer actions, such as subscribing to a premium plan or making a bulk purchase. Send real-time alerts to a Slack channel to enable your team to take immediate action, like reaching out to thank the customer or offering personalized support. diff --git a/components/recharge/actions/cancel-subscription/cancel-subscription.mjs b/components/recharge/actions/cancel-subscription/cancel-subscription.mjs new file mode 100644 index 0000000000000..91135a10cc744 --- /dev/null +++ b/components/recharge/actions/cancel-subscription/cancel-subscription.mjs @@ -0,0 +1,48 @@ +import recharge from "../../recharge.app.mjs"; + +export default { + key: "recharge-cancel-subscription", + name: "Cancel Subscription", + description: "Cancels an existing subscription. [See the documentation](https://developer.rechargepayments.com/2021-11/subscriptions/subscriptions_cancel)", + version: "0.0.1", + type: "action", + props: { + recharge, + subscriptionId: { + propDefinition: [ + recharge, + "subscriptionId", + ], + }, + cancellationReason: { + type: "string", + label: "Cancellation Reason", + description: "Reason for subscription cancellation.", + }, + cancellationReasonComment: { + type: "string", + label: "Cancellation Reason Comment", + description: "Cancellation reason comment. Maximum length is 1024 characters.", + optional: true, + }, + sendEmail: { + type: "boolean", + label: "Send Email", + description: "If set to `false`, subscription cancelled email will not be sent to customer and store owner.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.recharge.cancelSubscription({ + $, + subscriptionId: this.subscriptionId, + data: { + cancellation_reason: this.cancellationReason, + cancellation_reason_comments: this.cancellationReasonComment, + send_email: this.sendEmail, + }, + }); + $.export("$summary", `Successfully cancelled subscription ${this.subscriptionId}`); + return response; + }, +}; diff --git a/components/recharge/actions/common/common-customer.mjs b/components/recharge/actions/common/common-customer.mjs new file mode 100644 index 0000000000000..3442b4a51f539 --- /dev/null +++ b/components/recharge/actions/common/common-customer.mjs @@ -0,0 +1,62 @@ +import recharge from "../../recharge.app.mjs"; + +export function getCustomerProps(optional = false) { + return { + email: { + propDefinition: [ + recharge, + "email", + ], + optional, + }, + firstName: { + propDefinition: [ + recharge, + "firstName", + ], + optional, + }, + lastName: { + propDefinition: [ + recharge, + "lastName", + ], + optional, + }, + phone: { + propDefinition: [ + recharge, + "phone", + ], + optional: true, + }, + externalCustomerId: { + propDefinition: [ + recharge, + "externalCustomerId", + ], + optional: true, + }, + taxExempt: { + propDefinition: [ + recharge, + "taxExempt", + ], + optional: true, + }, + }; +} + +export function getCustomerData() { + return { + email: this.email, + first_name: this.firstName, + last_name: this.lastName, + phone: this.phone, + external_customer_id: this.externalCustomerId && { + ecommerce: this.externalCustomerId, + }, + apply_credit_to_next_recurring_charge: this.applyCreditToNextRecurringCharge, + tax_exempt: this.taxExempt, + }; +} diff --git a/components/recharge/actions/create-customer/create-customer.mjs b/components/recharge/actions/create-customer/create-customer.mjs new file mode 100644 index 0000000000000..53545150b999b --- /dev/null +++ b/components/recharge/actions/create-customer/create-customer.mjs @@ -0,0 +1,29 @@ +import recharge from "../../recharge.app.mjs"; +import { + getCustomerData, getCustomerProps, +} from "../common/common-customer.mjs"; + +export default { + key: "recharge-create-customer", + name: "Create Customer", + description: "Creates a customer. [See the documentation](https://developer.rechargepayments.com/2021-11/customers/customers_create)", + version: "0.0.1", + type: "action", + props: { + recharge, + ...getCustomerProps(), + }, + methods: { + getCustomerData, + }, + async run({ $ }) { + const data = this.getCustomerData(); + const response = await this.recharge.createCustomer({ + $, + data, + }); + + $.export("$summary", `Successfully created customer (ID: ${response?.customer?.id})`); + return response; + }, +}; diff --git a/components/recharge/actions/create-subscription/create-subscription.mjs b/components/recharge/actions/create-subscription/create-subscription.mjs new file mode 100644 index 0000000000000..ee185e33e12ab --- /dev/null +++ b/components/recharge/actions/create-subscription/create-subscription.mjs @@ -0,0 +1,93 @@ +import recharge from "../../recharge.app.mjs"; + +export default { + key: "recharge-create-subscription", + name: "Create Subscription", + description: "Creates a new subscription allowing a customer to subscribe to a product. [See the documentation](https://developer.rechargepayments.com/2021-11/subscriptions/subscriptions_create)", + version: "0.0.1", + type: "action", + props: { + recharge, + addressId: { + propDefinition: [ + recharge, + "addressId", + ], + }, + chargeIntervalFrequency: { + type: "integer", + label: "Charge Interval Frequency", + description: "The number of units, specified in `Order Interval Unit`, between each charge.", + }, + nextChargeScheduledAt: { + type: "string", + label: "Next Charge Scheduled At", + description: "This will set the first charge date of a new subscription. Can be a date string such as `2021-12-17`.", + }, + orderIntervalFrequency: { + type: "integer", + label: "Order Interval Frequency", + description: "The number of units, specified in `Order Interval Unit`, between each order.", + }, + orderIntervalUnit: { + type: "string", + label: "Order Interval Unit", + description: "The frequency unit used to determine when a subscription order is created.", + options: [ + "day", + "week", + "month", + ], + }, + quantity: { + type: "integer", + label: "Quantity", + description: "The quantity of the product.", + }, + externalVariantId: { + propDefinition: [ + recharge, + "externalVariantId", + ], + }, + additionalOptions: { + type: "object", + label: "Additional Options", + description: "Additional parameters to be passed in the request. [See the documentation](https://developer.rechargepayments.com/2021-11/subscriptions/subscriptions_create) for all available parameters.", + }, + }, + async run({ $ }) { + const response = await this.recharge.createSubscription({ + $, + data: { + address_id: this.addressId, + charge_interval_frequency: this.chargeIntervalFrequency, + next_charge_scheduled_at: this.nextChargeScheduledAt, + order_interval_frequency: this.orderIntervalFrequency, + order_interval_unit: this.orderIntervalUnit, + quantity: this.quantity, + external_variant_id: this.externalVariantId && { + ecommerce: this.externalVariantId, + }, + ...Object.fromEntries(Object.entries(this.additionalOptions).map(([ + key, + value, + ]) => { + try { + value = JSON.parse(value); + } catch (e) { + e; // JSON parsing is optional + } + + return [ + key, + value, + ]; + })), + }, + }); + + $.export("$summary", `Successfully created a new subscription with ID ${response?.subscription?.id}`); + return response; + }, +}; diff --git a/components/recharge/actions/update-customer/update-customer.mjs b/components/recharge/actions/update-customer/update-customer.mjs new file mode 100644 index 0000000000000..3788ea8525a18 --- /dev/null +++ b/components/recharge/actions/update-customer/update-customer.mjs @@ -0,0 +1,42 @@ +import recharge from "../../recharge.app.mjs"; +import { + getCustomerData, getCustomerProps, +} from "../common/common-customer.mjs"; + +export default { + key: "recharge-update-customer", + name: "Update Customer", + description: "Updates an existing customer's details. [See the documentation](https://developer.rechargepayments.com/2021-11/customers/customers_update)", + version: "0.0.1", + type: "action", + props: { + recharge, + customerId: { + propDefinition: [ + recharge, + "customerId", + ], + }, + applyCreditToNextRecurringCharge: { + propDefinition: [ + recharge, + "applyCreditToNextRecurringCharge", + ], + }, + ...getCustomerProps(true), + }, + methods: { + getCustomerData, + }, + async run({ $ }) { + const data = this.getCustomerData(); + const response = await this.recharge.updateCustomer({ + $, + customerId: this.customerId, + data, + }); + + $.export("$summary", `Successfully updated customer ${this.customerId}`); + return response; + }, +}; diff --git a/components/recharge/package.json b/components/recharge/package.json new file mode 100644 index 0000000000000..7ac225897beee --- /dev/null +++ b/components/recharge/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/recharge", + "version": "0.1.0", + "description": "Pipedream Recharge Components", + "main": "recharge.app.mjs", + "keywords": [ + "pipedream", + "recharge" + ], + "homepage": "https://pipedream.com/apps/recharge", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "1.6.0" + } +} diff --git a/components/recharge/recharge.app.mjs b/components/recharge/recharge.app.mjs index 2407e0e451bba..2e003660416b9 100644 --- a/components/recharge/recharge.app.mjs +++ b/components/recharge/recharge.app.mjs @@ -1,11 +1,217 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "recharge", - propDefinitions: {}, + propDefinitions: { + customerId: { + type: "string", + label: "Customer ID", + description: "Select a customer or provide a custom customer ID.", + async options({ page = 0 }) { + const { customers } = await this.listCustomers({ + params: { + page: page + 1, + }, + }); + return customers?.map?.(({ + email, id, + }) => ({ + label: email, + value: id, + })); + }, + }, + subscriptionId: { + type: "string", + label: "Subscription ID", + description: "Select a subscription or provide a custom subscription ID.", + async options({ page = 0 }) { + const { subscriptions } = await this.listSubscriptions({ + params: { + page: page + 1, + }, + }); + return subscriptions?.map?.(({ + id, sku, + }) => ({ + label: sku + ? `SKU ${sku}` + : `ID ${id}`, + value: id, + })); + }, + }, + addressId: { + type: "string", + label: "Address ID", + description: "Select an address or provide a custom address ID.", + async options({ page = 0 }) { + const { addresses } = await this.listAddresses({ + params: { + page: page + 1, + }, + }); + return addresses?.map?.((item) => ({ + label: [ + item.address1, + item.address2, + ].filter((i) => i).join(), + value: item.id, + })); + }, + }, + externalVariantId: { + type: "string", + label: "External Variant ID", + description: "The variant id as it appears in the external e-commerce platform.", + async options({ page = 0 }) { + const { products } = await this.listProducts({ + params: { + page: page + 1, + }, + }); + return products?.flatMap(({ variants }) => variants?.map((item) => ({ + label: item.title ?? item.sku, + value: item.external_variant_id, + }))).filter((i) => i) ?? []; + }, + }, + discountId: { + type: "string", + label: "Discount ID", + description: "The unique identifier for the discount.", + optional: true, + }, + firstName: { + type: "string", + label: "First Name", + description: "Customer's first name.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Customer's last name.", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "Email address of the customer.", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Customer's phone number. Must be in E.164 format, such as `+16175551212`", + optional: true, + }, + externalCustomerId: { + type: "string", + label: "External Customer ID", + description: "The customer ID as it appears in the external e-commerce platform.", + optional: true, + }, + taxExempt: { + type: "boolean", + label: "Tax Exempt", + description: "Whether or not the customer is tax exempt.", + optional: true, + }, + applyCreditToNextRecurringCharge: { + type: "boolean", + label: "Apply Credit to Next Recurring Charge", + description: "A boolean that indicates whether Recharge credits will be applied to the next recurring charge.", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.rechargeapps.com"; + }, + async _makeRequest({ + $ = this, + headers, + ...otherOpts + }) { + return axios($, { + ...otherOpts, + baseURL: this._baseUrl(), + headers: { + ...headers, + "X-Recharge-Access-Token": this.$auth.api_key, + }, + }); + }, + async listAddresses(args) { + return this._makeRequest({ + url: "/addresses", + ...args, + }); + }, + async listCustomers(args) { + return this._makeRequest({ + url: "/customers", + ...args, + }); + }, + async listProducts(args) { + return this._makeRequest({ + url: "/products", + ...args, + }); + }, + async listSubscriptions(args) { + return this._makeRequest({ + url: "/subscriptions", + ...args, + }); + }, + async createSubscription(args) { + return this._makeRequest({ + method: "POST", + url: "/subscriptions", + ...args, + }); + }, + async cancelSubscription({ + subscriptionId, ...args + }) { + return this._makeRequest({ + method: "POST", + url: `/subscriptions/${subscriptionId}/cancel`, + ...args, + }); + }, + async createCustomer(args) { + return this._makeRequest({ + method: "POST", + url: "/customers", + ...args, + }); + }, + async updateCustomer({ + customerId, ...args + }) { + return this._makeRequest({ + method: "PUT", + url: `/customers/${customerId}`, + ...args, + }); + }, + async createWebhook(args) { + return this._makeRequest({ + method: "POST", + url: "/webhooks", + ...args, + }); + }, + async deleteWebhook(id) { + return this._makeRequest({ + method: "DELETE", + url: `/webhooks/${id}`, + }); }, }, }; diff --git a/components/recharge/sources/common/common.mjs b/components/recharge/sources/common/common.mjs new file mode 100644 index 0000000000000..4dc42b1b3ccf6 --- /dev/null +++ b/components/recharge/sources/common/common.mjs @@ -0,0 +1,49 @@ +import app from "../../recharge.app.mjs"; + +export default { + props: { + app, + http: "$.interface.http", + db: "$.service.db", + }, + methods: { + getSummary() { + return "New event"; + }, + _getWebhookId() { + return this.db.get("webhookId"); + }, + _setWebhookId(value) { + this.db.set("webhookId", value); + }, + }, + hooks: { + async activate() { + const data = { + ...this.getHookData(), + address: this.http.endpoint, + }; + + const { webhook: { id } } = await this.app.createWebhook({ + data, + }); + this._setWebhookId(id); + }, + async deactivate() { + const id = this._getWebhookId(); + if (id) { + await this.app.deleteWebhook(id); + } + }, + }, + async run({ body }) { + if (body) { + const ts = Date.now(); + this.$emit(body, { + id: ts, + summary: this.getSummary(body), + ts, + }); + } + }, +}; diff --git a/components/recharge/sources/new-customer-created/new-customer-created.mjs b/components/recharge/sources/new-customer-created/new-customer-created.mjs new file mode 100644 index 0000000000000..56db1ae683080 --- /dev/null +++ b/components/recharge/sources/new-customer-created/new-customer-created.mjs @@ -0,0 +1,27 @@ +import common from "../common/common.mjs"; + +export default { + ...common, + key: "recharge-new-customer-created", + name: "New Customer Created (Instant)", + description: "Emit new event when a new customer is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSummary() { + return "New Customer"; + }, + getHookData() { + return { + topic: "customer/created", + included_objects: [ + "addresses", + "metafields", + // "payment_methods", // listed in documentation, but API returns an error + ], + }; + }, + }, +}; diff --git a/components/recharge/sources/new-order-created/new-order-created.mjs b/components/recharge/sources/new-order-created/new-order-created.mjs new file mode 100644 index 0000000000000..db5641494ae21 --- /dev/null +++ b/components/recharge/sources/new-order-created/new-order-created.mjs @@ -0,0 +1,26 @@ +import common from "../common/common.mjs"; + +export default { + ...common, + key: "recharge-new-order-created", + name: "New Order Created (Instant)", + description: "Emit new event when a new order is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSummary() { + return "New Order"; + }, + getHookData() { + return { + topic: "order/created", + included_objects: [ + "customer", + "metafields", + ], + }; + }, + }, +}; diff --git a/components/recharge/sources/new-subscription-created/new-subscription-created.mjs b/components/recharge/sources/new-subscription-created/new-subscription-created.mjs new file mode 100644 index 0000000000000..187eccb61abb5 --- /dev/null +++ b/components/recharge/sources/new-subscription-created/new-subscription-created.mjs @@ -0,0 +1,26 @@ +import common from "../common/common.mjs"; + +export default { + ...common, + key: "recharge-new-subscription-created", + name: "New Subscription Created (Instant)", + description: "Emit new event when a new subscription is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSummary() { + return "New Subscription"; + }, + getHookData() { + return { + topic: "subscription/created", + included_objects: [ + // "customer", // listed in documentation, but API returns an error + "metafields", + ], + }; + }, + }, +}; diff --git a/components/recreation_gov/README.md b/components/recreation_gov/README.md new file mode 100644 index 0000000000000..f20908caf8ba4 --- /dev/null +++ b/components/recreation_gov/README.md @@ -0,0 +1,11 @@ +# Overview + +The Recreation.gov API provides access to data about federal recreational activities and areas. With it, you can fetch details about campsites, tours, permits, and articles. On Pipedream, leverage this API to automate notifications, analyze recreational data, or integrate with other services for trip planning or environmental monitoring. + +# Example Use Cases + +- **Campsite Availability Alerts**: Set up a workflow on Pipedream that periodically checks for new campsite availability at your favorite parks. Pair it with the Twilio app to send SMS alerts when a spot opens up. + +- **Recreational Data Analysis**: Create a workflow that collects data on park visitations, campsite usage, and permit issuance. Pipe this data to Google Sheets, allowing for regular analysis and reporting on trends and patterns. + +- **Environmental Monitoring Integration**: Combine the Recreation.gov API with weather apps to inform users of the best times to visit based on weather conditions. Integrate with an email service like SendGrid to send custom weather-based recommendations for outdoor activities. diff --git a/components/recruit_crm/README.md b/components/recruit_crm/README.md new file mode 100644 index 0000000000000..9fb6922fd0128 --- /dev/null +++ b/components/recruit_crm/README.md @@ -0,0 +1,14 @@ +# Overview + +The Recruit CRM API provides a suite of functions to automate and streamline recruitment processes. It allows you to integrate your recruitment workflow with other services, create custom applications, or even build bots. With Pipedream, you can connect the Recruit CRM API to a vast array of other apps and services to automate actions like updating candidate records, syncing with HR systems, or triggering notifications based on recruitment activities. + +# Example Use Cases + +- **Automate Candidate Sourcing** + Whenever a new job opening is created in Recruit CRM, use Pipedream to automatically post the job details to multiple job boards or social media platforms like LinkedIn, Indeed or Glassdoor. + +- **Sync Candidate Progress with Google Sheets** + Monitor candidate progress through the recruitment pipeline by connecting Recruit CRM to Google Sheets. Each time a candidate moves to a new stage, Pipedream can automatically update a Google Sheet, keeping your team in sync. + +- **Candidate Feedback Collection and Analysis** + After an interview, trigger a feedback form to the interviewer using Google Forms or Typeform. Once the feedback is submitted, Pipedream can parse the responses and update the candidate's record in Recruit CRM, or send a digest to the hiring team. diff --git a/components/recruitee/README.md b/components/recruitee/README.md new file mode 100644 index 0000000000000..c0181c6b13653 --- /dev/null +++ b/components/recruitee/README.md @@ -0,0 +1,11 @@ +# Overview + +The Recruitee API allows you to automate and integrate your recruitment process with other tools and services. It provides programmatic access to manipulate jobs, candidates, and various recruitment activities. By using Pipedream, you can create custom workflows that interact with the Recruitee platform, enabling you to streamline your hiring process, synchronize recruitment data with other systems, and trigger actions based on recruitment events. + +# Example Use Cases + +- **Sync New Candidates to Google Sheets**: When a new candidate applies, automatically add their details to a Google Sheet. This workflow uses the Recruitee API to detect new candidates and the Google Sheets API to update a spreadsheet, keeping a real-time record of applicants. + +- **Trigger Email Campaigns for New Positions**: Send targeted email campaigns to potential candidates when a new job is posted. This workflow could use the Recruitee API to monitor for new jobs and trigger an email campaign via Mailchimp or another email service, ensuring prompt outreach to your talent pool. + +- **Automate Candidate Evaluation Notifications**: Notify team members in Slack when a candidate reaches a specific stage in the evaluation process. This workflow would use the Recruitee API to watch for candidate status changes and the Slack API to send messages to the relevant hiring team's channel or direct messages to specific evaluators. diff --git a/components/recruiterflow/package.json b/components/recruiterflow/package.json new file mode 100644 index 0000000000000..4fb51f701a3f3 --- /dev/null +++ b/components/recruiterflow/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/recruiterflow", + "version": "0.0.1", + "description": "Pipedream Recruiterflow Components", + "main": "recruiterflow.app.mjs", + "keywords": [ + "pipedream", + "recruiterflow" + ], + "homepage": "https://pipedream.com/apps/recruiterflow", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/recruiterflow/recruiterflow.app.mjs b/components/recruiterflow/recruiterflow.app.mjs new file mode 100644 index 0000000000000..2ed913a9dd926 --- /dev/null +++ b/components/recruiterflow/recruiterflow.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "recruiterflow", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/recruitis/README.md b/components/recruitis/README.md new file mode 100644 index 0000000000000..794a732e96cb8 --- /dev/null +++ b/components/recruitis/README.md @@ -0,0 +1,11 @@ +# Overview + +The Recruitis API is a tool designed to streamline the recruitment process by allowing developers to automate and integrate different parts of the recruitment workflow. Using Pipedream, you could leverage the Recruitis API to create custom serverless workflows, connecting various apps and services to automate tasks such as candidate screening, interview scheduling, feedback collection, and more. Pipedream’s capability to trigger workflows on specific events, and to connect with numerous other apps, makes it particularly suited for enhancing the efficiency of recruitment processes with the Recruitis API. + +# Example Use Cases + +- **Automated Candidate Screening**: Automatically screen candidates as soon as they apply. Connect the Recruitis API to filter applications based on custom criteria (e.g., skills, experience, location). Qualified candidates can be moved to the next stage in the pipeline, while others receive a polite rejection email via an Email Service Provider like SendGrid. + +- **Interview Scheduling**: Streamline the scheduling process by connecting the Recruitis API with Google Calendar. When a candidate passes the screening stage, trigger a workflow that checks the hiring team’s availability and sends a calendar invite to both the interviewers and the candidate, ensuring no double bookings happen. + +- **Candidate Feedback Aggregation**: After interviews, collect feedback efficiently by connecting the Recruitis API to a form tool like Typeform. Trigger a workflow to send out a feedback form to interviewers. Once feedback is collected, aggregate the responses and update the candidate's status in the Recruitis system, keeping everyone in the loop automatically. diff --git a/components/recruitis/actions/create-answer/create-answer.mjs b/components/recruitis/actions/create-answer/create-answer.mjs new file mode 100644 index 0000000000000..97e5287f576e3 --- /dev/null +++ b/components/recruitis/actions/create-answer/create-answer.mjs @@ -0,0 +1,84 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../recruitis.app.mjs"; + +export default { + key: "recruitis-create-answer", + name: "Create Answer", + description: "Create a new answer for a job. [See the documentation](https://docs.recruitis.io/api/#tag/Candidates/paths/~1answers/post)", + version: "0.0.1", + type: "action", + props: { + app, + jobID: { + propDefinition: [ + app, + "jobID", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + phone: { + propDefinition: [ + app, + "phone", + ], + }, + twitter: { + propDefinition: [ + app, + "twitter", + ], + }, + facebook: { + propDefinition: [ + app, + "facebook", + ], + }, + linkedin: { + propDefinition: [ + app, + "linkedin", + ], + }, + skype: { + propDefinition: [ + app, + "skype", + ], + }, + }, + async run({ $ }) { + if (this.linkedin && !this.linkedin.includes("/in")) { + throw new ConfigurationError("Linkedin URL is wrong, it should contain \"/in\". E.g https://linkedin.com/in/pipedream"); + } + + const response = await this.app.createAnswer({ + $, + data: { + job_id: this.jobID, + email: this.email, + name: this.name, + phone: this.phone, + twitter: this.twitter, + facebook: this.facebook, + linkedin: this.linkedin, + skype: this.skype, + }, + }); + + $.export("$summary", `Successfully created an answer for the user with ID: '${response.payload.user_id}'`); + + return response; + }, +}; diff --git a/components/recruitis/actions/create-candidate/create-candidate.mjs b/components/recruitis/actions/create-candidate/create-candidate.mjs new file mode 100644 index 0000000000000..f8ed073bfc075 --- /dev/null +++ b/components/recruitis/actions/create-candidate/create-candidate.mjs @@ -0,0 +1,85 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../recruitis.app.mjs"; + +export default { + key: "recruitis-create-candidate", + name: "Create Candidate", + description: "Create a new candidate. [See the documentation](https://docs.recruitis.io/api/#tag/Candidates/paths/~1candidates/post)", + version: "0.0.1", + type: "action", + props: { + app, + email: { + propDefinition: [ + app, + "email", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + phone: { + propDefinition: [ + app, + "phone", + ], + }, + twitter: { + propDefinition: [ + app, + "twitter", + ], + }, + facebook: { + propDefinition: [ + app, + "facebook", + ], + }, + linkedin: { + propDefinition: [ + app, + "linkedin", + ], + }, + skype: { + propDefinition: [ + app, + "skype", + ], + }, + gdprAgreement: { + propDefinition: [ + app, + "gdprAgreement", + ], + }, + }, + async run({ $ }) { + if (this.linkedin && !this.linkedin.includes("/in")) { + throw new ConfigurationError("Linkedin URL is wrong, it should contain \"/in\". E.g https://linkedin.com/in/pipedream"); + } + + const response = await this.app.createCandidate({ + $, + data: { + job_id: this.jobID, + email: this.email, + name: this.name, + phone: this.phone, + twitter: this.twitter, + facebook: this.facebook, + linkedin: this.linkedin, + skype: this.skype, + gdpr_agreement: this.gdprAgreement, + }, + }); + + $.export("$summary", `Successfully created user with ID: '${response.payload.id}'`); + + return response; + }, +}; diff --git a/components/recruitis/actions/create-job/create-job.mjs b/components/recruitis/actions/create-job/create-job.mjs new file mode 100644 index 0000000000000..ddf91bbbb60ab --- /dev/null +++ b/components/recruitis/actions/create-job/create-job.mjs @@ -0,0 +1,37 @@ +import app from "../../recruitis.app.mjs"; + +export default { + key: "recruitis-create-job", + name: "Create Job", + description: "Creates a new job ad and puts it in classifieds. [See the documentation](https://docs.recruitis.io/api/#tag/Jobs/paths/~1jobs/post)", + version: "0.0.1", + type: "action", + props: { + app, + title: { + propDefinition: [ + app, + "title", + ], + }, + description: { + propDefinition: [ + app, + "description", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createJob({ + $, + data: { + description: this.description, + title: this.title, + }, + }); + + $.export("$summary", `Successfully created job at: '${response.payload.edit_url}'`); + + return response; + }, +}; diff --git a/components/recruitis/actions/get-jobs/get-jobs.mjs b/components/recruitis/actions/get-jobs/get-jobs.mjs new file mode 100644 index 0000000000000..9b75a39a45cf4 --- /dev/null +++ b/components/recruitis/actions/get-jobs/get-jobs.mjs @@ -0,0 +1,21 @@ +import app from "../../recruitis.app.mjs"; + +export default { + key: "recruitis-get-jobs", + name: "Get Jobs", + description: "Get jobs from recruitis profile. [See the documentation](https://docs.recruitis.io/api/#tag/Jobs/paths/~1jobs/get)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.getJobs({ + $, + }); + + $.export("$summary", `Successfully retrieved ${response.payload.length} job(s)`); + + return response; + }, +}; diff --git a/components/recruitis/package.json b/components/recruitis/package.json index 2e0b83f7b2aa2..d2ecf82e2312d 100644 --- a/components/recruitis/package.json +++ b/components/recruitis/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/recruitis", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Recruitis Components", "main": "recruitis.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" } -} \ No newline at end of file +} diff --git a/components/recruitis/recruitis.app.mjs b/components/recruitis/recruitis.app.mjs index beb9fc1a18320..3e82d7fe2f98d 100644 --- a/components/recruitis/recruitis.app.mjs +++ b/components/recruitis/recruitis.app.mjs @@ -1,11 +1,126 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "recruitis", - propDefinitions: {}, + propDefinitions: { + title: { + type: "string", + label: "Title", + description: "Title of the job", + }, + description: { + type: "string", + label: "Description", + description: "Description of the job", + }, + name: { + type: "string", + label: "Name", + description: "Applicant's name", + }, + email: { + type: "string", + label: "Email", + description: "Applicant's email address", + }, + skype: { + type: "string", + label: "Skype", + description: "Applicant's Skype link", + optional: true, + }, + linkedin: { + type: "string", + label: "LinkedIn", + description: "Applicant's LinkedIn link", + optional: true, + }, + facebook: { + type: "string", + label: "Facebook", + description: "Applicant's Facebook link", + optional: true, + }, + twitter: { + type: "string", + label: "Twitter", + description: "Applicant's Twitter link", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Applicant's phone number", + optional: true, + }, + gdprAgreement: { + type: "boolean", + label: "GDPR Agreement", + description: "Indicates whether the applicant has agreed to GDPR terms", + }, + jobID: { + type: "string", + label: "Job ID", + description: "ID of the job", + async options() { + const response = await this.getJobs(); + + return response?.payload?.map(({ + job_id, title, + }) => ({ + value: job_id, + label: title, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://app.recruitis.io/api2"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_token}`, + }, + }); + }, + async getJobs(args = {}) { + return this._makeRequest({ + path: "/jobs", + ...args, + }); + }, + async createAnswer(args = {}) { + return this._makeRequest({ + method: "post", + path: "/answers", + ...args, + }); + }, + async createJob(args = {}) { + return this._makeRequest({ + method: "post", + path: "/jobs", + ...args, + }); + }, + async createCandidate(args = {}) { + return this._makeRequest({ + method: "post", + path: "/candidates", + ...args, + }); }, }, }; diff --git a/components/recurly/README.md b/components/recurly/README.md index 175f3b1e958cc..fa5fcc3d757b2 100644 --- a/components/recurly/README.md +++ b/components/recurly/README.md @@ -1,22 +1,11 @@ # Overview -The Recurly API provides developers an array of powerful methods to build and -integrate with Recurly’s subscription billing service. Using the API, you can -create unique customer experiences, customized to fit your brand and needs, as -well as payment collection, tracking usage in real-time, setting up promotions, -and managing customer subscriptions. +Recurly API provides robust functionality for subscription management, including creating and updating customer accounts, managing billing information, and handling subscription lifecycle events. With Pipedream, you can leverage these capabilities to create automated workflows that respond to events in Recurly, sync data with other systems, and perform actions that enhance your subscription operations and customer experiences. -Here are some common examples you can build with the Recurly API: +# Example Use Cases -- Manage customers & accounts: Create and update customers, their billing info - and subscription plans. -- Collect payments: Accept ACH payments, one-time credit cards, and update - subscription payment methods. -- Real-time usage tracking: Track changes in user usage to power usage-based - billing & usage-based access management. -- Promotions & discounts: Create and apply discounts to accounts, including - coupon codes and percentage discounts. -- Reporting & Analytics: Get real-time reports to quickly gauge and understand - your business performance. -- Security & Fraud Prevention: Prevent fraudulent transactions and manage - customer data securely. +- **Invoice Payment Tracking to Slack**: Send a notification to a designated Slack channel when a Recurly invoice is paid. This keeps your team instantly informed about revenue events and can be expanded to include customer details for a personal touch. + +- **Dunning Process Management with Email Notifications**: Trigger an email campaign using SendGrid for customers entering the dunning process. Customize reminders and payment updates to encourage subscription continuity and maintain positive customer relationships. + +- **Sync New Subscribers to a Google Sheets**: Automatically add new Recurly subscribers to a Google Sheet for easy access to subscription data. This can assist with reporting, analysis, and sharing up-to-date information with team members who may not have direct access to Recurly. diff --git a/components/redcircle_api/README.md b/components/redcircle_api/README.md new file mode 100644 index 0000000000000..b614ba7ed8500 --- /dev/null +++ b/components/redcircle_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The RedCircle API enables podcasters to manage their shows, episodes, and analytics programmatically. Through Pipedream, you can harness this API to create custom workflows, integrating podcast operations with other apps and services. Automate tasks like uploading new episodes, syncing analytics data, or triggering marketing campaigns when a new episode goes live. With Pipedream's serverless platform, you can build these automations without managing infrastructure, leaning on Pipedream's vast app ecosystem and event-driven architecture. + +# Example Use Cases + +- **Automatic Episode Publishing**: When you upload a new episode to a cloud storage platform (like Dropbox or Google Drive), Pipedream detects the new file and uses the RedCircle API to publish the episode to your podcast. + +- **Listener Engagement Analysis**: After an episode is released, Pipedream pulls the latest analytics data from the RedCircle API and sends a comprehensive report via email or into a Slack channel, helping you stay informed on listener engagement. + +- **Social Media Marketing Automation**: When a new episode goes live, Pipedream triggers a workflow that posts the episode details to social media platforms like Twitter or Facebook, increasing your reach and saving you time on manual updates. diff --git a/components/redcircle_api/package.json b/components/redcircle_api/package.json new file mode 100644 index 0000000000000..57b45351906af --- /dev/null +++ b/components/redcircle_api/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/redcircle_api", + "version": "0.0.1", + "description": "Pipedream RedCircle API Components", + "main": "redcircle_api.app.mjs", + "keywords": [ + "pipedream", + "redcircle_api" + ], + "homepage": "https://pipedream.com/apps/redcircle_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/redcircle_api/redcircle_api.app.mjs b/components/redcircle_api/redcircle_api.app.mjs new file mode 100644 index 0000000000000..b57d3ab691910 --- /dev/null +++ b/components/redcircle_api/redcircle_api.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "redcircle_api", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/reddit/README.md b/components/reddit/README.md index a76bdb5391bc9..f0f7e5ad140cb 100644 --- a/components/reddit/README.md +++ b/components/reddit/README.md @@ -1,15 +1,6 @@ # Overview -1. You could build a tool that monitors specific subreddits and notifies you - when new posts match certain criteria. -2. You could build a tool that analyzes the posts and comments in a given - subreddit to generate statistics or visualizations about the topic. -3. You could build a bot that automatically posts or comments in response to - certain keywords or phrases. -4. You could build a tool that helps manage your Reddit account by automating - tasks like posting, messaging, or voting. -5. You could build a tool that extracts data from Reddit posts and comments for - use in other applications. +The Reddit API unlocks the potential to automate interactions with one of the largest online communities. With Pipedream, you can tap into the Reddit ecosystem to monitor trends, engage with audiences, or curate content. Whether it's tracking mentions of your brand, auto-posting to subreddits, or gathering data for analysis, the API offers a breadth of functionalities — from getting posts and comments to creating new threads and managing account settings. # Getting Started @@ -42,3 +33,11 @@ In order to maintain access to the Reddit APIs, you will need to register your a 3. When connecting the Reddit app in Pipedream, copy and paste your **client id** along with your **client secret**. 4. Click **Connect** and your custom Reddit app should be integrated into Pipedream! + +# Example Use Cases + +- **Automated Social Listening**: Create a workflow that triggers whenever your brand is mentioned in specific subreddits. Use this to gather real-time feedback, monitor sentiment, and stay ahead of PR issues. Connect with a sentiment analysis tool like Google NLP on Pipedream to evaluate the tone of mentions. + +- **Content Curation and Notification**: Set up a system that watches for new trending posts in your industry's subreddits, then compile and email a daily digest using Pipedream's built-in email action. This can keep a content marketing team informed and ready to engage with the latest trends. + +- **Subreddit Management Automation**: For moderators and community managers, automate common subreddit tasks such as removing posts that don't meet certain criteria, banning users who repeatedly violate rules, or tagging and categorizing posts. This can be linked with a database like Airtable on Pipedream to log actions and keep an organized record. diff --git a/components/redmine/README.md b/components/redmine/README.md new file mode 100644 index 0000000000000..beabd2b0e3497 --- /dev/null +++ b/components/redmine/README.md @@ -0,0 +1,11 @@ +# Overview + +The Redmine API allows for integration with the Redmine project management tool, enabling the automation of tasks like issue tracking, project management, and time tracking. Using Pipedream, you can create workflows that trigger on specific Redmine events or perform actions in Redmine based on triggers from other apps. By harnessing the Redmine API on Pipedream, you can connect Redmine with a multitude of other apps and services to streamline your project management processes. + +# Example Workflows + +- **Sync Redmine Issues to Google Sheets**: Automatically export newly created or updated Redmine issues to a Google Sheets spreadsheet for advanced reporting or data analysis. This workflow can be set up to run periodically, ensuring your Sheets are always up-to-date with the latest issue data. + +- **Slack Notifications for Redmine Updates**: Set up a workflow where updates to issues or projects in Redmine trigger notifications in a designated Slack channel. This keeps your team informed in real-time about important changes, like status updates or newly assigned tasks. + +- **GitHub Commit Linked to Redmine Issues**: Whenever a new commit is pushed to a GitHub repository, search for Redmine issue IDs in the commit message and update the corresponding issue with the commit details. This helps maintain a clear link between code changes and project tasks. diff --git a/components/referral_rocket/package.json b/components/referral_rocket/package.json new file mode 100644 index 0000000000000..9859a84ab005a --- /dev/null +++ b/components/referral_rocket/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/referral_rocket", + "version": "0.0.1", + "description": "Pipedream Referral Rocket Components", + "main": "referral_rocket.app.mjs", + "keywords": [ + "pipedream", + "referral_rocket" + ], + "homepage": "https://pipedream.com/apps/referral_rocket", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/referral_rocket/referral_rocket.app.mjs b/components/referral_rocket/referral_rocket.app.mjs new file mode 100644 index 0000000000000..94d8bcc305989 --- /dev/null +++ b/components/referral_rocket/referral_rocket.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "referral_rocket", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/referralhero/README.md b/components/referralhero/README.md new file mode 100644 index 0000000000000..f15815f0cca69 --- /dev/null +++ b/components/referralhero/README.md @@ -0,0 +1,11 @@ +# Overview + +The ReferralHero API allows you to build custom referral marketing campaigns, manage participants, and track referral performance directly from Pipedream. You can automate participant registration, reward distribution, and integrate with other services for enhanced functionality like email marketing, CRM updates, or analytics. By leveraging Pipedream’s serverless platform, you can create workflows that react to events in ReferralHero, enrich referral data, or synchronize it with other apps. + +# Example Use Cases + +- **Sync New Referrals to Google Sheets**: When a new referral signs up, automatically add their details to a Google Sheets spreadsheet. This workflow can help maintain an up-to-date list for tracking and further analysis. + +- **Send Welcome Emails via SendGrid**: Automatically send personalized welcome emails using SendGrid to new participants who join your referral program. This can improve engagement and provide immediate confirmation upon registration. + +- **Update CRM with Referral Status**: On a successful referral, trigger a workflow to update the referrer's status in a CRM like Salesforce. This ensures your sales team has the latest information on customer referrals for follow-up. diff --git a/components/referralrock/README.md b/components/referralrock/README.md index 6e374f736c7e0..cf7469b9c6bc4 100644 --- a/components/referralrock/README.md +++ b/components/referralrock/README.md @@ -1,23 +1,11 @@ # Overview -With ReferralRock API, you can build automated referral programs that track, -manage, and measure the success of your referral program. The API allows you -to easily create, integrate, and customize referral programs that you can use -to drive signups and increase sales. +ReferralRock's API unlocks the power of referral marketing within your applications, allowing you to automate the integration of your referral programs into your business processes. By leveraging this API on Pipedream, you can create dynamic, serverless workflows that respond to events in ReferralRock, synchronize data with other platforms, reward customer loyalty, and track the performance of your referral campaigns in real-time. This enables a seamless blend of referral marketing with your existing tech stack. -Powered by ReferralRock API, you can build the following programs: +# Example Use Cases -- Customizable rewards program : Build a custom rewards program that rewards - customers for referrals and purchases. -- Brand ambassador program : Identify and reward people who promote your - products to their friends, family, and followers. -- Refer-a-friend program : Give customers a unique URL to share with their - friends and reward them for completed purchases. -- VIP program : Offer exclusive rewards and discounts to your most loyal - customers. -- Referral contest : Ramp-up signups and sales by creating referral contests - with exciting prizes. -- Email invitation program : Automatically trigger referral-driven emails to - customers and followers with incentives to join your program. -- Reach out to influencers : Connect with and reward influencers that have - large followerships who can help spread the word about your products. +- **Automated Referral Reward Fulfillment**: When a new referral reaches a successful conversion status in ReferralRock, trigger a Pipedream workflow to send a personalized thank-you email via SendGrid and automatically issue a reward, such as a coupon code from your e-commerce platform. + +- **Lead Enrichment and Follow-Up**: On receiving a new referral lead, use Pipedream to enrich the lead data with Clearbit, then create a contact in HubSpot and assign a task to your sales team to follow up, ensuring quick engagement and maximizing the chance of conversion. + +- **Referral Performance Dashboard Sync**: Set up a Pipedream workflow that periodically fetches referral data from ReferralRock and updates a Google Sheets dashboard, providing your marketing team with up-to-date insights about the referral program's performance and ROI. diff --git a/components/referrizer/README.md b/components/referrizer/README.md new file mode 100644 index 0000000000000..4616b7e1c3e4e --- /dev/null +++ b/components/referrizer/README.md @@ -0,0 +1,11 @@ +# Overview + +The Referrizer API lets you automate the engagement and tracking of your referral marketing campaigns. By integrating with Pipedream, you can stitch the Referrizer API into workflows that streamline your marketing efforts, track referrals, manage rewards, and sync customer data across multiple platforms. Pipedream’s serverless execution model allows you to focus on defining the logic without worrying about the infrastructure. + +# Example Use Cases + +- **Sync New Referral Sign-ups to CRM**: Automatically add new referral sign-ups from Referrizer to your CRM like Salesforce or HubSpot. When a new referral is captured by Referrizer, trigger a workflow in Pipedream to create a new contact or update an existing one in your CRM, ensuring your sales team has the latest leads to follow up on. + +- **Send Custom Thank-You Messages**: Craft personalized thank-you emails or SMS to customers who refer new clients through Twilio or SendGrid. Set up a Pipedream workflow that triggers when a referral is made on Referrizer, fetches the referrer’s contact details, and sends a customized message to thank them for their referral, reinforcing positive customer relationships. + +- **Aggregate Referral Data for Analytics**: Collect and analyze referral data in real-time by sending it from Referrizer to Google Sheets or a database like PostgreSQL. Implement a Pipedream workflow that takes each new referral event, extracts relevant data, and appends it to a Google Sheet for easy tracking and analysis, giving you insights into the most effective referral sources. diff --git a/components/refersion/README.md b/components/refersion/README.md index b330de5629d15..2a17eaae08eff 100644 --- a/components/refersion/README.md +++ b/components/refersion/README.md @@ -1,37 +1,11 @@ # Overview -The Refersion API gives you the power to build secure and feature-rich -applications that extend the functionality of the Refersion platform. With the -Refersion API, you can create applications that target referral and affiliate -marketing campaigns, product promotions, and other marketing initiatives. +Refersion is a powerful affiliate tracking platform that streamlines the process of managing, tracking, and growing your affiliate network. Through its API, you can automate affiliate registrations, commission tracking, and payout processes. Pipedream's serverless platform allows you to connect Refersion with hundreds of other apps to automate workflows, sync data across platforms, and respond to events in real time. -You can use the Refersion API to: +# Example Use Cases -- Automate referral and affiliate marketing campaigns -- Create product promotions -- Access sales tracking reports -- Create affiliate links -- Monitor performance of campaigns -- Integrate with third-party applications -- Create custom dashboards to report on marketing initiatives -- Track user referrals -- Create custom tools for affiliates and merchants -- Create promotions and discount codes -- Manage subscriber data -- Monitor promotional and coupon codes -- Generate referral links -- Automatically create accounts -- Manage payment schedules -- Track sales and payments -- Build real-time analytics and reporting on campaigns -- Create reports for performance optimization -- Monitor customer interactions -- Send notifications to affiliates -- Create personalization for users -- Set access levels for affiliates and merchants -- Monitor and approve affiliate applications -- Automatically generate coupon codes -- Manage customer relationships -- Generate reports to measure ROI -- Create custom landing pages -- Customize the branding of applications +- **Affiliate Performance Reporting to Slack**: Trigger a Pipedream workflow whenever an affiliate generates a sale. Use the Refersion API to retrieve the sale details and then format a message summarizing the affiliate's performance, which is sent to a specified Slack channel. This allows for immediate team updates on affiliate success. + +- **Email Drip Campaign for New Affiliates**: When a new affiliate registers through the Refersion API, use Pipedream to add them to a Mailchimp list automatically. Then, trigger a series of welcome and educational emails over the following weeks to engage and inform the new affiliate about maximizing their impact. + +- **Affiliate Payout Automation with Stripe**: At the end of each payout period, compile affiliate commissions using the Refersion API and execute a bulk payout with Stripe through Pipedream. This workflow ensures that affiliates are paid in a timely manner, with minimal manual intervention. diff --git a/components/refiner/actions/identify-user/identify-user.mjs b/components/refiner/actions/identify-user/identify-user.mjs new file mode 100644 index 0000000000000..cfacb13522162 --- /dev/null +++ b/components/refiner/actions/identify-user/identify-user.mjs @@ -0,0 +1,43 @@ +import { ConfigurationError } from "@pipedream/platform"; +import refiner from "../../refiner.app.mjs"; + +export default { + key: "refiner-identify-user", + name: "Identify User", + description: "Identify a user with user ID or email. If the user does not exist, a new one will be created. [See the documentation](https://refiner.io/docs/api/#identify-user)", + version: "0.0.1", + type: "action", + props: { + refiner, + userId: { + propDefinition: [ + refiner, + "userId", + ], + optional: true, + }, + email: { + propDefinition: [ + refiner, + "email", + ], + optional: true, + }, + }, + async run({ $ }) { + if (!this.userId && !this.email) { + throw new ConfigurationError("Either User ID or Email must be provided to identify the user."); + } + + const response = await this.refiner.identifyUser({ + $, + data: { + id: this.userId, + email: this.email, + }, + }); + + $.export("$summary", `User identified successfully. Contact UUID: ${response.contact_uuid}`); + return response; + }, +}; diff --git a/components/refiner/actions/track-event/track-event.mjs b/components/refiner/actions/track-event/track-event.mjs new file mode 100644 index 0000000000000..50816625d77e8 --- /dev/null +++ b/components/refiner/actions/track-event/track-event.mjs @@ -0,0 +1,49 @@ +import { ConfigurationError } from "@pipedream/platform"; +import refiner from "../../refiner.app.mjs"; + +export default { + key: "refiner-track-event", + name: "Track Event", + description: "Tracks a user event in Refiner. [See the documentation](https://refiner.io/docs/api/#track-event)", + version: "0.0.1", + type: "action", + props: { + refiner, + eventName: { + type: "string", + label: "Event Name", + description: "The name of the event or signal being tracked.", + }, + userId: { + propDefinition: [ + refiner, + "userId", + ], + optional: true, + }, + email: { + propDefinition: [ + refiner, + "email", + ], + optional: true, + }, + }, + async run({ $ }) { + if (!this.userId && !this.email) { + throw new ConfigurationError("Either User ID or Email must be provided to track the event."); + } + + const response = await this.refiner.trackEvent({ + $, + data: { + event: this.eventName, + id: this.userId, + email: this.email, + }, + }); + + $.export("$summary", `Tracked event "${this.eventName}" successfully.`); + return response; + }, +}; diff --git a/components/refiner/package.json b/components/refiner/package.json new file mode 100644 index 0000000000000..a941ecdb5faa6 --- /dev/null +++ b/components/refiner/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/refiner", + "version": "0.1.0", + "description": "Pipedream Refiner Components", + "main": "refiner.app.mjs", + "keywords": [ + "pipedream", + "refiner" + ], + "homepage": "https://pipedream.com/apps/refiner", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/refiner/refiner.app.mjs b/components/refiner/refiner.app.mjs new file mode 100644 index 0000000000000..cf10d14125339 --- /dev/null +++ b/components/refiner/refiner.app.mjs @@ -0,0 +1,133 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "refiner", + propDefinitions: { + userId: { + type: "string", + label: "User ID", + description: "The ID of the user to identify or track events for.", + async options({ page }) { + const { items } = await this.listContacts({ + params: { + page: page + 1, + }, + }); + + return items.map(({ + uuid: value, email: label, + }) => ({ + label, + value, + })); + }, + }, + email: { + type: "string", + label: "Email", + description: "The email address of the user to identify or track events for.", + optional: true, + }, + segmentId: { + type: "string", + label: "Segment ID", + description: "The ID of the segment to emit events for.", + async options({ page }) { + const { items } = await this.listSegments({ + params: { + page: page + 1, + }, + }); + + return items.map(({ + uuid: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.refiner.io/v1"; + }, + _headers() { + return { + "Authorization": `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts", + ...opts, + }); + }, + listResponses(opts = {}) { + return this._makeRequest({ + path: "/responses", + ...opts, + }); + }, + listSegments(opts = {}) { + return this._makeRequest({ + path: "/segments", + ...opts, + }); + }, + identifyUser(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/identify-user", + ...opts, + }); + }, + trackEvent(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/track-event", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const { + items, + pagination: { + current_page, last_page, + }, + } = await fn({ + params, + ...opts, + }); + for (const d of items) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = current_page < last_page; + + } while (hasMore); + }, + }, +}; diff --git a/components/refiner/sources/common/base.mjs b/components/refiner/sources/common/base.mjs new file mode 100644 index 0000000000000..212cf5432a699 --- /dev/null +++ b/components/refiner/sources/common/base.mjs @@ -0,0 +1,64 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import refiner from "../../refiner.app.mjs"; + +export default { + props: { + refiner, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + + const response = this.refiner.paginate({ + fn: this.getFunction(), + params: this.getParams(), + }); + + let responseArray = []; + for await (const item of response) { + const itemDate = this.getItemDate(item); + if (Date.parse(itemDate) <= lastDate) break; + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + const itemDate = this.getItemDate(responseArray[0]); + this._setLastDate(itemDate); + } + + for (const item of responseArray.reverse()) { + const itemDate = this.getItemDate(item); + + this.$emit(item, { + id: item.uuid, + summary: this.getSummary(item), + ts: Date.parse(itemDate), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/refiner/sources/new-segment-entry/new-segment-entry.mjs b/components/refiner/sources/new-segment-entry/new-segment-entry.mjs new file mode 100644 index 0000000000000..d8cc44f2f32e0 --- /dev/null +++ b/components/refiner/sources/new-segment-entry/new-segment-entry.mjs @@ -0,0 +1,40 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "refiner-new-segment-entry", + name: "New Segment Entry", + description: "Emit new event whenever a user enters a segment in Refiner. [See the documentation](https://refiner.io/docs/api/#get-contacts)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + segmentId: { + propDefinition: [ + common.props.refiner, + "segmentId", + ], + }, + }, + methods: { + ...common.methods, + getFunction() { + return this.refiner.listContacts; + }, + getParams() { + return { + segment_uuid: this.segmentId, + }; + }, + getSummary(item) { + return `User ${item.email} entered segment ${this.segmentId}`; + }, + getItemDate(item) { + return item.segments + .filter(({ uuid }) => uuid === this.segmentId)[0].created_at; + }, + }, + sampleEmit, +}; diff --git a/components/refiner/sources/new-segment-entry/test-event.mjs b/components/refiner/sources/new-segment-entry/test-event.mjs new file mode 100644 index 0000000000000..3c347997c7c09 --- /dev/null +++ b/components/refiner/sources/new-segment-entry/test-event.mjs @@ -0,0 +1,32 @@ +export default { + "uuid": "15cce3d0-ed5d-11ea-aaef-e58a2a43e996", + "remote_id": "Your-Contact-ID", + "email": "jane@awesome.com", + "display_name": "Jane Doe", + "first_seen_at": "2020-09-02T20:44:24.000000Z", + "last_seen_at": "2020-09-02T21:57:50.000000Z", + "attributes": { + "a_user_attribute": "Manager", + "another_one": "Marketing", + "a_survey_answer": "9", + "another_answer": "ABC", + }, + "segments": [ + { + "uuid": "0ff87720-9ae5-11ea-bce5-65a395204572", + "name": "Power Users Segment" + }, + ], + "account": { + "uuid": "15d08cc0-ed5d-11ea-b2ce-c1b46bd4b7c4", + "remote_id": "Your-Account-Id", + "domain": "awesome.com", + "display_name": "Awesome Inc.", + "first_seen_at": "2020-09-02T20:44:24.000000Z", + "last_seen_at": "2020-09-02T21:57:50.000000Z", + "attributes": { + "an_account_attribute": "Computer Software", + "another_one": "2020", + } + } +} \ No newline at end of file diff --git a/components/refiner/sources/new-survey-completion/new-survey-completion.mjs b/components/refiner/sources/new-survey-completion/new-survey-completion.mjs new file mode 100644 index 0000000000000..5ed361b3494e5 --- /dev/null +++ b/components/refiner/sources/new-survey-completion/new-survey-completion.mjs @@ -0,0 +1,30 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "refiner-new-survey-completion", + name: "New Survey Completion", + description: "Emit new event whenever a user completes a survey in Refiner. [See the documentation](https://refiner.io/docs/api/#get-responses)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.refiner.listResponses; + }, + getParams() { + return { + include: "completed", + }; + }, + getSummary(item) { + return `Survey (${item.form.uuid}) completed by user ${item.contact.uuid}`; + }, + getItemDate(item) { + return item.completed_at; + }, + }, + sampleEmit, +}; diff --git a/components/refiner/sources/new-survey-completion/test-event.mjs b/components/refiner/sources/new-survey-completion/test-event.mjs new file mode 100644 index 0000000000000..894542ed21802 --- /dev/null +++ b/components/refiner/sources/new-survey-completion/test-event.mjs @@ -0,0 +1,31 @@ +export default { + "uuid": "cb47c260-ed64-11ea-8187-a7fc8351fba4", + "first_shown_at": "2020-09-02 21:53:33", + "last_shown_at": "2020-09-02 21:53:33", + "show_counter": 1, + "first_data_reception_at": "2020-09-02 21:53:33", + "last_data_reception_at": "2020-09-02 21:53:33", + "completed_at": "2020-09-02 21:53:33", + "received_at": "2020-09-02 21:53:33", + "dismissed_at": null, + "form": { + "uuid": "3894dc20-8fe9-11ea-892e-d13af52e06ae", + "name": "My first survey" + }, + "data": { + "first_question": "First Reply", + "second_question": "Second Reply" + }, + "contact": { + "uuid": "e5365340-ed47-11ea-9a73-61d23f380055", + "remote_id": "YOUR-USER-ID-123", + "email": "jane@awesome.com", + "display_name": "Jane Doe", + "account": { + "uuid": "5ab55ab0-ebf3-11ea-a442-4fa2c72e1cf7", + "remote_id": "Your account ID", + "display_name": "Awesome Inc.", + "domain": null + } + } +} \ No newline at end of file diff --git a/components/reflect/README.md b/components/reflect/README.md new file mode 100644 index 0000000000000..5a81c4bded750 --- /dev/null +++ b/components/reflect/README.md @@ -0,0 +1,11 @@ +# Overview + +The Reflect API offers a platform for creating automated tests without writing code. It enables users to build workflows that can record and replay web interactions, check the functionality and performance of web applications, and trigger alerts or actions based on test results. With Pipedream, you can harness the Reflect API to integrate automated testing into complex workflows, combining it with various apps and services for a seamless DevOps experience. + +# Example Use Cases + +- **Automated Regression Testing**: Set up a Pipedream workflow to trigger Reflect tests after code commits. Connect it with GitHub to listen for `push` events and automatically run relevant tests, ensuring that new changes don't break existing functionality. + +- **Scheduled UI Testing**: Create a workflow that triggers Reflect UI tests at regular intervals. You can schedule tests to run nightly, ensuring your application's front-end remains consistent and functional without manual intervention. + +- **Alerts and Notifications Based on Test Results**: Configure a Pipedream workflow where Reflect API test results are evaluated, and depending on outcome, it sends notifications. Integrate with Slack to alert your team immediately if a test fails, allowing for rapid response to potential issues. diff --git a/components/reform/README.md b/components/reform/README.md new file mode 100644 index 0000000000000..5d1dec5d4b485 --- /dev/null +++ b/components/reform/README.md @@ -0,0 +1,11 @@ +# Overview + +The Reform API allows you to automate the management and analysis of forms and surveys. By connecting Reform to Pipedream, you can create, update, and retrieve form submissions, and set up workflows that trigger on new responses. This opens up possibilities for integrating form data with other tools, managing event-driven notifications, or feeding customer insights into your CRM systems—all in a serverless environment that scales with your needs. + +# Example Use Cases + +- **Automated Response Aggregation**: Collect submissions from a Reform form and aggregate them in a Google Sheet. Whenever a new form is submitted, a Pipedream workflow triggers, extracts the form data, and appends it to the Google Sheet, keeping records up-to-date without manual entry. + +- **Dynamic CRM Updates**: Keep your CRM system in sync with customer feedback. Each time a Reform submission comes through, use Pipedream to parse the response and update the relevant customer profile in HubSpot, ensuring your sales and support teams have the latest information. + +- **Slack Notification for Survey Responses**: Stay on top of user feedback by getting instant notifications. Set up a Pipedream workflow that listens for new Reform submissions and sends a formatted message to a designated Slack channel, allowing your team to quickly react to customer insights or issues. diff --git a/components/regal/actions/add-contact-to-event/add-contact-to-event.mjs b/components/regal/actions/add-contact-to-event/add-contact-to-event.mjs new file mode 100644 index 0000000000000..401d775b8bbc7 --- /dev/null +++ b/components/regal/actions/add-contact-to-event/add-contact-to-event.mjs @@ -0,0 +1,110 @@ +import regal from "../../regal.app.mjs"; + +export default { + key: "regal-add-contact-to-event", + name: "Add Contact to Event", + description: "Add a contact to an event. [See the documentation](https://developer.regal.io/reference/api)", + version: "0.0.1", + type: "action", + props: { + regal, + userId: { + propDefinition: [ + regal, + "userId", + ], + optional: false, + }, + phone: { + propDefinition: [ + regal, + "phone", + ], + }, + email: { + propDefinition: [ + regal, + "email", + ], + }, + name: { + propDefinition: [ + regal, + "name", + ], + }, + customPropertyName1: { + type: "string", + label: "Custom Property Name (1)", + description: "Name of a custom property to add to the event", + optional: true, + }, + customPropertyValue1: { + type: "string", + label: "Custom Property Value (1)", + description: "Value of a custom property to add to the event", + optional: true, + }, + additionalProperties: { + type: "integer", + label: "Additional Properties to Add", + description: "The number of additional properties to add to the event", + optional: true, + reloadProps: true, + }, + }, + additionalProps() { + const props = {}; + if (!this.additionalProperties > 0) { + return props; + } + for (let i = 2; i < this.additionalProperties + 2; i++) { + props[`customPropertyName${i}`] = { + type: "string", + label: `Custom Property Name (${i})`, + description: "Name of a custom property to add to the event", + }; + props[`customPropertyValue${i}`] = { + type: "string", + label: `Custom Property Value (${i})`, + description: "Value of a custom property to add to the event", + }; + } + return props; + }, + async run({ $ }) { + const properties = this.customPropertyName1 + ? { + [this.customPropertyName1]: this.customPropertyValue, + } + : {}; + for (let i = 2; i < this.additionalProperties + 2; i++) { + properties[this[`customPropertyName${i}`]] = this[`customPropertyValue${i}`]; + } + + const response = await this.regal.customEvent({ + $, + data: { + userId: this.userId, + traits: { + phones: this.phone + ? { + [this.phone]: {}, + } + : undefined, + emails: this.email + ? { + [this.email]: {}, + } + : undefined, + }, + name: this.name, + properties, + }, + }); + if (response?.message === "ok") { + $.export("$summary", `Successfully added contact with ID "${this.userId}" to event`); + } + return response; + }, +}; diff --git a/components/regal/actions/create-or-update-contact/create-or-update-contact.mjs b/components/regal/actions/create-or-update-contact/create-or-update-contact.mjs new file mode 100644 index 0000000000000..5363a5eb09965 --- /dev/null +++ b/components/regal/actions/create-or-update-contact/create-or-update-contact.mjs @@ -0,0 +1,137 @@ +import regal from "../../regal.app.mjs"; + +export default { + key: "regal-create-or-update-contact", + name: "Create or Update Contact", + description: "Create or update a contacts. [See the documentation](https://developer.regal.io/reference/api)", + version: "0.0.1", + type: "action", + props: { + regal, + userId: { + propDefinition: [ + regal, + "userId", + ], + }, + phone: { + propDefinition: [ + regal, + "phone", + ], + }, + smsOptIn: { + type: "boolean", + label: "SMS Opt-In", + description: "SMS subscription status. Defaults to `true`", + default: true, + optional: true, + }, + voiceOptIn: { + type: "boolean", + label: "Voice Opt-In", + description: "Voice subscription status. Defaults to `true`", + default: true, + optional: true, + }, + email: { + propDefinition: [ + regal, + "email", + ], + }, + emailOptIn: { + type: "boolean", + label: "Email Opt-In", + description: "Email subscription status. Defaults to `true`", + default: true, + optional: true, + }, + firstName: { + propDefinition: [ + regal, + "firstName", + ], + }, + lastName: { + propDefinition: [ + regal, + "lastName", + ], + }, + streetAddress: { + type: "string", + label: "Street Address", + description: "Street address of the contact", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "City of the contact", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "State/Region of the contact", + optional: true, + }, + zip: { + type: "string", + label: "Zip", + description: "Zip code of the contact", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "Country of the contact. Example: `United States`", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.regal.customEvent({ + $, + data: { + userId: this.userId, + traits: { + firstName: this.firstName, + lastName: this.lastName, + phones: this.phone + ? { + [this.phone]: { + voiceOptIn: { + subscribed: this.voiceOptIn, + }, + smsOptIn: { + subscribed: this.smsOptIn, + }, + }, + } + : undefined, + emails: this.email + ? { + [this.email]: { + emailOptIn: { + subscribed: this.emailOptIn, + }, + }, + } + : undefined, + address: { + street: this.streetAddress, + city: this.city, + state: this.state, + zipcode: this.zip, + country: this.country, + }, + }, + }, + }); + if (response?.message === "ok") { + $.export("$summary", "Successfully created or updated contact"); + } + return response; + }, +}; diff --git a/components/regal/package.json b/components/regal/package.json new file mode 100644 index 0000000000000..706fafa971121 --- /dev/null +++ b/components/regal/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/regal", + "version": "0.2.0", + "description": "Pipedream Regal Components", + "main": "regal.app.mjs", + "keywords": [ + "pipedream", + "regal" + ], + "homepage": "https://pipedream.com/apps/regal", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/regal/regal.app.mjs b/components/regal/regal.app.mjs new file mode 100644 index 0000000000000..edbb21936176b --- /dev/null +++ b/components/regal/regal.app.mjs @@ -0,0 +1,76 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "regal", + propDefinitions: { + userId: { + type: "string", + label: "User ID", + description: "Unique identifier for your contact", + optional: true, + }, + firstName: { + type: "string", + label: "First Name", + description: "First name of the contact", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the contact", + optional: true, + }, + name: { + type: "string", + label: "Event Name", + description: "Name of the event", + optional: true, + }, + eventSource: { + type: "string", + label: "Event Source", + description: "Source of the event", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "Email address of the contact", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Phone number of the contact. Example: `+1 (954) 555-2399`", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://events.regalvoice.com"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `${this.$auth.api_key}`, + }, + }); + }, + customEvent(args = {}) { + return this._makeRequest({ + method: "post", + path: "/events", + ...args, + }); + }, + }, +}; diff --git a/components/regfox/README.md b/components/regfox/README.md index d28f8302f69f4..7c705d4ab95e9 100644 --- a/components/regfox/README.md +++ b/components/regfox/README.md @@ -1,20 +1,11 @@ # Overview -RegFox makes building custom events easy and enables you to create the best -event experience for your attendees. With the RegFox API, you can easily create -custom events that are tailored to your organization and group, no matter the -size. +The RegFox API provides programmatic access to event registration functionalities, allowing for the creation of custom registration experiences, data retrieval, and the automation of event management tasks. With Pipedream, you can harness this API to automate repetitive tasks, sync registration data with other services, and trigger actions based on event activities. -The RegFox API offers a wide range of features to build tailored experiences -for your attendees. From ticketing, registration and payments, to surveys and -badges, to post-event analytics, RegFox enables you to manage the entire event -lifecycle through its open API. Here are a few examples of what you can build -using the RegFox API: +# Example Use Cases -- Create custom ticketing and registration forms -- Sell tickets online and process payments -- Create surveys and collect feedback -- Create badges with custom branding -- Generate post-event analytics reports -- Manage the attendee list and track check-ins -- Integrate with popular event management platforms +- **Automated Attendee Follow-Up**: After a participant registers for an event via RegFox, automate the process of sending personalized follow-up emails or SMS messages using integrations with email platforms like SendGrid or messaging services like Twilio. This can ensure attendees receive all necessary information and feel engaged from the outset. + +- **Dynamic Registration Data Sync**: Keep a real-time sync between RegFox registration data and other platforms like Salesforce or Google Sheets. Whenever a new registration occurs, Pipedream can trigger a workflow that updates or creates a new contact in your CRM, or appends the registration details to a Google Sheet, enabling you to maintain up-to-date records across systems. + +- **Event Feedback Collection**: After an event concludes, use Pipedream to trigger a workflow that sends out a feedback survey via tools like Typeform or SurveyMonkey. By integrating with RegFox, you can ensure that the survey is only sent to attendees, and follow-up actions can be automated based on the responses received, such as tagging the attendee record with feedback for future reference. diff --git a/components/reipro/README.md b/components/reipro/README.md index f714dee91dcac..42fd6545a685c 100644 --- a/components/reipro/README.md +++ b/components/reipro/README.md @@ -1,29 +1,11 @@ # Overview -Creating more efficient real estate investing tools with the REIPro API +The REIPro API provides a suite of tools designed for real estate investors and professionals, enabling access to a wealth of property data, lead management, and marketing campaign functionalities. Leveraging the REIPro API within Pipedream, users can automate multifaceted real estate tasks, from syncing property information to various platforms, managing leads effectively, to executing timely follow-ups and nurturing campaigns. The API's capabilities can be harnessed to streamline operations, improve data accuracy, and connect various stages of the real estate investment lifecycle for enhanced decision-making and efficiency. -The REIPro API is a tool specifically designed to help real estate investors -streamline their business operations. With REIPro, investors can access various -important features to help their day-to-day activities such as marketing, lead -management, investor accounts, and transaction tracking. +# Example Use Cases -Using the REIPro API, you can create a range of helpful applications for real -estate investments. Here are just some of the things you can build to benefit -investors: +- **Lead Enrichment and Follow-up Automation**: Automatically retrieve property details from REIPro when a new lead is captured in a CRM like Salesforce. Use this data to enrich lead profiles and trigger personalized follow-up emails or SMS messages through integrations with SendGrid or Twilio, ensuring timely engagement with potential clients. -- Automated lead tracking and lead scoring application that allows you to use - criteria such as budget and location to prioritize leads and target the best - prospects. -- Automated marketing campaigns using A/B tests for creating the most effective - marketing strategies for different target audiences. -- Automated application that creates investor accounts and seamlessly - integrates with existing financial and banking software, allowing you to - manage multiple accounts easily. -- Automatically updated transactions application that tracks changes and - updates them in real-time, keeping all data up to date. -- Property valuation and assessment application that uses REIPro's data to - instantly generate realistic and accurate estimates of potential property - prices. +- **Marketing Campaign Analysis**: Integrate REIPro with marketing platforms like Facebook Ads or Google Analytics to track the performance of real estate listings. Create workflows that pull campaign data into REIPro to measure ROI, adjust marketing strategies, and identify the most effective channels for different property types. -With REIPro's API, you can quickly and easily build a range of useful -applications that will make investing smoother and more efficient. +- **Investment Opportunity Alerts**: Construct a workflow where REIPro feeds property data into a decision-making model hosted on Pipedream. When investment criteria are met—such as location, price range, or ROI potential—automatic notifications are sent via Slack or email, allowing investors to act quickly on potential deals. diff --git a/components/reipro/package.json b/components/reipro/package.json new file mode 100644 index 0000000000000..98affac718c49 --- /dev/null +++ b/components/reipro/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/reipro", + "version": "0.6.0", + "description": "Pipedream reipro Components", + "main": "reipro.app.mjs", + "keywords": [ + "pipedream", + "reipro" + ], + "homepage": "https://pipedream.com/apps/reipro", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/reishost/README.md b/components/reishost/README.md new file mode 100644 index 0000000000000..fe829ef92fd40 --- /dev/null +++ b/components/reishost/README.md @@ -0,0 +1,11 @@ +# Overview + +ReisHost API provides integration options for managing hosting services, including server management, domain handling, and billing processes. It's designed for web hosting companies that need to automate and streamline their operations. By integrating the ReisHost API with Pipedream, users can create customized workflows that automate routine tasks, synchronize data across multiple platforms, and react to specific triggers from the ReisHost system, such as new user registrations or payment confirmations. + +# Example Use Cases + +- **Automate Server Provisioning**: When a new customer signs up through a form on your website (e.g., Google Forms), automatically create a server space in ReisHost. You can also send a welcome email via SendGrid, including details about their server and how to access support. + +- **Synchronize Billing Information**: Whenever a payment is received and processed in ReisHost, use Pipedream to update the customer's billing status in a connected CRM system like Salesforce. This ensures all team members have the latest information and can follow up on customer accounts accurately. + +- **Domain Registration Alerts**: Set up a workflow where, upon a new domain registration via ReisHost, a notification is sent through Slack to your support team. This allows them to immediately perform any necessary follow-ups or verifications, improving response times and customer service. diff --git a/components/reishost/actions/send-console-command/send-console-command.mjs b/components/reishost/actions/send-console-command/send-console-command.mjs new file mode 100644 index 0000000000000..9eeef92e15dad --- /dev/null +++ b/components/reishost/actions/send-console-command/send-console-command.mjs @@ -0,0 +1,33 @@ +import reishost from "../../reishost.app.mjs"; + +export default { + key: "reishost-send-console-command", + name: "Send Console Command", + description: "Runs a command on your server's console, providing direct command execution flexibility.", + version: "0.0.1", + type: "action", + props: { + reishost, + serverId: { + type: "string", + label: "Server Id", + description: "The server's identifier.", + }, + command: { + type: "string", + label: "Command", + description: "The command you want to execute on the server.", + }, + }, + async run({ $ }) { + const response = await this.reishost.sendCommand({ + serverId: this.serverId, + data: { + command: this.command, + }, + }); + + $.export("$summary", "Command successfully executed!"); + return response; + }, +}; diff --git a/components/reishost/package.json b/components/reishost/package.json new file mode 100644 index 0000000000000..f4df88cad6335 --- /dev/null +++ b/components/reishost/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/reishost", + "version": "0.1.0", + "description": "Pipedream ReisHost Components", + "main": "reishost.app.mjs", + "keywords": [ + "pipedream", + "reishost" + ], + "homepage": "https://pipedream.com/apps/reishost", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} + diff --git a/components/reishost/reishost.app.mjs b/components/reishost/reishost.app.mjs new file mode 100644 index 0000000000000..2423924f3c306 --- /dev/null +++ b/components/reishost/reishost.app.mjs @@ -0,0 +1,41 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "reishost", + propDefinitions: { + command: { + type: "string", + label: "Command", + description: "The command you want to execute on the server.", + }, + }, + methods: { + _baseUrl() { + return `${this.$auth.endpoint_structure}/api`; + }, + _headers() { + return { + "Authorization": `Bearer ${this.$auth.api_key}`, + }; + }, + async _makeRequest({ + $ = this, path = "/", ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + sendCommand({ + serverId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/client/servers/${serverId}/command`, + ...opts, + }); + }, + }, +}; diff --git a/components/rejoiner/actions/add-customer-to-list/add-customer-to-list.mjs b/components/rejoiner/actions/add-customer-to-list/add-customer-to-list.mjs new file mode 100644 index 0000000000000..af3f2989ff67f --- /dev/null +++ b/components/rejoiner/actions/add-customer-to-list/add-customer-to-list.mjs @@ -0,0 +1,112 @@ +import rejoiner from "../../rejoiner.app.mjs"; + +export default { + key: "rejoiner-add-customer-to-list", + name: "Add Customer to List", + description: "Adds a customer to a specific list, or if the customer already exists, will update the record of that customer with the supplied data. [See the documentation](https://docs.rejoiner.com/reference/add-customer-to-list)", + version: "0.0.1", + type: "action", + props: { + rejoiner, + listId: { + propDefinition: [ + rejoiner, + "listId", + ], + }, + email: { + propDefinition: [ + rejoiner, + "email", + ], + }, + firstName: { + propDefinition: [ + rejoiner, + "firstName", + ], + }, + lastName: { + propDefinition: [ + rejoiner, + "lastName", + ], + }, + phone: { + propDefinition: [ + rejoiner, + "phone", + ], + }, + timezone: { + propDefinition: [ + rejoiner, + "timezone", + ], + }, + language: { + propDefinition: [ + rejoiner, + "language", + ], + }, + address1: { + propDefinition: [ + rejoiner, + "address1", + ], + }, + address2: { + propDefinition: [ + rejoiner, + "address2", + ], + }, + city: { + propDefinition: [ + rejoiner, + "city", + ], + }, + state: { + propDefinition: [ + rejoiner, + "state", + ], + }, + postalCode: { + propDefinition: [ + rejoiner, + "postalCode", + ], + }, + country: { + propDefinition: [ + rejoiner, + "country", + ], + }, + }, + async run({ $ }) { + const response = await this.rejoiner.addCustomerToList({ + $, + listId: this.listId, + data: { + email: this.email, + first_name: this.firstName, + last_name: this.lastName, + phone: this.phone, + timezone: this.timezone, + language: this.language, + address1: this.address1, + address2: this.address2, + city: this.city, + state: this.state, + postal_code: this.postalCode, + country: this.country, + }, + }); + $.export("$summary", `Added customer ${this.email} to list ${this.listId}`); + return response; + }, +}; diff --git a/components/rejoiner/actions/start-journey/start-journey.mjs b/components/rejoiner/actions/start-journey/start-journey.mjs new file mode 100644 index 0000000000000..e193ff53ac00e --- /dev/null +++ b/components/rejoiner/actions/start-journey/start-journey.mjs @@ -0,0 +1,46 @@ +import rejoiner from "../../rejoiner.app.mjs"; + +export default { + key: "rejoiner-start-journey", + name: "Start Journey", + description: "Triggers the beginning of a customer journey in Rejoiner. [See the documentation](https://docs.rejoiner.com/reference/trigger-webhook-journey)", + version: "0.0.1", + type: "action", + props: { + rejoiner, + webhookUrl: { + type: "string", + label: "Webhook Endpoint URL", + description: "Webhook URL of the journey. A webhook-triggered journey will provide an explicit Webhook Endpoint URL to be used for triggering the journey", + }, + email: { + propDefinition: [ + rejoiner, + "email", + ], + }, + metadata: { + type: "object", + label: "Metadata", + description: "Metadata to be attached to the customer's journey session metadata", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.rejoiner._makeRequest({ + $, + method: "POST", + url: this.webhookUrl, + data: { + email: this.email, + session_data: this.metadata + ? typeof this.metadata === "string" + ? JSON.parse(this.metadata) + : this.metadata + : undefined, + }, + }); + $.export("$summary", `Triggered journey for customer ${this.email}`); + return response; + }, +}; diff --git a/components/rejoiner/actions/update-customer-profile/update-customer-profile.mjs b/components/rejoiner/actions/update-customer-profile/update-customer-profile.mjs new file mode 100644 index 0000000000000..e1101b0834b75 --- /dev/null +++ b/components/rejoiner/actions/update-customer-profile/update-customer-profile.mjs @@ -0,0 +1,107 @@ +import rejoiner from "../../rejoiner.app.mjs"; + +export default { + key: "rejoiner-update-customer-profile", + name: "Update Customer Profile", + description: "Updates a customer's profile information. [See the documentation](https://docs.rejoiner.com/reference/update-customer-profile)", + version: "0.0.1", + type: "action", + props: { + rejoiner, + email: { + propDefinition: [ + rejoiner, + "email", + ], + }, + firstName: { + propDefinition: [ + rejoiner, + "firstName", + ], + }, + lastName: { + propDefinition: [ + rejoiner, + "lastName", + ], + }, + phone: { + propDefinition: [ + rejoiner, + "phone", + ], + }, + timezone: { + propDefinition: [ + rejoiner, + "timezone", + ], + }, + language: { + propDefinition: [ + rejoiner, + "language", + ], + }, + address1: { + propDefinition: [ + rejoiner, + "address1", + ], + }, + address2: { + propDefinition: [ + rejoiner, + "address2", + ], + }, + city: { + propDefinition: [ + rejoiner, + "city", + ], + }, + state: { + propDefinition: [ + rejoiner, + "state", + ], + }, + postalCode: { + propDefinition: [ + rejoiner, + "postalCode", + ], + }, + country: { + propDefinition: [ + rejoiner, + "country", + ], + }, + }, + async run({ $ }) { + const response = await this.rejoiner.updateCustomerProfile({ + $, + params: { + email: this.email, + }, + data: { + first_name: this.firstName, + last_name: this.lastName, + phone: this.phone, + timezone: this.timezone, + language: this.language, + address1: this.address1, + address2: this.address2, + city: this.city, + state: this.state, + postal_code: this.postalCode, + country: this.country, + }, + }); + $.export("$summary", `Updated customer profile for customer ${this.email}`); + return response; + }, +}; diff --git a/components/rejoiner/package.json b/components/rejoiner/package.json new file mode 100644 index 0000000000000..7a44d0c90e943 --- /dev/null +++ b/components/rejoiner/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/rejoiner", + "version": "0.1.0", + "description": "Pipedream Rejoiner Components", + "main": "rejoiner.app.mjs", + "keywords": [ + "pipedream", + "rejoiner" + ], + "homepage": "https://pipedream.com/apps/rejoiner", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/rejoiner/rejoiner.app.mjs b/components/rejoiner/rejoiner.app.mjs new file mode 100644 index 0000000000000..0a98dd63ff46c --- /dev/null +++ b/components/rejoiner/rejoiner.app.mjs @@ -0,0 +1,172 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "rejoiner", + propDefinitions: { + listId: { + type: "string", + label: "List ID", + description: "Unique identifier for the list", + async options() { + const lists = await this.listLists(); + return lists?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + email: { + type: "string", + label: "Email", + description: "The email address of the customer", + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the customer", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the customer", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "A phone number for the customer", + optional: true, + }, + timezone: { + type: "string", + label: "Timezone", + description: "Timezone of customer, should be in list of [TZ database names](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)", + optional: true, + }, + language: { + type: "string", + label: "Language", + description: "Two letter [code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) of customers language", + optional: true, + }, + address1: { + type: "string", + label: "Address Line 1", + description: "First line of address for customer", + optional: true, + }, + address2: { + type: "string", + label: "Address Line 2", + description: "Second line of address for customer", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "City where customer lives", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "State where customer lives", + optional: true, + }, + postalCode: { + type: "string", + label: "Postal Code", + description: "Postal code of customer's address", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "Country where customer lives", + optional: true, + }, + }, + methods: { + _baseUrl() { + return `https://rj2.rejoiner.com/api/v2/${this.$auth.site_id}`; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `Rejoiner ${this.$auth.api_key}`, + "Content-Type": "application/json", + }, + ...otherOpts, + }); + }, + listLists(opts = {}) { + return this._makeRequest({ + path: "/lists", + ...opts, + }); + }, + listListContacts({ + listId, ...opts + }) { + return this._makeRequest({ + path: `/lists/${listId}/contacts/`, + ...opts, + }); + }, + addCustomerToList({ + listId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/lists/${listId}/contacts/`, + ...opts, + }); + }, + updateCustomerProfile(opts = {}) { + return this._makeRequest({ + method: "PUT", + path: "/customers/by_email/", + ...opts, + }); + }, + async *paginate({ + fn, + args, + max, + }) { + args = { + ...args, + params: { + ...args?.params, + page: 1, + }, + }; + let total, itemCount = 0; + + do { + const { + results, count, + } = await fn(args); + for (const item of results) { + yield item; + itemCount++; + if (max && itemCount >= max) { + return; + } + } + total = count; + args.params.page++; + } while (itemCount < total); + }, + }, +}; diff --git a/components/rejoiner/sources/new-contact-in-list/new-contact-in-list.mjs b/components/rejoiner/sources/new-contact-in-list/new-contact-in-list.mjs new file mode 100644 index 0000000000000..5d49aa57d5078 --- /dev/null +++ b/components/rejoiner/sources/new-contact-in-list/new-contact-in-list.mjs @@ -0,0 +1,49 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import rejoiner from "../../rejoiner.app.mjs"; + +export default { + key: "rejoiner-new-contact-in-list", + name: "New Contact in List", + description: "Emit new event when a contact is added to the specified list. [See the documentation](https://docs.rejoiner.com/reference/retrieve-list-contacts).", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + rejoiner, + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + listId: { + propDefinition: [ + rejoiner, + "listId", + ], + }, + }, + methods: { + generateMeta(contact) { + return { + id: contact.id, + summary: contact.email, + ts: Date.now(), + }; + }, + }, + async run() { + const results = this.rejoiner.paginate({ + fn: this.rejoiner.listListContacts, + args: { + listId: this.listId, + }, + }); + + for await (const item of results) { + const contact = item.customer; + const meta = this.generateMeta(contact); + this.$emit(item, meta); + } + }, +}; diff --git a/components/relavate/README.md b/components/relavate/README.md new file mode 100644 index 0000000000000..b3a0422456c4f --- /dev/null +++ b/components/relavate/README.md @@ -0,0 +1,14 @@ +# Overview + +The Relavate API provides tools for managing customer relations and sales efforts, enabling users to automate and optimize their CRM activities. With capabilities for managing contacts, companies, deals, and tasks, the API can serve as a backbone for CRM automation, enhancing customer interaction and business processes efficiency. On Pipedream, you can leverage these functionalities to create advanced workflows, integrate with other apps, and automate tasks based on events from the Relavate system or external triggers. + +# Example Use Cases + +- **Automated Lead Capture and Follow-Up**: + Automatically add new leads captured from various sources like web forms (using Typeform or Google Forms) directly into Relavate as contacts. Set up subsequent automated emails (using SendGrid or Mailgun) to these contacts to ensure timely follow-up, enhancing lead engagement without manual intervention. + +- **Deal Progress Notification**: + Monitor changes in deal stages within Relavate and automatically send notifications via Slack or email whenever a deal progresses or closes. This keeps teams informed in real-time, improving coordination and response times in dynamic sales environments. + +- **Task Synchronization Across Platforms**: + Sync tasks and reminders from Relavate with other project management tools like Asana or Trello. Whenever a task is updated or a new task is created in Relavate, reflect these changes in the chosen project management tool, ensuring consistency and visibility across different platforms and teams. diff --git a/components/relavate/actions/create-affiliate-lead/create-affiliate-lead.mjs b/components/relavate/actions/create-affiliate-lead/create-affiliate-lead.mjs new file mode 100644 index 0000000000000..b2cf19aab6b6e --- /dev/null +++ b/components/relavate/actions/create-affiliate-lead/create-affiliate-lead.mjs @@ -0,0 +1,100 @@ +import relavate from "../../relavate.app.mjs"; + +export default { + key: "relavate-create-affiliate-lead", + name: "Create Affiliate Lead", + description: "This component enables you to create a new affiliate lead for marketing. [See the documentation](https://api.relavate.co/#2a307851-9d54-42ea-9f54-3fb600b152a5)", + version: "0.0.1", + type: "action", + props: { + relavate, + campaignId: { + propDefinition: [ + relavate, + "campaignId", + ], + reloadProps: true, + }, + status: { + type: "string", + label: "Status", + description: "The status of the lead being created", + options: [ + "Pending", + "Won", + ], + }, + client: { + type: "string", + label: "Client", + description: "The company or name of the client", + }, + }, + async additionalProps() { + const props = {}; + const partnerType = this.relavate.getPartnerType(); + if (partnerType === "partner") { + props.vendorId = { + type: "string", + label: "Vendor ID", + description: "The ID of the vendor", + options: async () => await this.getVendorIdPropOptions(), + }; + } + if (partnerType === "vendor") { + props.partnerId = { + type: "string", + label: "Partner ID", + description: "The ID of the partner", + options: async () => await this.getPartnerIdPropOptions(), + }; + } + return props; + }, + methods: { + async getVendorIdPropOptions() { + const vendors = await this.relavate.listVendors(); + const vendorIds = vendors?.map(({ vendorID }) => vendorID ); + const options = []; + for (const vendorId of vendorIds) { + const { name } = await this.relavate.getVendor({ + vendorId, + }); + options.push({ + value: vendorId, + label: name, + }); + } + return options; + }, + async getPartnerIdPropOptions() { + const partners = await this.relavate.listPartners(); + const partnerIds = partners?.map(({ partnerID }) => partnerID ); + const options = []; + for (const partnerId of partnerIds) { + const { name } = await this.relavate.getPartner({ + partnerId, + }); + options.push({ + value: partnerId, + label: name, + }); + } + return options; + }, + }, + async run({ $ }) { + const response = await this.relavate.createAffiliateLead({ + $, + params: { + campaignID: this.campaignId, + client: this.client, + status: this.status, + vendorID: this.vendorId, + partnerID: this.partnerId, + }, + }); + $.export("$summary", `Created affiliate lead with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/relavate/package.json b/components/relavate/package.json new file mode 100644 index 0000000000000..c3b9d59277f2b --- /dev/null +++ b/components/relavate/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/relavate", + "version": "0.1.0", + "description": "Pipedream Relavate Components", + "main": "relavate.app.mjs", + "keywords": [ + "pipedream", + "relavate" + ], + "homepage": "https://pipedream.com/apps/relavate", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} diff --git a/components/relavate/relavate.app.mjs b/components/relavate/relavate.app.mjs new file mode 100644 index 0000000000000..3b2d691a52c37 --- /dev/null +++ b/components/relavate/relavate.app.mjs @@ -0,0 +1,99 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "relavate", + propDefinitions: { + campaignId: { + type: "string", + label: "Campaign ID", + description: "The ID of the campaign", + async options() { + const campaigns = await this.listCampaigns(); + return campaigns?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://app.relavate.co/api"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "X-key": this.$auth.api_key, + "X-secret": this.$auth.api_secret, + "X-partnerType": this.$auth.partner_type, + }, + }); + }, + getPartnerType() { + return this.$auth.partner_type; + }, + getVendor({ + vendorId, ...opts + }) { + return this._makeRequest({ + path: `/vendors/${vendorId}`, + ...opts, + }); + }, + getPartner({ + partnerId, ...opts + }) { + return this._makeRequest({ + path: `/partners/${partnerId}`, + ...opts, + }); + }, + listVendors(opts = {}) { + return this._makeRequest({ + path: "/partners/vendors/all", + ...opts, + }); + }, + listPartners(opts = {}) { + return this._makeRequest({ + path: "/vendors/partners/all", + ...opts, + }); + }, + listCampaigns(opts = {}) { + return this._makeRequest({ + path: "/affiliate-campaigns", + ...opts, + }); + }, + listAffiliateLeads(opts = {}) { + return this._makeRequest({ + path: "/affiliate-leads", + ...opts, + }); + }, + listReferrals(opts = {}) { + return this._makeRequest({ + path: "/referrals", + ...opts, + }); + }, + createAffiliateLead(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/affiliate-leads", + ...opts, + }); + }, + }, +}; diff --git a/components/relavate/sources/common/base.mjs b/components/relavate/sources/common/base.mjs new file mode 100644 index 0000000000000..ecdedc29dfc1a --- /dev/null +++ b/components/relavate/sources/common/base.mjs @@ -0,0 +1,57 @@ +import relavate from "../../relavate.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + relavate, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getStartDate() { + return this.db.get("startDate") || this._today(); + }, + _setStartDate(startDate) { + this.db.set("startDate", startDate); + }, + _today() { + return new Date().toISOString() + .split("T")[0]; + }, + generateMeta(item) { + return { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item.created_at), + }; + }, + getParams() { + return {}; + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + }, + async run() { + const resourceFn = this.getResourceFn(); + const params = this.getParams(); + const results = await resourceFn({ + params, + }); + if (!results?.length) { + return; + } + for (const item of results) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + } + }, +}; diff --git a/components/relavate/sources/new-affiliate-campaign/new-affiliate-campaign.mjs b/components/relavate/sources/new-affiliate-campaign/new-affiliate-campaign.mjs new file mode 100644 index 0000000000000..9b8ba0c3e4891 --- /dev/null +++ b/components/relavate/sources/new-affiliate-campaign/new-affiliate-campaign.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + type: "source", + key: "relavate-new-affiliate-campaign", + name: "New Affiliate Campaign", + description: "Emit new event when a new affiliate campaign is created.", + version: "0.0.1", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.relavate.listCampaigns; + }, + getSummary(campaign) { + return `New campaign: ${campaign.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/relavate/sources/new-affiliate-campaign/test-event.mjs b/components/relavate/sources/new-affiliate-campaign/test-event.mjs new file mode 100644 index 0000000000000..ed0d08473883d --- /dev/null +++ b/components/relavate/sources/new-affiliate-campaign/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "id": 4, + "partnerID": 2, + "vendorID": 4, + "name": "Tracking link campaign", + "type": "Link", + "created_at": "2023-10-17T10:23:37.000000Z", + "updated_at": "2023-10-17T10:23:37.000000Z", + "status": "Active", + "campaignBaseURL": "https://nichols.com", + "campaignUrlPath": null, + "linkFormat": "vendorWebsite", + "couponCode": null, + "subtitle": null +} \ No newline at end of file diff --git a/components/relavate/sources/new-referral/new-referral.mjs b/components/relavate/sources/new-referral/new-referral.mjs new file mode 100644 index 0000000000000..2e29ae0207d99 --- /dev/null +++ b/components/relavate/sources/new-referral/new-referral.mjs @@ -0,0 +1,39 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "relavate-new-referral", + name: "New Referral", + description: "Emit new event when a new referral is created in Relavate", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + _getStartDate() { + return this.db.get("startDate") || this._today(); + }, + _setStartDate(startDate) { + this.db.set("startDate", startDate); + }, + _today() { + return new Date().toISOString() + .split("T")[0]; + }, + getResourceFn() { + return this.relavate.listReferrals; + }, + getParams() { + const startDate = this._getStartDate(); + this._setStartDate(this._today()); + return { + startDate, + }; + }, + getSummary(referral) { + return `New referral with ID ${referral.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/relavate/sources/new-referral/test-event.mjs b/components/relavate/sources/new-referral/test-event.mjs new file mode 100644 index 0000000000000..9a2b1a30030c5 --- /dev/null +++ b/components/relavate/sources/new-referral/test-event.mjs @@ -0,0 +1,19 @@ +export default { + "id": 4, + "vendorID": 4, + "partnerID": 2, + "company": "Sample Company", + "status": "New", + "revenue": 5000, + "frequency": "Annual", + "dateAdded": "2023-05-20", + "comment": "{\"ops\":[{\"insert\":\"This is a sample comment\"}]}", + "clientContactID": 7, + "created_at": "2023-10-17T09:52:11.000000Z", + "updated_at": "2023-10-17T09:54:28.000000Z", + "wonDate": null, + "lostDate": "2023-10-17", + "annualRevenue": 5000, + "lostID": 10, + "comment_text": "This is a sample comment" +} \ No newline at end of file diff --git a/components/relevance_ai/README.md b/components/relevance_ai/README.md new file mode 100644 index 0000000000000..53164fe417c2b --- /dev/null +++ b/components/relevance_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +Relevance AI is a powerful tool for handling complex data operations like clustering, vector search, and data visualization. On Pipedream, you can use the Relevance AI API to automate data enrichment, analysis, and integration tasks. This integration opens up possibilities for dynamic data workflows, enabling real-time data processing and insights generation across various platforms. + +# Example Use Cases + +- **Customer Feedback Analysis**: Automate the processing of customer feedback by connecting Relevance AI with a CRM like Salesforce. Extract key themes and sentiments from feedback using Relevance AI's natural language processing capabilities, then sync these insights back to Salesforce to help guide product improvements and customer service strategies. + +- **Real-Time Market Trend Monitoring**: Set up a workflow where Relevance AI analyzes social media data streamed through the Twitter API. Use vector search to detect and cluster emerging trends, then push notifications via Slack or email when new trends are detected, enabling businesses to react quickly to market changes. + +- **E-commerce Product Recommendations**: Enhance an e-commerce platform by integrating Relevance AI with Shopify. Analyze customer purchase data and browsing behavior to cluster similar user profiles and product categories. Use these insights to automate personalized product recommendations, improving customer experience and increasing sales. diff --git a/components/relevance_ai/actions/message-agent/message-agent.mjs b/components/relevance_ai/actions/message-agent/message-agent.mjs new file mode 100644 index 0000000000000..686e98dd58ec6 --- /dev/null +++ b/components/relevance_ai/actions/message-agent/message-agent.mjs @@ -0,0 +1,48 @@ +import relevanceAi from "../../relevance_ai.app.mjs"; + +export default { + key: "relevance_ai-message-agent", + name: "Send Message to Agent", + description: "Sends a message directly to an agent in Relevance AI. This action doesn't wait for an agent response.", + version: "0.0.1", + type: "action", + props: { + relevanceAi, + agentId: { + type: "string", + label: "Agent ID", + description: "The ID of the agent to send the message to.", + }, + conversationId: { + propDefinition: [ + relevanceAi, + "conversationId", + ({ agentId }) => ({ + agentId, + }), + ], + }, + message: { + type: "string", + label: "Message", + description: "The message to send to the agent.", + }, + }, + async run({ $ }) { + const response = await this.relevanceAi.sendMessage({ + $, + data: { + message: { + role: "user", + content: this.message, + }, + debug: false, + agent_id: this.agentId, + conversation_id: this.conversationId, + }, + }); + + $.export("$summary", `Message sent to agent ID ${this.agentId}`); + return response; + }, +}; diff --git a/components/relevance_ai/actions/run-tool/run-tool.mjs b/components/relevance_ai/actions/run-tool/run-tool.mjs new file mode 100644 index 0000000000000..3b27c5e5fc9db --- /dev/null +++ b/components/relevance_ai/actions/run-tool/run-tool.mjs @@ -0,0 +1,63 @@ +import { sleep } from "../../common/utils.mjs"; +import relevanceAI from "../../relevance_ai.app.mjs"; + +export default { + key: "relevance_ai-run-tool", + name: "Run Tool", + description: "Executes a specific tool within Relevance AI and waits for a response for up to 60 seconds. [See the documentation](https://relevanceai.com/docs/build-custom-tools/create-a-tool)", + version: "0.0.1", + type: "action", + props: { + relevanceAI, + toolId: { + propDefinition: [ + relevanceAI, + "toolId", + ], + }, + parameters: { + type: "object", + label: "Parameters", + description: "The parameters for the tool execution.", + }, + timeout: { + type: "integer", + label: "Timeout", + description: "The time to wait for a response from the tool execution, in seconds.", + default: 60, + max: 60, + min: 1, + }, + }, + async run({ $ }) { + const response = await this.relevanceAI.executeTool({ + $, + toolId: this.toolId, + data: { + params: this.parameters, + }, + }); + + const jobId = response.job_id; + let toolOutput = {}; + let cont = true; + let timeLimit = 0; + + do { + timeLimit += 5; + await sleep(5000); + const jobStatus = await this.relevanceAI.getJobStatus({ + $, + jobId, + toolId: this.toolId, + }); + if (jobStatus.type === "complete") { + toolOutput = jobStatus.updates[0].output.output; + cont = false; + } + } while (cont && (timeLimit <= this.timeout)); + + $.export("$summary", `Successfully executed tool with ID ${this.toolId}`); + return toolOutput; + }, +}; diff --git a/components/relevance_ai/common/constants.mjs b/components/relevance_ai/common/constants.mjs new file mode 100644 index 0000000000000..e67b995b66aa4 --- /dev/null +++ b/components/relevance_ai/common/constants.mjs @@ -0,0 +1 @@ +export const LIMIT = 1000; diff --git a/components/relevance_ai/common/utils.mjs b/components/relevance_ai/common/utils.mjs new file mode 100644 index 0000000000000..2eb594bdd0804 --- /dev/null +++ b/components/relevance_ai/common/utils.mjs @@ -0,0 +1 @@ +export const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); diff --git a/components/relevance_ai/package.json b/components/relevance_ai/package.json new file mode 100644 index 0000000000000..235525664fae3 --- /dev/null +++ b/components/relevance_ai/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/relevance_ai", + "version": "0.1.0", + "description": "Pipedream Relevance AI Components", + "main": "relevance_ai.app.mjs", + "keywords": [ + "pipedream", + "relevance_ai" + ], + "homepage": "https://pipedream.com/apps/relevance_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" + } +} + diff --git a/components/relevance_ai/relevance_ai.app.mjs b/components/relevance_ai/relevance_ai.app.mjs new file mode 100644 index 0000000000000..17b259e523c09 --- /dev/null +++ b/components/relevance_ai/relevance_ai.app.mjs @@ -0,0 +1,130 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "relevance_ai", + propDefinitions: { + conversationId: { + type: "string", + label: "Conversation Id", + description: "The ID of the covnersation.", + async options({ + page, agentId, + }) { + const { results } = await this.listConversations({ + data: { + filters: [ + { + filter_type: "exact_match", + field: "type", + condition_value: "conversation", + }, + { + filter_type: "exact_match", + field: "conversation.agent_id", + condition_value: agentId, + }, + ], + include_hidden: true, + page: page + 1, + page_size: LIMIT, + }, + }); + + return results.map(({ + knowledge_set: value, metadata, + }) => ({ + label: metadata?.conversation?.title || value, + value, + })); + }, + }, + toolId: { + type: "string", + label: "Tool ID", + description: "The ID of the tool to execute.", + async options({ page }) { + const { results } = await this.listTools({ + params: { + filters: JSON.stringify([ + { + field: "project", + condition: "==", + condition_value: this.$auth.project, + filter_type: "exact_match", + }, + ]), + page: page + 1, + page_size: LIMIT, + }, + }); + + return results.map(({ + studio_id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return `https://api-${this.$auth.region}.stack.tryrelevance.com/latest`; + }, + _headers() { + return { + "Authorization": `${this.$auth.project}:${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: this._headers(), + }); + }, + listConversations(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/knowledge/sets/list", + ...opts, + }); + }, + listTools(opts = {}) { + return this._makeRequest({ + path: "/studios/list", + ...opts, + }); + }, + getJobStatus({ + toolId, jobId, + }) { + return this._makeRequest({ + path: `/studios/${toolId}/async_poll/${jobId}`, + params: { + ending_update_only: true, + }, + }); + }, + sendMessage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/agents/trigger", + ...opts, + }); + }, + executeTool({ + toolId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/studios/${toolId}/trigger_async`, + ...opts, + }); + }, + }, +}; diff --git a/components/relink_url_shortener/README.md b/components/relink_url_shortener/README.md index 194d41fbfa0da..2f121bbe01968 100644 --- a/components/relink_url_shortener/README.md +++ b/components/relink_url_shortener/README.md @@ -1,27 +1,11 @@ # Overview -Rel.ink is an incredibly useful and powerful URL Shortener API. With Rel.ink, -you can streamline URLs, improve web performance, and increase user engagement. -This makes Rel.ink a great choice for developers looking to craft unique, -feature-rich web apps. +Relink - URL Shortener API offers a straightforward way to shorten URLs, track clicks, and analyze the performance of your links. It's handy for crafting more manageable links for social media, improving email marketing click-through rates, or keeping tabs on how your audience engages with your content. Using Pipedream, you can seamlessly integrate Relink with other services, creating automated workflows that save time and provide valuable insights. -The possibilities are practically endless. Here are a few examples of -applications you can create with Rel.ink's URL Shortener API: +# Example Use Cases -- Social Media Links: Create a shorten URL for Twitter, LinkedIn, and other - social media accounts, to make them easier to share. -- QR Codes: Generate durable, shareable QR codes for your business cards, - website URLs, or any type of data transfer. -- Affiliate Links: Track the performance of your affiliate links using - Rel.ink's analytics and attribution. -- Video Links: Use Rel.ink's video preview feature to display rich visuals for - video URLs. -- Form Links: Use Rel.ink's form links feature to customize the user experience - for web forms. -- Custom Links: Create a unique, branded link for users to quickly access your - website or application. +- **Social Media Post Scheduler with Shortened URLs**: Integrate Relink with a social media management tool like Buffer or Hootsuite on Pipedream. Automate the process of shortening URLs for new blog posts and schedule them to be shared across your social media profiles, while also tracking engagement through click data. -Rel.ink offers an advanced URL Shortener API that you can use to implement a -variety of features and applications—from social media links to custom links, -QR codes to video previews. With Rel.ink, you can craft custom web apps that -streamline and optimize the user experience. +- **Email Marketing Enhancement**: Combine Relink with an email marketing platform like Mailchimp on Pipedream. Automatically shorten URLs included in your email campaigns, allowing for cleaner aesthetics and deeper insight into which links are driving the most traffic, leading to more effective campaigns. + +- **Content Performance Dashboard**: Use Pipedream to connect Relink with a dashboard tool like Google Data Studio. Shorten links for content across platforms, then collect and visualize click data in a centralized dashboard to analyze which content performs best and where your audience is most engaged. diff --git a/components/relink_url_shortener/package.json b/components/relink_url_shortener/package.json new file mode 100644 index 0000000000000..4fae0b5c31ca9 --- /dev/null +++ b/components/relink_url_shortener/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/relink_url_shortener", + "version": "0.6.0", + "description": "Pipedream relink_url_shortener Components", + "main": "relink_url_shortener.app.mjs", + "keywords": [ + "pipedream", + "relink_url_shortener" + ], + "homepage": "https://pipedream.com/apps/relink_url_shortener", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/relintex_crm/package.json b/components/relintex_crm/package.json new file mode 100644 index 0000000000000..adb0ac416d047 --- /dev/null +++ b/components/relintex_crm/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/relintex_crm", + "version": "0.0.1", + "description": "Pipedream Relintex CRM Components", + "main": "relintex_crm.app.mjs", + "keywords": [ + "pipedream", + "relintex_crm" + ], + "homepage": "https://pipedream.com/apps/relintex_crm", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/relintex_crm/relintex_crm.app.mjs b/components/relintex_crm/relintex_crm.app.mjs new file mode 100644 index 0000000000000..e2b0cdbc8624e --- /dev/null +++ b/components/relintex_crm/relintex_crm.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "relintex_crm", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/remarkety/README.md b/components/remarkety/README.md new file mode 100644 index 0000000000000..b97142b06f37b --- /dev/null +++ b/components/remarkety/README.md @@ -0,0 +1,11 @@ +# Overview + +The Remarkety API lets you automate your email marketing campaigns by leveraging customer data. With it, you can build workflows that trigger personalized emails based on shopping behavior and customer profiles. Remarkety's API on Pipedream is a powerhouse for creating dynamic, data-driven marketing strategies by connecting various apps to orchestrate complex workflows, such as syncing customer data, segmenting audiences, or triggering targeted campaigns. + +# Example Use Cases + +- **Cart Abandonment Email Trigger**: When a customer leaves items in their cart without completing a purchase, you can use the Remarkety API to trigger a targeted cart abandonment email. Combine this with scheduled checks of shopping cart statuses and use Pipedream's cron job feature to send reminders at optimal times. + +- **Customer Segmentation and Targeted Campaigns**: Segment customers based on their purchase history and preferences. Use the Remarkety API to tag customers in your database and create targeted email campaigns. Integrate with Shopify to sync customer purchases and create segments based on recent activity or total spend. + +- **Engagement Metrics to Google Sheets**: Use Remarkety to track customer engagement with your emails, such as opens and clicks. Then, send this data to a Google Sheet using Pipedream's Google Sheets app. This allows for easy monitoring of campaign performance and can inform future marketing strategies. diff --git a/components/remote/README.md b/components/remote/README.md new file mode 100644 index 0000000000000..573bf96e8ba67 --- /dev/null +++ b/components/remote/README.md @@ -0,0 +1,11 @@ +# Overview + +The Remote API on Pipedream allows you to automate tasks related to global payroll, benefits, taxes, and compliance for a remote team. By integrating the Remote API with Pipedream’s serverless platform, you can create workflows that react to events within Remote, or from other sources. Automate employee onboarding, offboarding, and data sync between Remote and your HR systems. Effectively, you can connect Remote to hundreds of other apps to streamline HR processes, gather insights, and maintain up-to-date records across all your platforms. + +# Example Use Cases + +- **Automate Employee Onboarding**: When a new hire is added in your HR platform, trigger a Pipedream workflow that creates the employee profile in Remote. This could include setting up payroll details and benefits according to predefined parameters. + +- **Sync Employee Updates to a CRM**: Whenever an employee's role or details are updated in Remote, use Pipedream to capture these changes and update their profile in a connected CRM like Salesforce, ensuring sales operations are always informed of the current team structure. + +- **Offboarding and Compliance Automation**: When an employee is offboarded in Remote, trigger a Pipedream workflow to revoke their access from all company systems, such as email, internal tools like Slack, and send a task to the legal team to ensure all compliance matters are addressed. diff --git a/components/remote/package.json b/components/remote/package.json index 56576ba959f00..4833d85741074 100644 --- a/components/remote/package.json +++ b/components/remote/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/remote_retrieval/README.md b/components/remote_retrieval/README.md new file mode 100644 index 0000000000000..7a39cbc6b01e6 --- /dev/null +++ b/components/remote_retrieval/README.md @@ -0,0 +1,3 @@ +# Overview + +The Remote-Retriever API is tailored for automating the retrieval of contact information, enhancing your CRM data, or developing lead generation tools. By integrating it with Pipedream, you can efficiently extract valuable data and automate workflows for marketing, sales, or customer support. Pipedream’s serverless platform allows you to connect Retriever with numerous other apps, triggering actions based on new data, or updating systems instantly. diff --git a/components/remote_retrieval/actions/create-order/create-order.mjs b/components/remote_retrieval/actions/create-order/create-order.mjs new file mode 100644 index 0000000000000..7980aaabb69c8 --- /dev/null +++ b/components/remote_retrieval/actions/create-order/create-order.mjs @@ -0,0 +1,177 @@ +import app from "../../remote_retrieval.app.mjs"; + +export default { + key: "remote_retrieval-create-order", + name: "Create Order", + description: "Create order in Remote Retrieval. [See the documentation](https://www.remoteretrieval.com/api-integration/#create-order)", + version: "0.0.2", + type: "action", + props: { + app, + typeOfEquipment: { + propDefinition: [ + app, + "typeOfEquipment", + ], + }, + orderType: { + propDefinition: [ + app, + "orderType", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + addressLine1: { + propDefinition: [ + app, + "addressLine1", + ], + }, + addressLine2: { + propDefinition: [ + app, + "addressLine2", + ], + }, + addressCity: { + propDefinition: [ + app, + "addressCity", + ], + }, + addressState: { + propDefinition: [ + app, + "addressState", + ], + }, + addressCountry: { + propDefinition: [ + app, + "addressCountry", + ], + }, + addressZip: { + propDefinition: [ + app, + "addressZip", + ], + }, + phone: { + propDefinition: [ + app, + "phone", + ], + }, + returnPersonName: { + propDefinition: [ + app, + "returnPersonName", + ], + }, + returnCompanyName: { + propDefinition: [ + app, + "returnCompanyName", + ], + }, + returnAddressLine1: { + propDefinition: [ + app, + "returnAddressLine1", + ], + }, + returnAddressLine2: { + propDefinition: [ + app, + "returnAddressLine2", + ], + }, + returnAddressCity: { + propDefinition: [ + app, + "returnAddressCity", + ], + }, + returnAddressState: { + propDefinition: [ + app, + "returnAddressState", + ], + }, + returnAddressCountry: { + propDefinition: [ + app, + "returnAddressCountry", + ], + }, + returnAddressZip: { + propDefinition: [ + app, + "returnAddressZip", + ], + }, + companyEmail: { + propDefinition: [ + app, + "companyEmail", + ], + }, + companyPhone: { + propDefinition: [ + app, + "companyPhone", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.createOrder({ + $, + data: { + orders: [ + { + type_of_equipment: this.typeOfEquipment, + order_type: this.orderType, + employee_info: { + email: this.email, + name: this.name, + address_line_1: this.addressLine1, + address_line_2: this.addressLine2 || "", + address_city: this.addressCity, + address_state: this.addressState, + address_country: this.addressCountry, + address_zip: this.addressZip, + phone: this.phone, + }, + company_info: { + return_person_name: this.returnPersonName, + return_company_name: this.returnCompanyName, + return_address_line_1: this.returnAddressLine1, + return_address_line_2: this.returnAddressLine2 || "", + return_address_city: this.returnAddressCity, + return_address_state: this.returnAddressState, + return_address_country: this.returnAddressCountry, + return_address_zip: this.returnAddressZip, + email: this.companyEmail, + phone: this.companyPhone, + }, + }, + ], + }, + }); + $.export("$summary", "Successfully created order"); + return response; + }, +}; diff --git a/components/remote_retrieval/actions/get-order-details/get-order-details.mjs b/components/remote_retrieval/actions/get-order-details/get-order-details.mjs new file mode 100644 index 0000000000000..35e8d0c20bbdd --- /dev/null +++ b/components/remote_retrieval/actions/get-order-details/get-order-details.mjs @@ -0,0 +1,28 @@ +import app from "../../remote_retrieval.app.mjs"; + +export default { + key: "remote_retrieval-get-order-details", + name: "Get Order Details", + description: "Get the details of the specified order. [See the documentation](https://www.remoteretrieval.com/api-integration/#order-detail)", + version: "0.0.2", + type: "action", + props: { + app, + orderId: { + propDefinition: [ + app, + "orderId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getOrderDetails({ + $, + params: { + ORDER_ID: this.orderId, + }, + }); + $.export("$summary", `Successfully retrieved details of order with ID '${this.orderId}'`); + return response; + }, +}; diff --git a/components/remote_retrieval/actions/get-orders/get-orders.mjs b/components/remote_retrieval/actions/get-orders/get-orders.mjs new file mode 100644 index 0000000000000..9a66684723f77 --- /dev/null +++ b/components/remote_retrieval/actions/get-orders/get-orders.mjs @@ -0,0 +1,21 @@ +import app from "../../remote_retrieval.app.mjs"; + +export default { + key: "remote_retrieval-get-orders", + name: "Get Orders", + description: "Get a list of all orders. [See the documentation](https://www.remoteretrieval.com/api-integration/#all-orders)", + version: "0.0.2", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.getOrders({ + $, + }); + if (response?.results?.length) { + $.export("$summary", `Successfully retrieved ${response.results.length} orders`); + } + return response; + }, +}; diff --git a/components/remote_retrieval/common/constants.mjs b/components/remote_retrieval/common/constants.mjs new file mode 100644 index 0000000000000..80516c5465c3b --- /dev/null +++ b/components/remote_retrieval/common/constants.mjs @@ -0,0 +1,10 @@ +export default { + EQUIPMENT_TYPES: [ + "Laptop", + "Monitor", + ], + ORDER_TYPES: [ + "Return To Company", + "Sell this Equipment", + ], +}; diff --git a/components/remote_retrieval/common/utils.mjs b/components/remote_retrieval/common/utils.mjs new file mode 100644 index 0000000000000..a7e8a35885f72 --- /dev/null +++ b/components/remote_retrieval/common/utils.mjs @@ -0,0 +1,11 @@ +async function streamIterator(stream) { + const resources = []; + for await (const resource of stream) { + resources.push(resource); + } + return resources; +} + +export default { + streamIterator, +}; diff --git a/components/remote_retrieval/package.json b/components/remote_retrieval/package.json new file mode 100644 index 0000000000000..7228609fe358c --- /dev/null +++ b/components/remote_retrieval/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/remote_retrieval", + "version": "0.1.2", + "description": "Pipedream Remote Retrieval Components", + "main": "remote_retrieval.app.mjs", + "keywords": [ + "pipedream", + "remote_retrieval" + ], + "homepage": "https://pipedream.com/apps/remote_retrieval", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} diff --git a/components/remote_retrieval/remote_retrieval.app.mjs b/components/remote_retrieval/remote_retrieval.app.mjs new file mode 100644 index 0000000000000..e7f3be3fa411f --- /dev/null +++ b/components/remote_retrieval/remote_retrieval.app.mjs @@ -0,0 +1,174 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "remote_retrieval", + propDefinitions: { + orderId: { + type: "string", + label: "Order ID", + description: "ID of the Order", + async options() { + const response = await this.getOrders(); + const orderIds = response.results; + return orderIds?.map(({ + order_id, employee_info, + }) => ({ + value: order_id, + label: employee_info.name, + })) || []; + }, + }, + typeOfEquipment: { + type: "string", + label: "Type of Equipment", + description: "The type of equipment", + options: constants.EQUIPMENT_TYPES, + }, + orderType: { + type: "string", + label: "Order Type", + description: "The type of order", + options: constants.ORDER_TYPES, + }, + email: { + type: "string", + label: "Employee Email", + description: "Employee email address", + }, + name: { + type: "string", + label: "Employee Name", + description: "Employee full name", + }, + addressLine1: { + type: "string", + label: "Employee Address Line 1", + description: "Employee Address in line 1", + }, + addressLine2: { + type: "string", + label: "Employee Address Line 2", + description: "Employee Address in line 2, it is not a mandatory field", + optional: true, + }, + addressCity: { + type: "string", + label: "Employee City", + description: "Employee city", + }, + addressState: { + type: "string", + label: "Employee State", + description: "Employee state", + }, + addressCountry: { + type: "string", + label: "Employee Country", + description: "Employee country", + }, + addressZip: { + type: "string", + label: "Employee Zip", + description: "Employee zip", + }, + phone: { + type: "string", + label: "Employee Phone", + description: "Employee phone", + }, + returnPersonName: { + type: "string", + label: "Return Person Name", + description: "Company person name", + }, + returnCompanyName: { + type: "string", + label: "Return Company Name", + description: "Company name", + }, + returnAddressLine1: { + type: "string", + label: "Return Address Line 1", + description: "Company address in line 1", + }, + returnAddressLine2: { + type: "string", + label: "Return Address Line 2", + description: "Company address in line 2, it is not a mandatory field", + optional: true, + }, + returnAddressCity: { + type: "string", + label: "Return Address City", + description: "Company city", + }, + returnAddressState: { + type: "string", + label: "Return Address State", + description: "Company state", + }, + returnAddressCountry: { + type: "string", + label: "Return Address Country", + description: "Company country", + }, + returnAddressZip: { + type: "string", + label: "Return Address Zip", + description: "Company zip", + }, + companyEmail: { + type: "string", + label: "Company Email", + description: "Company email", + }, + companyPhone: { + type: "string", + label: "Company Phone", + description: "Company phone", + }, + }, + methods: { + _baseUrl() { + // return "https://remoteretrieval.com/RR-enterprise/remoteretrieval/public/index.php/api/v1"; + return "https://remoteretrieval.com/api/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + async createOrder(args = {}) { + return this._makeRequest({ + path: "/create-order", + method: "post", + ...args, + }); + }, + async getOrders(args = {}) { + return this._makeRequest({ + path: "/orders", + ...args, + }); + }, + async getOrderDetails(args = {}) { + return this._makeRequest({ + path: "/device_returns", + ...args, + }); + }, + }, +}; diff --git a/components/remotelock/README.md b/components/remotelock/README.md index a24b1c190a28e..3c92a14d2ab74 100644 --- a/components/remotelock/README.md +++ b/components/remotelock/README.md @@ -1,20 +1,14 @@ # Overview -Turn your locks into a smart lock system with RemoteLock. The RemoteLock API -enables users to build access control solutions for a variety of environments, -making controlling access easier than ever. With the RemoteLock API, users can -develop: +The RemoteLock API offers a platform to control and monitor physical access to properties by managing locks and users remotely. This API allows for the integration of lock management into custom applications, enabling automation in granting access, tracking entry, and ensuring security. Leveraging this with Pipedream's serverless platform, users can create automations that react to various triggers, like scheduling access for guests or syncing access permissions with property management systems. -- An online lock system for residences that allows for remote access for - keyless entry -- Limit access rights for employees in an office, factory, or other work - environment with an automated access control system -- Connect locks to existing access control systems for added security and - convenience -- Customize entry and exit requirements for medical and academic buildings -- Integrate locks with business applications to improve customer experience and - maintain tight security -- Create automated and combination locks for commercial, retail, and industrial - use -- Provide clients and users with a secure alternative to traditional keyed - locks +# Example Use Cases + +- **Scheduled Access for Short-Term Rentals** + Automate the generation of access codes for guests based on short-term rental bookings from platforms like Airbnb. When a new booking is detected in the rental platform, generate a unique code via RemoteLock and send it to the guest. + +- **Employee Onboarding and Offboarding** + Streamline the process of granting or revoking building access as part of HR workflows. When an employee is added to or removed from the company's HR system, automatically update their access permissions in RemoteLock. + +- **Sync Access Codes with Event Registrations** + For event spaces that use RemoteLock, create a workflow that syncs attendee registration from an event management app like Eventbrite. Generate and send personalized access codes to attendees before the event. diff --git a/components/remotelock/package.json b/components/remotelock/package.json new file mode 100644 index 0000000000000..5b2ed69a2def8 --- /dev/null +++ b/components/remotelock/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/remotelock", + "version": "0.6.0", + "description": "Pipedream remotelock Components", + "main": "remotelock.app.mjs", + "keywords": [ + "pipedream", + "remotelock" + ], + "homepage": "https://pipedream.com/apps/remotelock", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/remove_bg/README.md b/components/remove_bg/README.md index 494ab3a2d6d93..ee15f276040cd 100644 --- a/components/remove_bg/README.md +++ b/components/remove_bg/README.md @@ -1,16 +1,11 @@ # Overview -Remove.bg is an automated background removal tool, allowing you to quickly and -easily remove backgrounds from any image. With a few API requests, you can -easily transform any image into a background-free photo. +Remove.bg is a powerful API for automatic image background removal, allowing developers to seamlessly strip backgrounds from images and photos. Integrating Remove.bg in Pipedream workflows enables the creation of automated processes for graphic design, e-commerce product listings, profile picture moderation, and more. The API uses sophisticated AI algorithms to detect and separate foreground elements from their backgrounds, offering a clean and precise cutout that can be used across various applications. -The [Remove.bg API](https://remove.bg) will allow you to quickly and -efficiently edit your images, adding a little bit of extra finesse to any image -editing. Here are just a few examples what you can do with the Remove.bg API: +# Example Use Cases -- Easily remove objects and people from any images -- Generate stock photos by removing backgrounds -- Outline objects by eliminating their backgrounds -- Quickly and easily prepare images for printing -- Create playful montages by putting objects on different backgrounds -- Transform any photo into a sharp, vibrant visual +- **Automated Product Image Processing**: Process product photos uploaded to a cloud storage like Dropbox. Whenever a new image is added, Pipedream triggers a workflow that sends the image to Remove.bg to strip the background, then saves the processed image back to Dropbox or another storage service, ready for e-commerce listing. + +- **Streamline Profile Photo Moderation**: Improve user experience by automatically formatting profile pictures for a community platform or social network. When users upload their profile photo, Pipedream can use Remove.bg to remove the background and standardize the profile image look across the platform. + +- **Dynamic Marketing Material Creation**: Generate marketing materials by combining user-uploaded images with branded backgrounds. When a user uploads an image to a service like Google Drive, Pipedream can trigger a workflow that removes the image background via Remove.bg and overlays it onto a selection of brand-specific backgrounds, ready for campaign usage. diff --git a/components/remove_bg/package.json b/components/remove_bg/package.json new file mode 100644 index 0000000000000..03e079515d4e0 --- /dev/null +++ b/components/remove_bg/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/remove_bg", + "version": "0.6.0", + "description": "Pipedream remove_bg Components", + "main": "remove_bg.app.mjs", + "keywords": [ + "pipedream", + "remove_bg" + ], + "homepage": "https://pipedream.com/apps/remove_bg", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/render/README.md b/components/render/README.md index 58a3d44131e0c..7cef602f36347 100644 --- a/components/render/README.md +++ b/components/render/README.md @@ -1,25 +1,11 @@ # Overview -Render API is a flexible, powerful, and easy-to-use API that allows developers -to easily and quickly build and deploy complex applications. With Render API, -developers are empowered to create full stack web and mobile applications using -dynamic data. +The Render API enables developers to automate deployment workflows, manage services, and interact with Render's infrastructure programmatically. Through Pipedream, you can tap into this API to create powerful serverless workflows that seamlessly integrate with your DevOps pipeline. By connecting Render with other apps available on Pipedream, you can orchestrate complex automation scenarios, monitoring, and notifications, ensuring that your deployment process is as efficient and responsive as possible. -The Render API enables you to use cloud infrastructure to quickly build and -deploy fast, cost-effective, and resilient applications. Render API is a -powerful, scalable, and secure distributed computing service. It is optimized -to enable developers to efficiently build and deploy modern, cloud-native -applications, and even deeper custom experiences. +# Example Use Cases -The following are examples of what can be built with Render API: +- **Continuous Deployment Trigger**: Automate your deployment process by triggering a new build on Render when changes are pushed to your GitHub repository. Use Pipedream's GitHub triggers to kick off this workflow, ensuring that your application is always up to date with the latest codebase. -- Website and applications for startups and businesses -- Large-scale web applications for enterprises -- Social networks and portals -- eCommerce stores and shopping carts -- API-based services -- Data storage and analytics -- Mobile applications -- Chatbots -- Machine learning applications -- Automation and robotics +- **Deployment Status Notifications**: Keep your team informed by sending real-time notifications to Slack whenever a deployment starts, succeeds, or fails. Set up a Pipedream workflow that listens for webhook events from Render and uses the Slack API to broadcast the status updates to a designated channel. + +- **Resource Scaling Based on Traffic**: Dynamically scale your Render services based on traffic patterns by integrating with analytics platforms like Google Analytics. Create a Pipedream workflow that periodically checks site traffic and adjusts service scaling on Render accordingly, helping you maintain optimal performance while managing costs. diff --git a/components/renderform/README.md b/components/renderform/README.md new file mode 100644 index 0000000000000..afbe1ba06290a --- /dev/null +++ b/components/renderform/README.md @@ -0,0 +1,11 @@ +# Overview + +The RenderForm API empowers you to automate the conversion of HTML forms into static, embeddable forms. By integrating RenderForm with Pipedream, you can streamline the process of capturing responses and integrating them into your data pipeline. With Pipedream's serverless platform, you can create workflows that trigger on form submissions, pre-fill forms with data from external sources, and connect form data to other apps and services, such as CRMs, email marketing tools, or databases. + +# Example Use Cases + +- **Dynamic Form Generation and Email Distribution**: Create a Pipedream workflow that triggers whenever a new customer is added to your CRM. Use the customer data to dynamically generate a personalized feedback form with the RenderForm API, and then email the form to the customer using the SendGrid app to collect insights. + +- **Survey Data Collection and Analysis**: Build a workflow that triggers on form submission, captures the data via the RenderForm API, and stores the responses in a Google Sheets document. Then, use this data to run analysis or create visualizations that can be presented in company dashboards using Google Data Studio. + +- **Automated Support Ticket Creation**: Set up a Pipedream workflow where customer support request forms, generated by RenderForm, feed directly into a ticketing system when submitted. Each submission can create a new ticket in Zendesk automatically, ensuring that no customer query goes unnoticed and your support team can track and respond efficiently. diff --git a/components/rentcast/actions/find-rental-listings/find-rental-listings.mjs b/components/rentcast/actions/find-rental-listings/find-rental-listings.mjs new file mode 100644 index 0000000000000..c2d1b9ec1ba6f --- /dev/null +++ b/components/rentcast/actions/find-rental-listings/find-rental-listings.mjs @@ -0,0 +1,120 @@ +import rentcast from "../../rentcast.app.mjs"; + +export default { + key: "rentcast-find-rental-listings", + name: "Find Rental Listings", + description: "Search for rental listings in a geographical area, or by a specific address. [See the documentation](https://developers.rentcast.io/reference/rental-listings-long-term)", + version: "0.0.1", + type: "action", + props: { + rentcast, + infoAlert: { + propDefinition: [ + rentcast, + "infoAlert", + ], + }, + address: { + propDefinition: [ + rentcast, + "address", + ], + description: "The full address of the property, in the format of `Street, City, State, Zip`, e.g. `5500 Grand Lake Drive, San Antonio, TX, 78244`. Used to retrieve data for a specific property, or together with the `Radius` prop to search for listings in a specific area", + }, + latitude: { + propDefinition: [ + rentcast, + "latitude", + ], + description: "The latitude of the property, e.g. `29.475962`. Use with `Radius` to search for listings in a specific area", + }, + longitude: { + propDefinition: [ + rentcast, + "longitude", + ], + description: "The longitude of the property, e.g. `-98.351442`. Use with `Radius` to search for listings in a specific area", + }, + radius: { + type: "string", + label: "Radius", + description: "The radius of the search area in miles, with a maximum of 100.", + optional: true, + }, + propertyType: { + propDefinition: [ + rentcast, + "propertyType", + ], + description: "The type of the property, used to search for listings matching this criteria. [See the documentation](https://developers.rentcast.io/reference/property-types) for more information", + }, + bedrooms: { + propDefinition: [ + rentcast, + "bedrooms", + ], + description: "The number of bedrooms, used to search for listings matching this criteria. Use `0` to indicate a studio layout", + }, + bathrooms: { + propDefinition: [ + rentcast, + "bathrooms", + ], + description: "The number of bathrooms, used to search for listings matching this criteria. Supports fractions to indicate partial bathrooms", + }, + status: { + type: "string", + label: "Status", + description: "The current listing status, used to search for listings matching this criteria", + optional: true, + options: [ + "Active", + "Inactive", + ], + }, + daysOld: { + propDefinition: [ + rentcast, + "daysOld", + ], + }, + maxListings: { + type: "integer", + label: "Max Listings", + description: "The maximum number of listings to return. Each API call can retrieve up to `500` listings, so a higher amount will require multiple requests.", + optional: true, + min: 1, + default: 50, + max: 2000, + }, + }, + async run({ $ }) { + let { + rentcast, maxListings, ...params + } = this; + + const totalItems = []; + let offset = 0; + do { + const limit = Math.min(maxListings, 500); + const response = await rentcast.findRentalListings({ + $, + params: { + ...params, + limit, + offset, + }, + }); + const length = response?.length; + if (!length) { + break; + } + totalItems.push(...response.slice(0, maxListings)); + offset += length; + maxListings -= length; + } while (maxListings > 0); + + $.export("$summary", `Successfully fetched ${totalItems.length} rental listings`); + return totalItems; + }, +}; diff --git a/components/rentcast/actions/get-market-statistics/get-market-statistics.mjs b/components/rentcast/actions/get-market-statistics/get-market-statistics.mjs new file mode 100644 index 0000000000000..1896baf9e83dc --- /dev/null +++ b/components/rentcast/actions/get-market-statistics/get-market-statistics.mjs @@ -0,0 +1,35 @@ +import rentcast from "../../rentcast.app.mjs"; + +export default { + key: "rentcast-get-market-statistics", + name: "Get Market Statistics", + description: "Get aggregate rental statistics and listing trends for a single US zip code. [See the documentation](https://developers.rentcast.io/reference/market-statistics)", + version: "0.0.1", + type: "action", + props: { + rentcast, + zipCode: { + propDefinition: [ + rentcast, + "zipCode", + ], + }, + historyRange: { + type: "integer", + label: "History Range", + description: "The time range for historical record entries, in months (defaults to 12)", + optional: true, + }, + }, + async run({ $ }) { + const { + rentcast, ...params + } = this; + const response = await rentcast.getMarketStatistics({ + $, + params, + }); + $.export("$summary", `Fetched market statistics for zip code ${this.zipCode}`); + return response; + }, +}; diff --git a/components/rentcast/actions/get-rent-estimate/get-rent-estimate.mjs b/components/rentcast/actions/get-rent-estimate/get-rent-estimate.mjs new file mode 100644 index 0000000000000..e694e6225f388 --- /dev/null +++ b/components/rentcast/actions/get-rent-estimate/get-rent-estimate.mjs @@ -0,0 +1,92 @@ +import rentcast from "../../rentcast.app.mjs"; + +export default { + key: "rentcast-get-rent-estimate", + name: "Get Rent Estimate", + description: "Get a property rent estimate and comparable properties. [See the documentation](https://developers.rentcast.io/reference/rent-estimate-long-term)", + version: "0.0.1", + type: "action", + props: { + rentcast, + infoAlert: { + propDefinition: [ + rentcast, + "infoAlert", + ], + }, + address: { + propDefinition: [ + rentcast, + "address", + ], + }, + latitude: { + propDefinition: [ + rentcast, + "latitude", + ], + }, + longitude: { + propDefinition: [ + rentcast, + "longitude", + ], + }, + propertyType: { + propDefinition: [ + rentcast, + "propertyType", + ], + }, + bedrooms: { + propDefinition: [ + rentcast, + "bedrooms", + ], + }, + bathrooms: { + propDefinition: [ + rentcast, + "bathrooms", + ], + }, + squareFootage: { + type: "string", + label: "Square Footage", + description: "The total living area size of the property, in square feet", + optional: true, + }, + maxRadius: { + type: "string", + label: "Max Radius", + description: "The maximum distance between comparable listings and the subject property, in miles", + optional: true, + }, + daysOld: { + propDefinition: [ + rentcast, + "daysOld", + ], + }, + compCount: { + type: "integer", + label: "Comparable Count", + description: "The number of comparable listings to use when calculating the value estimate, between 5 and 25. Defaults to 15 if not provided", + optional: true, + min: 5, + max: 25, + default: 15, + }, + }, + async run({ $ }) { + const { + rentcast, ...params + } = this; + const response = await rentcast.fetchRentEstimate({ + $, + params, + }); + $.export("$summary", "Successfully fetched rent estimate"); + return response; + }, +}; diff --git a/components/rentcast/common/constants.mjs b/components/rentcast/common/constants.mjs new file mode 100644 index 0000000000000..b0828e1cfec46 --- /dev/null +++ b/components/rentcast/common/constants.mjs @@ -0,0 +1,33 @@ +export const PROPERTY_TYPE_OPTIONS = [ + { + value: "Single Family", + label: "A detached, single-family property", + }, + { + value: "Condo", + label: + "A single unit in a condominium development or building, which is part of a homeowner’s association (HOA)", + }, + { + value: "Townhouse", + label: + "A single-family property that shares walls with other adjacent homes, and is typically part of a homeowner’s association (HOA)", + }, + { + value: "Manufactured", + label: + "A pre-fabricated or mobile home, typically constructed at a factory", + }, + { + value: "Multi-Family", + label: "A residential multi-family building (2-4 units)", + }, + { + value: "Apartment", + label: "A commercial multi-family building or apartment complex (5+ units)", + }, + { + value: "Land", + label: "A single parcel of vacant, undeveloped land", + }, +]; diff --git a/components/rentcast/package.json b/components/rentcast/package.json new file mode 100644 index 0000000000000..0fd3bc25ac643 --- /dev/null +++ b/components/rentcast/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/rentcast", + "version": "0.1.0", + "description": "Pipedream RentCast Components", + "main": "rentcast.app.mjs", + "keywords": [ + "pipedream", + "rentcast" + ], + "homepage": "https://pipedream.com/apps/rentcast", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} diff --git a/components/rentcast/rentcast.app.mjs b/components/rentcast/rentcast.app.mjs new file mode 100644 index 0000000000000..11a8e2938fccc --- /dev/null +++ b/components/rentcast/rentcast.app.mjs @@ -0,0 +1,112 @@ +import { axios } from "@pipedream/platform"; +import { PROPERTY_TYPE_OPTIONS } from "./common/constants.mjs"; + +export default { + type: "app", + app: "rentcast", + propDefinitions: { + zipCode: { + type: "string", + label: "Zip Code", + description: "A valid 5-digit US zip code", + }, + infoAlert: { + type: "alert", + alertType: "info", + content: "You must specify either `Address`, or `Latitude` and `Longitude`.", + }, + address: { + type: "string", + label: "Address", + description: "The **full address** of the property, in the format of `Street, City, State, Zip`, e.g. `5500 Grand Lake Drive, San Antonio, TX, 78244`", + optional: true, + }, + latitude: { + type: "string", + label: "Latitude", + description: "The latitude of the property, e.g. `29.475962`", + optional: true, + }, + longitude: { + type: "string", + label: "Longitude", + description: "The longitude of the property, e.g. `-98.351442`", + optional: true, + }, + propertyType: { + type: "string", + label: "Property Type", + description: "The type of the property. [See the documentation](https://developers.rentcast.io/reference/property-types) for more information", + optional: true, + options: PROPERTY_TYPE_OPTIONS, + }, + bedrooms: { + type: "integer", + label: "Bedrooms", + description: "The number of bedrooms in the property. Use `0` to indicate a studio layout", + optional: true, + min: 0, + }, + bathrooms: { + type: "string", + label: "Bathrooms", + description: "The number of bathrooms in the property. Supports fractions to indicate partial bathrooms", + optional: true, + }, + squareFootage: { + type: "string", + label: "Square Footage", + description: "The total living area size of the property, in square feet", + optional: true, + }, + maxRadius: { + type: "string", + label: "Max Radius", + description: "The maximum distance between comparable listings and the subject property, in miles", + optional: true, + }, + daysOld: { + type: "integer", + label: "Days Old", + description: "The maximum number of days since comparable listings were last seen on the market, with a minimum of 1", + optional: true, + min: 1, + }, + }, + methods: { + _baseUrl() { + return "https://api.rentcast.io/v1"; + }, + async _makeRequest({ + $ = this, headers, ...args + }) { + return axios($, { + baseURL: this._baseUrl(), + headers: { + ...headers, + "accept": "application/json", + "X-Api-Key": `${this.$auth.api_key}`, + }, + ...args, + }); + }, + async getMarketStatistics(args) { + return this._makeRequest({ + url: "/markets", + ...args, + }); + }, + async fetchRentEstimate(args) { + return this._makeRequest({ + url: "/avm/rent/long-term", + ...args, + }); + }, + async findRentalListings(args) { + return this._makeRequest({ + url: "/listings/rental/long-term", + ...args, + }); + }, + }, +}; diff --git a/components/rentman/actions/create-item/create-item.mjs b/components/rentman/actions/create-item/create-item.mjs new file mode 100644 index 0000000000000..9ba9d281d4430 --- /dev/null +++ b/components/rentman/actions/create-item/create-item.mjs @@ -0,0 +1,59 @@ +import { + ADDITIONAL_PROPS, CONTACT_NAME_FIELDS, +} from "../../common/props.mjs"; +import { snakeCaseData } from "../../common/utils.mjs"; +import rentman from "../../rentman.app.mjs"; + +export default { + key: "rentman-create-item", + name: "Create Item", + description: "Creates a new item based on the specified type. [See the documentation](https://api.rentman.net)", + version: "0.0.1", + type: "action", + props: { + rentman, + itemType: { + propDefinition: [ + rentman, + "itemType", + ], + reloadProps: true, + }, + }, + async additionalProps() { + let props = {}; + if (this.itemType) { + props = ADDITIONAL_PROPS[this.itemType]; + if (this.itemType === "contacts" && this.type) { + props = { + ...props, + ...CONTACT_NAME_FIELDS[this.type], + }; + } + } + return props; + }, + async run({ $ }) { + const { + rentman, + itemType, + contactId, + invoiceId, + projectId, + equipmentId, + projectRequestId, + crewId, + ...data + } = this; + + const response = await rentman.createItem({ + $, + itemType, + parentId: contactId || projectId || crewId || invoiceId || projectRequestId || equipmentId, + data: snakeCaseData(data), + }); + + $.export("$summary", `New ${itemType} succesfully created with Id: ${response.data.id} `); + return response; + }, +}; diff --git a/components/rentman/actions/find-item/find-item.mjs b/components/rentman/actions/find-item/find-item.mjs new file mode 100644 index 0000000000000..44609aa4bf4d2 --- /dev/null +++ b/components/rentman/actions/find-item/find-item.mjs @@ -0,0 +1,37 @@ +import { ADDITIONAL_PROPS_SEARCH } from "../../common/props.mjs"; +import rentman from "../../rentman.app.mjs"; + +export default { + key: "rentman-find-item", + name: "Find Item", + description: "Searches for an item in the system using the item type as the filtering criteria. [See the documentation](https://api.rentman.net/)", + version: "0.0.1", + type: "action", + props: { + rentman, + itemType: { + propDefinition: [ + rentman, + "itemType", + ], + reloadProps: true, + }, + }, + async additionalProps() { + if (this.itemType) { + return { + itemId: ADDITIONAL_PROPS_SEARCH[this.itemType], + }; + } + }, + async run({ $ }) { + const response = await this.rentman.getItem({ + $, + itemType: this.itemType, + itemId: this.itemId, + }); + + $.export("$summary", `Successfully found item with ID ${this.itemId}`); + return response; + }, +}; diff --git a/components/rentman/actions/update-item/update-item.mjs b/components/rentman/actions/update-item/update-item.mjs new file mode 100644 index 0000000000000..af0ff20f550f7 --- /dev/null +++ b/components/rentman/actions/update-item/update-item.mjs @@ -0,0 +1,53 @@ +import { + ADDITIONAL_PROPS, ADDITIONAL_PROPS_SEARCH, +} from "../../common/props.mjs"; +import { snakeCaseData } from "../../common/utils.mjs"; +import rentman from "../../rentman.app.mjs"; + +export default { + key: "rentman-update-item", + name: "Update Rentman Item", + description: "Updates the details of an existing item based on its type. [See the documentation](https://api.rentman.net)", + version: "0.0.1", + type: "action", + props: { + rentman, + itemType: { + propDefinition: [ + rentman, + "itemType", + ], + reloadProps: true, + }, + }, + async additionalProps() { + if (this.itemType) { + const item = ADDITIONAL_PROPS_SEARCH[this.itemType]; + return { + itemId: { + ...item, + description: `${item.description} to update.`, + }, + ...ADDITIONAL_PROPS[this.itemType], + }; + } + }, + async run({ $ }) { + const { + rentman, + itemType, + itemId, + ...data + } = this; + + const response = await rentman.updateItem({ + $, + itemType, + itemId, + data: snakeCaseData(data), + }); + + $.export("$summary", `Successfully updated item with ID ${itemId}`); + return response; + }, +}; diff --git a/components/rentman/common/constants.mjs b/components/rentman/common/constants.mjs new file mode 100644 index 0000000000000..6e279e33853f4 --- /dev/null +++ b/components/rentman/common/constants.mjs @@ -0,0 +1,263 @@ +export const LIMIT = 100; + +export const AVAILABILITY_STATUS_OPTIONS = [ + { + label: "B - This value is returned when the crew availability status is available.", + value: "B", + }, + { + label: "N - This value is returned when the crew availability status is unavailable.", + value: "N", + }, + { + label: "O - This value is returned when the crew availability status is unknown.", + value: "O", + }, +]; + +export const ITEM_TYPE_OPTIONS = [ + { + label: "Appointment", + value: "appointments", + }, + { + label: "Contact Person", + value: "contactpersons", + }, + { + label: "Contact", + value: "contacts", + }, + { + label: "Costs", + value: "costs", + }, + { + label: "Availability", + value: "crewavailability", + }, + { + label: "Payment", + value: "payments", + }, + { + label: "Equipment Rental Request", + value: "projectrequestequipment", + }, + { + label: "Rental Request", + value: "projectrequests", + }, + { + label: "Stock Adjustment", + value: "stockmovements", + }, +]; + +export const COUNTRY_OPTIONS = [ + "af", + "al", + "dz", + "ad", + "ao", + "ai", + "ar", + "am", + "aw", + "au", + "at", + "az", + "bs", + "bh", + "bd", + "bb", + "by", + "be", + "bz", + "bj", + "bm", + "bt", + "bo", + "bq", + "ba", + "bw", + "br", + "bn", + "bg", + "kh", + "cm", + "ca", + "ky", + "cl", + "cn", + "co", + "cd", + "cg", + "cr", + "hr", + "cu", + "cw", + "cy", + "cz", + "dk", + "dj", + "do", + "ec", + "eg", + "sv", + "gq", + "ee", + "et", + "fo", + "fj", + "fi", + "fr", + "gf", + "pf", + "ga", + "ge", + "de", + "gh", + "gr", + "gd", + "gp", + "gu", + "gt", + "gn", + "gy", + "ht", + "hn", + "hk", + "hu", + "is", + "in", + "id", + "ir", + "iq", + "ie", + "im", + "il", + "it", + "ci", + "jm", + "jp", + "jo", + "kz", + "ke", + "ki", + "xk", + "kw", + "kg", + "la", + "lv", + "lb", + "ls", + "lr", + "li", + "lt", + "lu", + "mo", + "mk", + "mg", + "my", + "mv", + "ml", + "mt", + "mq", + "mr", + "mu", + "mx", + "md", + "mc", + "mn", + "me", + "ma", + "mz", + "na", + "np", + "an", + "nc", + "nz", + "ni", + "ng", + "no", + "om", + "pk", + "ps", + "pa", + "pg", + "py", + "pe", + "ph", + "pl", + "pt", + "pr", + "qa", + "re", + "ro", + "ru", + "rw", + "vc", + "sm", + "sa", + "sn", + "rs", + "sc", + "sg", + "sk", + "si", + "so", + "za", + "kr", + "ss", + "es", + "lk", + "mf", + "sx", + "sd", + "sr", + "sj", + "sz", + "se", + "ch", + "sy", + "tw", + "tz", + "th", + "nl", + "tg", + "tt", + "tn", + "tr", + "tc", + "us", + "ug", + "ua", + "ae", + "gb", + "uy", + "uz", + "vu", + "va", + "ve", + "vn", + "vi", + "zm", + "zw", +]; + +export const PAYMENT_IMPORT_SOURCE_OPTIONS = [ + "none", + "exactonline", + "quickbooks", + "xero", + "publicapi", +]; + +export const RECURRENCE_UNIT_OPTIONS = [ + "once", + "days", + "weeks", + "months", + "years", +]; diff --git a/components/rentman/common/props.mjs b/components/rentman/common/props.mjs new file mode 100644 index 0000000000000..4140fbe4a4836 --- /dev/null +++ b/components/rentman/common/props.mjs @@ -0,0 +1,816 @@ +import { + AVAILABILITY_STATUS_OPTIONS, + COUNTRY_OPTIONS, + PAYMENT_IMPORT_SOURCE_OPTIONS, + RECURRENCE_UNIT_OPTIONS, +} from "../common/constants.mjs"; + +const contactId = { + type: "string", + label: "Contact Id", + description: "The Id of the contact.", + optional: true, + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "contacts"), + }), +}; +const projectRequestId = { + type: "string", + label: "Project Request Id", + description: "The id of the project request.", + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "projectrequests"), + }), +}; +const custom = { + type: "object", + label: "Custom", + description: "The object containing all custom defined fields.", + optional: true, +}; +const folder = { + type: "string", + label: "Folder", + description: "The contact's folder.", + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "folders"), + returnType: "folders", + filterFunction: ({ itemtype }) => itemtype === "contact", + }), +}; +const projectId = { + type: "string", + label: "Project Id", + description: "The Id of the project.", + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "projects"), + }), +}; +const subproject = { + type: "string", + label: "Subproject", + description: "The cost's subproject.", + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listSubprojects, + returnType: "subprojects", + }), +}; + +export const ADDITIONAL_PROPS = { + "appointments": { + name: { + type: "string", + label: "Name", + description: "The appointment name.", + optional: true, + }, + start: { + type: "string", + label: "Start", + description: "The appointment start date-time. **Format: YYYY-MM-DDTHH:MM:SSSZ**.", + }, + end: { + type: "string", + label: "End", + description: "The appointment start date-time. **Format: YYYY-MM-DDTHH:MM:SSSZ**.", + }, + color: { + type: "string", + label: "Color", + description: "The appointment color.", + optional: true, + }, + location: { + type: "string", + label: "Location", + description: "The appointment location.", + optional: true, + }, + remark: { + type: "string", + label: "Remark", + description: "The appointment description.", + optional: true, + }, + isPublic: { + type: "boolean", + label: "Is Public", + description: "Whether the appointment is public or private.", + optional: true, + }, + isPlannable: { + type: "boolean", + label: "Is Plannable", + description: "If yes, employees can be scheduled during this appointment.", + optional: true, + }, + }, + "contactpersons": { + contactId, + firstname: { + type: "string", + label: "First Name", + description: "The person's first name.", + optional: true, + }, + middleName: { + type: "string", + label: "Middle Name", + description: "The person's middle name.", + optional: true, + }, + lastname: { + type: "string", + label: "Last Name", + description: "The person's last name.", + optional: true, + }, + function: { + type: "string", + label: "Function", + description: "The person's function.", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The person's phone.", + optional: true, + }, + street: { + type: "string", + label: "Street", + description: "The person's street address.", + optional: true, + }, + number: { + type: "string", + label: "Number", + description: "The person's street address number.", + optional: true, + }, + postalcode: { + type: "string", + label: "Postal Code", + description: "The person's address postal code.", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "The person's address city.", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "The person's address state.", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "The person's address country.", + options: COUNTRY_OPTIONS, + optional: true, + }, + mobilephone: { + type: "string", + label: "Mobile Phone", + description: "The person's mobile phone.", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "The person's email address.", + optional: true, + }, + custom, + }, + "contacts": { + folder, + type: { + type: "string", + label: "type", + description: "The contact's type.", + options: [ + "private", + "company", + ], + reloadProps: true, + }, + extNameLine: { + type: "string", + label: "Ext Name Line", + description: "The contact's extra name line.", + optional: true, + }, + distance: { + type: "integer", + label: "Distance", + description: "Distance from warehouse to visiting address.", + optional: true, + }, + travelTime: { + type: "integer", + label: "Travel Time", + description: "Travel time from warehouse to visiting address.", + optional: true, + }, + code: { + type: "string", + label: "Code", + description: "Automatically generated if left empty.", + optional: true, + }, + accountingCode: { + type: "string", + label: "Accounting Code", + description: "External identifier used for integrations with accounting software.", + optional: true, + }, + visitCity: { + type: "string", + label: "Visit City", + description: "City (visiting address).", + optional: true, + }, + visitStreet: { + type: "string", + label: "Visit Street", + description: "Street (visiting address).", + optional: true, + }, + visitNumber: { + type: "string", + label: "Visit Number", + description: " House number (visiting address) .", + optional: true, + }, + visitPostalcode: { + type: "string", + label: "Visit Postal code", + description: "Postal code (visiting address).", + optional: true, + }, + visitState: { + type: "string", + label: "Visit State", + description: "State/Province (visiting address).", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "Country (visiting address).", + options: COUNTRY_OPTIONS, + optional: true, + }, + phone_1: { + type: "string", + label: "Phone 1", + description: "The contact's phone number.", + optional: true, + }, + phone_2: { + type: "string", + label: "Phone 2", + description: "The contact's additional phone number.", + optional: true, + }, + email_1: { + type: "string", + label: "Email 1", + description: "The contact's email address.", + optional: true, + }, + email_2: { + type: "string", + label: "Email 2", + description: "The contact's additional email address.", + optional: true, + }, + website: { + type: "string", + label: "Website", + description: "The contact's website.", + optional: true, + }, + custom, + }, + "costs": { + projectId, + name: { + type: "string", + label: "Name", + description: "The cost's name.", + optional: true, + }, + remark: { + type: "string", + label: "Name", + description: "The cost's description.", + optional: true, + }, + subproject, + taxclass: { + type: "string", + label: "Tax Class", + description: "This class, in combination with the chosen VAT scheme at project level, determines the final VAT rate.", + optional: true, + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "taxclasses"), + returnType: "taxclasses", + }), + }, + ledger: { + type: "string", + label: "Ledger", + description: "The cost's ledger code.", + optional: true, + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "ledgercodes"), + returnType: "ledgercodes", + }), + }, + verkoopprijs: { + type: "integer", + label: "Sale Price", + description: "The cost's sale price.", + optional: true, + }, + inkoopprijs: { + type: "integer", + label: "Purchase Price", + description: "The cost's purchase price.", + optional: true, + }, + custom, + }, + "crewavailability": { + crewId: { + type: "string", + label: "Crew Id", + description: "The crew Id.", + optional: true, + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "crew"), + }), + }, + start: { + type: "string", + label: "Start", + description: "The availability's start date-time. **Format: YYYY-MM-DDTHH:MM:SSSZ**.", + optional: true, + }, + end: { + type: "string", + label: "End", + description: "The availability's end date-time. **Format: YYYY-MM-DDTHH:MM:SSSZ**.", + optional: true, + }, + status: { + type: "string", + label: "Status", + description: "The availability's status.", + options: AVAILABILITY_STATUS_OPTIONS, + optional: true, + }, + remark: { + type: "string", + label: "Remark", + description: "The availability's description.", + optional: true, + }, + recurrenceIntervalUnit: { + type: "string", + label: "Recurrence Interval Unit", + description: "The type od the recurrence", + options: RECURRENCE_UNIT_OPTIONS, + optional: true, + }, + recurrenceEnddate: { + type: "string", + label: "Recurrence End Date", + description: "The date-time when the occuence will finish. **Format: YYYY-MM-DDTHH:MM:SSSZ**.", + optional: true, + }, + recurrenceInterval: { + type: "integer", + label: "Recurrence Interval", + description: "interval in how many days/weeks/months/years the availability should reoccur.", + optional: true, + }, + }, + "payments": { + invoiceId: { + type: "string", + label: "Invoice Id", + description: "The id of the invoice.", + optional: true, + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "invoices"), + }), + }, + moment: { + type: "string", + label: "Moment", + description: "The payment's date-time. **Format: YYYY-MM-DDTHH:MM:SSSZ**.", + }, + amount: { + type: "integer", + label: "Amount", + description: "The payment amount.", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "The payment's description.", + optional: true, + }, + paymentImportSource: { + type: "string", + label: "Payment Import Source", + description: "Accounting software this payment was imported from.", + options: PAYMENT_IMPORT_SOURCE_OPTIONS, + optional: true, + }, + }, + "projectrequestequipment": { + projectRequestId, + quantity: { + type: "integer", + label: "Quantity", + description: "The quantity of equipment of the request.", + optional: true, + }, + quantityTotal: { + type: "integer", + label: "Quantity Total", + description: "The total quantity of equipment of the request.", + optional: true, + }, + isComment: { + type: "boolean", + label: "Is Comment", + description: "Whether the request is a comment or not.", + optional: true, + }, + isKit: { + type: "boolean", + label: "Is Kit", + description: "Whether the request is a kit or not.", + optional: true, + }, + discount: { + type: "integer", + label: "Discount", + description: "The request equipment discount.", + optional: true, + }, + linkedEquipment: { + type: "string", + label: "Linked Equipment", + description: "The equipment related with this request.", + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "equipment"), + returnType: "equipment", + }), + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "The name of the project request equipment.", + optional: true, + }, + externalRemark: { + type: "string", + label: "External Remark", + description: "The description of the project request equipment.", + optional: true, + }, + parent: { + type: "string", + label: "Parent", + description: "The kit or case it is part of.", + optional: true, + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "projectrequestequipment"), + returnType: "projectrequestequipment", + }), + }, + unitPrice: { + type: "integer", + label: "Unit Price", + description: "The price of the unit.", + optional: true, + }, + factor: { + type: "string", + label: "Factor", + description: "The project request equipment's factor.", + optional: true, + }, + }, + "projectrequests": { + planperiodStart: { + type: "string", + label: "Plan Period Start", + description: "The date-time planned to start the project. **Format: YYYY-MM-DDTHH:MM:SSSZ**.", + }, + planperiodEnd: { + type: "string", + label: "Plan Period End", + description: "The date-time planned to end the project. **Format: YYYY-MM-DDTHH:MM:SSSZ**.", + }, + name: { + type: "string", + label: "Name", + description: "The name of the project.", + optional: true, + }, + linkedContact: { + type: "string", + label: "Linked Contact", + description: "The id of the linked contact.", + optional: true, + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "contacts"), + returnType: "contacts", + }), + }, + contactMailingStreet: { + type: "string", + label: "Contact Mailing Street", + description: "The contact mailing's street address.", + optional: true, + }, + contactMailingNumber: { + type: "string", + label: "Contact Mailing Number", + description: "The contact mailing's street address number.", + optional: true, + }, + contactMailingPostalcode: { + type: "string", + label: "Contact Mailing Postalcode", + description: "The contact mailing's postalcode.", + optional: true, + }, + contactMailingCity: { + type: "string", + label: "Contact Mailing City", + description: "The contact mailing's city.", + optional: true, + }, + contactMailingCountry: { + type: "string", + label: "Contact Mailing Country", + description: "The contact mailing's country.", + optional: true, + }, + contactName: { + type: "string", + label: "Contact Name", + description: "The contact's name.", + optional: true, + }, + contactPersonFirstName: { + type: "string", + label: "Contact Person First Name", + description: "The contact person's first name.", + optional: true, + }, + contactPersonMiddleName: { + type: "string", + label: "Contact Person MiddleName", + description: "The contact person's middle name.", + optional: true, + }, + contactPersonLastname: { + type: "string", + label: "Contact Person Lastname", + description: "The contact person's last name.", + optional: true, + }, + contactPersonEmail: { + type: "string", + label: "Contact Person Email", + description: "The contact person's email.", + optional: true, + }, + usageperiodStart: { + type: "string", + label: "Usage Period Start", + description: "When the usage period starts. **Format: YYYY-MM-DDTHH:MM:SSSZ**.", + optional: true, + }, + usageperiodEnd: { + type: "string", + label: "Usage Period End", + description: "When the usage period ends. **Format: YYYY-MM-DDTHH:MM:SSSZ**.", + optional: true, + }, + language: { + type: "string", + label: "Language", + description: "The project's language.", + optional: true, + }, + externalReference: { + type: "integer", + label: "External Reference", + description: "An external reference code.", + optional: true, + }, + remark: { + type: "string", + label: "Remark", + description: "The project's description.", + optional: true, + }, + price: { + type: "integer", + label: "Price", + description: "The project's price.", + optional: true, + }, + }, + "stockmovements": { + equipmentId: { + type: "string", + label: "Equipment Id", + description: "The id of the equipment.", + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "equipment"), + }), + }, + amount: { + type: "integer", + label: "Amount", + description: "The amount of the equipment.", + optional: true, + }, + projectequipment: { + type: "string", + label: "Project Equipment", + description: "The id of the project equipment.", + optional: true, + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "projectequipment"), + returnType: "projectequipment", + filterFunction: ({ equipment }) => equipment === `/equipment/${this.equipmentId}`, + }), + }, + description: { + type: "string", + label: "Description", + description: "The description of the equipment.", + optional: true, + }, + details: { + type: "string", + label: "Details", + description: "The details of the equipment.", + optional: true, + }, + date: { + type: "string", + label: "Date", + description: "The date of the movement.", + optional: true, + }, + stockLocation: { + type: "string", + label: "Stock Location", + description: "The id of the stock location.", + optional: true, + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "stocklocations"), + returnType: "stocklocations", + }), + }, + }, +}; + +export const ADDITIONAL_PROPS_SEARCH = { + "appointments": { + type: "string", + label: "Appointment Id", + description: "The id of the appointment", + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "appointments"), + }), + }, + "contactpersons": { + type: "string", + label: "Person Id", + description: "The id of the person", + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "contactpersons"), + }), + }, + "contacts": { + ...contactId, + description: "The id of the contact", + }, + "costs": { + type: "string", + label: "Cost Id", + description: "The id of the cost", + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "costs"), + }), + }, + "crewavailability": { + type: "string", + label: "Crew Availability Id", + description: "The id of the crewe availability", + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "crewavailability"), + }), + }, + "payments": { + type: "string", + label: "Payment Id", + description: "The id of the payment", + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "payments"), + }), + }, + "projectrequestequipment": { + type: "string", + label: "Project Request Equipment Id", + description: "The id of the project request equipment", + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "projectrequestequipment"), + }), + }, + "projectrequests": { + ...projectRequestId, + description: "The id of the project request", + }, + "stockmovements": { + type: "string", + label: "Stock Movement Id", + description: "The id of the stock movement", + options: async ({ page }) => this.rentman.asyncOpt({ + page, + fn: this.rentman.listItems.bind(null, "stockmovements"), + }), + }, +}; + +export const CONTACT_NAME_FIELDS = { + "company": { + name: { + type: "string", + label: "Name", + description: "The contact's name.", + }, + }, + "private": { + firstname: { + type: "string", + label: "First Name", + description: "The contact's first name.", + optional: true, + }, + surfix: { + type: "string", + label: "Surfix", + description: "The contact's middle name.", + optional: true, + }, + surname: { + type: "string", + label: "Surname", + description: "The contact's last name.", + optional: true, + }, + }, +}; diff --git a/components/rentman/common/utils.mjs b/components/rentman/common/utils.mjs new file mode 100644 index 0000000000000..ce71422a6c9ee --- /dev/null +++ b/components/rentman/common/utils.mjs @@ -0,0 +1,12 @@ +const camelToSnakeCase = (str) => str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); + +export const snakeCaseData = (data) => { + const snakeCaseData = {}; + for (const [ + key, + value, + ] of Object.entries(data)) { + snakeCaseData[camelToSnakeCase(key)] = value; + } + return snakeCaseData; +}; diff --git a/components/rentman/package.json b/components/rentman/package.json new file mode 100644 index 0000000000000..4949936ec5769 --- /dev/null +++ b/components/rentman/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/rentman", + "version": "0.1.0", + "description": "Pipedream Rentman Components", + "main": "rentman.app.mjs", + "keywords": [ + "pipedream", + "rentman" + ], + "homepage": "https://pipedream.com/apps/rentman", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/rentman/rentman.app.mjs b/components/rentman/rentman.app.mjs new file mode 100644 index 0000000000000..f45732bc83544 --- /dev/null +++ b/components/rentman/rentman.app.mjs @@ -0,0 +1,133 @@ +import { axios } from "@pipedream/platform"; +import { + ITEM_TYPE_OPTIONS, LIMIT, +} from "./common/constants.mjs"; + +export default { + type: "app", + app: "rentman", + propDefinitions: { + itemType: { + type: "string", + label: "Item Type", + description: "The type of item", + options: ITEM_TYPE_OPTIONS, + }, + }, + methods: { + _baseUrl() { + return "https://api.rentman.net/"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.api_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createItem({ + itemType, parentId, ...opts + }) { + const parentNames = { + "costs": "projects", + "crewavailability": "crew", + "payments": "invoices", + "contactpersons": "contacts", + "projectrequestequipment": "projectrequests", + "stockmovements": "equipment", + }; + const parentName = parentNames[itemType]; + + return this._makeRequest({ + method: "POST", + path: parentName + ? `${parentName}/${parentId}/${itemType}` + : itemType, + ...opts, + }); + }, + updateItem({ + itemType, itemId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `${itemType}/${itemId}`, + ...opts, + }); + }, + getItem({ + itemType, itemId, + }) { + return this._makeRequest({ + path: `${itemType}/${itemId}`, + }); + }, + listItems(path, opts) { + return this._makeRequest({ + path, + ...opts, + }); + }, + listSubprojects({ projectId }) { + return this._makeRequest({ + path: `projects/${projectId}/subprojects`, + }); + }, + async asyncOpt({ + page, fn, returnType = null, filterFunction = null, + }) { + let { data } = await fn({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + + if (filterFunction) { + data = data.filter(filterFunction); + } + + return data.map(({ + id, displayname: label, + }) => ({ + label, + value: returnType + ? `/${returnType}/${id}` + : id, + })); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.limit = LIMIT; + params.offset = LIMIT * page++; + const { data } = await fn({ + params, + ...opts, + }); + for (const d of data) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = data.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/rentman/sources/common/base.mjs b/components/rentman/sources/common/base.mjs new file mode 100644 index 0000000000000..4dc9725cae200 --- /dev/null +++ b/components/rentman/sources/common/base.mjs @@ -0,0 +1,59 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import rentman from "../../rentman.app.mjs"; + +export default { + props: { + rentman, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01"; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const dateField = this.getDateField(); + const response = this.rentman.paginate({ + fn: this.getFunction(), + params: this.getParams({ + lastDate, + }), + maxResults, + }); + + let responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + if (responseArray.length) { + this._setLastDate(responseArray[0][dateField]); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item[dateField]), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/rentman/sources/new-file-updated/new-file-updated.mjs b/components/rentman/sources/new-file-updated/new-file-updated.mjs new file mode 100644 index 0000000000000..c04dd70970d7b --- /dev/null +++ b/components/rentman/sources/new-file-updated/new-file-updated.mjs @@ -0,0 +1,31 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "rentman-new-file-updated", + name: "New File Uploaded/Updated", + description: "Emit new event when a file is uploaded/updated. [See the documentation](https://api.rentman.net/)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getDateField() { + return "modified"; + }, + getFunction() { + return this.rentman.listItems.bind(null, "files"); + }, + getParams({ lastDate }) { + return { + "modified[gt]": lastDate, + "sort": "-modified", + }; + }, + getSummary(item) { + return `New file updated: ${item.displayname}`; + }, + }, + sampleEmit, +}; diff --git a/components/rentman/sources/new-file-updated/test-event.mjs b/components/rentman/sources/new-file-updated/test-event.mjs new file mode 100644 index 0000000000000..bc082e847b92a --- /dev/null +++ b/components/rentman/sources/new-file-updated/test-event.mjs @@ -0,0 +1,28 @@ +export default { + "id": 277, + "created": "2024-08-05T22:19:14-06:00", + "modified": "2024-08-05T22:19:14-06:00", + "creator": null, + "displayname": "email.txt", + "readable_name": "email.txt", + "expiration": null, + "size": 14898, + "image": false, + "item": 0, + "itemtype": null, + "description": "", + "in_documents": false, + "in_webshop": false, + "classified": false, + "public": false, + "type": "text/plain", + "preview_of": null,"previewstatus": "no", + "file_item": 25,"file_itemtype": "Message", + "path": "", + "path_without_file_name": "", + "name_without_extension": "email", + "extension": "txt", + "url": "https://rentman-production.s3.eu-west-1.amazonaws.com/110532/rm4_enmeldemo_277_email.txt?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=2F20240814%2Feu-west-1%2Fs3%2Faws4_request&X-Amz-Date=&X-Amz-SignedHeaders=host&X-Amz-Expires=36000&X-Amz-Signature=fa3053e812208b526fec21f661dad9155066f54a590aa7269cac1ee72c95d822", + "proxy_url": "https://redirect.rentman.net/file.php?file=eyJidWNrZXRuYW1lIjoicmVudG1hbi1wcm9kdWN0aW9uIiwiZnVsbG5hbWUiOiIxMTA1MzJcL3JtNF9lbm1lbGRlbW9fMjc3X2VtYWlsLnR4dCIsImlhdCI6MTcyMzY2Mjc2MCwiZXhwIjoxNzIzNjYzMzYwfQ.iGjkl99FXnAT3IIwrRYUeTkBWKMbRym97TwsgRBFafo", + "updateHash": "8ff3ace225f898ede283e3cf82f4803f" +} \ No newline at end of file diff --git a/components/rentman/sources/new-item-created/new-item-created.mjs b/components/rentman/sources/new-item-created/new-item-created.mjs new file mode 100644 index 0000000000000..04e84af624a92 --- /dev/null +++ b/components/rentman/sources/new-item-created/new-item-created.mjs @@ -0,0 +1,40 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "rentman-new-item-created", + name: "New Item Created", + description: "Emit new event when an item is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + itemType: { + propDefinition: [ + common.props.rentman, + "itemType", + ], + }, + }, + methods: { + ...common.methods, + getDateField() { + return "created"; + }, + getFunction() { + return this.rentman.listItems.bind(null, this.itemType); + }, + getParams({ lastDate }) { + return { + "created[gt]": lastDate, + "sort": "-created", + }; + }, + getSummary(item) { + return `New ${this.itemType} created: ${item.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/rentman/sources/new-item-created/test-event.mjs b/components/rentman/sources/new-item-created/test-event.mjs new file mode 100644 index 0000000000000..17efd4e098388 --- /dev/null +++ b/components/rentman/sources/new-item-created/test-event.mjs @@ -0,0 +1,64 @@ +export default { + "custom": {}, + "id": 3641, + "created": "2024-08-05T22:19:14-06:00", + "modified": "2024-08-05T22:19:14-06:00", + "creator": "/crew/33", + "displayname": "Teatro dell'Opera di Roma", + "folder": "/folders/35", + "type": "company", + "ext_name_line": "", + "firstname": "", + "distance": 0, + "travel_time": 0, + "surfix": "", + "surname": "", + "longitude": 0, + "latitude": 0, + "code": "24", + "accounting_code": "", + "name": "Teatro dell'Opera di Roma", + "gender": "", + "mailing_city": "Rome", + "mailing_street": "Piazza Beniamino Gigli", + "mailing_number": "", + "mailing_postalcode": "00184", + "mailing_state": "", + "mailing_country": "mx", + "visit_city": "Rome", + "visit_street": "Piazza Beniamino Gigli", + "visit_number": "", + "visit_postalcode": "00184", + "visit_state": "", + "country": "mx", + "invoice_city": "Rome", + "invoice_street": "Piazza Beniamino Gigli", + "invoice_number": "", + "invoice_postalcode": "00184", + "invoice_state": "", + "invoice_country": "mx", + "phone_1": "", + "phone_2": "", + "email_1": "", + "email_2": "", + "website": "", + "VAT_code": "", + "fiscal_code": "", + "commerce_code": "", + "purchase_number": "", + "bic": "", + "bank_account": "", + "default_person": null, + "admin_contactperson": null, + "discount_crew": 0, + "discount_transport": 0, + "discount_rental": 0, + "discount_sale": 0, + "discount_total": 0, + "projectnote": "", + "projectnote_title": "", + "contact_warning": "", + "discount_subrent": 0, + "tags": "trial", + "updateHash": "7b1f676960e0837a9c80756bc0ebe07e" +} \ No newline at end of file diff --git a/components/repairshopr/README.md b/components/repairshopr/README.md index 1d6367109e6ec..0e3192c725cd2 100644 --- a/components/repairshopr/README.md +++ b/components/repairshopr/README.md @@ -1,20 +1,11 @@ # Overview -With the RepairShopr API, you can build powerful applications revolving around -your business. Whether you're looking for data to display on an internal -dashboard, or an intuitive customer-facing kiosk, the API opens up limitless -possibilities. +The RepairShopr API bridges your repair shop management activities with powerful automation capabilities on Pipedream. Whether it's triggering workflows from new tickets, updating client records, or syncing invoices, this API has got you covered. Streamline your repair service operations by automating routine tasks, integrating with countless other apps, and refining customer interactions, all through the convenience of Pipedream's serverless platform. -Here are just a few examples of things you can build using the RepairShopr API: +# Example Use Cases -- Automate customer onboarding processes. -- Create customer self-service portals. -- Monitor customer, technician, and equipment performance. -- Generate automated reports. -- Build comprehensive dashboards to review information from all perspectives. -- Build personalized customer accounts with detailed information. -- Develop a comprehensive ticketing system for customer requests. -- Create detailed estimates for customers. -- Track inventory and stock status. -- Monitor and control access to financial data. -- Implement a mobile app for technicians. +- **Automatic Ticket Creation**: Seamlessly create RepairShopr tickets from customer emails or support requests received via integrated apps like Zendesk or Intercom. This workflow could scan incoming messages for specific keywords and, upon detecting a service request, automatically generate a new ticket in RepairShopr, ensuring no customer issue goes unnoticed. + +- **Inventory Level Alerting**: Stay ahead of stock shortages by setting up a workflow that monitors inventory levels within RepairShopr. When a critical item's stock falls below a predefined threshold, trigger an alert and create a restock task in project management tools like Asana or Trello, or send a notification to a Slack channel dedicated to inventory management. + +- **Invoice Syncing and Payment Tracking**: Connect RepairShopr to accounting software like QuickBooks or Xero. Automate the process of syncing completed invoices from RepairShopr to the accounting system. Additionally, set up alerts for overdue payments and automatically send follow-up reminders to clients via email or SMS through Twilio, reducing the manual effort involved in accounts receivable. diff --git a/components/replicate/README.md b/components/replicate/README.md new file mode 100644 index 0000000000000..0ac61ebc67b2f --- /dev/null +++ b/components/replicate/README.md @@ -0,0 +1,11 @@ +# Overview + +The Replicate API allows you to access a wide array of machine learning models for tasks such as image generation, text-to-image, and more. Using Pipedream, you can orchestrate these models to automate content creation, analyze media, or enhance data with AI-generated insights. Pipedream's serverless platform empowers you to create workflows that react to events, schedule tasks, and integrate with numerous other services, all harnessing the power of Replicate's AI models. + +# Example Use Cases + +- **Content Automation for Social Media**: Use the Replicate API in Pipedream to generate images or videos based on trending topics. Trigger the workflow with a scheduled event or a webhook from a trend analysis tool, process the content using an appropriate Replicate model, and post directly to social media platforms like Twitter or Instagram through their APIs. + +- **Enhanced Customer Support with Image Moderation**: Automate the screening of user-generated images in support tickets. When a new ticket is created in your helpdesk software (e.g., Zendesk), trigger a Pipedream workflow to use Replicate’s content moderation models to assess and flag inappropriate content, streamlining the support process and maintaining community standards. + +- **Dynamic Email Campaigns with Personalized Images**: Generate personalized images for email marketing campaigns. Combine customer data from a CRM platform like HubSpot with the Replicate API in a Pipedream workflow to create custom visuals. Integrate with an email service like SendGrid to distribute unique, compelling email content that increases engagement. diff --git a/components/repliq/actions/get-credits-count/get-credits-count.mjs b/components/repliq/actions/get-credits-count/get-credits-count.mjs new file mode 100644 index 0000000000000..774bd968f9af4 --- /dev/null +++ b/components/repliq/actions/get-credits-count/get-credits-count.mjs @@ -0,0 +1,19 @@ +import repliq from "../../repliq.app.mjs"; + +export default { + key: "repliq-get-credits-count", + name: "Get Credits Count", + description: "Retrieve the total number of credits left for the month. [See the documentation](https://developer.repliq.co/get/g1-credits-count)", + version: "0.0.1", + type: "action", + props: { + repliq, + }, + async run({ $ }) { + const response = await this.repliq.getCreditsCount({ + $, + }); + $.export("$summary", `Successfully retrieved credits count: ${response.creditsCount}`); + return response; + }, +}; diff --git a/components/repliq/actions/launch-template/launch-template.mjs b/components/repliq/actions/launch-template/launch-template.mjs new file mode 100644 index 0000000000000..fbe1e8d550da1 --- /dev/null +++ b/components/repliq/actions/launch-template/launch-template.mjs @@ -0,0 +1,57 @@ +import { predefinedProps } from "../../common/props.mjs"; +import repliq from "../../repliq.app.mjs"; + +export default { + key: "repliq-launch-template", + name: "Launch Repliq Template", + description: "Launch a Repliq process by deploying the selected template. [See the documentation](https://developer.repliq.co/)", + version: "0.0.1", + type: "action", + props: { + repliq, + templateId: { + propDefinition: [ + repliq, + "templateId", + ], + reloadProps: true, + }, + }, + async additionalProps() { + let props = {}; + if (this.templateId) { + const templates = await this.repliq.listTemplates(); + const template = templates.find((item) => item.id === this.templateId); + + props = { + ...predefinedProps[template.type], + email: { + type: "string", + label: "Email", + description: "The account email you want to send this result to.", + optional: true, + }, + webhook: { + type: "string", + label: "Webhook", + description: "Attach a webhook URL that will trigger when the process is ready for you to use. You cannot use multiple URLs.", + optional: true, + }, + }; + } + return props; + }, + async run({ $ }) { + const { + repliq, + ...data + } = this; + + const response = await repliq.launchTemplate({ + $, + data, + }); + $.export("$summary", `Successfully launched template with ID ${this.templateId}`); + return response; + }, +}; diff --git a/components/repliq/common/props.mjs b/components/repliq/common/props.mjs new file mode 100644 index 0000000000000..13ceef0f39436 --- /dev/null +++ b/components/repliq/common/props.mjs @@ -0,0 +1,109 @@ +const url = { + type: "string", + label: "URL", + description: "The url of the website you want to use.", +}; +const firstName = { + type: "string", + label: "First Name", + description: "The name of the person you create the process for.", +}; +const lastName = { + type: "string", + label: "Last Name", + description: "The last name of the person you create the process for.", + optional: true, +}; +const companyName = { + type: "string", + label: "Company Name", + description: "The name of the company you create the process for.", +}; +const jobTitle = { + type: "string", + label: "Job Title", + description: "The Job Title of the person you create the process for.", + optional: true, +}; +const icebreaker = { + type: "string", + label: "Icebreaker", + description: "Use an icebreaker as introduction for your process specific to the person you create the process for.", + optional: true, +}; +const yourCustomVariable = { + type: "string", + label: "Your Custom Variable", + description: "The yourCustomVariable you use for your process.", + optional: true, +}; + +export const predefinedProps = { + ai_image: { + firstName, + lastName, + jobTitle, + companyName: { + ...companyName, + optional: true, + }, + yourCustomVariable, + }, + ai_avatar: { + url, + companyName: { + ...companyName, + optional: true, + }, + firstName: { + ...firstName, + optional: true, + }, + jobTitle, + icebreaker, + }, + gpt_image: { + companyName, + firstName: { + ...firstName, + optional: true, + }, + lastName, + jobTitle, + competion: { + type: "string", + label: "Competion", + description: "The competion of the company you create an image for.", + optional: true, + }, + yourCustomVariable, + }, + icebreaker: { + url, + firstName, + }, + text_to_video: { + firstName, + prospectImgUrl: { + type: "string", + label: "Prospect Img URL", + description: "The linkedin profile url of the person you want to reach out to.", + optional: true, + }, + lastName, + companyName: { + ...companyName, + optional: true, + }, + jobTitle, + icebreaker, + }, + video_scale: { + url, + firstName: { + ...firstName, + optional: true, + }, + lastName, + }, +}; diff --git a/components/repliq/package.json b/components/repliq/package.json new file mode 100644 index 0000000000000..1bb671ce35da6 --- /dev/null +++ b/components/repliq/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/repliq", + "version": "0.1.0", + "description": "Pipedream RepliQ Components", + "main": "repliq.app.mjs", + "keywords": [ + "pipedream", + "repliq" + ], + "homepage": "https://pipedream.com/apps/repliq", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} + diff --git a/components/repliq/repliq.app.mjs b/components/repliq/repliq.app.mjs new file mode 100644 index 0000000000000..771e9112b5da1 --- /dev/null +++ b/components/repliq/repliq.app.mjs @@ -0,0 +1,63 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "repliq", + propDefinitions: { + templateId: { + type: "string", + label: "Template ID", + description: "The ID of the template you want to deploy.", + async options() { + const templates = await this.listTemplates(); + if (templates.error) return []; + + return templates.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.repliq.co/v2"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + getCreditsCount(opts = {}) { + return this._makeRequest({ + path: "/getCreditsCount", + ...opts, + }); + }, + listTemplates() { + return this._makeRequest({ + path: "/getTemplateList", + }); + }, + launchTemplate(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/launchTemplate", + ...opts, + }); + }, + }, +}; diff --git a/components/reply/README.md b/components/reply/README.md index 5536832464d70..c481bea32fac0 100644 --- a/components/reply/README.md +++ b/components/reply/README.md @@ -1,25 +1,11 @@ # Overview -What can you build with the Reply API? The possibilities are endless! Reply is -Used to power conversational Interfaces, AI Automation, and data enrichment -operations within many modern apps, websites, and services. It can be used to -create interactive chatbots, natural language processing, entity recognition, -and more. +The Reply API on Pipedream empowers you to automate sales and outreach by integrating with a powerful sales engagement platform. By harnessing this API, you can create, update, and track communication with leads, manage contact information, and automate follow-up emails based on triggers from a vast array of other apps. This enables you to streamline sales workflows, enhance lead engagement, and improve conversion rates without manual intervention. -Here are some examples of what you can build using the Reply API: +# Example Use Cases -- Chatbots: Create bots that can understand natural language and answer - questions in real-time. -- Automatic Responses: Automatically respond to messages based on predetermined - keywords or phrases. -- Data Enrichment: Automatically fetch data from external sources and apply it - to users' messages. -- Natural Language Processing: Automatically analyze text and detect - intentions, entities, and sentiment. -- Intent Detection: Identify the user’s intent behind their message. -- Entity Extraction: Extract entities from user’s messages for further - processing. -- Trigger Automations: Trigger automated actions based on user input. -- Adaptive Reply: Reply with contextually relevant answers based on user input. -- Content Curation: Curate content from multiple sources and display it in the - users’ responses. +- **Automated Lead Nurturing Workflow**: Utilize inbound lead data from a form submission or CRM like Salesforce to automatically add new leads to a Reply sequence. When a new lead is detected, Pipedream can create a contact in Reply and enroll them in an email sequence, ensuring timely follow-ups and consistent engagement. + +- **Prospect Engagement Tracking**: Connect Reply to a messaging platform such as Slack using Pipedream. Whenever a prospect opens an email or clicks a link in the sequence, trigger a notification on Slack. This real-time alert system helps sales teams to react promptly, tailoring their approach based on prospect actions for higher effectiveness. + +- **Synchronization with a CRM**: Integrate Reply with HubSpot or any CRM available on Pipedream to keep your sales data in sync. Each time a contact is updated or a new interaction is logged in Reply, Pipedream can update corresponding records in the CRM. This ensures that your sales team always has the latest information at their fingertips, reducing data silos and enhancing collaboration. diff --git a/components/reply/package.json b/components/reply/package.json new file mode 100644 index 0000000000000..b3d82ea804e13 --- /dev/null +++ b/components/reply/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/reply", + "version": "0.6.0", + "description": "Pipedream reply Components", + "main": "reply.app.mjs", + "keywords": [ + "pipedream", + "reply" + ], + "homepage": "https://pipedream.com/apps/reply", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/reply_io/README.md b/components/reply_io/README.md index 4a299cb9671d9..e6451b03e5535 100644 --- a/components/reply_io/README.md +++ b/components/reply_io/README.md @@ -1,23 +1,11 @@ # Overview -Using Reply.io's API, you can build a variety of powerful applications for your -personal and business needs. By taking advantage of its powerful automation -capabilities, you can automate routine activities such as customer onboarding, -lead nurturing, customer support, and marketing outreach. You can also use -Reply.io to quickly scale up your operations, reduce customer support response -times, and stay organized. +The Reply.io API on Pipedream allows users to automate their sales engagement and follow-up processes efficiently. Leveraging this API, you can streamline communication by triggering personalized emails, managing contacts, and analyzing the performance of sales campaigns. Using Pipedream's serverless platform, you can create workflows that perform actions in Reply.io in response to events from other apps, schedule tasks, and process data in real-time. -Here are some examples of applications you can build with Reply.io's API: +# Example Use Cases -- Lead nurturing sequences -- Automated customer onboarding -- Automated customer support -- Automated marketing outreach -- Customer feedback surveys -- Automated notifications -- Sales automation -- Lead scoring -- Multi-channel customer support -- User onboarding automation -- Multi-step follow up sequences -- Predictive lead scoring +- **Lead Qualification Automation**: When a new lead is captured in a CRM like Salesforce, you can automatically add them to a Reply.io campaign, ensuring a timely and consistent follow-up. If the lead responds, the workflow can pause the campaign and notify the sales team via Slack to take over the conversation. + +- **Webinar Follow-Up**: After hosting a webinar using a platform like Zoom, you can sync the attendee list to Reply.io and trigger personalized follow-up sequences. Depending on attendee engagement, you might score leads and funnel them into different nurturing tracks or sales pipelines. + +- **Feedback Collection Automation**: Post-purchase or after a service is rendered, you can automatically send out customer satisfaction surveys via Reply.io. Based on the survey responses, gathered in a tool like Typeform, the workflow can create support tickets in Zendesk for customers who reported issues, or add satisfied customers to a referral program campaign. diff --git a/components/repuso/README.md b/components/repuso/README.md index a0a904afd6ee6..0965304d2933e 100644 --- a/components/repuso/README.md +++ b/components/repuso/README.md @@ -1,21 +1,11 @@ # Overview -Repuso API is a powerful and easy to use API that offers a wide range of -content creation and management capabilities. With it, you can easily create, -manage, and publish content for use in any application. From creating websites, -blogs and other web content, to building applications for mobile and desktop, -to creating multimedia presentations, Repuso API is a great tool for content -creators of all kinds. +Repuso is a tool designed for aggregating, managing, and displaying customer reviews and testimonials from various channels. With the Repuso API on Pipedream, you can automate the collection and distribution of this feedback. This allows for dynamic monitoring of brand reputation, instant sharing of positive customer experiences across platforms, and an efficient way to respond to customer insights. By leveraging Pipedream's capabilities, you can connect Repuso with a myriad of services, like CRMs, marketing tools, and social media platforms, to enhance customer engagement strategies. -Here is a list of some of the things you can build with the Repuso API: +# Example Use Cases -- Websites: Create dynamic websites, blogs, and other web content with ease. -- Applications: Build applications for iOS, Android, Windows, and other - platforms. -- Multimedia Presentations: Create stunning multimedia presentations with - dynamic visuals, audio and video. -- Content Management: Manage and publish content with great control. -- Google Analytics Integration: Track and analyze your content with ease. -- Social Media Integration: Integrate with popular social networks like - Twitter, Facebook and more. -- ECommerce Platforms: Create eCommerce websites with great scalability. +- **Automate Review Aggregation**: Collect reviews automatically from Repuso and insert them into a Google Sheets spreadsheet for analysis. This workflow streamlines feedback management, making it easier to monitor and respond to customer sentiments in real-time. + +- **Dynamic Testimonial Display on Website**: Use Repuso to fetch the latest positive testimonials and automatically update your website's testimonial section via a CMS like WordPress. This ensures your site always showcases the most recent and impactful customer feedback. + +- **Social Proof Marketing Campaigns**: Trigger an email marketing campaign through a platform like Mailchimp when new 5-star reviews are detected. These emails can highlight positive customer stories to prospects, leveraging social proof to enhance marketing efforts. diff --git a/components/reputation_lyncs/README.md b/components/reputation_lyncs/README.md new file mode 100644 index 0000000000000..737be72d4db2b --- /dev/null +++ b/components/reputation_lyncs/README.md @@ -0,0 +1,11 @@ +# Overview + +Reputation Lyncs offers an API that allows you to monitor and manage online reviews and ratings across various platforms. In Pipedream, this API can be harnessed to create powerful workflows that automate the process of tracking your business's reputation, responding to reviews, and analyzing sentiment across different review sites. Pipedream's serverless platform enables you to integrate these features with other apps seamlessly, triggering actions based on review data or updating dashboards with real-time insights. + +# Example Use Cases + +- **Automated Review Alerts**: Trigger a Pipedream workflow whenever a new review is posted. Use this to send real-time notifications to Slack, email, or SMS, helping your customer service team respond promptly to feedback. + +- **Sentiment Analysis Dashboard**: Collect and analyze reviews using the Reputation Lyncs API, then pass the data to a sentiment analysis service. Aggregate the results on a dashboard tool like Google Sheets or Tableau, giving you a clear view of your reputation trends over time. + +- **Review Aggregation and Response**: Combine your review data from Reputation Lyncs with a CRM like HubSpot or Salesforce. This workflow could fetch new reviews, log them in the CRM, and even start email sequences or tasks based on the review content, such as thanking a customer for positive feedback or reaching out to address a complaint. diff --git a/components/rescuetime/README.md b/components/rescuetime/README.md index 32b03b47d980d..567a86a7b7875 100644 --- a/components/rescuetime/README.md +++ b/components/rescuetime/README.md @@ -1,18 +1,11 @@ # Overview -The RescueTime API can help you obtain actionable insights from your digital -usage data. With it, you can develop projects that measure and compare daily -activity, measure time spent on specific tasks, and identify and track -productivity trends. With this powerful API, you have the ability to optimize -your productivity and understand how to best manage your digital life. +The RescueTime API provides access to detailed data about how you spend your time on digital devices, including time spent on apps and websites. With this API on Pipedream, you can create custom workflows that trigger actions based on your productivity data. For instance, you can set up alerts for excessive social media usage or automate weekly productivity reports. Pipedream's platform enables these automations by connecting RescueTime to hundreds of other apps, allowing for intricate workflows that can enhance personal productivity and time management. -Here are some ways you can use the RescueTime API: +# Example Use Cases -- Monitor how much time you spend on specific tasks, websites and apps. -- Analyze daily productivity and behavior over time. -- Set up personal or team productivity goals. -- Integrate with existing third-party systems to measure productivity. -- Create customized reports and visualizations. -- Run automations and get notifications based on specific digital activity. -- Receive detailed time breakdowns for any timeframe. -- Measure team productivity across multiple devices. +- **Daily Productivity Report to Slack**: Send a summary of your daily activity and productivity score from RescueTime to a Slack channel. This workflow could help teams stay accountable and offer insights into work patterns. + +- **Focused Work Mode Automation**: Trigger a 'Do Not Disturb' mode on communication apps like Slack or your phone when you enter a focused work session in RescueTime. This ensures you remain undisturbed during periods of high productivity. + +- **Time Management Dashboard**: Aggregate your RescueTime data with calendar events from Google Calendar and emails from Gmail to create a comprehensive time management dashboard. This can help you visualize where your time is going and adjust accordingly. diff --git a/components/resend/README.md b/components/resend/README.md new file mode 100644 index 0000000000000..8351c33370985 --- /dev/null +++ b/components/resend/README.md @@ -0,0 +1,11 @@ +# Overview + +The Resend API opens a direct line to powerful communication automation, letting you send, receive, and track messages across various channels like SMS and email. In Pipedream, Resend's capabilities can be harnessed within serverless workflows, allowing for dynamic interactions based on triggers from a multitude of integrated services. From confirmation messages to multi-step notification sequences, you can craft tailored communication flows that react in real-time to your application or service's needs. + +# Example Use Cases + +- **Automated Order Confirmation Emails**: When a new order is placed in an e-commerce platform like Shopify, Pipedream can trigger a workflow that uses the Resend API to send a confirmation email to the customer. The content can be dynamically populated with order details, providing a personalized touch. + +- **SMS Notifications for Critical System Alerts**: Link Resend with a monitoring tool like Datadog within Pipedream. When a critical alert is detected, trigger a workflow that sends an urgent SMS to the on-call engineer, reducing response times for system outages or performance issues. + +- **Multi-channel Customer Support Tickets**: Combine Resend with Help Scout in Pipedream. When a support ticket is created or updated, the workflow can send a message via the customer's preferred channel. This ensures timely updates and enhances the overall customer support experience. diff --git a/components/resource_guru/README.md b/components/resource_guru/README.md new file mode 100644 index 0000000000000..6aca22460e854 --- /dev/null +++ b/components/resource_guru/README.md @@ -0,0 +1,11 @@ +# Overview + +The Resource Guru API lets you manage resources, bookings, and clients programmatically, which is key for automating scheduling and project management tasks. Within Pipedream, you can leverage this API to create workflows that trigger on various events, such as new bookings or changed availability, and perform actions like updating calendars, syncing with project management tools, or even sending notifications to team members. + +# Example Use Cases + +- **Sync Bookings with Google Calendar**: When a new booking is made in Resource Guru, a Pipedream workflow can trigger to automatically create an event in Google Calendar. This ensures that your schedule is always up-to-date across all platforms. + +- **Manage Project Tasks in Asana**: Upon changing a booking status in Resource Guru, trigger a Pipedream workflow to update a corresponding task in Asana. This could include changing the task status, adjusting deadlines, or reassigning to different team members based on resource availability. + +- **Send Slack Notifications on Resource Changes**: Configure a Pipedream workflow to send a notification to Slack whenever a resource is added or removed in Resource Guru. This keeps the entire team informed in real-time about the availability of personnel or equipment. diff --git a/components/respond_io/README.md b/components/respond_io/README.md new file mode 100644 index 0000000000000..ce0d2dc64fa69 --- /dev/null +++ b/components/respond_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The Respond.io API provides a gateway to interact with a platform designed for business messaging, allowing for automation of conversations, contact management, and message dispatch across various channels like SMS, WhatsApp, and Facebook Messenger. In Pipedream, you can construct workflows that leverage these capabilities to sync with other services, trigger actions based on received messages, or even conduct analytics over message data. This harnesses the power of serverless execution to simplify and enhance customer communication strategies. + +# Example Use Cases + +- **Syncing Contacts with CRM**: Whenever a new contact is added in Respond.io, the workflow can trigger to add or update the corresponding entry in a CRM like Salesforce, ensuring that contact information is always synchronized between the systems. + +- **Customer Support Ticketing**: Create a workflow that listens for specific keywords or phrases in incoming messages on Respond.io. When detected, it can automatically generate a support ticket in a platform like Zendesk or Jira, streamlining the support process. + +- **Broadcasting Time-Sensitive Promotions**: Use Pipedream's scheduled tasks to send out promotional messages via Respond.io at optimal times. This could be connected to a marketing platform like Mailchimp to coordinate email and messaging campaigns. diff --git a/components/respond_io/package.json b/components/respond_io/package.json index 781c763b1623c..9ae45abbb6238 100644 --- a/components/respond_io/package.json +++ b/components/respond_io/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/rest_countries_pe/README.md b/components/rest_countries_pe/README.md new file mode 100644 index 0000000000000..ab1e997e6dd72 --- /dev/null +++ b/components/rest_countries_pe/README.md @@ -0,0 +1,11 @@ +# Overview + +The REST Countries PE API provides data on countries across the globe, including information on name, population, area, language, currency, and much more. On Pipedream, this can turn into a powerhouse for automating workflows that necessitate geographical and demographic insights. By leveraging this API, you can enrich datasets, create educational content, trigger actions based on country-specific criteria, or simply gather data for analytical purposes. The ease of use with Pipedream's serverless platform means you can focus on logic and flow rather than the intricacies of infrastructure. + +# Example Use Cases + +- **Generate Country Profiles for a Website**: Automatically fetch detailed information about countries to create or update profiles on a travel or educational website. Use the API to retrieve data and a Pipedream workflow to format and push the content to a CMS like WordPress. + +- **Send Location-Based Notifications**: Trigger notifications or emails when certain criteria are met, such as a new language being recognized in a country. Integrate the API with Pipedream's email or notification services to alert subscribers when there's relevant new information. + +- **Data Enrichment for Analytics**: Before running a geo-targeted marketing campaign, use the REST Countries PE API within a Pipedream workflow to enrich a customer database with country-specific data, ensuring more personalized and effective outreach. diff --git a/components/retable/README.md b/components/retable/README.md new file mode 100644 index 0000000000000..0928aeaf97ae5 --- /dev/null +++ b/components/retable/README.md @@ -0,0 +1,11 @@ +# Overview + +The Retable API enables you to interact with your Retable data programmatically, allowing for seamless integration of your tables within your apps or workflows. With Pipedream, you can harness this capability to automate tasks, sync data across platforms, or trigger actions based on changes in your tables. Pipedream’s low-code platform provides the tools to create powerful serverless workflows using Retable API, without the need for extensive coding knowledge. + +# Example Use Cases + +- **Automated Data Backup**: Create a workflow that triggers at regular intervals to back up your Retable tables to a cloud storage service like Google Drive or Dropbox. This ensures your data is safe and accessible from anywhere. + +- **Real-time CRM Updates**: Connect Retable to a CRM platform such as HubSpot. Each time a new entry is added to a Retable table, trigger a workflow that creates or updates a contact in HubSpot, keeping your sales or customer support teams always in sync with the latest data. + +- **Issue Tracking Integration**: For development teams using Retable to track issues, set up a workflow that integrates with GitHub or Jira. When an issue is updated or marked as resolved in Retable, automatically update the corresponding issue in GitHub or Jira, streamlining the project management process. diff --git a/components/retailed/README.md b/components/retailed/README.md new file mode 100644 index 0000000000000..49ad504cdc7ac --- /dev/null +++ b/components/retailed/README.md @@ -0,0 +1,11 @@ +# Overview + +The Retailed API enables developers to tap into a trove of retail data, ranging from product details to inventory management. Through Pipedream, one can use this API to craft automated workflows that react to specific triggers or schedule tasks to regularly fetch data. With seamless integration capabilities, Pipedream can connect the Retailed API to numerous other apps, allowing for a rich ecosystem of automations that can save time, optimize retail operations, and enhance data analysis. + +# Example Use Cases + +- **Inventory Stock Alerts**: Automatically monitor product inventory levels using the Retailed API. When stock falls below a certain threshold, trigger a workflow that sends an alert via email or posts a message to a Slack channel. This enables timely restocking and ensures that inventory levels are sufficient. + +- **New Product Listings Synchronization**: Whenever a new product is added to the retail system, use the Retailed API to fetch the product details and automatically create or update listings across multiple platforms, such as Shopify or WooCommerce. This ensures that all sales channels are in sync with the latest inventory. + +- **Sales Reporting Automation**: Generate daily or weekly sales reports by pulling data from the Retailed API. These reports can then be formatted and delivered to stakeholders via email or saved to Google Drive, allowing for easy access and historical sales tracking. diff --git a/components/retell/README.md b/components/retell/README.md new file mode 100644 index 0000000000000..6a49005ac03d5 --- /dev/null +++ b/components/retell/README.md @@ -0,0 +1,11 @@ +# Overview + +The Retell API enables innovative ways to analyze and extract valuable insights from voice and video communications. By utilizing this API on Pipedream, you can craft automated workflows that tap into Retell's AI-driven features such as transcriptions, sentiment analysis, and keyword extraction. Pipedream's serverless platform allows these workflows to trigger in response to various events, streamlining your data processing and ensuring real-time analytics. + +# Example Use Cases + +- **Customer Support Call Analysis**: Automatically transcribe customer support calls using the Retell API. Use the transcriptions for sentiment analysis, identifying common issues, and training support staff. Integrate with a CRM like Salesforce to append call summaries and sentiment scores to customer records. + +- **Meeting Summaries and Action Items**: Connect the Retell API with your video conferencing tools, such as Zoom, to capture meeting conversations. Automatically generate summaries and action items, and post them to a team collaboration platform like Slack or Microsoft Teams. + +- **Content Creation from Webinars**: Transform webinar recordings into blog posts or social media content. Use the Retell API to transcribe webinars, then pass the text to an NLP service for content structuring and summarization. Automatically publish the processed content to WordPress or schedule posts on social media platforms. diff --git a/components/retell/package.json b/components/retell/package.json new file mode 100644 index 0000000000000..70a4a949a8489 --- /dev/null +++ b/components/retell/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/retell", + "version": "0.0.1", + "description": "Pipedream Retell Components", + "main": "retell.app.mjs", + "keywords": [ + "pipedream", + "retell" + ], + "homepage": "https://pipedream.com/apps/retell", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/retell/retell.app.mjs b/components/retell/retell.app.mjs new file mode 100644 index 0000000000000..e8db713121fba --- /dev/null +++ b/components/retell/retell.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "retell", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/retently/README.md b/components/retently/README.md new file mode 100644 index 0000000000000..a680be9d543f8 --- /dev/null +++ b/components/retently/README.md @@ -0,0 +1,11 @@ +# Overview + +The Retently API offers a robust set of features for gathering and leveraging customer feedback. With it, you can automate the collection of NPS scores, follow up on customer surveys, and integrate customer sentiment data into other business tools. Pipedream makes these interactions smooth by triggering workflows from Retently events or sending data back to Retently, enabling you to synchronize your customer feedback with the rest of your tech stack effortlessly. + +# Example Use Cases + +- **Sync Retently NPS Scores to Google Sheets**: Use Retently's webhook to trigger a Pipedream workflow whenever a new NPS score is submitted. The workflow can parse the score and associated feedback, then push this data into a Google Sheet for easy tracking and analysis. This allows for real-time updating and sharing of customer sentiment data within your organization. + +- **Trigger Email Follow-Ups Based On Customer Feedback**: After a customer completes a survey, a Pipedream workflow can assess the feedback and, based on predefined criteria (e.g., low NPS score), automatically trigger an email through SendGrid to follow up. This can help in addressing customer concerns promptly and improving overall satisfaction. + +- **Create Support Tickets from Negative Feedback**: When a customer survey indicates dissatisfaction, a Pipedream workflow can create a support ticket in a service like Zendesk. This ensures that the customer's issue is addressed by your support team, potentially turning a negative experience into a positive one by providing timely and effective assistance. diff --git a/components/retool/README.md b/components/retool/README.md new file mode 100644 index 0000000000000..9e797cdba6c2c --- /dev/null +++ b/components/retool/README.md @@ -0,0 +1,11 @@ +# Overview + +Retool is a platform that makes it faster to build internal tools with a drag-and-drop interface. The Retool API allows you to programmatically manage various aspects of your Retool applications, such as triggering queries, updating apps, or retrieving data from your custom tools. In Pipedream, you can utilize this API to automate workflows between Retool and other apps, creating powerful integrations that streamline your internal processes. + +# Example Use Cases + +- **Sync User Data from CRM to Retool**: Automatically update user data in a Retool app when a new contact is added to your CRM platform (e.g., Salesforce). This keeps your internal tools in sync with real-time customer information. + +- **Process Form Submissions with Retool and Send Emails**: When a user submits a form within a Retool app, trigger a workflow in Pipedream to process the submission. Then, use an email service like SendGrid to send a customized email based on the form data. + +- **Schedule Database Backups to Retool**: Use Pipedream's built-in cron job feature to periodically trigger a Retool query that backs up your database to a storage service like Amazon S3. This automates the backup process and ensures your data is always safely stored. diff --git a/components/retool/package.json b/components/retool/package.json index 50823de3fced9..96ac5d3393259 100644 --- a/components/retool/package.json +++ b/components/retool/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/retriever/README.md b/components/retriever/README.md new file mode 100644 index 0000000000000..8c975cb5cc432 --- /dev/null +++ b/components/retriever/README.md @@ -0,0 +1,11 @@ +# Overview + +The Retriever API is designed for automating contact information retrieval, enriching your CRM data, or building lead generation tools. By integrating it with Pipedream, you can strategically extract valuable data and automate workflows for marketing, sales, or customer support. Pipedream’s serverless platform enables you to connect Retriever with hundreds of other apps, triggering actions based on new data, or updating systems in real-time. + +# Example Use Cases + +- **Lead Enrichment for Sales Teams**: Automatically enrich lead information in your CRM when a new contact is added. Use Retriever API to fetch additional details and Pipedream to update the record in CRMs like Salesforce or HubSpot, ensuring your sales team has the most current data. + +- **Automated Contact Syncing in Marketing Campaigns**: Sync contact details between your marketing platforms and Retriever. When a user signs up via a platform like Mailchimp, use Pipedream to trigger Retriever to gather more info and update the user profile, personalizing your marketing efforts. + +- **Customer Support Ticket Enrichment**: Enhance the customer support experience by providing more context to support agents. When a new support ticket is created in tools like Zendesk, use Pipedream with Retriever to pull in additional customer data, equipping agents with a fuller picture to resolve issues efficiently. diff --git a/components/returnly/package.json b/components/returnly/package.json new file mode 100644 index 0000000000000..162befb160113 --- /dev/null +++ b/components/returnly/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/returnly", + "version": "0.6.0", + "description": "Pipedream returnly Components", + "main": "returnly.app.mjs", + "keywords": [ + "pipedream", + "returnly" + ], + "homepage": "https://pipedream.com/apps/returnly", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/rev/README.md b/components/rev/README.md index 954512782038b..1c18266e4b21c 100644 --- a/components/rev/README.md +++ b/components/rev/README.md @@ -1,14 +1,11 @@ # Overview -With Rev's API, you can build integrations that access the full range of -professional services our platform offers. This includes text, audio, and video -transcription, translation, and captioning - Faster, and at scale. - -Here are some examples of what you can create with the Rev API: - -- Transcription of any voice or video recordings for accurate data extraction -- Text-to-speech conversion for different languages -- Automated captioning and subtitling for media -- Machine translation for multilingual projects -- Audio engineering services for media projects -- Quality assurance tools with Transcription accuracy feedback +The Rev API offers transcription, caption, and translation services, which you can integrate into automated workflows on Pipedream. Imagine automatically transcribing recorded meetings, generating captions for user-generated video content, or translating documents on-the-fly. With Rev, you can streamline media production, enhance accessibility, and break down language barriers, all while maintaining high accuracy. + +# Example Use Cases + +- **Automated Podcast Transcription**: When a new podcast episode is uploaded to a hosting platform like SoundCloud, trigger a Pipedream workflow to send the audio file to Rev for transcription. Once the transcript is ready, it can be automatically posted to your website or blog, and even emailed to subscribers for enhanced engagement. + +- **Video Content Captioning**: For a YouTube channel, use Pipedream to watch for new video uploads. Upon detection, send the video to Rev for captioning. After the captions are completed, they can be automatically uploaded back to the YouTube video, ensuring your content is accessible to a wider audience, including those with hearing impairments. + +- **Multilingual Document Translation for Customer Support**: Whenever a non-English support ticket is received via a platform like Zendesk, a Pipedream workflow sends the text to Rev for translation. The translated text is then routed back into the customer support flow, enabling representatives to understand and respond effectively, streamlining support for a global user base. diff --git a/components/rev_ai/README.md b/components/rev_ai/README.md index 6254e29f915b2..a029c2f90f932 100644 --- a/components/rev_ai/README.md +++ b/components/rev_ai/README.md @@ -1,31 +1,11 @@ # Overview -Rev.ai API is an automatic speech recognition (ASR) software that enables users -to extract meaning from audio files. With this highly accurate software, you -can develop powerful and relevant solutions for businesses and organizations. -Whether you're looking to create smart virtual assistants, streamline customer -engagements, or boost digital transcription compliance, Rev.ai API provides you -with the tools you need to get the job done. Here are some of the things you -can build with Rev.AI API: +Rev.ai offers speech recognition and transcription capabilities, making it possible to convert audio into text with high accuracy. This API provides developers access to advanced speech-to-text functions, allowing you to transcribe interviews, meetings, or any audio content quickly. Using Pipedream, these transcriptions can be integrated into workflows that trigger actions in other apps, enriching your data and automating repetitive tasks. -- Chatbots and virtual assistants: Rev.ai API can be used to add speech - recognition capability to your existing chatbot/virtual assistant and make it - more interactive and efficient. -- Speech Enabled Interfaces and Controls: Rev.ai API enables you to create - speech enabled interfaces and controls for a variety of applications such as - TVs, vehicles, home automation systems, etc. -- Automatic Speech Recognition: With Rev.ai API, you can easily integrate - automatic speech recognition capabilities into your application, enabling you - to quickly transcribe spoken language into text. -- Intelligent Digital Transcription: With Rev.ai API you can create accurate, - real-time digital transcription from audio files, allowing you to quickly and - accurately transcribe recorded conversations and other spoken content. -- Voice Biometrics: Rev.ai API can be used to add voice biometrics capabilities - to your application, allowing users to easily verify their identities using - the power of their voices. -- Natural Language Understanding (NLU): Rev.ai API can be used to create - natural language understanding solutions that enable your applications to - understand the meaning of spoken language, allowing them to understand and - respond to user input. -- Voice Data Analytics: With Rev.ai API, you can develop voice data analytics - solutions that make it easy to analyze voice data and gain valuable insights. +# Example Use Cases + +- **Automated Meeting Transcriptions to Task Manager**: Whenever a new audio file is uploaded to a cloud storage service like Dropbox, Pipedream triggers a workflow that sends the file to Rev.ai for transcription. The resulting text can be parsed to identify action items and then automatically added as tasks to a project management tool such as Asana, ensuring that no to-do item is missed from verbal meetings. + +- **Customer Support Call Analysis**: After a support call is recorded, the audio can be sent to Rev.ai via Pipedream. Once transcribed, the text is analyzed for sentiment and keywords using Pipedream's built-in code steps or an AI service like MonkeyLearn. This analysis can then be logged in a CRM like Salesforce or sent as a summary report to a Slack channel, giving insights into customer satisfaction and agent performance. + +- **Content Creation from Podcasts**: Using a Pipedream workflow, podcast episodes uploaded to an RSS feed can be automatically transcribed by Rev.ai. The transcriptions can then be formatted and published as blog posts to platforms like WordPress or Medium. Additionally, key phrases and topics from the transcript can be extracted to generate tags and SEO-friendly metadata to enhance discoverability. diff --git a/components/rev_ai/package.json b/components/rev_ai/package.json new file mode 100644 index 0000000000000..6da1a9fd5a430 --- /dev/null +++ b/components/rev_ai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/rev_ai", + "version": "0.6.0", + "description": "Pipedream rev_ai Components", + "main": "rev_ai.app.mjs", + "keywords": [ + "pipedream", + "rev_ai" + ], + "homepage": "https://pipedream.com/apps/rev_ai", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/revamp_crm/README.md b/components/revamp_crm/README.md new file mode 100644 index 0000000000000..1ba723341dfb8 --- /dev/null +++ b/components/revamp_crm/README.md @@ -0,0 +1,11 @@ +# Overview + +The Revamp CRM API provides a suite of tools for managing customer relationships and sales processes. Integrating Revamp CRM with Pipedream allows users to automate actions based on CRM events, sync data between multiple platforms, and create custom workflows to streamline business operations. With Pipedream's serverless platform, you can trigger workflows from Revamp CRM events, perform operations on your CRM data, and connect Revamp CRM with other apps to power up your sales and marketing efforts. + +# Example Use Cases + +- **Automated Contact Syncing**: When a new contact is added to Revamp CRM, a Pipedream workflow can automatically sync this contact to a Google Sheets spreadsheet. This is great for sharing client lists with teams who may not have access to the CRM, or for creating backup lists. + +- **Lead Qualification and Routing**: Evaluate new leads in Revamp CRM using a Pipedream workflow, which can score leads based on custom criteria. Qualified leads can be routed automatically to the correct sales rep in Slack, ensuring a prompt and organized follow-up. + +- **Task Creation for Follow-ups**: Whenever a sales opportunity reaches a certain stage in Revamp CRM, a Pipedream workflow can create a follow-up task in a project management tool like Trello or Asana. This ensures that no opportunity falls through the cracks and that the sales team stays on top of important tasks. diff --git a/components/revel_systems/README.md b/components/revel_systems/README.md index 25a6fe3d966b8..a34cb0b67469e 100644 --- a/components/revel_systems/README.md +++ b/components/revel_systems/README.md @@ -1,17 +1,11 @@ # Overview -Using the Revel Systems API, it is possible to access point-of-sale functions -and customer relationship management in a single platform. Businesses can -control customer and transaction data across multiple locations anywhere in the -world. +The Revel Systems API provides a rich interface to a robust Point of Sale (POS) system, enabling businesses to automate and streamline operations related to sales, inventory, and customer management. By tapping into this API via Pipedream, you can trigger workflows from real-time events in your POS, sync data across various business apps, and analyze transactional data to glean actionable insights, all in an effort to enhance efficiency and customer experience. -The API can be used in a variety of different ways to create custom solutions -and applications, including: +# Example Use Cases -- Online and In-Store Point-of-Sale Solutions -- Inventory Management and Supply Chain Solutions -- Mobile Ordering, Payment, and Delivery Applications -- Customer Relationship Management Tools -- Corporate Dashboards and Analytics Platforms -- Gift Card and Loyalty Program Management Applications -- And more! +- **Sales Data Analysis and Reporting:** Automatically send daily sales data from Revel Systems to Google Sheets using Pipedream. Apply data transformations in Pipedream to structure the data for analysis, then use Google Sheets to generate reports and visualize trends or identify top-selling products. + +- **Inventory Management:** Create a workflow that monitors inventory levels in Revel Systems. When stock for a particular item dips below a defined threshold, Pipedream triggers an order placement through a supplier's API or sends a restock notification to your Slack channel, ensuring you never run out of popular items. + +- **Customer Loyalty Program Automation:** Integrate Revel Systems with a CRM platform like HubSpot using Pipedream. When a customer reaches a certain spending threshold on Revel Systems, trigger a workflow that updates their loyalty status in HubSpot and sends a personalized reward via email, nurturing customer relationships and encouraging repeat business. diff --git a/components/revel_systems/package.json b/components/revel_systems/package.json new file mode 100644 index 0000000000000..4db6310e0a586 --- /dev/null +++ b/components/revel_systems/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/revel_systems", + "version": "0.6.0", + "description": "Pipedream revel_systems Components", + "main": "revel_systems.app.mjs", + "keywords": [ + "pipedream", + "revel_systems" + ], + "homepage": "https://pipedream.com/apps/revel_systems", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/reversecontact/README.md b/components/reversecontact/README.md new file mode 100644 index 0000000000000..f207640f1f2a3 --- /dev/null +++ b/components/reversecontact/README.md @@ -0,0 +1,11 @@ +# Overview + +The Reverse Contact API unlocks the potential to enrich and reverse-lookup contact details. By feeding it an email, phone number, or social media handle, you can unveil a wealth of associated information, such as other contact methods, social profiles, and more. In Pipedream, you can leverage this API to automate data enrichment for leads, verify user data, or integrate enriched contact data into CRMs or marketing platforms. + +# Example Use Cases + +- **Lead Enrichment in CRM**: Automatically enrich new CRM leads with additional contact details. When a new lead is added to your CRM, trigger a Pipedream workflow that calls the Reverse Contact API to retrieve more info, then updates the CRM record with the new data. + +- **User Verification for E-commerce**: Streamline user verification for e-commerce transactions. On user signup or order placement, use a Pipedream workflow to validate the provided contact info via the Reverse Contact API, reducing fraud and ensuring reliable communication. + +- **Marketing Campaign Personalization**: Tailor marketing campaigns by appending detailed contact profiles. Pipedream can trigger a workflow on campaign creation that enriches the contact list with data from Reverse Contact API, allowing for more targeted and personalized messaging. diff --git a/components/reviewflowz/README.md b/components/reviewflowz/README.md new file mode 100644 index 0000000000000..83b1431fdfb27 --- /dev/null +++ b/components/reviewflowz/README.md @@ -0,0 +1,11 @@ +# Overview + +Reviewflowz API lets you streamline your product review management process. It helps you capture, monitor, and act on customer feedback from multiple platforms in one place. On Pipedream, you can use this API to automate the gathering and organizing of reviews, notify teams about new feedback, and integrate review data into other tools like CRMs or analytics services, turning reviews into actionable insights. + +# Example Use Cases + +- **Automatically Post New Reviews to Slack**: Use Pipedream to create a workflow where new reviews captured by Reviewflowz trigger a notification in a designated Slack channel. This keeps teams instantly informed and ready to act on the latest customer sentiments. + +- **Sync Reviews to a Google Sheet for Analysis**: With Pipedream, you can set up a workflow that automatically adds new reviews to a Google Sheet. This is perfect for businesses that perform regular analysis on customer feedback and want to keep all data in one accessible location. + +- **Create Zendesk Tickets from Negative Reviews**: Develop a Pipedream workflow that watches for new reviews with low ratings and automatically creates tickets in Zendesk. This enables customer support teams to swiftly address concerns and improve customer satisfaction. diff --git a/components/reviews_io/README.md b/components/reviews_io/README.md new file mode 100644 index 0000000000000..fd05a30faa68e --- /dev/null +++ b/components/reviews_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The Reviews.io API allows for automated management and retrieval of review data from the Reviews.io platform. Within Pipedream, you can leverage this API to build serverless workflows that trigger on specific events, integrate with other services, manipulate and analyze review data, and automate responses or notifications. By tapping into Reviews.io via Pipedream, businesses can enhance their customer feedback loop, monitor their reputation, and engage with their reviewers efficiently. + +# Example Use Cases + +- **Sync Reviews to Google Sheets**: Automatically push new reviews collected on Reviews.io into a Google Sheets document. This workflow can help businesses track and analyze customer sentiment over time, easily share feedback with team members, and maintain records for compliance purposes. + +- **Trigger Email Alerts for Negative Feedback**: Set up a Pipedream workflow that listens for new Reviews.io reviews and triggers an email alert via an SMTP service or an app like Sendgrid when a review falls below a certain star rating. This enables fast response to customer complaints and can help in improving customer service and satisfaction. + +- **Post New Reviews to Slack Channels**: Create a Pipedream workflow that posts new positive reviews from Reviews.io to a designated Slack channel to boost team morale and celebrate customer satisfaction. Alternatively, critical reviews can be routed to a customer support channel to prompt immediate action. diff --git a/components/revolt/actions/add-group-member/add-group-member.mjs b/components/revolt/actions/add-group-member/add-group-member.mjs new file mode 100644 index 0000000000000..53f1b058b2fc8 --- /dev/null +++ b/components/revolt/actions/add-group-member/add-group-member.mjs @@ -0,0 +1,35 @@ +import app from "../../revolt.app.mjs"; + +export default { + key: "revolt-add-group-member", + name: "Add Group Member", + description: "Adds another user to the group. [See the documentation](https://developers.revolt.chat/developers/api/reference.html#tag/groups/put/channels/{group_id}/recipients/{member_id})", + version: "0.0.1", + type: "action", + props: { + app, + target: { + propDefinition: [ + app, + "target", + ], + }, + member: { + propDefinition: [ + app, + "member", + ], + }, + }, + async run({ $ }) { + const response = await this.app.addGroupMember({ + $, + target: this.target, + member: this.member, + }); + + $.export("$summary", `Successfully added the user with ID '${this.member}' to the group with ID '${this.target}'`); + + return response; + }, +}; diff --git a/components/revolt/actions/create-group/create-group.mjs b/components/revolt/actions/create-group/create-group.mjs new file mode 100644 index 0000000000000..7eded0f8bd0fc --- /dev/null +++ b/components/revolt/actions/create-group/create-group.mjs @@ -0,0 +1,54 @@ +import app from "../../revolt.app.mjs"; + +export default { + key: "revolt-create-group", + name: "Create Group", + description: "Create a new group channel. [See the documentation](https://developers.revolt.chat/developers/api/reference.html#tag/groups/post/channels/create)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "name", + ], + }, + description: { + propDefinition: [ + app, + "description", + ], + optional: true, + }, + users: { + propDefinition: [ + app, + "users", + ], + optional: true, + }, + nsfw: { + propDefinition: [ + app, + "nsfw", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.app.createGroup({ + $, + data: { + name: this.name, + description: this.description, + users: this.users, + nsfw: this.nsfw, + }, + }); + + $.export("$summary", `Successfully created group with ID: '${response._id}'`); + + return response; + }, +}; diff --git a/components/revolt/actions/send-friend-request/send-friend-request.mjs b/components/revolt/actions/send-friend-request/send-friend-request.mjs new file mode 100644 index 0000000000000..3eb16bfc49c3b --- /dev/null +++ b/components/revolt/actions/send-friend-request/send-friend-request.mjs @@ -0,0 +1,30 @@ +import app from "../../revolt.app.mjs"; + +export default { + key: "revolt-send-friend-request", + name: "Send Friend Request", + description: "Send a friend request to another user. [See the documentation](https://developers.revolt.chat/developers/api/reference.html#tag/relationships/post/users/friend)", + version: "0.0.1", + type: "action", + props: { + app, + username: { + propDefinition: [ + app, + "username", + ], + }, + }, + async run({ $ }) { + const response = await this.app.sendFriendRequest({ + $, + data: { + username: this.username, + }, + }); + + $.export("$summary", `Successfully sent friend request to '${this.username}'`); + + return response; + }, +}; diff --git a/components/revolt/package.json b/components/revolt/package.json new file mode 100644 index 0000000000000..13e61307aa332 --- /dev/null +++ b/components/revolt/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/revolt", + "version": "0.1.0", + "description": "Pipedream Revolt Components", + "main": "revolt.app.mjs", + "keywords": [ + "pipedream", + "revolt" + ], + "homepage": "https://pipedream.com/apps/revolt", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} diff --git a/components/revolt/revolt.app.mjs b/components/revolt/revolt.app.mjs new file mode 100644 index 0000000000000..0567836479f7f --- /dev/null +++ b/components/revolt/revolt.app.mjs @@ -0,0 +1,87 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "revolt", + propDefinitions: { + name: { + type: "string", + label: "Name", + description: "The name of the group", + }, + description: { + type: "string", + label: "Description", + description: "Group description", + }, + users: { + type: "string[]", + label: "Members", + description: "IDs of the users", + }, + nsfw: { + type: "boolean", + label: "NSFW", + description: "Whether this group is age-restricted", + }, + target: { + type: "string", + label: "Target", + description: "ID of the group", + }, + member: { + type: "string", + label: "Member", + description: "ID of the member", + }, + username: { + type: "string", + label: "User name", + description: "Username and discriminator combo separated by #", + }, + }, + methods: { + _baseUrl() { + return "https://revolt.chat/api"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "x-session-token": `${this.$auth.oauth_access_token}`, + }, + }); + }, + async createGroup(args = {}) { + return this._makeRequest({ + method: "post", + path: "/channels/create", + ...args, + }); + }, + async addGroupMember({ + target, member, ...args + }) { + return this._makeRequest({ + method: "put", + path: `/channels/${target}/recipients/${member}`, + ...args, + }); + }, + async sendFriendRequest(args = {}) { + return this._makeRequest({ + method: "post", + path: "/users/friend", + ...args, + }); + }, + }, +}; diff --git a/components/revue/package.json b/components/revue/package.json new file mode 100644 index 0000000000000..2f68cea1e795b --- /dev/null +++ b/components/revue/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/revue", + "version": "0.6.0", + "description": "Pipedream revue Components", + "main": "revue.app.mjs", + "keywords": [ + "pipedream", + "revue" + ], + "homepage": "https://pipedream.com/apps/revue", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/reward_sciences/README.md b/components/reward_sciences/README.md new file mode 100644 index 0000000000000..f443d93b549fc --- /dev/null +++ b/components/reward_sciences/README.md @@ -0,0 +1,11 @@ +# Overview + +The Reward Sciences API enables the integration of loyalty and rewards functionalities into your services. With it, you can create and manage rewards programs, automate point distribution, track user activities, and redeem rewards. On Pipedream, you can tap into these capabilities to build automated workflows that interact with other apps, trigger actions based on events, and manipulate data to enhance customer engagement. + +# Example Use Cases + +- **Automated Reward for Customer Reviews:** After a customer submits a review through a service like Trustpilot, the Reward Sciences API can be triggered on Pipedream to award points to the customer's account, encouraging more feedback and customer interaction. + +- **E-commerce Purchase Rewards:** When a new order is placed in Shopify, use Pipedream to invoke the Reward Sciences API to add points to the customer's loyalty account based on the purchase amount, fostering repeat business. + +- **Referral Program Management:** Integrate a form tool like Typeform with Reward Sciences via Pipedream. When a user submits a referral form, you can automatically grant points to the referrer's account, streamlining your referral incentives. diff --git a/components/reward_sciences/package.json b/components/reward_sciences/package.json index fb33dc76ec335..290a00429c331 100644 --- a/components/reward_sciences/package.json +++ b/components/reward_sciences/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/rewardful/README.md b/components/rewardful/README.md index 989cc4d2a3672..149865133eac7 100644 --- a/components/rewardful/README.md +++ b/components/rewardful/README.md @@ -1,23 +1,11 @@ # Overview -The Rewardful API offers an easy way to build rewards programs into your web -and mobile applications. With Rewardful you can create referral programs that -reward users for spreading the word about your product or service, implement -referral rewards to help drive more traffic to your website, or offer -content-based rewards and discounts. +Rewardful is a potent API for managing affiliate and referral programs directly within your application or platform. By connecting Rewardful with Pipedream, you can automate the tracking, attribution, and compensation of affiliate-driven customer conversions, reducing manual overhead and increasing the efficiency of your marketing efforts. With Rewardful's API, you can create affiliates, track campaigns, and disburse rewards seamlessly. -The Rewardful API makes it easy to create a rich rewards program with functions -like tracking user referrals, sending out reward coupons and offers, and -displaying real-time analytics and insights. +# Example Use Cases -Here are some examples of what you can create with the Rewardful API: +- **Affiliate Onboarding Automation**: When a new affiliate signs up through a form on your website (managed by an app like Typeform), trigger a Pipedream workflow to automatically add the affiliate to Rewardful, set up their campaign, and send them a welcome email with their unique tracking links using an email platform like SendGrid. -- Create referral bonus systems for customers -- Award points for purchases or reaching milestones -- Offer rewards for users who return to your site or complete specific tasks -- Track referrals and develop rewards for advocates -- Offer content-based rewards and discount offers -- Send out coupon codes and in-app notifications to users -- Display real-time analytics and insights on the Rewardful dashboard -- Reward customers for sharing or reviewing your products or services on social - media +- **Real-time Reward Notifications**: Monitor Rewardful for successful referral conversions and use Pipedream to trigger a message to the corresponding affiliate via Slack, notifying them of their successful referral and the reward they've earned, enhancing motivation and engagement. + +- **Monthly Rewardful Analytics Reports**: Schedule a monthly Pipedream workflow that collates all referral data from Rewardful, generates a performance report, and emails it to your marketing team through a service like Mailgun. This workflow could also include a data visualization tool like Google Sheets or Data Studio to create comprehensive analytics dashboards. diff --git a/components/rex/README.md b/components/rex/README.md new file mode 100644 index 0000000000000..a9bfdcaa282c1 --- /dev/null +++ b/components/rex/README.md @@ -0,0 +1,11 @@ +# Overview + +The Rex API allows for automation and integration of real estate CRM functionalities within the Pipedream platform. By leveraging Rex, you can streamline operations, synchronize data across various platforms, and trigger actions within the Rex ecosystem based on events from other apps or services. Whether you're managing listings, contacts, or marketing efforts, the Rex API on Pipedream can be a powerful tool to enhance productivity and customer engagement in real estate businesses. + +# Example Use Cases + +- **Synchronize Contacts Between Rex and Email Marketing Services**: Automatically add or update contacts in an email marketing service like Mailchimp whenever a new contact is added in Rex. This keeps your marketing lists up-to-date and ensures that communications reach the right audience. + +- **Real Estate Listing Alerts**: Configure a workflow that monitors changes in property listings on Rex. When a new property is listed or an existing one is updated, send alerts through platforms like Slack or email to keep the team informed and responsive to market changes. + +- **Automate Task Creation for New Leads**: When a new lead is captured in Rex, use Pipedream to create follow-up tasks in project management tools such as Trello or Asana. This ensures that each lead is promptly addressed, potentially increasing conversion rates. diff --git a/components/richpanel/actions/add-ticket-message/add-ticket-message.mjs b/components/richpanel/actions/add-ticket-message/add-ticket-message.mjs new file mode 100644 index 0000000000000..6bd4f210b0906 --- /dev/null +++ b/components/richpanel/actions/add-ticket-message/add-ticket-message.mjs @@ -0,0 +1,46 @@ +import richpanel from "../../richpanel.app.mjs"; + +export default { + key: "richpanel-add-ticket-message", + name: "Add Ticket Message", + description: "Adds a message to an existing ticket. [See the documentation](https://developer.richpanel.com/reference/update-a-conversation)", + version: "0.0.1", + type: "action", + props: { + richpanel, + conversationId: { + propDefinition: [ + richpanel, + "conversationId", + ], + }, + commentBody: { + propDefinition: [ + richpanel, + "commentBody", + ], + }, + commentSenderType: { + propDefinition: [ + richpanel, + "commentSenderType", + ], + }, + }, + async run({ $ }) { + const response = await this.richpanel.updateTicket({ + $, + conversationId: this.conversationId, + data: { + ticket: { + comment: { + body: this.commentBody, + sender_type: this.commentSenderType, + }, + }, + }, + }); + $.export("$summary", `Added message to ticket ${this.conversationId} successfully`); + return response; + }, +}; diff --git a/components/richpanel/actions/create-ticket/create-ticket.mjs b/components/richpanel/actions/create-ticket/create-ticket.mjs new file mode 100644 index 0000000000000..2f4e7b8518036 --- /dev/null +++ b/components/richpanel/actions/create-ticket/create-ticket.mjs @@ -0,0 +1,206 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { VIA_CHANNEL_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import richpanel from "../../richpanel.app.mjs"; + +export default { + key: "richpanel-create-ticket", + name: "Create Ticket", + description: "Creates a new support ticket in Richpanel. [See the documentation](https://developer.richpanel.com/reference/create-conversation).", + version: "0.0.1", + type: "action", + props: { + richpanel, + id: { + propDefinition: [ + richpanel, + "createId", + ], + optional: true, + }, + status: { + propDefinition: [ + richpanel, + "status", + ], + }, + subject: { + type: "string", + label: "Subject", + description: "The subject of the ticket.", + optional: true, + }, + commentBody: { + propDefinition: [ + richpanel, + "commentBody", + ], + }, + priority: { + type: "string", + label: "Priority", + description: "The priority of the ticket.", + options: [ + "HIGH", + "LOW", + ], + optional: true, + }, + commentSenderType: { + propDefinition: [ + richpanel, + "commentSenderType", + ], + }, + viaChannel: { + type: "string", + label: "Via Channel", + description: "The channel via which the ticket is created.", + reloadProps: true, + options: VIA_CHANNEL_OPTIONS, + }, + viaSourceFromAddress: { + type: "string", + label: "Via Source From Address", + description: "The email address of the source from.", + hidden: true, + }, + viaSourceFromName: { + type: "string", + label: "Via Source From Name", + description: "The name of the source from.", + hidden: true, + }, + viaSourceToAddress: { + type: "string", + label: "Via Source To Address", + description: "The email address of the source to.", + hidden: true, + }, + viaSourceToName: { + type: "string", + label: "Via Source To Name", + description: "The name of the source to.", + hidden: true, + }, + viaSourceFromNumber: { + type: "string", + label: "Via Source From Number", + description: "The phone number of the source from.", + hidden: true, + }, + viaSourceToNumber: { + type: "string", + label: "Via Source To Number", + description: "The phone number of the source to.", + hidden: true, + }, + viaSourceFrom: { + type: "object", + label: "Via Source From", + description: "The object source from which the ticket was created. **Examples: {\"address\": \"abc@email.com\"} or {\"id\": \"+16692668044\"}. It depends on the selected channel**.", + hidden: true, + }, + viaSourceTo: { + type: "object", + label: "Via Source To", + description: "The object source to which the ticket was created. **Examples: {\"address\": \"abc@email.com\"} or {\"id\": \"+16692668044\"}. It depends on the selected channel**.", + hidden: true, + }, + tags: { + propDefinition: [ + richpanel, + "tags", + ], + optional: true, + }, + }, + async additionalProps(props) { + switch (this.viaChannel) { + case "email" : + props.viaSourceFromAddress.hidden = false; + props.viaSourceFromName.hidden = false; + props.viaSourceToAddress.hidden = false; + props.viaSourceToName.hidden = false; + props.viaSourceFrom.hidden = true; + props.viaSourceTo.hidden = true; + props.viaSourceFromNumber.hidden = true; + props.viaSourceToNumber.hidden = true; + break; + case "aircall" : + props.viaSourceFromNumber.hidden = false; + props.viaSourceToNumber.hidden = false; + props.viaSourceFrom.hidden = true; + props.viaSourceTo.hidden = true; + props.viaSourceFromAddress.hidden = true; + props.viaSourceFromName.hidden = true; + props.viaSourceToAddress.hidden = true; + props.viaSourceToName.hidden = true; + break; + default: + props.viaSourceFrom.hidden = false; + props.viaSourceTo.hidden = false; + props.viaSourceFromAddress.hidden = true; + props.viaSourceFromName.hidden = true; + props.viaSourceToAddress.hidden = true; + props.viaSourceToName.hidden = true; + props.viaSourceFromNumber.hidden = true; + props.viaSourceToNumber.hidden = true; + } + return {}; + }, + async run({ $ }) { + try { + const source = {}; + switch (this.viaChannel) { + case "email" : + source.from = { + address: this.viaSourceFromAddress, + name: this.viaSourceFromName, + }; + source.to = { + address: this.viaSourceToAddress, + name: this.viaSourceToName, + }; + break; + case "aircall" : + source.from = { + id: this.viaSourceFromNumber, + }; + source.to = { + id: this.viaSourceToNumber, + }; + break; + default: + source.from = parseObject(this.viaSourceFrom); + source.to = parseObject(this.viaSourceTo); + } + + const response = await this.richpanel.createTicket({ + $, + data: { + ticket: { + id: this.id, + status: this.status, + subject: this.subject, + comment: { + body: this.commentBody, + sender_type: this.commentSenderType, + }, + priority: this.priority, + via: { + channel: this.viaChannel, + source, + }, + tags: parseObject(this.tags), + }, + }, + }); + + $.export("$summary", `Created ticket ${response.ticket.id}`); + return response; + } catch ({ response }) { + throw new ConfigurationError(response?.data?.error?.message); + } + }, +}; diff --git a/components/richpanel/actions/update-ticket-status/update-ticket-status.mjs b/components/richpanel/actions/update-ticket-status/update-ticket-status.mjs new file mode 100644 index 0000000000000..00a7637941308 --- /dev/null +++ b/components/richpanel/actions/update-ticket-status/update-ticket-status.mjs @@ -0,0 +1,37 @@ +import richpanel from "../../richpanel.app.mjs"; + +export default { + key: "richpanel-update-ticket-status", + name: "Update Ticket Status", + description: "Updates the status of an existing ticket in Richpanel. [See the documentation](https://developer.richpanel.com/reference/update-a-conversation).", + version: "0.0.1", + type: "action", + props: { + richpanel, + conversationId: { + propDefinition: [ + richpanel, + "conversationId", + ], + }, + status: { + propDefinition: [ + richpanel, + "status", + ], + }, + }, + async run({ $ }) { + const response = await this.richpanel.updateTicket({ + $, + conversationId: this.conversationId, + data: { + ticket: { + status: this.status, + }, + }, + }); + $.export("$summary", `Updated ticket ${this.conversationId} to status ${this.status}`); + return response; + }, +}; diff --git a/components/richpanel/common/constants.mjs b/components/richpanel/common/constants.mjs new file mode 100644 index 0000000000000..27f1dac482253 --- /dev/null +++ b/components/richpanel/common/constants.mjs @@ -0,0 +1,52 @@ +export const STATUS_OPTIONS = [ + { + label: "Open", + value: "OPEN", + }, + { + label: "Closed", + value: "CLOSED", + }, +]; + +export const COMMENT_SENDER_TYPE_OPTIONS = [ + { + label: "Customer", + value: "customer", + }, + { + label: "Operator", + value: "operator", + }, +]; + +export const VIA_CHANNEL_OPTIONS = [ + { + label: "Email", + value: "email", + }, + { + label: "Messenger", + value: "messenger", + }, + { + label: "Facebook Message", + value: "facebook_message", + }, + { + label: "Instagram", + value: "instagram", + }, + { + label: "Aircall", + value: "aircall", + }, + { + label: "Phone", + value: "phone", + }, + { + label: "WhatsApp", + value: "whatsapp", + }, +]; diff --git a/components/richpanel/common/utils.mjs b/components/richpanel/common/utils.mjs new file mode 100644 index 0000000000000..e7e1c8305652b --- /dev/null +++ b/components/richpanel/common/utils.mjs @@ -0,0 +1,26 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; + +export const camelToSnakeCase = (str) => str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); diff --git a/components/richpanel/package.json b/components/richpanel/package.json new file mode 100644 index 0000000000000..01290215ca3d2 --- /dev/null +++ b/components/richpanel/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/richpanel", + "version": "0.1.0", + "description": "Pipedream Richpanel Components", + "main": "richpanel.app.mjs", + "keywords": [ + "pipedream", + "richpanel" + ], + "homepage": "https://pipedream.com/apps/richpanel", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/richpanel/richpanel.app.mjs b/components/richpanel/richpanel.app.mjs new file mode 100644 index 0000000000000..82811996dc383 --- /dev/null +++ b/components/richpanel/richpanel.app.mjs @@ -0,0 +1,148 @@ +import { axios } from "@pipedream/platform"; +import { + COMMENT_SENDER_TYPE_OPTIONS, + STATUS_OPTIONS, +} from "./common/constants.mjs"; + +export default { + type: "app", + app: "richpanel", + propDefinitions: { + createId: { + type: "string", + label: "Ticket ID", + description: "The ID of the ticket to create", + }, + status: { + type: "string", + label: "Status", + description: "The status of the ticket", + options: STATUS_OPTIONS, + }, + commentBody: { + type: "string", + label: "Comment Body", + description: "The body of the comment for the ticket", + }, + commentSenderType: { + type: "string", + label: "Comment Sender Type", + description: "The sender type of the comment", + options: COMMENT_SENDER_TYPE_OPTIONS, + }, + tags: { + type: "string[]", + label: "Tags", + description: "Tags associated with the new ticket.", + async options() { + const { tag } = await this.listTags(); + + return tag.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + conversationId: { + type: "string", + label: "Ticket ID", + description: "ID of the ticket to update", + async options({ page }) { + const { ticket } = await this.listTickets({ + params: { + page: page + 1, + }, + }); + + console.log("ticket: ", ticket); + + return ticket.map(({ + id: value, subject: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.richpanel.com/v1"; + }, + _headers() { + return { + "x-richpanel-key": this.$auth.api_key, + "Content-Type": "application/json", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createTicket(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/tickets", + ...opts, + }); + }, + listTags() { + return this._makeRequest({ + path: "/tags", + }); + }, + listTickets(opts = {}) { + return this._makeRequest({ + path: "/tickets", + ...opts, + }); + }, + updateTicket({ + conversationId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/tickets/${conversationId}`, + ...opts, + }); + }, + + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const { ticket } = await fn({ + params, + ...opts, + }); + + if (!ticket) { + return; + } + + for (const d of ticket) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = ticket && ticket.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/richpanel/sources/common/base.mjs b/components/richpanel/sources/common/base.mjs new file mode 100644 index 0000000000000..4a04b05f489a2 --- /dev/null +++ b/components/richpanel/sources/common/base.mjs @@ -0,0 +1,72 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import { camelToSnakeCase } from "../../common/utils.mjs"; +import richpanel from "../../richpanel.app.mjs"; + +export default { + props: { + richpanel, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01"; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async prepareData(data) { + const response = []; + for await (const item of data) { + response.push(item); + } + return response; + }, + getParams() { + return {}; + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const dateField = this.getDateField(); + + const response = this.richpanel.paginate({ + fn: this.richpanel.listTickets, + maxResults, + params: { + ...this.getParams(), + startDate: lastDate, + sortKey: dateField, + sortOrder: "DESC", + }, + }); + + let responseArray = await this.prepareData(response, lastDate); + + if (responseArray.length) { + this._setLastDate(responseArray[0][camelToSnakeCase(dateField)]); + } + + for (const item of responseArray.reverse()) { + const dateVal = item[camelToSnakeCase(dateField)]; + this.$emit(item, { + id: `${item.id}-${dateVal}`, + summary: this.getSummary(item), + ts: Date.parse(dateVal), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/richpanel/sources/new-message/new-message.mjs b/components/richpanel/sources/new-message/new-message.mjs new file mode 100644 index 0000000000000..92d6a9161f680 --- /dev/null +++ b/components/richpanel/sources/new-message/new-message.mjs @@ -0,0 +1,37 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "richpanel-new-message", + name: "New Message in Ticket", + description: "Emit new event when a new message is sent on an existing or new ticket.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getDateField() { + return "updatedAt"; + }, + getSummary(item) { + return `New message on ticket ${item.id}`; + }, + async prepareData(data, lastDate) { + const response = []; + for await (const item of data) { + for (const message of item.comments) { + if (Date.parse(message.created_at) > Date.parse(lastDate)) { + response.push({ + ...message, + updated_at: message.created_at, + ticket_id: item.id, + }); + } + } + } + return response; + }, + }, + sampleEmit, +}; diff --git a/components/richpanel/sources/new-message/test-event.mjs b/components/richpanel/sources/new-message/test-event.mjs new file mode 100644 index 0000000000000..899cbd81e9a61 --- /dev/null +++ b/components/richpanel/sources/new-message/test-event.mjs @@ -0,0 +1,16 @@ +export default { + "attachments": [], + "author_id": "0bb92ea8-1ea0-4f85-8d7c-90f183b2636c", + "created_at": "2021-04-14T16:22:51.000Z", + "updated_at": "2021-04-14T16:22:51.000Z", + "body": "made by Anushka Goyal", + "id": "453347427_idontknow436_186779", + "metadata": {}, + "plain_body": "made by Anushka Goyal", + "type": "VoiceComment", + "via": { + "channel": "aircall" + }, + "isOperator": true, + "ticket_id": "453347427_idontknow436", +} \ No newline at end of file diff --git a/components/richpanel/sources/new-ticket/new-ticket.mjs b/components/richpanel/sources/new-ticket/new-ticket.mjs new file mode 100644 index 0000000000000..4f353840ef2b1 --- /dev/null +++ b/components/richpanel/sources/new-ticket/new-ticket.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "richpanel-new-ticket", + name: "New Support Ticket Created", + description: "Emit new event when a support ticket is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getDateField() { + return "createdAt"; + }, + getSummary(item) { + return `New Ticket: ${item.subject}`; + }, + }, + sampleEmit, +}; diff --git a/components/richpanel/sources/new-ticket/test-event.mjs b/components/richpanel/sources/new-ticket/test-event.mjs new file mode 100644 index 0000000000000..4c6f009ff8d81 --- /dev/null +++ b/components/richpanel/sources/new-ticket/test-event.mjs @@ -0,0 +1,30 @@ +export default { + "id": "453347427_idontknow436_186779", + "created_at": "2021-04-14T16:22:51.000Z", + "updated_at": "2021-04-14T16:22:51.000Z", + "assignee_id": "0bb92ea8-1ea0-4f85-8d7c-90f183b2636c", + "organization_id": "idontknow436", + "priority": "LOW", + "recipient": "186779", + "status": "OPEN", + "subject": "Outgoing call to +1 844-902-3197", + "via": {}, + "comments": [ + { + "attachments": [], + "author_id": "0bb92ea8-1ea0-4f85-8d7c-90f183b2636c", + "created_at": "2021-04-14T16:22:51.000Z", + "body": "made by Anushka Goyal", + "id": "453347427_idontknow436_186779", + "metadata": {}, + "plain_body": "made by Anushka Goyal", + "type": "VoiceComment", + "via": { + "channel": "aircall" + }, + "isOperator": true + } + ], + "url": "https://app.richpanel.com/453347427_idontknow436_186779", + "tags": [] +} \ No newline at end of file diff --git a/components/riddle_quiz_maker/README.md b/components/riddle_quiz_maker/README.md index 88d416443ca76..fe4cb3be55db3 100644 --- a/components/riddle_quiz_maker/README.md +++ b/components/riddle_quiz_maker/README.md @@ -1,26 +1,11 @@ # Overview -With Riddle Quiz Maker you can make creating engaging quizzes and surveys with -visuals super easy. The API enables you to quickly and easily build interactive -quizzes and surveys to capture leads, measure results and engage users. +Riddle Quiz Maker API lets you tap into a rich seam of interactive content, from quizzes to polls and surveys. By leveraging this API on Pipedream, you can automate interactions with your quizzes, extract responses in real-time, synchronize data across platforms, and trigger actions based on user engagement. Exploiting Pipedream's serverless platform allows you to create workflows that respond swiftly, integrate with numerous apps, and streamline content management—turning audience insights into actionable intelligence. -With the Riddle Quiz Maker API, you can quickly and easily create: +# Example Use Cases -- Lead Generation Quizzes: Ask insightful questions to capture leads and better - understand your audience -- Personality Quizzes: Engage and entertain with personality quizzes that share - stories of users’ results -- Video Quizzes: Create interactive video quizzes to encourage users to watch - and engage with a video -- Knowledge Tests: Challenge your community or students with knowledge tests - that test a user's expertise -- Survey Platforms: Get valuable insights and feedback from your audience & - customers -- Branded Ads & Competitions: Incentivize users to join competitions, enter - contests or answer ads with our quizzes -- Learning Assessments: Assess user progress and teach them more with scoring - quizzes and surveys +- **Content Engagement Trigger for Email Campaigns**: When a user completes a quiz, you could trigger a workflow that adds or updates their information in a Mailchimp audience list, including their quiz results. This allows for personalized follow-up emails, tailored content delivery, or segmenting users based on their preferences. -With the Riddle Quiz Maker API, you can create intelligent, dynamic quizzes to -engage with your audience and collect valuable insights that help grow your -business. Try it today! +- **Real-time Analytics Dashboard Update**: After a new set of quiz responses is collected, you could automate the transfer of this data to a Google Sheets spreadsheet. From there, it’s possible to visualize this data using tools like Google Data Studio, providing up-to-date analytics to inform marketing strategies or content creation. + +- **Lead Scoring and CRM Integration**: Use quiz results to score leads and automatically push this information into a CRM like Salesforce. Scoring can be based on the user's answers, indicating their interest level or fit for a product or service, allowing for more efficient lead prioritization and follow-up by the sales team. diff --git a/components/riddle_quiz_maker/package.json b/components/riddle_quiz_maker/package.json new file mode 100644 index 0000000000000..05191a56802bf --- /dev/null +++ b/components/riddle_quiz_maker/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/riddle_quiz_maker", + "version": "0.6.0", + "description": "Pipedream riddle_quiz_maker Components", + "main": "riddle_quiz_maker.app.mjs", + "keywords": [ + "pipedream", + "riddle_quiz_maker" + ], + "homepage": "https://pipedream.com/apps/riddle_quiz_maker", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/ringba/README.md b/components/ringba/README.md index aab60b392d24c..b11859d43c1e2 100644 --- a/components/ringba/README.md +++ b/components/ringba/README.md @@ -1,21 +1,11 @@ # Overview -The Ringba API is a cloud-native tool that helps businesses around the world -better manage, analyze, and optimize their website operations. With the Ringba -API, businesses can easily incorporate dynamic features, such as dynamic -routing, ad trafficking, and analytics, into their websites to improve customer -experience and maximize profits. Here are some examples of what you can build -with the Ringba API: +The Ringba API provides programmatic access to call tracking and management functionalities, allowing users to automate their telephony operations, analyze call data, and streamline caller experiences. By leveraging the Ringba API on Pipedream, you can create custom serverless workflows that respond dynamically to call events, synchronize call details with CRM systems, generate real-time analytics, and optimize marketing efforts based on call patterns. -- Dynamic Routing: Route customers to different landing pages or offers - depending on their traffic source and other attributes. -- Website Tracking: Monitor website performance and user behaviors to optimize - engagement and conversions. -- Ad Trafficking and Management: Manage and optimize ad campaigns from one - central hub, including trafficking, monitoring, and reporting. -- Real-time Call Tracking: Monitor inbound and outbound calls to better - identify and serve customers. -- Call Routing: Route inbound phone calls to the appropriate agents based on - customer filters. -- Custom reporting: Generate custom reports to show key performance statistics - from a centralized dashboard. +# Example Use Cases + +- **Real-Time Call Data Sync to CRM**: When a call is completed in Ringba, use Pipedream to automatically capture the call details and log them in a CRM like Salesforce. Pipedream can parse call metadata, such as caller ID, call duration, and outcome, and then map this data to the corresponding CRM record, ensuring sales teams have real-time access to call interactions. + +- **Dynamic Call Routing Based on Marketing Campaigns**: Use Pipedream to analyze incoming call data against active marketing campaigns. If a caller ID or dialed number matches a specific campaign, you can trigger a workflow that dynamically routes the call to the most appropriate agent or team, optimizing caller experience and campaign performance. + +- **Automated Call Analytics Reporting**: With Pipedream, set up a workflow that aggregates call data from Ringba at regular intervals, processes it to compute key performance metrics like call volume trends, conversion rates, and caller demographics, and then compiles the insights into a report that can be automatically sent to stakeholders via email or Slack. diff --git a/components/ringba/package.json b/components/ringba/package.json new file mode 100644 index 0000000000000..89973e67207af --- /dev/null +++ b/components/ringba/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/ringba", + "version": "0.6.0", + "description": "Pipedream ringba Components", + "main": "ringba.app.mjs", + "keywords": [ + "pipedream", + "ringba" + ], + "homepage": "https://pipedream.com/apps/ringba", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/ringcentral/README.md b/components/ringcentral/README.md index 6c5d02a571a93..62dfea31e9f45 100644 --- a/components/ringcentral/README.md +++ b/components/ringcentral/README.md @@ -1,18 +1,11 @@ # Overview -RingCentral API is a powerful tool that allows users to build applications and -solutions that make communication easier and more convenient. With RingCentral -API, developers can build communication solutions that include voice and video, -SMS/MMS, Fax, and other features. +RingCentral is a comprehensive cloud communication platform that melds messaging, video, and phone services. Integrating RingCentral with Pipedream allows for automated workflows involving SMS sending, call management, and leveraging communication logs for data-driven insights. With Pipedream's serverless execution model, you can trigger actions based on specific RingCentral events or schedule tasks to run at predetermined intervals, all without writing extensive code. -With RingCentral API, developers can build: +# Example Use Cases -- Voice and Video calls with advanced interactions, such as ringback tones, - call screening, call forwarding, call recording and conference calling. -- Rich messaging applications leveraging SMS, MMS, and chat functionality. -- Fax solutions, such as sending and receiving faxes through a web application. -- Presence and availability services, allowing users to set their status and - view the status of other users. -- Automated voice solutions such as voice recognition, voice menus and more. -- Voice analytics and call quality monitoring. -- Contact center solutions with the ability to monitor and manage queues. +- **Customer Support Ticket Trigger**: When a missed call is detected on RingCentral, create a support ticket in Zendesk. This ensures timely follow-up and maintains high customer service standards. + +- **SMS Campaign Analytics**: Capture SMS messages sent from RingCentral, store them with Pipedream's built-in data stores, and analyze the data for campaign performance. Integrate with Google Sheets to easily share insights across your team. + +- **Automated Meeting Reminders**: Before a scheduled meeting, use RingCentral API to send a reminder via SMS to all participants. Sync meeting details from a Google Calendar event to ensure reminders are accurate and timely. diff --git a/components/ringover/README.md b/components/ringover/README.md new file mode 100644 index 0000000000000..9ca60bc55c301 --- /dev/null +++ b/components/ringover/README.md @@ -0,0 +1,14 @@ +# Overview + +The Ringover API allows you to automate and integrate telephony features with your existing apps and services. With Pipedream and Ringover, you can create serverless workflows that handle voice and SMS events, manage contacts, and analyze call data without the need for a backend infrastructure. By leveraging Pipedream's capability to connect with various APIs, you can sync Ringover activities with CRM systems, support tickets, messaging platforms, and more, all in real time. + +# Example Use Cases + +**Log Incoming Calls in Google Sheets** +Automatically log details of incoming calls in a Google Sheet for record-keeping and analysis. Each time a call is received on Ringover, a new row is added to the sheet with the caller's information and call duration. + +**Sync Missed Calls with Slack Notifications** +Stay on top of missed calls by creating a workflow that sends a Slack message to a designated channel or user when a call is missed on Ringover. This ensures immediate awareness and follow-up. + +**Create Contacts from Sign-Up Forms** +When a user signs up via a web form, automatically create a new contact in Ringover's address book. This can help sales and support teams to quickly reach out to new leads or customers with streamlined communication. diff --git a/components/rise/README.md b/components/rise/README.md index fa30859e7048b..0c6672ac77cf7 100644 --- a/components/rise/README.md +++ b/components/rise/README.md @@ -1,22 +1,11 @@ # Overview -The Rise API gives developers the tools to create custom financial applications -for users. From basic budgeting and expense tracking apps to complex digital -asset portfolios, the Rise API can help you develop the perfect application for -users. +The Rise API empowers users to automate and integrate their learning management system workflows with ease. With this API, you can manipulate course data, track user progress, and manage enrollments without manual intervention. Imagine the possibilities when you connect Rise to your HR system, Customer Support platform, or Content Management System. Streamline onboarding, keep tabs on employee training, and ensure compliance requirements are met seamlessly. -With the Rise API, users can securely access their financial accounts, gain -insights into their spending habits, and programmatically move money into and -out of accounts. +# Example Use Cases -Here are some examples of applications you can build using the Rise API: +- **Automated Employee Onboarding and Training Programs**: Trigger an automated workflow in Pipedream to enroll new hires in essential training courses as soon as they are added to your HR system. Courses can be assigned based on role, department, or location, and progress can be reported back to managers or HR for compliance tracking. -- Automated investment portfolios -- Automatically sync bank accounts with your app -- Personalized budgeting and expense tracking -- Real-time financial insights and predictions -- Create a digital wallet to store and make payments -- Automate tax filing and direct payment services -- Bank-grade authentication for app users -- Reward-based savings and investment accounts -- And much more! +- **Customer Education Tracking**: Connect Rise with your Customer Relationship Management (CRM) system. When a customer signs up for your product or service, automatically enroll them in a product tutorial or training course. Monitor their progress and send follow-up reminders or additional resources based on their engagement with the course materials. + +- **Content Update Notifications**: Set up a workflow to monitor changes in course content. Whenever a course is updated on Rise, automatically notify relevant teams or individuals via email, Slack, or other communication platforms. This ensures all stakeholders are kept in the loop about the latest training content and materials. diff --git a/components/riskadvisor/README.md b/components/riskadvisor/README.md new file mode 100644 index 0000000000000..7c596b4bc809b --- /dev/null +++ b/components/riskadvisor/README.md @@ -0,0 +1,11 @@ +# Overview + +The RiskAdvisor API offers a way to integrate insurance advisory services into your applications seamlessly. By leveraging this API within Pipedream, you can automate the process of obtaining insurance quotes, processing claims, and providing personalized insurance advice. Pipedream's serverless execution environment allows you to create workflows that react to various triggers (such as webhooks, emails, or schedules) and interact with the RiskAdvisor API to carry out tasks without manual intervention. This can streamline operations for insurance agencies, fintech apps, or any business that needs to integrate insurance-related services. + +# Example Use Cases + +- **Automated Insurance Quote Generation**: Create a workflow that triggers when a customer submits a form on your site. Collect the data and send it to the RiskAdvisor API to get an insurance quote, then email the quote to the customer and save it to a Google Sheet for tracking. + +- **Claims Processing Automation**: Set up a workflow that kicks off when a customer files a claim via your app. Use the RiskAdvisor API to submit the claim details, and then connect to a CRM like Salesforce to update the customer's record with the claim status. + +- **Periodic Insurance Advice Newsletter**: Build a workflow that runs on a schedule to fetch the latest insurance advice using the RiskAdvisor API. Format the advice into an email-friendly layout and send a newsletter to subscribers through an email service like SendGrid or Mailgun. diff --git a/components/ritekit/README.md b/components/ritekit/README.md index 153eab1d10e22..9c749a05cadbf 100644 --- a/components/ritekit/README.md +++ b/components/ritekit/README.md @@ -1,20 +1,11 @@ # Overview -RiteKit can help you harness the power of social media through its powerful -API. With RiteKit's API, you can easily build a variety of solutions that help -you better understand and engage with audiences across multiple social -networks. Here are just some of the things you can easily create using the -Ritekit API: +RiteKit API offers a suite of tools to enrich social media engagement. Through Pipedream, you can automate hashtag suggestions, generate images for social posts, and retrieve analytics. This integration can power up your social media strategy by automating tasks like crafting posts with trending hashtags, scheduling content with optimized images, and analyzing performance without manual effort. -- Monitor brand mentions and sentiment across multiple channels -- Gather customer insights on social media trends -- Optimize content strategies -- Automate creation and publishing of content -- Manage social media accounts -- Utilize AI-powered search solutions -- Track competitor performance on social media -- Improve influencer marketing campaigns -- Generate real-time analytics reports -- Retarget potential customers on social media -- Measure the success of campaigns with multiple metrics -- Monitor the content propagation in real-time +# Example Use Cases + +- **Auto-Generated Social Media Posts with Trending Hashtags**: Use RiteKit API to get trending hashtags based on your content. Set up a workflow that triggers when you draft a new blog post in your CMS (like WordPress). The API suggests relevant hashtags, which Pipedream adds to your social media posts queued in Buffer or Hootsuite. + +- **Enhanced Blog Post Sharing with Auto-Created Images**: When a new article is published on your blog, trigger a workflow that uses the RiteKit API to generate an image with the article's title and author. This image is then automatically tweeted via the Twitter API, making the shared content more engaging and likely to stand out. + +- **Social Media Performance Analytics to Google Sheets**: Schedule a daily or weekly Pipedream workflow that calls RiteKit's analytics endpoints for your social media accounts. Aggregate the data and push it to Google Sheets, where you can keep a running log of engagement metrics to inform your content strategy. diff --git a/components/ritekit/package.json b/components/ritekit/package.json new file mode 100644 index 0000000000000..b761bd6be5e5a --- /dev/null +++ b/components/ritekit/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/ritekit", + "version": "0.6.0", + "description": "Pipedream ritekit Components", + "main": "ritekit.app.mjs", + "keywords": [ + "pipedream", + "ritekit" + ], + "homepage": "https://pipedream.com/apps/ritekit", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/rkvst/README.md b/components/rkvst/README.md new file mode 100644 index 0000000000000..a03c4a30c51e3 --- /dev/null +++ b/components/rkvst/README.md @@ -0,0 +1,11 @@ +# Overview + +The DataTrails API, formerly known as RKVST, is designed to track the provenance and integrity of digital assets across their lifecycle. With this API, you can create an immutable record of actions, decisions, and processes that affect any asset, thereby providing an auditable trail. Leveraging Pipedream, you can automate workflows that require secure, verifiable, and traceable asset history. You can react to events in real-time, integrate with other services, and trigger actions based on asset changes or compliance checks. + +# Example Use Cases + +- **Automated Compliance Reporting**: Create a workflow that triggers whenever a new compliance report is generated in DataTrails. The report can be sent automatically to relevant stakeholders via email (using the Pipedream Email by Zapier app) and saved to cloud storage solutions like Dropbox for easy access and archiving. + +- **Asset Change Notifications**: Set up a Pipedream workflow that listens for asset status changes in DataTrails. When a change occurs, send a notification through Slack or Microsoft Teams to inform the team promptly, ensuring that everyone is aware of the latest asset state and can act accordingly. + +- **Continuous Integrity Verification**: Develop a workflow where Pipedream periodically checks the integrity of an asset using DataTrails API. If any discrepancies are found, the workflow can create an incident ticket in a system like Jira or ServiceNow, initiating a review process to investigate and remediate any issues. diff --git a/components/roam_research/README.md b/components/roam_research/README.md new file mode 100644 index 0000000000000..8d46332ef1fdc --- /dev/null +++ b/components/roam_research/README.md @@ -0,0 +1,11 @@ +# Overview + +The Roam Research API lets you interact with Roam's knowledge graph, enabling programmatic access for creating, querying, and modifying notes. This capability opens up automated workflows that can enrich your notes with data from various sources, sync your Roam database with external tools, or even trigger actions based on updates within your notes. + +# Example Use Cases + +- **Automated Daily Notes Creation**: Use Pipedream's cron job feature to trigger a workflow that automatically creates a new note in Roam Research every morning, pre-populated with your custom template including tasks, goals, or any relevant links and resources for the day. + +- **Content Aggregator**: Combine Roam Research with RSS feeds or any news API available on Pipedream. Set up a workflow that fetches the latest news articles or blog posts and adds them as bullet points or pages in your Roam Research database, ensuring you never miss important updates in your fields of interest. + +- **Twitter Integration for Idea Capture**: Connect Roam Research with Twitter using Pipedream. Whenever you like a tweet, the workflow saves the tweet's content as a note in your Roam database. This can be a great way to capture ideas and inspirations on the go, essentially using Twitter as a funnel into your personal knowledge management system. diff --git a/components/roam_research/package.json b/components/roam_research/package.json index 5cf17545e18af..47b328b71ce49 100644 --- a/components/roam_research/package.json +++ b/components/roam_research/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/roamresearch/actions/add-content-to-daily-note-page/add-content-to-daily-note-page.mjs b/components/roamresearch/actions/add-content-to-daily-note-page/add-content-to-daily-note-page.mjs new file mode 100644 index 0000000000000..7a390bad1db72 --- /dev/null +++ b/components/roamresearch/actions/add-content-to-daily-note-page/add-content-to-daily-note-page.mjs @@ -0,0 +1,63 @@ +import app from "../../roamresearch.app.mjs"; + +export default { + key: "roamresearch-add-content-to-daily-note-page", + name: "Add Content To Daily Note Page", + description: "Adds content as a child block to a daily note page in Roam Research (access to encrypted and non encrypted graphs). [See the documentation](https://roamresearch.com/#/app/developer-documentation/page/eb8OVhaFC).", + version: "0.0.2", + type: "action", + props: { + app, + dailyNoteTitle: { + type: "string", + label: "Date For Daily Note", + description: "The date for the daily note page title, formatted as `MM-DD-YYYY`. Keep in mind the Daily Note page should exist.", + }, + content: { + propDefinition: [ + app, + "content", + ], + }, + nestUnder: { + propDefinition: [ + app, + "nestUnder", + ], + }, + }, + async run({ $ }) { + const { + app, + dailyNoteTitle, + content, + nestUnder, + } = this; + + const response = app.appendBlocks({ + $, + data: { + "location": { + page: { + title: { + "daily-note-page": dailyNoteTitle, + }, + }, + ...(nestUnder && { + "nest-under": { + string: nestUnder, + }, + }), + }, + "append-data": [ + { + string: content, + }, + ], + }, + }); + + $.export("$summary", "Successfully added content to daily note page."); + return response; + }, +}; diff --git a/components/roamresearch/actions/add-content-to-page/add-content-to-page.mjs b/components/roamresearch/actions/add-content-to-page/add-content-to-page.mjs new file mode 100644 index 0000000000000..dd22bbf9a5606 --- /dev/null +++ b/components/roamresearch/actions/add-content-to-page/add-content-to-page.mjs @@ -0,0 +1,61 @@ +import app from "../../roamresearch.app.mjs"; + +export default { + key: "roamresearch-add-content-to-page", + name: "Add Content To Page", + description: "Add content as a child block to an existing or new page in Roam Research (access to encrypted and non encrypted graphs). [See the documentation](https://roamresearch.com/#/app/developer-documentation/page/eb8OVhaFC).", + version: "0.0.2", + type: "action", + props: { + app, + title: { + type: "string", + label: "Page Title", + description: "Title of the page to add content to.", + }, + content: { + propDefinition: [ + app, + "content", + ], + }, + nestUnder: { + propDefinition: [ + app, + "nestUnder", + ], + }, + }, + async run({ $ }) { + const { + app, + title, + content, + nestUnder, + } = this; + + const response = app.appendBlocks({ + $, + data: { + "location": { + page: { + title, + }, + ...(nestUnder && { + "nest-under": { + string: nestUnder, + }, + }), + }, + "append-data": [ + { + string: content, + }, + ], + }, + }); + + $.export("$summary", "Successfully added content to page."); + return response; + }, +}; diff --git a/components/roamresearch/actions/add-content-underneath-block/add-content-underneath-block.mjs b/components/roamresearch/actions/add-content-underneath-block/add-content-underneath-block.mjs new file mode 100644 index 0000000000000..405b0274b0619 --- /dev/null +++ b/components/roamresearch/actions/add-content-underneath-block/add-content-underneath-block.mjs @@ -0,0 +1,60 @@ +import app from "../../roamresearch.app.mjs"; + +export default { + key: "roamresearch-add-content-underneath-block", + name: "Add Content Underneath Block", + description: "Add content underneath an existing block in your Roam Research graph (access to encrypted and non encrypted graphs). [See the documentation](https://roamresearch.com/)", + version: "0.0.2", + type: "action", + props: { + app, + blockUid: { + type: "string", + label: "Block UID", + description: "The block UID in the Roam graph you want to append content to.", + }, + content: { + type: "string", + label: "Content", + description: "The content of the block to be added.", + }, + nestUnder: { + type: "string", + label: "Nest Under", + description: "Title of the block to nest the new block under.", + optional: true, + }, + }, + async run({ $ }) { + const { + app, + blockUid, + content, + nestUnder, + } = this; + + const response = app.appendBlocks({ + $, + data: { + "location": { + block: { + uid: blockUid, + }, + ...(nestUnder && { + "nest-under": { + string: nestUnder, + }, + }), + }, + "append-data": [ + { + string: content, + }, + ], + }, + }); + + $.export("$summary", "Successfully added content underneath block."); + return response; + }, +}; diff --git a/components/roamresearch/actions/append-blocks/append-blocks.mjs b/components/roamresearch/actions/append-blocks/append-blocks.mjs new file mode 100644 index 0000000000000..1a807828ade77 --- /dev/null +++ b/components/roamresearch/actions/append-blocks/append-blocks.mjs @@ -0,0 +1,41 @@ +import utils from "../../common/utils.mjs"; +import app from "../../roamresearch.app.mjs"; + +export default { + key: "roamresearch-append-blocks", + name: "Append Blocks", + description: "Generic append blocks for Roam Research pages. [See the documentation](https://roamresearch.com/#/app/developer-documentation/page/kjnAseQ-K).", + version: "0.0.1", + type: "action", + props: { + app, + location: { + type: "object", + label: "Location", + description: "The location to append the block to. [See the documentation](https://roamresearch.com/#/app/developer-documentation/page/kjnAseQ-K).", + }, + appendData: { + type: "string[]", + label: "Append Data", + description: "The data to append to the block. [See the documentation](https://roamresearch.com/#/app/developer-documentation/page/kjnAseQ-K).", + }, + }, + async run({ $ }) { + const { + app, + location, + appendData, + } = this; + + const response = await app.appendBlocks({ + $, + data: { + location, + ["append-data"]: utils.parseArray(appendData), + }, + }); + + $.export("$summary", "Successfully ran append blocks."); + return response; + }, +}; diff --git a/components/roamresearch/actions/get-page-or-block-data/get-page-or-block-data.mjs b/components/roamresearch/actions/get-page-or-block-data/get-page-or-block-data.mjs new file mode 100644 index 0000000000000..b4fc68ee96a48 --- /dev/null +++ b/components/roamresearch/actions/get-page-or-block-data/get-page-or-block-data.mjs @@ -0,0 +1,51 @@ +import app from "../../roamresearch.app.mjs"; + +export default { + key: "roamresearch-get-page-or-block-data", + name: "Get Page Or Block Data", + description: "Get the data for a page or block in Roam Research (access only to non ecrypted graphs). [See the documentation](https://roamresearch.com/#/app/developer-documentation/page/eb8OVhaFC).", + version: "0.0.2", + type: "action", + props: { + app, + resourceType: { + propDefinition: [ + app, + "resourceType", + ], + }, + pageOrBlock: { + propDefinition: [ + app, + "pageOrBlock", + ], + }, + }, + async run({ $ }) { + const { + app, + resourceType, + pageOrBlock, + } = this; + + const attribute = resourceType === "page" + ? ":node/title" + : ":block/uid"; + + const response = await app.pull({ + $, + data: { + selector: `[${attribute} :block/string :block/order {:block/children ...}]`, + eid: `[${attribute} "${pageOrBlock}"]`, + }, + }); + + if (!response.result) { + $.export("$summary", `Failed to get data for ${resourceType}: \`${pageOrBlock}\`.`); + return response; + } + + $.export("$summary", `Successfully got data for ${resourceType}: \`${pageOrBlock}\`.`); + return response; + }, +}; diff --git a/components/roamresearch/actions/pull-many/pull-many.mjs b/components/roamresearch/actions/pull-many/pull-many.mjs new file mode 100644 index 0000000000000..50d4f2a2fbbe2 --- /dev/null +++ b/components/roamresearch/actions/pull-many/pull-many.mjs @@ -0,0 +1,45 @@ +import app from "../../roamresearch.app.mjs"; + +export default { + key: "roamresearch-pull-many", + name: "Pull Many", + description: "Generic pull many for Roam Research pages. [See the documentation](https://roamresearch.com/#/app/developer-documentation/page/mdnjFsqoA).", + version: "0.0.1", + type: "action", + props: { + app, + eids: { + type: "string", + label: "Entity IDs", + description: "The entity IDs to pull. Eg. `[[:block/uid \"08-30-2022\"] [:block/uid \"08-31-2022\"]]`.", + }, + selector: { + type: "string", + label: "Selector", + description: "The selector to pull. Eg. `[:block/uid :node/title :block/string {:block/children [:block/uid :block/string]} {:block/refs [:node/title :block/string :block/uid]}]`.", + }, + }, + async run({ $ }) { + const { + app, + eids, + selector, + } = this; + + const response = await app.pullMany({ + $, + data: { + eids, + selector, + }, + }); + + if (!response.result) { + $.export("$summary", `Failed to pull many data for entity IDs: \`${eids}\`.`); + return response; + } + + $.export("$summary", "Successfully ran pull many."); + return response; + }, +}; diff --git a/components/roamresearch/actions/pull/pull.mjs b/components/roamresearch/actions/pull/pull.mjs new file mode 100644 index 0000000000000..e0e9fd8f6be29 --- /dev/null +++ b/components/roamresearch/actions/pull/pull.mjs @@ -0,0 +1,45 @@ +import app from "../../roamresearch.app.mjs"; + +export default { + key: "roamresearch-pull", + name: "Pull", + description: "Generic pull for Roam Research pages. [See the documentation](https://roamresearch.com/#/app/developer-documentation/page/mdnjFsqoA).", + version: "0.0.1", + type: "action", + props: { + app, + eid: { + type: "string", + label: "Entity ID", + description: "The entity ID to pull. Eg. `[:block/uid \"08-30-2022\"]`.", + }, + selector: { + type: "string", + label: "Selector", + description: "The selector to pull. Eg. `[:block/uid :node/title :block/string {:block/children [:block/uid :block/string]} {:block/refs [:node/title :block/string :block/uid]}]`.", + }, + }, + async run({ $ }) { + const { + app, + eid, + selector, + } = this; + + const response = await app.pull({ + $, + data: { + eid, + selector, + }, + }); + + if (!response.result) { + $.export("$summary", `Failed to pull data for entity ID: \`${eid}\`.`); + return response; + } + + $.export("$summary", "Successfully ran pull."); + return response; + }, +}; diff --git a/components/roamresearch/actions/query/query.mjs b/components/roamresearch/actions/query/query.mjs new file mode 100644 index 0000000000000..e1d9d743474b3 --- /dev/null +++ b/components/roamresearch/actions/query/query.mjs @@ -0,0 +1,40 @@ +import app from "../../roamresearch.app.mjs"; + +export default { + key: "roamresearch-query", + name: "Query", + description: "Generic query for Roam Research pages. [See the documentation](https://roamresearch.com/#/app/developer-documentation/page/mdnjFsqoA).", + version: "0.0.1", + type: "action", + props: { + app, + query: { + type: "string", + label: "Query", + description: "The query to run in Datalog language. Eg. `[:find ?block-uid ?block-str :in $ ?search-string :where [?b :block/uid ?block-uid] [?b :block/string ?block-str] [(clojure.string/includes? ?block-str ?search-string)]]`.", + }, + args: { + type: "string[]", + label: "Arguments", + description: "The arguments to pass to the query. Eg. `apple` as the firs argument.", + }, + }, + async run({ $ }) { + const { + app, + query, + args, + } = this; + + const response = await app.query({ + $, + data: { + query, + args, + }, + }); + + $.export("$summary", "Successfully ran query."); + return response; + }, +}; diff --git a/components/roamresearch/actions/search-title/search-title.mjs b/components/roamresearch/actions/search-title/search-title.mjs new file mode 100644 index 0000000000000..47b7ccfc86365 --- /dev/null +++ b/components/roamresearch/actions/search-title/search-title.mjs @@ -0,0 +1,41 @@ +import app from "../../roamresearch.app.mjs"; + +export default { + key: "roamresearch-search-title", + name: "Search Title", + description: "Search for a title in Roam Research pages (access only to non ecrypted graphs). [See the documentation](https://roamresearch.com/#/app/developer-documentation/page/eb8OVhaFC).", + version: "0.0.2", + type: "action", + props: { + app, + title: { + type: "string", + label: "Search Title", + description: "The title to search for.", + }, + }, + async run({ $ }) { + const { + app, + title, + } = this; + const response = await app.query({ + $, + data: { + query: `[ + :find (pull ?b [:block/uid :node/title]) + :in $ ?search-string + :where [?b :node/title ?page-title] [ + (clojure.string/includes? ?page-title ?search-string) + ] + ]`, + args: [ + title, + ], + }, + }); + + $.export("$summary", `Successfully searched for title: \`${title}\`.`); + return response; + }, +}; diff --git a/components/roamresearch/actions/write/write.mjs b/components/roamresearch/actions/write/write.mjs new file mode 100644 index 0000000000000..dd698c0c6cdda --- /dev/null +++ b/components/roamresearch/actions/write/write.mjs @@ -0,0 +1,185 @@ +import app from "../../roamresearch.app.mjs"; +import utils from "../../common/utils.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "roamresearch-write", + name: "Write", + description: "Generic write for Roam Research pages. [See the documentation](https://roamresearch.com/#/app/developer-documentation/page/mdnjFsqoA).", + version: "0.0.1", + type: "action", + props: { + app, + action: { + type: "string", + label: "Action", + description: "The action to run. Eg. `create-block`.", + options: Object.values(constants.ACTION), + reloadProps: true, + }, + }, + additionalProps() { + const { action } = this; + + if (action === constants.ACTION.CREATE_BLOCK) { + return { + location: { + type: "object", + label: "Location", + description: "The location to create the block where `order` is required and either `parent-uid` or `page-title` is required.", + default: { + ["parent-uid"]: "optional", + ["page-title"]: "optional", + order: "last", + }, + }, + block: { + type: "object", + label: "Block", + description: "The block to create where `string` is required.", + default: { + string: "required", + uid: "optional", + open: "optional", + heading: "optional", + ["text-align"]: "optional", + ["children-view-type"]: "optional", + }, + }, + }; + } + + if (action === constants.ACTION.MOVE_BLOCK) { + return { + location: { + type: "object", + label: "Location", + description: "The location to move the block where `order` is required and either `parent-uid` or `page-title` is required.", + default: { + ["parent-uid"]: "optional", + ["page-title"]: "optional", + order: "last", + }, + }, + block: { + type: "object", + label: "Block", + description: "The block to move where `uid` is required.", + default: { + uid: "required", + }, + }, + }; + } + + if (action === constants.ACTION.UPDATE_BLOCK) { + return { + block: { + type: "object", + label: "Block", + description: "The block to update where `uid` is required.", + default: { + uid: "required", + string: "optional", + open: "optional", + heading: "optional", + ["text-align"]: "optional", + ["children-view-type"]: "optional", + }, + }, + }; + } + + if (action === constants.ACTION.DELETE_BLOCK) { + return { + block: { + type: "object", + label: "Block", + description: "The block to delete where `uid` is required.", + default: { + uid: "required", + }, + }, + }; + } + + if (action === constants.ACTION.CREATE_PAGE) { + return { + page: { + type: "object", + label: "Page", + description: "The page to create where `title` is required.", + default: { + title: "required", + uid: "optional", + ["children-view-type"]: "optional", + }, + }, + }; + } + + if (action === constants.ACTION.UPDATE_PAGE) { + return { + page: { + type: "object", + label: "Page", + description: "The page to update where `uid` is required.", + default: { + uid: "required", + title: "optional", + ["children-view-type"]: "optional", + }, + }, + }; + } + + if (action === constants.ACTION.DELETE_PAGE) { + return { + page: { + type: "object", + label: "Page", + description: "The page to delete where `uid` is required.", + default: { + uid: "required", + }, + }, + }; + } + + if (action === constants.ACTION.BATCH_ACTIONS) { + return { + actions: { + type: "string[]", + label: "Actions", + description: "The actions to run in batch. Eg. `{ \"action\": \"create-block\", \"location\": {...}, \"block\": {...} }`", + }, + }; + } + + return {}; + }, + async run({ $ }) { + const { + app, + action, + location, + block, + page, + actions, + } = this; + + const response = await app.write({ + $, + data: { + action, + location, + block, + page, + actions: utils.parseArray(actions), + }, + }); + + $.export("$summary", "Successfully ran the action."); + return response; + }, +}; diff --git a/components/roamresearch/common/constants.mjs b/components/roamresearch/common/constants.mjs new file mode 100644 index 0000000000000..dd068eea7592b --- /dev/null +++ b/components/roamresearch/common/constants.mjs @@ -0,0 +1,27 @@ +const SUBDOMAIN_PLACEHOLDER = "{subdomain}"; +const BASE_URL = `https://${SUBDOMAIN_PLACEHOLDER}.roamresearch.com`; +const VERSION_PATH = "/api/graph"; + +const API = { + DEFAULT: "api", + APPEND: "append-api", +}; + +const ACTION = { + CREATE_BLOCK: "create-block", + MOVE_BLOCK: "move-block", + UPDATE_BLOCK: "update-block", + DELETE_BLOCK: "delete-block", + CREATE_PAGE: "create-page", + UPDATE_PAGE: "update-page", + DELETE_PAGE: "delete-page", + BATCH_ACTIONS: "batch-actions", +}; + +export default { + SUBDOMAIN_PLACEHOLDER, + BASE_URL, + VERSION_PATH, + API, + ACTION, +}; diff --git a/components/roamresearch/common/utils.mjs b/components/roamresearch/common/utils.mjs new file mode 100644 index 0000000000000..5b2a021c30b86 --- /dev/null +++ b/components/roamresearch/common/utils.mjs @@ -0,0 +1,57 @@ +import { ConfigurationError } from "@pipedream/platform"; + +const parseJson = (input) => { + const parse = (value) => { + if (typeof(value) === "string") { + try { + return parseJson(JSON.parse(value)); + } catch (e) { + return value; + } + } else if (typeof(value) === "object" && value !== null) { + return Object.entries(value) + .reduce((acc, [ + key, + val, + ]) => Object.assign(acc, { + [key]: parse(val), + }), {}); + } + return value; + }; + + return parse(input); +}; + +function parseArray(value) { + try { + if (!value) { + return; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +function getNestedProperty(obj, propertyString) { + const properties = propertyString.split("."); + return properties.reduce((prev, curr) => prev?.[curr], obj); +} + +export default { + parseArray: (value) => parseArray(value)?.map(parseJson), + getNestedProperty, +}; diff --git a/components/roamresearch/package.json b/components/roamresearch/package.json new file mode 100644 index 0000000000000..9cf9052e11646 --- /dev/null +++ b/components/roamresearch/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/roamresearch", + "version": "0.1.1", + "description": "Pipedream roamresearch Components", + "main": "roamresearch.app.mjs", + "keywords": [ + "pipedream", + "roamresearch" + ], + "homepage": "https://pipedream.com/apps/roamresearch", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" + } +} diff --git a/components/roamresearch/roamresearch.app.mjs b/components/roamresearch/roamresearch.app.mjs new file mode 100644 index 0000000000000..92763d72086b6 --- /dev/null +++ b/components/roamresearch/roamresearch.app.mjs @@ -0,0 +1,93 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "roamresearch", + propDefinitions: { + resourceType: { + type: "string", + label: "Resource Type", + description: "The type of resource to get data for.", + options: [ + "page", + "block", + ], + }, + pageOrBlock: { + type: "string", + label: "Page Title Or Block UID", + description: "The page title of the block uid to get data for. Page title example: `My Page` and Block UID example: `ideWWvTgI`.", + }, + content: { + type: "string", + label: "Content", + description: "The content of the block to be added.", + }, + nestUnder: { + type: "string", + label: "Nest Under", + description: "Title of the block to nest the new block under.", + optional: true, + }, + }, + methods: { + getUrl(path, api = constants.API.DEFAULT) { + const { graph_name: graphName } = this.$auth; + const baseUrl = constants.BASE_URL.replace(constants.SUBDOMAIN_PLACEHOLDER, api); + return `${baseUrl}${constants.VERSION_PATH}/${graphName}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "X-Authorization": `Bearer ${this.$auth.api_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, api, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path, api), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + appendBlocks(args = {}) { + return this.post({ + api: constants.API.APPEND, + path: "/append-blocks", + ...args, + }); + }, + query(args = {}) { + return this.post({ + path: "/q", + ...args, + }); + }, + pull(args = {}) { + return this.post({ + path: "/pull", + ...args, + }); + }, + pullMany(args = {}) { + return this.post({ + path: "/pull-many", + ...args, + }); + }, + write(args = {}) { + return this.post({ + path: "/write", + ...args, + }); + }, + }, +}; diff --git a/components/roamresearch/sources/new-modified-linked-reference/new-modified-linked-reference.mjs b/components/roamresearch/sources/new-modified-linked-reference/new-modified-linked-reference.mjs new file mode 100644 index 0000000000000..1f06de7c21bfc --- /dev/null +++ b/components/roamresearch/sources/new-modified-linked-reference/new-modified-linked-reference.mjs @@ -0,0 +1,94 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import app from "../../roamresearch.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "roamresearch-new-modified-linked-reference", + name: "New Modified Linked Reference", + description: "Emit new event for each new or modified linked reference in Roam Research.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + resourceType: { + propDefinition: [ + app, + "resourceType", + ], + }, + pageOrBlock: { + propDefinition: [ + app, + "pageOrBlock", + ], + }, + }, + methods: { + getResourcesName() { + return "result.:block/_refs"; + }, + getResourcesFn() { + return this.app.pull; + }, + getResourcesFnArgs() { + const { + resourceType, + pageOrBlock, + } = this; + + const attribute = resourceType === "page" + ? ":node/title" + : ":block/uid"; + + return { + data: { + selector: `[${attribute} :block/string :block/order :edit/time {:block/_refs ...}]`, + eid: `[${attribute} "${pageOrBlock}"]`, + }, + }; + }, + generateMeta(resource) { + const ts = resource[":edit/time"]; + return { + id: ts, + summary: `Link Reference: ${resource[":block/string"]}`, + ts, + }; + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + }, + async run() { + const { + getResourcesFn, + getResourcesName, + getResourcesFnArgs, + processResource, + } = this; + + const resourcesFn = getResourcesFn(); + const response = await resourcesFn(getResourcesFnArgs()); + const resources = utils.getNestedProperty(response, getResourcesName()); + + if (!resources) { + console.log("No resources found"); + return; + } + + Array.from(resources) + .reverse() + .forEach(processResource); + }, +}; diff --git a/components/robly/README.md b/components/robly/README.md index 36a1fa24b8f6c..6ffe2e3b52b87 100644 --- a/components/robly/README.md +++ b/components/robly/README.md @@ -1,18 +1,11 @@ # Overview -Robly API is an email marketing platform that allows for powerful customization -and automation for email campaigns. Using the Robly API, developers can create -sophisticated campaigns to send both email newsletters and automated emails -triggered by the rules set by the user. Here are some examples of what you can -build with the Robly API: +Robly API enables seamless integration of Robly's email marketing tools with other services. Pipedream acts as a bridge, automating tasks between Robly and various apps to streamline email campaigns, subscriber management, and analytical reporting. Using Pipedream with Robly API, you can trigger workflows based on subscriber activity, sync contact lists across platforms, and automate email campaign follow-ups, enhancing the efficiency and personalization of marketing efforts. -- Customizable email campaigns and templates -- Automated emails that can be triggered by user events -- Detailed reports on email campaign performance -- A/B testing features to optimize campaigns -- Send transactional emails -- Schedule emails for welcome series -- Dynamically customize email content -- Personalize messages with user data -- Automatically segment campaigns -- Create custom rules to trigger automated emails +# Example Use Cases + +- **Sync New Subscribers to CRM**: Automatically add new subscribers from Robly to a CRM like Salesforce or HubSpot. When a subscriber joins a Robly list, Pipedream detects the event and creates or updates the subscriber's record in the CRM, ensuring that contact information is consistent and up-to-date across platforms. + +- **Distribute Lead Magnets**: When a user signs up for a lead magnet on your website, use Pipedream to automatically enroll them in the corresponding Robly email campaign, delivering the content directly to their inbox. This workflow can be extended by tracking the delivery and open rates, enabling targeted follow-ups based on subscriber engagement. + +- **Automate Feedback Collection Post-Campaign**: After an email campaign is sent, trigger a workflow that waits a specific duration, then sends a follow-up email through Robly asking for feedback. Integrate with survey tools like Typeform or Google Forms, and use Pipedream to record responses back in Robly for segmentation or further analysis. diff --git a/components/robly/package.json b/components/robly/package.json new file mode 100644 index 0000000000000..b49467bf979fe --- /dev/null +++ b/components/robly/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/robly", + "version": "0.6.0", + "description": "Pipedream robly Components", + "main": "robly.app.mjs", + "keywords": [ + "pipedream", + "robly" + ], + "homepage": "https://pipedream.com/apps/robly", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/robocorp/README.md b/components/robocorp/README.md new file mode 100644 index 0000000000000..0fff8ca801c1c --- /dev/null +++ b/components/robocorp/README.md @@ -0,0 +1,11 @@ +# Overview + +The Robocorp API allows you to automate complex business processes by leveraging the power of robotic process automation (RPA). With this API, you can manage and run bots that perform tasks ranging from simple data entry to intricate workflows involving various systems and data sources. Integrating the Robocorp API with Pipedream opens up possibilities for orchestrating these automation tasks based on triggers from a multitude of web applications and services, streamlining processes without the need for manual intervention. + +# Example Use Cases + +- **Automated Report Generation and Distribution**: Use Robocorp to generate periodic financial reports. Trigger a workflow on Pipedream when a new row is added to a Google Sheets spreadsheet, which instructs Robocorp to process the data and generate a report. Once the report is ready, use Pipedream to send it via email to relevant stakeholders. + +- **Issue Tracker Integration**: Integrate Robocorp with an issue tracking system like Jira. Whenever a high-priority issue is created in Jira, trigger a Pipedream workflow that instructs a Robocorp bot to perform an immediate diagnostic routine, log the results in the issue tracker, and notify the engineering team through Slack. + +- **E-commerce Order Processing**: Automate the processing of new orders from an e-commerce platform like Shopify. When a new order is received, Pipedream can trigger a Robocorp bot to validate the order information, update inventory databases, and initiate the shipping process, ensuring a swift response to each customer purchase. diff --git a/components/robocorp/package.json b/components/robocorp/package.json new file mode 100644 index 0000000000000..b06e7359cade3 --- /dev/null +++ b/components/robocorp/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/robocorp", + "version": "0.0.1", + "description": "Pipedream Robocorp Components", + "main": "robocorp.app.mjs", + "keywords": [ + "pipedream", + "robocorp" + ], + "homepage": "https://pipedream.com/apps/robocorp", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/robocorp/robocorp.app.mjs b/components/robocorp/robocorp.app.mjs new file mode 100644 index 0000000000000..dd18b87e5694a --- /dev/null +++ b/components/robocorp/robocorp.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "robocorp", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/roboflow/README.md b/components/roboflow/README.md new file mode 100644 index 0000000000000..de111c71e1854 --- /dev/null +++ b/components/roboflow/README.md @@ -0,0 +1,11 @@ +# Overview + +The Roboflow API is a robust machine learning interface that allows developers to upload, annotate, train, and deploy computer vision models. Using Pipedream, you can create powerful, serverless workflows to automate tasks involving image and video processing. With the API, you can programmatically manage datasets, kick off model training, and utilize trained models to analyze new data. + +# Example Use Cases + +- **Automated Image Processing Pipeline**: Upload images to Roboflow, trigger a model to process them, and receive analyzed data. For example, integrate with Dropbox on Pipedream to watch for new image uploads, send them to Roboflow for annotation, and store the results in a Google Sheet. + +- **Real-time Video Analysis Feedback Loop**: Connect a video streaming service to Roboflow via Pipedream. As new video snippets are streamed, they're sent to Roboflow's API for analysis. The processed data could then trigger events in other apps, like sending alerts via Slack if a certain condition is met. + +- **Scheduled Model Retraining and Evaluation**: Set up a Pipedream workflow that periodically re-trains your Roboflow model with new data and evaluates its performance. This workflow could post the evaluation results to a GitHub repository as an issue to track model improvements over time. diff --git a/components/rocket_chat/README.md b/components/rocket_chat/README.md index d818119c0a17f..3a4c0f0fd64ae 100644 --- a/components/rocket_chat/README.md +++ b/components/rocket_chat/README.md @@ -1,16 +1,11 @@ # Overview -Using the Rocket Chat API, you can build a variety of communication and -collaboration applications. Here are some examples of things you can build: +Rocket Chat is a customizable and open-source team chat platform that's a popular alternative for team communication. With its API, you can automate a variety of tasks, such as sending messages, creating channels, managing users, and integrating with various services for a seamless workflow. Leveraging Pipedream's capabilities, you can use Rocket Chat API to create intricate workflows and connect them with other apps to streamline communication, trigger actions based on messages, and synchronize data across platforms. -- Real-time Chat and Messaging Applications -- Video and Voice Calling Applications -- Private Communities with Groups and Conversations -- File and Document Sharing Applications -- Compliance, Security, and Privacy Applications -- Automated Chatbots -- Integrations with External Applications and APIs -- Broadcasting Applications -- Custom Dashboards and Analytics -- Multi-Platform Applications with Client SDKs -- In-App Notification and Push Messages +# Example Use Cases + +- **Automated Helpdesk Notifications**: Listen to new messages in a specific Rocket Chat support channel using Pipedream's event sources. When a message contains certain keywords (like "help" or "issue"), automatically create a ticket in a helpdesk system like Zendesk or Jira. This ensures that support requests are tracked and handled systematically. + +- **Team Activity Digests**: Compile daily or weekly summaries of messages and activity from specific Rocket Chat channels. Use a Pipedream scheduled workflow to fetch this data and format it into a digest. Then, send the digest via email using an integration with SendGrid or directly post it in another channel, keeping the team updated on important discussions or announcements. + +- **Real-time CRM Updates**: Detect when sales team members mention deals or clients in Rocket Chat and use Pipedream to trigger updates in a CRM like Salesforce or HubSpot. You can configure the workflow to parse messages for deal IDs or client names, then update the corresponding records in the CRM, ensuring that conversation insights are captured and accessible for future reference. diff --git a/components/rocket_chat/actions/create-channel/create-channel.mjs b/components/rocket_chat/actions/create-channel/create-channel.mjs new file mode 100644 index 0000000000000..8cd2cc3eda8da --- /dev/null +++ b/components/rocket_chat/actions/create-channel/create-channel.mjs @@ -0,0 +1,46 @@ +import rocketchat from "../../rocket_chat.app.mjs"; + +export default { + key: "rocket_chat-create-channel", + name: "Create Channel", + description: "Creates a new channel. [See the documentation](https://developer.rocket.chat/reference/api/rest-api/endpoints/rooms/channels-endpoints/create-channel)", + version: "0.0.1", + type: "action", + props: { + rocketchat, + name: { + type: "string", + label: "Room Name", + description: "The name of the new room", + }, + members: { + propDefinition: [ + rocketchat, + "username", + ], + type: "string[]", + label: "Members", + description: "The list of usernames to be added to the new channel", + optional: true, + }, + readOnly: { + type: "boolean", + label: "Read Only", + description: "Set if the channel is read-only or not. The default value is `false`.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.rocketchat.createChannel({ + $, + data: { + name: this.name, + members: this.members, + readOnly: this.readOnly, + }, + }); + + $.export("$summary", `Successfully created channel ${this.name}`); + return response; + }, +}; diff --git a/components/rocket_chat/actions/send-direct-message/send-direct-message.mjs b/components/rocket_chat/actions/send-direct-message/send-direct-message.mjs new file mode 100644 index 0000000000000..f86f00c23de32 --- /dev/null +++ b/components/rocket_chat/actions/send-direct-message/send-direct-message.mjs @@ -0,0 +1,34 @@ +import rocketchat from "../../rocket_chat.app.mjs"; + +export default { + key: "rocket_chat-send-direct-message", + name: "Send Direct Message", + description: "Sends a new direct message to a specific user. [See the documentation](https://developer.rocket.chat/reference/api/rest-api/endpoints/messaging/chat-endpoints/postmessage)", + version: "0.0.1", + type: "action", + props: { + rocketchat, + username: { + propDefinition: [ + rocketchat, + "username", + ], + }, + text: { + type: "string", + label: "Message Text", + description: "The text of the message to be sent", + }, + }, + async run({ $ }) { + const response = await this.rocketchat.sendMessage({ + $, + data: { + channel: `@${this.username}`, + text: this.text, + }, + }); + $.export("$summary", `Successfully sent message to ${this.recipientUsername}`); + return response; + }, +}; diff --git a/components/rocket_chat/actions/set-status/set-status.mjs b/components/rocket_chat/actions/set-status/set-status.mjs new file mode 100644 index 0000000000000..2e0ed227a1caf --- /dev/null +++ b/components/rocket_chat/actions/set-status/set-status.mjs @@ -0,0 +1,36 @@ +import rocketchat from "../../rocket_chat.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "rocket_chat-set-status", + name: "Set Status", + description: "Updates the user's status. [See the documentation](https://developer.rocket.chat/reference/api/rest-api/endpoints/user-management/users-endpoints/set-user-status)", + version: "0.0.1", + type: "action", + props: { + rocketchat, + statusText: { + type: "string", + label: "Status Text", + description: "The status text to be set for the user", + }, + statusType: { + type: "string", + label: "Status Type", + description: "The status type to be set for the user (online, away, busy or offline)", + options: constants.STATUS_TYPES, + optional: true, + }, + }, + async run({ $ }) { + const response = await this.rocketchat.updateUserStatus({ + $, + data: { + message: this.statusText, + status: this.statusType, + }, + }); + $.export("$summary", `Successfully updated status to "${this.statusText}"`); + return response; + }, +}; diff --git a/components/rocket_chat/common/constants.mjs b/components/rocket_chat/common/constants.mjs new file mode 100644 index 0000000000000..1faec91e1aae1 --- /dev/null +++ b/components/rocket_chat/common/constants.mjs @@ -0,0 +1,13 @@ +const DEFAULT_LIMIT = 50; + +const STATUS_TYPES = [ + "online", + "away", + "busy", + "offline", +]; + +export default { + STATUS_TYPES, + DEFAULT_LIMIT, +}; diff --git a/components/rocket_chat/package.json b/components/rocket_chat/package.json new file mode 100644 index 0000000000000..8a90f1c01ebfa --- /dev/null +++ b/components/rocket_chat/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/rocket_chat", + "version": "0.0.1", + "description": "Pipedream Rocket Chat Components", + "main": "rocket_chat.app.mjs", + "keywords": [ + "pipedream", + "rocket_chat" + ], + "homepage": "https://pipedream.com/apps/roboflow", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/rocket_chat/rocket_chat.app.mjs b/components/rocket_chat/rocket_chat.app.mjs index 899e8b37b9adf..4f887a22c30be 100644 --- a/components/rocket_chat/rocket_chat.app.mjs +++ b/components/rocket_chat/rocket_chat.app.mjs @@ -1,11 +1,145 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "rocket_chat", - propDefinitions: {}, + propDefinitions: { + username: { + type: "string", + label: "Recipient Username", + description: "The username of the recipient for the direct message", + async options({ page }) { + const limit = constants.DEFAULT_LIMIT; + const { users } = await this.listUsers({ + params: { + count: limit, + offset: page * limit, + }, + }); + return users?.map(({ + username: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + channelId: { + type: "string", + label: "Channel ID", + description: "The identifier of a room", + async options({ page }) { + const limit = constants.DEFAULT_LIMIT; + const { channels } = await this.listChannels({ + params: { + count: limit, + offset: page * limit, + }, + }); + return channels?.map(({ + _id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `https://${this.$auth.domain}/api/v1`; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "X-Auth-Token": this.$auth["X-Auth-Token"], + "X-User-Id": this.$auth["X-User-Id"], + }, + params: { + ...params, + userId: this.$auth["X-User-Id"], + }, + }); + }, + listChannels(opts = {}) { + return this._makeRequest({ + path: "/channels.list", + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users.list", + ...opts, + }); + }, + listMessages(opts = {}) { + return this._makeRequest({ + path: "/chat.syncMessages", + ...opts, + }); + }, + createChannel(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/channels.create", + ...opts, + }); + }, + sendMessage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/chat.postMessage", + ...opts, + }); + }, + updateUserStatus(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/users.setStatus", + ...opts, + }); + }, + async *paginate({ + resourceFn, + params, + resourceType, + resourceSubType, + max, + }) { + const limit = constants.DEFAULT_LIMIT; + params = { + ...params, + count: limit, + offset: 0, + }; + let total, count = 0; + do { + const response = await resourceFn({ + params, + }); + const items = resourceSubType + ? response[resourceType][resourceSubType] + : response[resourceType]; + for (const item of items) { + yield item; + count++; + if (max && count >= max) { + return; + } + total = items?.length; + params.offset += limit; + } + } while (total === limit); }, }, }; diff --git a/components/rocket_chat/sources/common/base.mjs b/components/rocket_chat/sources/common/base.mjs new file mode 100644 index 0000000000000..c54e675ec7cea --- /dev/null +++ b/components/rocket_chat/sources/common/base.mjs @@ -0,0 +1,69 @@ +import rocketchat from "../../rocket_chat.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + rocketchat, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || this.oneDayAgo(); + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + oneDayAgo() { + return new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString() + .slice(0, 19) + "Z"; + }, + getParams() { + return {}; + }, + getResourceSubType() { + return null; + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + getResourceType() { + throw new Error("getResourceType is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run() { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + + const resourceFn = this.getResourceFn(); + const params = this.getParams(); + const resourceType = this.getResourceType(); + const resourceSubType = this.getResourceSubType(); + const items = this.rocketchat.paginate({ + resourceFn, + params, + resourceType, + resourceSubType, + }); + + for await (const item of items) { + if (Date.parse(item._updatedAt) > Date.parse(lastTs)) { + if (Date.parse(item._updatedAt) > Date.parse(maxTs)) { + maxTs = item._updatedAt; + } + const meta = this.generateMeta(item); + this.$emit(item, meta); + } + } + + this._setLastTs(maxTs); + }, +}; diff --git a/components/rocket_chat/sources/new-channel-created/new-channel-created.mjs b/components/rocket_chat/sources/new-channel-created/new-channel-created.mjs new file mode 100644 index 0000000000000..9cd06437c018a --- /dev/null +++ b/components/rocket_chat/sources/new-channel-created/new-channel-created.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "rocket_chat-new-channel-created", + name: "New Channel Created", + description: "Emit new event when a new channel is created in RocketChat.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.rocketchat.listChannels; + }, + getResourceType() { + return "channels"; + }, + generateMeta(channel) { + return { + id: channel._id, + summary: `New Channel: ${channel.name}`, + ts: Date.parse(channel._updatedAt), + }; + }, + }, + sampleEmit, +}; diff --git a/components/rocket_chat/sources/new-channel-created/test-event.mjs b/components/rocket_chat/sources/new-channel-created/test-event.mjs new file mode 100644 index 0000000000000..73c76b11c18b6 --- /dev/null +++ b/components/rocket_chat/sources/new-channel-created/test-event.mjs @@ -0,0 +1,22 @@ +export default { + "_id": "66180ef1c634d9a724e258e9", + "fname": "new_channel", + "_updatedAt": "2024-04-11T16:27:42.413Z", + "customFields": {}, + "topic": "", + "broadcast": false, + "encrypted": false, + "name": "new_channel", + "t": "c", + "msgs": 1, + "usersCount": 1, + "u": { + "_id": "initialuser", + "username": "testuser", + "name": "testuser" + }, + "ts": "2024-04-11T16:25:21.163Z", + "ro": false, + "default": false, + "sysMes": true +} \ No newline at end of file diff --git a/components/rocket_chat/sources/new-message-public-channel/new-message-public-channel.mjs b/components/rocket_chat/sources/new-message-public-channel/new-message-public-channel.mjs new file mode 100644 index 0000000000000..36b39fef21d4c --- /dev/null +++ b/components/rocket_chat/sources/new-message-public-channel/new-message-public-channel.mjs @@ -0,0 +1,47 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "rocket_chat-new-message-public-channel", + name: "New Message in Public Channel", + description: "Emit new event when a new message is posted to a specific public channel.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + channelId: { + propDefinition: [ + common.props.rocketchat, + "channelId", + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.rocketchat.listMessages; + }, + getParams() { + return { + roomId: this.channelId, + lastUpdate: this._getLastTs(), + }; + }, + getResourceType() { + return "result"; + }, + getResourceSubType() { + return "updated"; + }, + generateMeta(message) { + return { + id: message._id, + summary: `New Message: ${message.msg}`, + ts: Date.parse(message._updatedAt), + }; + }, + }, + sampleEmit, +}; diff --git a/components/rocket_chat/sources/new-message-public-channel/test-event.mjs b/components/rocket_chat/sources/new-message-public-channel/test-event.mjs new file mode 100644 index 0000000000000..5ddd445ea6b24 --- /dev/null +++ b/components/rocket_chat/sources/new-message-public-channel/test-event.mjs @@ -0,0 +1,26 @@ +export default { + "_id": "2TpNxYP9QWD8KeHKA", + "rid": "GENERAL", + "msg": "hello world", + "ts": "2024-04-11T16:32:06.775Z", + "u": { + "_id": "initialuser", + "username": "testuser", + "name": "testuser" + }, + "_updatedAt": "2024-04-11T16:32:06.889Z", + "urls": [], + "mentions": [], + "channels": [], + "md": [ + { + "type": "PARAGRAPH", + "value": [ + { + "type": "PLAIN_TEXT", + "value": "hello world" + } + ] + } + ] +} \ No newline at end of file diff --git a/components/rocket_chat/sources/new-user-created/new-user-created.mjs b/components/rocket_chat/sources/new-user-created/new-user-created.mjs new file mode 100644 index 0000000000000..a226f7da1eeda --- /dev/null +++ b/components/rocket_chat/sources/new-user-created/new-user-created.mjs @@ -0,0 +1,34 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "rocket_chat-new-user-created", + name: "New User Created", + description: "Emit new event when a new user is created in RocketChat.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + generateMeta(user) { + return { + id: user._id, + summary: `New User: ${user.name}`, + ts: Date.now(), + }; + }, + }, + async run() { + const users = this.rocketchat.paginate({ + resourceFn: this.rocketchat.listUsers, + resourceType: "users", + }); + + for await (const user of users) { + const meta = this.generateMeta(user); + this.$emit(user, meta); + } + }, + sampleEmit, +}; diff --git a/components/rocket_chat/sources/new-user-created/test-event.mjs b/components/rocket_chat/sources/new-user-created/test-event.mjs new file mode 100644 index 0000000000000..e29be38dd753d --- /dev/null +++ b/components/rocket_chat/sources/new-user-created/test-event.mjs @@ -0,0 +1,18 @@ +export default { + "_id": "2JCQdy4RhPfHWhyQu", + "username": "testuser", + "emails": [ + { + "address": "test@test.com", + "verified": true + } + ], + "type": "user", + "roles": [ + "user" + ], + "status": "offline", + "active": true, + "name": "testuser", + "nameInsensitive": "testuser" +} \ No newline at end of file diff --git a/components/rocketreach/README.md b/components/rocketreach/README.md index a4bc6c11380e5..6711fdc07841f 100644 --- a/components/rocketreach/README.md +++ b/components/rocketreach/README.md @@ -1,18 +1,11 @@ # Overview -The RocketReach API provides access to comprehensive yet reliable data about -people and organizations. With this API, you can build applications that can -retrieve contact information, corporate hierarchy, and professional profiles. +The RocketReach API lets you tap into a vast database of professionals to find email addresses, phone numbers, and social media profiles. With it, you can enrich lead data, automate the process of lead generation, and integrate this enriched data into your CRM or marketing automation tools. Pipedream's platform allows you to create serverless workflows using the RocketReach API to trigger events, handle data, and connect with countless other apps to streamline your processes. -Here are some examples of what can be built using the RocketReach API: +# Example Use Cases -- Professional networking applications -- Recruiting platforms -- Knowledge networks -- Sales and Marketing intelligence -- Social recruiting platforms -- Lead generation tools -- Contact information searching -- Social media connectors -- Corporate directory search tools -- Development of Outlook add-ons and plugins +- **Lead Enrichment Workflow**: Automatically enrich new leads added to your CRM (like Salesforce) by triggering a Pipedream workflow when a new lead is created. The workflow would use the RocketReach API to fetch additional contact information and then update the lead's record in Salesforce with this data. + +- **Cold Outreach Automation**: Set up a workflow that listens for new sign-ups on your website via a webhook. For each sign-up, use RocketReach to find additional contact details and then automatically send personalized cold outreach emails using a service like SendGrid or Mailgun. + +- **Social Media Lead Generation**: Create a flow that monitors mentions of specific keywords on Twitter using the Twitter Developer App. With each mention, use RocketReach to find the contact details of the Twitter user and then add them to a Google Sheets spreadsheet for lead tracking and further outreach campaigns. diff --git a/components/rockset/README.md b/components/rockset/README.md index 2bc3ee90e4131..8cb19a5049dec 100644 --- a/components/rockset/README.md +++ b/components/rockset/README.md @@ -1,32 +1,11 @@ # Overview -Rockset API is a modern API for real-time query on complex data. With Rockset, -you can build applications with routinely updated datasets, such as web traffic -logs, machine generated data, sensor data, ad-tracking data, and IoT device -data, to unlock real-time insights and actionability. In this article, we’ll -discuss the various types of applications you can build with Rockset API. +Rockset is a real-time indexing database service designed for low-latency, high-concurrency analytics. With the Rockset API, you can query your datasets, create and manage collections, and integrate with event streams for real-time analytics. Using Pipedream's serverless platform, you can automate workflows that react to database events, sync data across services, or trigger actions based on analytical insights. -Rockset gives you the power to: +# Example Use Cases -- Quickly ingest and index data from a variety of sources such as Kafka, - MongoDB and Amazon S3. -- Perform complex analytical queries regardless of data complexity, structure - and size. -- Perform interactive computing on large volumes of data using SQL. -- Build web or mobile applications with sophisticated analytics capabilities. +- **Real-Time Dashboard Updates:** Couple Rockset with a web app service like Netlify to automatically deploy updates to your dashboards. When Rockset detects a significant data change or trend, a Pipedream workflow triggers a rebuild of your static site hosted on Netlify, ensuring your analytics dashboards reflect the latest insights. -Here are some example applications you can build using Rockset API: +- **Data Pipeline Orchestration:** Use Rockset in tandem with AWS Lambda to process and move large datasets. Set up a Pipedream workflow that listens for new data in Rockset collections, processes the data with a Lambda function, and then pipes it to another database or data warehouse like Amazon Redshift for long-term storage and complex querying. -- Personalized Shopping Experiences: Leverage data from many customer - touchpoints, such as social media, web and app usage, customer support - tickets and customer reviews, to build personalized shopping experiences. -- Automated Fraud Detection: Create and monitor automated fraud detection - systems that ingest and analyze various types of data in real-time to detect - anomalous user behaviors. -- Smart IoT: Create a connected infrastructure that lets you develop and deploy - intelligent applications that can understand, store and analyze IoT data - streams. -- Machine Learning: Build applications that can quickly access, prepare and - analyze data used for predictive analysis. -- Log Management and Analytics: Collect, monitor and analyze log data from - various sources to monitor system performance and alert on anomalies. +- **Event-Driven Notifications:** Combine Rockset with communication apps like Slack or email services such as SendGrid. Create a Pipedream workflow that sends notifications or reports to specified channels or recipients when the Rockset API returns query results that meet certain business-critical conditions, like inventory levels falling below a threshold or unusual patterns in user activity. diff --git a/components/rockset/package.json b/components/rockset/package.json new file mode 100644 index 0000000000000..83167e3fd4b2b --- /dev/null +++ b/components/rockset/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/rockset", + "version": "0.6.0", + "description": "Pipedream rockset Components", + "main": "rockset.app.mjs", + "keywords": [ + "pipedream", + "rockset" + ], + "homepage": "https://pipedream.com/apps/rockset", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/roll/README.md b/components/roll/README.md index 9aea99d9d0146..d42746b45d2bf 100644 --- a/components/roll/README.md +++ b/components/roll/README.md @@ -1,34 +1,11 @@ # Overview -The Roll API is a powerful platform enabling developers to build powerful and -custom web applications. Through the Roll API, developers have the ability to -extend and customize the core features of the Roll platform. With the Roll API, -developers can create powerful web applications that provide a more engaging -experience for their users. +The Roll API serves as a bridge to Roll's project management software, enabling users to automate tasks, manipulate project data, and integrate with countless other apps within the Pipedream ecosystem. By leveraging this API, you can streamline operations, sync data across various platforms, and create custom notifications that power up your project management workflows. -The Roll API can be used to create a variety of applications including: +# Example Use Cases -- Online Forms: Create customized forms with a drag and drop editor, capture - and store data from users, and integrate with other business systems. -- Survey and Quiz Platforms: Design surveys and quizzes for better user - engagement and improve the understanding of customer needs and preferences. -- Collaboration Tools: Create team-based platforms to facilitate communication - and collaboration between teams, and allow for easy sharing of files and - documents. -- Workflow Automation: Automate data collection, business process flows, and - workflow dependencies so teams can focus on getting work done faster. -- Social Login: Allow users to securely sign in with their social network - accounts. -- Event Registration: Create event registration pages, capture ticket - information and manage event attendance in real-time. -- User Management: Create a secure and user-friendly user portal that includes - user profiles, permission settings, and access controls. -- E-Commerce Platforms: Build online stores with integrated payment gateways, - and comprehensive user interface for managing customers and orders. -- React Native Apps: Create cross-platform native mobile applications with the - Roll API. +- **Automated Project Creation**: Trigger a workflow in Pipedream to automatically create a new project in Roll whenever a new client is added to your CRM system like Salesforce. This ensures that your project management tool remains in sync with your sales pipeline, reducing manual entry and the chance for error. -With the Roll API, developers can build a wide variety of applications with -robust features and capabilities. With Roll’s system of powerful APIs, -developers can create powerful and engaging applications that fulfill the needs -of their users. +- **Time Tracking Integration**: Connect Roll with time tracking tools such as Toggl to automatically update time entries in Roll tasks. Each time an entry is logged in Toggl, a corresponding update can be sent to the relevant task in Roll, providing real-time insights into project hours and resource allocation. + +- **Dynamic Reporting and Notifications**: Use Pipedream to craft a workflow where Roll project updates trigger custom Slack notifications. This could include summary reports of completed tasks, upcoming deadlines, or budget alerts, keeping the whole team informed and aligned without constant manual checking. diff --git a/components/rollbar/README.md b/components/rollbar/README.md new file mode 100644 index 0000000000000..fc8507467c57e --- /dev/null +++ b/components/rollbar/README.md @@ -0,0 +1,11 @@ +# Overview + +Rollbar is an error tracking software that provides you with the ability to monitor, analyze, and manage errors in real-time. Through its API, Rollbar offers endpoints for fetching items, updating items, and managing projects, among other tasks. Integrating the Rollbar API on Pipedream allows you to automate responses to errors, synchronize error data with other tools, and create custom alerts or dashboards. With Pipedream's serverless platform, you can connect Rollbar events to hundreds of other services without writing complex code. + +# Example Use Cases + +- **Automated Error Response**: Create a workflow that listens for new Rollbar items and automatically sends a message with error details to a Slack channel. This allows your development team to react promptly to issues reported by Rollbar. + +- **Issue Tracking Integration**: Whenever Rollbar detects a new error, use Pipedream to create a corresponding issue in GitHub or Jira. This keeps your issue tracking in sync with actual errors and helps in prioritizing the bug-fixing process. + +- **Error Metrics Dashboard**: Collect and send error metrics from Rollbar to a Google Sheet or a data visualization tool like Grafana. Use this workflow to create a custom dashboard for monitoring error trends and to inform decision-making. diff --git a/components/rollbar/actions/create-a-project/create-a-project.mjs b/components/rollbar/actions/create-a-project/create-a-project.mjs new file mode 100644 index 0000000000000..b6ff08769144b --- /dev/null +++ b/components/rollbar/actions/create-a-project/create-a-project.mjs @@ -0,0 +1,30 @@ +import app from "../../rollbar.app.mjs"; + +export default { + key: "rollbar-create-a-project", + name: "Create a Project", + description: "Creates a new project in Rollbar. [See the documentation](https://docs.rollbar.com/reference/create-a-project)", + version: "0.0.1", + type: "action", + props: { + app, + projectName: { + propDefinition: [ + app, + "projectName", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createProject({ + $, + data: { + name: this.projectName, + }, + }); + + $.export("$summary", `Successfully created project ${this.projectName}`); + + return response; + }, +}; diff --git a/components/rollbar/actions/delete-a-project/delete-a-project.mjs b/components/rollbar/actions/delete-a-project/delete-a-project.mjs new file mode 100644 index 0000000000000..2902ff4d9d3a2 --- /dev/null +++ b/components/rollbar/actions/delete-a-project/delete-a-project.mjs @@ -0,0 +1,28 @@ +import app from "../../rollbar.app.mjs"; + +export default { + key: "rollbar-delete-a-project", + name: "Delete a Project", + description: "Deletes a project in Rollbar. [See the documentation](https://docs.rollbar.com/reference/delete-a-project)", + version: "0.0.1", + type: "action", + props: { + app, + projectId: { + propDefinition: [ + app, + "projectId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.deleteProject({ + $, + projectId: this.projectId, + }); + + $.export("$summary", `Successfully deleted project with ID ${this.projectId}`); + + return response; + }, +}; diff --git a/components/rollbar/actions/list-projects/list-projects.mjs b/components/rollbar/actions/list-projects/list-projects.mjs new file mode 100644 index 0000000000000..4199f28d1d902 --- /dev/null +++ b/components/rollbar/actions/list-projects/list-projects.mjs @@ -0,0 +1,21 @@ +import app from "../../rollbar.app.mjs"; + +export default { + key: "rollbar-list-projects", + name: "List Projects", + description: "Lists all projects in Rollbar. [See the documentation](https://docs.rollbar.com/reference/list-all-projects)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.listProjects({ + $, + }); + + $.export("$summary", "Successfully listed all projects"); + + return response; + }, +}; diff --git a/components/rollbar/package.json b/components/rollbar/package.json index 05bd964ca291b..2b50ecb39e263 100644 --- a/components/rollbar/package.json +++ b/components/rollbar/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/rollbar", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Rollbar Components", "main": "rollbar.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" } -} \ No newline at end of file +} diff --git a/components/rollbar/rollbar.app.mjs b/components/rollbar/rollbar.app.mjs index 23a32d9cb97a5..d2c793660cdde 100644 --- a/components/rollbar/rollbar.app.mjs +++ b/components/rollbar/rollbar.app.mjs @@ -1,11 +1,67 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "rollbar", - propDefinitions: {}, + propDefinitions: { + projectName: { + type: "string", + label: "Project Name", + description: "The name of the project to create", + }, + projectId: { + type: "string", + label: "Project ID", + description: "The ID of the project", + async options() { + const { result: resources } = await this.listProjects(); + + return resources.map(({ + id, name, + }) => ({ + value: id, + label: name, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.rollbar.com/api/1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "X-Rollbar-Access-Token": this.$auth.access_token, + }, + }); + }, + async createProject(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/projects", + ...args, + }); + }, + async listProjects() { + return this._makeRequest({ + path: "/projects", + }); + }, + async deleteProject({ projectId }) { + return this._makeRequest({ + method: "DELETE", + path: `/project/${projectId}`, + }); }, }, }; diff --git a/components/ronin/package.json b/components/ronin/package.json new file mode 100644 index 0000000000000..3bb5677055733 --- /dev/null +++ b/components/ronin/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/ronin", + "version": "0.6.0", + "description": "Pipedream ronin Components", + "main": "ronin.app.mjs", + "keywords": [ + "pipedream", + "ronin" + ], + "homepage": "https://pipedream.com/apps/ronin", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/rosette_text_analytics/README.md b/components/rosette_text_analytics/README.md new file mode 100644 index 0000000000000..5c989170be1dd --- /dev/null +++ b/components/rosette_text_analytics/README.md @@ -0,0 +1,11 @@ +# Overview + +The Rosette Text Analytics API brings advanced language processing to the table, allowing you to extract entities, relationships, and sentiment from text. With Pipedream, you can integrate this powerful text analysis into your workflows, triggering actions based on the insights extracted from documents, social media posts, customer feedback, and more. You can use it to enrich CRM data, automate content moderation, or even drive market research by sentiment analysis. + +# Example Use Cases + +- **Enhanced Customer Support**: Analyze support tickets with the Rosette API to detect sentiment and key phrases. Based on the analysis, categorize the tickets and route them to the appropriate teams or escalate priority cases automatically. + +- **Content Recommendation Engine**: Leverage the entity extraction and categorization features of Rosette API to analyze articles or posts. Use this data in Pipedream to tag content and recommend related articles to users based on their reading history. + +- **Social Media Monitoring**: Connect Rosette API with social media platforms within Pipedream to monitor brand mentions. Extract key entities and sentiment to gauge brand perception and respond proactively to both positive and negative feedback. diff --git a/components/rosette_text_analytics/package.json b/components/rosette_text_analytics/package.json new file mode 100644 index 0000000000000..3a2550861116b --- /dev/null +++ b/components/rosette_text_analytics/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/rosette_text_analytics", + "version": "0.0.1", + "description": "Pipedream Rosette Text Analytics Components", + "main": "rosette_text_analytics.app.mjs", + "keywords": [ + "pipedream", + "rosette_text_analytics" + ], + "homepage": "https://pipedream.com/apps/rosette_text_analytics", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/rosette_text_analytics/rosette_text_analytics.app.mjs b/components/rosette_text_analytics/rosette_text_analytics.app.mjs new file mode 100644 index 0000000000000..97d451e23d4b3 --- /dev/null +++ b/components/rosette_text_analytics/rosette_text_analytics.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "rosette_text_analytics", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/route4me/README.md b/components/route4me/README.md new file mode 100644 index 0000000000000..ba1242e205d67 --- /dev/null +++ b/components/route4me/README.md @@ -0,0 +1,11 @@ +# Overview + +The Route4Me API provides access to a suite of tools designed for route optimization and fleet management. With its functions, you can plan routes, track vehicles, and manage addresses and members. Integrating Route4Me with Pipedream allows you to automate workflows involving logistics, dispatching, and real-time tracking. By leveraging Pipedream's capabilities, you can create event-driven automation, connect with numerous services, and handle complex logic without worrying about infrastructure. + +# Example Use Cases + +- **Dynamic Route Adjustment Based on Weather Conditions**: Connect Route4Me to a weather API on Pipedream. Automatically adjust delivery routes and dispatch notifications to drivers when inclement weather is predicted along their routes, ensuring safety and efficiency. + +- **Automated Customer Notification for Package Delivery**: Use Route4Me's API with Pipedream to monitor delivery statuses. When a package is marked as out for delivery, automatically send an email or SMS to the customer with an estimated delivery window using an app like Twilio or SendGrid. + +- **Scheduled Fleet Maintenance Alerts**: Combine Route4Me's vehicle tracking data with Pipedream's scheduling capabilities. Set up a system to alert fleet managers when vehicles have reached certain mileage or time intervals, indicating a need for maintenance or inspection. diff --git a/components/rss/README.md b/components/rss/README.md index e41b4427937ac..5db7757495901 100644 --- a/components/rss/README.md +++ b/components/rss/README.md @@ -1,14 +1,11 @@ # Overview -With the RSS API you have the power to create powerful tools and applications. -RSS is a great way to reliably subscribe to, track and build around your -favorite content sources. Here are **some examples** of things you can create -using the RSS API: - -- A personal news website to syndicate articles from multiple sources. -- A custom feed reader to deliver timely notifications of updates and news. -- A live editorial dashboard to track news, trends and public sentiment. -- An automated “report bot” to aggregate and report on news topics. -- A competitor tracking tool to stay on top of industry news. -- A custom RSS-based search engine or RSS-supported deep learning engine. -- A live events feed to notify users and followers of new developments. +The RSS app allows users to automatically fetch and parse updates from web feeds. This functionality is pivotal for staying abreast of content changes or updates from websites, blogs, and news outlets that offer RSS feeds. With Pipedream, you can harness the RSS API to trigger workflows that enable a broad range of automations, like content aggregation, monitoring for specific keywords, notifications, and data synchronization across platforms. + +# Example Use Cases + +- **Content Syndication to Social Media**: Automate the sharing of latest blog posts or news articles from an RSS feed to social media platforms like Twitter or Facebook. When a new item appears in the RSS feed, Pipedream can post it to your social media accounts, keeping your followers engaged with fresh content. + +- **Keyword Monitoring and Alerts**: Monitor an RSS feed for specific keywords or phrases and set up alerts through email or messaging apps like Slack. This is useful for PR tracking, brand monitoring, or competitive analysis. When an item containing the keyword is detected, Pipedream can trigger an alert, ensuring you're always informed about relevant mentions. + +- **Content Backup to Cloud Storage**: Backup new items from an RSS feed directly to a cloud storage service like Google Drive or Dropbox. Each time a new post is published, Pipedream can create a file containing the content or link, preserving it in your storage. This can be valuable for archival purposes or content curation. diff --git a/components/rss/actions/merge-rss-feeds/merge-rss-feeds.ts b/components/rss/actions/merge-rss-feeds/merge-rss-feeds.ts index 23c6f528a49ad..53d207fe79cd9 100644 --- a/components/rss/actions/merge-rss-feeds/merge-rss-feeds.ts +++ b/components/rss/actions/merge-rss-feeds/merge-rss-feeds.ts @@ -5,7 +5,7 @@ export default defineAction({ name: "Merge RSS Feeds", description: "Retrieve multiple RSS feeds and return a merged array of items sorted by date [See documentation](https://www.rssboard.org/rss-specification)", key: "rss-merge-rss-feeds", - version: "1.2.6", + version: "1.2.7", type: "action", props: { rss, diff --git a/components/rss/app/rss.app.ts b/components/rss/app/rss.app.ts index b721d30dd1745..1739b56bb7dc9 100644 --- a/components/rss/app/rss.app.ts +++ b/components/rss/app/rss.app.ts @@ -1,13 +1,10 @@ -import { axios } from "@pipedream/platform"; -import FeedParser from "feedparser"; -import { Item } from "feedparser"; -import hash from "object-hash"; -import { defineApp } from "@pipedream/types"; import { - ConfigurationError, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + axios, ConfigurationError, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, } from "@pipedream/platform"; +import { defineApp } from "@pipedream/types"; +import FeedParser, { Item } from "feedparser"; import { ReadStream } from "fs"; -import querystring from "querystring"; +import hash from "object-hash"; export default defineApp({ type: "app", @@ -57,8 +54,6 @@ export default defineApp({ return hash(item); }, async fetchFeed(url: string): Promise { - const params = querystring.parse(url); - url = url.split("?")[0]; const res = await axios(this, { url, method: "GET", @@ -68,7 +63,6 @@ export default defineApp({ validateStatus: () => true, // does not throw on any bad status code responseType: "stream", // stream is required for feedparser returnFullResponse: true, - params, }); // Handle status codes as error codes diff --git a/components/rss/package.json b/components/rss/package.json index 6813c857da4fd..781a4f5cd7713 100644 --- a/components/rss/package.json +++ b/components/rss/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/rss", - "version": "0.5.6", + "version": "0.5.7", "description": "Pipedream RSS Components", "main": "dist/app/rss.app.mjs", "types": "dist/app/rss.app.d.ts", @@ -28,7 +28,6 @@ "@pipedream/platform": "^1.4.0", "feedparser": "^2.2.10", "object-hash": "^3.0.0", - "querystring": "^0.2.1", "rss-parser": "^3.12.0", "string-to-stream": "^3.0.1" } diff --git a/components/rss/sources/new-item-from-multiple-feeds/new-item-from-multiple-feeds.ts b/components/rss/sources/new-item-from-multiple-feeds/new-item-from-multiple-feeds.ts index cc25eafdb0a6a..eb144d57c1171 100644 --- a/components/rss/sources/new-item-from-multiple-feeds/new-item-from-multiple-feeds.ts +++ b/components/rss/sources/new-item-from-multiple-feeds/new-item-from-multiple-feeds.ts @@ -8,7 +8,7 @@ export default defineSource({ name: "New Item From Multiple RSS Feeds", type: "source", description: "Emit new items from multiple RSS feeds", - version: "1.2.6", + version: "1.2.7", props: { ...rssCommon.props, urls: { diff --git a/components/rss/sources/new-item-in-feed/new-item-in-feed.ts b/components/rss/sources/new-item-in-feed/new-item-in-feed.ts index 81fd8ad23b9f7..f0219faf1ecfc 100644 --- a/components/rss/sources/new-item-in-feed/new-item-in-feed.ts +++ b/components/rss/sources/new-item-in-feed/new-item-in-feed.ts @@ -1,5 +1,5 @@ -import rss from "../../app/rss.app"; import { defineSource } from "@pipedream/types"; +import rss from "../../app/rss.app"; import rssCommon from "../common/common"; export default defineSource({ @@ -7,7 +7,7 @@ export default defineSource({ key: "rss-new-item-in-feed", name: "New Item in Feed", description: "Emit new items from an RSS feed", - version: "1.2.6", + version: "1.2.7", type: "source", dedupe: "unique", props: { diff --git a/components/rss/sources/random-item-in-multiple-feeds/random-item-in-multiple-feeds.ts b/components/rss/sources/random-item-in-multiple-feeds/random-item-in-multiple-feeds.ts index 0411def2b2466..766db6bc227e9 100644 --- a/components/rss/sources/random-item-in-multiple-feeds/random-item-in-multiple-feeds.ts +++ b/components/rss/sources/random-item-in-multiple-feeds/random-item-in-multiple-feeds.ts @@ -8,7 +8,7 @@ export default defineSource({ name: "Random item from multiple RSS feeds", type: "source", description: "Emit a random item from multiple RSS feeds", - version: "0.2.6", + version: "0.2.7", props: { ...rssCommon.props, urls: { diff --git a/components/rudderstack/README.md b/components/rudderstack/README.md index 5284bdbf7b182..3d865c0074b7d 100644 --- a/components/rudderstack/README.md +++ b/components/rudderstack/README.md @@ -1,29 +1,13 @@ # Overview -Using the RudderStack API, you can create powerful applications and software -solutions. With our APIs, you can build integrations with a range of marketing, -support and analytics tools. You can also use our APIs to stream data from both -online and mobile sources. +The RudderStack HTTP API gives you the power to track and send events to RudderStack from anywhere you can make an HTTP request. With this API, you can streamline data from your apps, websites, and servers directly into RudderStack, enabling real-time analytics and insights. Using Pipedream, you can harness this capability to automate data collection and orchestration, syncing event data with other services, triggering actions based on customer behavior, or even enriching event data before it hits your data warehouse. -By connecting to thousands of advertising, analytics, support and marketing -tools through RudderStack’s cloud-native APIs, it's easier for developers to -build integrations in less time. With our simple APIs you can capture user -events, user attributes and device information more easily, without having to -write complex lines of code. +# Example Use Cases -Here are some examples of the things you can build with the RudderStack API: +- **Real-Time Customer Segmentation**: Automate customer segmentation by sending custom events to RudderStack whenever a user performs a significant action on your platform. Use the Pipedream workflow to listen for events from your app, enrich the event data with user attributes, and send it to RudderStack to segment users based on their behavior in real-time. -- Connect applications to popular data warehouses and destination destination - stores, such as Amazon Redshift. -- Create real-time user profiles that span multiple platforms, including Apple - and Android. -- Integrate data from multiple sources and systems, including web, mobile and - native applications. -- Create powerful dashboards that provide actionable insights, as well as - reporting and analytics capabilities. -- Build automated marketing funnels and target specific users based on their - actions. -- Create segmentation rules and behavior-based targeting, allowing you to - tailor content to each user. -- Integrate with multiple third-party tools, including Salesforce, Mailchimp, - Hubspot, and more. +- **Multi-Channel Marketing Automation**: Create a Pipedream workflow that reacts to new sign-ups or user events by triggering marketing workflows across email, SMS, or social media. Capture the events via RudderStack, process them in Pipedream, and connect with apps like Mailchimp, Twilio, or Facebook Ads to launch targeted campaigns. + +- **Product Analytics and Feedback Loop**: Integrate RudderStack with product analytics tools like Mixpanel or Amplitude via Pipedream. As users interact with your product, collect this data, send it through RudderStack, and forward it to your analytics platform, enabling you to track feature usage and create a feedback loop to inform product development. + +Please note that the RudderStack HTTP API provided in the prompt is incorrect and seems to be associated with Vision6. The examples provided are based on the premise of using RudderStack's actual capabilities with Pipedream. diff --git a/components/rudderstack_transformation/README.md b/components/rudderstack_transformation/README.md new file mode 100644 index 0000000000000..2c3dd5c57bfcc --- /dev/null +++ b/components/rudderstack_transformation/README.md @@ -0,0 +1,11 @@ +# Overview + +The RudderStack Transformation API enables you to process and transform data before it's delivered to your data warehouse or other analytics tools. Within Pipedream, you can harness this API to customize the shape and structure of your data, apply business logic, filter out unnecessary information, or enrich data with additional attributes before forwarding it. + +# Example Use Cases + +- **Enrich CRM Data Before Storage**: Use the RudderStack Transformation API in Pipedream to enrich leads with extra information from an external API (like Clearbit) before inserting them into your CRM. This can help create a richer profile of your leads directly in your CRM system. + +- **Filter and Redirect Event Streams**: Create a Pipedream workflow that ingests event data from a source app, uses RudderStack to apply transformations like filtering out events that don't meet certain criteria, and then directs the clean data to specific endpoints such as Google Analytics or your custom analytics service. + +- **Real-time Data Validation and Transformation**: Set up a Pipedream workflow where incoming data from a service like Typeform is validated and transformed using RudderStack. The transformed data could then be sent to a database like PostgreSQL, ensuring only clean, structured data is stored. diff --git a/components/ruly/README.md b/components/ruly/README.md new file mode 100644 index 0000000000000..382ea70304501 --- /dev/null +++ b/components/ruly/README.md @@ -0,0 +1,11 @@ +# Overview + +The Ruly API enables users to automate interactions with their Ruly legal compliance management system. With this API, you can streamline your legal and regulatory processes by leveraging Pipedream's capabilities to integrate Ruly with other apps, create custom notifications, synchronize data, and trigger actions based on events within Ruly. For instance, you could sync Ruly updates to project management tools, send custom alerts through communication platforms like Slack, or even manage tasks and deadlines automatically. + +# Example Use Cases + +- **Automated Compliance Alerting**: Set up a workflow on Pipedream that listens for updates in your compliance status on Ruly and sends a notification to your team's Slack channel. This ensures everyone is aware of compliance changes in real-time. + +- **Task Synchronization with Project Management Tools**: Create a workflow to synchronize Ruly tasks and deadlines with a project management tool like Asana. Whenever a task is created or updated in Ruly, it's automatically reflected in Asana, helping your team stay on track with compliance-related projects. + +- **Document Management Automation**: Implement a workflow that triggers when new compliance documents are added to Ruly. The workflow can automatically upload these documents to a cloud storage service like Google Drive and share them with relevant stakeholders, maintaining organization and accessibility. diff --git a/components/ruly/package.json b/components/ruly/package.json index c83da5ed2e3ec..363fc0c04ef99 100644 --- a/components/ruly/package.json +++ b/components/ruly/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/rumble/README.md b/components/rumble/README.md new file mode 100644 index 0000000000000..af1b43a0cfd37 --- /dev/null +++ b/components/rumble/README.md @@ -0,0 +1,11 @@ +# Overview + +Rumble is a video hosting platform that encourages content discovery and sharing. With the Rumble API on Pipedream, you can automate video uploads, manage user content, and analyze video performance metrics. For instance, you might automate video uploads in response to events, sync video statistics to a database for analysis, or integrate with social media platforms to widen your content's reach. + +# Example Use Cases + +- **Automated Video Uploads Workflow**: Trigger a workflow on Pipedream that listens for new files in a Dropbox folder. When a new video is detected, the workflow automatically uploads the video to your Rumble account. This saves time and ensures consistent video publishing schedules. + +- **Content Moderation and Notification**: Use a workflow that monitors new comments on your Rumble videos. When certain keywords or patterns are detected, suggesting inappropriate or spam content, the workflow could flag the comment and notify you via Slack or email. This helps maintain a clean and professional comment section on your videos. + +- **Video Performance Dashboard Integration**: Set up a workflow triggered by a recurring schedule to fetch the latest viewing stats for your Rumble videos and insert them into a Google Sheets document. This creates a continually updated dashboard that provides insights into video performance, helping to inform your content strategy. diff --git a/components/rumble/package.json b/components/rumble/package.json new file mode 100644 index 0000000000000..2dc2712634bea --- /dev/null +++ b/components/rumble/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/rumble", + "version": "0.0.1", + "description": "Pipedream Rumble Components", + "main": "rumble.app.mjs", + "keywords": [ + "pipedream", + "rumble" + ], + "homepage": "https://pipedream.com/apps/rumble", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/rumble/rumble.app.mjs b/components/rumble/rumble.app.mjs new file mode 100644 index 0000000000000..7612196fcb0ad --- /dev/null +++ b/components/rumble/rumble.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "rumble", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/runpod/package.json b/components/runpod/package.json new file mode 100644 index 0000000000000..af63e9481f89f --- /dev/null +++ b/components/runpod/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/runpod", + "version": "0.0.1", + "description": "Pipedream RunPod Components", + "main": "runpod.app.mjs", + "keywords": [ + "pipedream", + "runpod" + ], + "homepage": "https://pipedream.com/apps/runpod", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/runpod/runpod.app.mjs b/components/runpod/runpod.app.mjs new file mode 100644 index 0000000000000..96f6458376990 --- /dev/null +++ b/components/runpod/runpod.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "runpod", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/runware/actions/image-background-removal/image-background-removal.mjs b/components/runware/actions/image-background-removal/image-background-removal.mjs new file mode 100644 index 0000000000000..abd486ebeaf65 --- /dev/null +++ b/components/runware/actions/image-background-removal/image-background-removal.mjs @@ -0,0 +1,141 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { v4 as uuid } from "uuid"; +import app from "../../runware.app.mjs"; +import constants from "../../common/constants.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "runware-image-background-removal", + name: "Image Background Removal", + description: "Request an image background removal task to be processed by the Runware API. [See the documentation](https://docs.runware.ai/en/image-editing/background-removal).", + version: "0.0.1", + type: "action", + props: { + app, + inputImage: { + propDefinition: [ + app, + "inputImage", + ], + }, + outputType: { + propDefinition: [ + app, + "outputType", + ], + }, + outputFormat: { + propDefinition: [ + app, + "outputFormat", + ], + }, + includeCost: { + propDefinition: [ + app, + "includeCost", + ], + }, + rgb: { + type: "string[]", + label: "RGB", + description: "An array representing the `[red, green, blue]` values that define the color of the removed background. Eg. `[255, 255, 255]`.", + optional: true, + }, + postProcessMask: { + type: "boolean", + label: "Post-Process Mask", + description: "Flag indicating whether to post-process the mask. Controls whether the mask should undergo additional post-processing. This step can improve the accuracy and quality of the background removal mask.", + optional: true, + }, + returnOnlyMask: { + type: "boolean", + label: "Return Only Mask", + description: "Flag indicating whether to return only the mask. The mask is the opposite of the image background removal.", + optional: true, + }, + alphaMatting: { + type: "boolean", + label: "Alpha Matting", + description: "Flag indicating whether to use alpha matting. Alpha matting is a post-processing technique that enhances the quality of the output by refining the edges of the foreground object.", + optional: true, + }, + alphaMattingForegroundThreshold: { + type: "integer", + label: "Alpha Matting Foreground Threshold", + description: "Threshold value used in alpha matting to distinguish the foreground from the background. Adjusting this parameter affects the sharpness and accuracy of the foreground object edges. Eg. `240`.", + optional: true, + min: 1, + max: 255, + }, + alphaMattingBackgroundThreshold: { + type: "integer", + label: "Alpha Matting Background Threshold", + description: "Threshold value used in alpha matting to refine the background areas. It influences how aggressively the algorithm removes the background while preserving image details. The higher the value, the more computation is needed and therefore the more expensive the operation is. Eg. `10`.", + optional: true, + min: 1, + max: 255, + }, + alphaMattingErodeSize: { + type: "integer", + label: "Alpha Matting Erode Size", + description: "Specifies the size of the erosion operation used in alpha matting. Erosion helps in smoothing the edges of the foreground object for a cleaner removal of the background. Eg. `10`.", + optional: true, + min: 1, + max: 255, + }, + }, + methods: { + getRGBA(rgb, alpha = 0) { + if (rgb) { + const parsed = utils.parseArray(rgb).map((value) => parseInt(value, 10)); + return parsed.concat(alpha); + } + }, + }, + async run({ $ }) { + const { + app, + getRGBA, + inputImage, + outputType, + outputFormat, + includeCost, + rgb, + postProcessMask, + returnOnlyMask, + alphaMatting, + alphaMattingForegroundThreshold, + alphaMattingBackgroundThreshold, + alphaMattingErodeSize, + } = this; + + if (rgb && utils.parseArray(rgb).length !== 3) { + throw new ConfigurationError("The **RGB** array must contain exactly 3 integer numbers. Eg. `[255, 255, 255]`."); + } + + const response = await app.post({ + $, + data: [ + { + taskType: constants.TASK_TYPE.IMAGE_BACKGROUND_REMOVAL.value, + taskUUID: uuid(), + inputImage, + outputType, + outputFormat, + includeCost, + rgba: getRGBA(rgb), + postProcessMask, + returnOnlyMask, + alphaMatting, + alphaMattingForegroundThreshold, + alphaMattingBackgroundThreshold, + alphaMattingErodeSize, + }, + ], + }); + + $.export("$summary", `Successfully requested image background removal task with UUID \`${response.data[0].taskUUID}\`.`); + return response; + }, +}; diff --git a/components/runware/actions/image-caption/image-caption.mjs b/components/runware/actions/image-caption/image-caption.mjs new file mode 100644 index 0000000000000..dd3f12c1322d4 --- /dev/null +++ b/components/runware/actions/image-caption/image-caption.mjs @@ -0,0 +1,48 @@ +import { v4 as uuid } from "uuid"; +import app from "../../runware.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "runware-image-caption", + name: "Image Caption", + description: "Request an image caption task to be processed by the Runware API. [See the documentation](https://docs.runware.ai/en/utilities/image-to-text).", + version: "0.0.1", + type: "action", + props: { + app, + inputImage: { + propDefinition: [ + app, + "inputImage", + ], + }, + includeCost: { + propDefinition: [ + app, + "includeCost", + ], + }, + }, + async run({ $ }) { + const { + app, + inputImage, + includeCost, + } = this; + + const response = await app.post({ + $, + data: [ + { + taskType: constants.TASK_TYPE.IMAGE_CAPTION.value, + taskUUID: uuid(), + inputImage, + includeCost, + }, + ], + }); + + $.export("$summary", `Successfully requested image caption task with UUID \`${response.data[0].taskUUID}\`.`); + return response; + }, +}; diff --git a/components/runware/actions/image-control-net-preprocess/image-control-net-preprocess.mjs b/components/runware/actions/image-control-net-preprocess/image-control-net-preprocess.mjs new file mode 100644 index 0000000000000..fe5a1958b53be --- /dev/null +++ b/components/runware/actions/image-control-net-preprocess/image-control-net-preprocess.mjs @@ -0,0 +1,126 @@ +import { v4 as uuid } from "uuid"; +import app from "../../runware.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "runware-image-control-net-preprocess", + name: "Image Control Net Preprocess", + description: "Request an image control net preprocess task to be processed by the Runware API. [See the documentation](https://docs.runware.ai/en/image-editing/controlnet-tools).", + version: "0.0.1", + type: "action", + props: { + app, + inputImage: { + propDefinition: [ + app, + "inputImage", + ], + }, + outputType: { + propDefinition: [ + app, + "outputType", + ], + }, + outputFormat: { + propDefinition: [ + app, + "outputFormat", + ], + }, + includeCost: { + propDefinition: [ + app, + "includeCost", + ], + }, + preProcessorType: { + type: "string", + label: "Preprocessor Type", + description: "The preprocessor to be used.", + optional: true, + options: [ + "canny", + "depth", + "mlsd", + "normalbae", + "openpose", + "tile", + "seg", + "lineart", + "lineart_anime", + "shuffle", + "scribble", + "softedge", + ], + }, + height: { + propDefinition: [ + app, + "height", + ], + }, + width: { + propDefinition: [ + app, + "width", + ], + }, + lowThresholdCanny: { + type: "integer", + label: "Low Threshold Canny", + description: "Defines the lower threshold when using the Canny edge detection preprocessor. The recommended value is `100`.", + optional: true, + }, + highThresholdCanny: { + type: "integer", + label: "High Threshold Canny", + description: "Defines the high threshold when using the Canny edge detection preprocessor. The recommended value is `200`.", + optional: true, + }, + includeHandsAndFaceOpenPose: { + type: "boolean", + label: "Include Hands and Face OpenPose", + description: "Include the hands and face in the pose outline when using the OpenPose preprocessor.", + optional: true, + }, + }, + async run({ $ }) { + const { + app, + outputType, + outputFormat, + includeCost, + inputImage, + preProcessorType, + height, + width, + lowThresholdCanny, + highThresholdCanny, + includeHandsAndFaceOpenPose, + } = this; + + const response = await app.post({ + $, + data: [ + { + taskType: constants.TASK_TYPE.IMAGE_CONTROL_NET_PREPROCESS.value, + taskUUID: uuid(), + outputType, + outputFormat, + inputImage, + includeCost, + height, + width, + preProcessorType, + lowThresholdCanny, + highThresholdCanny, + includeHandsAndFaceOpenPose, + }, + ], + }); + + $.export("$summary", `Successfully requested image control net preprocess task with UUID \`${response.data[0].taskUUID}\`.`); + return response; + }, +}; diff --git a/components/runware/actions/image-inference/image-inference.mjs b/components/runware/actions/image-inference/image-inference.mjs new file mode 100644 index 0000000000000..f1daafceace2a --- /dev/null +++ b/components/runware/actions/image-inference/image-inference.mjs @@ -0,0 +1,435 @@ +import { v4 as uuid } from "uuid"; +import app from "../../runware.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "runware-image-inference", + name: "Image Inference", + description: "Request an image inference task to be processed by the Runware API. [See the documentation](https://docs.runware.ai/en/image-inference/api-reference).", + version: "0.0.1", + type: "action", + props: { + app, + structure: { + type: "string", + label: "Structure", + description: "The structure of the task to be processed.", + options: Object.values(constants.IMAGE_INFERENCE_STRUCTURE), + reloadProps: true, + }, + model: { + type: "string", + label: "Model", + description: "This identifier is a unique string that represents a specific model. You can find the AIR identifier of the model you want to use in our [Model Explorer](https://docs.runware.ai/en/image-inference/models#model-explorer), which is a tool that allows you to search for models based on their characteristics. More information about the AIR system can be found in the [Models page](https://docs.runware.ai/en/image-inference/models). Eg. `civitai:78605@83390`.", + }, + positivePrompt: { + type: "string", + label: "Positive Prompt", + description: "A positive prompt is a text instruction to guide the model on generating the image. It is usually a sentence or a paragraph that provides positive guidance for the task. This parameter is essential to shape the desired results. For example, if the positive prompt is `dragon drinking coffee`, the model will generate an image of a dragon drinking coffee. The more detailed the prompt, the more accurate the results. The length of the prompt must be between 4 and 2000 characters.", + }, + height: { + propDefinition: [ + app, + "height", + ], + }, + width: { + propDefinition: [ + app, + "width", + ], + }, + uploadEndpoint: { + type: "string", + label: "Upload Endpoint", + description: "This parameter allows you to specify a URL to which the generated image will be uploaded as binary image data using the HTTP PUT method. For example, an S3 bucket URL can be used as the upload endpoint. When the image is ready, it will be uploaded to the specified URL.", + optional: true, + }, + checkNSFW: { + type: "boolean", + label: "Check NSFW", + description: "This parameter is used to enable or disable the NSFW check. When enabled, the API will check if the image contains NSFW (not safe for work) content. This check is done using a pre-trained model that detects adult content in images. When the check is enabled, the API will return `NSFWContent: true` in the response object if the image is flagged as potentially sensitive content. If the image is not flagged, the API will return `NSFWContent: false`. If this parameter is not used, the parameter `NSFWContent` will not be included in the response object. Adds `0.1` seconds to image inference time and incurs additional costs. The NSFW filter occasionally returns false positives and very rarely false negatives.", + optional: true, + }, + includeCost: { + propDefinition: [ + app, + "includeCost", + ], + }, + scheduler: { + type: "string", + label: "Scheduler", + description: "An scheduler is a component that manages the inference process. Different schedulers can be used to achieve different results like more detailed images, faster inference, or more accurate results. The default scheduler is the one that the model was trained with, but you can choose a different one to get different results. Schedulers are explained in more detail in the [Schedulers page](https://docs.runware.ai/en/image-inference/schedulers).", + optional: true, + }, + seed: { + type: "string", + label: "Seed", + description: "A seed is a value used to randomize the image generation. If you want to make images reproducible (generate the same image multiple times), you can use the same seed value. When requesting multiple images with the same seed, the seed will be incremented by 1 (+1) for each image generated. Min: `0` Max: `9223372036854776000`. Defaults to `Random`.", + optional: true, + }, + numberResults: { + type: "integer", + label: "Number Of Results", + description: "The number of images to generate from the specified prompt. If **Seed** is set, it will be incremented by 1 (+1) for each image generated.", + optional: true, + }, + }, + additionalProps() { + const { structure } = this; + + const seedImage = { + type: "string", + label: "Seed Image", + description: "When doing Image-to-Image, Inpainting or Outpainting, this parameter is **required**. Specifies the seed image to be used for the diffusion process. The image can be specified in one of the following formats:\n - An UUID v4 string of a [previously uploaded image](https://docs.runware.ai/en/getting-started/image-upload) or a [generated image](https://docs.runware.ai/en/image-inference/api-reference).\n - A data URI string representing the image. The data URI must be in the format `data:;base64,` followed by the base64-encoded image. For example: `data:image/png;base64,iVBORw0KGgo...`.\n - A base64 encoded image without the data URI prefix. For example: `iVBORw0KGgo...`.\n - A URL pointing to the image. The image must be accessible publicly. Supported formats are: PNG, JPG and WEBP.", + }; + + const maskImage = { + type: "string", + label: "Mask Image", + description: "When doing Inpainting or Outpainting, this parameter is **required**. Specifies the mask image to be used for the inpainting process. The image can be specified in one of the following formats:\n - An UUID v4 string of a [previously uploaded image](https://docs.runware.ai/en/getting-started/image-upload) or a [generated image](https://docs.runware.ai/en/image-inference/api-reference).\n - A data URI string representing the image. The data URI must be in the format `data:;base64,` followed by the base64-encoded image. For example: `data:image/png;base64,iVBORw0KGgo...`.\n - A base64 encoded image without the data URI prefix. For example: `iVBORw0KGgo...`.\n - A URL pointing to the image. The image must be accessible publicly. Supported formats are: PNG, JPG and WEBP.", + }; + + const strength = { + type: "string", + label: "Strength", + description: "When doing Image-to-Image, Inpainting or Outpainting, this parameter is used to determine the influence of the **Seed Image** image in the generated output. A higher value results in more influence from the original image, while a lower value allows more creative deviation. Min: `0` Max: `1` and Default: `0.8`.", + optional: true, + }; + + const controlNetModel = { + type: "string", + label: "ControlNet Model 0", + description: "For basic/common ControlNet models, you can check the list of available models [here](https://docs.runware.ai/en/image-inference/models#basic-controlnet-models). For custom or specific ControlNet models, we make use of the [AIR system](https://github.com/civitai/civitai/wiki/AIR-%E2%80%90-Uniform-Resource-Names-for-AI) to identify ControlNet models. This identifier is a unique string that represents a specific model. You can find the AIR identifier of the ControlNet model you want to use in our [Model Explorer](https://docs.runware.ai/en/image-inference/models#model-explorer), which is a tool that allows you to search for models based on their characteristics. More information about the AIR system can be found in the [Models page](https://docs.runware.ai/en/image-inference/models).", + }; + + const controlNetGuideImage = { + type: "string", + label: "ControlNet Guide Image 0", + description: "The guide image for ControlNet.", + }; + + const controlNetWeight = { + type: "integer", + label: "ControlNet Weight 0", + description: "The weight for ControlNet.", + }; + + const controlNetStartStep = { + type: "integer", + label: "ControlNet Start Step 0", + description: "The start step for ControlNet.", + }; + + const controlNetEndStep = { + type: "integer", + label: "ControlNet End Step 0", + description: "The end step for ControlNet.", + }; + + const controlNetControlMode = { + type: "string", + label: "ControlNet Control Mode 0", + description: "The control mode for ControlNet.", + }; + + const loraModel = { + type: "string", + label: "LoRA Model 0", + description: "We make use of the [AIR system](https://github.com/civitai/civitai/wiki/AIR-%E2%80%90-Uniform-Resource-Names-for-AI) to identify LoRA models. This identifier is a unique string that represents a specific model. You can find the AIR identifier of the LoRA model you want to use in our [Model Explorer](https://docs.runware.ai/en/image-inference/models#model-explorer), which is a tool that allows you to search for models based on their characteristics. More information about the AIR system can be found in the [Models page](https://docs.runware.ai/en/image-inference/models).", + }; + + const loraWeight = { + type: "integer", + label: "LoRA Weight 0", + description: "It is possible to use multiple LoRAs at the same time. With the `weight` parameter you can assign the importance of the LoRA with respect to the others. The sum of all `weight` parameters must always be `1`. If needed, we will increase the values proportionally to achieve it.", + optional: true, + }; + + if (structure === constants.IMAGE_INFERENCE_STRUCTURE.TEXT_TO_IMAGE.value) { + return { + outputType: { + type: "string", + label: "Output Type", + description: "Specifies the output type in which the image is returned.", + optional: true, + options: [ + "base64Data", + "dataURI", + "URL", + ], + }, + outputFormat: { + type: "string", + label: "Output Format", + description: "Specifies the format of the output image.", + optional: true, + options: [ + "PNG", + "JPG", + "WEBP", + ], + }, + negativePrompt: { + type: "string", + label: "Negative Prompt", + description: "A negative prompt is a text instruction to guide the model on generating the image. It is usually a sentence or a paragraph that provides negative guidance for the task. This parameter helps to avoid certain undesired results. For example, if the negative prompt is `red dragon, cup`, the model will follow the positive prompt but will avoid generating an image of a red dragon or including a cup. The more detailed the prompt, the more accurate the results. The length of the prompt must be between 4 and 2000 characters.", + optional: true, + }, + steps: { + type: "integer", + label: "Steps", + description: "The number of steps is the number of iterations the model will perform to generate the image. The higher the number of steps, the more detailed the image will be. However, increasing the number of steps will also increase the time it takes to generate the image and may not always result in a better image (some [schedulers](https://docs.runware.ai/en/image-inference/api-reference#request-scheduler) work differently). When using your own models you can specify a new default value for the number of steps. Defaults to `20`.", + min: 1, + max: 100, + optional: true, + }, + CFGScale: { + type: "string", + label: "CFG Scale", + description: "Guidance scale represents how closely the images will resemble the prompt or how much freedom the AI model has. Higher values are closer to the prompt. Low values may reduce the quality of the results. Min: `0`, Max: `30` Default: `7`.", + optional: true, + }, + }; + } + + if (structure === constants.IMAGE_INFERENCE_STRUCTURE.IMAGE_TO_IMAGE.value) { + return { + seedImage, + strength, + }; + } + + if (structure === constants.IMAGE_INFERENCE_STRUCTURE.IN_OUT_PAINTING.value) { + return { + seedImage, + maskImage, + strength, + }; + } + + if (structure === constants.IMAGE_INFERENCE_STRUCTURE.REFINER.value) { + return { + refinerModel: { + type: "string", + label: "Refiner Model", + description: "We make use of the [AIR system](https://github.com/civitai/civitai/wiki/AIR-%E2%80%90-Uniform-Resource-Names-for-AI) to identify refinement models. This identifier is a unique string that represents a specific model. Note that refiner models are only SDXL based. You can find the AIR identifier of the refinement model you want to use in our [Model Explorer](https://docs.runware.ai/en/image-inference/models#model-explorer), which is a tool that allows you to search for models based on their characteristics. More information about the AIR system can be found in the [Models page](https://docs.runware.ai/en/image-inference/models).", + }, + refinerStartStep: { + type: "integer", + label: "Refiner Start Step", + description: "Represents the step number at which the refinement process begins. The initial model will generate the image up to this step, after which the refiner model takes over to enhance the result. It can take values from `0` (first step) to the number of [steps](https://docs.runware.ai/en/image-inference/api-reference#request-steps) specified.", + optional: true, + }, + }; + } + + if (structure === constants.IMAGE_INFERENCE_STRUCTURE.CONTROL_NET.value) { + return { + controlNetModel1: { + ...controlNetModel, + label: "Control Net Model 1", + }, + controlNetGuideImage1: { + ...controlNetGuideImage, + label: "Control Net Guide Image 1", + }, + controlNetWeight1: { + ...controlNetWeight, + label: "Control Net Weight 1", + }, + controlNetStartStep1: { + ...controlNetStartStep, + label: "Control Net Start Step 1", + }, + controlNetEndStep1: { + label: "Control Net End Step 1", + ...controlNetEndStep, + }, + controlNetControlMode1: { + ...controlNetControlMode, + label: "Control Net Control Mode 1", + }, + controlNetModel2: { + ...controlNetModel, + label: "Control Net Model 2", + optional: true, + }, + controlNetGuideImage2: { + ...controlNetGuideImage, + label: "Control Net Guide Image 2", + optional: true, + }, + controlNetWeight2: { + ...controlNetWeight, + label: "Control Net Weight 2", + optional: true, + }, + controlNetStartStep2: { + ...controlNetStartStep, + label: "Control Net Start Step 2", + optional: true, + }, + controlNetEndStep2: { + ...controlNetEndStep, + label: "Control Net End Step 2", + optional: true, + }, + controlNetControlMode2: { + ...controlNetControlMode, + label: "Control Net Control Mode 2", + optional: true, + }, + }; + } + + if (structure === constants.IMAGE_INFERENCE_STRUCTURE.LORA.value) { + return { + loraModel1: { + ...loraModel, + label: "LoRA Model 1", + }, + loraWeight1: { + label: "LoRA Weight 1", + ...loraWeight, + }, + loraModel2: { + label: "LoRA Model 2", + ...loraModel, + optional: true, + }, + loraWeight2: { + label: "LoRA Weight 2", + ...loraWeight, + }, + }; + } + + return {}; + }, + async run({ $ }) { + const { + app, + outputType, + outputFormat, + uploadEndpoint, + checkNSFW, + includeCost, + positivePrompt, + negativePrompt, + seedImage, + maskImage, + strength, + height, + width, + model, + steps, + scheduler, + seed, + numberResults, + CFGScale, + refinerModel, + refinerStartStep, + controlNetModel1, + controlNetGuideImage1, + controlNetWeight1, + controlNetStartStep1, + controlNetEndStep1, + controlNetControlMode1, + controlNetModel2, + controlNetGuideImage2, + controlNetWeight2, + controlNetStartStep2, + controlNetEndStep2, + controlNetControlMode2, + loraModel1, + loraWeight1, + loraModel2, + loraWeight2, + } = this; + + const data = { + taskType: constants.TASK_TYPE.IMAGE_INFERENCE.value, + taskUUID: uuid(), + positivePrompt, + outputType, + outputFormat, + uploadEndpoint, + checkNSFW, + includeCost, + negativePrompt, + seedImage, + maskImage, + strength, + height, + width, + model, + steps, + scheduler, + seed: seed + ? parseInt(seed) + : undefined, + numberResults, + CFGScale, + refiner: refinerModel + ? { + model: refinerModel, + startStep: refinerStartStep, + } + : undefined, + controlNet: controlNetModel1 + ? [ + { + model: controlNetModel1, + guideImage: controlNetGuideImage1, + weight: controlNetWeight1, + startStep: controlNetStartStep1, + endStep: controlNetEndStep1, + controlMode: controlNetControlMode1, + }, + ...(controlNetModel2 + ? [ + { + model: controlNetModel2, + guideImage: controlNetGuideImage2, + weight: controlNetWeight2, + startStep: controlNetStartStep2, + endStep: controlNetEndStep2, + controlMode: controlNetControlMode2, + }, + ] + : [] + ), + ] + : undefined, + lora: loraModel1 + ? [ + { + model: loraModel1, + weight: loraWeight1, + }, + ...(loraModel2 + ? [ + { + model: loraModel2, + weight: loraWeight2, + }, + ] + : [] + ), + ] + : undefined, + }; + + const response = await app.post({ + $, + data: [ + data, + ], + }); + + $.export("$summary", `Successfully requested image inference task with UUID \`${response.data[0].taskUUID}\`.`); + return response; + }, +}; diff --git a/components/runware/actions/image-upscale/image-upscale.mjs b/components/runware/actions/image-upscale/image-upscale.mjs new file mode 100644 index 0000000000000..49879c4573a71 --- /dev/null +++ b/components/runware/actions/image-upscale/image-upscale.mjs @@ -0,0 +1,73 @@ +import { v4 as uuid } from "uuid"; +import app from "../../runware.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "runware-image-upscale", + name: "Image Upscale", + description: "Request an image upscale task to be processed by the Runware API. [See the documentation](https://docs.runware.ai/en/image-editing/upscaling).", + version: "0.0.1", + type: "action", + props: { + app, + inputImage: { + propDefinition: [ + app, + "inputImage", + ], + }, + outputType: { + propDefinition: [ + app, + "outputType", + ], + }, + outputFormat: { + propDefinition: [ + app, + "outputFormat", + ], + }, + upscaleFactor: { + type: "integer", + label: "Upscale Factor", + description: "The level of upscaling performed. Each will increase the size of the image by the corresponding factor. Eg. `2`.", + min: 2, + max: 4, + }, + includeCost: { + propDefinition: [ + app, + "includeCost", + ], + }, + }, + async run({ $ }) { + const { + app, + inputImage, + outputType, + outputFormat, + upscaleFactor, + includeCost, + } = this; + + const response = await app.post({ + $, + data: [ + { + taskType: constants.TASK_TYPE.IMAGE_UPSCALE.value, + taskUUID: uuid(), + inputImage, + outputType, + outputFormat, + upscaleFactor, + includeCost, + }, + ], + }); + + $.export("$summary", `Successfully requested image upscale task with UUID \`${response.data[0].taskUUID}\`.`); + return response; + }, +}; diff --git a/components/runware/actions/prompt-enhance/prompt-enhance.mjs b/components/runware/actions/prompt-enhance/prompt-enhance.mjs new file mode 100644 index 0000000000000..0d21956a34469 --- /dev/null +++ b/components/runware/actions/prompt-enhance/prompt-enhance.mjs @@ -0,0 +1,65 @@ +import { v4 as uuid } from "uuid"; +import app from "../../runware.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "runware-prompt-enhance", + name: "Prompt Enhance", + description: "Request a prompt enhance task to be processed by the Runware API. [See the documentation](https://docs.runware.ai/en/utilities/prompt-enhancer).", + version: "0.0.1", + type: "action", + props: { + app, + prompt: { + type: "string", + label: "Prompt", + description: "The prompt that you intend to enhance.", + }, + promptMaxLength: { + type: "integer", + label: "Prompt Max Length", + description: "Represents the maximum length of the enhanced prompt that you intend to receive. Min `12`, Max `400`.", + min: 12, + max: 400, + }, + promptVersions: { + type: "integer", + label: "Prompt Versions", + description: "The number of prompt versions that will be received. Min `1`, Max `5`.", + min: 1, + max: 5, + }, + includeCost: { + propDefinition: [ + app, + "includeCost", + ], + }, + }, + async run({ $ }) { + const { + app, + prompt, + promptMaxLength, + promptVersions, + includeCost, + } = this; + + const response = await app.post({ + $, + data: [ + { + taskUUID: uuid(), + taskType: constants.TASK_TYPE.PROMPT_ENHANCE.value, + prompt, + promptMaxLength, + promptVersions, + includeCost, + }, + ], + }); + + $.export("$summary", `Successfully requested prompt enhance task with UUID \`${response.data[0].taskUUID}\`.`); + return response; + }, +}; diff --git a/components/runware/common/constants.mjs b/components/runware/common/constants.mjs new file mode 100644 index 0000000000000..c7e2902406234 --- /dev/null +++ b/components/runware/common/constants.mjs @@ -0,0 +1,63 @@ +const BASE_URL = "https://api.runware.ai"; +const VERSION_PATH = "/v1"; + +const TASK_TYPE = { + IMAGE_INFERENCE: { + value: "imageInference", + label: "Image Inference", + }, + IMAGE_CONTROL_NET_PREPROCESS: { + value: "imageControlNetPreProcess", + label: "Image Control Net Preprocess", + }, + IMAGE_UPSCALE: { + value: "imageUpscale", + label: "Image Upscale", + }, + IMAGE_BACKGROUND_REMOVAL: { + value: "imageBackgroundRemoval", + label: "Image Background Removal", + }, + IMAGE_CAPTION: { + value: "imageCaption", + label: "Image Caption", + }, + PROMPT_ENHANCE: { + value: "promptEnhance", + label: "Prompt Enhance", + }, +}; + +const IMAGE_INFERENCE_STRUCTURE = { + TEXT_TO_IMAGE: { + value: "textToImage", + label: "Text to Image", + }, + IMAGE_TO_IMAGE: { + value: "imageToImage", + label: "Image to Image", + }, + IN_OUT_PAINTING: { + value: "inOutpainting", + label: "In/Outpainting", + }, + REFINER: { + value: "refiner", + label: "Refiner", + }, + CONTROL_NET: { + value: "controlNet", + label: "Control Net", + }, + LORA: { + value: "lora", + label: "LoRA", + }, +}; + +export default { + BASE_URL, + VERSION_PATH, + IMAGE_INFERENCE_STRUCTURE, + TASK_TYPE, +}; diff --git a/components/runware/common/utils.mjs b/components/runware/common/utils.mjs new file mode 100644 index 0000000000000..2e10f7e6444f5 --- /dev/null +++ b/components/runware/common/utils.mjs @@ -0,0 +1,28 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function parseArray(value) { + try { + if (!value) { + return; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +export default { + parseArray, +}; diff --git a/components/runware/package.json b/components/runware/package.json new file mode 100644 index 0000000000000..345513e700ef6 --- /dev/null +++ b/components/runware/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/runware", + "version": "0.2.0", + "description": "Pipedream Runware Components", + "main": "runware.app.mjs", + "keywords": [ + "pipedream", + "runware" + ], + "homepage": "https://pipedream.com/apps/runware", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3", + "uuid": "^10.0.0" + } +} diff --git a/components/runware/runware.app.mjs b/components/runware/runware.app.mjs new file mode 100644 index 0000000000000..8fea1cf1c8ff4 --- /dev/null +++ b/components/runware/runware.app.mjs @@ -0,0 +1,93 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "runware", + propDefinitions: { + taskType: { + type: "string", + label: "Task Type", + description: "The type of task to be processed by the Runware API.", + options: Object.values(constants.TASK_TYPE), + }, + outputType: { + type: "string", + label: "Output Type", + description: "Specifies the output type in which the image is returned.", + optional: true, + options: [ + "base64Data", + "dataURI", + "URL", + ], + }, + outputFormat: { + type: "string", + label: "Output Format", + description: "Specifies the format of the output image.", + optional: true, + options: [ + "PNG", + "JPG", + "WEBP", + ], + }, + includeCost: { + type: "boolean", + label: "Include Cost", + description: "If set to `true`, the cost to perform the task will be included in the response object. Defaults to `false`.", + optional: true, + }, + height: { + type: "integer", + label: "Height", + description: "Used to define the height dimension of the generated image. Certain models perform better with specific dimensions. The value must be divisible by 64, eg: `512`, `576`, `640` ... `2048`.", + min: 512, + max: 2048, + }, + width: { + type: "integer", + label: "Width", + description: "Used to define the width dimension of the generated image. Certain models perform better with specific dimensions. The value must be divisible by 64, eg: `512`, `576`, `640` ... `2048`.", + min: 512, + max: 2048, + }, + inputImage: { + type: "string", + label: "Input Image", + description: "Specifies the input image to be processed. The image can be specified in one of the following formats:\n - An UUID v4 string of a [previously uploaded image](https://docs.runware.ai/en/getting-started/image-upload) or a [generated image](https://docs.runware.ai/en/image-inference/api-reference).\n - A data URI string representing the image. The data URI must be in the format `data:;base64,` followed by the base64-encoded image. For example: `data:image/png;base64,iVBORw0KGgo...`.\n - A base64 encoded image without the data URI prefix. For example: `iVBORw0KGgo...`.\n - A URL pointing to the image. The image must be accessible publicly.\nSupported formats are: PNG, JPG and WEBP.", + }, + }, + methods: { + getUrl(path = "") { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Content-Type": "application/json", + "Authorization": `Bearer ${this.$auth.api_key}`, + }; + }, + async makeRequest({ + $ = this, path, headers, ...args + }) { + const response = await axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + if (response.errors) { + throw new Error(JSON.stringify(response.errors)); + } + return response; + }, + post(args = {}) { + return this.makeRequest({ + method: "POST", + ...args, + }); + }, + }, +}; diff --git a/components/rytr/README.md b/components/rytr/README.md new file mode 100644 index 0000000000000..0e746688734d1 --- /dev/null +++ b/components/rytr/README.md @@ -0,0 +1,11 @@ +# Overview + +The Rytr API lets you automate content creation, leveraging AI to generate text for various purposes like email drafts, blog ideas, social media posts, and more. On Pipedream, you can build workflows that capitalize on Rytr's capabilities, integrating with other apps to create dynamic, content-focused automations. Whether you're populating a CMS with blog posts, drafting emails based on CRM updates, or generating social media content from trending topics, Pipedream makes it easy to harness Rytr's AI writing assistant within your custom workflows. + +# Example Use Cases + +- **Content Generation Triggered by Calendar Events**: Automate blog post creation by connecting Google Calendar to Rytr via Pipedream. Schedule events with topics, and when the event fires, use Rytr to generate a draft based on the event's details, ready for review and publication. + +- **Email Drafting from CRM Updates**: Pair Rytr with a CRM app like Salesforce on Pipedream. When a sales opportunity status changes, trigger Rytr to create personalized email content that corresponds with the update, allowing for quick, tailored communication with potential customers. + +- **Social Media Content from Trending Topics**: Link Rytr to a social listening platform like Twitter. Monitor for trending hashtags or keywords, and use Rytr to craft relevant and engaging social media posts that resonate with current trends, keeping your brand's social presence timely and relevant. diff --git a/components/ryver/README.md b/components/ryver/README.md new file mode 100644 index 0000000000000..638d776be2220 --- /dev/null +++ b/components/ryver/README.md @@ -0,0 +1,11 @@ +# Overview + +The Ryver API allows you to automate interactions with the Ryver platform, a tool for team communication and task management. By leveraging the API on Pipedream, you can create custom workflows that respond to events in Ryver, send messages, manage tasks, users, and teams, or integrate with other services to extend Ryver's functionality. With Pipedream's serverless platform, you can design scalable and maintenance-free automations that react in real-time to your business needs. + +# Example Use Cases + +- **Automate Task Creation from Emails**: When you receive an email to a designated address, use Pipedream to parse the email content and automatically create a new task in Ryver. This can be especially useful for managing customer support requests or streamlining how your team handles incoming tasks. + +- **Sync Ryver Notifications with Calendar Events**: Connect Ryver to Google Calendar using Pipedream. When a new event is added to a specific Google Calendar, a notification can be posted to a Ryver forum or team, ensuring everyone is aware of upcoming meetings or deadlines. + +- **Integrate Customer Feedback into Ryver**: Combine Ryver with a customer feedback tool like Typeform. Whenever a new form entry is submitted, Pipedream can capture the data and create a post in Ryver to discuss the feedback with your team, or add it as a topic in a relevant forum for broader team analysis and response. diff --git a/components/sailpoint/README.md b/components/sailpoint/README.md index 5fa765d49c448..d5af36bcd47f2 100644 --- a/components/sailpoint/README.md +++ b/components/sailpoint/README.md @@ -1,24 +1,11 @@ # Overview -SailPoint is a powerful set of APIs that allow you to use advanced identity -management capabilities in your applications. With SailPoint, you can -dramatically reduce risk and create efficiencies across your organization. +The SailPoint API enables the automation of identity governance tasks, streamlining user access management, compliance controls, and security policies across an organization's IT environment. It's a powerful tool for managing identities, entitlements, and role-based access controls, ensuring that the right people have the right access at the right time. With Pipedream, you can use this API to create workflows that react to specific events, orchestrate multi-step processes, and integrate with other services to enhance identity operations. -The SailPoint API provides a wide range of features that you can use to create -powerful identity management solutions. Here are a few examples of applications -you can build with the SailPoint API: +# Example Use Cases -- Authentication & Authorization: Determine access to resources and - applications, such as user provisioning and single sign-on -- Self-service Identity Management: Allow users to update or manage their own - accounts and profiles -- Data Governance: Establish policies and procedures for the collection, - storage, and access of sensitive data -- Social Login: Enable users to log into applications using their favorite - social media accounts -- Risk Management & Compliance: Monitor user access to ensure adherence to - security protocols and compliance regulations +- **Automated User Onboarding and Offboarding**: Trigger a workflow in Pipedream when a new employee is added to your HR system, such as BambooHR. Automatically create accounts in SailPoint, assign appropriate access rights based on role, and send an email to the new user with their account details using SendGrid. Conversely, when an employee leaves, ensure access is revoked across all systems and confirm deprovisioning with a message to a Slack channel. -These are just a few examples of the types of applications you can build with -the SailPoint API. With the right approach and a little creativity, you can -create a unique identity management solution tailored to your needs. +- **Periodic Access Reviews and Compliance Audits**: Schedule a recurring workflow in Pipedream that initiates an access review process in SailPoint. Fetch a list of users and their access rights, compare it against compliance requirements or a predefined access matrix, and flag any discrepancies. The workflow could then open a ticket in a system like ServiceNow, requesting review and adjustment of access rights. + +- **Real-time Security Alerts and Remediation**: Monitor SailPoint for irregular access patterns or policy violations using Pipedream's event sources. On detecting an anomaly, the workflow could post an alert to a security team's Slack channel, create an incident in PagerDuty, and temporarily suspend the user's access pending investigation, all automatically, reducing response time and potential security risks. diff --git a/components/sakari_sms/README.md b/components/sakari_sms/README.md new file mode 100644 index 0000000000000..4759725220edb --- /dev/null +++ b/components/sakari_sms/README.md @@ -0,0 +1,11 @@ +# Overview + +The Sakari SMS API allows you to send and receive text messages programmatically, giving you the power to create custom messaging experiences within your apps. On Pipedream, you can harness this capability to build serverless workflows that integrate SMS with various services and automate communication processes. Whether it's sending notifications, processing inbound messages, or coordinating with other APIs, the possibilities are broad. + +# Example Use Cases + +- **Automated Customer Support Notifications**: Trigger an SMS to a customer when a support ticket is updated in your helpdesk platform (like Zendesk or Freshdesk). The workflow can listen for ticket updates and use Sakari SMS to send a text to the customer with the status of their request. + +- **Order Confirmation Texts**: Connect Sakari SMS with an e-commerce platform (like Shopify or WooCommerce). Send an order confirmation and shipping updates via SMS whenever a new order is placed or a shipping status is updated, keeping customers informed in real-time. + +- **Two-Factor Authentication (2FA) System**: Implement an extra layer of security for your app by using Sakari SMS to send 2FA codes. Set up a Pipedream workflow that triggers an SMS with a one-time passcode when a user attempts to log in or perform a sensitive action within your system. diff --git a/components/sakari_sms/package.json b/components/sakari_sms/package.json index 9ee7d1e133b23..dd8ba3ec8cb09 100644 --- a/components/sakari_sms/package.json +++ b/components/sakari_sms/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/saleor/README.md b/components/saleor/README.md new file mode 100644 index 0000000000000..5e8bf66ed0df2 --- /dev/null +++ b/components/saleor/README.md @@ -0,0 +1,11 @@ +# Overview + +Saleor API enables developers to craft a robust e-commerce platform by providing endpoints for product management, orders, customers, and more. With Pipedream's serverless platform, you can automate workflows integrating Saleor's capabilities with various other apps and services. Whether you wish to synchronize inventory, trigger customized marketing campaigns upon certain actions, or streamline order processing, the Saleor API in Pipedream serves as a bridge to connect your e-commerce data with the wider ecosystem of online tools. + +# Example Use Cases + +- **Automated Order Confirmation Emails**: When a new order is placed in Saleor, Pipedream triggers a workflow that sends a personalized confirmation email to the customer using a service like SendGrid. This ensures that customers receive timely order details and improves the overall shopping experience. + +- **Real-time Inventory Sync with Google Sheets**: Keep inventory levels in check by using Saleor's webhook for product updates to trigger a Pipedream workflow. This workflow updates a Google Sheet in real-time with current stock numbers, providing a quick and accessible view of your inventory status. + +- **Slack Notifications for High-Value Orders**: Create a Pipedream workflow that monitors new orders from the Saleor API; when an order exceeds a certain value, it sends a notification to a dedicated Slack channel. This allows your team to act quickly on high-priority sales and provide exceptional service to your top customers. diff --git a/components/saleor/package.json b/components/saleor/package.json new file mode 100644 index 0000000000000..7c7cef90cd517 --- /dev/null +++ b/components/saleor/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/saleor", + "version": "0.0.1", + "description": "Pipedream Saleor Components", + "main": "saleor.app.mjs", + "keywords": [ + "pipedream", + "saleor" + ], + "homepage": "https://pipedream.com/apps/saleor", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/saleor/saleor.app.mjs b/components/saleor/saleor.app.mjs new file mode 100644 index 0000000000000..0e31fb43413b5 --- /dev/null +++ b/components/saleor/saleor.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "saleor", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/sales_simplify/README.md b/components/sales_simplify/README.md index c8de8eea68548..1a3481c018bfe 100644 --- a/components/sales_simplify/README.md +++ b/components/sales_simplify/README.md @@ -1,18 +1,11 @@ # Overview -Sales Simplify offers a range of powerful tools and APIs to help businesses -streamline their sales and marketing processes. With the Sales Simplify API, -developers can create product features, integrate customer data, and track -performance all from one central platform. Here are a few examples of what you -can build using the Sales Simplify API. +The Sales Simplify API, integrated within Pipedream's ecosystem, is a powerhouse for automating sales and customer relationship tasks. Utilize this API to streamline lead management, automate follow-ups, and track customer interactions systematically. With Pipedream, you can effortlessly create serverless workflows that link Sales Simplify with various other apps, providing a cohesive and efficient automation system. Harness the capability to trigger events based on new leads, update customer records in real-time, and generate data-driven insights that keep your sales pipeline flowing smoothly. -Examples of what you can build: +# Example Use Cases -- E-commerce stores -- Financial and accounting applications -- Contact and customer management solutions -- Automated marketing systems -- Lead generation and tracking systems -- CRM suites -- Invoicing and payment solutions -- Reporting and analytics applications +- **Lead Qualification Automation**: When a new lead is captured in Sales Simplify, Pipedream triggers a workflow that automatically qualifies the lead based on preset criteria. If qualified, the lead is added to a CRM like Salesforce, and a personalized email sequence is initiated via SendGrid, ensuring prompt and relevant engagement. + +- **Sales Activity Digest**: Compile a daily or weekly summary of sales activities and performance metrics from Sales Simplify and send it to a Slack channel. This workflow keeps the sales team informed and aligned, using Pipedream to fetch the data, process it, and deliver insightful reports to the right stakeholders without manual intervention. + +- **Customer Support Ticketing**: Integrate Sales Simplify with a ticketing system like Zendesk. Use Pipedream to monitor customer interactions. When a potential issue is detected, such as a negative sentiment in communication, automatically create a support ticket in Zendesk. This ensures swift action and maintains a high standard of customer service. diff --git a/components/salesblink/README.md b/components/salesblink/README.md index a4027c82e4a69..0bf854ddbafda 100644 --- a/components/salesblink/README.md +++ b/components/salesblink/README.md @@ -1,25 +1,11 @@ # Overview -Welcome to SalesBlink - A leading cloud-based application programming interface -(API) used to develop innovative sales applications. With our API, you can -create powerful applications to improve the efficiency and effectiveness of -your sales process. From customer relationship management to lead generation to -remarketing, SalesBlink provides the flexibility and power to tailor your -experience around your sales process needs and preferences. +SalesBlink is a comprehensive sales outreach automation suite that enables users to find prospects, outreach, and close deals. Leveraging the SalesBlink API on Pipedream allows for creating custom automated workflows to streamline the lead generation, outreach, and follow-up processes. Developers can use this integration to sync leads between platforms, automate outreach campaigns, follow up systematically, and track the entire sales pipeline effectively. -Here are just a few examples of what you can create with SalesBlink's API: +# Example Use Cases -- Customer Relationship Management (CRM): Easily manage leads, contacts, and - customer information. -- Contact Synchronization: Synchronize SalesBlink contact information with - external systems. -- Document Templates: Create dynamic, templated documents for use in sales - processes. -- Remarketing Automation and Tools: Automate customer follow-up and targeted - marketing campaigns. -- Lead Management: Manage leads across multiple platforms and channels. -- Sales Reports and Analytics: Generate sales reports, analyze performance, and - make informed decisions. -- Email Marketing: Use SalesBlink's email automation tools to send emails to - customers based on their preferences. -- Customer Support: Manage customer support tickets and service requests. +- **Lead Generation to Google Sheets:** Trigger a Pipedream workflow whenever new prospects are found using SalesBlink's prospecting tools. Automatically add these prospects' details to a Google Sheets spreadsheet for further analysis or to share with your team. + +- **CRM Synchronization:** Sync lead status updates from SalesBlink to your CRM system. When a lead moves down the sales funnel in SalesBlink, use Pipedream to update their status in your CRM, like Salesforce or HubSpot, ensuring all teams have the most current info. + +- **Custom Email Follow-Up Sequences:** After sending an initial outreach email through SalesBlink, set up a Pipedream workflow to monitor replies. If a prospect hasn't responded within a set period, trigger a follow-up email sequence directly from SalesBlink to keep the conversation going. diff --git a/components/salesflare/README.md b/components/salesflare/README.md index bb1c15c238b93..807667c59fa89 100644 --- a/components/salesflare/README.md +++ b/components/salesflare/README.md @@ -1,22 +1,11 @@ # Overview -The Salesflare API provides powerful tools for businesses to maximize their -customer experience. It offers a range of features that allow you to create -custom integrations and automated processes to help you make the most of your -customer data. Here are just a few examples of what you can build: +The Salesflare API on Pipedream enables the automation of CRM tasks, enhancing customer relationship management with minimal manual effort. It provides seamless integration with Salesflare's functionalities such as managing contacts, accounts, opportunities, and tasks. By tapping into this API, you can synchronize customer data, automate follow-up reminders, or trigger personalized campaigns based on customer interactions. Essentially, it offers a bridge between Salesflare's rich CRM features and the plethora of apps supported by Pipedream, allowing for sophisticated, custom automation workflows that save time and boost efficiency. -- Automated customer data enrichments that continuously keep customer profiles - up to date -- Automated task and reminder systems to keep your teams on track -- Custom in-app notifications to quickly alert customers to new deals and - offers -- Integrate Salesflare with your existing 3rd-party services to streamline - workflows -- Build custom customer segmentation systems -- Automatically generate customer insights and trends -- Connect customer data across all sources to provide a 360 view -- Utilize machine learning to predict customer behavior and trends -- Model customer lifetime value -- Custom reporting to measure success -- Automated surveys and customer feedback loops -- Export customer data to any 3rd-party service. +# Example Use Cases + +- **Lead Scoring and Prioritization**: Automate the process of scoring leads based on interaction data from Salesflare. Use this information to prioritize follow-ups by creating a workflow that sorts leads into different buckets (hot, warm, cold) and assigns them to the sales team accordingly. + +- **Automated Data Sync Between Salesflare and Email Marketing Platforms**: Set up a workflow that triggers whenever a new contact is added to Salesflare. Automatically add that contact to your email marketing platform, like Mailchimp, to ensure your mailing list is always up-to-date and can receive the latest marketing campaigns. + +- **Customer Onboarding Sequences Based on Deal Closure**: When a deal is marked as won in Salesflare, trigger an onboarding sequence for your customer. This workflow would connect with project management tools such as Asana, creating tasks for your team to kickstart the onboarding process, and sending a personalized welcome email to the new customer. diff --git a/components/salesforce_rest_api/README.md b/components/salesforce_rest_api/README.md index 96913f9981580..96bf743f61495 100644 --- a/components/salesforce_rest_api/README.md +++ b/components/salesforce_rest_api/README.md @@ -1,3 +1,7 @@ +# Overview + +The Salesforce (REST API) provides a powerful platform for creating and managing customer relationships with a wide array of features like data manipulation, querying, and complex automation. With Pipedream's serverless execution, you can create workflows that automate your sales processes, sync data with other platforms, enhance customer engagement, and trigger actions based on specific events. Dive into Salesforce data, streamline lead management, track customer interactions, and push or pull data to or from Salesforce seamlessly. + # Getting Started You can install the Pipedream Salesforce app in the [Accounts](https://pipedream.com/accounts) section of your Pipedream account, or directly in a workflow. @@ -65,6 +69,24 @@ If you'd like to utilize Pipedream's webhook triggers, you will need to add the 11. You should now be able to use the Salesforce integration along with the webhook triggers if you configured the required permissions above. +# Example Use Cases + +## **Synchronization between Salesforce and External Databases** + +Synchronize Salesforce with external databases to ensure data accuracy and consistency without manual effort. + +## **Real-Time Customer Support Notifications** + +Improve customer service responsiveness by triggering real-time notifications to support teams when new tickets are created. + +## **Lead Scoring and Engagement Automation** + +Streamline lead management by automatically scoring and engaging leads through personalized campaigns, enhancing conversion rates. + +## **Dynamic Inventory Management** + +Keep inventory levels accurate and responsive to sales activity by updating Salesforce in real-time based on e-commerce data. + # Troubleshooting @@ -103,3 +125,73 @@ If you happen to stumble on the error: `UNKNOWN_EXCEPTION: admin operation alrea 15. Back to the Pipedream Workflow, create a new step with the **Salesforce Convert SOAP Object** action. 16. In the **XML Soap Object** field, select the path from the trigger or type in `{{steps.trigger.event.body}}`. 17. That's it! You can now deploy the workflow and you will receive instant updates from Salesforce. + +## API Errors + +Below is a list of potential HTTP response status codes with their corresponding causes: + +### **400** + +The request couldn't be understood, usually because the JSON or XML body contains an error. + +### **401** + +The session ID or OAuth token used has expired or is invalid. The response body contains the message and errorCode. + +### **403** + +The request has been refused. Check that the logged-in user has appropriate permissions. If the error code is REQUEST_LIMIT_EXCEEDED, the API request limits in your org have been exceeded. + +### **404** + +The requested resource couldn't be found. Check the URI for errors, and ensure there are no sharing issues. + +### **405** + +The method specified isn’t allowed for the resource indicated in the URI. + +### **409** + +The request couldn’t be completed due to a conflict with the current state of the resource. Ensure the API version is compatible with the resource requested. + +### **410** + +The requested resource has been retired or removed. Update or delete references to the resource. + +### **412** + +The request wasn't executed because it didn't meet one or more preconditions specified in the request headers. + + +### **414** + +The length of the URI exceeds the 16,384-byte limit. + +### **415** + +The entity in the request is in a format that's not supported by the specified method. + +### **420** + +Salesforce Edge doesn’t have routing information for the request host. Contact Salesforce Customer Support. + +### **428** + +The request wasn’t executed because it wasn't conditional. Add a Conditional Request Header like If-Match to the request. + +### **431** + +The combined length of the URI and headers exceeds the 16,384-byte limit. + +### **500** + +An error within Lightning Platform has occurred, preventing request completion. Contact Salesforce Customer Support. + +### **502** + +Salesforce Edge couldn’t communicate successfully with the Salesforce instance. + +### **503** + +The server is currently unable to handle the request, typically due to maintenance or being overloaded. + diff --git a/components/salesforce_rest_api/actions/add-contact-to-campaign/add-contact-to-campaign.mjs b/components/salesforce_rest_api/actions/add-contact-to-campaign/add-contact-to-campaign.mjs index e3c2745f4262a..e34e0fbf29b30 100644 --- a/components/salesforce_rest_api/actions/add-contact-to-campaign/add-contact-to-campaign.mjs +++ b/components/salesforce_rest_api/actions/add-contact-to-campaign/add-contact-to-campaign.mjs @@ -1,52 +1,52 @@ -import salesForceRestApi from "../../salesforce_rest_api.app.mjs"; -import { - removeNullEntries, toSingleLineString, -} from "../../common/utils.mjs"; +import salesforce from "../../salesforce_rest_api.app.mjs"; import constants from "../../common/constants.mjs"; export default { key: "salesforce_rest_api-add-contact-to-campaign", name: "Add Contact to Campaign", - description: toSingleLineString(` - Adds an existing contact to an existing campaign. - See [Event SObject](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_campaignmember.htm) - and [Create Record](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_sobject_create.htm) - `), - version: "0.0.6", + description: "Adds an existing contact to an existing campaign. [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_campaignmember.htm)", + version: "0.1.0", type: "action", props: { - salesForceRestApi, + salesforce, campaignId: { propDefinition: [ - salesForceRestApi, - "sobjectId", + salesforce, + "recordId", () => ({ - objectType: constants.OBJECT_TYPE.CAMPAIGN, + objType: "Campaign", + nameField: "Name", }), ], label: "Campaign ID", - description: "ID of the Campaign to which this Lead is associated.", + description: "The Campaign to add a Contact to.", }, contactId: { propDefinition: [ - salesForceRestApi, - "sobjectId", + salesforce, + "recordId", () => ({ - objectType: constants.OBJECT_TYPE.CONTACT, + objType: "Contact", + nameField: "Name", }), ], label: "Contact ID", - description: "ID of the Contact who is associated with a Campaign.", + description: "The Contact to add to the selected Campaign.", }, }, async run({ $ }) { - const data = removeNullEntries({ - CampaignId: this.campaignId, - ContactId: this.contactId, + const { + salesforce, campaignId, contactId, + } = this; + const response = await salesforce.createObject({ + $, + objectType: constants.OBJECT_TYPE.CAMPAIGN_MEMBER, + data: { + CampaignId: campaignId, + ContactId: contactId, + }, }); - const response = await this.salesForceRestApi - .createObject(constants.OBJECT_TYPE.CAMPAIGN_MEMBER, data); - response && $.export("$summary", "Successfully added contact to campaign"); + $.export("$summary", `Successfully added contact (ID: ${contactId}) to campaign (ID: ${campaignId})`); return response; }, }; diff --git a/components/salesforce_rest_api/actions/add-lead-to-campaign/add-lead-to-campaign.mjs b/components/salesforce_rest_api/actions/add-lead-to-campaign/add-lead-to-campaign.mjs index 15bedd89e365a..ac4dc7fe5aa01 100644 --- a/components/salesforce_rest_api/actions/add-lead-to-campaign/add-lead-to-campaign.mjs +++ b/components/salesforce_rest_api/actions/add-lead-to-campaign/add-lead-to-campaign.mjs @@ -1,51 +1,52 @@ -import salesForceRestApi from "../../salesforce_rest_api.app.mjs"; -import { - removeNullEntries, toSingleLineString, -} from "../../common/utils.mjs"; +import salesforce from "../../salesforce_rest_api.app.mjs"; import constants from "../../common/constants.mjs"; export default { key: "salesforce_rest_api-add-lead-to-campaign", name: "Add Lead to Campaign", - description: toSingleLineString(` - Adds an existing lead to an existing campaign. - See [Event SObject](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_campaignmember.htm) - and [Create Record](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_sobject_create.htm) - `), - version: "0.0.6", + description: "Adds an existing lead to an existing campaign. [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_campaignmember.htm)", + version: "0.1.0", type: "action", props: { - salesForceRestApi, + salesforce, campaignId: { propDefinition: [ - salesForceRestApi, - "sobjectId", + salesforce, + "recordId", () => ({ - objectType: constants.OBJECT_TYPE.CAMPAIGN, + objType: "Campaign", + nameField: "Name", }), ], label: "Campaign ID", - description: "ID of the Campaign to which this Lead is associated.", + description: "The Campaign to add a Lead to.", }, leadId: { propDefinition: [ - salesForceRestApi, - "sobjectId", + salesforce, + "recordId", () => ({ - objectType: constants.OBJECT_TYPE.LEAD, + objType: "Lead", + nameField: "Name", }), ], label: "Lead ID", - description: "ID of the Lead who is associated with a Campaign.", + description: "The Lead to add to the selected Campaign.", }, }, async run({ $ }) { - const data = removeNullEntries({ - CampaignId: this.campaignId, - LeadId: this.leadId, + const { + salesforce, campaignId, leadId, + } = this; + const response = await salesforce.createObject({ + $, + objectType: constants.OBJECT_TYPE.CAMPAIGN_MEMBER, + data: { + CampaignId: campaignId, + LeadId: leadId, + }, }); - const response = await this.salesForceRestApi.createObject("CampaignMember", data); - response && $.export("$summary", "Successfully added lead to campaign"); + $.export("$summary", `Successfully added lead (ID: ${leadId}) to campaign (ID: ${campaignId})`); return response; }, }; diff --git a/components/salesforce_rest_api/actions/common/base-create-update.mjs b/components/salesforce_rest_api/actions/common/base-create-update.mjs new file mode 100644 index 0000000000000..62d0daf4f2a75 --- /dev/null +++ b/components/salesforce_rest_api/actions/common/base-create-update.mjs @@ -0,0 +1,111 @@ +import { ConfigurationError } from "@pipedream/platform"; +import salesforce from "../../salesforce_rest_api.app.mjs"; +import { getAdditionalFields } from "../../common/props-utils.mjs"; + +export const additionalFields = { + type: "object", + label: "Additional Fields", + description: + "Other fields to set for this record. Values will be parsed as JSON where applicable.", + optional: true, +}; + +export function getProps({ + objType, + createOrUpdate = "create", + docsLink = "https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_concepts.htm", + showDocsInfo = true, + showDateInfo = false, +}) { + let { initialProps } = objType; + if (initialProps && createOrUpdate === "update") { + initialProps = Object.fromEntries( + Object.entries(initialProps).map(([ + key, + value, + ]) => [ + key, + { + ...value, + optional: true, + }, + ]), + ); + } + + return { + salesforce, + ...showDocsInfo && { + docsInfo: { + type: "alert", + alertType: "info", + content: `[See the documentation](${docsLink}) for more information on available fields.`, + }, + }, + ...showDateInfo && { + dateInfo: { + type: "alert", + alertType: "warning", + content: "Date fields should be a [valid date string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format) or a Unix timestamp in milliseconds. Example values: `2022-01-15T18:30:00.000Z` or `1642271400000`.", + }, + }, + ...objType[createOrUpdate === "create" + ? "createProps" + : "updateProps"], + ...initialProps, + useAdvancedProps: { + propDefinition: [ + salesforce, + "useAdvancedProps", + ], + }, + }; +} + +export default { + methods: { + getObjectType() { + return ""; + }, + getAdvancedProps() { + return {}; + }, + getAdditionalFields, + formatDateTimeProps(props = {}) { + // https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_valid_date_formats.htm + return Object.fromEntries(Object.entries(props).filter(([ + , value, + ]) => value !== undefined) + .map(([ + key, + value, + ]) => { + const numValue = Number(value); + const date = new Date(Number.isNaN(numValue) + ? value + : numValue); + if (Number.isNaN(date.valueOf())) { + throw new ConfigurationError(`Invalid date format for prop \`${key}\`. Please provide a valid date format.`); + } + return [ + key, + date.toISOString(), + ]; + })); + }, + }, + async additionalProps() { + const objectType = this.getObjectType(); + if (!this.useAdvancedProps || !objectType) return {}; + + const fields = (await this.salesforce.getFieldsForObjectType(objectType)); + const fieldNames = fields.map((f) => f.name); + const filteredProps = Object.fromEntries(Object.entries(this.getAdvancedProps()).filter(([ + key, + ]) => fieldNames.includes(key) || key[0] === key[0].toLowerCase())); + return { + ...filteredProps, + additionalFields, + }; + }, +}; diff --git a/components/salesforce_rest_api/actions/common/base.mjs b/components/salesforce_rest_api/actions/common/base.mjs deleted file mode 100644 index 251cd3f8200a6..0000000000000 --- a/components/salesforce_rest_api/actions/common/base.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import salesforce from "../../salesforce_rest_api.app.mjs"; - -export default { - props: { - salesforce, - }, - methods: { - additionalProps(selector, sobject) { - if (!selector || !sobject) { - return {}; - } - return selector.reduce((props, prop) => ({ - ...props, - [prop]: sobject[prop], - }), {}); - }, - }, -}; diff --git a/components/salesforce_rest_api/actions/convert-soap-xml-to-json/convert-soap-xml-to-json.mjs b/components/salesforce_rest_api/actions/convert-soap-xml-to-json/convert-soap-xml-to-json.mjs index 3da34fd40dcf4..076f0e9979af2 100644 --- a/components/salesforce_rest_api/actions/convert-soap-xml-to-json/convert-soap-xml-to-json.mjs +++ b/components/salesforce_rest_api/actions/convert-soap-xml-to-json/convert-soap-xml-to-json.mjs @@ -5,10 +5,17 @@ export default { key: "salesforce_rest_api-convert-soap-xml-to-json", name: "Convert SOAP XML Object to JSON", description: "Converts a SOAP XML Object received from Salesforce to JSON", - version: "0.0.5", + version: "0.0.6", type: "action", props: { salesforce_rest_api, + infoBox: { + type: "alert", + alertType: "info", + content: `This action is useful in conjunction with Salesforce Flow Builder, and is primarily used if Instant triggers are not working for your use case. +\\ +[See the documentation](https://pipedream.com/apps/salesforce-rest-api#troubleshooting) for more details.`, + }, xml: { type: "string", label: "XML Soap Object", @@ -17,14 +24,14 @@ export default { extractNotificationOnly: { type: "boolean", label: "Extract Notifications Only", - description: "Extracts only the notification parts from the XML. Default: `true`.", + description: "Whether to extract only the notification parts from the XML. Default: `true`.", optional: true, default: true, }, failOnError: { type: "boolean", label: "Fail on Error", - description: "If should fail on error when extracting notifications. Default: `false`.", + description: "Whether the action should fail if an error occurs when extracting notifications. Default: `false`.", optional: true, default: false, }, @@ -44,6 +51,7 @@ export default { try { const notifications = json.elements[0].elements[0].elements[0].elements .filter(({ name }) => name === "Notification"); + $.export("$summary", "Successfully converted to JSON and extracted notifications"); return { notifications, }; diff --git a/components/salesforce_rest_api/actions/create-account/create-account.mjs b/components/salesforce_rest_api/actions/create-account/create-account.mjs index 892c38806ab0c..8ecf098b909bf 100644 --- a/components/salesforce_rest_api/actions/create-account/create-account.mjs +++ b/components/salesforce_rest_api/actions/create-account/create-account.mjs @@ -1,52 +1,49 @@ -import common from "../common/base.mjs"; +import common, { getProps } from "../common/base-create-update.mjs"; import account from "../../common/sobjects/account.mjs"; -import { - pickBy, pick, -} from "lodash-es"; -import { toSingleLineString } from "../../common/utils.mjs"; -const { salesforce } = common.props; +export const docsLink = "https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_account.htm"; export default { ...common, key: "salesforce_rest_api-create-account", name: "Create Account", - description: toSingleLineString(` - Creates a Salesforce account, representing an individual account, - which is an organization or person involved with your business (such as customers, competitors, and partners). - See [Account SObject](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_account.htm) - and [Create Record](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_sobject_create.htm) - `), - version: "0.2.7", + description: `Creates a Salesforce account. [See the documentation](${docsLink})`, + version: "0.3.1", type: "action", - props: { - salesforce, - Name: { - type: "string", - label: "Name", - description: "Name of the account.", + methods: { + ...common.methods, + getObjectType() { + return "Account"; }, - selector: { - propDefinition: [ - salesforce, - "fieldSelector", - ], - description: `${salesforce.propDefinitions.fieldSelector.description} Account`, - options: () => Object.keys(account), - reloadProps: true, + getAdvancedProps() { + return account.extraProps; }, }, - additionalProps() { - return this.additionalProps(this.selector, account); - }, + props: getProps({ + objType: account, + docsLink, + }), async run({ $ }) { - const data = pickBy(pick(this, [ - "Name", - ...this.selector, - ])); - const response = await this.salesforce.createAccount({ + /* eslint-disable no-unused-vars */ + const { + salesforce, + getAdvancedProps, + getObjectType, + getAdditionalFields, + formatDateTimeProps, + useAdvancedProps, + docsInfo, + dateInfo, + additionalFields, + ...data + } = this; + /* eslint-enable no-unused-vars */ + const response = await salesforce.createRecord("Account", { $, - data, + data: { + ...data, + ...getAdditionalFields(), + }, }); $.export("$summary", `Successfully created account "${this.Name}"`); return response; diff --git a/components/salesforce_rest_api/actions/create-attachment/create-attachment.mjs b/components/salesforce_rest_api/actions/create-attachment/create-attachment.mjs index 0bee1fe1087d4..ccfe0edd3eb4b 100644 --- a/components/salesforce_rest_api/actions/create-attachment/create-attachment.mjs +++ b/components/salesforce_rest_api/actions/create-attachment/create-attachment.mjs @@ -1,63 +1,49 @@ -import common from "../common/base.mjs"; +import common, { getProps } from "../common/base-create-update.mjs"; import attachment from "../../common/sobjects/attachment.mjs"; -import { - pickBy, pick, -} from "lodash-es"; -import { toSingleLineString } from "../../common/utils.mjs"; +import fs from "fs"; -const { salesforce } = common.props; +const docsLink = "https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_attachment.htm"; + +/* eslint-disable no-unused-vars */ +const { + useAdvancedProps, ...props +} = getProps({ + objType: attachment, + docsLink, +}); +/* eslint-enable no-unused-vars */ export default { ...common, key: "salesforce_rest_api-create-attachment", name: "Create Attachment", - description: toSingleLineString(` - Creates an attachment, which represents a file that a User has uploaded and attached to a parent object. - See [Attachment SObject](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_attachment.htm) - and [Create Record](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_sobject_create.htm) - `), - version: "0.3.7", + description: `Creates an Attachment on a parent object. [See the documentation](${docsLink})`, + version: "0.4.1", type: "action", - props: { - salesforce, - Body: { - type: "string", - label: "Body", - description: "Encoded file data.", - }, - Name: { - type: "string", - label: "Name", - description: "Name of the attached file. Maximum size is 255 characters.", - }, - ParentId: { - type: "string", - label: "Parent ID", - description: "ID of the parent object of the attachment. The following objects are supported as parents of attachments:\n* Account\n* Asset\n* Campaign\n* Case\n* Contact\n* Contract\n* Custom objects\n* EmailMessage\n* EmailTemplate\n* Event\n* Lead\n* Opportunity\n* Product2\n* Solution\n* Task", - }, - selector: { - propDefinition: [ - salesforce, - "fieldSelector", - ], - description: `${salesforce.propDefinitions.fieldSelector.description} Attachment`, - options: () => Object.keys(attachment), - reloadProps: true, - }, - }, - additionalProps() { - return this.additionalProps(this.selector, attachment); - }, + props, async run({ $ }) { - const data = pickBy(pick(this, [ - "Body", - "Name", - "ParentId", - ...this.selector, - ])); - const response = await this.salesforce.createAttachment({ + /* eslint-disable no-unused-vars */ + const { + salesforce, + getAdvancedProps, + getAdditionalFields, + formatDateTimeProps, + docsInfo, + filePathOrContent, + ...data + } = this; + /* eslint-enable no-unused-vars */ + + const body = filePathOrContent.includes("tmp/") + ? (await fs.promises.readFile(filePathOrContent)).toString("base64") + : filePathOrContent; + + const response = await salesforce.createRecord("Attachment", { $, - data, + data: { + Body: body, + ...data, + }, }); $.export("$summary", `Successfully created attachment "${this.Name}"`); return response; diff --git a/components/salesforce_rest_api/actions/create-campaign/create-campaign.mjs b/components/salesforce_rest_api/actions/create-campaign/create-campaign.mjs index 7a702ddcdd2ee..e30835c78ca0b 100644 --- a/components/salesforce_rest_api/actions/create-campaign/create-campaign.mjs +++ b/components/salesforce_rest_api/actions/create-campaign/create-campaign.mjs @@ -1,54 +1,58 @@ -import common from "../common/base.mjs"; +import common, { getProps } from "../common/base-create-update.mjs"; import campaign from "../../common/sobjects/campaign.mjs"; -import { - pickBy, pick, -} from "lodash-es"; -import { toSingleLineString } from "../../common/utils.mjs"; -const { salesforce } = common.props; +const docsLink = "https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_campaign.htm"; export default { ...common, key: "salesforce_rest_api-create-campaign", name: "Create Campaign", - description: toSingleLineString(` - Creates a marketing campaign, such as a direct mail promotion, webinar, or trade show. - See [Campaign SObject](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_campaign.htm) - and [Create Record](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_sobject_create.htm) - `), - version: "0.2.7", + description: `Creates a marketing campaign. [See the documentation](${docsLink})`, + version: "0.3.1", type: "action", - props: { - salesforce, - Name: { - type: "string", - label: "Name", - description: "Required. Name of the campaign. Limit: is 80 characters.", + methods: { + ...common.methods, + getObjectType() { + return "Campaign"; }, - selector: { - propDefinition: [ - salesforce, - "fieldSelector", - ], - description: `${salesforce.propDefinitions.fieldSelector.description} Campaign`, - options: () => Object.keys(campaign), - reloadProps: true, - optional: true, + getAdvancedProps() { + return campaign.extraProps; }, }, - additionalProps() { - return this.additionalProps(this.selector, campaign); - }, + props: getProps({ + objType: campaign, + docsLink, + showDateInfo: true, + }), async run({ $ }) { - const data = pickBy(pick(this, [ - "Name", - ...this.selector, - ])); - const response = await this.salesforce.createCampaign({ + /* eslint-disable no-unused-vars, max-len */ + const { + salesforce, + getAdvancedProps, + getObjectType, + getAdditionalFields, + formatDateTimeProps, + useAdvancedProps, + docsInfo, + dateInfo, + additionalFields, + StartDate, + EndDate, + ...data + } = this; + /* eslint-enable no-unused-vars, max-len */ + const response = await salesforce.createRecord("Campaign", { $, - data, + data: { + ...data, + ...formatDateTimeProps({ + StartDate, + EndDate, + }), + ...getAdditionalFields(), + }, }); - $.export("$summary", `Created campaign "${this.Name}"`); + $.export("$summary", `Successfully created campaign "${this.Name}"`); return response; }, }; diff --git a/components/salesforce_rest_api/actions/create-case/create-case.mjs b/components/salesforce_rest_api/actions/create-case/create-case.mjs index 3639dc94a78a8..e44943c686121 100644 --- a/components/salesforce_rest_api/actions/create-case/create-case.mjs +++ b/components/salesforce_rest_api/actions/create-case/create-case.mjs @@ -1,53 +1,57 @@ -import common from "../common/base.mjs"; -import salesforceCase from "../../common/sobjects/case.mjs"; -import { - pickBy, pick, -} from "lodash-es"; -import { toSingleLineString } from "../../common/utils.mjs"; +import common, { getProps } from "../common/base-create-update.mjs"; +import caseObj from "../../common/sobjects/case.mjs"; -const { salesforce } = common.props; +const docsLink = "https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_case.htm"; export default { ...common, key: "salesforce_rest_api-create-case", name: "Create Case", - description: toSingleLineString(` - Creates a Salesforce case, which represents a customer issue or problem. - See [Case SObject](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_case.htm) - and [Create Record](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_sobject_create.htm) - `), - version: "0.2.7", + description: `Creates a Case, which represents a customer issue or problem. [See the documentation](${docsLink})`, + version: "0.3.1", type: "action", - props: { - salesforce, - SuppliedEmail: { - type: "string", - label: "Supplied email", - description: "The email address that was entered when the case was created. Label is Email.If your organization has an active auto-response rule, SuppliedEmail is required when creating a case via the API. Auto-response rules use the email in the contact specified by ContactId. If no email address is in the contact record, the email specified here is used.", + methods: { + ...common.methods, + getObjectType() { + return "Case"; }, - selector: { - propDefinition: [ - salesforce, - "fieldSelector", - ], - description: `${salesforce.propDefinitions.fieldSelector.description} Case`, - options: () => Object.keys(salesforceCase), - reloadProps: true, + getAdvancedProps() { + return caseObj.extraProps; }, }, - additionalProps() { - return this.additionalProps(this.selector, salesforceCase); - }, + props: getProps({ + objType: caseObj, + docsLink, + showDateInfo: true, + }), async run({ $ }) { - const data = pickBy(pick(this, [ - "SuppliedEmail", - ...this.selector, - ])); - const response = await this.salesforce.createCase({ + /* eslint-disable no-unused-vars */ + const { + salesforce, + getAdvancedProps, + getObjectType, + getAdditionalFields, + formatDateTimeProps, + useAdvancedProps, + docsInfo, + dateInfo, + additionalFields, + SlaStartDate, + ...data + } = this; + /* eslint-enable no-unused-vars */ + const response = await salesforce.createRecord("Case", { $, - data, + data: { + ...data, + ...formatDateTimeProps({ + SlaStartDate, + }), + ...getAdditionalFields(), + }, }); - $.export("$summary", `Successfully created case for ${this.SuppliedEmail}`); + const summary = (this.SuppliedName && ` "${this.SuppliedName}"`) ?? (this.SuppliedEmail && ` with email ${this.SuppliedEmail}`) ?? ""; + $.export("$summary", `Successfully created case${summary}`); return response; }, }; diff --git a/components/salesforce_rest_api/actions/create-casecomment/create-casecomment.mjs b/components/salesforce_rest_api/actions/create-casecomment/create-casecomment.mjs index b382c01377675..575113c430eac 100644 --- a/components/salesforce_rest_api/actions/create-casecomment/create-casecomment.mjs +++ b/components/salesforce_rest_api/actions/create-casecomment/create-casecomment.mjs @@ -1,49 +1,37 @@ -import common from "../common/base.mjs"; +import common, { getProps } from "../common/base-create-update.mjs"; import caseComment from "../../common/sobjects/caseComment.mjs"; -import { - pickBy, pick, -} from "lodash-es"; -import { toSingleLineString } from "../../common/utils.mjs"; -const { salesforce } = common.props; +const docsLink = "https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_casecomment.htm"; + +/* eslint-disable no-unused-vars */ +const { + useAdvancedProps, ...props +} = getProps({ + objType: caseComment, + docsLink, +}); +/* eslint-enable no-unused-vars */ export default { ...common, key: "salesforce_rest_api-create-casecomment", - name: "Create CaseComment", - description: toSingleLineString(` - Creates a Case Comment that provides additional information about the associated Case. - See [CaseComment SObject](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_casecomment.htm) - and [Create Record](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_sobject_create.htm) - `), - version: "0.2.7", + name: "Create Case Comment", + description: `Creates a Case Comment on a selected Case. [See the documentation](${docsLink})`, + version: "0.3.1", type: "action", - props: { - salesforce, - ParentId: { - type: "string", - label: "Parent ID", - description: "Required. ID of the parent Case of the CaseComment.", - }, - selector: { - propDefinition: [ - salesforce, - "fieldSelector", - ], - description: `${salesforce.propDefinitions.fieldSelector.description} CaseComment`, - options: () => Object.keys(caseComment), - reloadProps: true, - }, - }, - additionalProps() { - return this.additionalProps(this.selector, caseComment); - }, + props, async run({ $ }) { - const data = pickBy(pick(this, [ - "ParentId", - ...this.selector, - ])); - const response = await this.salesforce.createCaseComment({ + /* eslint-disable no-unused-vars */ + const { + salesforce, + getAdvancedProps, + getAdditionalFields, + formatDateTimeProps, + docsInfo, + ...data + } = this; + /* eslint-enable no-unused-vars */ + const response = await salesforce.createRecord("CaseComment", { $, data, }); diff --git a/components/salesforce_rest_api/actions/create-contact/create-contact.mjs b/components/salesforce_rest_api/actions/create-contact/create-contact.mjs index ac56612e98dbc..1e90e785aa6bb 100644 --- a/components/salesforce_rest_api/actions/create-contact/create-contact.mjs +++ b/components/salesforce_rest_api/actions/create-contact/create-contact.mjs @@ -1,51 +1,54 @@ -import common from "../common/base.mjs"; +import common, { getProps } from "../common/base-create-update.mjs"; import contact from "../../common/sobjects/contact.mjs"; -import { - pickBy, pick, -} from "lodash-es"; -import { toSingleLineString } from "../../common/utils.mjs"; -const { salesforce } = common.props; +export const docsLink = "https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_contact.htm"; export default { ...common, key: "salesforce_rest_api-create-contact", name: "Create Contact", - description: toSingleLineString(` - Creates a Contact, which is a person associated with an account. - See [Contact SObject](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_contact.htm) - and [Create Record](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_sobject_create.htm) - `), - version: "0.2.7", + description: `Creates a contact. [See the documentation](${docsLink})`, + version: "0.3.1", type: "action", - props: { - salesforce, - LastName: { - type: "string", - label: "Last name", - description: "Last name of the contact up to 80 characters.", + methods: { + ...common.methods, + getObjectType() { + return "Contact"; }, - selector: { - propDefinition: [ - salesforce, - "fieldSelector", - ], - description: `${salesforce.propDefinitions.fieldSelector.description} Contact`, - options: () => Object.keys(contact), - reloadProps: true, + getAdvancedProps() { + return contact.extraProps; }, }, - additionalProps() { - return this.additionalProps(this.selector, contact); - }, + props: getProps({ + objType: contact, + docsLink, + showDateInfo: true, + }), async run({ $ }) { - const data = pickBy(pick(this, [ - "LastName", - ...this.selector, - ])); - const response = await this.salesforce.createContact({ + /* eslint-disable no-unused-vars */ + const { + salesforce, + getAdvancedProps, + getObjectType, + getAdditionalFields, + formatDateTimeProps, + useAdvancedProps, + docsInfo, + dateInfo, + additionalFields, + Birthdate, + ...data + } = this; + /* eslint-enable no-unused-vars */ + const response = await salesforce.createRecord("Contact", { $, - data, + data: { + ...data, + ...formatDateTimeProps({ + Birthdate, + }), + ...getAdditionalFields(), + }, }); $.export("$summary", `Successfully created contact "${this.LastName}"`); return response; diff --git a/components/salesforce_rest_api/actions/create-content-note/create-content-note.mjs b/components/salesforce_rest_api/actions/create-content-note/create-content-note.mjs new file mode 100644 index 0000000000000..ced5bf4476291 --- /dev/null +++ b/components/salesforce_rest_api/actions/create-content-note/create-content-note.mjs @@ -0,0 +1,91 @@ +/* eslint-disable no-unused-vars */ +import common, { getProps } from "../common/base-create-update.mjs"; +import contentNote from "../../common/sobjects/content-note.mjs"; +import contentDocumentLink from "../../common/sobjects/content-document-link.mjs"; + +const docsLink = "https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_contentnote.htm"; + +const { + useAdvancedProps: contentNoteUseAdvancedProps, + ...contentNoteProps +} = getProps({ + objType: contentNote, + docsLink, +}); + +const { + useAdvancedProps: contentDocumentLinkUseAdvancedProps, + ...contentDocumentLinkProps +} = getProps({ + objType: contentDocumentLink, + docsLink, + showDocsInfo: false, +}); + +export default { + ...common, + key: "salesforce_rest_api-create-content-note", + name: "Create Content Note", + description: `Creates a content note. [See the documentation](${docsLink}) and [Set Up Notes](https://help.salesforce.com/s/articleView?id=sales.notes_admin_setup.htm&type=5).`, + version: "0.0.1", + type: "action", + props: { + ...contentNoteProps, + ...contentDocumentLinkProps, + }, + methods: { + ...common.methods, + escapeHtml4(unsafe) { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + }, + }, + async run({ $ }) { + const { + salesforce, + escapeHtml4, + Title, + Content, + OwnerId, + LinkedEntityId, + ShareType, + Visibility, + } = this; + + const contentNoteResponse = await salesforce.createRecord("ContentNote", { + $, + data: { + Title, + Content: Buffer.from(escapeHtml4(Content)).toString("base64"), + OwnerId: OwnerId, + }, + }); + + if (!LinkedEntityId) { + $.export("$summary", `Successfully created content note with ID \`${contentNoteResponse.id}\`.`); + return { + contentNote: contentNoteResponse, + }; + } + + const contentDocumentLinkResponse = await salesforce.createRecord("ContentDocumentLink", { + $, + data: { + ContentDocumentId: contentNoteResponse.id, + LinkedEntityId, + ShareType, + Visibility, + }, + }); + + $.export("$summary", `Successfully created content note with ID \`${contentNoteResponse.id}\` and document link with ID \`${contentDocumentLinkResponse.id}\`.`); + return { + contentNote: contentNoteResponse, + contentDocumentLink: contentDocumentLinkResponse, + }; + }, +}; diff --git a/components/salesforce_rest_api/actions/create-event/create-event.mjs b/components/salesforce_rest_api/actions/create-event/create-event.mjs index fb233f865f7e5..b427102b2ccd8 100644 --- a/components/salesforce_rest_api/actions/create-event/create-event.mjs +++ b/components/salesforce_rest_api/actions/create-event/create-event.mjs @@ -1,80 +1,71 @@ -import salesforce from "../../salesforce_rest_api.app.mjs"; -import { - removeNullEntries, toSingleLineString, -} from "../../common/utils.mjs"; +import common, { getProps } from "../common/base-create-update.mjs"; +import event from "../../common/sobjects/event.mjs"; + +const docsLink = + "https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_event.htm"; export default { + ...common, key: "salesforce_rest_api-create-event", name: "Create Event", - description: toSingleLineString(` - Creates an event, which represents an event in the calendar. - See [Event SObject](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_event.htm) - and [Create Record](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_sobject_create.htm) - `), - version: "0.2.7", + description: `Creates an event. [See the documentation](${docsLink})`, + version: "0.3.1", type: "action", - props: { - salesforce, - IsAllDayEvent: { - type: "boolean", - label: "All-Day Event", - description: "Indicates whether the ActivityDate field (true) or the ActivityDateTime field (false) is used to define the date or time of the event.", - reloadProps: true, - }, - AcceptedEventInviteeIds: { - propDefinition: [ - salesforce, - "AcceptedEventInviteeIds", - ], - }, - Description: { - type: "string", - label: "Description", - description: "Contains a text description of the event. Limit: 32,000 characters.", + methods: { + ...common.methods, + getObjectType() { + return "Event"; }, - Subject: { - type: "string", - label: "Subject", - description: "The subject line of the event, such as Call, Email, or Meeting. Limit: 255 characters.", + getAdvancedProps() { + return event.extraProps; }, }, - async additionalProps() { - const props = {}; - if (this.IsAllDayEvent) { - props.ActivityDate = { - type: "string", - label: "Due Date Only (YYYY/MM/DD)", - description: "Contains the event's due date if the IsAllDayEvent flag is set to true.", - }; - } else { - props.ActivityDateTime = { - type: "string", - label: "Due Date Time", - description: "Contains the event's due date if the IsAllDayEvent flag is set to false. The time portion of this field is always transferred in the Coordinated Universal Time (UTC) time zone. Translate the time portion to or from a local time zone for the user or the application, as appropriate.", - }; - props.DurationInMinutes = { - type: "integer", - label: "Duration in minutes", - description: "Contains the event length, in minutes.", - }; - } - return props; - }, + props: getProps({ + objType: event, + docsLink, + showDateInfo: true, + }), async run({ $ }) { - const data = removeNullEntries({ - IsAllDayEvent: this.IsAllDayEvent, - AcceptedEventInviteeIds: this.AcceptedEventInviteeIds, - Description: this.Description, - Subject: this.Subject, - ActivityDate: this.ActivityDate && new Date(this.ActivityDate).toUTCString(), - ActivityDateTime: this.ActivityDateTime, - DurationInMinutes: this.DurationInMinutes, - }); - const response = await this.salesforce.createEvent({ + /* eslint-disable no-unused-vars */ + const { + salesforce, + getAdvancedProps, + getObjectType, + getAdditionalFields, + formatDateTimeProps, + useAdvancedProps, + docsInfo, + dateInfo, + additionalFields, + ActivityDate, + EndDateTime, + RecurrenceEndDateOnly, + RecurrenceStartDateTime, + ReminderDateTime, + StartDateTime, + RecurrenceDayOfWeekMask, + ...data + } = this; + /* eslint-enable no-unused-vars */ + const response = await salesforce.createRecord("Event", { $, - data, + data: { + ...data, + ...formatDateTimeProps({ + ActivityDate, + EndDateTime, + RecurrenceEndDateOnly, + RecurrenceStartDateTime, + ReminderDateTime, + StartDateTime, + }), + RecurrenceDayOfWeekMask: RecurrenceDayOfWeekMask?.reduce?.((acc, val) => acc + val, 0), + ...getAdditionalFields(), + }, }); - $.export("$summary", "Succcessfully created event"); + $.export("$summary", `Succcessfully created event${this.Subject + ? ` "${this.Subject}"` + : ""}`); return response; }, }; diff --git a/components/salesforce_rest_api/actions/create-lead/create-lead.mjs b/components/salesforce_rest_api/actions/create-lead/create-lead.mjs index 8bb5954d38617..e440b44863bd0 100644 --- a/components/salesforce_rest_api/actions/create-lead/create-lead.mjs +++ b/components/salesforce_rest_api/actions/create-lead/create-lead.mjs @@ -1,59 +1,51 @@ -import common from "../common/base.mjs"; +import common, { getProps } from "../common/base-create-update.mjs"; import lead from "../../common/sobjects/lead.mjs"; -import { - pickBy, pick, -} from "lodash-es"; -import { toSingleLineString } from "../../common/utils.mjs"; -const { salesforce } = common.props; +const docsLink = "https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_lead.htm"; export default { ...common, key: "salesforce_rest_api-create-lead", name: "Create Lead", - description: toSingleLineString(` - Creates a lead, which represents a prospect or lead. - See [Lead SObject](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_lead.htm) - and [Create Record](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_sobject_create.htm) - `), - version: "0.2.7", + description: `Creates a lead. [See the documentation](${docsLink})`, + version: "0.3.1", type: "action", - props: { - salesforce, - Company: { - type: "string", - label: "Company", - description: "The lead's company. Note If person account record types have been enabled, and if the value of Company is null, the lead converts to a person account.", + methods: { + ...common.methods, + getObjectType() { + return "Lead"; }, - LastName: { - type: "string", - label: "Last name", - description: "Required. Last name of the lead up to 80 characters.", + getAdvancedProps() { + return lead.extraProps; }, - selector: { - propDefinition: [ - salesforce, - "fieldSelector", - ], - description: `${salesforce.propDefinitions.fieldSelector.description} Lead`, - options: () => Object.keys(lead), - reloadProps: true, - }, - }, - additionalProps() { - return this.additionalProps(this.selector, lead); }, + props: getProps({ + objType: lead, + docsLink, + }), async run({ $ }) { - const data = pickBy(pick(this, [ - "Company", - "LastName", - ...this.selector, - ])); - const response = await this.salesforce.createLead({ + /* eslint-disable no-unused-vars */ + const { + salesforce, + getAdvancedProps, + getObjectType, + getAdditionalFields, + formatDateTimeProps, + useAdvancedProps, + docsInfo, + dateInfo, + additionalFields, + ...data + } = this; + /* eslint-enable no-unused-vars */ + const response = await salesforce.createRecord("Lead", { $, - data, + data: { + ...data, + ...getAdditionalFields(), + }, }); - $.export("$summary", `Successfully created lead for ${this.Company}`); + $.export("$summary", `Successfully created lead "${this.LastName}"`); return response; }, }; diff --git a/components/salesforce_rest_api/actions/create-note/create-note.mjs b/components/salesforce_rest_api/actions/create-note/create-note.mjs index 47b167281d580..b001e2dedfca9 100644 --- a/components/salesforce_rest_api/actions/create-note/create-note.mjs +++ b/components/salesforce_rest_api/actions/create-note/create-note.mjs @@ -1,55 +1,36 @@ -import common from "../common/base.mjs"; +import common, { getProps } from "../common/base-create-update.mjs"; import note from "../../common/sobjects/note.mjs"; -import { - pickBy, pick, -} from "lodash-es"; -import { toSingleLineString } from "../../common/utils.mjs"; -const { salesforce } = common.props; +const docsLink = "https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_note.htm"; + +/* eslint-disable no-unused-vars */ +const { + useAdvancedProps, ...props +} = getProps({ + objType: note, + docsLink, +}); +/* eslint-enable no-unused-vars */ export default { ...common, key: "salesforce_rest_api-create-note", name: "Create Note", - description: toSingleLineString(` - Creates a note, which is text associated with a custom object or a standard object, such as a Contact, Contract, or Opportunity. - See [Note SObject](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_note.htm) - and [Create Record](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_sobject_create.htm) - `), - version: "0.2.7", + description: `Creates a note. [See the documentation](${docsLink})`, + version: "0.3.1", type: "action", - props: { - salesforce, - ParentId: { - type: "string", - label: "Parent ID", - description: "ID of the object associated with the note.", - }, - Title: { - type: "string", - label: "Title", - description: "Title of the note.", - }, - selector: { - propDefinition: [ - salesforce, - "fieldSelector", - ], - description: `${salesforce.propDefinitions.fieldSelector.description} Note`, - options: () => Object.keys(note), - reloadProps: true, - }, - }, - additionalProps() { - return this.additionalProps(this.selector, note); - }, + props, async run({ $ }) { - const data = pickBy(pick(this, [ - "ParentId", - "Title", - ...this.selector, - ])); - const response = await this.salesforce.createNote({ + /* eslint-disable no-unused-vars */ + const { + salesforce, + getAdvancedProps, + getAdditionalFields, + formatDateTimeProps, + docsInfo, ...data + } = this; + /* eslint-enable no-unused-vars */ + const response = await salesforce.createRecord("Note", { $, data, }); diff --git a/components/salesforce_rest_api/actions/create-opportunity/create-opportunity.mjs b/components/salesforce_rest_api/actions/create-opportunity/create-opportunity.mjs index 6d0f561d47a5f..933047d33df0a 100644 --- a/components/salesforce_rest_api/actions/create-opportunity/create-opportunity.mjs +++ b/components/salesforce_rest_api/actions/create-opportunity/create-opportunity.mjs @@ -1,63 +1,54 @@ -import common from "../common/base.mjs"; +import common, { getProps } from "../common/base-create-update.mjs"; import opportunity from "../../common/sobjects/opportunity.mjs"; -import { - pickBy, pick, -} from "lodash-es"; -import { toSingleLineString } from "../../common/utils.mjs"; -const { salesforce } = common.props; +export const docsLink = "https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_opportunity.htm"; export default { ...common, key: "salesforce_rest_api-create-opportunity", name: "Create Opportunity", - description: toSingleLineString(` - Creates an opportunity, which represents an opportunity, which is a sale or pending deal. - See [Opportunity SObject](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_opportunity.htm) - and [Create Record](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_sobject_create.htm) - `), - version: "0.2.7", + description: `Creates an opportunity. [See the documentation](${docsLink})`, + version: "0.3.1", type: "action", - props: { - salesforce, - CloseDate: { - type: "string", - label: "Close date", - description: "Date when the opportunity is expected to close.", + methods: { + ...common.methods, + getObjectType() { + return "Opportunity"; }, - Name: { - type: "string", - label: "Name", - description: "A name for this opportunity. Limit: 120 characters.", + getAdvancedProps() { + return opportunity.extraProps; }, - StageName: { - type: "string", - label: "StageName", - description: "Current stage of this record. The StageName field controls several other fields on an opportunity. Each of the fields can be directly set or implied by changing the StageName field. In addition, the StageName field is a picklist, so it has additional members in the returned describeSObjectResult to indicate how it affects the other fields. To obtain the stage name values in the picklist, query the OpportunityStage object. If the StageName is updated, then the ForecastCategoryName, IsClosed, IsWon, and Probability are automatically updated based on the stage-category mapping.", - }, - selector: { - propDefinition: [ - salesforce, - "fieldSelector", - ], - description: `${salesforce.propDefinitions.fieldSelector.description} Opportunity`, - options: () => Object.keys(opportunity), - reloadProps: true, - }, - }, - additionalProps() { - return this.additionalProps(this.selector, opportunity); }, + props: getProps({ + objType: opportunity, + docsLink, + showDateInfo: true, + }), async run({ $ }) { - const data = pickBy(pick(this, [ - "CloseDate", - "Name", - "StageName", - ...this.selector, - ])); - const response = await this.salesforce.createOpportunity({ + /* eslint-disable no-unused-vars */ + const { + salesforce, + getAdvancedProps, + getObjectType, + getAdditionalFields, + formatDateTimeProps, + useAdvancedProps, + docsInfo, + dateInfo, + additionalFields, + CloseDate, + ...data + } = this; + /* eslint-enable no-unused-vars */ + const response = await salesforce.createRecord("Opportunity", { $, - data, + data: { + ...data, + ...formatDateTimeProps({ + CloseDate, + }), + ...getAdditionalFields(), + }, }); $.export("$summary", `Successfully created opportunity "${this.Name}"`); return response; diff --git a/components/salesforce_rest_api/actions/create-record/create-record.mjs b/components/salesforce_rest_api/actions/create-record/create-record.mjs index 8e22d0d271869..28a72030a31cd 100644 --- a/components/salesforce_rest_api/actions/create-record/create-record.mjs +++ b/components/salesforce_rest_api/actions/create-record/create-record.mjs @@ -1,14 +1,14 @@ +import { + convertFieldsToProps, getAdditionalFields, +} from "../../common/props-utils.mjs"; import salesforce from "../../salesforce_rest_api.app.mjs"; -import { toSingleLineString } from "../../common/utils.mjs"; +import { additionalFields } from "../common/base-create-update.mjs"; export default { key: "salesforce_rest_api-create-record", name: "Create Record", - description: toSingleLineString(` - Create new records of a given resource. - See [docs](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_sobject_create.htm) - `), - version: "0.2.7", + description: "Create a record of a given object. [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_sobject_create.htm)", + version: "0.3.1", type: "action", props: { salesforce, @@ -17,20 +17,54 @@ export default { salesforce, "objectType", ], - description: "SObject Type for this record", - }, - sobject: { - type: "object", - label: "SObject fields and values", - description: "Data of the SObject record to create", + description: "The type of object to create a record of", + reloadProps: true, }, }, + methods: { + getAdditionalFields, + convertFieldsToProps, + }, + async additionalProps() { + const { objectType } = this; + const fields = await this.salesforce.getFieldsForObjectType(objectType); + + const requiredFields = fields.filter((field) => { + return field.createable && !field.nillable && !field.defaultedOnCreate; + }); + + const requiredFieldProps = this.convertFieldsToProps(requiredFields); + + return { + docsInfo: { + type: "alert", + alertType: "info", + content: `[See the documentation](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_${objectType.toLowerCase()}.htm) for information on all available fields.`, + }, + ...requiredFieldProps, + additionalFields, + }; + }, async run({ $ }) { - const response = await this.salesforce.createRecord(this.objectType, { + /* eslint-disable no-unused-vars */ + const { + salesforce, + objectType, + getAdditionalFields: getData, + convertFieldsToProps, + docsInfo, + additionalFields, + ...data + } = this; + /* eslint-enable no-unused-vars */ + const response = await salesforce.createRecord(objectType, { $, - data: this.sobject, + data: { + ...data, + ...getData(), + }, }); - $.export("$summary", `Created record "${this.objectType}"`); + $.export("$summary", `Successfully created ${objectType} record (ID: ${response.id})`); return response; }, }; diff --git a/components/salesforce_rest_api/actions/create-task/create-task.mjs b/components/salesforce_rest_api/actions/create-task/create-task.mjs index 54b10bdbf2dc7..d4ebb4703c606 100644 --- a/components/salesforce_rest_api/actions/create-task/create-task.mjs +++ b/components/salesforce_rest_api/actions/create-task/create-task.mjs @@ -1,59 +1,67 @@ -import common from "../common/base.mjs"; +import common, { getProps } from "../common/base-create-update.mjs"; import task from "../../common/sobjects/task.mjs"; -import { - pickBy, pick, -} from "lodash-es"; -import { toSingleLineString } from "../../common/utils.mjs"; -const { salesforce } = common.props; +const docsLink = + "https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_task.htm"; export default { ...common, key: "salesforce_rest_api-create-task", name: "Create Task", - description: toSingleLineString(` - Creates a task, which represents a business activity such as making a phone call or other to-do items. - See [Task SObject](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_task.htm) - and [Create Record](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_sobject_create.htm) - `), - version: "0.3.7", + description: `Creates a task. [See the documentation](${docsLink})`, + version: "0.4.1", type: "action", - props: { - salesforce, - Priority: { - type: "string", - label: "Priority", - description: "Required. Indicates the importance or urgency of a task, such as high or low.", + methods: { + ...common.methods, + getObjectType() { + return "Task"; }, - Status: { - type: "string", - label: "Status", - description: "Required. The status of the task, such as In Progress or Completed. Each predefined Status field implies a value for the IsClosed flag. To obtain picklist values, query the TaskStatus object. Note This field can't be updated for recurring tasks (IsRecurrence is true).", + getAdvancedProps() { + return task.extraProps; }, - selector: { - propDefinition: [ - salesforce, - "fieldSelector", - ], - description: `${salesforce.propDefinitions.fieldSelector.description} Task`, - options: () => Object.keys(task), - reloadProps: true, - }, - }, - additionalProps() { - return this.additionalProps(this.selector, task); }, + props: getProps({ + objType: task, + docsLink, + showDateInfo: true, + }), async run({ $ }) { - const data = pickBy(pick(this, [ - "Priority", - "Status", - ...this.selector, - ])); - const response = await this.salesforce.createTask({ + /* eslint-disable no-unused-vars */ + const { + salesforce, + getAdvancedProps, + getObjectType, + getAdditionalFields, + formatDateTimeProps, + useAdvancedProps, + docsInfo, + dateInfo, + additionalFields, + ActivityDate, + RecurrenceEndDateOnly, + RecurrenceStartDateOnly, + ReminderDateTime, + RecurrenceDayOfWeekMask, + ...data + } = this; + /* eslint-enable no-unused-vars */ + const response = await salesforce.createRecord("Task", { $, - data, + data: { + ...data, + ...formatDateTimeProps({ + ActivityDate, + RecurrenceEndDateOnly, + RecurrenceStartDateOnly, + ReminderDateTime, + }), + RecurrenceDayOfWeekMask: RecurrenceDayOfWeekMask?.reduce?.((acc, val) => acc + val, 0), + ...getAdditionalFields(), + }, }); - $.export("$summary", "Successfully created task"); + $.export("$summary", `Succcessfully created task${this.Subject + ? ` "${this.Subject}"` + : ""}`); return response; }, }; diff --git a/components/salesforce_rest_api/actions/create-user/create-user.mjs b/components/salesforce_rest_api/actions/create-user/create-user.mjs index 8d0a33718dd70..a41f116115a1b 100644 --- a/components/salesforce_rest_api/actions/create-user/create-user.mjs +++ b/components/salesforce_rest_api/actions/create-user/create-user.mjs @@ -1,215 +1,52 @@ -import common from "../common/base.mjs"; -import utils from "../../common/props-utils.mjs"; -import { toSingleLineString } from "../../common/utils.mjs"; +import common, { getProps } from "../common/base-create-update.mjs"; +import user from "../../common/sobjects/user.mjs"; -const { salesforce } = common.props; +const docsLink = + "https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_user.htm"; export default { ...common, key: "salesforce_rest_api-create-user", name: "Create User", - description: toSingleLineString(` - Creates a Salesforce user. - See [User SObject](https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_objects_user.htm) - and [Create Record](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_sobject_create.htm) - `), - version: "0.0.1", + description: `Creates a Salesforce user. [See the documentation](${docsLink})`, + version: "0.1.1", type: "action", - props: { - salesforce, - alias: { - type: "string", - label: "Alias", - description: "Alias of the user. The alias can contain only underscores and alphanumeric characters. It must be unique in your org, not include spaces, not end with a hyphen, and not contain two consecutive hyphens.", - }, - email: { - type: "string", - label: "Email", - description: "The email address of the user.", - }, - emailEncodingKey: { - type: "string", - label: "Email Encoding Key", - description: "The key used to encode the user's email.", - options: [ - "ISO-8859-1", - "UTF-8", - "Shift_JIS", - "EUC-JP", - "ISO-2022-JP", - ], - default: "UTF-8", - }, - languageLocaleKey: { - type: "string", - label: "Language Locale Key", - description: "The user's language locale key.", - async options() { - const fields = await this.salesforce.getFieldsForObjectType("User"); - const { picklistValues } = fields.find(({ name }) => name === "LanguageLocaleKey"); - return picklistValues.map(({ - value, label, - }) => ({ - label, - value, - })); - }, - }, - firstName: { - type: "string", - label: "First Name", - description: "The user's first name.", - optional: true, - }, - lastName: { - type: "string", - label: "Last Name", - description: "The user's last name.", - }, - localeSidKey: { - type: "string", - label: "Locale Sid Key", - description: "The user's locale sid key.", - async options() { - const fields = await this.salesforce.getFieldsForObjectType("User"); - const { picklistValues } = fields.find(({ name }) => name === "LocaleSidKey"); - return picklistValues.map(({ - value, label, - }) => ({ - label, - value, - })); - }, - }, - profileId: { - type: "string", - label: "Profile ID", - description: "The ID of the user's profile.", - async options() { - const { records } = await this.salesforce.query({ - query: "SELECT Id, Name FROM Profile", - }); - return records.map(({ - Id: value, Name: label, - }) => ({ - label, - value, - })); - }, - }, - timeZoneSidKey: { - type: "string", - label: "Time Zone Sid Key", - description: "The user's time zone sid key.", - async options() { - const fields = await this.salesforce.getFieldsForObjectType("User"); - const { picklistValues } = fields.find(({ name }) => name === "TimeZoneSidKey"); - return picklistValues.map(({ - value, label, - }) => ({ - label, - value, - })); - }, - }, - userName: { - type: "string", - label: "User Name", - description: "The user's username. It should be in email format. Eg. `john@acme.com`.", - }, - title: { - type: "string", - label: "Title", - description: "The user's title.", - optional: true, - }, - department: { - type: "string", - label: "Department", - description: "The department the user belongs to.", - optional: true, - }, - division: { - type: "string", - label: "Division", - description: "The division the user belongs to.", - optional: true, - }, - phone: { - type: "string", - label: "Phone", - description: "The user's phone number.", - optional: true, - }, - mobilePhone: { - type: "string", - label: "Mobile Phone", - description: "The user's mobile phone number.", - optional: true, - }, - street: { - type: "string", - label: "Street", - description: "The user's street address.", - optional: true, - }, - city: { - type: "string", - label: "City", - description: "The user's city.", - optional: true, - }, - state: { - type: "string", - label: "State", - description: "The user's state.", - optional: true, - }, - postalCode: { - type: "string", - label: "Postal Code", - description: "The user's postal code.", - optional: true, - }, - country: { - type: "string", - label: "Country", - description: "The user's country.", - optional: true, - }, - userRoleId: { - type: "string", - label: "User Role ID", - description: "The ID of the user's role.", - optional: true, - }, - isActive: { - type: "boolean", - label: "Is Active", - description: "Whether the user is active.", - optional: true, - }, - }, methods: { - createUser(args = {}) { - return this.salesforce._makeRequest({ - method: "POST", - url: this.salesforce._sObjectTypeApiUrl("User"), - ...args, - }); + ...common.methods, + getObjectType() { + return "User"; + }, + getAdvancedProps() { + return user.extraProps; }, }, + props: getProps({ + objType: user, + docsLink, + }), async run({ $ }) { + /* eslint-disable no-unused-vars */ const { - createUser, + salesforce, + getAdvancedProps, + getObjectType, + getAdditionalFields, + formatDateTimeProps, + useAdvancedProps, + docsInfo, + dateInfo, + additionalFields, ...data } = this; - - const response = await createUser({ + /* eslint-enable no-unused-vars */ + const response = await salesforce.createRecord("User", { $, - data: utils.keysToCapitalCase(data), + data: { + ...data, + ...getAdditionalFields(), + }, }); - $.export("$summary", `Successfully created user with ID \`${response.id}\``); + $.export("$summary", `Successfully created user (ID: ${response.id})`); return response; }, }; diff --git a/components/salesforce_rest_api/actions/delete-opportunity/delete-opportunity.mjs b/components/salesforce_rest_api/actions/delete-opportunity/delete-opportunity.mjs index 3a79d64fddd5c..8d7ccec6c201e 100644 --- a/components/salesforce_rest_api/actions/delete-opportunity/delete-opportunity.mjs +++ b/components/salesforce_rest_api/actions/delete-opportunity/delete-opportunity.mjs @@ -1,30 +1,34 @@ import salesforce from "../../salesforce_rest_api.app.mjs"; -import { toSingleLineString } from "../../common/utils.mjs"; export default { key: "salesforce_rest_api-delete-opportunity", name: "Delete Opportunity", - description: toSingleLineString(` - Deletes an opportunity. - See [Opportunity SObject](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_opportunity.htm) - and [Delete Record](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_delete_record.htm) - `), - version: "0.2.7", + description: "Deletes an opportunity. [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_delete_record.htm)", + version: "0.3.0", type: "action", props: { salesforce, - OpportunityId: { - type: "string", + recordId: { + propDefinition: [ + salesforce, + "recordId", + () => ({ + objType: "Opportunity", + nameField: "Name", + }), + ], label: "Opportunity ID", - description: "ID of the Opportunity to delete", + description: "ID of the opportunity to delete.", }, }, async run({ $ }) { - const response = await this.salesforce.deleteOpportunity({ + const { recordId } = this; + const response = await this.salesforce.deleteRecord({ + sobjectType: "Opportunity", $, - id: this.OpportunityId, + recordId, }); - $.export("$summary", "Successfully deleted opportunity"); + $.export("$summary", `Successfully deleted opportunity (ID: ${recordId})`); return response; }, }; diff --git a/components/salesforce_rest_api/actions/delete-record/delete-record.mjs b/components/salesforce_rest_api/actions/delete-record/delete-record.mjs index db7f508eb6e62..4ea946d854315 100644 --- a/components/salesforce_rest_api/actions/delete-record/delete-record.mjs +++ b/components/salesforce_rest_api/actions/delete-record/delete-record.mjs @@ -1,42 +1,44 @@ -import salesForceRestApi from "../../salesforce_rest_api.app.mjs"; +import salesforce from "../../salesforce_rest_api.app.mjs"; export default { key: "salesforce_rest_api-delete-record", - name: "Delete a Record in an Object", + name: "Delete Record", description: - "Deletes an existing record in an object. [API Doc](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_query.htm)", - version: "0.1.5", + "Deletes an existing record in an object. [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_sobject_retrieve_delete.htm)", + version: "0.2.0", type: "action", props: { - salesForceRestApi, + salesforce, sobjectType: { propDefinition: [ - salesForceRestApi, + salesforce, "objectType", ], + description: "The type of object to delete a record of.", }, - sobjectId: { + recordId: { propDefinition: [ - salesForceRestApi, - "sobjectId", + salesforce, + "recordId", (c) => ({ - objectType: c.sobjectType, + objType: c.sobjectType, }), ], description: - "ID of the Salesforce standard object to get field values from.", + "The record to delete.", }, }, async run({ $ }) { const { sobjectType, - sobjectId, + recordId, } = this; - const response = await this.salesForceRestApi.deleteObject( + const response = await this.salesforce.deleteRecord({ + $, sobjectType, - sobjectId, - ); - response && $.export("$summary", `Successfully deleted record with ID ${sobjectId}`); + recordId, + }); + $.export("$summary", `Successfully deleted record (ID: ${recordId})`); return response; }, }; diff --git a/components/salesforce_rest_api/actions/find-create-record/find-create-record.mjs b/components/salesforce_rest_api/actions/find-create-record/find-create-record.mjs deleted file mode 100644 index e4e947bb79709..0000000000000 --- a/components/salesforce_rest_api/actions/find-create-record/find-create-record.mjs +++ /dev/null @@ -1,89 +0,0 @@ -import { ConfigurationError } from "@pipedream/platform"; -import salesForceRestApi from "../../salesforce_rest_api.app.mjs"; - -export default { - key: "salesforce_rest_api-find-create-record", - name: "Get Field Values from Object Record and optionally create one is none is found. ", - description: - "Finds a specified Salesforce record by a field. Optionally, create one if none is found. [API Docs](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_get_field_values.htm)", - version: "0.1.5", - type: "action", - props: { - salesForceRestApi, - sobjectType: { - propDefinition: [ - salesForceRestApi, - "objectType", - ], - }, - sobjectId: { - propDefinition: [ - salesForceRestApi, - "sobjectId", - (c) => ({ - objectType: c.sobjectType, - }), - ], - description: - "ID of the Salesforce standard object to get field values from.", - }, - sobjectFields: { - type: "string[]", - label: "Fields to get values from", - description: - "list of the Salesforce standard object's fields to get values from.", - }, - createIfNotFound: { - type: "boolean", - label: "Create new object", - description: "Create a new object if none is found", - reloadProps: true, - }, - }, - async additionalProps() { - return { - sobject: { - type: "object", - label: "Salesforce standard object", - description: `Data of the Salesforce standard object record to create. - Salesforce standard objects are described in [Standard Objects](https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_objects_list.htm) section of the [SOAP API Developer Guide](https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_quickstart_intro.htm).`, - }, - }; - }, - async run({ $ }) { - const { - sobjectType, - sobjectId, - sobjectFields, - sobject, - createIfNotFound, - } = this; - let data; - try { - data = await this.salesForceRestApi.getSObject( - sobjectType, - sobjectId, - sobjectFields && { - fields: sobjectFields.join(","), - }, - ); - } catch (error) { - if (!createIfNotFound) throw new ConfigurationError("Record not found"); - } - - if (createIfNotFound && !data) { - const response = await this.salesForceRestApi.createObject( - sobjectType, - sobject, - ); - response && $.export( - "$summary", "Record successfully created", - ); - return response; - } - if (data) { - $.export("$summary", "Record found!"); - } - return data; - }, -}; diff --git a/components/salesforce_rest_api/actions/find-records/find-records.mjs b/components/salesforce_rest_api/actions/find-records/find-records.mjs index 53df0cba502fd..813f662dc9e65 100644 --- a/components/salesforce_rest_api/actions/find-records/find-records.mjs +++ b/components/salesforce_rest_api/actions/find-records/find-records.mjs @@ -1,52 +1,67 @@ -import salesForceRestApi from "../../salesforce_rest_api.app.mjs"; +import salesforce from "../../salesforce_rest_api.app.mjs"; export default { key: "salesforce_rest_api-find-records", - name: "Get Object Records", + name: "Find Records", description: - "Retrieves all records in an object or a record in an object by the given ID or criteria. [API Doc](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_get_field_values.htm)", - version: "0.1.5", + "Retrieves selected fields for some or all records of a selected object. [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_get_field_values.htm)", + version: "0.2.0", type: "action", props: { - salesForceRestApi, + salesforce, sobjectType: { propDefinition: [ - salesForceRestApi, + salesforce, "objectType", ], + description: "The type of object to obtain records of.", }, - ids: { + fieldsToObtain: { propDefinition: [ - salesForceRestApi, - "sobjectId", + salesforce, + "fieldsToObtain", (c) => ({ - objectType: c.sobjectType, + objType: c.sobjectType, }), ], - type: "string[]", }, - fields: { + recordIds: { + propDefinition: [ + salesforce, + "recordId", + (c) => ({ + objType: c.sobjectType, + }), + ], + label: "Record ID(s)", type: "string[]", - label: "Fields to get values from", + optional: true, description: - "list of the Salesforce standard object's fields to get values from.", + "The record(s) to retrieve. If not specified, all records will be retrieved.", }, }, async run({ $ }) { - const { + let { sobjectType, - fields, - ids, + recordIds, + fieldsToObtain, } = this; - const response = await this.salesForceRestApi.getRecords( - sobjectType, { - fields: Array.isArray(fields) && fields.join(",") || fields, - ids: Array.isArray(ids) && ids.join(",") || ids, - }, - ); - if (response) { - $.export("$summary", "Record found successfully"); + + if (typeof recordIds === "string") recordIds = recordIds.split(","); + if (typeof fieldsToObtain === "string") fieldsToObtain = fieldsToObtain.split(","); + + let query = `SELECT ${fieldsToObtain.join(", ")} FROM ${sobjectType}`; + + if (recordIds?.length) { + query += ` WHERE Id IN ('${recordIds.join("','")}')`; } - return response; + + const { records } = await this.salesforce.query({ + $, + query, + }); + + $.export("$summary", `Sucessfully retrieved ${records.length} records`); + return records; }, }; diff --git a/components/salesforce_rest_api/actions/get-sobject-fields-values/get-sobject-fields-values.mjs b/components/salesforce_rest_api/actions/get-sobject-fields-values/get-sobject-fields-values.mjs deleted file mode 100644 index 741ae8451d889..0000000000000 --- a/components/salesforce_rest_api/actions/get-sobject-fields-values/get-sobject-fields-values.mjs +++ /dev/null @@ -1,57 +0,0 @@ -import salesforce from "../../salesforce_rest_api.app.mjs"; -import { toSingleLineString } from "../../common/utils.mjs"; - -export default { - key: "salesforce_rest_api-get-sobject-fields-values", - name: "Get Field Values from a Standard Object Record", - description: toSingleLineString(` - Retrieve field values from a record. You can specify the fields you want to retrieve. - See [docs](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_get_field_values.htm) - `), - version: "0.2.7", - type: "action", - props: { - salesforce, - objectType: { - propDefinition: [ - salesforce, - "objectType", - ], - }, - sobjectId: { - propDefinition: [ - salesforce, - "sobjectId", - (c) => ({ - objectType: c.objectType, - }), - ], - }, - fields: { - type: "string[]", - label: "SObject Fields", - description: "List of fields of the Standard object to get values from", - propDefinition: [ - salesforce, - "field", - (c) => ({ - objectType: c.objectType, - }), - ], - optional: true, - }, - }, - async run({ $ }) { - const params = {}; - if (this.fields?.length > 0) { - params.fields = this.fields.join(","); - } - const response = await this.salesforce.getRecordFieldValues(this.objectType, { - $, - id: this.sobjectId, - params, - }); - $.export("$summary", `Retrieved ${this.objectType} field values`); - return response; - }, -}; diff --git a/components/salesforce_rest_api/actions/insert-blob-data/insert-blob-data.mjs b/components/salesforce_rest_api/actions/insert-blob-data/insert-blob-data.mjs index e870dc6ed2af3..ab9050fae8a14 100644 --- a/components/salesforce_rest_api/actions/insert-blob-data/insert-blob-data.mjs +++ b/components/salesforce_rest_api/actions/insert-blob-data/insert-blob-data.mjs @@ -1,14 +1,10 @@ import salesforce from "../../salesforce_rest_api.app.mjs"; -import { toSingleLineString } from "../../common/utils.mjs"; export default { key: "salesforce_rest_api-insert-blob-data", name: "Insert Blob Data", - description: toSingleLineString(` - Inserts blob data in Salesforce standard objects. - See [docs](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_sobject_insert_update_blob.htm) - `), - version: "0.2.7", + description: "Inserts blob data in Salesforce standard objects. [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_sobject_insert_update_blob.htm)", + version: "0.2.8", type: "action", props: { salesforce, @@ -71,7 +67,7 @@ export default { headers, data, }); - $.export("$summary", `Inserted Blob data to ${this.sobjectName}`); + $.export("$summary", `Successfully inserted blob data in ${this.sobjectName}`); return response; }, }; diff --git a/components/salesforce_rest_api/actions/post-feed-to-chatter/post-feed-to-chatter.mjs b/components/salesforce_rest_api/actions/post-feed-to-chatter/post-feed-to-chatter.mjs index 5b46ed9ea1358..99a221ffddba5 100644 --- a/components/salesforce_rest_api/actions/post-feed-to-chatter/post-feed-to-chatter.mjs +++ b/components/salesforce_rest_api/actions/post-feed-to-chatter/post-feed-to-chatter.mjs @@ -1,39 +1,64 @@ -import salesForceRestApi from "../../salesforce_rest_api.app.mjs"; -import { toSingleLineString } from "../../common/utils.mjs"; +import salesforce from "../../salesforce_rest_api.app.mjs"; export default { key: "salesforce_rest_api-post-feed-to-chatter", name: "Post a Message to Chatter Feed", - description: toSingleLineString(` - Posts a message to the Chatter Feed. - [See doc](https://developer.salesforce.com/docs/atlas.en-us.chatterapi.meta/chatterapi/quickreference_post_feed_item.htm) - `), - version: "0.0.6", + description: + "Post a feed item in Chatter. [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.chatterapi.meta/chatterapi/quickreference_post_feed_item.htm)", + version: "0.1.0", type: "action", props: { - salesForceRestApi, - text: { - label: "Text body", - description: "Text body.", - type: "string", + salesforce, + sobjectType: { + propDefinition: [ + salesforce, + "objectType", + ], + description: "The type of object to select a record from.", }, subjectId: { - label: "Subject ID", - description: "Specify the user, group, or record that will parent the feed item.", - type: "string", + propDefinition: [ + salesforce, + "recordId", + (c) => ({ + objType: c.sobjectType, + }), + ], + description: "The record that will parent the feed item.", + }, + messageSegments: { + label: "Message segments", + description: + "Each message segment can be a text string, which will be treated as a segment of `type: Text`, or a [message segment object](https://developer.salesforce.com/docs/atlas.en-us.chatterapi.meta/chatterapi/connect_requests_message_body_input.htm) such as `{ \"type\": \"Mention\", \"username\": \"john\" }`", + type: "string[]", }, }, async run({ $ }) { - const params = { - text: this.text, + const data = { subjectId: this.subjectId, feedElementType: "FeedItem", + body: { + messageSegments: this.messageSegments.map((segment) => { + if (typeof segment === "string") { + try { + return JSON.parse(segment); + } catch (err) { + return { + type: "Text", + text: segment, + }; + } + } + + return segment; + }), + }, }; - const response = await this.salesForceRestApi.postFeed({ + const response = await this.salesforce.postFeed({ $, - params, + data, }); - response && $.export("$summary", "Successfully added message to chatter feed"); + response && $.export("$summary", "Successfully posted feed item"); return response; }, }; diff --git a/components/salesforce_rest_api/actions/search-string/search-string.mjs b/components/salesforce_rest_api/actions/search-string/search-string.mjs index b490099f33c32..a04a990e92c42 100644 --- a/components/salesforce_rest_api/actions/search-string/search-string.mjs +++ b/components/salesforce_rest_api/actions/search-string/search-string.mjs @@ -1,19 +1,25 @@ -import salesForceRestApi from "../../salesforce_rest_api.app.mjs"; +import salesforce from "../../salesforce_rest_api.app.mjs"; export default { key: "salesforce_rest_api-search-string", name: "Search Object Records", description: - "Searches for records in an object using a parameterized search. [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_search_parameterized.htm)", - version: "0.0.1", + "Searches for records in an object using a parameterized search. [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_search_parameterized_get.htm)", + version: "0.0.2", type: "action", props: { - salesForceRestApi, + salesforce, + infoBox: { + type: "alert", + alertType: "info", + content: "If you need a more flexible search, consider using the **SOQL Search** or **SOSL Search** actions instead.", + }, sobjectType: { propDefinition: [ - salesForceRestApi, + salesforce, "objectType", ], + description: "The type of object to search for records.", }, searchTerm: { type: "string", @@ -22,9 +28,9 @@ export default { }, fields: { type: "string[]", - label: "Fields to get values from", + label: "Fields", description: - "List of the Salesforce object's fields to get values from.", + "List of the Salesforce object's fields to get values from, such as `Id` or `Name`.", }, }, async run({ $ }) { @@ -33,21 +39,17 @@ export default { searchTerm, fields, } = this; - try { - const response = await this.salesForceRestApi.parameterizedSearch({ + + const response = await this.salesforce.parameterizedSearch({ + $, + params: { q: searchTerm, sobject: sobjectType, fields: fields.join(","), - }); - const resultsFound = response.searchRecords.length; - $.export("$summary", "Search completed successfully"); - $.export("results_found", resultsFound); - return response; - } catch (error) { - console.error(error); - $.export("$summary", "Search failed"); - $.export("results_found", 0); - throw new Error(`Search failed: ${error.message}`); - } + }, + }); + const resultsFound = response.searchRecords.length; + $.export("$summary", `Sucessfully found ${resultsFound} results`); + return response; }, }; diff --git a/components/salesforce_rest_api/actions/soql-search/soql-search.mjs b/components/salesforce_rest_api/actions/soql-search/soql-search.mjs index 0ae23f48e5c58..5d9297b9e6c70 100644 --- a/components/salesforce_rest_api/actions/soql-search/soql-search.mjs +++ b/components/salesforce_rest_api/actions/soql-search/soql-search.mjs @@ -1,21 +1,26 @@ -import { toSingleLineString } from "../../common/utils.mjs"; import salesforce from "../../salesforce_rest_api.app.mjs"; +import { docsInfo } from "../sosl-search/sosl-search.mjs"; + +const docsLink = "https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_examples.htm"; export default { key: "salesforce_rest_api-soql-search", - name: "SOQL Search", - description: toSingleLineString(` - Executes a SOQL query. - See [docs](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql.htm) - `), - version: "0.2.8", + name: "SOQL Query (Object Query)", + description: `Executes a [Salesforce Object Query Language (SOQL)](${docsLink}) query-based, SQL-like search.`, + version: "0.2.9", type: "action", props: { salesforce, + docsInfo, + exampleInfo: { + type: "alert", + alertType: "info", + content: "Example query: `SELECT Id, Name, BillingCity FROM Account`", + }, query: { type: "string", label: "SOQL Query", - description: "A SOQL search query", + description: `A SOQL search query. [See the documentation](${docsLink}) for examples and more information.`, }, }, async run({ $ }) { diff --git a/components/salesforce_rest_api/actions/sosl-search/sosl-search.mjs b/components/salesforce_rest_api/actions/sosl-search/sosl-search.mjs index 0e9f03c864e03..5ae3f484c16f0 100644 --- a/components/salesforce_rest_api/actions/sosl-search/sosl-search.mjs +++ b/components/salesforce_rest_api/actions/sosl-search/sosl-search.mjs @@ -1,21 +1,31 @@ import salesforce from "../../salesforce_rest_api.app.mjs"; -import { toSingleLineString } from "../../common/utils.mjs"; + +const docsLink = "https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_sosl_examples.htm"; + +export const docsInfo = { + type: "alert", + alertType: "info", + content: "You can find helpful information on SOQL and SOSL in [the Salesforce documentation](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_sosl_intro.htm).", +}; export default { key: "salesforce_rest_api-sosl-search", - name: "SOSL Search", - description: toSingleLineString(` - Executes the specified SOSL search. - See [docs](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_sosl.htm) - `), - version: "0.2.7", + name: "SOSL Search (Object Search)", + description: `Executes a [Salesforce Object Search Language (SOSL)](${docsLink}) text-based search query.`, + version: "0.2.8", type: "action", props: { salesforce, + docsInfo, + exampleInfo: { + type: "alert", + alertType: "info", + content: "Example search: `FIND {Joe Smith} IN Name Fields RETURNING lead(name, phone)`", + }, search: { type: "string", label: "SOSL Query", - description: "A SOSL search query", + description: `A SOSL search query. [See the documentation](${docsLink}) for examples and more information.`, }, }, async run({ $ }) { @@ -23,7 +33,7 @@ export default { $, search: this.search, }); - $.export("$summary", "Successfully returned ${response.length} results for SOSL search"); + $.export("$summary", `Successfully returned ${response.searchRecords?.length} results for SOSL search`); return response; }, }; diff --git a/components/salesforce_rest_api/actions/update-account/update-account.mjs b/components/salesforce_rest_api/actions/update-account/update-account.mjs index a28f044beb96d..4868eb90c6c03 100644 --- a/components/salesforce_rest_api/actions/update-account/update-account.mjs +++ b/components/salesforce_rest_api/actions/update-account/update-account.mjs @@ -1,62 +1,75 @@ -import common from "../common/base.mjs"; +import common, { getProps } from "../common/base-create-update.mjs"; import account from "../../common/sobjects/account.mjs"; -import { - pickBy, pick, -} from "lodash-es"; -import { toSingleLineString } from "../../common/utils.mjs"; +import { docsLink } from "../create-account/create-account.mjs"; -const { salesforce } = common.props; +const { + salesforce, ...props +} = getProps({ + createOrUpdate: "update", + objType: account, + docsLink, +}); export default { ...common, key: "salesforce_rest_api-update-account", name: "Update Account", - description: toSingleLineString(` - Updates a Salesforce account, representing an individual account, - which is an organization or person involved with your business (such as customers, competitors, and partners). - See [Account SObject](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_account.htm) - and [Update Record](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_update_fields.htm) - `), - version: "0.2.7", + description: `Updates a Salesforce account. [See the documentation](${docsLink})`, + version: "0.3.1", type: "action", - props: { - salesforce, - AccountId: { - type: "string", - label: "Account ID", - description: "ID of the Account to modify.", + methods: { + ...common.methods, + getObjectType() { + return "Account"; }, - Name: { - type: "string", - label: "Name", - description: "Name of the account. Maximum size is 255 characters. If the account has a record type of Person Account:\nThis value is the concatenation of the FirstName, MiddleName, LastName, and Suffix of the associated person contact.", - optional: true, + getAdvancedProps() { + return account.extraProps; }, - selector: { + }, + props: { + salesforce, + accountId: { propDefinition: [ salesforce, - "fieldSelector", + "recordId", + () => ({ + objType: "Account", + nameField: "Name", + }), ], - description: `${salesforce.propDefinitions.fieldSelector.description} Account`, - options: () => Object.keys(account), - reloadProps: true, + label: "Account ID", + description: "The Account to update.", }, - }, - additionalProps() { - return this.additionalProps(this.selector, account); + ...props, }, async run({ $ }) { - const data = pickBy(pick(this, [ - "AccountId", - "Name", - ...this.selector, - ])); - const response = await this.salesforce.updateAccount({ + /* eslint-disable no-unused-vars */ + const { + salesforce, + getAdvancedProps, + getObjectType, + getAdditionalFields, + formatDateTimeProps, + accountId, + useAdvancedProps, + docsInfo, + dateInfo, + additionalFields, + ...data + } = this; + /* eslint-enable no-unused-vars */ + const response = await salesforce.updateRecord("Account", { $, - id: this.AccountId, - data, + id: accountId, + data: { + ...data, + ...getAdditionalFields(), + }, }); - $.export("$summary", `Successfully updated account ${this.AccountId}`); + $.export( + "$summary", + `Successfully updated account (ID: ${this.accountId})`, + ); return response; }, }; diff --git a/components/salesforce_rest_api/actions/update-contact/update-contact.mjs b/components/salesforce_rest_api/actions/update-contact/update-contact.mjs index 5a51d07995fa0..36f63b47f7392 100644 --- a/components/salesforce_rest_api/actions/update-contact/update-contact.mjs +++ b/components/salesforce_rest_api/actions/update-contact/update-contact.mjs @@ -1,61 +1,80 @@ -import common from "../common/base.mjs"; +import common, { getProps } from "../common/base-create-update.mjs"; import contact from "../../common/sobjects/contact.mjs"; -import { - pickBy, pick, -} from "lodash-es"; -import { toSingleLineString } from "../../common/utils.mjs"; +import { docsLink } from "../create-contact/create-contact.mjs"; -const { salesforce } = common.props; +const { + salesforce, ...props +} = getProps({ + createOrUpdate: "update", + objType: contact, + docsLink, + showDateInfo: true, +}); export default { ...common, key: "salesforce_rest_api-update-contact", name: "Update Contact", - description: toSingleLineString(` - Updates a Contact, which is a person associated with an account. - See [Contact SObject](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_contact.htm) - and [Update Record](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_update_fields.htm) - `), - version: "0.2.7", + description: `Updates a contact. [See the documentation](${docsLink})`, + version: "0.3.1", type: "action", - props: { - salesforce, - ContactId: { - type: "string", - label: "Contact ID", - description: "ID of the Contact to update.", + methods: { + ...common.methods, + getObjectType() { + return "Contact"; }, - LastName: { - type: "string", - label: "Last Name", - description: "Last name of the contact up to 80 characters.", - optional: true, + getAdvancedProps() { + return contact.extraProps; }, - selector: { + }, + props: { + salesforce, + contactId: { propDefinition: [ salesforce, - "fieldSelector", + "recordId", + () => ({ + objType: "Contact", + nameField: "Name", + }), ], - description: `${salesforce.propDefinitions.fieldSelector.description} Contact`, - options: () => Object.keys(contact), - reloadProps: true, + label: "Contact ID", + description: "The Contact to update.", }, - }, - additionalProps() { - return this.additionalProps(this.selector, contact); + ...props, }, async run({ $ }) { - const data = pickBy(pick(this, [ - "ContactId", - "LastName", - ...this.selector, - ])); - const response = await this.salesforce.updateContact({ + /* eslint-disable no-unused-vars */ + const { + salesforce, + getAdvancedProps, + getObjectType, + getAdditionalFields, + formatDateTimeProps, + contactId, + useAdvancedProps, + docsInfo, + dateInfo, + additionalFields, + Birthdate, + ...data + } = this; + /* eslint-enable no-unused-vars */ + const response = await salesforce.updateRecord("Contact", { $, - id: this.ContactId, - data, + id: contactId, + data: { + ...data, + ...formatDateTimeProps({ + Birthdate, + }), + ...getAdditionalFields(), + }, }); - $.export("$summary", `Successfully updated contact for ${this.ContactId}`); + $.export( + "$summary", + `Successfully updated contact (ID: ${this.contactId})`, + ); return response; }, }; diff --git a/components/salesforce_rest_api/actions/update-opportunity/update-opportunity.mjs b/components/salesforce_rest_api/actions/update-opportunity/update-opportunity.mjs index 684d3932ee586..46e6ff90b1dff 100644 --- a/components/salesforce_rest_api/actions/update-opportunity/update-opportunity.mjs +++ b/components/salesforce_rest_api/actions/update-opportunity/update-opportunity.mjs @@ -1,75 +1,77 @@ -import common from "../common/base.mjs"; +import common, { getProps } from "../common/base-create-update.mjs"; import opportunity from "../../common/sobjects/opportunity.mjs"; -import { - pickBy, pick, -} from "lodash-es"; -import { toSingleLineString } from "../../common/utils.mjs"; +import { docsLink } from "../create-opportunity/create-opportunity.mjs"; -const { salesforce } = common.props; +const { + salesforce, ...props +} = getProps({ + createOrUpdate: "update", + objType: opportunity, + docsLink, + showDateInfo: true, +}); export default { ...common, key: "salesforce_rest_api-update-opportunity", name: "Update Opportunity", - description: toSingleLineString(` - Updates an opportunity, which represents an opportunity, which is a sale or pending deal. - See [Opportunity SObject](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_opportunity.htm) - and [Update Record](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_update_fields.htm) - `), - version: "0.2.7", + description: `Updates an opportunity. [See the documentation](${docsLink})`, + version: "0.3.1", type: "action", - props: { - salesforce, - OpportunityId: { - type: "string", - label: "Opportunity ID", - description: "ID of the Opportunity to update.", - }, - CloseDate: { - type: "string", - label: "CloseDate", - description: "Date when the opportunity is expected to close.", - optional: true, - }, - Name: { - type: "string", - label: "Name", - description: "A name for this opportunity. Limit: 120 characters.", - optional: true, + methods: { + ...common.methods, + getObjectType() { + return "Opportunity"; }, - StageName: { - type: "string", - label: "StageName", - description: "Current stage of this record. The StageName field controls several other fields on an opportunity. Each of the fields can be directly set or implied by changing the StageName field. In addition, the StageName field is a picklist, so it has additional members in the returned describeSObjectResult to indicate how it affects the other fields. To obtain the stage name values in the picklist, query the OpportunityStage object. If the StageName is updated, then the ForecastCategoryName, IsClosed, IsWon, and Probability are automatically updated based on the stage-category mapping.", - optional: true, + getAdvancedProps() { + return opportunity.extraProps; }, - selector: { + }, + props: { + salesforce, + opportunityId: { propDefinition: [ salesforce, - "fieldSelector", + "recordId", + () => ({ + objType: "Opportunity", + nameField: "Name", + }), ], - description: `${salesforce.propDefinitions.fieldSelector.description} Opportunity`, - options: () => Object.keys(opportunity), - reloadProps: true, + label: "Opportunity ID", + description: "The Opportunity to update.", }, - }, - additionalProps() { - return this.additionalProps(this.selector, opportunity); + ...props, }, async run({ $ }) { - const data = pickBy(pick(this, [ - "OpportunityId", - "CloseDate", - "Name", - "StageName", - ...this.selector, - ])); - const response = await this.salesforce.updateOpportunity({ + /* eslint-disable no-unused-vars */ + const { + salesforce, + getAdvancedProps, + getObjectType, + getAdditionalFields, + formatDateTimeProps, + opportunityId, + useAdvancedProps, + docsInfo, + dateInfo, + additionalFields, + CloseDate, + ...data + } = this; + /* eslint-enable no-unused-vars */ + const response = await salesforce.updateRecord("Opportunity", { $, - id: this.OpportunityId, - data, + id: opportunityId, + data: { + ...data, + ...formatDateTimeProps({ + CloseDate, + }), + ...getAdditionalFields(), + }, }); - $.export("$summary", `Successfully updated opportunity ${this.OpportunityId}`); + $.export("$summary", `Successfully updated opportunity (ID: ${opportunityId})`); return response; }, }; diff --git a/components/salesforce_rest_api/actions/update-record/update-record.mjs b/components/salesforce_rest_api/actions/update-record/update-record.mjs index 6cf3d616eb279..5d175f183a360 100644 --- a/components/salesforce_rest_api/actions/update-record/update-record.mjs +++ b/components/salesforce_rest_api/actions/update-record/update-record.mjs @@ -1,46 +1,93 @@ +import { + convertFieldsToProps, getAdditionalFields, +} from "../../common/props-utils.mjs"; import salesforce from "../../salesforce_rest_api.app.mjs"; -import { toSingleLineString } from "../../common/utils.mjs"; +import { additionalFields } from "../common/base-create-update.mjs"; export default { key: "salesforce_rest_api-update-record", name: "Update Record", - description: toSingleLineString(` - Updates a record of a given resource. - [See docs here](https://developer.salesforce.com/docs/atlas.en-us.228.0.api_rest.meta/api_rest/dome_update_fields.htm) - `), - version: "0.2.7", + description: "Update fields of a record. [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_update_fields.htm)", + version: "0.3.1", type: "action", props: { salesforce, - objectType: { + sobjectType: { propDefinition: [ salesforce, "objectType", ], - description: "SObject Type of record to be updated", + description: "The type of object to update a record of.", + }, - sobjectId: { + recordId: { propDefinition: [ salesforce, - "sobjectId", + "recordId", (c) => ({ - objectType: c.objectType, + objType: c.sobjectType, }), ], - description: "ID of the SObject record to be updated", + description: + "The record to update.", }, - sobject: { - type: "object", - label: "SObject fields and values", - description: "SObject record data to patch", + fieldsToUpdate: { + propDefinition: [ + salesforce, + "fieldsToUpdate", + (c) => ({ + objType: c.sobjectType, + }), + ], + reloadProps: true, }, }, + methods: { + getAdditionalFields, + convertFieldsToProps, + }, + async additionalProps() { + const { + sobjectType, fieldsToUpdate, + } = this; + const fields = await this.salesforce.getFieldsForObjectType(sobjectType); + + const selectedFields = fields.filter(({ name }) => fieldsToUpdate.includes(name)); + const selectedFieldProps = this.convertFieldsToProps(selectedFields); + + return { + docsInfo: { + type: "alert", + alertType: "info", + content: `[See the documentation](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_${sobjectType.toLowerCase()}.htm) for information on all available fields.`, + }, + ...selectedFieldProps, + additionalFields, + }; + }, async run({ $ }) { - await this.salesforce.updateRecord(this.objectType, { + /* eslint-disable no-unused-vars */ + const { + salesforce, + sobjectType, + recordId, + fieldsToUpdate, + getAdditionalFields: getData, + convertFieldsToProps, + docsInfo, + additionalFields, + ...data + } = this; + /* eslint-enable no-unused-vars */ + const response = await this.salesforce.updateRecord(sobjectType, { $, - id: this.sobjectId, - data: this.sobject, + id: recordId, + data: { + ...data, + ...getData(), + }, }); - $.export("$summary", `Updated record ${this.objectType}`); + $.export("$summary", `Successfully updated ${sobjectType} record (ID: ${recordId})`); + return response; }, }; diff --git a/components/salesforce_rest_api/actions/upsert-record/upsert-record.mjs b/components/salesforce_rest_api/actions/upsert-record/upsert-record.mjs new file mode 100644 index 0000000000000..a95b4321c4a72 --- /dev/null +++ b/components/salesforce_rest_api/actions/upsert-record/upsert-record.mjs @@ -0,0 +1,120 @@ +import { + convertFieldsToProps, getAdditionalFields, +} from "../../common/props-utils.mjs"; +import salesforce from "../../salesforce_rest_api.app.mjs"; +import { additionalFields } from "../common/base-create-update.mjs"; + +export default { + key: "salesforce_rest_api-upsert-record", + name: "Upsert Record", + description: "Create or update a record of a given object. [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_upsert.htm)", + version: "0.0.2", + type: "action", + props: { + salesforce, + objectType: { + propDefinition: [ + salesforce, + "objectType", + ], + description: "The type of object to create a record of", + reloadProps: true, + }, + }, + methods: { + getAdditionalFields, + convertFieldsToProps, + async upsertRecord(sobjectName, { + externalIdFieldName, externalIdValue, ...args + }) { + const url = `${this.salesforce._sObjectTypeApiUrl(sobjectName)}/${externalIdFieldName}/${externalIdValue}`; + return this.salesforce._makeRequest({ + url, + method: "PATCH", + ...args, + }); + }, + }, + async additionalProps() { + const { objectType } = this; + const fields = await this.salesforce.getFieldsForObjectType(objectType); + + const requiredFields = fields.filter((field) => { + return field.createable && field.updateable && !field.nillable && !field.defaultedOnCreate; + }); + + const externalIdFieldOptions = fields.filter((field) => field.externalId).map(({ + label, name, + }) => ({ + label, + value: name, + })); + + const requiredFieldProps = this.convertFieldsToProps(requiredFields); + + return { + docsInfo: { + type: "alert", + alertType: "info", + content: `[See the documentation](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_${objectType.toLowerCase()}.htm) for information on all available fields.`, + }, + externalIdFieldName: { + type: "string", + label: "External ID Field", + description: "The field to use as the external ID to identify the record.", + options: externalIdFieldOptions, + }, + docsInfoExtId: { + type: "alert", + alertType: "info", + content: "If you don't see any fields in the above list, you probably need to create one in Salesforce's Object Manager. Only a field marked as an external id field can be used to identify a record.", + }, + externalIdValue: { + type: "string", + label: "External ID Value", + description: "The value of the external ID field selected above. If a record with this value exists, it will be updated, otherwise a new one will be created.", + }, + updateOnly: { + type: "boolean", + label: "Update Only", + description: "If enabled, the action will only update an existing record, but not create one.", + optional: true, + }, + ...requiredFieldProps, + additionalFields, + }; + }, + async run({ $ }) { + /* eslint-disable no-unused-vars */ + const { + salesforce, + objectType, + getAdditionalFields: getData, + convertFieldsToProps, + docsInfo, + docsInfoExtId, + additionalFields, + externalIdFieldName, + externalIdValue, + updateOnly, + ...data + } = this; + /* eslint-enable no-unused-vars */ + const response = await this.upsertRecord(objectType, { + $, + externalIdFieldName, + externalIdValue, + params: { + updateOnly, + }, + data: { + ...data, + ...getData(), + }, + }); + $.export("$summary", `Successfully ${response.created + ? "created" + : "updated"} ${objectType} record (ID: ${response.id})`); + return response; + }, +}; diff --git a/components/salesforce_rest_api/common/all-sobjects.mjs b/components/salesforce_rest_api/common/all-sobjects.mjs new file mode 100644 index 0000000000000..1a75aa83c60fc --- /dev/null +++ b/components/salesforce_rest_api/common/all-sobjects.mjs @@ -0,0 +1,3812 @@ +// The execution context of additionalProps prevents usage of propDefinitions +// and of standard async options methods (an arrow function is required) +// And the execution context of async options prevents any variable "leaking" +// so the only way is to actually paste these out as plain individual functions, +// unfortunately, without any references to code outside the function + +/* +getRecords: () => this.salesforce.listRecordOptions({ objType: "ObjType", +nameField: "NameField" }) +*/ + +const sobjects = [ + { + name: "AIInsightAction", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AIInsightAction", + nameField: "Name", + }), + }, + { + name: "AIInsightFeedback", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AIInsightFeedback", + nameField: "Name", + }), + }, + { + name: "AIInsightReason", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AIInsightReason", + nameField: "Name", + }), + }, + { + name: "AIInsightValue", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AIInsightValue", + nameField: "Name", + }), + }, + { + name: "AIRecordInsight", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AIRecordInsight", + nameField: "Name", + }), + }, + { + name: "Account", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Account", + nameField: "Name", + }), + }, + { + name: "AccountCleanInfo", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AccountCleanInfo", + nameField: "Name", + }), + }, + { + name: "AccountContactRole", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AccountContactRole", + nameField: "Id", + }), + }, + { + name: "AccountFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AccountFeed", + nameField: "Id", + }), + }, + { + name: "AccountHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AccountHistory", + nameField: "Id", + }), + }, + { + name: "AdditionalNumber", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AdditionalNumber", + nameField: "Name", + }), + }, + { + name: "Address", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Address", + nameField: "Name", + }), + }, + { + name: "Announcement", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Announcement", + nameField: "Id", + }), + }, + { + name: "ApexClass", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ApexClass", + nameField: "Name", + }), + }, + { + name: "ApexComponent", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ApexComponent", + nameField: "Name", + }), + }, + { + name: "ApexPage", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ApexPage", + nameField: "Name", + }), + }, + { + name: "ApexTrigger", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ApexTrigger", + nameField: "Name", + }), + }, + { + name: "ApiAnomalyEventStore", + nameField: "ApiAnomalyEventNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ApiAnomalyEventStore", + nameField: "ApiAnomalyEventNumber", + }), + }, + { + name: "ApiAnomalyEventStoreFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ApiAnomalyEventStoreFeed", + nameField: "Id", + }), + }, + { + name: "AppAnalyticsQueryRequest", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AppAnalyticsQueryRequest", + nameField: "Name", + }), + }, + { + name: "AppUsageAssignment", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AppUsageAssignment", + nameField: "Name", + }), + }, + { + name: "Asset", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Asset", + nameField: "Name", + }), + }, + { + name: "AssetAction", + nameField: "AssetActionNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AssetAction", + nameField: "AssetActionNumber", + }), + }, + { + name: "AssetActionSource", + nameField: "AssetActionSourceNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AssetActionSource", + nameField: "AssetActionSourceNumber", + }), + }, + { + name: "AssetFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AssetFeed", + nameField: "Id", + }), + }, + { + name: "AssetHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AssetHistory", + nameField: "Id", + }), + }, + { + name: "AssetRelationship", + nameField: "AssetRelationshipNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AssetRelationship", + nameField: "AssetRelationshipNumber", + }), + }, + { + name: "AssetRelationshipFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AssetRelationshipFeed", + nameField: "Id", + }), + }, + { + name: "AssetRelationshipHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AssetRelationshipHistory", + nameField: "Id", + }), + }, + { + name: "AssetStatePeriod", + nameField: "AssetStatePeriodNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AssetStatePeriod", + nameField: "AssetStatePeriodNumber", + }), + }, + { + name: "AssignedResource", + nameField: "AssignedResourceNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AssignedResource", + nameField: "AssignedResourceNumber", + }), + }, + { + name: "AssignedResourceFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AssignedResourceFeed", + nameField: "Id", + }), + }, + { + name: "AssociatedLocation", + nameField: "AssociatedLocationNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AssociatedLocation", + nameField: "AssociatedLocationNumber", + }), + }, + { + name: "AssociatedLocationHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AssociatedLocationHistory", + nameField: "Id", + }), + }, + { + name: "Attachment", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Attachment", + nameField: "Name", + }), + }, + { + name: "AuthorizationForm", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AuthorizationForm", + nameField: "Name", + }), + }, + { + name: "AuthorizationFormConsent", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AuthorizationFormConsent", + nameField: "Name", + }), + }, + { + name: "AuthorizationFormConsentHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AuthorizationFormConsentHistory", + nameField: "Id", + }), + }, + { + name: "AuthorizationFormDataUse", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AuthorizationFormDataUse", + nameField: "Name", + }), + }, + { + name: "AuthorizationFormDataUseHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AuthorizationFormDataUseHistory", + nameField: "Id", + }), + }, + { + name: "AuthorizationFormHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AuthorizationFormHistory", + nameField: "Id", + }), + }, + { + name: "AuthorizationFormText", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AuthorizationFormText", + nameField: "Name", + }), + }, + { + name: "AuthorizationFormTextHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "AuthorizationFormTextHistory", + nameField: "Id", + }), + }, + { + name: "BackgroundOperation", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "BackgroundOperation", + nameField: "Name", + }), + }, + { + name: "BrandTemplate", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "BrandTemplate", + nameField: "Name", + }), + }, + { + name: "BriefcaseAssignment", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "BriefcaseAssignment", + nameField: "Id", + }), + }, + { + name: "BusinessHours", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "BusinessHours", + nameField: "Name", + }), + }, + { + name: "BusinessProcess", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "BusinessProcess", + nameField: "Name", + }), + }, + { + name: "BuyerGroup", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "BuyerGroup", + nameField: "Name", + }), + }, + { + name: "BuyerGroupFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "BuyerGroupFeed", + nameField: "Id", + }), + }, + { + name: "BuyerGroupHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "BuyerGroupHistory", + nameField: "Id", + }), + }, + { + name: "CalendarView", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CalendarView", + nameField: "Name", + }), + }, + { + name: "CallCenter", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CallCenter", + nameField: "Name", + }), + }, + { + name: "Campaign", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Campaign", + nameField: "Name", + }), + }, + { + name: "CampaignFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CampaignFeed", + nameField: "Id", + }), + }, + { + name: "CampaignHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CampaignHistory", + nameField: "Id", + }), + }, + { + name: "CampaignMember", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CampaignMember", + nameField: "Id", + }), + }, + { + name: "CampaignMemberStatus", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CampaignMemberStatus", + nameField: "Id", + }), + }, + { + name: "CardPaymentMethod", + nameField: "CardPaymentMethodNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CardPaymentMethod", + nameField: "CardPaymentMethodNumber", + }), + }, + { + name: "CartCheckoutSession", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CartCheckoutSession", + nameField: "Name", + }), + }, + { + name: "CartDeliveryGroup", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CartDeliveryGroup", + nameField: "Name", + }), + }, + { + name: "CartDeliveryGroupMethod", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CartDeliveryGroupMethod", + nameField: "Name", + }), + }, + { + name: "CartItem", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CartItem", + nameField: "Name", + }), + }, + { + name: "CartRelatedItem", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CartRelatedItem", + nameField: "Name", + }), + }, + { + name: "CartTax", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CartTax", + nameField: "Name", + }), + }, + { + name: "CartValidationOutput", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CartValidationOutput", + nameField: "Name", + }), + }, + { + name: "Case", + nameField: "CaseNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Case", + nameField: "CaseNumber", + }), + }, + { + name: "CaseComment", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CaseComment", + nameField: "Id", + }), + }, + { + name: "CaseContactRole", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CaseContactRole", + nameField: "Id", + }), + }, + { + name: "CaseFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CaseFeed", + nameField: "Id", + }), + }, + { + name: "CaseHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CaseHistory", + nameField: "Id", + }), + }, + { + name: "CaseSolution", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CaseSolution", + nameField: "Id", + }), + }, + { + name: "CaseTeamMember", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CaseTeamMember", + nameField: "Id", + }), + }, + { + name: "CaseTeamRole", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CaseTeamRole", + nameField: "Name", + }), + }, + { + name: "CaseTeamTemplate", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CaseTeamTemplate", + nameField: "Name", + }), + }, + { + name: "CaseTeamTemplateMember", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CaseTeamTemplateMember", + nameField: "Id", + }), + }, + { + name: "CaseTeamTemplateRecord", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CaseTeamTemplateRecord", + nameField: "Id", + }), + }, + { + name: "CategoryData", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CategoryData", + nameField: "Id", + }), + }, + { + name: "CategoryNode", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CategoryNode", + nameField: "Id", + }), + }, + { + name: "ChatterActivity", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ChatterActivity", + nameField: "Id", + }), + }, + { + name: "CollaborationGroupFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CollaborationGroupFeed", + nameField: "Id", + }), + }, + { + name: "CollaborationGroupMember", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CollaborationGroupMember", + nameField: "Id", + }), + }, + { + name: "CollaborationGroupRecord", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CollaborationGroupRecord", + nameField: "Id", + }), + }, + { + name: "CommSubscription", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CommSubscription", + nameField: "Name", + }), + }, + { + name: "CommSubscriptionChannelType", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CommSubscriptionChannelType", + nameField: "Name", + }), + }, + { + name: "CommSubscriptionChannelTypeFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CommSubscriptionChannelTypeFeed", + nameField: "Id", + }), + }, + { + name: "CommSubscriptionChannelTypeHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CommSubscriptionChannelTypeHistory", + nameField: "Id", + }), + }, + { + name: "CommSubscriptionConsent", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CommSubscriptionConsent", + nameField: "Name", + }), + }, + { + name: "CommSubscriptionConsentFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CommSubscriptionConsentFeed", + nameField: "Id", + }), + }, + { + name: "CommSubscriptionConsentHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CommSubscriptionConsentHistory", + nameField: "Id", + }), + }, + { + name: "CommSubscriptionFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CommSubscriptionFeed", + nameField: "Id", + }), + }, + { + name: "CommSubscriptionHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CommSubscriptionHistory", + nameField: "Id", + }), + }, + { + name: "CommSubscriptionTiming", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CommSubscriptionTiming", + nameField: "Name", + }), + }, + { + name: "CommSubscriptionTimingFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CommSubscriptionTimingFeed", + nameField: "Id", + }), + }, + { + name: "CommSubscriptionTimingHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CommSubscriptionTimingHistory", + nameField: "Id", + }), + }, + { + name: "ConferenceNumber", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ConferenceNumber", + nameField: "Name", + }), + }, + { + name: "ConsumptionRate", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ConsumptionRate", + nameField: "Name", + }), + }, + { + name: "ConsumptionRateHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ConsumptionRateHistory", + nameField: "Id", + }), + }, + { + name: "ConsumptionSchedule", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ConsumptionSchedule", + nameField: "Name", + }), + }, + { + name: "ConsumptionScheduleFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ConsumptionScheduleFeed", + nameField: "Id", + }), + }, + { + name: "ConsumptionScheduleHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ConsumptionScheduleHistory", + nameField: "Id", + }), + }, + { + name: "Contact", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Contact", + nameField: "Name", + }), + }, + { + name: "ContactCleanInfo", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContactCleanInfo", + nameField: "Name", + }), + }, + { + name: "ContactFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContactFeed", + nameField: "Id", + }), + }, + { + name: "ContactHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContactHistory", + nameField: "Id", + }), + }, + { + name: "ContactPointAddress", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContactPointAddress", + nameField: "Name", + }), + }, + { + name: "ContactPointAddressHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContactPointAddressHistory", + nameField: "Id", + }), + }, + { + name: "ContactPointConsent", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContactPointConsent", + nameField: "Name", + }), + }, + { + name: "ContactPointConsentHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContactPointConsentHistory", + nameField: "Id", + }), + }, + { + name: "ContactPointEmail", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContactPointEmail", + nameField: "Name", + }), + }, + { + name: "ContactPointEmailHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContactPointEmailHistory", + nameField: "Id", + }), + }, + { + name: "ContactPointPhone", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContactPointPhone", + nameField: "Name", + }), + }, + { + name: "ContactPointPhoneHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContactPointPhoneHistory", + nameField: "Id", + }), + }, + { + name: "ContactPointTypeConsent", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContactPointTypeConsent", + nameField: "Name", + }), + }, + { + name: "ContactPointTypeConsentHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContactPointTypeConsentHistory", + nameField: "Id", + }), + }, + { + name: "ContactRequest", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContactRequest", + nameField: "Name", + }), + }, + { + name: "ContentDocumentFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContentDocumentFeed", + nameField: "Id", + }), + }, + { + name: "ContentDocumentHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContentDocumentHistory", + nameField: "Id", + }), + }, + { + name: "ContentFolder", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContentFolder", + nameField: "Name", + }), + }, + { + name: "ContentVersionHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContentVersionHistory", + nameField: "Id", + }), + }, + { + name: "Contract", + nameField: "ContractNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Contract", + nameField: "ContractNumber", + }), + }, + { + name: "ContractContactRole", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContractContactRole", + nameField: "Id", + }), + }, + { + name: "ContractFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContractFeed", + nameField: "Id", + }), + }, + { + name: "ContractHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContractHistory", + nameField: "Id", + }), + }, + { + name: "ContractLineItem", + nameField: "LineItemNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContractLineItem", + nameField: "LineItemNumber", + }), + }, + { + name: "ContractLineItemHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ContractLineItemHistory", + nameField: "Id", + }), + }, + { + name: "CredentialStuffingEventStore", + nameField: "CredentialStuffingEventNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CredentialStuffingEventStore", + nameField: "CredentialStuffingEventNumber", + }), + }, + { + name: "CredentialStuffingEventStoreFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CredentialStuffingEventStoreFeed", + nameField: "Id", + }), + }, + { + name: "CreditMemo", + nameField: "DocumentNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CreditMemo", + nameField: "DocumentNumber", + }), + }, + { + name: "CreditMemoFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CreditMemoFeed", + nameField: "Id", + }), + }, + { + name: "CreditMemoHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CreditMemoHistory", + nameField: "Id", + }), + }, + { + name: "CreditMemoInvApplication", + nameField: "CreditMemoInvoiceNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CreditMemoInvApplication", + nameField: "CreditMemoInvoiceNumber", + }), + }, + { + name: "CreditMemoInvApplicationFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CreditMemoInvApplicationFeed", + nameField: "Id", + }), + }, + { + name: "CreditMemoInvApplicationHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CreditMemoInvApplicationHistory", + nameField: "Id", + }), + }, + { + name: "CreditMemoLine", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CreditMemoLine", + nameField: "Name", + }), + }, + { + name: "CreditMemoLineFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CreditMemoLineFeed", + nameField: "Id", + }), + }, + { + name: "CreditMemoLineHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "CreditMemoLineHistory", + nameField: "Id", + }), + }, + { + name: "DandBCompany", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "DandBCompany", + nameField: "Name", + }), + }, + { + name: "DashboardComponentFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "DashboardComponentFeed", + nameField: "Id", + }), + }, + { + name: "DashboardFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "DashboardFeed", + nameField: "Id", + }), + }, + { + name: "DataAssessmentFieldMetric", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "DataAssessmentFieldMetric", + nameField: "Name", + }), + }, + { + name: "DataAssessmentMetric", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "DataAssessmentMetric", + nameField: "Name", + }), + }, + { + name: "DataAssessmentValueMetric", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "DataAssessmentValueMetric", + nameField: "Name", + }), + }, + { + name: "DataKitDeploymentLog", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "DataKitDeploymentLog", + nameField: "Id", + }), + }, + { + name: "DataUseLegalBasis", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "DataUseLegalBasis", + nameField: "Name", + }), + }, + { + name: "DataUseLegalBasisHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "DataUseLegalBasisHistory", + nameField: "Id", + }), + }, + { + name: "DataUsePurpose", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "DataUsePurpose", + nameField: "Name", + }), + }, + { + name: "DataUsePurposeHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "DataUsePurposeHistory", + nameField: "Id", + }), + }, + { + name: "DatacloudOwnedEntity", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "DatacloudOwnedEntity", + nameField: "Name", + }), + }, + { + name: "DatacloudPurchaseUsage", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "DatacloudPurchaseUsage", + nameField: "Name", + }), + }, + { + name: "DigitalWallet", + nameField: "DigitalWalletNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "DigitalWallet", + nameField: "DigitalWalletNumber", + }), + }, + { + name: "Document", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Document", + nameField: "Name", + }), + }, + { + name: "DuplicateRecordItem", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "DuplicateRecordItem", + nameField: "Name", + }), + }, + { + name: "DuplicateRecordSet", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "DuplicateRecordSet", + nameField: "Name", + }), + }, + { + name: "EmailMessage", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "EmailMessage", + nameField: "Id", + }), + }, + { + name: "EmailMessageRelation", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "EmailMessageRelation", + nameField: "Id", + }), + }, + { + name: "EmailServicesAddress", + nameField: "LocalPart", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "EmailServicesAddress", + nameField: "LocalPart", + }), + }, + { + name: "EmailServicesFunction", + nameField: "FunctionName", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "EmailServicesFunction", + nameField: "FunctionName", + }), + }, + { + name: "EmailTemplate", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "EmailTemplate", + nameField: "Name", + }), + }, + { + name: "EngagementChannelType", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "EngagementChannelType", + nameField: "Name", + }), + }, + { + name: "EngagementChannelTypeFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "EngagementChannelTypeFeed", + nameField: "Id", + }), + }, + { + name: "EngagementChannelTypeHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "EngagementChannelTypeHistory", + nameField: "Id", + }), + }, + { + name: "EnhancedLetterhead", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "EnhancedLetterhead", + nameField: "Name", + }), + }, + { + name: "EnhancedLetterheadFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "EnhancedLetterheadFeed", + nameField: "Id", + }), + }, + { + name: "Entitlement", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Entitlement", + nameField: "Name", + }), + }, + { + name: "EntitlementContact", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "EntitlementContact", + nameField: "Name", + }), + }, + { + name: "EntitlementFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "EntitlementFeed", + nameField: "Id", + }), + }, + { + name: "EntitlementHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "EntitlementHistory", + nameField: "Id", + }), + }, + { + name: "EntityMilestone", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "EntityMilestone", + nameField: "Name", + }), + }, + { + name: "EntityMilestoneFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "EntityMilestoneFeed", + nameField: "Id", + }), + }, + { + name: "EntityMilestoneHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "EntityMilestoneHistory", + nameField: "Id", + }), + }, + { + name: "EntitySubscription", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "EntitySubscription", + nameField: "Id", + }), + }, + { + name: "Event", + nameField: "Subject", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Event", + nameField: "Subject", + }), + }, + { + name: "EventFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "EventFeed", + nameField: "Id", + }), + }, + { + name: "EventRelation", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "EventRelation", + nameField: "Id", + }), + }, + { + name: "ExpressionFilter", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ExpressionFilter", + nameField: "Name", + }), + }, + { + name: "ExpressionFilterCriteria", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ExpressionFilterCriteria", + nameField: "Name", + }), + }, + { + name: "ExternalEvent", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ExternalEvent", + nameField: "Name", + }), + }, + { + name: "ExternalEventMapping", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ExternalEventMapping", + nameField: "Name", + }), + }, + { + name: "FeedComment", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FeedComment", + nameField: "Id", + }), + }, + { + name: "FeedItem", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FeedItem", + nameField: "Id", + }), + }, + { + name: "FeedRevision", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FeedRevision", + nameField: "Id", + }), + }, + { + name: "FileSearchActivity", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FileSearchActivity", + nameField: "Name", + }), + }, + { + name: "FinanceBalanceSnapshot", + nameField: "FinanceBalanceSnapshotNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FinanceBalanceSnapshot", + nameField: "FinanceBalanceSnapshotNumber", + }), + }, + { + name: "FinanceTransaction", + nameField: "FinanceTransactionNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FinanceTransaction", + nameField: "FinanceTransactionNumber", + }), + }, + { + name: "FiscalYearSettings", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FiscalYearSettings", + nameField: "Name", + }), + }, + { + name: "FlowInterview", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FlowInterview", + nameField: "Name", + }), + }, + { + name: "FlowInterviewLog", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FlowInterviewLog", + nameField: "Name", + }), + }, + { + name: "FlowInterviewLogEntry", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FlowInterviewLogEntry", + nameField: "Name", + }), + }, + { + name: "FlowRecordRelation", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FlowRecordRelation", + nameField: "Name", + }), + }, + { + name: "FlowStageRelation", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FlowStageRelation", + nameField: "Name", + }), + }, + { + name: "Folder", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Folder", + nameField: "Name", + }), + }, + { + name: "FulfillmentOrder", + nameField: "FulfillmentOrderNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FulfillmentOrder", + nameField: "FulfillmentOrderNumber", + }), + }, + { + name: "FulfillmentOrderFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FulfillmentOrderFeed", + nameField: "Id", + }), + }, + { + name: "FulfillmentOrderItemAdjustment", + nameField: "FulfillmentOrderItemAdjustmentNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FulfillmentOrderItemAdjustment", + nameField: "FulfillmentOrderItemAdjustmentNumber", + }), + }, + { + name: "FulfillmentOrderItemAdjustmentFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FulfillmentOrderItemAdjustmentFeed", + nameField: "Id", + }), + }, + { + name: "FulfillmentOrderItemTax", + nameField: "FulfillmentOrderItemTaxNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FulfillmentOrderItemTax", + nameField: "FulfillmentOrderItemTaxNumber", + }), + }, + { + name: "FulfillmentOrderItemTaxFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FulfillmentOrderItemTaxFeed", + nameField: "Id", + }), + }, + { + name: "FulfillmentOrderLineItem", + nameField: "FulfillmentOrderLineItemNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FulfillmentOrderLineItem", + nameField: "FulfillmentOrderLineItemNumber", + }), + }, + { + name: "FulfillmentOrderLineItemFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "FulfillmentOrderLineItemFeed", + nameField: "Id", + }), + }, + { + name: "Group", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Group", + nameField: "Name", + }), + }, + { + name: "GroupMember", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "GroupMember", + nameField: "Id", + }), + }, + { + name: "Holiday", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Holiday", + nameField: "Name", + }), + }, + { + name: "Idea", + nameField: "Title", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Idea", + nameField: "Title", + }), + }, + { + name: "IdeaComment", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "IdeaComment", + nameField: "Id", + }), + }, + { + name: "Image", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Image", + nameField: "Name", + }), + }, + { + name: "Individual", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Individual", + nameField: "Name", + }), + }, + { + name: "IndividualHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "IndividualHistory", + nameField: "Id", + }), + }, + { + name: "InstalledMobileApp", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "InstalledMobileApp", + nameField: "Name", + }), + }, + { + name: "Invoice", + nameField: "DocumentNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Invoice", + nameField: "DocumentNumber", + }), + }, + { + name: "InvoiceFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "InvoiceFeed", + nameField: "Id", + }), + }, + { + name: "InvoiceHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "InvoiceHistory", + nameField: "Id", + }), + }, + { + name: "InvoiceLine", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "InvoiceLine", + nameField: "Name", + }), + }, + { + name: "InvoiceLineFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "InvoiceLineFeed", + nameField: "Id", + }), + }, + { + name: "InvoiceLineHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "InvoiceLineHistory", + nameField: "Id", + }), + }, + { + name: "Lead", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Lead", + nameField: "Name", + }), + }, + { + name: "LeadCleanInfo", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "LeadCleanInfo", + nameField: "Name", + }), + }, + { + name: "LeadFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "LeadFeed", + nameField: "Id", + }), + }, + { + name: "LeadHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "LeadHistory", + nameField: "Id", + }), + }, + { + name: "LegalEntity", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "LegalEntity", + nameField: "Name", + }), + }, + { + name: "LegalEntityFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "LegalEntityFeed", + nameField: "Id", + }), + }, + { + name: "LegalEntityHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "LegalEntityHistory", + nameField: "Id", + }), + }, + { + name: "ListEmail", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ListEmail", + nameField: "Name", + }), + }, + { + name: "ListEmailIndividualRecipient", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ListEmailIndividualRecipient", + nameField: "Name", + }), + }, + { + name: "ListEmailRecipientSource", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ListEmailRecipientSource", + nameField: "Name", + }), + }, + { + name: "Location", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Location", + nameField: "Name", + }), + }, + { + name: "LocationFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "LocationFeed", + nameField: "Id", + }), + }, + { + name: "LocationHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "LocationHistory", + nameField: "Id", + }), + }, + { + name: "Macro", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Macro", + nameField: "Name", + }), + }, + { + name: "MacroHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "MacroHistory", + nameField: "Id", + }), + }, + { + name: "MacroInstruction", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "MacroInstruction", + nameField: "Name", + }), + }, + { + name: "MacroUsage", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "MacroUsage", + nameField: "Name", + }), + }, + { + name: "MailmergeTemplate", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "MailmergeTemplate", + nameField: "Name", + }), + }, + { + name: "MatchingInformation", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "MatchingInformation", + nameField: "Name", + }), + }, + { + name: "MessagingDeliveryError", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "MessagingDeliveryError", + nameField: "Name", + }), + }, + { + name: "MessagingEndUser", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "MessagingEndUser", + nameField: "Name", + }), + }, + { + name: "MessagingEndUserHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "MessagingEndUserHistory", + nameField: "Id", + }), + }, + { + name: "MessagingSession", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "MessagingSession", + nameField: "Name", + }), + }, + { + name: "MessagingSessionFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "MessagingSessionFeed", + nameField: "Id", + }), + }, + { + name: "MessagingSessionHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "MessagingSessionHistory", + nameField: "Id", + }), + }, + { + name: "MlFeatureValueMetric", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "MlFeatureValueMetric", + nameField: "Name", + }), + }, + { + name: "Note", + nameField: "Title", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Note", + nameField: "Title", + }), + }, + { + name: "OperatingHours", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "OperatingHours", + nameField: "Name", + }), + }, + { + name: "OperatingHoursFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "OperatingHoursFeed", + nameField: "Id", + }), + }, + { + name: "OperatingHoursHoliday", + nameField: "OperatingHoursHolidayNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "OperatingHoursHoliday", + nameField: "OperatingHoursHolidayNumber", + }), + }, + { + name: "OperatingHoursHolidayFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "OperatingHoursHolidayFeed", + nameField: "Id", + }), + }, + { + name: "Opportunity", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Opportunity", + nameField: "Name", + }), + }, + { + name: "OpportunityCompetitor", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "OpportunityCompetitor", + nameField: "Id", + }), + }, + { + name: "OpportunityContactRole", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "OpportunityContactRole", + nameField: "Id", + }), + }, + { + name: "OpportunityFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "OpportunityFeed", + nameField: "Id", + }), + }, + { + name: "OpportunityFieldHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "OpportunityFieldHistory", + nameField: "Id", + }), + }, + { + name: "OpportunityHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "OpportunityHistory", + nameField: "Id", + }), + }, + { + name: "OpportunityLineItem", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "OpportunityLineItem", + nameField: "Name", + }), + }, + { + name: "Order", + nameField: "OrderNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Order", + nameField: "OrderNumber", + }), + }, + { + name: "OrderFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "OrderFeed", + nameField: "Id", + }), + }, + { + name: "OrderHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "OrderHistory", + nameField: "Id", + }), + }, + { + name: "OrderItem", + nameField: "OrderItemNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "OrderItem", + nameField: "OrderItemNumber", + }), + }, + { + name: "OrderItemFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "OrderItemFeed", + nameField: "Id", + }), + }, + { + name: "OrderItemHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "OrderItemHistory", + nameField: "Id", + }), + }, + { + name: "OrgDeleteRequest", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "OrgDeleteRequest", + nameField: "Name", + }), + }, + { + name: "OrgWideEmailAddress", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "OrgWideEmailAddress", + nameField: "Id", + }), + }, + { + name: "Organization", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Organization", + nameField: "Name", + }), + }, + { + name: "Partner", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Partner", + nameField: "Id", + }), + }, + { + name: "PartyConsent", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "PartyConsent", + nameField: "Name", + }), + }, + { + name: "PartyConsentFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "PartyConsentFeed", + nameField: "Id", + }), + }, + { + name: "PartyConsentHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "PartyConsentHistory", + nameField: "Id", + }), + }, + { + name: "Payment", + nameField: "PaymentNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Payment", + nameField: "PaymentNumber", + }), + }, + { + name: "PaymentAuthorization", + nameField: "PaymentAuthorizationNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "PaymentAuthorization", + nameField: "PaymentAuthorizationNumber", + }), + }, + { + name: "PaymentGateway", + nameField: "PaymentGatewayName", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "PaymentGateway", + nameField: "PaymentGatewayName", + }), + }, + { + name: "PaymentGatewayLog", + nameField: "PaymentGatewayLogNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "PaymentGatewayLog", + nameField: "PaymentGatewayLogNumber", + }), + }, + { + name: "PaymentGroup", + nameField: "PaymentGroupNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "PaymentGroup", + nameField: "PaymentGroupNumber", + }), + }, + { + name: "PaymentLineInvoice", + nameField: "PaymentLineInvoiceNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "PaymentLineInvoice", + nameField: "PaymentLineInvoiceNumber", + }), + }, + { + name: "Period", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Period", + nameField: "Id", + }), + }, + { + name: "Pricebook2", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Pricebook2", + nameField: "Name", + }), + }, + { + name: "Pricebook2History", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Pricebook2History", + nameField: "Id", + }), + }, + { + name: "PricebookEntry", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "PricebookEntry", + nameField: "Name", + }), + }, + { + name: "PricebookEntryHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "PricebookEntryHistory", + nameField: "Id", + }), + }, + { + name: "ProcessException", + nameField: "ProcessExceptionNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ProcessException", + nameField: "ProcessExceptionNumber", + }), + }, + { + name: "ProcessInstanceNode", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ProcessInstanceNode", + nameField: "Id", + }), + }, + { + name: "Product2", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Product2", + nameField: "Name", + }), + }, + { + name: "Product2Feed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Product2Feed", + nameField: "Id", + }), + }, + { + name: "Product2History", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Product2History", + nameField: "Id", + }), + }, + { + name: "ProductAttribute", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ProductAttribute", + nameField: "Name", + }), + }, + { + name: "ProductAttributeSetProduct", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ProductAttributeSetProduct", + nameField: "Name", + }), + }, + { + name: "ProductCatalog", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ProductCatalog", + nameField: "Name", + }), + }, + { + name: "ProductCatalogFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ProductCatalogFeed", + nameField: "Id", + }), + }, + { + name: "ProductCatalogHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ProductCatalogHistory", + nameField: "Id", + }), + }, + { + name: "ProductCategory", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ProductCategory", + nameField: "Name", + }), + }, + { + name: "ProductCategoryFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ProductCategoryFeed", + nameField: "Id", + }), + }, + { + name: "ProductCategoryHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ProductCategoryHistory", + nameField: "Id", + }), + }, + { + name: "ProductCategoryProduct", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ProductCategoryProduct", + nameField: "Name", + }), + }, + { + name: "ProductCategoryProductHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ProductCategoryProductHistory", + nameField: "Id", + }), + }, + { + name: "ProductConsumptionSchedule", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ProductConsumptionSchedule", + nameField: "Id", + }), + }, + { + name: "Profile", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Profile", + nameField: "Name", + }), + }, + { + name: "Promotion", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Promotion", + nameField: "Name", + }), + }, + { + name: "PromotionFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "PromotionFeed", + nameField: "Id", + }), + }, + { + name: "PromotionHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "PromotionHistory", + nameField: "Id", + }), + }, + { + name: "PromptAction", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "PromptAction", + nameField: "Name", + }), + }, + { + name: "QueueSobject", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "QueueSobject", + nameField: "Id", + }), + }, + { + name: "QuickText", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "QuickText", + nameField: "Name", + }), + }, + { + name: "QuickTextHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "QuickTextHistory", + nameField: "Id", + }), + }, + { + name: "QuickTextUsage", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "QuickTextUsage", + nameField: "Name", + }), + }, + { + name: "QuoteTemplateRichTextData", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "QuoteTemplateRichTextData", + nameField: "Name", + }), + }, + { + name: "Recommendation", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Recommendation", + nameField: "Name", + }), + }, + { + name: "RecordAction", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "RecordAction", + nameField: "Id", + }), + }, + { + name: "RecordType", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "RecordType", + nameField: "Name", + }), + }, + { + name: "Refund", + nameField: "RefundNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Refund", + nameField: "RefundNumber", + }), + }, + { + name: "RefundLinePayment", + nameField: "RefundLinePaymentNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "RefundLinePayment", + nameField: "RefundLinePaymentNumber", + }), + }, + { + name: "ReportAnomalyEventStore", + nameField: "ReportAnomalyEventNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ReportAnomalyEventStore", + nameField: "ReportAnomalyEventNumber", + }), + }, + { + name: "ReportAnomalyEventStoreFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ReportAnomalyEventStoreFeed", + nameField: "Id", + }), + }, + { + name: "ReportFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ReportFeed", + nameField: "Id", + }), + }, + { + name: "ResourceAbsence", + nameField: "AbsenceNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ResourceAbsence", + nameField: "AbsenceNumber", + }), + }, + { + name: "ResourceAbsenceFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ResourceAbsenceFeed", + nameField: "Id", + }), + }, + { + name: "ResourceAbsenceHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ResourceAbsenceHistory", + nameField: "Id", + }), + }, + { + name: "ResourcePreference", + nameField: "ResourcePreferenceNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ResourcePreference", + nameField: "ResourcePreferenceNumber", + }), + }, + { + name: "ResourcePreferenceFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ResourcePreferenceFeed", + nameField: "Id", + }), + }, + { + name: "ResourcePreferenceHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ResourcePreferenceHistory", + nameField: "Id", + }), + }, + { + name: "ReturnOrder", + nameField: "ReturnOrderNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ReturnOrder", + nameField: "ReturnOrderNumber", + }), + }, + { + name: "ReturnOrderFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ReturnOrderFeed", + nameField: "Id", + }), + }, + { + name: "ReturnOrderHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ReturnOrderHistory", + nameField: "Id", + }), + }, + { + name: "ReturnOrderItemAdjustment", + nameField: "ReturnOrderItemAdjustmentNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ReturnOrderItemAdjustment", + nameField: "ReturnOrderItemAdjustmentNumber", + }), + }, + { + name: "ReturnOrderItemTax", + nameField: "ReturnOrderItemTaxNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ReturnOrderItemTax", + nameField: "ReturnOrderItemTaxNumber", + }), + }, + { + name: "ReturnOrderLineItem", + nameField: "ReturnOrderLineItemNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ReturnOrderLineItem", + nameField: "ReturnOrderLineItemNumber", + }), + }, + { + name: "ReturnOrderLineItemFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ReturnOrderLineItemFeed", + nameField: "Id", + }), + }, + { + name: "ReturnOrderLineItemHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ReturnOrderLineItemHistory", + nameField: "Id", + }), + }, + { + name: "Scontrol", + nameField: "DeveloperName", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Scontrol", + nameField: "DeveloperName", + }), + }, + { + name: "SearchPromotionRule", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "SearchPromotionRule", + nameField: "Id", + }), + }, + { + name: "ServiceAppointment", + nameField: "AppointmentNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceAppointment", + nameField: "AppointmentNumber", + }), + }, + { + name: "ServiceAppointmentFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceAppointmentFeed", + nameField: "Id", + }), + }, + { + name: "ServiceAppointmentHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceAppointmentHistory", + nameField: "Id", + }), + }, + { + name: "ServiceContract", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceContract", + nameField: "Name", + }), + }, + { + name: "ServiceContractFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceContractFeed", + nameField: "Id", + }), + }, + { + name: "ServiceContractHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceContractHistory", + nameField: "Id", + }), + }, + { + name: "ServiceResource", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceResource", + nameField: "Name", + }), + }, + { + name: "ServiceResourceFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceResourceFeed", + nameField: "Id", + }), + }, + { + name: "ServiceResourceHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceResourceHistory", + nameField: "Id", + }), + }, + { + name: "ServiceResourceSkill", + nameField: "SkillNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceResourceSkill", + nameField: "SkillNumber", + }), + }, + { + name: "ServiceResourceSkillFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceResourceSkillFeed", + nameField: "Id", + }), + }, + { + name: "ServiceResourceSkillHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceResourceSkillHistory", + nameField: "Id", + }), + }, + { + name: "ServiceTerritory", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceTerritory", + nameField: "Name", + }), + }, + { + name: "ServiceTerritoryFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceTerritoryFeed", + nameField: "Id", + }), + }, + { + name: "ServiceTerritoryHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceTerritoryHistory", + nameField: "Id", + }), + }, + { + name: "ServiceTerritoryMember", + nameField: "MemberNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceTerritoryMember", + nameField: "MemberNumber", + }), + }, + { + name: "ServiceTerritoryMemberFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceTerritoryMemberFeed", + nameField: "Id", + }), + }, + { + name: "ServiceTerritoryMemberHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceTerritoryMemberHistory", + nameField: "Id", + }), + }, + { + name: "ServiceTerritoryWorkType", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceTerritoryWorkType", + nameField: "Name", + }), + }, + { + name: "ServiceTerritoryWorkTypeFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceTerritoryWorkTypeFeed", + nameField: "Id", + }), + }, + { + name: "ServiceTerritoryWorkTypeHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ServiceTerritoryWorkTypeHistory", + nameField: "Id", + }), + }, + { + name: "SessionHijackingEventStore", + nameField: "SessionHijackingEventNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "SessionHijackingEventStore", + nameField: "SessionHijackingEventNumber", + }), + }, + { + name: "SessionHijackingEventStoreFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "SessionHijackingEventStoreFeed", + nameField: "Id", + }), + }, + { + name: "SetupAssistantStep", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "SetupAssistantStep", + nameField: "Name", + }), + }, + { + name: "Shift", + nameField: "ShiftNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Shift", + nameField: "ShiftNumber", + }), + }, + { + name: "ShiftFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ShiftFeed", + nameField: "Id", + }), + }, + { + name: "ShiftHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ShiftHistory", + nameField: "Id", + }), + }, + { + name: "Shipment", + nameField: "ShipmentNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Shipment", + nameField: "ShipmentNumber", + }), + }, + { + name: "ShipmentFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ShipmentFeed", + nameField: "Id", + }), + }, + { + name: "ShipmentHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ShipmentHistory", + nameField: "Id", + }), + }, + { + name: "SiteFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "SiteFeed", + nameField: "Id", + }), + }, + { + name: "SiteHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "SiteHistory", + nameField: "Id", + }), + }, + { + name: "SkillRequirement", + nameField: "SkillNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "SkillRequirement", + nameField: "SkillNumber", + }), + }, + { + name: "SkillRequirementFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "SkillRequirementFeed", + nameField: "Id", + }), + }, + { + name: "SkillRequirementHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "SkillRequirementHistory", + nameField: "Id", + }), + }, + { + name: "Solution", + nameField: "SolutionName", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Solution", + nameField: "SolutionName", + }), + }, + { + name: "SolutionFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "SolutionFeed", + nameField: "Id", + }), + }, + { + name: "SolutionHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "SolutionHistory", + nameField: "Id", + }), + }, + { + name: "StaticResource", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "StaticResource", + nameField: "Name", + }), + }, + { + name: "StreamingChannel", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "StreamingChannel", + nameField: "Name", + }), + }, + { + name: "Task", + nameField: "Subject", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Task", + nameField: "Subject", + }), + }, + { + name: "TaskFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "TaskFeed", + nameField: "Id", + }), + }, + { + name: "ThreatDetectionFeedback", + nameField: "ThreatDetectionFeedbackNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ThreatDetectionFeedback", + nameField: "ThreatDetectionFeedbackNumber", + }), + }, + { + name: "ThreatDetectionFeedbackFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "ThreatDetectionFeedbackFeed", + nameField: "Id", + }), + }, + { + name: "TimeSlot", + nameField: "TimeSlotNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "TimeSlot", + nameField: "TimeSlotNumber", + }), + }, + { + name: "TodayGoal", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "TodayGoal", + nameField: "Name", + }), + }, + { + name: "Topic", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Topic", + nameField: "Name", + }), + }, + { + name: "TopicAssignment", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "TopicAssignment", + nameField: "Id", + }), + }, + { + name: "TopicFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "TopicFeed", + nameField: "Id", + }), + }, + { + name: "User", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "User", + nameField: "Name", + }), + }, + { + name: "UserAppInfo", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "UserAppInfo", + nameField: "Id", + }), + }, + { + name: "UserAppMenuCustomization", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "UserAppMenuCustomization", + nameField: "Id", + }), + }, + { + name: "UserEmailPreferredPerson", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "UserEmailPreferredPerson", + nameField: "Name", + }), + }, + { + name: "UserFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "UserFeed", + nameField: "Id", + }), + }, + { + name: "UserProvAccount", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "UserProvAccount", + nameField: "Name", + }), + }, + { + name: "UserProvAccountStaging", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "UserProvAccountStaging", + nameField: "Name", + }), + }, + { + name: "UserProvMockTarget", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "UserProvMockTarget", + nameField: "Name", + }), + }, + { + name: "UserProvisioningLog", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "UserProvisioningLog", + nameField: "Name", + }), + }, + { + name: "UserProvisioningRequest", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "UserProvisioningRequest", + nameField: "Name", + }), + }, + { + name: "UserRole", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "UserRole", + nameField: "Name", + }), + }, + { + name: "VoiceCall", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "VoiceCall", + nameField: "Id", + }), + }, + { + name: "VoiceCallFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "VoiceCallFeed", + nameField: "Id", + }), + }, + { + name: "VoiceCallRecording", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "VoiceCallRecording", + nameField: "Name", + }), + }, + { + name: "Vote", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "Vote", + nameField: "Id", + }), + }, + { + name: "WaveAutoInstallRequest", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WaveAutoInstallRequest", + nameField: "Name", + }), + }, + { + name: "WaveCompatibilityCheckItem", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WaveCompatibilityCheckItem", + nameField: "Name", + }), + }, + { + name: "WebCart", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WebCart", + nameField: "Name", + }), + }, + { + name: "WebCartHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WebCartHistory", + nameField: "Id", + }), + }, + { + name: "WebLink", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WebLink", + nameField: "Name", + }), + }, + { + name: "WebStore", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WebStore", + nameField: "Name", + }), + }, + { + name: "WebStoreBuyerGroup", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WebStoreBuyerGroup", + nameField: "Name", + }), + }, + { + name: "WebStoreCatalog", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WebStoreCatalog", + nameField: "Name", + }), + }, + { + name: "WebStoreCatalogHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WebStoreCatalogHistory", + nameField: "Id", + }), + }, + { + name: "WorkOrder", + nameField: "WorkOrderNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkOrder", + nameField: "WorkOrderNumber", + }), + }, + { + name: "WorkOrderFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkOrderFeed", + nameField: "Id", + }), + }, + { + name: "WorkOrderHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkOrderHistory", + nameField: "Id", + }), + }, + { + name: "WorkOrderLineItem", + nameField: "LineItemNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkOrderLineItem", + nameField: "LineItemNumber", + }), + }, + { + name: "WorkOrderLineItemFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkOrderLineItemFeed", + nameField: "Id", + }), + }, + { + name: "WorkOrderLineItemHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkOrderLineItemHistory", + nameField: "Id", + }), + }, + { + name: "WorkPlan", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkPlan", + nameField: "Name", + }), + }, + { + name: "WorkPlanFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkPlanFeed", + nameField: "Id", + }), + }, + { + name: "WorkPlanHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkPlanHistory", + nameField: "Id", + }), + }, + { + name: "WorkPlanTemplate", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkPlanTemplate", + nameField: "Name", + }), + }, + { + name: "WorkPlanTemplateEntry", + nameField: "WorkPlanTemplateEntryNumber", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkPlanTemplateEntry", + nameField: "WorkPlanTemplateEntryNumber", + }), + }, + { + name: "WorkStep", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkStep", + nameField: "Name", + }), + }, + { + name: "WorkStepTemplate", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkStepTemplate", + nameField: "Name", + }), + }, + { + name: "WorkType", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkType", + nameField: "Name", + }), + }, + { + name: "WorkTypeFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkTypeFeed", + nameField: "Id", + }), + }, + { + name: "WorkTypeGroup", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkTypeGroup", + nameField: "Name", + }), + }, + { + name: "WorkTypeGroupFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkTypeGroupFeed", + nameField: "Id", + }), + }, + { + name: "WorkTypeGroupHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkTypeGroupHistory", + nameField: "Id", + }), + }, + { + name: "WorkTypeGroupMember", + nameField: "Name", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkTypeGroupMember", + nameField: "Name", + }), + }, + { + name: "WorkTypeGroupMemberFeed", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkTypeGroupMemberFeed", + nameField: "Id", + }), + }, + { + name: "WorkTypeGroupMemberHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkTypeGroupMemberHistory", + nameField: "Id", + }), + }, + { + name: "WorkTypeHistory", + nameField: "Id", + getRecords: () => + this.salesforce.listRecordOptions({ + objType: "WorkTypeHistory", + nameField: "Id", + }), + }, +]; +export default sobjects; diff --git a/components/salesforce_rest_api/common/constants-props.mjs b/components/salesforce_rest_api/common/constants-props.mjs new file mode 100644 index 0000000000000..db777f8190e68 --- /dev/null +++ b/components/salesforce_rest_api/common/constants-props.mjs @@ -0,0 +1,1539 @@ +export const WEEKDAY_MASK_OPTIONS = [ + { + label: "Sunday", + value: 1, + }, + { + label: "Monday", + value: 2, + }, + { + label: "Tuesday", + value: 4, + }, + { + label: "Wednesday", + value: 8, + }, + { + label: "Thursday", + value: 16, + }, + { + label: "Friday", + value: 32, + }, + { + label: "Saturday", + value: 64, + }, +]; + +export const RECURRENCE_INSTANCE_OPTIONS = [ + { + label: "1st", + value: "First", + }, + { + label: "2nd", + value: "Second", + }, + { + label: "3rd", + value: "Third", + }, + { + label: "4th", + value: "Fourth", + }, + { + label: "last", + value: "Last", + }, +]; + +export const RECURRENCE_MONTH_OPTIONS = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + +export const TIMEZONE_OPTIONS = [ + { + label: "(GMT+14:00) Line Islands Time (Pacific/Kiritimati)", + value: "Pacific/Kiritimati", + }, + { + label: "(GMT+13:00) Phoenix Islands Time (Pacific/Enderbury)", + value: "Pacific/Enderbury", + }, + { + label: "(GMT+13:00) Tonga Standard Time (Pacific/Tongatapu)", + value: "Pacific/Tongatapu", + }, + { + label: "(GMT+12:45) Chatham Standard Time (Pacific/Chatham)", + value: "Pacific/Chatham", + }, + { + label: + "(GMT+12:00) Petropavlovsk-Kamchatski Standard Time (Asia/Kamchatka)", + value: "Asia/Kamchatka", + }, + { + label: "(GMT+12:00) New Zealand Standard Time (Pacific/Auckland)", + value: "Pacific/Auckland", + }, + { + label: "(GMT+12:00) Fiji Standard Time (Pacific/Fiji)", + value: "Pacific/Fiji", + }, + { + label: "(GMT+11:00) Solomon Islands Time (Pacific/Guadalcanal)", + value: "Pacific/Guadalcanal", + }, + { + label: "(GMT+11:00) Norfolk Island Standard Time (Pacific/Norfolk)", + value: "Pacific/Norfolk", + }, + { + label: "(GMT+10:30) Lord Howe Standard Time (Australia/Lord_Howe)", + value: "Australia/Lord_Howe", + }, + { + label: "(GMT+10:00) Australian Eastern Standard Time (Australia/Brisbane)", + value: "Australia/Brisbane", + }, + { + label: "(GMT+10:00) Australian Eastern Standard Time (Australia/Sydney)", + value: "Australia/Sydney", + }, + { + label: "(GMT+09:30) Australian Central Standard Time (Australia/Adelaide)", + value: "Australia/Adelaide", + }, + { + label: "(GMT+09:30) Australian Central Standard Time (Australia/Darwin)", + value: "Australia/Darwin", + }, + { + label: "(GMT+09:00) Korean Standard Time (Asia/Seoul)", + value: "Asia/Seoul", + }, + { + label: "(GMT+09:00) Japan Standard Time (Asia/Tokyo)", + value: "Asia/Tokyo", + }, + { + label: "(GMT+08:00) Hong Kong Standard Time (Asia/Hong_Kong)", + value: "Asia/Hong_Kong", + }, + { + label: "(GMT+08:00) Malaysia Time (Asia/Kuala_Lumpur)", + value: "Asia/Kuala_Lumpur", + }, + { + label: "(GMT+08:00) Philippine Standard Time (Asia/Manila)", + value: "Asia/Manila", + }, + { + label: "(GMT+08:00) China Standard Time (Asia/Shanghai)", + value: "Asia/Shanghai", + }, + { + label: "(GMT+08:00) Singapore Standard Time (Asia/Singapore)", + value: "Asia/Singapore", + }, + { + label: "(GMT+08:00) Taipei Standard Time (Asia/Taipei)", + value: "Asia/Taipei", + }, + { + label: "(GMT+08:00) Australian Western Standard Time (Australia/Perth)", + value: "Australia/Perth", + }, + { + label: "(GMT+07:00) Indochina Time (Asia/Bangkok)", + value: "Asia/Bangkok", + }, + { + label: "(GMT+07:00) Indochina Time (Asia/Ho_Chi_Minh)", + value: "Asia/Ho_Chi_Minh", + }, + { + label: "(GMT+07:00) Western Indonesia Time (Asia/Jakarta)", + value: "Asia/Jakarta", + }, + { + label: "(GMT+06:30) Myanmar Time (Asia/Rangoon)", + value: "Asia/Rangoon", + }, + { + label: "(GMT+06:00) Bangladesh Standard Time (Asia/Dhaka)", + value: "Asia/Dhaka", + }, + { + label: "(GMT+05:45) Nepal Time (Asia/Kathmandu)", + value: "Asia/Kathmandu", + }, + { + label: "(GMT+05:30) India Standard Time (Asia/Colombo)", + value: "Asia/Colombo", + }, + { + label: "(GMT+05:30) India Standard Time (Asia/Kolkata)", + value: "Asia/Kolkata", + }, + { + label: "(GMT+05:00) Pakistan Standard Time (Asia/Karachi)", + value: "Asia/Karachi", + }, + { + label: "(GMT+05:00) Uzbekistan Standard Time (Asia/Tashkent)", + value: "Asia/Tashkent", + }, + { + label: "(GMT+05:00) Yekaterinburg Standard Time (Asia/Yekaterinburg)", + value: "Asia/Yekaterinburg", + }, + { + label: "(GMT+04:30) Afghanistan Time (Asia/Kabul)", + value: "Asia/Kabul", + }, + { + label: "(GMT+04:00) Azerbaijan Standard Time (Asia/Baku)", + value: "Asia/Baku", + }, + { + label: "(GMT+04:00) Gulf Standard Time (Asia/Dubai)", + value: "Asia/Dubai", + }, + { + label: "(GMT+04:00) Georgia Standard Time (Asia/Tbilisi)", + value: "Asia/Tbilisi", + }, + { + label: "(GMT+04:00) Armenia Standard Time (Asia/Yerevan)", + value: "Asia/Yerevan", + }, + { + label: "(GMT+03:00) Eastern European Standard Time (Africa/Cairo)", + value: "Africa/Cairo", + }, + { + label: "(GMT+03:00) East Africa Time (Africa/Nairobi)", + value: "Africa/Nairobi", + }, + { + label: "(GMT+03:00) Arabian Standard Time (Asia/Baghdad)", + value: "Asia/Baghdad", + }, + { + label: "(GMT+03:00) Eastern European Summer Time (Asia/Beirut)", + value: "Asia/Beirut", + }, + { + label: "(GMT+03:00) Israel Daylight Time (Asia/Jerusalem)", + value: "Asia/Jerusalem", + }, + { + label: "(GMT+03:00) Arabian Standard Time (Asia/Kuwait)", + value: "Asia/Kuwait", + }, + { + label: "(GMT+03:00) Arabian Standard Time (Asia/Riyadh)", + value: "Asia/Riyadh", + }, + { + label: "(GMT+03:00) Eastern European Summer Time (Europe/Athens)", + value: "Europe/Athens", + }, + { + label: "(GMT+03:00) Eastern European Summer Time (Europe/Bucharest)", + value: "Europe/Bucharest", + }, + { + label: "(GMT+03:00) Eastern European Summer Time (Europe/Helsinki)", + value: "Europe/Helsinki", + }, + { + label: "(GMT+03:00) Eastern European Standard Time (Europe/Istanbul)", + value: "Europe/Istanbul", + }, + { + label: "(GMT+03:00) Moscow Standard Time (Europe/Minsk)", + value: "Europe/Minsk", + }, + { + label: "(GMT+03:00) Moscow Standard Time (Europe/Moscow)", + value: "Europe/Moscow", + }, + { + label: "(GMT+02:00) South Africa Standard Time (Africa/Johannesburg)", + value: "Africa/Johannesburg", + }, + { + label: "(GMT+02:00) Central European Summer Time (Europe/Amsterdam)", + value: "Europe/Amsterdam", + }, + { + label: "(GMT+02:00) Central European Summer Time (Europe/Berlin)", + value: "Europe/Berlin", + }, + { + label: "(GMT+02:00) Central European Summer Time (Europe/Brussels)", + value: "Europe/Brussels", + }, + { + label: "(GMT+02:00) Central European Summer Time (Europe/Paris)", + value: "Europe/Paris", + }, + { + label: "(GMT+02:00) Central European Summer Time (Europe/Prague)", + value: "Europe/Prague", + }, + { + label: "(GMT+02:00) Central European Summer Time (Europe/Rome)", + value: "Europe/Rome", + }, + { + label: "(GMT+01:00) Central European Standard Time (Africa/Algiers)", + value: "Africa/Algiers", + }, + { + label: "(GMT+01:00) Western European Summer Time (Africa/Casablanca)", + value: "Africa/Casablanca", + }, + { + label: "(GMT+01:00) Irish Standard Time (Europe/Dublin)", + value: "Europe/Dublin", + }, + { + label: "(GMT+01:00) Western European Summer Time (Europe/Lisbon)", + value: "Europe/Lisbon", + }, + { + label: "(GMT+01:00) British Summer Time (Europe/London)", + value: "Europe/London", + }, + { + label: "(GMT+00:00) Azores Summer Time (Atlantic/Azores)", + value: "Atlantic/Azores", + }, + { + label: "(GMT+00:00) Greenwich Mean Time (GMT)", + value: "GMT", + }, + { + label: "(GMT-01:00) East Greenland Summer Time (America/Scoresbysund)", + value: "America/Scoresbysund", + }, + { + label: "(GMT-01:00) Cape Verde Standard Time (Atlantic/Cape_Verde)", + value: "Atlantic/Cape_Verde", + }, + { + label: "(GMT-02:00) South Georgia Time (Atlantic/South_Georgia)", + value: "Atlantic/South_Georgia", + }, + { + label: "(GMT-02:30) Newfoundland Daylight Time (America/St_Johns)", + value: "America/St_Johns", + }, + { + label: + "(GMT-03:00) Argentina Standard Time (America/Argentina/Buenos_Aires)", + value: "America/Argentina/Buenos_Aires", + }, + { + label: "(GMT-03:00) Atlantic Daylight Time (America/Halifax)", + value: "America/Halifax", + }, + { + label: "(GMT-03:00) Brasilia Standard Time (America/Sao_Paulo)", + value: "America/Sao_Paulo", + }, + { + label: "(GMT-03:00) Atlantic Daylight Time (Atlantic/Bermuda)", + value: "Atlantic/Bermuda", + }, + { + label: "(GMT-04:00) Venezuela Time (America/Caracas)", + value: "America/Caracas", + }, + { + label: "(GMT-04:00) Eastern Daylight Time (America/Indiana/Indianapolis)", + value: "America/Indiana/Indianapolis", + }, + { + label: "(GMT-04:00) Eastern Daylight Time (America/New_York)", + value: "America/New_York", + }, + { + label: "(GMT-04:00) Atlantic Standard Time (America/Puerto_Rico)", + value: "America/Puerto_Rico", + }, + { + label: "(GMT-04:00) Chile Standard Time (America/Santiago)", + value: "America/Santiago", + }, + { + label: "(GMT-05:00) Colombia Standard Time (America/Bogota)", + value: "America/Bogota", + }, + { + label: "(GMT-05:00) Central Daylight Time (America/Chicago)", + value: "America/Chicago", + }, + { + label: "(GMT-05:00) Peru Standard Time (America/Lima)", + value: "America/Lima", + }, + { + label: "(GMT-05:00) Eastern Standard Time (America/Panama)", + value: "America/Panama", + }, + { + label: "(GMT-06:00) Mountain Daylight Time (America/Denver)", + value: "America/Denver", + }, + { + label: "(GMT-06:00) Central Standard Time (America/El_Salvador)", + value: "America/El_Salvador", + }, + { + label: "(GMT-06:00) Central Standard Time (America/Mexico_City)", + value: "America/Mexico_City", + }, + { + label: "(GMT-07:00) Pacific Daylight Time (America/Los_Angeles)", + value: "America/Los_Angeles", + }, + { + label: "(GMT-07:00) Mexican Pacific Standard Time (America/Mazatlan)", + value: "America/Mazatlan", + }, + { + label: "(GMT-07:00) Mountain Standard Time (America/Phoenix)", + value: "America/Phoenix", + }, + { + label: "(GMT-07:00) Pacific Daylight Time (America/Tijuana)", + value: "America/Tijuana", + }, + { + label: "(GMT-08:00) Alaska Daylight Time (America/Anchorage)", + value: "America/Anchorage", + }, + { + label: "(GMT-08:00) Pitcairn Time (Pacific/Pitcairn)", + value: "Pacific/Pitcairn", + }, + { + label: "(GMT-09:00) Hawaii-Aleutian Daylight Time (America/Adak)", + value: "America/Adak", + }, + { + label: "(GMT-09:00) Gambier Time (Pacific/Gambier)", + value: "Pacific/Gambier", + }, + { + label: "(GMT-09:30) Marquesas Time (Pacific/Marquesas)", + value: "Pacific/Marquesas", + }, + { + label: "(GMT-10:00) Hawaii-Aleutian Standard Time (Pacific/Honolulu)", + value: "Pacific/Honolulu", + }, + { + label: "(GMT-11:00) Niue Time (Pacific/Niue)", + value: "Pacific/Niue", + }, + { + label: "(GMT-11:00) Samoa Standard Time (Pacific/Pago_Pago)", + value: "Pacific/Pago_Pago", + }, +]; + +export const RECURRENCE_TYPE_OPTIONS = [ + { + label: "Recurs Daily", + value: "RecursDaily", + }, + { + label: "Recurs Every Weekday", + value: "RecursEveryWeekday", + }, + { + label: "Recurs Monthly", + value: "RecursMonthly", + }, + { + label: "Recurs Monthly Nth", + value: "RecursMonthlyNth", + }, + { + label: "Recurs Weekly", + value: "RecursWeekly", + }, + { + label: "Recurs Yearly", + value: "RecursYearly", + }, + { + label: "Recurs Yearly Nth", + value: "RecursYearlyNth", + }, +]; + +export const GEOCODE_ACCURACY_OPTIONS = [ + { + label: "Address", + value: "Address", + }, + { + label: "Near Address", + value: "NearAddress", + }, + { + label: "Block", + value: "Block", + }, + { + label: "Street", + value: "Street", + }, + { + label: "Extended Zip", + value: "ExtendedZip", + }, + { + label: "Zip", + value: "Zip", + }, + { + label: "Neighborhood", + value: "Neighborhood", + }, + { + label: "City", + value: "City", + }, + { + label: "County", + value: "County", + }, + { + label: "State", + value: "State", + }, + { + label: "Unknown", + value: "Unknown", + }, +]; + +export const CLEAN_STATUS_OPTIONS = [ + { + label: "In Sync", + value: "Matched", + }, + { + label: "Different", + value: "Different", + }, + { + label: "Reviewed", + value: "Acknowledged", + }, + { + label: "Not Found", + value: "NotFound", + }, + { + label: "Inactive", + value: "Inactive", + }, + { + label: "Not Compared", + value: "Pending", + }, + { + label: "Select Match", + value: "SelectMatch", + }, + { + label: "Skipped", + value: "Skipped", + }, +]; + +export const RECORD_SOURCE_OPTIONS = [ + "Web", + "Phone Inquiry", + "Partner Referral", + "Purchased List", + "Other", +]; + +export const EMAIL_ENCODING_OPTIONS = [ + { + label: "Unicode (UTF-8)", + value: "UTF-8", + }, + { + label: "General US & Western Europe (ISO-8859-1, ISO-LATIN-1)", + value: "ISO-8859-1", + }, + { + label: "Japanese (Shift-JIS)", + value: "Shift_JIS", + }, + { + label: "Japanese (JIS)", + value: "ISO-2022-JP", + }, + { + label: "Japanese (EUC)", + value: "EUC-JP", + }, + { + label: "Korean (ks_c_5601-1987)", + value: "ks_c_5601-1987", + }, + { + label: "Traditional Chinese (Big5)", + value: "Big5", + }, + { + label: "Simplified Chinese (GB2312)", + value: "GB2312", + }, + { + label: "Traditional Chinese Hong Kong (Big5-HKSCS)", + value: "Big5-HKSCS", + }, + { + label: "Japanese (Shift-JIS_2004)", + value: "x-SJIS_0213", + }, +]; + +export const LANGUAGE_OPTIONS = [ + { + label: "English", + value: "en_US", + }, + { + label: "German", + value: "de", + }, + { + label: "Spanish", + value: "es", + }, + { + label: "French", + value: "fr", + }, + { + label: "Italian", + value: "it", + }, + { + label: "Japanese", + value: "ja", + }, + { + label: "Swedish", + value: "sv", + }, + { + label: "Korean", + value: "ko", + }, + { + label: "Chinese (Traditional)", + value: "zh_TW", + }, + { + label: "Chinese (Simplified)", + value: "zh_CN", + }, + { + label: "Portuguese (Brazil)", + value: "pt_BR", + }, + { + label: "Dutch", + value: "nl_NL", + }, + { + label: "Danish", + value: "da", + }, + { + label: "Thai", + value: "th", + }, + { + label: "Finnish", + value: "fi", + }, + { + label: "Russian", + value: "ru", + }, + { + label: "Spanish (Mexico)", + value: "es_MX", + }, + { + label: "Norwegian", + value: "no", + }, +]; + +export const LOCALE_OPTIONS = [ + { + label: "Afrikaans (South Africa)", + value: "af_ZA", + }, + { + label: "Albanian (Albania)", + value: "sq_AL", + }, + { + label: "Arabic (Algeria)", + value: "ar_DZ", + }, + { + label: "Arabic (Bahrain)", + value: "ar_BH", + }, + { + label: "Arabic (Egypt)", + value: "ar_EG", + }, + { + label: "Arabic (Iraq)", + value: "ar_IQ", + }, + { + label: "Arabic (Jordan)", + value: "ar_JO", + }, + { + label: "Arabic (Kuwait)", + value: "ar_KW", + }, + { + label: "Arabic (Lebanon)", + value: "ar_LB", + }, + { + label: "Arabic (Libya)", + value: "ar_LY", + }, + { + label: "Arabic (Morocco)", + value: "ar_MA", + }, + { + label: "Arabic (Oman)", + value: "ar_OM", + }, + { + label: "Arabic (Qatar)", + value: "ar_QA", + }, + { + label: "Arabic (Saudi Arabia)", + value: "ar_SA", + }, + { + label: "Arabic (Sudan)", + value: "ar_SD", + }, + { + label: "Arabic (Tunisia)", + value: "ar_TN", + }, + { + label: "Arabic (United Arab Emirates)", + value: "ar_AE", + }, + { + label: "Arabic (Yemen)", + value: "ar_YE", + }, + { + label: "Armenian (Armenia)", + value: "hy_AM", + }, + { + label: "Azerbaijani (Azerbaijan)", + value: "az_AZ", + }, + { + label: "Bangla (Bangladesh)", + value: "bn_BD", + }, + { + label: "Bangla (India)", + value: "bn_IN", + }, + { + label: "Basque (Spain)", + value: "eu_ES", + }, + { + label: "Belarusian (Belarus)", + value: "be_BY", + }, + { + label: "Bosnian (Bosnia & Herzegovina)", + value: "bs_BA", + }, + { + label: "Bulgarian (Bulgaria)", + value: "bg_BG", + }, + { + label: "Burmese (Myanmar [Burma])", + value: "my_MM", + }, + { + label: "Catalan (Spain)", + value: "ca_ES", + }, + { + label: "Chinese (China, Pinyin Ordering)", + value: "zh_CN_PINYIN", + }, + { + label: "Chinese (China, Stroke Ordering)", + value: "zh_CN_STROKE", + }, + { + label: "Chinese (China)", + value: "zh_CN", + }, + { + label: "Chinese (Hong Kong SAR China, Stroke Ordering)", + value: "zh_HK_STROKE", + }, + { + label: "Chinese (Hong Kong SAR China)", + value: "zh_HK", + }, + { + label: "Chinese (Macao SAR China)", + value: "zh_MO", + }, + { + label: "Chinese (Singapore)", + value: "zh_SG", + }, + { + label: "Chinese (Taiwan, Stroke Ordering)", + value: "zh_TW_STROKE", + }, + { + label: "Chinese (Taiwan)", + value: "zh_TW", + }, + { + label: "Croatian (Croatia)", + value: "hr_HR", + }, + { + label: "Czech (Czechia)", + value: "cs_CZ", + }, + { + label: "Danish (Denmark)", + value: "da_DK", + }, + { + label: "Dutch (Aruba)", + value: "nl_AW", + }, + { + label: "Dutch (Belgium)", + value: "nl_BE", + }, + { + label: "Dutch (Netherlands)", + value: "nl_NL", + }, + { + label: "Dutch (Suriname)", + value: "nl_SR", + }, + { + label: "Dzongkha (Bhutan)", + value: "dz_BT", + }, + { + label: "English (Antigua & Barbuda)", + value: "en_AG", + }, + { + label: "English (Australia)", + value: "en_AU", + }, + { + label: "English (Bahamas)", + value: "en_BS", + }, + { + label: "English (Barbados)", + value: "en_BB", + }, + { + label: "English (Belize)", + value: "en_BZ", + }, + { + label: "English (Bermuda)", + value: "en_BM", + }, + { + label: "English (Botswana)", + value: "en_BW", + }, + { + label: "English (Cameroon)", + value: "en_CM", + }, + { + label: "English (Canada)", + value: "en_CA", + }, + { + label: "English (Cayman Islands)", + value: "en_KY", + }, + { + label: "English (Eritrea)", + value: "en_ER", + }, + { + label: "English (Eswatini)", + value: "en_SZ", + }, + { + label: "English (Falkland Islands)", + value: "en_FK", + }, + { + label: "English (Fiji)", + value: "en_FJ", + }, + { + label: "English (Gambia)", + value: "en_GM", + }, + { + label: "English (Ghana)", + value: "en_GH", + }, + { + label: "English (Gibraltar)", + value: "en_GI", + }, + { + label: "English (Guyana)", + value: "en_GY", + }, + { + label: "English (Hong Kong SAR China)", + value: "en_HK", + }, + { + label: "English (India)", + value: "en_IN", + }, + { + label: "English (Indonesia)", + value: "en_ID", + }, + { + label: "English (Ireland)", + value: "en_IE", + }, + { + label: "English (Jamaica)", + value: "en_JM", + }, + { + label: "English (Kenya)", + value: "en_KE", + }, + { + label: "English (Liberia)", + value: "en_LR", + }, + { + label: "English (Madagascar)", + value: "en_MG", + }, + { + label: "English (Malawi)", + value: "en_MW", + }, + { + label: "English (Malaysia)", + value: "en_MY", + }, + { + label: "English (Mauritius)", + value: "en_MU", + }, + { + label: "English (Namibia)", + value: "en_NA", + }, + { + label: "English (New Zealand)", + value: "en_NZ", + }, + { + label: "English (Nigeria)", + value: "en_NG", + }, + { + label: "English (Pakistan)", + value: "en_PK", + }, + { + label: "English (Papua New Guinea)", + value: "en_PG", + }, + { + label: "English (Philippines)", + value: "en_PH", + }, + { + label: "English (Rwanda)", + value: "en_RW", + }, + { + label: "English (Samoa)", + value: "en_WS", + }, + { + label: "English (Seychelles)", + value: "en_SC", + }, + { + label: "English (Sierra Leone)", + value: "en_SL", + }, + { + label: "English (Singapore)", + value: "en_SG", + }, + { + label: "English (Sint Maarten)", + value: "en_SX", + }, + { + label: "English (Solomon Islands)", + value: "en_SB", + }, + { + label: "English (South Africa)", + value: "en_ZA", + }, + { + label: "English (St. Helena)", + value: "en_SH", + }, + { + label: "English (Tanzania)", + value: "en_TZ", + }, + { + label: "English (Tonga)", + value: "en_TO", + }, + { + label: "English (Trinidad & Tobago)", + value: "en_TT", + }, + { + label: "English (Uganda)", + value: "en_UG", + }, + { + label: "English (United Kingdom)", + value: "en_GB", + }, + { + label: "English (United States)", + value: "en_US", + }, + { + label: "English (Vanuatu)", + value: "en_VU", + }, + { + label: "Estonian (Estonia)", + value: "et_EE", + }, + { + label: "Finnish (Finland)", + value: "fi_FI", + }, + { + label: "French (Belgium)", + value: "fr_BE", + }, + { + label: "French (Canada)", + value: "fr_CA", + }, + { + label: "French (Comoros)", + value: "fr_KM", + }, + { + label: "French (France)", + value: "fr_FR", + }, + { + label: "French (Guinea)", + value: "fr_GN", + }, + { + label: "French (Haiti)", + value: "fr_HT", + }, + { + label: "French (Luxembourg)", + value: "fr_LU", + }, + { + label: "French (Mauritania)", + value: "fr_MR", + }, + { + label: "French (Monaco)", + value: "fr_MC", + }, + { + label: "French (Switzerland)", + value: "fr_CH", + }, + { + label: "French (Wallis & Futuna)", + value: "fr_WF", + }, + { + label: "Georgian (Georgia)", + value: "ka_GE", + }, + { + label: "German (Austria)", + value: "de_AT", + }, + { + label: "German (Belgium)", + value: "de_BE", + }, + { + label: "German (Germany)", + value: "de_DE", + }, + { + label: "German (Luxembourg)", + value: "de_LU", + }, + { + label: "German (Switzerland)", + value: "de_CH", + }, + { + label: "Greek (Greece)", + value: "el_GR", + }, + { + label: "Gujarati (India)", + value: "gu_IN", + }, + { + label: "Hebrew (Israel)", + value: "iw_IL", + }, + { + label: "Hindi (India)", + value: "hi_IN", + }, + { + label: "Hungarian (Hungary)", + value: "hu_HU", + }, + { + label: "Icelandic (Iceland)", + value: "is_IS", + }, + { + label: "Indonesian (Indonesia)", + value: "in_ID", + }, + { + label: "Irish (Ireland)", + value: "ga_IE", + }, + { + label: "Italian (Italy)", + value: "it_IT", + }, + { + label: "Italian (Switzerland)", + value: "it_CH", + }, + { + label: "Japanese (Japan)", + value: "ja_JP", + }, + { + label: "Kannada (India)", + value: "kn_IN", + }, + { + label: "Kazakh (Kazakhstan)", + value: "kk_KZ", + }, + { + label: "Khmer (Cambodia)", + value: "km_KH", + }, + { + label: "Korean (South Korea)", + value: "ko_KR", + }, + { + label: "Kyrgyz (Kyrgyzstan)", + value: "ky_KG", + }, + { + label: "Lao (Laos)", + value: "lo_LA", + }, + { + label: "Latvian (Latvia)", + value: "lv_LV", + }, + { + label: "Lithuanian (Lithuania)", + value: "lt_LT", + }, + { + label: "Luba-Katanga (Congo - Kinshasa)", + value: "lu_CD", + }, + { + label: "Luxembourgish (Luxembourg)", + value: "lb_LU", + }, + { + label: "Macedonian (North Macedonia)", + value: "mk_MK", + }, + { + label: "Malay (Brunei)", + value: "ms_BN", + }, + { + label: "Malay (Malaysia)", + value: "ms_MY", + }, + { + label: "Malayalam (India)", + value: "ml_IN", + }, + { + label: "Maltese (Malta)", + value: "mt_MT", + }, + { + label: "Marathi (India)", + value: "mr_IN", + }, + { + label: "Montenegrin (Montenegro, USD)", + value: "sh_ME_USD", + }, + { + label: "Montenegrin (Montenegro)", + value: "sh_ME", + }, + { + label: "Nepali (Nepal)", + value: "ne_NP", + }, + { + label: "Norwegian (Norway)", + value: "no_NO", + }, + { + label: "Pashto (Afghanistan)", + value: "ps_AF", + }, + { + label: "Polish (Poland)", + value: "pl_PL", + }, + { + label: "Portuguese (Angola)", + value: "pt_AO", + }, + { + label: "Portuguese (Brazil)", + value: "pt_BR", + }, + { + label: "Portuguese (Cape Verde)", + value: "pt_CV", + }, + { + label: "Portuguese (Mozambique)", + value: "pt_MZ", + }, + { + label: "Portuguese (Portugal)", + value: "pt_PT", + }, + { + label: "Portuguese (São Tomé & Príncipe)", + value: "pt_ST", + }, + { + label: "Romanian (Moldova)", + value: "ro_MD", + }, + { + label: "Romanian (Romania)", + value: "ro_RO", + }, + { + label: "Romansh (Switzerland)", + value: "rm_CH", + }, + { + label: "Rundi (Burundi)", + value: "rn_BI", + }, + { + label: "Russian (Kazakhstan)", + value: "ru_KZ", + }, + { + label: "Russian (Russia)", + value: "ru_RU", + }, + { + label: "Serbian (Cyrillic) (Bosnia and Herzegovina)", + value: "sr_BA", + }, + { + label: "Serbian (Cyrillic) (Serbia)", + value: "sr_CS", + }, + { + label: "Serbian (Latin) (Bosnia and Herzegovina)", + value: "sh_BA", + }, + { + label: "Serbian (Latin) (Serbia)", + value: "sh_CS", + }, + { + label: "Serbian (Serbia)", + value: "sr_RS", + }, + { + label: "Slovak (Slovakia)", + value: "sk_SK", + }, + { + label: "Slovenian (Slovenia)", + value: "sl_SI", + }, + { + label: "Somali (Djibouti)", + value: "so_DJ", + }, + { + label: "Somali (Somalia)", + value: "so_SO", + }, + { + label: "Spanish (Argentina)", + value: "es_AR", + }, + { + label: "Spanish (Bolivia)", + value: "es_BO", + }, + { + label: "Spanish (Chile)", + value: "es_CL", + }, + { + label: "Spanish (Colombia)", + value: "es_CO", + }, + { + label: "Spanish (Costa Rica)", + value: "es_CR", + }, + { + label: "Spanish (Dominican Republic)", + value: "es_DO", + }, + { + label: "Spanish (Ecuador)", + value: "es_EC", + }, + { + label: "Spanish (El Salvador)", + value: "es_SV", + }, + { + label: "Spanish (Guatemala)", + value: "es_GT", + }, + { + label: "Spanish (Honduras)", + value: "es_HN", + }, + { + label: "Spanish (Mexico)", + value: "es_MX", + }, + { + label: "Spanish (Nicaragua)", + value: "es_NI", + }, + { + label: "Spanish (Panama)", + value: "es_PA", + }, + { + label: "Spanish (Paraguay)", + value: "es_PY", + }, + { + label: "Spanish (Peru)", + value: "es_PE", + }, + { + label: "Spanish (Puerto Rico)", + value: "es_PR", + }, + { + label: "Spanish (Spain)", + value: "es_ES", + }, + { + label: "Spanish (United States)", + value: "es_US", + }, + { + label: "Spanish (Uruguay)", + value: "es_UY", + }, + { + label: "Spanish (Venezuela)", + value: "es_VE", + }, + { + label: "Swahili (Kenya)", + value: "sw_KE", + }, + { + label: "Swedish (Sweden)", + value: "sv_SE", + }, + { + label: "Tagalog (Philippines)", + value: "tl_PH", + }, + { + label: "Tajik (Tajikistan)", + value: "tg_TJ", + }, + { + label: "Tamil (India)", + value: "ta_IN", + }, + { + label: "Tamil (Sri Lanka)", + value: "ta_LK", + }, + { + label: "Telugu (India)", + value: "te_IN", + }, + { + label: "Te reo (New Zealand)", + value: "mi_NZ", + }, + { + label: "Thai (Thailand)", + value: "th_TH", + }, + { + label: "Tigrinya (Ethiopia)", + value: "ti_ET", + }, + { + label: "Turkish (Türkiye)", + value: "tr_TR", + }, + { + label: "Ukrainian (Ukraine)", + value: "uk_UA", + }, + { + label: "Urdu (Pakistan)", + value: "ur_PK", + }, + { + label: "Uzbek (Latin, Uzbekistan)", + value: "uz_LATN_UZ", + }, + { + label: "Vietnamese (Vietnam)", + value: "vi_VN", + }, + { + label: "Welsh (United Kingdom)", + value: "cy_GB", + }, + { + label: "Xhosa (South Africa)", + value: "xh_ZA", + }, + { + label: "Yoruba (Benin)", + value: "yo_BJ", + }, + { + label: "Zulu (South Africa)", + value: "zu_ZA", + }, +]; diff --git a/components/salesforce_rest_api/common/props-async-options.mjs b/components/salesforce_rest_api/common/props-async-options.mjs new file mode 100644 index 0000000000000..3438a96a6d508 --- /dev/null +++ b/components/salesforce_rest_api/common/props-async-options.mjs @@ -0,0 +1,154 @@ +// Note: the arrow function syntax is required when calling from within additionalProps, +// whereas when using regular props, the standard method syntax is needed instead. +// These props are more commonly used in additionalProps, so all are defined as such, +// and the options method needs to be redefined if used in regular props. + +import allSobjects from "./all-sobjects.mjs"; + +const findSobjectOptions = (objName) => { + return allSobjects.find(({ name }) => name === objName)?.getRecords; +}; + +export default { + AccountId: { + type: "string", + label: "Account ID", + description: "The ID of an Account.", + options: findSobjectOptions("Account"), + }, + BusinessHoursId: { + type: "string", + label: "Business Hours ID", + description: "The ID of a Business Hours record.", + options: findSobjectOptions("BusinessHours"), + }, + CallCenterId: { + type: "string", + label: "Call Center ID", + description: "The ID of a Call Center.", + options: findSobjectOptions("CallCenter"), + }, + CampaignId: { + type: "string", + label: "Campaign ID", + description: "The ID of a Campaign.", + options: findSobjectOptions("Campaign"), + }, + CaseId: { + type: "string", + label: "Case ID", + description: "The ID of a Case.", + options: findSobjectOptions("Case"), + }, + CommunityId: { + type: "string", + label: "Community ID", + description: "The ID of a Community (Zone) record.", + options: () => + this.salesforce.listRecordOptions({ + objType: "Community", + nameField: "Name", + }), + }, + ContactId: { + type: "string", + label: "Contact ID", + description: "The ID of a Contact.", + options: findSobjectOptions("Contact"), + }, + ContractId: { + type: "string", + label: "Contract ID", + description: "The ID of a Contract.", + options: findSobjectOptions("Contract"), + }, + ContactOrLeadIds: { + type: "string[]", + label: "Contact or Lead IDs", + description: "The IDs of Contacts or Leads.", + options: async () => { + const contacts = await this.salesforce.listRecordOptions({ + objType: "Contact", + nameField: "Name", + }); + const leads = await this.salesforce.listRecordOptions({ + objType: "Lead", + nameField: "Name", + }); + return [ + ...(contacts ?? []), + ...(leads ?? []), + ]; + }, + }, + IndividualId: { + type: "string", + label: "Individual ID", + description: "The ID of an Individual.", + options: findSobjectOptions("Individual"), + }, + LeadId: { + type: "string", + label: "Lead ID", + description: "The ID of a Lead.", + options: findSobjectOptions("Lead"), + }, + OperatingHoursId: { + type: "string", + label: "Operating Hours ID", + description: "The ID of an Operating Hours record.", + options: findSobjectOptions("OperatingHours"), + }, + OpportunityId: { + type: "string", + label: "Opportunity ID", + description: "The ID of an Opportunity.", + options: findSobjectOptions("Opportunity"), + }, + Pricebook2Id: { + type: "string", + label: "Pricebook2 ID", + description: "The ID of a Pricebook2 record.", + options: findSobjectOptions("Pricebook2"), + }, + ProfileId: { + type: "string", + label: "Profile ID", + description: "The ID of a Profile.", + options: findSobjectOptions("Profile"), + }, + ServiceContractId: { + type: "string", + label: "ServiceContract ID", + description: "The ID of a Service Contract record.", + options: findSobjectOptions("ServiceContract"), + }, + UserId: { + type: "string", + label: "User ID", + description: "The ID of a User in your organization.", + options: findSobjectOptions("User"), + }, + UserRoleId: { + type: "string", + label: "User Role ID", + description: "The ID of a User Role record.", + options: findSobjectOptions("UserRole"), + }, + QuestionId: { + type: "string", + label: "Question ID", + description: "The ID of a Question.", + options: () => + this.salesforce.listRecordOptions({ + objType: "Question", + nameField: "Title", + }), + }, + RecordTypeId: { + type: "string", + label: "Record Type ID", + description: "ID of the record type assigned to this record.", + options: findSobjectOptions("RecordType"), + }, +}; diff --git a/components/salesforce_rest_api/common/props-utils.mjs b/components/salesforce_rest_api/common/props-utils.mjs index 49c6f718d3c66..731bfcc477344 100644 --- a/components/salesforce_rest_api/common/props-utils.mjs +++ b/components/salesforce_rest_api/common/props-utils.mjs @@ -1,38 +1,95 @@ -function toCapitalCase(str) { - return str - .split(" ") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(""); -} +import allSobjects from "./all-sobjects.mjs"; -function filterProps(props) { - if (!props) { - return; - } +export function getAdditionalFields() { return Object.fromEntries( - Object.entries(props) - .filter(([ - key, - value, - ]) => typeof (value) !== "function" - && ![ - "app", - "salesforce", - ].includes(key)), - ); -} - -function keysToCapitalCase(data = {}) { - return Object.entries(filterProps(data)) - .reduce((acc, [ + Object.entries(this.additionalFields ?? {}).map(([ key, value, - ]) => ({ - ...acc, - [toCapitalCase(key)]: value, - }), {}); + ]) => { + try { + return [ + key, + JSON.parse(value), + ]; + } catch (err) { + return [ + key, + value, + ]; + } + }), + ); } -export default { - keysToCapitalCase, +export const convertFieldsToProps = (fields) => { + const getFieldPropType = (fieldType) => { + // https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/field_types.htm + switch (fieldType) { + case "boolean": + return "boolean"; + case "int": + return "integer"; + case "multipicklist": + return "string[]"; + default: + return "string"; + } + }; + + return fields + .map((field) => { + const { type } = field; + const prop = { + type: getFieldPropType(type), + label: field.name, + description: `Field type: \`${type}\``, + }; + if ([ + "date", + "datetime", + ].includes(type)) { + prop.description = `This is a \`${type}\` field. [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_valid_date_formats.htm) for the expected format.`; + } else if ( + [ + "picklist", + "multipicklist", + ].includes(type) && + field.picklistValues?.length + ) { + prop.description = `Select ${ + type === "picklist" + ? "a value" + : "one or more values" + } from the list.`; + prop.options = field.picklistValues.map(({ + label, value, + }) => ({ + label, + value, + })); + } else if (type === "reference") { + if (field.referenceTo?.length === 1) { + const objName = field.referenceTo[0]; + prop.description = `The ID of a${ + objName.startsWith("A") + ? "n" + : "" + } \`${objName}\` record.`; + const optionsFn = allSobjects.find( + ({ name }) => name === objName, + )?.getRecords; + if (optionsFn) prop.options = optionsFn; + } else if (field.referenceTo?.length > 1) { + prop.description = `The ID of a record of one of these object types: ${field.referenceTo + .map((s) => `\`${s}\``) + .join(", ")}`; + } + } + + return prop; + }) + .reduce((obj, prop) => { + obj[prop.label] = prop; + return obj; + }, {}); }; diff --git a/components/salesforce_rest_api/common/sobjects/account.mjs b/components/salesforce_rest_api/common/sobjects/account.mjs index 1db5c40635645..efa5d8d1c3c8d 100644 --- a/components/salesforce_rest_api/common/sobjects/account.mjs +++ b/components/salesforce_rest_api/common/sobjects/account.mjs @@ -1,27 +1,354 @@ +import { + CLEAN_STATUS_OPTIONS, GEOCODE_ACCURACY_OPTIONS, RECORD_SOURCE_OPTIONS, +} from "../constants-props.mjs"; +import commonProps from "../props-async-options.mjs"; + export default { - AccountNumber: { - type: "string", - label: "Account Number", - description: "Account number assigned to this account (not the unique, system-generated ID assigned during creation). Maximum size is 40 characters.", + createProps: { + Name: { + type: "string", + label: "Account Name", + description: "Name of the account. Max 255 characters.", + }, }, - AccountSource: { - type: "string", - label: "Account Source", - description: "The source of the account record. For example, Advertisement, Data.com, or Trade Show. The source is selected from a picklist of available values, which are set by an administrator in Salesforce. Each picklist value can have up to 40 characters.", + initialProps: { + AccountNumber: { + type: "string", + label: "Account Number", + description: + "Account number assigned to this account (not the unique, system-generated ID assigned during creation). Max 40 characters.", + optional: true, + }, + Description: { + type: "string", + label: "Description", + description: "Text description of the account. Limited to 32,000 KB.", + optional: true, + }, + Phone: { + type: "string", + label: "Phone", + description: "Phone number for this account. Max 40 characters.", + optional: true, + }, + Website: { + type: "string", + label: "Website", + description: "The website of this account. Max 255 characters.", + optional: true, + }, }, - AnnualRevenue: { - type: "string", - label: "Annual Revenue", - description: "Estimated annual revenue of the account.", - }, - BillingCity: { - type: "string", - label: "Billing City", - description: "Details for the billing address of this account. Maximum size is 40 characters.", - }, - BillingCountry: { - type: "string", - label: "Billing Country", - description: "Details for the billing address of this account. Maximum size is 80 characters.", + extraProps: { + OperatingHoursId: { + ...commonProps.OperatingHoursId, + description: "The operating hours associated with the account.", + optional: true, + }, + OwnerId: { + ...commonProps.UserId, + label: "Owner ID", + description: "The ID of the user who currently owns this account (defaults to the user logged in).", + optional: true, + }, + ParentId: { + ...commonProps.AccountId, + label: "Parent Account ID", + description: "ID of the parent account, if any.", + optional: true, + }, + RecordTypeId: { + ...commonProps.RecordTypeId, + optional: true, + }, + AccountSource: { + type: "string", + label: "Account Source", + description: + "The source of the account record. Available values are set by an administrator.", + optional: true, + options: RECORD_SOURCE_OPTIONS, + }, + AnnualRevenue: { + type: "string", + label: "Annual Revenue", + description: "Estimated annual revenue of the account.", + optional: true, + }, + BillingCity: { + type: "string", + label: "Billing City", + description: "Max 40 characters.", + optional: true, + }, + BillingCountry: { + type: "string", + label: "Billing Country", + description: "Max 80 characters.", + optional: true, + }, + BillingGeocodeAccuracy: { + type: "string", + label: "Billing Geocode Accuracy", + description: "Accuracy level of the geocode for the billing address.", + optional: true, + options: GEOCODE_ACCURACY_OPTIONS, + }, + BillingLatitude: { + type: "string", + label: "Billing Latitude", + description: + "A number between -90 and 90 with up to 15 decimal places. Use with `Billing Longitude` to specify the precise geolocation of a billing address.", + optional: true, + }, + BillingLongitude: { + type: "string", + label: "Billing Longitude", + description: + "A number between -180 and 180 with up to 15 decimal places. Use with `Billing Latitude` to specify the precise geolocation of a billing address.", + optional: true, + }, + BillingPostalCode: { + type: "string", + label: "Billing Zip/Postal Code", + description: "Max 20 characters.", + optional: true, + }, + BillingState: { + type: "string", + label: "Billing State/Province", + description: "Max 80 characters.", + optional: true, + }, + BillingStreet: { + type: "string", + label: "Billing Street", + description: "Street address for the billing address of this account.", + optional: true, + }, + CleanStatus: { + type: "string", + label: "Clean Status", + description: + "Indicates the record's clean status as compared with Data.com.", + optional: true, + options: CLEAN_STATUS_OPTIONS, + }, + DunsNumber: { + type: "string", + label: "D-U-N-S Number", + description: "Unique 9-digit number (Data Universal Numbering System).", + optional: true, + }, + Fax: { + type: "string", + label: "Account Fax", + description: "Fax number for the account.", + optional: true, + }, + HasOptedOutOfEmail: { + type: "boolean", + label: "Email Opt Out", + description: "Indicates whether the contact doesn't want to receive email from Salesforce (true) or does (false)", + optional: true, + }, + Industry: { + type: "string", + label: "Industry", + description: + "An industry associated with this account. Max 40 characters.", + optional: true, + }, + IsPriorityRecord: { + type: "boolean", + label: "Is Priority Record", + description: + "Shows whether the user has marked the account as important.", + optional: true, + }, + NaicsCode: { + type: "string", + label: "NAICS Code", + description: + "6-digit code (North American Industry Classification System)", + optional: true, + }, + NaicsDesc: { + type: "string", + label: "NAICS Description", + description: + "A brief description of an org's line of business, based on its NAICS code. Max 120 characters.", + optional: true, + }, + NumberOfEmployees: { + type: "integer", + label: "Employees", + description: + "Number of employees working at the company represented by this account.", + max: 99999999, + optional: true, + }, + Ownership: { + type: "string", + label: "Ownership", + description: "Ownership type for the account.", + optional: true, + options: [ + "Public", + "Private", + "Subsidiary", + "Other", + ], + }, + PersonIndividualId: { + type: "string", + label: "Person Individual ID", + description: "ID of the data privacy record associated with this person's account.", + optional: true, + }, + Rating: { + type: "string", + label: "Account Rating", + description: "The account's prospect rating.", + optional: true, + options: [ + "Hot", + "Warm", + "Cold", + ], + }, + ShippingCity: { + type: "string", + label: "Shipping City", + description: "Max 40 characters.", + optional: true, + }, + ShippingCountry: { + type: "string", + label: "Shipping Country", + description: "Max 80 characters.", + optional: true, + }, + ShippingGeocodeAccuracy: { + type: "string", + label: "Shipping Geocode Accuracy", + description: "Accuracy level of the geocode for the shipping address.", + optional: true, + options: [ + { + label: "Address", + value: "Address", + }, + { + label: "Near Address", + value: "NearAddress", + }, + { + label: "Block", + value: "Block", + }, + { + label: "Street", + value: "Street", + }, + { + label: "Extended Zip", + value: "ExtendedZip", + }, + { + label: "Zip", + value: "Zip", + }, + ], + }, + ShippingLatitude: { + type: "string", + label: "Shipping Latitude", + description: + "A number between -90 and 90 with up to 15 decimal places. Use with `Shipping Longitude` to specify the precise geolocation of a shipping address.", + optional: true, + }, + ShippingLongitude: { + type: "string", + label: "Shipping Longitude", + description: + "A number between -180 and 180 with up to 15 decimal places. Use with `Shipping Latitude` to specify the precise geolocation of a shipping address.", + optional: true, + }, + ShippingPostalCode: { + type: "string", + label: "Shipping Zip/Postal Code", + description: "Max 20 characters.", + optional: true, + }, + ShippingState: { + type: "string", + label: "Shipping State/Province", + description: "Max 80 characters.", + optional: true, + }, + ShippingStreet: { + type: "string", + label: "Shipping Street", + description: + "The street address of the shipping address for this account. Max 255 characters.", + optional: true, + }, + Sic: { + type: "string", + label: "SIC Code", + description: + "Standard Industrial Classification code of the company's main business categorization, for example, 57340 for Electronics. Max 20 characters.", + optional: true, + }, + SicDesc: { + type: "string", + label: "SIC Description", + description: + "A brief description of an org's line of business, based on its SIC code. Max 80 characters.", + optional: true, + }, + Site: { + type: "string", + label: "Account Site", + description: + "Name of the account's location, for example Headquarters or London. Max 80 characters.", + optional: true, + }, + TickerSymbol: { + type: "string", + label: "Ticker Symbol", + description: + "The stock market symbol for this account. Maximum of 20 characters.", + optional: true, + }, + Tradestyle: { + type: "string", + label: "Tradestyle", + description: + "A name, different from its legal name, that an org may use for conducting business. Similar to “Doing business as” or “DBA”. Max 255 characters.", + optional: true, + }, + Type: { + type: "string", + label: "Account Type", + description: "Type of account.", + optional: true, + options: [ + "Prospect", + "Customer - Direct", + "Customer - Channel", + "Channel Partner / Reseller", + "Installation Partner", + "Technology Partner", + "Other", + ], + }, + YearStarted: { + type: "string", + label: "Year Started", + description: + "The year when an org was legally established. Max 4 characters", + optional: true, + }, }, }; diff --git a/components/salesforce_rest_api/common/sobjects/attachment.mjs b/components/salesforce_rest_api/common/sobjects/attachment.mjs index 1253504928220..fc01ea14726d8 100644 --- a/components/salesforce_rest_api/common/sobjects/attachment.mjs +++ b/components/salesforce_rest_api/common/sobjects/attachment.mjs @@ -1,22 +1,61 @@ +import salesforce from "../../salesforce_rest_api.app.mjs"; + export default { - ContentType: { - type: "string", - label: "Content Type", - description: "The content type of the attachment. If the Don't allow HTML uploads as attachments or document records security setting is enabled for your organization, you cannot upload files with the following file extensions: .htm, .html, .htt, .htx, .mhtm, .mhtml, .shtm, .shtml, .acgi, .svg. When you insert a document or attachment through the API, make sure that this field is set to the appropriate MIME type.", + createProps: { + Name: { + type: "string", + label: "File Name", + description: "Name of the attached file. Max 255 characters.", + }, + filePathOrContent: { + type: "string", + label: "File Path or Content", + description: "The path to a file in the `tmp` folder [(see the documentation)](https://pipedream.com/docs/code/nodejs/working-with-files). Alternatively, you can provide the base64-encoded file data.", + }, + ContentType: { + type: "string", + label: "Content Type", + description: "The content type (MIME type) of the attachment. For example, `image/png`.", + }, + ParentId: { + type: "string", + label: "Parent ID", + description: "ID of the parent object of the attachment. [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_attachment.htm) for supported objects.", + }, }, - Description: { - type: "string", - label: "Description", - description: "Description of the attachment. Maximum size is 500 characters. This field is available in API version 18.0 and later.", + updateProps: { + IsPartnerShared: { + type: "boolean", + label: "Is Shared With Partner", + description: "Whether this record is shared with a connection using Salesforce to Salesforce.", + optional: true, + }, }, - IsPrivate: { - type: "boolean", - label: "Is Private?", - description: "Indicates whether this record is viewable only by the owner and administrators (true) or viewable by all otherwise-allowed users (false). During a create or update call, it is possible to mark an Attachment record as private even if you are not the owner. This can result in a situation in which you can no longer access the record that you just inserted or updated. Label is Private.Attachments on tasks or events can't be marked private.", - }, - OwnerId: { - type: "string", - label: "Owner ID", - description: "ID of the User who owns the attachment. This field was required previous to release 9.0. Beginning with release 9.0, it can be null on create. The owner of an attachment on a task or event must be the same as the owner of the task or event.", + initialProps: { + Description: { + type: "string", + label: "Description", + description: "Description of the attachment. Max 500 characters.", + optional: true, + }, + IsPrivate: { + type: "boolean", + label: "Private", + description: "Whether this record is viewable only by the owner and administrators (true) or viewable by all otherwise-allowed users (false).", + optional: true, + }, + OwnerId: { + propDefinition: [ + salesforce, + "recordId", + () => ({ + objType: "User", + nameField: "Name", + }), + ], + label: "Owner ID", + description: "ID of the user who owns the attachment.", + optional: true, + }, }, }; diff --git a/components/salesforce_rest_api/common/sobjects/campaign.mjs b/components/salesforce_rest_api/common/sobjects/campaign.mjs index 229ae6dc7d6d0..e8c19631317e5 100644 --- a/components/salesforce_rest_api/common/sobjects/campaign.mjs +++ b/components/salesforce_rest_api/common/sobjects/campaign.mjs @@ -1,27 +1,129 @@ +import commonProps from "../props-async-options.mjs"; + export default { - ActualCost: { - type: "string", - label: "Actual Cost", - description: "Amount of money spent to run the campaign.", + initialProps: { + Name: { + type: "string", + label: "Name", + description: "Name of the campaign. Max 80 characters.", + }, + Description: { + type: "string", + label: "Description", + description: + "Description of the campaign. Limit: 32 KB. Only the first 255 characters display in reports.", + optional: true, + }, + Status: { + type: "string", + label: "Status", + description: "Status of the campaign. Max 40 characters.", + optional: true, + options: [ + "Planned", + "In Progress", + "Completed", + "Aborted", + ], + }, + Type: { + type: "string", + label: "Type", + description: "Type of campaign. Max 40 characters.", + optional: true, + options: [ + "Conference", + "Webinar", + "Trade Show", + "Public Relations", + "Partners", + "Referral Program", + "Advertisement", + "Banner Ads", + "Direct Mail", + "Email", + "Telemarketing", + "Other", + ], + }, }, - BudgetedCost: { - type: "string", - label: "Budgeted Cost", - description: "Amount of money budgeted for the campaign.", - }, - CampaignImageId: { - type: "string", - label: "Campaign Image ID", - description: "ID of the campaign image. Available in API version 42.0 and later.", - }, - CampaignMemberRecordTypeId: { - type: "string", - label: "Campaign Member Record Type ID", - description: "The record type ID for CampaignMember records associated with the campaign.", - }, - Description: { - type: "string", - label: "Description", - description: "Description of the campaign. Limit: 32 KB. Only the first 255 characters display in reports.", + extraProps: { + OwnerId: { + ...commonProps.UserId, + label: "Owner ID", + description: "The ID of the user who owns this campaign (defaults to the user logged in).", + optional: true, + }, + ParentCampaign: { + ...commonProps.CampaignId, + label: "Parent Campaign ID", + description: "The campaign above this one in the campaign hierarchy.", + optional: true, + }, + RecordTypeId: { + ...commonProps.RecordTypeId, + optional: true, + }, + StartDate: { + type: "string", + label: "Start Date", + description: "Starting date for the campaign.", + optional: true, + }, + EndDate: { + type: "string", + label: "End Date", + description: "Ending date for the campaign. Responses received after this date are still counted.", + optional: true, + }, + ActualCost: { + type: "string", + label: "Actual Cost", + description: "Amount of money spent to run the campaign.", + optional: true, + }, + BudgetedCost: { + type: "string", + label: "Budgeted Cost", + description: "Amount of money budgeted for the campaign.", + optional: true, + }, + CampaignImageId: { + type: "string", + label: "Campaign Image ID", + description: "ID of the campaign image.", + optional: true, + }, + CurrencyIsoCode: { + type: "string", + label: "Currency ISO Code", + description: + "Available only for organizations with the multicurrency feature enabled. Contains the ISO code for any currency allowed by the organization.", + optional: true, + }, + ExpectedResponse: { + type: "string", + label: "Expected Response (%)", + description: "Percentage of responses you expect to receive for the campaign.", + optional: true, + }, + ExpectedRevenue: { + type: "string", + label: "Expected Revenue", + description: "Amount of money you expect to generate from the campaign.", + optional: true, + }, + IsActive: { + type: "boolean", + label: "Active", + description: "Indicates whether this campaign is active (default is `false`).", + optional: true, + }, + NumberSent: { + type: "integer", + label: "Number Sent", + description: "Number of individuals targeted by the campaign. For example, the number of emails sent.", + optional: true, + }, }, }; diff --git a/components/salesforce_rest_api/common/sobjects/case.mjs b/components/salesforce_rest_api/common/sobjects/case.mjs index 6a40093d57e38..17544e4b6810f 100644 --- a/components/salesforce_rest_api/common/sobjects/case.mjs +++ b/components/salesforce_rest_api/common/sobjects/case.mjs @@ -1,17 +1,197 @@ +import commonProps from "../props-async-options.mjs"; +import salesforce from "../../salesforce_rest_api.app.mjs"; + export default { - AccountId: { - type: "string", - label: "Account ID", - description: "ID of the account associated with this case.", + initialProps: { + Description: { + type: "string", + label: "Description", + description: "A text description of the case. Limit: 32 KB.", + optional: true, + }, + Status: { + type: "string", + label: "Status", + description: "The status of the case.", + optional: true, + options: [ + "New", + "Working", + "Escalated", + "Closed", + ], + }, + SuppliedEmail: { + type: "string", + label: "Email", + description: "The email address associated with the case.", + optional: true, + }, + SuppliedName: { + type: "string", + label: "Name", + description: "The name of the case.", + optional: true, + }, }, - Description: { - type: "string", - label: "Description", - description: "A text description of the case. Limit: 32 KB.", - }, - IsEscalated: { - type: "boolean", - label: "Is Escalated?", - description: "Indicates whether the case has been escalated (true) or not. A case's escalated state does not affect how you can use a case, or whether you can query, delete, or update it. You can set this flag via the API. Label is Escalated.", + extraProps: { + AccountId: { + ...commonProps.AccountId, + description: "ID of the Account associated with this case.", + optional: true, + }, + BusinessHoursId: { + ...commonProps.BusinessHoursId, + description: "ID of the Business Hours associated with this case.", + optional: true, + }, + CommunityId: { + ...commonProps.CommunityId, + description: + "ID of the [Community (Zone)](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_community.htm) associated with this case.", + optional: true, + }, + ContactId: { + ...commonProps.ContactId, + description: "ID of the Contact associated with this case.", + optional: true, + }, + FeedItemId: { + ...commonProps.FeedItemId, + description: "ID of the question in Chatter associated with the case.", + optional: true, + }, + OwnerId: { + ...commonProps.UserId, + description: "ID of the user who owns the case.", + optional: true, + }, + ParentId: { + ...commonProps.CaseId, + description: "The ID of the parent case in the hierarchy.", + optional: true, + }, + QuestionId: { + ...commonProps.QuestionId, + description: + "The question in the answers community that is associated with the case.", + optional: true, + }, + RecordTypeId: { + ...commonProps.RecordTypeId, + optional: true, + }, + ServiceContractId: { + propDefinition: [ + salesforce, + "recordId", + () => ({ + objType: "ServiceContract", + nameField: "Name", + }), + ], + label: "Service Contract ID", + description: "ID of the ServiceContract associated with the entitlement.", + }, + IsEscalated: { + type: "boolean", + label: "Escalated", + description: + "Indicates whether the case has been escalated. A case's escalated state does not affect how you can use a case, or whether you can query, delete, or update it.", + optional: true, + }, + IsStopped: { + type: "boolean", + label: "Is Stopped", + description: + "Indicates whether an entitlement process on a case is stopped.", + optional: true, + }, + Language: { + type: "string", + label: "Language", + description: "The language of the case.", + optional: true, + }, + Origin: { + type: "string", + label: "Case Origin", + description: "The source of the case.", + optional: true, + options: [ + "Phone", + "Email", + "Web", + ], + }, + Priority: { + type: "string", + label: "Priority", + description: "The importance or urgency of the case.", + optional: true, + options: [ + "High", + "Medium", + "Low", + ], + }, + Reason: { + type: "string", + label: "Reason", + description: "The reason why the case was created.", + optional: true, + options: [ + "Installation", + "Equipment Complexity", + "Performance", + "Breakdown", + "Equipment Design", + "Feedback", + "Other", + ], + }, + SlaStartDate: { + type: "string", + label: "SLA Start Date", + description: "The time that the case entered an entitlement process.", + optional: true, + }, + SourceId: { + type: "string", + label: "Source ID", + description: "The ID of the social post source.", + optional: true, + }, + Subject: { + type: "string", + label: "Subject", + description: "The subject of the case. Max 255 characters.", + optional: true, + }, + SuppliedCompany: { + type: "string", + label: "Company", + description: "The company name that was entered when the case was created.", + optional: true, + }, + SuppliedPhone: { + type: "string", + label: "Phone", + description: "The phone number that was entered when the case was created.", + optional: true, + }, + Type: { + type: "string", + label: "Type", + description: "The type of case.", + optional: true, + options: [ + "Mechanical", + "Electrical", + "Electronic", + "Structural", + "Other", + ], + }, }, }; diff --git a/components/salesforce_rest_api/common/sobjects/caseComment.mjs b/components/salesforce_rest_api/common/sobjects/caseComment.mjs index faf33431e887a..62edc5474c65b 100644 --- a/components/salesforce_rest_api/common/sobjects/caseComment.mjs +++ b/components/salesforce_rest_api/common/sobjects/caseComment.mjs @@ -1,7 +1,31 @@ +import salesforce from "../../salesforce_rest_api.app.mjs"; + export default { - CommentBody: { - type: "string", - label: "Comment Body", - description: "Text of the CaseComment. The maximum size of the comment body is 4,000 bytes. Label is Body.", + initialProps: { + CommentBody: { + type: "string", + label: "Body", + description: "Text of the CaseComment. Max size is 4,000 bytes.", + optional: true, + }, + ParentId: { + propDefinition: [ + salesforce, + "recordId", + () => ({ + objType: "Case", + nameField: "CaseNumber", + }), + ], + label: "Parent Case ID", + description: "ID of the parent Case.", + }, + IsPublished: { + type: "boolean", + label: "Is Published", + description: + "Indicates whether the CaseComment is visible to customers in the Self-Service portal.", + optional: true, + }, }, }; diff --git a/components/salesforce_rest_api/common/sobjects/contact.mjs b/components/salesforce_rest_api/common/sobjects/contact.mjs index 31eee067ba28f..7b2254399a895 100644 --- a/components/salesforce_rest_api/common/sobjects/contact.mjs +++ b/components/salesforce_rest_api/common/sobjects/contact.mjs @@ -1,47 +1,211 @@ +import { + CLEAN_STATUS_OPTIONS, GEOCODE_ACCURACY_OPTIONS, RECORD_SOURCE_OPTIONS, +} from "../constants-props.mjs"; +import commonProps from "../props-async-options.mjs"; + export default { - AccountId: { - type: "string", - label: "Account ID", - description: "ID of the account that's the parent of this contact. We recommend that you update up to 50 contacts simultaneously when changing the accounts on contacts enabled for a Customer Portal or partner portal. We also recommend that you make this update after business hours.", + initialProps: { + Description: { + type: "string", + label: "Contact Description", + description: "A description of the contact, up to 32 KB.", + optional: true, + }, + Email: { + type: "string", + label: "Email", + description: "The contact's email address.", + optional: true, + }, + FirstName: { + type: "string", + label: "First Name", + description: "The contact's first name, up to 40 characters.", + optional: true, + }, + LastName: { + type: "string", + label: "Last Name", + description: "The contact's last name, up to 80 characters.", + }, + Phone: { + type: "string", + label: "Business Phone", + description: "Phone number for the contact.", + optional: true, + }, }, - Birthdate: { - type: "string", - label: "Birth Date", - description: "The contact's birth date.Filter criteria for report filters, list view filters, and SOQL queries ignore the year portion of the Birthdate field. For example, this SOQL query returns contacts with birthdays later in the year than today:view sourceprint?1SELECT Name, Birthdate2FROM Contact3WHERE Birthdate > TODAY", - }, - Department: { - type: "string", - label: "Department", - description: "The contact's department.", - }, - Description: { - type: "string", - label: "Description", - description: "A description of the contact. Label is Contact Description up to 32 KB.", - }, - Email: { - type: "string", - label: "Email", - description: "The contact's email address.", - }, - FirstName: { - type: "string", - label: "First Name", - description: "The contact's first name up to 40 characters.", - }, - Phone: { - type: "string", - label: "Phone", - description: "Telephone number for the contact. Label is Business Phone.", - }, - Suffix: { - type: "string", - label: "Suffix", - description: "Name suffix of the contact up to 40 characters. To enable this field, ask Salesforce Customer Support for help.", - }, - Title: { - type: "string", - label: "Title", - description: "Title of the contact, such as CEO or Vice President.", + extraProps: { + AccountId: { + ...commonProps.AccountId, + description: "ID of the account that's the parent of this contact.", + optional: true, + }, + IndividualId: { + ...commonProps.IndividualId, + optional: true, + }, + OwnerId: { + ...commonProps.UserId, + description: + "The ID of the owner of the account associated with this contact.", + optional: true, + }, + RecordTypeId: { + ...commonProps.RecordTypeId, + optional: true, + }, + ReportsToId: { + ...commonProps.ContactId, + label: "Reports To ID", + optional: true, + }, + AssistantName: { + type: "string", + label: "Assistant's Name", + description: "The assistant's name.", + optional: true, + }, + AssistantPhone: { + type: "string", + label: "Assistant's Phone", + description: "The assistant's phone number.", + optional: true, + }, + Birthdate: { + type: "string", + label: "Birthdate", + description: "The contact's birthdate.", + optional: true, + }, + CleanStatus: { + type: "string", + label: "Clean Status", + description: + "Indicates the record's clean status as compared with Data.com.", + optional: true, + options: CLEAN_STATUS_OPTIONS, + }, + Department: { + type: "string", + label: "Department", + description: "The contact's department.", + optional: true, + }, + Fax: { + type: "string", + label: "Business Fax", + description: "The contact's fax number.", + optional: true, + }, + HasOptedOutOfEmail: { + type: "boolean", + label: "Email Opt Out", + description: + "Indicates whether the contact doesn't want to receive email from Salesforce (`true`) or does (`false`).", + optional: true, + }, + HasOptedOutOfFax: { + type: "boolean", + label: "Fax Opt Out", + description: "Indicates whether the contact prohibits receiving faxes.", + optional: true, + }, + HomePhone: { + type: "string", + label: "Home Phone", + description: "The contact's home phone number.", + optional: true, + }, + LeadSource: { + type: "string", + label: "Lead Source", + description: "The source of the lead that was converted to this contact.", + optional: true, + options: RECORD_SOURCE_OPTIONS, + }, + MailingCity: { + type: "string", + label: "Mailing City", + description: "The city of the contact's mailing address.", + optional: true, + }, + MailingCountry: { + type: "string", + label: "Mailing Country", + description: "The country of the contact's mailing address.", + optional: true, + }, + MailingGeocodeAccuracy: { + type: "string", + label: "Mailing Geocode Accuracy", + description: "Accuracy level of the geocode for the mailing address.", + optional: true, + options: GEOCODE_ACCURACY_OPTIONS, + }, + MailingLatitude: { + type: "string", + label: "Mailing Latitude", + description: + "A number between -90 and 90 with up to 15 decimal places. Use with `Mailing Longitude` to specify the precise geolocation of a mailing address.", + optional: true, + }, + MailingLongitude: { + type: "string", + label: "Mailing Longitude", + description: + "A number between -180 and 180 with up to 15 decimal places. Use with `Mailing Latitude` to specify the precise geolocation of a mailing address.", + optional: true, + }, + MailingPostalCode: { + type: "string", + label: "Mailing Postal Code", + description: "The postal code of the contact's mailing address.", + optional: true, + }, + MailingState: { + type: "string", + label: "Mailing State", + description: "The state of the contact's mailing address.", + optional: true, + }, + MiddleName: { + type: "string", + label: "Middle Name", + description: "The contact's middle name, up to 40 characters.", + optional: true, + }, + MobilePhone: { + type: "string", + label: "Mobile Phone", + description: "The contact's mobile phone number.", + optional: true, + }, + Salutation: { + type: "string", + label: "Salutation", + description: "The contact's salutation.", + optional: true, + options: [ + "Mr.", + "Ms.", + "Mrs.", + "Dr.", + "Prof.", + "Mx.", + ], + }, + Suffix: { + type: "string", + label: "Suffix", + description: "Name suffix of the contact up to 40 characters.", + optional: true, + }, + Title: { + type: "string", + label: "Title", + description: "Title of the contact, such as CEO or Vice President.", + optional: true, + }, }, }; diff --git a/components/salesforce_rest_api/common/sobjects/content-document-link.mjs b/components/salesforce_rest_api/common/sobjects/content-document-link.mjs new file mode 100644 index 0000000000000..0d1016238ea13 --- /dev/null +++ b/components/salesforce_rest_api/common/sobjects/content-document-link.mjs @@ -0,0 +1,51 @@ +export default { + initialProps: { + LinkedEntityId: { + type: "string", + label: "Linked Entity ID", + description: "ID of the linked object. Can include Chatter users, groups, records (any that support Chatter feed tracking including custom objects), and Salesforce CRM Content libraries.", + optional: true, + }, + ShareType: { + type: "string", + label: "Share Type", + description: "The permission granted to the user of the shared file in a library. This is determined by the permission the user already has in the library.", + optional: true, + default: "I", + options: [ + { + label: "Viewer Permission", + value: "V", + }, + { + label: "Collaborator Permission", + value: "C", + }, + { + label: "Inferred Permission", + value: "I", + }, + ], + }, + Visibility: { + type: "string", + label: "Visibility", + description: "Specifies whether this file is available to all users, internal users, or shared users.", + optional: true, + options: [ + { + label: "All Users", + value: "AllUsers", + }, + { + label: "Internal Users", + value: "InternalUsers", + }, + { + label: "Shared Users", + value: "SharedUsers", + }, + ], + }, + }, +}; diff --git a/components/salesforce_rest_api/common/sobjects/content-note.mjs b/components/salesforce_rest_api/common/sobjects/content-note.mjs new file mode 100644 index 0000000000000..9afef3cb76e9a --- /dev/null +++ b/components/salesforce_rest_api/common/sobjects/content-note.mjs @@ -0,0 +1,34 @@ +import salesforce from "../../salesforce_rest_api.app.mjs"; + +export default { + initialProps: { + OwnerId: { + propDefinition: [ + salesforce, + "recordId", + () => ({ + objType: "User", + nameField: "Name", + }), + ], + label: "Owner ID", + description: "ID of the user who owns the note.", + }, + Title: { + type: "string", + label: "Title", + description: "Title of the note.", + }, + Content: { + type: "string", + label: "Content", + description: "The content or body of the note, which can include properly formatted HTML or plain text.", + }, + IsReadOnly: { + type: "boolean", + label: "Read Only", + description: "Indicates whether the note is read only.", + optional: true, + }, + }, +}; diff --git a/components/salesforce_rest_api/common/sobjects/event.mjs b/components/salesforce_rest_api/common/sobjects/event.mjs index 80405e519cb8f..5ec46534a039b 100644 --- a/components/salesforce_rest_api/common/sobjects/event.mjs +++ b/components/salesforce_rest_api/common/sobjects/event.mjs @@ -1,22 +1,222 @@ +import { // eslint-disable-next-line max-len + RECURRENCE_INSTANCE_OPTIONS, RECURRENCE_MONTH_OPTIONS, TIMEZONE_OPTIONS, RECURRENCE_TYPE_OPTIONS, WEEKDAY_MASK_OPTIONS, +} from "../constants-props.mjs"; +import commonProps from "../props-async-options.mjs"; + export default { - AcceptedEventInviteeIds: { - type: "string", - label: "Accepted Event Invitee IDs", - description: "A string array of contact or lead IDs who accepted this event. This JunctionIdList is linked to the AcceptedEventRelation child relationship. Warning Adding a JunctionIdList field name to the fieldsToNull property deletes all related junction records. This action can't be undone.", + initialProps: { + AcceptedEventInviteeIds: { + ...commonProps.ContactOrLeadIds, + label: "Accepted Event Invitee IDs", + description: "One or more Contact or Lead IDs who accepted this event.", + optional: true, + async options() { + const contacts = await this.salesforce.listRecordOptions({ + objType: "Contact", + nameField: "Name", + }); + const leads = await this.salesforce.listRecordOptions({ + objType: "Lead", + nameField: "Name", + }); + return [ + ...(contacts ?? []), + ...(leads ?? []), + ]; + }, + }, + ActivityDate: { + type: "string", + label: "Due Date / Time", + description: + "The date/time (`ActivityDateTime`) of the event, or only the date (`ActivityDate`) if it is an all-day event.", + }, + Description: { + type: "string", + label: "Description", + description: "A text description of the event. Limit: 32,000 characters.", + optional: true, + }, + DurationInMinutes: { + type: "integer", + label: "Duration (in minutes)", + description: "The event length in minutes.", + optional: true, + }, + EndDateTime: { + type: "string", + label: "End Date / Time", + description: "The date/time when the event ends.", + optional: true, + }, + IsAllDayEvent: { + type: "boolean", + label: "All-Day Event", + description: "Whether the event is an all-day event.", + optional: true, + }, + Location: { + type: "string", + label: "Location", + description: "The location of the event.", + optional: true, + }, }, - ActivityDate: { - type: "string", - label: "Activity Date", - description: "Contains the event's due date if the IsAllDayEvent flag is set to true. This field is a date field with a timestamp that is always set to midnight in the Coordinated Universal Time (UTC) time zone. Don't attempt to alter the timestamp to account for time zone differences. Label is Due Date Only.This field is required in versions 12.0 and earlier if the IsAllDayEvent flag is set to true. The value for this field and StartDateTime must match, or one of them must be null.", - }, - Description: { - type: "string", - label: "Description", - description: "Contains a text description of the event. Limit: 32,000 characters.", - }, - Subject: { - type: "string", - label: "Subject", - description: "The subject line of the event, such as Call, Email, or Meeting. Limit: 255 characters.", + extraProps: { + DeclinedEventInviteeIds: { + ...commonProps.ContactOrLeadIds, + label: "Declined Event Invitee IDs", + description: "One or more Contact or Lead IDs who declined this event.", + optional: true, + }, + UndecidedEventInviteeIds: { + ...commonProps.ContactOrLeadIds, + label: "Undecided Event Invitee IDs", + description: "One or more Contact or Lead IDs who are undecided about this event.", + optional: true, + }, + OwnerId: { + ...commonProps.UserId, + label: "Assigned to ID", + description: "ID of the user or public calendar who owns the event.", + optional: true, + }, + IsPrivate: { + type: "boolean", + label: "Private", + description: + "If true, users other than the creator of the event can't see the event details when viewing the event user's calendar.", + optional: true, + }, + IsRecurrence: { + type: "boolean", + label: "Recurring Event", + description: + "Indicates whether a Salesforce Classic event is scheduled to repeat itself.", + optional: true, + }, + IsReminderSet: { + type: "boolean", + label: "Reminder Set", + description: "Indicates whether the activity is a reminder.", + optional: true, + }, + IsVisibleInSelfService: { + type: "boolean", + label: "Visible in Self-Service", + description: + "Indicates whether an event associated with an object can be viewed in the Customer Portal.", + optional: true, + }, + RecurrenceDayOfMonth: { + type: "integer", + label: "Recurrence Day of Month", + description: "The day of the month on which the event repeats.", + optional: true, + }, + RecurrenceDayOfWeekMask: { + type: "integer[]", + label: "Recurrence Day of Week", + description: "The day(s) of the week on which the event repeats.", + optional: true, + options: WEEKDAY_MASK_OPTIONS, + }, + RecurrenceEndDateOnly: { + type: "string", + label: "Recurrence End Date", + description: "Indicates the last date on which the event repeats.", + optional: true, + }, + RecurrenceInstance: { + type: "string", + label: "Recurrence Instance", + description: + "Indicates the frequency of the Salesforce Classic event's recurrence.", + optional: true, + options: RECURRENCE_INSTANCE_OPTIONS, + }, + RecurrenceInterval: { + type: "integer", + label: "Recurrence Interval", + description: + "Indicates the interval between Salesforce Classic recurring events.", + optional: true, + }, + RecurrenceMonthOfYear: { + type: "string", + label: "Recurrence Month of Year", + description: + "Indicates the month in which the Salesforce Classic recurring event repeats.", + optional: true, + options: RECURRENCE_MONTH_OPTIONS, + }, + RecurrenceStartDateTime: { + type: "string", + label: "Recurrence Start Date / Time", + description: + "Indicates the date and time when the Salesforce Classic recurring event begins.", + optional: true, + }, + RecurrenceTimeZoneSidKey: { + type: "string", + label: "Recurrence Time Zone", + description: + "Indicates the time zone associated with a Salesforce Classic recurring event.", + optional: true, + options: TIMEZONE_OPTIONS, + }, + RecurrenceType: { + type: "string", + label: "Recurrence Type", + description: "Indicates how often the Salesforce Classic event repeats.", + optional: true, + options: RECURRENCE_TYPE_OPTIONS, + }, + ReminderDateTime: { + type: "string", + label: "Reminder Date / Time", + description: "Represents the time when the reminder is scheduled to fire", + optional: true, + }, + ShowAs: { + type: "string", + label: "Show As", + description: + "Indicates how this event appears when another user views the calendar.", + optional: true, + options: [ + { + label: "Busy", + value: "Busy", + }, + { + label: "Out of Office", + value: "OutOfOffice", + }, + { + label: "Free", + value: "Free", + }, + ], + }, + StartDateTime: { + type: "string", + label: "Start Date / Time", + description: "Indicates the start date and time of the event.", + optional: true, + }, + Subject: { + type: "string", + label: "Subject", + description: "The subject line of the event. Limit: 255 characters.", + optional: true, + options: [ + "Call", + "Email", + "Meeting", + "Send Letter/Quote", + "Other", + ], + }, }, }; diff --git a/components/salesforce_rest_api/common/sobjects/lead.mjs b/components/salesforce_rest_api/common/sobjects/lead.mjs index 10c4a6aa4ebab..cef42c876a6f2 100644 --- a/components/salesforce_rest_api/common/sobjects/lead.mjs +++ b/components/salesforce_rest_api/common/sobjects/lead.mjs @@ -1,27 +1,250 @@ +import { + CLEAN_STATUS_OPTIONS, GEOCODE_ACCURACY_OPTIONS, RECORD_SOURCE_OPTIONS, +} from "../constants-props.mjs"; +import commonProps from "../props-async-options.mjs"; + export default { - City: { - type: "string", - label: "City", - description: "City for the lead's address.", + createProps: { + IsConverted: { + type: "boolean", + label: "Converted", + description: "Indicates whether the lead has been converted", + optional: true, + }, }, - Country: { - type: "string", - label: "Country", - description: "The lead's country.", + initialProps: { + Company: { + type: "string", + label: "Company", + description: "The lead's company.", + }, + Description: { + type: "string", + label: "Description", + description: "The lead's description.", + optional: true, + }, + Email: { + type: "string", + label: "Email", + description: "The lead's email address.", + optional: true, + }, + FirstName: { + type: "string", + label: "First Name", + description: "The lead's first name.", + optional: true, + }, + LastName: { + type: "string", + label: "Last Name", + description: "The lead's last name.", + }, + Phone: { + type: "string", + label: "Phone", + description: "The lead's phone number.", + optional: true, + }, }, - Description: { - type: "string", - label: "Description", - description: "The lead's description.", - }, - Email: { - type: "string", - label: "Email", - description: "The lead's email address.", - }, - FirstName: { - type: "string", - label: "First Name", - description: "The lead's first name up to 40 characters.", + extraProps: { + ConvertedAccountId: { + ...commonProps.AccountId, + label: "Converted Account ID", + description: "The account into which the lead converted.", + optional: true, + }, + ConvertedContactId: { + ...commonProps.ContactId, + label: "Converted Contact ID", + description: "The contact into which the lead converted.", + optional: true, + }, + ConvertedOpportunityId: { + ...commonProps.OpportunityId, + label: "Converted Opportunity ID", + description: "The opportunity into which the lead converted.", + optional: true, + }, + IndividualId: { + ...commonProps.IndividualId, + label: "Individual ID", + description: "ID of the data privacy record associated with this lead.", + optional: true, + }, + OwnerId: { + ...commonProps.UserId, + label: "Owner ID", + description: "ID of the lead's owner.", + optional: true, + }, + RecordTypeId: { + ...commonProps.RecordTypeId, + optional: true, + }, + AnnualRevenue: { + type: "string", + label: "Annual Revenue", + description: "Annual revenue for the lead's company.", + optional: true, + }, + City: { + type: "string", + label: "City", + description: "City for the lead's address.", + optional: true, + }, + CleanStatus: { + type: "string", + label: "Clean Status", + description: + "Indicates the record's clean status compared with Data.com.", + options: CLEAN_STATUS_OPTIONS, + }, + CompanyDunsNumber: { + type: "string", + label: "Company D-U-N-S Number", + description: + "The Data Universal Numbering System (D-U-N-S) number (max 9 characters).", + optional: true, + }, + Country: { + type: "string", + label: "Country", + description: "The lead's country.", + optional: true, + }, + Fax: { + type: "string", + label: "Fax", + description: "The lead's fax number.", + optional: true, + }, + HasOptedOutOfFax: { + type: "boolean", + label: "Fax Opt Out", + description: + "Indicates whether the lead doesn't want to receive faxes from Salesforce (`true`) or not (`false`)", + optional: true, + }, + GeocodeAccuracy: { + type: "string", + label: "Geocode Accuracy", + description: "Accuracy level of the geocode for the address.", + optional: true, + options: GEOCODE_ACCURACY_OPTIONS, + }, + Industry: { + type: "string", + label: "Industry", + description: "Industry in which the lead works.", + optional: true, + }, + IsUnreadByOwner: { + type: "boolean", + label: "Unread by Owner", + description: "If true, lead has been assigned, but not yet viewed.", + optional: true, + }, + Latitude: { + type: "string", + label: "Latitude", + description: + "A number between -90 and 90 with up to 15 decimal places. Use with `Longitude` to specify the precise geolocation of an address.", + optional: true, + }, + Longitude: { + type: "string", + label: "Longitude", + description: + "A number between -180 and 180 with up to 15 decimal places. Use with `Latitude` to specify the precise geolocation of an address.", + optional: true, + }, + LeadSource: { + type: "string", + label: "Lead Source", + description: "The lead's source.", + optional: true, + options: RECORD_SOURCE_OPTIONS, + }, + MiddleName: { + type: "string", + label: "Middle Name", + description: "The lead's middle name up to 40 characters.", + optional: true, + }, + MobilePhone: { + type: "string", + label: "Mobile Phone", + description: "The lead's mobile phone number.", + optional: true, + }, + NumberOfEmployees: { + type: "integer", + label: "Employees", + description: "Number of employees at the lead's company.", + optional: true, + }, + PostalCode: { + type: "string", + label: "Zip/Postal Code", + description: "The lead's postal code.", + optional: true, + }, + Rating: { + type: "string", + label: "Rating", + description: "Rating of the lead.", + optional: true, + options: [ + "Hot", + "Warm", + "Cold", + ], + }, + State: { + type: "string", + label: "State/Province", + description: "State for the address of the lead.", + optional: true, + }, + Status: { + type: "string", + label: "Status", + description: "Status code for this converted lead.", + optional: true, + options: [ + "Open - Not Contacted", + "Working - Contacted", + "Closed - Converted", + "Closed - Not Converted", + ], + }, + Street: { + type: "string", + label: "Street", + description: "Street number and name for the address of the lead.", + optional: true, + }, + Suffix: { + type: "string", + label: "Suffix", + description: "The lead's name suffix up to 40 characters.", + optional: true, + }, + Title: { + type: "string", + label: "Title", + description: + "Title for the lead, such as CFO or CEO. The maximum size is 128 characters. ", + optional: true, + }, + Website: { + type: "string", + label: "Website", + description: "Website for the lead.", + optional: true, + }, }, }; diff --git a/components/salesforce_rest_api/common/sobjects/note.mjs b/components/salesforce_rest_api/common/sobjects/note.mjs index 3a19c81aec589..1016a567f7e21 100644 --- a/components/salesforce_rest_api/common/sobjects/note.mjs +++ b/components/salesforce_rest_api/common/sobjects/note.mjs @@ -1,17 +1,40 @@ +import salesforce from "../../salesforce_rest_api.app.mjs"; + export default { - Body: { - type: "string", - label: "Body", - description: "Body of the note. Limited to 32 KB.", - }, - IsPrivate: { - type: "boolean", - label: "Is Private", - description: "If true, only the note owner or a user with the “Modify All Data” permission can view the note or query it via the API. Note that if a user who does not have the “Modify All Data” permission sets this field to true on a note that they do not own, then they can no longer query, delete, or update the note.", - }, - OwnerId: { - type: "string", - label: "Owner ID", - description: "ID of the user who owns the note.", + initialProps: { + Body: { + type: "string", + label: "Body", + description: "Body of the note. Limited to 32 KB.", + }, + IsPrivate: { + type: "boolean", + label: "Private", + description: "If true, only the note owner or a user with the “Modify All Data” permission can view the note or query it via the API.", + optional: true, + }, + OwnerId: { + propDefinition: [ + salesforce, + "recordId", + () => ({ + objType: "User", + nameField: "Name", + }), + ], + label: "Owner ID", + description: "ID of the user who owns the note.", + optional: true, + }, + ParentId: { + type: "string", + label: "Parent ID", + description: "ID of the object associated with the note. [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_note.htm) for which objects can be referenced.", + }, + Title: { + type: "string", + label: "Title", + description: "Title of the note.", + }, }, }; diff --git a/components/salesforce_rest_api/common/sobjects/opportunity.mjs b/components/salesforce_rest_api/common/sobjects/opportunity.mjs index bdf96c354cfd7..6f281f2d02f27 100644 --- a/components/salesforce_rest_api/common/sobjects/opportunity.mjs +++ b/components/salesforce_rest_api/common/sobjects/opportunity.mjs @@ -1,27 +1,153 @@ +import { RECORD_SOURCE_OPTIONS } from "../constants-props.mjs"; +import commonProps from "../props-async-options.mjs"; +import salesforce from "../../salesforce_rest_api.app.mjs"; + export default { - AccountId: { - type: "string", - label: "Account ID", - description: "ID of the account associated with this opportunity.", + createProps: { + ContactId: { + propDefinition: [ + salesforce, + "recordId", + () => ({ + objType: "Contact", + nameField: "Name", + }), + ], + label: "Contact ID", + description: + "ID of the contact associated with this opportunity, set as the primary contact.", + optional: true, + }, }, - Amount: { - type: "string", - label: "Amount", - description: "Estimated total sale amount. For opportunities with products, the amount is the sum of the related products. Any attempt to update this field, if the record has products, will be ignored. The update call will not be rejected, and other fields will be updated as specified, but the Amount will be unchanged.", + initialProps: { + CloseDate: { + type: "string", + label: "Close Date", + description: "Date when the opportunity is expected to close.", + }, + Description: { + type: "string", + label: "Description", + description: + "Text description of the opportunity. Limit: 32,000 characters.", + optional: true, + }, + Name: { + type: "string", + label: "Name", + description: "A name for this opportunity. Limit: 120 characters", + }, + StageName: { + type: "string", + label: "Stage Name", + description: + "Current stage of this record. This controls several other fields on an opportunity.", + options: [ + "Prospecting", + "Qualification", + "Needs Analysis", + "Value Proposition", + "Id. Decision Makers", + "Perception Analysis", + "Proposal/Price Quote", + "Negotiation/Review", + "Closed Won", + "Closed Lost", + ], + }, }, - CampaignId: { - type: "string", - label: "Campaign ID", - description: "ID of a related Campaign. This field is defined only for those organizations that have the campaign feature Campaigns enabled. The User must have read access rights to the cross-referenced Campaign object in order to create or update that campaign into this field on the opportunity.", - }, - ContactId: { - type: "string", - label: "Contact ID", - description: "ID of the contact associated with this opportunity, set as the primary contact. Read-only field that is derived from the opportunity contact role, which is created at the same time the opportunity is created. This field can only be populated when it's created, and can't be updated. To update the value in this field, change the IsPrimary flag on the OpportunityContactRole associated with this opportunity. Available in API version 46.0 and later.", - }, - ContractId: { - type: "string", - label: "Contract ID", - description: "ID of the contract that's associated with this opportunity.", + extraProps: { + AccountId: { + ...commonProps.AccountId, + description: "ID of the account associated with this opportunity.", + optional: true, + }, + CampaignId: { + ...commonProps.CampaignId, + description: "ID of a related Campaign.", + optional: true, + }, + OwnerId: { + ...commonProps.UserId, + description: + "ID of the User who has been assigned to work this opportunity.", + optional: true, + }, + Pricebook2Id: { + ...commonProps.Pricebook2Id, + description: "ID of a related Pricebook2 object.", + optional: true, + }, + RecordTypeId: { + ...commonProps.RecordTypeId, + optional: true, + }, + Amount: { + type: "string", + label: "Amount", + description: + "Estimated total sale amount. For opportunities with products, the amount is the sum of the related products.", + optional: true, + }, + ForecastCategoryName: { + type: "string", + label: "Forecast Category Name", + description: "The name of the forecast category.", + optional: true, + options: [ + "Omitted", + "Pipeline", + "Best Case", + "Commit", + "Closed", + ], + }, + IsExcludedFromTerritory2Filter: { + type: "boolean", + label: "Excluded from Filter", + description: + "Used for Filter-Based Opportunity Territory Assignment. Indicates whether the opportunity is excluded (`true`) or included (`false`) each time the APEX filter is executed.", + optional: true, + }, + LeadSource: { + type: "string", + label: "Lead Source", + description: + "Source of this opportunity.", + optional: true, + options: RECORD_SOURCE_OPTIONS, + }, + NextStep: { + type: "string", + label: "Next Step", + description: + "Description of next task in closing opportunity. Limit: 255 characters.", + optional: true, + }, + Probability: { + type: "string", + label: "Probability (%)", + description: + "Percentage of estimated confidence in closing the opportunity.", + optional: true, + }, + TotalOpportunityQuantity: { + type: "integer", + label: "Quantity", + description: "Number of items included in this opportunity.", + optional: true, + }, + Type: { + type: "string", + label: "Opportunity Type", + description: "Type of opportunity.", + optional: true, + options: [ + "Existing Customer - Upgrade", + "Existing Customer - Replacement", + "Existing Customer - Downgrade", + "New Customer", + ], + }, }, }; diff --git a/components/salesforce_rest_api/common/sobjects/task.mjs b/components/salesforce_rest_api/common/sobjects/task.mjs index 931692972e552..1e842b4255b20 100644 --- a/components/salesforce_rest_api/common/sobjects/task.mjs +++ b/components/salesforce_rest_api/common/sobjects/task.mjs @@ -1,25 +1,246 @@ -import constants from "../constants.mjs"; +import { + RECURRENCE_INSTANCE_OPTIONS, + RECURRENCE_MONTH_OPTIONS, + TIMEZONE_OPTIONS, + RECURRENCE_TYPE_OPTIONS, + WEEKDAY_MASK_OPTIONS, +} from "../constants-props.mjs"; +import commonProps from "../props-async-options.mjs"; export default { - ActivityDate: { - type: "string", - label: "Activity date", - description: "Represents the due date of the task. This field has a timestamp that is always set to midnight in the Coordinated Universal Time (UTC) time zone. The timestamp is not relevant; do not attempt to alter it to accommodate time zone differences. Note This field can't be set or updated for a recurring task (IsRecurrence is true).", + createProps: { + IsRecurrence: { + type: "boolean", + label: "Recurring", + description: + "Indicates whether the task is scheduled to repeat itself (`true`) or only occurs once (`false`).", + optional: true, + }, + TaskSubtype: { + type: "string", + label: "Task Subtype", + description: "The subtype of the task.", + optional: true, + options: [ + { + label: "Task", + value: "Task", + }, + { + label: "Email", + value: "Email", + }, + { + label: "List Email", + value: "ListEmail", + }, + { + label: "Cadence", + value: "Cadence", + }, + { + label: "Call", + value: "Call", + }, + { + label: "LinkedIn", + value: "LinkedIn", + }, + ], + }, }, - Description: { - type: "string", - label: "Description", - description: "Contains a text description of the task.", + initialProps: { + ActivityDate: { + type: "string", + label: "Due Date", + description: "Represents the due date of the task.", + optional: true, + }, + Description: { + type: "string", + label: "Description", + description: "A text description of the task.", + optional: true, + }, + Priority: { + type: "string", + label: "Priority", + description: + "Indicates the importance or urgency of a task, such as high or low.", + options: [ + "High", + "Normal", + "Low", + ], + }, }, - Subject: { - type: "string", - label: "Subject", - description: "The subject line of the task, such as “Call” or “Send Quote.” Limit: 255 characters.", - }, - TaskSubtype: { - type: "string", - label: "Task sub-type", - description: "Provides standard subtypes to facilitate creating and searching for specific task subtypes. This field isn't updateable. TaskSubtype values: Task Email List Email Cadence Call Note The Cadence subtype is an internal value used by High Velocity Sales, and can't be set manually.", - options: constants.TASK_SUB_TYPES, + extraProps: { + OwnerId: { + ...commonProps.UserId, + label: "Owner ID", + description: "ID of the User or Group who owns the record.", + optional: true, + }, + TaskWhoIds: { + ...commonProps.ContactOrLeadIds, + label: "Related IDs", + description: "One or more Contact or Lead IDs related to this task.", + optional: true, + }, + CallDisposition: { + type: "string", + label: "Call Result", + description: + "Represents the result of a given call, for example, “we'll call back,” or “call unsuccessful.” Limit is 255 characters.", + optional: true, + }, + CallDurationInSeconds: { + type: "integer", + label: "Call Duration", + description: "Duration of the call in seconds.", + optional: true, + }, + CallObject: { + type: "string", + label: "Call Object Identifier", + description: "Name of a call center. Limit is 255 characters.", + optional: true, + }, + CallType: { + type: "string", + label: "Call Type", + description: "The type of call being answered.", + optional: true, + options: [ + "Internal", + "Inbound", + "Outbound", + ], + }, + IsReminderSet: { + type: "boolean", + label: "Reminder Set", + description: + "Indicates whether a popup reminder has been set for the task.", + optional: true, + }, + IsVisibleInSelfService: { + type: "boolean", + label: "Visible in Self-Service", + description: + "Indicates whether a task associated with an object can be viewed in the Customer Portal.", + optional: true, + }, + RecurrenceDayOfMonth: { + type: "integer", + label: "Recurrence Day of Month", + description: "The day of the month in which the task repeats.", + optional: true, + }, + RecurrenceDayOfWeekMask: { + type: "integer[]", + label: "Recurrence Day of Week Mask", + description: "The day(s) of the week on which the task repeats.", + optional: true, + options: WEEKDAY_MASK_OPTIONS, + }, + RecurrenceEndDateOnly: { + type: "string", + label: "Recurrence End Date", + description: "The last date on which the task repeats.", + optional: true, + }, + RecurrenceInstance: { + type: "string", + label: "Recurrence Instance", + description: "The frequency of the recurring task.", + optional: true, + options: RECURRENCE_INSTANCE_OPTIONS, + }, + RecurrenceInterval: { + type: "integer", + label: "Recurrence Interval", + description: "The interval between recurring tasks.", + optional: true, + }, + RecurrenceMonthOfYear: { + type: "string", + label: "Recurrence Month of Year", + description: "The month of the year in which the task repeats.", + optional: true, + options: RECURRENCE_MONTH_OPTIONS, + }, + RecurrenceRegeneratedType: { + type: "string", + label: "Repeat This Task", + description: "Represents what triggers a repeating task to repeat.", + optional: true, + options: [ + { + label: "After due date", + value: "RecurrenceRegenerateAfterDueDate", + }, + { + label: "After date completed", + value: "RecurrenceRegenerateAfterToday", + }, + { + label: "(Task Closed)", + value: "RecurrenceRegenerated", + }, + ], + }, + RecurrenceStartDateOnly: { + type: "string", + label: "Recurrence Start Date", + description: "The date when the recurring task begins.", + optional: true, + }, + RecurrenceTimeZoneSidKey: { + type: "string", + label: "Recurrence Time Zone", + description: "The time zone associated with the recurring task.", + optional: true, + options: TIMEZONE_OPTIONS, + }, + RecurrenceType: { + type: "string", + label: "Recurrence Type", + description: "Indicates how often the task repeats.", + optional: true, + options: RECURRENCE_TYPE_OPTIONS, + }, + ReminderDateTime: { + type: "string", + label: "Reminder Date / Time", + description: "Represents the time when the reminder is scheduled to fire", + optional: true, + }, + Status: { + type: "string", + label: "Status", + description: "The status of the task.", + optional: true, + options: [ + "Not Started", + "In Progress", + "Completed", + "Waiting on someone else", + "Deferred", + ], + }, + Subject: { + type: "string", + label: "Subject", + description: "The subject line of the task. Limit: 255 characters.", + optional: true, + options: [ + "Call", + "Email", + "Send Letter", + "Send Quote", + "Other", + ], + }, }, }; diff --git a/components/salesforce_rest_api/common/sobjects/user.mjs b/components/salesforce_rest_api/common/sobjects/user.mjs new file mode 100644 index 0000000000000..e77a6f9e2f1df --- /dev/null +++ b/components/salesforce_rest_api/common/sobjects/user.mjs @@ -0,0 +1,965 @@ +import salesforce from "../../salesforce_rest_api.app.mjs"; +import { + EMAIL_ENCODING_OPTIONS, + GEOCODE_ACCURACY_OPTIONS, + LANGUAGE_OPTIONS, + LOCALE_OPTIONS, + TIMEZONE_OPTIONS, +} from "../constants-props.mjs"; +import commonProps from "../props-async-options.mjs"; + +export default { + updateProps: { + UserPreferencesEmailVerified: { + type: "boolean", + label: "Email Verified", + description: + "Indicates whether a user's email address is verified (true) or unverified (false).", + optional: true, + }, + }, + initialProps: { + Alias: { + type: "string", + label: "Alias", + description: "The user's alias (max 8 characters).", + }, + CommunityNickname: { + type: "string", + label: "Nickname", + description: + "Name used to identify this user in the Experience Cloud site.", + }, + DigestFrequency: { + type: "string", + label: "Chatter Email Highlights Frequency", + description: + "The send frequency of the user's Chatter personal email digest.", + options: [ + { + label: "Daily", + value: "D", + }, + { + label: "Weekly", + value: "W", + }, + { + label: "Never", + value: "N", + }, + ], + }, + Email: { + type: "string", + label: "Email", + description: "The user's email address.", + }, + EmailEncodingKey: { + type: "string", + label: "Email Encoding", + description: "The email encoding for the user.", + options: EMAIL_ENCODING_OPTIONS, + }, + LanguageLocaleKey: { + type: "string", + label: "Language", + description: "The user's language.", + options: LANGUAGE_OPTIONS, + }, + LastName: { + type: "string", + label: "Last Name", + description: "The user's last name.", + }, + LocaleSidKey: { + type: "string", + label: "Locale", + description: + "The locale affects formatting and parsing of values, especially numeric values, in the user interface.", + options: LOCALE_OPTIONS, + }, + ProfileId: { + propDefinition: [ + salesforce, + "recordId", + () => ({ + objType: "Profile", + nameField: "Name", + }), + ], + label: "Profile ID", + description: + "ID of the user's Profile. Use this value to cache metadata based on profile.", + }, + TimeZoneSidKey: { + type: "string", + label: "Time Zone", + description: + "A User time zone affects the offset used when displaying or entering times in the user interface.", + options: TIMEZONE_OPTIONS, + }, + Username: { + type: "string", + label: "Username", + description: + "Contains the name that a user enters to log in to the API or the user interface. The value for this field must be in the form of an email address, using all lowercase characters. It must also be unique across all organizations.", + }, + UserPermissionsMarketingUser: { + type: "boolean", + label: "Marketing User", + description: + "Indicates whether the user is enabled to manage campaigns in the user interface (true) or not (false).", + }, + UserPermissionsOfflineUser: { + type: "boolean", + label: "Offline User", + description: + "Indicates whether the user is enabled to use Offline Edition (true) or not (false).", + }, + }, + extraProps: { + AccountId: { + ...commonProps.AccountId, + description: "ID of the Account associated with a Customer Portal user.", + optional: true, + }, + CallCenterId: { + ...commonProps.CallCenterId, + description: + "If Salesforce CRM Call Center is enabled, represents the call center that this user is assigned to.", + optional: true, + }, + ContactId: { + ...commonProps.ContactId, + description: "ID of the Contact associated with this account.", + optional: true, + }, + DelegatedApproverId: { + ...commonProps.UserId, + label: "Delegated Approver ID", + description: "ID of the user who is a delegated approver for this user.", + optional: true, + }, + IndividualId: { + ...commonProps.IndividualId, + description: "ID of the data privacy record associated with this user.", + optional: true, + }, + ManagerId: { + ...commonProps.UserId, + label: "Manager ID", + description: "The ID of the user who manages this user.", + optional: true, + }, + UserRoleId: { + ...commonProps.UserRoleId, + description: "ID of the user's UserRole.", + optional: true, + }, + AboutMe: { + type: "string", + label: "About Me", + description: + "Information about the user, such as areas of interest or skills.", + optional: true, + }, + City: { + type: "string", + label: "City", + description: + "The city associated with the user. Up to 40 characters allowed.", + optional: true, + }, + CompanyName: { + type: "string", + label: "Company Name", + description: "The name of the user's company.", + optional: true, + }, + Country: { + type: "string", + label: "Country", + description: + "The country associated with the user. Up to 80 characters allowed.", + optional: true, + }, + DefaultGroupNotificationFrequency: { + type: "string", + label: "Default Notification Frequency when Joining Groups", + description: + "The default frequency for sending the user's Chatter group email notifications when the user joins groups.", + optional: true, + options: [ + { + label: "Email on Each Post", + value: "P", + }, + { + label: "Daily Digests", + value: "D", + }, + { + label: "Weekly Digests", + value: "W", + }, + { + label: "Never", + value: "N", + }, + ], + }, + Department: { + type: "string", + label: "Department", + description: "The company department associated with the user.", + optional: true, + }, + Division: { + type: "string", + label: "Division", + description: "The division associated with this user.", + optional: true, + }, + EmailPreferencesAutoBcc: { + type: "boolean", + label: "Auto Bcc", + description: + "Determines whether the user receives copies of sent emails.", + optional: true, + }, + EmployeeNumber: { + type: "string", + label: "Employee Number", + description: "The user's employee number.", + optional: true, + }, + Extension: { + type: "string", + label: "Extension", + description: "The user's phone extension number.", + optional: true, + }, + Fax: { + type: "string", + label: "Fax", + description: "The user's fax number.", + optional: true, + }, + FederationIdentifier: { + type: "string", + label: "SAML Federation ID", + description: + "Indicates the value that must be listed in the Subject element of a Security Assertion Markup Language (SAML) IDP certificate to authenticate the user for a client application using single sign-on.", + optional: true, + }, + FirstName: { + type: "string", + label: "First Name", + description: "The user's first name.", + optional: true, + }, + ForecastEnabled: { + type: "boolean", + label: "Allow Forecasting", + description: "Indicates whether the user is enabled for forecasts.", + optional: true, + }, + GeocodeAccuracy: { + type: "string", + label: "Geocode Accuracy", + description: + "The level of accuracy of a location's geographical coordinates compared with its physical address.", + optional: true, + options: GEOCODE_ACCURACY_OPTIONS, + }, + IsActive: { + type: "boolean", + label: "Active", + description: "Indicates whether the user has access to log in.", + optional: true, + }, + JigsawImportLimitOverride: { + type: "integer", + label: "Data.com Monthly Addition Limit", + description: + "The Data.com user's monthly addition limit. The value must be between zero and the organization's monthly addition limit.", + optional: true, + }, + Latitude: { + type: "string", + label: "Latitude", + description: + "A number between -90 and 90 with up to 15 decimal places. Use with `Longitude` to specify the precise geolocation of an address.", + optional: true, + }, + Longitude: { + type: "string", + label: "Longitude", + description: + "A number between -180 and 180 with up to 15 decimal places. Use with `Latitude` to specify the precise geolocation of an address.", + optional: true, + }, + MiddleName: { + type: "string", + label: "Middle Name", + description: "The user's middle name. Maximum size is 40 characters.", + optional: true, + }, + MobilePhone: { + type: "string", + label: "Mobile", + description: "The user's mobile device number.", + optional: true, + }, + Phone: { + type: "string", + label: "Phone", + description: "The user's phone number.", + optional: true, + }, + PostalCode: { + type: "string", + label: "Zip/Postal Code", + description: "The user's postal or ZIP code.", + optional: true, + }, + ReceivesAdminInfoEmails: { + type: "boolean", + label: "Admin Info Emails", + description: + "Indicates whether the user receives email for administrators from Salesforce (true) or not (false).", + optional: true, + }, + ReceivesInfoEmails: { + type: "boolean", + label: "Info Emails", + description: + "Indicates whether the user receives informational email from Salesforce (true) or not (false).", + optional: true, + }, + SenderEmail: { + type: "string", + label: "Email Sender Address", + description: + "The email address used as the From address when the user sends emails.", + optional: true, + }, + SenderName: { + type: "string", + label: "Email Sender Name", + description: + "The name used as the email sender when the user sends emails.", + optional: true, + }, + Signature: { + type: "string", + label: "Email Signature", + description: "The signature text added to emails.", + optional: true, + }, + State: { + type: "string", + label: "State", + description: + "The state associated with the User. Up to 80 characters allowed.", + optional: true, + }, + Street: { + type: "string", + label: "Street", + description: "The street address associated with the User.", + optional: true, + }, + Suffix: { + type: "string", + label: "Suffix", + description: "The user's name suffix. Maximum size is 40 characters.", + optional: true, + }, + Title: { + type: "string", + label: "Title", + description: "The user's business title, such as Vice President.", + optional: true, + }, + UserPermissionsCallCenterAutoLogin: { + type: "boolean", + label: "Auto-login To Call Center", + description: + "Required if Salesforce CRM Call Center is enabled. Indicates whether the user is enabled to use the auto login feature of the call center (true) or not (false).", + optional: true, + }, + UserPermissionsChatterAnswersUser: { + type: "boolean", + label: "Chatter Answers User", + description: + "Indicates whether the portal user is enabled to use the Chatter Answers feature (true) or not (false).", + optional: true, + }, + UserPermissionsInteractionUser: { + type: "boolean", + label: "Flow User", + description: "Indicates whether the user can run flows or not.", + optional: true, + }, + UserPermissionsJigsawProspectingUser: { + type: "boolean", + label: "Data.com User", + description: + "Indicates whether the user is allocated one Data.com user license (true) or not (false).", + optional: true, + }, + UserPermissionsKnowledgeUser: { + type: "boolean", + label: "Knowledge User", + description: + "Indicates whether the user is enabled to use Salesforce Knowledge (true) or not (false).", + optional: true, + }, + UserPermissionsLiveAgentUser: { + type: "boolean", + label: "Live Agent User", + description: + "Indicates whether the user is enabled to use Chat (true) or not (false).", + optional: true, + }, + UserPermissionsSFContentUser: { + type: "boolean", + label: "Salesforce CRM Content User", + description: + "Indicates whether the user is allocated one Salesforce CRM Content User License (true) or not (false).", + optional: true, + }, + UserPermissionsSiteforceContributorUser: { + type: "boolean", + label: "Site.com Contributor User", + description: + "Indicates whether the user is allocated one Site.com Contributor feature license (true) or not (false).", + optional: true, + }, + UserPermissionsSiteforcePublisherUser: { + type: "boolean", + label: "Site.com Publisher User", + description: + "Indicates whether the user is allocated one Site.com Publisher feature license (true) or not (false).", + optional: true, + }, + UserPermissionsSupportUser: { + type: "boolean", + label: "Service Cloud User", + description: "When true, the user can use the Salesforce console.", + optional: true, + }, + UserPermissionsWorkDotComUserFeature: { + type: "boolean", + label: "WDC User", + description: + "Indicates whether the WDC feature is enabled for the user (true) or not (false).", + optional: true, + }, + UserPreferencesActivityRemindersPopup: { + type: "boolean", + label: "ActivityRemindersPopup", + description: + "When true, a reminder window automatically opens when an activity reminder is due. Corresponds to the Trigger alert when reminder comes due checkbox at the Reminders page in the personal settings in the user interface.", + optional: true, + }, + UserPreferencesAllowConversationReminders: { + type: "boolean", + label: "Allow Conversation Reminders", + description: + "When true, voice and call reminders are displayed as notification cards in Lightning Experience. Corresponds to the Show conversation reminders in Lightning Experience checkbox in the Activity Reminders page in the personal settings in the user interface.", + optional: true, + }, + UserPreferencesApexPagesDeveloperMode: { + type: "boolean", + label: "Apex Pages Developer Mode", + description: + "When true, indicates that the user has enabled developer mode for editing Visualforce pages and controllers.", + optional: true, + }, + UserPreferencesAutoForwardCall: { + type: "boolean", + label: "Auto Forward Call", + description: + "When true, the user receives Dialer calls simultaneously in their browser and on their forwarding number.", + optional: true, + }, + UserPreferencesContentEmailAsAndWhen: { + type: "boolean", + label: "Content Email As And When", + description: + "When false, a user with Salesforce CRM Content subscriptions receives a once-daily email summary if activity occurs on the subscribed content, libraries, tags, or authors.", + optional: true, + }, + UserPreferencesContentNoEmail: { + type: "boolean", + label: "Content No Email", + description: + "When false, a user with Salesforce CRM Content subscriptions receives email notifications if activity occurs on the subscribed content, libraries, tags, or authors.", + optional: true, + }, + UserPreferencesEnableAutoSubForFeeds: { + type: "boolean", + label: "Enable Auto Sub For Feeds", + description: + "When true, the user automatically subscribes to feeds for any objects that the user creates.", + optional: true, + }, + UserPreferencesDisableAllFeedsEmail: { + type: "boolean", + label: "Disable All Feeds Email", + description: + "When false, the user automatically receives email for all updates to Chatter feeds, based on the types of feed emails and digests the user has enabled.", + optional: true, + }, + UserPreferencesDisableBookmarkEmail: { + type: "boolean", + label: "Disable Bookmark Email", + description: + "When false, the user automatically receives email every time someone comments on a Chatter feed item after the user has bookmarked it.", + optional: true, + }, + UserPreferencesDisableChangeCommentEmail: { + type: "boolean", + label: "Disable Change Comment Email", + description: + "When false, the user automatically receives email every time someone comments on a change the user has made, such as an update to their profile.", + optional: true, + }, + UserPreferencesDisableEndorsementEmail: { + type: "boolean", + label: "Disable Endorsement Email", + description: + "When false, the member automatically receives email every time someone endorses them for a topic.", + optional: true, + }, + UserPreferencesDisableFileShareNotificationsForApi: { + type: "boolean", + label: "Disable File Share Notifications For Api", + description: + "When false, email notifications are sent from the person who shared the file to the users that the file is shared with.", + optional: true, + }, + UserPreferencesDisableFollowersEmail: { + type: "boolean", + label: "Disable Followers Email", + description: + "When false, the user automatically receives email every time someone starts following the user in Chatter.", + optional: true, + }, + UserPreferencesDisableLaterCommentEmail: { + type: "boolean", + label: "Disable Later Comment Email", + description: + "When false, the user automatically receives email every time someone comments on a feed item after the user has commented on the feed item.", + optional: true, + }, + UserPreferencesDisableLikeEmail: { + type: "boolean", + label: "Disable Like Email", + description: + "When false, the user automatically receives email every time someone likes their post or comment.", + optional: true, + }, + UserPreferencesDisableMentionsPostEmail: { + type: "boolean", + label: "Disable Mentions Post Email", + description: + "When false, the user automatically receives email every time they're mentioned in posts.", + optional: true, + }, + UserPreferencesDisableProfilePostEmail: { + type: "boolean", + label: "Disable Profile Post Email", + description: + "When false, the user automatically receives email every time someone posts to the user's profile.", + optional: true, + }, + UserPreferencesDisableSharePostEmail: { + type: "boolean", + label: "Disable Share Post Email", + description: + "When false, the user automatically receives email every time their post is shared.", + optional: true, + }, + UserPreferencesDisableFeedbackEmail: { + type: "boolean", + label: "Disable Feedback Email", + description: + "When false, the user automatically receives emails related to WDC feedback. The user receives these emails when someone requests or offers feedback, shares feedback with the user, or reminds the user to answer a feedback request.", + optional: true, + }, + UserPreferencesDisCommentAfterLikeEmail: { + type: "boolean", + label: "Dis Comment After Like Email", + description: + "When false, the user automatically receives email every time someone comments on a post that the user liked.", + optional: true, + }, + UserPreferencesDisMentionsCommentEmail: { + type: "boolean", + label: "Dis Mentions Comment Email", + description: + "When false, the user automatically receives email every time the user is mentioned in comments.", + optional: true, + }, + UserPreferencesDisableMessageEmail: { + type: "boolean", + label: "Disable Message Email", + description: + "When false, the user automatically receives email for Chatter messages sent to the user.", + optional: true, + }, + UserPreferencesDisableRewardEmail: { + type: "boolean", + label: "Disable Reward Email", + description: + "When false, the user automatically receives emails related to WDC rewards. The user receives these emails when someone gives a reward to the user.", + optional: true, + }, + UserPreferencesDisableWorkEmail: { + type: "boolean", + label: "Disable Work Email", + description: + "When false, the user receives emails related to WDC feedback, goals, and coaching. The user must also sign up for individual emails listed on the WDC email settings page. When true, the user doesn't receive any emails related to WDC feedback, goals, or coaching even if they're signed up for individual emails.", + optional: true, + }, + UserPreferencesDisProfPostCommentEmail: { + type: "boolean", + label: "Dis Prof Post Comment Email", + description: + "When false, the user automatically receives email every time someone comments on posts on the user's profile.", + optional: true, + }, + UserPreferencesEnableVoiceCallRecording: { + type: "boolean", + label: "EnableVoiceCallRecording", + description: "When true, voice call recording is enabled for the user.", + optional: true, + }, + UserPreferencesEnableVoiceLocalPresence: { + type: "boolean", + label: "EnableVoiceLocalPresence", + description: + "When true, local numbers are shown when the user calls customers with Sales Dialer.", + optional: true, + }, + UserPreferencesEventRemindersCheckboxDefault: { + type: "boolean", + label: "Event Reminders Checkbox Default", + description: + "When true, a reminder popup is automatically set on the user's events. Corresponds to the `By default, set reminder on Events to...` checkbox on the Reminders page in the user interface. This field is related to UserPreference and customizing activity reminders.", + optional: true, + }, + UserPreferencesHideBiggerPhotoCallout: { + type: "boolean", + label: "Hide Bigger Photo Callout", + description: + "When true, users can choose to hide the callout text below the large profile photo.", + optional: true, + }, + UserPreferencesHideChatterOnboardingSplash: { + type: "boolean", + label: "Hide Chatter Onboarding Splash", + description: + "When true, the initial Chatter onboarding prompts don't appear.", + optional: true, + }, + UserPreferencesHideCSNDesktopTask: { + type: "boolean", + label: "Hide CSN Desktop Task", + description: + "When true, the Chatter recommendations panel never displays the recommendation to install Chatter Desktop.", + optional: true, + }, + UserPreferencesHideCSNGetChatterMobileTask: { + type: "boolean", + label: "Hide CSN Get Chatter Mobile Task", + description: + "When true, the Chatter recommendations panel never displays the recommendation to install Chatter Mobile.", + optional: true, + }, + UserPreferencesHideSecondChatterOnboardingSplash: { + type: "boolean", + label: "HideSecondChatterOnboardingSplash", + description: + "When true, the secondary Chatter onboarding prompts don't appear.", + optional: true, + }, + UserPreferencesHideS1BrowserUI: { + type: "boolean", + label: "Hide S1 Browser UI", + description: + "Controls the interface that the user sees when logging in to Salesforce from a supported mobile browser. If false, the user is automatically redirected to the Salesforce mobile web. If true, the user sees the full Salesforce site.", + optional: true, + }, + UserPreferencesHideSfxWelcomeMat: { + type: "boolean", + label: "Hide Sfx Welcome Mat", + description: + "Controls whether a user sees the Lightning Experience new user message. That message welcomes users to the new interface and provides step-by-step instructions that describe how to return to Salesforce Classic.", + optional: true, + }, + UserPreferencesJigsawListUser: { + type: "boolean", + label: "Data.com List User", + description: + "When true, the user is a Data.com List user so shares record additions from a pool.", + optional: true, + }, + UserPreferencesLightningExperiencePreferred: { + type: "boolean", + label: "Lightning Experience Preferred", + description: + "When true, redirects the user to the Lightning Experience interface. Label is Switch to Lightning Experience.", + optional: true, + }, + UserPreferencesLiveAgentMiawSetupDeflection: { + type: "boolean", + label: "Live Agent Miaw Setup Deflection", + description: + "When true, disables the pop-up to deflect users on Chat setup nodes to the Messaging setup. The default value is false.", + optional: true, + }, + UserPreferencesNativeEmailClient: { + type: "boolean", + label: "Native Email Client", + description: + "Use this field to set a default email preference for the user's native email client.", + optional: true, + }, + UserPreferencesOutboundBridge: { + type: "boolean", + label: "Outbound Bridge", + description: + "When true, outbound calls are made through the user's phone.", + optional: true, + }, + UserPreferencesPathAssistantCollapsed: { + type: "boolean", + label: "Path Assistant Collapsed", + description: + "When true, Sales Path appears collapsed or hidden to the user.", + optional: true, + }, + UserPreferencesReceiveNoNotificationsAsApprover: { + type: "boolean", + label: "Receive No Notifications As Approver", + description: + "Controls email notifications from the approval process for approvers. If true, emails are disabled. If false, emails are enabled. The default value is false.", + optional: true, + }, + UserPreferencesReceiveNotificationsAsDelegatedApprover: { + type: "boolean", + label: "Receive Notifications As Delegated Approver", + description: + "Controls email notifications from the approval process for delegated approvers. If true, emails are enabled. If false, emails are disabled. The default value is false.", + optional: true, + }, + UserPreferencesReminderSoundOff: { + type: "boolean", + label: "Reminde Sound Off", + description: + "When true, a sound automatically plays when an activity reminder is due. Corresponds to the `Play a reminder sound` checkbox on the Reminders page in the user interface.", + optional: true, + }, + UserPreferencesShowCityToExternalUsers: { + type: "boolean", + label: "Show City To External Users", + description: + "Indicates the visibility of the city field in the user's contact information. ", + optional: true, + }, + UserPreferencesShowCityToGuestUsers: { + type: "boolean", + label: "Show City To Guest Users", + description: + "Indicates the visibility of the city field in the user's contact information.", + optional: true, + }, + UserPreferencesShowCountryToExternalUsers: { + type: "boolean", + label: "Show Country To External Users", + description: + "Indicates the visibility of the country field in the user's contact information.", + optional: true, + }, + UserPreferencesShowCountryToGuestUsers: { + type: "boolean", + label: "Show Country To Guest Users", + description: + "Indicates the visibility of the country field in the user's contact information.", + optional: true, + }, + UserPreferencesShowEmailToExternalUsers: { + type: "boolean", + label: "Show Email To External Users", + description: + "Indicates the visibility of the email address field in the user's contact information.", + optional: true, + }, + UserPreferencesShowEmailToGuestUsers: { + type: "boolean", + label: "Show Email To Guest Users", + description: + "Indicates the visibility of the email address field in the user's contact information.", + optional: true, + }, + UserPreferencesShowFaxToExternalUsers: { + type: "boolean", + label: "Show Fax To External Users", + description: + "Indicates the visibility of the fax number field in the user's contact information.", + optional: true, + }, + UserPreferencesShowFaxToGuestUsers: { + type: "boolean", + label: "Show Fax To Guest Users", + description: + "Indicates the visibility of the fax number field in the user's contact information.", + optional: true, + }, + UserPreferencesShowManagerToExternalUsers: { + type: "boolean", + label: "Show Manager To External Users", + description: + "Indicates the visibility of the manager field in the user's contact information.", + optional: true, + }, + UserPreferencesShowManagerToGuestUsers: { + type: "boolean", + label: "Show Manager To Guest Users", + description: + "Indicates the visibility of the manager field in the user's contact information..", + optional: true, + }, + UserPreferencesShowMobilePhoneToExternalUsers: { + type: "boolean", + label: "Show Mobile Phone To External Users", + description: + "Indicates the visibility of the mobile device number field in the user's contact information.", + optional: true, + }, + UserPreferencesShowMobilePhoneToGuestUsers: { + type: "boolean", + label: "Show Mobile Phone To Guest Users", + description: + "Indicates the visibility of the mobile phone field in the user's contact information.", + optional: true, + }, + UserPreferencesShowPostalCodeToExternalUsers: { + type: "boolean", + label: "Show Postal Code To External Users", + description: + "Indicates the visibility of the postal or ZIP code field in the user's contact information.", + optional: true, + }, + UserPreferencesShowPostalCodeToGuestUsers: { + type: "boolean", + label: "Show Postal Code To Guest Users", + description: + "Indicates the visibility of the postal or ZIP code field in the user's contact information.", + optional: true, + }, + UserPreferencesShowProfilePicToGuestUsers: { + type: "boolean", + label: "Show Profile Pic To Guest Users", + description: + "Indicates the visibility of the user's profile photo.", + optional: true, + }, + UserPreferencesShowStateToExternalUsers: { + type: "boolean", + label: "Show State To External Users", + description: + "Indicates the visibility of the state field in the user's contact information.", + optional: true, + }, + UserPreferencesShowStateToGuestUsers: { + type: "boolean", + label: "Show State To Guest Users", + description: + "Indicates the visibility of the state field in the user's contact information.", + optional: true, + }, + UserPreferencesShowStreetAddressToExternalUsers: { + type: "boolean", + label: "Show Street Address To External Users", + description: + "Indicates the visibility of the street address field in the user's contact information.", + optional: true, + }, + UserPreferencesShowStreetAddressToGuestUsers: { + type: "boolean", + label: "Show Street Address To Guest Users", + description: + "Indicates the visibility of the street address field in the user's contact information.", + optional: true, + }, + UserPreferencesShowTitleToExternalUsers: { + type: "boolean", + label: "Show Title To External Users", + description: + "Indicates the visibility of the business title field in the user's contact information.", + optional: true, + }, + UserPreferencesShowTitleToGuestUsers: { + type: "boolean", + label: "Show Title To Guest Users", + description: + "Indicates the visibility of the business title field in the user's contact information.", + optional: true, + }, + UserPreferencesShowWorkPhoneToExternalUsers: { + type: "boolean", + label: "Show Work Phone To External Users", + description: + "Indicates the visibility of the work phone number field in the user's contact information.", + optional: true, + }, + UserPreferencesShowWorkPhoneToGuestUsers: { + type: "boolean", + label: "Show Work Phone To Guest Users", + description: + "Indicates the visibility of the work phone field in the user's contact information.", + optional: true, + }, + UserPreferencesSortFeedByComment: { + type: "boolean", + label: "Sort Feed By Comment", + description: + "Specifies the data value used in sorting a user's feed. When true, the feed is sorted by most recent comment activity. When false, the feed is sorted by post date.", + optional: true, + }, + UserPreferencesSuppressEventSFXReminders: { + type: "boolean", + label: "Suppress Event SFX Reminders", + description: + "When true, event reminders don't appear. Corresponds to the Show event reminders in Lightning Experience checkbox on the Activity Reminders page in the user interface.", + optional: true, + }, + UserPreferencesSuppressTaskSFXReminders: { + type: "boolean", + label: "Suppress Task SFX Reminders", + description: + "When true, task reminders don't appear. Corresponds to the `Show task reminders in Lightning Experience.` checkbox on the Activity Reminders page in the user interface.", + optional: true, + }, + UserPreferencesTaskRemindersCheckboxDefault: { + type: "boolean", + label: "Task Reminders Checkbox Default", + description: + "When true, a reminder popup is automatically set on the user's tasks. Corresponds to the `By default, set reminder on Tasks to...` checkbox on the Reminders page in the user interface.", + optional: true, + }, + UserPreferencesUserDebugModePref: { + type: "boolean", + label: "User Debug Mode Pref", + description: + "When true, the Lightning Component framework executes in debug mode for the user. Corresponds to the `Debug Mode` checkbox on the Advanced User Details page of personal settings in the user interface.", + optional: true, + }, + }, +}; diff --git a/components/salesforce_rest_api/common/utils.mjs b/components/salesforce_rest_api/common/utils.mjs deleted file mode 100644 index 78d8967d318a5..0000000000000 --- a/components/salesforce_rest_api/common/utils.mjs +++ /dev/null @@ -1,51 +0,0 @@ -/** - * A utility function that accepts a string as an argument and reformats it in - * order to remove newline characters and consecutive spaces. Useful when - * dealing with very long templated strings that are split into multiple lines. - * - * @example - * // returns "This is a much cleaner string" - * toSingleLineString(` - * This is a much - * cleaner string - * `); - * - * @param {string} multiLineString the input string to reformat - * @returns a formatted string based on the content of the input argument, - * without newlines and multiple spaces - * Source: {@linkcode ../aws/sources/common/utils.mjs utils.mjs} - */ -function toSingleLineString(multiLineString) { - return multiLineString - .trim() - .replace(/\n/g, " ") - .replace(/\s{2,}/g, " "); -} - -const removeNullEntries = (obj) => - obj && Object.entries(obj).reduce((acc, [ - key, - value, - ]) => { - const isNumber = typeof value === "number"; - const isBoolean = typeof value === "boolean"; - const isNotEmpyString = typeof value === "string" && value.trim() !== ""; - const isNotEmptyArray = Array.isArray(value) && value.length; - const isNotEmptyObject = - typeof value === "object" && - value !== null && - !Array.isArray(value) && - Object.keys(value).length !== 0; - isNotEmptyObject && (value = removeNullEntries(value)); - return ((value || value === false) && - (isNotEmpyString || isNotEmptyArray || isNotEmptyObject || isBoolean || isNumber)) - ? { - ...acc, - [key]: value, - } - : acc; - }, {}); - -export { - toSingleLineString, removeNullEntries, -}; diff --git a/components/salesforce_rest_api/package.json b/components/salesforce_rest_api/package.json index 3cdfe0cb0335f..f95719a9bf08b 100644 --- a/components/salesforce_rest_api/package.json +++ b/components/salesforce_rest_api/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/salesforce_rest_api", - "version": "1.2.1", + "version": "1.5.0", "description": "Pipedream Salesforce (REST API) Components", "main": "salesforce_rest_api.app.mjs", "keywords": [ @@ -10,7 +10,7 @@ "homepage": "https://pipedream.com/apps/salesforce_rest_api", "author": "Pipedream (https://pipedream.com/)", "dependencies": { - "@pipedream/platform": "^1.2.0", + "@pipedream/platform": "^3.0.0", "fast-xml-parser": "^4.3.2", "handlebars": "^4.7.7", "lodash": "^4.17.21", diff --git a/components/salesforce_rest_api/salesforce_rest_api.app.mjs b/components/salesforce_rest_api/salesforce_rest_api.app.mjs index 432218f1b751d..cdf27c39ea0fc 100644 --- a/components/salesforce_rest_api/salesforce_rest_api.app.mjs +++ b/components/salesforce_rest_api/salesforce_rest_api.app.mjs @@ -4,19 +4,6 @@ export default { type: "app", app: "salesforce_rest_api", propDefinitions: { - sobjectId: { - type: "string", - label: "SObject ID", - description: "ID of the Standard object to get field values from", - async options(context) { - const { objectType } = context; - const { recentItems } = await this.listSObjectTypeIds(objectType); - return recentItems.map((item) => ({ - label: item.Name, - value: item.Id, - })); - }, - }, objectType: { type: "string", label: "SObject Type", @@ -38,6 +25,21 @@ export default { return sobjects.filter(filter).map(mapper); }, }, + recordId: { + type: "string", + label: "Record ID", + description: "The ID of the record of the selected object type.", + async options({ + objType, + nameField, + }) { + if (!nameField) nameField = await this.getNameFieldForObjectType(objType); + return this.listRecordOptions({ + objType, + nameField, + }); + }, + }, field: { type: "string", label: "Field", @@ -53,37 +55,30 @@ export default { return fields.filter(filter).map(({ name }) => name); }, }, - fieldUpdatedTo: { - type: "string", - label: "Field Updated to", - description: - "If provided, the trigger will only fire when the updated field is an EXACT MATCH (including spacing and casing) to the value you provide in this field", - optional: true, - }, - fieldSelector: { + fieldsToUpdate: { type: "string[]", - label: "Field Selector", - description: "Select fields for the Standard Object", - options: () => [], // override options for each object, e.g., () => Object.keys(account) + label: "Fields to Update", + description: "Select the field(s) you want to update for this record.", + async options({ objType }) { + const fields = await this.getFieldsForObjectType(objType); + return fields.filter((field) => field.updateable).map(({ name }) => name); + }, }, - AcceptedEventInviteeIds: { + fieldsToObtain: { type: "string[]", - label: "Accepted Event Invitee IDs", - async options() { - const { recentItems: contacts } = await this.listSObjectTypeIds("Contact"); - const { recentItems: leads } = await this.listSObjectTypeIds("Lead"); - const allContacts = [ - ...contacts, - ...leads, - ]; - return allContacts.map(({ - Name, Id, - }) => ({ - label: Name, - value: Id, - })); + label: "Fields to Obtain", + description: "Select the field(s) to obtain for the selected record(s) (or all records).", + async options({ objType }) { + const fields = await this.getFieldsForObjectType(objType); + return fields.map(({ name }) => name); }, - description: "A string array of contact or lead IDs who accepted this event. This JunctionIdList is linked to the AcceptedEventRelation child relationship. Warning Adding a JunctionIdList field name to the fieldsToNull property deletes all related junction records. This action can't be undone.", + }, + useAdvancedProps: { + type: "boolean", + label: "See All Props", + description: "Set to true to see all available props for this object.", + optional: true, + reloadProps: true, }, }, methods: { @@ -138,10 +133,6 @@ export default { const baseUrl = this._sObjectTypeApiUrl(sObjectType); return `${baseUrl}/describe`; }, - _sObjectTypeUpdatedApiUrl(sObjectType) { - const baseUrl = this._sObjectTypeApiUrl(sObjectType); - return `${baseUrl}/updated`; - }, _sObjectTypeDeletedApiUrl(sObjectType) { const baseUrl = this._sObjectTypeApiUrl(sObjectType); return `${baseUrl}/deleted`; @@ -223,35 +214,32 @@ export default { }); return data.fields; }, - async getHistorySObjectForObjectType(objectType) { - const { sobjects } = await this.listSObjectTypes(); - const historyObject = sobjects.find( - (sobject) => - sobject.associateParentEntity === objectType && - this.isHistorySObject(sobject), - ); - return historyObject; - }, - async createObject(objectType, data) { - const url = `${this._sObjectsApiUrl()}/${objectType}`; + async createObject({ + objectType, ...args + }) { return this._makeRequest({ - url, - data, + url: `${this._sObjectsApiUrl()}/${objectType}`, method: "POST", + ...args, }); }, - async deleteObject(objectType, sobjectId) { - const url = `${this._sObjectsApiUrl()}/${objectType}/${sobjectId}`; + async deleteRecord({ + sobjectType, recordId, ...args + }) { + const url = `${this._sObjectsApiUrl()}/${sobjectType}/${recordId}`; return this._makeRequest({ url, method: "DELETE", + ...args, }); }, - async getRecords(objectType, params) { + async getRecords({ + objectType, ...args + }) { const url = `${this._sCompositeApiUrl()}/${objectType}`; return this._makeRequest({ url, - params, + ...args, }); }, async getSObject(objectType, id, params = null) { @@ -261,17 +249,6 @@ export default { params, }); }, - async getUpdatedForObjectType(objectType, start, end) { - const url = this._sObjectTypeUpdatedApiUrl(objectType); - const params = { - start: this._formatDateString(start), - end: this._formatDateString(end), - }; - return this._makeRequest({ - url, - params, - }); - }, async getDeletedForObjectType(objectType, start, end) { const url = this._sObjectTypeDeletedApiUrl(objectType); const params = { @@ -293,159 +270,6 @@ export default { }, }); }, - async createAccount({ - $, data, - }) { - const url = this._sObjectTypeApiUrl("Account"); - return this._makeRequest({ - $, - url, - method: "POST", - data, - }); - }, - async updateAccount({ - $, id, data, - }) { - const url = this._sObjectDetailsApiUrl("Account", id); - return this._makeRequest({ - $, - url, - method: "PATCH", - data, - }); - }, - async createAttachment({ - $, data, - }) { - const url = this._sObjectTypeApiUrl("Attachment"); - return this._makeRequest({ - $, - url, - method: "POST", - data, - }); - }, - async createCampaign({ - $, data, - }) { - const url = this._sObjectTypeApiUrl("Campaign"); - return this._makeRequest({ - $, - url, - method: "POST", - data, - }); - }, - async createCase({ - $, data, - }) { - const url = this._sObjectTypeApiUrl("Case"); - return this._makeRequest({ - $, - url, - method: "POST", - data, - }); - }, - async createCaseComment({ - $, data, - }) { - const url = this._sObjectTypeApiUrl("CaseComment"); - return this._makeRequest({ - $, - url, - method: "POST", - data, - }); - }, - async createContact({ - $, data, - }) { - const url = this._sObjectTypeApiUrl("Contact"); - return this._makeRequest({ - $, - url, - method: "POST", - data, - }); - }, - async updateContact({ - $, id, data, - }) { - const url = this._sObjectDetailsApiUrl("Contact", id); - return this._makeRequest({ - $, - url, - method: "PATCH", - data, - }); - }, - async createEvent({ - $, data, - }) { - const url = this._sObjectTypeApiUrl("Event"); - return this._makeRequest({ - $, - url, - method: "POST", - data, - }); - }, - async createLead({ - $, data, - }) { - const url = this._sObjectTypeApiUrl("Lead"); - return this._makeRequest({ - $, - url, - method: "POST", - data, - }); - }, - async createNote({ - $, data, - }) { - const url = this._sObjectTypeApiUrl("Note"); - return this._makeRequest({ - $, - url, - method: "POST", - data, - }); - }, - async createOpportunity({ - $, data, - }) { - const url = this._sObjectTypeApiUrl("Opportunity"); - return this._makeRequest({ - $, - url, - method: "POST", - data, - }); - }, - async updateOpportunity({ - $, id, data, - }) { - const url = this._sObjectDetailsApiUrl("Opportunity", id); - return this._makeRequest({ - $, - url, - method: "PATCH", - data, - }); - }, - async getRecordFieldValues(sobjectName, { - $, id, params, - }) { - const url = this._sObjectDetailsApiUrl(sobjectName, id); - return this._makeRequest({ - $, - url, - params, - }); - }, async createRecord(sobjectName, { $, data, }) { @@ -468,27 +292,6 @@ export default { data, }); }, - async createTask({ - $, data, - }) { - const url = this._sObjectTypeApiUrl("Task"); - return this._makeRequest({ - $, - url, - method: "POST", - data, - }); - }, - async deleteOpportunity({ - $, id, - }) { - const url = this._sObjectDetailsApiUrl("Opportunity", id); - return this._makeRequest({ - $, - url, - method: "DELETE", - }); - }, async query({ $, query, }) { @@ -499,6 +302,26 @@ export default { url, }); }, + async listRecordOptions({ + objType, + nameField = "Id", + }) { + const fields = [ + "Id", + ...nameField === "Id" + ? [] + : [ + nameField, + ], + ]; + const { records } = await this.query({ + query: `SELECT ${fields.join(", ")} FROM ${objType}`, + }); + return records?.map?.((item) => ({ + label: item[nameField], + value: item.Id, + })) ?? []; + }, async search({ $, search, }) { @@ -509,14 +332,14 @@ export default { url, }); }, - async parameterizedSearch(params) { + async parameterizedSearch(args) { const baseUrl = this._baseApiVersionUrl(); const url = `${baseUrl}/parameterizedSearch/`; return this._makeRequest({ url, method: "GET", - params, + ...args, }); }, async insertBlobData(sobjectName, { @@ -534,7 +357,7 @@ export default { }; return axios($ ?? this, requestConfig); }, - async postFeed(args = {}) { + async postFeed(args) { const baseUrl = this._baseApiVersionUrl(); const url = `${baseUrl}/chatter/feed-elements`; return this._makeRequest({ diff --git a/components/salesforce_rest_api/sources/common-instant.mjs b/components/salesforce_rest_api/sources/common-instant.mjs deleted file mode 100644 index 68334cdf739ad..0000000000000 --- a/components/salesforce_rest_api/sources/common-instant.mjs +++ /dev/null @@ -1,146 +0,0 @@ -import { v4 as uuidv4 } from "uuid"; -import salesforce from "../salesforce_rest_api.app.mjs"; -import salesforceWebhooks from "salesforce-webhooks"; - -const { SalesforceClient } = salesforceWebhooks; - -export default { - dedupe: "unique", - props: { - salesforce, - db: "$.service.db", - // eslint-disable-next-line pipedream/props-label,pipedream/props-description - http: { - type: "$.interface.http", - customResponse: true, - }, - objectType: { - label: "Object Type", - description: "The type of object for which to monitor events", - propDefinition: [ - salesforce, - "objectType", - ], - }, - }, - hooks: { - async activate() { - // Retrieve metadata about the SObject specified by the user - const nameField = await this.salesforce.getNameFieldForObjectType(this.objectType); - this.setNameField(nameField); - - // Create the webhook in the Salesforce platform - const secretToken = uuidv4(); - let webhookData; - try { - webhookData = await this.createWebhook({ - endpointUrl: this.http.endpoint, - sObjectType: this.objectType, - event: this.getEventType(), - secretToken, - fieldsToCheck: this.getFieldsToCheck(), - fieldsToCheckMode: this.getFieldsToCheckMode(), - skipValidation: true, // neccessary for custom objects - }); - } catch (err) { - console.log("Create webhook error:", err); - throw err; - } - this._setSecretToken(secretToken); - this._setWebhookData(webhookData); - }, - async deactivate() { - // Create the webhook from the Salesforce platform - const webhookData = this._getWebhookData(); - await this.deleteWebhook(webhookData); - }, - }, - methods: { - getClient() { - const { salesforce } = this; - return new SalesforceClient({ - apiVersion: salesforce._apiVersion(), - authToken: salesforce._authToken(), - instance: salesforce._subdomain(), - }); - }, - createWebhook(args = {}) { - const client = this.getClient(); - return client.createWebhook(args); - }, - deleteWebhook(args = {}) { - const client = this.getClient(); - return client.deleteWebhook(args); - }, - _getSecretToken() { - return this.db.get("secretToken"); - }, - _setSecretToken(secretToken) { - this.db.set("secretToken", secretToken); - }, - _getWebhookData() { - return this.db.get("webhookData"); - }, - _setWebhookData(webhookData) { - this.db.set("webhookData", webhookData); - }, - getNameField() { - return this.db.get("nameField"); - }, - setNameField(nameField) { - this.db.set("nameField", nameField); - }, - _isValidSource(event) { - const webhookToken = event.headers["x-webhook-token"]; - const secretToken = this._getSecretToken(); - return webhookToken === secretToken; - }, - processEvent(event) { - const { body } = event; - const meta = this.generateMeta(event); - this.$emit(body, meta); - }, - generateMeta() { - throw new Error("generateMeta is not implemented"); - }, - getEventType() { - throw new Error("getEventType is not implemented"); - }, - /** - * This method returns the fields in the SObject type (e.g. Account, Lead, etc.) that the event - * source should listen for updates to. This base implementation returns `undefined`, to not - * necessitate any specific fields to be updated. - * - * @returns the fields in the SObject type for which to receive updates - */ - getFieldsToCheck() { - return undefined; - }, - /** - * This method returns whether the event source should listen for updates where `all` the fields - * in the SObject are updated, or when `any` of them are. This base implementation returns - * `undefined` to use to client's default `fieldToCheckMode` (`any`). - * - * @returns whether the webhook should receive events when `all` the fields to check are - * updated, or when `any` of them are - */ - getFieldsToCheckMode() { - return undefined; - }, - }, - async run(event) { - if (!this._isValidSource(event)) { - this.http.respond({ - statusCode: 404, - }); - console.log("Skipping event from unrecognized source"); - return; - } - - this.http.respond({ - statusCode: 200, - }); - - await this.processEvent(event); - }, -}; diff --git a/components/salesforce_rest_api/sources/common-webhook-methods.mjs b/components/salesforce_rest_api/sources/common-webhook-methods.mjs new file mode 100644 index 0000000000000..7e65d8842e129 --- /dev/null +++ b/components/salesforce_rest_api/sources/common-webhook-methods.mjs @@ -0,0 +1,71 @@ +import salesforceWebhooks from "salesforce-webhooks"; + +const { SalesforceClient } = salesforceWebhooks; + +export default { + getClient() { + const { salesforce } = this; + return new SalesforceClient({ + apiVersion: salesforce._apiVersion(), + authToken: salesforce._authToken(), + instance: salesforce._subdomain(), + }); + }, + createWebhook(args = {}) { + const client = this.getClient(); + return client.createWebhook(args); + }, + deleteWebhook(args = {}) { + const client = this.getClient(); + return client.deleteWebhook(args); + }, + _getSecretToken() { + return this.db.get("secretToken"); + }, + _setSecretToken(secretToken) { + this.db.set("secretToken", secretToken); + }, + _getWebhookData() { + return this.db.get("webhookData"); + }, + _setWebhookData(webhookData) { + this.db.set("webhookData", webhookData); + }, + _isValidSource(event) { + const webhookToken = event.headers["x-webhook-token"]; + const secretToken = this._getSecretToken(); + return webhookToken === secretToken; + }, + processWebhookEvent(event) { + const { body } = event; + const meta = this.generateWebhookMeta(event); + this.$emit(body, meta); + }, + generateWebhookMeta() { + throw new Error("generateWebhookMeta is not implemented"); + }, + getEventType() { + throw new Error("getEventType is not implemented"); + }, + /** + * This method returns the fields in the SObject type (e.g. Account, Lead, etc.) that the event + * source should listen for updates to. This base implementation returns `undefined`, to not + * necessitate any specific fields to be updated. + * + * @returns the fields in the SObject type for which to receive updates + */ + getFieldsToCheck() { + return undefined; + }, + /** + * This method returns whether the event source should listen for updates where `all` the fields + * in the SObject are updated, or when `any` of them are. This base implementation returns + * `undefined` to use to client's default `fieldToCheckMode` (`any`). + * + * @returns whether the webhook should receive events when `all` the fields to check are + * updated, or when `any` of them are + */ + getFieldsToCheckMode() { + return undefined; + }, +}; diff --git a/components/salesforce_rest_api/sources/common.mjs b/components/salesforce_rest_api/sources/common.mjs index 2dc2c4f45d42c..38dbf81babe83 100644 --- a/components/salesforce_rest_api/sources/common.mjs +++ b/components/salesforce_rest_api/sources/common.mjs @@ -1,21 +1,26 @@ import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; import salesforce from "../salesforce_rest_api.app.mjs"; import constants from "../common/constants.mjs"; +import { v4 as uuidv4 } from "uuid"; +import commonWebhookMethods from "./common-webhook-methods.mjs"; export default { dedupe: "unique", props: { salesforce, db: "$.service.db", - // eslint-disable-next-line pipedream/props-label,pipedream/props-description + http: { + type: "$.interface.http", + customResponse: true, + }, timer: { type: "$.interface.timer", + description: "The timer is only used as a fallback if instant event delivery (webhook) is not available.", default: { intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, }, }, objectType: { - type: "string", label: "Object Type", description: "The type of object for which to monitor events", propDefinition: [ @@ -26,17 +31,51 @@ export default { }, hooks: { async activate() { - const latestDateCovered = this.getLatestDateCovered(); - if (!latestDateCovered) { - const now = new Date().toISOString(); - this.setLatestDateCovered(now); + // Attempt to create the webhook + const secretToken = uuidv4(); + let webhookData; + try { + webhookData = await this.createWebhook({ + endpointUrl: this.http.endpoint, + sObjectType: this.objectType, + event: this.getEventType(), + secretToken, + fieldsToCheck: this.getFieldsToCheck(), + fieldsToCheckMode: this.getFieldsToCheckMode(), + skipValidation: true, // neccessary for custom objects + }); + console.log("Webhook created successfully"); + } catch (err) { + console.log("Error creating webhook:", err); + console.log("The source will operate on the polling schedule instead."); + + const latestDateCovered = this.getLatestDateCovered(); + if (!latestDateCovered) { + const now = new Date().toISOString(); + this.setLatestDateCovered(now); + } + + await this.timerActivateHook?.(); } + this._setSecretToken(secretToken); + this._setWebhookData(webhookData); const nameField = await this.salesforce.getNameFieldForObjectType(this.objectType); this.setNameField(nameField); }, + async deactivate() { + // Delete the webhook, if it exists + const webhookData = this._getWebhookData(); + if (webhookData) { + await this.deleteWebhook(webhookData); + } + }, }, methods: { + ...commonWebhookMethods, + timerActivateHook() { + return null; + }, getObjectTypeColumns() { return this.db.get("columns") ?? []; }, @@ -55,8 +94,8 @@ export default { setNameField(nameField) { this.db.set("nameField", nameField); }, - processEvent() { - throw new Error("processEvent is not implemented"); + processTimerEvent() { + throw new Error("processTimerEvent is not implemented"); }, getObjectTypeDescription(objectType) { const { salesforce } = this; @@ -120,21 +159,45 @@ export default { }, }, async run(event) { - const startTimestamp = this.getLatestDateCovered(); - const endTimestamp = new Date(event.timestamp * 1000).toISOString(); - const timeDiffSec = Math.floor( - (Date.parse(endTimestamp) - Date.parse(startTimestamp)) / 1000, - ); - if (timeDiffSec < 60) { - console.log(` - Skipping execution since the last one happened approximately ${timeDiffSec} seconds ago - `); - return; + // Timer event + if (event.timestamp) { + if (this._getWebhookData()) { + console.log("Ignoring timer event (webhook active)"); + return; + } + const startTimestamp = this.getLatestDateCovered(); + const endTimestamp = new Date(event.timestamp * 1000).toISOString(); + const timeDiffSec = Math.floor( + (Date.parse(endTimestamp) - Date.parse(startTimestamp)) / 1000, + ); + if (timeDiffSec < 60) { + console.log(` + Skipping execution (already executed less than 60 seconds ago) + `); + return; + } + + await this.processTimerEvent({ + startTimestamp, + endTimestamp, + }); } - await this.processEvent({ - startTimestamp, - endTimestamp, - }); + // Webhook event + else { + if (!this._isValidSource(event)) { + this.http.respond({ + statusCode: 404, + }); + console.log("Skipping event from unrecognized source"); + return; + } + + this.http.respond({ + statusCode: 200, + }); + + await this.processWebhookEvent(event); + } }, }; diff --git a/components/salesforce_rest_api/sources/new-outbound-message/new-outbound-message.mjs b/components/salesforce_rest_api/sources/new-outbound-message/new-outbound-message.mjs index b1067ea92de9f..a0d232dcbb309 100644 --- a/components/salesforce_rest_api/sources/new-outbound-message/new-outbound-message.mjs +++ b/components/salesforce_rest_api/sources/new-outbound-message/new-outbound-message.mjs @@ -5,17 +5,25 @@ export default { type: "source", name: "New Outbound Message (Instant)", key: "salesforce_rest_api-new-outbound-message", - description: "Emit new event when a new outbound message is received in Salesforce. See Salesforce's guide on setting up [Outbound Messaging](https://sforce.co/3JbZJom). Set the Outbound Message's Endpoint URL to the endpoint of the created source. The \"Send Session ID\" option must be enabled for validating outbound messages from Salesforce.", - version: "0.1.6", + description: "Emit new event when a new outbound message is received in Salesforce.", + version: "0.1.7", dedupe: "unique", props: { db: "$.service.db", - // eslint-disable-next-line pipedream/props-label,pipedream/props-description http: { type: "$.interface.http", customResponse: true, }, salesforce, + infoBox: { + type: "alert", + alertType: "info", + content: `See Salesforce's guide on [setting up Outbound Messaging](https://sforce.co/3JbZJom). +\\ +Set the Outbound Message's \`Endpoint URL\` to the endpoint of this source, which you can view after it is created. +\\ +The \`Send Session ID\` option must be enabled in Salesforce for validating outbound messages.`, + }, }, methods: { _unwrapMessage(message) { diff --git a/components/salesforce_rest_api/sources/new-record-instant/new-record-instant.mjs b/components/salesforce_rest_api/sources/new-record-instant/new-record-instant.mjs index a2f13fbdaafdd..382ba6a826ba5 100644 --- a/components/salesforce_rest_api/sources/new-record-instant/new-record-instant.mjs +++ b/components/salesforce_rest_api/sources/new-record-instant/new-record-instant.mjs @@ -1,13 +1,27 @@ import startCase from "lodash/startCase.js"; -import common from "../common-instant.mjs"; +import common from "../common.mjs"; export default { ...common, type: "source", name: "New Record (Instant, of Selectable Type)", key: "salesforce_rest_api-new-record-instant", - description: "Emit new event immediately after a record of arbitrary object type (selected as an input parameter by the user) is created", - version: "0.0.4", + description: "Emit new event when a record of the selected object type is created. [See the documentation](https://sforce.co/3yPSJZy)", + version: "0.2.0", + props: { + ...common.props, + fieldsToObtain: { + propDefinition: [ + common.props.salesforce, + "fieldsToObtain", + (c) => ({ + objType: c.objectType, + }), + ], + optional: true, + description: "Select the field(s) to be retrieved for the records. Only applicable if the source is running on a timer. If running on a webhook, or if not specified, all fields will be retrieved.", + }, + }, hooks: { ...common.hooks, async deploy() { @@ -15,7 +29,7 @@ export default { const nameField = await this.salesforce.getNameFieldForObjectType(objectType); this.setNameField(nameField); - // emit hisorical events + // emit historical events const { recentItems } = await this.salesforce.listSObjectTypeIds(objectType); const ids = recentItems.map((item) => item.Id); for (const id of ids.slice(-25)) { @@ -26,13 +40,29 @@ export default { "UserId": id, }, }; - this.processEvent(event); + this.processWebhookEvent(event); } }, }, methods: { ...common.methods, - generateMeta(data) { + generateTimerMeta(item, fieldName) { + const { objectType } = this; + const { + CreatedDate: createdDate, + [fieldName]: name, + Id: id, + } = item; + const entityType = startCase(objectType); + const summary = `New ${entityType} created: ${name ?? id}`; + const ts = Date.parse(createdDate); + return { + id, + summary, + ts, + }; + }, + generateWebhookMeta(data) { const nameField = this.getNameField(); const { New: newObject } = data.body; const { @@ -41,7 +71,7 @@ export default { [nameField]: name, } = newObject; const entityType = startCase(this.objectType).toLowerCase(); - const summary = `New ${entityType} created: ${name}`; + const summary = `New ${entityType} created: ${name ?? id}`; const ts = Date.parse(createdDate); return { id, @@ -52,5 +82,63 @@ export default { getEventType() { return "new"; }, + async processTimerEvent(eventData) { + const { + paginate, + objectType, + setLatestDateCovered, + getObjectTypeColumns, + getNameField, + generateTimerMeta, + $emit: emit, + } = this; + + const { + startTimestamp, + endTimestamp, + } = eventData; + + const fieldName = getNameField(); + const columns = getObjectTypeColumns(); + + const events = await paginate({ + objectType, + startTimestamp, + endTimestamp, + columns, + }); + + const [ + latestEvent, + ] = events; + + if (latestEvent?.CreatedDate) { + const latestDateCovered = new Date(latestEvent.CreatedDate); + latestDateCovered.setSeconds(0); + setLatestDateCovered(latestDateCovered.toISOString()); + } + + Array.from(events) + .reverse() + .forEach((item) => { + const meta = generateTimerMeta(item, fieldName); + emit(item, meta); + }); + }, + async timerActivateHook() { + const { + objectType, + getObjectTypeDescription, + setObjectTypeColumns, + } = this; + + let columns = this.fieldsToObtain; + if (!columns?.length) { + const { fields } = await getObjectTypeDescription(objectType); + columns = fields.map(({ name }) => name); + } + + setObjectTypeColumns(columns); + }, }, }; diff --git a/components/salesforce_rest_api/sources/new-record/new-record.mjs b/components/salesforce_rest_api/sources/new-record/new-record.mjs deleted file mode 100644 index c53ae8f19e824..0000000000000 --- a/components/salesforce_rest_api/sources/new-record/new-record.mjs +++ /dev/null @@ -1,91 +0,0 @@ -import startCase from "lodash/startCase.js"; - -import common from "../common.mjs"; - -export default { - ...common, - type: "source", - name: "New Record (of Selectable Type)", - key: "salesforce_rest_api-new-record", - description: "Emit new event (at regular intervals) when a record of arbitrary object type (selected as an input parameter by the user) is created. See [the docs](https://sforce.co/3yPSJZy) for more information.", - version: "0.0.6", - hooks: { - ...common.hooks, - async activate() { - const { - objectType, - getObjectTypeDescription, - setObjectTypeColumns, - } = this; - - await common.hooks.activate.call(this); - - const { fields } = await getObjectTypeDescription(objectType); - const columns = fields.map(({ name }) => name); - - setObjectTypeColumns(columns); - }, - }, - methods: { - ...common.methods, - generateMeta(item, fieldName) { - const { objectType } = this; - const { - CreatedDate: createdDate, - [fieldName]: name, - Id: id, - } = item; - const entityType = startCase(objectType); - const summary = `New ${entityType} created: ${name}`; - const ts = Date.parse(createdDate); - return { - id, - summary, - ts, - }; - }, - async processEvent(eventData) { - const { - paginate, - objectType, - setLatestDateCovered, - getObjectTypeColumns, - getNameField, - generateMeta, - $emit: emit, - } = this; - - const { - startTimestamp, - endTimestamp, - } = eventData; - - const fieldName = getNameField(); - const columns = getObjectTypeColumns(); - - const events = await paginate({ - objectType, - startTimestamp, - endTimestamp, - columns, - }); - - const [ - latestEvent, - ] = events; - - if (latestEvent?.CreatedDate) { - const latestDateCovered = new Date(latestEvent.CreatedDate); - latestDateCovered.setSeconds(0); - setLatestDateCovered(latestDateCovered.toISOString()); - } - - Array.from(events) - .reverse() - .forEach((item) => { - const meta = generateMeta(item, fieldName); - emit(item, meta); - }); - }, - }, -}; diff --git a/components/salesforce_rest_api/sources/record-deleted-instant/record-deleted-instant.mjs b/components/salesforce_rest_api/sources/record-deleted-instant/record-deleted-instant.mjs index 3522cc44ba4da..46cd9b7485fdc 100644 --- a/components/salesforce_rest_api/sources/record-deleted-instant/record-deleted-instant.mjs +++ b/components/salesforce_rest_api/sources/record-deleted-instant/record-deleted-instant.mjs @@ -1,17 +1,16 @@ import startCase from "lodash/startCase.js"; - -import common from "../common-instant.mjs"; +import common from "../common.mjs"; export default { ...common, type: "source", name: "New Deleted Record (Instant, of Selectable Type)", key: "salesforce_rest_api-record-deleted-instant", - description: "Emit new event immediately after a record of arbitrary object type (selected as an input parameter by the user) is deleted", - version: "0.0.4", + description: "Emit new event when a record of the selected object type is deleted. [See the documentation](https://sforce.co/3msDDEE)", + version: "0.1.0", methods: { ...common.methods, - generateMeta(data) { + generateWebhookMeta(data) { const nameField = this.getNameField(); const { Old: oldObject } = data.body; const { @@ -29,8 +28,44 @@ export default { ts, }; }, + generateTimerMeta(item) { + const { + id, + deletedDate, + } = item; + const entityType = startCase(this.objectType); + const summary = `${entityType} deleted: ${id}`; + const ts = Date.parse(deletedDate); + return { + id, + summary, + ts, + }; + }, getEventType() { return "deleted"; }, + async processTimerEvent(eventData) { + const { + startTimestamp, + endTimestamp, + } = eventData; + const { + deletedRecords, + latestDateCovered, + } = await this.salesforce.getDeletedForObjectType( + this.objectType, + startTimestamp, + endTimestamp, + ); + this.setLatestDateCovered(latestDateCovered); + + // When a record is deleted, the `getDeleted` API only shows the ID of the + // deleted item and the date in which it was deleted. + deletedRecords.forEach((item) => { + const meta = this.generateTimerMeta(item); + this.$emit(item, meta); + }); + }, }, }; diff --git a/components/salesforce_rest_api/sources/record-deleted/record-deleted.mjs b/components/salesforce_rest_api/sources/record-deleted/record-deleted.mjs deleted file mode 100644 index cda160468e9ea..0000000000000 --- a/components/salesforce_rest_api/sources/record-deleted/record-deleted.mjs +++ /dev/null @@ -1,51 +0,0 @@ -import startCase from "lodash/startCase.js"; - -import common from "../common.mjs"; - -export default { - ...common, - type: "source", - name: "New Deleted Record (of Selectable Type)", - key: "salesforce_rest_api-record-deleted", - description: "Emit new event (at regular intervals) when a record of arbitrary object type (selected as an input parameter by the user) is deleted. [See the docs](https://sforce.co/3msDDEE) for more information.", - version: "0.0.4", - methods: { - ...common.methods, - generateMeta(item) { - const { - id, - deletedDate, - } = item; - const entityType = startCase(this.objectType); - const summary = `${entityType} deleted: ${id}`; - const ts = Date.parse(deletedDate); - return { - id, - summary, - ts, - }; - }, - async processEvent(eventData) { - const { - startTimestamp, - endTimestamp, - } = eventData; - const { - deletedRecords, - latestDateCovered, - } = await this.salesforce.getDeletedForObjectType( - this.objectType, - startTimestamp, - endTimestamp, - ); - this.setLatestDateCovered(latestDateCovered); - - // When a record is deleted, the `getDeleted` API only shows the ID of the - // deleted item and the date in which it was deleted. - deletedRecords.forEach((item) => { - const meta = this.generateMeta(item); - this.$emit(item, meta); - }); - }, - }, -}; diff --git a/components/salesforce_rest_api/sources/record-updated-instant/record-updated-instant.mjs b/components/salesforce_rest_api/sources/record-updated-instant/record-updated-instant.mjs index 83663d0c790d6..46db6b7992e6f 100644 --- a/components/salesforce_rest_api/sources/record-updated-instant/record-updated-instant.mjs +++ b/components/salesforce_rest_api/sources/record-updated-instant/record-updated-instant.mjs @@ -1,17 +1,35 @@ import startCase from "lodash/startCase.js"; - -import common from "../common-instant.mjs"; +import common from "../common.mjs"; +import constants from "../../common/constants.mjs"; +const { salesforce } = common.props; export default { ...common, type: "source", name: "New Updated Record (Instant, of Selectable Type)", key: "salesforce_rest_api-record-updated-instant", - description: "Emit new event immediately after a record of arbitrary type (selected as an input parameter by the user) is updated", - version: "0.1.7", + description: "Emit new event when a record of the selected type is updated. [See the documentation](https://sforce.co/3yPSJZy)", + version: "0.2.0", + props: { + ...common.props, + fields: { + propDefinition: [ + salesforce, + "field", + ({ objectType }) => ({ + objectType, + filter: ({ updateable }) => updateable, + }), + ], + label: "Fields To Watch", + type: "string[]", + optional: true, + description: "If specified, events will only be emitted if at least one of the selected fields is updated. This filter is only available when a webhook is created successfully.", + }, + }, methods: { ...common.methods, - generateMeta(data) { + generateWebhookMeta(data) { const nameField = this.getNameField(); const { New: newObject } = data.body; const { @@ -29,8 +47,122 @@ export default { ts, }; }, + generateTimerMeta(item, fieldName) { + const { objectType } = this; + + const { + LastModifiedDate: lastModifiedDate, + [fieldName]: name, + Id: id, + } = item; + + const entityType = startCase(objectType); + const summary = `${entityType} updated: ${name}`; + const ts = Date.parse(lastModifiedDate); + return { + id: `${id}-${ts}`, + summary, + ts, + }; + }, getEventType() { return "updated"; }, + isEventRelevant(changedFields) { + const { fields } = this; + return fields?.length + ? Object.keys(changedFields).some((key) => fields.includes(key)) + : true; + }, + getChangedFields(body) { + return Object.entries(body.New).filter(([ + key, + value, + ]) => { + const oldValue = body.Old[key]; + return ( + value !== undefined + && oldValue !== undefined + && JSON.stringify(value) !== JSON.stringify(oldValue) + ); + }) + .reduce((obj, [ + key, + value, + ]) => { + obj[key] = { + old: body.Old[key], + new: value, + }; + return obj; + }, {}); + }, + processWebhookEvent(event) { + const { body } = event; + const changedFields = this.getChangedFields(body); + if (this.isEventRelevant(changedFields)) { + const meta = this.generateWebhookMeta(event); + this.$emit({ + ...body, + changedFields, + }, meta); + } + }, + async processTimerEvent(eventData) { + const { + getNameField, + getObjectTypeColumns, + paginate, + objectType, + setLatestDateCovered, + generateTimerMeta, + $emit: emit, + } = this; + + const { + startTimestamp, + endTimestamp, + } = eventData; + + const fieldName = getNameField(); + const columns = getObjectTypeColumns(); + + const events = await paginate({ + objectType, + startTimestamp, + endTimestamp, + columns, + dateFieldName: constants.FIELD_NAME.LAST_MODIFIED_DATE, + }); + + const [ + latestEvent, + ] = events; + + if (latestEvent?.LastModifiedDate) { + const latestDateCovered = new Date(latestEvent.LastModifiedDate); + latestDateCovered.setSeconds(0); + setLatestDateCovered(latestDateCovered.toISOString()); + } + + Array.from(events) + .reverse() + .forEach((item) => { + const meta = generateTimerMeta(item, fieldName); + emit(item, meta); + }); + }, + async timerActivateHook() { + const { + objectType, + getObjectTypeDescription, + setObjectTypeColumns, + } = this; + + const { fields } = await getObjectTypeDescription(objectType); + const columns = fields.map(({ name }) => name); + + setObjectTypeColumns(columns); + }, }, }; diff --git a/components/salesforce_rest_api/sources/record-updated/record-updated.mjs b/components/salesforce_rest_api/sources/record-updated/record-updated.mjs deleted file mode 100644 index 442bb74458415..0000000000000 --- a/components/salesforce_rest_api/sources/record-updated/record-updated.mjs +++ /dev/null @@ -1,94 +0,0 @@ -import startCase from "lodash/startCase.js"; -import common from "../common.mjs"; -import constants from "../../common/constants.mjs"; - -export default { - ...common, - type: "source", - name: "New Updated Record (of Selectable Type)", - key: "salesforce_rest_api-record-updated", - description: "Emit new event (at regular intervals) when a record of arbitrary type (selected as an input parameter by the user) is updated. [See the docs](https://sforce.co/3yPSJZy) for more information.", - version: "0.1.12", - hooks: { - ...common.hooks, - async activate() { - const { - objectType, - getObjectTypeDescription, - setObjectTypeColumns, - } = this; - - await common.hooks.activate.call(this); - - const { fields } = await getObjectTypeDescription(objectType); - const columns = fields.map(({ name }) => name); - - setObjectTypeColumns(columns); - }, - }, - methods: { - ...common.methods, - generateMeta(item, fieldName) { - const { objectType } = this; - - const { - LastModifiedDate: lastModifiedDate, - [fieldName]: name, - Id: id, - } = item; - - const entityType = startCase(objectType); - const summary = `${entityType} updated: ${name}`; - const ts = Date.parse(lastModifiedDate); - return { - id: `${id}-${ts}`, - summary, - ts, - }; - }, - async processEvent(eventData) { - const { - getNameField, - getObjectTypeColumns, - paginate, - objectType, - setLatestDateCovered, - generateMeta, - $emit: emit, - } = this; - - const { - startTimestamp, - endTimestamp, - } = eventData; - - const fieldName = getNameField(); - const columns = getObjectTypeColumns(); - - const events = await paginate({ - objectType, - startTimestamp, - endTimestamp, - columns, - dateFieldName: constants.FIELD_NAME.LAST_MODIFIED_DATE, - }); - - const [ - latestEvent, - ] = events; - - if (latestEvent?.LastModifiedDate) { - const latestDateCovered = new Date(latestEvent.LastModifiedDate); - latestDateCovered.setSeconds(0); - setLatestDateCovered(latestDateCovered.toISOString()); - } - - Array.from(events) - .reverse() - .forEach((item) => { - const meta = generateMeta(item, fieldName); - emit(item, meta); - }); - }, - }, -}; diff --git a/components/salesforce_rest_api/sources/updated-field-on-record-instant/updated-field-on-record-instant.mjs b/components/salesforce_rest_api/sources/updated-field-on-record-instant/updated-field-on-record-instant.mjs deleted file mode 100644 index d6bd6eeb7db5b..0000000000000 --- a/components/salesforce_rest_api/sources/updated-field-on-record-instant/updated-field-on-record-instant.mjs +++ /dev/null @@ -1,76 +0,0 @@ -import startCase from "lodash/startCase.js"; - -import common from "../common-instant.mjs"; -const { salesforce } = common.props; - -export default { - ...common, - type: "source", - name: "New Updated Field on Record (Instant, of Selectable Type)", - key: "salesforce_rest_api-updated-field-on-record-instant", - description: "Emit new event immediately after a field of your choosing is updated on any record of a specified Salesforce object", - version: "0.1.6", - props: { - ...common.props, - field: { - propDefinition: [ - salesforce, - "field", - ({ objectType }) => ({ - objectType, - }), - ], - }, - fieldUpdatedTo: { - propDefinition: [ - salesforce, - "fieldUpdatedTo", - ], - }, - }, - methods: { - ...common.methods, - isEventRelevant(event) { - if (!this.fieldUpdatedTo) { - return true; - } - const { New: newObject } = event.body; - const { [this.field]: newFieldValue } = newObject; - return !this.fieldUpdatedTo || this.fieldUpdatedTo === newFieldValue; - }, - generateMeta(data) { - const nameField = this.getNameField(); - const { New: newObject } = data.body; - const { - LastModifiedDate: lastModifiedDate, - Id: id, - [nameField]: name, - } = newObject; - const entityType = startCase(this.objectType); - const summary = `${this.field} on ${entityType}: ${name}`; - const ts = Date.parse(lastModifiedDate); - const compositeId = `${id}-${ts}`; - return { - id: compositeId, - summary, - ts, - }; - }, - processEvent(event) { - const { body } = event; - if (!this.isEventRelevant(event)) { - return; - } - const meta = this.generateMeta(event); - this.$emit(body, meta); - }, - getEventType() { - return "updated"; - }, - getFieldsToCheck() { - return [ - this.field, - ]; - }, - }, -}; diff --git a/components/salesforce_rest_api/sources/updated-field-on-record/updated-field-on-record.mjs b/components/salesforce_rest_api/sources/updated-field-on-record/updated-field-on-record.mjs deleted file mode 100644 index bf173051f817c..0000000000000 --- a/components/salesforce_rest_api/sources/updated-field-on-record/updated-field-on-record.mjs +++ /dev/null @@ -1,161 +0,0 @@ -import words from "lodash/words.js"; -import common from "../common.mjs"; - -const { salesforce } = common.props; - -/** - * Uses the Salesforce REST API's [sObject Get Updated endpoint](https://sforce.co/3yPSJZy) on the - * [StandardObjectNamedHistory model](https://sforce.co/3Fn4lWB) to get changes to field values of - * an sObject type. Associated sObject records are retrieved and emitted for history object records - * matching configured `field` and `fieldUpdatedTo` prop values. - */ -export default { - ...common, - dedupe: "greatest", - type: "source", - name: "New Updated Field on Record (of Selectable Type)", - key: "salesforce_rest_api-updated-field-on-record", - description: "Emit new event (at regular intervals) when a field of your choosing is updated on any record of a specified Salesforce object. Field history tracking must be enabled for the chosen field. See the docs on [field history tracking](https://sforce.co/3mtj0rF) and [history objects](https://sforce.co/3Fn4lWB) for more information.", - version: "0.1.11", - props: { - ...common.props, - objectType: { - type: common.props.objectType.type, - label: common.props.objectType.label, - description: common.props.objectType.description, - propDefinition: [ - salesforce, - "objectType", - () => ({ - filter: ({ - replicateable, - associateEntityType, - }) => replicateable && associateEntityType === "History", - mapper: ({ associateParentEntity: value }) => words(value).join(" "), - }), - ], - }, - field: { - propDefinition: [ - salesforce, - "field", - ({ objectType }) => ({ - objectType, - filter: ({ updateable }) => updateable, - }), - ], - }, - fieldUpdatedTo: { - propDefinition: [ - salesforce, - "fieldUpdatedTo", - ], - }, - }, - hooks: { - ...common.hooks, - async activate() { - const { - objectType, - getObjectTypeDescription, - setHistoryObjectType, - setObjectTypeColumns, - } = this; - - await common.hooks.activate.call(this); - - const historyObjectType = `${objectType}History`; - - const { fields } = await getObjectTypeDescription(historyObjectType); - const columns = fields.map(({ name }) => name); - - setHistoryObjectType(historyObjectType); - setObjectTypeColumns(columns); - }, - }, - methods: { - ...common.methods, - getHistoryObjectType() { - return this.db.get("historyObjectType"); - }, - setHistoryObjectType(historyObjectType) { - this.db.set("historyObjectType", historyObjectType); - }, - isRelevant(item) { - const { - field, - fieldUpdatedTo, - } = this; - - const isFieldRelevant = - item.Field === field - || item.Field === `${item.DataType}${field}`; - - const isFieldValueRelevant = - !fieldUpdatedTo - || item.NewValue === fieldUpdatedTo; - - return isFieldRelevant && isFieldValueRelevant; - }, - generateMeta(event) { - const { - objectType, - field, - } = this; - - const { - CreatedDate: createdDate, - Id: id, - [`${objectType}Id`]: objectId, - } = event; - - const ts = Date.parse(createdDate); - return { - id: `${id}-${ts}`, - summary: `${field} on ${objectType}: ${objectId}`, - ts, - }; - }, - async processEvent({ - startTimestamp, endTimestamp, - }) { - const { - getHistoryObjectType, - getObjectTypeColumns, - setLatestDateCovered, - isRelevant, - paginate, - generateMeta, - $emit: emit, - } = this; - - const objectType = getHistoryObjectType(); - const columns = getObjectTypeColumns(); - - const events = await paginate({ - objectType, - startTimestamp, - endTimestamp, - columns, - }); - - const [ - latestEvent, - ] = events; - - if (latestEvent?.CreatedDate) { - const latestDateCovered = new Date(latestEvent.CreatedDate); - latestDateCovered.setSeconds(0); - setLatestDateCovered(latestDateCovered.toISOString()); - } - - Array.from(events) - .reverse() - .filter(isRelevant) - .forEach((event) => { - const meta = generateMeta(event); - emit(event, meta); - }); - }, - }, -}; diff --git a/components/saleslens/README.md b/components/saleslens/README.md new file mode 100644 index 0000000000000..e967eb2b558a8 --- /dev/null +++ b/components/saleslens/README.md @@ -0,0 +1,11 @@ +# Overview + +SalesLens is an analytics platform designed to help you understand and engage with your customers. By analyzing customer interactions, SalesLens provides insights that can lead to more effective sales strategies and improved customer engagement. With the SalesLens API, you can access customer data, manage accounts, and trigger personalized actions based on customer behavior. In Pipedream, you can leverage this capability to create automated workflows that respond to data events in real-time, integrate with other apps, or process large datasets asynchronously. + +# Example Use Cases + +- **Automate Lead Scoring and Notification**: Use the SalesLens API within Pipedream to score leads based on their interactions and automatically notify the sales team via Slack or email when a lead reaches a threshold score. This ensures immediate follow-up with potential customers who are most engaged. + +- **Sync Customer Data with CRM Systems**: With Pipedream, you can build a workflow that syncs customer data from SalesLens to your CRM platform, such as Salesforce or HubSpot. This keeps your sales team informed with the latest insights and ensures that customer interactions are up-to-date across all systems. + +- **Personalized Marketing Campaigns**: Trigger personalized marketing campaigns in tools like Mailchimp or SendGrid based on customer behavior data from SalesLens. Set up a Pipedream workflow to segment customers according to their activity and preferences, then execute targeted email campaigns designed to convert. diff --git a/components/salesmate/README.md b/components/salesmate/README.md new file mode 100644 index 0000000000000..3b7fab388b2e6 --- /dev/null +++ b/components/salesmate/README.md @@ -0,0 +1,11 @@ +# Overview + +The Salesmate API enables automation of CRM-related tasks within the Salesmate platform. By leveraging this API on Pipedream, you can craft serverless workflows that interact with Salesmate data, such as contacts, deals, and activities. This allows for seamless integration of Salesmate with other services to enhance sales processes, enable real-time data updates, and trigger custom actions based on specific events within your CRM. + +# Example Use Cases + +- **Sync New Contacts to Mailchimp:** When a new contact is added to Salesmate, automatically add them to a designated Mailchimp list. This keeps your email marketing lists up to date without manual entry, ensuring new leads receive your campaigns. + +- **Create Deals from Shopify Orders:** Once an order is placed in Shopify, create a corresponding deal in Salesmate. This workflow can help track new sales opportunities and maintain alignment between your e-commerce platform and sales pipeline. + +- **Slack Notifications for Deal Milestones:** Send a message to a specific Slack channel whenever a deal in Salesmate reaches a key milestone or is won. This encourages team awareness and timely celebration of sales achievements. diff --git a/components/salesmsg/README.md b/components/salesmsg/README.md new file mode 100644 index 0000000000000..6fb625dc5d11c --- /dev/null +++ b/components/salesmsg/README.md @@ -0,0 +1,11 @@ +# Overview + +The Salesmsg API facilitates two-way SMS and MMS communications, allowing users to send, receive, and manage messages programmatically. It provides a suite of tools to integrate texting capabilities into various workflows and systems. On Pipedream, you can harness this API to automate conversations, sync with customer databases, trigger notifications, and much more, utilizing Pipedream's serverless platform to connect Salesmsg with a plethora of other apps and services. + +# Example Use Cases + +- **Automated Customer Support Tickets**: Create a workflow that listens for incoming messages on Salesmsg, parses the content, and automatically creates a support ticket in a system like Zendesk or Jira. This enables immediate ticket creation without manual intervention. + +- **SMS Drip Campaigns Based on User Actions**: Use the Salesmsg API on Pipedream to send a series of timed follow-up messages to users who take a specific action in your app like signing up or making a purchase. Connect to apps like Shopify or Salesforce to trigger these messages based on user behavior. + +- **Real-time CRM Updates**: Build a workflow that updates a contact record in a CRM like HubSpot whenever a text message is sent or received in Salesmsg. This keeps your CRM up-to-date with the latest customer interactions, ensuring a coherent view of communication history. diff --git a/components/salespype/README.md b/components/salespype/README.md new file mode 100644 index 0000000000000..ec292f443a71f --- /dev/null +++ b/components/salespype/README.md @@ -0,0 +1,11 @@ +# Overview + +The Salespype API provides a suite of tools to automate and integrate your CRM workflows. With Pipedream, you can harness this power to connect Salespype with a vast array of other services, creating customized workflows without needing to manage server infrastructure. Whether you're looking to sync contacts, manage deals, automate follow-ups, or trigger custom actions based on CRM events, Pipedream makes it straightforward. + +# Example Workflows + +- **Sync Salespype Contacts with Google Sheets**: Automate the synchronization of new contacts from Salespype into a Google Sheet. Each time a new contact is added in Salespype, it triggers a Pipedream workflow that appends the contact's details to a specified Google Sheet. This can be used for backup, reporting, or further data analysis. + +- **Create Tasks in Asana from Salespype Deals**: Whenever a new deal reaches a specific stage in Salespype, trigger a Pipedream workflow that creates a corresponding task in Asana. This helps in task management and ensures that the team focuses on the right deals at the right time without manual entry. + +- **Send Custom Email Alerts for Stalled Deals**: Set up a Pipedream workflow that monitors deal progress in Salespype. If a deal hasn't moved stages within a set timeframe, trigger an email alert to the assigned salesperson or team via an email service like SendGrid. This can prompt timely follow-ups and keep deals moving forward. diff --git a/components/salespype/actions/add-contact-to-campaign/add-contact-to-campaign.mjs b/components/salespype/actions/add-contact-to-campaign/add-contact-to-campaign.mjs new file mode 100644 index 0000000000000..a60b85d30f8e9 --- /dev/null +++ b/components/salespype/actions/add-contact-to-campaign/add-contact-to-campaign.mjs @@ -0,0 +1,33 @@ +import salespype from "../../salespype.app.mjs"; + +export default { + key: "salespype-add-contact-to-campaign", + name: "Add Contact to Campaign", + description: "Adds a contact to a campaign. [See the documentation](https://documenter.getpostman.com/view/5101444/2s93Y3u1Eb#4b2f8b3e-155d-4485-9a25-4f7d98d04b53)", + version: "0.0.1", + type: "action", + props: { + salespype, + campaignId: { + propDefinition: [ + salespype, + "campaignId", + ], + }, + contactId: { + propDefinition: [ + salespype, + "contactId", + ], + }, + }, + async run({ $ }) { + const response = await this.salespype.addContactToCampaign({ + $, + campaignId: this.campaignId, + contactId: this.contactId, + }); + $.export("$summary", `Added contact ${this.contactId} to campaign ${this.campaignId}`); + return response; + }, +}; diff --git a/components/salespype/actions/create-contact/create-contact.mjs b/components/salespype/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..862f01822770f --- /dev/null +++ b/components/salespype/actions/create-contact/create-contact.mjs @@ -0,0 +1,88 @@ +import salespype from "../../salespype.app.mjs"; + +export default { + key: "salespype-create-contact", + name: "Create Contact", + description: "Creates a new contact in Salespype. [See the documentation](https://documenter.getpostman.com/view/5101444/2s93Y3u1Eb#0a9f8441-c7fa-48dc-b02b-0117037d86ab)", + version: "0.0.1", + type: "action", + props: { + salespype, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the contact", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the contact", + }, + email: { + type: "string", + label: "Email", + description: "The email address of the contact", + }, + address: { + type: "string", + label: "Address", + description: "The address of the contact", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "The city of the contact", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "The state of the contact", + optional: true, + }, + zip: { + type: "string", + label: "ZIP Code", + description: "The ZIP code of the contact", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "The country of the contact", + optional: true, + }, + companyName: { + type: "string", + label: "Company Name", + description: "The company name of the contact", + optional: true, + }, + birthDate: { + type: "string", + label: "Birthdate", + description: "The birthdate of the contact", + optional: true, + }, + }, + async run({ $ }) { + const contact = await this.salespype.createContact({ + $, + data: { + firstName: this.firstName, + lastName: this.lastName, + email: this.email, + address: this.address, + city: this.city, + state: this.state, + zip: this.zip, + country: this.country, + companyName: this.companyName, + birthDate: this.birthDate, + }, + }); + $.export("$summary", `Created contact ${this.firstName} ${this.lastName} (${this.email})`); + return contact; + }, +}; diff --git a/components/salespype/actions/create-task/create-task.mjs b/components/salespype/actions/create-task/create-task.mjs new file mode 100644 index 0000000000000..b6bf72bc6755d --- /dev/null +++ b/components/salespype/actions/create-task/create-task.mjs @@ -0,0 +1,72 @@ +import salespype from "../../salespype.app.mjs"; + +export default { + key: "salespype-create-task", + name: "Create Task", + description: "Creates a new task in Salespype. [See the documentation](https://documenter.getpostman.com/view/5101444/2s93Y3u1Eb#a9c6449a-b844-465c-a342-deea01e52c3f)", + version: "0.0.1", + type: "action", + props: { + salespype, + contactId: { + propDefinition: [ + salespype, + "contactId", + ], + }, + task: { + type: "string", + label: "Task", + description: "The task description", + }, + taskTypeId: { + propDefinition: [ + salespype, + "taskTypeId", + ], + }, + date: { + type: "string", + label: "Date", + description: "The date for the task. E.g. `2021-02-20`", + }, + time: { + type: "string", + label: "Time", + description: "The time for the task. E.g. `34:00:34`", + }, + duration: { + type: "string", + label: "Duration", + description: "The duration of the task. E.g. `34:00:34`", + }, + note: { + type: "string", + label: "Note", + description: "Additional notes for the task", + }, + }, + async run({ $ }) { + const { + task, message, + } = await this.salespype.createTask({ + $, + contactId: this.contactId, + data: { + task: this.task, + taskTypeId: this.taskTypeId, + date: this.date, + time: this.time, + duration: this.duration, + note: this.note, + }, + }); + + if (message) { + throw new Error(`${message}`); + } + + $.export("$summary", `Created task '${this.task}' with ID ${task.id}`); + return task; + }, +}; diff --git a/components/salespype/package.json b/components/salespype/package.json new file mode 100644 index 0000000000000..375ac02fea886 --- /dev/null +++ b/components/salespype/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/salespype", + "version": "0.1.0", + "description": "Pipedream Salespype Components", + "main": "salespype.app.mjs", + "keywords": [ + "pipedream", + "salespype" + ], + "homepage": "https://pipedream.com/apps/salespype", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "md5": "^2.3.0" + } +} diff --git a/components/salespype/salespype.app.mjs b/components/salespype/salespype.app.mjs new file mode 100644 index 0000000000000..e296df1766732 --- /dev/null +++ b/components/salespype/salespype.app.mjs @@ -0,0 +1,142 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "salespype", + propDefinitions: { + contactId: { + type: "string", + label: "Contact ID", + description: "The unique identifier of the contact", + async options({ page }) { + const { contacts } = await this.listContacts({ + params: { + page: page + 1, + }, + }); + return contacts?.map(({ + id: value, fullName: label, + }) => ({ + value, + label, + })) || []; + }, + }, + campaignId: { + type: "string", + label: "Campaign ID", + description: "The unique identifier of the campaign", + async options({ page }) { + const { campaigns } = await this.listCampaigns({ + params: { + page: page + 1, + }, + }); + return campaigns?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })) || []; + }, + }, + taskTypeId: { + type: "string", + label: "Task Type ID", + description: "The unique identifier of the task type", + async options() { + const { taskTypes } = await this.listTaskTypes(); + return taskTypes?.map(({ + id: value, task: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.pypepro.io/crm/v1"; + }, + _makeRequest({ + $ = this, + path, + ...otherOpts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + apikey: this.$auth.api_token, + }, + ...otherOpts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts/list", + ...opts, + }); + }, + listCampaigns(opts = {}) { + return this._makeRequest({ + path: "/campaigns", + ...opts, + }); + }, + listTaskTypes(opts = {}) { + return this._makeRequest({ + path: "/tasks/types", + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contacts", + ...opts, + }); + }, + addContactToCampaign({ + campaignId, contactId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/campaigns/${campaignId}/contacts/${contactId}`, + ...opts, + }); + }, + createTask({ + contactId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/tasks/contacts/${contactId}`, + ...opts, + }); + }, + async *paginate({ + fn, params, resourceKey, max, + }) { + params = { + ...params, + page: 0, + }; + let totalPages, count = 0; + do { + params.page++; + const results = await fn({ + params, + }); + const items = results[resourceKey]; + for (const item of items) { + yield item; + if (max && ++count >= max) { + return; + } + } + totalPages = results.totalPages; + } while (params.page < totalPages); + }, + }, +}; diff --git a/components/salespype/sources/common/base.mjs b/components/salespype/sources/common/base.mjs new file mode 100644 index 0000000000000..1d3cc9ff79cf4 --- /dev/null +++ b/components/salespype/sources/common/base.mjs @@ -0,0 +1,80 @@ +import salespype from "../../salespype.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + salespype, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLast() { + return this.db.get("last") || 0; + }, + _setLast(last) { + this.db.set("last", last); + }, + async processEvent(max) { + const last = this._getLast(); + let maxLast = last; + const resourceFn = this.getResourceFn(); + + const items = this.salespype.paginate({ + fn: resourceFn, + params: this.getParams(), + resourceKey: this.getResourceKey(), + max, + }); + + const results = []; + for await (const item of items) { + const fieldValue = this.getFieldValue(item); + if (fieldValue >= last) { + results.push(item); + maxLast = Math.max(maxLast, fieldValue); + } + } + + if (!results.length) { + return; + } + + this._setLast(maxLast); + + results.reverse().forEach((item) => { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }); + }, + getParams() { + return { + order: "desc", + }; + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + getResourceKey() { + throw new Error("getResourceKey is not implemented"); + }, + getFieldValue() { + throw new Error("getFieldValue is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/salespype/sources/contact-updated/contact-updated.mjs b/components/salespype/sources/contact-updated/contact-updated.mjs new file mode 100644 index 0000000000000..41b8c8c244c6a --- /dev/null +++ b/components/salespype/sources/contact-updated/contact-updated.mjs @@ -0,0 +1,35 @@ +import common from "../common/base.mjs"; +import md5 from "md5"; + +export default { + ...common, + key: "salespype-contact-updated", + name: "Contact Updated", + description: "Emit new event when an existing contact is updated. [See the documentation](https://documenter.getpostman.com/view/5101444/2s93Y3u1Eb#e8b86665-e0b3-4c2e-9bd0-05fcf81f6c48)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.salespype.listContacts; + }, + getResourceKey() { + return "contacts"; + }, + getFieldValue(contact) { + return Date.parse(contact.updatedAt); + }, + generateMeta(contact) { + return { + id: md5(JSON.stringify({ + ...contact, + createdAt: undefined, + updatedAt: undefined, + })), + summary: `Contact Updated: ${contact.id}`, + ts: Date.parse(contact.updatedAt), + }; + }, + }, +}; diff --git a/components/salespype/sources/new-campaign-created/new-campaign-created.mjs b/components/salespype/sources/new-campaign-created/new-campaign-created.mjs new file mode 100644 index 0000000000000..d64221b28e1a7 --- /dev/null +++ b/components/salespype/sources/new-campaign-created/new-campaign-created.mjs @@ -0,0 +1,30 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "salespype-new-campaign-created", + name: "New Campaign Created", + description: "Emit new events when a new campaign is created. [See the documentation](https://documenter.getpostman.com/view/5101444/2s93Y3u1Eb#f6b1d9d0-0251-4b6c-b70e-699b35f59a39)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.salespype.listCampaigns; + }, + getResourceKey() { + return "campaigns"; + }, + getFieldValue(campaign) { + return campaign.id; + }, + generateMeta(campaign) { + return { + id: campaign.id, + summary: `New Campaign: ${campaign.id}`, + ts: Date.now(), + }; + }, + }, +}; diff --git a/components/salespype/sources/new-contact-created/new-contact-created.mjs b/components/salespype/sources/new-contact-created/new-contact-created.mjs new file mode 100644 index 0000000000000..bfb306a0ccf40 --- /dev/null +++ b/components/salespype/sources/new-contact-created/new-contact-created.mjs @@ -0,0 +1,30 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "salespype-new-contact-created", + name: "New Contact Created", + description: "Emit new event when a new contact is created. [See the documentation](https://documenter.getpostman.com/view/5101444/2s93Y3u1Eb#e8b86665-e0b3-4c2e-9bd0-05fcf81f6c48)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.salespype.listContacts; + }, + getResourceKey() { + return "contacts"; + }, + getFieldValue(contact) { + return Date.parse(contact.createdAt); + }, + generateMeta(contact) { + return { + id: contact.id, + summary: `New Contact Created: ${contact.id}`, + ts: Date.parse(contact.createdAt), + }; + }, + }, +}; diff --git a/components/salestown/README.md b/components/salestown/README.md index fccc744c5a0f3..a60af9d94e62d 100644 --- a/components/salestown/README.md +++ b/components/salestown/README.md @@ -1,36 +1,11 @@ # Overview -The Salestown API provides a powerful set of tools to help developers build -sophisticated, feature-rich applications quickly and easily. With this API, it -is possible to create customized web and mobile apps with powerful features, -such as: +The Salestown API enables automation around retail management, allowing you to streamline inventory, sales, and customer data processes. By leveraging Pipedream's serverless platform, you can create workflows that respond to events in Salestown in real-time, connect to hundreds of other services, and process data without managing servers. Use cases may range from syncing inventory levels with an eCommerce store to automating customer outreach after a purchase. -- Social media integration -- Mobile commerce -- Data visualization -- Automated marketing -- Personalization -- Location-based searches -- Payment integration -- Custom checkout forms +# Example Use Cases -These are some of the amazing things that can be built with the Salestown API: +- **Inventory Level Syncing**: Automatically update the stock levels on your Shopify store whenever a sale is recorded in Salestown. This ensures that your online and physical inventories are in sync, reducing the risk of overselling products that are no longer in stock. -- Online stores - Create online stores with marketplaces, payment gateways, and - other ecommerce features. -- Auction sites - Develop auction sites with rich features like multiple - payment options, discounts and promotions. -- Customized dashboards - Create data-driven dashboards with visualizations and - analytics. -- User profiles - Design custom user profiles with ratings, reviews, - recommendations and more. -- Chatbots - Build chatbots that can engage visitors, answer questions and - provide support. -- API-driven applications - Develop API-driven applications that allow users to - access data from different sources. -- Online marketplaces - Build online marketplaces with listing and search - functionality. -- Community websites - Create community websites with discussion forums, polls - and other social features. -- Mobile apps - Develop mobile apps with features like push notifications, - in-app purchases and much more. +- **Customer Satisfaction Surveys**: Trigger an email survey to a customer using SendGrid after a purchase is completed in Salestown. Gain insights into your customer service and product satisfaction, and use this data to improve your offerings. + +- **Sales Data Analysis and Reporting**: On a scheduled basis, aggregate your sales data from Salestown and send it to a Google Sheets spreadsheet. Use this to analyze sales trends, generate reports, and forecast inventory needs without manual data entry. diff --git a/components/samcart/README.md b/components/samcart/README.md index 41b301a7f618e..9ae5616fe657c 100644 --- a/components/samcart/README.md +++ b/components/samcart/README.md @@ -1,19 +1,11 @@ # Overview -The SamCart API provides a powerful and comprehensive way to build custom -integration solutions customized to any e-commerce business. With the SamCart -API, you can quickly and easily create automated solutions that help manage, -track, and integrate with your store. Here are a few of the things you can -build with the SamCart API: +SamCart is an e-commerce platform designed to optimize the checkout experience for businesses selling products online. With the SamCart API, you can automate various aspects of e-commerce management, from order processing to customer follow-ups. On Pipedream, you can harness this API to create custom workflows that respond to events in SamCart, sync data across other tools like CRM and email marketing platforms, or even aggregate sales data for reporting and analysis. -- Automate customer communication when an order is placed or shipped -- Connect SamCart with external payment processor systems -- Create third-party shopping cart or checkout list integrations -- Sync orders, customers, and products with your own internal databases -- Automatically throttle inventory levels and availability -- Create custom reporting solutions to track your store’s performance -- Automate fulfillment processes through third-party services -- Automatically sync customer data and order information across disparate - systems -- Create custom product bundles and specialized discounts -- Create loyalty and rewards programs to incentivize customers +# Example Use Cases + +- **Order Fulfillment Automation**: When a new order is placed in SamCart, trigger a Pipedream workflow that sends order data to a fulfillment service like Shipstation. Automatically create shipping labels and update order status in SamCart once the product is on its way to the customer. + +- **Customer Drip Campaign Integration**: After a customer makes a purchase, use Pipedream to add them to a segmented email list in Mailchimp based on the product they bought. Kick off a tailored drip campaign that upsells related products, encourages reviews, or offers discounts on future purchases. + +- **Sales Data Aggregation and Reporting**: Connect SamCart to a Google Sheets spreadsheet using Pipedream. Every time a sale is made, append the sales data to the sheet. Utilize this for real-time sales tracking, generating weekly or monthly sales reports, and spotting trends to inform marketing strategies. diff --git a/components/samsara/actions/create-address/create-address.mjs b/components/samsara/actions/create-address/create-address.mjs new file mode 100644 index 0000000000000..9cee5ee544c75 --- /dev/null +++ b/components/samsara/actions/create-address/create-address.mjs @@ -0,0 +1,109 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { + cleanObject, + parseObject, +} from "../../common/utils.mjs"; +import samsara from "../../samsara.app.mjs"; + +export default { + key: "samsara-create-address", + name: "Create Address", + description: "Adds a new address to the organization. The user must provide the necessary props such as formatted address, geofence, and name. [See the documentation](https://developers.samsara.com/reference/createaddress)", + version: "0.0.1", + type: "action", + props: { + samsara, + addressTypes: { + type: "string[]", + label: "Address Types", + description: "Reporting location type associated with the address (used for ELD reporting purposes).", + options: [ + "yard", + "shortHaul", + "workforceSite", + "riskZone", + "industrialSite", + "alertsOnly", + ], + optional: true, + }, + contactIds: { + propDefinition: [ + samsara, + "contactIds", + ], + optional: true, + }, + externalIds: { + propDefinition: [ + samsara, + "externalIds", + ], + optional: true, + }, + formattedAddress: { + type: "string", + label: "Formatted Address", + description: "The full street address for this address/geofence, as it might be recognized by Google Maps.", + }, + geofence: { + type: "object", + label: "Geofence", + description: "The geofence that defines this address and its bounds. This can either be a circle or a polygon, but not both. [See the documentation](https://developers.samsara.com/reference/createaddress)", + }, + latitude: { + type: "string", + label: "Latitude", + description: "Latitude of the address. Will be geocoded from **Formatted Address** if not provided.", + optional: true, + }, + longitude: { + type: "string", + label: "Longitude", + description: "Longitude of the address. Will be geocoded from **Formatted Address** if not provided.", + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "Name of the address.", + }, + notes: { + type: "string", + label: "Notes", + description: "Notes about the address.", + }, + tagIds: { + propDefinition: [ + samsara, + "tagIds", + ], + optional: true, + }, + }, + async run({ $ }) { + try { + const response = await this.samsara.createAddress({ + $, + data: cleanObject({ + addressTypes: parseObject(this.addressTypes), + contactIds: parseObject(this.contactIds), + externalIds: parseObject(this.externalIds), + formattedAddress: this.formattedAddress, + geofence: parseObject(this.geofence), + latitude: this.latitude, + longitude: this.longitude, + name: this.name, + notes: this.notes, + tagIds: parseObject(this.tagIds), + }), + }); + + $.export("$summary", `Successfully created address with name Id: ${response.data?.id}`); + return response; + + } catch (error) { + throw new ConfigurationError(JSON.parse(error.message).message); + } + }, +}; diff --git a/components/samsara/actions/create-contact/create-contact.mjs b/components/samsara/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..a7f26afcd70ea --- /dev/null +++ b/components/samsara/actions/create-contact/create-contact.mjs @@ -0,0 +1,54 @@ +import { ConfigurationError } from "@pipedream/platform"; +import samsara from "../../samsara.app.mjs"; + +export default { + key: "samsara-create-contact", + name: "Create Contact", + description: "Adds a new contact to the organization. [See the documentation](https://developers.samsara.com/reference/createcontact)", + version: "0.0.1", + type: "action", + props: { + samsara, + firstName: { + type: "string", + label: "First Name", + description: "First name of the contact.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the contact.", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "Email address of the contact.", + optional: true, + }, + phone: { + type: "string", + label: "Phone Number", + description: "Phone number of the contact.", + optional: true, + }, + }, + async run({ $ }) { + if (!this.email && !this.phone) { + throw new ConfigurationError("Either Email or Phone Number must be provided."); + } + + const response = await this.samsara.createContact({ + $, + data: { + firstName: this.firstName, + lastName: this.lastName, + email: this.email, + phone: this.phone, + }, + }); + $.export("$summary", `Successfully created contact ${this.firstName} ${this.lastName}`); + return response; + }, +}; diff --git a/components/samsara/actions/create-route/create-route.mjs b/components/samsara/actions/create-route/create-route.mjs new file mode 100644 index 0000000000000..ad3c23569dc8e --- /dev/null +++ b/components/samsara/actions/create-route/create-route.mjs @@ -0,0 +1,95 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import samsara from "../../samsara.app.mjs"; + +export default { + key: "samsara-create-route", + name: "Create Route", + description: "Generates a new route to an existing address. As a prerequisite, the user must create an address using the 'create-address' action if the location is not already available in the address book. The user must provide the necessary props such as destination address.", + version: "0.0.1", + type: "action", + props: { + samsara, + driverId: { + propDefinition: [ + samsara, + "driverId", + ], + optional: true, + }, + externalIds: { + propDefinition: [ + samsara, + "externalIds", + ], + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "Name for the route", + }, + notes: { + type: "string", + label: "Notes", + description: "Notes about the route.", + optional: true, + }, + routeCompletionCondition: { + type: "string", + label: "Route Completion Condition", + description: "Defaults to 'arriveLastStop' which ends the route upon arriving at the final stop. The condition 'departLastStop' ends the route upon departing the last stop. If 'arriveLastStop' is set, then the departure time of the final stop should not be set.", + options: [ + "arriveLastStop", + "departLastStop", + ], + optional: true, + }, + routeStartingCondition: { + type: "string", + label: "Route Starting Condition", + description: "Defaults to 'departFirstStop' which starts the route upon departing the first stop in the route. The condition 'arriveFirstStop' starts the route upon arriving at the first stop in the route. If 'departFirstStop' is set, the arrival time of the first stop should not be set.", + options: [ + "departFirstStop", + "arriveFirstStop", + ], + optional: true, + }, + stops: { + type: "object", + label: "Stops", + description: "List of objects of stops along the route. For each stop, exactly one of addressId and singleUseLocation are required. Depending on the settings on your route, either a scheduledArrivalTime or scheduledDepartureTime must be specified for the first job. [See further information](https://developers.samsara.com/reference/createroute)", + }, + vehicleId: { + propDefinition: [ + samsara, + "vehicleId", + ], + optional: true, + }, + }, + async run({ $ }) { + try { + const response = await this.samsara.createRoute({ + $, + data: { + driverId: this.driverId, + externalIds: parseObject(this.externalIds), + name: this.name, + notes: this.notes, + settings: { + routeCompletionCondition: this.routeCompletionCondition, + routeStartingCondition: this.routeStartingCondition, + }, + stops: parseObject(this.stops), + vehicleId: this.vehicleId, + }, + }); + + $.export("$summary", `Successfully created a new route with Id: ${response.data?.id}`); + return response; + } catch ({ message }) { + throw new ConfigurationError(JSON.parse(message).message); + } + }, +}; diff --git a/components/samsara/common/utils.mjs b/components/samsara/common/utils.mjs new file mode 100644 index 0000000000000..2c8bb0118ddff --- /dev/null +++ b/components/samsara/common/utils.mjs @@ -0,0 +1,33 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; + +export const cleanObject = (o) => { + for (let k in o || {}) { + if (typeof o[k] === "undefined") { + delete o[k]; + } + } + return o; +}; diff --git a/components/samsara/package.json b/components/samsara/package.json new file mode 100644 index 0000000000000..8d4907c30d35c --- /dev/null +++ b/components/samsara/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/samsara", + "version": "0.1.0", + "description": "Pipedream Samsara Components", + "main": "samsara.app.mjs", + "keywords": [ + "pipedream", + "samsara" + ], + "homepage": "https://pipedream.com/apps/samsara", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} + diff --git a/components/samsara/samsara.app.mjs b/components/samsara/samsara.app.mjs new file mode 100644 index 0000000000000..15032a4b2dc1d --- /dev/null +++ b/components/samsara/samsara.app.mjs @@ -0,0 +1,164 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "samsara", + propDefinitions: { + contactIds: { + type: "string[]", + label: "Contact Ids", + description: "An array of Contact IDs associated with this Address.", + async options({ page }) { + const { data } = await this.listContacts({ + params: { + page, + }, + }); + + return data.map((contact) => ({ + label: `${contact.firstName} ${contact.lastName}` || contact.email || contact.phone, + value: contact.id, + })); + }, + }, + driverId: { + type: "string", + label: "Driver Id", + description: "ID of the driver.", + async options({ page }) { + const { data } = await this.listDrivers({ + params: { + page, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + tagIds: { + type: "string[]", + label: "Tag Ids", + description: "An array of IDs of tags to associate with this address.", + async options({ page }) { + const { data } = await this.listTags({ + params: { + page, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + vehicleId: { + type: "string", + label: "Vehicle Id", + description: "ID of the vehicle.", + async options({ page }) { + const { data } = await this.listVehicles({ + params: { + page, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + externalIds: { + type: "object", + label: "External Ids", + description: "The external IDs for the given object.", + }, + }, + methods: { + _baseUrl() { + return "https://api.samsara.com"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.api_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contacts", + ...opts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts", + ...opts, + }); + }, + listDrivers(opts = {}) { + return this._makeRequest({ + path: "/fleet/drivers", + ...opts, + }); + }, + listTags(opts = {}) { + return this._makeRequest({ + path: "/tags", + ...opts, + }); + }, + listVehicles(opts = {}) { + return this._makeRequest({ + path: "/fleet/vehicles", + ...opts, + }); + }, + createAddress(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/addresses", + ...opts, + }); + }, + createRoute(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/fleet/routes", + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + }); + }, + }, +}; diff --git a/components/samsara/sources/common/base.mjs b/components/samsara/sources/common/base.mjs new file mode 100644 index 0000000000000..4affd78e1fc37 --- /dev/null +++ b/components/samsara/sources/common/base.mjs @@ -0,0 +1,61 @@ +import samsara from "../../samsara.app.mjs"; + +export default { + props: { + samsara, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + name: { + type: "string", + label: "Name", + description: "The name of the webhook. This will appear in both Samsara's cloud dashboard and the API. It can be set or updated through the Samsara Dashboard or through the API at any time.", + }, + }, + methods: { + getWebhookProps() { + return {}; + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + _getHookId() { + return this.db.get("hookId"); + }, + checkEvent() { + return true; + }, + }, + hooks: { + async activate() { + const webhook = await this.samsara.createWebhook({ + data: { + name: this.name, + url: this.http.endpoint, + eventTypes: this.getEventTypes(), + }, + }); + this._setHookId(webhook.id); + }, + async deactivate() { + const hookId = this._getHookId(); + return await this.samsara.deleteWebhook(hookId); + }, + }, + async run({ body }) { + this.http.respond({ + status: 200, + }); + + if (this.checkEvent(body)) { + this.$emit(body, { + id: body.eventId, + summary: this.getSummary(body), + ts: body.eventTime, + }); + } + + }, +}; diff --git a/components/samsara/sources/new-document-submitted-instant/new-document-submitted-instant.mjs b/components/samsara/sources/new-document-submitted-instant/new-document-submitted-instant.mjs new file mode 100644 index 0000000000000..1a2f7f3091f96 --- /dev/null +++ b/components/samsara/sources/new-document-submitted-instant/new-document-submitted-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "samsara-new-document-submitted-instant", + name: "New Document Submitted (Instant)", + description: "Emit new event when a document is submitted.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + }, + methods: { + ...common.methods, + getEventTypes() { + return [ + "DocumentSubmitted", + ]; + }, + getSummary({ data }) { + return `New document with Id: ${data.document.id} successfully submitted!`; + }, + }, + sampleEmit, +}; diff --git a/components/samsara/sources/new-document-submitted-instant/test-event.mjs b/components/samsara/sources/new-document-submitted-instant/test-event.mjs new file mode 100644 index 0000000000000..7a109ba9c7d1d --- /dev/null +++ b/components/samsara/sources/new-document-submitted-instant/test-event.mjs @@ -0,0 +1,224 @@ +export default { + "eventId": "017db07f-6e95-470e-8cc0-a371f9deed2b", + "eventTime": "1970-01-20T06:39:05.683Z", + "eventType": "DocumentSubmitted", + "orgId": 20936, + "webhookId": "1411751028848270", + "data": { + "document": { + "conditionalFieldSections": [ + { + "conditionalFieldFirstIndex": 5083143870897228000, + "conditionalFieldLastIndex": 2770896918544835600, + "triggeringFieldIndex": 8389989517472915000, + "triggeringFieldValue": "Optiona 1" + }, + { + "conditionalFieldFirstIndex": 5083143870897228000, + "conditionalFieldLastIndex": 2770896918544835600, + "triggeringFieldIndex": 8389989517472915000, + "triggeringFieldValue": "Optiona 1" + }, + { + "conditionalFieldFirstIndex": 5083143870897228000, + "conditionalFieldLastIndex": 2770896918544835600, + "triggeringFieldIndex": 8389989517472915000, + "triggeringFieldValue": "Optiona 1" + } + ], + "createdAtTime": "1976-03-06T08:22:53Z", + "documentType": { + "id": "9814a1fa-f0c6-408b-bf85-51dc3bc71ac7", + "name": "Fleet Truck List" + }, + "fields": [ + { + "label": "Load weight", + "type": "photo", + "value": { + "barcodeValue": [ + { + "barcodeType": "org.gs1.EAN-13", + "barcodeValue": "0853883003114" + }, + { + "barcodeType": "org.gs1.EAN-13", + "barcodeValue": "0853883003114" + }, + { + "barcodeType": "org.gs1.EAN-13", + "barcodeValue": "0853883003114" + }, + { + "barcodeType": "org.gs1.EAN-13", + "barcodeValue": "0853883003114" + } + ], + "dateTimeValue": { + "dateTime": "1984-10-23T07:14:29Z" + }, + "multipleChoiceValue": [ + { + "selected": false, + "value": "Yes" + }, + { + "selected": false, + "value": "Yes" + }, + { + "selected": false, + "value": "Yes" + } + ], + "numberValue": 123.456, + "photoValue": [ + { + "id": "f5271458-21f9-4a9f-a290-780c6d8840ff", + "url": "https://samsara-driver-media-upload.s3.us-west-2.amazonaws.com/123456" + }, + { + "id": "f5271458-21f9-4a9f-a290-780c6d8840ff", + "url": "https://samsara-driver-media-upload.s3.us-west-2.amazonaws.com/123456" + } + ], + "scannedDocumentValue": [ + { + "id": "f5271458-21f9-4a9f-a290-780c6d8840ff", + "url": "https://samsara-driver-media-upload.s3.us-west-2.amazonaws.com/123456" + }, + { + "id": "f5271458-21f9-4a9f-a290-780c6d8840ff", + "url": "https://samsara-driver-media-upload.s3.us-west-2.amazonaws.com/123456" + }, + { + "id": "f5271458-21f9-4a9f-a290-780c6d8840ff", + "url": "https://samsara-driver-media-upload.s3.us-west-2.amazonaws.com/123456" + } + ], + "signatureValue": { + "id": "9814a1fa-f0c6-408b-bf85-51dc3bc71ac7", + "name": "John Smith", + "signedAtTime": "1996-01-30T06:14:20Z", + "url": "https://samsara-driver-media-upload.s3.us-west-2.amazonaws.com/123456" + }, + "stringValue": "Red Truck" + } + }, + { + "label": "Load weight", + "type": "photo", + "value": { + "barcodeValue": [ + { + "barcodeType": "org.gs1.EAN-13", + "barcodeValue": "0853883003114" + }, + { + "barcodeType": "org.gs1.EAN-13", + "barcodeValue": "0853883003114" + }, + { + "barcodeType": "org.gs1.EAN-13", + "barcodeValue": "0853883003114" + }, + { + "barcodeType": "org.gs1.EAN-13", + "barcodeValue": "0853883003114" + } + ], + "dateTimeValue": { + "dateTime": "1984-10-23T07:14:29Z" + }, + "multipleChoiceValue": [ + { + "selected": false, + "value": "Yes" + }, + { + "selected": false, + "value": "Yes" + }, + { + "selected": false, + "value": "Yes" + } + ], + "numberValue": 123.456, + "photoValue": [ + { + "id": "f5271458-21f9-4a9f-a290-780c6d8840ff", + "url": "https://samsara-driver-media-upload.s3.us-west-2.amazonaws.com/123456" + }, + { + "id": "f5271458-21f9-4a9f-a290-780c6d8840ff", + "url": "https://samsara-driver-media-upload.s3.us-west-2.amazonaws.com/123456" + } + ], + "scannedDocumentValue": [ + { + "id": "f5271458-21f9-4a9f-a290-780c6d8840ff", + "url": "https://samsara-driver-media-upload.s3.us-west-2.amazonaws.com/123456" + }, + { + "id": "f5271458-21f9-4a9f-a290-780c6d8840ff", + "url": "https://samsara-driver-media-upload.s3.us-west-2.amazonaws.com/123456" + }, + { + "id": "f5271458-21f9-4a9f-a290-780c6d8840ff", + "url": "https://samsara-driver-media-upload.s3.us-west-2.amazonaws.com/123456" + } + ], + "signatureValue": { + "id": "9814a1fa-f0c6-408b-bf85-51dc3bc71ac7", + "name": "John Smith", + "signedAtTime": "1996-01-30T06:14:20Z", + "url": "https://samsara-driver-media-upload.s3.us-west-2.amazonaws.com/123456" + }, + "stringValue": "Red Truck" + } + } + ], + "id": "9814a1fa-f0c6-408b-bf85-51dc3bc71ac7", + "name": "Dropoff Slip 123", + "notes": "Missing a crate", + "route": { + "externalIds": { + "myRouteId": "abc" + }, + "id": "131313", + "name": "Pineapple delivery" + }, + "routeStop": { + "externalIds": { + "siteId": "54" + }, + "id": "494123", + "name": "Company Warehouse #1" + }, + "state": "submitted", + "updatedAtTime": "2009-01-10T02:28:18Z" + }, + "driver": { + "externalIds": { + "payrollId": "ABFS18600" + }, + "id": "45646", + "name": "Driver Bob" + }, + "vehicle": { + "assetType": "vehicle", + "externalIds": { + "maintenanceId": "250020" + }, + "gateway": { + "model": "VG34", + "serial": "GFRV-43N-VGX" + }, + "id": "494123", + "licensePlate": "6SAM123", + "name": "Fleet Truck #1", + "vin": "1GBJ6P1B2HV112765" + } + } +} \ No newline at end of file diff --git a/components/samsara/sources/new-route-job-completion-instant/new-route-job-completion-instant.mjs b/components/samsara/sources/new-route-job-completion-instant/new-route-job-completion-instant.mjs new file mode 100644 index 0000000000000..0f5dec5445e6a --- /dev/null +++ b/components/samsara/sources/new-route-job-completion-instant/new-route-job-completion-instant.mjs @@ -0,0 +1,35 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "samsara-new-route-job-completion-instant", + name: "New Route Job Completion (Instant)", + description: "Emit new event when a job is completed on a Samsara route.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + }, + methods: { + ...common.methods, + getEventTypes() { + return [ + "RouteStopDeparture", + "RouteStopArrival", + ]; + }, + getSummary({ data }) { + return `Route ${data.route.name} successfully completed.`; + }, + checkEvent({ data }) { + const filteredStops = data.route.stops.filter((stop) => { + return stop.state != "departed"; + }); + + return !filteredStops.length; + }, + }, + sampleEmit, +}; diff --git a/components/samsara/sources/new-route-job-completion-instant/test-event.mjs b/components/samsara/sources/new-route-job-completion-instant/test-event.mjs new file mode 100644 index 0000000000000..62de0100b98ae --- /dev/null +++ b/components/samsara/sources/new-route-job-completion-instant/test-event.mjs @@ -0,0 +1,217 @@ +export default { + "eventId": "017db07f-6e95-470e-8cc0-a371f9deed2b", + "eventTime": "1970-01-20T06:39:05.683Z", + "eventType": "RouteStopArrival", + "orgId": 20936, + "webhookId": "1411751028848270", + "data": { + "driver": { + "externalIds": { + "payrollId": "ABFS18600" + }, + "id": "45646", + "name": "Driver Bob" + }, + "operation": "stop arrived", + "route": { + "actualRouteEndTime": "2019-06-13T19:08:25Z", + "actualRouteStartTime": "2019-06-13T19:08:25Z", + "externalIds": { + "myRouteId": "abc" + }, + "id": "342341", + "name": "Bid 123", + "notes": "These are my notes", + "scheduledRouteEndTime": "2019-06-13T19:08:25Z", + "scheduledRouteStartTime": "2019-06-13T19:08:25Z", + "settings": { + "routeCompletionCondition": "arriveLastStop", + "routeStartingCondition": "departFirstStop" + }, + "stops": [ + { + "actualArrivalTime": "2006-01-02T15:04:05+07:00", + "actualDepartureTime": "2006-01-02T15:04:05+07:00", + "address": { + "externalIds": { + "siteId": "54" + }, + "id": "494123", + "name": "Company Office #1" + }, + "documents": [ + { + "id": "494123", + "name": "Fuel Receipt Wichita" + }, + { + "id": "494123", + "name": "Fuel Receipt Wichita" + } + ], + "enRouteTime": "2006-01-02T15:04:05+07:00", + "eta": "2006-01-02T15:04:05+07:00", + "externalIds": { + "siteId": "54" + }, + "id": "324231", + "liveSharingUrl": "https://cloud.samsara.com/fleet/viewer/job/fleet_viewer_token", + "locationLiveSharingLinks": [ + { + "expiresAtTime": "2020-01-27T07:06:25Z", + "liveSharingUrl": "https://cloud.samsara.com/o/123456/fleet/viewer/address/gEAitEnnOwcv92cuPzcU", + "name": "Name" + }, + { + "expiresAtTime": "2020-01-27T07:06:25Z", + "liveSharingUrl": "https://cloud.samsara.com/o/123456/fleet/viewer/address/gEAitEnnOwcv92cuPzcU", + "name": "Name" + } + ], + "name": "Stop #1", + "notes": "These are my notes", + "scheduledArrivalTime": "2019-06-13T19:08:25Z", + "scheduledDepartureTime": "2019-06-13T19:08:25Z", + "singleUseLocation": { + "address": "1234 Main St, San Jose, CA", + "latitude": 123.456, + "longitude": 37.459 + }, + "skippedTime": "2006-01-02T15:04:05+07:00", + "state": "scheduled" + }, + { + "actualArrivalTime": "2006-01-02T15:04:05+07:00", + "actualDepartureTime": "2006-01-02T15:04:05+07:00", + "address": { + "externalIds": { + "siteId": "54" + }, + "id": "494123", + "name": "Company Office #1" + }, + "documents": [ + { + "id": "494123", + "name": "Fuel Receipt Wichita" + }, + { + "id": "494123", + "name": "Fuel Receipt Wichita" + } + ], + "enRouteTime": "2006-01-02T15:04:05+07:00", + "eta": "2006-01-02T15:04:05+07:00", + "externalIds": { + "siteId": "54" + }, + "id": "324231", + "liveSharingUrl": "https://cloud.samsara.com/fleet/viewer/job/fleet_viewer_token", + "locationLiveSharingLinks": [ + { + "expiresAtTime": "2020-01-27T07:06:25Z", + "liveSharingUrl": "https://cloud.samsara.com/o/123456/fleet/viewer/address/gEAitEnnOwcv92cuPzcU", + "name": "Name" + }, + { + "expiresAtTime": "2020-01-27T07:06:25Z", + "liveSharingUrl": "https://cloud.samsara.com/o/123456/fleet/viewer/address/gEAitEnnOwcv92cuPzcU", + "name": "Name" + } + ], + "name": "Stop #1", + "notes": "These are my notes", + "scheduledArrivalTime": "2019-06-13T19:08:25Z", + "scheduledDepartureTime": "2019-06-13T19:08:25Z", + "singleUseLocation": { + "address": "1234 Main St, San Jose, CA", + "latitude": 123.456, + "longitude": 37.459 + }, + "skippedTime": "2006-01-02T15:04:05+07:00", + "state": "scheduled" + }, + { + "actualArrivalTime": "2006-01-02T15:04:05+07:00", + "actualDepartureTime": "2006-01-02T15:04:05+07:00", + "address": { + "externalIds": { + "siteId": "54" + }, + "id": "494123", + "name": "Company Office #1" + }, + "documents": [ + { + "id": "494123", + "name": "Fuel Receipt Wichita" + }, + { + "id": "494123", + "name": "Fuel Receipt Wichita" + } + ], + "enRouteTime": "2006-01-02T15:04:05+07:00", + "eta": "2006-01-02T15:04:05+07:00", + "externalIds": { + "siteId": "54" + }, + "id": "324231", + "liveSharingUrl": "https://cloud.samsara.com/fleet/viewer/job/fleet_viewer_token", + "locationLiveSharingLinks": [ + { + "expiresAtTime": "2020-01-27T07:06:25Z", + "liveSharingUrl": "https://cloud.samsara.com/o/123456/fleet/viewer/address/gEAitEnnOwcv92cuPzcU", + "name": "Name" + }, + { + "expiresAtTime": "2020-01-27T07:06:25Z", + "liveSharingUrl": "https://cloud.samsara.com/o/123456/fleet/viewer/address/gEAitEnnOwcv92cuPzcU", + "name": "Name" + } + ], + "name": "Stop #1", + "notes": "These are my notes", + "scheduledArrivalTime": "2019-06-13T19:08:25Z", + "scheduledDepartureTime": "2019-06-13T19:08:25Z", + "singleUseLocation": { + "address": "1234 Main St, San Jose, CA", + "latitude": 123.456, + "longitude": 37.459 + }, + "skippedTime": "2006-01-02T15:04:05+07:00", + "state": "scheduled" + } + ] + }, + "routeStopDetails": { + "actualArrivalTime": "2006-01-02T15:04:05+07:00", + "actualDepartureTime": "2006-01-02T15:04:05+07:00", + "enRouteTime": "2006-01-02T15:04:05+07:00", + "eta": "2006-01-02T15:04:05+07:00", + "externalIds": { + "siteId": "54" + }, + "id": "141414", + "liveSharingUrl": "https://cloud.samsara.com/fleet/viewer/job/fleet_viewer_token", + "skippedTime": "2006-01-02T15:04:05+07:00", + "state": "scheduled" + }, + "time": "2020-01-27T07:06:25Z", + "type": "route tracking", + "vehicle": { + "assetType": "vehicle", + "externalIds": { + "maintenanceId": "250020" + }, + "gateway": { + "model": "VG34", + "serial": "GFRV-43N-VGX" + }, + "id": "494123", + "licensePlate": "6SAM123", + "name": "Fleet Truck #1", + "vin": "1GBJ6P1B2HV112765" + } + } +} \ No newline at end of file diff --git a/components/samsara/sources/new-route-started-instant/new-route-started-instant.mjs b/components/samsara/sources/new-route-started-instant/new-route-started-instant.mjs new file mode 100644 index 0000000000000..2e51294d81204 --- /dev/null +++ b/components/samsara/sources/new-route-started-instant/new-route-started-instant.mjs @@ -0,0 +1,34 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "samsara-new-route-started-instant", + name: "New Route Started (Instant)", + description: "Emit new event when a new route is stated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + }, + methods: { + ...common.methods, + getEventTypes() { + return [ + "RouteStopDeparture", + ]; + }, + getSummary({ data }) { + return `New route with Id: ${data.route.id} successfully started!`; + }, + checkEvent({ data }) { + const filteredStops = data.route.stops.filter((stop) => { + return stop.state === "departed"; + }); + + return filteredStops.length === 1; + }, + }, + sampleEmit, +}; diff --git a/components/samsara/sources/new-route-started-instant/test-event.mjs b/components/samsara/sources/new-route-started-instant/test-event.mjs new file mode 100644 index 0000000000000..e868690baba6d --- /dev/null +++ b/components/samsara/sources/new-route-started-instant/test-event.mjs @@ -0,0 +1,217 @@ +export default { + "eventId": "017db07f-6e95-470e-8cc0-a371f9deed2b", + "eventTime": "1970-01-20T06:39:05.683Z", + "eventType": "RouteStopDeparture", + "orgId": 20936, + "webhookId": "1411751028848270", + "data": { + "driver": { + "externalIds": { + "payrollId": "ABFS18600" + }, + "id": "45646", + "name": "Driver Bob" + }, + "operation": "stop arrived", + "route": { + "actualRouteEndTime": "2019-06-13T19:08:25Z", + "actualRouteStartTime": "2019-06-13T19:08:25Z", + "externalIds": { + "myRouteId": "abc" + }, + "id": "342341", + "name": "Bid 123", + "notes": "These are my notes", + "scheduledRouteEndTime": "2019-06-13T19:08:25Z", + "scheduledRouteStartTime": "2019-06-13T19:08:25Z", + "settings": { + "routeCompletionCondition": "arriveLastStop", + "routeStartingCondition": "departFirstStop" + }, + "stops": [ + { + "actualArrivalTime": "2006-01-02T15:04:05+07:00", + "actualDepartureTime": "2006-01-02T15:04:05+07:00", + "address": { + "externalIds": { + "siteId": "54" + }, + "id": "494123", + "name": "Company Office #1" + }, + "documents": [ + { + "id": "494123", + "name": "Fuel Receipt Wichita" + }, + { + "id": "494123", + "name": "Fuel Receipt Wichita" + } + ], + "enRouteTime": "2006-01-02T15:04:05+07:00", + "eta": "2006-01-02T15:04:05+07:00", + "externalIds": { + "siteId": "54" + }, + "id": "324231", + "liveSharingUrl": "https://cloud.samsara.com/fleet/viewer/job/fleet_viewer_token", + "locationLiveSharingLinks": [ + { + "expiresAtTime": "2020-01-27T07:06:25Z", + "liveSharingUrl": "https://cloud.samsara.com/o/123456/fleet/viewer/address/gEAitEnnOwcv92cuPzcU", + "name": "Name" + }, + { + "expiresAtTime": "2020-01-27T07:06:25Z", + "liveSharingUrl": "https://cloud.samsara.com/o/123456/fleet/viewer/address/gEAitEnnOwcv92cuPzcU", + "name": "Name" + } + ], + "name": "Stop #1", + "notes": "These are my notes", + "scheduledArrivalTime": "2019-06-13T19:08:25Z", + "scheduledDepartureTime": "2019-06-13T19:08:25Z", + "singleUseLocation": { + "address": "1234 Main St, San Jose, CA", + "latitude": 123.456, + "longitude": 37.459 + }, + "skippedTime": "2006-01-02T15:04:05+07:00", + "state": "scheduled" + }, + { + "actualArrivalTime": "2006-01-02T15:04:05+07:00", + "actualDepartureTime": "2006-01-02T15:04:05+07:00", + "address": { + "externalIds": { + "siteId": "54" + }, + "id": "494123", + "name": "Company Office #1" + }, + "documents": [ + { + "id": "494123", + "name": "Fuel Receipt Wichita" + }, + { + "id": "494123", + "name": "Fuel Receipt Wichita" + } + ], + "enRouteTime": "2006-01-02T15:04:05+07:00", + "eta": "2006-01-02T15:04:05+07:00", + "externalIds": { + "siteId": "54" + }, + "id": "324231", + "liveSharingUrl": "https://cloud.samsara.com/fleet/viewer/job/fleet_viewer_token", + "locationLiveSharingLinks": [ + { + "expiresAtTime": "2020-01-27T07:06:25Z", + "liveSharingUrl": "https://cloud.samsara.com/o/123456/fleet/viewer/address/gEAitEnnOwcv92cuPzcU", + "name": "Name" + }, + { + "expiresAtTime": "2020-01-27T07:06:25Z", + "liveSharingUrl": "https://cloud.samsara.com/o/123456/fleet/viewer/address/gEAitEnnOwcv92cuPzcU", + "name": "Name" + } + ], + "name": "Stop #1", + "notes": "These are my notes", + "scheduledArrivalTime": "2019-06-13T19:08:25Z", + "scheduledDepartureTime": "2019-06-13T19:08:25Z", + "singleUseLocation": { + "address": "1234 Main St, San Jose, CA", + "latitude": 123.456, + "longitude": 37.459 + }, + "skippedTime": "2006-01-02T15:04:05+07:00", + "state": "scheduled" + }, + { + "actualArrivalTime": "2006-01-02T15:04:05+07:00", + "actualDepartureTime": "2006-01-02T15:04:05+07:00", + "address": { + "externalIds": { + "siteId": "54" + }, + "id": "494123", + "name": "Company Office #1" + }, + "documents": [ + { + "id": "494123", + "name": "Fuel Receipt Wichita" + }, + { + "id": "494123", + "name": "Fuel Receipt Wichita" + } + ], + "enRouteTime": "2006-01-02T15:04:05+07:00", + "eta": "2006-01-02T15:04:05+07:00", + "externalIds": { + "siteId": "54" + }, + "id": "324231", + "liveSharingUrl": "https://cloud.samsara.com/fleet/viewer/job/fleet_viewer_token", + "locationLiveSharingLinks": [ + { + "expiresAtTime": "2020-01-27T07:06:25Z", + "liveSharingUrl": "https://cloud.samsara.com/o/123456/fleet/viewer/address/gEAitEnnOwcv92cuPzcU", + "name": "Name" + }, + { + "expiresAtTime": "2020-01-27T07:06:25Z", + "liveSharingUrl": "https://cloud.samsara.com/o/123456/fleet/viewer/address/gEAitEnnOwcv92cuPzcU", + "name": "Name" + } + ], + "name": "Stop #1", + "notes": "These are my notes", + "scheduledArrivalTime": "2019-06-13T19:08:25Z", + "scheduledDepartureTime": "2019-06-13T19:08:25Z", + "singleUseLocation": { + "address": "1234 Main St, San Jose, CA", + "latitude": 123.456, + "longitude": 37.459 + }, + "skippedTime": "2006-01-02T15:04:05+07:00", + "state": "scheduled" + } + ] + }, + "routeStopDetails": { + "actualArrivalTime": "2006-01-02T15:04:05+07:00", + "actualDepartureTime": "2006-01-02T15:04:05+07:00", + "enRouteTime": "2006-01-02T15:04:05+07:00", + "eta": "2006-01-02T15:04:05+07:00", + "externalIds": { + "siteId": "54" + }, + "id": "141414", + "liveSharingUrl": "https://cloud.samsara.com/fleet/viewer/job/fleet_viewer_token", + "skippedTime": "2006-01-02T15:04:05+07:00", + "state": "scheduled" + }, + "time": "2020-01-27T07:06:25Z", + "type": "route tracking", + "vehicle": { + "assetType": "vehicle", + "externalIds": { + "maintenanceId": "250020" + }, + "gateway": { + "model": "VG34", + "serial": "GFRV-43N-VGX" + }, + "id": "494123", + "licensePlate": "6SAM123", + "name": "Fleet Truck #1", + "vin": "1GBJ6P1B2HV112765" + } + } +} \ No newline at end of file diff --git a/components/san_francisco_open_data_datasf/README.md b/components/san_francisco_open_data_datasf/README.md index e0f3178ec9c76..b863b73df51df 100644 --- a/components/san_francisco_open_data_datasf/README.md +++ b/components/san_francisco_open_data_datasf/README.md @@ -1,18 +1,11 @@ # Overview -Have you ever been curious about the data available in San Francisco? The -DataSF API provides you with access to dozens of datasets, giving you powerful -insights into the heart of the city. With this data, you can build and create a -variety of projects, from visualizations to exploration tools. +The San Francisco Open Data - DataSF API unlocks a wealth of government data spanning multiple domains such as transportation, housing, and public health. It provides developers with access to rich datasets, which can be integrated into applications to derive insights, inform decision-making, and power data-driven solutions. Pipedream's serverless platform amplifies this potential by enabling users to create automated workflows that leverage this data in concert with other apps and services. -The DataSF API provides a wealth of data about San Francisco, from crime -statistics to building permits. Here are some example projects you can create -with this data: +# Example Use Cases -- A map that visualizes live crime data -- A tool to search for available housing -- A comprehensive transportation app -- An interactive dashboard for budget data -- A tool that compares various industry trends in different areas of the city -- A tool to explore San Francisco's population and demographics -- A tool to compare different public services in the area +- **Automated Alert System for Street Cleaning Schedules**: Extract street cleaning schedules from DataSF and use Twilio SMS integration on Pipedream to notify residents the day before their street is scheduled for cleaning, minimizing parking fines and improving urban cleanliness. + +- **Real-Time Public Health Dashboard**: Use DataSF's public health datasets to trigger a workflow on Pipedream that aggregates the latest health metrics, processes them, and then sends the data to a Google Sheets dashboard, providing real-time insights for public health officials and the public. + +- **Housing Market Analysis Bot**: Combine housing data available through DataSF with Pipedream's Slack integration to create a bot that delivers weekly updates on housing market trends to realtors, investors, or interested buyers in a dedicated Slack channel. diff --git a/components/san_francisco_open_data_datasf/package.json b/components/san_francisco_open_data_datasf/package.json new file mode 100644 index 0000000000000..0b306305cfa85 --- /dev/null +++ b/components/san_francisco_open_data_datasf/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/san_francisco_open_data_datasf", + "version": "0.6.0", + "description": "Pipedream san_francisco_open_data_datasf Components", + "main": "san_francisco_open_data_datasf.app.mjs", + "keywords": [ + "pipedream", + "san_francisco_open_data_datasf" + ], + "homepage": "https://pipedream.com/apps/san_francisco_open_data_datasf", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/sapling/README.md b/components/sapling/README.md new file mode 100644 index 0000000000000..33aec690575a7 --- /dev/null +++ b/components/sapling/README.md @@ -0,0 +1,11 @@ +# Overview + +The Sapling API, designed for language checking and grammar correction, can bolster communication across various platforms. Integrating Sapling with Pipedream allows you to automate text analysis and correction workflows. By harnessing Pipedream's event-driven architecture, you can trigger actions based on text input, process the text through Sapling's API for grammar and style suggestions, and output the improved content to a desired location, such as a CRM, a database, or a communication tool. + +# Example Use Cases + +- **Automated Email Proofreading**: Before sending out important emails from your CRM, they could automatically go through Sapling for grammar and style checks. The corrected emails can then be sent out or returned for further review. + +- **Customer Support Ticket Enhancement**: As tickets are received, use Sapling to refine the language in automated responses. This ensures customer communications are clear and professional. + +- **Real-Time Slack Message Improvements**: For a team emphasizing communication quality, integrate Sapling to assess messages before they're posted to Slack channels. A bot can suggest edits to users, helping maintain high standards. diff --git a/components/sapling/package.json b/components/sapling/package.json index 142566ab9fc89..1f210ff8bd792 100644 --- a/components/sapling/package.json +++ b/components/sapling/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/sapling_ai/README.md b/components/sapling_ai/README.md new file mode 100644 index 0000000000000..f6bd03f61396a --- /dev/null +++ b/components/sapling_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +Sapling.ai offers an AI-driven writing assistant that can help you write better and faster by providing grammar and style suggestions. With its API, you can automate text analysis and correction processes within Pipedream workflows. By integrating Sapling.ai's API in Pipedream, you can create powerful automations that enhance writing quality across various applications, from customer support messages to content creation. + +# Example Use Cases + +- **Automated Grammar and Style Checks for Customer Support Tickets**: Use Sapling.ai to analyze and suggest improvements to customer support messages before they are sent. Integrate with a customer support platform like Zendesk to fetch tickets, process the text with Sapling.ai, and post the revised content back to the ticket. + +- **Content Quality Assurance for CMS Platforms**: Before publishing content on a CMS like WordPress, automate the proofreading process using Sapling.ai. Pull draft posts from WordPress, send them through Sapling.ai's API for review, and then automatically update the post drafts with suggestions. + +- **Enhanced Email Communication**: Improve the quality of outbound emails by incorporating Sapling.ai's API. Connect to an email service like Gmail, scan outgoing emails for errors or style issues, apply Sapling.ai's corrections, and send the polished emails to recipients. diff --git a/components/sare/README.md b/components/sare/README.md new file mode 100644 index 0000000000000..ed76ff00fec12 --- /dev/null +++ b/components/sare/README.md @@ -0,0 +1,11 @@ +# Overview + +The SARE API provides robust tools for email marketing and automation, enabling users to manage subscribers, send emails, and analyze campaigns directly via API. By leveraging SARE's features on Pipedream, you can automate workflows involving email operations, integrate with other platforms for enhanced data handling and customer engagement, and streamline communications in various marketing campaigns. This integration can particularly benefit marketers looking to automate responses based on subscriber behavior or integrate email marketing metrics with other business tools. + +# Example Use Cases + +- **Automated Welcome Email Sequence**: Trigger a workflow on Pipedream when a new user signs up via your app, using the Shopify or WordPress trigger. The workflow would then use the SARE API to add the subscriber to a specific mailing list and send a series of pre-defined welcome emails over several days, enhancing user engagement right from the start. + +- **User Feedback Collection Campaign**: After a customer purchases a product, use a Pipedream workflow triggered by a Stripe or PayPal purchase event. This workflow could utilize the SARE API to send a personalized email requesting product feedback or a review, thereby increasing direct interaction and gathering valuable customer insights. + +- **Re-engagement Campaigns for Dormant Users**: Set up a workflow triggered by a custom cron job that checks for users who haven't interacted with your service for a set period, perhaps 6 months. Using the SARE API through Pipedream, you can automatically send re-engagement emails offering special promotions or updates about new features, aiming to reactivate these dormant users. diff --git a/components/sare/actions/add-email/add-email.mjs b/components/sare/actions/add-email/add-email.mjs new file mode 100644 index 0000000000000..32363d8172aad --- /dev/null +++ b/components/sare/actions/add-email/add-email.mjs @@ -0,0 +1,69 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import sare from "../../sare.app.mjs"; + +export default { + key: "sare-add-email", + name: "Add Email", + description: "Add an email to SARE. Optionally, assign the subscriber to a group. [See the documentation](https://dev.sare.pl/rest-api/other/index.html#post-/email/add)", + version: "0.0.1", + type: "action", + props: { + sare, + email: { + propDefinition: [ + sare, + "email", + ], + }, + gsm: { + propDefinition: [ + sare, + "gsm", + ], + }, + groups: { + propDefinition: [ + sare, + "groups", + ], + description: "The group to assign the email to.", + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "The name assigned to the email address.", + optional: true, + }, + comment: { + type: "string", + label: "Comment", + description: "Address comment.", + optional: true, + }, + }, + async run({ $ }) { + const { response } = await this.sare.addEmail({ + $, + data: { + emails: [ + { + email: this.email, + gsm: this.gsm, + groups: this.groups && parseObject(this.groups), + name: this.name, + comment: this.comment, + }, + ], + }, + }); + + if (response.error?.length) { + throw new ConfigurationError(response.error[0].why); + } + + $.export("$summary", `Successfully added email: ${this.email}`); + return response; + }, +}; diff --git a/components/sare/actions/remove-from-group/remove-from-group.mjs b/components/sare/actions/remove-from-group/remove-from-group.mjs new file mode 100644 index 0000000000000..dff9780862265 --- /dev/null +++ b/components/sare/actions/remove-from-group/remove-from-group.mjs @@ -0,0 +1,35 @@ +import { parseObject } from "../../common/utils.mjs"; +import sare from "../../sare.app.mjs"; + +export default { + key: "sare-remove-from-group", + name: "Remove Email from Groups", + description: "Remove email from specified groups in SARE. [See the documentation](https://dev.sare.pl/rest-api/other/index.html#post-/group/remove_emails)", + version: "0.0.1", + type: "action", + props: { + sare, + emails: { + propDefinition: [ + sare, + "emails", + ], + }, + groups: { + propDefinition: [ + sare, + "groups", + ], + }, + }, + async run({ $ }) { + const response = await this.sare.removeEmailFromGroups({ + data: { + emails: this.emails && parseObject(this.emails), + groups: this.groups && parseObject(this.groups), + }, + }); + $.export("$summary", "Successfully removed emails from groups"); + return response; + }, +}; diff --git a/components/sare/actions/send-email/send-email.mjs b/components/sare/actions/send-email/send-email.mjs new file mode 100644 index 0000000000000..37b9605c22da2 --- /dev/null +++ b/components/sare/actions/send-email/send-email.mjs @@ -0,0 +1,98 @@ +import { ConfigurationError } from "@pipedream/platform"; +import sare from "../../sare.app.mjs"; + +export default { + key: "sare-send-email", + name: "Send Transactional Email", + description: "Send a transactional email. [See the documentation](https://dev.sare.pl/rest-api/other/index.html#post-/send/mail/transactional)", + version: "0.0.1", + type: "action", + props: { + sare, + email: { + propDefinition: [ + sare, + "email", + ], + }, + subject: { + propDefinition: [ + sare, + "subject", + ], + }, + from: { + propDefinition: [ + sare, + "from", + ], + }, + newsletterType: { + type: "string", + label: "Newsletter Type", + description: "The type of the newsletter you want to send.", + optional: true, + options: [ + "ready", + "temporary", + ], + }, + newsletter: { + propDefinition: [ + sare, + "newsletter", + ({ newsletterType }) => ({ + newsletterType, + }), + ], + optional: true, + }, + html: { + type: "string", + label: "HTML", + description: "Message content in html format.", + optional: true, + }, + txt: { + type: "string", + label: "TXT", + description: "Message content in text format.", + optional: true, + }, + encoding: { + type: "string", + label: "Encoding", + description: "Coding of the sent newsletter. The coding must be consistent with those available in the system.", + optional: true, + }, + replyto: { + type: "string", + label: "Reply To", + description: "Reply-To field for shipping. The value should be a syntactically valid email address.", + optional: true, + }, + }, + async run({ $ }) { + if ((!this.newsletter) && (!this.html) && (!this.txt)) { + throw new ConfigurationError("You must provide at least **Newsletter**, **HTML** or **TXT**."); + } + + const response = await this.sare.sendTransactionalEmail({ + $, + data: [ + { + email: this.email, + subject: this.subject, + from: this.from, + newsletter: this.newsletter, + html: this.html, + txt: this.txt, + encoding: this.encoding, + replyto: this.replyto, + }, + ], + }); + $.export("$summary", `Successfully sent email to ${this.email}`); + return response; + }, +}; diff --git a/components/sare/common/utils.mjs b/components/sare/common/utils.mjs new file mode 100644 index 0000000000000..154701004e72a --- /dev/null +++ b/components/sare/common/utils.mjs @@ -0,0 +1,18 @@ +export const parseObject = (obj) => { + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + return JSON.parse(obj); + } + return obj; +}; diff --git a/components/sare/package.json b/components/sare/package.json new file mode 100644 index 0000000000000..f121dcddbea0d --- /dev/null +++ b/components/sare/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/sare", + "version": "0.1.0", + "description": "Pipedream SARE Components", + "main": "sare.app.mjs", + "keywords": [ + "pipedream", + "sare" + ], + "homepage": "https://pipedream.com/apps/sare", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} + diff --git a/components/sare/sare.app.mjs b/components/sare/sare.app.mjs new file mode 100644 index 0000000000000..4d25201603ca3 --- /dev/null +++ b/components/sare/sare.app.mjs @@ -0,0 +1,123 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "sare", + propDefinitions: { + email: { + type: "string", + label: "Email", + description: "The email address of the subscriber.", + }, + gsm: { + type: "string", + label: "GSM", + description: "The GSM number of the subscriber.", + optional: true, + }, + groups: { + type: "string[]", + label: "Groups", + description: "The groups to remove the email subscribers from.", + async options() { + const { response } = await this.listGroups(); + + return response.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + emails: { + type: "string[]", + label: "Emails", + description: "The email addresses of the subscribers to remove from groups.", + }, + subject: { + type: "string", + label: "Subject", + description: "The subject of the transactional email.", + }, + from: { + type: "string", + label: "From", + description: "The sender email address.", + }, + newsletter: { + type: "integer", + label: "Newsletter", + description: "Newsletter ID in the system.", + async options({ + page, newsletterType, + }) { + const { response: { data } } = await this.listNewsletters({ + newsletterType, + page, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return `https://s.enewsletter.pl/api/v1/${this.$auth.uid}`; + }, + _headers() { + return { + "Content-Type": "application/json", + "ApiKey": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: this._headers(), + }); + }, + addEmail(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/email/add", + ...opts, + }); + }, + listGroups(opts = {}) { + return this._makeRequest({ + path: "/group/list", + ...opts, + }); + }, + listNewsletters({ + newsletterType, page, + }) { + return this._makeRequest({ + path: `/newsletter/list/${newsletterType}/${page}`, + }); + }, + removeEmailFromGroups(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/group/remove_emails", + ...opts, + }); + }, + sendTransactionalEmail(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/send/mail/transactional", + ...opts, + }); + }, + }, +}; diff --git a/components/satismeter/README.md b/components/satismeter/README.md new file mode 100644 index 0000000000000..102017c3e08ac --- /dev/null +++ b/components/satismeter/README.md @@ -0,0 +1,11 @@ +# Overview + +SatisMeter captures customer feedback and sentiment directly through targeted surveys, providing insights into customer satisfaction and product engagement. With the SatisMeter API on Pipedream, you can automate interactions with survey data, sync customer information with other services, and trigger actions based on feedback scores. By leveraging Pipedream's serverless platform, you can create workflows that respond in real-time to survey results, enrich customer profiles, and influence your product's roadmap with actionable insights. + +# Example Use Cases + +- **Automate Feedback Collection into CRM**: Sync SatisMeter survey responses with your CRM, like Salesforce or HubSpot, to update customer profiles with their latest feedback scores. This can help sales and support teams tailor their outreach and support based on up-to-date customer sentiment. + +- **Trigger Actions Based on Sentiment Analysis**: Use SatisMeter survey results to perform sentiment analysis. If negative feedback is detected, automatically create a task in a project management tool like Asana or Trello for your customer success team to follow up and address any issues. + +- **Email Campaigns Triggered by Survey Responses**: Integrate SatisMeter with email marketing platforms such as Mailchimp or SendGrid. Trigger personalized email campaigns based on specific survey responses to engage customers, provide educational content, or offer incentives to those who may have given lower satisfaction scores. diff --git a/components/satuit/README.md b/components/satuit/README.md new file mode 100644 index 0000000000000..6776b708981f6 --- /dev/null +++ b/components/satuit/README.md @@ -0,0 +1,11 @@ +# Overview + +The Satuit API enables seamless integration of asset management and CRM functionalities into various business processes. With it, you can automate tasks such as data synchronization, report generation, and client management. Pipedream, with its serverless execution model, allows you to connect Satuit to hundreds of other apps to create powerful workflows without managing any infrastructure. You could, for instance, trigger a workflow with a new database entry, process the data within Pipedream, and then use the Satuit API to update customer records or create tasks for follow-ups. + +# Example Use Cases + +- **Automate Client Onboarding**: Upon receiving a new client form submission from a website (like Typeform or Google Forms), use Pipedream to parse the submitted data and automatically create a new client record in Satuit. + +- **Sync Email Campaign Responses**: Connect Satuit with an email marketing service like Mailchimp via Pipedream. When a recipient opens an email or clicks a link, it triggers a Pipedream workflow that logs the activity in the Satuit CRM, helping you to keep track of engagement. + +- **Daily Portfolio Reports**: Set up a Pipedream scheduled workflow to generate daily portfolio reports from Satuit and send them to stakeholders via a messaging platform like Slack. This ensures that all relevant parties receive timely investment updates. diff --git a/components/saucelabs/README.md b/components/saucelabs/README.md new file mode 100644 index 0000000000000..2f31e95273f4c --- /dev/null +++ b/components/saucelabs/README.md @@ -0,0 +1,11 @@ +# Overview + +Sauce Labs API lets you automate your web and mobile app testing. This means you can create, manage, and run tests programmatically on different browsers and devices, gather results, and tap into Sauce Labs' extensive browser/device coverage without manual intervention. With Pipedream, you can connect Sauce Labs to a wide array of other services to streamline your testing pipeline, react to testing events in real-time, and integrate test outcomes into your CI/CD workflow, issue tracking, or notification systems. + +# Example Use Cases + +- **Automate Test Execution after Code Commits**: Trigger a suite of automated tests on Sauce Labs when new code is committed to a GitHub repository. Use Pipedream's GitHub trigger to start Sauce Labs tests and then handle results with subsequent steps. + +- **Monitor Test Results and Notify Teams**: Set up a workflow that listens for completed tests on Sauce Labs, parses the results, and sends a formatted report to Slack, email, or another communication platform to keep your team informed. + +- **Integrate Test Outcomes with JIRA**: After tests are completed in Sauce Labs, use the results to create or update JIRA issues automatically. This can help in tracking bugs or test failures directly within your project management tool. diff --git a/components/savvycal/README.md b/components/savvycal/README.md new file mode 100644 index 0000000000000..55cc5e5fb01c2 --- /dev/null +++ b/components/savvycal/README.md @@ -0,0 +1,11 @@ +# Overview + +SavvyCal makes scheduling meetings less of a hassle with personalized links and overlaying of calendars to find optimal meeting times. The SavvyCal API allows for the automated creation and management of scheduling links, the coordination of calendars, and the customization of scheduling parameters. Within Pipedream, you can integrate SavvyCal with various other apps to streamline your scheduling processes. This means you can automatically create meetings based on triggers from other apps, sync schedules across platforms, or send notifications when new meetings are scheduled. + +# Example Use Cases + +- **Automate Meeting Invitations After Webinar Sign-ups**: When someone signs up for your webinar through an app like Eventbrite, trigger a Pipedream workflow that sends a personalized SavvyCal scheduling link to the registrant, inviting them to book a one-on-one follow-up meeting. + +- **Sync New Meetings to a Google Calendar**: Once a new meeting is scheduled in SavvyCal, use Pipedream to automatically create an event in a Google Calendar, ensuring all your appointments are in sync and accessible across your devices. + +- **Send Notifications for New Meetings**: Set up a Pipedream workflow that listens for new meetings created in SavvyCal and then sends a message with the meeting details to a Slack channel, keeping your team informed about upcoming appointments. diff --git a/components/scale_ai/README.md b/components/scale_ai/README.md new file mode 100644 index 0000000000000..6e042ed8d67e4 --- /dev/null +++ b/components/scale_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +Scale AI offers an API to automate and streamline data labeling for machine learning applications, providing access to a global workforce and sophisticated tools. With Scale AI's API on Pipedream, you can integrate scalable data annotation workflows directly into your apps. Trigger tasks, manage datasets, and receive annotated data, all within Pipedream's serverless platform. This enables seamless automation of labeling tasks, integration with machine learning pipelines, and real-time updates on annotations. + +# Example Use Cases + +- **Automated Image Labeling Pipeline**: Trigger Scale AI tasks for image labeling whenever new images are uploaded to an S3 bucket. Use AWS S3 trigger to start the workflow on Pipedream, process images with Scale AI, and finally store labeled data back in S3 or send it to a database. + +- **Real-time Data Annotation Monitoring**: Set up a Pipedream workflow that listens for Scale AI webhook events. Process these events to monitor the progress of data annotation tasks in real-time. You could further integrate this with Slack or email to notify your team as tasks are completed or reviewed. + +- **Sentiment Analysis Feedback Loop**: Use Scale AI for sentiment analysis on user-submitted text. Start with a trigger from a database or a web form on Pipedream. Send the text to Scale AI for sentiment analysis, and then use the results to update user profiles, or route the feedback to the correct team within your organization. diff --git a/components/scalr/README.md b/components/scalr/README.md new file mode 100644 index 0000000000000..27a38d21ec09a --- /dev/null +++ b/components/scalr/README.md @@ -0,0 +1,11 @@ +# Overview + +The Scalr API enables programmatic interaction with Scalr's infrastructure management and automation capabilities. On Pipedream, you can harness this API to create diverse workflows that automate cloud resource provisioning, management, and monitoring. These workflows can trigger on various events and integrate with other services to streamline your DevOps processes. + +# Example Use Cases + +- **Automated Resource Scaling**: Create a workflow that listens for webhooks signaling high traffic on your site. Use the Scalr API to automatically scale up your cloud infrastructure and handle the load, then scale down when traffic normalizes. + +- **Compliance Monitoring**: Set up a scheduled workflow that checks the state of your infrastructure against compliance rules. If it finds any discrepancies, the workflow can alert your team via Slack or email and even create a ticket in Jira. + +- **Infrastructure Deployment from Git Push**: Trigger a workflow on a git push event to a specific branch in GitHub. The workflow uses the Scalr API to deploy the latest version of your application on your cloud infrastructure, ensuring continuous deployment with minimal manual intervention. diff --git a/components/schedule/README.md b/components/schedule/README.md index f1694752c1489..f8d41bddef47c 100644 --- a/components/schedule/README.md +++ b/components/schedule/README.md @@ -1,16 +1,11 @@ # Overview -With Schedule - A trigger provided by Pipedream - You can easily build -automated workflows that run on regular times or intervals. Some examples of -things that you can build using the Schedule API include: +The Schedule app in Pipedream is a powerful tool that allows you to trigger workflows at regular intervals, ranging from every minute to once a year. This enables the automation of repetitive tasks and the scheduling of actions to occur without manual intervention. By leveraging this API, you can execute code, run integrations, and process data on a reliable schedule, all within Pipedream's serverless environment. -- Automated data retrieval from a third-party service, like pulling stats from - your Salesforce account on a set schedule. -- Uploading new data sets to a database with a predetermined interval. -- Automatic emails to customers or leads at a certain time of the day. -- Automating data analysis based on a set schedule. -- Automatically optimizing social media postings according to a specified - timeline. -- Updating webpages at a certain interval with newly available content. -- Re-running reports on a periodic basis. -- Refreshing a cache of data at a given frequency. +# Example Use Cases + +- **Daily Sales Reports**: Automatically aggregate and send daily sales data from an e-commerce platform such as Shopify to a Google Sheets document every morning. This scheduled workflow could involve fetching the previous day's sales data, processing it, and using the Google Sheets API to update a spreadsheet, providing an up-to-date sales report for quick review. + +- **Social Media Posting**: Schedule regular social media posts across platforms like Twitter or Facebook by setting up a workflow that runs weekly. It could select content from a predefined source, like a CMS or database, format the post, and use the respective social media platform's API to publish the content, keeping your social media presence active with minimal effort. + +- **Database Backup and Maintenance**: Create a weekly or monthly scheduled workflow to back up your database to a service like Dropbox or AWS S3. It could export the latest database snapshot, compress and encrypt the data, and then store it securely offsite. Additionally, this can be coupled with database maintenance tasks such as cleaning up old logs or optimizing tables. diff --git a/components/schedule/package.json b/components/schedule/package.json new file mode 100644 index 0000000000000..a1480f41a385c --- /dev/null +++ b/components/schedule/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/schedule", + "version": "0.6.0", + "description": "Pipedream schedule Components", + "main": "schedule.app.mjs", + "keywords": [ + "pipedream", + "schedule" + ], + "homepage": "https://pipedream.com/apps/schedule", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/schedule_it/README.md b/components/schedule_it/README.md new file mode 100644 index 0000000000000..d1baf9e55123a --- /dev/null +++ b/components/schedule_it/README.md @@ -0,0 +1,11 @@ +# Overview + +Schedule it offers an API that lets users manage appointments, resources, and scheduling efficiently. In Pipedream, this API can drive powerful automations for schedule management, notifications, and coordination between multiple platforms. With this API, you can create events, update schedules, and integrate with other services to streamline operations. By leveraging Pipedream's serverless platform, you can construct workflows that respond to scheduling triggers, manipulate data, and interact with an array of applications without the hassle of managing infrastructure. + +# Example Use Cases + +- **Automated Appointment Reminders**: Use Schedule it to create appointments and then set up a Pipedream workflow that sends out reminder emails or SMS messages through integrations with services like SendGrid or Twilio a day before the scheduled appointment. + +- **Resource Allocation Tracker**: When a new booking is made in Schedule it, trigger a Pipedream workflow that updates a Google Sheet or Airtable base with the resource allocation details. This can help in tracking the usage of resources like rooms, equipment, or personnel in real-time. + +- **Cross-Platform Scheduling Sync**: Sync Schedule it with external calendars such as Google Calendar or Outlook. Whenever an event is created or updated in Schedule it, use a Pipedream workflow to reflect those changes in the respective external calendar, ensuring all schedules stay aligned across platforms. diff --git a/components/scheduleonce/README.md b/components/scheduleonce/README.md index 7c55740fa7e35..4bf69ef557af1 100644 --- a/components/scheduleonce/README.md +++ b/components/scheduleonce/README.md @@ -1,29 +1,11 @@ # Overview -The ScheduleOnce API can be used to build powerful and intuitive scheduling -experiences for users on both web and mobile devices. It simplifies the -scheduling process by providing access to robust features that make it easy to -plan, book, and manage calendar events. +The ScheduleOnce API enables the automation of scheduling processes, allowing you to integrate your booking flow into custom applications or services. By leveraging this API on Pipedream, you can create dynamic workflows that react to booking events, synchronize with other calendar services, and streamline communications based on the scheduling data. This interaction opens up numerous possibilities for creating efficient, time-saving automations that keep your scheduling in sync with your business operations. -The ScheduleOnce API provides three main capabilities: +# Example Use Cases -- Book Management - Enable customers to easily manage their bookings, including - the ability to reschedule, cancel or check availability with an automated - workflow. -- Calendar Synchronization - Synchronize schedules across multiple calendars, - including Google, Outlook, and Apple Calendar. -- Personalization - Add custom fields and workflows, such as product selection - or automated follow up tasks. +- **Automated Appointment Confirmation Emails**: When a new appointment is scheduled via ScheduleOnce, trigger an automation in Pipedream to send a personalized confirmation email to the client using an email service like SendGrid. This workflow can include details about the appointment, a calendar invite, and pre-meeting instructions. -With the ScheduleOnce API, you can easily build a range of powerful scheduling -tools and experiences, including: +- **Synchronization with CRM**: After a booking is made, the client’s details are automatically added or updated in a CRM like Salesforce. This could include the appointment time, the nature of the meeting, and any notes provided during the booking process, ensuring your sales team has up-to-date information. -- Online Appointment Booking Software -- Client Portal Scheduling Systems -- Scheduling Software for Retail -- Employee Scheduling and Attendance Tracking Solutions -- Event Management, Registration and Payment Solutions -- Task and Meeting Management Solutions -- Location-Based Services and Reservations Solutions -- Automated Payment and Invoicing Solutions -- Automated Scheduled Follow-up Actions +- **Post-appointment Follow-up Workflow**: Once an appointment is completed, trigger a sequence of follow-up actions such as sending a thank-you email, soliciting feedback via a survey tool like Typeform, or creating a follow-up task in a project management tool like Trello. This can help maintain engagement and gather valuable insights from clients. diff --git a/components/scheduleonce/package.json b/components/scheduleonce/package.json new file mode 100644 index 0000000000000..f2c0bd3bab53f --- /dev/null +++ b/components/scheduleonce/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/scheduleonce", + "version": "0.6.0", + "description": "Pipedream scheduleonce Components", + "main": "scheduleonce.app.mjs", + "keywords": [ + "pipedream", + "scheduleonce" + ], + "homepage": "https://pipedream.com/apps/scheduleonce", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/scopemaster/README.md b/components/scopemaster/README.md index 28aeb1fe52537..ed29b3b5bd9e4 100644 --- a/components/scopemaster/README.md +++ b/components/scopemaster/README.md @@ -1,28 +1,11 @@ # Overview -The ScopeMaster API is an API designed for developers who want to create -automated processes for web testing. With this API, developers can automate the -analysis of page performance for website analytics and quality assurance. With -this tool, developers can test multiple aspects of their website, from page -performance to accessibility, as well as quality assurance and scalability. +The ScopeMaster API offers a sophisticated platform for automating and enhancing software requirements analysis. It enables the analysis of user stories for quality and size, helping teams get a clear understanding of the scope of their projects. With ScopeMaster, you can streamline requirement gathering, identify potential issues early on, and improve estimation accuracy. Harness this API within Pipedream to create powerful integrations that optimize software development workflows. -The ScopeMaster API offers powerful automated web testing features that help -ensure quality across the web. It can be used to create custom page tests for -page performance, accessibility, scalability, security, and other aspects. -Developers can customize the tests to match their specific website requirements -and track website performance in real time. +# Example Use Cases -Here are some examples of what you can build using the ScopeMaster API: +- **Automated Quality Analysis of User Stories**: Trigger a Pipedream workflow whenever a new user story is created in your project management tool, such as Jira. Use the ScopeMaster API to analyze the story for completeness and consistency, then post the analysis back to Jira to guide the author on improvements. -- An automated web performance monitoring platform that refreshes and updates - data quickly. -- A performance analysis platform that tests multiple aspects of a website, - including page loading times and other vital metrics. -- A scalability testing platform that can detect potential problems before they - arise and provide insights to developers on ways to improve scalability. -- A quality assurance platform that automatically performs tests on webpages - and helps ensure that webpages are up-to-date and secure. -- A tool for accessibility testing to ensure webpages are accessible to all - users. -- A platform for stress testing that allows developers to monitor and analyze - the performance of websites under high load conditions. +- **Enhanced Estimation Accuracy**: Integrate ScopeMaster with a time tracking tool like Toggl. When a developer completes a task, use Pipedream to send the actual time spent to ScopeMaster. Compare estimated versus actual effort to refine future estimations and improve sprint planning. + +- **Early Detection of Requirement Ambiguities**: Connect ScopeMaster with GitHub issues. When new issues are created, they are sent to ScopeMaster for analysis. The API’s response, containing insights on ambiguities or missing details, can trigger a workflow that alerts the issue creator to refine the requirements before development starts. diff --git a/components/scoredetect/README.md b/components/scoredetect/README.md new file mode 100644 index 0000000000000..bd2dc928fa2e0 --- /dev/null +++ b/components/scoredetect/README.md @@ -0,0 +1,11 @@ +# Overview + +The ScoreDetect API provides real-time sports scores and stats, offering a treasure trove of data for sports enthusiasts, bettors, and developers building applications tied to sports events. With Pipedream, you can leverage this API to create serverless workflows that react to live sports data, integrating with countless other apps to automate notifications, update databases, sync with social media, or even trigger custom actions based on game events. + +# Example Use Cases + +- **Real-Time Score Updates to Slack**: Use ScoreDetect to monitor scores for your favorite team and set up a Pipedream workflow that sends a message to a Slack channel whenever there's a change in the score. This keeps your community or team informed without the need to constantly check external apps or websites. + +- **Sports Betting Odds Analysis**: With ScoreDetect, you can pull in betting odds and results, then connect to a Google Sheets spreadsheet using Pipedream to log and analyze this data over time. This could help refine betting strategies or provide insights for content creation on sports betting forums or blogs. + +- **Game Start Alerts via SMS**: Create a Pipedream workflow that uses ScoreDetect to track when games are starting. Combine this with the Twilio SMS service to send out alerts to subscribers, ensuring they never miss a game. This can be particularly useful for sports event organizers or bars that show live games and want to attract patrons. diff --git a/components/scoro/README.md b/components/scoro/README.md new file mode 100644 index 0000000000000..13f428b61cf16 --- /dev/null +++ b/components/scoro/README.md @@ -0,0 +1,11 @@ +# Overview + +Scoro is a comprehensive solution that combines project management, time tracking, and CRM functionalities. With the Scoro API, you can automate your business workflows, sync data across different systems, and enhance productivity by reducing manual tasks. The API allows you to access, manipulate, and integrate your Scoro data with other services. When used on Pipedream, you can create serverless workflows to connect Scoro with hundreds of other apps, respond to Scoro events in real-time, and automate actions based on triggers from within Scoro or other platforms. + +# Example Use Cases + +- **Sync Scoro Contacts with Google Sheets:** Automatically export new or updated contact information from Scoro to a Google Sheets spreadsheet. This keeps your contact data synchronized without manual input, and since Google Sheets is also supported by Pipedream, setting up this workflow is straightforward. + +- **Manage Scoro Projects with GitHub Issues:** Create a link between Scoro and GitHub where new GitHub issues trigger the creation of corresponding tasks or projects in Scoro. For teams that manage their code in GitHub, this ensures project management aspects are updated in Scoro without switching contexts. + +- **Automate Invoice Creation and Email Notifications:** When a deal is won in Scoro, trigger an automation that creates an invoice and then sends an email notification through SendGrid or another email service. This reduces the manual work involved in billing and ensures prompt communication with clients. diff --git a/components/scrape_it_cloud/README.md b/components/scrape_it_cloud/README.md new file mode 100644 index 0000000000000..0c81b5e9bbd6e --- /dev/null +++ b/components/scrape_it_cloud/README.md @@ -0,0 +1,11 @@ +# Overview + +The Scrape-It.Cloud API allows you to automate the extraction of data from websites. It can parse, scrape, and retrieve content without the need for manual intervention. With this API on Pipedream, you can build workflows that trigger on various events and use the scraped data for numerous applications like data analysis, lead generation, and content aggregation. + +# Example Use Cases + +- **Content Change Detection**: Monitor changes on a webpage and receive notifications. When Scrape-It.Cloud detects a change, it can trigger a Pipedream workflow that sends an alert via email, Slack, or another messaging app. + +- **Competitor Price Monitoring**: Use Scrape-It.Cloud to track competitor pricing. Pipedream can schedule the scrape and integrate with a database like Google Sheets or Airtable to log price changes over time, enabling market analysis and strategic responses. + +- **Aggregating News for Curated Digests**: Scrape news sites or blogs for the latest articles. Pipedream workflows can combine this data with an app like SendGrid to distribute a curated newsletter to subscribers with the most recent updates in their industry. diff --git a/components/scrapegraphai/actions/start-local-scraper/start-local-scraper.mjs b/components/scrapegraphai/actions/start-local-scraper/start-local-scraper.mjs new file mode 100644 index 0000000000000..97c66f16ddf25 --- /dev/null +++ b/components/scrapegraphai/actions/start-local-scraper/start-local-scraper.mjs @@ -0,0 +1,56 @@ +import scrapegraphai from "../../scrapegraphai.app.mjs"; + +export default { + key: "scrapegraphai-start-local-scraper", + name: "Start Local Scraper", + description: "Extract content from HTML content using AI by providing a natural language prompt and the HTML content. [See the documentation](https://docs.scrapegraphai.com/api-reference/endpoint/localscraper/start)", + version: "0.0.1", + type: "action", + props: { + scrapegraphai, + html: { + type: "string", + label: "HTML", + description: "The HTML to scrape", + }, + prompt: { + propDefinition: [ + scrapegraphai, + "prompt", + ], + }, + waitForCompletion: { + propDefinition: [ + scrapegraphai, + "waitForCompletion", + ], + }, + }, + async run({ $ }) { + let response = await this.scrapegraphai.startLocalScraper({ + $, + data: { + website_html: this.html, + user_prompt: this.prompt, + }, + }); + + if (this.waitForCompletion) { + const timer = (ms) => new Promise((res) => setTimeout(res, ms)); + while (response.status !== "completed" && response.status !== "failed") { + response = await this.scrapegraphai.getLocalScraperStatus({ + $, + requestId: response.request_id, + }); + await timer(3000); + } + } + + if (response.status !== "failed") { + $.export("$summary", `Successfully ${this.waitForCompletion + ? "completed" + : "started" } scraping HTML.`); + } + return response; + }, +}; diff --git a/components/scrapegraphai/actions/start-markdownify/start-markdownify.mjs b/components/scrapegraphai/actions/start-markdownify/start-markdownify.mjs new file mode 100644 index 0000000000000..6b0702ebf5c2f --- /dev/null +++ b/components/scrapegraphai/actions/start-markdownify/start-markdownify.mjs @@ -0,0 +1,51 @@ +import scrapegraphai from "../../scrapegraphai.app.mjs"; + +export default { + key: "scrapegraphai-start-markdownify", + name: "Start Markdownify", + description: "Convert any webpage into clean, readable Markdown format. [See the documentation](https://docs.scrapegraphai.com/api-reference/endpoint/markdownify/start)", + version: "0.0.1", + type: "action", + props: { + scrapegraphai, + url: { + propDefinition: [ + scrapegraphai, + "url", + ], + description: "The URL of the website to convert into markdown", + }, + waitForCompletion: { + propDefinition: [ + scrapegraphai, + "waitForCompletion", + ], + }, + }, + async run({ $ }) { + let response = await this.scrapegraphai.startMarkdownify({ + $, + data: { + website_url: this.url, + }, + }); + + if (this.waitForCompletion) { + const timer = (ms) => new Promise((res) => setTimeout(res, ms)); + while (response.status !== "completed" && response.status !== "failed") { + response = await this.scrapegraphai.getMarkdownifyStatus({ + $, + requestId: response.request_id, + }); + await timer(3000); + } + } + + if (response.status !== "failed") { + $.export("$summary", `Successfully ${this.waitForCompletion + ? "completed" + : "started" } converting ${this.url} to markdown.`); + } + return response; + }, +}; diff --git a/components/scrapegraphai/actions/start-smart-scraper/start-smart-scraper.mjs b/components/scrapegraphai/actions/start-smart-scraper/start-smart-scraper.mjs new file mode 100644 index 0000000000000..65777516228c8 --- /dev/null +++ b/components/scrapegraphai/actions/start-smart-scraper/start-smart-scraper.mjs @@ -0,0 +1,57 @@ +import scrapegraphai from "../../scrapegraphai.app.mjs"; + +export default { + key: "scrapegraphai-start-smart-scraper", + name: "Start Smart Scraper", + description: "Extract content from a webpage using AI by providing a natural language prompt and a URL. [See the documentation](https://docs.scrapegraphai.com/api-reference/endpoint/smartscraper/start).", + version: "0.0.1", + type: "action", + props: { + scrapegraphai, + url: { + propDefinition: [ + scrapegraphai, + "url", + ], + }, + prompt: { + propDefinition: [ + scrapegraphai, + "prompt", + ], + }, + waitForCompletion: { + propDefinition: [ + scrapegraphai, + "waitForCompletion", + ], + }, + }, + async run({ $ }) { + let response = await this.scrapegraphai.startSmartScraper({ + $, + data: { + website_url: this.url, + user_prompt: this.prompt, + }, + }); + + if (this.waitForCompletion) { + const timer = (ms) => new Promise((res) => setTimeout(res, ms)); + while (response.status !== "completed" && response.status !== "failed") { + response = await this.scrapegraphai.getSmartScraperStatus({ + $, + requestId: response.request_id, + }); + await timer(3000); + } + } + + if (response.status !== "failed") { + $.export("$summary", `Successfully ${this.waitForCompletion + ? "completed" + : "started" } scraping ${this.url}.`); + } + return response; + }, +}; diff --git a/components/scrapegraphai/package.json b/components/scrapegraphai/package.json new file mode 100644 index 0000000000000..ee342f5acde98 --- /dev/null +++ b/components/scrapegraphai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/scrapegraphai", + "version": "0.1.0", + "description": "Pipedream ScrapeGraphAI Components", + "main": "scrapegraphai.app.mjs", + "keywords": [ + "pipedream", + "scrapegraphai" + ], + "homepage": "https://pipedream.com/apps/scrapegraphai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/scrapegraphai/scrapegraphai.app.mjs b/components/scrapegraphai/scrapegraphai.app.mjs new file mode 100644 index 0000000000000..a35f9ca91450d --- /dev/null +++ b/components/scrapegraphai/scrapegraphai.app.mjs @@ -0,0 +1,87 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "scrapegraphai", + propDefinitions: { + url: { + type: "string", + label: "URL to Scrape", + description: "The URL of the website to scrape.", + }, + prompt: { + type: "string", + label: "Prompt", + description: "A prompt describing what you want to extract. Example: `Extract info about the company`", + }, + waitForCompletion: { + type: "boolean", + label: "Wait For Completion", + description: "Set to `true` to poll the API in 3-second intervals until the request is completed", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.scrapegraphai.com/v1"; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "sgai-apikey": `${this.$auth.api_key}`, + }, + ...opts, + }); + }, + startSmartScraper(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/smartscraper", + ...opts, + }); + }, + getSmartScraperStatus({ + requestId, ...opts + }) { + return this._makeRequest({ + path: `/smartscraper/${requestId}`, + ...opts, + }); + }, + startLocalScraper(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/localscraper", + ...opts, + }); + }, + getLocalScraperStatus({ + requestId, ...opts + }) { + return this._makeRequest({ + path: `/localscraper/${requestId}`, + ...opts, + }); + }, + startMarkdownify(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/markdownify", + ...opts, + }); + }, + getMarkdownifyStatus({ + requestId, ...opts + }) { + return this._makeRequest({ + path: `/markdownify/${requestId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/scrapein_/README.md b/components/scrapein_/README.md new file mode 100644 index 0000000000000..92200206baeef --- /dev/null +++ b/components/scrapein_/README.md @@ -0,0 +1,11 @@ +# Overview + +The Soax API provides access to a robust proxy and scraping service that allows users to gather data efficiently and safely from across the web. By leveraging Soax’s rotating proxies, you can access and retrieve data without triggering anti-scraping measures, making it useful for tasks like competitive analysis, market research, SEO monitoring, and more. Integrating Soax with Pipedream enhances these capabilities by automating data collection workflows, enriching the data with other services, and triggering actions based on the retrieved data. + +# Example Use Cases + +- **Market Trends Analysis**: Automate the periodic collection of pricing and product availability data from e-commerce sites using Soax’s rotating proxies. Use Pipedream to schedule these requests, process the data, and store the results in a Google Sheets document for further analysis and trend spotting. + +- **SEO Monitoring**: Set up a workflow on Pipedream using Soax to regularly scrape search engine results for specified keywords related to your business. Analyze changes in search rankings and automatically send this data to a Slack channel to keep your marketing team informed in real-time. + +- **Social Media Sentiment Analysis**: Use the Soax API on Pipedream to scrape social media platforms for mentions of your brand or product. Automatically process this data through a sentiment analysis tool and push the results to a CRM like HubSpot to help your customer service team respond proactively to customer feedback. diff --git a/components/scrapein_/package.json b/components/scrapein_/package.json new file mode 100644 index 0000000000000..4b324c64dd6ac --- /dev/null +++ b/components/scrapein_/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/scrapein_", + "version": "0.0.1", + "description": "Pipedream Soax Components", + "main": "scrapein_.app.mjs", + "keywords": [ + "pipedream", + "scrapein_" + ], + "homepage": "https://pipedream.com/apps/scrapein_", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/scrapein_/scrapein_.app.mjs b/components/scrapein_/scrapein_.app.mjs new file mode 100644 index 0000000000000..11857fb42f619 --- /dev/null +++ b/components/scrapein_/scrapein_.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "scrapein_", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/scrapeninja/README.md b/components/scrapeninja/README.md new file mode 100644 index 0000000000000..e7ccafaed9591 --- /dev/null +++ b/components/scrapeninja/README.md @@ -0,0 +1,11 @@ +# Overview + +ScrapeNinja API on Pipedream allows you to craft powerful serverless workflows for web scraping without the hassle of managing proxies or browsers. It's a tool that can extract data from websites, handling JavaScript rendering and anti-bot measures with ease. By integrating ScrapeNinja with Pipedream, you can automate data collection, collate and process the scraped data, and connect it to numerous other services for further analysis, alerting, or storage. + +# Example Use Cases + +- **Automated Competitor Price Monitoring**: Use ScrapeNinja to scrape pricing information from competitor websites and feed the data into a Pipedream workflow. You can then compare these prices against your own, and if certain thresholds are met, automate a notification or update pricing in your ecommerce platform using Pipedream's integration with Shopify or WooCommerce. + +- **Real-time News Aggregation**: Set up ScrapeNinja to gather the latest news articles from various outlets. Process and filter this content within Pipedream to identify articles relevant to your industry. The results can be automatically posted to a Slack channel or compiled into a digest email using Pipedream's Email by Zapier app. + +- **Lead Generation from Online Directories**: Configure ScrapeNinja to extract data from online business directories. Use Pipedream to enrich this data, for instance by using the Clearbit app to find additional company information. Finally, push the leads into a CRM system like Salesforce or HubSpot, all within the same Pipedream workflow. diff --git a/components/scrapfly/actions/account-info/account-info.mjs b/components/scrapfly/actions/account-info/account-info.mjs new file mode 100644 index 0000000000000..4d7c4969cdfd7 --- /dev/null +++ b/components/scrapfly/actions/account-info/account-info.mjs @@ -0,0 +1,19 @@ +import scrapfly from "../../scrapfly.app.mjs"; + +export default { + key: "scrapfly-account-info", + name: "Retrieve Scrapfly Account Info", + description: "Retrieve current subscription and account usage details from Scrapfly. [See the documentation](https://scrapfly.io/docs/account#api)", + version: "0.0.1", + type: "action", + props: { + scrapfly, + }, + async run({ $ }) { + const response = await this.scrapfly.getAccountInfo({ + $, + }); + $.export("$summary", "Successfully retrieved account information"); + return response; + }, +}; diff --git a/components/scrapfly/actions/ai-data-extraction/ai-data-extraction.mjs b/components/scrapfly/actions/ai-data-extraction/ai-data-extraction.mjs new file mode 100644 index 0000000000000..0e2078c4135c7 --- /dev/null +++ b/components/scrapfly/actions/ai-data-extraction/ai-data-extraction.mjs @@ -0,0 +1,88 @@ +import { ConfigurationError } from "@pipedream/platform"; +import fs from "fs"; +import { checkTmp } from "../../common/utils.mjs"; +import scrapfly from "../../scrapfly.app.mjs"; + +export default { + key: "scrapfly-ai-data-extraction", + name: "AI Data Extraction", + description: "Automate content extraction from any text-based source using AI, LLM, and custom parsing. [See the documentation](https://scrapfly.io/docs/extraction-api/getting-started)", + version: "0.0.1", + type: "action", + props: { + scrapfly, + body: { + propDefinition: [ + scrapfly, + "body", + ], + }, + contentType: { + propDefinition: [ + scrapfly, + "contentType", + ], + }, + url: { + propDefinition: [ + scrapfly, + "url", + ], + }, + charset: { + type: "string", + label: "Charset", + description: "Charset of the document pass in the body. If you are not sure, you can use the `auto` value and we will try to detect it. Bad charset can lead to bad extraction, so it's important to set it correctly. **The most common charset is `utf-8` for text document and `ascii` for binary**. The symptom of a bad charset is that the text is not correctly displayed (accent, special characters, etc).", + default: "auto", + optional: true, + }, + extractionTemplate: { + type: "string", + label: "Extraction Template", + description: "Define an extraction template to get structured data. Use an ephemeral template (declared on the fly on the API call) or a stored template (declared in the dashboard) by using the template name.", + optional: true, + }, + extractionPrompt: { + type: "string", + label: "Extraction Prompt", + description: "Instruction to extract data or ask a question on the scraped content with an LLM (Large Language Model). [Must be url encoded](https://scrapfly.io/web-scraping-tools/urlencode).", + optional: true, + }, + extractionModel: { + type: "string", + label: "Extraction Model", + description: "AI Extraction to auto parse document to get structured data. E.g., `product`, `review`, `real-estate`, `article`.", + optional: true, + }, + webhookName: { + type: "string", + label: "Webhook Name", + description: "Queue you scrape request and redirect API response to a provided webhook endpoint. You can create a webhook endpoint from your `dashboard`, it takes the name of the webhook. Webhooks are scoped to the given project/env.", + optional: true, + }, + }, + async run({ $ }) { + if (!this.extractionTemplate && !this.extractionPrompt && !this.extractionModel) { + throw new ConfigurationError("You must provide at least **Extraction Template**, **Extraction Prompt** or **Extraction Model**"); + } + const response = await this.scrapfly.automateContentExtraction({ + $, + headers: { + "content-type": this.contentType, + }, + maxBodyLength: Infinity, + params: { + url: this.url, + charset: this.charset, + extraction_template: this.extractionTemplate, + extraction_prompt: this.extractionPrompt, + extraction_model: this.extractionModel, + webhook_name: this.webhookName, + }, + data: fs.readFileSync(checkTmp(this.body)).toString(), + }); + + $.export("$summary", "Successfully extracted content"); + return response; + }, +}; diff --git a/components/scrapfly/actions/scrape-page/scrape-page.mjs b/components/scrapfly/actions/scrape-page/scrape-page.mjs new file mode 100644 index 0000000000000..554872c4a1c1f --- /dev/null +++ b/components/scrapfly/actions/scrape-page/scrape-page.mjs @@ -0,0 +1,156 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { + FORMAT_OPTIONS, + PROXY_COUNTRY_OPTIONS, + PROXY_POOL_OPTIONS, +} from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import scrapfly from "../../scrapfly.app.mjs"; + +export default { + key: "scrapfly-scrape-page", + name: "Scrape Page", + description: "Extract data from a specified web page. [See the documentation](https://scrapfly.io/docs/scrape-api/getting-started)", + version: "0.0.1", + type: "action", + props: { + scrapfly, + url: { + propDefinition: [ + scrapfly, + "url", + ], + }, + headers: { + type: "object", + label: "Headers", + description: "Pass custom headers to the request.", + optional: true, + }, + lang: { + type: "string", + label: "Language", + description: "Select page language. By default it uses the language of the selected proxy location. Behind the scenes, it configures the `Accept-Language` HTTP header. If the website support the language, the content will be in that lang. **Note: you cannot set headers `Accept-Language` header manually**. [See the documentation](https://scrapfly.io/docs/scrape-api/getting-started#spec)", + optional: true, + }, + os: { + type: "string", + label: "Operating System", + description: "Operating System, if not selected it's random. **Note: you cannot set os parameter and `User-Agent` header at the same time.** [See the documentation](https://scrapfly.io/docs/scrape-api/getting-started#spec)", + optional: true, + }, + timeout: { + type: "integer", + label: "Timeout", + description: "Timeout in milliseconds. It represents the maximum time allowed for Scrapfly to perform the scrape. Since `timeout` is not trivial to understand see our [extended documentation on timeouts](https://scrapfly.io/docs/scrape-api/understand-timeout)", + optional: true, + }, + format: { + type: "string", + label: "Format", + description: "Format of the response.", + options: FORMAT_OPTIONS, + optional: true, + }, + retry: { + type: "boolean", + label: "Retry", + description: "Improve reliability with retries on failure.", + optional: true, + }, + proxifiedResponse: { + type: "boolean", + label: "Proxified Response", + description: "Return the content of the page directly.", + optional: true, + }, + debug: { + type: "boolean", + label: "Debug", + description: "Store the API result and take a screenshot if rendering js is enabled.", + optional: true, + }, + correlationId: { + type: "string", + label: "Correlation ID", + description: "Helper ID for correlating a group of scrapes.", + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "Add tags to your scrapes to group them.", + optional: true, + }, + dns: { + type: "boolean", + label: "DNS", + description: "Query and retrieve target DNS information.", + optional: true, + }, + ssl: { + type: "boolean", + label: "SSL", + description: "SSL option.", + optional: true, + }, + proxyPool: { + type: "string", + label: "Proxy Pool", + description: "Select the proxy pool to use.", + optional: true, + options: PROXY_POOL_OPTIONS, + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + props.country = { + type: "string", + label: "Country", + description: "Proxy country location. If not set it chooses a random location available. A reference to a country must be ISO 3166 alpha-2 (2 letters). The available countries are defined by the proxy pool you use. [See the documentation](https://scrapfly.io/docs/scrape-api/getting-started#spec)", + optional: true, + options: PROXY_COUNTRY_OPTIONS[this.proxyPool], + }; + return props; + }, + async run({ $ }) { + try { + let headers = ""; + if (this.headers) { + headers = Object.keys(parseObject(this.headers)) + .reduce((acc, key) => { + acc.push(`headers[${key}]=${encodeURIComponent(this.headers[key])}`); + return acc; + }, []); + } + const params = { + url: this.url, + proxy_pool: this.proxyPool, + country: this.country, + lang: this.lang, + os: this.os, + timeout: this.timeout, + format: this.format, + retry: this.retry, + proxified_response: this.proxifiedResponse, + debug: this.debug, + correlation_id: this.correlationId, + tags: parseObject(this.tags), + dns: this.dns, + ssl: this.ssl, + headers, + }; + + const response = await this.scrapfly.extractWebPageContent({ + $, + params, + }); + + $.export("$summary", `Successfully scraped content from ${this.url}`); + return response; + } catch ({ response: { data: { message } } }) { + throw new ConfigurationError(message); + } + }, +}; diff --git a/components/scrapfly/common/constants.mjs b/components/scrapfly/common/constants.mjs new file mode 100644 index 0000000000000..625839df3ff85 --- /dev/null +++ b/components/scrapfly/common/constants.mjs @@ -0,0 +1,731 @@ +export const PROXY_POOL_OPTIONS = [ + "public_datacenter_pool", + "public_residential_pool", +]; + +export const FORMAT_OPTIONS = [ + "raw", + "text", + "markdown", + "markdown:no_links,no_imagesLLM", + "clean_html", + "json", +]; + +export const CONTENT_TYPE_OPTIONS = [ + "application/json", + "application/jsonld", + "application/xml", + "text/plain", + "text/html", + "text/markdown", + "text/csv", + "application/xhtml+xml", +]; + +export const PROXY_COUNTRY_OPTIONS = { + public_datacenter_pool: [ + { + label: "Albania", + value: "al", + }, + { + label: "Armenia", + value: "am", + }, + { + label: "Argentina", + value: "ar", + }, + { + label: "Austria", + value: "at", + }, + { + label: "Australia", + value: "au", + }, + { + label: "Belgium", + value: "be", + }, + { + label: "Bulgaria", + value: "bg", + }, + { + label: "Bolivia", + value: "bo", + }, + { + label: "Brazil", + value: "br", + }, + { + label: "Belarus", + value: "by", + }, + { + label: "Canada", + value: "ca", + }, + { + label: "Switzerland", + value: "ch", + }, + { + label: "Chile", + value: "cl", + }, + { + label: "China", + value: "cn", + }, + { + label: "Colombia", + value: "co", + }, + { + label: "Czechia", + value: "cz", + }, + { + label: "Germany", + value: "de", + }, + { + label: "Denmark", + value: "dk", + }, + { + label: "Ecuador", + value: "ec", + }, + { + label: "Estonia", + value: "ee", + }, + { + label: "Spain", + value: "es", + }, + { + label: "Finland", + value: "fi", + }, + { + label: "France", + value: "fr", + }, + { + label: "United Kingdom", + value: "gb", + }, + { + label: "Georgia", + value: "ge", + }, + { + label: "Greece", + value: "gr", + }, + { + label: "Croatia", + value: "hr", + }, + { + label: "Hungary", + value: "hu", + }, + { + label: "Ireland", + value: "ie", + }, + { + label: "Israel", + value: "il", + }, + { + label: "India", + value: "in", + }, + { + label: "Iceland", + value: "is", + }, + { + label: "Italy", + value: "it", + }, + { + label: "Japan", + value: "jp", + }, + { + label: "South Korea", + value: "kr", + }, + { + label: "Lithuania", + value: "lt", + }, + { + label: "Latvia", + value: "lv", + }, + { + label: "Mexico", + value: "mx", + }, + { + label: "Netherlands", + value: "nl", + }, + { + label: "Norway", + value: "no", + }, + { + label: "New Zealand", + value: "nz", + }, + { + label: "Peru", + value: "pe", + }, + { + label: "Pakistan", + value: "pk", + }, + { + label: "Poland", + value: "pl", + }, + { + label: "Portugal", + value: "pt", + }, + { + label: "Romania", + value: "ro", + }, + { + label: "Russia", + value: "ru", + }, + { + label: "Saudi Arabia", + value: "sa", + }, + { + label: "Sweden", + value: "se", + }, + { + label: "Slovakia", + value: "sk", + }, + { + label: "Türkiye", + value: "tr", + }, + { + label: "Ukraine", + value: "ua", + }, + { + label: "United States", + value: "us", + }, + ], + public_residential_pool: [ + { + label: "Andorra", + value: "ad", + }, + { + label: "United Arab Emirates", + value: "ae", + }, + { + label: "Afghanistan", + value: "af", + }, + { + label: "Albania", + value: "al", + }, + { + label: "Armenia", + value: "am", + }, + { + label: "Angola", + value: "ao", + }, + { + label: "Argentina", + value: "ar", + }, + { + label: "Austria", + value: "at", + }, + { + label: "Australia", + value: "au", + }, + { + label: "Aruba", + value: "aw", + }, + { + label: "Azerbaijan", + value: "az", + }, + { + label: "Bosnia & Herzegovina", + value: "ba", + }, + { + label: "Bangladesh", + value: "bd", + }, + { + label: "Belgium", + value: "be", + }, + { + label: "Bulgaria", + value: "bg", + }, + { + label: "Bahrain", + value: "bh", + }, + { + label: "Benin", + value: "bj", + }, + { + label: "Bolivia", + value: "bo", + }, + { + label: "Brazil", + value: "br", + }, + { + label: "Bahamas", + value: "bs", + }, + { + label: "Bhutan", + value: "bt", + }, + { + label: "Belarus", + value: "by", + }, + { + label: "Belize", + value: "bz", + }, + { + label: "Canada", + value: "ca", + }, + { + label: "Switzerland", + value: "ch", + }, + { + label: "Côte d’Ivoire", + value: "ci", + }, + { + label: "Chile", + value: "cl", + }, + { + label: "China", + value: "cn", + }, + { + label: "Colombia", + value: "co", + }, + { + label: "Costa Rica", + value: "cr", + }, + { + label: "Cuba", + value: "cu", + }, + { + label: "Czechia", + value: "cz", + }, + { + label: "Germany", + value: "de", + }, + { + label: "Denmark", + value: "dk", + }, + { + label: "Dominica", + value: "dm", + }, + { + label: "Ecuador", + value: "ec", + }, + { + label: "Estonia", + value: "ee", + }, + { + label: "Egypt", + value: "eg", + }, + { + label: "Spain", + value: "es", + }, + { + label: "Ethiopia", + value: "et", + }, + { + label: "Finland", + value: "fi", + }, + { + label: "Fiji", + value: "fj", + }, + { + label: "France", + value: "fr", + }, + { + label: "United Kingdom", + value: "gb", + }, + { + label: "Georgia", + value: "ge", + }, + { + label: "Ghana", + value: "gh", + }, + { + label: "Gambia", + value: "gm", + }, + { + label: "Greece", + value: "gr", + }, + { + label: "Hong Kong SAR China", + value: "hk", + }, + { + label: "Honduras", + value: "hn", + }, + { + label: "Croatia", + value: "hr", + }, + { + label: "Haiti", + value: "ht", + }, + { + label: "Hungary", + value: "hu", + }, + { + label: "Indonesia", + value: "id", + }, + { + label: "Ireland", + value: "ie", + }, + { + label: "Israel", + value: "il", + }, + { + label: "India", + value: "in", + }, + { + label: "Iraq", + value: "iq", + }, + { + label: "Iran", + value: "ir", + }, + { + label: "Iceland", + value: "is", + }, + { + label: "Italy", + value: "it", + }, + { + label: "Jordan", + value: "jo", + }, + { + label: "Japan", + value: "jp", + }, + { + label: "Kenya", + value: "ke", + }, + { + label: "Cambodia", + value: "kh", + }, + { + label: "South Korea", + value: "kr", + }, + { + label: "Kazakhstan", + value: "kz", + }, + { + label: "Lebanon", + value: "lb", + }, + { + label: "Liberia", + value: "lr", + }, + { + label: "Lithuania", + value: "lt", + }, + { + label: "Latvia", + value: "lv", + }, + { + label: "Morocco", + value: "ma", + }, + { + label: "Monaco", + value: "mc", + }, + { + label: "Madagascar", + value: "mg", + }, + { + label: "North Macedonia", + value: "mk", + }, + { + label: "Mongolia", + value: "mn", + }, + { + label: "Mauritania", + value: "mr", + }, + { + label: "Malta", + value: "mt", + }, + { + label: "Mauritius", + value: "mu", + }, + { + label: "Maldives", + value: "mv", + }, + { + label: "Mexico", + value: "mx", + }, + { + label: "Malaysia", + value: "my", + }, + { + label: "Mozambique", + value: "mz", + }, + { + label: "Nigeria", + value: "ng", + }, + { + label: "Netherlands", + value: "nl", + }, + { + label: "Norway", + value: "no", + }, + { + label: "New Zealand", + value: "nz", + }, + { + label: "Oman", + value: "om", + }, + { + label: "Panama", + value: "pa", + }, + { + label: "Peru", + value: "pe", + }, + { + label: "Philippines", + value: "ph", + }, + { + label: "Pakistan", + value: "pk", + }, + { + label: "Poland", + value: "pl", + }, + { + label: "Puerto Rico", + value: "pr", + }, + { + label: "Portugal", + value: "pt", + }, + { + label: "Paraguay", + value: "py", + }, + { + label: "Qatar", + value: "qa", + }, + { + label: "Romania", + value: "ro", + }, + { + label: "Serbia", + value: "rs", + }, + { + label: "Russia", + value: "ru", + }, + { + label: "Saudi Arabia", + value: "sa", + }, + { + label: "Seychelles", + value: "sc", + }, + { + label: "Sudan", + value: "sd", + }, + { + label: "Sweden", + value: "se", + }, + { + label: "Singapore", + value: "sg", + }, + { + label: "Slovenia", + value: "si", + }, + { + label: "Slovakia", + value: "sk", + }, + { + label: "Senegal", + value: "sn", + }, + { + label: "South Sudan", + value: "ss", + }, + { + label: "Tunisia", + value: "tn", + }, + { + label: "Türkiye", + value: "tr", + }, + { + label: "Taiwan", + value: "tw", + }, + { + label: "Ukraine", + value: "ua", + }, + { + label: "Uganda", + value: "ug", + }, + { + label: "United States", + value: "us", + }, + { + label: "Uruguay", + value: "uy", + }, + { + label: "Uzbekistan", + value: "uz", + }, + { + label: "Venezuela", + value: "ve", + }, + { + label: "British Virgin Islands", + value: "vg", + }, + { + label: "Vietnam", + value: "vn", + }, + { + label: "Yemen", + value: "ye", + }, + { + label: "South Africa", + value: "za", + }, + ], +}; diff --git a/components/scrapfly/common/utils.mjs b/components/scrapfly/common/utils.mjs new file mode 100644 index 0000000000000..0cd1a12b6a4ba --- /dev/null +++ b/components/scrapfly/common/utils.mjs @@ -0,0 +1,31 @@ +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; + +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/scrapfly/package.json b/components/scrapfly/package.json new file mode 100644 index 0000000000000..f2bc0dcc759f4 --- /dev/null +++ b/components/scrapfly/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/scrapfly", + "version": "0.1.0", + "description": "Pipedream Scrapfly Components", + "main": "scrapfly.app.mjs", + "keywords": [ + "pipedream", + "scrapfly" + ], + "homepage": "https://pipedream.com/apps/scrapfly", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} + diff --git a/components/scrapfly/scrapfly.app.mjs b/components/scrapfly/scrapfly.app.mjs new file mode 100644 index 0000000000000..53e4c77fa946b --- /dev/null +++ b/components/scrapfly/scrapfly.app.mjs @@ -0,0 +1,68 @@ +import { axios } from "@pipedream/platform"; +import { CONTENT_TYPE_OPTIONS } from "./common/constants.mjs"; + +export default { + type: "app", + app: "scrapfly", + propDefinitions: { + url: { + type: "string", + label: "URL", + description: "This URL is used to transform any relative URLs in the document into absolute URLs automatically. It can be either the base URL or the exact URL of the document. [Must be url encoded](https://scrapfly.io/web-scraping-tools/urlencode).", + }, + body: { + type: "string", + label: "Body", + description: "The request body must contain the content of the page you want to extract data from. The content must be in the format specified by the `content-type` header or via the `content_type` HTTP parameter. Provide a file from `/tmp`. To upload a file to `/tmp` folder, please follow the doc [here](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + }, + contentType: { + type: "string", + label: "Content Type", + description: "Content type of the document pass in the body - You must specify the content type of the document by using this parameter or via the `content-type` header. This parameter has priority over the `content-type` header.", + options: CONTENT_TYPE_OPTIONS, + }, + }, + methods: { + _baseUrl() { + return "https://api.scrapfly.io"; + }, + _params(params = {}) { + return { + ...params, + key: `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, params, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + params: this._params(params), + ...opts, + }); + }, + getAccountInfo(opts = {}) { + return this._makeRequest({ + path: "/account", + ...opts, + }); + }, + extractWebPageContent({ + params, ...opts + }) { + return this._makeRequest({ + method: "GET", + path: "/scrape", + params, + ...opts, + }); + }, + automateContentExtraction(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/extraction", + ...opts, + }); + }, + }, +}; diff --git a/components/scrapingant/README.md b/components/scrapingant/README.md new file mode 100644 index 0000000000000..3680730690469 --- /dev/null +++ b/components/scrapingant/README.md @@ -0,0 +1,11 @@ +# Overview + +The ScrapingAnt API allows you to scrape web pages without getting blocked. It can handle JavaScript rendering, cookies, sessions, and can even interact with web pages as if a real person were browsing. Using Pipedream, you can integrate ScrapingAnt with countless other apps to automate data extraction and feed this data into various business processes, analytics tools, or databases. + +# Example Use Cases + +- **Scrape and Monitor Competitor Pricing**: Extract pricing data from competitor websites and send alerts or log the data when changes are detected. Combine with Pipedream's cron scheduler to run this check regularly. + +- **Aggregate News Articles for Trend Analysis**: Scrape news articles from various sources to perform trend analysis or sentiment analysis. Use Pipedream's built-in code steps to process the scraped data, and perhaps send it to a Google Sheets document for further analysis. + +- **Automate Job Listings Collection**: Collect job listings from multiple career websites to create a consolidated list of opportunities. Integrate with Slack using Pipedream to send daily digests of new listings to a channel for job seekers. diff --git a/components/scrapingant/actions/general-extraction/general-extraction.mjs b/components/scrapingant/actions/general-extraction/general-extraction.mjs new file mode 100644 index 0000000000000..860c0ed9460ff --- /dev/null +++ b/components/scrapingant/actions/general-extraction/general-extraction.mjs @@ -0,0 +1,105 @@ +import app from "../../scrapingant.app.mjs"; + +export default { + key: "scrapingant-general-extraction", + name: "General Extraction", + description: "Send a request using the standard extraction method of ScrapingAnt. [See the documentation](https://docs.scrapingant.com/request-response-format)", + version: "0.0.1", + type: "action", + props: { + app, + url: { + propDefinition: [ + app, + "url", + ], + }, + browser: { + propDefinition: [ + app, + "browser", + ], + reloadProps: true, + }, + returnPageSource: { + propDefinition: [ + app, + "returnPageSource", + ], + disabled: true, + hidden: true, + }, + cookies: { + propDefinition: [ + app, + "cookies", + ], + }, + jsSnippet: { + propDefinition: [ + app, + "jsSnippet", + ], + disabled: true, + hidden: true, + }, + proxyType: { + propDefinition: [ + app, + "proxyType", + ], + }, + proxyCountry: { + propDefinition: [ + app, + "proxyCountry", + ], + }, + waitForSelector: { + propDefinition: [ + app, + "waitForSelector", + ], + }, + blockResource: { + propDefinition: [ + app, + "blockResource", + ], + disabled: true, + hidden: true, + }, + }, + async additionalProps(existingProps) { + const props = {}; + if (this.browser) { + existingProps.returnPageSource.hidden = false; + existingProps.returnPageSource.disabled = false; + existingProps.jsSnippet.hidden = false; + existingProps.jsSnippet.disabled = false; + existingProps.blockResource.hidden = false; + existingProps.blockResource.disabled = false; + } + + return props; + }, + + async run({ $ }) { + const response = await this.app.generalExtraction({ + $, + params: { + url: this.url, + browser: this.browser, + return_page_source: this.returnPageSource, + cookies: this.cookies, + js_snippet: this.jsSnippet, + proxy_type: this.proxyType, + proxy_country: this.proxyCountry, + wait_for_selector: this.waitForSelector, + block_resource: this.blockResource, + }, + }); + $.export("$summary", "Successfully sent the request to ScrapingAnt"); + return response; + }, +}; diff --git a/components/scrapingant/common/constants.mjs b/components/scrapingant/common/constants.mjs new file mode 100644 index 0000000000000..d9e83df79edbe --- /dev/null +++ b/components/scrapingant/common/constants.mjs @@ -0,0 +1,126 @@ +export default { + PROXY_COUNTRIES: [ + { + label: "World", + value: "", + }, + { + label: "Brazil", + value: "BR", + }, + { + label: "Canada", + value: "CA", + }, + { + label: "China", + value: "CN", + }, + { + label: "Czech Republic", + value: "CZ", + }, + { + label: "France", + value: "FR", + }, + { + label: "Germany", + value: "DE", + }, + { + label: "Hong Kong", + value: "HK", + }, + { + label: "India", + value: "IN", + }, + { + label: "Indonesia", + value: "ID", + }, + { + label: "Italy", + value: "IT", + }, + { + label: "Israel", + value: "IL", + }, + { + label: "Japan", + value: "JP", + }, + { + label: "Netherlands", + value: "NL", + }, + { + label: "Poland", + value: "PL", + }, + { + label: "Russia", + value: "RU", + }, + { + label: "Saudi Arabia", + value: "SA", + }, + { + label: "Singapore", + value: "SG", + }, + { + label: "South Korea", + value: "KR", + }, + { + label: "Spain", + value: "ES", + }, + { + label: "United Kingdom", + value: "GB", + }, + { + label: "United Arab Emirates", + value: "AE", + }, + { + label: "USA", + value: "US", + }, + { + label: "Vietnam", + value: "VN", + }, + ], + PROXY_TYPES: [ + { + label: "Residential", + value: "residential", + }, + { + label: "Datacenter", + value: "datacenter", + }, + ], + RESOURCE_TYPES: [ + "document", + "stylesheet", + "image", + "media", + "font", + "script", + "texttrack", + "xhr", + "fetch", + "eventsource", + "websocket", + "manifest", + "other", + ], + +}; diff --git a/components/scrapingant/package.json b/components/scrapingant/package.json index a5db939a0a1aa..01400364fee48 100644 --- a/components/scrapingant/package.json +++ b/components/scrapingant/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/scrapingant", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream ScrapingAnt Components", "main": "scrapingant.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } -} \ No newline at end of file +} diff --git a/components/scrapingant/scrapingant.app.mjs b/components/scrapingant/scrapingant.app.mjs index 5204975b31454..7bfa28e192529 100644 --- a/components/scrapingant/scrapingant.app.mjs +++ b/components/scrapingant/scrapingant.app.mjs @@ -1,11 +1,92 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "scrapingant", - propDefinitions: {}, + propDefinitions: { + url: { + type: "string", + label: "URL", + description: "The URL to scrape", + }, + browser: { + type: "boolean", + label: "Browser", + description: "Enables using a headless browser for scraping", + optional: true, + }, + returnPageSource: { + type: "boolean", + label: "Return Page Source", + description: "Enables returning data returned by the server and unaltered by the browser. When true JS won't be rendered", + optional: true, + }, + cookies: { + type: "string", + label: "Cookies", + description: "Cookies to pass with a scraping request to the target site, i.e.: `cookie_name1=cookie_value1;cookie_name2=cookie_value2`", + optional: true, + }, + jsSnippet: { + type: "string", + label: "JS Snippet", + description: "Base64 encoded JS snippet to run once page being loaded in the ScrapingAnt browser", + optional: true, + }, + proxyType: { + type: "string", + label: "Proxy Type", + description: "Specifies the proxy type to make the request from", + options: constants.PROXY_TYPES, + optional: true, + }, + proxyCountry: { + type: "string", + label: "Proxy Country", + description: "Specifies the proxy country to make the request from", + options: constants.PROXY_COUNTRIES, + optional: true, + }, + waitForSelector: { + type: "string", + label: "Wait for Selector", + description: "The CSS selector of the element Scrapingant will wait for before returning the result", + optional: true, + }, + blockResource: { + type: "string[]", + label: "Block Resource", + description: "Prevents cloud browser from loading specified resource types", + options: constants.RESOURCE_TYPES, + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.scrapingant.com/v2"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "x-api-key": `${this.$auth.api_token}`, + }, + }); + }, + async generalExtraction(args = {}) { + return this._makeRequest({ + path: "/general", + ...args, + }); }, }, }; diff --git a/components/scrapingbee/README.md b/components/scrapingbee/README.md new file mode 100644 index 0000000000000..31690293790c6 --- /dev/null +++ b/components/scrapingbee/README.md @@ -0,0 +1,11 @@ +# Overview + +The ScrapingBee API lets you extract data from web pages programmatically. It handles headless browsers and rotating proxies, so you can focus on data extraction without worrying about common web scraping issues like getting blocked. On Pipedream, you can tap into ScrapingBee's capability to create serverless workflows that scrape web data and connect them with numerous services for processing, storing, or triggering actions. + +# Example Use Cases + +- **Automated Content Aggregation**: Build a workflow that uses ScrapingBee to scrape news articles or blog posts from various sites daily. Combine it with Pipedream's built-in cron scheduler to automate the process. The extracted data can be sent to apps like Google Sheets for organization and analysis, or used to update content on your website via CMS API. + +- **E-commerce Price Monitoring**: Set up a Pipedream workflow that employs ScrapingBee to regularly check product prices on competitor websites. Use this data to adjust your pricing strategy dynamically. You could connect this workflow to Slack or email, to notify your team whenever there's a significant price change detected. + +- **SEO Backlink Verification**: Leverage ScrapingBee with Pipedream to verify the presence of backlinks on external websites. This can be part of a broader SEO monitoring strategy, ensuring that your backlink profile remains strong. For found backlinks, update a database through a service like Airtable or send a summary report to a communication platform such as Discord to keep the SEO team informed. diff --git a/components/scrapingbot/README.md b/components/scrapingbot/README.md new file mode 100644 index 0000000000000..a6af9617cdc6d --- /dev/null +++ b/components/scrapingbot/README.md @@ -0,0 +1,11 @@ +# Overview + +ScrapingBot API on Pipedream allows you to scrape websites without getting blocked, fetching crucial information while bypassing common defenses. Whether you're extracting product details, real estate listings, or automating competitor research, this API combined with Pipedream's serverless platform offers you the tools to automate these tasks efficiently. Pipedream's ability to trigger workflows via HTTP requests, schedule them, or react to events, means you can create robust scraping operations that integrate seamlessly with hundreds of other apps. + +# Example Use Cases + +- **Real-time Price Monitoring Workflow**: Extract current pricing information from e-commerce sites using ScrapingBot and use Pipedream's built-in actions to send this data to Google Sheets. Keep an always-updated record of competitors’ prices or track price changes over time. + +- **Lead Generation Automation**: Scrape contact information from business directories with ScrapingBot. Then, enrich the data using Clearbit and add the leads automatically to a CRM platform like Salesforce or HubSpot using Pipedream's native integrations. + +- **Content Change Detection System**: Monitor web pages for content changes with ScrapingBot. Whenever a specified change is detected, trigger an alert using Twilio to send an SMS or Slack to post a message, ensuring you're always the first to know about updates on a target website. diff --git a/components/scraptio/README.md b/components/scraptio/README.md new file mode 100644 index 0000000000000..c30854ac09859 --- /dev/null +++ b/components/scraptio/README.md @@ -0,0 +1,11 @@ +# Overview + +The Scraptio API enables seamless web scraping and data extraction from HTML content. With Scraptio on Pipedream, you can automate the collection of web data, monitor changes on websites, and integrate scraped data with other services. Pipedream's no-code platform enhances Scraptio's capabilities by allowing you to create workflows that trigger on schedules or events, process data, and connect to hundreds of other apps. + +# Example Use Cases + +- **Automated Content Monitoring**: Pair Scraptio with Pipedream's cron job feature to periodically scrape website content. Use this workflow to track changes, such as price fluctuations or updates on competitor web pages, and send alerts via email or Slack when specific changes are detected. + +- **Lead Generation and Enrichment**: Scrape professional directories or job listing sites with Scraptio to gather potential leads. Then, enrich this data using a service like Clearbit within Pipedream, and automatically add the enhanced lead information to a CRM like Salesforce. + +- **News Aggregation**: Create a custom news feed by using Scraptio to extract headlines, summaries, and links from various news outlets. With Pipedream, aggregate these into a single data store, and then share the compiled news via a messaging app like Telegram or an email newsletter service. diff --git a/components/screendesk/README.md b/components/screendesk/README.md new file mode 100644 index 0000000000000..fe35c8bf30fdf --- /dev/null +++ b/components/screendesk/README.md @@ -0,0 +1,11 @@ +# Overview + +The Screendesk API lets you automate customer support tasks directly within your apps or websites. Imagine streamlining interactions like ticket creation, response tracking, and support analytics. With Pipedream, you can easily craft workflows that interact with the Screendesk API to enhance customer support efficiency, trigger actions based on ticket status, or synchronize support data with other tools. + +# Example Use Cases + +- **Automate Ticket Creation from Emails**: Set up a workflow that monitors an email inbox for support requests using the built-in Pipedream Email trigger. When a new email arrives, parse the content and automatically create a ticket in Screendesk, ensuring no request goes unnoticed. + +- **Sync Support Tickets with a CRM**: Maintain a seamless flow of information between Screendesk and a CRM like Salesforce. Anytime a new ticket is filed or updated in Screendesk, use Pipedream’s Salesforce app to reflect those changes in the CRM, keeping all customer interactions in sync. + +- **Aggregate Support Metrics for Analytics**: Collect and send ticket metrics from Screendesk to a data visualization tool like Google Sheets or Data Studio. With a scheduled Pipedream workflow, you can regularly pull data from Screendesk and update your dashboards, giving you up-to-date insights on your support performance. diff --git a/components/screenshot_fyi/actions/create-screenshot/create-screenshot.mjs b/components/screenshot_fyi/actions/create-screenshot/create-screenshot.mjs new file mode 100644 index 0000000000000..90b5a468a97d0 --- /dev/null +++ b/components/screenshot_fyi/actions/create-screenshot/create-screenshot.mjs @@ -0,0 +1,74 @@ +import screenshot_fyi from "../../screenshot_fyi.app.mjs"; + +export default { + key: "screenshot_fyi-create-screenshot", + name: "Create Screenshot", + description: "Takes a screenshot of a webpage using Screenshot.fyi. [See the documentation](https://www.screenshot.fyi/api-docs)", + version: "0.0.1", + type: "action", + props: { + screenshot_fyi, + url: { + type: "string", + label: "URL", + description: "The URL of the webpage to capture", + }, + width: { + type: "integer", + label: "Width", + description: "Width of the viewport in pixels. Default: `1440`", + optional: true, + }, + height: { + type: "integer", + label: "Height", + description: "Height of the viewport in pixels. Default: `900`", + optional: true, + }, + fullPage: { + type: "boolean", + label: "Full Page", + description: "Capture the full scrollable page. Default: `false`", + optional: true, + }, + format: { + type: "string", + label: "Format", + description: "The format of the screenshot. Default: `jpg`", + options: [ + "png", + "jpg", + "jpeg", + ], + optional: true, + }, + disableCookieBanners: { + type: "boolean", + label: "Disable Cookie Banners", + description: "Attempt to remove cookie consent banners. Default: `true`", + optional: true, + }, + darkMode: { + type: "boolean", + label: "Dark Mode", + description: "Enable dark mode when taking the screenshot. Default: `false`", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.screenshot_fyi.takeScreenshot({ + $, + params: { + url: this.url, + width: this.width, + height: this.height, + fullPage: this.fullPage, + format: this.format, + disableCookieBanners: this.disableCookieBanners, + darkMode: this.darkMode, + }, + }); + $.export("$summary", `Screenshot taken for ${this.url}`); + return response; + }, +}; diff --git a/components/screenshot_fyi/package.json b/components/screenshot_fyi/package.json new file mode 100644 index 0000000000000..db634ce53f182 --- /dev/null +++ b/components/screenshot_fyi/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/screenshot_fyi", + "version": "0.1.0", + "description": "Pipedream screenshot.fyi Components", + "main": "screenshot_fyi.app.mjs", + "keywords": [ + "pipedream", + "screenshot_fyi" + ], + "homepage": "https://pipedream.com/apps/screenshot_fyi", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/screenshot_fyi/screenshot_fyi.app.mjs b/components/screenshot_fyi/screenshot_fyi.app.mjs new file mode 100644 index 0000000000000..f68730d766788 --- /dev/null +++ b/components/screenshot_fyi/screenshot_fyi.app.mjs @@ -0,0 +1,33 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "screenshot_fyi", + propDefinitions: {}, + methods: { + _baseUrl() { + return "https://screenshot.fyi/api"; + }, + _makeRequest({ + $ = this, + path, + params, + ...otherOpts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + params: { + ...params, + accessKey: this.$auth.access_key, + }, + ...otherOpts, + }); + }, + takeScreenshot(opts = {}) { + return this._makeRequest({ + path: "/take", + ...opts, + }); + }, + }, +}; diff --git a/components/screenshotone/README.md b/components/screenshotone/README.md new file mode 100644 index 0000000000000..59bd09b188355 --- /dev/null +++ b/components/screenshotone/README.md @@ -0,0 +1,11 @@ +# Overview + +ScreenshotOne API allows you to capture screenshots of web pages programmatically. It's a powerful tool you can leverage within Pipedream workflows to automate the process of taking screenshots, whether it's for archiving content, monitoring changes on websites, or capturing data for reporting. By combining ScreenshotOne with Pipedream's capabilities, you can easily integrate screenshot functionalities into multifaceted workflows, triggering actions in other apps, storing images, or processing the data further. + +# Example Use Cases + +- **Content Archival**: Automate the process of taking daily or weekly screenshots of important web pages and store them in cloud storage services like Google Drive or Dropbox. This can help in maintaining records for compliance or historical reference. + +- **Visual Monitoring**: Set up a workflow that captures screenshots of your website after every deployment to track visual changes over time or to detect unexpected visual anomalies, sending alerts if differences are spotted using image comparison tools. + +- **Reporting Dashboards**: Create automated reports by taking screenshots of data dashboards at regular intervals. These can then be compiled into PDFs or presentations, and shared via email or Slack to keep team members updated with the latest metrics. diff --git a/components/screenshotone/package.json b/components/screenshotone/package.json index 3ce4d15d20c6f..c28069ebe7e02 100644 --- a/components/screenshotone/package.json +++ b/components/screenshotone/package.json @@ -18,4 +18,4 @@ "stream": "^0.0.2", "util": "^0.12.5" } -} \ No newline at end of file +} diff --git a/components/sdk/package.json b/components/sdk/package.json new file mode 100644 index 0000000000000..f8ba24fbcd236 --- /dev/null +++ b/components/sdk/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/sdk", + "version": "0.1.2", + "description": "Pipedream SDK Components", + "main": "sdk.app.mjs", + "keywords": [ + "pipedream", + "sdk", + "integrations", + "api" + ], + "homepage": "https://pipedream.com/docs/connect/workflows", + "author": "Pipedream (https://pipedream.com/)", + "private": true +} diff --git a/components/sdk/sdk.app.mjs b/components/sdk/sdk.app.mjs new file mode 100644 index 0000000000000..5673b6dba3d83 --- /dev/null +++ b/components/sdk/sdk.app.mjs @@ -0,0 +1,5 @@ +export default { + type: "app", + app: "sdk", + propDefinitions: {}, +}; diff --git a/components/sdk/sources/nextjs-event-received/nextjs-event-received.mjs b/components/sdk/sources/nextjs-event-received/nextjs-event-received.mjs new file mode 100644 index 0000000000000..132f58233ecc3 --- /dev/null +++ b/components/sdk/sources/nextjs-event-received/nextjs-event-received.mjs @@ -0,0 +1,16 @@ +/* eslint-disable pipedream/source-description */ +/* eslint-disable pipedream/source-name */ +import sdk from "../../sdk.app.mjs"; + +export default { + name: "Next.js", + version: "0.0.2", + key: "sdk-nextjs-event-received", + description: "Emit a new event via the Next.js Pipedream SDK.", + props: { + sdk, + }, + type: "source", + methods: {}, + async run() {}, +}; diff --git a/components/sdk/sources/nodejs-event-received/nodejs-event-received.mjs b/components/sdk/sources/nodejs-event-received/nodejs-event-received.mjs new file mode 100644 index 0000000000000..623d18cfa4ebb --- /dev/null +++ b/components/sdk/sources/nodejs-event-received/nodejs-event-received.mjs @@ -0,0 +1,16 @@ +/* eslint-disable pipedream/source-description */ +/* eslint-disable pipedream/source-name */ +import sdk from "../../sdk.app.mjs"; + +export default { + name: "Node.js", + version: "0.0.2", + key: "sdk-nodejs-event-received", + description: "Emit a new event via the Node.js Pipedream SDK.", + props: { + sdk, + }, + type: "source", + methods: {}, + async run() {}, +}; diff --git a/components/sdk/sources/nuxtjs-event-received/nuxtjs-event-received.mjs b/components/sdk/sources/nuxtjs-event-received/nuxtjs-event-received.mjs new file mode 100644 index 0000000000000..a9283baff04d7 --- /dev/null +++ b/components/sdk/sources/nuxtjs-event-received/nuxtjs-event-received.mjs @@ -0,0 +1,16 @@ +/* eslint-disable pipedream/source-description */ +/* eslint-disable pipedream/source-name */ +import sdk from "../../sdk.app.mjs"; + +export default { + name: "NuxtJS", + version: "0.0.2", + key: "sdk-nuxtjs-event-received", + description: "Emit a new event via the Nuxt.js Pipedream SDK.", + props: { + sdk, + }, + type: "source", + methods: {}, + async run() {}, +}; diff --git a/components/sdk/sources/python-event-received/python-event-received.mjs b/components/sdk/sources/python-event-received/python-event-received.mjs new file mode 100644 index 0000000000000..b1e168bbde536 --- /dev/null +++ b/components/sdk/sources/python-event-received/python-event-received.mjs @@ -0,0 +1,16 @@ +/* eslint-disable pipedream/source-description */ +/* eslint-disable pipedream/source-name */ +import sdk from "../../sdk.app.mjs"; + +export default { + name: "Python", + version: "0.0.2", + key: "sdk-python-event-received", + description: "Emit a new event via the Python Pipedream SDK.", + props: { + sdk, + }, + type: "source", + methods: {}, + async run() {}, +}; diff --git a/components/sdk/sources/reactjs-event-received/reactjs-event-received.mjs b/components/sdk/sources/reactjs-event-received/reactjs-event-received.mjs new file mode 100644 index 0000000000000..1aa3d1b69b4dc --- /dev/null +++ b/components/sdk/sources/reactjs-event-received/reactjs-event-received.mjs @@ -0,0 +1,16 @@ +/* eslint-disable pipedream/source-description */ +/* eslint-disable pipedream/source-name */ +import sdk from "../../sdk.app.mjs"; + +export default { + name: "React.js", + version: "0.0.2", + key: "sdk-reactjs-event-received", + description: "Emit a new event via the React.js Pipedream SDK.", + props: { + sdk, + }, + type: "source", + methods: {}, + async run() {}, +}; diff --git a/components/sdk/sources/vue-event-received/vue-event-received.mjs b/components/sdk/sources/vue-event-received/vue-event-received.mjs new file mode 100644 index 0000000000000..005e51f6e87ed --- /dev/null +++ b/components/sdk/sources/vue-event-received/vue-event-received.mjs @@ -0,0 +1,16 @@ +/* eslint-disable pipedream/source-description */ +/* eslint-disable pipedream/source-name */ +import sdk from "../../sdk.app.mjs"; + +export default { + name: "Vue", + version: "0.0.2", + key: "sdk-vue-event-received", + description: "Emit a new event via the Vue Pipedream SDK.", + props: { + sdk, + }, + type: "source", + methods: {}, + async run() {}, +}; diff --git a/components/search_api/README.md b/components/search_api/README.md new file mode 100644 index 0000000000000..cdd222f9524bc --- /dev/null +++ b/components/search_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Search API API allows you to create and manage a search engine for your website or application, providing robust search capabilities like full-text search, faceting, filtering, and autocomplete. Integrating this API into Pipedream workflows unleashes the potential for automating content indexing, performing complex searches based on triggers, and synchronizing search results with other apps for analytics, monitoring, or further processing. + +# Example Use Cases + +- **Content Indexing Automation**: Automatically index new content from your CMS to your Search API engine whenever a new post or page is published. This ensures that your search engine is always up-to-date with the latest content. + +- **Issue Tracker Search Integration**: Integrate Search API with a platform like GitHub. When a new issue is labeled as a bug, trigger a search in your product's documentation to find relevant troubleshooting content, and post the results in a comment on the issue. + +- **E-commerce Search Analytics**: Tie in Search API with an e-commerce platform like Shopify. Track searches performed on your site, analyze the most common queries, and feed this data into a Google Sheets document for easy review and action on popular items or search terms that return no results. diff --git a/components/search_api/package.json b/components/search_api/package.json index c0e3f5ab653fa..d2838e278f188 100644 --- a/components/search_api/package.json +++ b/components/search_api/package.json @@ -15,4 +15,4 @@ "dependencies": { "@pipedream/platform": "^1.5.1" } -} \ No newline at end of file +} diff --git a/components/seatable/README.md b/components/seatable/README.md new file mode 100644 index 0000000000000..5812a0338dbe9 --- /dev/null +++ b/components/seatable/README.md @@ -0,0 +1,11 @@ +# Overview + +The SeaTable API allows you to interact programmatically with SeaTable databases. With it, you can create, update, and delete records, rows, and columns, manipulate tables, and manage users. In Pipedream, you can harness this API to automate data flows between SeaTable and other apps, trigger workflows based on changes in your SeaTable bases, and manipulate data without manual intervention. Think of it as connecting your spreadsheets' data to the wider web. + +# Example Use Cases + +- **Automated Data Entry from Web Forms to SeaTable**: Trigger a Pipedream workflow with a new form submission from Typeform. Extract the data and create a new row in a SeaTable base to keep track of survey results or lead information without lifting a finger. + +- **Sync SeaTable Records with Google Calendar**: Whenever a new event is added to a SeaTable base, such as a project deadline or a meeting, use Pipedream to create a corresponding event in Google Calendar. This ensures your schedule is always up-to-date across platforms. + +- **Aggregate Social Media Metrics in SeaTable**: Collect social media stats from platforms like Twitter or Facebook via their APIs and insert the data into SeaTable. Use Pipedream to run this workflow daily, giving you a clear, continually updated view of your social media performance metrics. diff --git a/components/seatable/actions/common/rows.mjs b/components/seatable/actions/common/rows.mjs new file mode 100644 index 0000000000000..77bf3b30c9b02 --- /dev/null +++ b/components/seatable/actions/common/rows.mjs @@ -0,0 +1,38 @@ +import seatable from "../../seatable.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + seatable, + tableName: { + propDefinition: [ + seatable, + "tableName", + ], + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (!this.tableName) { + return props; + } + const { columns } = await this.seatable.listColumns({ + baseUuid: await this.seatable.getBaseUuid(), + params: { + table_name: this.tableName, + }, + }); + for (const column of columns) { + if (column.editable) { + props[column.name] = { + type: constants.COLUMN_TYPES[column.type] || "string", + label: column.name, + description: `Value of column ${column.name}. [See the docs](https://api.seatable.io/reference/models#supported-column-types) for info about supported column types`, + optional: true, + }; + } + } + return props; + }, +}; diff --git a/components/seatable/actions/create-row/create-row.mjs b/components/seatable/actions/create-row/create-row.mjs new file mode 100644 index 0000000000000..73686821a7ca6 --- /dev/null +++ b/components/seatable/actions/create-row/create-row.mjs @@ -0,0 +1,31 @@ +import common from "../common/rows.mjs"; + +export default { + ...common, + key: "seatable-create-row", + name: "Create Row", + description: "Creates a new row in the specified table. [See the documentation](https://api.seatable.io/reference/add-row)", + version: "0.0.1", + type: "action", + async run({ $ }) { + const { + seatable, + tableName, + ...rowValues + } = this; + const response = await seatable.createRow({ + $, + baseUuid: await this.seatable.getBaseUuid({ + $, + }), + data: { + table_name: tableName, + row: { + ...rowValues, + }, + }, + }); + $.export("$summary", `Successfully created row with ID ${response._id}`); + return response; + }, +}; diff --git a/components/seatable/actions/delete-row/delete-row.mjs b/components/seatable/actions/delete-row/delete-row.mjs new file mode 100644 index 0000000000000..aebc2e8906e7f --- /dev/null +++ b/components/seatable/actions/delete-row/delete-row.mjs @@ -0,0 +1,41 @@ +import seatable from "../../seatable.app.mjs"; + +export default { + key: "seatable-delete-row", + name: "Delete Row", + description: "Deletes a specific row from a specified table. [See the documentation](https://api.seatable.io/reference/delete-row)", + version: "0.0.1", + type: "action", + props: { + seatable, + tableName: { + propDefinition: [ + seatable, + "tableName", + ], + }, + rowId: { + propDefinition: [ + seatable, + "rowId", + (c) => ({ + tableName: c.tableName, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.seatable.deleteRow({ + $, + baseUuid: await this.seatable.getBaseUuid({ + $, + }), + data: { + table_name: this.tableName, + row_id: this.rowId, + }, + }); + $.export("$summary", `Successfully deleted row with ID ${this.rowId}`); + return response; + }, +}; diff --git a/components/seatable/actions/update-row/update-row.mjs b/components/seatable/actions/update-row/update-row.mjs new file mode 100644 index 0000000000000..0ca99c55ed3d4 --- /dev/null +++ b/components/seatable/actions/update-row/update-row.mjs @@ -0,0 +1,45 @@ +import common from "../common/rows.mjs"; + +export default { + ...common, + key: "seatable-update-row", + name: "Update Row", + description: "Updates an existing row in a specified table. [See the documentation](https://api.seatable.io/reference/update-row)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + rowId: { + propDefinition: [ + common.props.seatable, + "rowId", + (c) => ({ + tableName: c.tableName, + }), + ], + }, + }, + async run({ $ }) { + const { + seatable, + tableName, + rowId, + ...rowValues + } = this; + const response = await seatable.updateRow({ + $, + baseUuid: await this.seatable.getBaseUuid({ + $, + }), + data: { + table_name: tableName, + row_id: rowId, + row: { + ...rowValues, + }, + }, + }); + $.export("$summary", `Successfully updated row with ID ${this.rowId}`); + return response; + }, +}; diff --git a/components/seatable/common/constants.mjs b/components/seatable/common/constants.mjs new file mode 100644 index 0000000000000..459c8d0d1612e --- /dev/null +++ b/components/seatable/common/constants.mjs @@ -0,0 +1,28 @@ +const DEFAULT_LIMIT = 20; + +const COLUMN_TYPES = { + "text": "string", + "email": "string", + "url": "string", + "long-text": "string", + "number": "integer", + "percent": "integer", + "dolar": "integer", + "euro": "integer", + "collaborator": "string[]", + "date": "string", + "duration": "string", + "single-select": "string", + "multiple-select": "string[]", + "image": "string[]", + "file": "string[]", + "checkbox": "boolean", + "rate": "integer", + "formula": "string", + "link": "string[]", +}; + +export default { + DEFAULT_LIMIT, + COLUMN_TYPES, +}; diff --git a/components/seatable/package.json b/components/seatable/package.json new file mode 100644 index 0000000000000..4c08c71cb4e26 --- /dev/null +++ b/components/seatable/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/seatable", + "version": "0.1.0", + "description": "Pipedream SeaTable Components", + "main": "seatable.app.mjs", + "keywords": [ + "pipedream", + "seatable" + ], + "homepage": "https://pipedream.com/apps/seatable", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/seatable/seatable.app.mjs b/components/seatable/seatable.app.mjs new file mode 100644 index 0000000000000..fb55d493f3a2e --- /dev/null +++ b/components/seatable/seatable.app.mjs @@ -0,0 +1,154 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "seatable", + propDefinitions: { + tableName: { + type: "string", + label: "Table Name", + description: "The name of a table", + async options() { + const { tables } = await this.getBaseInfo({ + baseUuid: await this.getBaseUuid(), + }); + return tables?.map(({ name }) => name ) || []; + }, + }, + rowId: { + type: "string", + label: "Row ID", + description: "The identifier of a row", + async options({ + tableName, page, + }) { + const { rows } = await this.listRows({ + baseUuid: await this.getBaseUuid(), + params: { + table_name: tableName, + limit: constants.DEFAULT_LIMIT, + start: page * constants.DEFAULT_LIMIT, + }, + }); + return rows.map(({ + _id: value, Name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://cloud.seatable.io"; + }, + _headers(useAccountToken) { + const token = useAccountToken + ? this.$auth.account_token + : this.$auth.oauth_access_token; + return { + Authorization: `Bearer ${token}`, + }; + }, + _makeRequest({ + $ = this, + path, + useAccountToken = false, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._headers(useAccountToken), + ...opts, + }); + }, + listBases(opts = {}) { + return this._makeRequest({ + path: "/api/v2.1/user-admin-dtables", + useAccountToken: true, + ...opts, + }); + }, + async getBaseUuid(opts = {}) { + const { + personal: bases, groups, + } = await this.listBases(opts); + for (const group of groups) { + bases.push(...group.dtables); + } + const { uuid } = bases.find(({ name }) => name === this.$auth.base_name); + return uuid; + }, + getBaseInfo({ + baseUuid, ...opts + }) { + return this._makeRequest({ + path: `/dtable-server/dtables/${baseUuid}`, + ...opts, + }); + }, + listRows({ + baseUuid, ...opts + }) { + return this._makeRequest({ + path: `/dtable-server/api/v1/dtables/${baseUuid}/rows`, + ...opts, + }); + }, + listColumns({ + baseUuid, ...opts + }) { + return this._makeRequest({ + path: `/dtable-server/api/v1/dtables/${baseUuid}/columns/`, + ...opts, + }); + }, + createRow({ + baseUuid, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/dtable-server/api/v1/dtables/${baseUuid}/rows/`, + ...opts, + }); + }, + updateRow({ + baseUuid, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/dtable-server/api/v1/dtables/${baseUuid}/rows/`, + ...opts, + }); + }, + deleteRow({ + baseUuid, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/dtable-server/api/v1/dtables/${baseUuid}/rows/`, + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: `/api/v2.1/workspace/${this.$auth.workspace_id}/dtable/${this.$auth.base_name}/webhooks/`, + useAccountToken: true, + ...opts, + }); + }, + deleteWebhook({ + hookId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/api/v2.1/workspace/${this.$auth.workspace_id}/dtable/${this.$auth.base_name}/webhooks/${hookId}/`, + useAccountToken: true, + ...opts, + }); + }, + }, +}; diff --git a/components/seatable/sources/common/base.mjs b/components/seatable/sources/common/base.mjs new file mode 100644 index 0000000000000..880b60092f91f --- /dev/null +++ b/components/seatable/sources/common/base.mjs @@ -0,0 +1,48 @@ +import seatable from "../../seatable.app.mjs"; + +export default { + props: { + seatable, + db: "$.service.db", + http: "$.interface.http", + }, + hooks: { + async activate() { + const { webhook } = await this.seatable.createWebhook({ + data: { + url: this.http.endpoint, + }, + }); + this._setHookId(webhook.id); + }, + async deactivate() { + const hookId = this._getHookId(); + if (hookId) { + await this.seatable.deleteWebhook({ + hookId, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + isRelevant() { + return true; + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run(event) { + const { body } = event; + if (this.isRelevant(body)) { + const meta = this.generateMeta(body); + this.$emit(body, meta); + } + }, +}; diff --git a/components/seatable/sources/new-or-updated-row/new-or-updated-row.mjs b/components/seatable/sources/new-or-updated-row/new-or-updated-row.mjs new file mode 100644 index 0000000000000..e3d2ba87a1c27 --- /dev/null +++ b/components/seatable/sources/new-or-updated-row/new-or-updated-row.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "seatable-new-or-updated-row", + name: "New or Updated Row (Instant)", + description: "Emit new event when a row is added or updated in a table.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + isRelevant(event) { + return event.data.op_type === "insert_row" || event.data.op_type === "modify_row"; + }, + generateMeta(event) { + const ts = Date.now(); + const id = event.data.row_id; + return { + id: `${id}-${ts}`, + summary: `New or Updated Row with ID ${id}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/seatable/sources/new-or-updated-row/test-event.mjs b/components/seatable/sources/new-or-updated-row/test-event.mjs new file mode 100644 index 0000000000000..b95d6b4953bb6 --- /dev/null +++ b/components/seatable/sources/new-or-updated-row/test-event.mjs @@ -0,0 +1,24 @@ +export default { + "event": "update", + "data": { + "dtable_uuid": "aa7e1a1e333b4538877501bf0d5f3fb9", + "row_id": "IlNeedgAS2iKiwCYPgwNUw", + "op_user": "989ae609c4764308afbd659993885de1@auth.local", + "op_type": "modify_row", + "op_time": 1711741987.935, + "table_id": "35oe", + "table_name": "2022 Communication Depart.", + "row_name": "2022-01-02 00:00:00", + "row_name_option": "", + "row_data": [ + { + "column_key": "DQF4", + "column_name": "Place", + "column_type": "text", + "column_data": {}, + "value": "HQ", + "old_value": "" + } + ] + } +} \ No newline at end of file diff --git a/components/seatable/sources/new-row-created/new-row-created.mjs b/components/seatable/sources/new-row-created/new-row-created.mjs new file mode 100644 index 0000000000000..9efcd46b41d71 --- /dev/null +++ b/components/seatable/sources/new-row-created/new-row-created.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "seatable-new-row-created", + name: "New Row Created (Instant)", + description: "Emit new event when a new row is added to a table.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + isRelevant(event) { + return event.data.op_type === "insert_row"; + }, + generateMeta(event) { + const id = event.data.row_id; + return { + id, + summary: `New Row with ID ${id}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/seatable/sources/new-row-created/test-event.mjs b/components/seatable/sources/new-row-created/test-event.mjs new file mode 100644 index 0000000000000..fde581bc53854 --- /dev/null +++ b/components/seatable/sources/new-row-created/test-event.mjs @@ -0,0 +1,16 @@ +export default { + "event": "update", + "data": { + "dtable_uuid": "aa7e1a1e333b4538877501bf0d5f3fb9", + "row_id": "e8xQhPN9R6Gk4ApNCAkQ6A", + "op_user": "", + "op_type": "insert_row", + "op_time": 1711740568.904, + "table_id": "35oe", + "table_name": "2022 Communication Depart.", + "row_name": "", + "row_name_option": "", + "row_data": [], + "op_app": "pddev" + } +} \ No newline at end of file diff --git a/components/security_reporter/actions/create-assessment/create-assessment.mjs b/components/security_reporter/actions/create-assessment/create-assessment.mjs new file mode 100644 index 0000000000000..a7294085ad758 --- /dev/null +++ b/components/security_reporter/actions/create-assessment/create-assessment.mjs @@ -0,0 +1,82 @@ +import { SCORING_SYSTEM_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import securityReporter from "../../security_reporter.app.mjs"; + +export default { + key: "security_reporter-create-assessment", + name: "Create Security Assessment", + description: "Creates a new security assessment. [See the documentation](https://trial3.securityreporter.app/api-documentation)", + version: "0.0.1", + type: "action", + props: { + securityReporter, + clientId: { + propDefinition: [ + securityReporter, + "clientId", + ], + }, + assessmentTemplateId: { + propDefinition: [ + securityReporter, + "assessmentTemplateId", + ], + }, + languageId: { + propDefinition: [ + securityReporter, + "languageId", + ], + optional: true, + }, + title: { + type: "string", + label: "Title", + description: "The title of the assessment. Must not be greater than 191 characters.", + }, + tags: { + type: "string[]", + label: "Tags", + description: "Tags to organize assessments. Tags are not case sensitive. Must not be greater than 191 characters.", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "A short description. Must not be greater than 191 characters.", + optional: true, + }, + scoringSystem: { + type: "string", + label: "Scoring System", + description: "The scoring system for the assessment. If not set, the default scoring system will be used. The default scoring system can be changed in the settings. Must be a valid scoring system.", + options: SCORING_SYSTEM_OPTIONS, + optional: true, + }, + themeId: { + propDefinition: [ + securityReporter, + "themeId", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.securityReporter.createAssessment({ + $, + clientId: this.clientId, + data: { + assessment_template_id: this.assessmentTemplateId, + language_id: this.languageId, + title: this.title, + tags: parseObject(this.tags), + description: this.description, + scoring_system: this.scoringSystem, + theme_id: this.themeId, + }, + }); + + $.export("$summary", `Successfully created assessment with Id: ${response.id}`); + return response; + }, +}; diff --git a/components/security_reporter/actions/create-finding/create-finding.mjs b/components/security_reporter/actions/create-finding/create-finding.mjs new file mode 100644 index 0000000000000..8e3bf5c33384b --- /dev/null +++ b/components/security_reporter/actions/create-finding/create-finding.mjs @@ -0,0 +1,240 @@ +import { + COMPLEXITY_OPTIONS, + OWASP_OPTIONS, + PRIORITY_OPTIONS, + SCORING_SYSTEM_OPTIONS, + SEVERITY_ONLY_SEVERITY_OPTIONS, + SEVERITY_OPTIONS, +} from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import securityReporter from "../../security_reporter.app.mjs"; + +export default { + key: "security_reporter-create-finding", + name: "Create Security Finding", + description: "Creates a new security finding. [See the documentation](https://trial3.securityreporter.app/api-documentation)", + version: "0.0.1", + type: "action", + props: { + securityReporter, + assessmentId: { + propDefinition: [ + securityReporter, + "assessmentId", + ], + }, + title: { + type: "string", + label: "Title", + description: "Title of the finding. Must not be greater than 191 characters.", + }, + targets: { + propDefinition: [ + securityReporter, + "targets", + ({ assessmentId }) => ({ + assessmentId, + }), + ], + }, + assessmentSectionId: { + propDefinition: [ + securityReporter, + "assessmentSectionId", + ({ assessmentId }) => ({ + assessmentId, + }), + ], + reloadProps: true, + }, + isVulnerability: { + type: "boolean", + label: "Is Vulnerability", + description: "Whether the finding is for a vulnerability (and has associated severity metrics).", + reloadProps: true, + }, + foundAt: { + type: "string", + label: "Found At", + description: "The date when the finding was found. Format: `YYYY-MM-DDTHH:MM:SS`.", + optional: true, + }, + priority: { + type: "string", + label: "Priority", + description: "How urgent resolving this finding is. Must be a valid priority.", + options: PRIORITY_OPTIONS, + optional: true, + }, + complexity: { + type: "string", + label: "Complexity", + description: "How complex resolving this finding is. Must be a valid complexity.", + options: COMPLEXITY_OPTIONS, + optional: true, + }, + action: { + type: "string", + label: "Action", + description: "The recommended action (under 500 characters) to resolve this finding. **Example: Update ...**", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "The description of the finding. **Example: There is ...**", + }, + risk: { + type: "string", + label: "Risk", + description: "The risk associated with the finding. **Example: A hacker could ...**", + optional: true, + }, + recommendation: { + type: "string", + label: "Recommendation", + description: "The recommendation for the finding. **Example: Update ...**", + optional: true, + }, + proof: { + type: "string", + label: "Proof", + description: "The proof for the finding. **Example: See attached ...**", + optional: true, + }, + references: { + type: "string", + label: "References", + description: "The references for the finding. **Example: - https://owasp.org/Top10/A03_2021-Injection/`\n - https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/**", + optional: true, + }, + draftDocuments: { + type: "string[]", + label: "Draft Documents", + description: "Document IDs of uploaded draft documents.", + optional: true, + }, + draftDocumentsFile: { + type: "string[]", + label: "Draft Documents File ", + description: "The path to a file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp).", + optional: true, + }, + resolvers: { + propDefinition: [ + securityReporter, + "resolvers", + ({ assessmentId }) => ({ + assessmentId, + }), + ], + optional: true, + }, + userGroups: { + propDefinition: [ + securityReporter, + "userGroups", + ({ assessmentId }) => ({ + assessmentId, + }), + ], + optional: true, + }, + classifications: { + type: "string[]", + label: "Classifications", + description: "An array with classifications by classification system. You can use any combination of CWE, CAPEC or VRT classifications. Note that classifications are ignored if their system is not set in the assessment.", + optional: true, + }, + SMScoringSystem: { + type: "string", + label: "Severity Metrics Scoring System", + description: "The scoring system you want to use. [See the documentation](https://trial3.securityreporter.app/api-documentation#scoring-systems) for further information.", + options: SCORING_SYSTEM_OPTIONS, + reloadProps: true, + }, + }, + async additionalProps() { + const props = { + severity: { + type: "string", + label: "Severity", + description: "The severity of the finding. Determined from severity metrics otherwise. Must be a valid severity.", + options: SEVERITY_OPTIONS, + optional: !this.isVulnerability, + }, + }; + switch (this.SMScoringSystem) { + case "owasp": + props.severityMetricsImpact = { + type: "string", + label: "Severity Metrics Impact", + description: "The impact metric.", + options: OWASP_OPTIONS, + }; + props.severityMetricsLikelihood = { + type: "string", + label: "Severity Metrics Likelihood", + description: "The likelihood metric.", + options: OWASP_OPTIONS, + }; + break; + case "cvss_v3_1": + props.cvssString = { + type: "string", + label: "Severity Metrics CVSS String", + description: "The Common Vulnerability Scoring System uses a combination of eight [base metrics](https://www.first.org/cvss/v3.1/specification-document#Base-Metrics) to compute the base severity score. Currently only the base metrics are supported. A calculator to transform base metrics into a severity score can be found [here](https://www.first.org/cvss/calculator/3.1). Manual calculations are not needed as the severity_score and severity of a model will be automatically computed upon save.", + }; + break; + case "severity_only": + props.severityOnlySeverity = { + type: "string", + label: "Severity Metrics Severity", + description: "Severity only is the simplest scoring system. It simply sets the severity directly without any underlying math.", + options: SEVERITY_ONLY_SEVERITY_OPTIONS, + }; + } + return props; + }, + async run({ $ }) { + const fileIds = await this.securityReporter.prepareFiles({ + draftDocumentsFile: this.draftDocumentsFile, + draftDocuments: this.draftDocuments, + }); + + const response = await this.securityReporter.createSecurityFinding({ + $, + assessmentId: this.assessmentId, + data: { + title: this.title, + targets: parseObject(this.targets), + assessment_section_id: this.assessmentSectionId, + is_vulnerability: this.isVulnerability, + severity_metrics: { + impact: this.severityMetricsImpact && parseInt(this.severityMetricsImpact), + likelihood: this.severityMetricsLikelihood && parseInt(this.severityMetricsLikelihood), + cvss_string: this.cvssString, + severity: this.severityOnlySeverity && parseInt(this.severityOnlySeverity), + scoring_system: this.SMScoringSystem, + }, + severity: this.severity && parseInt(this.severity), + found_at: this.foundAt, + priority: this.priority && parseInt(this.priority), + complexity: this.complexity && parseInt(this.complexity), + action: this.action, + description: this.description, + risk: this.risk, + recommendation: this.recommendation, + proof: this.proof, + references: this.references, + draft_documents: fileIds, + resolvers: parseObject(this.resolvers), + user_groups: parseObject(this.userGroups), + classifications: parseObject(this.classifications), + }, + }); + + $.export("$summary", `Successfully created security finding with title: ${this.title}`); + return response; + }, +}; diff --git a/components/security_reporter/actions/update-finding/update-finding.mjs b/components/security_reporter/actions/update-finding/update-finding.mjs new file mode 100644 index 0000000000000..71fc31b708a73 --- /dev/null +++ b/components/security_reporter/actions/update-finding/update-finding.mjs @@ -0,0 +1,281 @@ +import { + COMPLEXITY_OPTIONS, + OWASP_OPTIONS, + PRIORITY_OPTIONS, + REVIEW_STATUS_OPTIONS, + SCORING_SYSTEM_OPTIONS, + SEVERITY_ONLY_SEVERITY_OPTIONS, + STATUS_OPTIONS, +} from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import securityReporter from "../../security_reporter.app.mjs"; + +export default { + key: "security_reporter-update-finding", + name: "Update Security Finding", + description: "Updates an existing security finding. [See the documentation](https://trial3.securityreporter.app/api-documentation)", + version: "0.0.1", + type: "action", + props: { + securityReporter, + findingId: { + propDefinition: [ + securityReporter, + "findingId", + ], + }, + title: { + type: "string", + label: "Title", + description: "Title of the finding. Must not be greater than 191 characters.", + optional: true, + }, + targets: { + propDefinition: [ + securityReporter, + "targets", + ({ findingId }) => ({ + findingId, + }), + ], + optional: true, + }, + assessmentSectionId: { + propDefinition: [ + securityReporter, + "assessmentSectionId", + ({ findingId }) => ({ + findingId, + }), + ], + optional: true, + reloadProps: true, + }, + isVulnerability: { + type: "boolean", + label: "Is Vulnerability", + description: "Whether the finding is for a vulnerability (and has associated severity metrics).", + optional: true, + reloadProps: true, + }, + status: { + type: "string", + label: "Status", + description: "The current status of the finding. Can not be changed to or from Retest Pending. Must be a valid finding status.", + options: STATUS_OPTIONS, + optional: true, + }, + resolvedTargets: { + propDefinition: [ + securityReporter, + "resolvedTargets", + ({ findingId }) => ({ + findingId, + }), + ], + optional: true, + }, + reviewStatus: { + type: "string", + label: "Review Status", + description: "The current review status of the finding. Must be a valid review status.", + options: REVIEW_STATUS_OPTIONS, + optional: true, + }, + foundAt: { + type: "string", + label: "Found At", + description: "The date when the finding was found. Format: `YYYY-MM-DDTHH:MM:SS`.", + optional: true, + }, + priority: { + type: "string", + label: "Priority", + description: "How urgent resolving this finding is. Must be a valid priority.", + options: PRIORITY_OPTIONS, + optional: true, + }, + complexity: { + type: "string", + label: "Complexity", + description: "How complex resolving this finding is. Must be a valid complexity.", + options: COMPLEXITY_OPTIONS, + optional: true, + }, + action: { + type: "string", + label: "Action", + description: "The recommended action (under 500 characters) to resolve this finding. **Example: Update ...**", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "The description of the finding. **Example: There is ...**", + optional: true, + }, + risk: { + type: "string", + label: "Risk", + description: "The risk associated with the finding. **Example: A hacker could ...**", + optional: true, + }, + recommendation: { + type: "string", + label: "Recommendation", + description: "The recommendation for the finding. **Example: Update ...**", + optional: true, + }, + proof: { + type: "string", + label: "Proof", + description: "The proof for the finding. **Example: See attached ...**", + optional: true, + }, + references: { + type: "string", + label: "References", + description: "The references for the finding. **Example: - https://owasp.org/Top10/A03_2021-Injection/`\n - https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/**", + optional: true, + }, + draftDocuments: { + type: "string[]", + label: "Draft Documents", + description: "Document IDs of uploaded draft documents.", + optional: true, + }, + draftDocumentsFile: { + type: "string[]", + label: "Draft Documents File ", + description: "The path to a file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp).", + optional: true, + }, + resolvers: { + propDefinition: [ + securityReporter, + "resolvers", + ({ findingId }) => ({ + findingId, + }), + ], + optional: true, + }, + userGroups: { + propDefinition: [ + securityReporter, + "userGroups", + ({ findingId }) => ({ + findingId, + }), + ], + optional: true, + }, + classifications: { + type: "string[]", + label: "Classifications", + description: "An array with classifications by classification system. You can use any combination of CWE, CAPEC or VRT classifications. Note that classifications are ignored if their system is not set in the assessment.", + optional: true, + }, + SMScoringSystem: { + type: "string", + label: "Severity Metrics Scoring System", + description: "The scoring system you want to use. [See the documentation](https://trial3.securityreporter.app/api-documentation#scoring-systems) for further information.", + options: SCORING_SYSTEM_OPTIONS, + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.isVulnerability) { + const { severity_metrics: SM } = await this.securityReporter.getFinding({ + findingId: this.findingId, + }); + switch (this.SMScoringSystem) { + case "owasp": + props.severityMetricsImpact = { + type: "string", + label: "Severity Metrics Impact", + description: "The impact metric.", + options: OWASP_OPTIONS, + default: (SM && SM.scoring_system === "owasp") + ? `${SM.impact}` + : "", + }; + props.severityMetricsLikelihood = { + type: "string", + label: "Severity Metrics Likelihood", + description: "The likelihood metric.", + options: OWASP_OPTIONS, + default: (SM && SM.scoring_system === "owasp") + ? `${SM.likelihood}` + : "", + }; + break; + case "cvss_v3_1": + props.cvssString = { + type: "string", + label: "Severity Metrics CVSS String", + description: "The Common Vulnerability Scoring System uses a combination of eight [base metrics](https://www.first.org/cvss/v3.1/specification-document#Base-Metrics) to compute the base severity score. Currently only the base metrics are supported. A calculator to transform base metrics into a severity score can be found [here](https://www.first.org/cvss/calculator/3.1). Manual calculations are not needed as the severity_score and severity of a model will be automatically computed upon save.", + default: (SM && SM.scoring_system === "cvss_v3_1") + ? `${SM.cvss_string}` + : "", + }; + break; + case "severity_only": + props.severityOnlySeverity = { + type: "string", + label: "Severity Metrics Severity", + description: "Severity only is the simplest scoring system. It simply sets the severity directly without any underlying math.", + options: SEVERITY_ONLY_SEVERITY_OPTIONS, + default: (SM && SM.scoring_system === "severity_only") + ? `${SM.severity}` + : "", + }; + } + } + return props; + }, + async run({ $ }) { + const fileIds = await this.securityReporter.prepareFiles({ + draftDocumentsFile: this.draftDocumentsFile, + draftDocuments: this.draftDocuments, + }); + + const response = await this.securityReporter.updateSecurityFinding({ + $, + findingId: this.findingId, + data: { + title: this.title, + targets: parseObject(this.targets), + assessment_section_id: this.assessmentSectionId, + is_vulnerability: this.isVulnerability, + status: this.status && parseInt(this.status), + resolved_targets: parseObject(this.resolved_targets), + severity_metrics: { + impact: this.severityMetricsImpact && parseInt(this.severityMetricsImpact), + likelihood: this.severityMetricsLikelihood && parseInt(this.severityMetricsLikelihood), + cvss_string: this.cvssString, + severity: this.severityOnlySeverity && parseInt(this.severityOnlySeverity), + scoring_system: this.SMScoringSystem, + }, + review_status: this.reviewStatus && parseInt(this.reviewStatus), + found_at: this.foundAt, + priority: this.priority && parseInt(this.priority), + complexity: this.complexity && parseInt(this.complexity), + action: this.action, + description: this.description, + risk: this.risk, + recommendation: this.recommendation, + proof: this.proof, + references: this.references, + draft_documents: fileIds, + resolvers: parseObject(this.resolvers), + user_groups: parseObject(this.userGroups), + classifications: parseObject(this.classifications), + }, + }); + + $.export("$summary", `Successfully updated finding with ID ${this.findingId}`); + return response; + }, +}; diff --git a/components/security_reporter/common/constants.mjs b/components/security_reporter/common/constants.mjs new file mode 100644 index 0000000000000..d1f06a8cc27d2 --- /dev/null +++ b/components/security_reporter/common/constants.mjs @@ -0,0 +1,147 @@ +export const SEVERITY_OPTIONS = [ + { + label: "Not Applicable", + value: "0", + }, + { + label: "Unknown", + value: "1", + }, + { + label: "OK", + value: "2", + }, + { + label: "False Positive", + value: "3", + }, +]; + +export const PRIORITY_OPTIONS = [ + { + label: "Unknown", + value: "0", + }, + { + label: "Low", + value: "1", + }, + { + label: "Medium", + value: "2", + }, + { + label: "High", + value: "3", + }, +]; + +export const COMPLEXITY_OPTIONS = [ + { + label: "Unknown", + value: "0", + }, + { + label: "Trivial", + value: "1", + }, + { + label: "Medium", + value: "2", + }, + { + label: "Complex", + value: "3", + }, +]; + +export const SCORING_SYSTEM_OPTIONS = [ + { + label: "OWASP Risk Rating Methodology", + value: "owasp", + }, + { + label: "CVSS v3.1", + value: "cvss_v3_1", + }, + { + label: "Severity Only", + value: "severity_only", + }, +]; + +export const OWASP_OPTIONS = [ + { + label: "Low", + value: "0", + }, + { + label: "Medium", + value: "1", + }, + { + label: "High", + value: "2", + }, +]; + +export const SEVERITY_ONLY_SEVERITY_OPTIONS = [ + { + label: "Info", + value: "8", + }, + { + label: "Low", + value: "9", + }, + { + label: "Medium", + value: "10", + }, + { + label: "High", + value: "11", + }, + { + label: "Critical", + value: "12", + }, +]; + +export const STATUS_OPTIONS = [ + { + label: "Unresolved", + value: "0", + }, + { + label: "Resolved", + value: "1", + }, + { + label: "Retest Pending", + value: "2", + }, + { + label: "Accepted Risk", + value: "3", + }, +]; + +export const REVIEW_STATUS_OPTIONS = [ + { + label: "Draft", + value: "0", + }, + { + label: "Under Review", + value: "1", + }, + { + label: "Revision Requested", + value: "2", + }, + { + label: "Published", + value: "3", + }, +]; diff --git a/components/security_reporter/common/utils.mjs b/components/security_reporter/common/utils.mjs new file mode 100644 index 0000000000000..d1e7ed1a22d98 --- /dev/null +++ b/components/security_reporter/common/utils.mjs @@ -0,0 +1,31 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; + +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; diff --git a/components/security_reporter/package.json b/components/security_reporter/package.json new file mode 100644 index 0000000000000..9836a4bd0c18f --- /dev/null +++ b/components/security_reporter/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/security_reporter", + "version": "0.1.0", + "description": "Pipedream Security Reporter Components", + "main": "security_reporter.app.mjs", + "keywords": [ + "pipedream", + "security_reporter" + ], + "homepage": "https://pipedream.com/apps/security_reporter", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1", + "form-data": "^4.0.0" + } +} diff --git a/components/security_reporter/security_reporter.app.mjs b/components/security_reporter/security_reporter.app.mjs new file mode 100644 index 0000000000000..2f36ffb9bbdc9 --- /dev/null +++ b/components/security_reporter/security_reporter.app.mjs @@ -0,0 +1,421 @@ +import { axios } from "@pipedream/platform"; +import FormData from "form-data"; +import fs from "fs"; +import { + checkTmp, + parseObject, +} from "./common/utils.mjs"; + +export default { + type: "app", + app: "security_reporter", + propDefinitions: { + clientId: { + type: "string", + label: "Client ID", + description: "The ID of the client", + async options({ page }) { + const { data } = await this.listClients({ + params: { + "page[number]": page + 1, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + assessmentTemplateId: { + type: "string", + label: "Assessment Template ID", + description: "The ID of the assessment template", + async options({ page }) { + const { data } = await this.listAssessmentTemplates({ + params: { + "page[number]": page + 1, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + languageId: { + type: "string", + label: "Language ID", + description: "The ID of the language", + async options({ page }) { + const { data } = await this.listLanguages({ + params: { + "page[number]": page + 1, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + themeId: { + type: "string", + label: "Theme ID", + description: "ID of the report theme. If this is not set, the default theme will be used.", + async options({ page }) { + const { data } = await this.listThemes({ + params: { + "page[number]": page + 1, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + resolvers: { + type: "string[]", + label: "Resolvers", + description: "User IDs of users assigned to resolve the finding.", + async options({ + assessmentId, findingId, + }) { + const id = await this.getAssesmentId({ + assessmentId, + findingId, + }); + const { users } = await this.getAssessment({ + assessmentId: id, + params: { + include: "users", + }, + }); + + return users.map(({ + id: value, email: label, + }) => ({ + label, + value, + })); + }, + }, + userGroups: { + type: "string[]", + label: "User Groups", + description: "The user groups for the finding", + async options({ + assessmentId, findingId, + }) { + const id = await this.getAssesmentId({ + assessmentId, + findingId, + }); + const { userGroups } = await this.getAssessment({ + assessmentId: id, + params: { + include: "userGroups", + }, + }); + + return userGroups.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + resolvedTargets: { + type: "string[]", + label: "Resolved Targets", + description: "The targets for which the finding is resolved. If all targets are resolved, the finding is resolved as well.", + async options({ findingId }) { + const { targets } = await this.getFinding({ + findingId, + params: { + include: "targets", + }, + }); + + return targets.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + targets: { + type: "string[]", + label: "Targets", + description: "The IDs of targets the finding applies to. Each target must belong to the assessment.", + async options({ + page, assessmentId, findingId, + }) { + const id = await this.getAssesmentId({ + assessmentId, + findingId, + }); + const { data } = await this.listTargets({ + params: { + "page[number]": page + 1, + "filter[assessment_id]": id, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + assessmentSectionId: { + type: "string", + label: "Assessment Section ID", + description: "The ID of the assessment section to put the finding in. The section must belong to the assessment, and its can_have_findings must be true.", + async options({ + assessmentId, findingId, + }) { + const id = await this.getAssesmentId({ + assessmentId, + findingId, + }); + const { sections } = await this.getAssessment({ + assessmentId: id, + params: { + include: "sections", + }, + }); + + return sections.filter(({ can_have_findings }) => can_have_findings).map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + name: { + type: "string", + label: "Name", + description: "The name associated with the webhook.", + }, + assessmentId: { + type: "string", + label: "Assessment ID", + description: "The ID of the assessment", + async options({ page }) { + const { data } = await this.listAssessments({ + params: { + "page[number]": page + 1, + }, + }); + + return data.map(({ + id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + findingId: { + type: "string", + label: "Finding ID", + description: "The ID of the finding", + async options({ page }) { + const { data } = await this.listFindings({ + params: { + "page[number]": page + 1, + }, + }); + + return data.map(({ + id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return `${this.$auth.base_url}/api/v1`; + }, + _headers(headers = {}) { + return { + "Authorization": `Bearer ${this.$auth.api_token}`, + "Content-Type": "application/json", + "Accept": "application/json", + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(headers), + ...opts, + }); + }, + async getAssesmentId({ + assessmentId, findingId, + }) { + if (assessmentId) return assessmentId; + const { assessment_id: id } = await this.getFinding({ + findingId, + }); + return id; + }, + createAssessment({ + clientId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/clients/${clientId}/assessments`, + ...opts, + }); + }, + getAssessment({ + assessmentId, ...opts + }) { + return this._makeRequest({ + path: `/assessments/${assessmentId}`, + ...opts, + }); + }, + getFinding({ + findingId, ...opts + }) { + return this._makeRequest({ + path: `/findings/${findingId}`, + ...opts, + }); + }, + listClients(opts = {}) { + return this._makeRequest({ + path: "/clients", + ...opts, + }); + }, + listTargets(opts = {}) { + return this._makeRequest({ + path: "/targets", + ...opts, + }); + }, + + listAssessments(opts = {}) { + return this._makeRequest({ + path: "/assessments", + ...opts, + }); + }, + listFindings(opts = {}) { + return this._makeRequest({ + path: "/findings", + ...opts, + }); + }, + listAssessmentTemplates(opts = {}) { + return this._makeRequest({ + path: "/assessment-templates", + ...opts, + }); + }, + listLanguages(opts = {}) { + return this._makeRequest({ + path: "/languages", + ...opts, + }); + }, + listThemes(opts = {}) { + return this._makeRequest({ + path: "/themes", + ...opts, + }); + }, + createSecurityFinding({ + assessmentId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/assessments/${assessmentId}/findings`, + ...opts, + }); + }, + updateSecurityFinding({ + findingId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/findings/${findingId}`, + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + }); + }, + uploadFile(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/documents", + ...opts, + }); + }, + async prepareFiles({ + draftDocumentsFile = null, draftDocuments = null, + }) { + const fileIds = []; + if (draftDocumentsFile) { + const files = parseObject(draftDocumentsFile); + for (const path of files) { + const data = new FormData(); + const file = fs.createReadStream(checkTmp(path)); + data.append("file", file); + data.append("documentable_type", "Finding"); + data.append("section", "description"); + const { id } = await this.uploadFile({ + data, + headers: data.getHeaders(), + }); + fileIds.push(id); + } + } + + if (draftDocuments) { + for (const document of parseObject(draftDocuments)) { + fileIds.push(document); + } + } + return fileIds; + }, + }, +}; diff --git a/components/security_reporter/sources/common/base.mjs b/components/security_reporter/sources/common/base.mjs new file mode 100644 index 0000000000000..69bc95ecbbf18 --- /dev/null +++ b/components/security_reporter/sources/common/base.mjs @@ -0,0 +1,56 @@ +import { randomUUID } from "crypto"; +import securityReporter from "../../security_reporter.app.mjs"; + +export default { + props: { + securityReporter, + http: { + type: "$.interface.http", + customResponse: false, + }, + db: "$.service.db", + name: { + propDefinition: [ + securityReporter, + "name", + ], + }, + }, + hooks: { + async activate() { + const { id } = await this.securityReporter.createWebhook({ + data: { + url: this.http.endpoint, + name: this.name, + secret: randomUUID(), + auth_method: 0, + types: this.getTypes(), + mode: 0, + }, + }); + this._setWebhookId(id); + }, + async deactivate() { + const webhookId = this._getWebhookId(); + if (webhookId) { + await this.securityReporter.deleteWebhook(webhookId); + } + }, + }, + methods: { + _getWebhookId() { + return this.db.get("webhookId"); + }, + _setWebhookId(webhookId) { + this.db.set("webhookId", webhookId); + }, + }, + async run({ body }) { + const ts = Date.parse(body.model.updated_at); + this.$emit(body, { + id: `${body.model.id}-${ts}`, + summary: this.getSummary(body.model), + ts, + }); + }, +}; diff --git a/components/security_reporter/sources/new-assessment-instant/new-assessment-instant.mjs b/components/security_reporter/sources/new-assessment-instant/new-assessment-instant.mjs new file mode 100644 index 0000000000000..3c61e77a936bc --- /dev/null +++ b/components/security_reporter/sources/new-assessment-instant/new-assessment-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "security_reporter-new-assessment-instant", + name: "New Assessment Created (Instant)", + description: "Emit new event when an assessment is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTypes() { + return [ + "assessment:created", + ]; + }, + getSummary(item) { + return `New assessment created: ${item.title}`; + }, + }, + sampleEmit, +}; diff --git a/components/security_reporter/sources/new-assessment-instant/test-event.mjs b/components/security_reporter/sources/new-assessment-instant/test-event.mjs new file mode 100644 index 0000000000000..87d759ed58769 --- /dev/null +++ b/components/security_reporter/sources/new-assessment-instant/test-event.mjs @@ -0,0 +1,58 @@ +export default { + "webhook_type": "assessment:created", + "model_type": "Assessment", + "model": { + "id": "e7b579a30cf7293991e14840802f722b", + "assessment_template_name": "NCSC Guidelines", + "assessment_template_id": "ncsc", + "client_id": "e7b579a30cf7293991e14840802f722b", + "theme_id": "e7b579a30cf7293991e14840802f722b", + "language_id": "en", + "status": 0, + "on_hold": false, + "title": "assessment test", + "title_with_date": "assessment test", + "tags": [ + "tag01", + "tag02" + ], + "description": "description test", + "scoring_system": "cvss_v3_1", + "action_plan": true, + "schedule_color": "#b5d4d3", + "internal_details": "# Invoice Details\n\n[caution] Demo text. This template text can be changed via the [settings page](reporter::settings).\n\n# Financial Contact(s)\n\nDemo text.\n\n# Quotation Details\n\nDemo text.\n\n# Technical Contact(s)\n\nDemo text.\n\n# Report Recipient(s)\n\nDemo text.\n\n# Internal Remarks\n\nDemo text.", + "researcher_research_hours": null, + "total_research_hours": null, + "show_targets": true, + "show_results": true, + "auto_generate_pdf": true, + "auto_generate_management_pdf": false, + "management_report_available": false, + "show_report_toc": true, + "show_management_report_toc": true, + "completed_date": null, + "researcher_briefing": "# Research Introduction\n\n[caution] Demo text. This template text can be changed via the [settings page](reporter::settings).\n\n# Technical Contact(s)\n\nDemo text.\n\n# Out Of Scope Items\n\nDemo text.\n\n# Whitelisting Details\n\nDemo text.\n\n# Remarks\n\nDemo text.", + "researcher_account_details": null, + "classifications": [ + "CWE", + "CAPEC" + ], + "restrict_findings_to_resolvers": false, + "custom_short_id": false, + "local_short_id": null, + "short_id": null, + "next_deadline": null, + "show_shared_information": true, + "shared_information": "", + "results_table_options": { + "by": "section", + "full_short_id": false, + "show_vulnerability_icons": true, + "vulnerability_min_severity": 8, + "hide_short_id": false, + "show_overall_risk_severity_badge": true + }, + "created_at": "2024-09-04T15:46:03.692868Z", + "updated_at": "2024-09-04T15:46:03.692868Z" + } +} \ No newline at end of file diff --git a/components/security_reporter/sources/new-finding-instant/new-finding-instant.mjs b/components/security_reporter/sources/new-finding-instant/new-finding-instant.mjs new file mode 100644 index 0000000000000..9e1f6bcf97741 --- /dev/null +++ b/components/security_reporter/sources/new-finding-instant/new-finding-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "security_reporter-new-finding-instant", + name: "New Finding Created (Instant)", + description: "Emit new event when a finding is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTypes() { + return [ + "finding:created", + ]; + }, + getSummary(item) { + return `New Finding: ${item.title}`; + }, + }, + sampleEmit, +}; diff --git a/components/security_reporter/sources/new-finding-instant/test-event.mjs b/components/security_reporter/sources/new-finding-instant/test-event.mjs new file mode 100644 index 0000000000000..8b91f3f2128b3 --- /dev/null +++ b/components/security_reporter/sources/new-finding-instant/test-event.mjs @@ -0,0 +1,46 @@ +export default { + "webhook_type": "finding:created", + "model_type": "Finding", + "model": { + "id": "e7b579a30cf7293991e14840802f722b", + "assessment_id": "e7b579a30cf7293991e14840802f722b", + "user_id": "e7b579a30cf7293991e14840802f722b", + "assessment_section_id": "e7b579a30cf7293991e14840802f722b", + "is_vulnerability": true, + "classifications": {}, + "classification_categories": {}, + "severity": 12, + "severity_score": 9.5, + "severity_metrics": { + "impact": 2, + "likelihood": 2, + "scoring_system": "owasp" + }, + "status": 0, + "review_status": 0, + "finding_template_id": null, + "has_caution": false, + "original_is_vulnerability": null, + "original_severity": null, + "original_severity_metrics": null, + "original_severity_score": null, + "number": 15, + "short_id": "R-FGKbBI-EABCBq-202408-015", + "title": "finding test", + "priority": 2, + "complexity": 1, + "action": "Action test", + "description": "description test", + "risk": "Risk test", + "recommendation": "REcommendation test", + "proof": "Proof test", + "references": "References test", + "found_at": "2024-09-04T20:22:22.000000Z", + "published_at": null, + "reviewed_at": null, + "reviewed_by_user_id": null, + "created_at": "2024-09-04T15:32:06.716244Z", + "resolved_at": null, + "updated_at": "2024-09-04T15:32:06.716244Z" + } +} \ No newline at end of file diff --git a/components/security_reporter/sources/new-finding-updated-instant/new-finding-updated-instant.mjs b/components/security_reporter/sources/new-finding-updated-instant/new-finding-updated-instant.mjs new file mode 100644 index 0000000000000..3e070eb8dc396 --- /dev/null +++ b/components/security_reporter/sources/new-finding-updated-instant/new-finding-updated-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "security_reporter-new-finding-updated-instant", + name: "New Finding Updated (Instant)", + description: "Emit new event when a finding is updated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTypes() { + return [ + "finding:updated", + ]; + }, + getSummary(item) { + return `New finding updated: ${item.title}`; + }, + }, + sampleEmit, +}; diff --git a/components/security_reporter/sources/new-finding-updated-instant/test-event.mjs b/components/security_reporter/sources/new-finding-updated-instant/test-event.mjs new file mode 100644 index 0000000000000..8b91f3f2128b3 --- /dev/null +++ b/components/security_reporter/sources/new-finding-updated-instant/test-event.mjs @@ -0,0 +1,46 @@ +export default { + "webhook_type": "finding:created", + "model_type": "Finding", + "model": { + "id": "e7b579a30cf7293991e14840802f722b", + "assessment_id": "e7b579a30cf7293991e14840802f722b", + "user_id": "e7b579a30cf7293991e14840802f722b", + "assessment_section_id": "e7b579a30cf7293991e14840802f722b", + "is_vulnerability": true, + "classifications": {}, + "classification_categories": {}, + "severity": 12, + "severity_score": 9.5, + "severity_metrics": { + "impact": 2, + "likelihood": 2, + "scoring_system": "owasp" + }, + "status": 0, + "review_status": 0, + "finding_template_id": null, + "has_caution": false, + "original_is_vulnerability": null, + "original_severity": null, + "original_severity_metrics": null, + "original_severity_score": null, + "number": 15, + "short_id": "R-FGKbBI-EABCBq-202408-015", + "title": "finding test", + "priority": 2, + "complexity": 1, + "action": "Action test", + "description": "description test", + "risk": "Risk test", + "recommendation": "REcommendation test", + "proof": "Proof test", + "references": "References test", + "found_at": "2024-09-04T20:22:22.000000Z", + "published_at": null, + "reviewed_at": null, + "reviewed_by_user_id": null, + "created_at": "2024-09-04T15:32:06.716244Z", + "resolved_at": null, + "updated_at": "2024-09-04T15:32:06.716244Z" + } +} \ No newline at end of file diff --git a/components/securitytrails/actions/get-domain-details/get-domain-details.mjs b/components/securitytrails/actions/get-domain-details/get-domain-details.mjs new file mode 100644 index 0000000000000..3edce98284643 --- /dev/null +++ b/components/securitytrails/actions/get-domain-details/get-domain-details.mjs @@ -0,0 +1,41 @@ +import app from "../../securitytrails.app.mjs"; + +export default { + key: "securitytrails-get-domain-details", + name: "Get Domain Details", + description: "Returns the current data about the given hostname. [See the documentation](https://docs.securitytrails.com/reference/domain-details)", + version: "0.0.1", + type: "action", + props: { + app, + hostname: { + propDefinition: [ + app, + "hostname", + ], + }, + }, + methods: { + getDomainDetails({ + hostname, ...args + } = {}) { + return this.app._makeRequest({ + path: `/domain/${hostname}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + getDomainDetails, + hostname, + } = this; + + const response = await getDomainDetails({ + $, + hostname, + }); + $.export("$summary", `Successfully retrieved details for hostname \`${response.hostname}\`.`); + return response; + }, +}; diff --git a/components/securitytrails/actions/get-history-dns/get-history-dns.mjs b/components/securitytrails/actions/get-history-dns/get-history-dns.mjs new file mode 100644 index 0000000000000..caf797cfd7c9b --- /dev/null +++ b/components/securitytrails/actions/get-history-dns/get-history-dns.mjs @@ -0,0 +1,74 @@ +import app from "../../securitytrails.app.mjs"; + +export default { + key: "securitytrails-get-history-dns", + name: "Get Historical DNS", + description: "Lists out specific historical information about the given hostname parameter. [See the documentation](https://docs.securitytrails.com/reference/history-dns)", + version: "0.0.1", + type: "action", + props: { + app, + hostname: { + propDefinition: [ + app, + "hostname", + ], + }, + type: { + type: "string", + label: "Record Type", + description: "Select the DNS record type to query historical data for.", + options: [ + { + label: "A", + value: "a", + }, + { + label: "AAAA", + value: "aaaa", + }, + { + label: "MX", + value: "mx", + }, + { + label: "NS", + value: "ns", + }, + { + label: "SOA", + value: "soa", + }, + { + label: "TXT", + value: "txt", + }, + ], + }, + }, + methods: { + getHistoryDns({ + hostname, type, ...args + } = {}) { + return this.app._makeRequest({ + path: `/history/${hostname}/dns/${type}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + getHistoryDns, + hostname, + type, + } = this; + + const response = await getHistoryDns({ + $, + hostname, + type, + }); + $.export("$summary", `Successfully retrieved historical DNS data for type \`${response.type}\`.`); + return response; + }, +}; diff --git a/components/securitytrails/actions/get-ips-neighbors/get-ips-neighbors.mjs b/components/securitytrails/actions/get-ips-neighbors/get-ips-neighbors.mjs new file mode 100644 index 0000000000000..4a256d5a26297 --- /dev/null +++ b/components/securitytrails/actions/get-ips-neighbors/get-ips-neighbors.mjs @@ -0,0 +1,41 @@ +import app from "../../securitytrails.app.mjs"; + +export default { + key: "securitytrails-get-ips-neighbors", + name: "Get IPs Neighbors", + description: "Returns the neighbors in any given IP level range and essentially allows you to explore closeby IP addresses.", + version: "0.0.1", + type: "action", + props: { + app, + ipaddress: { + type: "string", + label: "IP Address", + description: "Enter the IP address to find neighbors for. Eg. `8.8.8.8`.", + }, + }, + methods: { + getIpNeighbors({ + ipaddress, ...args + }) { + return this.app._makeRequest({ + path: `/ips/nearby/${ipaddress}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + getIpNeighbors, + ipaddress, + } = this; + + const response = await getIpNeighbors({ + $, + ipaddress, + }); + + $.export("$summary", `Successfully retrieved \`${response?.blocks.length}\` neighbor(s).`); + return response; + }, +}; diff --git a/components/securitytrails/common/constants.mjs b/components/securitytrails/common/constants.mjs new file mode 100644 index 0000000000000..ee0b63dfedf6b --- /dev/null +++ b/components/securitytrails/common/constants.mjs @@ -0,0 +1,7 @@ +const BASE_URL = "https://api.securitytrails.com"; +const VERSION_PATH = "/v1"; + +export default { + BASE_URL, + VERSION_PATH, +}; diff --git a/components/securitytrails/package.json b/components/securitytrails/package.json new file mode 100644 index 0000000000000..a891515dfa2d0 --- /dev/null +++ b/components/securitytrails/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/securitytrails", + "version": "0.1.0", + "description": "Pipedream SecurityTrails Components", + "main": "securitytrails.app.mjs", + "keywords": [ + "pipedream", + "securitytrails" + ], + "homepage": "https://pipedream.com/apps/securitytrails", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.1" + } +} diff --git a/components/securitytrails/securitytrails.app.mjs b/components/securitytrails/securitytrails.app.mjs new file mode 100644 index 0000000000000..c88964fd8cf19 --- /dev/null +++ b/components/securitytrails/securitytrails.app.mjs @@ -0,0 +1,34 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "securitytrails", + propDefinitions: { + hostname: { + type: "string", + label: "Hostname", + description: "Enter the hostname to query information for. Eg. `oracle.com`", + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers = {}) { + return { + ...headers, + APIKEY: this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + }, +}; diff --git a/components/seen/package.json b/components/seen/package.json new file mode 100644 index 0000000000000..02465fa020364 --- /dev/null +++ b/components/seen/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/seen", + "version": "0.0.2", + "description": "Pipedream SEEN Components", + "main": "seen.app.mjs", + "keywords": [ + "pipedream", + "seen" + ], + "homepage": "https://pipedream.com/apps/seen", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/seen/seen.app.mjs b/components/seen/seen.app.mjs new file mode 100644 index 0000000000000..a624bcda83c01 --- /dev/null +++ b/components/seen/seen.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "seen", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/segment/README.md b/components/segment/README.md index 6f1ad8d88fb53..2f977f78b50a7 100644 --- a/components/segment/README.md +++ b/components/segment/README.md @@ -1,32 +1,11 @@ # Overview -You can do a lot of amazing things with Segment's API. Segment's API enables -you to collect and connect customer data quickly and easily. With it, you can -power your analytics, marketing, and data warehousing requirements, and improve -your customer experience and performance. +The Segment API enables you to collect, manage, and route your customer analytics data to various tools for marketing, analytics, and data warehousing. By harnessing the Segment API on Pipedream, you can automate the ingestion and syncing of user events and properties to multiple destinations, allowing you to create seamless data pipelines without manual intervention. Pipedream's serverless platform empowers you to build custom workflows that react to events in real-time, enrich data, or trigger actions across your tech stack. -The Segment API provides you with full control over your customer data, -including file storage, ETL, data modeling, data enrichment and -transformations, and data queries. With it, you can build powerful solutions -that help you uncover useful customer insights in real-time. Here are a few -examples of what you can do using the Segment API: +# Example Use Cases -- Collect and store customer data from multiple sources: You can collect - customer data from multiple sources such as web traffic, mobile and web apps, - ecommerce stores, CRMs, and marketing automation platforms. -- Build custom data models to gain deeper insights into customer behavior: With - the Segment API, you can build custom models that organize customer data into - meaningful categories to gain deeper consumer insights. -- Automatically enrich customer data with 3rd party APIs: The Segment API - allows you to automatically map, enrich, and clean customer data, creating a - single source of truth for better segmentation and decision making. -- Analyze customer data in real-time for better decision making: With the - Segment API, you can monitor customer data in real-time, uncovering patterns - and creating reports in a few clicks. -- Automate data pipelines to quickly and easily access data: You can quickly - and easily create data pipelines with the Segment API, and get accurate data - delivered on demand. -- Connect customer data to over 250 cloud services: With the Segment API, you - can quickly and easily connect customer data to over 250 cloud services and - systems, such as analytics, marketing automation platforms, CRMs, and data - warehouses. +- **User Signup Attribution**: Track the source of new user signups by capturing signup events via Segment and enriching them with the original referral source. Then, store this information in a Google Sheet for further analysis or use in marketing campaigns. + +- **Personalized Email Campaigns**: When a user performs a specific action tracked by Segment, like completing a purchase, trigger an automated email via SendGrid with personalized recommendations or a thank-you note based on the user’s purchase history. + +- **Real-time Slack Alerts**: Set up real-time notifications to a Slack channel when high-value customers perform key actions in your app, like upgrading their subscription. Use this workflow to alert sales or support teams to engage with the customer proactively. diff --git a/components/segmetrics/README.md b/components/segmetrics/README.md new file mode 100644 index 0000000000000..c4e69c4e65eb0 --- /dev/null +++ b/components/segmetrics/README.md @@ -0,0 +1,11 @@ +# Overview + +The SegMetrics API grants programmatic access to SegMetrics data, allowing users to extract detailed insights about their marketing efforts. With it, you can automate the retrieval of conversion data, contact lists, tags, and more, to better understand your marketing funnel performance. Leveraging Pipedream's processing capabilities, you can create intricate workflows that respond to this data in real-time, running logic or actions contingent on the insights you gain from SegMetrics. + +# Example Use Cases + +- **Sync SegMetrics Contacts to a CRM**: Automate the addition of new contacts from SegMetrics into a CRM like Salesforce or HubSpot. Every time a new contact is created in SegMetrics, it could trigger a Pipedream workflow that checks if the contact already exists in your CRM, and if not, creates a new contact record. + +- **Trigger Email Campaigns Based on Conversion Data**: Use conversion data from SegMetrics to trigger targeted email campaigns in Mailchimp or SendGrid. For example, when a contact reaches a certain stage in your funnel as reported by SegMetrics, Pipedream can trigger a workflow to send a personalized email to the contact to nudge them along to the next stage. + +- **Create Custom Slack Alerts for Key Performance Indicators (KPIs)**: Set up a workflow that monitors your key metrics in SegMetrics, such as lifetime value (LTV) or conversion rates. When critical thresholds are met or exceeded, have Pipedream send an alert to a Slack channel so your team can take immediate action or simply stay informed about marketing performance in real-time. diff --git a/components/selectpdf/README.md b/components/selectpdf/README.md new file mode 100644 index 0000000000000..1d79143d65f80 --- /dev/null +++ b/components/selectpdf/README.md @@ -0,0 +1,11 @@ +# Overview + +The SelectPdf API on Pipedream allows you to tap into the utility of SelectPdf's features directly within automated workflows. Primarily, this API provides the capability to convert HTML to PDF, generate PDFs from URLs, and manipulate existing PDFs. By integrating SelectPdf API with Pipedream, you can automate document generation, customize PDFs on-the-fly, and streamline the distribution of these documents across various platforms. + +# Example Use Cases + +- **HTML to PDF Conversion Automation**: Automatically convert HTML documents to PDF format whenever a new document is added to a cloud storage service like Google Drive or Dropbox. The workflow can be triggered by a new file event, process the HTML content, and use SelectPdf to create a PDF that can be stored back to the cloud or sent via email. + +- **Dynamic Report Generation**: Generate custom PDF reports from template URLs when a specific event occurs, such as the closure of a sales deal in a CRM like Salesforce. The workflow listens for a trigger from the CRM, pulls in dynamic data to populate the report template, converts it via SelectPdf, and then emails the report to stakeholders. + +- **PDF Manipulation after User Actions**: When a user fills out a form in Typeform, trigger a Pipedream workflow to retrieve the submission, use SelectPdf to merge it with a pre-existing PDF document as a new page or section, and then automatically upload the modified PDF to a service like Box or send it for e-signature using DocuSign. diff --git a/components/sellercloud/README.md b/components/sellercloud/README.md new file mode 100644 index 0000000000000..f977363b193af --- /dev/null +++ b/components/sellercloud/README.md @@ -0,0 +1,11 @@ +# Overview + +The Sellercloud API taps into a robust e-commerce platform that streamlines multichannel sales operations, inventory management, and order fulfillment. By integrating Sellercloud with Pipedream, you can automate tasks like syncing inventory levels, managing orders, and updating product listings across various channels. With Pipedream's serverless platform, you can create workflows that react to events in Sellercloud or initiate actions based on triggers from other apps. + +# Example Use Cases + +- **Sync Inventory to Google Sheets**: Every time your inventory levels change in Sellercloud, a Pipedream workflow automatically updates a Google Sheets spreadsheet. This keeps your inventory data accessible and makes it easy to share with team members who prefer working with spreadsheets. + +- **Automate Order Processing**: When a new order is received in Sellercloud, a Pipedream workflow triggers, which can then check the order details, verify inventory, and send the order information to a shipping service like ShipStation. This streamlines the order fulfillment process and reduces the need for manual intervention. + +- **Dynamic Pricing Updates**: Monitor competitor pricing or stock levels using a web scraping tool, and then use Pipedream to adjust your Sellercloud product prices based on this data. This helps maintain competitive pricing automatically and can help improve sales performance. diff --git a/components/sellix/README.md b/components/sellix/README.md index 50df601dc7297..035a8551c53ce 100644 --- a/components/sellix/README.md +++ b/components/sellix/README.md @@ -1,36 +1,11 @@ # Overview -With this powerful platform, you can create your own e-commerce store with -fully customizable features. No technical expertise required. The Sellix API is -designed for business owners and developers alike to build robust stores with -powerful functionality and features. +The Sellix API empowers e-commerce businesses to automate their digital storefronts on the Pipedream platform. With this API, users can manage products, orders, and coupons programmatically, streamlining operations like inventory management, customer engagement, and sales tracking. Pipedream's serverless execution model and vast array of integrations make it an ideal environment to create custom workflows that leverage Sellix's capabilities. -The Sellix API allows you to create a wide range of e-commerce stores with -features such as: +# Example Use Cases -- Hosted checkout pages -- Customizable product pages -- Integrated Payment Systems -- Discounts and coupons -- Customer Profiles -- Automated tax and shipping calculations -- Comprehensive order management tools -- Robust reporting tools -- Mobile optimized experiences +- **Order Processing Automation**: Trigger a workflow on Pipedream when a new order is placed on Sellix. Validate the order details, automatically generate and send digital products or licenses to the customer's email, and log the sale in an external accounting system like QuickBooks for seamless financial tracking. -These are just some examples of what you can build with the Sellix API. With -the ability to further customize your store, you can create a unique, -feature-rich experience for your customers. +- **Customer Support Tickets**: When a customer submits a support request via a platform like Zendesk or Help Scout, use Pipedream to check their order history in Sellix. If a recent purchase is found, prioritize the ticket, and auto-respond with a personalized message acknowledging their recent purchase and promising expedited support. -Here are some examples of stores made using the Sellix API: - -- A dropshipping store -- A subscription-based store -- A B2B store -- A multi-vendor marketplace -- An online merchandise store -- An online course or tutorial store -- A gaming or gaming related store -- An event ticketing store -- A retail store -- A digital goods store +- **Marketing Campaigns with Dynamic Discounts**: Monitor customer activity on Sellix and identify high-value customers. Use Pipedream to trigger an email campaign via SendGrid or Mailchimp, offering these customers exclusive discounts. Automatically create and send unique coupon codes through Sellix, tracking the redemption rate to measure the campaign's success. diff --git a/components/sellsy/README.md b/components/sellsy/README.md new file mode 100644 index 0000000000000..113952644162b --- /dev/null +++ b/components/sellsy/README.md @@ -0,0 +1,11 @@ +# Overview + +The Sellsy API allows you to automate and enhance your customer relationship management and sales processes. Via Pipedream, a serverless integration and automation platform, you can leverage the API to sync data, trigger workflows based on specific actions within Sellsy, and connect Sellsy with a multitude of other services to streamline operations, enhance user engagements, and optimize sales activities. + +# Example Use Cases + +- **Automated Customer Onboarding Emails**: Once a new sale is recorded in Sellsy, use Pipedream to trigger an automated email sequence via SendGrid or Gmail to welcome the customer and provide them with useful information about their purchase. This helps in personalizing customer interaction without manual effort. + +- **Sync New Contacts to CRM**: When a new contact is added in Sellsy, automatically add that contact to a CRM like Salesforce or HubSpot using Pipedream. This ensures all customer data are up-to-date across all platforms, enhancing data consistency and availability for sales teams. + +- **Real-time Inventory Updates**: Set up a workflow in Pipedream that monitors changes in inventory levels within Sellsy. Connect this workflow to a messaging app like Slack to send real-time alerts to relevant teams when stock levels are low. This can help in maintaining optimal inventory levels and improving order fulfillment times. diff --git a/components/sellsy/package.json b/components/sellsy/package.json new file mode 100644 index 0000000000000..eb0216586e949 --- /dev/null +++ b/components/sellsy/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/sellsy", + "version": "0.0.1", + "description": "Pipedream Sellsy Components", + "main": "sellsy.app.mjs", + "keywords": [ + "pipedream", + "sellsy" + ], + "homepage": "https://pipedream.com/apps/sellsy", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/sellsy/sellsy.app.mjs b/components/sellsy/sellsy.app.mjs new file mode 100644 index 0000000000000..e756dda26c597 --- /dev/null +++ b/components/sellsy/sellsy.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "sellsy", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/semaphore/README.md b/components/semaphore/README.md new file mode 100644 index 0000000000000..c1a38a401fb05 --- /dev/null +++ b/components/semaphore/README.md @@ -0,0 +1,11 @@ +# Overview + +The Semaphore API lets you manage and control your CI/CD pipelines programmatically. With Pipedream's serverless platform, you can build workflows that interact with Semaphore to automate tasks such as triggering deployments, fetching the status of pipelines, and more. You can trigger these workflows on a schedule, or in response to events, using Pipedream's event sources. + +# Example Use Cases + +- **Automated Deployment Triggering**: Combine GitHub and Semaphore on Pipedream to create a workflow that triggers a deployment after a successful push to a specific branch. This setup can streamline your deployment process, ensuring that your latest code is always deployed. + +- **Real-Time Notifications**: Integrate Semaphore with Slack using Pipedream to send real-time notifications to your team. When a pipeline fails or succeeds, the workflow can post a message to a designated Slack channel, keeping everyone informed. + +- **Conditional Pipeline Execution**: Use Pipedream to listen for webhooks from your project management tool (e.g., Jira). When a ticket moves to a "Ready for Deployment" status, it can trigger a workflow that checks conditions and initiates the Semaphore deployment pipeline if all criteria are met. diff --git a/components/semaphore/package.json b/components/semaphore/package.json new file mode 100644 index 0000000000000..ae350e0bf575d --- /dev/null +++ b/components/semaphore/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/semaphore", + "version": "0.0.1", + "description": "Pipedream Semaphore Components", + "main": "semaphore.app.mjs", + "keywords": [ + "pipedream", + "semaphore" + ], + "homepage": "https://pipedream.com/apps/semaphore", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/semaphore/semaphore.app.mjs b/components/semaphore/semaphore.app.mjs new file mode 100644 index 0000000000000..2d4327e0de075 --- /dev/null +++ b/components/semaphore/semaphore.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "semaphore", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/semgrep/README.md b/components/semgrep/README.md new file mode 100644 index 0000000000000..a79ff27ba3c4b --- /dev/null +++ b/components/semgrep/README.md @@ -0,0 +1,11 @@ +# Overview + +The Semgrep API enables developers to integrate powerful static code analysis within their workflows on Pipedream. Semgrep scans codebases for bugs, security issues, and code standards, making it a vital tool for maintaining code quality. On Pipedream, you can automate code reviews, enforce coding standards, and trigger alerts or actions based on scan results. By connecting Semgrep to Pipedream, you can streamline your CI/CD pipelines, notify teams of critical issues, and even auto-fix problems under certain conditions. + +# Example Use Cases + +- **Automated Code Review Notifications**: Trigger a Semgrep scan whenever code is pushed to a GitHub repository. If issues are detected, format the results and send them as a pull request comment or to a Slack channel, notifying developers of potential problems immediately. + +- **Enforce Coding Standards on Merge Requests**: Before merging code into the main branch, use Semgrep to ensure it meets your organization's coding standards. If the code fails the check, block the merge request and create an issue in Jira or another project management tool for remediation. + +- **Scheduled Codebase Audits with Reporting**: Schedule regular Semgrep scans of your entire codebase and collate the findings into a report. Send this report to an email list, or log it in a tool like Confluence for team review, ensuring ongoing code hygiene and security compliance. diff --git a/components/semrush/README.md b/components/semrush/README.md index 3e0fcaf93e704..49450b2174391 100644 --- a/components/semrush/README.md +++ b/components/semrush/README.md @@ -1,18 +1,11 @@ # Overview -Using the SEMrush API, you can build powerful digital marketing solutions to -help monitor, track and improve your website's visibility in search engine -rankings. The API provides access to valuable marketing data including domain -rankings, positions in SERPs, organic search engine traffic, backlink -information and more. With this data, you can develop custom tools for data -analysis, reporting, and optimization. +The SEMrush API offers a suite of tools for SEO, content marketing, competitor research, PPC and social media marketing analysis. With Pipedream's capabilities, you can automate data extraction for SEO audits, track keyword rankings, and glean insights from your competitors' online strategies. By utilizing the SEMrush API on Pipedream, you can create workflows that trigger actions in other apps, generate reports, and streamline your marketing efforts. -Here are some examples of what you can build with the SEMrush API: +# Example Use Cases -- Analyze website ranking performance over time -- Identify profitable keywords and optimize content -- Get insights into competitors' keyword strategies -- Monitor backlink performance -- Generate reports for website audits -- Track visibility of content across multiple websites -- Automate marketing tasks such as monitoring, reporting and optimization +- **SEO Performance Reporting**: Automatically generate SEO reports by fetching keyword analytics, site audit results, and backlink analytics from SEMrush. Pipe this data into Google Sheets for a live dashboard or schedule weekly email summaries using the Gmail app. + +- **Content Strategy Optimization**: Build a workflow that retrieves the top-performing pages and keyword trends for your domain and competitors'. Use this data to inform your content strategy by identifying gaps and opportunities, then push this data to Trello or Asana to manage your content calendar. + +- **Ad Spend Efficiency**: Monitor your PPC campaigns by creating a pipeline that checks your SEMrush ad spend and performance data. Integrate with Slack or Microsoft Teams to send alerts when ad spend exceeds your thresholds or when there are significant changes in campaign performances. diff --git a/components/semrush/package.json b/components/semrush/package.json new file mode 100644 index 0000000000000..687e91c2e93e8 --- /dev/null +++ b/components/semrush/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/semrush", + "version": "0.6.0", + "description": "Pipedream semrush Components", + "main": "semrush.app.mjs", + "keywords": [ + "pipedream", + "semrush" + ], + "homepage": "https://pipedream.com/apps/semrush", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/sendbird/README.md b/components/sendbird/README.md index 64c02868edd85..08883d515acb1 100644 --- a/components/sendbird/README.md +++ b/components/sendbird/README.md @@ -1,20 +1,11 @@ # Overview -Building real-time communication solutions is now easier than ever with -Sendbird's communication API. With the Sendbird API, you can easily customize -messaging & voice/video communication within your app with full flexibility & -control. +The Sendbird API provides programmatic access to advanced chat features, enabling the creation and management of in-app messaging for community forums, customer support, or any other chat-based interaction. By leveraging the Sendbird API on Pipedream, you can automate user management, message and channel handling, and event tracking. Pipedream's serverless platform simplifies these automations, offering a way to integrate chat functionalities with other services, trigger workflows from chat events, and handle real-time data processing without writing extensive infrastructure code. -We provide clients an intuitive messaging platform with a set of easy and -powerful APIs so they can quickly and easily build inlayed communication -experiences into any app or service. +# Example Use Cases -With Sendbird, you can build: +- **Sync Chat Users with CRM**: Automatically sync new user registrations from Sendbird to a Customer Relationship Management (CRM) system like Salesforce. When a user signs up in Sendbird, a workflow triggers that adds or updates the user's information in Salesforce, ensuring customer records are always up-to-date. -- Chat and Group Messaging -- Voice and Video Calling -- Push Notifications -- Contact and User Management -- Presence and Typing Indicators -- Contextual Carousels for Rich Media -- Customizable UI and Live Event/Activity Streams +- **Moderate Chat Content**: Implement real-time chat moderation by connecting Sendbird with a content moderation service like WebPurify. Each message sent within Sendbird can trigger a Pipedream workflow, which then passes the message to WebPurify for review. If the message is flagged, take action such as notifying moderators, deleting the message, or suspending the user. + +- **Aggregate Chat Metrics**: Collect and visualize chat analytics by sending conversation data from Sendbird to a dashboard tool like Google Sheets or Data Studio. Use Pipedream workflows to capture message counts, user engagement metrics, or support response times, then push that data into the dashboard app for real-time reporting and insights. diff --git a/components/sendbird_ai_chabot/actions/send-bot-message/send-bot-message.mjs b/components/sendbird_ai_chabot/actions/send-bot-message/send-bot-message.mjs new file mode 100644 index 0000000000000..86dbb1a53faa2 --- /dev/null +++ b/components/sendbird_ai_chabot/actions/send-bot-message/send-bot-message.mjs @@ -0,0 +1,43 @@ +import sendbird from "../../sendbird_ai_chabot.app.mjs"; + +export default { + key: "sendbird_ai_chabot-send-bot-message", + name: "Send Bot Message", + description: "Sends a bot message to a group channel. [See the documentation](https://sendbird.com/docs/chat/platform-api/v3/bot/sending-a-bot-message/send-a-bot-message)", + version: "0.0.1", + type: "action", + props: { + sendbird, + botId: { + propDefinition: [ + sendbird, + "botId", + ], + }, + channelUrl: { + propDefinition: [ + sendbird, + "channelUrl", + ], + }, + message: { + type: "string", + label: "Message", + description: "Specifies the content of the message sent by the bot", + }, + }, + async run({ $ }) { + const response = await this.sendbird.sendBotMessage({ + $, + botId: this.botId, + data: { + channel_url: this.channelUrl, + message: this.message, + }, + }); + if (response?.message?.message_id) { + $.export("$summary", `Successfully sent message with ID: ${response.message.message_id}`); + } + return response; + }, +}; diff --git a/components/sendbird_ai_chabot/package.json b/components/sendbird_ai_chabot/package.json new file mode 100644 index 0000000000000..956c26913d6b6 --- /dev/null +++ b/components/sendbird_ai_chabot/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/sendbird_ai_chabot", + "version": "0.1.0", + "description": "Pipedream Sendbird AI chabot Components", + "main": "sendbird_ai_chabot.app.mjs", + "keywords": [ + "pipedream", + "sendbird_ai_chabot" + ], + "homepage": "https://pipedream.com/apps/sendbird_ai_chabot", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} diff --git a/components/sendbird_ai_chabot/sendbird_ai_chabot.app.mjs b/components/sendbird_ai_chabot/sendbird_ai_chabot.app.mjs new file mode 100644 index 0000000000000..3ae9f5c708e79 --- /dev/null +++ b/components/sendbird_ai_chabot/sendbird_ai_chabot.app.mjs @@ -0,0 +1,94 @@ +import { axios } from "@pipedream/platform"; +const DEFAULT_LIMIT = 20; + +export default { + type: "app", + app: "sendbird_ai_chabot", + propDefinitions: { + botId: { + type: "string", + label: "Bot ID", + description: "The identifier of a bot", + async options({ page }) { + const { bots } = await this.listBots({ + params: { + limit: DEFAULT_LIMIT, + offset: page * DEFAULT_LIMIT, + }, + }); + return bots?.map(({ bot }) => ({ + label: bot.bot_nickname, + value: bot.bot_userid, + })) || []; + }, + }, + channelUrl: { + type: "string", + label: "Channel URL", + description: "Specifies the URL of the channel", + async options({ page }) { + const { channels } = await this.listGroupChannels({ + params: { + limit: DEFAULT_LIMIT, + offset: page * DEFAULT_LIMIT, + }, + }); + return channels?.map(({ + name: label, channel_url: value, + }) => ({ + label, + value, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return `https://api-${this.$auth.application_id}.sendbird.com/v3`; + }, + _headers() { + return { + "Api-Token": `${this.$auth.api_token}`, + }; + }, + _makeRequest({ + $ = this, + path, + ...args + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._headers(), + ...args, + }); + }, + updateWebhook(opts = {}) { + return this._makeRequest({ + method: "PUT", + path: "/applications/settings/webhook", + ...opts, + }); + }, + listBots(opts = {}) { + return this._makeRequest({ + path: "/bots", + ...opts, + }); + }, + listGroupChannels(opts = {}) { + return this._makeRequest({ + path: "/group_channels", + ...opts, + }); + }, + sendBotMessage({ + botId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/bots/${botId}/send`, + ...opts, + }); + }, + }, +}; diff --git a/components/sendbird_ai_chabot/sources/common/events.mjs b/components/sendbird_ai_chabot/sources/common/events.mjs new file mode 100644 index 0000000000000..5bf791b6c53e2 --- /dev/null +++ b/components/sendbird_ai_chabot/sources/common/events.mjs @@ -0,0 +1,14 @@ +export default [ + { + value: "form:submit", + label: "form submitted", + }, + { + value: "group_channel:bot_message_send", + label: "bot message sent within group channel", + }, + { + value: "group_channel:message_send", + label: "message sent within group channel", + }, +]; diff --git a/components/sendbird_ai_chabot/sources/new-event-received/new-event-received.mjs b/components/sendbird_ai_chabot/sources/new-event-received/new-event-received.mjs new file mode 100644 index 0000000000000..cbef1b3ae7c23 --- /dev/null +++ b/components/sendbird_ai_chabot/sources/new-event-received/new-event-received.mjs @@ -0,0 +1,65 @@ +import sendbird from "../../sendbird_ai_chabot.app.mjs"; +import events from "../common/events.mjs"; + +export default { + key: "sendbird_ai_chabot-new-event-received", + name: "New Event Received (Instant)", + description: "Emit new event when a new webhook event is received.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + sendbird, + http: { + type: "$.interface.http", + customResponse: true, + }, + events: { + type: "string[]", + label: "Events", + description: "The events to subscribe to", + options: events, + }, + }, + hooks: { + async activate() { + await this.sendbird.updateWebhook({ + data: { + enabled: true, + url: this.http.endpoint, + enabled_events: this.events, + }, + }); + }, + async deactivate() { + await this.sendbird.updateWebhook({ + data: { + enabled: false, + url: this.http.endpoint, + enabled_events: [], + }, + }); + }, + }, + methods: { + generateMeta(body) { + const ts = Date.now(); + return { + id: ts, + summary: `New ${body.category} event`, + ts, + }; + }, + }, + async run(event) { + const { body } = event; + if (!body) { + return; + } + this.http.respond({ + status: 200, + }); + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, +}; diff --git a/components/sendblue/README.md b/components/sendblue/README.md index ba4847b764f01..12b4a13bd5daa 100644 --- a/components/sendblue/README.md +++ b/components/sendblue/README.md @@ -1,25 +1,11 @@ # Overview -The Sendblue API is a comprehensive platform for building messaging and email -customer service applications. With the Sendblue API, developers can easily add -communication features to their applications and services. +Sendblue API offers a powerful gateway to automate communication via iMessage, enabling businesses to send and receive messages programmatically. By leveraging Sendblue on Pipedream, you can create intricate workflows that respond to events in real-time, tailor customer interactions, and streamline notifications. It provides a unique opportunity to enhance customer engagement through one of the most widely used messaging platforms. -Sendblue’s APIs provide a wide range of features, including: real-time customer -messaging, automated customer service responses, custom email triggers, -transactional email delivery, task automation, contact management, data -tracking and analytics, and much more. With the Sendblue API, developers can -easily create powerful customer messaging applications and integrate customer -service features into their existing applications. +# Example Use Cases -Here is a list of examples of what can be built using the Sendblue API: +- **Customer Support Automation**: Create a workflow that listens for customer queries via iMessage and automatically routes them to the appropriate department or support agent on your platform. Utilize Sendblue with a CRM app like Salesforce to log conversations and maintain customer records seamlessly. -- Automated customer service chatbot -- Real-time customer communication and engagement -- Automated task reminders -- Automated customer onboarding flows -- Marketing automation triggers -- Custom email delivery -- Contact management to store customer data -- Transactional email sending -- Analytics and reporting dashboards -- And many more. +- **Order Confirmation and Updates**: Set up an automated system that sends order confirmations, shipping updates, and delivery notifications directly through iMessage. Pair Sendblue with an e-commerce platform like Shopify to trigger these communications based on order status changes. + +- **Appointment Reminders**: Build a workflow to automatically send appointment reminders and rescheduling options to clients. Connect Sendblue to a calendar service like Google Calendar to trigger reminder messages a day before the scheduled appointment, reducing no-shows. diff --git a/components/sendcloud/README.md b/components/sendcloud/README.md new file mode 100644 index 0000000000000..26431d9fa93b0 --- /dev/null +++ b/components/sendcloud/README.md @@ -0,0 +1,11 @@ +# Overview + +The Sendcloud API offers a suite of tools for streamlining shipping processes for e-commerce businesses. Using the API, you can create and manage shipments, print shipping labels, track packages, and handle returns with ease. When integrated with Pipedream, the Sendcloud API enables you to automate your shipping workflow, connect with other apps and services, and create custom notifications and actions based on shipping events. + +# Example Use Cases + +- **Order Fulfillment Automation**: When a new order is received in your e-commerce platform, such as Shopify, you can use Pipedream to automatically create a shipment in Sendcloud, print the label, and update the order status back in Shopify. + +- **Shipping Notifications**: Leverage Pipedream to monitor the status of shipments in Sendcloud and send real-time notifications to customers via email or SMS through SendGrid or Twilio when their package is shipped, out for delivery, or delivered. + +- **Return Handling**: Set up a Pipedream workflow to detect when a return is initiated in Sendcloud. Automatically notify your customer support team in Slack or Zendesk and update inventory levels in your database or ERP system. diff --git a/components/sendcloud/actions/create-a-parcel/create-a-parcel.mjs b/components/sendcloud/actions/create-a-parcel/create-a-parcel.mjs new file mode 100644 index 0000000000000..23e19acd80856 --- /dev/null +++ b/components/sendcloud/actions/create-a-parcel/create-a-parcel.mjs @@ -0,0 +1,67 @@ +import app from "../../sendcloud.app.mjs"; + +export default { + key: "sendcloud-create-a-parcel", + name: "Create a Parcel", + description: "Creates a new parcel under your Sendcloud API credentials. [See the documentation](https://api.sendcloud.dev/docs/sendcloud-public-api/parcels/operations/create-a-parcel)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "name", + ], + }, + address: { + propDefinition: [ + app, + "address", + ], + }, + city: { + propDefinition: [ + app, + "city", + ], + }, + houseNumber: { + propDefinition: [ + app, + "houseNumber", + ], + }, + postalCode: { + propDefinition: [ + app, + "postalCode", + ], + }, + country: { + propDefinition: [ + app, + "country", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createParcel({ + $, + data: { + parcel: { + name: this.name, + address: this.address, + city: this.city, + house_number: this.houseNumber, + postal_code: this.postalCode, + country: this.country, + }, + }, + }); + + $.export("$summary", `Successfully created parcel with ID: ${response.parcel.id}`); + + return response; + }, +}; diff --git a/components/sendcloud/actions/list-parcels/list-parcels.mjs b/components/sendcloud/actions/list-parcels/list-parcels.mjs new file mode 100644 index 0000000000000..ab7cbc4754325 --- /dev/null +++ b/components/sendcloud/actions/list-parcels/list-parcels.mjs @@ -0,0 +1,21 @@ +import app from "../../sendcloud.app.mjs"; + +export default { + key: "sendcloud-list-parcels", + name: "List Parcels", + description: "Retrieves a list of all the parcels under your API credentials. [See the documentation](https://api.sendcloud.dev/docs/sendcloud-public-api/parcels/operations/list-parcels)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.listParcels({ + $, + }); + + $.export("$summary", "Successfully retrieved the list of parcels"); + + return response; + }, +}; diff --git a/components/sendcloud/actions/update-a-parcel/update-a-parcel.mjs b/components/sendcloud/actions/update-a-parcel/update-a-parcel.mjs new file mode 100644 index 0000000000000..d29edb84efbda --- /dev/null +++ b/components/sendcloud/actions/update-a-parcel/update-a-parcel.mjs @@ -0,0 +1,80 @@ +import app from "../../sendcloud.app.mjs"; + +export default { + key: "sendcloud-update-a-parcel", + name: "Update a Parcel", + description: "Updates a parcel under your API credentials. [See the documentation](https://api.sendcloud.dev/docs/sendcloud-public-api/parcels/operations/update-a-parcel)", + version: "0.0.1", + type: "action", + props: { + app, + parcelId: { + propDefinition: [ + app, + "parcelId", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + optional: true, + }, + address: { + propDefinition: [ + app, + "address", + ], + optional: true, + }, + city: { + propDefinition: [ + app, + "city", + ], + optional: true, + }, + houseNumber: { + propDefinition: [ + app, + "houseNumber", + ], + optional: true, + }, + postalCode: { + propDefinition: [ + app, + "postalCode", + ], + optional: true, + }, + country: { + propDefinition: [ + app, + "country", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.app.updateParcel({ + $, + id: this.parcelId, + data: { + parcel: { + name: this.name, + address: this.address, + city: this.city, + house_number: this.houseNumber, + postal_code: this.postalCode, + country: this.country, + }, + }, + }); + + $.export("$summary", `Successfully updated parcel with ID ${this.parcelId}`); + + return response; + }, +}; diff --git a/components/sendcloud/common/constants.mjs b/components/sendcloud/common/constants.mjs new file mode 100644 index 0000000000000..85c6ea7ac20ac --- /dev/null +++ b/components/sendcloud/common/constants.mjs @@ -0,0 +1,1000 @@ +export default { + COUNTRIES: [ + { + label: "Andorra", + value: "AD", + }, + { + label: "United Arab Emirates", + value: "AE", + }, + { + label: "Afghanistan", + value: "AF", + }, + { + label: "Antigua and Barbuda", + value: "AG", + }, + { + label: "Anguilla", + value: "AI", + }, + { + label: "Albania", + value: "AL", + }, + { + label: "Armenia", + value: "AM", + }, + { + label: "Angola", + value: "AO", + }, + { + label: "Antarctica", + value: "AQ", + }, + { + label: "Argentina", + value: "AR", + }, + { + label: "American Samoa", + value: "AS", + }, + { + label: "Austria", + value: "AT", + }, + { + label: "Australia", + value: "AU", + }, + { + label: "Aruba", + value: "AW", + }, + { + label: "Åland Islands", + value: "AX", + }, + { + label: "Azerbaijan", + value: "AZ", + }, + { + label: "Bosnia and Herzegovina", + value: "BA", + }, + { + label: "Barbados", + value: "BB", + }, + { + label: "Bangladesh", + value: "BD", + }, + { + label: "Belgium", + value: "BE", + }, + { + label: "Burkina Faso", + value: "BF", + }, + { + label: "Bulgaria", + value: "BG", + }, + { + label: "Bahrain", + value: "BH", + }, + { + label: "Burundi", + value: "BI", + }, + { + label: "Benin", + value: "BJ", + }, + { + label: "Saint Barthélemy", + value: "BL", + }, + { + label: "Bermuda", + value: "BM", + }, + { + label: "Brunei Darussalam", + value: "BN", + }, + { + label: "Bolivia, Plurinational State of", + value: "BO", + }, + { + label: "Bonaire, Sint Eustatius and Saba", + value: "BQ", + }, + { + label: "Brazil", + value: "BR", + }, + { + label: "Bahamas", + value: "BS", + }, + { + label: "Bhutan", + value: "BT", + }, + { + label: "Bouvet Island", + value: "BV", + }, + { + label: "Botswana", + value: "BW", + }, + { + label: "Belarus", + value: "BY", + }, + { + label: "Belize", + value: "BZ", + }, + { + label: "Canada", + value: "CA", + }, + { + label: "Cocos (Keeling) Islands", + value: "CC", + }, + { + label: "Congo, Democratic Republic of the", + value: "CD", + }, + { + label: "Central African Republic", + value: "CF", + }, + { + label: "Congo", + value: "CG", + }, + { + label: "Switzerland", + value: "CH", + }, + { + label: "Côte d'Ivoire", + value: "CI", + }, + { + label: "Cook Islands", + value: "CK", + }, + { + label: "Chile", + value: "CL", + }, + { + label: "Cameroon", + value: "CM", + }, + { + label: "China", + value: "CN", + }, + { + label: "Colombia", + value: "CO", + }, + { + label: "Costa Rica", + value: "CR", + }, + { + label: "Cuba", + value: "CU", + }, + { + label: "Cabo Verde", + value: "CV", + }, + { + label: "Curaçao", + value: "CW", + }, + { + label: "Christmas Island", + value: "CX", + }, + { + label: "Cyprus", + value: "CY", + }, + { + label: "Czechia", + value: "CZ", + }, + { + label: "Germany", + value: "DE", + }, + { + label: "Djibouti", + value: "DJ", + }, + { + label: "Denmark", + value: "DK", + }, + { + label: "Dominica", + value: "DM", + }, + { + label: "Dominican Republic", + value: "DO", + }, + { + label: "Algeria", + value: "DZ", + }, + { + label: "Ecuador", + value: "EC", + }, + { + label: "Estonia", + value: "EE", + }, + { + label: "Egypt", + value: "EG", + }, + { + label: "Western Sahara", + value: "EH", + }, + { + label: "Eritrea", + value: "ER", + }, + { + label: "Spain", + value: "ES", + }, + { + label: "Ethiopia", + value: "ET", + }, + { + label: "Finland", + value: "FI", + }, + { + label: "Fiji", + value: "FJ", + }, + { + label: "Falkland Islands (Malvinas)", + value: "FK", + }, + { + label: "Micronesia, Federated States of", + value: "FM", + }, + { + label: "Faroe Islands", + value: "FO", + }, + { + label: "France", + value: "FR", + }, + { + label: "Gabon", + value: "GA", + }, + { + label: "United Kingdom of Great Britain and Northern Ireland", + value: "GB", + }, + { + label: "Grenada", + value: "GD", + }, + { + label: "Georgia", + value: "GE", + }, + { + label: "French Guiana", + value: "GF", + }, + { + label: "Guernsey", + value: "GG", + }, + { + label: "Ghana", + value: "GH", + }, + { + label: "Gibraltar", + value: "GI", + }, + { + label: "Greenland", + value: "GL", + }, + { + label: "Gambia", + value: "GM", + }, + { + label: "Guinea", + value: "GN", + }, + { + label: "Guadeloupe", + value: "GP", + }, + { + label: "Equatorial Guinea", + value: "GQ", + }, + { + label: "Greece", + value: "GR", + }, + { + label: "South Georgia and the South Sandwich Islands", + value: "GS", + }, + { + label: "Guatemala", + value: "GT", + }, + { + label: "Guam", + value: "GU", + }, + { + label: "Guinea-Bissau", + value: "GW", + }, + { + label: "Guyana", + value: "GY", + }, + { + label: "Hong Kong", + value: "HK", + }, + { + label: "Heard Island and McDonald Islands", + value: "HM", + }, + { + label: "Honduras", + value: "HN", + }, + { + label: "Croatia", + value: "HR", + }, + { + label: "Haiti", + value: "HT", + }, + { + label: "Hungary", + value: "HU", + }, + { + label: "Indonesia", + value: "ID", + }, + { + label: "Ireland", + value: "IE", + }, + { + label: "Israel", + value: "IL", + }, + { + label: "Isle of Man", + value: "IM", + }, + { + label: "India", + value: "IN", + }, + { + label: "British Indian Ocean Territory", + value: "IO", + }, + { + label: "Iraq", + value: "IQ", + }, + { + label: "Iran, Islamic Republic of", + value: "IR", + }, + { + label: "Iceland", + value: "IS", + }, + { + label: "Italy", + value: "IT", + }, + { + label: "Jersey", + value: "JE", + }, + { + label: "Jamaica", + value: "JM", + }, + { + label: "Jordan", + value: "JO", + }, + { + label: "Japan", + value: "JP", + }, + { + label: "Kenya", + value: "KE", + }, + { + label: "Kyrgyzstan", + value: "KG", + }, + { + label: "Cambodia", + value: "KH", + }, + { + label: "Kiribati", + value: "KI", + }, + { + label: "Comoros", + value: "KM", + }, + { + label: "Saint Kitts and Nevis", + value: "KN", + }, + { + label: "Korea, Democratic People's Republic of", + value: "KP", + }, + { + label: "Korea, Republic of", + value: "KR", + }, + { + label: "Kuwait", + value: "KW", + }, + { + label: "Cayman Islands", + value: "KY", + }, + { + label: "Kazakhstan", + value: "KZ", + }, + { + label: "Lao People's Democratic Republic", + value: "LA", + }, + { + label: "Lebanon", + value: "LB", + }, + { + label: "Saint Lucia", + value: "LC", + }, + { + label: "Liechtenstein", + value: "LI", + }, + { + label: "Sri Lanka", + value: "LK", + }, + { + label: "Liberia", + value: "LR", + }, + { + label: "Lesotho", + value: "LS", + }, + { + label: "Lithuania", + value: "LT", + }, + { + label: "Luxembourg", + value: "LU", + }, + { + label: "Latvia", + value: "LV", + }, + { + label: "Libya", + value: "LY", + }, + { + label: "Morocco", + value: "MA", + }, + { + label: "Monaco", + value: "MC", + }, + { + label: "Moldova, Republic of", + value: "MD", + }, + { + label: "Montenegro", + value: "ME", + }, + { + label: "Saint Martin (French part)", + value: "MF", + }, + { + label: "Madagascar", + value: "MG", + }, + { + label: "Marshall Islands", + value: "MH", + }, + { + label: "North Macedonia", + value: "MK", + }, + { + label: "Mali", + value: "ML", + }, + { + label: "Myanmar", + value: "MM", + }, + { + label: "Mongolia", + value: "MN", + }, + { + label: "Macao", + value: "MO", + }, + { + label: "Northern Mariana Islands", + value: "MP", + }, + { + label: "Martinique", + value: "MQ", + }, + { + label: "Mauritania", + value: "MR", + }, + { + label: "Montserrat", + value: "MS", + }, + { + label: "Malta", + value: "MT", + }, + { + label: "Mauritius", + value: "MU", + }, + { + label: "Maldives", + value: "MV", + }, + { + label: "Malawi", + value: "MW", + }, + { + label: "Mexico", + value: "MX", + }, + { + label: "Malaysia", + value: "MY", + }, + { + label: "Mozambique", + value: "MZ", + }, + { + label: "Namibia", + value: "NA", + }, + { + label: "New Caledonia", + value: "NC", + }, + { + label: "Niger", + value: "NE", + }, + { + label: "Norfolk Island", + value: "NF", + }, + { + label: "Nigeria", + value: "NG", + }, + { + label: "Nicaragua", + value: "NI", + }, + { + label: "Netherlands, Kingdom of the", + value: "NL", + }, + { + label: "Norway", + value: "NO", + }, + { + label: "Nepal", + value: "NP", + }, + { + label: "Nauru", + value: "NR", + }, + { + label: "Niue", + value: "NU", + }, + { + label: "New Zealand", + value: "NZ", + }, + { + label: "Oman", + value: "OM", + }, + { + label: "Panama", + value: "PA", + }, + { + label: "Peru", + value: "PE", + }, + { + label: "French Polynesia", + value: "PF", + }, + { + label: "Papua New Guinea", + value: "PG", + }, + { + label: "Philippines", + value: "PH", + }, + { + label: "Pakistan", + value: "PK", + }, + { + label: "Poland", + value: "PL", + }, + { + label: "Saint Pierre and Miquelon", + value: "PM", + }, + { + label: "Pitcairn", + value: "PN", + }, + { + label: "Puerto Rico", + value: "PR", + }, + { + label: "Palestine, State of", + value: "PS", + }, + { + label: "Portugal", + value: "PT", + }, + { + label: "Palau", + value: "PW", + }, + { + label: "Paraguay", + value: "PY", + }, + { + label: "Qatar", + value: "QA", + }, + { + label: "Réunion", + value: "RE", + }, + { + label: "Romania", + value: "RO", + }, + { + label: "Serbia", + value: "RS", + }, + { + label: "Russian Federation", + value: "RU", + }, + { + label: "Rwanda", + value: "RW", + }, + { + label: "Saudi Arabia", + value: "SA", + }, + { + label: "Solomon Islands", + value: "SB", + }, + { + label: "Seychelles", + value: "SC", + }, + { + label: "Sudan", + value: "SD", + }, + { + label: "Sweden", + value: "SE", + }, + { + label: "Singapore", + value: "SG", + }, + { + label: "Saint Helena, Ascension and Tristan da Cunha", + value: "SH", + }, + { + label: "Slovenia", + value: "SI", + }, + { + label: "Svalbard and Jan Mayen", + value: "SJ", + }, + { + label: "Slovakia", + value: "SK", + }, + { + label: "Sierra Leone", + value: "SL", + }, + { + label: "San Marino", + value: "SM", + }, + { + label: "Senegal", + value: "SN", + }, + { + label: "Somalia", + value: "SO", + }, + { + label: "Suriname", + value: "SR", + }, + { + label: "South Sudan", + value: "SS", + }, + { + label: "Sao Tome and Principe", + value: "ST", + }, + { + label: "El Salvador", + value: "SV", + }, + { + label: "Sint Maarten (Dutch part)", + value: "SX", + }, + { + label: "Syrian Arab Republic", + value: "SY", + }, + { + label: "Eswatini", + value: "SZ", + }, + { + label: "Turks and Caicos Islands", + value: "TC", + }, + { + label: "Chad", + value: "TD", + }, + { + label: "French Southern Territories", + value: "TF", + }, + { + label: "Togo", + value: "TG", + }, + { + label: "Thailand", + value: "TH", + }, + { + label: "Tajikistan", + value: "TJ", + }, + { + label: "Tokelau", + value: "TK", + }, + { + label: "Timor-Leste", + value: "TL", + }, + { + label: "Turkmenistan", + value: "TM", + }, + { + label: "Tunisia", + value: "TN", + }, + { + label: "Tonga", + value: "TO", + }, + { + label: "Turkey", + value: "TR", + }, + { + label: "Trinidad and Tobago", + value: "TT", + }, + { + label: "Tuvalu", + value: "TV", + }, + { + label: "Taiwan, Province of China", + value: "TW", + }, + { + label: "Tanzania, United Republic of", + value: "TZ", + }, + { + label: "Ukraine", + value: "UA", + }, + { + label: "Uganda", + value: "UG", + }, + { + label: "United States Minor Outlying Islands", + value: "UM", + }, + { + label: "United States of America", + value: "US", + }, + { + label: "Uruguay", + value: "UY", + }, + { + label: "Uzbekistan", + value: "UZ", + }, + { + label: "Holy See", + value: "VA", + }, + { + label: "Saint Vincent and the Grenadines", + value: "VC", + }, + { + label: "Venezuela, Bolivarian Republic of", + value: "VE", + }, + { + label: "Virgin Islands, British", + value: "VG", + }, + { + label: "Virgin Islands, U.S.", + value: "VI", + }, + { + label: "Viet Nam", + value: "VN", + }, + { + label: "Vanuatu", + value: "VU", + }, + { + label: "Wallis and Futuna", + value: "WF", + }, + { + label: "Samoa", + value: "WS", + }, + { + label: "Yemen", + value: "YE", + }, + { + label: "Mayotte", + value: "YT", + }, + { + label: "South Africa", + value: "ZA", + }, + { + label: "Zambia", + value: "ZM", + }, + { + label: "Zimbabwe", + value: "ZW", + }, + ], +}; diff --git a/components/sendcloud/package.json b/components/sendcloud/package.json index 2cd52cf771c50..7d47208589981 100644 --- a/components/sendcloud/package.json +++ b/components/sendcloud/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/sendcloud", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Sendcloud Components", "main": "sendcloud.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" } -} \ No newline at end of file +} diff --git a/components/sendcloud/sendcloud.app.mjs b/components/sendcloud/sendcloud.app.mjs index 934fa3120028b..d8266d3da695e 100644 --- a/components/sendcloud/sendcloud.app.mjs +++ b/components/sendcloud/sendcloud.app.mjs @@ -1,11 +1,112 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "sendcloud", - propDefinitions: {}, + propDefinitions: { + parcelId: { + type: "string", + label: "Parcel ID", + description: "The unique identifier of the parcel", + async options({ prevContext }) { + const params = {}; + + if (prevContext?.cursor) params.cursor = prevContext.cursor; + + const { + parcels: resources, next, + } = await this.listParcels({ + params, + }); + + return { + context: { + cursor: next, + }, + options: resources.map(({ + id, name, + }) => ({ + value: id, + label: name, + })), + }; + }, + }, + name: { + type: "string", + label: "Name", + description: "Name of the recipient", + }, + address: { + type: "string", + label: "Address", + description: "Address of the recipient", + }, + city: { + type: "string", + label: "City", + description: "City of the recipient", + }, + houseNumber: { + type: "string", + label: "House Number", + description: "House number of the recipient", + }, + postalCode: { + type: "string", + label: "Postal Code", + description: "Zip code of the recipient", + }, + country: { + type: "string", + label: "country", + description: "Country of the recipient", + options: constants.COUNTRIES, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://panel.sendcloud.sc/api/v2"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + auth, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + auth: { + ...auth, + username: `${this.$auth.public_key}`, + password: `${this.$auth.secret_key}`, + }, + }); + }, + async createParcel(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/parcels", + ...args, + }); + }, + async updateParcel({ + id, ...args + }) { + return this._makeRequest({ + method: "PUT", + path: `/parcels/${id}`, + ...args, + }); + }, + async listParcels(args = {}) { + return this._makeRequest({ + path: "/parcels", + ...args, + }); }, }, }; diff --git a/components/sender/README.md b/components/sender/README.md index 407e844051bb7..ed45df1ae1094 100644 --- a/components/sender/README.md +++ b/components/sender/README.md @@ -1,24 +1,11 @@ # Overview -The Sender API is a modern, reliable platform offering push notifications, -emails, webhooks and more. With powerful tools and features, you can build all -types of notification-based applications quickly and easily. +The Sender API, available on Pipedream, offers powerful capabilities to automate communication and marketing workflows. By leveraging this API, you can streamline how you manage emails, newsletters, and subscriber lists directly from your app environment. This can include sending personalized email campaigns, managing contacts, analyzing campaign performance, and even triggering transactional emails based on user actions. -Here are just a few examples of things you can build with the Sender API: +# Example Use Cases -- Newsletters: Create automated newsletters that are triggered when certain - conditions are met. -- Announcement systems: Keep your users up-to-date with time-sensitive - notifications. -- User engagement applications: Hook into the Sender API to send messages and - create an engagement loop. -- Web Push Notifications: Deliver tailored messages directly to the user’s web - browser. -- Tracking notifications: Get detailed data about each message sent and track - their conversion rate in real-time. -- Auto-follow-up sequences: Automatically schedule follow-up emails to ensure - your users are always engaged. +- **Automated Welcome Email Sequence**: Automatically trigger a sequence of welcome emails through Sender when a new user signs up on your platform. You can personalize the content based on user data from your sign-up form, ensuring a warm, customized onboarding process. -With the Sender API, you can create almost any type of notification-based -application with ease. Get started today and start leveraging the power of -notifications for your business! +- **Subscriber List Management**: Sync your customer data from various platforms, like Shopify or WooCommerce, with Sender. Whenever a new purchase is made, use Pipedream to add that customer to a specific subscriber list on Sender, so they can receive relevant follow-up emails and offers. + +- **Feedback Loop for Product Launches**: After releasing a new product, send out announcement emails via Sender. Use Pipedream to monitor responses or interactions with the email, such as clicks or replies, and funnel that data into a CRM like HubSpot or Salesforce. This feedback can inform future product development and marketing strategies. diff --git a/components/sendfox_oauth/README.md b/components/sendfox_oauth/README.md index ff24d03ed4bea..f757b3550d614 100644 --- a/components/sendfox_oauth/README.md +++ b/components/sendfox_oauth/README.md @@ -1,17 +1,11 @@ # Overview -With SendFox's API, you can easily create powerful integrations that are -tailored to fit the needs of your project. Whether you're looking to manage -customer relationships, send automated transactional emails, or create -marketing automation workflows, the SendFox API has you covered. +Sendfox is an email marketing platform designed for content creators. With the Sendfox (OAuth) API on Pipedream, you can automate your marketing efforts by creating workflows that trigger based on specific events or conditions. This includes sending customized emails, managing contacts, and analyzing campaign performance, all programmatically. By leveraging Pipedream's capabilities, you can connect Sendfox to a myriad of other services to streamline processes, such as collecting new subscribers from various sources, syncing contact lists, or triggering campaigns from external events. -Here are some examples of what you can build with the SendFox API: +# Example Use Cases -- Create automated customer onboarding flows -- Generate invoices and payment acknowledgement emails -- Send automated transactional emails with just a few lines of code -- Create highly customized marketing automations -- Automate drip email sequences for sales or promotions -- Target customers based on their activity and engagement data -- Monitor the delivery of emails and keep an eye on bounce rates -- Sync customer data across multiple systems or services +- **Automated Welcome Email Sequence**: Trigger a personalized welcome email sequence in Sendfox whenever a new user subscribes through a Google Form. Use Pipedream to listen for new form submissions, extract the user's information, and add them to a specific Sendfox list that kicks off the welcome email sequence. + +- **Content-Based Email Campaigns**: Create an automation that sends out an email campaign from Sendfox whenever a new blog post is published on your WordPress site. Use Pipedream to watch for new posts on WordPress, then draft and send an email to your subscribers with the post content, ensuring they never miss an update. + +- **E-commerce Customer Follow-Up**: After a customer makes a purchase on Shopify, set up a Pipedream workflow that adds them to a Sendfox list and triggers a follow-up email sequence for feedback or cross-selling related products. This workflow can help boost customer engagement and drive repeat business. diff --git a/components/sendfox_oauth/package.json b/components/sendfox_oauth/package.json new file mode 100644 index 0000000000000..227f5d289bf77 --- /dev/null +++ b/components/sendfox_oauth/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/sendfox_oauth", + "version": "0.6.0", + "description": "Pipedream sendfox_oauth Components", + "main": "sendfox_oauth.app.mjs", + "keywords": [ + "pipedream", + "sendfox_oauth" + ], + "homepage": "https://pipedream.com/apps/sendfox_oauth", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/sendfox_personal_access_token/README.md b/components/sendfox_personal_access_token/README.md index 2e1c4e3fe189b..5f636e658a2fa 100644 --- a/components/sendfox_personal_access_token/README.md +++ b/components/sendfox_personal_access_token/README.md @@ -1,18 +1,11 @@ # Overview -Building with Sendfox's Personal Access Token API can help you create custom -automated emails, increase user engagement, send mass emails and SMS campaigns, -and more. Here are a few examples of what you can build with the Sendfox -Personal Access Token API: +Sendfox is an email marketing tool designed for content creators. You can automate your email marketing operations by programmatically managing contacts, campaigns, and lists. Pipedream leverages this by allowing you to connect Sendfox with numerous other apps, providing a seamless integration platform to streamline your email marketing workflows. Automate the process of gathering leads from various platforms, syncing your subscriber lists, or triggering personalized email sequences based on specific actions or events. -- Create custom automated emails sent to your contacts -- Automate your sales and marketing processes -- Send mass emails and SMS campaigns to your contacts -- Track email opens and clicks for an in-depth understanding of customer - engagement -- Create automated follow-up emails -- Trigger email campaigns based on user interactions -- Personalize emails to specific segments of your contacts -- Develop workflow automation and mission-critical business processes -- Automatically send personalized messages based on customer activities -- Integrate your Sendfox account with other apps and platforms via API +# Example Use Cases + +- **Lead Capture Automation**: Integrate Sendfox with Typeform via Pipedream to automatically add new form respondents to your Sendfox contact list. When someone completes a Typeform survey or a subscription form, Pipedream triggers an automation that captures the respondent's details and directly subscribes them to your Sendfox list, ensuring you're always building your audience with the latest leads. + +- **Content-Based Email Sequences**: Connect Sendfox with your CMS, like WordPress. Whenever you publish a new blog post, Pipedream triggers an email campaign in Sendfox to notify your subscribers about the fresh content. This keeps your audience engaged and returning to your site, boosting both traffic and loyalty. + +- **E-commerce Customer Follow-Up**: Pair Sendfox with Shopify on Pipedream to create a targeted post-purchase follow-up sequence. After a customer makes a purchase, trigger a workflow that enrolls them into a Sendfox automation series designed to provide additional value, ask for feedback, or promote repeat business. This enhances customer experience and encourages brand loyalty. diff --git a/components/sendfox_personal_access_token/package.json b/components/sendfox_personal_access_token/package.json new file mode 100644 index 0000000000000..2f23606ba97f1 --- /dev/null +++ b/components/sendfox_personal_access_token/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/sendfox_personal_access_token", + "version": "0.6.0", + "description": "Pipedream sendfox_personal_access_token Components", + "main": "sendfox_personal_access_token.app.mjs", + "keywords": [ + "pipedream", + "sendfox_personal_access_token" + ], + "homepage": "https://pipedream.com/apps/sendfox_personal_access_token", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/sendgrid/README.md b/components/sendgrid/README.md index 9386e174bb313..2eb53d88be80b 100644 --- a/components/sendgrid/README.md +++ b/components/sendgrid/README.md @@ -1,11 +1,63 @@ # Overview -With the Twilio SendGrid API, you can build a number of features into your -application to manage your SendGrid account and send emails. For example, you -can use the API to: - -- Create and manage SendGrid accounts -- Send emails through SendGrid -- Manage email templates -- Get statistics on email sends -- And more! +The Twilio SendGrid API opens up a world of possibilities for email automation, enabling you to send emails efficiently and track their performance. With this API, you can programmatically create and send personalized email campaigns, manage contacts, and parse inbound emails for data extraction. When you harness the power of Pipedream, you can connect SendGrid to hundreds of other apps to automate workflows, such as triggering email notifications based on specific actions, syncing email stats with your analytics, or handling incoming emails to create tasks or tickets. + +# Example Use Cases + +- **Automated Customer Support Tickets**: When a customer sends an email to your support address, SendGrid's Inbound Parse Webhook can catch it. Pipedream can then take that email, extract the relevant info, and automatically create a ticket in your customer support platform, such as Zendesk. + +- **Email Campaign Stats to Google Sheets**: After sending an email campaign through SendGrid, you may want to analyze the performance data. Pipedream can automatically fetch the stats, like opens, clicks, and bounces, and log them into a Google Sheet for easy tracking and visualization. + +- **E-commerce Order Confirmation and Follow-up**: When a new order is received in an e-commerce platform like Shopify, you can use Pipedream to trigger an order confirmation email via SendGrid. After a set period, you can follow up with another email asking for feedback or offering a discount on future purchases. + +# Getting Started + +First, open the SendGrid console and log in. + +Then open the [Integration Guide](https://app.sendgrid.com/guide/integrate) and select the **Web API** option. + +![Open the Twilio SendGrid Integration Guide to begin the process of creating an API key to connect with Pipedream](https://res.cloudinary.com/pipedreamin/image/upload/v1715176223/marketplace/apps/sendgrid/CleanShot_2024-05-07_at_15.17.56_2x_zhynua.png) + +Then choose any language from the next menu. Choose any programming language; this selection only affects the example code shown. The key step is obtaining the API key. + +After picking a language, you'll be prompted to generate an API key, we recommend naming it `Pipedream` for easy identification. + +![Name the API key "pipedream" and then after clicking the Generate button, copy the API key and paste it into Pipedream](https://res.cloudinary.com/pipedreamin/image/upload/v1715176222/marketplace/apps/sendgrid/CleanShot_2024-05-07_at_15.19.44_2x_hcduuf.png) + +After creating the API key, copy it and paste it into the appropriate configuration field in a Pipedream SendGrid connected account, either through a Pipedream action/trigger or through the Connected Accounts section of the dashboard. + +You can skip the remaining steps of the Integration guide that test your API key, as these are not necessary for the integration with Pipedream. + +# Troubleshooting + +SendGrid uses standard HTTP status codes to help troubleshoot issues. + +If a SendGrid API call fails, Pipedream will show the error code which you can match with below: + +## 400 - Bad Request + +This error is shown when the request is missing data or is malformed. An example is a malformed email address; SendGrid won't accept invalid email addresses to deliver mail. + +## 401 - Unauthorized + +This means that your SendGrid API key connected to Pipedream is invalid or is missing from the request. + +Ensure the API key is copied correctly from your SendGrid console and included properly as an `Authorization` header in the format `Bearer ${your token here}`. + +## 403 - Sender email isn't verified + +This error occurs when you attempt to use an unverified sender email address under your account. To resolve this error, use the correct email address or verify the email address. See the [verification requirements here](https://docs.sendgrid.com/for-developers/sending-email/sender-identity/). + +## 406 - Missing Accept header + +Make sure to pass `Accept: application/json` in the headers of your HTTP request in order for SendGrid to process your request correctly. + +## 429 - Rate Limit + +This error is thrown when you're sending too many API requests in a short window. The `X-RateLimit-Remaining` and `X-RateLimit-Reset` headers in the response give you the amount of requests remaining in the current rate limit window. + +You can use [Concurrency and Throttling](https://pipedream.com/docs/workflows/concurrency-and-throttling) in your workflow to throttle how quickly your workflow processes new events. + +## 500 - Internal Server Error + +This means that SendGrid is having issues processing requests; check their [status page](https://status.sendgrid.com/) for updates on the service. For these issues, contact SendGrid directly. diff --git a/components/sendgrid/actions/add-email-to-global-suppression/add-email-to-global-suppression.mjs b/components/sendgrid/actions/add-email-to-global-suppression/add-email-to-global-suppression.mjs index ad996d468dc67..70d9071ab09c3 100644 --- a/components/sendgrid/actions/add-email-to-global-suppression/add-email-to-global-suppression.mjs +++ b/components/sendgrid/actions/add-email-to-global-suppression/add-email-to-global-suppression.mjs @@ -5,7 +5,7 @@ export default { key: "sendgrid-add-email-to-global-suppression", name: "Add Email to Global Suppression", description: "Allows you to add one or more email addresses to the global suppressions group. [See the docs here](https://sendgrid.api-docs.io/v3.0/suppressions-global-suppressions/add-recipient-addresses-to-the-global-suppression-group)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { ...common.props, diff --git a/components/sendgrid/actions/add-or-update-contact/add-or-update-contact.mjs b/components/sendgrid/actions/add-or-update-contact/add-or-update-contact.mjs index 95feea7824c13..6050fdfdcad74 100644 --- a/components/sendgrid/actions/add-or-update-contact/add-or-update-contact.mjs +++ b/components/sendgrid/actions/add-or-update-contact/add-or-update-contact.mjs @@ -6,7 +6,7 @@ export default { key: "sendgrid-add-or-update-contact", name: "Add or Update Contact", description: "Adds or updates a contact. [See the docs here](https://docs.sendgrid.com/api-reference/contacts/add-or-update-a-contact)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { ...common.props, diff --git a/components/sendgrid/actions/create-contact-list/create-contact-list.mjs b/components/sendgrid/actions/create-contact-list/create-contact-list.mjs index fda8dc134dd99..4a9492e4955cb 100644 --- a/components/sendgrid/actions/create-contact-list/create-contact-list.mjs +++ b/components/sendgrid/actions/create-contact-list/create-contact-list.mjs @@ -5,7 +5,7 @@ export default { key: "sendgrid-create-contact-list", name: "Create Contact List", description: "Allows you to create a new contact list. [See the docs here](https://docs.sendgrid.com/api-reference/lists/create-list)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { ...common.props, diff --git a/components/sendgrid/actions/create-send/create-send.mjs b/components/sendgrid/actions/create-send/create-send.mjs new file mode 100644 index 0000000000000..e12d28cea8f64 --- /dev/null +++ b/components/sendgrid/actions/create-send/create-send.mjs @@ -0,0 +1,172 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import common from "../common/common.mjs"; + +export default { + ...common, + key: "sendgrid-create-send", + name: "Create Send", + description: "Create a single send. [See the docs here](https://www.twilio.com/docs/sendgrid/api-reference/single-sends/create-single-send)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + name: { + type: "string", + label: "Name", + description: "The name of the Single Send.", + }, + categoryIds: { + propDefinition: [ + common.props.sendgrid, + "categoryIds", + ], + optional: true, + }, + sendAt: { + type: "string", + label: "Send At", + description: "Set this property to an ISO 8601 formatted date-time (YYYY-MM-DDTHH:MM:SSZ) when you would like to send the Single Send. Please note that any `send_at` property value set with this endpoint will prepopulate the send date in the SendGrid user interface (UI). However, the Single Send will remain an unscheduled draft until it's updated with the [Schedule Single Send](https://www.twilio.com/docs/sendgrid/api-reference/single-sends/schedule-single-send) endpoint or SendGrid application UI. Setting this property to `now` with this endpoint will cause an error.", + optional: true, + }, + listIds: { + propDefinition: [ + common.props.sendgrid, + "listIds", + ], + description: "The recipient List IDs that will receive the Single Send.", + optional: true, + hidden: true, + }, + segmentIds: { + propDefinition: [ + common.props.sendgrid, + "segmentIds", + ], + optional: true, + hidden: true, + }, + all: { + type: "boolean", + label: "All", + description: "Set to `true` to send to All Contacts. If set to `false`, at least one `List Ids` or `Segment Ids` value must be provided before the Single Send is scheduled to be sent to recipients.", + default: true, + reloadProps: true, + }, + subject: { + type: "string", + label: "Subject", + description: "The subject line of the Single Send. Do not include this field when using a `Design Id`.", + optional: true, + }, + htmlContent: { + type: "string", + label: "HTML Content", + description: "The HTML content of the Single Send. Do not include this field when using a `Design Id`.", + optional: true, + }, + plainContent: { + type: "string", + label: "Plain Content", + description: "The plain text content of the Single Send. Do not include this field when using a `Design Id`.", + optional: true, + }, + generatePlainContent: { + type: "boolean", + label: "Generate Plain Content", + description: "If set to `true`, `Plain Content` is always generated from `HTML Content`. If set to false, `Plain Content` is not altered.", + optional: true, + }, + designId: { + propDefinition: [ + common.props.sendgrid, + "designId", + ], + optional: true, + }, + editor: { + type: "string", + label: "Editor", + description: "The editor is used to modify the Single Send's design in the Marketing Campaigns App.", + options: [ + "design", + "code", + ], + optional: true, + }, + suppressionGroupId: { + propDefinition: [ + common.props.sendgrid, + "asmGroupId", + ], + optional: true, + }, + customUnsubscribeUrl: { + type: "string", + label: "Custom Unsubscribe URL", + description: "The URL allowing recipients to unsubscribe — you must provide this or the `Suppression Group Id`.", + optional: true, + }, + senderId: { + propDefinition: [ + common.props.sendgrid, + "senderId", + ], + optional: true, + }, + ipPool: { + type: "string", + label: "IP Pool", + description: "The name of the IP Pool from which the Single Send emails are sent.", + optional: true, + }, + }, + async additionalProps(props) { + props.listIds.hidden = this.all; + props.segmentIds.hidden = this.all; + return {}; + }, + async run({ $ }) { + if (!this.suppressionGroupId && !this.customUnsubscribeUrl) { + throw new ConfigurationError("You must provide either `ASM Group ID` or the `Custom Unsubscribe URL`."); + } + try { + const resp = await this.sendgrid.createSingleSend({ + $, + data: { + name: this.name, + categories: parseObject(this.categoryIds), + send_at: this.sendAt, + send_to: { + list_ids: !this.all + ? parseObject(this.listIds) + : null, + segment_ids: !this.all + ? parseObject(this.segmentIds) + : null, + all: this.all, + }, + email_config: { + subject: this.subject, + html_content: this.htmlContent, + plain_content: this.plainContent, + generate_plain_content: this.generatePlainContent, + design_id: this.designId, + editor: this.editor, + suppression_group_id: this.suppressionGroupId, + custom_unsubscribe_url: this.customUnsubscribeUrl, + sender_id: this.senderId, + ip_pool: this.ipPool, + }, + }, + }); + $.export("$summary", `Successfully created single send ${this.name}`); + return resp; + } catch (e) { + const errors = e.split("Unexpected error (status code: ERR_BAD_REQUEST):")[1]; + const errorJson = JSON.parse(errors); + + throw new ConfigurationError(errorJson.data.errors[0].message); + } + }, +}; diff --git a/components/sendgrid/actions/delete-blocks/delete-blocks.mjs b/components/sendgrid/actions/delete-blocks/delete-blocks.mjs index d21a502796b21..bc1134e950786 100644 --- a/components/sendgrid/actions/delete-blocks/delete-blocks.mjs +++ b/components/sendgrid/actions/delete-blocks/delete-blocks.mjs @@ -5,7 +5,7 @@ export default { key: "sendgrid-delete-blocks", name: "Delete Blocks", description: "Allows you to delete all email addresses on your blocks list. [See the docs here](https://docs.sendgrid.com/api-reference/blocks-api/delete-blocks)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { ...common.props, diff --git a/components/sendgrid/actions/delete-bounces/delete-bounces.mjs b/components/sendgrid/actions/delete-bounces/delete-bounces.mjs index 106a774bff4d8..d04c0d91d6631 100644 --- a/components/sendgrid/actions/delete-bounces/delete-bounces.mjs +++ b/components/sendgrid/actions/delete-bounces/delete-bounces.mjs @@ -5,7 +5,7 @@ export default { key: "sendgrid-delete-bounces", name: "Delete Bounces", description: "Allows you to delete all emails on your bounces list. [See the docs here](https://docs.sendgrid.com/api-reference/bounces-api/delete-bounces)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { ...common.props, diff --git a/components/sendgrid/actions/delete-contacts/delete-contacts.mjs b/components/sendgrid/actions/delete-contacts/delete-contacts.mjs index af1193c00757b..630f73000c3fc 100644 --- a/components/sendgrid/actions/delete-contacts/delete-contacts.mjs +++ b/components/sendgrid/actions/delete-contacts/delete-contacts.mjs @@ -5,7 +5,7 @@ export default { key: "sendgrid-delete-contacts", name: "Delete Contacts", description: "Allows you to delete one or more contacts. [See the docs here](https://docs.sendgrid.com/api-reference/contacts/delete-contacts)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { ...common.props, diff --git a/components/sendgrid/actions/delete-global-suppression/delete-global-suppression.mjs b/components/sendgrid/actions/delete-global-suppression/delete-global-suppression.mjs index e8229e8022ad2..a854cec3c5056 100644 --- a/components/sendgrid/actions/delete-global-suppression/delete-global-suppression.mjs +++ b/components/sendgrid/actions/delete-global-suppression/delete-global-suppression.mjs @@ -6,7 +6,7 @@ export default { key: "sendgrid-delete-global-suppression", name: "Delete Global Suppression", description: "Allows you to remove an email address from the global suppressions group. [See the docs here](https://docs.sendgrid.com/api-reference/suppressions-global-suppressions/delete-a-global-suppression)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { ...common.props, diff --git a/components/sendgrid/actions/delete-list/delete-list.mjs b/components/sendgrid/actions/delete-list/delete-list.mjs index 7edcff6ad4e09..fcbf401a01dd1 100644 --- a/components/sendgrid/actions/delete-list/delete-list.mjs +++ b/components/sendgrid/actions/delete-list/delete-list.mjs @@ -5,7 +5,7 @@ export default { key: "sendgrid-delete-list", name: "Delete List", description: "Allows you to delete a specific contact list. [See the docs here](https://docs.sendgrid.com/api-reference/lists/delete-a-list)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { ...common.props, diff --git a/components/sendgrid/actions/get-a-block/get-a-block.mjs b/components/sendgrid/actions/get-a-block/get-a-block.mjs index 24f8602441761..923276deada11 100644 --- a/components/sendgrid/actions/get-a-block/get-a-block.mjs +++ b/components/sendgrid/actions/get-a-block/get-a-block.mjs @@ -6,7 +6,7 @@ export default { key: "sendgrid-get-a-block", name: "Get a Block", description: "Gets a specific block. [See the docs here](https://docs.sendgrid.com/api-reference/blocks-api/retrieve-a-specific-block)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { ...common.props, diff --git a/components/sendgrid/actions/get-a-global-suppression/get-a-global-suppression.mjs b/components/sendgrid/actions/get-a-global-suppression/get-a-global-suppression.mjs index e83c7c56e833c..775c795c759a9 100644 --- a/components/sendgrid/actions/get-a-global-suppression/get-a-global-suppression.mjs +++ b/components/sendgrid/actions/get-a-global-suppression/get-a-global-suppression.mjs @@ -6,7 +6,7 @@ export default { key: "sendgrid-get-a-global-suppression", name: "Get A Global Suppression", description: "Gets a global suppression. [See the docs here](https://docs.sendgrid.com/api-reference/suppressions-global-suppressions/retrieve-a-global-suppression)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { ...common.props, diff --git a/components/sendgrid/actions/get-all-bounces/get-all-bounces.mjs b/components/sendgrid/actions/get-all-bounces/get-all-bounces.mjs index aaaee47fb9b63..5bd9a6cf4224d 100644 --- a/components/sendgrid/actions/get-all-bounces/get-all-bounces.mjs +++ b/components/sendgrid/actions/get-all-bounces/get-all-bounces.mjs @@ -6,7 +6,7 @@ export default { key: "sendgrid-get-all-bounces", name: "Get All Bounces", description: "Allows you to get all of your bounces. [See the docs here](https://docs.sendgrid.com/api-reference/bounces-api/retrieve-all-bounces)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { ...common.props, diff --git a/components/sendgrid/actions/get-contact-lists/get-contact-lists.mjs b/components/sendgrid/actions/get-contact-lists/get-contact-lists.mjs index ace639d9db668..fe1c4d38fb4fb 100644 --- a/components/sendgrid/actions/get-contact-lists/get-contact-lists.mjs +++ b/components/sendgrid/actions/get-contact-lists/get-contact-lists.mjs @@ -5,7 +5,7 @@ export default { key: "sendgrid-get-contact-lists", name: "Get Contact Lists", description: "Allows you to get details of your contact lists. [See the docs here](https://docs.sendgrid.com/api-reference/lists/get-all-lists)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { ...common.props, diff --git a/components/sendgrid/actions/list-blocks/list-blocks.mjs b/components/sendgrid/actions/list-blocks/list-blocks.mjs index 2b5ab9b36e3e0..d4607dc9e483a 100644 --- a/components/sendgrid/actions/list-blocks/list-blocks.mjs +++ b/components/sendgrid/actions/list-blocks/list-blocks.mjs @@ -6,7 +6,7 @@ export default { key: "sendgrid-list-blocks", name: "List Blocks", description: "Allows you to list all email addresses that are currently on your blocks list. [See the docs here](https://docs.sendgrid.com/api-reference/blocks-api/retrieve-all-blocks)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { ...common.props, diff --git a/components/sendgrid/actions/list-global-suppressions/list-global-suppressions.mjs b/components/sendgrid/actions/list-global-suppressions/list-global-suppressions.mjs index 2b96282290053..8aa2b55933258 100644 --- a/components/sendgrid/actions/list-global-suppressions/list-global-suppressions.mjs +++ b/components/sendgrid/actions/list-global-suppressions/list-global-suppressions.mjs @@ -6,7 +6,7 @@ export default { key: "sendgrid-list-global-suppressions", name: "List Global Suppressions", description: "Allows you to get a list of all email address that are globally suppressed. [See the docs here](https://docs.sendgrid.com/api-reference/suppressions-global-suppressions/retrieve-all-global-suppressions)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { ...common.props, diff --git a/components/sendgrid/actions/remove-contact-from-list/remove-contact-from-list.mjs b/components/sendgrid/actions/remove-contact-from-list/remove-contact-from-list.mjs index 975f01f99ef79..52621eabb57a8 100644 --- a/components/sendgrid/actions/remove-contact-from-list/remove-contact-from-list.mjs +++ b/components/sendgrid/actions/remove-contact-from-list/remove-contact-from-list.mjs @@ -5,7 +5,7 @@ export default { key: "sendgrid-remove-contact-from-list", name: "Remove Contact From List", description: "Allows you to remove contacts from a given list. [See the docs here](https://docs.sendgrid.com/api-reference/lists/remove-contacts-from-a-list)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { ...common.props, diff --git a/components/sendgrid/actions/search-contacts/search-contacts.mjs b/components/sendgrid/actions/search-contacts/search-contacts.mjs index a4211590e5468..b0feca6388420 100644 --- a/components/sendgrid/actions/search-contacts/search-contacts.mjs +++ b/components/sendgrid/actions/search-contacts/search-contacts.mjs @@ -1,13 +1,13 @@ +import { ConfigurationError } from "@pipedream/platform"; import common from "../common/common.mjs"; import constants from "../common/constants.mjs"; -import { ConfigurationError } from "@pipedream/platform"; export default { ...common, key: "sendgrid-search-contacts", name: "Search Contacts", description: "Searches contacts with a SGQL query. [See the docs here](https://docs.sendgrid.com/api-reference/contacts/search-contacts)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { ...common.props, diff --git a/components/sendgrid/actions/send-email-multiple-recipients/send-email-multiple-recipients.mjs b/components/sendgrid/actions/send-email-multiple-recipients/send-email-multiple-recipients.mjs index 18bb9009b0b19..a2a1f23ea035b 100644 --- a/components/sendgrid/actions/send-email-multiple-recipients/send-email-multiple-recipients.mjs +++ b/components/sendgrid/actions/send-email-multiple-recipients/send-email-multiple-recipients.mjs @@ -6,7 +6,7 @@ export default { key: "sendgrid-send-email-multiple-recipients", name: "Send Email Multiple Recipients", description: "This action sends a personalized e-mail to multiple specified recipients. [See the docs here](https://docs.sendgrid.com/api-reference/mail-send/mail-send)", - version: "0.0.4", + version: "0.0.5", type: "action", props: { ...common.props, diff --git a/components/sendgrid/actions/send-email-single-recipient/send-email-single-recipient.mjs b/components/sendgrid/actions/send-email-single-recipient/send-email-single-recipient.mjs index 2eefa5cebff61..1514ce68d1b8d 100644 --- a/components/sendgrid/actions/send-email-single-recipient/send-email-single-recipient.mjs +++ b/components/sendgrid/actions/send-email-single-recipient/send-email-single-recipient.mjs @@ -6,7 +6,7 @@ export default { key: "sendgrid-send-email-single-recipient", name: "Send Email Single Recipient", description: "This action sends a personalized e-mail to the specified recipient. [See the docs here](https://docs.sendgrid.com/api-reference/mail-send/mail-send)", - version: "0.0.6", + version: "0.0.7", type: "action", props: { ...common.props, diff --git a/components/sendgrid/actions/validate-email/validate-email.mjs b/components/sendgrid/actions/validate-email/validate-email.mjs index 7aa2c703abf7c..4d1fd7b07248c 100644 --- a/components/sendgrid/actions/validate-email/validate-email.mjs +++ b/components/sendgrid/actions/validate-email/validate-email.mjs @@ -6,7 +6,7 @@ export default { key: "sendgrid-validate-email", name: "Validate Email", description: "Validates an email address. This action requires a Sendgrid's Pro or Premier plan. [See the docs here](https://docs.sendgrid.com/api-reference/e-mail-address-validation/validate-an-email)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { ...common.props, diff --git a/components/sendgrid/common/constants.mjs b/components/sendgrid/common/constants.mjs new file mode 100644 index 0000000000000..ea830c15a04cb --- /dev/null +++ b/components/sendgrid/common/constants.mjs @@ -0,0 +1 @@ +export const LIMIT = 100; diff --git a/components/sendgrid/common/utils.mjs b/components/sendgrid/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/sendgrid/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/sendgrid/package.json b/components/sendgrid/package.json index 93e6e8c04ac37..d09b9a5814181 100644 --- a/components/sendgrid/package.json +++ b/components/sendgrid/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/sendgrid", - "version": "0.3.10", + "version": "0.4.0", "description": "Pipedream Sendgrid Components", "main": "sendgrid.app.js", "keywords": [ @@ -10,7 +10,7 @@ "homepage": "https://pipedream.com/apps/sendgrid", "author": "Pipedream (https://pipedream.com/)", "dependencies": { - "@pipedream/platform": "^1.2.0", + "@pipedream/platform": "^3.0.3", "@sendgrid/client": "^7.6.2", "@sendgrid/eventwebhook": "^7.4.5", "async-retry": "^1.3.1", diff --git a/components/sendgrid/sendgrid.app.mjs b/components/sendgrid/sendgrid.app.mjs index 0dd7f47df80e5..49fef6ab87c7b 100644 --- a/components/sendgrid/sendgrid.app.mjs +++ b/components/sendgrid/sendgrid.app.mjs @@ -1,8 +1,10 @@ -import { axios } from "@pipedream/platform"; -import get from "lodash/get.js"; -import retry from "async-retry"; +import { + axios, ConfigurationError, +} from "@pipedream/platform"; import sendgrid from "@sendgrid/client"; -import { ConfigurationError } from "@pipedream/platform"; +import retry from "async-retry"; +import get from "lodash/get.js"; +import { LIMIT } from "./common/constants.mjs"; export default { type: "app", @@ -15,12 +17,71 @@ export default { optional: true, async options() { const lists = await this.getAllContactLists(); + console.log("lists: ", lists); + return lists.map((list) => ({ label: list.name, value: list.id, })); }, }, + segmentIds: { + type: "string[]", + label: "Segment Ids", + description: "The recipient Segment IDs that will receive the Single Send.", + optional: true, + async options() { + const { results } = await this.getAllSegments(); + return results.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + designId: { + type: "string", + label: "Design Id", + description: "A design id can be used in place of `HTML Content`, `Plain Content`, and/or `Subject`.", + async options() { + const { result } = await this.getAllDesigns(); + return result.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + categoryIds: { + type: "string[]", + label: "Categories", + description: "The categories to associate with this Single Send.", + async options({ page }) { + const { categories } = await this.getAllCategories({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + return categories.map((category) => category); + }, + }, + senderId: { + type: "string", + label: "Sender Id", + description: "The ID of the verified Sender.", + async options() { + const results = await this.getAllSenders(); + return results.map(({ + id: value, nickname: label, + }) => ({ + label, + value, + })); + }, + }, contactIds: { type: "string[]", label: "Contact IDs", @@ -405,6 +466,44 @@ export default { const { data } = await this._makeClientRequest(config); return data; }, + createSingleSend({ + $, ...opts + }) { + const baseUrl = this._apiUrl(); + return this._makeRequest({ + method: "POST", + headers: this._makeRequestHeader(), + url: `${baseUrl}/marketing/singlesends`, + ...opts, + }, $); + }, + getAllDesigns(opts = {}) { + const baseUrl = this._apiUrl(); + return this._makeRequest({ + headers: this._makeRequestHeader(), + url: `${baseUrl}/designs`, + ...opts, + }); + }, + getAllSenders(opts = {}) { + const baseUrl = this._apiUrl(); + return this._makeRequest({ + headers: this._makeRequestHeader(), + url: `${baseUrl}/senders`, + ...opts, + }); + }, + getAllCategories({ + $, ...opts + }) { + const baseUrl = this._apiUrl(); + return this._makeRequest({ + headers: this._makeRequestHeader(), + url: `${baseUrl}/marketing/singlesends/categories`, + ...opts, + }, $); + }, + /** * Deletes all email addresses on on the associated account blocks list * @@ -590,6 +689,14 @@ export default { } while (contactLists.length < maxItems); return contactLists.slice(0, maxItems); }, + getAllSegments(opts = {}) { + const baseUrl = this._apiUrl(); + return this._makeRequest({ + headers: this._makeRequestHeader(), + url: `${baseUrl}/marketing/segments/2.0`, + ...opts, + }); + }, /** * Lists all email addresses that are currently the associated account blocks list. * diff --git a/components/sendgrid/sources/events/events.mjs b/components/sendgrid/sources/events/events.mjs index d20baba1e3f7d..149604cee4344 100644 --- a/components/sendgrid/sources/events/events.mjs +++ b/components/sendgrid/sources/events/events.mjs @@ -7,7 +7,7 @@ export default { key: "sendgrid-events", name: "New Events (Instant)", description: "Emit new event when any of the specified SendGrid events is received", - version: "0.0.5", + version: "0.0.6", type: "source", dedupe: "unique", props: { diff --git a/components/sendgrid/sources/new-contact/new-contact.mjs b/components/sendgrid/sources/new-contact/new-contact.mjs index 2bf933feac07a..ec0a897278805 100644 --- a/components/sendgrid/sources/new-contact/new-contact.mjs +++ b/components/sendgrid/sources/new-contact/new-contact.mjs @@ -6,7 +6,7 @@ export default { key: "sendgrid-new-contact", name: "New Contact", description: "Emit new event when a new contact is created", - version: "0.0.6", + version: "0.0.7", type: "source", dedupe: "unique", hooks: { diff --git a/components/sendicate/README.md b/components/sendicate/README.md index db672b825497b..44170d2a31b37 100644 --- a/components/sendicate/README.md +++ b/components/sendicate/README.md @@ -1,21 +1,11 @@ # Overview -Using the Sendicate API you can create a wide range of email campaigns to -engage with your customers or potential customers. Through the API you can: +Sendicate is an email marketing service that allows you to design, send, and track stunning email campaigns. With its intuitive interface and powerful features, it's built for creating engaging newsletters and marketing emails. Using Pipedream, you can leverage the Sendicate API to automate email campaign management, contact list updates, and performance analytics. Whether you're syncing subscriber data, triggering campaign sends based on specific actions, or analyzing email engagement, Pipedream's no-code platform can craft custom workflows that connect Sendicate seamlessly with hundreds of other services. -- Create, send and track multi-channel email campaigns -- Build advanced segmentation for your campaigns -- Personalize campaigns with custom content for each recipient -- Automatically send triggered emails based on user events -- Collect and analyze data on user behavior +# Example Use Cases -Below are a few examples of what you can build using the Sendicate API: +- **Subscriber Sync Workflow**: Keep your Sendicate contact lists up-to-date automatically. Whenever a new subscriber joins through your website form, Pipedream can catch that data and add the subscriber to a Sendicate list. Pair this with a CRM like Salesforce to ensure all your contact points are synced. -- Newsletters -- Lead nurturing emails -- Welcome emails -- Abandoned cart reminders -- Transactional emails -- Surveys -- Promotional campaigns -- Newsletter subscription forms +- **Automated Campaign Trigger**: Launch Sendicate email campaigns in response to user actions. For example, when a user completes a purchase on your e-commerce platform, Pipedream can trigger a Sendicate API call to send a thank you email or a follow-up survey, enriching customer experience without manual effort. + +- **Performance Analytics Dashboard**: Streamline your campaign analytics by using Pipedream to fetch campaign performance data from Sendicate and send it to a data visualization tool like Google Sheets or Tableau. Get real-time insights into open rates, click-throughs, and engagement metrics, enabling data-driven decisions for future campaigns. diff --git a/components/sendicate/package.json b/components/sendicate/package.json new file mode 100644 index 0000000000000..d1d8036a87059 --- /dev/null +++ b/components/sendicate/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/sendicate", + "version": "0.6.0", + "description": "Pipedream sendicate Components", + "main": "sendicate.app.mjs", + "keywords": [ + "pipedream", + "sendicate" + ], + "homepage": "https://pipedream.com/apps/sendicate", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/sendinblue/README.md b/components/sendinblue/README.md index 93dede91472e9..3639458ece106 100644 --- a/components/sendinblue/README.md +++ b/components/sendinblue/README.md @@ -1,20 +1,11 @@ # Overview -With the SendinBlue API, you can create and manage powerful marketing campaigns -and communications with ease. From transactional emails and SMS to newsletters -and more, the SendinBlue API has you covered. Here are some examples of what -you can build: +The SendinBlue API offers a suite of email marketing tools, including campaign creation, contact list management, and transactional email sending. It's designed for crafting personalized messages, automating marketing workflows, and tracking the performance of email campaigns. With Pipedream, you can harness the power of SendinBlue by automating interactions with other services, reacting to events with custom code, and stitching together complex workflows with minimal effort. -- Create engaging emails using SendinBlue's drag-and-drop email editor, and - send them to your subscribers with the API -- Connect multiple communication channels including email, SMS, voice and chat - for a unified customer experience -- Automate your workflow with SendinBlue's automated campaigns and optimize the - process with advanced segmentation -- Personalize each message to your recipients with dynamic tags, merge fields - and attachments -- Set up transactional emails to streamline communication and ensure your users - receive important notifications quickly and securely -- Generate powerful analytics to measure the success of your campaigns -- Integrate with third party applications using the SendinBlue REST API and - webhooks +# Example Use Cases + +- **Automated Welcome Email Series**: When a new user signs up for your service, trigger a Pipedream workflow that adds them to a SendinBlue contact list and starts a drip email campaign to onboard them smoothly. + +- **Support Ticket Follow-Up Surveys**: After a support ticket is marked resolved in your helpdesk software, use Pipedream to send a follow-up survey via SendinBlue to gauge customer satisfaction and collect feedback for service improvement. + +- **Event-Triggered Promotions**: Combine SendinBlue with your e-commerce platform. When a customer makes a purchase, or an item goes on sale, Pipedream can kick off a targeted email campaign, offering relevant products, discounts, or loyalty rewards. diff --git a/components/sendlane/README.md b/components/sendlane/README.md new file mode 100644 index 0000000000000..e9140d045c646 --- /dev/null +++ b/components/sendlane/README.md @@ -0,0 +1,11 @@ +# Overview + +Sendlane's API taps into the power of email marketing automation allowing you to manage contacts, lists, campaigns, and more programmatically. With the Sendlane API on Pipedream, you can automate workflows that react to various triggers, such as incoming webhooks or scheduled times, to perform actions within Sendlane or other connected services. You can create complex, multi-step workflows that streamline your email marketing efforts, sync data across platforms, and personalize customer interactions. + +# Example Use Cases + +- **Automated Contact Syncing Between Platforms**: Sync new contacts from a CRM like HubSpot to Sendlane. Whenever a contact is added or updated in HubSpot, a Pipedream workflow triggers and updates or creates the contact in Sendlane, keeping your lists up-to-date automatically. + +- **Dynamic Email Campaign Trigger**: Launch an email campaign in Sendlane based on customer behavior tracked in an e-commerce platform like Shopify. For instance, when a customer completes a purchase, Pipedream detects the event and triggers a Sendlane campaign to send a personalized thank you email or a post-purchase follow-up series. + +- **Real-Time Analytics Reporting**: Send real-time analytics from Sendlane to a Google Sheet for easy monitoring. Pipedream can listen for campaign events, such as email opens or link clicks, then log this data into a Google Sheet, providing you with up-to-date campaign performance metrics. diff --git a/components/sendle/README.md b/components/sendle/README.md index 11c8eeea2fa17..80409f76ab6d3 100644 --- a/components/sendle/README.md +++ b/components/sendle/README.md @@ -1,21 +1,11 @@ # Overview -With the Sendle API, you can build applications that streamline the delivery -and shipping processes of your business. Whether you're a large company or a -small business, the API brings a powerful delivery network to your fingertips. -Here are just some of the things you can do with the Sendle API: +The Sendle API is a shipping and logistics interface designed for seamless e-commerce integration. With it, you can automate the booking and tracking of deliveries, manage shipping rates, and handle parcel pickups directly within your e-commerce platform. Leveraging Pipedream's serverless platform allows you to connect Sendle's capabilities with various other services to streamline your shipping operations, improve customer service, and optimize logistics without writing extensive code. -- Automate schedulings and pickups, creating more efficient shipping operations -- Create custom pricing models for special delivery orders -- Calculate cost estimates for delivery jobs to help you set accurate price - points for customers -- Streamline customer communication, providing them with uptime tracking and - status updates -- Track deliveries from start to finish with real-time insights into shipments -- Utilize Sendle’s driver network for external shipping needs -- Integrate powerful third-party transport providers for larger shipments -- Sync with existing systems to keep your shipping process as efficient as - possible -- Enable complex inbound and outbound shipping processes -- Develop custom reporting so you can manage and optimize the shipping process - better than ever before. +# Example Use Cases + +- **Automated Order Fulfillment Workflow**: When a new order is placed in Shopify, trigger a workflow in Pipedream that creates a shipping order with Sendle. After the shipping label is created, update the Shopify order with the tracking details and send a notification to the customer via email or SMS. + +- **Scheduled Shipping Rate Sync**: Use Pipedream's scheduled tasks to fetch current shipping rates from Sendle daily and update these rates in a Google Sheet. This can be used for quick reference by customer support teams or for syncing rates with custom pricing tools used in your e-commerce storefront. + +- **Customer Service Integration**: Whenever a delivery status in Sendle updates to "Delivered," trigger a Pipedream workflow that logs this event in a CRM like HubSpot. Follow this by automatically sending a follow-up satisfaction survey via Typeform to the customer, ensuring timely feedback on their delivery experience. diff --git a/components/sendloop/README.md b/components/sendloop/README.md new file mode 100644 index 0000000000000..7b6f69913dc28 --- /dev/null +++ b/components/sendloop/README.md @@ -0,0 +1,11 @@ +# Overview + +Sendloop is a robust email marketing platform designed to help businesses engage with their audience. With Sendloop's API on Pipedream, you can automate email campaigns, manage subscribers, and track the performance of your emails. Pipedream's serverless platform enables you to connect Sendloop with hundreds of other apps, creating powerful workflows that save time and enhance productivity. + +# Example Use Cases + +- **Automated Subscription Confirmation Emails**: Trigger an automated email from Sendloop when a new user subscribes to your service via a web form. Use Pipedream to listen for new submissions and automatically add the subscriber to Sendloop, sending a welcome email in response. + +- **Synchronize Subscribers with CRM**: Maintain a consistent subscriber list between Sendloop and your CRM platform, like Salesforce. Set up a Pipedream workflow that adds or updates CRM contacts whenever a new subscriber is added in Sendloop, ensuring your marketing and sales teams have up-to-date information. + +- **Monitor Campaign Performance**: Create a data dashboard in Google Sheets to monitor the performance of your Sendloop email campaigns. Use Pipedream to periodically fetch campaign data from Sendloop and append it to a Google Sheet for easy tracking and visualization. diff --git a/components/sendloop/actions/add-subscriber/add-subscriber.mjs b/components/sendloop/actions/add-subscriber/add-subscriber.mjs new file mode 100644 index 0000000000000..b3bd1905067ba --- /dev/null +++ b/components/sendloop/actions/add-subscriber/add-subscriber.mjs @@ -0,0 +1,42 @@ +import sendloop from "../../sendloop.app.mjs"; + +export default { + key: "sendloop-add-subscriber", + name: "Add Subscriber", + description: "Adds a new subscriber to a specified list.", + version: "0.0.1", + type: "action", + props: { + sendloop, + emailAddress: { + type: "string", + label: "Email Address", + description: "The email address of the subscriber", + }, + listId: { + propDefinition: [ + sendloop, + "listId", + ], + }, + subscriptionIp: { + type: "string", + label: "Subscription IP", + description: "IP address of the subscriber", + }, + }, + async run({ $ }) { + const response = await this.sendloop.addSubscriber({ + $, + data: { + EmailAddress: this.emailAddress, + ListID: this.listId, + SubscriptionIP: this.subscriptionIp, + }, + }); + if (response.Success) { + $.export("$summary", `Successfully added subscriber ${this.emailAddress} to list ${this.listId}`); + } + return response; + }, +}; diff --git a/components/sendloop/actions/remove-subscriber/remove-subscriber.mjs b/components/sendloop/actions/remove-subscriber/remove-subscriber.mjs new file mode 100644 index 0000000000000..7058e2819a57c --- /dev/null +++ b/components/sendloop/actions/remove-subscriber/remove-subscriber.mjs @@ -0,0 +1,46 @@ +import sendloop from "../../sendloop.app.mjs"; + +export default { + key: "sendloop-remove-subscriber", + name: "Remove Subscriber", + description: "Unsubscribe an email address from your lists.", + version: "0.0.1", + type: "action", + props: { + sendloop, + listId: { + propDefinition: [ + sendloop, + "listId", + ], + }, + subscriberEmail: { + propDefinition: [ + sendloop, + "subscriberEmail", + (c) => ({ + listId: c.listId, + }), + ], + }, + unsubscriptionIp: { + type: "string", + label: "Unsubscription IP", + description: "IP address of the unsubscription", + }, + }, + async run({ $ }) { + const response = await this.sendloop.removeSubscriber({ + $, + data: { + ListID: this.listId, + EmailAddress: this.subscriberEmail, + UnsubscriptionIP: this.unsubscriptionIp, + }, + }); + if (response.Success) { + $.export("$summary", `Successfully removed subscriber ${this.subscriberEmail}.`); + } + return response; + }, +}; diff --git a/components/sendloop/common/constants.mjs b/components/sendloop/common/constants.mjs new file mode 100644 index 0000000000000..6414d992bb568 --- /dev/null +++ b/components/sendloop/common/constants.mjs @@ -0,0 +1,5 @@ +const DEFAULT_LIMIT = 50; + +export default { + DEFAULT_LIMIT, +}; diff --git a/components/sendloop/package.json b/components/sendloop/package.json index d0414fb9bb71b..973bc57e801e6 100644 --- a/components/sendloop/package.json +++ b/components/sendloop/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/sendloop", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Sendloop Components", "main": "sendloop.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" } -} \ No newline at end of file +} diff --git a/components/sendloop/sendloop.app.mjs b/components/sendloop/sendloop.app.mjs index d757b2a3a8617..ca6f3caf5147e 100644 --- a/components/sendloop/sendloop.app.mjs +++ b/components/sendloop/sendloop.app.mjs @@ -1,11 +1,164 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "sendloop", - propDefinitions: {}, + propDefinitions: { + listId: { + type: "string", + label: "List ID", + description: "The ID of the list", + async options() { + const { Lists: lists } = await this.listLists(); + return lists?.map(({ + ListID: value, Name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + campaignId: { + type: "string", + label: "Campaign ID", + description: "The ID of a campaign", + async options() { + const { Campaigns: campaigns } = await this.listCampaigns(); + return campaigns?.map(({ + CampaignID: value, CampaignName: label, + }) => ({ + value, + label, + })) || []; + }, + }, + subscriberEmail: { + type: "string", + label: "Subscriber Email", + description: "The email address of a subscriber", + async options({ + listId, page, + }) { + const { Subscribers: subscribers } = await this.listSubscribers({ + data: { + ListID: listId, + Page: page, + }, + }); + return subscribers?.map(({ EmailAddress: email }) => email) || []; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `https://${this.$auth.account}.sendloop.com/api/v3`; + }, + _authData(data) { + return { + ...data, + "APIKey": `${this.$auth.api_key}`, + }; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + data, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + method: "POST", + url: `${this._baseUrl()}/${path}`, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + data: this._authData(data), + }); + }, + getSubscriber(opts = {}) { + return this._makeRequest({ + path: "/Subscriber.Get/json", + ...opts, + }); + }, + listCampaigns({ + data, ...opts + } = {}) { + return this._makeRequest({ + path: "/Campaign.GetList/json", + data: { + ...data, + IgnoreDrafts: 0, + IgnoreSending: 0, + IgnorePaused: 0, + IgnoreSent: 0, + IgnoreFaied: 0, + IgnoreApproval: 0, + }, + ...opts, + }); + }, + listLists(opts = {}) { + return this._makeRequest({ + path: "/List.GetList/json", + ...opts, + }); + }, + listSubscribers(opts = {}) { + return this._makeRequest({ + path: "/Subscriber.Browse/json", + ...opts, + }); + }, + listEmailOpens(opts = {}) { + return this._makeRequest({ + path: "/Data.Campaign.EmailOpens/json", + ...opts, + }); + }, + listBounces(opts = {}) { + return this._makeRequest({ + path: "/Data.Campaign.Bounces/json", + ...opts, + }); + }, + addSubscriber(opts = {}) { + return this._makeRequest({ + path: "/Subscriber.Subscribe/json", + ...opts, + }); + }, + removeSubscriber(opts = {}) { + return this._makeRequest({ + path: "/Subscriber.Unsubscribe/json", + ...opts, + }); + }, + async *paginate({ + resourceFn, + resourceType, + data, + }) { + data = { + ...data, + Limit: constants.DEFAULT_LIMIT, + Page: 1, + }; + let total = 0; + do { + const response = await resourceFn({ + data, + }); + if (!response.Success) return; + const items = response[resourceType]; + for (const item of items) { + yield item; + } + total = items?.length; + data.Page++; + } while (total === data.Page); }, }, }; diff --git a/components/sendloop/sources/common/base.mjs b/components/sendloop/sources/common/base.mjs new file mode 100644 index 0000000000000..70650da330d17 --- /dev/null +++ b/components/sendloop/sources/common/base.mjs @@ -0,0 +1,61 @@ +import sendloop from "../../sendloop.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + sendloop, + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + async getSubscriber(item) { + const { Subscriber: subscriber } = await this.sendloop.getSubscriber({ + data: { + ListID: item.ListID, + }, + }); + return subscriber; + }, + isRelevant() { + return true; + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + getResourceType() { + throw new Error("getResourceType is not implemented"); + }, + getData() { + throw new Error("getData is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run() { + const resourceFn = this.getResourceFn(); + const resourceType = this.getResourceType(); + const data = this.getData(); + + const items = this.sendloop.paginate({ + resourceFn, + resourceType, + data, + }); + + for await (const item of items) { + if (!this.isRelevant(item)) { + return; + } + const subscriber = resourceType === "Subscribers" || !item?.ListID + ? item + : await this.getSubscriber(item); + const meta = this.generateMeta(subscriber); + this.$emit(subscriber, meta); + } + }, +}; diff --git a/components/sendloop/sources/new-email-opened/new-email-opened.mjs b/components/sendloop/sources/new-email-opened/new-email-opened.mjs new file mode 100644 index 0000000000000..31bf02477b5da --- /dev/null +++ b/components/sendloop/sources/new-email-opened/new-email-opened.mjs @@ -0,0 +1,43 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "sendloop-new-email-opened", + name: "New Email Opened", + description: "Emit new event when a subscriber opens an email.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + campaignId: { + propDefinition: [ + common.props.sendloop, + "campaignId", + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.sendloop.listEmailOpens; + }, + getResourceType() { + return "Data"; + }, + getData() { + return { + CampaignID: this.campaignId, + }; + }, + generateMeta(data) { + return { + id: data.SubscriberID, + summary: `New Email Open: ${data.SubscriberID}`, + ts: Date.parse(data.OpenDate), + }; + }, + }, + sampleEmit, +}; diff --git a/components/sendloop/sources/new-email-opened/test-event.mjs b/components/sendloop/sources/new-email-opened/test-event.mjs new file mode 100644 index 0000000000000..e6664fc7b709e --- /dev/null +++ b/components/sendloop/sources/new-email-opened/test-event.mjs @@ -0,0 +1,4 @@ +export default { + "SubscriberID": "568", + "OpenDate": "2011-05-25 00:24:19" +} \ No newline at end of file diff --git a/components/sendloop/sources/new-hard-bounce/new-hard-bounce.mjs b/components/sendloop/sources/new-hard-bounce/new-hard-bounce.mjs new file mode 100644 index 0000000000000..e1468a9e38ab6 --- /dev/null +++ b/components/sendloop/sources/new-hard-bounce/new-hard-bounce.mjs @@ -0,0 +1,46 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "sendloop-new-hard-bounce", + name: "New Hard Bounce", + description: "Emit new event when a subscriber's status changes to 'hard bounce'.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + campaignId: { + propDefinition: [ + common.props.sendloop, + "campaignId", + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.sendloop.listBounces; + }, + getResourceType() { + return "Data"; + }, + getData() { + return { + CampaignID: this.campaignId, + }; + }, + isRelevant(data) { + return data.BounceType === "Hard"; + }, + generateMeta(subscriber) { + return { + id: subscriber.SubscriberID, + summary: `New Hard Bounce: ${subscriber.EmailAddress}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/sendloop/sources/new-hard-bounce/test-event.mjs b/components/sendloop/sources/new-hard-bounce/test-event.mjs new file mode 100644 index 0000000000000..e636d2f891751 --- /dev/null +++ b/components/sendloop/sources/new-hard-bounce/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "SubscriberID": "1", + "EmailAddress": "test@example.com", + "BounceType": "Hard", + "SubscriptionStatus": "Subscribed", + "SubscriptionDate": "2024-03-12", + "SubscriptionIP": "34.174.67.46 - API (Subscriber.Import)", + "UnsubscriptionDate": "0000-00-00", + "UnsubscriptionIP": "0.0.0.0", + "OptInDate": "0000-00-00", + "CustomField1": "Test" +} \ No newline at end of file diff --git a/components/sendloop/sources/new-subscriber-added/new-subscriber-added.mjs b/components/sendloop/sources/new-subscriber-added/new-subscriber-added.mjs new file mode 100644 index 0000000000000..48cc5efb35245 --- /dev/null +++ b/components/sendloop/sources/new-subscriber-added/new-subscriber-added.mjs @@ -0,0 +1,43 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "sendloop-new-subscriber-added", + name: "New Subscriber Added", + description: "Emit new event when a new subscriber is added to the list.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + listId: { + propDefinition: [ + common.props.sendloop, + "listId", + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.sendloop.listSubscribers; + }, + getResourceType() { + return "Subscribers"; + }, + getData() { + return { + ListID: this.listId, + }; + }, + generateMeta(subscriber) { + return { + id: subscriber.SubscriberID, + summary: `New Subscriber: ${subscriber.EmailAddress}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/sendloop/sources/new-subscriber-added/test-event.mjs b/components/sendloop/sources/new-subscriber-added/test-event.mjs new file mode 100644 index 0000000000000..61bea766497b8 --- /dev/null +++ b/components/sendloop/sources/new-subscriber-added/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "SubscriberID": "1", + "EmailAddress": "test@example.com", + "BounceType": "Not Bounced", + "SubscriptionStatus": "Subscribed", + "SubscriptionDate": "2024-03-12", + "SubscriptionIP": "34.174.67.46 - API (Subscriber.Import)", + "UnsubscriptionDate": "0000-00-00", + "UnsubscriptionIP": "0.0.0.0", + "OptInDate": "0000-00-00", + "CustomField1": "Test" +} \ No newline at end of file diff --git a/components/sendoso/README.md b/components/sendoso/README.md index 261ac4c605348..c70c24d9f2a9e 100644 --- a/components/sendoso/README.md +++ b/components/sendoso/README.md @@ -1,17 +1,11 @@ # Overview -The Sendoso API provides an easy-to-use platform to quickly and effectively -send physical gifts, cards, and other items to your customers, vendors, or -other key stakeholders. With the Sendoso API, you can build powerful customer -engagement campaigns that include personalized physical gifts and gestures. +Sendoso is a sending platform that enables companies to engage with customers through direct mail, eGifts, and other physical deliveries. Utilizing the Sendoso API via Pipedream allows for the automation of these outreach efforts, seamlessly integrating with CRM systems, marketing automation, or customer support tools. By leveraging workflows and events, you can trigger Sendoso actions based on user behavior, sales milestones, or support tickets, enhancing the physical impact of your digital engagements. -The following are some of the things you can build using the Sendoso API: +# Example Use Cases -- Personalized bulk mailing campaigns, such as holiday cards or special - occasion items -- Targeted customer experience campaigns that incorporate a unique physical - gift directly related to the customer journey -- Automated thank you packages for key customers and vendors -- Email programs that include personalized physical products -- Invitation and promotional campaigns that incorporate physical gifts -- Corporate gifting programs for employee recognition or development +- **Lead Nurturing Campaigns**: Automatically send personalized gifts or swag to leads who have reached a specific stage in your sales funnel in Salesforce. This can improve conversion rates and foster customer relationships. + +- **Customer Milestone Celebrations**: Set up a workflow that monitors subscription lengths or user achievements in your service platform, like Stripe, and sends a token of appreciation via Sendoso when a customer reaches a new tier or milestone. + +- **Event Follow-Up**: After an event stored in Eventbrite, automate sending thank you notes or relevant merchandise to attendees, showing appreciation and keeping your brand top of mind. diff --git a/components/sendowl/README.md b/components/sendowl/README.md index feb82b1a0f4da..d1ff85d974eae 100644 --- a/components/sendowl/README.md +++ b/components/sendowl/README.md @@ -1,27 +1,11 @@ # Overview -The SendOwl API allows developers to quickly and easily take advantage of its -powerful ecommerce features with a range of APIs. With this API, you can easily -create and manage powerful ecommerce strategies, allowing you to build and -scale your business. +The SendOwl API allows you to manage and automate sales and distribution of digital goods. Using Pipedream, you can harness the power of SendOwl to create tailored workflows that respond to events, synchronize customer data, generate reports, and enhance customer experiences. By leveraging Pipedream's capabilities, you can connect SendOwl to numerous other services for a seamless data flow, making processes efficient and reducing manual effort. -Using the SendOwl API, you could easily create and manage your own sales -platform, enabling you to set up customized shopping sites, build and manage -digital product stores, and more. Additionally, this API can be used to -integrate with external services, such as payment gateways and subscription -services, to ensure sales and fulfilment is secure and fast. +# Example Use Cases -Here are some examples of the types of ecommerce solutions you could build -using SendOwl API: +- **Order Processing Automation**: When a new order is received in SendOwl, use Pipedream to automate the fulfillment process. Extract the order details and customer information and feed them into a CRM like Salesforce, or send a personalized 'Thank You' email via SendGrid, enhancing the customer relationship. -- Custom Shop Builder: Build your own, custom store frontend, including pages - like "checkout" and "cart". -- Digital Product Stores: Manage digital products like eBooks, music, software, - and more. -- Payment Gateway Integrations: Easy integration with external payment gateways - such as Stripe. -- Subscription Services: Easily manage your subscription services with SendOwl - API. -- Secure Fulfilment Services: Take advantage of fast and secure fulfilment - services. -- Recurring Billing: Setup and manage recurring billing for your customers. +- **License Key Distribution**: For software sales via SendOwl, set up a workflow to automatically generate and distribute license keys post-purchase. Upon detecting a successful transaction in SendOwl, trigger a function to create a license key, then use Pipedream to send it through a secure email service like Mailgun, ensuring a smooth product delivery. + +- **Analytics Reporting**: Aggregate sales data from SendOwl to produce analytics reports. Collect transaction details periodically and push them to a Google Sheets document. Integrate with data visualization tools like Tableau for insightful dashboards, allowing you to monitor trends and make informed business decisions. diff --git a/components/sendpulse/README.md b/components/sendpulse/README.md index 441e39a1dead0..7e53cc5c06a5c 100644 --- a/components/sendpulse/README.md +++ b/components/sendpulse/README.md @@ -1,21 +1,11 @@ # Overview -SendPulse is a powerful platform that lets you send emails, push notifications, -and SMS messages using their API. It offers advanced features such as email -segmentation, autoresponders, advanced analytics, and more. With the SendPulse -API, you can build amazing automated marketing solutions that can help you -increase conversions, reach more users, and boost your customer engagement. +The SendPulse API taps into the power of multi-channel marketing automation, enabling you to manage mailing lists, send emails, and analyze campaign performance. By leveraging Pipedream, you can stitch SendPulse into a network of apps to create automated workflows. Imagine syncing new sign-ups from your CRM to SendPulse, triggering personalized email sequences based on customer behavior, or pulling campaign stats into your analytics dashboard - All in real-time and without writing a single line of server-side code. -Send pulse API can be used to build: +# Example Use Cases -- Automated lead nurturing campaigns -- Automated welcome emails -- Automated re-engagement campaigns -- SMS campaigns -- Push notifications -- Trigger-based emails -- Personalized customer journeys -- Multi-level segmentation -- Custom user onboarding -- Automated customer feedback -- And much more! +- **Automated Subscriber Onboarding**: As soon as a user signs up on your platform, trigger a workflow that adds them to a specific mailing list in SendPulse. Then, automatically send a personalized welcome email series to guide them through your product's features. + +- **Behavior-Driven Email Campaigns**: Connect SendPulse with your e-commerce platform. When a customer abandons their cart, initiate an automated email via SendPulse that reminds them of their incomplete purchase and offers a discount to incentivize completion. + +- **Lead Qualification and Nurturing**: Integrate SendPulse with a form submission tool like Typeform. Upon receiving a new form entry, add the contact to SendPulse and use lead scoring to segment the contact into the appropriate email campaign for nurturing. diff --git a/components/sendpulse/package.json b/components/sendpulse/package.json new file mode 100644 index 0000000000000..202200c751835 --- /dev/null +++ b/components/sendpulse/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/sendpulse", + "version": "0.6.0", + "description": "Pipedream sendpulse Components", + "main": "sendpulse.app.mjs", + "keywords": [ + "pipedream", + "sendpulse" + ], + "homepage": "https://pipedream.com/apps/sendpulse", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/sendsms/README.md b/components/sendsms/README.md new file mode 100644 index 0000000000000..5e62d075d2780 --- /dev/null +++ b/components/sendsms/README.md @@ -0,0 +1,11 @@ +# Overview + +The sendSMS API provides a straightforward way to integrate SMS capabilities into automation workflows on Pipedream. This API allows you to send text messages directly to users' phones, which can be pivotal for timely alerts, critical notifications, or personalized marketing messages. With Pipedream's serverless platform, you can trigger these SMS communications based on a wide variety of events, such as new e-commerce orders, support ticket updates, or even changes in third-party APIs. + +# Example Use Cases + +- **Order Confirmation Texts**: When a customer completes a purchase on your e-commerce platform (like Shopify), a Pipedream workflow can be triggered, capturing the order details and using the sendSMS API to send a confirmation message to the customer's phone number. + +- **Appointment Reminders**: Sync a calendaring app like Google Calendar with Pipedream. When an upcoming appointment is nearing, a workflow can automatically fetch the appointment details and use the sendSMS API to dispatch a reminder SMS to the participant. + +- **Real-time Alerts from IoT Devices**: If you have IoT devices that monitor certain conditions (like a smart thermostat tracking temperature), you can connect these devices to Pipedream. Whenever the device detects a specified condition (e.g., temperature too high), it can trigger a Pipedream workflow that sends an alert via the sendSMS API to responsible personnel. diff --git a/components/sendsms/actions/add-contact/add-contact.mjs b/components/sendsms/actions/add-contact/add-contact.mjs new file mode 100644 index 0000000000000..b767ae25fc241 --- /dev/null +++ b/components/sendsms/actions/add-contact/add-contact.mjs @@ -0,0 +1,49 @@ +import sendsms from "../../sendsms.app.mjs"; + +export default { + key: "sendsms-add-contact", + name: "Add Contact", + description: "Add a new contact into a specified group in SendSMS. [See the documentation](https://www.sendsms.ro/api/#add-a-contact)", + version: "0.0.1", + type: "action", + props: { + sendsms, + groupId: { + propDefinition: [ + sendsms, + "groupId", + ], + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "A phone number in E.164 Format but without the + sign (eg. 40727363767)", + }, + firstName: { + type: "string", + label: "First Name", + description: "First name of the user", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the user", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.sendsms.addContact({ + $, + params: { + group_id: this.groupId, + phone_number: this.phoneNumber, + first_name: this.firstName, + last_name: this.lastName, + }, + }); + + $.export("$summary", `The new contact was successfully created with Id: ${response.details}`); + return response; + }, +}; diff --git a/components/sendsms/actions/check-blocklist/check-blocklist.mjs b/components/sendsms/actions/check-blocklist/check-blocklist.mjs new file mode 100644 index 0000000000000..5475bade9ff2b --- /dev/null +++ b/components/sendsms/actions/check-blocklist/check-blocklist.mjs @@ -0,0 +1,32 @@ +import sendsms from "../../sendsms.app.mjs"; + +export default { + key: "sendsms-check-blocklist", + name: "Check Blocklist", + description: "Checks if a specific phone number is in the blocklist. [See the documentation](https://www.sendsms.ro/api/)", + version: "0.0.1", + type: "action", + props: { + sendsms, + phoneNumber: { + type: "integer", + label: "Phone Number to Check", + description: "The phone number to check in the blocklist, in E.164 format without the + sign (e.g., 40727363767).", + }, + }, + async run({ $ }) { + const response = await this.sendsms.checkBlocklist({ + $, + params: { + phonenumber: this.phoneNumber, + }, + }); + + if (response.status < 0) { + throw new Error(response.message); + } + + $.export("$summary", `The phone number ${this.phoneNumber} is ${response.message}`); + return response; + }, +}; diff --git a/components/sendsms/actions/send-message-with-unsubscribe-link/send-message-with-unsubscribe-link.mjs b/components/sendsms/actions/send-message-with-unsubscribe-link/send-message-with-unsubscribe-link.mjs new file mode 100644 index 0000000000000..37abb1595bcb0 --- /dev/null +++ b/components/sendsms/actions/send-message-with-unsubscribe-link/send-message-with-unsubscribe-link.mjs @@ -0,0 +1,113 @@ +import sendsms from "../../sendsms.app.mjs"; + +export default { + name: "Send Message with Unsubscribe Link", + description: "This action sends an SMS message with an unsubscribe link using the SendSMS.ro API. [See the documentation](https://www.sendsms.ro/api/?shell#send-message-with-unsubscribe-link)", + key: "sendsms-send-message-with-unsubscribe-link", + version: "0.0.2", + type: "action", + props: { + sendsms, + to: { + propDefinition: [ + sendsms, + "to", + ], + }, + text: { + propDefinition: [ + sendsms, + "text", + ], + }, + from: { + propDefinition: [ + sendsms, + "from", + ], + optional: true, + }, + reportMask: { + propDefinition: [ + sendsms, + "reportMask", + ], + optional: true, + }, + report_url: { + propDefinition: [ + sendsms, + "report_url", + ], + optional: true, + }, + charset: { + propDefinition: [ + sendsms, + "charset", + ], + optional: true, + }, + data_coding: { + propDefinition: [ + sendsms, + "data_coding", + ], + optional: true, + }, + message_class: { + propDefinition: [ + sendsms, + "message_class", + ], + optional: true, + }, + auto_detect_encoding: { + propDefinition: [ + sendsms, + "auto_detect_encoding", + ], + optional: true, + }, + short: { + propDefinition: [ + sendsms, + "short", + ], + optional: true, + }, + ctype: { + propDefinition: [ + sendsms, + "ctype", + ], + optional: true, + }, + }, + async run({ $ }) { + const { + sendsms, + reportMask, + ...params + } = this; + + const totalMask = reportMask?.length + ? reportMask.reduce((partialSum, a) => partialSum + a, 0) + : null; + + const response = await sendsms.sendSmsGDPR({ + $, + params: { + ...params, + report_mask: totalMask, + }, + }); + + if (response.status < 0) { + throw new Error(response.message); + } + + $.export("$summary", `Successfully sent message to '${this.to}'`); + return response; + }, +}; diff --git a/components/sendsms/actions/send-message/send-message.mjs b/components/sendsms/actions/send-message/send-message.mjs new file mode 100644 index 0000000000000..f575bed10fd6e --- /dev/null +++ b/components/sendsms/actions/send-message/send-message.mjs @@ -0,0 +1,113 @@ +import sendsms from "../../sendsms.app.mjs"; + +export default { + name: "Send Message", + description: "This action sends an SMS message using the SendSMS.ro API. [See the documentation](https://www.sendsms.ro/api/#send-message)", + key: "sendsms-send-message", + version: "0.0.2", + type: "action", + props: { + sendsms, + to: { + propDefinition: [ + sendsms, + "to", + ], + }, + text: { + propDefinition: [ + sendsms, + "text", + ], + }, + from: { + propDefinition: [ + sendsms, + "from", + ], + optional: true, + }, + reportMask: { + propDefinition: [ + sendsms, + "reportMask", + ], + optional: true, + }, + report_url: { + propDefinition: [ + sendsms, + "report_url", + ], + optional: true, + }, + charset: { + propDefinition: [ + sendsms, + "charset", + ], + optional: true, + }, + data_coding: { + propDefinition: [ + sendsms, + "data_coding", + ], + optional: true, + }, + message_class: { + propDefinition: [ + sendsms, + "message_class", + ], + optional: true, + }, + auto_detect_encoding: { + propDefinition: [ + sendsms, + "auto_detect_encoding", + ], + optional: true, + }, + short: { + propDefinition: [ + sendsms, + "short", + ], + optional: true, + }, + ctype: { + propDefinition: [ + sendsms, + "ctype", + ], + optional: true, + }, + }, + async run({ $ }) { + const { + sendsms, + reportMask, + ...params + } = this; + + const totalMask = reportMask?.length + ? reportMask.reduce((partialSum, a) => partialSum + a, 0) + : null; + + const response = await sendsms.sendSms({ + $, + params: { + ...params, + report_mask: totalMask, + }, + }); + + if (response.status < 0) { + throw new Error(response.message); + } + + $.export("$summary", `Successfully sent message to '${this.to}'`); + return response; + }, +}; diff --git a/components/sendsms/package.json b/components/sendsms/package.json new file mode 100644 index 0000000000000..ab2ec7f12c26b --- /dev/null +++ b/components/sendsms/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/sendsms", + "version": "0.2.0", + "description": "Pipedream sendSMS Components", + "main": "sendsms.app.mjs", + "keywords": [ + "pipedream", + "sendsms" + ], + "homepage": "https://pipedream.com/apps/sendsms", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/sendsms/sendsms.app.mjs b/components/sendsms/sendsms.app.mjs new file mode 100644 index 0000000000000..4814bee353141 --- /dev/null +++ b/components/sendsms/sendsms.app.mjs @@ -0,0 +1,183 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "sendsms", + propDefinitions: { + groupId: { + type: "integer", + label: "Group ID", + description: "The ID of the group to add the contact to.", + async options({ page }) { + const { details } = await this.listGroups({ + params: { + page, + }, + }); + + return details.map(({ + id, name: label, + }) => ({ + label, + value: parseInt(id), + })); + }, + }, + reportMask: { + type: "integer", + label: "Report Mask", + description: "Reporting options for delivery status.", + options: [ + { + label: "Delivered", + value: 1, + }, + { + label: "Undelivered", + value: 2, + }, + { + label: "Queued at network", + value: 4, + }, + { + label: "Sent to network", + value: 8, + }, + { + label: "Failed at network", + value: 16, + }, + ], + }, + report_url: { + type: "string", + label: "Report URL", + description: "The URL to send delivery reports to.", + }, + charset: { + type: "string", + label: "Charset", + description: "Character encoding for the text message.", + default: "UTF-8", + }, + data_coding: { + type: "string", + label: "Data Coding", + description: "The encoding scheme of the message text.", + }, + message_class: { + type: "string", + label: "Message Class", + description: "Class of the SMS message.", + }, + auto_detect_encoding: { + type: "integer", + label: "Auto Detect Encoding", + description: "Automatically detect text encoding.", + }, + short: { + type: "boolean", + label: "Short", + description: "Use short encoding if possible.", + }, + ctype: { + type: "integer", + label: "CType", + description: "Type of the content.", + default: 1, + }, + to: { + type: "string", + label: "To", + description: "The phone number to send the SMS to, in E.164 format without the + sign (e.g., 40727363767).", + }, + text: { + type: "string", + label: "Text", + description: "The text message to send.", + }, + from: { + type: "string", + label: "From", + description: "The sender ID that appears on the recipient's device.", + }, + }, + methods: { + _baseUrl() { + return "https://api.sendsms.ro/json"; + }, + _params(params) { + return { + ...params, + username: `${this.$auth.username}`, + password: `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, params, ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: this._baseUrl(), + params: this._params(params), + }); + }, + listGroups({ + params, ...opts + }) { + return this._makeRequest({ + params: { + ...params, + action: "address_book_groups_get_list", + }, + ...opts, + }); + }, + sendSms({ + params, ...opts + }) { + return this._makeRequest({ + params: { + action: "message_send", + ...params, + }, + ...opts, + }); + }, + sendSmsGDPR({ + params, ...opts + }) { + return this._makeRequest({ + params: { + action: "message_send_gdpr", + ...params, + }, + ...opts, + }); + }, + checkBlocklist({ + params, ...opts + }) { + return this._makeRequest({ + params: { + action: "blocklist_check", + ...params, + }, + ...opts, + }); + }, + addContact({ + params, ...opts + }) { + return this._makeRequest({ + method: "POST", + params: { + action: "address_book_contact_add", + ...params, + }, + ...opts, + }); + }, + }, +}; diff --git a/components/sendspark/actions/create-dynamic-video/create-dynamic-video.mjs b/components/sendspark/actions/create-dynamic-video/create-dynamic-video.mjs new file mode 100644 index 0000000000000..af44b78348d11 --- /dev/null +++ b/components/sendspark/actions/create-dynamic-video/create-dynamic-video.mjs @@ -0,0 +1,68 @@ +import sendspark from "../../sendspark.app.mjs"; + +export default { + key: "sendspark-create-dynamic-video", + name: "Create Dynamic Video", + description: "Creates a new dynamic video campaign. [See the documentation](https://help.sendspark.com/en/articles/9051823-api-automatically-create-dynamic-videos-via-api-integration)", + version: "0.0.1", + type: "action", + props: { + sendspark, + dynamicId: { + propDefinition: [ + sendspark, + "dynamicId", + ], + }, + contactName: { + type: "string", + label: "Name", + description: "Name of the contact.", + }, + contactEmail: { + type: "string", + label: "Email", + description: "Email of the contact.", + }, + company: { + type: "string", + label: "Company", + description: "The name of the company.", + optional: true, + }, + jobTitle: { + type: "string", + label: "Job Title", + description: "The title of the job.", + optional: true, + }, + backgroundUrl: { + type: "string", + label: "Background URL", + description: "What do you want to show in the contact video.", + }, + }, + async run({ $ }) { + const { + sendspark, + dynamicId, + ...data + } = this; + + const response = await sendspark.createDynamicVideoCampaign({ + $, + dynamicId, + data: { + processAndAuthorizeCharge: true, + prospectDepurationConfig: { + forceCreation: false, + payloadDepurationStrategy: "keep-first-valid", + }, + prospect: data, + }, + }); + + $.export("$summary", `Successfully created dynamic video campaign with contact name ${this.contactName}`); + return response; + }, +}; diff --git a/components/sendspark/package.json b/components/sendspark/package.json new file mode 100644 index 0000000000000..9bedb9ac13bfb --- /dev/null +++ b/components/sendspark/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/sendspark", + "version": "0.1.0", + "description": "Pipedream Sendspark Components", + "main": "sendspark.app.mjs", + "keywords": [ + "pipedream", + "sendspark" + ], + "homepage": "https://pipedream.com/apps/sendspark", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/sendspark/sendspark.app.mjs b/components/sendspark/sendspark.app.mjs new file mode 100644 index 0000000000000..fa27b69cd2fba --- /dev/null +++ b/components/sendspark/sendspark.app.mjs @@ -0,0 +1,58 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "sendspark", + propDefinitions: { + dynamicId: { + type: "string", + label: "Dynamic Campaign ID", + description: "The ID of the dynamic campaign.", + async options() { + const { response: { data } } = await this.listDynamicCampaigns(); + + return data.map(({ + _id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return `${this.$auth.api_url}/v1/workspaces/${this.$auth.workspace_id}`; + }, + _headers() { + return { + "x-api-key": `${this.$auth.api_key}`, + "x-api-secret": `${this.$auth.api_secret_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listDynamicCampaigns(opts = {}) { + return this._makeRequest({ + path: "/dynamics", + ...opts, + }); + }, + createDynamicVideoCampaign({ + dynamicId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/dynamics/${dynamicId}/prospect`, + ...opts, + }); + }, + }, +}; diff --git a/components/sendx/README.md b/components/sendx/README.md new file mode 100644 index 0000000000000..5644e57f9ce50 --- /dev/null +++ b/components/sendx/README.md @@ -0,0 +1,11 @@ +# Overview + +The SendX API offers a toolbox for email marketing automation, allowing developers to integrate SendX's capabilities into Pipedream workflows seamlessly. On Pipedream, you can harness this API to trigger actions based on various conditions, synchronize data across platforms, automate email campaigns, and personalize interactions with your subscribers. The power of Pipedream lies in its ability to connect the dots between SendX and numerous other apps, creating a cohesive automation ecosystem. + +# Example Use Cases + +- **Sync SendX Contacts with CRM**: Keep your CRM contacts in sync with your SendX subscribers. Whenever a new subscriber is added to SendX, automatically add them to your CRM platform, like Salesforce or HubSpot, ensuring all customer touchpoints remain updated. + +- **Automate Welcome Email Series**: Trigger a sequence of welcome emails through SendX when a user signs up on your website. Use Pipedream's HTTP trigger to initiate the workflow, enhancing subscriber engagement right from the start without manual intervention. + +- **Aggregate Subscription Analytics**: Collect and send subscription metrics from SendX to a data visualization tool like Google Sheets or Tableau. Set up a scheduled Pipedream workflow to fetch data periodically and represent your email campaign performance comprehensively. diff --git a/components/sendx/actions/add-tag-contact/add-tag-contact.mjs b/components/sendx/actions/add-tag-contact/add-tag-contact.mjs new file mode 100644 index 0000000000000..203e56696c280 --- /dev/null +++ b/components/sendx/actions/add-tag-contact/add-tag-contact.mjs @@ -0,0 +1,40 @@ +import sendx from "../../sendx.app.mjs"; + +export default { + key: "sendx-add-tag-contact", + name: "Add Tag to Contact", + description: "Associates a user-provided tag with a specified contact in SendX. This action requires the contact's identification detail and the tag.", + version: "0.0.1", + type: "action", + props: { + sendx, + contactEmail: { + propDefinition: [ + sendx, + "contactEmail", + ], + }, + tag: { + propDefinition: [ + sendx, + "tag", + ], + }, + }, + async run({ $ }) { + const response = await this.sendx.updateTag({ + $, + params: { + email: this.contactEmail, + }, + data: { + addTags: [ + this.tag, + ], + }, + }); + + $.export("$summary", `Successfully associated tag '${this.tag}' with contact '${this.contactEmail}'`); + return response; + }, +}; diff --git a/components/sendx/actions/create-update-contact/create-update-contact.mjs b/components/sendx/actions/create-update-contact/create-update-contact.mjs new file mode 100644 index 0000000000000..29eb4c4fc0ff9 --- /dev/null +++ b/components/sendx/actions/create-update-contact/create-update-contact.mjs @@ -0,0 +1,79 @@ +import { parseObject } from "../../common/utils.mjs"; +import sendx from "../../sendx.app.mjs"; + +export default { + key: "sendx-create-update-contact", + name: "Create or Update Contact", + description: "Creates a new contact or updates an existing one with user-provided data. [See the documentation](https://github.com/sendx/sendx-api-nodejs/tree/master)", + version: "0.0.1", + type: "action", + props: { + sendx, + email: { + type: "string", + label: "Email", + description: "The email of the contact.", + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the contact.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the contact.", + optional: true, + }, + newEmail: { + type: "string", + label: "New Email", + description: "The new email of the contact if it needs to be updated.", + optional: true, + }, + company: { + type: "string", + label: "Company", + description: "The company of the contact.", + optional: true, + }, + birthday: { + type: "string", + label: "Birthday", + description: "The birthday of the contact in YYYY-MM-DD format.", + optional: true, + }, + customFields: { + type: "object", + label: "Custom Fields", + description: "Custom fields for the contact.", + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "Tags to associate with the contact.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.sendx.upsertContact({ + $, + data: { + email: this.email, + firstName: this.firstName, + lastName: this.lastName, + newEmail: this.newEmail, + company: this.company, + birthday: this.birthday, + customFields: parseObject(this.customFields), + tags: parseObject(this.tags), + }, + }); + console.log("response: ", response); + + $.export("$summary", `Successfully created or updated the contact with email: ${this.email}`); + return response; + }, +}; diff --git a/components/sendx/actions/remove-tag-contact/remove-tag-contact.mjs b/components/sendx/actions/remove-tag-contact/remove-tag-contact.mjs new file mode 100644 index 0000000000000..eaca2ab224a4d --- /dev/null +++ b/components/sendx/actions/remove-tag-contact/remove-tag-contact.mjs @@ -0,0 +1,40 @@ +import sendx from "../../sendx.app.mjs"; + +export default { + key: "sendx-remove-tag-contact", + name: "Remove Tag from Contact", + description: "De-associates a user-provided tag from a given contact in SendX.", + version: "0.0.1", + type: "action", + props: { + sendx, + contactEmail: { + propDefinition: [ + sendx, + "contactEmail", + ], + }, + tag: { + propDefinition: [ + sendx, + "tag", + ], + }, + }, + async run({ $ }) { + const response = await this.sendx.updateTag({ + $, + params: { + email: this.contactEmail, + }, + data: { + removeTags: [ + this.tag, + ], + }, + }); + + $.export("$summary", `Successfully associated tag '${this.tag}' with contact '${this.contactEmail}'`); + return response; + }, +}; diff --git a/components/sendx/common/utils.mjs b/components/sendx/common/utils.mjs new file mode 100644 index 0000000000000..154701004e72a --- /dev/null +++ b/components/sendx/common/utils.mjs @@ -0,0 +1,18 @@ +export const parseObject = (obj) => { + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + return JSON.parse(obj); + } + return obj; +}; diff --git a/components/sendx/package.json b/components/sendx/package.json new file mode 100644 index 0000000000000..a5398ec4f11ab --- /dev/null +++ b/components/sendx/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/sendx", + "version": "0.1.0", + "description": "Pipedream SendX Components", + "main": "sendx.app.mjs", + "keywords": [ + "pipedream", + "sendx" + ], + "homepage": "https://pipedream.com/apps/sendx", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" + } +} diff --git a/components/sendx/sendx.app.mjs b/components/sendx/sendx.app.mjs new file mode 100644 index 0000000000000..f346792fb1df2 --- /dev/null +++ b/components/sendx/sendx.app.mjs @@ -0,0 +1,108 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "sendx", + propDefinitions: { + contactDetails: { + type: "object", + label: "Contact Details", + description: "The details of the contact to create or update, including name, email, and other essential information.", + }, + contactEmail: { + type: "string", + label: "Contact Email", + description: "The email of the contact.", + }, + tag: { + type: "string", + label: "Tag", + description: "The tag to associate with a contact.", + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the contact.", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the contact.", + }, + email: { + type: "string", + label: "Email", + description: "The email of the contact.", + }, + newEmail: { + type: "string", + label: "New Email", + description: "The new email of the contact if it needs to be updated.", + optional: true, + }, + company: { + type: "string", + label: "Company", + description: "The company of the contact.", + optional: true, + }, + birthday: { + type: "string", + label: "Birthday", + description: "The birthday of the contact in YYYY-MM-DD format.", + optional: true, + }, + customFields: { + type: "object", + label: "Custom Fields", + description: "Custom fields for the contact.", + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "Tags to associate with the contact.", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://app.sendx.io/api/v1"; + }, + _headers() { + return { + "api_key": `${this.$auth.api_key}`, + }; + }, + _params(params) { + return { + ...params, + team_id: `${this.$auth.team_id}`, + }; + }, + _makeRequest({ + $ = this, path, params, ...otherOpts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + params: this._params(params), + ...otherOpts, + }); + }, + upsertContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contact/identify", + ...opts, + }); + }, + updateTag(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contact/track", + ...opts, + }); + }, + }, +}; diff --git a/components/sendy/README.md b/components/sendy/README.md new file mode 100644 index 0000000000000..3bad679437c93 --- /dev/null +++ b/components/sendy/README.md @@ -0,0 +1,11 @@ +# Overview + +The Sendy API allows you to programmatically manage email campaigns, subscribers, and lists. With Pipedream's capabilities, you can leverage Sendy API to create custom workflows that respond to events, synchronize data across platforms, and automate email marketing tasks. Pipedream provides a serverless platform where you can connect Sendy with hundreds of other apps without writing complex code, harnessing the power of Sendy's features with event-driven programming and API integration. + +# Example Use Cases + +- **Automate Subscriber Syncing Between Platforms**: Create a workflow on Pipedream that listens for new user sign-ups on your platform via webhooks, then automatically adds those users as subscribers to a specific list in Sendy. This keeps your email lists up-to-date without manual intervention. + +- **Trigger Email Campaigns from CRM Events**: Set up a Pipedream workflow that monitors updates in a CRM like Salesforce. When a lead reaches a certain stage, automatically trigger a Sendy email campaign to that lead. This ensures timely follow-up emails that could help in converting leads into customers. + +- **Auto-Respond to Unsubscription Requests**: Implement a Pipedream workflow that handles incoming unsubscription requests. Upon receiving an unsubscribe webhook from your app or website, the workflow can remove the user from the Sendy list and log the action in a Google Sheet for recordkeeping. This helps maintain compliance with email marketing regulations. diff --git a/components/sendy/actions/add-update-subscriber/add-update-subscriber.mjs b/components/sendy/actions/add-update-subscriber/add-update-subscriber.mjs new file mode 100644 index 0000000000000..86631fd0d5539 --- /dev/null +++ b/components/sendy/actions/add-update-subscriber/add-update-subscriber.mjs @@ -0,0 +1,88 @@ +import sendy from "../../sendy.app.mjs"; + +export default { + key: "sendy-add-update-subscriber", + name: "Add or Update a Subscriber", + description: "Adds a new subscriber or updates existing subscriber's details for a specific list. [See the documentation](https://sendy.co/api?app_path=https://sendy.email/dev2#subscribe)", + version: "0.0.1", + type: "action", + props: { + sendy, + brandId: { + propDefinition: [ + sendy, + "brandId", + ], + }, + list: { + propDefinition: [ + sendy, + "listId", + ({ brandId }) => ({ + brandId, + }), + ], + }, + email: { + propDefinition: [ + sendy, + "email", + ], + }, + name: { + type: "string", + label: "Name", + description: "The user's name.", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "user's [2 letter country code](https://datahub.io/core/country-list#resource-data).", + optional: true, + }, + ipaddress: { + type: "string", + label: "IP Address", + description: "The user's IP address.", + optional: true, + }, + referrer: { + type: "string", + label: "Referrer", + description: "The URL where the user signed up from.", + optional: true, + }, + gdpr: { + type: "boolean", + label: "GDPR", + description: "If you're signing up EU users in a GDPR compliant manner, set this to true.", + optional: true, + }, + silent: { + type: "boolean", + label: "silent", + description: "Set to True if your list is 'Double opt-in' but you want to bypass that and signup the user to the list as 'Single Opt-in instead'", + optional: true, + }, + }, + async run({ $ }) { + const { + sendy, + ...data + } = this; + + const response = await sendy.addOrUpdateSubscriber({ + $, + data: { + ...data, + boolean: true, + }, + }); + + $.export("$summary", `Successfully ${response === 1 + ? "added" + : "updated"} the subscriber with email: ${this.email}`); + return response; + }, +}; diff --git a/components/sendy/actions/create-draft-campaign/create-draft-campaign.mjs b/components/sendy/actions/create-draft-campaign/create-draft-campaign.mjs new file mode 100644 index 0000000000000..43d7f1631f910 --- /dev/null +++ b/components/sendy/actions/create-draft-campaign/create-draft-campaign.mjs @@ -0,0 +1,176 @@ +import { parseObject } from "../../common/utils.mjs"; +import sendy from "../../sendy.app.mjs"; + +export default { + key: "sendy-create-draft-campaign", + name: "Create Draft Campaign", + description: "Creates a new draft campaign ready to be filled in with details. [See the documentation](https://sendy.co/api?app_path=https://sendy.email/dev2#create-send-campaigns)", + version: "0.0.1", + type: "action", + props: { + sendy, + fromName: { + type: "string", + label: "From Name", + description: "The 'From Name' of your campaign.", + }, + fromEmail: { + type: "string", + label: "From Email", + description: "The 'From Email' of your campaign.", + }, + replyTo: { + type: "string", + label: "Reply To", + description: "The 'Reply To' of your campaign.", + }, + title: { + type: "string", + label: "Title", + description: "The 'Title' of your campaign.", + }, + subject: { + type: "string", + label: "Subject", + description: "The 'Subject' of your campaign.", + }, + plainText: { + type: "string", + label: "Plain Text", + description: "The 'Plain text version' of your campaign.", + optional: true, + }, + htmlText: { + type: "string", + label: "HTML Text", + description: "The 'HTML version' of your campaign.", + }, + brandId: { + propDefinition: [ + sendy, + "brandId", + ], + }, + listIds: { + propDefinition: [ + sendy, + "listId", + ({ brandId }) => ({ + brandId, + }), + ], + type: "string[]", + label: "List IDs", + description: "A list of List IDs.", + optional: true, + }, + segmentIds: { + type: "string[]", + label: "Segment IDs", + description: "A list of segment IDs. Segment ids can be found in the segments setup page.", + optional: true, + }, + excludeListIds: { + propDefinition: [ + sendy, + "listId", + ({ brandId }) => ({ + brandId, + }), + ], + type: "string[]", + label: "Exclude List IDs", + description: "Lists to exclude from your campaign.", + optional: true, + }, + excludeSegmentIds: { + type: "string[]", + label: "Exclude Segment IDs", + description: "Segments to exclude from your campaign. Segment ids can be found in the segments setup page.", + optional: true, + }, + queryString: { + type: "string", + label: "Query String", + description: "e.g., Google Analytics tags.", + optional: true, + }, + trackOpens: { + type: "string", + label: "Track Opens", + description: "Open tracking behaviour.", + options: [ + { + label: "Disabled", + value: "0", + }, + { + label: "Enabled", + value: "1", + }, + { + label: "Anonymous", + value: "2", + }, + ], + }, + trackClicks: { + type: "string", + label: "Track Clicks", + description: "Click tracking behaviour.", + options: [ + { + label: "Disabled", + value: "0", + }, + { + label: "Enabled", + value: "1", + }, + { + label: "Anonymous", + value: "2", + }, + ], + }, + scheduleDateTime: { + type: "string", + label: "Schedule Date Time", + description: "Format: June 15, 2021 6:05pm. Minutes in increments of '5', eg. 6pm, 6:05pm, 6:10pm, 6:15pm..", + optional: true, + }, + scheduleTimezone: { + type: "string", + label: "Schedule Timezone", + description: "e.g., 'America/New_York'. See the [list of PHP's supported timezones](https://www.php.net/manual/en/timezones.php). This parameter only applies if you're scheduling your campaign with 'Schedule Date Time' parameter. Sendy will use your default timezone if this parameter is empty.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.sendy.createDraftCampaign({ + $, + data: { + from_name: this.fromName, + from_email: this.fromEmail, + reply_to: this.replyTo, + title: this.title, + subject: this.subject, + plain_text: this.plainText, + html_text: this.htmlText, + list_ids: this.listIds && parseObject(this.listIds), + segment_ids: this.segmentIds && parseObject(this.segmentIds), + exclude_list_ids: this.excludeListIds && parseObject(this.excludeListIds), + exclude_segment_ids: this.excludeSegmentIds && parseObject(this.excludeSegmentIds), + brand_id: this.brandId, + query_string: this.queryString, + track_opens: this.trackOpens, + track_clicks: this.trackClicks, + schedule_date_time: this.scheduleDateTime, + schedule_timezone: this.scheduleTimezone, + }, + }); + + $.export("$summary", "Draft campaign created successfully"); + return response; + }, +}; diff --git a/components/sendy/actions/unsubscribe-email/unsubscribe-email.mjs b/components/sendy/actions/unsubscribe-email/unsubscribe-email.mjs new file mode 100644 index 0000000000000..f345a34c2ee71 --- /dev/null +++ b/components/sendy/actions/unsubscribe-email/unsubscribe-email.mjs @@ -0,0 +1,48 @@ +import sendy from "../../sendy.app.mjs"; + +export default { + key: "sendy-unsubscribe-email", + name: "Unsubscribe Email", + description: "Removes a subscriber from a specified list. [See the documentation](https://sendy.co/api?app_path=https://sendy.email/dev2#unsubscribe)", + version: "0.0.1", + type: "action", + props: { + sendy, + brandId: { + propDefinition: [ + sendy, + "brandId", + ], + }, + listId: { + propDefinition: [ + sendy, + "listId", + ({ brandId }) => ({ + brandId, + }), + ], + }, + email: { + propDefinition: [ + sendy, + "email", + ], + }, + }, + async run({ $ }) { + const response = await this.sendy.removeSubscriber({ + $, + data: { + list: this.listId, + email: this.email, + boolean: true, + }, + }); + + $.export("$summary", response != 1 + ? response + : `Successfully unsubscribed email: ${this.email} from list: ${this.listId}`); + return response; + }, +}; diff --git a/components/sendy/common/utils.mjs b/components/sendy/common/utils.mjs new file mode 100644 index 0000000000000..154701004e72a --- /dev/null +++ b/components/sendy/common/utils.mjs @@ -0,0 +1,18 @@ +export const parseObject = (obj) => { + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + return JSON.parse(obj); + } + return obj; +}; diff --git a/components/sendy/package.json b/components/sendy/package.json index cc03d3306f132..c0b7adb2934b0 100644 --- a/components/sendy/package.json +++ b/components/sendy/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/sendy", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Sendy Components", "main": "sendy.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" } -} \ No newline at end of file +} + diff --git a/components/sendy/sendy.app.mjs b/components/sendy/sendy.app.mjs index c7976c1b5050e..2f3358b9f717d 100644 --- a/components/sendy/sendy.app.mjs +++ b/components/sendy/sendy.app.mjs @@ -1,11 +1,113 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "sendy", - propDefinitions: {}, + propDefinitions: { + brandId: { + type: "string", + label: "Brand ID", + description: "The ID of the brand.", + async options() { + const data = await this.listBrands(); + + return Object.keys(data).map(((key) => ({ + label: data[key].name, + value: data[key].id, + }))); + }, + }, + listId: { + type: "string", + label: "List ID", + description: "The ID of the list you want to subscribe a user.", + async options({ brandId }) { + const data = await this.listLists({ + data: { + brand_id: brandId, + }, + }); + + return Object.keys(data).map((key) => ({ + label: data[key].name, + value: data[key].id, + })); + }, + }, + email: { + type: "string", + label: "Email", + description: "The email address of the subscriber.", + }, + name: { + type: "string", + label: "Name", + description: "The subscriber's name.", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "The subscriber's country (2-letter code).", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `https://${this.$auth.domain}`; + }, + _data(data = {}) { + return { + ...data, + api_key: `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, data, ...otherOpts + }) { + return axios($, { + url: this._baseUrl() + path, + data: this._data(data), + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + ...otherOpts, + }); + }, + addOrUpdateSubscriber(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/subscribe", + ...opts, + }); + }, + listBrands(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/api/brands/get-brands.php", + ...opts, + }); + }, + listLists(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/api/lists/get-lists.php", + ...opts, + }); + }, + createDraftCampaign(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/api/campaigns/create.php", + ...opts, + }); + }, + removeSubscriber(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/unsubscribe", + ...opts, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/sensibo/README.md b/components/sensibo/README.md new file mode 100644 index 0000000000000..41c98826cf812 --- /dev/null +++ b/components/sensibo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Sensibo API provides programmatic access to Sensibo's smart air conditioning features, allowing you to control and monitor your climate devices. By integrating the Sensibo API with Pipedream, you can automate climate control, receive updates on environmental conditions, and interact with other apps for a smarter home or office setup. Create workflows that respond to triggers such as temperature changes, time schedules, or other app events, and manage your AC units efficiently. + +# Example Use Cases + +- **Smart Temperature Control**: Adjust your AC settings based on real-time temperature data from a weather API. If the outside temperature exceeds your comfort zone, Pipedream can trigger your Sensibo device to cool or heat your space before you arrive. + +- **Energy Saving Routine**: Create a workflow that turns off your Sensibo-controlled AC units during peak energy consumption hours. Link it with your calendar to ensure the AC is on only when meetings are scheduled in your smart office. + +- **Voice-Controlled AC**: Integrate Sensibo with a voice assistant platform like Google Assistant. Set up a Pipedream workflow that listens for voice commands and adjusts your AC settings accordingly, offering a hands-free way to manage your environment. diff --git a/components/sensibo/package.json b/components/sensibo/package.json new file mode 100644 index 0000000000000..9a9e4ed1f3907 --- /dev/null +++ b/components/sensibo/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/sensibo", + "version": "0.0.1", + "description": "Pipedream Sensibo Components", + "main": "sensibo.app.mjs", + "keywords": [ + "pipedream", + "sensibo" + ], + "homepage": "https://pipedream.com/apps/sensibo", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/sensibo/sensibo.app.mjs b/components/sensibo/sensibo.app.mjs new file mode 100644 index 0000000000000..8c07d91cc0200 --- /dev/null +++ b/components/sensibo/sensibo.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "sensibo", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/senta/README.md b/components/senta/README.md new file mode 100644 index 0000000000000..5db4b41644dff --- /dev/null +++ b/components/senta/README.md @@ -0,0 +1,11 @@ +# Overview + +The Senta API allows you to automate and integrate practice management tasks within the Senta platform. Leveraging Pipedream's capabilities, you can create workflows that streamline client onboarding, automate communication, task management, and synchronize data with other services. With Pipedream's serverless platform, these workflows can be triggered by various events, such as incoming emails, schedule timings, or actions from other apps, offering a seamless way to enhance your practice management operations. + +# Example Use Cases + +- **Client Onboarding Automation**: When a new client fills out a form on your website, trigger a Pipedream workflow to create a new client record in Senta. Then, automatically assign tasks to your team for the onboarding process, and send a welcome email to the client with all necessary information to start the engagement. + +- **Task Scheduling and Reminder System**: Use Pipedream to set up a workflow that connects with your Google Calendar. When a new task is assigned to a team member in Senta, it automatically creates a corresponding event in Google Calendar and sets reminders. This ensures that deadlines are met and tasks are completed on time. + +- **Invoice Management and Follow-ups**: Connect Senta to Xero or another accounting app available on Pipedream. When an invoice is due in Senta, automatically generate the invoice in the accounting software. If the invoice remains unpaid past its due date, the workflow can trigger follow-up emails to the client, ensuring timely payments. diff --git a/components/sentry/README.md b/components/sentry/README.md index d301772c8f736..54e10972748cb 100644 --- a/components/sentry/README.md +++ b/components/sentry/README.md @@ -1,18 +1,11 @@ # Overview -Sentry is an error tracking and monitoring platform that can be used to detect -and debug problems with applications. With the Sentry API, developers and -system administrators can build custom tools and applications using the Sentry -platform. +The Sentry API on Pipedream allows you to automate error tracking and responses in your applications. Sentry's robust issue tracking and release monitoring align with Pipedream's ability to craft custom workflows, enabling developers to connect error alerts to a plethora of other services for notifications, analysis, task management, and more. With Sentry's detailed diagnostic data, these workflows can help reduce downtime by triggering quick actions upon issue detection. -Using the Sentry API, here are some examples of the kinds of custom tools and -applications that can be built: +# Example Use Cases -- Debugging tool to detect and troubleshoot errors -- Application monitoring dashboard -- Alert system to send notifications when an error occurs -- Anomaly detection system to identify potential issues -- Automated testing system to quickly identify and correct bugs -- Event logging system to track system metrics and usage data -- Visualization tool to track the progress and performance over time of the - application or system +- **Automated Issue Alerting**: Trigger a workflow on Pipedream when Sentry reports a new issue or an increase in issue frequency. Use this to send real-time notifications through Slack, SMS via Twilio, or email through SendGrid to alert your development team immediately. + +- **Issue Management Integration**: Capture new Sentry issues and create corresponding tickets in project management tools like Jira or Trello. Enrich the tickets with stack traces, last occurrences, and user impact data from Sentry, ensuring the team has all the necessary context to tackle the problem. + +- **Performance Metric Reporting**: Generate periodic reports on performance metrics and error trends using Sentry's API. Feed this data into a Google Sheets document or a dashboard tool like Data Studio for visualization, enabling your team to analyze and respond to application performance insights over time. diff --git a/components/sentry/actions/list-issue-events/list-issue-events.mjs b/components/sentry/actions/list-issue-events/list-issue-events.mjs index e74831efe4922..2413379605ac0 100644 --- a/components/sentry/actions/list-issue-events/list-issue-events.mjs +++ b/components/sentry/actions/list-issue-events/list-issue-events.mjs @@ -4,7 +4,7 @@ export default { key: "sentry-list-issue-events", name: "List Issue Events", description: "Return a list of events bound to an issue. [See the docs here](https://docs.sentry.io/api/events/list-an-issues-events/)", - version: "0.1.0", + version: "0.1.1", type: "action", props: { sentry, diff --git a/components/sentry/actions/list-project-events/list-project-events.mjs b/components/sentry/actions/list-project-events/list-project-events.mjs index 5955e8f9faa91..18985b0774913 100644 --- a/components/sentry/actions/list-project-events/list-project-events.mjs +++ b/components/sentry/actions/list-project-events/list-project-events.mjs @@ -2,7 +2,7 @@ import app from "../../sentry.app.mjs"; export default { key: "sentry-list-project-events", - version: "0.1.0", + version: "0.1.1", type: "action", name: "List Project Events.", description: "Return a list of events bound to a project. [See the docs here](https://docs.sentry.io/api/events/list-a-projects-events/)", diff --git a/components/sentry/actions/list-project-issues/list-project-issues.mjs b/components/sentry/actions/list-project-issues/list-project-issues.mjs index a1498f37b8569..db000832e75e9 100644 --- a/components/sentry/actions/list-project-issues/list-project-issues.mjs +++ b/components/sentry/actions/list-project-issues/list-project-issues.mjs @@ -2,7 +2,7 @@ import app from "../../sentry.app.mjs"; export default { key: "sentry-list-project-issues", - version: "0.1.0", + version: "0.1.1", type: "action", name: "List Project Issues.", description: "Return a list of issues bound to a project. [See the docs here](https://docs.sentry.io/api/issues/list-a-projects-issues/)", diff --git a/components/sentry/actions/update-issue/update-issue.mjs b/components/sentry/actions/update-issue/update-issue.mjs index 312ca7d5270b6..da4242b2b1d8b 100644 --- a/components/sentry/actions/update-issue/update-issue.mjs +++ b/components/sentry/actions/update-issue/update-issue.mjs @@ -3,7 +3,7 @@ import options from "../../options.mjs"; export default { key: "sentry-update-issue", - version: "0.1.0", + version: "0.1.1", type: "action", name: "Update Issue.", description: "Updates an individual issue's attributes. Only the attributes submitted are modified.[See the docs here](https://docs.sentry.io/api/events/update-an-issue/)", diff --git a/components/sentry/package.json b/components/sentry/package.json index 8ac0bf32d94ed..82696d35cc924 100644 --- a/components/sentry/package.json +++ b/components/sentry/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/sentry", - "version": "0.4.1", + "version": "0.4.2", "description": "Pipedream Sentry Components", "main": "sentry.app.js", "keywords": [ diff --git a/components/sentry/sentry.app.mjs b/components/sentry/sentry.app.mjs index 3776f210dad8b..4aaba40e053d9 100644 --- a/components/sentry/sentry.app.mjs +++ b/components/sentry/sentry.app.mjs @@ -172,7 +172,7 @@ export default { ...requestConfig, params, }; - } else if (prevContext.nextPage) { + } else if (String(prevContext.nextPage.results).toLowerCase() === "true") { // sentry returns this field as a string... so adding this in case they change it to a boolean // Retrieve next page of options. url = prevContext.nextPage.url; } else { diff --git a/components/sentry/sources/issue-event/issue-event.mjs b/components/sentry/sources/issue-event/issue-event.mjs index ea7b53c18e983..a9ccf0c362ccf 100644 --- a/components/sentry/sources/issue-event/issue-event.mjs +++ b/components/sentry/sources/issue-event/issue-event.mjs @@ -2,7 +2,7 @@ import sentry from "../../sentry.app.mjs"; export default { key: "sentry-issue-event", - version: "0.1.1", + version: "0.1.2", name: "New Issue Event (Instant)", description: "Emit new events for issues that have been created or updated.", type: "source", diff --git a/components/seqera/README.md b/components/seqera/README.md new file mode 100644 index 0000000000000..ac9d3b8d9293b --- /dev/null +++ b/components/seqera/README.md @@ -0,0 +1,11 @@ +# Overview + +The Seqera API serves as a connection point for handling complex data pipeline and workflow orchestration tasks within the life sciences domain. With this API, you can manage workflows, monitor pipeline executions, and control job submissions. Leveraging Pipedream, you can seamlessly integrate Seqera with various services to automate processes, react to pipeline events, or synchronize data across platforms, thus enhancing efficiency in bioinformatics analysis. + +# Example Use Cases + +- **Automated Workflow Management**: Use Pipedream to trigger a Seqera workflow when new data is uploaded to a cloud storage service like AWS S3. Once the analysis is complete, Pipedream can notify the team via Slack or email with the results or any issues encountered during execution. + +- **Real-time Monitoring and Alerts**: Set up a Pipedream workflow to monitor the status of Seqera jobs. When a job fails or completes, Pipedream can post a message in a specified Slack channel or send an SMS via Twilio, keeping the team updated in real-time. + +- **Data Integration and Reporting**: Create a Pipedream workflow that retrieves results from completed Seqera workflows and integrates them with data visualization tools like Google Sheets or Tableau. This can help in creating automated reports or dashboards for quick analysis and decision-making. diff --git a/components/seqera/actions/create-action/create-action.mjs b/components/seqera/actions/create-action/create-action.mjs index e87320d3ec6ec..c28c9f15a7876 100644 --- a/components/seqera/actions/create-action/create-action.mjs +++ b/components/seqera/actions/create-action/create-action.mjs @@ -6,7 +6,7 @@ export default { key: "seqera-create-action", name: "Create Pipeline Action", description: "Creates a new pipeline action in Seqera. [See the documentation](https://docs.seqera.io/platform/23.3.0/api/overview)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { app, diff --git a/components/seqera/actions/create-compute-environment/create-compute-environment.mjs b/components/seqera/actions/create-compute-environment/create-compute-environment.mjs index 66de51b4a7c74..7947f43f6b98f 100644 --- a/components/seqera/actions/create-compute-environment/create-compute-environment.mjs +++ b/components/seqera/actions/create-compute-environment/create-compute-environment.mjs @@ -5,7 +5,7 @@ export default { key: "seqera-create-compute-environment", name: "Create Compute Environment", description: "Creates a new compute environment in Seqera Tower. [See the documentation](https://docs.seqera.io/platform/23.3.0/api/overview)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { app, diff --git a/components/seqera/actions/create-pipeline/create-pipeline.mjs b/components/seqera/actions/create-pipeline/create-pipeline.mjs index bc9acb85fa5e5..b9dc787f2308e 100644 --- a/components/seqera/actions/create-pipeline/create-pipeline.mjs +++ b/components/seqera/actions/create-pipeline/create-pipeline.mjs @@ -5,7 +5,7 @@ export default { key: "seqera-create-pipeline", name: "Create Pipeline", description: "Creates a new pipeline in a user context. [See the documentation](https://docs.seqera.io/platform/23.3.0/api/overview)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { app, diff --git a/components/seqera/package.json b/components/seqera/package.json index adf038d81abe6..361247492f40b 100644 --- a/components/seqera/package.json +++ b/components/seqera/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/seqera", - "version": "0.1.0", + "version": "0.1.1", "description": "Pipedream Seqera Components", "main": "seqera.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/seqera/seqera.app.mjs b/components/seqera/seqera.app.mjs index 7146bb522b90c..03f2781fa6901 100644 --- a/components/seqera/seqera.app.mjs +++ b/components/seqera/seqera.app.mjs @@ -165,9 +165,11 @@ export default { ...args, }); }, - listRuns(args = {}) { + listWorkflows({ + workspaceId, ...args + }) { return this._makeRequest({ - path: "/ga4gh/wes/v1/runs", + path: `/workflow?workspaceId=${workspaceId}`, ...args, }); }, @@ -177,18 +179,18 @@ export default { resourceName, max = constants.DEFAULT_MAX, }) { - let nextPageToken; + const params = { + ...resourcesFnArgs?.params, + max: constants.DEFAULT_LIMIT, + offset: 0, + }; let resourcesCount = 0; while (true) { const response = await resourcesFn({ ...resourcesFnArgs, - params: { - ...resourcesFnArgs?.params, - page_size: constants.DEFAULT_LIMIT, - page_token: nextPageToken, - }, + params, }); const nextResources = resourceName && response[resourceName] || response; @@ -207,12 +209,12 @@ export default { } } - if (Number(response.next_page_token) === 0) { + if (resourcesCount >= response.totalSize) { console.log("No more pages found"); return; } - nextPageToken = response.next_page_token; + params.offset += params.max; } }, paginate(args = {}) { diff --git a/components/seqera/sources/new-run-created/new-run-created.mjs b/components/seqera/sources/new-run-created/new-run-created.mjs index 8e95d55f3a6de..97b55910edce7 100644 --- a/components/seqera/sources/new-run-created/new-run-created.mjs +++ b/components/seqera/sources/new-run-created/new-run-created.mjs @@ -5,7 +5,7 @@ export default { key: "seqera-new-run-created", name: "New Run Created", description: "Emit new event when a new run is created in Seqera. [See the documentation](https://docs.seqera.io/platform/23.3.0/api/overview)", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "unique", props: { @@ -18,22 +18,31 @@ export default { intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, }, }, + workspaceId: { + propDefinition: [ + app, + "workspaceId", + ], + optional: false, + }, }, methods: { getResourceName() { - return "runs"; + return "workflows"; }, getResourcesFn() { - return this.app.listRuns; + return this.app.listWorkflows; }, getResourcesFnArgs() { - return; + return { + workspaceId: this.workspaceId, + }; }, - generateMeta(resource) { + generateMeta({ workflow }) { return { - id: resource.run_id, - summary: `New Run: ${resource.run_id}`, - ts: Date.now(), + id: workflow.id, + summary: `New Run: ${workflow.id}`, + ts: Date.parse(workflow.dateCreated), }; }, processResource(resource) { diff --git a/components/serpapi/README.md b/components/serpapi/README.md new file mode 100644 index 0000000000000..6f7fb800205c3 --- /dev/null +++ b/components/serpapi/README.md @@ -0,0 +1,11 @@ +# Overview + +SerpApi is a powerful tool that scrapes search engine data, bypassing the need to handle the complexity of managing proxies or parsing HTML. With SerpApi, you can extract structured data from Google, Bing, Yahoo, and other search engines in real-time. This makes it invaluable for SEO analysis, market research, and competitive intelligence. When used with Pipedream, SerpApi can automate monitoring of search engine results, track ranking changes, and integrate this data into numerous applications like CRMs, marketing platforms, or custom databases. + +# Example Use Cases + +- **SEO Performance Dashboard**: Automatically fetch daily search results for specific keywords from SerpApi and send this data to Google Sheets using Pipedream. Use this setup to create a live SEO performance dashboard that updates with new ranking data, enabling continuous monitoring of keyword positions across various search engines. + +- **Competitive Analysis Alerts**: Set up a Pipedream workflow where SerpApi monitors search results for competitor names or products. Integrate with Slack or email through Pipedream to send automatic alerts when there are significant changes in the SERPs, such as a new competitor entering top search results or changes in ad placements. + +- **Market Trend Analysis**: Use SerpApi to extract search trends and related queries data, then send this information to a data visualization tool like Tableau or Power BI through Pipedream. This workflow enables businesses to spot emerging trends, understand market desires, and adjust marketing strategies accordingly by analyzing shifts in search query popularity and patterns. diff --git a/components/serpapi/actions/scrape-search/scrape-search.mjs b/components/serpapi/actions/scrape-search/scrape-search.mjs new file mode 100644 index 0000000000000..493d9a7550c5a --- /dev/null +++ b/components/serpapi/actions/scrape-search/scrape-search.mjs @@ -0,0 +1,55 @@ +import app from "../../serpapi.app.mjs"; + +export default { + key: "serpapi-scrape-search", + name: "Scrape Search", + description: "Scrape the results from a search engine via SerpApi service. [See the documentation](https://serpapi.com/search-api)", + version: "0.0.3", + type: "action", + props: { + app, + engine: { + propDefinition: [ + app, + "engine", + ], + }, + q: { + propDefinition: [ + app, + "q", + ], + }, + device: { + propDefinition: [ + app, + "device", + ], + }, + noCache: { + propDefinition: [ + app, + "noCache", + ], + }, + }, + async run({ $ }) { + const response = await this.app.scrapeSearch({ + $, + params: { + engine: this.engine, + }, + data: { + q: this.q, + device: this.device, + no_cache: Boolean(this.noCache) === true + ? "true" + : "false", + }, + }); + + $.export("$summary", `Successfully sent query to '${this.engine}'`); + + return response; + }, +}; diff --git a/components/serpapi/common/constants.mjs b/components/serpapi/common/constants.mjs new file mode 100644 index 0000000000000..85fef50a5a507 --- /dev/null +++ b/components/serpapi/common/constants.mjs @@ -0,0 +1,15 @@ +export default { + ENGINES: [ + "google", + "google_images", + "google_news", + "google_trends", + "baidu", + "bing", + ], + DEVICES: [ + "desktop", + "tablet", + "mobile", + ], +}; diff --git a/components/serpapi/package.json b/components/serpapi/package.json new file mode 100644 index 0000000000000..58fab8d394f61 --- /dev/null +++ b/components/serpapi/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/serpapi", + "version": "0.1.2", + "description": "Pipedream SerpApi Components", + "main": "serpapi.app.mjs", + "keywords": [ + "pipedream", + "serpapi" + ], + "homepage": "https://pipedream.com/apps/serpapi", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/serpapi/serpapi.app.mjs b/components/serpapi/serpapi.app.mjs new file mode 100644 index 0000000000000..843d5a422c00e --- /dev/null +++ b/components/serpapi/serpapi.app.mjs @@ -0,0 +1,58 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "serpapi", + propDefinitions: { + engine: { + type: "string", + label: "Engine", + description: "Engine to send the query to", + options: constants.ENGINES, + }, + q: { + type: "string", + label: "Query", + description: "The query you want to search. You can use anything that you would use in a regular Google search. e.g. `inurl:`, `site:`, `intitle:`. SerpApi also supports advanced search query parameters such as `as_dt` and `as_eq`. See the [full list](https://serpapi.com/advanced-google-query-parameters) of supported advanced search query parameters.", + }, + device: { + type: "string", + label: "Device", + description: "Defines the device to use to get the results", + options: constants.DEVICES, + }, + noCache: { + type: "boolean", + label: "No Cache", + description: "Force SerpApi to fetch the Google results even if a cached version is already present", + }, + }, + methods: { + _baseUrl() { + return "https://serpapi.com"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + params: { + ...params, + api_key: this.$auth.api_key, + }, + }); + }, + async scrapeSearch(args = {}) { + return this._makeRequest({ + path: "/search", + ...args, + }); + }, + }, +}; diff --git a/components/serpdog/README.md b/components/serpdog/README.md new file mode 100644 index 0000000000000..460eb7fd31b65 --- /dev/null +++ b/components/serpdog/README.md @@ -0,0 +1,11 @@ +# Overview + +The Serpdog API lets you track search engine rankings for keywords across different regions and devices. Using this API on Pipedream, you can automate SEO monitoring tasks, integrate ranking data into reports, or trigger actions based on ranking changes. Pipedream's serverless platform facilitates the creation of workflows using Serpdog's data, without the need for a dedicated server or complex infrastructure setup. + +# Example Use Cases + +- **Daily SEO Report Generation**: Build a Pipedream workflow that triggers daily, fetching current rankings for your tracked keywords from Serpdog. Combine this data with other metrics from Google Analytics using the Google Analytics API, and send a comprehensive SEO performance report to Slack or via email. + +- **Alerts for Ranking Changes**: Set up a Pipedream workflow that checks Serpdog for keyword ranking changes at regular intervals. If significant changes are detected, such as a drop out of the top 10 results, trigger a notification to a specified Discord channel using the Discord Webhook app on Pipedream to quickly inform your SEO team. + +- **Sync Ranking Data with a Database**: Create a Pipedream workflow that periodically calls the Serpdog API to retrieve the latest ranking information. Store this data in a PostgreSQL database by connecting to the PostgreSQL app on Pipedream. Use this historical data to analyze trends and make informed decisions about your SEO strategy. diff --git a/components/serpdog/package.json b/components/serpdog/package.json new file mode 100644 index 0000000000000..5623fbfeb38bf --- /dev/null +++ b/components/serpdog/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/serpdog", + "version": "0.0.1", + "description": "Pipedream Serpdog Components", + "main": "serpdog.app.mjs", + "keywords": [ + "pipedream", + "serpdog" + ], + "homepage": "https://pipedream.com/apps/serpdog", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/serpdog/serpdog.app.mjs b/components/serpdog/serpdog.app.mjs new file mode 100644 index 0000000000000..f82747bac26bb --- /dev/null +++ b/components/serpdog/serpdog.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "serpdog", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/serphouse/README.md b/components/serphouse/README.md new file mode 100644 index 0000000000000..4d151e90c8c0d --- /dev/null +++ b/components/serphouse/README.md @@ -0,0 +1,11 @@ +# Overview + +The SERPhouse API provides a way to fetch real-time search engine results pages (SERPs) from Google and Bing. With it, you can gather SEO data, monitor keyword rankings, and gain insights into search trends. On Pipedream, you can leverage this API to build serverless workflows that automate SEO tasks, conduct competitive analysis, and integrate with other apps to enhance your digital marketing strategies. + +# Example Use Cases + +- **Track Keyword Rankings**: Set up a scheduled workflow on Pipedream that uses the SERPhouse API to check the rankings of your website's keywords daily. Collect this data in a Google Sheets document for easy tracking and visualization of your SEO performance over time. + +- **Monitor Competitor SEO Strategies**: Create a workflow that detects changes in SERP features (like Featured Snippets, People Also Ask, etc.) for competitor keywords. When changes are detected, send an alert via Slack to keep your marketing team informed and reactive to shifts in the SEO landscape. + +- **Analyze Search Trends for Content Creation**: Use SERPhouse API to fetch popular queries and topics related to your business. Pipe this data into an AI text generation service, like OpenAI's GPT-3 on Pipedream, to generate content ideas or even draft initial content outlines based on trending search terms. diff --git a/components/serphouse/package.json b/components/serphouse/package.json new file mode 100644 index 0000000000000..f493623ac967c --- /dev/null +++ b/components/serphouse/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/serphouse", + "version": "0.0.1", + "description": "Pipedream SERPhouse Components", + "main": "serphouse.app.mjs", + "keywords": [ + "pipedream", + "serphouse" + ], + "homepage": "https://pipedream.com/apps/serphouse", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/serphouse/serphouse.app.mjs b/components/serphouse/serphouse.app.mjs new file mode 100644 index 0000000000000..6db701d8211f1 --- /dev/null +++ b/components/serphouse/serphouse.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "serphouse", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/serply/README.md b/components/serply/README.md new file mode 100644 index 0000000000000..5645dcc842c42 --- /dev/null +++ b/components/serply/README.md @@ -0,0 +1,11 @@ +# Overview + +The Serply API offers a way to automate SEO tasks by providing data about search engine results pages (SERPs). It enables you to retrieve search results, rankings, and related keywords, which can be essential for SEO analysis and content strategy. On Pipedream, you can leverage Serply to create powerful workflows that trigger on various events, process SERP data, and integrate with other services for a seamless automation experience. + +# Example Use Cases + +- **Content Trend Monitoring**: Using Serply with Pipedream, you can monitor specific keywords for trends and changes in search engine results. Set up a scheduled workflow that fetches daily SERP data for targeted keywords and sends a summary report via email or Slack, keeping your content team in the loop. + +- **Competitor SERP Position Tracking**: Create a workflow where Serply checks the SERP positions of your competitor's domains for key search terms. With this data, trigger actions like updating a Google Sheet for historical tracking or generating alerts if significant changes are detected. + +- **Keyword Research and Expansion**: Pair Serply with a Pipedream workflow that analyzes your website's content to find new keyword opportunities. Use Serply to get suggestions based on current high-performing keywords and automatically add them to a content planning tool like Trello or Asana for review. diff --git a/components/serveravatar/README.md b/components/serveravatar/README.md index 6090388298fa7..afaed8f0703bb 100644 --- a/components/serveravatar/README.md +++ b/components/serveravatar/README.md @@ -1,18 +1,14 @@ # Overview -ServerAvatar is a powerful API that can be used to build a wide range of apps. -With ServerAvatar, developers can easily create their own servers, configure -them, and manage their app infrastructure. +The ServerAvatar API lets you manage and automate server operations, catering to developers and system admins who seek efficiency in server management tasks. Whether it's deploying new sites, keeping tabs on server health, or automating server updates, the API opens a window to streamline such processes with your own systems or third-party apps. With Pipedream, you can connect ServerAvatar to other services, triggering actions in one app based on events from ServerAvatar, or vice versa. -ServerAvatar comes with a comprehensive set of resources that make it easy and -intuitive to develop applications and configure their settings. Here are a few -examples of the things that you can build with the ServerAvatar API: +# Example Use Cases -- Website hosting systems -- Database and file storage -- Security systems -- Network monitoring & automation -- Load balancing & scaling -- Continuous deployment -- Automated backups -- Remote management & access +- **Automated Server Health Monitoring** + Monitor your server's health by setting up a workflow that triggers whenever ServerAvatar sends an alert. This can be connected to communication apps like Slack or email services to instantly notify your team, ensuring prompt attention to server issues. + +- **Dynamic Resource Scaling** + Create a workflow that responds to ServerAvatar's server load metrics, automatically adjusting resource allocation on cloud platforms like AWS or Azure. This ensures optimal performance during traffic spikes without human intervention. + +- **Continuous Deployment Pipeline** + Integrate ServerAvatar with GitHub via Pipedream to trigger deployments whenever a new commit is pushed to the master branch. This workflow can automate testing and deployment processes, keeping your live sites up-to-date with the latest changes. diff --git a/components/servicem8/README.md b/components/servicem8/README.md index edc7006c3fd7a..26c0a45485e39 100644 --- a/components/servicem8/README.md +++ b/components/servicem8/README.md @@ -1,17 +1,11 @@ # Overview -Using ServiceM8 API, you can build powerful, business-critical apps that -benefit your ServiceM8 customers. The API allows you to quickly connect with -the ServiceM8 platform to build apps, generate reports, and integrate with -third-party services. Here are some examples of what can be built using the -ServiceM8 API: +The ServiceM8 API allows businesses to streamline their field service management by automating tasks and integrating with other tools. Through Pipedream, you can harness this API to create custom workflows that trigger actions within ServiceM8 or sync data with other apps. Automate job scheduling, dispatching, invoicing, and more by reacting to events in real-time. Enhance productivity by connecting ServiceM8 to CRMs, accounting software, or custom databases, ensuring consistent and updated information across platforms. -- Automated job scheduling and management. -- Connect with other systems such as Xero, MYOB, and Salesforce. -- Create custom rules that trigger notifications and actions. -- Create a single view of customer information. -- Generate powerful, real-time insights and reports. -- Integrate with Machine Learning and AI services. -- Integrate with real-time analytics tools. -- Automated customer notifications and updates. -- Automated invoicing and payment collection. +# Example Use Cases + +- **Job Status to Slack Notifications**: Whenever a job status is updated in ServiceM8, trigger a Pipedream workflow that sends a customized message to a designated Slack channel. This keeps teams immediately informed about job progress without manually checking the ServiceM8 app. + +- **Automated Invoicing with QuickBooks**: Create a workflow that detects job completion in ServiceM8 and automatically generates an invoice in QuickBooks. This eliminates the need to manually enter data, reducing errors, and accelerating the billing cycle. + +- **Dynamic Scheduling with Google Calendar**: Sync ServiceM8 job bookings with a Google Calendar, updating the calendar in real-time when new jobs are scheduled or existing ones are modified. This improves coordination and ensures that all stakeholders have visibility into the schedule. diff --git a/components/servicenow/README.md b/components/servicenow/README.md index 13e126b78160d..7f98c15ad2499 100644 --- a/components/servicenow/README.md +++ b/components/servicenow/README.md @@ -1,76 +1,61 @@ # Overview -Using the ServiceNow API, you can build a variety of powerful applications that -help you extend and enhance the capabilities of your ServiceNow implementation. -The possibilities are endless! You can: - -- Create custom pages and widgets that provide additional functionality and - data visualizations tailored to your needs -- Automate user and business processes with the help of integration tools -- Streamline production deployments and drive up efficiency with workflow - systems -- Develop custom plugins to facilitate data exchange with other systems -- Leverage machine learning to extract knowledge from large data sets and - create predictive models -- Create interactive chatbots for providing support or collecting feedback -- Generate custom reports for tracking performance and resource utilization -- And much more! - -No matter whether you are looking to extend existing features or build -something totally new, the ServiceNow API offers the perfect solution! +The ServiceNow API lets developers access and manipulate records, manage workflows, and integrate with other services on its IT service management platform. These capabilities support automating tasks, syncing data across platforms, and boosting operational efficiencies. # Getting Started -Before you can use the ServiceNow REST API from a workflow, you need to configure an OAuth app in your ServiceNow instance that will grant access tokens to your users and authenticate requests to its REST API. +Before using the ServiceNow REST API from a workflow, configure **two** OAuth apps in your ServiceNow instance. These apps will grant access tokens to your users and authenticate requests to its REST API. -1. In your ServiceNow instance, visit the **Application Registry** and create a new app, choosing the **Create an OAuth API endpoint for external clients** option. -2. Name it something memorable, then leave every other field blank or keep the defaults, except for the **Redirect URL**, which should be: `https://api.pipedream.com/connect/oauth/oa_g2oiqA/callback`. Your app should look something like this: +## Create an External Client OAuth App -
-ServiceNow OAuth app config -
+First, sign into your [ServiceNow Developer Portal](https://developer.servicenow.com/dev.do#!/home) account to create or access an instance. -1. Next, you'll need to copy the client ID and secret generated in **Step 2**, and add another app. This time, select the option to **Connect to a third party OAuth Provider**. -2. Name this app something like **Pipedream OAuth Validator**, and add the client ID / secret from **Step 2**. Change the grant type to **Authorization Code**, and set the **Token URL** to `oauth_token.do` (without any hostname, this refers to the current instance). Finally, add the same **Redirect URL** as you did above: `https://api.pipedream.com/connect/oauth/oa_g2oiqA/callback`. This app's configuration should look something like this when complete: +1. Go to **System OAuth > Application Registry**. -
-ServiceNow OAuth validator app config -
+ ![Find the OAuth Client option under the ServiceNow application registry](https://res.cloudinary.com/pipedreamin/image/upload/v1715264549/marketplace/apps/servicenow/CleanShot_2024-05-09_at_10.18.36_ntausg.png) -1. Visit [https://pipedream.com/accounts](https://pipedream.com/accounts), and click the button labeled **Click Here to Connect An App** in the top-right. In the modal that appears, search for **ServiceNow** and select it. You'll be prompted to enter the client ID and client secret from **Step 2** above, as well as the name of your instance. The instance name is the _host_ portion of your instance's URL: that is, the `dev98042` in `https://dev98042.service-now.com/`. +2. Create a new app by selecting **New** in the top right corner. -
-Pipedream app config -
+ ![Create a new ServiceNow application under the OAuth Clients section in the Application Registry](https://res.cloudinary.com/pipedreamin/image/upload/v1715265062/marketplace/apps/servicenow/CleanShot_2024-05-09_at_10.30.51_jpi4ct.png) -6. Press **Connect** in the bottom-right of the modal. This should open up a new window asking you to login to your ServiceNow instance. This authorizes Pipedream's access to your ServiceNow account, and you should be ready to connect to your instance's REST API! +3. Choose **Create an OAuth API endpoint for external clients**: -Collectively, the two apps you configured in your ServiceNow instance allow your instance to issue new OAuth access tokens for the user who authenticated in **Step 6**. This allows Pipedream to retrieve a fresh access token before it makes requests to the ServiceNow REST API. + ![Create a new app, and make sure to choose the OAuth API endpoint for external clients option](https://res.cloudinary.com/pipedreamin/image/upload/v1715264615/marketplace/apps/servicenow/CleanShot_2024-05-09_at_10.19.09_pgezqf.png) -## ServiceNow Authorization Reference +4. Name your app, such as `Pipedream`. Use the default settings but specify the **Redirect URL**: `https://api.pipedream.com/connect/oauth/oa_g2oiqA/callback`. + +5. Click **Create**. It will appear in the Application Registry once created. + + ![You should see the Pipedream app listed in the ServiceNow Registry after making those changes](https://res.cloudinary.com/pipedreamin/image/upload/v1715264960/marketplace/apps/servicenow/CleanShot_2024-05-09_at_10.21.12_iwlxgq.png) + +### Create the OAuth Validator app -[This ServiceNow doc](https://docs.servicenow.com/bundle/orlando-platform-administration/page/administer/security/concept/c_OAuthAuthorizationCodeFlow.html) describes the general flow we ask you to implement above. In that doc, the app you create in **Step 2** is referred to as the **client application**, and the app in **Step 4** is referred to as the **OAuth provider application registry record**. +1. Copy the client ID and secret from the `Pipedream` app you created above. +2. Name this app `Pipedream OAuth Validator` and add the previously copied client ID and secret. +3. Set the grant type to **Authorization Code** and the **Token URL** to `oauth_token.do`. +4. Use the same **Redirect URL** as before. -## Additional Guidance For Hardened or Mature Instances ### +5. Visit [Pipedream's account page](https://pipedream.com/accounts), and click **Click Here to Connect An App**. Search for **ServiceNow** and select it. Enter the client ID, client secret, and your instance name (e.g., `dev98042` from `https://dev98042.service-now.com/`). -The instructions above are likely to work on a fresh, out-of-the-box instance but may work imperfectly on ServiceNow instances that have been customized or have applied various security hardening recommendations such as the [explicit roles plugin](https://docs.servicenow.com/en-US/bundle/vancouver-platform-security/page/administer/security/reference/explicit-role-plugin.html). +6. Press **Connect**. A new window will prompt you to login to your ServiceNow instance, authorizing Pipedream's access to the ServiceNow REST API. -Symptoms of problems here may include getting a **504 Gateway Time-out** error when completing step 6 above. If you manually test the connection deatails in a tool like Postman, you may get an error like this: +## ServiceNow Authorization Reference + +[This ServiceNow doc](https://docs.servicenow.com/bundle/orlando-platform-administration/page/administer/security/concept/c_OAuthAuthorizationCodeFlow.html) outlines the flow you should implement. -``` -{ - "error_description":"access_denied", - "error":"server_error" -} -``` +## Additional Guidance For Hardened or Mature Instances -In these instances, the following tips may be helpful: +The standard instructions may not apply perfectly to customized or hardened ServiceNow instances. If you face a **504 Gateway Time-out** error or similar, consider these tips: -* Create a dedicated role for this purpose, and assign it to a service account that is only used for this purpose. You should not set it for web service access only, since interactive access is required to complete Pipedream setup. -* Ensure that the dedicated role has ACLs configured to allow read for the oauth_credential table - both the table and table.\* for all fields. -* Assign snc_internal to this service account. This is important if you are using the explicit roles plugin as part of instance security hardening. +* Assign a dedicated role and service account for this integration. +* Ensure the role has ACLs configured for the `oauth_credential` table and other necessary tables. -Finally, while not required, you should also check that the role has associated ACLs for any tables you want to work with; by default they may if you use snc_internal, but some fields extended from task or other tables may require additional ACLs based on your instance's configuration. +# Example Use Cases + +- **Incident Management Automation**: Automatically create incidents in ServiceNow from alerts in Datadog or New Relic. +- **HR Onboarding Workflow**: Trigger a Pipedream workflow to set up new employee accounts in ServiceNow from HR systems like Workday. +- **Customer Support Ticket Sync**: Keep customer support tickets synced between ServiceNow and CRM platforms like Salesforce. # Troubleshooting -If you're getting a **504 Gateway Time-out** error when attempting to connect your ServiceNow account, review the section above on "Additional Guidance For Hardened or Mature Instances". + +If you encounter a **504 Gateway Time-out** error, refer to the 'Additional Guidance For Hardened or Mature Instances' section for solutions. diff --git a/components/servicenow/package.json b/components/servicenow/package.json new file mode 100644 index 0000000000000..13cbbe37ea6df --- /dev/null +++ b/components/servicenow/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/servicenow", + "version": "0.6.0", + "description": "Pipedream servicenow Components", + "main": "servicenow.app.mjs", + "keywords": [ + "pipedream", + "servicenow" + ], + "homepage": "https://pipedream.com/apps/servicenow", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/serwersms_pl/README.md b/components/serwersms_pl/README.md new file mode 100644 index 0000000000000..3eb47e31f46cd --- /dev/null +++ b/components/serwersms_pl/README.md @@ -0,0 +1,11 @@ +# Overview + +The SerwerSMS.pl API enables integration of robust SMS messaging capabilities into your applications or workflows. With this API, you can send text messages, manage contacts, receive delivery reports, and automate responses based on incoming messages. On Pipedream, you can harness the SerwerSMS.pl API to create event-driven workflows, automating SMS-based interactions with efficiency and ease. By connecting to various apps available on Pipedream, you can design powerful, multi-step automations that react to triggers and perform actions like sending alerts, notifications, or automated marketing campaigns. + +# Example Use Cases + +- **Automated Customer Support Notifications**: Trigger an SMS to customers using the SerwerSMS.pl API when a new ticket is created in Zendesk. Automate follow-ups or status updates based on ticket progress, ensuring customers are kept informed. + +- **E-Commerce Order Alerts**: Send order confirmation and dispatch alerts via SMS when a new order is placed on Shopify. Use SerwerSMS.pl to keep customers updated about their order status, providing a better shopping experience. + +- **Event-Driven Marketing Campaigns**: Deploy time-sensitive promotions or messages by connecting the SerwerSMS.pl API with a calendar service like Google Calendar. Send SMS reminders for upcoming events or sales, enhancing customer engagement. diff --git a/components/sessions/README.md b/components/sessions/README.md new file mode 100644 index 0000000000000..ae3a8ffeaa8d4 --- /dev/null +++ b/components/sessions/README.md @@ -0,0 +1,11 @@ +# Overview + +The Sessions API enables developers to handle real-time data streams and manage user sessions effectively. With Pipedream's power to integrate various services, the Sessions API can be the backbone of workflows that require managing user states across events. You can track user activities, tailor user experiences based on session data, or even trigger specific actions when certain session events occur. + +# Example Use Cases + +- **Real-time User Activity Dashboard**: Create a workflow that taps into the Sessions API to feed user session data into a live dashboard app like Geckoboard or Databox. This can provide instant insights into user behavior, helping businesses to make data-driven decisions rapidly. + +- **Personalized User Engagement**: Harness the Sessions API within a Pipedream workflow to detect when a user's session starts or ends. Combine this with the Twilio app to send personalized SMS messages or notifications, encouraging continued engagement or offering timely support. + +- **Session-Based Access Control**: Construct a workflow that utilizes the Sessions API to monitor user session states and integrates with a security service like Auth0. This can ensure that user access is appropriately granted or revoked in real time, enhancing security and user management. diff --git a/components/setmoreappointments/README.md b/components/setmoreappointments/README.md new file mode 100644 index 0000000000000..8fa31e1733546 --- /dev/null +++ b/components/setmoreappointments/README.md @@ -0,0 +1,11 @@ +# Overview + +The Setmore API opens doors to automating appointment scheduling and calendar management, allowing you to sync appointments, customer information, and services across different platforms. This seamless integration through Pipedream can trigger workflows from new appointments, update external CRMs with customer data, or prep team communication channels with upcoming schedule details. By harnessing the Setmore API on Pipedream, you're equipping yourself to orchestrate complex operations with simplicity and precision. + +# Example Use Cases + +- **Automatic Appointment Confirmation Emails**: When a new appointment is scheduled in Setmore, use Pipedream to send a customized confirmation email to the customer. This can be done by integrating with email services like Sendgrid or Mailgun, providing immediate communication and enhanced customer experience. + +- **Syncing Appointments to Google Calendar**: Keep your Google Calendar in sync with Setmore appointments. Whenever a new appointment gets booked or an existing one is updated, Pipedream can automate the process of reflecting these changes in your Google Calendar, ensuring that your schedule is always up-to-date across platforms. + +- **CRM Integration for New Customers**: Integration with a CRM system like Salesforce or HubSpot can be automated when a new customer is added in Setmore. Pipedream can capture the customer details and push them to your CRM, maintaining a unified database for sales and support teams. diff --git a/components/setmoreappointments/actions/create-appointment/create-appointment.mjs b/components/setmoreappointments/actions/create-appointment/create-appointment.mjs index aaefca43b0f69..23c657fd67cdf 100644 --- a/components/setmoreappointments/actions/create-appointment/create-appointment.mjs +++ b/components/setmoreappointments/actions/create-appointment/create-appointment.mjs @@ -5,7 +5,7 @@ export default { key: "setmoreappointments-create-appointment", name: "Create Appointment", description: "Create a new appointment in Setmore Appointments. [See the documentation](https://setmore.docs.apiary.io/#introduction/appointments/create-an-appointment)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { setmore, diff --git a/components/setmoreappointments/actions/create-customer/create-customer.mjs b/components/setmoreappointments/actions/create-customer/create-customer.mjs index 5b69f912f38d4..977879ee5b692 100644 --- a/components/setmoreappointments/actions/create-customer/create-customer.mjs +++ b/components/setmoreappointments/actions/create-customer/create-customer.mjs @@ -4,7 +4,7 @@ export default { key: "setmoreappointments-create-customer", name: "Create Customer", description: "Create a new customer in Setmore Appointments. [See the documentation](https://setmore.docs.apiary.io/#introduction/customers/create-a-customer)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { setmore, diff --git a/components/setmoreappointments/package.json b/components/setmoreappointments/package.json index c45b719705ff2..123b8b84031bd 100644 --- a/components/setmoreappointments/package.json +++ b/components/setmoreappointments/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/setmoreappointments", - "version": "0.1.0", + "version": "0.2.0", "description": "Pipedream Setmore Components", "main": "setmoreappointments.app.mjs", "keywords": [ @@ -13,6 +13,6 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.5.1" + "@pipedream/platform": "^3.0.3" } } diff --git a/components/setmoreappointments/setmoreappointments.app.mjs b/components/setmoreappointments/setmoreappointments.app.mjs index e6a2cc451fa77..329c14654e296 100644 --- a/components/setmoreappointments/setmoreappointments.app.mjs +++ b/components/setmoreappointments/setmoreappointments.app.mjs @@ -89,6 +89,12 @@ export default { ...args, }); }, + listAppointments(args = {}) { + return this._makeRequest({ + path: "/appointments", + ...args, + }); + }, createCustomer(args = {}) { return this._makeRequest({ path: "/customer/create", @@ -103,5 +109,21 @@ export default { ...args, }); }, + async *paginate({ + resourceFn, + params, + resourceKey, + }) { + do { + const { data } = await resourceFn({ + params, + }); + const items = data[resourceKey]; + for (const item of items) { + yield item; + } + params.cursor = data.cursor; + } while (params.cursor); + }, }, }; diff --git a/components/setmoreappointments/sources/new-appointment-created/new-appointment-created.mjs b/components/setmoreappointments/sources/new-appointment-created/new-appointment-created.mjs new file mode 100644 index 0000000000000..e6acf8404d214 --- /dev/null +++ b/components/setmoreappointments/sources/new-appointment-created/new-appointment-created.mjs @@ -0,0 +1,88 @@ +import setmore from "../../setmoreappointments.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "setmoreappointments-new-appointment-created", + name: "New Appointment Created", + description: "Emit new event when a new appointment is created in Setmore. [See the documentation](https://setmore.docs.apiary.io/#introduction/appointments/fetch-appointments-by-date-range)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + setmore, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + staffKey: { + propDefinition: [ + setmore, + "staffKey", + ], + optional: true, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || this.getMonthAgo(); + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + getMonthAgo() { + const now = new Date(); + const oneMonthAgo = new Date(now); + oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1); + return this.formatDate(oneMonthAgo); + }, + getToday() { + const now = new Date(); + return this.formatDate(now); + }, + getYearFromNow() { + const now = new Date(); + now.setFullYear(now.getFullYear() + 1); + return this.formatDate(now); + }, + formatDate(date) { + const day = String(date.getDate()).padStart(2, "0"); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const year = date.getFullYear(); + return `${day}-${month}-${year}`; + }, + generateMeta(appointment) { + return { + id: appointment.key, + summary: `New Appointment with Key: ${appointment.key}`, + ts: Date.now(), + }; + }, + }, + async run() { + const startDate = this._getLastDate(); + const endDate = this.getYearFromNow(); + + const appointments = this.setmore.paginate({ + resourceFn: this.setmore.listAppointments, + resourceKey: "appointments", + params: { + startDate, + endDate, + staff_key: this.staffKey, + customerDetails: true, + }, + }); + + for await (const appointment of appointments) { + const meta = this.generateMeta(appointment); + this.$emit(appointment, meta); + } + + this._setLastDate(this.getToday()); + }, + sampleEmit, +}; diff --git a/components/setmoreappointments/sources/new-appointment-created/test-event.mjs b/components/setmoreappointments/sources/new-appointment-created/test-event.mjs new file mode 100644 index 0000000000000..cc39503ea6c42 --- /dev/null +++ b/components/setmoreappointments/sources/new-appointment-created/test-event.mjs @@ -0,0 +1,26 @@ +export default { + "key": "94eb4230-40d5-4651-b59a-0cccb7d9ab4d", + "duration": 15, + "customer": { + "key": "c291f522b55b2e4a4a579300cdd57455a4316873d", + "address": "", + "city": "", + "state": "", + "comment": "test", + "company_key": "c5247961-06b5-46e3-9cdb-e5187a02b67f", + "contact_type": "Customer", + "first_name": "Test", + "last_name": "Customer", + "email_id": "test@test.com", + "postal_code": "", + "image_url": "" + }, + "cost": 0, + "currency": "MXN", + "start_time": "2025-01-16T12:00Z", + "end_time": "2025-01-16T12:15Z", + "staff_key": "r91e68f3227a75e4b066822ea7e649709376a8244-d", + "service_key": "s8353404352c72fdc85ae4ebc786e4e74ec5c4e7c", + "customer_key": "c291f522b55b2e4a4a579300cdd57455a4316873d", + "label": "No Label" +} \ No newline at end of file diff --git a/components/sevdesk/README.md b/components/sevdesk/README.md new file mode 100644 index 0000000000000..7ae021d750165 --- /dev/null +++ b/components/sevdesk/README.md @@ -0,0 +1,11 @@ +# Overview + +The sevDesk API allows you to automate tasks around billing, accounting, and inventory management within the sevDesk platform. With it, you can create invoices, manage customers, and handle your accounting processes programmatically. When integrated into Pipedream workflows, you can connect sevDesk with other apps to streamline your financial operations, trigger actions based on events, and sync data across your business stack, enabling a seamless financial workflow automation. + +# Example Use Cases + +- **Automate Invoice Creation and Emailing**: When a new order is received in an e-commerce platform like Shopify, trigger a Pipedream workflow to create an invoice in sevDesk and email it to the customer automatically. This ensures timely billing and reduces manual effort. + +- **Sync Contacts Between CRM and sevDesk**: Keep your CRM, such as Salesforce, and sevDesk contact lists in sync. When a new contact is added to Salesforce, a Pipedream workflow can be triggered to create or update the contact details in sevDesk, ensuring consistent data across platforms. + +- **Financial Reporting and Notifications**: Set up a Pipedream workflow to generate periodic financial reports from sevDesk data and send them to services like Slack or email. This can help you keep your team informed about the company's financial health without manual intervention. diff --git a/components/sevdesk/actions/cancel-invoice/cancel-invoice.mjs b/components/sevdesk/actions/cancel-invoice/cancel-invoice.mjs new file mode 100644 index 0000000000000..d3d4274848c44 --- /dev/null +++ b/components/sevdesk/actions/cancel-invoice/cancel-invoice.mjs @@ -0,0 +1,31 @@ +import { ConfigurationError } from "@pipedream/platform"; +import sevdesk from "../../sevdesk.app.mjs"; + +export default { + key: "sevdesk-cancel-invoice", + name: "Cancel Invoice", + description: "Cancels an existing invoice in sevDesk. [See the documentation](https://api.sevdesk.de/#tag/Invoice/operation/cancelInvoice)", + version: "0.0.1", + type: "action", + props: { + sevdesk, + invoiceId: { + propDefinition: [ + sevdesk, + "invoiceId", + ], + }, + }, + async run({ $ }) { + try { + const response = await this.sevdesk.cancelInvoice({ + $, + invoiceId: this.invoiceId, + }); + $.export("$summary", `Successfully canceled invoice with ID ${this.invoiceId}`); + return response; + } catch ({ message }) { + throw new ConfigurationError(JSON.parse(message).error.message); + } + }, +}; diff --git a/components/sevdesk/actions/create-invoice/create-invoice.mjs b/components/sevdesk/actions/create-invoice/create-invoice.mjs new file mode 100644 index 0000000000000..c99a0aeaf6605 --- /dev/null +++ b/components/sevdesk/actions/create-invoice/create-invoice.mjs @@ -0,0 +1,246 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { + CURRENCY_OPTIONS, + INVOICE_TYPE_OPTIONS, + SEND_TYPE_OPTIONS, + STATUS_OPTIONS, + TAX_TYPE_OPTIONS, +} from "../../common/constants.mjs"; +import sevdesk from "../../sevdesk.app.mjs"; + +export default { + key: "sevdesk-create-invoice", + name: "Create Invoice", + description: "Creates a new invoice with optional details like invoice date, due date, discount amount, and invoice items. [See the documentation](https://api.sevdesk.de/)", + version: "0.0.1", + type: "action", + props: { + sevdesk, + invoiceNumber: { + type: "string", + label: "Invoice Number", + description: "The invoice number", + optional: true, + }, + contactId: { + propDefinition: [ + sevdesk, + "contactId", + ], + }, + invoiceDate: { + type: "string", + label: "Invoice Date", + description: "Needs to be provided as timestamp or dd.mm.yyyy", + }, + header: { + type: "string", + label: "Header", + description: "Normally consist of prefix plus the invoice number.", + optional: true, + }, + headText: { + type: "string", + label: "Head Text", + description: "Certain html tags can be used here to format your text.", + optional: true, + }, + footText: { + type: "string", + label: "Foot Text", + description: "Certain html tags can be used here to format your text.", + optional: true, + }, + timeToPay: { + type: "integer", + label: "Time To Pay", + description: "The time the customer has to pay the invoice in days.", + optional: true, + }, + discount: { + type: "integer", + label: "Discount", + description: "If you want to give a discount, define the percentage here. Otherwise provide zero as value.", + default: 0, + }, + address: { + type: "string", + label: "Address", + description: "Complete address of the recipient including name, street, city, zip and country. * Line breaks can be used and will be displayed on the invoice pdf.", + optional: true, + }, + addressCountryId: { + propDefinition: [ + sevdesk, + "addressCountryId", + ], + }, + payDate: { + type: "string", + label: "Pay Date", + description: "Needs to be timestamp or `dd.mm.yyyy`.", + optional: true, + }, + deliveryDate: { + type: "string", + label: "Delivery Date", + description: "Timestamp. This can also be a date range if you also use `Delivery Date Until`.", + optional: true, + }, + deliveryDateUntil: { + type: "string", + label: "Delivery Date Until", + description: "If the delivery date should be a time range, another timestamp can be provided in this attribute * to define a range from timestamp used in deliveryDate attribute to the timestamp used here.", + optional: true, + }, + status: { + type: "string", + label: "Status", + description: "Please have a look in Sevdesk's [Types and status of invoices](https://api.sevdesk.de/#section/Types-and-status-of-invoices) to see what the different status codes mean.", + options: STATUS_OPTIONS, + }, + smallSettlement: { + type: "boolean", + label: "Small Settlement", + description: "Defines if the client uses the small settlement scheme. If yes, the invoice must not contain any vat.", + optional: true, + }, + taxRate: { + type: "string", + label: "Tax Rate", + description: "Is overwritten by invoice position tax rates.", + }, + taxText: { + type: "string", + label: "Tax Text", + description: "A common tax text would be 'Tax 19%'.", + }, + taxType: { + type: "string", + label: "Tax Type", + description: "Tax type of the invoice.", + options: TAX_TYPE_OPTIONS, + }, + taxSetId: { + propDefinition: [ + sevdesk, + "taxSetId", + ], + optional: true, + }, + paymentMethodId: { + propDefinition: [ + sevdesk, + "paymentMethodId", + ], + optional: true, + }, + sendDate: { + type: "string", + label: "Send Date", + description: "The date the invoice was sent to the customer.", + optional: true, + }, + invoiceType: { + type: "string", + label: "Invoice Type", + description: "Type of the invoice. For more information on the different types, check [this](https://api.sevdesk.de/#tag/Invoice/Types-and-status-of-invoices) section", + options: INVOICE_TYPE_OPTIONS, + }, + currency: { + type: "string", + label: "Currency", + description: "Currency used in the invoice. Needs to be currency code according to ISO-4217.", + options: CURRENCY_OPTIONS, + }, + showNet: { + type: "boolean", + label: "Show Net", + description: "If true, the net amount of each position will be shown on the invoice. Otherwise gross amount.", + optional: true, + }, + sendType: { + type: "string", + label: "Send Type", + description: "Type which was used to send the invoice.", + options: SEND_TYPE_OPTIONS, + optional: true, + }, + originId: { + propDefinition: [ + sevdesk, + "originId", + ], + optional: true, + }, + customerInternalNote: { + type: "string", + label: "Customer Internal Note", + description: "Internal note of the customer. Contains data entered into field 'Referenz/Bestellnummer'.", + optional: true, + }, + }, + async run({ $ }) { + const { + sevdesk, + contactId, + addressCountryId, + taxRate, + taxSetId, + paymentMethodId, + originId, + ...data + } = this; + + if (this.taxType && !taxSetId) { + throw new ConfigurationError("Tax Set needs to be added when you chose the tax type custom!"); + } + + if (paymentMethodId) { + data.paymentMethod = { + id: paymentMethodId, + objectName: "PaymentMethod", + }; + } + if (taxSetId) { + data.taxSet = { + id: taxSetId, + objectName: "TaxSet", + }; + } + if (originId) { + data.origin = { + id: originId, + objectName: "Origin", + }; + } + + const { objects } = await sevdesk.checkMe(); + const contactPersonId = objects[0].id; + + const response = await sevdesk.createInvoice({ + $, + data: { + objectName: "Invoice", + contact: { + id: contactId, + objectName: "Contact", + }, + contactPerson: { + id: contactPersonId, + objectName: "SevUser", + }, + addressCountry: { + id: addressCountryId, + objectName: "StaticCountry", + }, + taxRate: parseFloat(taxRate), + mapAll: true, + ...data, + }, + }); + + $.export("$summary", `Successfully created invoice with ID ${response.objects.id}`); + return response; + }, +}; diff --git a/components/sevdesk/actions/send-invoice-email/send-invoice-email.mjs b/components/sevdesk/actions/send-invoice-email/send-invoice-email.mjs new file mode 100644 index 0000000000000..aac5740699e7b --- /dev/null +++ b/components/sevdesk/actions/send-invoice-email/send-invoice-email.mjs @@ -0,0 +1,88 @@ +import { ConfigurationError } from "@pipedream/platform"; +import sevdesk from "../../sevdesk.app.mjs"; + +export default { + key: "sevdesk-send-invoice-email", + name: "Send Invoice Email", + description: "Sends an invoice via email. [See the documentation](https://api.sevdesk.de/#tag/Invoice/operation/sendInvoiceViaEMail)", + version: "0.0.1", + type: "action", + props: { + sevdesk, + invoiceId: { + propDefinition: [ + sevdesk, + "invoiceId", + ], + }, + toEmail: { + type: "string", + label: "To Email", + description: "The recipient of the email.", + }, + subject: { + type: "string", + label: "Subject", + description: "The subject of the email.", + }, + text: { + type: "string", + label: "Text", + description: "The text of the email. Can contain html.", + }, + copy: { + type: "boolean", + label: "Copy", + description: "Should a copy of this email be sent to you?", + optional: true, + }, + additionalAttachments: { + type: "string[]", + label: "Additional Attachments", + description: "Additional attachments to the mail. List of IDs of existing documents in your * sevdesk account", + optional: true, + }, + ccEmail: { + type: "string[]", + label: "Cc Email", + description: "List of mail addresses to be put as cc", + optional: true, + }, + bccEmail: { + type: "string[]", + label: "Bcc Email", + description: "List of mail addresses to be put as bcc", + optional: true, + }, + + }, + async run({ $ }) { + + try { + const response = await this.sevdesk.sendInvoice({ + $, + invoiceId: this.invoiceId, + data: { + toEmail: this.toEmail, + subject: this.subject, + text: this.text, + copy: this.copy, + additionalAttachments: this.additionalAttachments + ? this.additionalAttachments.join() + : null, + ccEmail: this.ccEmail + ? this.ccEmail.join() + : null, + bccEmail: this.bccEmail + ? this.bccEmail.join() + : null, + }, + }); + + $.export("$summary", `Successfully sent invoice ${this.invoiceId} via email to ${this.toEmail}`); + return response; + } catch ({ message }) { + throw new ConfigurationError(JSON.parse(message).error.message); + } + }, +}; diff --git a/components/sevdesk/common/constants.mjs b/components/sevdesk/common/constants.mjs new file mode 100644 index 0000000000000..dc9cb47bf7b85 --- /dev/null +++ b/components/sevdesk/common/constants.mjs @@ -0,0 +1,750 @@ +export const LIMIT = 100; + +export const STATUS_OPTIONS = [ + "100", + "200", + "500", + "600", + "750", + "1000", +]; + +export const TAX_TYPE_OPTIONS = [ + { + label: "Default - show sales tax.", + value: "default", + }, + { + label: "EU - Tax-free intra-community delivery (European Union).", + value: "eu", + }, + { + label: "Noteu - tax liability of the recipient of the service (outside the EU, e.g. Switzerland).", + value: "noteu", + }, + { + label: "Custom - Using custom tax set.", + value: "custom", + }, + { + label: "SS - Not subject to VAT according to §19 1 UStG Tax rates are heavily connected to the tax type used.", + value: "ss", + }, +]; + +export const INVOICE_TYPE_OPTIONS = [ + { + label: "Normal invoice", + value: "RE", + }, + { + label: "Recurring invoice", + value: "WKR", + }, + { + label: "Cancellation invoice", + value: "SR", + }, + { + label: "Reminder invoice", + value: "MA", + }, + { + label: "Part invoice", + value: "TR", + }, + { + label: "Final invoice", + value: "ER", + }, +]; + +export const SEND_TYPE_OPTIONS = [ + "VPR", + "VPDF", + "VM", + "VP", +]; + +export const CURRENCY_OPTIONS = [ + { + value: "AED", + label: "United Arab Emirates Dirham", + }, + { + value: "AFN", + label: "Afghan Afghani", + }, + { + value: "ALL", + label: "Albanian Lek", + }, + { + value: "AMD", + label: "Armenian Dram", + }, + { + value: "ANG", + label: "Netherlands Antillean Guilder", + }, + { + value: "AOA", + label: "Angolan Kwanza", + }, + { + value: "ARS", + label: "Argentine Peso", + }, + { + value: "AUD", + label: "Australian Dollar", + }, + { + value: "AWG", + label: "Aruban Florin", + }, + { + value: "AZN", + label: "Azerbaijani Manat", + }, + { + value: "BAM", + label: "Bosnia-Herzegovina Convertible Mark", + }, + { + value: "BBD", + label: "Barbadian Dollar", + }, + { + value: "BDT", + label: "Bangladeshi Taka", + }, + { + value: "BGN", + label: "Bulgarian Lev", + }, + { + value: "BHD", + label: "Bahraini Dinar", + }, + { + value: "BIF", + label: "Burundian Franc", + }, + { + value: "BMD", + label: "Bermudan Dollar", + }, + { + value: "BND", + label: "Brunei Dollar", + }, + { + value: "BOB", + label: "Bolivian Boliviano", + }, + { + value: "BRL", + label: "Brazilian Real", + }, + { + value: "BSD", + label: "Bahamian Dollar", + }, + { + value: "BTC", + label: "Bitcoin", + }, + { + value: "BTN", + label: "Bhutanese Ngultrum", + }, + { + value: "BWP", + label: "Botswanan Pula", + }, + { + value: "BYN", + label: "Belarusian Ruble", + }, + { + value: "BZD", + label: "Belize Dollar", + }, + { + value: "CAD", + label: "Canadian Dollar", + }, + { + value: "CDF", + label: "Congolese Franc", + }, + { + value: "CHF", + label: "Swiss Franc", + }, + { + value: "CLF", + label: "Chilean Unit of Account (UF)", + }, + { + value: "CLP", + label: "Chilean Peso", + }, + { + value: "CNH", + label: "Chinese Yuan (Offshore)", + }, + { + value: "CNY", + label: "Chinese Yuan", + }, + { + value: "COP", + label: "Colombian Peso", + }, + { + value: "CRC", + label: "Costa Rican Colón", + }, + { + value: "CUC", + label: "Cuban Convertible Peso", + }, + { + value: "CUP", + label: "Cuban Peso", + }, + { + value: "CVE", + label: "Cape Verdean Escudo", + }, + { + value: "CZK", + label: "Czech Republic Koruna", + }, + { + value: "DJF", + label: "Djiboutian Franc", + }, + { + value: "DKK", + label: "Danish Krone", + }, + { + value: "DOP", + label: "Dominican Peso", + }, + { + value: "DZD", + label: "Algerian Dinar", + }, + { + value: "EGP", + label: "Egyptian Pound", + }, + { + value: "ERN", + label: "Eritrean Nakfa", + }, + { + value: "ETB", + label: "Ethiopian Birr", + }, + { + value: "EUR", + label: "Euro", + }, + { + value: "FJD", + label: "Fijian Dollar", + }, + { + value: "FKP", + label: "Falkland Islands Pound", + }, + { + value: "GBP", + label: "British Pound Sterling", + }, + { + value: "GEL", + label: "Georgian Lari", + }, + { + value: "GGP", + label: "Guernsey Pound", + }, + { + value: "GHS", + label: "Ghanaian Cedi", + }, + { + value: "GIP", + label: "Gibraltar Pound", + }, + { + value: "GMD", + label: "Gambian Dalasi", + }, + { + value: "GNF", + label: "Guinean Franc", + }, + { + value: "GTQ", + label: "Guatemalan Quetzal", + }, + { + value: "GYD", + label: "Guyanaese Dollar", + }, + { + value: "HKD", + label: "Hong Kong Dollar", + }, + { + value: "HNL", + label: "Honduran Lempira", + }, + { + value: "HRK", + label: "Croatian Kuna", + }, + { + value: "HTG", + label: "Haitian Gourde", + }, + { + value: "HUF", + label: "Hungarian Forint", + }, + { + value: "IDR", + label: "Indonesian Rupiah", + }, + { + value: "ILS", + label: "Israeli New Sheqel", + }, + { + value: "IMP", + label: "Manx pound", + }, + { + value: "INR", + label: "Indian Rupee", + }, + { + value: "IQD", + label: "Iraqi Dinar", + }, + { + value: "IRR", + label: "Iranian Rial", + }, + { + value: "ISK", + label: "Icelandic Króna", + }, + { + value: "JEP", + label: "Jersey Pound", + }, + { + value: "JMD", + label: "Jamaican Dollar", + }, + { + value: "JOD", + label: "Jordanian Dinar", + }, + { + value: "JPY", + label: "Japanese Yen", + }, + { + value: "KES", + label: "Kenyan Shilling", + }, + { + value: "KGS", + label: "Kyrgystani Som", + }, + { + value: "KHR", + label: "Cambodian Riel", + }, + { + value: "KMF", + label: "Comorian Franc", + }, + { + value: "KPW", + label: "North Korean Won", + }, + { + value: "KRW", + label: "South Korean Won", + }, + { + value: "KWD", + label: "Kuwaiti Dinar", + }, + { + value: "KYD", + label: "Cayman Islands Dollar", + }, + { + value: "KZT", + label: "Kazakhstani Tenge", + }, + { + value: "LAK", + label: "Laotian Kip", + }, + { + value: "LBP", + label: "Lebanese Pound", + }, + { + value: "LKR", + label: "Sri Lankan Rupee", + }, + { + value: "LRD", + label: "Liberian Dollar", + }, + { + value: "LSL", + label: "Lesotho Loti", + }, + { + value: "LYD", + label: "Libyan Dinar", + }, + { + value: "MAD", + label: "Moroccan Dirham", + }, + { + value: "MDL", + label: "Moldovan Leu", + }, + { + value: "MGA", + label: "Malagasy Ariary", + }, + { + value: "MKD", + label: "Macedonian Denar", + }, + { + value: "MMK", + label: "Myanma Kyat", + }, + { + value: "MNT", + label: "Mongolian Tugrik", + }, + { + value: "MOP", + label: "Macanese Pataca", + }, + { + value: "MRU", + label: "Mauritanian Ouguiya", + }, + { + value: "MUR", + label: "Mauritian Rupee", + }, + { + value: "MVR", + label: "Maldivian Rufiyaa", + }, + { + value: "MWK", + label: "Malawian Kwacha", + }, + { + value: "MXN", + label: "Mexican Peso", + }, + { + value: "MYR", + label: "Malaysian Ringgit", + }, + { + value: "MZN", + label: "Mozambican Metical", + }, + { + value: "NAD", + label: "Namibian Dollar", + }, + { + value: "NGN", + label: "Nigerian Naira", + }, + { + value: "NIO", + label: "Nicaraguan Córdoba", + }, + { + value: "NOK", + label: "Norwegian Krone", + }, + { + value: "NPR", + label: "Nepalese Rupee", + }, + { + value: "NZD", + label: "New Zealand Dollar", + }, + { + value: "OMR", + label: "Omani Rial", + }, + { + value: "PAB", + label: "Panamanian Balboa", + }, + { + value: "PEN", + label: "Peruvian Nuevo Sol", + }, + { + value: "PGK", + label: "Papua New Guinean Kina", + }, + { + value: "PHP", + label: "Philippine Peso", + }, + { + value: "PKR", + label: "Pakistani Rupee", + }, + { + value: "PLN", + label: "Polish Zloty", + }, + { + value: "PYG", + label: "Paraguayan Guarani", + }, + { + value: "QAR", + label: "Qatari Rial", + }, + { + value: "RON", + label: "Romanian Leu", + }, + { + value: "RSD", + label: "Serbian Dinar", + }, + { + value: "RUB", + label: "Russian Ruble", + }, + { + value: "RWF", + label: "Rwandan Franc", + }, + { + value: "SAR", + label: "Saudi Riyal", + }, + { + value: "SBD", + label: "Solomon Islands Dollar", + }, + { + value: "SCR", + label: "Seychellois Rupee", + }, + { + value: "SDG", + label: "Sudanese Pound", + }, + { + value: "SEK", + label: "Swedish Krona", + }, + { + value: "SGD", + label: "Singapore Dollar", + }, + { + value: "SHP", + label: "Saint Helena Pound", + }, + { + value: "SLL", + label: "Sierra Leonean Leone", + }, + { + value: "SOS", + label: "Somali Shilling", + }, + { + value: "SRD", + label: "Surinamese Dollar", + }, + { + value: "SSP", + label: "South Sudanese Pound", + }, + { + value: "STD", + label: "São Tomé and Príncipe Dobra (pre-2018)", + }, + { + value: "STN", + label: "São Tomé and Príncipe Dobra", + }, + { + value: "SVC", + label: "Salvadoran Colón", + }, + { + value: "SYP", + label: "Syrian Pound", + }, + { + value: "SZL", + label: "Swazi Lilangeni", + }, + { + value: "THB", + label: "Thai Baht", + }, + { + value: "TJS", + label: "Tajikistani Somoni", + }, + { + value: "TMT", + label: "Turkmenistani Manat", + }, + { + value: "TND", + label: "Tunisian Dinar", + }, + { + value: "TOP", + label: "Tongan Pa'anga", + }, + { + value: "TRY", + label: "Turkish Lira", + }, + { + value: "TTD", + label: "Trinidad and Tobago Dollar", + }, + { + value: "TWD", + label: "New Taiwan Dollar", + }, + { + value: "TZS", + label: "Tanzanian Shilling", + }, + { + value: "UAH", + label: "Ukrainian Hryvnia", + }, + { + value: "UGX", + label: "Ugandan Shilling", + }, + { + value: "USD", + label: "United States Dollar", + }, + { + value: "UYU", + label: "Uruguayan Peso", + }, + { + value: "UZS", + label: "Uzbekistan Som", + }, + { + value: "VEF", + label: "Venezuelan Bolívar Fuerte (Old)", + }, + { + value: "VES", + label: "Venezuelan Bolívar Soberano", + }, + { + value: "VND", + label: "Vietnamese Dong", + }, + { + value: "VUV", + label: "Vanuatu Vatu", + }, + { + value: "WST", + label: "Samoan Tala", + }, + { + value: "XAF", + label: "CFA Franc BEAC", + }, + { + value: "XAG", + label: "Silver Ounce", + }, + { + value: "XAU", + label: "Gold Ounce", + }, + { + value: "XCD", + label: "East Caribbean Dollar", + }, + { + value: "XDR", + label: "Special Drawing Rights", + }, + { + value: "XOF", + label: "CFA Franc BCEAO", + }, + { + value: "XPD", + label: "Palladium Ounce", + }, + { + value: "XPF", + label: "CFP Franc", + }, + { + value: "XPT", + label: "Platinum Ounce", + }, + { + value: "YER", + label: "Yemeni Rial", + }, + { + value: "ZAR", + label: "South African Rand", + }, + { + value: "ZMW", + label: "Zambian Kwacha", + }, + { + value: "ZWL", + label: "Zimbabwean Dollar", + }, +]; diff --git a/components/sevdesk/package.json b/components/sevdesk/package.json index b84d3fbde93d5..793f847f56ce4 100644 --- a/components/sevdesk/package.json +++ b/components/sevdesk/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/sevdesk", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream sevDesk Components", "main": "sevdesk.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" } -} \ No newline at end of file +} + diff --git a/components/sevdesk/sevdesk.app.mjs b/components/sevdesk/sevdesk.app.mjs index b07509125811a..c9baddfbd81b6 100644 --- a/components/sevdesk/sevdesk.app.mjs +++ b/components/sevdesk/sevdesk.app.mjs @@ -1,11 +1,224 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + export default { type: "app", app: "sevdesk", - propDefinitions: {}, + propDefinitions: { + addressCountryId: { + type: "string", + label: "Address Country ID", + description: "Can be omitted as complete address is defined in address attribute.", + async options() { + const { objects } = await this.listCountries({ + params: { + limit: 999, + }, + }); + + return objects.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + contactId: { + type: "string", + label: "Contact ID", + description: "The ID of the contact for the invoice.", + async options() { + const { objects } = await this.listContacts(); + + return objects.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + invoiceId: { + type: "integer", + label: "Invoice Id", + description: "ID of invoice to be manipulated.", + async options() { + const { objects } = await this.listInvoices(); + + return objects.filter((item) => item.status != 1000).map(({ + id, header, invoiceNumber, + }) => ({ + label: `${header || id} - ${invoiceNumber}`, + value: parseInt(id), + })); + }, + }, + originId: { + type: "string", + label: "Origin Id", + description: "The Id of the order.", + async options() { + const { objects } = await this.listOrders(); + + return objects.map(({ + id: value, header, orderNumber, + }) => ({ + label: `${header} - ${orderNumber}`, + value, + })); + }, + }, + paymentMethodId: { + type: "string", + label: "Payment Method Id", + description: "Payment method used for the invoice.", + async options() { + const { objects } = await this.listPaymentMethods(); + + return objects.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + taxSetId: { + type: "string", + label: "Tax Set Id", + description: "Tax set of the invoice. Needs to be added if you chose the tax type custom.", + async options() { + const { objects } = await this.listTaxSets(); + + return objects.map(({ + id: value, displayText: label, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://my.sevdesk.de/api/v1"; + }, + _headers() { + return { + "Authorization": `${this.$auth.api_token}`, + "Content-Type": "application/json", + }; + }, + _makeRequest({ + $ = this, path, ...otherOpts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...otherOpts, + }); + }, + checkMe(opts = {}) { + return this._makeRequest({ + path: "/SevUser", + ...opts, + }); + }, + createInvoice(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/Invoice", + ...opts, + }); + }, + cancelInvoice({ + invoiceId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/Invoice/${invoiceId}/cancelInvoice`, + ...opts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/Contact", + ...opts, + }); + }, + listCountries(opts = {}) { + return this._makeRequest({ + path: "/StaticCountry", + ...opts, + }); + }, + listInvoices(opts = {}) { + return this._makeRequest({ + path: "/Invoice", + ...opts, + }); + }, + listOrders(opts = {}) { + return this._makeRequest({ + path: "/Order", + ...opts, + }); + }, + listVouchers(opts = {}) { + return this._makeRequest({ + path: "/Voucher", + ...opts, + }); + }, + listPaymentMethods(opts = {}) { + return this._makeRequest({ + path: "/PaymentMethod", + ...opts, + }); + }, + listTaxSets(opts = {}) { + return this._makeRequest({ + path: "/TaxSet", + ...opts, + }); + }, + sendInvoice({ + invoiceId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/Invoice/${invoiceId}/sendViaEmail`, + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.limit = LIMIT; + params.offset = LIMIT * page; + page++; + + const { objects } = await fn({ + params, + ...opts, + }); + for (const d of objects) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = objects.length; + + } while (hasMore); }, }, -}; \ No newline at end of file +}; diff --git a/components/sevdesk/sources/common/base.mjs b/components/sevdesk/sources/common/base.mjs new file mode 100644 index 0000000000000..be965faba4497 --- /dev/null +++ b/components/sevdesk/sources/common/base.mjs @@ -0,0 +1,62 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import sevdesk from "../../sevdesk.app.mjs"; + +export default { + props: { + sevdesk, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const response = this.sevdesk.paginate({ + fn: this.getFunc(), + params: { + createAfter: lastDate, + depth: 1, + }, + }); + + let responseArray = []; + + for await (const item of response) { + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastDate(Date.parse(responseArray[0].create) / 1000); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item.create), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/sevdesk/sources/new-contact/new-contact.mjs b/components/sevdesk/sources/new-contact/new-contact.mjs new file mode 100644 index 0000000000000..008f096e2182b --- /dev/null +++ b/components/sevdesk/sources/new-contact/new-contact.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "sevdesk-new-contact", + name: "New Contact Created", + description: "Emit new event when a contact is created in SevDesk.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunc() { + return this.sevdesk.listContacts; + }, + getSummary(data) { + return `New contact created with Id: ${data.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/sevdesk/sources/new-contact/test-event.mjs b/components/sevdesk/sources/new-contact/test-event.mjs new file mode 100644 index 0000000000000..4cc209290956b --- /dev/null +++ b/components/sevdesk/sources/new-contact/test-event.mjs @@ -0,0 +1,48 @@ +export default { + "id": "0", + "objectName": "Contact", + "create": "2019-08-24T14:15:22Z", + "update": "2019-08-24T14:15:22Z", + "name": "string", + "status": "100", + "customerNumber": "Customer-1337", + "parent": { + "id": "0", + "objectName": "Contact" + }, + "surename": "John", + "familyname": "Snow", + "titel": "Commander", + "category": { + "id": "3", + "objectName": "Category" + }, + "description": "Rightful king of the seven kingdoms", + "academicTitle": "string", + "gender": "string", + "sevClient": { + "id": "0", + "objectName": "SevClient" + }, + "name2": "Targaryen", + "birthday": "2019-08-24", + "vatNumber": "string", + "bankAccount": "string", + "bankNumber": "string", + "defaultCashbackTime": "string", + "defaultCashbackPercent": "string", + "defaultTimeToPay": "string", + "taxNumber": "string", + "taxOffice": "string", + "exemptVat": "false", + "taxType": "default", + "taxSet": { + "id": "0", + "objectName": "TaxSet" + }, + "defaultDiscountAmount": "string", + "defaultDiscountPercentage": "false", + "buyerReference": "string", + "governmentAgency": "false", + "additionalInformation": "string" +} \ No newline at end of file diff --git a/components/sevdesk/sources/new-order/new-order.mjs b/components/sevdesk/sources/new-order/new-order.mjs new file mode 100644 index 0000000000000..807642091a2a9 --- /dev/null +++ b/components/sevdesk/sources/new-order/new-order.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "sevdesk-new-order", + name: "New Order Created", + description: "Emit new event for each new order created in SevDesk.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunc() { + return this.sevdesk.listOrders; + }, + getSummary(data) { + return `New order created with Id: ${data.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/sevdesk/sources/new-order/test-event.mjs b/components/sevdesk/sources/new-order/test-event.mjs new file mode 100644 index 0000000000000..3d90af4ca66dd --- /dev/null +++ b/components/sevdesk/sources/new-order/test-event.mjs @@ -0,0 +1,62 @@ +export default { + "id": "0", + "objectName": "Order", + "create": "2019-08-24T14:15:22Z", + "update": "2019-08-24T14:15:22Z", + "orderNumber": "Offer-1000", + "contact": { + "id": "0", + "objectName": "Contact" + }, + "orderDate": "01.01.2020", + "status": "100", + "header": "My Offer-1000", + "headText": "string", + "footText": "string", + "addressCountry": { + "id": "string", + "objectName": "string" + }, + "createUser": { + "id": "0", + "objectName": "SevUser" + }, + "sevClient": { + "id": "0", + "objectName": "SevClient" + }, + "deliveryTerms": "string", + "paymentTerms": "string", + "origin": { + "id": "0", + "objectName": "Order" + }, + "version": "0", + "smallSettlement": "0", + "contactPerson": { + "id": "0", + "objectName": "SevUser" + }, + "taxRate": "19", + "taxSet": { + "id": "0", + "objectName": "TaxSet" + }, + "taxText": "Umsatzsteuer 19%", + "taxType": "default", + "orderType": "AN", + "sendDate": "2019-08-24T14:15:22Z", + "address": "string", + "currency": "EUR", + "sumNet": "string", + "sumTax": "string", + "sumGross": "string", + "sumDiscounts": "string", + "sumNetForeignCurrency": "string", + "sumTaxForeignCurrency": "string", + "sumGrossForeignCurrency": "string", + "sumDiscountsForeignCurrency": "string", + "customerInternalNote": "string", + "showNet": 1, + "sendType": "VPR" +} \ No newline at end of file diff --git a/components/sevdesk/sources/new-voucher/new-voucher.mjs b/components/sevdesk/sources/new-voucher/new-voucher.mjs new file mode 100644 index 0000000000000..7ab25eff809bd --- /dev/null +++ b/components/sevdesk/sources/new-voucher/new-voucher.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "sevdesk-new-voucher", + name: "New Voucher Created", + description: "Emit new event when a new voucher is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunc() { + return this.sevdesk.listVouchers; + }, + getSummary(data) { + return `New voucher created with Id: ${data.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/sevdesk/sources/new-voucher/test-event.mjs b/components/sevdesk/sources/new-voucher/test-event.mjs new file mode 100644 index 0000000000000..a29e8c21e2c10 --- /dev/null +++ b/components/sevdesk/sources/new-voucher/test-event.mjs @@ -0,0 +1,60 @@ +export default { + "id": "0", + "objectName": "string", + "mapAll": true, + "create": "01.01.2020", + "update": "01.01.2020", + "sevClient": { + "id": "0", + "objectName": "SevClient" + }, + "createUser": { + "id": "0", + "objectName": "SevUser" + }, + "voucherDate": "01.01.2020", + "supplier": { + "id": "0", + "objectName": "Contact" + }, + "supplierName": "John Snow", + "description": "Voucher-1000", + "document": { + "id": "0", + "objectName": "Document" + }, + "payDate": "01.01.2020", + "status": "50", + "sumNet": "0", + "sumTax": "0", + "sumGross": "0", + "sumNetAccounting": "0", + "sumTaxAccounting": "0", + "sumGrossAccounting": "0", + "sumDiscounts": "0", + "sumDiscountsForeignCurrency": "0", + "paidAmount": 0, + "taxType": "default", + "creditDebit": "C", + "costCentre": { + "id": "0", + "objectName": "string" + }, + "voucherType": "VOU", + "currency": "EUR", + "propertyForeignCurrencyDeadline": "01.01.2022", + "propertyExchangeRate": "0.8912", + "recurringInterval": "P0Y0M1W", + "recurringStartDate": "01.01.2020", + "recurringNextVoucher": "01.02.2020", + "recurringLastVoucher": "01.01.2021", + "recurringEndDate": "01.01.2021", + "enshrined": "01.01.2020", + "taxSet": { + "id": "0", + "objectName": "TaxSet" + }, + "paymentDeadline": "01.01.2020", + "deliveryDate": "01.01.2020", + "deliveryDateUntil": "01.01.2020" +} \ No newline at end of file diff --git a/components/seven/.gitignore b/components/seven/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/seven/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/seven/README.md b/components/seven/README.md new file mode 100644 index 0000000000000..1c799e70027e3 --- /dev/null +++ b/components/seven/README.md @@ -0,0 +1,11 @@ +# Overview + +The Seven API offers tools to augment business communications by providing powerful messaging capabilities. With Pipedream, you can harness these features to create custom, automated workflows that trigger on events, process data, and interact with other apps. Imagine sending personalized SMS campaigns, automating customer support responses, or syncing communication data with CRM platforms. Pipedream's serverless platform allows you to build these integrations with ease, tapping into a wide range of pre-built connectors or using HTTP requests to interact with the Seven API directly. + +# Example Use Cases + +- **Automated Customer Support Messaging Workflow**: Trigger a workflow in Pipedream when a ticket is created in a support platform like Zendesk. Use the Seven API to send an automated SMS to the customer, acknowledging the receipt of their support request and providing an estimated response time. + +- **SMS Marketing Campaigns with Audience Segmentation**: Kick off a Pipedream workflow based on a subscriber event in Mailchimp. Segment the audience based on subscriber data, and use the Seven API to send targeted SMS marketing campaigns to those segments, tracking engagement through custom events. + +- **Syncing SMS Communication with CRM**: Whenever a new SMS message is sent via the Seven API, trigger a Pipedream workflow that logs the message details into a CRM platform like Salesforce. This ensures sales and support teams have up-to-date communication records for each customer. diff --git a/components/seven/actions/lookup-cnam/lookup-cnam.mjs b/components/seven/actions/lookup-cnam/lookup-cnam.mjs new file mode 100644 index 0000000000000..8a0026467581b --- /dev/null +++ b/components/seven/actions/lookup-cnam/lookup-cnam.mjs @@ -0,0 +1,30 @@ +import seven from "../../seven.app.mjs"; + +export default { + key: "seven-lookup-cnam", + name: "Lookup CNAM", + description: "Look up caller name information via Seven. [See the documentation](https://docs.seven.io/en/rest-api/endpoints/lookup#cnam)", + version: "0.0.1", + type: "action", + props: { + seven, + number: { + propDefinition: [ + seven, + "number", + ], + }, + }, + async run({ $ }) { + const response = await this.seven.lookupCnam({ + $, + params: { + number: this.number, + }, + }); + if (response.success) { + $.export("$summary", `Successfully looked up caller information for number: ${this.number}`); + } + return response; + }, +}; diff --git a/components/seven/actions/lookup-format/lookup-format.mjs b/components/seven/actions/lookup-format/lookup-format.mjs new file mode 100644 index 0000000000000..d351f038a51df --- /dev/null +++ b/components/seven/actions/lookup-format/lookup-format.mjs @@ -0,0 +1,30 @@ +import seven from "../../seven.app.mjs"; + +export default { + key: "seven-lookup-format", + name: "Lookup Format", + description: "Look up phone number formatting via Seven. [See the documentation](https://docs.seven.io/en/rest-api/endpoints/lookup#format)", + version: "0.0.1", + type: "action", + props: { + seven, + number: { + propDefinition: [ + seven, + "number", + ], + }, + }, + async run({ $ }) { + const response = await this.seven.lookupFormat({ + $, + params: { + number: this.number, + }, + }); + if (response.success) { + $.export("$summary", `Successfully looked up phone number formatting for number: ${this.number}`); + } + return response; + }, +}; diff --git a/components/seven/actions/lookup-hlr/lookup-hlr.mjs b/components/seven/actions/lookup-hlr/lookup-hlr.mjs new file mode 100644 index 0000000000000..cb21cb37d99e8 --- /dev/null +++ b/components/seven/actions/lookup-hlr/lookup-hlr.mjs @@ -0,0 +1,30 @@ +import seven from "../../seven.app.mjs"; + +export default { + key: "seven-lookup-hlr", + name: "Lookup HLR", + description: "Look up home location register information via Seven. [See the documentation](https://docs.seven.io/en/rest-api/endpoints/lookup#hlr)", + version: "0.0.1", + type: "action", + props: { + seven, + number: { + propDefinition: [ + seven, + "number", + ], + }, + }, + async run({ $ }) { + const response = await this.seven.lookupHlr({ + $, + params: { + number: this.number, + }, + }); + if (response.success) { + $.export("$summary", `Successfully looked up location register information for number: ${this.number}`); + } + return response; + }, +}; diff --git a/components/seven/actions/make-tts-call/make-tts-call.mjs b/components/seven/actions/make-tts-call/make-tts-call.mjs new file mode 100644 index 0000000000000..f13c2ac9c4027 --- /dev/null +++ b/components/seven/actions/make-tts-call/make-tts-call.mjs @@ -0,0 +1,34 @@ +import seven from "../../seven.app.mjs"; + +export default { + key: "seven-make-tts-call", + name: "Make TTS Call", + description: "Make a text-to-speech call via Seven. [See the documentation](https://docs.seven.io/en/rest-api/endpoints/voice#send-voice-call)", + version: "0.0.1", + type: "action", + props: { + seven, + to: { + propDefinition: [ + seven, + "to", + ], + }, + text: { + type: "string", + label: "Text", + description: "Text message to be read out", + }, + }, + async run({ $ }) { + const response = await this.seven.sendTtsCall({ + $, + data: { + to: this.to, + text: this.text, + }, + }); + $.export("$summary", `Successfully sent TTS call to number: ${this.to}`); + return response; + }, +}; diff --git a/components/seven/actions/send-sms/send-sms.mjs b/components/seven/actions/send-sms/send-sms.mjs new file mode 100644 index 0000000000000..4edbd17529570 --- /dev/null +++ b/components/seven/actions/send-sms/send-sms.mjs @@ -0,0 +1,34 @@ +import seven from "../../seven.app.mjs"; + +export default { + key: "seven-send-sms", + name: "Send SMS", + description: "Send SMS via Seven. [See the documentation](https://docs.seven.io/en/rest-api/endpoints/sms#send-sms)", + version: "0.0.1", + type: "action", + props: { + seven, + to: { + propDefinition: [ + seven, + "to", + ], + }, + text: { + type: "string", + label: "Text", + description: "SMS message to send", + }, + }, + async run({ $ }) { + const response = await this.seven.sendSms({ + $, + data: { + to: this.to, + text: this.text, + }, + }); + $.export("$summary", `Successfully sent SMS message to number: ${this.to}`); + return response; + }, +}; diff --git a/components/seven/app/seven.app.ts b/components/seven/app/seven.app.ts deleted file mode 100644 index 973fdfdc01f07..0000000000000 --- a/components/seven/app/seven.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "seven", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/seven/package.json b/components/seven/package.json index 274b60fa603d0..93898e9636f80 100644 --- a/components/seven/package.json +++ b/components/seven/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/seven", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Seven Components", - "main": "dist/app/seven.app.mjs", + "main": "seven.app.mjs", "keywords": [ "pipedream", "seven" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/seven", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" } -} \ No newline at end of file +} diff --git a/components/seven/seven.app.mjs b/components/seven/seven.app.mjs new file mode 100644 index 0000000000000..fde7507fc8229 --- /dev/null +++ b/components/seven/seven.app.mjs @@ -0,0 +1,71 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "seven", + propDefinitions: { + number: { + type: "string", + label: "Number", + description: "The phone number to look up. (e.g. `49176123456789`)", + }, + to: { + type: "string", + label: "To", + description: "The destination phone number. Accepts all common formats like `0049171123456789`, `49171123456789`, `+49171123456789`", + }, + }, + methods: { + _baseUrl() { + return "https://gateway.seven.io/api"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, + path, + ...args + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._headers(), + ...args, + }); + }, + lookupCnam(opts = {}) { + return this._makeRequest({ + path: "/lookup/cnam", + ...opts, + }); + }, + lookupFormat(opts = {}) { + return this._makeRequest({ + path: "/lookup/format", + ...opts, + }); + }, + lookupHlr(opts = {}) { + return this._makeRequest({ + path: "/lookup/hlr", + ...opts, + }); + }, + sendSms(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/sms", + ...opts, + }); + }, + sendTtsCall(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/voice", + ...opts, + }); + }, + }, +}; diff --git a/components/seventodos/README.md b/components/seventodos/README.md index 34371ba42b8f4..d24724379e169 100644 --- a/components/seventodos/README.md +++ b/components/seventodos/README.md @@ -1,17 +1,11 @@ # Overview -7todos is a powerful task automation API for managing tasks and to-dos. With -this powerful API, you can easily create custom workflows and automate -processes that help you save valuable time. In short, it is an efficient way to -simplify your task management. +The 7todos API empowers you to streamline the task management process by allowing you to create, read, update, and delete todos within your 7todos lists. With Pipedream, you can leverage these capabilities to automate workflows, sync tasks across multiple platforms, set reminders, or even analyze task completion rates. By connecting 7todos to other apps, you can create a seamless ecosystem for personal productivity or team collaboration. -Here are some of the things that you can build with the 7todos API: +# Example Use Cases -- Automate repetitive tasks like data recording, filtering and sorting. -- Create custom task filters that show relevant tasks only. -- Send out task notifications, reminders and updates to users. -- Automate team task assignments. -- Create projects and assign tasks to members of your team. -- Track and monitor task progress. -- Create custom task views and dashboards. -- Create custom reports to view task and activity analysis. +- **Task Synchronization with Google Calendar**: Create a Pipedream workflow that listens for new tasks added in 7todos, then automatically creates corresponding events in Google Calendar. This ensures that your tasks are not just confined to your todo list but are also visible as part of your daily schedule. + +- **Slack Notifications for Completed Tasks**: Set up a Pipedream automation that monitors for task completions in 7todos. Once a task is marked completed, trigger a notification to a specified Slack channel or direct message to keep your team updated on project progress in real-time. + +- **Email Digest of Daily Tasks**: Use Pipedream to construct a daily workflow that fetches all tasks due for the day from 7todos each morning and compiles them into an email digest. Send this digest to your email or distribute it to your team to provide a clear outline of the day's priorities. diff --git a/components/sftp/README.md b/components/sftp/README.md index 11b88b7ecfdc3..c1e27f2159576 100644 --- a/components/sftp/README.md +++ b/components/sftp/README.md @@ -1,21 +1,11 @@ # Overview -SFTP (SSH File Transfer Protocol) is a secure file transfer protocol for -exchanging files between computers over a secure connection. The protocol -utilizes public-key cryptography to provide authentication and encryption of -the data being transferred. SFTP is widely used in enterprise environments for -securely transferring data between locations and users, as well as for managing -remote computer systems (for example, for monitoring and software/security -updates). +The SFTP (key-based auth) app on Pipedream allows you to securely transfer and manage files over a network. It uses SSH for data transfer and provides the same level of security as SSH, without requiring password authentication, making automation workflows more secure and less prone to human error. You can automate file uploads, downloads, synchronization tasks, and efficiently manage your remote files in a serverless environment. Integrating this with other Pipedream-supported apps enables you to craft powerful and secure data flow systems. -Here are some examples of what you can build using the SFTP (key-based auth) -API: +# Example Use Cases -- Secure file backup and restore solutions. -- Text editors that can securely open, modify and save remote files. -- Manage remote uploads, downloads, editing, navigation, etc. -- Automate remote system management tasks (e.g., installation of applications, - software/security updates). -- Share and sync files between multiple systems. -- Remote user authentication. -- Custom solutions tailored to specific needs. +- **Automated Data Backup**: Sync local data to a remote SFTP server on a schedule. Whenever files are added or updated in a specific local directory, Pipedream triggers a workflow that automatically uploads these files to a designated SFTP folder, ensuring off-site backups are always up-to-date. + +- **Log File Aggregation**: Collect log files from multiple servers. Set up a Pipedream workflow that connects to various SFTP servers, downloads log files at regular intervals, and consolidates them into a single storage solution like Amazon S3 for analysis or long-term storage. + +- **Web Content Deployment**: After a Git push to the master branch, automatically deploy web content to a production server. Using Pipedream triggers that respond to GitHub events, fetch the latest web assets from the repository and upload them to the SFTP server, automating the deployment process. diff --git a/components/sftp/actions/upload-file/upload-file.mjs b/components/sftp/actions/upload-file/upload-file.mjs index d1c436fda12de..8b49f960c74ee 100644 --- a/components/sftp/actions/upload-file/upload-file.mjs +++ b/components/sftp/actions/upload-file/upload-file.mjs @@ -6,7 +6,7 @@ export default { key: "sftp-upload-file", name: "Upload File", description: "Uploads a file or string in UTF-8 format to the SFTP server. [See the documentation](https://github.com/theophilusx/ssh2-sftp-client#org99d1b64)", - version: "0.2.0", + version: "0.3.0", type: "action", props: { app, diff --git a/components/sftp/package.json b/components/sftp/package.json index c3473e5f21706..bc942c6d6a189 100644 --- a/components/sftp/package.json +++ b/components/sftp/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/sftp", - "version": "0.3.8", + "version": "0.4.1", "description": "Pipedream SFTP Components", "main": "sftp.app.mjs", "keywords": [ @@ -10,8 +10,8 @@ "homepage": "https://pipedream.com/apps/sftp", "author": "Pipedream (https://pipedream.com/)", "dependencies": { - "@pipedream/platform": "^1.2.0", - "ssh2-sftp-client": "^8.1.0" + "@pipedream/platform": "^3.0.3", + "ssh2-sftp-client": "^11.0.0" }, "gitHead": "e12480b94cc03bed4808ebc6b13e7fdb3a1ba535", "publishConfig": { diff --git a/components/sftp/sftp.app.mjs b/components/sftp/sftp.app.mjs index cfec450fccfe3..9382150a8a095 100644 --- a/components/sftp/sftp.app.mjs +++ b/components/sftp/sftp.app.mjs @@ -20,23 +20,27 @@ export default { }, }, methods: { - async connect() { + getConfig() { const { host, username, privateKey, } = this.$auth; - const config = { + return { host, username, privateKey, }; - + }, + async connect() { const sftp = new Client(); - await sftp.connect(config); + await sftp.connect(this.getConfig()); return sftp; }, + async disconnect(sftp) { + await sftp.end(); + }, async put({ buffer, remotePath, diff --git a/components/sftp/sources/common/base.mjs b/components/sftp/sources/common/base.mjs deleted file mode 100644 index 0cb6e6d4d92f6..0000000000000 --- a/components/sftp/sources/common/base.mjs +++ /dev/null @@ -1,125 +0,0 @@ -import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; - -export default { - props: { - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - db: "$.service.db", - rootDirectory: { - label: "Root directory", - description: "Root directory to be watched. example: `/public`", - type: "string", - default: "/", - }, - maxDepth: { - label: "Maximum watcher depth", - description: "Watch all subdirectories within of the root directory, considering the selected maximum depth.", - type: "integer", - min: 0, - }, - }, - methods: { - setFiles(files) { - this.db.set("files", files); - }, - getFiles() { - return this.db.get("files") || []; - }, - getChangesWithEvent(currentFiles, previousFiles) { - const fileChanges = []; - // deleted file detection; - for (const prvFile of previousFiles) { - const file = currentFiles.find((p) => p.path === prvFile.path); - if (!file) { - fileChanges.push({ - ...prvFile, - event: "deleted", - }); - } - } - // created, updated file detection; - for (const file of currentFiles) { - const prvFile = previousFiles.find((p) => p.path === file.path); - if (prvFile) { - // unchanged files (checking size and last update time) - if (prvFile.size === file.size && prvFile.modifyTime === file.modifyTime) { - continue; - } - // file has changed - fileChanges.push({ - ...file, - event: "updated", - }); - } else { - // new file detection - fileChanges.push({ - ...file, - event: "created", - }); - } - } - return fileChanges; - }, - async listDirectories(sftp, parent, currDepth) { - const files = await sftp.list(parent); - return files.filter((file) => file.type === "d") - .map((directory) => ({ - ...directory, - parent, - path: `${parent}/${directory.name}`, - depth: currDepth, - })); - }, - async listDirectoriesDeep(sftp, parent, maxDepth, currDepth) { - if (currDepth > maxDepth) { - return []; - } - const nextDepth = currDepth + 1; - const rootDirectories = await this.listDirectories(sftp, parent, currDepth); - const childrenDirectories = []; - for (const item of rootDirectories) { - const path = `${parent}/${item.name}`; - const directories = await this.listDirectoriesDeep(sftp, path, maxDepth, nextDepth); - childrenDirectories.push(...directories); - } - rootDirectories.push(...childrenDirectories); - return rootDirectories; - }, - async listAllFilesFromDirectories(sftp, directories) { - const allFiles = []; - for (const directory of directories) { - const listingResult = await sftp.list(directory.path); - const files = listingResult.filter((file) => file.type !== "d") - .map((file) => ({ - ...file, - directory: directory.path, - path: `${directory.path}/${file.name}`, - depth: directory.depth || 0, - })); - allFiles.push(...files); - } - return allFiles; - }, - validateRootDirectory(rootDirectory) { - if (!rootDirectory) { - throw new Error("Must provide root directory"); - } - if (!rootDirectory.startsWith("/")) { - throw new Error("The root directory must to be the absolute path and start with a slash, such as: '/public'"); - } - }, - emitEvents(events) { - for (const fileChangeEvent of events) { - this.$emit(fileChangeEvent, { - id: `${fileChangeEvent.event}|${fileChangeEvent.path}|${fileChangeEvent.size}|${fileChangeEvent.modifyTime}`, - summary: `${fileChangeEvent.event} ${fileChangeEvent.path}`, - ts: new Date(fileChangeEvent.modifyTime), - }); - } - }, - }, -}; diff --git a/components/sftp/sources/watch-remote-directory/watch-remote-directory.mjs b/components/sftp/sources/watch-remote-directory/watch-remote-directory.mjs index e783c48da7600..39dc455220b12 100644 --- a/components/sftp/sources/watch-remote-directory/watch-remote-directory.mjs +++ b/components/sftp/sources/watch-remote-directory/watch-remote-directory.mjs @@ -1,15 +1,16 @@ -import sftpApp from "../../sftp.app.mjs"; +import { createHash } from "crypto"; +import app from "../../sftp.app.mjs"; import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; export default { key: "sftp-watch-remote-directory", name: "New Remote Directory Watcher", description: "Emit new events when files get created, changed or deleted from a remote directory. [See the docs](https://github.com/theophilusx/ssh2-sftp-client#orgfac43d1)", - version: "0.0.4", + version: "0.1.1", type: "source", dedupe: "unique", props: { - sftpApp, + app, timer: { type: "$.interface.timer", default: { @@ -121,9 +122,13 @@ export default { } }, getEventId(event) { - return `${event.event}|${event.path}|${event.size}|${event.modifyTime}`; + return createHash("md5") + .update(event.path + event.modifyTime) + .digest("hex"); }, emitEvents(events) { + // sort events by modification time in ascending order + events.sort((a, b) => a.modifyTime - b.modifyTime); for (const fileChangeEvent of events) { this.$emit(fileChangeEvent, { id: this.getEventId(fileChangeEvent), @@ -132,17 +137,11 @@ export default { }); } }, - async connect() { - return await this.sftpApp.connect(); - }, - async disconnect(sftp) { - await sftp.end(); - }, }, async run() { this.validateRootDirectory(this.rootDirectory); - const sftp = await this.connect(); + const sftp = await this.app.connect(); let directories = [ { @@ -159,6 +158,6 @@ export default { this.emitEvents(filesChangesWithEvent); this.setFiles(currentFiles); - await this.disconnect(sftp); + await this.app.disconnect(sftp); }, }; diff --git a/components/sftp_password_based_auth/README.md b/components/sftp_password_based_auth/README.md index 50f05bc281767..9e652ff77a7f6 100644 --- a/components/sftp_password_based_auth/README.md +++ b/components/sftp_password_based_auth/README.md @@ -1,18 +1,11 @@ # Overview -SFTP (Secure File Transfer Protocol) is a network protocol that provides secure -file transfers over secure shell (SSH) connections. It is an extension of the -Secure Shell protocol, designed for secure file exchange between hosts. A user -can securely transfer files between systems without having to worry about -external threats, as the protocol is encrypted and authenticated. +SFTP on Pipedream enables secure file management on remote servers directly from your workflows. Utilize this app to automate file uploads, downloads, and synchronization tasks between your systems and SFTP servers. Implement robust data pipelines or deploy content to web servers without manual intervention, all while maintaining a high level of security with password-based authentication. -With the SFTP API, you can build applications that securely transfer data, such -as files and documents, between two different systems. Some example -applications include: +# Example Use Cases -- Secure document sharing between two systems -- Secure file synchronization between two systems -- Secure backups of data over the network -- Secure transfers of large files -- Secure uploads and downloads of data -- Media streaming between two systems +- **Scheduled Data Backups**: Automatically back up critical data from your local systems to an SFTP server. Set a cron trigger in Pipedream to periodically upload files, ensuring your backups are both current and off-site. + +- **E-commerce Order Processing**: When a new order is placed via a platform like Shopify, use Pipedream's Shopify trigger to fetch order details, generate invoices, and securely transfer them to an SFTP server for archiving and further processing. + +- **Website Content Deployment**: Set up a Git-triggered workflow that responds to commits or tags. When updates are pushed to your repository, Pipedream can fetch the latest content and deploy it to your web server via SFTP, streamlining your deployment process. diff --git a/components/sftp_password_based_auth/actions/upload-file/upload-file.mjs b/components/sftp_password_based_auth/actions/upload-file/upload-file.mjs index 914690f87b314..3b40e79f2f01d 100644 --- a/components/sftp_password_based_auth/actions/upload-file/upload-file.mjs +++ b/components/sftp_password_based_auth/actions/upload-file/upload-file.mjs @@ -1,44 +1,14 @@ -// legacy_hash_id: a_YEikdQ -import Client from "ssh2-sftp-client"; +import utils from "../../common/utils.mjs"; +import component from "@pipedream/sftp/actions/upload-file/upload-file.mjs"; export default { + ...component, + props: utils.buildAppProps({ + component, + }), key: "sftp_password_based_auth-upload-file", - name: "Upload String as File", - description: "Uploads a UTF-8 string as a file on an SFTP server", - version: "0.1.1", + name: "Upload File (Password Auth)", + description: "Uploads a file or string in UTF-8 format to the SFTP server. [See the documentation](https://github.com/theophilusx/ssh2-sftp-client#org99d1b64)", + version: "0.2.1", type: "action", - props: { - sftp_password_based_auth: { - type: "app", - app: "sftp_password_based_auth", - }, - data: { - type: "string", - description: "A UTF-8 string to upload as a file on the remote server.", - }, - remotePath: { - type: "string", - label: "Remote Path", - description: "The path to the remote file to be created on the server.", - }, - }, - async run({ $ }) { - const { - host, - username, - password, - } = this.sftp_password_based_auth.$auth; - - const config = { - host, - username, - password, - }; - - const sftp = new Client(); - - await sftp.connect(config); - $.export("putResponse", await sftp.put(Buffer.from(this.data), this.remotePath)); - await sftp.end(); - }, }; diff --git a/components/sftp_password_based_auth/common/utils.mjs b/components/sftp_password_based_auth/common/utils.mjs new file mode 100644 index 0000000000000..86d009918da0f --- /dev/null +++ b/components/sftp_password_based_auth/common/utils.mjs @@ -0,0 +1,79 @@ +import app from "../sftp_password_based_auth.app.mjs"; + +function buildPropDefinitions({ + app = {}, props = {}, +}) { + return Object.entries(props) + .reduce((newProps, [ + key, + prop, + ]) => { + if (!prop.propDefinition) { + return Object.assign(newProps, { + [key]: prop, + }); + } + + const [ + , ...propDefinitionItems + ] = prop.propDefinition; + + return Object.assign(newProps, { + [key]: Object.assign(prop, { + propDefinition: [ + app, + ...propDefinitionItems, + ], + }), + }); + }, {}); +} + +function getPropsOnly({ + component, omitProps = [], appLabel, addedProps = {}, +} = {}) { + const { + // eslint-disable-next-line no-unused-vars + [appLabel]: appProp, + ...props + } = component.props; + const builtProps = Object.entries(props) + .reduce((reduction, [ + key, + value, + ]) => { + if (omitProps.includes(key)) { + return reduction; + } + return Object.assign(reduction, { + [key]: value, + }); + }, {}); + return { + ...builtProps, + ...addedProps, + }; +} + +function buildAppProps({ + component, omitProps = [], appLabel = "app", addedProps, +} = {}) { + return { + [appLabel]: app, + ...buildPropDefinitions({ + app, + props: getPropsOnly({ + component, + omitProps, + appLabel, + addedProps, + }), + }), + }; +} + +export default { + buildPropDefinitions, + getPropsOnly, + buildAppProps, +}; diff --git a/components/sftp_password_based_auth/package.json b/components/sftp_password_based_auth/package.json index f18cfdcf5e928..48604d8fec3c7 100644 --- a/components/sftp_password_based_auth/package.json +++ b/components/sftp_password_based_auth/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/sftp_password_based_auth", - "version": "0.3.5", + "version": "0.4.2", "description": "Pipedream SFTP Components", "main": "sftp_password_based_auth.app.mjs", "keywords": [ @@ -9,11 +9,11 @@ ], "homepage": "https://pipedream.com/apps/sftp_password_based_auth", "author": "Pipedream (https://pipedream.com/)", - "dependencies": { - "ssh2-sftp-client": "^8.1.0" - }, "gitHead": "e12480b94cc03bed4808ebc6b13e7fdb3a1ba535", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/sftp": "^0.4.1" } } diff --git a/components/sftp_password_based_auth/sftp_password_based_auth.app.mjs b/components/sftp_password_based_auth/sftp_password_based_auth.app.mjs index 30e255891c542..5c790ca3f9377 100644 --- a/components/sftp_password_based_auth/sftp_password_based_auth.app.mjs +++ b/components/sftp_password_based_auth/sftp_password_based_auth.app.mjs @@ -1,26 +1,23 @@ -import Client from "ssh2-sftp-client"; +import sftpApp from "@pipedream/sftp"; export default { + ...sftpApp, type: "app", app: "sftp_password_based_auth", - propDefinitions: {}, methods: { - async connect() { + ...sftpApp.methods, + getConfig() { const { host, username, password, } = this.$auth; - const config = { + return { host, username, password, }; - - const sftp = new Client(); - await sftp.connect(config); - return sftp; }, }, }; diff --git a/components/sftp_password_based_auth/sources/watch-remote-directory/watch-remote-directory.mjs b/components/sftp_password_based_auth/sources/watch-remote-directory/watch-remote-directory.mjs index ce4de2a234326..09c1240fc4fae 100644 --- a/components/sftp_password_based_auth/sources/watch-remote-directory/watch-remote-directory.mjs +++ b/components/sftp_password_based_auth/sources/watch-remote-directory/watch-remote-directory.mjs @@ -1,21 +1,14 @@ -import sftpApp from "../../sftp_password_based_auth.app.mjs"; -import base from "../../../sftp/sources/watch-remote-directory/watch-remote-directory.mjs"; +import utils from "../../common/utils.mjs"; +import component from "@pipedream/sftp/sources/watch-remote-directory/watch-remote-directory.mjs"; export default { - ...base, + ...component, + props: utils.buildAppProps({ + component, + }), key: "sftp_password_based_auth-watch-remote-directory", - name: "New Remote Directory Watcher", + name: "New Remote Directory Watcher (Password Auth)", description: "Emit new events when files get created, changed or deleted from a remote directory. [See the docs](https://github.com/theophilusx/ssh2-sftp-client#orgfac43d1)", - version: "0.0.1", + version: "0.1.2", type: "source", - props: { - ...base.props, - sftpApp, - }, - methods: { - ...base.methods, - async connect() { - return await this.sftpApp.connect(); - }, - }, }; diff --git a/components/shadertoy/README.md b/components/shadertoy/README.md new file mode 100644 index 0000000000000..1c868c9d4694d --- /dev/null +++ b/components/shadertoy/README.md @@ -0,0 +1,11 @@ +# Overview + +The Shadertoy API opens a window to a universe of visual creations by providing programmatic access to a wealth of shaders created by the Shadertoy community. These shaders are snippets of GLSL code that produce stunning graphics and visual effects. In Pipedream, you can leverage this API to trigger workflows that interact with these shaders, such as retrieving shader information, displaying shader visuals, or even monitoring the latest creations. By combining Shadertoy API with Pipedream's capability to connect to numerous other services, your creative and technical projects can flourish with dynamic, visually rich content. + +# Example Use Cases + +- **Automated Shader Showcase on Social Media**: Integrate Shadertoy API with Twitter API to post a daily or weekly shader spotlight. The workflow can fetch a random or the latest shader and share it along with a brief description or creator shout-out, engaging followers with visually captivating content. + +- **Personalized Shader Notifications**: Use the Shadertoy API with the Pipedream Cron Scheduler to check for new shaders by a favorite artist or tag. When a new shader is detected, send a notification through email, Slack, or SMS via Twilio. This keeps you updated with real-time alerts on shader releases that match your interests. + +- **Dynamic Visuals for Web Applications**: Pair the Shadertoy API with a web app via HTTP / Webhook triggers to embed live shader visuals into your web pages. You can configure a workflow to serve up the latest trending shaders, or shaders tagged with specific themes, providing an ever-changing visual experience for your users. diff --git a/components/shadertoy/actions/access-assets/access-assets.mjs b/components/shadertoy/actions/access-assets/access-assets.mjs new file mode 100644 index 0000000000000..6f916a4373a63 --- /dev/null +++ b/components/shadertoy/actions/access-assets/access-assets.mjs @@ -0,0 +1,27 @@ +import app from "../../shadertoy.app.mjs"; + +export default { + key: "shadertoy-access-assets", + name: "Access Shader Assets", + description: "Accesses an asset from source. [See the documentation](https://www.shadertoy.com/howto)", + version: "0.0.1", + type: "action", + props: { + app, + assetSource: { + type: "string", + label: "Asset Source", + description: "The source to the specific asset", + }, + }, + async run({ $ }) { + const response = await this.app.getShaderAsset({ + $, + assetSource: this.assetSource, + }); + + $.export("$summary", `Successfully accessed asset "${this.assetSource}"`); + + return response; + }, +}; diff --git a/components/shadertoy/actions/get-shader/get-shader.mjs b/components/shadertoy/actions/get-shader/get-shader.mjs new file mode 100644 index 0000000000000..ceb163a1a68b2 --- /dev/null +++ b/components/shadertoy/actions/get-shader/get-shader.mjs @@ -0,0 +1,28 @@ +import app from "../../shadertoy.app.mjs"; + +export default { + key: "shadertoy-get-shader", + name: "Get Shader", + description: "Returns a specific shader based on an ID. [See the documentation](https://www.shadertoy.com/howto)", + version: "0.0.1", + type: "action", + props: { + app, + shaderId: { + propDefinition: [ + app, + "shaderId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getShaderById({ + $, + shaderId: this.shaderId, + }); + + $.export("$summary", `Successfully retrieved shader with ID: ${this.shaderId}`); + + return response; + }, +}; diff --git a/components/shadertoy/actions/list-shaders/list-shaders.mjs b/components/shadertoy/actions/list-shaders/list-shaders.mjs new file mode 100644 index 0000000000000..e95ef5c9af918 --- /dev/null +++ b/components/shadertoy/actions/list-shaders/list-shaders.mjs @@ -0,0 +1,21 @@ +import app from "../../shadertoy.app.mjs"; + +export default { + key: "shadertoy-list-shaders", + name: "List Shaders", + description: "Returns a list of all shaders. [See the documentation](https://www.shadertoy.com/howto)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.listShaders({ + $, + }); + + $.export("$summary", `Found ${response.Results.length} shaders`); + + return response; + }, +}; diff --git a/components/shadertoy/actions/search-shaders/search-shaders.mjs b/components/shadertoy/actions/search-shaders/search-shaders.mjs new file mode 100644 index 0000000000000..e3cc74a7c9d68 --- /dev/null +++ b/components/shadertoy/actions/search-shaders/search-shaders.mjs @@ -0,0 +1,28 @@ +import app from "../../shadertoy.app.mjs"; + +export default { + key: "shadertoy-search-shaders", + name: "Search Shaders", + description: "Returns an array of shader IDs based on the query string. [See the documentation](https://www.shadertoy.com/howto)", + version: "0.0.1", + type: "action", + props: { + app, + query: { + propDefinition: [ + app, + "query", + ], + }, + }, + async run({ $ }) { + const response = await this.app.searchShaders({ + $, + query: this.query, + }); + + $.export("$summary", `Found ${response.Results.length} shaders matching the query "${this.query}"`); + + return response; + }, +}; diff --git a/components/shadertoy/package.json b/components/shadertoy/package.json new file mode 100644 index 0000000000000..461354ad4a5b1 --- /dev/null +++ b/components/shadertoy/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/shadertoy", + "version": "0.1.0", + "description": "Pipedream Shadertoy Components", + "main": "shadertoy.app.mjs", + "keywords": [ + "pipedream", + "shadertoy" + ], + "homepage": "https://pipedream.com/apps/shadertoy", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/shadertoy/shadertoy.app.mjs b/components/shadertoy/shadertoy.app.mjs new file mode 100644 index 0000000000000..380557f2435a0 --- /dev/null +++ b/components/shadertoy/shadertoy.app.mjs @@ -0,0 +1,69 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "shadertoy", + propDefinitions: { + query: { + type: "string", + label: "Search Query", + description: "The search query string to find shaders", + }, + shaderId: { + type: "string", + label: "Shader ID", + description: "The unique identifier of the shader", + }, + }, + methods: { + _baseUrl() { + return "https://www.shadertoy.com"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + params: { + ...params, + key: `${this.$auth.app_key}`, + }, + }); + }, + async searchShaders({ + query, ...args + }) { + return this._makeRequest({ + path: `/api/v1/shaders/query/${query}`, + ...args, + }); + }, + async getShaderById({ + shaderId, ...args + }) { + return this._makeRequest({ + path: `/api/v1/shaders/${shaderId}`, + ...args, + }); + }, + async listShaders(args = {}) { + return this._makeRequest({ + path: "/api/v1/shaders", + ...args, + }); + }, + async getShaderAsset({ + assetSource, ...args + }) { + return this._makeRequest({ + path: `/media/previz/${assetSource}`, + ...args, + }); + }, + }, +}; diff --git a/components/sharepoint/README.md b/components/sharepoint/README.md new file mode 100644 index 0000000000000..52e77117db3f1 --- /dev/null +++ b/components/sharepoint/README.md @@ -0,0 +1,11 @@ +# Overview + +The Microsoft SharePoint Online API opens up a world of possibilities for integrating your SharePoint content with other services and automating tasks. With Pipedream, you can harness this API to create powerful workflows that trigger on events in SharePoint, manipulate data, and connect with countless other apps. Create custom automations for document management, team notifications, content moderation, and more, without the need to manage infrastructure. + +# Example Use Cases + +- **Automate Document Approval Workflows**: Trigger a workflow in Pipedream whenever a new document is uploaded to a SharePoint library. Use the API to send approval requests via email using an app like SendGrid. Once approved, update the document's metadata in SharePoint to reflect the change. + +- **Sync SharePoint Lists with External Databases**: Keep a SharePoint list in sync with an external database, such as MySQL or PostgreSQL. Use scheduled Pipedream workflows to fetch records from the database and update the corresponding SharePoint list items, or vice versa. + +- **Aggregate SharePoint Analytics**: Collect and aggregate analytics from SharePoint, like page views or document downloads. Send this data to a service like Google Sheets or Data Studio for advanced reporting and visualization. diff --git a/components/sharpspring/README.md b/components/sharpspring/README.md index 7034c2e3e6ff6..2e6bc2689076d 100644 --- a/components/sharpspring/README.md +++ b/components/sharpspring/README.md @@ -1,17 +1,11 @@ # Overview - Using the SharpSpring API, you can access the potential of SharpSpring's - powerful marketing automation platform. With the SharpSpring API, you can - build a wide range of capabilities, from email campaigns to more complex - initiatives. Here's a few examples of what you can build: +The SharpSpring API offers a gateway to an extensive marketing platform, enabling you to automate your marketing workflows, manage customer relationships, and glean insights from comprehensive analytics. With Pipedream, you can harness this power to create seamless integrations that trigger actions in SharpSpring based on external events or data, sync leads and contacts across platforms, and personalize marketing efforts based on user behavior and preferences. -- Automated email campaigns -- Automated lead nurturing -- Lead scoring -- Landing page creation -- Dynamic content personalization -- Custom user dashboards -- Webhooks and automated triggers -- Analytics and reporting -- Web forms to capture leads -- Systems integrations with other platforms +# Example Use Cases + +- **Lead Scoring Automation**: Automatically adjust lead scores in SharpSpring based on interactions captured from other platforms. For instance, if a lead watches a webinar on Zoom, Pipedream can catch the webinar attendance webhook and update the lead score in SharpSpring, ensuring your sales team prioritizes hot leads. + +- **Cross-Platform Contact Sync**: Keep SharpSpring contacts in sync with other CRMs like Salesforce or HubSpot. When a new contact is added to Salesforce, Pipedream can detect the event and create or update that contact in SharpSpring, maintaining consistency across your sales tools. + +- **Personalized Email Campaigns Triggered by Behavior**: Trigger personalized email campaigns in SharpSpring based on customer behavior tracked on your website or app. For example, if a user abandons a shopping cart, Pipedream can capture this event, and SharpSpring can send a tailored email to encourage purchase completion. diff --git a/components/sharpspring/package.json b/components/sharpspring/package.json new file mode 100644 index 0000000000000..b83810caf82a4 --- /dev/null +++ b/components/sharpspring/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/sharpspring", + "version": "0.6.0", + "description": "Pipedream sharpspring Components", + "main": "sharpspring.app.mjs", + "keywords": [ + "pipedream", + "sharpspring" + ], + "homepage": "https://pipedream.com/apps/sharpspring", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/sheetdb/README.md b/components/sheetdb/README.md new file mode 100644 index 0000000000000..fec93633f1196 --- /dev/null +++ b/components/sheetdb/README.md @@ -0,0 +1,11 @@ +# Overview + +SheetDB API turns your Google Sheets into a JSON API, enabling you to manage the content within your spreadsheet through RESTful endpoints. With Pipedream, you can harness this capability to build robust automations and workflows that interact with your spreadsheet data dynamically. Whether you're updating rows based on external triggers, syncing data to other platforms, or building a makeshift CRM, SheetDB paired with Pipedream's zero-management execution environment lets you deploy these solutions rapidly. + +# Example Use Cases + +- **Automated Content Management System (CMS)**: Trigger a Pipedream workflow whenever a row is added to your SheetDB-backed Google Sheet, automatically creating a blog post in WordPress. This workflow listens for new entries and uses the WordPress API to publish content, effectively turning your Google Sheet into a simple CMS. + +- **Customer Feedback Aggregation**: Connect SheetDB with a customer service platform like Zendesk. When a support ticket is resolved, Pipedream triggers an update to a dedicated Google Sheet through SheetDB, summarizing feedback. This workflow enables easy tracking of customer satisfaction trends over time. + +- **Inventory Management Alert System**: Link SheetDB with a messaging app like Slack. Track inventory levels in a Google Sheet and set a Pipedream workflow to monitor stock quantities via SheetDB. Once inventory drops below a threshold, it sends an alert to a Slack channel, prompting a restock action. diff --git a/components/sheetdb/actions/create-rows/create-rows.mjs b/components/sheetdb/actions/create-rows/create-rows.mjs new file mode 100644 index 0000000000000..fb463ab9d42a1 --- /dev/null +++ b/components/sheetdb/actions/create-rows/create-rows.mjs @@ -0,0 +1,45 @@ +import utils from "../../common/utils.mjs"; +import app from "../../sheetdb.app.mjs"; + +export default { + key: "sheetdb-create-rows", + name: "Create Rows", + description: "Create rows in a Google Sheet using the SheetDB API. [See the documentation](https://docs.sheetdb.io/sheetdb-api/create)", + version: "0.0.1", + type: "action", + props: { + app, + data: { + description: "An array of strings representing the rows to create in a Google Sheet as JSON objects. The keys inside the object should be the column names, and the values will be filled in the spreadsheet. The rows will be added at the end of the sheet. Eg. `[ { \"column1\": \"My Content\" } ]`", + propDefinition: [ + app, + "data", + ], + }, + }, + methods: { + createRows(args = {}) { + return this.app.post(args); + }, + }, + async run({ $ }) { + const { + createRows, + data, + } = this; + + const response = await createRows({ + $, + data: utils.parseArray(data), + }); + + if (response.error) { + $.export("$summary", "No rows were created."); + return response; + } + + $.export("$summary", `Successfully created \`${response.created}\` row(s).`); + + return response; + }, +}; diff --git a/components/sheetdb/actions/delete-rows/delete-rows.mjs b/components/sheetdb/actions/delete-rows/delete-rows.mjs new file mode 100644 index 0000000000000..53c4b439c7cc2 --- /dev/null +++ b/components/sheetdb/actions/delete-rows/delete-rows.mjs @@ -0,0 +1,55 @@ +import app from "../../sheetdb.app.mjs"; + +export default { + key: "sheetdb-delete-rows", + name: "Delete Rows", + description: "Deletes the specified row(s) in a SheetDB sheet by matching a column name and value. [See the documentation](https://docs.sheetdb.io/sheetdb-api/delete#delete-with-single-query)", + version: "0.0.1", + type: "action", + props: { + app, + columnName: { + propDefinition: [ + app, + "columnName", + ], + }, + columnValue: { + propDefinition: [ + app, + "columnValue", + ], + }, + }, + methods: { + deleteRows({ + columnName, columnValue, ...args + } = {}) { + return this.app.delete({ + path: `/${columnName}/${encodeURIComponent(columnValue)}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + deleteRows, + columnName, + columnValue, + } = this; + + const response = await deleteRows({ + $, + columnName, + columnValue, + }); + + if (response.error) { + $.export("$summary", "No rows were deleted."); + return response; + } + + $.export("$summary", `Successfully deleted \`${response.deleted}\` row(s).`); + return response; + }, +}; diff --git a/components/sheetdb/actions/get-column-names/get-column-names.mjs b/components/sheetdb/actions/get-column-names/get-column-names.mjs new file mode 100644 index 0000000000000..2b77aa9e09faf --- /dev/null +++ b/components/sheetdb/actions/get-column-names/get-column-names.mjs @@ -0,0 +1,25 @@ +import app from "../../sheetdb.app.mjs"; + +export default { + key: "sheetdb-get-column-names", + name: "Get Column Names", + description: "Get column names of a Google Sheet using the SheetDB API. [See the documentation](https://docs.sheetdb.io/sheetdb-api/read#keys)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.getKeys({ + $, + }); + + if (!response.length) { + $.export("$summary", "No column names were found."); + return response; + } + + $.export("$summary", `Successfully retrieved \`${response.length}\` column name(s).`); + return response; + }, +}; diff --git a/components/sheetdb/actions/search-content/search-content.mjs b/components/sheetdb/actions/search-content/search-content.mjs new file mode 100644 index 0000000000000..dd307ebf58879 --- /dev/null +++ b/components/sheetdb/actions/search-content/search-content.mjs @@ -0,0 +1,53 @@ +import app from "../../sheetdb.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "sheetdb-search-content", + name: "Search Content", + description: "Search for content in a Google Sheet using the SheetDB API. [See the documentation](https://docs.sheetdb.io/sheetdb-api/search)", + version: "0.0.1", + type: "action", + props: { + app, + params: { + propDefinition: [ + app, + "params", + ], + }, + }, + methods: { + searchContent(args = {}) { + return this.app._makeRequest({ + path: "/search_or", + ...args, + }); + }, + }, + async run({ $ }) { + const { + searchContent, + params, + } = this; + + const response = await searchContent({ + $, + params: Object.entries(utils.parse(params)) + .reduce((acc, [ + key, + value, + ]) => ({ + ...acc, + [key]: value, + }), {}), + }); + + if (!response.length) { + $.export("$summary", "No rows were found."); + return response; + } + + $.export("$summary", `Successfully retrieved \`${response.length}\` row(s).`); + return response; + }, +}; diff --git a/components/sheetdb/actions/update-rows/update-rows.mjs b/components/sheetdb/actions/update-rows/update-rows.mjs new file mode 100644 index 0000000000000..ffc949f8f7b5e --- /dev/null +++ b/components/sheetdb/actions/update-rows/update-rows.mjs @@ -0,0 +1,75 @@ +import app from "../../sheetdb.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "sheetdb-update-rows", + name: "Update Rows", + description: "Updates the content for the specified row(s). [See the documentation](https://docs.sheetdb.io/sheetdb-api/update#update-with-single-query)", + version: "0.0.1", + type: "action", + props: { + app, + columnName: { + propDefinition: [ + app, + "columnName", + ], + }, + columnValue: { + propDefinition: [ + app, + "columnValue", + ], + }, + data: { + label: "Data To Update", + propDefinition: [ + app, + "params", + ], + }, + }, + methods: { + updateRows({ + columnName, columnValue, ...args + } = {}) { + return this.app.patch({ + path: `/${columnName}/${columnValue}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + updateRows, + columnName, + columnValue, + data, + } = this; + + const response = await updateRows({ + $, + columnName, + columnValue, + data: JSON.stringify({ + data: Object.entries(utils.parse(data)) + .reduce((acc, [ + key, + value, + ]) => ({ + ...acc, + [key]: value, + }), {}), + }), + }); + + if (response.error) { + $.export("$summary", "No rows were updated."); + return response; + } + + $.export("$summary", `Successfully updated \`${response.updated}\` row(s).`); + + return response; + }, +}; diff --git a/components/sheetdb/common/utils.mjs b/components/sheetdb/common/utils.mjs new file mode 100644 index 0000000000000..0dc0cc8f7c5a7 --- /dev/null +++ b/components/sheetdb/common/utils.mjs @@ -0,0 +1,55 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function isJson(value) { + try { + JSON.parse(value); + } catch (e) { + return false; + } + + return true; +} + +function parse(value) { + if (!Object.keys(value).length) { + throw new ConfigurationError("Please provide at least one object property."); + } + + if (typeof(value) === "object") { + return value; + } + + if (isJson(value)) { + return JSON.parse(value); + } + + throw new ConfigurationError("Make sure the custom expression contains a valid object"); +} + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +export default { + parse, + parseArray: (value) => parseArray(value).map(parse), +}; diff --git a/components/sheetdb/package.json b/components/sheetdb/package.json new file mode 100644 index 0000000000000..6f23646d04318 --- /dev/null +++ b/components/sheetdb/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/sheetdb", + "version": "0.1.0", + "description": "Pipedream SheetDB Components", + "main": "sheetdb.app.mjs", + "keywords": [ + "pipedream", + "sheetdb" + ], + "homepage": "https://pipedream.com/apps/sheetdb", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/sheetdb/sheetdb.app.mjs b/components/sheetdb/sheetdb.app.mjs new file mode 100644 index 0000000000000..680f4dc896cec --- /dev/null +++ b/components/sheetdb/sheetdb.app.mjs @@ -0,0 +1,93 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "sheetdb", + propDefinitions: { + params: { + type: "object", + label: "Search Query", + description: "An object to specify the search query for finding content in a Google Sheet. The key is the column name and the value is the value to search for in that column. Eg. `{\"column1\": \"My Content\"}`.", + }, + data: { + type: "string[]", + label: "Row Data", + description: "An array of strings representing the rows to create or update in a Google Sheet as JSON objects. The keys inside the object should be the column names, and the values will be filled in the spreadsheet. The rows will be added at the end of the sheet. Eg. `[ { \"column1\": \"My Content\" } ]`", + }, + columnName: { + type: "string", + label: "Column Name", + description: "The name of the column.", + async options() { + const columns = await this.getKeys(); + return columns.map((value) => value); + }, + }, + columnValue: { + type: "string", + label: "Column Value", + description: "The value of the column to match for update/delete operations.", + }, + }, + methods: { + _baseUrl() { + return "https://sheetdb.io/api/v1"; + }, + getHeaders(headers) { + return { + ...headers, + "Authorization": `Bearer ${this.$auth.api_key}`, + "Accept": "application/json", + "Content-Type": "application/json", + }; + }, + async _makeRequest({ + $ = this, path = "", headers, ...args + } = {}) { + try { + const response = await axios($, { + ...args, + debug: true, + url: `${this._baseUrl()}/${this.$auth.api_id}${path}`, + headers: this.getHeaders(headers), + }); + + return response; + + } catch (error) { + const STAUS_ERROR_CODES = [ + 400, + 404, + ]; + if (STAUS_ERROR_CODES.includes(error.response?.status)) { + return error.response.data; + } + throw error; + } + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + patch(args = {}) { + return this._makeRequest({ + method: "PATCH", + ...args, + }); + }, + getKeys(args = {}) { + return this._makeRequest({ + path: "/keys", + ...args, + }); + }, + }, +}; diff --git a/components/shift4/README.md b/components/shift4/README.md new file mode 100644 index 0000000000000..6b635c22bc60c --- /dev/null +++ b/components/shift4/README.md @@ -0,0 +1,11 @@ +# Overview + +The Shift4 API provides robust payment processing capabilities, enabling secure transactions for various business needs. On Pipedream, you can leverage this API to create automated workflows that respond to events, handle payments, and sync transaction data with other services. By integrating Shift4 with Pipedream, you unlock a realm of possibilities for automating payment operations, analyzing sales data, and improving customer experiences with real-time actions. + +# Example Use Cases + +- **Automate Payment Confirmation Emails**: Once a payment is processed via Shift4, trigger a workflow in Pipedream to send a tailored confirmation email to the customer. This could integrate with email services like SendGrid or Mailgun to ensure prompt communication. + +- **Sync Payments with Accounting Software**: After a transaction is completed, use Pipedream to automatically record the payment details in accounting software such as QuickBooks or Xero, facilitating real-time financial reporting and simplifying bookkeeping processes. + +- **Monitor and Alert for Failed Transactions**: Set up a Pipedream workflow that listens for failed payment events from Shift4. When a failure is detected, the workflow can instantly notify your support team via apps like Slack or PagerDuty, allowing for rapid response to resolve customer issues. diff --git a/components/shift4/actions/create-charge/create-charge.mjs b/components/shift4/actions/create-charge/create-charge.mjs new file mode 100644 index 0000000000000..8dcb28236535b --- /dev/null +++ b/components/shift4/actions/create-charge/create-charge.mjs @@ -0,0 +1,129 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { TYPE_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import shift4 from "../../shift4.app.mjs"; + +export default { + key: "shift4-create-charge", + name: "Create Charge", + description: "Creates a new charge object. [See the documentation](https://dev.shift4.com/docs/api#charges-create-a-new-charge)", + version: "0.0.1", + type: "action", + props: { + shift4, + customerId: { + propDefinition: [ + shift4, + "customerId", + ], + optional: true, + }, + amount: { + propDefinition: [ + shift4, + "amount", + ], + }, + currency: { + propDefinition: [ + shift4, + "currency", + ], + }, + type: { + type: "string", + label: "Type", + description: "The type of the charge.", + options: TYPE_OPTIONS, + optional: true, + }, + description: { + propDefinition: [ + shift4, + "description", + ], + optional: true, + }, + card: { + propDefinition: [ + shift4, + "card", + ], + optional: true, + }, + paymentMethod: { + type: "string", + label: "Payment Method", + description: "Payment method details or identifier.", + optional: true, + }, + flow: { + type: "object", + label: "Flow", + description: "Details specific to the payment method charge.", + optional: true, + }, + captured: { + type: "boolean", + label: "Captured", + description: "Whether this charge should be immediately captured.", + optional: true, + }, + shipping: { + type: "object", + label: "Shipping", + description: "Shipping details. Sample object: `{name: \"string\", address: {line1: \"string\", line2: \"string\", zip: \"string\", city: \"string\", state: \"string\", country: \"country represented as two-letter ISO country code\"}}`", + optional: true, + }, + billing: { + type: "object", + label: "Billing", + description: "Billing details. Sample object: `{name: \"string\", email: \"string\", address: {line1: \"string\", line2: \"string\", zip: \"string\", city: \"string\", state: \"string\", country: \"country represented as two-letter ISO country code\"}, vat: \"string\"}`", + optional: true, + }, + threeDSecure: { + type: "object", + label: "3D Secure", + description: "3D Secure options.", + optional: true, + }, + metadata: { + propDefinition: [ + shift4, + "metadata", + ], + optional: true, + }, + }, + async run({ $ }) { + if (!this.customerId && !this.card && !this.paymentMethod) { + throw new ConfigurationError("Either **CustomerId**, **Card** or **PaymentMethod** is required!"); + } + + try { + const response = await this.shift4.createCharge({ + $, + data: { + amount: this.amount, + currency: this.currency, + type: this.type, + description: this.description, + customerId: this.customerId, + card: this.card && parseObject(this.card), + paymentMethod: this.paymentMethod && parseObject(this.paymentMethod), + flow: this.flow && parseObject(this.flow), + captured: this.captured, + shipping: this.shipping && parseObject(this.shipping), + billing: this.billing && parseObject(this.billing), + threeDSecure: this.threeDSecure && parseObject(this.threeDSecure), + metadata: this.metadata && parseObject(this.metadata), + }, + }); + + $.export("$summary", `Successfully created charge with Id: ${response.id}`); + return response; + } catch ({ message }) { + throw new ConfigurationError(JSON.parse(message).error.message); + } + }, +}; diff --git a/components/shift4/actions/create-customer/create-customer.mjs b/components/shift4/actions/create-customer/create-customer.mjs new file mode 100644 index 0000000000000..2f146e254fd1a --- /dev/null +++ b/components/shift4/actions/create-customer/create-customer.mjs @@ -0,0 +1,52 @@ +import { parseObject } from "../../common/utils.mjs"; +import shift4 from "../../shift4.app.mjs"; + +export default { + key: "shift4-create-customer", + name: "Create Customer", + description: "Creates a new customer object. [See the documentation](https://dev.shift4.com/docs/api#customers-create-a-customer)", + version: "0.0.1", + type: "action", + props: { + shift4, + email: { + type: "string", + label: "Email", + description: "The email address of the customer.", + }, + description: { + type: "string", + label: "Description", + description: "A description for the customer.", + optional: true, + }, + card: { + propDefinition: [ + shift4, + "card", + ], + optional: true, + }, + metadata: { + propDefinition: [ + shift4, + "metadata", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.shift4.createCustomer({ + $, + data: { + email: this.email, + description: this.description, + card: this.card && parseObject(this.card), + metadata: this.metadata && parseObject(this.metadata), + }, + }); + + $.export("$summary", `Successfully created customer with Id: ${response.id}`); + return response; + }, +}; diff --git a/components/shift4/actions/create-plan/create-plan.mjs b/components/shift4/actions/create-plan/create-plan.mjs new file mode 100644 index 0000000000000..f0663b00600f3 --- /dev/null +++ b/components/shift4/actions/create-plan/create-plan.mjs @@ -0,0 +1,93 @@ +import { parseObject } from "../../common/utils.mjs"; +import shift4 from "../../shift4.app.mjs"; + +export default { + key: "shift4-create-plan", + name: "Create Plan", + description: "Creates a new plan object. [See the documentation](https://dev.shift4.com/docs/api#plan-create)", + version: "0.0.1", + type: "action", + props: { + shift4, + amount: { + propDefinition: [ + shift4, + "amount", + ], + description: "Subscription charge amount in minor units of a given currency. For example, 10€ is represented as \"1000\", and 10¥ is represented as \"10\".", + }, + currency: { + propDefinition: [ + shift4, + "currency", + ], + description: "Subscription charge currency represented as a three-letter ISO currency code.", + }, + interval: { + type: "string", + label: "Interval", + description: "The interval at which a plan is set to recur. Could be 'day', 'week', 'month', or 'year'.", + options: [ + "day", + "week", + "month", + "year", + ], + }, + name: { + type: "string", + label: "Name", + description: "The name of the plan.", + }, + intervalCount: { + type: "integer", + label: "Interval Count", + description: "The number of intervals between each subscription billing. For example, if `interval`=`month` and `intervalCount`=`3`, subscriptions created with this plan will be billed every 3 months.", + optional: true, + }, + billingCycles: { + type: "integer", + label: "Billing Cycles", + description: "The number of billing cycles for the payment period. If left blank, the subscription will continue indefinitely.", + optional: true, + }, + trialPeriodDays: { + type: "integer", + label: "Trial Period Days", + description: "The number of trial period days granted when subscribing a customer to this plan.", + optional: true, + }, + recursTo: { + propDefinition: [ + shift4, + "recursTo", + ], + optional: true, + }, + metadata: { + propDefinition: [ + shift4, + "metadata", + ], + optional: true, + }, + }, + async run({ $ }) { + const { + shift4, + metadata, + ...data + } = this; + + const response = await shift4.createPlan({ + $, + data: { + ...data, + metadata: metadata && parseObject(metadata), + }, + }); + + $.export("$summary", `Successfully created plan with Id: '${response.id}'`); + return response; + }, +}; diff --git a/components/shift4/actions/create-token/create-token.mjs b/components/shift4/actions/create-token/create-token.mjs new file mode 100644 index 0000000000000..9354fc9723985 --- /dev/null +++ b/components/shift4/actions/create-token/create-token.mjs @@ -0,0 +1,102 @@ +import shift4 from "../../shift4.app.mjs"; + +export default { + key: "shift4-create-token", + name: "Create Token", + description: "Creates a new token object. [See the documentation](https://dev.shift4.com/docs/api#token-create)", + version: "0.0.1", + type: "action", + props: { + shift4, + number: { + type: "string", + label: "Card Number", + description: "Card number without any separators.", + }, + expMonth: { + type: "string", + label: "Expiration Month", + description: "Card expiration month.", + }, + expYear: { + type: "string", + label: "Expiration Year", + description: "Card expiration year.", + }, + cvc: { + type: "string", + label: "CVC", + description: "Card security code.", + }, + cardholderName: { + type: "string", + label: "Cardholder Name", + description: "Name of the cardholder.", + optional: true, + }, + addressLine1: { + type: "string", + label: "Address Line 1", + description: "First line of the address.", + optional: true, + }, + addressLine2: { + type: "string", + label: "Address Line 2", + description: "Second line of the address.", + optional: true, + }, + addressCity: { + type: "string", + label: "City", + description: "City of the address.", + optional: true, + }, + addressState: { + type: "string", + label: "State", + description: "State of the address.", + optional: true, + }, + addressZip: { + type: "string", + label: "Zip Code", + description: "Zip code of the address.", + optional: true, + }, + addressCountry: { + type: "string", + label: "Country", + description: "Country represented as a three-letter ISO country code.", + optional: true, + }, + fraudCheckData: { + type: "object", + label: "Fraud Check Data", + description: "Additional data used for fraud protection.", + optional: true, + }, + }, + async run({ $ }) { + const { + shift4, + fraudCheckData, + ...data + } = this; + + const fraudCheck = fraudCheckData + ? JSON.stringify(fraudCheckData) + : undefined; + + const response = await shift4.createToken({ + $, + data: { + ...data, + fraudCheckData: fraudCheck, + }, + }); + + $.export("$summary", `Successfully created token with Id: '${response.id}'`); + return response; + }, +}; diff --git a/components/shift4/common/constants.mjs b/components/shift4/common/constants.mjs new file mode 100644 index 0000000000000..e90f2dd248e82 --- /dev/null +++ b/components/shift4/common/constants.mjs @@ -0,0 +1,20 @@ +export const LIMIT = 100; + +export const TYPE_OPTIONS = [ + { + label: "First Recurring", + value: "first_recurring", + }, + { + label: "Subsequent Recurring", + value: "subsequent_recurring", + }, + { + label: "Merchant Initiated", + value: "merchant_initiated", + }, + { + label: "Customer Initiated", + value: "customer_initiated", + }, +]; diff --git a/components/shift4/common/utils.mjs b/components/shift4/common/utils.mjs new file mode 100644 index 0000000000000..4b5f3f9d301f8 --- /dev/null +++ b/components/shift4/common/utils.mjs @@ -0,0 +1,22 @@ +export const parseObject = (obj) => { + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/shift4/package.json b/components/shift4/package.json new file mode 100644 index 0000000000000..135a71b28b029 --- /dev/null +++ b/components/shift4/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/shift4", + "version": "0.1.0", + "description": "Pipedream Shift4 Components", + "main": "shift4.app.mjs", + "keywords": [ + "pipedream", + "shift4" + ], + "homepage": "https://pipedream.com/apps/shift4", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" + } +} + diff --git a/components/shift4/shift4.app.mjs b/components/shift4/shift4.app.mjs new file mode 100644 index 0000000000000..4b4ab85a721bf --- /dev/null +++ b/components/shift4/shift4.app.mjs @@ -0,0 +1,191 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "shift4", + propDefinitions: { + amount: { + type: "integer", + label: "Amount", + description: "The charge amount in minor units of a given currency. For example, 10€ is represented as '1000'.", + }, + card: { + type: "string", + label: "Card", + description: "Card token, card details or card identifier.", + }, + currency: { + type: "string", + label: "Currency", + description: "The charge currency represented as a three-letter ISO currency code.", + }, + customerId: { + type: "string", + label: "Customer ID", + description: "Identifier of the customer that will be associated with this charge.", + async options({ prevContext }) { + const { list } = await this.listCustomers({ + params: { + limit: LIMIT, + startingAfterId: prevContext.lastId, + }, + }); + + return { + options: list.map(({ + id: value, email: label, + }) => ({ + label, + value, + })), + context: { + lastId: list.length + ? list[list.length - 1].id + : null, + }, + }; + }, + }, + description: { + type: "string", + label: "Description", + description: "A description for the charge.", + optional: true, + }, + metadata: { + type: "object", + label: "Metadata", + description: "Metadata object.", + }, + orderIdentifier: { + type: "string", + label: "Order Identifier", + description: "The identifier of the order related to the charge that was updated.", + }, + recursTo: { + type: "string", + label: "Recurs To", + description: "The plan to which this plan will recur after the billing cycles have completed.", + async options({ prevContext }) { + const { list } = await this.listPlans({ + params: { + limit: LIMIT, + startingAfterId: prevContext.lastId, + }, + }); + + return { + options: list.map(({ + id: value, name: label, + }) => ({ + label, + value, + })), + context: { + lastId: list.length + ? list[list.length - 1].id + : null, + }, + }; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.shift4.com"; + }, + _auth() { + return { + username: `${this.$auth.api_key_secret}`, + password: "", + }; + }, + _makeRequest({ + $ = this, path, ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + auth: this._auth(), + }); + }, + createCharge(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/charges", + ...opts, + }); + }, + createPlan(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/plans", + ...opts, + }); + }, + createToken(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/tokens", + ...opts, + }); + }, + createCustomer(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/customers", + ...opts, + }); + }, + listCustomers(opts = {}) { + return this._makeRequest({ + path: "/customers", + ...opts, + }); + }, + listEvents(opts = {}) { + return this._makeRequest({ + path: "/events", + ...opts, + }); + }, + listPlans(opts = {}) { + return this._makeRequest({ + path: "/plans", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, filterTypes, ...opts + }) { + let hasMore = false; + let count = 0; + let lastId = null; + + do { + params.limit = LIMIT; + params.startingAfterId = lastId; + const { + list, + hasMore: hasMoreItems, + } = await fn({ + params, + ...opts, + }); + for (const d of list) { + if (filterTypes.includes(d.type)) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + } + + hasMore = hasMoreItems; + + } while (hasMore); + }, + }, +}; diff --git a/components/shift4/sources/charge-updated/charge-updated.mjs b/components/shift4/sources/charge-updated/charge-updated.mjs new file mode 100644 index 0000000000000..a008c95d4c137 --- /dev/null +++ b/components/shift4/sources/charge-updated/charge-updated.mjs @@ -0,0 +1,34 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "shift4-charge-updated", + name: "New Charge Updated", + description: "Emit new event when a charge object is updated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFilterTypes() { + return [ + "CHARGE_SUCCEEDED", + "CHARGE_FAILED", + "CHARGE_UPDATED", + "CHARGE_CAPTURED", + "CHARGE_REFUNDED", + "CHARGE_DISPUTE_CREATED", + "CHARGE_DISPUTE_UPDATED", + "CHARGE_DISPUTE_WON", + "CHARGE_DISPUTE_LOST", + "CHARGE_DISPUTE_FUNDS_WITHDRAWN", + "CHARGE_DISPUTE_FUNDS_RESTORED", + ]; + }, + getSummary(item) { + return `New charge updated event with Id: ${item.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/shift4/sources/charge-updated/test-event.mjs b/components/shift4/sources/charge-updated/test-event.mjs new file mode 100644 index 0000000000000..32f8e23083077 --- /dev/null +++ b/components/shift4/sources/charge-updated/test-event.mjs @@ -0,0 +1,49 @@ +export default { + "id": "event_1234567890", + "created": 1234567890, + "objectType": "event", + "type": "CHARGE_UPDATED", + "data": { + "id": "char_1234567890", + "created": 1234567890, + "objectType": "charge", + "amount": 2000, + "amountRefunded": 0, + "currency": "USD", + "card": { + "id": "card_1234567890", + "created": 1234567890, + "objectType": "card", + "first6": "401200", + "last4": "0007", + "fingerprint": "BMophBOvfsd234h", + "expMonth": "07", + "expYear": "2027", + "cardholderName": "", + "customerId": "cust_1234567890", + "brand": "Visa", + "type": "Credit Card", + "country": "CH", + "addressLine1": "", + "addressLine2": "", + "addressCity": "", + "addressState": "", + "addressZip": "", + "addressCountry": "", + "issuer": "SHIFT4 TEST" + }, + "customerId": "cust_1234567890", + "captured": true, + "refunded": false, + "disputed": false, + "fraudDetails": { + "status": "safe", + "score": 0 + }, + "avsCheck": { + "result": "unavailable" + }, + "status": "successful", + "clientObjectId": "client_char_1234567890" + } +} \ No newline at end of file diff --git a/components/shift4/sources/common/base.mjs b/components/shift4/sources/common/base.mjs new file mode 100644 index 0000000000000..87bd45ff6655a --- /dev/null +++ b/components/shift4/sources/common/base.mjs @@ -0,0 +1,63 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import shift4 from "../../shift4.app.mjs"; + +export default { + props: { + shift4, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(created) { + this.db.set("lastDate", created); + }, + generateMeta(item) { + return { + id: item.id, + summary: this.getSummary(item), + ts: item.created, + }; + }, + async startEvent(maxResults = 0) { + const lastDate = this._getLastDate(); + + const data = this.shift4.paginate({ + fn: this.shift4.listEvents, + maxResults, + params: { + created: { + gt: lastDate, + }, + }, + filterTypes: this.getFilterTypes(), + }); + + const responseArray = []; + for await (const item of data) { + responseArray.push(item); + } + + if (responseArray.length) this._setLastDate(responseArray[0].created); + + for (const item of responseArray.reverse()) { + this.$emit(item, this.generateMeta(item)); + } + }, + }, + hooks: { + async deploy() { + await this.startEvent(25); + }, + }, + async run() { + await this.startEvent(); + }, +}; diff --git a/components/shift4/sources/new-charge/new-charge.mjs b/components/shift4/sources/new-charge/new-charge.mjs new file mode 100644 index 0000000000000..7845c965e78fd --- /dev/null +++ b/components/shift4/sources/new-charge/new-charge.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "shift4-new-charge", + name: "New Charge", + description: "Emit new event when a new charge is successfully created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFilterTypes() { + return [ + "CHARGE_SUCCEEDED", + ]; + }, + getSummary(item) { + return `New charge created event with Id: ${item.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/shift4/sources/new-charge/test-event.mjs b/components/shift4/sources/new-charge/test-event.mjs new file mode 100644 index 0000000000000..17b19cb9c2ffa --- /dev/null +++ b/components/shift4/sources/new-charge/test-event.mjs @@ -0,0 +1,46 @@ +export default { + "id": "event_1234567890", + "created": 1234567890, + "objectType": "event", + "type": "CHARGE_SUCCEEDED", + "data": { + "id": "char_1234567890", + "created": 1234567890, + "objectType": "charge", + "amount": 2000, + "amountRefunded": 0, + "currency": "USD", + "card": { + "id": "card_1234567890", + "created": 1234567890, + "objectType": "card", + "first6": "401200", + "last4": "0007", + "fingerprint": "BMophBO0Q123564mrty2VT", + "expMonth": "07", + "expYear": "2027", + "cardholderName": "", + "customerId": "cust_1234567890", + "brand": "Visa", + "type": "Credit Card", + "country": "CH", + "addressLine1": "", + "addressLine2": "", + "addressCity": "", + "addressState": "", + "addressZip": "", + "addressCountry": "", + "issuer": "SHIFT4 TEST" + }, + "customerId": "cust_1234567890", + "captured": true, + "refunded": false, + "disputed": false, + "avsCheck": { + "result": "unavailable" + }, + "status": "successful", + "clientObjectId": "client_char_1234567890" + }, + "log": "log_1234567890" +} \ No newline at end of file diff --git a/components/shift4/sources/new-customer/new-customer.mjs b/components/shift4/sources/new-customer/new-customer.mjs new file mode 100644 index 0000000000000..e7040ec807132 --- /dev/null +++ b/components/shift4/sources/new-customer/new-customer.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "shift4-new-customer", + name: "New Customer", + description: "Emit new event when a new customer is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFilterTypes() { + return [ + "CUSTOMER_CREATED", + ]; + }, + getSummary(item) { + return `New customer created event with Id: ${item.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/shift4/sources/new-customer/test-event.mjs b/components/shift4/sources/new-customer/test-event.mjs new file mode 100644 index 0000000000000..fd18b7d6a53f8 --- /dev/null +++ b/components/shift4/sources/new-customer/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "id": "event_1234567890", + "created": 1234567890, + "objectType": "event", + "type": "CUSTOMER_CREATED", + "data": { + "id": "cust_1234567890", + "created": 1234567890, + "objectType": "customer", + "email": "email@test.com", + "description": "description", + "metadata": {} + }, + "log": "log_1234567890" +} \ No newline at end of file diff --git a/components/shipcloud/README.md b/components/shipcloud/README.md index 9dbe03171f799..3e62c371dcd94 100644 --- a/components/shipcloud/README.md +++ b/components/shipcloud/README.md @@ -1,24 +1,11 @@ # Overview -With the shipcloud API, you can easily and quickly create solutions for your -business’ shipping requirements. Here are some examples of what you can create -using the shipcloud API: +Shipcloud is a shipping service provider API that streamlines parcel shipping processes for businesses of all sizes. With Shipcloud, you can access multiple carriers, compare shipping rates, create shipping labels, track parcels, and handle returns within a single interface. Utilizing this API on Pipedream enables automation of shipping workflows, leading to efficient and scalable logistics operations. -- Connect your existing systems - Integrate with your existing e-commerce - platform, ERP or marketplace, such as Amazon and Shopify. -- Automate your shipping processes - Send shipping labels with tracking info to - customers and use our webhooks to stay up to date. -- Get real-time carrier rates - Compare automated rates for any combination of - country, weight, and size, and receive delivery notifications. -- Automate returns and refunds - Integrate your shipping process with - third-party logistics providers, allowing customers to return and exchange - items easily. -- Create custom tracking portals - Design custom tracking pages tailored to - your needs and branded with your logo. -- Monitor carrier performance - Get in-depth statistics and analytics of your - shipment delivery performance, identify trends, and give customers accurate - shipping info. +# Example Use Cases -This is just a small sample of what you can create with the shipcloud API – the -possibilities are truly endless. With shipcloud, you can make sure that your -business shipping needs are taken care of quickly and easily. +- **Automated Order Fulfillment**: Integrate Shipcloud with an e-commerce platform like Shopify on Pipedream. When a new order is placed, the workflow triggers automatically, creating a shipping label with Shipcloud and updating the order status with the tracking information. + +- **Inventory Management Sync**: Connect Shipcloud to an inventory management app like Airtable. When a shipment status updates to 'delivered' on Shipcloud, the workflow updates the inventory count in Airtable, ensuring stock levels are always accurate. + +- **Customer Notification System**: Pair Shipcloud with a CRM platform like HubSpot. Set up a workflow where, upon shipment creation or status change, an automated email or SMS is sent to the customer with the tracking details and expected delivery date, improving customer experience. diff --git a/components/shipday/README.md b/components/shipday/README.md new file mode 100644 index 0000000000000..0d15d294da159 --- /dev/null +++ b/components/shipday/README.md @@ -0,0 +1,11 @@ +# Overview + +The Shipday API lets you manage and automate delivery operations seamlessly. By integrating with Pipedream, you can orchestrate workflows that trigger from various events, sync data between services, and automate notifications or order tracking. With Pipedream's serverless platform, you can craft custom workflows that react in real-time to changes in delivery statuses, new order placements, or updates from other integrated services. This flexibility allows businesses to enhance their delivery processes, improve customer communication, and streamline operations. + +# Example Use Cases + +- **Automate Order Dispatch**: When a new order is received in your e-commerce platform (like Shopify), use Shipday's API within Pipedream to automatically create a delivery. This cuts down on manual entry and speeds up the delivery process. + +- **Real-time Delivery Status Updates**: Set up a workflow that listens for webhook events from Shipday for delivery status changes. Whenever a delivery status is updated, trigger a Pipedream workflow to send real-time notifications to customers via email or SMS, keeping them informed every step of the way. + +- **Synchronize Delivery Data with Google Sheets**: As deliveries are completed, use Pipedream to capture data from Shipday and append it to a Google Sheet. This workflow is useful for generating real-time reports, tracking delivery performance, or maintaining records without manual data entry. diff --git a/components/shipengine/README.md b/components/shipengine/README.md index a67af37ccd47a..04b0eb24e33c6 100644 --- a/components/shipengine/README.md +++ b/components/shipengine/README.md @@ -1,24 +1,11 @@ # Overview -Using ShipEngine's API, you can build software applications that enable -seamless and powerful e-commerce shipping experiences for customers, -businesses, and marketplace integrations. +The Shipengine API enables robust automation possibilities for shipping logistics. From label creation, rate comparison, to tracking shipments, the API provides the tools to streamline e-commerce and shipping workflows. By harnessing Shipengine's capabilities through Pipedream, you can build seamless integrations that connect your shipping operations with other business processes, enhancing efficiency and reducing manual overhead. -The ShipEngine API enables you to access the full range of shipping features, -from calculating rates and creating shipments to tracking packages and printing -labels. With the ShipEngine API, you can effortlessly bring top-tier shipping -capabilities to your business, marketplace, or website. +# Example Use Cases -Some of the things you can build with ShipEngine's API include: +- **Automated Shipping Label Creation**: Trigger a workflow on Pipedream when an order is placed in your e-commerce platform, like Shopify or WooCommerce. Automatically create a shipping label with Shipengine, attach the label to the customer's order details, and update the order status to "Shipped." -- Instantly provide customers with accurate shipping rates, tailored to their - region, package weight, and other factors -- Create custom fulfillment process and manage orders throughout the order - lifecycle -- Automate the quoting, printing, and tracking of shipments -- Offer discounted shipping rates on multiple carriers -- Access detailed tracking information for packages, including estimated - delivery date, current location in transit, and any delivery exceptions -- Schedule pickups from carriers and print return labels -- Integrate with multiple carriers such as UPS, FedEx, and USPS, with more - being added all the time +- **Real-time Shipping Rates Comparison**: Set up a Pipedream workflow that triggers when a customer reaches the checkout page on your website. Call Shipengine to fetch real-time shipping rates from multiple carriers, then present the best options to the customer, optimizing for cost or delivery speed. + +- **Order Tracking and Notifications**: Implement a workflow on Pipedream that monitors the status of shipments via Shipengine's tracking API. When a shipment status changes (e.g., "Out for Delivery"), send an automatic update to the customer through email or SMS, using services like SendGrid or Twilio. diff --git a/components/shiphero/.gitignore b/components/shiphero/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/shiphero/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/shiphero/README.md b/components/shiphero/README.md new file mode 100644 index 0000000000000..b620f6bdf2d40 --- /dev/null +++ b/components/shiphero/README.md @@ -0,0 +1,11 @@ +# Overview + +The ShipHero API enables granular control over warehousing and order fulfillment processes, offering endpoints for managing inventory, orders, returns, and shipping. In Pipedream, you can leverage this API to automate routine tasks, sync data across multiple platforms, and trigger actions based on specific events. This can save time, reduce errors, and increase operational efficiency for e-commerce businesses. + +# Example Use Cases + +- **Automated Order Processing**: Use ShipHero to automatically process orders when they are received from an e-commerce platform like Shopify or WooCommerce. When a new order is placed, Pipedream can trigger a workflow that creates a corresponding order in ShipHero, streamlining the fulfillment process without manual intervention. + +- **Inventory Level Syncing**: Keep inventory levels in sync across multiple sales channels. When inventory updates occur in ShipHero, a Pipedream workflow can propagate these changes to other platforms, such as Amazon or eBay, ensuring accurate stock levels and preventing overselling. + +- **Return Management Automation**: Simplify the returns process by automating return label generation and refund processing. When a customer initiates a return, Pipedream can automatically create a return label via ShipHero and update the order status in your CRM or customer service platform, like Zendesk, to keep all stakeholders informed. diff --git a/components/shiphero/actions/get-order/get-order.mjs b/components/shiphero/actions/get-order/get-order.mjs new file mode 100644 index 0000000000000..13e0eff6d351d --- /dev/null +++ b/components/shiphero/actions/get-order/get-order.mjs @@ -0,0 +1,41 @@ +import app from "../../shiphero.app.mjs"; +import orderQueries from "../../common/queries/order.mjs"; + +export default { + key: "shiphero-get-order", + name: "Get Order", + description: "Get an order. [See the documentation](https://developer.shiphero.com/getting-started/)", + type: "action", + version: "0.0.1", + props: { + app, + orderId: { + propDefinition: [ + app, + "orderId", + ], + }, + }, + methods: { + getOrder(variables = {}) { + return this.app.makeRequest({ + query: orderQueries.getOrder, + variables, + }); + }, + }, + async run({ $: step }) { + const { + // eslint-disable-next-line no-unused-vars + app, + getOrder, + ...variables + } = this; + + const response = await getOrder(variables); + + step.export("$summary", `Successfully retrieved order with request ID \`${response.order.request_id}\`.`); + + return response; + }, +}; diff --git a/components/shiphero/actions/list-products/list-products.mjs b/components/shiphero/actions/list-products/list-products.mjs new file mode 100644 index 0000000000000..34df1cf1f49db --- /dev/null +++ b/components/shiphero/actions/list-products/list-products.mjs @@ -0,0 +1,83 @@ +import app from "../../shiphero.app.mjs"; +import productQueries from "../../common/queries/product.mjs"; + +export default { + key: "shiphero-list-products", + name: "List Products", + description: "List products. [See the documentation](https://developer.shiphero.com/getting-started/)", + type: "action", + version: "0.0.1", + props: { + app, + sku: { + type: "string", + label: "SKU", + description: "The SKU of the product.", + optional: true, + }, + createdFrom: { + type: "string", + label: "Created From", + description: "The date the product was created from. A DateTime field type that understand **ISO 8601** strings, besides datetime objects. It supports strings with and without times, as well as using `T` or space as delimiter\nEg.\n- `YYYY-mm-dd`\n- `YYYY-mm-dd HH:MM:SS`\n - `YYYY-mm-ddTHH:MM:SS`", + optional: true, + }, + createdTo: { + type: "string", + label: "Created To", + description: "The date the product was created to. A DateTime field type that understand **ISO 8601** strings, besides datetime objects. It supports strings with and without times, as well as using `T` or space as delimiter\nEg.\n- `YYYY-mm-dd`\n- `YYYY-mm-dd HH:MM:SS`\n - `YYYY-mm-ddTHH:MM:SS`", + optional: true, + }, + updatedFrom: { + type: "string", + label: "Updated From", + description: "The date the product was updated from. A DateTime field type that understand **ISO 8601** strings, besides datetime objects. It supports strings with and without times, as well as using `T` or space as delimiter\nEg.\n- `YYYY-mm-dd`\n- `YYYY-mm-dd HH:MM:SS`\n - `YYYY-mm-ddTHH:MM:SS`", + optional: true, + }, + updatedTo: { + type: "string", + label: "Updated To", + description: "The date the product was updated to. A DateTime field type that understand **ISO 8601** strings, besides datetime objects. It supports strings with and without times, as well as using `T` or space as delimiter\nEg.\n- `YYYY-mm-dd`\n- `YYYY-mm-dd HH:MM:SS`\n - `YYYY-mm-ddTHH:MM:SS`", + optional: true, + }, + customerAccountId: { + type: "string", + label: "Customer Account ID", + description: "The ID of the customer account.", + optional: true, + }, + hasKits: { + type: "boolean", + label: "Has Kits", + description: "Whether the product has kits.", + optional: true, + }, + analyze: { + type: "boolean", + label: "Analyze", + description: "Whether to analyze the product.", + optional: true, + }, + }, + methods: { + listProducts(variables = {}) { + return this.app.makeRequest({ + query: productQueries.listProducts, + variables, + }); + }, + }, + async run({ $: step }) { + const { + // eslint-disable-next-line no-unused-vars + app, + listProducts, + ...variables + } = this; + + const response = await listProducts(variables); + + step.export("$summary", `Successfully retrieved products with request ID \`${response.products.request_id}\`.`); + + return response; + }, +}; diff --git a/components/shiphero/actions/update-order/update-order.mjs b/components/shiphero/actions/update-order/update-order.mjs new file mode 100644 index 0000000000000..42be05702f6eb --- /dev/null +++ b/components/shiphero/actions/update-order/update-order.mjs @@ -0,0 +1,131 @@ +import app from "../../shiphero.app.mjs"; +import orderMutations from "../../common/mutations/order.mjs"; + +export default { + key: "shiphero-update-order", + name: "Update Order", + description: "Update an order. [See the documentation](https://developer.shiphero.com/getting-started/)", + type: "action", + version: "0.0.1", + props: { + app, + orderId: { + propDefinition: [ + app, + "orderId", + ], + }, + orderNumber: { + type: "string", + label: "Order Number", + description: "The order number.", + optional: true, + }, + partnerOrderId: { + type: "string", + label: "Partner Order ID", + description: "The partner order ID.", + optional: true, + }, + fulfillmentStatus: { + type: "string", + label: "Fulfillment Status", + description: "The fulfillment status.", + optional: true, + }, + orderDate: { + type: "string", + label: "Order Date", + description: "The order date. A DateTime field type that understand **ISO 8601** strings, besides datetime objects. It supports strings with and without times, as well as using `T` or space as delimiter\nEg.\n- `YYYY-mm-dd`\n- `YYYY-mm-dd HH:MM:SS`\n - `YYYY-mm-ddTHH:MM:SS`", + optional: true, + }, + totalTax: { + type: "string", + label: "Total Tax", + description: "The total tax for the order.", + optional: true, + }, + subtotal: { + type: "string", + label: "Subtotal", + description: "The subtotal for the order.", + optional: true, + }, + totalDiscounts: { + type: "string", + label: "Total Discounts", + description: "The total discounts for the order.", + optional: true, + }, + totalPrice: { + type: "string", + label: "Total Price", + description: "The total price for the order.", + optional: true, + }, + customInvoiceUrl: { + type: "string", + label: "Custom Invoice URL", + description: "The custom invoice URL for the order.", + optional: true, + }, + profile: { + type: "string", + label: "Profile", + description: "The profile for the order.", + optional: true, + }, + packingNote: { + type: "string", + label: "Packing Note", + description: "The packing note for the order.", + optional: true, + }, + requiredShipDate: { + type: "string", + label: "Required Ship Date", + description: "The required ship date for the order. A DateTime field type that understand **ISO 8601** strings, besides datetime objects. It supports strings with and without times, as well as using `T` or space as delimiter\nEg.\n- `YYYY-mm-dd`\n- `YYYY-mm-dd HH:MM:SS`\n - `YYYY-mm-ddTHH:MM:SS`", + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "The tags for the order.", + optional: true, + }, + priorityFlag: { + type: "boolean", + label: "Priority Flag", + description: "The priority flag for the order.", + optional: true, + }, + giftNote: { + type: "string", + label: "Gift Note", + description: "The gift note for the order.", + optional: true, + }, + }, + methods: { + updateOrder(variables = {}) { + return this.app.makeRequest({ + query: orderMutations.updateOrder, + variables, + }); + }, + }, + async run({ $: step }) { + const { + // eslint-disable-next-line no-unused-vars + app, + updateOrder, + ...variables + } = this; + + const response = await updateOrder(variables); + + step.export("$summary", `Successfully updated order with request ID \`${response.order_update.request_id}\`.`); + + return response; + }, +}; diff --git a/components/shiphero/app/shiphero.app.ts b/components/shiphero/app/shiphero.app.ts deleted file mode 100644 index 997a553f4f00c..0000000000000 --- a/components/shiphero/app/shiphero.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "shiphero", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/shiphero/common/constants.mjs b/components/shiphero/common/constants.mjs new file mode 100644 index 0000000000000..40bd222a59a0b --- /dev/null +++ b/components/shiphero/common/constants.mjs @@ -0,0 +1,11 @@ +const BASE_URL = "https://public-api.shiphero.com"; +const VERSION_PATH = "/graphql"; +const DEFAULT_LIMIT = 20; +const SHARED_SIGNATURE_SECRET = "sharedSignatureSecret"; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_LIMIT, + SHARED_SIGNATURE_SECRET, +}; diff --git a/components/shiphero/common/mutations/order.mjs b/components/shiphero/common/mutations/order.mjs new file mode 100644 index 0000000000000..141efdc9d82a2 --- /dev/null +++ b/components/shiphero/common/mutations/order.mjs @@ -0,0 +1,92 @@ +import { gql } from "graphql-request"; + +export default { + updateOrder: gql` + mutation updateOrder( + $orderId: String!, + $orderNumber: String, + $partnerOrderId: String, + $fulfillmentStatus: String, + $orderDate: ISODateTime, + $totalTax: String, + $subtotal: String, + $totalDiscounts: String, + $totalPrice: String, + $customInvoiceUrl: String, + $profile: String, + $giftNote: String, + $packingNote: String, + $requiredShipDate: ISODateTime, + $tags: [String], + $priorityFlag: Boolean + ) { + order_update( + data: { + order_id: $orderId, + order_number: $orderNumber, + partner_order_id: $partnerOrderId, + fulfillment_status: $fulfillmentStatus, + order_date: $orderDate, + total_tax: $totalTax, + subtotal: $subtotal, + total_discounts: $totalDiscounts, + total_price: $totalPrice, + custom_invoice_url: $customInvoiceUrl, + profile: $profile, + gift_note: $giftNote, + packing_note: $packingNote, + required_ship_date: $requiredShipDate, + tags: $tags, + priority_flag: $priorityFlag + } + ) { + request_id + order { + id + order_number + partner_order_id + shop_name + fulfillment_status + order_date + total_tax + subtotal + total_discounts + total_price + box_name + auto_print_return_label + custom_invoice_url + account_id + updated_at + email + profile + gift_note + packing_note + required_ship_date + tags + priority_flag + attachments { + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + total_count + edges { + cursor + node { + id + url + description + filename + file_type + file_size + created_at + } + } + } + } + } + } + `, +}; diff --git a/components/shiphero/common/mutations/webhook.mjs b/components/shiphero/common/mutations/webhook.mjs new file mode 100644 index 0000000000000..3d0f0ca3f33fd --- /dev/null +++ b/components/shiphero/common/mutations/webhook.mjs @@ -0,0 +1,45 @@ +import { gql } from "graphql-request"; + +export default { + createWebhook: gql` + mutation createWebhook( + $name: String! + $url: String! + $shopName: String! + ) { + webhook_create( + data: { + name: $name, + url: $url, + shop_name: $shopName + } + ) { + request_id + webhook { + id + account_id + shop_name + url + name + source + shared_signature_secret + } + } + } + `, + deleteWebhook: gql` + mutation deleteWebhook( + $name: String! + $shopName: String! + ) { + webhook_delete( + data: { + name: $name + shop_name: $shopName + } + ) { + request_id + } + } + `, +}; diff --git a/components/shiphero/common/queries/order.mjs b/components/shiphero/common/queries/order.mjs new file mode 100644 index 0000000000000..f891bf4605e27 --- /dev/null +++ b/components/shiphero/common/queries/order.mjs @@ -0,0 +1,121 @@ +import { gql } from "graphql-request"; + +export default { + getOrder: gql` + query getOrder( + $orderId: String! + $analyze: Boolean + ) { + order( + id: $orderId + analyze: $analyze + ) { + request_id + data { + id + order_number + partner_order_id + shop_name + fulfillment_status + order_date + total_tax + subtotal + total_discounts + total_price + box_name + auto_print_return_label + custom_invoice_url + account_id + updated_at + email + profile + gift_note + packing_note + required_ship_date + tags + priority_flag + attachments { + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + total_count + edges { + cursor + node { + id + url + description + filename + file_type + file_size + created_at + } + } + } + } + } + } + `, + listOrders: gql` + query listOrders( + $shopName: String + $orderNumber: String + $warehouseId: String + $sku: String + $email: String + $updatedFrom: ISODateTime + $updatedTo: ISODateTime + $orderDateFrom: ISODateTime + $orderDateTo: ISODateTime + $customerAccountId: String + $analyze: Boolean + $sort: String + $before: String + $after: String + $first: Int + $last: Int + ) { + orders( + shop_name: $shopName + order_number: $orderNumber + warehouse_id: $warehouseId + sku: $sku + email: $email + updated_from: $updatedFrom + updated_to: $updatedTo + order_date_from: $orderDateFrom + order_date_to: $orderDateTo + customer_account_id: $customerAccountId + analyze: $analyze + ) { + request_id + data ( + sort: $sort + before: $before + after: $after + first: $first + last: $last + ) { + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + edges { + cursor + node { + id + order_number + shop_name + order_date + } + } + } + } + } + `, +}; diff --git a/components/shiphero/common/queries/product.mjs b/components/shiphero/common/queries/product.mjs new file mode 100644 index 0000000000000..410221f6c71d9 --- /dev/null +++ b/components/shiphero/common/queries/product.mjs @@ -0,0 +1,89 @@ +import { gql } from "graphql-request"; + +export default { + listProducts: gql` + query listProducts( + $sku: String + $createdFrom: ISODateTime + $createdTo: ISODateTime + $updatedFrom: ISODateTime + $updatedTo: ISODateTime + $customerAccountId: String + $hasKits: Boolean + $analyze: Boolean + $sort: String + $before: String + $after: String + $first: Int + $last: Int + ) { + products( + sku: $sku + created_from: $createdFrom + created_to: $createdTo + updated_from: $updatedFrom + updated_to: $updatedTo + customer_account_id: $customerAccountId + has_kits: $hasKits + analyze: $analyze + ) { + request_id + data( + sort: $sort + before: $before + after: $after + first: $first + last: $last + ) { + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + edges { + node { + id + legacy_id + account_id + name + sku + barcode + country_of_manufacture + tariff_code + kit + kit_build + no_air + final_sale + customs_value + customs_description + not_owned + dropship + needs_serial_number + thumbnail + large_thumbnail + created_at + updated_at + product_note + virtual + ignore_on_invoice + ignore_on_customs + needs_lot_tracking + images { + src + position + } + tags + vendors { + vendor_id + vendor_sku + price + } + packer_note + } + } + } + } + } + `, +}; diff --git a/components/shiphero/package.json b/components/shiphero/package.json index 118a9079acaa0..b5c0d73a97dd6 100644 --- a/components/shiphero/package.json +++ b/components/shiphero/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/shiphero", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream ShipHero Components", "main": "dist/app/shiphero.app.mjs", "keywords": [ "pipedream", "shiphero" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/shiphero", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "graphql-request": "^7.1.0" } } diff --git a/components/shiphero/shiphero.app.mjs b/components/shiphero/shiphero.app.mjs new file mode 100644 index 0000000000000..9ca0514cff7e8 --- /dev/null +++ b/components/shiphero/shiphero.app.mjs @@ -0,0 +1,83 @@ +import "graphql/language/index.js"; +import { GraphQLClient } from "graphql-request"; +import constants from "./common/constants.mjs"; +import orderQueries from "./common/queries/order.mjs"; + +export default { + type: "app", + app: "shiphero", + propDefinitions: { + orderId: { + type: "string", + label: "Order ID", + description: "The ID of the order.", + async options({ prevContext: { after } }) { + if (after === null) { + return []; + } + const { + orders: { + data: { + edges, pageInfo: { + hasNextPage, endCursor, + }, + }, + }, + } = + await this.listOrders({ + first: constants.DEFAULT_LIMIT, + after, + sort: "-order_date", + }); + return { + options: edges.map(({ + node: { + id: value, order_number: label, + }, + }) => ({ + label, + value, + })), + context: { + after: hasNextPage + ? endCursor + : null, + }, + }; + }, + }, + }, + methods: { + getConfig(conf) { + return { + ...conf, + headers: { + ...conf?.headers, + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "Content-Type": "application/json", + }, + }; + }, + getClient(conf) { + return new GraphQLClient( + `${constants.BASE_URL}${constants.VERSION_PATH}`, + this.getConfig(conf), + ); + }, + async makeRequest({ + headers, query, variables, + } = {}) { + const response = await this.getClient(headers).request(query, variables); + if (response.errors?.length) { + throw new Error(JSON.stringify(response, null, 2)); + } + return response; + }, + listOrders(variables = {}) { + return this.makeRequest({ + query: orderQueries.listOrders, + variables, + }); + }, + }, +}; diff --git a/components/shiphero/sources/common/events.mjs b/components/shiphero/sources/common/events.mjs new file mode 100644 index 0000000000000..abb45d5f71bb9 --- /dev/null +++ b/components/shiphero/sources/common/events.mjs @@ -0,0 +1,13 @@ +export default { + INVENTORY_UPDATE: "Inventory Update", + SHIPMENT_UPDATE: "Shipment Update", + ORDER_CANCELED: "Order Canceled", + PO_UPDATE: "PO Update", + RETURN_UPDATE: "Return Update", + TOTE_COMPLETE: "Tote Complete", + TOTE_CLEARED: "Tote Cleared", + ORDER_PACKED_OUT: "Order Packed Out", + PACKAGE_ADDED: "Package Added", + ORDER_ALLOCATED: "Order Allocated", + ORDER_DEALLOCATED: "Order Deallocated", +}; diff --git a/components/shiphero/sources/common/webhook.mjs b/components/shiphero/sources/common/webhook.mjs new file mode 100644 index 0000000000000..8c5f2f822df7b --- /dev/null +++ b/components/shiphero/sources/common/webhook.mjs @@ -0,0 +1,118 @@ +import { createHmac } from "crypto"; +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../shiphero.app.mjs"; +import constants from "../../common/constants.mjs"; +import webhookMutations from "../../common/mutations/webhook.mjs"; + +export default { + props: { + app, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + shopName: { + type: "string", + label: "Shop Name", + description: "The name of the shop. Eg: `pipedream-api`", + }, + }, + hooks: { + async activate() { + const { + http: { endpoint: url }, + createWebhook, + getEventName, + setSharedSignatureSecret, + shopName, + } = this; + const response = + await createWebhook({ + url, + name: getEventName(), + shopName, + }); + setSharedSignatureSecret(response.webhook_create.webhook.shared_signature_secret); + }, + async deactivate() { + const { + deleteWebhook, + getEventName, + shopName, + } = this; + await deleteWebhook({ + name: getEventName(), + shopName, + }); + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + setSharedSignatureSecret(value) { + this.db.set(constants.SHARED_SIGNATURE_SECRET, value); + }, + getSharedSignatureSecret() { + return this.db.get(constants.SHARED_SIGNATURE_SECRET); + }, + getEventName() { + throw new ConfigurationError("getEventName is not implemented"); + }, + processResource(resource) { + this.$emit(resource, this.generateMeta(resource)); + }, + verifySignature(bodyRaw, signature) { + const signatureSecret = this.getSharedSignatureSecret(); + const hash = createHmac("sha256", signatureSecret) + .update(bodyRaw) + .digest("base64"); + return hash === signature; + }, + createWebhook(variables = {}) { + return this.app.makeRequest({ + query: webhookMutations.createWebhook, + variables, + }); + }, + deleteWebhook(variables = {}) { + return this.app.makeRequest({ + query: webhookMutations.deleteWebhook, + variables, + }); + }, + }, + async run({ + body, bodyRaw, headers, + }) { + const signature = headers["x-shiphero-hmac-sha256"]; + const { + http, + verifySignature, + processResource, + } = this; + + if (!signature) { + console.log("No signature found"); + return http.respond({ + status: 401, + }); + } + + const isValidSignature = verifySignature(bodyRaw, signature); + + if (!isValidSignature) { + console.log("Invalid signature"); + return http.respond({ + status: 401, + }); + } + + http.respond({ + status: 200, + }); + + processResource(body); + }, +}; diff --git a/components/shiphero/sources/order-allocated-instant/order-allocated-instant.mjs b/components/shiphero/sources/order-allocated-instant/order-allocated-instant.mjs new file mode 100644 index 0000000000000..235a0e0e6ef1d --- /dev/null +++ b/components/shiphero/sources/order-allocated-instant/order-allocated-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "shiphero-order-allocated-instant", + name: "Order Allocated (Instant)", + description: "Emit new event when an order is allocated. [See the documentation](https://developer.shiphero.com/webhooks#webhook_create).", + type: "source", + version: "0.0.1", + dedupe: "unique", + methods: { + ...common.methods, + getEventName() { + return events.ORDER_ALLOCATED; + }, + generateMeta(resource) { + return { + id: resource.order_id, + summary: `Order Allocated: ${resource.order_id}`, + ts: Date.parse(resource.allocated_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/shiphero/sources/order-allocated-instant/test-event.mjs b/components/shiphero/sources/order-allocated-instant/test-event.mjs new file mode 100644 index 0000000000000..66a913bb70fbc --- /dev/null +++ b/components/shiphero/sources/order-allocated-instant/test-event.mjs @@ -0,0 +1,25 @@ +export default { + "webhook_type": "Order Allocated", + "allocation_reference": 362137624, + "account_id": 6334, + "account_uuid": "QWNjb3VudDo2MzM0", + "warehouse_id": 11790, + "warehouse_uuid": "V2FyZWhvdXNlOjExNzkw", + "allocated_at": "2021-07-24T02:03:01", + "order_number": "MO351", + "order_id": 204159062, + "order_uuid": "T3JkZXI6MjA0MTU5MDYy", + "partner_order_id": "MO351", + "line_items": [ + { + "id": 550365415, + "item_uuid": "TGluZUl0ZW06NTUwMzY1NDE1", + "partner_line_item_id": "MO351-301706334", + "quantity": 1, + "sku": "1122335364", + "is_kit_component": false, + "created_at": "2021-07-24T02:03:01" + } + ], + "ready_to_ship": 1 +}; diff --git a/components/shiphero/sources/order-deallocated-instant/order-deallocated-instant.mjs b/components/shiphero/sources/order-deallocated-instant/order-deallocated-instant.mjs new file mode 100644 index 0000000000000..fb1f04bcf5f8e --- /dev/null +++ b/components/shiphero/sources/order-deallocated-instant/order-deallocated-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "shiphero-order-deallocated-instant", + name: "Order Deallocated (Instant)", + description: "Emit new event when an order is deallocated. [See the documentation](https://developer.shiphero.com/webhooks#webhook_create).", + type: "source", + version: "0.0.1", + dedupe: "unique", + methods: { + ...common.methods, + getEventName() { + return events.ORDER_DEALLOCATED; + }, + generateMeta(resource) { + return { + id: resource.order_id, + summary: `Order Deallocated: ${resource.order_id}`, + ts: Date.parse(resource.deallocated_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/shiphero/sources/order-deallocated-instant/test-event.mjs b/components/shiphero/sources/order-deallocated-instant/test-event.mjs new file mode 100644 index 0000000000000..b93d9f4c0f8ce --- /dev/null +++ b/components/shiphero/sources/order-deallocated-instant/test-event.mjs @@ -0,0 +1,11 @@ +export default { + "webhook_type": "Order Deallocated", + "allocation_reference": 362137624, + "account_id": 6334, + "account_uuid": "QWNjb3VudDo2MzM0", + "order_id": 204159062, + "order_number": "MO351", + "order_uuid": "T3JkZXI6MjA0MTU5MDYy", + "partner_order_id": "MO351", + "deallocated_at": "2021-07-24T02:07:56" +}; diff --git a/components/shiphero/sources/order-packed-out-instant/order-packed-out-instant.mjs b/components/shiphero/sources/order-packed-out-instant/order-packed-out-instant.mjs new file mode 100644 index 0000000000000..adc96bf2190be --- /dev/null +++ b/components/shiphero/sources/order-packed-out-instant/order-packed-out-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "shiphero-order-packed-out-instant", + name: "Order Packed Out (Instant)", + description: "Emit new event when an order is packed out. [See the documentation](https://developer.shiphero.com/webhooks#webhook_create).", + type: "source", + version: "0.0.1", + dedupe: "unique", + methods: { + ...common.methods, + getEventName() { + return events.ORDER_PACKED_OUT; + }, + generateMeta(resource) { + return { + id: resource.order_id, + summary: `Order Packed Out: ${resource.order_id}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/shiphero/sources/order-packed-out-instant/test-event.mjs b/components/shiphero/sources/order-packed-out-instant/test-event.mjs new file mode 100644 index 0000000000000..ec094273320e8 --- /dev/null +++ b/components/shiphero/sources/order-packed-out-instant/test-event.mjs @@ -0,0 +1,17 @@ +export default { + "webhook_type": "Order Packed Out", + "tote_id": "14156034", + "tote_uuid": "VG90ZToxNDE1NjAzNA==", + "tote_name": "Tote-14156034", + "order_id": 203849703, + "order_uuid": "T3JkZXI6MjAzODQ5NzAz", + "items": [ + { + "order_id": 203849703, + "order_uuid": "T3JkZXI6MjAzODQ5NzAz", + "line_item_id": 549638170, + "pick_id": 123425, + "sku": "1122335454" + } + ] +}; diff --git a/components/shippotoken/README.md b/components/shippotoken/README.md index ff9f75fe96b1c..686863238e73c 100644 --- a/components/shippotoken/README.md +++ b/components/shippotoken/README.md @@ -1,15 +1,11 @@ # Overview -Shippo's API allows developers to quickly integrate their own software with -shipping and tracking systems. With the API, developers can create applications -that expand the capabilities of shipping and tracking services. Here are a few -examples of what you could build with the Shippo API: - -- Automatically create shipping labels for your ecommerce business -- Monitor incoming shipments -- Get real-time tracking updates for your customers -- Automatically compare different shipping services and get the best rate -- Pre-fill customs paperwork from order information -- Generate return labels -- Store and re-use addresses and shipping preferences -- Allow customers to print their own shipping labels +The Shippo API lets you streamline and automate a wide range of shipping tasks from creating labels to tracking packages. With Pipedream, you can connect Shippo to various other services and trigger actions based on shipment statuses, rates, and other shipping events. Automate notifications, sync tracking data with customer databases, or kick off custom logistics workflows. + +# Example Use Cases + +- **Automated Shipping Notifications**: Trigger an email or SMS to customers using services like SendGrid or Twilio when a package is shipped, out for delivery, or delivered. This enhances customer service by providing timely updates. + +- **E-commerce Order Fulfillment**: Connect Shippo to an e-commerce platform like Shopify. When a new order is placed, automatically generate shipping labels and customs documents, then update the order status with the tracking number. + +- **Inventory Management**: When a shipment is marked as delivered, use Pipedream to trigger an inventory update in a database like Airtable or Google Sheets, ensuring inventory counts are accurate and up-to-date. diff --git a/components/shipstation/README.md b/components/shipstation/README.md index 70407edf8cae4..5900e193fcb4d 100644 --- a/components/shipstation/README.md +++ b/components/shipstation/README.md @@ -1,35 +1,11 @@ # Overview -ShipStation API provides an easy way for developers to access and integrate -shipping data into their applications. Using Shipstation API, developers can -easily add shipping capabilities to their applications and build applications -in the following areas: +The ShipStation API allows for streamlined management of shipping operations for e-commerce. With it, you can automate order processing, label creation, and tracking updates. Pipedream excels as a platform for integrating ShipStation with other services to create custom workflows. -- Shipping rate and label generation -- Automating fulfillment processes -- Order entry tracking -- Tracking package status -- Integrating with multi-carrier solutions -- Automating data exchange with third-party applications -- Access and customize order information -- Generating custom shipping documents -- Estimating shipping cost calculations -- Accessing order history -- Automated order importing and routing -- Shipping data analytics -- Analyzing order data for customer and marketing insights +# Example Use Cases -ShipStation APIs easily integrate into existing applications and provide a -great way to add shipping capabilities to your app. With ShipStation API, -developers can build the following applications: +- **Order Sync and Notification**: Create a workflow that syncs new orders from your e-commerce platform into ShipStation, then automatically sends a notification via Slack or email when the order is dispatched. This keeps teams instantly informed about order statuses. -- Shopping Cart Integrations -- Shipping Label Generator -- E-Commerce Platforms -- Shipping Tracking Analytics Platforms -- Order and Tracking Management Platforms -- Multi-carrier Shipping Solutions -- Supplier Management Platforms -- Returns Management Platforms -- Logistics Platforms -- Shipping Cost Estimation Platforms +- **Inventory Management**: Set up a workflow that monitors ShipStation for shipped orders and updates the inventory counts in a database or an app like Airtable. This can trigger reorder notifications when stock levels fall below a certain threshold, ensuring inventory is always replenished. + +- **Customer Support Automation**: Implement a workflow where, after an order is shipped, a follow-up email is sent to the customer with personalized content based on their purchase history. Integrate with a CRM like HubSpot to tailor the outreach and enhance customer experience. diff --git a/components/shopify/actions/add-product-to-custom-collection/add-product-to-custom-collection.mjs b/components/shopify/actions/add-product-to-custom-collection/add-product-to-custom-collection.mjs index 12f66b131296e..5aa2a9b891161 100644 --- a/components/shopify/actions/add-product-to-custom-collection/add-product-to-custom-collection.mjs +++ b/components/shopify/actions/add-product-to-custom-collection/add-product-to-custom-collection.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-add-product-to-custom-collection", name: "Add Products to Custom Collections", description: "Adds a product or products to a custom collection or collections. [See the documentation](https://shopify.dev/docs/api/admin-rest/2023-01/resources/collect#post-collects)", - version: "0.0.5", + version: "0.0.6", type: "action", props: { shopify, diff --git a/components/shopify/actions/add-tags/add-tags.mjs b/components/shopify/actions/add-tags/add-tags.mjs index d3ea4954931ac..3e454d9d31dea 100644 --- a/components/shopify/actions/add-tags/add-tags.mjs +++ b/components/shopify/actions/add-tags/add-tags.mjs @@ -4,7 +4,7 @@ import common from "./common.mjs"; export default { ...common, name: "Add Tags", - version: "0.0.11", + version: "0.0.12", key: "shopify-add-tags", description: "Add tags. [See the documentation](https://shopify.dev/api/admin-graphql/2022-07/mutations/tagsadd)", type: "action", diff --git a/components/shopify/actions/create-article/create-article.mjs b/components/shopify/actions/create-article/create-article.mjs index c1409de900f7b..20473ff5d2bfe 100644 --- a/components/shopify/actions/create-article/create-article.mjs +++ b/components/shopify/actions/create-article/create-article.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-create-article", name: "Create Article", description: "Create a new blog article. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/article#post-blogs-blog-id-articles)", - version: "0.0.4", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify/actions/create-blog/create-blog.mjs b/components/shopify/actions/create-blog/create-blog.mjs index c214465e4384f..d2801dee2caec 100644 --- a/components/shopify/actions/create-blog/create-blog.mjs +++ b/components/shopify/actions/create-blog/create-blog.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-create-blog", name: "Create Blog", description: "Create a new blog. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/blog#post-blogs)", - version: "0.0.4", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify/actions/create-custom-collection/create-custom-collection.mjs b/components/shopify/actions/create-custom-collection/create-custom-collection.mjs index fcafbcc3a2cce..e24b085fea73f 100644 --- a/components/shopify/actions/create-custom-collection/create-custom-collection.mjs +++ b/components/shopify/actions/create-custom-collection/create-custom-collection.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-create-custom-collection", name: "Create Custom Collection", description: "Create a new custom collection. [See the documentation](https://shopify.dev/docs/api/admin-rest/2023-01/resources/customcollection#post-custom-collections)", - version: "0.0.5", + version: "0.0.6", type: "action", props: { shopify, diff --git a/components/shopify/actions/create-metafield/create-metafield.mjs b/components/shopify/actions/create-metafield/create-metafield.mjs index 80ff0afe98caf..f4f4fd5014cf8 100644 --- a/components/shopify/actions/create-metafield/create-metafield.mjs +++ b/components/shopify/actions/create-metafield/create-metafield.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-create-metafield", name: "Create Metafield", description: "Creates a metafield belonging to a resource. [See the docs](https://shopify.dev/api/admin-rest/2023-01/resources/metafield#post-blogs-blog-id-metafields)", - version: "0.0.9", + version: "0.0.10", type: "action", props: { ...metafieldActions.props, diff --git a/components/shopify/actions/create-metaobject/create-metaobject.mjs b/components/shopify/actions/create-metaobject/create-metaobject.mjs index 204735f4274bb..951f2f510bda0 100644 --- a/components/shopify/actions/create-metaobject/create-metaobject.mjs +++ b/components/shopify/actions/create-metaobject/create-metaobject.mjs @@ -8,7 +8,7 @@ export default { key: "shopify-create-metaobject", name: "Create Metaobject", description: "Creates a metaobject. [See the documentation](https://shopify.dev/docs/api/admin-graphql/2023-04/mutations/metaobjectCreate)", - version: "0.0.4", + version: "0.0.5", type: "action", props: { shopify, diff --git a/components/shopify/actions/create-page/create-page.mjs b/components/shopify/actions/create-page/create-page.mjs index 8ec9e7dfa2707..33a2eb5ca38f9 100644 --- a/components/shopify/actions/create-page/create-page.mjs +++ b/components/shopify/actions/create-page/create-page.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-create-page", name: "Create Page", description: "Create a new page. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/page#post-pages)", - version: "0.0.4", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify/actions/create-product-variant/common.mjs b/components/shopify/actions/create-product-variant/common.mjs index a453636b0b190..5ad25688143f6 100644 --- a/components/shopify/actions/create-product-variant/common.mjs +++ b/components/shopify/actions/create-product-variant/common.mjs @@ -1,16 +1,69 @@ +import { ConfigurationError } from "@pipedream/platform"; + export default { + props: { + available: { + type: "string", + label: "Available Quantity", + description: "Sets the available inventory quantity", + optional: true, + }, + barcode: { + type: "string", + label: "Barcode", + description: "The barcode, UPC, or ISBN number for the product", + optional: true, + }, + weight: { + type: "string", + label: "Weight", + description: "The weight of the product variant in the unit system specified with Weight Unit", + optional: true, + }, + weightUnit: { + type: "string", + label: "Weight Unit", + description: "The unit of measurement that applies to the product variant's weight. If you don't specify a value for weight_unit, then the shop's default unit of measurement is applied.", + optional: true, + options: [ + "g", + "kg", + "oz", + "lb", + ], + }, + }, async run({ $ }) { + if (this.available && !this.locationId) { + throw new ConfigurationError("Must enter LocationId to set the available quantity"); + } + const productVariant = { option1: this.option, price: this.price, image_id: this.imageId, sku: this.sku, + barcode: this.barcode, + weight: this.weight, + weight_unit: this.weightUnit, }; - const response = (await this.shopify.createProductVariant( + let { result } = await this.shopify.createProductVariant( this.productId, productVariant, - )).result; - $.export("$summary", `Created new product variant \`${response.title}\` with id \`${response.id}\``); - return response; + ); + + if (this.available) { + const { result: inventoryLevel } = await this.shopify.updateInventoryLevel({ + inventory_item_id: result.inventory_item_id, + location_id: this.locationId, + available: this.available, + }); + const { result: updatedProductVariant } = await this.shopify.getProductVariant(result.id); + result = updatedProductVariant; + result.inventoryLevel = inventoryLevel; + } + + $.export("$summary", `Created new product variant \`${result.title}\` with id \`${result.id}\``); + return result; }, }; diff --git a/components/shopify/actions/create-product-variant/create-product-variant.mjs b/components/shopify/actions/create-product-variant/create-product-variant.mjs index 8580b18d3776c..bc83cd1b75922 100644 --- a/components/shopify/actions/create-product-variant/create-product-variant.mjs +++ b/components/shopify/actions/create-product-variant/create-product-variant.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-create-product-variant", name: "Create Product Variant", description: "Create a new product variant. [See the documentation](https://shopify.dev/api/admin-rest/2022-01/resources/product-variant#[post]/admin/api/2022-01/products/{product_id}/variants.json)", - version: "0.0.11", + version: "0.0.13", type: "action", props: { shopify, @@ -44,6 +44,13 @@ export default { "sku", ], }, + locationId: { + propDefinition: [ + shopify, + "locationId", + ], + optional: true, + }, ...common.props, }, }; diff --git a/components/shopify/actions/create-product/create-product.mjs b/components/shopify/actions/create-product/create-product.mjs index 92c77f70143bf..52ee14b761584 100644 --- a/components/shopify/actions/create-product/create-product.mjs +++ b/components/shopify/actions/create-product/create-product.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-create-product", name: "Create Product", description: "Create a new product. [See the documentation](https://shopify.dev/api/admin-rest/2022-01/resources/product#[post]/admin/api/2022-01/products.json)", - version: "0.0.11", + version: "0.0.12", type: "action", props: { shopify, diff --git a/components/shopify/actions/create-smart-collection/create-smart-collection.mjs b/components/shopify/actions/create-smart-collection/create-smart-collection.mjs index a4f91e78e85cd..426fe039520d4 100644 --- a/components/shopify/actions/create-smart-collection/create-smart-collection.mjs +++ b/components/shopify/actions/create-smart-collection/create-smart-collection.mjs @@ -11,7 +11,7 @@ export default { You can fill in any number of rules by selecting more than one option in each prop. [See documentation](https://shopify.dev/api/admin-rest/2021-10/resources/smartcollection#post-smart-collections) `), - version: "0.0.11", + version: "0.0.12", type: "action", props: { shopify, diff --git a/components/shopify/actions/delete-article/delete-article.mjs b/components/shopify/actions/delete-article/delete-article.mjs index d6bdcb1215a89..23ef4f6171d31 100644 --- a/components/shopify/actions/delete-article/delete-article.mjs +++ b/components/shopify/actions/delete-article/delete-article.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-delete-article", name: "Delete Article", description: "Delete an existing blog article. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/article#delete-blogs-blog-id-articles-article-id)", - version: "0.0.4", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify/actions/delete-blog/delete-blog.mjs b/components/shopify/actions/delete-blog/delete-blog.mjs index be8cc077c8d79..0795f818d8c7d 100644 --- a/components/shopify/actions/delete-blog/delete-blog.mjs +++ b/components/shopify/actions/delete-blog/delete-blog.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-delete-blog", name: "Delete Blog", description: "Delete an existing blog. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/blog#delete-blogs-blog-id)", - version: "0.0.4", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify/actions/delete-metafield/delete-metafield.mjs b/components/shopify/actions/delete-metafield/delete-metafield.mjs index 30415096da06f..5deb7069659b3 100644 --- a/components/shopify/actions/delete-metafield/delete-metafield.mjs +++ b/components/shopify/actions/delete-metafield/delete-metafield.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-delete-metafield", name: "Delete Metafield", description: "Deletes a metafield belonging to a resource. [See the documentation](https://shopify.dev/docs/api/admin-rest/2023-01/resources/metafield#delete-blogs-blog-id-metafields-metafield-id)", - version: "0.0.7", + version: "0.0.8", type: "action", props: { ...metafieldActions.props, diff --git a/components/shopify/actions/delete-page/delete-page.mjs b/components/shopify/actions/delete-page/delete-page.mjs index 957370c5221ae..29931ac7c6859 100644 --- a/components/shopify/actions/delete-page/delete-page.mjs +++ b/components/shopify/actions/delete-page/delete-page.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-delete-page", name: "Delete Page", description: "Delete an existing page. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/page#delete-pages-page-id)", - version: "0.0.4", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify/actions/get-articles/get-articles.mjs b/components/shopify/actions/get-articles/get-articles.mjs index 0db589aec4f98..8672f34edf977 100644 --- a/components/shopify/actions/get-articles/get-articles.mjs +++ b/components/shopify/actions/get-articles/get-articles.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-get-articles", name: "Get Articles", description: "Retrieve a list of all articles from a blog. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/article#get-blogs-blog-id-articles)", - version: "0.0.4", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify/actions/get-metafields/get-metafields.mjs b/components/shopify/actions/get-metafields/get-metafields.mjs index 3f708aface0d4..b0953bd5fec66 100644 --- a/components/shopify/actions/get-metafields/get-metafields.mjs +++ b/components/shopify/actions/get-metafields/get-metafields.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-get-metafields", name: "Get Metafields", description: "Retrieves a list of metafields that belong to a resource. [See the docs](https://shopify.dev/api/admin-rest/2023-01/resources/metafield#get-metafields?metafield[owner-id]=382285388&metafield[owner-resource]=blog)", - version: "0.0.10", + version: "0.0.11", type: "action", props: { ...metafieldActions.props, diff --git a/components/shopify/actions/get-metaobjects/get-metaobjects.mjs b/components/shopify/actions/get-metaobjects/get-metaobjects.mjs index 67504cc29cfea..3887872bbf5c8 100644 --- a/components/shopify/actions/get-metaobjects/get-metaobjects.mjs +++ b/components/shopify/actions/get-metaobjects/get-metaobjects.mjs @@ -8,7 +8,7 @@ export default { key: "shopify-get-metaobjects", name: "Get Metaobjects", description: "Retrieves a list of metaobjects. [See the documentation](https://shopify.dev/docs/api/admin-graphql/2023-04/queries/metaobjects)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { shopify, diff --git a/components/shopify/actions/get-pages/get-pages.mjs b/components/shopify/actions/get-pages/get-pages.mjs index 7ff556e6cc464..d749198a2458a 100644 --- a/components/shopify/actions/get-pages/get-pages.mjs +++ b/components/shopify/actions/get-pages/get-pages.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-get-pages", name: "Get Pages", description: "Retrieve a list of all pages. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/page#get-pages)", - version: "0.0.4", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify/actions/search-custom-collection-by-name/search-custom-collection-by-name.mjs b/components/shopify/actions/search-custom-collection-by-name/search-custom-collection-by-name.mjs index 00d9d90143f45..9c1d318506c0f 100644 --- a/components/shopify/actions/search-custom-collection-by-name/search-custom-collection-by-name.mjs +++ b/components/shopify/actions/search-custom-collection-by-name/search-custom-collection-by-name.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-search-custom-collection-by-name", name: "Search Custom Collection by Name", description: "Search for a custom collection by name/title. [See the documentation](https://shopify.dev/docs/api/admin-rest/2023-01/resources/customcollection#get-custom-collections)", - version: "0.0.4", + version: "0.0.5", type: "action", props: { shopify, diff --git a/components/shopify/actions/search-product-variant/search-product-variant.mjs b/components/shopify/actions/search-product-variant/search-product-variant.mjs index c4f87a7c0d4aa..7ef213e513c50 100644 --- a/components/shopify/actions/search-product-variant/search-product-variant.mjs +++ b/components/shopify/actions/search-product-variant/search-product-variant.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-search-product-variant", name: "Search for Product Variant", description: "Search for product variants or create one if not found. [See the documentation](https://shopify.dev/api/admin-rest/2022-01/resources/product-variant#top)", - version: "0.0.11", + version: "0.0.12", type: "action", props: { shopify, diff --git a/components/shopify/actions/search-products/search-products.mjs b/components/shopify/actions/search-products/search-products.mjs index 559cbd298ac68..c36e4f67a21ab 100644 --- a/components/shopify/actions/search-products/search-products.mjs +++ b/components/shopify/actions/search-products/search-products.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-search-products", name: "Search for Products", description: "Search for products. [See the documentation](https://shopify.dev/api/admin-rest/2022-01/resources/product#[get]/admin/api/2022-01/products.json)", - version: "0.0.11", + version: "0.0.12", type: "action", props: { shopify, diff --git a/components/shopify/actions/update-article/update-article.mjs b/components/shopify/actions/update-article/update-article.mjs index 62b4363a43277..efb3cea6889d1 100644 --- a/components/shopify/actions/update-article/update-article.mjs +++ b/components/shopify/actions/update-article/update-article.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-update-article", name: "Update Article", description: "Update a blog article. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/article#put-blogs-blog-id-articles-article-id)", - version: "0.0.4", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify/actions/update-inventory-level/update-inventory-level.mjs b/components/shopify/actions/update-inventory-level/update-inventory-level.mjs index 20c80535da54b..b38d53f28493d 100644 --- a/components/shopify/actions/update-inventory-level/update-inventory-level.mjs +++ b/components/shopify/actions/update-inventory-level/update-inventory-level.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-update-inventory-level", name: "Update Inventory Level", description: "Sets the inventory level for an inventory item at a location. [See the documenation](https://shopify.dev/api/admin-rest/2022-01/resources/inventorylevel#[post]/admin/api/2022-01/inventory_levels/set.json)", - version: "0.0.11", + version: "0.0.12", type: "action", props: { shopify, diff --git a/components/shopify/actions/update-metafield/update-metafield.mjs b/components/shopify/actions/update-metafield/update-metafield.mjs index 92856b7a5956d..f7c248b0d0d4f 100644 --- a/components/shopify/actions/update-metafield/update-metafield.mjs +++ b/components/shopify/actions/update-metafield/update-metafield.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-update-metafield", name: "Update Metafield", description: "Updates a metafield belonging to a resource. [See the docs](https://shopify.dev/api/admin-rest/2023-01/resources/metafield#put-blogs-blog-id-metafields-metafield-id)", - version: "0.0.9", + version: "0.0.10", type: "action", props: { ...metafieldActions.props, diff --git a/components/shopify/actions/update-metaobject/update-metaobject.mjs b/components/shopify/actions/update-metaobject/update-metaobject.mjs index e391b0ef09b70..07e8b70042776 100644 --- a/components/shopify/actions/update-metaobject/update-metaobject.mjs +++ b/components/shopify/actions/update-metaobject/update-metaobject.mjs @@ -8,7 +8,7 @@ export default { key: "shopify-update-metaobject", name: "Update Metaobject", description: "Updates a metaobject. [See the documentation](https://shopify.dev/docs/api/admin-graphql/2023-04/mutations/metaobjectUpdate)", - version: "0.0.5", + version: "0.0.6", type: "action", methods: { ...metaobjects.methods, diff --git a/components/shopify/actions/update-page/update-page.mjs b/components/shopify/actions/update-page/update-page.mjs index 9a7c88e84ab55..ada9cffa505eb 100644 --- a/components/shopify/actions/update-page/update-page.mjs +++ b/components/shopify/actions/update-page/update-page.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-update-page", name: "Update Page", description: "Update an existing page. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/page#put-pages-page-id)", - version: "0.0.4", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify/actions/update-product-variant/common.mjs b/components/shopify/actions/update-product-variant/common.mjs index 172bdb8eed309..f390352aa72c3 100644 --- a/components/shopify/actions/update-product-variant/common.mjs +++ b/components/shopify/actions/update-product-variant/common.mjs @@ -2,6 +2,36 @@ import { ConfigurationError } from "@pipedream/platform"; export default { props: { + available: { + type: "string", + label: "Available Quantity", + description: "Sets the available inventory quantity", + optional: true, + }, + barcode: { + type: "string", + label: "Barcode", + description: "The barcode, UPC, or ISBN number for the product", + optional: true, + }, + weight: { + type: "string", + label: "Weight", + description: "The weight of the product variant in the unit system specified with Weight Unit", + optional: true, + }, + weightUnit: { + type: "string", + label: "Weight Unit", + description: "The unit of measurement that applies to the product variant's weight. If you don't specify a value for weight_unit, then the shop's default unit of measurement is applied.", + optional: true, + options: [ + "g", + "kg", + "oz", + "lb", + ], + }, harmonizedSystemCode: { type: "integer", label: "Harmonized System Code", @@ -19,10 +49,27 @@ export default { sku, countryCodeOfOrigin, harmonizedSystemCode, + locationId, + available, + barcode, + weight, + weightUnit, } = this; - if (!option && !price && !imageId && !metafieldsArray && !sku) { - throw new ConfigurationError("Must enter one of `title`, `price`, `imageId`, `metafields`, or `sku`."); + if (available && !locationId) { + throw new ConfigurationError("Must enter LocationId to set the available quantity"); + } + + if (!option + && !price + && !imageId + && !metafieldsArray + && !sku + && !barcode + && !weight + && !weightUnit + ) { + throw new ConfigurationError("Must enter one of `title`, `price`, `imageId`, `metafields`, `sku`, `barcode`, `weight`, or `weightUnit`."); } const metafields = await this.createMetafieldsArray(metafieldsArray, productVariantId, "variants"); @@ -34,6 +81,9 @@ export default { image_id: imageId, metafields, sku, + barcode, + weight, + weight_unit: weightUnit, }); response.productVariant = productVariant; @@ -47,6 +97,17 @@ export default { response.inventoryItem = inventoryItem; } + if (available) { + const { result: inventoryLevel } = await this.shopify.updateInventoryLevel({ + inventory_item_id: productVariant.inventory_item_id, + location_id: locationId, + available, + }); + response.inventoryLevel = inventoryLevel; + const { result: updatedVariant } = await this.shopify.getProductVariant(productVariantId); + response.productVariant = updatedVariant; + } + $.export("$summary", `Updated product variant \`${productVariant.title}\` with id \`${productVariant.id}\``); return response; }, diff --git a/components/shopify/actions/update-product-variant/update-product-variant.mjs b/components/shopify/actions/update-product-variant/update-product-variant.mjs index 2d2ca1039a9c4..ff1a6f7abbc17 100644 --- a/components/shopify/actions/update-product-variant/update-product-variant.mjs +++ b/components/shopify/actions/update-product-variant/update-product-variant.mjs @@ -8,7 +8,7 @@ export default { key: "shopify-update-product-variant", name: "Update Product Variant", description: "Update an existing product variant. [See the docs](https://shopify.dev/api/admin-rest/2022-01/resources/product-variant#[put]/admin/api/2022-01/variants/{variant_id}.json)", - version: "0.0.13", + version: "0.0.15", type: "action", props: { shopify, @@ -67,10 +67,11 @@ export default { ], description: "The country code [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) of where the item came from", }, - harmonizedSystemCode: { - type: "integer", - label: "Harmonized System Code", - description: "The general [harmonized system](https://en.wikipedia.org/wiki/Harmonized_System) code for the inventory item", + locationId: { + propDefinition: [ + shopify, + "locationId", + ], optional: true, }, ...common.props, diff --git a/components/shopify/actions/update-product/update-product.mjs b/components/shopify/actions/update-product/update-product.mjs index 695993420caaa..542682cf1fc39 100644 --- a/components/shopify/actions/update-product/update-product.mjs +++ b/components/shopify/actions/update-product/update-product.mjs @@ -8,7 +8,7 @@ export default { key: "shopify-update-product", name: "Update Product", description: "Update an existing product. [See the docs](https://shopify.dev/api/admin-rest/2022-01/resources/product#[put]/admin/api/2022-01/products/{product_id}.json)", - version: "0.1.3", + version: "0.1.4", type: "action", props: { shopify, diff --git a/components/shopify/common-app.mjs b/components/shopify/common-app.mjs index 83f415a1e60fd..db8008285cb23 100644 --- a/components/shopify/common-app.mjs +++ b/components/shopify/common-app.mjs @@ -1,3 +1,4 @@ +import fs from "fs"; import get from "lodash.get"; import Shopify from "shopify-api-node"; import toPath from "lodash.topath"; @@ -199,12 +200,7 @@ export default { images: { type: "string[]", label: "Images", - description: toSingleLineString(` - A list of product base64 encoded image objects. - Each one represents an image associated with the product or a link that will be downloaded by Shopify. - Example: \`["R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==","http://example.com/rails_logo.gif"]\`. - More information at [Shopify Product API](https://shopify.dev/api/admin-rest/2022-01/resources/product#[post]/admin/api/2022-01/products.json) - `), + description: "A list of [product image](https://shopify.dev/docs/admin-api/rest/reference/products/product-image) objects, each one representing an image associated with the product. For each row you can either specify a Pipedream path like `/tmp/my-image.png` or a URL like `https://example.com/image.png`. More information at [Shopify Product API](https://shopify.dev/api/admin-rest/2022-01/resources/product#[post]/admin/api/2022-01/products.json)", optional: true, }, options: { @@ -514,13 +510,29 @@ export default { } throw new TypeError("variable should be an array or string"); }, + getImageBase64Encoded(path) { + if (!fs.existsSync(path)) { + throw new Error(`File path does not exist: ${path}`); + } + const image = fs.readFileSync(path); + return image.toString("base64"); + }, parseImages(images) { - if (!images) return []; - return images.map((image) => ({ - [image.includes("http") - ? "src" - : "attachment"]: image, - })); + if (!images) { + return []; + } + + return images.map((image) => { + const key = + image.startsWith("http") + ? "src" + : "attachment"; + return { + [key]: key === "attachment" + ? this.getImageBase64Encoded(image) + : image, + }; + }); }, dayAgo() { const dayAgo = new Date(); diff --git a/components/shopify/common/constants.mjs b/components/shopify/common/constants.mjs index 1e87fed1e4a00..e0c4a1639029f 100644 --- a/components/shopify/common/constants.mjs +++ b/components/shopify/common/constants.mjs @@ -1,6 +1,6 @@ const STORE_PLACEHOLDER = "{store_name}"; const BASE_URL = `https://${STORE_PLACEHOLDER}.myshopify.com`; -const VERSION_PATH = "/admin/api/2023-04"; +const VERSION_PATH = "/admin/api/2024-04"; const DEFAULT_MAX = 300; diff --git a/components/shopify/package.json b/components/shopify/package.json index 5cac026de346b..13f1439452a21 100644 --- a/components/shopify/package.json +++ b/components/shopify/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/shopify", - "version": "0.6.2", + "version": "0.6.6", "description": "Pipedream Shopify Components", "main": "shopify.app.mjs", "keywords": [ diff --git a/components/shopify/sources/common/constants.mjs b/components/shopify/sources/common/constants.mjs index 3450fc5ebf15e..f9ccbeb9f6915 100644 --- a/components/shopify/sources/common/constants.mjs +++ b/components/shopify/sources/common/constants.mjs @@ -7,6 +7,7 @@ const HEADER = { }; const EVENT_TOPIC = { + APP_SCOPES_UPDATE: "app/scopes_update", APP_UNINSTALLED: "app/uninstalled", BULK_OPERATIONS_FINISH: "bulk_operations/finish", COLLECTION_LISTINGS_ADD: "collection_listings/add", @@ -15,9 +16,34 @@ const EVENT_TOPIC = { COLLECTIONS_CREATE: "collections/create", COLLECTIONS_DELETE: "collections/delete", COLLECTIONS_UPDATE: "collections/update", + DISCOUNTS_CREATE: "discounts/create", + DISCOUNTS_DELETE: "discounts/delete", + DISCOUNTS_UPDATE: "discounts/update", + DISCOUNTS_REDEEMCODE_ADDED: "discounts/redeemcode_added", + DISCOUNTS_REDEEMCODE_REMOVED: "discounts/redeemcode_removed", DOMAINS_CREATE: "domains/create", DOMAINS_DESTROY: "domains/destroy", DOMAINS_UPDATE: "domains/update", + FULFILLMENT_HOLDS_ADDED: "fulfillment_holds/added", + FULFILLMENT_HOLDS_RELEASED: "fulfillment_holds/released", + FULFILLMENT_ORDERS_CANCELLATION_REQUEST_ACCEPTED: "fulfillment_orders/cancellation_request_accepted", + FULFILLMENT_ORDERS_CANCELLATION_REQUEST_REJECTED: "fulfillment_orders/cancellation_request_rejected", + FULFILLMENT_ORDERS_CANCELLATION_REQUEST_SUBMITTED: "fulfillment_orders/cancellation_request_submitted", + FULFILLMENT_ORDERS_CANCELLED: "fulfillment_orders/cancelled", + FULFILLMENT_ORDERS_FULFILLMENT_REQUEST_ACCEPTED: "fulfillment_orders/fulfillment_request_accepted", + FULFILLMENT_ORDERS_FULFILLMENT_REQUEST_REJECTED: "fulfillment_orders/fulfillment_request_rejected", + FULFILLMENT_ORDERS_FULFILLMENT_REQUEST_SUBMITTED: "fulfillment_orders/fulfillment_request_submitted", + FULFILLMENT_ORDERS_FULFILLMENT_SERVICE_FAILED_TO_COMPLETE: "fulfillment_orders/fulfillment_service_failed_to_complete", + FULFILLMENT_ORDERS_HOLD_RELEASED: "fulfillment_orders/hold_released", + FULFILLMENT_ORDERS_LINE_ITEMS_PREPARED_FOR_LOCAL_DELIVERY: "fulfillment_orders/line_items_prepared_for_local_delivery", + FULFILLMENT_ORDERS_LINE_ITEMS_PREPARED_FOR_PICKUP: "fulfillment_orders/line_items_prepared_for_pickup", + FULFILLMENT_ORDERS_MERGED: "fulfillment_orders/merged", + FULFILLMENT_ORDERS_MOVED: "fulfillment_orders/moved", + FULFILLMENT_ORDERS_ORDER_ROUTING_COMPLETE: "fulfillment_orders/order_routing_complete", + FULFILLMENT_ORDERS_PLACED_ON_HOLD: "fulfillment_orders/placed_on_hold", + FULFILLMENT_ORDERS_RESCHEDULED: "fulfillment_orders/rescheduled", + FULFILLMENT_ORDERS_SCHEDULED_FULFILLMENT_ORDER_READY: "fulfillment_orders/scheduled_fulfillment_order_ready", + FULFILLMENT_ORDERS_SPLIT: "fulfillment_orders/split", INVENTORY_ITEMS_CREATE: "inventory_items/create", INVENTORY_ITEMS_DELETE: "inventory_items/delete", INVENTORY_ITEMS_UPDATE: "inventory_items/update", @@ -29,9 +55,12 @@ const EVENT_TOPIC = { LOCATIONS_CREATE: "locations/create", LOCATIONS_DELETE: "locations/delete", LOCATIONS_UPDATE: "locations/update", + LOCATIONS_ACTIVATE: "locations/activate", + LOCATIONS_DEACTIVATE: "locations/deactivate", MARKETS_CREATE: "markets/create", MARKETS_DELETE: "markets/delete", MARKETS_UPDATE: "markets/update", + PAYMENT_SCHEDULE_DUE: "payment_schedules/due", PRODUCT_LISTINGS_ADD: "product_listings/add", PRODUCT_LISTINGS_REMOVE: "product_listings/remove", PRODUCT_LISTINGS_UPDATE: "product_listings/update", @@ -39,20 +68,38 @@ const EVENT_TOPIC = { PRODUCTS_DELETE: "products/delete", PRODUCTS_UPDATE: "products/update", REFUNDS_CREATE: "refunds/create", + RETURNS_APPROVE: "returns/approve", + RETURNS_CANCEL: "returns/cancel", + RETURNS_CLOSE: "returns/close", + RETURNS_DECLINE: "returns/decline", + RETURNS_PROCESS: "returns/process", + RETURNS_REOPEN: "returns/reopen", + RETURNS_REQUEST: "returns/request", + RETURNS_UPDATE: "returns/update", SCHEDULED_PRODUCT_LISTINGS_ADD: "scheduled_product_listings/add", SCHEDULED_PRODUCT_LISTINGS_REMOVE: "scheduled_product_listings/remove", SCHEDULED_PRODUCT_LISTINGS_UPDATE: "scheduled_product_listings/update", + SEGMENTS_CREATE: "segments/create", + SEGMENTS_DELETE: "segments/delete", + SEGMENTS_UPDATE: "segments/update", SELLING_PLAN_GROUPS_CREATE: "selling_plan_groups/create", SELLING_PLAN_GROUPS_DELETE: "selling_plan_groups/delete", SELLING_PLAN_GROUPS_UPDATE: "selling_plan_groups/update", SHOP_UPDATE: "shop/update", + SUBSCRIPTION_CONTRACTS_ACTIVATE: "subscription_contracts/activate", + SUBSCRIPTION_CONTRACTS_CANCEL: "subscription_contracts/cancel", + SUBSCRIPTION_CONTRACTS_EXPIRE: "subscription_contracts/expire", + SUBSCRIPTION_CONTRACTS_FAIL: "subscription_contracts/fail", + SUBSCRIPTION_CONTRACTS_PAUSE: "subscription_contracts/pause", SUBSCRIPTION_CONTRACTS_CREATE: "subscription_contracts/create", SUBSCRIPTION_CONTRACTS_UPDATE: "subscription_contracts/update", THEMES_CREATE: "themes/create", THEMES_DELETE: "themes/delete", THEMES_PUBLISH: "themes/publish", - EVENT_TOPICS_THEMES_UPDATE: "event-topics-themes-update", THEMES_UPDATE: "themes/update", + VARIANTS_IN_STOCK: "variants/in_stock", + VARIANTS_OUT_OF_STOCK: "variants/out_of_stock", + EVENT_TOPICS_THEMES_UPDATE: "event-topics-themes-update", }; const EVENT_TOPICS = Object.values(EVENT_TOPIC); diff --git a/components/shopify/sources/customer-data-request/customer-data-request.mjs b/components/shopify/sources/customer-data-request/customer-data-request.mjs index 476fbf6d76353..971133831cd7e 100644 --- a/components/shopify/sources/customer-data-request/customer-data-request.mjs +++ b/components/shopify/sources/customer-data-request/customer-data-request.mjs @@ -2,7 +2,7 @@ import shopify from "../../shopify.app.mjs"; export default { name: "New Customer Data Request", - version: "0.0.12", + version: "0.0.13", key: "shopify-customer-data-request", description: "Emit new customer data requests for data via a GDPR request.", type: "source", diff --git a/components/shopify/sources/new-abandoned-cart/new-abandoned-cart.mjs b/components/shopify/sources/new-abandoned-cart/new-abandoned-cart.mjs index fbdda402e6f01..4ee05fa0bb42e 100644 --- a/components/shopify/sources/new-abandoned-cart/new-abandoned-cart.mjs +++ b/components/shopify/sources/new-abandoned-cart/new-abandoned-cart.mjs @@ -7,7 +7,7 @@ export default { name: "New Abandoned Cart", type: "source", description: "Emit new event each time a user abandons their cart.", - version: "0.0.18", + version: "0.0.19", dedupe: "unique", props: { shopify, diff --git a/components/shopify/sources/new-article/new-article.mjs b/components/shopify/sources/new-article/new-article.mjs index 9743a7a7666ec..474b4b2dc4efc 100644 --- a/components/shopify/sources/new-article/new-article.mjs +++ b/components/shopify/sources/new-article/new-article.mjs @@ -7,7 +7,7 @@ export default { name: "New Article", type: "source", description: "Emit new event for each new article in a blog.", - version: "0.0.17", + version: "0.0.18", dedupe: "unique", props: { shopify, diff --git a/components/shopify/sources/new-event-emitted/new-event-emitted.mjs b/components/shopify/sources/new-event-emitted/new-event-emitted.mjs index b8629d7089cbe..cdc56045377bf 100644 --- a/components/shopify/sources/new-event-emitted/new-event-emitted.mjs +++ b/components/shopify/sources/new-event-emitted/new-event-emitted.mjs @@ -7,7 +7,7 @@ export default { name: "New Event Emitted (Instant)", type: "source", description: "Emit new event for each new Shopify event.", - version: "0.0.10", + version: "0.0.12", dedupe: "unique", props: { ...common.props, diff --git a/components/shopify/sources/new-page/new-page.mjs b/components/shopify/sources/new-page/new-page.mjs index d5b9c40fee0c0..354b395f53719 100644 --- a/components/shopify/sources/new-page/new-page.mjs +++ b/components/shopify/sources/new-page/new-page.mjs @@ -7,7 +7,7 @@ export default { name: "New Page", type: "source", description: "Emit new event for each new page published.", - version: "0.0.17", + version: "0.0.18", dedupe: "unique", props: { shopify, diff --git a/components/shopify/sources/new-product-created/new-product-created.mjs b/components/shopify/sources/new-product-created/new-product-created.mjs index c291c3088ecf5..975429cdb0112 100644 --- a/components/shopify/sources/new-product-created/new-product-created.mjs +++ b/components/shopify/sources/new-product-created/new-product-created.mjs @@ -7,7 +7,7 @@ export default { name: "New Product Created (Instant)", type: "source", description: "Emit new event for each product added to a store.", - version: "0.0.10", + version: "0.0.12", dedupe: "unique", methods: { ...common.methods, diff --git a/components/shopify/sources/product-added-to-custom-collection/product-added-to-custom-collection.mjs b/components/shopify/sources/product-added-to-custom-collection/product-added-to-custom-collection.mjs index 9465cf4ae8b98..ab49324ecc55d 100644 --- a/components/shopify/sources/product-added-to-custom-collection/product-added-to-custom-collection.mjs +++ b/components/shopify/sources/product-added-to-custom-collection/product-added-to-custom-collection.mjs @@ -6,7 +6,7 @@ export default { key: "shopify-product-added-to-custom-collection", name: "New product added to custom collection", description: "Emit new event each time a product is added to a custom collection.", - version: "0.0.5", + version: "0.0.6", type: "source", dedupe: "unique", props: { diff --git a/components/shopify_developer_app/README.md b/components/shopify_developer_app/README.md index 9e4459bb1c160..4858e0f081ffc 100644 --- a/components/shopify_developer_app/README.md +++ b/components/shopify_developer_app/README.md @@ -1,11 +1,20 @@ # Overview -By creating a custom app on Shopify, you will be able to configure the exact scopes that you require to build the workflows that you need. + +The [Shopify Admin REST & GraphQL API](https://shopify.dev/docs/api/admin) unleashes a myriad of possibilities to automate and enhance online store operations. It provides programmatic access to Shopify functionalities, allowing users to manage products, customers, orders, and more. Leveraging the Shopify Admin API within Pipedream, developers can create custom workflows that automate repetitive tasks, sync data across platforms, and respond dynamically to events in Shopify. + +This integration can be used as a custom app on your store, or for automating actions on behalf of merchants through your Shopify app. + +Looking for integrating into the Shopify Partner API for your apps, themes or referrals? Check out our [Shopify Partner API integration](https://pipedream.com/apps/shopify-partner). # Getting Started +## As a merchant + +By creating a custom app on Shopify, you will be able to configure the exact scopes that you require to build the workflows that you need. + To get started, you will need to create a custom Shopify app in the Shopify Admin UI. The steps are outlined below: -## Create a Shopify app +### Create a Shopify app 1. Sign in to your Shopify admin account, and navigate to the store that you are managing. 2. Click **Develop apps**. 3. Click **Allow custom app development**. @@ -13,19 +22,61 @@ To get started, you will need to create a custom Shopify app in the Shopify Admi ![Create an App](https://res.cloudinary.com/dpenc2lit/image/upload/v1688060015/Screenshot_2023-06-29_at_10.11.43_AM_unkom4.png) -## Configure Admin API scopes -1. In the new app you have created, under the **Configuration** tab, click **Configure Admin API scopes** +### Configure Admin API scopes +1. In the new app you have created, under the **API Credentials** tab, click **Configure Admin API scopes** 2. Select the scopes that you require for your use case. You may modify your scopes at any time by returning to the app configuration page. -## Generate the Admin API access token +### Generate the Admin API access token 1. Under API credentials, click **Install app**. 2. Click **Reveal token once** and save it in a secure location (we recommend using a password manager such as 1Password) -- you will need it when setting up authentication on Pipedream, and it is only revealed once. If you happen to lose this, you will need to uninstall the app, and reinstall it on Shopify to generate a new access token. ![API Credentials](https://res.cloudinary.com/dpenc2lit/image/upload/v1688061470/Screenshot_2023-06-29_at_10.54.53_AM_jta5gc.png) -## Connect your Shopify app with Pipedream using your access token +### Connect your Shopify app with Pipedream using your access token At this point, you should have a Pipedream App connected to your Shopify store, and a long-lived access token. 1. When prompted in Pipedream after trying to connect the Shopify Developer App, copy and paste your **shop id** along with your **access token** saved from the previous step. 2. Click **Connect** and your custom Shopify app should be integrated into Pipedream! + +## As an app developer + +As a Shopify App Developer, you can use Pipedream to automate actions on behalf of merchants by leveraging the merchants offline access token. + +### Connect your database + +First, you'll need to connect your app's database to Pipedream. Pipedream connects to SQL and No-SQL databases. Here's a list of popular options in Pipedream: + +* [PostgreSQL](https://pipedream.com/apps/postgresql) +* [MySQL](https://pipedream.com/apps/mysql) +* [Supabase](https://pipedream.com/apps/supabase) +* [Firebase](https://pipedream.com/apps/firebase) +* [MongoDB](https://pipedream.com/apps/mongodb) + +Once your database is connected to Pipedream, you'll be able to query the database for a specific merchant's token. + +### Retrieve a merchants access token + +In an HTTP triggered workflow, you can use a body parameter to reference a shop by it's `myshopify.com` domain, or unique Shop ID. + +Pass the shop ID to your database query step to retrieve the corresponding record of the shop to retrieve the shop's access token. + +### Use the Shopify Developer App integration + +Then pass the shop's access token to a no-code Shopify Developer App action, or use Node.js/Python code to perform a raw HTTP request against the Shopify Admin GraphQL or REST API. + +For example, in a pre-built action like *Add Tags*, click _use external authentication_ to pass in your database stored access token: + +![Use external account to pass in your database managed Shopify store oauth access tokens to perform actions on behalf of merchants](https://res.cloudinary.com/pipedreamin/image/upload/v1714495695/marketplace/apps/shopify_developer_a/CleanShot_2024-04-30_at_12.47.21_cewyzb.png) + +This will switch the action to allow you to pass in the merchants access token from your database query step: + +![Using the store's access token as a prop input](https://res.cloudinary.com/pipedreamin/image/upload/v1714495801/marketplace/apps/shopify_developer_a/CleanShot_2024-04-30_at_12.49.43_qclqdi.png) + +# Example Use Cases + +- **Automated Order Fulfillment Workflow**: When a new order is received in Shopify, a Pipedream workflow can be triggered, automatically notifying a fulfillment service or updating an inventory management system. This ensures quick response times and keeps inventory levels accurate. + +- **Customer Segmentation and Personalized Marketing**: Pipedream can listen for customer creation events on Shopify. When a new customer is added, the workflow can segment them based on predefined criteria, such as order value or location, and add them to corresponding marketing campaigns in email marketing platforms like Mailchimp. + +- **Real-time Stock Level Alerts**: Set up a Pipedream workflow to monitor product stock levels on Shopify. When a product's stock falls below a certain threshold, trigger an alert that is sent out via Slack, SMS, or email to prompt immediate restocking actions, ensuring that popular products are always available to customers. diff --git a/components/shopify_developer_app/actions/add-product-to-custom-collection/add-product-to-custom-collection.mjs b/components/shopify_developer_app/actions/add-product-to-custom-collection/add-product-to-custom-collection.mjs index 88b7f320f5914..710f9cbbad39e 100644 --- a/components/shopify_developer_app/actions/add-product-to-custom-collection/add-product-to-custom-collection.mjs +++ b/components/shopify_developer_app/actions/add-product-to-custom-collection/add-product-to-custom-collection.mjs @@ -1,12 +1,12 @@ import shopify from "../../shopify_developer_app.app.mjs"; -import common from "../../../shopify/actions/add-product-to-custom-collection/common.mjs"; +import common from "@pipedream/shopify/actions/add-product-to-custom-collection/common.mjs"; export default { ...common, key: "shopify_developer_app-add-product-to-custom-collection", name: "Add Products to Custom Collections", description: "Adds a product or products to a custom collection or collections. [See the docs](https://shopify.dev/docs/api/admin-rest/2023-01/resources/collect#post-collects)", - version: "0.0.2", + version: "0.0.4", type: "action", props: { shopify, diff --git a/components/shopify_developer_app/actions/add-tags/add-tags.mjs b/components/shopify_developer_app/actions/add-tags/add-tags.mjs index 71bc509b12a1c..62be49b75da50 100644 --- a/components/shopify_developer_app/actions/add-tags/add-tags.mjs +++ b/components/shopify_developer_app/actions/add-tags/add-tags.mjs @@ -1,10 +1,10 @@ import shopify from "../../shopify_developer_app.app.mjs"; -import common from "../../../shopify/actions/add-tags/common.mjs"; +import common from "@pipedream/shopify/actions/add-tags/common.mjs"; export default { ...common, name: "Add Tags", - version: "0.0.2", + version: "0.0.4", key: "shopify_developer_app-add-tags", description: "Add tags. [See the documentation](https://shopify.dev/api/admin-graphql/2022-07/mutations/tagsadd)", type: "action", diff --git a/components/shopify_developer_app/actions/common/metafield-actions.mjs b/components/shopify_developer_app/actions/common/metafield-actions.mjs index 55f8f48ff60a0..97b660b61bbf4 100644 --- a/components/shopify_developer_app/actions/common/metafield-actions.mjs +++ b/components/shopify_developer_app/actions/common/metafield-actions.mjs @@ -1,5 +1,5 @@ import shopify from "../../shopify_developer_app.app.mjs"; -import metafieldActions from "../../../shopify/actions/common/metafield-actions.mjs"; +import metafieldActions from "@pipedream/shopify/actions/common/metafield-actions.mjs"; export default { ...metafieldActions, diff --git a/components/shopify_developer_app/actions/common/metaobjects.mjs b/components/shopify_developer_app/actions/common/metaobjects.mjs index 3f5985267c25f..bcef0bffcfe3a 100644 --- a/components/shopify_developer_app/actions/common/metaobjects.mjs +++ b/components/shopify_developer_app/actions/common/metaobjects.mjs @@ -1,4 +1,4 @@ -import metaobjects from "../../../shopify/actions/common/metaobjects.mjs"; +import metaobjects from "@pipedream/shopify/actions/common/metaobjects.mjs"; import { axios } from "@pipedream/platform"; export default { @@ -9,7 +9,7 @@ export default { $ = this, ...args }) { return axios($, { - url: `https://${this.shopify.getShopId()}.myshopify.com/admin/api/2023-04/graphql.json`, + url: `https://${this.shopify.getShopId()}.myshopify.com/admin/api/2024-04/graphql.json`, method: "POST", headers: { "X-Shopify-Access-Token": `${this.shopify.$auth.access_token}`, diff --git a/components/shopify_developer_app/actions/create-article/create-article.mjs b/components/shopify_developer_app/actions/create-article/create-article.mjs index 5b3589412db70..b9178d6055461 100644 --- a/components/shopify_developer_app/actions/create-article/create-article.mjs +++ b/components/shopify_developer_app/actions/create-article/create-article.mjs @@ -1,12 +1,12 @@ import app from "../../common/rest-admin.mjs"; -import common from "../../../shopify/actions/create-article/common.mjs"; +import common from "@pipedream/shopify/actions/create-article/common.mjs"; export default { ...common, key: "shopify_developer_app-create-article", name: "Create Article", description: "Create a new blog article. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/article#post-blogs-blog-id-articles)", - version: "0.0.2", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify_developer_app/actions/create-blog/create-blog.mjs b/components/shopify_developer_app/actions/create-blog/create-blog.mjs index ec601fbe26d00..b912c5b8aad75 100644 --- a/components/shopify_developer_app/actions/create-blog/create-blog.mjs +++ b/components/shopify_developer_app/actions/create-blog/create-blog.mjs @@ -1,12 +1,12 @@ import app from "../../common/rest-admin.mjs"; -import common from "../../../shopify/actions/create-blog/common.mjs"; +import common from "@pipedream/shopify/actions/create-blog/common.mjs"; export default { ...common, key: "shopify_developer_app-create-blog", name: "Create Blog", description: "Create a new blog. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/blog#post-blogs)", - version: "0.0.2", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify_developer_app/actions/create-custom-collection/create-custom-collection.mjs b/components/shopify_developer_app/actions/create-custom-collection/create-custom-collection.mjs index 05a820dd1be07..3c4c53bc3cbf4 100644 --- a/components/shopify_developer_app/actions/create-custom-collection/create-custom-collection.mjs +++ b/components/shopify_developer_app/actions/create-custom-collection/create-custom-collection.mjs @@ -1,12 +1,12 @@ import shopify from "../../shopify_developer_app.app.mjs"; -import common from "../../../shopify/actions/create-custom-collection/common.mjs"; +import common from "@pipedream/shopify/actions/create-custom-collection/common.mjs"; export default { ...common, key: "shopify_developer_app-create-custom-collection", name: "Create Custom Collection", description: "Create a new custom collection. [See the documentation](https://shopify.dev/docs/api/admin-rest/2023-01/resources/customcollection#post-custom-collections)", - version: "0.0.2", + version: "0.0.4", type: "action", props: { shopify, diff --git a/components/shopify_developer_app/actions/create-customer/create-customer.mjs b/components/shopify_developer_app/actions/create-customer/create-customer.mjs index 5e72e4a3a95be..021f966114327 100644 --- a/components/shopify_developer_app/actions/create-customer/create-customer.mjs +++ b/components/shopify_developer_app/actions/create-customer/create-customer.mjs @@ -1,12 +1,12 @@ import shopify from "../../shopify_developer_app.app.mjs"; -import common from "../../../shopify/actions/create-customer/common.mjs"; +import common from "@pipedream/shopify/actions/create-customer/common.mjs"; export default { ...common, key: "shopify_developer_app-create-customer", name: "Create Customer", description: "Create a new customer. [See the documentation](https://shopify.dev/api/admin-rest/2022-01/resources/customer#[post]/admin/api/2022-01/customers.json)", - version: "0.0.2", + version: "0.0.4", type: "action", props: { shopify, diff --git a/components/shopify_developer_app/actions/create-metafield/create-metafield.mjs b/components/shopify_developer_app/actions/create-metafield/create-metafield.mjs index d65b0d804856e..70a494aaf55d9 100644 --- a/components/shopify_developer_app/actions/create-metafield/create-metafield.mjs +++ b/components/shopify_developer_app/actions/create-metafield/create-metafield.mjs @@ -1,12 +1,12 @@ import metafieldActions from "../common/metafield-actions.mjs"; -import common from "../../../shopify/actions/create-metafield/common.mjs"; +import common from "@pipedream/shopify/actions/create-metafield/common.mjs"; export default { ...common, key: "shopify_developer_app-create-metafield", name: "Create Metafield", description: "Creates a metafield belonging to a resource. [See the docs](https://shopify.dev/api/admin-rest/2023-01/resources/metafield#post-blogs-blog-id-metafields)", - version: "0.0.2", + version: "0.0.5", type: "action", props: { ...metafieldActions.props, diff --git a/components/shopify_developer_app/actions/create-metaobject/create-metaobject.mjs b/components/shopify_developer_app/actions/create-metaobject/create-metaobject.mjs index 4e4f68c17fa1e..87bcae6e58aee 100644 --- a/components/shopify_developer_app/actions/create-metaobject/create-metaobject.mjs +++ b/components/shopify_developer_app/actions/create-metaobject/create-metaobject.mjs @@ -1,6 +1,6 @@ import shopify from "../../shopify_developer_app.app.mjs"; import metaobjects from "../common/metaobjects.mjs"; -import common from "../../../shopify/actions/create-metaobject/common.mjs"; +import common from "@pipedream/shopify/actions/create-metaobject/common.mjs"; export default { ...metaobjects, @@ -8,7 +8,7 @@ export default { key: "shopify_developer_app-create-metaobject", name: "Create Metaobject", description: "Creates a metaobject. [See the documentation](https://shopify.dev/docs/api/admin-graphql/2023-04/mutations/metaobjectCreate)", - version: "0.0.3", + version: "0.0.6", type: "action", props: { shopify, diff --git a/components/shopify_developer_app/actions/create-order/create-order.mjs b/components/shopify_developer_app/actions/create-order/create-order.mjs index 4102f7e3adf72..f93f5d0b74a4d 100644 --- a/components/shopify_developer_app/actions/create-order/create-order.mjs +++ b/components/shopify_developer_app/actions/create-order/create-order.mjs @@ -1,6 +1,6 @@ import shopify from "../../shopify_developer_app.app.mjs"; -import common from "../../../shopify/actions/create-order/common.mjs"; -import { toSingleLineString } from "../../../shopify/actions/common/common.mjs"; +import common from "@pipedream/shopify/actions/create-order/common.mjs"; +import { toSingleLineString } from "@pipedream/shopify/actions/common/common.mjs"; export default { ...common, @@ -10,7 +10,7 @@ export default { For full order object details [see the docs](https://shopify.dev/api/admin-rest/2022-01/resources/order#[post]/admin/api/2022-01/orders.json) or [see examples](https://shopify.dev/api/admin-rest/2022-01/resources/order#[post]/admin/api/#{api_version}/orders.json_examples) `), - version: "0.0.2", + version: "0.0.4", type: "action", props: { shopify, diff --git a/components/shopify_developer_app/actions/create-page/create-page.mjs b/components/shopify_developer_app/actions/create-page/create-page.mjs index 7e017a3f81793..7dfdea7d7c47c 100644 --- a/components/shopify_developer_app/actions/create-page/create-page.mjs +++ b/components/shopify_developer_app/actions/create-page/create-page.mjs @@ -1,12 +1,12 @@ import app from "../../common/rest-admin.mjs"; -import common from "../../../shopify/actions/create-page/common.mjs"; +import common from "@pipedream/shopify/actions/create-page/common.mjs"; export default { ...common, key: "shopify_developer_app-create-page", name: "Create Page", description: "Create a new page. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/page#post-pages)", - version: "0.0.2", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify_developer_app/actions/create-product-variant/create-product-variant.mjs b/components/shopify_developer_app/actions/create-product-variant/create-product-variant.mjs index 31fa085fb8e8b..130724c01f303 100644 --- a/components/shopify_developer_app/actions/create-product-variant/create-product-variant.mjs +++ b/components/shopify_developer_app/actions/create-product-variant/create-product-variant.mjs @@ -1,12 +1,12 @@ import shopify from "../../shopify_developer_app.app.mjs"; -import common from "../../../shopify/actions/create-product-variant/common.mjs"; +import common from "@pipedream/shopify/actions/create-product-variant/common.mjs"; export default { ...common, key: "shopify_developer_app-create-product-variant", name: "Create Product Variant", description: "Create a new product variant. [See the documentation](https://shopify.dev/api/admin-rest/2022-01/resources/product-variant#[post]/admin/api/2022-01/products/{product_id}/variants.json)", - version: "0.0.2", + version: "0.0.5", type: "action", props: { shopify, @@ -44,6 +44,13 @@ export default { "sku", ], }, + locationId: { + propDefinition: [ + shopify, + "locationId", + ], + optional: true, + }, ...common.props, }, }; diff --git a/components/shopify_developer_app/actions/create-product/create-product.mjs b/components/shopify_developer_app/actions/create-product/create-product.mjs index 09b8941f13404..5a1bfb8c0d78f 100644 --- a/components/shopify_developer_app/actions/create-product/create-product.mjs +++ b/components/shopify_developer_app/actions/create-product/create-product.mjs @@ -1,12 +1,12 @@ import shopify from "../../shopify_developer_app.app.mjs"; -import common from "../../../shopify/actions/create-product/common.mjs"; +import common from "@pipedream/shopify/actions/create-product/common.mjs"; export default { ...common, key: "shopify_developer_app-create-product", name: "Create Product", description: "Create a new product. [See the documentation](https://shopify.dev/api/admin-rest/2022-01/resources/product#[post]/admin/api/2022-01/products.json)", - version: "0.0.2", + version: "0.0.4", type: "action", props: { shopify, diff --git a/components/shopify_developer_app/actions/create-smart-collection/create-smart-collection.mjs b/components/shopify_developer_app/actions/create-smart-collection/create-smart-collection.mjs index 4ff2408a5f63a..a2a10865eba3e 100644 --- a/components/shopify_developer_app/actions/create-smart-collection/create-smart-collection.mjs +++ b/components/shopify_developer_app/actions/create-smart-collection/create-smart-collection.mjs @@ -1,6 +1,6 @@ import shopify from "../../shopify_developer_app.app.mjs"; -import { toSingleLineString } from "../../../shopify/actions/common/common.mjs"; -import common from "../../../shopify/actions/create-smart-collection/common.mjs"; +import { toSingleLineString } from "@pipedream/shopify/actions/common/common.mjs"; +import common from "@pipedream/shopify/actions/create-smart-collection/common.mjs"; export default { ...common, @@ -11,7 +11,7 @@ export default { You can fill in any number of rules by selecting more than one option in each prop. [See documentation](https://shopify.dev/api/admin-rest/2021-10/resources/smartcollection#post-smart-collections) `), - version: "0.0.2", + version: "0.0.4", type: "action", props: { shopify, diff --git a/components/shopify_developer_app/actions/delete-article/delete-article.mjs b/components/shopify_developer_app/actions/delete-article/delete-article.mjs index 8792e16b0e689..bba5dd9c0b961 100644 --- a/components/shopify_developer_app/actions/delete-article/delete-article.mjs +++ b/components/shopify_developer_app/actions/delete-article/delete-article.mjs @@ -1,12 +1,12 @@ import app from "../../common/rest-admin.mjs"; -import common from "../../../shopify/actions/delete-article/common.mjs"; +import common from "@pipedream/shopify/actions/delete-article/common.mjs"; export default { ...common, key: "shopify_developer_app-delete-article", name: "Delete Article", description: "Delete an existing blog article. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/article#delete-blogs-blog-id-articles-article-id)", - version: "0.0.2", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify_developer_app/actions/delete-blog/delete-blog.mjs b/components/shopify_developer_app/actions/delete-blog/delete-blog.mjs index df20f418f0137..d65fe34cab0a1 100644 --- a/components/shopify_developer_app/actions/delete-blog/delete-blog.mjs +++ b/components/shopify_developer_app/actions/delete-blog/delete-blog.mjs @@ -1,12 +1,12 @@ import app from "../../common/rest-admin.mjs"; -import common from "../../../shopify/actions/delete-blog/common.mjs"; +import common from "@pipedream/shopify/actions/delete-blog/common.mjs"; export default { ...common, key: "shopify_developer_app-delete-blog", name: "Delete Blog", description: "Delete an existing blog. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/blog#delete-blogs-blog-id)", - version: "0.0.2", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify_developer_app/actions/delete-metafield/delete-metafield.mjs b/components/shopify_developer_app/actions/delete-metafield/delete-metafield.mjs index 83771d0e8402b..3ff2a8ac4e940 100644 --- a/components/shopify_developer_app/actions/delete-metafield/delete-metafield.mjs +++ b/components/shopify_developer_app/actions/delete-metafield/delete-metafield.mjs @@ -1,12 +1,12 @@ import metafieldActions from "../common/metafield-actions.mjs"; -import common from "../../../shopify/actions/delete-metafield/common.mjs"; +import common from "@pipedream/shopify/actions/delete-metafield/common.mjs"; export default { ...common, key: "shopify_developer_app-delete-metafield", name: "Delete Metafield", description: "Deletes a metafield belonging to a resource. [See the documentation](https://shopify.dev/docs/api/admin-rest/2023-01/resources/metafield#delete-blogs-blog-id-metafields-metafield-id)", - version: "0.0.2", + version: "0.0.5", type: "action", props: { ...metafieldActions.props, diff --git a/components/shopify_developer_app/actions/delete-page/delete-page.mjs b/components/shopify_developer_app/actions/delete-page/delete-page.mjs index 90d71897782e1..ee75823a58f66 100644 --- a/components/shopify_developer_app/actions/delete-page/delete-page.mjs +++ b/components/shopify_developer_app/actions/delete-page/delete-page.mjs @@ -1,12 +1,12 @@ import app from "../../common/rest-admin.mjs"; -import common from "../../../shopify/actions/delete-page/common.mjs"; +import common from "@pipedream/shopify/actions/delete-page/common.mjs"; export default { ...common, key: "shopify_developer_app-delete-page", name: "Delete Page", description: "Delete an existing page. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/page#delete-pages-page-id)", - version: "0.0.2", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify_developer_app/actions/get-articles/get-articles.mjs b/components/shopify_developer_app/actions/get-articles/get-articles.mjs index 5aeeec818426c..68e60384242dc 100644 --- a/components/shopify_developer_app/actions/get-articles/get-articles.mjs +++ b/components/shopify_developer_app/actions/get-articles/get-articles.mjs @@ -1,12 +1,12 @@ import app from "../../common/rest-admin.mjs"; -import common from "../../../shopify/actions/get-articles/common.mjs"; +import common from "@pipedream/shopify/actions/get-articles/common.mjs"; export default { ...common, key: "shopify_developer_app-get-articles", name: "Get Articles", description: "Retrieve a list of all articles from a blog. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/article#get-blogs-blog-id-articles)", - version: "0.0.2", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify_developer_app/actions/get-metafields/get-metafields.mjs b/components/shopify_developer_app/actions/get-metafields/get-metafields.mjs index 734358b5bed95..666d2fd3fd4ad 100644 --- a/components/shopify_developer_app/actions/get-metafields/get-metafields.mjs +++ b/components/shopify_developer_app/actions/get-metafields/get-metafields.mjs @@ -1,12 +1,12 @@ import metafieldActions from "../common/metafield-actions.mjs"; -import common from "../../../shopify/actions/get-metafields/common.mjs"; +import common from "@pipedream/shopify/actions/get-metafields/common.mjs"; export default { ...common, key: "shopify_developer_app-get-metafields", name: "Get Metafields", description: "Retrieves a list of metafields that belong to a resource. [See the docs](https://shopify.dev/api/admin-rest/2023-01/resources/metafield#get-metafields?metafield[owner-id]=382285388&metafield[owner-resource]=blog)", - version: "0.0.2", + version: "0.0.5", type: "action", props: { ...metafieldActions.props, diff --git a/components/shopify_developer_app/actions/get-metaobjects/get-metaobjects.mjs b/components/shopify_developer_app/actions/get-metaobjects/get-metaobjects.mjs index cb37ffff44d23..c4f8cd9e79aac 100644 --- a/components/shopify_developer_app/actions/get-metaobjects/get-metaobjects.mjs +++ b/components/shopify_developer_app/actions/get-metaobjects/get-metaobjects.mjs @@ -1,6 +1,6 @@ import shopify from "../../shopify_developer_app.app.mjs"; import metaobjects from "../common/metaobjects.mjs"; -import common from "../../../shopify/actions/get-metaobjects/common.mjs"; +import common from "@pipedream/shopify/actions/get-metaobjects/common.mjs"; export default { ...metaobjects, @@ -8,7 +8,7 @@ export default { key: "shopify_developer_app-get-metaobjects", name: "Get Metaobjects", description: "Retrieves a list of metaobjects. [See the documentation](https://shopify.dev/docs/api/admin-graphql/2023-04/queries/metaobjects)", - version: "0.0.2", + version: "0.0.5", type: "action", props: { shopify, diff --git a/components/shopify_developer_app/actions/get-order/get-order.mjs b/components/shopify_developer_app/actions/get-order/get-order.mjs new file mode 100644 index 0000000000000..989055522d95d --- /dev/null +++ b/components/shopify_developer_app/actions/get-order/get-order.mjs @@ -0,0 +1,25 @@ +import app from "../../common/rest-admin.mjs"; + +export default { + key: "shopify_developer_app-get-order", + name: "Get Order", + description: "Retrieve an order by specifying the order ID. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2024-10/resources/order#get-orders)", + version: "0.0.2", + type: "action", + props: { + app, + orderId: { + propDefinition: [ + app, + "orderId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getOrder({ + orderId: this.orderId, + }); + $.export("$summary", `Successfully retrieved order with ID: ${this.orderId}`); + return response; + }, +}; diff --git a/components/shopify_developer_app/actions/get-pages/get-pages.mjs b/components/shopify_developer_app/actions/get-pages/get-pages.mjs index c192288eb870d..56d705fa8f120 100644 --- a/components/shopify_developer_app/actions/get-pages/get-pages.mjs +++ b/components/shopify_developer_app/actions/get-pages/get-pages.mjs @@ -1,12 +1,12 @@ import app from "../../common/rest-admin.mjs"; -import common from "../../../shopify/actions/get-pages/common.mjs"; +import common from "@pipedream/shopify/actions/get-pages/common.mjs"; export default { ...common, key: "shopify_developer_app-get-pages", name: "Get Pages", description: "Retrieve a list of all pages. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/page#get-pages)", - version: "0.0.2", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify_developer_app/actions/search-custom-collection-by-name/search-custom-collection-by-name.mjs b/components/shopify_developer_app/actions/search-custom-collection-by-name/search-custom-collection-by-name.mjs index 2267ac66f0355..71f1c30311a00 100644 --- a/components/shopify_developer_app/actions/search-custom-collection-by-name/search-custom-collection-by-name.mjs +++ b/components/shopify_developer_app/actions/search-custom-collection-by-name/search-custom-collection-by-name.mjs @@ -1,12 +1,12 @@ import shopify from "../../shopify_developer_app.app.mjs"; -import common from "../../../shopify/actions/search-custom-collection-by-name/common.mjs"; +import common from "@pipedream/shopify/actions/search-custom-collection-by-name/common.mjs"; export default { ...common, key: "shopify_developer_app-search-custom-collection-by-name", name: "Search Custom Collection by Name", description: "Search for a custom collection by name/title. [See the documentation](https://shopify.dev/docs/api/admin-rest/2023-01/resources/customcollection#get-custom-collections)", - version: "0.0.2", + version: "0.0.4", type: "action", props: { shopify, diff --git a/components/shopify_developer_app/actions/search-customers/search-customers.mjs b/components/shopify_developer_app/actions/search-customers/search-customers.mjs index 0cdcb0f4eff56..1d75df419293e 100644 --- a/components/shopify_developer_app/actions/search-customers/search-customers.mjs +++ b/components/shopify_developer_app/actions/search-customers/search-customers.mjs @@ -1,12 +1,12 @@ import shopify from "../../shopify_developer_app.app.mjs"; -import common from "../../../shopify/actions/search-customers/common.mjs"; +import common from "@pipedream/shopify/actions/search-customers/common.mjs"; export default { ...common, key: "shopify_developer_app-search-customers", name: "Search for Customers", description: "Search for a customer or a list of customers. [See the documentation](https://shopify.dev/api/admin-rest/2022-01/resources/customer#[get]/admin/api/2022-01/customers.json)", - version: "0.0.2", + version: "0.0.4", type: "action", props: { shopify, diff --git a/components/shopify_developer_app/actions/search-product-variant/search-product-variant.mjs b/components/shopify_developer_app/actions/search-product-variant/search-product-variant.mjs index 635e4910e69f3..ca699cdc98304 100644 --- a/components/shopify_developer_app/actions/search-product-variant/search-product-variant.mjs +++ b/components/shopify_developer_app/actions/search-product-variant/search-product-variant.mjs @@ -1,12 +1,12 @@ import shopify from "../../shopify_developer_app.app.mjs"; -import common from "../../../shopify/actions/search-product-variant/common.mjs"; +import common from "@pipedream/shopify/actions/search-product-variant/common.mjs"; export default { ...common, key: "shopify_developer_app-search-product-variant", name: "Search for Product Variant", description: "Search for product variants or create one if not found. [See the documentation](https://shopify.dev/api/admin-rest/2022-01/resources/product-variant#top)", - version: "0.0.2", + version: "0.0.4", type: "action", props: { shopify, diff --git a/components/shopify_developer_app/actions/search-products/search-products.mjs b/components/shopify_developer_app/actions/search-products/search-products.mjs index 57e5bd2f43116..28b3b091a7ee9 100644 --- a/components/shopify_developer_app/actions/search-products/search-products.mjs +++ b/components/shopify_developer_app/actions/search-products/search-products.mjs @@ -1,12 +1,12 @@ import shopify from "../../shopify_developer_app.app.mjs"; -import common from "../../../shopify/actions/search-products/common.mjs"; +import common from "@pipedream/shopify/actions/search-products/common.mjs"; export default { ...common, key: "shopify_developer_app-search-products", name: "Search for Products", description: "Search for products. [See the documentation](https://shopify.dev/api/admin-rest/2022-01/resources/product#[get]/admin/api/2022-01/products.json)", - version: "0.0.2", + version: "0.0.4", type: "action", props: { shopify, diff --git a/components/shopify_developer_app/actions/update-article/update-article.mjs b/components/shopify_developer_app/actions/update-article/update-article.mjs index 279353c63f27c..5f8789e25251e 100644 --- a/components/shopify_developer_app/actions/update-article/update-article.mjs +++ b/components/shopify_developer_app/actions/update-article/update-article.mjs @@ -1,12 +1,12 @@ import app from "../../common/rest-admin.mjs"; -import common from "../../../shopify/actions/update-article/common.mjs"; +import common from "@pipedream/shopify/actions/update-article/common.mjs"; export default { ...common, key: "shopify_developer_app-update-article", name: "Update Article", description: "Update a blog article. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/article#put-blogs-blog-id-articles-article-id)", - version: "0.0.2", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify_developer_app/actions/update-customer/update-customer.mjs b/components/shopify_developer_app/actions/update-customer/update-customer.mjs index d9c9b70d3b6e4..97fba4f7f2862 100644 --- a/components/shopify_developer_app/actions/update-customer/update-customer.mjs +++ b/components/shopify_developer_app/actions/update-customer/update-customer.mjs @@ -1,6 +1,6 @@ import shopify from "../../shopify_developer_app.app.mjs"; import metafieldActions from "../common/metafield-actions.mjs"; -import common from "../../../shopify/actions/update-customer/common.mjs"; +import common from "@pipedream/shopify/actions/update-customer/common.mjs"; export default { ...metafieldActions, @@ -8,7 +8,7 @@ export default { key: "shopify_developer_app-update-customer", name: "Update Customer", description: "Update a existing customer. [See the docs](https://shopify.dev/api/admin-rest/2022-01/resources/customer#[put]/admin/api/2022-01/customers/{customer_id}.json)", - version: "0.0.2", + version: "0.0.5", type: "action", props: { shopify, diff --git a/components/shopify_developer_app/actions/update-inventory-level/update-inventory-level.mjs b/components/shopify_developer_app/actions/update-inventory-level/update-inventory-level.mjs index 5c0d5af89040c..de1232e85d10b 100644 --- a/components/shopify_developer_app/actions/update-inventory-level/update-inventory-level.mjs +++ b/components/shopify_developer_app/actions/update-inventory-level/update-inventory-level.mjs @@ -1,12 +1,12 @@ import shopify from "../../shopify_developer_app.app.mjs"; -import common from "../../../shopify/actions/update-inventory-level/common.mjs"; +import common from "@pipedream/shopify/actions/update-inventory-level/common.mjs"; export default { ...common, key: "shopify_developer_app-update-inventory-level", name: "Update Inventory Level", description: "Sets the inventory level for an inventory item at a location. [See the documenation](https://shopify.dev/api/admin-rest/2022-01/resources/inventorylevel#[post]/admin/api/2022-01/inventory_levels/set.json)", - version: "0.0.2", + version: "0.0.4", type: "action", props: { shopify, diff --git a/components/shopify_developer_app/actions/update-metafield/update-metafield.mjs b/components/shopify_developer_app/actions/update-metafield/update-metafield.mjs index 023ba4fc89e04..1a67063d2d609 100644 --- a/components/shopify_developer_app/actions/update-metafield/update-metafield.mjs +++ b/components/shopify_developer_app/actions/update-metafield/update-metafield.mjs @@ -1,19 +1,77 @@ import metafieldActions from "../common/metafield-actions.mjs"; -import common from "../../../shopify/actions/update-metafield/common.mjs"; +import common from "@pipedream/shopify/actions/update-metafield/common.mjs"; +import shopify from "@pipedream/shopify/shopify.app.mjs"; export default { ...common, key: "shopify_developer_app-update-metafield", name: "Update Metafield", description: "Updates a metafield belonging to a resource. [See the docs](https://shopify.dev/api/admin-rest/2023-01/resources/metafield#put-blogs-blog-id-metafields-metafield-id)", - version: "0.0.2", + version: "0.0.5", type: "action", props: { ...metafieldActions.props, ...common.props, }, + async additionalProps() { + const props = await this.getOwnerIdProp(this.ownerResource); + + props.metafieldId = { + type: "string", + label: "Metafield ID", + description: "The metafield to update", + }; + props.value = { + type: "string", + label: "Value", + description: "The data to store in the metafield", + }; + + return props; + }, methods: { ...metafieldActions.methods, ...common.methods, + async getOwnerIdProp(ownerResource) { + const resources = { + product: shopify.propDefinitions.productId, + variants: shopify.propDefinitions.productVariantId, + product_image: { + ...shopify.propDefinitions.imageId, + optional: false, + }, + customer: shopify.propDefinitions.customerId, + collection: { + ...shopify.propDefinitions.collectionId, + optional: false, + }, + blog: shopify.propDefinitions.blogId, + article: shopify.propDefinitions.articleId, + page: shopify.propDefinitions.pageId, + order: shopify.propDefinitions.orderId, + draft_order: shopify.propDefinitions.draftOrderId, + }; + + const props = {}; + + if (ownerResource === "variants" || ownerResource === "product_image") { + props.productId = resources.product; + } + if (ownerResource === "article") { + props.blogId = resources.blog; + } + + Object.values(resources).forEach((resource) => { + delete resource.options; + }); + Object.values(props).forEach((prop) => { + delete prop.options; + }); + + return { + ...props, + ownerId: resources[ownerResource], + }; + }, }, }; diff --git a/components/shopify_developer_app/actions/update-metaobject/update-metaobject.mjs b/components/shopify_developer_app/actions/update-metaobject/update-metaobject.mjs index 359f68a4f8bf0..b70771409c898 100644 --- a/components/shopify_developer_app/actions/update-metaobject/update-metaobject.mjs +++ b/components/shopify_developer_app/actions/update-metaobject/update-metaobject.mjs @@ -1,6 +1,6 @@ import shopify from "../../shopify_developer_app.app.mjs"; import metaobjects from "../common/metaobjects.mjs"; -import common from "../../../shopify/actions/update-metaobject/common.mjs"; +import common from "@pipedream/shopify/actions/update-metaobject/common.mjs"; export default { ...metaobjects, @@ -8,7 +8,7 @@ export default { key: "shopify_developer_app-update-metaobject", name: "Update Metaobject", description: "Updates a metaobject. [See the documentation](https://shopify.dev/docs/api/admin-graphql/2023-04/mutations/metaobjectUpdate)", - version: "0.0.4", + version: "0.0.7", type: "action", methods: { ...metaobjects.methods, diff --git a/components/shopify_developer_app/actions/update-page/update-page.mjs b/components/shopify_developer_app/actions/update-page/update-page.mjs index ce836e74384fc..881e97a343d18 100644 --- a/components/shopify_developer_app/actions/update-page/update-page.mjs +++ b/components/shopify_developer_app/actions/update-page/update-page.mjs @@ -1,12 +1,12 @@ import app from "../../common/rest-admin.mjs"; -import common from "../../../shopify/actions/update-page/common.mjs"; +import common from "@pipedream/shopify/actions/update-page/common.mjs"; export default { ...common, key: "shopify_developer_app-update-page", name: "Update Page", description: "Update an existing page. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/page#put-pages-page-id)", - version: "0.0.2", + version: "0.0.6", type: "action", props: { app, diff --git a/components/shopify_developer_app/actions/update-product-variant/update-product-variant.mjs b/components/shopify_developer_app/actions/update-product-variant/update-product-variant.mjs index e67714aeef38d..ac8aa5cab6666 100644 --- a/components/shopify_developer_app/actions/update-product-variant/update-product-variant.mjs +++ b/components/shopify_developer_app/actions/update-product-variant/update-product-variant.mjs @@ -1,6 +1,6 @@ import shopify from "../../shopify_developer_app.app.mjs"; import metafieldActions from "../common/metafield-actions.mjs"; -import common from "../../../shopify/actions/update-product-variant/common.mjs"; +import common from "@pipedream/shopify/actions/update-product-variant/common.mjs"; export default { ...common, @@ -8,7 +8,7 @@ export default { key: "shopify_developer_app-update-product-variant", name: "Update Product Variant", description: "Update an existing product variant. [See the docs](https://shopify.dev/api/admin-rest/2022-01/resources/product-variant#[put]/admin/api/2022-01/variants/{variant_id}.json)", - version: "0.0.2", + version: "0.0.6", type: "action", props: { shopify, @@ -67,10 +67,11 @@ export default { ], description: "The country code [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) of where the item came from", }, - harmonizedSystemCode: { - type: "integer", - label: "Harmonized System Code", - description: "The general [harmonized system](https://en.wikipedia.org/wiki/Harmonized_System) code for the inventory item", + locationId: { + propDefinition: [ + shopify, + "locationId", + ], optional: true, }, ...common.props, diff --git a/components/shopify_developer_app/actions/update-product/update-product.mjs b/components/shopify_developer_app/actions/update-product/update-product.mjs index f26674fd068c2..587299fc43641 100644 --- a/components/shopify_developer_app/actions/update-product/update-product.mjs +++ b/components/shopify_developer_app/actions/update-product/update-product.mjs @@ -1,6 +1,6 @@ import shopify from "../../shopify_developer_app.app.mjs"; import metafieldActions from "../common/metafield-actions.mjs"; -import common from "../../../shopify/actions/update-product/common.mjs"; +import common from "@pipedream/shopify/actions/update-product/common.mjs"; export default { ...common, @@ -8,7 +8,7 @@ export default { key: "shopify_developer_app-update-product", name: "Update Product", description: "Update an existing product. [See the docs](https://shopify.dev/api/admin-rest/2022-01/resources/product#[put]/admin/api/2022-01/products/{product_id}.json)", - version: "0.0.2", + version: "0.0.5", type: "action", props: { shopify, diff --git a/components/shopify_developer_app/common/rest-admin.mjs b/components/shopify_developer_app/common/rest-admin.mjs index e7f6118def660..cf99e4afc9cb5 100644 --- a/components/shopify_developer_app/common/rest-admin.mjs +++ b/components/shopify_developer_app/common/rest-admin.mjs @@ -54,6 +54,20 @@ export default { })); }, }, + orderId: { + type: "string", + label: "Order ID", + description: "The unique numeric identifier for the order.", + async options() { + const { orders } = await this.listOrders(); + return orders.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, }, methods: { ...common.methods, @@ -65,5 +79,19 @@ export default { ...headers, }; }, + listOrders(args = {}) { + return this.makeRequest({ + path: "/orders", + ...args, + }); + }, + getOrder({ + orderId, ...args + }) { + return this.makeRequest({ + path: `/orders/${orderId}`, + ...args, + }); + }, }, }; diff --git a/components/shopify_developer_app/package.json b/components/shopify_developer_app/package.json index 74aca1a4bb6d7..b56c7dbd37ae0 100644 --- a/components/shopify_developer_app/package.json +++ b/components/shopify_developer_app/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/shopify_developer_app", - "version": "0.2.2", + "version": "0.6.1", "description": "Pipedream Shopify (Developer App) Components", "main": "shopify_developer_app.app.mjs", "keywords": [ @@ -13,7 +13,8 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.5.1", + "@pipedream/platform": "^3.0.0", + "@pipedream/shopify": "^0.6.5", "shopify-api-node": "^3.12.5" } } diff --git a/components/shopify_developer_app/shopify_developer_app.app.mjs b/components/shopify_developer_app/shopify_developer_app.app.mjs index 6685fa6884807..1b89fd0146a89 100644 --- a/components/shopify_developer_app/shopify_developer_app.app.mjs +++ b/components/shopify_developer_app/shopify_developer_app.app.mjs @@ -1,4 +1,4 @@ -import commonApp from "../shopify/common-app.mjs"; +import commonApp from "@pipedream/shopify/common-app.mjs"; import Shopify from "shopify-api-node"; export default { diff --git a/components/shopify_developer_app/sources/common/webhook-metafields.mjs b/components/shopify_developer_app/sources/common/webhook-metafields.mjs new file mode 100644 index 0000000000000..e881397ec2d25 --- /dev/null +++ b/components/shopify_developer_app/sources/common/webhook-metafields.mjs @@ -0,0 +1,20 @@ +import common from "./webhook.mjs"; + +export default { + ...common, + props: { + ...common.props, + metafieldNamespaces: { + type: "string[]", + label: "Metafield Namespaces", + description: "Array of namespaces for any metafields that should be included with each webhook. Metafield definitions can be found in your store's Settings -> Custom Data. Select a metafield to view its namespace under \"Namespace and key\". For example, if the value is `custom.test_metafield`, the namespace is `custom`.", + optional: true, + }, + privateMetafieldNamespaces: { + type: "string[]", + label: "Private Metafield Namespaces", + description: "Array of namespaces for any private metafields that should be included with each webhook. Metafield definitions can be found in your store's Settings -> Custom Data. Select a metafield to view its namespace under \"Namespace and key\". For example, if the value is `custom.test_metafield`, the namespace is `custom`.", + optional: true, + }, + }, +}; diff --git a/components/shopify_developer_app/sources/common/webhook.mjs b/components/shopify_developer_app/sources/common/webhook.mjs index 31ded4fc14e16..969dfc985199a 100644 --- a/components/shopify_developer_app/sources/common/webhook.mjs +++ b/components/shopify_developer_app/sources/common/webhook.mjs @@ -9,18 +9,6 @@ export default { type: "$.interface.http", customResponse: true, }, - metafieldNamespaces: { - type: "string[]", - label: "Metafield Namespaces", - description: "Array of namespaces for any metafields that should be included with each webhook", - optional: true, - }, - privateMetafieldNamespaces: { - type: "string[]", - label: "Private Metafield Namespaces", - description: "Array of namespaces for any private metafields that should be included with each webhook", - optional: true, - }, }, hooks: { async activate() { @@ -59,6 +47,39 @@ export default { return this.app.getShopId() === shopId && this.getTopic() === topic; }, + checkMetaFields({ + metafields = [], private_metafields: privateMetafields = [], + } = {}) { + let validateMetafield = true; + let validatePrivateMetafield = true; + if (this.metafieldNamespaces?.length) { + validateMetafield = false; + for (const metafieldNamespace of this.metafieldNamespaces) { + for (const metafieldObj of metafields) { + if (metafieldObj.namespace === metafieldNamespace) { + validateMetafield = true; + break; + } + } + if (validateMetafield) break; + } + } + + if (this.privateMetafieldNamespaces?.length) { + validatePrivateMetafield = false; + for (const privateMetafieldNamespace of this.privateMetafieldNamespaces) { + for (const privateMetafieldObj of privateMetafields) { + if (privateMetafieldObj.namespace === privateMetafieldNamespace) { + validatePrivateMetafield = true; + break; + } + } + if (validatePrivateMetafield) break; + } + } + + return validateMetafield && validatePrivateMetafield; + }, isRelevant() { return true; }, diff --git a/components/shopify_developer_app/sources/new-abandoned-cart/new-abandoned-cart.mjs b/components/shopify_developer_app/sources/new-abandoned-cart/new-abandoned-cart.mjs index 72f462e15c4fd..8835ae9ba65e5 100644 --- a/components/shopify_developer_app/sources/new-abandoned-cart/new-abandoned-cart.mjs +++ b/components/shopify_developer_app/sources/new-abandoned-cart/new-abandoned-cart.mjs @@ -1,5 +1,5 @@ import shopify from "../../shopify_developer_app.app.mjs"; -import common from "../../../shopify/sources/new-abandoned-cart/common.mjs"; +import common from "@pipedream/shopify/sources/new-abandoned-cart/common.mjs"; export default { ...common, @@ -7,7 +7,7 @@ export default { name: "New Abandoned Cart", type: "source", description: "Emit new event each time a user abandons their cart.", - version: "0.0.3", + version: "0.0.5", dedupe: "unique", props: { shopify, diff --git a/components/shopify_developer_app/sources/new-article/new-article.mjs b/components/shopify_developer_app/sources/new-article/new-article.mjs index 0a2de7fdf14ba..b1f804b75e44a 100644 --- a/components/shopify_developer_app/sources/new-article/new-article.mjs +++ b/components/shopify_developer_app/sources/new-article/new-article.mjs @@ -1,5 +1,5 @@ import shopify from "../../shopify_developer_app.app.mjs"; -import common from "../../../shopify/sources/new-article/common.mjs"; +import common from "@pipedream/shopify/sources/new-article/common.mjs"; export default { ...common, @@ -7,7 +7,7 @@ export default { name: "New Article", type: "source", description: "Emit new event for each new article in a blog.", - version: "0.0.2", + version: "0.0.4", dedupe: "unique", props: { shopify, diff --git a/components/shopify_developer_app/sources/new-cancelled-order/new-cancelled-order.mjs b/components/shopify_developer_app/sources/new-cancelled-order/new-cancelled-order.mjs index 254ca1238e4b0..af89099ca31ef 100644 --- a/components/shopify_developer_app/sources/new-cancelled-order/new-cancelled-order.mjs +++ b/components/shopify_developer_app/sources/new-cancelled-order/new-cancelled-order.mjs @@ -1,5 +1,5 @@ -import common from "../common/webhook.mjs"; import constants from "../common/constants.mjs"; +import common from "../common/webhook-metafields.mjs"; export default { ...common, @@ -7,7 +7,7 @@ export default { name: "New Cancelled Order (Instant)", type: "source", description: "Emit new event each time a new order is cancelled.", - version: "0.0.1", + version: "0.0.6", dedupe: "unique", methods: { ...common.methods, diff --git a/components/shopify_developer_app/sources/new-customer-created/new-customer-created.mjs b/components/shopify_developer_app/sources/new-customer-created/new-customer-created.mjs index 6b01c199f8d87..ee31f1eadcbbb 100644 --- a/components/shopify_developer_app/sources/new-customer-created/new-customer-created.mjs +++ b/components/shopify_developer_app/sources/new-customer-created/new-customer-created.mjs @@ -1,5 +1,5 @@ -import common from "../common/webhook.mjs"; import constants from "../common/constants.mjs"; +import common from "../common/webhook-metafields.mjs"; export default { ...common, @@ -7,7 +7,7 @@ export default { name: "New Customer Created (Instant)", type: "source", description: "Emit new event for each new customer added to a store.", - version: "0.0.1", + version: "0.0.6", dedupe: "unique", methods: { ...common.methods, diff --git a/components/shopify_developer_app/sources/new-draft-order/new-draft-order.mjs b/components/shopify_developer_app/sources/new-draft-order/new-draft-order.mjs index b1f2a265a4e53..a75ade3c4da2f 100644 --- a/components/shopify_developer_app/sources/new-draft-order/new-draft-order.mjs +++ b/components/shopify_developer_app/sources/new-draft-order/new-draft-order.mjs @@ -1,5 +1,5 @@ -import common from "../common/webhook.mjs"; import constants from "../common/constants.mjs"; +import common from "../common/webhook-metafields.mjs"; export default { ...common, @@ -7,7 +7,7 @@ export default { name: "New Draft Order (Instant)", type: "source", description: "Emit new event for each new draft order submitted to a store.", - version: "0.0.1", + version: "0.0.6", dedupe: "unique", methods: { ...common.methods, diff --git a/components/shopify_developer_app/sources/new-event-emitted/new-event-emitted.mjs b/components/shopify_developer_app/sources/new-event-emitted/new-event-emitted.mjs index aa9e2a5e62fa3..9ec5a93f1fb2b 100644 --- a/components/shopify_developer_app/sources/new-event-emitted/new-event-emitted.mjs +++ b/components/shopify_developer_app/sources/new-event-emitted/new-event-emitted.mjs @@ -1,5 +1,5 @@ -import common from "../common/webhook.mjs"; import constants from "../common/constants.mjs"; +import common from "../common/webhook-metafields.mjs"; export default { ...common, @@ -7,7 +7,7 @@ export default { name: "New Event Emitted (Instant)", type: "source", description: "Emit new event for each new Shopify event.", - version: "0.0.1", + version: "0.0.6", dedupe: "unique", props: { ...common.props, diff --git a/components/shopify_developer_app/sources/new-fulfillment-event/new-fulfillment-event.mjs b/components/shopify_developer_app/sources/new-fulfillment-event/new-fulfillment-event.mjs new file mode 100644 index 0000000000000..ae266723c7ced --- /dev/null +++ b/components/shopify_developer_app/sources/new-fulfillment-event/new-fulfillment-event.mjs @@ -0,0 +1,26 @@ +import constants from "../common/constants.mjs"; +import common from "../common/webhook-metafields.mjs"; + +export default { + ...common, + key: "shopify_developer_app-new-fulfillment-event", + name: "New Fulfillment Event (Instant)", + type: "source", + description: "Emit new event for each new fulfillment event for a store.", + version: "0.0.4", + dedupe: "unique", + methods: { + ...common.methods, + getTopic() { + return constants.EVENT_TOPIC.FULFILLMENT_EVENTS_CREATE; + }, + generateMeta(resource) { + const ts = Date.parse(resource.updated_at); + return { + id: ts, + summary: `New Fulfillment Event ${resource.id}.`, + ts, + }; + }, + }, +}; diff --git a/components/shopify_developer_app/sources/new-order-created/new-order-created.mjs b/components/shopify_developer_app/sources/new-order-created/new-order-created.mjs index 20630b44b1052..0acefc9c1bde4 100644 --- a/components/shopify_developer_app/sources/new-order-created/new-order-created.mjs +++ b/components/shopify_developer_app/sources/new-order-created/new-order-created.mjs @@ -1,5 +1,5 @@ -import common from "../common/webhook.mjs"; import constants from "../common/constants.mjs"; +import common from "../common/webhook-metafields.mjs"; export default { ...common, @@ -7,7 +7,7 @@ export default { name: "New Order Created (Instant)", type: "source", description: "Emit new event for each new order submitted to a store.", - version: "0.0.1", + version: "0.0.6", dedupe: "unique", methods: { ...common.methods, diff --git a/components/shopify_developer_app/sources/new-order-fulfilled/new-order-fulfilled.mjs b/components/shopify_developer_app/sources/new-order-fulfilled/new-order-fulfilled.mjs new file mode 100644 index 0000000000000..0412f66ece6ed --- /dev/null +++ b/components/shopify_developer_app/sources/new-order-fulfilled/new-order-fulfilled.mjs @@ -0,0 +1,26 @@ +import constants from "../common/constants.mjs"; +import common from "../common/webhook-metafields.mjs"; + +export default { + ...common, + key: "shopify_developer_app-new-order-fulfilled", + name: "New Order Fulfilled (Instant)", + type: "source", + description: "Emit new event whenever an order is fulfilled.", + version: "0.0.3", + dedupe: "unique", + methods: { + ...common.methods, + getTopic() { + return constants.EVENT_TOPIC.ORDERS_FULFILLED; + }, + generateMeta(resource) { + const ts = Date.parse(resource.updated_at); + return { + id: ts, + summary: `New Fulfilled Order ${resource.id}.`, + ts, + }; + }, + }, +}; diff --git a/components/shopify_developer_app/sources/new-page/new-page.mjs b/components/shopify_developer_app/sources/new-page/new-page.mjs index ba21cf47668be..e788231c7866d 100644 --- a/components/shopify_developer_app/sources/new-page/new-page.mjs +++ b/components/shopify_developer_app/sources/new-page/new-page.mjs @@ -1,5 +1,5 @@ import shopify from "../../shopify_developer_app.app.mjs"; -import common from "../../../shopify/sources/new-page/common.mjs"; +import common from "@pipedream/shopify/sources/new-page/common.mjs"; export default { ...common, @@ -7,7 +7,7 @@ export default { name: "New Page", type: "source", description: "Emit new event for each new page published.", - version: "0.0.2", + version: "0.0.4", dedupe: "unique", props: { shopify, diff --git a/components/shopify_developer_app/sources/new-paid-order/new-paid-order.mjs b/components/shopify_developer_app/sources/new-paid-order/new-paid-order.mjs index 604730e287859..a21e926821ddc 100644 --- a/components/shopify_developer_app/sources/new-paid-order/new-paid-order.mjs +++ b/components/shopify_developer_app/sources/new-paid-order/new-paid-order.mjs @@ -1,5 +1,5 @@ -import common from "../common/webhook.mjs"; import constants from "../common/constants.mjs"; +import common from "../common/webhook-metafields.mjs"; export default { ...common, @@ -7,7 +7,7 @@ export default { name: "New Paid Order (Instant)", type: "source", description: "Emit new event each time a new order is paid.", - version: "0.0.1", + version: "0.0.6", dedupe: "unique", methods: { ...common.methods, diff --git a/components/shopify_developer_app/sources/new-product-created/new-product-created.mjs b/components/shopify_developer_app/sources/new-product-created/new-product-created.mjs index cea80c0485ade..0d208ae1bf7d1 100644 --- a/components/shopify_developer_app/sources/new-product-created/new-product-created.mjs +++ b/components/shopify_developer_app/sources/new-product-created/new-product-created.mjs @@ -1,5 +1,5 @@ -import common from "../common/webhook.mjs"; import constants from "../common/constants.mjs"; +import common from "../common/webhook-metafields.mjs"; export default { ...common, @@ -7,7 +7,7 @@ export default { name: "New Product Created (Instant)", type: "source", description: "Emit new event for each product added to a store.", - version: "0.0.1", + version: "0.0.6", dedupe: "unique", methods: { ...common.methods, diff --git a/components/shopify_developer_app/sources/new-product-updated/new-product-updated.mjs b/components/shopify_developer_app/sources/new-product-updated/new-product-updated.mjs new file mode 100644 index 0000000000000..f33a849e6022d --- /dev/null +++ b/components/shopify_developer_app/sources/new-product-updated/new-product-updated.mjs @@ -0,0 +1,55 @@ +import constants from "../common/constants.mjs"; +import common from "../common/webhook-metafields.mjs"; + +export default { + ...common, + key: "shopify_developer_app-new-product-updated", + name: "New Product Updated (Instant)", + description: "Emit new event for each product updated in a store.", + version: "0.0.4", + type: "source", + dedupe: "unique", + props: { + ...common.props, + productType: { + type: "string", + label: "Product Type", + description: "Filter results by product type", + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "Filter results by product tag(s)", + optional: true, + }, + }, + methods: { + ...common.methods, + getTopic() { + return constants.EVENT_TOPIC.PRODUCTS_UPDATE; + }, + isRelevant(resource) { + let relevant = true; + if (this.productType && resource.product_type !== this.productType) { + relevant = false; + } + if (this.tags?.length) { + this.tags.forEach((tag) => { + if (!resource.tags?.includes(tag)) { + relevant = false; + } + }); + } + return relevant; + }, + generateMeta(resource) { + const ts = Date.parse(resource.updated_at); + return { + id: `${resource.id}-${ts}`, + summary: `Product Updated ${resource.id}`, + ts, + }; + }, + }, +}; diff --git a/components/shopify_developer_app/sources/new-product-updated/test-event.mjs b/components/shopify_developer_app/sources/new-product-updated/test-event.mjs new file mode 100644 index 0000000000000..465572db40b94 --- /dev/null +++ b/components/shopify_developer_app/sources/new-product-updated/test-event.mjs @@ -0,0 +1,64 @@ +export default { + "admin_graphql_api_id": "gid://shopify/Product/8416570474776", + "body_html": null, + "created_at": "2023-06-30T13:38:03-04:00", + "handle": "best-product-ever", + "id": 8416570474776, + "product_type": "", + "published_at": "2023-06-30T13:38:03-04:00", + "template_suffix": null, + "title": "product", + "updated_at": "2024-06-18T14:37:58-04:00", + "vendor": "Testing", + "status": "active", + "published_scope": "web", + "tags": "abc, def", + "variants": [ + { + "admin_graphql_api_id": "gid://shopify/ProductVariant/45612961333528", + "barcode": null, + "compare_at_price": null, + "created_at": "2023-06-30T13:38:03-04:00", + "fulfillment_service": "manual", + "id": 45612961333528, + "inventory_management": null, + "inventory_policy": "deny", + "position": 1, + "price": "0.00", + "product_id": 8416570474776, + "sku": "", + "taxable": true, + "title": "Default Title", + "updated_at": "2023-10-27T10:08:17-04:00", + "option1": "Default Title", + "option2": null, + "option3": null, + "grams": 0, + "image_id": null, + "weight": 0, + "weight_unit": "lb", + "inventory_item_id": 47661178487064, + "inventory_quantity": 0, + "old_inventory_quantity": 0, + "requires_shipping": true + }, + ], + "options": [ + { + "name": "Title", + "id": 10664239726872, + "product_id": 8416570474776, + "position": 1, + "values": [ + "Default Title", + ] + } + ], + "images": [], + "image": null, + "variant_ids": [ + { + "id": 45612961333528 + } + ] +} \ No newline at end of file diff --git a/components/shopify_developer_app/sources/new-refund-created/new-refund-created.mjs b/components/shopify_developer_app/sources/new-refund-created/new-refund-created.mjs new file mode 100644 index 0000000000000..93a0035a8f423 --- /dev/null +++ b/components/shopify_developer_app/sources/new-refund-created/new-refund-created.mjs @@ -0,0 +1,26 @@ +import constants from "../common/constants.mjs"; +import common from "../common/webhook.mjs"; + +export default { + ...common, + key: "shopify_developer_app-new-refund-created", + name: "New Refund Created (Instant)", + description: "Emit new event when a new refund is created.", + version: "0.0.3", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTopic() { + return constants.EVENT_TOPIC.REFUNDS_CREATE; + }, + generateMeta(resource) { + const ts = Date.parse(resource.created_at); + return { + id: `${resource.id}-${ts}`, + summary: `Refund Created ${resource.id}`, + ts, + }; + }, + }, +}; diff --git a/components/shopify_developer_app/sources/new-refund-created/test-event.mjs b/components/shopify_developer_app/sources/new-refund-created/test-event.mjs new file mode 100644 index 0000000000000..d5514d9f8373f --- /dev/null +++ b/components/shopify_developer_app/sources/new-refund-created/test-event.mjs @@ -0,0 +1,155 @@ +export default { + "id": 1002961240349, + "order_id": 6273085767965, + "created_at": "2024-08-20T11:19:18-04:00", + "note": "", + "user_id": 90630848797, + "processed_at": "2024-08-20T11:19:18-04:00", + "restock": true, + "duties": [], + "total_duties_set": { + "shop_money": { + "amount": "0", + "currency_code": "VND" + }, + "presentment_money": { + "amount": "0", + "currency_code": "VND" + } + }, + "return": null, + "refund_shipping_lines": [], + "admin_graphql_api_id": "gid://shopify/Refund/1002961240349", + "order_adjustments": [], + "refund_line_items": [ + { + "id": 665556615453, + "quantity": 1, + "line_item_id": 16014782595357, + "location_id": 76683084061, + "restock_type": "cancel", + "subtotal": 20, + "total_tax": 0, + "subtotal_set": { + "shop_money": { + "amount": "20", + "currency_code": "VND" + }, + "presentment_money": { + "amount": "20", + "currency_code": "VND" + } + }, + "total_tax_set": { + "shop_money": { + "amount": "0", + "currency_code": "VND" + }, + "presentment_money": { + "amount": "0", + "currency_code": "VND" + } + }, + "line_item": { + "id": 16014782595357, + "variant_id": 44392954495261, + "title": "T-shirt 1", + "quantity": 1, + "sku": "", + "variant_title": "Pipedream1003", + "vendor": "Pipedream0930", + "fulfillment_service": "manual", + "product_id": 8137104785693, + "requires_shipping": true, + "taxable": false, + "gift_card": false, + "name": "T-shirt 1 - Pipedream1003", + "variant_inventory_management": "shopify", + "properties": [], + "product_exists": true, + "fulfillable_quantity": 0, + "grams": 0, + "price": "20", + "total_discount": "0", + "fulfillment_status": null, + "price_set": { + "shop_money": { + "amount": "20", + "currency_code": "VND" + }, + "presentment_money": { + "amount": "20", + "currency_code": "VND" + } + }, + "total_discount_set": { + "shop_money": { + "amount": "0", + "currency_code": "VND" + }, + "presentment_money": { + "amount": "0", + "currency_code": "VND" + } + }, + "discount_allocations": [], + "duties": [], + "admin_graphql_api_id": "gid://shopify/LineItem/16014782595357", + "tax_lines": [ + { + "title": "VAT", + "price": "0", + "rate": 0.1, + "channel_liable": false, + "price_set": { + "shop_money": { + "amount": "0", + "currency_code": "VND" + }, + "presentment_money": { + "amount": "0", + "currency_code": "VND" + } + } + } + ] + } + } + ], + "transactions": [ + { + "id": 7582534205725, + "order_id": 6273085767965, + "kind": "refund", + "gateway": "manual", + "status": "success", + "message": "Refunded 20.00 from manual gateway", + "created_at": "2024-08-20T11:19:18-04:00", + "test": false, + "authorization": null, + "location_id": null, + "user_id": 90630848797, + "parent_id": 7582533648669, + "processed_at": "2024-08-20T11:19:18-04:00", + "device_id": null, + "error_code": null, + "source_name": "1830279", + "receipt": {}, + "amount": "20", + "currency": "VND", + "payment_id": "#1031.2", + "total_unsettled_set": { + "presentment_money": { + "amount": "0.0", + "currency": "VND" + }, + "shop_money": { + "amount": "0.0", + "currency": "VND" + } + }, + "manual_payment_gateway": true, + "admin_graphql_api_id": "gid://shopify/OrderTransaction/7582534205725" + } + ] +} \ No newline at end of file diff --git a/components/shopify_developer_app/sources/new-shipment/new-shipment.mjs b/components/shopify_developer_app/sources/new-shipment/new-shipment.mjs deleted file mode 100644 index 5a32a905d7351..0000000000000 --- a/components/shopify_developer_app/sources/new-shipment/new-shipment.mjs +++ /dev/null @@ -1,26 +0,0 @@ -import common from "../common/webhook.mjs"; -import constants from "../common/constants.mjs"; - -export default { - ...common, - key: "shopify_developer_app-new-shipment", - name: "New Shipment (Instant)", - type: "source", - description: "Emit new event for each new fulfillment event for a store.", - version: "0.0.1", - dedupe: "unique", - methods: { - ...common.methods, - getTopic() { - return constants.EVENT_TOPIC.ORDERS_FULFILLED; - }, - generateMeta(resource) { - const ts = Date.parse(resource.updated_at); - return { - id: ts, - summary: `New Shipped Order ${resource.id}.`, - ts, - }; - }, - }, -}; diff --git a/components/shopify_developer_app/sources/new-updated-customer/new-updated-customer.mjs b/components/shopify_developer_app/sources/new-updated-customer/new-updated-customer.mjs index 0f5e617350156..f971478ff1873 100644 --- a/components/shopify_developer_app/sources/new-updated-customer/new-updated-customer.mjs +++ b/components/shopify_developer_app/sources/new-updated-customer/new-updated-customer.mjs @@ -1,5 +1,5 @@ -import common from "../common/webhook.mjs"; import constants from "../common/constants.mjs"; +import common from "../common/webhook-metafields.mjs"; export default { ...common, @@ -7,7 +7,7 @@ export default { name: "New Updated Customer (Instant)", type: "source", description: "Emit new event each time a customer's information is updated.", - version: "0.0.1", + version: "0.0.6", dedupe: "unique", methods: { ...common.methods, diff --git a/components/shopify_developer_app/sources/new-updated-order/new-updated-order.mjs b/components/shopify_developer_app/sources/new-updated-order/new-updated-order.mjs index 71e1dc2b0a107..5067a38bb82a5 100644 --- a/components/shopify_developer_app/sources/new-updated-order/new-updated-order.mjs +++ b/components/shopify_developer_app/sources/new-updated-order/new-updated-order.mjs @@ -1,5 +1,5 @@ -import common from "../common/webhook.mjs"; import constants from "../common/constants.mjs"; +import common from "../common/webhook-metafields.mjs"; export default { ...common, @@ -7,7 +7,7 @@ export default { name: "New Updated Order (Instant)", type: "source", description: "Emit new event each time an order is updated.", - version: "0.0.1", + version: "0.0.6", dedupe: "unique", methods: { ...common.methods, diff --git a/components/shopify_developer_app/sources/product-added-to-custom-collection/product-added-to-custom-collection.mjs b/components/shopify_developer_app/sources/product-added-to-custom-collection/product-added-to-custom-collection.mjs index 99f8853ad01bb..86a0232f08935 100644 --- a/components/shopify_developer_app/sources/product-added-to-custom-collection/product-added-to-custom-collection.mjs +++ b/components/shopify_developer_app/sources/product-added-to-custom-collection/product-added-to-custom-collection.mjs @@ -1,12 +1,12 @@ import shopify from "../../shopify_developer_app.app.mjs"; -import common from "../../../shopify/sources/product-added-to-custom-collection/common.mjs"; +import common from "@pipedream/shopify/sources/product-added-to-custom-collection/common.mjs"; export default { ...common, key: "shopify_developer_app-product-added-to-custom-collection", name: "New product added to custom collection", description: "Emit new event each time a product is added to a custom collection.", - version: "0.0.2", + version: "0.0.4", type: "source", dedupe: "unique", props: { diff --git a/components/shopify_partner/README.md b/components/shopify_partner/README.md index a935b98405dbd..fe2dd8ad24a08 100644 --- a/components/shopify_partner/README.md +++ b/components/shopify_partner/README.md @@ -1,4 +1,8 @@ -# Shopify Partner API Integration +# Overview + +The Shopify Partner API lets you tap into a comprehensive suite of features to manage and analyze multiple Shopify stores. You can automate tasks like creating development stores, adding collaborators, tracking payouts, and more. This API serves as a powerful tool for developers, agencies, and freelancers who manage multiple Shopify shops for their clients. Through Pipedream, you can effortlessly integrate Shopify Partner API with other services to create tailored, efficient workflows that save time and enhance productivity. + +# Getting Started ## Configure API Key @@ -24,3 +28,11 @@ This action will poll the Shopify Partner API for new app installs, requires an ### New Transactions This action will poll the Shopify Partner API for new app charges, including reoccurring, one time and usage charges. + +# Example Use Cases + +- **Automated Development Store Setup**: Streamline the process of setting up new development stores for clients by creating a Pipedream workflow. This can automatically configure store preferences, install essential apps, and set up staff accounts when a new project is initiated in your project management tool (like Trello or Asana). + +- **Collaborator Access Management**: Craft a workflow on Pipedream that manages collaborator requests and access levels. For instance, when a new team member is added to your GitHub repository, the workflow can send a collaborator invitation for the corresponding Shopify store, ensuring seamless team onboarding. + +- **Payout Tracking and Notifications**: Build a notification system with Pipedream that monitors and reports on your Shopify Partner payouts. Configure a workflow to send a summary of payouts via Slack or email on a regular basis, or trigger alerts when payouts fall below a certain threshold, helping you maintain financial oversight. \ No newline at end of file diff --git a/components/shopify_partner/actions/verify-webhook/verify-webhook.mjs b/components/shopify_partner/actions/verify-webhook/verify-webhook.mjs index 82051ae0b2189..72fb351742f65 100644 --- a/components/shopify_partner/actions/verify-webhook/verify-webhook.mjs +++ b/components/shopify_partner/actions/verify-webhook/verify-webhook.mjs @@ -3,7 +3,7 @@ import crypto from "crypto"; export default { name: "Verify Webhook", - version: "0.0.3", + version: "0.0.5", key: "shopify_partner-verify-webhook", description: "Verify an incoming webhook from Shopify. Exits the workflow if the signature is not valid, otherwise returns `true`", diff --git a/components/shopify_partner/package.json b/components/shopify_partner/package.json index 86cdb0f961812..8cecfee696bcb 100644 --- a/components/shopify_partner/package.json +++ b/components/shopify_partner/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/shopify_partner", - "version": "0.1.0", + "version": "0.1.3", "description": "Pipedream Shopify Partner Components", "main": "shopify_partner.app.js", "keywords": [ diff --git a/components/shopify_partner/shopify_partner.app.mjs b/components/shopify_partner/shopify_partner.app.mjs index a26f53bec0734..95f6deb742058 100644 --- a/components/shopify_partner/shopify_partner.app.mjs +++ b/components/shopify_partner/shopify_partner.app.mjs @@ -1,13 +1,16 @@ import "graphql/language/index.js"; import { GraphQLClient } from "graphql-request"; +export const PARTNER_API_VERSION = "2024-10"; + export default { type: "app", app: "shopify_partner", propDefinitions: { appId: { type: "string", - description: "Open your app in the partner portal, and look at the URL to find its ID. If your URL is *https://partners.shopify.com/3027494/apps/51358007297/overview*, enter `51358007297` here.", + description: + "Open your app in the partner portal, and look at the URL to find its ID. If your URL is *https://partners.shopify.com/3027494/apps/51358007297/overview*, enter `51358007297` here.", label: "Shopify App ID", reloadProps: true, }, @@ -45,7 +48,8 @@ export default { value: "backward", }, ], - description: "Which direction to paginate through records. Forwards will only look into the future, whereas backwards will comb through all records.", + description: + "Which direction to paginate through records. Forwards will only look into the future, whereas backwards will comb through all records.", default: "forward", }, }, @@ -74,7 +78,7 @@ export default { paginationDirection = "forward", recordsPerRun = 50, }) { - const endpoint = `https://partners.shopify.com/${this.$auth.organization_id}/api/2023-04/graphql.json`; + const endpoint = `https://partners.shopify.com/${this.$auth.organization_id}/api/${PARTNER_API_VERSION}/graphql.json`; const client = new GraphQLClient(endpoint, { headers: { "Content-Type": "application/json", diff --git a/components/shopify_partner/sources/new-app-charges/new-app-charges.mjs b/components/shopify_partner/sources/new-app-charges/new-app-charges.mjs index ce8ff39c76005..0ec56559e68b8 100644 --- a/components/shopify_partner/sources/new-app-charges/new-app-charges.mjs +++ b/components/shopify_partner/sources/new-app-charges/new-app-charges.mjs @@ -5,7 +5,7 @@ export default { key: "shopify_partner-new-app-charges", name: "New App Charges", type: "source", - version: "0.0.15", + version: "0.0.18", description: "Emit new events when new app charges made to your partner account.", ...common, @@ -28,17 +28,16 @@ export default { }, async run() { const { - createdAtMin, - createdAtMax, - after, - db, + createdAtMin, createdAtMax, db, } = this; - const variables = { - ...(createdAtMin || {}), - ...(createdAtMax || {}), - ...(after || {}), - }; + const variables = {}; + if (createdAtMin) { + variables.createdAtMin = createdAtMin.trim(); + } + if (createdAtMax) { + variables.createdAtMax = createdAtMax.trim(); + } await this.shopify.query({ db, @@ -50,7 +49,9 @@ export default { return null; }, handleEmit: (data) => { - console.log(data.transactions.edges.map(({ node: { ...txn } }) => txn.id)); + console.log( + data.transactions.edges.map(({ node: { ...txn } }) => txn.id), + ); data.transactions.edges.map(({ node: { ...txn } }) => { this.$emit(txn, { diff --git a/components/shopify_partner/sources/new-app-installs/new-app-installs.mjs b/components/shopify_partner/sources/new-app-installs/new-app-installs.mjs index c7c96d0bc3c18..99f28060ee5f5 100644 --- a/components/shopify_partner/sources/new-app-installs/new-app-installs.mjs +++ b/components/shopify_partner/sources/new-app-installs/new-app-installs.mjs @@ -6,7 +6,7 @@ export default { key: "shopify_partner-new-app-installs", name: "New App Installs", type: "source", - version: "0.1.0", + version: "0.1.3", description: "Emit new events when new shops install your app.", ...common, props: { @@ -33,17 +33,18 @@ export default { }, async run() { const { - appId, - occurredAtMin, - occurredAtMax, - db, + appId, occurredAtMin, occurredAtMax, db, } = this; const variables = { appId: `gid://partners/App/${appId}`, - ...(occurredAtMin || {}), - ...(occurredAtMax || {}), }; + if (occurredAtMin) { + variables.occurredAtMin = occurredAtMin.trim(); + } + if (occurredAtMax) { + variables.occurredAtMax = occurredAtMax.trim(); + } await this.shopify.query({ db, diff --git a/components/shopify_partner/sources/new-app-relationship-events/new-app-relationship-events.mjs b/components/shopify_partner/sources/new-app-relationship-events/new-app-relationship-events.mjs index e3bd8b8d43dca..dcf9cbac439a5 100644 --- a/components/shopify_partner/sources/new-app-relationship-events/new-app-relationship-events.mjs +++ b/components/shopify_partner/sources/new-app-relationship-events/new-app-relationship-events.mjs @@ -7,8 +7,9 @@ export default { key: "shopify_partner-new-app-relationship-events", name: "New App Relationship Events", type: "source", - version: "0.1.0", - description: "Emit new events when new shops installs, uninstalls, subscribes or unsubscribes your app.", + version: "0.1.3", + description: + "Emit new events when new shops installs, uninstalls, subscribes or unsubscribes your app.", ...common, props: { ...common.props, @@ -56,18 +57,24 @@ export default { const variables = { appId: `gid://partners/App/${appId}`, - ...(occurredAtMin || {}), - ...(occurredAtMax || {}), }; + if (occurredAtMin) { + variables.occurredAtMin = occurredAtMin.trim(); + } + if (occurredAtMax) { + variables.occurredAtMax = occurredAtMax.trim(); + } console.log("Querying events"); await this.shopify.query({ db, key: "shopify_partner-relationship-events", - query: this.paginationDirection === "backward" || !this.db.get("shopify_partner-relationship-events") // on the first run, pull records from present day - ? getAppRelationshipEventsBackwards - : getAppRelationshipEventsForwards, + query: + this.paginationDirection === "backward" || + !this.db.get("shopify_partner-relationship-events") // on the first run, pull records from present day + ? getAppRelationshipEventsBackwards + : getAppRelationshipEventsForwards, variables, handleEmit: (data) => { data.app.events.edges.map(({ node: { ...event } }) => { @@ -90,11 +97,12 @@ export default { console.log("First event in batch: ", first); return first?.cursor; } - }, - hasNextPagePath: this.paginationDirection === "forward" || !this.db.get("shopify_partner-relationship-events") - ? "app.events.pageInfo.hasNextPage" - : "app.events.pageInfo.hasPreviousPage", + hasNextPagePath: + this.paginationDirection === "forward" || + !this.db.get("shopify_partner-relationship-events") + ? "app.events.pageInfo.hasNextPage" + : "app.events.pageInfo.hasPreviousPage", paginationDirection, recordsPerRun, }); diff --git a/components/shopify_partner/sources/new-app-uninstalls/new-app-uninstalls.mjs b/components/shopify_partner/sources/new-app-uninstalls/new-app-uninstalls.mjs index 183b34b35d1e4..2607c609dc7a3 100644 --- a/components/shopify_partner/sources/new-app-uninstalls/new-app-uninstalls.mjs +++ b/components/shopify_partner/sources/new-app-uninstalls/new-app-uninstalls.mjs @@ -6,7 +6,7 @@ export default { key: "shopify_partner-new-app-uninstalls", name: "New App Uninstalls", type: "source", - version: "0.1.0", + version: "0.1.3", description: "Emit new events when new shops uninstall your app.", ...common, props: { @@ -32,17 +32,18 @@ export default { }, async run() { const { - appId, - occurredAtMin, - occurredAtMax, - db, + appId, occurredAtMin, occurredAtMax, db, } = this; const variables = { appId: `gid://partners/App/${appId}`, - ...(occurredAtMin || {}), - ...(occurredAtMax || {}), }; + if (occurredAtMin) { + variables.occurredAtMin = occurredAtMin.trim(); + } + if (occurredAtMax) { + variables.occurredAtMax = occurredAtMax.trim(); + } await this.shopify.query({ db, diff --git a/components/shopmonkey/package.json b/components/shopmonkey/package.json new file mode 100644 index 0000000000000..d936ed92851b7 --- /dev/null +++ b/components/shopmonkey/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/shopmonkey", + "version": "0.6.0", + "description": "Pipedream shopmonkey Components", + "main": "shopmonkey.app.mjs", + "keywords": [ + "pipedream", + "shopmonkey" + ], + "homepage": "https://pipedream.com/apps/shopmonkey", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/shoprocket/README.md b/components/shoprocket/README.md new file mode 100644 index 0000000000000..58835eba7f40a --- /dev/null +++ b/components/shoprocket/README.md @@ -0,0 +1,11 @@ +# Overview + +The Shoprocket API empowers you to seamlessly integrate e-commerce capabilities into your website or app. With Pipedream, you can connect Shoprocket to hundreds of other services to automate tasks like syncing orders to a database, updating inventory in real-time, or triggering custom email campaigns when a new product is added. Pipedream's serverless platform allows for building custom workflows that can react to various Shoprocket events, manipulate the data, and interact with numerous APIs and services without the hassle of managing infrastructure. + +# Example Use Cases + +- **Order Processing Automation**: When a new order is placed in Shoprocket, trigger a Pipedream workflow to validate the order details, then generate an invoice using the QuickBooks API, and finally send the invoice to the customer via SendGrid. + +- **Real-Time Inventory Sync**: Keep your inventory up-to-date across multiple platforms. When inventory changes in Shoprocket, use a Pipedream workflow to instantly update stock levels in a connected ERP system like NetSuite and simultaneously send a Slack notification to the inventory management team. + +- **Customer Follow-Up Campaigns**: After a purchase, automatically enroll customers in a follow-up campaign by triggering a Pipedream workflow that adds the customer's email to a Mailchimp list, sends a personalized thank you email, and schedules a series of tailored marketing emails aimed at repeat engagement. diff --git a/components/shopwaive/README.md b/components/shopwaive/README.md new file mode 100644 index 0000000000000..d86b32cb17f57 --- /dev/null +++ b/components/shopwaive/README.md @@ -0,0 +1,11 @@ +# Overview + +The Shopwaive API enables seamless integration of e-commerce and retail management features into your existing platforms. Using Pipedream, you can connect the Shopwaive API to a vast array of services to automate tasks such as updating inventory, syncing orders across platforms, and managing customer interactions. This API, when harnessed through Pipedream’s serverless platform, offers a powerful way to streamline e-commerce operations, reduce manual overhead, and enhance the customer experience with real-time updates and actions. + +# Example Use Cases + +- **Order Synchronization Across Platforms**: Automatically sync new orders from Shopwaive to your CRM or database. When a new order is placed in Shopwaive, a Pipedream workflow triggers and updates the relevant customer records, ensuring all systems reflect the latest data without manual intervention. + +- **Stock Level Alerting**: Set up a workflow that monitors your Shopwaive inventory levels and sends alerts via email or messaging apps like Slack when stock for a particular item falls below a certain threshold. This real-time alerting helps maintain inventory accuracy and prevent stockouts. + +- **Automated Customer Follow-Up**: Create a workflow where after an order is marked as delivered in Shopwaive, an automated follow-up email is sent to the customer asking for feedback or providing a discount code for their next purchase, enhancing customer engagement and loyalty. diff --git a/components/short/README.md b/components/short/README.md index b50b982a779c0..07fd7b8fdf829 100644 --- a/components/short/README.md +++ b/components/short/README.md @@ -1,22 +1,11 @@ # Overview -With the Short.io API, you can create powerful links for a variety of -use-cases. It is possible to shorten, brand, measure, and even monetize your -links. +Short.io provides a robust API for URL shortening, allowing you to create, delete, and track shortened links programmatically. By integrating with Pipedream, you can automate link creation or aggregation of click data in real-time, triggering workflows in response to events like link clicks or creating short links in bulk from a data source. -Here is a list of uses for the Short.io API: +# Example Use Cases -- Shorten URLs – Create shortened URLs you can share on social media, web - pages, and other places where you want a clean, concise link. -- Brand Links – Create SEO-friendly branded links with custom domains and - analytics tracking. -- Measure Performance – Analyze ROI and user engagement via links in app - notifications, emails, SMS, etc. -- Monetize Your Links – Monetize traffic with pay-per-click advertising links, - affiliate programs, and subscription services. -- Automation – Automatically shorten URLs for SEO purposes, or for integrating - into other websites and services. -- Track & Report – Get real-time statistics about link performance, as well as - performance on integrated services. -- Security & Privacy – Secure your links with password protection and UTM - parameters to better protect your data. +- **Dynamic Link Generation for Marketing Campaigns**: Automate the creation of unique short links for email campaigns using customer data from a CRM like Salesforce. When a new contact is added to a campaign, Pipedream can trigger a workflow that generates a custom Short.io link, which is then emailed to the contact, providing personalized follow-up and tracking engagement. + +- **Real-time Click Analytics Reporting**: Use Short.io with Pipedream to monitor click data and generate real-time analytics reports. Connect to a data visualization tool like Google Sheets or Tableau, and whenever a Short.io link is clicked, Pipedream can append the click information to a sheet or database, enabling live tracking of campaign performance. + +- **Automated Social Media Posting**: Combine Short.io with social media platforms such as Twitter or Facebook via Pipedream. Create a workflow where blog posts or content updates on a CMS trigger the generation of a short link, which is then automatically posted to your social media profiles, streamlining the content sharing process and enhancing social media management. diff --git a/components/short/actions/create-a-link/create-a-link.mjs b/components/short/actions/create-a-link/create-a-link.mjs index 5aca310dc4e6c..ddb577e56c54e 100644 --- a/components/short/actions/create-a-link/create-a-link.mjs +++ b/components/short/actions/create-a-link/create-a-link.mjs @@ -2,19 +2,39 @@ import shortApp from "../../short.app.mjs"; import common from "../common/common.mjs"; import lodash from "lodash"; +const { + props: { // eslint-disable-next-line no-unused-vars + domain, ...props + }, +} = common; + export default { key: "short-create-a-link", - name: "Create a Short Link", - description: "Create a Short Link. [See the docs](https://developers.short.io/reference/linkspost).", - version: "0.0.1", + name: "Create Link", + description: "Create a Short Link. [See the documentation](https://developers.short.io/reference/linkspost).", + version: "0.1.0", type: "action", props: { shortApp, - ...common.props, + domainId: { + propDefinition: [ + shortApp, + "domainId", + ], + }, + ...props, + folderId: { + propDefinition: [ + shortApp, + "folderId", + ({ domainId }) => ({ + domainId, + }), + ], + }, }, async run({ $ }) { const param = lodash.pick(this, [ - "domain", "originalURL", "path", "title", @@ -32,8 +52,13 @@ export default { "utmContent", "cloaking", "redirectType", + "folderId", ]); - const link = await this.shortApp.createLink($, param); + const { hostname: domain } = await this.shortApp.getDomainInfo(this.domainId); + const link = await this.shortApp.createLink($, { + domain, + ...param, + }); $.export("$summary", `Successfully created the link: ${link.secureShortURL}`); return link; }, diff --git a/components/short/actions/delete-a-link/delete-a-link.mjs b/components/short/actions/delete-a-link/delete-a-link.mjs index 485251d0bc6e5..8599db080e796 100644 --- a/components/short/actions/delete-a-link/delete-a-link.mjs +++ b/components/short/actions/delete-a-link/delete-a-link.mjs @@ -2,9 +2,9 @@ import shortApp from "../../short.app.mjs"; export default { key: "short-delete-a-link", - name: "Delete a Short Link", - description: "Delete a Short Link. [See the docs](https://developers.short.io/reference/linksbylinkiddelete).", - version: "0.0.1", + name: "Delete Link", + description: "Delete a Short Link. [See the documentation](https://developers.short.io/reference/linksbylinkiddelete).", + version: "0.0.2", type: "action", props: { shortApp, diff --git a/components/short/actions/domain-statistics/domain-statistics.mjs b/components/short/actions/domain-statistics/domain-statistics.mjs index d5d3eb6666985..f2ae2516e46fb 100644 --- a/components/short/actions/domain-statistics/domain-statistics.mjs +++ b/components/short/actions/domain-statistics/domain-statistics.mjs @@ -3,9 +3,9 @@ import lodash from "lodash"; export default { key: "short-domain-statistics", - name: "Domain Statistics.", - description: "Returns detailed statistics for domain in given period. [See the docs](https://developers.short.io/reference/getdomaindomainid).", - version: "0.0.1", + name: "Get Domain Statistics", + description: "Returns detailed statistics for a domain in given period. [See the documentation](https://developers.short.io/reference/getdomaindomainid).", + version: "0.0.2", type: "action", props: { shortApp, diff --git a/components/short/actions/expire-a-link/expire-a-link.mjs b/components/short/actions/expire-a-link/expire-a-link.mjs index 681bdcbce7148..60d8db137f26f 100644 --- a/components/short/actions/expire-a-link/expire-a-link.mjs +++ b/components/short/actions/expire-a-link/expire-a-link.mjs @@ -3,9 +3,9 @@ import lodash from "lodash"; export default { key: "short-expire-a-link", - name: "Expire a Link.", - description: "Expire a link by id. [See the docs](https://developers.short.io/reference/linksbylinkidpost).", - version: "0.0.1", + name: "Expire Link", + description: "Expire a short link by id. [See the documentation](https://developers.short.io/reference/linksbylinkidpost).", + version: "0.0.2", type: "action", props: { shortApp, diff --git a/components/short/actions/update-a-link/update-a-link.mjs b/components/short/actions/update-a-link/update-a-link.mjs index 2fee030695568..03f8a7f965d1a 100644 --- a/components/short/actions/update-a-link/update-a-link.mjs +++ b/components/short/actions/update-a-link/update-a-link.mjs @@ -4,9 +4,9 @@ import lodash from "lodash"; export default { key: "short-update-a-link", - name: "Update a Short Link.", - description: "Update original url, title or path for existing URL by id. [See the docs](https://developers.short.io/reference/linksbylinkidpost).", - version: "0.0.1", + name: "Update Link", + description: "Update original URL, title or path for existing URL by id. [See the documentation](https://developers.short.io/reference/linksbylinkidpost).", + version: "0.0.2", type: "action", props: { shortApp, diff --git a/components/short/package.json b/components/short/package.json index b3cc2202a2de0..c493777081cb2 100644 --- a/components/short/package.json +++ b/components/short/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/short", - "version": "0.0.5", + "version": "0.1.0", "description": "Pipedream Short.io Components", "main": "short.app.mjs", "keywords": [ diff --git a/components/short/short.app.mjs b/components/short/short.app.mjs index 4709b85368fcf..8c9991e745ef8 100644 --- a/components/short/short.app.mjs +++ b/components/short/short.app.mjs @@ -20,6 +20,15 @@ export default { return this.listDomainsOpts(true); }, }, + folderId: { + type: "string", + label: "Folder ID", + description: "The folder to use.", + optional: true, + async options({ domainId }) { + return this.listFolderOptions(domainId); + }, + }, originalURL: { type: "string", label: "Original URL", @@ -325,5 +334,21 @@ export default { })); return response; }, + async getDomainInfo(domainId) { + return axios(this, this._getRequestParams({ + path: `/domains/${domainId}`, + })); + }, + async listFolderOptions(domainId) { + const response = await axios(this, this._getRequestParams({ + path: `/links/folders/${domainId}`, + })); + return response.linkFolders?.map(({ + id, name, + }) => ({ + label: name, + value: id, + })); + }, }, }; diff --git a/components/short/sources/new-link-created/new-link-created.mjs b/components/short/sources/new-link-created/new-link-created.mjs index c3c9a499bd428..52ee94e855301 100644 --- a/components/short/sources/new-link-created/new-link-created.mjs +++ b/components/short/sources/new-link-created/new-link-created.mjs @@ -3,9 +3,9 @@ import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; export default { key: "short-new-link-created", - name: "New event for each link created.", + name: "New Link Created", description: "Emit new event when a link is created.", - version: "0.0.3", + version: "0.0.4", type: "source", dedupe: "unique", props: { diff --git a/components/short_menu/actions/create-short-link/create-short-link.mjs b/components/short_menu/actions/create-short-link/create-short-link.mjs new file mode 100644 index 0000000000000..74355507d1fba --- /dev/null +++ b/components/short_menu/actions/create-short-link/create-short-link.mjs @@ -0,0 +1,45 @@ +import shortMenu from "../../short_menu.app.mjs"; + +export default { + key: "short_menu-create-short-link", + name: "Create Short Link", + description: "Create a new short link. [See the documentationo](https://docs.shortmenu.com/api-reference/endpoint/create-link)", + version: "0.0.1", + type: "action", + props: { + shortMenu, + destinationUrl: { + type: "string", + label: "Destination URL", + description: "Destination URL of the new short link. Must be a valid URL starting with a scheme such as `https`.", + }, + slug: { + type: "string", + label: "Slug", + description: "Optional slug for the new short link. If empty, a random slug will be generated. Must be unique within the domain.", + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "Tags to assign to the new short link", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.shortMenu.createShortLink({ + $, + data: { + destinationUrl: this.destinationUrl, + domain: this.shortMenu.$auth.custom_domain, + slug: this.slug, + tags: this.tags?.map((tag) => ({ + name: tag, + })) || [], + }, + }); + + $.export("$summary", `Successfully created short link ${response.shortUrl}`); + return response; + }, +}; diff --git a/components/short_menu/package.json b/components/short_menu/package.json new file mode 100644 index 0000000000000..76167142a47ce --- /dev/null +++ b/components/short_menu/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/short_menu", + "version": "0.1.0", + "description": "Pipedream Short Menu Components", + "main": "short_menu.app.mjs", + "keywords": [ + "pipedream", + "short_menu" + ], + "homepage": "https://pipedream.com/apps/short_menu", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/short_menu/short_menu.app.mjs b/components/short_menu/short_menu.app.mjs new file mode 100644 index 0000000000000..93d9a6d6528a3 --- /dev/null +++ b/components/short_menu/short_menu.app.mjs @@ -0,0 +1,31 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "short_menu", + methods: { + _baseUrl() { + return "https://api.shortmenu.com"; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "x-api-key": `${this.$auth.api_key}`, + }, + ...opts, + }); + }, + createShortLink(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/links", + ...opts, + }); + }, + }, +}; diff --git a/components/shortcut/README.md b/components/shortcut/README.md index 332d9e1c15e27..4dfc0399422e5 100644 --- a/components/shortcut/README.md +++ b/components/shortcut/README.md @@ -1,15 +1,11 @@ # Overview -With the Shortcut (formerly Clubhouse.io) API, developers can create various -applications that tap into the power of Shortcut. Here are just a few examples -of what can be built using the Shortcut API: - -- Automation and integration tools that automate certain Shortcut processes -- Dashboard and visualization tools that display key metrics -- Cross-team collaboration tools that allow more groups to work together -- Team management tools that simplify workload tracking and resource scheduling -- Reporting and analytics systems that enhance data-driven decision making -- Custom fields and filters to customize the capabilities of Shortcut -- Internal discussion and team communication systems integrating Shortcut with - Slack or other collaboration tools -- Knowledgebase and helpdesk management tools for customer service +The Shortcut (formerly Clubhouse.io) API empowers teams to automate project management tasks, streamline workflows, and integrate with other tools seamlessly. Using the API through Pipedream, you can trigger actions based on project updates, synchronize data across platforms, and manipulate Shortcut resources like stories, epics, and sprints programmatically. This ability to interact with Shortcut entities opens up a plethora of automation possibilities, optimizing the development cycle and communication pathways. + +# Example Use Cases + +- **Sync Shortcut Stories with GitHub Issues**: Automatically create a linked GitHub issue when a new story is created in Shortcut. Conversely, when an issue is closed in GitHub, update the corresponding Shortcut story's status. This keeps both systems in sync and ensures that the team is on the same page across platforms. + +- **Automate Sprint Retrospectives**: At the end of a sprint, generate a report based on story statuses and comments in Shortcut and post the retrospective findings to a Slack channel. This workflow can help teams to quickly gather insights on sprint performance without manual data compilation. + +- **Enhance Customer Support**: When a customer submits a support ticket in Zendesk, analyze the content and, if it's feature-related, automatically create a story in Shortcut. This streamlines the process of tracking feature requests and ensures they're considered in the product development lifecycle. diff --git a/components/shorten_rest/README.md b/components/shorten_rest/README.md new file mode 100644 index 0000000000000..b974dcd16da9c --- /dev/null +++ b/components/shorten_rest/README.md @@ -0,0 +1,11 @@ +# Overview + +Shorten.REST API on Pipedream allows you to automate URL shortening, expanding, and tracking within your custom workflows. With this API, you can create short, branded links programmatically, obtain detailed analytics on click-throughs, and manage your URLs efficiently, all within Pipedream's serverless platform. This enables seamless integration of URL management into your applications, marketing campaigns, or day-to-day tasks while leveraging various triggers and actions from other apps available on Pipedream. + +# Example Use Cases + +- **Automate Link Shortening for Social Media Posts**: Post to platforms like Twitter or Facebook with shortened links. Whenever a new blog post goes live on your CMS, use Pipedream to trigger a workflow that shortens the URL and posts an update with the shortened link to your social media accounts. + +- **Dynamic QR Code Generation for Events**: Create QR codes for event tickets or promotions. When a user registers for an event on Eventbrite, Pipedream can catch the webhook, shorten the event URL, and generate a QR code using Shorten.REST API, which can then be emailed to the attendee. + +- **Link Analytics Integration with Google Sheets**: Collect and analyze click data on your short links. Each time a short link is clicked, Pipedream can log the click data to a Google Sheet, allowing you to track performance and engagement over time without manual entry. diff --git a/components/shorten_rest/actions/get-clicks/get-clicks.mjs b/components/shorten_rest/actions/get-clicks/get-clicks.mjs new file mode 100644 index 0000000000000..34f4f373ba897 --- /dev/null +++ b/components/shorten_rest/actions/get-clicks/get-clicks.mjs @@ -0,0 +1,29 @@ +import app from "../../shorten_rest.app.mjs"; + +export default { + key: "shorten_rest-get-clicks", + name: "Get Clicks", + description: "Gets the click data. [See the documentation](https://docs.shorten.rest/#tag/Click/operation/GetClicks)", + version: "0.0.1", + type: "action", + props: { + app, + }, + methods: { + getClicks(args = {}) { + return this.app._makeRequest({ + path: "/clicks", + ...args, + }); + }, + }, + async run({ $ }) { + const { getClicks } = this; + + const response = await getClicks({ + $, + }); + $.export("$summary", `Successfully retrieved \`${response.clicks.length}\` click(s).`); + return response; + }, +}; diff --git a/components/shorten_rest/actions/short-link/short-link.mjs b/components/shorten_rest/actions/short-link/short-link.mjs new file mode 100644 index 0000000000000..43dc8854f0738 --- /dev/null +++ b/components/shorten_rest/actions/short-link/short-link.mjs @@ -0,0 +1,60 @@ +import app from "../../shorten_rest.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "shorten_rest-short-link", + name: "Shorten Link", + description: "Shortens a given long URL into an alias. If the alias name is not provided, the system generates one. If the domain input is not provided, it defaults to short.fyi. [See the documentation](https://docs.shorten.rest/#tag/Alias/operation/CreateAlias)", + version: "0.0.1", + type: "action", + props: { + app, + domainName: { + type: "string", + label: "Domain Name", + description: "The domain which alias will belong to (string without `http/https` or `/`). Eg. `your.domain.com`. Defaults to `short.fyi`", + optional: true, + }, + aliasName: { + type: "string", + label: "Alias Name", + description: "Alias (without `/` at the beginning). Eg. `aBcDe012`. Defaults to `@rnd`", + optional: true, + }, + destinations: { + type: "string[]", + label: "Destinations", + description: "Array of objects (**DestinationModel**). Where each row should be a JSON object like this: `{\"url\": \"mydomain.com\"}`. Please read [the docs here](https://docs.shorten.rest/#section/Introduction)", + }, + }, + methods: { + createAlias(args = {}) { + return this.app.post({ + path: "/aliases", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createAlias, + domainName, + aliasName, + destinations, + } = this; + + const response = await createAlias({ + $, + params: { + aliasName, + domainName, + }, + data: { + destinations: utils.parseArray(destinations), + }, + }); + + $.export("$summary", `Successfully shortened the link to \`${response.shortUrl}\``); + return response; + }, +}; diff --git a/components/shorten_rest/common/utils.mjs b/components/shorten_rest/common/utils.mjs new file mode 100644 index 0000000000000..eceb6a9167739 --- /dev/null +++ b/components/shorten_rest/common/utils.mjs @@ -0,0 +1,50 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function isJson(value) { + value = + typeof(value) !== "string" + ? JSON.stringify(value) + : value; + + try { + value = JSON.parse(value); + } catch (e) { + return false; + } + + return typeof(value) === "object" && value !== null; +} + +function valueToObject(value) { + if (!isJson(value)) { + return value; + } + return JSON.parse(value); +} + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid JSON array object"); + } +} + +export default { + parseArray: (value) => parseArray(value).map(valueToObject), +}; diff --git a/components/shorten_rest/package.json b/components/shorten_rest/package.json index 24931e40573f1..d4e3d0ee9cd74 100644 --- a/components/shorten_rest/package.json +++ b/components/shorten_rest/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/shorten_rest", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Shorten.REST Components", "main": "shorten_rest.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" } -} \ No newline at end of file +} diff --git a/components/shorten_rest/shorten_rest.app.mjs b/components/shorten_rest/shorten_rest.app.mjs index 240b2254d0a8c..32b71c5957c4c 100644 --- a/components/shorten_rest/shorten_rest.app.mjs +++ b/components/shorten_rest/shorten_rest.app.mjs @@ -1,11 +1,31 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "shorten_rest", - propDefinitions: {}, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.shorten.rest"; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + const config = { + ...args, + url: this._baseUrl() + path, + headers: { + ...headers, + "Content-Type": "application/json", + "x-api-key": this.$auth.api_key, + }, + }; + return axios($, config); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/shortpixel/README.md b/components/shortpixel/README.md new file mode 100644 index 0000000000000..c89b701cc4558 --- /dev/null +++ b/components/shortpixel/README.md @@ -0,0 +1,11 @@ +# Overview + +The ShortPixel API is a powerful tool for optimizing images and PDFs to improve website load times and performance without sacrificing quality. With the ShortPixel API on Pipedream, you can build automated workflows that trigger image optimization processes, handle optimized data, and integrate this functionality with other apps. Pipedream's serverless platform allows for creating scalable and maintenance-free workflows, turning manual image optimization into automated, seamless operations. + +# Example Use Cases + +- **Automated Website Image Optimization**: Set up a workflow where every new image uploaded to your cloud storage (like AWS S3) is automatically sent to ShortPixel for optimization, then the optimized image is saved back in the storage with improved load times for your website's visitors. + +- **Scheduled Bulk Image Optimization**: Create a workflow that periodically scans a specific folder in your Google Drive or Dropbox for new images, sends them to ShortPixel for optimization, and then replaces the original with the optimized versions or stores them in a separate "optimized" folder. + +- **Dynamic Email Campaigns with Optimized Images**: Integrate ShortPixel in a workflow where images uploaded to a CMS are optimized and then dynamically inserted into outgoing email campaigns via an email service like SendGrid, ensuring fast-loading emails that enhance user engagement and conversion rates. diff --git a/components/shortpixel/actions/optimize-image/optimize-image.mjs b/components/shortpixel/actions/optimize-image/optimize-image.mjs new file mode 100644 index 0000000000000..3c0fffd097499 --- /dev/null +++ b/components/shortpixel/actions/optimize-image/optimize-image.mjs @@ -0,0 +1,118 @@ +import shortpixel from "../../shortpixel.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; +import fs from "fs"; + +export default { + key: "shortpixel-optimize-image", + name: "Optimize Image", + description: "Optimize and/or adjust an image using ShortPixel. [See the documentation](https://shortpixel.com/knowledge-base/article/shortpixel-adaptive-images-api-parameters/)", + version: "0.0.1", + type: "action", + props: { + shortpixel, + url: { + type: "string", + label: "URL", + description: "The URL of the image to optimize", + }, + width: { + type: "integer", + label: "Width", + description: "The width in pixels of the new image", + optional: true, + }, + height: { + type: "integer", + label: "Height", + description: "The height in pixels of the new image", + optional: true, + }, + cropStyle: { + type: "string", + label: "Crop Style", + description: "The crop style, useful when both width and height are specified", + options: [ + "top", + "right", + "bottom", + "left", + "center", + ], + optional: true, + }, + quality: { + type: "string", + label: "Quality", + description: "The quality setting of the new image", + options: [ + "lqip", + "lossless", + "glossy", + "lossy", + ], + optional: true, + }, + filename: { + type: "string", + label: "Filename", + description: "Optionally, enter a filename that will be used to save the image in /tmp", + optional: true, + }, + }, + methods: { + buildParams() { + const paramArray = [ + "ret_wait ", + ]; + if (this.width) { + paramArray.push(`w_${this.width}`); + } + if (this.height) { + paramArray.push(`h_${this.height}`); + } + if (this.cropStyle) { + paramArray.push(`c_${this.cropStyle}`); + } + if (this.quality) { + paramArray.push(`q_${this.quality}`); + } + return paramArray.join(","); + }, + downloadFileToTmp(file, filePath) { + const rawcontent = file.toString("base64"); + const buffer = Buffer.from(rawcontent, "base64"); + fs.writeFileSync(filePath, buffer); + }, + }, + async run({ $ }) { + if (!this.width && !this.height && !this.cropStyle && !this.quality) { + throw new ConfigurationError("Must enter at least one of `width`, `height`, `cropStyle`, or `quality`"); + } + + const params = this.buildParams(); + + let response = { + url: `${this.shortpixel._baseUrl()}/client/${params}/${this.url}`, + }; + + try { + const image = await this.shortpixel.optimizeImage({ + $, + params, + url: this.url, + responseType: "arraybuffer", + }); + if (this.filename) { + const filePath = this.filename.includes("tmp/") + ? this.filename + : `/tmp/${this.filename}`; + this.downloadFileToTmp(image, filePath); + response.filePath = filePath; + } + } catch { + throw new Error(`Unable to process image at URL: ${this.url}`); + } + + return response; + }, +}; diff --git a/components/shortpixel/package.json b/components/shortpixel/package.json new file mode 100644 index 0000000000000..2f3f6e60c303b --- /dev/null +++ b/components/shortpixel/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/shortpixel", + "version": "0.1.0", + "description": "Pipedream ShortPixel Components", + "main": "shortpixel.app.mjs", + "keywords": [ + "pipedream", + "shortpixel" + ], + "homepage": "https://pipedream.com/apps/shortpixel", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/shortpixel/shortpixel.app.mjs b/components/shortpixel/shortpixel.app.mjs new file mode 100644 index 0000000000000..9a5ad65e85713 --- /dev/null +++ b/components/shortpixel/shortpixel.app.mjs @@ -0,0 +1,35 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "shortpixel", + propDefinitions: {}, + methods: { + _baseUrl() { + return "https://cdn.shortpixel.ai"; + }, + _makeRequest({ + $ = this, + path, + params, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + params: { + ...params, + key: this.$auth.api_key, + }, + ...opts, + }); + }, + optimizeImage({ + params, url, ...opts + }) { + return this._makeRequest({ + path: `/client/${params}/${url}`, + ...opts, + }); + }, + }, +}; diff --git a/components/shotstack/README.md b/components/shotstack/README.md new file mode 100644 index 0000000000000..98c12016f7ce5 --- /dev/null +++ b/components/shotstack/README.md @@ -0,0 +1,11 @@ +# Overview + +The Shotstack API is a video editing platform that allows you to automate the creation and production of videos. With it, you can edit clips, add transitions, overlays, and backgrounds, and even include music in your videos. Integrating Shotstack with Pipedream can supercharge your workflow by automating the video creation process. You can trigger video edits in response to various events, manage assets, or even kick off rendering jobs based on data from other apps and services. + +# Example Use Cases + +- **Automated Video Generation from Form Submissions**: When a user submits a form with content to be included in a video, the form data can be sent to Shotstack via Pipedream to create a personalized video. This could be great for user-generated content, personalized marketing, or event recaps. + +- **Video Updates from Project Management Tools**: Link Shotstack to a project management app like Trello or Asana. When a project reaches a certain stage, trigger a workflow on Pipedream to compile project highlights and updates into a video summary, which can then be shared with the team or stakeholders. + +- **Social Media Content Creation**: Set up a workflow where new posts or images from social media platforms (like Instagram or Twitter) are automatically turned into a video slideshow via Shotstack. This allows for rapid content repurposing and easy sharing across different platforms. diff --git a/components/showpad/README.md b/components/showpad/README.md index 1ee3fa89e8ffe..5efea44ba3c4c 100644 --- a/components/showpad/README.md +++ b/components/showpad/README.md @@ -1,26 +1,11 @@ # Overview -The Showpad API () provides API endpoints and tools that -developers can use to build innovative applications. With Showpad API, you can -develop powerful applications that integrate with Showpad content, users, and -activities to enhance the user experience. Here are some examples of what you -can build with the Showpad API: +The Showpad API opens up a world of possibilities for enhancing sales enablement and content management. With it, you can streamline content sharing, track engagement, and integrate sales coaching into various platforms. It offers programmatic access to manage users, groups, permissions, and content, enabling automated processes that save time and boost productivity. -- Content management capabilities: Showpad API enables you to create folders, - upload files, and create content links. You can also create and manage views, - collections, campaigns, and more. -- User authentication and authorization: Showpad API allows you to authenticate - and authorize users in your system. -- User activities and metrics tracking: Showpad API provides the ability to - track user activities such as viewing, downloading, and sharing content. You - can also get an overview of user metrics such as views, downloads, and - sharing. -- Content delivery and management: Showpad API enables you to deliver content - to a wide range of end-users and devices, as well as to manage the - distribution of content. -- Custom content integration and customization: Showpad API allows developers - to integrate with custom content sources, enabling customization and - extending the capabilities of Showpad's content library. -- Advanced search capabilities: With Showpad API, you can enable custom - searches, allowing users to easily find the right content. You can also - enable filters and sorting capabilities to refine search results. +# Example Use Cases + +- **Automated Content Distribution**: Trigger a workflow on Pipedream to distribute new marketing materials to sales teams in Showpad whenever a new file is uploaded to a Dropbox folder. This ensures that the sales team always has the latest materials at their fingertips. + +- **Lead Engagement Tracking**: Connect Showpad to a CRM like Salesforce on Pipedream. Automate the import of engagement data from Showpad into Salesforce, giving sales reps insights into which prospects are interacting with shared content, and tailoring follow-ups based on interest shown. + +- **Personalized Sales Training**: Use Pipedream to monitor performance data in Showpad and trigger personalized training content to be sent to sales reps based on their performance. For example, if a rep is struggling with a particular product pitch, automatically provide them with specific training materials to improve their skills. diff --git a/components/shutterstock/package.json b/components/shutterstock/package.json new file mode 100644 index 0000000000000..a073500c93e4c --- /dev/null +++ b/components/shutterstock/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/shutterstock", + "version": "0.0.1", + "description": "Pipedream Shutterstock Components", + "main": "shutterstock.app.mjs", + "keywords": [ + "pipedream", + "shutterstock" + ], + "homepage": "https://pipedream.com/apps/shutterstock", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/shutterstock/shutterstock.app.mjs b/components/shutterstock/shutterstock.app.mjs new file mode 100644 index 0000000000000..b5f44e95591fb --- /dev/null +++ b/components/shutterstock/shutterstock.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "shutterstock", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/sidetracker/package.json b/components/sidetracker/package.json new file mode 100644 index 0000000000000..58aaff1bf6b13 --- /dev/null +++ b/components/sidetracker/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/sidetracker", + "version": "0.0.1", + "description": "Pipedream Sidetracker Components", + "main": "sidetracker.app.mjs", + "keywords": [ + "pipedream", + "sidetracker" + ], + "homepage": "https://pipedream.com/apps/sidetracker", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/sidetracker/sidetracker.app.mjs b/components/sidetracker/sidetracker.app.mjs new file mode 100644 index 0000000000000..292a44105502c --- /dev/null +++ b/components/sidetracker/sidetracker.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "sidetracker", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/sierra_interactive/README.md b/components/sierra_interactive/README.md new file mode 100644 index 0000000000000..c1bc3c5b58f39 --- /dev/null +++ b/components/sierra_interactive/README.md @@ -0,0 +1,11 @@ +# Overview + +The Sierra Interactive API offers a suite of functions geared towards real estate professionals, enabling users to manage leads, property listings, and perform various CRM-related tasks. With Pipedream's serverless platform, you can build automated workflows that leverage the Sierra Interactive API to streamline your real estate business processes. Pipedream facilitates the integration of Sierra Interactive with numerous other apps, such as Google Sheets for data storage, Slack for notifications, or Mailchimp for email marketing, to create powerful, customized solutions. + +# Example Use Cases + +- **Lead Synchronization with Google Sheets**: Automatically sync new leads from Sierra Interactive to a Google Sheets spreadsheet. This workflow helps maintain an up-to-date list of leads for reporting and analysis without manual data entry. + +- **Instant Slack Notifications for New Property Listings**: Set up a workflow where Pipedream listens for new property listings in Sierra Interactive and immediately sends a notification with details to a designated Slack channel. This allows real estate teams to stay informed about new listings on-the-go. + +- **Automated Email Campaigns with Mailchimp**: Create a workflow that adds new leads from Sierra Interactive to a specific Mailchimp audience, triggering a pre-designed email campaign or sequence. This ensures prompt and personalized follow-ups with potential clients. diff --git a/components/sifter/README.md b/components/sifter/README.md index 8dee8531a162a..6060e0e5af817 100644 --- a/components/sifter/README.md +++ b/components/sifter/README.md @@ -1,17 +1,11 @@ # Overview -The Sifter API enables developers to build custom solutions that leverage data -from the Sifter bug tracking system. With the API, developers can create -actions and integrations that enables Sifter to interact with other -applications, making Sifter even more powerful and customizable. +The Sifter API enables developers to craft workflows for tracking and managing customer interactions and support inquiries efficiently. By leveraging the Sifter API on Pipedream, users can automate issue tracking, streamline customer communication, and ensure quick resolution of support tickets. With Pipedream's ability to connect to hundreds of apps, you can create multi-step workflows that trigger actions in other tools, leading to increased productivity and a better support experience. -The following are some examples of what can be built using the Sifter API: +# Example Use Cases -- Custom dashboards that allow users to view and interact with Sifter data. -- Automated solutions to create, modify, and close Sifter issues without manual - intervention. -- Browser plug-ins that allow for auto-complete when entering Sifter data. -- Mobile apps that allow users to work with Sifter on the go. -- Add-ons that help make Sifter more accessible to users with disabilities. -- Custom tools that facilitate data import and export between Sifter and other - applications. +- **Support Ticket Triaging**: Automatically classify and assign new support tickets from Sifter to the appropriate team or team member within your organization. Use sentiment analysis to prioritize urgent issues and notify the team via Slack or email. + +- **Customer Feedback Loop**: When a ticket is marked as resolved in Sifter, trigger a workflow to follow up with the customer via email. Include a survey link to gather feedback, and post the responses to a Google Sheet for analysis. + +- **Issue Resolution Tracking**: Set up a workflow that updates a project management tool like Trello or Asana whenever an issue status changes in Sifter. Create a card for new issues, move it to the appropriate list based on status, and notify project stakeholders. diff --git a/components/sigma/README.md b/components/sigma/README.md new file mode 100644 index 0000000000000..b5f82cc9d3e51 --- /dev/null +++ b/components/sigma/README.md @@ -0,0 +1,11 @@ +# Overview + +The Sigma API allows users to automate and integrate their business intelligence directly from Sigma Computing into other services and workflows. Sigma is a powerful tool for creating live, actionable dashboards and reports from cloud data warehouses. With Pipedream, users can harness this capability to trigger workflows based on Sigma events, manipulate and analyze Sigma data, and synchronize insights across other apps, enhancing data-driven decision-making processes. + +# Example Use Cases + +- **Automatically Distribute Sigma Reports**: Set up a workflow on Pipedream where Sigma reports are automatically fetched and emailed to stakeholders at regular intervals or in response to specific data changes. This can be integrated with email platforms like SendGrid to streamline communication. + +- **Real-time Slack Notifications for Sigma Dashboard Updates**: Create a Pipedream workflow that monitors changes in Sigma dashboards or reports and sends notifications through Slack. This helps teams stay updated on the latest insights without constantly checking Sigma. + +- **Sync Sigma Data with Google Sheets for Further Analysis**: Use Pipedream to develop a workflow where data from Sigma is automatically extracted and pushed into Google Sheets. This allows for additional data manipulation or sharing within teams who prefer accessing data through Google Sheets. diff --git a/components/sigma/actions/create-team/create-team.mjs b/components/sigma/actions/create-team/create-team.mjs new file mode 100644 index 0000000000000..5b5ca217f08c5 --- /dev/null +++ b/components/sigma/actions/create-team/create-team.mjs @@ -0,0 +1,83 @@ +import app from "../../sigma.app.mjs"; + +export default { + key: "sigma-create-team", + name: "Create Team", + description: "Create a team. [See the documentation](https://docs.sigmacomputing.com/#post-/v2/teams)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + description: "The name for the new team.", + propDefinition: [ + app, + "name", + ], + }, + description: { + description: "A description for the team.", + propDefinition: [ + app, + "description", + ], + }, + members: { + type: "string[]", + label: "Member IDs", + description: "List of member IDs to add to the team.", + propDefinition: [ + app, + "memberId", + ], + }, + createTeamFolder: { + type: "boolean", + label: "Create Team Folder", + description: "Whether to create a folder for the team.", + optional: true, + }, + visibility: { + type: "string", + label: "Visibility", + description: "The visibility setting for the team.", + options: [ + "public", + "private", + ], + optional: true, + }, + }, + methods: { + createTeam(args = {}) { + return this.app.post({ + path: "/teams", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createTeam, + name, + description, + members, + createTeamFolder, + visibility, + } = this; + + const response = await createTeam({ + $, + data: { + name, + description, + members, + createTeamFolder, + visibility, + }, + }); + + $.export("$summary", `Successfully created team with ID \`${response.teamId}\``); + return response; + }, +}; diff --git a/components/sigma/actions/create-workbook/create-workbook.mjs b/components/sigma/actions/create-workbook/create-workbook.mjs new file mode 100644 index 0000000000000..1da80b35d2af1 --- /dev/null +++ b/components/sigma/actions/create-workbook/create-workbook.mjs @@ -0,0 +1,69 @@ +import app from "../../sigma.app.mjs"; + +export default { + key: "sigma-create-workbook", + name: "Create Workbook", + description: "Creates a new blank workbook. [See the documentation](https://docs.sigmacomputing.com/#post-/v2/workbooks)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + description: "The name for the new workbook.", + propDefinition: [ + app, + "name", + ], + }, + folderId: { + description: "Folder where the workbook should be saved within the user's Sigma environment.", + propDefinition: [ + app, + "folderId", + ], + }, + description: { + description: "A description for the workbook.", + propDefinition: [ + app, + "description", + ], + }, + ownerId: { + description: "ID of the user (member) who will own the workbook.", + propDefinition: [ + app, + "memberId", + ], + }, + }, + methods: { + createWorkbook(args = {}) { + return this.app.post({ + path: "/workbooks", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createWorkbook, + name, + folderId, + description, + ownerId, + } = this; + + const response = await createWorkbook({ + $, + data: { + name, + folderId, + description, + ownerId, + }, + }); + $.export("$summary", `Successfully created workbook with ID \`${response.workbookId}\`.`); + return response; + }, +}; diff --git a/components/sigma/actions/list-datasets/list-datasets.mjs b/components/sigma/actions/list-datasets/list-datasets.mjs new file mode 100644 index 0000000000000..bd2f81175ae21 --- /dev/null +++ b/components/sigma/actions/list-datasets/list-datasets.mjs @@ -0,0 +1,36 @@ +import app from "../../sigma.app.mjs"; + +export default { + key: "sigma-list-datasets", + name: "List Datasets", + description: "Returns a list of available datasets. [See the documentation](https://docs.sigmacomputing.com/#get-/v2/datasets)", + version: "0.0.1", + type: "action", + props: { + app, + }, + methods: { + listDatasets(args = {}) { + return this.app._makeRequest({ + path: "/datasets", + ...args, + }); + }, + }, + async run({ $ }) { + const { + app, + listDatasets, + } = this; + + const response = await app.paginate({ + resourcesFn: listDatasets, + resourcesFnArgs: { + $, + }, + resourceName: "entries", + }); + $.export("$summary", `Successfully listed \`${response.length}\` dataset(s)`); + return response; + }, +}; diff --git a/components/sigma/common/constants.mjs b/components/sigma/common/constants.mjs new file mode 100644 index 0000000000000..4bb6cb7fea164 --- /dev/null +++ b/components/sigma/common/constants.mjs @@ -0,0 +1,9 @@ +const VERSION_PATH = "/v2"; +const DEFAULT_LIMIT = 50; +const DEFAULT_MAX = 600; + +export default { + VERSION_PATH, + DEFAULT_LIMIT, + DEFAULT_MAX, +}; diff --git a/components/sigma/common/utils.mjs b/components/sigma/common/utils.mjs new file mode 100644 index 0000000000000..903b2593ed3c2 --- /dev/null +++ b/components/sigma/common/utils.mjs @@ -0,0 +1,11 @@ +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +export default { + iterate, +}; diff --git a/components/sigma/package.json b/components/sigma/package.json new file mode 100644 index 0000000000000..c1c14bdd4ed3e --- /dev/null +++ b/components/sigma/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/sigma", + "version": "0.1.0", + "description": "Pipedream Sigma Components", + "main": "sigma.app.mjs", + "keywords": [ + "pipedream", + "sigma" + ], + "homepage": "https://pipedream.com/apps/sigma", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "1.6.5" + } +} diff --git a/components/sigma/sigma.app.mjs b/components/sigma/sigma.app.mjs new file mode 100644 index 0000000000000..9cc4ad318bf46 --- /dev/null +++ b/components/sigma/sigma.app.mjs @@ -0,0 +1,152 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; + +export default { + type: "app", + app: "sigma", + propDefinitions: { + name: { + type: "string", + label: "Name", + description: "The name for the new item.", + }, + folderId: { + type: "string", + label: "Folder ID", + description: "ID of the folder", + async options({ page }) { + const { entries } = await this.listFiles({ + params: { + typeFilters: [ + "folder", + ], + limit: constants.DEFAULT_LIMIT, + page, + }, + }); + return entries.map(({ + id: value, path, name, + }) => ({ + label: `${path}/${name}`, + value, + })); + }, + }, + description: { + type: "string", + label: "Description", + description: "A description for the item.", + optional: true, + }, + memberId: { + type: "string", + label: "Member ID", + description: "The ID of the member.", + optional: true, + async options({ page }) { + const { entries } = await this.listMembers({ + params: { + limit: constants.DEFAULT_LIMIT, + page, + }, + }); + return entries.map(({ + memberId: value, firstName, lastName, email, + }) => ({ + value, + label: [ + firstName, + lastName, + email, + ].filter(Boolean) + .join(" "), + })); + }, + }, + }, + methods: { + getUrl(path) { + return `${this.$auth.server}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + listFiles(args = {}) { + return this._makeRequest({ + path: "/files", + ...args, + }); + }, + listMembers(args = {}) { + return this._makeRequest({ + path: "/members", + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, resourceName, + max = constants.DEFAULT_MAX, + }) { + let page = 0; + let resourcesCount = 0; + + while (true) { + const response = + await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + limit: constants.DEFAULT_LIMIT, + page, + }, + }); + + const nextResources = resourceName && response[resourceName] || response; + + if (!nextResources?.length) { + console.log("No resources found"); + return; + } + + for (const resource of nextResources) { + yield resource; + resourcesCount += 1; + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + if (!response.hasMore) { + console.log("No more resources to fetch"); + return; + } + + page += 1; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); + }, + }, +}; diff --git a/components/signable/README.md b/components/signable/README.md new file mode 100644 index 0000000000000..7f8b87057c277 --- /dev/null +++ b/components/signable/README.md @@ -0,0 +1,11 @@ +# Overview + +The Signable API allows for the automation of electronic signature processes. Integrating this API into Pipedream workflows enables users to create, send, and manage documents that require signatures, streamlining the handling of contracts and agreements. By leveraging this API, you can automate notifications, sync data with other services, or initiate signature requests as part of larger automated business processes. + +# Example Use Cases + +- **Automated Document Workflow**: When a new document is uploaded to Google Drive, automatically create and send a signature request for it using Signable. Once signed, the document is then saved back to a designated Google Drive folder. + +- **Signature Status Tracking**: Create a workflow that listens for webhook events from Signable to track the status of signature requests. With each update, sync the status to a row in Google Sheets, allowing for a live dashboard of document statuses. + +- **Onboarding Process Automation**: For new employee onboarding, trigger a Pipedream workflow that sends out necessary employment documents via Signable when a new record is added to an HR system, such as BambooHR. Follow up with an email confirmation to the HR team once all documents are signed. diff --git a/components/signable/package.json b/components/signable/package.json index a1351f149940f..2eb56b2c0237e 100644 --- a/components/signable/package.json +++ b/components/signable/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/signalwire/README.md b/components/signalwire/README.md new file mode 100644 index 0000000000000..df275df0bfce4 --- /dev/null +++ b/components/signalwire/README.md @@ -0,0 +1,11 @@ +# Overview + +The SignalWire API provides powerful communication capabilities, allowing developers to send and receive text messages, make voice calls, and manage telephony features programmatically. On Pipedream, you can leverage these functionalities to create serverless workflows that integrate with numerous apps and services. With Pipedream's easy-to-use interface and robust connectivity options, you can build automated processes that trigger from various events and perform actions using SignalWire without managing infrastructure. + +# Example Use Cases + +- **SMS Notifications for New eCommerce Orders**: When a new order comes in on an eCommerce platform like Shopify, use SignalWire to send an SMS notification to the customer confirming their order and providing a tracking number. A Pipedream workflow can listen for new orders and then trigger the SignalWire API to send the message. + +- **Voice Alert System for Website Downtime**: Combine SignalWire with a monitoring service like UptimeRobot. When your website goes down, UptimeRobot triggers a Pipedream workflow that uses SignalWire to place a voice call to the website administrator, alerting them immediately to the issue for a faster response. + +- **Two-Factor Authentication for User Logins**: When a user attempts to login to your custom application, trigger a Pipedream workflow that sends a unique code via SMS through SignalWire. This workflow ensures an additional layer of security by verifying the user's identity with a code sent to their mobile device. diff --git a/components/signaturely/README.md b/components/signaturely/README.md new file mode 100644 index 0000000000000..f26f2ae08367f --- /dev/null +++ b/components/signaturely/README.md @@ -0,0 +1,11 @@ +# Overview + +Signaturely API lets you automate document signing processes within your apps. You can harness this functionality on Pipedream to create, send, and manage legally-binding signatures on documents without manual intervention. Whether you're sending contracts, onboarding new employees, or needing signatures from clients, integrating Signaturely with Pipedream can streamline your workflow, save time, and reduce errors. + +# Example Use Cases + +- **Automate Employee Onboarding**: Automatically send onboarding documents when a new employee is added to your HR system. Once the employee details are entered into your HR app, trigger a Pipedream workflow to send the necessary documents via Signaturely for e-signature. + +- **Contract Management**: When a new contract is created in your CRM, use this trigger to send the contract through Signaturely for signature. After the contract is signed, Pipedream can update the status in your CRM and store the signed document in a cloud storage service like Google Drive. + +- **Client Onboarding**: Initiate a seamless onboarding experience for your clients by sending all necessary paperwork through Signaturely once they sign up through your website. Integrate with form submission tools like Typeform on Pipedream to detect new submissions and trigger the document signing process. diff --git a/components/signaturit/.gitignore b/components/signaturit/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/signaturit/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/signaturit/README.md b/components/signaturit/README.md new file mode 100644 index 0000000000000..f43aba1962445 --- /dev/null +++ b/components/signaturit/README.md @@ -0,0 +1,11 @@ +# Overview + +The Signaturit API enables automated document signing, tracking, and management directly within Pipedream. By integrating this API, users can create workflows to send documents for signature, check the status of sent documents, and receive updates when documents are signed or declined, all from a serverless platform. By leveraging Pipedream's capacity for integrating various APIs and services, users can seamlessly include e-signature capabilities into their business processes without manual intervention. + +# Example Use Cases + +- **Automated Contract Workflow**: When a new client is added to a CRM like Salesforce, automatically trigger a workflow in Pipedream to send a contract for signature via Signaturit. Once signed, the contract can be saved to a cloud storage service such as Google Drive and the CRM record updated. + +- **HR Onboarding Process**: Automate the employee onboarding process by triggering an onboarding workflow in Pipedream when a new employee is added to an HR platform like BambooHR. Send employment documents through Signaturit for e-signature and store signed documents in a secure location, notifying HR upon completion. + +- **Real Estate Closing Documents**: Set up a Pipedream workflow to monitor a project management tool like Trello for a card move that indicates a property sale is finalizing. Automatically send closing documents to the buyer for signature through Signaturit and notify the real estate agent upon document completion. diff --git a/components/signaturit/actions/create-certified-email/create-certified-email.mjs b/components/signaturit/actions/create-certified-email/create-certified-email.mjs new file mode 100644 index 0000000000000..8bedaa6cdf933 --- /dev/null +++ b/components/signaturit/actions/create-certified-email/create-certified-email.mjs @@ -0,0 +1,116 @@ +import FormData from "form-data"; +import fs from "fs"; +import { TYPE_OPTIONS } from "../../common/constants.mjs"; +import { + checkTmp, + parseObject, +} from "../../common/utils.mjs"; +import signaturit from "../../signaturit.app.mjs"; + +export default { + key: "signaturit-create-certified-email", + name: "Create Certified Email", + description: "Initiates the creation of a certified email. [See the documentation](https://docs.signaturit.com/api/latest#emails_create_email)", + version: "0.0.1", + type: "action", + props: { + signaturit, + body: { + propDefinition: [ + signaturit, + "body", + ], + optional: true, + }, + brandingId: { + propDefinition: [ + signaturit, + "brandingId", + ], + optional: true, + }, + eventsUrl: { + propDefinition: [ + signaturit, + "eventsUrl", + ], + optional: true, + }, + data: { + propDefinition: [ + signaturit, + "data", + ], + optional: true, + }, + attachments: { + propDefinition: [ + signaturit, + "attachments", + ], + optional: true, + }, + recipients: { + propDefinition: [ + signaturit, + "recipients", + ], + }, + type: { + type: "string", + label: "Type", + description: "Type of certified email.", + options: TYPE_OPTIONS, + optional: true, + }, + subject: { + propDefinition: [ + signaturit, + "subject", + ], + optional: true, + }, + }, + async run({ $ }) { + const formData = new FormData(); + + let i = 0; + for (const recipient of parseObject(this.recipients)) { + for (const [ + key, + value, + ] of Object.entries(recipient)) { + formData.append(`recipients[${i}][${key}]`, value); + } + i++; + } + if (this.data) { + for (const [ + key, + value, + ] of Object.entries(this.data)) { + formData.append(`data[${key}]`, value); + } + i++; + } + if (this.body) formData.append("body", this.body); + if (this.brandingId) formData.append("branding_id", this.brandingId); + if (this.eventsUrl) formData.append("events_url", this.eventsUrl); + if (this.type) formData.append("type", this.type); + if (this.subject) formData.append("subject", this.subject); + + if (this.attachments) { + let j = 0; + for (const file of parseObject(this.attachments)) { + formData.append(`attachments[${j++}]`, fs.createReadStream(checkTmp(file))); + } + } + const response = await this.signaturit.createCertifiedEmail({ + $, + data: formData, + headers: formData.getHeaders(), + }); + $.export("$summary", `Created certified email with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/signaturit/actions/create-signature-request-from-template/create-signature-request-from-template.mjs b/components/signaturit/actions/create-signature-request-from-template/create-signature-request-from-template.mjs new file mode 100644 index 0000000000000..11198fe5734ea --- /dev/null +++ b/components/signaturit/actions/create-signature-request-from-template/create-signature-request-from-template.mjs @@ -0,0 +1,227 @@ +import FormData from "form-data"; +import fs from "fs"; +import { + DELIVERY_TYPE_OPTIONS, + SIGNATURE_TYPE_OPTIONS, + SIGNING_MODE_OPTIONS, +} from "../../common/constants.mjs"; +import { + checkTmp, + parseObject, +} from "../../common/utils.mjs"; +import signaturit from "../../signaturit.app.mjs"; + +export default { + key: "signaturit-create-signature-request-from-template", + name: "Create Signature Request from Template", + description: "Creates a signature request using a pre-existing template. [See the documentation](https://docs.signaturit.com/api/latest#signatures_create_signature)", + version: "0.0.1", + type: "action", + props: { + signaturit, + body: { + propDefinition: [ + signaturit, + "body", + ], + description: "Email body (html code is allowed) for **email** and **sms** type requests. Note: For **sms** request types it will be truncated to 120 characters Note: For **sms** the body should contain the tag **{{url}}** where we will include the document url.", + optional: true, + }, + brandingId: { + propDefinition: [ + signaturit, + "brandingId", + ], + optional: true, + }, + callbackUrl: { + type: "string", + label: "Callback URL", + description: "Url to redirect the user when finish the signature process.", + optional: true, + }, + data: { + propDefinition: [ + signaturit, + "data", + ], + optional: true, + }, + deliveryType: { + type: "string", + label: "Delivery Type", + description: "Delivery type for signature request.", + options: DELIVERY_TYPE_OPTIONS, + optional: true, + }, + expireTime: { + type: "integer", + label: "Expiration Time (days)", + description: "Expiration time of the document in days (max 365).", + min: 1, + max: 365, + optional: true, + }, + eventsUrl: { + propDefinition: [ + signaturit, + "eventsUrl", + ], + optional: true, + }, + files: { + propDefinition: [ + signaturit, + "attachments", + ], + label: "Files", + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "Name assigned to the signature request.", + }, + recipients: { + propDefinition: [ + signaturit, + "recipients", + ], + description: "List of recipients in JSON format, e.g., '{\"name\": \"John Doe\", \"email\": \"john@example.com\", \"phone\": \"34555667788\"}'. [See the documentation](https://docs.signaturit.com/api/latest#signatures_create_signature) for further information.", + }, + cc: { + type: "string[]", + label: "CC", + description: "List with email recipients containing name and email for people that will receive a copy of the signed document when the process is completed. e.g., '{\"name\": \"John Doe\", \"email\": \"john@example.com\"}'.", + optional: true, + }, + replyTo: { + type: "string", + label: "Reply To", + description: "Email Reply-To address.", + optional: true, + }, + reminders: { + type: "string[]", + label: "Reminders (days)", + description: "Reminders in days to send automatic reminders. You can set it 0 to disable reminders. E.g. [1,3,4]", + optional: true, + }, + signingMode: { + type: "string", + label: "Signing Mode", + description: "The signing mode lets you control the order in which your recipients receive and sign your documents.", + options: SIGNING_MODE_OPTIONS, + optional: true, + }, + subject: { + propDefinition: [ + signaturit, + "subject", + ], + optional: true, + }, + templates: { + propDefinition: [ + signaturit, + "templates", + ], + }, + type: { + type: "string", + label: "Type", + description: "The type of the signature.", + options: SIGNATURE_TYPE_OPTIONS, + optional: true, + }, + }, + methods: { + transformArray({ + arr, prefixBase, formData, + }) { + let result = []; + function processObject(obj, prefix = "") { + for (let key in obj) { + if (Array.isArray(obj[key])) { + obj[key].forEach((item, index) => { + processObject(item, `${prefix}[${key}][${index}]`); + }); + } else if (typeof obj[key] === "object") { + processObject(obj[key], `${prefix}[${key}]`); + } else { + result.push({ + key: `${prefixBase}${prefix}[${key}]`, + value: `${obj[key]}`, + }); + } + } + } + arr.map((item, index) => { + processObject(item, `[${index}]`); + }); + for (const { + key, value, + } of result) { + formData.append(key, value); + } + return result; + }, + }, + async run({ $ }) { + const formData = new FormData(); + this.transformArray({ + arr: parseObject(this.recipients), + prefixBase: "recipients", + formData, + }); + if (this.cc) { + let j = 0; + for (const item of parseObject(this.cc)) { + formData.append(`cc[${j++}]`, item); + } + } + if (this.data) { + for (const [ + key, + value, + ] of Object.entries(parseObject(this.data))) { + formData.append(`data[${key}]`, value); + } + } + if (this.templates) { + let k = 0; + for (const templateId of parseObject(this.templates)) { + formData.append(`templates[${k++}]`, templateId); + } + } + if (this.reminders) { + let m = 0; + for (const reminder of parseObject(this.reminders)) { + formData.append(`reminders[${m++}]`, reminder); + } + } + if (this.body) formData.append("body", this.body); + if (this.brandingId) formData.append("branding_id", this.brandingId); + if (this.callbackUrl) formData.append("callback_url", this.callbackUrl); + if (this.deliveryType) formData.append("delivery_type", this.deliveryType); + if (this.expireTime) formData.append("expire_time", this.expireTime); + if (this.eventsUrl) formData.append("events_url", this.eventsUrl); + if (this.name) formData.append("name", this.name); + if (this.replyTo) formData.append("reply_to", this.replyTo); + if (this.type) formData.append("type", this.type); + + if (this.files) { + let k = 0; + for (const file of parseObject(this.files)) { + formData.append(`files[${k++}]`, fs.createReadStream(checkTmp(file))); + } + } + const response = await this.signaturit.createSignatureRequest({ + $, + data: formData, + headers: formData.getHeaders(), + }); + $.export("$summary", `Created signature request with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/signaturit/actions/send-signature-request-reminder/send-signature-request-reminder.mjs b/components/signaturit/actions/send-signature-request-reminder/send-signature-request-reminder.mjs new file mode 100644 index 0000000000000..7ce371d9838f7 --- /dev/null +++ b/components/signaturit/actions/send-signature-request-reminder/send-signature-request-reminder.mjs @@ -0,0 +1,26 @@ +import signaturit from "../../signaturit.app.mjs"; + +export default { + key: "signaturit-send-signature-request-reminder", + name: "Send Signature Request Reminder", + description: "Sends a reminder for a pending signature request. [See the documentation](https://docs.signaturit.com/api/latest#signatures_send_reminder)", + version: "0.0.1", + type: "action", + props: { + signaturit, + signatureRequestId: { + propDefinition: [ + signaturit, + "signatureRequestId", + ], + }, + }, + async run({ $ }) { + const response = await this.signaturit.sendReminder({ + $, + signatureRequestId: this.signatureRequestId, + }); + $.export("$summary", `Sent reminder for signature request ${this.signatureRequestId}`); + return response; + }, +}; diff --git a/components/signaturit/app/signaturit.app.ts b/components/signaturit/app/signaturit.app.ts deleted file mode 100644 index e2cef98c1f866..0000000000000 --- a/components/signaturit/app/signaturit.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "signaturit", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/signaturit/common/constants.mjs b/components/signaturit/common/constants.mjs new file mode 100644 index 0000000000000..4f6a575022b0b --- /dev/null +++ b/components/signaturit/common/constants.mjs @@ -0,0 +1,57 @@ +export const LIMIT = 100; + +export const TYPE_OPTIONS = [ + { + label: "Delivery - Send the email as it is certifying the delivery process.", + value: "delivery", + }, + { + label: "Open Document - Send a modified version of the email with a button that redirects the user to our platform to open the **PDF** attachments. With this method, you can track when the user opens the attached files. Note: This method only supports **PDF** documents to be attached.", + value: "open_document", + }, + { + label: "Open Every Document - This type works like the **Open Document** type but allows to track the opening of every **PDF** file in emails with multiple attachments.", + value: "open_every_document", + }, +]; + +export const DELIVERY_TYPE_OPTIONS = [ + { + label: "Email - The signature request is sent by email. This is the default behavior when no type is specified.", + value: "email", + }, + { + label: "SMS - The signature request is sent by SMS. You must include the **phone** in the **recipients** parameter.", + value: "sms", + }, + { + label: "URL - The signature request is not sent to the signer. Instead of this, the creation request will return a **url** parameter that you can open in the browser to complete the signature.", + value: "url", + }, +]; + +export const SIGNING_MODE_OPTIONS = [ + { + label: "Sequential - Each recipient receives the request once the previous recipient has completed their action.", + value: "sequential", + }, + { + label: "Parallel - All recipients receive the request in parallel.", + value: "parallel", + }, +]; + +export const SIGNATURE_TYPE_OPTIONS = [ + { + label: "Simple - A simple signature request is created.", + value: "simple", + }, + { + label: "Advanced - An advanced signature request is created. We capture the biometric information of the signer with the signature draw.", + value: "advanced", + }, + { + label: "Smart - The system creates different type of signature depending in the user device. A simple signature is created for desktop pcs and advanced signature is created for mobile and tablet devices.", + value: "smart", + }, +]; diff --git a/components/signaturit/common/utils.mjs b/components/signaturit/common/utils.mjs new file mode 100644 index 0000000000000..b75bc9340048a --- /dev/null +++ b/components/signaturit/common/utils.mjs @@ -0,0 +1,39 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; + +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; + +export const getDateFormatted = (dateStr) => { + const date = new Date(dateStr); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; +}; diff --git a/components/signaturit/package.json b/components/signaturit/package.json index 90b869197bb74..81bceb2a914a3 100644 --- a/components/signaturit/package.json +++ b/components/signaturit/package.json @@ -1,16 +1,20 @@ { "name": "@pipedream/signaturit", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Signaturit Components", - "main": "dist/app/signaturit.app.mjs", + "main": "signaturit.app.mjs", "keywords": [ "pipedream", "signaturit" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/signaturit", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "form-data": "^4.0.1", + "fs": "^0.0.1-security" } } diff --git a/components/signaturit/signaturit.app.mjs b/components/signaturit/signaturit.app.mjs new file mode 100644 index 0000000000000..7078264908002 --- /dev/null +++ b/components/signaturit/signaturit.app.mjs @@ -0,0 +1,172 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "signaturit", + propDefinitions: { + body: { + type: "string", + label: "Body", + description: "Email body (html code is allowed).", + }, + brandingId: { + type: "string", + label: "Branding ID", + description: "ID of the branding to use.", + async options() { + const data = await this.listBrandings(); + + return data.map(({ id }) => id); + }, + }, + eventsUrl: { + type: "string", + label: "Events URL", + description: "URL to receive real-time events.", + }, + data: { + type: "object", + label: "Custom Data", + description: "Custom key-value data in JSON format.", + }, + attachments: { + type: "string[]", + label: "Attachments", + description: "A list of paths to the files saved to the `/tmp` directory (e.g. `/tmp/example.pdf`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + }, + recipients: { + type: "string[]", + label: "Recipients", + description: "List of recipients in JSON format, e.g., '{\"name\": \"John Doe\", \"email\": \"john@example.com\"}'. [See the documentation](https://docs.signaturit.com/api/latest#emails_create_email) for further information.", + }, + subject: { + type: "string", + label: "Subject", + description: "Email subject for **email** type requests.", + }, + templates: { + type: "string[]", + label: "Templates", + description: "Templates to use in the signature request.", + async options({ page }) { + const data = await this.listTemplates({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + signatureRequestId: { + type: "string", + label: "Signature Request ID", + description: "ID of the signature request to send a reminder for.", + async options({ page }) { + const data = await this.listSignatureRequests({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + + return data.map(({ id }) => id); + }, + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.domain}.signaturit.com/v3`; + }, + _headers(headers = {}) { + return { + ...headers, + "Authorization": `Bearer ${this.$auth.access_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(headers), + ...opts, + }); + }, + listBrandings() { + return this._makeRequest({ + path: "/brandings.json", + }); + }, + listTemplates() { + return this._makeRequest({ + path: "/templates.json", + }); + }, + createCertifiedEmail(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/emails.json", + ...opts, + }); + }, + createSignatureRequest(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/signatures.json", + ...opts, + }); + }, + sendReminder({ + signatureRequestId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/signatures/${signatureRequestId}/reminder.json`, + ...opts, + }); + }, + listSignatureRequests(opts = {}) { + return this._makeRequest({ + method: "GET", + path: "/signatures.json", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.limit = LIMIT; + params.offset = LIMIT * page; + page++; + + const data = await fn({ + params, + ...opts, + }); + for (const d of data) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = data.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/signaturit/sources/common/base.mjs b/components/signaturit/sources/common/base.mjs new file mode 100644 index 0000000000000..97ced71101be7 --- /dev/null +++ b/components/signaturit/sources/common/base.mjs @@ -0,0 +1,62 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import { getDateFormatted } from "../../common/utils.mjs"; +import app from "../../signaturit.app.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + + const response = this.app.paginate({ + fn: this.app.listSignatureRequests, + params: { + status: "completed", + since: getDateFormatted(lastDate), + }, + maxResults, + }); + + let responseArray = []; + for await (const item of response) { + if (Date.parse(item.created_at) <= lastDate) break; + responseArray.push(item); + } + + if (responseArray.length) { + this._setLastDate(Date.parse(responseArray[0].created_at)); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: `New signed document: ${item.id}`, + ts: Date.parse(item.created_at), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/signaturit/sources/new-signed-document/new-signed-document.mjs b/components/signaturit/sources/new-signed-document/new-signed-document.mjs new file mode 100644 index 0000000000000..3b0e998e6c2c3 --- /dev/null +++ b/components/signaturit/sources/new-signed-document/new-signed-document.mjs @@ -0,0 +1,13 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "signaturit-new-signed-document", + name: "New Signed Document", + description: "Emit new event when a document has been newly signed.", + version: "0.0.1", + type: "source", + dedupe: "unique", + sampleEmit, +}; diff --git a/components/signaturit/sources/new-signed-document/test-event.mjs b/components/signaturit/sources/new-signed-document/test-event.mjs new file mode 100644 index 0000000000000..cf77f1539cd29 --- /dev/null +++ b/components/signaturit/sources/new-signed-document/test-event.mjs @@ -0,0 +1,24 @@ +export default { + created_at: "2014-08-21T08:53:35+0000", + data: [], + documents: [ + { + email: "john@signaturit.com", + events: [ + { + type: "email_processed", + created_at: "2014-08-21T08:55:14+0000" + } + ], + file: { + name: "document.pdf", + pages: 11, + size: 72218, + }, + id: "9781f42d-2910-11e4-b3d4-0aa7697eb409", + name: "John", + status: "ready", + } + ], + id: "974e6f6c-2910-11e4-b3d4-0aa7697eb409" +} \ No newline at end of file diff --git a/components/signerx/README.md b/components/signerx/README.md new file mode 100644 index 0000000000000..a34f8a084f486 --- /dev/null +++ b/components/signerx/README.md @@ -0,0 +1,11 @@ +# Overview + +The SignerX API enables automation of digital signing processes, offering a robust solution for managing document signatures electronically. Through Pipedream's integration capabilities, users can craft custom workflows that link SignerX with various other apps and services, enhancing efficiency and streamlining operations where signature collection and management are critical. This can include automated reminders for pending signatures, syncing signed documents to cloud storage, or updating records in CRM systems upon completion of signing. + +# Example Use Cases + +- **Automate Document Signing Requests with CRM Updates**: When a new contract is added to a CRM platform like Salesforce, trigger a Pipedream workflow to automatically send the document for signature via SignerX. Once signed, update the CRM record with the status of the document to keep all team members informed. + +- **Sync Signed Documents to Cloud Storage**: After a document is signed using SignerX, set up a workflow on Pipedream to automatically upload the signed version to a cloud storage service such as Google Drive or Dropbox. This helps in maintaining backups and ensuring compliance with data storage policies. + +- **Email Notifications on Signature Completion**: Configure a workflow where, upon the successful completion of a document signing in SignerX, an automated email notification is sent to relevant stakeholders using a service like SendGrid. This can inform them that the document has been signed and is ready for the next steps in the process. diff --git a/components/signerx/actions/send-template-signatures/send-template-signatures.mjs b/components/signerx/actions/send-template-signatures/send-template-signatures.mjs new file mode 100644 index 0000000000000..ee57e2134ba92 --- /dev/null +++ b/components/signerx/actions/send-template-signatures/send-template-signatures.mjs @@ -0,0 +1,47 @@ +import signerx from "../../signerx.app.mjs"; + +export default { + key: "signerx-send-template-signatures", + name: "Send Template Signatures", + description: "Add a recipient to a template and invite to sign. [See the documentation](https://documenter.getpostman.com/view/13877745/2sa3xv9kni)", + version: "0.0.1", + type: "action", + props: { + signerx, + packageId: { + propDefinition: [ + signerx, + "packageId", + ], + }, + email: { + type: "string", + label: "Email", + description: "The email address of the recipient", + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the recipient", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the recipient", + }, + }, + async run({ $ }) { + const response = await this.signerx.addRecipientToTemplate({ + $, + packageId: this.packageId, + data: { + id: this.packageId, + email: this.email, + first_name: this.firstName, + last_name: this.lastName, + }, + }); + $.export("$summary", `Successfully added recipient ${this.firstName} ${this.lastName} to package ${this.packageId}`); + return response; + }, +}; diff --git a/components/signerx/actions/upload-package/upload-package.mjs b/components/signerx/actions/upload-package/upload-package.mjs new file mode 100644 index 0000000000000..fcc41e86a6134 --- /dev/null +++ b/components/signerx/actions/upload-package/upload-package.mjs @@ -0,0 +1,63 @@ +import FormData from "form-data"; +import fs from "fs"; +import { checkTmp } from "../../common/utils.mjs"; +import signerx from "../../signerx.app.mjs"; + +export default { + key: "signerx-upload-package", + name: "Upload Package", + description: "Quickly create a draft for a new package/document by uploading a file or providing a file_url to a PDF document. [See the documentation](https://documenter.getpostman.com/view/13877745/2sa3xv9kni)", + version: "0.0.1", + type: "action", + props: { + signerx, + documentType: { + type: "string", + label: "Document Type", + description: "whether you hava the file or an URL to the file.", + options: [ + "file", + "file_url", + ], + reloadProps: true, + }, + name: { + type: "string", + label: "Name", + description: "The name of the package/document", + }, + }, + async additionalProps() { + const props = {}; + props.file = (this.documentType === "file") + ? { + type: "string", + label: "File", + description: "The path to the pdf file saved to the `/tmp` directory (e.g. `/tmp/example.pdf`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + } + : { + type: "string", + label: "File URL", + description: "The URL to the file.", + }; + return props; + }, + async run({ $ }) { + const data = new FormData(); + if (this.documentType === "file") { + data.append("file", fs.createReadStream(checkTmp(this.file))); + } else { + data.append("file_url", this.file); + } + data.append("name", this.name); + + const response = await this.signerx.createDraftPackage({ + $, + data, + headers: data.getHeaders(), + }); + + $.export("$summary", `Successfully created draft package with name "${this.name}"`); + return response; + }, +}; diff --git a/components/signerx/common/utils.mjs b/components/signerx/common/utils.mjs new file mode 100644 index 0000000000000..1a5e36f32a603 --- /dev/null +++ b/components/signerx/common/utils.mjs @@ -0,0 +1,6 @@ +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; diff --git a/components/signerx/package.json b/components/signerx/package.json new file mode 100644 index 0000000000000..0447ae74fa60a --- /dev/null +++ b/components/signerx/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/signerx", + "version": "0.1.0", + "description": "Pipedream SignerX Components", + "main": "signerx.app.mjs", + "keywords": [ + "pipedream", + "signerx" + ], + "homepage": "https://pipedream.com/apps/signerx", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0", + "form-data": "^4.0.0" + } +} diff --git a/components/signerx/signerx.app.mjs b/components/signerx/signerx.app.mjs new file mode 100644 index 0000000000000..c86f4d6048487 --- /dev/null +++ b/components/signerx/signerx.app.mjs @@ -0,0 +1,97 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "signerx", + propDefinitions: { + packageId: { + type: "string", + label: "Package ID", + description: "The ID of the package", + async options({ page }) { + const { data } = await this.listPackages({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.signerx.com"; + }, + _headers(headers = {}) { + return { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(headers), + ...opts, + }); + }, + listPackages(opts = {}) { + return this._makeRequest({ + path: "/packages", + ...opts, + }); + }, + addRecipientToTemplate({ + packageId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/packages/${packageId}/recipients`, + ...opts, + }); + }, + createDraftPackage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/packages/create-and-upload", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const { + data, + next_page_url, + } = await fn({ + params, + ...opts, + }); + for (const d of data) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = next_page_url; + + } while (hasMore); + }, + }, +}; diff --git a/components/signerx/sources/common/base.mjs b/components/signerx/sources/common/base.mjs new file mode 100644 index 0000000000000..de09c9c4dc759 --- /dev/null +++ b/components/signerx/sources/common/base.mjs @@ -0,0 +1,64 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import signerx from "../../signerx.app.mjs"; + +export default { + props: { + signerx, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01T00:00:00.000Z"; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + getParams() { + return {}; + }, + getDateField() { + return "created_at"; + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const dateField = this.getDateField(); + const response = this.signerx.paginate({ + fn: this.signerx.listPackages, + maxResults, + params: this.getParams(), + }); + + let responseArray = []; + for await (const item of response) { + if (Date.parse(item[dateField]) <= Date.parse(lastDate)) break; + responseArray.push(item); + } + + if (responseArray.length) { + this._setLastDate(responseArray[0][dateField]); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item[dateField]), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/signerx/sources/new-package-created/new-package-created.mjs b/components/signerx/sources/new-package-created/new-package-created.mjs new file mode 100644 index 0000000000000..9cc4276500fea --- /dev/null +++ b/components/signerx/sources/new-package-created/new-package-created.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "signerx-new-package-created", + name: "New Package Created", + description: "Emit new event when a package is newly created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getParams() { + return { + order_by: "created_at", + direction: "desc", + }; + }, + getSummary(item) { + return `New Package Created: ${item.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/signerx/sources/new-package-created/test-event.mjs b/components/signerx/sources/new-package-created/test-event.mjs new file mode 100644 index 0000000000000..b9bbdd25561e3 --- /dev/null +++ b/components/signerx/sources/new-package-created/test-event.mjs @@ -0,0 +1,92 @@ +export default { + "id": "cm06zfbgibqo8i5", + "name": "file test 03", + "status": { + "id": "cm06zfbgibqo8i5", + "name": "Draft", + "classification": "draft" + }, + "type": { + "id": "cm06zfbgibqo8i5", + "name": "Recipients", + "classification": "recipients" + }, + "created_by": { + "id": "cm06zfbgibqo8i5", + "first_name": "User", + "middle_name": null, + "last_name": "Surname", + "email": "user@email.com", + "email_domain": "", + "is_free_mail": false, + "timezone": "America/Mazatlan", + "phone_number": null, + "phone_number_confirmed": false, + "image_url": "https://preview-api.s3.us-west-1.wasabisys.com/users/clwplsuru0002hzpb7zt7rkuh/.png?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=XQR26WRFPGQK9RCPFILH%2F20240823%2Fus-west-1%2Fs3%2Faws4_request&X-Amz-Date=20240823T194726Z&X-Amz-SignedHeaders=host&X-Amz-Expires=300&X-Amz-Signature=d4c66141f31edc343e246176ff7fc47be246a1e3f36fccfd3631bb1cf416547c", + "user_image_preference": "uploaded", + "image_color": "#02D163", + "role": null, + "email_confirmed": true, + "created_at": "2024-05-27T23:32:33.000000Z", + "updated_at": "2024-08-23T17:25:27.000000Z", + "provider": null, + "meta": { + "current_signing_platform": "scanner-and-email", + "current_signing_platform_name": null, + "current_signing_platform_explanation": null, + "challenge_solving_with_approveme": null, + "signed_up_for_template": 0, + "seen_fieldmapping_intro_at": null, + "seen_fieldmapping_label_hint_at": null + }, + "last_login": "2024-08-23T16:44:35.000000Z", + "has_password": true, + "has_created_package": true, + "inbox_count": 0, + "total_packages_received": 0, + "total_packages_created": 3, + "total_templates_created": 0 + }, + "cancelled_at": null, + "account_name": "AccountName", + "recipients": [ + { + "id": "cm06zgpb04s52x5s", + "first_name": "Recipient", + "middle_name": "", + "last_name": "Surname", + "full_name": "Recipient Surname", + "email": "recipient@email.com", + "send_order": null, + "status": "not-viewed" + } + ], + "fields_preview": [], + "parent_id": null, + "sender_name": "Sender @ Name", + "completed_at": null, + "created_at": "2024-08-23T17:25:23.000000Z", + "published_at": null, + "account_id": "clwplsuryzpb8ldnczoq", + "folder_id": null, + "account_type": { + "name": "lite", + "packages_limit": 3, + "packages_count": 3, + "block_package_creation_until": "2024-08-31T00:00:00+00:00", + "templates_limit": 0, + "templates_count": 0, + "user_limit": 1, + "price_monthly": null, + "discounted_monthly": 0, + "price_yearly": null, + "discounted_yearly": 0, + "add_branding_template_cost": null, + "user_cost": null, + "template_cost": null, + "is_active": true, + "is_featured": true + }, + "account_color": "#00a9ff", + "account_image_url": null +} \ No newline at end of file diff --git a/components/signerx/sources/new-package-published/new-package-published.mjs b/components/signerx/sources/new-package-published/new-package-published.mjs new file mode 100644 index 0000000000000..69c99dbdc7ea5 --- /dev/null +++ b/components/signerx/sources/new-package-published/new-package-published.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "signerx-new-package-published", + name: "New Package Published", + description: "Emit new event when a package is published.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getDateField() { + return "published_at"; + }, + getParams() { + return { + direction: "desc", + status_ids: "published", + }; + }, + getSummary(item) { + return `New Package Published: ${item.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/signerx/sources/new-package-published/test-event.mjs b/components/signerx/sources/new-package-published/test-event.mjs new file mode 100644 index 0000000000000..a3915992975b1 --- /dev/null +++ b/components/signerx/sources/new-package-published/test-event.mjs @@ -0,0 +1,92 @@ +export default { + "id": "cm06zfbgibqo8i5", + "name": "file test 03", + "status": { + "id": "cm06zfbgibqo8i5", + "name": "Draft", + "classification": "draft" + }, + "type": { + "id": "cm06zfbgibqo8i5", + "name": "Recipients", + "classification": "recipients" + }, + "created_by": { + "id": "cm06zfbgibqo8i5", + "first_name": "User", + "middle_name": null, + "last_name": "Surname", + "email": "user@email.com", + "email_domain": "", + "is_free_mail": false, + "timezone": "America/Mazatlan", + "phone_number": null, + "phone_number_confirmed": false, + "image_url": "https://preview-api.s3.us-west-1.wasabisys.com/users/clwplsuru0002hzpb7zt7rkuh/.png?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=XQR26WRFPGQK9RCPFILH%2F20240823%2Fus-west-1%2Fs3%2Faws4_request&X-Amz-Date=20240823T194726Z&X-Amz-SignedHeaders=host&X-Amz-Expires=300&X-Amz-Signature=d4c66141f31edc343e246176ff7fc47be246a1e3f36fccfd3631bb1cf416547c", + "user_image_preference": "uploaded", + "image_color": "#02D163", + "role": null, + "email_confirmed": true, + "created_at": "2024-05-27T23:32:33.000000Z", + "updated_at": "2024-08-23T17:25:27.000000Z", + "provider": null, + "meta": { + "current_signing_platform": "scanner-and-email", + "current_signing_platform_name": null, + "current_signing_platform_explanation": null, + "challenge_solving_with_approveme": null, + "signed_up_for_template": 0, + "seen_fieldmapping_intro_at": null, + "seen_fieldmapping_label_hint_at": null + }, + "last_login": "2024-08-23T16:44:35.000000Z", + "has_password": true, + "has_created_package": true, + "inbox_count": 0, + "total_packages_received": 0, + "total_packages_created": 3, + "total_templates_created": 0 + }, + "cancelled_at": null, + "account_name": "AccountName", + "recipients": [ + { + "id": "cm06zgpb04s52x5s", + "first_name": "Recipient", + "middle_name": "", + "last_name": "Surname", + "full_name": "Recipient Surname", + "email": "recipient@email.com", + "send_order": null, + "status": "not-viewed" + } + ], + "fields_preview": [], + "parent_id": null, + "sender_name": "Sender @ Name", + "completed_at": null, + "created_at": "2024-08-23T17:25:23.000000Z", + "published_at": "2024-08-23T17:25:23.000000Z", + "account_id": "clwplsuryzpb8ldnczoq", + "folder_id": null, + "account_type": { + "name": "lite", + "packages_limit": 3, + "packages_count": 3, + "block_package_creation_until": "2024-08-31T00:00:00+00:00", + "templates_limit": 0, + "templates_count": 0, + "user_limit": 1, + "price_monthly": null, + "discounted_monthly": 0, + "price_yearly": null, + "discounted_yearly": 0, + "add_branding_template_cost": null, + "user_cost": null, + "template_cost": null, + "is_active": true, + "is_featured": true + }, + "account_color": "#00a9ff", + "account_image_url": null +} \ No newline at end of file diff --git a/components/signerx/sources/new-package-signed/new-package-signed.mjs b/components/signerx/sources/new-package-signed/new-package-signed.mjs new file mode 100644 index 0000000000000..3e86527a9c48f --- /dev/null +++ b/components/signerx/sources/new-package-signed/new-package-signed.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "signerx-new-package-signed", + name: "New Package Signed", + description: "Emit new event when a package has been signed.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getDateField() { + return "completed_at"; + }, + getParams() { + return { + direction: "desc", + status_ids: "complete", + }; + }, + getSummary(item) { + return `New Package Signed: ${item.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/signerx/sources/new-package-signed/test-event.mjs b/components/signerx/sources/new-package-signed/test-event.mjs new file mode 100644 index 0000000000000..3a2281d1b2a0e --- /dev/null +++ b/components/signerx/sources/new-package-signed/test-event.mjs @@ -0,0 +1,92 @@ +export default { + "id": "cm06zfbgibqo8i5", + "name": "file test 03", + "status": { + "id": "cm06zfbgibqo8i5", + "name": "Draft", + "classification": "draft" + }, + "type": { + "id": "cm06zfbgibqo8i5", + "name": "Recipients", + "classification": "recipients" + }, + "created_by": { + "id": "cm06zfbgibqo8i5", + "first_name": "User", + "middle_name": null, + "last_name": "Surname", + "email": "user@email.com", + "email_domain": "", + "is_free_mail": false, + "timezone": "America/Mazatlan", + "phone_number": null, + "phone_number_confirmed": false, + "image_url": "https://preview-api.s3.us-west-1.wasabisys.com/users/clwplsuru0002hzpb7zt7rkuh/.png?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=XQR26WRFPGQK9RCPFILH%2F20240823%2Fus-west-1%2Fs3%2Faws4_request&X-Amz-Date=20240823T194726Z&X-Amz-SignedHeaders=host&X-Amz-Expires=300&X-Amz-Signature=d4c66141f31edc343e246176ff7fc47be246a1e3f36fccfd3631bb1cf416547c", + "user_image_preference": "uploaded", + "image_color": "#02D163", + "role": null, + "email_confirmed": true, + "created_at": "2024-05-27T23:32:33.000000Z", + "updated_at": "2024-08-23T17:25:27.000000Z", + "provider": null, + "meta": { + "current_signing_platform": "scanner-and-email", + "current_signing_platform_name": null, + "current_signing_platform_explanation": null, + "challenge_solving_with_approveme": null, + "signed_up_for_template": 0, + "seen_fieldmapping_intro_at": null, + "seen_fieldmapping_label_hint_at": null + }, + "last_login": "2024-08-23T16:44:35.000000Z", + "has_password": true, + "has_created_package": true, + "inbox_count": 0, + "total_packages_received": 0, + "total_packages_created": 3, + "total_templates_created": 0 + }, + "cancelled_at": null, + "account_name": "AccountName", + "recipients": [ + { + "id": "cm06zgpb04s52x5s", + "first_name": "Recipient", + "middle_name": "", + "last_name": "Surname", + "full_name": "Recipient Surname", + "email": "recipient@email.com", + "send_order": null, + "status": "not-viewed" + } + ], + "fields_preview": [], + "parent_id": null, + "sender_name": "Sender @ Name", + "published_at": "2024-08-23T17:25:23.000000Z", + "created_at": "2024-08-23T17:25:23.000000Z", + "published_at": "2024-08-23T17:25:23.000000Z", + "account_id": "clwplsuryzpb8ldnczoq", + "folder_id": null, + "account_type": { + "name": "lite", + "packages_limit": 3, + "packages_count": 3, + "block_package_creation_until": "2024-08-31T00:00:00+00:00", + "templates_limit": 0, + "templates_count": 0, + "user_limit": 1, + "price_monthly": null, + "discounted_monthly": 0, + "price_yearly": null, + "discounted_yearly": 0, + "add_branding_template_cost": null, + "user_cost": null, + "template_cost": null, + "is_active": true, + "is_featured": true + }, + "account_color": "#00a9ff", + "account_image_url": null +} \ No newline at end of file diff --git a/components/signnow/actions/create-document-from-template/create-document-from-template.mjs b/components/signnow/actions/create-document-from-template/create-document-from-template.mjs new file mode 100644 index 0000000000000..4fd0cd68ebf51 --- /dev/null +++ b/components/signnow/actions/create-document-from-template/create-document-from-template.mjs @@ -0,0 +1,51 @@ +import app from "../../signnow.app.mjs"; + +export default { + key: "signnow-create-document-from-template", + name: "Create Document From Template", + description: "Creates a new document copy out of a template. [See the documentation](https://docs.signnow.com/docs/signnow/template/operations/create-a-template-copy)", + version: "0.0.1", + type: "action", + props: { + app, + templateId: { + propDefinition: [ + app, + "templateId", + ], + }, + documentName: { + propDefinition: [ + app, + "documentName", + ], + }, + }, + methods: { + createDocumentFromTemplate({ + templateId, ...args + }) { + return this.app.post({ + path: `/template/${templateId}/copy`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + createDocumentFromTemplate, + templateId, + documentName, + } = this; + + const response = await createDocumentFromTemplate({ + $, + templateId, + data: { + document_name: documentName, + }, + }); + $.export("$summary", `Successfully created a document from template with ID: \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/signnow/actions/prefill-text-fields/prefill-text-fields.mjs b/components/signnow/actions/prefill-text-fields/prefill-text-fields.mjs new file mode 100644 index 0000000000000..dca975b712f23 --- /dev/null +++ b/components/signnow/actions/prefill-text-fields/prefill-text-fields.mjs @@ -0,0 +1,98 @@ +import app from "../../signnow.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "signnow-prefill-text-fields", + name: "Prefill Text Fields", + description: "Adds values to fields that the signers can later edit when they receive the document for signature. [See the documentation](https://docs.signnow.com/docs/signnow/document/operations/update-a-v-2-document-prefill-text)", + version: "0.0.1", + type: "action", + props: { + app, + documentId: { + propDefinition: [ + app, + "documentId", + ], + }, + numberOfFields: { + type: "integer", + label: "Number Of Fields", + description: "The number of fields to generate. Defaults to 1.", + default: 1, + reloadProps: true, + }, + }, + methods: { + fieldsPropsMapper(prefix) { + const { + [`${prefix}FieldName`]: fieldName, + [`${prefix}PrefilledText`]: prefilledText, + } = this; + + return { + field_name: fieldName, + prefilled_text: prefilledText, + }; + }, + getFieldsPropDefinitions({ + prefix, label, + } = {}) { + return { + [`${prefix}FieldName`]: { + type: "string", + label: `${label} - Field Name`, + description: "Name of the field", + }, + [`${prefix}PrefilledText`]: { + type: "string", + label: `${label} - Prefilled Text`, + description: "Text to prefill in the field", + }, + }; + }, + prefillTextFields({ + documentId, ...args + } = {}) { + return this.app.put({ + path: `/v2/documents/${documentId}/prefill-texts`, + ...args, + }); + }, + }, + additionalProps() { + const { + numberOfFields, + getFieldsPropDefinitions, + } = this; + + return utils.getAdditionalProps({ + numberOfFields, + fieldName: "field", + getPropDefinitions: getFieldsPropDefinitions, + }); + }, + async run({ $ }) { + const { + prefillTextFields, + documentId, + numberOfFields, + fieldsPropsMapper, + } = this; + + const response = await prefillTextFields({ + $, + documentId, + data: { + fields: utils.getFieldsProps({ + numberOfFields, + fieldName: "field", + propsMapper: fieldsPropsMapper, + }), + }, + }); + + $.export("$summary", "Successfully pre-filled text fields in the document"); + return response; + }, +}; diff --git a/components/signnow/actions/send-field-invite/send-field-invite.mjs b/components/signnow/actions/send-field-invite/send-field-invite.mjs new file mode 100644 index 0000000000000..eed77877093d5 --- /dev/null +++ b/components/signnow/actions/send-field-invite/send-field-invite.mjs @@ -0,0 +1,83 @@ +import app from "../../signnow.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "signnow-send-field-invite", + name: "Send Field Invite", + description: "Creates and sends a field invite to sign a document. [See the documentation](https://docs.signnow.com/docs/signnow/field-invite/operations/create-a-document-invite)", + version: "0.0.1", + type: "action", + props: { + app, + documentId: { + propDefinition: [ + app, + "documentId", + ], + }, + to: { + type: "string[]", + label: "To", + description: "Email addresses and settings for all recipients. Each row should be represented as a JSON object with the following required properties as an example: `{\"email\": \"test1@example.com\", \"role\": \"Recipient 1\"}`", + }, + from: { + type: "string", + label: "From", + description: "Sender's email address: you can use only the email address associated with your signNow account (login email) as `from` address", + }, + cc: { + type: "string[]", + label: "CC", + description: "Email addresses for CC recipients", + optional: true, + }, + subject: { + type: "string", + label: "Subject", + description: "Email subject for all signers.", + optional: true, + }, + message: { + type: "string", + label: "Message", + description: "Email message for all signers.", + optional: true, + }, + }, + methods: { + sendFieldInvite({ + documentId, ...args + } = {}) { + return this.app.post({ + path: `/document/${documentId}/invite`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + sendFieldInvite, + documentId, + to, + from, + cc, + subject, + message, + } = this; + + const response = await sendFieldInvite({ + $, + documentId, + data: { + to: utils.parseArray(to), + from, + cc: utils.parseArray(cc), + subject, + message, + }, + }); + + $.export("$summary", "Successfully sent field invite to sign the document."); + return response; + }, +}; diff --git a/components/signnow/actions/upload-document-with-tags/upload-document-with-tags.mjs b/components/signnow/actions/upload-document-with-tags/upload-document-with-tags.mjs new file mode 100644 index 0000000000000..4928b889f93ad --- /dev/null +++ b/components/signnow/actions/upload-document-with-tags/upload-document-with-tags.mjs @@ -0,0 +1,50 @@ +import fs from "fs"; +import FormData from "form-data"; +import app from "../../signnow.app.mjs"; + +export default { + key: "signnow-upload-document-with-tags", + name: "Upload Document With Tags", + description: "Uploads a file that contains SignNow text tags. [See the documentation](https://docs.signnow.com/docs/signnow/document/operations/create-a-document-fieldextract)", + version: "0.0.1", + type: "action", + props: { + app, + file: { + type: "string", + label: "File Path", + description: "File path of a file previously downloaded in Pipedream E.g. (`/tmp/my-file.pdf`). [Download a file to the `/tmp` directory](https://pipedream.com/docs/code/nodejs/http-requests/#download-a-file-to-the-tmp-directory)", + }, + }, + methods: { + uploadDocumentWithTags({ + data, ...args + } = {}) { + return this.app.post({ + path: "/document/fieldextract", + headers: { + "Content-Type": `multipart/form-data; boundary=${data._boundary}`, + }, + data, + ...args, + }); + }, + }, + async run({ $ }) { + const { + uploadDocumentWithTags, + file, + } = this; + + const data = new FormData(); + data.append("file", fs.createReadStream(file)); + + const response = await uploadDocumentWithTags({ + $, + data, + }); + + $.export("$summary", `Document with tags uploaded successfully with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/signnow/common/constants.mjs b/components/signnow/common/constants.mjs new file mode 100644 index 0000000000000..05da15f9c8edb --- /dev/null +++ b/components/signnow/common/constants.mjs @@ -0,0 +1,11 @@ +const BASE_URL = "https://api.signnow.com"; +const WEBHOOK_ID = "webhookId"; +const SECRET_KEY = "secretKey"; +const SEP = "_"; + +export default { + BASE_URL, + WEBHOOK_ID, + SECRET_KEY, + SEP, +}; diff --git a/components/signnow/common/utils.mjs b/components/signnow/common/utils.mjs new file mode 100644 index 0000000000000..735b891329112 --- /dev/null +++ b/components/signnow/common/utils.mjs @@ -0,0 +1,141 @@ +import { ConfigurationError } from "@pipedream/platform"; +import constants from "./constants.mjs"; + +function toPascalCase(str) { + return str.replace(/(\w)(\w*)/g, (_, group1, group2) => + group1.toUpperCase() + group2.toLowerCase()); +} + +function getMetadataProp({ + index, fieldName, prefix, label, +} = {}) { + const fieldIdx = index + 1; + const key = `${fieldName}${fieldIdx}`; + return { + prefix: prefix + ? `${prefix}${key}${constants.SEP}` + : `${key}${constants.SEP}`, + label: label + ? `${label} - ${toPascalCase(fieldName)} ${fieldIdx}` + : `${toPascalCase(fieldName)} ${fieldIdx}`, + }; +} + +function getFieldProps({ + index, fieldName, prefix, + propsMapper = function propsMapper(prefix) { + const { [`${prefix}name`]: name } = this; + return { + name, + }; + }, +} = {}) { + const { prefix: metaPrefix } = getMetadataProp({ + index, + fieldName, + prefix, + }); + return propsMapper(metaPrefix); +} + +function getFieldsProps({ + numberOfFields, fieldName, propsMapper, prefix, +} = {}) { + return Array.from({ + length: numberOfFields, + }).map((_, index) => getFieldProps({ + index, + fieldName, + prefix, + propsMapper, + })); +} + +function getAdditionalProps({ + numberOfFields, fieldName, prefix, label, + getPropDefinitions = async ({ + prefix, label, + }) => ({ + [`${prefix}name`]: { + type: "string", + label, + description: "The name of the field.", + optional: true, + }, + }), +} = {}) { + return Array.from({ + length: numberOfFields, + }).reduce(async (reduction, _, index) => { + const { + prefix: metaPrefix, + label: metaLabel, + } = getMetadataProp({ + index, + fieldName, + prefix, + label, + }); + + return { + ...await reduction, + ...await getPropDefinitions({ + prefix: metaPrefix, + label: metaLabel, + }), + }; + }, {}); +} + +const parseJson = (input) => { + const parse = (value) => { + if (typeof(value) === "string") { + try { + return parseJson(JSON.parse(value)); + } catch (e) { + return value; + } + } else if (typeof(value) === "object" && value !== null) { + return Object.entries(value) + .reduce((acc, [ + key, + val, + ]) => Object.assign(acc, { + [key]: parse(val), + }), {}); + } + return value; + }; + + return parse(input); +}; + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +export default { + getFieldProps, + getFieldsProps, + getAdditionalProps, + parseArray: (value) => parseArray(value).map(parseJson), +}; diff --git a/components/signnow/package.json b/components/signnow/package.json new file mode 100644 index 0000000000000..2ad5180d4bfbf --- /dev/null +++ b/components/signnow/package.json @@ -0,0 +1,22 @@ +{ + "name": "@pipedream/signnow", + "version": "0.1.0", + "description": "Pipedream signNow Components", + "main": "signnow.app.mjs", + "keywords": [ + "pipedream", + "signnow" + ], + "homepage": "https://pipedream.com/apps/signnow", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0", + "crypto": "^1.0.1", + "form-data": "^4.0.0", + "fs": "^0.0.1-security", + "uuid": "^10.0.0" + } +} diff --git a/components/signnow/signnow.app.mjs b/components/signnow/signnow.app.mjs new file mode 100644 index 0000000000000..16dfe7d656d1b --- /dev/null +++ b/components/signnow/signnow.app.mjs @@ -0,0 +1,69 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "signnow", + propDefinitions: { + templateId: { + type: "string", + label: "Template ID", + description: "The ID of the template. You can find the ID by clicking on the template in the **SignNow Templates** page and copying the ID from the menu.", + }, + documentId: { + type: "string", + label: "Document ID", + description: "The ID of the document. You can find the ID by clicking on the document in the **SignNow Documents** page and copying the ID from the menu.", + }, + documentName: { + type: "string", + label: "Document Name", + description: "The name of the document", + optional: true, + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + getUserInfo(args = {}) { + return this._makeRequest({ + path: "/user", + ...args, + }); + }, + post(args = {}) { + return this._makeRequest({ + ...args, + method: "POST", + }); + }, + put(args = {}) { + return this._makeRequest({ + ...args, + method: "PUT", + }); + }, + delete(args = {}) { + return this._makeRequest({ + ...args, + method: "DELETE", + }); + }, + }, +}; diff --git a/components/signnow/sources/common/base.mjs b/components/signnow/sources/common/base.mjs new file mode 100644 index 0000000000000..bbf8b5a183dd9 --- /dev/null +++ b/components/signnow/sources/common/base.mjs @@ -0,0 +1,14 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../signnow.app.mjs"; + +export default { + props: { + app, + db: "$.service.db", + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + }, +}; diff --git a/components/signnow/sources/common/events.mjs b/components/signnow/sources/common/events.mjs new file mode 100644 index 0000000000000..8170346082bf9 --- /dev/null +++ b/components/signnow/sources/common/events.mjs @@ -0,0 +1,78 @@ +export default { + USER_DOCUMENT_COMPLETE: "user.document.complete", + USER_DOCUMENT_CREATE: "user.document.create", + USER_DOCUMENT_DELETE: "user.document.delete", + USER_DOCUMENT_OPEN: "user.document.open", + USER_DOCUMENT_UPDATE: "user.document.update", + USER_TEMPLATE_COPY: "user.template.copy", + USER_DOCUMENT_FIELDINVITE_CREATE: "user.document.fieldinvite.create", + USER_DOCUMENT_FIELDINVITE_DECLINE: "user.document.fieldinvite.decline", + USER_DOCUMENT_FIELDINVITE_DELETE: "user.document.fieldinvite.delete", + USER_DOCUMENT_FIELDINVITE_REASSIGN: "user.document.fieldinvite.reassign", + USER_DOCUMENT_FIELDINVITE_SENT: "user.document.fieldinvite.sent", + USER_DOCUMENT_FIELDINVITE_SIGNED: "user.document.fieldinvite.signed", + USER_DOCUMENT_FIELDINVITE_REPLACE: "user.document.fieldinvite.replace", + USER_DOCUMENT_FREEFORM_CREATE: "user.document.freeform.create", + USER_DOCUMENT_FREEFORM_SIGNED: "user.document.freeform.signed", + USER_DOCUMENT_FIELDINVITE_CONSENT_AGREED: "user.document.fieldinvite.consent.agreed", + USER_DOCUMENT_FIELDINVITE_CONSENT_DECLINED: "user.document.fieldinvite.consent.declined", + USER_DOCUMENT_FIELDINVITE_CONSENT_WITHDRAWN: "user.document.fieldinvite.consent.withdrawn", + USER_DOCUMENT_FIELDINVITE_AUTHENTICATION_FAILED: "user.document.fieldinvite.authentication.failed", + USER_DOCUMENT_FIELDINVITE_EMAIL_DELIVERY_FAILED: "user.document.fieldinvite.email.delivery.failed", + USER_INVITE_EXPIRED: "user.invite.expired", + USER_DOCUMENT_GROUP_EVENTS: "User document group events", + USER_DOCUMENT_GROUP_CREATE: "user.document_group.create", + USER_DOCUMENT_GROUP_UPDATE: "user.document_group.update", + USER_DOCUMENT_GROUP_COMPLETE: "user.document_group.complete", + USER_DOCUMENT_GROUP_DELETE: "user.document_group.delete", + USER_DOCUMENT_GROUP_INVITE_CREATE: "user.document_group.invite.create", + USER_DOCUMENT_GROUP_INVITE_RESEND: "user.document_group.invite.resend", + USER_DOCUMENT_GROUP_INVITE_UPDATE: "user.document_group.invite.update", + USER_DOCUMENT_GROUP_INVITE_CANCEL: "user.document_group.invite.cancel", + USER_DOCUMENT_GROUP_INVITE_DECLINED: "user.document_group.invite.declined", + USER_DOCUMENT_GROUP_FREEFORM_CREATE: "user.document_group.freeform.create", + USER_DOCUMENT_GROUP_FREEFORM_RESEND: "user.document_group.freeform.resend", + USER_DOCUMENT_GROUP_FREEFORM_SIGNED: "user.document_group.freeform.signed", + USER_DOCUMENT_GROUP_FREEFORM_CANCEL: "user.document_group.freeform.cancel", + USER_DOCUMENT_GROUP_INVITE_CONSENT_AGREED: "user.document_group.invite.consent.agreed", + USER_DOCUMENT_GROUP_INVITE_CONSENT_DECLINED: "user.document_group.invite.consent.declined", + USER_DOCUMENT_GROUP_INVITE_CONSENT_WITHDRAWN: "user.document_group.invite.consent.withdrawn", + USER_DOCUMENT_GROUP_INVITE_AUTHENTICATION_FAILED: "user.document_group.invite.authentication.failed", + USER_DOCUMENT_GROUP_INVITE_EMAIL_DELIVERY_FAILED: "user.document_group.invite.email.delivery.failed", + DOCUMENT_COMPLETE: "document.complete", + DOCUMENT_DELETE: "document.delete", + DOCUMENT_OPEN: "document.open", + DOCUMENT_UPDATE: "document.update", + TEMPLATE_COPY: "template.copy", + DOCUMENT_FIELDINVITE_CREATE: "document.fieldinvite.create", + DOCUMENT_FIELDINVITE_DECLINE: "document.fieldinvite.decline", + DOCUMENT_FIELDINVITE_DELETE: "document.fieldinvite.delete", + DOCUMENT_FIELDINVITE_REASSIGN: "document.fieldinvite.reassign", + DOCUMENT_FIELDINVITE_SENT: "document.fieldinvite.sent", + DOCUMENT_FIELDINVITE_SIGNED: "document.fieldinvite.signed", + DOCUMENT_FIELDINVITE_REPLACE: "document.fieldinvite.replace", + DOCUMENT_FREEFORM_CREATE: "document.freeform.create", + DOCUMENT_FREEFORM_SIGNED: "document.freeform.signed", + DOCUMENT_FIELDINVITE_CONSENT_AGREED: "document.fieldinvite.consent.agreed", + DOCUMENT_FIELDINVITE_CONSENT_DECLINED: "document.fieldinvite.consent.declined", + DOCUMENT_FIELDINVITE_CONSENT_WITHDRAWN: "document.fieldinvite.consent.withdrawn", + DOCUMENT_FIELDINVITE_AUTHENTICATION_FAILED: "document.fieldinvite.authentication.failed", + DOCUMENT_FIELDINVITE_EMAIL_DELIVERY_FAILED: "document.fieldinvite.email.delivery.failed", + DOCUMENT_GROUP_DELETE: "document_group.delete", + DOCUMENT_GROUP_UPDATE: "document_group.update", + DOCUMENT_GROUP_COMPLETE: "document_group.complete", + DOCUMENT_GROUP_INVITE_CREATE: "document_group.invite.create", + DOCUMENT_GROUP_INVITE_RESEND: "document_group.invite.resend", + DOCUMENT_GROUP_INVITE_UPDATE: "document_group.invite.update", + DOCUMENT_GROUP_INVITE_CANCEL: "document_group.invite.cancel", + DOCUMENT_GROUP_INVITE_DECLINED: "document_group.invite.declined", + DOCUMENT_GROUP_FREEFORM_CREATE: "document_group.freeform.create", + DOCUMENT_GROUP_FREEFORM_RESEND: "document_group.freeform.resend", + DOCUMENT_GROUP_FREEFORM_SIGNED: "document_group.freeform.signed", + DOCUMENT_GROUP_FREEFORM_CANCEL: "document_group.freeform.cancel", + DOCUMENT_GROUP_INVITE_CONSENT_AGREED: "document_group.invite.consent.agreed", + DOCUMENT_GROUP_INVITE_CONSENT_DECLINED: "document_group.invite.consent.declined", + DOCUMENT_GROUP_INVITE_CONSENT_WITHDRAWN: "document_group.invite.consent.withdrawn", + DOCUMENT_GROUP_INVITE_AUTHENTICATION_FAILED: "document_group.invite.authentication.failed", + DOCUMENT_GROUP_INVITE_EMAIL_DELIVERY_FAILED: "document_group.invite.email.delivery.failed", +}; diff --git a/components/signnow/sources/common/webhook.mjs b/components/signnow/sources/common/webhook.mjs new file mode 100644 index 0000000000000..8bb6facb7994d --- /dev/null +++ b/components/signnow/sources/common/webhook.mjs @@ -0,0 +1,142 @@ +import { v4 as uuid } from "uuid"; +import { createHmac } from "crypto"; +import { ConfigurationError } from "@pipedream/platform"; +import common from "./base.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + ...common, + props: { + ...common.props, + http: { + type: "$.interface.http", + customResponse: true, + }, + }, + hooks: { + async activate() { + const { + http: { endpoint: callback }, + createWebhook, + listWebhooks, + getEventName, + setWebhookId, + setSecretKey, + getEntityId, + } = this; + const secretKey = uuid(); + const entityId = await getEntityId(); + + await createWebhook({ + data: { + event: getEventName(), + entity_id: entityId, + action: "callback", + attributes: { + callback, + use_tls_12: true, + secret_key: secretKey, + }, + }, + }); + + const { data } = await listWebhooks(); + const webhook = data.find(({ json_attributes: attrs }) => { + return secretKey === attrs?.secret_key; + }); + + setSecretKey(secretKey); + setWebhookId(webhook?.id); + }, + async deactivate() { + const { + deleteWebhook, + getWebhookId, + } = this; + + const webhookId = getWebhookId(); + if (webhookId) { + await deleteWebhook({ + webhookId, + }); + } + }, + }, + methods: { + ...common.methods, + getEntityId() { + throw new ConfigurationError("getEntityId is not implemented"); + }, + setWebhookId(value) { + this.db.set(constants.WEBHOOK_ID, value); + }, + getWebhookId() { + return this.db.get(constants.WEBHOOK_ID); + }, + setSecretKey(value) { + this.db.set(constants.SECRET_KEY, value); + }, + getSecretKey() { + return this.db.get(constants.SECRET_KEY); + }, + getEventName() { + throw new ConfigurationError("getEventName is not implemented"); + }, + verifySignature(bodyRaw, signature) { + const secretKey = this.getSecretKey(); + const hash = createHmac("sha256", secretKey) + .update(bodyRaw) + .digest("base64"); + return hash === signature; + }, + processResource(resource) { + this.$emit(resource, this.generateMeta(resource)); + }, + createWebhook(args = {}) { + return this.app.post({ + debug: true, + path: "/api/v2/events", + ...args, + }); + }, + deleteWebhook({ + webhookId, ...args + } = {}) { + return this.app.delete({ + debug: true, + path: `/v2/event-subscriptions/${webhookId}`, + ...args, + }); + }, + listWebhooks(args = {}) { + return this.app._makeRequest({ + debug: true, + path: "/v2/event-subscriptions", + ...args, + }); + }, + }, + async run({ + body, bodyRaw, headers: { ["x-signnow-signature"]: signature }, + }) { + const { + http, + verifySignature, + processResource, + } = this; + + const validSignature = verifySignature(bodyRaw, signature); + + if (!validSignature) { + return http.respond({ + status: 401, + }); + } + + http.respond({ + status: 200, + }); + + processResource(body); + }, +}; diff --git a/components/signnow/sources/completed-document-instant/completed-document-instant.mjs b/components/signnow/sources/completed-document-instant/completed-document-instant.mjs new file mode 100644 index 0000000000000..1499332e3bb8f --- /dev/null +++ b/components/signnow/sources/completed-document-instant/completed-document-instant.mjs @@ -0,0 +1,36 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "signnow-completed-document-instant", + name: "Completed Document (Instant)", + description: "Emit new event when all signers have filled in and signed the document. [See the documentation](https://docs.signnow.com/docs/signnow/webhooks/operations/create-a-api-v-2-event)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + async getEntityId() { + const { id } = await this.app.getUserInfo(); + return id; + }, + getEventName() { + return events.USER_DOCUMENT_COMPLETE; + }, + generateMeta(resource) { + const { + document_id: documentId, + document_name: documentName, + } = resource.content; + const { timestamp: ts } = resource.meta; + return { + id: `${documentId}-${ts}`, + summary: `Completed Document: ${documentName}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/signnow/sources/completed-document-instant/test-event.mjs b/components/signnow/sources/completed-document-instant/test-event.mjs new file mode 100644 index 0000000000000..85a30ad3ede01 --- /dev/null +++ b/components/signnow/sources/completed-document-instant/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "meta": { + "timestamp": 1724791134, + "event": "user.document.complete", + "environment": "https://api.signnow.com", + "callback_url": "https://a891baddb996f875ac3165efb11afbce.m.pipedream.net", + "access_token": "3145395970e758a81f649ae0ed4d919a1d7c5a1f18e3b489a49fa64a8bc56432", + "initiator_id": "6e6eb2347e1f4746b88ae1102fcecfb9ee1e8543" + }, + "content": { + "document_id": "eb7db0adac8a405b9ff6d08bfc8ca6d21cb02432", + "document_name": "RUT", + "user_id": "6e6eb2347e1f4746b88ae1102fcecfb9ee1e84322" + } +}; diff --git a/components/signnow/sources/updated-document-instant/test-event.mjs b/components/signnow/sources/updated-document-instant/test-event.mjs new file mode 100644 index 0000000000000..99421ae3e57c6 --- /dev/null +++ b/components/signnow/sources/updated-document-instant/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "meta": { + "timestamp": 1724791134, + "event": "user.document.update", + "environment": "https://api.signnow.com", + "callback_url": "https://a891baddb996f875ac3165efb11afbce.m.pipedream.net", + "access_token": "3145395970e758a81f649ae0ed4d919a1d7c5a1f18e3b489a49fa64a8bc56432", + "initiator_id": "6e6eb2347e1f4746b88ae1102fcecfb9ee1e8543" + }, + "content": { + "document_id": "eb7db0adac8a405b9ff6d08bfc8ca6d21cb02432", + "document_name": "RUT", + "user_id": "6e6eb2347e1f4746b88ae1102fcecfb9ee1e84322" + } +}; diff --git a/components/signnow/sources/updated-document-instant/updated-document-instant.mjs b/components/signnow/sources/updated-document-instant/updated-document-instant.mjs new file mode 100644 index 0000000000000..71e23c286f7d5 --- /dev/null +++ b/components/signnow/sources/updated-document-instant/updated-document-instant.mjs @@ -0,0 +1,36 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "signnow-updated-document-instant", + name: "Updated Document (Instant)", + description: "Emit new event when a document has been updated. [See the documentation](https://docs.signnow.com/docs/signnow/webhooks/operations/create-a-api-v-2-event)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + async getEntityId() { + const { id } = await this.app.getUserInfo(); + return id; + }, + getEventName() { + return events.USER_DOCUMENT_UPDATE; + }, + generateMeta(resource) { + const { + document_id: documentId, + document_name: documentName, + } = resource.content; + const { timestamp: ts } = resource.meta; + return { + id: `${documentId}-${ts}`, + summary: `Updated Document: ${documentName}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/signpath/package.json b/components/signpath/package.json new file mode 100644 index 0000000000000..0b97f50917d4f --- /dev/null +++ b/components/signpath/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/signpath", + "version": "0.0.1", + "description": "Pipedream SignPath Components", + "main": "signpath.app.mjs", + "keywords": [ + "pipedream", + "signpath" + ], + "homepage": "https://pipedream.com/apps/signpath", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/signpath/signpath.app.mjs b/components/signpath/signpath.app.mjs new file mode 100644 index 0000000000000..34dc7223dfb3f --- /dev/null +++ b/components/signpath/signpath.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "signpath", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/signrequest/README.md b/components/signrequest/README.md new file mode 100644 index 0000000000000..bcdba2c73b3cd --- /dev/null +++ b/components/signrequest/README.md @@ -0,0 +1,11 @@ +# Overview + +With the SignRequest API on Pipedream, you can automate sending documents for signatures, monitor the status of sent documents, and streamline the e-signature process within your own applications or workflows. This facilitates swift and secure digital signatures, allowing you to integrate SignRequest with other services for enhanced document management and notification systems. + +# Example Use Cases + +- **Automated Document Workflow for HR Onboarding**: Use SignRequest to send employment contracts as part of an automated HR onboarding process. When a new employee is added to your HR system, Pipedream can trigger a workflow to send the necessary documents for signature via SignRequest, then update your HR system once the documents are signed. + +- **Contract Management with Google Drive**: Set up a workflow where new contracts added to a specific Google Drive folder are automatically sent out for signatures. Pipedream can monitor the folder for new files, send them through SignRequest, and then move signed contracts to a 'Completed' folder within Google Drive. + +- **Sales Pipeline Signature Tracking**: Integrate SignRequest with a CRM like Salesforce. Whenever a sales deal reaches a certain stage in the pipeline, Pipedream can trigger a SignRequest to send out the sales agreement for signature, then update the CRM record with the signature status, keeping the sales process moving smoothly. diff --git a/components/signwell/README.md b/components/signwell/README.md index 0bbfcfddfb30d..d1f2d3867010d 100644 --- a/components/signwell/README.md +++ b/components/signwell/README.md @@ -1,23 +1,11 @@ # Overview -The SignWell API is a powerful tool for creating accessible digital signing -solutions. With the SignWell API, developers can create a range of solutions -that address the need for online or electronic signatures. These solutions can -be used for contract signing, document signing, e-forms, and other signing -needs. +Harness the power of SignWell's API on Pipedream to automate the e-signature process and streamline document workflows. Integrate e-signatures into your business processes seamlessly, triggering actions based on document status, auto-filling templates with customer data, and syncing signed documents to your storage solutions. The SignWell API through Pipedream empowers you to create custom, scalable solutions for document management that save time and eliminate manual errors. -The SignWell API provides developers the tools they need to create solutions -with a modern, scalable, and secure digital signing experience. It features -features such as rich document image support, optimized mobile experience, and -audit trails. This API is designed to help developers build easy-to-use -solutions with features such as personalized branding and customization, -powerful workflow automation, and advanced cryptography. +# Example Use Cases -Here are some of the solutions that can be developed using the SignWell API: +- **Automated Client Onboarding**: Automatically send out personalized SignWell e-signature requests when a new client is added to your CRM. Once the document is signed, trigger an onboarding email series and update the client's status in your CRM system. -- Digital contract signing -- Electronic signature and e-form solutions -- Remote signing and e-authorization solutions -- Multi-factor authentication solutions -- Electronic document signing solutions -- Digital ID verification solutions +- **Contract Renewal Management**: Set up a workflow to detect approaching contract end dates stored in a database. Use SignWell to send renewal agreements for e-signature and, upon completion, update contract records and notify account managers. + +- **Document Backup and Organization**: After a document is signed via SignWell, automatically back it up to cloud storage like Google Drive or Dropbox. Then, use Pipedream to parse the document's metadata and organize it into the correct folder structure based on predefined rules. diff --git a/components/silfer_bots/README.md b/components/silfer_bots/README.md index de8c4558f0d01..553b7a949dcf0 100644 --- a/components/silfer_bots/README.md +++ b/components/silfer_bots/README.md @@ -1,25 +1,11 @@ # Overview -SilFer Bots is a conversational intelligence platform that enables you to -create intelligent virtual assistants, chatbots, and applications to meet the -needs of businesses and their customers. It is a comprehensive, secure, and -enterprise-grade platform to build, deploy, automate, and scale conversational -solutions. +The SilFer Bots API enables the creation and management of chatbots that can be used for automating customer interactions across various messaging platforms. Leveraging Pipedream's strengths, you can develop intricate workflows that respond to events, synchronize data, and facilitate seamless communication between your SilFer Bots and other applications. The API's capabilities include sending messages, managing contacts, and handling bot events, which opens up a plethora of automation opportunities to enhance customer engagement and operational efficiency. -With SilFer Bots, businesses can quickly create virtual assistants to capture -leads, enhance customer support, and provide personalized experiences. The -platform is designed to easily integrate with legacy systems and chatbot APIs, -providing complete control and better customer service. +# Example Use Cases -Examples of what can be built using SilFer bots: +- **Lead Qualification Automation**: When a new user interacts with your SilFer Bot, use Pipedream to capture the conversation and feed the details into a CRM like Salesforce. Apply logic to score the lead based on interaction and automatically assign a follow-up task in the CRM to the relevant sales team member. -- Virtual Assistants -- Chatbots -- Business Automation -- Business Intelligence -- Customer Support -- Sales Funnels -- Surveys and Feedback bots -- Appointment Scheduling systems -- Interactive Games and Content -- Notifications and Reminders +- **Customer Support Ticketing**: Configure your SilFer Bot to identify support requests and trigger a Pipedream workflow that creates a ticket in a platform like Zendesk. The workflow can categorize the ticket based on the conversation and prioritize it accordingly. + +- **Event-Driven Content Delivery**: Set up your SilFer Bot to offer content based on user requests or actions. With Pipedream, when a user interacts with the bot showing interest in a specific topic, automatically send them a personalized email via SendGrid with related content or resources. diff --git a/components/silfer_bots/package.json b/components/silfer_bots/package.json new file mode 100644 index 0000000000000..c1439789ace87 --- /dev/null +++ b/components/silfer_bots/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/silfer_bots", + "version": "0.6.0", + "description": "Pipedream silfer_bots Components", + "main": "silfer_bots.app.mjs", + "keywords": [ + "pipedream", + "silfer_bots" + ], + "homepage": "https://pipedream.com/apps/silfer_bots", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/similarweb_digitalrank_api/README.md b/components/similarweb_digitalrank_api/README.md new file mode 100644 index 0000000000000..9c0aa41a2e061 --- /dev/null +++ b/components/similarweb_digitalrank_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Similarweb DigitalRank API provides insights into website traffic and engagement, offering metrics like global rank, country rank, and category rank of a domain. With this API on Pipedream, you can automate competitive analysis, monitor changes in website popularity, and integrate web ranking data into your marketing, SEO, or content strategies. Pipedream's serverless platform allows you to create workflows that react to this data in real-time, connecting to other services to act upon these insights. + +# Example Use Cases + +- **Automated Competitor Tracking**: Trigger a Pipedream workflow whenever there's a change in the rank of specified competitors. Store this data in a Pipedream data store or send it to a Google Sheet for trend analysis, and receive notifications via Slack or email when significant rank changes occur. + +- **SEO Performance Dashboard Updates**: Use the Similarweb DigitalRank API to fetch daily rankings for your own domains, and update a dashboard tool like Geckoboard or Klipfolio. This real-time visibility can help SEO teams to quickly respond to changes in web performance or strategize on content creation. + +- **Lead Generation for Sales Teams**: Identify top-ranked websites in certain categories and use this data to generate leads. Combine Similarweb rankings with Clearbit or Hunter to find contact information of key stakeholders at these high-ranking websites, and automatically add them to a CRM like Salesforce or HubSpot. diff --git a/components/similarweb_digitalrank_api/actions/get-ranked-sites/get-ranked-sites.mjs b/components/similarweb_digitalrank_api/actions/get-ranked-sites/get-ranked-sites.mjs new file mode 100644 index 0000000000000..e48b494b20ea6 --- /dev/null +++ b/components/similarweb_digitalrank_api/actions/get-ranked-sites/get-ranked-sites.mjs @@ -0,0 +1,29 @@ +import app from "../../similarweb_digitalrank_api.app.mjs"; + +export default { + key: "similarweb_digitalrank_api-get-ranked-sites", + name: "Get Ranked Sites", + description: "List the top-ranking websites globally. [See the documentation](https://developers.similarweb.com/docs/digital-rank-api)", + version: "0.0.1", + type: "action", + props: { + app, + limit: { + propDefinition: [ + app, + "limit", + ], + }, + }, + async run({ $ }) { + const response = await this.app.listTopRankingWebsites({ + $, + params: { + limit: this.limit, + }, + }); + + $.export("$summary", `Successfully listed the top ${this.limit} ranking websites globally.`); + return response; + }, +}; diff --git a/components/similarweb_digitalrank_api/actions/get-subscription-status/get-subscription-status.mjs b/components/similarweb_digitalrank_api/actions/get-subscription-status/get-subscription-status.mjs new file mode 100644 index 0000000000000..668b26de77d9b --- /dev/null +++ b/components/similarweb_digitalrank_api/actions/get-subscription-status/get-subscription-status.mjs @@ -0,0 +1,21 @@ +import app from "../../similarweb_digitalrank_api.app.mjs"; + +export default { + key: "similarweb_digitalrank_api-get-subscription-status", + name: "Get Subscription Status", + description: "Returns the number of monthly data points remaining in your Similarweb account. [See the documentation](https://developers.similarweb.com/docs/digital-rank-api)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.getSubscriptionStatus({ + $, + }); + + $.export("$summary", `You have ${response.user_remaining} monthly data points remaining in your Similarweb account.`); + + return response; + }, +}; diff --git a/components/similarweb_digitalrank_api/actions/get-website-rank/get-website-rank.mjs b/components/similarweb_digitalrank_api/actions/get-website-rank/get-website-rank.mjs new file mode 100644 index 0000000000000..bdf7a93ae73c2 --- /dev/null +++ b/components/similarweb_digitalrank_api/actions/get-website-rank/get-website-rank.mjs @@ -0,0 +1,28 @@ +import app from "../../similarweb_digitalrank_api.app.mjs"; + +export default { + key: "similarweb_digitalrank_api-get-website-rank", + name: "Get Website Rank", + description: "Retrieves the global rank of a specific website using Similarweb's Digital Rank API. [See the documentation](https://developers.similarweb.com/docs/digital-rank-api)", + version: "0.0.1", + type: "action", + props: { + app, + domain: { + propDefinition: [ + app, + "domain", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getWebsiteRank({ + $, + domain: this.domain, + }); + + $.export("$summary", `Retrieved global rank for domain: ${this.domain}`); + + return response; + }, +}; diff --git a/components/similarweb_digitalrank_api/package.json b/components/similarweb_digitalrank_api/package.json index 02b66b33a16d5..f43af4b25cb24 100644 --- a/components/similarweb_digitalrank_api/package.json +++ b/components/similarweb_digitalrank_api/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/similarweb_digitalrank_api", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Similarweb DigitalRank API Components", "main": "similarweb_digitalrank_api.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" } -} \ No newline at end of file +} diff --git a/components/similarweb_digitalrank_api/similarweb_digitalrank_api.app.mjs b/components/similarweb_digitalrank_api/similarweb_digitalrank_api.app.mjs index 2e02faacdc26f..b27490176aae0 100644 --- a/components/similarweb_digitalrank_api/similarweb_digitalrank_api.app.mjs +++ b/components/similarweb_digitalrank_api/similarweb_digitalrank_api.app.mjs @@ -1,11 +1,60 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "similarweb_digitalrank_api", - propDefinitions: {}, + propDefinitions: { + limit: { + type: "integer", + label: "Limit", + description: "The number of desired results", + optional: true, + }, + domain: { + type: "string", + label: "Domain", + description: "Enter the domain to retrieve its global rank, i.e. `google.com`", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.similarweb.com"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + ...otherOpts + } = opts; + return axios($, { + url: this._baseUrl() + path, + params: { + ...params, + api_key: `${this.$auth.api_key}`, + }, + ...otherOpts, + }); + }, + async listTopRankingWebsites(args = {}) { + return this._makeRequest({ + path: "/v1/similar-rank/top-sites/", + ...args, + }); + }, + async getWebsiteRank({ + domain, ...args + }) { + return this._makeRequest({ + path: `/v1/similar-rank/${domain}/rank`, + ...args, + }); + }, + async getSubscriptionStatus(args = {}) { + return this._makeRequest({ + path: "/user-capabilities", + ...args, + }); }, }, }; diff --git a/components/simla_com/actions/create-customer/create-customer.mjs b/components/simla_com/actions/create-customer/create-customer.mjs new file mode 100644 index 0000000000000..d67ac9ae5c421 --- /dev/null +++ b/components/simla_com/actions/create-customer/create-customer.mjs @@ -0,0 +1,115 @@ +import app from "../../simla_com.app.mjs"; + +export default { + key: "simla_com-create-customer", + name: "Create Customer", + description: "Creates a new customer profile. [See the documentation](https://docs.simla.com/Developers/API/APIVersions/APIv5#post--api-v5-customers-create).", + version: "0.0.1", + type: "action", + props: { + app, + firstName: { + type: "string", + label: "First Name", + description: "The name of the customer.", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the customer.", + optional: true, + }, + phones: { + type: "string[]", + label: "Phone Numbers", + description: "The phone numbers of the customer.", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "The email address of the customer.", + optional: true, + }, + postalCode: { + type: "string", + label: "Postal Code", + description: "The postal code of the customer.", + optional: true, + }, + countryIso: { + optional: true, + propDefinition: [ + app, + "countryIso", + ], + }, + region: { + type: "string", + label: "Region", + description: "The region of the customer.", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "The city of the customer.", + optional: true, + }, + street: { + type: "string", + label: "Street", + description: "The street of the customer.", + optional: true, + }, + }, + methods: { + createCustomer(args = {}) { + return this.app.post({ + path: "/customers/create", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createCustomer, + firstName, + lastName, + phones, + email, + postalCode, + countryIso, + region, + city, + street, + } = this; + + const response = await createCustomer({ + $, + data: { + customer: JSON.stringify({ + firstName, + lastName, + email, + address: { + index: postalCode, + countryIso, + region, + city, + street, + }, + ...(phones?.length && { + phones: phones.map((number) => ({ + number, + })), + }), + }), + }, + }); + + $.export("$summary", `Successfully created customer with ID \`${response.id}\`.`); + + return response; + }, +}; diff --git a/components/simla_com/actions/create-order/create-order.mjs b/components/simla_com/actions/create-order/create-order.mjs new file mode 100644 index 0000000000000..b1e0d688d6f75 --- /dev/null +++ b/components/simla_com/actions/create-order/create-order.mjs @@ -0,0 +1,195 @@ +import app from "../../simla_com.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "simla_com-create-order", + name: "Create Order", + description: "Creates a new order with customer and order details. [See the documentation](https://docs.simla.com/Developers/API/APIVersions/APIv5#post--api-v5-orders-create).", + version: "0.0.1", + type: "action", + props: { + app, + site: { + propDefinition: [ + app, + "site", + ], + }, + customerType: { + propDefinition: [ + app, + "customerType", + ], + }, + customerId: { + propDefinition: [ + app, + "customerId", + ({ customerType }) => ({ + customerType, + }), + ], + }, + countryIso: { + propDefinition: [ + app, + "countryIso", + ], + }, + orderType: { + propDefinition: [ + app, + "orderType", + ], + }, + deliveryCost: { + type: "string", + label: "Delivery Cost", + description: "The cost of delivery.", + }, + deliveryNetCost: { + type: "string", + label: "Delivery Net Cost", + description: "The net cost of delivery.", + }, + numberOfItems: { + type: "integer", + label: "Number Of Items", + description: "The number of items to be ordered.", + default: 1, + reloadProps: true, + }, + }, + methods: { + itemsPropsMapper(prefix) { + const { + [`${prefix}initialPrice`]: initialPrice, + [`${prefix}quantity`]: quantity, + [`${prefix}comment`]: comment, + [`${prefix}purchasePrice`]: purchasePrice, + [`${prefix}productName`]: productName, + [`${prefix}discountManualAmount`]: discountManualAmount, + [`${prefix}discountManualPercent`]: discountManualPercent, + } = this; + + return { + initialPrice, + quantity, + comment, + purchasePrice, + productName, + discountManualAmount, + discountManualPercent, + }; + }, + getItemsPropDefinitions({ + prefix, + label, + } = {}) { + return { + [`${prefix}initialPrice`]: { + type: "string", + label: `${label} - Initial Price`, + description: "The initial price of the item.", + optional: true, + }, + [`${prefix}quantity`]: { + type: "string", + label: `${label} - Quantity`, + description: "The quantity of the item.", + optional: true, + }, + [`${prefix}comment`]: { + type: "string", + label: `${label} - Comment`, + description: "The comment for the item.", + optional: true, + }, + [`${prefix}purchasePrice`]: { + type: "string", + label: `${label} - Purchase Price`, + description: "The purchase price of the item.", + optional: true, + }, + [`${prefix}productName`]: { + type: "string", + label: `${label} - Product Name`, + description: "The name of the product.", + optional: true, + }, + [`${prefix}discountManualAmount`]: { + type: "string", + label: `${label} - Discount Manual Amount`, + description: "The manual discount amount for the item.", + optional: true, + }, + [`${prefix}discountManualPercent`]: { + type: "string", + label: `${label} - Discount Manual Percent`, + description: "The manual discount percent for the item.", + optional: true, + }, + }; + }, + createOrder(args = {}) { + return this.app.post({ + path: "/orders/create", + ...args, + }); + }, + }, + async additionalProps() { + const { + numberOfItems, + getItemsPropDefinitions, + } = this; + + return utils.getAdditionalProps({ + numberOfFields: numberOfItems, + fieldName: "item", + getPropDefinitions: getItemsPropDefinitions, + }); + }, + async run({ $ }) { + const { + numberOfItems, + itemsPropsMapper, + createOrder, + countryIso, + orderType, + site, + customerType, + customerId, + deliveryCost, + deliveryNetCost, + } = this; + + const response = await createOrder({ + $, + data: { + site, + customer: JSON.stringify({ + customerType, + customerId, + }), + order: JSON.stringify({ + countryIso, + orderType, + customerType, + customerId, + delivery: { + cost: deliveryCost, + netCost: deliveryNetCost, + }, + items: utils.getFieldsProps({ + numberOfFields: numberOfItems, + fieldName: "item", + propsMapper: itemsPropsMapper, + }), + }), + }, + }); + $.export("$summary", `Successfully created order with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/simla_com/common/constants.mjs b/components/simla_com/common/constants.mjs new file mode 100644 index 0000000000000..01c27eabff9d9 --- /dev/null +++ b/components/simla_com/common/constants.mjs @@ -0,0 +1,29 @@ +const SUBDOMAIN_PLACEHOLDER = "{subdomain}"; +const VERSION_PLACEHOLDER = "{version}"; +const BASE_URL = `https://${SUBDOMAIN_PLACEHOLDER}.simla.com/api/${VERSION_PLACEHOLDER}`; +const IS_FIRST_RUN = "isFirstRun"; +const LAST_DATE_AT = "lastDateAt"; +const DEFAULT_LIMIT = 20; +const DEFAULT_MAX = 200; + +const CUSTOMER_TYPE = { + CUSTOMER: { + label: "Customer", + value: "customer", + }, + CORPORATE: { + label: "Corporate Customer", + value: "customer_corporate", + }, +}; + +export default { + SUBDOMAIN_PLACEHOLDER, + VERSION_PLACEHOLDER, + BASE_URL, + IS_FIRST_RUN, + CUSTOMER_TYPE, + LAST_DATE_AT, + DEFAULT_LIMIT, + DEFAULT_MAX, +}; diff --git a/components/simla_com/common/utils.mjs b/components/simla_com/common/utils.mjs new file mode 100644 index 0000000000000..aced76b9a7d22 --- /dev/null +++ b/components/simla_com/common/utils.mjs @@ -0,0 +1,107 @@ +import constants from "./constants.mjs"; + +function getNestedProperty(obj, propertyString) { + const properties = propertyString.split("."); + return properties.reduce((prev, curr) => prev?.[curr], obj); +} + +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +function toPascalCase(str) { + return str.replace(/(\w)(\w*)/g, (_, group1, group2) => + group1.toUpperCase() + group2.toLowerCase()); +} + +function getMetadataProp({ + index, fieldName, prefix, label, +} = {}) { + const fieldIdx = index + 1; + const key = `${fieldName}${fieldIdx}`; + return { + prefix: prefix + ? `${prefix}${key}${constants.SEP}` + : `${key}${constants.SEP}`, + label: label + ? `${label} - ${toPascalCase(fieldName)} ${fieldIdx}` + : `${toPascalCase(fieldName)} ${fieldIdx}`, + }; +} + +function getFieldProps({ + index, fieldName, prefix, + propsMapper = function propsMapper(prefix) { + const { [`${prefix}name`]: name } = this; + return { + name, + }; + }, +} = {}) { + const { prefix: metaPrefix } = getMetadataProp({ + index, + fieldName, + prefix, + }); + return propsMapper(metaPrefix); +} + +function getFieldsProps({ + numberOfFields, fieldName, propsMapper, prefix, +} = {}) { + return Array.from({ + length: numberOfFields, + }).map((_, index) => getFieldProps({ + index, + fieldName, + prefix, + propsMapper, + })); +} + +function getAdditionalProps({ + numberOfFields, fieldName, prefix, label, + getPropDefinitions = async ({ + prefix, label, + }) => ({ + [`${prefix}name`]: { + type: "string", + label, + description: "The name of the field.", + optional: true, + }, + }), +} = {}) { + return Array.from({ + length: numberOfFields, + }).reduce(async (reduction, _, index) => { + const { + prefix: metaPrefix, + label: metaLabel, + } = getMetadataProp({ + index, + fieldName, + prefix, + label, + }); + + return { + ...await reduction, + ...await getPropDefinitions({ + prefix: metaPrefix, + label: metaLabel, + }), + }; + }, {}); +} + +export default { + getNestedProperty, + iterate, + getFieldsProps, + getAdditionalProps, +}; diff --git a/components/simla_com/package.json b/components/simla_com/package.json new file mode 100644 index 0000000000000..3eeec870d5d10 --- /dev/null +++ b/components/simla_com/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/simla_com", + "version": "0.1.0", + "description": "Pipedream Simla.com Components", + "main": "simla_com.app.mjs", + "keywords": [ + "pipedream", + "simla_com" + ], + "homepage": "https://pipedream.com/apps/simla_com", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.1" + } +} diff --git a/components/simla_com/simla_com.app.mjs b/components/simla_com/simla_com.app.mjs new file mode 100644 index 0000000000000..47dd69a43cac9 --- /dev/null +++ b/components/simla_com/simla_com.app.mjs @@ -0,0 +1,216 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; + +export default { + type: "app", + app: "simla_com", + propDefinitions: { + countryIso: { + type: "string", + label: "Country ISO", + description: "The ISO code of the customer's country (ISO 3166-1 alpha-2).", + async options() { + const { countriesIso } = await this.listCountries(); + return countriesIso; + }, + }, + orderType: { + type: "string", + label: "Order Type", + description: "The type of the order.", + async options() { + const { orderTypes } = await this.listOrderTypes(); + return Object.values(orderTypes) + .map(({ + code: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + site: { + type: "string", + label: "Site", + description: "The site of the order.", + async options() { + const { sites } = await this.listSites(); + return Object.values(sites) + .map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + customerType: { + type: "string", + label: "Customer Type", + description: "The type of the customer.", + options: Object.values(constants.CUSTOMER_TYPE), + }, + customerId: { + type: "string", + label: "Customer ID", + description: "The unique identifier for the customer.", + async options({ + page, customerType, params, + }) { + const isCustomer = customerType === constants.CUSTOMER_TYPE.CUSTOMER.value; + const args = { + params: { + ...params, + page: page + 1, + }, + }; + const fieldName = isCustomer + ? "customers" + : "customersCorporate"; + const { [fieldName]: customers } = + isCustomer + ? await this.listCustomers(args) + : await this.listCorporateCustomers(args); + return customers.map(({ + id: value, firstName, lastName, site, + }) => ({ + label: isCustomer + ? [ + firstName, + lastName, + ].join(" ") + : site, + value, + })); + }, + }, + }, + methods: { + getUrl(path) { + const { + subdomain, + version, + } = this.$auth; + const baseUrl = constants.BASE_URL + .replace(constants.SUBDOMAIN_PLACEHOLDER, subdomain) + .replace(constants.VERSION_PLACEHOLDER, version); + return `${baseUrl}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "X-API-KEY": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + listCountries(args = {}) { + return this._makeRequest({ + path: "/reference/countries", + ...args, + }); + }, + listOrderTypes(args = {}) { + return this._makeRequest({ + path: "/reference/order-types", + ...args, + }); + }, + listSites(args = {}) { + return this._makeRequest({ + path: "/reference/sites", + ...args, + }); + }, + listCustomers(args = {}) { + return this._makeRequest({ + path: "/customers", + ...args, + }); + }, + listCorporateCustomers(args = {}) { + return this._makeRequest({ + path: "/customers-corporate", + ...args, + }); + }, + listCustomerChangeHistory(args = {}) { + return this._makeRequest({ + path: "/customers/history", + ...args, + }); + }, + listOrders(args = {}) { + return this._makeRequest({ + path: "/orders", + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, resourceName, + lastDateAt, dateField, + max = constants.DEFAULT_MAX, + }) { + let page = 1; + let resourcesCount = 0; + + while (true) { + const response = + await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + page, + }, + }); + + const nextResources = utils.getNestedProperty(response, resourceName); + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + const isDateGreater = + lastDateAt + && Date.parse(resource[dateField]) >= Date.parse(lastDateAt); + + if (!lastDateAt || isDateGreater) { + yield resource; + resourcesCount += 1; + } + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + if (nextResources.length < constants.DEFAULT_LIMIT) { + console.log("No next page found"); + return; + } + + page += 1; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); + }, + }, +}; diff --git a/components/simla_com/sources/common/polling.mjs b/components/simla_com/sources/common/polling.mjs new file mode 100644 index 0000000000000..d56c7d764f974 --- /dev/null +++ b/components/simla_com/sources/common/polling.mjs @@ -0,0 +1,120 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../simla_com.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + deploy() { + this.setIsFirstRun(true); + }, + }, + methods: { + isDescendingOrder() { + return true; + }, + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + setIsFirstRun(value) { + this.db.set(constants.IS_FIRST_RUN, value); + }, + getIsFirstRun() { + return this.db.get(constants.IS_FIRST_RUN); + }, + setLastDateAt(value) { + this.db.set(constants.LAST_DATE_AT, value); + }, + getLastDateAt() { + return this.db.get(constants.LAST_DATE_AT); + }, + getDateField() { + throw new ConfigurationError("getDateField is not implemented"); + }, + sortFn() { + return; + }, + isResourceRelevant() { + return true; + }, + getResourceName() { + throw new ConfigurationError("getResourceName is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + }, + async run() { + const { + app, + getDateField, + getLastDateAt, + getResourcesFn, + getResourcesFnArgs, + getResourceName, + processResource, + getIsFirstRun, + setIsFirstRun, + setLastDateAt, + isDescendingOrder, + } = this; + + const isFirstRun = getIsFirstRun(); + const dateField = getDateField(); + const lastDateAt = getLastDateAt(); + const isDescending = isDescendingOrder(); + + const otherArgs = isFirstRun + ? { + max: constants.DEFAULT_LIMIT, + } + : { + dateField, + lastDateAt, + }; + + const resources = await app.paginate({ + resourcesFn: getResourcesFn(), + resourcesFnArgs: getResourcesFnArgs(), + resourceName: getResourceName(), + ...otherArgs, + }); + + const orderedResources = isDescending + ? resources + : Array.from(resources).reverse(); + orderedResources.forEach(processResource); + + if (resources.length) { + const latestResource = orderedResources[0]; + if (latestResource) { + setLastDateAt(latestResource[dateField]); + } + } + + if (isFirstRun) { + setIsFirstRun(false); + } + }, +}; diff --git a/components/simla_com/sources/new-customer/new-customer.mjs b/components/simla_com/sources/new-customer/new-customer.mjs new file mode 100644 index 0000000000000..300d17ee1feda --- /dev/null +++ b/components/simla_com/sources/new-customer/new-customer.mjs @@ -0,0 +1,41 @@ +import common from "../common/polling.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + ...common, + key: "simla_com-new-customer", + name: "New Customer", + description: "Emit new event when a new customer is created. [See the documentation](https://docs.simla.com/Developers/API/APIVersions/APIv5#get--api-v5-customers).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getDateField() { + return "createdAt"; + }, + getResourceName() { + return "customers"; + }, + getResourcesFn() { + return this.app.listCustomers; + }, + getResourcesFnArgs() { + return { + params: { + limit: constants.DEFAULT_LIMIT, + filter: { + dateFrom: this.getLastDateAt(), + }, + }, + }; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Customer: ${resource.id}`, + ts: Date.parse(resource.createdAt), + }; + }, + }, +}; diff --git a/components/simla_com/sources/new-order/new-order.mjs b/components/simla_com/sources/new-order/new-order.mjs new file mode 100644 index 0000000000000..a26ea29a17710 --- /dev/null +++ b/components/simla_com/sources/new-order/new-order.mjs @@ -0,0 +1,41 @@ +import common from "../common/polling.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + ...common, + key: "simla_com-new-order", + name: "New Order", + description: "Emit new event when an order is created. [See the documentation](https://docs.simla.com/Developers/API/APIVersions/APIv5#get--api-v5-orders).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getDateField() { + return "createdAt"; + }, + getResourceName() { + return "orders"; + }, + getResourcesFn() { + return this.app.listOrders; + }, + getResourcesFnArgs() { + return { + params: { + limit: constants.DEFAULT_LIMIT, + filter: { + createdAtFrom: this.getLastDateAt(), + }, + }, + }; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Order: ${resource.id}`, + ts: Date.parse(resource.createdAt), + }; + }, + }, +}; diff --git a/components/simla_com/sources/updated-customer/updated-customer.mjs b/components/simla_com/sources/updated-customer/updated-customer.mjs new file mode 100644 index 0000000000000..da2ad005cc7bd --- /dev/null +++ b/components/simla_com/sources/updated-customer/updated-customer.mjs @@ -0,0 +1,44 @@ +import common from "../common/polling.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + ...common, + key: "simla_com-updated-customer", + name: "Updated Customer", + description: "Emit new event when a customer is updated. [See the documentation](https://docs.simla.com/Developers/API/APIVersions/APIv5#get--api-v5-customers).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + isDescendingOrder() { + return false; + }, + getDateField() { + return "createdAt"; + }, + getResourceName() { + return "history"; + }, + getResourcesFn() { + return this.app.listCustomerChangeHistory; + }, + getResourcesFnArgs() { + return { + params: { + limit: constants.DEFAULT_LIMIT, + filter: { + startDate: this.getLastDateAt(), + }, + }, + }; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `Updated Customer: ${resource.customer?.id}`, + ts: Date.parse(resource.createdAt), + }; + }, + }, +}; diff --git a/components/simple_analytics/README.md b/components/simple_analytics/README.md new file mode 100644 index 0000000000000..2645004a45a68 --- /dev/null +++ b/components/simple_analytics/README.md @@ -0,0 +1,11 @@ +# Overview + +The Simple Analytics API offers streamlined access to your website analytics data. Pipedream's integration allows you to automate actions based on this data, like triggering events when specific metrics change or combining analytics with other data sources for richer insights. By creating workflows on Pipedream, you can harness the power of Simple Analytics to monitor site performance, understand user behavior, and improve decision-making processes. + +# Example Use Cases + +- **Daily Performance Report**: Automate the daily retrieval of site metrics from Simple Analytics and send a formatted report to your team using Slack or email. Keep everyone updated on key performance indicators without manual data pulls. + +- **Real-Time Alerting**: Set up a workflow that monitors traffic spikes or drops in real-time. If Simple Analytics reports a significant change, Pipedream can trigger notifications via SMS (using Twilio) or messaging apps, allowing you to respond promptly to potential issues or opportunities. + +- **Content Optimization**: Integrate Simple Analytics with Airtable or Google Sheets to track the performance of different content pieces. Use this data to trigger a workflow that prioritizes high-performing content for further marketing efforts or SEO optimization. diff --git a/components/simplebackups/README.md b/components/simplebackups/README.md new file mode 100644 index 0000000000000..8e6fabca47909 --- /dev/null +++ b/components/simplebackups/README.md @@ -0,0 +1,11 @@ +# Overview + +SimpleBackups is a tool for automating the backup of databases and files to a cloud storage service. With its API, you can manage backups, retrieve backup information, and trigger backups programmatically. Using Pipedream, you can integrate SimpleBackups with a multitude of apps to create custom and powerful automated workflows. This can help streamline operations such as monitoring backup statuses, triggering backups based on specific events, and keeping teams informed about backup health. + +# Example Use Cases + +- **Automated Backup Triggers**: - Integrate SimpleBackups with a scheduling app on Pipedream to trigger database backups at the end of each business day. Automatically store these backups in a cloud storage solution like Amazon S3, and then use Pipedream to notify your team via Slack or email with the backup details. + +- **Backup Monitoring and Alerts**: - Connect SimpleBackups to a monitoring tool on Pipedream to track backup completion or failures. Set up a workflow that sends real-time alerts to your preferred communication platform, such as Microsoft Teams, whenever a backup job completes successfully or encounters an issue. + +- **Backup Reports and Dashboard Updates**: - Use Pipedream to create a workflow that periodically fetches the latest backup logs from SimpleBackups and compiles them into a report. This report can then be sent to a Google Sheets document, updating a live backup dashboard that your team can access for quick reference. diff --git a/components/simplebackups/package.json b/components/simplebackups/package.json new file mode 100644 index 0000000000000..73ffc02575a0e --- /dev/null +++ b/components/simplebackups/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/simplebackups", + "version": "0.0.1", + "description": "Pipedream SimpleBackups Components", + "main": "simplebackups.app.mjs", + "keywords": [ + "pipedream", + "simplebackups" + ], + "homepage": "https://pipedream.com/apps/simplebackups", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/simplebackups/simplebackups.app.mjs b/components/simplebackups/simplebackups.app.mjs new file mode 100644 index 0000000000000..2b5791c6c462e --- /dev/null +++ b/components/simplebackups/simplebackups.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "simplebackups", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/simplehash/README.md b/components/simplehash/README.md new file mode 100644 index 0000000000000..81184370442c9 --- /dev/null +++ b/components/simplehash/README.md @@ -0,0 +1,11 @@ +# Overview + +SimpleHash API offers tools to easily verify and retrieve blockchain data, simplifying the complexities involved in interacting with various blockchain protocols. With SimpleHash on Pipedream, you can automate workflows to monitor transactions, validate wallet addresses, fetch non-fungible token (NFT) metadata, and more, all in real-time. Pipedream's serverless platform lets you connect SimpleHash with hundreds of other apps to trigger actions, process data, and create powerful automations without needing to manage infrastructure. + +# Example Use Cases + +- **Blockchain Transaction Notifications**: Set up a Pipedream workflow that listens for new transactions on a specific blockchain address using SimpleHash. When a new transaction is detected, automatically send out a notification via email, Slack, or SMS with services like SendGrid, Slack, or Twilio integrated into your workflow. + +- **NFT Metadata Tracking**: Use SimpleHash to track NFT ownership changes. Create a Pipedream workflow that triggers when a specific NFT is transferred, retrieving the new owner's data and updating an Airtable or Google Sheets database with the latest information, making it easy to monitor and analyze NFT market trends. + +- **Crypto Payment Verification**: Implement a Pipedream workflow to verify cryptocurrency payments for an online store. When a customer sends a payment to the store's wallet, SimpleHash can confirm the transaction, and the workflow can then update the order status in a Shopify or WooCommerce store and email a receipt to the customer using a service like Mailgun. diff --git a/components/simplekpi/README.md b/components/simplekpi/README.md new file mode 100644 index 0000000000000..508c35bd117c7 --- /dev/null +++ b/components/simplekpi/README.md @@ -0,0 +1,11 @@ +# Overview + +The SimpleKPI API enables users to programmatically manage and access their key performance indicators (KPIs), providing an interface for operations like retrieving KPI data, updating KPI values, and managing users and reports. Leveraging the SimpleKPI API within Pipedream, you can create powerful, serverless workflows that automate the tracking and reporting of performance metrics, integrate with other apps to enrich your KPI data, or trigger actions based on performance thresholds. + +# Example Use Cases + +- **Automated KPI Reporting to Slack**: Use the SimpleKPI API on Pipedream to fetch your latest KPI data and send automated reports directly to a Slack channel. This keeps your team updated with the latest performance numbers without manual intervention. + +- **Performance-Triggered Email Alerts**: Set up a workflow where Pipedream monitors specific KPIs from SimpleKPI, and when a KPI crosses a certain threshold, trigger an email alert via SendGrid or another email service. This can be used for real-time alerts on critical business metrics. + +- **Data Enrichment with CRM Integration**: Integrate SimpleKPI with a CRM like Salesforce. Use Pipedream to pull in sales data from Salesforce, calculate KPIs, and push the results back to SimpleKPI. This ensures that your KPIs reflect the most up-to-date data from your sales pipeline. diff --git a/components/simplero/README.md b/components/simplero/README.md index 021a05b81ddc9..c657351033643 100644 --- a/components/simplero/README.md +++ b/components/simplero/README.md @@ -1,13 +1,11 @@ # Overview -The Simplero API provides all the tools to help you create, manage, and scale -your online business. With access to all of the core features of Simplero, -you've got all the power and flexibility to develop and launch powerful online -businesses. Here are a few examples of what you can do with the Simplero API: - -- Customize and manage your website or blog using the Simplero API -- Create and manage product catalogs, payment plans, and subscription plans -- Send automated emails or notifications to customers or prospects -- Create and manage shopping carts and order forms -- Track customer data including orders, payments, and activities -- Integrate your Simplero data with other third-party applications or services +The Simplero API provides a gateway to interact with Simplero's comprehensive platform for information publishers, allowing you to automate tasks related to courses, memberships, and digital products. With the API, you can manage contacts, subscriptions, products, and content, offering a means to integrate Simplero's functionalities with other services and internal systems. Via Pipedream, you can craft serverless workflows that trigger from a variety of events and connect Simplero with hundreds of other apps to streamline your digital product sales, course enrollment processes, and customer engagement strategies. + +# Example Use Cases + +- **Automated Course Enrollment**: When a new contact is added to a CRM like HubSpot, use Pipedream to enroll the contact automatically in a Simplero course, streamlining the onboarding process for new members or students. + +- **Membership Renewal Notifications**: Set up a Pipedream workflow that listens for subscription renewal events in Simplero. When a renewal is processed, trigger an automated email through SendGrid to thank the member and provide them with updated membership details. + +- **Sync Purchases to Accounting Software**: After a purchase is made in Simplero, use Pipedream to capture the sale data and sync it to accounting software like QuickBooks for real-time financial tracking and reporting. diff --git a/components/simplero/package.json b/components/simplero/package.json new file mode 100644 index 0000000000000..23a6681494408 --- /dev/null +++ b/components/simplero/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/simplero", + "version": "0.6.0", + "description": "Pipedream simplero Components", + "main": "simplero.app.mjs", + "keywords": [ + "pipedream", + "simplero" + ], + "homepage": "https://pipedream.com/apps/simplero", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/simplesat/README.md b/components/simplesat/README.md index 7d5abed622d7d..2ee860412e5c0 100644 --- a/components/simplesat/README.md +++ b/components/simplesat/README.md @@ -1,25 +1,11 @@ # Overview -Using the Simplesat API, you can access a range of insightful data and build -products to address customer feedback quickly and easily. Here are some key -features the Simplesat API can provide to help you build an effective feedback -system: +The Simplesat API allows you to automate and integrate customer feedback workflows directly with your other business processes. With this API, you can seamlessly gather customer satisfaction ratings, Net Promoter Scores, and firsthand testimonials. You can trigger actions based on feedback, sync data to CRM or support tools, and analyze customer sentiment in real-time. -- Automated Data Collection: Access responses to feedback surveys without - requiring manual export of data at any stage. -- Text Analysis: Quickly gain insights from customer feedback using sentiment - analysis or categorizing comments by topics. -- Flexible Integration: Integrate feedback data into your existing systems - quickly and painlessly. +# Example Use Cases -Here are some example applications that can be built with the Simplesat API: +- **Customer Satisfaction-Driven Support Tickets**: Automate the creation of support tickets in a tool like Zendesk when a customer submits a negative satisfaction survey, flagging the customer service team to follow up and address the concerns quickly. -- Employee Engagement Survey: Create customized surveys to measure employee - satisfaction levels, get valuable insight on areas for improvement and take - the necessary steps for improvement. -- Customer Satisfaction Score: Construct metrics from customer survey responses - to measure and track customer satisfaction in real-time. -- Monitor Social Media: Collect responses from social media posts to track - customer sentiment and feelings about your brand. -- Trend Analysis: Analyze customer feedback for trends over time to make - informed decisions about product improvement. +- **NPS Score Aggregation and Alerting**: Use Pipedream to aggregate Net Promoter Score responses into a Google Sheet for easy tracking. Set up alerts to notify the team via Slack or email when scores below a certain threshold are submitted, indicating a potential customer churn risk. + +- **Testimonial Collection and Marketing Automation**: Collect positive testimonials through the Simplesat API and automatically push them to your marketing platforms, like Mailchimp, to be featured in newsletters or on your website, enhancing social proof and brand reputation. diff --git a/components/simpletexting/README.md b/components/simpletexting/README.md index 90fc62ae79978..8eebdd6d98ce4 100644 --- a/components/simpletexting/README.md +++ b/components/simpletexting/README.md @@ -1,29 +1,11 @@ # Overview -The SimpleTexting API provides developers with tools to build powerful -applications that leverage the power of SMS. With the API, you can easily send -and receive text messages, manage contacts, create and send surveys, create -automated campaigns, and more. Here are some examples of what you can build -using the SimpleTexting API: +The SimpleTexting API offers a suite of messaging capabilities that can be leveraged to build powerful communication workflows on Pipedream. It enables automated sending of SMS and MMS messages, managing contacts, and receiving message replies. By integrating SimpleTexting with Pipedream, you can create real-time, event-driven automations that trigger SMS campaigns based on user behavior, synchronize contacts across platforms, and streamline notifications for various operational processes. -- Interactive customer service bots: Use automated text messages to create - interactive conversations with customers, providing answers to common - queries, product recommendations, and more. -- SMS alerts: Leverage automated text messages to send alerts and updates to - customers, such as ticket confirmations and order tracking. -- Automated campaign flows: Create automated workflows that consist of multiple - steps, such as sending targeted messages, setting triggers, and tracking user - response. -- Assistive scheduling: Automatically schedule messages to be sent out at a - later date, such as appointment reminders, notifications of upcoming - deadlines, and so on. -- Text-to-landline services: Create automated surveys and connect your users to - a representative via their phone. -- Two-way broadcasting: Design broadcast messages that allow users to reply - with a selection of options and create automated follow-ups based on their - response. -- Bulk messaging: Send text messages to large numbers of contacts, with - built-in tools to target specific audiences and track open and response - rates. -- Message segmentation: Group contacts into segments based on user data and - create targeted messages based on those segments. +# Example Use Cases + +- **Automated Customer Support Follow-up**: Create a workflow where after a customer support ticket is resolved in your helpdesk software (e.g., Zendesk), an SMS message is automatically sent via SimpleTexting to gather feedback on the support experience. + +- **E-commerce Order Confirmation and Updates**: Set up an automation where once a new order is placed through an e-commerce platform (e.g., Shopify), a confirmation message is sent to the customer. Further, track the order status and send real-time shipping updates via SMS as the order progresses. + +- **Event Registration Alerts**: When a participant registers for an event via an event management platform (e.g., Eventbrite), use SimpleTexting to send a confirmation message with event details, and schedule reminder texts leading up to the event date. diff --git a/components/simplybook_me/README.md b/components/simplybook_me/README.md new file mode 100644 index 0000000000000..24c31f3c399c7 --- /dev/null +++ b/components/simplybook_me/README.md @@ -0,0 +1,11 @@ +# Overview + +SimplyBook.me is a robust appointment scheduling API that enables businesses to manage bookings, clients, and services efficiently. By leveraging the SimplyBook.me API on Pipedream, users can automate the scheduling process, sync data across various platforms, and enhance customer interactions through tailored communications and streamlined workflows. This API integration facilitates real-time booking updates, client management, and service modifications, making it a vital tool for businesses aiming to optimize their appointment scheduling and operational workflows. + +# Example Use Cases + +- **Automated Appointment Confirmation Emails**: Use SimplyBook.me on Pipedream to automate the process of sending confirmation emails through SendGrid or Gmail whenever a new appointment is booked. This can include details such as date, time, and service provider to ensure clients have all the necessary information. + +- **Sync Appointments to Google Calendar**: Automatically sync new bookings from SimplyBook.me to a Google Calendar. This workflow can help manage personal or team schedules by ensuring all appointments are visible on one's primary calendar, reducing the risk of double-booking and enhancing time management. + +- **Real-Time Customer Feedback Collection**: After an appointment is completed, trigger an automated workflow on Pipedream to send a feedback request via SMS or email through Twilio or SendGrid. This helps in gathering immediate client feedback which can be used to improve services and client satisfaction. diff --git a/components/simplybook_me/package.json b/components/simplybook_me/package.json new file mode 100644 index 0000000000000..c60e8d5c28bbd --- /dev/null +++ b/components/simplybook_me/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/simplybook_me", + "version": "0.0.1", + "description": "Pipedream SimplyBook.me Components", + "main": "simplybook_me.app.mjs", + "keywords": [ + "pipedream", + "simplybook_me" + ], + "homepage": "https://pipedream.com/apps/simplybook_me", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/simplybook_me/simplybook_me.app.mjs b/components/simplybook_me/simplybook_me.app.mjs new file mode 100644 index 0000000000000..372148e76a2c6 --- /dev/null +++ b/components/simplybook_me/simplybook_me.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "simplybook_me", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/simvoly/README.md b/components/simvoly/README.md index e7b767b8e6e9a..5ec15246030a2 100644 --- a/components/simvoly/README.md +++ b/components/simvoly/README.md @@ -1,19 +1,14 @@ # Overview -Simvoly API is designed to make it easy to build and maintain websites and -online services. It provides a powerful set of tools that enable developers to -quickly create almost any type of web application. Whether you're a -professional web developer, a startup looking to get your product to market, or -just someone with an awesome idea, Simvoly API will help you bring your vision -to life. Here are a few examples of what you can build using the Simvoly API: +The Simvoly API provides a toolkit for managing and automating tasks related to your Simvoly website. With this API, you can craft customized experiences, such as programmatically adding new products to an online store, managing customer data, and automating content updates. By leveraging Pipedream, you can integrate Simvoly with a vast array of other services, creating seamless data flow and event-driven automation that streamline your web operations and marketing efforts. -- Business websites -- E-commerce stores -- Online forums -- Social media platforms -- Blogs -- Classified advertisements -- Portfolios -- Event management sites -- Online banking and payment systems -- Online gaming and entertainment sites +# Example Use Cases + +- **Automated E-commerce Product Listings** + Create a workflow in Pipedream that listens for new products in your inventory management system (like Shopify or WooCommerce). When a new product is detected, automatically add that product to your Simvoly online store, complete with descriptions, prices, and images. + +- **Dynamic Content Updates** + Trigger a Pipedream workflow with a specific schedule (e.g., daily or weekly) to fetch fresh content from a CMS like WordPress or Contentful. Use the Simvoly API to update sections of your website with the latest blog posts, news articles, or featured case studies, keeping your content dynamic and engaging. + +- **Customer Journey Tracking** + Integrate Simvoly with a CRM platform such as Salesforce or HubSpot using Pipedream. When a user signs up or performs an action on your Simvoly site, add or update their details in your CRM. Use this data to trigger personalized email campaigns or to score leads based on interaction with your website. diff --git a/components/simvoly/package.json b/components/simvoly/package.json new file mode 100644 index 0000000000000..9867a22826cef --- /dev/null +++ b/components/simvoly/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/simvoly", + "version": "0.6.0", + "description": "Pipedream simvoly Components", + "main": "simvoly.app.mjs", + "keywords": [ + "pipedream", + "simvoly" + ], + "homepage": "https://pipedream.com/apps/simvoly", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/sinch_messagemedia/package.json b/components/sinch_messagemedia/package.json new file mode 100644 index 0000000000000..43d5a259d9036 --- /dev/null +++ b/components/sinch_messagemedia/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/sinch_messagemedia", + "version": "0.0.1", + "description": "Pipedream Sinch MessageMedia Components", + "main": "sinch_messagemedia.app.mjs", + "keywords": [ + "pipedream", + "sinch_messagemedia" + ], + "homepage": "https://pipedream.com/apps/sinch_messagemedia", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/sinch_messagemedia/sinch_messagemedia.app.mjs b/components/sinch_messagemedia/sinch_messagemedia.app.mjs new file mode 100644 index 0000000000000..5d1798c421b5e --- /dev/null +++ b/components/sinch_messagemedia/sinch_messagemedia.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "sinch_messagemedia", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/siteleaf/README.md b/components/siteleaf/README.md index 8a4891eef135e..2539a19b4d54d 100644 --- a/components/siteleaf/README.md +++ b/components/siteleaf/README.md @@ -1,21 +1,11 @@ # Overview -With the Siteleaf API, you can build a wide range of powerful projects and -tools to manage your content. You can create custom applications to manage -static websites, e-commerce stores, blogs, and more. You can also build app -integrations for easy content management and advanced features like user -management and analytics. Here are some examples of what you can build with the -Siteleaf API: +The Siteleaf API facilitates content management by enabling programmatic interactions with your Siteleaf site. Via the API, you can automate content creation, updates, and retrieval, making it possible to keep your website current without manual intervention. It's designed for developers who need to streamline their content operations or integrate their Siteleaf content with other tools and services. -- Custom CMS for websites -- E-commerce stores -- Dynamic archives for blogs -- Multi-lingual sites -- Discussion forums -- User profiles and management -- Analytics and tracking -- Single page applications -- Mobile apps for content management -- Visual content authoring tools -- Automated publishing pipelines -- App integrations +# Example Use Cases + +- **Automated Content Synchronization**: Sync Siteleaf content with a database or a CMS like WordPress. When a new post is added in Siteleaf, Pipedream detects the change and automatically updates the external system with the new content. + +- **Content Backup Workflow**: Set up a scheduled Pipedream workflow that backs up your Siteleaf content to cloud storage services like Google Drive or Dropbox. This ensures your data is safe and can be restored if needed. + +- **Social Media Integration**: Automate the sharing of new Siteleaf posts to social media platforms. When you publish a new post, Pipedream triggers a workflow that posts the update to Twitter, LinkedIn, or Facebook, increasing your content's reach without extra effort. diff --git a/components/sitespeakai/README.md b/components/sitespeakai/README.md new file mode 100644 index 0000000000000..8c37e7acf1844 --- /dev/null +++ b/components/sitespeakai/README.md @@ -0,0 +1,11 @@ +# Overview + +SiteSpeakAI is a versatile API that brings voice to your digital spaces. You can use it to convert text into lifelike spoken audio in various languages and dialects. With Pipedream, you can automate and integrate this capability into various workflows, reacting to events, processing data, and connecting with other APIs or services. Whether to enhance user experience with voice responses, create audio content dynamically, or provide accessibility features, SiteSpeakAI opens up a world of audio interaction possibilities. + +# Example Use Cases + +- **Dynamic Audio Content Creation**: Process new blog posts or written content, and use SiteSpeakAI to convert them into spoken word audio files. Store these audio files, or distribute them through podcast platforms or content delivery networks for a wider reach. + +- **Voice-Enabled Notifications**: Set up a Pipedream workflow that listens to triggers such as form submissions or support tickets. Utilize SiteSpeakAI to create audio messages and integrate with messaging platforms like Twilio to send voice messages, providing a personal touch to notifications. + +- **Multilingual Support for Global Audiences**: Use SiteSpeakAI within a workflow that takes user-generated content, detects the language with Google's Cloud Translation API, and produces audio in the user's native tongue, enhancing global accessibility and user engagement. diff --git a/components/skillzrun/README.md b/components/skillzrun/README.md new file mode 100644 index 0000000000000..1fc7e98373b1d --- /dev/null +++ b/components/skillzrun/README.md @@ -0,0 +1,11 @@ +# Overview + +SkillzRun API provides robust features for managing online courses, student interactions, and educational content delivery. This API enables developers to automate educational workflows, integrate e-learning systems, and enhance student engagement through customized interactions. By integrating SkillzRun with Pipedream, users can automate processes like enrollment notifications, course updates, and dynamic content delivery, reducing manual effort and increasing operational efficiency. + +# Example Use Cases + +- **Automated Student Enrollment Notifications**: Create a workflow where SkillzRun triggers an event every time a new student enrolls in a course. Use Pipedream to catch this trigger and automate an email or SMS notification via SendGrid or Twilio, welcoming the student and providing them with necessary course materials and access details. + +- **Course Progress Tracker**: Set up a workflow where SkillzRun sends data to Pipedream about student progress in a course. Connect this with Google Sheets or Airtable to log this data. This allows educators to monitor student progress in real-time and intervene when necessary to provide additional support or resources. + +- **Feedback Collection Automation**: Implement a workflow that triggers at the end of each course module via SkillzRun, asking for student feedback. Use Pipedream to integrate this trigger with a survey tool like Typeform or Google Forms, automatically sending out feedback forms. Gathered data can be pushed to a CRM like HubSpot or Salesforce for analysis and to enhance course content based on feedback. diff --git a/components/skillzrun/actions/create-user-with-offers/create-user-with-offers.mjs b/components/skillzrun/actions/create-user-with-offers/create-user-with-offers.mjs new file mode 100644 index 0000000000000..eb6df4503fb3e --- /dev/null +++ b/components/skillzrun/actions/create-user-with-offers/create-user-with-offers.mjs @@ -0,0 +1,84 @@ +import skillzrun from "../../skillzrun.app.mjs"; + +export default { + key: "skillzrun-create-user-with-offers", + name: "Create User With Offers", + description: "Creates a new user with their associated offers in the SkillzRun app. [See the documentation](https://api.skillzrun.com/external/api/swagger/static/index.html#/users/post_external_api_users_create_with_orders)", + version: "0.0.1", + type: "action", + props: { + skillzrun, + email: { + propDefinition: [ + skillzrun, + "email", + ], + }, + name: { + propDefinition: [ + skillzrun, + "name", + ], + }, + offerIds: { + propDefinition: [ + skillzrun, + "offerIds", + ], + }, + phone: { + propDefinition: [ + skillzrun, + "phone", + ], + }, + isActive: { + propDefinition: [ + skillzrun, + "isActive", + ], + }, + seesAllSubjects: { + propDefinition: [ + skillzrun, + "seesAllSubjects", + ], + }, + ignoreNotOpenLevels: { + propDefinition: [ + skillzrun, + "ignoreNotOpenLevels", + ], + }, + ignoreStopItems: { + propDefinition: [ + skillzrun, + "ignoreStopItems", + ], + }, + noteAboutUser: { + propDefinition: [ + skillzrun, + "noteAboutUser", + ], + }, + }, + async run({ $ }) { + const response = await this.skillzrun.createUserWithOffers({ + $, + data: { + name: this.name, + email: this.email, + phone: this.phone, + isActive: this.isActive, + seesAllSubjects: this.seesAllSubjects, + ignoreNotOpenLevels: this.ignoreNotOpenLevels, + ignoreStopItems: this.ignoreStopItems, + noteAboutUser: this.noteAboutUser, + offerIds: this.offerIds.map((id) => +id), + }, + }); + $.export("$summary", `Successfully created user ${this.email}`); + return response; + }, +}; diff --git a/components/skillzrun/actions/upsert-user/upsert-user.mjs b/components/skillzrun/actions/upsert-user/upsert-user.mjs new file mode 100644 index 0000000000000..4453e29bc7cdb --- /dev/null +++ b/components/skillzrun/actions/upsert-user/upsert-user.mjs @@ -0,0 +1,105 @@ +import skillzrun from "../../skillzrun.app.mjs"; + +export default { + key: "skillzrun-upsert-user", + name: "Upsert User", + description: "Creates or updates a user based on the user email prop. [See the documentation](https://api.skillzrun.com/external/api/swagger/static/index.html#/users/post_external_api_users_upsert)", + version: "0.0.1", + type: "action", + props: { + skillzrun, + email: { + propDefinition: [ + skillzrun, + "email", + ], + }, + name: { + propDefinition: [ + skillzrun, + "name", + ], + }, + phone: { + propDefinition: [ + skillzrun, + "phone", + ], + }, + isActive: { + propDefinition: [ + skillzrun, + "isActive", + ], + }, + seesAllSubjects: { + propDefinition: [ + skillzrun, + "seesAllSubjects", + ], + }, + ignoreNotOpenLevels: { + propDefinition: [ + skillzrun, + "ignoreNotOpenLevels", + ], + }, + ignoreStopItems: { + propDefinition: [ + skillzrun, + "ignoreStopItems", + ], + }, + noteAboutUser: { + propDefinition: [ + skillzrun, + "noteAboutUser", + ], + }, + isDemoMode: { + type: "boolean", + label: "Is Demo Mode", + description: "Whether the user is in demo mode", + optional: true, + }, + aboutMe: { + type: "string", + label: "About Me", + description: "Information about the user", + optional: true, + }, + personalLink: { + type: "string", + label: "Personal Link", + description: "The url of the user's personal link", + optional: true, + }, + newPassword: { + type: "string", + label: "New Password", + description: "The password of the user", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.skillzrun.upsertUser({ + $, + data: { + name: this.name, + email: this.email, + phone: this.phone, + newPassword: this.newPassword, + isActive: this.isActive, + isDemoMode: this.isDemoMode, + seesAllSubjects: this.seesAllSubjects, + ignoreNotOpenLevels: this.ignoreNotOpenLevels, + ignoreStopItems: this.ignoreStopItems, + noteAboutUser: this.noteAboutUser, + aboutMe: this.aboutMe, + personalLink: this.personalLink, + }, + }); + $.export("$summary", `Successfully upserted user ${this.email}`); + return response; + }, +}; diff --git a/components/skillzrun/common/constants.mjs b/components/skillzrun/common/constants.mjs new file mode 100644 index 0000000000000..6414d992bb568 --- /dev/null +++ b/components/skillzrun/common/constants.mjs @@ -0,0 +1,5 @@ +const DEFAULT_LIMIT = 50; + +export default { + DEFAULT_LIMIT, +}; diff --git a/components/skillzrun/package.json b/components/skillzrun/package.json new file mode 100644 index 0000000000000..a3940ee4e1c04 --- /dev/null +++ b/components/skillzrun/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/skillzrun", + "version": "0.1.0", + "description": "Pipedream SkillzRun Components", + "main": "skillzrun.app.mjs", + "keywords": [ + "pipedream", + "skillzrun" + ], + "homepage": "https://pipedream.com/apps/skillzrun", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.4" + } +} diff --git a/components/skillzrun/skillzrun.app.mjs b/components/skillzrun/skillzrun.app.mjs new file mode 100644 index 0000000000000..14df820215d6e --- /dev/null +++ b/components/skillzrun/skillzrun.app.mjs @@ -0,0 +1,98 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "skillzrun", + propDefinitions: { + email: { + type: "string", + label: "User Email", + description: "The email of the user. This is required and must be unique.", + }, + name: { + type: "string", + label: "User Name", + description: "The name of the user", + }, + phone: { + type: "string", + label: "Phone", + description: "The phone number of the user", + optional: true, + }, + isActive: { + type: "boolean", + label: "Is Active", + description: "Whether the user is active", + optional: true, + }, + seesAllSubjects: { + type: "boolean", + label: "Sees All Subjects", + description: "Whether the user sees all offers", + optional: true, + }, + ignoreNotOpenLevels: { + type: "boolean", + label: "Ignore Not Open Levels", + description: "Whether to ignore not open levels", + optional: true, + }, + ignoreStopItems: { + type: "boolean", + label: "Ignore Stop Items", + description: "Whether to ignore stop items", + optional: true, + }, + noteAboutUser: { + type: "string", + label: "Note About User", + description: "Additional information about the user", + optional: true, + }, + offerIds: { + type: "integer[]", + label: "Offer IDs", + description: "The IDs of the associated offers.", + }, + }, + methods: { + _baseUrl() { + return "https://api.skillzrun.com/external/api"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "x-api-key": `${this.$auth.api_key}`, + }, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users", + ...opts, + }); + }, + upsertUser(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/users/upsert", + ...opts, + }); + }, + createUserWithOffers(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/users/create-with-orders", + ...opts, + }); + }, + }, +}; diff --git a/components/skillzrun/sources/new-user-created/new-user-created.mjs b/components/skillzrun/sources/new-user-created/new-user-created.mjs new file mode 100644 index 0000000000000..982f3f27c1fe3 --- /dev/null +++ b/components/skillzrun/sources/new-user-created/new-user-created.mjs @@ -0,0 +1,87 @@ +import skillzrun from "../../skillzrun.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import constants from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "skillzrun-new-user-created", + name: "New User Created", + description: "Emit new event when a new user has been created in SkillzRun. [See the documentation](https://api.skillzrun.com/external/api/swagger/static/index.html#/users/get_external_api_users_)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + skillzrun, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + emitEvent(user) { + const meta = this.generateMeta(user); + this.$emit(user, meta); + }, + generateMeta(user) { + return { + id: user.id, + summary: `New user created: ${user.name}`, + ts: user.createdAt, + }; + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + const limit = constants.DEFAULT_LIMIT; + const params = { + sort: "createdAt", + order: "DESC", + start: 0, + end: limit, + }; + let total, done, count = 0; + const users = []; + do { + const { items } = await this.skillzrun.listUsers({ + params, + }); + for (const user of items) { + if (user.createdAt >= lastTs) { + users.push(user); + count++; + if (max && count >= max) { + done = true; + break; + } + } else { + done = true; + break; + } + } + total = items?.length; + params.start += limit; + params.end += limit; + } while (total === limit && !done); + + this._setLastTs(users[0].createdAt); + users.reverse().forEach((user) => this.emitEvent(user)); + }, + }, + async run() { + await this.processEvent(); + }, + sampleEmit, +}; diff --git a/components/skillzrun/sources/new-user-created/test-event.mjs b/components/skillzrun/sources/new-user-created/test-event.mjs new file mode 100644 index 0000000000000..70475f6a51c7b --- /dev/null +++ b/components/skillzrun/sources/new-user-created/test-event.mjs @@ -0,0 +1,34 @@ +export default { + "id": 70223, + "createdAt": 1713817815015, + "updatedAt": 1713817815015, + "companyId": 3356, + "invited": true, + "unknownUser": false, + "name": "Test User", + "email": "user@example.com", + "phone": "", + "locale": "en", + "timezoneoffset": 0, + "lastOnline": null, + "device": null, + "isActive": true, + "seesAllSubjects": false, + "ignoreNotOpenLevels": false, + "ignoreStopItems": false, + "subjects": [], + "isDemoMode": false, + "demoStartAt": null, + "noteAboutUser": "", + "photo": null, + "aboutMe": "", + "personalLink": "", + "fieldsToBePublic": { + "email": true, + "phone": true, + "photo": true, + "aboutMe": true, + "personalLink": true + }, + "levelPacksAccess": [] +} \ No newline at end of file diff --git a/components/skyciv/README.md b/components/skyciv/README.md new file mode 100644 index 0000000000000..4f3a34395d8a6 --- /dev/null +++ b/components/skyciv/README.md @@ -0,0 +1,11 @@ +# Overview + +SkyCiv API offers engineering analysis and design capabilities within the cloud, allowing structural calculations and manipulations over the web. By integrating SkyCiv with Pipedream, users can automate complex engineering workflows, connect with other apps for enhanced data handling, and trigger actions based on structural analysis results. This seamless integration facilitates real-time decision-making and can significantly streamline operations in construction, architecture, and engineering projects. + +# Example Use Cases + +- **Automated Load Analysis Report Generation**: Trigger a SkyCiv analysis for a new project design uploaded to Google Drive. Once the analysis is complete, use the results to generate a report with Google Docs, and email it directly to stakeholders using Gmail. This workflow ensures that project updates and critical analyses are communicated efficiently and without manual intervention. + +- **Project Status Updates via Slack**: Configure a workflow where SkyCiv sends structural analysis updates to Slack. Whenever a structural analysis reaches a certain threshold or fails, a notification is pushed to a dedicated Slack channel, keeping the team informed and responsive to potential issues. This automation enhances project monitoring and team communication. + +- **Real-time Material Cost Estimation**: Integrate SkyCiv with a financial app like QuickBooks to automatically estimate and update the cost implications of various material choices based on the structural analysis results. Whenever a new analysis is run, update the budget in QuickBooks to reflect changes in material costs, helping maintain financial accuracy throughout the project lifecycle. diff --git a/components/skyciv/package.json b/components/skyciv/package.json new file mode 100644 index 0000000000000..04d11ca5e5e46 --- /dev/null +++ b/components/skyciv/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/skyciv", + "version": "0.0.1", + "description": "Pipedream SkyCiv Components", + "main": "skyciv.app.mjs", + "keywords": [ + "pipedream", + "skyciv" + ], + "homepage": "https://pipedream.com/apps/skyciv", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/skyciv/skyciv.app.mjs b/components/skyciv/skyciv.app.mjs new file mode 100644 index 0000000000000..27d70a91bef59 --- /dev/null +++ b/components/skyciv/skyciv.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "skyciv", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/skyvern/actions/create-run-task/create-run-task.mjs b/components/skyvern/actions/create-run-task/create-run-task.mjs new file mode 100644 index 0000000000000..c8618f6846393 --- /dev/null +++ b/components/skyvern/actions/create-run-task/create-run-task.mjs @@ -0,0 +1,80 @@ +import { parseObject } from "../../common/utils.mjs"; +import skyvern from "../../skyvern.app.mjs"; + +export default { + key: "skyvern-create-run-task", + name: "Create and Run Task", + description: "Create a new task and run it instantly in Skyvern. Useful for one-off automations. [See the documentation](https://docs.skyvern.com/)", + version: "0.0.1", + type: "action", + props: { + skyvern, + url: { + type: "string", + label: "URL", + description: "It must be a http or https URL.", + }, + navigationGoal: { + type: "string", + label: "Navigation Goal", + description: "The prompt that tells the agent what the user-facing goal is. This is the guiding light for the LLM as it navigates a particular website / sitemap to achieve this specified goal.", + optional: true, + }, + dataExtractionGoal: { + type: "string", + label: "Data Extraction Goal", + description: "The prompt that instructs the agent to extract information once the agent has achieved its **User Goal**.", + optional: true, + }, + navigationPayload: { + type: "object", + label: "Navigation Payload", + description: "JSON-formatted payload with any \"facts\" or information that would help the agent perform its job. In the case of navigating an insurance quote, this payload would include any user information to help fill out the insurance flow such as date of birth, or age they got their license, and so on. This can include nested information, and the formatting isn't validated.", + optional: true, + }, + webhookCallbackUrl: { + propDefinition: [ + skyvern, + "webhookCallbackUrl", + ], + description: "The callback URL once our system is finished processing this async task.", + optional: true, + }, + extractedInformationSchema: { + type: "object", + label: "Extracted Information Schema", + description: "Used to enforce a JSON schema spec to be enforced in the data_extraction_goal. Similar to [https://json-schema.org/](https://json-schema.org/) definition.", + optional: true, + }, + totpVerificationUrl: { + type: "string", + label: "TOTP Verification URL", + description: "The URL of your TOTP endpoint. If this field is provided, Skyvern will call the URL to fetch the TOTP/2FA/MFA code when needed.", + optional: true, + }, + totpIdentifier: { + type: "string", + label: "TOTP Identifier", + description: "The email address or the phone number which receives the TOTP/2FA/MFA code. If this field is provided, Skyvern will fetch the code that is pushed to [Skyvern's TOTP API](https://docs.skyvern.com/running-tasks/advanced-features#push-code-to-skyvern).", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.skyvern.createAndRunTask({ + $, + data: { + url: this.url, + navigation_goal: this.navigationGoal, + data_extraction_goal: this.dataExtractionGoal, + navigation_payload: parseObject(this.navigationPayload), + webhook_callback_url: this.webhookCallbackUrl, + proxyLocation: "RESIDENTIAL", + extracted_information_schema: parseObject(this.extractedInformationSchema), + totp_verification_url: this.totpVerificationUrl, + totp_identifier: this.totpIdentifier, + }, + }); + $.export("$summary", `Created and ran task with ID ${response.task_id}`); + return response; + }, +}; diff --git a/components/skyvern/actions/get-workflow/get-workflow.mjs b/components/skyvern/actions/get-workflow/get-workflow.mjs new file mode 100644 index 0000000000000..530fde4c96ed6 --- /dev/null +++ b/components/skyvern/actions/get-workflow/get-workflow.mjs @@ -0,0 +1,26 @@ +import skyvern from "../../skyvern.app.mjs"; + +export default { + key: "skyvern-get-workflow", + name: "Get Workflow Run Details", + description: "Retrieve details of runs of a specific Skyvern workflow. Useful for checking the status and result of a run. [See the documentation](https://docs.skyvern.com/workflows/getting-workflows)", + version: "0.0.1", + type: "action", + props: { + skyvern, + workflowId: { + propDefinition: [ + skyvern, + "workflowId", + ], + }, + }, + async run({ $ }) { + const response = await this.skyvern.getWorkflowRunDetails({ + $, + workflowId: this.workflowId, + }); + $.export("$summary", `Successfully retrieved run details for workflow: ${this.workflowId}`); + return response; + }, +}; diff --git a/components/skyvern/actions/run-workflow/run-workflow.mjs b/components/skyvern/actions/run-workflow/run-workflow.mjs new file mode 100644 index 0000000000000..f1bfe4fbc26bd --- /dev/null +++ b/components/skyvern/actions/run-workflow/run-workflow.mjs @@ -0,0 +1,46 @@ +import { parseObject } from "../../common/utils.mjs"; +import skyvern from "../../skyvern.app.mjs"; + +export default { + key: "skyvern-run-workflow", + name: "Run Workflow", + description: "Trigger a predefined workflow in Skyvern, allowing the execution of complex routines from Pipedream. [See the documentation](https://docs.skyvern.com/workflows/running-workflows)", + version: "0.0.1", + type: "action", + props: { + skyvern, + workflowId: { + propDefinition: [ + skyvern, + "workflowId", + ], + }, + data: { + type: "object", + label: "Data", + description: "The data field is used to pass in required and optional parameters that a workflow accepts. [See the documentation](https://docs.skyvern.com/workflows/running-workflows) for further information.", + optional: true, + }, + webhookCallbackUrl: { + propDefinition: [ + skyvern, + "webhookCallbackUrl", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.skyvern.triggerWorkflow({ + $, + workflowId: this.workflowId, + data: { + data: parseObject(this.data), + proxyLocation: "RESIDENTIAL", + webhookCallbackUrl: this.webhookCallbackUrl, + }, + }); + + $.export("$summary", `Successfully triggered workflow with ID ${response.workflow_id}`); + return response; + }, +}; diff --git a/components/skyvern/common/utils.mjs b/components/skyvern/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/skyvern/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/skyvern/package.json b/components/skyvern/package.json new file mode 100644 index 0000000000000..62d24d3d820c7 --- /dev/null +++ b/components/skyvern/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/skyvern", + "version": "0.1.0", + "description": "Pipedream Skyvern Components", + "main": "skyvern.app.mjs", + "keywords": [ + "pipedream", + "skyvern" + ], + "homepage": "https://pipedream.com/apps/skyvern", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/skyvern/skyvern.app.mjs b/components/skyvern/skyvern.app.mjs new file mode 100644 index 0000000000000..98db90739188d --- /dev/null +++ b/components/skyvern/skyvern.app.mjs @@ -0,0 +1,112 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "skyvern", + propDefinitions: { + workflowId: { + type: "string", + label: "Workflow ID", + description: "The unique identifier for a workflow", + async options({ page }) { + const workflows = await this.listWorkflows({ + params: { + page: page + 1, + }, + }); + return workflows.map(({ + workflow_permanent_id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + webhookCallbackUrl: { + type: "string", + label: "Webhook Callback URL", + description: "URL where system will send callback once it finishes executing the workflow run.", + }, + }, + methods: { + _baseUrl() { + return "https://api.skyvern.com/api/v1"; + }, + _headers() { + return { + "x-api-key": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listWorkflows({ + params, ...opts + }) { + return this._makeRequest({ + path: "/workflows", + params: { + ...params, + only_workflows: true, + }, + ...opts, + }); + }, + getWorkflowRunDetails({ + workflowId, ...opts + }) { + const path = `/workflows/${workflowId}/runs`; + return this._makeRequest({ + path, + ...opts, + }); + }, + triggerWorkflow({ + workflowId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/workflows/${workflowId}/run`, + ...opts, + }); + }, + createAndRunTask(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/tasks", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const data = await fn({ + params, + ...opts, + }); + for (const d of data) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = data.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/skyvern/sources/new-or-updated-workflow/new-or-updated-workflow.mjs b/components/skyvern/sources/new-or-updated-workflow/new-or-updated-workflow.mjs new file mode 100644 index 0000000000000..686b81f5ebebb --- /dev/null +++ b/components/skyvern/sources/new-or-updated-workflow/new-or-updated-workflow.mjs @@ -0,0 +1,67 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import skyvern from "../../skyvern.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "skyvern-new-or-updated-workflow", + name: "New or Updated Workflow", + description: "Emit new event when a workflow is created or updated in Skyvern.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + skyvern, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + + const response = this.skyvern.paginate({ + fn: this.skyvern.listWorkflows, + maxResults, + }); + + let responseArray = []; + for await (const item of response) { + if (Date.parse(item.modified_at) <= lastDate) break; + responseArray.push(item); + } + + if (responseArray.length) { + this._setLastDate(responseArray[0].modified_at); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: `${item.modified_at}-${item.workflow_permanent_id}`, + summary: `New Workflow ${item.version === 1 + ? "Created" + : "Updated"}: ${item.title}`, + ts: Date.parse(item.modified_at), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, + sampleEmit, +}; diff --git a/components/skyvern/sources/new-or-updated-workflow/test-event.mjs b/components/skyvern/sources/new-or-updated-workflow/test-event.mjs new file mode 100644 index 0000000000000..ef4fdef8db798 --- /dev/null +++ b/components/skyvern/sources/new-or-updated-workflow/test-event.mjs @@ -0,0 +1,27 @@ +export default { + "workflow_id": "string", + "organization_id": "string", + "title": "string", + "workflow_permanent_id": "string", + "version": "integer", + "is_saved_task": "boolean", + "description": "string", + "proxy_location": "string | null", + "webhook_callback_url": "string | null", + "totp_verification_url": "string | null", + "workflow_definition": { + "parameters": [ + { + "parameter_type": "string", + "key": "string", + "description": "string | null" + } + ], + "blocks": [ + { + "label": "string", + "block_type": "string" + } + ] + } +} \ No newline at end of file diff --git a/components/slack/README.md b/components/slack/README.md index 1ea197bdd53f5..72f31a7d6dcc7 100644 --- a/components/slack/README.md +++ b/components/slack/README.md @@ -1,6 +1,6 @@ # Overview -The Pipedream Slack app enables you to build event-driven workflows that interact with the Slack API. When you authorize the Pipedream app's access to your workspace, you can use [Pipedream workflows](/workflows/) to perform common Slack [actions](#workflow-actions), or [write your own code](/code/) against the Slack API. +The Pipedream Slack app enables you to build event-driven workflows that interact with the Slack API. Once you authorize the Pipedream app's access to your workspace, you can use [Pipedream workflows](/workflows/) to perform common Slack [actions](#workflow-actions) or [write your own code](/code/) against the Slack API. The Pipedream Slack app is not a typical app. You don't interact with it directly as a bot, and it doesn't add custom functionality to your workspace out of the box. It makes it easier to automate anything you'd typically use the Slack API for, using Pipedream workflows. @@ -13,29 +13,71 @@ The Pipedream Slack app is not a typical app. You don't interact with it directl ## Should I use the Slack or Slack Bot app on Pipedream? -The Slack app is the easiest and most convienent option to get started. It installs the official Pipedream bot into your Slack workspace with just a few clicks. +The Slack app is the easiest and most convenient option to get started. It installs the official Pipedream bot into your Slack workspace with just a few clicks. However, if you'd like to use your own bot registered with the [Slack API](https://api.slack.com), you can use the [Slack Bot app](https://pipedream.com/apps/slack-bot) instead. -The Slack Bot requires a bot token to allow your Pipedream workflows to authenticate as your bot. The extra set up steps allow you to list your custom bot on the Slack Marketplace, or install the bot on other workspaces as your bot's name instead of as Pipedream. +The Slack Bot requires a bot token to allow your Pipedream workflows to authenticate as your bot. The extra setup steps allow you to list your custom bot on the Slack Marketplace or install the bot on other workspaces as your bot's name instead of as Pipedream. ## Accounts 1. Visit [https://pipedream.com/accounts](https://pipedream.com/accounts). 2. Click on the **Click Here To Connect An App** button in the top-right. -3. Search for "Slack" among the list of apps, and select it. +3. Search for "Slack" among the list of apps and select it. 4. This will open a new window asking you to allow Pipedream access to your Slack workspace. Choose the right workspace where you'd like to install the app, then click **Allow**. -5. That's it! You can now use this Slack account in any [actions](#workflow-actions), or [link it to any code step](/connected-accounts/#connecting-accounts). +5. That's it! You can now use this Slack account in any [actions](#workflow-actions) or [link it to any code step](/connected-accounts/#connecting-accounts). ## Within a workflow 1. [Create a new workflow](https://pipedream.com/new). 2. Select your trigger (HTTP, Cron, etc.). -3. Click on the **+** button below the trigger step, and search for "Slack". +3. Click the **+** button below the trigger step and search for "Slack". 4. Select the **Send a Message** action. 5. Click the **Connect Account** button near the top of the step. This will prompt you to select any existing Slack accounts you've previously authenticated with Pipedream, or you can select a **New** account. Clicking **New** opens a new window asking you to allow Pipedream access to your Slack workspace. Choose the right workspace where you'd like to install the app, then click **Allow**. -6. That's it! You can now connect to the Slack API using any of the Slack actions within a Pipedream workflow. +6. After allowing access, you can connect to the Slack API using any of the Slack actions within a Pipedream workflow. + +# Example Use Cases + +- **Automated Standup Reports**: Trigger a workflow on Pipedream to collect standup updates from team members within a Slack channel at a scheduled time. The workflow compiles updates into a formatted report and posts it to a designated channel or sends it via email using an app like SendGrid. + +- **Customer Support Ticketing**: Use Pipedream to monitor a Slack support channel for new messages. On detecting a message, the workflow creates a ticket in a customer support platform like Zendesk or Jira. It can also format and forward critical information back to the Slack channel to keep the team updated. + +- **Real-time CRM Updates**: Configure a Pipedream workflow to listen for specific trigger words in sales-related Slack channels. When mentioned, the workflow fetches corresponding data from a CRM tool like Salesforce and posts the latest deal status or customer information in the Slack conversation for quick reference. + # Troubleshooting -Please [reach out](https://pipedream.com/support/) to the Pipedream team with any technical issues or questions about the Slack integration. We're happy to help! +## Error Responses + +Slack's API will always return JSON, regardless if the request was successfully processed or not. + +Each JSON response includes an `ok` boolean property indicating whether the action succeeded or failed. + +Example of a successful response: + +```json +{ + "ok": true +} +``` + +If the `ok` property is false, Slack will also include an `error` property with a short machine-readable code that describes the error. + +Example of a failure: +```json +{ + "ok": false, + "error": "invalid_parameters" +} +``` + +Additionally, if the action is successful, there's still a chance of a `warning` property in the response. This may contain a comma-separated list of warning codes. + +Example of a successful response, but with warnings: + +```json +{ + "ok": true, + "warnings": "invalid_character_set" +} +``` diff --git a/components/slack/actions/add-emoji-reaction/add-emoji-reaction.mjs b/components/slack/actions/add-emoji-reaction/add-emoji-reaction.mjs index 64b01d37e7529..f12fdea19b9bc 100644 --- a/components/slack/actions/add-emoji-reaction/add-emoji-reaction.mjs +++ b/components/slack/actions/add-emoji-reaction/add-emoji-reaction.mjs @@ -3,8 +3,8 @@ import slack from "../../slack.app.mjs"; export default { key: "slack-add-emoji-reaction", name: "Add Emoji Reaction", - description: "Add an emoji reaction to a message. [See docs here](https://api.slack.com/methods/reactions.add)", - version: "0.0.5", + description: "Add an emoji reaction to a message. [See the documentation](https://api.slack.com/methods/reactions.add)", + version: "0.0.13", type: "action", props: { slack, @@ -13,16 +13,14 @@ export default { slack, "conversation", ], - optional: false, - description: "Channel to add reaction to, or channel where the message to add reaction to was posted (used with timestamp).", + description: "Channel where the message to add reaction to was posted.", }, timestamp: { propDefinition: [ slack, - "timestamp", + "messageTs", ], - optional: false, - description: "Timestamp of the message to add reaction to.", + description: "Timestamp of the message to add reaction to. e.g. `1403051575.000407`.", }, icon_emoji: { propDefinition: [ @@ -33,11 +31,13 @@ export default { optional: false, }, }, - async run() { - return await this.slack.sdk().reactions.add({ + async run({ $ }) { + const response = await this.slack.sdk().reactions.add({ channel: this.conversation, timestamp: this.timestamp, name: this.icon_emoji, }); + $.export("$summary", `Successfully added ${this.icon_emoji} emoji reaction.`); + return response; }, }; diff --git a/components/slack/actions/add-star/add-star.mjs b/components/slack/actions/add-star/add-star.mjs deleted file mode 100644 index 3c5d984502778..0000000000000 --- a/components/slack/actions/add-star/add-star.mjs +++ /dev/null @@ -1,43 +0,0 @@ -import slack from "../../slack.app.mjs"; - -export default { - key: "slack-add-star", - name: "Add Star", - description: "Add a star to an item on behalf of the authenticated user. [See docs here](https://api.slack.com/methods/stars.add)", - version: "0.0.13", - type: "action", - props: { - slack, - conversation: { - propDefinition: [ - slack, - "conversation", - ], - optional: true, - description: "Channel to add star to, or channel where the message to add star to was posted (used with timestamp).", - }, - timestamp: { - propDefinition: [ - slack, - "timestamp", - ], - optional: true, - description: "Timestamp of the message to add star to.", - }, - file: { - propDefinition: [ - slack, - "file", - ], - optional: true, - description: "File to add star to.", - }, - }, - async run() { - return await this.slack.sdk().stars.add({ - channel: this.conversation, - timestamp: this.timestamp, - file: this.file, - }); - }, -}; diff --git a/components/slack/actions/approve-workflow/approve-workflow.mjs b/components/slack/actions/approve-workflow/approve-workflow.mjs new file mode 100644 index 0000000000000..e61bf187c26e2 --- /dev/null +++ b/components/slack/actions/approve-workflow/approve-workflow.mjs @@ -0,0 +1,87 @@ +import slack from "../../slack.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "slack-approve-workflow", + name: "Approve Workflow", + description: "Suspend the workflow until approved by a Slack message. [See the documentation](https://pipedream.com/docs/code/nodejs/rerun#flowsuspend)", + version: "0.0.2", + type: "action", + props: { + slack, + channelType: { + type: "string", + label: "Channel Type", + description: "The type of channel to send to. User/Direct Message (im), Group (mpim), Private Channel or Public Channel", + async options() { + return constants.CHANNEL_TYPE_OPTIONS; + }, + }, + conversation: { + propDefinition: [ + slack, + "conversation", + (c) => ({ + types: c.channelType === "Channels" + ? [ + constants.CHANNEL_TYPE.PUBLIC, + constants.CHANNEL_TYPE.PRIVATE, + ] + : [ + c.channelType, + ], + }), + ], + }, + message: { + type: "string", + label: "Message", + description: "Text to include with the Approve and Cancel Buttons", + }, + }, + async run({ $ }) { + const { + resume_url, cancel_url, + } = $.flow.suspend(); + + const response = await this.slack.sdk().chat.postMessage({ + text: "Click here to approve or cancel workflow", + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: this.message, + }, + }, + { + type: "actions", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: "Approve", + }, + style: "primary", + url: resume_url, + }, + { + type: "button", + text: { + type: "plain_text", + text: "Cancel", + }, + style: "danger", + url: cancel_url, + }, + ], + }, + ], + channel: this.conversation, + }); + + $.export("$summary", "Successfully sent message"); + return response; + }, +}; diff --git a/components/slack/actions/archive-channel/archive-channel.mjs b/components/slack/actions/archive-channel/archive-channel.mjs index 194a77a48266b..7afcdc75028e4 100644 --- a/components/slack/actions/archive-channel/archive-channel.mjs +++ b/components/slack/actions/archive-channel/archive-channel.mjs @@ -1,10 +1,11 @@ import slack from "../../slack.app.mjs"; +import constants from "../../common/constants.mjs"; export default { key: "slack-archive-channel", name: "Archive Channel", - description: "Archive a channel. [See docs here](https://api.slack.com/methods/conversations.archive)", - version: "0.0.13", + description: "Archive a channel. [See the documentation](https://api.slack.com/methods/conversations.archive)", + version: "0.0.21", type: "action", props: { slack, @@ -12,12 +13,21 @@ export default { propDefinition: [ slack, "conversation", + () => ({ + types: [ + constants.CHANNEL_TYPE.PUBLIC, + constants.CHANNEL_TYPE.PRIVATE, + constants.CHANNEL_TYPE.MPIM, + ], + }), ], }, }, - async run() { - return await this.slack.sdk().conversations.archive({ + async run({ $ }) { + const response = await this.slack.sdk().conversations.archive({ channel: this.conversation, }); + $.export("$summary", "Successfully archived channel."); + return response; }, }; diff --git a/components/slack/actions/common/build-blocks.mjs b/components/slack/actions/common/build-blocks.mjs index 597fbe45c6a63..fdbc9a70e4b17 100644 --- a/components/slack/actions/common/build-blocks.mjs +++ b/components/slack/actions/common/build-blocks.mjs @@ -1,13 +1,10 @@ import common from "./send-message.mjs"; -/* eslint-disable pipedream/required-properties-key, pipedream/required-properties-name, - pipedream/required-properties-version, pipedream/required-properties-description */ export default { - type: "action", props: { passArrayOrConfigure: { type: "string", - label: "Reference Existing Blocks Array or Configure Manually?", + label: "Add Blocks - Reference Existing Blocks Array or Configure Manually?", description: "Would you like to reference an array of blocks from a previous step (for example, `{{steps.blocks.$return_value}}`), or configure them in this action?", options: [ { @@ -19,6 +16,7 @@ export default { value: "configure", }, ], + optional: true, reloadProps: true, }, }, @@ -83,7 +81,8 @@ export default { } }, }, - async additionalProps() { + async additionalProps(existingProps) { + await common.additionalProps.call(this, existingProps); const props = {}; const sectionDescription = "Add a **section** block to your message and configure with plain text or mrkdwn. See [Slack's docs](https://api.slack.com/reference/block-kit/blocks?ref=bk#section) for more info."; const contextDescription = "Add a **context** block to your message and configure with plain text or mrkdwn. Define multiple items if you'd like multiple elements in the context block. See [Slack's docs](https://api.slack.com/reference/block-kit/blocks?ref=bk#context) for more info."; @@ -92,6 +91,9 @@ export default { const propsContext = this.createBlockProp("string[]", "Context Block Text", contextDescription); const propsLinkButton = this.createBlockProp("object", "Link Button", linkButtonDescription); + if (!this.passArrayOrConfigure) { + return props; + } if (this.passArrayOrConfigure == "array") { props.blocks = { type: common.props.slack.propDefinitions.blocks.type, diff --git a/components/slack/actions/common/send-message.mjs b/components/slack/actions/common/send-message.mjs index 0e1a9e62a2f47..e40b2d4545d21 100644 --- a/components/slack/actions/common/send-message.mjs +++ b/components/slack/actions/common/send-message.mjs @@ -1,9 +1,6 @@ import slack from "../../slack.app.mjs"; -/* eslint-disable pipedream/required-properties-key, pipedream/required-properties-name, - pipedream/required-properties-version, pipedream/required-properties-description */ export default { - type: "action", props: { slack, as_user: { @@ -12,62 +9,151 @@ export default { "as_user", ], }, + post_at: { + propDefinition: [ + slack, + "post_at", + ], + }, + include_sent_via_pipedream_flag: { + type: "boolean", + optional: true, + default: true, + label: "Include link to Pipedream", + description: "Defaults to `true`, includes a link to Pipedream at the end of your Slack message.", + }, + customizeBotSettings: { + type: "boolean", + label: "Customize Bot Settings", + description: "Customize the username and/or icon of the Bot", + optional: true, + reloadProps: true, + }, username: { propDefinition: [ slack, "username", ], + hidden: true, }, icon_emoji: { propDefinition: [ slack, "icon_emoji", ], + hidden: true, }, icon_url: { propDefinition: [ slack, "icon_url", ], + hidden: true, }, - post_at: { + replyToThread: { + type: "boolean", + label: "Reply to Thread", + description: "Reply to an existing thread", + optional: true, + reloadProps: true, + }, + thread_ts: { propDefinition: [ slack, - "post_at", + "messageTs", ], + description: "Provide another message's `ts` value to make this message a reply (e.g., if triggering on new Slack messages, enter `{{event.ts}}`). Avoid using a reply's `ts` value; use its parent instead. e.g. `1403051575.000407`.", + optional: true, + hidden: true, }, - include_sent_via_pipedream_flag: { + thread_broadcast: { + propDefinition: [ + slack, + "thread_broadcast", + ], + hidden: true, + }, + addMessageMetadata: { type: "boolean", + label: "Add Message Metadata", + description: "Set the metadata event type and payload", optional: true, - default: true, - label: "Include link to workflow", - description: "Defaults to `true`, includes a link to the workflow at the end of your Slack message.", + reloadProps: true, }, metadata_event_type: { propDefinition: [ slack, "metadata_event_type", ], + hidden: true, }, metadata_event_payload: { propDefinition: [ slack, "metadata_event_payload", ], + hidden: true, + }, + configureUnfurlSettings: { + type: "boolean", + label: "Configure Unfurl Settings", + description: "Configure settings for unfurling links and media", + optional: true, + reloadProps: true, }, + unfurl_links: { + propDefinition: [ + slack, + "unfurl_links", + ], + hidden: true, + }, + unfurl_media: { + propDefinition: [ + slack, + "unfurl_media", + ], + hidden: true, + }, + }, + async additionalProps(props) { + if (this.conversation && this.replyToThread) { + props.thread_ts.hidden = false; + props.thread_broadcast.hidden = false; + } + if (this.customizeBotSettings) { + props.username.hidden = false; + props.icon_emoji.hidden = false; + props.icon_url.hidden = false; + } + if (this.addMessageMetadata) { + props.metadata_event_type.hidden = false; + props.metadata_event_payload.hidden = false; + } + if (this.configureUnfurlSettings) { + props.unfurl_links.hidden = false; + props.unfurl_media.hidden = false; + } + return {}; }, methods: { _makeSentViaPipedreamBlock() { const workflowId = process.env.PIPEDREAM_WORKFLOW_ID; - // The link is a URL without a protocol to prevent link unfurling. See - // https://api.slack.com/reference/messaging/link-unfurling#classic_unfurl - const link = `https://pipedream.com/@/${workflowId}?o=a&a=slack`; + const baseLink = "https://pipedream.com"; + const linkText = !workflowId + ? "Pipedream Connect" + : "Pipedream"; + + const link = !workflowId + ? `${baseLink}/connect` + : `${baseLink}/@/${workflowId}?o=a&a=slack`; + return { "type": "context", "elements": [ { "type": "mrkdwn", - "text": `Sent via <${link}|Pipedream>`, + "text": `Sent via <${link}|${linkText}>`, }, ], }; @@ -90,6 +176,9 @@ export default { }, }; }, + getChannelId() { + return this.conversation ?? this.reply_channel; + }, }, async run({ $ }) { let blocks = this.blocks; @@ -111,7 +200,7 @@ export default { if (this.metadata_event_type) { - if (typeof metadataEventPayload === "string") { + if (typeof this.metadata_event_payload === "string") { try { metadataEventPayload = JSON.parse(this.metadata_event_payload); } catch (error) { @@ -127,7 +216,7 @@ export default { const obj = { text: this.text, - channel: this.conversation ?? this.reply_channel, + channel: await this.getChannelId(), attachments: this.attachments, unfurl_links: this.unfurl_links, unfurl_media: this.unfurl_media, @@ -139,36 +228,27 @@ export default { mrkdwn: this.mrkdwn, blocks, link_names: this.link_names, - reply_broadcast: this.reply_broadcast, + reply_broadcast: this.thread_broadcast, thread_ts: this.thread_ts, metadata: this.metadata || null, }; - console.log({ - text: this.text, - channel: this.conversation ?? this.reply_channel, - attachments: this.attachments, - unfurl_links: this.unfurl_links, - unfurl_media: this.unfurl_media, - parse: this.parse, - as_user: this.as_user, - username: this.username, - icon_emoji: this.icon_emoji, - icon_url: this.icon_url, - mrkdwn: this.mrkdwn, - blocks, - link_names: this.link_names, - reply_broadcast: this.reply_broadcast, - thread_ts: this.thread_ts, - metadata: this.metadata || null, - }); - if (this.post_at) { - obj.post_at = this.post_at; + obj.post_at = Math.floor(new Date(this.post_at).getTime() / 1000); return await this.slack.sdk().chat.scheduleMessage(obj); } const resp = await this.slack.sdk().chat.postMessage(obj); - $.export("$summary", "Successfully sent a message to channel ID " + resp.channel); + const { channel } = await this.slack.conversationsInfo({ + channel: resp.channel, + }); + let channelName = `#${channel?.name}`; + if (channel.is_im) { + const usernames = await this.slack.userNames(); + channelName = `@${usernames[channel.user]}`; + } else if (channel.is_mpim) { + channelName = `@${channel.purpose.value}`; + } + $.export("$summary", `Successfully sent a message to ${channelName}`); return resp; }, }; diff --git a/components/slack/actions/complete-reminder/complete-reminder.mjs b/components/slack/actions/complete-reminder/complete-reminder.mjs deleted file mode 100644 index 89a0ca5d89551..0000000000000 --- a/components/slack/actions/complete-reminder/complete-reminder.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import slack from "../../slack.app.mjs"; - -export default { - key: "slack-complete-reminder", - name: "Complete Reminder", - description: "Complete a reminder. [See docs here](https://api.slack.com/methods/reminders.complete)", - version: "0.0.13", - type: "action", - props: { - slack, - reminder: { - propDefinition: [ - slack, - "reminder", - ], - }, - }, - async run() { - return await this.slack.sdk().reminders.complete({ - reminder: this.reminder, - }); - }, -}; diff --git a/components/slack/actions/create-channel/create-channel.mjs b/components/slack/actions/create-channel/create-channel.mjs index 79b2cc699fca3..7e9c3bb886357 100644 --- a/components/slack/actions/create-channel/create-channel.mjs +++ b/components/slack/actions/create-channel/create-channel.mjs @@ -3,8 +3,8 @@ import slack from "../../slack.app.mjs"; export default { key: "slack-create-channel", name: "Create a Channel", - description: "Create a new channel. [See docs here](https://api.slack.com/methods/conversations.create)", - version: "0.0.14", + description: "Create a new channel. [See the documentation](https://api.slack.com/methods/conversations.create)", + version: "0.0.22", type: "action", props: { slack, @@ -21,10 +21,15 @@ export default { optional: true, }, }, - async run() { - return await this.slack.sdk().conversations.create({ - name: this.channelName, + async run({ $ }) { + // parse name + const name = this.channelName.replace(/\s+/g, "-").toLowerCase(); + + const response = await this.slack.sdk().conversations.create({ + name, is_private: this.isPrivate, }); + $.export("$summary", `Successfully created channel ${this.channelName}`); + return response; }, }; diff --git a/components/slack/actions/create-reminder/create-reminder.mjs b/components/slack/actions/create-reminder/create-reminder.mjs index 4a5bb72253ba4..4537de5e9fb5d 100644 --- a/components/slack/actions/create-reminder/create-reminder.mjs +++ b/components/slack/actions/create-reminder/create-reminder.mjs @@ -3,8 +3,8 @@ import slack from "../../slack.app.mjs"; export default { key: "slack-create-reminder", name: "Create Reminder", - description: "Create a reminder. [See docs here](https://api.slack.com/methods/reminders.add)", - version: "0.0.14", + description: "Create a reminder. [See the documentation](https://api.slack.com/methods/reminders.add)", + version: "0.0.22", type: "action", props: { slack, @@ -15,10 +15,8 @@ export default { ], }, timestamp: { - propDefinition: [ - slack, - "timestamp", - ], + type: "string", + label: "Timestamp", description: "When this reminder should happen: the Unix timestamp (up to five years from now), the number of seconds until the reminder (if within 24 hours), or a natural language description (Ex. in 15 minutes, or every Thursday)", }, team_id: { @@ -36,12 +34,14 @@ export default { optional: true, }, }, - async run() { - return await this.slack.sdk().reminders.add({ + async run({ $ }) { + const response = await this.slack.sdk().reminders.add({ text: this.text, team_id: this.team_id, time: this.timestamp, user: this.user, }); + $.export("$summary", `Successfully created reminder with ID ${response.reminder.id}`); + return response; }, }; diff --git a/components/slack/actions/delete-file/delete-file.mjs b/components/slack/actions/delete-file/delete-file.mjs index d16d521ca1e26..79c0f6fd3eca1 100644 --- a/components/slack/actions/delete-file/delete-file.mjs +++ b/components/slack/actions/delete-file/delete-file.mjs @@ -3,8 +3,8 @@ import slack from "../../slack.app.mjs"; export default { key: "slack-delete-file", name: "Delete File", - description: "Delete a file. [See docs here](https://api.slack.com/methods/files.delete)", - version: "0.0.13", + description: "Delete a file. [See the documentation](https://api.slack.com/methods/files.delete)", + version: "0.0.21", type: "action", props: { slack, @@ -15,9 +15,11 @@ export default { ], }, }, - async run() { - return await this.slack.sdk().files.delete({ + async run({ $ }) { + const response = await this.slack.sdk().files.delete({ file: this.file, }); + $.export("$summary", `Successfully deleted file with ID ${this.file}`); + return response; }, }; diff --git a/components/slack/actions/delete-message/delete-message.mjs b/components/slack/actions/delete-message/delete-message.mjs index 2ed06dec0b3a8..8b022b1375518 100644 --- a/components/slack/actions/delete-message/delete-message.mjs +++ b/components/slack/actions/delete-message/delete-message.mjs @@ -3,8 +3,8 @@ import slack from "../../slack.app.mjs"; export default { key: "slack-delete-message", name: "Delete Message", - description: "Delete a message. [See docs here](https://api.slack.com/methods/chat.delete)", - version: "0.0.13", + description: "Delete a message. [See the documentation](https://api.slack.com/methods/chat.delete)", + version: "0.0.21", type: "action", props: { slack, @@ -17,7 +17,7 @@ export default { timestamp: { propDefinition: [ slack, - "timestamp", + "messageTs", ], }, as_user: { @@ -28,11 +28,13 @@ export default { description: "Pass true to update the message as the authed user. Bot users in this context are considered authed users.", }, }, - async run() { - return await this.slack.sdk().chat.delete({ + async run({ $ }) { + const response = await this.slack.sdk().chat.delete({ channel: this.conversation, ts: this.timestamp, as_user: this.as_user, }); + $.export("$summary", "Successfully deleted message."); + return response; }, }; diff --git a/components/slack/actions/delete-reminder/delete-reminder.mjs b/components/slack/actions/delete-reminder/delete-reminder.mjs deleted file mode 100644 index 5ffdfa40de296..0000000000000 --- a/components/slack/actions/delete-reminder/delete-reminder.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import slack from "../../slack.app.mjs"; - -export default { - key: "slack-delete-reminder", - name: "Delete Reminder", - description: "Delete a reminder. [See docs here](https://api.slack.com/methods/reminders.delete)", - version: "0.0.13", - type: "action", - props: { - slack, - reminder: { - propDefinition: [ - slack, - "reminder", - ], - }, - }, - async run() { - return await this.slack.sdk().reminders.delete({ - reminder: this.reminder, - }); - }, -}; diff --git a/components/slack/actions/find-message/find-message.mjs b/components/slack/actions/find-message/find-message.mjs index 0e59f69358ef2..fd711732d4919 100644 --- a/components/slack/actions/find-message/find-message.mjs +++ b/components/slack/actions/find-message/find-message.mjs @@ -3,8 +3,8 @@ import slack from "../../slack.app.mjs"; export default { key: "slack-find-message", name: "Find Message", - description: "Find a Slack message. [See docs here](https://api.slack.com/methods/search.messages)", - version: "0.0.13", + description: "Find a Slack message. [See the documentation](https://api.slack.com/methods/search.messages)", + version: "0.0.21", type: "action", props: { slack, @@ -14,13 +14,6 @@ export default { "query", ], }, - count: { - propDefinition: [ - slack, - "count", - ], - optional: true, - }, teamId: { propDefinition: [ slack, @@ -29,11 +22,26 @@ export default { optional: true, }, }, - async run() { - return this.slack.searchMessages({ + async run({ $ }) { + const matches = []; + const params = { query: this.query, - count: this.count, team_id: this.teamId, - }); + page: 1, + }; + let hasMore; + + do { + const { messages } = await this.slack.searchMessages(params); + matches.push(...messages.matches); + hasMore = messages?.length; + params.page++; + } while (hasMore); + + $.export("$summary", `Found ${matches.length} matching message${matches.length === 1 + ? "" + : "s"}`); + + return matches; }, }; diff --git a/components/slack/actions/find-user-by-email/find-user-by-email.mjs b/components/slack/actions/find-user-by-email/find-user-by-email.mjs index e4dfc44c71e40..728ffd6b374f0 100644 --- a/components/slack/actions/find-user-by-email/find-user-by-email.mjs +++ b/components/slack/actions/find-user-by-email/find-user-by-email.mjs @@ -3,8 +3,8 @@ import slack from "../../slack.app.mjs"; export default { key: "slack-find-user-by-email", name: "Find User by Email", - description: "Find a user by matching against their email. [See docs here](https://api.slack.com/methods/users.lookupByEmail)", - version: "0.0.13", + description: "Find a user by matching against their email. [See the documentation](https://api.slack.com/methods/users.lookupByEmail)", + version: "0.0.21", type: "action", props: { slack, @@ -15,9 +15,13 @@ export default { ], }, }, - async run() { - return await this.slack.sdk().users.lookupByEmail({ + async run({ $ }) { + const response = await this.slack.sdk().users.lookupByEmail({ email: this.email, }); + if (response.ok) { + $.export("$summary", `Successfully found user with ID ${response.user.id}`); + } + return response; }, }; diff --git a/components/slack/actions/get-channel/get-channel.mjs b/components/slack/actions/get-channel/get-channel.mjs deleted file mode 100644 index 759d970d02785..0000000000000 --- a/components/slack/actions/get-channel/get-channel.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import slack from "../../slack.app.mjs"; - -export default { - key: "slack-get-channel", - name: "Get Channel", - description: "Return information about a workspace channel. [See docs here](https://api.slack.com/methods/conversations.info)", - version: "0.0.13", - type: "action", - props: { - slack, - conversation: { - propDefinition: [ - slack, - "conversation", - ], - }, - }, - async run() { - return await this.slack.sdk().conversations.info({ - channel: this.conversation, - }); - }, -}; diff --git a/components/slack/actions/get-file/get-file.mjs b/components/slack/actions/get-file/get-file.mjs index e2cd202a6cc95..decd64e9b9d0c 100644 --- a/components/slack/actions/get-file/get-file.mjs +++ b/components/slack/actions/get-file/get-file.mjs @@ -3,8 +3,8 @@ import slack from "../../slack.app.mjs"; export default { key: "slack-get-file", name: "Get File", - description: "Return information about a file. [See docs here](https://api.slack.com/methods/files.info)", - version: "0.0.13", + description: "Return information about a file. [See the documentation](https://api.slack.com/methods/files.info)", + version: "0.0.21", type: "action", props: { slack, @@ -14,17 +14,12 @@ export default { "file", ], }, - count: { - propDefinition: [ - slack, - "count", - ], - }, }, - async run() { - return await this.slack.sdk().files.info({ + async run({ $ }) { + const response = await this.slack.sdk().files.info({ file: this.file, - count: this.count, }); + $.export("$summary", `Successfully retrieved file with ID ${this.file}`); + return response; }, }; diff --git a/components/slack/actions/get-reminder/get-reminder.mjs b/components/slack/actions/get-reminder/get-reminder.mjs deleted file mode 100644 index 280ba5015709b..0000000000000 --- a/components/slack/actions/get-reminder/get-reminder.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import slack from "../../slack.app.mjs"; - -export default { - key: "slack-get-reminder", - name: "Get Reminder", - description: "Return information about a reminder. [See docs here](https://api.slack.com/methods/reminders.info)", - version: "0.0.13", - type: "action", - props: { - slack, - reminder: { - propDefinition: [ - slack, - "reminder", - ], - }, - }, - async run() { - return await this.slack.sdk().reminders.info({ - reminder: this.reminder, - }); - }, -}; diff --git a/components/slack/actions/invite-user-to-channel/invite-user-to-channel.mjs b/components/slack/actions/invite-user-to-channel/invite-user-to-channel.mjs index 88ae3d08276cd..b47dbe7df5bdc 100644 --- a/components/slack/actions/invite-user-to-channel/invite-user-to-channel.mjs +++ b/components/slack/actions/invite-user-to-channel/invite-user-to-channel.mjs @@ -3,8 +3,8 @@ import slack from "../../slack.app.mjs"; export default { key: "slack-invite-user-to-channel", name: "Invite User to Channel", - description: "Invite a user to an existing channel. [See docs here](https://api.slack.com/methods/conversations.invite)", - version: "0.0.13", + description: "Invite a user to an existing channel. [See the documentation](https://api.slack.com/methods/conversations.invite)", + version: "0.0.21", type: "action", props: { slack, @@ -21,10 +21,12 @@ export default { ], }, }, - async run() { - return await this.slack.sdk().conversations.invite({ + async run({ $ }) { + const response = await this.slack.sdk().conversations.invite({ channel: this.conversation, users: this.user, }); + $.export("$summary", `Successfully invited user ${this.user} to channel with ID ${this.conversation}`); + return response; }, }; diff --git a/components/slack/actions/join-channel/join-channel.mjs b/components/slack/actions/join-channel/join-channel.mjs deleted file mode 100644 index 8904ebe49e6e6..0000000000000 --- a/components/slack/actions/join-channel/join-channel.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import slack from "../../slack.app.mjs"; - -export default { - key: "slack-join-channel", - name: "Join Channel", - description: "Join an existing channel. [See docs here](https://api.slack.com/methods/conversations.join)", - version: "0.0.13", - type: "action", - props: { - slack, - conversation: { - propDefinition: [ - slack, - "conversation", - ], - }, - }, - async run() { - return await this.slack.sdk().conversations.join({ - channel: this.conversation, - }); - }, -}; diff --git a/components/slack/actions/kick-user/kick-user.mjs b/components/slack/actions/kick-user/kick-user.mjs index 716599556bc3e..fb5a9f50616a6 100644 --- a/components/slack/actions/kick-user/kick-user.mjs +++ b/components/slack/actions/kick-user/kick-user.mjs @@ -1,10 +1,11 @@ import slack from "../../slack.app.mjs"; +import constants from "../../common/constants.mjs"; export default { key: "slack-kick-user", name: "Kick User", - description: "Remove a user from a conversation. [See docs here](https://api.slack.com/methods/conversations.kick)", - version: "0.0.13", + description: "Remove a user from a conversation. [See the documentation](https://api.slack.com/methods/conversations.kick)", + version: "0.0.21", type: "action", props: { slack, @@ -12,19 +13,31 @@ export default { propDefinition: [ slack, "conversation", + () => ({ + types: [ + constants.CHANNEL_TYPE.PUBLIC, + constants.CHANNEL_TYPE.PRIVATE, + constants.CHANNEL_TYPE.MPIM, + ], + }), ], }, user: { propDefinition: [ slack, "user", + (c) => ({ + channelId: c.conversation, + }), ], }, }, - async run() { - return await this.slack.sdk().conversations.kick({ + async run({ $ }) { + const response = await this.slack.sdk().conversations.kick({ channel: this.conversation, user: this.user, }); + $.export("$summary", `Successfully kicked user ${this.user} from channel with ID ${this.conversation}`); + return response; }, }; diff --git a/components/slack/actions/leave-channel/leave-channel.mjs b/components/slack/actions/leave-channel/leave-channel.mjs deleted file mode 100644 index fba7b1f888fec..0000000000000 --- a/components/slack/actions/leave-channel/leave-channel.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import slack from "../../slack.app.mjs"; - -export default { - key: "slack-leave-channel", - name: "Leave Channel", - description: "Leave an existing channel. [See docs here](https://api.slack.com/methods/conversations.leave)", - version: "0.0.13", - type: "action", - props: { - slack, - conversation: { - propDefinition: [ - slack, - "conversation", - ], - }, - }, - async run() { - return await this.slack.sdk().conversations.leave({ - channel: this.conversation, - }); - }, -}; diff --git a/components/slack/actions/list-channels/list-channels.mjs b/components/slack/actions/list-channels/list-channels.mjs index 9ef07a00684a8..860e217fe0919 100644 --- a/components/slack/actions/list-channels/list-channels.mjs +++ b/components/slack/actions/list-channels/list-channels.mjs @@ -3,13 +3,17 @@ import slack from "../../slack.app.mjs"; export default { key: "slack-list-channels", name: "List Channels", - description: "Return a list of all channels in a workspace. [See docs here](https://api.slack.com/methods/conversations.list)", - version: "0.0.13", + description: "Return a list of all channels in a workspace. [See the documentation](https://api.slack.com/methods/conversations.list)", + version: "0.0.21", type: "action", props: { slack, }, - async run() { - return await this.slack.sdk().conversations.list(); + async run({ $ }) { + const response = await this.slack.sdk().conversations.list(); + $.export("$summary", `Successfully found ${response.length} channel${response.length === 1 + ? "" + : "s"}`); + return response; }, }; diff --git a/components/slack/actions/list-files/list-files.mjs b/components/slack/actions/list-files/list-files.mjs index 52428c3c33a56..f98cea79bda54 100644 --- a/components/slack/actions/list-files/list-files.mjs +++ b/components/slack/actions/list-files/list-files.mjs @@ -3,8 +3,8 @@ import slack from "../../slack.app.mjs"; export default { key: "slack-list-files", name: "List Files", - description: "Return a list of files within a team. [See docs here](https://api.slack.com/methods/files.list)", - version: "0.0.41", + description: "Return a list of files within a team. [See the documentation](https://api.slack.com/methods/files.list)", + version: "0.0.49", type: "action", props: { slack, @@ -14,13 +14,6 @@ export default { "conversation", ], }, - count: { - propDefinition: [ - slack, - "count", - ], - optional: true, - }, team_id: { propDefinition: [ slack, @@ -36,12 +29,27 @@ export default { optional: true, }, }, - async run() { - return await this.slack.sdk().files.list({ + async run({ $ }) { + const allFiles = []; + const params = { channel: this.conversation, - count: this.count, user: this.user, team_id: this.team_id, - }); + page: 1, + }; + let hasMore; + + do { + const { files } = await this.slack.sdk().files.list(params); + allFiles.push(...files); + hasMore = files.length; + params.page++; + } while (hasMore); + + $.export("$summary", `Successfully retrieved ${allFiles.length} file${allFiles.length === 1 + ? "" + : "s"}`); + + return allFiles; }, }; diff --git a/components/slack/actions/list-group-members/list-group-members.mjs b/components/slack/actions/list-group-members/list-group-members.mjs new file mode 100644 index 0000000000000..07acd355d3f88 --- /dev/null +++ b/components/slack/actions/list-group-members/list-group-members.mjs @@ -0,0 +1,42 @@ +import slack from "../../slack.app.mjs"; + +export default { + key: "slack-list-group-members", + name: "List Group Members", + description: "List all users in a User Group. [See the documentation](https://api.slack.com/methods/usergroups.users.list)", + version: "0.0.6", + type: "action", + props: { + slack, + userGroup: { + propDefinition: [ + slack, + "userGroup", + ], + }, + team: { + propDefinition: [ + slack, + "team", + ], + optional: true, + description: "Encoded team id where the user group exists, required if org token is used.", + }, + }, + async run({ $ }) { + const { + userGroup, + team, + } = this; + const response = await this.slack.sdk().usergroups.users.list({ + usergroup: userGroup, + team_id: team, + }); + if (response.users?.length) { + $.export("$summary", `Successfully retrieved ${response.users.length} user${response.users.length === 1 + ? "" + : "s"}`); + } + return response; + }, +}; diff --git a/components/slack/actions/list-members-in-channel/list-members-in-channel.mjs b/components/slack/actions/list-members-in-channel/list-members-in-channel.mjs index 9892bbce9c882..a848c49b6f25b 100644 --- a/components/slack/actions/list-members-in-channel/list-members-in-channel.mjs +++ b/components/slack/actions/list-members-in-channel/list-members-in-channel.mjs @@ -3,8 +3,8 @@ import slack from "../../slack.app.mjs"; export default { key: "slack-list-members-in-channel", name: "List Members in Channel", - description: "Retrieve members of a channel. [See docs here](https://api.slack.com/methods/conversations.members)", - version: "0.0.13", + description: "Retrieve members of a channel. [See the documentation](https://api.slack.com/methods/conversations.members)", + version: "0.0.21", type: "action", props: { slack, @@ -14,10 +14,28 @@ export default { "conversation", ], }, + returnUsernames: { + type: "boolean", + label: "Return Usernames", + description: "Optionally, return usernames in addition to IDs", + optional: true, + }, }, - async run() { - return await this.slack.sdk().conversations.members({ + async run({ $ }) { + const { members } = await this.slack.sdk().conversations.members({ channel: this.conversation, }); + let channelMembers = members; + if (this.returnUsernames) { + const usernames = await this.slack.userNames(); + channelMembers = channelMembers?.map((id) => ({ + id, + username: usernames[id], + })) || []; + } + $.export("$summary", `Successfully retrieved ${channelMembers.length} member${channelMembers.length === 1 + ? "" + : "s"}`); + return channelMembers; }, }; diff --git a/components/slack/actions/list-reminders/list-reminders.mjs b/components/slack/actions/list-reminders/list-reminders.mjs deleted file mode 100644 index e42d599cefc74..0000000000000 --- a/components/slack/actions/list-reminders/list-reminders.mjs +++ /dev/null @@ -1,24 +0,0 @@ -import slack from "../../slack.app.mjs"; - -export default { - key: "slack-list-reminders", - name: "List Reminders", - description: "List all reminders for a given user. [See docs here](https://api.slack.com/methods/reminders.list)", - version: "0.0.13", - type: "action", - props: { - slack, - team_id: { - propDefinition: [ - slack, - "team", - ], - optional: true, - }, - }, - async run() { - return await this.slack.sdk().reminders.list({ - team_id: this.team_id, - }); - }, -}; diff --git a/components/slack/actions/list-replies/list-replies.mjs b/components/slack/actions/list-replies/list-replies.mjs index 350069e687683..4ad82785cb1c9 100644 --- a/components/slack/actions/list-replies/list-replies.mjs +++ b/components/slack/actions/list-replies/list-replies.mjs @@ -3,8 +3,8 @@ import slack from "../../slack.app.mjs"; export default { key: "slack-list-replies", name: "List Replies", - description: "Retrieve a thread of messages posted to a conversation. [See docs here](https://api.slack.com/methods/conversations.replies)", - version: "0.0.13", + description: "Retrieve a thread of messages posted to a conversation. [See the documentation](https://api.slack.com/methods/conversations.replies)", + version: "0.0.21", type: "action", props: { slack, @@ -17,14 +17,18 @@ export default { timestamp: { propDefinition: [ slack, - "timestamp", + "messageTs", ], }, }, - async run() { - return await this.slack.sdk().conversations.replies({ + async run({ $ }) { + const response = await this.slack.sdk().conversations.replies({ channel: this.conversation, ts: this.timestamp, }); + $.export("$summary", `Successfully retrieved ${response.messages.length} reply message${response.messages.length === 1 + ? "" + : "s"}`); + return response; }, }; diff --git a/components/slack/actions/list-user-groups-users/list-user-groups-users.mjs b/components/slack/actions/list-user-groups-users/list-user-groups-users.mjs deleted file mode 100644 index 20697a6a12900..0000000000000 --- a/components/slack/actions/list-user-groups-users/list-user-groups-users.mjs +++ /dev/null @@ -1,36 +0,0 @@ -import slack from "../../slack.app.mjs"; - -export default { - key: "slack-list-user-groups-users", - name: "List User Groups Users", - description: "List all users in a User Group. [See docs here](https://api.slack.com/methods/usergroups.users.list)", - version: "0.0.6", - type: "action", - props: { - slack, - userGroup: { - propDefinition: [ - slack, - "userGroup", - ], - }, - team: { - propDefinition: [ - slack, - "team", - ], - optional: true, - description: "Encoded team id where the user group exists, required if org token is used.", - }, - }, - async run() { - const { - userGroup, - team, - } = this; - return await this.slack.sdk().usergroups.users.list({ - usergroup: userGroup, - team_id: team, - }); - }, -}; diff --git a/components/slack/actions/list-users/list-users.mjs b/components/slack/actions/list-users/list-users.mjs index a659eba606c25..8620fc846a02c 100644 --- a/components/slack/actions/list-users/list-users.mjs +++ b/components/slack/actions/list-users/list-users.mjs @@ -3,8 +3,8 @@ import slack from "../../slack.app.mjs"; export default { key: "slack-list-users", name: "List Users", - description: "Return a list of all users in a workspace. [See docs here](https://api.slack.com/methods/users.list)", - version: "0.0.13", + description: "Return a list of all users in a workspace. [See the documentation](https://api.slack.com/methods/users.list)", + version: "0.0.21", type: "action", props: { slack, @@ -16,9 +16,13 @@ export default { optional: true, }, }, - async run() { - return this.slack.usersList({ + async run({ $ }) { + const response = await this.slack.usersList({ team_id: this.teamId, }); + $.export("$summary", `Successfully retrieved ${response.members.length} user${response.members.length === 1 + ? "" + : "s"}`); + return response; }, }; diff --git a/components/slack/actions/remove-star/remove-star.mjs b/components/slack/actions/remove-star/remove-star.mjs deleted file mode 100644 index f51587ecf719e..0000000000000 --- a/components/slack/actions/remove-star/remove-star.mjs +++ /dev/null @@ -1,40 +0,0 @@ -import slack from "../../slack.app.mjs"; - -export default { - key: "slack-remove-star", - name: "Remove Star", - description: "Remove a star from an item on behalf of the authenticated user. [See docs here](https://api.slack.com/methods/stars.remove)", - version: "0.0.13", - type: "action", - props: { - slack, - conversation: { - propDefinition: [ - slack, - "conversation", - ], - optional: true, - }, - timestamp: { - propDefinition: [ - slack, - "timestamp", - ], - optional: true, - }, - file: { - propDefinition: [ - slack, - "file", - ], - optional: true, - }, - }, - async run() { - return await this.slack.sdk().stars.remove({ - channel: this.conversation, - timestamp: this.timestamp, - file: this.file, - }); - }, -}; diff --git a/components/slack/actions/reply-to-a-message/reply-to-a-message.mjs b/components/slack/actions/reply-to-a-message/reply-to-a-message.mjs index ef75920a23889..ad06554331d93 100644 --- a/components/slack/actions/reply-to-a-message/reply-to-a-message.mjs +++ b/components/slack/actions/reply-to-a-message/reply-to-a-message.mjs @@ -6,43 +6,44 @@ export default { key: "slack-reply-to-a-message", name: "Reply to a Message Thread", description: "Send a message as a threaded reply. See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here", - version: "0.1.18", + version: "0.1.26", type: "action", props: { slack: common.props.slack, - thread_ts: { + conversation: { propDefinition: [ slack, - "thread_ts", + "conversation", ], - optional: false, }, - reply_broadcast: { + text: { propDefinition: [ slack, - "reply_broadcast", + "text", ], }, - conversation: { + mrkdwn: { propDefinition: [ slack, - "conversation", + "mrkdwn", ], - optional: false, }, - text: { + ...common.props, + replyToThread: { + ...common.props.replyToThread, + hidden: true, + }, + thread_ts: { propDefinition: [ slack, - "text", + "messageTs", ], - optional: false, }, - mrkdwn: { + thread_broadcast: { propDefinition: [ slack, - "mrkdwn", + "thread_broadcast", ], }, - ...common.props, }, }; diff --git a/components/slack/actions/send-block-kit-message/send-block-kit-message.mjs b/components/slack/actions/send-block-kit-message/send-block-kit-message.mjs index fae21eac2a3d9..a90531597abdf 100644 --- a/components/slack/actions/send-block-kit-message/send-block-kit-message.mjs +++ b/components/slack/actions/send-block-kit-message/send-block-kit-message.mjs @@ -1,14 +1,14 @@ -import common from "../common/send-message.mjs"; import buildBlocks from "../common/build-blocks.mjs"; +import common from "../common/send-message.mjs"; export default { ...common, ...buildBlocks, - name: "Build and Send a Block Kit Message (Beta)", - description: "Configure custom blocks and send to a channel, group, or user. [See Slack's docs for more info](https://api.slack.com/tools/block-kit-builder).", - version: "0.3.0", - type: "action", key: "slack-send-block-kit-message", + name: "Build and Send a Block Kit Message", + description: "Configure custom blocks and send to a channel, group, or user. [See the documentation](https://api.slack.com/tools/block-kit-builder).", + version: "0.4.2", + type: "action", props: { slack: common.props.slack, conversation: { @@ -18,10 +18,10 @@ export default { ], }, text: { - propDefinition: [ - common.props.slack, - "notificationText", - ], + type: "string", + label: "Notification Text", + description: "Optionally provide a string for Slack to display as the new message notification (if you do not provide this, notification will be blank).", + optional: true, }, ...common.props, ...buildBlocks.props, diff --git a/components/slack/actions/send-custom-message/send-custom-message.mjs b/components/slack/actions/send-custom-message/send-custom-message.mjs deleted file mode 100644 index a90e2f22df4eb..0000000000000 --- a/components/slack/actions/send-custom-message/send-custom-message.mjs +++ /dev/null @@ -1,80 +0,0 @@ -import common from "../common/send-message.mjs"; - -export default { - ...common, - key: "slack-send-custom-message", - name: "Send a Custom Message", - description: "Customize advanced setttings and send a message to a channel, group or user. See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here", - version: "0.2.17", - type: "action", - props: { - slack: common.props.slack, - conversation: { - propDefinition: [ - common.props.slack, - "conversation", - ], - }, - text: { - propDefinition: [ - common.props.slack, - "text", - ], - }, - mrkdwn: { - propDefinition: [ - common.props.slack, - "mrkdwn", - ], - }, - attachments: { - propDefinition: [ - common.props.slack, - "attachments", - ], - }, - unfurl_links: { - propDefinition: [ - common.props.slack, - "unfurl_links", - ], - }, - unfurl_media: { - propDefinition: [ - common.props.slack, - "unfurl_media", - ], - }, - parse: { - propDefinition: [ - common.props.slack, - "parse", - ], - }, - blocks: { - propDefinition: [ - common.props.slack, - "blocks", - ], - }, - link_names: { - propDefinition: [ - common.props.slack, - "link_names", - ], - }, - reply_broadcast: { - propDefinition: [ - common.props.slack, - "reply_broadcast", - ], - }, - thread_ts: { - propDefinition: [ - common.props.slack, - "thread_ts", - ], - }, - ...common.props, - }, -}; diff --git a/components/slack/actions/send-direct-message/send-direct-message.mjs b/components/slack/actions/send-direct-message/send-direct-message.mjs deleted file mode 100644 index 318e6dbf898fb..0000000000000 --- a/components/slack/actions/send-direct-message/send-direct-message.mjs +++ /dev/null @@ -1,53 +0,0 @@ -import common from "../common/send-message.mjs"; - -export default { - ...common, - key: "slack-send-direct-message", - name: "Send a Direct Message", - description: "Send a direct message to a single user. See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here", - version: "0.2.18", - type: "action", - props: { - slack: common.props.slack, - conversation: { - propDefinition: [ - common.props.slack, - "user", - ], - }, - text: { - propDefinition: [ - common.props.slack, - "text", - ], - }, - mrkdwn: { - propDefinition: [ - common.props.slack, - "mrkdwn", - ], - }, - username: { - propDefinition: [ - common.props.slack, - "username", - ], - description: "Optionally customize your bot's username (default is `Pipedream`).", - }, - icon_emoji: { - propDefinition: [ - common.props.slack, - "icon_emoji", - ], - description: "Optionally use an emoji as the bot icon for this message (e.g., `:fire:`). This value overrides `icon_url` if both are provided.", - }, - icon_url: { - propDefinition: [ - common.props.slack, - "icon_url", - ], - description: "Optionally provide an image URL to use as the bot icon for this message.", - }, - ...common.props, - }, -}; diff --git a/components/slack/actions/send-group-message/send-group-message.mjs b/components/slack/actions/send-group-message/send-group-message.mjs deleted file mode 100644 index 921d7e1f5726f..0000000000000 --- a/components/slack/actions/send-group-message/send-group-message.mjs +++ /dev/null @@ -1,53 +0,0 @@ -import common from "../common/send-message.mjs"; - -export default { - ...common, - key: "slack-send-group-message", - name: "Send Group Message", - description: "Send a direct message to a group of users. See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here", - version: "0.2.18", - type: "action", - props: { - slack: common.props.slack, - conversation: { - propDefinition: [ - common.props.slack, - "group", - ], - }, - text: { - propDefinition: [ - common.props.slack, - "text", - ], - }, - mrkdwn: { - propDefinition: [ - common.props.slack, - "mrkdwn", - ], - }, - username: { - propDefinition: [ - common.props.slack, - "username", - ], - description: "Optionally customize your bot's username (default is `Pipedream`).", - }, - icon_emoji: { - propDefinition: [ - common.props.slack, - "icon_emoji", - ], - description: "Optionally use an emoji as the bot icon for this message (e.g., `:fire:`). This value overrides `icon_url` if both are provided.", - }, - icon_url: { - propDefinition: [ - common.props.slack, - "icon_url", - ], - description: "Optionally provide an image URL to use as the bot icon for this message.", - }, - ...common.props, - }, -}; diff --git a/components/slack/actions/send-large-message/send-large-message.mjs b/components/slack/actions/send-large-message/send-large-message.mjs index b1e38ba6b7914..2e2a8c0ee9898 100644 --- a/components/slack/actions/send-large-message/send-large-message.mjs +++ b/components/slack/actions/send-large-message/send-large-message.mjs @@ -5,7 +5,7 @@ export default { key: "slack-send-large-message", name: "Send a Large Message (3000+ characters)", description: "Send a large message (more than 3000 characters) to a channel, group or user. See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here", - version: "0.0.13", + version: "0.0.21", type: "action", props: { slack: common.props.slack, @@ -27,30 +27,9 @@ export default { "mrkdwn", ], }, - username: { - propDefinition: [ - common.props.slack, - "username", - ], - description: "Optionally customize your bot's username (default is `Pipedream`).", - }, - icon_emoji: { - propDefinition: [ - common.props.slack, - "icon_emoji", - ], - description: "Optionally use an emoji as the bot icon for this message (e.g., `:fire:`). This value overrides `icon_url` if both are provided.", - }, - icon_url: { - propDefinition: [ - common.props.slack, - "icon_url", - ], - description: "Optionally provide an image URL to use as the bot icon for this message.", - }, ...common.props, }, - async run() { + async run({ $ }) { if (this.include_sent_via_pipedream_flag) { const sentViaPipedreamText = this._makeSentViaPipedreamBlock(); this.text += `\n\n\n${sentViaPipedreamText.elements[0].text}`; @@ -59,10 +38,12 @@ export default { let metadataEventPayload; if (this.metadata_event_type) { - try { - metadataEventPayload = JSON.parse(this.metadata_event_payload); - } catch (error) { - throw new Error(`Invalid JSON in metadata_event_payload: ${error.message}`); + if (typeof this.metadata_event_payload === "string") { + try { + metadataEventPayload = JSON.parse(this.metadata_event_payload); + } catch (error) { + throw new Error(`Invalid JSON in metadata_event_payload: ${error.message}`); + } } this.metadata = { @@ -80,13 +61,30 @@ export default { icon_url: this.icon_url, mrkdwn: this.mrkdwn, metadata: this.metadata || null, + reply_broadcast: this.thread_broadcast, + thread_ts: this.thread_ts, + unfurl_links: this.unfurl_links, + unfurl_media: this.unfurl_media, }; + let response; if (this.post_at) { obj.post_at = this.post_at; - return this.slack.sdk().chat.scheduleMessage(obj); + response = await this.slack.sdk().chat.scheduleMessage(obj); } - return this.slack.sdk().chat.postMessage(obj); + response = await this.slack.sdk().chat.postMessage(obj); + const { channel } = await this.slack.conversationsInfo({ + channel: response.channel, + }); + let channelName = `#${channel?.name}`; + if (channel.is_im) { + const usernames = await this.slack.userNames(); + channelName = `@${usernames[channel.user]}`; + } else if (channel.is_mpim) { + channelName = `@${channel.purpose.value}`; + } + $.export("$summary", `Successfully sent a message to ${channelName}`); + return response; }, }; diff --git a/components/slack/actions/send-message-advanced/send-message-advanced.mjs b/components/slack/actions/send-message-advanced/send-message-advanced.mjs new file mode 100644 index 0000000000000..7d851b50e55e0 --- /dev/null +++ b/components/slack/actions/send-message-advanced/send-message-advanced.mjs @@ -0,0 +1,70 @@ +import common from "../common/send-message.mjs"; +import buildBlocks from "../common/build-blocks.mjs"; + +export default { + ...common, + ...buildBlocks, + key: "slack-send-message-advanced", + name: "Send Message (Advanced)", + description: "Customize advanced setttings and send a message to a channel, group or user. See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here", + version: "0.0.4", + type: "action", + props: { + slack: common.props.slack, + conversation: { + propDefinition: [ + common.props.slack, + "conversation", + ], + }, + text: { + propDefinition: [ + common.props.slack, + "text", + ], + description: "If you're using `blocks`, this is used as a fallback string to display in notifications. If you aren't, this is the main body text of the message. It can be formatted as plain text, or with mrkdwn.", + }, + mrkdwn: { + propDefinition: [ + common.props.slack, + "mrkdwn", + ], + }, + attachments: { + propDefinition: [ + common.props.slack, + "attachments", + ], + }, + parse: { + propDefinition: [ + common.props.slack, + "parse", + ], + }, + link_names: { + propDefinition: [ + common.props.slack, + "link_names", + ], + }, + ...common.props, + ...buildBlocks.props, + }, + methods: { + ...common.methods, + ...buildBlocks.methods, + async getGeneratedBlocks() { + return await buildBlocks.run.call(this); // call buildBlocks.run with the current context + }, + }, + async run({ $ }) { + if (this.passArrayOrConfigure) { + this.blocks = await this.getGeneratedBlocks(); // set the blocks prop for common.run to use + } + const resp = await common.run.call(this, { + $, + }); // call common.run with the current context + return resp; + }, +}; diff --git a/components/slack/actions/send-message-private-channel/send-message-private-channel.mjs b/components/slack/actions/send-message-private-channel/send-message-private-channel.mjs deleted file mode 100644 index eae0fd2e71c05..0000000000000 --- a/components/slack/actions/send-message-private-channel/send-message-private-channel.mjs +++ /dev/null @@ -1,59 +0,0 @@ -import common from "../common/send-message.mjs"; -import constants from "../../common/constants.mjs"; - -export default { - ...common, - key: "slack-send-message-private-channel", - name: "Send Message to a Private Channel", - description: "Send a message to a private channel and customize the name and avatar of the bot that posts the message. See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here", - version: "0.2.18", - type: "action", - props: { - slack: common.props.slack, - conversation: { - propDefinition: [ - common.props.slack, - "channelId", - () => ({ - types: [ - constants.CHANNEL_TYPE.PRIVATE, - ], - }), - ], - }, - text: { - propDefinition: [ - common.props.slack, - "text", - ], - }, - mrkdwn: { - propDefinition: [ - common.props.slack, - "mrkdwn", - ], - }, - username: { - propDefinition: [ - common.props.slack, - "username", - ], - description: "Optionally customize your bot's username (default is `Pipedream`).", - }, - icon_emoji: { - propDefinition: [ - common.props.slack, - "icon_emoji", - ], - description: "Optionally use an emoji as the bot icon for this message (e.g., `:fire:`). This value overrides `icon_url` if both are provided.", - }, - icon_url: { - propDefinition: [ - common.props.slack, - "icon_url", - ], - description: "Optionally provide an image URL to use as the bot icon for this message.", - }, - ...common.props, - }, -}; diff --git a/components/slack/actions/send-message-public-channel/send-message-public-channel.mjs b/components/slack/actions/send-message-public-channel/send-message-public-channel.mjs deleted file mode 100644 index 8b30714586533..0000000000000 --- a/components/slack/actions/send-message-public-channel/send-message-public-channel.mjs +++ /dev/null @@ -1,59 +0,0 @@ -import common from "../common/send-message.mjs"; -import constants from "../../common/constants.mjs"; - -export default { - ...common, - key: "slack-send-message-public-channel", - name: "Send Message to a Public Channel", - description: "Send a message to a public channel and customize the name and avatar of the bot that posts the message. See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here", - version: "0.2.17", - type: "action", - props: { - slack: common.props.slack, - conversation: { - propDefinition: [ - common.props.slack, - "channelId", - () => ({ - types: [ - constants.CHANNEL_TYPE.PUBLIC, - ], - }), - ], - }, - text: { - propDefinition: [ - common.props.slack, - "text", - ], - }, - mrkdwn: { - propDefinition: [ - common.props.slack, - "mrkdwn", - ], - }, - username: { - propDefinition: [ - common.props.slack, - "username", - ], - description: "Optionally customize your bot's username (default is `Pipedream`).", - }, - icon_emoji: { - propDefinition: [ - common.props.slack, - "icon_emoji", - ], - description: "Optionally use an emoji as the bot icon for this message (e.g., `:fire:`). This value overrides `icon_url` if both are provided.", - }, - icon_url: { - propDefinition: [ - common.props.slack, - "icon_url", - ], - description: "Optionally provide an image URL to use as the bot icon for this message.", - }, - ...common.props, - }, -}; diff --git a/components/slack/actions/send-message-to-channel/send-message-to-channel.mjs b/components/slack/actions/send-message-to-channel/send-message-to-channel.mjs new file mode 100644 index 0000000000000..ea592f15b3750 --- /dev/null +++ b/components/slack/actions/send-message-to-channel/send-message-to-channel.mjs @@ -0,0 +1,40 @@ +import common from "../common/send-message.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + ...common, + key: "slack-send-message-to-channel", + name: "Send Message to Channel", + description: "Send a message to a public or private channel. [See the documentation](https://api.slack.com/methods/chat.postMessage)", + version: "0.0.2", + type: "action", + props: { + slack: common.props.slack, + conversation: { + propDefinition: [ + common.props.slack, + "conversation", + () => ({ + types: [ + constants.CHANNEL_TYPE.PUBLIC, + constants.CHANNEL_TYPE.PRIVATE, + ], + }), + ], + description: "Select a public or private channel", + }, + text: { + propDefinition: [ + common.props.slack, + "text", + ], + }, + mrkdwn: { + propDefinition: [ + common.props.slack, + "mrkdwn", + ], + }, + ...common.props, + }, +}; diff --git a/components/slack/actions/send-message-to-user-or-group/send-message-to-user-or-group.mjs b/components/slack/actions/send-message-to-user-or-group/send-message-to-user-or-group.mjs new file mode 100644 index 0000000000000..2101a05049267 --- /dev/null +++ b/components/slack/actions/send-message-to-user-or-group/send-message-to-user-or-group.mjs @@ -0,0 +1,73 @@ +import common from "../common/send-message.mjs"; +import constants from "../../common/constants.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + ...common, + key: "slack-send-message-to-user-or-group", + name: "Send Message to User or Group", + description: "Send a message to a user or group. [See the documentation](https://api.slack.com/methods/chat.postMessage)", + version: "0.0.2", + type: "action", + props: { + slack: common.props.slack, + users: { + propDefinition: [ + common.props.slack, + "user", + ], + type: "string[]", + label: "Users", + description: "Select the user(s) to message", + optional: true, + }, + conversation: { + propDefinition: [ + common.props.slack, + "conversation", + () => ({ + types: [ + constants.CHANNEL_TYPE.MPIM, + ], + }), + ], + description: "Select the group to message", + optional: true, + }, + text: { + propDefinition: [ + common.props.slack, + "text", + ], + }, + mrkdwn: { + propDefinition: [ + common.props.slack, + "mrkdwn", + ], + }, + ...common.props, + }, + methods: { + ...common.methods, + openConversation(args = {}) { + return this.slack.makeRequest({ + method: "conversations.open", + ...args, + }); + }, + async getChannelId() { + if (!this.conversation && !this.users?.length) { + throw new ConfigurationError("Must select a group or user(s) to message"); + } + + if (this.conversation) { + return this.conversation; + } + const { channel: { id } } = await this.openConversation({ + users: this.users.join(), + }); + return id; + }, + }, +}; diff --git a/components/slack/actions/send-message/send-message.mjs b/components/slack/actions/send-message/send-message.mjs new file mode 100644 index 0000000000000..8c2f645cae6f6 --- /dev/null +++ b/components/slack/actions/send-message/send-message.mjs @@ -0,0 +1,51 @@ +import common from "../common/send-message.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + ...common, + key: "slack-send-message", + name: "Send Message", + description: "Send a message to a user, group, private channel or public channel. [See the documentation](https://api.slack.com/methods/chat.postMessage)", + version: "0.0.17", + type: "action", + props: { + slack: common.props.slack, + channelType: { + type: "string", + label: "Channel Type", + description: "The type of channel to send to. User/Direct Message (im), Group (mpim), Private Channel or Public Channel", + async options() { + return constants.CHANNEL_TYPE_OPTIONS; + }, + }, + conversation: { + propDefinition: [ + common.props.slack, + "conversation", + (c) => ({ + types: c.channelType === "Channels" + ? [ + constants.CHANNEL_TYPE.PUBLIC, + constants.CHANNEL_TYPE.PRIVATE, + ] + : [ + c.channelType, + ], + }), + ], + }, + text: { + propDefinition: [ + common.props.slack, + "text", + ], + }, + mrkdwn: { + propDefinition: [ + common.props.slack, + "mrkdwn", + ], + }, + ...common.props, + }, +}; diff --git a/components/slack/actions/set-channel-description/set-channel-description.mjs b/components/slack/actions/set-channel-description/set-channel-description.mjs new file mode 100644 index 0000000000000..727cd9e234cdb --- /dev/null +++ b/components/slack/actions/set-channel-description/set-channel-description.mjs @@ -0,0 +1,32 @@ +import slack from "../../slack.app.mjs"; + +export default { + key: "slack-set-channel-description", + name: "Set Channel Description", + description: "Change the description or purpose of a channel. [See the documentation](https://api.slack.com/methods/conversations.setPurpose)", + version: "0.0.6", + type: "action", + props: { + slack, + conversation: { + propDefinition: [ + slack, + "conversation", + ], + }, + purpose: { + propDefinition: [ + slack, + "purpose", + ], + }, + }, + async run({ $ }) { + const response = await this.slack.sdk().conversations.setPurpose({ + channel: this.conversation, + purpose: this.purpose, + }); + $.export("$summary", `Successfully set description for channel with ID ${this.conversation}`); + return response; + }, +}; diff --git a/components/slack/actions/set-channel-purpose/set-channel-purpose.mjs b/components/slack/actions/set-channel-purpose/set-channel-purpose.mjs deleted file mode 100644 index d3d77d14dbd8d..0000000000000 --- a/components/slack/actions/set-channel-purpose/set-channel-purpose.mjs +++ /dev/null @@ -1,30 +0,0 @@ -import slack from "../../slack.app.mjs"; - -export default { - key: "slack-set-channel-purpose", - name: "Set Channel Purpose", - description: "Change the purpose of a channel. [See docs here](https://api.slack.com/methods/conversations.setPurpose)", - version: "0.0.14", - type: "action", - props: { - slack, - conversation: { - propDefinition: [ - slack, - "conversation", - ], - }, - purpose: { - propDefinition: [ - slack, - "purpose", - ], - }, - }, - async run() { - return await this.slack.sdk().conversations.setPurpose({ - channel: this.conversation, - purpose: this.purpose, - }); - }, -}; diff --git a/components/slack/actions/set-channel-topic/set-channel-topic.mjs b/components/slack/actions/set-channel-topic/set-channel-topic.mjs index 8bb155be84814..10c5bd8d4a3c4 100644 --- a/components/slack/actions/set-channel-topic/set-channel-topic.mjs +++ b/components/slack/actions/set-channel-topic/set-channel-topic.mjs @@ -3,8 +3,8 @@ import slack from "../../slack.app.mjs"; export default { key: "slack-set-channel-topic", name: "Set Channel Topic", - description: "Set the topic on a selected channel. [See docs here](https://api.slack.com/methods/conversations.setTopic)", - version: "0.0.13", + description: "Set the topic on a selected channel. [See the documentation](https://api.slack.com/methods/conversations.setTopic)", + version: "0.0.21", type: "action", props: { slack, @@ -21,10 +21,12 @@ export default { ], }, }, - async run() { - return await this.slack.sdk().conversations.setTopic({ + async run({ $ }) { + const response = await this.slack.sdk().conversations.setTopic({ channel: this.conversation, topic: this.topic, }); + $.export("$summary", `Successfully set topic for channel with ID ${this.conversation}`); + return response; }, }; diff --git a/components/slack/actions/set-status/set-status.mjs b/components/slack/actions/set-status/set-status.mjs new file mode 100644 index 0000000000000..292c9cfc50682 --- /dev/null +++ b/components/slack/actions/set-status/set-status.mjs @@ -0,0 +1,44 @@ +import slack from "../../slack.app.mjs"; + +export default { + key: "slack-set-status", + name: "Set Status", + description: "Set the current status for a user. [See the documentation](https://api.slack.com/methods/users.profile.set)", + version: "0.0.6", + type: "action", + props: { + slack, + statusText: { + type: "string", + label: "Status Text", + description: "The displayed text", + }, + statusEmoji: { + propDefinition: [ + slack, + "icon_emoji", + ], + label: "Status Emoji", + description: "The emoji to display with the status", + optional: true, + }, + statusExpiration: { + type: "string", + label: "Status Expiration", + description: "The datetime of when the status will expire in ISO 8601 format. (Example: `2014-01-01T00:00:00Z`)", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.slack.sdk().users.profile.set({ + profile: { + status_text: this.statusText, + status_emoji: this.statusEmoji && `:${this.statusEmoji}:`, + status_expiration: this.statusExpiration + && Math.floor(new Date(this.statusExpiration).getTime() / 1000), + }, + }); + $.export("$summary", "Successfully updated status."); + return response; + }, +}; diff --git a/components/slack/actions/unarchive-channel/unarchive-channel.mjs b/components/slack/actions/unarchive-channel/unarchive-channel.mjs deleted file mode 100644 index 1ca61365bb70f..0000000000000 --- a/components/slack/actions/unarchive-channel/unarchive-channel.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import slack from "../../slack.app.mjs"; - -export default { - key: "slack-unarchive-channel", - name: "Unarchive Channel", - description: "Unarchive a channel. [See docs here](https://api.slack.com/methods/conversations.unarchive)", - version: "0.0.13", - type: "action", - props: { - slack, - conversation: { - propDefinition: [ - slack, - "conversation", - ], - }, - }, - async run() { - return await this.slack.sdk().conversations.unarchive({ - channel: this.conversation, - }); - }, -}; diff --git a/components/slack/actions/update-group-members/update-group-members.mjs b/components/slack/actions/update-group-members/update-group-members.mjs new file mode 100644 index 0000000000000..616a527e7c8a3 --- /dev/null +++ b/components/slack/actions/update-group-members/update-group-members.mjs @@ -0,0 +1,67 @@ +import slack from "../../slack.app.mjs"; + +export default { + key: "slack-update-group-members", + name: "Update Groups Members", + description: "Update the list of users for a User Group. [See the documentation](https://api.slack.com/methods/usergroups.users.update)", + version: "0.0.6", + type: "action", + props: { + slack, + userGroup: { + propDefinition: [ + slack, + "userGroup", + ], + }, + usersToAdd: { + propDefinition: [ + slack, + "user", + ], + type: "string[]", + label: "Users to Add", + description: "A list of encoded user IDs that represent the users to add to the group.", + optional: true, + }, + usersToRemove: { + propDefinition: [ + slack, + "user", + ], + type: "string[]", + label: "Users to Remove", + description: "A list of encoded user IDs that represent the users to remove from the group.", + optional: true, + }, + team: { + propDefinition: [ + slack, + "team", + ], + optional: true, + description: "Encoded team id where the user group exists, required if org token is used.", + }, + }, + async run({ $ }) { + const { + userGroup, + usersToAdd, + usersToRemove, + team, + } = this; + let { users } = await this.slack.sdk().usergroups.users.list({ + usergroup: userGroup, + team_id: team, + }); + users = users.filter((user) => !usersToRemove.includes(user)); + users.push(...usersToAdd); + const response = await this.slack.sdk().usergroups.users.update({ + usergroup: userGroup, + users, + team_id: team, + }); + $.export("$summary", `Successfully updated members of group with ID ${this.userGroup}`); + return response; + }, +}; diff --git a/components/slack/actions/update-message/update-message.mjs b/components/slack/actions/update-message/update-message.mjs index 6e7f33ead950a..9b0e69172d4e2 100644 --- a/components/slack/actions/update-message/update-message.mjs +++ b/components/slack/actions/update-message/update-message.mjs @@ -3,8 +3,8 @@ import slack from "../../slack.app.mjs"; export default { key: "slack-update-message", name: "Update Message", - description: "Update a message. [See docs here](https://api.slack.com/methods/chat.update)", - version: "0.1.13", + description: "Update a message. [See the documentation](https://api.slack.com/methods/chat.update)", + version: "0.1.21", type: "action", props: { slack, @@ -17,7 +17,7 @@ export default { timestamp: { propDefinition: [ slack, - "timestamp", + "messageTs", ], }, text: { @@ -40,13 +40,15 @@ export default { ], }, }, - async run() { - return await this.slack.sdk().chat.update({ + async run({ $ }) { + const response = await this.slack.sdk().chat.update({ ts: this.timestamp, text: this.text, channel: this.conversation, as_user: this.as_user, attachments: this.attachments, }); + $.export("$summary", "Successfully updated message"); + return response; }, }; diff --git a/components/slack/actions/update-profile/update-profile.mjs b/components/slack/actions/update-profile/update-profile.mjs index 5cfd9bebd98d3..0455b4e11a3ad 100644 --- a/components/slack/actions/update-profile/update-profile.mjs +++ b/components/slack/actions/update-profile/update-profile.mjs @@ -1,30 +1,87 @@ import slack from "../../slack.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; export default { key: "slack-update-profile", name: "Update Profile", - description: "Update basic profile field such as name or title. [See docs here](https://api.slack.com/methods/users.profile.set)", - version: "0.0.13", + description: "Update basic profile field such as name or title. [See the documentation](https://api.slack.com/methods/users.profile.set)", + version: "0.0.21", type: "action", props: { slack, - name: { - propDefinition: [ - slack, - "name", - ], + displayName: { + type: "string", + label: "Display Name", + description: "The display name the user has chosen to identify themselves by in their workspace profile", + optional: true, + }, + firstName: { + type: "string", + label: "First Name", + description: "The user's first name", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "The user's last name", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The user's phone number, in any format", + optional: true, + }, + pronouns: { + type: "string", + label: "Pronouns", + description: "The user's pronouns", + optional: true, + }, + title: { + type: "string", + label: "Title", + description: "The user's title", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "The user's email address. You cannot update your own email using this method. This field can only be changed by admins for users on paid teams.", + optional: true, }, - value: { + user: { propDefinition: [ slack, - "value", + "user", ], + description: "ID of user to change. This argument may only be specified by admins on paid teams.", + optional: true, }, }, - async run() { - return await this.slack.sdk().users.profile.set({ - name: this.name, - value: this.value, + async run({ $ }) { + if (!this.displayName + && !this.firstName + && !this.lastName + && !this.phone + && !this.pronouns + && !this.title + ) { + throw new ConfigurationError("Please provide at least one value to update"); + } + const response = await this.slack.sdk().users.profile.set({ + profile: { + display_name: this.displayName, + first_name: this.firstName, + last_name: this.lastName, + phone: this.phone, + pronouns: this.pronouns, + title: this.title, + }, + user: this.user, }); + $.export("$summary", "Successfully updated profile"); + return response; }, }; diff --git a/components/slack/actions/update-user-groups-users/update-user-groups-users.mjs b/components/slack/actions/update-user-groups-users/update-user-groups-users.mjs deleted file mode 100644 index 116f91854808a..0000000000000 --- a/components/slack/actions/update-user-groups-users/update-user-groups-users.mjs +++ /dev/null @@ -1,45 +0,0 @@ -import slack from "../../slack.app.mjs"; - -export default { - key: "slack-update-user-groups-users", - name: "Update User Groups Users", - description: "Update the list of users for a User Group. [See docs here](https://api.slack.com/methods/usergroups.users.update)", - version: "0.0.6", - type: "action", - props: { - slack, - userGroup: { - propDefinition: [ - slack, - "userGroup", - ], - }, - users: { - propDefinition: [ - slack, - "users", - ], - description: "A list of encoded user IDs that represent the entire list of users for the User Group.", - }, - team: { - propDefinition: [ - slack, - "team", - ], - optional: true, - description: "Encoded team id where the user group exists, required if org token is used.", - }, - }, - async run() { - const { - userGroup, - users, - team, - } = this; - return await this.slack.sdk().usergroups.users.update({ - usergroup: userGroup, - users: users, - team_id: team, - }); - }, -}; diff --git a/components/slack/actions/upload-file/upload-file.mjs b/components/slack/actions/upload-file/upload-file.mjs index bfdf53ccc4b0f..c82e8ec0ce6fb 100644 --- a/components/slack/actions/upload-file/upload-file.mjs +++ b/components/slack/actions/upload-file/upload-file.mjs @@ -1,12 +1,12 @@ import { ConfigurationError } from "@pipedream/platform"; -import slack from "../../slack.app.mjs"; import fs from "fs"; +import slack from "../../slack.app.mjs"; export default { key: "slack-upload-file", name: "Upload File", - description: "Upload a file. [See docs here](https://api.slack.com/methods/files.upload)", - version: "0.0.16", + description: "Upload a file. [See the documentation](https://api.slack.com/methods/files.upload)", + version: "0.0.25", type: "action", props: { slack, @@ -23,7 +23,6 @@ export default { ], }, initialComment: { - label: "Report Name", description: "Will be added as an initial comment before the image", propDefinition: [ slack, @@ -32,15 +31,17 @@ export default { optional: true, }, }, - async run() { + async run({ $ }) { if (!fs.existsSync(this.content)) { - throw new ConfigurationError(`\`${this.content}\` not found, is needed a valid \`/tmp\` path`); + throw new ConfigurationError(`\`${this.content}\` not found, a valid \`/tmp\` path is needed`); } - - return await this.slack.sdk().files.upload({ + const response = await this.slack.sdk().filesUploadV2({ file: fs.createReadStream(this.content), - channels: this.conversation, + channel_id: this.conversation, initial_comment: this.initialComment, + filename: this.content.split("/").pop(), }); + $.export("$summary", "Successfully uploaded file"); + return response; }, }; diff --git a/components/slack/actions/verify-slack-signature/verify-slack-signature.mjs b/components/slack/actions/verify-slack-signature/verify-slack-signature.mjs index 41ba5701fe1ce..08b75636cbaf6 100644 --- a/components/slack/actions/verify-slack-signature/verify-slack-signature.mjs +++ b/components/slack/actions/verify-slack-signature/verify-slack-signature.mjs @@ -1,11 +1,11 @@ -import slack from "../../slack.app.mjs"; import crypto from "crypto"; +import slack from "../../slack.app.mjs"; export default { key: "slack-verify-slack-signature", name: "Verify Slack Signature", - description: "Verifying requests from Slack, slack signs its requests using a secret that's unique to your app. [See docs here](https://api.slack.com/authentication/verifying-requests-from-slack)", - version: "0.0.6", + description: "Verifying requests from Slack, slack signs its requests using a secret that's unique to your app. [See the documentation](https://api.slack.com/authentication/verifying-requests-from-slack)", + version: "0.0.14", type: "action", props: { slack, diff --git a/components/slack/common/constants.mjs b/components/slack/common/constants.mjs index 57ea40d0f9e44..c96e534bc3936 100644 --- a/components/slack/common/constants.mjs +++ b/components/slack/common/constants.mjs @@ -8,8 +8,24 @@ const CHANNEL_TYPE = { IM: "im", }; +const CHANNEL_TYPE_OPTIONS = [ + { + label: "Channels", + value: "Channels", + }, + { + label: "Group", + value: CHANNEL_TYPE.MPIM, + }, + { + label: "User / Direct Message", + value: CHANNEL_TYPE.IM, + }, +]; + export default { MAX_RESOURCES, LIMIT, CHANNEL_TYPE, + CHANNEL_TYPE_OPTIONS, }; diff --git a/components/slack/package.json b/components/slack/package.json index 6a14fb6100ba7..060f3e2262d73 100644 --- a/components/slack/package.json +++ b/components/slack/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/slack", - "version": "0.4.31", + "version": "0.9.1", "description": "Pipedream Slack Components", "main": "slack.app.mjs", "keywords": [ @@ -14,6 +14,7 @@ "access": "public" }, "dependencies": { - "@slack/web-api": "^5.15.0" + "@pipedream/platform": "^3.0.0", + "@slack/web-api": "^7.0.4" } } diff --git a/components/slack/slack.app.mjs b/components/slack/slack.app.mjs index a7826424c9675..836c8d99fdccc 100644 --- a/components/slack/slack.app.mjs +++ b/components/slack/slack.app.mjs @@ -9,17 +9,26 @@ export default { type: "string", label: "User", description: "Select a user", - async options({ prevContext }) { + async options({ + prevContext, channelId, + }) { const types = [ "im", ]; - const [ + let [ userNames, conversationsResp, ] = await Promise.all([ prevContext.userNames ?? this.userNames(), this.availableConversations(types.join(), prevContext.cursor), ]); + if (channelId) { + const { members } = await this.sdk().conversations.members({ + channel: channelId, + }); + conversationsResp.conversations = conversationsResp.conversations + .filter((c) => members.includes(c.user || c.id)); + } return { options: conversationsResp.conversations.map((c) => ({ label: `@${userNames[c.user]}`, @@ -55,22 +64,6 @@ export default { }; }, }, - users: { - type: "string[]", - label: "Users", - description: "Select users", - async options() { - const userNames = await this.userNames(); - const users = []; - for (const key of Object.keys(userNames)) { - users.push({ - label: userNames[key], - value: key, - }); - } - return users; - }, - }, userGroup: { type: "string", label: "User Group", @@ -99,12 +92,16 @@ export default { type: "string", label: "Channel", description: "Select a public or private channel, or a user or group", - async options({ prevContext }) { + async options({ + prevContext, types, + }) { let { - types, cursor, userNames: userNamesOrPromise, } = prevContext; + if (prevContext?.types) { + types = prevContext.types; + } if (types == null) { const { response_metadata: { scopes } } = await this.authTest(); types = [ @@ -122,14 +119,17 @@ export default { } } const [ - userNames, + userNames = await this.userNames(), conversationsResp, ] = await Promise.all([ userNamesOrPromise, this.availableConversations(types.join(), cursor), ]); + const conversations = userNames + ? conversationsResp.conversations + : conversationsResp.conversations.filter((c) => !c.is_im); return { - options: conversationsResp.conversations.map((c) => { + options: conversations.map((c) => { if (c.is_im) { return { label: `Direct messaging with: @${userNames[c.user]}`, @@ -162,7 +162,10 @@ export default { label: "Channel ID", description: "Select the channel's id.", async options({ - prevContext, types = [], channelsFilter = (channel) => channel, excludeArchived = true, + prevContext, + types = Object.values(constants.CHANNEL_TYPE), + channelsFilter = (channel) => channel, + excludeArchived = true, }) { const userNames = prevContext.userNames || await this.userNames(); const { @@ -177,12 +180,26 @@ export default { const options = channels .filter(channelsFilter) - .map((c) => ({ - value: c.id, - label: c.is_im - ? `@${userNames[c.user]}` - : c.name, - })); + .map((c) => { + if (c.is_im) { + return { + label: `Direct messaging with: @${userNames[c.user]}`, + value: c.id, + }; + } else if (c.is_mpim) { + return { + label: c.purpose.value, + value: c.id, + }; + } else { + return { + label: `${c.is_private + ? "Private" + : "Public"} channel: ${c.name}`, + value: c.id, + }; + } + }); return { options, @@ -217,27 +234,16 @@ export default { }; }, }, - notificationText: { + messageTs: { type: "string", - label: "Notification Text", - description: "Optionally provide a string for Slack to display as the new message notification (if you do not provide this, notification will be blank).", - optional: true, + label: "Message Timestamp", + description: "Timestamp of a message. e.g. `1403051575.000407`.", }, text: { type: "string", label: "Text", description: "Text of the message to send (see Slack's [formatting docs](https://api.slack.com/reference/surfaces/formatting)). This field is usually necessary, unless you're providing only attachments instead.", }, - name: { - type: "string", - label: "Name", - description: "Name of a single key to set.", - }, - value: { - type: "string", - label: "Value", - description: "Value to set a single key to.", - }, topic: { type: "string", label: "Topic", @@ -253,15 +259,24 @@ export default { label: "Query", description: "Search query.", }, - team_id: { - type: "string", - label: "Team ID", - description: "The ID of the team.", - }, file: { type: "string", label: "File ID", description: "Specify a file by providing its ID.", + async options({ + channel, page, + }) { + const { files } = await this.sdk().files.list({ + channel, + page: page + 1, + }); + return files?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, }, attachments: { type: "string", @@ -272,45 +287,47 @@ export default { unfurl_links: { type: "boolean", label: "Unfurl Links", - description: "`TRUE` by default. Pass `FALSE` to disable unfurling of links.", + description: "Default to `false`. Pass `true` to enable unfurling of links.", + default: false, optional: true, }, unfurl_media: { type: "boolean", label: "Unfurl Media", - description: "`TRUE` by default. Pass `FALSE` to disable unfurling of media content.", + description: "Defaults to `false`. Pass `true` to enable unfurling of media content.", + default: false, optional: true, }, parse: { type: "string", label: "Parse", - description: "Change how messages are treated. Defaults to none. See below.", + description: "Change how messages are treated. Defaults to none. By default, URLs will be hyperlinked. Set `parse` to `none` to remove the hyperlinks. The behavior of `parse` is different for text formatted with `mrkdwn`. By default, or when `parse` is set to `none`, `mrkdwn` formatting is implemented. To ignore `mrkdwn` formatting, set `parse` to full.", optional: true, }, as_user: { type: "boolean", label: "Send as User", - description: "Optionally pass `TRUE` to post the message as the authed user, instead of as a bot. Defaults to `FALSE`.", + description: "Optionally pass `true` to post the message as the authenticated user, instead of as a bot. Defaults to `false`.", default: false, optional: true, }, mrkdwn: { label: "Send text as Slack mrkdwn", type: "boolean", - description: "`TRUE` by default. Pass `FALSE` to disable Slack markup parsing. [See docs here](https://api.slack.com/reference/surfaces/formatting)", + description: "`true` by default. Pass `false` to disable Slack markup parsing. [See docs here](https://api.slack.com/reference/surfaces/formatting)", default: true, optional: true, }, post_at: { label: "Schedule message", - description: "Messages can only be scheduled up to 120 days in advance, and cannot be scheduled for the past. The datetime format should be a unix timestamp (e.g., `1650507616`, [see here](https://www.epochconverter.com/) for help with this format).", - type: "integer", + description: "Messages can only be scheduled up to 120 days in advance, and cannot be scheduled for the past. The datetime should be in ISO 8601 format. (Example: `2014-01-01T00:00:00Z`)", + type: "string", optional: true, }, username: { type: "string", label: "Bot Username", - description: "Optionally customize your bot's user name (default is `Pipedream`). Must be used in conjunction with `as_user` set to false, otherwise ignored.", + description: "Optionally customize your bot's user name (default is `Pipedream`). Must be used in conjunction with `Send as User` set to false, otherwise ignored.", optional: true, }, blocks: { @@ -322,7 +339,7 @@ export default { icon_emoji: { type: "string", label: "Icon (emoji)", - description: "Optionally provide an emoji to use as the icon for this message. E.g., `:fire:` Overrides `icon_url`. Must be used in conjunction with `as_user` set to `false`, otherwise ignored.", + description: "Optionally provide an emoji to use as the icon for this message. E.g., `:fire:` Overrides `icon_url`. Must be used in conjunction with `Send as User` set to `false`, otherwise ignored.", optional: true, async options() { return await this.getCustomEmojis(); @@ -334,15 +351,15 @@ export default { type: "string", }, link_names: { - type: "string", + type: "boolean", label: "Link Names", description: "Find and link channel names and usernames.", optional: true, }, - reply_broadcast: { + thread_broadcast: { type: "boolean", - label: "Reply Broadcasts", - description: "Used in conjunction with thread_ts and indicates whether reply should be made visible to everyone in the channel or conversation. Defaults to false.", + label: "Send Channel Message", + description: "If `true`, posts in the thread and channel. Used in conjunction with `Message Timestamp` and indicates whether reply should be made visible to everyone in the channel. Defaults to `false`.", default: false, optional: true, }, @@ -352,22 +369,10 @@ export default { description: "Provide the channel or conversation ID for the thread to reply to (e.g., if triggering on new Slack messages, enter `{{event.channel}}`). If the channel does not match the thread timestamp, a new message will be posted to this channel.", optional: true, }, - thread_ts: { - label: "Thread Timestamp", - type: "string", - description: "Provide another message's `ts` value to make this message a reply (e.g., if triggering on new Slack messages, enter `{{event.ts}}`). Avoid using a reply's `ts` value; use its parent instead.", - optional: true, - }, - timestamp: { - label: "Timestamp", - type: "string", - description: "Timestamp of the relevant data.", - optional: true, - }, icon_url: { type: "string", label: "Icon (image URL)", - description: "Optionally provide an image URL to use as the icon for this message. Must be used in conjunction with `as_user` set to `false`, otherwise ignored.", + description: "Optionally provide an image URL to use as the icon for this message. Must be used in conjunction with `Send as User` set to `false`, otherwise ignored.", optional: true, }, initial_comment: { @@ -376,12 +381,6 @@ export default { description: "The message text introducing the file", optional: true, }, - count: { - type: "integer", - label: "Count", - description: "Number of items to return per page.", - optional: true, - }, email: { type: "string", label: "Email", @@ -390,13 +389,13 @@ export default { metadata_event_type: { type: "string", label: "Metadata Event Type", - description: "The name of the metadata event", + description: "The name of the metadata event. Example: `task_created`", optional: true, }, metadata_event_payload: { type: "string", label: "Metadata Event Payload", - description: "The payload of the metadata event. Must be a JSON string e.g. `{\"key\": \"value\"}`", + description: "The payload of the metadata event. Must be a JSON string. Example: `{ \"id\": \"11223\", \"title\": \"Redesign Homepage\"}`", optional: true, }, ignoreMyself: { @@ -410,13 +409,6 @@ export default { label: "Keyword", description: "Keyword to monitor", }, - isUsername: { - type: "boolean", - label: "Is Username", - description: "Filters out mentions of the keyword that are not a username", - default: false, - optional: true, - }, ignoreBot: { type: "boolean", label: "Ignore Bots", diff --git a/components/slack/sources/common/base.mjs b/components/slack/sources/common/base.mjs index 77d8762f41632..1f518ccc40baa 100644 --- a/components/slack/sources/common/base.mjs +++ b/components/slack/sources/common/base.mjs @@ -92,6 +92,15 @@ export default { return info.user.name; }); }, + async getRealName(id) { + return this.maybeCached(`users_real_names:${id}`, async () => { + const info = await this.slack.sdk().users.info({ + user: id, + }); + if (!info.ok) throw new Error(info.error); + return info.user.real_name; + }); + }, async getBotName(id) { return this.maybeCached(`bots:${id}`, async () => { const info = await this.slack.sdk().bots.info({ @@ -166,7 +175,7 @@ export default { } this.$emit(event, { - id: event.client_msg_id || event.pipedream_msg_id, + id: event.client_msg_id || event.pipedream_msg_id || event.channel.id, summary: this.getSummary(event), ts: event.event_ts || Date.now(), }); diff --git a/components/slack/sources/new-channel-created/new-channel-created.mjs b/components/slack/sources/new-channel-created/new-channel-created.mjs new file mode 100644 index 0000000000000..8d155271e68db --- /dev/null +++ b/components/slack/sources/new-channel-created/new-channel-created.mjs @@ -0,0 +1,32 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "slack-new-channel-created", + name: "New Channel Created (Instant)", + version: "0.0.8", + description: "Emit new event when a new channel is created.", + type: "source", + dedupe: "unique", + props: { + ...common.props, + // eslint-disable-next-line pipedream/props-description,pipedream/props-label + slackApphook: { + type: "$.interface.apphook", + appProp: "slack", + async eventNames() { + return [ + "channel_created", + ]; + }, + }, + }, + methods: { + ...common.methods, + getSummary({ channel: { name } }) { + return `New channel created - ${name}`; + }, + }, + sampleEmit, +}; diff --git a/components/slack/sources/new-channel-created/test-event.mjs b/components/slack/sources/new-channel-created/test-event.mjs new file mode 100644 index 0000000000000..6bcfee6fd7deb --- /dev/null +++ b/components/slack/sources/new-channel-created/test-event.mjs @@ -0,0 +1,44 @@ +export default { + "type": "channel_created", + "channel": { + "id": "C024BE91L", + "name": "fun", + "is_channel": true, + "is_group": false, + "is_im": false, + "is_mpim": false, + "is_private": false, + "created": 1360782804, + "is_archived": false, + "is_general": false, + "unlinked": 0, + "name_normalized": "fun", + "is_shared": false, + "is_frozen": false, + "is_org_shared": false, + "is_pending_ext_shared": false, + "pending_shared": [], + "context_team_id": "TLZ203R5", + "updated": 1714140253251, + "parent_conversation": null, + "creator": "U024BE7LH", + "is_ext_shared": false, + "shared_team_ids": [ + "TLZ203R5" + ], + "pending_connected_team_ids": [], + "topic": { + "value": "", + "creator": "", + "last_set": 0 + }, + "purpose": { + "value": "", + "creator": "", + "last_set": 0 + }, + "previous_names": [] + }, + "event_ts": "1714140253.002700", + "pipedream_msg_id": "pd_1714140255038_bkbl3pxpkp" +} \ No newline at end of file diff --git a/components/slack/sources/new-direct-message/new-direct-message.mjs b/components/slack/sources/new-direct-message/new-direct-message.mjs index d888beeaf3bc5..fff5e35f0c0fe 100644 --- a/components/slack/sources/new-direct-message/new-direct-message.mjs +++ b/components/slack/sources/new-direct-message/new-direct-message.mjs @@ -1,10 +1,11 @@ import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "slack-new-direct-message", name: "New Direct Message (Instant)", - version: "1.0.12", + version: "1.0.21", description: "Emit new event when a message was posted in a direct message channel", type: "source", dedupe: "unique", @@ -26,6 +27,13 @@ export default { "ignoreBot", ], }, + ignoreSelf: { + type: "boolean", + label: "Ignore Messages from Yourself", + description: "Ignores messages sent to yourself", + default: false, + optional: true, + }, }, methods: { ...common.methods, @@ -33,13 +41,13 @@ export default { return "New direct message received"; }, processEvent(event) { - if (event.user == this.slack.mySlackId()) { - return; - } - if ((this.ignoreBot) && (event.subtype == "bot_message" || event.bot_id)) { + if ((this.ignoreSelf && event.user == this.slack.mySlackId()) + || ((this.ignoreBot) && (event.subtype === "bot_message" || event.bot_id)) + || (event.subtype === "message_changed")) { return; } return event; }, }, + sampleEmit, }; diff --git a/components/slack/sources/new-direct-message/test-event.mjs b/components/slack/sources/new-direct-message/test-event.mjs new file mode 100644 index 0000000000000..d19486ed235f0 --- /dev/null +++ b/components/slack/sources/new-direct-message/test-event.mjs @@ -0,0 +1,28 @@ +export default { + "user": "USLACKBOT", + "type": "message", + "ts": "1716401124.947359", + "text": "Feeling great!", + "team": "TS8319547", + "blocks": [ + { + "type": "rich_text", + "block_id": "bid/", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "text", + "text": "Feeling great!" + } + ] + } + ] + } + ], + "channel": "DS676Q73J", + "event_ts": "1716401124.947359", + "channel_type": "im", + "pipedream_msg_id": "pd_1716401126905_tjxu6josgz" +} \ No newline at end of file diff --git a/components/slack/sources/new-interaction-event-received/README.md b/components/slack/sources/new-interaction-event-received/README.md index d756be1788814..4cb59ba41fe5e 100644 --- a/components/slack/sources/new-interaction-event-received/README.md +++ b/components/slack/sources/new-interaction-event-received/README.md @@ -6,7 +6,7 @@ Slack messages can contain interactive elements like buttons, dropdowns, radio b Then this source will be triggered when you or another Slack user in your workspace clicks a button, selects an option or fills out a form. -![Example feed of interaction events coming from Slack]([screenshots/CleanShot_2022-11-10_at_10.19.152x.png](https://res.cloudinary.com/pipedreamin/image/upload/v1668443818/docs/components/CleanShot_2022-11-10_at_10.19.152x_eyiims.png)) +![Example feed of interaction events coming from Slack](https://res.cloudinary.com/pipedreamin/image/upload/v1668443818/docs/components/CleanShot_2022-11-10_at_10.19.152x_eyiims.png) With this trigger, you can build workflows that perform some work with other APIs or services, and then reply back to the original message. @@ -16,7 +16,7 @@ With this trigger, you can build workflows that perform some work with other API What this short video to learn how to use this in a workflow, or follow the guide below. -First, if you haven’t already - send yourself a message containing one or more interactive elements. Use the ******************Sending the message with an interactive element****************** guide below to send a messsage containing a button. +First, if you haven’t already - send yourself a message containing one or more interactive elements. Use the ******************Sending the message with an interactive element****************** guide below to send a message containing a button. If you have already sent a message containing an element, skip to **********************************************Configuring the source.********************************************** @@ -31,21 +31,21 @@ Then select a **************Channel************** you’d like to send the messa ```jsx [ { - type: "actions", - elements: [ + "type": "actions", + "elements": [ { - type: "button", - text: { - type: "plain_text", - text: "Click Me", - emoji: true, + "type": "button", + "text": { + "type": "plain_text", + "text": "Click Me", + "emoji": true }, - value: "click_me_123", - action_id: "button_click", - }, - ], - }, -]; + "value": "click_me_123", + "action_id": "button_click" + } + ] + } +] ``` Your ******************Slack - Send Message Using Block Kit****************** should look like this: @@ -54,7 +54,7 @@ Your ******************Slack - Send Message Using Block Kit****************** sh ## Configuring the source -By default, this source will listen to ******all****** interactive events from your Slack workspace that your connected Slack account has authorization to view. +By default, this source will listen to ******all****** interactive events from your Slack workspace that your connected Slack account has authorization to view. Please note that only messages created via [Slack - Send Block Kit Message](https://pipedream.com/apps/slack/actions/send-block-kit-message) Action, or via API call from the Pipedream app will emit an interaction event with this trigger. Block kit messages sent directly via the Slack's block kit builder will not trigger an interaction event. You can filter these events by selecting a specific **************channel************** and/or a specific **********action_id.********** @@ -72,12 +72,14 @@ For example, in the section above using the Block Kit to create a message, we de If you pass `button_click` as a required `action_id` to this source, then only interactivity events with the `action_id` of `"button_click"` will trigger this source. -# Troubleshooting +## Troubleshooting -## I’m clicking buttons, but no events are being received +### I’m clicking buttons, but no events are being received Follow these steps to make sure your source is configured correctly: 1. Make sure that your `action_id` or ****************channels**************** filters apply to that message, remove the filters to make sure that’s not the case. 1. Make sure that the message comes from the same Slack account that this source is configured with. + +1. Make sure that the message was sent via Pipedream action (e.g. [Slack - Send Block Kit Message](https://pipedream.com/apps/slack/actions/send-block-kit-message) Action) or via API call from the Pipedream app. diff --git a/components/slack/sources/new-interaction-event-received/new-interaction-event-received.mjs b/components/slack/sources/new-interaction-event-received/new-interaction-event-received.mjs index 5af588fec7f2d..4b9ecc995867e 100644 --- a/components/slack/sources/new-interaction-event-received/new-interaction-event-received.mjs +++ b/components/slack/sources/new-interaction-event-received/new-interaction-event-received.mjs @@ -1,19 +1,23 @@ import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; export default { - name: "New Interaction Events", - version: "0.0.9", + name: "New Interaction Events (Instant)", + version: "0.0.18", key: "slack-new-interaction-event-received", - description: - "Emit new events on new Slack [interactivity events](https://api.slack.com/interactivity) sourced from [Block Kit interactive elements](https://api.slack.com/interactivity/components), [Slash commands](https://api.slack.com/interactivity/slash-commands), or [Shortcuts](https://api.slack.com/interactivity/shortcuts).", + description: "Emit new events on new Slack [interactivity events](https://api.slack.com/interactivity) sourced from [Block Kit interactive elements](https://api.slack.com/interactivity/components), [Slash commands](https://api.slack.com/interactivity/slash-commands), or [Shortcuts](https://api.slack.com/interactivity/shortcuts).", type: "source", props: { ...common.props, + alert: { + type: "alert", + alertType: "info", + content: "Please note that only messages created via Pipedream's [Send Block Kit Message](https://pipedream.com/apps/slack/actions/send-block-kit-message) Action, or via API call from the Pipedream app will emit an interaction event with this trigger. \n\nBlock kit messages sent directly via the Slack's block kit builder will not trigger an interaction event. \n\nSee the [documentation](https://pipedream.com/apps/slack/triggers/new-interaction-event-received) for more details.", + }, action_ids: { type: "string[]", label: "Action IDs", - description: - "Filter interaction events by specific `action_id`'s to subscribe for new interaction events. If none selected, all `action_ids` will emit new events.", + description: "Filter interaction events by specific `action_id`'s to subscribe for new interaction events. If none are specified, all `action_ids` created via Pipedream will emit new events.", optional: true, default: [], }, @@ -24,8 +28,7 @@ export default { ], type: "string[]", label: "Channels", - description: - "Filter interaction events by one or more channels. If none selected, any interaction event in any channel will emit new events.", + description: "Filter interaction events by one or more channels. If none selected, any interaction event in any channel will emit new events.", optional: true, default: [], }, @@ -98,4 +101,5 @@ export default { }, ); }, + sampleEmit, }; diff --git a/components/slack/sources/new-interaction-event-received/test-event.mjs b/components/slack/sources/new-interaction-event-received/test-event.mjs new file mode 100644 index 0000000000000..940a52c301ea8 --- /dev/null +++ b/components/slack/sources/new-interaction-event-received/test-event.mjs @@ -0,0 +1,86 @@ +export default { + "event": { + "type": "block_actions", + "user": { + "id": "US676PZLY", + "username": "test.user", + "name": "test.user", + "team_id": "TS8319547" + }, + "api_app_id": "AN9231S6L", + "token": "UYc82mtyZWRhvUXQ6TXrv4wq", + "container": { + "type": "message", + "message_ts": "1716402983.247149", + "channel_id": "CS8319KD5", + "is_ephemeral": false + }, + "trigger_id": "7161731794692.892103311143.4020ed3595908eca11e4076438354dbb", + "team": { + "id": "TS8319547", + "domain": "test-j1q3506" + }, + "enterprise": null, + "is_enterprise_install": false, + "channel": { + "id": "CS8319KD5", + "name": "testing" + }, + "message": { + "subtype": "bot_message", + "text": "Click Me button Sent via ", + "username": "Pipedream", + "type": "message", + "ts": "1716402983.247149", + "bot_id": "BRTDL45RQ", + "app_id": "AN9231S6L", + "blocks": [ + { + "type": "actions", + "block_id": "SJp0j", + "elements": [ + { + "type": "button", + "action_id": "button_click", + "text": { + "type": "plain_text", + "text": "Click Me", + "emoji": true + }, + "value": "click_me_123" + } + ] + }, + { + "type": "context", + "block_id": "ysmBN", + "elements": [ + { + "type": "mrkdwn", + "text": "Sent via ", + "verbatim": false + } + ] + } + ] + }, + "state": { + "values": {} + }, + "response_url": "https://hooks.slack.com/actions/TS8319547/7156351250101/J0w1NoVIXjChEwp4WQab4tcv", + "actions": [ + { + "action_id": "button_click", + "block_id": "SJp0j", + "text": { + "type": "plain_text", + "text": "Click Me", + "emoji": true + }, + "value": "click_me_123", + "type": "button", + "action_ts": "1716403200.549150" + } + ] + } +} \ No newline at end of file diff --git a/components/slack/sources/new-keyword-mention/new-keyword-mention.mjs b/components/slack/sources/new-keyword-mention/new-keyword-mention.mjs new file mode 100644 index 0000000000000..da8b56b1a0ed8 --- /dev/null +++ b/components/slack/sources/new-keyword-mention/new-keyword-mention.mjs @@ -0,0 +1,129 @@ +import common from "../common/base.mjs"; +import constants from "../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "slack-new-keyword-mention", + name: "New Keyword Mention (Instant)", + version: "0.0.6", + description: "Emit new event when a specific keyword is mentioned in a channel", + type: "source", + dedupe: "unique", + props: { + ...common.props, + conversations: { + propDefinition: [ + common.props.slack, + "conversation", + ], + type: "string[]", + label: "Channels", + description: "Select one or more channels to monitor for new messages.", + optional: true, + }, + // eslint-disable-next-line pipedream/props-description,pipedream/props-label + slackApphook: { + type: "$.interface.apphook", + appProp: "slack", + async eventNames() { + return this.conversations || [ + "message", + ]; + }, + }, + keyword: { + propDefinition: [ + common.props.slack, + "keyword", + ], + }, + ignoreBot: { + propDefinition: [ + common.props.slack, + "ignoreBot", + ], + }, + }, + hooks: { + ...common.hooks, + async deploy() { + // emit historical events + const messages = await this.getMatches({ + query: this.keyword, + sort: "timestamp", + }); + const filteredMessages = this.conversations?.length > 0 + ? messages.filter((message) => this.conversations.includes(message.channel.id)) + : messages; + await this.emitHistoricalEvents(filteredMessages.slice(-25).reverse()); + }, + }, + methods: { + ...common.methods, + async getMatches(params) { + return (await this.slack.searchMessages(params)).messages.matches || []; + }, + async emitHistoricalEvents(messages) { + for (const message of messages) { + const event = await this.processEvent({ + ...message, + subtype: message.subtype || constants.SUBTYPE.PD_HISTORY_MESSAGE, + }); + if (event) { + if (!event.client_msg_id) { + event.pipedream_msg_id = `pd_${Date.now()}_${Math.random().toString(36) + .substr(2, 10)}`; + } + + this.$emit(event, { + id: event.client_msg_id || event.pipedream_msg_id, + summary: this.getSummary(event), + ts: event.event_ts || Date.now(), + }); + } + } + }, + getSummary() { + return "New keyword mention received"; + }, + async processEvent(event) { + const { + type: msgType, + subtype, + bot_id: botId, + text, + } = event; + + if (msgType !== "message") { + console.log(`Ignoring event with unexpected type "${msgType}"`); + return; + } + + // This source is designed to just emit an event for each new message received. + // Due to inconsistencies with the shape of message_changed and message_deleted + // events, we are ignoring them for now. If you want to handle these types of + // events, feel free to change this code!! + if (subtype && !constants.ALLOWED_SUBTYPES.includes(subtype)) { + console.log(`Ignoring message with subtype. "${subtype}"`); + return; + } + + if ((this.ignoreBot) && (subtype === constants.SUBTYPE.BOT_MESSAGE || botId)) { + return; + } + + let emitEvent = false; + if (text.indexOf(this.keyword) !== -1) { + emitEvent = true; + } else if (subtype === constants.SUBTYPE.PD_HISTORY_MESSAGE) { + emitEvent = true; + } + + if (emitEvent) { + return event; + } + }, + }, + sampleEmit, +}; diff --git a/components/slack/sources/new-keyword-mention/test-event.mjs b/components/slack/sources/new-keyword-mention/test-event.mjs new file mode 100644 index 0000000000000..7c85b12599e3d --- /dev/null +++ b/components/slack/sources/new-keyword-mention/test-event.mjs @@ -0,0 +1,28 @@ +export default { + "user": "US676PZLY", + "type": "message", + "ts": "1716404766.096289", + "client_msg_id": "b26387fd-5afe-46a9-bf63-a7aabd6fb40f", + "text": "hello", + "team": "TS8319547", + "blocks": [ + { + "type": "rich_text", + "block_id": "aY6KK", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "text", + "text": "hello" + } + ] + } + ] + } + ], + "channel": "CS8319KD5", + "event_ts": "1716404766.096289", + "channel_type": "channel" +} \ No newline at end of file diff --git a/components/slack/sources/new-mention/new-mention.mjs b/components/slack/sources/new-mention/new-mention.mjs deleted file mode 100644 index 46c52014d9fb5..0000000000000 --- a/components/slack/sources/new-mention/new-mention.mjs +++ /dev/null @@ -1,151 +0,0 @@ -import common from "../common/base.mjs"; -import constants from "../common/constants.mjs"; - -export default { - ...common, - key: "slack-new-mention", - name: "New Mention (Instant)", - version: "1.0.15", - description: "Emit new event when a username or specific keyword is mentioned in a channel", - type: "source", - dedupe: "unique", - props: { - ...common.props, - conversations: { - propDefinition: [ - common.props.slack, - "conversation", - ], - type: "string[]", - label: "Channels", - description: "Select one or more channels to monitor for new messages.", - optional: true, - }, - // eslint-disable-next-line pipedream/props-description,pipedream/props-label - slackApphook: { - type: "$.interface.apphook", - appProp: "slack", - async eventNames() { - return this.conversations || [ - "message", - ]; - }, - }, - keyword: { - propDefinition: [ - common.props.slack, - "keyword", - ], - }, - isUsername: { - propDefinition: [ - common.props.slack, - "isUsername", - ], - }, - ignoreBot: { - propDefinition: [ - common.props.slack, - "ignoreBot", - ], - }, - }, - hooks: { - ...common.hooks, - async deploy() { - // emit historical events - const messages = await this.getMatches({ - query: this.keyword, - sort: "timestamp", - }); - const filteredMessages = this.conversations?.length > 0 - ? messages.filter((message) => this.conversations.includes(message.channel.id)) - : messages; - await this.emitHistoricalEvents(filteredMessages.slice(-25).reverse()); - }, - }, - methods: { - ...common.methods, - async getMatches(params) { - return (await this.slack.searchMessages(params)).messages.matches || []; - }, - async emitHistoricalEvents(messages) { - for (const message of messages) { - const event = await this.processEvent({ - ...message, - subtype: message.subtype || constants.SUBTYPE.PD_HISTORY_MESSAGE, - }); - if (event) { - if (!event.client_msg_id) { - event.pipedream_msg_id = `pd_${Date.now()}_${Math.random().toString(36) - .substr(2, 10)}`; - } - - this.$emit(event, { - id: event.client_msg_id || event.pipedream_msg_id, - summary: this.getSummary(event), - ts: event.event_ts || Date.now(), - }); - } - } - }, - getSummary() { - return "New mention received"; - }, - async processEvent(event) { - const { - type: msgType, - subtype, - bot_id: botId, - text, - blocks = [], - } = event; - const [ - { - elements: [ - { elements = [] } = {}, - ] = [], - } = {}, - ] = blocks; - - if (msgType !== "message") { - console.log(`Ignoring event with unexpected type "${msgType}"`); - return; - } - - // This source is designed to just emit an event for each new message received. - // Due to inconsistencies with the shape of message_changed and message_deleted - // events, we are ignoring them for now. If you want to handle these types of - // events, feel free to change this code!! - if (subtype && !constants.ALLOWED_SUBTYPES.includes(subtype)) { - console.log(`Ignoring message with subtype. "${subtype}"`); - return; - } - - if ((this.ignoreBot) && (subtype === constants.SUBTYPE.BOT_MESSAGE || botId)) { - return; - } - - let emitEvent = false; - if (this.isUsername && elements) { - for (const item of elements) { - if (item.user_id) { - const username = await this.getUserName(item.user_id); - if (username === this.keyword) { - emitEvent = true; - break; - } - } - } - } else if (text.indexOf(this.keyword) !== -1) { - emitEvent = true; - } else if (subtype === constants.SUBTYPE.PD_HISTORY_MESSAGE) { - emitEvent = true; - } - - if (emitEvent) { - return event; - } - }, - }, -}; diff --git a/components/slack/sources/new-message-in-channels/new-message-in-channels.mjs b/components/slack/sources/new-message-in-channels/new-message-in-channels.mjs index c973b6f699a9c..1ab6d48129082 100644 --- a/components/slack/sources/new-message-in-channels/new-message-in-channels.mjs +++ b/components/slack/sources/new-message-in-channels/new-message-in-channels.mjs @@ -1,12 +1,12 @@ import common from "../common/base.mjs"; -import sampleEmit from "./test-event.mjs"; import constants from "../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "slack-new-message-in-channels", name: "New Message In Channels (Instant)", - version: "1.0.14", + version: "1.0.23", description: "Emit new event when a new message is posted to one or more channels", type: "source", dedupe: "unique", @@ -72,7 +72,6 @@ export default { if ((this.ignoreBot) && (event.subtype == "bot_message" || event.bot_id)) { return; } - console.log(event.s); // There is no thread message type only the thread_ts field // indicates if the message is part of a thread in the event. if (this.ignoreThreads && event.thread_ts) { diff --git a/components/slack/sources/new-reaction-added/new-reaction-added.mjs b/components/slack/sources/new-reaction-added/new-reaction-added.mjs index 7f336f5bf2d7c..6dd322c14a644 100644 --- a/components/slack/sources/new-reaction-added/new-reaction-added.mjs +++ b/components/slack/sources/new-reaction-added/new-reaction-added.mjs @@ -1,10 +1,11 @@ import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "slack-new-reaction-added", name: "New Reaction Added (Instant)", - version: "1.1.16", + version: "1.1.24", description: "Emit new event when a member has added an emoji reaction to a message", type: "source", dedupe: "unique", @@ -54,6 +55,7 @@ export default { optional: true, }, includeUserData: { + label: "Include User Data", description: "Include user object in the response. Default `false`", type: "boolean", optional: true, @@ -103,4 +105,5 @@ export default { return event; }, }, + sampleEmit, }; diff --git a/components/slack/sources/new-reaction-added/test-event.mjs b/components/slack/sources/new-reaction-added/test-event.mjs new file mode 100644 index 0000000000000..106603e2da1c3 --- /dev/null +++ b/components/slack/sources/new-reaction-added/test-event.mjs @@ -0,0 +1,193 @@ +export default { + "type": "reaction_added", + "user": "US676PZLY", + "reaction": "squirrel", + "item": { + "type": "message", + "channel": "CS8319KD5", + "ts": "1716405857.659549" + }, + "item_user": "US676PZLY", + "event_ts": "1716406183.000100", + "userInfo": { + "id": "US676PZLY", + "team_id": "TS8319547", + "name": "test.user", + "deleted": false, + "color": "9f69e7", + "real_name": "Test User", + "tz": "America/New_York", + "tz_label": "Eastern Daylight Time", + "tz_offset": -14400, + "profile": { + "title": "", + "phone": "", + "skype": "", + "real_name": "Test User", + "real_name_normalized": "Test User", + "display_name": "", + "display_name_normalized": "", + "fields": null, + "status_text": "", + "status_emoji": "", + "status_emoji_display_info": [], + "status_expiration": 0, + "avatar_hash": "g010b11df3bb", + "email": "test@sample.com", + "first_name": "Test", + "last_name": "User", + "status_text_canonical": "", + "team": "TS8319547" + }, + "is_admin": true, + "is_owner": true, + "is_primary_owner": true, + "is_restricted": false, + "is_ultra_restricted": false, + "is_bot": false, + "is_app_user": false, + "updated": 1703787612, + "is_email_confirmed": true, + "has_2fa": false, + "who_can_share_contact_card": "EVERYONE" + }, + "itemUserInfo": { + "id": "US676PZLY", + "team_id": "TS8319547", + "name": "test.user", + "deleted": false, + "color": "9f69e7", + "real_name": "Test User", + "tz": "America/New_York", + "tz_label": "Eastern Daylight Time", + "tz_offset": -14400, + "profile": { + "title": "", + "phone": "", + "skype": "", + "real_name": "Test User", + "real_name_normalized": "Test User", + "display_name": "", + "display_name_normalized": "", + "fields": null, + "status_text": "", + "status_emoji": "", + "status_emoji_display_info": [], + "status_expiration": 0, + "avatar_hash": "g010b11df3bb", + "email": "test@sample.com", + "first_name": "Test", + "last_name": "User", + "status_text_canonical": "", + "team": "TS8319547" + }, + "is_admin": true, + "is_owner": true, + "is_primary_owner": true, + "is_restricted": false, + "is_ultra_restricted": false, + "is_bot": false, + "is_app_user": false, + "updated": 1703787612, + "is_email_confirmed": true, + "has_2fa": false, + "who_can_share_contact_card": "EVERYONE" + }, + "message": { + "ok": true, + "messages": [ + { + "user": "US676PZLY", + "type": "message", + "ts": "1716405857.659549", + "client_msg_id": "fd68d844-687e-41bf-8475-4215bef572c7", + "text": "hello", + "team": "TS8319547", + "blocks": [ + { + "type": "rich_text", + "block_id": "ZL1yL", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "text", + "text": "hello" + } + ] + } + ] + } + ], + "reactions": [ + { + "name": "squirrel", + "users": [ + "US676PZLY" + ], + "count": 1 + } + ] + } + ], + "has_more": false, + "response_metadata": { + "scopes": [ + "identify", + "commands", + "channels:history", + "groups:history", + "im:history", + "mpim:history", + "channels:read", + "emoji:read", + "files:read", + "groups:read", + "im:read", + "mpim:read", + "reactions:read", + "reminders:read", + "search:read", + "stars:read", + "team:read", + "users:read", + "users:read.email", + "pins:read", + "usergroups:read", + "dnd:read", + "users.profile:read", + "channels:write", + "chat:write:user", + "chat:write:bot", + "files:write:user", + "groups:write", + "im:write", + "mpim:write", + "reactions:write", + "reminders:write", + "stars:write", + "users:write", + "pins:write", + "usergroups:write", + "dnd:write", + "users.profile:write", + "links:read", + "links:write", + "remote_files:share", + "remote_files:read", + "bookmarks:write", + "calls:write", + "calls:read" + ], + "acceptedScopes": [ + "channels:history", + "groups:history", + "mpim:history", + "im:history", + "read" + ] + } + }, + "pipedream_msg_id": "pd_1716406186371_xezly8lgzn" +} \ No newline at end of file diff --git a/components/slack/sources/new-saved-message/new-saved-message.mjs b/components/slack/sources/new-saved-message/new-saved-message.mjs new file mode 100644 index 0000000000000..70126267892b3 --- /dev/null +++ b/components/slack/sources/new-saved-message/new-saved-message.mjs @@ -0,0 +1,32 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "slack-new-saved-message", + name: "New Saved Message (Instant)", + version: "0.0.4", + description: "Emit new event when a message is saved. Note: The endpoint is marked as deprecated, and Slack might shut this off at some point down the line.", + type: "source", + dedupe: "unique", + props: { + ...common.props, + // eslint-disable-next-line pipedream/props-description,pipedream/props-label + slackApphook: { + type: "$.interface.apphook", + appProp: "slack", + async eventNames() { + return [ + "star_added", + ]; + }, + }, + }, + methods: { + ...common.methods, + getSummary() { + return "New saved message"; + }, + }, + sampleEmit, +}; diff --git a/components/slack/sources/new-saved-message/test-event.mjs b/components/slack/sources/new-saved-message/test-event.mjs new file mode 100644 index 0000000000000..433e5bcb8d4ec --- /dev/null +++ b/components/slack/sources/new-saved-message/test-event.mjs @@ -0,0 +1,37 @@ +export default { + "type": "star_added", + "user": "US676PZLY", + "item": { + "type": "message", + "channel": "C055ECVUMLN", + "message": { + "user": "US676PZLY", + "type": "message", + "ts": "1718379912.272779", + "client_msg_id": "def19b3b-4283-47bd-a2da-f32b35c0329c", + "text": "hello", + "team": "TS8319547", + "blocks": [ + { + "type": "rich_text", + "block_id": "ZL1yL", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "text", + "text": "hello" + } + ] + } + ] + } + ], + "permalink": "https://michellestest-j1q3506.slack.com/archives/C055ECVUMLN/p1718379912272779" + }, + "date_create": 1718385156 + }, + "event_ts": "1718385156.694322", + "pipedream_msg_id": "pd_1718385158733_tl8yx25evl" +} \ No newline at end of file diff --git a/components/slack/sources/new-star-added/new-star-added.mjs b/components/slack/sources/new-star-added/new-star-added.mjs deleted file mode 100644 index f5e527aa56855..0000000000000 --- a/components/slack/sources/new-star-added/new-star-added.mjs +++ /dev/null @@ -1,45 +0,0 @@ -import common from "../common/base.mjs"; -import constants from "../common/constants.mjs"; - -export default { - ...common, - key: "slack-new-star-added", - name: "New Star Added (Instant)", - version: "0.0.17", - description: "Emit new event when a star is added to an item", - type: "source", - dedupe: "unique", - props: { - ...common.props, - // eslint-disable-next-line pipedream/props-description,pipedream/props-label - slackApphook: { - type: "$.interface.apphook", - appProp: "slack", - async eventNames() { - return [ - "star_added", - ]; - }, - }, - eventTypes: { - type: "string[]", - label: "Event Types", - description: "The types of event to emit. If not specified, all events will be emitted.", - options: constants.eventsOptions, - optional: true, - }, - }, - methods: { - ...common.methods, - getSummary({ item: { type } }) { - return `New star added - ${constants.events[type] ?? type}`; - }, - async processEvent(event) { - if (this.eventTypes == null - || this.eventTypes.length === 0 - || this.eventTypes.includes(event.item.type)) { - return event; - } - }, - }, -}; diff --git a/components/slack/sources/new-user-added/new-user-added.mjs b/components/slack/sources/new-user-added/new-user-added.mjs new file mode 100644 index 0000000000000..78f9f8cad3f51 --- /dev/null +++ b/components/slack/sources/new-user-added/new-user-added.mjs @@ -0,0 +1,32 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "slack-new-user-added", + name: "New User Added (Instant)", + version: "0.0.2", + description: "Emit new event when a new member joins a workspace.", + type: "source", + dedupe: "unique", + props: { + ...common.props, + // eslint-disable-next-line pipedream/props-description,pipedream/props-label + slackApphook: { + type: "$.interface.apphook", + appProp: "slack", + async eventNames() { + return [ + "team_join", + ]; + }, + }, + }, + methods: { + ...common.methods, + getSummary({ user: { name } }) { + return `New User: ${name}`; + }, + }, + sampleEmit, +}; diff --git a/components/slack/sources/new-user-added/test-event.mjs b/components/slack/sources/new-user-added/test-event.mjs new file mode 100644 index 0000000000000..3bcd19da42f6a --- /dev/null +++ b/components/slack/sources/new-user-added/test-event.mjs @@ -0,0 +1,48 @@ +export default { + "type": "team_join", + "user": { + "id": "U080GULP8SD", + "team_id": "TS8319547", + "name": "", + "deleted": false, + "color": "9b3b45", + "real_name": "", + "tz": "America/New_York", + "tz_label": "Eastern Standard Time", + "tz_offset": -18000, + "profile": { + "title": "", + "phone": "", + "skype": "", + "real_name": "", + "real_name_normalized": "", + "display_name": "", + "display_name_normalized": "", + "fields": {}, + "status_text": "", + "status_emoji": "", + "status_emoji_display_info": [], + "status_expiration": 0, + "avatar_hash": "g96b6e8b38c2", + "email": "", + "first_name": "", + "last_name": "", + "status_text_canonical": "", + "team": "TS8319547" + }, + "is_admin": false, + "is_owner": false, + "is_primary_owner": false, + "is_restricted": false, + "is_ultra_restricted": false, + "is_bot": false, + "is_app_user": false, + "updated": 1731094476, + "is_email_confirmed": true, + "who_can_share_contact_card": "EVERYONE", + "presence": "away" + }, + "cache_ts": 1731094476, + "event_ts": "1731094477.001400", + "pipedream_msg_id": "pd_1731094479305_v1ic236by8" +} \ No newline at end of file diff --git a/components/slack/sources/new-user-mention/new-user-mention.mjs b/components/slack/sources/new-user-mention/new-user-mention.mjs new file mode 100644 index 0000000000000..369b92ba15a7a --- /dev/null +++ b/components/slack/sources/new-user-mention/new-user-mention.mjs @@ -0,0 +1,117 @@ +import common from "../common/base.mjs"; +import constants from "../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "slack-new-user-mention", + name: "New User Mention (Instant)", + version: "0.0.6", + description: "Emit new event when a username or specific keyword is mentioned in a channel", + type: "source", + dedupe: "unique", + props: { + ...common.props, + conversations: { + propDefinition: [ + common.props.slack, + "conversation", + ], + type: "string[]", + label: "Channels", + description: "Select one or more channels to monitor for new messages.", + optional: true, + }, + // eslint-disable-next-line pipedream/props-description,pipedream/props-label + slackApphook: { + type: "$.interface.apphook", + appProp: "slack", + async eventNames() { + return this.conversations || [ + "message", + ]; + }, + }, + user: { + propDefinition: [ + common.props.slack, + "user", + ], + }, + keyword: { + propDefinition: [ + common.props.slack, + "keyword", + ], + optional: true, + }, + ignoreBot: { + propDefinition: [ + common.props.slack, + "ignoreBot", + ], + }, + }, + methods: { + ...common.methods, + getSummary() { + return "New mention received"; + }, + async processEvent(event) { + const { + type: msgType, + subtype, + bot_id: botId, + text, + blocks = [], + } = event; + const [ + { + elements: [ + { elements = [] } = {}, + ] = [], + } = {}, + ] = blocks; + + if (msgType !== "message") { + console.log(`Ignoring event with unexpected type "${msgType}"`); + return; + } + + // This source is designed to just emit an event for each new message received. + // Due to inconsistencies with the shape of message_changed and message_deleted + // events, we are ignoring them for now. If you want to handle these types of + // events, feel free to change this code!! + if (subtype && !constants.ALLOWED_SUBTYPES.includes(subtype)) { + console.log(`Ignoring message with subtype. "${subtype}"`); + return; + } + + if ((this.ignoreBot) && (subtype === constants.SUBTYPE.BOT_MESSAGE || botId)) { + return; + } + + let emitEvent = false; + if (elements) { + let userMatch = false; + for (const item of elements) { + if (item.user_id && item.user_id === this.user) { + userMatch = true; + break; + } + } + if (userMatch && (!this.keyword || text.indexOf(this.keyword) !== -1)) { + emitEvent = true; + } + } + if (subtype === constants.SUBTYPE.PD_HISTORY_MESSAGE) { + emitEvent = true; + } + + if (emitEvent) { + return event; + } + }, + }, + sampleEmit, +}; diff --git a/components/slack/sources/new-user-mention/test-event.mjs b/components/slack/sources/new-user-mention/test-event.mjs new file mode 100644 index 0000000000000..7c85b12599e3d --- /dev/null +++ b/components/slack/sources/new-user-mention/test-event.mjs @@ -0,0 +1,28 @@ +export default { + "user": "US676PZLY", + "type": "message", + "ts": "1716404766.096289", + "client_msg_id": "b26387fd-5afe-46a9-bf63-a7aabd6fb40f", + "text": "hello", + "team": "TS8319547", + "blocks": [ + { + "type": "rich_text", + "block_id": "aY6KK", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "text", + "text": "hello" + } + ] + } + ] + } + ], + "channel": "CS8319KD5", + "event_ts": "1716404766.096289", + "channel_type": "channel" +} \ No newline at end of file diff --git a/components/slack_bot/README.md b/components/slack_bot/README.md index 6c68ef4e9c11f..7b9534b1c6879 100644 --- a/components/slack_bot/README.md +++ b/components/slack_bot/README.md @@ -1,22 +1,11 @@ # Overview -Slack Bot APIs offer powerful and flexible ways to automate repetitive tasks -and integrate workflows from various services. With the help of these APIs, it -is possible to develop Slack bots capable of exiting and entering specific -channels as well as performing various actions requested from users. +The Slack Bot API allows you to build rich, interactive bots for Slack workspaces. These bots can respond to messages, post updates, and interact with users in various ways. With the Slack Bot API on Pipedream, developers can create automated workflows that trigger on specific events in Slack, such as new messages or reactions, and then perform defined actions, like sending data to other apps or processing the information within Pipedream's serverless platform. This tight integration with Pipedream enables both simple and complex automations, leveraging Pipedream's ability to connect with numerous apps and its powerful built-in code steps. -A Slack bot can be used to automate specific tasks and carry out routine admin -jobs such as checking the team’s working time, organizing team meetings, -managing customer inquiries and so on. These bots can be programmed to inform -the members of the team when tasks are completed. Some of the examples of what -you can create using the Slack Bot APIs are: +# Example Use Cases -- Automated reminders -- Productivity and time tracking -- Data analysis and processing -- Internal communication releases -- Integrations with external tools and services -- Conversation and inquiry management -- Automation of customer service -- Scheduling and tracking of tasks -- Personalised customer recommendations +- **Issue Tracker Integration**: When a user mentions a bug in a Slack channel, the bot can capture the message, format it, and create an issue in a tool like GitHub or Jira. This streamlines the bug reporting process and ensures that issues are tracked and managed efficiently. + +- **Customer Support Ticket Creation**: If a customer asks for help in a dedicated support channel, the Slack Bot can trigger a workflow that creates a support ticket in a CRM or helpdesk software like Zendesk. The bot can also reply to the customer with a ticket number and expected response time, improving the customer service experience. + +- **Daily Stand-up Automation**: The Slack Bot can initiate daily stand-up meetings by sending a message to a team channel asking for updates. It then collects responses, formats them into a report, and sends the compiled update to a project management app like Trello or Asana, or even emails it to stakeholders, keeping everyone informed with minimal manual intervention. diff --git a/components/slack_bot/actions/add-emoji-reaction/add-emoji-reaction.mjs b/components/slack_bot/actions/add-emoji-reaction/add-emoji-reaction.mjs new file mode 100644 index 0000000000000..b2cbd842ef5d8 --- /dev/null +++ b/components/slack_bot/actions/add-emoji-reaction/add-emoji-reaction.mjs @@ -0,0 +1,15 @@ +import component from "../../../slack/actions/add-emoji-reaction/add-emoji-reaction.mjs"; +import utils from "../../common/utils.mjs"; + +/* eslint-disable pipedream/required-properties-type */ +/* eslint-disable pipedream/required-properties-name */ + +export default { + ...component, + props: utils.buildAppProps({ + component, + }), + key: "slack_bot-add-emoji-reaction", + description: "Add an emoji reaction to a message. [See the documentation](https://api.slack.com/methods/reactions.add)", + version: "0.0.2", +}; diff --git a/components/slack_bot/actions/archive-channel/archive-channel.mjs b/components/slack_bot/actions/archive-channel/archive-channel.mjs index ebf22ecdfc6ef..ff30f5dc3f953 100644 --- a/components/slack_bot/actions/archive-channel/archive-channel.mjs +++ b/components/slack_bot/actions/archive-channel/archive-channel.mjs @@ -13,20 +13,19 @@ export default { conversation: { propDefinition: [ undefined, - "channelId", + "conversation", () => ({ types: [ constants.CHANNEL_TYPE.PUBLIC, constants.CHANNEL_TYPE.PRIVATE, constants.CHANNEL_TYPE.MPIM, ], - channelsFilter: (channel) => channel.is_member, }), ], }, }, }), key: "slack_bot-archive-channel", - description: "Archive a channel (Bot). [See docs here](https://api.slack.com/methods/conversations.archive)", - version: "0.0.2", + description: "Archive a channel (Bot). [See the documentation](https://api.slack.com/methods/conversations.archive)", + version: "0.0.4", }; diff --git a/components/slack_bot/actions/create-channel/create-channel.mjs b/components/slack_bot/actions/create-channel/create-channel.mjs index 9f24f8a3d2b80..38e483a573076 100644 --- a/components/slack_bot/actions/create-channel/create-channel.mjs +++ b/components/slack_bot/actions/create-channel/create-channel.mjs @@ -10,6 +10,6 @@ export default { component, }), key: "slack_bot-create-channel", - description: "Create a new channel (Bot). [See docs here](https://api.slack.com/methods/conversations.create)", - version: "0.0.2", + description: "Create a new channel (Bot). [See the documentation](https://api.slack.com/methods/conversations.create)", + version: "0.0.4", }; diff --git a/components/slack_bot/actions/delete-file/delete-file.mjs b/components/slack_bot/actions/delete-file/delete-file.mjs index 4ed44c6230e47..f78a6c22c670f 100644 --- a/components/slack_bot/actions/delete-file/delete-file.mjs +++ b/components/slack_bot/actions/delete-file/delete-file.mjs @@ -10,6 +10,6 @@ export default { component, }), key: "slack_bot-delete-file", - description: "Delete a file (Bot). [See docs here](https://api.slack.com/methods/files.delete)", - version: "0.0.2", + description: "Delete a file (Bot). [See the documentation](https://api.slack.com/methods/files.delete)", + version: "0.0.4", }; diff --git a/components/slack_bot/actions/delete-message/delete-message.mjs b/components/slack_bot/actions/delete-message/delete-message.mjs index 7882f70fe7a1d..024b0c0bd4bdc 100644 --- a/components/slack_bot/actions/delete-message/delete-message.mjs +++ b/components/slack_bot/actions/delete-message/delete-message.mjs @@ -13,6 +13,6 @@ export default { ], }), key: "slack_bot-delete-message", - description: "Delete a message (Bot). [See docs here](https://api.slack.com/methods/chat.delete)", - version: "0.0.2", + description: "Delete a message (Bot). [See the documentation](https://api.slack.com/methods/chat.delete)", + version: "0.0.4", }; diff --git a/components/slack_bot/actions/find-user-by-email/find-user-by-email.mjs b/components/slack_bot/actions/find-user-by-email/find-user-by-email.mjs index c672c854bd862..7b5ad6006b82b 100644 --- a/components/slack_bot/actions/find-user-by-email/find-user-by-email.mjs +++ b/components/slack_bot/actions/find-user-by-email/find-user-by-email.mjs @@ -10,6 +10,6 @@ export default { component, }), key: "slack_bot-find-user-by-email", - description: "Find a user by matching against their email (Bot). [See docs here](https://api.slack.com/methods/users.lookupByEmail)", - version: "0.0.2", + description: "Find a user by matching against their email (Bot). [See the documentation](https://api.slack.com/methods/users.lookupByEmail)", + version: "0.0.4", }; diff --git a/components/slack_bot/actions/get-channel/get-channel.mjs b/components/slack_bot/actions/get-channel/get-channel.mjs deleted file mode 100644 index 21836815c9591..0000000000000 --- a/components/slack_bot/actions/get-channel/get-channel.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import component from "../../../slack/actions/get-channel/get-channel.mjs"; -import utils from "../../common/utils.mjs"; -import constants from "../../common/constants.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...component, - props: utils.buildAppProps({ - component, - omitProps: [ - "conversation", - ], - addedProps: { - conversation: { - propDefinition: [ - undefined, - "channelId", - () => ({ - types: [ - constants.CHANNEL_TYPE.PUBLIC, - constants.CHANNEL_TYPE.PRIVATE, - constants.CHANNEL_TYPE.MPIM, - constants.CHANNEL_TYPE.IM, - ], - }), - ], - }, - }, - }), - key: "slack_bot-get-channel", - description: "Return information about a workspace channel (Bot). [See docs here](https://api.slack.com/methods/conversations.info)", - version: "0.0.2", -}; diff --git a/components/slack_bot/actions/get-file/get-file.mjs b/components/slack_bot/actions/get-file/get-file.mjs index b9710a9aaef69..979416d402537 100644 --- a/components/slack_bot/actions/get-file/get-file.mjs +++ b/components/slack_bot/actions/get-file/get-file.mjs @@ -10,6 +10,6 @@ export default { component, }), key: "slack_bot-get-file", - description: "Return information about a file (Bot). [See docs here](https://api.slack.com/methods/files.info)", - version: "0.0.2", + description: "Return information about a file (Bot). [See the documentation](https://api.slack.com/methods/files.info)", + version: "0.0.4", }; diff --git a/components/slack_bot/actions/invite-user-to-channel/invite-user-to-channel.mjs b/components/slack_bot/actions/invite-user-to-channel/invite-user-to-channel.mjs index 55a71213f7c5f..05dde4f7b9c55 100644 --- a/components/slack_bot/actions/invite-user-to-channel/invite-user-to-channel.mjs +++ b/components/slack_bot/actions/invite-user-to-channel/invite-user-to-channel.mjs @@ -10,6 +10,6 @@ export default { component, }), key: "slack_bot-invite-user-to-channel", - description: "Invite a user to an existing channel (Bot). [See docs here](https://api.slack.com/methods/conversations.invite)", - version: "0.0.2", + description: "Invite a user to an existing channel (Bot). [See the documentation](https://api.slack.com/methods/conversations.invite)", + version: "0.0.4", }; diff --git a/components/slack_bot/actions/join-channel/join-channel.mjs b/components/slack_bot/actions/join-channel/join-channel.mjs deleted file mode 100644 index 501e4c1771bc9..0000000000000 --- a/components/slack_bot/actions/join-channel/join-channel.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import component from "../../../slack/actions/join-channel/join-channel.mjs"; -import constants from "../../common/constants.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...component, - props: utils.buildAppProps({ - component, - omitProps: [ - "conversation", - ], - addedProps: { - conversation: { - propDefinition: [ - undefined, - "channelId", - () => ({ - types: [ - constants.CHANNEL_TYPE.PUBLIC, - constants.CHANNEL_TYPE.MPIM, - ], - channelsFilter: (channel) => !channel.is_member, - }), - ], - }, - }, - }), - key: "slack_bot-join-channel", - description: "Join an existing channel (Bot). [See docs here](https://api.slack.com/methods/conversations.join)", - version: "0.0.2", -}; diff --git a/components/slack_bot/actions/leave-channel/leave-channel.mjs b/components/slack_bot/actions/leave-channel/leave-channel.mjs deleted file mode 100644 index 33148cae9e2ad..0000000000000 --- a/components/slack_bot/actions/leave-channel/leave-channel.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import component from "../../../slack/actions/leave-channel/leave-channel.mjs"; -import constants from "../../common/constants.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...component, - props: utils.buildAppProps({ - component, - omitProps: [ - "conversation", - ], - addedProps: { - conversation: { - propDefinition: [ - undefined, - "channelId", - () => ({ - types: [ - constants.CHANNEL_TYPE.PUBLIC, - constants.CHANNEL_TYPE.PRIVATE, - constants.CHANNEL_TYPE.MPIM, - ], - channelsFilter: (channel) => channel.is_member, - }), - ], - }, - }, - }), - key: "slack_bot-leave-channel", - description: "Leave an existing channel (Bot). [See docs here](https://api.slack.com/methods/conversations.leave)", - version: "0.0.2", -}; diff --git a/components/slack_bot/actions/list-channels/list-channels.mjs b/components/slack_bot/actions/list-channels/list-channels.mjs index 72d09f15a6041..adf71170d26cb 100644 --- a/components/slack_bot/actions/list-channels/list-channels.mjs +++ b/components/slack_bot/actions/list-channels/list-channels.mjs @@ -10,6 +10,6 @@ export default { component, }), key: "slack_bot-list-channels", - description: "Return a list of all channels in a workspace (Bot). [See docs here](https://api.slack.com/methods/conversations.list)", - version: "0.0.2", + description: "Return a list of all channels in a workspace (Bot). [See the documentation](https://api.slack.com/methods/conversations.list)", + version: "0.0.4", }; diff --git a/components/slack_bot/actions/list-files/list-files.mjs b/components/slack_bot/actions/list-files/list-files.mjs index 68bd117650c9b..d016f72012dd6 100644 --- a/components/slack_bot/actions/list-files/list-files.mjs +++ b/components/slack_bot/actions/list-files/list-files.mjs @@ -16,21 +16,19 @@ export default { conversation: { propDefinition: [ undefined, - "channelId", + "conversation", () => ({ types: [ constants.CHANNEL_TYPE.PUBLIC, constants.CHANNEL_TYPE.PRIVATE, constants.CHANNEL_TYPE.MPIM, - constants.CHANNEL_TYPE.IM, ], - channelsFilter: (channel) => channel.is_im || channel.is_member, }), ], }, }, }), key: "slack_bot-list-files", - description: "Return a list of files within a team (Bot). [See docs here](https://api.slack.com/methods/files.list)", - version: "0.0.2", + description: "Return a list of files within a team (Bot). [See the documentation](https://api.slack.com/methods/files.list)", + version: "0.0.4", }; diff --git a/components/slack_bot/actions/list-group-members/list-group-members.mjs b/components/slack_bot/actions/list-group-members/list-group-members.mjs new file mode 100644 index 0000000000000..cf461541c4dd7 --- /dev/null +++ b/components/slack_bot/actions/list-group-members/list-group-members.mjs @@ -0,0 +1,15 @@ +import component from "../../../slack/actions/list-group-members/list-group-members.mjs"; +import utils from "../../common/utils.mjs"; + +/* eslint-disable pipedream/required-properties-type */ +/* eslint-disable pipedream/required-properties-name */ + +export default { + ...component, + props: utils.buildAppProps({ + component, + }), + key: "slack_bot-list-group-members", + description: "List all users in a User Group (Bot). [See the documentation](https://api.slack.com/methods/usergroups.users.list)", + version: "0.0.2", +}; diff --git a/components/slack_bot/actions/list-members-in-channel/list-members-in-channel.mjs b/components/slack_bot/actions/list-members-in-channel/list-members-in-channel.mjs index 66c9fd0cd2189..1e0785b59b0ae 100644 --- a/components/slack_bot/actions/list-members-in-channel/list-members-in-channel.mjs +++ b/components/slack_bot/actions/list-members-in-channel/list-members-in-channel.mjs @@ -16,7 +16,7 @@ export default { conversation: { propDefinition: [ undefined, - "channelId", + "conversation", () => ({ types: [ constants.CHANNEL_TYPE.PUBLIC, @@ -29,6 +29,6 @@ export default { }, }), key: "slack_bot-list-members-in-channel", - description: "Retrieve members of a channel (Bot). [See docs here](https://api.slack.com/methods/conversations.members)", - version: "0.0.2", + description: "Retrieve members of a channel (Bot). [See the documentation](https://api.slack.com/methods/conversations.members)", + version: "0.0.4", }; diff --git a/components/slack_bot/actions/list-replies/list-replies.mjs b/components/slack_bot/actions/list-replies/list-replies.mjs index d4ab898b58dda..410be522a946c 100644 --- a/components/slack_bot/actions/list-replies/list-replies.mjs +++ b/components/slack_bot/actions/list-replies/list-replies.mjs @@ -17,7 +17,7 @@ export default { conversation: { propDefinition: [ undefined, - "channelId", + "conversation", () => ({ types: [ constants.CHANNEL_TYPE.PUBLIC, @@ -31,13 +31,13 @@ export default { timestamp: { propDefinition: [ undefined, - "timestamp", + "messageTs", ], optional: false, }, }, }), key: "slack_bot-list-replies", - description: "Retrieve a thread of messages posted to a conversation (Bot). [See docs here](https://api.slack.com/methods/conversations.replies)", - version: "0.0.2", + description: "Retrieve a thread of messages posted to a conversation (Bot). [See the documentation](https://api.slack.com/methods/conversations.replies)", + version: "0.0.5", }; diff --git a/components/slack_bot/actions/list-user-groups-users/list-user-groups-users.mjs b/components/slack_bot/actions/list-user-groups-users/list-user-groups-users.mjs deleted file mode 100644 index bc570f29e2570..0000000000000 --- a/components/slack_bot/actions/list-user-groups-users/list-user-groups-users.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import component from "../../../slack/actions/list-user-groups-users/list-user-groups-users.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...component, - props: utils.buildAppProps({ - component, - }), - key: "slack_bot-list-user-groups-users", - description: "List all users in a User Group (Bot). [See docs here](https://api.slack.com/methods/usergroups.users.list)", - version: "0.0.2", -}; diff --git a/components/slack_bot/actions/list-users/list-users.mjs b/components/slack_bot/actions/list-users/list-users.mjs index 56485bc405b42..71d7d375a161b 100644 --- a/components/slack_bot/actions/list-users/list-users.mjs +++ b/components/slack_bot/actions/list-users/list-users.mjs @@ -10,6 +10,6 @@ export default { component, }), key: "slack_bot-list-users", - description: "Return a list of all users in a workspace (Bot). [See docs here](https://api.slack.com/methods/users.list)", - version: "0.0.2", + description: "Return a list of all users in a workspace (Bot). [See the documentation](https://api.slack.com/methods/users.list)", + version: "0.0.4", }; diff --git a/components/slack_bot/actions/reply-to-a-message/reply-to-a-message.mjs b/components/slack_bot/actions/reply-to-a-message/reply-to-a-message.mjs index e586189504b4b..bd66ae9aa8362 100644 --- a/components/slack_bot/actions/reply-to-a-message/reply-to-a-message.mjs +++ b/components/slack_bot/actions/reply-to-a-message/reply-to-a-message.mjs @@ -14,5 +14,5 @@ export default { }), key: "slack_bot-reply-to-a-message", description: "Send a message as a threaded reply (Bot). See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here", - version: "0.0.2", + version: "0.0.5", }; diff --git a/components/slack_bot/actions/send-block-kit-message/send-block-kit-message.mjs b/components/slack_bot/actions/send-block-kit-message/send-block-kit-message.mjs deleted file mode 100644 index edca9068c2730..0000000000000 --- a/components/slack_bot/actions/send-block-kit-message/send-block-kit-message.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import component from "../../../slack/actions/send-block-kit-message/send-block-kit-message.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...component, - props: utils.buildAppProps({ - component, - omitProps: [ - "as_user", - ], - }), - key: "slack_bot-send-block-kit-message", - description: "Send a message using Slack's Block Kit UI framework to a channel, group or user (Bot). See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here", - version: "0.0.2", -}; diff --git a/components/slack_bot/actions/send-custom-message/send-custom-message.mjs b/components/slack_bot/actions/send-custom-message/send-custom-message.mjs deleted file mode 100644 index 7a8d7e6167e9a..0000000000000 --- a/components/slack_bot/actions/send-custom-message/send-custom-message.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import component from "../../../slack/actions/send-custom-message/send-custom-message.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...component, - props: utils.buildAppProps({ - component, - omitProps: [ - "as_user", - ], - }), - key: "slack_bot-send-custom-message", - description: "Customize advanced setttings and send a message to a channel, group or user (Bot). See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here", - version: "0.0.2", -}; diff --git a/components/slack_bot/actions/send-direct-message/send-direct-message.mjs b/components/slack_bot/actions/send-direct-message/send-direct-message.mjs deleted file mode 100644 index 286811d02a80b..0000000000000 --- a/components/slack_bot/actions/send-direct-message/send-direct-message.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import component from "../../../slack/actions/send-direct-message/send-direct-message.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...component, - props: utils.buildAppProps({ - component, - omitProps: [ - "as_user", - ], - }), - key: "slack_bot-send-direct-message", - description: "Send a direct message to a single user (Bot). See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here", - version: "0.0.2", -}; diff --git a/components/slack_bot/actions/send-group-message/send-group-message.mjs b/components/slack_bot/actions/send-group-message/send-group-message.mjs deleted file mode 100644 index c21fa7c69cf49..0000000000000 --- a/components/slack_bot/actions/send-group-message/send-group-message.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import component from "../../../slack/actions/send-group-message/send-group-message.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...component, - props: utils.buildAppProps({ - component, - omitProps: [ - "as_user", - ], - }), - key: "slack_bot-send-group-message", - description: "Send a direct message to a group of users (Bot). See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here", - version: "0.0.2", -}; diff --git a/components/slack_bot/actions/send-large-message/send-large-message.mjs b/components/slack_bot/actions/send-large-message/send-large-message.mjs index f733d46ca629d..4465741376ce6 100644 --- a/components/slack_bot/actions/send-large-message/send-large-message.mjs +++ b/components/slack_bot/actions/send-large-message/send-large-message.mjs @@ -14,5 +14,5 @@ export default { }), key: "slack_bot-send-large-message", description: "Send a large message (more than 3000 characters) to a channel, group or user (Bot). See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here", - version: "0.0.2", + version: "0.0.5", }; diff --git a/components/slack_bot/actions/send-message-advanced/send-message-advanced.mjs b/components/slack_bot/actions/send-message-advanced/send-message-advanced.mjs new file mode 100644 index 0000000000000..b58e3cbc9ea85 --- /dev/null +++ b/components/slack_bot/actions/send-message-advanced/send-message-advanced.mjs @@ -0,0 +1,18 @@ +import component from "../../../slack/actions/send-message-advanced/send-message-advanced.mjs"; +import utils from "../../common/utils.mjs"; + +/* eslint-disable pipedream/required-properties-type */ +/* eslint-disable pipedream/required-properties-name */ + +export default { + ...component, + props: utils.buildAppProps({ + component, + omitProps: [ + "as_user", + ], + }), + key: "slack_bot-send-message-advanced", + description: "Customize advanced setttings and send a message to a channel, group or user (Bot). See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here", + version: "0.0.5", +}; diff --git a/components/slack_bot/actions/send-message-private-channel/send-message-private-channel.mjs b/components/slack_bot/actions/send-message-private-channel/send-message-private-channel.mjs deleted file mode 100644 index fe69f8ee8c8cd..0000000000000 --- a/components/slack_bot/actions/send-message-private-channel/send-message-private-channel.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import component from "../../../slack/actions/send-message-private-channel/send-message-private-channel.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...component, - props: utils.buildAppProps({ - component, - omitProps: [ - "as_user", - ], - }), - key: "slack_bot-send-message-private-channel", - description: "Send a message to a private channel and customize the name and avatar of the bot that posts the message (Bot). See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here", - version: "0.0.2", -}; diff --git a/components/slack_bot/actions/send-message-public-channel/send-message-public-channel.mjs b/components/slack_bot/actions/send-message-public-channel/send-message-public-channel.mjs deleted file mode 100644 index 3f8bd97b744d5..0000000000000 --- a/components/slack_bot/actions/send-message-public-channel/send-message-public-channel.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import component from "../../../slack/actions/send-message-public-channel/send-message-public-channel.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...component, - props: utils.buildAppProps({ - component, - omitProps: [ - "as_user", - ], - }), - key: "slack_bot-send-message-public-channel", - description: "Send a message to a public channel and customize the name and avatar of the bot that posts the message (Bot). See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here", - version: "0.0.2", -}; diff --git a/components/slack_bot/actions/send-message/send-message.mjs b/components/slack_bot/actions/send-message/send-message.mjs new file mode 100644 index 0000000000000..8002ecce52d61 --- /dev/null +++ b/components/slack_bot/actions/send-message/send-message.mjs @@ -0,0 +1,18 @@ +import component from "../../../slack/actions/send-message/send-message.mjs"; +import utils from "../../common/utils.mjs"; + +/* eslint-disable pipedream/required-properties-type */ +/* eslint-disable pipedream/required-properties-name */ + +export default { + ...component, + props: utils.buildAppProps({ + component, + omitProps: [ + "as_user", + ], + }), + key: "slack_bot-send-message", + description: "Send a message to a user, group, private channel or public channel (Bot). See [postMessage](https://api.slack.com/methods/chat.postMessage) or [scheduleMessage](https://api.slack.com/methods/chat.scheduleMessage) docs here", + version: "0.0.4", +}; diff --git a/components/slack_bot/actions/set-channel-description/set-channel-description.mjs b/components/slack_bot/actions/set-channel-description/set-channel-description.mjs new file mode 100644 index 0000000000000..ed8f440429ad0 --- /dev/null +++ b/components/slack_bot/actions/set-channel-description/set-channel-description.mjs @@ -0,0 +1,15 @@ +import component from "../../../slack/actions/set-channel-description/set-channel-description.mjs"; +import utils from "../../common/utils.mjs"; + +/* eslint-disable pipedream/required-properties-type */ +/* eslint-disable pipedream/required-properties-name */ + +export default { + ...component, + props: utils.buildAppProps({ + component, + }), + key: "slack_bot-set-channel-description", + description: "Change the description or purpose of a channel (Bot). [See the documentation](https://api.slack.com/methods/conversations.setPurpose)", + version: "0.0.2", +}; diff --git a/components/slack_bot/actions/set-channel-purpose/set-channel-purpose.mjs b/components/slack_bot/actions/set-channel-purpose/set-channel-purpose.mjs deleted file mode 100644 index 1da13ba49cee0..0000000000000 --- a/components/slack_bot/actions/set-channel-purpose/set-channel-purpose.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import component from "../../../slack/actions/set-channel-purpose/set-channel-purpose.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...component, - props: utils.buildAppProps({ - component, - }), - key: "slack_bot-set-channel-purpose", - description: "Change the purpose of a channel (Bot). [See docs here](https://api.slack.com/methods/conversations.setPurpose)", - version: "0.0.2", -}; diff --git a/components/slack_bot/actions/set-channel-topic/set-channel-topic.mjs b/components/slack_bot/actions/set-channel-topic/set-channel-topic.mjs index f5f15beac3496..b845b906580c8 100644 --- a/components/slack_bot/actions/set-channel-topic/set-channel-topic.mjs +++ b/components/slack_bot/actions/set-channel-topic/set-channel-topic.mjs @@ -10,6 +10,6 @@ export default { component, }), key: "slack_bot-set-channel-topic", - description: "Set the topic on a selected channel (Bot). [See docs here](https://api.slack.com/methods/conversations.setTopic)", - version: "0.0.2", + description: "Set the topic on a selected channel (Bot). [See the documentation](https://api.slack.com/methods/conversations.setTopic)", + version: "0.0.4", }; diff --git a/components/slack_bot/actions/unarchive-channel/unarchive-channel.mjs b/components/slack_bot/actions/unarchive-channel/unarchive-channel.mjs deleted file mode 100644 index 7e26e280b7d4e..0000000000000 --- a/components/slack_bot/actions/unarchive-channel/unarchive-channel.mjs +++ /dev/null @@ -1,38 +0,0 @@ -import component from "../../../slack/actions/unarchive-channel/unarchive-channel.mjs"; -import constants from "../../common/constants.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -// Bug reported at https://api.slack.com/methods/conversations.unarchive#markdown -// Bot tokens (xoxb-...) cannot currently be used to unarchive conversations. -// For now, please use a user token (xoxp-...) to unarchive -// the conversation rather than a bot token. - -export default { - ...component, - props: utils.buildAppProps({ - component, - addedProps: { - conversation: { - propDefinition: [ - undefined, - "channelId", - () => ({ - types: [ - constants.CHANNEL_TYPE.PUBLIC, - constants.CHANNEL_TYPE.PRIVATE, - constants.CHANNEL_TYPE.MPIM, - ], - excludeArchived: false, - channelsFilter: (channel) => channel.is_archived, - }), - ], - }, - }, - }), - key: "slack_bot-unarchive-channel", - description: "Unarchive a channel (Bot). [See docs here](https://api.slack.com/methods/conversations.unarchive)", - version: "0.0.2", -}; diff --git a/components/slack_bot/actions/update-group-members/update-group-members.mjs b/components/slack_bot/actions/update-group-members/update-group-members.mjs new file mode 100644 index 0000000000000..d9aedd2658964 --- /dev/null +++ b/components/slack_bot/actions/update-group-members/update-group-members.mjs @@ -0,0 +1,15 @@ +import component from "../../../slack/actions/update-group-members/update-group-members.mjs"; +import utils from "../../common/utils.mjs"; + +/* eslint-disable pipedream/required-properties-type */ +/* eslint-disable pipedream/required-properties-name */ + +export default { + ...component, + props: utils.buildAppProps({ + component, + }), + key: "slack_bot-update-group-members", + description: "Update the list of users for a User Group (Bot). [See docs here](https://api.slack.com/methods/usergroups.users.update)", + version: "0.0.2", +}; diff --git a/components/slack_bot/actions/update-message/update-message.mjs b/components/slack_bot/actions/update-message/update-message.mjs index ffc943fb011b8..376d9402ee5a7 100644 --- a/components/slack_bot/actions/update-message/update-message.mjs +++ b/components/slack_bot/actions/update-message/update-message.mjs @@ -13,6 +13,6 @@ export default { ], }), key: "slack_bot-update-message", - description: "Update a message (Bot). [See docs here](https://api.slack.com/methods/chat.update)", - version: "0.0.2", + description: "Update a message (Bot). [See the documentation](https://api.slack.com/methods/chat.update)", + version: "0.0.4", }; diff --git a/components/slack_bot/actions/update-user-groups-users/update-user-groups-users.mjs b/components/slack_bot/actions/update-user-groups-users/update-user-groups-users.mjs deleted file mode 100644 index e638819c2f7f4..0000000000000 --- a/components/slack_bot/actions/update-user-groups-users/update-user-groups-users.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import component from "../../../slack/actions/update-user-groups-users/update-user-groups-users.mjs"; -import utils from "../../common/utils.mjs"; - -/* eslint-disable pipedream/required-properties-type */ -/* eslint-disable pipedream/required-properties-name */ - -export default { - ...component, - props: utils.buildAppProps({ - component, - }), - key: "slack_bot-update-user-groups-users", - description: "Update the list of users for a User Group (Bot). [See docs here](https://api.slack.com/methods/usergroups.users.update)", - version: "0.0.2", -}; diff --git a/components/slack_bot/actions/upload-file/upload-file.mjs b/components/slack_bot/actions/upload-file/upload-file.mjs index e12a229878e68..940b8f29e47e8 100644 --- a/components/slack_bot/actions/upload-file/upload-file.mjs +++ b/components/slack_bot/actions/upload-file/upload-file.mjs @@ -10,6 +10,6 @@ export default { component, }), key: "slack_bot-upload-file", - description: "Upload a file (Bot). [See docs here](https://api.slack.com/methods/files.upload)", - version: "0.0.2", + description: "Upload a file (Bot). [See the documentation](https://api.slack.com/methods/files.upload)", + version: "0.0.5", }; diff --git a/components/slack_bot/common/utils.mjs b/components/slack_bot/common/utils.mjs index 8fd239056e3a8..abcb6846f8830 100644 --- a/components/slack_bot/common/utils.mjs +++ b/components/slack_bot/common/utils.mjs @@ -1,5 +1,7 @@ import app from "../slack_bot.app.mjs"; +const CONVERSATION_PERMISSION_MESSAGE = "In order to list a Slack channel, **your bot must be a member of that channel**, and in order to list private channels, your bot must have the `groups:read` scope in the `OAuth & Permissions` settings in Slack for this bot."; + async function streamIterator(stream) { let resources = []; for await (const resource of stream) { @@ -23,6 +25,12 @@ function buildPropDefinitions({ }; } + if (key === "conversation") { + prop.description = prop.description + ? `${prop.description} ${CONVERSATION_PERMISSION_MESSAGE}` + : `Select a public or private channel, or a user or group. ${CONVERSATION_PERMISSION_MESSAGE}`; + } + const [ , ...propDefinitionItems ] = prop.propDefinition; @@ -87,4 +95,5 @@ function buildAppProps({ export default { streamIterator, buildAppProps, + CONVERSATION_PERMISSION_MESSAGE, }; diff --git a/components/slack_bot/package.json b/components/slack_bot/package.json index 7e99c0bd5254f..3d69251679617 100644 --- a/components/slack_bot/package.json +++ b/components/slack_bot/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/slack_bot", - "version": "0.4.14", + "version": "0.5.5", "description": "Pipedream Slack_bot Components", "main": "slack_bot.app.mjs", "keywords": [ @@ -14,7 +14,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.5.1", + "@pipedream/platform": "^3.0.0", "@slack/web-api": "^5.15.0" } } diff --git a/components/slack_bot/sources/new-direct-message/new-direct-message.mjs b/components/slack_bot/sources/new-direct-message/new-direct-message.mjs index b567dd36a3590..866dacb30d375 100644 --- a/components/slack_bot/sources/new-direct-message/new-direct-message.mjs +++ b/components/slack_bot/sources/new-direct-message/new-direct-message.mjs @@ -1,11 +1,12 @@ import common from "../common/base.mjs"; import constants from "../../common/constants.mjs"; +import utils from "../../common/utils.mjs"; export default { ...common, key: "slack_bot-new-direct-message", name: "New Direct Message", - version: "0.0.2", + version: "0.0.4", description: "Emit new event when a message is posted in a direct message channel (Bot). To open a conversation, use the Pipedream Action \"Send a Direct Message\" to send a message from the Bot, or enable direct messages to the Bot in your App Settings (Settings->App Home->Show Tabs->Messages Tab), and send a direct message to the Bot.", type: "source", dedupe: "unique", @@ -22,7 +23,7 @@ export default { }), ], label: "User Channel", - description: "Events will only be emitted for direct messages between this user and the Bot.", + description: `Events will only be emitted for direct messages between this user and the Bot. ${utils.CONVERSATION_PERMISSION_MESSAGE}`, }, }, methods: { diff --git a/components/slack_bot/sources/new-message-in-channel/new-message-in-channel.mjs b/components/slack_bot/sources/new-message-in-channel/new-message-in-channel.mjs index e5c4b99b94a9d..c08ae2aa5c953 100644 --- a/components/slack_bot/sources/new-message-in-channel/new-message-in-channel.mjs +++ b/components/slack_bot/sources/new-message-in-channel/new-message-in-channel.mjs @@ -1,11 +1,12 @@ import common from "../common/base.mjs"; import constants from "../../common/constants.mjs"; +import utils from "../../common/utils.mjs"; export default { ...common, key: "slack_bot-new-message-in-channel", name: "New Message In Channel", - version: "0.0.2", + version: "0.0.4", description: "Emit new event when a new message is posted to a public, private or group channel (Bot)", type: "source", dedupe: "unique", @@ -23,6 +24,7 @@ export default { ], }), ], + description: `Select the channel's ID. ${utils.CONVERSATION_PERMISSION_MESSAGE}`, }, }, methods: { diff --git a/components/slicktext/README.md b/components/slicktext/README.md index 6d7c417ad5146..f32925cd1898b 100644 --- a/components/slicktext/README.md +++ b/components/slicktext/README.md @@ -1,23 +1,11 @@ # Overview -SlickText API can be used to create text message marketing campaigns to build -and maintain a healthy customer base. With its powerful API, you can integrate -text messaging functionality into your own web applications and even create -your own text message marketing program. +SlickText provides a platform to harness the power of text messaging for marketing and communication purposes. With its API, you can automate the sending of SMS messages, manage contacts, and track message performance directly through Pipedream. Pipedream's serverless execution model and vast connectivity with other apps make it an ideal environment to build complex messaging workflows, enriching communications with data from CRMs, customer support platforms, and marketing tools. -Here are some examples of what you can do with the SlickText API: +# Example Use Cases -- Create a custom sign-up form to collect customer contact information and - automatically add customers to your text messaging list. -- Send transactional text messages to your customers, such as order - confirmations or appointment reminders. -- Send targeted, personalized text message campaigns to your customers. -- Automate segmentation of your list to ensure your text messages reach the - right audience. -- Create mobile-friendly surveys and gather feedback from your customers. -- Track and analyze text message performance metrics to optimize future - campaigns. -- Create automated text message reminders for customers about upcoming events - or promotions. -- Create an autoresponder messaging system to quickly answer customer - inquiries. +- **SMS Marketing Automation**: Trigger a SlickText SMS campaign when a new subscriber is added to a Mailchimp mailing list. This can help bridge the gap between email and SMS marketing, ensuring that your audience receives your message through their preferred channel. + +- **Event-Triggered Text Alerts**: Send text messages via SlickText when specific events occur in your web application. For instance, set up a workflow that sends a discount code via SMS when a customer signs up on your website, leveraging apps like Stripe for payment events, or Shopify for e-commerce interactions. + +- **Customer Feedback Collection**: Collect customer feedback by sending automated surveys after a purchase. When a new order is detected in Square, trigger a SlickText message with a survey link. The responses can be collected and aggregated in Google Sheets for analysis, allowing for real-time customer sentiment analysis. diff --git a/components/slite/README.md b/components/slite/README.md new file mode 100644 index 0000000000000..2b484272766aa --- /dev/null +++ b/components/slite/README.md @@ -0,0 +1,11 @@ +# Overview + +Slite's API enables you to automate interactions with the Slite note-taking and collaboration platform within Pipedream. By tapping into the Slite API, you can create, update, and search notes and channels, or automate content synchronization and alerts. Integrating Slite with Pipedream can streamline team communication, content management, and ensure key information is shared and acted upon promptly. + +# Example Use Cases + +- **Sync Slite Notes to Google Drive**: Automatically export new or updated Slite notes to a specified Google Drive folder. A workflow can monitor changes in Slite and create or update corresponding documents in Google Drive, ensuring your document backups are always up-to-date. + +- **Aggregate Feedback to Slite**: Compile feedback from different sources like Typeform or Google Forms. When a new form submission is detected, a Pipedream workflow can create a note in Slite, or append to an existing one, to consolidate all feedback in one place for easy review and action. + +- **Slite Collaboration Alerts**: Keep your team informed by setting up a Pipedream workflow where any updates to specific Slite channels trigger notifications in Slack. This ensures that the team stays on top of the latest edits and additions without having to constantly check Slite for updates. diff --git a/components/slottable/README.md b/components/slottable/README.md new file mode 100644 index 0000000000000..97fdbd8703ccd --- /dev/null +++ b/components/slottable/README.md @@ -0,0 +1,11 @@ +# Overview + +The Slottable API provides a way to manage scheduling by creating, updating, and deleting slots related to appointments, classes, or any event that requires booking. With Pipedream's serverless platform, you can automate workflows that trigger based on actions within Slottable or external events, and then interact with the Slottable API to manage your scheduling data. This might include syncing booking information with a calendar, sending reminders or follow-up emails, or aggregating data for analytics. + +# Example Use Cases + +- **Calendar Sync**: Automatically sync new or updated slots from Slottable to a Google Calendar, ensuring your schedule is always up-to-date across platforms. + +- **Appointment Reminder System**: Send an SMS reminder, via Twilio, to clients a day before their scheduled slot to reduce no-shows. + +- **Analytics Reporting**: Collect slot booking data over time and send it to a Google Sheets spreadsheet for analysis and reporting on booking trends. diff --git a/components/slottable/package.json b/components/slottable/package.json index 5b7020934afe3..6c3422dd43aaf 100644 --- a/components/slottable/package.json +++ b/components/slottable/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/slottable", - "version": "0.1.0", + "version": "0.1.1", "description": "Pipedream Slottable Components", "main": "slottable.app.mjs", "keywords": [ diff --git a/components/slottable/slottable.app.mjs b/components/slottable/slottable.app.mjs index 3fe7271c55880..cd0224187d621 100644 --- a/components/slottable/slottable.app.mjs +++ b/components/slottable/slottable.app.mjs @@ -1,5 +1,6 @@ import { axios } from "@pipedream/platform"; +// Please remove this comment after testing DJ-2872 export default { type: "app", app: "slottable", diff --git a/components/slottable/sources/booking-contact-updated/booking-contact-updated.mjs b/components/slottable/sources/booking-contact-updated/booking-contact-updated.mjs index c3a8b7cc35593..4a717479cf3d8 100644 --- a/components/slottable/sources/booking-contact-updated/booking-contact-updated.mjs +++ b/components/slottable/sources/booking-contact-updated/booking-contact-updated.mjs @@ -4,7 +4,7 @@ export default { ...common, key: "slottable-booking-contact-updated", name: "Booking Contact Updated", - version: "0.0.1", + version: "0.0.2", description: "Emit new event when a booking contact is changed (new, updated, or deleted) in Slottable.", type: "source", dedupe: "unique", diff --git a/components/slottable/sources/contact-updated/contact-updated.mjs b/components/slottable/sources/contact-updated/contact-updated.mjs index 865b84a1fa2e8..e9f8c4893eb0e 100644 --- a/components/slottable/sources/contact-updated/contact-updated.mjs +++ b/components/slottable/sources/contact-updated/contact-updated.mjs @@ -4,7 +4,7 @@ export default { ...common, key: "slottable-contact-updated", name: "Contact Updated", - version: "0.0.1", + version: "0.0.2", description: "Emit new event when a contact is changed (new, updated, or deleted) in Slottable.", type: "source", dedupe: "unique", diff --git a/components/slybroadcast/.gitignore b/components/slybroadcast/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/slybroadcast/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/slybroadcast/README.md b/components/slybroadcast/README.md new file mode 100644 index 0000000000000..a3c731205d90e --- /dev/null +++ b/components/slybroadcast/README.md @@ -0,0 +1,11 @@ +# Overview + +The Slybroadcast API lets you send voice messages directly to the voicemail boxes of your audience without ringing their phones. Integrating this API with Pipedream allows you to automate voice message campaigns, follow-ups, and notifications, by triggering voice messages based on events in other apps or through scheduled workflows. + +# Example Use Cases + +- **Automated Customer Follow-Up**: After a customer makes a purchase on your e-commerce platform, automatically send a thank you voice message to foster a personal connection and enhance customer satisfaction. + +- **Event-Triggered Alerts**: Set up a workflow where a voice message is sent through Slybroadcast when your monitoring system detects a critical issue, ensuring stakeholders are promptly informed with a personal touch even if they miss an email or text alert. + +- **Appointment Reminders**: Connect Slybroadcast to your calendar or booking system to send out voicemail reminders, reducing no-shows for appointments and meetings. diff --git a/components/slybroadcast/actions/start-campaign-audio-file/start-campaign-audio-file.mjs b/components/slybroadcast/actions/start-campaign-audio-file/start-campaign-audio-file.mjs new file mode 100644 index 0000000000000..54173d404b6ee --- /dev/null +++ b/components/slybroadcast/actions/start-campaign-audio-file/start-campaign-audio-file.mjs @@ -0,0 +1,42 @@ +import slybroadcast from "../../slybroadcast.app.mjs"; + +export default { + key: "slybroadcast-start-campaign-audio-file", + name: "Start Campaign with Audio File", + description: "Start a new voicemail campaign using an audio file uploaded to your slybroadcast account. [See the documentation](https://www.slybroadcast.com/documentation.php)", + version: "0.0.1", + type: "action", + props: { + slybroadcast, + audioFile: { + propDefinition: [ + slybroadcast, + "audioFile", + ], + }, + recipients: { + propDefinition: [ + slybroadcast, + "recipients", + ], + }, + date: { + propDefinition: [ + slybroadcast, + "date", + ], + }, + }, + async run({ $ }) { + const response = await this.slybroadcast.startCampaign({ + $, + data: { + c_record_audio: this.audioFile, + c_phone: this.recipients.join(), + c_date: this.date, + }, + }); + $.export("$summary", `Started campaign with audio file ID: ${this.audioFile}`); + return response; + }, +}; diff --git a/components/slybroadcast/actions/start-campaign-audio-url/start-campaign-audio-url.mjs b/components/slybroadcast/actions/start-campaign-audio-url/start-campaign-audio-url.mjs new file mode 100644 index 0000000000000..c7b9aec53d4ff --- /dev/null +++ b/components/slybroadcast/actions/start-campaign-audio-url/start-campaign-audio-url.mjs @@ -0,0 +1,52 @@ +import slybroadcast from "../../slybroadcast.app.mjs"; + +export default { + key: "slybroadcast-start-campaign-audio-url", + name: "Start Campaign With Audio URL", + description: "Launch a new voicemail campaign to an individual or a group using an audio file url. [See the documentation](https://www.slybroadcast.com/documentation.php)", + version: "0.0.1", + type: "action", + props: { + slybroadcast, + audioFileUrl: { + type: "string", + label: "Audio File URL", + description: "The URL of the audio file for the voicemail campaign", + }, + audioFileType: { + type: "string", + label: "Audio File Type", + description: "WAV, Mp3 or M4a", + options: [ + "WAV", + "Mp3", + "M4a", + ], + }, + recipients: { + propDefinition: [ + slybroadcast, + "recipients", + ], + }, + date: { + propDefinition: [ + slybroadcast, + "date", + ], + }, + }, + async run({ $ }) { + const response = await this.slybroadcast.startCampaign({ + $, + data: { + c_url: this.audioFileUrl, + c_audio: this.audioFileType, + c_phone: this.recipients.join(), + c_date: this.date, + }, + }); + $.export("$summary", "Campaign started successfully"); + return response; + }, +}; diff --git a/components/slybroadcast/app/slybroadcast.app.ts b/components/slybroadcast/app/slybroadcast.app.ts deleted file mode 100644 index ecb4a51753113..0000000000000 --- a/components/slybroadcast/app/slybroadcast.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "slybroadcast", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/slybroadcast/package.json b/components/slybroadcast/package.json index 2d11b0f8b761a..20c656178bce2 100644 --- a/components/slybroadcast/package.json +++ b/components/slybroadcast/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/slybroadcast", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream Slybroadcast Components", - "main": "dist/app/slybroadcast.app.mjs", + "main": "slybroadcast.app.mjs", "keywords": [ "pipedream", "slybroadcast" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/slybroadcast", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/slybroadcast/slybroadcast.app.mjs b/components/slybroadcast/slybroadcast.app.mjs new file mode 100644 index 0000000000000..b1cf736e582ce --- /dev/null +++ b/components/slybroadcast/slybroadcast.app.mjs @@ -0,0 +1,70 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "slybroadcast", + propDefinitions: { + audioFile: { + type: "string", + label: "Audio File", + description: "The audio file uploaded to your slybroadcast account", + async options() { + const files = await this.listAudioFiles(); + const filenames = [ + ...files.matchAll(/"\|"(.*?)"\|"/g), + ].map((match) => match[1]); + return filenames; + }, + }, + recipients: { + type: "string[]", + label: "Recipients", + description: "Array of recipients' phone numbers for the voicemail campaign", + }, + date: { + type: "string", + label: "Date", + description: "Date/Time of delivery in Eastern Time. YYYY-MM-DD HH:MM:SS *Must use 24-hour time format. Example: 5:00pm = 17:00:00", + default: "now", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://www.mobile-sphere.com/gateway/"; + }, + _makeRequest(opts = {}) { + const { + $ = this, method = "POST", path, data, ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + method, + url: `${this._baseUrl()}${path}`, + data: { + ...data, + c_uid: `${this.$auth.email}`, + c_password: `${this.$auth.password}`, + }, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }); + }, + listAudioFiles(opts = {}) { + return this._makeRequest({ + path: "/vmb.aflist.php", + data: { + c_method: "get_audio_list", + }, + ...opts, + }); + }, + startCampaign(opts = {}) { + return this._makeRequest({ + path: "/vmb.php", + ...opts, + }); + }, + }, +}; diff --git a/components/smaily/README.md b/components/smaily/README.md index 2c59c5f0a99b7..081487fc227c5 100644 --- a/components/smaily/README.md +++ b/components/smaily/README.md @@ -1,19 +1,11 @@ # Overview -The Smaily API is a powerful tool for developers to create engaging and -automated customer communication at scale. With its wide array of features, you -can create customized marketing campaigns, trigger real-time emails and -autoresponders, and monitor your metrics. Here are a few examples of what you -can build with Smaily API: +The Smaily API allows for email marketing automation, where you can manage contacts, templates, and send out campaigns. With Pipedream's power to integrate a multitude of apps, you can craft custom workflows that react to events from various services, and then use Smaily to send targeted communications, all in a serverless environment. This seamless integration can be a game-changer for marketers seeking to enhance their audience engagement through personalized and timely emails based on user behavior or data. -- Automate Welcome series for new customers with pre-defined triggers and - message contents. -- Create custom membership flows to incorporate different types of users into - your membership journeys. -- Set up dynamic transactional emails to update customers on their orders. -- Use powerful segmentation tools to target emails to specific groups of - people. -- Track email open and click statistics to determine the effectiveness of your - campaigns. -- Integrate your website with the API to trigger emails on various user - actions. +# Example Use Cases + +- **Automated Welcome Email Series**: Trigger a workflow in Pipedream when a new user signs up via a platform like Shopify. The workflow would add the user to a specific Smaily contact list and then send a series of welcome emails that guide them through the product features or offer initial purchase discounts. + +- **Survey Feedback Follow-Up**: After a customer completes a feedback survey in Typeform, use Pipedream to capture the event, analyze the feedback, and, based on their satisfaction score, trigger a personalized follow-up email via Smaily, either thanking them for positive feedback or addressing any concerns raised. + +- **Event-Driven Newsletters**: Integrate with a CMS like WordPress to monitor when new content is posted. Use Pipedream to fetch the latest articles and automatically populate a Smaily newsletter template, which is then sent to subscribers, keeping them updated with fresh content. diff --git a/components/small_improvements/.gitignore b/components/small_improvements/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/small_improvements/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/small_improvements/README.md b/components/small_improvements/README.md new file mode 100644 index 0000000000000..e73a9b7912bdb --- /dev/null +++ b/components/small_improvements/README.md @@ -0,0 +1,11 @@ +# Overview + +The Small Improvements API allows for the automation of feedback, performance reviews, and goals within your organization. It enables interaction with Small Improvements' features, such as requesting feedback, managing users, and reviewing objectives. With Pipedream, you can harness this API to create seamless workflows that trigger actions across various applications, reacting to events in real-time, processing data, and automating repetitive tasks without writing any code. + +# Example Use Cases + +- **Automate Feedback Collection Post-Project**: Trigger a workflow in Pipedream when a project in your project management tool (like Jira) is marked complete. Use the Small Improvements API to request feedback for team members involved in the project, streamlining the review process and ensuring timely responses. + +- **Sync Performance Reviews to HRIS**: After a performance review cycle is completed in Small Improvements, use Pipedream to send the review data to your Human Resources Information System (HRIS), such as BambooHR. This workflow ensures that performance data is centralized, aiding in broader HR processes and analytics. + +- **Goal Tracking with Calendar Integration**: When goals are updated or new goals are set in Small Improvements, Pipedream can automatically create or update events in a Google Calendar dedicated to tracking key objectives. This keeps everyone aligned and informed about progress and upcoming goal assessments. diff --git a/components/small_improvements/actions/create-meeting-notes/create-meeting-notes.mjs b/components/small_improvements/actions/create-meeting-notes/create-meeting-notes.mjs new file mode 100644 index 0000000000000..a45e2a7cebaa0 --- /dev/null +++ b/components/small_improvements/actions/create-meeting-notes/create-meeting-notes.mjs @@ -0,0 +1,62 @@ +import smallImprovements from "../../small_improvements.app.mjs"; + +export default { + key: "small_improvements-create-meeting-notes", + name: "Create Meeting Notes", + description: "Create meeting notes in Small Improvements. [See the documentation](https://storage.googleapis.com/si-rest-api-docs/dist/index.html#meeting-note-resource)", + version: "0.0.1", + type: "action", + props: { + smallImprovements, + participantId: { + propDefinition: [ + smallImprovements, + "participantId", + ], + }, + meetingId: { + propDefinition: [ + smallImprovements, + "meetingId", + ({ participantId }) => ({ + participantId, + }), + ], + }, + content: { + type: "string", + label: "Content", + description: "The content of the note.", + }, + visibility: { + type: "string", + label: "Visibility", + description: "The visibility of the note.", + options: [ + "PRIVATE", + "SHARED", + ], + optional: true, + }, + draft: { + type: "boolean", + label: "Draft", + description: "Whether the note is draft or not.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.smallImprovements.createMeetingNotes({ + $, + meetingId: this.meetingId, + data: { + content: this.content, + meetingId: this.meetingId, + visibility: this.visibility, + draft: this.draft, + }, + }); + $.export("$summary", `Successfully created a note with Id: ${response.id}`); + return response; + }, +}; diff --git a/components/small_improvements/actions/list-users/list-users.mjs b/components/small_improvements/actions/list-users/list-users.mjs new file mode 100644 index 0000000000000..01d52d07624a7 --- /dev/null +++ b/components/small_improvements/actions/list-users/list-users.mjs @@ -0,0 +1,42 @@ +import smallImprovements from "../../small_improvements.app.mjs"; + +export default { + key: "small_improvements-list-users", + name: "List All Users", + description: "List all users from Small Improvements. [See the documentation](https://storage.googleapis.com/si-rest-api-docs/dist/index.html)", + version: "0.0.1", + type: "action", + props: { + smallImprovements, + includeGuests: { + type: "boolean", + label: "Include Guests", + description: "whether the request will only bring fully registered users or not.", + optional: true, + }, + managerId: { + type: "string", + label: "Manager Id", + description: "The id of a manager to filter the results.", + optional: true, + }, + showLocked: { + type: "boolean", + label: "Show Locked", + description: "whether the request will bring blocked users or not", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.smallImprovements.listAllUsers({ + $, + params: { + includeGuests: this.includeGuests || false, + managerId: this.managerId, + showLocked: this.showLocked || false, + }, + }); + $.export("$summary", `Successfully listed ${response.length} users`); + return response; + }, +}; diff --git a/components/small_improvements/app/small_improvements.app.ts b/components/small_improvements/app/small_improvements.app.ts deleted file mode 100644 index 78f359f86baac..0000000000000 --- a/components/small_improvements/app/small_improvements.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "small_improvements", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/small_improvements/package.json b/components/small_improvements/package.json index 685a71249d49d..f7c31b8a687c1 100644 --- a/components/small_improvements/package.json +++ b/components/small_improvements/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/small_improvements", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Small Improvements Components", - "main": "dist/app/small_improvements.app.mjs", + "main": "small_improvements.app.mjs", "keywords": [ "pipedream", "small_improvements" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/small_improvements", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" } -} \ No newline at end of file +} diff --git a/components/small_improvements/small_improvements.app.mjs b/components/small_improvements/small_improvements.app.mjs new file mode 100644 index 0000000000000..5729347943f98 --- /dev/null +++ b/components/small_improvements/small_improvements.app.mjs @@ -0,0 +1,87 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "small_improvements", + propDefinitions: { + meetingId: { + type: "string", + label: "Meeting ID", + description: "The ID of the meeting.", + async options({ participantId }) { + const data = await this.listMeetings({ + params: { + participants: participantId, + }, + }); + + return data.map(({ + id: value, title, + }) => ({ + label: title || value, + value, + })); + }, + }, + participantId: { + type: "string", + label: "Participant ID", + description: "The ID of the participant in the meeting.", + async options() { + const data = await this.listAllUsers({ + params: { + includeGuests: true, + showLocked: true, + }, + }); + + return data.map(({ + id: value, name, email, + }) => ({ + label: `${name} (${email})`, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://app.small-improvements.com/api/v2"; + }, + _headers() { + return { + "Authorization": `Bearer ${this.$auth.api_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: this._headers(), + }); + }, + createMeetingNotes({ + meetingId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/meetings/${meetingId}/notes`, + ...opts, + }); + }, + listAllUsers(opts = {}) { + return this._makeRequest({ + path: "/users", + ...opts, + }); + }, + listMeetings(opts = {}) { + return this._makeRequest({ + path: "/meetings", + ...opts, + }); + }, + }, +}; diff --git a/components/smartengage/README.md b/components/smartengage/README.md new file mode 100644 index 0000000000000..2a38610d160f3 --- /dev/null +++ b/components/smartengage/README.md @@ -0,0 +1,11 @@ +# Overview + +The SmartEngage API enables automated interactions with customers across multiple channels like email, SMS, and push notifications. It focuses on unifying these channels to provide a cohesive messaging experience. Utilizing the API in Pipedream allows you to craft workflows that respond to customer behavior, synchronize data across platforms, and send targeted communications, all in real-time. With Pipedream's serverless platform, you can trigger these workflows based on events from other apps, process the data, and call the SmartEngage API without managing infrastructure. + +# Example Use Cases + +- **Syncing Subscriber Data**: Automatically update or create new subscribers in SmartEngage when a user signs up on your platform. This can be triggered by a new user event from a connected app like Shopify or WordPress in Pipedream. + +- **Multi-channel Follow-Ups**: Send a follow-up email or SMS through SmartEngage after a user abandons their cart. Start the workflow with a webhook from your e-commerce platform and set conditions based on user activity to choose the channel and content of the message. + +- **Behavior-Based Notifications**: Trigger personalized push notifications via SmartEngage based on user interactions within your app or website. Use analytics from tools like Google Analytics or Mixpanel in Pipedream to identify key user actions and tailor notifications accordingly. diff --git a/components/smartengage/package.json b/components/smartengage/package.json index a298244f8c9f6..c27099c20baea 100644 --- a/components/smartengage/package.json +++ b/components/smartengage/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/smartproxy/README.md b/components/smartproxy/README.md new file mode 100644 index 0000000000000..b78d572a89ebd --- /dev/null +++ b/components/smartproxy/README.md @@ -0,0 +1,11 @@ +# Overview + +Smartproxy is a tool that grants you access to a vast pool of residential IPs, allowing you to scrape data, automate tasks, and bypass geolocation restrictions without getting blocked. It's valuable for tasks that require mimicking real user behavior and accessing web data with minimal footprint. On Pipedream, you can leverage the Smartproxy API to create workflows that automate these tasks, integrate with other services, and handle the data as needed, all in a serverless environment. + +# Example Use Cases + +- **Scrape Product Data**: Automate the extraction of product information from e-commerce websites while rotating proxies to prevent IP bans. You can trigger the workflow on a schedule, process the data, and store it in a Google Sheet for analysis. + +- **Monitor Ad Campaigns**: Use Smartproxy to rotate your IP when checking ad campaigns for different geographical locations. This workflow can send the retrieved data to Slack to alert your marketing team in real time about the appearance and position of ads. + +- **Aggregate News Content**: Create a serverless pipeline that fetches news articles from various international sources using Smartproxy to bypass geo-restrictions. The content can then be sent to an AI service like OpenAI for summarization, sentiment analysis, or categorization, and the results pushed to a database or a CMS. diff --git a/components/smartproxy/package.json b/components/smartproxy/package.json index 36f05bb6879cd..06f2bef5bd9fb 100644 --- a/components/smartproxy/package.json +++ b/components/smartproxy/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/smartreach/README.md b/components/smartreach/README.md new file mode 100644 index 0000000000000..4b8f2b32bd9fb --- /dev/null +++ b/components/smartreach/README.md @@ -0,0 +1,11 @@ +# Overview + +The SmartReach API integrates with Pipedream to enable automated workflows for email outreach and sales engagement. Within Pipedream, users can harness SmartReach's capabilities to create, update, track, and manage email campaigns, prospects, and templates. This allows for sophisticated outreach strategies that react to events in real-time, such as incoming emails, link clicks, or replied-to messages, and connect these events with other apps to streamline sales and marketing efforts. + +# Example Use Cases + +- **Syncing New Prospects from CRM to SmartReach**: Automatically add new contacts from a CRM platform like Salesforce to a SmartReach campaign. When a new contact is created in Salesforce, Pipedream triggers a workflow that adds the contact to a specified SmartReach campaign, ensuring no prospect is missed. + +- **Automating Follow-Ups Based on Prospect Engagement**: Use Pipedream to monitor prospect engagement with emails sent through SmartReach. If a prospect clicks a link or opens an email multiple times, trigger a follow-up email or task creation in a connected tool like Trello or Asana to engage the hot lead promptly. + +- **Maintaining Email List Hygiene**: Keep your SmartReach email lists clean by automating the process of removing bounced emails or unsubscribes. Connect SmartReach to an email verification service like Hunter. When Hunter flags an email as invalid or an unsubscribe is detected, Pipedream can remove the contact from your SmartReach lists. diff --git a/components/smartrmail/README.md b/components/smartrmail/README.md new file mode 100644 index 0000000000000..c365880013894 --- /dev/null +++ b/components/smartrmail/README.md @@ -0,0 +1,11 @@ +# Overview + +The SmartrMail API lets you tap into your email marketing efforts by enabling you to manage subscribers, send and track email campaigns, and automate workflows. By leveraging this API on Pipedream, you can integrate email marketing with a vast array of apps and services, allowing for seamless and automated data exchange. Use cases include synchronizing subscriber lists, triggering personalized emails based on user actions, and analyzing campaign performance with external tools. + +# Example Use Cases + +- **Sync New E-Commerce Customers to SmartrMail**: Automatically add new customers from your e-commerce platform, like Shopify, to a SmartrMail subscriber list, ensuring your email marketing campaigns reach all recent purchasers. + +- **Trigger Emails Based on User Activity**: Send targeted emails through SmartrMail when users perform specific actions on your website or app, such as abandoning a cart or viewing a product, by integrating webhooks or using platforms like Segment. + +- **Analyze Campaign Performance in a Dashboard**: Use the SmartrMail API to pull campaign data into a BI tool like Google Data Studio. Automate the aggregation of email campaign metrics to visualize and analyze the effectiveness of your marketing strategies. diff --git a/components/smartroutes/README.md b/components/smartroutes/README.md new file mode 100644 index 0000000000000..5596f2cfbb313 --- /dev/null +++ b/components/smartroutes/README.md @@ -0,0 +1,11 @@ +# Overview + +The SmartRoutes API facilitates optimized routing for logistics and delivery services by calculating the best paths for multiple destinations. By integrating this API into Pipedream workflows, you can automate the process of generating efficient routes, tracking delivery status, and updating stakeholders. This streamlines operations for businesses that rely on prompt deliveries, such as e-commerce, food services, or courier companies. + +# Example Use Cases + +- **Dynamic Delivery Route Optimization**: Automate the creation of delivery routes by triggering a Pipedream workflow with new orders from an e-commerce platform like Shopify. Use the SmartRoutes API to calculate the most efficient paths and then distribute the routes to drivers through SMS or a mobile app. + +- **Real-Time Delivery Updates**: Configure a workflow that runs whenever a delivery status changes, using webhooks or a schedule to poll the SmartRoutes API. Send real-time notifications to customers via email or SMS with Twilio, keeping them informed about their delivery status. + +- **Scheduled Route Planning**: Create a daily or weekly workflow that compiles all pending deliveries from a database or Google Sheets, processes them through the SmartRoutes API to find the best routes, and then updates a management dashboard with the planned routes and estimated times of arrival. diff --git a/components/smartroutes/actions/create-order/create-order.mjs b/components/smartroutes/actions/create-order/create-order.mjs new file mode 100644 index 0000000000000..6175252c4a79a --- /dev/null +++ b/components/smartroutes/actions/create-order/create-order.mjs @@ -0,0 +1,261 @@ +import smartroutes from "../../smartroutes.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "smartroutes-create-order", + name: "Create Order", + description: "Creates a new order in the smartroutes. [See the documentation](https://api.smartroutes.io/v2/docs/api/#tag/Orders/paths/~1orders/post)", + version: "0.0.1", + type: "action", + props: { + smartroutes, + orderNumber: { + type: "string", + label: "Order Number", + description: "The order number", + }, + customerAccount: { + propDefinition: [ + smartroutes, + "customerAccount", + ], + }, + type: { + type: "string", + label: "Type", + description: "Type of the order (delivery, pickup, or shipment).", + options: [ + "delivery", + "pickup", + "shipment", + ], + }, + deliveryContactName: { + type: "string", + label: "Delivery Contact Name", + description: "Name of the contact person.", + optional: true, + }, + deliveryContactNumber: { + type: "string", + label: "Delivery Contact Number", + description: "Contact number of the person.", + optional: true, + }, + deliveryContactEmail: { + type: "string", + label: "Delivery Contact Email", + description: "Email of the contact person.", + optional: true, + }, + deliveryAddress: { + type: "string", + label: "Delivery Address", + description: "Delivery address.", + optional: true, + }, + deliveryPostcode: { + type: "string", + label: "Delivery Postcode", + description: "Postcode for delivery address.", + optional: true, + }, + deliveryLat: { + type: "string", + label: "Delivery Latitude", + description: "Latitude of the delivery location.", + optional: true, + }, + deliveryLng: { + type: "string", + label: "Delivery Longitude", + description: "Longitude of the delivery location.", + optional: true, + }, + deliveryDuration: { + type: "integer", + label: "Delivery Duration", + description: "Duration for order delivery in minutes.", + optional: true, + }, + deliveryNotes: { + type: "string", + label: "Delivery Notes", + description: "Notes for delivery instructions.", + optional: true, + }, + deliveryDate: { + type: "string", + label: "Delivery Date", + description: "Date for order delivery.", + optional: true, + }, + pickupAddress: { + type: "string", + label: "Pickup Address", + description: "Address for order pickup.", + optional: true, + }, + pickupPostcode: { + type: "string", + label: "Pickup Postcode", + description: "Postcode for pickup address.", + optional: true, + }, + pickupDuration: { + type: "integer", + label: "Pickup Duration", + description: "Duration for order pickup in minutes.", + optional: true, + }, + pickupLat: { + type: "string", + label: "Pickup Latitude", + description: "Latitude of the pickup location.", + optional: true, + }, + pickupLng: { + type: "string", + label: "Pickup Longitude", + description: "Longitude of the pickup location.", + optional: true, + }, + pickupNotes: { + type: "string", + label: "Pickup Notes", + description: "Notes for pickup instructions.", + optional: true, + }, + pickupContactName: { + type: "string", + label: "Pickup Contact Name", + description: "Name of the contact person for pickup.", + optional: true, + }, + pickupContactNumber: { + type: "string", + label: "Pickup Contact Number", + description: "Contact number of the person for pickup.", + optional: true, + }, + pickupContactEmail: { + type: "string", + label: "Pickup Contact Email", + description: "Email of the contact person for pickup.", + optional: true, + }, + parts: { + type: "integer", + label: "Parts", + description: "Number of parts in the order.", + optional: true, + }, + lineItems: { + type: "string[]", + label: "Line Items", + description: "Array of line items in the order. Each line item object must contain `product_code`, `product_name`, and `product_quantity`.", + optional: true, + }, + timeWindows: { + type: "string[]", + label: "Time Windows", + description: "Array of time windows for order delivery or pickup. Each time window object must contain `from` (Start time of the time window. Ex: \"08:00\") and `to` (End time of the time window. Ex: \"12:00\").", + optional: true, + }, + skills: { + type: "string[]", + label: "Skills", + description: "List of required skills for the order. Skills listed must exist within your Vehicle Settings.", + optional: true, + }, + customFields: { + propDefinition: [ + smartroutes, + "customFields", + ], + reloadProps: true, + }, + capacities: { + propDefinition: [ + smartroutes, + "capacities", + ], + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.customFields?.length) { + for (const field of this.customFields) { + props[`customField_${field}`] = { + type: "string", + label: `Value of ${field}`, + }; + } + } + if (this.capacities?.length) { + for (const capacity of this.capacities) { + props[`capacity_${capacity}`] = { + type: "integer", + label: `Capacity of ${capacity}`, + }; + } + } + return props; + }, + methods: { + buildCustomFieldsObj() { + return this.customFields.map((field) => ({ + name: field, + value: this[`customField_${field}`], + })); + }, + buildCapacitiesObj() { + return this.capacities.map((capacity) => ({ + type: capacity, + capacity: this[`capacity_${capacity}`], + })); + }, + }, + async run({ $ }) { + const { orders } = await this.smartroutes.createOrder({ + $, + data: [ + { + order_number: this.orderNumber, + customer: { + account: this.customerAccount, + }, + type: this.type, + delivery_contact_name: this.deliveryContactName, + delivery_contact_number: this.deliveryContactNumber, + delivery_contact_email: this.deliveryContactEmail, + delivery_address: this.deliveryAddress, + delivery_postcode: this.deliveryPostcode, + delivery_lat: this.deliveryLat, + delivery_lng: this.deliveryLng, + delivery_duration: this.deliveryDuration, + delivery_notes: this.deliveryNotes, + delivery_date: this.deliveryDate, + pickup_address: this.pickupAddress, + pickup_postcode: this.pickupPostcode, + pickup_duration: this.pickupDuration, + pickup_lat: this.pickupLat, + pickup_lng: this.pickupLng, + pickup_notes: this.pickupNotes, + pickup_contact_name: this.pickupContactName, + pickup_contact_number: this.pickupContactNumber, + pickup_contact_email: this.pickupContactEmail, + parts: this.parts, + line_items: utils.parseObjArray(this.lineItems), + time_windows: utils.parseObjArray(this.timeWindows), + skills: this.skills, + custom_fields: this.customFields?.length && this.buildCustomFieldsObj(), + capacities: this.capacities?.length && this.buildCapacitiesObj(), + }, + ], + }); + $.export("$summary", `Successfully created order with ID: ${orders[0].id}`); + return orders; + }, +}; diff --git a/components/smartroutes/common/utils.mjs b/components/smartroutes/common/utils.mjs new file mode 100644 index 0000000000000..abd7bff178fea --- /dev/null +++ b/components/smartroutes/common/utils.mjs @@ -0,0 +1,13 @@ +export default { + parseObjArray(arr) { + if (!arr) { + return undefined; + } + if (typeof arr === "string") { + return JSON.parse(arr); + } + return arr?.map((item) => typeof item === "string" + ? JSON.parse(item) + : item) || []; + }, +}; diff --git a/components/smartroutes/package.json b/components/smartroutes/package.json index e4495bb7f6a9e..d27f093cb173f 100644 --- a/components/smartroutes/package.json +++ b/components/smartroutes/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/smartroutes", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream SmartRoutes Components", "main": "smartroutes.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" } -} \ No newline at end of file +} diff --git a/components/smartroutes/smartroutes.app.mjs b/components/smartroutes/smartroutes.app.mjs index fe5e19dcd6310..2c4e91528ca93 100644 --- a/components/smartroutes/smartroutes.app.mjs +++ b/components/smartroutes/smartroutes.app.mjs @@ -1,11 +1,86 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "smartroutes", - propDefinitions: {}, + propDefinitions: { + customerAccount: { + type: "string", + label: "Customer Account Number", + description: "Account number of a customer", + async options() { + const { customers } = await this.listCustomers(); + return customers?.map(({ + account: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + customFields: { + type: "string[]", + label: "Custom Fields", + description: "Custom fields for the order", + optional: true, + async options() { + const { custom_fields: fields } = await this.listCustomFields(); + return fields?.map(({ name }) => name ) || []; + }, + }, + capacities: { + type: "string[]", + label: "Capacities", + description: "Capacities for the order", + optional: true, + async options() { + const { capacities } = await this.listCapacities(); + return capacities?.map(({ type }) => type ) || []; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.smartroutes.io/v2"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "x-access-key": `${this.$auth.api_key}`, + }, + }); + }, + listCustomers(opts = {}) { + return this._makeRequest({ + path: "/customers", + ...opts, + }); + }, + listCustomFields(opts = {}) { + return this._makeRequest({ + path: "/custom-fields", + ...opts, + }); + }, + listCapacities(opts = {}) { + return this._makeRequest({ + path: "/capacities", + ...opts, + }); + }, + createOrder(opts = {}) { + return this._makeRequest({ + method: "post", + path: "/orders", + ...opts, + }); }, }, }; diff --git a/components/smartsheet/README.md b/components/smartsheet/README.md index 292629c824f12..25c3b91277277 100644 --- a/components/smartsheet/README.md +++ b/components/smartsheet/README.md @@ -1,19 +1,11 @@ # Overview -The Smartsheet API gives developers access to Smartsheet platform features and -data, enabling you to build custom applications and integrations that extend -the functionality of Smartsheet. With the Smartsheet API, you can create -powerful integrations that are tailored to your specific business needs. +The Smartsheet API unlocks the power of managing and automating complex workflows, directly interacting with Smartsheet's features such as sheets, rows, columns, and attachments. You can create, read, update, and delete sheets, share them with others, and extract complex data reports. Leveraging the API on Pipedream allows for seamless integration with other services for enhanced productivity and data management. Whether you're orchestrating an approval process, syncing data across platforms, or automating project tracking, the Smartsheet API pairs with Pipedream's serverless platform to build powerful, scalable, and automated workflows. -Using the Smartsheet API, you can: +# Example Use Cases -- Create and manage sheets, rows, and columns -- Automate and manage sheet features such as formulas and conditional - formatting -- Develop custom sheet views, including sorting and filtering options -- Capture and track changes with versioning -- Read, write, and delete data -- Create reports, dashboards, and timelines -- Collaborate with comments and notifications -- Store and organize files in Google Drive, Box and Dropbox -- Run workflows and automate actions +- **Project Management Automation**: Automatically create tasks in Smartsheet from GitHub issues. Each time a new issue is opened in a specified GitHub repository, a Pipedream workflow triggers to create a corresponding task in Smartsheet, ensuring project managers have instant visibility of development queues. + +- **Sales Lead Tracking**: Sync new leads from a Google Form to a Smartsheet sales pipeline. When a prospective customer submits their information, a Pipedream workflow captures this data, populates a row in Smartsheet, and can even trigger an email notification to the sales team via Gmail. + +- **Inventory Management**: Connect Smartsheet to Shopify for real-time inventory updates. Anytime a product's stock changes on Shopify, a Pipedream workflow updates the relevant inventory sheet in Smartsheet, keeping supply chain managers in sync with current stock levels. diff --git a/components/smartsuite/.gitignore b/components/smartsuite/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/smartsuite/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/smartsuite/README.md b/components/smartsuite/README.md new file mode 100644 index 0000000000000..6525e1fa7121a --- /dev/null +++ b/components/smartsuite/README.md @@ -0,0 +1,11 @@ +# Overview + +The SmartSuite API offers a way to streamline work by automating tasks, managing data, and integrating with other services. Within Pipedream, you can leverage this API to create workflows that react to events in SmartSuite, manipulate data within SmartSuite, or synchronize data between SmartSuite and other apps. This could range from updating project statuses, to aggregating data for reports, to syncing contacts across platforms. + +# Example Use Cases + +- **Project Management Automation**: Automatically update a project's status in SmartSuite when a related GitHub issue is closed. This workflow would use the GitHub app to trigger an event in Pipedream when an issue is closed, then use the SmartSuite API to update the project status accordingly. + +- **Customer Feedback Collection**: Collect customer feedback from a Typeform survey and create a new item in a SmartSuite feedback tracker. Upon form submission, the workflow would be triggered, capturing the feedback and using the SmartSuite API to create a record, ensuring customer insights are centrally stored and actionable. + +- **Daily Sales Report Generation**: Generate a daily sales report by aggregating data from a SmartSuite sales tracker and emailing it through SendGrid. Set up a scheduled trigger in Pipedream to fetch the latest sales data from SmartSuite each day and compile it into a report, which is then sent out as an email using SendGrid's API. diff --git a/components/smartsuite/actions/create-record/create-record.mjs b/components/smartsuite/actions/create-record/create-record.mjs new file mode 100644 index 0000000000000..bd49af35ab414 --- /dev/null +++ b/components/smartsuite/actions/create-record/create-record.mjs @@ -0,0 +1,66 @@ +import smartsuite from "../../smartsuite.app.mjs"; + +export default { + key: "smartsuite-create-record", + name: "Create Record", + description: "Creates a new record. [See the documentation](https://developers.smartsuite.com/docs/solution-data/records/create-record)", + version: "0.0.1", + type: "action", + props: { + smartsuite, + tableId: { + propDefinition: [ + smartsuite, + "tableId", + ], + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (!this.tableId) { + return props; + } + const { structure: fields } = await this.smartsuite.listFields({ + tableId: this.tableId, + }); + for (const field of fields) { + if (!field.params.is_auto_generated + && !field.params.system + && field.field_type !== "linkedrecordfield" + && field.field_type !== "filefield" + && field.field_type !== "userfield" + ) { + props[field.slug] = { + type: "string", + label: field.label, + optional: !field.params.required, + options: field.params.choices + ? field.params.choices.map(({ + value, label, + }) => ({ + value, + label, + })) + : undefined, + }; + } + } + return props; + }, + async run({ $ }) { + const { + smartsuite, + tableId, + ...data + } = this; + + const response = await smartsuite.createRecord({ + $, + tableId, + data, + }); + $.export("$summary", `Successfully created record with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/smartsuite/actions/find-records/find-records.mjs b/components/smartsuite/actions/find-records/find-records.mjs new file mode 100644 index 0000000000000..ed69c6de3af56 --- /dev/null +++ b/components/smartsuite/actions/find-records/find-records.mjs @@ -0,0 +1,70 @@ +import smartsuite from "../../smartsuite.app.mjs"; + +export default { + key: "smartsuite-find-records", + name: "Find Records", + description: "Search for records based on matching field(s). [See the documentation](https://developers.smartsuite.com/docs/solution-data/records/list-records)", + version: "0.0.1", + type: "action", + props: { + smartsuite, + tableId: { + propDefinition: [ + smartsuite, + "tableId", + ], + }, + fieldIds: { + propDefinition: [ + smartsuite, + "fieldIds", + (c) => ({ + tableId: c.tableId, + }), + ], + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (!this.tableId || !this.fieldIds?.length) { + return props; + } + const { structure: fields } = await this.smartsuite.listFields({ + tableId: this.tableId, + }); + for (const fieldId of this.fieldIds) { + const field = fields.find(({ slug }) => slug === fieldId); + props[fieldId] = { + type: "string", + label: field.label, + }; + } + return props; + }, + async run({ $ }) { + const fields = this.fieldIds?.length + ? this.fieldIds.map((field) => ({ + comparison: "is", + field, + value: this[field], + })) + : undefined; + const { items } = await this.smartsuite.listRecords({ + $, + tableId: this.tableId, + data: { + filter: { + operator: "and", + fields, + }, + }, + }); + if (items?.length) { + $.export("$summary", `Successfully found ${items.length} record${items.length === 1 + ? "" + : "s"}`); + } + return items; + }, +}; diff --git a/components/smartsuite/app/smartsuite.app.ts b/components/smartsuite/app/smartsuite.app.ts deleted file mode 100644 index c025f3008e762..0000000000000 --- a/components/smartsuite/app/smartsuite.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "smartsuite", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/smartsuite/package.json b/components/smartsuite/package.json index 9808f0cd5001b..159f2af208cbf 100644 --- a/components/smartsuite/package.json +++ b/components/smartsuite/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/smartsuite", - "version": "0.0.2", + "version": "0.1.1", "description": "Pipedream SmartSuite Components", - "main": "dist/app/smartsuite.app.mjs", + "main": "smartsuite.app.mjs", "keywords": [ "pipedream", "smartsuite" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/smartsuite", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/smartsuite/smartsuite.app.mjs b/components/smartsuite/smartsuite.app.mjs new file mode 100644 index 0000000000000..c5b46ee0b6ebb --- /dev/null +++ b/components/smartsuite/smartsuite.app.mjs @@ -0,0 +1,145 @@ +import { axios } from "@pipedream/platform"; +const DEFAULT_LIMIT = 50; + +export default { + type: "app", + app: "smartsuite", + propDefinitions: { + tableId: { + type: "string", + label: "Table ID", + description: "The identifier of a table", + async options({ page }) { + const { results: tables } = await this.listTables({ + params: { + limit: DEFAULT_LIMIT, + offset: page * DEFAULT_LIMIT, + }, + }); + return tables?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + fieldIds: { + type: "string[]", + label: "Field IDs", + description: "The field ID(s) (\"slug\") to search by", + optional: true, + async options({ tableId }) { + const { structure: fields } = await this.listFields({ + tableId, + }); + return fields?.map(({ + slug: value, label, + }) => ({ + value, + label, + })) || []; + }, + }, + solutionId: { + type: "string", + label: "Solution ID", + description: "The identifier of a solution", + async options() { + const solutions = await this.listSolutions(); + return solutions?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://app.smartsuite.com/api/v1"; + }, + _baseWebhookUrl() { + return "https://webhooks.smartsuite.com/smartsuite.webhooks.engine.Webhooks"; + }, + _headers() { + return { + "Authorization": `Token ${this.$auth.api_token}`, + "ACCOUNT-ID": `${this.$auth.account_id}`, + "Content-Type": "application/json", + }; + }, + _makeRequest({ + $ = this, + url, + path, + ...opts + }) { + return axios($, { + url: url || `${this._baseUrl()}${path}`, + headers: this._headers(), + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + url: `${this._baseWebhookUrl()}/CreateWebhook`, + ...opts, + }); + }, + deleteWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + url: `${this._baseWebhookUrl()}/DeleteWebhook`, + ...opts, + }); + }, + listWebhookEvents(opts = {}) { + return this._makeRequest({ + method: "POST", + url: `${this._baseWebhookUrl()}/ListEvents`, + ...opts, + }); + }, + listTables(opts = {}) { + return this._makeRequest({ + path: "/applications", + ...opts, + }); + }, + listSolutions(opts = {}) { + return this._makeRequest({ + path: "/solutions", + ...opts, + }); + }, + listFields({ + tableId, ...opts + }) { + return this._makeRequest({ + path: `/applications/${tableId}`, + ...opts, + }); + }, + listRecords({ + tableId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/applications/${tableId}/records/list/`, + ...opts, + }); + }, + createRecord({ + tableId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/applications/${tableId}/records/`, + ...opts, + }); + }, + }, +}; diff --git a/components/smartsuite/sources/common/base.mjs b/components/smartsuite/sources/common/base.mjs new file mode 100644 index 0000000000000..0f64216840c67 --- /dev/null +++ b/components/smartsuite/sources/common/base.mjs @@ -0,0 +1,110 @@ +import smartsuite from "../../smartsuite.app.mjs"; + +export default { + props: { + smartsuite, + db: "$.service.db", + http: "$.interface.http", + timer: { + label: "Webhook renewal schedule", + description: "The SmartSuite API requires occasional renewal of webhooks. **This runs in the background, so you should not need to modify this schedule**.", + type: "$.interface.timer", + static: { + intervalSeconds: 24 * 60 * 60, // once per day + }, + }, + solutionId: { + propDefinition: [ + smartsuite, + "solutionId", + ], + }, + }, + hooks: { + async activate() { + const { webhook: { webhook_id: hookId } } = await this.smartsuite.createWebhook({ + data: { + webhook: { + filter: { + solution: {}, + }, + kinds: [ + this.getEventType(), + ], + locator: { + account_id: this.smartsuite.$auth.account_id, + solution_id: this.solutionId, + }, + notification_status: { + enabled: { + url: this.http.endpoint, + }, + }, + }, + }, + }); + this._setHookId(hookId); + }, + async deactivate() { + const hookId = this._getHookId(); + if (hookId) { + await this.smartsuite.deleteWebhook({ + data: { + webhook_id: hookId, + }, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + _getPageToken() { + return this.db.get("pageToken"); + }, + _setPageToken(pageToken) { + this.db.set("pageToken", pageToken); + }, + generateMeta(event) { + return { + id: event.event_id, + summary: this.getSummary(event), + ts: Date.parse(event.event_at), + }; + }, + getEventType() { + throw new Error("getEventType is not implemented"); + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + }, + async run() { + const hookId = this._getHookId(); + const pageToken = this._getPageToken(); + + const { + events, next_page_token: nextPageToken, + } = await this.smartsuite.listWebhookEvents({ + data: { + webhook_id: hookId, + page_token: pageToken, + }, + }); + + this._setPageToken(nextPageToken); + + if (!events?.length) { + return; + } + + events.forEach((event) => { + const meta = this.generateMeta(event); + this.$emit(event, meta); + }); + }, +}; diff --git a/components/smartsuite/sources/new-record-created-instant/new-record-created-instant.mjs b/components/smartsuite/sources/new-record-created-instant/new-record-created-instant.mjs new file mode 100644 index 0000000000000..41661125c0593 --- /dev/null +++ b/components/smartsuite/sources/new-record-created-instant/new-record-created-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "smartsuite-new-record-created-instant", + name: "New Record Created (Instant)", + description: "Emit new event when a new record is created", + version: "0.0.2", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return "RECORD_CREATED"; + }, + getSummary({ record_event_data: data }) { + return `New record created with ID: ${data.record_id}`; + }, + }, + sampleEmit, +}; diff --git a/components/smartsuite/sources/new-record-created-instant/test-event.mjs b/components/smartsuite/sources/new-record-created-instant/test-event.mjs new file mode 100644 index 0000000000000..759834836f3a7 --- /dev/null +++ b/components/smartsuite/sources/new-record-created-instant/test-event.mjs @@ -0,0 +1,79 @@ +export default { + "webhook_id": "2f7cc6e0-b709-4b04-964a-3706c89c78e3", + "locator": { + "account_id": "WORKSPACE_ID", + "solution_id": "63b87cad645b3949631b55bf", + }, + "event_id": "2f7cc6e0-b709-4b04-964a-3706c89c78e3.994", + "kind": "RECORD_CREATED", + "event_at": "2023-06-21T21:14:06.263Z", + "record_event_data": { + "record_id": "6493681eba09b400816eb754", + "locator": { + "account_id": "WORKSPACE_ID", + "solution_id": "63b87cad645b3949631b55bf", + "application_id": "63b87cad645b3949631b55c1", + }, + "data": { + "last_updated": { + "on": "2023-06-21T21:14:06.247000Z", + "by": "63a1f65723aaf6bcb564b1f1", + }, + "due_date": { + "is_overdue": false, + "status_updated_on": "2023-06-21T21:14:06.248000Z", + "from_date": { + "date": null, + "include_time": false, + }, + "to_date": { + "date": null, + "include_time": false, + }, + "status_is_completed": false, + }, + "description": { + "data": { + "type": "doc", + "content": [], + }, + "preview": "", + "html": "
\n \n
", + }, + "deleted_by": null, + "title": "test", + "priority": "", + "application_id": "63b87cad645b3949631b55c1", + "followed_by": [], + "application_slug": "ssb9kaxn", + "deleted_date": { + "date": null, + "include_time": false, + }, + "comments_count": 0, + "autonumber": 2, + "ranking": { + "default": "aagckvkhyq", + }, + "id": "6493681eba09b400816eb754", + "first_created": { + "on": "2023-06-21T21:14:06.247000Z", + "by": "63a1f65723aaf6bcb564b1f1", + }, + "assigned_to": [], + "status": { + "updated_on": "2023-06-21T21:14:06.248000Z", + "value": "backlog", + }, + }, + "previous": {}, + }, + "ctx": { + "change_id": "ba1ffa93-0c51-48ab-aff2-64d9128d6c35", + "change_size": 1, + "batch_id": "65832e55206fdfd247b84abd", + "batch_size": 1, + "source": "UNKNOWN", + "handler": "INTERACTIVE", + }, +}; diff --git a/components/smartsuite/sources/record-updated-instant/record-updated-instant.mjs b/components/smartsuite/sources/record-updated-instant/record-updated-instant.mjs new file mode 100644 index 0000000000000..332e121b9fbda --- /dev/null +++ b/components/smartsuite/sources/record-updated-instant/record-updated-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "smartsuite-record-updated-instant", + name: "Record Updated (Instant)", + description: "Emit new event when an existing record is updated", + version: "0.0.2", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return "RECORD_UPDATED"; + }, + getSummary({ record_event_data: data }) { + return `Record updated with ID: ${data.record_id}`; + }, + }, + sampleEmit, +}; diff --git a/components/smartsuite/sources/record-updated-instant/test-event.mjs b/components/smartsuite/sources/record-updated-instant/test-event.mjs new file mode 100644 index 0000000000000..35918dfb6e395 --- /dev/null +++ b/components/smartsuite/sources/record-updated-instant/test-event.mjs @@ -0,0 +1,79 @@ +export default { + "webhook_id": "2f7cc6e0-b709-4b04-964a-3706c89c78e3", + "locator": { + "account_id": "WORKSPACE_ID", + "solution_id": "63b87cad645b3949631b55bf", + }, + "event_id": "2f7cc6e0-b709-4b04-964a-3706c89c78e3.994", + "kind": "RECORD_UPDATED", + "event_at": "2023-06-21T21:14:06.263Z", + "record_event_data": { + "record_id": "6493681eba09b400816eb754", + "locator": { + "account_id": "WORKSPACE_ID", + "solution_id": "63b87cad645b3949631b55bf", + "application_id": "63b87cad645b3949631b55c1", + }, + "data": { + "last_updated": { + "on": "2023-06-21T21:14:06.247000Z", + "by": "63a1f65723aaf6bcb564b1f1", + }, + "due_date": { + "is_overdue": false, + "status_updated_on": "2023-06-21T21:14:06.248000Z", + "from_date": { + "date": null, + "include_time": false, + }, + "to_date": { + "date": null, + "include_time": false, + }, + "status_is_completed": false, + }, + "description": { + "data": { + "type": "doc", + "content": [], + }, + "preview": "", + "html": "
\n \n
", + }, + "deleted_by": null, + "title": "test", + "priority": "", + "application_id": "63b87cad645b3949631b55c1", + "followed_by": [], + "application_slug": "ssb9kaxn", + "deleted_date": { + "date": null, + "include_time": false, + }, + "comments_count": 0, + "autonumber": 2, + "ranking": { + "default": "aagckvkhyq", + }, + "id": "6493681eba09b400816eb754", + "first_created": { + "on": "2023-06-21T21:14:06.247000Z", + "by": "63a1f65723aaf6bcb564b1f1", + }, + "assigned_to": [], + "status": { + "updated_on": "2023-06-21T21:14:06.248000Z", + "value": "backlog", + }, + }, + "previous": {}, + }, + "ctx": { + "change_id": "ba1ffa93-0c51-48ab-aff2-64d9128d6c35", + "change_size": 1, + "batch_id": "65832e55206fdfd247b84abd", + "batch_size": 1, + "source": "UNKNOWN", + "handler": "INTERACTIVE", + }, +}; diff --git a/components/smartthings/README.md b/components/smartthings/README.md index 0080f606c8943..8db425db6a5f4 100644 --- a/components/smartthings/README.md +++ b/components/smartthings/README.md @@ -1,31 +1,11 @@ # Overview -The Samsung SmartThings API is an open platform that enables developers to -quickly and securely connect a wide range of devices, services, and -applications to the SmartThings smart home and connected environment. With the -API, you can integrate your SmartThings-compatible products with a range of -third-party services and applications for enhanced convenience, automation, and -control. +The Samsung SmartThings API allows you to interface with various IoT devices within the Samsung SmartThings ecosystem, enabling control and monitoring from a centralized platform. Leveraging this API on Pipedream, you can create automated workflows that respond to device events, control devices programmatically, and integrate IoT data with hundreds of other services for advanced home automation, data logging, and smart notifications. -Using the SmartThings API, you can create custom automations and routines, -helpful notifications, unique user experiences, and otherwise extend the -functionality of your SmartThings-enabled devices. You can also control your -devices with voice commands or control remotely over the internet, adding an -extra layer of convenience to your home or business. +# Example Use Cases -Examples of What You Can Build with the SmartThings API +- **Smart Home Security System**: Integrate SmartThings with a messaging app like Slack or Twilio on Pipedream. Create a workflow that sends an alert when SmartThings detects unexpected motion in your home or an open door/window after you've set your home status to "Away." This way, you're immediately notified of any potential security breaches. -- Automatically adjust the temperature of your thermostat when motion is - detected in a certain room -- Set up lights to turn off when you leave the room -- Display your garden’s temperature and weather conditions on a mobile app -- Set connected appliances to run at certain times and even synchronize them - with a calendar -- Control the lighting around your home from your smartphone -- Receive notifications when doors and windows are opened -- Create a security system by connecting detectors and cameras -- Automate the lock on your doors based on the location of your smartphone -- Track your energy usage and set energy-saving schedules for connected - appliances -- Receive personalized alerts about air quality based on changes in the - environment. +- **Energy Consumption Tracker**: Pair SmartThings with Google Sheets on Pipedream to monitor and log energy usage. Set up a workflow that records power consumption data from SmartThings-compatible plugs and switches to a spreadsheet. This can help you analyze trends over time, identify high-usage devices, and optimize energy usage to save on bills. + +- **Automated Lighting Scenarios**: Use the SmartThings API with a weather app like OpenWeatherMap on Pipedream. Design workflows that adjust your SmartThings-connected lights based on real-time weather conditions. For instance, as sunlight dims on cloudy days, your indoor lights could automatically brighten to maintain a consistent ambiance. diff --git a/components/smartthings/package.json b/components/smartthings/package.json new file mode 100644 index 0000000000000..08fae7203dbb7 --- /dev/null +++ b/components/smartthings/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/smartthings", + "version": "0.6.0", + "description": "Pipedream smartthings Components", + "main": "smartthings.app.mjs", + "keywords": [ + "pipedream", + "smartthings" + ], + "homepage": "https://pipedream.com/apps/smartthings", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/smarty/README.md b/components/smarty/README.md new file mode 100644 index 0000000000000..58b5d63eabb69 --- /dev/null +++ b/components/smarty/README.md @@ -0,0 +1,11 @@ +# Overview + +The Smarty API, previously known as SmartyStreets, provides powerful location data services, including address validation, geocoding, and autocomplete for addresses. Integrating Smarty with Pipedream allows you to automate processes that require address verification or geolocation data, enriching datasets, improving delivery accuracy, and enhancing user experiences through auto-complete suggestions. By leveraging Pipedream's serverless platform, you can create workflows that react to events, process data in real-time, and connect Smarty with hundreds of other services without managing infrastructure. + +# Example Use Cases + +- **Address Validation for E-commerce Checkout**: Automate the validation of shipping addresses during the checkout process of an e-commerce app. When a new order is placed, trigger a Pipedream workflow that uses the Smarty API to confirm that the address is valid, reducing shipping errors and improving customer satisfaction. + +- **Geocoding Addresses from CRM Entries**: When a new contact is added to a CRM like HubSpot, automatically geocode the address and store the coordinates using the Smarty API. This data can be used to visualize customer locations on a map or for route optimization for sales reps. + +- **Autocomplete Address in User Sign-up Forms**: Improve user experience by adding autocomplete to address fields in sign-up forms. Connect a Pipedream workflow to your form submissions, use the Smarty API to fetch autocomplete suggestions, and dynamically update the form field to help users quickly complete address information. diff --git a/components/smartymeet/README.md b/components/smartymeet/README.md new file mode 100644 index 0000000000000..a28b54cdb2327 --- /dev/null +++ b/components/smartymeet/README.md @@ -0,0 +1,11 @@ +# Overview + +SmartyMeet API facilitates the scheduling and management of meetings, integrating powerful features like automated scheduling, reminders, and calendar syncing. With this API, users can automate the entire lifecycle of a meeting from initiation to follow-up, directly interfacing with calendars, conferencing tools, and communication platforms to streamline operations. Using Pipedream, these capabilities can be extended to create dynamic, automated workflows that interact with other applications, handle data seamlessly, and improve collaboration efficiency. + +# Example Use Cases + +- **Automated Meeting Setup with Google Calendar**: Automatically schedule meetings in SmartyMeet and sync them with Google Calendar whenever a new form is submitted on your website. This can be set up to capture client information and preferences, scheduling meetings at optimal times without manual intervention. + +- **Meeting Reminder System via SMS with Twilio**: Set up a workflow where SmartyMeet sends meeting details to Twilio, which then sends an SMS reminder to all participants 24 hours before the scheduled meeting time. This ensures higher attendance rates and keeps all participants informed. + +- **Post-Meeting Feedback Collection via Email**: After a meeting concludes, trigger an automated email through SendGrid to collect feedback from all participants. This data can be stored in a Google Sheet for analysis, helping to improve future meeting effectiveness and participant satisfaction. diff --git a/components/smartymeet/package.json b/components/smartymeet/package.json new file mode 100644 index 0000000000000..5de65bb78405a --- /dev/null +++ b/components/smartymeet/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/smartymeet", + "version": "0.0.1", + "description": "Pipedream SmartyMeet Components", + "main": "smartymeet.app.mjs", + "keywords": [ + "pipedream", + "smartymeet" + ], + "homepage": "https://pipedream.com/apps/smartymeet", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/smartymeet/smartymeet.app.mjs b/components/smartymeet/smartymeet.app.mjs new file mode 100644 index 0000000000000..baf1b75926a50 --- /dev/null +++ b/components/smartymeet/smartymeet.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "smartymeet", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/smiirl/README.md b/components/smiirl/README.md index b008a3e964070..96b3bfc3a8026 100644 --- a/components/smiirl/README.md +++ b/components/smiirl/README.md @@ -1,24 +1,11 @@ # Overview -The Smiirl API is a powerful tool that allows developers to create custom -integrations to monitor and analyze their customers' activity. With the Smiirl -API, developers can build and integrate digital displays that communicate -real-time data from a variety of online sources. These sources include social -media, e-commerce stores, emails, mobile apps, and more. By combining dynamic -data with physical displays, developers can create interactive digital signage -experiences that engage and inform customers in exciting new ways. +The Smiirl API lets you interface with Smiirl counters to dynamically update and manage the display of real-time data. Leveraging Pipedream's robust integration platform, you can connect Smiirl to various data sources, triggering updates that reflect key metrics like social media followers, sales figures, or any other number that's essential to your business or personal goals. -Examples of What You Can Build Using the Smiirl API: +# Example Use Cases -- Digital counters that display live follower counts and other metrics from - social media platforms -- Live product and sales data displayed in physical stores -- Live customer feedback ratings and reviews -- Attendance monitors that track active users and long-time customers in a - space -- Reward and loyalty programs that use physical displays to recognize customer - achievements -- Live polls that crowd-sourcing opinions and opinions from people in a certain - location -- Connection meters that show how many people are connected to devices in a - location +- **Automatically Update Counter with E-Commerce Sales**: Set up an automation that listens for new orders on your e-commerce platform (like Shopify). Whenever a new sale is processed, a Pipedream workflow triggers an update to your Smiirl counter, instantly reflecting the updated sales numbers in your physical store or office. + +- **Display Real-Time Subscriber Growth**: Hook up Smiirl to your YouTube or Twitch account using Pipedream. Each time your subscriber count increases, the Smiirl counter is updated, offering a live view of your channel's growing popularity to visitors or employees. + +- **Showcase Environmental Metrics**: If you're monitoring environmental data, such as energy consumption or carbon footprint, link your monitoring tools to Smiirl via Pipedream. As the data updates, so does your Smiirl counter, making your commitment to sustainability visible and keeping the team informed and motivated. diff --git a/components/smoove/README.md b/components/smoove/README.md index d607623c15ff2..4270b381ff61e 100644 --- a/components/smoove/README.md +++ b/components/smoove/README.md @@ -1,18 +1,11 @@ # Overview -With the Smoove API, you can build applications that help you easily manage and -automate your customer service processes. Here are just a few examples of what -you can build: +The Smoove API, part of the Sendbird platform, offers a suite of communication capabilities such as messaging, chat, and video interactions. With Pipedream, you can leverage these features to automate interactions, sync communication data with other services, and create event-driven workflows. For example, you can trigger actions in other applications when new messages arrive, synchronize user profiles across platforms, or automate notifications for chat events. -- Intelligent customer service automation tools that help diagnose and resolve - customer issues -- Automatically segment customers and trigger personalized messages based on - their behavior -- Develop CRM friendly dashboards to keep track of customer interactions -- Integrate with Google or Amazon Alexa to allow customers to quickly get help -- Build tools to manage omnichannel customer conversations -- Automatically respond to customers on social media with preset messages -- Personalize customer onboarding experiences to ensure a smooth transition -- Automatically book meetings with customers based on their preferences -- Improve customer engagement with conversation-driven applications -- Automate customer care with triggers to help engage customers in real-time +# Example Use Cases + +- **Customer Support Ticketing Automation**: When a new message is received via Smoove, trigger a Pipedream workflow to create a support ticket in a tool like Zendesk. The workflow can categorize the ticket based on message content and assign it to the appropriate team, streamlining the support process. + +- **User Engagement Analysis**: Analyze chat data by connecting Smoove to a data analytics platform like Google BigQuery through Pipedream. Automatically export chat logs, run sentiment analysis, or generate engagement reports to gain insights into user interactions and improve service offerings. + +- **Real-time Notifications for CRM Updates**: Integrate Smoove with a CRM platform like Salesforce using Pipedream. Set up a workflow to send real-time notifications to sales or support teams when a user sends a message, updating the CRM record with the latest interaction details to help personalize follow-ups. diff --git a/components/sms_alert/README.md b/components/sms_alert/README.md new file mode 100644 index 0000000000000..e534f44817fde --- /dev/null +++ b/components/sms_alert/README.md @@ -0,0 +1,11 @@ +# Overview + +The SMS Alert API on Pipedream allows you to integrate SMS notifications into your workflows. It provides a straightforward method to send text messages, enabling instant communication for alerts, confirmations, or any time-sensitive info. By leveraging Pipedream's serverless platform, you can trigger SMS sends based on a variety of events and connect them to other services for a fully automated process. + +# Example Use Cases + +- **Customer Order Notifications**: Automate order confirmation texts by linking an e-commerce platform like Shopify to SMS Alert. When a new order is placed, trigger a workflow that sends a personalized SMS to the customer, confirming their purchase and providing a tracking number. + +- **Server Downtime Alerts**: Set up a monitor that checks your server's health at regular intervals using a cron job on Pipedream. If it detects downtime, it triggers the SMS Alert API to send a text to your IT team, so they can act fast to resolve the issue. + +- **Appointment Reminders**: Connect your appointment scheduling software, such as Calendly, to SMS Alert. Create a workflow that sends reminder texts to clients a day before their scheduled appointment, reducing no-shows and improving customer satisfaction. diff --git a/components/sms_everyone/README.md b/components/sms_everyone/README.md new file mode 100644 index 0000000000000..770a65c97ea36 --- /dev/null +++ b/components/sms_everyone/README.md @@ -0,0 +1,11 @@ +# Overview + +The SMS Everyone API lets you send SMS messages to users in Australia, offering a platform for automated text message communication. In Pipedream, you can harness this API to create workflows that trigger SMS notifications based on various events. Combine it with other apps and services to tailor unique automation solutions, such as sending alerts, reminders, or marketing messages directly from your Pipedream workflows. + +# Example Use Cases + +- **Customer Order Updates**: Automate order confirmation and dispatch notifications by linking the SMS Everyone API with an e-commerce platform like Shopify. When a new order is placed or shipped, trigger a Pipedream workflow that sends an SMS to the customer with their order details and tracking information. + +- **Appointment Reminders**: Pair the SMS Everyone API with Google Calendar. Set up a Pipedream workflow that checks your calendar for upcoming appointments and automatically sends an SMS reminder to the participants a day before the meeting. + +- **Server Downtime Alerts**: Integrate the SMS Everyone API with monitoring tools like Datadog or New Relic. Create a workflow in Pipedream that listens for server downtime or performance issues and sends an SMS alert to the IT support team for immediate action. diff --git a/components/sms_everyone/actions/send-sms/send-sms.mjs b/components/sms_everyone/actions/send-sms/send-sms.mjs new file mode 100644 index 0000000000000..d39ba15e1cb93 --- /dev/null +++ b/components/sms_everyone/actions/send-sms/send-sms.mjs @@ -0,0 +1,71 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import smsEveryone from "../../sms_everyone.app.mjs"; + +export default { + key: "sms_everyone-send-sms", + name: "Send SMS", + description: "Send an SMS message or a bulk SMS campaign. [See the documentation](https://www.smseveryone.com.au/restapi)", + version: "0.0.1", + type: "action", + props: { + smsEveryone, + originator: { + type: "string", + label: "Originator", + description: "Sender Number. Check the restrictions in your country as to whether you can use a word origin. If you send from a number, use international format without the + sign.", + }, + destinations: { + type: "string[]", + label: "Destinations", + description: "Phone number/s of the recipient/s. Ideally this should also be sent in international format without the + sign.", + optional: true, + }, + message: { + type: "string", + label: "Message", + description: "The message content.", + }, + timescheduled: { + type: "string", + label: "Time Scheduled", + description: "If you're scheduling a message, include this field. Format: \"YYYYMMDDHHMM\" **If you're sending now, this field is not necessary**. If yuo set the date/time in the past, the SMS will be sent immediately.", + optional: true, + }, + reference: { + type: "string", + label: "Reference", + description: "Your unique reference ID of the message.", + optional: true, + }, + crmids: { + propDefinition: [ + smsEveryone, + "crmids", + ], + optional: true, + }, + }, + async run({ $ }) { + if (!this.destinations && !this.crmids) { + throw new ConfigurationError("You must provide at least Destinations or CRM IDs."); + } + const response = await this.smsEveryone.sendSMS({ + $, + data: { + Originator: this.originator, + Destinations: parseObject(this.destinations), + Message: this.message, + TimeScheduled: this.timescheduled, + Reference: this.reference, + Action: "create", + CrmIds: parseObject(this.crmids), + }, + }); + + if (response.Code) throw new ConfigurationError(response.Message); + + $.export("$summary", `Successfully sent sms with CampaignId: ${response.CampaignId}`); + return response; + }, +}; diff --git a/components/sms_everyone/common/utils.mjs b/components/sms_everyone/common/utils.mjs new file mode 100644 index 0000000000000..f77b6ac759c25 --- /dev/null +++ b/components/sms_everyone/common/utils.mjs @@ -0,0 +1,40 @@ +import { ConfigurationError } from "@pipedream/platform"; + +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; + +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; + +export const checkPhoneNumbers = (phoneNumbers) => { + phoneNumbers.map((phoneNumber) => { + if (!phoneNumber.startsWith("+")) + throw new ConfigurationError("The phone numbers must start with '+'"); + }); +}; diff --git a/components/sms_everyone/package.json b/components/sms_everyone/package.json index 80467f53cde08..5da930426e7df 100644 --- a/components/sms_everyone/package.json +++ b/components/sms_everyone/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/sms_everyone", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream SMS Everyone Components", "main": "sms_everyone.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" } -} \ No newline at end of file +} diff --git a/components/sms_everyone/sms_everyone.app.mjs b/components/sms_everyone/sms_everyone.app.mjs index 21c80a00a7ffd..e9b826c8a0ea1 100644 --- a/components/sms_everyone/sms_everyone.app.mjs +++ b/components/sms_everyone/sms_everyone.app.mjs @@ -1,11 +1,61 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "sms_everyone", - propDefinitions: {}, + propDefinitions: { + crmids: { + type: "string[]", + label: "CRM IDs", + description: "The list ID of a list of mobile numbers that you want to send to. First you must upload the list to SMS Everyone interface and retrieve the list ID from SMS Everyone lists page.", + async options() { + const { Groups } = await this.listLists({ + data: { + Action: "List", + }, + }); + + return Groups.map(({ + CrmId: value, Description: label, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _apiUrl() { + return "https://smseveryone.com/api"; + }, + _auth() { + return { + username: `${this.$auth.username}`, + password: `${this.$auth.password}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: `${this._apiUrl()}${path}`, + auth: this._auth(), + ...opts, + }); + }, + listLists(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/crm", + ...opts, + }); + }, + sendSMS(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/campaign", + ...opts, + }); }, }, }; diff --git a/components/sms_fusion/actions/get-balance/get-balance.mjs b/components/sms_fusion/actions/get-balance/get-balance.mjs new file mode 100644 index 0000000000000..05816ef981327 --- /dev/null +++ b/components/sms_fusion/actions/get-balance/get-balance.mjs @@ -0,0 +1,19 @@ +import smsFusion from "../../sms_fusion.app.mjs"; + +export default { + key: "sms_fusion-get-balance", + name: "Get Balance", + description: "Get current account balance including credit limits in SMS Fusion. [See the documentation](https://docs.smsfusion.com.au/)", + version: "0.0.1", + type: "action", + props: { + smsFusion, + }, + async run({ $ }) { + const response = await this.smsFusion.getBalance({ + $, + }); + $.export("$summary", "Successfully retrieved account balance"); + return response; + }, +}; diff --git a/components/sms_fusion/actions/perform-hlr-lookup/perform-hlr-lookup.mjs b/components/sms_fusion/actions/perform-hlr-lookup/perform-hlr-lookup.mjs new file mode 100644 index 0000000000000..f4c883028b21b --- /dev/null +++ b/components/sms_fusion/actions/perform-hlr-lookup/perform-hlr-lookup.mjs @@ -0,0 +1,40 @@ +import smsFusion from "../../sms_fusion.app.mjs"; + +export default { + key: "sms_fusion-perform-hlr-lookup", + name: "Perform HLR Lookup", + description: "Perform HLR on a number with SMS Fusion. [See the documentation](https://docs.smsfusion.com.au/)", + version: "0.0.1", + type: "action", + props: { + smsFusion, + phoneNumber: { + propDefinition: [ + smsFusion, + "phoneNumber", + ], + description: "The phone number to lookup in MSISDN format. Example: `61412345678`", + }, + countryCode: { + propDefinition: [ + smsFusion, + "countryCode", + ], + }, + }, + async run({ $ }) { + const response = await this.smsFusion.hlrLookup({ + $, + params: { + num: this.phoneNumber, + cc: this.countryCode, + }, + }); + + if (response.id) { + $.export("$summary", "Successfully performed HLR Lookup"); + } + + return response; + }, +}; diff --git a/components/sms_fusion/actions/send-sms/send-sms.mjs b/components/sms_fusion/actions/send-sms/send-sms.mjs new file mode 100644 index 0000000000000..97aea89bbbc9f --- /dev/null +++ b/components/sms_fusion/actions/send-sms/send-sms.mjs @@ -0,0 +1,43 @@ +import smsFusion from "../../sms_fusion.app.mjs"; + +export default { + key: "sms_fusion-send-sms", + name: "Send SMS", + description: "Send an SMS using SMS Fusion. [See the documentation](https://docs.smsfusion.com.au/)", + version: "0.0.1", + type: "action", + props: { + smsFusion, + message: { + type: "string", + label: "Message", + description: "The contents of the SMS you wish to send", + }, + phoneNumber: { + propDefinition: [ + smsFusion, + "phoneNumber", + ], + }, + countryCode: { + propDefinition: [ + smsFusion, + "countryCode", + ], + }, + }, + async run({ $ }) { + const response = await this.smsFusion.sendSMS({ + $, + params: { + msg: this.message, + num: this.phoneNumber, + cc: this.countryCode, + }, + }); + if (response.success) { + $.export("$summary", "Successfully sent SMS message"); + } + return response; + }, +}; diff --git a/components/sms_fusion/package.json b/components/sms_fusion/package.json new file mode 100644 index 0000000000000..0dd633ba0aadf --- /dev/null +++ b/components/sms_fusion/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/sms_fusion", + "version": "0.1.0", + "description": "Pipedream SMS Fusion Components", + "main": "sms_fusion.app.mjs", + "keywords": [ + "pipedream", + "sms_fusion" + ], + "homepage": "https://pipedream.com/apps/sms_fusion", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/sms_fusion/sms_fusion.app.mjs b/components/sms_fusion/sms_fusion.app.mjs new file mode 100644 index 0000000000000..f587d048a6948 --- /dev/null +++ b/components/sms_fusion/sms_fusion.app.mjs @@ -0,0 +1,60 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "sms_fusion", + propDefinitions: { + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number to send the message to in MSISDN format. Example: `61412345678`", + }, + countryCode: { + type: "string", + label: "Country Code", + description: "A 2 character country code ISO 3166-2 for formatting local numbers internationally", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "http://api.smsfusion.com.au"; + }, + _makeRequest({ + $ = this, + path, + params, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "Accept": "application/json", + }, + params: { + ...params, + key: `${this.$auth.api_key}`, + }, + ...opts, + }); + }, + getBalance(opts = {}) { + return this._makeRequest({ + path: "/balance/", + ...opts, + }); + }, + hlrLookup(opts = {}) { + return this._makeRequest({ + path: "/hlr/", + ...opts, + }); + }, + sendSMS(opts = {}) { + return this._makeRequest({ + path: "/sms/", + ...opts, + }); + }, + }, +}; diff --git a/components/sms_it/README.md b/components/sms_it/README.md new file mode 100644 index 0000000000000..5d82d2231ddbf --- /dev/null +++ b/components/sms_it/README.md @@ -0,0 +1,11 @@ +# Overview + +The SMS-IT API provides a way to send and receive SMS messages programmatically. Using Pipedream, you can create event-driven workflows that leverage this API for a variety of use cases. You can integrate SMS capabilities into your applications, automate notifications, confirmations, or alerts, and engage with your audience through text messages. In Pipedream, you can use pre-built actions or run custom Node.js code to connect with the SMS-IT API, making it a versatile tool for developers looking to add SMS functionality to their services. + +# Example Use Cases + +- **Automated Appointment Reminders**: Trigger a workflow in Pipedream when an appointment is scheduled in your calendar (like Google Calendar). Send an SMS reminder through SMS-IT to the customer one day before the appointment to reduce no-shows. + +- **E-commerce Order Updates**: Connect SMS-IT with an e-commerce platform (such as Shopify) in Pipedream. When an order status changes to 'Shipped', automatically send an SMS with tracking details to the customer, enhancing the post-purchase experience. + +- **System Outage Alerts**: Monitor your service's health using a tool like Uptime Robot. Create a Pipedream workflow that listens for downtime alerts and uses SMS-IT to send real-time outage notifications to a list of IT personnel for rapid response. diff --git a/components/sms_magic/README.md b/components/sms_magic/README.md index 267ee09357e9a..8d7960e5918f0 100644 --- a/components/sms_magic/README.md +++ b/components/sms_magic/README.md @@ -1,19 +1,11 @@ # Overview -The SMS Magic API allows you to quickly and securely send and receive text -messages from your system. With it, you can build powerful custom messaging -solutions for a variety of use cases. Here are just some of the ways you can -use the SMS Magic API to enhance your business's communications abilities: +The SMS Magic API on Pipedream opens a world of possiblities for automating communication workflows. With it, you can send personalized text messages, schedule campaigns, and track message delivery within your applications. It's especially powerful for sales and support teams who need to engage customers on a massive scale without sacrificing the personal touch. -- Increase communication efficiency with automated SMS reminders for customer - appointments, orders and returns. -- Streamline your customer support by providing 24/7 support availability via - SMS. -- Update customers with helpful product info or customer service follow-ups - with automated or customer directed SMS messaging. -- Send emergency notifications quickly and securely. -- Build automated two-way contact forms. -- Receive text messages and trigger automated actions to respond. +# Example Use Cases -By using the SMS Magic API, you can take your communication capabilities to the -next level, ensuring the customer experience remains seamless and efficient. +- **Automated Customer Follow-Ups**: Create a workflow that triggers after a customer completes a purchase on your e-commerce platform. Utilize SMS Magic to send a personalized thank you message, and schedule a follow-up message a few days later to ask for a review or offer further assistance. + +- **Event-Driven Promotions**: Connect SMS Magic to a calendar app like Google Calendar on Pipedream. When a specific event (e.g., Black Friday) is approaching, automatically send out discount offers or special announcements to your segmented customer list to drive sales. + +- **Support Ticket Alerts**: Integrate SMS Magic with a support ticketing system such as Zendesk. Set up a workflow that alerts customers via SMS when their support ticket status changes. This keeps customers informed and improves satisfaction with real-time updates. diff --git a/components/sms_partner/README.md b/components/sms_partner/README.md new file mode 100644 index 0000000000000..add9a96c76cad --- /dev/null +++ b/components/sms_partner/README.md @@ -0,0 +1,11 @@ +# Overview + +The SMS Partner API allows you to integrate SMS functionalities within your Pipedream workflows. This API gives you the power to send text messages, check delivery statuses, and manage contacts directly through programmatic means. By leveraging this API on Pipedream, you can automate notifications, streamline communication processes, and build complex messaging workflows that can interact with a plethora of other services available on Pipedream's platform. + +# Example Use Cases + +- **Automated Customer Support Notifications**: Create a workflow that listens for incoming support tickets via your preferred support platform (like Zendesk). When a new ticket is received, use the SMS Partner API to send a text message to the assigned support agent, ensuring they're promptly informed about the new issue. + +- **Order Confirmation and Delivery Updates**: Connect an e-commerce platform like Shopify to your workflow. Trigger the workflow when a new order is placed or when an order's shipping status changes. Utilize the SMS Partner API to send the customer real-time updates via SMS on their order confirmation and delivery status. + +- **Survey Distribution and Response Collection**: After a user interacts with your service, trigger a workflow that sends them a text message with a link to a feedback survey using the SMS Partner API. Collect responses with a tool like Google Forms and use Pipedream's processing capabilities to analyze the feedback data and act upon it, such as categorizing feedback or triggering follow-up actions. diff --git a/components/sms_partner/package.json b/components/sms_partner/package.json index f8cd57708eef9..0117c80dcb49f 100644 --- a/components/sms_partner/package.json +++ b/components/sms_partner/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/smsapi/README.md b/components/smsapi/README.md new file mode 100644 index 0000000000000..53476925cdf25 --- /dev/null +++ b/components/smsapi/README.md @@ -0,0 +1,11 @@ +# Overview + +The SMSAPI is a tool that lets you send text messages at scale. On Pipedream, you can leverage this API to create workflows that trigger on various events, like incoming emails, form submissions, or scheduled times, and send SMS messages as part of an automated process. This can help with alerts, marketing, or operational communications. Pipedream's capabilities for connecting to various APIs and services enable complex workflows that can be built without managing your own infrastructure. + +# Example Use Cases + +- **Order Confirmation Texts**: After a customer places an order on your ecommerce platform, trigger a workflow in Pipedream that listens for new orders and uses SMSAPI to send a confirmation text to the customer's phone number. + +- **Appointment Reminders**: Connect your calendar or booking system to Pipedream. Use a scheduled workflow to fetch upcoming appointments and send reminders via SMSAPI a day before the appointment is due. + +- **Critical Alert System**: Integrate your monitoring tools with Pipedream. Set up a workflow that listens for alerts, such as server downtime or high error rates, and automatically sends a high-priority text message through SMSAPI to your on-call engineers. diff --git a/components/smslink_nc/actions/create-contact/create-contact.mjs b/components/smslink_nc/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..221b6f02160a1 --- /dev/null +++ b/components/smslink_nc/actions/create-contact/create-contact.mjs @@ -0,0 +1,92 @@ +import app from "../../smslink_nc.app.mjs"; + +export default { + key: "smslink_nc-create-contact", + name: "Create Contact", + description: "Create a new contact. [See the documentation](https://api.smslink.nc/api/documentation#/Contact/556b84f384422939a9db51e60685798a).", + version: "0.0.1", + type: "action", + props: { + app, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number of the contact.", + }, + email: { + type: "string", + label: "Email", + description: "The email of the contact.", + optional: true, + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the contact.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the contact.", + optional: true, + }, + param1: { + type: "string", + label: "Param 1", + description: "Custom parameter 1.", + optional: true, + }, + param2: { + type: "string", + label: "Param 2", + description: "Custom parameter 2.", + optional: true, + }, + param3: { + type: "string", + label: "Param 3", + description: "Custom parameter 3.", + optional: true, + }, + }, + methods: { + createContact(args = {}) { + return this.app.post({ + path: "/contact", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createContact, + phoneNumber, + email, + firstName, + lastName, + param1, + param2, + param3, + } = this; + + const response = await createContact({ + $, + data: { + contacts: [ + { + phone_number: phoneNumber, + email, + first_name: firstName, + last_name: lastName, + param_1: param1, + param_2: param2, + param_3: param3, + }, + ], + }, + }); + $.export("$summary", "Successfully created a new contact."); + return response; + }, +}; diff --git a/components/smslink_nc/actions/delete-contact/delete-contact.mjs b/components/smslink_nc/actions/delete-contact/delete-contact.mjs new file mode 100644 index 0000000000000..585032d7a3cdb --- /dev/null +++ b/components/smslink_nc/actions/delete-contact/delete-contact.mjs @@ -0,0 +1,53 @@ +import app from "../../smslink_nc.app.mjs"; + +export default { + key: "smslink_nc-delete-contact", + name: "Delete Contact", + description: "Deletes a contact. [See the documentation](https://api.smslink.nc/api/documentation)", + version: "0.0.1", + type: "action", + props: { + app, + phoneNumber: { + label: "Contact Phone Number", + description: "The phone number of the contact to delete.", + propDefinition: [ + app, + "contactId", + () => ({ + mapper: ({ + phone_number: value, first_name: firstName, last_name: lastName, + }) => ({ + value, + label: `${firstName || ""} ${lastName || ""} (${value})`.trim(), + }), + }), + ], + }, + }, + methods: { + deleteContact(args = {}) { + return this.app.delete({ + path: "/contact", + ...args, + }); + }, + }, + async run({ $ }) { + const { + deleteContact, + phoneNumber, + } = this; + + const response = await deleteContact({ + $, + data: { + phone_numbers: [ + phoneNumber, + ], + }, + }); + $.export("$summary", "Successfully deleted contact."); + return response; + }, +}; diff --git a/components/smslink_nc/actions/delete-sms-campaign/delete-sms-campaign.mjs b/components/smslink_nc/actions/delete-sms-campaign/delete-sms-campaign.mjs new file mode 100644 index 0000000000000..be56e2a61d5cb --- /dev/null +++ b/components/smslink_nc/actions/delete-sms-campaign/delete-sms-campaign.mjs @@ -0,0 +1,47 @@ +import app from "../../smslink_nc.app.mjs"; + +export default { + key: "smslink_nc-delete-sms-campaign", + name: "Delete SMS Campaign", + description: "Delete an existing SMS campaign. [See the documentation](https://api.smslink.nc/api/documentation)", + version: "0.0.1", + type: "action", + props: { + app, + campaignId: { + propDefinition: [ + app, + "campaignId", + ], + }, + }, + methods: { + deleteCampaign({ + campaignId, ...args + } = {}) { + return this.app.delete({ + path: `/sms-campaign/${campaignId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + deleteCampaign, + campaignId, + } = this; + + await deleteCampaign({ + $, + campaignId, + params: { + by: "id", + }, + }); + + $.export("$summary", "Successfully deleted SMS campaign."); + return { + sucess: true, + }; + }, +}; diff --git a/components/smslink_nc/package.json b/components/smslink_nc/package.json new file mode 100644 index 0000000000000..cdf297634022f --- /dev/null +++ b/components/smslink_nc/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/smslink_nc", + "version": "0.1.0", + "description": "Pipedream SMSlink nc Components", + "main": "smslink_nc.app.mjs", + "keywords": [ + "pipedream", + "smslink_nc" + ], + "homepage": "https://pipedream.com/apps/smslink_nc", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" + } +} diff --git a/components/smslink_nc/smslink_nc.app.mjs b/components/smslink_nc/smslink_nc.app.mjs new file mode 100644 index 0000000000000..457c68a5a7604 --- /dev/null +++ b/components/smslink_nc/smslink_nc.app.mjs @@ -0,0 +1,83 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "smslink_nc", + propDefinitions: { + contactId: { + type: "string", + label: "Contact ID", + description: "The ID of the contact to delete or manipulate.", + async options({ + mapper = ({ + id: value, phone_number: phoneNumber, first_name: firstName, last_name: lastName, + }) => ({ + value, + label: `${firstName || ""} ${lastName || ""} (${phoneNumber})`.trim(), + }), + }) { + const { object: { data } } = await this.getContacts(); + return data.map(mapper); + }, + }, + campaignId: { + type: "string", + label: "Campaign ID", + description: "The ID of the SMS campaign to delete or manipulate.", + async options() { + const { object: { data } } = await this.getSMSCampaigns(); + return data.map(({ + id: value, title: label, + }) => ({ + value, + label, + })); + }, + }, + }, + methods: { + getUrl(path) { + return `https://api.smslink.nc/api${path}`; + }, + getHeaders(headers) { + return { + Accept: "application/json", + Authorization: `Bearer ${this.$auth.personal_access_token}`, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + getContacts(args = {}) { + return this._makeRequest({ + path: "/contact", + ...args, + }); + }, + getSMSCampaigns(args = {}) { + return this._makeRequest({ + path: "/sms-campaign", + ...args, + }); + }, + }, +}; diff --git a/components/smstools/actions/add-contact-opt-out/add-contact-opt-out.mjs b/components/smstools/actions/add-contact-opt-out/add-contact-opt-out.mjs new file mode 100644 index 0000000000000..a696fba6d52cd --- /dev/null +++ b/components/smstools/actions/add-contact-opt-out/add-contact-opt-out.mjs @@ -0,0 +1,34 @@ +import { ConfigurationError } from "@pipedream/platform"; +import smstools from "../../smstools.app.mjs"; + +export default { + key: "smstools-add-contact-opt-out", + name: "Add Contact to Opt-Out List", + description: "Adds a selected contact to the opt-out list, stopping further communications. [See the documentation](https://www.smstools.com/en/sms-gateway-api/add_optout)", + version: "0.0.1", + type: "action", + props: { + smstools, + contactNumber: { + propDefinition: [ + smstools, + "contactNumber", + ], + }, + }, + async run({ $ }) { + try { + const response = await this.smstools.addOptOut({ + $, + data: { + number: this.contactNumber, + }, + }); + + $.export("$summary", `Successfully added contact number ${this.contactNumber} to the opt-out list.`); + return response; + } catch (e) { + throw new ConfigurationError("The number is already opted-out or does not exist in the database."); + } + }, +}; diff --git a/components/smstools/actions/add-contact/add-contact.mjs b/components/smstools/actions/add-contact/add-contact.mjs new file mode 100644 index 0000000000000..082c219cf366a --- /dev/null +++ b/components/smstools/actions/add-contact/add-contact.mjs @@ -0,0 +1,114 @@ +import { ConfigurationError } from "@pipedream/platform"; +import smstools from "../../smstools.app.mjs"; + +export default { + key: "smstools-add-contact", + name: "Add Contact to Group", + description: "Adds a new contact to an existing contact list. [See the documentation](https://www.smstools.com/en/sms-gateway-api/add_contact)", + version: "0.0.1", + type: "action", + props: { + smstools, + phone: { + type: "string", + label: "Phone Number", + description: "The phone number of the contact.", + }, + groupid: { + propDefinition: [ + smstools, + "groupId", + ], + }, + firstName: { + type: "string", + label: "First Name", + description: "First name of the contact.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the contact.", + optional: true, + }, + birthday: { + type: "string", + label: "Birthday", + description: "Birthday of the contact. **Format: YYYY-MM-DD**.", + optional: true, + }, + extra1: { + type: "string", + label: "Extra 1", + description: "Extra field 1 for the contact.", + optional: true, + }, + extra2: { + type: "string", + label: "Extra 2", + description: "Extra field 2 for the contact.", + optional: true, + }, + extra3: { + type: "string", + label: "Extra 3", + description: "Extra field 3 for the contact.", + optional: true, + }, + extra4: { + type: "string", + label: "Extra 4", + description: "Extra field 4 for the contact.", + optional: true, + }, + extra5: { + type: "string", + label: "Extra 5", + description: "Extra field 5 for the contact.", + optional: true, + }, + extra6: { + type: "string", + label: "Extra 6", + description: "Extra field 6 for the contact.", + optional: true, + }, + extra7: { + type: "string", + label: "Extra 7", + description: "Extra field 7 for the contact.", + optional: true, + }, + extra8: { + type: "string", + label: "Extra 8", + description: "Extra field 8 for the contact.", + optional: true, + }, + unsubscribed: { + type: "boolean", + label: "Unsubscribed", + description: "Indicates if the contact is unsubscribed.", + optional: true, + }, + }, + async run({ $ }) { + try { + const { + smstools, + ...data + } = this; + + const response = await smstools.addContact({ + $, + data, + }); + + $.export("$summary", `Successfully added contact with ID: ${response.ID}`); + return response; + } catch (e) { + throw new ConfigurationError(e.response.data.errorMsg); + } + }, +}; diff --git a/components/smstools/actions/send-sms/send-sms.mjs b/components/smstools/actions/send-sms/send-sms.mjs new file mode 100644 index 0000000000000..aff1345e0b164 --- /dev/null +++ b/components/smstools/actions/send-sms/send-sms.mjs @@ -0,0 +1,72 @@ +import smstools from "../../smstools.app.mjs"; + +export default { + key: "smstools-send-sms", + name: "Send SMS or WhatsApp Message", + description: "Sends a SMS or WhatsApp message to a specified contact. [See the documentation](https://www.smstools.com/en/sms-gateway-api/send_message)", + version: "0.0.1", + type: "action", + props: { + smstools, + message: { + type: "string", + label: "Message", + description: "The message to be sent.", + }, + to: { + propDefinition: [ + smstools, + "contactNumber", + ], + type: "string[]", + description: "The contact(s) to send the message to.", + }, + sender: { + propDefinition: [ + smstools, + "sender", + ], + }, + date: { + type: "string", + label: "Scheduled Date", + description: "The date to send the message. **Format: yyyy-MM-dd HH:mm**. If not provided, the message will be sent as soon as possible.", + optional: true, + }, + reference: { + type: "string", + label: "Reference", + description: "Reference for the message.", + optional: true, + }, + test: { + type: "boolean", + label: "Test", + description: "Test mode for the message.", + optional: true, + }, + subId: { + propDefinition: [ + smstools, + "subId", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.smstools.sendMessage({ + $, + data: { + message: this.message, + to: this.to, + sender: this.sender, + date: this.date, + reference: this.reference, + test: this.test, + subId: this.subId, + }, + }); + $.export("$summary", `Message sent successfully with ID: ${response.messageid}`); + return response; + }, +}; diff --git a/components/smstools/package.json b/components/smstools/package.json new file mode 100644 index 0000000000000..a69571462df76 --- /dev/null +++ b/components/smstools/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/smstools", + "version": "0.1.0", + "description": "Pipedream SMSTools Components", + "main": "smstools.app.mjs", + "keywords": [ + "pipedream", + "smstools" + ], + "homepage": "https://pipedream.com/apps/smstools", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/smstools/smstools.app.mjs b/components/smstools/smstools.app.mjs new file mode 100644 index 0000000000000..5f2d0e4f35784 --- /dev/null +++ b/components/smstools/smstools.app.mjs @@ -0,0 +1,165 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "smstools", + propDefinitions: { + groupId: { + type: "string", + label: "Group ID", + description: "The group ID where the contact should be added.", + async options({ page }) { + const groups = await this.getGroups({ + params: { + page: page + 1, + }, + }); + return groups.map(({ + ID: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + contactNumber: { + type: "string", + label: "Contact Number", + description: "Select a contact number to add to the opt-out list.", + async options({ page }) { + const { contacts } = await this.getContactNumbers({ + params: { + page: page + 1, + }, + }); + return contacts.map(({ phone }) => phone); + }, + }, + sender: { + type: "string", + label: "Sender", + description: "The sender ID for the message.", + async options() { + const senders = await this.getSenderIds(); + return senders.map(({ + ID: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + subId: { + type: "string", + label: "Sub ID", + description: "Subaccount ID from which the message is sent.", + async options() { + const subaccounts = await this.getSubAccounts(); + return subaccounts.map(({ + ID: value, username: label, + }) => ({ + label, + value, + })); + }, + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.smsgatewayapi.com/v1"; + }, + _params(params = {}) { + return { + client_id: `${this.$auth.client_id}`, + client_secret: `${this.$auth.client_secret}`, + ...params, + }; + }, + _makeRequest({ + $ = this, path, params, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + params: this._params(params), + ...opts, + }); + }, + getInboxMessages(opts = {}) { + return this._makeRequest({ + path: "/message/inbox", + ...opts, + }); + }, + getContactNumbers(opts = {}) { + return this._makeRequest({ + path: "/contact", + ...opts, + }); + }, + getGroups(opts = {}) { + return this._makeRequest({ + path: "/groups", + ...opts, + }); + }, + getSenderIds(opts = {}) { + return this._makeRequest({ + path: "/senderids", + ...opts, + }); + }, + getSubAccounts(opts = {}) { + return this._makeRequest({ + path: "/subaccount", + ...opts, + }); + }, + addContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contact", + ...opts, + }); + }, + addOptOut(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/optouts", + ...opts, + }); + }, + sendMessage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/message/send", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const { messages } = await fn({ + params, + ...opts, + }); + for (const d of messages) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = messages.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/smstools/sources/new-inbound-message/new-inbound-message.mjs b/components/smstools/sources/new-inbound-message/new-inbound-message.mjs new file mode 100644 index 0000000000000..600a8616bdde3 --- /dev/null +++ b/components/smstools/sources/new-inbound-message/new-inbound-message.mjs @@ -0,0 +1,64 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import smstools from "../../smstools.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "smstools-new-inbound-message", + name: "New Inbound Message", + description: "Emit new event when a new inbound message is received.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + smstools, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + async emitEvent(maxResults = false) { + const lastId = this._getLastId(); + const response = this.smstools.paginate({ + fn: this.smstools.getInboxMessages, + maxResults, + }); + + let responseArray = []; + for await (const item of response) { + if (item.ID <= lastId) break; + responseArray.push(item); + } + + if (responseArray.length) { + this._setLastId(responseArray[0].ID); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.ID, + summary: `New inbound message from ${item.sender}`, + ts: Date.parse(item.date), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, + sampleEmit, +}; diff --git a/components/smstools/sources/new-inbound-message/test-event.mjs b/components/smstools/sources/new-inbound-message/test-event.mjs new file mode 100644 index 0000000000000..2fad07180fa26 --- /dev/null +++ b/components/smstools/sources/new-inbound-message/test-event.mjs @@ -0,0 +1,8 @@ +export default { + "ID": "{ID}", + "message": "Hello sms", + "sender": "{nr}", + "type": "sms", + "date": "2022-01-01 12:00:00", + "receiver": "{inbox_nr}" +} \ No newline at end of file diff --git a/components/smtp2go/README.md b/components/smtp2go/README.md index 754926212466f..69013fc596d2b 100644 --- a/components/smtp2go/README.md +++ b/components/smtp2go/README.md @@ -1,23 +1,11 @@ # Overview -At SMTP2GO, we provide powerful API that allows you to build customized -integrations, tailored to their own specific applications and platforms. +SMTP2GO is an email delivery service that offers a robust SMTP API for sending emails. With the SMTP2GO API, you can programmatically send emails, get detailed reports on email delivery, and manage your sender reputation. It's useful for transactional emails, where reliability and deliverability are paramount, such as sending receipts, notifications, or password resets. When paired with Pipedream, SMTP2GO becomes a powerhouse, enabling you to create intricate automated workflows combining email capabilities with hundreds of other apps. -The API offers a range of ways to interact with SMTP2GO, from simple HTTP -requests to full featured libraries for various programming languages. By -utilizing the API, you’ll be able to quickly and reliably send emails from your -applications – no matter the size or user base – with ease. +# Example Use Cases -The following are some examples of what you can build using our API: +- **Customer Order Confirmation Emails**: Trigger an email via SMTP2GO when a new order is placed in an ecommerce platform like Shopify. Include order details by pulling data from the Shopify order object, ensuring the customer receives timely and accurate confirmation. -- Automated Email Notifications - Send out daily or weekly summaries, welcome - messages, or subscription confirmation emails -- Email Automations - Set up triggered mails based on events, such as when a - new customer registers -- Email Sign-up Forms - Create sign-up forms for campaigns, or special offers -- Email Authentication Platforms - Power your own authentication platform with - SMTP2GO -- Bulk Emails - Send out large sums of emails quickly -- Analytics - Track your email delivery performance -- Integrations - Integrate with apps such as MailChimp, Zoho, and Salesforce -- Email Serverless Solutions - Offload your email-sending requirements with us +- **Alerts for Monitoring Systems**: Set up a workflow that sends an email alert through SMTP2GO when a monitoring system like Datadog detects an issue with your app's performance. Customize the alert content based on the severity and type of issue to keep your team informed. + +- **Automated Email Reports**: Create a workflow where SMTP2GO dispatches daily or weekly email reports to stakeholders. This can integrate with Google Sheets or SQL databases to compile data into the email body or attachments, giving recipients insightful analytics regularly. diff --git a/components/smugmug/README.md b/components/smugmug/README.md index d365c4308ef0a..ebbc3ef6f00dc 100644 --- a/components/smugmug/README.md +++ b/components/smugmug/README.md @@ -1,23 +1,11 @@ # Overview -The SmugMug API makes it possible to access and build upon the powerful SmugMug -platform in creative and custom ways. With the SmugMug API, you can build -applications that interact with users' photos, galleries and albums on the -SmugMug platform. Whether you wish to create a custom photo store, custom photo -workflow, or customize the way people interact with your SmugMug galleries, the -SmugMug API makes it all possible. Here are some examples of what you can -create with the SmugMug API: +The SmugMug API provides programmatic access to a user's SmugMug account, allowing them to manage photos, albums, and account settings. With Pipedream, you can automate tasks such as uploading new images, synchronizing photo galleries with other platforms, or triggering actions based on account activity. The API's capabilities paired with Pipedream's serverless platform enable you to craft custom workflows that react to events in SmugMug or orchestrate tasks across multiple apps. -- Create a custom photo store to display, sell, and deliver photos to - customers. -- Integrate photo albums into blogs and websites. -- Upload photos to SmugMug from your own apps. -- Automate photo workflow and editing tasks. -- Access image meta data and create custom photo searches. -- Retrieve and impose photo upload restrictions. -- Connect SmugMug galleries to custom themes. -- Automatically resize, crop, and watermark photos. -- Create your own custom photo galleries. -- Allow customers to download photos with custom watermarks. -- Build applications to allow mass uploads and downloads. -- Monitor and track photo download activity. +# Example Use Cases + +- **Automated Photo Backup**: Whenever a new photo is added to a SmugMug album, trigger a Pipedream workflow that backs up the image to a cloud storage service like Google Drive or Dropbox. This ensures that you have redundant copies of your precious memories or professional shots. + +- **Social Media Integration**: Create a workflow that posts your latest SmugMug photos to social media platforms automatically. For instance, when you upload a new photo to a designated "Public" album in SmugMug, Pipedream can share that photo to your Facebook Page or Twitter account, helping you maintain an active social media presence without manual effort. + +- **Website Gallery Sync**: If you manage a personal or business website, use Pipedream to synchronize SmugMug albums with your site's gallery. Set up a webhook in Pipedream that listens for new SmugMug events and updates your website's photo gallery in real-time, ensuring visitors always see the latest content without manual updating. diff --git a/components/snapchat_marketing/README.md b/components/snapchat_marketing/README.md index a547995f241a2..f3bbce43d597d 100644 --- a/components/snapchat_marketing/README.md +++ b/components/snapchat_marketing/README.md @@ -1,20 +1,11 @@ # Overview -The Snapchat Marketing API provides developers the opportunity to build -applications that engage the vast Snapchat audience. Whether you’re -Game-developer, Advertising Agency, Brand, or Influencer, the Snapchat API -enables you to create a wide range of experiences within the Snapchat -ecosystem. +The Snapchat Marketing API provides a programmable interface to interact with Snapchat's advertising tools. This API enables automated creation and management of ad campaigns, audience targeting, and performance analytics, which can be a boon for marketers seeking to streamline their Snapchat advertising workflows. Leveraging this API on Pipedream, users can create serverless workflows that automate repetitive tasks, integrate with other marketing tools, and dynamically respond to campaign performance data. -Below is a list of examples of what can be built using the Snapchat API: +# Example Use Cases -- Developed creative campaigns that reach the Snapchat audience -- Built AR (augmented reality) experiences and lenses -- Created branded Snapchat content -- Created Snap Ads and other ad formats -- Developed tools for content management within Snapchat -- Developed tools to help manage influencer and brand partnerships -- Developed real-time analytics to help improve engagement with the Snapchat - audience -- Accessed Snapchat data to help inform decisions around ad placement -- Created experiences to reward and engage Snapchat users +- **Automate Ad Campaign Creation**: Utilize Pipedream to listen for new product listings on an eCommerce platform like Shopify. When a new product is listed, automatically create a corresponding ad campaign in Snapchat, including setting budgets, target demographics, and creative assets. + +- **Dynamic Campaign Adjustment**: Set up a workflow that monitors campaign performance metrics through the Snapchat Marketing API. If the cost per acquisition (CPA) rises above a certain threshold, the workflow could adjust the bid amount or pause the campaign and send an alert to the marketing team via Slack or email. + +- **Synchronize Audiences**: Implement a workflow that syncs email lists from a Customer Relationship Management (CRM) app like HubSpot with Snapchat's Custom Audiences. Periodically update the audience in Snapchat to ensure the most recent contacts are being targeted for upcoming ad campaigns. diff --git a/components/snapdocs/README.md b/components/snapdocs/README.md index f2a8ed6134e5f..de8a2ee58ef81 100644 --- a/components/snapdocs/README.md +++ b/components/snapdocs/README.md @@ -1,20 +1,11 @@ # Overview -The Snapdocs API provides developers with access to a powerful suite of tools -that allow for the complete automation of mortgage and real estate closings. -With the Snapdocs API, developers can build a variety of software applications -ranging from custom portals for organizations and their customers, to -applications that are focused on document security, workflow automation, and -much more. +The Snapdocs API enables seamless integration with its real estate closing platform, allowing users to automate document handling, notifications, and scheduling within the real estate closing process. By leveraging the Snapdocs API on Pipedream, you can craft custom workflows that streamline communication between agents, clients, and other stakeholders, orchestrate document management, and sync data across various platforms involved in the transaction process. -Here are some examples of what every developer can build using the Snapdocs -API: +# Example Use Cases -- Create custom digital portals for organizations and their customers -- Automate workflows and tasks to improve operational efficiency -- Establish document security and compliance standards -- Create data visualizations to monitor progress and productivity -- Integrate with popular systems and platforms to enhance data collaboration -- Create custom document templates and merge data quickly -- Develop automated compliance protocols to ensure accurate documents -- Create real-time alert notifications for key events and changes in documents +- **Automated Document Syncing**: Trigger a workflow on Pipedream whenever a new document is uploaded to Snapdocs. Automatically sync this document to a cloud storage service like Google Drive or Dropbox, ensuring that all stakeholders have immediate access to the latest versions. + +- **Dynamic Notification System**: Set up a Pipedream workflow to monitor status changes in Snapdocs, such as when a closing is scheduled or completed. Then, dispatch real-time notifications through email, SMS via Twilio, or messaging apps like Slack to keep all parties informed and engaged throughout the closing process. + +- **Data Consolidation for Analytics**: Whenever a closing is completed on Snapdocs, trigger a Pipedream workflow to collect data points related to the transaction. Push this data to a Google Sheets document or a database like PostgreSQL for further analysis, reporting, or to inform business strategy and decision-making. diff --git a/components/snappy/README.md b/components/snappy/README.md index 6d03a70f31bef..198f52f412a4d 100644 --- a/components/snappy/README.md +++ b/components/snappy/README.md @@ -1,34 +1,14 @@ # Overview -The [Snappy API](https://besnappy.com is a comprehensive set of tools that -provides developers the capability to build powerful customer service -applications. From automating support workflows and building customer-facing -chatbots, to integrating with enterprise systems, the Snappy API can do it all. +Snappy API enables interaction with Snappy, a customer support tool designed to streamline communication and enhance support team efficiency. By leveraging the Snappy API on Pipedream, you can automate ticket management, organize customer inquiries, and orchestrate seamless communication between support staff and customers. With Pipedream's ability to connect to countless services, you can create workflows that react to events in real-time, sync data across platforms, and personalize customer interactions, all in a serverless environment. -The main features of the Snappy API are focused on helping developers design -and deploy custom customer service applications. These features enable -development teams to build tools such as: +# Example Use Cases -- Automation: Automate interactions with customers by routing inquiries to the - right people, popping in AI-enabled chatbot conversations and delivering - personalized content to customers with ease. -- Agency: Integrate with websites, mobile applications and our comprehensive - agency platform to create custom customer support experiences that fit a - user’s specific needs. -- Conversations: Engage customers through real-time conversations and intuitive - support flows with all communications managed through a single dashboard. -- Insights: Analyze customer conversations and turn them into actionable - insights using AI-driven analytics to improve customer experience. -- Integration: Connect Snappy to different enterprise systems to get the most - out of your customer service processes. +- **Automatic Ticket Creation from Emails** +When a customer sends an email to your support address, use Pipedream to listen for these emails and automatically create a ticket in Snappy. This ensures that no customer query goes unnoticed and that your team can start working on issues as soon as they come in. -Examples of Applications that Can be Built with the Snappy API: +- **Slack Alerts for High-Priority Tickets** +Configure a workflow that watches for new high-priority tickets in Snappy and sends a Slack message to a designated channel or user. This way, your team can swiftly respond to critical issues and provide timely support, keeping customer satisfaction high. -- Automated customer ticketing systems -- Self-service portals -- Chatbot applications -- Help desk applications -- Customer relationship management (CRM) systems -- Virtual assistant applications -- Customer feedback systems -- Knowledge bases +- **Sync Tickets to CRM** +Create a Pipedream workflow that, upon closing a ticket in Snappy, updates the respective customer's record in a CRM like Salesforce or HubSpot. This workflow ensures the customer's history reflects their latest interactions with support, enabling better follow-ups and service continuity. diff --git a/components/snatchbot/README.md b/components/snatchbot/README.md new file mode 100644 index 0000000000000..ed2f2dc9e3822 --- /dev/null +++ b/components/snatchbot/README.md @@ -0,0 +1,11 @@ +# Overview + +The SnatchBot API provides a programmatic window to SnatchBot's chatbot platform, allowing you to manage and interact with your bots outside of the SnatchBot interface. With this API, you can execute tasks like sending messages, retrieving chat history, and managing your bot's structure and behavior. When integrated into Pipedream workflows, the SnatchBot API shines in automating interactions, syncing chat data with other systems, and reacting to events with custom logic and third-party services. + +# Example Use Cases + +- **Automate Customer Support Follow-Ups**: Trigger a workflow in Pipedream when a SnatchBot conversation ends, then use the SendGrid app to email a follow-up survey to the user. This gathers feedback and improves your chatbot service. + +- **Sync Chat Sessions to CRM**: After a conversation in SnatchBot, automatically create or update a contact record in a CRM like Salesforce or HubSpot. This keeps your customer profiles updated with the latest interaction details. + +- **Broadcast Messages from Slack**: Use Pipedream to listen for commands in a Slack channel, then call the SnatchBot API to broadcast messages to users from within Slack. This is handy for timely announcements or alerts. diff --git a/components/snipcart/README.md b/components/snipcart/README.md index 6a29ae0bf7e80..97c8a4be2c67e 100644 --- a/components/snipcart/README.md +++ b/components/snipcart/README.md @@ -1,25 +1,11 @@ # Overview -The Snipcart API offers a powerful and versitile way to build ecommerce -functionality into your web applications. With its RESTful API, it offers an -intuitive way to add shopping cart and payment processing capabilities. +Snipcart is a developer-centric e-commerce solution designed to be easily embedded into any website. With the Snipcart API, you gain the ability to automate cart and checkout operations, manage products, retrieve order details, update inventory, and handle customers. The flexibility of this API opens doors to enhancing e-commerce workflows, including real-time inventory management, order processing, and personalized customer engagement. -Snipcart is also great for creating a custom user experience for customers. -With its flexibility and customization options, you can use Snipcart to provide -your users with tailored uses for their shopping experience. Here are just some -of the things you could build with Snipcart: +# Example Use Cases -- Create an interactive ecommerce website with product pages and checkout - processes -- Build a customized product database with easy search and filtering options -- Have the ability to take orders and payments quickly -- Define shipping rules and implementations -- Add discounts and coupons to encourage customer loyalty -- Allow customers to track their orders -- Set up secure payment methods and payment gateways -- Integrate your payment system with other third-party services -- Create multiple languages, currencies, and payment options -- Integrate with other API systems to enable automatic inventory tracking and - management -- Create customer segmentation rules to target specific customers -- Create custom thank you pages after each purchase +- **Order Fulfillment Automation**: Automate the order fulfillment process by integrating Snipcart with a shipping service like Shippo. When a new order is placed in Snipcart, trigger a workflow in Pipedream that automatically creates a new shipment in Shippo, prints shipping labels, and updates the order status in Snipcart. + +- **Real-Time Inventory Sync**: Keep inventory levels in sync across multiple platforms. When a product's stock changes in Snipcart, use Pipedream to reflect those changes in an external inventory management system or another sales channel like Shopify, ensuring accurate stock counts and preventing overselling. + +- **Post-Purchase Customer Engagement**: Strengthen customer relationships with post-purchase communication. After an order is completed, trigger a Pipedream workflow that sends personalized follow-up emails via SendGrid, requests feedback, or enrolls customers in a rewards program, enriching the overall customer experience. diff --git a/components/snipcart/actions/create-discount/create-discount.mjs b/components/snipcart/actions/create-discount/create-discount.mjs new file mode 100644 index 0000000000000..d11cfd2e19a10 --- /dev/null +++ b/components/snipcart/actions/create-discount/create-discount.mjs @@ -0,0 +1,108 @@ +import app from "../../snipcart.app.mjs"; + +export default { + key: "snipcart-create-discount", + name: "Create Discount", + description: "Create a new Discount. [See the documentation](https://docs.snipcart.com/v3/api-reference/discounts#post-discounts)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "name", + ], + }, + trigger: { + propDefinition: [ + app, + "trigger", + ], + reloadProps: true, + }, + code: { + propDefinition: [ + app, + "code", + ], + optional: true, + hidden: true, + }, + totalToReach: { + propDefinition: [ + app, + "totalToReach", + ], + optional: true, + hidden: true, + }, + type: { + propDefinition: [ + app, + "type", + ], + reloadProps: true, + }, + amount: { + propDefinition: [ + app, + "amount", + ], + optional: true, + hidden: true, + }, + rate: { + propDefinition: [ + app, + "rate", + ], + optional: true, + hidden: true, + }, + maxNumberOfUsages: { + propDefinition: [ + app, + "maxNumberOfUsages", + ], + }, + }, + async additionalProps(props) { + const triggerIsCode = this.trigger === "Code"; + const triggerIsTotal = this.trigger === "Total"; + + props.code.hidden = !triggerIsCode; + props.code.optional = !triggerIsCode; + props.totalToReach.hidden = !triggerIsTotal; + props.totalToReach.optional = !triggerIsTotal; + + const typeIsFixedAmount = this.type === "FixedAmount"; + const typeIsRate = this.type === "Rate"; + + props.amount.hidden = !typeIsFixedAmount; + props.amount.optional = !typeIsFixedAmount; + props.rate.hidden = !typeIsRate; + props.rate.optional = !typeIsRate; + + return {}; + }, + async run({ $ }) { + const response = await this.app.createDiscount({ + $, + data: { + name: this.name, + maxNumberOfUsages: this.maxNumberOfUsages, + trigger: this.trigger, + code: this.code, + totalToReach: this.totalToReach, + type: this.type, + amount: this.amount, + rate: this.rate, + }, + }); + + $.export("$summary", `Successfully created Discount with ID '${response.id}'`); + + return response; + }, +}; diff --git a/components/snipcart/actions/delete-discount/delete-discount.mjs b/components/snipcart/actions/delete-discount/delete-discount.mjs new file mode 100644 index 0000000000000..08dd04447d331 --- /dev/null +++ b/components/snipcart/actions/delete-discount/delete-discount.mjs @@ -0,0 +1,28 @@ +import app from "../../snipcart.app.mjs"; + +export default { + key: "snipcart-delete-discount", + name: "Delete Discount", + description: "Delete a Discount. [See the documentation](https://docs.snipcart.com/v3/api-reference/discounts#delete-discountsid)", + version: "0.0.1", + type: "action", + props: { + app, + discountId: { + propDefinition: [ + app, + "discountId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.deleteDiscount({ + $, + id: this.discountId, + }); + + $.export("$summary", `Successfully deleted Discount with ID '${this.discountId}'`); + + return response; + }, +}; diff --git a/components/snipcart/actions/update-discount/update-discount.mjs b/components/snipcart/actions/update-discount/update-discount.mjs new file mode 100644 index 0000000000000..d875949ccb0fe --- /dev/null +++ b/components/snipcart/actions/update-discount/update-discount.mjs @@ -0,0 +1,122 @@ +import app from "../../snipcart.app.mjs"; + +export default { + key: "snipcart-update-discount", + name: "Update Discount", + description: "Update a Discount. [See the documentation](https://docs.snipcart.com/v3/api-reference/discounts#put-discountsid)", + version: "0.0.1", + type: "action", + props: { + app, + discountId: { + propDefinition: [ + app, + "discountId", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + optional: true, + }, + trigger: { + propDefinition: [ + app, + "trigger", + ], + optional: true, + reloadProps: true, + }, + code: { + propDefinition: [ + app, + "code", + ], + optional: true, + hidden: true, + }, + totalToReach: { + propDefinition: [ + app, + "totalToReach", + ], + optional: true, + hidden: true, + }, + type: { + propDefinition: [ + app, + "type", + ], + optional: true, + reloadProps: true, + }, + amount: { + propDefinition: [ + app, + "amount", + ], + optional: true, + hidden: true, + }, + rate: { + propDefinition: [ + app, + "rate", + ], + optional: true, + hidden: true, + }, + maxNumberOfUsages: { + propDefinition: [ + app, + "maxNumberOfUsages", + ], + }, + }, + async additionalProps(props) { + const triggerIsCode = this.trigger === "Code"; + const triggerIsTotal = this.trigger === "Total"; + + props.code.hidden = !triggerIsCode; + props.code.optional = !triggerIsCode; + props.totalToReach.hidden = !triggerIsTotal; + props.totalToReach.optional = !triggerIsTotal; + + const typeIsFixedAmount = this.type === "FixedAmount"; + const typeIsRate = this.type === "Rate"; + + props.amount.hidden = !typeIsFixedAmount; + props.amount.optional = !typeIsFixedAmount; + props.rate.hidden = !typeIsRate; + props.rate.optional = !typeIsRate; + + return {}; + }, + async run({ $ }) { + const discount = await this.app.getDiscount({ + $, + id: this.discountId, + }); + const response = await this.app.updateDiscount({ + $, + id: this.discountId, + data: { + name: this.name || discount.name, + maxNumberOfUsages: this.maxNumberOfUsages || discount.maxNumberOfUsages, + trigger: this.trigger || discount.trigger, + code: this.code || discount.code, + totalToReach: this.totalToReach || discount.totalToReach, + type: this.type || discount.type, + amount: this.amount || discount.amount, + rate: this.rate || discount.rate, + }, + }); + + $.export("$summary", `Successfully updated Discount with ID '${response.id}'`); + + return response; + }, +}; diff --git a/components/snipcart/common/constants.mjs b/components/snipcart/common/constants.mjs new file mode 100644 index 0000000000000..818a11dd9fd57 --- /dev/null +++ b/components/snipcart/common/constants.mjs @@ -0,0 +1,10 @@ +export default { + TRIGGER_OPTIONS: [ + "Total", + "Code", + ], + TYPE_OPTIONS: [ + "FixedAmount", + "Rate", + ], +}; diff --git a/components/snipcart/package.json b/components/snipcart/package.json new file mode 100644 index 0000000000000..d5c0c8838c01d --- /dev/null +++ b/components/snipcart/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/snipcart", + "version": "0.1.0", + "description": "Pipedream Snipcart Components", + "main": "snipcart.app.mjs", + "keywords": [ + "pipedream", + "snipcart" + ], + "homepage": "https://pipedream.com/apps/snipcart", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/snipcart/snipcart.app.mjs b/components/snipcart/snipcart.app.mjs index f51d845f959f9..5a05ba7d748e7 100644 --- a/components/snipcart/snipcart.app.mjs +++ b/components/snipcart/snipcart.app.mjs @@ -1,11 +1,126 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "snipcart", - propDefinitions: {}, + propDefinitions: { + discountId: { + type: "string", + label: "Discount ID", + description: "ID of the Discount", + async options() { + const discountsIds = await this.listDiscounts(); + + return discountsIds.map(({ + id, name, + }) => ({ + value: id, + label: name, + })); + }, + }, + name: { + type: "string", + label: "Name", + description: "Name of the discount", + }, + maxNumberOfUsages: { + type: "integer", + label: "Max Number of Usages", + description: "The maximum number of usage for the discount. If not specified, customers will be able to use this discount indefinitely.", + optional: true, + }, + trigger: { + type: "string", + label: "Trigger", + description: "Condition that will trigger the discount", + options: constants.TRIGGER_OPTIONS, + }, + code: { + type: "string", + label: "Code", + description: "Code for the discount", + }, + totalToReach: { + type: "string", + label: "Total to Reach", + description: "Minimum amount required to activate the discount. Required when discount Trigger is `FixedAmount`", + }, + type: { + type: "string", + label: "Type", + description: "The type of action that the discount will apply to", + options: constants.TYPE_OPTIONS, + }, + amount: { + type: "string", + label: "Amount", + description: "Discount amount. Required when discount type is `FixedAmount`", + }, + rate: { + type: "string", + label: "Rate", + description: "Discount percentage, i.e.: `10`. Required when discount type is `Rate`", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://app.snipcart.com/api"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + auth: { + username: `${this.$auth.api_key}`, + password: "", + }, + }); + }, + getDiscount({ + id, ...args + }) { + return this._makeRequest({ + path: `/discounts/${id}`, + ...args, + }); + }, + createDiscount(args = {}) { + return this._makeRequest({ + method: "post", + path: "/discounts", + ...args, + }); + }, + updateDiscount({ + id, ...args + }) { + return this._makeRequest({ + method: "put", + path: `/discounts/${id}`, + ...args, + }); + }, + deleteDiscount({ + id, ...args + }) { + return this._makeRequest({ + method: "delete", + path: `/discounts/${id}`, + ...args, + }); + }, + listDiscounts(args = {}) { + return this._makeRequest({ + path: "/discounts", + ...args, + }); }, }, }; diff --git a/components/snov/README.md b/components/snov/README.md index 8546d62ac46e9..b2daff769f8c5 100644 --- a/components/snov/README.md +++ b/components/snov/README.md @@ -1,21 +1,11 @@ # Overview -The Snov.io API is a powerful tool that can be used to automate marketing and -recruitment tasks. With the Snov.io API, you can build automated solutions that -can help you more effectively find leads, build relationships with prospects, -and recruit the right talent. +Snov API enables automated lead generation and outreach by allowing users to find emails, verify them, and send email sequences. On Pipedream, you can craft workflows that react to various triggers and connect Snov with other apps to streamline your sales and marketing efforts. Whether updating CRM contacts, enriching leads with additional data, or automating follow-up emails, Snov’s API paired with Pipedream’s capabilities can save time and improve lead management processes. -Here are a few examples of what you can build with the Snov.io API: +# Example Use Cases -- Automate lead generation: create automated systems that identify and target - prospects that match your ideal customer profile -- Create email drip campaigns: send automated messages to prospects at specific - times and track their response -- Automate recruitment: post job ads, sort and identify potential candidates, - and automatically send follow-up messages -- Advanced search: find contacts and profiles from millions of websites quickly - and accurately -- Enrichment & Verification: verify contact information to boost deliverability - and reduce bounces -- Sales automation: create targeted lists of prospects and set up automated - follow-up messages for each contact. +- **Lead Generation to CRM Integration**: When a new domain is added to your monitoring list, use Snov to find relevant emails and then add them as contacts in your CRM, such as Salesforce or HubSpot, automatically. This keeps your CRM updated with fresh leads without manual data entry. + +- **Email Verification and Customer Onboarding**: After a user signs up on your platform, employ Snov to verify the email address. Once verified, trigger a personalized onboarding email sequence through SendGrid. This ensures you're engaging with real users and provides a warm welcome to your service. + +- **Event-Driven Outreach Campaigns**: Launch targeted email campaigns with Snov whenever a specific event occurs, like a user completing a purchase on your e-commerce platform. Integrate Snov with Shopify to capture event details, craft personalized follow-ups, and trigger a Snov email sequence to encourage repeat business or request feedback. diff --git a/components/snov/package.json b/components/snov/package.json new file mode 100644 index 0000000000000..d189a65c050d4 --- /dev/null +++ b/components/snov/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/snov", + "version": "0.6.0", + "description": "Pipedream snov Components", + "main": "snov.app.mjs", + "keywords": [ + "pipedream", + "snov" + ], + "homepage": "https://pipedream.com/apps/snov", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/snowflake/actions/execute-sql-query/execute-sql-query.mjs b/components/snowflake/actions/execute-sql-query/execute-sql-query.mjs new file mode 100644 index 0000000000000..a98f79dd8ae5c --- /dev/null +++ b/components/snowflake/actions/execute-sql-query/execute-sql-query.mjs @@ -0,0 +1,31 @@ +import snowflake from "../../snowflake.app.mjs"; + +export default { + name: "Execute SQL Query", + version: "0.2.0", + key: "snowflake-execute-sql-query", + description: "Execute a custom Snowflake query. See [our docs](https://pipedream.com/docs/databases/working-with-sql) to learn more about working with SQL in Pipedream.", + type: "action", + props: { + snowflake, + // eslint-disable-next-line pipedream/props-description + sql: { + type: "sql", + auth: { + app: "snowflake", + }, + label: "SQL Query", + }, + }, + async run({ $ }) { + const args = this.snowflake.executeQueryAdapter(this.sql); + + const data = await this.snowflake.executeQuery(args); + + $.export("$summary", `Returned ${data.length} ${data.length === 1 + ? "row" + : "rows"}`); + + return data; + }, +}; diff --git a/components/snowflake/actions/insert-multiple-rows/insert-multiple-rows.mjs b/components/snowflake/actions/insert-multiple-rows/insert-multiple-rows.mjs index 5c35bc409c416..8da7918bc2b50 100644 --- a/components/snowflake/actions/insert-multiple-rows/insert-multiple-rows.mjs +++ b/components/snowflake/actions/insert-multiple-rows/insert-multiple-rows.mjs @@ -6,7 +6,7 @@ export default { key: "snowflake-insert-multiple-rows", name: "Insert Multiple Rows", description: "Insert multiple rows into a table", - version: "0.0.8", + version: "0.1.2", props: { snowflake, database: { diff --git a/components/snowflake/actions/insert-row/insert-row.mjs b/components/snowflake/actions/insert-row/insert-row.mjs index 075c1412df4ab..fbe646fcab3dd 100644 --- a/components/snowflake/actions/insert-row/insert-row.mjs +++ b/components/snowflake/actions/insert-row/insert-row.mjs @@ -5,7 +5,7 @@ export default { key: "snowflake-insert-row", name: "Insert Single Row", description: "Insert a row into a table", - version: "1.0.7", + version: "1.1.2", props: { snowflake, database: { diff --git a/components/snowflake/package.json b/components/snowflake/package.json index 8e643f0f2fa5e..6907b2812e781 100644 --- a/components/snowflake/package.json +++ b/components/snowflake/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/snowflake", - "version": "0.9.5", + "version": "0.13.0", "description": "Pipedream Snowflake Components", "main": "snowflake.app.mjs", "keywords": [ @@ -14,10 +14,10 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.2.0", + "@pipedream/platform": "^3.0.0", "asn1.js": "^5.0.0", "lodash-es": "^4.17.21", - "snowflake-sdk": "^1.6.10", + "@pipedream/snowflake-sdk": "^1.0.8", "uuid": "^8.3.2" } } diff --git a/components/snowflake/snowflake.app.mjs b/components/snowflake/snowflake.app.mjs index 426ccf540a2cf..7e5406ab67736 100644 --- a/components/snowflake/snowflake.app.mjs +++ b/components/snowflake/snowflake.app.mjs @@ -1,5 +1,13 @@ -import snowflake from "snowflake-sdk"; +import { createPrivateKey } from "crypto"; +import { snowflake } from "@pipedream/snowflake-sdk"; import { promisify } from "util"; +import { + sqlProxy, sqlProp, +} from "@pipedream/platform"; + +snowflake.configure({ + logLevel: "WARN", +}); export default { app: "snowflake", @@ -82,25 +90,130 @@ export default { }, }, methods: { + ...sqlProxy.methods, + ...sqlProp.methods, async _getConnection() { if (this.connection) { return this.connection; } - this.connection = snowflake.createConnection({ - ...this.$auth, - application: "PIPEDREAM_PIPEDEAM", - }); + const options = this.getClientConfiguration(); + this.connection = snowflake.createConnection(options); await promisify(this.connection.connect).bind(this.connection)(); return this.connection; }, - async getRows(statement) { + _extractPrivateKey(key, passphrase) { + if (!key?.startsWith("-----BEGIN ENCRYPTED PRIVATE KEY-----")) { + // Key undefined or is not encrypted, no need to extract anything + return key; + } + + const privateKeyObject = createPrivateKey({ + key, + format: "pem", + passphrase, + }); + return privateKeyObject.export({ + format: "pem", + type: "pkcs8", + }); + }, + /** + * A helper method to get the configuration object that's directly fed to + * the Snowflake client constructor. Used by other features (like SQL proxy) + * to initialize their client in an identical way. + * + * @returns The configuration object for the Snowflake client + */ + getClientConfiguration() { + // Snowflake docs: + // https://docs.snowflake.com/en/developer-guide/node-js/nodejs-driver-options#authentication-options + const { + private_key: originalPrivateKey, + private_key_pass: privateKeyPass, + ...auth + } = this.$auth; + const privateKey = this._extractPrivateKey(originalPrivateKey, privateKeyPass); + const authenticator = privateKey + ? "SNOWFLAKE_JWT" + : "SNOWFLAKE"; + return { + ...auth, + application: "PIPEDREAM_PIPEDREAM", + authenticator, + privateKey, + privateKeyPass, + }; + }, + /** + * A helper method to get the schema of the database. Used by other features + * (like the `sql` prop) to enrich the code editor and provide the user with + * auto-complete and fields suggestion. + * + * @returns {DbInfo} The schema of the database, which is a + * JSON-serializable object. + */ + async getSchema() { + const sqlText = ` + SELECT LOWER(t.table_schema) AS "tableSchema", + LOWER(t.table_name) AS "tableName", + TO_NUMERIC(t.row_count) AS "rowCount", + LOWER(c.column_name) AS "columnName", + LOWER(c.data_type) AS "dataType", + LOWER(c.is_nullable) AS "isNullable", + LOWER(c.column_default) AS "columnDefault" + FROM information_schema.tables AS t + JOIN information_schema.columns AS c ON t.table_name = c.table_name + AND t.table_schema = c.table_schema + WHERE t.table_schema = ? + ORDER BY t.table_name, + c.ordinal_position; + `; + const binds = [ + this.$auth.schema, + ]; + const rows = await this.executeQuery({ + sqlText, + binds, + }); + return rows.reduce((acc, row) => { + acc[row.tableName] ??= { + metadata: { + rowCount: row.rowCount, + }, + schema: {}, + }; + acc[row.tableName].schema[row.columnName] = { + ...row, + }; + return acc; + }, {}); + }, + proxyAdapter(preparedStatement = {}) { + const { + sqlText: query = "", + binds: params = [], + } = preparedStatement; + return { + query, + params, + }; + }, + executeQueryAdapter(proxyArgs = {}) { + const { + query: sqlText = "", + params: binds = [], + } = proxyArgs; + return { + sqlText, + binds, + }; + }, + async executeQuery(statement) { const connection = await this._getConnection(); const executedStatement = connection.execute(statement); - return executedStatement.streamRows(); - }, - async collectRows(statement) { - const rowStream = await this.getRows(statement); + + const rowStream = await executedStatement.streamRows(); const rows = []; for await (const row of rowStream) { rows.push(row); @@ -111,19 +224,19 @@ export default { database, schema, }) { let sqlText = `SHOW TABLES IN SCHEMA ${database}.${schema}`; - return this.collectRows({ + return this.executeQuery({ sqlText, }); }, async listDatabases() { const sqlText = "SHOW DATABASES"; - return this.collectRows({ + return this.executeQuery({ sqlText, }); }, async listSchemas(database) { const sqlText = "SHOW SCHEMAS IN DATABASE IDENTIFIER(:1)"; - return this.collectRows({ + return this.executeQuery({ sqlText, binds: [ database, @@ -132,26 +245,26 @@ export default { }, async listWarehouses() { const sqlText = "SHOW WAREHOUSES"; - return this.collectRows({ + return this.executeQuery({ sqlText, }); }, async listUsers() { const sqlText = "SHOW USERS"; - return this.collectRows({ + return this.executeQuery({ sqlText, }); }, async maxQueryHistoryTimestamp() { const sqlText = "SELECT MAX(START_TIME) AS max_ts FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY"; - const maxTs = await this.collectRows({ + const maxTs = await this.executeQuery({ sqlText, }); return +new Date(maxTs[0]?.MAX_TS); }, async maxTaskHistoryTimestamp() { const sqlText = "SELECT MAX(QUERY_START_TIME) AS max_ts FROM TABLE(INFORMATION_SCHEMA.TASK_HISTORY())"; - const maxTs = await this.collectRows({ + const maxTs = await this.executeQuery({ sqlText, }); return +new Date(maxTs[0]?.MAX_TS); @@ -165,7 +278,7 @@ export default { sqlText, binds, }; - return this.collectRows(statement); + return this.executeQuery(statement); }, // Query Snowflake query history. // start and endTime bound the query. Both expect epoch ms timestamps. @@ -181,7 +294,7 @@ export default { sqlText, }; console.log(`Running query: ${sqlText}`); - return this.collectRows(statement); + return this.executeQuery(statement); }, async getFailedTasksInDatabase({ startTime, database, schemas, taskName, @@ -219,7 +332,7 @@ export default { sqlText, binds, }; - return this.collectRows(statement); + return this.executeQuery(statement); }, async getFailedTasksInWarehouse({ startTime, endTime, warehouse, @@ -239,7 +352,7 @@ export default { warehouse, ], }; - return this.collectRows(statement); + return this.executeQuery(statement); }, async getChangesForSpecificObject(startTime, endTime, objectType) { const filters = `QUERY_TYPE != 'SELECT' AND (QUERY_TEXT ILIKE '%CREATE ${objectType}%' OR QUERY_TEXT ILIKE '%ALTER ${objectType}%' OR QUERY_TEXT ILIKE '%DROP ${objectType}%')`; @@ -253,7 +366,7 @@ export default { sqlText, binds, }; - return this.collectRows(statement); + return this.executeQuery(statement); }, async insertRows(tableName, columns, binds) { const sqlText = `INSERT INTO ${tableName} (${columns.join(",")}) VALUES (${columns.map(() => "?").join(", ")});`; @@ -261,7 +374,7 @@ export default { sqlText, binds, }; - return this.collectRows(statement); + return this.executeQuery(statement); }, }, }; diff --git a/components/snowflake/sources/change-to-warehouse/change-to-warehouse.mjs b/components/snowflake/sources/change-to-warehouse/change-to-warehouse.mjs index 7fa77a413ca88..68ef4609ff0d7 100644 --- a/components/snowflake/sources/change-to-warehouse/change-to-warehouse.mjs +++ b/components/snowflake/sources/change-to-warehouse/change-to-warehouse.mjs @@ -17,7 +17,7 @@ export default { // eslint-disable-next-line name: "New, Updated, or Deleted Warehouse", description: "Emit new events when a warehouse is created, altered, or dropped", - version: "0.0.9", + version: "0.1.2", async run() { await this.watchObjectsAndEmitChanges("WAREHOUSE", this.warehouses, this.queryTypes); }, diff --git a/components/snowflake/sources/common-delete.mjs b/components/snowflake/sources/common-delete.mjs index a5996155dc214..3c830c20e120a 100644 --- a/components/snowflake/sources/common-delete.mjs +++ b/components/snowflake/sources/common-delete.mjs @@ -43,7 +43,7 @@ export default { return array; }, async fetchData() { - return this.snowflake.getRows({ + return this.snowflake.executeQuery({ sqlText: this.getSqlText(), }); }, diff --git a/components/snowflake/sources/common-table-scan.mjs b/components/snowflake/sources/common-table-scan.mjs index 58ee7284e8eb7..c220627a45d50 100644 --- a/components/snowflake/sources/common-table-scan.mjs +++ b/components/snowflake/sources/common-table-scan.mjs @@ -15,7 +15,7 @@ export default { propDefinition: [ common.props.snowflake, "schema", - (c) => ({ + (c) => ({ database: c.database, }), ], @@ -146,7 +146,7 @@ export default { sqlText, binds, }; - const rowStream = await this.snowflake.getRows(statement); + const rowStream = await this.snowflake.executeQuery(statement); for await (const row of rowStream) { return row[this.uniqueKey]; } diff --git a/components/snowflake/sources/common-update.mjs b/components/snowflake/sources/common-update.mjs index 6805edb0e66a8..7c399e2391349 100644 --- a/components/snowflake/sources/common-update.mjs +++ b/components/snowflake/sources/common-update.mjs @@ -26,7 +26,7 @@ export default { return this.db.set("dbValues", values); }, async fetchData() { - return this.snowflake.getRows({ + return this.snowflake.executeQuery({ sqlText: this.getSqlText(), }); }, diff --git a/components/snowflake/sources/common.mjs b/components/snowflake/sources/common.mjs index 414fbfd5400e9..884a1b5670e3b 100644 --- a/components/snowflake/sources/common.mjs +++ b/components/snowflake/sources/common.mjs @@ -21,17 +21,24 @@ export default { _setLastMaxTimestamp(lastMaxTimestamp) { this.db.set("lastMaxTimestamp", lastMaxTimestamp); }, + async streamToArray(stream) { + const result = []; + for await (const item of stream) { + result.push(item); + } + return result; + }, async processCollection(statement, timestamp) { - const rowStream = await this.snowflake.getRows(statement); - this.$emit({ - rows: rowStream, + const rowStream = await this.snowflake.executeQuery(statement); + const rows = await this.streamToArray(rowStream); + this.$emit(rows, this.generateMetaForCollection({ timestamp, - }); + })); }, async processSingle(statement, timestamp) { let lastResultId; let rowCount = 0; - const rowStream = await this.snowflake.getRows(statement); + const rowStream = await this.snowflake.executeQuery(statement); for await (const row of rowStream) { const meta = this.generateMeta({ row, diff --git a/components/snowflake/sources/deleted-role/deleted-role.mjs b/components/snowflake/sources/deleted-role/deleted-role.mjs index 99a7e84b51d97..2255938778efd 100644 --- a/components/snowflake/sources/deleted-role/deleted-role.mjs +++ b/components/snowflake/sources/deleted-role/deleted-role.mjs @@ -6,7 +6,7 @@ export default { key: "snowflake-deleted-role", name: "New Deleted Role", description: "Emit new event when a role is deleted", - version: "0.0.6", + version: "0.1.2", methods: { ...common.methods, getSqlText() { diff --git a/components/snowflake/sources/deleted-user/deleted-user.mjs b/components/snowflake/sources/deleted-user/deleted-user.mjs index 9f0a693f16f21..9d6e36df1203d 100644 --- a/components/snowflake/sources/deleted-user/deleted-user.mjs +++ b/components/snowflake/sources/deleted-user/deleted-user.mjs @@ -6,7 +6,7 @@ export default { key: "snowflake-deleted-user", name: "New Deleted User", description: "Emit new event when a user is deleted", - version: "0.0.6", + version: "0.1.2", methods: { ...common.methods, getSqlText() { diff --git a/components/snowflake/sources/failed-task-in-schema/failed-task-in-schema.mjs b/components/snowflake/sources/failed-task-in-schema/failed-task-in-schema.mjs index e2d17157d1deb..ae1f3b03b1a17 100644 --- a/components/snowflake/sources/failed-task-in-schema/failed-task-in-schema.mjs +++ b/components/snowflake/sources/failed-task-in-schema/failed-task-in-schema.mjs @@ -39,7 +39,7 @@ export default { // eslint-disable-next-line name: "Failed Task in Schema", description: "Emit new events when a task fails in a database schema", - version: "0.0.10", + version: "0.1.2", async run() { await this.emitFailedTasks({ database: this.database, diff --git a/components/snowflake/sources/new-database/new-database.mjs b/components/snowflake/sources/new-database/new-database.mjs index c763aa4f32b9f..0be27e16f77e8 100644 --- a/components/snowflake/sources/new-database/new-database.mjs +++ b/components/snowflake/sources/new-database/new-database.mjs @@ -7,7 +7,7 @@ export default { key: "snowflake-new-database", name: "New Database", description: "Emit new event when a database is created", - version: "0.0.8", + version: "0.1.2", methods: { ...common.methods, alwaysRunInSingleProcessMode() { diff --git a/components/snowflake/sources/new-role/new-role.mjs b/components/snowflake/sources/new-role/new-role.mjs index edb6c361b3657..b55bc1cc07506 100644 --- a/components/snowflake/sources/new-role/new-role.mjs +++ b/components/snowflake/sources/new-role/new-role.mjs @@ -6,7 +6,7 @@ export default { key: "snowflake-new-role", name: "New Role", description: "Emit new event when a role is created", - version: "0.0.7", + version: "0.1.2", methods: { ...common.methods, alwaysRunInSingleProcessMode() { diff --git a/components/snowflake/sources/new-row/new-row.mjs b/components/snowflake/sources/new-row/new-row.mjs index 1bfd0ca18401e..b3ed3fc35f2da 100644 --- a/components/snowflake/sources/new-row/new-row.mjs +++ b/components/snowflake/sources/new-row/new-row.mjs @@ -6,7 +6,7 @@ export default { key: "snowflake-new-row", name: "New Row", description: "Emit new event when a row is added to a table", - version: "0.1.9", + version: "0.2.2", methods: { ...common.methods, async getStatement(lastResultId) { diff --git a/components/snowflake/sources/new-schema/new-schema.mjs b/components/snowflake/sources/new-schema/new-schema.mjs index e4a7ebd94c643..b52708d5f8a88 100644 --- a/components/snowflake/sources/new-schema/new-schema.mjs +++ b/components/snowflake/sources/new-schema/new-schema.mjs @@ -7,7 +7,7 @@ export default { key: "snowflake-new-schema", name: "New Schema", description: "Emit new event when a schema is created", - version: "0.0.8", + version: "0.1.2", methods: { ...common.methods, alwaysRunInSingleProcessMode() { diff --git a/components/snowflake/sources/new-table/new-table.mjs b/components/snowflake/sources/new-table/new-table.mjs index f0e3fee913d15..e22a9820efcf0 100644 --- a/components/snowflake/sources/new-table/new-table.mjs +++ b/components/snowflake/sources/new-table/new-table.mjs @@ -7,7 +7,7 @@ export default { key: "snowflake-new-table", name: "New Table", description: "Emit new event when a table is created", - version: "0.0.8", + version: "0.1.2", methods: { ...common.methods, alwaysRunInSingleProcessMode() { diff --git a/components/snowflake/sources/new-user/new-user.mjs b/components/snowflake/sources/new-user/new-user.mjs index 44cc2dee085fa..2ed4951da9176 100644 --- a/components/snowflake/sources/new-user/new-user.mjs +++ b/components/snowflake/sources/new-user/new-user.mjs @@ -6,7 +6,7 @@ export default { key: "snowflake-new-user", name: "New User", description: "Emit new event when a user is created", - version: "0.0.7", + version: "0.1.2", methods: { ...common.methods, alwaysRunInSingleProcessMode() { diff --git a/components/snowflake/sources/query-results/query-results.mjs b/components/snowflake/sources/query-results/query-results.mjs index 358a56989244d..00c31110c3516 100644 --- a/components/snowflake/sources/query-results/query-results.mjs +++ b/components/snowflake/sources/query-results/query-results.mjs @@ -8,7 +8,7 @@ export default { name: "New Query Results", // eslint-disable-next-line description: "Run a SQL query on a schedule, triggering a workflow for each row of results", - version: "0.1.9", + version: "0.2.2", props: { ...common.props, sqlQuery: { diff --git a/components/snowflake/sources/updated-role/updated-role.mjs b/components/snowflake/sources/updated-role/updated-role.mjs index 55f34108d0801..56ed51fc7d2dd 100644 --- a/components/snowflake/sources/updated-role/updated-role.mjs +++ b/components/snowflake/sources/updated-role/updated-role.mjs @@ -6,7 +6,7 @@ export default { key: "snowflake-updated-role", name: "New Update Role", description: "Emit new event when a role is updated", - version: "0.0.6", + version: "0.1.2", methods: { ...common.methods, getLookUpKey() { diff --git a/components/snowflake/sources/updated-user/updated-user.mjs b/components/snowflake/sources/updated-user/updated-user.mjs index 87a598c007224..f89f88091857c 100644 --- a/components/snowflake/sources/updated-user/updated-user.mjs +++ b/components/snowflake/sources/updated-user/updated-user.mjs @@ -6,7 +6,7 @@ export default { key: "snowflake-updated-user", name: "New Update User", description: "Emit new event when a user is updated", - version: "0.0.6", + version: "0.1.2", methods: { ...common.methods, getLookUpKey() { diff --git a/components/snowflake/sources/usage-monitor/usage-monitor.mjs b/components/snowflake/sources/usage-monitor/usage-monitor.mjs index 92c1d45d04764..0df075de144fb 100644 --- a/components/snowflake/sources/usage-monitor/usage-monitor.mjs +++ b/components/snowflake/sources/usage-monitor/usage-monitor.mjs @@ -6,7 +6,7 @@ export default { key: "snowflake-usage-monitor", name: "New Usage Monitor", description: "Emit new event when a query is executed in the specified params", - version: "0.0.6", + version: "0.1.2", dedupe: "unique", props: { snowflake, @@ -204,7 +204,7 @@ export default { }; }, async fetchData() { - return this.snowflake.getRows(this.getSqlStatement()); + return this.snowflake.executeQuery(this.getSqlStatement()); }, emit(event) { this.$emit(event, { diff --git a/components/snowflake_test/package.json b/components/snowflake_test/package.json new file mode 100644 index 0000000000000..4ade0b730fac4 --- /dev/null +++ b/components/snowflake_test/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/snowflake_test", + "version": "0.0.1", + "description": "Pipedream Snowflake - TEST Components", + "main": "snowflake_test.app.mjs", + "keywords": [ + "pipedream", + "snowflake_test" + ], + "homepage": "https://pipedream.com/apps/snowflake_test", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/snowflake_test/snowflake_test.app.mjs b/components/snowflake_test/snowflake_test.app.mjs new file mode 100644 index 0000000000000..8cdf86fd00135 --- /dev/null +++ b/components/snowflake_test/snowflake_test.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "snowflake_test", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/snyk/README.md b/components/snyk/README.md new file mode 100644 index 0000000000000..8ce6a8d93c5ee --- /dev/null +++ b/components/snyk/README.md @@ -0,0 +1,11 @@ +# Overview + +The Snyk API provides programmatic access to Snyk's vulnerability detection capabilities, enabling you to automate security analysis and monitor for security issues in your codebases, open-source dependencies, container images, and more. On Pipedream, you can harness this API to create automated, serverless workflows that integrate Snyk's security insights with your development and operations processes. With Pipedream's ability to connect to hundreds of apps, you can seamlessly integrate Snyk's data with other services like Slack for notifications, JIRA for issue tracking, or GitHub for code management. + +# Example Use Cases + +- **Automated Vulnerability Alerts in Slack**: Trigger a Pipedream workflow whenever Snyk identifies a new vulnerability in your project. The workflow can filter for high-severity issues and send a formatted alert to a designated Slack channel, keeping your team immediately informed. + +- **JIRA Ticket Creation for Security Issues**: When Snyk finds security issues, use Pipedream to automatically create JIRA tickets, assigning them to the appropriate team members with all relevant details. This ensures that vulnerabilities are tracked and addressed according to your project's workflow. + +- **Continuous Security Audits with Scheduled Workflows**: Set up a Pipedream workflow to periodically trigger Snyk scans on your repositories and monitor the security status of your projects. The results could then be logged in Google Sheets or sent via email, providing you with a regular security report. diff --git a/components/social_intents/README.md b/components/social_intents/README.md index 0e85cc44a6f82..007ab9dc203bd 100644 --- a/components/social_intents/README.md +++ b/components/social_intents/README.md @@ -1,15 +1,17 @@ # Overview -Some examples of things you can build using the Social Intents include: +The Social Intents API enables seamless integration of live chat, customer feedback, and email list building services into your digital platforms. You can byukd workflows that automatically trigger actions based on chat events, gather insights from customer interactions, and enhance your marketing strategies by connecting Social Intents to various other tools, like CRMs, email marketing services, and databases. -- Live chat with your website visitors from the messaging tools you already use at your company: **Microsoft Teams**, **Slack**, **Zoom**, or **Webex**. -- Embed Live Chat Snippet on the Website -- You can have a customizable chat widget and unlimited team members +# Example Use Cases -
-
+- **Instant Customer Support Notifications**: When a new message is received through Social Intents live chat, use Pipedream to send a notification to the support team via Slack or email, ensuring immediate attention and a quick response time. + +- **Feedback Analysis and Aggregation**: After collecting customer feedback via Social Intents, set up a workflow that funnels this data into a Google Sheets spreadsheet for easy analysis, or pushes it directly into a tool like Tableau to visualize customer satisfaction trends. + +- **Automated Contact Segmentation**: Integrate Social Intents with email marketing platforms like Mailchimp or Constant Contact. When a visitor subscribes to your list via a Social Intents widget, automatically add their contact information to segmented lists based on their inquiries or interests, to tailor future marketing campaigns. -# How to use webhooks +# Getting Started +## How to use webhooks ### Copy the URL on your created source diff --git a/components/softledger/README.md b/components/softledger/README.md new file mode 100644 index 0000000000000..0b50dd94182f1 --- /dev/null +++ b/components/softledger/README.md @@ -0,0 +1,11 @@ +# Overview + +SoftLedger is a powerful API that enables you to automate accounting and financial tasks by integrating with your existing financial stack. By leveraging SoftLedger API on Pipedream, you can create custom workflows that streamline financial reporting, manage inventory, automate ledger entries, and sync financial data across applications. Pipedream's serverless platform allows you to connect the SoftLedger API with hundreds of other apps to trigger actions, process data, and automate repetitive tasks without writing complex code. + +# Example Use Cases + +- **Sync Invoices to Accounting Software**: Automate the process of capturing sales data by syncing invoices from e-commerce platforms like Shopify to SoftLedger. Whenever a new sale occurs, trigger a workflow that creates a corresponding invoice entry in SoftLedger, keeping your financial records up-to-date. + +- **Consolidate Financial Reports**: Simplify monthly financial close by aggregating data from multiple sources into SoftLedger. Set up a workflow that pulls data from payment gateways such as Stripe and PayPal, ensuring that all revenue streams are accurately reflected in your financial statements. + +- **Automate Expense Tracking**: Track expenses seamlessly by creating a workflow that monitors transaction updates from credit card processors or expense management tools like Expensify. Upon detecting new transactions, automatically create expense entries in SoftLedger, streamlining the reconciliation process and providing real-time visibility into financial data. diff --git a/components/softr/.gitignore b/components/softr/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/softr/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/softr/README.md b/components/softr/README.md index 703b2c4d9c815..ad051002d7986 100644 --- a/components/softr/README.md +++ b/components/softr/README.md @@ -1,23 +1,11 @@ # Overview -The Softr API makes it easy to build powerful integrations with a range of -software systems, including CRMs, cloud applications, payment systems, and -web-based tools. With the Softr API, you can access and extend the features and -capabilities of all these products, making them even better for you and your -users. +The Softr API unlocks the power to automate and integrate Softr-built web apps with a multitude of external services and internal workflows. With it, you can streamline data manipulation, sync content, or trigger actions based on specific events. Create, update, and fetch data from your Softr applications, and leverage this functionality to enhance user experience, improve operational efficiency, and reduce manual workloads. When used on Pipedream, the API can connect with other apps to create robust, serverless workflows that operate in real-time or on a scheduled basis. -Here are some examples of powerful integrations you can build with Softr: +# Example Use Cases -- Automate customer onboarding by creating an integration with your CRM. -- Set up an automated billing system for customers by linking payment systems - like Stripe and PayPal. -- Integrate with cloud tools like Slack and Dropbox to keep your team in - connected and informed. -- Access social media profiles like Twitter and Facebook to enhance the data - used in marketing and customer support. -- Generate realtime notifications to keep your team alerted of any changes or - updates. -- Connect to online ordering systems like Shopify and WooCommerce. -- Improve business intelligence analytics with custom reports. -- Extend the functionality of web-based systems, including accounting and ERP - software. +- **Automated Content Publishing**: Create a scheduled workflow on Pipedream that pulls the latest content from a CMS like WordPress, formats it, and pushes it to your Softr app. This way, your app, acting as a content portal, is always up to date with fresh articles or posts. + +- **Enhanced Lead Management**: Connect Softr to a CRM tool like Salesforce. Each time a new lead is captured on your Softr app, it triggers a Pipedream workflow that automatically adds or updates that lead's info in Salesforce. It ensures that your sales team has the latest data without manual entry. + +- **User Feedback Aggregation**: Integrate your Softr app with a messaging app like Slack using Pipedream. When users submit feedback through your Softr app, the workflow triggers and sends a formatted message to a dedicated Slack channel, enabling quick team response and user engagement tracking. diff --git a/components/softr/actions/create-user/create-user.mjs b/components/softr/actions/create-user/create-user.mjs new file mode 100644 index 0000000000000..4000c9b56a553 --- /dev/null +++ b/components/softr/actions/create-user/create-user.mjs @@ -0,0 +1,48 @@ +import softr from "../../softr.app.mjs"; + +export default { + key: "softr-create-user", + name: "Create User", + description: "Creates a new user within your Softr app. [See the documentation](https://docs.softr.io/softr-api/tTFQ5vSAUozj5MsKixMH8C/api-setup-and-endpoints/j1PrTZxt7pv3iZCnZ5Fp19#create-user)", + version: "0.0.1", + type: "action", + props: { + softr, + fullName: { + type: "string", + label: "Full Name", + description: "Full name of the new user", + }, + email: { + propDefinition: [ + softr, + "email", + ], + }, + password: { + type: "string", + label: "Password", + description: "Password for the new user. If you don't specify a password, it will be generated for the user automatically.", + optional: true, + }, + generateMagicLink: { + type: "boolean", + label: "Generate Magic Link", + description: "Whether you want to generate a Magic Link for the user or not", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.softr.createUser({ + $, + data: { + full_name: this.fullName, + email: this.email, + password: this.password, + generate_magic_link: this.generateMagicLink, + }, + }); + $.export("$summary", `Successfully created user with email: ${this.email}`); + return response; + }, +}; diff --git a/components/softr/actions/delete-user/delete-user.mjs b/components/softr/actions/delete-user/delete-user.mjs new file mode 100644 index 0000000000000..47a4ed9bca521 --- /dev/null +++ b/components/softr/actions/delete-user/delete-user.mjs @@ -0,0 +1,26 @@ +import softr from "../../softr.app.mjs"; + +export default { + key: "softr-delete-user", + name: "Delete User", + description: "Removes an existing user from your Softr app. Be aware, this action is irreversible. [See the documentation](https://docs.softr.io/softr-api/tTFQ5vSAUozj5MsKixMH8C/api-setup-and-endpoints/j1PrTZxt7pv3iZCnZ5Fp19#delete-user)", + version: "0.0.1", + type: "action", + props: { + softr, + email: { + propDefinition: [ + softr, + "email", + ], + }, + }, + async run({ $ }) { + const response = await this.softr.deleteUser({ + $, + email: this.email, + }); + $.export("$summary", `Successfully deleted user with email: ${this.email}`); + return response; + }, +}; diff --git a/components/softr/actions/generate-magic-link/generate-magic-link.mjs b/components/softr/actions/generate-magic-link/generate-magic-link.mjs new file mode 100644 index 0000000000000..d9d515a00181e --- /dev/null +++ b/components/softr/actions/generate-magic-link/generate-magic-link.mjs @@ -0,0 +1,26 @@ +import softr from "../../softr.app.mjs"; + +export default { + key: "softr-generate-magic-link", + name: "Generate Magic Link", + description: "Generate a Magic Link for the specified user in your Softr app. [See the documentation](https://docs.softr.io/softr-api/tTFQ5vSAUozj5MsKixMH8C/api-setup-and-endpoints/j1PrTZxt7pv3iZCnZ5Fp19#generate-a-magic-link-for-the-user)", + version: "0.0.1", + type: "action", + props: { + softr, + email: { + propDefinition: [ + softr, + "email", + ], + }, + }, + async run({ $ }) { + const response = await this.softr.generateMagicLink({ + $, + email: this.email, + }); + $.export("$summary", `Successfully generated magic link: ${response}`); + return response; + }, +}; diff --git a/components/softr/app/softr.app.ts b/components/softr/app/softr.app.ts deleted file mode 100644 index 88685bcaf0c50..0000000000000 --- a/components/softr/app/softr.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "softr", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); diff --git a/components/softr/package.json b/components/softr/package.json index 611ddc580c744..8946c9c211f4c 100644 --- a/components/softr/package.json +++ b/components/softr/package.json @@ -1,18 +1,18 @@ { "name": "@pipedream/softr", - "version": "0.0.4", + "version": "0.1.0", "description": "Pipedream Softr Components", - "main": "dist/app/softr.app.mjs", + "main": "softr.app.mjs", "keywords": [ "pipedream", "softr" ], - "files": [ - "dist" - ], "homepage": "https://pipedream.com/apps/softr", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.4" } } diff --git a/components/softr/softr.app.mjs b/components/softr/softr.app.mjs new file mode 100644 index 0000000000000..9368b69eeaa14 --- /dev/null +++ b/components/softr/softr.app.mjs @@ -0,0 +1,59 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "softr", + propDefinitions: { + email: { + type: "string", + label: "Email", + description: "Email address of the user", + }, + }, + methods: { + _baseUrl() { + return "https://studio-api.softr.io/v1/api"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "Softr-Api-Key": `${this.$auth.api_key}`, + "Softr-Domain": `${this.$auth.domain}`, + "Content-Type": "application/json", + }, + }); + }, + createUser(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/users", + ...opts, + }); + }, + deleteUser({ + email, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/users/${email}`, + ...opts, + }); + }, + generateMagicLink({ + email, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/users/magic-link/generate/${email}`, + ...opts, + }); + }, + }, +}; diff --git a/components/solarwinds_service_desk/README.md b/components/solarwinds_service_desk/README.md new file mode 100644 index 0000000000000..d308b1cff1cc3 --- /dev/null +++ b/components/solarwinds_service_desk/README.md @@ -0,0 +1,11 @@ +# Overview + +The SolarWinds Service Desk API provides a way to automate and integrate your IT service management processes. Using this API in Pipedream, you can create, update, and track service requests, manage incidents, problems, and changes, access asset information, and leverage user data within your workflows. It's about connecting your service desk with other apps to streamline processes, reduce response times, and enhance service delivery. + +# Example Use Cases + +- **Automated Incident Reporting**: Create a workflow on Pipedream that monitors for specific system alerts or error logs from your infrastructure (possibly using a tool like Datadog). When an issue is detected, automatically create an incident in SolarWinds Service Desk, assigning the right team and priority based on the alert details. + +- **SLA Monitoring**: Set up a Pipedream workflow that checks ongoing incidents in SolarWinds Service Desk periodically and evaluates their response times against SLAs. If an incident is at risk of breaching its SLA, the workflow could notify a manager via email or Slack to take immediate action. + +- **Asset Management Update**: Whenever a new asset is registered in your inventory system, use a Pipedream workflow to add or update the asset details in SolarWinds Service Desk. This ensures your IT asset information is always in sync and up-to-date for efficient service management. diff --git a/components/solcast/README.md b/components/solcast/README.md new file mode 100644 index 0000000000000..227bc242a2345 --- /dev/null +++ b/components/solcast/README.md @@ -0,0 +1,11 @@ +# Overview + +The Solcast API offers solar energy forecasting and historical solar data, enabling developers to integrate accurate solar radiation, PV power output, and weather data into their applications. With the Solcast API on Pipedream, you can automate data retrieval for solar project planning, performance monitoring, and energy system integration. Pipedream's serverless platform means you can set up workflows that respond to various triggers to process and act on Solcast data in real-time, without managing infrastructure. + +# Example Use Cases + +- **Solar Power Generation Forecasting**: Automate the retrieval of solar power forecasts for multiple locations and feed this data into a database or analytics tool. This can be used for optimizing energy usage and storage or preparing the grid for changes in solar power supply. + +- **Solar Data Aggregation for Smart Homes**: Collect solar radiation and PV power output data to manage smart home energy systems efficiently. Use this data to control home appliances, battery storage systems, and even to automate the sale of excess energy back to the grid. + +- **Weather Impact Analysis on Solar Panels**: Trigger workflows to analyze the impact of weather events on solar panel efficiency. Combine Solcast data with weather apps to alert maintenance teams about necessary inspections or cleaning, ensuring the maximum efficiency of solar installations. diff --git a/components/solcast/actions/get-live-weather/get-live-weather.mjs b/components/solcast/actions/get-live-weather/get-live-weather.mjs new file mode 100644 index 0000000000000..6ee8e7625cfe1 --- /dev/null +++ b/components/solcast/actions/get-live-weather/get-live-weather.mjs @@ -0,0 +1,49 @@ +import solcast from "../../solcast.app.mjs"; + +export default { + key: "solcast-get-live-weather", + name: "Get Live Weather", + description: "Get irradiance and weather estimated actuals for near real-time and past 7 days for the requested location, derived from satellite and numerical weather models. [See the documentation](https://docs.solcast.com.au/#b9863910-c788-4e98-a3af-eb8da8f49647)", + version: "0.0.1", + type: "action", + props: { + solcast, + latitude: { + propDefinition: [ + solcast, + "latitude", + ], + }, + longitude: { + propDefinition: [ + solcast, + "longitude", + ], + }, + hours: { + propDefinition: [ + solcast, + "hours", + ], + }, + period: { + propDefinition: [ + solcast, + "period", + ], + }, + }, + async run({ $ }) { + const response = await this.solcast.getLiveWeather({ + $, + params: { + latitude: this.latitude, + longitude: this.longitude, + hours: this.hours, + period: this.period, + }, + }); + $.export("$summary", `Successfully retrieved live weather data for location \`${this.latitude}, ${this.longitude}\``); + return response; + }, +}; diff --git a/components/solcast/actions/get-monthly-averages/get-monthly-averages.mjs b/components/solcast/actions/get-monthly-averages/get-monthly-averages.mjs new file mode 100644 index 0000000000000..560e68baad983 --- /dev/null +++ b/components/solcast/actions/get-monthly-averages/get-monthly-averages.mjs @@ -0,0 +1,42 @@ +import solcast from "../../solcast.app.mjs"; + +export default { + key: "solcast-get-monthly-averages", + name: "Get Monthly Averages", + description: "Get montly weather averages for a location. [See the documentation](https://docs.solcast.com.au/#7ad3c227-d385-4455-b17f-3efcb8d4c695)", + version: "0.0.1", + type: "action", + props: { + solcast, + latitude: { + propDefinition: [ + solcast, + "latitude", + ], + }, + longitude: { + propDefinition: [ + solcast, + "longitude", + ], + }, + timezone: { + type: "string", + label: "Timezone", + description: "Date time to return in data set. Accepted values are `utc`, `longitudinal`, or a range `-13` to `13` for utc offset.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.solcast.getMonthlyAverages({ + $, + params: { + latitude: this.latitude, + longitude: this.longitude, + timezone: this.timezone, + }, + }); + $.export("$summary", `Successfully retrieved monthly averages for location \`${this.latitude}, ${this.longitude}\``); + return response; + }, +}; diff --git a/components/solcast/actions/get-weather-forecast/get-weather-forecast.mjs b/components/solcast/actions/get-weather-forecast/get-weather-forecast.mjs new file mode 100644 index 0000000000000..2258534737e76 --- /dev/null +++ b/components/solcast/actions/get-weather-forecast/get-weather-forecast.mjs @@ -0,0 +1,49 @@ +import solcast from "../../solcast.app.mjs"; + +export default { + key: "solcast-get-weather-forecast", + name: "Get Weather Forecast", + description: "Get irradiance and weather forecasts for the requested location from the present up to 14 days ahead, derived from satellite and numerical weather models. [See the documentation](https://docs.solcast.com.au/#4e0e8a96-7a12-4654-8407-6bbbb37478b1)", + version: "0.0.1", + type: "action", + props: { + solcast, + latitude: { + propDefinition: [ + solcast, + "latitude", + ], + }, + longitude: { + propDefinition: [ + solcast, + "longitude", + ], + }, + hours: { + propDefinition: [ + solcast, + "hours", + ], + }, + period: { + propDefinition: [ + solcast, + "period", + ], + }, + }, + async run({ $ }) { + const response = await this.solcast.getWeatherForecast({ + $, + params: { + latitude: this.latitude, + longitude: this.longitude, + hours: this.hours, + period: this.period, + }, + }); + $.export("$summary", `Successfully retrieved forecast data for location \`${this.latitude}, ${this.longitude}\``); + return response; + }, +}; diff --git a/components/solcast/package.json b/components/solcast/package.json new file mode 100644 index 0000000000000..30bd64c214558 --- /dev/null +++ b/components/solcast/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/solcast", + "version": "0.1.0", + "description": "Pipedream Solcast Components", + "main": "solcast.app.mjs", + "keywords": [ + "pipedream", + "solcast" + ], + "homepage": "https://pipedream.com/apps/solcast", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/solcast/solcast.app.mjs b/components/solcast/solcast.app.mjs new file mode 100644 index 0000000000000..89ee3422a5c55 --- /dev/null +++ b/components/solcast/solcast.app.mjs @@ -0,0 +1,75 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "solcast", + propDefinitions: { + latitude: { + type: "string", + label: "Latitude", + description: "The latitude of the location", + }, + longitude: { + type: "string", + label: "Longitude", + description: "The longitude of the location", + }, + hours: { + type: "integer", + label: "Hours", + description: "Time window of the response in hours from 1 to 336", + max: 336, + optional: true, + }, + period: { + type: "string", + label: "Period", + description: "Length of the averaging period in ISO 8601 format. Default is `PT30M`", + options: [ + "PT5M", + "PT10M", + "PT15M", + "PT20M", + "PT30M", + "PT60M", + ], + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.solcast.com.au"; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.api_key}`, + }, + ...opts, + }); + }, + getLiveWeather(opts = {}) { + return this._makeRequest({ + path: "/data/live/radiation_and_weather", + ...opts, + }); + }, + getWeatherForecast(opts = {}) { + return this._makeRequest({ + path: "/data/forecast/radiation_and_weather", + ...opts, + }); + }, + getMonthlyAverages(opts = {}) { + return this._makeRequest({ + path: "/monthly_averages", + ...opts, + }); + }, + }, +}; diff --git a/components/solve_crm/README.md b/components/solve_crm/README.md index 0aa0c1d10f855..a25a31337122f 100644 --- a/components/solve_crm/README.md +++ b/components/solve_crm/README.md @@ -1,32 +1,11 @@ # Overview -The Solve CRM API offers a comprehensive set of features to developers, -allowing them to build custom CRM applications and integrations that smoothly -fit into their current business processes. The API provides a wide range of -features and capabilities, making it easy to automate certain processes and -leverage the Solve CRM platform to improve user experience and collaboration. +The Solve CRM API offers a variety of endpoints to manage customer relationships and sales processes programmatically. It enables users to create, read, update, and delete customer data, manage projects, track email communication, and automate workflows involving contacts and companies. Pipedream harnesses this power, allowing you to connect Solve CRM with a myriad of other apps and services to streamline sales pipelines, enhance customer engagement, and automate routine tasks. -With the Solve CRM API, you can build a variety of applications and -integrations, from web-based CRM dashboards to automated workflows. Some -examples of applications you can create using the Solve CRM API include: +# Example Use Cases -- Custom CRM Applications – Create high-performance applications that are - tailored to your specific business requirements, allowing for streamlined - customer data management, project tracking, and collaboration -- Automated Workflows – Develop automated processes that are triggered by - complex events, allowing for more efficient task management and seamless - communication -- Reporting Dashboard – Create a report dashboard that displays timesheets, - tasks, and project status, giving users a sense of overall project progress -- Third-Party Integrations – Establish connections with third-party services - like Salesforce, Zendesk, or Dropbox, leveraging the Solve CRM platform to - access customer data, streamline collaboration, and easily share relevant - information. -- Custom Forms – Design custom forms to gather customer data, allowing - businesses to easily manage and track customer relations. -- Collaboration Tools – Create tools for seamless collaboration for teams, - allowing for efficient communication and task completion. +- **Lead Enrichment and Follow-Up**: Automate the process of enriching new leads with additional data from external sources (like Clearbit) as soon as they are added to Solve CRM. Then, trigger an email sequence from a platform like SendGrid to engage these leads immediately. -The possibilities are limitless with Solve CRM's powerful API capabilities, -giving developers the ability to create and customize applications that -perfectly fit their business needs. +- **Support Ticket Creation from Email**: Monitor a dedicated support email inbox with an Email Trigger in Pipedream. Parse received emails for issues and automatically create corresponding support tickets in Solve CRM, assigning them to the appropriate team or individual. + +- **Project Management Sync**: When a new project is created in Solve CRM, trigger a workflow that creates a matching project in a project management app like Trello or Asana, syncing all necessary data. This keeps teams aligned across platforms and ensures seamless project tracking. diff --git a/components/sonar/README.md b/components/sonar/README.md index 6180bc7845180..c5617181c3277 100644 --- a/components/sonar/README.md +++ b/components/sonar/README.md @@ -1,23 +1,11 @@ # Overview -The Purple Sonar API allows developers to quickly create custom audio -processing applications. With its powerful and sophisticated algorithms, the -API allows for a wide range of audio processing applications such as speech -recognition, music analysis, audio processing, and more. +The Purple Sonar API offers real-time threat intelligence and URL analysis, crucial for enhancing cybersecurity measures. By integrating with Pipedream, you can create workflows that automate the process of monitoring, analyzing, and responding to potential threats. Use cases include triaging alerts, enriching incident reports with additional data, or even proactively scanning URLs in company communications to pre-emptively block malicious links. -Here are some examples of what you can build with the Purple Sonar API: +# Example Use Cases -- Speech Recognition & Synthesis: Create applications that are capable of - recognizing and synthesizing user speech. -- Music Analysis: Analyze audio sources to identify patterns and extract - harmonic content. -- Audio Processing: Manipulate various audio sources, such as applying effects - and filters. -- Singing Voice Amplification: Make vocals louder and clearer for karaoke, EDM, - and live performances. -- Automated Mixing/Mastering: Automatically mix/master multiple audio sources - together. -- System Identification: Analyze audio signals and create audio-based systems - that are capable of reacting in response to environmental changes. -- Speech Enrichment: Enhance the intelligibility of speech with noise - cancellation, echo reduction, and volume equalization. +- **Automated Alert Triage**: Create a workflow that triggers when your security system flags a suspicious URL. Purple Sonar can analyze the URL, and if it’s deemed malicious, the workflow can automatically update your firewall rules to block it, send notifications to your security team, and log the incident in your SIEM system. + +- **Incident Report Enrichment**: Set up a Pipedream workflow that runs when new incidents are reported. The workflow calls Purple Sonar to gather detailed threat intelligence on the URLs involved. This data is then appended to the incident ticket in your tracking system (like JIRA), providing richer context for the analysts. + +- **Proactive Communication Scanning**: Harness a workflow that scans URLs within emails or chat messages as they're sent or received. Using Purple Sonar, the workflow assesses the risks of the links, and if a link is found to be harmful, it can automatically alert the sender, receiver, and IT department, or even remove the message from the communication platform. diff --git a/components/sonar/package.json b/components/sonar/package.json new file mode 100644 index 0000000000000..ce2d150a6c832 --- /dev/null +++ b/components/sonar/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/sonar", + "version": "0.6.0", + "description": "Pipedream sonar Components", + "main": "sonar.app.mjs", + "keywords": [ + "pipedream", + "sonar" + ], + "homepage": "https://pipedream.com/apps/sonar", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/sonarcloud/README.md b/components/sonarcloud/README.md new file mode 100644 index 0000000000000..80cb349f63ef4 --- /dev/null +++ b/components/sonarcloud/README.md @@ -0,0 +1,11 @@ +# Overview + +The SonarCloud API facilitates automated code quality checks and security vulnerability assessments. By integrating with Pipedream, developers can harness this power within serverless workflows, triggering actions based on code analysis results, monitoring project metrics, and automating project management tasks. The API allows for enhanced productivity by connecting code quality data with other services and tools, streamlining the development process. + +# Example Use Cases + +- **Automate Issue Tracking on Project Analysis Completion**: When SonarCloud completes an analysis and discovers issues, trigger a Pipedream workflow to automatically create issues in a project management tool like Jira. This ensures that code quality improvements are tracked and managed efficiently. + +- **Merge Request Analysis for Continuous Integration**: Use Pipedream to trigger a workflow upon merge requests in GitHub or GitLab. The workflow could call the SonarCloud API to run an analysis on the new code. If the quality gate passes, the workflow could automatically merge the request, ensuring only quality code makes it to the main branch. + +- **Daily Code Quality Report via Email**: Set up a Pipedream scheduled workflow to fetch daily code quality metrics from SonarCloud. The workflow could compile a report and send it via email using a service like SendGrid. This keeps the team informed about the project's health and progress without manual checks. diff --git a/components/sonarcloud/package.json b/components/sonarcloud/package.json index 03522b7555787..ffea21f06611f 100644 --- a/components/sonarcloud/package.json +++ b/components/sonarcloud/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/sonarcloud", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream SonarCloud Components", "main": "sonarcloud.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" } -} \ No newline at end of file +} diff --git a/components/sonarcloud/sonarcloud.app.mjs b/components/sonarcloud/sonarcloud.app.mjs index 179c7318b518b..c29e30da24648 100644 --- a/components/sonarcloud/sonarcloud.app.mjs +++ b/components/sonarcloud/sonarcloud.app.mjs @@ -1,11 +1,89 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "sonarcloud", - propDefinitions: {}, + propDefinitions: { + organization: { + type: "string", + label: "Organization", + description: "The key of the organization that will own the webhook.", + async options({ page }) { + const { organizations } = await this.listOrganizations({ + params: { + p: page + 1, + member: true, + }, + }); + + return organizations.map(({ + key: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + project: { + type: "string", + label: "Project", + description: "The key of the project that will own the webhook.", + async options({ + page, organization, + }) { + const { components } = await this.listProjects({ + params: { + p: page + 1, + organization, + }, + }); + + return components.map(({ + key: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `https://${this.$auth.token}@sonarcloud.io/api`; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks/create", + ...opts, + }); + }, + deleteWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks/delete", + ...opts, + }); + }, + listOrganizations(opts = {}) { + return this._makeRequest({ + path: "/organizations/search", + ...opts, + }); + }, + listProjects(opts = {}) { + return this._makeRequest({ + path: "/projects/search", + ...opts, + }); }, }, }; diff --git a/components/sonarcloud/sources/new-analysis-completed-instant/new-analysis-completed-instant.mjs b/components/sonarcloud/sources/new-analysis-completed-instant/new-analysis-completed-instant.mjs new file mode 100644 index 0000000000000..a60a6b45b2e37 --- /dev/null +++ b/components/sonarcloud/sources/new-analysis-completed-instant/new-analysis-completed-instant.mjs @@ -0,0 +1,72 @@ +import sonarcloud from "../../sonarcloud.app.mjs"; + +export default { + key: "sonarcloud-new-analysis-completed-instant", + name: "New Analysis Completed (Instant)", + description: "Emit new event when a new analisys is completed.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + sonarcloud, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + name: { + type: "string", + label: "Name", + description: "Name displayed in the administration console of webhooks.", + }, + organization: { + propDefinition: [ + sonarcloud, + "organization", + ], + }, + project: { + propDefinition: [ + sonarcloud, + "project", + ({ organization }) => ({ + organization, + }), + ], + optional: true, + }, + }, + hooks: { + async activate() { + const { webhook } = await this.sonarcloud.createWebhook({ + params: { + name: this.name, + organization: this.organization, + project: this.project, + url: this.http.endpoint, + }, + }); + this.db.set("hookId", webhook.key); + }, + async deactivate() { + await this.sonarcloud.deleteWebhook({ + params: { + webhook: this.db.get("hookId"), + }, + }); + }, + }, + async run(event) { + this.http.respond({ + status: 200, + }); + + const { body } = event; + + this.$emit(body, { + id: body.revision, + summary: `New analisys completed with revision key: ${body.revision}`, + ts: Date.parse(body.changedAt), + }); + }, +}; diff --git a/components/sonix/README.md b/components/sonix/README.md new file mode 100644 index 0000000000000..75e626a52c26f --- /dev/null +++ b/components/sonix/README.md @@ -0,0 +1,11 @@ +# Overview + +The Sonix API enables automated transcription of audio and video files into text, offering functions like uploading media, managing files, and retrieving transcripts. Leveraging Pipedream’s capabilities, you can integrate the Sonix API with various services to streamline media processing workflows, making transcription tasks more efficient. By automating interactions with Sonix, you can trigger actions based on the transcription status, analyze content, and connect transcribed text with other apps for further processing or analysis. + +# Example Use Cases + +- **Transcription Triggered from Cloud Storage Upload**: When a new audio file is uploaded to cloud storage (e.g., Google Drive), Pipedream detects the event and triggers a workflow that uploads the file to Sonix for transcription. Once the transcript is ready, it can be saved back to Google Drive or sent via email. + +- **Media Management with Slack Notifications**: Use Pipedream to monitor your Sonix account for new transcripts. When a transcript is completed, send a notification with details and a download link to a designated Slack channel. This keeps the team updated on transcription progress in real-time without manual checks. + +- **Content Analysis and Keyword Extraction**: After a transcript is generated, a workflow can pass the text to a natural language processing (NLP) service, like the Google Cloud Language API, to extract keywords and analyze sentiment. The results could then be sent to a Google Sheet for tracking trends over time. diff --git a/components/sourceforge/README.md b/components/sourceforge/README.md new file mode 100644 index 0000000000000..f0f595e66aa07 --- /dev/null +++ b/components/sourceforge/README.md @@ -0,0 +1,11 @@ +# Overview + +The Sourceforge API provides programmable access to Sourceforge's repository hosting and collaborative features, allowing for automation of project management, file distribution, and reporting tasks. With this API on Pipedream, developers can create workflows to streamline software development processes, enhance collaboration, and integrate with other services for a more robust development lifecycle. + +# Example Use Cases + +- **Automated Release Distribution**: Set up a Pipedream workflow to detect when a new release is tagged in your Sourceforge project, then automatically distribute the release files to other platforms such as GitHub or BitBucket. + +- **Project Analytics Report**: Create a workflow that periodically fetches download statistics for your Sourceforge projects, and sends a formatted report to your team via email or Slack, so everyone stays informed on the project's traction without manually checking the site. + +- **Issue Tracking Integration**: Connect Sourceforge's issue tracker with a project management tool like Trello or Jira. When a new issue is submitted on Sourceforge, a corresponding card or ticket is automatically created in Trello or Jira, ensuring that no task goes unnoticed and your workflow remains seamless. diff --git a/components/spamcheck_ai/actions/delete-spam-report/delete-spam-report.mjs b/components/spamcheck_ai/actions/delete-spam-report/delete-spam-report.mjs new file mode 100644 index 0000000000000..4cfbf0f4440fb --- /dev/null +++ b/components/spamcheck_ai/actions/delete-spam-report/delete-spam-report.mjs @@ -0,0 +1,26 @@ +import app from "../../spamcheck_ai.app.mjs"; + +export default { + key: "spamcheck_ai-delete-spam-report", + name: "Delete Report", + description: "Delete a spam report. [See the documentation](https://app.spamcheck.ai/api_docs/index.html)", + version: "0.0.1", + type: "action", + props: { + app, + reportId: { + propDefinition: [ + app, + "reportId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.deleteReport({ + $, + id: this.reportId, + }); + $.export("$summary", `Successfully deleted the report with ID: '${this.reportId}'`); + return response; + }, +}; diff --git a/components/spamcheck_ai/actions/post-spam-check/post-spam-check.mjs b/components/spamcheck_ai/actions/post-spam-check/post-spam-check.mjs new file mode 100644 index 0000000000000..e5c9793761cce --- /dev/null +++ b/components/spamcheck_ai/actions/post-spam-check/post-spam-check.mjs @@ -0,0 +1,35 @@ +import app from "../../spamcheck_ai.app.mjs"; + +export default { + key: "spamcheck_ai-post-spam-check", + name: "Spam Check", + description: "Post a new spam check for an email or IP. [See the documentation](https://app.spamcheck.ai/api_docs/index.html)", + version: "0.0.1", + type: "action", + props: { + app, + ip: { + propDefinition: [ + app, + "ip", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + }, + async run({ $ }) { + const response = await this.app.spamCheck({ + $, + data: { + ip: this.ip, + email: this.email, + }, + }); + $.export("$summary", `Successfully posted spam check with ID '${response.id}'`); + return response; + }, +}; diff --git a/components/spamcheck_ai/actions/post-spam-report/post-spam-report.mjs b/components/spamcheck_ai/actions/post-spam-report/post-spam-report.mjs new file mode 100644 index 0000000000000..c7bfbc5f3c480 --- /dev/null +++ b/components/spamcheck_ai/actions/post-spam-report/post-spam-report.mjs @@ -0,0 +1,56 @@ +import app from "../../spamcheck_ai.app.mjs"; + +export default { + key: "spamcheck_ai-post-spam-report", + name: "Spam Report", + description: "Create a new spam report. [See the documentation](https://app.spamcheck.ai/api_docs/index.html)", + version: "0.0.1", + type: "action", + props: { + app, + ip: { + propDefinition: [ + app, + "ip", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + desiredOutcome: { + propDefinition: [ + app, + "desiredOutcome", + ], + }, + result: { + propDefinition: [ + app, + "result", + ], + }, + notes: { + propDefinition: [ + app, + "notes", + ], + }, + }, + async run({ $ }) { + const response = await this.app.spamReport({ + $, + data: { + ip: this.ip, + email: this.email, + desired_outcome: this.desiredOutcome, + result: this.result, + notes: this.notes, + }, + }); + $.export("$summary", "Successfully created a new spam report"); + return response; + }, +}; diff --git a/components/spamcheck_ai/package.json b/components/spamcheck_ai/package.json new file mode 100644 index 0000000000000..44f69e888093f --- /dev/null +++ b/components/spamcheck_ai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/spamcheck_ai", + "version": "0.1.0", + "description": "Pipedream SpamCheck.ai Components", + "main": "spamcheck_ai.app.mjs", + "keywords": [ + "pipedream", + "spamcheck_ai" + ], + "homepage": "https://pipedream.com/apps/spamcheck_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/spamcheck_ai/spamcheck_ai.app.mjs b/components/spamcheck_ai/spamcheck_ai.app.mjs new file mode 100644 index 0000000000000..ba2265ec5284e --- /dev/null +++ b/components/spamcheck_ai/spamcheck_ai.app.mjs @@ -0,0 +1,96 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "spamcheck_ai", + propDefinitions: { + reportId: { + type: "string", + label: "Report ID", + description: "ID of the Report", + async options() { + const reportsIds = await this.getSpamReports(); + return reportsIds.map(({ id }) => ({ + value: id, + })); + }, + }, + ip: { + type: "string", + label: "IP", + description: "IP to check for known spam activities", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "Email to check for known spam activities", + optional: true, + }, + desiredOutcome: { + type: "boolean", + label: "Desired Outcome", + description: "Desired outcome of the report", + }, + result: { + type: "boolean", + label: "Result", + description: "Result of the report", + }, + notes: { + type: "string", + label: "Notes", + description: "Notes of the report", + }, + }, + methods: { + _baseUrl() { + return "https://api.spamcheck.ai/api/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "Api-Key": `${this.$auth.api_key}`, + }, + }); + }, + async spamCheck(args = {}) { + return this._makeRequest({ + method: "post", + path: "/spam/check", + ...args, + }); + }, + async spamReport(args = {}) { + return this._makeRequest({ + method: "post", + path: "/spam_reports", + ...args, + }); + }, + async deleteReport({ + id, ...args + }) { + return this._makeRequest({ + method: "delete", + path: `/spam_reports/${id}`, + ...args, + }); + }, + async getSpamReports(args = {}) { + return this._makeRequest({ + path: "/spam_reports", + ...args, + }); + }, + }, +}; diff --git a/components/sparkpost/README.md b/components/sparkpost/README.md new file mode 100644 index 0000000000000..3dca606e827c3 --- /dev/null +++ b/components/sparkpost/README.md @@ -0,0 +1,11 @@ +# Overview + +SparkPost API allows you to send and track emails with precision and scale. Using this API on Pipedream, you can automate email operations, integrate with other services, and analyze email performance. With Pipedream’s serverless platform, you can create workflows that trigger on specific events, process data, and perform actions in response to the insights collected from SparkPost or other apps. + +# Example Use Cases + +- **Transactional Email Workflow**: Automatically send transactional emails through SparkPost when an event occurs in your app, like a new user sign-up or a purchase confirmation. Use Pipedream's built-in HTTP requests to listen for these events and trigger the SparkPost API to dispatch the emails. + +- **Email Performance Dashboard**: Collect and visualize email metrics from SparkPost in real-time. Set up a workflow that fetches delivery, open, and click statistics from SparkPost, then sends this data to Google Sheets or a BI tool like Tableau, allowing you to monitor and analyze your email performance dynamically. + +- **Multi-Channel Notification System**: Create a multi-channel alert system that sends notifications via email through SparkPost and other channels like SMS or Slack when a specific trigger is hit. For instance, if your app detects a system failure or a critical event, Pipedream can orchestrate notifications across various platforms to ensure rapid response. diff --git a/components/sparkpost/package.json b/components/sparkpost/package.json index 4af28f66f4d77..748a080ae500c 100644 --- a/components/sparkpost/package.json +++ b/components/sparkpost/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/specific/actions/create-conversation/create-conversation.mjs b/components/specific/actions/create-conversation/create-conversation.mjs new file mode 100644 index 0000000000000..152ca87f35439 --- /dev/null +++ b/components/specific/actions/create-conversation/create-conversation.mjs @@ -0,0 +1,167 @@ +import specific from "../../specific.app.mjs"; + +export default { + key: "specific-create-conversation", + name: "Create Conversation", + description: "Create a new conversation. [See the documentation](https://public-api.specific.app/docs/mutations/createConversation)", + version: "0.0.1", + type: "action", + props: { + specific, + content: { + type: "string", + label: "Content", + description: "Conversation content as String or ProseMirror document.", + reloadProps: true, + }, + insertedAt: { + propDefinition: [ + specific, + "insertedAt", + ], + optional: true, + }, + assignee: { + type: "string", + label: "Assignee", + description: "The user's email.", + optional: true, + }, + sourceId: { + propDefinition: [ + specific, + "sourceId", + ], + optional: true, + }, + companyId: { + propDefinition: [ + specific, + "companyId", + ], + optional: true, + }, + contactId: { + propDefinition: [ + specific, + "contactId", + ], + optional: true, + }, + sourceUrl: { + type: "string", + label: "Source URL", + description: "Source url where the conversation was gathered.", + optional: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.content) { + const { data: { customFields } } = await this.specific.query({ + model: "customFields", + where: "{type: {equals: conversation }}", + fields: "name", + }); + for (const { name } of customFields) { + props[`customField-${name}`] = { + type: "string", + label: name, + description: `Custom Field: ${name}`, + optional: true, + }; + } + } + return props; + }, + async run({ $ }) { + const { + specific, + ...data + } = this; + + const customFields = this.specific.parseCustomFields(data); + + const response = await specific.mutation({ + $, + model: "createConversation", + data: `{ + ${this.assignee + ? `assignee: { + connectOrIgnore: { + email: "${this.assignee}" + } + }` + : ""} + ${this.companyId + ? `company: { + connect: { + id: "${this.companyId}" + } + }` + : ""} + ${this.contactId + ? `contact: { + connect: { + id: "${this.contactId}" + } + }` + : ""} + content: "${this.content}" + ${customFields + ? `customFields: ${customFields}` + : ""} + ${this.insertedAt + ? `insertedAt: "${this.insertedAt}"` + : ""} + ${this.sourceId + ? `source: { + connect: { + id: "${this.sourceId}" + } + }` + : ""} + ${this.sourceUrl + ? `sourceUrl: "${this.sourceUrl}"` + : ""} + }`, + fields: ` + customFields + id + insertedAt + name + plainText + sourceUrl + assignee { + email + fullName + id + } + company { + contactsCount + customFields + id + name + visitorId + } + contact { + customFields + email + id + name + visitorId + } + source { + id + name + }`, + on: "Conversation", + }); + + if (response.errors) throw new Error(response.errors[0].message); + + $.export("$summary", `Successfully created conversation for user ID: ${response.data?.createConversation?.id}`); + return response; + }, +}; + diff --git a/components/specific/actions/find-company/find-company.mjs b/components/specific/actions/find-company/find-company.mjs new file mode 100644 index 0000000000000..0aacbc19bce04 --- /dev/null +++ b/components/specific/actions/find-company/find-company.mjs @@ -0,0 +1,34 @@ +import specific from "../../specific.app.mjs"; + +export default { + key: "specific-find-company", + name: "Find Company", + description: "Retrieve details of a specified company. [See the documentation](https://public-api.specific.app/docs/types/Query)", + version: "0.0.1", + type: "action", + props: { + specific, + companyId: { + propDefinition: [ + specific, + "companyId", + ], + }, + }, + async run({ $ }) { + const response = await this.specific.query({ + $, + model: "companies", + where: `{id: {equals: "${this.companyId}"}}`, + fields: `id + name + customFields + visitorId + contactsCount`, + on: "Company", + }); + + $.export("$summary", `Successfully retrieved details for company ID: ${this.companyId}`); + return response; + }, +}; diff --git a/components/specific/actions/update-create-contact/update-create-contact.mjs b/components/specific/actions/update-create-contact/update-create-contact.mjs new file mode 100644 index 0000000000000..78bdce87db262 --- /dev/null +++ b/components/specific/actions/update-create-contact/update-create-contact.mjs @@ -0,0 +1,111 @@ +import specific from "../../specific.app.mjs"; + +export default { + key: "specific-update-create-contact", + name: "Update or Create Contact", + description: "Modify an existing contact's details or create a new one if the specified contact does not exist. [See the documentation](https://public-api.specific.app/docs/types/contact)", + version: "0.0.1", + type: "action", + props: { + specific, + contactEmail: { + propDefinition: [ + specific, + "contactEmail", + ], + reloadProps: true, + }, + companyId: { + propDefinition: [ + specific, + "companyId", + ], + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "Contact's name.", + optional: true, + }, + email: { + type: "string", + label: "New Email", + description: "New email to update", + optional: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.content) { + const { data: { customFields } } = await this.specific.query({ + model: "customFields", + where: "{type: {equals: contact }}", + fields: "name", + }); + for (const { name } of customFields) { + props[`customField-${name}`] = { + type: "string", + label: name, + description: `Custom Field: ${name}`, + optional: true, + }; + } + } + return props; + }, + async run({ $ }) { + const { + specific, + ...data + } = this; + + const customFields = this.specific.parseCustomFields(data); + + const response = await specific.mutation({ + $, + model: "createOrUpdateContact", + on: "CreatedOrUpdatedContacts", + data: `{ + ${this.companyId + ? `company: { + connect: { + id: "${this.companyId}" + } + }` + : ""} + ${customFields + ? `customFields: ${customFields}` + : ""} + ${this.email + ? `email: "${this.email}"` + : ""} + ${this.name + ? `name: "${this.name}"` + : ""} + }`, + where: `{email: "${this.contactEmail}"}`, + fields: ` + contacts { + id + name + email + visitorId + customFields + company { + contactsCount + customFields + id + name + visitorId + } + }`, + }); + + if (response.errors) throw new Error(response.errors[0].message); + + $.export("$summary", `Successfully updated or created contact with email ${this.contactEmail}`); + return response; + }, +}; + diff --git a/components/specific/common/utils.mjs b/components/specific/common/utils.mjs new file mode 100644 index 0000000000000..d0af679ba89cc --- /dev/null +++ b/components/specific/common/utils.mjs @@ -0,0 +1,8 @@ +export const stringifyObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return JSON.stringify(obj); + } + return obj; +}; diff --git a/components/specific/package.json b/components/specific/package.json new file mode 100644 index 0000000000000..d5a4503190f70 --- /dev/null +++ b/components/specific/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/specific", + "version": "0.1.0", + "description": "Pipedream Specific Components", + "main": "specific.app.mjs", + "keywords": [ + "pipedream", + "specific" + ], + "homepage": "https://pipedream.com/apps/specific", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/specific/sources/common/base.mjs b/components/specific/sources/common/base.mjs new file mode 100644 index 0000000000000..a50e5e820c6e6 --- /dev/null +++ b/components/specific/sources/common/base.mjs @@ -0,0 +1,82 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { stringifyObject } from "../../common/utils.mjs"; +import specific from "../../specific.app.mjs"; + +export default { + props: { + specific, + http: { + type: "$.interface.http", + customResponse: false, + }, + db: "$.service.db", + code: { + type: "string", + label: "Code", + description: "Webhook's secret code", + secret: true, + }, + sourceId: { + propDefinition: [ + specific, + "sourceId", + ], + type: "string[]", + optional: true, + }, + }, + hooks: { + async activate() { + const { data } = await this.specific.mutation({ + model: "subscribeWebhook", + data: `{ + code: "${this.code}" + operation: ["${this.getOperation()}"] + ${this.sourceId?.length + ? `sources: ${stringifyObject(this.sourceId)}` + : ""} + url: "${this.http.endpoint}" + }`, + fields: ` + inactive + inactiveReason + operation + url + `, + on: "Webhook", + onValidationError: true, + }); + + if (data?.subscribeWebhook?.fieldErrors?.length) { + throw new ConfigurationError(data.subscribeWebhook?.fieldErrors[0].message); + } + + }, + async deactivate() { + await this.specific.mutation({ + model: "unsubscribeWebhook", + where: `{url: "${this.http.endpoint}"}`, + fields: ` + inactive + inactiveReason + operation + url + `, + on: "Webhook", + }); + + }, + }, + async run({ + headers, body, + }) { + if (headers["x-code"] != this.code) return; + + const ts = Date.parse(body.insertedAt || new Date()); + this.$emit(body, { + id: `${body.id || body.workspaceId}-${ts}`, + summary: this.getSummary(body), + ts: ts, + }); + }, +}; diff --git a/components/specific/sources/new-contact-instant/new-contact-instant.mjs b/components/specific/sources/new-contact-instant/new-contact-instant.mjs new file mode 100644 index 0000000000000..1bfe57c155ca5 --- /dev/null +++ b/components/specific/sources/new-contact-instant/new-contact-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "specific-new-contact-instant", + name: "New Contact Created (Instant)", + description: "Emit new event whenever a new contact is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getOperation() { + return "new-contact"; + }, + getSummary(body) { + return `New contact created: ${body.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/specific/sources/new-contact-instant/test-event.mjs b/components/specific/sources/new-contact-instant/test-event.mjs new file mode 100644 index 0000000000000..d2d4989e32463 --- /dev/null +++ b/components/specific/sources/new-contact-instant/test-event.mjs @@ -0,0 +1,7 @@ +export default { + "id": "specific_cnC1e5fe56v53o", + "email": "user@specific.app", + "name": "User Name", + "custom_fields": {}, + "workspace_id": "12345678-134-134-1234-1234567890" +} \ No newline at end of file diff --git a/components/specific/sources/new-conversation-instant/new-conversation-instant.mjs b/components/specific/sources/new-conversation-instant/new-conversation-instant.mjs new file mode 100644 index 0000000000000..2b002a1816557 --- /dev/null +++ b/components/specific/sources/new-conversation-instant/new-conversation-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "specific-new-conversation-instant", + name: "New Conversation Instant", + description: "Emit new event whenever a new conversation is initiated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getOperation() { + return "new-conversation"; + }, + getSummary(body) { + return `New conversation initiated: ${body.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/specific/sources/new-conversation-instant/test-event.mjs b/components/specific/sources/new-conversation-instant/test-event.mjs new file mode 100644 index 0000000000000..4569ed2d32bde --- /dev/null +++ b/components/specific/sources/new-conversation-instant/test-event.mjs @@ -0,0 +1,24 @@ +export default { + "text": "Conversation Text\n", + "name": "Conversation Name", + "customFields": { + "callId": "1234", + "release": "12" + }, + "assignee": null, + "company": { + "id": "specific_cnC1e5fe56v53o", + "name": "Secret Intelligence Service", + "customFields": {} + }, + "contact": { + "id": "specific_cnC1e5fe56v53o", + "name": "James Bond", + "email": "james@cia.gov", + "customFields": {} + }, + "source": null, + "insertedAt": "2024-07-19T20:28:17", + "sourceUrl": "htpps://urltest.com", + "workspaceId": "12345678-134-134-1234-1234567890" +} \ No newline at end of file diff --git a/components/specific/specific.app.mjs b/components/specific/specific.app.mjs new file mode 100644 index 0000000000000..975bef2ac8c9a --- /dev/null +++ b/components/specific/specific.app.mjs @@ -0,0 +1,166 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "specific", + propDefinitions: { + customFields: { + type: "string", + label: "customFields", + description: "customFields", + }, + insertedAt: { + type: "string", + label: "Inserted At", + description: "Date and time when the conversation was inserted in format YYYY-MM-DDTHH:mm:ss.sssZ", + default: (new Date).toISOString(), + }, + sourceId: { + type: "string", + label: "Source Id", + description: "The Id of the source associated with the conversation", + async options() { + return await this.listAsyncOptions({ + model: "sources", + }); + }, + }, + contactId: { + type: "string", + label: "Contact Id", + description: "The id of the contact associated with the conversation", + async options() { + return await this.listAsyncOptions({ + model: "contacts", + }); + }, + }, + contactEmail: { + type: "string", + label: "Contact Email", + description: "The email of the contact", + async options() { + return await this.listAsyncOptions({ + model: "contacts", + fields: ` + name + email`, + }); + }, + }, + companyId: { + type: "string", + label: "Company ID", + description: "The ID of the company to retrieve details for", + async options() { + return await this.listAsyncOptions({ + model: "companies", + }); + }, + }, + }, + methods: { + _baseUrl() { + return "https://public-api.specific.app/graphql"; + }, + _headers() { + return { + Authorization: `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, ...opts + }) { + return axios($, { + method: "POST", + url: this._baseUrl(), + headers: this._headers(), + ...opts, + }); + }, + query({ + model, where, fields, + }) { + return this._makeRequest({ + data: { + query: `query { + ${model} ${where + ? `(where: ${where})` + : ""} { + ${fields} + } + }`, + }, + }); + }, + mutation({ + model, data = null, fields, on, where, onValidationError = false, + }) { + return this._makeRequest({ + data: { + query: `mutation { + ${model} ( + ${data + ? `data: ${data}` + : ""} ${where + ? `where: ${where}` + : ""}) { + ...on ${on || model} { + ${fields} + } + ...on DbError { + message + } + ...on GraphQLError { + message + } + ${onValidationError + ? ` + ...on ValidationError { + message + fieldErrors { + message + path + } + }` + : ""} + } + }`, + }, + }); + }, + async listAsyncOptions ({ + model, fields = ` + id + name`, + }) { + const { data } = await this.query({ + model, + fields, + }); + + return data + ? data[model]?.map(({ + id, email, name: label, + }) => ({ + label, + value: id || email, + })) + : []; + }, + parseCustomFields(data) { + let customFields = Object.keys(data) + .filter((key) => key.startsWith("customField-")) + .reduce((res, key) => { + res[key.substring(12)] = data[key]; + return res; + }, {}); + + return JSON.stringify(customFields) + .replace(/"([^"]+)":/g, ` + $1:`) + .replace(/,/g, ` + `); + }, + }, +}; diff --git a/components/speechace/README.md b/components/speechace/README.md new file mode 100644 index 0000000000000..e153b703fdeb0 --- /dev/null +++ b/components/speechace/README.md @@ -0,0 +1,11 @@ +# Overview + +The Speechace API offers capabilities for detailed speech analysis, particularly useful for language learning applications. With this API, you can assess users' pronunciation and fluency in English by analyzing audio inputs. In Pipedream, you can leverage this API to craft workflows that trigger on various events, process audio data, and perform actions based on the analysis—such as storing results, providing feedback, or integrating with other services for enhanced functionality. + +# Example Use Cases + +- **Pronunciation Feedback System**: When a user submits an audio recording through a form or chatbot, trigger a Pipedream workflow to send the audio to the Speechace API. Once processed, you can automatically send personalized pronunciation feedback to the user via email or messaging app integration like Slack. + +- **Language Learning Dashboard**: Compile and store pronunciation scores and fluency metrics in a Pipedream data store or an external database like Google Sheets whenever users complete speaking exercises. Use this aggregated data to create a dashboard that tracks progress and highlights areas for improvement. + +- **Automated Language Assessment**: Automate the process of language proficiency assessment by triggering a workflow that sends candidates' speech samples to the Speechace API during an online test. The results can then be used to grade the pronunciation part of the test, which can be sent to an LMS like Moodle or Canvas. diff --git a/components/speechace/actions/score-scripted-recording/score-scripted-recording.mjs b/components/speechace/actions/score-scripted-recording/score-scripted-recording.mjs new file mode 100644 index 0000000000000..331855762a23c --- /dev/null +++ b/components/speechace/actions/score-scripted-recording/score-scripted-recording.mjs @@ -0,0 +1,68 @@ +import speechace from "../../speechace.app.mjs"; +import FormData from "form-data"; +import fs from "fs"; + +export default { + key: "speechace-score-scripted-recording", + name: "Score Scripted Recording", + description: "Scores a scripted recording based on fluency and pronunciation. [See the documentation](https://docs.speechace.com/#c34b11dd-8172-441a-bc27-223339d48d8e)", + version: "0.0.1", + type: "action", + props: { + speechace, + filePath: { + propDefinition: [ + speechace, + "filePath", + ], + }, + text: { + type: "string", + label: "Text", + description: "A word, phrase, or sentence to score", + }, + questionInfo: { + type: "string", + label: "Question Info", + description: "A unique identifier for the activity or question this user audio is answering", + optional: true, + }, + dialect: { + propDefinition: [ + speechace, + "dialect", + ], + }, + userId: { + propDefinition: [ + speechace, + "userId", + ], + }, + }, + async run({ $ }) { + const data = new FormData(); + const content = fs.createReadStream(this.filePath.includes("tmp/") + ? this.filePath + : `/tmp/${this.filePath}`); + data.append("user_audio_file", content); + if (this.text) { + data.append("text", this.text); + } + if (this.questionInfo) { + data.append("question_info", this.questionInfo); + } + + const response = await this.speechace.scoreScript({ + $, + params: { + dialect: this.dialect, + user_id: this.userId, + }, + data, + headers: data.getHeaders(), + }); + $.export("$summary", `Scored scripted recording for audio file: ${this.filePath}`); + return response; + }, +}; diff --git a/components/speechace/actions/transcribe-and-score-recording/transcribe-and-score-recording.mjs b/components/speechace/actions/transcribe-and-score-recording/transcribe-and-score-recording.mjs new file mode 100644 index 0000000000000..c78b2e888e2b0 --- /dev/null +++ b/components/speechace/actions/transcribe-and-score-recording/transcribe-and-score-recording.mjs @@ -0,0 +1,60 @@ +import speechace from "../../speechace.app.mjs"; +import FormData from "form-data"; +import fs from "fs"; + +export default { + key: "speechace-transcribe-and-score-recording", + name: "Transcribe and Score Recording", + description: "Transcribes and scores a provided speech recording. [See the documentation](https://docs.speechace.com/#76089b5d-7e25-4744-8d32-f6c230acf217)", + version: "0.0.1", + type: "action", + props: { + speechace, + filePath: { + propDefinition: [ + speechace, + "filePath", + ], + }, + relevanceContext: { + type: "string", + label: "Relevance Context", + description: "Question Prompt text provided to the user. When this parameter is passed, the relevance of the user audio transcript is evaluated given the relevance_context and a resulting relevance class is returned in .speech_score.relevance.class", + optional: true, + }, + dialect: { + propDefinition: [ + speechace, + "dialect", + ], + }, + userId: { + propDefinition: [ + speechace, + "userId", + ], + }, + }, + async run({ $ }) { + const data = new FormData(); + const content = fs.createReadStream(this.filePath.includes("tmp/") + ? this.filePath + : `/tmp/${this.filePath}`); + data.append("user_audio_file", content); + if (this.relevanceContext) { + data.append("relevance_context", this.relevanceContext); + } + + const response = await this.speechace.transcribeAndScore({ + $, + params: { + dialect: this.dialect, + user_id: this.userId, + }, + data, + headers: data.getHeaders(), + }); + $.export("$summary", `Transcription and scoring completed for audio file: ${this.filePath}`); + return response; + }, +}; diff --git a/components/speechace/package.json b/components/speechace/package.json new file mode 100644 index 0000000000000..8d4c0088e1bfa --- /dev/null +++ b/components/speechace/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/speechace", + "version": "0.1.0", + "description": "Pipedream Speechace Components", + "main": "speechace.app.mjs", + "keywords": [ + "pipedream", + "speechace" + ], + "homepage": "https://pipedream.com/apps/speechace", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0", + "form-data": "^4.0.0" + } +} diff --git a/components/speechace/speechace.app.mjs b/components/speechace/speechace.app.mjs new file mode 100644 index 0000000000000..710f6afd22ae8 --- /dev/null +++ b/components/speechace/speechace.app.mjs @@ -0,0 +1,73 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "speechace", + propDefinitions: { + dialect: { + type: "string", + label: "Dialect", + description: "The dialect to use for scoring", + options: [ + { + label: "US English", + value: "en-us", + }, + { + label: "UK English", + value: "en-gb", + }, + ], + optional: true, + default: "en-us", + }, + filePath: { + type: "string", + label: "File Path", + description: "The path to an audio file (wav, mp3, m4a, webm, ogg, aiff) in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + }, + userId: { + type: "string", + label: "User ID", + description: "A unique anonymized identifier for the end-user who spoke the audio", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.speechace.co/api"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + method = "POST", + path, + headers, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + method, + url: `${this._baseUrl()}${path}`, + headers, + params: { + ...params, + key: `${this.$auth.product_key}`, + }, + }); + }, + transcribeAndScore(opts = {}) { + return this._makeRequest({ + path: "/scoring/speech/v9/json", + ...opts, + }); + }, + scoreScript(opts = {}) { + return this._makeRequest({ + path: "/scoring/text/v9/json", + ...opts, + }); + }, + }, +}; diff --git a/components/spider/actions/scrape-new-page/scrape-new-page.mjs b/components/spider/actions/scrape-new-page/scrape-new-page.mjs new file mode 100644 index 0000000000000..8aa9325d9c445 --- /dev/null +++ b/components/spider/actions/scrape-new-page/scrape-new-page.mjs @@ -0,0 +1,46 @@ +import spider from "../../spider.app.mjs"; + +export default { + key: "spider-scrape-new-page", + name: "Scrape New Page", + description: "Initiates a new page scrape (crawl). [See the documentation](https://spider.cloud/docs/api#crawl-website)", + version: "0.0.1", + type: "action", + props: { + spider, + infoBox: { + type: "alert", + alertType: "info", + content: "See [the Spider documentation](https://spider.cloud/docs/api#crawl-website) for information on limits and best practices.", + }, + url: { + type: "string", + label: "URL", + description: "The URI resource to crawl, e.g. `https://spider.cloud`. This can be a comma split list for multiple urls.", + }, + limit: { + type: "integer", + label: "Limit", + description: "The maximum amount of pages allowed to crawl per website. Default is 0, which crawls all pages.", + optional: true, + }, + storeData: { + type: "boolean", + label: "Store Data", + description: "Decide whether to store data. Default is `false`.", + optional: true, + }, + }, + async run({ $ }) { + const content = await this.spider.initiateCrawl({ + $, + data: { + url: this.url, + limit: this.limit, + store_data: this.storeData, + }, + }); + $.export("$summary", `Successfully scraped URL ${this.url}`); + return content; + }, +}; diff --git a/components/spider/package.json b/components/spider/package.json new file mode 100644 index 0000000000000..6f467bb3f29fe --- /dev/null +++ b/components/spider/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/spider", + "version": "0.1.0", + "description": "Pipedream Spider Components", + "main": "spider.app.mjs", + "keywords": [ + "pipedream", + "spider" + ], + "homepage": "https://pipedream.com/apps/spider", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/spider/spider.app.mjs b/components/spider/spider.app.mjs new file mode 100644 index 0000000000000..115ffb6352914 --- /dev/null +++ b/components/spider/spider.app.mjs @@ -0,0 +1,32 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "spider", + propDefinitions: {}, + methods: { + _baseUrl() { + return "https://api.spider.cloud"; + }, + async _makeRequest({ + $ = this, path = "/", headers, ...otherOpts + } = {}) { + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "Authorization": `Bearer ${this.$auth.api_key}`, + "Content-Type": "application/json", + }, + }); + }, + async initiateCrawl(args) { + return this._makeRequest({ + method: "POST", + path: "/crawl", + ...args, + }); + }, + }, +}; diff --git a/components/spiritme/actions/generate-video/generate-video.mjs b/components/spiritme/actions/generate-video/generate-video.mjs new file mode 100644 index 0000000000000..877d30e35c9cc --- /dev/null +++ b/components/spiritme/actions/generate-video/generate-video.mjs @@ -0,0 +1,160 @@ +import spiritme from "../../spiritme.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "spiritme-generate-video", + name: "Generate Video", + description: "Generates a new video using specific voice and avatar props. [See the documentation](https://api.spiritme.tech/api/swagger/#/videos/videos_create)", + version: "0.0.1", + type: "action", + props: { + spiritme, + name: { + type: "string", + label: "Name", + description: "Name of the video", + }, + avatar: { + propDefinition: [ + spiritme, + "avatar", + ], + }, + voice: { + propDefinition: [ + spiritme, + "voice", + ], + }, + text: { + type: "string", + label: "Text", + description: "The text to use for the video. Example: `Hello everyone! I am a virtual avatar from Spiritme.` Use tags ` text ` to add emotions to the generated video. The list of supported emotions are `neutral`, `semismile`, `smile`, `happiness`, `sadness`, and `surprise`. Either text or audio file is required.", + optional: true, + }, + audioFile: { + propDefinition: [ + spiritme, + "file", + () => ({ + type: [ + "audio", + ], + }), + ], + label: "Audio File", + description: "Identifier of an audio file. Either text or audio file is required.", + }, + media: { + propDefinition: [ + spiritme, + "file", + () => ({ + type: [ + "image", + "video", + ], + }), + ], + label: "Media File", + description: "Identifier of an image or video file. One of avatar or media is required.", + }, + viewType: { + type: "string", + label: "View Type", + description: "Content as is or content in circle. Supported only for avatars.", + optional: true, + options: [ + "rectangular", + "circular", + ], + }, + autoEmotionsMarkup: { + type: "boolean", + label: "Auto Emotions Markup", + description: "Add emotions automatically by AI", + optional: true, + }, + waitForCompletion: { + type: "boolean", + label: "Wait For Completion", + description: "Set to `true` to poll the API in 3-second intervals until the video is completed", + optional: true, + }, + }, + async run({ $ }) { + const { + spiritme, + name, + avatar, + voice, + text, + audioFile, + media, + viewType, + autoEmotionsMarkup, + waitForCompletion, + } = this; + + if (!avatar && !media) { + throw new ConfigurationError("One of `Avatar` or `Media File` is required"); + } + + if (!text && !audioFile) { + throw new ConfigurationError("One of `Text` or `Audio File` is required"); + } + + let response = await spiritme.generateVideo({ + $, + data: { + name, + slides: [ + { + audio_source: { + text, + voice: voice + ? { + id: voice, + } + : undefined, + file: audioFile + ? { + id: audioFile, + } + : undefined, + }, + layers: [ + { + avatar: avatar + ? { + id: avatar, + } + : undefined, + media: media + ? { + id: media, + } + : undefined, + view_type: viewType, + }, + ], + }, + ], + auto_emotions_markup: autoEmotionsMarkup, + }, + }); + + if (waitForCompletion) { + const timer = (ms) => new Promise((res) => setTimeout(res, ms)); + while (response.status !== "success") { + response = await spiritme.getVideo({ + videoId: response.id, + }); + await timer(3000); + } + } + + $.export("$summary", `Generated video with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/spiritme/package.json b/components/spiritme/package.json new file mode 100644 index 0000000000000..bd5ab8d1f5885 --- /dev/null +++ b/components/spiritme/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/spiritme", + "version": "0.1.0", + "description": "Pipedream SpiritMe Components", + "main": "spiritme.app.mjs", + "keywords": [ + "pipedream", + "spiritme" + ], + "homepage": "https://pipedream.com/apps/spiritme", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/spiritme/sources/new-avatar-video-completion/new-avatar-video-completion.mjs b/components/spiritme/sources/new-avatar-video-completion/new-avatar-video-completion.mjs new file mode 100644 index 0000000000000..324f3c55da3cb --- /dev/null +++ b/components/spiritme/sources/new-avatar-video-completion/new-avatar-video-completion.mjs @@ -0,0 +1,85 @@ +import spiritme from "../../spiritme.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "spiritme-new-avatar-video-completion", + name: "New Avatar Video Completion", + description: "Emit new event when an avatar video completes rendering.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + spiritme, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + emitEvent(video) { + const meta = this.generateMeta(video); + this.$emit(video, meta); + }, + generateMeta(video) { + return { + id: video.id, + summary: `New Video: ${video.name}`, + ts: Date.parse(video.gd), + }; + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + const params = { + limit: 100, + offset: 0, + status: [ + "success", + ], + }; + let total; + + const videos = []; + do { + const { results } = await this.spiritme.listVideos({ + params, + }); + for (const video of results) { + const ts = Date.parse(video.gd); + if (ts >= lastTs && (!max || videos.length < max)) { + videos.push(video); + } else { + break; + } + } + params.offset += params.limit; + total = results?.length; + } while (total === params.limit && (!max || videos.length < max)); + + if (!videos.length) { + return; + } + + this._setLastTs(Date.parse(videos[0].gd)); + videos.reverse().forEach((video) => this.emitEvent(video)); + }, + }, + async run() { + await this.processEvent(); + }, + sampleEmit, +}; diff --git a/components/spiritme/sources/new-avatar-video-completion/test-event.mjs b/components/spiritme/sources/new-avatar-video-completion/test-event.mjs new file mode 100644 index 0000000000000..9ffb34ea90bd9 --- /dev/null +++ b/components/spiritme/sources/new-avatar-video-completion/test-event.mjs @@ -0,0 +1,51 @@ +export default { + "id": 85987, + "cd": "2024-06-28T21:32:14.878982Z", + "gd": "2024-06-28T21:34:37.340485Z", + "name": "Video", + "slides": [ + { + "id": 1, + "audio_source": { + "text": "Pipedream is the best!", + "voice": { + "id": 18, + "name": "Kimberly", + "label": "English Kimberly", + "provider": "amazon", + "sex": "female" + }, + "file": null + }, + "layers": [ + { + "id": 1, + "avatar": { + "id": 9889, + "name": "Elizabeth", + "preview": "https://cdn.spiritme.tech/media/avatars/previews/ec724f6e58a22d3ec9dd81fe825f3eb1ea2a99b6.png", + "frame_width": 1073, + "frame_height": 1177 + }, + "media": null, + "x": 0.5, + "y": 1, + "scale": 1, + "crop": null, + "view_type": "rectangular" + } + ], + "background": null + } + ], + "webhook": null, + "resolution": { + "width": 1920, + "height": 1080 + }, + "auto_emotions_markup": true, + "enable_subtitles": false, + "status": "success", + "result": "https://cdn.spiritme.tech/media/videos/3fa94ae367237676442cde44feacaf5c24875966.mp4", + "thumbnail": "https://cdn.spiritme.tech/media/videos/65dd50bc2b996b3457fcb4d98ce5c270bed54a6d.jpeg" +} \ No newline at end of file diff --git a/components/spiritme/spiritme.app.mjs b/components/spiritme/spiritme.app.mjs new file mode 100644 index 0000000000000..805293810722a --- /dev/null +++ b/components/spiritme/spiritme.app.mjs @@ -0,0 +1,131 @@ +import { axios } from "@pipedream/platform"; +const DEFAULT_LIMIT = 50; + +export default { + type: "app", + app: "spiritme", + propDefinitions: { + avatar: { + type: "string", + label: "Avatar", + description: "The identifier for the avatar to be used. One of avatar or media is required.", + optional: true, + async options({ page }) { + const { results } = await this.listAvatars({ + params: { + limit: DEFAULT_LIMIT, + offset: page * DEFAULT_LIMIT, + }, + }); + return results?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + voice: { + type: "string", + label: "Voice", + description: "The identifier of the voice to be used", + optional: true, + async options({ page }) { + const { results } = await this.listVoices({ + params: { + limit: DEFAULT_LIMIT, + offset: page * DEFAULT_LIMIT, + }, + }); + return results?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + file: { + type: "string", + label: "Voice", + description: "The identifier of the file to be used", + optional: true, + async options({ + page, type, + }) { + const { results } = await this.listFiles({ + params: { + limit: DEFAULT_LIMIT, + offset: page * DEFAULT_LIMIT, + type, + }, + }); + return results?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.spiritme.tech/api"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Token ${this.$auth.api_key}`, + Accept: "application/json", + }, + }); + }, + listAvatars(opts = {}) { + return this._makeRequest({ + path: "/avatars/", + ...opts, + }); + }, + listVoices(opts = {}) { + return this._makeRequest({ + path: "/tts/voices/", + ...opts, + }); + }, + listFiles(opts = {}) { + return this._makeRequest({ + path: "/files/", + ...opts, + }); + }, + listVideos(opts = {}) { + return this._makeRequest({ + path: "/videos/", + ...opts, + }); + }, + getVideo({ + videoId, ...opts + }) { + return this._makeRequest({ + path: `/videos/${videoId}/`, + ...opts, + }); + }, + generateVideo(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/videos/", + ...opts, + }); + }, + }, +}; diff --git a/components/splynx/README.md b/components/splynx/README.md new file mode 100644 index 0000000000000..dab74016a67ae --- /dev/null +++ b/components/splynx/README.md @@ -0,0 +1,11 @@ +# Overview + +The Splynx API provides a gateway to manage and automate Internet Service Provider (ISP) tasks such as billing, networking, and customer management. By tapping into this API via Pipedream, you can create robust integrations and workflows that streamline operations, enhance customer service, and drive data-driven decisions. Pipedream's platform enables the connection of Splynx's features with hundreds of other apps without the need for extensive coding, facilitating task automation and data synchronization in real-time. + +# Example Use Cases + +- **Automated Billing Notifications**: Send automated billing reminders or payment confirmations by linking Splynx with communication platforms like Twilio or SendGrid. When Splynx triggers a billing event, a Pipedream workflow can capture this and send out an SMS or email to the customer. + +- **Customer Support Ticketing Integration**: Improve response times and customer service by connecting Splynx to a support ticketing system like Zendesk. When a new support request is created in Splynx, Pipedream can create a corresponding ticket in Zendesk, ensuring that all customer issues are tracked and resolved efficiently. + +- **Network Status Updates on Slack**: Keep your team informed of network incidents by using Pipedream to integrate Splynx with Slack. Set up a workflow that monitors Splynx for network alerts and automatically posts messages to a designated Slack channel, facilitating quick response and team collaboration. diff --git a/components/splynx/actions/common/common-create-update.mjs b/components/splynx/actions/common/common-create-update.mjs new file mode 100644 index 0000000000000..7439a701ccde6 --- /dev/null +++ b/components/splynx/actions/common/common-create-update.mjs @@ -0,0 +1,124 @@ +import splynx from "../../splynx.app.mjs"; + +export default { + props: { + splynx, + name: { + type: "string", + label: "Name", + description: "The full name of the customer", + }, + partnerId: { + type: "integer", + label: "Partner ID", + description: + "Partner id. You can get it at page \"Administration / Partners\"", + }, + locationId: { + type: "integer", + label: "Location ID", + description: + "Location id. You can get it at page \"Administration / Locations\"", + }, + category: { + type: "string", + label: "Category", + description: "Category of the customer", + options: [ + { + label: "Private person", + value: "person", + }, + { + label: "Company", + value: "company", + }, + ], + }, + login: { + type: "string", + label: "Login", + description: "The login of the user", + optional: true, + }, + status: { + type: "string", + label: "Status", + description: "The status of the customer", + optional: true, + options: [ + "new", + "active", + "blocked", + "disabled", + ], + default: "new", + }, + email: { + type: "string", + label: "Email", + description: "The email of the customer", + optional: true, + }, + billingEmail: { + type: "string", + label: "Billing Email", + description: "The billing email of the customer", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The phone number of the customer, e.g. `555-0134`", + optional: true, + }, + street: { + type: "string", + label: "Street", + description: "The street of the customer", + optional: true, + }, + zipCode: { + type: "string", + label: "Zip Code", + description: "The zip code of the customer", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "The city of the customer", + optional: true, + }, + dateAdd: { + type: "string", + label: "Date Added", + description: "The date the customer was added (defaults to current date)", + optional: true, + }, + }, + methods: { + getData() { + const { // eslint-disable-next-line no-unused-vars + splynx, + partnerId, + locationId, + billingEmail, + street, + zipCode, + dateAdd, + ...data + } = this; + + return { + ...data, + partner_id: partnerId, + location_id: locationId, + billing_email: billingEmail, + street_1: street, + zip_code: zipCode, + date_add: dateAdd, + }; + }, + }, +}; diff --git a/components/splynx/actions/create-customer/create-customer.mjs b/components/splynx/actions/create-customer/create-customer.mjs new file mode 100644 index 0000000000000..c1f5b47e51265 --- /dev/null +++ b/components/splynx/actions/create-customer/create-customer.mjs @@ -0,0 +1,22 @@ +import common from "../common/common-create-update.mjs"; + +export default { + ...common, + key: "splynx-create-customer", + name: "Create Customer", + description: + "Creates a new customer with the provided details. [See the documentation](https://splynx.docs.apiary.io/#reference/customers/customers-collection/create-a-customer)", + version: "0.0.1", + type: "action", + async run({ $ }) { + const response = await this.splynx.createCustomer({ + $, + data: this.getData(), + }); + $.export( + "$summary", + `Successfully created customer "${this.name}" (ID: ${response.id})`, + ); + return response; + }, +}; diff --git a/components/splynx/actions/create-internet-service/create-internet-service.mjs b/components/splynx/actions/create-internet-service/create-internet-service.mjs new file mode 100644 index 0000000000000..d661e820319bc --- /dev/null +++ b/components/splynx/actions/create-internet-service/create-internet-service.mjs @@ -0,0 +1,105 @@ +import splynx from "../../splynx.app.mjs"; + +export default { + key: "splynx-create-internet-service", + name: "Create Internet Service", + description: "Creates a new internet service with specified details. [See the documentation](https://splynx.docs.apiary.io/#reference/services/internet-services-collection/create-service)", + version: "0.0.1", + type: "action", + props: { + splynx, + customerId: { + propDefinition: [ + splynx, + "customerId", + ], + }, + tariffId: { + propDefinition: [ + splynx, + "tariffId", + ], + }, + status: { + type: "string", + label: "Status", + description: "Status of the service", + options: [ + "active", + "disabled", + "hidden", + "pending", + ], + }, + description: { + type: "string", + label: "Description", + description: "Description of the service", + }, + quantity: { + type: "integer", + label: "Quantity", + description: "Quantity", + }, + startDate: { + type: "string", + label: "Start Date", + description: "Start date. Format: `YYYY-MM-DD`", + }, + endDate: { + type: "string", + label: "End Date", + description: "End date. Format: `YYYY-MM-DD`", + }, + login: { + type: "string", + label: "Login", + description: "Service login. Recommended to use customer login as prefix", + }, + takingIpv4: { + type: "integer", + label: "Taking IPv4", + description: "Taking IPv4", + options: [ + { + label: "None (Router will assign IP)", + value: 0, + }, + { + label: "Permanent IP (from Static IPs)", + value: 1, + }, + { + label: "Dynamic IP (from IP Pools)", + value: 2, + }, + ], + }, + additionalOptions: { + type: "object", + label: "Additional Options", + description: "Additional parameters to be passed in this request. [See the documentation](https://splynx.docs.apiary.io/#reference/services/internet-services-collection/create-service) for available parameters", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.splynx.createInternetService({ + $, + customerId: this.customerId, + data: { + tariff_id: this.tariffId, + status: this.status, + description: this.description, + quantity: this.quantity, + start_date: this.startDate, + end_date: this.endDate, + login: this.login, + taking_ipv4: this.takingIpv4, + ...this.additionalOptions, + }, + }); + + $.export("$summary", `Successfully created a new internet service (ID: ${response.id})`); + return response; + }, +}; diff --git a/components/splynx/actions/list-customers/list-customers.mjs b/components/splynx/actions/list-customers/list-customers.mjs new file mode 100644 index 0000000000000..76d5318b2afe3 --- /dev/null +++ b/components/splynx/actions/list-customers/list-customers.mjs @@ -0,0 +1,21 @@ +import splynx from "../../splynx.app.mjs"; + +export default { + key: "splynx-list-customers", + name: "List Customers", + description: + "Get a list of your customers. [See the documentation](https://splynx.docs.apiary.io/#reference/customers/customers-collection/list-all-customers)", + version: "0.0.1", + type: "action", + props: { + splynx, + }, + async run({ $ }) { + const response = await this.splynx.listCustomers(); + $.export( + "$summary", + `Successfully listed ${response.length} customers`, + ); + return response; + }, +}; diff --git a/components/splynx/actions/update-customer/update-customer.mjs b/components/splynx/actions/update-customer/update-customer.mjs new file mode 100644 index 0000000000000..80339c8bdbe31 --- /dev/null +++ b/components/splynx/actions/update-customer/update-customer.mjs @@ -0,0 +1,45 @@ +import common from "../common/common-create-update.mjs"; + +const { + splynx, ...props +} = common.props; + +export default { + ...common, + key: "splynx-update-customer", + name: "Update Customer", + description: + "Updates information of an existing customer. [See the documentation](https://splynx.docs.apiary.io/#reference/customers/customer/update-a-customer)", + version: "0.0.1", + type: "action", + props: { + splynx, + customerId: { + propDefinition: [ + splynx, + "customerId", + ], + }, + ...Object.fromEntries( + Object.entries(props).map(([ + key, + value, + ]) => [ + key, + { + ...value, + optional: true, + }, + ]), + ), + }, + async run({ $ }) { + const response = await this.splynx.updateCustomer({ + $, + customerId: this.customerId, + data: this.getData(), + }); + $.export("$summary", `Successfully updated customer ${this.customerId}`); + return response; + }, +}; diff --git a/components/splynx/package.json b/components/splynx/package.json new file mode 100644 index 0000000000000..32f291b63bd3f --- /dev/null +++ b/components/splynx/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/splynx", + "version": "0.1.0", + "description": "Pipedream Splynx Components", + "main": "splynx.app.mjs", + "keywords": [ + "pipedream", + "splynx" + ], + "homepage": "https://pipedream.com/apps/splynx", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/splynx/splynx.app.mjs b/components/splynx/splynx.app.mjs new file mode 100644 index 0000000000000..2d1d21d1cdca8 --- /dev/null +++ b/components/splynx/splynx.app.mjs @@ -0,0 +1,88 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "splynx", + propDefinitions: { + customerId: { + type: "string", + label: "Customer ID", + description: "Select a customer to update, or provide a customer ID.", + async options() { + const customers = await this.listCustomers(); + return customers?.map(({ + name, id, + }) => ({ + label: name, + value: id, + })); + }, + }, + tariffId: { + type: "string", + label: "Tariff ID", + description: "Select a tariff, or provide a tariff ID.", + async options() { + const tariffs = await this.listTariffs(); + return tariffs?.map(({ + title, id, + }) => ({ + label: title, + value: id, + })); + }, + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.subdomain}.splynx.app/api/2.0/admin`; + }, + async _makeRequest({ + $ = this, headers, ...otherOpts + }) { + return axios($, { + ...otherOpts, + baseURL: this._baseUrl(), + headers: { + ...headers, + Authorization: `Splynx-EA (access_token=${this.$auth.oauth_access_token})`, + }, + }); + }, + async listCustomers() { + return this._makeRequest({ + url: "/customers/customer", + }); + }, + async listTariffs() { + return this._makeRequest({ + url: "/tariffs/internet", + }); + }, + async createCustomer(args) { + return this._makeRequest({ + method: "POST", + url: "/customers/customer", + ...args, + }); + }, + async createInternetService({ + customerId, ...args + }) { + return this._makeRequest({ + method: "POST", + url: `/customers/customer/${customerId}/internet-services`, + ...args, + }); + }, + async updateCustomer({ + customerId, ...args + }) { + return this._makeRequest({ + method: "PUT", + url: `/customers/customer/${customerId}`, + ...args, + }); + }, + }, +}; diff --git a/components/spoke_phone/README.md b/components/spoke_phone/README.md new file mode 100644 index 0000000000000..f1b35e84982f6 --- /dev/null +++ b/components/spoke_phone/README.md @@ -0,0 +1,11 @@ +# Overview + +The Spoke Phone API provides a means to programmatically interact with Spoke Phone's voice and SMS services. With this API on Pipedream, you can automate call logging, SMS messaging, and dynamically manage users and call flows. Pipedream's serverless platform allows for the creation of complex workflows, integrating Spoke Phone with a multitude of other apps, triggering actions based on events such as incoming calls or messages, and even processing data with built-in code steps. + +# Example Use Cases + +- **Sync Call Records with CRM**: Automatically log call details into a CRM like Salesforce or HubSpot whenever a call ends on Spoke Phone. You can extract call metadata, including duration, participants, and outcome, and push this data to the CRM to keep customer records up to date. + +- **SMS Customer Support Tickets**: Create a workflow that listens for incoming SMS messages on Spoke Phone and creates a new support ticket in a tool like Zendesk or Jira. This allows your support team to track and respond to customer inquiries that come in via SMS. + +- **Voice Transcription Analysis**: After a call finishes, run a workflow that takes the call recording from Spoke Phone, sends it to a service like Google Speech-to-Text for transcription, analyzes the sentiment with a tool like Google Natural Language API, and then stores the results in a database for quality assurance and training purposes. diff --git a/components/spoke_phone/package.json b/components/spoke_phone/package.json index da51f7a6d5d58..9c35d19c3f367 100644 --- a/components/spoke_phone/package.json +++ b/components/spoke_phone/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/spondyr/README.md b/components/spondyr/README.md index fb1b8f70a6886..baee97dbd154d 100644 --- a/components/spondyr/README.md +++ b/components/spondyr/README.md @@ -1,31 +1,11 @@ # Overview -With the Spondyr API, you can build powerful digital experiences in less time. -It is an API that connects multiple services, databases, and resources in one -place, enabling developers to quickly build serverless applications with -enterprise-grade security, scalability, and performance. +The Spondyr API, known as MailboxValidator, is a tool designed to clean and verify email lists, ensuring that businesses can keep their email marketing databases free of invalid, inactive, or disposable email addresses. By integrating Spondyr with Pipedream, you can automate the process of maintaining a high-quality email list, triggering email validation workflows, and integrating with other services to enhance user management, campaign effectiveness, and overall data hygiene. -The Spondyr API can help developers reduce the complexity of creating -applications by providing the tools to connect multiple services, eliminate -excess code, and rapidly develop user experiences. Here are a few examples of -the experiences you can create with Spondyr API: +# Example Use Cases -- ECommerce Applications – Spondyr API allows developers to create secure and - reliable eCommerce applications with integration to multiple payment - providers. -- Cloud Storage Systems – Build cloud-based storage systems that are flawless, - fast, and secure. -- Enterprise Dashboards – Create interactive dashboards with insights that your - customers need to make the right decisions. -- Mobile Applications – Build mobile apps with real-time data updates and - secure authentication. -- Chatbots – Create interactive and AI-powered chatbots to provide instant - customer support. -- Social Networks – Create powerful and secure social networks with the - flexibility to add and modify features. -- Data Visualization – Leverage Spondyr API and create stunning data - visualization applications. +- **Email Verification on User Sign-up**: Trigger a workflow in Pipedream when a new user signs up via your app's API. Use the Spondyr API to validate the email address. If it's valid, continue the sign-up process; if not, reject the sign-up and notify the user to provide a valid email. -With Spondyr API, the possibilities of what you can create are endless. So the -next time you need to build a digital experience, give Spondyr API a try and -see what you can create. +- **Scheduled Email List Cleaning**: Set up a Pipedream cron job to periodically trigger a workflow that sends batches of email addresses from your database to the Spondyr API for validation. After cleaning, update your database by removing invalid emails, and use Pipedream's built-in integrations to log the results to a Google Sheets spreadsheet for record-keeping and further analysis. + +- **Real-time Lead Validation in Marketing Campaigns**: Integrate the Spondyr API with your marketing platform via Pipedream. When a lead is captured through a landing page form, instantly validate their email with Spondyr. Based on the validation result, you could route the lead to different nurturing tracks or notify your sales team about high-quality leads with valid email addresses. diff --git a/components/spoonacular/README.md b/components/spoonacular/README.md new file mode 100644 index 0000000000000..7b98b8715cacc --- /dev/null +++ b/components/spoonacular/README.md @@ -0,0 +1,11 @@ +# Overview + +The Spoonacular API is a robust culinary interface that can enhance apps with food and nutrition data. With rich endpoints for recipes, ingredients, and meal planning, developers can automate content creation, manage diet tracking, or enrich e-commerce platforms. Pipedream's serverless platform empowers you to create custom Spoonacular workflows, linking with other APIs and services to streamline tasks, like auto-generating shopping lists or scheduling weekly meal plans based on dietary preferences. + +# Example Use Cases + +- **Recipe Content Automation**: Integrate Spoonacular with a CMS platform like WordPress on Pipedream. Automatically fetch new, trending recipes and post them directly to your food blog, enabling you to keep content fresh with minimal effort. + +- **Diet Tracker Integration**: Sync Spoonacular with Google Sheets on Pipedream to log nutritional information. Each time a user logs a meal, Pipedream can call Spoonacular to retrieve dietary data, append it to a sheet, and provide real-time macro tracking for fitness apps. + +- **Smart Grocery List Creation**: Combine Spoonacular with Twilio SMS on Pipedream to generate and send shopping lists. When a user plans meals for the week, Spoonacular can help create a grocery list, which Pipedream then texts to the user via Twilio, simplifying shopping trips. diff --git a/components/sportsdata/README.md b/components/sportsdata/README.md index 6ae9e6cb2d270..46825e180967d 100644 --- a/components/sportsdata/README.md +++ b/components/sportsdata/README.md @@ -1,16 +1,11 @@ # Overview -Using the SportsData API, you can create applications to provide a wide range -of sports data and statistics. With the API, you can access data on players, -teams, competitions, games, and much more. Here are a few examples of what you -can build with the SportsData API: +SportsData API serves up real-time and historical sports data including scores, odds, projections, stats, and news across a variety of sports leagues. By leveraging Pipedream, you can create custom workflows that respond to this data in real-time, integrating with a multitude of other services to automate notifications, data analysis, and content creation. -- Sports Betting Applications -- Fantasy Sports Applications -- Sports Scoreboards -- Sports Analytics Dashboards -- Sports Statistics Applications -- Sports News Applications -- Sports Social Networks -- Player Profile Applications -- Team Profile Applications +# Example Use Cases + +- **Real-Time Score Updates to Slack**: Create a Pipedream workflow that subscribes to real-time score updates for specific games using SportsData API. Use the Pipedream Slack app to send these updates into a Slack channel, keeping fans or bettors in-the-know without having to check multiple sources. + +- **Fantasy Sports Stats Aggregator**: Build a workflow that pulls player stats and injury reports from SportsData API and aggregates them into a Google Sheet via the Google Sheets app on Pipedream. This can be used to maintain an updated database for fantasy sports enthusiasts to make informed decisions about their team lineups. + +- **Automated Sports News Digest**: Set up a Pipedream workflow that gathers the latest sports news from the SportsData API and compiles a daily or weekly digest. Use the Email by Pipedream app to automatically send this digest to a mailing list of subscribers, providing them with a curated summary of the most important updates. diff --git a/components/spotify/README.md b/components/spotify/README.md index 76c77f5ea74a6..3315b67ad989b 100644 --- a/components/spotify/README.md +++ b/components/spotify/README.md @@ -1,8 +1,11 @@ # Overview - -Assuming you have a Spotify Developer account (https://developer.spotify.com/), you can use the Spotify API to build the following: -- An app that displays a user's top artists, tracks, and genres -- A playlist generator that creates a playlist based on a user's favorite artists -- An app that shows a user's friends who also listen to a particular artist -- A concert finder that shows a user upcoming concerts for their favorite artists \ No newline at end of file +The Spotify API on Pipedream offers a creative playground for music lovers and developers alike. With it, you can manage playlists, search for music, get recently played tracks, and manipulate playback among other features. It's a gateway to a rich dataset of music and user information, enabling the creation of personalized and dynamic music experiences. + +# Example Use Cases + +- **Automated Playlist Creation**: Generate playlists automatically based on user-defined criteria such as mood, genre, or recent activity. For instance, create a "Workout Hits" playlist that updates weekly with high-energy tracks popular in your region. + +- **Music Discovery and Sharing**: Set up a workflow that curates new music based on your listening habits, then shares these discoveries to your social media accounts or with friends via email or messaging platforms like Slack. + +- **Listening Analytics**: Build a custom dashboard that collects your streaming data such as favorite artists, tracks, and genres over time. Integrate this with data visualization tools to gain insights into your listening habits. diff --git a/components/spotify/actions/get-currently-playing-track/get-currently-playing-track.mjs b/components/spotify/actions/get-currently-playing-track/get-currently-playing-track.mjs new file mode 100644 index 0000000000000..3103ecaafbbf2 --- /dev/null +++ b/components/spotify/actions/get-currently-playing-track/get-currently-playing-track.mjs @@ -0,0 +1,56 @@ +import { axios } from "@pipedream/platform"; +import spotify from "../../spotify.app.mjs"; +import { ITEM_TYPES } from "../../consts.mjs"; + +export default { + name: "Get currently playing track", + description: + "Get the object currently being played on the user's Spotify account. [See the documentation](https://developer.spotify.com/documentation/web-api/reference/get-the-users-currently-playing-track)", + key: "spotify-get-currently-playing-track", + version: "0.0.1", + type: "action", + props: { + spotify, + market: { + propDefinition: [ + spotify, + "market", + ], + optional: true, + }, + }, + async run({ $ }) { + const { market } = this; + + try { + const res = await axios( + $, + this.spotify._getAxiosParams({ + method: "GET", + path: "/me/player/currently-playing", + params: { + market, + additional_types: [ + ITEM_TYPES.TRACK, + ITEM_TYPES.EPISODE, + ].join(","), + }, + }), + ); + + const itemType = res?.currently_playing_type || "track"; + const itemName = res?.item?.name || "Nothing"; + $.export("$summary", `Currently playing ${itemType}: ${itemName}`); + + return { + playing: !!res, + type: res?.currently_playing_type, + item: res?.item, + progress_ms: res?.progress_ms, + is_playing: res?.is_playing, + }; + } catch (error) { + throw new Error(`Failed to get currently playing track for user: ${error.message}`); + } + }, +}; diff --git a/components/spotify/package.json b/components/spotify/package.json index 714e27cdff439..021dc4ea3c1c0 100644 --- a/components/spotify/package.json +++ b/components/spotify/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/spotify", - "version": "0.7.0", + "version": "0.7.1", "description": "Pipedream Spotify Components", "main": "spotify.app.js", "keywords": [ @@ -14,6 +14,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.4.0" + "@pipedream/platform": "^3.0.3", + "lodash": "^4.17.21" } } diff --git a/components/spotlightr/README.md b/components/spotlightr/README.md new file mode 100644 index 0000000000000..cc39a8798b6de --- /dev/null +++ b/components/spotlightr/README.md @@ -0,0 +1,11 @@ +# Overview + +The Spotlightr API provides a platform for users to interact with their Spotlightr video content and analytics programmatically. Using Pipedream, you can automate various tasks such as uploading videos, managing video settings, and retrieving video analytics. This empowers you to integrate video-related workflows into your business processes, enhance content management, and gain insights from video engagement data, all through serverless workflows. + +# Example Use Cases + +- **Automated Video Uploads and Notifications**: Create a workflow that automates video uploads to Spotlightr. Once a video is uploaded, trigger a notification through email or a messaging app such as Slack to inform your team that new content is available. + +- **Scheduled Video Analytics Reports**: Set up a scheduled workflow to fetch video analytics from Spotlightr regularly. Aggregate the data and send a formatted report to Google Sheets for easy tracking and visualization, keeping stakeholders informed about video performance. + +- **Dynamic Video Content Update for Marketing Campaigns**: Connect Spotlightr with a CRM like HubSpot. Whenever a new lead is added to a specific campaign in HubSpot, trigger a workflow that updates a Spotlightr video's settings or content to tailor it to the campaign, enhancing the viewer's experience. diff --git a/components/spreadsheet_com/README.md b/components/spreadsheet_com/README.md new file mode 100644 index 0000000000000..1c038b42e219c --- /dev/null +++ b/components/spreadsheet_com/README.md @@ -0,0 +1,11 @@ +# Overview + +The Spreadsheet.com API provides programmatic access to manage sheets, rows, and other data within Spreadsheet.com. Integrating this API into Pipedream allows you to automate tasks like updating rows based on external triggers, syncing data between multiple platforms, or even processing data using custom logic. With Pipedream, you can craft workflows that react in real-time to events, schedule data manipulation tasks, and connect to countless other services via their APIs. + +# Example Use Cases + +- **Sync Spreadsheet.com with Google Calendar**: When a new event is scheduled in Google Calendar, a Pipedream workflow can trigger to create or update a corresponding row in a Spreadsheet.com sheet, keeping a detailed log of your events and their details for easy tracking and analysis. + +- **Manage Customer Support Tickets**: When a new support ticket is submitted through a platform like Zendesk, trigger a Pipedream workflow to add the ticket details to Spreadsheet.com. Further automation can notify support staff via Slack or email and update the ticket status in Spreadsheet.com based on follow-up actions. + +- **Process E-commerce Orders**: Connect Spreadsheet.com to an e-commerce platform like Shopify. Each time an order is placed, a Pipedream workflow fires, adding order details to a sheet. It can also perform inventory checks, update stock levels, and send a restock request to your supplier if inventory runs low. diff --git a/components/sproutgigs/README.md b/components/sproutgigs/README.md new file mode 100644 index 0000000000000..6a60a70c65cc9 --- /dev/null +++ b/components/sproutgigs/README.md @@ -0,0 +1,11 @@ +# Overview + +The SproutGigs API enables you to access and manage micro-jobs and tasks within their platform. Through Pipedream, you can interact with this API to automate your gig economy interactions, like posting jobs, analyzing submitted work, or auto-approving tasks. By leveraging Pipedream's capabilities, you can trigger workflows with events from other apps, process data, and connect with countless other services to enhance your gig management processes. + +# Example Use Cases + +- **Automated Job Posting**: When a new project is added to your project management tool like Trello or Asana, automatically post a corresponding job listing on SproutGigs to find freelancers. + +- **Task Submission Review Process**: Set up a workflow that triggers when a task is submitted on SproutGigs. It could send the submission to a Slack channel for quick team review and voting on whether to approve or reject the work. + +- **Payment Automation**: After a task is approved on SproutGigs, use Pipedream to connect with a payment platform like Stripe to automatically process payments to the freelancer, streamlining the financial side of gig management. diff --git a/components/sproutgigs/package.json b/components/sproutgigs/package.json new file mode 100644 index 0000000000000..05acb878ce262 --- /dev/null +++ b/components/sproutgigs/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/sproutgigs", + "version": "0.0.1", + "description": "Pipedream SproutGigs Components", + "main": "sproutgigs.app.mjs", + "keywords": [ + "pipedream", + "sproutgigs" + ], + "homepage": "https://pipedream.com/apps/sproutgigs", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/sproutgigs/sproutgigs.app.mjs b/components/sproutgigs/sproutgigs.app.mjs new file mode 100644 index 0000000000000..aececcb59ce0c --- /dev/null +++ b/components/sproutgigs/sproutgigs.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "sproutgigs", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/spydra/README.md b/components/spydra/README.md new file mode 100644 index 0000000000000..1c3ee5f7cca44 --- /dev/null +++ b/components/spydra/README.md @@ -0,0 +1,11 @@ +# Overview + +The Spydra API lets you automate interactions with the Spydra search engine, enabling developers to query various databases and information repositories. With Spydra's API on Pipedream, one can build powerful serverless workflows that respond to events, process data, and integrate with countless other services. You could create triggers that kick off actions whenever specific conditions are met, or schedule searches to ensure you're always informed with the latest data. + +# Example Use Cases + +- **Content Change Detection Workflow**: Set up a Pipedream workflow that regularly uses Spydra to search for specific terms on websites. When new content is detected, it can notify you via email, Slack, or another communication platform integrated with Pipedream. + +- **Market Research Automation**: Use Spydra to perform market research by querying for industry-specific terms. Aggregate and process the results within Pipedream, then store them in a Google Sheets document for easy analysis or send the data to a BI tool like Tableau for visualization. + +- **Competitor Monitoring System**: Create a workflow that monitors competitor web presence for certain keywords using Spydra. If there's new activity, this system could automatically create tasks in a project management tool like Trello or Asana, prompting your marketing team to review and respond accordingly. diff --git a/components/spydra/package.json b/components/spydra/package.json index 669ac278e0220..3edfbf8ba0629 100644 --- a/components/spydra/package.json +++ b/components/spydra/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/square/README.md b/components/square/README.md index 6c16602d70b3c..05028866f1c4b 100644 --- a/components/square/README.md +++ b/components/square/README.md @@ -1,30 +1,11 @@ # Overview -The Square API, provided by Square Inc., is a powerful tool for creating -integrations with in-store, online, and mobile payments. +The Square API opens up a realm of possibilities for merchants to effortlessly manage their sales, inventory, customers, and payments. With Pipedream, you can automate actions based on events in Square, such as new transactions, updated orders, or customer profile changes. Craft custom workflows that react in real-time, sync data across platforms, or streamline reporting. Through Pipedream's serverless platform, you can connect Square with countless other apps without writing extensive code, turning manual tasks into automated, efficient processes. -The Square API allows apps to access functions such as: +# Example Use Cases -- Credit and debit card payments -- Digital wallets -- Gift cards -- Subscriptions -- Point of sale -- Ordering & Delivery +- **Sales Data Syncing**: Automatically sync new sales data from Square to a Google Sheet. Each time a sale occurs, Pipedream captures the transaction details and appends them to a designated Google Sheet, offering real-time sales tracking and simplified accounting. -It provides developers with the capability to build powerful, secure, and easy -to use applications that integrate with Square’s suite of products and -services. In addition, the API allows developers to quickly build, customize, -and iterate on their Square integrations. +- **Customer Relationship Management**: Enhance customer engagement by linking Square customer creation to a CRM like Salesforce. Whenever a new customer profile is added in Square, Pipedream triggers a workflow that adds or updates the customer's details in Salesforce, ensuring your CRM always has the latest information. -Examples of what you can build using the Square API: - -- Mobile apps and websites for accepting payments -- ECommerce sites -- Mobile POS systems -- Subscription services -- In-store checkout systems -- Online invoicing -- Payment processing for loyalty programs -- Cloud-based inventory and reporting tools -- API endpoints for customer and staff management +- **Inventory Level Alerts**: Create an inventory alert system by leveraging Square's inventory tracking. When inventory levels for a product fall below a certain threshold, Pipedream sends a notification through Slack or email, prompting immediate restocking actions to avoid potential sales losses. diff --git a/components/squarespace/README.md b/components/squarespace/README.md index c6968e44e5165..7d714b08eb4e6 100644 --- a/components/squarespace/README.md +++ b/components/squarespace/README.md @@ -1,26 +1,11 @@ # Overview -The Squarespace API allows developers to create, update, and view Squarespace -content programmatically. Its expansive features make it possible to build a -variety of applications, such as custom ecommerce stores, portfolios, and more. -Here are just a few examples of what you can create with the Squarespace API. +Squarespace's API provides a means to interface programmatically with your Squarespace site, allowing for a plethora of automation opportunities such as manipulating inventory, modifying site content, and syncing data with third-party services. When combined with Pipedream, this can translate into powerful workflows that streamline operations, enhance customer engagement, and keep various platforms in sync with your Squarespace data in real-time. -- eCommerce Websites: Using the Squarespace API, developers can build their own - eCommerce stores that integrate with Squarespace’s existing commerce - platform. This allows you to create and manage products, accept payments, and - customize a customer experience. -- Portfolios: Developers can create a portfolio website with the Squarespace - API. This allows you to automatically import and display any images, videos, - or other digital content from across the web to create an impressive - portfolio. -- Member Management: The Squarespace API also provides developers with the - ability to manage members and their access to content and services. This - includes user authentication, account management, and data security. -- Custom APIs: Developers can also build custom APIs and applications with the - Squarespace API. This could include custom algorithms for analytics, - automated content delivery, and more. +# Example Use Cases -By utilizing the Squarespace API, developers have the opportunity to build a -wide range of applications that can be used in conjunction with the Squarespace -platform. Whether it's an eCommerce store, a portfolio website, or custom APIs, -the Squarespace developers have it all. +- **Automate Inventory Management**: When a product's stock levels change in your external inventory system, trigger a workflow on Pipedream to update the corresponding item's inventory count on Squarespace. This ensures your online storefront reflects the latest stock information, preventing overselling and maintaining customer trust. + +- **Content Sync Across Platforms**: Automatically push new blog posts from a headless CMS to your Squarespace blog. When a new post is published in your CMS, a Pipedream workflow triggers, creating a formatted post on Squarespace. This keeps content fresh and consistent across your platforms without manual entry. + +- **Dynamic Email Campaigns with Customer Data**: Integrate Squarespace customer data with an email marketing service like Mailchimp. When a new order is placed on Squarespace, use Pipedream to capture customer details and create a personalized follow-up email sequence in Mailchimp, enhancing the customer experience and encouraging repeat business. diff --git a/components/ssh/README.md b/components/ssh/README.md index 19aa90fd5a5e3..3338c255475de 100644 --- a/components/ssh/README.md +++ b/components/ssh/README.md @@ -1,21 +1,11 @@ # Overview -The Secure Shell (SSH) API is an powerful tool for securely connecting to -remote systems over a network, allowing users to easily and securely access -their systems or services. This API is useful for a variety of tasks including -data transfer and remote system management. +The SSH (Secure Shell) key-based authentication API allows you to execute commands on a remote server securely. With Pipedream, leverage this capability to automate server management tasks, execute deployment scripts, or gather data from your server infrastructure. By integrating with other apps on Pipedream, you can create seamless workflows that trigger actions on your servers in response to various events. -Key-based authentication is a way of validating the identity of the client to -the server. It works by generating and exchanging a digital key that must match -in order for the user to access the server. This provides an extra layer of -security, as it is much harder to guess a digital key than a user password. +# Example Use Cases -With the SSH (key-based auth) API, you can use key-based authentication to -create an encrypted, authenticated channel between two computers. This can be -used for securely accessing services, such as: +- **Automated Deployment**: Trigger a workflow on Pipedream when your code repository (like GitHub) senses a new commit to the master branch. The workflow would initiate an SSH session to your production server and pull the latest code changes, ensuring continuous deployment without manual intervention. -- Securing server access -- Sharing data with other systems -- Managing remote systems -- Transferring large files between computers -- Encrypting communication between two computers +- **Scheduled Server Maintenance**: Set up a scheduled workflow in Pipedream that SSHs into your server to perform routine maintenance such as package updates, cleaning temp directories, or backing up databases. This could be paired with a notification service like Slack to inform your team when maintenance tasks have been completed. + +- **Real-time Server Monitoring and Alerts**: Create a Pipedream workflow that periodically SSHs into your server to check system health, like disk space or running services. If it detects an issue, it could send an alert through an app like PagerDuty or send a detailed report to an email via SendGrid, enabling prompt response to potential problems. diff --git a/components/ssh_password_based_auth/README.md b/components/ssh_password_based_auth/README.md index bd3ee9194284b..3af527162c0d5 100644 --- a/components/ssh_password_based_auth/README.md +++ b/components/ssh_password_based_auth/README.md @@ -1,29 +1,11 @@ # Overview -The SSH (Secure Shell) API provides users with a secure and reliable way to -connect to a remote server. This can be used to connect to remote systems and -transfer data and files, as well as manage and configure remote systems. With -the SSH API, you can authenticate with a password-based authentication. +The SSH (password-based auth) app on Pipedream facilitates the orchestration of commands and automations on remote servers securely. With it, you can execute shell commands, manage files, and run scripts on your server as part of Pipedream workflows. This unlocks the potential for a host of automations like deploying applications, monitoring system performance, or automating backups — all triggered by events from numerous apps supported on Pipedream. -This is a powerful tool for managing remote systems and networks, providing a -secure and efficient way to access and manage resources. You can use the SSH -API to build the following applications and services: +# Example Use Cases -- Remote Tunneling/Port Forwarding - Create secure tunnels over a network, - allowing traffic to pass through a secure port. -- File Transfers - Move files securely to and from remote systems. -- System/Network Management - Manage and configure remote hosts, networks, and - systems. -- Remote System Backup/Restore - Create secure backups of systems and networks, - and restore them in case of an emergency. -- Scripting/Automation - Automate administrative and maintenance tasks. -- Secure Shell Hosting - Create and host secure shell servers, allowing users - to connect securely to remote systems and networks. -- Secure Network Tunneling - Create secure tunnels over an existing network, - allowing encrypted traffic to pass through. -- Secure Chat - Create secure chat servers, allowing users to communicate - securely through an encrypted channel. -- Secure File Storage - Create secure file storage solutions, allowing users to - store their data securely behind encryption. -- Secure VPN - Create secure Virtual Private Network (VPN) systems, allowing - users to remotely connect securely to remote networks. +- **Automated Deployment on Git Push**: When a new commit is pushed to a specific branch on GitHub, trigger a Pipedream workflow that uses SSH to pull the latest code changes and restarts the application on a remote server. This streamlines deployment processes and ensures that the latest version is always running. + +- **Scheduled Database Backups**: Set up a Pipedream workflow that triggers on a schedule (e.g., nightly) to SSH into your server and execute a database backup script. The workflow could then save the backup file to cloud storage like Google Drive or Dropbox, offering you both automation and offsite backup storage. + +- **Real-time Server Monitoring and Alerts**: Create a Pipedream workflow that periodically runs diagnostics via SSH on your server. The results could be sent to a monitoring app like Datadog, or used to trigger alerts via email or Slack if certain metrics exceed predefined thresholds, ensuring prompt response to potential issues. diff --git a/components/ssh_password_based_auth/package.json b/components/ssh_password_based_auth/package.json new file mode 100644 index 0000000000000..2f1400d8c5990 --- /dev/null +++ b/components/ssh_password_based_auth/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/ssh_password_based_auth", + "version": "0.6.0", + "description": "Pipedream ssh_password_based_auth Components", + "main": "ssh_password_based_auth.app.mjs", + "keywords": [ + "pipedream", + "ssh_password_based_auth" + ], + "homepage": "https://pipedream.com/apps/ssh_password_based_auth", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/sslmate_cert_spotter_api/README.md b/components/sslmate_cert_spotter_api/README.md index 4b5645e0fed3a..b067091f53f45 100644 --- a/components/sslmate_cert_spotter_api/README.md +++ b/components/sslmate_cert_spotter_api/README.md @@ -1,24 +1,11 @@ # Overview -SSLMate — Cert Spotter API provides an easy way to build powerful applications -and services for working with SSL information. With the Cert Spotter API, you -can quickly access and query important SSL certificate data about domain names, -certificate fingerprints and other related information. +The SSLMate — Cert Spotter API allows you to monitor SSL/TLS certificates across the web, catching potentially misissued certs and identifying certificates that could affect your domain security. By tapping into the API with Pipedream, you can automate alerts, integrate certificate data into security analyses, and streamline compliance checks by reacting to newly issued certificates in real-time. -By connecting to SSLMate’s extensive SSL certificate repository, you can easily -build automated applications and services that can be used to monitor SSL -certificates, compare SSL certificates and detect changes in SSL certificate -properties. With the Cert Spotter API, you can build these solutions quickly -and cost-effectively, without needing to perform manual certificate checks. +# Example Use Cases -Here are some examples of the solutions you can build using the SSLMate — Cert -Spotter API: +- **Domain Security Monitoring**: Automate the monitoring of SSL/TLS certificates for your organization's domains. Set up a workflow in Pipedream that periodically checks for new certificates using the Cert Spotter API. If a new, unexpected certificate is spotted, trigger an alert via Email, Slack, or another communication app integrated within Pipedream. -- Monitor certificates for expiration dates and alert users of expirations -- Automatically detect suspect certificates and flag them for review -- Automatically test and verify certificates for trust level -- Check for differences between certificates and find inconsistencies -- Keep track of changes in certificates over time and detect changes quickly -- Compare certificates from multiple servers -- Automatically monitor certificate locations and detect if certificates are - moved +- **Compliance Verification**: Create a Pipedream workflow that verifies the presence of SSL/TLS certificates for all your company's domains. Ensure they meet compliance standards by checking details like expiration dates and issuer credibility. If a certificate doesn't comply, the workflow could log the issue in a tool like Jira and notify the responsible team. + +- **Incident Response Coordination**: Kick off incident response protocols when an unauthorized certificate is identified. Use the Cert Spotter API within Pipedream to listen for such events and, upon detection, automatically create an incident ticket in a service management platform like ServiceNow, and page the security team through PagerDuty. diff --git a/components/sslmate_cert_spotter_api/package.json b/components/sslmate_cert_spotter_api/package.json new file mode 100644 index 0000000000000..864f938a0ad04 --- /dev/null +++ b/components/sslmate_cert_spotter_api/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/sslmate_cert_spotter_api", + "version": "0.6.0", + "description": "Pipedream sslmate_cert_spotter_api Components", + "main": "sslmate_cert_spotter_api.app.mjs", + "keywords": [ + "pipedream", + "sslmate_cert_spotter_api" + ], + "homepage": "https://pipedream.com/apps/sslmate_cert_spotter_api", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/stack_ai/README.md b/components/stack_ai/README.md new file mode 100644 index 0000000000000..ec883b3fc5b09 --- /dev/null +++ b/components/stack_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +The Stack AI API offers artificial intelligence capabilities that can be leveraged to analyze and interpret data, enhance automation, and make predictions based on patterns. In Pipedream, you can create serverless workflows that integrate Stack AI's functionalities to process vast amounts of data and connect it with other apps to enable smart automation, all without managing infrastructure. + +# Example Use Cases + +- **Data Enrichment and Analysis**: Use Stack AI to analyze customer feedback from a database, enrich the data by adding sentiment analysis, categorize feedback, and then store the results in Google Sheets for easy visualization and reporting. + +- **Smart Notifications**: Implement a workflow where Stack AI monitors your application logs; upon detecting anomalies or patterns that suggest an issue, it could trigger an alert via Slack or email, allowing your team to respond rapidly to potential problems. + +- **Dynamic Content Generation**: Leverage Stack AI to generate content based on current trends detected from social media inputs. The system could create blog post drafts, auto-generate news summaries, or craft personalized marketing messages, which could then be reviewed and published via a CMS like WordPress. diff --git a/components/stack_ai/package.json b/components/stack_ai/package.json index 9bf631b51c141..521a5e768ee53 100644 --- a/components/stack_ai/package.json +++ b/components/stack_ai/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/stack_exchange/README.md b/components/stack_exchange/README.md index 85501c5bb1efc..49c68326cbe0d 100644 --- a/components/stack_exchange/README.md +++ b/components/stack_exchange/README.md @@ -1,22 +1,11 @@ # Overview -The Stack Exchange API offers an extensive range of tools for developers who -want to access content from the Stack Exchange network. With the API, -developers can build a variety of applications and services to leverage the -full power of the Stack Exchange platform. Here are some examples of -applications you can create using the Stack Exchange API: +The Stack Exchange API provides programmatic access to Stack Overflow and other sites on the Stack Exchange network. With this API, you can fetch questions, answers, comments, user profiles, and other data that enables you to integrate Stack Exchange's wealth of knowledge and community activities into your own applications. Using Pipedream, you can harness this data to create custom workflows that react to events on Stack Exchange, compile reports, or even automate interactions, providing a powerful tool for developers, data analysts, and content creators. -- Real-time monitoring and analytics for your Stack Exchange account or for - content posted by other users. -- A tool to search Stack Exchange questions and retrieve answers. -- A system that allows you to monitor Stack Exchange content in different - languages. -- Automated moderation tools to help you manage and review content on Stack - Exchange. -- A service that allows users to collaborate on Stack Exchange projects. -- An application that displays enhanced Stack Exchange content, such as rich - media and live updating feeds. -- Tools to help developers create custom applications and services that - facilitate data exchange within the Stack Exchange network. -- A service that allows you to integrate Stack Exchange content into other - applications and services. +# Example Use Cases + +- **Track New Questions on Specific Tags**: Automatically monitor new questions on Stack Overflow for specific tags (like `javascript` or `python`). When a new question is posted, trigger a Pipedream workflow that captures the question details and sends a notification via Slack, enabling a team of developers to quickly respond or collaborate on an answer. + +- **Aggregate Top Answers for Weekly Digest**: Compile a weekly digest of top answers for a given tag. Use the Stack Exchange API to fetch top-rated answers over the past week, then use Pipedream to format this data into a Markdown document and send it via email using a service like SendGrid. This can be a valuable resource for continuous learning within developer teams. + +- **Auto-Respond to Comments on Your Posts**: Set up a Pipedream workflow that listens for new comments on your Stack Overflow posts. Use sentiment analysis (integrated through an app like Google Cloud Natural Language API) to determine the tone of the comment. If it's a question or positive comment, automatically post a predefined thank-you message or helpful response, saving time on community interaction. diff --git a/components/stack_overflow_for_teams/package.json b/components/stack_overflow_for_teams/package.json new file mode 100644 index 0000000000000..ae68e23a56b28 --- /dev/null +++ b/components/stack_overflow_for_teams/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/stack_overflow_for_teams", + "version": "0.0.1", + "description": "Pipedream Stack Overflow for Teams Components", + "main": "stack_overflow_for_teams.app.mjs", + "keywords": [ + "pipedream", + "stack_overflow_for_teams" + ], + "homepage": "https://pipedream.com/apps/stack_overflow_for_teams", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/stack_overflow_for_teams/stack_overflow_for_teams.app.mjs b/components/stack_overflow_for_teams/stack_overflow_for_teams.app.mjs new file mode 100644 index 0000000000000..3399807288e6d --- /dev/null +++ b/components/stack_overflow_for_teams/stack_overflow_for_teams.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "stack_overflow_for_teams", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/stackshare_api/README.md b/components/stackshare_api/README.md new file mode 100644 index 0000000000000..fb5d5fca0aace --- /dev/null +++ b/components/stackshare_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The StackShare API allows you to tap into a comprehensive directory of software tools and cloud infrastructure services. With this API, you can fetch details about different tech stacks, tools, and services used by companies. On Pipedream, you can leverage this API to automate workflows, integrate with other apps, and enrich your datasets with tech stack information. For instance, you could trigger workflows based on new tool releases, compare stacks across companies, or update your CRM with the latest tech used by your prospects. + +# Example Use Cases + +- **Tech Stack Updates to Slack**: Automatically send updates to a Slack channel when a company adds a new tool to their tech stack on StackShare. This can be valuable for sales or competitive analysis teams who need to stay informed about prospective clients' technology choices. + +- **Sync Tool Info with Airtable**: Maintain an Airtable base with detailed records of tools and services. Set up a Pipedream workflow that periodically fetches updates from StackShare and syncs this information into Airtable, keeping the database current for market research or internal use. + +- **Trigger Emails Based on Stack Changes**: Monitor specific companies' tech stacks for changes and trigger email alerts through SendGrid when a new tool is added or removed. This could be useful for marketing teams to tailor their outreach or for product teams to track adoption of their tools in the market. diff --git a/components/stackshare_api/package.json b/components/stackshare_api/package.json index a9a3947dd80ab..e53466d0c671b 100644 --- a/components/stackshare_api/package.json +++ b/components/stackshare_api/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/stammer_ai/README.md b/components/stammer_ai/README.md new file mode 100644 index 0000000000000..0aae1bf742243 --- /dev/null +++ b/components/stammer_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +Stammer.ai API offers the ability to transcribe and analyze speech, making it invaluable for applications needing speech-to-text conversion, sentiment analysis, or keyword recognition. On Pipedream, you can harness these capabilities to automate data collection, content generation, and insight analysis. By connecting Stammer.ai with other apps, you can create powerful workflows that act on voice-driven data. + +# Example Use Cases + +- **Voicemail to Text for Customer Support**: Convert voicemail messages to text and create support tickets in Zendesk or another customer service platform. This workflow can categorize and prioritize requests based on sentiment and keyword analysis. + +- **Meeting Minutes Generation**: Transcribe team meeting recordings and post the text to Google Docs. Summarize action points and automatically schedule follow-ups in project management tools like Trello or Asana based on the discussion. + +- **Voice-Commanded Task Creation**: Use voice commands to create tasks in project management apps. Analyze the voice note for urgency and automatically classify the task's priority and assign it in apps like Jira or Monday.com. diff --git a/components/stannp/README.md b/components/stannp/README.md new file mode 100644 index 0000000000000..15992358c2548 --- /dev/null +++ b/components/stannp/README.md @@ -0,0 +1,11 @@ +# Overview + +The Stannp API integrates with Pipedream to automate direct mail campaigns. You can create, manage, and send personalized postcards, letters, or flyers using Stannp's services. With Pipedream's serverless platform, you can connect Stannp to various triggers and actions, creating workflows that handle everything from customer outreach to event-triggered mailings. This combination is powerful for businesses looking to blend digital and physical marketing strategies seamlessly. + +# Example Use Cases + +- **Automated Customer Birthday Cards**: Trigger a Stannp campaign to send personalized birthday cards to customers. Use a CRM trigger in Pipedream, like Salesforce or HubSpot, to fetch customer birthdays, then automatically instruct Stannp to mail out a pre-designed birthday card. + +- **E-commerce Order Follow-ups**: After an order is placed on an e-commerce platform like Shopify, use Pipedream to trigger a workflow that sends a thank-you postcard or a discount offer for future purchases through Stannp, enhancing customer retention. + +- **Event Attendee Thank-Yous**: Post-event, use Pipedream to trigger from an event management app like Eventbrite. Automatically send personalized thank-you notes or feedback request letters to attendees via the Stannp API. diff --git a/components/starloop/README.md b/components/starloop/README.md new file mode 100644 index 0000000000000..2a115a43dc47e --- /dev/null +++ b/components/starloop/README.md @@ -0,0 +1,11 @@ +# Overview + +The Starloop API provides a pathway to streamline customer feedback management by allowing the automation of review requests, monitoring of multiple review sites, and analysis of customer sentiment. With Pipedream, you can craft serverless workflows that leverage these capabilities, integrating Starloop's functionalities with other apps and services to amplify your customer relationship strategies. + +# Example Use Cases + +- **Automated Review Request Workflow**: Trigger a review request in Starloop whenever a customer completes a purchase on your e-commerce platform. Pair Starloop with Shopify or WooCommerce on Pipedream to detect new orders and automatically reach out for customer reviews. + +- **Review Monitoring and Alerts**: Keep a vigilant eye on customer reviews across platforms. Use Starloop with Pipedream to monitor new reviews and send alerts via Slack or email when a review is detected, ensuring quick responses to customer feedback. + +- **Sentiment Analysis Dashboard**: Gather insights from customer reviews by connecting Starloop to a sentiment analysis service. Process new reviews through natural language processing (NLP) tools and visualize the results in a dashboard app like Google Data Studio for actionable insights. diff --git a/components/starshipit/README.md b/components/starshipit/README.md new file mode 100644 index 0000000000000..1e119c804f2c8 --- /dev/null +++ b/components/starshipit/README.md @@ -0,0 +1,11 @@ +# Overview + +The Starshipit API is a tool for optimizing and automating shipping and fulfillment processes. By integrating it with Pipedream, you can create powerful serverless workflows that connect Starshipit with other apps and services to streamline e-commerce operations, reduce manual entry, and improve customer experiences. From automating order dispatch to synchronizing tracking information across platforms, the possibilities are vast. + +# Example Use Cases + +- **Automated Order Processing**: Trigger a workflow in Pipedream whenever a new order is placed on an e-commerce platform like Shopify. The workflow then uses the Starshipit API to create a shipping label, allocates a courier, and dispatches the order automatically. + +- **Real-Time Shipping Updates**: Set up a Pipedream workflow that listens for webhook events from Starshipit signaling a change in shipping status. When a status update occurs, the workflow can update the order status on platforms like WooCommerce, send a notification to Slack for the fulfillment team, and email the customer the updated tracking information. + +- **Centralized Shipping Analytics**: Collect shipping data from Starshipit via its API and feed it into a Pipedream workflow. This data can be aggregated and pushed to a Google Sheets document or a data visualization tool like Tableau for comprehensive analysis and reporting on shipping performance. diff --git a/components/starton/README.md b/components/starton/README.md new file mode 100644 index 0000000000000..6e3e0b2ab577f --- /dev/null +++ b/components/starton/README.md @@ -0,0 +1,11 @@ +# Overview + +The Starton API taps into the burgeoning world of blockchain, enabling developers to build applications that interact with smart contracts and manage tokens without deep blockchain expertise. On Pipedream, you can harness this power to create serverless workflows that automate tasks like monitoring smart contract events, managing NFTs, or executing blockchain transactions in response to various triggers from webhooks, schedules, or app events. + +# Example Use Cases + +- **Automated NFT Portfolio Tracking**: Build a workflow that triggers daily, pulling your NFT inventory using the Starton API. Connect this with a data store on Pipedream to track changes over time, and set up an email or Slack notification to alert you when new NFTs are added. + +- **Smart Contract Event Monitoring**: Utilize the Starton API to watch for specific events on a smart contract, such as new transactions or fulfillment of certain conditions. When an event occurs, trigger a Pipedream workflow that logs the details to Google Sheets, sends a message with Twilio, or updates a dashboard in Data Studio for real-time monitoring. + +- **Conditional Blockchain Transactions Execution**: Create a Pipedream workflow that listens for webhooks from an e-commerce platform. When a new purchase is confirmed, use the Starton API to execute a blockchain transaction, like transferring a token or minting an NFT, as part of the digital product delivery process. diff --git a/components/starton/package.json b/components/starton/package.json index ac50d6f5750d4..2124e64931c12 100644 --- a/components/starton/package.json +++ b/components/starton/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/status_hero/README.md b/components/status_hero/README.md index 960ad2315c377..9b97615bae1ea 100644 --- a/components/status_hero/README.md +++ b/components/status_hero/README.md @@ -1,21 +1,11 @@ # Overview -The Status Hero API provides a powerful set of functions that enable developers -to create custom applications enabling teams to better communicate and -collaborate. With the Status Hero API, you can build a wide range of -applications to improve team communication and organization. +The Status Hero API allows you to interface with the Status Hero platform programmatically, enabling you to automate stand-ups, collect team status updates, and integrate this data with other apps and services. By leveraging Pipedream's capabilities, you can set up workflows that react to new check-ins, sync updates to project management tools, or analyze team productivity by aggregating status data. -Some examples of what you can build with the Status Hero API include: +# Example Use Cases -- App integrations that allow you to quickly upload daily logs, team - performance scores, project updates and other related data. -- Custom dashboards to visualize team performance, quickly identify trends and - monitor performance. -- Automated reminders for team check-ins, team meetings, and other activities. -- Connected notifcations to alert members of new notifications, updates and - surveys. -- Automated processes to streamline communication between teams. -- Custom reporting to easily access team data and evaluate performance. +- **Daily Stand-up Compilation**: Automatically compile all team members' status updates into a single document or message. For example, once all updates are submitted, a workflow could pull the data from Status Hero and format it into a Slack digest, sending it to a designated channel. -By utilizing the Status Hero API, you can create a rich array of applications -to help streamline communication and improve team collaboration. +- **Project Management Sync**: Sync status updates to project management tools like Trello or Asana. When a team member submits an update with tasks they've worked on, a Pipedream workflow could automatically create or update cards/tasks in Trello or Asana, keeping project boards up to date with minimal manual entry. + +- **Productivity Analytics**: Collect and analyze team productivity by aggregating status updates over time. You could set up a Pipedream workflow to store updates in a database like Google Sheets or Airtable. From there, analyze the data to identify trends, bottlenecks, and team performance insights. diff --git a/components/statuscake/README.md b/components/statuscake/README.md index 8da97ae3f9e57..3b752b0a36f29 100644 --- a/components/statuscake/README.md +++ b/components/statuscake/README.md @@ -1,21 +1,11 @@ # Overview -Using the Statuscake API, you can build a variety of applications to monitor -and manage your website's performance. Here are a few examples: +StatusCake provides a powerful API that allows you to automate the monitoring of your websites and servers. With this API on Pipedream, you can create workflows that respond to uptime events, performance metrics, and maintain a robust oversight of your web infrastructure's health. The API enables you to automate alerting, integrate with other tools for a seamless DevOps ecosystem, and perform actions based on the status of your monitored endpoints. -- Create a custom dashboard that shows performance statistics, such as uptime, - latency and response times. -- Create an alert system that emails or texts you whenever your website is - down. -- Track the load time of webpages, and set thresholds to determine when - performance is affected. -- Automate the creation and configuration of tests, such as ping testing and - URL checking. -- Generate performance reports on a regular basis, to keep up with changing - trends in usage. -- Create an API key to securely monitor your website from external - applications. +# Example Use Cases -This is only a small subset of the types of applications you can build using -the Statuscake API. With its extensive selection of features, you can create -powerful monitoring solutions for both small and large scale websites. +- **Incident Response Coordination**: Automate the creation of a ticket in a service like Jira or Zendesk when StatusCake detects a downtime event. This ensures that your support or devops team is immediately aware of issues and can act on them promptly. + +- **Performance Metrics Logging**: Capture performance data from StatusCake and log it to a time series database like InfluxDB. Use this data to analyze trends over time, helping you to preemptively spot potential performance degradations before they become critical. + +- **Status Updates via Communication Platforms**: Send a message to a Slack channel or post a status update to a Twitter account whenever a test fails or recovers. This keeps your team or user base informed about the operational status of your services in real-time. diff --git a/components/statuspage/README.md b/components/statuspage/README.md index dbe2b94d2ddea..8a2e936362fe4 100644 --- a/components/statuspage/README.md +++ b/components/statuspage/README.md @@ -1,19 +1,11 @@ # Overview -Using the Statuspage API, you can develop powerful and secure software -solutions that enable communication with end-users. You can use it to build: +The Statuspage API allows you to automate the management and orchestration of incident communication directly from Pipedream. With this API, you can create, update, and resolve incidents, manage maintenance events, and retrieve information about components and subscribers. It's an efficient way to ensure transparency and inform stakeholders during outages or scheduled maintenance by programmatically controlling your status pages. -- End-user facing applications that can be integrated with your other systems, - allowing customers to access your support team and get status updates. -- Internal applications to manage customer accounts and payments, keeping track - of customer satisfaction and service levels. -- Automated incident response systems that can be triggered to send - notifications and provide updates in the event of an incident. -- A real-time dashboard to monitor performance and reliability metrics, - aggregate and analyze customer support data, and keep track of compliance. -- Self-service applications to put the power in the hands of your customers and - give them the information they need quickly and easily. -- Custom integrations to drive effective communication and collaboration with - other tools and systems. -- Automation tools that can get updates from your Statuspage to other systems, - allowing for streamlined and consistent communication. +# Example Use Cases + +- **Incident Response Automation**: Trigger a workflow on Pipedream when a monitoring app detects an outage, automatically creating an incident on Statuspage and sending notifications via communication platforms like Slack or email. + +- **Scheduled Maintenance Reminder**: Set up a recurring Pipedream workflow that posts upcoming maintenance events to Statuspage and simultaneously reminds your team via a tool like Google Calendar, ensuring that all parties are aware and prepared. + +- **Subscriber Update Sync**: Whenever a Statuspage incident is updated or resolved, a workflow can sync this status to a customer relationship management platform like Salesforce or Zendesk, keeping customer-facing teams informed and improving response times. diff --git a/components/stealthgpt/package.json b/components/stealthgpt/package.json new file mode 100644 index 0000000000000..25fa1a9e8ea97 --- /dev/null +++ b/components/stealthgpt/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/stealthgpt", + "version": "0.0.1", + "description": "Pipedream StealthGPT Components", + "main": "stealthgpt.app.mjs", + "keywords": [ + "pipedream", + "stealthgpt" + ], + "homepage": "https://pipedream.com/apps/stealthgpt", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/stealthgpt/stealthgpt.app.mjs b/components/stealthgpt/stealthgpt.app.mjs new file mode 100644 index 0000000000000..2814835f94524 --- /dev/null +++ b/components/stealthgpt/stealthgpt.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "stealthgpt", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/stealthseminar/README.md b/components/stealthseminar/README.md new file mode 100644 index 0000000000000..e0f81508ea4f7 --- /dev/null +++ b/components/stealthseminar/README.md @@ -0,0 +1,11 @@ +# Overview + +The StealthSeminar API enables automated control over webinar events. With it, you can register participants, retrieve webinar stats, and manage your webinars programmatically. When used within Pipedream's ecosystem, you can craft workflows that trigger on specific conditions, manipulate data, and connect with countless other apps. Pipedream's serverless platform offers a powerful way to glue together different API services, making your webinar management more efficient and integrated with your existing tech stack. + +# Example Use Cases + +- **Automated Attendee Registration**: Register attendees to your webinars automatically when a new sign-up is detected in your CRM, like Salesforce or HubSpot. Pipedream can listen for new contacts and use StealthSeminar's API to register them for a scheduled webinar, all without manual intervention. + +- **Webinar Performance Reporting to Slack**: After a webinar concludes, compile key performance metrics and send a summary report directly to a Slack channel. By combining StealthSeminar's data with Pipedream's Slack integration, you keep your team instantly informed about webinar engagement and attendance. + +- **Dynamic Email Follow-ups with SendGrid**: Use attendee behavior data from StealthSeminar to segment your audience and send personalized follow-up emails through SendGrid. Whether an attendee missed the event or asked a question, tailor your email content accordingly and automate the sending process with Pipedream workflows. diff --git a/components/storeganise/actions/add-invoice-payment/add-invoice-payment.mjs b/components/storeganise/actions/add-invoice-payment/add-invoice-payment.mjs new file mode 100644 index 0000000000000..332fff52c97ba --- /dev/null +++ b/components/storeganise/actions/add-invoice-payment/add-invoice-payment.mjs @@ -0,0 +1,61 @@ +import storeganise from "../../storeganise.app.mjs"; + +export default { + key: "storeganise-add-invoice-payment", + name: "Add Invoice Payment", + description: "Adds a payment to the targeted invoice. [See the documentation](https://pipedream-dev-trial.storeganise.com/api/docs/admin/invoices#admin_invoices._invoiceId_payments)", + version: "0.0.1", + type: "action", + props: { + storeganise, + invoiceId: { + propDefinition: [ + storeganise, + "invoiceId", + ], + }, + amount: { + type: "string", + label: "Amount", + description: "The payment amount", + }, + method: { + type: "string", + label: "Method", + description: "The payment method", + options: [ + "cash", + "card", + "cheque", + "transfer", + "other", + ], + }, + paymentDate: { + type: "string", + label: "Payment Date", + description: "The date of the payment in YYYY-MM-DD format (e.g., `2018-02-18`)", + }, + note: { + type: "string", + label: "Note", + description: "The content of the note", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.storeganise.addPaymentToInvoice({ + $, + invoiceId: this.invoiceId, + data: { + type: "manual", + amount: this.amount, + method: this.method, + date: this.paymentDate, + notes: this.note, + }, + }); + $.export("$summary", `Successfully added payment to invoice ${this.invoiceId}`); + return response; + }, +}; diff --git a/components/storeganise/actions/mark-invoice-paid/mark-invoice-paid.mjs b/components/storeganise/actions/mark-invoice-paid/mark-invoice-paid.mjs new file mode 100644 index 0000000000000..483e1ad5db656 --- /dev/null +++ b/components/storeganise/actions/mark-invoice-paid/mark-invoice-paid.mjs @@ -0,0 +1,29 @@ +import storeganise from "../../storeganise.app.mjs"; + +export default { + key: "storeganise-mark-invoice-paid", + name: "Mark Invoice as Paid", + description: "Marks the selected invoice as paid in Storeganise. [See the documentation](https://pipedream-dev-trial.storeganise.com/api/docs/admin/invoices#admin_invoices.PUT_invoiceId)", + version: "0.0.1", + type: "action", + props: { + storeganise, + invoiceId: { + propDefinition: [ + storeganise, + "invoiceId", + ], + }, + }, + async run({ $ }) { + const response = await this.storeganise.updateInvoice({ + $, + invoiceId: this.invoiceId, + data: { + state: "paid", + }, + }); + $.export("$summary", `Invoice ${this.invoiceId} was marked as paid successfully`); + return response; + }, +}; diff --git a/components/storeganise/package.json b/components/storeganise/package.json new file mode 100644 index 0000000000000..54bd0ced0283c --- /dev/null +++ b/components/storeganise/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/storeganise", + "version": "0.1.0", + "description": "Pipedream Storeganise Components", + "main": "storeganise.app.mjs", + "keywords": [ + "pipedream", + "storeganise" + ], + "homepage": "https://pipedream.com/apps/storeganise", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} diff --git a/components/storeganise/sources/common/base.mjs b/components/storeganise/sources/common/base.mjs new file mode 100644 index 0000000000000..a9a05c1546364 --- /dev/null +++ b/components/storeganise/sources/common/base.mjs @@ -0,0 +1,78 @@ +import storeganise from "../../storeganise.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + storeganise, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastCreated() { + return this.db.get("lastCreated"); + }, + _setLastCreated(lastCreated) { + this.db.set("lastCreated", lastCreated); + }, + getParams() { + return {}; + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + async processEvent(max) { + const lastCreated = this._getLastCreated(); + let maxCreated = lastCreated; + const resourceFn = this.getResourceFn(); + const params = { + ...this.getParams(lastCreated), + limit: max || 50, + offset: 0, + }; + let total, done, count = 0; + + do { + const items = await resourceFn({ + params, + }); + total = items?.length; + if (!total) { + break; + } + for (const item of items) { + if (!lastCreated || (Date.parse(item.created) >= Date.parse(lastCreated))) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + if (!maxCreated || (Date.parse(item.created) > Date.parse(maxCreated))) { + maxCreated = item.created; + } + count++; + if (max && count >= max) { + done = true; + break; + } + } + } + params.offset += params.limit; + } while (total === params.limit && !done); + + this._setLastCreated(maxCreated); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/storeganise/sources/new-invoice-created/new-invoice-created.mjs b/components/storeganise/sources/new-invoice-created/new-invoice-created.mjs new file mode 100644 index 0000000000000..9b272da24f97a --- /dev/null +++ b/components/storeganise/sources/new-invoice-created/new-invoice-created.mjs @@ -0,0 +1,35 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "storeganise-new-invoice-created", + name: "New Invoice Created", + description: "Emit new event when a new invoice is created in Storeganise.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.storeganise.listInvoices; + }, + getParams(lastCreated) { + return { + include: [ + "billing", + "customFields", + ], + updatedAfter: lastCreated, + }; + }, + generateMeta(invoice) { + return { + id: invoice.id, + summary: `New Invoice Created: ${invoice.id}`, + ts: Date.parse(invoice.created), + }; + }, + }, + sampleEmit, +}; diff --git a/components/storeganise/sources/new-invoice-created/test-event.mjs b/components/storeganise/sources/new-invoice-created/test-event.mjs new file mode 100644 index 0000000000000..9031f40da75f9 --- /dev/null +++ b/components/storeganise/sources/new-invoice-created/test-event.mjs @@ -0,0 +1,105 @@ +export default { + "created": "2024-09-04T06:43:39.749Z", + "creditApplied": 0, + "endDate": "2024-11-01", + "id": "66d8019b88e79a0015dc969d", + "items": [], + "owner": { + "id": "66ce753e7405aa00158ba554", + "name": "Homer Simpson" + }, + "payments": [], + "sid": "cen000041", + "sent": "2024-09-04T06:43:39.984Z", + "siteId": "66ce753e7405aa00158ba679", + "unitRentalId": "66d8019b88e79a0015dc9677", + "startDate": "2024-10-01", + "state": "sent", + "tags": [ + "moveIn", + "month" + ], + "taxPercent": 20, + "type": "unit", + "labels": [], + "updated": "2024-09-04T06:43:39.985Z", + "entries": [ + { + "id": "66d8019b88e79a0015dc96a2", + "desc": "Deposit", + "date": "2024-09-04", + "code": "deposit", + "created": "2024-09-04T06:43:39.769Z", + "tags": [ + "deposit" + ], + "amount": 99, + "qty": 1, + "subtotal": 99, + "tax": 0, + "total": 99, + "type": "deposit" + }, + { + "id": "66d8019b88e79a0015dc96a3", + "desc": "Prorated rent", + "date": "2024-09-04", + "endDate": "2024-10-01", + "code": "rent", + "created": "2024-09-04T06:43:39.769Z", + "tags": [ + "rent", + "prorated" + ], + "amount": 87.75, + "qty": 1, + "subtotal": 87.75, + "taxPct": 20, + "tax": 17.55, + "total": 105.3, + "type": "revenue" + }, + { + "id": "66d8019b88e79a0015dc96a4", + "desc": "Rent", + "date": "2024-10-01", + "endDate": "2024-11-01", + "code": "rent", + "created": "2024-09-04T06:43:39.769Z", + "tags": [ + "rent" + ], + "amount": 99, + "qty": 1, + "subtotal": 99, + "taxPct": 20, + "tax": 19.8, + "total": 118.8, + "type": "revenue" + }, + { + "id": "66d8019b88e79a0015dc96a5", + "desc": "Admin fee", + "date": "2024-10-01", + "code": "products", + "created": "2024-09-04T06:43:39.769Z", + "tags": [ + "charge" + ], + "amount": 10, + "qty": 1, + "subtotal": 10, + "taxPct": 10, + "tax": 1, + "total": 11, + "type": "revenue" + } + ], + "subtotal": 295.75, + "tax": 38.35, + "total": 334.1, + "amountPaid": 0, + "balance": 334.1, + "customFields": {}, + "billing": null +} \ No newline at end of file diff --git a/components/storeganise/sources/new-unit-rental-created/new-unit-rental-created.mjs b/components/storeganise/sources/new-unit-rental-created/new-unit-rental-created.mjs new file mode 100644 index 0000000000000..396ca4fedef16 --- /dev/null +++ b/components/storeganise/sources/new-unit-rental-created/new-unit-rental-created.mjs @@ -0,0 +1,37 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "storeganise-new-unit-rental-created", + name: "New Unit Rental Created", + description: "Emit new event when a unit rental is created in Storeganise", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.storeganise.listUnitRentals; + }, + getParams(lastCreated) { + return { + include: [ + "unit", + "owner", + "agreementUrl", + "customFields", + ], + updatedAfter: lastCreated, + }; + }, + generateMeta(unitRental) { + return { + id: unitRental.id, + summary: `New Unit Rental Created: ${unitRental.id}`, + ts: Date.parse(unitRental.created), + }; + }, + }, + sampleEmit, +}; diff --git a/components/storeganise/sources/new-unit-rental-created/test-event.mjs b/components/storeganise/sources/new-unit-rental-created/test-event.mjs new file mode 100644 index 0000000000000..d43886d1345c5 --- /dev/null +++ b/components/storeganise/sources/new-unit-rental-created/test-event.mjs @@ -0,0 +1,139 @@ +export default { + "id": "66d801ac786e1000159e7d84", + "unitId": "66ce753e7405aa00158ba8b7", + "siteId": "66ce753e7405aa00158ba68e", + "ownerId": "66ce753e7405aa00158ba54e", + "deposit": 179, + "price": 179, + "moveInJobId": "66d801ac786e1000159e7d6a", + "startDate": "2024-09-12", + "depositBalance": 0, + "charges": [ + { + "id": "66d801ac786e1000159e7d85", + "title": { + "en": "Admin fee" + }, + "amount": 10, + "startDate": "2024-09-12", + "endDate": "2024-09-12", + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "prepaymentBalance": 0, + "revenueBalance": 0, + "state": "reserved", + "invoiceDate": 1, + "systemCharges": { + "unitDeposit": {}, + "unitRent": {} + }, + "created": "2024-09-04T06:43:56.858Z", + "updated": "2024-09-04T06:43:56.877Z", + "unit": { + "id": "66ce753e7405aa00158ba8b7", + "siteId": "66ce753e7405aa00158ba68e", + "typeId": "66ce753e7405aa00158ba965", + "name": "sv004", + "state": "reserved", + "length": 10, + "width": 5, + "height": 8, + "measure": "ft", + "floor": "1", + "defaultDeposit": 179, + "defaultPrice": 179, + "featureIds": [ + "66ce753e7405aa00158ba50d", + "66ce753e7405aa00158ba50e", + "66ce753e7405aa00158ba510", + "66ce753e7405aa00158ba513" + ], + "ownerId": "66ce753e7405aa00158ba54e", + "rentalId": "66d801ac786e1000159e7d84", + "blockedReason": null, + "labels": [], + "created": "2018-10-29T11:31:48.586Z", + "updated": "2024-09-04T06:43:56.871Z", + "customFields": {} + }, + "owner": { + "id": "66ce753e7405aa00158ba54e", + "address": "Moe's Cafe, 10201 Pico Blvd, Los Angeles, CA 90064, USA", + "area": "paradise", + "billingTrigger": "auto", + "charges": [ + { + "id": "66ce753e7405aa00158ba552", + "type": "recurringCharge", + "sourceType": "preset", + "sourceId": "627a0a17afcd8f00041104b3", + "title": { + "en": "$5 Special discount" + }, + "amount": -5, + "startDate": "2018-10-15" + }, + { + "id": "66ce753e7405aa00158ba553", + "type": "recurringCharge", + "sourceType": "preset", + "sourceId": "627a0a17afcd8f00041104b5", + "title": { + "en": "Additional Insurance" + }, + "amount": 15, + "startDate": "2018-10-01" + } + ], + "created": "2018-10-30T04:46:23.267Z", + "deposits": [ + { + "id": "66ce753e7405aa00158ba54f", + "amount": 179, + "note": "Deposit: Central - 5' x 10' unit (50 sqft)", + "created": "2018-10-30T04:50:16.328Z" + }, + { + "id": "66ce753e7405aa00158ba550", + "amount": 99, + "note": "Deposit: Sunset Park - 5' x 5' (25 sq ft)", + "created": "2018-10-30T04:50:33.804Z" + } + ], + "files": [], + "email": "spaceup+bart@storeganise.com", + "firstName": "Bart", + "hasItemsInStorage": true, + "itemCounts": { + "bulky_furniture_m": 3, + "bulky_furniture_l": 1, + "bulky_furniture_s": 2, + "bulky": 2, + "goh": 1, + "doc": 3, + "box": 5 + }, + "invoiceDate": 1, + "isActive": true, + "language": "en", + "lastName": "Simpson", + "market": { + "id": "66ce753e7405aa00158ba649" + }, + "name": "Bart Simpson", + "phone": "555-636-1234", + "planId": "66ce753e7405aa00158ba507", + "referCode": "0z8z0tm5", + "roles": [], + "permissions": [], + "updated": "2024-08-31T17:04:14.754Z", + "overdue": "2024-02-15T18:00:10.178Z", + "labels": [ + "66ce753e7405aa00158ba539" + ], + "customFields": {}, + "billingMethod": "default" + }, + "customFields": {} +} \ No newline at end of file diff --git a/components/storeganise/sources/new-user-created/new-user-created.mjs b/components/storeganise/sources/new-user-created/new-user-created.mjs new file mode 100644 index 0000000000000..9be2372f147c1 --- /dev/null +++ b/components/storeganise/sources/new-user-created/new-user-created.mjs @@ -0,0 +1,40 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "storeganise-new-user-created", + name: "New User Created", + description: "Emit new event when a new user is created in Storeganise", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.storeganise.listUsers; + }, + getParams(lastCreated) { + return { + include: [ + "customFields", + "valetOrders", + "items", + "units", + "billing", + "settings", + "creditsDebits", + ], + updatedAfter: lastCreated, + }; + }, + generateMeta(user) { + return { + id: user.id, + summary: `New User Created: ${user.id}`, + ts: Date.parse(user.created), + }; + }, + }, + sampleEmit, +}; diff --git a/components/storeganise/sources/new-user-created/test-event.mjs b/components/storeganise/sources/new-user-created/test-event.mjs new file mode 100644 index 0000000000000..ccda65a4d9d3c --- /dev/null +++ b/components/storeganise/sources/new-user-created/test-event.mjs @@ -0,0 +1,2555 @@ +export default { + "id": "66ce753e7405aa00158ba546", + "billingTrigger": "default", + "charges": [], + "created": "2018-10-15T17:25:28.526Z", + "deposits": [], + "files": [], + "email": "charlie@storeganise.com", + "firstName": "Charlie", + "isActive": false, + "emailConfirmed": "2022-09-07T10:03:13.863Z", + "language": "en", + "lastName": "Davison", + "market": { + "id": "66ce753e7405aa00158ba640" + }, + "name": "Charlie Davison", + "referCode": "z03307lh", + "roles": [], + "permissions": [], + "signinAt": "2024-08-07T08:08:33.473Z", + "updated": "2024-08-07T08:08:33.474Z", + "labels": [], + "customFields": {}, + "valetOrders": [], + "items": [], + "units": [], + "billingMethod": "default", + "billing": null, + "settings": { + "marketId": "66ce753e7405aa00158ba640", + "areas": [ + { + "id": "66ce753e7405aa00158ba4fa", + "title": { + "en": "Paradise", + "es": "Paraíso", + "de": "Paradies", + "fr": "Paradis", + "pt": "Paraíso" + }, + "surcharge": 0, + "note": {} + }, + { + "id": "66ce753e7405aa00158ba4fb", + "title": { + "en": "Downtown", + "es": "Centro de la ciudad", + "de": "Zentrum", + "fr": "Centre ville", + "pt": "Centro da cidade" + }, + "surcharge": 0, + "note": {} + }, + { + "id": "66ce753e7405aa00158ba4fc", + "title": { + "en": "Green Valley", + "es": "Green Valley", + "de": "Grüntal", + "fr": "Green Valley", + "pt": "Green Valley" + }, + "surcharge": 10, + "note": { + "en": "Please note that a $10 surcharge is added for deliveries or collections to/from Green Valley.", + "es": "Tenga en cuenta que se agrega un recargo de €10 por entregas o colecciones desde / hacia Green Valley.", + "de": "Für Zustellung nach und Abholungen von Grüntal wird ein € 10 Aufpreis verrechnet.", + "fr": "Notez qu'une surcharge de €10 est appliquée pour les livraisons ou collectes vers/depuis Freen Valley", + "pt": "Observe que uma sobretaxa de €10 é adicionada para entregas ou coletas de/para Green Valley." + } + } + ], + "billing": "stripe", + "brand": { + "color": "#fff", + "linkColor": "#4992EB", + "textColor": "#4992EB" + }, + "captcha": "recaptcha", + "collectionWaitTime": 20, + "companyName": "Pipedream Dev", + "currency": "$", + "currencyCode": "USD", + "dateFormats": { + "long": "YYYY-MM-DD", + "short": "YYYY-MM-DD", + "time": "h:mma" + }, + "helpPhone": { + "display": "+1 416 639 0873", + "full": "14166390873" + }, + "helpUrl": "http://help.storeganise.com", + "invoicePeriod": "month", + "items": [ + { + "id": "66ce753e7405aa00158ba641", + "type": "box", + "price": 8, + "displayPrice": { + "en": "$8/month" + } + }, + { + "id": "66ce753e7405aa00158ba642", + "type": "goh", + "price": 12, + "displayPrice": { + "en": "$12/month" + } + }, + { + "id": "66ce753e7405aa00158ba643", + "type": "doc", + "price": 5, + "displayPrice": { + "en": "$5/month" + } + }, + { + "id": "66ce753e7405aa00158ba644", + "type": "bulky", + "price": 12, + "displayPrice": { + "en": "$12/month" + } + } + ], + "languages": [ + "en" + ], + "modes": [ + "selfStorage" + ], + "olark": { + "id": "8547-576-10-1479" + }, + "plans": [], + "products": [ + "66ce753e7405aa00158ba509", + "66ce753e7405aa00158ba50a", + "66ce753e7405aa00158ba50b" + ], + "requireAgreeTerms": true, + "siteUrl": "https://storeganise.com", + "sites": [ + { + "id": "66ce753e7405aa00158ba679", + "title": { + "en": "Central", + "de": "Central", + "es": "Central", + "fr": "Central", + "pt": "Central" + }, + "subtitle": { + "en": "Right in the centre of things!", + "de": "Mitten im Zentrum des Geschehens!", + "es": "Justo en el centro de todo", + "fr": "En plein cœur de l'action !", + "pt": "Bem no centro das coisas!" + }, + "code": "central", + "image": "https://storeganise.s3.amazonaws.com/images/20220621/sites/site5.png", + "address": { + "en": "212 S Las Vegas Blvd, \nLas Vegas", + "de": "212 S Las Vegas Blvd, \nLas Vegas", + "es": "212 S Las Vegas Blvd, \nLas Vegas", + "fr": "212 S Las Vegas Blvd, \nLas Vegas", + "pt": "212 S Las Vegas Blvd, \nLas Vegas" + }, + "phone": "702 555 1234", + "email": "central@storeganise.com", + "info": { + "en": "Perfect for all your storage needs with a variety of sizes. \n\nNot sure what unit you need? Give us a call or [**send us an email**](mailto:hello@storeganise.com) and we'll help you decide!", + "de": "Nicht sicher, welche Abteilgröße Sie brauchen? Rufen Sie uns an oder [**schreiben Sie uns eine E-Mail**](mailto:hello@storeganise.com) und wir helfen Ihnen!", + "es": "¿No está seguro de qué unidad necesita? Llámenos o [**envíenos un correo electrónico**](mailto:hello@storeganise.com) y le ayudaremos a decidir.", + "fr": "Pas sûr de quelles unités vous avez besoin? Applez-nous ou [**envoyez-nous un email**](mailto:hello@storeganise.com) et nous vous aiderons à décider!", + "pt": "Não tem certeza de qual unidade você precisa? Ligue para nós ou [**envie-nos um e-mail**](mailto:hello@storeganise.com) e podemos ajudar a decidir!" + }, + "hours": { + "en": "| | | | |\n|:--------:|:--------:|:----:|:--------:|\n| Mon | 09:00 | ~ | 18:00 |\n| Tue | 09:00 | ~ | 18:00 |\n| Wed | 09:00 | ~ | 18:00 |\n| Thu | 09:00 | ~ | 18:00 |\n| Fri | 09:00 | ~ | 18:00 |\n| Sat | 09:00 | ~ | 18:00 |\n| Sun | Closed | | |", + "es": "| | | | |\n|:--------:|:--------:|:----:|:--------:|\n| Lu | 09:00 | ~ | 18:00 |\n| Ma | 09:00 | ~ | 18:00 |\n| Mi | 09:00 | ~ | 18:00 |\n| Ju | 09:00 | ~ | 18:00 |\n| Vi | 09:00 | ~ | 18:00 |\n| Sa | 09:00 | ~ | 18:00 |\n| Do | Cerrado | | | ", + "fr": "| | | | |\n| :--------: |:--------:|:----:| :--------:|\n| Lun | 09:00 | ~ | 18:00 |\n| Mar | 09:00 | ~ | 18:00 |\n| Mer | 09:00 | ~ | 18:00 |\n| Jeu | 09:00 | ~ | 18:00 |\n| Ven | 09:00 | ~ | 18:00 |\n| Sam | 09:00 | ~ | 18:00 |\n| Dim | Fermé | | | ", + "pt": "| | | | |\n|:--------:|:--------:|:----:|:--------:|\n| Lu | 09:00 | ~ | 18:00 |\n| Ma | 09:00 | ~ | 18:00 |\n| Mi | 09:00 | ~ | 18:00 |\n| Ju | 09:00 | ~ | 18:00 |\n| Vi | 09:00 | ~ | 18:00 |\n| Sa | 09:00 | ~ | 18:00 |\n| Do | Cerrado | | | ", + "de": "| | | | |\n|:--------:|:--------:|:----:|:--------:|\n| Mo | 09:00 | ~ | 18:00 |\n| Di | 09:00 | ~ | 18:00 |\n| Mi | 09:00 | ~ | 18:00 |\n| Do| 09:00 | ~ | 18:00 |\n| Fr| 09:00 | ~ | 18:00 |\n| Sa | 09:00 | ~ | 18:00 |\n| So | Geschlossen| | | \n " + }, + "lat": 36.1683983, + "lng": -115.1519712, + "enableMoveInAgreement": true, + "occupancy": { + "blocked": 3, + "available": 186, + "reserved": 1, + "occupied": 2, + "overdue": 2 + }, + "unitTypeGroups": [], + "unitTypes": [ + { + "id": "66ce753e7405aa00158ba95b", + "title": { + "en": "5' x 5' (25 sq ft)", + "de": "5' x 5' (25 p2)", + "es": "5 x 5 (2.5 m²)", + "fr": "5 x 5 (2.5 m²)", + "pt": "5' x 5' (25 p2)" + }, + "subtitle": { + "en": "Telephone box", + "de": "Telefonzelle", + "es": "Cabina telefónica", + "fr": "Cabine téléphonique", + "pt": "Cabine Telefonica" + }, + "info": { + "en": "# More about this unit\n- 3' wide roll up door\n- 8' ceiling\n- Large elevators\n- Temperature & humidity controlled\n\nLearn more " + }, + "code": "5x5", + "siteId": "66ce753e7405aa00158ba679", + "image": "https://storeganise.s3.amazonaws.com/images/20220621/unit-types/5x5.png", + "deposit": 50, + "price": 50, + "price_dynamic": 50, + "availability": { + "available": 90, + "total": 92 + }, + "occupancy": { + "available": 90, + "blocked": 1, + "reserved": 1, + "occupied": 0, + "overdue": 0 + }, + "tagIds": [ + "66ce753e7405aa00158ba513", + "66ce753e7405aa00158ba50d", + "66ce753e7405aa00158ba50e", + "66ce753e7405aa00158ba50f" + ], + "charges": [ + { + "id": "66ce753e7405aa00158ba95c", + "type": "movein", + "title": { + "en": "Admin fee" + }, + "amount": 10, + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "occupancyPrices": [], + "systemCharges": { + "unitDeposit": {}, + "unitRent": {} + }, + "labels": [], + "created": "2024-05-17T10:13:33.970Z", + "updated": "2024-09-04T06:43:39.393Z", + "tags": [ + { + "id": "66ce753e7405aa00158ba513", + "title": { + "en": "Wi-Fi", + "es": "Wi-Fi", + "de": "WLAN", + "fr": "Wi-Fi", + "pt": "Wi-Fi" + }, + "icon": "fas fa-rss" + }, + { + "id": "66ce753e7405aa00158ba50d", + "title": { + "en": "Air conditioning", + "es": "Aire acondicionado", + "de": "Klimaanlage", + "fr": "Air conditionné", + "pt": "Ar condicionado " + }, + "icon": "fas fa-asterisk" + }, + { + "id": "66ce753e7405aa00158ba50e", + "title": { + "en": "Free parking", + "es": "Estacionamiento gratis", + "de": "Gratis Parkplatz", + "fr": "Parking gratuit", + "pt": "Estacionamento Gratuito" + }, + "icon": "fas fa-car" + }, + { + "id": "66ce753e7405aa00158ba50f", + "title": { + "en": "Free move in service", + "es": "Servicio de traslado gratuito", + "de": "Kostenloser Umzugsservice", + "fr": "Service d'emménagement gratuit", + "pt": "Serviço de mudança" + }, + "icon": "fas fa-truck" + } + ] + }, + { + "id": "66ce753e7405aa00158ba95d", + "title": { + "en": "5' x 10' (50 sqft)", + "de": "5' x 10' (50 p2)", + "es": "5' x 10' (5 m²)", + "fr": "5' x 10' (5 m²)", + "pt": "5' x 10' (50 p2)" + }, + "subtitle": { + "en": "1 bedroom apartment", + "de": "1-Zimmer-Wohnung", + "es": "Apartamento de 1 quarto", + "fr": "Appartement 1 chambre", + "pt": "Apartamento de 1 quarto" + }, + "info": { + "en": "# More about this unit\n- 3' wide roll up door\n- 8' ceiling\n- Large elevators\n- Temperature & humidity controlled\n\nLearn more " + }, + "code": "5x10", + "siteId": "66ce753e7405aa00158ba679", + "image": "https://storeganise.s3.amazonaws.com/images/20220621/unit-types/5x10.png", + "deposit": 179, + "price": 179, + "price_dynamic": 179, + "availability": { + "available": 68, + "total": 70 + }, + "occupancy": { + "available": 68, + "blocked": 2, + "reserved": 0, + "occupied": 0, + "overdue": 0 + }, + "tagIds": [ + "66ce753e7405aa00158ba513", + "66ce753e7405aa00158ba510", + "66ce753e7405aa00158ba50e", + "66ce753e7405aa00158ba50d" + ], + "charges": [ + { + "id": "66ce753e7405aa00158ba95e", + "type": "movein", + "title": { + "en": "Admin fee" + }, + "amount": 10, + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "occupancyPrices": [], + "systemCharges": { + "unitDeposit": {}, + "unitRent": {} + }, + "labels": [], + "created": "2024-05-17T10:13:33.970Z", + "updated": "2024-09-04T06:43:14.600Z", + "tags": [ + { + "id": "66ce753e7405aa00158ba513", + "title": { + "en": "Wi-Fi", + "es": "Wi-Fi", + "de": "WLAN", + "fr": "Wi-Fi", + "pt": "Wi-Fi" + }, + "icon": "fas fa-rss" + }, + { + "id": "66ce753e7405aa00158ba510", + "title": { + "en": "Ground floor", + "es": "Planta baja", + "de": "Ebenerdig", + "fr": "Rez-de-chaussée", + "pt": "Garagem" + }, + "icon": "fas fa-warehouse" + }, + { + "id": "66ce753e7405aa00158ba50e", + "title": { + "en": "Free parking", + "es": "Estacionamiento gratis", + "de": "Gratis Parkplatz", + "fr": "Parking gratuit", + "pt": "Estacionamento Gratuito" + }, + "icon": "fas fa-car" + }, + { + "id": "66ce753e7405aa00158ba50d", + "title": { + "en": "Air conditioning", + "es": "Aire acondicionado", + "de": "Klimaanlage", + "fr": "Air conditionné", + "pt": "Ar condicionado " + }, + "icon": "fas fa-asterisk" + } + ] + }, + { + "id": "66ce753e7405aa00158ba95f", + "title": { + "en": "10' x 10' (100 sqft)", + "de": "10' x 10' (100 p2)", + "es": "10' x 10' (9 m²)", + "fr": "10' x 10' (9 m²)", + "pt": "10' x 10' (100 p2)" + }, + "subtitle": { + "en": "2 bedroom apartment", + "de": "2-Zimmer-Wohnung", + "es": "Apartamento de 2 quartos", + "fr": "Appartement 2 chambres", + "pt": "Apartamento de 2 quartos" + }, + "info": { + "en": "# More about this unit\n- 3' wide roll up door\n- 8' ceiling\n- Large elevators\n- Temperature & humidity controlled\n- Free wifi throughout\n\nLearn more " + }, + "code": "10x10", + "siteId": "66ce753e7405aa00158ba679", + "image": "https://storeganise.s3.amazonaws.com/images/20220621/unit-types/10x10.png", + "deposit": 239, + "price": 239, + "price_dynamic": 239, + "availability": { + "available": 14, + "total": 16 + }, + "occupancy": { + "available": 14, + "blocked": 0, + "reserved": 0, + "occupied": 2, + "overdue": 2 + }, + "tagIds": [ + "66ce753e7405aa00158ba50d", + "66ce753e7405aa00158ba50f", + "66ce753e7405aa00158ba513" + ], + "charges": [ + { + "id": "66ce753e7405aa00158ba960", + "type": "movein", + "title": { + "en": "Admin fee" + }, + "amount": 10, + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "occupancyPrices": [], + "systemCharges": { + "unitDeposit": {}, + "unitRent": {} + }, + "labels": [], + "created": "2024-05-17T10:13:33.970Z", + "updated": "2024-09-04T06:43:14.609Z", + "tags": [ + { + "id": "66ce753e7405aa00158ba50d", + "title": { + "en": "Air conditioning", + "es": "Aire acondicionado", + "de": "Klimaanlage", + "fr": "Air conditionné", + "pt": "Ar condicionado " + }, + "icon": "fas fa-asterisk" + }, + { + "id": "66ce753e7405aa00158ba50f", + "title": { + "en": "Free move in service", + "es": "Servicio de traslado gratuito", + "de": "Kostenloser Umzugsservice", + "fr": "Service d'emménagement gratuit", + "pt": "Serviço de mudança" + }, + "icon": "fas fa-truck" + }, + { + "id": "66ce753e7405aa00158ba513", + "title": { + "en": "Wi-Fi", + "es": "Wi-Fi", + "de": "WLAN", + "fr": "Wi-Fi", + "pt": "Wi-Fi" + }, + "icon": "fas fa-rss" + } + ] + }, + { + "id": "66ce753e7405aa00158ba961", + "title": { + "en": "10' x 20' (200 sqft)", + "de": "10' x 20' (200 sqft)", + "es": "10' x 20' (200 sqft)", + "fr": "10' x 20' (200 sqft)", + "pt": "10' x 20' (200 p2)" + }, + "subtitle": { + "en": "Family house", + "de": "Einfamilienhaus", + "es": "Casa inteira", + "fr": "Maison familiale", + "pt": "Casa inteira" + }, + "info": { + "en": "# More about this unit\n- 3' wide roll up door\n- 8' ceiling\n- Large elevators\n- Temperature & humidity controlled\n- Free wifi throughout\n\nLearn more " + }, + "code": "10x20", + "siteId": "66ce753e7405aa00158ba679", + "image": "https://storeganise.s3.amazonaws.com/images/20220621/unit-types/10x20.png", + "deposit": 300, + "price": 300, + "price_dynamic": 300, + "availability": { + "available": 14, + "total": 14 + }, + "occupancy": { + "available": 14, + "blocked": 0, + "reserved": 0, + "occupied": 0, + "overdue": 0 + }, + "tagIds": [ + "66ce753e7405aa00158ba50d", + "66ce753e7405aa00158ba50f", + "66ce753e7405aa00158ba50e", + "66ce753e7405aa00158ba510", + "66ce753e7405aa00158ba513" + ], + "charges": [ + { + "id": "66ce753e7405aa00158ba962", + "type": "movein", + "title": { + "en": "Admin fee" + }, + "amount": 10, + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "occupancyPrices": [], + "systemCharges": { + "unitDeposit": {}, + "unitRent": {} + }, + "labels": [], + "created": "2024-05-17T10:13:33.971Z", + "updated": "2024-09-04T06:43:14.620Z", + "tags": [ + { + "id": "66ce753e7405aa00158ba50d", + "title": { + "en": "Air conditioning", + "es": "Aire acondicionado", + "de": "Klimaanlage", + "fr": "Air conditionné", + "pt": "Ar condicionado " + }, + "icon": "fas fa-asterisk" + }, + { + "id": "66ce753e7405aa00158ba50f", + "title": { + "en": "Free move in service", + "es": "Servicio de traslado gratuito", + "de": "Kostenloser Umzugsservice", + "fr": "Service d'emménagement gratuit", + "pt": "Serviço de mudança" + }, + "icon": "fas fa-truck" + }, + { + "id": "66ce753e7405aa00158ba50e", + "title": { + "en": "Free parking", + "es": "Estacionamiento gratis", + "de": "Gratis Parkplatz", + "fr": "Parking gratuit", + "pt": "Estacionamento Gratuito" + }, + "icon": "fas fa-car" + }, + { + "id": "66ce753e7405aa00158ba510", + "title": { + "en": "Ground floor", + "es": "Planta baja", + "de": "Ebenerdig", + "fr": "Rez-de-chaussée", + "pt": "Garagem" + }, + "icon": "fas fa-warehouse" + }, + { + "id": "66ce753e7405aa00158ba513", + "title": { + "en": "Wi-Fi", + "es": "Wi-Fi", + "de": "WLAN", + "fr": "Wi-Fi", + "pt": "Wi-Fi" + }, + "icon": "fas fa-rss" + } + ] + } + ], + "products": [ + { + "id": "66ce753e7405aa00158ba682", + "title": { + "en": "Box", + "de": "Schachtel", + "es": "Caja", + "fr": "Box", + "pt": "Caixa" + }, + "description": { + "en": "Carton box: 40 x 60 x 30 cm", + "de": "Kartonschachtel: 40 x 60 x 30 cm", + "es": "Caja: 40 x 60 x 30 cm", + "fr": "Box carton: 40 x 60 x 30 cm", + "pt": "Caixa: 40 x 60 x 30 cm" + }, + "price": 5, + "displayPrice": { + "en": "$5 per box", + "de": "€5 pro Karton", + "es": "$5 por caja", + "fr": "$5 par box", + "pt": "$5 por caixa" + }, + "image": "https://storeganise.s3.amazonaws.com/images/20220621/products-services/cardboard-box-medium.png", + "type": "movein", + "showInApp": true, + "hidden": false, + "trackInventory": true, + "inventory": 500, + "maxQuantity": 30, + "accountingCodeId": "66ce753e7405aa00158ba519" + }, + { + "id": "66ce753e7405aa00158ba683", + "title": { + "en": "Packing tape", + "de": "Packband", + "es": "Cinta de embalaje", + "fr": "Ruban adhésif d'emballage", + "pt": "Fita adesiva" + }, + "description": { + "en": "Width: 2.5\". Includes tape holder & cutter.", + "de": "Breite 6 cm; inklusive Abroller & Schneider", + "es": "Ancho: 2.5\"", + "fr": "Largeur: 2.5\". cutter et support inclus", + "pt": "Largura: 2,5 \". Inclui suporte e cortador de fita." + }, + "price": 9.99, + "displayPrice": { + "en": "$9.99 per roll", + "de": "$9.99 pro Rolle", + "es": "$9.99 por rollo", + "fr": "$9.99 par rouleau", + "pt": "$9.99 por rollo" + }, + "image": "https://storeganise.s3.amazonaws.com/images/20220621/products-services/tape-dispenser.png", + "type": "movein", + "showInApp": true, + "hidden": false, + "trackInventory": true, + "inventory": 200, + "maxQuantity": 10, + "accountingCodeId": "66ce753e7405aa00158ba519" + }, + { + "id": "66ce753e7405aa00158ba684", + "title": { + "en": "Move-in service", + "de": "Einzugsservice", + "es": "Servicio de mudanza", + "fr": "Service d'emménagement", + "pt": "Serviço de mudança" + }, + "description": { + "en": "Let us help you move your stuff. Get a van with a driver for free for 2 hours!", + "de": "Wir helfen Ihnen beim Umzug! Holen Sie sich einen Transporter mit Fahrer - die ersten 2 Stunden sind gratis!", + "es": "Permítanos ayudarlo a mover sus cosas. ¡Obtenga una camioneta con conductor gratis por 2 horas!", + "fr": "Laissez-nous vous aider à transporter vos affaires. Profitez d'une camionette avec chauffeur gratuit pour 2 heures!", + "pt": "Deixe-nos ajudá-lo a mover suas coisas. Pegue uma van com motorista gratuitamente por 2 horas!" + }, + "price": 0, + "displayPrice": { + "en": "Free!", + "de": "Kostenlos!", + "es": "Gratis!", + "fr": "Gratuit!", + "pt": "Gratuito!" + }, + "image": "https://storeganise.s3.amazonaws.com/images/20220621/products-services/truck.png", + "type": "movein", + "showInApp": true, + "hidden": false, + "maxQuantity": 1, + "accountingCodeId": "66ce753e7405aa00158ba519" + }, + { + "id": "66ce753e7405aa00158ba685", + "title": { + "en": "Insurance", + "de": "Versicherung", + "es": "Seguro", + "fr": "Assurance", + "pt": "Seguro" + }, + "description": { + "en": "Insurance covering up to $5,000 worth of items", + "de": "Versicherung für Gegenstände im Wert von bis zu $5.000", + "es": "Cobertura de seguro de hasta $5.000 en pertenencias", + "fr": "Une assurance couvrant jusqu'à $5000 $ d'articles.", + "pt": "Cobertura de seguro de até $5.000 em pertences" + }, + "price": 10, + "displayPrice": { + "en": "$10 per month", + "de": "$10 pro Monat", + "es": "$10 por mes", + "fr": "$10/mois", + "pt": "$10 por mês" + }, + "image": "https://storeganise.s3.amazonaws.com/images/20220621/products-services/umbrella.png", + "type": "recurring", + "showInApp": true, + "hidden": false, + "maxQuantity": 5, + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "labels": [ + "66ce753e7405aa00158ba52b" + ], + "measure": "ft", + "area_gross": 50000, + "area_net": 45000, + "excludedBookingDates": [], + "bookingDaysAhead": 25, + "bookingCutoffTime": 23, + "created": "2020-05-21T02:29:38.090Z", + "updated": "2024-09-04T06:43:39.404Z", + "availability": { + "66ce753e7405aa00158ba95b": { + "available": 90, + "total": 92 + }, + "66ce753e7405aa00158ba95d": { + "available": 68, + "total": 70 + }, + "66ce753e7405aa00158ba95f": { + "available": 14, + "total": 16 + }, + "66ce753e7405aa00158ba961": { + "available": 14, + "total": 14 + } + } + }, + { + "id": "66ce753e7405aa00158ba68e", + "title": { + "en": "Spring Valley", + "de": "Spring Valley", + "es": "Spring Valley", + "fr": "Spring Valley", + "pt": "Spring Valley" + }, + "subtitle": { + "en": "Our biggest location with more than 1,000 units!", + "de": "Unser größter Standort mit mehr als 1.000 Einheiten!", + "es": "Nuestra mayor ubicación con más de 1.000 unidades", + "fr": "Notre plus grand emplacement avec plus de 1 000 unités !", + "pt": "Nossa maior localização com mais de 1.000 unidades!" + }, + "code": "spring_valley", + "image": "https://storeganise.s3.amazonaws.com/images/20220621/sites/site4.png", + "address": { + "en": "5355 S Rainbow Blvd, \nLas Vegas", + "de": "5355 S Rainbow Blvd, \nLas Vegas", + "es": "5355 S Rainbow Blvd, \nLas Vegas", + "fr": "5355 S Rainbow Blvd, \nLas Vegas", + "pt": "5355 S Rainbow Blvd, \nLas Vegas" + }, + "phone": "702 555 9876", + "email": "springvalley@storeganise.com", + "info": { + "en": "Not sure what unit you need? Give us a call or [send us an email](mailto:hello@storeganise.com) and we'll help you decide!\n\n### Opening hours\n| Mon | 09:00 | ~ | 18:00 |\n| :-------- |:--------:|:----:| :--------:|\n| Tue | 09:00 | ~ | 18:00 |\n| Wed | 09:00 | ~ | 18:00 |\n| Thu | 09:00 | ~ | 18:00 |\n| Fri | 09:00 | ~ | 18:00 |\n| Sat | 08:00 | ~ | 21:00 |\n| Sun | 08:00 | ~ | 19:00 | \n \n\n\nLearn more about our Spring Valley location" + }, + "hours": {}, + "lat": 36.0956155, + "lng": -115.253143, + "enableMoveInAgreement": true, + "occupancy": { + "blocked": 1, + "available": 164, + "reserved": 1, + "occupied": 1, + "overdue": 1 + }, + "unitTypeGroups": [], + "unitTypes": [ + { + "id": "66ce753e7405aa00158ba963", + "title": { + "en": "5' x 5' (25 sq ft)", + "de": "5' x 5' (25 p2)", + "es": "5 x 5 (2.5 m²)", + "fr": "5 x 5 (2.5 m²)", + "pt": "5' x 5' (25 p2)" + }, + "subtitle": { + "en": "Telephone box", + "de": "Telefonzelle", + "es": "Cabina telefónica", + "fr": "Cabine téléphonique", + "pt": "Cabine Telefonica" + }, + "info": { + "en": "# More about this unit\n- 3' wide roll up door\n- 8' ceiling\n- Large elevators\n- Temperature & humidity controlled\n\nLearn more " + }, + "code": "5x5", + "siteId": "66ce753e7405aa00158ba68e", + "image": "https://storeganise.s3.amazonaws.com/images/20220621/unit-types/5x5.png", + "deposit": 99, + "price": 50, + "price_dynamic": 50, + "availability": { + "available": 66, + "total": 66 + }, + "occupancy": { + "available": 66, + "blocked": 0, + "reserved": 0, + "occupied": 0, + "overdue": 0 + }, + "tagIds": [ + "66ce753e7405aa00158ba513", + "66ce753e7405aa00158ba50d", + "66ce753e7405aa00158ba50e", + "66ce753e7405aa00158ba50f" + ], + "charges": [ + { + "id": "66ce753e7405aa00158ba964", + "type": "movein", + "title": { + "en": "Admin fee" + }, + "amount": 10, + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "occupancyPrices": [], + "systemCharges": { + "unitDeposit": {}, + "unitRent": {} + }, + "labels": [], + "created": "2024-05-17T10:13:34.021Z", + "updated": "2024-09-03T15:45:10.864Z", + "tags": [ + { + "id": "66ce753e7405aa00158ba513", + "title": { + "en": "Wi-Fi", + "es": "Wi-Fi", + "de": "WLAN", + "fr": "Wi-Fi", + "pt": "Wi-Fi" + }, + "icon": "fas fa-rss" + }, + { + "id": "66ce753e7405aa00158ba50d", + "title": { + "en": "Air conditioning", + "es": "Aire acondicionado", + "de": "Klimaanlage", + "fr": "Air conditionné", + "pt": "Ar condicionado " + }, + "icon": "fas fa-asterisk" + }, + { + "id": "66ce753e7405aa00158ba50e", + "title": { + "en": "Free parking", + "es": "Estacionamiento gratis", + "de": "Gratis Parkplatz", + "fr": "Parking gratuit", + "pt": "Estacionamento Gratuito" + }, + "icon": "fas fa-car" + }, + { + "id": "66ce753e7405aa00158ba50f", + "title": { + "en": "Free move in service", + "es": "Servicio de traslado gratuito", + "de": "Kostenloser Umzugsservice", + "fr": "Service d'emménagement gratuit", + "pt": "Serviço de mudança" + }, + "icon": "fas fa-truck" + } + ] + }, + { + "id": "66ce753e7405aa00158ba965", + "title": { + "en": "5' x 10' (50 sqft)", + "de": "5' x 10' (50 p2)", + "es": "5' x 10' (5 m²)", + "fr": "5' x 10' (5 m²)", + "pt": "5' x 10' (50 p2)" + }, + "subtitle": { + "en": "1 bedroom apartment", + "de": "1-Zimmer-Wohnung", + "es": "Apartamento de 1 quarto", + "fr": "Appartement 1 chambre", + "pt": "Apartamento de 1 quarto" + }, + "info": { + "en": "# More about this unit\n- 3' wide roll up door\n- 8' ceiling\n- Large elevators\n- Temperature & humidity controlled\n\nLearn more " + }, + "code": "5x10", + "siteId": "66ce753e7405aa00158ba68e", + "image": "https://storeganise.s3.amazonaws.com/images/20220621/unit-types/5x10.png", + "deposit": 179, + "price": 179, + "price_dynamic": 179, + "availability": { + "available": 70, + "total": 73 + }, + "occupancy": { + "available": 70, + "blocked": 1, + "reserved": 1, + "occupied": 1, + "overdue": 1 + }, + "tagIds": [ + "66ce753e7405aa00158ba513", + "66ce753e7405aa00158ba510", + "66ce753e7405aa00158ba50e", + "66ce753e7405aa00158ba50d" + ], + "charges": [ + { + "id": "66ce753e7405aa00158ba966", + "type": "movein", + "title": { + "en": "Admin fee" + }, + "amount": 10, + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "occupancyPrices": [], + "systemCharges": { + "unitDeposit": {}, + "unitRent": {} + }, + "labels": [], + "created": "2024-05-17T10:13:34.022Z", + "updated": "2024-09-04T06:43:56.913Z", + "tags": [ + { + "id": "66ce753e7405aa00158ba513", + "title": { + "en": "Wi-Fi", + "es": "Wi-Fi", + "de": "WLAN", + "fr": "Wi-Fi", + "pt": "Wi-Fi" + }, + "icon": "fas fa-rss" + }, + { + "id": "66ce753e7405aa00158ba510", + "title": { + "en": "Ground floor", + "es": "Planta baja", + "de": "Ebenerdig", + "fr": "Rez-de-chaussée", + "pt": "Garagem" + }, + "icon": "fas fa-warehouse" + }, + { + "id": "66ce753e7405aa00158ba50e", + "title": { + "en": "Free parking", + "es": "Estacionamiento gratis", + "de": "Gratis Parkplatz", + "fr": "Parking gratuit", + "pt": "Estacionamento Gratuito" + }, + "icon": "fas fa-car" + }, + { + "id": "66ce753e7405aa00158ba50d", + "title": { + "en": "Air conditioning", + "es": "Aire acondicionado", + "de": "Klimaanlage", + "fr": "Air conditionné", + "pt": "Ar condicionado " + }, + "icon": "fas fa-asterisk" + } + ] + }, + { + "id": "66ce753e7405aa00158ba967", + "title": { + "en": "10' x 10' (100 sqft)", + "de": "10' x 10' (100 p2)", + "es": "10' x 10' (9 m²)", + "fr": "10' x 10' (9 m²)", + "pt": "10' x 10' (100 p2)" + }, + "subtitle": { + "en": "2 bedroom apartment", + "de": "2-Zimmer-Wohnung", + "es": "Apartamento de 2 quartos", + "fr": "Appartement 2 chambres", + "pt": "Apartamento de 2 quartos" + }, + "info": { + "en": "# More about this unit\n- 3' wide roll up door\n- 8' ceiling\n- Large elevators\n- Temperature & humidity controlled\n- Free wifi throughout\n\nLearn more " + }, + "code": "10x10", + "siteId": "66ce753e7405aa00158ba68e", + "image": "https://storeganise.s3.amazonaws.com/images/20220621/unit-types/10x10.png", + "deposit": 239, + "price": 239, + "price_dynamic": 239, + "availability": { + "available": 14, + "total": 14 + }, + "occupancy": { + "available": 14, + "blocked": 0, + "reserved": 0, + "occupied": 0, + "overdue": 0 + }, + "tagIds": [ + "66ce753e7405aa00158ba50d", + "66ce753e7405aa00158ba50f", + "66ce753e7405aa00158ba513" + ], + "charges": [ + { + "id": "66ce753e7405aa00158ba968", + "type": "movein", + "title": { + "en": "Admin fee" + }, + "amount": 10, + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "occupancyPrices": [], + "systemCharges": { + "unitDeposit": {}, + "unitRent": {} + }, + "labels": [], + "created": "2024-05-17T10:13:34.022Z", + "updated": "2024-09-03T15:45:10.914Z", + "tags": [ + { + "id": "66ce753e7405aa00158ba50d", + "title": { + "en": "Air conditioning", + "es": "Aire acondicionado", + "de": "Klimaanlage", + "fr": "Air conditionné", + "pt": "Ar condicionado " + }, + "icon": "fas fa-asterisk" + }, + { + "id": "66ce753e7405aa00158ba50f", + "title": { + "en": "Free move in service", + "es": "Servicio de traslado gratuito", + "de": "Kostenloser Umzugsservice", + "fr": "Service d'emménagement gratuit", + "pt": "Serviço de mudança" + }, + "icon": "fas fa-truck" + }, + { + "id": "66ce753e7405aa00158ba513", + "title": { + "en": "Wi-Fi", + "es": "Wi-Fi", + "de": "WLAN", + "fr": "Wi-Fi", + "pt": "Wi-Fi" + }, + "icon": "fas fa-rss" + } + ] + }, + { + "id": "66ce753e7405aa00158ba969", + "title": { + "en": "10' x 20' (200 sqft)", + "de": "10' x 20' (200 sqft)", + "es": "10' x 20' (200 sqft)", + "fr": "10' x 20' (200 sqft)", + "pt": "10' x 20' (200 p2)" + }, + "subtitle": { + "en": "Family house", + "de": "Einfamilienhaus", + "es": "Casa inteira", + "fr": "Maison familiale", + "pt": "Casa inteira" + }, + "info": { + "en": "# More about this unit\n- 3' wide roll up door\n- 8' ceiling\n- Large elevators\n- Temperature & humidity controlled\n- Free wifi throughout\n\nLearn more " + }, + "code": "10x20", + "siteId": "66ce753e7405aa00158ba68e", + "image": "https://storeganise.s3.amazonaws.com/images/20220621/unit-types/10x20.png", + "deposit": 300, + "price": 300, + "price_dynamic": 300, + "availability": { + "available": 14, + "total": 14 + }, + "occupancy": { + "available": 14, + "blocked": 0, + "reserved": 0, + "occupied": 0, + "overdue": 0 + }, + "tagIds": [ + "66ce753e7405aa00158ba50d", + "66ce753e7405aa00158ba50f", + "66ce753e7405aa00158ba50e", + "66ce753e7405aa00158ba510", + "66ce753e7405aa00158ba513" + ], + "charges": [ + { + "id": "66ce753e7405aa00158ba96a", + "type": "movein", + "title": { + "en": "Admin fee" + }, + "amount": 10, + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "occupancyPrices": [], + "systemCharges": { + "unitDeposit": {}, + "unitRent": {} + }, + "labels": [], + "created": "2024-05-17T10:13:34.023Z", + "updated": "2024-09-03T15:45:10.901Z", + "tags": [ + { + "id": "66ce753e7405aa00158ba50d", + "title": { + "en": "Air conditioning", + "es": "Aire acondicionado", + "de": "Klimaanlage", + "fr": "Air conditionné", + "pt": "Ar condicionado " + }, + "icon": "fas fa-asterisk" + }, + { + "id": "66ce753e7405aa00158ba50f", + "title": { + "en": "Free move in service", + "es": "Servicio de traslado gratuito", + "de": "Kostenloser Umzugsservice", + "fr": "Service d'emménagement gratuit", + "pt": "Serviço de mudança" + }, + "icon": "fas fa-truck" + }, + { + "id": "66ce753e7405aa00158ba50e", + "title": { + "en": "Free parking", + "es": "Estacionamiento gratis", + "de": "Gratis Parkplatz", + "fr": "Parking gratuit", + "pt": "Estacionamento Gratuito" + }, + "icon": "fas fa-car" + }, + { + "id": "66ce753e7405aa00158ba510", + "title": { + "en": "Ground floor", + "es": "Planta baja", + "de": "Ebenerdig", + "fr": "Rez-de-chaussée", + "pt": "Garagem" + }, + "icon": "fas fa-warehouse" + }, + { + "id": "66ce753e7405aa00158ba513", + "title": { + "en": "Wi-Fi", + "es": "Wi-Fi", + "de": "WLAN", + "fr": "Wi-Fi", + "pt": "Wi-Fi" + }, + "icon": "fas fa-rss" + } + ] + } + ], + "products": [ + { + "id": "66ce753e7405aa00158ba693", + "title": { + "en": "Box", + "de": "Schachtel", + "es": "Caja", + "fr": "Box", + "pt": "Caixa" + }, + "description": { + "en": "Carton box: 40 x 60 x 30 cm", + "de": "Kartonschachtel: 40 x 60 x 30 cm", + "es": "Caja: 40 x 60 x 30 cm", + "fr": "Box carton: 40 x 60 x 30 cm", + "pt": "Caixa: 40 x 60 x 30 cm" + }, + "price": 5, + "displayPrice": { + "en": "$5 per box", + "de": "€5 pro Karton", + "es": "$5 por caja", + "fr": "$5 par box", + "pt": "$5 por caixa" + }, + "image": "https://storeganise.s3.amazonaws.com/images/20220621/products-services/cardboard-box-medium.png", + "type": "movein", + "showInApp": true, + "hidden": false, + "trackInventory": true, + "inventory": 500, + "maxQuantity": 30, + "accountingCodeId": "66ce753e7405aa00158ba519" + }, + { + "id": "66ce753e7405aa00158ba694", + "title": { + "en": "Packing tape", + "de": "Packband", + "es": "Cinta de embalaje", + "fr": "Ruban adhésif d'emballage", + "pt": "Fita adesiva" + }, + "description": { + "en": "Width: 2.5\". Includes tape holder & cutter.", + "de": "Breite 6 cm; inklusive Abroller & Schneider", + "es": "Ancho: 2.5\"", + "fr": "Largeur: 2.5\". cutter et support inclus", + "pt": "Largura: 2,5 \". Inclui suporte e cortador de fita." + }, + "price": 9.99, + "displayPrice": { + "en": "$9.99 per roll", + "de": "$9.99 pro Rolle", + "es": "$9.99 por rollo", + "fr": "$9.99 par rouleau", + "pt": "$9.99 por rollo" + }, + "image": "https://storeganise.s3.amazonaws.com/images/20220621/products-services/tape-dispenser.png", + "type": "movein", + "showInApp": true, + "hidden": false, + "trackInventory": true, + "inventory": 200, + "maxQuantity": 10, + "accountingCodeId": "66ce753e7405aa00158ba519" + }, + { + "id": "66ce753e7405aa00158ba695", + "title": { + "en": "Move-in service", + "de": "Einzugsservice", + "es": "Servicio de mudanza", + "fr": "Service d'emménagement", + "pt": "Serviço de mudança" + }, + "description": { + "en": "Let us help you move your stuff. Get a van with a driver for free for 2 hours!", + "de": "Wir helfen Ihnen beim Umzug! Holen Sie sich einen Transporter mit Fahrer - die ersten 2 Stunden sind gratis!", + "es": "Permítanos ayudarlo a mover sus cosas. ¡Obtenga una camioneta con conductor gratis por 2 horas!", + "fr": "Laissez-nous vous aider à transporter vos affaires. Profitez d'une camionette avec chauffeur gratuit pour 2 heures!", + "pt": "Deixe-nos ajudá-lo a mover suas coisas. Pegue uma van com motorista gratuitamente por 2 horas!" + }, + "price": 0, + "displayPrice": { + "en": "Free!", + "de": "Kostenlos!", + "es": "Gratis!", + "fr": "Gratuit!", + "pt": "Gratuito!" + }, + "image": "https://storeganise.s3.amazonaws.com/images/20220621/products-services/truck.png", + "type": "movein", + "showInApp": true, + "hidden": false, + "maxQuantity": 1, + "accountingCodeId": "66ce753e7405aa00158ba519" + }, + { + "id": "66ce753e7405aa00158ba696", + "title": { + "en": "Insurance", + "de": "Versicherung", + "es": "Seguro", + "fr": "Assurance", + "pt": "Seguro" + }, + "description": { + "en": "Insurance covering up to $5,000 worth of items", + "de": "Versicherung für Gegenstände im Wert von bis zu $5.000", + "es": "Cobertura de seguro de hasta $5.000 en pertenencias", + "fr": "Une assurance couvrant jusqu'à $5000 $ d'articles.", + "pt": "Cobertura de seguro de até $5.000 em pertences" + }, + "price": 10, + "displayPrice": { + "en": "$10 per month", + "de": "$10 pro Monat", + "es": "$10 por mes", + "fr": "$10/mois", + "pt": "$10 por mês" + }, + "image": "https://storeganise.s3.amazonaws.com/images/20220621/products-services/umbrella.png", + "type": "recurring", + "showInApp": true, + "hidden": false, + "maxQuantity": 5, + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "labels": [ + "66ce753e7405aa00158ba52e" + ], + "measure": "ft", + "area_gross": 10000, + "area_net": 8000, + "excludedBookingDates": [], + "created": "2020-05-21T02:29:38.098Z", + "updated": "2024-09-04T06:43:56.920Z", + "availability": { + "66ce753e7405aa00158ba963": { + "available": 66, + "total": 66 + }, + "66ce753e7405aa00158ba965": { + "available": 70, + "total": 73 + }, + "66ce753e7405aa00158ba967": { + "available": 14, + "total": 14 + }, + "66ce753e7405aa00158ba969": { + "available": 14, + "total": 14 + } + } + }, + { + "id": "66ce753e7405aa00158ba69f", + "title": { + "en": "Sunset Park", + "de": "Sunset Park", + "es": "Sunset Park", + "fr": "Sunset Park", + "pt": "Sunset Park" + }, + "subtitle": { + "en": "By the park", + "de": "Direkt am Park", + "es": "Junto al parque", + "fr": "Près du parc", + "pt": "Junto ao parque" + }, + "code": "sunset", + "image": "https://storeganise.s3.amazonaws.com/images/20220621/sites/site2.png", + "address": { + "en": "2640 East Sunset Road, \nLas Vegas", + "de": "2640 East Sunset Road, \nLas Vegas", + "es": "2640 East Sunset Road, \nLas Vegas", + "fr": "2640 East Sunset Road, \nLas Vegas", + "pt": "2640 East Sunset Road, \nLas Vegas" + }, + "phone": "702 555 6789", + "email": "sunset@storeganise.com", + "info": { + "en": "Not sure what unit you need? Give us a call or [send us an email](mailto:hello@storeganise.com) and we'll help you decide!" + }, + "hours": { + "en": "### Opening hours\n| Mon | 11:00 | ~ | 19:00 |\n| :-------- |:--------:|:----:| :--------:|\n| Tue | 11:00 | ~ | 19:00 |\n| Wed | 11:00 | ~ | 19:00 |\n| Thu | 11:00 | ~ | 19:00 |\n| Fri | 11:00 | ~ | 19:00 |\n| Sat | 08:00 | ~ | 21:00 |\n| Sun | 08:00 | ~ | 19:00 | \n \n\n\nLearn more about our Sunset Park location" + }, + "lat": 36.0692095, + "lng": -115.1154575, + "enableMoveInAgreement": true, + "occupancy": { + "blocked": 0, + "available": 153, + "reserved": 1, + "occupied": 1, + "overdue": 1 + }, + "unitTypeGroups": [], + "unitTypes": [ + { + "id": "66ce753e7405aa00158ba96b", + "title": { + "en": "5' x 5' (25 sq ft)", + "de": "5' x 5' (25 p2)", + "es": "5 x 5 (2.5 m²)", + "fr": "5 x 5 (2.5 m²)", + "pt": "5' x 5' (25 p2)" + }, + "subtitle": { + "en": "Telephone box", + "de": "Telefonzelle", + "es": "Cabina telefónica", + "fr": "Cabine téléphonique", + "pt": "Cabine Telefonica" + }, + "info": { + "en": "# More about this unit\n- 3' wide roll up door\n- 8' ceiling\n- Large elevators\n- Temperature & humidity controlled\n\nLearn more " + }, + "code": "5x5", + "siteId": "66ce753e7405aa00158ba69f", + "image": "https://storeganise.s3.amazonaws.com/images/20220621/unit-types/5x5.png", + "deposit": 99, + "price": 50, + "price_dynamic": 50, + "availability": { + "available": 72, + "total": 74 + }, + "occupancy": { + "available": 72, + "blocked": 0, + "reserved": 1, + "occupied": 1, + "overdue": 1 + }, + "tagIds": [ + "66ce753e7405aa00158ba513", + "66ce753e7405aa00158ba50d", + "66ce753e7405aa00158ba50e", + "66ce753e7405aa00158ba50f" + ], + "charges": [ + { + "id": "66ce753e7405aa00158ba96c", + "type": "movein", + "title": { + "en": "Admin fee" + }, + "amount": 10, + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "occupancyPrices": [], + "systemCharges": { + "unitDeposit": {}, + "unitRent": {} + }, + "labels": [], + "created": "2024-05-17T10:13:34.064Z", + "updated": "2024-09-03T15:45:10.905Z", + "tags": [ + { + "id": "66ce753e7405aa00158ba513", + "title": { + "en": "Wi-Fi", + "es": "Wi-Fi", + "de": "WLAN", + "fr": "Wi-Fi", + "pt": "Wi-Fi" + }, + "icon": "fas fa-rss" + }, + { + "id": "66ce753e7405aa00158ba50d", + "title": { + "en": "Air conditioning", + "es": "Aire acondicionado", + "de": "Klimaanlage", + "fr": "Air conditionné", + "pt": "Ar condicionado " + }, + "icon": "fas fa-asterisk" + }, + { + "id": "66ce753e7405aa00158ba50e", + "title": { + "en": "Free parking", + "es": "Estacionamiento gratis", + "de": "Gratis Parkplatz", + "fr": "Parking gratuit", + "pt": "Estacionamento Gratuito" + }, + "icon": "fas fa-car" + }, + { + "id": "66ce753e7405aa00158ba50f", + "title": { + "en": "Free move in service", + "es": "Servicio de traslado gratuito", + "de": "Kostenloser Umzugsservice", + "fr": "Service d'emménagement gratuit", + "pt": "Serviço de mudança" + }, + "icon": "fas fa-truck" + } + ] + }, + { + "id": "66ce753e7405aa00158ba96d", + "title": { + "en": "5' x 10' (50 sqft)", + "de": "5' x 10' (50 p2)", + "es": "5' x 10' (5 m²)", + "fr": "5' x 10' (5 m²)", + "pt": "5' x 10' (50 p2)" + }, + "subtitle": { + "en": "1 bedroom apartment", + "de": "1-Zimmer-Wohnung", + "es": "Apartamento de 1 quarto", + "fr": "Appartement 1 chambre", + "pt": "Apartamento de 1 quarto" + }, + "info": { + "en": "# More about this unit\n- 3' wide roll up door\n- 8' ceiling\n- Large elevators\n- Temperature & humidity controlled\n\nLearn more " + }, + "code": "5x10", + "siteId": "66ce753e7405aa00158ba69f", + "image": "https://storeganise.s3.amazonaws.com/images/20220621/unit-types/5x10.png", + "deposit": 179, + "price": 179, + "price_dynamic": 179, + "availability": { + "available": 54, + "total": 54 + }, + "occupancy": { + "available": 54, + "blocked": 0, + "reserved": 0, + "occupied": 0, + "overdue": 0 + }, + "tagIds": [ + "66ce753e7405aa00158ba513", + "66ce753e7405aa00158ba510", + "66ce753e7405aa00158ba50e", + "66ce753e7405aa00158ba50d" + ], + "charges": [ + { + "id": "66ce753e7405aa00158ba96e", + "type": "movein", + "title": { + "en": "Admin fee" + }, + "amount": 10, + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "occupancyPrices": [], + "systemCharges": { + "unitDeposit": {}, + "unitRent": {} + }, + "labels": [], + "created": "2024-05-17T10:13:34.064Z", + "updated": "2024-09-03T15:45:10.868Z", + "tags": [ + { + "id": "66ce753e7405aa00158ba513", + "title": { + "en": "Wi-Fi", + "es": "Wi-Fi", + "de": "WLAN", + "fr": "Wi-Fi", + "pt": "Wi-Fi" + }, + "icon": "fas fa-rss" + }, + { + "id": "66ce753e7405aa00158ba510", + "title": { + "en": "Ground floor", + "es": "Planta baja", + "de": "Ebenerdig", + "fr": "Rez-de-chaussée", + "pt": "Garagem" + }, + "icon": "fas fa-warehouse" + }, + { + "id": "66ce753e7405aa00158ba50e", + "title": { + "en": "Free parking", + "es": "Estacionamiento gratis", + "de": "Gratis Parkplatz", + "fr": "Parking gratuit", + "pt": "Estacionamento Gratuito" + }, + "icon": "fas fa-car" + }, + { + "id": "66ce753e7405aa00158ba50d", + "title": { + "en": "Air conditioning", + "es": "Aire acondicionado", + "de": "Klimaanlage", + "fr": "Air conditionné", + "pt": "Ar condicionado " + }, + "icon": "fas fa-asterisk" + } + ] + }, + { + "id": "66ce753e7405aa00158ba96f", + "title": { + "en": "10' x 10' (100 sqft)", + "de": "10' x 10' (100 p2)", + "es": "10' x 10' (9 m²)", + "fr": "10' x 10' (9 m²)", + "pt": "10' x 10' (100 p2)" + }, + "subtitle": { + "en": "2 bedroom apartment", + "de": "2-Zimmer-Wohnung", + "es": "Apartamento de 2 quartos", + "fr": "Appartement 2 chambres", + "pt": "Apartamento de 2 quartos" + }, + "info": { + "en": "# More about this unit\n- 3' wide roll up door\n- 8' ceiling\n- Large elevators\n- Temperature & humidity controlled\n- Free wifi throughout\n\nLearn more " + }, + "code": "10x10", + "siteId": "66ce753e7405aa00158ba69f", + "image": "https://storeganise.s3.amazonaws.com/images/20220621/unit-types/10x10.png", + "deposit": 239, + "price": 239, + "price_dynamic": 239, + "availability": { + "available": 14, + "total": 14 + }, + "occupancy": { + "available": 14, + "blocked": 0, + "reserved": 0, + "occupied": 0, + "overdue": 0 + }, + "tagIds": [ + "66ce753e7405aa00158ba50d", + "66ce753e7405aa00158ba50f", + "66ce753e7405aa00158ba513" + ], + "charges": [ + { + "id": "66ce753e7405aa00158ba970", + "type": "movein", + "title": { + "en": "Admin fee" + }, + "amount": 10, + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "occupancyPrices": [], + "systemCharges": { + "unitDeposit": {}, + "unitRent": {} + }, + "labels": [], + "created": "2024-05-17T10:13:34.064Z", + "updated": "2024-09-03T15:45:10.873Z", + "tags": [ + { + "id": "66ce753e7405aa00158ba50d", + "title": { + "en": "Air conditioning", + "es": "Aire acondicionado", + "de": "Klimaanlage", + "fr": "Air conditionné", + "pt": "Ar condicionado " + }, + "icon": "fas fa-asterisk" + }, + { + "id": "66ce753e7405aa00158ba50f", + "title": { + "en": "Free move in service", + "es": "Servicio de traslado gratuito", + "de": "Kostenloser Umzugsservice", + "fr": "Service d'emménagement gratuit", + "pt": "Serviço de mudança" + }, + "icon": "fas fa-truck" + }, + { + "id": "66ce753e7405aa00158ba513", + "title": { + "en": "Wi-Fi", + "es": "Wi-Fi", + "de": "WLAN", + "fr": "Wi-Fi", + "pt": "Wi-Fi" + }, + "icon": "fas fa-rss" + } + ] + }, + { + "id": "66ce753e7405aa00158ba971", + "title": { + "en": "10' x 20' (200 sqft)", + "de": "10' x 20' (200 sqft)", + "es": "10' x 20' (200 sqft)", + "fr": "10' x 20' (200 sqft)", + "pt": "10' x 20' (200 p2)" + }, + "subtitle": { + "en": "Family house", + "de": "Einfamilienhaus", + "es": "Casa inteira", + "fr": "Maison familiale", + "pt": "Casa inteira" + }, + "info": { + "en": "# More about this unit\n- 3' wide roll up door\n- 8' ceiling\n- Large elevators\n- Temperature & humidity controlled\n- Free wifi throughout\n\nLearn more " + }, + "code": "10x20", + "siteId": "66ce753e7405aa00158ba69f", + "image": "https://storeganise.s3.amazonaws.com/images/20220621/unit-types/10x20.png", + "deposit": 300, + "price": 300, + "price_dynamic": 300, + "availability": { + "available": 13, + "total": 13 + }, + "occupancy": { + "available": 13, + "blocked": 0, + "reserved": 0, + "occupied": 0, + "overdue": 0 + }, + "tagIds": [ + "66ce753e7405aa00158ba50d", + "66ce753e7405aa00158ba50f", + "66ce753e7405aa00158ba50e", + "66ce753e7405aa00158ba510", + "66ce753e7405aa00158ba513" + ], + "charges": [ + { + "id": "66ce753e7405aa00158ba972", + "type": "movein", + "title": { + "en": "Admin fee" + }, + "amount": 10, + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "occupancyPrices": [], + "systemCharges": { + "unitDeposit": {}, + "unitRent": {} + }, + "labels": [], + "created": "2024-05-17T10:13:34.065Z", + "updated": "2024-09-03T15:45:10.877Z", + "tags": [ + { + "id": "66ce753e7405aa00158ba50d", + "title": { + "en": "Air conditioning", + "es": "Aire acondicionado", + "de": "Klimaanlage", + "fr": "Air conditionné", + "pt": "Ar condicionado " + }, + "icon": "fas fa-asterisk" + }, + { + "id": "66ce753e7405aa00158ba50f", + "title": { + "en": "Free move in service", + "es": "Servicio de traslado gratuito", + "de": "Kostenloser Umzugsservice", + "fr": "Service d'emménagement gratuit", + "pt": "Serviço de mudança" + }, + "icon": "fas fa-truck" + }, + { + "id": "66ce753e7405aa00158ba50e", + "title": { + "en": "Free parking", + "es": "Estacionamiento gratis", + "de": "Gratis Parkplatz", + "fr": "Parking gratuit", + "pt": "Estacionamento Gratuito" + }, + "icon": "fas fa-car" + }, + { + "id": "66ce753e7405aa00158ba510", + "title": { + "en": "Ground floor", + "es": "Planta baja", + "de": "Ebenerdig", + "fr": "Rez-de-chaussée", + "pt": "Garagem" + }, + "icon": "fas fa-warehouse" + }, + { + "id": "66ce753e7405aa00158ba513", + "title": { + "en": "Wi-Fi", + "es": "Wi-Fi", + "de": "WLAN", + "fr": "Wi-Fi", + "pt": "Wi-Fi" + }, + "icon": "fas fa-rss" + } + ] + } + ], + "products": [ + { + "id": "66ce753e7405aa00158ba6a4", + "title": { + "en": "Box", + "de": "Schachtel", + "es": "Caja", + "fr": "Box", + "pt": "Caixa" + }, + "description": { + "en": "Carton box: 40 x 60 x 30 cm", + "de": "Kartonschachtel: 40 x 60 x 30 cm", + "es": "Caja: 40 x 60 x 30 cm", + "fr": "Box carton: 40 x 60 x 30 cm", + "pt": "Caixa: 40 x 60 x 30 cm" + }, + "price": 5, + "displayPrice": { + "en": "$5 per box", + "de": "€5 pro Karton", + "es": "$5 por caja", + "fr": "$5 par box", + "pt": "$5 por caixa" + }, + "image": "https://storeganise.s3.amazonaws.com/images/20220621/products-services/cardboard-box-medium.png", + "type": "movein", + "showInApp": true, + "hidden": false, + "trackInventory": true, + "inventory": 500, + "maxQuantity": 30, + "accountingCodeId": "66ce753e7405aa00158ba519" + }, + { + "id": "66ce753e7405aa00158ba6a5", + "title": { + "en": "Packing tape", + "de": "Packband", + "es": "Cinta de embalaje", + "fr": "Ruban adhésif d'emballage", + "pt": "Fita adesiva" + }, + "description": { + "en": "Width: 2.5\". Includes tape holder & cutter.", + "de": "Breite 6 cm; inklusive Abroller & Schneider", + "es": "Ancho: 2.5\"", + "fr": "Largeur: 2.5\". cutter et support inclus", + "pt": "Largura: 2,5 \". Inclui suporte e cortador de fita." + }, + "price": 9.99, + "displayPrice": { + "en": "$9.99 per roll", + "de": "$9.99 pro Rolle", + "es": "$9.99 por rollo", + "fr": "$9.99 par rouleau", + "pt": "$9.99 por rollo" + }, + "image": "https://storeganise.s3.amazonaws.com/images/20220621/products-services/tape-dispenser.png", + "type": "movein", + "showInApp": true, + "hidden": false, + "trackInventory": true, + "inventory": 200, + "maxQuantity": 10, + "accountingCodeId": "66ce753e7405aa00158ba519" + }, + { + "id": "66ce753e7405aa00158ba6a7", + "title": { + "en": "Insurance", + "de": "Versicherung", + "es": "Seguro", + "fr": "Assurance", + "pt": "Seguro" + }, + "description": { + "en": "Insurance covering up to $5,000 worth of items", + "de": "Versicherung für Gegenstände im Wert von bis zu $5.000", + "es": "Cobertura de seguro de hasta $5.000 en pertenencias", + "fr": "Une assurance couvrant jusqu'à $5000 $ d'articles.", + "pt": "Cobertura de seguro de até $5.000 em pertences" + }, + "price": 10, + "displayPrice": { + "en": "$10 per month", + "de": "$10 pro Monat", + "es": "$10 por mes", + "fr": "$10/mois", + "pt": "$10 por mês" + }, + "image": "https://storeganise.s3.amazonaws.com/images/20220621/products-services/umbrella.png", + "type": "recurring", + "showInApp": true, + "hidden": false, + "maxQuantity": 5, + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "labels": [ + "66ce753e7405aa00158ba52c" + ], + "measure": "ft", + "area_gross": 7500, + "area_net": 7000, + "excludedBookingDates": [], + "created": "2020-05-21T02:29:38.096Z", + "updated": "2024-07-31T17:00:18.454Z", + "availability": { + "66ce753e7405aa00158ba96b": { + "available": 72, + "total": 74 + }, + "66ce753e7405aa00158ba96d": { + "available": 54, + "total": 54 + }, + "66ce753e7405aa00158ba96f": { + "available": 14, + "total": 14 + }, + "66ce753e7405aa00158ba971": { + "available": 13, + "total": 13 + } + } + }, + { + "id": "66ce753e7405aa00158ba6b0", + "title": { + "en": "Vegas Strip Parking & Vehicle Storage", + "de": "Vegas Strip Parking & Vehicle Storage", + "es": "Vegas Strip Parking & Vehicle Storage", + "fr": "Vegas Strip Parking & Vehicle Storage", + "pt": "Vegas Strip Parking & Vehicle Storage" + }, + "subtitle": { + "en": "Right on the Strip!", + "de": "Direkt am Strip!", + "es": "¡Justo en el Strip!", + "fr": "En plein dans le Strip !", + "pt": "Bem na Tira!" + }, + "code": "vegas-strip", + "image": "https://storeganise.s3.amazonaws.com/images/20220621/sites/site1.png", + "address": { + "en": "3655 Las Vegas Blvd S, \nLas Vegas", + "de": "3655 Las Vegas Blvd S, \nLas Vegas", + "es": "3655 Las Vegas Blvd S, \nLas Vegas", + "fr": "3655 Las Vegas Blvd S, \nLas Vegas", + "pt": "3655 Las Vegas Blvd S, \nLas Vegas" + }, + "phone": "702 555 4321", + "email": "lasvegasstrip@storeganise.com", + "info": { + "en": "Not sure what unit you need? Give us a call or [send us an email](mailto:hello@storeganise.com) and we'll help you decide!\n\n### Opening hours\n| Mon | 09:00 | ~ | 18:00 |\n| :-------- |:--------:|:----:| :--------:|\n| Tue | 09:00 | ~ | 18:00 |\n| Wed | 09:00 | ~ | 18:00 |\n| Thu | 09:00 | ~ | 18:00 |\n| Fri | 09:00 | ~ | 18:00 |\n| Sat | 08:00 | ~ | 21:00 |\n| Sun | 08:00 | ~ | 19:00 | \n \n\n\nLearn more about our Vegas Strip location" + }, + "hours": {}, + "lat": 36.1121549, + "lng": -115.1737387, + "enableMoveInAgreement": true, + "occupancy": { + "blocked": 1, + "available": 159, + "reserved": 0, + "occupied": 0, + "overdue": 0 + }, + "unitTypeGroups": [], + "unitTypes": [ + { + "id": "66ce753e7405aa00158ba973", + "title": { + "en": "Personal vehicle parking", + "de": "Personal vehicle parking", + "es": "Personal vehicle parking", + "fr": "Personal vehicle parking", + "pt": "Personal vehicle parking" + }, + "subtitle": { + "en": "For personal vehicles including cars, trucks, and vans", + "de": "For personal vehicles including cars, trucks, and vans", + "es": "For personal vehicles including cars, trucks, and vans", + "fr": "For personal vehicles including cars, trucks, and vans", + "pt": "For personal vehicles including cars, trucks, and vans" + }, + "info": { + "en": "# More about this unit\n- 3' wide roll up door\n- 8' ceiling\n- Large elevators\n- Temperature & humidity controlled\n\nLearn more " + }, + "code": "5x5", + "siteId": "66ce753e7405aa00158ba6b0", + "image": "https://storeganise-test.s3.amazonaws.com/627a0a17afcd8f0004110377/uploads/ff1afa19a1388d962a4872f0b9df51f7.png", + "deposit": 50, + "price": 50, + "price_dynamic": 50, + "availability": { + "available": 71, + "total": 72 + }, + "occupancy": { + "available": 71, + "blocked": 1, + "reserved": 0, + "occupied": 0, + "overdue": 0 + }, + "tagIds": [ + "66ce753e7405aa00158ba513", + "66ce753e7405aa00158ba50d", + "66ce753e7405aa00158ba50e", + "66ce753e7405aa00158ba50f" + ], + "charges": [ + { + "id": "66ce753e7405aa00158ba974", + "type": "movein", + "title": { + "en": "Admin fee" + }, + "amount": 10, + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "occupancyPrices": [], + "systemCharges": { + "unitDeposit": {}, + "unitRent": {} + }, + "labels": [], + "created": "2024-05-17T10:13:34.105Z", + "updated": "2024-09-03T15:45:10.882Z", + "tags": [ + { + "id": "66ce753e7405aa00158ba513", + "title": { + "en": "Wi-Fi", + "es": "Wi-Fi", + "de": "WLAN", + "fr": "Wi-Fi", + "pt": "Wi-Fi" + }, + "icon": "fas fa-rss" + }, + { + "id": "66ce753e7405aa00158ba50d", + "title": { + "en": "Air conditioning", + "es": "Aire acondicionado", + "de": "Klimaanlage", + "fr": "Air conditionné", + "pt": "Ar condicionado " + }, + "icon": "fas fa-asterisk" + }, + { + "id": "66ce753e7405aa00158ba50e", + "title": { + "en": "Free parking", + "es": "Estacionamiento gratis", + "de": "Gratis Parkplatz", + "fr": "Parking gratuit", + "pt": "Estacionamento Gratuito" + }, + "icon": "fas fa-car" + }, + { + "id": "66ce753e7405aa00158ba50f", + "title": { + "en": "Free move in service", + "es": "Servicio de traslado gratuito", + "de": "Kostenloser Umzugsservice", + "fr": "Service d'emménagement gratuit", + "pt": "Serviço de mudança" + }, + "icon": "fas fa-truck" + } + ] + }, + { + "id": "66ce753e7405aa00158ba975", + "title": { + "en": "Trailer and caravan parking", + "de": "Trailer and caravan parking", + "es": "Trailer and caravan parking", + "fr": "Trailer and caravan parking", + "pt": "Trailer and caravan parking" + }, + "subtitle": { + "en": "For trailers and pull behinds", + "de": "For trailers and pull behinds", + "es": "For trailers and pull behinds", + "fr": "For trailers and pull behinds", + "pt": "For trailers and pull behinds" + }, + "info": { + "en": "# More about this unit\n- 3' wide roll up door\n- 8' ceiling\n- Large elevators\n- Temperature & humidity controlled\n\nLearn more " + }, + "code": "5x10", + "siteId": "66ce753e7405aa00158ba6b0", + "image": "https://storeganise-test.s3.amazonaws.com/627a0a17afcd8f0004110377/uploads/ccd7d86c07b3d01bd6ef6dae9d0ed048.png", + "deposit": 179, + "price": 179, + "price_dynamic": 179, + "availability": { + "available": 58, + "total": 58 + }, + "occupancy": { + "available": 58, + "blocked": 0, + "reserved": 0, + "occupied": 0, + "overdue": 0 + }, + "tagIds": [ + "66ce753e7405aa00158ba513", + "66ce753e7405aa00158ba510", + "66ce753e7405aa00158ba50e", + "66ce753e7405aa00158ba50d" + ], + "charges": [ + { + "id": "66ce753e7405aa00158ba976", + "type": "movein", + "title": { + "en": "Admin fee" + }, + "amount": 10, + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "occupancyPrices": [], + "systemCharges": { + "unitDeposit": {}, + "unitRent": {} + }, + "labels": [], + "created": "2024-05-17T10:13:34.105Z", + "updated": "2024-09-03T15:45:10.886Z", + "tags": [ + { + "id": "66ce753e7405aa00158ba513", + "title": { + "en": "Wi-Fi", + "es": "Wi-Fi", + "de": "WLAN", + "fr": "Wi-Fi", + "pt": "Wi-Fi" + }, + "icon": "fas fa-rss" + }, + { + "id": "66ce753e7405aa00158ba510", + "title": { + "en": "Ground floor", + "es": "Planta baja", + "de": "Ebenerdig", + "fr": "Rez-de-chaussée", + "pt": "Garagem" + }, + "icon": "fas fa-warehouse" + }, + { + "id": "66ce753e7405aa00158ba50e", + "title": { + "en": "Free parking", + "es": "Estacionamiento gratis", + "de": "Gratis Parkplatz", + "fr": "Parking gratuit", + "pt": "Estacionamento Gratuito" + }, + "icon": "fas fa-car" + }, + { + "id": "66ce753e7405aa00158ba50d", + "title": { + "en": "Air conditioning", + "es": "Aire acondicionado", + "de": "Klimaanlage", + "fr": "Air conditionné", + "pt": "Ar condicionado " + }, + "icon": "fas fa-asterisk" + } + ] + }, + { + "id": "66ce753e7405aa00158ba977", + "title": { + "en": "Boat parking", + "de": "Boat parking", + "es": "Boat parking", + "fr": "Boat parking", + "pt": "Boat parking" + }, + "subtitle": { + "en": "For boats up to 30'", + "de": "For boats up to 30'", + "es": "For boats up to 30'", + "fr": "For boats up to 30'", + "pt": "For boats up to 30'" + }, + "info": { + "en": "# More about this unit\n- 3' wide roll up door\n- 8' ceiling\n- Large elevators\n- Temperature & humidity controlled\n- Free wifi throughout\n\nLearn more " + }, + "code": "10x10", + "siteId": "66ce753e7405aa00158ba6b0", + "image": "https://storeganise-test.s3.amazonaws.com/627a0a17afcd8f0004110377/uploads/bad320989114b48dc37ec990130497d7.png", + "deposit": 239, + "price": 239, + "price_display": 0, + "price_dynamic": 239, + "availability": { + "available": 17, + "total": 17 + }, + "occupancy": { + "available": 17, + "blocked": 0, + "reserved": 0, + "occupied": 0, + "overdue": 0 + }, + "tagIds": [ + "66ce753e7405aa00158ba50d", + "66ce753e7405aa00158ba50f", + "66ce753e7405aa00158ba513" + ], + "charges": [ + { + "id": "66ce753e7405aa00158ba978", + "type": "movein", + "title": { + "en": "Admin fee" + }, + "amount": 10, + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "occupancyPrices": [], + "systemCharges": { + "unitDeposit": {}, + "unitRent": {} + }, + "labels": [], + "created": "2024-05-17T10:13:34.105Z", + "updated": "2024-09-03T15:45:10.918Z", + "tags": [ + { + "id": "66ce753e7405aa00158ba50d", + "title": { + "en": "Air conditioning", + "es": "Aire acondicionado", + "de": "Klimaanlage", + "fr": "Air conditionné", + "pt": "Ar condicionado " + }, + "icon": "fas fa-asterisk" + }, + { + "id": "66ce753e7405aa00158ba50f", + "title": { + "en": "Free move in service", + "es": "Servicio de traslado gratuito", + "de": "Kostenloser Umzugsservice", + "fr": "Service d'emménagement gratuit", + "pt": "Serviço de mudança" + }, + "icon": "fas fa-truck" + }, + { + "id": "66ce753e7405aa00158ba513", + "title": { + "en": "Wi-Fi", + "es": "Wi-Fi", + "de": "WLAN", + "fr": "Wi-Fi", + "pt": "Wi-Fi" + }, + "icon": "fas fa-rss" + } + ] + }, + { + "id": "66ce753e7405aa00158ba979", + "title": { + "en": "Motorhome parking", + "de": "Motorhome parking", + "es": "Motorhome parking", + "fr": "Motorhome parking", + "pt": "Motorhome parking" + }, + "subtitle": { + "en": "For motorhomes and RVs up to 40'", + "de": "For motorhomes and RVs up to 40'", + "es": "For motorhomes and RVs up to 40'", + "fr": "For motorhomes and RVs up to 40'", + "pt": "For motorhomes and RVs up to 40'" + }, + "info": { + "en": "# More about this unit\n- 3' wide roll up door\n- 8' ceiling\n- Large elevators\n- Temperature & humidity controlled\n- Free wifi throughout\n\nLearn more " + }, + "code": "10x20", + "siteId": "66ce753e7405aa00158ba6b0", + "image": "https://storeganise-test.s3.amazonaws.com/627a0a17afcd8f0004110377/uploads/3528ccb8e788e9fe6bfcc80f544e1b68.png", + "deposit": 300, + "price": 300, + "price_dynamic": 300, + "availability": { + "available": 13, + "total": 13 + }, + "occupancy": { + "available": 13, + "blocked": 0, + "reserved": 0, + "occupied": 0, + "overdue": 0 + }, + "tagIds": [ + "66ce753e7405aa00158ba50d", + "66ce753e7405aa00158ba50f", + "66ce753e7405aa00158ba50e", + "66ce753e7405aa00158ba510", + "66ce753e7405aa00158ba513" + ], + "charges": [ + { + "id": "66ce753e7405aa00158ba97a", + "type": "movein", + "title": { + "en": "Admin fee" + }, + "amount": 10, + "accountingCodeId": "66ce753e7405aa00158ba519" + } + ], + "occupancyPrices": [], + "systemCharges": { + "unitDeposit": {}, + "unitRent": {} + }, + "labels": [], + "created": "2024-05-17T10:13:34.106Z", + "updated": "2024-09-03T15:45:10.909Z", + "tags": [ + { + "id": "66ce753e7405aa00158ba50d", + "title": { + "en": "Air conditioning", + "es": "Aire acondicionado", + "de": "Klimaanlage", + "fr": "Air conditionné", + "pt": "Ar condicionado " + }, + "icon": "fas fa-asterisk" + }, + { + "id": "66ce753e7405aa00158ba50f", + "title": { + "en": "Free move in service", + "es": "Servicio de traslado gratuito", + "de": "Kostenloser Umzugsservice", + "fr": "Service d'emménagement gratuit", + "pt": "Serviço de mudança" + }, + "icon": "fas fa-truck" + }, + { + "id": "66ce753e7405aa00158ba50e", + "title": { + "en": "Free parking", + "es": "Estacionamiento gratis", + "de": "Gratis Parkplatz", + "fr": "Parking gratuit", + "pt": "Estacionamento Gratuito" + }, + "icon": "fas fa-car" + }, + { + "id": "66ce753e7405aa00158ba510", + "title": { + "en": "Ground floor", + "es": "Planta baja", + "de": "Ebenerdig", + "fr": "Rez-de-chaussée", + "pt": "Garagem" + }, + "icon": "fas fa-warehouse" + }, + { + "id": "66ce753e7405aa00158ba513", + "title": { + "en": "Wi-Fi", + "es": "Wi-Fi", + "de": "WLAN", + "fr": "Wi-Fi", + "pt": "Wi-Fi" + }, + "icon": "fas fa-rss" + } + ] + } + ], + "products": [], + "labels": [ + "66ce753e7405aa00158ba52e" + ], + "measure": "ft", + "area_gross": 7500, + "area_net": 7000, + "excludedBookingDates": [], + "created": "2020-05-21T02:29:38.093Z", + "updated": "2023-06-16T14:26:11.246Z", + "availability": { + "66ce753e7405aa00158ba973": { + "available": 71, + "total": 72 + }, + "66ce753e7405aa00158ba975": { + "available": 58, + "total": 58 + }, + "66ce753e7405aa00158ba977": { + "available": 17, + "total": 17 + }, + "66ce753e7405aa00158ba979": { + "available": 13, + "total": 13 + } + } + } + ], + "tax": { + "type": "Tax", + "percent": 20 + }, + "termsUrl": "https://storeganise.com/terms", + "wurdLanguages": { + "en": "en", + "es": "es", + "fr": "fr", + "pt": "pt", + "de": "de" + } + } +} \ No newline at end of file diff --git a/components/storeganise/storeganise.app.mjs b/components/storeganise/storeganise.app.mjs new file mode 100644 index 0000000000000..cb61e01353b5d --- /dev/null +++ b/components/storeganise/storeganise.app.mjs @@ -0,0 +1,78 @@ +import { axios } from "@pipedream/platform"; +const DEFAULT_LIMIT = 20; + +export default { + type: "app", + app: "storeganise", + propDefinitions: { + invoiceId: { + type: "string", + label: "Invoice ID", + description: "The ID of the invoice", + async options({ page }) { + const invoices = await this.listInvoices({ + params: { + limit: DEFAULT_LIMIT, + offset: page * DEFAULT_LIMIT, + }, + }); + return invoices?.map(({ id }) => id); + }, + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.subdomain}.storeganise.com/api/v1/admin`; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `ApiKey ${this.$auth.api_key}`, + }, + }); + }, + updateInvoice({ + invoiceId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/invoices/${invoiceId}`, + ...opts, + }); + }, + addPaymentToInvoice({ + invoiceId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/invoices/${invoiceId}/payments`, + ...opts, + }); + }, + listInvoices(opts = {}) { + return this._makeRequest({ + path: "/invoices", + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users", + ...opts, + }); + }, + listUnitRentals(opts = {}) { + return this._makeRequest({ + path: "/unit-rentals", + ...opts, + }); + }, + }, +}; diff --git a/components/storerocket/package.json b/components/storerocket/package.json new file mode 100644 index 0000000000000..2ea0a22ff8f2f --- /dev/null +++ b/components/storerocket/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/storerocket", + "version": "0.0.1", + "description": "Pipedream Storerocket Components", + "main": "storerocket.app.mjs", + "keywords": [ + "pipedream", + "storerocket" + ], + "homepage": "https://pipedream.com/apps/storerocket", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/storerocket/storerocket.app.mjs b/components/storerocket/storerocket.app.mjs new file mode 100644 index 0000000000000..b395b5698defc --- /dev/null +++ b/components/storerocket/storerocket.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "storerocket", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/stormboard/README.md b/components/stormboard/README.md index d569d240e65eb..c61d4c13c4ac6 100644 --- a/components/stormboard/README.md +++ b/components/stormboard/README.md @@ -1,21 +1,11 @@ # Overview -The Stormboard API allows developers to access and integrate the functionality -of Stormboard with other applications. With the Stormboard API, developers can -build applications to allow their users to collaborate, brainstorm, build -lists, and organize workflow in teams. Here are some of the many things you can -build with the Stormboard API: +Stormboard is a digital workspace designed for interactive planning and collaboration. Leveraging its API on Pipedream, you can automate workflows to streamline idea management and decision-making processes. Create, update, and manage ideas captured in Stormboard in real-time, and sync them with other business tools for enhanced productivity. -- Real-time group discussion boards -- Generate meeting agendas with tasks and timelines -- Create and assign tasks to team members -- Collect feedback from stakeholders -- Prioritize ideas and features for product development -- Integrate data from other applications into collaborative workspaces -- Manage project progress from concept to completion -- Leverage data from customer surveys and feedback loops -- Visualize sales pipelines -- Create interactive maps of customer locations -- Brainstorm and execute complex marketing campaigns -- Track and changes to team documents in real-time -- Create automated alerts and notifications +# Example Use Cases + +- **Automated Idea Distribution:** Once ideas are brainstormed in Stormboard, use Pipedream to classify and send them to relevant team members through email or messaging apps like Slack. This ensures that ideas are promptly acted upon. + +- **Enhanced Project Management:** Integrate Stormboard with project management tools such as Trello or Asana. Convert Stormboard sticky notes into tasks automatically, assigning them to team members and setting due dates based on predefined criteria. + +- **Real-Time Reporting and Analytics:** Use Pipedream to extract data from Stormboard for real-time analytics. Feed this data into BI tools like Tableau or Google Sheets to visualize the progress of brainstorming sessions and the status of ideas and decisions. diff --git a/components/stormglass_io/README.md b/components/stormglass_io/README.md index f70acfbfa2573..3ecdb88e59fd6 100644 --- a/components/stormglass_io/README.md +++ b/components/stormglass_io/README.md @@ -1,19 +1,11 @@ # Overview -Stormglass.io is a data platform that exposes ocean and weather data as an -expansive and comprehensive set of APIs. With its suite of APIs and data sets, -you can gain insights and bring your applications to life with real-time data. -Here are a few examples of what you can build with the Stormglass.io API: +The Stormglass.io API provides granular, location-based marine weather data, a treasure trove for developers keen on crafting weather-aware applications. With this API, you can access a plethora of metrics such as air temperature, atmospheric pressure, wind speed, and more. Pipedream amplifies this potential by offering a platform to integrate Stormglass.io with countless other apps, automating actions based on specific weather conditions or forecasts. -- Real-time marine traffic analysis -- Wave forecasting columns -- Coastal weather and ocean monitoring -- Wave and water quality simulation -- Real-time ocean current prediction -- Wind and wave database -- Sailboat route optimisation -- Historical and current buoy data -- Weather route optimization -- Marine life tracking -- Fishing hot spot mapping -- Real-time boat performance analytics +# Example Use Cases + +- **Weather-Triggered Social Media Updates**: Use Stormglass.io to monitor weather conditions and automatically generate and post updates to social media platforms via Pipedream's Twitter or Facebook integrations when specific weather criteria are met, like a perfect sunny day at the beach or an incoming storm warning. + +- **Smart Home Adjustments**: Connect Stormglass.io with smart home apps like Philips Hue or Nest on Pipedream. Set up a workflow that adjusts lighting and temperature settings based on real-time weather data; for example, dimming lights and lowering AC usage on cooler, overcast days. + +- **Marine Activity Planner**: Combine Stormglass.io with calendar apps such as Google Calendar on Pipedream. Automate the creation of optimal sailing or fishing events by analyzing upcoming weather conditions, notifying participants via email or SMS, and updating calendar events accordingly. diff --git a/components/storyblok/README.md b/components/storyblok/README.md index 40d82805213dd..ef12d3be7ee00 100644 --- a/components/storyblok/README.md +++ b/components/storyblok/README.md @@ -1,29 +1,11 @@ # Overview -The Storyblok API is the perfect tool for developers looking to build out -stunning websites and apps in an open platform that is secure and scalable. -With a suite of build-in components, an easily customizeable UI/UX, and -powerful integrations, the Storyblok API empowers developers to create powerful -experiences without sacrificing speed of development. +The Storyblok API opens a channel to manage and deliver content dynamically, allowing developers to automate content-related workflows and integrate with other services. Using the API on Pipedream, you can create, update, and retrieve content from Storyblok, and connect these actions with hundreds of other apps for seamless content operations. This can streamline content management, facilitate omnichannel publishing, and enable real-time content updates in various frontend applications. -Using the Storyblok API, you can: +# Example Use Cases -- Create E-commerce sites with at-a-glance product management, pricing, and - checkout options -- Create Portfolio websites to showcase your work with ease -- Create members only content that's secured and managed with drag and drop - editing -- Create Dynamic Apps with custom mobile UI/UX features -- Create Blogging Platofmrs for sharing posts amongst users -- Create Multi-Lingual Apps with support for more languages than ever before -- Create Dynamic Web Pages to showcase the latest news and updates quickly -- Integrate Social Media Api's into your app to connect with users on all - platforms -- Create Schedules and Calendars to keep users informed on events -- Create Protected Resources with your own authentication and authorization - mechanisms. -- Create Customizable User Access Rights to give users access to the content - they need. -- Publish Pages Instantly with a single click, without having to worry about - hosting. -- Generate Dynamic SEO Friendly URLs for all your content. +- **Content Synchronization Across Platforms**: Automate the distribution of content from Storyblok to multiple platforms like WordPress or Shopify. When a new story is published or updated in Storyblok, use Pipedream to push that content automatically to your e-commerce site or blog, ensuring consistency and saving time. + +- **Automated Social Media Updates**: Share your latest content on social media channels whenever a new story is added to Storyblok. A Pipedream workflow can be set up to listen for new stories and post updates, along with a custom message and link, to Twitter, LinkedIn, or Facebook, increasing your content’s reach without extra manual effort. + +- **Dynamic Email Campaigns**: Integrate Storyblok with email marketing tools such as Mailchimp or SendGrid. Use Pipedream to trigger an email campaign whenever a new piece of content is published on Storyblok, ensuring your audience is always informed about the latest articles or updates, driving engagement through timely and relevant content delivery. diff --git a/components/storyblok/package.json b/components/storyblok/package.json new file mode 100644 index 0000000000000..b5482c0a7c814 --- /dev/null +++ b/components/storyblok/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/storyblok", + "version": "0.6.0", + "description": "Pipedream storyblok Components", + "main": "storyblok.app.mjs", + "keywords": [ + "pipedream", + "storyblok" + ], + "homepage": "https://pipedream.com/apps/storyblok", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/storyscale/README.md b/components/storyscale/README.md new file mode 100644 index 0000000000000..1c923f945678d --- /dev/null +++ b/components/storyscale/README.md @@ -0,0 +1,11 @@ +# Overview + +The StoryScale API enables the creation, management, and retrieval of storytelling content. Integrated within Pipedream's ecosystem, you can automate interactions with the StoryScale API to streamline content workflows, react to events, and sync data across various platforms. Pipedream's serverless platform simplifies the process of setting up event-driven workflows, making it straightforward to harness the capabilities of the StoryScale API without writing complex infrastructure code. + +# Example Use Cases + +- **Content Sync Between StoryScale and CMS**: Sync storytelling content from StoryScale to a content management system (CMS) like WordPress. Whenever a new story is published in StoryScale, Pipedream triggers a workflow that creates or updates a corresponding post in WordPress, ensuring your website content stays fresh and aligned with your storytelling platform. + +- **Social Media Storytelling Updates**: Share new StoryScale content updates on social media platforms such as Twitter or LinkedIn. When a story is updated or tagged with specific keywords, Pipedream can push a notification or an automated post to your social media accounts, increasing audience engagement and driving traffic back to your core storytelling content. + +- **Analytics Integration for Story Performance**: Integrate StoryScale with analytics tools like Google Analytics to track story engagement. Each time a story is viewed, a Pipedream workflow can capture the event and send the data to Google Analytics. This allows you to measure the impact of your stories and gain insights into your audience's behavior for better content targeting. diff --git a/components/strava/README.md b/components/strava/README.md index 6147ad9c92d27..3ab7e43fc34be 100644 --- a/components/strava/README.md +++ b/components/strava/README.md @@ -1,5 +1,11 @@ -# Overview +# Overview -[Read the Strava docs at https://docs.pipedream.com](https://docs.pipedream.com/apps/strava/). +The Strava API lets you tap into the robust data from Strava's fitness app, which is a playground for athletes worldwide to track their workouts. With Pipedream, you can automate actions based on activities uploaded to Strava, such as running, biking, or swimming. Imagine syncing workout data to spreadsheets for analysis, auto-posting achievements to social media, or integrating with calendar apps for better scheduling. Pipedream's serverless platform makes it a breeze to create workflows that can listen for Strava webhooks, process data, and trigger actions in countless other apps. -## Getting Started +# Example Use Cases + +- **Sync Strava Activities to Google Sheets for Analysis**: Whenever a new activity is uploaded to Strava, a Pipedream workflow can be triggered, capturing relevant data like duration, distance, and pace. It then formats and sends this data to a Google Sheets document for tracking and further analysis. This can be invaluable for coaches and athletes looking to visualize progress over time. + +- **Share Completed Workouts on Twitter**: Leverage the social aspect of workouts by automatically sharing your completed Strava activities on Twitter. Once an activity is saved on Strava, Pipedream triggers a workflow that extracts the details of the workout and posts a custom message along with stats to your Twitter account, celebrating your achievements and encouraging followers. + +- **Integrate Workout Data with a Custom Slack Bot**: Keep your team or coach updated by sending a Slack message containing key workout details immediately after you finish an activity. With Pipedream, you can create a workflow that captures Strava activity details and uses a Slack bot to post those details in a designated channel or direct message. This keeps your fitness community engaged and informed in real-time. diff --git a/components/streak/README.md b/components/streak/README.md index 13a62f601caf0..0543ce452a1e5 100644 --- a/components/streak/README.md +++ b/components/streak/README.md @@ -1,21 +1,11 @@ # Overview -With Streak's API, you can build powerful and custom applications to extend and -integrate with Streak. Here are some of the things you can build with the -Streak API: +The Streak API taps into the core of Streak’s CRM functionality within Gmail, allowing users to automate interactions with pipeline data, such as leads, contacts, and sales opportunities. With Pipedream, you can harness this API to craft serverless workflows that react to events in Streak and connect with countless other services to streamline CRM tasks, sync data across platforms, or trigger communications based on pipeline changes. -- Create custom objects within Streak, such as customer profiles or tasks -- Automate customer onboarding processes by automatically sending emails and - creating customer profiles within Streak -- Update customer profiles in Streak based on customer activity -- Keep customer records up-to-date by automatically syncing data from - third-party applications -- Automatically sync customer contact information from third-parties -- Build an integration with a CRM to keep contact information up-to-date in - Streak -- Create custom triggers to automatically take actions within Streak based on - customer actions -- Create custom reports to analyze customer behavior or trends in their data -- Store customer notes and other records for easy retrieval and review -- Create custom views and filters to quickly analyze and report on customer - data +# Example Use Cases + +- **Automated Lead Follow-Up Emails**: Trigger an automated email sequence when a new lead is added to a Streak pipeline, using Gmail or a third-party email service. Tailor follow-up content based on lead data from Streak to create a personalized touch without manual effort. + +- **Pipeline Change Notifications**: Send real-time alerts via Slack or another messaging platform when a deal moves to a new stage in your Streak pipeline. Keep the team instantly informed about progress without them having to constantly check their CRM. + +- **Data Synchronization with Google Sheets**: Automatically export new and updated pipeline data to a Google Sheet for reporting and analysis. Use this to keep backup records, generate custom reports, or feed data into other analytics tools for deeper insights. diff --git a/components/streamlabs/README.md b/components/streamlabs/README.md index 64087e2a0bce8..d7d75e41a06c2 100644 --- a/components/streamlabs/README.md +++ b/components/streamlabs/README.md @@ -1,12 +1,11 @@ # Overview -The Streamlabs API allows developers to create powerful applications and -integrations with Streamlabs. With the Streamlabs API, you can create custom -tools and experiences tailored specifically to your users. Streamlabs offers -the tools and resources you need to easily build the following: - -- Engagement tools such as interactive overlays, polls, and stream alerts -- Chat bots allowing users to interact with each other in fun ways -- Integrations with other services, such as Discord and Twitch -- Custom analytics and reporting -- Custom donation pages, payments, and more +The Streamlabs API opens doors to automating and enhancing live streaming experiences. By tapping into Streamlabs' functionalities, you can automate alerts, manage donations, and interact with your audience in real time. Augment your streaming workflow by integrating with other services to cut down on manual processes, respond to events as they happen, and personalize the interaction with your viewers. + +# Example Use Cases + +- **Automated Donation Thank-Yous**: Instantly acknowledge donations by triggering personalized thank-you messages or on-screen alerts using Streamlabs with Pipedream. Integrate with a messaging app like Discord or Slack to send a special shout-out to your community's channel whenever a donation is made. + +- **Real-Time Alerts for New Followers**: Keep the hype going by setting up real-time follower notifications. When someone follows, use Streamlabs with Pipedream to broadcast a custom alert on your stream, and at the same time, capture the follower's info for your records or further engagement strategies. + +- **Stream Performance Analytics Digest**: After each stream, automatically gather performance analytics from Streamlabs and send a summary to your email or store it in a Google Sheet. Use Pipedream's cron scheduling to trigger this workflow at a regular interval, ensuring you have consistent post-stream reviews. diff --git a/components/streamlabs/actions/create-donation/create-donation.mjs b/components/streamlabs/actions/create-donation/create-donation.mjs new file mode 100644 index 0000000000000..f4e6226937400 --- /dev/null +++ b/components/streamlabs/actions/create-donation/create-donation.mjs @@ -0,0 +1,74 @@ +import streamlabs from "../../streamlabs.app.mjs"; +import currencies from "../../common/currencies.mjs"; + +export default { + key: "streamlabs-create-donation", + name: "Create Donation", + description: "Create a donation for the authenticated user. [See the documentation](https://dev.streamlabs.com/reference/donations-1)", + version: "0.0.1", + type: "action", + props: { + streamlabs, + name: { + type: "string", + label: "Name", + description: "The name of the donor. Has to be between 2-25 length and should only contain utf8 characters", + }, + identifier: { + type: "string", + label: "Identifier", + description: "An identifier for this donor, which is used to group donations with the same donor. For example, if you create more than one donation with the same identifier, they will be grouped together as if they came from the same donor. Typically this is best suited as an email address, or a unique hash.", + }, + amount: { + type: "string", + label: "Amount", + description: "The amount of this donation", + }, + currency: { + type: "string", + label: "Currency", + description: "The 3 letter currency code for this donation. Must be one of the [supported currency codes](https://dev.streamlabs.com/docs/currency-codes)", + options: currencies, + }, + message: { + type: "string", + label: "Message", + description: "The message from the donor. Must be < 255 characters", + optional: true, + }, + createdAt: { + type: "string", + label: "Created At", + description: "A timestamp that identifies when this donation was made. If left blank, it will default to now. Enter in ISO-8601 format (e.g., `2018-02-18T02:30:00-07:00` or `2018-02-18T08:00:00Z`, where Z stands for UTC)", + optional: true, + }, + skipAlert: { + type: "string", + label: "Skip Alert", + description: "Set to `yes` if you need to skip the alert. Default is `no`", + options: [ + "yes", + "no", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.streamlabs.createDonation({ + $, + data: { + name: this.name, + identifier: this.identifier, + amount: parseFloat(this.amount), + currency: this.currency, + message: this.message, + createdAt: this.createdAt && Date.parse(this.createdAt), + skip_alert: this.skipAlert, + }, + }); + if (response?.donation_id) { + $.export("$summary", `Successfully created donation with ID: ${response.donation_id}`); + } + return response; + }, +}; diff --git a/components/streamlabs/actions/send-alert/send-alert.mjs b/components/streamlabs/actions/send-alert/send-alert.mjs new file mode 100644 index 0000000000000..90464f67a3405 --- /dev/null +++ b/components/streamlabs/actions/send-alert/send-alert.mjs @@ -0,0 +1,74 @@ +import streamlabs from "../../streamlabs.app.mjs"; + +export default { + key: "streamlabs-send-alert", + name: "Send Alert", + description: "Sends an alert to the stream overlay with a custom message, image, and sound. [See the documentation](https://dev.streamlabs.com/reference/alerts)", + version: "0.0.1", + type: "action", + props: { + streamlabs, + type: { + type: "string", + label: "Type", + description: "determines which alert box this alert will show up in", + options: [ + "follow", + "subscription", + "donation", + "host", + ], + }, + message: { + type: "string", + label: "Message", + description: "The message to show with this alert", + }, + imageHref: { + type: "string", + label: "Image HREF", + description: "The href pointing to an image resource to play when this alert shows", + optional: true, + }, + soundHref: { + type: "string", + label: "Sound HREF", + description: "The href pointing to a sound resource to play when this alert shows", + optional: true, + }, + userMessage: { + type: "string", + label: "User Message", + description: "Acting as the second heading, this shows below message", + optional: true, + }, + duration: { + type: "string", + label: "Duration", + description: "How many seconds this alert should be displayed. Value should be in milliseconds. Ex: `1000` for 1 second.", + optional: true, + }, + specialTextColor: { + type: "string", + label: "Special Text Color", + description: "The color to use for special tokens. Must be a valid CSS color string", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.streamlabs.sendAlert({ + $, + data: { + type: this.type, + message: this.message, + image_href: this.imageHref, + sound_href: this.soundHref, + user_message: this.userMessage, + duration: this.duration, + special_text_color: this.specialTextColor, + }, + }); + $.export("$summary", `Alert sent with message: ${this.message}`); + return response; + }, +}; diff --git a/components/streamlabs/actions/send-test-alert/send-test-alert.mjs b/components/streamlabs/actions/send-test-alert/send-test-alert.mjs new file mode 100644 index 0000000000000..3ededd67bcfb5 --- /dev/null +++ b/components/streamlabs/actions/send-test-alert/send-test-alert.mjs @@ -0,0 +1,64 @@ +import streamlabs from "../../streamlabs.app.mjs"; + +export default { + key: "streamlabs-send-test-alert", + name: "Send Test Alert", + description: "Send a test alert to the stream overlay in StreamLabs. [See the documentation](https://dev.streamlabs.com/reference/alertssend_test_alert)", + version: "0.0.1", + type: "action", + props: { + streamlabs, + platform: { + type: "string", + label: "Platform", + description: "The streaming platform", + options: [ + "twitch", + "youtube", + ], + reloadProps: true, + }, + }, + additionalProps() { + if (!this.platform) { + return {}; + } + const props = { + type: { + type: "string", + label: "Type", + description: "The type of the alert", + }, + }; + if (this.platform === "twitch") { + props.type.options = [ + "follow", + "subscription", + "donation", + "host", + "bits", + "raid", + ]; + } + if (this.platform === "youtube") { + props.type.options = [ + "subscription", + "sponsor", + "superchat", + "donation", + ]; + } + return props; + }, + async run({ $ }) { + const response = await this.streamlabs.sendTestAlert({ + $, + data: { + platform: this.platform, + type: this.type, + }, + }); + $.export("$summary", "Successfully sent test alert"); + return response; + }, +}; diff --git a/components/streamlabs/common/currencies.mjs b/components/streamlabs/common/currencies.mjs new file mode 100644 index 0000000000000..8a86e5edfa79c --- /dev/null +++ b/components/streamlabs/common/currencies.mjs @@ -0,0 +1,90 @@ +export default [ + { + value: "USD", + label: "US Dollar", + }, + { + value: "AUD", + label: "Australian Dollar", + }, + { + value: "BRL", + label: "Brazilian Real", + }, + { + value: "CAD", + label: "Canadian Dollar", + }, + { + value: "CZK", + label: "Czech Koruna", + }, + { + value: "DKK", + label: "Danish Krone", + }, + { + value: "EUR", + label: "Euro", + }, + { + value: "HKD", + label: "Hong Kong Dollar", + }, + { + value: "ILS", + label: "Israeli New Sheqel", + }, + { + value: "MYR", + label: "Malaysian Ringgit", + }, + { + value: "MXN", + label: "Mexican Peso", + }, + { + value: "NOK", + label: "Norwegian Krone", + }, + { + value: "NZD", + label: "New Zealand Dollar", + }, + { + value: "PHP", + label: "Philippine Peso", + }, + { + value: "PLN", + label: "Polish Zloty", + }, + { + value: "GBP", + label: "Pound Sterling", + }, + { + value: "RUB", + label: "Russian Ruble", + }, + { + value: "SGD", + label: "Singapore Dollar", + }, + { + value: "SEK", + label: "Swedish Krona", + }, + { + value: "CHF", + label: "Swiss Franc", + }, + { + value: "THB", + label: "Thai Baht", + }, + { + value: "TRY", + label: "Turkish Lira", + }, +]; diff --git a/components/streamlabs/package.json b/components/streamlabs/package.json new file mode 100644 index 0000000000000..b6f33111e3558 --- /dev/null +++ b/components/streamlabs/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/streamlabs", + "version": "0.0.1", + "description": "Pipedream Streamlabs Components", + "main": "streamlabs.app.mjs", + "keywords": [ + "pipedream", + "streamlabs" + ], + "homepage": "https://pipedream.com/apps/streamlabs", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/streamlabs/streamlabs.app.mjs b/components/streamlabs/streamlabs.app.mjs index 11983339a57f6..7b2f37cdf4ba0 100644 --- a/components/streamlabs/streamlabs.app.mjs +++ b/components/streamlabs/streamlabs.app.mjs @@ -1,11 +1,51 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "streamlabs", propDefinitions: {}, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://streamlabs.com/api/v1.0"; + }, + _accessToken() { + return this.$auth.oauth_access_token; + }, + _makeRequest({ + $ = this, + path, + data, + ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + data: { + access_token: this._accessToken(), + ...data, + }, + }); + }, + sendAlert(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/alerts", + ...opts, + }); + }, + createDonation(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/donations", + ...opts, + }); + }, + sendTestAlert(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/alerts/send_test_alert", + ...opts, + }); }, }, }; diff --git a/components/streamtime/README.md b/components/streamtime/README.md new file mode 100644 index 0000000000000..f9e07276d539e --- /dev/null +++ b/components/streamtime/README.md @@ -0,0 +1,11 @@ +# Overview + +The Streamtime API allows you to interact with the Streamtime project management platform, streamlining operations such as task management, time tracking, and project planning. On Pipedream, you can leverage this API to create automated workflows that integrate with various other services, trigger actions based on events, and sync data across apps. It's about connecting Streamtime's capabilities with other tools you use for a seamless project management experience. + +# Example Use Cases + +- **Sync Streamtime Projects with Google Calendar**: Create a workflow on Pipedream that listens for new projects or deadlines on Streamtime and automatically adds them as events in Google Calendar. This keeps your schedule up-to-date across platforms without manual entry. + +- **Automated Time Tracking Reminders**: Set up a Pipedream workflow that sends a Slack message to team members who haven't logged their time in Streamtime by the end of the day. This ensures that time tracking is kept consistent and helps in accurate project billing. + +- **Create Invoices from Completed Tasks**: Build a workflow that watches for tasks marked as completed in Streamtime and uses that data to generate invoices through an accounting app like QuickBooks. This automates the billing process, reducing the time spent on administrative tasks. diff --git a/components/streamwish/README.md b/components/streamwish/README.md new file mode 100644 index 0000000000000..6fad5c91b3969 --- /dev/null +++ b/components/streamwish/README.md @@ -0,0 +1,11 @@ +# Overview + +The StreamWish API lets you connect and automate actions related to streaming services. With Pipedream, you can create workflows that trigger on specific events or schedules and use the StreamWish API to manage wishlists, alert viewers to new content, or aggregate streaming service data. By integrating the StreamWish API into Pipedream, you can design custom automations that enhance viewer engagement and streamline the content discovery process. + +# Example Use Cases + +- **Content Release Notifications**: Use Pipedream to monitor StreamWish for new releases on a user's wishlist. When a new item is available, automatically send out notifications via email, SMS, or a platform like Slack to inform the user immediately. + +- **Viewer Engagement Analytics**: Trigger a workflow on Pipedream when a user adds an item to their wishlist on StreamWish. Collect these events to track engagement and preferences, then push this data to Google Sheets or a database for analysis and visualization. + +- **Social Media Content Promotion**: Whenever a popular wishlist item becomes available on a streaming service, automatically post an update on social media platforms like Twitter or Facebook through Pipedream to engage followers and drive traffic to the content. diff --git a/components/streamwish/package.json b/components/streamwish/package.json index 88702a1cef4fa..9e6458060b749 100644 --- a/components/streamwish/package.json +++ b/components/streamwish/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/stripe/README.md b/components/stripe/README.md index 4cfbc7de9aded..f7d2c44e413f3 100644 --- a/components/stripe/README.md +++ b/components/stripe/README.md @@ -1,11 +1,60 @@ # Overview -With Stripe, you can easily accept payments online. Stripe provides a simple -and powerful way to accept payments over the internet. With Stripe, you can -easily - -- Accept payments from major credit and debit cards -- Process payments in over 130 currencies -- Set up recurring billing -- Accept Apple Pay, Google Pay, and other alternative payment methods -- And much more! +The Stripe API is a powerful tool for managing online payments, subscriptions, and invoices. With Pipedream, you can leverage this API to automate payment processing, monitor transactions, and sync billing data with other services. Pipedream's no-code platform allows for quick integration and creation of serverless workflows that react to Stripe events in real-time. For instance, you might automatically update customer records, send personalized emails after successful payments, or escalate failed transactions to your support team. + + +# Getting Started + +To connect your Stripe account to Pipedream, you'll need to generate a new Stripe API key and save it in Pipedream. + +1. Open the [Stripe API keys dashboard](https://dashboard.stripe.com/apikeys) + +2. Create a new Restricted Key + +3. Connect your Restricted API key + +Once your Restricted API key is created, copy and paste it into Pipedream, and click *Save* + +That's it, now you're able to use your Stripe API key within your Pipedream workflows with no-code actions or with Node.js/Python. + +# Example Use Cases + +- **Customer Subscription Lifecycle Management**: Create a workflow that triggers when a new customer subscribes to a service. The workflow can update a CRM like Salesforce with the new subscription details, send a welcome email via SendGrid, and create a task in Asana for the onboarding team. + +- **Real-Time Fraud Alerting System**: Set up a Pipedream workflow that listens for Stripe events indicating possible fraudulent activity, such as multiple declined payments. When detected, the workflow can immediately send alerts through Slack to your risk management team and log the incident in a Google Sheet for review. + +- **Monthly Financial Reporting**: Develop a scheduled workflow that runs at the end of each month. It can fetch transaction data from Stripe, aggregate sales and refunds, calculate net revenue, and then compile and send a report to stakeholders via email or post it to a private report dashboard like Tableau. + +# Troubleshooting + +Stripe's API uses standard HTTP status codes to describe errors. + +## 200 - OK + +The request was received and everything worked as expected. + +## 400 - Bad Request + +The request is missing data, or has malformed data. Double check that your payload has the required arguments and it's properly formatted. + +## 401 - Unauthorized + +The request isn't authorized to perform the action. Double check that the restricted key you created for Pipedream has access to the resource you're attempting to use. + +You can also edit exisiting keys within your [Stripe API key dashboard](https://dashboard.stripe.com/apikeys), which is more convienent than creating a new keys. + +## 404 - Not found + +The resource isn't available. This usually means that the ID to the resource like the customer, subscription or transaction, etc. is incorrect, or is missing from the URL. + +## 409 - Conflict + +This request conflicts with another. This usually is due to the same idempotency key being used on another request. You may need to reduce the [concurrency and throttle the workflow](https://pipedream.com/docs/workflows/concurrency-and-throttling) to a single worker to prevent race conditions. + +## 429 - Rate limit reached + +You've reached the API rate limit, you'll need to reduce how frequently you're sending Stripe API requests with that key. Stripe allows 100 read requests and 100 write requests per second. + +## 5xx - Server error + +Something went wrong on Stripe's end. Check the [Stripe status page](https://status.stripe.com/) for more details. \ No newline at end of file diff --git a/components/stripe/package.json b/components/stripe/package.json index 15adf67db34ad..bc94345284cee 100644 --- a/components/stripe/package.json +++ b/components/stripe/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/stripe", - "version": "0.6.0", + "version": "0.6.2", "description": "Pipedream Stripe Components", "main": "stripe.app.mjs", "keywords": [ diff --git a/components/stripe/sources/abandoned-cart/abandoned-cart.mjs b/components/stripe/sources/abandoned-cart/abandoned-cart.mjs index e7c3c4e54a93a..9363fe973befb 100644 --- a/components/stripe/sources/abandoned-cart/abandoned-cart.mjs +++ b/components/stripe/sources/abandoned-cart/abandoned-cart.mjs @@ -5,7 +5,7 @@ export default { key: "stripe-abandoned-cart", name: "New Abandoned Cart", type: "source", - version: "0.1.0", + version: "0.1.1", description: "Emit new event when a customer abandons their cart.", methods: { ...common.methods, diff --git a/components/stripe/sources/canceled-subscription/canceled-subscription.mjs b/components/stripe/sources/canceled-subscription/canceled-subscription.mjs index d071a950d700e..01600f021b655 100644 --- a/components/stripe/sources/canceled-subscription/canceled-subscription.mjs +++ b/components/stripe/sources/canceled-subscription/canceled-subscription.mjs @@ -5,7 +5,7 @@ export default { key: "stripe-canceled-subscription", name: "Canceled Subscription", type: "source", - version: "0.1.0", + version: "0.1.1", description: "Emit new event for each new canceled subscription", methods: { ...common.methods, diff --git a/components/stripe/sources/common/webhook-base.mjs b/components/stripe/sources/common/webhook-base.mjs index ca5ebadae9fcd..0256f192277cf 100644 --- a/components/stripe/sources/common/webhook-base.mjs +++ b/components/stripe/sources/common/webhook-base.mjs @@ -14,9 +14,12 @@ export default { throw new Error("getEvent method is not implemented"); }, emitEvent(event) { + const idText = event.data.object?.id ? + `object ID ${event.data.object.id}` : + `event ID ${event.id}`; this.$emit(event, { id: event.id, - summary: `New event ${event.type} with ID ${event.data.id}`, + summary: `New event ${event.type} (${idText})`, ts: Date.parse(event.created), }); }, diff --git a/components/stripe/sources/custom-webhook-events/custom-webhook-events.mjs b/components/stripe/sources/custom-webhook-events/custom-webhook-events.mjs index 1ad32a9f311f2..e20dac143c5ae 100644 --- a/components/stripe/sources/custom-webhook-events/custom-webhook-events.mjs +++ b/components/stripe/sources/custom-webhook-events/custom-webhook-events.mjs @@ -7,7 +7,7 @@ export default { key: "stripe-custom-webhook-events", name: "New Custom Webhook Events", type: "source", - version: "0.1.0", + version: "0.1.1", description: "Emit new event on each webhook event", props: { ...common.props, diff --git a/components/stripe/sources/new-customer/new-customer.mjs b/components/stripe/sources/new-customer/new-customer.mjs index b851cec240b69..282604f6ebc96 100644 --- a/components/stripe/sources/new-customer/new-customer.mjs +++ b/components/stripe/sources/new-customer/new-customer.mjs @@ -5,7 +5,7 @@ export default { key: "stripe-new-customer", name: "New Customer", type: "source", - version: "0.1.0", + version: "0.1.1", description: "Emit new event for each new customer", methods: { ...common.methods, diff --git a/components/stripe/sources/new-dispute/new-dispute.mjs b/components/stripe/sources/new-dispute/new-dispute.mjs index 50cc325a9f822..d5c749fb335c1 100644 --- a/components/stripe/sources/new-dispute/new-dispute.mjs +++ b/components/stripe/sources/new-dispute/new-dispute.mjs @@ -5,7 +5,7 @@ export default { key: "stripe-new-dispute", name: "New Dispute", type: "source", - version: "0.1.0", + version: "0.1.1", description: "Emit new event for each new dispute", methods: { ...common.methods, diff --git a/components/stripe/sources/new-failed-invoice-payment/new-failed-invoice-payment.mjs b/components/stripe/sources/new-failed-invoice-payment/new-failed-invoice-payment.mjs index 58a58172b2608..f56ac808d347d 100644 --- a/components/stripe/sources/new-failed-invoice-payment/new-failed-invoice-payment.mjs +++ b/components/stripe/sources/new-failed-invoice-payment/new-failed-invoice-payment.mjs @@ -5,7 +5,7 @@ export default { key: "stripe-new-failed-invoice-payment", name: "New Failed Invoice Payment", type: "source", - version: "0.1.0", + version: "0.1.1", description: "Emit new event for each new failed invoice payment", methods: { ...common.methods, diff --git a/components/stripe/sources/new-failed-payment/new-failed-payment.mjs b/components/stripe/sources/new-failed-payment/new-failed-payment.mjs index c34c25c6f8fe4..aa14c8818d4f5 100644 --- a/components/stripe/sources/new-failed-payment/new-failed-payment.mjs +++ b/components/stripe/sources/new-failed-payment/new-failed-payment.mjs @@ -5,7 +5,7 @@ export default { key: "stripe-new-failed-payment", name: "New Failed Payment", type: "source", - version: "0.1.0", + version: "0.1.1", description: "Emit new event for each new failed payment", methods: { ...common.methods, diff --git a/components/stripe/sources/new-invoice/new-invoice.mjs b/components/stripe/sources/new-invoice/new-invoice.mjs index fd2d212f760a8..0678061f4d0af 100644 --- a/components/stripe/sources/new-invoice/new-invoice.mjs +++ b/components/stripe/sources/new-invoice/new-invoice.mjs @@ -5,7 +5,7 @@ export default { key: "stripe-new-invoice", name: "New Invoice", type: "source", - version: "0.1.0", + version: "0.1.1", description: "Emit new event for each new invoice", methods: { ...common.methods, diff --git a/components/stripe/sources/new-payment/new-payment.mjs b/components/stripe/sources/new-payment/new-payment.mjs index ccce72d1f6ee9..7d2be492f3daf 100644 --- a/components/stripe/sources/new-payment/new-payment.mjs +++ b/components/stripe/sources/new-payment/new-payment.mjs @@ -5,7 +5,7 @@ export default { key: "stripe-new-payment", name: "New Payment", type: "source", - version: "0.1.0", + version: "0.1.2", description: "Emit new event for each new payment", methods: { ...common.methods, @@ -14,5 +14,15 @@ export default { "payment_intent.created", ]; }, + emitEvent(event) { + const amount = event.data.object?.amount; + this.$emit(event, { + id: event.id, + summary: `New payment${amount + ? " of $" + amount + : ""} received`, + ts: Date.parse(event.created), + }); + }, }, }; diff --git a/components/stripe/sources/new-subscription/new-subscription.mjs b/components/stripe/sources/new-subscription/new-subscription.mjs index 1e9cf331005ef..1fb5b8767a6b8 100644 --- a/components/stripe/sources/new-subscription/new-subscription.mjs +++ b/components/stripe/sources/new-subscription/new-subscription.mjs @@ -5,7 +5,7 @@ export default { key: "stripe-new-subscription", name: "New Subscription", type: "source", - version: "0.1.0", + version: "0.1.1", description: "Emit new event for each new subscription", methods: { ...common.methods, diff --git a/components/stripe/sources/subscription-updated/subscription-updated.mjs b/components/stripe/sources/subscription-updated/subscription-updated.mjs index 3b273005f474c..08bab842d1695 100644 --- a/components/stripe/sources/subscription-updated/subscription-updated.mjs +++ b/components/stripe/sources/subscription-updated/subscription-updated.mjs @@ -5,7 +5,7 @@ export default { key: "stripe-subscription-updated", name: "Subscription Updated", type: "source", - version: "0.1.0", + version: "0.1.1", description: "Emit new event on a new subscription is updated", methods: { ...common.methods, diff --git a/components/stripo/README.md b/components/stripo/README.md new file mode 100644 index 0000000000000..29c50c29cd61a --- /dev/null +++ b/components/stripo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Stripo API lets you automate email design, creation, and management processes, streamlining your workflow within Pipedream. With Stripo, you can create email templates, customize them, and export directly to various email services. Pipedream, with its serverless execution model, can trigger workflows based on numerous events, such as webhook invocations, email events, or schedule timers. By combining Stripo with Pipedream, you can craft dynamic email campaigns, auto-generate personalized emails from templates, and synchronize your email designs with other platforms or databases effortlessly. + +# Example Use Cases + +- **Automated Email Campaign Creation**: With Stripo integrated into Pipedream, you can automate the creation of email campaigns. When a new subscriber is added to your CRM, a Pipedream workflow can trigger, pulling a pre-designed template from Stripo, customizing it with the subscriber's details, and pushing it to your email marketing platform. + +- **Dynamic Content Updates for Templates**: Set up a workflow that listens for updates in your CMS. When new content goes live, Pipedream can automatically update your Stripo email templates with the latest articles, products, or announcements, ensuring your email content stays fresh and engaging. + +- **Sync Design Changes Across Platforms**: Pipedream can monitor for design updates in Stripo. When you save a new version of an email template, the workflow can trigger to export and sync these changes to multiple email service providers or marketing tools, maintaining consistency across your email campaigns. diff --git a/components/stripo/actions/get-raw-html/get-raw-html.mjs b/components/stripo/actions/get-raw-html/get-raw-html.mjs new file mode 100644 index 0000000000000..f7146d996e402 --- /dev/null +++ b/components/stripo/actions/get-raw-html/get-raw-html.mjs @@ -0,0 +1,26 @@ +import stripo from "../../stripo.app.mjs"; + +export default { + key: "stripo-get-raw-html", + name: "Get Raw HTML & CSS", + description: "Retrieves the HTML and CSS code of the selected email message in Stripo. [See the documentation](https://api.stripo.email/reference/getrawemail)", + version: "0.0.1", + type: "action", + props: { + stripo, + emailId: { + propDefinition: [ + stripo, + "emailId", + ], + }, + }, + async run({ $ }) { + const response = await this.stripo.getRawHtml({ + $, + emailId: this.emailId, + }); + $.export("$summary", `Successfully retrieved HTML & CSS from email with ID: ${this.emailId}`); + return response; + }, +}; diff --git a/components/stripo/actions/remove-email/remove-email.mjs b/components/stripo/actions/remove-email/remove-email.mjs new file mode 100644 index 0000000000000..0572393109732 --- /dev/null +++ b/components/stripo/actions/remove-email/remove-email.mjs @@ -0,0 +1,26 @@ +import stripo from "../../stripo.app.mjs"; + +export default { + key: "stripo-remove-email", + name: "Remove Email", + description: "Removes an existing message from the user's project in Stripo. [See the documentation](https://api.stripo.email/reference/deleteemail)", + version: "0.0.1", + type: "action", + props: { + stripo, + emailId: { + propDefinition: [ + stripo, + "emailId", + ], + }, + }, + async run({ $ }) { + const response = await this.stripo.removeEmail({ + $, + emailId: this.emailId, + }); + $.export("$summary", `Successfully removed email with ID: ${this.emailId}`); + return response; + }, +}; diff --git a/components/stripo/actions/search-emails/search-emails.mjs b/components/stripo/actions/search-emails/search-emails.mjs new file mode 100644 index 0000000000000..b9c2a586cb4a5 --- /dev/null +++ b/components/stripo/actions/search-emails/search-emails.mjs @@ -0,0 +1,44 @@ +import stripo from "../../stripo.app.mjs"; + +export default { + key: "stripo-search-emails", + name: "Search Emails", + description: "Searches existing emails by search query in Stripo. [See the documentation](https://api.stripo.email/reference/findemails)", + version: "0.0.1", + type: "action", + props: { + stripo, + query: { + propDefinition: [ + stripo, + "query", + ], + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "The maximum number of results to return", + default: 100, + optional: true, + }, + }, + async run({ $ }) { + const results = this.stripo.paginate({ + fn: this.stripo.listEmails, + params: { + queryStr: this.query, + }, + max: this.maxResults, + }); + + const emails = []; + for await (const item of results) { + emails.push(item); + } + + $.export("$summary", `Successfully retrieved ${emails.length} email${emails.length === 1 + ? "" + : "s"}`); + return emails; + }, +}; diff --git a/components/stripo/package.json b/components/stripo/package.json new file mode 100644 index 0000000000000..f4200d7bf8425 --- /dev/null +++ b/components/stripo/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/stripo", + "version": "0.1.0", + "description": "Pipedream Stripo Components", + "main": "stripo.app.mjs", + "keywords": [ + "pipedream", + "stripo" + ], + "homepage": "https://pipedream.com/apps/stripo", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/stripo/sources/new-email-created/new-email-created.mjs b/components/stripo/sources/new-email-created/new-email-created.mjs new file mode 100644 index 0000000000000..bcca28cca9157 --- /dev/null +++ b/components/stripo/sources/new-email-created/new-email-created.mjs @@ -0,0 +1,84 @@ +import stripo from "../../stripo.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "stripo-new-email-created", + name: "New Email Created", + description: "Emit new event when a new email is created in Stripo. [See the documentation](https://api.stripo.email/reference/findemails)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + stripo, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + query: { + propDefinition: [ + stripo, + "query", + ], + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + generateMeta(email) { + return { + id: email.emailId, + summary: `New Email: ${email.name}`, + ts: Date.parse(email.updatedTime), + }; + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + + const results = this.stripo.paginate({ + fn: this.stripo.listEmails, + params: { + queryStr: this.query, + sortingColumn: "createdTime", + sortingAsc: false, + }, + max, + }); + + const emails = []; + for await (const item of results) { + const ts = Date.parse(item.updatedTime); + if (ts >= lastTs) { + emails.push(item); + } + } + + if (!emails.length) { + return; + } + + this._setLastTs(Date.parse(emails[0].updatedTime)); + + emails.reverse().forEach((email) => { + const meta = this.generateMeta(email); + this.$emit(email, meta); + }); + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + async run() { + await this.processEvent(); + }, + sampleEmit, +}; diff --git a/components/stripo/sources/new-email-created/test-event.mjs b/components/stripo/sources/new-email-created/test-event.mjs new file mode 100644 index 0000000000000..516b4ed70b184 --- /dev/null +++ b/components/stripo/sources/new-email-created/test-event.mjs @@ -0,0 +1,11 @@ +export default { + "emailId": 9031276, + "name": "New Message", + "editorUrl": "https://my.stripo.email/cabinet/#/template-editor/?emailId=9031276&projectId=1189279&type=EMAIL", + "previewUrl": "https://viewstripo.email/575439dc-9d57-4e3f-989e-23a1a63971901738788020521", + "updatedTime": "2025-02-05T20:40:34.46", + "hasAmp": false, + "preheader": null, + "title": "", + "previewImage": "https://doc.stripocdn.email/content/guids/cabinet-icons/87baef10-e401-11ef-a29e-e99b514f747b.png" +} \ No newline at end of file diff --git a/components/stripo/stripo.app.mjs b/components/stripo/stripo.app.mjs new file mode 100644 index 0000000000000..433af24ff2f07 --- /dev/null +++ b/components/stripo/stripo.app.mjs @@ -0,0 +1,98 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "stripo", + propDefinitions: { + emailId: { + type: "string", + label: "Email ID", + description: "The identifier of an email", + async options({ page }) { + const { data } = await this.listEmails({ + params: { + page, + }, + }); + return data?.map(({ + emailId: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + query: { + type: "string", + label: "Query", + description: "The query to search for", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://my.stripo.email/emailgeneration/v1"; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "Stripo-Api-Auth": this.$auth.api_key, + "Content-Type": "application/json", + }, + ...opts, + }); + }, + listEmails(opts = {}) { + return this._makeRequest({ + path: "/emails", + ...opts, + }); + }, + getRawHtml({ + emailId, ...opts + }) { + return this._makeRequest({ + path: `/raw-email/${emailId}`, + ...opts, + }); + }, + removeEmail({ + emailId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/emails/${emailId}`, + ...opts, + }); + }, + async *paginate({ + fn, params, max, + }) { + params = { + ...params, + page: 0, + }; + let totalResults, count = 0; + do { + const { + data, total, + } = await fn({ + params, + }); + totalResults = total; + for (const item of data) { + yield item; + count++; + if (max && count >= max) { + return; + } + } + } while (count < totalResults); + }, + }, +}; diff --git a/components/studio_by_ai21_labs/README.md b/components/studio_by_ai21_labs/README.md new file mode 100644 index 0000000000000..4fa7140760fc4 --- /dev/null +++ b/components/studio_by_ai21_labs/README.md @@ -0,0 +1,11 @@ +# Overview + +Studio by AI21 Labs API enables the crafting of human-like text using advanced language models. It can generate content, answer questions, summarize text, or even customize language models for specific tasks. When integrated into Pipedream, this API becomes part of powerful automations that can process and generate textual content dynamically, reacting to various triggers such as incoming emails, form submissions, or scheduled events. + +# Example Use Cases + +- **Content Generation on Demand**: Trigger a workflow with a webhook that receives topics from a CMS. Use AI21 Labs to generate blog posts or articles based on those topics, and then send the generated content back to the CMS or a team member for review. + +- **Automated Customer Support**: Connect a customer support ticketing system to trigger when a new ticket arrives. Use AI21 Labs to compose draft replies or suggest solutions based on the ticket content, reducing response time and assisting support teams. + +- **Event-Driven Email Summarization**: Set up a workflow that triggers when new emails arrive in a Gmail inbox. Extract the body of the email and use AI21 Labs to provide a summary. This summary can then be posted to a Slack channel or saved in a Google Doc for quick review. diff --git a/components/successeve/README.md b/components/successeve/README.md new file mode 100644 index 0000000000000..7758bdf131aa6 --- /dev/null +++ b/components/successeve/README.md @@ -0,0 +1,11 @@ +# Overview + +The Successeve API is a tool for managing events, attendees, and ticketing. It's built to simplify the process of organizing events and handling participant engagement. On Pipedream, you can leverage this API to automate event-related tasks, sync data with other services like CRM platforms, or trigger communications based on event activities. By connecting Successeve to various apps on Pipedream, you can create seamless workflows to enhance event management efficiency. + +# Example Use Cases + +- **Automated Event Follow-Up Emails**: After an event concludes, trigger an automated workflow on Pipedream that sends a follow-up email to attendees via a service like SendGrid. This can include a thank-you message, a survey link, or promotional content for upcoming events. + +- **Sync Event Attendees to CRM**: When a new attendee registers for an event, use the Successeve API to capture their details and add them to a CRM platform like Salesforce. This workflow ensures your sales team has the latest information to engage with attendees and tailor follow-up interactions. + +- **Real-Time Slack Notifications for New Registrations**: Set up a Pipedream workflow that listens for new event registrations and posts a notification to a designated Slack channel. This can keep your team informed about the attendee count and engagement in real-time. diff --git a/components/sugarcrm_/README.md b/components/sugarcrm_/README.md new file mode 100644 index 0000000000000..37db97e55bf26 --- /dev/null +++ b/components/sugarcrm_/README.md @@ -0,0 +1,11 @@ +# Overview + +The SugarCRM API unlocks the potential to streamline customer relationship management by automating interactions and data flow between SugarCRM and other platforms. With Pipedream, you can harness this API to create serverless workflows that react to CRM events, synchronize data, or trigger actions based on predefined conditions. Pipedream's no-code platform simplifies connecting SugarCRM to hundreds of other apps, enabling non-technical users to build complex integrations with ease. + +# Example Use Cases + +- **Sync Contacts to Google Sheets**: Automatically export new or updated contacts from SugarCRM to a Google Sheet for easy sharing and data analysis. This workflow triggers each time a contact is created or modified in SugarCRM. + +- **Customer Support Ticket Creation**: Create a support ticket in a tool like Zendesk or Help Scout whenever a case is updated in SugarCRM with a specific status, ensuring that customer support is promptly notified and can act quickly. + +- **Lead Score Enhancement with Clearbit**: Enrich lead data in SugarCRM by pulling in additional information from Clearbit whenever a new lead is added. Use the enriched data to score and prioritize leads more effectively for sales follow-up. diff --git a/components/sugarcrm_/package.json b/components/sugarcrm_/package.json index dd274434670ee..1bffd37c22662 100644 --- a/components/sugarcrm_/package.json +++ b/components/sugarcrm_/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/suitedash/.gitignore b/components/suitedash/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/suitedash/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/suitedash/README.md b/components/suitedash/README.md new file mode 100644 index 0000000000000..f3754ad2718a4 --- /dev/null +++ b/components/suitedash/README.md @@ -0,0 +1,11 @@ +# Overview + +The SuiteDash API lets you streamline business operations by automating tasks within SuiteDash, a platform designed for client management, project management, invoicing, and more. Leveraging this API in Pipedream, you can develop custom, serverless workflows. These can include syncing client data across platforms, automating project updates, or triggering event-based invoicing. The key here is to harness the data and actions available through the API to enhance efficiency and reduce manual workload. + +# Example Use Cases + +- **Client Onboarding Automation**: When a new client signs up on your website, use Pipedream to trigger a workflow that creates a new client profile in SuiteDash. You can then send a welcome email from SuiteDash or another email service connected within Pipedream, like SendGrid. + +- **Project Management Integration**: Connect SuiteDash to a project management tool like Trello using Pipedream. Each time a project is updated in SuiteDash, automatically create a corresponding card in Trello with project details. This keeps teams aligned and ensures that project statuses are mirrored across both platforms. + +- **Invoice Generation and Follow-Up**: After a project is marked complete in SuiteDash, use Pipedream to generate an invoice automatically and send it to the client. In addition, schedule follow-up reminders via email or SMS (using an integrated service like Twilio) if the invoice remains unpaid after a certain period. diff --git a/components/suitedash/actions/create-company/create-company.mjs b/components/suitedash/actions/create-company/create-company.mjs new file mode 100644 index 0000000000000..c497119ddf5a0 --- /dev/null +++ b/components/suitedash/actions/create-company/create-company.mjs @@ -0,0 +1,79 @@ +import suitedash from "../../suitedash.app.mjs"; + +export default { + key: "suitedash-create-company", + name: "Create Company", + description: "Creates a new company in SuiteDash. [See the documentation](https://app.suitedash.com/secure-api/swagger)", + version: "0.0.1", + type: "action", + props: { + suitedash, + companyName: { + propDefinition: [ + suitedash, + "companyName", + ], + }, + companyRole: { + propDefinition: [ + suitedash, + "role", + ], + }, + firstName: { + type: "string", + label: "First Name", + description: "First name of the company's primary contact", + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the company's primary contact", + }, + email: { + type: "string", + label: "Email", + description: "Email address of the company's primary contact", + }, + sendWelcomeEmail: { + type: "boolean", + label: "Send Welcome Email", + description: "Send welcome email to the primary contact. Default: `false`", + optional: true, + default: false, + }, + createPrimaryContactIfNotExists: { + type: "boolean", + label: "Create Primary Contact If Not Exists", + description: "Create a Primary Contact with all provided data if the email does not exist. Default: `true`", + optional: true, + default: true, + }, + preventIndividualMode: { + type: "boolean", + label: "Prevent Individual Mode", + description: "Prevent this Primary Contact from switching into `Individual Mode`. Default: `false`", + optional: true, + default: false, + }, + }, + async run({ $ }) { + const response = await this.suitedash.createCompany({ + $, + data: { + name: this.companyName, + role: this.companyRole, + primaryContact: { + first_name: this.firstName, + last_name: this.lastName, + email: this.email, + send_welcome_email: this.sendWelcomeEmail, + create_primary_contact_if_not_exists: this.createPrimaryContactIfNotExists, + prevent_individual_mode: this.preventIndividualMode, + }, + }, + }); + $.export("$summary", `Successfully created company ${this.companyName}`); + return response; + }, +}; diff --git a/components/suitedash/actions/create-contact/create-contact.mjs b/components/suitedash/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..e81198c85fae6 --- /dev/null +++ b/components/suitedash/actions/create-contact/create-contact.mjs @@ -0,0 +1,55 @@ +import suitedash from "../../suitedash.app.mjs"; + +export default { + key: "suitedash-create-contact", + name: "Create Contact", + description: "Creates a new contact in SuiteDash. [See the documentation](https://app.suitedash.com/secure-api/swagger)", + version: "0.0.1", + type: "action", + props: { + suitedash, + firstName: { + type: "string", + label: "Contact First Name", + description: "The first name of the new contact", + }, + lastName: { + type: "string", + label: "Contact Last Name", + description: "The last name of the new contact", + }, + email: { + type: "string", + label: "Contact Email", + description: "The email of the new contact", + }, + role: { + propDefinition: [ + suitedash, + "role", + ], + label: "Contact Role", + description: "The role of the new contact", + }, + sendWelcomeEmail: { + type: "boolean", + label: "Send Welcome Email", + description: "Whether to send a welcome email to the new contact", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.suitedash.createContact({ + $, + data: { + first_name: this.firstName, + last_name: this.lastName, + email: this.email, + role: this.role, + send_welcome_email: this.sendWelcomeEmail, + }, + }); + $.export("$summary", `Successfully created contact ${this.firstName} ${this.lastName}`); + return response; + }, +}; diff --git a/components/suitedash/actions/update-company/update-company.mjs b/components/suitedash/actions/update-company/update-company.mjs new file mode 100644 index 0000000000000..ee0f0959e2182 --- /dev/null +++ b/components/suitedash/actions/update-company/update-company.mjs @@ -0,0 +1,82 @@ +import suitedash from "../../suitedash.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "suitedash-update-company", + name: "Update Company", + description: "Updates an existing company's details in SuiteDash. [See the documentation](https://app.suitedash.com/secure-api/swagger)", + version: "0.0.1", + type: "action", + props: { + suitedash, + companyId: { + propDefinition: [ + suitedash, + "companyId", + ], + }, + companyName: { + propDefinition: [ + suitedash, + "companyName", + ], + optional: true, + }, + website: { + type: "string", + label: "Website", + description: "The website of the company.", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The phone number of the company", + optional: true, + }, + companyAddress: { + type: "string", + label: "Company Address", + description: "The full address of the company. Example: dba Staybridge Suites Mount Laurel 324 Church Road Mount Laurel, NJ 09478", + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "An array of tags associated with the company", + optional: true, + }, + backgroundInfo: { + type: "string", + label: "Background Info", + description: "Background information about the company", + optional: true, + }, + }, + async run({ $ }) { + if (!this.companyName + && !this.website + && !this.phone + && !this.companyAddress + && !this.tags + && !this.backgroundInfo + ) { + throw new ConfigurationError("Please enter at least one field to update"); + } + + const response = await this.suitedash.updateCompany({ + $, + companyId: this.companyId, + data: { + name: this.companyName, + website: this.website, + phone: this.phone, + full_address: this.companyAddress, + tags: this.tags, + background_info: this.backgroundInfo, + }, + }); + $.export("$summary", `Successfully updated company ${this.companyId}`); + return response; + }, +}; diff --git a/components/suitedash/app/suitedash.app.ts b/components/suitedash/app/suitedash.app.ts deleted file mode 100644 index c8b2488f4b65c..0000000000000 --- a/components/suitedash/app/suitedash.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "suitedash", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/suitedash/package.json b/components/suitedash/package.json index 8eb4b2fddf4be..ea621228da30d 100644 --- a/components/suitedash/package.json +++ b/components/suitedash/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/suitedash", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream SuiteDash Components", "main": "dist/app/suitedash.app.mjs", "keywords": [ "pipedream", "suitedash" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/suitedash", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/suitedash/sources/common/base.mjs b/components/suitedash/sources/common/base.mjs new file mode 100644 index 0000000000000..13274bd46d638 --- /dev/null +++ b/components/suitedash/sources/common/base.mjs @@ -0,0 +1,79 @@ +import suitedash from "../../suitedash.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + suitedash, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + getParams() { + return {}; + }, + getTsField() { + return "created"; + }, + generateMeta(item) { + return { + id: item.uid, + summary: this.getSummary(item), + ts: Date.parse(item[this.getTsField()]), + }; + }, + getFn() { + throw new Error("getFn is not implemented"); + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + let results = []; + + const items = this.suitedash.paginate({ + fn: this.getFn(), + params: this.getParams(), + }); + + for await (const item of items) { + const ts = Date.parse(item[this.getTsField()]); + if (ts >= lastTs) { + results.push(item); + maxTs = Math.max(ts, maxTs); + } + } + + this._setLastTs(maxTs); + + if (max) { + results = results.slice(-1 * max); + } + + results.forEach((item) => { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/suitedash/sources/new-company/new-company.mjs b/components/suitedash/sources/new-company/new-company.mjs new file mode 100644 index 0000000000000..1606ba0fc2e2d --- /dev/null +++ b/components/suitedash/sources/new-company/new-company.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "suitedash-new-company", + name: "New Company Created", + description: "Emit new event when a new company is created in SuiteDash", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFn() { + return this.suitedash.listCompanies; + }, + getSummary(company) { + if (!company || typeof company !== "object") { + return "New Company: Unknown"; + } + const name = company.name || "Unnamed"; + return `New Company: ${name}`; + }, + }, +}; diff --git a/components/suitedash/sources/new-contact/new-contact.mjs b/components/suitedash/sources/new-contact/new-contact.mjs new file mode 100644 index 0000000000000..d8d13259f9144 --- /dev/null +++ b/components/suitedash/sources/new-contact/new-contact.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "suitedash-new-contact", + name: "New Contact Created", + description: "Emit new event when a new contact is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFn() { + return this.suitedash.listContacts; + }, + getSummary(contact) { + const firstName = contact.first_name || ""; + const lastName = contact.last_name || ""; + const fullName = [ + firstName, + lastName, + ].filter(Boolean).join(" ") || "Unknown"; + return `New Contact: ${fullName}`; + }, + }, +}; diff --git a/components/suitedash/suitedash.app.mjs b/components/suitedash/suitedash.app.mjs new file mode 100644 index 0000000000000..a0dc627f72308 --- /dev/null +++ b/components/suitedash/suitedash.app.mjs @@ -0,0 +1,119 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "suitedash", + propDefinitions: { + companyId: { + type: "string", + label: "Company ID", + description: "The ID of the company", + async options({ page }) { + const { data } = await this.listCompanies({ + params: { + page: page + 1, + }, + }); + return data?.map(({ + uid: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + companyName: { + type: "string", + label: "Company Name", + description: "The name of the new company.", + }, + role: { + type: "string", + label: "Company Role", + description: "The role of the new company.", + options: [ + "Lead", + "Client", + "Prospect", + ], + }, + }, + methods: { + _baseUrl() { + return "https://app.suitedash.com/secure-api"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "Accept": "application/json", + "X-Public-ID": this.$auth.public_id, + "X-Secret-Key": this.$auth.secret_key, + }, + }); + }, + listCompanies(opts = {}) { + return this._makeRequest({ + path: "/companies", + ...opts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts", + ...opts, + }); + }, + createCompany(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/company", + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contact", + ...opts, + }); + }, + updateCompany({ + companyId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/company/${companyId}`, + ...opts, + }); + }, + async *paginate({ + fn, + params, + }) { + params = { + ...params, + page: 1, + }; + let hasMore; + do { + const { + data, meta: { pagination }, + } = await fn({ + params, + }); + for (const item of data) { + yield item; + } + hasMore = params.page < pagination.totalPages; + params.page++; + } while (hasMore); + }, + }, +}; diff --git a/components/summit/actions/list-models/list-models.mjs b/components/summit/actions/list-models/list-models.mjs new file mode 100644 index 0000000000000..5c12f8f4a1e7f --- /dev/null +++ b/components/summit/actions/list-models/list-models.mjs @@ -0,0 +1,29 @@ +import summit from "../../summit.app.mjs"; + +export default { + key: "summit-list-models", + name: "List Models", + description: "Returns a list of models from Summit. [See the documentation](https://summit.readme.io/reference/apps)", + version: "0.0.1", + type: "action", + props: { + summit, + }, + async run({ $ }) { + const results = this.summit.paginate({ + resourceFn: this.summit.listModels, + resourceType: "apps", + args: { + $, + }, + }); + const models = []; + for await (const model of results) { + models.push(model); + } + $.export("$summary", `Successfully retrieved ${models.length} model${models.length === 1 + ? "" + : "s"}`); + return models; + }, +}; diff --git a/components/summit/actions/run-model/run-model.mjs b/components/summit/actions/run-model/run-model.mjs new file mode 100644 index 0000000000000..764cd3a0ecc15 --- /dev/null +++ b/components/summit/actions/run-model/run-model.mjs @@ -0,0 +1,69 @@ +import summit from "../../summit.app.mjs"; + +export default { + key: "summit-run-model", + name: "Run Model", + description: "Executes a model within Summit and captures the response fields. [See the documentation](https://summit.readme.io/reference/model-api)", + version: "0.0.1", + type: "action", + props: { + summit, + model: { + propDefinition: [ + summit, + "model", + ], + reloadProps: true, + }, + experimentalFlatten: { + type: "integer", + label: "Experimental Flatten", + description: "This allows you to choose the index of the column you'd like to pull from the grid. -1 is the last column, which is useful for grabbing the last slice of a time series model, like a forecast.", + optional: true, + }, + }, + async additionalProps() { + const props = {}; + if (!this.model) { + return props; + } + const { parameters } = await this.summit.getModel({ + model: this.model, + }); + for (const parameter of parameters) { + props[parameter.name] = { + type: "string", + label: `${parameter.name}`, + default: `${parameter.default_value}`, + }; + if (parameter.value_choices) { + props[parameter.name].options = parameter.value_choices; + } + } + return props; + }, + async run({ $ }) { + const { parameters } = await this.summit.getModel({ + model: this.model, + }); + const parametersObj = {}; + for (const parameter of parameters) { + parametersObj[parameter.name] = this[parameter.name]; + } + const data = { + parameters: parametersObj, + }; + if (this.experimentalFlatten !== undefined) { + data.output = { + "__experimental_flatten": this.experimentalFlatten, + }; + } + const response = await this.summit.runModel({ + $, + model: this.model, + data, + }); + $.export("$summary", `Successfully executed model \`${(this.model).split("/").pop()}\``); + return response; + }, +}; diff --git a/components/summit/package.json b/components/summit/package.json new file mode 100644 index 0000000000000..208fd2a8486eb --- /dev/null +++ b/components/summit/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/summit", + "version": "0.1.0", + "description": "Pipedream Summit Components", + "main": "summit.app.mjs", + "keywords": [ + "pipedream", + "summit" + ], + "homepage": "https://pipedream.com/apps/summit", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} diff --git a/components/summit/sources/new-model-added/new-model-added.mjs b/components/summit/sources/new-model-added/new-model-added.mjs new file mode 100644 index 0000000000000..e9ffc4954663b --- /dev/null +++ b/components/summit/sources/new-model-added/new-model-added.mjs @@ -0,0 +1,48 @@ +import summit from "../../summit.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + key: "summit-new-model-added", + name: "New Model Added", + description: "Emit new event when a new model is added to your organization in Summit. [See the documentation](https://summit.readme.io/reference/apps)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + summit, + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + generateMeta(model) { + return { + id: model.id, + summary: `New Modle ${model.name}`, + ts: Date.now(), + }; + }, + async processEvent(max) { + const results = this.summit.paginate({ + resourceFn: this.summit.listModels, + resourceType: "apps", + max, + }); + for await (const model of results) { + const meta = this.generateMeta(model); + this.$emit(model, meta); + } + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/summit/summit.app.mjs b/components/summit/summit.app.mjs new file mode 100644 index 0000000000000..cebfd46569954 --- /dev/null +++ b/components/summit/summit.app.mjs @@ -0,0 +1,98 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "summit", + propDefinitions: { + model: { + type: "string", + label: "Model", + description: "The identifier model to run in the format `:organization_slug/:external_id/:app_slug`", + async options({ prevContext }) { + const args = prevContext?.next + ? { + url: prevContext.next, + } + : {}; + const { + apps, next, + } = await this.listModels(args); + return { + options: apps?.map(({ + app_identifier: value, name: label, + }) => ({ + value, + label, + })) || [], + context: { + next, + }, + }; + }, + }, + + }, + methods: { + _baseUrl() { + return "https://api.usesummit.com/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "x-api-key": `${this.$auth.api_key}`, + }, + ...otherOpts, + }); + }, + listModels(opts = {}) { + return this._makeRequest({ + path: "/apps", + ...opts, + }); + }, + getModel({ + model, ...opts + }) { + return this._makeRequest({ + path: `/${model}/`, + ...opts, + }); + }, + runModel({ + model, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/${model}/`, + ...opts, + }); + }, + async *paginate({ + resourceFn, + resourceType, + args = {}, + max, + }) { + let next, count = 0; + do { + const results = await resourceFn(args); + const items = results[resourceType]; + for (const item of items) { + yield item; + count++; + if (max && count >= max) { + return; + } + } + next = results?.next; + args.url = next; + } while (next); + }, + }, +}; diff --git a/components/sumo_logic/README.md b/components/sumo_logic/README.md index 65935ab842f3f..b0153bf8576f4 100644 --- a/components/sumo_logic/README.md +++ b/components/sumo_logic/README.md @@ -1,18 +1,11 @@ # Overview -The Sumo Logic API is a powerful and easy to use tool that can be used to -enhance your data experience. You can use it to build custom solutions for your -data needs or integrate it into existing solutions for greater flexibility and -control. +The Sumo Logic API facilitates the automation of your log management and analytics operations, enabling you to programmatically access Sumo Logic's data ingestion and query capabilities. With this API, you can streamline log data collection, conduct analysis, manage users, and set up alerts, all of which can be harnessed to enhance monitoring, security, and compliance procedures within your organization. Leveraging these functions within Pipedream workflows can help in creating dynamic, cross-functional integrations to optimize data-driven decision-making processes. -Some of the most common things you can build using the Sumo Logic API include: +# Example Use Cases -- Collect and manage log data from multiple sources -- Automate analyses and transformations of raw data -- Create custom dashboards and visualizations -- Automatically investigate data anomalies -- Manage data pipelines and data flows -- Integrate with other services and applications -- Generate ad-hoc reports and alerts -- Build custom search queries to locate specific data elements -- Distribute datasets to other systems and services +- **Automated Security Alerts**: Craft a workflow that connects Sumo Logic with Slack using Pipedream. Whenever Sumo Logic detects a potential security threat or anomaly in your logs, it triggers a Pipedream workflow that parses the event and automatically sends a formatted alert message to a designated Slack channel, keeping your security team instantly informed. + +- **Log-Triggered Incident Creation**: Integrate Sumo Logic with Jira through Pipedream. This workflow monitors your logs for specific error patterns or thresholds being exceeded and, upon detection, triggers the creation of an incident ticket in Jira. This means your support or dev team doesn't miss critical issues and can address them promptly based on the log data insights. + +- **Real-Time Dashboard Updates**: Use Pipedream to connect Sumo Logic to Google Sheets. Set up a workflow that periodically runs Sumo Logic queries to retrieve log data analytics, then processes and updates a Google Sheets dashboard with the latest insights. This automates the reporting process, giving stakeholders continuous access to real-time data visualizations. diff --git a/components/sumo_logic/package.json b/components/sumo_logic/package.json new file mode 100644 index 0000000000000..6c5ec6fa97c04 --- /dev/null +++ b/components/sumo_logic/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/sumo_logic", + "version": "0.6.0", + "description": "Pipedream sumo_logic Components", + "main": "sumo_logic.app.mjs", + "keywords": [ + "pipedream", + "sumo_logic" + ], + "homepage": "https://pipedream.com/apps/sumo_logic", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/sumup/README.md b/components/sumup/README.md new file mode 100644 index 0000000000000..ab9e9b9707b39 --- /dev/null +++ b/components/sumup/README.md @@ -0,0 +1,11 @@ +# Overview + +The SumUp API allows you to integrate your payment processing operations with SumUp's payment platform, creating seamless financial transactions for your business. With Pipedream, you can automate payment flows, sync transaction data with other tools, and build custom notifications based on payment events. You can use Pipedream’s serverless platform to trigger workflows on various SumUp events, process the data, and connect with countless other apps to extend functionality. + +# Example Use Cases + +- **Automated Receipt Generation**: When a payment is processed through SumUp, trigger a Pipedream workflow to generate a digital receipt using a PDF generation service like PDF.co, and then email it to the customer using a service like SendGrid. + +- **Sales Dashboard Update**: After every SumUp transaction, use a Pipedream workflow to update a real-time sales dashboard hosted on a service like Google Sheets. This could involve parsing transaction details and updating a row with the new sales data. + +- **Inventory Management on Shopify**: Post-sales, trigger a Pipedream workflow to deduct the sold items from inventory in your Shopify store. This keeps your stock levels accurate without manual updates, streamlining inventory management. diff --git a/components/sumup/actions/get-merchant-profile/get-merchant-profile.mjs b/components/sumup/actions/get-merchant-profile/get-merchant-profile.mjs new file mode 100644 index 0000000000000..407e1bee1dc50 --- /dev/null +++ b/components/sumup/actions/get-merchant-profile/get-merchant-profile.mjs @@ -0,0 +1,19 @@ +import sumup from "../../sumup.app.mjs"; + +export default { + key: "sumup-get-merchant-profile", + name: "Get Merchant Profile", + description: "Retrieves merchant profile data. [See the documenation](https://developer.sumup.com/api/merchant/get-merchant-profile)", + version: "0.0.1", + type: "action", + props: { + sumup, + }, + async run({ $ }) { + const response = await this.sumup.getMerchantProfile({ + $, + }); + $.export("$summary", "Successfully retrieved merchant account data"); + return response; + }, +}; diff --git a/components/sumup/actions/list-transactions/list-transactions.mjs b/components/sumup/actions/list-transactions/list-transactions.mjs new file mode 100644 index 0000000000000..e518b3cebe0ff --- /dev/null +++ b/components/sumup/actions/list-transactions/list-transactions.mjs @@ -0,0 +1,66 @@ +import sumup from "../../sumup.app.mjs"; + +export default { + key: "sumup-list-transactions", + name: "List Transactions", + description: "Lists detailed history of all transactions associated with the merchant profile. [See the documenation](https://developer.sumup.com/api/transactions/list-detailed)", + version: "0.0.1", + type: "action", + props: { + sumup, + transactionCode: { + type: "string", + label: "Transaction Code", + description: "Transaction code returned by the acquirer/processing entity after processing the transaction", + optional: true, + }, + statuses: { + propDefinition: [ + sumup, + "statuses", + ], + }, + paymentTypes: { + propDefinition: [ + sumup, + "paymentTypes", + ], + }, + startDate: { + type: "string", + label: "Start Date", + description: "Earliest transaction date in ISO Format. Example: `2020-02-29T10:56:56.876Z`", + optional: true, + }, + endDate: { + type: "string", + label: "End Date", + description: "Latest transaction date in ISO Format. Example: `2020-02-29T10:56:56.876Z`", + optional: true, + }, + limit: { + type: "integer", + label: "Limit", + description: "The maximum number of transactions to return", + default: 100, + optional: true, + }, + }, + async run({ $ }) { + const { items } = await this.sumup.listTransactions({ + $, + params: { + transaction_code: this.transactionCode, + statuses: this.statuses, + payment_types: this.paymentTypes, + oldest_time: this.startDate, + newest_time: this.endDate, + limit: this.limit, + }, + }); + $.export("$summary", `Successfully retrieved ${items.length} transactions${items.length === 1 + ? "" + : "s"}`); + return items; + }, +}; diff --git a/components/sumup/actions/retrieve-dba/retrieve-dba.mjs b/components/sumup/actions/retrieve-dba/retrieve-dba.mjs new file mode 100644 index 0000000000000..9b79c9126c5b9 --- /dev/null +++ b/components/sumup/actions/retrieve-dba/retrieve-dba.mjs @@ -0,0 +1,19 @@ +import sumup from "../../sumup.app.mjs"; + +export default { + key: "sumup-retrieve-dba", + name: "Retrieve DBA", + description: "Retrieves Doing Business As profile. [See the documenation](https://developer.sumup.com/api/merchant/get-doing-business-as)", + version: "0.0.1", + type: "action", + props: { + sumup, + }, + async run({ $ }) { + const response = await this.sumup.retrieveDba({ + $, + }); + $.export("$summary", "Successfully retrieved DBA data"); + return response; + }, +}; diff --git a/components/sumup/common/constants.mjs b/components/sumup/common/constants.mjs new file mode 100644 index 0000000000000..eb4da7962540f --- /dev/null +++ b/components/sumup/common/constants.mjs @@ -0,0 +1,17 @@ +const TRANSACTION_STATUSES = [ + "SUCCESSFUL", + "CANCELLED", + "FAILED", + "PENDING", +]; + +const PAYMENT_TYPES = [ + "ECOM", + "RECURRING", + "BOLETO", +]; + +export default { + TRANSACTION_STATUSES, + PAYMENT_TYPES, +}; diff --git a/components/sumup/package.json b/components/sumup/package.json new file mode 100644 index 0000000000000..528551f85cd21 --- /dev/null +++ b/components/sumup/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/sumup", + "version": "0.1.0", + "description": "Pipedream SumUp Components", + "main": "sumup.app.mjs", + "keywords": [ + "pipedream", + "sumup" + ], + "homepage": "https://pipedream.com/apps/sumup", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/sumup/sources/new-transaction/new-transaction.mjs b/components/sumup/sources/new-transaction/new-transaction.mjs new file mode 100644 index 0000000000000..b58dac8f19aa7 --- /dev/null +++ b/components/sumup/sources/new-transaction/new-transaction.mjs @@ -0,0 +1,73 @@ +import sumup from "../../sumup.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "sumup-new-transaction", + name: "New Transaction", + description: "Emit new event when a new transaction is posted in SumUp. [See the documenation](https://developer.sumup.com/api/transactions/list-detailed)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + sumup, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + statuses: { + propDefinition: [ + sumup, + "statuses", + ], + }, + paymentTypes: { + propDefinition: [ + sumup, + "paymentTypes", + ], + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || this.oneDayAgo(); + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + oneDayAgo() { + return new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString() + .slice(0, 19) + "Z"; + }, + generateMeta(transaction) { + return { + id: transaction.id, + summary: `New Transaction ${transaction.id}`, + ts: Date.parse(transaction.timestamp), + }; + }, + }, + async run() { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + const { items } = await this.sumup.listTransactions({ + params: { + statuses: this.statuses, + payment_types: this.paymentTypes, + oldest_time: lastTs, + }, + }); + for (const item of items) { + if (Date.parse(item.timestamp) > Date.parse(maxTs)) { + maxTs = item.timestamp; + } + const meta = this.generateMeta(item); + this.$emit(item, meta); + } + this._setLastTs(maxTs); + }, + sampleEmit, +}; diff --git a/components/sumup/sources/new-transaction/test-event.mjs b/components/sumup/sources/new-transaction/test-event.mjs new file mode 100644 index 0000000000000..d125f0c3647fe --- /dev/null +++ b/components/sumup/sources/new-transaction/test-event.mjs @@ -0,0 +1,18 @@ +export default { + "amount": 10.1, + "currency": "EUR", + "id": "6b425463-3e1b-431d-83fa-1e51c2925e99", + "installments_count": null, + "payment_type": null, + "status": null, + "timestamp": "2020-02-29T10:56:56.876Z", + "transaction_code": "TEENSK4W2K", + "payout_plan": null, + "payouts_received": null, + "payouts_total": null, + "product_summary": null, + "card_type": null, + "transaction_id": null, + "type": null, + "user": null +} \ No newline at end of file diff --git a/components/sumup/sumup.app.mjs b/components/sumup/sumup.app.mjs new file mode 100644 index 0000000000000..c6e1528a0b802 --- /dev/null +++ b/components/sumup/sumup.app.mjs @@ -0,0 +1,62 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "sumup", + propDefinitions: { + statuses: { + type: "string[]", + label: "Statuses", + description: "Filter by current status of the transaction", + options: constants.TRANSACTION_STATUSES, + optional: true, + }, + paymentTypes: { + type: "string[]", + label: "Payment Types", + description: "Filter by payment type used for the transaction", + options: constants.PAYMENT_TYPES, + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.sumup.com/v0.1/me"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, + path, + ...args + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._headers(), + ...args, + }); + }, + getMerchantProfile(opts = {}) { + return this._makeRequest({ + path: "/merchant-profile", + ...opts, + }); + }, + retrieveDba(opts = {}) { + return this._makeRequest({ + path: "/merchant-profile/doing-business-as", + ...opts, + }); + }, + listTransactions(opts = {}) { + return this._makeRequest({ + path: "/transactions/history", + ...opts, + }); + }, + }, +}; diff --git a/components/supabase/README.md b/components/supabase/README.md index e015fa38dfcfb..793e6b9f18e0b 100644 --- a/components/supabase/README.md +++ b/components/supabase/README.md @@ -1,26 +1,8 @@ # Overview -Supabase is an open-source database, authentication system, and hosting -platform that allows you to quickly and easily build powerful web-based -applications. Supabase’s API provides developers with the ability to quickly -create, manage, and control data on their projects. - -When combined with other libraries, such as React or Redux, developers can -create fast, secure, and reliable websites. With Supabase’s API, developers can -connect, query, and manage data on their web projects via a simple and -intuitive interface. - -Here is a list of some of the things you can build using the Supabase API: - -- Professional websites with custom content management -- Node.js applications that read and write data to the database -- GraphQL APIs for connecting front-end applications to the backend -- Cloud-based Single-page applications -- Web-based applications with authentication and security features -- Mobile apps with real-time synchronization to the cloud database -- IoT applications with remote control capabilities -- Machine learning applications that use predictive analytics +Supabase is a real-time backend-as-a-service that provides developers with a suite of tools to quickly build and scale their applications. It offers database storage, authentication, instant APIs, and real-time subscriptions. With the Supabase API, you can perform CRUD operations on your database, manage users, and listen to database changes in real time. When integrated with Pipedream, you can automate workflows that react to these database events, synchronize data across multiple services, or streamline user management processes. +# Getting Started ## Webhooks Supabase provides support for Database Webhooks, and you can easily configure them through the Supabase website by following these simple steps: @@ -32,3 +14,11 @@ Supabase provides support for Database Webhooks, and you can easily configure th 5. In the `HTTP Request` section, select the `POST` method and paste the URL generated by the Pipedream source. That's it! You will now receive notifications in your trigger whenever an event occurs. + +# Example Use Cases + +- **Sync Supabase Data to Google Sheets**: Automatically update a Google Sheet with new data from a Supabase database. Whenever a new row is added to a specified Supabase table, a Pipedream workflow is triggered, appending the data to a Google Sheet. This can be useful for non-technical stakeholders needing real-time visibility into the database without direct access. + +- **User Signup Email Confirmation**: Send a welcome email to new users who sign up via your Supabase-powered app. Utilize Pipedream to listen for new signups in Supabase, and then trigger an email through an email service provider like SendGrid. This helps in engaging users from the moment they create an account. + +- **Aggregate Logs and Monitor Events**: Collect logs from your Supabase application and send them to a logging platform like Datadog. Monitor events in your Supabase database, such as updates or deletes, and forward these events from Pipedream to Datadog for real-time monitoring and analysis. This workflow is key for maintaining oversight and security within your application. \ No newline at end of file diff --git a/components/supabase/actions/batch-insert-rows/batch-insert-rows.mjs b/components/supabase/actions/batch-insert-rows/batch-insert-rows.mjs new file mode 100644 index 0000000000000..ce51b6bc8a795 --- /dev/null +++ b/components/supabase/actions/batch-insert-rows/batch-insert-rows.mjs @@ -0,0 +1,83 @@ +import supabase from "../../supabase.app.mjs"; +import fs from "fs"; +import { parse } from "csv-parse/sync"; + +export default { + key: "supabase-batch-insert-rows", + name: "Batch Insert Rows", + description: "Inserts new rows into a database. [See the documentation](https://supabase.com/docs/reference/javascript/insert)", + version: "0.0.1", + type: "action", + props: { + supabase, + table: { + propDefinition: [ + supabase, + "table", + ], + description: "Name of the table to insert rows into", + }, + source: { + type: "string", + label: "Source of data", + description: "Whether to enter the row data as an array of objects or to import from a CSV file", + options: [ + "Array", + "CSV File", + ], + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.source === "Array") { + props.data = { + type: "string[]", + label: "Row Data", + description: "An array of objects, each object representing a row. Enter column names and values as key/value pairs", + }; + } + if (this.source === "CSV File") { + props.filePath = { + type: "string", + label: "File Path", + description: "The path to a csv file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + }; + } + return props; + }, + methods: { + parseArray(arr) { + if (Array.isArray(arr)) { + return arr.map((item) => { + return typeof item === "string" + ? JSON.parse(item) + : item; + }); + } + if (typeof arr === "string") { + return JSON.parse(arr); + } + }, + getRowsFromCSV(filePath) { + const fileContent = fs.readFileSync(filePath.includes("tmp/") + ? filePath + : `/tmp/${filePath}`, "utf-8"); + const rows = parse(fileContent, { + columns: true, + skip_empty_lines: true, + }); + return rows; + }, + }, + async run({ $ }) { + const data = this.source === "CSV File" + ? this.getRowsFromCSV(this.filePath) + : this.parseArray(this.data); + + const response = await this.supabase.insertRow(this.table, data); + + $.export("$summary", `Successfully inserted rows into table ${this.table}`); + return response; + }, +}; diff --git a/components/supabase/actions/delete-row/delete-row.mjs b/components/supabase/actions/delete-row/delete-row.mjs index b85c47a429fb3..4ca2757f1155c 100644 --- a/components/supabase/actions/delete-row/delete-row.mjs +++ b/components/supabase/actions/delete-row/delete-row.mjs @@ -4,7 +4,7 @@ export default { key: "supabase-delete-row", name: "Delete Row", type: "action", - version: "0.0.3", + version: "0.1.2", description: "Deletes row(s) in a database. [See the docs here](https://supabase.com/docs/reference/javascript/delete)", props: { supabase, @@ -32,9 +32,7 @@ export default { }, async run({ $ }) { const response = await this.supabase.deleteRow(this.table, this.column, this.value); - if (response) { - $.export("$summary", `Successfully deleted ${response.length} row(s) from table ${this.table}`); - } + $.export("$summary", `Successfully deleted ${response.length} row(s) from table ${this.table}`); return response; }, }; diff --git a/components/supabase/actions/insert-row/insert-row.mjs b/components/supabase/actions/insert-row/insert-row.mjs index ed46388d61dcf..223bd8124382e 100644 --- a/components/supabase/actions/insert-row/insert-row.mjs +++ b/components/supabase/actions/insert-row/insert-row.mjs @@ -4,7 +4,7 @@ export default { key: "supabase-insert-row", name: "Insert Row", type: "action", - version: "0.0.3", + version: "0.1.2", description: "Inserts a new row into a database. [See the docs here](https://supabase.com/docs/reference/javascript/insert)", props: { supabase, @@ -24,9 +24,7 @@ export default { }, async run({ $ }) { const response = await this.supabase.insertRow(this.table, this.data); - if (response) { - $.export("$summary", `Successfully inserted row into table ${this.table}`); - } + $.export("$summary", `Successfully inserted row into table ${this.table}`); return response; }, }; diff --git a/components/supabase/actions/remote-procedure-call/remote-procedure-call.mjs b/components/supabase/actions/remote-procedure-call/remote-procedure-call.mjs index 330cade17b83d..eba974b7bf4a9 100644 --- a/components/supabase/actions/remote-procedure-call/remote-procedure-call.mjs +++ b/components/supabase/actions/remote-procedure-call/remote-procedure-call.mjs @@ -4,7 +4,7 @@ export default { key: "supabase-remote-procedure-call", name: "Remote Procedure Call", type: "action", - version: "0.0.3", + version: "0.1.2", description: "Call a Postgres function in a database. [See the docs here](https://supabase.com/docs/reference/javascript/rpc)", props: { supabase, @@ -22,9 +22,7 @@ export default { }, async run({ $ }) { const response = await this.supabase.remoteProcedureCall(this.functionName, this.args); - if (response) { - $.export("$summary", `Successfully executed remote procedure call ${this.functionName}`); - } + $.export("$summary", `Successfully executed remote procedure call ${this.functionName}`); return response; }, }; diff --git a/components/supabase/actions/select-row/select-row.mjs b/components/supabase/actions/select-row/select-row.mjs index 6a040383efc1b..11b47389ee3e6 100644 --- a/components/supabase/actions/select-row/select-row.mjs +++ b/components/supabase/actions/select-row/select-row.mjs @@ -5,7 +5,7 @@ export default { key: "supabase-select-row", name: "Select Row", type: "action", - version: "0.0.6", + version: "0.1.2", description: "Selects row(s) in a database. [See the docs here](https://supabase.com/docs/reference/javascript/select)", props: { supabase, @@ -81,9 +81,7 @@ export default { sortOrder, max, }); - if (response) { - $.export("$summary", `Successfully retrieved ${response.length} rows from table ${table}`); - } + $.export("$summary", `Successfully retrieved ${response.length} rows from table ${table}`); return response; }, }; diff --git a/components/supabase/actions/update-row/update-row.mjs b/components/supabase/actions/update-row/update-row.mjs index 6a2245b91dd57..ff48a6852c1d1 100644 --- a/components/supabase/actions/update-row/update-row.mjs +++ b/components/supabase/actions/update-row/update-row.mjs @@ -4,7 +4,7 @@ export default { key: "supabase-update-row", name: "Update Row", type: "action", - version: "0.0.3", + version: "0.1.2", description: "Updates row(s) in a database. [See the docs here](https://supabase.com/docs/reference/javascript/update)", props: { supabase, @@ -39,9 +39,7 @@ export default { }, async run({ $ }) { const response = await this.supabase.updateRow(this.table, this.column, this.value, this.data); - if (response) { - $.export("$summary", `Successfully updated ${response.length} row(s) from table ${this.table}`); - } + $.export("$summary", `Successfully updated ${response.length} row(s) from table ${this.table}`); return response; }, }; diff --git a/components/supabase/actions/upsert-row/upsert-row.mjs b/components/supabase/actions/upsert-row/upsert-row.mjs index 3e727375ca43a..a6a5cbdc8cba3 100644 --- a/components/supabase/actions/upsert-row/upsert-row.mjs +++ b/components/supabase/actions/upsert-row/upsert-row.mjs @@ -4,7 +4,7 @@ export default { key: "supabase-upsert-row", name: "Upsert Row", type: "action", - version: "0.0.3", + version: "0.1.2", description: "Updates a row in a database or inserts new row if not found. [See the docs here](https://supabase.com/docs/reference/javascript/upsert)", props: { supabase, @@ -24,9 +24,7 @@ export default { }, async run({ $ }) { const response = await this.supabase.upsertRow(this.table, this.data); - if (response) { - $.export("$summary", `Successfully upserted row into table ${this.table}`); - } + $.export("$summary", `Successfully upserted row into table ${this.table}`); return response; }, }; diff --git a/components/supabase/package.json b/components/supabase/package.json index 70fc72b04b3b4..9b476a137385e 100644 --- a/components/supabase/package.json +++ b/components/supabase/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/supabase", - "version": "0.1.3", + "version": "0.3.0", "description": "Pipedream Supabase Components", "main": "supabase.app.mjs", "keywords": [ @@ -13,7 +13,8 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.2.1", - "@supabase/supabase-js": "^2.39.0" + "@pipedream/platform": "^3.0.3", + "@supabase/supabase-js": "^2.45.6", + "csv-parse": "^5.5.6" } } diff --git a/components/supabase/sources/new-row-added/new-row-added.mjs b/components/supabase/sources/new-row-added/new-row-added.mjs index e02833344d383..7eb470aa3bf85 100644 --- a/components/supabase/sources/new-row-added/new-row-added.mjs +++ b/components/supabase/sources/new-row-added/new-row-added.mjs @@ -9,7 +9,7 @@ export default { key: "supabase-new-row-added", name: "New Row Added", description: "Emit new event for every new row added in a table. [See documentation here](https://supabase.com/docs/reference/javascript/select)", - version: "0.0.1", + version: "0.0.4", type: "source", props: { ...base.props, diff --git a/components/supabase/sources/new-webhook-event/new-webhook-event.mjs b/components/supabase/sources/new-webhook-event/new-webhook-event.mjs index 996d2a4f45792..07d0331dcf7ec 100644 --- a/components/supabase/sources/new-webhook-event/new-webhook-event.mjs +++ b/components/supabase/sources/new-webhook-event/new-webhook-event.mjs @@ -5,7 +5,7 @@ export default { key: "supabase-new-webhook-event", name: "New Webhook Event (Instant)", description: "Emit new event for every `insert`, `update`, or `delete` operation in a table. This source requires user configuration using the Supabase website. More information in the README. [Also see documentation here](https://supabase.com/docs/guides/database/webhooks#creating-a-webhook)", - version: "0.0.2", + version: "0.0.5", type: "source", props: { ...base.props, diff --git a/components/supabase/supabase.app.mjs b/components/supabase/supabase.app.mjs index 3e469dc43ea09..30fd3156dafdd 100644 --- a/components/supabase/supabase.app.mjs +++ b/components/supabase/supabase.app.mjs @@ -1,4 +1,4 @@ -import { createClient } from "@supabase/supabase-js@2.39.0"; +import { createClient } from "@supabase/supabase-js"; import constants from "./common/constants.mjs"; export default { @@ -44,6 +44,11 @@ export default { }, }, methods: { + verifyForErrors(resp) { + if (resp.error) { + throw new Error(JSON.stringify(resp, null, 2)); + } + }, async _client() { return createClient(`https://${this.$auth.subdomain}.supabase.co`, this.$auth.service_key); }, @@ -64,10 +69,8 @@ export default { filterMethod(query, column, value); } const resp = await query; - if (resp.error) { - throw new Error(JSON.stringify(resp.error, null, 2)); - } - return resp.data; + this.verifyForErrors(resp); + return resp; }, baseFilter(client, table, orderBy, ascending, max) { return client @@ -112,42 +115,47 @@ export default { }, async insertRow(table, rowData = {}) { const client = await this._client(); - const { data } = await client + const resp = await client .from(table) .insert(rowData) .select(); - return data; + this.verifyForErrors(resp); + return resp; }, async updateRow(table, column, value, rowData = {}) { const client = await this._client(); - const { data } = await client + const resp = await client .from(table) .update(rowData) .eq(column, value) .select(); - return data; + this.verifyForErrors(resp); + return resp; }, async upsertRow(table, rowData = {}) { const client = await this._client(); - const { data } = await client + const resp = await client .from(table) .upsert(rowData) .select(); - return data; + this.verifyForErrors(resp); + return resp; }, async deleteRow(table, column, value) { const client = await this._client(); - const { data } = await client + const resp = await client .from(table) .delete() .eq(column, value) .select(); - return data; + this.verifyForErrors(resp); + return resp; }, async remoteProcedureCall(functionName, args = {}) { const client = await this._client(); - const { data } = await client.rpc(functionName, args); - return data; + const resp = await client.rpc(functionName, args); + this.verifyForErrors(resp); + return resp; }, }, }; diff --git a/components/supabase_management_api/actions/create-project/create-project.mjs b/components/supabase_management_api/actions/create-project/create-project.mjs new file mode 100644 index 0000000000000..a742e7b165fc4 --- /dev/null +++ b/components/supabase_management_api/actions/create-project/create-project.mjs @@ -0,0 +1,61 @@ +import supabaseManagementApi from "../../supabase_management_api.app.mjs"; + +export default { + key: "supabase_management_api-create-project", + name: "Create Project", + description: "Creates a new Supabase project within a specified organization. [See the documentation](https://supabase.com/docs/reference/api/v1-create-a-project)", + version: "0.0.1", + type: "action", + props: { + supabaseManagementApi, + organizationId: { + propDefinition: [ + supabaseManagementApi, + "organizationId", + ], + }, + projectName: { + type: "string", + label: "Project Name", + description: "The name of the project", + }, + dbPassword: { + type: "string", + label: "Database Password", + description: "The password for the database", + }, + region: { + propDefinition: [ + supabaseManagementApi, + "region", + ], + }, + instanceSize: { + propDefinition: [ + supabaseManagementApi, + "instanceSize", + ], + }, + templateUrl: { + type: "string", + label: "Template URL", + description: "The template URL used to create the project from the CLI", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.supabaseManagementApi.createProject({ + $, + data: { + organization_id: this.organizationId, + name: this.projectName, + db_pass: this.dbPassword, + region: this.region, + desired_instance_size: this.instanceSize, + template_url: this.templateUrl, + }, + }); + $.export("$summary", `Successfully created project: ${this.projectName}`); + return response; + }, +}; diff --git a/components/supabase_management_api/actions/generate-typescript-types/generate-typescript-types.mjs b/components/supabase_management_api/actions/generate-typescript-types/generate-typescript-types.mjs new file mode 100644 index 0000000000000..dda311f832ab4 --- /dev/null +++ b/components/supabase_management_api/actions/generate-typescript-types/generate-typescript-types.mjs @@ -0,0 +1,35 @@ +import supabaseManagementApi from "../../supabase_management_api.app.mjs"; + +export default { + key: "supabase_management_api-generate-typescript-types", + name: "Generate TypeScript Types", + description: "Generates TypeScript types based on the current database schema for a specified Supabase project. [See the documentation](https://supabase.com/docs/reference/api/v1-generate-typescript-types)", + version: "0.0.1", + type: "action", + props: { + supabaseManagementApi, + projectRef: { + propDefinition: [ + supabaseManagementApi, + "projectRef", + ], + }, + includedSchemas: { + type: "boolean", + label: "Included Schemas", + description: "Schemas to be included in the TypeScript types generation", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.supabaseManagementApi.generateTypescriptTypes({ + $, + projectRef: this.projectRef, + params: { + include_schemas: this.includeSchemas, + }, + }); + $.export("$summary", `Successfully generated TypeScript types for project ${this.projectRef}`); + return response; + }, +}; diff --git a/components/supabase_management_api/common/constants.mjs b/components/supabase_management_api/common/constants.mjs new file mode 100644 index 0000000000000..c154a86f8573b --- /dev/null +++ b/components/supabase_management_api/common/constants.mjs @@ -0,0 +1,35 @@ +const REGIONS = [ + "us-east-1", + "us-west-1", + "us-west-2", + "ap-east-1", + "ap-southeast-1", + "ap-northeast-1", + "ap-northeast-2", + "ap-southeast-2", + "eu-west-1", + "eu-west-2", + "eu-west-3", + "eu-central-1", + "ca-central-1", + "ap-south-1", + "sa-east-1", +]; + +const INSTANCE_SIZES = [ + "micro", + "small", + "medium", + "large", + "xlarge", + "2xlarge", + "4xlarge", + "8xlarge", + "12xlarge", + "16xlarge", +]; + +export default { + REGIONS, + INSTANCE_SIZES, +}; diff --git a/components/supabase_management_api/package.json b/components/supabase_management_api/package.json new file mode 100644 index 0000000000000..d13ee23c6a8f2 --- /dev/null +++ b/components/supabase_management_api/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/supabase_management_api", + "version": "0.1.0", + "description": "Pipedream Supabase Management API Components", + "main": "supabase_management_api.app.mjs", + "keywords": [ + "pipedream", + "supabase_management_api" + ], + "homepage": "https://pipedream.com/apps/supabase_management_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/supabase_management_api/sources/new-backup-completed/new-backup-completed.mjs b/components/supabase_management_api/sources/new-backup-completed/new-backup-completed.mjs new file mode 100644 index 0000000000000..d25c327d55590 --- /dev/null +++ b/components/supabase_management_api/sources/new-backup-completed/new-backup-completed.mjs @@ -0,0 +1,51 @@ +import supabaseManagementApi from "../../supabase_management_api.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + key: "supabase_management_api-new-backup-completed", + name: "New Backup Completed", + description: "Emit new event when a database backup is completed for a project. [See the documentation](https://supabase.com/docs/reference/api/v1-list-all-backups)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + supabaseManagementApi, + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + projectRef: { + propDefinition: [ + supabaseManagementApi, + "projectRef", + ], + }, + }, + methods: { + generateMeta(backup) { + return { + id: backup.inserted_at, + summary: "New Backup Completed", + ts: Date.now(), + }; + }, + }, + async run() { + const backups = await this.supabaseManagementApi.listDatabaseBackups({ + projectRef: this.projectRef, + }); + + if (!backups?.length) { + return; + } + + for (const backup of backups) { + if (backup.status === "COMPLETED") { + const meta = this.generateMeta(backup); + this.$emit(backup, meta); + } + } + }, +}; diff --git a/components/supabase_management_api/supabase_management_api.app.mjs b/components/supabase_management_api/supabase_management_api.app.mjs new file mode 100644 index 0000000000000..c782b00a37c67 --- /dev/null +++ b/components/supabase_management_api/supabase_management_api.app.mjs @@ -0,0 +1,104 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "supabase_management_api", + propDefinitions: { + projectRef: { + type: "string", + label: "Project Reference", + description: "The reference to the Supabase project", + async options() { + const projects = await this.listProjects(); + return projects?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + organizationId: { + type: "string", + label: "Organization ID", + description: "The ID of the organization", + async options() { + const organizations = await this.listOrganizations(); + return organizations?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + region: { + type: "string", + label: "Region", + description: "The region for the Supabase project", + options: constants.REGIONS, + }, + instanceSize: { + type: "string", + label: "Instance Size", + description: "The desired instance size for the Supabase project", + options: constants.INSTANCE_SIZES, + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.supabase.com/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + listProjects(opts = {}) { + return this._makeRequest({ + path: "/projects", + ...opts, + }); + }, + listOrganizations(opts = {}) { + return this._makeRequest({ + path: "/organizations", + ...opts, + }); + }, + listDatabaseBackups({ + projectRef, ...opts + }) { + return this._makeRequest({ + path: `/projects/${projectRef}/database/backups`, + ...opts, + }); + }, + generateTypescriptTypes({ + projectRef, ...opts + }) { + return this._makeRequest({ + path: `/projects/${projectRef}/types/typescript`, + ...opts, + }); + }, + createProject(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/projects", + ...opts, + }); + }, + }, +}; diff --git a/components/supercast/README.md b/components/supercast/README.md new file mode 100644 index 0000000000000..aac0c5cb9ab6b --- /dev/null +++ b/components/supercast/README.md @@ -0,0 +1,11 @@ +# Overview + +The Supercast API offers the ability to automate interactions with your Supercast account, enabling you to manage subscribers, analyze subscription data, and integrate with other services. With the Supercast API on Pipedream, you can create serverless workflows that trigger from various events, helping to streamline operations, personalize subscriber engagement, and extend the functionality of your podcast subscription service. + +# Example Use Cases + +- **Subscriber Sync to CRM**: Automatically sync new Supercast subscribers to a Customer Relationship Management (CRM) system like HubSpot. When a new subscriber is added in Supercast, Pipedream captures the event and adds or updates the subscriber's details in the CRM, ensuring sales and customer service teams have the latest information. + +- **Subscription Status Webhook**: Use Pipedream to set up a webhook that listens for subscription updates from Supercast, such as cancellations or renewals. Upon receiving a notification, Pipedream can trigger workflows that update customer segments in email marketing platforms like Mailchimp, or push notifications to Slack to alert your team. + +- **Analytics Dashboard Update**: Integrate Supercast with data visualization tools like Google Sheets or Tableau. Whenever there's new subscription activity, Pipedream can process the data and update your custom analytics dashboard, giving you real-time insights into your podcast's performance and subscriber trends. diff --git a/components/supercast/actions/create-creator/create-creator.mjs b/components/supercast/actions/create-creator/create-creator.mjs new file mode 100644 index 0000000000000..72b1f484b5624 --- /dev/null +++ b/components/supercast/actions/create-creator/create-creator.mjs @@ -0,0 +1,65 @@ +import app from "../../supercast.app.mjs"; + +export default { + key: "supercast-create-creator", + name: "Create a Channel Creator", + description: "Creates a new channel creator on Supercast. [See the documentation](https://supercast.readme.io/reference/postcreators)", + version: "0.0.1", + type: "action", + props: { + app, + email: { + type: "string", + label: "Email", + description: "Email of the creator", + }, + firstName: { + type: "string", + label: "First Name", + description: "First name of the creator", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the creator", + optional: true, + }, + password: { + type: "string", + label: "Password", + description: "Password for the creator account. On creation, if left unset Creator must be managed entirely through API.", + optional: true, + }, + }, + methods: { + createChannelCreator(args = {}) { + return this.app.post({ + path: "/creators", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createChannelCreator, + email, + firstName, + lastName, + password, + } = this; + + const response = await createChannelCreator({ + $, + data: { + email, + first_name: firstName, + last_name: lastName, + password, + }, + }); + + $.export("$summary", `Successfully created channel creator with ID ${response.id}`); + return response; + }, +}; diff --git a/components/supercast/actions/create-episode/create-episode.mjs b/components/supercast/actions/create-episode/create-episode.mjs new file mode 100644 index 0000000000000..25ad8563ec812 --- /dev/null +++ b/components/supercast/actions/create-episode/create-episode.mjs @@ -0,0 +1,79 @@ +import app from "../../supercast.app.mjs"; + +export default { + key: "supercast-create-episode", + name: "Create an Episode", + description: "Creates a new episode on Supercast. [See the documentation](https://supercast.readme.io/reference/postepisodes)", + version: "0.0.1", + type: "action", + props: { + app, + title: { + type: "string", + label: "Title", + description: "The title of the episode or question", + }, + summary: { + type: "string", + label: "Summary", + description: "Summary of the episode", + optional: true, + }, + explicit: { + type: "boolean", + label: "Explicit Content", + description: "Whether the episode contains explicit content.", + optional: true, + }, + language: { + type: "string", + label: "Language", + description: "The [ISO 639-1 language code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) for the episode", + optional: true, + }, + publishedAt: { + type: "string", + label: "Published At", + description: "The date and time when the episode should be published. Eg. `2020-01-01T00:00:00Z`", + optional: true, + }, + }, + methods: { + yesOrNo(value) { + return value === true + ? "yes" + : "no"; + }, + createEpisode(args = {}) { + return this.app.post({ + path: "/episodes", + ...args, + }); + }, + }, + async run({ $ }) { + const { + yesOrNo, + createEpisode, + title, + summary, + explicit, + language, + publishedAt, + } = this; + + const response = await createEpisode({ + $, + data: { + title, + summary, + explicit: yesOrNo(explicit), + language, + published_at: publishedAt, + }, + }); + + $.export("$summary", `Successfully created episode with ID \`${response.id}\``); + return response; + }, +}; diff --git a/components/supercast/common/constants.mjs b/components/supercast/common/constants.mjs new file mode 100644 index 0000000000000..8c8607560132f --- /dev/null +++ b/components/supercast/common/constants.mjs @@ -0,0 +1,9 @@ +const BASE_URL = "https://supercast.tech"; +const VERSION_PATH = "/api/v1"; +const DEFAULT_MAX = 600; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_MAX, +}; diff --git a/components/supercast/common/utils.mjs b/components/supercast/common/utils.mjs new file mode 100644 index 0000000000000..903b2593ed3c2 --- /dev/null +++ b/components/supercast/common/utils.mjs @@ -0,0 +1,11 @@ +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +export default { + iterate, +}; diff --git a/components/supercast/package.json b/components/supercast/package.json new file mode 100644 index 0000000000000..6165c4300750a --- /dev/null +++ b/components/supercast/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/supercast", + "version": "0.1.0", + "description": "Pipedream Supercast Components", + "main": "supercast.app.mjs", + "keywords": [ + "pipedream", + "supercast" + ], + "homepage": "https://pipedream.com/apps/supercast", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/supercast/sources/common/polling.mjs b/components/supercast/sources/common/polling.mjs new file mode 100644 index 0000000000000..58edd1b4d5fa3 --- /dev/null +++ b/components/supercast/sources/common/polling.mjs @@ -0,0 +1,53 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../supercast.app.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + processResources(resources) { + Array.from(resources) + .reverse() + .forEach(this.processResource); + }, + }, + async run() { + const { + app, + getResourcesFn, + processResources, + } = this; + + const resources = await app.paginate({ + resourcesFn: getResourcesFn(), + resourcesFnArgs: { + debug: true, + }, + }); + + processResources(resources); + }, +}; diff --git a/components/supercast/sources/new-episode-created/new-episode-created.mjs b/components/supercast/sources/new-episode-created/new-episode-created.mjs new file mode 100644 index 0000000000000..330c3411f7b49 --- /dev/null +++ b/components/supercast/sources/new-episode-created/new-episode-created.mjs @@ -0,0 +1,24 @@ +import common from "../common/polling.mjs"; + +export default { + ...common, + key: "supercast-new-episode-created", + name: "New Episode Created", + description: "Emit new event when a new episode is created in Supercast. [See the documentation](https://supercast.readme.io/reference/postepisodes)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourcesFn() { + return this.app.listEpisodes; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Episode: ${resource.title}`, + ts: Date.parse(resource.created_at), + }; + }, + }, +}; diff --git a/components/supercast/sources/new-question-created/new-question-created.mjs b/components/supercast/sources/new-question-created/new-question-created.mjs new file mode 100644 index 0000000000000..21155d99ab600 --- /dev/null +++ b/components/supercast/sources/new-question-created/new-question-created.mjs @@ -0,0 +1,24 @@ +import common from "../common/polling.mjs"; + +export default { + ...common, + key: "supercast-new-question-created", + name: "New Question Created", + description: "Emit new event when a new question is created. [See the documentation](https://supercast.readme.io/reference/getquestions)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourcesFn() { + return this.app.listQuestions; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Question: ${resource.title}`, + ts: Date.parse(resource.created_at), + }; + }, + }, +}; diff --git a/components/supercast/supercast.app.mjs b/components/supercast/supercast.app.mjs new file mode 100644 index 0000000000000..3abeac4a447b2 --- /dev/null +++ b/components/supercast/supercast.app.mjs @@ -0,0 +1,87 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; + +export default { + type: "app", + app: "supercast", + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Accept": "application/json", + "Content-Type": "application/json", + "Authorization": `Bearer ${this.$auth.access_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + const config = { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }; + return axios($, config); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + listEpisodes(args = {}) { + return this._makeRequest({ + path: "/episodes", + ...args, + }); + }, + listQuestions(args = {}) { + return this._makeRequest({ + path: "/questions", + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, + max = constants.DEFAULT_MAX, + }) { + let page = 1; + let resourcesCount = 0; + + while (true) { + const nextResources = + await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + page, + }, + }); + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + yield resource; + resourcesCount += 1; + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + page += 1; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); + }, + }, +}; diff --git a/components/superdocu/README.md b/components/superdocu/README.md index 233e8abae04d9..97bcdc1ce234f 100644 --- a/components/superdocu/README.md +++ b/components/superdocu/README.md @@ -1,21 +1,11 @@ # Overview -Superdocu is an innovative document and workflow management API designed to -make creating, editing, and maintaining digital documents easier than ever. -With its comprehensive set of tools, developers can create customized -applications for a wide range of document-related needs. Whether it's -automating document generation for an e-commerce platform or streamlining -document collaboration for an enterprise company, Superdocu's robust API makes -the task easy. +The Superdocu API empowers users to automate document collection and management processes, streamlining the way businesses handle paperwork for clients and employees. It facilitates the creation of document requests, tracking of submissions, and management of user data, all programmatically. Leveraging the Superdocu API on Pipedream allows users to build powerful, serverless workflows that can interact with a broad array of other applications, improving efficiency, reducing manual labor, and ensuring critical documents are collected and processed on time. -Below are some of the applications you can build with the Superdocu API: +# Example Use Cases -- Online multimedia magazines -- Collaborative document editing -- Intelligent document indexing -- Automated document generation -- Secure document storage and distribution -- Document annotation and tagging -- Integrations with third-party services -- Document security and encryption -- Dynamic document rendering. +- **Automated Document Collection for Client Onboarding**: Create a Pipedream workflow that triggers when a new client is added to your CRM, like Salesforce. Automatically send a Superdocu document request to the client, with personalized instructions and deadlines for submission. Once documents are submitted, update the client's record in Salesforce, moving them to the next stage in the onboarding process. + +- **HR Employee Onboarding**: When a new employee record is created in your HR platform (for example, BambooHR), trigger a Superdocu document request for mandatory onboarding documents such as tax forms and identification. Once the documents are collected, use Pipedream to notify the HR team and store the files securely in a service like Google Drive or Dropbox. + +- **Contract Management Automation**: Set up a workflow that listens for new contracts created in a project management tool like Asana or Trello. When a new contract card is added, use Superdocu to request necessary signatures from all parties. Upon receiving the signed documents, Pipedream can update the project status and archive the final contract in a designated cloud storage platform, and notify team members through communication channels such as Slack or email. diff --git a/components/supernotes/README.md b/components/supernotes/README.md new file mode 100644 index 0000000000000..3018d3887879f --- /dev/null +++ b/components/supernotes/README.md @@ -0,0 +1,11 @@ +# Overview + +The Supernotes API lets you automate your note-taking and knowledge management tasks. With Supernotes, you can create, update, and organize cards, collaborate on notes with teammates, and track changes in your knowledge base. Using Pipedream, you can harness these abilities to create intricate workflows that leverage the capabilities of Supernotes in concert with hundreds of other services, creating a seamless integration for productivity and collaboration. + +# Example Use Cases + +- **Card Creation Automation**: Automatically create Supernotes cards from emails flagged as important in Gmail. When an email meets certain criteria, Pipedream can capture the content and create a card in Supernotes, ensuring you have a note of all crucial information without manual entry. + +- **Daily Digest Compilation**: Build a workflow that gathers all new cards from Supernotes and compiles them into a daily digest. This digest can then be sent to Slack, giving you and your team an overview of the day's notes and updates without having to leave your communication platform. + +- **Content Triggered Actions**: Set up a Pipedream workflow that listens for specific keywords or tags in newly created Supernotes cards. When a match is found, trigger an action such as sending a notification to Discord or creating a task in Asana, linking back to the relevant card for quick reference. diff --git a/components/superphone/README.md b/components/superphone/README.md new file mode 100644 index 0000000000000..f6b0c3eefadbf --- /dev/null +++ b/components/superphone/README.md @@ -0,0 +1,11 @@ +# Overview + +SuperPhone is a communication platform that allows users to manage text and call campaigns through a personal phone number. Leveraging the SuperPhone API on Pipedream, you can automate interactions with contacts, send personalized messages, and get analytics on message deliverability and engagement. With Pipedream's serverless platform, you can trigger workflows on various events, like receiving a new message or a specific keyword, and connect to countless other apps to extend functionality. + +# Example Use Cases + +- **Automate Welcome Messages**: When a new contact is added in SuperPhone, trigger a Pipedream workflow to send a personalized welcome text. This can help engage users right from the start without manual effort. + +- **Sync Contacts with CRM**: Whenever a contact is updated or tagged in SuperPhone, use a Pipedream workflow to sync this information with your CRM platform like Salesforce or HubSpot. This ensures your customer data is always up to date across all platforms. + +- **Text-to-Email Campaigns**: Create a Pipedream workflow that listens for specific keywords in incoming SuperPhone messages and automatically sends related promotional content or responses via an email marketing service like Mailchimp. This can bridge the gap between your SMS and email marketing efforts. diff --git a/components/supersaas/README.md b/components/supersaas/README.md index 672f065a6f59f..b3b17560e8fd9 100644 --- a/components/supersaas/README.md +++ b/components/supersaas/README.md @@ -1,26 +1,11 @@ # Overview -With the SuperSaaS API, you can build a variety of applications and services to -help manage appointment bookings and scheduling. This flexible and powerful API -can be used to create automated workflows, powerful integrations, and robust -back-end services. +The SuperSaaS API unlocks the power to automate and streamline your appointment scheduling processes. This tool is tailored for businesses and individuals who manage bookings, reservations, or rentals. By leveraging the API with Pipedream's integration capabilities, you can craft custom workflows that handle everything from syncing appointment data with your calendar, sending customized notifications, to processing payments or follow-ups based on booking activities. -SuperSaaS provides a comprehensive set of features, including access to your -appointments and scheduling data, making it possible to build a variety of -projects: +# Example Use Cases -- Create a custom online booking service, using SuperSaaS authentication and - scheduling tools -- Develop a connected calendar to sync bookings with popular products like - Outlook, Google Calendar, Apple Calendar -- Develop a custom dashboard for customers to easily manage their appointments -- Leverage advanced analytics to track appointment trends and gain insights -- Automate appointment reminders and notifications workflows -- Use SuperSaaS payment gateway to accept payments securely -- Build multi-user, multi-office appointment scheduling systems -- Connect with other products, like Slack, to automatically post appointment - and scheduling updates -- Develop custom forms and templates to collect information from booking - customers -- Create custom pricing models and rules for each appointment -- Create an admin interface for managing any aspect of appointment bookings +- **Calendar Sync with Google Calendar:** Automatically sync new SuperSaaS appointments to a Google Calendar. When an appointment is booked in SuperSaaS, Pipedream triggers a workflow that creates or updates an event in Google Calendar, ensuring your schedule is always up-to-date. + +- **Customer Follow-Up via Email:** Streamline communication by setting up an automation that sends personalized follow-up emails through SendGrid or Mailgun after a client books an appointment. Pipedream can capture booking details and use them to populate and dispatch a tailored email, enhancing the customer experience with minimal manual intervention. + +- **Payment Processing with Stripe:** Integrate SuperSaaS with Stripe to automate payment capture after a booking is made. When an appointment is scheduled and marked as paid in SuperSaaS, Pipedream can trigger a corresponding charge in Stripe, streamlining the revenue collection process for your business. diff --git a/components/supportbee/README.md b/components/supportbee/README.md index 43da29d03cd3d..0da5737e8987f 100644 --- a/components/supportbee/README.md +++ b/components/supportbee/README.md @@ -1,14 +1,11 @@ # Overview -Using the SupportBee API, you can build software solutions to supercharge your -customer service. Here are some examples of what you can create with the -SupportBee API: - -- Integrate support tickets with external systems and databases -- Automate customer service workflows -- Generate customer self-service portals -- Create customer support applications -- Automate ticket routing and routing rules -- Create automated ticket notifications -- Generate reports to measure customer service performance -- Share customer data among customer service teams +SupportBee's API hooks into its customer support ticketing system, allowing for programmatic ticket management, reporting, and collaboration. With Pipedream, you can leverage this API to automate responses, escalate issues based on keywords, or sync with other tools to streamline your support workflow. + +# Example Use Cases + +- **Auto-Response to Common Queries**: Use Pipedream to listen for new tickets with specific phrases and automatically respond with canned answers or helpful resources, cutting down the initial response time. + +- **Ticket Escalation via Sentiment Analysis**: Connect SupportBee to a sentiment analysis service. If a ticket's sentiment score is negative, trigger a workflow on Pipedream that prioritizes the ticket and notifies the appropriate team or manager. + +- **Sync Support Tickets with a CRM**: Automatically create or update customer profiles in a CRM like Salesforce whenever a new ticket is received or resolved in SupportBee, ensuring the sales team is up-to-date on customer issues. diff --git a/components/supportivekoala/README.md b/components/supportivekoala/README.md index ecfb20263f4fd..cf3c570bbc7ce 100644 --- a/components/supportivekoala/README.md +++ b/components/supportivekoala/README.md @@ -1,33 +1,11 @@ # Overview -Supportivekoala is a modern integrations platform that provides APIs and -modular components for building, launching and scaling customer-centric -applications and web services. With Supportivekoala, you can develop a wide -range of applications and services to bring customer-oriented business -solutions to life. +The Supportivekoala API offers a range of functionalities to create personalized images with overlaid text, which can be used for social media posts, custom greeting cards, or visual content for various marketing campaigns. With its ability to dynamically generate images, the API lends itself to automation for engaging customers or audiences through visual elements that are tailored to specific individuals or groups. -Supportivekoala makes it easy to build conversational applications, software -integrations, and automated customer experiences. Here are just some of the -applications and services you can create with Supportivekoala: +# Example Use Cases -- Chatbots: Create automated chatbot experiences to help customers and improve - overall customer satisfaction. -- Software integrations: Streamline and automate back-office processes across - multiple applications and services. -- Live support: Connect customer support reps with customers on multiple - platforms, including web, mobile, and messenger. -- Automation: Automate customer processes such as onboarding, billing and - customer service. -- Delivery service integration: Connect your customer service platform with - delivery services like Uber, DoorDash, and Instacart. -- Payment integration: Enable customers to securely make payments from within - Supportivekoala’s applications and services. -- Customer segmentation: Segment customers by interests, demographics and - purchase history for deeper insights and better customer experiences. -- Personalization: Automate and personalize customer experiences by leveraging - customer data. +- **Personalized Welcome Images for Onboarding Emails**: Automate the creation of custom welcome images for new subscribers or users. When a new user signs up via a form, trigger a Pipedream workflow that calls the Supportivekoala API to generate a personalized image with the user's name, and send it as part of an onboarding email sequence using an email service like SendGrid. -No matter what your customer service needs are, you can use Supportivekoala to -develop applications and web services that will help you provide the best -customer experiences. Start building with Supportivekoala today for fast, -convenient, and secure customer service solutions. +- **Custom Social Media Posts for Followers' Birthdays**: Integrate the Supportivekoala API with a CRM like HubSpot to fetch user data. On a customer's birthday, use the API to create a custom birthday greeting image and post it to your social media platforms, such as Facebook or Twitter, using their respective Pipedream actions. + +- **Event-Triggered Promotional Material**: Leverage Supportivekoala to generate unique promotional images when a specific event occurs, such as a sale or product launch. Set up a workflow that listens for webhook events from your e-commerce platform like Shopify. When the event is triggered, use the Supportivekoala API to create an image with the sale details and distribute it across various marketing channels. diff --git a/components/survey2connect/README.md b/components/survey2connect/README.md new file mode 100644 index 0000000000000..bea364d510438 --- /dev/null +++ b/components/survey2connect/README.md @@ -0,0 +1,11 @@ +# Overview + +The Survey2Connect API grants you the power to automate and integrate your survey data with ease. With this API on Pipedream, you can streamline survey logistics, responses, and analytics, making your data work harder for you. Fetch survey results, manage contacts, and trigger actions in real-time as responses come in. It's a dynamic way to connect feedback with your existing tools and workflows. + +# Example Use Cases + +- **Automate Feedback Collection to CRM**: When a new survey response is received, automatically add or update the contact details in your CRM system. This ensures that customer data is current and actionable. + +- **Sync Survey Responses to Google Sheets**: On new survey completion, append the respondent's answers to a Google Sheet. This creates a live data repository for analysis without manual entry. + +- **Trigger Email Campaigns Based on Survey Data**: If a survey response indicates a specific interest or issue, trigger a targeted email campaign from a platform like SendGrid or Mailgun to address the respondent's preferences or concerns. diff --git a/components/survey2connect/package.json b/components/survey2connect/package.json index 791f73bb8a860..3845ef6d230f0 100644 --- a/components/survey2connect/package.json +++ b/components/survey2connect/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/survey_monkey/README.md b/components/survey_monkey/README.md index 7abd3af5fadba..9c2bccfcb914e 100644 --- a/components/survey_monkey/README.md +++ b/components/survey_monkey/README.md @@ -1,23 +1,11 @@ # Overview -The SurveyMonkey API provides a convenient way for developers to build amazing -survey solutions. With the development of the API, developers have a limitless -potential to create products that make collecting, analyzing, and sharing -survey data easier than ever before. +The SurveyMonkey API provides dynamic access to survey creation, retrieval, and distribution functionality, along with deep insights into responses. With Pipedream’s serverless integration platform, you can automate custom workflows that trigger actions based on survey events, analyze survey data, or synchronize survey results with other data sources. Pipedream’s ability to connect with hundreds of apps opens up a vast landscape of automation possibilities, directly integrating with data visualization tools, CRMs, marketing platforms, and more. -With the SurveyMonkey API, you can +# Example Use Cases -- Create survey-driven mobile apps -- Build custom survey experiences -- Enhance your existing software with survey capabilities -- Create email surveys -- Analyze survey data more quickly -- Integrate surveys into online and physical products -- Develop custom survey designs -- Create and edit surveys programmatically -- Create complex survey logic -- and more! +- **Automated Survey Response Processing**: When a new survey response is received, trigger a Pipedream workflow to parse the data, perform sentiment analysis using a tool like the Google Natural Language API, and then store the results in a Google Sheets spreadsheet for team collaboration and real-time analysis. -The possibilities are endless with the SurveyMonkey API. With its powerful -features, there is a world of solutions possible for businesses and individuals -alike. Let SurveyMonkey power your data collection and analysis needs. +- **Survey Distribution via Email Campaigns**: For every new contact added to a Mailchimp list, use a Pipedream workflow to automatically send a personalized SurveyMonkey survey link. Then, based on survey completion, trigger a follow-up campaign or tag the contact for segmentation and targeted marketing efforts. + +- **Real-time Survey Alerts and Reporting**: Configure a Pipedream workflow to monitor for new survey completions. When a completion is detected, use the workflow to format a summary and send it via Slack to a designated channel, or via email to stakeholders. This ensures immediate visibility of survey results and enables quick action on the collected feedback. diff --git a/components/survey_sparrow/README.md b/components/survey_sparrow/README.md new file mode 100644 index 0000000000000..bd2159b8b38c8 --- /dev/null +++ b/components/survey_sparrow/README.md @@ -0,0 +1,11 @@ +# Overview + +The Survey Sparrow API allows you to automate interactions with your surveys, such as creating and sending surveys, retrieving survey responses, and managing contacts. Integrating this API on Pipedream enables the creation of powerful serverless workflows that can react to survey events, analyze responses in real-time, and connect survey data with other apps to enhance data-driven decision-making. + +# Example Use Cases + +- **Automate Follow-Up Emails**: Trigger a workflow in Pipedream whenever a new survey response is received. Use the respondent's data to send personalized follow-up emails via an email platform like SendGrid or Mailgun, thanking participants or providing further instructions. + +- **Survey Data Analysis and Reporting**: Collect survey responses and feed them into a Google Sheets document for further analysis. Set up periodic reports to be generated and sent to your team through platforms like Slack or via email, ensuring everyone is up-to-date with the latest feedback. + +- **Contact Management Integration**: Whenever a new contact is added in your CRM, such as Salesforce, use Pipedream to automatically create a new contact in Survey Sparrow. This keeps your survey contacts in sync and ensures that all potential respondents are accounted for in your outreach efforts. diff --git a/components/survey_sparrow/package.json b/components/survey_sparrow/package.json new file mode 100644 index 0000000000000..b06161799ee3f --- /dev/null +++ b/components/survey_sparrow/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/survey_sparrow", + "version": "0.0.1", + "description": "Pipedream Survey Sparrow Components", + "main": "survey_sparrow.app.mjs", + "keywords": [ + "pipedream", + "survey_sparrow" + ], + "homepage": "https://pipedream.com/apps/survey_sparrow", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/survey_sparrow/survey_sparrow.app.mjs b/components/survey_sparrow/survey_sparrow.app.mjs new file mode 100644 index 0000000000000..cea70d28bda29 --- /dev/null +++ b/components/survey_sparrow/survey_sparrow.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "survey_sparrow", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/surveybot/README.md b/components/surveybot/README.md index 9c1bdd6f15146..23e393d89727e 100644 --- a/components/surveybot/README.md +++ b/components/surveybot/README.md @@ -1,36 +1,11 @@ # Overview -The Surveybot API is an easy-to-use platform that allows you to create powerful -automated surveys. With the Surveybot API, you can build a variety of automated -surveys that can help you garner feedback, measure user engagement, and gain -insight into customer opinions. Some of the features available with the -Surveybot API include: +The Surveybot API is a tool designed to enhance engagement and gather insights through conversational surveys within Facebook Messenger. By leveraging Pipedream's capabilities, you can automate responses, analyze survey data, and integrate with an array of other services such as CRMs, email marketing platforms, or helpdesk software to streamline workflows and act upon the gathered data effectively. -- Intuitive and easy-to-use survey creation tools -- Customizable survey logic and settings -- Advanced voice and text recognition capabilities -- Integration with CRMs and other third-party services -- In-depth analytics and data visualization +# Example Use Cases -Here are some of the things you can create and do with the Surveybot API: +- **Customer Feedback Collection to Data Analysis Tools**: With Pipedream, set up a workflow where Surveybot collects customer satisfaction data post-purchase, and automatically send the results to data analysis tools such as Google Sheets or BigQuery. Slice and dice the data to gain actionable insights or trigger follow-up actions based on customer sentiment. -- Gamified surveys that reward and incentivize users -- Stickier, interactive surveys that ask different questions based on previous - answers -- Automated surveys that trigger after an app user completes a specific task or - event -- Specific surveys based on user profiles and preferences -- Automate customer onboarding surveys -- Track customer sentiment and feedback over time -- Gather product feedback to improve the user experience -- Test hypotheses, product ideations, and prototypes -- Optimize user experience and measure user engagement -- Send automated, timely reminders to encourage survey completion -- Gain insights into customer preferences, opinions, and feature requests -- And much more! +- **Support Ticket Creation from Survey Responses**: Whenever a survey response indicates a customer issue, use Pipedream to create a ticket in a customer support app like Zendesk or Intercom. This quick conversion from feedback to support ticket can help resolve issues swiftly and improve overall customer experience. -So whether you're a B2B business looking to better understand customer needs, a -startup looking to gain market insights, or a brand looking to measure user -experience and gain feedback, the Surveybot API has you covered. With its -comprehensive set of features and advanced data analytics, Surveybot can easily -create an automated survey tailored to fit your specific needs. +- **Lead Qualification and Addition to CRM**: Use Surveybot to qualify leads through a set of questions. With Pipedream, assess the responses and automatically add qualified leads to a CRM such as Salesforce or HubSpot, including all relevant data points. This can significantly cut down manual entry and ensure timely follow-up with potential customers. diff --git a/components/surveybot/package.json b/components/surveybot/package.json new file mode 100644 index 0000000000000..5474f4c3e2c20 --- /dev/null +++ b/components/surveybot/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/surveybot", + "version": "0.6.0", + "description": "Pipedream surveybot Components", + "main": "surveybot.app.mjs", + "keywords": [ + "pipedream", + "surveybot" + ], + "homepage": "https://pipedream.com/apps/surveybot", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/surveycto/README.md b/components/surveycto/README.md new file mode 100644 index 0000000000000..deb2fdb796b51 --- /dev/null +++ b/components/surveycto/README.md @@ -0,0 +1,11 @@ +# Overview + +The SurveyCTO API enables automated interactions with SurveyCTO services, a platform focusing on data collection and organization for research and survey projects. With this API, you can seamlessly retrieve, push, and manage survey data. Integrating SurveyCTO with Pipedream opens a realm of possibilities for automating data workflows, such as triggering actions based on new survey submissions, syncing collected data with other databases or apps, or even automating reports and notifications. + +# Example Use Cases + +- **Automate Data Collection Alerts**: When a new survey submission is received in SurveyCTO, trigger a Pipedream workflow to send a notification through email, Slack, or another messaging service. This keeps your team instantly informed about new data entries. + +- **Synchronize Survey Data with Cloud Storage**: Set up a Pipedream workflow to automatically transfer new survey submissions from SurveyCTO to a cloud storage service like Google Drive or Dropbox. This ensures your data backups are always up to date without manual intervention. + +- **Integrate Survey Data with Analytics Platforms**: Use Pipedream to send SurveyCTO data to analytics tools such as Google Sheets or Tableau for real-time analysis. Whenever a survey is completed, the workflow can parse the response and append the data to a sheet or dashboard, providing immediate insights. diff --git a/components/surveymethods/README.md b/components/surveymethods/README.md index eaa9ee4a742f4..8aa2f1d6d8ffc 100644 --- a/components/surveymethods/README.md +++ b/components/surveymethods/README.md @@ -1,31 +1,11 @@ # Overview -The SurveyMethods API provides an easy way to build surveys and poll -applications. With the SurveyMethods API, you can build a wide variety of -applications that help customers understand their market and customers better. -By utilizing the SurveyMethods API, businesses and organizations can -communicate with their customers and stakeholders quickly and efficiently. +The SurveyMethods API allows users to automate the management and analysis of surveys and the corresponding data. With the Pipedream integration, you can trigger workflows based on survey events, sync survey results with other databases or apps, and manage survey distribution programmatically. This serves to streamline the survey process from distribution to data actioning, all in real-time and without the need for manual intervention. -These are just some of the applications that can be built with the -SurveyMethods API: +# Example Use Cases -- Online Polls: With the SurveyMethods API, businesses can quickly collect - customer opinions by creating online polls. This allows them to understand - their customers better and make informed decisions. -- Customer Surveys: The SurveyMethods API can be used to quickly create - customer surveys that help organizations better understand their customers’ - needs. -- Market Research: By using the SurveyMethods API, businesses can quickly get - market insights from their customers. This helps them keep track of their - competitors and make more informed decisions. -- Event Feedback: Businesses can create event feedback surveys with the - SurveyMethods API that make it easier for them to get feedback from - attendees. -- Employee Satisfaction: Companies can use the SurveyMethods API to survey - their employees about their satisfaction with the work environment, benefits, - etc. -- Recruitment: Companies can create recruitment polls with the SurveyMethods - API to gather important information about potential job applicants. -- Training Evaluation: Companies can use the SurveyMethods API to create - training evaluation surveys that help them assess the effectiveness of their - employee training programs. +- **Automated Survey Responses Aggregation**: Capture SurveyMethods responses and aggregate them in a Google Sheet for real-time analysis. Each time a new response is received, a Pipedream workflow is triggered, appending the response data to a designated Google Sheet. This is perfect for teams needing to analyze feedback instantly without manual exports. + +- **Immediate Follow-up on Survey Feedback**: Instantly reach out to respondents who provided specific feedback. Say a respondent indicates dissatisfaction; Pipedream can trigger an email through an app like SendGrid, offering support or a discount code, thus acting swiftly on user sentiment. + +- **Survey Completion Tracking in CRM**: Log completed surveys as events in a CRM like Salesforce. When a respondent completes a survey, Pipedream adds a record of this completion to the respondent's contact entry on Salesforce. This helps sales and customer success teams personalize follow-up communications. diff --git a/components/surveysparrow/README.md b/components/surveysparrow/README.md new file mode 100644 index 0000000000000..bc599d3bde1c7 --- /dev/null +++ b/components/surveysparrow/README.md @@ -0,0 +1,11 @@ +# Overview + +The SurveySparrow API lets you tap into a robust platform for gathering feedback and insights. With Pipedream, you can automate interactions with your surveys, manage contacts, and analyze responses in real time. You can create workflows that trigger on new survey responses, sync data to other services, or even kick off email campaigns based on survey results. The power of Pipedream's serverless platform means you can integrate SurveySparrow with hundreds of other apps, enabling limitless automation scenarios without writing extensive code. + +# Example Use Cases + +- **Automate Response Acknowledgment**: Send a thank you email automatically using the Gmail app on Pipedream when a new SurveySparrow response is received. This keeps your respondents engaged and shows appreciation for their input. + +- **Sync Survey Data to Google Sheets**: Every time a survey is completed, append the responses to a Google Sheet for easy data analysis and sharing. This workflow can be vital for teams that rely on collaborative data analysis. + +- **Trigger Follow-up Actions Based on Responses**: Use SurveySparrow responses to trigger follow-up tasks or actions in project management tools like Trello or Asana. For instance, if a respondent reports an issue, automatically create a Trello card to ensure it is addressed in a timely manner. diff --git a/components/surveysparrow/actions/common/share-base.mjs b/components/surveysparrow/actions/common/share-base.mjs new file mode 100644 index 0000000000000..3cd432e571493 --- /dev/null +++ b/components/surveysparrow/actions/common/share-base.mjs @@ -0,0 +1,119 @@ +import common from "../../common/constants.mjs"; +import surveysparrow from "../../surveysparrow.app.mjs"; + +export default { + props: { + surveysparrow, + survey: { + propDefinition: [ + surveysparrow, + "survey", + ], + }, + name: { + type: "string", + label: "Name", + description: "Name of the channel.", + }, + mode: { + type: "string", + label: "Mode", + description: "Mode of the channel.", + options: common.MODE_OPTIONS, + optional: true, + reloadProps: true, + }, + }, + async additionalProps() { + let props = {}; + if (this.mode === "RECURRING") { + props.frequency = { + type: "string", + label: "Frequency", + description: "The frequency of the recurring.", + options: common.FREQUENCY_OPTIONS, + reloadProps: true, + }; + if (this.frequency === "WEEKLY") { + props.weekDay = { + type: "integer[]", + label: "Week Days", + description: "The days of the week of the schedule.", + options: common.WEEK_OPTIONS, + }; + } + if ((this.frequency === "MONTHLY") || (this.frequency === "YEARLY")) { + props.day = { + type: "string[]", + label: "On", + description: "The day of the schedule.", + options: common.DAY_OPTIONS, + }; + } + if (this.frequency === "YEARLY") { + props.month = { + type: "integer[]", + label: "Month", + description: "The months of the schedule.", + options: common.MONTH_OPTIONS, + }; + } + if (this.frequency) { + props.hour = { + type: "integer", + label: "Hour", + description: "The hour of the schedule.", + min: 1, + max: 24, + }; + props.minute = { + type: "integer", + label: "Minute", + description: "The minute of the schedule.", + min: 0, + max: 59, + }; + } + } + + return props; + }, + async run({ $ }) { + const scheduleObject = (this.mode === "RECURRING") + ? { + schedule: { + frequency: this.frequency, + config: { + D: this.day && this.day.map((day) => { + if (day === "LAST DAY") return 0; + return parseInt(day); + }).sort((a, b) => a - b), + M: this.month && this.month.sort((a, b) => a - b), + d: this.weekDay && this.weekDay.sort((a, b) => a - b), + h: [ + this.hour, + ], + m: [ + this.minute, + ], + }, + }, + } + : {}; + + const response = await this.surveysparrow.createChannel({ + $, + data: { + type: this.getChannelType(), + mode: this.mode, + survey_id: this.survey, + name: this.name, + ...this.getData(), + ...scheduleObject, + }, + }); + + $.export("$summary", this.getSummary()); + return response; + }, +}; diff --git a/components/surveysparrow/actions/create-contact/create-contact.mjs b/components/surveysparrow/actions/create-contact/create-contact.mjs index e4f6c64839f07..313edaf73acd5 100644 --- a/components/surveysparrow/actions/create-contact/create-contact.mjs +++ b/components/surveysparrow/actions/create-contact/create-contact.mjs @@ -4,7 +4,7 @@ export default { key: "surveysparrow-create-contact", name: "Create Contact", description: "Creates a new contact. [See the documentation](https://developers.surveysparrow.com/rest-apis/contacts#postV3Contacts)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { surveySparrow, @@ -40,6 +40,7 @@ export default { }, async run({ $ }) { const { data: response } = await this.surveySparrow.createContact({ + $, data: { email: this.email, full_name: this.fullName, @@ -47,7 +48,6 @@ export default { job_title: this.jobTitle, contact_type: this.contactType, }, - $, }); if (response) { diff --git a/components/surveysparrow/actions/create-survey/create-survey.mjs b/components/surveysparrow/actions/create-survey/create-survey.mjs index 262dfbcef4ea5..88553224fb3ed 100644 --- a/components/surveysparrow/actions/create-survey/create-survey.mjs +++ b/components/surveysparrow/actions/create-survey/create-survey.mjs @@ -4,7 +4,7 @@ export default { key: "surveysparrow-create-survey", name: "Create Survey", description: "Creates a new survey. [See the documentation](https://developers.surveysparrow.com/rest-apis/survey#postV3Surveys)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { surveySparrow, diff --git a/components/surveysparrow/actions/share-nps-survey-sms/share-nps-survey-sms.mjs b/components/surveysparrow/actions/share-nps-survey-sms/share-nps-survey-sms.mjs new file mode 100644 index 0000000000000..2e2696baaa9bb --- /dev/null +++ b/components/surveysparrow/actions/share-nps-survey-sms/share-nps-survey-sms.mjs @@ -0,0 +1,35 @@ +import common from "../common/share-base.mjs"; + +export default { + ...common, + key: "surveysparrow-share-nps-survey-sms", + name: "Share NPS Survey via SMS", + description: "Sends a saved NPS share template via SMS to given mobile number recipients. [See the documentation](https://developers.surveysparrow.com/rest-apis/channels#postV3Channels)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + sms: { + type: "string", + label: "SMS message", + description: "Message of sms, both {company_name} and {survey_link} variables are required.", + default: "{survey_link} from {company_name}", + }, + }, + methods: { + ...common.methods, + getChannelType() { + return "SMS"; + }, + getSummary() { + return "Successfully sent NPS survey via SMS"; + }, + getData() { + return { + sms: { + message: this.sms, + }, + }; + }, + }, +}; diff --git a/components/surveysparrow/actions/share-survey-email/share-survey-email.mjs b/components/surveysparrow/actions/share-survey-email/share-survey-email.mjs new file mode 100644 index 0000000000000..e0167ff4fc9fa --- /dev/null +++ b/components/surveysparrow/actions/share-survey-email/share-survey-email.mjs @@ -0,0 +1,43 @@ +import common from "../common/share-base.mjs"; + +export default { + ...common, + key: "surveysparrow-share-survey-email", + name: "Share Survey via Email", + description: "Sends a saved email share template to a provided email address. Configure the saved template's name and the recipient's email address. [See the documentation](https://developers.surveysparrow.com/rest-apis)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + subject: { + type: "string", + label: "Subject", + description: "Subject of the email.", + }, + themeId: { + propDefinition: [ + common.props.surveysparrow, + "themeId", + ], + optional: true, + }, + }, + methods: { + ...common.methods, + getChannelType() { + return "EMAIL"; + }, + getSummary() { + return "Successfully sent NPS survey via Email"; + }, + getData() { + return { + email: { + subject: this.subject, + theme_id: this.themeId, + }, + }; + }, + }, +}; + diff --git a/components/surveysparrow/actions/update-survey/update-survey.mjs b/components/surveysparrow/actions/update-survey/update-survey.mjs index 8804fdf689763..f6902ff332f84 100644 --- a/components/surveysparrow/actions/update-survey/update-survey.mjs +++ b/components/surveysparrow/actions/update-survey/update-survey.mjs @@ -1,12 +1,12 @@ -import surveySparrow from "../../surveysparrow.app.mjs"; -import pickBy from "lodash.pickby"; import { ConfigurationError } from "@pipedream/platform"; +import pickBy from "lodash.pickby"; +import surveySparrow from "../../surveysparrow.app.mjs"; export default { key: "surveysparrow-update-survey", name: "Update Survey", description: "Updates an existing survey. [See the documentation](https://developers.surveysparrow.com/rest-apis/survey#patchV3SurveysId)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { surveySparrow, diff --git a/components/surveysparrow/common/constants.mjs b/components/surveysparrow/common/constants.mjs index 22e99ac6e7510..eadee1a1f2aa4 100644 --- a/components/surveysparrow/common/constants.mjs +++ b/components/surveysparrow/common/constants.mjs @@ -19,8 +19,141 @@ const VISIBILITY_OPTIONS = [ "Mine", ]; +const DAY_OPTIONS = [ + "LAST DAY", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", +]; + +const MONTH_OPTIONS = [ + { + label: "Jan", + value: 1, + }, + { + label: "Feb", + value: 2, + }, + { + label: "Mar", + value: 3, + }, + { + label: "Apr", + value: 4, + }, + { + label: "May", + value: 5, + }, + { + label: "Jun", + value: 6, + }, + { + label: "Jul", + value: 7, + }, + { + label: "Aug", + value: 8, + }, + { + label: "Sep", + value: 9, + }, + { + label: "Oct", + value: 10, + }, + { + label: "Nov", + value: 11, + }, + { + label: "Dec", + value: 12, + }, +]; + +const WEEK_OPTIONS = [ + { + label: "Sun", + value: 1, + }, + { + label: "Mon", + value: 2, + }, + { + label: "Tue", + value: 3, + }, + { + label: "Wed", + value: 4, + }, + { + label: "Thu", + value: 5, + }, + { + label: "Fri", + value: 6, + }, + { + label: "Sat", + value: 7, + }, +]; + +const FREQUENCY_OPTIONS = [ + "WEEKLY", + "MONTHLY", + "YEARLY", +]; +const MODE_OPTIONS = [ + "BLAST", + "RECURRING", + "RELATIVE_RECURRING", +]; + export default { SURVEY_TYPE_OPTIONS, CONTACT_TYPE_OPTIONS, VISIBILITY_OPTIONS, + FREQUENCY_OPTIONS, + MODE_OPTIONS, + DAY_OPTIONS, + WEEK_OPTIONS, + MONTH_OPTIONS, }; diff --git a/components/surveysparrow/package.json b/components/surveysparrow/package.json index aefc06097803d..83788e54a4c84 100644 --- a/components/surveysparrow/package.json +++ b/components/surveysparrow/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/surveysparrow", - "version": "0.1.0", + "version": "0.2.0", "description": "Pipedream SurveySparrow Components", "main": "surveysparrow.app.mjs", "keywords": [ diff --git a/components/surveysparrow/sources/common/base.mjs b/components/surveysparrow/sources/common/base.mjs new file mode 100644 index 0000000000000..6c21acc6a65b1 --- /dev/null +++ b/components/surveysparrow/sources/common/base.mjs @@ -0,0 +1,77 @@ +import surveySparrow from "../../surveysparrow.app.mjs"; + +export default { + props: { + surveySparrow, + db: "$.service.db", + http: "$.interface.http", + survey: { + propDefinition: [ + surveySparrow, + "survey", + ], + }, + }, + hooks: { + async deploy() { + const { data: responses } = await this.surveySparrow.listResponses({ + params: { + limit: 25, + order: "DESC", + survey_id: this.survey, + }, + }); + for (const response of responses) { + const meta = this.generateMeta(response); + this.$emit(response, meta); + } + }, + async activate() { + const { data: hook } = await this.surveySparrow.createWebhook({ + data: { + url: this.http.endpoint, + http_method: "POST", + survey_id: this.survey, + payload: { + id: "{submission_id}", + }, + }, + }); + this._setHookId(hook.id); + }, + async deactivate() { + const hookId = this._getHookId(); + await this.surveySparrow.deleteWebhook({ + hookId, + }); + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + generateMeta(response) { + return { + id: response.id, + summary: `New response with ID ${response.id}`, + ts: Date.now(), + }; + }, + }, + async run(event) { + const { body } = event; + + const { data: response } = await this.surveySparrow.getResponse({ + responseId: body.id, + params: { + survey_id: this.survey, + }, + }); + + const meta = this.generateMeta(response); + this.$emit(response, meta); + }, +}; diff --git a/components/surveysparrow/sources/new-ces-submission-instant/new-ces-submission-instant.mjs b/components/surveysparrow/sources/new-ces-submission-instant/new-ces-submission-instant.mjs new file mode 100644 index 0000000000000..6d08d98848498 --- /dev/null +++ b/components/surveysparrow/sources/new-ces-submission-instant/new-ces-submission-instant.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "surveysparrow-new-ces-submission-instant", + name: "New CES Submission (Instant)", + description: "Emit new event when a customer effort score (CES) survey receives a new submission.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + survey: { + propDefinition: [ + common.props.surveySparrow, + "survey", + () => ({ + surveyType: "CES", + }), + ], + }, + }, + sampleEmit, +}; diff --git a/components/surveysparrow/sources/new-ces-submission-instant/test-event.mjs b/components/surveysparrow/sources/new-ces-submission-instant/test-event.mjs new file mode 100644 index 0000000000000..4eb03c3e30d05 --- /dev/null +++ b/components/surveysparrow/sources/new-ces-submission-instant/test-event.mjs @@ -0,0 +1,142 @@ +export default { + "id": 123123123, + "completed_time": "2024-03-12T18:42:15.293Z", + "survey_id": 123123123, + "contact_id": null, + "state": "COMPLETED", + "channel_id": 123123123, + "language": null, + "start_time": "2024-03-12T18:41:15.293Z", + "contact": null, + "channel": { + "name": "System Share - 14 Mar 2024 11:41", + "type": "SYSTEM", + "status": "ACTIVE" + }, + "answers": [ + { + "answer": "Value for money", + "detailed_answer": [ + { + "choice_id": 123123123, + "choice_text": "Value for money", + "img": "https://static.surveysparrow.com/application/production/1593497232934__123123123835f1a8f7f4b9be159a8f3397a8f9bb9def03fa55fa3__value-for-money.png" + } + ], + "other": false, + "position": 2, + "other_choice_text": "Value for money", + "question": "What are the top reasons that made you choose us?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": 4, + "rating_scale": 5, + "question": "How would you rate the quality of our product?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": 8, + "step": 10, + "question": "How satisfied are you with the competency of our customer support?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "Product quality exceeded expectations, but the checkout process needs streamlining.", + "answer_sentiment": null, + "question": "Uh-oh! What could have been better?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "Efficient order tracking and timely updates contribute to a positive shopping experience.", + "answer_sentiment": null, + "question": "Off the top of your head, what are the 3 words that you'll use to describe MediaPandaa?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": 0, + "step": 10, + "question": "On a scale of 0-10, how likely are you to recommend MediaPandaa to a friend or colleague?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "Smooth shopping experience with quick delivery and excellent customer service.", + "answer_sentiment": null, + "question": "What prompted you to score an $Question_123123123?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "This e-commerce website is fantastic, offering a wide range of products and a user-friendly interface!", + "answer_sentiment": null, + "question": "What prompted you to score a $Question_123123123?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "2024-03-12T18:41:15.293Z", + "question_id": "startTime" + }, + { + "answer": "2024-03-12T18:42:15.293Z", + "question_id": "submittedTime" + }, + { + "answer": "null", + "question_id": "ip" + }, + { + "answer": "COMPUTER", + "question_id": "deviceType" + }, + { + "answer": "null", + "question_id": "location" + }, + { + "answer": "", + "question_id": "browser" + }, + { + "answer": "", + "question_id": "browserLanguage" + }, + { + "answer": "", + "question_id": "os" + }, + { + "answer": "America/Los_Angeles", + "question_id": "timeZone" + }, + { + "answer": "null", + "question_id": "latitude" + }, + { + "answer": "null", + "question_id": "longitude" + }, + { + "question_id": "totalScore" + } + ], + "device": {}, + "tags": [], + "variables": [], + "expressions": [] +} \ No newline at end of file diff --git a/components/surveysparrow/sources/new-csat-submission-instant/new-csat-submission-instant.mjs b/components/surveysparrow/sources/new-csat-submission-instant/new-csat-submission-instant.mjs new file mode 100644 index 0000000000000..ec4ec9c1e2925 --- /dev/null +++ b/components/surveysparrow/sources/new-csat-submission-instant/new-csat-submission-instant.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "surveysparrow-new-csat-submission-instant", + name: "New CSAT Submission (Instant)", + description: "Emit new event when a customer satisfaction (CSAT) survey receives a new submission.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + survey: { + propDefinition: [ + common.props.surveySparrow, + "survey", + () => ({ + surveyType: "CSAT", + }), + ], + }, + }, + sampleEmit, +}; diff --git a/components/surveysparrow/sources/new-csat-submission-instant/test-event.mjs b/components/surveysparrow/sources/new-csat-submission-instant/test-event.mjs new file mode 100644 index 0000000000000..4eb03c3e30d05 --- /dev/null +++ b/components/surveysparrow/sources/new-csat-submission-instant/test-event.mjs @@ -0,0 +1,142 @@ +export default { + "id": 123123123, + "completed_time": "2024-03-12T18:42:15.293Z", + "survey_id": 123123123, + "contact_id": null, + "state": "COMPLETED", + "channel_id": 123123123, + "language": null, + "start_time": "2024-03-12T18:41:15.293Z", + "contact": null, + "channel": { + "name": "System Share - 14 Mar 2024 11:41", + "type": "SYSTEM", + "status": "ACTIVE" + }, + "answers": [ + { + "answer": "Value for money", + "detailed_answer": [ + { + "choice_id": 123123123, + "choice_text": "Value for money", + "img": "https://static.surveysparrow.com/application/production/1593497232934__123123123835f1a8f7f4b9be159a8f3397a8f9bb9def03fa55fa3__value-for-money.png" + } + ], + "other": false, + "position": 2, + "other_choice_text": "Value for money", + "question": "What are the top reasons that made you choose us?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": 4, + "rating_scale": 5, + "question": "How would you rate the quality of our product?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": 8, + "step": 10, + "question": "How satisfied are you with the competency of our customer support?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "Product quality exceeded expectations, but the checkout process needs streamlining.", + "answer_sentiment": null, + "question": "Uh-oh! What could have been better?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "Efficient order tracking and timely updates contribute to a positive shopping experience.", + "answer_sentiment": null, + "question": "Off the top of your head, what are the 3 words that you'll use to describe MediaPandaa?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": 0, + "step": 10, + "question": "On a scale of 0-10, how likely are you to recommend MediaPandaa to a friend or colleague?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "Smooth shopping experience with quick delivery and excellent customer service.", + "answer_sentiment": null, + "question": "What prompted you to score an $Question_123123123?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "This e-commerce website is fantastic, offering a wide range of products and a user-friendly interface!", + "answer_sentiment": null, + "question": "What prompted you to score a $Question_123123123?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "2024-03-12T18:41:15.293Z", + "question_id": "startTime" + }, + { + "answer": "2024-03-12T18:42:15.293Z", + "question_id": "submittedTime" + }, + { + "answer": "null", + "question_id": "ip" + }, + { + "answer": "COMPUTER", + "question_id": "deviceType" + }, + { + "answer": "null", + "question_id": "location" + }, + { + "answer": "", + "question_id": "browser" + }, + { + "answer": "", + "question_id": "browserLanguage" + }, + { + "answer": "", + "question_id": "os" + }, + { + "answer": "America/Los_Angeles", + "question_id": "timeZone" + }, + { + "answer": "null", + "question_id": "latitude" + }, + { + "answer": "null", + "question_id": "longitude" + }, + { + "question_id": "totalScore" + } + ], + "device": {}, + "tags": [], + "variables": [], + "expressions": [] +} \ No newline at end of file diff --git a/components/surveysparrow/sources/new-nps-submission-instant/new-nps-submission-instant.mjs b/components/surveysparrow/sources/new-nps-submission-instant/new-nps-submission-instant.mjs new file mode 100644 index 0000000000000..2a592ffc6c445 --- /dev/null +++ b/components/surveysparrow/sources/new-nps-submission-instant/new-nps-submission-instant.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "surveysparrow-new-nps-submission-instant", + name: "New NPS Submission (Instant)", + description: "Emit new event when a net promoter score (NPS) survey receives a new submission.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + survey: { + propDefinition: [ + common.props.surveySparrow, + "survey", + () => ({ + surveyType: "NPS", + }), + ], + }, + }, + sampleEmit, +}; diff --git a/components/surveysparrow/sources/new-nps-submission-instant/test-event.mjs b/components/surveysparrow/sources/new-nps-submission-instant/test-event.mjs new file mode 100644 index 0000000000000..4eb03c3e30d05 --- /dev/null +++ b/components/surveysparrow/sources/new-nps-submission-instant/test-event.mjs @@ -0,0 +1,142 @@ +export default { + "id": 123123123, + "completed_time": "2024-03-12T18:42:15.293Z", + "survey_id": 123123123, + "contact_id": null, + "state": "COMPLETED", + "channel_id": 123123123, + "language": null, + "start_time": "2024-03-12T18:41:15.293Z", + "contact": null, + "channel": { + "name": "System Share - 14 Mar 2024 11:41", + "type": "SYSTEM", + "status": "ACTIVE" + }, + "answers": [ + { + "answer": "Value for money", + "detailed_answer": [ + { + "choice_id": 123123123, + "choice_text": "Value for money", + "img": "https://static.surveysparrow.com/application/production/1593497232934__123123123835f1a8f7f4b9be159a8f3397a8f9bb9def03fa55fa3__value-for-money.png" + } + ], + "other": false, + "position": 2, + "other_choice_text": "Value for money", + "question": "What are the top reasons that made you choose us?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": 4, + "rating_scale": 5, + "question": "How would you rate the quality of our product?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": 8, + "step": 10, + "question": "How satisfied are you with the competency of our customer support?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "Product quality exceeded expectations, but the checkout process needs streamlining.", + "answer_sentiment": null, + "question": "Uh-oh! What could have been better?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "Efficient order tracking and timely updates contribute to a positive shopping experience.", + "answer_sentiment": null, + "question": "Off the top of your head, what are the 3 words that you'll use to describe MediaPandaa?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": 0, + "step": 10, + "question": "On a scale of 0-10, how likely are you to recommend MediaPandaa to a friend or colleague?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "Smooth shopping experience with quick delivery and excellent customer service.", + "answer_sentiment": null, + "question": "What prompted you to score an $Question_123123123?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "This e-commerce website is fantastic, offering a wide range of products and a user-friendly interface!", + "answer_sentiment": null, + "question": "What prompted you to score a $Question_123123123?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "2024-03-12T18:41:15.293Z", + "question_id": "startTime" + }, + { + "answer": "2024-03-12T18:42:15.293Z", + "question_id": "submittedTime" + }, + { + "answer": "null", + "question_id": "ip" + }, + { + "answer": "COMPUTER", + "question_id": "deviceType" + }, + { + "answer": "null", + "question_id": "location" + }, + { + "answer": "", + "question_id": "browser" + }, + { + "answer": "", + "question_id": "browserLanguage" + }, + { + "answer": "", + "question_id": "os" + }, + { + "answer": "America/Los_Angeles", + "question_id": "timeZone" + }, + { + "answer": "null", + "question_id": "latitude" + }, + { + "answer": "null", + "question_id": "longitude" + }, + { + "question_id": "totalScore" + } + ], + "device": {}, + "tags": [], + "variables": [], + "expressions": [] +} \ No newline at end of file diff --git a/components/surveysparrow/sources/new-survey-response/new-survey-response.mjs b/components/surveysparrow/sources/new-survey-response/new-survey-response.mjs index acbf7dd65a3f2..9d6fccd222f00 100644 --- a/components/surveysparrow/sources/new-survey-response/new-survey-response.mjs +++ b/components/surveysparrow/sources/new-survey-response/new-survey-response.mjs @@ -1,83 +1,13 @@ -import surveySparrow from "../../surveysparrow.app.mjs"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; export default { + ...common, key: "surveysparrow-new-survey-response", - name: "New Survey Response", - description: "Emit new event each time a the specified survey receives a response. [See the documentation](https://developers.surveysparrow.com/rest-apis/webhooks#postV3Webhooks)", - version: "0.0.1", + name: "New Survey Response (Instant)", + description: "Emit new event each time a the specified survey receives a response.", + version: "0.0.2", type: "source", dedupe: "unique", - props: { - surveySparrow, - db: "$.service.db", - http: "$.interface.http", - survey: { - propDefinition: [ - surveySparrow, - "survey", - ], - }, - }, - hooks: { - async deploy() { - const { data: responses } = await this.surveySparrow.listResponses({ - params: { - limit: 25, - order: "DESC", - survey_id: this.survey, - }, - }); - for (const response of responses) { - const meta = this.generateMeta(response); - this.$emit(response, meta); - } - }, - async activate() { - const { data: hook } = await this.surveySparrow.createWebhook({ - data: { - url: this.http.endpoint, - http_method: "POST", - survey_id: this.survey, - payload: { - id: "{submission_id}", - }, - }, - }); - this._setHookId(hook.id); - }, - async deactivate() { - const hookId = this._getHookId(); - await this.surveySparrow.deleteWebhook({ - hookId, - }); - }, - }, - methods: { - _getHookId() { - return this.db.get("hookId"); - }, - _setHookId(hookId) { - this.db.set("hookId", hookId); - }, - generateMeta(response) { - return { - id: response.id, - summary: `New response with ID ${response.id}`, - ts: Date.now(), - }; - }, - }, - async run(event) { - const { body } = event; - - const { data: response } = await this.surveySparrow.getResponse({ - responseId: body.id, - params: { - survey_id: this.survey, - }, - }); - - const meta = this.generateMeta(response); - this.$emit(response, meta); - }, + sampleEmit, }; diff --git a/components/surveysparrow/sources/new-survey-response/test-event.mjs b/components/surveysparrow/sources/new-survey-response/test-event.mjs new file mode 100644 index 0000000000000..4eb03c3e30d05 --- /dev/null +++ b/components/surveysparrow/sources/new-survey-response/test-event.mjs @@ -0,0 +1,142 @@ +export default { + "id": 123123123, + "completed_time": "2024-03-12T18:42:15.293Z", + "survey_id": 123123123, + "contact_id": null, + "state": "COMPLETED", + "channel_id": 123123123, + "language": null, + "start_time": "2024-03-12T18:41:15.293Z", + "contact": null, + "channel": { + "name": "System Share - 14 Mar 2024 11:41", + "type": "SYSTEM", + "status": "ACTIVE" + }, + "answers": [ + { + "answer": "Value for money", + "detailed_answer": [ + { + "choice_id": 123123123, + "choice_text": "Value for money", + "img": "https://static.surveysparrow.com/application/production/1593497232934__123123123835f1a8f7f4b9be159a8f3397a8f9bb9def03fa55fa3__value-for-money.png" + } + ], + "other": false, + "position": 2, + "other_choice_text": "Value for money", + "question": "What are the top reasons that made you choose us?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": 4, + "rating_scale": 5, + "question": "How would you rate the quality of our product?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": 8, + "step": 10, + "question": "How satisfied are you with the competency of our customer support?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "Product quality exceeded expectations, but the checkout process needs streamlining.", + "answer_sentiment": null, + "question": "Uh-oh! What could have been better?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "Efficient order tracking and timely updates contribute to a positive shopping experience.", + "answer_sentiment": null, + "question": "Off the top of your head, what are the 3 words that you'll use to describe MediaPandaa?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": 0, + "step": 10, + "question": "On a scale of 0-10, how likely are you to recommend MediaPandaa to a friend or colleague?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "Smooth shopping experience with quick delivery and excellent customer service.", + "answer_sentiment": null, + "question": "What prompted you to score an $Question_123123123?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "This e-commerce website is fantastic, offering a wide range of products and a user-friendly interface!", + "answer_sentiment": null, + "question": "What prompted you to score a $Question_123123123?", + "question_tags": [], + "question_id": 123123123, + "skipped": false + }, + { + "answer": "2024-03-12T18:41:15.293Z", + "question_id": "startTime" + }, + { + "answer": "2024-03-12T18:42:15.293Z", + "question_id": "submittedTime" + }, + { + "answer": "null", + "question_id": "ip" + }, + { + "answer": "COMPUTER", + "question_id": "deviceType" + }, + { + "answer": "null", + "question_id": "location" + }, + { + "answer": "", + "question_id": "browser" + }, + { + "answer": "", + "question_id": "browserLanguage" + }, + { + "answer": "", + "question_id": "os" + }, + { + "answer": "America/Los_Angeles", + "question_id": "timeZone" + }, + { + "answer": "null", + "question_id": "latitude" + }, + { + "answer": "null", + "question_id": "longitude" + }, + { + "question_id": "totalScore" + } + ], + "device": {}, + "tags": [], + "variables": [], + "expressions": [] +} \ No newline at end of file diff --git a/components/surveysparrow/surveysparrow.app.mjs b/components/surveysparrow/surveysparrow.app.mjs index 552f124c7b284..d0df0b604e05f 100644 --- a/components/surveysparrow/surveysparrow.app.mjs +++ b/components/surveysparrow/surveysparrow.app.mjs @@ -9,10 +9,13 @@ export default { type: "string", label: "Survey", description: "Identifier of a survey", - async options({ page }) { + async options({ + page, surveyType, + }) { const { data: surveys } = await this.listSurveys({ params: { page: page + 1, + survey_type: surveyType, }, }); return surveys?.map(({ @@ -54,6 +57,25 @@ export default { description: "Welcome Text of the survey", optional: true, }, + themeId: { + type: "string", + label: "Theme Id", + description: "Id of the email theme.", + async options({ page }) { + const { data } = await this.listThemes({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, }, methods: { _baseUrl() { @@ -102,6 +124,12 @@ export default { ...args, }); }, + listThemes(args = {}) { + return this._makeRequest({ + path: "/email_themes", + ...args, + }); + }, listResponses(args = {}) { return this._makeRequest({ path: "/responses", @@ -131,5 +159,53 @@ export default { ...args, }); }, + sendEmailShareTemplate({ + savedEmailTemplateName, + recipientEmailAddress, + }) { + return this._makeRequest({ + path: "/share/email", + method: "POST", + data: { + template_name: savedEmailTemplateName, + recipient_email: recipientEmailAddress, + }, + }); + }, + createChannel(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/channels", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const { + data, + has_next_page: hasNext, + } = await fn({ + params, + ...opts, + }); + for (const d of data) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = hasNext; + + } while (hasMore); + }, }, }; diff --git a/components/survicate/README.md b/components/survicate/README.md new file mode 100644 index 0000000000000..7b06667d8d863 --- /dev/null +++ b/components/survicate/README.md @@ -0,0 +1,11 @@ +# Overview + +The Survicate API empowers you to seamlessly integrate survey data into your systems, automate actions based on feedback, and sync responses across your tech stack. With Pipedream, you can create serverless workflows that leverage this API to respond in real-time to survey submissions, aggregate data for analysis, or trigger personalized marketing campaigns. Pipedream's no-code platform simplifies the process of connecting Survicate with other apps like Slack, Google Sheets, or CRMs such as Salesforce, to streamline data flows and enhance customer insights. + +# Example Use Cases + +- **Trigger Slack Notifications for New Survey Responses**: When a new survey response is received in Survicate, automatically send a notification to a designated Slack channel. This keeps your team informed and can prompt immediate follow-up if needed. + +- **Store Survey Responses in Google Sheets**: Save new Survicate survey responses to a Google Sheets spreadsheet. This allows for easy tracking and analysis of feedback over time, enabling data-driven decisions without manual data entry. + +- **Create or Update Contacts in CRM from Survey Responses**: Upon receiving a new survey response, check if the respondent exists in a CRM like Salesforce. If they do, update their record with the new insights; if they don't, create a new contact. This ensures your CRM reflects the latest customer feedback. diff --git a/components/survser/README.md b/components/survser/README.md new file mode 100644 index 0000000000000..16a8bc295ab0d --- /dev/null +++ b/components/survser/README.md @@ -0,0 +1,11 @@ +# Overview + +The Survser API is designed to streamline survey management and analysis, providing a set of tools to create, distribute, and analyze surveys. On Pipedream, you can automate survey distribution, sync responses with other data systems, and trigger actions based on survey outcomes. This enables seamless integration with other apps for advanced data processing, notifications, and more, enhancing decision-making processes and response times. + +# Example Use Cases + +- **Automated Survey Distribution Workflow**: Automatically send out surveys to new subscribers by integrating Survser with email marketing platforms like Mailchimp on Pipedream. When a new subscriber is added to Mailchimp, it triggers a workflow that sends a customized survey via Survser to gather initial feedback. + +- **Real-Time Survey Response Analysis and Notification**: Set up a real-time notification system on Pipedream that triggers whenever a new survey response is received in Survser. The response can be analyzed immediately, and if certain criteria are met, notifications can be sent through Slack or email to relevant stakeholders. This is particularly useful for time-sensitive feedback collection and action. + +- **Survey Data Integration with CRM Systems**: Automatically update CRM records in Salesforce or HubSpot when a survey is completed. This workflow can enrich customer profiles with new survey data, enabling more personalized marketing and sales strategies. The integration ensures that the CRM system reflects the latest customer insights without manual data entry. diff --git a/components/survser/package.json b/components/survser/package.json new file mode 100644 index 0000000000000..6ea28a68cbccb --- /dev/null +++ b/components/survser/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/survser", + "version": "0.1.0", + "description": "Pipedream Survser Components", + "main": "survser.app.mjs", + "keywords": [ + "pipedream", + "survser" + ], + "homepage": "https://pipedream.com/apps/survser", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/survser/sources/new-survey-response/new-survey-response.mjs b/components/survser/sources/new-survey-response/new-survey-response.mjs new file mode 100644 index 0000000000000..df05bd76e2c40 --- /dev/null +++ b/components/survser/sources/new-survey-response/new-survey-response.mjs @@ -0,0 +1,79 @@ +import survser from "../../survser.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "survser-new-survey-response", + name: "New Survey Response", + description: "Emit new event when a new survey response is received. [See the documentation](https://docs.survser.com/docs/api)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + survser, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + surveyId: { + propDefinition: [ + survser, + "surveyId", + ], + }, + }, + hooks: { + async deploy() { + await this.processEvents(25); + }, + }, + methods: { + _getLastCreated() { + return this.db.get("lastCreated"); + }, + _setLastCreated(lastCreated) { + this.db.set("lastCreated", lastCreated); + }, + emitEvent(response) { + const meta = this.generateMeta(response); + this.$emit(response, meta); + }, + generateMeta(response) { + return { + id: response.id, + summary: `New Response for Survey: ${response.survey.name}`, + ts: Date.parse(response.createdAt), + }; + }, + async processEvents(max) { + const lastCreated = this._getLastCreated(); + let responses = await this.survser.getSurveyResponses({ + params: { + surveyId: this.surveyId, + }, + }); + if (!responses?.length) { + return; + } + if (max) { + responses = responses.slice(-1 * max); + } + this._setLastCreated(Date.parse(responses[responses.length - 1].createdAt)); + for (const response of responses.reverse()) { + const ts = Date.parse(response.createdAt); + if (ts >= lastCreated) { + this.emitEvent(response); + } else { + return; + } + } + }, + }, + async run() { + await this.processEvents(); + }, + sampleEmit, +}; diff --git a/components/survser/sources/new-survey-response/test-event.mjs b/components/survser/sources/new-survey-response/test-event.mjs new file mode 100644 index 0000000000000..1181f3c23956e --- /dev/null +++ b/components/survser/sources/new-survey-response/test-event.mjs @@ -0,0 +1,26 @@ +export default { + "_id": "666341df4e9ca47f32a9bc5b", + "survey": { + "id": "666340d4a1db6e21ec16e036", + "name": "Customer Effort Survey" + }, + "responses": [ + { + "questionId": "4352cefc-30f8-4419-81bf-16161acea2d5", + "questionFormat": "csat", + "answer": { + "value": "3", + "text": "Very Easy" + } + } + ], + "pageUrl": "https://survser.com/", + "user": { + "name": "Anonymous" + }, + "anonIdentifier": "f1bb25af-69d8-4fa5-975e-b759a8a2fc49", + "complete": true, + "createdAt": "2024-06-07T17:22:39.818Z", + "updatedAt": "2024-06-07T17:22:40.719Z", + "id": "666341df4e9ca47f32a9bc5b" +} \ No newline at end of file diff --git a/components/survser/survser.app.mjs b/components/survser/survser.app.mjs new file mode 100644 index 0000000000000..851131aa0d8cb --- /dev/null +++ b/components/survser/survser.app.mjs @@ -0,0 +1,55 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "survser", + propDefinitions: { + surveyId: { + type: "string", + label: "Survey ID", + description: "Identifier of the survey to watch for responses", + async options() { + const surveys = await this.getSurveys(); + return surveys?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://www.survser.com/api/public"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + params: { + ...params, + key: `${this.$auth.api_key}`, + }, + }); + }, + getSurveys(opts = {}) { + return this._makeRequest({ + path: "/survey/list", + ...opts, + }); + }, + getSurveyResponses(opts = {}) { + return this._makeRequest({ + path: "/response/list", + ...opts, + }); + }, + }, +}; diff --git a/components/svix/README.md b/components/svix/README.md new file mode 100644 index 0000000000000..cb7fcd2206d21 --- /dev/null +++ b/components/svix/README.md @@ -0,0 +1,11 @@ +# Overview + +The Svix API enables developers to manage and automate webhooks with ease. By integrating with Pipedream, you can leverage serverless workflows to react to incoming webhooks, manage webhook endpoints, and send out messages to subscribed endpoints. Whether you're seeking to enhance your application's notifications system or streamline event-driven integrations, Svix's API, when combined with Pipedream's capabilities, provides a robust platform for automating and scaling your webhook infrastructure. + +# Example Use Cases + +- **Automate Webhook Event Logging**: Capture incoming webhook events from Svix and log them to a Google Sheets spreadsheet for easy monitoring and auditing. With Pipedream's built-in Google Sheets integration, setting up this workflow is straightforward and doesn't require managing servers. + +- **Dynamic Webhook Endpoint Management**: Use Pipedream to dynamically create, update, or delete webhook endpoints in Svix based on triggers from other apps, like a new user signup on Auth0. This workflow can help maintain an updated list of subscribers as your user base evolves. + +- **Multi-App Notification System**: Build a robust notification system that uses Svix to dispatch messages to multiple services like Slack, Email, or SMS whenever a specific event occurs in your application. Pipedream acts as the orchestrator, parsing the event data and triggering the Svix API to notify all relevant endpoints. diff --git a/components/swaggerhub/README.md b/components/swaggerhub/README.md new file mode 100644 index 0000000000000..1dde1b5910765 --- /dev/null +++ b/components/swaggerhub/README.md @@ -0,0 +1,11 @@ +# Overview + +The SwaggerHub API offers capabilities to streamline working with your Swagger (OpenAPI) definitions within SwaggerHub. On Pipedream, you can automate interactions with the SwaggerHub API to keep APIs in sync, manage your API versions, and integrate your API design workflow with other tools and services. Leverage Pipedream's serverless platform to create, update, and share your SwaggerHub API definitions across different environments or teams efficiently. + +# Example Use Cases + +- **Sync SwaggerHub Definitions with GitHub Repositories**: Automatically push updates from SwaggerHub API definitions to a specified GitHub repository whenever a new version is saved in SwaggerHub. This ensures that your API specs and your codebase are in sync without manual intervention. + +- **Notify Team on Slack for New API Versions**: Send a notification to a Slack channel when a new version of an API is published in SwaggerHub. This keeps your team informed about the latest API changes and promotes quick feedback and collaboration. + +- **Backup API Definitions to Amazon S3**: Create a backup of your API definitions by automatically uploading them to an Amazon S3 bucket whenever changes are made. This serves as an additional layer of security for your API specs and allows you to maintain version history in a robust storage service. diff --git a/components/swagup/README.md b/components/swagup/README.md index ea5c0a9ffe237..ec153e48f7a2b 100644 --- a/components/swagup/README.md +++ b/components/swagup/README.md @@ -1,29 +1,11 @@ # Overview -SwagUp API is a powerful tool that puts the power of automated product -fulfillment in anyone’s hands. With the SwagUp automated product fulfillment -service, you can build custom features for your platform, mobile app, website, -or product in order to deliver physical goods quickly and easily. Here are some -of the things you can create using the SwagUp API: +The SwagUp API allows you to automate the creation and distribution of swag packs. You can programmatically design swag items, assemble them into packs, and manage orders and inventory. Integrating SwagUp with Pipedream opens up endless possibilities for sending personalized swag to customers, employees, or event attendees efficiently, aligning this delightful process with various triggers from other apps and services. -- Manage Fulfillment Experiences: Develop custom, on-demand product fulfillment - experiences that are tailored to your customer’s needs. -- Create Brand Templates: Design and create personalizable templates for your - physical products that can be used to customize product packaging or design - display pieces. -- Integrate with Platforms: Integrate SwagUp with existing platforms to enable - unified and seamless product fulfillment. -- Build Custom websites: Build custom websites with SwagUp’s product - fulfillment system, allowing you to offer an engaging customer experience. -- Develop Custom Apps: Develop custom apps for product fulfillment that allow - personalized order processing with tailored features. -- Automate Product Supply Chains: Automate and manage the entire product supply - chain from product delivery to invoicing with the SwagUp API. -- Connect Third-Party APIs: Connect third-party APIs to the SwagUp platform for - seamless order processing and product delivery. +# Example Use Cases -With the SwagUp API, you can create a multitude of automated product -fulfillment experiences that are tailored directly to customer needs. With this -powerful platform, you can offer unique product experiences and boost customer -engagement, while simultaneously streamlining order processing, invoicing and -product delivery. +- **Customer Milestone Rewards**: Trigger a SwagUp pack order when a user reaches a new level in your service's reward program. Connect SwagUp to a CRM like Salesforce, so once a user achieves the milestone, Salesforce sends an event to Pipedream, which then calls SwagUp to dispatch the reward. + +- **Welcome Kits for New Employees**: Onboard new hires with a custom swag pack. Set up a workflow where an HR platform like BambooHR, upon adding a new employee, triggers a SwagUp API request through Pipedream to send out a branded welcome kit, making the new employee feel part of the team from day one. + +- **Event Attendee Thank-Yous**: After an event concludes, send attendees a thank-you pack. Use an event management platform like Eventbrite to track registrations and attendance. When an event ends, Pipedream listens for the trigger, and then the SwagUp API is called to send swag to participants as a token of appreciation, enhancing post-event engagement. diff --git a/components/swapcard_exhibitor/common/constants.mjs b/components/swapcard_exhibitor/common/constants.mjs new file mode 100644 index 0000000000000..ea830c15a04cb --- /dev/null +++ b/components/swapcard_exhibitor/common/constants.mjs @@ -0,0 +1 @@ +export const LIMIT = 100; diff --git a/components/swapcard_exhibitor/common/queries.mjs b/components/swapcard_exhibitor/common/queries.mjs new file mode 100644 index 0000000000000..424ae4e0d7a73 --- /dev/null +++ b/components/swapcard_exhibitor/common/queries.mjs @@ -0,0 +1,68 @@ +export const queries = { + events: ` + query Events ($page: Int!, $pageSize: Int!){ + events(page: $page, pageSize: $pageSize) { + id + title + } + } + `, + eventPerson: ` + query EventPerson( + $eventId: ID!, + $filters: [EventPersonFilter!], + $sort: [EventPersonSort!], + $cursor: CursorPaginationInput + ) { + eventPerson( + eventId: $eventId, + filters: $filters, + sort: $sort, + cursor: $cursor + ) { + nodes { + id + userId + email + firstName + lastName + jobTitle + secondJobTitle + photoUrl + organization + websiteUrl + biography + tags + isVisible + source + createdAt + updatedAt + communityProfileUpdatedAt + type + engagementScore + clientIds + registration { + id + paymentStatus + status + confirmationCode + registeredAt + canceledAt + checkIn + checkInSource + } + } + pageInfo { + hasPrevPage + currentPage + lastPage + totalItems + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } + `, +}; diff --git a/components/swapcard_exhibitor/package.json b/components/swapcard_exhibitor/package.json new file mode 100644 index 0000000000000..5cdb43be2aad7 --- /dev/null +++ b/components/swapcard_exhibitor/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/swapcard_exhibitor", + "version": "0.1.0", + "description": "Pipedream Swapcard Exhibitor Components", + "main": "swapcard_exhibitor.app.mjs", + "keywords": [ + "pipedream", + "swapcard_exhibitor" + ], + "homepage": "https://pipedream.com/apps/swapcard_exhibitor", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} + diff --git a/components/swapcard_exhibitor/sources/common/base.mjs b/components/swapcard_exhibitor/sources/common/base.mjs new file mode 100644 index 0000000000000..5a3ab1c4d6974 --- /dev/null +++ b/components/swapcard_exhibitor/sources/common/base.mjs @@ -0,0 +1,69 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import swapcardExhibitor from "../../swapcard_exhibitor.app.mjs"; + +export default { + props: { + swapcardExhibitor, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + eventId: { + propDefinition: [ + swapcardExhibitor, + "eventId", + ], + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01T00:00:00Z"; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + + const response = this.swapcardExhibitor.paginate({ + fn: this.getFunction(), + maxResults, + eventId: this.eventId, + type: this.getType(), + filters: this.getFilters(lastDate), + sort: { + field: "REGISTERED_AT", + order: "DESC", + }, + }); + + let responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + if (responseArray.length) { + this._setLastDate(this.getDate(responseArray[0])); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(this.getDate(item)), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/swapcard_exhibitor/sources/new-connection/new-connection.mjs b/components/swapcard_exhibitor/sources/new-connection/new-connection.mjs new file mode 100644 index 0000000000000..da33f79377b01 --- /dev/null +++ b/components/swapcard_exhibitor/sources/new-connection/new-connection.mjs @@ -0,0 +1,36 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "swapcard_exhibitor-new-connection", + name: "New Connection Formed", + description: "Emit new event when a new connection is formed (new lead).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.swapcardExhibitor.getEventPeople; + }, + getDate(item) { + return item.registration.registeredAt; + }, + getFilters(lastDate) { + return [ + { + lastUpdatedSince: lastDate, + sources: "REGISTRATION", + }, + ]; + }, + getType() { + return "eventPerson"; + }, + getSummary(lead) { + return `New connection: ${lead.firstName} ${lead.lastName}`; + }, + }, + sampleEmit, +}; diff --git a/components/swapcard_exhibitor/sources/new-connection/test-event.mjs b/components/swapcard_exhibitor/sources/new-connection/test-event.mjs new file mode 100644 index 0000000000000..e18be5a64868d --- /dev/null +++ b/components/swapcard_exhibitor/sources/new-connection/test-event.mjs @@ -0,0 +1,32 @@ +export default { + "id": "RXZlbnzI0OTkyMTQ=", + "userId": "RXZlbnzI0OTkyMTQ==", + "email": "lead@email.com", + "firstName": "First Name", + "lastName": "Last Name", + "jobTitle": null, + "secondJobTitle": null, + "photoUrl": null, + "organization": null, + "websiteUrl": null, + "biography": null, + "tags": [], + "isVisible": true, + "source": "REGISTRATION", + "createdAt": "2010-08-26 19:06:20", + "updatedAt": "2010-08-26 19:06:21", + "communityProfileUpdatedAt": "2010-08-26 19:06:20", + "type": null, + "engagementScore": -1, + "clientIds": [], + "registration": { + "id": "RXZlbnzI0OTkyMTQ==", + "paymentStatus": "PAID", + "status": "REGISTERED", + "confirmationCode": "RYB9", + "registeredAt": "2010-08-26 19:06:20", + "canceledAt": null, + "checkIn": null, + "checkInSource": null + } +} \ No newline at end of file diff --git a/components/swapcard_exhibitor/swapcard_exhibitor.app.mjs b/components/swapcard_exhibitor/swapcard_exhibitor.app.mjs new file mode 100644 index 0000000000000..58d56863a1357 --- /dev/null +++ b/components/swapcard_exhibitor/swapcard_exhibitor.app.mjs @@ -0,0 +1,99 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; +import { queries } from "./common/queries.mjs"; + +export default { + type: "app", + app: "swapcard_exhibitor", + propDefinitions: { + eventId: { + type: "string", + label: "Event ID", + description: "The ID of the event to monitor for new connections (leads).", + async options({ page }) { + const { data: { events } } = await this.listEvents({ + page: page + 1, + pageSize: LIMIT, + }); + + return events.map(({ + id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://developer.swapcard.com/event-admin/graphql"; + }, + _headers() { + return { + "Content-Type": "application/json", + "Authorization": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, ...opts + }) { + return axios($, { + method: "POST", + url: this._baseUrl(), + headers: this._headers(), + ...opts, + }); + }, + listEvents(variables) { + return this._makeRequest({ + data: { + query: queries.events, + variables, + }, + }); + }, + async getEventPeople(variables) { + return this._makeRequest({ + data: { + query: queries.eventPerson, + variables, + }, + }); + }, + async *paginate({ + fn, maxResults = null, type, ...variables + }) { + let hasMore = false; + let count = 0; + let cursor; + + do { + variables.cursor = { + first: LIMIT, + after: cursor, + }; + const { + data: { + [type]: { + nodes, pageInfo, + }, + }, + } = await fn( + variables, + ); + + for (const node of nodes) { + yield node; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + cursor = pageInfo.endCursor; + + } while (hasMore); + }, + }, +}; diff --git a/components/swapi/README.md b/components/swapi/README.md index 3bebef13e2b07..3b48d8c453af4 100644 --- a/components/swapi/README.md +++ b/components/swapi/README.md @@ -1,21 +1,11 @@ # Overview -The SWAPI (Star Wars API) provides a wealth of data about the Star Wars -universe to build applications. With the SWAPI, developers can access detailed -Star Wars related data about people, planets, films, species and much more. -From web and mobile applications to data analysis, the possibilities are -limitless. +The SWAPI - Star Wars API is a treasure trove of structured data from the Star Wars universe. It's a go-to resource for fetching information about planets, spaceships, vehicles, people, films, and species from the iconic franchise. Using Pipedream, you can harness this data to create automations and workflows that trigger based on specific criteria from SWAPI. For instance, you could set up a workflow that notifies you when new data is added, enrich customer profiles with their favorite Star Wars characters, or even use it for trivia games by pulling random facts. -Here are a few examples of what developers can build using the SWAPI: +# Example Use Cases -- Create a Star Wars encyclopedia to document the Star Wars universe and its - characters. -- Design a custom interactive map to explore the planets of the Star Wars - universe. -- Analyze the data to uncover patterns and insights about Star Wars films, - characters, and species. -- Design a search engine to query Star Wars related data. -- Create a mobile app that displays information about the characters, films and - species in the Star Wars universe. -- Design a live score system specialized in providing stats related to the Star - Wars films and characters. +- **Daily Star Wars Trivia Email**: Create a workflow that pulls a random Star Wars fact from SWAPI each day, and use the SendGrid app on Pipedream to email it to a subscriber list. Fans get a daily dose of trivia, keeping them engaged and entertained. + +- **Slack Bot for Character Lookup**: Build a Slack bot using Pipedream that responds to commands in a channel. When someone types '/get-character Luke Skywalker', the workflow fetches the character data from SWAPI and posts it directly in the Slack conversation. + +- **New Film Notification Service**: Design an automation that monitors SWAPI for new additions to the 'films' endpoint. When a new film is detected, Pipedream can trigger a workflow that sends a push notification via the Twilio app to users who've signed up for alerts, keeping them in the loop about the latest Star Wars releases. diff --git a/components/swell/README.md b/components/swell/README.md new file mode 100644 index 0000000000000..88da0be9b0e8d --- /dev/null +++ b/components/swell/README.md @@ -0,0 +1,11 @@ +# Overview + +The Swell API is like a Swiss Army knife for e-commerce platforms, giving you the power to twist, shape, and scale your online store. Within Pipedream's serverless environment, you can automate actions like syncing new orders to your CRM, updating inventory in real-time, or even crafting personalized marketing campaigns based on customer behavior. With triggers, actions, and the ability to combine Swell with a myriad of other apps, Pipedream turns the Swell API into a potent tool for e-commerce automation. + +# Example Use Cases + +- **Order to CRM Sync**: Automatically push new Swell orders to Salesforce or HubSpot. When a new order is placed, the workflow triggers and creates a new contact or deal in your CRM, ensuring your sales team has the freshest data. + +- **Inventory Update Notifications**: Set up a workflow that monitors inventory levels. When stock for a popular product dips below a certain threshold, it triggers an alert via Slack or email to prompt a restock, keeping your store optimized. + +- **Personalized Email Campaigns**: Combine Swell with SendGrid to send out personalized email campaigns based on customer purchase history. Trigger an email sequence when a customer buys a specific product, creating opportunities for upselling and enhanced engagement. diff --git a/components/swell/package.json b/components/swell/package.json index 80b0672adcebe..51f4fc82d16db 100644 --- a/components/swell/package.json +++ b/components/swell/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/swiftype/README.md b/components/swiftype/README.md new file mode 100644 index 0000000000000..41feb4ffdbecf --- /dev/null +++ b/components/swiftype/README.md @@ -0,0 +1,11 @@ +# Overview + +The Swiftype API offers a potent toolset for adding search functionality to your websites and applications, allowing you to index and search your content. With Pipedream, you can build serverless workflows integrating Swiftype API to automate indexing, manage search engines, and react to search analytics. This can be a game-changer for content-driven sites, providing insights into user search behavior and automating updates to the search index. + +# Example Use Cases + +- **Automated Content Indexing**: When new content is posted on your CMS (like WordPress), you could use Pipedream to trigger a workflow that automatically adds this new content to your Swiftype search index. This ensures that your search function always provides the most up-to-date results without manual intervention. + +- **Search Analytics to Slack**: Set up a workflow where Pipedream fetches search analytics from Swiftype, such as top search queries or no-result queries, and send this data to a Slack channel. This can help you quickly gather insights and make informed decisions about content strategy or search optimization. + +- **Sync Search Data with Google Sheets**: Use Pipedream to create a workflow that captures user search queries via Swiftype API and logs them in a Google Sheet. This can be valuable for analyzing search trends over time or sharing search data across your team without giving direct access to Swiftype's dashboard. diff --git a/components/switch/package.json b/components/switch/package.json new file mode 100644 index 0000000000000..28b77532b08ee --- /dev/null +++ b/components/switch/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/switch", + "version": "0.0.1", + "description": "Pipedream Switch Components", + "main": "switch.app.mjs", + "keywords": [ + "pipedream", + "switch" + ], + "homepage": "https://pipedream.com/apps/switch", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/switch/switch.app.mjs b/components/switch/switch.app.mjs new file mode 100644 index 0000000000000..82aa23bb59005 --- /dev/null +++ b/components/switch/switch.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "switch", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/switchboard/README.md b/components/switchboard/README.md index 3f3b7627d0d65..84f11ba5c4bfa 100644 --- a/components/switchboard/README.md +++ b/components/switchboard/README.md @@ -1,24 +1,11 @@ # Overview -The Switchboard API is a framework that enables developers to build machine -learning capabilities into their applications. It allows developers to quickly -create algorithms and models that can be used to predict user behavior, analyze -sentiment, and understand natural language processing. The Switchboard API is -easy to use and gives developers full control over the underlying models, so -that they can customize their applications to fit their specific product -requirements. +The Switchboard API offers a platform to manage multi-platform data pipelines, enabling seamless data synchronization and transformation across diverse systems. With Pipedream, you can leverage the Switchboard API to create serverless workflows that automate data operations, reducing manual overhead and creating real-time, event-driven processes. These workflows can integrate with numerous apps available on Pipedream, making it possible to connect data sources, handle data in transit, and trigger actions based on custom logic. -With the Switchbot API, developers can build powerful applications that can -dramatically improve user experiences. Here are some of the things that you can -do with the Switchboard API: +# Example Use Cases -- Generating personalized recommendations based on user browsing behavior. -- Automatically classifying customer feedback and complaints. -- Analyzing customer sentiment to understand customer opinion on various - topics. -- Automatically detecting and responding to malicious activity. -- Automatically recognizing faces in photographs. -- Understanding natural language processing and responding to customer - inquiries. -- Predicting customer churn and proactively responding to potential customer - losses. +- **Real-Time Data Aggregation and Reporting**: Automate the collection of data from multiple sources, like sales metrics from CRMs and marketing stats from analytics tools. Use the Switchboard API to process and unify this data, then send it to Google Sheets or a BI tool like Tableau on Pipedream for real-time reporting and visualization. + +- **Social Media Content Syndication**: Amplify your online presence by automatically publishing content across multiple social media platforms. When a new blog post is published, use the Switchboard API to distribute the content to social networks like Twitter, LinkedIn, and Facebook via Pipedream, keeping your audience engaged across all channels. + +- **Cross-Platform Customer Support**: Enhance customer support by integrating ticketing platforms like Zendesk with communication apps such as Slack. With the Switchboard API on Pipedream, when a new support ticket is created, automate notifications to the relevant support team's Slack channel and sync follow-up actions between systems to streamline support workflows. diff --git a/components/symbl_ai/README.md b/components/symbl_ai/README.md index 746aa33df05d7..ff37f5ed4cd93 100644 --- a/components/symbl_ai/README.md +++ b/components/symbl_ai/README.md @@ -1,22 +1,11 @@ # Overview -Symbl.ai is a leading AI-powered platform that enables enterprises to quickly -build and launch AI-driven applications. The Symbl.ai API offers powerful tools -for developers and data scientists to create AI-powered voice and text-based -experiences to meet various use cases. With the Symbl.ai API, developers can -quickly and easily build applications that can provide insights and support for -various activities such as customer support, business meetings, customer -engagement, etc. +The Symbl.ai API supercharges your applications with advanced conversation intelligence. By tapping into Symbl.ai via Pipedream, you can automate the analysis of voice, text, and video communications. Extract actionable items, follow-ups, and insights in real-time or from recorded content. This API's magic lies in its ability to provide contextually relevant insights, topic detection, sentiment analysis, and conversation metrics without extensive training data or setup time. -Here are a few examples of what you can build using the Symbl.ai API: +# Example Use Cases -- Speech recognition and transcription for customer service calls -- Automation of customer onboarding processes -- AI-powered dialog management for customer engagement -- Business intelligence gathering from audio/video calls -- Virtual assistant for automated meetings -- Natural language processing for sentiment analysis -- Text-based customer service bots -- Handwritten text recognition -- Automatic summarization of voice calls -- AI- Guided customer support processes +- **Customer Support Call Summarization**: Automate the summarization of customer support calls by connecting Symbl.ai to a voice recording source on Pipedream. After a call ends, the recording gets processed, and the API provides a summary, action items, and questions. The results could be sent to a CRM like Salesforce or a project management tool like Trello to track follow-up tasks. + +- **Meeting Insight Capture**: With Symbl.ai integrated into your video conferencing tools via Pipedream, automatically capture key points from your online meetings. Post-meeting, Symbl.ai can generate insights and topics discussed, which can be pushed to a Slack channel or an email summary to ensure all participants are aligned on outcomes and next steps. + +- **Real-time Compliance Monitoring**: For industries with strict compliance requirements, use Symbl.ai to flag specific language or sentiment in real-time during calls. This workflow can be set up on Pipedream to trigger alerts or log incidents in systems like Zendesk or JIRA, ensuring immediate action and maintaining compliance standards. diff --git a/components/sympla/README.md b/components/sympla/README.md index 762eb388bb83b..bb65eb0fec894 100644 --- a/components/sympla/README.md +++ b/components/sympla/README.md @@ -1,20 +1,11 @@ # Overview -The Sympla API gives developers the power to bring Sympla's leading event and -ticketing platform to existing applications and websites. With the Sympla API, -developers can access features that make it easy to plan, promote, and manage -events of all sizes. +The Sympla API allows for the seamless integration of event management capabilities into various applications or services. With this API, you can automate tasks like attendee registration, event creation, ticket sales tracking, and participant communication. It's particularly useful for event organizers looking to streamline operations and enhance the attendee experience through automation. -Some examples of what can be created using the Sympla API include: +# Example Use Cases -- Event registration forms -- Custom ticket types -- Event management tools -- Payment gateway integrations -- Promotional materials such as galleries, videos and slideshows -- Webhooks for event notifications and updates -- Automated emails for ticket delivery and reminders -- Widgets and plugins for embedding Sympla tickets in websites, apps and social - media -- Accurate analytics to help understand ticket sales and attendance trends -- Integration with ticket outlets and third party applications +- **Automated Attendee Follow-up**: After an event registration on Sympla, use Pipedream to trigger a personalized email sequence or SMS notifications to the attendee via SendGrid or Twilio, providing them with details, updates, or resources related to the event. + +- **Real-time Sales Dashboard**: Connect Sympla to Google Sheets or Airtable on Pipedream to create a live dashboard that updates in real-time as tickets are sold. Use this workflow to monitor sales data, generate reports, and gain actionable insights on event performance. + +- **Event Engagement Analytics**: Tie in Sympla's attendee data with a tool like Segment or Google Analytics on Pipedream. Keep track of attendee engagement by monitoring their interactions through various touchpoints pre, during, and post-event to enhance future event planning and marketing strategies. diff --git a/components/syncro/README.md b/components/syncro/README.md index 7bdf46b4ac3e7..51954e5a20454 100644 --- a/components/syncro/README.md +++ b/components/syncro/README.md @@ -1,32 +1,11 @@ # Overview -The Syncro API offers a comprehensive suite of tools to help you build a -variety of powerful applications that can make your business more efficient and -organized. With the Syncro API, you can create applications that help you -manage customer data, create custom invoices and manage inventory, create -workflows and work orders, and much more. Here are some of the ways you can use -the Syncro API to build applications: +Syncro is an API that empowers users to streamline their customer support and feedback management by automating interactions and integrating with various systems. By leveraging Syncro with Pipedream, you can automate tasks such as syncing customer feedback to a CRM, triggering alerts based on feedback scores, or creating support tickets from customer responses. Pipedream’s serverless platform facilitates the creation of these automated workflows without the need for complex infrastructure, making it straightforward to connect Syncro with other apps and services to enhance your customer support operations. -- Create customer accounts and manage customer data: You can build applications - to store, track and manage customer data including invoices, subscriptions, - payments and profile information. -- Create custom invoices and manage inventory: With Syncro, you can build - applications to automatically generate invoices, track inventory, and manage - the supply chain process. -- Create workflows and work orders: Whether you’re managing a large team or - managing complex projects, you can use Syncro to create and manage workflows - and work orders for each task. -- Create reports: Syncro gives you the tools you need to create custom reports. - This allows you to gain insights into your business, see which areas are most - successful and make changes accordingly. -- Automate tasks: You can create applications that automate tasks related to - customer service, billing and other areas. This can help you streamline - processes and cut down on manual labor. -- Create integrations: Syncro allows you to connect with dozens of different - applications and services, making it easy to integrate with your current - systems and services. -- Create custom email templates: Create custom email templates to automate - customer onboarding, order processing and more. -- Create notifications and alerts: You can use Syncro to set up notifications - and alerts when a certain task has been completed or a deadline is - approaching. +# Example Use Cases + +- **Automated Feedback Collection to CRM**: Trigger a workflow in Pipedream whenever a new feedback entry is submitted in Syncro. Use this trigger to create or update a customer record in a CRM like Salesforce, ensuring customer feedback is directly linked to the customer profile for personalized follow-up and service. + +- **Support Ticket Creation from Negative Feedback**: Monitor feedback scores in Syncro and set a threshold for negative feedback. When a score falls below this threshold, automatically create a support ticket in a tool such as Zendesk or JIRA, ensuring prompt attention to dissatisfied customers and the opportunity to address their concerns quickly. + +- **Real-Time Alerts for High-Priority Feedback**: Set up a workflow that sends real-time notifications through Slack, SMS, or email whenever a feedback response indicates urgent attention is needed, such as a critical product issue or a potential churn risk. This allows teams to react swiftly and prioritize issues that could impact customer satisfaction or business operations. diff --git a/components/synthflow/package.json b/components/synthflow/package.json new file mode 100644 index 0000000000000..13f86d16f65ad --- /dev/null +++ b/components/synthflow/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/synthflow", + "version": "0.0.1", + "description": "Pipedream Synthflow Components", + "main": "synthflow.app.mjs", + "keywords": [ + "pipedream", + "synthflow" + ], + "homepage": "https://pipedream.com/apps/synthflow", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/synthflow/synthflow.app.mjs b/components/synthflow/synthflow.app.mjs new file mode 100644 index 0000000000000..d3432b63c182f --- /dev/null +++ b/components/synthflow/synthflow.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "synthflow", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/t2m_url_shortener/README.md b/components/t2m_url_shortener/README.md new file mode 100644 index 0000000000000..61cf0a5a3edd5 --- /dev/null +++ b/components/t2m_url_shortener/README.md @@ -0,0 +1,11 @@ +# Overview + +The T2M URL Shortener API lets you shorten URLs, manage them, and track analytics within Pipedream's automated workflows. By integrating with Pipedream, you can create shortened URLs on the fly, update them, and get click data without manual intervention. This capability is especially useful for marketing, content distribution, and social media management, where streamlined URL management can be seamlessly embedded into broader automated tasks. + +# Example Use Cases + +- **Automate Social Media Posts with Shortened URLs**: Create a workflow that triggers when you have a new blog post. Use the T2M URL Shortener API to shorten the post URL and then automatically publish a post to social platforms like Twitter or Facebook using their respective Pipedream integrations. + +- **Track Marketing Campaign Performance**: Whenever you create a new marketing campaign, use T2M to generate a unique shortened URL and send it via email or SMS through integrations like SendGrid or Twilio. Track clicks and engagement through the T2M dashboard, feeding data back into Pipedream for analysis and reporting. + +- **Dynamic QR Code Generation**: Generate a shortened URL for event tickets or promotional materials, and then use the T2M URL linked with a QR code generator app within Pipedream to create QR codes. This can be distributed digitally or printed for physical marketing materials, and the usage can be tracked for insights. diff --git a/components/tableau/README.md b/components/tableau/README.md new file mode 100644 index 0000000000000..282bb97ebed62 --- /dev/null +++ b/components/tableau/README.md @@ -0,0 +1,17 @@ +# Overview + +The Tableau API allows you to tap into the robust data visualization and business intelligence capabilities of Tableau. Within Pipedream, you can leverage this API to automate reporting, manage users, update data sources, and extract insights. This enables you to integrate Tableau's analytics with other services, streamlining your data workflows and ensuring your dashboards remain up-to-date with minimal manual effort. + +# Example Use Cases + +- **Automated Snapshot Sharing**: Generate snapshots of your key Tableau dashboards and share them via email or Slack at regular intervals. This keeps your team informed with the latest business insights without manual exports. + + _Example Workflow_: Trigger a Pipedream workflow on a schedule; use Tableau API to capture a view of the dashboard; send the image via Gmail or post to a Slack channel using their respective Pipedream app integrations. + +- **Dynamic Data Updates**: Automatically update Tableau data sources when new data comes into your backend systems, ensuring that Tableau dashboards reflect the most current data. + + _Example Workflow_: Trigger a Pipedream workflow with a webhook when new data is added to a database; process and format the data within the workflow; use Tableau API to refresh the corresponding data source on Tableau Server or Tableau Online. + +- **User Management Automation**: Streamline user provisioning by automating the addition and removal of users to and from Tableau sites based on HR software triggers or internal databases. + + _Example Workflow_: Trigger a Pipedream workflow from a user management event in an app like BambooHR; use conditions within the workflow to determine if the user should be added or removed; utilize Tableau API to update the user list on the relevant Tableau site. diff --git a/components/taggun/README.md b/components/taggun/README.md new file mode 100644 index 0000000000000..231c02a616c3c --- /dev/null +++ b/components/taggun/README.md @@ -0,0 +1,11 @@ +# Overview + +Taggun API offers a powerful way to extract meaningful data from receipts and invoices using machine learning. By submitting images or PDFs, it can pull out key details like the date, merchant info, totals, tax amounts, and line items. This capability is gold for automating expense tracking and financial analysis. In Pipedream, you can slice Taggun's prowess into your workflows to parse receipts on the fly, integrate with accounting software, or even manage inventory based on purchase data. + +# Example Use Cases + +- **Automated Expense Reporting**: Use Taggun to scan receipts, extract payment details, and then automatically feed this data into a Google Sheets spreadsheet. This can help streamline the process of tracking business expenses or preparing financial reports. + +- **E-commerce Purchase Validation**: When customers submit proof of purchase, Taggun can validate the receipt details. Combine this with a Shopify hook in Pipedream to confirm orders, automate loyalty points allocation, or manage product warranties. + +- **Inventory Management System Update**: After receiving invoices from vendors, use Taggun to extract product and pricing information. Then, with a simple integration, update your inventory management system, like Airtable, to reflect current stock levels and costs. diff --git a/components/taggun/package.json b/components/taggun/package.json new file mode 100644 index 0000000000000..1be2d23a08b96 --- /dev/null +++ b/components/taggun/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/taggun", + "version": "0.0.1", + "description": "Pipedream Taggun Components", + "main": "taggun.app.mjs", + "keywords": [ + "pipedream", + "taggun" + ], + "homepage": "https://pipedream.com/apps/taggun", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/taggun/taggun.app.mjs b/components/taggun/taggun.app.mjs new file mode 100644 index 0000000000000..74fb1a3da2669 --- /dev/null +++ b/components/taggun/taggun.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "taggun", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/taleez/actions/add-candidate-to-job/add-candidate-to-job.mjs b/components/taleez/actions/add-candidate-to-job/add-candidate-to-job.mjs new file mode 100644 index 0000000000000..26acaba9a6892 --- /dev/null +++ b/components/taleez/actions/add-candidate-to-job/add-candidate-to-job.mjs @@ -0,0 +1,37 @@ +import taleez from "../../taleez.app.mjs"; + +export default { + key: "taleez-add-candidate-to-job", + name: "Add Candidate to Job", + description: "Links an existing candidate to a job offer. [See the documentation](https://api.taleez.com/swagger-ui/index.html#/jobs/addCandidate_1)", + version: "0.0.1", + type: "action", + props: { + taleez, + candidateId: { + propDefinition: [ + taleez, + "candidateId", + ], + }, + jobId: { + propDefinition: [ + taleez, + "jobId", + ], + }, + }, + async run({ $ }) { + const response = await this.taleez.linkCandidateToJob({ + $, + jobId: this.jobId, + data: { + ids: [ + this.candidateId, + ], + }, + }); + $.export("$summary", `Linked candidate ${this.candidateId} to job ${this.jobId} successfully`); + return response; + }, +}; diff --git a/components/taleez/actions/create-candidate/create-candidate.mjs b/components/taleez/actions/create-candidate/create-candidate.mjs new file mode 100644 index 0000000000000..3ba20b075a2cd --- /dev/null +++ b/components/taleez/actions/create-candidate/create-candidate.mjs @@ -0,0 +1,60 @@ +import taleez from "../../taleez.app.mjs"; + +export default { + key: "taleez-create-candidate", + name: "Create Candidate", + description: "Creates a new candidate in Taleez. [See the documentation](https://api.taleez.com/swagger-ui/index.html#/candidates/create_1)", + version: "0.0.1", + type: "action", + props: { + taleez, + firstName: { + type: "string", + label: "First Name", + description: "First name of the candidate", + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the candidate", + }, + email: { + type: "string", + label: "Email", + description: "Candidate email address. Must be unique in your company", + }, + phone: { + type: "string", + label: "Phone", + description: "Candidate phone (formats : 0611223344, +33611223344, 00336112233). Ignored if not valid.", + optional: true, + }, + unitId: { + propDefinition: [ + taleez, + "unitId", + ], + }, + recruiterId: { + propDefinition: [ + taleez, + "recruiterId", + ], + }, + }, + async run({ $ }) { + const response = await this.taleez.createCandidate({ + $, + data: { + firstName: this.firstName, + lastName: this.lastName, + mail: this.email, + phone: this.phone, + unitId: this.unitId, + recruiterId: this.recruiterId, + }, + }); + $.export("$summary", `Created candidate ${this.firstName} ${this.lastName} successfully`); + return response; + }, +}; diff --git a/components/taleez/actions/list-jobs/list-jobs.mjs b/components/taleez/actions/list-jobs/list-jobs.mjs new file mode 100644 index 0000000000000..5f4225d9ac017 --- /dev/null +++ b/components/taleez/actions/list-jobs/list-jobs.mjs @@ -0,0 +1,74 @@ +import taleez from "../../taleez.app.mjs"; + +export default { + key: "taleez-list-jobs", + name: "List Jobs", + description: "Retrieves a list of jobs in your company. [See the documentation](https://api.taleez.com/swagger-ui/index.html#/jobs/list_3)", + version: "0.0.1", + type: "action", + props: { + taleez, + unitId: { + propDefinition: [ + taleez, + "unitId", + ], + }, + status: { + propDefinition: [ + taleez, + "status", + ], + }, + contract: { + propDefinition: [ + taleez, + "contract", + ], + }, + city: { + propDefinition: [ + taleez, + "city", + ], + }, + companyLabel: { + propDefinition: [ + taleez, + "companyLabel", + ], + }, + tag: { + propDefinition: [ + taleez, + "tag", + ], + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "The maximum number of jobs to retrieve. Default: `100`", + optional: true, + }, + }, + async run({ $ }) { + const { list: jobs } = await this.taleez.listJobs({ + $, + params: { + unitId: this.unitId, + status: this.status, + contract: this.contract, + city: this.city, + companyLabel: this.companyLabel, + tag: this.tag, + pageSize: this.maxResults, + withDetails: true, + withProps: true, + }, + }); + $.export("$summary", `Successfully retrieved ${jobs?.length} job${jobs?.length === 1 + ? "" + : "s"}`); + return jobs; + }, +}; diff --git a/components/taleez/package.json b/components/taleez/package.json new file mode 100644 index 0000000000000..23c7cf61d9646 --- /dev/null +++ b/components/taleez/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/taleez", + "version": "0.1.0", + "description": "Pipedream Taleez Components", + "main": "taleez.app.mjs", + "keywords": [ + "pipedream", + "taleez" + ], + "homepage": "https://pipedream.com/apps/taleez", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/taleez/sources/common/base.mjs b/components/taleez/sources/common/base.mjs new file mode 100644 index 0000000000000..18e388b92402e --- /dev/null +++ b/components/taleez/sources/common/base.mjs @@ -0,0 +1,72 @@ +import taleez from "../../taleez.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + taleez, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + emitEvent(item) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + const tsField = this.getTsField(); + + const results = this.taleez.paginate({ + fn: this.getResourceFn(), + args: this.getArgs(), + max, + }); + + for await (const item of results) { + if (tsField) { + const ts = item[tsField]; + if (ts > lastTs) { + this.emitEvent(item); + maxTs = Math.max(ts, maxTs); + } + } else { + this.emitEvent(item); + } + } + + this._setLastTs(maxTs); + }, + getArgs() { + return {}; + }, + getTsField() { + return undefined; + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/taleez/sources/new-candidate-created/new-candidate-created.mjs b/components/taleez/sources/new-candidate-created/new-candidate-created.mjs new file mode 100644 index 0000000000000..37f3978ddf081 --- /dev/null +++ b/components/taleez/sources/new-candidate-created/new-candidate-created.mjs @@ -0,0 +1,34 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "taleez-new-candidate-created", + name: "New Candidate Created", + description: "Emit new event when a candidate is added in Taleez. [See the documentation](https://api.taleez.com/swagger-ui/index.html#/candidates/list_4)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.taleez.listCandidates; + }, + getArgs() { + return { + params: { + withProps: true, + }, + }; + }, + getTsField() { + return "dateCreation"; + }, + generateMeta(candidate) { + return { + id: candidate.id, + summary: `New Candidate: ${candidate.firstName} ${candidate.lastName}`, + ts: candidate.dateCreation, + }; + }, + }, +}; diff --git a/components/taleez/sources/new-job-listed/new-job-listed.mjs b/components/taleez/sources/new-job-listed/new-job-listed.mjs new file mode 100644 index 0000000000000..22d293d974ca5 --- /dev/null +++ b/components/taleez/sources/new-job-listed/new-job-listed.mjs @@ -0,0 +1,77 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "taleez-new-job-listed", + name: "New Job Listing Created", + description: "Emit new event when a job listing is created in Taleez. [See the documentation](https://api.taleez.com/swagger-ui/index.html#/jobs/list_3)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + unitId: { + propDefinition: [ + common.props.taleez, + "unitId", + ], + }, + status: { + propDefinition: [ + common.props.taleez, + "status", + ], + }, + contract: { + propDefinition: [ + common.props.taleez, + "contract", + ], + }, + city: { + propDefinition: [ + common.props.taleez, + "city", + ], + }, + companyLabel: { + propDefinition: [ + common.props.taleez, + "companyLabel", + ], + }, + tag: { + propDefinition: [ + common.props.taleez, + "tag", + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.taleez.listJobs; + }, + getArgs() { + return { + params: { + unitId: this.unitId, + status: this.status, + contract: this.contract, + city: this.city, + companyLabel: this.companyLabel, + tag: this.tag, + withDetails: true, + withProps: true, + }, + }; + }, + generateMeta(job) { + return { + id: job.id, + summary: `New Job: ${job.label}`, + ts: job.dateCreation, + }; + }, + }, +}; diff --git a/components/taleez/taleez.app.mjs b/components/taleez/taleez.app.mjs new file mode 100644 index 0000000000000..d42464cd5e69b --- /dev/null +++ b/components/taleez/taleez.app.mjs @@ -0,0 +1,225 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "taleez", + propDefinitions: { + jobId: { + type: "string", + label: "Job Listing ID", + description: "The ID of the job listing", + async options({ page }) { + const { list } = await this.listJobs({ + params: { + page, + }, + }); + return list?.map(({ + id: value, label, + }) => ({ + label, + value, + })); + }, + }, + candidateId: { + type: "string", + label: "Candidate ID", + description: "The ID of the candidate to link", + async options({ page }) { + const { list } = await this.listCandidates({ + params: { + page, + }, + }); + return list?.map(({ + id: value, firstName, lastName, + }) => ({ + label: `${firstName} ${lastName}`, + value, + })); + }, + }, + unitId: { + type: "string", + label: "Unit ID", + description: "Filter by unit ID", + optional: true, + async options({ page }) { + const { list } = await this.listUnits({ + params: { + page, + }, + }); + return list?.map(({ + id: value, publicName: label, + }) => ({ + label, + value, + })); + }, + }, + recruiterId: { + type: "string", + label: "Recruiter ID", + description: "The ID of the recruiter adding this candidate", + optional: true, + async options({ page }) { + const { list } = await this.listRecruiters({ + params: { + page, + }, + }); + return list?.map(({ + id: value, firstName, lastName, + }) => ({ + label: `${firstName} ${lastName}`, + value, + })); + }, + }, + status: { + type: "string", + label: "Status", + description: "Filter by job status", + options: [ + "DRAFT", + "PUBLISHED", + "DONE", + "SUSPENDED", + ], + optional: true, + }, + contract: { + type: "string", + label: "Contract", + description: "Filter by job contract", + options: [ + "CDI", + "CDD", + "INTERIM", + "FREELANCE", + "INTERNSHIP", + "APPRENTICESHIP", + "STUDENT", + "VIE", + "FRANCHISE", + "STATUTE", + "VACATAIRE", + "LIBERAL", + "CDI_CHANTIER", + "INTERMITTENT", + "SEASON", + "OTHER", + "VOLUNTEER", + "PERMANENT", + "FIXEDTERM", + ], + optional: true, + }, + city: { + type: "string", + label: "City", + description: "Filter by job city", + optional: true, + }, + companyLabel: { + type: "string", + label: "Company Label", + description: "Filter by company label", + optional: true, + }, + tag: { + type: "string", + label: "Tag", + description: "Filter by job tag", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.taleez.com/0"; + }, + _makeRequest({ + $ = this, + path, + ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "x-taleez-api-secret": this.$auth.secret_key, + }, + }); + }, + listJobs(opts = {}) { + return this._makeRequest({ + path: "/jobs", + ...opts, + }); + }, + listCandidates(opts = {}) { + return this._makeRequest({ + path: "/candidates", + ...opts, + }); + }, + listUnits(opts = {}) { + return this._makeRequest({ + path: "/units", + ...opts, + }); + }, + listRecruiters(opts = {}) { + return this._makeRequest({ + path: "/recruiters", + ...opts, + }); + }, + createCandidate(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/candidates", + ...opts, + }); + }, + linkCandidateToJob({ + jobId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/jobs/${jobId}/candidates`, + ...opts, + }); + }, + async *paginate({ + fn, args = {}, max, + }) { + let hasMorePages = true; + let page = 0; + let count = 0; + + while (hasMorePages) { + const { + list, hasMore, + } = await fn({ + ...args, + params: { + ...args?.params, + page, + pageSize: 1000, + }, + }); + for (const item of list) { + yield item; + if (max && ++count >= max) { + return; + } + } + hasMorePages = hasMore; + page++; + } + }, + }, +}; diff --git a/components/talend/README.md b/components/talend/README.md new file mode 100644 index 0000000000000..4560f6607b7c5 --- /dev/null +++ b/components/talend/README.md @@ -0,0 +1,11 @@ +# Overview + +The Talend API provides a robust toolkit for data integration and management, enabling the automation of tasks such as data extraction, transformation, and loading (ETL). By leveraging the Talend API within Pipedream, you can create intricate workflows that automate data operations, integrate with various data sources, and orchestrate data pipelines. It's a tool designed for data professionals seeking to streamline data processes and ensure data quality across their systems. + +# Example Use Cases + +- **Data Synchronization Between Systems**: Use the Talend API to build a workflow that syncs data between different systems such as a CRM and a marketing platform. Whenever a new lead is added to the CRM, Talend can process and clean the data, then Pipedream can trigger an update or insert the lead into the marketing platform. + +- **Automated Data Backups**: Craft a Pipedream workflow that utilizes the Talend API to automatically back up critical business data at scheduled intervals. This could involve extracting data from a database, transforming it into a suitable format, and then storing it securely on a cloud storage platform like AWS S3 or Google Cloud Storage. + +- **Real-Time Data Processing and Notifications**: Design a Pipedream workflow that listens for data updates from a live data source, processes the data using Talend, and then triggers a notification system like Twilio or SendGrid to alert relevant parties about the updated information or anomalies detected in the data stream. diff --git a/components/talend/package.json b/components/talend/package.json index e091954a5499b..d96aa741bf1ae 100644 --- a/components/talend/package.json +++ b/components/talend/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/talenox/README.md b/components/talenox/README.md new file mode 100644 index 0000000000000..2f716827c54e7 --- /dev/null +++ b/components/talenox/README.md @@ -0,0 +1,11 @@ +# Overview + +Talenox is a cloud-based HR software designed to handle payroll, leave, and employee records. With the Talenox API, you can automate various HR processes and integrate employee data across your business systems. On Pipedream, you can build workflows that trigger on events within Talenox or connect to other apps to streamline HR operations, manage staff details, automate payroll calculations, and synchronize employee data with other services like CRMs, messaging platforms, or time-tracking apps. + +# Example Use Cases + +- **Employee Onboarding Workflow**: Automate the onboarding process by triggering a workflow in Pipedream when a new employee is added in Talenox. The workflow can send a welcome email, create accounts in necessary business tools like Slack or Trello, and add the new hire to a Google Sheets tracking document. + +- **Payroll Processing Notification**: Set up a workflow that listens for payroll completion events in Talenox. Once payroll is processed, it can notify the finance team via email or a Slack message, and post a summary in a dedicated Slack channel or a Microsoft Teams group. + +- **Leave Balance Updates**: Create a workflow that updates your company's internal calendar or a project management tool like Asana when an employee's leave is approved in Talenox. It ensures that team availability is always up-to-date and helps in planning resources and deadlines. diff --git a/components/talenthr/actions/create-employee/create-employee.mjs b/components/talenthr/actions/create-employee/create-employee.mjs new file mode 100644 index 0000000000000..f4db509832bf0 --- /dev/null +++ b/components/talenthr/actions/create-employee/create-employee.mjs @@ -0,0 +1,182 @@ +import { + OVERTIME_STATUS, + PAY_RATE_PERIOD_OPTIONS, + PAY_RATE_SCHEDULE_OPTIONS, +} from "../../common/constants.mjs"; +import talenthr from "../../talenthr.app.mjs"; + +export default { + key: "talenthr-create-employee", + name: "Create Employee", + description: "Hires a new employee and registers them in the system. [See the documentation](https://apidocs.talenthr.io/#2950f0ba-b27b-4d4b-855f-4b79b667767c)", + version: "0.0.1", + type: "action", + props: { + talenthr, + firstName: { + propDefinition: [ + talenthr, + "firstName", + ], + }, + lastName: { + propDefinition: [ + talenthr, + "lastName", + ], + }, + email: { + propDefinition: [ + talenthr, + "email", + ], + }, + hireDate: { + propDefinition: [ + talenthr, + "hireDate", + ], + }, + employmentStatusId: { + propDefinition: [ + talenthr, + "employmentStatusId", + ], + }, + reportsToEmployeeId: { + propDefinition: [ + talenthr, + "employeeId", + ], + optional: true, + }, + jobTitleId: { + propDefinition: [ + talenthr, + "jobTitleId", + ], + optional: true, + }, + jobLocationId: { + propDefinition: [ + talenthr, + "jobLocationId", + ], + optional: true, + }, + divisionId: { + propDefinition: [ + talenthr, + "divisionId", + ], + optional: true, + }, + departmentId: { + propDefinition: [ + talenthr, + "departmentId", + ], + optional: true, + }, + payRate: { + type: "string", + label: "Pay Rate", + description: "Employee's wage and must have 2 decimals. E.g 1255.38", + }, + payRatePeriod: { + type: "string", + label: "Pay Rate Period", + description: "The period over which money is earned.", + options: PAY_RATE_PERIOD_OPTIONS, + }, + payRateSchedule: { + type: "string", + label: "Pay Rate Schedule", + description: "Frequency of the wage.", + options: PAY_RATE_SCHEDULE_OPTIONS, + optional: true, + }, + overtimeStatus: { + type: "string", + label: "Overtime Status", + description: "Determining whether an employee is exempt or non-exempt from overtime regulations.", + options: OVERTIME_STATUS, + optional: true, + }, + preventEmail: { + type: "boolean", + label: "Prevent Email", + description: "Opt for 'true', if you don't want to send an invitation email to the hiring employee, else 'false'.", + optional: true, + }, + isExisting: { + type: "boolean", + label: "Is Existing", + description: "Opt for 'false' if the employee is a new hire and you want to run the Automatic Onboarding process, else 'true'.", + optional: true, + }, + whoId: { + type: "integer", + label: "Who Id", + description: "The employee who will meet the newly hired employee. Required if **When Time** and address is present.", + optional: true, + }, + address: { + propDefinition: [ + talenthr, + "address", + ], + optional: true, + }, + whenTime: { + type: "string", + label: "When Time", + description: "The date time that the meeting will take place. Required if **Who Id** and address is present. The hire date must be formatted as 'YYYY-MM-DD HH:II'.", + optional: true, + }, + instructions: { + type: "string", + label: "Instructions", + description: "Important Instructions for the newly hired employee.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.talenthr.createEmployee({ + $, + data: { + first_name: this.firstName, + last_name: this.lastName, + email: this.email, + hire_date: this.hireDate, + employment_status: { + employment_status_id: this.employmentStatusId, + }, + reports_to_employee_id: this.reportsToEmployeeId, + job_record: { + job_title_id: this.jobTitleId, + location_id: this.jobLocationId, + division_id: this.divisionId, + department_id: this.departmentId, + }, + compensation_record: { + pay_rate: parseFloat(this.payRate), + pay_rate_period: this.payRatePeriod, + pay_rate_schedule: this.payRateSchedule, + overtime_status: this.overtimeStatus, + }, + prevent_email: this.preventEmail, + is_existing: this.isExisting, + hire_packet: { + who_id: this.whoId, + address: this.address, + when_time: this.whenTime, + instructions: this.instrwuctions, + }, + }, + }); + + $.export("$summary", `Successfully created employee: ${response.data.id}`); + return response; + }, +}; diff --git a/components/talenthr/actions/respond-time-off-request/respond-time-off-request.mjs b/components/talenthr/actions/respond-time-off-request/respond-time-off-request.mjs new file mode 100644 index 0000000000000..92d8b665fa33d --- /dev/null +++ b/components/talenthr/actions/respond-time-off-request/respond-time-off-request.mjs @@ -0,0 +1,44 @@ +import talenthr from "../../talenthr.app.mjs"; + +export default { + key: "talenthr-respond-time-off-request", + name: "Respond to Time Off Request", + description: "Responds to an employee's time off request. This action requires the request ID and the response status as props. [See the documentation](https://apidocs.talenthr.io/)", + version: "0.0.1", + type: "action", + props: { + talenthr, + employeeId: { + propDefinition: [ + talenthr, + "employeeId", + ], + }, + timeOffRequestId: { + propDefinition: [ + talenthr, + "timeOffRequestId", + ({ employeeId }) => ({ + employeeId, + }), + ], + }, + accept: { + type: "boolean", + label: "Accept", + description: "Approve 'true' or Reject 'false' the specified time off request. If the time off request has been answered or it has been cancelled then you cannot appove or reject it.", + }, + }, + async run({ $ }) { + const response = await this.talenthr.respondToTimeOffRequest({ + timeOffRequestId: this.timeOffRequestId, + employeeId: this.employeeId, + data: { + accept: this.accept, + }, + }); + + $.export("$summary", `Successfully responded to time off request with ID ${this.timeOffRequestId}`); + return response; + }, +}; diff --git a/components/talenthr/actions/update-employee/update-employee.mjs b/components/talenthr/actions/update-employee/update-employee.mjs new file mode 100644 index 0000000000000..d545c83e07dcd --- /dev/null +++ b/components/talenthr/actions/update-employee/update-employee.mjs @@ -0,0 +1,410 @@ +import { + CITIZENSHIP_OPTIONS, + GENDER_OPTIONS, + LANGUAGE_OPTIONS, + MARITAL_STATUS_OPTIONS, +} from "../../common/constants.mjs"; +import { + clearObj, + parseObject, +} from "../../common/utils.mjs"; +import talenthr from "../../talenthr.app.mjs"; + +export default { + key: "talenthr-update-employee", + name: "Update Employee", + description: "Allows updating an existing employee's data in the system. [See the documentation](https://apidocs.talenthr.io/)", + version: "0.0.1", + type: "action", + props: { + talenthr, + employeeId: { + propDefinition: [ + talenthr, + "employeeId", + ], + }, + firstName: { + propDefinition: [ + talenthr, + "firstName", + ], + }, + lastName: { + propDefinition: [ + talenthr, + "lastName", + ], + }, + email: { + propDefinition: [ + talenthr, + "email", + ], + }, + reportsToEmployeeId: { + propDefinition: [ + talenthr, + "employeeId", + ], + label: "Reports To Employee Id", + description: "If the **Reports To Employee Id** is 'null' then the current employee will be the head of the company.", + optional: true, + }, + + ssn: { + type: "string", + label: "SSN", + description: "The social security number of the employee", + optional: true, + }, + birthDate: { + type: "string", + label: "Birth Date", + description: "The birth date must be formatted as 'YYYY-MM-DD' and must be between now and 1930-01-01.", + optional: true, + }, + personalEmail: { + type: "string", + label: "Personal Email", + description: "The email has to be unique (among personal emails).", + optional: true, + }, + maritalStatus: { + type: "string", + label: "Marital Status", + description: "The marital status of the employee", + options: MARITAL_STATUS_OPTIONS, + optional: true, + }, + gender: { + type: "string", + label: "Gender", + description: "The gender of the employee", + options: GENDER_OPTIONS, + optional: true, + }, + nationality: { + propDefinition: [ + talenthr, + "nationality", + ], + optional: true, + }, + citizenship: { + type: "string", + label: "Citizenship", + description: "The citizenship of the employee", + options: CITIZENSHIP_OPTIONS, + optional: true, + }, + workPhone: { + type: "string", + label: "Work Phone", + description: "The phone number of the employee's work", + optional: true, + }, + mobilePhone: { + type: "string", + label: "Mobile Phone", + description: "The mobile phone numbe of the employee", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The phone numbe of the employee", + optional: true, + }, + address: { + propDefinition: [ + talenthr, + "address", + ], + optional: true, + }, + country: { + propDefinition: [ + talenthr, + "country", + ], + optional: true, + }, + postalCode: { + type: "string", + label: "Postal Code", + description: "The postal code where the employee lives", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "The city where the employee lives", + optional: true, + }, + emergencyContactFullName: { + type: "string", + label: "Emergency Contact Name", + description: "The emergency contact's full name", + optional: true, + }, + emergencyContactRelationshipTypeId: { + propDefinition: [ + talenthr, + "emergencyContactRelationshipTypeId", + ], + optional: true, + }, + emergencyContactPhone: { + type: "string", + label: "Emergency Contact Phone", + description: "The emergency contact's phone number", + optional: true, + }, + emergencyContactAddress: { + type: "string", + label: "Emergency Contact Address", + description: "The emergency contact's address", + optional: true, + }, + linkedInUrl: { + type: "string", + label: "LinkedIn", + description: "The URL of the linkedIn", + optional: true, + }, + employeeNumber: { + type: "string", + label: "Employee Number", + description: "An external number of the employee", + optional: true, + }, + passportNumber: { + type: "string", + label: "Passport Number", + description: "The number of the employee's passport", + optional: true, + }, + passportIssuedDate: { + type: "string", + label: "Passport Issued Date", + description: "The issued date of the employee's passport. **Format YYYY-MM-DD**", + optional: true, + }, + passportExpiryDate: { + type: "string", + label: "Passport Expiry Date", + description: "The expiry date of the employee's passport. **Format YYYY-MM-DD**", + optional: true, + }, + passportIssuingCountry: { + type: "string", + label: "Passport Issue Country", + description: "The issuing country of the employee's passport", + optional: true, + }, + visaType: { + type: "string", + label: "Visa Type", + description: "The type of the employee's visa", + optional: true, + }, + visaNumber: { + type: "string", + label: "Visa Number", + description: "The number of the employee's visa", + optional: true, + }, + visaExpiryDate: { + type: "string", + label: "Visa Expiry Date", + description: "The expiry date of the employee's visa. **Format YYYY-MM-DD**", + optional: true, + }, + driverLicenseNumber: { + type: "string", + label: "Driver License Number", + description: "The number of the employee's driver license", + optional: true, + }, + driverLicenseIssuedDate: { + type: "string", + label: "Driver License Issued Date", + description: "The issued date of the employee's driver license. **Format YYYY-MM-DD**", + optional: true, + }, + driverLicenseExpiryDate: { + type: "string", + label: "Driver License Expiry Date", + description: "The expiry date of the employee's driver license. **Format YYYY-MM-DD**", + optional: true, + }, + driverLicenseIssuingCountry: { + type: "string", + label: "Driver License Issue Country", + description: "The issuing country of the employee's driver license", + optional: true, + }, + secAddress: { + type: "string", + label: "Second Address", + description: "An employee's aditional address", + optional: true, + }, + secCity: { + type: "string", + label: "Second City", + description: "An employee's aditional city", + optional: true, + }, + secPostalCode: { + type: "string", + label: "Second Postal Code", + description: "An employee's aditional postal code", + optional: true, + }, + secCountry: { + type: "string", + label: "Second Country", + description: "An employee's aditional country", + optional: true, + }, + twitterUrl: { + type: "string", + label: "Twitter", + description: "The employee's twitter URL", + optional: true, + }, + facebookUrl: { + type: "string", + label: "Facebook", + description: "The employee's facebook URL", + optional: true, + }, + instagramUrl: { + type: "string", + label: "Instagram", + description: "The employee's instagram URL", + optional: true, + }, + pinterestUrl: { + type: "string", + label: "Pinterest", + description: "The employee's pinterest URL", + optional: true, + }, + githubUrl: { + type: "string", + label: "Github", + description: "The employee's github URL", + optional: true, + }, + behanceUrl: { + type: "string", + label: "Behance", + description: "The employee's behance URL", + optional: true, + }, + skypeName: { + type: "string", + label: "Skype Name", + description: "The employee's skype name", + optional: true, + }, + shirtSize: { + type: "string", + label: "Shirt Size", + description: "The size of the shirt the employee wears", + optional: true, + }, + tShirtSize: { + type: "string", + label: "T-Shirt Size", + description: "The size of the t-shirt the employee wears", + optional: true, + }, + hrLanguages: { + type: "integer[]", + label: "HR Languages", + description: "A list of language ids", + options: LANGUAGE_OPTIONS, + optional: true, + }, + hrFamily: { + type: "string[]", + label: "HR Family", + description: "An array of family objects. Example: `{\"name\": \"Jhon Doe\", \"gender\": \"Male\", \"birth_date\": \"2001-10-10\", \"family_member_relationship_id\": \"1\" }`", + optional: true, + }, + allergies: { + type: "string", + label: "Allergies", + description: "The employee's allegies", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.talenthr.updateEmployee({ + $, + employeeId: this.employeeId, + data: clearObj({ + first_name: this.firstName, + last_name: this.lastName, + email: this.email, + ssn: this.ssn, + birth_date: this.birthDate, + personal_email: this.personalEmail, + marital_status: this.maritalStatus, + gender: this.gender, + nationality: this.nationality, + citizenship: this.citizenship, + work_phone: this.workPhone, + mobile_phone: this.mobilePhone, + phone: this.phone, + address: this.address, + country: this.country, + postal_code: this.postalCode, + city: this.city, + reports_to_employee_id: this.reportsToEmployeeId, + emergency_contact: { + full_name: this.emergencyContactFullName, + relationship_type_id: this.emergencyContactRelationshipTypeId, + phone: this.emergencyContactPhone, + address: this.emergencyContactAddress, + }, + linked_in_url: this.linkedInUrl, + employee_number: this.employeeNumber, + passport_number: this.passportNumber, + passport_issued_date: this.passportIssuedDate, + passport_expiry_date: this.passportExpiryDate, + passport_issuing_country: this.passportIssuingCountry, + visa_type: this.visaType, + visa_number: this.visaNumber, + visa_expiry_date: this.visaExpiryDate, + driver_license_number: this.driverLicenseNumber, + driver_license_issued_date: this.driverLicenseIssuedDate, + driver_license_expiry_date: this.driverLicenseExpiryDate, + driver_license_issuing_country: this.driverLicenseIssuingCountry, + sec_address: this.secAddress, + sec_city: this.secCity, + sec_postal_code: this.secPostalCode, + sec_country: this.secCountry, + twitter_url: this.twitterUrl, + facebook_url: this.facebookUrl, + instagram_url: this.instagramUrl, + pinterest_url: this.pinterestUrl, + github_url: this.githubUrl, + behance_url: this.behanceUrl, + skype_name: this.skypeName, + shirt_size: this.shirtSize, + t_shirt_size: this.tShirtSize, + hr_languages: parseObject(this.hrLanguages), + hr_family: parseObject(this.hrFamily), + allergies: this.allergies, + }), + }); + + $.export("$summary", `Successfully updated employee ID ${this.employeeId}`); + return response; + }, +}; diff --git a/components/talenthr/common/constants.mjs b/components/talenthr/common/constants.mjs new file mode 100644 index 0000000000000..e58739af2d695 --- /dev/null +++ b/components/talenthr/common/constants.mjs @@ -0,0 +1,674 @@ +export const LIMIT = 100; + +export const PAY_RATE_PERIOD_OPTIONS = [ + "hour", + "day", + "week", + "month", + "quarter", + "year", +]; + +export const PAY_RATE_SCHEDULE_OPTIONS = [ + "once-per-month", + "twice-per-month", + "every-other-week", +]; + +export const OVERTIME_STATUS = [ + "exempt", + "non-exempt", +]; + +export const MARITAL_STATUS_OPTIONS = [ + "Single", + "Separated", + "Married", + "Widowed", +]; + +export const GENDER_OPTIONS = [ + "Male", + "Female", + "Non-binary", +]; + +export const CITIZENSHIP_OPTIONS = [ + "Afghan", + "Albanian", + "Algerian", + "American", + "Andorran", + "Angolan", + "Antiguans", + "Argentinean", + "Armenian", + "Australian", + "Austrian", + "Azerbaijani", + "Bahamian", + "Bahraini", + "Bangladeshi", + "Barbadian", + "Barbudans", + "Batswana", + "Belarusian", + "Belgian", + "Belizean", + "Beninese", + "Bhutanese", + "Bolivian", + "Bosnian", + "Brazilian", + "British", + "Bruneian", + "Bulgarian", + "Burkinabe", + "Burmese", + "Burundian", + "Cambodian", + "Cameroonian", + "Canadian", + "Cape Verdean", + "Central African", + "Chadian", + "Chilean", + "Chinese", + "Colombian", + "Comoran", + "Congolese", + "Costa Rican", + "Croatian", + "Cuban", + "Cypriot", + "Czech", + "Danish", + "Djibouti", + "Dominican", + "Dutch", + "East Timorese", + "Ecuadorean", + "Egyptian", + "Emirian", + "Equatorial Guinean", + "Eritrean", + "Estonian", + "Ethiopian", + "Fijian", + "Filipino", + "Finnish", + "French", + "Gabonese", + "Gambian", + "Georgian", + "German", + "Ghanaian", + "Greek", + "Grenadian", + "Guatemalan", + "Guinea-Bissauan", + "Guinean", + "Guyanese", + "Haitian", + "Herzegovinian", + "Honduran", + "Hungarian", + "I-Kiribati", + "Icelander", + "Indian", + "Indonesian", + "Iranian", + "Iraqi", + "Irish", + "Israeli", + "Italian", + "Ivorian", + "Jamaican", + "Japanese", + "Jordanian", + "Kazakhstani", + "Kenyan", + "Kittian and Nevisian", + "Kuwaiti", + "Kyrgyz", + "Laotian", + "Latvian", + "Lebanese", + "Liberian", + "Libyan", + "Liechtensteiner", + "Lithuanian", + "Luxembourger", + "Malagasy", + "Malawian", + "Malaysian", + "Maldivan", + "Malian", + "Maltese", + "Marshallese", + "Mauritanian", + "Mauritian", + "Mexican", + "Micronesian", + "Moldovan", + "Monacan", + "Mongolian", + "Moroccan", + "Mosotho", + "Motswana", + "Mozambican", + "Namibian", + "Nauruan", + "Nepalese", + "New Zealander", + "Nicaraguan", + "Nigerian", + "Nigerien", + "North Korean", + "North Macedonian", + "Northern Irish", + "Norwegian", + "Omani", + "Pakistani", + "Palauan", + "Palestinian", + "Panamanian", + "Papua New Guinean", + "Paraguayan", + "Peruvian", + "Polish", + "Portuguese", + "Qatari", + "Romanian", + "Russian", + "Rwandan", + "Saint Lucian", + "Salvadoran", + "Samoan", + "San Marinese", + "Sao Tomean", + "Saudi", + "Scottish", + "Senegalese", + "Serbian", + "Seychellois", + "Sierra Leonean", + "Singaporean", + "Slovakian", + "Slovenian", + "Solomon Islander", + "Somali", + "South African", + "South Korean", + "Spanish", + "Sri Lankan", + "Sudanese", + "Surinamer", + "Swazi", + "Swedish", + "Swiss", + "Syrian", + "Taiwanese", + "Tajik", + "Tanzanian", + "Thai", + "Togolese", + "Tongan", + "Trinidadian/Tobagonian", + "Tunisian", + "Turkish", + "Tuvaluan", + "Ugandan", + "Ukrainian", + "Uruguayan", + "Uzbekistani", + "Venezuelan", + "Vietnamese", + "Welsh", + "Yemenite", + "Zambian", + "Zimbabwean", +]; + +export const LANGUAGE_OPTIONS = [ + { + label: "Akan", + value: 123, + }, + { + label: "Amharic", + value: 50, + }, + { + label: "Arabic", + value: 22, + }, + { + label: "Arabic, Algerian", + value: 60, + }, + { + label: "Arabic, Egyptian", + value: 42, + }, + { + label: "Arabic, Moroccan", + value: 77, + }, + { + label: "Arabic, Najdi", + value: 101, + }, + { + label: "Arabic, Saidi", + value: 78, + }, + { + label: "Arabic, Sanaani", + value: 116, + }, + { + label: "Arabic, Sudanese", + value: 58, + }, + { + label: "Arabic, Tunisian", + value: 105, + }, + { + label: "Assamese", + value: 86, + }, + { + label: "Awadhi", + value: 74, + }, + { + label: "Azerbaijani, North", + value: 119, + }, + { + label: "Azerbaijani, South", + value: 68, + }, + { + label: "Belarusan", + value: 99, + }, + { + label: "Bengali", + value: 23, + }, + { + label: "Bhojpuri", + value: 51, + }, + { + label: "Bulgarian", + value: 108, + }, + { + label: "Burmese", + value: 57, + }, + { + label: "Cebuano", + value: 84, + }, + { + label: "Chhattisgarhi", + value: 96, + }, + { + label: "Chinese, Gan", + value: 73, + }, + { + label: "Chinese, Hakka", + value: 56, + }, + { + label: "Chinese, Jin", + value: 54, + }, + { + label: "Chinese, Mandarin", + value: 19, + }, + { + label: "Chinese, Min Bei", + value: 98, + }, + { + label: "Chinese, Min Nan", + value: 53, + }, + { + label: "Chinese, Wu", + value: 38, + }, + { + label: "Chinese, Xiang", + value: 63, + }, + { + label: "Chinese, Yue", + value: 35, + }, + { + label: "Chittagonian", + value: 88, + }, + { + label: "Croatian", + value: 72, + }, + { + label: "Czech", + value: 93, + }, + { + label: "Deccan", + value: 97, + }, + { + label: "Dutch", + value: 75, + }, + { + label: "English", + value: 17, + }, + { + label: "Farsi", + value: 69, + }, + { + label: "Farsi, Eastern", + value: 122, + }, + { + label: "French", + value: 21, + }, + { + label: "German", + value: 28, + }, + { + label: "Greek", + value: 94, + }, + { + label: "Gujarati", + value: 48, + }, + { + label: "Haitian Creole French", + value: 118, + }, + { + label: "Haryanvi", + value: 91, + }, + { + label: "Hausa", + value: 41, + }, + { + label: "Hiligaynon", + value: 124, + }, + { + label: "Hindi", + value: 20, + }, + { + label: "Hungarian", + value: 87, + }, + { + label: "Igbo", + value: 81, + }, + { + label: "Ilocano", + value: 114, + }, + { + label: "Indonesian", + value: 27, + }, + { + label: "Iranian Persian", + value: 40, + }, + { + label: "Italian", + value: 45, + }, + { + label: "Japanese", + value: 29, + }, + { + label: "Javanese", + value: 44, + }, + { + label: "Kannada", + value: 47, + }, + { + label: "Kazakh", + value: 113, + }, + { + label: "Khmer, Central", + value: 121, + }, + { + label: "Korean", + value: 39, + }, + { + label: "Kurmanji", + value: 125, + }, + { + label: "Lingala", + value: 61, + }, + { + label: "Lombard", + value: 110, + }, + { + label: "Madura", + value: 89, + }, + { + label: "Magahi", + value: 95, + }, + { + label: "Maithili", + value: 70, + }, + { + label: "Malagasy", + value: 104, + }, + { + label: "Malay", + value: 80, + }, + { + label: "Malayalam", + value: 64, + }, + { + label: "Marathi", + value: 31, + }, + { + label: "Marwari", + value: 92, + }, + { + label: "Napoletano-Calabrese", + value: 120, + }, + { + label: "Nepali", + value: 82, + }, + { + label: "Nigerian", + value: 30, + }, + { + label: "Oriya", + value: 65, + }, + { + label: "Oromo, West-Central", + value: 111, + }, + { + label: "Pashto, Northern", + value: 102, + }, + { + label: "Pashto, Southern", + value: 112, + }, + { + label: "Polish", + value: 59, + }, + { + label: "Portuguese", + value: 25, + }, + { + label: "Punjabi, Eastern", + value: 52, + }, + { + label: "Punjabi, Western", + value: 46, + }, + { + label: "Romanian", + value: 67, + }, + { + label: "Russian", + value: 24, + }, + { + label: "Rwanda", + value: 106, + }, + { + label: "Saraiki", + value: 83, + }, + { + label: "Serbian", + value: 71, + }, + { + label: "Shonda", + value: 126, + }, + { + label: "Sindhi", + value: 76, + }, + { + label: "Sinhala", + value: 90, + }, + { + label: "Somali", + value: 103, + }, + { + label: "Spanish", + value: 18, + }, + { + label: "Sunda", + value: 66, + }, + { + label: "Swahili", + value: 43, + }, + { + label: "Swedish", + value: 109, + }, + { + label: "Tagalog", + value: 37, + }, + { + label: "Tamil", + value: 34, + }, + { + label: "Tatar", + value: 115, + }, + { + label: "Telugu", + value: 32, + }, + { + label: "Thai", + value: 49, + }, + { + label: "Thai, Northeastern", + value: 85, + }, + { + label: "Turkish", + value: 33, + }, + { + label: "Ukrainian", + value: 62, + }, + { + label: "Urdu", + value: 26, + }, + { + label: "Uyghur", + value: 117, + }, + { + label: "Uzbek, Northern", + value: 79, + }, + { + label: "Vietnamese", + value: 36, + }, + { + label: "Yoruba", + value: 55, + }, + { + label: "Zhuang, Northern", + value: 100, + }, + { + label: "Zulu", + value: 107, + }, +]; diff --git a/components/talenthr/common/utils.mjs b/components/talenthr/common/utils.mjs new file mode 100644 index 0000000000000..9ad67c1c54e64 --- /dev/null +++ b/components/talenthr/common/utils.mjs @@ -0,0 +1,35 @@ +export const parseObject = (obj) => { + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + return JSON.parse(obj); + } + return obj; +}; + +export const clearObj = (obj) => { + return Object.entries(obj) + .filter(([ + , + v, + ]) => (v != null && v != "" && JSON.stringify(v) != "{}")) + .reduce((acc, [ + k, + v, + ]) => ({ + ...acc, + [k]: (!Array.isArray(v) && v === Object(v)) + ? clearObj(v) + : v, + }), {}); +}; diff --git a/components/talenthr/package.json b/components/talenthr/package.json new file mode 100644 index 0000000000000..d4f35e87d85c9 --- /dev/null +++ b/components/talenthr/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/talenthr", + "version": "0.1.0", + "description": "Pipedream TalentHR Components", + "main": "talenthr.app.mjs", + "keywords": [ + "pipedream", + "talenthr" + ], + "homepage": "https://pipedream.com/apps/talenthr", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} diff --git a/components/talenthr/sources/common/base.mjs b/components/talenthr/sources/common/base.mjs new file mode 100644 index 0000000000000..d347a2eaa065e --- /dev/null +++ b/components/talenthr/sources/common/base.mjs @@ -0,0 +1,69 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import talenthr from "../../talenthr.app.mjs"; + +export default { + props: { + talenthr, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + getConfig() { + return {}; + }, + sortData(data) { + return data; + }, + async emitEvent(maxResults = false) { + const lastId = this._getLastId(); + + const response = this.talenthr.paginate({ + fn: this.getFunction(), + maxResults, + ...this.getConfig(), + }); + + let responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + responseArray = this.sortData(responseArray); + responseArray = responseArray.filter((item) => item.id > lastId); + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastId(responseArray[0].id); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(new Date()), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/talenthr/sources/new-ats-application/new-ats-application.mjs b/components/talenthr/sources/new-ats-application/new-ats-application.mjs new file mode 100644 index 0000000000000..2745d20bbb61e --- /dev/null +++ b/components/talenthr/sources/new-ats-application/new-ats-application.mjs @@ -0,0 +1,41 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "talenthr-new-ats-application", + name: "New Job Application Submitted", + description: "Emit new event when a new job application is submitted. [See the documentation](https://apidocs.talenthr.io/)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + jobPositionId: { + propDefinition: [ + common.props.talenthr, + "jobPositionId", + ], + optional: true, + }, + }, + methods: { + ...common.methods, + getConfig() { + return { + jobPositionId: this.jobPositionId, + params: { + order: "desc", + sort: "created_at", + }, + }; + }, + getFunction() { + return this.talenthr.listNewJobApplications; + }, + getSummary(item) { + return `New Job Application: ${item.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/talenthr/sources/new-ats-application/test-event.mjs b/components/talenthr/sources/new-ats-application/test-event.mjs new file mode 100644 index 0000000000000..77d4a8001ddf3 --- /dev/null +++ b/components/talenthr/sources/new-ats-application/test-event.mjs @@ -0,0 +1,14 @@ +export default { + "id": 2, + "first_name": "John", + "last_name": "Doe", + "email": "jdoe@example.com", + "phone": "0030 6985*******", + "address": "address-name", + "created_at": "2023-12-11 11:16:50", + "updated_at": "2023-12-11 11:16:51", + "deleted_at": null, + "starred": false, + "applications_count": 1, + "added_at": "2023-12-11" +} \ No newline at end of file diff --git a/components/talenthr/sources/new-employee/new-employee.mjs b/components/talenthr/sources/new-employee/new-employee.mjs new file mode 100644 index 0000000000000..332f0a864a9f0 --- /dev/null +++ b/components/talenthr/sources/new-employee/new-employee.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "talenthr-new-employee", + name: "New Employee Created", + description: "Emit new event whenever a new employee is created. [See the documentation](https://apidocs.talenthr.io/)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.talenthr.listEmployees; + }, + getSummary({ + first_name: fName, last_name: lName, email, + }) { + return `New Employee: ${fName} ${lName} (${email})`; + }, + sortData(data) { + return data.sort((a, b) => b.id - a.id); + }, + }, + sampleEmit, +}; diff --git a/components/talenthr/sources/new-employee/test-event.mjs b/components/talenthr/sources/new-employee/test-event.mjs new file mode 100644 index 0000000000000..11ba1f439bb87 --- /dev/null +++ b/components/talenthr/sources/new-employee/test-event.mjs @@ -0,0 +1,29 @@ +export default { + "id": 19, + "user_id": 19, + "reports_to_employee_id": 1, + "photo_url": "https://d2spisfw2i7npf.cloudfront.net/public-assets/people/default-avatar.png...", + "u_id": 19, + "first_name": "John", + "last_name": "Doe", + "email": "jdoe@example.com", + "division_id": 13, + "division": "Europe", + "employment_status_id": 1, + "employment_status_name": "Full-Time", + "department_id": 6, + "department": "Administration & Operations", + "job_title_id": 26, + "job_title": "Business Analyst", + "location_id": 36, + "location": "Athens", + "termination_date": null, + "hire_date": "2023-11-28", + "linked_in_url": null, + "work_phone": null, + "user_role": { + "id": 3, + "slug": "employee", + "name": "Employee" + } +} \ No newline at end of file diff --git a/components/talenthr/talenthr.app.mjs b/components/talenthr/talenthr.app.mjs new file mode 100644 index 0000000000000..a9f961530cba9 --- /dev/null +++ b/components/talenthr/talenthr.app.mjs @@ -0,0 +1,375 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "talenthr", + propDefinitions: { + firstName: { + type: "string", + label: "First Name", + description: "The first name of the new employee", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the new employee", + }, + email: { + type: "string", + label: "Email", + description: "The email address of the new employee", + }, + hireDate: { + type: "string", + label: "Hire Date", + description: "The date of the new employee's hire. **Format YYYY-MM-DD**", + }, + employmentStatusId: { + type: "string", + label: "Employment Status Id", + description: "The employment status Id", + async options() { + const { data } = await this.listEmploymentStatuses(); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + employeeId: { + type: "string", + label: "Employee ID", + description: "The ID of the employee", + async options({ page }) { + const { data: { rows } } = await this.listEmployees({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + + return rows.map(({ + id: value, first_name: fName, last_name: lName, email, + }) => ({ + label: `${fName} ${lName} (${email})`, + value, + })); + }, + }, + timeOffRequestId: { + type: "string", + label: "Time Off Request ID", + description: "The ID of the employee's time off request", + async options({ + page, employeeId, + }) { + const { data: { rows } } = await this.listTimeOffRequests({ + employeeId, + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + + return rows.map(({ + id: value, timeoff_type_name: tName, start_date: sDate, end_date: eDate, + }) => ({ + label: `(${tName}) start: ${sDate} / end: ${eDate}`, + value, + })); + }, + }, + jobTitleId: { + type: "string", + label: "Job Title Id", + description: "The Id of the job title.", + async options() { + const { data } = await this.listJobTitles(); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + jobLocationId: { + type: "string", + label: "Job Location Id", + description: "The Id of the job location.", + async options() { + const { data } = await this.listJobLocations(); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + divisionId: { + type: "string", + label: "Division Id", + description: "The division for which the application has been submitted", + async options() { + const { data } = await this.listDivisions(); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + departmentId: { + type: "string", + label: "Department Id", + description: "The department for which the application has been submitted", + async options() { + const { data } = await this.listDepartments(); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + address: { + type: "string", + label: "Address", + description: "The address where the meeting will take place. Required if **Who Id** and **When Time** is present.", + }, + nationality: { + type: "string", + label: "Nationality", + description: "The nationality of the employee", + async options() { + const { data } = await this.listNationalities(); + return data; + }, + }, + country: { + type: "string", + label: "Country", + description: "The country where the employee lives", + async options() { + const { data } = await this.listCountries(); + return data; + }, + }, + emergencyContactRelationshipTypeId: { + type: "string", + label: "Emergency Contact Relationship Type", + description: "The type of the emergency contact's relationship", + async options() { + const { data } = await this.listRelationshipTypes(); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + jobPositionId: { + type: "string", + label: "Job Position", + description: "The job position for which the application has been submitted", + async options() { + const { data } = await this.listJobPositions(); + return data.map(({ + job_position_title: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + dataFields: { + type: "object", + label: "Data Fields", + description: "The data fields to be updated for the employee", + }, + requestId: { + type: "string", + label: "Request ID", + description: "The ID of the time off request", + }, + responseStatus: { + type: "string", + label: "Response Status", + description: "The response status for the time off request", + options: [ + "approved", + "pending", + "declined", + ], + }, + }, + methods: { + _baseUrl() { + return "https://pubapi.talenthr.io/v1"; + }, + _auth() { + return { + username: `${this.$auth.api_key}`, + password: "c", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + auth: this._auth(), + ...opts, + }); + }, + listEmploymentStatuses(opts = {}) { + return this._makeRequest({ + path: "/employment-statuses", + ...opts, + }); + }, + listJobTitles(opts = {}) { + return this._makeRequest({ + path: "/job-titles", + ...opts, + }); + }, + listJobLocations(opts = {}) { + return this._makeRequest({ + path: "/locations", + ...opts, + }); + }, + listDivisions(opts = {}) { + return this._makeRequest({ + path: "/divisions", + ...opts, + }); + }, + listDepartments(opts = {}) { + return this._makeRequest({ + path: "/departments", + ...opts, + }); + }, + listEmployees(opts = {}) { + return this._makeRequest({ + path: "/directory", + ...opts, + }); + }, + listNationalities(opts = {}) { + return this._makeRequest({ + path: "/nationalities", + ...opts, + }); + }, + listCountries(opts = {}) { + return this._makeRequest({ + path: "/countries", + ...opts, + }); + }, + listRelationshipTypes(opts = {}) { + return this._makeRequest({ + path: "/relationship-types", + ...opts, + }); + }, + listLanguages(opts = {}) { + return this._makeRequest({ + path: "/languages", + ...opts, + }); + }, + listJobPositions(opts = {}) { + return this._makeRequest({ + path: "/job-positions", + ...opts, + }); + }, + listTimeOffRequests({ + employeeId, ...opts + }) { + return this._makeRequest({ + path: `/employees/${employeeId}/time-off-requests`, + ...opts, + }); + }, + listNewJobApplications(opts = {}) { + const { + jobPositionId, ...otherOpts + } = opts; + return this._makeRequest({ + path: `${jobPositionId + ? `/job-positions/${jobPositionId}` + : ""}/ats-applicants`, + ...otherOpts, + }); + }, + createEmployee(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/employees/hire", + ...opts, + }); + }, + updateEmployee({ + employeeId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/employees/${employeeId}`, + ...opts, + }); + }, + respondToTimeOffRequest({ + timeOffRequestId, employeeId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/employees/${employeeId}/time-off-requests/${timeOffRequestId}/reply`, + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.limit = LIMIT; + params.offset = LIMIT * page++; + const { data: { rows } } = await fn({ + params, + ...opts, + }); + for (const d of rows) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = rows.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/talentlms/README.md b/components/talentlms/README.md index ee340726928d9..4d93f2322ca72 100644 --- a/components/talentlms/README.md +++ b/components/talentlms/README.md @@ -1,21 +1,11 @@ # Overview -Using the TalentLMS API, you can build a powerful e-learning platform with -features to help you deliver, track, and manage courses. The API provides the -capability to create, store, and serve course content, administer learners, and -monitor usage. With the API, you can design and implement a wide range of -applications and services for your users, giving them the flexibility to learn -wherever and whenever it is convenient for them. +TalentLMS's API enables you to automate and integrate your learning management tasks with ease. From syncing user data to generating detailed reports, the API opens up a world of possibilities for e-learning automation. With Pipedream, you can connect TalentLMS to a plethora of other apps, crafting personalized, intelligent workflows that operate seamlessly in the background, thus enhancing the learning experience for both instructors and trainees without manual intervention. -Here are some examples of what you can build using TalentLMS API: +# Example Use Cases -- Online course delivery platforms -- Learning management systems -- Employee onboarding solutions -- Virtual classrooms -- Post-training reinforcement programs -- Custom training portals for companies or organizations -- Gamified education solutions -- Mobile learning apps -- Performance evaluation tools -- Certification programs +- **Course Enrollment Automation**: Automate the enrollment process by connecting TalentLMS to a CRM like Salesforce. When a new contact is labeled as "interested in training" in Salesforce, Pipedream can trigger a workflow that automatically enrolls them in the corresponding course in TalentLMS. + +- **Attendance Tracking to Google Sheets**: Monitor and record course attendance by automating the data transfer from TalentLMS to Google Sheets. Each time a user completes a course session, Pipedream can capture this event and log their attendance in a dedicated Google Sheet, keeping track records up-to-date for reporting and compliance purposes. + +- **Real-time Alerts for User Progress**: Set up real-time notifications by connecting TalentLMS to messaging platforms such as Slack. When a user achieves a milestone, completes a course, or fails a quiz, Pipedream can send an alert to a designated Slack channel, keeping the team informed and ready to offer support or recognition. diff --git a/components/talkspirit/README.md b/components/talkspirit/README.md new file mode 100644 index 0000000000000..71d603a3f9b3f --- /dev/null +++ b/components/talkspirit/README.md @@ -0,0 +1,11 @@ +# Overview + +The talkSpirit API lets you tap into a comprehensive platform for team communication and collaboration. On Pipedream, you can orchestrate sophisticated workflows tapping into talkSpirit's features such as posting messages, managing users, and handling groups. Automate notifications, sync data with other apps, or create custom integrations that trigger actions within talkSpirit, all in real-time. + +# Example Use Cases + +- **Automated Team Notifications**: Trigger a workflow in Pipedream when a specific event happens in another app (like a new GitHub commit) and post an update to a talkSpirit group to keep your team informed. + +- **Event-Driven Group Management**: Maintain talkSpirit groups effortlessly by creating a Pipedream workflow that listens for changes in your HR system and adds or removes members from groups based on their department or role. + +- **Cross-Platform Project Updates**: Whenever a task is marked as completed in a project management tool like Trello, trigger a Pipedream workflow that posts a message to a relevant talkSpirit chat, keeping everyone aligned on project progress. diff --git a/components/tally/README.md b/components/tally/README.md index a43639dea40ca..c9f7d485a156c 100644 --- a/components/tally/README.md +++ b/components/tally/README.md @@ -1,24 +1,11 @@ # Overview -Using Tally API, you can build powerful applications that allow business users -to track, analyze, and automate their data. Here are some examples of what you -can build with the Tally API: +The Tally API offers a suite of automation capabilities for Tally, a form-building platform that allows users to create forms without needing to code. Leveraging this API within Pipedream can supercharge your data collection and processing. With Pipedream, you can react to form submissions in real-time, sync data to other services, and customize your workflows to suit various use cases including data analysis, lead capture, and feedback management. -- Accounting Integrations: Integrate financial data from Tally into any - third-party accounting and finance platform. -- Reporting & Analysis: Create custom dashboards and reports to get deep - insights into your business. -- Business Automations: Automate anything from customer support inquiries to - employee paychecks. -- Advertising & Marketing: Connect Tally with ad networks and marketing - platforms for better understanding of campaign performance. -- CRM Integrations: Improve customer satisfaction and support by integrating - Tally with your CRM. -- Connected Devices: Connect Tally with IoT devices so you can monitor and - analyze data in realtime. -- Inventory Management: Track inventory levels and manage stock level - automatically. -- ECommerce Platforms: Connect Tally with popular eCommerce platforms to get - accurate orders and transactions data. -- Billing & Payment Solutions: Connect Tally with payment gateways and billing - software to automate customer invoices and payments. +# Example Use Cases + +- **Automated Lead Capture to CRM**: Upon receiving a new form submission on Tally, trigger a workflow that automatically adds the respondent's details to your Customer Relationship Management (CRM) system. This keeps the sales team updated in real-time with fresh leads. + +- **Dynamic Feedback Aggregation**: Configure a Pipedream workflow to collect form submissions from Tally and insert them into Google Sheets. Apply further logic to categorize and analyze the feedback for actionable insights, which can then be reported to decision-makers. + +- **Event Registration and Confirmation**: Create an event registration form in Tally and set up a Pipedream workflow to send a personalized confirmation email via SendGrid to each registrant. Additionally, manage attendee lists and send reminders as the event date approaches. diff --git a/components/tally/package.json b/components/tally/package.json index 96806a37f9303..8d8977af4f39f 100644 --- a/components/tally/package.json +++ b/components/tally/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/tally", - "version": "0.0.6", + "version": "0.0.7", "description": "Pipedream Tally Components", "main": "tally.app.mjs", "keywords": [ diff --git a/components/tally/sources/new-response/new-response.mjs b/components/tally/sources/new-response/new-response.mjs index 341d3eda2de5d..eea2c7a9785fd 100644 --- a/components/tally/sources/new-response/new-response.mjs +++ b/components/tally/sources/new-response/new-response.mjs @@ -4,7 +4,7 @@ import sampleEmit from "./test-event.mjs"; export default { ...common, name: "New Response (Instant)", - version: "0.0.4", + version: "0.0.5", key: "tally-new-response", description: "Emit new event on each form message. [See the documentation](https://tallyso.notion.site/Tally-OAuth-2-reference-d0442c679a464664823628f675f43454)", type: "source", @@ -35,6 +35,18 @@ export default { return (field.options.filter(({ id }) => field.value.includes(id)).map(({ text }) => text)) .join(); }, + getSortedOptionsText(field) { + const idToTextMap = new Map( + field?.options.map(({ + id, text, + }) => [ + id, + text, + ]), + ); + const orderedTexts = field?.value.map((id) => idToTextMap.get(id)); + return orderedTexts?.join(); + }, getUrlResponse(field) { return (field.value.map(({ url }) => url)).join(); }, @@ -58,12 +70,15 @@ export default { if (field.type === "MULTIPLE_CHOICE") { parsedAnswer = this.getSingleResponse(field); } - if (field.type === "CHECKBOXES" || field.type === "DROPDOWN" || field.type === "MULTI_SELECT" || field.type === "RANKING") { + if (field.type === "CHECKBOXES" || field.type === "DROPDOWN" || field.type === "MULTI_SELECT") { if (!field.options) { continue; } parsedAnswer = this.getMultipeResponses(field); } + if (field.type === "RANKING") { + parsedAnswer = this.getSortedOptionsText(field); + } if (field.type === "FILE_UPLOAD" || field.type === "SIGNATURE") { parsedAnswer = this.getUrlResponse(field); } diff --git a/components/tapfiliate/README.md b/components/tapfiliate/README.md new file mode 100644 index 0000000000000..5d82605d0de40 --- /dev/null +++ b/components/tapfiliate/README.md @@ -0,0 +1,11 @@ +# Overview + +The Tapfiliate API lets you build custom affiliate marketing programs, handling tracking, attribution, and commission management for your business. By leveraging it on Pipedream, you can automate affiliate-related processes, sync affiliate data with other tools, and create real-time, event-driven workflows. Whether you're syncing affiliate data to a CRM, sending customized email notifications, or updating affiliate info across platforms, Pipedream's serverless platform streamlines these tasks with minimal setup. + +# Example Use Cases + +- **Affiliate Signup Notifications**: Auto-send personalized welcome emails to new affiliates using SendGrid or another email service as they join your Tapfiliate program. + +- **Commission Approval Workflows**: Set up a system where commissions are automatically reviewed and approved based on custom rules, and then synced with accounting software like QuickBooks for streamlined payment processing. + +- **Real-Time Affiliate Dashboard Updates**: Connect Tapfiliate with Google Sheets or a database service like Airtable to update affiliate performance metrics in real-time, providing instant insights for marketing strategy adjustments. diff --git a/components/tapform/README.md b/components/tapform/README.md new file mode 100644 index 0000000000000..6b4b2532002e4 --- /dev/null +++ b/components/tapform/README.md @@ -0,0 +1,11 @@ +# Overview + +The Tapform API furnishes a way to automate and integrate form data handling in Pipedream's serverless environment. With it, you can manipulate form submissions, manage user data, and connect forms to other services for complex workflows. This opens endless possibilities to streamline data collection processes, react in real-time to user submissions, and connect form data to more than 500+ apps available on Pipedream. + +# Example Use Cases + +- **Automated Lead Capture and CRM Integration**: When a new form submission comes in via Tapform, automate the capture of leads by sending the data directly into a CRM like HubSpot. This not only saves time but ensures that every potential customer is immediately accounted for and can be followed up with promptly. + +- **Real-Time Notification System**: Set up a workflow that triggers a notification through email or messaging platforms like Slack whenever a Tapform submission is received. This could be crucial for time-sensitive applications, such as customer support queries or order forms, allowing teams to react swiftly to customer needs. + +- **Data Aggregation and Analysis**: Collect submission data from Tapform and send it to a Google Sheets document for further analysis. This can help in compiling data over time, making it easier to spot trends, understand user behavior, and make informed decisions based on the aggregated form data. diff --git a/components/tapform/package.json b/components/tapform/package.json index 927f5246c46d3..c09f8973b9b7a 100644 --- a/components/tapform/package.json +++ b/components/tapform/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/tapform", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Tapform Components", "main": "tapform.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" } -} \ No newline at end of file +} diff --git a/components/tapform/sources/new-lead-added/new-lead-added.mjs b/components/tapform/sources/new-lead-added/new-lead-added.mjs new file mode 100644 index 0000000000000..e938b6dd54d69 --- /dev/null +++ b/components/tapform/sources/new-lead-added/new-lead-added.mjs @@ -0,0 +1,64 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import tapform from "../../tapform.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "tapform-new-lead-added", + name: "New Lead Added", + description: "Emit new event when a lead is added to Tapform.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + tapform, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01T00:00:00.001Z"; + }, + _setLastDate(created) { + this.db.set("lastDate", created); + }, + generateMeta(item) { + return { + id: item.id, + summary: `New lead added with Id: ${item.id}`, + ts: item.createdAt, + }; + }, + async startEvent(maxResults = 0) { + const lastDate = this._getLastDate(); + const data = await this.tapform.listLeads(); + let count = 0; + + const responseArray = []; + for (const item of data) { + if (Date.parse(item.createdAt) <= Date.parse(lastDate)) break; + if (maxResults && (maxResults <= ++count)) break; + responseArray.push(item); + } + + if (responseArray.length) this._setLastDate(responseArray[0].createdAt); + + for (const item of responseArray.reverse()) { + this.$emit(item, this.generateMeta(item)); + } + }, + }, + hooks: { + async deploy() { + await this.startEvent(25); + }, + }, + async run() { + await this.startEvent(); + }, + sampleEmit, +}; diff --git a/components/tapform/sources/new-lead-added/test-event.mjs b/components/tapform/sources/new-lead-added/test-event.mjs new file mode 100644 index 0000000000000..79f897e9122b5 --- /dev/null +++ b/components/tapform/sources/new-lead-added/test-event.mjs @@ -0,0 +1,21 @@ +export default { + "id": "string", + "firstName": "string", + "lastName": "string", + "email": "string", + "phoneNumber": "string", + "additionalNotes": "string", + "address": "string", + "createdAt": "DateTime", + "businessName": "string", + "answers": [ + { + "question": "string", + "answer": "any" + }, + { + "question": "string", + "answer": "any" + } + ] +} \ No newline at end of file diff --git a/components/tapform/tapform.app.mjs b/components/tapform/tapform.app.mjs index 8a306dd914f1d..36cc13675af7d 100644 --- a/components/tapform/tapform.app.mjs +++ b/components/tapform/tapform.app.mjs @@ -1,11 +1,31 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "tapform", - propDefinitions: {}, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _apiUrl() { + return "https://apimvp.tapform.io/api"; + }, + _getHeaders() { + return { + "tapform-api-key": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: `${this._apiUrl()}/${path}`, + headers: this._getHeaders(), + ...opts, + }); + }, + listLeads(args = {}) { + return this._makeRequest({ + path: "leads/zapier-leads", + ...args, + }); }, }, }; diff --git a/components/taskade/README.md b/components/taskade/README.md new file mode 100644 index 0000000000000..46a23f2c1949f --- /dev/null +++ b/components/taskade/README.md @@ -0,0 +1,11 @@ +# Overview + +Taskade is a unified workspace app designed to help teams manage tasks, write notes, and video chat within the same platform. With the Taskade API on Pipedream, you can automate workflows, synchronize tasks across various platforms, and streamline communications. This API enables you to programmatically manage tasks, projects, and their associated details, which can be integrated with hundreds of other apps available on Pipedream to create dynamic, custom workflows tailored to your team's needs. + +# Example Use Cases + +- **Automated Task Syncing Across Platforms**: Integrate Taskade with Google Calendar via Pipedream to automatically create or update events based on Taskade task deadlines. This ensures your scheduling remains in sync across both platforms, providing real-time updates and reminders for your team's tasks and deadlines. + +- **Team Communication Enhancement**: Connect Taskade with Slack using Pipedream. Whenever a new task is assigned, a notification can be sent to a designated Slack channel or directly to a team member. This immediate update facilitates better communication and ensures all members are aware of their responsibilities without needing to check multiple platforms. + +- **Project Management Automation**: Use Pipedream to integrate Taskade with Trello. Automate the creation of Trello cards based on new Taskade projects or tasks. This can be particularly useful for teams using Trello for visual project tracking while managing task details and assignments in Taskade, allowing fluid movement between detailed task management and visual project oversight. diff --git a/components/taskade/actions/create-task/create-task.mjs b/components/taskade/actions/create-task/create-task.mjs new file mode 100644 index 0000000000000..8503d903a0609 --- /dev/null +++ b/components/taskade/actions/create-task/create-task.mjs @@ -0,0 +1,75 @@ +import taskade from "../../taskade.app.mjs"; + +export default { + key: "taskade-create-task", + name: "Create Task", + description: "Creates a new task in Taskade. [See the documentation](https://developers.taskade.com/docs/api/tasks/create)", + version: "0.0.1", + type: "action", + props: { + taskade, + projectId: { + propDefinition: [ + taskade, + "projectId", + ], + }, + content: { + type: "string", + label: "Content", + description: "Content of the task", + }, + contentType: { + type: "string", + label: "Content Type", + description: "The type of content", + options: [ + "text/markdown", + "text/plain", + ], + }, + placement: { + type: "string", + label: "Placement", + description: "Placement of the task", + options: [ + "afterbegin", + "beforeend", + ], + }, + assignees: { + type: "string[]", + label: "Assignees", + description: "An array of user handles to assign to the task", + optional: true, + }, + }, + async run({ $ }) { + const task = await this.taskade.createTask({ + $, + projectId: this.projectId, + data: { + tasks: [ + { + content: this.content, + contentType: this.contentType, + placement: this.placement, + }, + ], + }, + }); + const taskId = task.item[0].id; + if (this.assignees?.length) { + await this.taskade.assignTask({ + $, + projectId: this.projectId, + taskId, + data: { + handles: this.assignees, + }, + }); + } + $.export("$summary", `Successfully created task with ID ${taskId}`); + return task; + }, +}; diff --git a/components/taskade/package.json b/components/taskade/package.json new file mode 100644 index 0000000000000..853933b0fd7c4 --- /dev/null +++ b/components/taskade/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/taskade", + "version": "0.1.0", + "description": "Pipedream Taskade Components", + "main": "taskade.app.mjs", + "keywords": [ + "pipedream", + "taskade" + ], + "homepage": "https://pipedream.com/apps/taskade", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/taskade/sources/new-task-created/new-task-created.mjs b/components/taskade/sources/new-task-created/new-task-created.mjs new file mode 100644 index 0000000000000..df0961d45fa6d --- /dev/null +++ b/components/taskade/sources/new-task-created/new-task-created.mjs @@ -0,0 +1,77 @@ +import taskade from "../../taskade.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "taskade-new-task-created", + name: "New Task Created", + description: "Emit new event when a new task is created in Taskade", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + taskade, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + projectId: { + propDefinition: [ + taskade, + "projectId", + ], + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getPreviousIds() { + return this.db.get("previousIds") || {}; + }, + _setPreviousIds(previousIds) { + this.db.set("previousIds", previousIds); + }, + emitEvent(task) { + const meta = this.generateMeta(task); + this.$emit(task, meta); + }, + generateMeta(task) { + return { + id: task.id, + summary: `New Task ID: ${task.id}`, + ts: Date.now(), + }; + }, + async processEvent(max) { + const tasks = []; + const items = this.taskade.paginate({ + resourceFn: this.taskade.listTasks, + args: { + projectId: this.projectId, + }, + resourceType: "items", + }); + for await (const item of items) { + tasks.push(item); + } + let previousIds = this._getPreviousIds(); + let newTasks = tasks.filter(({ id }) => !previousIds[id]); + newTasks.forEach(({ id }) => previousIds[id] = true); + this._setPreviousIds(previousIds); + newTasks = max + ? newTasks.slice(0, max) + : newTasks; + newTasks.forEach((task) => this.emitEvent(task)); + }, + }, + async run() { + await this.processEvent(); + }, + sampleEmit, +}; diff --git a/components/taskade/sources/new-task-created/test-event.mjs b/components/taskade/sources/new-task-created/test-event.mjs new file mode 100644 index 0000000000000..fa8da75002090 --- /dev/null +++ b/components/taskade/sources/new-task-created/test-event.mjs @@ -0,0 +1,5 @@ +export default { + "id": "85e89694-3ca9-49ac-8393-4595543d4643", + "text": "Getting Started", + "completed": false +} \ No newline at end of file diff --git a/components/taskade/taskade.app.mjs b/components/taskade/taskade.app.mjs new file mode 100644 index 0000000000000..5fda3702c4a67 --- /dev/null +++ b/components/taskade/taskade.app.mjs @@ -0,0 +1,118 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "taskade", + propDefinitions: { + projectId: { + type: "string", + label: "Project ID", + description: "The identifier of a project", + async options({ page }) { + const { items } = await this.listProjects({ + params: { + page: page + 1, + }, + }); + return items?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://www.taskade.com/api/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + listProjects(opts = {}) { + return this._makeRequest({ + path: "/me/projects", + ...opts, + }); + }, + listTasks({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/projects/${projectId}/tasks`, + ...opts, + }); + }, + createTask({ + projectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/projects/${projectId}/tasks`, + ...opts, + }); + }, + assignTask({ + projectId, taskId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/projects/${projectId}/tasks/${taskId}/assignees`, + ...opts, + }); + }, + createOrUpdateDueDate({ + projectId, taskId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/projects/${projectId}/tasks/${taskId}/date`, + ...opts, + }); + }, + async *paginate({ + resourceFn, + args, + resourceType, + max, + }) { + args = { + ...args, + params: { + ...args.params, + }, + }; + let count = 0; + do { + const results = await resourceFn(args); + const items = resourceType + ? results[resourceType] + : results; + if (!items?.length) { + return; + } + for (const item of items) { + yield item; + count++; + if (max && max >= count) { + return; + } + } + args.params.after = items[items.length - 1].id; + } while (args.params.after); + }, + }, +}; diff --git a/components/tave/README.md b/components/tave/README.md new file mode 100644 index 0000000000000..e7549bb012924 --- /dev/null +++ b/components/tave/README.md @@ -0,0 +1,11 @@ +# Overview + +The Tave API is a powerful tool for managing studio operations, including client management, job tracking, and financial insights. By integrating Tave with Pipedream, you can automate workflows, sync data across applications, and trigger actions based on specific events. This can save time, reduce manual errors, and allow you to focus on more creative aspects of your business. Whether you're looking to streamline your lead management, automate your booking process, or keep your accounts in sync, Pipedream's serverless platform facilitates these connections with minimal setup. + +# Example Use Cases + +- **Automated Lead Follow-Up**: When a new lead is created in Tave, use Pipedream to trigger an automated email sequence via SendGrid, ensuring prompt and consistent follow-up with potential clients. + +- **Job Status Sync with Google Calendar**: Keep your schedule up-to-date by creating a workflow that updates Google Calendar events whenever a job's status changes in Tave, allowing for seamless calendar management. + +- **Invoice and Payment Tracking**: Connect Tave to a Slack channel using Pipedream to send notifications when new invoices are created or payments are received, keeping the team informed about the financial status in real-time. diff --git a/components/tave/package.json b/components/tave/package.json index ca37060197df3..27046ab996466 100644 --- a/components/tave/package.json +++ b/components/tave/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/tavily/actions/send-query/send-query.mjs b/components/tavily/actions/send-query/send-query.mjs new file mode 100644 index 0000000000000..0bd76e4b2ece4 --- /dev/null +++ b/components/tavily/actions/send-query/send-query.mjs @@ -0,0 +1,51 @@ +import app from "../../tavily.app.mjs"; + +export default { + key: "tavily-send-query", + name: "Send Query", + description: "Search for data based on a query. [See the documentation](https://docs.tavily.com/docs/tavily-api/rest_api#post-search)", + version: "0.0.1", + type: "action", + props: { + app, + query: { + propDefinition: [ + app, + "query", + ], + }, + searchDepth: { + propDefinition: [ + app, + "searchDepth", + ], + }, + includeImages: { + propDefinition: [ + app, + "includeImages", + ], + }, + includeAnswer: { + propDefinition: [ + app, + "includeAnswer", + ], + }, + }, + async run({ $ }) { + const response = await this.app.sendQuery({ + $, + data: { + query: this.query, + search_depth: this.searchDepth, + include_images: this.includeImages, + include_answer: this.includeAnswer, + }, + }); + + $.export("$summary", `Successfully sent query and got the following answer: '${response.answer}'`); + + return response; + }, +}; diff --git a/components/tavily/common/constants.mjs b/components/tavily/common/constants.mjs new file mode 100644 index 0000000000000..3bc901f45f6c7 --- /dev/null +++ b/components/tavily/common/constants.mjs @@ -0,0 +1,6 @@ +export default { + SEARCH_DEPTHS: [ + "basic", + "advanced", + ], +}; diff --git a/components/tavily/package.json b/components/tavily/package.json new file mode 100644 index 0000000000000..8e27af4c796b3 --- /dev/null +++ b/components/tavily/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/tavily", + "version": "0.1.0", + "description": "Pipedream Tavily Components", + "main": "tavily.app.mjs", + "keywords": [ + "pipedream", + "tavily" + ], + "homepage": "https://pipedream.com/apps/tavily", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/tavily/tavily.app.mjs b/components/tavily/tavily.app.mjs new file mode 100644 index 0000000000000..00a68421bf90d --- /dev/null +++ b/components/tavily/tavily.app.mjs @@ -0,0 +1,58 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "tavily", + propDefinitions: { + query: { + type: "string", + label: "Query", + description: "The search query", + }, + searchDepth: { + type: "string", + label: "Search Depth", + description: "The depth of the search", + options: constants.SEARCH_DEPTHS, + }, + includeImages: { + type: "boolean", + label: "Include Images", + description: "Include a list of query related images in the response", + }, + includeAnswer: { + type: "boolean", + label: "Include Answer", + description: "Include answers in the search results", + }, + }, + methods: { + _baseUrl() { + return "https://api.tavily.com"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + data, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + data: { + ...data, + api_key: this.$auth.api_key, + }, + }); + }, + async sendQuery(args = {}) { + return this._makeRequest({ + method: "post", + path: "/search", + ...args, + }); + }, + }, +}; diff --git a/components/tawk_to/README.md b/components/tawk_to/README.md new file mode 100644 index 0000000000000..02b6302531542 --- /dev/null +++ b/components/tawk_to/README.md @@ -0,0 +1,11 @@ +# Overview + +The tawk.to API lets you interact with the tawk.to live chat platform programmatically. Using Pipedream, you can connect tawk.to to a variety of other apps and services to automate notifications, sync chat data, and enhance customer support operations. You can trigger workflows on new messages, follow up on conversations, extract chat transcripts, or link chat events to CRM systems, issue trackers, or databases—all in real-time. + +# Example Use Cases + +- **Sync Chat Transcripts to Google Sheets**: Automatically send completed chat transcripts from tawk.to to a Google Sheets spreadsheet. Use this workflow to maintain a log of customer interactions, analyze conversations for quality assurance, or generate reports. + +- **Create Trello Cards from Support Chats**: Whenever a tawk.to chat mentions a specific keyword or phrase, like "bug" or "feature request," create a card in Trello automatically. This helps you track customer feedback and ensures that your support and product development teams are aligned. + +- **Trigger SMS Notifications on High Priority Chats**: Set up a workflow that triggers an SMS alert through a service like Twilio when a high-priority chat starts on tawk.to. This way, key team members can jump into action immediately for top-tier customer support. diff --git a/components/tawk_to/package.json b/components/tawk_to/package.json index 08c12e0e4a90c..d723d28054b59 100644 --- a/components/tawk_to/package.json +++ b/components/tawk_to/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/tawk_to", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream tawk.to Components", "main": "tawk_to.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" } -} \ No newline at end of file +} diff --git a/components/tawk_to/sources/chat-ended-instant/chat-ended-instant.mjs b/components/tawk_to/sources/chat-ended-instant/chat-ended-instant.mjs new file mode 100644 index 0000000000000..846e814aa6dc7 --- /dev/null +++ b/components/tawk_to/sources/chat-ended-instant/chat-ended-instant.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "tawk_to-chat-ended-instant", + name: "Chat Ended (Instant)", + description: "Emit new event when a chat ends, usually after 90-150 seconds of inactivity", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "chat:end", + ]; + }, + generateMeta(event) { + return { + id: event.chatId, + summary: `Chat Ended ${event.chatId}`, + ts: Date.parse(event.time), + }; + }, + }, +}; diff --git a/components/tawk_to/sources/common/base.mjs b/components/tawk_to/sources/common/base.mjs new file mode 100644 index 0000000000000..01afd9043c8f5 --- /dev/null +++ b/components/tawk_to/sources/common/base.mjs @@ -0,0 +1,66 @@ +import tawkTo from "../../tawk_to.app.mjs"; + +export default { + props: { + tawkTo, + http: "$.interface.http", + db: "$.service.db", + type: { + propDefinition: [ + tawkTo, + "type", + ], + }, + propertyId: { + propDefinition: [ + tawkTo, + "propertyId", + (c) => ({ + type: c.type, + }), + ], + }, + }, + hooks: { + async activate() { + const events = this.getEvents(); + const { data } = await this.tawkTo.createWebhook({ + data: { + propertyId: this.propertyId, + events, + url: this.http.endpoint, + enabled: true, + name: `Pipedream ${events[0]} Webhook`, + }, + }); + this._setHookId(data?.hookId); + }, + async deactivate() { + const hookId = this._getHookId(); + if (hookId) { + await this.tawkTo.deleteWebhook({ + data: { + propertyId: this.propertyId, + hookId, + }, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run(event) { + const { body } = event; + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, +}; diff --git a/components/tawk_to/sources/new-chat-started-instant/new-chat-started-instant.mjs b/components/tawk_to/sources/new-chat-started-instant/new-chat-started-instant.mjs new file mode 100644 index 0000000000000..80e86c0c610cf --- /dev/null +++ b/components/tawk_to/sources/new-chat-started-instant/new-chat-started-instant.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "tawk_to-new-chat-started-instant", + name: "New Chat Started (Instant)", + description: "Emit new event when the first message in a chat is sent by a visitor or agent.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "chat:start", + ]; + }, + generateMeta(event) { + return { + id: event.chatId, + summary: `New Chat Started ${event.chatId}`, + ts: Date.parse(event.time), + }; + }, + }, +}; diff --git a/components/tawk_to/sources/new-ticket-instant/new-ticket-instant.mjs b/components/tawk_to/sources/new-ticket-instant/new-ticket-instant.mjs new file mode 100644 index 0000000000000..618d54ea04c12 --- /dev/null +++ b/components/tawk_to/sources/new-ticket-instant/new-ticket-instant.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "tawk_to-new-ticket-instant", + name: "New Ticket (Instant)", + description: "Emit new event when a new ticket is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + "ticket:create", + ]; + }, + generateMeta(event) { + return { + id: event.ticket.id, + summary: `New ticket ${event.ticket.id}`, + ts: Date.parse(event.time), + }; + }, + }, +}; diff --git a/components/tawk_to/tawk_to.app.mjs b/components/tawk_to/tawk_to.app.mjs index 436210949b2d3..1759224249464 100644 --- a/components/tawk_to/tawk_to.app.mjs +++ b/components/tawk_to/tawk_to.app.mjs @@ -1,11 +1,79 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "tawk_to", - propDefinitions: {}, + propDefinitions: { + type: { + type: "string", + label: "Type", + description: "Type to filter property to", + options: [ + "business", + "profile", + ], + }, + propertyId: { + type: "string", + label: "Property ID", + description: "Identifier of the property to watch", + async options({ type }) { + const { data } = await this.listProperties({ + data: { + type, + }, + }); + return data?.map(({ + propertyId: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.tawk.to/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + auth: { + username: `${this.$auth.api_key}`, + password: "f", + }, + headers: { + "Content-Type": "application/json", + }, + }); + }, + listProperties(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/property.list", + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks.create", + ...opts, + }); + }, + deleteWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks.delete", + ...opts, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/taxjar/README.md b/components/taxjar/README.md new file mode 100644 index 0000000000000..2804ae12f64f6 --- /dev/null +++ b/components/taxjar/README.md @@ -0,0 +1,14 @@ +# Overview + +The TaxJar API offers a suite of tools for automating sales tax calculations, reporting, and filings. With this API, you can get real-time sales tax rates and calculations at checkout, generate detailed sales tax reports for each state, and automate your sales tax filings. Integrating TaxJar with Pipedream allows you to create workflows that can enhance e-commerce platforms, financial systems, and accounting software, ensuring that sales tax handling is both accurate and effortless. + +# Example Use Cases + +- **Real-Time Sales Tax Calculation at Checkout** + Automatically calculate sales tax in Pipedream workflows for e-commerce orders. When a new order is received in Shopify, trigger a workflow that uses the TaxJar API to calculate the appropriate sales tax based on the customer’s location and the items purchased. + +- **Monthly Sales Tax Reporting** + Schedule a Pipedream workflow that runs at the end of every month, summarizing total sales and taxes collected through Stripe. Use TaxJar to prepare the tax reports needed for filing, ensuring compliance with minimal manual effort. + +- **Automated Tax Filing Reminders** + Combine the TaxJar API with Google Calendar in Pipedream to create workflows that remind you to file taxes before the due date. When TaxJar reports a nearing filing deadline, the workflow can create an event in your Google Calendar as a reminder. diff --git a/components/td_ameritrade/README.md b/components/td_ameritrade/README.md new file mode 100644 index 0000000000000..05382953c44dd --- /dev/null +++ b/components/td_ameritrade/README.md @@ -0,0 +1,11 @@ +# Overview + +The TD Ameritrade API offers access to a brokerage's suite of trading services. With it, you can retrieve market data, manage accounts, place trades, and get updates on orders and positions. Integrating the TD Ameritrade API with Pipedream allows for the automation of various trading strategies and the syncing of financial data with other services, all in a serverless environment. + +# Example Use Cases + +- **Automated Trading System**: Create a Pipedream workflow that reacts to specific market conditions by placing trades on TD Ameritrade. Use data from other sources to inform these conditions, like a stock reaching a certain price, or a news alert from a service like RSS or Twitter. + +- **Portfolio Synchronization**: Build a workflow that periodically fetches your portfolio information from TD Ameritrade and syncs it with your preferred spreadsheet app, like Google Sheets, to maintain an up-to-date overview of your investments. + +- **Alerts and Notifications**: Set up a Pipedream workflow to monitor your TD Ameritrade account for key events like executed trades or dividends received, and then send notifications through email, SMS, or messaging apps like Slack to keep you informed in real-time. diff --git a/components/teach_n_go/actions/create-prospect/create-prospect.mjs b/components/teach_n_go/actions/create-prospect/create-prospect.mjs new file mode 100644 index 0000000000000..0a9a288d76ab4 --- /dev/null +++ b/components/teach_n_go/actions/create-prospect/create-prospect.mjs @@ -0,0 +1,87 @@ +import app from "../../teach_n_go.app.mjs"; + +export default { + key: "teach_n_go-create-prospect", + name: "Create Prospect", + description: "Creates a new prospect inside Teach 'n Go. [See the documentation](https://intercom.help/teach-n-go/en/articles/5750592-prospect-registration-api)", + version: "0.0.1", + type: "action", + props: { + app, + firstName: { + propDefinition: [ + app, + "firstName", + ], + }, + lastName: { + propDefinition: [ + app, + "lastName", + ], + }, + mobilePhone: { + type: "integer", + label: "Mobile Phone", + description: "The prospect's contact number.", + optional: true, + }, + emailAddress: { + propDefinition: [ + app, + "emailAddress", + ], + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "General information you wish to capture.", + optional: true, + }, + gender: { + propDefinition: [ + app, + "gender", + ], + optional: true, + }, + dateOfBirth: { + propDefinition: [ + app, + "dateOfBirth", + ], + optional: true, + }, + courseSubject: { + type: "string", + label: "Course Subject", + description: "The students chosen subject.", + optional: true, + }, + courseLevel: { + type: "string", + label: "Course Level", + description: "The students chosen level.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.app.createProspect({ + $, + data: { + "fname": this.firstName, + "lname": this.lastName, + "mobile_phone": this.mobilePhone, + "email_address": this.emailAddress, + "description": this.description, + "gender": this.gender, + "date_of_birth": this.dateOfBirth, + "course_subject": this.courseSubject, + "course_level": this.courseLevel, + }, + }); + $.export("$summary", `Successfully created prospect with ID: ${response.data.id}`); + return response; + }, +}; diff --git a/components/teach_n_go/actions/create-student/create-student.mjs b/components/teach_n_go/actions/create-student/create-student.mjs new file mode 100644 index 0000000000000..c3ce761888919 --- /dev/null +++ b/components/teach_n_go/actions/create-student/create-student.mjs @@ -0,0 +1,194 @@ +import { PAYMENT_METHOD_OPTIONS } from "../../common/constants.mjs"; +import app from "../../teach_n_go.app.mjs"; + +export default { + key: "teach_n_go-create-student", + name: "Create Student", + description: "Registers a new student in Teach 'n Go. [See the documentation](https://intercom.help/teach-n-go/en/articles/6807235-new-student-and-class-registration-api)", + version: "0.0.1", + type: "action", + props: { + app, + firstName: { + propDefinition: [ + app, + "firstName", + ], + description: "The student's first name.", + }, + lastName: { + propDefinition: [ + app, + "lastName", + ], + description: "The student's last name.", + }, + gender: { + propDefinition: [ + app, + "gender", + ], + description: "The student's gender.", + optional: true, + }, + registrationDate: { + type: "string", + label: "Registration Date", + description: "The student's registration date. **Format: YYYY-MM-DD**", + optional: true, + }, + dateOfBirth: { + propDefinition: [ + app, + "dateOfBirth", + ], + description: "The student's date of birth. **Format: YYYY-MM-DD**", + optional: true, + }, + identificationNumber: { + type: "string", + label: "Identification Number", + description: "The external number to identify the student.", + optional: true, + }, + preferredPaymentMethod: { + type: "integer", + label: "Preferred Payment Method", + description: "The payment method the student want to use.", + options: PAYMENT_METHOD_OPTIONS, + optional: true, + }, + discountPercentage: { + type: "string", + label: "Discount Percentage", + description: "The discount percentage on the payment amount.", + optional: true, + }, + mobilePhoneCode: { + type: "integer", + label: "Mobile Phone Code", + description: "The region code of the mobile phone. Min length: 2, Max length: 4", + optional: true, + }, + mobilePhone: { + type: "integer", + label: "Mobile Phone", + description: "The student's mobile phone", + optional: true, + }, + homePhoneCode: { + type: "integer", + label: "Home Phone Code", + description: "The region code of the home phone. Min length: 2, Max length: 4", + optional: true, + }, + homePhone: { + type: "integer", + label: "Home Phone", + description: "The student's home phone", + optional: true, + }, + emailAddress: { + propDefinition: [ + app, + "emailAddress", + ], + description: "The student's email address.", + optional: true, + }, + streetNameAndNumber: { + type: "string", + label: "Street Name And Number", + description: "The student's full address.", + optional: true, + }, + flatFloor: { + type: "string", + label: "Flat Floor", + description: "The student's address flat floor if it exists.", + optional: true, + }, + area: { + type: "string", + label: "Area", + description: "The student's address area.", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "The student's city.", + optional: true, + }, + postcode: { + type: "string", + label: "Postcode", + description: "The student's postcode.", + optional: true, + }, + countryCode: { + type: "string", + label: "Country Code", + description: "The student's ISO 2 letter country code, e.g. (US, UK, IN).", + optional: true, + }, + generalNotes: { + type: "string", + label: "General Notes", + description: "Some student's additional notes.", + optional: true, + }, + medicalNotes: { + type: "string", + label: "Medical Notes", + description: "Some student's additional medical notes.", + optional: true, + }, + courses: { + propDefinition: [ + app, + "courses", + ], + optional: true, + }, + enrolmentDate: { + type: "string", + label: "Enrolment Date", + description: "The date of the student's enrolment.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.app.registerStudent({ + $, + data: { + fname: this.firstName, + lname: this.lastName, + gender: this.gender, + registration_date: this.registrationDate, + date_of_birth: this.dateOfBirth, + identification_number: this.identificationNumber, + preferred_payment_method: this.preferredPaymentMethod, + discount_percentage: this.discountPercentage && parseFloat(this.discountPercentage), + mobile_phone_code: this.mobilePhoneCode, + mobile_phone: this.mobilePhone, + home_phone_code: this.homePhoneCode, + home_phone: this.homePhone, + email_address: this.emailAddress, + street_name_and_number: this.streetNameAndNumber, + flat_floor: this.flatFloor, + area: this.area, + city: this.city, + postcode: this.postcode, + country_code: this.countryCode, + general_notes: this.generalNotes, + medical_notes: this.medicalNotes, + courses: this.courses, + enrolment_date: this.enrolmentDate, + }, + }); + + $.export("$summary", `Successfully registered student with ID: ${response.data.id}`); + return response; + }, +}; diff --git a/components/teach_n_go/common/constants.mjs b/components/teach_n_go/common/constants.mjs new file mode 100644 index 0000000000000..f8b20bd4ccc75 --- /dev/null +++ b/components/teach_n_go/common/constants.mjs @@ -0,0 +1,30 @@ +export const LIMIT = 100; + +export const GENDER_OPTIONS = [ + "Female", + "Male", + "Not Specified", +]; + +export const PAYMENT_METHOD_OPTIONS = [ + { + label: "Cash", + value: 1, + }, + { + label: "Cheque", + value: 2, + }, + { + label: "Credit Card", + value: 3, + }, + { + label: "Bank Transfer", + value: 4, + }, + { + label: "Direct Debit", + value: 5, + }, +]; diff --git a/components/teach_n_go/common/utils.mjs b/components/teach_n_go/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/teach_n_go/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/teach_n_go/package.json b/components/teach_n_go/package.json new file mode 100644 index 0000000000000..4db442120a99c --- /dev/null +++ b/components/teach_n_go/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/teach_n_go", + "version": "0.1.0", + "description": "Pipedream Teach 'n Go Components", + "main": "teach_n_go.app.mjs", + "keywords": [ + "pipedream", + "teach_n_go" + ], + "homepage": "https://pipedream.com/apps/teach_n_go", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/teach_n_go/sources/common/base.mjs b/components/teach_n_go/sources/common/base.mjs new file mode 100644 index 0000000000000..6eed1508280e6 --- /dev/null +++ b/components/teach_n_go/sources/common/base.mjs @@ -0,0 +1,59 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import app from "../../teach_n_go.app.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + async emitEvent(maxResults = false) { + const lastId = this._getLastId(); + + const response = this.app.paginate({ + fn: this.getFunction(), + }); + + let responseArray = []; + for await (const item of response) { + if (item.id <= lastId) break; + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastId(responseArray[0].id); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item.created || new Date()), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/teach_n_go/sources/new-class/new-class.mjs b/components/teach_n_go/sources/new-class/new-class.mjs new file mode 100644 index 0000000000000..e3dc8d48985b8 --- /dev/null +++ b/components/teach_n_go/sources/new-class/new-class.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "teach_n_go-new-class", + name: "New Class Created", + description: "Emit new event when a class is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.app.listCourses; + }, + getSummary(item) { + return `New Class: ${item.course_full_title}`; + }, + }, + sampleEmit, +}; diff --git a/components/teach_n_go/sources/new-class/test-event.mjs b/components/teach_n_go/sources/new-class/test-event.mjs new file mode 100644 index 0000000000000..6c4f7457f87bf --- /dev/null +++ b/components/teach_n_go/sources/new-class/test-event.mjs @@ -0,0 +1,46 @@ +export default { + "course_title": "Class title", + "course_subject": "Course subject", + "course_level": "Course level", + "school_id": 16385, + "start_date": "2024-10-21", + "end_date": "2024-10-28", + "recurrence": "weekly", + "payment_frequency": "free", + "id": 153319, + "photo": null, + "payment_fee": "0", + "course_description": "", + "course_full_title": "Class title [Online Lesson]", + "created": "2024-10-17 18:20:13", + "modified": "2024-10-17 18:20:15", + "color": "color1", + "course_started": false, + "course_ended": false, + "course_status": 1, + "num_enrolled_students": 0, + "teachers": "66792", + "classrooms": "39627", + "billing_month_start_date": "2024-10-01", + "billing_month_end_date": "2024-10-01", + "custom_payments": null, + "archived": false, + "awarding_body": "", + "course_code": "", + "book_code": "", + "total_lessons": 2, + "total_lessons_hrs": "02:00", + "skype_meeting_link": "", + "year": null, + "credit_hours": "", + "class_type": "", + "is_ended": null, + "teacher_hourly_fees": null, + "is_booking_class": false, + "subscription_plan_id": null, + "is_stripe_sub_allow": 0, + "created_by": 66114, + "modified_by": 66114, + "exception_dates": null, + "removed_exception_dates": null +} \ No newline at end of file diff --git a/components/teach_n_go/sources/new-student/new-student.mjs b/components/teach_n_go/sources/new-student/new-student.mjs new file mode 100644 index 0000000000000..5fe9a2abeb76f --- /dev/null +++ b/components/teach_n_go/sources/new-student/new-student.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "teach_n_go-new-student", + name: "New Student Registration", + description: "Emit new event when a new student is registered.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.app.listStudents; + }, + getSummary({ + fname, lname, email_address: email, + }) { + return `New Student: ${fname} ${lname}${email + ? ` - ${email}` + : ""}`; + }, + }, + sampleEmit, +}; diff --git a/components/teach_n_go/sources/new-student/test-event.mjs b/components/teach_n_go/sources/new-student/test-event.mjs new file mode 100644 index 0000000000000..a8692d2b06884 --- /dev/null +++ b/components/teach_n_go/sources/new-student/test-event.mjs @@ -0,0 +1,51 @@ +export default { + "id": 411081, + "school_id": 16385, + "identification_number": "123", + "fname": "Jhon", + "lname": "Doe", + "gender": "Male", + "registration_date": "2024-10-17", + "date_of_birth": "1991-02-02", + "email_address": "jhon@email.com", + "mobile_phonecode": "", + "mobile_phone": "", + "home_phonecode": "", + "home_phone": "", + "street_name_and_number": "", + "flat_floor": "", + "area": "", + "city": "", + "country_code": "", + "postcode": "", + "whole_address": "", + "general_notes": "", + "medical_notes": "", + "delayed_payments": "", + "num_enrolled_courses": 1, + "discount_percentage": "10", + "discount_type": "percentage", + "custom_fields": [], + "timezone": null, + "course_subject": null, + "course_level": null, + "photo_link": "/img/user-no-img-m.jpg", + "preferred_payment_method": "Credit Card", + "classes": [ + { + "course_title": "class title", + "course_full_title": "class title [Online Lesson]", + "course_subject": "", + "course_level": "", + "course_description": "", + "course_started": true, + "course_ended": false, + "school_id": 16385, + "recurrence": "weekly", + "payment_frequency": "free", + "enrolment_date": "2024-10-20", + "unenrolment_date": null, + "student_id": 411081 + } + ] +} \ No newline at end of file diff --git a/components/teach_n_go/teach_n_go.app.mjs b/components/teach_n_go/teach_n_go.app.mjs new file mode 100644 index 0000000000000..e86422f456a65 --- /dev/null +++ b/components/teach_n_go/teach_n_go.app.mjs @@ -0,0 +1,122 @@ +import { axios } from "@pipedream/platform"; +import { GENDER_OPTIONS } from "./common/constants.mjs"; + +export default { + type: "app", + app: "teach_n_go", + propDefinitions: { + firstName: { + type: "string", + label: "First Name", + description: "The prospect's first name.", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The prospect's surname.", + }, + gender: { + type: "string", + label: "Gender", + description: "The prospect's gender.", + options: GENDER_OPTIONS, + }, + dateOfBirth: { + type: "string", + label: "Date Of Birth", + description: "The prospect's date of birth. **Format: YYYY-MM-DD**", + }, + emailAddress: { + type: "string", + label: "Email Address", + description: "The prospect's email address.", + }, + courses: { + type: "integer[]", + label: "Courses", + description: "The student's course Ids.", + async options() { + const { data } = await this.listCourses(); + + return data.map(({ + course_full_title: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://app.teachngo.com"; + }, + _headers(headers = {}) { + return { + "X-API-KEY": `${this.$auth.api_key}`, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(headers), + ...opts, + }); + }, + createProspect(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/LeadsApi/add", + ...opts, + }); + }, + registerStudent(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/api/student", + ...opts, + }); + }, + listCourses(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/globalApis/course_list", + ...opts, + }); + }, + listStudents(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/globalApis/student_list", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, ...opts + }) { + let hasMore = false; + let page = 0; + + do { + params.page = ++page; + const { + data, + next, + } = await fn({ + params, + ...opts, + }); + + for (const d of data) { + yield d; + } + + hasMore = next; + + } while (hasMore); + }, + }, +}; diff --git a/components/teachable/README.md b/components/teachable/README.md new file mode 100644 index 0000000000000..b79b1478789fe --- /dev/null +++ b/components/teachable/README.md @@ -0,0 +1,11 @@ +# Overview + +The Teachable API opens up possibilities for automating and enhancing online course interactions and management. Using Pipedream, you can weave together workflows that interact with Teachable events, users, and courses. You can automate enrollment processes, synchronize course data with external systems, and engage students with timely notifications. Pipedream's serverless platform lets you create these workflows with minimal setup, leveraging its ability to trigger actions based on Teachable events or on a schedule, and to integrate with hundreds of other apps. + +# Example Use Cases + +- **Automated Enrollment Notifications**: Trigger a workflow whenever a new user enrolls in a Teachable course. Use Pipedream to send a personalized welcome email through SendGrid, thanking them for signing up and providing them with starter resources. + +- **Course Progression Monitoring**: Set up a workflow that monitors student progress and triggers when a student completes a course section. The action could be to congratulate them via Twilio SMS and unlock a bonus resource, or to add them to a segmented email campaign in Mailchimp for advanced topics. + +- **Synchronize Teachable Data with Google Sheets**: Create a workflow that runs on a schedule to fetch new course sign-ups and completion data. Use this data to update a Google Sheets spreadsheet, giving you a live dashboard of student engagement and course performance. diff --git a/components/team_sms/actions/get-phone-numbers-owned/get-phone-numbers-owned.mjs b/components/team_sms/actions/get-phone-numbers-owned/get-phone-numbers-owned.mjs new file mode 100644 index 0000000000000..48c0fd55a78cc --- /dev/null +++ b/components/team_sms/actions/get-phone-numbers-owned/get-phone-numbers-owned.mjs @@ -0,0 +1,25 @@ +import app from "../../team_sms.app.mjs"; + +export default { + key: "team_sms-get-phone-numbers-owned", + name: "Get Phone Numbers Owned", + description: "Retrieve all phone numbers owned by the user. [See the documentation](https://teamsms.io/api-doc)", + version: "0.0.1", + type: "action", + props: { + app, + }, + methods: { + getPhoneNumbersOwned(args = {}) { + return this.app._makeRequest({ + path: "/phone-numbers-owned", + ...args, + }); + }, + }, + async run({ $ }) { + const response = await this.getPhoneNumbersOwned(); + $.export("$summary", `Successfully retrieved \`${response.length}\` phone numbers`); + return response; + }, +}; diff --git a/components/team_sms/package.json b/components/team_sms/package.json new file mode 100644 index 0000000000000..abf9647249ee2 --- /dev/null +++ b/components/team_sms/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/team_sms", + "version": "0.1.0", + "description": "Pipedream Team SMS Components", + "main": "team_sms.app.mjs", + "keywords": [ + "pipedream", + "team_sms" + ], + "homepage": "https://pipedream.com/apps/team_sms", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/team_sms/sources/new-sms-received/new-sms-received.mjs b/components/team_sms/sources/new-sms-received/new-sms-received.mjs new file mode 100644 index 0000000000000..f4da203d2a34d --- /dev/null +++ b/components/team_sms/sources/new-sms-received/new-sms-received.mjs @@ -0,0 +1,53 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import app from "../../team_sms.app.mjs"; + +export default { + key: "team_sms-new-sms-received", + name: "New SMS Received", + description: "Emit new event for each new SMS received. [See the documentation](https://teamsms.io/api-doc)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + listSms(args = {}) { + return this.app._makeRequest({ + debug: true, + path: "/sms", + ...args, + }); + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New SMS: ${resource.id}`, + ts: Date.parse(resource.created_at), + }; + }, + }, + async run() { + const { + listSms, + generateMeta, + } = this; + + const resources = await listSms(); + + Array.from(resources) + .reverse() + .forEach((resource) => { + this.$emit(resource, generateMeta(resource)); + }); + }, +}; diff --git a/components/team_sms/team_sms.app.mjs b/components/team_sms/team_sms.app.mjs new file mode 100644 index 0000000000000..0b3a88f9af56f --- /dev/null +++ b/components/team_sms/team_sms.app.mjs @@ -0,0 +1,24 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "team_sms", + methods: { + _baseUrl() { + return "https://teamsms.io/api"; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this._baseUrl() + path, + headers: { + ...headers, + "X-API-KEY": this.$auth.api_key, + "Accept": "application/json", + }, + }); + }, + }, +}; diff --git a/components/team_up/README.md b/components/team_up/README.md new file mode 100644 index 0000000000000..6368442fcaa61 --- /dev/null +++ b/components/team_up/README.md @@ -0,0 +1,11 @@ +# Overview + +The Team Up API allows you to interact with your Team Up calendars programmatically, enabling the automation of event creation, updates, and deletion, as well as the retrieval of calendar data. By integrating the Team Up API with Pipedream, you can build workflows that trigger on specific events, automate tasks, and connect your calendar data with countless other services, bringing efficiency to team coordination and event management. + +# Example Use Cases + +- **Automated Event Creation from Emails**: Create a Pipedream workflow that triggers when a new email arrives in your Gmail inbox with a specific subject line. Extract event details from the email and use the Team Up API to add a new event to your calendar. + +- **Slack Notifications for Upcoming Events**: Set up a Pipedream workflow that runs periodically, checks your Team Up calendar for upcoming events within the next 24 hours, and posts reminders in a designated Slack channel, keeping your team informed and prepared. + +- **Syncing Team Up with Google Calendar**: Build a workflow on Pipedream that triggers when a new event is added to your Team Up calendar. Use this trigger to then create a corresponding event in Google Calendar, ensuring that your schedules are synchronized across different calendar platforms. diff --git a/components/teamcamp/README.md b/components/teamcamp/README.md new file mode 100644 index 0000000000000..5237fbdd0e9ff --- /dev/null +++ b/components/teamcamp/README.md @@ -0,0 +1,11 @@ +# Overview + +Teamcamp API facilitates seamless collaboration and project management by providing access to task, project, and team management functionalities programmatically. This API can be leveraged on Pipedream to automate routine operations, synchronize project data across multiple platforms, and enhance team productivity through custom integrations and automated workflows. With Teamcamp API on Pipedream, you can design workflows that trigger on specific actions within Teamcamp, manipulate data, and interact with other apps to streamline complex business processes. + +# Example Use Cases + +- **Project Status Updates to Slack**: Automatically send updates to a designated Slack channel whenever a project status changes in Teamcamp. This workflow can keep remote teams informed in real-time about project progress without manual reporting. + +- **Task Creation from Emails**: Create tasks in Teamcamp directly from incoming emails using the Gmail app on Pipedream. This can be particularly useful for support teams that need to convert support requests into actionable tasks without leaving their email inbox. + +- **Daily Project Summary to Email**: Schedule a daily workflow on Pipedream that gathers all task updates and project changes from Teamcamp and compiles them into a summary email. This email can be sent to all project stakeholders to keep them updated, reducing the need for frequent check-ins and meetings. diff --git a/components/teamcamp/actions/create-project/create-project.mjs b/components/teamcamp/actions/create-project/create-project.mjs new file mode 100644 index 0000000000000..5a3dcc30408ea --- /dev/null +++ b/components/teamcamp/actions/create-project/create-project.mjs @@ -0,0 +1,44 @@ +import app from "../../teamcamp.app.mjs"; + +export default { + key: "teamcamp-create-project", + name: "Create Project", + description: "Create a new project in the Workspace. [See the documentation](https://api.teamcamp.app/api-reference/project/createProject)", + version: "0.0.1", + type: "action", + props: { + app, + projectName: { + propDefinition: [ + app, + "projectName", + ], + }, + startDate: { + propDefinition: [ + app, + "startDate", + ], + }, + dueDate: { + propDefinition: [ + app, + "dueDate", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createProject({ + $, + data: { + projectName: this.projectName, + startDate: this.startDate, + dueDate: this.dueDate, + }, + }); + + $.export("$summary", `Successfully created project with ID: '${response.projectId}'`); + + return response; + }, +}; diff --git a/components/teamcamp/actions/create-task/create-task.mjs b/components/teamcamp/actions/create-task/create-task.mjs new file mode 100644 index 0000000000000..ccf84a481ca63 --- /dev/null +++ b/components/teamcamp/actions/create-task/create-task.mjs @@ -0,0 +1,51 @@ +import app from "../../teamcamp.app.mjs"; + +export default { + key: "teamcamp-create-task", + name: "Create Task", + description: "Create a new task in the Project. [See the documentation](https://api.teamcamp.app/api-reference/task/createTask)", + version: "0.0.1", + type: "action", + props: { + app, + projectId: { + propDefinition: [ + app, + "projectId", + ], + }, + taskName: { + propDefinition: [ + app, + "taskName", + ], + }, + description: { + propDefinition: [ + app, + "description", + ], + }, + dueDate: { + propDefinition: [ + app, + "dueDate", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createTask({ + $, + data: { + projectId: this.projectId, + taskName: this.taskName, + description: this.description, + dueDate: this.dueDate, + }, + }); + + $.export("$summary", `Successfully created task with ID '${response.taskId}'`); + + return response; + }, +}; diff --git a/components/teamcamp/package.json b/components/teamcamp/package.json new file mode 100644 index 0000000000000..ed01f54c4526e --- /dev/null +++ b/components/teamcamp/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/teamcamp", + "version": "0.1.0", + "description": "Pipedream Teamcamp Components", + "main": "teamcamp.app.mjs", + "keywords": [ + "pipedream", + "teamcamp" + ], + "homepage": "https://pipedream.com/apps/teamcamp", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/teamcamp/teamcamp.app.mjs b/components/teamcamp/teamcamp.app.mjs new file mode 100644 index 0000000000000..0f5265737ccff --- /dev/null +++ b/components/teamcamp/teamcamp.app.mjs @@ -0,0 +1,108 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "teamcamp", + propDefinitions: { + projectName: { + type: "string", + label: "Project Name", + description: "The Name of the project", + }, + startDate: { + type: "string", + label: "Start Date", + description: "The start date of the project, We expect yyyy-MM-dd", + }, + dueDate: { + type: "string", + label: "Due Date", + description: "The due date of the project, We expect yyyy-MM-dd", + }, + taskName: { + type: "string", + label: "Task Name", + description: "The Name of the task", + }, + description: { + type: "string", + label: "Description", + description: "Description of the task", + }, + priority: { + type: "string", + label: "Priority", + description: "The priority level of the task", + optional: true, + options: [ + "No Priority", + "Urgent", + "High", + "Medium", + "Low", + ], + }, + estimateTime: { + type: "string", + label: "Estimate Time", + description: "Estimated time required for the task (hh)", + optional: true, + }, + projectId: { + type: "string", + label: "Project ID", + description: "The ID of the project", + async options() { + const projectsIds = await this.listProjects(); + + return projectsIds.map(({ + projectId, projectName, + }) => ({ + value: projectId, + label: projectName, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.teamcamp.app/v1.0"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "apiKey": `${this.$auth.api_key}`, + }, + }); + }, + async createProject(args = {}) { + return this._makeRequest({ + method: "post", + path: "/project", + ...args, + }); + }, + async createTask(args = {}) { + return this._makeRequest({ + method: "post", + path: "/task", + ...args, + }); + }, + async listProjects(args = {}) { + return this._makeRequest({ + path: "/project", + ...args, + }); + }, + }, +}; diff --git a/components/teamdeck/README.md b/components/teamdeck/README.md index 36fdf40715f26..40ffec96a81ae 100644 --- a/components/teamdeck/README.md +++ b/components/teamdeck/README.md @@ -1,22 +1,11 @@ # Overview -With Teamdeck's API you can rapidly build powerful applications to help you -manage your workforce. With its RESTful interface it makes it easy to access -Teamdeck's resources such as contact management, scheduling, reporting, and -more. This allows you to quickly create applications that streamline and -automate the way you manage your workforce. +Teamdeck offers a versatile API that allows you to manage and automate aspects of resource management, like booking, time tracking, and reporting. With the Teamdeck API on Pipedream, you can create custom workflows to connect Teamdeck’s resource scheduling capabilities with other apps to streamline project management, automate timesheet collection, monitor team availability, and generate analytics. It's perfect for managers who want to optimize their team's productivity and for teams who aim to improve their operational workflows. -Here are just a few of the useful applications that you can build using -Teamdeck's API: +# Example Use Cases -- Scheduling Applications: Create applications that simplify creating, viewing, - and editing team schedules. -- Contact Management: Build applications to store and manage contact details of - employees and clients. -- Automated Reporting: Create automated reports on employment earnings and - hours worked. -- Time Tracking: Build applications that track hours worked by employees. -- Payroll Management: Create applications to manage payrolls for employees and - contractors. -- Recruiting Applications: Develop applications to manage recruitment and - onboarding of new staff. +- **Automated Timesheet Collection**: Set up a workflow that triggers weekly, automatically fetching timesheets from Teamdeck and sending them to your accounting software, such as QuickBooks, for payroll processing. This can save time and reduce manual entry errors. + +- **Project Management Integration**: Create a workflow that connects Teamdeck with project management tools like Trello or Asana. When a new project is created in your project management tool, automatically create corresponding bookings in Teamdeck to reserve time for your team members. + +- **Team Availability Notification**: Build a workflow that monitors your team’s availability in Teamdeck and sends alerts via Slack or email when a team member is overbooked or has free capacity. This can help in reallocating resources efficiently and ensuring a balanced workload. diff --git a/components/teamgantt/README.md b/components/teamgantt/README.md new file mode 100644 index 0000000000000..eab6233a8dab8 --- /dev/null +++ b/components/teamgantt/README.md @@ -0,0 +1,11 @@ +# Overview + +The TeamGantt API allows you to interact programmatically with your TeamGantt project management data. With it, you can create, read, update, and delete various project elements, such as tasks, milestones, and time entries. Integrating the TeamGantt API with Pipedream enables you to automate your project workflows, sync data across platforms, and react to events in real-time. By leveraging Pipedream's capabilities, you can connect TeamGantt to a myriad of other apps and services to streamline your project management processes. + +# Example Workflows + +- **Sync Project Tasks with Google Calendar**: Automatically create events in Google Calendar whenever a new task is added to a TeamGantt project. This ensures your scheduling remains coherent across both platforms and helps with time management without manual entry. + +- **Update Slack Status Based on Task Deadlines**: Set up a workflow to monitor your TeamGantt tasks for upcoming deadlines. When a deadline is near, automatically update your Slack status to reflect your focus on the impending task, helping your team know when you're busy. + +- **Email Reporting for Completed Milestones**: When a milestone is marked as completed in TeamGantt, trigger an automated email to stakeholders with the details of the completed milestone. This keeps everyone informed and can trigger subsequent phases of a project. diff --git a/components/teamgate/README.md b/components/teamgate/README.md index 159ceb11746a7..4fe62df265be3 100644 --- a/components/teamgate/README.md +++ b/components/teamgate/README.md @@ -1,17 +1,11 @@ # Overview -The Teamgate API is the perfect tool for developers to create custom -applications and integrations with the Teamgate CRM. With the Teamgate API, you -can build a wide range of custom tools or integrations to optimize your use of -Teamgate. Here are a few examples of what you can build with the Teamgate API: +Teamgate's API provides the ability to manage a sales pipeline, customer data, and interactions within the Teamgate CRM system. By leveraging the Teamgate API on Pipedream, you can automate various aspects of the sales process, synchronize customer information across platforms, and generate insights from sales data. With Pipedream's serverless integration capabilities, connecting Teamgate to other services becomes a streamlined process, enabling users to create custom workflows that trigger actions, process data, and manage tasks without manual intervention. -- Automate prospecting tasks, like creating contacts and deals in bulk and - updating existing records -- Sync customer data between Teamgate and other external systems, like an - ecommerce store or an email service -- Create custom dashboards that include Teamgate data -- Build custom workflows, like generating invoices and sending them to - customers -- Create custom reports and graphs to draw insights from data -- Create custom notifications to alert the team of important events -- Create a data migration tool to transition from one CRM system to Teamgate +# Example Use Cases + +- **Sales Lead Scoring and Distribution:** Automate the process of scoring sales leads based on custom criteria and distribute them to the appropriate sales rep. Using Teamgate's API on Pipedream, you can evaluate new lead information, score them, and assign them to team members in Teamgate based on their score, location, or product interest. + +- **Customer Data Enrichment:** Enhance customer profiles in Teamgate by integrating with enrichment services like Clearbit. As new contacts are added to Teamgate, trigger a workflow on Pipedream to fetch additional data points such as company size, industry, and social profiles, then update the corresponding records in Teamgate to ensure sales reps have rich, actionable data. + +- **Automated Deal Follow-ups:** Set up a workflow that sends personalized follow-up emails or SMS messages to prospects after a deal reaches a certain stage in Teamgate. Connect Teamgate to email platforms like SendGrid or messaging services like Twilio on Pipedream to automate communication, keeping deals moving and maintaining engagement without manual effort. diff --git a/components/teamioo/README.md b/components/teamioo/README.md new file mode 100644 index 0000000000000..f64e9f6b91f01 --- /dev/null +++ b/components/teamioo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Teamioo API provides a suite of tools to manage teamwork and project progression. With this API, you can automate your workflow, synchronize data across various platforms, and streamline project management tasks. On Pipedream, you can integrate Teamioo with other apps to create multi-step workflows, perform operations based on triggers, and manipulate data to fit your project management needs. + +# Example Use Cases + +- **Automated Project Reporting**: Set up a workflow that triggers every week to fetch project updates from Teamioo and compiles a report. This report is then automatically sent to stakeholders via email using the Gmail app on Pipedream. + +- **Task Synchronization with External Tools**: Create a workflow where new tasks created in Teamioo trigger the creation of corresponding issues in a GitHub repository. This keeps development work in sync with project management. + +- **Time Tracking Integration**: Configure a workflow where time entries submitted in Teamioo are sent to a time tracking tool like Toggl to centralize time management across different platforms. diff --git a/components/teamioo/actions/create-bookmark/create-bookmark.mjs b/components/teamioo/actions/create-bookmark/create-bookmark.mjs new file mode 100644 index 0000000000000..3689cededc5c1 --- /dev/null +++ b/components/teamioo/actions/create-bookmark/create-bookmark.mjs @@ -0,0 +1,99 @@ +import teamioo from "../../teamioo.app.mjs"; + +export default { + key: "teamioo-create-bookmark", + name: "Create Bookmark", + description: "Saves a website URL to the bookmarks. The 'url' and 'bookmark_type' are required. 'bookmark_type' can either be 'personal' or 'group'. An optional prop 'title' can be included to give the bookmark a custom name. [See the documentation](https://demo.teamioo.com/teamiooapi)", + version: "0.0.1", + type: "action", + props: { + teamioo, + bookmarkType: { + type: "string", + label: "Event Type", + description: "The type of the bookmark, either 'personal' or 'group'", + options: [ + "personal", + "group", + ], + reloadProps: true, + }, + url: { + type: "string", + label: "URL", + description: "The URL to bookmark", + }, + title: { + type: "string", + label: "Title", + description: "Uses bookmared website's title if omitted.", + optional: true, + }, + comment: { + type: "string", + label: "Comment", + description: "Bookmark comment (markdown supported).", + optional: true, + }, + taggedUsers: { + propDefinition: [ + teamioo, + "userId", + ], + type: "string[]", + label: "Tagged Users", + description: "Tagged users.", + optional: true, + }, + tags: { + propDefinition: [ + teamioo, + "tags", + ], + optional: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.bookmarkType === "group") { + props.groupId = { + type: "string", + label: "Group ID", + description: "The ID of the group.", + options: async () => { + const groups = await this.teamioo.listGroups(); + + return groups.map(({ + displayName: label, _id: value, + }) => ({ + label, + value, + })); + }, + }; + } + + return props; + }, + async run({ $ }) { + const { + teamioo, + bookmarkType, + groupId, + ...data + } = this; + + const response = await teamioo.createBookmark({ + $, + data: { + groupId: (bookmarkType === "personal") + ? "personal" + : groupId, + ...data, + }, + }); + + $.export("$summary", `Successfully saved the bookmark with Id: ${response.newDocId}`); + return response; + }, +}; diff --git a/components/teamioo/actions/create-calendar-event/create-calendar-event.mjs b/components/teamioo/actions/create-calendar-event/create-calendar-event.mjs new file mode 100644 index 0000000000000..64470c5e22242 --- /dev/null +++ b/components/teamioo/actions/create-calendar-event/create-calendar-event.mjs @@ -0,0 +1,161 @@ +import teamioo from "../../teamioo.app.mjs"; + +export default { + key: "teamioo-create-calendar-event", + name: "Create Calendar Event", + description: "Creates a new calendar event. [See the documentation](https://demo.teamioo.com/teamiooapi)", + version: "0.0.1", + type: "action", + props: { + teamioo, + eventType: { + type: "string", + label: "Event Type", + description: "The type of the event, either 'personal', 'group' or 'office'", + options: [ + "personal", + "group", + "office", + ], + reloadProps: true, + }, + title: { + type: "string", + label: "Title", + description: "Event title.", + }, + start: { + type: "string", + label: "Start", + description: "Start DATE of the event. Time fragment of this datetime is ignored.", + }, + end: { + type: "string", + label: "End", + description: "End DATE of the event. Time fragment of this datetime is ignored.", + optional: true, + }, + startTime: { + type: "string", + label: "Start Time", + description: "If present, time part of datetime indicates start time of the event. If omitted, the event is considered to be an \"all day\" event.", + optional: true, + }, + endTime: { + type: "string", + label: "End Time", + description: "If present, time part of datetime indicates end time of the event. StartTime parameter must be present as well.", + optional: true, + }, + note: { + type: "string", + label: "Note", + description: "The event note (markdown supported).", + optional: true, + }, + privacyLevel: { + type: "string", + label: "Privacy Level", + description: "Visibility of the event. This does not apply to \"group\" calendar events.", + options: [ + "public", + "private", + "secret", + ], + optional: true, + }, + location: { + type: "string", + label: "Location", + description: "Where the event takes place.", + optional: true, + }, + invitedUsers: { + propDefinition: [ + teamioo, + "userId", + ], + type: "string[]", + label: "Invited Users", + description: "Users invited to participate in the event. Each invited user receives an notification which he/she has to accept or reject.", + optional: true, + }, + connectedUsers: { + propDefinition: [ + teamioo, + "userId", + ], + type: "string[]", + label: "Connected Users", + description: "Users connected to this event. Combines well with calEventType. Only for \"office\" events.", + optional: true, + }, + taggedUsers: { + propDefinition: [ + teamioo, + "userId", + ], + type: "string[]", + label: "Tagged Users", + description: "Tagged users - ignored in private tasks.", + optional: true, + }, + tags: { + propDefinition: [ + teamioo, + "tags", + ], + optional: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.eventType === "group") { + props.groupId = { + type: "string", + label: "Group ID", + description: "The ID of the group.", + options: async () => { + const groups = await this.teamioo.listGroups(); + + return groups.map(({ + displayName: label, _id: value, + }) => ({ + label, + value, + })); + }, + }; + } + if (this.eventType === "office") { + props.calEventType = { + type: "string[]", + label: "Cal Event Type", + description: "Type of the event from custom defined calendar event types. Only for \"office\" events.", + }; + } + + return props; + }, + async run({ $ }) { + const { + teamioo, + eventType, + groupId, + ...data + } = this; + + const response = await teamioo.createEvent({ + $, + data: { + groupId: (eventType === "personal") + ? "personal" + : groupId, + ...data, + }, + }); + + $.export("$summary", `Successfully created event with Id: ${response.newDocId}`); + return response; + }, +}; diff --git a/components/teamioo/actions/create-task/create-task.mjs b/components/teamioo/actions/create-task/create-task.mjs new file mode 100644 index 0000000000000..0fdd2f5f4e1a3 --- /dev/null +++ b/components/teamioo/actions/create-task/create-task.mjs @@ -0,0 +1,121 @@ +import { PRIORITY_OPTIONS } from "../../common/constants.mjs"; +import teamioo from "../../teamioo.app.mjs"; + +export default { + key: "teamioo-create-task", + name: "Create Task", + description: "Creates a new task in Teamioo. [See the documentation](https://demo.teamioo.com/teamiooapi)", + version: "0.0.1", + type: "action", + props: { + teamioo, + taskType: { + type: "string", + label: "Task Type", + description: "The type of the task, either 'personal' or 'group'", + options: [ + "personal", + "group", + ], + reloadProps: true, + }, + headline: { + type: "string", + label: "Headline", + description: "The name of the task.", + }, + priority: { + type: "string", + label: "Priority", + description: "The urgency of the task.", + options: PRIORITY_OPTIONS, + }, + description: { + type: "string", + label: "Description", + description: "The description of the task (markdown supported).", + optional: true, + }, + hasFirstGroupIndex: { + type: "boolean", + label: "Has First Group Index", + description: "If true, the task will be pushed to the top of tasks in the specified group. Otherwise the next available task group index is used.", + optional: true, + }, + deadline: { + type: "string", + label: "Due Date", + description: "Deadline for finishing the task. Corresponding calendar event will be created in the group calendar.", + optional: true, + }, + assignedUser: { + propDefinition: [ + teamioo, + "userId", + ], + optional: true, + }, + taggedUsers: { + propDefinition: [ + teamioo, + "userId", + ], + type: "string[]", + label: "Tagged Users", + description: "Tagged users - ignored in private tasks.", + optional: true, + }, + tags: { + propDefinition: [ + teamioo, + "tags", + ], + optional: true, + }, + }, + async additionalProps() { + const props = {}; + if (this.taskType === "group") { + props.groupId = { + type: "string", + label: "Group ID", + description: "The ID of the group.", + options: async () => { + const groups = await this.teamioo.listGroups(); + + return groups.map(({ + displayName: label, _id: value, + }) => ({ + label, + value, + })); + }, + }; + } + + return props; + }, + async run({ $ }) { + const { + teamioo, + taskType, + groupId, + priority, + ...data + } = this; + + const response = await teamioo.createTask({ + $, + data: { + groupId: (taskType === "personal") + ? "personal" + : groupId, + priority: parseInt(priority), + ...data, + }, + }); + + $.export("$summary", `Successfully created task: ${response.newDocId}`); + return response; + }, +}; diff --git a/components/teamioo/common/constants.mjs b/components/teamioo/common/constants.mjs new file mode 100644 index 0000000000000..2494ea99d7b61 --- /dev/null +++ b/components/teamioo/common/constants.mjs @@ -0,0 +1,28 @@ +export const PRIORITY_OPTIONS = [ + { + label: "Idea", + value: "0", + }, + { + label: "Very low", + value: "1", + }, + { + label: "Low", + value: "2", + }, + { + label: "Medium", + value: "3", + }, + { + label: "High", + value: "4", + }, + { + label: "Very high", + value: "5", + }, +]; + +export const LIMIT = 100; diff --git a/components/teamioo/package.json b/components/teamioo/package.json new file mode 100644 index 0000000000000..ae1488d4cdf93 --- /dev/null +++ b/components/teamioo/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/teamioo", + "version": "0.1.0", + "description": "Pipedream Teamioo Components", + "main": "teamioo.app.mjs", + "keywords": [ + "pipedream", + "teamioo" + ], + "homepage": "https://pipedream.com/apps/teamioo", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1" + } +} + diff --git a/components/teamioo/sources/common/base.mjs b/components/teamioo/sources/common/base.mjs new file mode 100644 index 0000000000000..b893cc146222d --- /dev/null +++ b/components/teamioo/sources/common/base.mjs @@ -0,0 +1,71 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import teamioo from "../../teamioo.app.mjs"; + +export default { + props: { + teamioo, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + groupId: { + propDefinition: [ + teamioo, + "groupId", + ], + optional: true, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const response = this.teamioo.paginate({ + fn: this.getFunc(), + params: { + groupId: this.groupId + ? this.groupId + : "any", + }, + lastDate, + }); + + let responseArray = []; + + for await (const item of response) { + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastDate(Date.parse(responseArray[0].addedDate || responseArray[0].createdDate)); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item._id, + summary: this.getSummary(item), + ts: Date.parse(item.addedDate || item.createdDate), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/teamioo/sources/new-contact-added/new-contact-added.mjs b/components/teamioo/sources/new-contact-added/new-contact-added.mjs new file mode 100644 index 0000000000000..0b2b6c0f8a3be --- /dev/null +++ b/components/teamioo/sources/new-contact-added/new-contact-added.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "teamioo-new-contact-added", + name: "New Contact Added", + description: "Emit new event when a new contact (client) is added.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunc() { + return this.teamioo.listClients; + }, + getSummary(item) { + return `New client created with Id: ${item._id}`; + }, + }, + sampleEmit, +}; diff --git a/components/teamioo/sources/new-contact-added/test-event.mjs b/components/teamioo/sources/new-contact-added/test-event.mjs new file mode 100644 index 0000000000000..c266f2e0508d4 --- /dev/null +++ b/components/teamioo/sources/new-contact-added/test-event.mjs @@ -0,0 +1,27 @@ +export default { + _id: "6493eb8683b4e54e21fb0a8x", + addedDate: "2023-09-10T10:32:16.456Z", + group: { + _id: "64fd9b30bf3902e3fd5f9dcz", + name: "Teamioo group" + }, + createdBy: { + _id: "64fd9b30bf3902e3fd5f9dc9", + name: "Kamil Michalak" + }, + name: "Business contact", + tags: [ + { + _id: "64fd9b30bf3902e3fd5f9dcb", + originalName: "Teamioo" + }, + { + _id: "64fd9b30bf3902e3fd5f9dca", + originalName: "CMS" + }, + { + _id: "64fd9b30bf3902e3fd5f9dcg", + originalName: "Productivity" + }, + ], +} \ No newline at end of file diff --git a/components/teamioo/sources/new-group-bookmark-url/new-group-bookmark-url.mjs b/components/teamioo/sources/new-group-bookmark-url/new-group-bookmark-url.mjs new file mode 100644 index 0000000000000..18ad175bea4b9 --- /dev/null +++ b/components/teamioo/sources/new-group-bookmark-url/new-group-bookmark-url.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "teamioo-new-group-bookmark-url", + name: "New Group Bookmark URL", + description: "Emit new event when a new URL is bookmarked in a group.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunc() { + return this.teamioo.listBookmarks; + }, + getSummary(item) { + return `New group bookmark URL created with Id: ${item._id}`; + }, + }, + sampleEmit, +}; diff --git a/components/teamioo/sources/new-group-bookmark-url/test-event.mjs b/components/teamioo/sources/new-group-bookmark-url/test-event.mjs new file mode 100644 index 0000000000000..fb3afb5402c90 --- /dev/null +++ b/components/teamioo/sources/new-group-bookmark-url/test-event.mjs @@ -0,0 +1,28 @@ +export default { + _id: "6493eb8683b4e54e21fb0a8x", + url: "https://url.com/", + createdDate: "2023-09-10T10:32:16.456Z", + group: { + _id: "64fd9b30bf3902e3fd5f9dcz", + name: "Teamioo group" + }, + createdBy: { + _id: "64fd9b30bf3902e3fd5f9dc9", + name: "Kamil Michalak" + }, + title: "https://url.com", + tags: [ + { + _id: "64fd9b30bf3902e3fd5f9dcb", + originalName: "Teamioo" + }, + { + _id: "64fd9b30bf3902e3fd5f9dca", + originalName: "CMS" + }, + { + _id: "64fd9b30bf3902e3fd5f9dcg", + originalName: "Productivity" + }, + ], +} \ No newline at end of file diff --git a/components/teamioo/sources/new-group-member/new-group-member.mjs b/components/teamioo/sources/new-group-member/new-group-member.mjs new file mode 100644 index 0000000000000..b9bfe6bb426f6 --- /dev/null +++ b/components/teamioo/sources/new-group-member/new-group-member.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "teamioo-new-group-member", + name: "New Group Member", + description: "Emit new event when a new member is added.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunc() { + return this.teamioo.listGroupMembers; + }, + getSummary(item) { + return `New member added with Id: ${item._id}`; + }, + }, + sampleEmit, +}; diff --git a/components/teamioo/sources/new-group-member/test-event.mjs b/components/teamioo/sources/new-group-member/test-event.mjs new file mode 100644 index 0000000000000..1ce65ecce2990 --- /dev/null +++ b/components/teamioo/sources/new-group-member/test-event.mjs @@ -0,0 +1,16 @@ +export default { + _id: "6493eb8683b4e54e21fb0a8x", + addedDate: "2023-09-10T10:32:16.456Z", + user: { + _id: "64fd9b30bf3902e3fd5f9dcz", + name: "User Name" + }, + group: { + _id: "64fd9b30bf3902e3fd5f9dcz", + name: "Teamioo group" + }, + addedBy: { + _id: null, + name: "Kamil Michalak" + }, +} \ No newline at end of file diff --git a/components/teamioo/teamioo.app.mjs b/components/teamioo/teamioo.app.mjs new file mode 100644 index 0000000000000..195d27ee7e838 --- /dev/null +++ b/components/teamioo/teamioo.app.mjs @@ -0,0 +1,147 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "teamioo", + propDefinitions: { + userId: { + type: "string", + label: "Assigned User", + description: "Id of the user assigned to this task.", + async options() { + const data = await this.listUsers(); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + tags: { + type: "string[]", + label: "Tags", + description: "Teamioo tags.", + async options() { + const data = await this.listTags(); + + return data.map(({ value }) => value); + }, + }, + groupId: { + type: "string", + label: "Group ID", + description: "The ID of the group.", + async options() { + const groups = await this.listGroups(); + + return groups.map(({ + displayName: label, _id: value, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.environment}.teamioo.com/_api`; + }, + _params(params = {}) { + return { + teamiooAPIKey: this.$auth.api_key, + ...params, + }; + }, + _makeRequest({ + $ = this, path, params, ...otherOpts + }) { + return axios($, { + url: this._baseUrl() + path, + params: this._params(params), + ...otherOpts, + }); + }, + listBookmarks(opts = {}) { + return this._makeRequest({ + path: "/bookmarks", + ...opts, + }); + }, + listClients(opts = {}) { + return this._makeRequest({ + path: "/clients", + ...opts, + }); + }, + listGroupMembers(opts = {}) { + return this._makeRequest({ + path: "/groupMembers", + ...opts, + }); + }, + listGroups() { + return this._makeRequest({ + path: "/groups", + }); + }, + listTags() { + return this._makeRequest({ + path: "/tags", + }); + }, + listUsers() { + return this._makeRequest({ + path: "/users", + }); + }, + createBookmark(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/bookmarks", + ...opts, + }); + }, + createEvent(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/calEvents", + ...opts, + }); + }, + createTask(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/tasks", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, lastDate, ...opts + }) { + let hasMore = false; + let page = 0; + + do { + params.limit = LIMIT; + params.skip = LIMIT * page; + page++; + const data = await fn({ + params, + ...opts, + }); + for (const d of data) { + if (lastDate >= Date.parse(d.addedDate)) break; + + yield d; + } + + hasMore = data.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/teamleader_focus/README.md b/components/teamleader_focus/README.md new file mode 100644 index 0000000000000..c3bf09ecc8063 --- /dev/null +++ b/components/teamleader_focus/README.md @@ -0,0 +1,11 @@ +# Overview + +Teamleader Focus API lets you integrate your Teamleader account's features with Pipedream, enabling automation of various business processes. You can create, update, fetch, and manage data concerning contacts, companies, deals, projects, invoices, and tasks. Automating these aspects via Pipedream can streamline workflows, trigger actions based on events, and connect Teamleader Focus with other apps to enhance productivity. + +# Example Use Cases + +- **Sync New Contacts to Google Sheets**: When a new contact is added in Teamleader Focus, trigger a workflow in Pipedream to append the contact's details to a Google Sheet. This keeps your records in sync and allows for easy sharing and analysis of contact data. + +- **Send Slack Notifications for New Deals**: Set up a Pipedream workflow that listens for new deals in Teamleader Focus. When a deal is created, automatically send a notification to a designated Slack channel, keeping your team instantly informed about sales progress. + +- **Automate Invoice Creation and Emailing**: Whenever a project is marked as completed in Teamleader Focus, trigger a Pipedream workflow to generate an invoice for the project, and then email it to the client using an email service like SendGrid. This automates billing processes and ensures timely payment requests. diff --git a/components/teamup/README.md b/components/teamup/README.md new file mode 100644 index 0000000000000..9ada9239eee16 --- /dev/null +++ b/components/teamup/README.md @@ -0,0 +1,11 @@ +# Overview + +The TeamUp API provides an interface to interact with TeamUp's calendar services, allowing for robust management of schedules, events, and calendars. Leveraging this API within Pipedream, you can create automated workflows that handle event synchronization, notifications, and calendar data manipulation. Pipedream's serverless platform facilitates seamless integration with various services to enhance the capabilities of TeamUp, such as triggering actions on a schedule or in response to events, and connecting with other apps like Slack or Google Sheets for extended functionality. + +# Example Use Cases + +- **Automated Event Reminders**: Send custom reminders through Slack for upcoming TeamUp calendar events. This workflow ensures team members never miss important meetings or deadlines by automatically fetching events from TeamUp and posting reminders to a designated Slack channel. + +- **Synchronize Calendars**: Keep a Google Calendar in sync with a TeamUp calendar. Whenever a new event is added to TeamUp, the workflow creates a matching event in Google Calendar, and vice versa, ensuring all calendars reflect the same schedule. + +- **Event Analytics Reporting**: Generate weekly reports on event metrics and send them to team leaders via email. This workflow pulls event data from TeamUp, aggregates statistics such as attendance or cancellations, and compiles them into a report that's automatically emailed using a service like SendGrid. diff --git a/components/teamwave/README.md b/components/teamwave/README.md new file mode 100644 index 0000000000000..64c54ea0443c2 --- /dev/null +++ b/components/teamwave/README.md @@ -0,0 +1,11 @@ +# Overview + +The TeamWave API offers a conduit to their CRM, project management, and HR software, allowing you to automate workflows, sync data across platforms, and create custom integrations. By leveraging the TeamWave API in Pipedream, you can craft workflows that react to events within TeamWave, manipulate data, and connect to other services to streamline your business processes. + +# Example Use Cases + +- **Sync TeamWave Deals with a Google Sheet**: Automate the process of updating a Google Sheet with new deals from TeamWave. Each time a deal is created or updated in TeamWave, the workflow in Pipedream triggers and appends or modifies the corresponding row in the Google Sheet, ensuring data consistency and accessibility. + +- **Create Slack Notifications for New TeamWave Projects**: Set up instant notifications in a Slack channel whenever a new project is initiated in TeamWave. This workflow helps in keeping the entire team informed and allows for quick actions or discussions related to the new projects. + +- **Automate Task Creation in TeamWave from Trello Cards**: Whenever a new card is created in a specific Trello board, trigger a workflow in Pipedream to create a corresponding task in TeamWave. This connection ensures that project management efforts are aligned across different platforms, and helps maintain a single source of truth for task tracking. diff --git a/components/teamwork/README.md b/components/teamwork/README.md index d8d69748bb42e..1127753fd4d42 100644 --- a/components/teamwork/README.md +++ b/components/teamwork/README.md @@ -1,14 +1,11 @@ # Overview -Using the Teamwork API, you can build powerful tools and applications that help -you or your team stay connected and get more done. Here are a few examples of -things you can build with it: - -- Automate routine tasks such as adding projects and tasks, moving them through - progress stages, and assigning users to tasks -- Create custom reports and analytics on project data -- Schedule messaging to be sent out automatically about the progress of - projects or tasks -- Develop applications that sync with other services such as Gmail and Slack -- Create workflows and custom processes to automate how tasks are handled -- Set up notifications for when deadlines are due or tasks are completed +The Teamwork API taps into the power of project management, letting you streamline workflows by automating tasks like creating projects, assigning tasks, tracking time, or updating statuses. With Pipedream, you can effortlessly integrate these capabilities with hundreds of other apps to create custom automations that fit your team's needs, enhancing productivity and ensuring that your projects are always moving forward with the latest information. + +# Example Use Cases + +- **Automated Project Creation from CRMs**: Whenever a new deal is marked as won in your CRM, such as Salesforce or HubSpot, a workflow can trigger the creation of a corresponding project in Teamwork, complete with pre-configured task lists and milestones, ensuring that your team can immediately get started on new client work without manual data entry. + +- **Task Management with Real-Time Communication Tools**: Link Teamwork tasks to a Slack channel to notify your team when tasks are created, updated, or completed. This keeps everyone in the loop without having to constantly check for updates in Teamwork and improves collaboration as team members can discuss tasks right in Slack. + +- **Time Tracking Analytics and Reporting**: Synchronize time tracking data from Teamwork to a Google Sheets spreadsheet or a business intelligence tool like Tableau. This can empower management with insights into project hours, helping to evaluate performance, estimate better for future projects, and ensure timely completion within budget. diff --git a/components/teamwork_desk/README.md b/components/teamwork_desk/README.md new file mode 100644 index 0000000000000..25c1783b8dcf0 --- /dev/null +++ b/components/teamwork_desk/README.md @@ -0,0 +1,11 @@ +# Overview + +The Teamwork Desk API provides the means to programmatically access and manipulate customer support ticket data. By integrating it with Pipedream, you can automate ticketing workflows, streamline customer interactions, and connect support data with other business tools. Whether syncing tickets to a CRM, setting up custom alerts, or generating reports, the API's capabilities allow for a variety of automations to enhance support operations. + +# Example Use Cases + +- **Ticket to CRM Sync**: Automatically sync new tickets from Teamwork Desk to a CRM like Salesforce. When a new ticket arrives, Pipedream triggers a workflow that creates or updates a corresponding record in Salesforce, ensuring your sales team has the latest customer interaction data. + +- **Support Alert System**: Set up a custom alert system using Twilio for urgent support tickets. Pipedream can monitor ticket severity or keywords, and when a match is found, it sends an SMS to the on-duty support agent, enabling quick response to critical issues. + +- **Daily Support Summary Email**: Compile a daily summary of support tickets and send it via email with SendGrid. Pipedream schedules a workflow to fetch ticket data at the end of each day, format it, and send a digest to the support team, keeping everyone informed of the day's support load. diff --git a/components/telegram_bot_api/README.md b/components/telegram_bot_api/README.md index 2c21ec6af8b5f..5a54aa89c6991 100644 --- a/components/telegram_bot_api/README.md +++ b/components/telegram_bot_api/README.md @@ -1,12 +1,11 @@ # Overview -With the Telegram Bot API, you can build bots that perform a variety of tasks, -including: - -- Sending and receiving messages -- Social networking -- Content management -- File sharing -- Location sharing -- Bot administration -- And more! +The Telegram Bot API allows you to build bots that can interact with users on the Telegram platform. Using Pipedream, you can automate messaging, handle commands, and trigger actions based on conversations or alerts. Pipedream's serverless execution model enables you to create complex workflows involving Telegram messages without managing any infrastructure. With Pipedream's integration, you can process inbound messages, send outbound notifications, and connect the Telegram Bot API with numerous other services to create powerful automation solutions. + +# Example Use Cases + +- **Customer Support Ticketing**: Automate the creation of customer support tickets from Telegram messages. When a user sends a support request to your Telegram bot, Pipedream can capture the message and create a ticket in a service like Zendesk or Jira, assigning it to the appropriate team and sending a confirmation message back to the user. + +- **Content Distribution Network**: Use your Telegram bot to distribute content automatically. Pipedream can listen for new posts or updates from your CMS, such as WordPress, and then push that content to a Telegram channel or chat. This workflow is useful for bloggers, news agencies, or anyone wanting to keep their audience informed in real-time. + +- **E-commerce Order Notifications**: Send order updates or confirmations to customers via Telegram. When a new order is placed on your e-commerce platform, like Shopify, Pipedream can trigger a workflow that sends a personalized message to the customer's Telegram account, providing them peace of mind and enhancing the overall shopping experience. diff --git a/components/telesign/README.md b/components/telesign/README.md new file mode 100644 index 0000000000000..e5fa79d3371cd --- /dev/null +++ b/components/telesign/README.md @@ -0,0 +1,11 @@ +# Overview + +The TeleSign API furnishes developers with tools for communication and security. You can send SMS messages, voice alerts, and leverage phone number intelligence for risk assessments. Within Pipedream, you can integrate TeleSign to automate notifications, verification, and fraud prevention processes. It fosters real-time, event-driven actions, such as sending an SMS when a new user signs up, or assessing the risk of a transaction based on phone number data. + +# Example Use Cases + +- **User Registration Verification**: Automate the sending of verification codes via SMS when new users register on your platform. Trigger the TeleSign SMS API to dispatch a secure code and validate user phone numbers as part of your onboarding workflow. + +- **Transaction Fraud Alert**: Couple TeleSign's Score API with a payment platform like Stripe. Assess the risk associated with phone numbers during transactions. If a high-risk score is detected, trigger an alert and hold the transaction for further review. + +- **Appointment Reminders**: Streamline appointment management by integrating TeleSign with a calendar app like Google Calendar. Set up a workflow that sends SMS reminders when an appointment is upcoming, reducing no-shows and enhancing customer experience. diff --git a/components/telnyx/README.md b/components/telnyx/README.md index 56063d9725d86..8105e94c5aaa2 100644 --- a/components/telnyx/README.md +++ b/components/telnyx/README.md @@ -1,18 +1,11 @@ # Overview -Building sophisticated and modern communications applications is now easier -than ever with the Telnyx API. As a comprehensive suite of telecommunication -technology, the Telnyx API offers a variety of ways to build reliable -cloud-based communications. With the Telnyx API, developers and businesses can -easily build and deploy the following applications: +The Telnyx API offers a suite of telecommunication features enabling developers to manage voice, SMS, and other real-time communications. With Telnyx, you can programmatically send and receive text messages, handle voice calls, and control other telephony services using their robust API. Leveraging Pipedream's capabilities, this API can be hooked into workflows to automate complex tasks, react to incoming messages or calls, and integrate with countless other services for analytics, customer support, and more. -- Programmable Voice: Create voice call applications, teleconferencing, IVR, - and more with programmable voice -- Messaging: Build reliable chat, SMS, and MMS communication applications -- Number Management: Purchase, port, and manage phone numbers quickly and - securely -- Real-Time Reporting: Analyze call and messaging data in real-time -- Outbound Calling: Create powerful outbound call campaigns -- Emergency Services: Connect 911 calls quickly and accurately -- Call Recording: Record incoming and outgoing calls for quality assurance -- Rapid Deploy: Streamline development and operations with automated workflows +# Example Use Cases + +- **SMS Customer Support**: Automate responses to customer inquiries via SMS by integrating Telnyx with a customer support app like Zendesk. When an SMS is received, trigger a Pipedream workflow to create or update a ticket in Zendesk, ensuring customer issues are tracked and resolved efficiently. + +- **Voice Call Analytics**: Capture call data from Telnyx for analysis. Each time a call is made or received, use Pipedream to log details into a Google Sheets spreadsheet or a database for real-time analytics, helping you to understand call volume, duration, and geographic distribution of your users. + +- **Automated Notifications System**: Create a system that sends out SMS or voice call notifications through Telnyx based on triggers from a project management tool like Trello. Set up a Pipedream workflow that listens for specific status updates or tagged tasks in Trello, and then automatically sends out alerts to relevant stakeholders, keeping everyone informed with the latest project updates. diff --git a/components/telnyx/actions/dial-number/dial-number.mjs b/components/telnyx/actions/dial-number/dial-number.mjs new file mode 100644 index 0000000000000..1e34126c85ca8 --- /dev/null +++ b/components/telnyx/actions/dial-number/dial-number.mjs @@ -0,0 +1,62 @@ +import telnyxApp from "../../telnyx.app.mjs"; + +export default { + key: "telnyx-dial-number", + name: "Dial Number", + description: "Dial a number or SIP URI from a given Call Control App. [See the documentation](https://developers.telnyx.com/api/call-control/dial-call)", + version: "0.0.1", + type: "action", + props: { + telnyxApp, + callControlAppId: { + propDefinition: [ + telnyxApp, + "callControlAppId", + ], + }, + phoneNumber: { + propDefinition: [ + telnyxApp, + "phoneNumber", + ], + }, + to: { + type: "string", + label: "To", + description: "The DID or SIP URI to dial out to", + }, + fromDisplayName: { + type: "string", + label: "From Display Name", + description: "The string to be used as the caller id name (SIP From Display Name) presented to the destination (to number).", + optional: true, + }, + audioUrl: { + type: "string", + label: "Audio URL", + description: "The URL of a file to be played back to the callee when the call is answered. The URL can point to either a WAV or MP3 file.", + optional: true, + }, + webhookUrl: { + type: "string", + label: "Webhook URL", + description: "Use this field to override the URL to which Telnyx will send subsequent webhooks for this call.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.telnyxApp.dialNumber({ + $, + data: { + connection_id: this.callControlAppId, + from: this.phoneNumber, + to: this.to, + from_display_name: this.fromDisplayName, + audio_url: this.audioUrl, + webhook_url: this.webhookUrl, + }, + }); + $.export("$summary", "Successfully dialed number."); + return response; + }, +}; diff --git a/components/telnyx/actions/get-message/get-message.mjs b/components/telnyx/actions/get-message/get-message.mjs new file mode 100644 index 0000000000000..c088eb69dc250 --- /dev/null +++ b/components/telnyx/actions/get-message/get-message.mjs @@ -0,0 +1,27 @@ +import telnyxApp from "../../telnyx.app.mjs"; + +export default { + key: "telnyx-get-message", + name: "Retrieve a Message", + description: "Retrieve a message. [See the documentation](https://developers.telnyx.com/api/messaging/get-message)", + version: "0.0.3", + type: "action", + props: { + telnyxApp, + id: { + type: "string", + label: "Message Id", + description: "The id of the message.", + }, + }, + async run({ $ }) { + const message = await this.telnyxApp.getMessage({ + $, + params: { + id: this.id, + }, + }); + $.export("$summary", `Successfully retrieved message ${message.data.id}.`); + return message; + }, +}; diff --git a/components/telnyx/actions/get-messaging-profiles/get-messaging-profiles.mjs b/components/telnyx/actions/get-messaging-profiles/get-messaging-profiles.mjs new file mode 100644 index 0000000000000..1b29645b388ec --- /dev/null +++ b/components/telnyx/actions/get-messaging-profiles/get-messaging-profiles.mjs @@ -0,0 +1,49 @@ +import telnyxApp from "../../telnyx.app.mjs"; + +export default { + key: "telnyx-get-messaging-profiles", + name: "Get Messaging Profiles", + description: "Get a list of messaging profiles. [See the documentation](https://developers.telnyx.com/api/messaging/list-messaging-profiles)", + version: "0.0.5", + type: "action", + props: { + telnyxApp, + pageSize: { + type: "integer", + label: "Page Size", + description: "The size of the page.", + optional: true, + min: 1, + max: 250, + default: 20, + }, + pageNumber: { + type: "integer", + label: "Page Number", + description: "The page number to load.", + optional: true, + min: 1, + default: 1, + }, + filter: { + type: "string", + label: "Filter", + description: "Filter by name.", + optional: true, + }, + }, + async run({ $ }) { + const profiles = await this.telnyxApp.getMessagingProfiles({ + $, + params: { + "page[size]": this.pageSize || 20, + "page[number]": this.pageNumber || 1, + "filter[name]": this.filter, + }, + }); + $.export("$summary", `Successfully retrieved ${profiles.data.length} messaging profile${profiles.data.length === 1 + ? "" + : "s"}.`); + return profiles; + }, +}; diff --git a/components/telnyx/actions/get-phone-numbers/get-phone-numbers.mjs b/components/telnyx/actions/get-phone-numbers/get-phone-numbers.mjs new file mode 100644 index 0000000000000..8d350afab2d1b --- /dev/null +++ b/components/telnyx/actions/get-phone-numbers/get-phone-numbers.mjs @@ -0,0 +1,137 @@ +import telnyxApp from "../../telnyx.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "telnyx-get-phone-numbers", + name: "Get Phone Numbers", + description: "Get a list of phone numbers. [See the documentation](https://developers.telnyx.com/api/numbers/list-phone-numbers)", + version: "0.0.4", + type: "action", + props: { + telnyxApp, + pageSize: { + type: "integer", + label: "Page Size", + description: "The size of the page.", + optional: true, + min: 1, + max: 250, + default: 20, + }, + pageNumber: { + type: "integer", + label: "Page Number", + description: "The page number to load.", + optional: true, + min: 1, + default: 1, + }, + tag: { + type: "string", + label: "Tag", + description: "Filter by tag.", + optional: true, + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "Filter by phone number. Requires at least three digits. Non-numerical characters will result in no values being returned.", + optional: true, + }, + status: { + type: "string", + label: "Status", + description: "Filter by phone number status.", + options: Object.values(constants.phoneNumberStatus), + optional: true, + }, + connectionId: { + type: "string", + label: "Connection Id", + description: "Filter by connection Id.", + optional: true, + }, + contains: { + type: "string", + label: "Contains", + description: "Filter contains connection name. Requires at least three characters.", + optional: true, + }, + startsWith: { + type: "string", + label: "Starts With", + description: "Filter starts with connection name. Requires at least three characters.", + optional: true, + }, + endsWith: { + type: "string", + label: "Ends With", + description: "Filter ends with connection name. Requires at least three characters.", + optional: true, + }, + equals: { + type: "string", + label: "Equals", + description: "Filter by connection name.", + optional: true, + }, + paymentMethod: { + type: "string", + label: "Payment Method", + description: "Filter by usage payment method.", + options: Object.values(constants.paymentMethods), + optional: true, + }, + billingGroupId: { + type: "string", + label: "Billing Group Id", + description: "Filter by the billing group Id associated with phone numbers. To filter to only phone numbers that have no billing group associated them, set the value of this filter to the string 'null'.", + optional: true, + }, + emergencyAddressId: { + type: "string", + label: "Emergency Address Id", + description: "Filter by the emergency_address_id associated with phone numbers. To filter only phone numbers that have no emergency address associated with them, set the value of this filter to the string 'null'.", + optional: true, + }, + customerReference: { + type: "string", + label: "Customer Reference", + description: "Filter numbers via the customer reference set.", + optional: true, + }, + sort: { + type: "string", + label: "Sort", + description: "Specifies the sort order for results. If not given, results are sorted by created_at in descending order.", + options: Object.values(constants.sortPhoneNumbers), + optional: true, + }, + }, + async run({ $ }) { + const phoneNumbers = await this.telnyxApp.getPhoneNumbers({ + $, + params: { + "page[size]": this.pageSize || 20, + "page[number]": this.pageNumber || 1, + "filter[tag]": this.tag, + "filter[phone_number]": this.phoneNumber, + "filter[status]": this.status, + "filter[connection_id]": this.connectionId, + "filter[voice.connection_name][contains]": this.contains, + "filter[voice.connection_name][starts_with]": this.startsWith, + "filter[voice.connection_name][ends_with]": this.endsWith, + "filter[voice.connection_name][eq]": this.equals, + "filter[voice.usage_payment_method]": this.paymentMethod, + "filter[billing_group_id]": this.billingGroupId, + "filter[emergency_address_id]": this.emergencyAddressId, + "filter[customer_reference]": this.customerReference, + "sort": this.sort, + }, + }); + $.export("$summary", `Successfully retrieved ${phoneNumbers.data.length} phone number${phoneNumbers.data.length === 1 + ? "" + : "s"}.`); + return phoneNumbers; + }, +}; diff --git a/components/telnyx/actions/send-fax/send-fax.mjs b/components/telnyx/actions/send-fax/send-fax.mjs new file mode 100644 index 0000000000000..953a2e4f62c7a --- /dev/null +++ b/components/telnyx/actions/send-fax/send-fax.mjs @@ -0,0 +1,91 @@ +import telnyxApp from "../../telnyx.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "telnyx-send-fax", + name: "Send Fax", + description: "Sends a PDF document to a specified fax number using the Telnyx Fax API. [See the documentation](https://developers.telnyx.com/api/programmable-fax/send-fax)", + version: "0.0.1", + type: "action", + props: { + telnyxApp, + faxAppId: { + propDefinition: [ + telnyxApp, + "faxAppId", + ], + }, + mediaUrl: { + type: "string", + label: "Media URL", + description: "The URL to the PDF used for the fax's media.", + }, + phoneNumber: { + propDefinition: [ + telnyxApp, + "phoneNumber", + ], + }, + to: { + type: "string", + label: "To", + description: "The phone number, in E.164 format, the fax will be sent to or SIP URI", + }, + fromDisplayName: { + type: "string", + label: "From Display Name", + description: "The string to be used as the caller id name (SIP From Display Name) presented to the destination (to number).", + optional: true, + }, + quality: { + type: "string", + label: "Quality", + description: "The quality of the fax. The `ultra` settings provides the highest quality available, but also present longer fax processing times. `ultra_light` is best suited for images, while `ultra_dark` is best suited for text.", + optional: true, + options: Object.values(constants.faxQualities), + }, + ts38Enabled: { + type: "boolean", + label: "TS38 Enabled", + description: "The flag to disable the T.38 protocol", + optional: true, + }, + monochrome: { + type: "boolean", + label: "Monochrome", + description: "The flag to enable monochrome, `true` creates black and white fax results.", + optional: true, + }, + storeMedia: { + type: "boolean", + label: "Store Media", + description: "Should fax media be stored on temporary URL?", + optional: true, + }, + webhookUrl: { + type: "string", + label: "Webhook URL", + description: "Use this field to override the URL to which Telnyx will send subsequent webhooks for this fax.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.telnyxApp.sendFax({ + $, + data: { + connection_id: this.faxAppId, + media_url: this.mediaUrl, + from: this.phoneNumber, + to: this.to, + from_display_name: this.fromDisplayName, + quality: this.quality, + ts38_enabled: this.ts38Enabled, + monochrome: this.monochrome, + store_media: this.storeMedia, + webhook_url: this.webhookUrl, + }, + }); + $.export("$summary", `Successfully sent fax with Id: ${response.data.id}`); + return response; + }, +}; diff --git a/components/telnyx/actions/send-group-message/send-group-message.mjs b/components/telnyx/actions/send-group-message/send-group-message.mjs new file mode 100644 index 0000000000000..a1089df7012e0 --- /dev/null +++ b/components/telnyx/actions/send-group-message/send-group-message.mjs @@ -0,0 +1,77 @@ +import telnyxApp from "../../telnyx.app.mjs"; + +export default { + key: "telnyx-send-group-message", + name: "Send Group Message", + description: "Send a group MMS message. [See the documentation](https://developers.telnyx.com/api/messaging/create-group-mms-message)", + version: "0.0.3", + type: "action", + props: { + telnyxApp, + phoneNumber: { + optional: true, + propDefinition: [ + telnyxApp, + "phoneNumber", + ], + }, + to: { + type: "string[]", + label: "To", + description: "Receiving list of destinations. No more than 8 destinations are allowed.", + }, + text: { + type: "string", + label: "Text", + description: "Message content. Must be a valid UTF-8 string, and no longer then 5MB for MMS.", + optional: true, + }, + subject: { + type: "string", + label: "Subject", + description: "Subject of the MMS message.", + optional: true, + }, + mediaUrls: { + type: "string[]", + label: "Media URLs", + description: "URLs of media files to send with the message.", + optional: true, + }, + webhookUrl: { + type: "string", + label: "Webhook URL", + description: "URL to send delivery receipts to. Must be a valid URL.", + optional: true, + }, + webhookFailoverUrl: { + type: "string", + label: "Webhook Failover URL", + description: "URL to send delivery receipts to if the primary webhook fails. Must be a valid URL.", + optional: true, + }, + useProfileWebhooks: { + type: "boolean", + label: "Use Profile Webhooks", + description: "Whether to use the messaging profile's webhook URL for delivery receipts.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.telnyxApp.sendGroupMessage({ + $, + data: { + to: this.to, + from: this.phoneNumber, + text: this.text, + subject: this.subject, + media_urls: this.mediaUrls, + webhook_url: this.webhookUrl, + webhook_failover_url: this.webhookFailoverUrl, + use_profile_webhooks: this.useProfileWebhooks, + }, + }); + $.export("$summary", `Successfully sent MMS message with Id: ${response.data.id}`); + return response; + }, +}; diff --git a/components/telnyx/actions/send-message/send-message.mjs b/components/telnyx/actions/send-message/send-message.mjs new file mode 100644 index 0000000000000..0aa8e8983ae10 --- /dev/null +++ b/components/telnyx/actions/send-message/send-message.mjs @@ -0,0 +1,103 @@ +import telnyxApp from "../../telnyx.app.mjs"; + +export default { + key: "telnyx-send-message", + name: "Send Message", + description: "Send an SMS or MMS message. [See the documentation](https://developers.telnyx.com/docs/messaging/messages/send-message)", + version: "0.0.5", + type: "action", + props: { + telnyxApp, + messagingProfileId: { + optional: true, + propDefinition: [ + telnyxApp, + "messagingProfileId", + ], + }, + phoneNumber: { + optional: true, + propDefinition: [ + telnyxApp, + "phoneNumber", + ], + }, + to: { + type: "string", + label: "To", + description: "Receiving address (+E.164 formatted phone number or short code).", + }, + text: { + type: "string", + label: "Text", + description: "Message content. Must be a valid UTF-8 string, and no longer than 1600 characters for SMS or 5MB for MMS. Required if sending an SMS message.", + optional: true, + }, + subject: { + type: "string", + label: "Subject", + description: "Subject of the MMS message.", + optional: true, + }, + mediaUrls: { + type: "string[]", + label: "Media URLs", + description: "URLs of media files to send with the message.", + optional: true, + }, + webhookUrl: { + type: "string", + label: "Webhook URL", + description: "URL to send delivery receipts to. Must be a valid URL.", + optional: true, + }, + webhookFailoverUrl: { + type: "string", + label: "Webhook Failover URL", + description: "URL to send delivery receipts to if the primary webhook fails. Must be a valid URL.", + optional: true, + }, + useProfileWebhooks: { + type: "boolean", + label: "Use Profile Webhooks", + description: "Whether to use the messaging profile's webhook URL for delivery receipts.", + optional: true, + }, + type: { + type: "string", + label: "Type", + description: "Type of message to sent.", + optional: true, + options: [ + "SMS", + "MMS", + ], + }, + autoDetect: { + type: "boolean", + label: "Auto Detect", + description: "Automatically detect if an SMS message is unusually long and exceeds a recommended limit of message parts.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.telnyxApp.sendMessage({ + $, + data: { + to: this.to, + from: this.phoneNumber, + text: this.text, + messaging_profile_id: this.messagingProfileId, + subject: this.subject, + media_urls: this.mediaUrls, + webhook_url: this.webhookUrl, + webhook_failover_url: this.webhookFailoverUrl, + use_profile_webhooks: this.useProfileWebhooks, + type: this.type, + auto_detect: this.autoDetect, + }, + }); + $.export("$summary", `Successfully sent SMS/MMS message with Id: ${response.data.id}`); + return response; + }, +}; diff --git a/components/telnyx/common/constants.mjs b/components/telnyx/common/constants.mjs new file mode 100644 index 0000000000000..3a4721d5315a9 --- /dev/null +++ b/components/telnyx/common/constants.mjs @@ -0,0 +1,38 @@ +const phoneNumberStatus = { + "PURCHASE PENDING": "purchase_pending", + "PURCHASE FAILED": "purchase_failed", + "PORT PENDING": "port_pending", + "ACTIVE": "active", + "DELETED": "deleted", + "PORT FAILED": "port_failed", + "EMERGENCY ONLY": "emergency_only", + "PORTED OUT": "ported_out", + "PORT OUT PENDING": "port_out_pending", +}; + +const paymentMethods = { + "PAY PER MINUTE": "pay-per-minute", + "CHANNEL": "channel", +}; + +const sortPhoneNumbers = { + "PURCHASED AT": "purchased_at", + "PHONE NUMBER": "phone_number", + "CONNECTION NAME": "connection_name", + "USAGE PAYMENT METHOD": "usage_payment_method", +}; + +const faxQualities = { + "NORMAL": "normal", + "HIGH": "high", + "VERY_HIGH": "very_high", + "ULTRA_LIGHT": "ultra_light", + "ULTRA_DARK": "ultra_dark", +}; + +export default { + phoneNumberStatus, + paymentMethods, + sortPhoneNumbers, + faxQualities, +}; diff --git a/components/telnyx/package.json b/components/telnyx/package.json new file mode 100644 index 0000000000000..a8995b363f68f --- /dev/null +++ b/components/telnyx/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/telnyx", + "version": "0.2.3", + "description": "Pipedream Telnyx Components", + "main": "telnyx.app.mjs", + "keywords": [ + "pipedream", + "telnyx" + ], + "homepage": "https://pipedream.com/apps/telnyx", + "author": "Pipedream (https://pipedream.com/)", + "dependencies": { + "@pipedream/platform": "^1.6.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/components/telnyx/sources/common/base.mjs b/components/telnyx/sources/common/base.mjs new file mode 100644 index 0000000000000..3ec884ed2af8d --- /dev/null +++ b/components/telnyx/sources/common/base.mjs @@ -0,0 +1,69 @@ +import telnyxApp from "../../telnyx.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + telnyxApp, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs"); + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + getParams() { + return {}; + }, + getTs() { + return new Date().toISOString(); + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + + const resourceFn = this.getResourceFn(); + const params = this.getParams(lastTs); + + const results = this.telnyxApp.paginate({ + resourceFn, + params, + max, + }); + + for await (const item of results) { console.log(item); + const ts = this.getTs(item); + if (Date.parse(ts) >= Date.parse(lastTs)) { + maxTs = Date.parse(ts) > Date.parse(maxTs) + ? ts + : maxTs; + const meta = this.generateMeta(item); + this.$emit(item, meta); + } + } + + this._setLastTs(maxTs); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/telnyx/sources/new-call-received/new-call-received.mjs b/components/telnyx/sources/new-call-received/new-call-received.mjs new file mode 100644 index 0000000000000..550034d327be4 --- /dev/null +++ b/components/telnyx/sources/new-call-received/new-call-received.mjs @@ -0,0 +1,33 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "telnyx-new-call-received", + name: "New Call Received", + description: "Emit new event on an incoming call to a Call Control Application. [See the documentation](https://developers.telnyx.com/api/webhooks/get-webhook-deliveries)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.telnyxApp.listWebhookDeliveries; + }, + getParams(lastTs) { + return { + "filter[event_type]": "call.initiated", + "filter[started_at][gte]": lastTs, + }; + }, + getTs(event) { + return event.webhook.occurred_at; + }, + generateMeta(event) { + return { + id: event.id, + summary: `New call from: ${event.webhook.payload.from}`, + ts: Date.parse(this.getTs(event)), + }; + }, + }, +}; diff --git a/components/telnyx/sources/new-phone-number/new-phone-number.mjs b/components/telnyx/sources/new-phone-number/new-phone-number.mjs new file mode 100644 index 0000000000000..9791144e40dd1 --- /dev/null +++ b/components/telnyx/sources/new-phone-number/new-phone-number.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "telnyx-new-phone-number", + name: "New Phone Number", + description: "Emit new event when a new phone number is added [See the documentation](https://developers.telnyx.com/api/numbers/list-available-phone-numbers)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.telnyxApp.getPhoneNumbers; + }, + generateMeta(phoneNumber) { + return { + id: phoneNumber.phone_number, + summary: `New Phone Number: ${phoneNumber.phone_number}`, + ts: Date.now(), + }; + }, + }, +}; diff --git a/components/telnyx/telnyx.app.mjs b/components/telnyx/telnyx.app.mjs index 15559a7bf4045..0252148cd76ff 100644 --- a/components/telnyx/telnyx.app.mjs +++ b/components/telnyx/telnyx.app.mjs @@ -1,11 +1,204 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "telnyx", - propDefinitions: {}, + propDefinitions: { + messagingProfileId: { + type: "string", + label: "Messaging Profile Id", + description: "The Id of the messaging profile to use for sending the message.", + async options({ page }) { + const params = { + "page[number]": page + 1, + }; + const profiles = await this.getMessagingProfiles({ + params, + }); + return profiles.data.map((profile) => ({ + label: profile.name, + value: profile.id, + })); + }, + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number to send the message from.", + async options({ page }) { + const params = { + "page[number]": page + 1, + }; + const phoneNumbers = await this.getPhoneNumbers({ + params, + }); + return phoneNumbers.data.map((phoneNumber) => ({ + label: phoneNumber.phone_number, + value: phoneNumber.phone_number, + })); + }, + }, + faxAppId: { + type: "string", + label: "Fax Application ID", + description: "The connection/application ID to send the fax with.", + async options({ page }) { + const { data } = await this.listFaxApplications({ + params: { + "page[number]": page + 1, + }, + }); + return data?.map(({ + id: value, application_name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + callControlAppId: { + type: "string", + label: "Call Control Application ID", + description: "The ID of the Call Control App to be used when dialing the destination.", + async options({ page }) { + const { data } = await this.listCallControlApplications({ + params: { + "page[number]": page + 1, + }, + }); + return data?.map(({ + id: value, application_name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getHeaders() { + return { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.$auth.api_key}`, + }; + }, + async makeRequest(customConfig) { + const { + $, + path, + url, + ...otherConfig + } = customConfig; + + const basePath = "https://api.telnyx.com/v2"; + + const config = { + url: url ?? `${basePath}${path}`, + headers: this.getHeaders(), + ...otherConfig, + }; + try { + return await axios($ || this, config); + } catch (error) { + this.throwFormattedError(error); + } + }, + throwFormattedError(error) { + error = error.response; + throw new Error(`${error.status} - ${error.statusText} - ${error.data.errors[0].detail}`); + }, + sendMessage(args = {}) { + return this.makeRequest({ + method: "POST", + path: "/messages", + ...args, + }); + }, + getMessagingProfiles(args = {}) { + return this.makeRequest({ + path: "/messaging_profiles", + ...args, + }); + }, + getPhoneNumbers(args = {}) { + return this.makeRequest({ + path: "/phone_numbers", + ...args, + }); + }, + listFaxApplications(args = {}) { + return this.makeRequest({ + path: "/fax_applications", + ...args, + }); + }, + listCallControlApplications(args = {}) { + return this.makeRequest({ + path: "/call_control_applications", + ...args, + }); + }, + listWebhookDeliveries(args = {}) { + return this.makeRequest({ + path: "/webhook_deliveries", + ...args, + }); + }, + getMessage({ + id, ...args + }) { + return this.makeRequest({ + path: `/messages/${id}`, + ...args, + }); + }, + sendGroupMessage(args = {}) { + return this.makeRequest({ + method: "POST", + path: "/messages/group_mms", + ...args, + }); + }, + sendFax(args = {}) { + return this.makeRequest({ + method: "POST", + path: "/faxes", + ...args, + }); + }, + dialNumber(args = {}) { + return this.makeRequest({ + method: "POST", + path: "/calls", + ...args, + }); + }, + async *paginate({ + resourceFn, + params = {}, + max, + }) { + params = { + ...params, + "page[number]": 1, + }; + let hasMore, count = 0; + do { + const { + data, meta, + } = await resourceFn({ + params, + }); + for (const item of data) { + yield item; + count++; + if (count >= max) { + return; + } + } + params["page[number]"] += 1; + hasMore = count < meta.total_results; + } while (hasMore); }, }, }; diff --git a/components/temi/README.md b/components/temi/README.md index 9b0e9546d28c3..999ca56fc1dd0 100644 --- a/components/temi/README.md +++ b/components/temi/README.md @@ -1,23 +1,11 @@ # Overview -The Temi API gives developers the ability to create revolutionary new -applications that can interact with Temi, the world's first personal robot for -the home that treats each household member with respect and kindness. With this -powerful API, you can add voice commands, facial recognition, even autonomous -navigation to your app, making it truly powerful and revolutionary! +Temi API offers automated transcription services, converting audio and video files into text. With Pipedream's robust platform, you can exploit Temi's capabilities to activate a myriad of automated workflows, from transcribing meeting recordings for easy reference to triggering tasks based on the presence of keywords in transcribed text. The real power lies in connecting these transcriptions to other services for further analysis, archiving, or even real-time alerting. -The possibilities are endless with the Temi API. Here's some of the things you -can build: +# Example Use Cases -- Autonomous Navigation: Create an app that enables Temi to intuitively search - and reach its configured destinations. -- Voice Commands: Enable your app to respond to voice commands issued by Temi's - users. -- Facial Recognition: Implement facial recognition in your app and get Temi to - recognize and greet house members on arrival. -- Content Distribution: Create an app that allows users to store and access - media and other content on Temi. -- Robotics Apps: Create innovative robotic apps to let users interact with Temi - in more ways. -- Home Automation: Create an app that lets Temi control home devices like - lights and heaters, or monitor smoke and CO2 levels. +- **Automated Meeting Minutes**: Kick off a workflow when a new recording is uploaded to Google Drive, automatically sending the file to Temi for transcription. Then, format the resulting text and post it to a Slack channel or an internal wiki, ensuring team members have immediate access to meeting outcomes. + +- **Content Creation & SEO**: Send podcast or webinar recordings to Temi for transcription, then use Pipedream's integration with WordPress (or any other CMS) to publish the content. This can enhance SEO by providing text versions of your audio content, making it searchable and increasing web traffic. + +- **Customer Feedback Analysis**: After a support call ends, use Temi to transcribe the conversation; then analyze it with sentiment analysis tools like IBM Watson via Pipedream workflows. Push results into CRMs like Salesforce to track customer satisfaction trends or alert management about critical feedback. diff --git a/components/templated/README.md b/components/templated/README.md new file mode 100644 index 0000000000000..3054a31577734 --- /dev/null +++ b/components/templated/README.md @@ -0,0 +1,11 @@ +# Overview + +The Templated API allows users to generate custom documents based on predefined templates. It's a powerful tool when you need to create consistent documents or reports with variable data. In Pipedream, you can seamlessly integrate Templated with other services to automate document creation. Combine data from various sources, trigger document generation, and carry out follow-up actions like emailing the document or saving it to cloud storage. + +# Example Use Cases + +- **Automated Invoice Generation**: Trigger an invoice creation in Templated whenever a new sale is recorded in Stripe. Pipedream listens for Stripe sale events, then uses Templated to populate an invoice template with sale details, and finally emails the invoice to the customer. + +- **Dynamic Report Creation for CRM Updates**: When a sales opportunity status updates in Salesforce, Pipedream kicks off a workflow that sends data to Templated to generate a custom report. This report is then automatically uploaded to Google Drive and shared with relevant team members. + +- **Customized Email Campaigns**: Use Pipedream to watch for subscriber sign-ups via a Typeform. Upon a new entry, Pipedream sends subscriber data to Templated to create personalized welcome emails. These customized emails are then sent out via SendGrid to enhance user engagement. diff --git a/components/tento8/README.md b/components/tento8/README.md new file mode 100644 index 0000000000000..7c69d322d72e4 --- /dev/null +++ b/components/tento8/README.md @@ -0,0 +1,11 @@ +# Overview + +The 10to8 API lets you interact with your 10to8 appointment scheduling data programmatically. Use it within Pipedream to automate booking workflows, sync appointment data with other apps, send custom notifications, and more. By tapping into 10to8's API, you can create, update, and cancel bookings, retrieve customer details, and sync your calendar events, helping you streamline the scheduling process and integrate it with your digital ecosystem. + +# Example Use Cases + +- **Automate Appointment Confirmations**: Trigger a workflow on Pipedream when a new appointment is booked on 10to8. Automatically send a custom confirmation email via SendGrid or a personalized SMS through Twilio to the customer, including details of the appointment. + +- **Sync Appointments with Google Calendar**: Upon booking a new appointment in 10to8, use a Pipedream workflow to sync this appointment to a Google Calendar. This ensures your schedule is always up-to-date across platforms and you avoid double bookings. + +- **Aggregate Customer Feedback**: After an appointment is completed, trigger a workflow to send a feedback form using Typeform. Collect responses and save them to a Google Sheets document for easy analysis and follow-up. diff --git a/components/tento8/package.json b/components/tento8/package.json index 87d94aada3e7c..38d1e8df738dc 100644 --- a/components/tento8/package.json +++ b/components/tento8/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/terminus_app/README.md b/components/terminus_app/README.md new file mode 100644 index 0000000000000..354a5320ed0f8 --- /dev/null +++ b/components/terminus_app/README.md @@ -0,0 +1,11 @@ +# Overview + +The Terminus App API enables you to automate your account-based marketing operations by connecting Terminus with various other tools and services. Through Pipedream, you can craft serverless workflows that interact with the Terminus platform, such as syncing contact lists, triggering campaigns based on external events, and analyzing marketing performance data. Pipedream's no-code platform allows you to quickly create integrations with the Terminus App API and hundreds of other services with minimal setup. + +# Example Use Cases + +- **Automated Lead Qualification and Syncing**: Automatically qualify leads from a web form using a machine learning model and sync the high-scoring leads to Terminus for targeted ad campaigns. + +- **Dynamic Campaign Triggering**: Trigger Terminus ad campaigns based on customer activity in your product, such as feature usage or tier upgrades, by listening to webhooks from your SaaS platform. + +- **Marketing Performance Dashboard**: Pull data from Terminus on campaign performance and display it on a real-time dashboard in conjunction with data from other marketing tools like Google Analytics or Salesforce. diff --git a/components/terraform/README.md b/components/terraform/README.md new file mode 100644 index 0000000000000..652095466ba06 --- /dev/null +++ b/components/terraform/README.md @@ -0,0 +1,11 @@ +# Overview + +The Terraform API allows for the automation of infrastructure as code (IaC) management tasks. With Pipedream, you can orchestrate workflows that interact with Terraform to create, update, or destroy infrastructure programmatically. You can trigger workflows with webhooks, schedule them, or run them in response to events from other services. By integrating Terraform with Pipedream, you can streamline your DevOps processes, enforce compliance, and manage infrastructure changes with ease. + +# Example Use Cases + +- **Automated Infrastructure Deployment**: Trigger a Pipedream workflow to deploy new infrastructure as code changes are pushed to a GitHub repository. Use the GitHub app to listen for `push` events and then run Terraform plans and applies within Pipedream. + +- **Scheduled Infrastructure Audits**: Create a workflow that runs on a schedule to perform Terraform `plan` operations against your current state files, ensuring that the actual infrastructure matches the desired state. Send the output to a Slack channel for review using the Slack app. + +- **Infrastructure Change Notifications**: Set up a workflow that detects changes in Terraform state and sends notifications via email or messaging platforms. Use Pipedream’s built-in cron job feature to periodically check for state changes and alert your team using the Email by Pipedream or Twilio SMS app. diff --git a/components/tess_ai_by_pareto/actions/execute-agent/execute-agent.mjs b/components/tess_ai_by_pareto/actions/execute-agent/execute-agent.mjs new file mode 100644 index 0000000000000..6f0805e8eeebc --- /dev/null +++ b/components/tess_ai_by_pareto/actions/execute-agent/execute-agent.mjs @@ -0,0 +1,45 @@ +import { getQuestionProps } from "../../common/utils.mjs"; +import app from "../../tess_ai_by_pareto.app.mjs"; + +export default { + key: "tess_ai_by_pareto-execute-agent", + name: "Execute AI Agent", + description: + "Executes an AI Agent (template) with the given input. [See the documentation](https://tess.pareto.io/api/swagger#/default/f13b3be7386ce63d99fa4bdee0cf6c95)", + version: "0.0.1", + type: "action", + props: { + app, + templateId: { + propDefinition: [ + app, + "templateId", + ], + reloadProps: true, + }, + }, + methods: { + getQuestionProps, + }, + async additionalProps() { + const { questions } = await this.app.getTemplate(this.templateId); + return this.getQuestionProps(questions); + + }, + async run({ $ }) { + /* eslint-disable no-unused-vars */ + const { + app, templateId, getQuestionProps, ...data + } = this; + const response = await this.app.executeTemplate({ + $, + templateId, + data, + }); + $.export( + "$summary", + `Executed AI agent ${response.id}`, + ); + return response; + }, +}; diff --git a/components/tess_ai_by_pareto/actions/get-execution-response/get-execution-response.mjs b/components/tess_ai_by_pareto/actions/get-execution-response/get-execution-response.mjs new file mode 100644 index 0000000000000..79546f275d361 --- /dev/null +++ b/components/tess_ai_by_pareto/actions/get-execution-response/get-execution-response.mjs @@ -0,0 +1,30 @@ +import app from "../../tess_ai_by_pareto.app.mjs"; + +export default { + key: "tess_ai_by_pareto-get-execution-response", + name: "Get Agent Execution Response", + description: + "Retrieves the result of a previously executed AI Agent (template). [See the documentation](https://tess.pareto.io/api/swagger#/default/370b6709c5d9e8c17a76e1abb288e7ad)", + version: "0.0.1", + type: "action", + props: { + app, + executionId: { + type: "string", + label: "Agent Execution ID", + description: + "The ID of the AI Agent (template) execution to retrieve the result for.", + }, + }, + async run({ $ }) { + const result = await this.app.getTemplateResponse({ + $, + executionId: this.executionId, + }); + $.export( + "$summary", + `Retrieved response for execution ID ${this.executionId}`, + ); + return result; + }, +}; diff --git a/components/tess_ai_by_pareto/actions/search-ai-agents/search-ai-agents.mjs b/components/tess_ai_by_pareto/actions/search-ai-agents/search-ai-agents.mjs new file mode 100644 index 0000000000000..0d418cd82bee0 --- /dev/null +++ b/components/tess_ai_by_pareto/actions/search-ai-agents/search-ai-agents.mjs @@ -0,0 +1,47 @@ +import app from "../../tess_ai_by_pareto.app.mjs"; + +export default { + key: "tess_ai_by_pareto-search-ai-agents", + name: "Search AI Agents", + description: + "Retrieve AI Agents (templates) that match the specified criteria. [See the documentation](https://tess.pareto.io/api/swagger#/default/201046139d07458d530ad3526e0b3c2f)", + version: "0.0.1", + type: "action", + props: { + app, + query: { + type: "string", + label: "Search Query", + description: + "Search agents (templates) by title, description and long description.", + optional: true, + }, + type: { + type: "string", + label: "Type Filter", + description: "Filter by template type", + optional: true, + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "The maximum number of results to return", + optional: true, + default: 15, + min: 1, + max: 1000, + }, + }, + async run({ $ }) { + const response = await this.app.searchTemplates({ + $, + params: { + q: this.query, + type: this.type, + per_page: this.maxResults, + }, + }); + $.export("$summary", `Retrieved ${response.data?.length} templates`); + return response; + }, +}; diff --git a/components/tess_ai_by_pareto/common/utils.mjs b/components/tess_ai_by_pareto/common/utils.mjs new file mode 100644 index 0000000000000..b21ac571c51cd --- /dev/null +++ b/components/tess_ai_by_pareto/common/utils.mjs @@ -0,0 +1,21 @@ +export function getQuestionProps(questions) { + function getQuestionPropType(type) { + switch (type) { + case "number": + return "integer"; + default: + return "string"; + } + } + + return (questions ?? []).reduce((obj, question) => { + obj[question.name] = { + type: getQuestionPropType(question.type), + label: `Field: "${question.name}"`, + description: `Type: \`${question.type}\`. Description: "${question.description}"`, + options: question.options, + optional: !question.required, + }; + return obj; + }, {}); +} diff --git a/components/tess_ai_by_pareto/package.json b/components/tess_ai_by_pareto/package.json new file mode 100644 index 0000000000000..8060e0938e483 --- /dev/null +++ b/components/tess_ai_by_pareto/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/tess_ai_by_pareto", + "version": "0.1.0", + "description": "Pipedream Tess AI by Pareto Components", + "main": "tess_ai_by_pareto.app.mjs", + "keywords": [ + "pipedream", + "tess_ai_by_pareto" + ], + "homepage": "https://pipedream.com/apps/tess_ai_by_pareto", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/tess_ai_by_pareto/tess_ai_by_pareto.app.mjs b/components/tess_ai_by_pareto/tess_ai_by_pareto.app.mjs new file mode 100644 index 0000000000000..3769360ea2446 --- /dev/null +++ b/components/tess_ai_by_pareto/tess_ai_by_pareto.app.mjs @@ -0,0 +1,73 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "tess_ai_by_pareto", + propDefinitions: { + templateId: { + type: "string", + label: "AI Agent ID", + description: "The ID of the AI Agent (template) to execute.", + useQuery: true, + async options({ + page = 0, query, + }) { + const response = await this.searchTemplates({ + params: { + page: page + 1, + q: query || undefined, + }, + }); + return response?.data?.map((template) => ({ + label: template.title, + value: template.id, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://tess.pareto.io/api"; + }, + async _makeRequest({ + $ = this, path = "/", headers, ...otherOpts + } = {}) { + return axios($, { + url: this._baseUrl() + path, + headers: { + ...headers, + "Authorization": `Bearer ${this.$auth.api_token}`, + }, + ...otherOpts, + }); + }, + async executeTemplate({ + templateId, ...args + }) { + return this._makeRequest({ + method: "POST", + path: `/templates/${templateId}/execute`, + ...args, + }); + }, + async getTemplate(templateId) { + return this._makeRequest({ + path: `/templates/${templateId}`, + }); + }, + async searchTemplates(args) { + return this._makeRequest({ + path: "/templates", + ...args, + }); + }, + async getTemplateResponse({ + executionId, ...args + }) { + return this._makeRequest({ + path: `/template-responses/${executionId}`, + ...args, + }); + }, + }, +}; diff --git a/components/test_app_for_oauth_bug/package.json b/components/test_app_for_oauth_bug/package.json index 02be88bd77ddf..9623d61fc9913 100644 --- a/components/test_app_for_oauth_bug/package.json +++ b/components/test_app_for_oauth_bug/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/test_apps_for_checking_something_001/package.json b/components/test_apps_for_checking_something_001/package.json new file mode 100644 index 0000000000000..57e72a270b855 --- /dev/null +++ b/components/test_apps_for_checking_something_001/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/test_apps_for_checking_something_001", + "version": "0.0.1", + "description": "Pipedream test_apps_for_checking_something_001 Components", + "main": "test_apps_for_checking_something_001.app.mjs", + "keywords": [ + "pipedream", + "test_apps_for_checking_something_001" + ], + "homepage": "https://pipedream.com/apps/test_apps_for_checking_something_001", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/test_apps_for_checking_something_001/test_apps_for_checking_something_001.app.mjs b/components/test_apps_for_checking_something_001/test_apps_for_checking_something_001.app.mjs new file mode 100644 index 0000000000000..e24bbf06ae61a --- /dev/null +++ b/components/test_apps_for_checking_something_001/test_apps_for_checking_something_001.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "test_apps_for_checking_something_001", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/testmo/README.md b/components/testmo/README.md new file mode 100644 index 0000000000000..f15c4ce4801f6 --- /dev/null +++ b/components/testmo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Testmo API enables automation and integration of your testing workflows into the broader CI/CD pipeline. With Pipedream, you can use this API to trigger tests, update test cases, log results, and sync status with other project management tools. By creating custom serverless workflows on Pipedream, you can connect Testmo to various apps, manage test lifecycles, and respond to events from other services in real-time. + +# Example Use Cases + +- **Trigger Test Runs After Code Commits**: When a new commit is pushed to a repository in GitHub, use a Pipedream workflow to automatically trigger a series of test cases in Testmo. This ensures that new code is tested immediately, keeping your deployment pipeline efficient. + +- **Sync Test Results with Project Management Tools**: After tests are completed in Testmo, send the results to a project management tool like Jira or Asana. Use a Pipedream workflow to create or update issues based on the test outcomes, keeping your team in sync with the latest testing status. + +- **Aggregate Test Reports for Analytics**: Collect test results from Testmo and send them to a data visualization tool like Google Sheets or Data Studio. With a Pipedream workflow, you can create custom dashboards or reports that provide insights into your testing process and help identify areas for improvement. diff --git a/components/testmonitor/README.md b/components/testmonitor/README.md index b0f0e23e4b380..b3d0ddf0a0e8a 100644 --- a/components/testmonitor/README.md +++ b/components/testmonitor/README.md @@ -1,14 +1,11 @@ # Overview -The TestMonitor API provides you with a powerful way to manage and measure your -software testing process. With it you can build custom test scripts and -integrations that span multiple systems and platforms. Here are just a few -examples of what you can create with the TestMonitor API: - -- Automate tests on multiple platforms and operating systems through customized - test scripts -- Monitor API performance and defects with easy-to-use tools -- Create automated test environments for continuous integration -- Generate reports on test results and results analysis -- Create dashboards and reporting tools -- Integrate testing solutions with existing systems +TestMonitor API taps into the robust testing and project management platform, allowing you to automate issue tracking and test management processes. With this API, you can create, update, and retrieve issues, manage test cases and results, and integrate testing workflows with other systems. Leveraging Pipedream's serverless execution model, you can craft workflows that trigger on specific events within TestMonitor, reflect changes in real-time across other applications, or systematically analyze and report test outcomes. + +# Example Use Cases + +- **Automated Bug Tracking**: When a test case in TestMonitor fails, trigger a workflow that automatically creates a bug report in an issue tracking system like Jira or GitHub Issues. This keeps developers in the loop and ensures that no test failure goes unnoticed. + +- **Test Result Notifications**: Set up a workflow where team members get immediate notifications through Slack or email whenever a new test result is posted in TestMonitor. This keeps the whole team up to speed and can fast-track the fixing process. + +- **Synchronized Requirement Tracking**: Whenever a new requirement is added to a project management tool like Trello or Asana, trigger a Pipedream workflow that creates a corresponding test case in TestMonitor. This ensures that testing requirements stay in sync with project development milestones. diff --git a/components/testmonitor/actions/create-test-result/create-test-result.mjs b/components/testmonitor/actions/create-test-result/create-test-result.mjs new file mode 100644 index 0000000000000..1cba89bfcd150 --- /dev/null +++ b/components/testmonitor/actions/create-test-result/create-test-result.mjs @@ -0,0 +1,127 @@ +import { ConfigurationError } from "@pipedream/platform"; +import FormData from "form-data"; +import fs from "fs"; +import { + checkTmp, parseObject, +} from "../../common/utils.mjs"; +import testmonitor from "../../testmonitor.app.mjs"; + +export default { + key: "testmonitor-create-test-result", + name: "Create Test Result", + description: "Create a new test result. [See the docs here](https://docs.testmonitor.com/#tag/Test-Results/operation/PostTestResult)", + version: "0.0.1", + type: "action", + props: { + testmonitor, + projectId: { + propDefinition: [ + testmonitor, + "projectId", + ], + }, + testCaseId: { + propDefinition: [ + testmonitor, + "testCaseId", + ({ projectId }) => ({ + projectId, + }), + ], + }, + testRunId: { + propDefinition: [ + testmonitor, + "testRunId", + ({ projectId }) => ({ + projectId, + }), + ], + }, + draft: { + type: "boolean", + label: "Draft", + description: "Denotes if this test result is marked as draft.", + reloadProps: true, + }, + attachments: { + type: "string[]", + label: "Attachments", + description: "A list of attachment files.", + hidden: true, + optional: true, + }, + testResultStatusId: { + propDefinition: [ + testmonitor, + "testResultStatusId", + ({ projectId }) => ({ + projectId, + }), + ], + hidden: true, + }, + description: { + type: "string", + label: "Description", + description: "The description of the test result.", + optional: true, + }, + }, + async additionalProps(props) { + if (!this.draft) { + props.attachments.hidden = false; + props.testResultStatusId.hidden = false; + } + return {}; + }, + async run({ $ }) { + let testResultId; + let summary = ""; + try { + const response = await this.testmonitor.createTestResult({ + $, + data: { + test_case_id: this.testCaseId, + test_run_id: this.testRunId, + description: this.description, + draft: true, + }, + }); + testResultId = response.data.id; + + if (this.attachments) { + try { + for (const file of parseObject(this.attachments)) { + const data = new FormData(); + data.append("file", fs.createReadStream(checkTmp(file))); + await this.testmonitor.uploadAttachment({ + $, + testResultId, + data, + headers: data.getHeaders(), + }); + } + } catch (e) { + summary = ", but the attachments could not be loaded."; + } + } + + const updateResponse = await this.testmonitor.updateTestResult({ + $, + testResultId, + data: { + draft: this.draft, + test_result_status_id: this.testResultStatusId, + }, + }); + + $.export("$summary", `Successfully created test result with Id: ${testResultId}${summary}`); + return updateResponse; + } catch (e) { + throw new ConfigurationError((e.response.status === 400) + ? "It seems that there is already a test with this configuration!" + : e.response.data.message); + } + }, +}; diff --git a/components/testmonitor/actions/find-issue/find-issue.mjs b/components/testmonitor/actions/find-issue/find-issue.mjs index 46af4e0814552..8b12331e6f3b9 100644 --- a/components/testmonitor/actions/find-issue/find-issue.mjs +++ b/components/testmonitor/actions/find-issue/find-issue.mjs @@ -6,7 +6,7 @@ export default { key: "testmonitor-find-issue", name: "Find an Issue", description: "Retrieve a list of issues. [See the docs here](https://docs.testmonitor.com/#tag/Issues/operation/GetIssueCollection)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { ...common.props, diff --git a/components/testmonitor/actions/find-project/find-project.mjs b/components/testmonitor/actions/find-project/find-project.mjs index 9763253fe2967..43a7478026fa9 100644 --- a/components/testmonitor/actions/find-project/find-project.mjs +++ b/components/testmonitor/actions/find-project/find-project.mjs @@ -6,7 +6,7 @@ export default { key: "testmonitor-find-project", name: "Find a Project", description: "Retrieve a list of projects. [See the docs here](https://docs.testmonitor.com/#tag/Projects/operation/GetProjectCollection)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { ...common.props, diff --git a/components/testmonitor/actions/find-test-result/find-test-result.mjs b/components/testmonitor/actions/find-test-result/find-test-result.mjs index dc35751a08332..c05db0c104770 100644 --- a/components/testmonitor/actions/find-test-result/find-test-result.mjs +++ b/components/testmonitor/actions/find-test-result/find-test-result.mjs @@ -6,7 +6,7 @@ export default { key: "testmonitor-find-test-result", name: "Find a Test Result", description: "Retrieve a list of test results. [See the docs here](https://docs.testmonitor.com/#tag/Test-Results/operation/GetTestResultCollection)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { ...common.props, diff --git a/components/testmonitor/common/utils.mjs b/components/testmonitor/common/utils.mjs new file mode 100644 index 0000000000000..0cd1a12b6a4ba --- /dev/null +++ b/components/testmonitor/common/utils.mjs @@ -0,0 +1,31 @@ +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; + +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/testmonitor/package.json b/components/testmonitor/package.json index 0ca0c94bf9094..b3784a128c57a 100644 --- a/components/testmonitor/package.json +++ b/components/testmonitor/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/testmonitor", - "version": "0.0.3", + "version": "0.1.0", "description": "Pipedream Testmonitor Components", "main": "testmonitor.app.mjs", "keywords": [ @@ -10,7 +10,8 @@ "homepage": "https://pipedream.com/apps/testmonitor", "author": "Pipedream (https://pipedream.com/)", "dependencies": { - "@pipedream/platform": "^1.2.1", + "@pipedream/platform": "^3.0.3", + "fs": "^0.0.1-security", "moment": "^2.29.4" }, "publishConfig": { diff --git a/components/testmonitor/sources/new-issue/new-issue.mjs b/components/testmonitor/sources/new-issue/new-issue.mjs index 01702f24cdc71..fa61f228a7e95 100644 --- a/components/testmonitor/sources/new-issue/new-issue.mjs +++ b/components/testmonitor/sources/new-issue/new-issue.mjs @@ -5,7 +5,7 @@ export default { key: "testmonitor-new-issue", name: "New Issue", description: "Emit new event when a new issue is created.", - version: "0.0.2", + version: "0.0.3", type: "source", dedupe: "unique", methods: { diff --git a/components/testmonitor/sources/new-test-result/new-test-result.mjs b/components/testmonitor/sources/new-test-result/new-test-result.mjs index 2ca68bb3a7ca5..15ac1efc8a435 100644 --- a/components/testmonitor/sources/new-test-result/new-test-result.mjs +++ b/components/testmonitor/sources/new-test-result/new-test-result.mjs @@ -5,7 +5,7 @@ export default { key: "testmonitor-new-test-result", name: "New Test Result", description: "Emit new event when a new test result is created.", - version: "0.0.2", + version: "0.0.3", type: "source", dedupe: "unique", methods: { diff --git a/components/testmonitor/testmonitor.app.mjs b/components/testmonitor/testmonitor.app.mjs index ed4f0bb4317ae..a98f68bf76f1f 100644 --- a/components/testmonitor/testmonitor.app.mjs +++ b/components/testmonitor/testmonitor.app.mjs @@ -54,6 +54,70 @@ export default { })); }, }, + testCaseId: { + type: "integer", + label: "Test Case Id", + description: "The test case identifier where this test result belongs to.", + async options({ + page, projectId, + }) { + const { data } = await this.getTestCases({ + page: page + 1, + params: { + project_id: projectId, + }, + }); + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + testRunId: { + type: "integer", + label: "Test Run Id", + description: "The test run identifier where this test result belongs to.", + async options({ + page, projectId, + }) { + const { data } = await this.getTestRuns({ + page: page + 1, + params: { + project_id: projectId, + }, + }); + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + testResultStatusId: { + type: "integer", + label: "Test Result Status Id", + description: "The test result status identifier.", + async options({ + page, projectId, + }) { + const { data } = await this.getResultStatuses({ + params: { + page: page + 1, + project_id: projectId, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, query: { type: "string", label: "Query", @@ -88,28 +152,68 @@ export default { _apiUrl() { return `https://${this.$auth.domain}.testmonitor.com/api/v1`; }, - _getHeaders() { + _getHeaders(headers = {}) { return { + ...headers, "Authorization": `Bearer ${this.$auth.api_token}`, }; }, async _makeRequest({ - $ = this, path, ...opts + $ = this, path, headers, ...opts }) { const config = { url: `${this._apiUrl()}/${path}`, - headers: this._getHeaders(), + headers: this._getHeaders(headers), ...opts, }; + console.log("config: ", config); + return axios($, config); }, + createTestResult(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "test-results", + ...opts, + }); + }, + updateTestResult({ + testResultId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `test-results/${testResultId}`, + ...opts, + }); + }, + uploadAttachment({ + testResultId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `test-result/${testResultId}/attachments`, + ...opts, + }); + }, + getTestCases(opts = {}) { + return this._makeRequest({ + path: "test-cases", + ...opts, + }); + }, + getTestRuns(opts = {}) { + return this._makeRequest({ + path: "test-runs", + ...opts, + }); + }, getIssue({ - $, issueId, + issueId, ...opts }) { return this._makeRequest({ - $, path: `issues/${issueId}`, + ...opts, }); }, getIssues(params) { @@ -119,11 +223,11 @@ export default { }); }, getProject({ - $, projectId, + projectId, ...opts }) { return this._makeRequest({ - $, path: `projects/${projectId}`, + ...opts, }); }, getProjects(params) { @@ -133,11 +237,17 @@ export default { }); }, getTestResult({ - $, testResultId, + testResultId, ...opts }) { return this._makeRequest({ - $, path: `test-results/${testResultId}`, + ...opts, + }); + }, + getResultStatuses(opts = {}) { + return this._makeRequest({ + path: "test-result-statuses", + ...opts, }); }, getTestResults(params) { diff --git a/components/testmonitor/yarn.lock b/components/testmonitor/yarn.lock new file mode 100644 index 0000000000000..87d77b5df366b --- /dev/null +++ b/components/testmonitor/yarn.lock @@ -0,0 +1,95 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@pipedream/platform@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@pipedream/platform/-/platform-3.0.3.tgz#b0f1d1274e061fb581635a30fabc830358975fd2" + integrity sha512-7elalas41lnT8i6EAFkqB7fT/+hkLGEQ1njS6A7CVguTrEswaIYk/seKmkfkRY7+O6qncgnXswYIKCBML9Co7w== + dependencies: + axios "^1.7.4" + fp-ts "^2.0.2" + io-ts "^2.0.0" + querystring "^0.2.1" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^1.7.4: + version "1.7.9" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a" + integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + +form-data@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48" + integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +fp-ts@^2.0.2: + version "2.16.9" + resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.16.9.tgz#99628fc5e0bb3b432c4a16d8f4455247380bae8a" + integrity sha512-+I2+FnVB+tVaxcYyQkHUq7ZdKScaBlX53A41mxQtpIccsfyv8PzdzP7fzp2AY832T4aoK6UZ5WRX/ebGd8uZuQ== + +fs@^0.0.1-security: + version "0.0.1-security" + resolved "https://registry.yarnpkg.com/fs/-/fs-0.0.1-security.tgz#8a7bd37186b6dddf3813f23858b57ecaaf5e41d4" + integrity sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w== + +io-ts@^2.0.0: + version "2.2.21" + resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.2.21.tgz#4ef754176f7082a1099d04c7d5c4ea53267c530a" + integrity sha512-zz2Z69v9ZIC3mMLYWIeoUcwWD6f+O7yP92FMVVaXEOSZH1jnVBmET/urd/uoarD1WGBY4rCj8TAyMPzsGNzMFQ== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +moment@^2.29.4: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +querystring@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd" + integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg== diff --git a/components/tettra/README.md b/components/tettra/README.md new file mode 100644 index 0000000000000..436013768ffcc --- /dev/null +++ b/components/tettra/README.md @@ -0,0 +1,11 @@ +# Overview + +The Tettra API lets you automate your knowledge management tasks within the Tettra knowledge base. Using Pipedream, you can create workflows to streamline content creation, manage pages and categories, and sync with your team's tool stack. Pipedream’s serverless platform enables you to connect Tettra with hundreds of other apps to automate complex processes, share information across teams and systems, and trigger actions based on events in Tettra or other integrated services. + +# Example Use Cases + +- **Sync Tettra Updates to Slack**: Automatically post Tettra page updates or new pages to a dedicated Slack channel. This keeps your team informed about the latest knowledge base changes without leaving their communication hub. + +- **Tettra Content Backup**: Create a workflow that triggers at regular intervals to back up Tettra pages to a cloud storage solution like Google Drive or Dropbox. This ensures your team's knowledge is safe and version-controlled. + +- **Tettra Page Creation from GitHub Issues**: When a new issue is tagged as 'documentation' on GitHub, trigger a workflow to create a new Tettra page draft. This helps in maintaining up-to-date documentation for project repos and makes it easier to track documentation tasks. diff --git a/components/textcortex/README.md b/components/textcortex/README.md new file mode 100644 index 0000000000000..1a92a0c079aec --- /dev/null +++ b/components/textcortex/README.md @@ -0,0 +1,11 @@ +# Overview + +The TextCortex API enables automated creation of human-like text, making it a powerful tool for a variety of text generation tasks like drafting emails, creating content, or generating product descriptions. With Pipedream, you can integrate TextCortex into serverless workflows to harness AI writing assistance, trigger content creation based on events, or enhance data with generated text. The API's capability enhances automation and can be combined with other apps for more complex tasks. + +# Example Use Cases + +- **Automatically Generate Blog Post Drafts**: Trigger a workflow when a new topic is added to your content management system (e.g., WordPress). Use TextCortex to generate a draft post, then push the draft back to WordPress for review. + +- **Enhance CRM Entries with Personalized Descriptions**: When a new product is added to your CRM (e.g., Salesforce), use TextCortex to create a unique product description. This description can then be automatically uploaded to your CRM, keeping listings fresh and engaging. + +- **Auto-Compose and Send Customized Emails**: Set up a trigger for new sign-ups from a form or service (e.g., Typeform). Use TextCortex to compose a personalized welcome email based on the user's input, and send it using an email service provider like SendGrid. diff --git a/components/textgain/README.md b/components/textgain/README.md new file mode 100644 index 0000000000000..78cf0e6d1aa57 --- /dev/null +++ b/components/textgain/README.md @@ -0,0 +1,11 @@ +# Overview + +The Textgain API allows you to tap into advanced text analytics via Pipedream. It's designed to provide insights into text for various applications like sentiment analysis, language detection, and concept extraction. By integrating with Pipedream, you can automate the extraction of valuable data from text and connect it with countless other services for enhanced workflows, data processing, and decision-making. + +# Example Use Cases + +- **Sentiment Analysis for Customer Feedback**: Automate the collection of customer feedback from various sources like support tickets, reviews, or social media. Use the Textgain API on Pipedream to assess sentiment and route positive feedback to marketing platforms, while alerting customer service to address negative sentiments instantly. + +- **Language Detection and Translation Workflow**: Develop a system that detects the language of incoming messages from a chat application using Textgain's language detection. If the message isn't in the primary language of your support team, use a translation service like Google Translate (also available on Pipedream) to translate the message and continue the support process seamlessly. + +- **Concept Extraction for Content Enhancement**: Enhance your content strategy by extracting key concepts from articles, blog posts, or other text sources. Use Textgain API on Pipedream to identify trending topics or concepts and connect with a CMS platform to push relevant content or with a keyword tool to optimize SEO strategies. diff --git a/components/textit/README.md b/components/textit/README.md index ab28a376ce738..126a89a6ee401 100644 --- a/components/textit/README.md +++ b/components/textit/README.md @@ -1,25 +1,11 @@ # Overview -With the TextIt API, you can build a world of automated messaging applications -to support your own projects, business, or development needs. Here are just a -few of the many features that you can build using TextIt API: +TextIt is an API that specializes in automating SMS, voice, and social messaging workflows. By leveraging Pipedream's integration capabilities, the TextIt API can be used to create powerful communication automations that respond in real-time to incoming messages, dispatch notifications, and interact with users. With Pipedream, these automations can be connected to a plethora of services to enhance CRM systems, facilitate survey collection, or streamline event-driven notifications. -- Automated customer support applications -- Two-way messaging bots -- Lead generation systems -- Surveys and polls -- Cross-platform messaging services -- Group messaging -- Appointment reminders -- Bulk messaging services -- Automated information and broadcast services -- Multilingual messaging services -- And much more! +# Example Use Cases -With all of these great features, the possibilities with TextIt API are -endless. Whether you are building something for your business, personal -project, or just want to explore the possibilities of TextIt API, you can use -it to build practically anything you can dream of. +- **Customer Support Automation**: Automatically receive customer queries from TextIt and create tickets in a service like Zendesk. Use conditions to categorize and prioritize messages based on keywords, and respond with automated messages for common questions. -So what are you waiting for? Get started with TextIt API today, and start -building your application! +- **Survey Data Collection**: Send surveys via TextIt and collect responses in a Google Sheet. Analyze responses with Google Scripts to generate insights or trigger follow-up actions when specific answers are provided. + +- **Event-Triggered Notifications**: Integrate TextIt with an e-commerce platform like Shopify. Automatically notify customers about order confirmations, shipping updates, and delivery statuses via SMS, improving the overall customer experience. diff --git a/components/textline/actions/create-update-contact/create-update-contact.mjs b/components/textline/actions/create-update-contact/create-update-contact.mjs new file mode 100644 index 0000000000000..84739e20f67b3 --- /dev/null +++ b/components/textline/actions/create-update-contact/create-update-contact.mjs @@ -0,0 +1,122 @@ +import app from "../../textline.app.mjs"; + +export default { + key: "textline-create-update-contact", + name: "Create Or Update Contact", + description: "Create or update a contact in the Textline address book. [See the documentation](https://textline.docs.apiary.io/#reference/customers/customers/create-a-customer).", + version: "0.0.1", + type: "action", + props: { + app, + phoneNumber: { + propDefinition: [ + app, + "phoneNumber", + ], + }, + email: { + type: "string", + label: "Email", + description: "The email of the contact.", + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "The name of the contact.", + optional: true, + }, + notes: { + type: "string", + label: "Notes", + description: "Notes about the contact.", + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "Tags associated with the contact.", + optional: true, + }, + }, + methods: { + updateCustomer({ + uuid, ...args + } = {}) { + return this.app.put({ + path: `/customer/${uuid}`, + ...args, + }); + }, + createCustomer(args = {}) { + return this.app.post({ + path: "/customers", + ...args, + }); + }, + }, + async run({ $ }) { + const { + app, + updateCustomer, + createCustomer, + phoneNumber, + email, + name, + notes, + tags, + } = this; + + let response; + let uuid; + + try { + response = await createCustomer({ + $, + data: { + customer: { + phone_number: phoneNumber, + email, + name, + notes, + tags, + }, + }, + }); + } catch (error) { + if (error.response.status === 400 && error.response.data?.errors?.phone_number[0] === "Already in use") { + + ({ customer: { uuid } } = await app.getCustomerByPhoneNumber({ + $, + params: { + phone_number: phoneNumber, + }, + })); + + } else { + throw error; + } + } + + if (!uuid) { + $.export("$summary", `Successfully created the contact with uuid \`${response.customer.uuid}\`.`); + return response; + } + + response = await updateCustomer({ + $, + uuid, + data: { + customer: { + email, + name, + notes, + tags, + }, + }, + }); + + $.export("$summary", `Successfully updated the contact with uuid \`${uuid}\`.`); + return response; + }, +}; diff --git a/components/textline/actions/send-announcement/send-announcement.mjs b/components/textline/actions/send-announcement/send-announcement.mjs new file mode 100644 index 0000000000000..4eb0ea77af620 --- /dev/null +++ b/components/textline/actions/send-announcement/send-announcement.mjs @@ -0,0 +1,119 @@ +import app from "../../textline.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "textline-send-announcement", + name: "Send Announcement", + description: "Send an announcement to a group of contacts. [See the documentation](https://textline.docs.apiary.io/#reference/messaging-tools/announcements/send-an-announcement).", + version: "0.0.1", + type: "action", + props: { + app, + massTextGroupUuid: { + label: "Mass Text Group UUID", + description: "The UUID of the mass text group.", + propDefinition: [ + app, + "groupUuid", + ], + }, + massTextCommentBody: { + type: "string", + label: "Mass Text Comment Body", + description: "The content of the message to send.", + }, + massTextTitle: { + type: "string", + label: "Mass Text Title", + description: "The title of the message.", + }, + selectionType: { + type: "string", + label: "Selection Type", + description: "The type of the selection for the announcement.", + reloadProps: true, + options: Object.values(constants.SELECTION_TYPE), + }, + }, + additionalProps() { + const { selectionType } = this; + + if (selectionType === constants.SELECTION_TYPE.TAGS.value) { + return { + tag: { + type: "string", + label: "Tag", + description: "Send to all contacts with this tag.", + }, + }; + } + + if (selectionType === constants.SELECTION_TYPE.PHONE_NUMBERS.value) { + return { + phoneNumbers: { + type: "string[]", + label: "Phone Numbers", + description: "The phone numbers to send the announcement to.", + useQuery: true, + options: async ({ + page, query, + }) => { + const { customers } = await this.app.listCustomers({ + params: { + page, + page_size: 30, + query: query || "", + }, + }); + return customers.map(({ + name: label, phone_number: value, + }) => ({ + label, + value, + })); + }, + }, + }; + } + + return {}; + }, + methods: { + sendAnnouncement(args = {}) { + return this.app.post({ + path: "/announcements", + ...args, + }); + }, + }, + async run({ $ }) { + const { + sendAnnouncement, + selectionType, + massTextGroupUuid, + massTextCommentBody, + massTextTitle, + tag, + phoneNumbers, + } = this; + + const response = await sendAnnouncement({ + $, + data: { + selection_type: selectionType, + recipients: { + tag, + phone_numbers: phoneNumbers, + }, + mass_text: { + group_uuid: massTextGroupUuid, + comment_body: massTextCommentBody, + title: massTextTitle, + }, + }, + }); + + $.export("$summary", "Successfully sent the announcement."); + return response; + }, +}; diff --git a/components/textline/actions/send-message/send-message.mjs b/components/textline/actions/send-message/send-message.mjs new file mode 100644 index 0000000000000..55316e853d1a9 --- /dev/null +++ b/components/textline/actions/send-message/send-message.mjs @@ -0,0 +1,51 @@ +import app from "../../textline.app.mjs"; + +export default { + key: "textline-send-message", + name: "Send Message", + description: "Send a new message directly to a contact. [See the documentation](https://textline.docs.apiary.io/#reference/conversations/group-conversations/message-a-phone-number).", + version: "0.0.1", + type: "action", + props: { + app, + phoneNumber: { + propDefinition: [ + app, + "phoneNumber", + ], + }, + comment: { + type: "string", + label: "Message", + description: "The content of the message to send.", + }, + }, + methods: { + sendMessage(args = {}) { + return this.app.post({ + path: "/conversations", + ...args, + }); + }, + }, + async run({ $ }) { + const { + sendMessage, + phoneNumber, + comment, + } = this; + const response = await sendMessage({ + $, + data: { + phone_number: phoneNumber, + comment: { + body: comment, + }, + resolve: "1", + }, + }); + + $.export("$summary", `Successfully sent message to ${phoneNumber}.`); + return response; + }, +}; diff --git a/components/textline/common/constants.mjs b/components/textline/common/constants.mjs new file mode 100644 index 0000000000000..740c099551cb8 --- /dev/null +++ b/components/textline/common/constants.mjs @@ -0,0 +1,21 @@ +const BASE_URL = "https://application.textline.com"; +const VERSION_PATH = "/api"; +const DEFAULT_LIMIT = 30; + +const SELECTION_TYPE = { + TAGS: { + label: "Tags", + value: "tags", + }, + PHONE_NUMBERS: { + label: "Phone Numbers", + value: "phone_numbers", + }, +}; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_LIMIT, + SELECTION_TYPE, +}; diff --git a/components/textline/package.json b/components/textline/package.json new file mode 100644 index 0000000000000..735492706153f --- /dev/null +++ b/components/textline/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/textline", + "version": "0.1.0", + "description": "Pipedream Textline Components", + "main": "textline.app.mjs", + "keywords": [ + "pipedream", + "textline" + ], + "homepage": "https://pipedream.com/apps/textline", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/textline/textline.app.mjs b/components/textline/textline.app.mjs new file mode 100644 index 0000000000000..8ebf4d8bc4a7d --- /dev/null +++ b/components/textline/textline.app.mjs @@ -0,0 +1,114 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "textline", + propDefinitions: { + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number of the contact. Eg. `(222) 222-2222`.", + useQuery: true, + async options({ + page, + query, + mapper = ({ + name: label, phone_number: value, + }) => ({ + label, + value, + }), + }) { + const { customers } = await this.listCustomers({ + params: { + page, + page_size: constants.DEFAULT_LIMIT, + query: query || "", + }, + }); + return customers.map(mapper); + }, + }, + groupUuid: { + type: "string", + label: "Group UUID", + description: "The UUID of the group or organization.", + async options({ + params = { + include_groups: true, + include_users: false, + }, + }) { + const { groups } = await this.listOrganizationDetails({ + params, + }); + return groups.map(({ + name: label, uuid: value, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}.json`; + }, + getHeaders(headers) { + return { + ...headers, + "Content-Type": "application/json", + "x-tgp-access-token": this.$auth.access_token, + }; + }, + async _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + try { + return await axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + } catch (error) { + if (error.response?.status === 500) { + console.log("Error response", error.response); + throw new Error("Textline is currently experiencing issues. Please try again later."); + } + throw error; + } + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + put(args = {}) { + return this._makeRequest({ + method: "PUT", + ...args, + }); + }, + getCustomerByPhoneNumber(args = {}) { + return this._makeRequest({ + path: "/customers", + ...args, + }); + }, + listOrganizationDetails(args = {}) { + return this._makeRequest({ + path: "/organization", + ...args, + }); + }, + listCustomers(args = {}) { + return this._makeRequest({ + path: "/customers", + ...args, + }); + }, + }, +}; diff --git a/components/textlocal/README.md b/components/textlocal/README.md index ea6b3e2871a7c..863d47bca43cf 100644 --- a/components/textlocal/README.md +++ b/components/textlocal/README.md @@ -1,15 +1,11 @@ # Overview -You can do so much with the Textlocal API! Textlocal is an easy-to-use platform -for sending and receiving mobile and SMS messages, making it a great avenue for -businesses to communicate with their customers. Here are a few examples of what -you can build with the Textlocal API: - -- Automate SMS marketing campaigns to customers -- Notify customers of special offers and discounts -- Send reminders and appointment confirmations -- Create location-based notifications -- Receive two-way communication from customers -- Integrate text alerts into other applications, like business workflows and - CRMs -- Send mass notifications during emergencies +The Textlocal API on Pipedream allows for robust SMS messaging capabilities within workflows. You can send notifications, alerts, and updates directly to mobile users, automate marketing campaigns, or integrate SMS into multi-channel communication strategies. By leveraging Pipedream's serverless platform, you can create complex workflows involving Textlocal for various application domains without managing infrastructure, boosting productivity and engagement through the power of automated text messaging. + +# Example Use Cases + +- **SMS Notifications for E-commerce Orders**: Whenever a new order is placed on an e-commerce platform like Shopify, trigger a workflow in Pipedream that sends an order confirmation SMS to the customer using Textlocal. The message can include details like order number, expected delivery date, and a thank you note. + +- **Appointment Reminders for Healthcare**: In a healthcare app, use the Google Calendar API to monitor upcoming appointments. Set up a Pipedream workflow that sends SMS reminders to patients 24 hours before their appointment using Textlocal, reducing no-show rates and improving patient compliance. + +- **Real-time Alerts from IoT Devices**: Connect IoT devices, such as temperature sensors or security cameras, to Pipedream. Set a workflow that triggers an SMS alert via Textlocal when certain thresholds are met or anomalies are detected, providing immediate notification for quick action. diff --git a/components/textmagic/README.md b/components/textmagic/README.md index 0f2adf50330bd..2adf09fa9c925 100644 --- a/components/textmagic/README.md +++ b/components/textmagic/README.md @@ -1,26 +1,11 @@ # Overview -TextMagic API is a cloud-based platform that provides a range of messaging API -and Services that enables developers to quickly and easily add text messaging, -voice services and two-way conversations into web, mobile and other types of -applications. With TextMagic API, you are able to build reliable messaging -services, ranging from simple notifications and alerts to more robust -applications involving multi-step workflows and automation. +The TextMagic API offers a robust platform for managing and automating SMS communications. With this API, you can programmatically send text messages, manage contacts, create distribution lists, and track message delivery status. Leveraging Pipedream's capabilities, you can integrate TextMagic with a wide array of services to craft workflows that streamline notifications, alerts, and customer interactions through the power of SMS. -Here are some examples of what you can build using TextMagic API: +# Example Use Cases -- SMS Authentication & Verification: You can use TextMagic API to quickly and - easily verify the identity of your customers through one-time codes delivered - directly via SMS. -- Cloud-to-Device Messaging: You can use TextMagic API to interact directly - with your customers' device with messages, notifications, and alerts. -- Bulk Messaging: You can use TextMagic API to send one-to-many text or voice - messages to large audiences with just a few lines of code. -- Automation & Workflows: You can use TextMagic API to create automated - workflows that send out messages based on customer behavior, trigger - notifications, and schedule automated messages. -- Mobile App Integration: You can use TextMagic API to integrate your mobile - applications with text and voice messaging services. -- Multi-language Support: You can use TextMagic API to support over 200 - different languages, making it easier to send out messages in multiple - languages. +- **Automated Customer Support Notifications**: Trigger an SMS to a customer when their support ticket status changes in your helpdesk software. For instance, connect TextMagic to Zendesk on Pipedream to notify customers instantly when their ticket is received, updated, or resolved, enhancing the support experience. + +- **Appointment Reminders**: Integrate TextMagic with a calendar application like Google Calendar using Pipedream. Set up a workflow that sends out text message reminders to clients for upcoming appointments, cutting down on no-shows and ensuring your schedule remains efficient. + +- **Marketing Campaign Tracking**: Use TextMagic with an e-commerce platform like Shopify on Pipedream. Create a workflow that sends promotional messages to a curated list of contacts and then tracks engagement through delivery and click reports, giving you actionable insights into your marketing efforts. diff --git a/components/textrazor/README.md b/components/textrazor/README.md index 109dc7c6a73b7..8504de521d46b 100644 --- a/components/textrazor/README.md +++ b/components/textrazor/README.md @@ -1,28 +1,11 @@ # Overview -TextRazor is an API which provides powerful Natural Language Processing (NLP) -tools to help developers add enhanced analysis and structuring capabilities to -their applications. With its easy-to-use API, developers can analyze and -extract meaning from text, enabling richer applications in a range of -industries. +The TextRazor API offers advanced natural language processing capabilities that let you analyze text, extracting entities, concepts, and categorizing content with ease. Utilizing machine learning and AI, TextRazor can break down your content into actionable data, identifying the sentiment, relationship, and structure of the text provided. It's a gold mine for developers looking to automate content analysis, enrich search capabilities, or implement AI-driven content recommendations. -Some of the features include: +# Example Use Cases -- Morphological Analysis -- POS Tagging -- Entity Extraction -- Semantic Analysis +- **Content Categorization and Tagging**: Automate the process of content categorization by feeding articles or text snippets into TextRazor through Pipedream. Once analyzed, the returned entities and concepts can be used to tag and sort content in a CMS like WordPress, enabling enhanced searchability and content discovery. -Here are some examples of the possibilities using TextRazor: +- **Customer Feedback Analysis**: Connect TextRazor with a CRM platform like HubSpot on Pipedream. Analyze customer feedback or support tickets to extract sentiment and key topics. Use this data to route tickets to the appropriate teams automatically and to generate insights on common customer issues and sentiments. -- Automated Summarization and Keyword Extraction -- Natural Language Processing for Chatbots -- Topic and Sentiment Analysis -- Media Intelligence -- Content Categorization -- Question Answering -- Text-to-Speech Recognition -- Image Recognition -- Fraud Detection -- Spam Detection -- Data Clustering +- **Trend Monitoring and Insights**: Use TextRazor to monitor social media platforms or RSS feeds for trending topics and sentiment around your brand or industry. With Pipedream, you can integrate this analysis with tools like Slack to alert teams about real-time trends or potential PR crises, allowing for swift and informed responses. diff --git a/components/textrazor/package.json b/components/textrazor/package.json new file mode 100644 index 0000000000000..882ff672a972d --- /dev/null +++ b/components/textrazor/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/textrazor", + "version": "0.6.0", + "description": "Pipedream textrazor Components", + "main": "textrazor.app.mjs", + "keywords": [ + "pipedream", + "textrazor" + ], + "homepage": "https://pipedream.com/apps/textrazor", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/thanks_io/README.md b/components/thanks_io/README.md index 760469c0748b1..bfc197fb34189 100644 --- a/components/thanks_io/README.md +++ b/components/thanks_io/README.md @@ -1,23 +1,11 @@ # Overview -thanks.io provides an API that will help you recognize and reward your -employees, customers, and partners. With thanks.io, you can quickly and easily -create personalized tokens of appreciation with messages and funds. +The Thanks.io API lets you automate the sending of personalized postcards and letters. This API can be harnessed to craft tailored outreach campaigns, customer appreciation notes, or event-based mailings. By integrating with Pipedream, you can create intelligent, event-driven workflows that trigger mailings based on user activity, data changes, or milestones achieved in other apps, streamlining the way businesses and developers connect with their audiences physically. -Thanks.io offers powerful API features that let you create custom recognition -experiences for your clients and employees. With API calls, you can: +# Example Use Cases -- Create customizable message tokens that your team can send to recognize - individual accomplishments -- Reward your users with token-based payments for a job well done -- Track the performance of each user and reward them in real-time -- Easily create promo codes to give thanks to your team -- Issue holiday tokens as a way of saying thanks +- **Customer Appreciation Workflow**: Automate the sending of thank you postcards to customers after they make a purchase. Trigger a Thanks.io postcard dispatch by connecting Pipedream to an e-commerce platform, like Shopify, capturing new order events. -Examples of what you can build using the thanks.io API: +- **Birthday Club Mailing**: Build a workflow that sends out personalized birthday cards. Use a CRM platform, such as HubSpot, integrated with Pipedream to trigger a Thanks.io mailing each time a contact's birthday is approaching, fostering customer loyalty. -- Employee recognition programs -- Client loyalty programs -- Incentive programs -- Referral programs -- Gift giving programs +- **Real Estate Follow-Up**: For real estate agents, automate follow-ups with prospects or clients post-viewing. Connect a scheduling app like Calendly to Pipedream, triggering a Thanks.io mailer when a viewing is completed, adding a personal touch to the home buying process. diff --git a/components/thankster/README.md b/components/thankster/README.md new file mode 100644 index 0000000000000..0ee53bcbba7a3 --- /dev/null +++ b/components/thankster/README.md @@ -0,0 +1,11 @@ +# Overview + +The Thankster API lets you automate the sending of personalized, handwritten cards. This is particularly handy for businesses looking to scale their outreach while keeping a personal touch. With Pipedream, you can craft workflows that trigger card creation and dispatch based on various events and conditions. Whether it's a customer's birthday, a follow-up after a service, or a thank you note for a purchase, Pipedream's serverless platform enables you to set up these actions without setting up and managing infrastructure. + +# Example Use Cases + +- **Customer Birthday Celebration**: Automatically send a personalized, handwritten birthday card to customers on their birthday. Integrate with a CRM like HubSpot to fetch customer birthdates and use the Thankster API to send the cards. + +- **E-commerce Purchase Follow-Up**: Create a workflow where after a customer completes a purchase on your Shopify store, they receive a thank you card from your brand, boosting customer loyalty and engagement. + +- **Event Attendee Appreciation**: Post-event, send attendees a handwritten note thanking them for their presence. Connect with Eventbrite to collect attendee details, and use the Thankster API to send out the appreciation cards. diff --git a/components/the_bookie/actions/create-contact/create-contact.mjs b/components/the_bookie/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..c5811540b666a --- /dev/null +++ b/components/the_bookie/actions/create-contact/create-contact.mjs @@ -0,0 +1,139 @@ +import { ConfigurationError } from "@pipedream/platform"; +import thebookie from "../../the_bookie.app.mjs"; + +export default { + key: "the_bookie-create-contact", + name: "Create Contact", + description: "Instantly creates a new contact in the address book. [See the documentation](https://app.thebookie.nl/nl/help/article/api-documentatie/#contact_create)", + version: "0.0.1", + type: "action", + props: { + thebookie, + organisationName: { + type: "string", + label: "Organisation Name", + description: "The contact's organization name", + }, + street: { + type: "string", + label: "Street", + description: "The contact's address street", + optional: true, + }, + streetNumber: { + type: "string", + label: "Street Number", + description: "The contact's address number", + optional: true, + }, + streetNumberAddition: { + type: "string", + label: "Street Number Addition", + description: "The contact's address number addition", + optional: true, + }, + extraAddressLine: { + type: "string", + label: "Extra Address Line", + description: "The contact's extra address line", + optional: true, + }, + postalCode: { + type: "string", + label: "Postal Code", + description: "The contact's address postal code", + optional: true, + }, + town: { + type: "string", + label: "Town", + description: "The contact's city", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "The contact's country", + optional: true, + }, + isSupplier: { + type: "boolean", + label: "Is Supplier", + description: "Whether the contact is supplier or not", + optional: true, + }, + isClient: { + type: "boolean", + label: "Is Client", + description: "Whether the contact is client or not", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "The contact's email address", + optional: true, + }, + telephoneNumber: { + type: "string", + label: "Telephone Number", + description: "The contact's telephone number", + optional: true, + }, + mobileNumber: { + type: "string", + label: "Mobile Number", + description: "The contact's mobile number", + optional: true, + }, + firstName: { + type: "string", + label: "First Name", + description: "First name of the contact", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the contact", + optional: true, + }, + extraInfo: { + type: "string", + label: "Extra Info", + description: "An additional info", + optional: true, + }, + }, + async run({ $ }) { + if (!this.isClient && !this.isSupplier) { + throw new ConfigurationError("'Is Supplier' or 'Is Client' must be true (or both)"); + } + + const response = await this.thebookie.createContact({ + $, + data: { + organisation_name: this.organisationName, + street: this.street, + street_number: this.streetNumber, + street_number_addition: this.streetNumberAddition, + extra_address_line: this.extraAddressLine, + postal_code: this.postalCode, + town: this.town, + country: this.country, + is_supplier: this.isSupplier, + is_client: this.isClient, + email: this.email, + telephone_number: this.telephoneNumber, + mobile_number: this.mobileNumber, + first_name: this.firstName, + last_name: this.lastName, + extra_info: this.extraInfo, + }, + }); + + $.export("$summary", `Successfully created contact with ID ${response.id}`); + + return response; + }, +}; diff --git a/components/the_bookie/actions/create-sales-invoice/create-sales-invoice.mjs b/components/the_bookie/actions/create-sales-invoice/create-sales-invoice.mjs new file mode 100644 index 0000000000000..ed3bc56c00fb5 --- /dev/null +++ b/components/the_bookie/actions/create-sales-invoice/create-sales-invoice.mjs @@ -0,0 +1,99 @@ +import { ConfigurationError } from "@pipedream/platform"; +import fs from "fs"; +import { + checkTmp, parseObject, +} from "../../common/utils.mjs"; +import theBookie from "../../the_bookie.app.mjs"; + +export default { + key: "the_bookie-create-sales-invoice", + name: "Create Sales Invoice", + description: "Creates a new sales invoice. [See the documentation](https://app.thebookie.nl/nl/help/article/api-documentatie/#salesentry_create)", + version: "0.0.1", + type: "action", + props: { + theBookie, + contactId: { + propDefinition: [ + theBookie, + "contactId", + ], + }, + invoiceNumber: { + type: "string", + label: "Invoice Number", + description: "The number of the invoice", + }, + invoiceDate: { + type: "string", + label: "Invoice Date", + description: "The date of the invoice. **Format: YYYY-MM-DD**", + }, + expirationDate: { + type: "string", + label: "Expiration Date", + description: "The expiration date of the invoice. **Format: YYYY-MM-DD**", + }, + btwShifted: { + type: "string", + label: "VAT shifted", + description: "The VAT type", + options: [ + { + label: "No (standard)", + value: "NONE", + }, + { + label: "Shifted within The Netherlands", + value: "NL", + }, + { + label: "Shifted within EU", + value: "EU", + }, + { + label: "Shifted outside EU", + value: "NON_EU", + }, + ], + optional: true, + }, + journalEntryLines: { + type: "string[]", + label: "Journal Entry Lines", + description: "An array of stringified objects of item entry lines. **Example: { \"description\": \"Boekregel 1\", \"btw_type\": \"PROCENT_21\", \"amount\": \"1200.0\", \"quantity\": \"2.00\"}** btw_type can be only 'PERCENT_9', 'PERCENT_21' or 'PERCENT_0'", + optional: true, + }, + attachment: { + type: "string", + label: "Attachment", + description: "The path to the pdf file saved to the `/tmp` directory (e.g. `/tmp/example.pdf`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + optional: true, + }, + }, + async run({ $ }) { + if (!this.journalEntryLines) { + throw new ConfigurationError("At least one (1) 'Journal Entry Line' should be added"); + } + if (this.attachment) { + this.attachment = fs.readFileSync(checkTmp(this.attachment), { + encoding: "base64", + }); + } + const response = await this.theBookie.createInvoice({ + $, + data: { + contact_id: this.contactId, + invoice_number: this.invoiceNumber, + invoice_date: this.invoiceDate, + expiration_date: this.expirationDate, + btw_shifted: this.btwShifted, + journal_entry_lines: parseObject(this.journalEntryLines), + attachment: this.attachment, + }, + }); + + $.export("$summary", `Successfully created invoice with number ${this.invoiceNumber}`); + return response; + }, +}; diff --git a/components/the_bookie/actions/find-contact/find-contact.mjs b/components/the_bookie/actions/find-contact/find-contact.mjs new file mode 100644 index 0000000000000..c7e38a7d27e77 --- /dev/null +++ b/components/the_bookie/actions/find-contact/find-contact.mjs @@ -0,0 +1,51 @@ +import { capitalize } from "../../common/utils.mjs"; +import theBookie from "../../the_bookie.app.mjs"; + +export default { + key: "the_bookie-find-contact", + name: "Find Contacts", + description: "Find a contact from the address book. [See the documentation](https://app.thebookie.nl/nl/help/article/api-documentatie/#contact_list)", + version: "0.0.1", + type: "action", + props: { + theBookie, + search: { + type: "string", + label: "Search", + description: "Search by company name.", + optional: true, + }, + isClient: { + type: "boolean", + label: "Is Client", + description: "Return only client contacts.", + optional: true, + }, + isSupplier: { + type: "boolean", + label: "Is Supplier", + description: "Return only supplier contacts.", + optional: true, + }, + }, + async run({ $ }) { + const response = this.theBookie.paginate({ + $, + fn: this.theBookie.searchContact, + params: { + search: this.search, + is_client: capitalize(this.isClient), + is_supplier: capitalize(this.isSupplier), + }, + }); + + const responseArray = []; + + for await (const item of response) { + responseArray.push(item); + } + + $.export("$summary", `Found ${responseArray.length} contact(s)`); + return responseArray; + }, +}; diff --git a/components/the_bookie/common/constants.mjs b/components/the_bookie/common/constants.mjs new file mode 100644 index 0000000000000..ea830c15a04cb --- /dev/null +++ b/components/the_bookie/common/constants.mjs @@ -0,0 +1 @@ +export const LIMIT = 100; diff --git a/components/the_bookie/common/utils.mjs b/components/the_bookie/common/utils.mjs new file mode 100644 index 0000000000000..339bf1a6c02b9 --- /dev/null +++ b/components/the_bookie/common/utils.mjs @@ -0,0 +1,37 @@ +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; + +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; + +export const capitalize = (string) => { + if (!string) return null; + string = string.toString(); + return string[0].toUpperCase() + string.slice(1); +}; diff --git a/components/the_bookie/package.json b/components/the_bookie/package.json new file mode 100644 index 0000000000000..12092ea586510 --- /dev/null +++ b/components/the_bookie/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/the_bookie", + "version": "0.1.0", + "description": "Pipedream The Bookie Components", + "main": "the_bookie.app.mjs", + "keywords": [ + "pipedream", + "the_bookie" + ], + "homepage": "https://pipedream.com/apps/the_bookie", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.1" + } +} + diff --git a/components/the_bookie/sources/common/base.mjs b/components/the_bookie/sources/common/base.mjs new file mode 100644 index 0000000000000..ad1a51d9c1bc9 --- /dev/null +++ b/components/the_bookie/sources/common/base.mjs @@ -0,0 +1,64 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import theBookie from "../../the_bookie.app.mjs"; + +export default { + props: { + theBookie, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + filterItems(items, lastId) { + return items + .filter((item) => item.id > lastId) + .sort((a, b) => b.id - a.id); + }, + async emitEvent(maxResults = false) { + const lastId = this._getLastId(); + const response = this.theBookie.paginate({ + fn: this.getFunction(), + }); + + let responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + if (responseArray.length) { + responseArray = this.filterItems(responseArray, lastId); + + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastId(responseArray[0].id); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(new Date()), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/the_bookie/sources/new-contact-created/new-contact-created.mjs b/components/the_bookie/sources/new-contact-created/new-contact-created.mjs new file mode 100644 index 0000000000000..a334d763cfb74 --- /dev/null +++ b/components/the_bookie/sources/new-contact-created/new-contact-created.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "the_bookie-new-contact-created", + name: "New Contact Created", + description: "Emit new event when a contact is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.theBookie.listContacts; + }, + getSummary(item) { + return `New Contact created: ${item.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/the_bookie/sources/new-contact-created/test-event.mjs b/components/the_bookie/sources/new-contact-created/test-event.mjs new file mode 100644 index 0000000000000..6b5751a9e234e --- /dev/null +++ b/components/the_bookie/sources/new-contact-created/test-event.mjs @@ -0,0 +1,28 @@ +export default { + "id": 97645, + "organisation_name": "Gladiool Tuinarchitecten", + "street": "", + "street_number": "", + "street_number_addition": null, + "extra_address_line": "", + "postal_code": "", + "town": "", + "country": "", + "organisation_kvk_number": null, + "organisation_btw_number": null, + "is_supplier": true, + "is_client": true, + "email": null, + "telephone_number": null, + "mobile_number": null, + "status": 1, + "first_name": null, + "last_name": null, + "associated_ledger_account": [], + "btw_shifted_preset": "NONE", + "total_sales_amount_excl_btw": 0, + "total_sales_amount_payable": 0, + "total_purchase_amount_excl_btw": 0, + "total_purchase_amount_payable": 0, + "extra_info": "" +} \ No newline at end of file diff --git a/components/the_bookie/sources/new-invoice-created/new-invoice-created.mjs b/components/the_bookie/sources/new-invoice-created/new-invoice-created.mjs new file mode 100644 index 0000000000000..ed68a9557fe4b --- /dev/null +++ b/components/the_bookie/sources/new-invoice-created/new-invoice-created.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "the_bookie-new-invoice-created", + name: "New Invoice Created", + description: "Emit new event when a new invoice is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.theBookie.listInvoices; + }, + getSummary(item) { + return `New Invoice: ${item.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/the_bookie/sources/new-invoice-created/test-event.mjs b/components/the_bookie/sources/new-invoice-created/test-event.mjs new file mode 100644 index 0000000000000..d3bdb1bd2240e --- /dev/null +++ b/components/the_bookie/sources/new-invoice-created/test-event.mjs @@ -0,0 +1,13 @@ +export default { + "id": 292534, + "state": 10, + "invoice_payment_expired": false, + "contact_name": "Organisation Name", + "contact_id": 219464, + "total_amount_incl_btw": 0, + "invoice_date": "2024-09-18", + "expiration_date": "2024-11-18", + "state_display": "Voldaan", + "invoice_number": "78989789", + "financial_period": 59143 +} \ No newline at end of file diff --git a/components/the_bookie/sources/new-invoice-paid/new-invoice-paid.mjs b/components/the_bookie/sources/new-invoice-paid/new-invoice-paid.mjs new file mode 100644 index 0000000000000..e802ca3f27123 --- /dev/null +++ b/components/the_bookie/sources/new-invoice-paid/new-invoice-paid.mjs @@ -0,0 +1,25 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "the_bookie-new-invoice-paid", + name: "New Invoice Paid", + description: "Emit new event when the state of an invoice is changed to 'paid'.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + filterItems(items) { + return items.filter((item) => item.state === 20); + }, + getFunction() { + return this.theBookie.listInvoices; + }, + getSummary(item) { + return `Invoice ${item.id} paid`; + }, + }, + sampleEmit, +}; diff --git a/components/the_bookie/sources/new-invoice-paid/test-event.mjs b/components/the_bookie/sources/new-invoice-paid/test-event.mjs new file mode 100644 index 0000000000000..78e50f79ca608 --- /dev/null +++ b/components/the_bookie/sources/new-invoice-paid/test-event.mjs @@ -0,0 +1,13 @@ +export default { + "id": 292534, + "state": 20, + "invoice_payment_expired": false, + "contact_name": "Organisation Name", + "contact_id": 219464, + "total_amount_incl_btw": 0, + "invoice_date": "2024-09-18", + "expiration_date": "2024-11-18", + "state_display": "Voldaan", + "invoice_number": "78989789", + "financial_period": 59143 +} \ No newline at end of file diff --git a/components/the_bookie/the_bookie.app.mjs b/components/the_bookie/the_bookie.app.mjs new file mode 100644 index 0000000000000..b057bb184c14b --- /dev/null +++ b/components/the_bookie/the_bookie.app.mjs @@ -0,0 +1,121 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "the_bookie", + propDefinitions: { + contactId: { + type: "string", + label: "Contact ID", + description: "The ID of the contact", + async options({ page }) { + const { results } = await this.listContacts({ + params: { + is_client: true, + admin_id: `${this.$auth.admin_id}`, + limit: LIMIT, + offset: LIMIT * page, + }, + }); + + return results.map(({ + id: value, organisation_name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://app.thebookie.nl/nl/api/e1"; + }, + _headers(headers = {}) { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + Accept: "application/json", + ...headers, + }; + }, + _data(data) { + return data + ? { + ...data, + admin_id: `${this.$auth.admin_id}`, + } + : null; + }, + _makeRequest({ + $ = this, path, data, headers, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(headers), + data: this._data(data), + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contacts/create/", + ...opts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts/", + ...opts, + }); + }, + listInvoices(opts = {}) { + return this._makeRequest({ + path: "/sales-journals/", + ...opts, + }); + }, + createInvoice(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/sales-journals/create/", + ...opts, + }); + }, + searchContact({ + params, ...opts + }) { + return this._makeRequest({ + path: "/contacts", + params: { + ...params, + admin_id: `${this.$auth.admin_id}`, + }, + ...opts, + }); + }, + async *paginate({ + fn, params = {}, ...opts + }) { + let hasMore = false; + let page = 0; + + do { + params.limit = LIMIT; + params.offset = LIMIT * page++; + params.admin_id = `${this.$auth.admin_id}`; + const { results } = await fn({ + params, + ...opts, + }); + for (const d of results) { + yield d; + } + + hasMore = results.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/the_magic_drip/actions/add-lead-to-campaign/add-lead-to-campaign.mjs b/components/the_magic_drip/actions/add-lead-to-campaign/add-lead-to-campaign.mjs new file mode 100644 index 0000000000000..f27a8ff603700 --- /dev/null +++ b/components/the_magic_drip/actions/add-lead-to-campaign/add-lead-to-campaign.mjs @@ -0,0 +1,72 @@ +import app from "../../the_magic_drip.app.mjs"; + +export default { + key: "the_magic_drip-add-lead-to-campaign", + name: "Add Lead to Campaign", + description: "Add a lead to a campaign. [See the documentation](https://docs.themagicdrip.com/api-reference/endpoint/post-v1campaignleads)", + version: "0.0.1", + type: "action", + props: { + app, + campaignId: { + propDefinition: [ + app, + "campaignId", + ], + }, + firstName: { + type: "string", + label: "First Name", + description: "First name of the lead", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the lead", + }, + linkedInPublicUrl: { + type: "string", + label: "LinkedIn Public URL", + description: "LinkedIn public URL of the lead", + optional: true, + }, + company: { + type: "string", + label: "Company", + description: "Company of the lead", + optional: true, + }, + companyLinkedInUrl: { + type: "string", + label: "Company LinkedIn URL", + description: "LinkedIn URL of the company", + optional: true, + }, + customVariables: { + type: "object", + label: "Custom Variables", + description: "More information about the lead", + optional: true, + }, + }, + async run({ $ }) { + const { + app, campaignId, ...lead + } = this; + const response = await app.addLeadToCampaign({ + $, + campaignId, + data: { + leadsWithCustomVariables: [ + lead, + ], + }, + }); + $.export( + "$summary", + `Successfully added lead "${lead.lastName}" to campaign`, + ); + return response; + }, +}; diff --git a/components/the_magic_drip/actions/list-campaigns/list-campaigns.mjs b/components/the_magic_drip/actions/list-campaigns/list-campaigns.mjs new file mode 100644 index 0000000000000..ce20a7e7bd5cb --- /dev/null +++ b/components/the_magic_drip/actions/list-campaigns/list-campaigns.mjs @@ -0,0 +1,19 @@ +import app from "../../the_magic_drip.app.mjs"; + +export default { + key: "the_magic_drip-list-campaigns", + name: "List Campaigns", + description: "Retrieve all available campaigns. [See the documentation](https://docs.themagicdrip.com/api-reference/endpoint/get-v1campaign)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const { campaigns } = await this.app.listCampaigns({ + $, + }); + $.export("$summary", `Sucessfully retrieved ${campaigns?.length ?? 0} campaigns`); + return campaigns; + }, +}; diff --git a/components/the_magic_drip/actions/list-templates/list-templates.mjs b/components/the_magic_drip/actions/list-templates/list-templates.mjs new file mode 100644 index 0000000000000..d99ab2b4a44c0 --- /dev/null +++ b/components/the_magic_drip/actions/list-templates/list-templates.mjs @@ -0,0 +1,19 @@ +import app from "../../the_magic_drip.app.mjs"; + +export default { + key: "the_magic_drip-list-templates", + name: "List Templates", + description: "Retrieve all available templates. [See the documentation](https://docs.themagicdrip.com/api-reference/endpoint/get-v1templates)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const { templates } = await this.app.listTemplates({ + $, + }); + $.export("$summary", `Sucessfully retrieved ${templates?.length ?? 0} templates`); + return templates; + }, +}; diff --git a/components/the_magic_drip/actions/mark-campaign-active-or-inactive/mark-campaign-active-or-inactive.mjs b/components/the_magic_drip/actions/mark-campaign-active-or-inactive/mark-campaign-active-or-inactive.mjs new file mode 100644 index 0000000000000..a2ac55e62d25f --- /dev/null +++ b/components/the_magic_drip/actions/mark-campaign-active-or-inactive/mark-campaign-active-or-inactive.mjs @@ -0,0 +1,39 @@ +import app from "../../the_magic_drip.app.mjs"; + +export default { + key: "the_magic_drip-mark-campaign-active-or-inactive", + name: "Mark Campaign Active or Inactive", + description: "Marks a campaign as active or inactive. [See the documentation](https://docs.themagicdrip.com/api-reference/endpoint/post-v1campaign-active)", + version: "0.0.1", + type: "action", + props: { + app, + campaignId: { + propDefinition: [ + app, + "campaignId", + ], + }, + activate: { + type: "boolean", + label: "Activate", + description: "Set to `true` to activate, or `false` to deactivate the campaign", + }, + }, + async run({ $ }) { + const { + campaignId, activate, + } = this; + const response = await this.app.markCampaignActiveInactive({ + $, + campaignId, + activate, + }); + + $.export("$summary", `Successfully ${activate + ? "" + : "de"}activated campaign`); + + return response; + }, +}; diff --git a/components/the_magic_drip/package.json b/components/the_magic_drip/package.json new file mode 100644 index 0000000000000..b082e1ca3a97f --- /dev/null +++ b/components/the_magic_drip/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/the_magic_drip", + "version": "0.1.0", + "description": "Pipedream The Magic Drip Components", + "main": "the_magic_drip.app.mjs", + "keywords": [ + "pipedream", + "the_magic_drip" + ], + "homepage": "https://pipedream.com/apps/the_magic_drip", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/the_magic_drip/sources/common.mjs b/components/the_magic_drip/sources/common.mjs new file mode 100644 index 0000000000000..1d3b3ab59a27e --- /dev/null +++ b/components/the_magic_drip/sources/common.mjs @@ -0,0 +1,56 @@ +import app from "../the_magic_drip.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + app, + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + db: "$.service.db", + }, + methods: { + _getSavedIds() { + return this.db.get("savedIds") || []; + }, + _setSavedIds(value) { + this.db.set("savedIds", value); + }, + getItemId(item) { + return item.id; + }, + getItemMetadata() { + return { + summary: "New event", + ts: Date.now(), + }; + }, + async getAndProcessData(maxEmits = 0) { + const savedIds = this._getSavedIds(); + const items = await this.getItems(); + + items?.filter?.((item) => !savedIds.includes(this.getItemId(item))).forEach((item, index) => { + if ((!maxEmits) || (index < maxEmits)) { + this.$emit(item, { + id: this.getItemId(item), + ...this.getItemMetadata(item), + }); + } + savedIds.push(this.getItemId(item)); + }); + + this._setSavedIds(savedIds); + }, + }, + hooks: { + async deploy() { + await this.getAndProcessData(5); + }, + }, + async run() { + await this.getAndProcessData(); + }, +}; diff --git a/components/the_magic_drip/sources/new-campaign-created/new-campaign-created.mjs b/components/the_magic_drip/sources/new-campaign-created/new-campaign-created.mjs new file mode 100644 index 0000000000000..deb1c88ac56ac --- /dev/null +++ b/components/the_magic_drip/sources/new-campaign-created/new-campaign-created.mjs @@ -0,0 +1,27 @@ +import common from "../common.mjs"; + +export default { + ...common, + key: "the_magic_drip-new-campaign-created", + name: "New Campaign Created", + description: "Emit new event when a campaign is created. [See the documentation](https://docs.themagicdrip.com/api-reference/endpoint/get-v1campaign)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + async getItems() { + const { campaigns } = await this.app.listCampaigns(); + return campaigns; + }, + getItemId(item) { + return item.workflowId; + }, + getItemMetadata(item) { + return { + summary: `New Campaign: ${item.name}`, + ts: item.createdAt, + }; + }, + }, +}; diff --git a/components/the_magic_drip/sources/new-template-created/new-template-created.mjs b/components/the_magic_drip/sources/new-template-created/new-template-created.mjs new file mode 100644 index 0000000000000..9f7fa98c5d68f --- /dev/null +++ b/components/the_magic_drip/sources/new-template-created/new-template-created.mjs @@ -0,0 +1,27 @@ +import common from "../common.mjs"; + +export default { + ...common, + key: "the_magic_drip-new-template-created", + name: "New Template Created", + description: "Emit new event when a template is created. [See the documentation](https://docs.themagicdrip.com/api-reference/endpoint/get-v1templates)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + async getItems() { + const { templates } = await this.app.listTemplates(); + return templates; + }, + getItemId(item) { + return item.templateId; + }, + getItemMetadata(item) { + return { + summary: `New Template: ${item.name}`, + ts: item.createdAt, + }; + }, + }, +}; diff --git a/components/the_magic_drip/the_magic_drip.app.mjs b/components/the_magic_drip/the_magic_drip.app.mjs new file mode 100644 index 0000000000000..e233f9101d9b4 --- /dev/null +++ b/components/the_magic_drip/the_magic_drip.app.mjs @@ -0,0 +1,80 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "the_magic_drip", + propDefinitions: { + campaignId: { + type: "string", + label: "Campaign ID", + description: "Select a campaign", + async options() { + const { campaigns } = await this.listCampaigns(); + return campaigns?.map((campaign) => ({ + label: campaign.name, + value: campaign.workflowId, + })); + }, + }, + linkedinUrl: { + type: "string", + label: "LinkedIn URL", + description: "The LinkedIn URL of the lead", + optional: true, + }, + desiredState: { + type: "boolean", + label: "Desired State", + description: "Set to true to activate, false to deactivate the campaign", + }, + }, + methods: { + _baseUrl() { + return "https://api.themagicdrip.com/v1"; + }, + async _makeRequest({ + $, path, headers, ...otherOpts + } = {}) { + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "x-api-key": this.$auth.api_key, + }, + }); + }, + async listCampaigns(opts = {}) { + return this._makeRequest({ + path: "/campaign", + ...opts, + }); + }, + async addLeadToCampaign({ + campaignId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/campaign/leads/${campaignId}`, + ...opts, + }); + }, + async markCampaignActiveInactive({ + campaignId, activate, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/campaign/${campaignId}/${activate + ? "active" + : "inactive"}`, + ...opts, + }); + }, + async listTemplates(opts = {}) { + return this._makeRequest({ + path: "/templates", + ...opts, + }); + }, + }, +}; diff --git a/components/the_odds_api/README.md b/components/the_odds_api/README.md new file mode 100644 index 0000000000000..4561bb2933ee6 --- /dev/null +++ b/components/the_odds_api/README.md @@ -0,0 +1,11 @@ +# Overview + +With The Odds API on Pipedream, you can access a range of bookmakers and exchanges to get live and upcoming sports odds. This integration allows you to create workflows for odds comparison, betting opportunities, or even to power a custom alerting system for when odds reach a certain threshold. The data provided by The Odds API can be used to inform decisions or feed into other apps for enhanced functionality. + +# Example Use Cases + +- **Odds Monitoring to Slack**: Set up a serverless workflow that polls The Odds API for the latest odds and sends updates directly to a Slack channel. This is ideal for teams that want real-time updates on sports events. + +- **Odds Change Alerts via Email**: Create a workflow where Pipedream watches specific odds from The Odds API, and when those odds change beyond a set percentage, it triggers an automated email notification. This can keep gamblers or betting analysts informed of significant market moves. + +- **Data Aggregation with Google Sheets**: Use Pipedream to fetch odds from various sports and competitions, and automatically populate a Google Sheet with the data. This can serve as a powerful tool for tracking odds over time and analyzing trends for betting strategies. diff --git a/components/thinkific/README.md b/components/thinkific/README.md index a9df2954f9d1c..35dd637846b33 100644 --- a/components/thinkific/README.md +++ b/components/thinkific/README.md @@ -1,30 +1,11 @@ # Overview -The Thinkific API provides a comprehensive set of tools to help you create an -engaging online course. It can be used to create automated course creation, -course delivery, and course management systems. You can build a variety of -different applications and integrations with Thinkific’s API, ranging from -customer service to marketing to sales automation. +Thinkific's API opens a treasure trove of possibilities for automating and enhancing the e-learning experience. It provides programmatic access to your Thinkific site, allowing you to manage courses, users, enrollments, and more. With Pipedream's serverless platform, you can connect Thinkific to a multitude of other applications, automate tasks, sync data across various services, and trigger workflows based on specific events, all without writing a line of server code. -Here are a few examples of how you can benefit from the Thinkific API: +# Example Use Cases -- Enroll and manage users for your course: The Thinkific API allows you to - create new users, enroll them in your course, manage enrollment, and provide - access to their course content. -- Create custom payment systems and access coupon codes: Utilize the Thinkific - API to build custom payment systems, allowing you to create unique offers to - your users. Additionally, you can create customer coupon codes to give your - customers a discount on their purchase. -- Track and analyze user activity: Keep track of user activity in your course - using the Thinkific API. You can track page views, tries, completions, and - other user engagement metrics. -- Embed third party applications in your course: Embed third party apps into - your course with the Thinkific API. You can utilize existing apps to bring - new content and interactivity to your course. -- Automate sales and customer service: Automate sales and customer service - processes with the Thinkific API. Create automated sales funnels and respond - to customer inquiries with the Thinkific API. +- **Automated Student Onboarding**: When a student signs up for a course on Thinkific, use Pipedream to send a personalized welcome email via SendGrid, create a new contact in HubSpot, and enroll them in an onboarding email sequence. -There are many more possibilities with the Thinkific API, giving you complete -control over how you build and manage your online course. With the Thinkific -API, you have full control to bring all your ideas to life. +- **Course Completion Certificates**: Trigger a workflow on Pipedream when a student completes a course on Thinkific. Generate a personalized certificate using a tool like Canva or Google Slides and email it to the student, while also logging this achievement in Airtable for record-keeping. + +- **Synchronize Course Progress with CRM**: Keep track of student progress by triggering a Pipedream workflow every time a course lesson is completed. Update a custom field in a CRM platform like Salesforce to reflect their current completion percentage, enabling targeted follow-up campaigns. diff --git a/components/thinkific/actions/create-user/create-user.mjs b/components/thinkific/actions/create-user/create-user.mjs new file mode 100644 index 0000000000000..b9a2e35e73902 --- /dev/null +++ b/components/thinkific/actions/create-user/create-user.mjs @@ -0,0 +1,98 @@ +import thinkific from "../../thinkific.app.mjs"; + +export default { + key: "thinkific-create-user", + name: "Create User", + description: "Creates a new user on Thinkific. [See the documentation](https://developers.thinkific.com/api/api-documentation/#/Users/createUser)", + version: "0.0.2", + type: "action", + props: { + thinkific, + firstName: { + propDefinition: [ + thinkific, + "firstName", + ], + }, + lastName: { + propDefinition: [ + thinkific, + "lastName", + ], + }, + email: { + propDefinition: [ + thinkific, + "email", + ], + }, + password: { + propDefinition: [ + thinkific, + "password", + ], + }, + roles: { + propDefinition: [ + thinkific, + "roles", + ], + }, + company: { + propDefinition: [ + thinkific, + "company", + ], + }, + headline: { + propDefinition: [ + thinkific, + "headline", + ], + }, + affiliateCode: { + propDefinition: [ + thinkific, + "affiliateCode", + ], + }, + affiliateCommission: { + propDefinition: [ + thinkific, + "affiliateCommission", + ], + }, + affiliateCommissionType: { + propDefinition: [ + thinkific, + "affiliateCommissionType", + ], + }, + affiliatePayoutEmail: { + propDefinition: [ + thinkific, + "affiliatePayoutEmail", + ], + }, + }, + async run({ $ }) { + const response = await this.thinkific.createUser({ + $, + data: { + first_name: this.firstName, + last_name: this.lastName, + email: this.email, + password: this.password, + roles: this.roles, + company: this.company, + headline: this.headline, + affiliate_code: this.affiliateCode, + affiliate_commission: this.affiliateCommission && +this.affiliateCommission, + affiliate_commission_type: this.affiliateCommissionType, + affiliate_payout_email: this.affiliatePayoutEmail, + }, + }); + $.export("$summary", `Successfully created user with email ${this.email}`); + return response; + }, +}; diff --git a/components/thinkific/actions/enroll-user/enroll-user.mjs b/components/thinkific/actions/enroll-user/enroll-user.mjs new file mode 100644 index 0000000000000..7fe23290419f9 --- /dev/null +++ b/components/thinkific/actions/enroll-user/enroll-user.mjs @@ -0,0 +1,35 @@ +import thinkific from "../../thinkific.app.mjs"; + +export default { + key: "thinkific-enroll-user", + name: "Enroll User", + description: "Creates a new Enrollment for specified student in specified course. [See the documentation](https://developers.thinkific.com/api/api-documentation/#/Enrollments/createEnrollment)", + version: "0.0.2", + type: "action", + props: { + thinkific, + userId: { + propDefinition: [ + thinkific, + "userId", + ], + }, + courseId: { + propDefinition: [ + thinkific, + "courseId", + ], + }, + }, + async run({ $ }) { + const response = await this.thinkific.enrollUser({ + $, + data: { + user_id: this.userId, + course_id: this.courseId, + }, + }); + $.export("$summary", `Successfully enrolled user ${this.userId} in course ${this.courseId}`); + return response; + }, +}; diff --git a/components/thinkific/actions/update-user/update-user.mjs b/components/thinkific/actions/update-user/update-user.mjs new file mode 100644 index 0000000000000..f40e7cf43fb1c --- /dev/null +++ b/components/thinkific/actions/update-user/update-user.mjs @@ -0,0 +1,100 @@ +import thinkific from "../../thinkific.app.mjs"; + +export default { + key: "thinkific-update-user", + name: "Update User", + description: "Updates the information of a specific user on Thinkific. [See the documentation](https://developers.thinkific.com/api/api-documentation/#/Users/updateUserByID)", + version: "0.0.2", + type: "action", + props: { + thinkific, + userId: { + propDefinition: [ + thinkific, + "userId", + ], + }, + firstName: { + propDefinition: [ + thinkific, + "firstName", + ], + optional: true, + }, + lastName: { + propDefinition: [ + thinkific, + "lastName", + ], + optional: true, + }, + password: { + propDefinition: [ + thinkific, + "password", + ], + }, + roles: { + propDefinition: [ + thinkific, + "roles", + ], + }, + company: { + propDefinition: [ + thinkific, + "company", + ], + }, + headline: { + propDefinition: [ + thinkific, + "headline", + ], + }, + affiliateCode: { + propDefinition: [ + thinkific, + "affiliateCode", + ], + }, + affiliateCommission: { + propDefinition: [ + thinkific, + "affiliateCommission", + ], + }, + affiliateCommissionType: { + propDefinition: [ + thinkific, + "affiliateCommissionType", + ], + }, + affiliatePayoutEmail: { + propDefinition: [ + thinkific, + "affiliatePayoutEmail", + ], + }, + }, + async run({ $ }) { + const response = await this.thinkific.updateUser({ + $, + userId: this.userId, + data: { + first_name: this.firstName, + last_name: this.lastName, + password: this.password, + roles: this.roles, + company: this.company, + headline: this.headline, + affiliate_code: this.affiliateCode, + affiliate_commission: this.affiliateCommission && +this.affiliateCommission, + affiliate_commission_type: this.affiliateCommissionType, + affiliate_payout_email: this.affiliatePayoutEmail, + }, + }); + $.export("$summary", `Successfully updated user ${this.userId}`); + return response; + }, +}; diff --git a/components/thinkific/common/constants.mjs b/components/thinkific/common/constants.mjs new file mode 100644 index 0000000000000..e0ba719837005 --- /dev/null +++ b/components/thinkific/common/constants.mjs @@ -0,0 +1,120 @@ +const ROLES = [ + "affiliate", + "course_admin", + "group_analyst", + "site_admin", +]; + +const AFFILIATE_COMMISSION_TYPES = [ + "%", + "$", +]; + +const TOPIC_OPTIONS = [ + { + label: "Lead Created", + value: "lead.created", + }, + { + label: "Order Created", + value: "order.created", + }, + { + label: "Order Transition - Succeeded", + value: "order_transaction.succeeded", + }, + { + label: "Order Transition - Failed", + value: "order_transaction.failed", + }, + { + label: "Order Transition - Refunded", + value: "order_transaction.refunded", + }, + { + label: "Subscription - Cancelled", + value: "subscription.cancelled", + }, + { + label: "Subscription - Past Due", + value: "subscription.past_due", + }, + { + label: "Subscription - Unpaid", + value: "subscription.unpaid", + }, + { + label: "User - Signup", + value: "user.signup", + }, + { + label: "User - Signin", + value: "user.signin", + }, + { + label: "User - Updated", + value: "user.updated", + }, + { + label: "Enrollment - Trial", + value: "enrollment.trial", + }, + { + label: "Enrollment - Created", + value: "enrollment.created", + }, + { + label: "Enrollment - Completed", + value: "enrollment.completed", + }, + { + label: "Enrollment - Progress", + value: "enrollment.progress", + }, + { + label: "Course - Created", + value: "course.created", + }, + { + label: "Course - Deleted", + value: "course.deleted", + }, + { + label: "Course - Updated", + value: "course.updated", + }, + { + label: "Lesson - Completed", + value: "lesson.completed", + }, + { + label: "Quiz - Attempted", + value: "quiz.attempted", + }, + { + label: "App - Uninstalled", + value: "app.uninstalled", + }, + { + label: "Product - Created", + value: "product.created", + }, + { + label: "Product - Deleted", + value: "product.deleted", + }, + { + label: "Product - Updated", + value: "product.updated", + }, + { + label: "Plan - Updated", + value: "plan.updated", + }, +]; + +export default { + ROLES, + AFFILIATE_COMMISSION_TYPES, + TOPIC_OPTIONS, +}; diff --git a/components/thinkific/package.json b/components/thinkific/package.json new file mode 100644 index 0000000000000..075a0f59d6f17 --- /dev/null +++ b/components/thinkific/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/thinkific", + "version": "0.1.0", + "description": "Pipedream Thinkific Components", + "main": "thinkific.app.mjs", + "keywords": [ + "pipedream", + "thinkific" + ], + "homepage": "https://pipedream.com/apps/thinkific", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/thinkific/sources/common/base.mjs b/components/thinkific/sources/common/base.mjs new file mode 100644 index 0000000000000..690162bcd60ef --- /dev/null +++ b/components/thinkific/sources/common/base.mjs @@ -0,0 +1,53 @@ +import thinkific from "../../thinkific.app.mjs"; + +export default { + props: { + thinkific, + db: "$.service.db", + http: "$.interface.http", + }, + hooks: { + async activate() { + const { id } = await this.thinkific.createWebhook({ + data: { + target_url: this.http.endpoint, + topic: this.getTopic(), + }, + }); + this._setHookId(id); + }, + async deactivate() { + const hookId = this._getHookId(); + if (hookId) { + await this.thinkific.deleteWebhook({ + hookId, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getTopic() { + throw new Error("getTarget is not implemented"); + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + generateMeta(event) { + return { + id: event.id, + summary: this.getSummary(event), + ts: Date.parse(event.created_at), + }; + }, + }, + async run({ body }) { + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, +}; diff --git a/components/thinkific/sources/lesson-completed-instant/lesson-completed-instant.mjs b/components/thinkific/sources/lesson-completed-instant/lesson-completed-instant.mjs new file mode 100644 index 0000000000000..60cbba5dc0801 --- /dev/null +++ b/components/thinkific/sources/lesson-completed-instant/lesson-completed-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "thinkific-lesson-completed-instant", + name: "New Lesson Completed (Instant)", + description: "Emit new event when a user completes a lesson in a course.", + version: "0.0.2", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTopic() { + return "lesson.completed"; + }, + getSummary(event) { + return `Lesson Completed: ${event.payload.lesson.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/thinkific/sources/lesson-completed-instant/test-event.mjs b/components/thinkific/sources/lesson-completed-instant/test-event.mjs new file mode 100644 index 0000000000000..86e076a8140c7 --- /dev/null +++ b/components/thinkific/sources/lesson-completed-instant/test-event.mjs @@ -0,0 +1,33 @@ +export default { + "id": "20191029145629636106318", + "resource": "lesson", + "action": "completed", + "tenant_id": "12345", + "tenant_global_id": "feb8d00d-8040-4382-afee-491a6c8013af", + "created_at": "2019-10-29T18:56:29.474Z", + "payload": { + "chapter": { + "id": 123, + "name": "Chapter 1" + }, + "course": { + "id": 123, + "name": "Introduction to Webhooks" + }, + "enrollment": { + "id": 1084282 + }, + "lesson": { + "id": 24220, + "name": "Getting started with Webhooks", + "position": 0, + "type": "Video" + }, + "user": { + "email": "ninjas@thinkific.com", + "first_name": "Robert", + "id": 123456, + "last_name": "Smith" + } + } +} \ No newline at end of file diff --git a/components/thinkific/sources/new-event-instant/new-event-instant.mjs b/components/thinkific/sources/new-event-instant/new-event-instant.mjs new file mode 100644 index 0000000000000..0514f5eb4ccd3 --- /dev/null +++ b/components/thinkific/sources/new-event-instant/new-event-instant.mjs @@ -0,0 +1,32 @@ +import constants from "../../common/constants.mjs"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "thinkific-new-event-instant", + name: "New Event (Instant)", + description: "Emit new event when the selected topic is triggered.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + topic: { + type: "string", + label: "Topic", + description: "The topic used to trigger the events.", + options: constants.TOPIC_OPTIONS, + }, + }, + methods: { + ...common.methods, + getTopic() { + return this.topic; + }, + getSummary(event) { + return `New Event: ${event.payload.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/thinkific/sources/new-event-instant/test-event.mjs b/components/thinkific/sources/new-event-instant/test-event.mjs new file mode 100644 index 0000000000000..86e076a8140c7 --- /dev/null +++ b/components/thinkific/sources/new-event-instant/test-event.mjs @@ -0,0 +1,33 @@ +export default { + "id": "20191029145629636106318", + "resource": "lesson", + "action": "completed", + "tenant_id": "12345", + "tenant_global_id": "feb8d00d-8040-4382-afee-491a6c8013af", + "created_at": "2019-10-29T18:56:29.474Z", + "payload": { + "chapter": { + "id": 123, + "name": "Chapter 1" + }, + "course": { + "id": 123, + "name": "Introduction to Webhooks" + }, + "enrollment": { + "id": 1084282 + }, + "lesson": { + "id": 24220, + "name": "Getting started with Webhooks", + "position": 0, + "type": "Video" + }, + "user": { + "email": "ninjas@thinkific.com", + "first_name": "Robert", + "id": 123456, + "last_name": "Smith" + } + } +} \ No newline at end of file diff --git a/components/thinkific/sources/new-full-enrollment-instant/new-full-enrollment-instant.mjs b/components/thinkific/sources/new-full-enrollment-instant/new-full-enrollment-instant.mjs new file mode 100644 index 0000000000000..c51dcc14d8b8e --- /dev/null +++ b/components/thinkific/sources/new-full-enrollment-instant/new-full-enrollment-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "thinkific-new-full-enrollment-instant", + name: "New Full Enrollment (Instant)", + description: "Emit new event when a user enrolls in your course.", + version: "0.0.2", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTopic() { + return "enrollment.created"; + }, + getSummary(event) { + return `New Enrollment in Course: ${event.payload.course.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/thinkific/sources/new-full-enrollment-instant/test-event.mjs b/components/thinkific/sources/new-full-enrollment-instant/test-event.mjs new file mode 100644 index 0000000000000..9674d184c99b9 --- /dev/null +++ b/components/thinkific/sources/new-full-enrollment-instant/test-event.mjs @@ -0,0 +1,29 @@ +export default { + "id": "20180126171756020665195", + "resource": "enrollment", + "action": "created", + "tenant_id": "3", + "created_at": "2018-01-26T22:17:01.924Z", + "payload": { + "activated_at": "2018-01-26T22:16:52.255Z", + "completed_at": null, + "course": { + "id": 4, + "name": "Introduction to Webhooks" + }, + "course_id": 4, + "created_at": "2018-01-26T22:16:52.285Z", + "expiry_date": null, + "free_trial": false, + "id": 97472, + "percentage_completed": "0.0", + "started_at": "2018-01-26T22:17:01.891Z", + "updated_at": "2018-01-26T22:17:01.924Z", + "user": { + "email": "ninjas@thinkific.com", + "first_name": "Robert", + "id": 123456, + "last_name": "Smith" + } + } +} \ No newline at end of file diff --git a/components/thinkific/sources/new-order-instant/new-order-instant.mjs b/components/thinkific/sources/new-order-instant/new-order-instant.mjs new file mode 100644 index 0000000000000..7311ec416a897 --- /dev/null +++ b/components/thinkific/sources/new-order-instant/new-order-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "thinkific-new-order-instant", + name: "New Order (Instant)", + version: "0.0.2", + description: "Emit new event when a new purchase has been made.)", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTopic() { + return "order.created"; + }, + getSummary(event) { + return `New Order Created: ${event.payload.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/thinkific/sources/new-order-instant/test-event.mjs b/components/thinkific/sources/new-order-instant/test-event.mjs new file mode 100644 index 0000000000000..b68d83aaeea30 --- /dev/null +++ b/components/thinkific/sources/new-order-instant/test-event.mjs @@ -0,0 +1,39 @@ +export default { + "id": "20180126172320940835610", + "resource": "order", + "action": "created", + "tenant_id": "3", + "created_at": "2018-01-26T22:23:20.808Z", + "payload": { + "affiliate_referral_code": null, + "amount_cents": 5000, + "amount_dollars": 50, + "billing_name": "Robert Smith", + "coupon": { + "id": 12345678, + "code": "discount123", + "promotion_id": 1234567 + }, + "created_at": "2018-01-26T22:23:18.400Z", + "id": 19796, + "order_number": 1010, + "payment_type": "one-time", + "product_id": 1, + "product_name": "Introduction to Webhooks", + "status": "Complete", + "items": [ + { + "product_id": 1, + "product_name": "Introduction to Webhooks", + "amount_dollars": 50, + "amount_cents": 5000 + } + ], + "user": { + "email": "ninjas@thinkific.com", + "first_name": "Robert", + "id": 123456, + "last_name": "Smith" + } + } +} \ No newline at end of file diff --git a/components/thinkific/thinkific.app.mjs b/components/thinkific/thinkific.app.mjs index cf44ad069e539..33cc28e18f38e 100644 --- a/components/thinkific/thinkific.app.mjs +++ b/components/thinkific/thinkific.app.mjs @@ -1,11 +1,190 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "thinkific", - propDefinitions: {}, + propDefinitions: { + userId: { + type: "string", + label: "User ID", + description: "The ID of the user", + async options({ page }) { + const { items } = await this.listUsers({ + params: { + page: page + 1, + }, + }); + return items?.map(({ + id: value, full_name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + courseId: { + type: "string", + label: "Course ID", + description: "The ID of the course", + async options({ page }) { + const { items } = await this.listCourses({ + params: { + page: page + 1, + }, + }); + return items?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + firstName: { + type: "string", + label: "First Name", + description: "The user's first name", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The user's last name", + }, + email: { + type: "string", + label: "Email", + description: "The user's email address", + }, + password: { + type: "string", + label: "Password", + description: "The password of the User. If not included, the Express Sign In Link becomes activated for the User. Min length `8`.", + optional: true, + }, + roles: { + type: "string[]", + label: "Roles", + description: "The user's roles", + options: constants.ROLES, + optional: true, + }, + company: { + type: "string", + label: "Company", + description: "The user's company name", + optional: true, + }, + headline: { + type: "string", + label: "Headline", + description: "The user's headline", + optional: true, + }, + affiliateCode: { + type: "string", + label: "Affiliate Code", + description: "User's affiliate code", + optional: true, + }, + affiliateCommission: { + type: "string", + label: "Affiliate Commission", + description: "Required only if the User is an affiliate. This should be greater than 0 and less than or equal to 100 if the type is percentage or lower than 9999.99 if is a fixed type.", + optional: true, + }, + affiliateCommissionType: { + type: "string", + label: "Affiliate Commission Type", + description: "The affiliate payout type, it can be either `%` (percentage, default) or `$` (fixed amount). Required only if the User is an affiliate.`", + options: constants.AFFILIATE_COMMISSION_TYPES, + optional: true, + }, + affiliatePayoutEmail: { + type: "string", + label: "Affiliate Payout Email", + description: "The email of the User. Required only if the user is an affiliate. Used to pay the User out.", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.thinkific.com/api"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + ...headers, + "X-Auth-API-Key": `${this.$auth.api_key}`, + "X-Auth-Subdomain": `${this.$auth.subdomain}`, + "Content-Type": "application/json", + }, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/v2/webhooks", + headers: { + Authorization: `Bearer ${this.$auth.api_key}`, + }, + ...opts, + }); + }, + deleteWebhook({ + hookId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/v2/webhooks/${hookId}`, + headers: { + Authorization: `Bearer ${this.$auth.api_key}`, + }, + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/public/v1/users", + ...opts, + }); + }, + listCourses(opts = {}) { + return this._makeRequest({ + path: "/public/v1/courses", + ...opts, + }); + }, + enrollUser(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/public/v1/enrollments", + ...opts, + }); + }, + createUser(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/public/v1/users", + ...opts, + }); + }, + updateUser({ + userId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/public/v1/users/${userId}`, + ...opts, + }); }, }, }; diff --git a/components/thoughtful_gpt/README.md b/components/thoughtful_gpt/README.md new file mode 100644 index 0000000000000..095fbbe8b6d61 --- /dev/null +++ b/components/thoughtful_gpt/README.md @@ -0,0 +1,11 @@ +# Overview + +The Thoughtful GPT API lets you integrate advanced natural language processing into your workflows on Pipedream. Tap into the power of generative AI to create content, analyze text, or automate responses. With Pipedream, harness this API in serverless workflows that trigger from numerous events and connect to other apps for expanded functionality. + +# Example Use Cases + +- **Content Generation Pipeline**: Automate blog post creation by triggering a workflow with a new topic input, then use Thoughtful GPT to generate an article draft. Next, send the draft to a human review step via email or a project management tool before finalizing. + +- **Customer Support Automation**: Streamline support by triggering a workflow when a new support ticket arrives. Use Thoughtful GPT to understand the query and draft a response, then post the response back to the support platform or route to an agent for review. + +- **Social Media Content Scheduler**: Generate social media posts based on trending topics by combining Thoughtful GPT with a social media app like Twitter. Schedule and post content directly to your social media accounts, keeping your feed fresh and engaging. diff --git a/components/thoughtly/actions/create-contact/create-contact.mjs b/components/thoughtly/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..94c48ccc79b62 --- /dev/null +++ b/components/thoughtly/actions/create-contact/create-contact.mjs @@ -0,0 +1,62 @@ +import thoughtly from "../../thoughtly.app.mjs"; + +export default { + key: "thoughtly-create-contact", + name: "Create Contact", + description: "Generates a new contact within your Thoughtly team. [See the documentation](https://api.thought.ly/docs/#/contact/post_contact_create)", + version: "0.0.1", + type: "action", + props: { + thoughtly, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number of the new contact.", + }, + name: { + type: "string", + label: "Name", + description: "The name of the new contact.", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "The email of the new contact.", + optional: true, + }, + countryCode: { + type: "string", + label: "Country Code", + description: "The country code of the new contact's phone number.", + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "Tags associated with the new contact.", + optional: true, + }, + attributes: { + type: "object", + label: "Attributes", + description: "Additional attributes for the new contact.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.thoughtly.createContact({ + $, + data: { + phone_number: this.phoneNumber, + name: this.name, + email: this.email, + country_code: this.countryCode, + tags: this.tags, + attributes: this.attributes, + }, + }); + $.export("$summary", `Successfully created contact with ID: ${response.data.id}`); + return response; + }, +}; diff --git a/components/thoughtly/actions/trigger-call/trigger-call.mjs b/components/thoughtly/actions/trigger-call/trigger-call.mjs new file mode 100644 index 0000000000000..65c718ccd7637 --- /dev/null +++ b/components/thoughtly/actions/trigger-call/trigger-call.mjs @@ -0,0 +1,43 @@ +import { parseObject } from "../../common/utils.mjs"; +import thoughtly from "../../thoughtly.app.mjs"; + +export default { + key: "thoughtly-trigger-call", + name: "Trigger a Call", + description: "Triggers a call to a designated contact. [See the documentation](https://api.thought.ly/docs/#/contact/post_contact_call)", + version: "0.0.1", + type: "action", + props: { + thoughtly, + contactId: { + propDefinition: [ + thoughtly, + "contactId", + ], + }, + interviewId: { + propDefinition: [ + thoughtly, + "interviewId", + ], + }, + metadata: { + type: "object", + label: "Metadata", + description: "An object of metadata.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.thoughtly.callContact({ + $, + data: { + contact_id: this.contactId, + interview_id: this.interviewId, + metadata: parseObject(this.metadata), + }, + }); + $.export("$summary", `Successfully triggered a call to contact ID ${this.contactId}`); + return response; + }, +}; diff --git a/components/thoughtly/common/utils.mjs b/components/thoughtly/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/thoughtly/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/thoughtly/package.json b/components/thoughtly/package.json new file mode 100644 index 0000000000000..9a2a969102308 --- /dev/null +++ b/components/thoughtly/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/thoughtly", + "version": "0.1.0", + "description": "Pipedream Thoughtly Components", + "main": "thoughtly.app.mjs", + "keywords": [ + "pipedream", + "thoughtly" + ], + "homepage": "https://pipedream.com/apps/thoughtly", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} + diff --git a/components/thoughtly/sources/new-response-instant/new-response-instant.mjs b/components/thoughtly/sources/new-response-instant/new-response-instant.mjs new file mode 100644 index 0000000000000..d384baf29957b --- /dev/null +++ b/components/thoughtly/sources/new-response-instant/new-response-instant.mjs @@ -0,0 +1,50 @@ +import thoughtly from "../../thoughtly.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "thoughtly-new-response-instant", + name: "New Response (Instant)", + description: "Emit new event when a thoughtly gets a new response.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + thoughtly, + http: "$.interface.http", + db: "$.service.db", + interviewId: { + propDefinition: [ + thoughtly, + "interviewId", + ], + }, + }, + hooks: { + async activate() { + await this.thoughtly.createHook({ + data: { + type: "NEW_RESPONSE", + url: this.http.endpoint, + data: this.interviewId, + }, + }); + }, + async deactivate() { + await this.thoughtly.deleteHook({ + data: { + type: "NEW_RESPONSE", + url: this.http.endpoint, + data: this.interviewId, + }, + }); + }, + }, + async run({ body }) { + this.$emit(body, { + id: body.id, + summary: `New response received with Id: ${body.id}`, + ts: Date.parse(body.created), + }); + }, + sampleEmit, +}; diff --git a/components/thoughtly/sources/new-response-instant/test-event.mjs b/components/thoughtly/sources/new-response-instant/test-event.mjs new file mode 100644 index 0000000000000..ee83232ee5bee --- /dev/null +++ b/components/thoughtly/sources/new-response-instant/test-event.mjs @@ -0,0 +1,44 @@ +export default { + "id": "12345678-1234-1234-1234-123456789012", + "created": "2024-05-14T14:27:36.575Z", + "updated": "2024-05-14T14:27:36.704Z", + "version": 2, + "interview": { + "id": "12345678" + }, + "contact": { + "id": "12345678-1234-1234-1234-123456789012", + "created": "2024-05-14T13:26:36.017Z", + "updated": "2024-05-14T13:26:36.017Z", + "version": 1, + "team_id": "12345678-1234-1234-1234-123456789012", + "status": "ACTIVE", + "name": "Contact name", + "caller_type": null, + "phone_number": "+123456789", + "email": "contact@email.com", + "attributes": {}, + "tags": [] + }, + "team": {}, + "type": "PHONE_CALL", + "status": "NOT_STARTED", + "start_time": "2024-05-14T14:27:33.016Z", + "end_time": null, + "duration_ms": "0", + "conversation_history": [ + { + "date": "2024-05-14T14:27:36.574Z", + "text": "string", + "author": "ai", + "audio_url": "https://cdn.thoughtly.net/production-1234567890.wav", + "cumulative_duration_ms": 0 + } + ], + "ai_name": "Tessa", + "phone_number": "+123456789", + "recording_url": null, + "transcript": "Agent: agent text", + "summary_data": null, + "metadata": null +} \ No newline at end of file diff --git a/components/thoughtly/thoughtly.app.mjs b/components/thoughtly/thoughtly.app.mjs new file mode 100644 index 0000000000000..77831d8cde90a --- /dev/null +++ b/components/thoughtly/thoughtly.app.mjs @@ -0,0 +1,105 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "thoughtly", + propDefinitions: { + contactId: { + type: "string", + label: "Contact ID", + description: "The ID of the contact.", + async options({ page }) { + const { data: { contacts } } = await this.listContacts({ + params: { + page, + }, + }); + + return contacts.map(({ + id: value, name, phone_number, + }) => ({ + label: `${name || phone_number}`, + value, + })); + }, + }, + interviewId: { + type: "string", + label: "Interview ID", + description: "The ID of the interview.", + async options({ page }) { + const { data: { interviews } } = await this.listInterviews({ + params: { + page, + }, + }); + + return interviews.map(({ + id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.thought.ly"; + }, + _headers() { + return { + "x-api-token": `${this.$auth.api_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contact/create", + ...opts, + }); + }, + callContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contact/call", + ...opts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/contact", + ...opts, + }); + }, + listInterviews(opts = {}) { + return this._makeRequest({ + path: "/interview", + ...opts, + }); + }, + createHook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks/subscribe", + ...opts, + }); + }, + deleteHook(opts = {}) { + return this._makeRequest({ + method: "DELETE", + path: "/webhooks/unsubscribe", + ...opts, + }); + }, + }, +}; diff --git a/components/threads/README.md b/components/threads/README.md new file mode 100644 index 0000000000000..e38e2f30d503f --- /dev/null +++ b/components/threads/README.md @@ -0,0 +1,11 @@ +# Overview + +The Threads API on Pipedream enables you to automate your team's communication in Threads.com, a platform designed to make work discussions more organized and accessible. By integrating the API with Pipedream, you can create workflows to streamline notifications, manage discussions, and sync information across multiple services without writing complex code. + +# Example Use Cases + +- **Automated Status Updates**: Set up a workflow that triggers a new thread in Threads.com whenever a new task is marked as complete in your project management tool, like Trello or Asana. This keeps everyone in the loop with the latest project developments. + +- **Customer Feedback Collection**: Use a workflow that starts when a customer submits feedback through a form, like Typeform or Google Forms. Automatically share this feedback in a designated Threads.com space, ensuring that the right team members can take action and discuss next steps. + +- **Event-Driven Notifications**: Create a workflow that posts to Threads.com when specific events occur in your apps. For example, when a new sale is made in Shopify, a message could be posted to a sales discussion thread, alerting the team about the success and details of the transaction. diff --git a/components/threescribe/README.md b/components/threescribe/README.md index 83abaa3b45ee0..c2a2fc2a0b0ab 100644 --- a/components/threescribe/README.md +++ b/components/threescribe/README.md @@ -1,26 +1,11 @@ # Overview -3Scribe is an online platform that provides an API for developers who want to -build powerful web and mobile applications. By taking advantage of the -platform’s stored data, 3Scribe allows apps to connect with most popular data -sources, such as Facebook, Twitter, and Google, for seamless integration with -existing systems. +The 3Scribe API provides a suite of transcription and natural language processing services, enabling the conversion of audio and video into text. It also offers tools for analyzing and understanding content within these transcriptions, such as sentiment analysis, entity recognition, and keyword extraction. When integrated with Pipedream, 3Scribe becomes a powerful component within a larger automated workflow, providing the ability to trigger actions based on transcribed content, analyze customer interactions, or archive searchable transcripts for compliance and training purposes. -3Scribe's API helps developers to design and build robust applications quickly -and cost-effectively. With its streamlined workflow, developers are able to -utilize existing data sources in their projects, making development and -deployment much faster. Furthermore, 3Scribe’s highest quality standards ensure -that applications integrated with the API are secure, reliable and adhere to -the highest industry standards. +# Example Use Cases -Using the 3Scribe API, developers can create a wide variety of applications, -including: +- **Automated Meeting Minutes**: After every Zoom meeting, trigger a Pipedream workflow that sends the recording to 3Scribe for transcription. Once the transcript is ready, use Pipedream's Google Docs integration to format and save the minutes in a shared company folder, and notify attendees via email using the SendGrid app. -- Social media engagement and marketing tools -- Shopping and e-commerce sites -- Mobile applications -- Online data collection and analysis -- Customized customer relationship management (CRM) software -- Online customer service portals -- Collaboration tools -- Business intelligence dashboards for tracking customer behavior +- **Customer Call Analysis**: Connect 3Scribe to a CRM platform like Salesforce using Pipedream. Every customer support call recorded on your telephony platform can be transcribed by 3Scribe, with subsequent sentiment analysis to detect customer satisfaction. Results can be logged as part of the customer's record in Salesforce, triggering follow-up tasks or satisfaction surveys based on the sentiment. + +- **Content Creation and SEO**: Use 3Scribe on Pipedream to transcribe podcasts or webinars, extract keywords, and generate drafts of blog posts or content summaries. Integrate with WordPress to post the content directly to your site, optimizing for search engines by using the extracted keywords to improve the SEO of your published materials. diff --git a/components/thrivecart/README.md b/components/thrivecart/README.md index e210e41b48293..b137caa8f0261 100644 --- a/components/thrivecart/README.md +++ b/components/thrivecart/README.md @@ -1,32 +1,11 @@ # Overview -The Thrivecart API provides a state-of-the-art shopping cart solution for -creators, entrepreneurs, and marketers worldwide! With our powerful yet easy to -use API, you can build anything from sophisticated e-commerce sites to -automated order fulfillment systems. Here’s a few examples of what you can -create with the Thrivecart API: +Thrivecart's API opens doors to streamlining e-commerce operations by automating cart and sales processes. Harnessing this API through Pipedream allows you to trigger actions based on new sales, refunds, and customer behaviors—think real-time notifications, syncs with your CRM, or updating membership access. It's about moving data where it needs to go without manual intervention. -- Create an integrated order tracking system that connects directly with - Thrivecart, allowing customers to track and manage their orders. -- Build a custom landing page for upsells or cross-sells, allowing customers to - add additional items to their cart before checkout. -- Automate order fulfillment by connecting with third-party fulfillment - services like ShipStation or Fulfillment By Amazon (FBA). -- Create a custom loyalty program that rewards customers with discounts or - points for repeat purchases. -- Connect Thrivecart to existing email marketing platforms such as Mailchimp or - Constant Contact to send automated post-purchase emails to customers. -- Create custom abandonment cart emails that remind customers to finish their - purchase at a later date. -- Create a sophisticated split-testing system to optimize customers’ - experiences and maximize conversions. -- Connect with third-party services like Zapier to integrate other services - that you’re already using into the checkout process. -- Create a custom affiliate system that rewards partners for their referrals - with commissions or discount codes. -- Automatically create account pages for customers where they can view their - orders, updating information and more. +# Example Use Cases -These are just a few of the possibilities with the Thrivecart API! With our -easy-to-use API, the sky’s the limit when it comes to creating custom -e-commerce solutions. +- **Sync Purchases with a CRM**: When a new sale occurs in Thrivecart, use Pipedream to push this data to your CRM like Salesforce or HubSpot. Automate the creation of a customer record and log their purchase details, ensuring your sales team has real-time insights. + +- **Manage Email Marketing Lists**: After a successful transaction, leverage Pipedream to automatically add the customer to segmented email lists in Mailchimp or SendGrid. If it's a repeat purchase, trigger a "thank you" email sequence, or, for first-time buyers, start an onboarding campaign. + +- **Update Access to Membership Sites**: Use Thrivecart's webhook to signal Pipedream when a user purchases or refunds a membership. Then, Pipedream can communicate with platforms like WordPress or Kajabi to adjust the user's access levels accordingly, ensuring a seamless customer experience. diff --git a/components/thrivecart/package.json b/components/thrivecart/package.json new file mode 100644 index 0000000000000..0a8fcd83694d2 --- /dev/null +++ b/components/thrivecart/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/thrivecart", + "version": "0.6.0", + "description": "Pipedream thrivecart Components", + "main": "thrivecart.app.mjs", + "keywords": [ + "pipedream", + "thrivecart" + ], + "homepage": "https://pipedream.com/apps/thrivecart", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/tick/README.md b/components/tick/README.md index 52c8d11d3fe23..03388fdf31444 100644 --- a/components/tick/README.md +++ b/components/tick/README.md @@ -1,20 +1,11 @@ # Overview -With the Tick API, you can build a range of custom applications and -integrations to help your business work smarter and faster. The Tick API allows -users to access and manage their projects, tasks, timesheets, and invoices in -their own applications and websites. +Tick is a time tracking and project management tool designed to help teams keep track of time spent on various tasks and projects. With the Tick API, you can automate project time entries, generate reports, and sync time data with other tools. Specifically, when you plug the Tick API into Pipedream, you leverage serverless workflows to connect Tick with hundreds of other apps. You can automate time tracking, streamline project updates, and trigger actions based on time entry events. -Using the Tick API, you can: +# Example Use Cases -- Create timesheet entries and add time tracking to your own apps -- Receive notifications when a project is marked "Done" -- View, create and update invoices -- Pull real-time analytics such as hours logged, budgeted vs. actual, and - percentage completion -- Get up-to-date activity reports on project activity and task completion -- Automate data entry by creating and updating projects, tasks, and time - entries -- Integrate with other applications such as Slack and GSuite to streamline - workflows -- Use API Key authentication for secure access to data +- **Sync Time Entries with Accounting Software**: Use Pipedream to connect Tick to accounting software like QuickBooks. Whenever a new time entry is recorded in Tick, it triggers a workflow that creates a corresponding invoice item in QuickBooks, ensuring that billing for project hours is accurate and effortless. + +- **Project Time Tracking Alerts**: Set up a Pipedream workflow where, if a project approaches its time budget in Tick, notifications are sent to a Slack channel or by email to the project manager. This helps to keep projects on track and under budget by providing real-time alerts. + +- **Automated Reporting and Insights**: On a scheduled basis, such as the end of each week, Pipedream can trigger a workflow to pull time tracking data from Tick and feed it into a data visualization tool like Google Data Studio. This automatically generates insights into project progress and team productivity, helping stakeholders stay informed without manual report generation. diff --git a/components/ticket_source/package.json b/components/ticket_source/package.json new file mode 100644 index 0000000000000..f1d299f735c5a --- /dev/null +++ b/components/ticket_source/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/ticket_source", + "version": "0.0.1", + "description": "Pipedream Ticket Source Components", + "main": "ticket_source.app.mjs", + "keywords": [ + "pipedream", + "ticket_source" + ], + "homepage": "https://pipedream.com/apps/ticket_source", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/ticket_source/ticket_source.app.mjs b/components/ticket_source/ticket_source.app.mjs new file mode 100644 index 0000000000000..f85e48b61ee0a --- /dev/null +++ b/components/ticket_source/ticket_source.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "ticket_source", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/ticket_tailor/README.md b/components/ticket_tailor/README.md index 3a88988d57467..02ff43c8fdf47 100644 --- a/components/ticket_tailor/README.md +++ b/components/ticket_tailor/README.md @@ -1,18 +1,11 @@ # Overview -With the Ticket Tailor API you can easily build a wide range of ticketing -solutions for any type of event. Here are some examples of what you can build: +The Ticket Tailor API lets you tap into your event management toolkit, giving you the power to automate tasks like syncing attendee lists, sending custom emails after ticket purchase, and monitoring real-time event metrics. Leveraging Pipedream's capabilities, you can build workflows that respond to events in Ticket Tailor, such as new bookings, and connect these triggers to countless other apps to streamline your event management process and enhance attendee experience. -- Securely purchase and issue digital tickets to purchasers via email, SMS, or - print-at-home. -- Seamlessly integrate ticketing across multiple channels, including - pre-registration and walk-in payment. -- Automatically update ticket inventories once tickets are sold. -- Collect additional data about your customers through personalized ticket - forms. -- Set up automated customer service to answer common questions. -- Offer discounts, loyalty programs, upgrades, add-ons and more. -- Customize payment options to include various currencies, payment methods and - more. -- Produce comprehensive sales reports, export data and build custom dashboards. -- Enable access control through introductory level ticket customization. +# Example Use Cases + +- **Automated Attendee Welcome Messages**: Once a new ticket is purchased via Ticket Tailor, trigger an automated workflow on Pipedream that sends a personalized welcome email through SendGrid or another email service. This can include event details, links to additional resources, or exclusive offers. + +- **Real-time Event Dashboards**: Create a Pipedream workflow that listens for new ticket sales in Ticket Tailor and updates a Google Sheets or Airtable base in real-time. This allows for live tracking of ticket sales data, attendee demographics, and revenue for swift analytical insights and reporting. + +- **Post-Event Feedback Collection**: After an event concludes, use Pipedream to trigger a feedback survey dispatch through apps like Typeform or SurveyMonkey. This can be set to automatically send a certain time after the event ends, collecting valuable attendee insights to inform future events. diff --git a/components/ticktick/README.md b/components/ticktick/README.md index 5bfbc4ac19d90..96aa7f7237fea 100644 --- a/components/ticktick/README.md +++ b/components/ticktick/README.md @@ -1,20 +1,11 @@ # Overview -The Ticktick API allows developers to create and manage tasks and to dos in a -variety of environments. With the API, users can manage their personal tasks, -tasks from multiple team members, and combine tasks from diverse sources. Here -are some examples of what you can build with the Ticktick API: +The TickTick API lets you tap into a robust task management framework from Pipedream. If you've ever wanted to streamline your to-do list or automate routine task management, this is your gateway. Use the API to create tasks, retrieve task details, update task status, and more. Automating your TickTick tasks can free up time, ensuring important items never slip through the cracks. -- Automatically send reminders for upcoming tasks. -- Create task lists that can be shared with colleagues or team members. -- Set up recurring task lists, such as daily or weekly checklists. -- Synchronize tasks between multiple accounts, such as Gmail and Outlook. -- Display task lists in a variety of formats, such as an interactive calendar - or calendar view. -- Integrate with third-party applications and services, such as Google Drive - and Dropbox. -- Generate custom reports from task data. -- Automatically sync tasks between different devices, such as desktops, - tablets, and smartphones. -- Automatically notify assigned users about their tasks via email or SMS. -- Manage complex workflows and enable collaborative task completion. +# Example Use Cases + +- **Task Creation From Email**: Automatically create a TickTick task whenever you receive an email with a specific subject line or from a particular sender. This is great for turning emails into actionable items without manual entry. + +- **Daily Task Digest**: Generate a daily summary of your TickTick tasks and send it to your Slack channel every morning. This workflow keeps you and your team informed on the day's priorities. + +- **Meeting Follow-Ups**: After a Google Calendar event ends, create follow-up tasks in TickTick based on the meeting notes from Google Docs. This ensures that action items are captured and assigned without fail. diff --git a/components/tidy/README.md b/components/tidy/README.md new file mode 100644 index 0000000000000..f9ffa28dcf6f0 --- /dev/null +++ b/components/tidy/README.md @@ -0,0 +1,11 @@ +# Overview + +The Tidy API offers a range of functions tailored for property management, enabling users to automate tasks like booking cleanings, managing properties, and keeping track of services. Integrating the Tidy API into Pipedream workflows opens up possibilities for automating communications, scheduling, and property management tasks by connecting Tidy with hundreds of other apps. This can significantly streamline operations for property managers, cleaning services, and real estate businesses. + +# Example Use Cases + +- **Automate Cleaning Schedules**: Trigger a workflow in Pipedream that schedules a cleaning in Tidy whenever a guest checks out in your Airbnb booking system. This ensures your property is always clean and ready for the next guest without manual intervention. + +- **Sync Cleaning Status to Google Sheets**: After each cleaning job completion, use a Pipedream workflow to update a Google Sheets spreadsheet with the details. This can help you track cleaning times and maintain records for billing and operational purposes. + +- **Send Post-Cleaning Feedback Requests**: Set up a Pipedream workflow to automatically send a feedback request email through SendGrid or a similar email service to the property owner or manager once Tidy confirms a cleaning job is done. This can help in maintaining service quality and customer satisfaction. diff --git a/components/tidycal/README.md b/components/tidycal/README.md new file mode 100644 index 0000000000000..1f4f1134fc05d --- /dev/null +++ b/components/tidycal/README.md @@ -0,0 +1,11 @@ +# Overview + +The TidyCal API allows for scheduling automation, linking your calendar to create and manage bookings. In Pipedream, you can harness this API to create event-driven workflows, such as syncing with other calendar services, triggering reminders, or connecting with CRM systems to streamline your scheduling process. Pipedream's serverless platform enables you to build and run these workflows efficiently, without the need to manage infrastructure. + +# Example Use Cases + +- **Sync TidyCal with Google Calendar**: When a new booking is made via TidyCal, automatically create an event in Google Calendar. Use this to keep all your appointments in sync across different calendar services. + +- **Send Custom Email Reminders**: Set up a workflow that sends personalized email reminders to your clients via SendGrid, Mailgun, or Gmail before their appointment. Include details like date, time, and preparation instructions. + +- **Integrate with CRM Systems**: Upon a new TidyCal booking, add or update the client's details in a CRM like Salesforce or HubSpot. This can help in maintaining updated customer information and streamline follow-ups. diff --git a/components/tidyhq/package.json b/components/tidyhq/package.json new file mode 100644 index 0000000000000..4df3a74b65184 --- /dev/null +++ b/components/tidyhq/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/tidyhq", + "version": "0.0.1", + "description": "Pipedream TidyHQ Components", + "main": "tidyhq.app.mjs", + "keywords": [ + "pipedream", + "tidyhq" + ], + "homepage": "https://pipedream.com/apps/tidyhq", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/tidyhq/tidyhq.app.mjs b/components/tidyhq/tidyhq.app.mjs new file mode 100644 index 0000000000000..cb09055d371bd --- /dev/null +++ b/components/tidyhq/tidyhq.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "tidyhq", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/tilda/README.md b/components/tilda/README.md index 9cbe0a36e9965..d850a8e4e234c 100644 --- a/components/tilda/README.md +++ b/components/tilda/README.md @@ -1,26 +1,11 @@ # Overview -The Tilda API provides an easy-to-use interface for creating beautiful, custom -websites. With the Tilda API, you can create and manage a variety of content -types, from static HTML sites, to database-driven web applications. +The Tilda API on Pipedream allows you to programmatically interact with your Tilda websites, enabling you to retrieve project data, export pages or content, and sync information between Tilda and other services. By leveraging Pipedream's capabilities, you can automate content updates, streamline publication workflows, and connect Tilda with a myriad of third-party applications for enhanced productivity and dynamic content management. -Tilda provides an extensive library of features that make web building a -breeze. For example, with Tilda, you can easily add multimedia content, style -elements, manage SEO, customize layout and more. Plus, you can add third-party -integrations like Google Analytics to track website performance, and manage -users for easy collaboration. +# Example Use Cases -With the Tilda API, you can build a variety of websites, from basic static -sites, to more complex web applications. Here are just a few examples of what -you can create with Tilda: +- **Automated Content Backup:** Set up a Pipedream workflow that periodically triggers a backup of your Tilda website content. Store the backups in a cloud storage service like Dropbox, ensuring your data is safe and readily accessible for recovery or archiving. -- Informational websites -- Online portfolios -- Blogs -- E-commerce stores -- Database-driven web applications -- Communities -- Intranets -- Custom dashboards -- Corporate websites -- Online magazines +- **Content Sync Between Tilda and a CMS:** Create a workflow that synchronizes new pages or updates from Tilda with a Content Management System (CMS) like WordPress. When you publish or update content on Tilda, Pipedream can automatically reflect these changes on your WordPress site, keeping both platforms in sync. + +- **Tilda Analytics Reporting:** Connect Tilda with a data visualization tool such as Google Data Studio. Use Pipedream to extract website performance metrics from Tilda and feed them into Data Studio, allowing for the creation of custom reports and dashboards that track user engagement and site analytics. diff --git a/components/tilda/package.json b/components/tilda/package.json new file mode 100644 index 0000000000000..3d75b890a66b0 --- /dev/null +++ b/components/tilda/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/tilda", + "version": "0.6.0", + "description": "Pipedream tilda Components", + "main": "tilda.app.mjs", + "keywords": [ + "pipedream", + "tilda" + ], + "homepage": "https://pipedream.com/apps/tilda", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/tiledesk/package.json b/components/tiledesk/package.json new file mode 100644 index 0000000000000..a3d9398394542 --- /dev/null +++ b/components/tiledesk/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/tiledesk", + "version": "0.0.1", + "description": "Pipedream Tiledesk Components", + "main": "tiledesk.app.mjs", + "keywords": [ + "pipedream", + "tiledesk" + ], + "homepage": "https://pipedream.com/apps/tiledesk", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/tiledesk/tiledesk.app.mjs b/components/tiledesk/tiledesk.app.mjs new file mode 100644 index 0000000000000..ecadc11c60c11 --- /dev/null +++ b/components/tiledesk/tiledesk.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "tiledesk", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/time_doctor/README.md b/components/time_doctor/README.md new file mode 100644 index 0000000000000..0698ebe19424a --- /dev/null +++ b/components/time_doctor/README.md @@ -0,0 +1,11 @@ +# Overview + +The Time Doctor API lets you tap into detailed data on how you or your team spend work hours. With Pipedream's serverless platform, you can automate actions based on this data, like logging work hours, analyzing productivity, or integrating with other business tools to streamline workflows. + +# Example Use Cases + +- **Time Tracking Sync**: Automate the syncing of time tracking data from Time Doctor to your project management tool. Each time an entry is created or updated in Time Doctor, a Pipedream workflow can trigger to update tasks in your project management app, keeping all records aligned. + +- **Productivity Reports to Slack**: Generate daily or weekly productivity reports and send them to a dedicated Slack channel. Set up a Pipedream workflow that aggregates Time Doctor data, compiles a report, and posts it to Slack, so the team stays informed on productivity trends. + +- **Alerts for Unusual Activity**: Create alerts for when employees log more or fewer hours than a threshold. Use Pipedream to monitor Time Doctor entries, detect anomalies, and send notifications via email or SMS, helping to maintain consistent work patterns or identify burnout risks. diff --git a/components/time_doctor/package.json b/components/time_doctor/package.json index 8cf8b018f7f22..fcc7e765b2cfd 100644 --- a/components/time_doctor/package.json +++ b/components/time_doctor/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/time_tracker_by_ebillity/README.md b/components/time_tracker_by_ebillity/README.md new file mode 100644 index 0000000000000..f4451211eff70 --- /dev/null +++ b/components/time_tracker_by_ebillity/README.md @@ -0,0 +1,11 @@ +# Overview + +The Time Tracker by eBillity API lets you interact with a robust time tracking system for invoicing and payroll. On Pipedream, you can harness this API to create automated workflows that simplify how you manage time entries, client billing, and employee payroll processes. By connecting Time Tracker with other apps on Pipedream, you can automate data syncing, generate reports, and trigger actions, streamlining your time tracking operations. + +# Example Use Cases + +- **Sync Time Entries with Accounting Software**: Automatically sync recorded time entries from Time Tracker to your accounting software, like QuickBooks or Xero. When a new time entry is created in Time Tracker, the workflow triggers an action that creates a corresponding record in your accounting system for real-time invoice updating. + +- **Time Entry Approval Notifications**: Set up a workflow that monitors time entries for approval status. When a time entry is approved within Time Tracker, it triggers a Pipedream workflow that sends a notification via email or a messaging app, like Slack, to inform the relevant personnel or the employee who logged the hours. + +- **Automated Payroll Processing**: Create a workflow that aggregates approved time entries at the end of a pay period. The workflow can then interact with payroll services such as Gusto or ADP to automatically process payments based on the approved time data, reducing manual payroll efforts and the risk of errors. diff --git a/components/timebuzzer/README.md b/components/timebuzzer/README.md new file mode 100644 index 0000000000000..d2d53a8a13fde --- /dev/null +++ b/components/timebuzzer/README.md @@ -0,0 +1,11 @@ +# Overview + +The timeBuzzer API enables you to track, manage, and analyze time spent across various projects and tasks, directly from the Pipedream platform. With this API, you can create, update, and retrieve time entries, as well as manage projects and activities. This provides a powerful way to automate your workflow, integrate with other apps, and streamline your time tracking processes. + +# Example Use Cases + +- **Automated Time Tracking Alerts**: Send alerts through Slack or email when a new time entry is started without a project assigned, ensuring all time is accounted for correctly. + +- **Project Time Analysis Reports**: Generate weekly reports on time spent per project by connecting timeBuzzer to Google Sheets, allowing for easy analysis and insights into team productivity. + +- **Invoice Creation Based on Time Entries**: Integrate with accounting software like QuickBooks to create invoices automatically based on completed time entries, reducing manual data entry and potential for errors. diff --git a/components/timecamp/README.md b/components/timecamp/README.md index 99574edff0efb..8822977938026 100644 --- a/components/timecamp/README.md +++ b/components/timecamp/README.md @@ -1,38 +1,11 @@ # Overview -The TimeCamp API gives you access to an easy-to-use API with powerful features. -With TimeCamp API, you can develop custom applications for time tracking, -billing and other related tasks. Here are some of the most common tasks you can -do with TimeCamp API: +TimeCamp's API unlocks the potential to automate and integrate time tracking data with a myriad of services and platforms. By leveraging Pipedream's capabilities, you can create custom workflows that respond to events in TimeCamp, such as new time entries, projects, or tasks. These workflows can then trigger actions in other apps, or even process and analyze time tracking data to provide insights and streamline operations. -- Track time, activities, and expenses: Timesheets, project time, task - tracking, and activity logs. -- Manage users, projects and teams: Set user, team, and project roles and - manage them through APIs. -- Generate reports and dashboards: Generate time and activity reports for - insights. -- Invoice and bill customers: Automatically generate invoices and bill - customers for services and products. -- Integrate with other software: Connect with other software such as - QuickBooks, Salesforce, Asana, and more. -- Automate tasks and processes: Automate time and activity tracking, as well as - invoicing and billing processes. -- Monitor progress and performance: Track progress, productivity, and - performance, with reports, analytics, and dashboards. +# Example Use Cases -Here are some examples of what you can build using the TimeCamp API: +- **Automated Client Invoicing**: Send time tracking data from TimeCamp to an accounting app like QuickBooks to automatically create and send invoices to clients based on the hours logged in TimeCamp. -- An app to track employee time and attendance -- A plugin to create invoice and bill customers directly from the platform -- An integration to connect with other tools within the system -- A time tracking and attendance app for remote employees -- A project tracking tool to monitor tasks and resources -- An automated invoicing and billing system -- A custom analytics and reporting system for project performance -- An app to streamline employee onboarding and training -- A tracking and scheduling system to efficiently manage teams and resources. +- **Project Management Sync**: Keep project statuses updated by syncing new or modified TimeCamp projects and tasks with project management tools like Trello or Asana. When a task is marked complete in TimeCamp, automatically update the corresponding card or task in your project management tool. -The possibilities are truly endless with the TimeCamp API. With it, you can -build the necessary tools to help your business manage time and activity -tracking, invoicing and billing, and more. Unleash your creativity and start -building anything you need to help your business succeed. +- **Real-time Reporting and Alerts**: Set up a workflow that listens for new time entries in TimeCamp, processes the data, and sends a real-time report or alert to communication platforms like Slack or Microsoft Teams. This can keep teams informed about project progress or notify managers if time tracking thresholds are met or exceeded. diff --git a/components/timekit/README.md b/components/timekit/README.md index 76c6dda9951cb..5525fed976270 100644 --- a/components/timekit/README.md +++ b/components/timekit/README.md @@ -1,21 +1,11 @@ # Overview -Timekit's API allows developers to build and integrate sophisticated online -appointment booking software into their applications. With the Timekit API, you -can quickly and easily build a variety of powerful scheduling and appointment -booking solutions such as calendaring, appointment reminders, booking forms, -and more. The API is designed to be simple and easy to use, allowing developers -to create solutions that are both user friendly and powerful. +Timekit is a flexible booking and resource management API that enables developers to create and manage appointments and calendars. With Timekit, you can automate the scheduling process, sync calendars, manage bookings, and craft customized booking experiences. Using Pipedream, you can leverage Timekit to create efficient workflows that automate scheduling-related tasks, trigger actions based on calendar events, and integrate with various other services for a seamless operational ecosystem. -Here are just some of the things you can build with Timekit's API: +# Example Use Cases -- Calendaring and scheduling tools -- Custom appointment booking forms -- Automated appointment reminders -- Multi-calendar views -- Staff and resource management -- Client portals and self-service booking pages -- Integrations with popular third party services -- Stripe and other payment integrations -- Customizable branding and styling -- Event booking solutions +- **Automated Appointment Confirmation Emails**: Trigger an automated email via SendGrid to confirm appointments whenever a new booking is made through Timekit. This workflow ensures that both the service provider and the customer receive timely notifications, enhancing communication and reducing no-shows. + +- **Slack Notifications for Booking Changes**: Set up a workflow that sends instant Slack messages to a specific channel or user whenever an appointment is rescheduled or canceled. This keeps teams immediately informed about schedule changes, allowing for swift adjustments in staff allocation or customer management. + +- **Google Sheets Booking Log**: Record every new booking from Timekit into a Google Sheets spreadsheet. This is particularly useful for data analysis, tracking customer engagement, and maintaining a centralized log for appointments that can be accessed by various departments for coordination and strategic planning. diff --git a/components/timely_time_tracking/README.md b/components/timely_time_tracking/README.md new file mode 100644 index 0000000000000..9a910f6641808 --- /dev/null +++ b/components/timely_time_tracking/README.md @@ -0,0 +1,11 @@ +# Overview + +The Timely Time Tracking API lets you automate your time tracking, reporting, and project management tasks. With Pipedream, you can connect the API to create custom workflows that fit your needs without managing infrastructure. You can streamline time entries, sync project data, and trigger actions based on time tracked. Using Pipedream's serverless platform, you can execute code that interacts with Timely's data on a schedule, or in response to HTTP requests or events from other apps. + +# Example Use Cases + +- **Automate Time Tracking Reports**: Generate weekly or monthly time tracking reports and send them via email or store them in Google Sheets. Set up a Pipedream workflow that triggers on a schedule to fetch time entries from Timely, compile them into a report, and send it through Gmail or save it to Google Sheets. + +- **Project Time Budget Alerts**: Create a system that alerts project managers when the time spent on a project approaches its budget. A Pipedream workflow can monitor time entries in Timely, calculate the total time, and when it nears the budget threshold, it can send a notification through Slack or SMS via Twilio to the concerned individuals. + +- **Sync Timely Data with Project Management Tools**: Keep your project management tool, like Trello or Asana, in sync with Timely. Use Pipedream to build a workflow that listens for new time entries in Timely, then creates or updates tasks in Trello or Asana with the relevant time data, ensuring all your project information stays up-to-date. diff --git a/components/timely_time_tracking/package.json b/components/timely_time_tracking/package.json index e148ec1aaf643..91e51bad25de3 100644 --- a/components/timely_time_tracking/package.json +++ b/components/timely_time_tracking/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/timely_time_tracking/timely_time_tracking.app.mjs b/components/timely_time_tracking/timely_time_tracking.app.mjs index 19794d5a1aa40..8427027589717 100644 --- a/components/timely_time_tracking/timely_time_tracking.app.mjs +++ b/components/timely_time_tracking/timely_time_tracking.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/timetonic/README.md b/components/timetonic/README.md new file mode 100644 index 0000000000000..ebadba979b6fa --- /dev/null +++ b/components/timetonic/README.md @@ -0,0 +1,11 @@ +# Overview + +TimeTonic API on Pipedream allows you to automate workflows involving project management and data organization. With TimeTonic, you can create, update, retrieve, and manage databases and tables dynamically, ideal for businesses looking to streamline operations like task tracking, inventory management, and team collaboration. Leveraging Pipedream's capabilities, you can integrate TimeTonic with numerous other apps to automate data flow across platforms, trigger actions based on specific conditions, and maintain real-time synchronization of business operations. + +# Example Use Cases + +- **Project Task Automation**: Automatically create tasks in TimeTonic whenever a new sale is recorded in Shopify. This workflow can help ensure that each order triggers a set of fulfillment tasks, keeping your project management tightly aligned with sales activities. + +- **Team Collaboration Enhancement**: Sync TimeTonic with Slack to alert team members when a new task is assigned or updated. This integration keeps your team informed in real-time, enhancing communication and ensuring everyone is on the same page without manually checking task updates. + +- **Inventory Tracking System**: Set up a workflow where stock level updates in TimeTonic trigger reorder notifications in an email app like Gmail when inventory falls below a certain threshold. This helps maintain optimal stock levels through automated alerts, reducing the risk of stockouts and overstock situations. diff --git a/components/timetonic/actions/common/create-update-row.mjs b/components/timetonic/actions/common/create-update-row.mjs new file mode 100644 index 0000000000000..de684d75e77f0 --- /dev/null +++ b/components/timetonic/actions/common/create-update-row.mjs @@ -0,0 +1,178 @@ +import timetonic from "../../timetonic.app.mjs"; +import constants from "../../common/constants.mjs"; +import fs from "fs"; +import FormData from "form-data"; + +export default { + props: { + timetonic, + bookCode: { + propDefinition: [ + timetonic, + "bookCode", + ], + }, + tableId: { + propDefinition: [ + timetonic, + "tableId", + (c) => ({ + bookCode: c.bookCode, + }), + ], + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (!this.tableId || !this.bookCode) { + return props; + } + const { bookTables: { categories } } = await this.timetonic.listTables({ + params: { + b_c: this.bookCode, + includeFields: true, + }, + }); + const { fields } = categories.find(({ id }) => id === this.tableId); + for (const field of fields) { + if (!field?.readOnly) { + const id = `${field.id}`; + props[id] = { + type: constants.FIELD_TYPES[field.type] || "string", + label: field.name, + optional: this.isUpdate() + ? true + : !field?.required, + }; + if (field.type === "link") { + const linkTableId = field.link.category.id; + const { tableRows } = await this.timetonic.listRows({ + params: { + catId: linkTableId, + }, + }); + const options = tableRows?.map(({ + id, name: label, + }) => ({ + value: `${id}`, + label, + })) || []; + props[id].options = options; + props[id].description = "The Row ID from the linked table to create a link to"; + tableRows.forEach(({ + id: rowId, name, + }) => { + props[`${id}_${rowId}_link_text`] = { + type: "string", + default: name, + hidden: true, + }; + }); + } + if (field.type === "file" || field.type === "files") { + props[id].description = "The path to the file saved to the `/tmp` directory (e.g. `/tmp/example.pdf`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory)."; + props[`${id}_is_file`] = { + type: "boolean", + default: true, + hidden: true, + }; + } + } + } + return props; + }, + methods: { + isUpdate() { + return false; + }, + uploadFile($, fieldId, filePath, rowId) { + const fileStream = fs.createReadStream(filePath.includes("/tmp") + ? filePath + : `/tmp/${filePath}`); + const formData = new FormData(); + formData.append("qqfile", fileStream); + return this.timetonic.uploadFile({ + $, + params: { + b_c: this.bookCode, + fieldId, + rowId, + }, + data: formData, + headers: formData.getHeaders(), + }); + }, + }, + async run({ $ }) { + const { + timetonic, + // eslint-disable-next-line no-unused-vars + isUpdate, + uploadFile, + // eslint-disable-next-line no-unused-vars + bookCode, + tableId, + rowId = `tmp${Math.random().toString(36) + .substr(2, 9)}`, + ...fields + } = this; + + const fieldValues = {}; + const files = []; + for (const [ + key, + value, + ] of Object.entries(fields)) { + if (key.includes("link_text") || key.includes("is_file")) { + continue; + } + if (fields[`${key}_is_file`]) { + files.push({ + fieldId: key, + filePath: value, + }); + continue; + } + fieldValues[+key] = fields[`${key}_${value}_link_text`] + ? [ + { + row_id: +value, + value: fields[`${key}_${value}_link_text`], + }, + ] + : value; + } + // if fieldValues is empty, createOrUpdateRow will create a new row + // if updating, get and return the row instead + const response = isUpdate() && !Object.entries(fieldValues).length + ? await timetonic.getTableValues({ + $, + params: { + b_c: bookCode, + catId: tableId, + filterRowIds: { + row_ids: [ + rowId, + ], + }, + }, + }) + : await timetonic.createOrUpdateRow({ + $, + params: { + rowId, + catId: tableId, + fieldValues, + }, + }); + const newRowId = this.rowId || response.rows[0].id; + for (const file of files) { + await uploadFile($, file.fieldId, file.filePath, newRowId); + } + $.export("$summary", `Successfully ${isUpdate() + ? "updated" + : "created"} row in table ${tableId}`); + return response; + }, +}; diff --git a/components/timetonic/actions/create-row/create-row.mjs b/components/timetonic/actions/create-row/create-row.mjs new file mode 100644 index 0000000000000..ff71499913bcf --- /dev/null +++ b/components/timetonic/actions/create-row/create-row.mjs @@ -0,0 +1,10 @@ +import common from "../common/create-update-row.mjs"; + +export default { + ...common, + key: "timetonic-create-row", + name: "Create Row", + description: "Create a new row within an existing table in TimeTonic. [See the documentation](https://timetonic.com/live/apidoc/#api-Smart_table_operations-createOrUpdateTableRow)", + version: "0.0.1", + type: "action", +}; diff --git a/components/timetonic/actions/delete-row/delete-row.mjs b/components/timetonic/actions/delete-row/delete-row.mjs new file mode 100644 index 0000000000000..e8cf3c0b460e5 --- /dev/null +++ b/components/timetonic/actions/delete-row/delete-row.mjs @@ -0,0 +1,46 @@ +import timetonic from "../../timetonic.app.mjs"; + +export default { + key: "timetonic-delete-row", + name: "Delete Row", + description: "Deletes a row within an existing table in TimeTonic. [See the documentation](https://timetonic.com/live/apidoc/#api-Smart_table_operations-deleteTableRow)", + version: "0.0.1", + type: "action", + props: { + timetonic, + bookCode: { + propDefinition: [ + timetonic, + "bookCode", + ], + }, + tableId: { + propDefinition: [ + timetonic, + "tableId", + (c) => ({ + bookCode: c.bookCode, + }), + ], + }, + rowId: { + propDefinition: [ + timetonic, + "rowId", + (c) => ({ + tableId: c.tableId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.timetonic.deleteRow({ + $, + params: { + rowId: this.rowId, + }, + }); + $.export("$summary", `Successfully deleted row with ID ${this.rowId}`); + return response; + }, +}; diff --git a/components/timetonic/actions/search-rows/search-rows.mjs b/components/timetonic/actions/search-rows/search-rows.mjs new file mode 100644 index 0000000000000..ff5f336cb66d7 --- /dev/null +++ b/components/timetonic/actions/search-rows/search-rows.mjs @@ -0,0 +1,100 @@ +import timetonic from "../../timetonic.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "timetonic-search-rows", + name: "Search Rows", + description: "Perform a search across table rows based on given criteria. [See the documentation](https://timetonic.com/live/apidoc/#api-Smart_table_operations-listTableRowsById)", + version: "0.0.1", + type: "action", + props: { + timetonic, + bookCode: { + propDefinition: [ + timetonic, + "bookCode", + ], + }, + tableId: { + propDefinition: [ + timetonic, + "tableId", + (c) => ({ + bookCode: c.bookCode, + }), + ], + }, + searchField: { + propDefinition: [ + timetonic, + "fieldId", + (c) => ({ + bookCode: c.bookCode, + tableId: c.tableId, + }), + ], + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (!this.searchField || !this.bookCode || !this.tableId) { + return props; + } + const { tableValues: { fields } } = await this.timetonic.getTableValues({ + params: { + catId: this.tableId, + b_c: this.bookCode, + }, + }); + const field = fields.find(({ id }) => id === this.searchField); + props.searchValue = { + type: constants.FIELD_TYPES[field.type] || "string", + label: "Search Value", + description: "The value to search for", + }; + if (field.type === "link") { + props.searchValue.description = `Please enter the Row ID from ${field.link.category.name} to link to`; + } + return props; + }, + methods: { + findMatches(fields) { + const field = fields.find(({ id }) => id === this.searchField); + let matches; + if (field.type === "link") { + matches = field.values.filter(({ value }) => + value?.length && value.find((link) => link.row_id == this.searchValue)); + } else { + matches = field.values.filter(({ value }) => value == this.searchValue); + } + return matches.map(({ id }) => id); + }, + buildRow(fields, matches) { + const rows = []; + matches.forEach((match) => { + const row = {}; + fields.forEach((field) => { + row[field.name] = field.values.find(({ id }) => id === match).value; + }); + rows.push(row); + }); + return rows; + }, + }, + async run({ $ }) { + const { tableValues: { fields } } = await this.timetonic.getTableValues({ + $, + params: { + catId: this.tableId, + b_c: this.bookCode, + }, + }); + const matches = this.findMatches(fields); + const rows = this.buildRow(fields, matches); + $.export("$summary", `Found ${rows.length} matching row${rows.length === 1 + ? "" + : "s"}`); + return rows; + }, +}; diff --git a/components/timetonic/actions/update-row/update-row.mjs b/components/timetonic/actions/update-row/update-row.mjs new file mode 100644 index 0000000000000..ec742ab095e29 --- /dev/null +++ b/components/timetonic/actions/update-row/update-row.mjs @@ -0,0 +1,28 @@ +import common from "../common/create-update-row.mjs"; + +export default { + ...common, + key: "timetonic-update-row", + name: "Update Row", + description: "Updates the values within a specified row in a table. [See the documentation](https://timetonic.com/live/apidoc/#api-Smart_table_operations-createOrUpdateTableRow)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + rowId: { + propDefinition: [ + common.props.timetonic, + "rowId", + (c) => ({ + tableId: c.tableId, + }), + ], + }, + }, + methods: { + ...common.methods, + isUpdate() { + return true; + }, + }, +}; diff --git a/components/timetonic/common/constants.mjs b/components/timetonic/common/constants.mjs new file mode 100644 index 0000000000000..e100733dfe6be --- /dev/null +++ b/components/timetonic/common/constants.mjs @@ -0,0 +1,17 @@ +const DEFAULT_LIMIT = 100; + +const FIELD_TYPES = { + shorttext: "string", + mediumtext: "string", + email: "string", + phone: "string", + date: "string", + link: "string", + boolean: "boolean", + int: "integer", +}; + +export default { + DEFAULT_LIMIT, + FIELD_TYPES, +}; diff --git a/components/timetonic/package.json b/components/timetonic/package.json new file mode 100644 index 0000000000000..7ea1d96f6b3d8 --- /dev/null +++ b/components/timetonic/package.json @@ -0,0 +1,20 @@ +{ + "name": "@pipedream/timetonic", + "version": "0.1.0", + "description": "Pipedream TimeTonic Components", + "main": "timetonic.app.mjs", + "keywords": [ + "pipedream", + "timetonic" + ], + "homepage": "https://pipedream.com/apps/timetonic", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5", + "form-data": "^4.0.0", + "md5": "^2.3.0" + } +} diff --git a/components/timetonic/sources/common/base.mjs b/components/timetonic/sources/common/base.mjs new file mode 100644 index 0000000000000..c784c3b689a19 --- /dev/null +++ b/components/timetonic/sources/common/base.mjs @@ -0,0 +1,67 @@ +import timetonic from "../../timetonic.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + timetonic, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + bookCode: { + propDefinition: [ + timetonic, + "bookCode", + ], + }, + tableId: { + propDefinition: [ + timetonic, + "tableId", + (c) => ({ + bookCode: c.bookCode, + }), + ], + }, + }, + methods: { + formatFields(rows) { + rows.forEach((row) => { + const fields = {}; + for (const [ + key, + value, + ] of Object.entries(row.fields)) { + fields[key] = value.value; + } + row.fields = fields; + }); + return rows; + }, + }, + async run() { + const params = { + catId: this.tableId, + b_c: this.bookCode, + format: "rows", + }; + if (this.viewId) { + params.filterRowIds = { + applyViewFilters: this.viewId, + }; + } + const results = this.timetonic.paginate({ + resourceFn: this.timetonic.getTableValues, + params, + }); + const rows = []; + for await (const row of results) { + rows.push(row); + } + const formattedRows = this.formatFields(rows); + await this.processRows(formattedRows); + }, +}; diff --git a/components/timetonic/sources/new-table-row-in-view/new-table-row-in-view.mjs b/components/timetonic/sources/new-table-row-in-view/new-table-row-in-view.mjs new file mode 100644 index 0000000000000..c1404b26de242 --- /dev/null +++ b/components/timetonic/sources/new-table-row-in-view/new-table-row-in-view.mjs @@ -0,0 +1,40 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "timetonic-new-table-row-in-view", + name: "New Table Row in View", + description: "Emit new event when a new table row appears in a specific view.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + viewId: { + propDefinition: [ + common.props.timetonic, + "viewId", + (c) => ({ + bookCode: c.bookCode, + tableId: c.tableId, + }), + ], + }, + }, + methods: { + ...common.methods, + generateMeta(row) { + return { + id: row.id, + summary: `New Row in View with ID: ${row.id}`, + ts: Date.now(), + }; + }, + processRows(rows) { + for (const row of rows) { + const meta = this.generateMeta(row); + this.$emit(row.fields, meta); + } + }, + }, +}; diff --git a/components/timetonic/sources/new-table-row/new-table-row.mjs b/components/timetonic/sources/new-table-row/new-table-row.mjs new file mode 100644 index 0000000000000..a48489d659514 --- /dev/null +++ b/components/timetonic/sources/new-table-row/new-table-row.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "timetonic-new-table-row", + name: "New Table Row", + description: "Emit new event when a new table row is added in TimeTonic", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + generateMeta(row) { + return { + id: row.id, + summary: `New Row with ID: ${row.id}`, + ts: Date.now(), + }; + }, + processRows(rows) { + for (const row of rows) { + const meta = this.generateMeta(row); + this.$emit(row.fields, meta); + } + }, + }, +}; diff --git a/components/timetonic/sources/row-deleted/row-deleted.mjs b/components/timetonic/sources/row-deleted/row-deleted.mjs new file mode 100644 index 0000000000000..17a5ebca7ac4e --- /dev/null +++ b/components/timetonic/sources/row-deleted/row-deleted.mjs @@ -0,0 +1,41 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "timetonic-row-deleted", + name: "Row Deleted", + description: "Emit new event when a row is deleted in a table.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + _getRowIds() { + return this.db.get("rowIds") || []; + }, + _setRowIds(rowIds) { + this.db.set("rowIds", rowIds); + }, + generateMeta(row) { + return { + id: row.id, + summary: `Row with ID: ${row.id} Deleted`, + ts: Date.now(), + }; + }, + processRows(rows) { + const previousRowIds = this._getRowIds(); + const currentRowIds = rows.map(({ id }) => id); + for (const id of previousRowIds) { + if (!currentRowIds.includes(id)) { + const row = { + id, + }; + const meta = this.generateMeta(row); + this.$emit(row, meta); + } + } + this._setRowIds(currentRowIds); + }, + }, +}; diff --git a/components/timetonic/sources/row-updated/row-updated.mjs b/components/timetonic/sources/row-updated/row-updated.mjs new file mode 100644 index 0000000000000..70e5d0c44c7c6 --- /dev/null +++ b/components/timetonic/sources/row-updated/row-updated.mjs @@ -0,0 +1,41 @@ +import common from "../common/base.mjs"; +import md5 from "md5"; + +export default { + ...common, + key: "timetonic-row-updated", + name: "Row Updated", + description: "Emit new event when a row is updated in a table.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + _getPreviousRows() { + return this.db.get("previousRows") || {}; + }, + _setPreviousRows(previousRows) { + this.db.set("previousRows", previousRows); + }, + generateMeta(row) { + const ts = Date.now(); + return { + id: `${row.id}${ts}`, + summary: `Row Updated with ID: ${row.id}`, + ts, + }; + }, + processRows(rows) { + const previousRows = this._getPreviousRows(); + for (const row of rows) { + const hash = md5(JSON.stringify(row.fields)); + if (previousRows[row.id] !== hash) { + const meta = this.generateMeta(row); + this.$emit(row.fields, meta); + previousRows[row.id] = hash; + } + } + this._setPreviousRows(previousRows); + }, + }, +}; diff --git a/components/timetonic/timetonic.app.mjs b/components/timetonic/timetonic.app.mjs new file mode 100644 index 0000000000000..520ad00efab77 --- /dev/null +++ b/components/timetonic/timetonic.app.mjs @@ -0,0 +1,191 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "timetonic", + propDefinitions: { + bookCode: { + type: "string", + label: "Book Code", + description: "The book code of the book", + async options() { + const { allBooks: { books } } = await this.listBooks(); + return books?.map(({ + b_c: value, ownerPrefs, + }) => ({ + value, + label: ownerPrefs?.title, + })) || []; + }, + }, + tableId: { + type: "string", + label: "Table ID", + description: "The ID of the table", + async options({ bookCode }) { + const { bookTables: { categories } } = await this.listTables({ + params: { + b_c: bookCode, + }, + }); + return categories?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + rowId: { + type: "string", + label: "Row ID", + description: "The ID of the row", + async options({ tableId }) { + const { tableRows } = await this.listRows({ + params: { + catId: tableId, + }, + }); + return tableRows?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + fieldId: { + type: "integer", + label: "Field ID", + description: "The ID of the field to search", + async options({ + bookCode, tableId, + }) { + const { bookTables: { categories } } = await this.listTables({ + params: { + b_c: bookCode, + includeFields: true, + }, + }); + const { fields } = categories.find(({ id }) => id === tableId); + return fields?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + viewId: { + type: "string", + label: "View ID", + description: "The ID of the view", + async options({ + bookCode, tableId, + }) { + const { tableValues: { views } } = await this.getTableValues({ + params: { + catId: tableId, + b_c: bookCode, + }, + }); + return views?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://timetonic.com/live/api.php"; + }, + _userId() { + return this.$auth.user_id; + }, + _makeRequest(opts = {}) { + const { + $ = this, req, params, ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + method: "POST", + url: this._baseUrl(), + params: { + ...params, + req, + o_u: this._userId(), + u_c: this._userId(), + sesskey: this.$auth.api_key, + b_o: this._userId(), + }, + }); + }, + listBooks(opts = {}) { + return this._makeRequest({ + req: "getAllBooks", + ...opts, + }); + }, + listTables(opts = {}) { + return this._makeRequest({ + req: "getBookTables", + ...opts, + }); + }, + listRows(opts = {}) { + return this._makeRequest({ + req: "listTableRowsById", + ...opts, + }); + }, + getTableValues(opts = {}) { + return this._makeRequest({ + req: "getTableValues", + ...opts, + }); + }, + createOrUpdateRow(opts = {}) { + return this._makeRequest({ + req: "createOrUpdateTableRow", + ...opts, + }); + }, + deleteRow(opts = {}) { + return this._makeRequest({ + req: "deleteTableRow", + ...opts, + }); + }, + uploadFile(opts = {}) { + return this._makeRequest({ + req: "fileUpload", + ...opts, + }); + }, + async *paginate({ + resourceFn, + params, + }) { + params = { + ...params, + maxRows: constants.DEFAULT_LIMIT, + offset: 0, + }; + let total; + do { + const { tableValues: { rows } } = await resourceFn({ + params, + }); + for (const row of rows) { + yield row; + } + total = rows?.length; + params.offset += params.maxRows; + } while (total === params.maxRows); + }, + }, +}; diff --git a/components/timeular/README.md b/components/timeular/README.md new file mode 100644 index 0000000000000..e66f43b76d855 --- /dev/null +++ b/components/timeular/README.md @@ -0,0 +1,11 @@ +# Overview + +The Timeular API lets you track, analyze, and manage your time directly, integrating your time tracking activities with other apps and automations. With Pipedream, you can harness the API to create custom workflows, such as triggering events based on time tracking data, syncing time entries with project management tools, or even automating invoicing processes based on tracked time. It's about connecting Timeular's detailed time tracking capabilities with other services to streamline productivity and reporting. + +# Example Use Cases + +- **Synchronize Time Tracking with Project Management:** Automatically create tasks in Asana or Trello when a new time entry is started in Timeular. This keeps your project management tools up to date with the tasks you're actively working on. + +- **Time Tracking Data to Spreadsheets:** Send detailed Timeular tracking data to Google Sheets or Excel for further analysis and reporting. This workflow can be scheduled to run at regular intervals, ensuring your spreadsheets always reflect the latest data. + +- **Automated Invoicing Based on Time Entries:** Connect Timeular with an invoicing app like QuickBooks to generate invoices based on the time you've tracked. When a time entry is stopped, calculate the billable amount and create an invoice draft, ready for review and send-off. diff --git a/components/timing/README.md b/components/timing/README.md new file mode 100644 index 0000000000000..ca021b113e352 --- /dev/null +++ b/components/timing/README.md @@ -0,0 +1,11 @@ +# Overview + +The Timing app provides detailed time tracking capabilities, enabling users to automatically log time spent on various tasks and improve productivity. Using Pipedream’s integration, one could build workflows that trigger when new time entries are created, sync time data with other project management tools, or compile reports for invoicing and accountability. It's all about harnessing the power of automated time tracking to streamline processes. + +# Example Use Cases + +- **Sync Time Entries with Project Management Tools:** Automatically sync time entries from Timing to a project management app like Trello or Asana. When a new time entry is logged in Timing, a Pipedream workflow can create a corresponding task or log the time in the other app, keeping project statuses up to date. + +- **Generate Weekly Time Reports:** Use Timing with Pipedream to generate weekly time reports. Set up a workflow to aggregate time entries for the week, calculate total hours, and then send the report via email or Slack. This can be a boon for freelancers and consultants who need to provide regular updates to clients. + +- **Invoice Creation and Delivery:** Connect Timing to an invoicing platform like QuickBooks or Stripe. When a project reaches a certain number of hours or the end of a billing period, the workflow can compile time entry data, create an invoice, and even send it out to the client automatically. diff --git a/components/timing/actions/create-time-entry/create-time-entry.mjs b/components/timing/actions/create-time-entry/create-time-entry.mjs new file mode 100644 index 0000000000000..1043e29c35f87 --- /dev/null +++ b/components/timing/actions/create-time-entry/create-time-entry.mjs @@ -0,0 +1,63 @@ +import timing from "../../timing.app.mjs"; + +export default { + key: "timing-create-time-entry", + name: "Create Time Entry", + description: "Generates a new time entry in Timing app. [See the documentation](https://web.timingapp.com/docs/#time-entries-POSTapi-v1-time-entries)", + version: "0.0.1", + type: "action", + props: { + timing, + project: { + propDefinition: [ + timing, + "project", + ], + }, + startDate: { + propDefinition: [ + timing, + "startDate", + ], + }, + endDate: { + propDefinition: [ + timing, + "endDate", + ], + }, + title: { + propDefinition: [ + timing, + "title", + ], + }, + notes: { + propDefinition: [ + timing, + "notes", + ], + }, + replaceExisting: { + propDefinition: [ + timing, + "replaceExisting", + ], + }, + }, + async run({ $ }) { + const { data } = await this.timing.createNewTimeEntry({ + $, + data: { + start_date: this.startDate, + end_date: this.endDate, + project: this.project, + title: this.title, + notes: this.notes, + replace_existing: this.replaceExisting, + }, + }); + $.export("$summary", `Time entry ${data.self} created successfully`); + return data; + }, +}; diff --git a/components/timing/actions/start-timer/start-timer.mjs b/components/timing/actions/start-timer/start-timer.mjs new file mode 100644 index 0000000000000..03e16b0fa5ff7 --- /dev/null +++ b/components/timing/actions/start-timer/start-timer.mjs @@ -0,0 +1,58 @@ +import timing from "../../timing.app.mjs"; + +export default { + key: "timing-start-timer", + name: "Start Timer", + description: "Starts a new ongoing timer as per the current timestamp or specified start date. [See the documentation](https://web.timingapp.com/docs/#time-entries-POSTapi-v1-time-entries-start)", + version: "0.0.1", + type: "action", + props: { + timing, + project: { + propDefinition: [ + timing, + "project", + ], + }, + startDate: { + propDefinition: [ + timing, + "startDate", + ], + description: "The date this timer should have started at. Defaults to \"now\". Example: `2019-01-01T00:00:00+00:00`", + optional: true, + }, + title: { + propDefinition: [ + timing, + "title", + ], + }, + notes: { + propDefinition: [ + timing, + "notes", + ], + }, + replaceExisting: { + propDefinition: [ + timing, + "replaceExisting", + ], + }, + }, + async run({ $ }) { + const response = await this.timing.startNewTimer({ + $, + data: { + project: this.project, + start_date: this.startDate, + title: this.title, + notes: this.notes, + replace_existing: this.replaceExisting, + }, + }); + $.export("$summary", "Successfully started new timer"); + return response; + }, +}; diff --git a/components/timing/actions/stop-timer/stop-timer.mjs b/components/timing/actions/stop-timer/stop-timer.mjs new file mode 100644 index 0000000000000..ca57fd71a55a9 --- /dev/null +++ b/components/timing/actions/stop-timer/stop-timer.mjs @@ -0,0 +1,19 @@ +import timing from "../../timing.app.mjs"; + +export default { + key: "timing-stop-timer", + name: "Stop Timer", + description: "Stop the currently running timer. [See the documentation](https://web.timingapp.com/docs/#time-entries-PUTapi-v1-time-entries-stop)", + version: "0.0.1", + type: "action", + props: { + timing, + }, + async run({ $ }) { + const response = await this.timing.stopActiveTimer({ + $, + }); + $.export("$summary", "Successfully stopped timer"); + return response; + }, +}; diff --git a/components/timing/package.json b/components/timing/package.json new file mode 100644 index 0000000000000..7bc6b169b8e38 --- /dev/null +++ b/components/timing/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/timing", + "version": "0.1.0", + "description": "Pipedream Timing Components", + "main": "timing.app.mjs", + "keywords": [ + "pipedream", + "timing" + ], + "homepage": "https://pipedream.com/apps/timing", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/timing/sources/common/base.mjs b/components/timing/sources/common/base.mjs new file mode 100644 index 0000000000000..a58cad9afee5d --- /dev/null +++ b/components/timing/sources/common/base.mjs @@ -0,0 +1,33 @@ +import timing from "../../timing.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + timing, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + async getPaginatedTimeEntries() { + const params = { + page: 1, + }; + let total; + const results = []; + do { + const { data } = await this.timing.listTimeEntries({ + params, + }); + results.push(...data); + total = data?.length; + params.page++; + } while (total > 0); + return results; + }, + }, +}; diff --git a/components/timing/sources/new-project/new-project.mjs b/components/timing/sources/new-project/new-project.mjs new file mode 100644 index 0000000000000..22b9118fe6981 --- /dev/null +++ b/components/timing/sources/new-project/new-project.mjs @@ -0,0 +1,30 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "timing-new-project", + name: "New Project Created", + description: "Emit new event each time a new project is created", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + generateMeta(project) { + return { + id: project.self, + summary: `New Project: ${project.title}`, + ts: Date.now(), + }; + }, + }, + async run() { + const { data } = await this.timing.listProjects(); + for (const project of data) { + const meta = this.generateMeta(project); + this.$emit(project, meta); + } + }, + sampleEmit, +}; diff --git a/components/timing/sources/new-project/test-event.mjs b/components/timing/sources/new-project/test-event.mjs new file mode 100644 index 0000000000000..a6d107cb4e238 --- /dev/null +++ b/components/timing/sources/new-project/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "self": "/projects/3675780806789030400", + "team_id": null, + "title": "Test project", + "title_chain": [ + "Test project" + ], + "color": "#D84DFF", + "productivity_score": 1, + "is_archived": false, + "notes": null, + "children": [], + "parent": null, + "custom_fields": {} +} \ No newline at end of file diff --git a/components/timing/sources/new-time-entry/new-time-entry.mjs b/components/timing/sources/new-time-entry/new-time-entry.mjs new file mode 100644 index 0000000000000..dd57c58dd2451 --- /dev/null +++ b/components/timing/sources/new-time-entry/new-time-entry.mjs @@ -0,0 +1,30 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "timing-new-time-entry", + name: "New Time Entry", + description: "Emit new event each time a new time entry is created in Timing", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + generateMeta(timeEntry) { + return { + id: timeEntry.self, + summary: `New Time Entry: ${timeEntry.title || timeEntry.self}`, + ts: Date.now(), + }; + }, + }, + async run() { + const timeEntries = await this.getPaginatedTimeEntries(); + for (const timeEntry of timeEntries) { + const meta = this.generateMeta(timeEntry); + this.$emit(timeEntry, meta); + } + }, + sampleEmit, +}; diff --git a/components/timing/sources/new-time-entry/test-event.mjs b/components/timing/sources/new-time-entry/test-event.mjs new file mode 100644 index 0000000000000..abd6493f3eadc --- /dev/null +++ b/components/timing/sources/new-time-entry/test-event.mjs @@ -0,0 +1,14 @@ +export default { + "self": "/time-entries/3677960545383524864", + "start_date": "2024-04-09T00:00:00.000000+00:00", + "end_date": "2024-04-09T01:00:00.000000+00:00", + "duration": 3600, + "project": { + "self": "/projects/3675780806789030400" + }, + "title": "test time entry", + "notes": "notes", + "is_running": false, + "creator_name": "test@test.com", + "custom_fields": {} +} \ No newline at end of file diff --git a/components/timing/sources/new-timer-started-running/new-timer-started-running.mjs b/components/timing/sources/new-timer-started-running/new-timer-started-running.mjs new file mode 100644 index 0000000000000..14686a5f149e0 --- /dev/null +++ b/components/timing/sources/new-timer-started-running/new-timer-started-running.mjs @@ -0,0 +1,43 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "timing-new-timer-started-running", + name: "New Timer Started Running", + description: "Emit new event each time a new timer is started", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + _getLastStartedTs() { + return this.db.get("lastStartedTs") || 0; + }, + _setLastStartedTs(lastStartedTs) { + this.db.set("lastStartedTs", lastStartedTs); + }, + generateMeta(timeEntry) { + const startTime = Date.parse(timeEntry.start_date); + return { + id: `${timeEntry.self}-${startTime}`, + summary: `Timer started: ${timeEntry.title || timeEntry.self}`, + ts: startTime, + }; + }, + }, + async run() { + const lastStartedTs = this._getLastStartedTs(); + let maxTs = lastStartedTs; + const timeEntries = await this.getPaginatedTimeEntries(); + const newlyStarted = timeEntries + .filter(({ start_date: startDate }) => Date.parse(startDate) >= lastStartedTs); + for (const timeEntry of newlyStarted) { + maxTs = Math.max(Date.parse(timeEntry.start_date), maxTs); + const meta = this.generateMeta(timeEntry); + this.$emit(timeEntry, meta); + } + this._setLastStartedTs(maxTs); + }, + sampleEmit, +}; diff --git a/components/timing/sources/new-timer-started-running/test-event.mjs b/components/timing/sources/new-timer-started-running/test-event.mjs new file mode 100644 index 0000000000000..abd6493f3eadc --- /dev/null +++ b/components/timing/sources/new-timer-started-running/test-event.mjs @@ -0,0 +1,14 @@ +export default { + "self": "/time-entries/3677960545383524864", + "start_date": "2024-04-09T00:00:00.000000+00:00", + "end_date": "2024-04-09T01:00:00.000000+00:00", + "duration": 3600, + "project": { + "self": "/projects/3675780806789030400" + }, + "title": "test time entry", + "notes": "notes", + "is_running": false, + "creator_name": "test@test.com", + "custom_fields": {} +} \ No newline at end of file diff --git a/components/timing/timing.app.mjs b/components/timing/timing.app.mjs new file mode 100644 index 0000000000000..f450e906e25db --- /dev/null +++ b/components/timing/timing.app.mjs @@ -0,0 +1,103 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "timing", + propDefinitions: { + project: { + type: "string", + label: "Project", + description: "The project reference of the project. Example: `/projects/1`", + async options() { + const { data } = await this.listProjects(); + return data?.map(({ + self: value, title: label, + }) => ({ + value, + label, + })) || []; + }, + }, + startDate: { + type: "string", + label: "Start Date", + description: "The time entry's start date and time. Example: `2019-01-01T00:00:00+00:00`", + }, + endDate: { + type: "string", + label: "End Date", + description: "The time entry's end date and time. Example: `2019-01-01T01:00:00+00:00`", + }, + title: { + type: "string", + label: "Title", + description: "The time entry's title", + optional: true, + }, + notes: { + type: "string", + label: "Notes", + description: "The time entry's notes", + optional: true, + }, + replaceExisting: { + type: "boolean", + label: "Replace Existing", + description: "If `true`, any existing time entries that overlap with the new time entry will be adjusted to avoid overlap, or deleted altogether. Defaults to `false`.", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://web.timingapp.com/api/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `Bearer ${this.$auth.api_key}`, + "Accept": "application/json", + }, + }); + }, + listProjects(opts = {}) { + return this._makeRequest({ + path: "/projects", + ...opts, + }); + }, + listTimeEntries(opts = {}) { + return this._makeRequest({ + path: "/time-entries", + ...opts, + }); + }, + startNewTimer(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/time-entries/start", + ...opts, + }); + }, + stopActiveTimer(opts = {}) { + return this._makeRequest({ + method: "PUT", + path: "/time-entries/stop", + ...opts, + }); + }, + createNewTimeEntry(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/time-entries", + ...opts, + }); + }, + }, +}; diff --git a/components/tinybird/README.md b/components/tinybird/README.md index d64aad91f81e3..dfd5395becd47 100644 --- a/components/tinybird/README.md +++ b/components/tinybird/README.md @@ -1,15 +1,11 @@ # Overview -With Tinybird's powerful API, you can build a suite of analytics applications -and insights. Here are a few examples of what you can build: - -- Real-time dashboards: Use Tinybird's API to quickly generate custom - dashboards that visualize data in real-time. -- Complex analytics pipelines: Generate complex analytics pipelines using - Tinybird's API, allowing you to process, analyze and visualize data. -- Data analytics applications: Create interactive applications for data - exploration and analytics using our API and tools. -- Reporting tools: Create custom reports with our API, making it easy to - visualize and monitor data. -- IoT sentiment analysis and visualization: Build sentiment analysis - applications for streaming data from the Internet of Things. +Tinybird is a real-time analytics API platform that allows developers to ingest, transform, and consume large amounts of data with low latency. By leveraging SQL and data streaming, Tinybird helps in building data-intensive applications or augmenting existing ones with real-time analytics features. On Pipedream, you can automate data ingestion, transformation, and delivery to unlock insights and drive actions in real time, transforming how you respond to user behavior and operational events. + +# Example Use Cases + +- **Real-time Dashboard Updates:** Trigger a workflow on Pipedream when new data is ingested into Tinybird, process and aggregate it, then push the results to a real-time dashboard service like GeckoBoard or Klipfolio. This keeps your dashboards updated with the latest insights without manual intervention. + +- **Automated Data Enrichment:** Use Tinybird to consume raw data, and when a new event is detected, trigger a Pipedream workflow to enrich that data with additional information from external services (like Clearbit for enriching user profiles) before inserting it back into Tinybird for enhanced analysis. + +- **Event-Driven Notifications:** Set up a Pipedream workflow that listens for specific data patterns or thresholds in Tinybird, such as a spike in user sign-ups or error rates, and automatically send out alerts via email, SMS (using Twilio), or messaging apps (like Slack) to prompt immediate action. diff --git a/components/tinybird/package.json b/components/tinybird/package.json new file mode 100644 index 0000000000000..539df35af84a5 --- /dev/null +++ b/components/tinybird/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/tinybird", + "version": "0.6.0", + "description": "Pipedream tinybird Components", + "main": "tinybird.app.mjs", + "keywords": [ + "pipedream", + "tinybird" + ], + "homepage": "https://pipedream.com/apps/tinybird", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/tinypng/README.md b/components/tinypng/README.md index 43da4bc01c117..a0248bf3f101d 100644 --- a/components/tinypng/README.md +++ b/components/tinypng/README.md @@ -1,16 +1,14 @@ # Overview -The TinyPNG API is a set of REST or JSON APIs that can be used to compress and -optimize images for websites. This can help speed up webpage loading, reduce -bandwidth costs, and create a better user experience for visitors. With the -API, developers can create automated image compression processes, compress -images in bulk, and integrate image compression directly into applications or -websites. Here are a few examples of what developers can build using the -TinyPNG API: +The TinyPNG API lets you compress and optimize image files efficiently. With this API, you can shrink the size of PNG and JPEG files without a noticeable loss in quality, making it a crucial tool for improving website load times and saving bandwidth. On Pipedream, you can automate image optimization workflows, harnessing the power of TinyPNG to process batches of images, integrate with CMS platforms, trigger optimizations from various events, and more. -- Image compression web tools -- Bulk image compression tools -- Image optimizers for websites -- Image optimization plugins for CMSs -- Automatic image compression services -- Image optimization apps for mobile devices +# Example Use Cases + +- **Automated Image Optimization for Web Deployment** + Whenever you push new images to your GitHub repository, set up a Pipedream workflow that listens for the `push` event, grabs the images, and sends them to the TinyPNG API for optimization. The compressed images can then be automatically committed back to the repository or deployed to your web server. + +- **Dynamic Compression for User-Uploaded Content** + For platforms that handle user uploads, a Pipedream workflow can trigger when a new image is uploaded to a cloud storage service like Amazon S3. The image is then sent to TinyPNG for compression. Afterward, the optimized image can be stored back in S3 or sent to other services like Dropbox or Google Drive, ready for distribution or archival. + +- **Scheduled Image Optimization for CMS Libraries** + If you manage a content-rich site with WordPress, automate the maintenance of your media library. Set up a Pipedream workflow that runs on a schedule, fetches images from your WordPress media library, compresses them with TinyPNG, and replaces the originals. This keeps your site speedy and reduces storage costs over time. diff --git a/components/tinypng/actions/compress-image/compress-image.mjs b/components/tinypng/actions/compress-image/compress-image.mjs new file mode 100644 index 0000000000000..009a6d6013b6d --- /dev/null +++ b/components/tinypng/actions/compress-image/compress-image.mjs @@ -0,0 +1,69 @@ +import { ConfigurationError } from "@pipedream/platform"; +import fs from "fs"; +import { IMAGE_TYPES } from "../../common/constants.mjs"; +import { checkTmp } from "../../common/utils.mjs"; +import tinypng from "../../tinypng.app.mjs"; + +export default { + key: "tinypng-compress-image", + name: "Compress Image", + description: "Compress a WebP, JPEG, or PNG image using the TinyPNG API. [See the documentation](https://tinypng.com/developers/reference#compressing-images)", + version: "0.0.1", + type: "action", + props: { + tinypng, + url: { + propDefinition: [ + tinypng, + "url", + ], + }, + file: { + propDefinition: [ + tinypng, + "file", + ], + }, + }, + async run({ $ }) { + if (!this.url && !this.file) { + throw new Error("You must provide either a URL or a file for compression."); + } + + let data; + let headers = {}; + + if (this.url) { + data = { + source: { + url: this.url, + }, + }; + } + + if (this.file) { + const fileData = this.file.split("."); + const ext = fileData[1]; + + if (!IMAGE_TYPES.includes(ext)) { + throw new ConfigurationError("You can upload either WebP, JPEG or PNG."); + } + const file = fs.readFileSync(checkTmp(this.file)); + data = file; + headers = { + "Content-Type": `image/${ext}`, + "content-disposition": "attachment; filename=" + fileData[0], + }; + + } + + const response = await this.tinypng.compressImage({ + $, + data, + headers, + }); + + $.export("$summary", "Image processed successfully."); + return response; + }, +}; diff --git a/components/tinypng/actions/convert-image/convert-image.mjs b/components/tinypng/actions/convert-image/convert-image.mjs new file mode 100644 index 0000000000000..6c34bc54b7524 --- /dev/null +++ b/components/tinypng/actions/convert-image/convert-image.mjs @@ -0,0 +1,65 @@ +import fs from "fs"; +import stream from "stream"; +import { promisify } from "util"; +import { TYPE_OPTIONS } from "../../common/constants.mjs"; +import tinypng from "../../tinypng.app.mjs"; + +export default { + key: "tinypng-convert-image", + name: "Convert Image", + description: "Convert your images to your desired image type using TinyPNG. [See the documentation](https://tinypng.com/developers/reference#converting-images)", + version: "0.0.1", + type: "action", + props: { + tinypng, + imageId: { + propDefinition: [ + tinypng, + "imageId", + ], + }, + type: { + type: "string", + label: "Type", + description: "Convert the image to another format ('jpeg', 'webp', or 'png').", + options: TYPE_OPTIONS, + }, + transformBackground: { + type: "string", + label: "Transform Background", + description: "The background color to use when converting images with transparency to a format that does not support transparency (like JPEG). Use a hex value (e.g., '#FFFFFF') or 'white'/'black'.", + optional: true, + }, + }, + async run({ $ }) { + const data = await this.tinypng.manipulateImage({ + $, + imageId: this.imageId, + data: { + convert: { + type: this.type, + }, + ...this.transformBackground + ? { + transform: { + background: this.transformBackground, + }, + } + : {}, + }, + responseType: "stream", + }); + + const ext = this.type.split("/").pop(); + + const filePath = `/tmp/converted-image-${Date.parse(new Date())}.${ext}`; + + const pipeline = promisify(stream.pipeline); + await pipeline(data, fs.createWriteStream(filePath)); + + $.export("$summary", `Successfully converted the image to ${this.type}`); + return { + filePath, + }; + }, +}; diff --git a/components/tinypng/actions/resize-image/resize-image.mjs b/components/tinypng/actions/resize-image/resize-image.mjs new file mode 100644 index 0000000000000..7db7977beefa5 --- /dev/null +++ b/components/tinypng/actions/resize-image/resize-image.mjs @@ -0,0 +1,78 @@ +import { ConfigurationError } from "@pipedream/platform"; +import fs from "fs"; +import stream from "stream"; +import { promisify } from "util"; +import tinypng from "../../tinypng.app.mjs"; + +export default { + key: "tinypng-resize-image", + name: "Resize Image", + description: "Create resized versions of your uploaded image. [See the documentation](https://tinypng.com/developers/reference#resizing-images)", + version: "0.0.1", + type: "action", + props: { + tinypng, + imageId: { + propDefinition: [ + tinypng, + "imageId", + ], + }, + method: { + propDefinition: [ + tinypng, + "method", + ], + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + props.width = { + type: "integer", + label: "Width", + description: "The width to resize the image to (in pixels).", + optional: this.method === "scale", + }; + props.height = { + type: "integer", + label: "Height", + description: "The height to resize the image to (in pixels).", + optional: this.method === "scale", + }; + return props; + }, + async run({ $ }) { + if ((this.method === "scale") && (!this.width && !this.height)) { + throw new ConfigurationError("You must provide either **Height** or **Width**"); + } + + const { + headers, data, + } = await this.tinypng.manipulateImage({ + $, + imageId: this.imageId, + data: { + resize: { + method: this.method, + width: this.width, + height: this.height, + }, + }, + responseType: "stream", + returnFullResponse: true, + }); + + const ext = headers["content-type"].split("/").pop(); + + const filePath = `/tmp/resized-image-${Date.parse(new Date())}.${ext}`; + + const pipeline = promisify(stream.pipeline); + await pipeline(data, fs.createWriteStream(filePath)); + + $.export("$summary", `Image ${filePath} was successfully resized.`); + return { + filePath, + }; + }, +}; diff --git a/components/tinypng/common/constants.mjs b/components/tinypng/common/constants.mjs new file mode 100644 index 0000000000000..980e1be3509f6 --- /dev/null +++ b/components/tinypng/common/constants.mjs @@ -0,0 +1,40 @@ +export const IMAGE_TYPES = [ + "jpg", + "jpeg", + "png", + "webp", +]; + +export const METHOD_TYPES = [ + { + label: "Scale", + value: "scale", + }, + { + label: "Fit", + value: "fit", + }, + { + label: "Cover", + value: "cover", + }, + { + label: "Thumb", + value: "thumb", + }, +]; + +export const TYPE_OPTIONS = [ + { + label: "jpeg", + value: "image/jpeg", + }, + { + label: "Webp", + value: "image/webp", + }, + { + label: "png", + value: "image/png", + }, +]; diff --git a/components/tinypng/common/utils.mjs b/components/tinypng/common/utils.mjs new file mode 100644 index 0000000000000..1a5e36f32a603 --- /dev/null +++ b/components/tinypng/common/utils.mjs @@ -0,0 +1,6 @@ +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; diff --git a/components/tinypng/package.json b/components/tinypng/package.json new file mode 100644 index 0000000000000..c7f412bdb6086 --- /dev/null +++ b/components/tinypng/package.json @@ -0,0 +1,21 @@ +{ + "name": "@pipedream/tinypng", + "version": "0.1.0", + "description": "Pipedream TinyPNG AI Components", + "main": "tinypng.app.mjs", + "keywords": [ + "pipedream", + "tinypng" + ], + "homepage": "https://pipedream.com/apps/tinypng", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.5.1", + "fs": "^0.0.1-security", + "stream": "^0.0.2", + "util": "^0.12.5" + } +} diff --git a/components/tinypng/tinypng.app.mjs b/components/tinypng/tinypng.app.mjs index 514361da21a97..d1c3d27d8687b 100644 --- a/components/tinypng/tinypng.app.mjs +++ b/components/tinypng/tinypng.app.mjs @@ -1,11 +1,70 @@ +import { axios } from "@pipedream/platform"; +import { METHOD_TYPES } from "./common/constants.mjs"; + export default { type: "app", app: "tinypng", - propDefinitions: {}, + propDefinitions: { + url: { + type: "string", + label: "Image URL", + description: "URL of the image to compress (WebP, JPEG, or PNG).", + optional: true, + }, + file: { + type: "string", + label: "Image File", + description: "The path to the image file (WebP, JPEG, or PNG) saved to the `/tmp` directory (e.g. `/tmp/example.jpg`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + optional: true, + }, + imageId: { + type: "string", + label: "Image Id", + description: "The Id of the generated URL to fetch the compressed image. **E.g. https://api.tinify.com/output/[IMAGE_ID]**", + }, + method: { + type: "string", + label: "Method", + description: "The method to use for resizing the image. [See the reference](https://tinypng.com/developers/reference#resizing-images)", + options: METHOD_TYPES, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.tinify.com"; + }, + _auth() { + return { + username: `api:${this.$auth.api_key}`, + password: "", + }; + }, + _makeRequest({ + $ = this, + path, + ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + auth: this._auth(), + }); + }, + compressImage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/shrink", + ...opts, + }); + }, + manipulateImage({ + imageId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/output/${imageId}`, + ...opts, + }); }, }, }; diff --git a/components/tinyurl/README.md b/components/tinyurl/README.md index b791f5fcd1f99..0e8930f81314e 100644 --- a/components/tinyurl/README.md +++ b/components/tinyurl/README.md @@ -1,22 +1,11 @@ # Overview -The TinyURL API allows users to generate and manage TinyURLs for their -websites, webpages, and other web-based resources. This API allows developers -to create TinyURLs from URLs as well as manage existing TinyURLs and access -analytics data on the clicks received by each TinyURL. It also allows users to -brand their own customized URLs using additional features like custom slugs, -password protection and expiration. +The TinyURL API lets you shorten URLs seamlessly, which can be particularly useful when dealing with lengthy or complex web addresses. With Pipedream, you can integrate the TinyURL API to create concise, manageable links that can be easily shared, tracked, or embedded in various digital content. Beyond simple URL shortening, using Pipedream's serverless platform enables you to automate workflows that involve generating, distributing, and monitoring TinyURLs in conjunction with other apps and services. -Using the TinyURL API, developers can: +# Example Use Cases -- Create TinyURLs from URLs -- Manage existing TinyURLs -- Access analytics data on the clicks received by each TinyURL -- Brand their own customized URLs -- Add custom slugs to URLs -- Password protect URLs -- Set an expiration date on TinyURLs -- Track and monitor visitor information -- Monitor social media mentions and referrals -- Generate QR codes for your TinyURLs -- And more! +- **Content Sharing Automation**: Automate the sharing of content by shortening URLs before posting them to social media platforms like Twitter or Facebook. When a new blog post is published or a product is launched, Pipedream can trigger a workflow that creates a TinyURL and then shares that link across your social networks, tracking clicks and engagement. + +- **Event Registration Management**: When organizing an event, use Pipedream to send out invitations with TinyURLs. Set up a workflow that integrates with Eventbrite or Google Sheets, creating a short link for the registration page. This link can be included in emails, tweets, or any other promotional material. Capture RSVP data and follow up with personalized content based on the attendee's engagement with the link. + +- **Customer Support Efficiency**: Streamline customer support by providing short and easy-to-type URLs in support communications. Construct a workflow where every new support article or help guide is automatically given a TinyURL, which can be embedded in automated support emails, chatbots like Intercom, or SMS messages, making it simpler for customers to reach the help they need. diff --git a/components/tinyurl/actions/create-shortened-link/create-shortened-link.mjs b/components/tinyurl/actions/create-shortened-link/create-shortened-link.mjs new file mode 100644 index 0000000000000..01d013847387f --- /dev/null +++ b/components/tinyurl/actions/create-shortened-link/create-shortened-link.mjs @@ -0,0 +1,74 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import tinyurl from "../../tinyurl.app.mjs"; + +export default { + key: "tinyurl-create-shortened-link", + name: "Create Shortened Link", + description: "Creates a new shortened link. [See the documentation]()", + version: "0.0.1", + type: "action", + props: { + tinyurl, + url: { + propDefinition: [ + tinyurl, + "url", + ], + }, + domain: { + propDefinition: [ + tinyurl, + "domain", + ], + optional: true, + }, + alias: { + propDefinition: [ + tinyurl, + "alias", + ], + optional: true, + }, + tags: { + propDefinition: [ + tinyurl, + "tags", + ], + optional: true, + }, + expiresAt: { + propDefinition: [ + tinyurl, + "expiresAt", + ], + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "The alias description", + optional: true, + }, + }, + async run({ $ }) { + try { + const response = await this.tinyurl.createTinyURL({ + $, + data: { + url: this.url, + domain: this.domain, + alias: this.alias, + tags: parseObject(this.tags)?.join(","), + expires_at: this.expiresAt, + description: this.description, + }, + }); + $.export("$summary", `Created TinyURL: ${response.data.tiny_url}`); + return response; + } catch ({ response }) { + throw new ConfigurationError(response?.data?.errors[0]); + } + }, +}; + diff --git a/components/tinyurl/actions/retrieve-link-analytics/retrieve-link-analytics.mjs b/components/tinyurl/actions/retrieve-link-analytics/retrieve-link-analytics.mjs new file mode 100644 index 0000000000000..dca5af02f93ca --- /dev/null +++ b/components/tinyurl/actions/retrieve-link-analytics/retrieve-link-analytics.mjs @@ -0,0 +1,67 @@ +import { ConfigurationError } from "@pipedream/platform"; +import tinyurl from "../../tinyurl.app.mjs"; + +export default { + key: "tinyurl-retrieve-link-analytics", + name: "Retrieve Link Analytics", + description: "Retrieves analytics for a specific TinyURL link, including total clicks, geographic breakdowns, and device types. [See the documentation]()", + version: "0.0.1", + type: "action", + props: { + tinyurl, + alert: { + type: "alert", + alertType: "info", + content: "This action is only allowed for paid accounts.", + }, + domain: { + propDefinition: [ + tinyurl, + "domain", + ], + }, + urls: { + propDefinition: [ + tinyurl, + "urls", + ({ domain }) => ({ + domain, + }), + ], + }, + from: { + type: "string", + label: "From", + description: "The start datetime of analitics report", + }, + to: { + type: "string", + label: "To", + description: "The end datetime of analitics report. Default is now", + optional: true, + }, + tag: { + type: "string", + label: "Tag", + description: "Tag to get analytics for", + optional: true, + }, + }, + async run({ $ }) { + try { + const analytics = await this.tinyurl.retrieveAnalytics({ + $, + params: { + from: this.from, + to: this.to, + alias: this.urls, + tag: this.tag, + }, + }); + $.export("$summary", `Retrieved analytics for link ${this.urls}`); + return analytics; + } catch ({ response }) { + throw new ConfigurationError(response?.data?.errors[0]); + } + }, +}; diff --git a/components/tinyurl/actions/update-link-metadata/update-link-metadata.mjs b/components/tinyurl/actions/update-link-metadata/update-link-metadata.mjs new file mode 100644 index 0000000000000..02a163c0506f5 --- /dev/null +++ b/components/tinyurl/actions/update-link-metadata/update-link-metadata.mjs @@ -0,0 +1,86 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import tinyurl from "../../tinyurl.app.mjs"; + +export default { + key: "tinyurl-update-link-metadata", + name: "Update Link Metadata", + description: "Updates the metadata of an existing TinyURL. [See the documentation]()", + version: "0.0.1", + type: "action", + props: { + tinyurl, + domain: { + propDefinition: [ + tinyurl, + "domain", + ], + }, + urls: { + propDefinition: [ + tinyurl, + "urls", + ({ domain }) => ({ + domain, + }), + ], + }, + newDomain: { + type: "string", + label: "New Domain", + description: "The new domain you would like to switch to", + optional: true, + }, + newAlias: { + type: "string", + label: "New Alias", + description: "The new alias you would like to switch to", + optional: true, + }, + newStats: { + type: "boolean", + label: "New Stats", + description: "Turns on/off the collection of click analytics", + optional: true, + }, + newTags: { + type: "string[]", + label: "New Tags", + description: "Tags you would like this TinyURL to have. Overwrites the existing tags. **Paid accounts only**", + optional: true, + }, + newExpiresAt: { + type: "string", + label: "New Expires At", + description: "TinyURL expiration in ISO8601 format. If not set so TinyURL never expires, **Paid accounts only**", + optional: true, + }, + newDescription: { + type: "string", + label: "New Description", + description: "The new description", + optional: true, + }, + }, + async run({ $ }) { + try { + const response = await this.tinyurl.updateTinyURLMetadata({ + $, + data: { + domain: this.domain, + alias: this.urls, + new_domain: this.newDomain, + new_alias: this.newAlias, + new_stats: this.newStats, + new_tags: parseObject(this.newTags), + new_expires_at: this.newExpiresAt, + new_description: this.newDescription, + }, + }); + $.export("$summary", `Updated TinyURL metadata for link: ${response.data.tiny_url}`); + return response; + } catch ({ response }) { + throw new ConfigurationError(response?.data?.errors[0]); + } + }, +}; diff --git a/components/tinyurl/common/utils.mjs b/components/tinyurl/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/tinyurl/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/tinyurl/package.json b/components/tinyurl/package.json new file mode 100644 index 0000000000000..690e2e46fb9ae --- /dev/null +++ b/components/tinyurl/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/tinyurl", + "version": "0.1.0", + "description": "Pipedream TinyURL Components", + "main": "tinyurl.app.mjs", + "keywords": [ + "pipedream", + "tinyurl" + ], + "homepage": "https://pipedream.com/apps/tinyurl", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/tinyurl/sources/new-link-created/new-link-created.mjs b/components/tinyurl/sources/new-link-created/new-link-created.mjs new file mode 100644 index 0000000000000..3b711c45ee90a --- /dev/null +++ b/components/tinyurl/sources/new-link-created/new-link-created.mjs @@ -0,0 +1,62 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import tinyurl from "../../tinyurl.app.mjs"; + +export default { + key: "tinyurl-new-link-created", + name: "New Shortened URL Created", + description: "Emit new event when a new shortened URL is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + tinyurl, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + + const { data } = await this.tinyurl.listURLs({ + type: "available", + params: { + from: lastDate, + }, + }); + + if (data.length) { + if (maxResults && (data.length > maxResults)) { + data.length = maxResults; + } + this._setLastDate(data[0].created_at); + } + + for (const item of data.reverse()) { + this.$emit(item, { + id: item.alias, + summary: `New TinyURL: ${item.tiny_url}`, + ts: Date.parse(item.created_at), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/tinyurl/tinyurl.app.mjs b/components/tinyurl/tinyurl.app.mjs index 1abdae8ba38be..4829e0381fae9 100644 --- a/components/tinyurl/tinyurl.app.mjs +++ b/components/tinyurl/tinyurl.app.mjs @@ -1,11 +1,100 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "tinyurl", - propDefinitions: {}, + propDefinitions: { + urls: { + type: "string", + label: "Alias", + description: "The existing alias for this TinyURL", + async options({ domain }) { + const { data } = await this.listURLs({ + type: "available", + }); + + return data + .filter((alias) => alias.domain === domain) + .map(({ + alias: value, tiny_url: label, + }) => ({ + label, + value, + })); + }, + }, + url: { + type: "string", + label: "URL", + description: "The long URL that will be shortened", + }, + domain: { + type: "string", + label: "Domain", + description: "The domain you would like the TinyURL to use. Select from TinyURL free domain tinyurl.com or subscribe and use TinyURL branded domains.", + default: "tinyurl.com", + }, + alias: { + type: "string", + label: "Custom Alias", + description: "A short string of characters to use in the TinyURL. If ommitted, one will be randomly generated. When using a branded domain, this has a minimum length of 1 character.", + optional: true, + }, + expiresAt: { + type: "string", + label: "Expires At", + description: "TinyURL expiration in ISO8601 format. If not set so TinyURL never expires. **Example: 2024-10-25 10:11:12**. **Paid accounts only**", + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "Tags group and categorize TinyURLs together. **Paid accounts only**", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.tinyurl.com"; + }, + _headers() { + return { + "Authorization": `Bearer ${this.$auth.api_token}`, + "Content-Type": "application/json", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + const config = { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }; + console.log("config: ", config); + return axios($, config); + }, + createTinyURL(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/create", + ...opts, + }); + }, + updateTinyURLMetadata(opts = {}) { + return this._makeRequest({ + method: "PATCH", + path: "/update", + ...opts, + }); + }, + listURLs({ + type, ...opts + }) { + return this._makeRequest({ + path: `/urls/${type}`, + opts, + }); }, }, }; diff --git a/components/tisane_labs/README.md b/components/tisane_labs/README.md new file mode 100644 index 0000000000000..1b0affb7862fa --- /dev/null +++ b/components/tisane_labs/README.md @@ -0,0 +1,11 @@ +# Overview + +The Tisane Labs API offers advanced text analysis capabilities, focusing on abusive content detection and linguistic insights. With it, you can automate content moderation, extract entities, detect the sentiment, and identify the language of the text. In Pipedream, Tisane Labs API can be integrated into workflows to process text from various sources such as user comments, support tickets, or social media posts. By leveraging Pipedream's serverless platform, you can create real-time, event-driven applications that respond to text analyses, connect with other services, and perform actions based on the insights gained from the Tisane Labs API. + +# Example Use Cases + +- **Content Moderation for Community Platforms**: Automate the moderation of user-generated content across forums or social media platforms. Tisane Labs API can analyze posts for abusive language, and Pipedream can take actions like flagging, removing content, or notifying moderators if certain thresholds are met. + +- **Customer Feedback Sentiment Analysis**: Connect Tisane Labs API to a customer support app like Zendesk in Pipedream. Analyze incoming support tickets for sentiment, and categorize them based on urgency and emotion. This can help prioritize responses and improve customer support services. + +- **Multilingual Chatbot Understanding**: Use Tisane Labs API within Pipedream to enhance the capabilities of a chatbot by detecting the language and analyzing the sentiment of the user's input. Integrate with a translation API to respond to users in their own language, improving user engagement and satisfaction. diff --git a/components/tldr/actions/summarize-text/summarize-text.mjs b/components/tldr/actions/summarize-text/summarize-text.mjs new file mode 100644 index 0000000000000..618b2ea3c80d2 --- /dev/null +++ b/components/tldr/actions/summarize-text/summarize-text.mjs @@ -0,0 +1,40 @@ +import tldr from "../../tldr.app.mjs"; + +export default { + key: "tldr-summarize-text", + name: "Summarize Text", + description: "Reads in a piece of text and distills the main points. [See the documentation](https://runtldr.com/documentation)", + version: "0.0.1", + type: "action", + props: { + tldr, + inputText: { + type: "string", + label: "Text to Summarize", + description: "The text that needs to be summarized.", + }, + responseStyle: { + type: "string", + label: "Response Style", + description: "Style of the response (e.g., Funny, Serious).", + }, + responseLength: { + type: "integer", + label: "Response Length", + description: "Length of the response summary.", + }, + }, + async run({ $ }) { + const response = await this.tldr.summarize({ + $, + data: { + inputText: this.inputText, + responseLength: this.responseLength, + responseStyle: this.responseStyle, + }, + }); + + $.export("$summary", `Successfully summarized the text with the following input: "${this.inputText}"`); + return response; + }, +}; diff --git a/components/tldr/package.json b/components/tldr/package.json new file mode 100644 index 0000000000000..a8353a1194f10 --- /dev/null +++ b/components/tldr/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/tldr", + "version": "0.1.0", + "description": "Pipedream TLDR Components", + "main": "tldr.app.mjs", + "keywords": [ + "pipedream", + "tldr" + ], + "homepage": "https://pipedream.com/apps/tldr", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/tldr/tldr.app.mjs b/components/tldr/tldr.app.mjs new file mode 100644 index 0000000000000..9cec238fcb7cf --- /dev/null +++ b/components/tldr/tldr.app.mjs @@ -0,0 +1,33 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "tldr", + methods: { + _headers() { + return { + "Authorization": `Bearer ${this.$auth.api_key}`, + "Content-Type": "application/json", + }; + }, + _baseUrl() { + return "https://runtldr.com/apis/v1"; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + summarize(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/summarize", + ...opts, + }); + }, + }, +}; diff --git a/components/tmetric/README.md b/components/tmetric/README.md index 52fd3a3b1f3fd..aa7d6f5713c18 100644 --- a/components/tmetric/README.md +++ b/components/tmetric/README.md @@ -1,25 +1,11 @@ # Overview -Using the TMetric API, developers can build a wide range of applications that -help track and manage work hours. The API allows developers to access rich -features and functionality such as task estimation, automated invoicing, -reporting and analysis, resource management, and one-click time tracking - All -of which can be used to help teams and organizations optimize their -performance. +The TMetric API provides the ability to interact programmatically with TMetric's time tracking features, allowing developers to create, read, update, and delete time entries, projects, and workspaces. By leveraging the API within Pipedream, you can connect TMetric to hundreds of other apps to automate time tracking as part of larger business processes, such as invoicing, project management, and reporting workflows. -Here are a few examples of what developers can create using the TMetric API: +# Example Use Cases -- Automate time tracking and billing: Gain insight into where resources are - being used and how efficiently they are working. Automate the entire billing - process - From time tracking and invoicing to expense tracking. -- Project tracking: Monitor and analyze project progress from start to finish. - Get a detailed overview of progress, deadlines, and other performance - metrics. -- Resource management: Manage available resources within the team. Track and - compare availability for specific time periods and identify opportunities for - optimization. -- Task estimation: Estimate the amount of time needed to complete tasks. - Calculate estimated billable hours for project-specific time tracking. -- Real-time reporting: Generate meaningful real-time reports from ongoing - projects. Keep up-to-date on progress and provide analytics and insights to - help inform decision-making. +- **Automated Time Tracking Reports**: Generate weekly or monthly time tracking reports in TMetric and automatically send them to a specified email address using the SendGrid app within Pipedream. This workflow can help keep teams and clients updated on project progress without manual intervention. + +- **Project Management Sync**: Synchronize TMetric projects and tasks with a project management tool like Trello or Asana. Whenever a new task is created in the project management app, a corresponding time entry can be automatically created in TMetric, ensuring seamless tracking of work across platforms. + +- **Invoice Creation and Delivery**: Connect TMetric to an invoicing app like QuickBooks or Stripe. When a project reaches a certain amount of logged hours, trigger the creation of an invoice based on the time tracked and send it to the client. This creates a streamlined process from work completion to payment request. diff --git a/components/todoist/README.md b/components/todoist/README.md index 7d0c41769a0c0..df43804a31c68 100644 --- a/components/todoist/README.md +++ b/components/todoist/README.md @@ -1,11 +1,11 @@ # Overview -With the Todoist API, you can build a variety of applications and tools to help -you manage your to-do lists and tasks. Here are just a few examples of what you -can build: - -- A to-do list application that allows you to manage your tasks and to-dos in - one place. -- A task management tool that helps you keep track of your tasks and to-dos. -- A tool that helps you prioritize your tasks and to-dos. -- A tool that helps you track your progress on your tasks and to-dos. +The Todoist API unlocks the potential to automate task management with precision. With Pipedream, you can craft workflows that react to events in Todoist, like task completions or due date changes, or drive actions in Todoist, such as creating tasks or updating projects. By integrating Todoist with Pipedream, you can seamlessly connect your to-do list with other apps to streamline your productivity, manage tasks based on triggers from other services, or compile reports on your task management patterns. + +# Example Use Cases + +- **Task Completion Notifications**: Send a notification through Slack or email when a high-priority task is completed in Todoist. This keeps your team in sync and immediately alerts members of critical task progress. + +- **Calendar Syncing**: Automatically create a Google Calendar event when a new task with a due date is added to Todoist. This ensures you never miss a deadline and integrates your task list with your personal or work calendar. + +- **GitHub Issue Tracking**: Whenever a new GitHub issue is reported, create a corresponding task in Todoist and assign it to the relevant team member. This bridges the gap between issue tracking and personal task management. diff --git a/components/todoist_custom_app/README.md b/components/todoist_custom_app/README.md index c54f2013fb9d1..abc950450cfbe 100644 --- a/components/todoist_custom_app/README.md +++ b/components/todoist_custom_app/README.md @@ -1,24 +1,11 @@ # Overview -Todoist's Custom Apps API allows developers to extend capabilities of the -Todoist platform by creating custom applications that integrate seamlessly with -the Todoist ecosystem. This allows developers to remove barriers to doing -tasks, automate processes, and make working with Todoist more efficient and -enjoyable. Here are some examples of what you can build with the Custom Apps -API: +The Todoist Custom App API on Pipedream enables you to automate your task management by connecting Todoist with a vast array of other applications and services. Capitalize on this to sync tasks across platforms, escalate priority items, or trigger reminders and actions based on activity within Todoist. From sending a Slack message when a task is due to adding tasks via email parsing, the possibilities are extensive for boosting productivity and ensuring nothing falls through the cracks. -- Automate task management by creating an app that moves tasks from one project - to the next with preset parameters. -- Create an app that automates task creation for multi-step processes. -- Create an app to better keep track of your team's progress on tasks. -- Automate the flow of data from third-party calendars and services into - Todoist. -- Create an app that lets you monitor your task completion metrics. -- Create an app that sends notifications from Todoist in different formats to - different services. -- Automate the entry of task descriptions with natural language processing. -- Create an app to automate reporting on task completion. -- Create an app to allow you to access and modify your Todoist tasks in other - applications. -- Create an app that allows you to export and import your Todoist data in - various formats. +# Example Use Cases + +- **Task Completion Notifications**: Automate sending a message to a Slack channel when a task is marked complete in Todoist. Keep your team updated in real-time without manual updates, enhancing team communication and project tracking. + +- **Email to Task Conversion**: Use an incoming email to trigger the creation of a Todoist task. Parse important details from the email using Pipedream's built-in code steps and populate them into a new Todoist task, ensuring that action items from emails are captured and tracked efficiently. + +- **Daily Task Digest**: Compile and send a daily digest of tasks due that day from Todoist to your preferred communication platform, such as Microsoft Teams. Use Pipedream's scheduled triggers to automate the process, providing a clear overview of the day's priorities every morning. diff --git a/components/todoist_custom_app/package.json b/components/todoist_custom_app/package.json new file mode 100644 index 0000000000000..65b48616633fc --- /dev/null +++ b/components/todoist_custom_app/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/todoist_custom_app", + "version": "0.6.0", + "description": "Pipedream todoist_custom_app Components", + "main": "todoist_custom_app.app.mjs", + "keywords": [ + "pipedream", + "todoist_custom_app" + ], + "homepage": "https://pipedream.com/apps/todoist_custom_app", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/toggl/README.md b/components/toggl/README.md index 7374a9a42397b..572c1f971f891 100644 --- a/components/toggl/README.md +++ b/components/toggl/README.md @@ -1,26 +1,11 @@ # Overview -The Toggl Track API allows developers to build apps that help measure and -visualize employee productivity. Using the API, it is possible to build tools -such as timesheet managers, project-based dashboards, expense analysis, team -management applications, and more. +Toggl Track is a time tracking API that lets you start, stop, and manage timers and time entries, as well as manage projects, clients, and tasks associated with time records. With the Toggl Track API on Pipedream, you can automate time tracking activities, synchronize data across platforms, and generate insights from time tracking data to improve productivity and project management. -The Toggl Track API also offers data overviews and integrated reporting, as -well as access to Toggl Track’s easy-to-use user interface. This facilitates -the creation of customized application features that can help businesses save -time and maximize productivity. +# Example Use Cases -Here are a few examples of what you can build using the Toggl Track API: +- **Project Time Tracking Automation:** Automatically start a Toggl timer when you begin working on a GitHub issue. When the issue is closed, stop the timer. This creates a seamless link between your coding activity and time tracking. -- Timesheet manager: Create an application that gives users an overview of - their time tracking data. -- Project-based dashboard: Visualize employee’s performance data on a - project-by-project basis. -- Expense analysis: Analyze employees' expense data so that businesses can make - more informed decisions. -- Team management applications: Manage employee workloads more efficiently with - customized tools. -- Customized reports: Build customized reports based on specific criteria - related to time tracking data. -- User interface: Access and integrate Toggl Track's intuitive user interface - into applications. +- **Client Reporting Workflow:** Generate weekly time reports for clients by aggregating Toggl data and sending it via Gmail. Use the Pipedream workflow to filter time entries by client and project, compile the data, and format it into a nicely structured email. + +- **Slack Productivity Bot:** Create a Slack bot that prompts team members to log their time if they haven't started a timer by midday. Use Toggl's API to check for active timers and send reminders through Slack, encouraging timely time entry submissions. diff --git a/components/toggl/actions/create-client/create-client.mjs b/components/toggl/actions/create-client/create-client.mjs new file mode 100644 index 0000000000000..d6c76cfb3caac --- /dev/null +++ b/components/toggl/actions/create-client/create-client.mjs @@ -0,0 +1,45 @@ +import toggl from "../../toggl.app.mjs"; + +export default { + key: "toggl-create-client", + name: "Create Client", + description: "Create a new client in Toggl. [See the documentation](https://engineering.toggl.com/docs/api/clients#post-create-client)", + version: "0.0.1", + type: "action", + props: { + toggl, + workspaceId: { + propDefinition: [ + toggl, + "workspaceId", + ], + }, + name: { + propDefinition: [ + toggl, + "clientName", + ], + }, + notes: { + propDefinition: [ + toggl, + "notes", + ], + }, + }, + async run({ $ }) { + const response = await this.toggl.createClient({ + $, + workspaceId: this.workspaceId, + data: { + name: this.name, + notes: this.notes, + wid: this.workspaceId, + }, + }); + if (response.id) { + $.export("$summary", `Successfully created client with ID: ${response.id}`); + } + return response; + }, +}; diff --git a/components/toggl/actions/create-project/create-project.mjs b/components/toggl/actions/create-project/create-project.mjs new file mode 100644 index 0000000000000..e5572300820be --- /dev/null +++ b/components/toggl/actions/create-project/create-project.mjs @@ -0,0 +1,86 @@ +import toggl from "../../toggl.app.mjs"; + +export default { + key: "toggl-create-project", + name: "Create Project", + description: "Create a new project in Toggl. [See the documentation](https://engineering.toggl.com/docs/api/projects#post-workspaceprojects)", + version: "0.0.1", + type: "action", + props: { + toggl, + workspaceId: { + propDefinition: [ + toggl, + "workspaceId", + ], + }, + name: { + propDefinition: [ + toggl, + "projectName", + ], + }, + startDate: { + propDefinition: [ + toggl, + "startDate", + ], + }, + endDate: { + propDefinition: [ + toggl, + "endDate", + ], + }, + active: { + type: "boolean", + label: "Active", + description: "Whether the project is active or archived. Defaults to `true`.", + optional: true, + default: true, + }, + isPrivate: { + type: "boolean", + label: "Is Private?", + description: "Whether the project is private or not. Defaults to `false`.", + optional: true, + default: false, + }, + isShared: { + type: "boolean", + label: "Is Shared?", + description: "Whether the project is shared or not. Defaults to `false`.", + optional: true, + default: false, + }, + clientId: { + propDefinition: [ + toggl, + "clientId", + (c) => ({ + workspaceId: c.workspaceId, + }), + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.toggl.createProject({ + $, + workspaceId: this.workspaceId, + data: { + name: this.name, + start_date: this.startDate, + end_date: this.endDate, + active: this.active, + is_private: this.isPrivate, + is_shared: this.isShared, + client_id: this.clientId, + }, + }); + if (response.id) { + $.export("$summary", `Successfully created project with ID: ${response.id}`); + } + return response; + }, +}; diff --git a/components/toggl/actions/get-current-time-entry/get-current-time-entry.mjs b/components/toggl/actions/get-current-time-entry/get-current-time-entry.mjs index 394ec7a850a68..a7ba3b99d6979 100644 --- a/components/toggl/actions/get-current-time-entry/get-current-time-entry.mjs +++ b/components/toggl/actions/get-current-time-entry/get-current-time-entry.mjs @@ -2,9 +2,9 @@ import toggl from "../../toggl.app.mjs"; export default { name: "Get Current Time Entry", - version: "0.0.3", + version: "0.0.6", key: "toggl-get-current-time-entry", - description: "Get the time entry that is running now. [See docs here](https://github.com/toggl/toggl_api_docs/blob/master/chapters/time_entries.md#get-running-time-entry)", + description: "Get the time entry that is running now. [See docs here]https://developers.track.toggl.com/docs/api/time_entries#get-get-current-time-entry)", type: "action", props: { toggl, diff --git a/components/toggl/actions/get-time-entries/get-time-entries.mjs b/components/toggl/actions/get-time-entries/get-time-entries.mjs index 98ea6e766184d..527e02a0426a6 100644 --- a/components/toggl/actions/get-time-entries/get-time-entries.mjs +++ b/components/toggl/actions/get-time-entries/get-time-entries.mjs @@ -2,9 +2,9 @@ import toggl from "../../toggl.app.mjs"; export default { name: "Get Time Entries", - version: "0.0.3", + version: "0.0.6", key: "toggl-get-time-entries", - description: "Get the last thousand time entries. [See docs here](https://github.com/toggl/toggl_api_docs/blob/master/chapters/time_entries.md#get-time-entries-started-in-a-specific-time-range)", + description: "Get the last thousand time entries. [See docs here](https://developers.track.toggl.com/docs/api/time_entries#get-timeentries)", type: "action", props: { toggl, diff --git a/components/toggl/actions/get-time-entry/get-time-entry.mjs b/components/toggl/actions/get-time-entry/get-time-entry.mjs index c71f0d71c957e..8878141ebff4a 100644 --- a/components/toggl/actions/get-time-entry/get-time-entry.mjs +++ b/components/toggl/actions/get-time-entry/get-time-entry.mjs @@ -2,9 +2,9 @@ import toggl from "../../toggl.app.mjs"; export default { name: "Get Time Entry", - version: "0.0.3", + version: "0.0.6", key: "toggl-get-time-entry", - description: "Get details about a specific time entry. [See docs here](https://github.com/toggl/toggl_api_docs/blob/master/chapters/time_entries.md#get-time-entry-details)", + description: "Get details about a specific time entry. [See docs here](https://developers.track.toggl.com/docs/api/time_entries)", type: "action", props: { toggl, @@ -21,8 +21,7 @@ export default { timeEntryId: this.timeEntryId, }); - response && $.export("$summary", "Successfully retrieved time entry"); - + $.export("$summary", "Successfully retrieved time entry"); return response; }, }; diff --git a/components/toggl/actions/update-client/update-client.mjs b/components/toggl/actions/update-client/update-client.mjs new file mode 100644 index 0000000000000..cd7cd446f9e86 --- /dev/null +++ b/components/toggl/actions/update-client/update-client.mjs @@ -0,0 +1,61 @@ +import toggl from "../../toggl.app.mjs"; + +export default { + key: "toggl-update-client", + name: "Update Client", + description: "Updates an existing client in Toggl. [See the documentation](https://engineering.toggl.com/docs/api/clients#put-change-client)", + version: "0.0.1", + type: "action", + props: { + toggl, + workspaceId: { + propDefinition: [ + toggl, + "workspaceId", + ], + }, + clientId: { + propDefinition: [ + toggl, + "clientId", + (c) => ({ + workspaceId: c.workspaceId, + }), + ], + }, + name: { + propDefinition: [ + toggl, + "clientName", + ], + optional: true, + }, + notes: { + propDefinition: [ + toggl, + "notes", + ], + }, + }, + async run({ $ }) { + const client = await this.toggl.getClient({ + $, + workspaceId: this.workspaceId, + clientId: this.clientId, + }); + const response = await this.toggl.updateClient({ + $, + workspaceId: this.workspaceId, + clientId: this.clientId, + data: { + name: this.name || client.name, + notes: this.notes || client.notes, + wid: this.workspaceId, + }, + }); + if (response.id) { + $.export("$summary", `Successfully updated client with ID: ${response.id}`); + } + return response; + }, +}; diff --git a/components/toggl/actions/update-project/update-project.mjs b/components/toggl/actions/update-project/update-project.mjs new file mode 100644 index 0000000000000..475aca3c66885 --- /dev/null +++ b/components/toggl/actions/update-project/update-project.mjs @@ -0,0 +1,101 @@ +import toggl from "../../toggl.app.mjs"; + +export default { + key: "toggl-update-project", + name: "Update Project", + description: "Updates an existing project in Toggl. [See the documentation](https://engineering.toggl.com/docs/api/projects#put-workspaceproject)", + version: "0.0.1", + type: "action", + props: { + toggl, + workspaceId: { + propDefinition: [ + toggl, + "workspaceId", + ], + }, + projectId: { + propDefinition: [ + toggl, + "projectId", + (c) => ({ + workspaceId: c.workspaceId, + }), + ], + }, + name: { + propDefinition: [ + toggl, + "projectName", + ], + optional: true, + }, + startDate: { + propDefinition: [ + toggl, + "startDate", + ], + optional: true, + }, + endDate: { + propDefinition: [ + toggl, + "endDate", + ], + optional: true, + }, + active: { + type: "boolean", + label: "Active", + description: "Whether the project is active or archived.", + optional: true, + }, + isPrivate: { + type: "boolean", + label: "Is Private?", + description: "Whether the project is private or not.", + optional: true, + }, + isShared: { + type: "boolean", + label: "Is Shared?", + description: "Whether the project is shared or not.", + optional: true, + }, + clientId: { + propDefinition: [ + toggl, + "clientId", + (c) => ({ + workspaceId: c.workspaceId, + }), + ], + optional: true, + }, + }, + async run({ $ }) { + const project = await this.toggl.getProject({ + $, + workspaceId: this.workspaceId, + projectId: this.projectId, + }); + const response = await this.toggl.updateProject({ + $, + workspaceId: this.workspaceId, + projectId: this.projectId, + data: { + name: this.name || project.name, + start_date: this.startDate || project.start_date, + end_date: this.endDate || project.end_date, + active: this.active || project.active, + is_private: this.isPrivate || project.isPrivate, + is_shared: this.isShared || project.is_shared, + client_id: this.clientId || project.client_id, + }, + }); + if (response.id) { + $.export("$summary", `Successfully updated project with ID: ${response.id}`); + } + return response; + }, +}; diff --git a/components/toggl/package.json b/components/toggl/package.json index 814a4cd882619..b4ed8cc78bdc8 100644 --- a/components/toggl/package.json +++ b/components/toggl/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/toggl", - "version": "0.0.4", + "version": "0.1.0", "description": "Pipedream Toggl Components", "main": "toggl.app.js", "keywords": [ @@ -14,7 +14,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^0.10.0", + "@pipedream/platform": "^3.0.0", "toggl-api": "^1.0.2" } } diff --git a/components/toggl/sources/new-start-time-entry/new-start-time-entry.mjs b/components/toggl/sources/new-start-time-entry/new-start-time-entry.mjs index c111c4acfa11c..f165384cf166c 100644 --- a/components/toggl/sources/new-start-time-entry/new-start-time-entry.mjs +++ b/components/toggl/sources/new-start-time-entry/new-start-time-entry.mjs @@ -3,7 +3,7 @@ import base from "../common/base.mjs"; export default { ...base, name: "New Start Time Entry (Instant)", - version: "0.0.2", + version: "0.0.4", key: "toggl-new-start-time-entry", description: "Emit new event when a time entry is started. [See docs here](https://github.com/toggl/toggl_api_docs/blob/master/webhooks.md)", type: "source", diff --git a/components/toggl/sources/new-time-entry/new-time-entry.mjs b/components/toggl/sources/new-time-entry/new-time-entry.mjs index 919bfe395effa..f331e59cb712b 100644 --- a/components/toggl/sources/new-time-entry/new-time-entry.mjs +++ b/components/toggl/sources/new-time-entry/new-time-entry.mjs @@ -3,7 +3,7 @@ import base from "../common/base.mjs"; export default { ...base, name: "New Time Entry (Instant)", - version: "0.0.2", + version: "0.0.4", key: "toggl-new-time-entry", description: "Emit new event when a time entry is created. [See docs here](https://github.com/toggl/toggl_api_docs/blob/master/webhooks.md)", type: "source", diff --git a/components/toggl/sources/new-update-time-entry/new-update-time-entry.mjs b/components/toggl/sources/new-update-time-entry/new-update-time-entry.mjs index 964d43410c33d..2ecab32fe3250 100644 --- a/components/toggl/sources/new-update-time-entry/new-update-time-entry.mjs +++ b/components/toggl/sources/new-update-time-entry/new-update-time-entry.mjs @@ -3,7 +3,7 @@ import base from "../common/base.mjs"; export default { ...base, name: "New Update Time Entry (Instant)", - version: "0.0.2", + version: "0.0.4", key: "toggl-new-update-time-entry", description: "Emit new event when a time entry is updated. [See docs here](https://github.com/toggl/toggl_api_docs/blob/master/webhooks.md)", type: "source", diff --git a/components/toggl/sources/new-webhook-event/new-webhook-event.mjs b/components/toggl/sources/new-webhook-event/new-webhook-event.mjs index 96ec70ab16f56..ab086ad3e2fc4 100644 --- a/components/toggl/sources/new-webhook-event/new-webhook-event.mjs +++ b/components/toggl/sources/new-webhook-event/new-webhook-event.mjs @@ -4,7 +4,7 @@ import constants from "../common/constants.mjs"; export default { ...base, name: "New Webhook Event (Instant)", - version: "0.0.2", + version: "0.0.4", key: "toggl-new-webhook-event", description: "Emit new event on receive a webhook event. [See docs here](https://github.com/toggl/toggl_api_docs/blob/master/webhooks.md)", type: "source", diff --git a/components/toggl/toggl.app.mjs b/components/toggl/toggl.app.mjs index e76c7fe2bcc14..1becfff3707d3 100644 --- a/components/toggl/toggl.app.mjs +++ b/components/toggl/toggl.app.mjs @@ -39,6 +39,62 @@ export default { })); }, }, + clientId: { + label: "Client ID", + description: "The client ID", + type: "integer", + async options({ workspaceId }) { + const clients = await this.getClients({ + workspaceId, + }); + + return clients?.map((client) => ({ + label: client.name, + value: client.id, + })) || []; + }, + }, + projectId: { + label: "Project ID", + description: "The project ID", + type: "integer", + async options({ workspaceId }) { + const projects = await this.getProjects({ + workspaceId, + }); + + return projects?.map((project) => ({ + label: project.name, + value: project.id, + })) || []; + }, + }, + clientName: { + type: "string", + label: "Name", + description: "Name of the client", + }, + notes: { + type: "string", + label: "Notes", + description: "Notes about the client", + optional: true, + }, + projectName: { + type: "string", + label: "Name", + description: "Name of the project", + }, + startDate: { + type: "string", + label: "Start Date", + description: "The start date of the project in `YYYY-MM-DD` format", + }, + endDate: { + type: "string", + label: "End Date", + description: "The end date of the project in `YYYY-MM-DD` format", + }, }, methods: { _apiToken() { @@ -47,7 +103,7 @@ export default { _apiUrl(apiVersion) { return constants.API_BASE_URL_VERSIONS[apiVersion]; }, - async _makeRequest(apiVersion, path, options = {}, $ = this) { + _makeRequest(apiVersion, path, options = {}, $ = this) { return axios($, { url: `${this._apiUrl(apiVersion)}/${path}`, auth: { @@ -57,7 +113,7 @@ export default { ...options, }); }, - async createWebhook({ + createWebhook({ workspaceId, data, }) { return this._makeRequest("v1", `subscriptions/${workspaceId}`, { @@ -69,22 +125,30 @@ export default { }, }); }, - async removeWebhook({ + removeWebhook({ workspaceId, webhookId, }) { return this._makeRequest("v1", `subscriptions/${workspaceId}/${webhookId}`, { method: "delete", }); }, - async getWorkspaces({ + getWorkspaces({ $ }) { + return this._makeRequest("v9", "me/workspaces", {}, $); + }, + getClients({ + workspaceId, $, + }) { + return this._makeRequest("v9", `workspaces/${workspaceId}/clients`, {}, $); + }, + getProjects({ workspaceId, $, }) { - return this._makeRequest("v9", `workspaces/${workspaceId}`, {}, $); + return this._makeRequest("v9", `workspaces/${workspaceId}/projects`, {}, $); }, - async getCurrentTimeEntry({ $ } = {}) { + getCurrentTimeEntry({ $ } = {}) { return this._makeRequest("v9", "me/time_entries/current", {}, $); }, - async getTimeEntries({ + getTimeEntries({ params, $, } = {}) { return this._makeRequest("v9", "me/time_entries", { @@ -94,12 +158,52 @@ export default { }, }, $); }, - async getTimeEntry({ + getTimeEntry({ timeEntryId, $, } = {}) { - const { data } = await this._makeRequest("v8", `time_entries/${timeEntryId}`, {}, $); - - return data; + return this._makeRequest("v9", `me/time_entries/${timeEntryId}`, {}, $); + }, + getClient({ + workspaceId, clientId, $, + } = {}) { + return this._makeRequest("v9", `workspaces/${workspaceId}/clients/${clientId}`, {}, $); + }, + getProject({ + workspaceId, projectId, $, + } = {}) { + return this._makeRequest("v9", `workspaces/${workspaceId}/projects/${projectId}`, {}, $); + }, + createClient({ + workspaceId, data, $, + }) { + return this._makeRequest("v9", `workspaces/${workspaceId}/clients`, { + method: "post", + data, + }, $); + }, + createProject({ + workspaceId, data, $, + }) { + return this._makeRequest("v9", `workspaces/${workspaceId}/projects`, { + method: "post", + data, + }, $); + }, + updateClient({ + workspaceId, clientId, data, $, + }) { + return this._makeRequest("v9", `workspaces/${workspaceId}/clients/${clientId}`, { + method: "put", + data, + }, $); + }, + updateProject({ + workspaceId, projectId, data, $, + }) { + return this._makeRequest("v9", `workspaces/${workspaceId}/projects/${projectId}`, { + method: "put", + data, + }, $); }, }, }; diff --git a/components/token_metrics/README.md b/components/token_metrics/README.md new file mode 100644 index 0000000000000..9abd1ec9eee07 --- /dev/null +++ b/components/token_metrics/README.md @@ -0,0 +1,11 @@ +# Overview + +The Token Metrics API offers access to a trove of cryptocurrency data, including analytics, rankings, and predictions that leverage artificial intelligence and expert insights. With this API, you can automate investment strategies, integrate up-to-date crypto data into your applications, and stay informed with the latest market trends. When used on Pipedream, it allows you to build robust, serverless workflows that can react to various triggers and integrate with numerous services for a seamless data handling experience. + +# Example Use Cases + +- **Crypto Alert System**: Build a workflow on Pipedream that monitors the Token Metrics API for significant changes in crypto ratings or predictions. When certain thresholds are met, use the integrated Twilio app to send SMS alerts, ensuring you or your audience stay up-to-date on important market movements. + +- **Investment Tracker**: Create a Pipedream workflow that pulls daily investment insights and portfolio analytics from the Token Metrics API and logs them to a Google Sheets document. This allows for easy tracking and historical data analysis, helping to refine investment strategies over time. + +- **Market Dashboard Sync**: Use Pipedream to set up a workflow where market data and analytics from the Token Metrics API are fetched periodically and pushed to a real-time dashboard built with Geckoboard. This keeps your team or clients informed with the latest crypto market trends and forecasts. diff --git a/components/tolstoy/README.md b/components/tolstoy/README.md index d48afbd9e3996..fe4525f677e25 100644 --- a/components/tolstoy/README.md +++ b/components/tolstoy/README.md @@ -1,28 +1,11 @@ # Overview -Are you looking for a way to harness the power of the world’s most famous -authors? Look no further! The Tolstoy API provides an array of resources to -help you construct impressive applications and programs. +The Tolstoy API offers a suite of tools for engaging with customers through interactive video experiences. With it, you can automate the creation, analysis, and management of interactive video content, streamlining how you engage with your audience. By leveraging the Tolstoy API on Pipedream, you can create powerful automations that react to video interactions, manage leads, and trigger personalized follow-ups based on viewer behavior. This supports a more dynamic and responsive marketing strategy, improves lead qualification processes, and enhances customer support by providing interactive video experiences tailored to user actions and feedback. -From writing toys for children to hardcore software engineering applications, -the Tolstoy API provides users with access to a massive assortment of -literature by the world’s best and most respected authors, such as Fyodor -Dostoyevsky, Leo Tolstoy, Anton Chekhov, and many more. +# Example Use Cases -Want to get started with the Tolstoy API and start building projects? Here are -a few examples of what you can create: +- **Automated Lead Qualification and Follow-Up**: When a prospect completes a Tolstoy interactive video, Pipedream can capture their responses, qualify the lead based on pre-defined criteria, and automatically send personalized follow-up content via email through integration with an email marketing service such as Mailchimp. -- A recommendation engine that uses AI to suggest classic books from Russian - authors. -- A comprehensive search platform for books and study materials for college - students. -- A family-friendly game based on Russian literature. -- A guided tour of Russian literature and literature-related museums for - tourists. -- A web application to explore the history and popularity of Russian - literature. -- An online encyclopedia for highlighting the works of Russian authors. -- A text-based adventure game that places players in scenarios from Russian - novels. -- An educational platform for children to learn and explore the works of - Russian writers. +- **Dynamic Video Content Distribution**: Create a workflow that monitors customer behavior on your platform. Based on specific actions, such as visiting a product page multiple times, Pipedream triggers a Tolstoy API call to send a personalized interactive video to the customer, providing them with additional product information or a special offer. + +- **Real-Time Support Ticket Generation**: Implement a system where customer responses from a support-related Tolstoy video are sent to Pipedream, which then creates a support ticket in Zendesk or another customer service platform. This can include tagging the ticket based on the issues highlighted in the video interaction, ensuring the right support team member addresses the concern rapidly. diff --git a/components/tomba/README.md b/components/tomba/README.md index 32b4084812314..84416691d850c 100644 --- a/components/tomba/README.md +++ b/components/tomba/README.md @@ -1,23 +1,11 @@ # Overview -With Tomba, you can build high-performance, secure, and intuitive web, mobile, -and desktop applications. Using the [Tomba API](https://tomba.io/), developers -have access to advanced functionalities and features they can use to create -powerful applications quickly and efficiently. +The Tomba API is a potent tool for email discovery and domain search. With it, you can automate the process of finding and verifying email addresses linked to a domain, which could be pivotal for lead generation, outreach campaigns, or market research. By leveraging the Tomba API on Pipedream, you streamline these tasks into efficient workflows that can interact with numerous other apps and services, such as CRMs, marketing automation tools, or database managers. The synergy between Tomba and Pipedream allows for real-time processing and integration of email data within your existing business systems. -Tomba is an all-in-one platform that provides a unified API, allowing -developers to create reliable, secure and scalable applications. The Tomba API -enables developers to develop and deploy advanced and intuitive applications -with minimal effort. +# Example Use Cases -Here are some of the applications you can build using the Tomba API: +- **Lead Generation Automation**: Trigger a workflow when a new company is added to your CRM. Use Tomba to find email addresses associated with the company's domain. Enrich lead data in the CRM and follow up with an automated outreach sequence using an email marketing tool like Mailchimp. -- Low-Latency Web Applications -- Mobile Applications -- Real-Time Browser-based Applications -- Robust Desktop Applications -- Secure Cloud and Network Applications -- Intelligent IoT Applications -- Location-Aware Applications -- eCommerce Platforms -- Secure Financial Applications +- **Domain Research and Monitoring**: Schedule a regular Pipedream workflow that checks a list of domains and uses Tomba to gather any new email addresses. Store the findings in a Google Sheets document and send a Slack notification to the sales team with any updates or new leads. + +- **Verification and Clean-up for Marketing Campaigns**: Before launching an email campaign, run your email list through a Pipedream workflow that uses Tomba to verify the validity of each address. Update the campaign list in your email platform, like Sendgrid, to omit invalid or nonexistent emails, ensuring higher deliverability and engagement rates. diff --git a/components/tomtom/package.json b/components/tomtom/package.json new file mode 100644 index 0000000000000..ef4b2db67d596 --- /dev/null +++ b/components/tomtom/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/tomtom", + "version": "0.0.1", + "description": "Pipedream TomTom Components", + "main": "tomtom.app.mjs", + "keywords": [ + "pipedream", + "tomtom" + ], + "homepage": "https://pipedream.com/apps/tomtom", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/tomtom/tomtom.app.mjs b/components/tomtom/tomtom.app.mjs new file mode 100644 index 0000000000000..627cff85a8ccd --- /dev/null +++ b/components/tomtom/tomtom.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "tomtom", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/toneden/README.md b/components/toneden/README.md new file mode 100644 index 0000000000000..ab0413704c1d6 --- /dev/null +++ b/components/toneden/README.md @@ -0,0 +1,11 @@ +# Overview + +The ToneDen API offers a suite of music marketing tools that enables you to automate interactions with social media platforms, music services, and advertising campaigns. With Pipedream, you can stitch together these capabilities to create powerful automated workflows. This might include posting to social media, managing your advertising efforts, or analyzing your audience across various platforms. + +# Example Use Cases + +- **Automated Social Media Engagement**: Trigger a Pipedream workflow whenever your music or brand is mentioned on social media platforms. Use the ToneDen API to respond automatically or aggregate mentions for analysis. + +- **Advertising Campaign Management**: Set up a workflow to monitor your ToneDen advertising campaigns' performance. Trigger alerts or actions based on real-time data such as impressions, click-through rates, or budget thresholds. + +- **Audience Analysis Reports**: Schedule a daily or weekly Pipedream workflow to fetch audience insights from ToneDen. Compile these into a report and send it to your email or Slack to keep track of your audience growth and engagement trends. diff --git a/components/toneden/package.json b/components/toneden/package.json index 0564b8477867f..73f195444b575 100644 --- a/components/toneden/package.json +++ b/components/toneden/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/tookan/README.md b/components/tookan/README.md index 656d498548977..ad7247e3fd745 100644 --- a/components/tookan/README.md +++ b/components/tookan/README.md @@ -1,24 +1,11 @@ # Overview -Tookan is an API that enables developers to create powerful applications that -enable businesses to automate their operations and enable customers to have -greater control over their orders. The Tookan API provides a range of features, -including task scheduling, order tracking and monitoring, customer -notifications and more, that allow businesses to better manage their workflow -and customer service. Here is a list of some of the things you can build using -the Tookan API: +The Tookan API allows for the automation and streamlining of delivery and field services. By integrating with Pipedream, users can trigger workflows based on events within Tookan, such as new task creation, task status updates, or agent alerts. This enables businesses to connect Tookan with other apps and services, automating notifications, data synchronization, and logistics operations, leading to improved coordination, customer satisfaction, and operational efficiency. -- Delivery Management Applications – Automate the scheduling, tracking, and - completion of delivery services. -- Mobile Apps – Create task-tracking applications that send notifications to - customers and employees. -- Logistics Management Software – Create software that allows businesses to - track their shipment, delivery, and customer information. -- Maintenance Service Applications – Create applications to manage the - scheduling and tracking of maintenance services. -- Geolocation Services – Create location-based services to allow customers to - track their deliveries and deliveries to be tracked in real-time. -- Fleet Management Systems – Create applications that enable businesses to - manage and monitor their fleet operations. -- Task Management Systems – Create applications that manage tasks and track - customer communications. +# Example Use Cases + +- **Task Completion to Customer SMS Notification**: When a delivery task is marked as completed in Tookan, trigger a workflow on Pipedream that sends an SMS to the customer via Twilio, informing them of the delivery completion. + +- **New Tookan Task to Google Calendar Event**: For every new task created in Tookan, use Pipedream to create a corresponding event in Google Calendar, helping to keep track of deliveries or service appointments in a shared calendar. + +- **Tookan Agent GPS Update to Slack Notification**: Each time an agent's GPS location is updated in Tookan, trigger a Pipedream workflow that sends a real-time notification to a Slack channel, providing live updates on agent location for improved team awareness and coordination. diff --git a/components/totango/README.md b/components/totango/README.md index 704fb595e1349..044703b7bb783 100644 --- a/components/totango/README.md +++ b/components/totango/README.md @@ -1,29 +1,11 @@ # Overview -Totango is a powerful Customer Success platform that enables businesses to -create amazing customer experiences. The Totango API is a powerful way to -manage customers, automate processes, and gain insights about customer -behavior. With the Totango API, you can build applications and integrations -that do the following: +The Totango API taps into the realm of customer success, offering intricate data on customer interactions and health scores. This API is a gateway for syncing customer data, tracking events, and constructing a responsive and personalized customer journey. Utilize Pipedream's capabilities to connect Totango to a myriad of other apps, creating automated workflows that enhance customer insights, trigger actions based on customer status, and streamline communication across platforms. -- Retrieve customer data such as activity, usage, and attributes -- Create segments of customers based on activity, usage, and attributes -- Automate processes such as notifications, actions, and CRM sync -- Pull customer insights such as customer health scores and customer analytics -- Integrate with other systems such as Salesforce, Zendesk, and Slack +# Example Use Cases -Using the Totango API, you can build applications and services that enable -businesses to gain a deeper understanding of their customers and provide them -with better customer experiences. Here are some examples of what you can build -using the Totango API: +- **Customer Health Score Alerts**: Send real-time notifications to Slack when a customer's health score drops below a certain threshold in Totango. This immediate alert enables swift action, such as a customer success manager reaching out to address potential issues. -- Usage and billing analytics applications that track customer usage and - provide insights into customer payment history -- Automated notifications and follow up services that keep customers informed - about their orders and support inquiries -- Internal applications that connect customer data across different systems for - better analysis -- Customer health companions that track customer health metrics and recommend - personalized plans for boosting customer engagement -- Third-party integrations that connect to customer-facing components such as - mobile apps and websites +- **Automated Onboarding Emails**: When a new customer is added to Totango, trigger an automated email sequence in SendGrid. This sequence can include welcome emails, onboarding resources, and check-in messages, ensuring consistent engagement from the start. + +- **Support Ticket Integration**: Integrate Totango with Zendesk to automatically create support tickets when specific customer milestones are achieved or issues are detected. This ensures that support teams are proactively involved in the customer's journey, leading to better service and retention. diff --git a/components/tpscheck/README.md b/components/tpscheck/README.md new file mode 100644 index 0000000000000..d275fe115d954 --- /dev/null +++ b/components/tpscheck/README.md @@ -0,0 +1,11 @@ +# Overview + +The TPSCheck API provides access to the UK's Telephone Preference Service (TPS) and Corporate Telephone Preference Service (CTPS) databases, enabling businesses to check if a telephone number is registered. This helps in compliance with regulations by avoiding unwanted sales calls to these numbers. In Pipedream, you can use this API to automate the process of verifying numbers directly within your workflows, combining it with various triggers, actions from other apps, and custom logic for streamlined operations. + +# Example Use Cases + +- **Compliance Verification Before Calls**: Automate the process of checking phone numbers against the TPS/CTPS lists before initiating sales calls. If a number is registered, you can automatically reroute it to an exclusion list or log it for review. + +- **CRM Integration for Contact Updates**: Cross-reference your CRM contacts with the TPSCheck API. If a contact is on the TPS/CTPS list, update their record in your CRM tool, like HubSpot, to ensure no sales calls are made to that contact. + +- **Batch Processing of Marketing Lists**: Schedule a daily or weekly job that takes a list of phone numbers from a Google Sheet, checks each number with TPSCheck, and appends the verification results back to the sheet for a clean, compliant call list. diff --git a/components/trackingtime/README.md b/components/trackingtime/README.md new file mode 100644 index 0000000000000..9fb29a9a79c36 --- /dev/null +++ b/components/trackingtime/README.md @@ -0,0 +1,11 @@ +# Overview + +The TrackingTime API enables the automation of time tracking, task management, and reporting processes. In Pipedream, you can use this API to create powerful workflows that connect TrackingTime with other apps to streamline productivity, enhance project management, and generate insights on time allocation. Automate tasks based on project activities, sync time entries with calendars, or trigger notifications based on tracked time data. + +# Example Use Cases + +- **Automatically Log Time for Scheduled Meetings**: Sync your TrackingTime projects with Google Calendar events. Whenever a new event starts, trigger a workflow that logs time in TrackingTime for the duration of the event, ensuring accurate time tracking for meetings. + +- **Generate Weekly Time Reports**: Set up a weekly scheduled workflow that retrieves time tracking data from TrackingTime and compiles it into a report. This report can then be sent via email using a service like SendGrid, keeping stakeholders updated on time investment and project progress. + +- **Project Management Integration**: When a task is marked as completed in a project management tool like Trello, trigger a workflow that automatically stops the corresponding time entry in TrackingTime. This keeps your time records in sync with project milestones and deliverables. diff --git a/components/traffit/package.json b/components/traffit/package.json new file mode 100644 index 0000000000000..dfddafc3e9746 --- /dev/null +++ b/components/traffit/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/traffit", + "version": "0.0.1", + "description": "Pipedream Traffit Components", + "main": "traffit.app.mjs", + "keywords": [ + "pipedream", + "traffit" + ], + "homepage": "https://pipedream.com/apps/traffit", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/traffit/traffit.app.mjs b/components/traffit/traffit.app.mjs new file mode 100644 index 0000000000000..58ed416ccfc25 --- /dev/null +++ b/components/traffit/traffit.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "traffit", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/trainual/README.md b/components/trainual/README.md new file mode 100644 index 0000000000000..fdf5ef563d5f6 --- /dev/null +++ b/components/trainual/README.md @@ -0,0 +1,11 @@ +# Overview + +The Trainual API allows you to automate processes around creating, managing, and reporting on training within your organization. With this API on Pipedream, you can build workflows for managing users, subjects, topics, tests, and more, streamlining the onboarding and continuous education processes. By integrating Trainual with other apps on Pipedream, you can create dynamic, cross-functional automations that keep your team learning in sync with other business processes. + +# Example Use Cases + +- **Automated Onboarding Process**: Create a workflow that triggers when a new employee is added to your HR platform like BambooHR. The workflow creates a new user in Trainual, assigns specific subjects and topics based on the role, and sends a personalized welcome email with their training plan. + +- **Progress Tracking to Slack**: Set up a workflow where, whenever a user completes a topic or subject in Trainual, a notification is sent to a designated Slack channel. This keeps the team informed of each other's progress and promotes a culture of learning and achievement. + +- **Monthly Training Reports**: Develop a workflow that generates monthly reports on training progress. Connect Trainual with Google Sheets to aggregate data on user completion rates, quiz scores, and time spent on training material. Automatically email this report to management to review team performance and identify areas for improvement. diff --git a/components/trakt/README.md b/components/trakt/README.md new file mode 100644 index 0000000000000..cdcd1e4023fcd --- /dev/null +++ b/components/trakt/README.md @@ -0,0 +1,11 @@ +# Overview + +The Trakt API provides an interface to track and discover movies and TV shows, allowing you to manage watchlists, get personalized recommendations, and engage with a social community of film and TV enthusiasts. On Pipedream, you can harness this API to create custom workflows that automate your media activities, sync your watch status with other apps, or even analyze your viewing habits. + +# Example Use Cases + +- **Sync Watched Status to Calendar**: Automatically add movies or episodes you've watched to your Google Calendar. This workflow triggers when you mark an item as watched on Trakt, then creates an event in your calendar with the title and watch date. + +- **Curate Weekly Watchlist Email**: Gather your Trakt watchlist and recommendations at the end of each week, and send yourself an email with a curated list of what to watch next. This could include ratings, genres, and a brief synopsis to help you decide. + +- **Social Media Sharing**: Share your ratings or reviews on social media platforms like Twitter. After you rate a movie or episode on Trakt, this workflow posts your rating along with a personalized message to your Twitter feed, spreading the word about your latest picks. diff --git a/components/transform/README.md b/components/transform/README.md index 10e5763643274..3ba1081f18519 100644 --- a/components/transform/README.md +++ b/components/transform/README.md @@ -1,36 +1,11 @@ # Overview -TransForm is a powerful platform for building powerful mobile apps, web -portals, and custom business software. With the TransForm API, you can quickly -and easily build custom applications to meet the unique needs of your business. +The TransForm API allows you to automate server management tasks like creating, updating, and managing servers across multiple cloud platforms. It can be a game-changer for DevOps teams, system administrators, and developers who manage cloud infrastructure. By interfacing with TransForm via Pipedream, you can craft workflows that streamline server provisioning, configuration, and monitoring processes. -Here are some of the types of applications you can create using the TransForm -API: +# Example Use Cases -- CRM and ERP systems -- Business intelligence systems -- Mobile and web portals -- Social networks -- Corporate portals -- eCommerce applications -- Custom consumer and enterprise applications -- Machine Learning algorithms -- Internet of Things (IoT) applications -- Data management applications -- Integrations with 3rd party services and APIs -- And more! +- **Automated Server Provisioning**: Trigger a workflow in Pipedream to automatically provision a new server on AWS or DigitalOcean when a certain event occurs, such as pushing a new code release on GitHub. This can be particularly useful for setting up new environments for testing or ensuring scalability during high traffic events. -The TransForm API makes it easy to integrate your custom applications with -enterprise system such as Salesforce, Oracle, SAP, Dynamics, SharePoint, and -more. With an extensive library of APIs and over 160 pre-built extensions, you -can rapidly build and deploy business applications that are tailored to your -unique needs. Additionally, the advanced security features of the TransForm API -provide advanced measures of data protection and access control. +- **Dynamic DNS Updates**: Create a Pipedream workflow that listens for IP address changes from your cloud servers managed by TransForm. Upon detecting a change, the workflow could update DNS records in Cloudflare or another DNS provider. This is ideal for maintaining consistent access to servers with dynamic IP addresses, especially in environments with frequent scaling. -The TransForm API also includes powerful tools for cloud-enablement of your -applications. With the TransForm APIs, you can create immutable code that is -highly distributed, secure, and performant. This helps to ensure a reliable and -secure application experience for users and developers. - -To get started building with the TransForm API, visit the [TransForm -Documentation Hub](https://transformer.alphasoftware.com/). +- **Server Maintenance Automation**: Use Pipedream to schedule regular maintenance tasks on your servers via TransForm. For example, a workflow could be established to run security updates or backups during off-peak hours. You could even set up alerts to Slack or email to notify your team when maintenance is complete or if issues arise during the process. diff --git a/components/transifex/README.md b/components/transifex/README.md new file mode 100644 index 0000000000000..ba27c9e66de74 --- /dev/null +++ b/components/transifex/README.md @@ -0,0 +1,11 @@ +# Overview + +Transifex is a powerful cloud-based platform designed to help teams manage multilingual content effectively. With the Transifex API, you can automate the syncing of translations, manage localization projects, and streamline communication between developers and translators. Integrating Transifex with Pipedream allows you to connect your localization workflow with other services like GitHub, Slack, or email providers, enhancing productivity and reducing manual work. + +# Example Use Cases + +- **Automated Translation Updates to GitHub**: When a new translation is completed in Transifex, automatically push the updated files to a specific GitHub repository. This ensures that your software or content is always up-to-date with the latest translations without manual intervention. + +- **Sync Translation Progress with Slack**: Set up a workflow that notifies a Slack channel whenever a translation reaches a certain percentage of completion. This can help keep your team informed about the status of localization projects and can prompt immediate reviews or actions needed to finalize content. + +- **Automated Email Alerts for Translation Reviews**: Configure a workflow to send automated emails to reviewers when a translation is ready for final review. This can help shorten the review cycle and ensure high-quality translations are deployed faster. diff --git a/components/transifex/actions/download-file/download-file.mjs b/components/transifex/actions/download-file/download-file.mjs new file mode 100644 index 0000000000000..06385514a4f2f --- /dev/null +++ b/components/transifex/actions/download-file/download-file.mjs @@ -0,0 +1,63 @@ +import fs from "fs"; +import transifex from "../../transifex.app.mjs"; + +export default { + key: "transifex-download-file", + name: "Download File", + description: "Downloads a user-specified file from the Transifex platform. [See the documentation](https://developers.transifex.com/reference/get_resource-strings-async-downloads-resource-strings-async-download-id)", + version: "0.0.1", + type: "action", + props: { + transifex, + asyncDownloadId: { + propDefinition: [ + transifex, + "asyncDownloadId", + ], + }, + }, + methods: { + sleep(ms) { + return new Promise((r) => setTimeout(r, ms)); + }, + }, + async run({ $ }) { + const { data: { id: preparedDownloadId } } = await this.transifex.prepareDownload({ + headers: { + "Accept": "application/vnd.api+json", + "Content-Type": "application/vnd.api+json", + }, + data: { + data: { + "relationships": { + "resource": { + "data": { + "type": "resources", + "id": this.asyncDownloadId, + }, + }, + }, + "type": "resource_strings_async_downloads", + }, + }, + }); + + let response = ""; + do { + await this.sleep(5000); + response = await this.transifex.downloadFile({ + $, + asyncDownloadId: preparedDownloadId, + }); + + } while (response.id && response.id === preparedDownloadId); + + const filePath = `/tmp/downloaded_file_${this.asyncDownloadId}.json`; + fs.writeFileSync(filePath, JSON.stringify(response)); + + $.export("$summary", `Successfully downloaded file with asyncDownloadId: ${this.asyncDownloadId}`); + return { + filePath, + }; + }, +}; diff --git a/components/transifex/actions/upload-file/upload-file.mjs b/components/transifex/actions/upload-file/upload-file.mjs new file mode 100644 index 0000000000000..11fdd9c9d1834 --- /dev/null +++ b/components/transifex/actions/upload-file/upload-file.mjs @@ -0,0 +1,96 @@ +import fs from "fs"; +import { checkTmp } from "../../common/utils.mjs"; +import transifex from "../../transifex.app.mjs"; + +export default { + key: "transifex-upload-file", + name: "Upload File to Transifex", + description: "Uploads a given file to the Transifex platform. [See the documentation](https://developers.transifex.com/reference/post_resource-strings-async-uploads)", + version: "0.0.1", + type: "action", + props: { + transifex, + callbackUrl: { + type: "string", + label: "Callback URL", + description: "The url that will be called when the processing is completed.", + optional: true, + }, + file: { + propDefinition: [ + transifex, + "file", + ], + }, + keepTranslations: { + type: "boolean", + label: "Keep Transalations", + description: "Option to keep translations if a source string with the same key changes.", + optional: true, + }, + replaceEditedStrings: { + type: "boolean", + label: "Replace Edited Strings", + description: "Option to replace edited strings. If true, updated strings modified in the editor will be overwritten.", + optional: true, + }, + organizationId: { + propDefinition: [ + transifex, + "organizationId", + ], + }, + projectId: { + propDefinition: [ + transifex, + "projectId", + ({ organizationId }) => ({ + organizationId, + }), + ], + }, + resourceId: { + propDefinition: [ + transifex, + "resourceId", + ({ projectId }) => ({ + projectId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.transifex.uploadFile({ + $, + headers: { + "Accept": "application/vnd.api+json", + "Content-Type": "application/vnd.api+json", + }, + data: { + data: { + attributes: { + callback_url: this.callbackUrl, + content: fs.readFileSync(checkTmp(this.file), { + encoding: "base64", + }), + content_encoding: "base64", + keep_translations: this.keepTranslations, + replace_edited_strings: this.replaceEditedStrings, + }, + relationships: { + resource: { + data: { + type: "resources", + id: `${this.resourceId}`, + }, + }, + }, + type: "resource_strings_async_uploads", + }, + }, + }); + + $.export("$summary", `Successfully uploaded file with Id: ${response.data.id}`); + return response; + }, +}; diff --git a/components/transifex/common/utils.mjs b/components/transifex/common/utils.mjs new file mode 100644 index 0000000000000..1a5e36f32a603 --- /dev/null +++ b/components/transifex/common/utils.mjs @@ -0,0 +1,6 @@ +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; diff --git a/components/transifex/package.json b/components/transifex/package.json new file mode 100644 index 0000000000000..3e1794903a0c3 --- /dev/null +++ b/components/transifex/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/transifex", + "version": "0.1.0", + "description": "Pipedream Transifex Components", + "main": "transifex.app.mjs", + "keywords": [ + "pipedream", + "transifex" + ], + "homepage": "https://pipedream.com/apps/transifex", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0", + "fs": "^0.0.1-security" + } +} diff --git a/components/transifex/sources/common/base.mjs b/components/transifex/sources/common/base.mjs new file mode 100644 index 0000000000000..689a6883560be --- /dev/null +++ b/components/transifex/sources/common/base.mjs @@ -0,0 +1,78 @@ +import transifex from "../../transifex.app.mjs"; + +export default { + props: { + transifex, + http: { + type: "$.interface.http", + customResponse: false, + }, + db: "$.service.db", + organizationId: { + propDefinition: [ + transifex, + "organizationId", + ], + }, + projectId: { + propDefinition: [ + transifex, + "projectId", + ({ organizationId }) => ({ + organizationId, + }), + ], + }, + }, + methods: { + filterEvent() { + return true; + }, + }, + hooks: { + async activate() { + const { data } = await this.transifex.createTaskWebhook({ + headers: { + "Accept": "application/vnd.api+json", + "Content-Type": "application/vnd.api+json", + }, + data: { + data: { + attributes: { + active: true, + event_type: this.getEventType(), + callback_url: this.http.endpoint, + }, + relationships: { + project: { + data: { + id: this.projectId, + type: "projects", + }, + }, + }, + type: "project_webhooks", + }, + }, + }); + this.db.set("webhookId", data.id); + }, + async deactivate() { + const webhookId = this.db.get("webhookId"); + if (webhookId) { + await this.transifex.deleteTaskWebhook(webhookId); + } + }, + }, + async run({ body }) { + if (this.filterEvent(body)) { + const ts = Date.parse(new Date()); + this.$emit(body, { + id: `${body.resource}-${ts}`, + summary: this.getSummary(body), + ts: ts, + }); + } + + }, +}; diff --git a/components/transifex/sources/new-localization-activity-completed-instant/new-localization-activity-completed-instant.mjs b/components/transifex/sources/new-localization-activity-completed-instant/new-localization-activity-completed-instant.mjs new file mode 100644 index 0000000000000..8729ec3765c68 --- /dev/null +++ b/components/transifex/sources/new-localization-activity-completed-instant/new-localization-activity-completed-instant.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "transifex-new-localization-activity-completed-instant", + name: "New Localization Activity Completed (Instant)", + description: "Emit new event when a resource language is completely translated, reviewed, or filled up by TM or MT.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return "all_events"; + }, + filterEvent(body) { + if ([ + "translation_completed", + "review_completed", + "fillup_completed", + ].includes(body.event)) return true; + }, + getSummary(body) { + return `New event from resource: ${body.resource}.`; + }, + }, + sampleEmit, +}; diff --git a/components/transifex/sources/new-localization-activity-completed-instant/test-event.mjs b/components/transifex/sources/new-localization-activity-completed-instant/test-event.mjs new file mode 100644 index 0000000000000..912a48198cf59 --- /dev/null +++ b/components/transifex/sources/new-localization-activity-completed-instant/test-event.mjs @@ -0,0 +1,7 @@ +export default { + "project": "project_slug", + "translated": 100, + "resource": "resource_slug", + "event": "translation_completed", + "language": "lang_code" +} \ No newline at end of file diff --git a/components/transifex/sources/new-task-for-set-of-strings-instant/new-task-for-set-of-strings-instant.mjs b/components/transifex/sources/new-task-for-set-of-strings-instant/new-task-for-set-of-strings-instant.mjs new file mode 100644 index 0000000000000..cefed0ae675a0 --- /dev/null +++ b/components/transifex/sources/new-task-for-set-of-strings-instant/new-task-for-set-of-strings-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "transifex-new-task-for-set-of-strings-instant", + name: "New Task for Set of Strings (Instant)", + description: "Emit new event when the strings of a task are fully translated.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return "translation_completed"; + }, + getSummary(body) { + return `Resource ${body.resource} has translated ${body.translated}%`; + }, + }, + sampleEmit, +}; diff --git a/components/transifex/sources/new-task-for-set-of-strings-instant/test-event.mjs b/components/transifex/sources/new-task-for-set-of-strings-instant/test-event.mjs new file mode 100644 index 0000000000000..912a48198cf59 --- /dev/null +++ b/components/transifex/sources/new-task-for-set-of-strings-instant/test-event.mjs @@ -0,0 +1,7 @@ +export default { + "project": "project_slug", + "translated": 100, + "resource": "resource_slug", + "event": "translation_completed", + "language": "lang_code" +} \ No newline at end of file diff --git a/components/transifex/transifex.app.mjs b/components/transifex/transifex.app.mjs new file mode 100644 index 0000000000000..4fc84d07b88ee --- /dev/null +++ b/components/transifex/transifex.app.mjs @@ -0,0 +1,182 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "transifex", + propDefinitions: { + file: { + type: "string", + label: "File", + description: "The path to the json file saved to the `/tmp` directory (e.g. `/tmp/example.json`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + }, + organizationId: { + type: "string", + label: "Organization ID", + description: "The ID of the organization.", + async options({ prevContext: { next } }) { + const { + data, links, + } = await this.listOrganizations({ + params: { + "page[cursor]": next, + }, + }); + return { + options: data.map(({ + attributes: { name: label }, id: value, + }) => ({ + label, + value, + })), + context: { + next: this.getNextToken(links), + }, + }; + }, + }, + projectId: { + type: "string", + label: "Project ID", + description: "The ID of the project.", + async options({ + organizationId, prevContext: { next }, + }) { + const { + data, links, + } = await this.listProjects({ + params: { + "filter[organization]": organizationId, + "page[cursor]": next, + }, + }); + return { + options: data.map(({ + attributes: { name: label }, id: value, + }) => ({ + label, + value, + })), + context: { + next: this.getNextToken(links), + }, + }; + }, + }, + resourceId: { + type: "string", + label: "Resource ID", + description: "The ID of the resource.", + async options({ + projectId, prevContext: { next }, + }) { + const { + data, links, + } = await this.listResources({ + params: { + "filter[project]": projectId, + "page[cursor]": next, + }, + }); + return { + options: data.map(({ + id: value, attributes: { name: label }, + }) => ({ + label, + value, + })), + context: { + next: this.getNextToken(links), + }, + }; + }, + }, + asyncDownloadId: { + type: "string", + label: "Async Download ID", + description: "The ID of the asynchronous download.", + }, + }, + methods: { + _baseUrl() { + return "https://rest.api.transifex.com"; + }, + _headers(headers) { + return { + Authorization: `Bearer ${this.$auth.api_token}`, + ...headers, + }; + }, + _makeRequest({ + $ = this, headers, path = "/", ...opts + }) { + return axios($, { + ...opts, + url: this._baseUrl() + path, + headers: this._headers(headers), + }); + }, + getNextToken({ next }) { + if (!next) return false; + return next.split("[cursor]=")[1]; + }, + listOrganizations(opts = {}) { + return this._makeRequest({ + path: "/organizations", + ...opts, + }); + }, + listProjects(opts = {}) { + return this._makeRequest({ + path: "/projects", + ...opts, + }); + }, + listResources(opts = {}) { + return this._makeRequest({ + path: "/resources", + ...opts, + }); + }, + listLanguages(opts = {}) { + return this._makeRequest({ + path: "/languages", + ...opts, + }); + }, + uploadFile(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/resource_strings_async_uploads", + ...opts, + }); + }, + prepareDownload(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/resource_strings_async_downloads", + ...opts, + }); + }, + downloadFile({ + asyncDownloadId, ...opts + }) { + return this._makeRequest({ + path: `/resource_strings_async_downloads/${asyncDownloadId}`, + ...opts, + }); + }, + createTaskWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/project_webhooks", + ...opts, + }); + }, + deleteTaskWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/project_webhooks/${webhookId}`, + }); + }, + }, +}; diff --git a/components/transistor_fm/README.md b/components/transistor_fm/README.md index 4c4f4ef3cc4ba..02b7f0590ab8a 100644 --- a/components/transistor_fm/README.md +++ b/components/transistor_fm/README.md @@ -1,20 +1,11 @@ # Overview -With the Transistor.fm API, you can create powerful audio applications and -experiences. Whether you are building a scalable podcast hosting platform, a -streaming app, or a tool to manage and analyze audio content analytics, the -Transistor.fm API has you covered. Here are a few of the types of applications -you can build using our API: +Transistor.fm is a platform offering podcast hosting and analytics services. With its API, you can automate the upload and management of podcast episodes, access detailed analytics, and manage users. When interfaced with Pipedream, Transistor.fm's API enables the creation of tailored, serverless workflows that can streamline your podcasting process, engage your audience effectively, and integrate with your digital ecosystem, from social media to email marketing platforms. -- Custom podcast hosting platform: Create a tailored podcast hosting platform - that meets the specific needs of your customers. -- Streaming app: Develop a streaming app designed to help listeners discover - and explore new audio content. -- Audio content analytics and management tool: Create a tool to help track and - analyze data about audio content inventory, performance and usage. -- Audio/podcast search engine: Build a capable audio search engine that can - help listeners find audio content quickly and easily. -- Marketplace of audio files: Develop a marketplace of audio files for users to - discover, search for and purchase the audio content they need. -- Audio content aggregation and curation tool: Create a tool to help streamline - and simplify the process of finding, organizing, and managing audio content. +# Example Use Cases + +- **Automated Podcast Publishing Workflow**: Trigger a Pipedream workflow when a new episode is uploaded to your storage platform (like Dropbox or Google Drive). The workflow would fetch the episode file, upload it to Transistor.fm, and publish the episode with pre-defined metadata. Once live, it could then post a tweet via the Twitter API and update your website via a CMS like WordPress with the episode details. + +- **Podcast Performance Monitoring**: Schedule a Pipedream workflow to run weekly that pulls download stats and listener demographics from Transistor.fm's API and compiles them into a report. Then, using the Google Sheets API, it inserts this data into a spreadsheet for easy tracking and visualization, offering insights into the podcast's performance over time. + +- **Listener Engagement Enhancer**: Combine Transistor.fm with an email marketing service like Mailchimp via Pipedream. When a new episode is published, the workflow could extract the episode link and description, and then automatically send an e-newsletter to subscribers with the latest episode content, a personalized message, and a call to action to increase engagement. diff --git a/components/translate_com/README.md b/components/translate_com/README.md new file mode 100644 index 0000000000000..ef67b79ad7328 --- /dev/null +++ b/components/translate_com/README.md @@ -0,0 +1,11 @@ +# Overview + +The Translate.com API on Pipedream enables you to automate translation tasks, making it simpler to handle multilingual content and communication. With this API, you can dynamically translate text between languages, detect the language of input text, or even integrate machine translation into your services or workflows. Pipedream’s serverless platform connects the Translate.com API with hundreds of other apps to streamline your processes, such as automatically translating customer support tickets, social media posts, or updating multilingual content in real-time. + +# Example Use Cases + +- **Automated Customer Support Translation:** Create a workflow that listens for new customer support tickets from a platform like Zendesk. When a new ticket arrives, automatically detect the language and translate the message to English. Then, assign the ticket to the appropriate support team and send the translation for faster resolution. + +- **Real-Time Social Media Monitoring:** Monitor social media platforms for mentions in various languages using an app like Twitter. Translate these mentions to a singular language for easier analysis and sentiment tracking. You can also auto-respond to mentions in the native language of the poster, enhancing engagement. + +- **Content Management System Localization:** For new posts or updates in your Content Management System (CMS), like WordPress, set up an action to translate content into multiple languages before publishing. This can help you maintain multilingual websites and ensure content is accessible to a broader audience instantly. diff --git a/components/travis_ci/README.md b/components/travis_ci/README.md index ea3d2f4d7ed60..cbcc984dd52ca 100644 --- a/components/travis_ci/README.md +++ b/components/travis_ci/README.md @@ -1,15 +1,11 @@ # Overview -Build anything imaginable with the Travis CI API! With the Travis CI API, -you're able to easily create custom applications for continuous integration and -deployment of software projects. The chances are only limited by your -imagination! Here are a few examples of what you might build using the Travis -CI API: - -- Automated software builds and deployments -- Feedback workflow automation -- Continuous integration pipelines -- Automated testing services -- File transformation and validation pipelines -- Data warehousing solutions -- And much more! +The Travis CI API enables developers to automate and enhance their Continuous Integration and Delivery pipeline. With the API, you can manage builds, retrieve build information, cancel jobs, restart builds, and interact with various other Travis CI components programmatically. When you pair this functionality with Pipedream, you can automate reactions to build events, sync data between tools, and trigger workflows in other apps based on Travis CI activity. + +# Example Use Cases + +- **Automated Build Notifications**: Send real-time notifications to Slack, Discord, or Microsoft Teams when a Travis CI build fails. Use Pipedream's built-in connectors to streamline communication and alert your development team so they can address issues quickly. + +- **Dynamic Deployment Trigger**: Trigger deployments to environments such as AWS, Heroku, or Netlify based on successful Travis CI builds. Set up a Pipedream workflow that listens for build completion events and then uses the respective service's API to deploy the latest build automatically. + +- **Build and Test Metrics Dashboard**: Collect and aggregate build performance metrics into a data visualization tool like Google Sheets or Data Studio. Pipedream can capture build data from Travis CI, process the metrics, and then append them to a sheet or push to a dashboard for a comprehensive view of your CI pipeline health. diff --git a/components/travis_ci/package.json b/components/travis_ci/package.json new file mode 100644 index 0000000000000..09c89ee980b96 --- /dev/null +++ b/components/travis_ci/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/travis_ci", + "version": "0.6.0", + "description": "Pipedream travis_ci Components", + "main": "travis_ci.app.mjs", + "keywords": [ + "pipedream", + "travis_ci" + ], + "homepage": "https://pipedream.com/apps/travis_ci", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/trawlingweb/README.md b/components/trawlingweb/README.md new file mode 100644 index 0000000000000..73227c5634948 --- /dev/null +++ b/components/trawlingweb/README.md @@ -0,0 +1,11 @@ +# Overview + +The Trawlingweb API lets you access real-time data from across the web. By integrating with Pipedream, you can automate web monitoring tasks, analyze trends, and react to content changes without manual oversight. Pipedream's serverless platform allows for seamless integration with Trawlingweb, enabling you to create workflows that can, for instance, trigger on specific API responses, process the data, and connect with multiple other services. + +# Example Use Cases + +- **Content Change Detection and Notification**: Monitor specific URLs or keywords using Trawlingweb, and set up a Pipedream workflow to receive notifications via email or messaging platforms like Slack whenever changes are detected. This can be vital for reputation management or competitive analysis. + +- **Trend Analysis and Reporting**: Use Trawlingweb to track trends or mentions across the web. Then, employ Pipedream to analyze this data, create visual reports, and automatically send them to Google Sheets or a BI tool for further insights and decision-making. + +- **Automated Content Aggregation**: Build a workflow that uses Trawlingweb to aggregate content from various sources based on certain criteria, then push this content into a CMS like WordPress or a database for curation, enabling efficient content management. diff --git a/components/trawlingweb/package.json b/components/trawlingweb/package.json new file mode 100644 index 0000000000000..6178c34af6b91 --- /dev/null +++ b/components/trawlingweb/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/trawlingweb", + "version": "0.0.1", + "description": "Pipedream Trawlingweb Components", + "main": "trawlingweb.app.mjs", + "keywords": [ + "pipedream", + "trawlingweb" + ], + "homepage": "https://pipedream.com/apps/trawlingweb", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/trawlingweb/trawlingweb.app.mjs b/components/trawlingweb/trawlingweb.app.mjs new file mode 100644 index 0000000000000..3506a3db4307d --- /dev/null +++ b/components/trawlingweb/trawlingweb.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "trawlingweb", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/trello/README.md b/components/trello/README.md index e590f01ff608e..46b51858775d5 100644 --- a/components/trello/README.md +++ b/components/trello/README.md @@ -1,12 +1,11 @@ # Overview -With the Trello API, you can: - -- Create new boards -- Add and remove lists from boards -- Add and remove cards from lists -- Add comments to cards -- Add and remove attachments from cards -- Add and remove members from boards -- Change the background of boards -- And more! +Trello's API lets you craft workflows around managing boards, lists, cards, and users. With Pipedream, you can automate Trello tasks, like syncing cards with external databases, updating checklists, and posting notifications to other platforms. It enables seamless connection with other apps, fostering productivity by automating routine board operations, card management, and team notifications. + +# Example Use Cases + +- **Automated Project Management**: Sync Trello cards with a GitHub repository, creating issues from new cards, and updating cards when issues are closed. This keeps developers and stakeholders aligned across platforms. + +- **Marketing Campaign Tracker**: Connect Trello with Google Sheets to track campaign progress. When a card moves to the "Complete" list, automatically log the details in a spreadsheet, giving a real-time view of campaign status. + +- **Team Coordination**: Integrate Trello with Slack, notifying a channel when due dates are updated or cards are marked complete, ensuring the team stays informed of task progression without having to check Trello. diff --git a/components/trello/actions/add-attachment-to-card-via-url/add-attachment-to-card-via-url.mjs b/components/trello/actions/add-attachment-to-card-via-url/add-attachment-to-card-via-url.mjs deleted file mode 100644 index d6548aea182fc..0000000000000 --- a/components/trello/actions/add-attachment-to-card-via-url/add-attachment-to-card-via-url.mjs +++ /dev/null @@ -1,73 +0,0 @@ -import common from "../common.mjs"; - -export default { - ...common, - key: "trello-add-attachment-to-card-via-url", - name: "Add Attachment to Card via URL", - description: "Adds a file attachment on a card by referencing a public URL. [See the docs here](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-attachments-post)", - version: "0.0.2", - type: "action", - props: { - ...common.props, - board: { - propDefinition: [ - common.props.trello, - "board", - ], - }, - idCard: { - propDefinition: [ - common.props.trello, - "cards", - (c) => ({ - board: c.board, - }), - ], - type: "string", - label: "Card", - description: "The ID of the Card to add the Attachment to", - optional: false, - }, - name: { - propDefinition: [ - common.props.trello, - "name", - ], - }, - url: { - propDefinition: [ - common.props.trello, - "url", - ], - }, - mimeType: { - propDefinition: [ - common.props.trello, - "mimeType", - ], - }, - setCover: { - type: "boolean", - label: "Set Cover?", - description: "Determines whether to use the new attachment as a cover for the Card", - default: false, - }, - }, - async run({ $ }) { - const { - idCard, - name, - url, - mimeType, - setCover, - } = this; - const res = await this.trello.addAttachmentToCardViaUrl(idCard, { - name, - url, - mimeType, - setCover, - }, $); - $.export("$summary", `Successfully added attachement to card ${idCard}`); - return res; - }, -}; diff --git a/components/trello/actions/add-attachment-to-card/add-attachment-to-card.mjs b/components/trello/actions/add-attachment-to-card/add-attachment-to-card.mjs new file mode 100644 index 0000000000000..12c31d0063404 --- /dev/null +++ b/components/trello/actions/add-attachment-to-card/add-attachment-to-card.mjs @@ -0,0 +1,131 @@ +import { ConfigurationError } from "@pipedream/platform"; +import fs from "fs"; +import FormData from "form-data"; +import app from "../../trello.app.mjs"; + +export default { + key: "trello-add-attachment-to-card", + name: "Add Attachment To Card", + description: "Adds a file attachment on a card. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-attachments-post)", + version: "0.0.2", + type: "action", + props: { + app, + board: { + propDefinition: [ + app, + "board", + ], + }, + cardId: { + propDefinition: [ + app, + "cards", + (c) => ({ + board: c.board, + }), + ], + type: "string", + label: "Card", + description: "The ID of the Card to add the Attachment to", + optional: false, + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + fileType: { + propDefinition: [ + app, + "fileType", + ], + reloadProps: true, + }, + url: { + propDefinition: [ + app, + "url", + ], + hidden: true, + }, + file: { + propDefinition: [ + app, + "file", + ], + hidden: true, + }, + mimeType: { + propDefinition: [ + app, + "mimeType", + ], + hidden: true, + }, + setCover: { + type: "boolean", + label: "Set Cover?", + description: "Determines whether to use the new attachment as a cover for the Card", + default: false, + optional: true, + }, + }, + async additionalProps(props) { + const attachmentIsPath = this.fileType === "path"; + const attachmentIsUrl = this.fileType === "url"; + props.file.hidden = !attachmentIsPath; + props.mimeType.hidden = !attachmentIsPath; + props.url.hidden = !attachmentIsUrl; + + return {}; + }, + async run({ $ }) { + const { + cardId, + name, + url, + mimeType, + setCover, + file, + } = this; + + let response; + const params = { + name, + mimeType, + setCover, + }; + + if (file && !file?.startsWith("/tmp")) { + throw new ConfigurationError("The file path must be in the `/tmp` directory"); + } + + if (file) { + const form = new FormData(); + form.append("file", fs.createReadStream(file)); + + response = await this.app.addAttachmentToCard({ + $, + cardId, + params, + headers: form.getHeaders(), + data: form, + }); + + } else { + response = await this.app.addAttachmentToCard({ + $, + cardId, + params: { + ...params, + url, + }, + }); + } + + $.export("$summary", `Successfully added attachement to card ${cardId}`); + return response; + }, +}; diff --git a/components/trello/actions/add-checklist/add-checklist.mjs b/components/trello/actions/add-checklist/add-checklist.mjs index 2baeecec53dfd..0b97164741e8f 100644 --- a/components/trello/actions/add-checklist/add-checklist.mjs +++ b/components/trello/actions/add-checklist/add-checklist.mjs @@ -1,68 +1,76 @@ -// legacy_hash_id: a_WYieM3 -import { axios } from "@pipedream/platform"; +import app from "../../trello.app.mjs"; export default { key: "trello-add-checklist", - name: "Create a Checklist", - description: "Adds a new checklist to a card.", - version: "0.1.2", + name: "Add Checklist", + description: "Adds a new checklist to a card. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-checklists-post).", + version: "0.2.1", type: "action", props: { - trello: { - type: "app", - app: "trello", + app, + boardId: { + propDefinition: [ + app, + "board", + ], }, - id: { + cardId: { type: "string", + label: "Card ID", description: "The ID of the card.", + optional: false, + propDefinition: [ + app, + "cards", + ({ boardId }) => ({ + board: boardId, + }), + ], }, name: { type: "string", + label: "Checklist Name", description: "The name of the checklist.", optional: true, }, idChecklistSource: { - type: "string", - description: "The ID of a source checklist to copy into the new one.", optional: true, + propDefinition: [ + app, + "checklist", + ({ boardId }) => ({ + board: boardId, + }), + ], + label: "Copy from Checklist", }, pos: { - type: "string", - description: "The position of the checklist on the card. One of: top, bottom, or a positive number.", - optional: true, + propDefinition: [ + app, + "pos", + ], }, }, async run({ $ }) { - const oauthSignerUri = this.trello.$auth.oauth_signer_uri; - - let id = this.id; - const trelloParams = [ - "name", - "idChecklistSource", - "pos", - ]; - let p = this; - - const queryString = trelloParams.filter((param) => p[param]).map((param) => `${param}=${p[param]}`) - .join("&"); - - const config = { - url: `https://api.trello.com/1/cards/${id}/checklists?${queryString}`, - method: "POST", - data: "", - }; + const { + cardId, + name, + idChecklistSource, + pos, + } = this; - const token = { - key: this.trello.$auth.oauth_access_token, - secret: this.trello.$auth.oauth_refresh_token, - }; + const response = await this.app.addChecklist({ + $, + cardId, + params: { + name, + idChecklistSource, + pos, + }, + }); - const signConfig = { - token, - oauthSignerUri, - }; + $.export("$summary", `Successfully added checklist with ID: ${response.id}`); - const resp = await axios($, config, signConfig); - return resp; + return response; }, }; diff --git a/components/trello/actions/add-comment/add-comment.mjs b/components/trello/actions/add-comment/add-comment.mjs index ff3c134cd3aef..e9d15920e83b8 100644 --- a/components/trello/actions/add-comment/add-comment.mjs +++ b/components/trello/actions/add-comment/add-comment.mjs @@ -1,55 +1,64 @@ -// legacy_hash_id: a_Xzi26w -import { axios } from "@pipedream/platform"; +import app from "../../trello.app.mjs"; export default { key: "trello-add-comment", - name: "Create a Comment", - description: "Create a new comment on a specific card.", - version: "0.1.2", + name: "Add Comment", + description: "Create a new comment on a specific card. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-actions-comments-post).", + version: "0.2.1", type: "action", props: { - trello: { - type: "app", - app: "trello", + app, + boardId: { + propDefinition: [ + app, + "board", + ], }, - id: { + cardId: { type: "string", + label: "Card ID", description: "The ID of the card.", + optional: false, + propDefinition: [ + app, + "cards", + ({ boardId }) => ({ + board: boardId, + }), + ], }, text: { type: "string", + label: "Comment", description: "The comment to add.", }, }, + methods: { + addComment({ + cardId, ...args + } = {}) { + return this.app.post({ + path: `/cards/${cardId}/actions/comments`, + ...args, + }); + }, + }, async run({ $ }) { - const oauthSignerUri = this.trello.$auth.oauth_signer_uri; - - let id = this.id; - const trelloParams = [ - "text", - ]; - let p = this; - - const queryString = trelloParams.filter((param) => p[param]).map((param) => `${param}=${p[param]}`) - .join("&"); - - const config = { - url: `https://api.trello.com/1/cards/${id}/actions/comments?${queryString}`, - method: "POST", - data: "", - }; + const { + cardId, + text, + } = this; - const token = { - key: this.trello.$auth.oauth_access_token, - secret: this.trello.$auth.oauth_refresh_token, - }; + const response = await this.app.addComment({ + $, + cardId, + params: { + text, + }, + }); - const signConfig = { - token, - oauthSignerUri, - }; + $.export("$summary", `Successfully added comment with ID: ${response.id}`); - const resp = await axios($, config, signConfig); - return resp; + return response; }, }; diff --git a/components/trello/actions/add-existing-label-to-card/add-existing-label-to-card.mjs b/components/trello/actions/add-existing-label-to-card/add-existing-label-to-card.mjs index 78213af33b97a..fce3990ac1825 100644 --- a/components/trello/actions/add-existing-label-to-card/add-existing-label-to-card.mjs +++ b/components/trello/actions/add-existing-label-to-card/add-existing-label-to-card.mjs @@ -1,23 +1,22 @@ -import common from "../common.mjs"; +import app from "../../trello.app.mjs"; export default { - ...common, key: "trello-add-existing-label-to-card", name: "Add Existing Label to Card", - description: "Adds an existing label to the specified card. [See the docs here](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-idlabels-post)", - version: "0.0.2", + description: "Adds an existing label to the specified card. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-idlabels-post).", + version: "0.1.1", type: "action", props: { - ...common.props, + app, board: { propDefinition: [ - common.props.trello, + app, "board", ], }, - idCard: { + cardId: { propDefinition: [ - common.props.trello, + app, "cards", (c) => ({ board: c.board, @@ -28,21 +27,27 @@ export default { description: "The ID of the Card to add the Label to", optional: false, }, - idLabel: { + value: { propDefinition: [ - common.props.trello, + app, "label", (c) => ({ board: c.board, + card: c.cardId, + excludeCardLabels: true, }), ], }, }, async run({ $ }) { - const res = await this.trello.addExistingLabelToCard(this.idCard, { - value: this.idLabel, - }, $); - $.export("$summary", `Successfully added label ${this.idLabel} to card ${this.idCard}`); + const res = await this.app.addExistingLabelToCard({ + $, + cardId: this.cardId, + params: { + value: this.value, + }, + }); + $.export("$summary", `Successfully added label and returned \`${res.length}\` labels added.`); return res; }, }; diff --git a/components/trello/actions/add-file-as-attachment-via-url/add-file-as-attachment-via-url.mjs b/components/trello/actions/add-file-as-attachment-via-url/add-file-as-attachment-via-url.mjs deleted file mode 100644 index 52bd3f2d00cc8..0000000000000 --- a/components/trello/actions/add-file-as-attachment-via-url/add-file-as-attachment-via-url.mjs +++ /dev/null @@ -1,72 +0,0 @@ -// legacy_hash_id: a_elirYr -import { axios } from "@pipedream/platform"; - -export default { - key: "trello-add-file-as-attachment-via-url", - name: "Add Attachment to Card via URL", - description: "Create a file attachment on a card by referencing a public URL", - version: "0.1.2", - type: "action", - props: { - trello: { - type: "app", - app: "trello", - }, - name: { - type: "string", - label: "Name", - description: "The name of the attachment. Max length 256.", - optional: true, - }, - mimeType: { - type: "string", - label: "MIME Type", - description: "The mimeType of the attachment. Max length 256.", - optional: true, - }, - url: { - type: "string", - label: "File URL", - description: "A URL to a file you'd like to attach. Must start with http:// or https://", - }, - id: { - type: "string", - label: "ID", - description: "The ID of your Trello card", - }, - }, - async run({ $ }) { - const oauthSignerUri = this.trello.$auth.oauth_signer_uri; - - const trelloParams = [ - "name", - "mimeType", - "url", - ]; - let p = this; - - const queryString = trelloParams.filter((param) => p[param]).map((param) => `${param}=${encodeURIComponent(p[param])}`) - .join("&"); - - const config = { - url: `https://api.trello.com/1/cards/${this.id}/attachments?${queryString}`, - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }; - - const token = { - key: this.trello.$auth.oauth_access_token, - secret: this.trello.$auth.oauth_refresh_token, - }; - - const signConfig = { - token, - oauthSignerUri, - }; - - const resp = await axios($, config, signConfig); - return resp; - }, -}; diff --git a/components/trello/actions/add-image-attachment/add-image-attachment.mjs b/components/trello/actions/add-image-attachment/add-image-attachment.mjs deleted file mode 100644 index 5a4895f388068..0000000000000 --- a/components/trello/actions/add-image-attachment/add-image-attachment.mjs +++ /dev/null @@ -1,75 +0,0 @@ -// legacy_hash_id: a_B0im8k -import { axios } from "@pipedream/platform"; - -export default { - key: "trello-add-image-attachment", - name: "Add Image Attachment to Card", - description: "Adds image to card", - version: "0.1.3", - type: "action", - props: { - trello: { - type: "app", - app: "trello", - }, - id: { - type: "string", - description: "The ID of the card.", - }, - name: { - type: "string", - description: "The name of the attachment. Max length 256.", - optional: true, - }, - file: { - type: "string", - description: "The file to attach, as multipart/form-data", - optional: true, - }, - mimeType: { - type: "string", - description: "The mimeType of the attachment. Max length 256.", - optional: true, - }, - url: { - type: "string", - description: "A URL to attach. Must start with http:// or https://", - }, - }, - async run({ $ }) { - const oauthSignerUri = this.trello.$auth.oauth_signer_uri; - - let id = this.id; - const trelloParams = [ - "name", - "file", - "mimeType", - "url", - ]; - let p = this; - - const queryString = trelloParams.filter((param) => p[param]).map((param) => `${param}=${p[param]}`) - .join("&"); - - const config = { - url: `https://api.trello.com/1/cards/${id}/attachments?${queryString}`, - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }; - - const token = { - key: this.trello.$auth.oauth_access_token, - secret: this.trello.$auth.oauth_refresh_token, - }; - - const signConfig = { - token, - oauthSignerUri, - }; - - const resp = await axios($, config, signConfig); - return resp; - }, -}; diff --git a/components/trello/actions/add-label-to-card/add-label-to-card.mjs b/components/trello/actions/add-label-to-card/add-label-to-card.mjs deleted file mode 100644 index 6667f65914e5c..0000000000000 --- a/components/trello/actions/add-label-to-card/add-label-to-card.mjs +++ /dev/null @@ -1,55 +0,0 @@ -// legacy_hash_id: a_xqi4E7 -import { axios } from "@pipedream/platform"; - -export default { - key: "trello-add-label-to-card", - name: "Add Existing Label to Card", - description: "Add an existing label to a card.", - version: "0.1.2", - type: "action", - props: { - trello: { - type: "app", - app: "trello", - }, - id: { - type: "string", - description: "The ID of the card.", - }, - value: { - type: "string", - description: "The ID of the label to add", - }, - }, - async run({ $ }) { - const oauthSignerUri = this.trello.$auth.oauth_signer_uri; - - let id = this.id; - const trelloParams = [ - "value", - ]; - let p = this; - - const queryString = trelloParams.filter((param) => p[param]).map((param) => `${param}=${p[param]}`) - .join("&"); - - const config = { - url: `https://api.trello.com/1/cards/${id}/idLabels?${queryString}`, - method: "POST", - data: "", - }; - - const token = { - key: this.trello.$auth.oauth_access_token, - secret: this.trello.$auth.oauth_refresh_token, - }; - - const signConfig = { - token, - oauthSignerUri, - }; - - const resp = await axios($, config, signConfig); - return resp; - }, -}; diff --git a/components/trello/actions/add-member-to-card/add-member-to-card.mjs b/components/trello/actions/add-member-to-card/add-member-to-card.mjs index 153ce8dded70c..e70949454049a 100644 --- a/components/trello/actions/add-member-to-card/add-member-to-card.mjs +++ b/components/trello/actions/add-member-to-card/add-member-to-card.mjs @@ -1,23 +1,22 @@ -import common from "../common.mjs"; +import app from "../../trello.app.mjs"; export default { - ...common, key: "trello-add-member-to-card", name: "Add Member to Card", - description: "Adds a member to the specified card. [See the docs here](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-idmembers-post)", - version: "0.1.3", + description: "Adds a member to the specified card. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-idmembers-post).", + version: "0.2.1", type: "action", props: { - ...common.props, + app, board: { propDefinition: [ - common.props.trello, + app, "board", ], }, - idCard: { + cardId: { propDefinition: [ - common.props.trello, + app, "cards", (c) => ({ board: c.board, @@ -28,21 +27,27 @@ export default { description: "The ID of the Card to add the Member to", optional: false, }, - idMember: { + value: { propDefinition: [ - common.props.trello, + app, "member", (c) => ({ board: c.board, + card: c.cardId, + excludeCardMembers: true, }), ], }, }, async run({ $ }) { - const res = await this.trello.addMemberToCard(this.idCard, { - value: this.idMember, - }, $); - $.export("$summary", `Successfully added member ${res[0].fullName} to card ${this.idCard}`); + const res = await this.app.addMemberToCard({ + $, + cardId: this.cardId, + params: { + value: this.value, + }, + }); + $.export("$summary", `Successfully added member ${res[0].fullName} to card ${this.cardId}`); return res; }, }; diff --git a/components/trello/actions/archive-card/archive-card.mjs b/components/trello/actions/archive-card/archive-card.mjs index 8ea87fd5c6c75..678d67abd37a7 100644 --- a/components/trello/actions/archive-card/archive-card.mjs +++ b/components/trello/actions/archive-card/archive-card.mjs @@ -1,23 +1,22 @@ -import common from "../common.mjs"; +import app from "../../trello.app.mjs"; export default { - ...common, key: "trello-archive-card", name: "Archive Card", - description: "Archives a card. [See the docs here](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-put)", - version: "0.1.3", + description: "Archives a card. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-put).", + version: "0.2.1", type: "action", props: { - ...common.props, + app, board: { propDefinition: [ - common.props.trello, + app, "board", ], }, - idCard: { + cardId: { propDefinition: [ - common.props.trello, + app, "cards", (c) => ({ board: c.board, @@ -30,8 +29,14 @@ export default { }, }, async run({ $ }) { - const res = await this.trello.archiveCard(this.idCard, $); - $.export("$summary", `Successfully archived card ${this.idCard}`); + const res = await this.app.updateCard({ + $, + cardId: this.cardId, + data: { + closed: true, + }, + }); + $.export("$summary", `Successfully archived card ${this.cardId}`); return res; }, }; diff --git a/components/trello/actions/close-board/close-board.mjs b/components/trello/actions/close-board/close-board.mjs deleted file mode 100644 index a9355263e7c1c..0000000000000 --- a/components/trello/actions/close-board/close-board.mjs +++ /dev/null @@ -1,25 +0,0 @@ -import common from "../common.mjs"; - -export default { - ...common, - key: "trello-close-board", - name: "Close Board", - description: "Closes a board. [See the docs here](https://developer.atlassian.com/cloud/trello/rest/api-group-boards/#api-boards-id-put)", - version: "0.0.2", - type: "action", - props: { - ...common.props, - boardId: { - propDefinition: [ - common.props.trello, - "board", - ], - description: "The ID of the Board to close", - }, - }, - async run({ $ }) { - const res = await this.trello.closeBoard(this.boardId, $); - $.export("$summary", `Successfully closed board ${this.boardId}`); - return res; - }, -}; diff --git a/components/trello/actions/common.mjs b/components/trello/actions/common.mjs deleted file mode 100644 index e7c49e7c56099..0000000000000 --- a/components/trello/actions/common.mjs +++ /dev/null @@ -1,21 +0,0 @@ -import trello from "../trello.app.mjs"; - -export default { - props: { - trello, - }, - methods: { - /** - * Returns an array of objects that matches the object's `name` property with the `query` param - * - * @param {array} foundObjects an array of objects results of a Trello's get - * endpoint on `labels` and `lists`. - * @param {string} query the name string that will be use to query against `foundObjects.name` - * property - * @returns {array} the objects from `foundObjects` matching the specified query. - */ - getMatches(foundObjects, query) { - return foundObjects?.filter((obj) => obj.name.includes(query)) ?? []; - }, - }, -}; diff --git a/components/trello/actions/common/common.mjs b/components/trello/actions/common/common.mjs new file mode 100644 index 0000000000000..0fd5cade15a04 --- /dev/null +++ b/components/trello/actions/common/common.mjs @@ -0,0 +1,21 @@ +import app from "../../trello.app.mjs"; + +export default { + props: { + app, + }, + methods: { + /** + * Returns an array of objects that matches the object's `name` property with the `query` param + * + * @param {array} foundObjects an array of objects results of a Trello's get + * endpoint on `labels` and `lists`. + * @param {string} query the name string that will be use to query against `foundObjects.name` + * property + * @returns {array} the objects from `foundObjects` matching the specified query. + */ + getMatches(foundObjects, query) { + return foundObjects?.filter((obj) => obj.name.includes(query)) ?? []; + }, + }, +}; diff --git a/components/trello/actions/complete-checklist-item/complete-checklist-item.mjs b/components/trello/actions/complete-checklist-item/complete-checklist-item.mjs index 2ea93ee2f8b33..350fe5f55db69 100644 --- a/components/trello/actions/complete-checklist-item/complete-checklist-item.mjs +++ b/components/trello/actions/complete-checklist-item/complete-checklist-item.mjs @@ -1,49 +1,69 @@ -// legacy_hash_id: a_EViowW -import { axios } from "@pipedream/platform"; +import app from "../../trello.app.mjs"; export default { key: "trello-complete-checklist-item", name: "Complete a Checklist Item", - description: "Completes an existing checklist item in a card.", - version: "0.1.2", + description: "Completes an existing checklist item in a card. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-checkitem-idcheckitem-put).", + version: "0.2.1", type: "action", props: { - trello: { - type: "app", - app: "trello", + app, + board: { + propDefinition: [ + app, + "board", + ], }, - id: { + cardId: { + propDefinition: [ + app, + "cards", + (c) => ({ + board: c.board, + checklistCardsOnly: true, + }), + ], type: "string", + label: "Card ID", description: "The ID of the card.", + optional: false, }, - idCheckItem: { - type: "string", - description: "The ID of the checklist item to complete.", + checklistId: { + propDefinition: [ + app, + "checklist", + ({ cardId }) => ({ + card: cardId, + }), + ], + }, + checklistItemId: { + propDefinition: [ + app, + "checklistItemId", + ({ checklistId }) => ({ + checklistId, + }), + ], }, }, async run({ $ }) { - const oauthSignerUri = this.trello.$auth.oauth_signer_uri; - - let id = this.id; - let idCheckItem = this.idCheckItem; - - const config = { - url: `https://api.trello.com/1/cards/${id}/checkItem/${idCheckItem}?state=complete`, - method: "PUT", - data: "", - }; + const { + cardId, + checklistItemId, + } = this; - const token = { - key: this.trello.$auth.oauth_access_token, - secret: this.trello.$auth.oauth_refresh_token, - }; + const response = await this.app.completeChecklistItem({ + $, + cardId, + checklistItemId, + params: { + state: "complete", + }, + }); - const signConfig = { - token, - oauthSignerUri, - }; + $.export("$summary", `Successfully completed checklist item with ID: ${checklistItemId}`); - const resp = await axios($, config, signConfig); - return resp; + return response; }, }; diff --git a/components/trello/actions/copy-board/copy-board.mjs b/components/trello/actions/copy-board/copy-board.mjs deleted file mode 100644 index fb80f517dc2f6..0000000000000 --- a/components/trello/actions/copy-board/copy-board.mjs +++ /dev/null @@ -1,174 +0,0 @@ -// legacy_hash_id: a_B0izQg -import { axios } from "@pipedream/platform"; - -export default { - key: "trello-copy-board", - name: "Copy a Board", - description: "Creates a copy of an existing board.", - version: "0.1.2", - type: "action", - props: { - trello: { - type: "app", - app: "trello", - }, - name: { - type: "string", - description: "The new name for the board. 1 to 16384 characters long.", - }, - defaultLabels: { - type: "boolean", - description: "Determines whether to use the default set of labels.", - optional: true, - }, - defaultLists: { - type: "boolean", - description: "Determines whether to add the default set of lists to a board (To Do, Doing, Done). It is ignored if idBoardSource is provided.", - optional: true, - }, - desc: { - type: "string", - description: "A new description for the board, 0 to 16384 characters long.", - optional: true, - }, - idOrganization: { - type: "string", - description: "The id or name of the team the board should belong to.", - optional: true, - }, - idBoardSource: { - type: "string", - description: "The id of a board to copy into the new board.", - }, - keepFromSource: { - type: "string", - description: "To keep cards from the original board pass in the value \"cards\".", - optional: true, - options: [ - "none", - "cards", - ], - }, - powerUps: { - type: "string", - description: "The Power-Ups that should be enabled on the new board. One of: all, calendar, cardAging, recap, voting.", - optional: true, - }, - prefs_permissionLevel: { - type: "string", - description: "The permissions level of the board. One of: org, private, public.", - optional: true, - options: [ - "org", - "private", - "public", - ], - }, - prefs_voting: { - type: "string", - label: "Prefs Voting", - description: "Who can vote on this board. One of disabled, members, observers, org, public.", - optional: true, - options: [ - "disabled", - "members", - "observers", - "org", - "public", - ], - }, - prefs_comments: { - type: "string", - label: "Prefs Comments", - description: "Who can comment on cards on this board. One of: disabled, members, observers, org, public.", - optional: true, - options: [ - "disabled", - "members", - "observers", - "org", - "public", - ], - }, - prefs_invitations: { - type: "string", - label: "Prefs Invitations", - description: "Determines what types of members can invite users to join. One of: admins, members.", - optional: true, - options: [ - "admins", - "members", - ], - }, - prefs_selfJoin: { - type: "boolean", - description: "Determines whether users can join the boards themselves or whether they have to be invited.", - optional: true, - }, - prefs_cardCovers: { - type: "string", - description: "Determines whether card covers are enabled.", - optional: true, - }, - prefs_background: { - type: "string", - label: "Prefs Background", - description: "The id of a custom background or one of: blue, orange, green, red, purple, pink, lime, sky, grey.", - optional: true, - }, - prefs_cardAging: { - type: "string", - description: "Determines the type of card aging that should take place on the board if card aging is enabled. One of: pirate, regular.", - optional: true, - options: [ - "pirate", - "regular", - ], - }, - }, - async run({ $ }) { - const oauthSignerUri = this.trello.$auth.oauth_signer_uri; - - const trelloParams = [ - "name", - "defaultLabels", - "defaultLists", - "desc", - "idOrganization", - "idBoardSource", - "keepFromSource", - "powerUps", - "prefs_permissionLevel", - "prefs_voting", - "prefs_comments", - "prefs_invitations", - "prefs_selfJoin", - "prefs_cardCovers", - "prefs_background", - "prefs_cardAging", - ]; - let p = this; - - const queryString = trelloParams.filter((param) => p[param]).map((param) => `${param}=${p[param]}`) - .join("&"); - - const config = { - url: `https://api.trello.com/1/boards?${queryString}`, - method: "POST", - data: "", - }; - - const token = { - key: this.trello.$auth.oauth_access_token, - secret: this.trello.$auth.oauth_refresh_token, - }; - - const signConfig = { - token, - oauthSignerUri, - }; - - const resp = await axios($, config, signConfig); - return resp; - }, -}; diff --git a/components/trello/actions/create-board/create-board.mjs b/components/trello/actions/create-board/create-board.mjs index 9655060fc9c69..bdd57b75bebc8 100644 --- a/components/trello/actions/create-board/create-board.mjs +++ b/components/trello/actions/create-board/create-board.mjs @@ -1,49 +1,60 @@ -// legacy_hash_id: a_Nqi0GV -import { axios } from "@pipedream/platform"; +import app from "../../trello.app.mjs"; +import constants from "../../common/constants.mjs"; export default { key: "trello-create-board", name: "Create a Board", - description: "Creates a new Trello board.", - version: "0.1.2", + description: "Create a new Trello board or copy from an existing one. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-boards/#api-boards-post).", + version: "0.2.1", type: "action", props: { - trello: { - type: "app", - app: "trello", - }, + app, name: { type: "string", + label: "Name", description: "The new name for the board. 1 to 16384 characters long.", }, defaultLabels: { type: "boolean", + label: "Default Labels", description: "Determines whether to use the default set of labels.", optional: true, }, defaultLists: { type: "boolean", + label: "Default Lists", description: "Determines whether to add the default set of lists to a board (To Do, Doing, Done). It is ignored if idBoardSource is provided.", optional: true, }, desc: { type: "string", + label: "Description", description: "A new description for the board, 0 to 16384 characters long", optional: true, }, idOrganization: { type: "string", + label: "Organization ID", description: "The id or name of the team the board should belong to.", - optional: true, + optional: false, + propDefinition: [ + app, + "idOrganizations", + ], }, idBoardSource: { - type: "string", + label: "Board Source ID", description: "The id of a board to copy into the new board.", optional: true, + propDefinition: [ + app, + "board", + ], }, keepFromSource: { type: "string", - description: "To keep cards from the original board pass in the value \"cards\".", + label: "Keep From Source", + description: "To keep cards from the original board pass in the value `cards`.", optional: true, options: [ "none", @@ -52,124 +63,111 @@ export default { }, powerUps: { type: "string", - description: "The Power-Ups that should be enabled on the new board. One of: all, calendar, cardAging, recap, voting.", + label: "Power-Ups", + description: "The Power-Ups that should be enabled on the new board. One of: `all`, `calendar`, `cardAging`, `recap`, `voting`.", optional: true, + options: constants.POWER_UPS, }, - prefs_permissionLevel: { + prefsPermissionLevel: { type: "string", description: "The permissions level of the board. One of: org, private, public.", + label: "Prefs Permission Level", optional: true, - options: [ - "org", - "private", - "public", - ], + options: constants.PREFS_PERMISSION_LEVELS, }, - prefs_voting: { + prefsVoting: { type: "string", label: "Prefs Voting", description: "Who can vote on this board. One of disabled, members, observers, org, public.", optional: true, - options: [ - "disabled", - "members", - "observers", - "org", - "public", - ], + options: constants.PREFS_VOTING, }, - prefs_comments: { + prefsComments: { type: "string", label: "Prefs Comments", description: "Who can comment on cards on this board. One of: disabled, members, observers, org, public.", optional: true, - options: [ - "disabled", - "members", - "observers", - "org", - "public", - ], + options: constants.PREFS_COMMENTS, }, - prefs_invitations: { + prefsInvitations: { type: "string", label: "Prefs Invitations", description: "Determines what types of members can invite users to join. One of: admins, members.", optional: true, - options: [ - "admins", - "members", - ], + options: constants.PREFS_INVITATIONS, }, - prefs_selfJoin: { + prefsSelfJoin: { type: "boolean", + label: "Prefs Self Join", description: "Determines whether users can join the boards themselves or whether they have to be invited.", optional: true, }, - prefs_cardCovers: { + prefsCardCovers: { type: "boolean", + label: "Prefs Card Covers", description: "Determines whether card covers are enabled.", optional: true, }, - prefs_background: { + prefsBackground: { type: "string", label: "Prefs Background", - description: "The id of a custom background or one of: blue, orange, green, red, purple, pink, lime, sky, grey.", + description: "The id of a custom background or one of: `blue`, `orange`, `green`, `red`, `purple`, `pink`, `lime`, `sky`, `grey`.", optional: true, + options: constants.PREFS_BACKGROUNDS, }, - prefs_cardAging: { + prefsCardAging: { type: "string", + label: "Prefs Card Aging", description: "Determines the type of card aging that should take place on the board if card aging is enabled. One of: pirate, regular.", optional: true, - options: [ - "pirate", - "regular", - ], + options: constants.PREFS_CARD_AGING, }, }, async run({ $ }) { - const oauthSignerUri = this.trello.$auth.oauth_signer_uri; - - const trelloParams = [ - "name", - "defaultLabels", - "defaultLists", - "desc", - "idOrganization", - "idBoardSource", - "keepFromSource", - "powerUps", - "prefs_permissionLevel", - "prefs_voting", - "prefs_comments", - "prefs_invitations", - "prefs_selfJoin", - "prefs_cardCovers", - "prefs_background", - "prefs_cardAging", - ]; - let p = this; - - const queryString = trelloParams.filter((param) => p[param]).map((param) => `${param}=${p[param]}`) - .join("&"); - - const config = { - url: `https://api.trello.com/1/boards?${queryString}`, - method: "POST", - data: "", - }; + const { + app, + name, + defaultLabels, + defaultLists, + desc, + idOrganization, + idBoardSource, + keepFromSource, + powerUps, + prefsPermissionLevel, + prefsVoting, + prefsComments, + prefsInvitations, + prefsSelfJoin, + prefsCardCovers, + prefsBackground, + prefsCardAging, + } = this; - const token = { - key: this.trello.$auth.oauth_access_token, - secret: this.trello.$auth.oauth_refresh_token, - }; + const response = await app.createBoard({ + $, + params: { + name, + defaultLabels, + defaultLists, + desc, + idOrganization, + idBoardSource, + keepFromSource, + powerUps, + prefs_permissionLevel: prefsPermissionLevel, + prefs_voting: prefsVoting, + prefs_comments: prefsComments, + prefs_invitations: prefsInvitations, + prefs_selfJoin: prefsSelfJoin, + prefs_cardCovers: prefsCardCovers, + prefs_background: prefsBackground, + prefs_cardAging: prefsCardAging, + }, + }); - const signConfig = { - token, - oauthSignerUri, - }; + $.export("$summary", `Successfully created board with ID \`${response.id}\`.`); - const resp = await axios($, config, signConfig); - return resp; + return response; }, }; diff --git a/components/trello/actions/create-card/create-card.mjs b/components/trello/actions/create-card/create-card.mjs index b7980bc498325..fddb8544440a2 100644 --- a/components/trello/actions/create-card/create-card.mjs +++ b/components/trello/actions/create-card/create-card.mjs @@ -1,23 +1,26 @@ -import common from "../common.mjs"; +import app from "../../trello.app.mjs"; +import fs from "fs"; +import FormData from "form-data"; +import { ConfigurationError } from "@pipedream/platform"; +import constants from "../../common/constants.mjs"; export default { - ...common, key: "trello-create-card", name: "Create Card", - description: "Creates a new card. [See the docs here](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-post)", - version: "0.0.2", + description: "Creates a new card. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-post).", + version: "0.1.1", type: "action", props: { - ...common.props, + app, board: { propDefinition: [ - common.props.trello, + app, "board", ], }, name: { propDefinition: [ - common.props.trello, + app, "name", ], description: "The name of the card.", @@ -25,31 +28,32 @@ export default { }, desc: { propDefinition: [ - common.props.trello, + app, "desc", ], }, pos: { propDefinition: [ - common.props.trello, + app, "pos", ], + description: "The position of the new card. `top`, `bottom`, or a positive float", }, due: { propDefinition: [ - common.props.trello, + app, "due", ], }, dueComplete: { propDefinition: [ - common.props.trello, + app, "dueComplete", ], }, idList: { propDefinition: [ - common.props.trello, + app, "lists", (c) => ({ board: c.board, @@ -62,7 +66,7 @@ export default { }, idMembers: { propDefinition: [ - common.props.trello, + app, "member", (c) => ({ board: c.board, @@ -75,7 +79,7 @@ export default { }, idLabels: { propDefinition: [ - common.props.trello, + app, "label", (c) => ({ board: c.board, @@ -86,28 +90,38 @@ export default { description: "Array of labelIDs to add to the card", optional: true, }, + fileType: { + propDefinition: [ + app, + "fileType", + ], + optional: true, + reloadProps: true, + }, urlSource: { propDefinition: [ - common.props.trello, + app, "url", ], - optional: true, + hidden: true, }, - fileSource: { - type: "string", - label: "File Attachment Contents", - description: "Value must be in binary format", - optional: true, + file: { + propDefinition: [ + app, + "file", + ], + hidden: true, }, mimeType: { propDefinition: [ - common.props.trello, + app, "mimeType", ], + hidden: true, }, idCardSource: { propDefinition: [ - common.props.trello, + app, "cards", (c) => ({ board: c.board, @@ -115,42 +129,103 @@ export default { ], type: "string", label: "Copy Card", - description: "Specify an existing card to copy contents from", + description: "Specify an existing card to copy contents from. Keep in mind that if you copy a card, the **File Attachment Path**, **File Attachment URL** and **File Attachment Type** fields will be ignored.", + reloadProps: true, }, keepFromSource: { type: "string[]", label: "Copy From Source", description: "Specify which properties to copy from the source card", - options: [ - "all", - "attachments", - "checklists", - "comments", - "due", - "labels", - "members", - "stickers", - ], + options: constants.CARD_KEEP_FROM_SOURCE_PROPERTIES, optional: true, + hidden: true, }, address: { propDefinition: [ - common.props.trello, + app, "address", ], }, locationName: { propDefinition: [ - common.props.trello, + app, "locationName", ], }, coordinates: { propDefinition: [ - common.props.trello, + app, "coordinates", ], }, + customFieldIds: { + propDefinition: [ + app, + "customFieldIds", + (c) => ({ + boardId: c.board, + }), + ], + reloadProps: true, + }, + }, + async additionalProps(existingProps) { + const props = {}; + + const attachmentIsPath = this.fileType === "path"; + const attachmentIsUrl = this.fileType === "url"; + existingProps.file.hidden = !attachmentIsPath; + existingProps.mimeType.hidden = !attachmentIsPath; + existingProps.urlSource.hidden = !attachmentIsUrl; + + existingProps.keepFromSource.hidden = !this.idCardSource; + + if (!this.customFieldIds?.length) { + return props; + } + for (const customFieldId of this.customFieldIds) { + const customField = await this.app.getCustomField({ + customFieldId, + }); + props[customFieldId] = { + type: "string", + label: `Value for Custom Field - ${customField.name}`, + }; + if (customField.type === "list") { + props[customFieldId].options = customField.options?.map((option) => ({ + value: option.id, + label: option.value.text, + })) || []; + } + } + return props; + }, + methods: { + async getCustomFieldItems($) { + const customFieldItems = []; + for (const customFieldId of this.customFieldIds) { + const customField = await this.app.getCustomField({ + $, + customFieldId, + }); + const customFieldItem = { + idCustomField: customFieldId, + }; + if (customField.type === "list") { + customFieldItem.idValue = this[customFieldId]; + } else if (customField.type === "checkbox") { + customFieldItem.value = { + checked: this[customFieldId], + }; + } else { + customFieldItem.value = { + [customField.type]: this[customFieldId], + }; + } + customFieldItems.push(customFieldItem); + } + return customFieldItems; + }, }, async run({ $ }) { const { @@ -163,15 +238,18 @@ export default { idMembers, idLabels, urlSource, - fileSource, mimeType, + file, idCardSource, keepFromSource, address, locationName, coordinates, + customFieldIds, } = this; - const res = await this.trello.createCard({ + + let response; + const params = { name, desc, pos, @@ -180,16 +258,52 @@ export default { idList, idMembers, idLabels, - urlSource, - fileSource, mimeType, idCardSource, keepFromSource: keepFromSource?.join(","), address, locationName, coordinates, - }, $); - $.export("$summary", `Successfully created card ${res.id}`); - return res; + }; + + if (file && !file?.startsWith("/tmp")) { + throw new ConfigurationError("The file path must be in the `/tmp` directory"); + } + + if (file) { + const form = new FormData(); + form.append("fileSource", fs.createReadStream(file)); + + response = await this.app.createCard({ + $, + params, + headers: form.getHeaders(), + data: form, + }); + } else { + response = await this.app.createCard({ + $, + params: { + ...params, + urlSource, + }, + }); + } + + if (customFieldIds) { + const customFieldItems = await this.getCustomFieldItems($); + const customFields = await this.app.updateCustomFields({ + $, + cardId: response.id, + data: { + customFieldItems, + }, + }); + response.customFields = customFields; + } + + $.export("$summary", `Successfully created card \`${response.id}\`.`); + + return response; }, }; diff --git a/components/trello/actions/create-checklist-item/create-checklist-item.mjs b/components/trello/actions/create-checklist-item/create-checklist-item.mjs index 3f32fdf4a8061..cf8175c518072 100644 --- a/components/trello/actions/create-checklist-item/create-checklist-item.mjs +++ b/components/trello/actions/create-checklist-item/create-checklist-item.mjs @@ -1,67 +1,82 @@ -// legacy_hash_id: a_bKiP4j -import { axios } from "@pipedream/platform"; +import app from "../../trello.app.mjs"; export default { key: "trello-create-checklist-item", name: "Create a Checklist Item", - description: "Creates a new checklist item in a card.", - version: "0.1.2", + description: "Creates a new checklist item in a card. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-checklists/#api-checklists-id-checkitems-post).", + version: "0.2.1", type: "action", props: { - trello: { - type: "app", - app: "trello", + app, + board: { + propDefinition: [ + app, + "board", + ], }, - id: { + card: { + propDefinition: [ + app, + "cards", + ({ board }) => ({ + board, + checklistCardsOnly: true, + }), + ], type: "string", + label: "Card", + description: "The ID of the Card that the checklist item should be added to", + optional: false, + }, + checklistId: { + label: "Checklist ID", description: "ID of a checklist.", + propDefinition: [ + app, + "checklist", + ({ card }) => ({ + card, + }), + ], }, name: { type: "string", + label: "Name", description: "The name of the new check item on the checklist. Should be a string of length 1 to 16384.", }, pos: { - type: "string", - description: "The position of the check item in the checklist. One of: top, bottom, or a positive number.", - optional: true, + propDefinition: [ + app, + "pos", + ], }, checked: { type: "boolean", + label: "Checked", description: "Determines whether the check item is already checked when created.", optional: true, }, }, async run({ $ }) { - const oauthSignerUri = this.trello.$auth.oauth_signer_uri; - - let id = this.id; - const trelloParams = [ - "name", - "pos", - "checked", - ]; - let p = this; - - const queryString = trelloParams.filter((param) => p[param]).map((param) => `${param}=${p[param]}`) - .join("&"); - - const config = { - url: `https://api.trello.com/1/checklists/${id}/checkItems?${queryString}`, - method: "POST", - data: "", - }; + const { + checklistId, + name, + pos, + checked, + } = this; - const token = { - key: this.trello.$auth.oauth_access_token, - secret: this.trello.$auth.oauth_refresh_token, - }; + const response = await this.app.createChecklistItem({ + $, + checklistId, + params: { + name, + pos, + checked, + }, + }); - const signConfig = { - token, - oauthSignerUri, - }; + $.export("$summary", `Successfully created a checklist item with ID: ${response.id}`); - const resp = await axios($, config, signConfig); - return resp; + return response; }, }; diff --git a/components/trello/actions/create-checklist/create-checklist.mjs b/components/trello/actions/create-checklist/create-checklist.mjs deleted file mode 100644 index cc2c9891b2642..0000000000000 --- a/components/trello/actions/create-checklist/create-checklist.mjs +++ /dev/null @@ -1,66 +0,0 @@ -import common from "../common.mjs"; - -export default { - ...common, - key: "trello-create-checklist", - name: "Create Checklist", - description: "Creates a checklist on the specified card. [See the docs here](https://developer.atlassian.com/cloud/trello/rest/api-group-checklists/#api-checklists-post)", - version: "0.0.2", - type: "action", - props: { - ...common.props, - board: { - propDefinition: [ - common.props.trello, - "board", - ], - }, - idCard: { - propDefinition: [ - common.props.trello, - "cards", - (c) => ({ - board: c.board, - }), - ], - type: "string", - label: "Card", - description: "The ID of the Card that the checklist should be added to", - optional: false, - }, - name: { - propDefinition: [ - common.props.trello, - "name", - ], - description: "The name of the checklist. Should be a string of length 1 to 16384.", - }, - pos: { - propDefinition: [ - common.props.trello, - "pos", - ], - description: "The position of the new checklist. Valid values: `top`, `bottom`, or a positive float.", - }, - idChecklistSource: { - propDefinition: [ - common.props.trello, - "checklist", - (c) => ({ - board: c.board, - }), - ], - }, - }, - async run({ $ }) { - const opts = { - idCard: this.idCard, - name: this.name, - pos: this.pos, - idChecklistSource: this.idChecklistSource, - }; - const res = await this.trello.createChecklist(opts, $); - $.export("$summary", `Successfully created checklist ${res.name}`); - return res; - }, -}; diff --git a/components/trello/actions/create-comment-on-card/create-comment-on-card.mjs b/components/trello/actions/create-comment-on-card/create-comment-on-card.mjs deleted file mode 100644 index 616f080c19352..0000000000000 --- a/components/trello/actions/create-comment-on-card/create-comment-on-card.mjs +++ /dev/null @@ -1,42 +0,0 @@ -import common from "../common.mjs"; - -export default { - ...common, - key: "trello-create-comment-on-card", - name: "Create Comment on Card", - description: "Creates a new comment on a card. [See the docs here](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-actions-comments-post)", - version: "0.0.2", - type: "action", - props: { - ...common.props, - board: { - propDefinition: [ - common.props.trello, - "board", - ], - }, - idCard: { - propDefinition: [ - common.props.trello, - "cards", - (c) => ({ - board: c.board, - }), - ], - type: "string", - label: "Card", - description: "The ID of the card to create a new comment on", - optional: false, - }, - comment: { - type: "string", - label: "Comment", - description: "Text for the comment to be created.", - }, - }, - async run({ $ }) { - const res = await this.trello.createCommentOnCard(this.idCard, this.comment, $); - $.export("$summary", `Successfully added comment to card ${this.idCard}`); - return res; - }, -}; diff --git a/components/trello/actions/create-label/create-label.mjs b/components/trello/actions/create-label/create-label.mjs index 2f5b3cbc8c775..8e109639f8069 100644 --- a/components/trello/actions/create-label/create-label.mjs +++ b/components/trello/actions/create-label/create-label.mjs @@ -1,73 +1,61 @@ -// legacy_hash_id: a_l0iLRL -import { axios } from "@pipedream/platform"; +import app from "../../trello.app.mjs"; +import constants from "../../common/constants.mjs"; export default { key: "trello-create-label", name: "Create Label", - description: "Creates a new label on the specified board.", - version: "0.1.2", + description: "Creates a new label on the specified board. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-labels/#api-labels-post).", + version: "0.2.1", type: "action", props: { - trello: { - type: "app", - app: "trello", + app, + idBoard: { + type: "string", + label: "Board ID", + description: "The ID of the board to create the label on.", + propDefinition: [ + app, + "board", + ], }, name: { type: "string", + label: "Name", description: "Name for the label.", }, color: { type: "string", + label: "Color", description: "The color for the label. One of: yellow, purple, blue, red, green, orange, black, sky, pink, lime, null (null means no color, and the label will not show on the front of cards)", - options: [ - "yellow", - "purple", - "blue", - "red", - "green", - "orange", - "black", - "sky", - "pink", - "lime", - "null", - ], + options: constants.LABEL_COLORS, }, - idBoard: { - type: "string", - description: "The ID of the board to create the label on.", + }, + methods: { + createLabel(args = {}) { + return this.app.post({ + path: "/labels", + ...args, + }); }, }, async run({ $ }) { - const oauthSignerUri = this.trello.$auth.oauth_signer_uri; - - const trelloParams = [ - "name", - "color", - "idBoard", - ]; - let p = this; - - const queryString = trelloParams.filter((param) => p[param]).map((param) => `${param}=${p[param]}`) - .join("&"); - - const config = { - url: `https://api.trello.com/1/labels?${queryString}`, - method: "POST", - data: "", - }; + const { + idBoard, + name, + color, + } = this; - const token = { - key: this.trello.$auth.oauth_access_token, - secret: this.trello.$auth.oauth_refresh_token, - }; + const response = await this.app.createLabel({ + $, + params: { + idBoard, + name, + color, + }, + }); - const signConfig = { - token, - oauthSignerUri, - }; + $.export("$summary", `Successfully created label ${this.name}`); - const resp = await axios($, config, signConfig); - return resp; + return response; }, }; diff --git a/components/trello/actions/create-list/create-list.mjs b/components/trello/actions/create-list/create-list.mjs index 43a45c8c50c2e..b937590956ce0 100644 --- a/components/trello/actions/create-list/create-list.mjs +++ b/components/trello/actions/create-list/create-list.mjs @@ -1,67 +1,68 @@ -// legacy_hash_id: a_G1iBG7 -import { axios } from "@pipedream/platform"; +import app from "../../trello.app.mjs"; export default { key: "trello-create-list", name: "Create a List", - description: "Creates a new list on a board", - version: "0.1.2", + description: "Creates a new list. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-lists/#api-lists-post).", + version: "0.2.1", type: "action", props: { - trello: { - type: "app", - app: "trello", + app, + idBoard: { + type: "string", + label: "Board ID", + description: "The long ID of the board the list should be created on.", + propDefinition: [ + app, + "board", + ], }, name: { type: "string", + label: "Name", description: "Name for the list.", }, - idBoard: { - type: "string", - description: "The long ID of the board the list should be created on.", - }, idListSource: { type: "string", + label: "List Source ID", description: "ID of the list to copy into the new list.", optional: true, + propDefinition: [ + app, + "lists", + ({ idBoard }) => ({ + board: idBoard, + }), + ], }, pos: { - type: "string", - description: "Position of the list. top, bottom, or a positive floating point number.", - optional: true, + propDefinition: [ + app, + "pos", + ], + description: "Position of the list. `top`, `bottom`, or a positive floating point number", }, }, async run({ $ }) { - const oauthSignerUri = this.trello.$auth.oauth_signer_uri; - - const trelloParams = [ - "name", - "idBoard", - "idListSource", - "pos", - ]; - let p = this; - - const queryString = trelloParams.filter((param) => p[param]).map((param) => `${param}=${p[param]}`) - .join("&"); - - const config = { - url: `https://api.trello.com/1/lists?${queryString}`, - method: "POST", - data: "", - }; + const { + name, + idBoard, + idListSource, + pos, + } = this; - const token = { - key: this.trello.$auth.oauth_access_token, - secret: this.trello.$auth.oauth_refresh_token, - }; + const response = await this.app.createList({ + $, + params: { + name, + idBoard, + idListSource, + pos, + }, + }); - const signConfig = { - token, - oauthSignerUri, - }; + $.export("$summary", `Successfully created list ${this.name}`); - const resp = await axios($, config, signConfig); - return resp; + return response; }, }; diff --git a/components/trello/actions/delete-checklist/delete-checklist.mjs b/components/trello/actions/delete-checklist/delete-checklist.mjs index 205ef1c6308ce..582991e88559f 100644 --- a/components/trello/actions/delete-checklist/delete-checklist.mjs +++ b/components/trello/actions/delete-checklist/delete-checklist.mjs @@ -1,26 +1,26 @@ -import common from "../common.mjs"; +import app from "../../trello.app.mjs"; export default { - ...common, key: "trello-delete-checklist", name: "Delete Checklist", - description: "Deletes the specified checklist. [See the docs here](https://developer.atlassian.com/cloud/trello/rest/api-group-checklists/#api-checklists-id-delete)", - version: "0.1.3", + description: "Deletes the specified checklist. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-checklists/#api-checklists-id-delete).", + version: "0.2.1", type: "action", props: { - ...common.props, + app, board: { propDefinition: [ - common.props.trello, + app, "board", ], }, - idCard: { + carId: { propDefinition: [ - common.props.trello, + app, "cards", (c) => ({ board: c.board, + checklistCardsOnly: true, }), ], type: "string", @@ -28,19 +28,22 @@ export default { description: "The ID of the card containing the checklist do delete", optional: false, }, - idChecklist: { + checklistId: { propDefinition: [ - common.props.trello, + app, "checklist", - (c) => ({ - card: c.idCard, + ({ carId }) => ({ + card: carId, }), ], description: "The ID of the checklist to delete", }, }, async run({ $ }) { - await this.trello.deleteChecklist(this.idChecklist, $); - $.export("$summary", `Successfully deleted checklist ${this.idChecklist}`); + await this.app.deleteChecklist({ + $, + checklistId: this.checklistId, + }); + $.export("$summary", `Successfully deleted checklist ${this.checklistId}`); }, }; diff --git a/components/trello/actions/find-labels/find-labels.mjs b/components/trello/actions/find-labels/find-labels.mjs index 296e78165bb34..6964482a90056 100644 --- a/components/trello/actions/find-labels/find-labels.mjs +++ b/components/trello/actions/find-labels/find-labels.mjs @@ -1,42 +1,44 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; export default { ...common, key: "trello-find-labels", name: "Find a Label", - description: "Finds a label on a specific board by name. [See the docs here](https://developer.atlassian.com/cloud/trello/rest/api-group-boards/#api-boards-id-labels-get)", - version: "0.1.3", + description: "Finds a label on a specific board by name. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-boards/#api-boards-id-labels-get)", + version: "0.2.1", type: "action", props: { ...common.props, board: { propDefinition: [ - common.props.trello, + common.props.app, "board", ], description: "Unique identifier of the board to search for labels", }, name: { propDefinition: [ - common.props.trello, + common.props.app, "name", ], label: "Label Name", description: "Name of the label to find.", - optional: false, }, - labelLimit: { + limit: { type: "integer", - label: "Results", + label: "Limit", description: "The number of labels to be returned (up to 1000)", default: 50, }, }, async run({ $ }) { - const opts = { - limit: this.labelLimit, - }; - const labels = await this.trello.findLabel(this.board, opts, $); + const labels = await this.app.findLabel({ + $, + boardId: this.board, + params: { + limit: this.limit, + }, + }); const res = this.getMatches(labels, this.name); $.export("$summary", `Successfully retrieved ${res.length} label(s) with name ${this.name}`); return res; diff --git a/components/trello/actions/find-list/find-list.mjs b/components/trello/actions/find-list/find-list.mjs index 24c8d5053dcfe..181941af53909 100644 --- a/components/trello/actions/find-list/find-list.mjs +++ b/components/trello/actions/find-list/find-list.mjs @@ -1,24 +1,24 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; export default { ...common, key: "trello-find-list", name: "Find a List", - description: "Finds a list on a specific board by name. [See the docs here](https://developer.atlassian.com/cloud/trello/rest/api-group-boards/#api-boards-id-lists-get)", - version: "0.1.3", + description: "Finds a list on a specific board by name. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-boards/#api-boards-id-lists-get).", + version: "0.2.1", type: "action", props: { ...common.props, board: { propDefinition: [ - common.props.trello, + common.props.app, "board", ], description: "Specify the board to search for lists", }, name: { propDefinition: [ - common.props.trello, + common.props.app, "name", ], label: "List Name", @@ -27,7 +27,7 @@ export default { }, listFilter: { propDefinition: [ - common.props.trello, + common.props.app, "listFilter", ], }, @@ -38,10 +38,13 @@ export default { name, listFilter, } = this; - const opts = { - filter: listFilter, - }; - const lists = await this.trello.findList(board, opts, $); + const lists = await this.app.getLists({ + $, + boardId: board, + params: { + filter: listFilter, + }, + }); const res = this.getMatches(lists, name); $.export("$summary", `Successfully retrieved ${res.length} list(s) with name ${name}`); return res; diff --git a/components/trello/actions/get-card/get-card.mjs b/components/trello/actions/get-card/get-card.mjs index 286ba9e3b918f..001a1cacab555 100644 --- a/components/trello/actions/get-card/get-card.mjs +++ b/components/trello/actions/get-card/get-card.mjs @@ -1,23 +1,23 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; export default { ...common, key: "trello-get-card", name: "Get Card", - description: "Gets a card by its ID. [See the docs here](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-get)", - version: "0.1.6", + description: "Gets a card by its ID. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-get).", + version: "0.2.1", type: "action", props: { ...common.props, board: { propDefinition: [ - common.props.trello, + common.props.app, "board", ], }, cardId: { propDefinition: [ - common.props.trello, + common.props.app, "cards", (c) => ({ board: c.board, @@ -28,9 +28,21 @@ export default { description: "The ID of the card to get details of", optional: false, }, + customFieldItems: { + propDefinition: [ + common.props.app, + "customFieldItems", + ], + }, }, async run({ $ }) { - const res = await this.trello.getCard(this.cardId, $); + const res = await this.app.getCard({ + $, + cardId: this.cardId, + params: { + customFieldItems: this.customFieldItems, + }, + }); $.export("$summary", `Successfully retrieved card ${this.cardId}`); return res; }, diff --git a/components/trello/actions/get-list/get-list.mjs b/components/trello/actions/get-list/get-list.mjs index aebe42053c82a..ba7e28c7dd569 100644 --- a/components/trello/actions/get-list/get-list.mjs +++ b/components/trello/actions/get-list/get-list.mjs @@ -1,32 +1,46 @@ -import { axios } from "@pipedream/platform"; +import app from "../../trello.app.mjs"; export default { key: "trello-get-list", name: "Get List", - description: "Get information about a List", - version: "0.0.3", + description: "Get information about a List. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-lists/#api-lists-id-get).", + version: "0.1.1", type: "action", props: { - // eslint-disable-next-line pipedream/props-label, pipedream/props-description - trello: { - type: "app", - app: "trello", + app, + boardId: { + propDefinition: [ + app, + "board", + ], }, listId: { type: "string", label: "List ID", description: "The ID of the Trello list", + optional: false, + propDefinition: [ + app, + "lists", + ({ boardId }) => ({ + board: boardId, + }), + ], }, }, async run({ $ }) { - return await axios($, { - url: `https://api.trello.com/1/lists/${this.listId}`, - }, { - token: { - key: this.trello.$auth.oauth_access_token, - secret: this.trello.$auth.oauth_refresh_token, - }, - oauthSignerUri: this.trello.$auth.oauth_signer_uri, + const { + app, + listId, + } = this; + + const response = await app.getList({ + $, + listId, }); + + $.export("$summary", `Successfully retrieved list ${listId}.`); + + return response; }, }; diff --git a/components/trello/actions/move-card-to-list/move-card-to-list.mjs b/components/trello/actions/move-card-to-list/move-card-to-list.mjs index 10f4136009738..955a81949c248 100644 --- a/components/trello/actions/move-card-to-list/move-card-to-list.mjs +++ b/components/trello/actions/move-card-to-list/move-card-to-list.mjs @@ -1,23 +1,23 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; export default { ...common, key: "trello-move-card-to-list", name: "Move Card to List", - description: "Moves a card to the specified board/list pair. [See the docs here](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-put)", - version: "0.1.3", + description: "Moves a card to the specified board/list pair. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-put).", + version: "0.2.1", type: "action", props: { ...common.props, board: { propDefinition: [ - common.props.trello, + common.props.app, "board", ], }, - idCard: { + cardId: { propDefinition: [ - common.props.trello, + common.props.app, "cards", (c) => ({ board: c.board, @@ -28,9 +28,9 @@ export default { description: "The ID of the card to move", optional: false, }, - toIdList: { + idList: { propDefinition: [ - common.props.trello, + common.props.app, "lists", (c) => ({ board: c.board, @@ -43,11 +43,15 @@ export default { }, }, async run({ $ }) { - const res = await this.trello.moveCardToList(this.idCard, { - idBoard: this.board, - idList: this.toIdList, - }, $); - $.export("$summary", `Successfully moved card ${this.idCard} to list ${this.toIdList}`); + const res = await this.app.updateCard({ + $, + cardId: this.cardId, + data: { + idBoard: this.board, + idList: this.idList, + }, + }); + $.export("$summary", `Successfully moved card ${this.cardId} to list ${this.idList}`); return res; }, }; diff --git a/components/trello/actions/remove-label-from-card/remove-label-from-card.mjs b/components/trello/actions/remove-label-from-card/remove-label-from-card.mjs index d738bf9c73080..ed42c8ba921b5 100644 --- a/components/trello/actions/remove-label-from-card/remove-label-from-card.mjs +++ b/components/trello/actions/remove-label-from-card/remove-label-from-card.mjs @@ -1,23 +1,22 @@ -import common from "../common.mjs"; +import app from "../../trello.app.mjs"; export default { - ...common, key: "trello-remove-label-from-card", name: "Remove Card Label", - description: "Removes label from card. [See the docs here](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-idlabels-idlabel-delete)", - version: "0.1.3", + description: "Removes label from card. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-idlabels-idlabel-delete).", + version: "0.2.1", type: "action", props: { - ...common.props, + app, board: { propDefinition: [ - common.props.trello, + app, "board", ], }, - idCard: { + cardId: { propDefinition: [ - common.props.trello, + app, "cards", (c) => ({ board: c.board, @@ -28,20 +27,28 @@ export default { description: "The ID of the Card to remove the Label from", optional: false, }, - idLabel: { + labelId: { propDefinition: [ - common.props.trello, + app, "label", (c) => ({ board: c.board, + card: c.cardId, + cardLabelsOnly: true, }), ], description: "The ID of the Label to be removed from the card.", }, }, async run({ $ }) { - const res = await this.trello.removeLabelFromCard(this.idCard, this.idLabel, $); - $.export("$summary", `Successfully removed label ${this.idLabel} from card ${this.idCard}`); - return res; + await this.app.removeLabelFromCard({ + $, + cardId: this.cardId, + labelId: this.labelId, + }); + $.export("$summary", "Successfully sent request to remove label from card."); + return { + success: true, + }; }, }; diff --git a/components/trello/actions/rename-list/rename-list.mjs b/components/trello/actions/rename-list/rename-list.mjs index 8b0a006fb90a4..dc2040505f735 100644 --- a/components/trello/actions/rename-list/rename-list.mjs +++ b/components/trello/actions/rename-list/rename-list.mjs @@ -1,23 +1,23 @@ -import common from "../common.mjs"; +import common from "../common/common.mjs"; export default { ...common, key: "trello-rename-list", name: "Rename List", - description: "Renames an existing list. [See the docs here](https://developer.atlassian.com/cloud/trello/rest/api-group-lists/#api-lists-id-put)", - version: "0.0.2", + description: "Renames an existing list. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-lists/#api-lists-id-put).", + version: "0.1.1", type: "action", props: { ...common.props, board: { propDefinition: [ - common.props.trello, + common.props.app, "board", ], }, listId: { propDefinition: [ - common.props.trello, + common.props.app, "lists", (c) => ({ board: c.board, @@ -30,18 +30,32 @@ export default { }, name: { propDefinition: [ - common.props.trello, + common.props.app, "name", ], description: "The new name of the list", optional: false, }, }, + methods: { + renameList({ + listId, ...args + } = {}) { + return this.app.put({ + path: `/lists/${listId}`, + ...args, + }); + }, + }, async run({ $ }) { - const res = await this.trello.renameList(this.listId, { - name: this.name, - }, $); - $.export("$summary", `Successfully renamed list to ${this.name}`); + const res = await this.renameList({ + $, + listId: this.listId, + data: { + name: this.name, + }, + }); + $.export("$summary", `Successfully renamed list to \`${this.name}\`.`); return res; }, }; diff --git a/components/trello/actions/search-boards/search-boards.mjs b/components/trello/actions/search-boards/search-boards.mjs index da3aa728abeaf..abb6023e68dee 100644 --- a/components/trello/actions/search-boards/search-boards.mjs +++ b/components/trello/actions/search-boards/search-boards.mjs @@ -1,56 +1,60 @@ -import common from "../common.mjs"; +import app from "../../trello.app.mjs"; export default { - ...common, key: "trello-search-boards", name: "Search Boards", - description: "Searches for boards matching the specified query. [See the docs here](https://developer.atlassian.com/cloud/trello/rest/api-group-search/#api-search-get)", - version: "0.2.3", + description: "Searches for boards matching the specified query. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-search/#api-search-get).", + version: "0.3.1", type: "action", props: { - ...common.props, + app, query: { propDefinition: [ - common.props.trello, + app, "query", ], }, idOrganizations: { propDefinition: [ - common.props.trello, + app, "idOrganizations", ], description: "Specify the organizations to search for boards in", }, partial: { propDefinition: [ - common.props.trello, + app, "partial", ], + optional: true, }, boardFields: { propDefinition: [ - common.props.trello, + app, "boardFields", ], + optional: true, }, boardsLimit: { type: "integer", label: "Boards Limit", description: "The maximum number of boards to return.", default: 10, + optional: true, }, }, async run({ $ }) { - const opts = { - query: this.query, - idOrganizations: this.idOrganizations, - modelTypes: "boards", - board_fields: this.boardFields.join(","), - boards_limit: this.boardsLimit, - partial: this.partial, - }; - const { boards } = await this.trello.searchBoards(opts, $); + const { boards } = await this.app.search({ + $, + params: { + query: this.query, + idOrganizations: this.idOrganizations?.join(","), + modelTypes: "boards", + board_fields: this.boardFields.join(","), + boards_limit: this.boardsLimit, + partial: this.partial, + }, + }); $.export("$summary", `Successfully retrieved ${boards.length} board(s)`); return boards; }, diff --git a/components/trello/actions/search-cards/search-cards.mjs b/components/trello/actions/search-cards/search-cards.mjs index 0e6a0b37e2e04..da542c35f787b 100644 --- a/components/trello/actions/search-cards/search-cards.mjs +++ b/components/trello/actions/search-cards/search-cards.mjs @@ -1,58 +1,63 @@ -import common from "../common.mjs"; +import app from "../../trello.app.mjs"; export default { - ...common, key: "trello-search-cards", name: "Search Cards", - description: "Searches for cards matching the specified query. [See the docs here](https://developer.atlassian.com/cloud/trello/rest/api-group-search/#api-search-get)", - version: "0.1.3", + description: "Searches for cards matching the specified query. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-search/#api-search-get).", + version: "0.2.1", type: "action", props: { - ...common.props, + app, query: { propDefinition: [ - common.props.trello, + app, "query", ], }, idBoards: { propDefinition: [ - common.props.trello, + app, "board", ], type: "string[]", label: "Boards", description: "The IDs of boards to search for cards in", + optional: true, }, partial: { propDefinition: [ - common.props.trello, + app, "partial", ], + optional: true, }, cardFields: { propDefinition: [ - common.props.trello, + app, "cardFields", ], + optional: true, }, cardsLimit: { type: "integer", label: "Cards Limit", description: "The maximum number of cards to return.", default: 10, + optional: true, }, }, async run({ $ }) { - const opts = { - query: this.query, - idBoards: this.idBoards, - modelTypes: "cards", - card_fields: this.cardFields, - cards_limit: this.cardsLimit, - partial: this.partial, - }; - const { cards } = await this.trello.searchCards(opts, $); + const { cards } = await this.app.search({ + $, + params: { + query: this.query, + idBoards: this.idBoards, + modelTypes: "cards", + card_fields: this.cardFields, + cards_limit: this.cardsLimit, + partial: this.partial, + }, + }); $.export("$summary", `Successfully retrieved ${cards.length} card(s)`); return cards; }, diff --git a/components/trello/actions/search-checklists/search-checklists.mjs b/components/trello/actions/search-checklists/search-checklists.mjs index 1e42d4c7bbf47..7e8fac84be9b2 100644 --- a/components/trello/actions/search-checklists/search-checklists.mjs +++ b/components/trello/actions/search-checklists/search-checklists.mjs @@ -1,110 +1,147 @@ -// legacy_hash_id: a_1WiqM5 -import { axios } from "@pipedream/platform"; +import app from "../../trello.app.mjs"; export default { key: "trello-search-checklists", - name: "Find Checklist", - description: "Find a checklist on a particular board or card by name.", - version: "0.1.2", + name: "Search Checklists", + description: "Find a checklist on a particular board or card by name. [See the documentation here](https://developer.atlassian.com/cloud/trello/rest/api-group-boards/#api-boards-id-checklists-get) and [here](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-checklists-get).", + version: "0.2.1", type: "action", props: { - trello: { - type: "app", - app: "trello", - }, + app, type: { type: "string", + label: "Type", description: "Whether to search boards or cards for the checklist.", options: [ "board", "card", ], + reloadProps: true, + }, + boardId: { + propDefinition: [ + app, + "board", + ], + hidden: true, }, - id: { + cardId: { + propDefinition: [ + app, + "cards", + (c) => ({ + board: c.boardId, + }), + ], type: "string", - description: "The ID of the board or card.", + label: "Card ID", + description: "The ID of the card", + hidden: true, }, query: { type: "string", - description: "The query to search for.", + label: "Query", + description: "The query to search for", }, checkItems: { type: "string", - description: "all or none", + label: "Check Items", + description: "Whether to display checklist items in the result. Select `all` or `none`.", optional: true, options: [ "all", "none", ], + reloadProps: true, + hidden: true, }, - checkItem_fields: { - type: "string", + checkItemFields: { + type: "string[]", label: "CheckItem Fields", - description: "all or a comma-separated list of: name, nameData, pos, state, type", - optional: true, - }, - filter: { - type: "string", - description: "all or none", - optional: true, + description: "Fields to include in the results. `all` or a list of: `name`, `nameData`, `pos`, `state`, `type`", options: [ "all", - "none", + "name", + "nameData", + "pos", + "state", + "type", ], + optional: true, + hidden: true, }, fields: { - type: "string", - description: "all or a comma-separated list of: idBoard, idCard, name, pos", + type: "string[]", + label: "Fields", + description: "Fields to include in the results. `all` or a list of: `idBoard`, `idCard`, `name`, `pos`. Eg: `idBoard,idCard,name,pos`. Eg: `all`.", + options: [ + "all", + "idBoard", + "idCard", + "name", + "pos", + ], optional: true, + hidden: true, }, }, + additionalProps(props) { + const isCardSearch = this.type === "card"; + props.boardId.hidden = false; + props.cardId.hidden = !isCardSearch; + props.checkItems.hidden = !isCardSearch; + props.checkItemFields.hidden = !isCardSearch; + props.fields.hidden = !isCardSearch; + + props.checkItemFields.hidden = !(this.checkItems === "all"); + + return {}; + }, async run({ $ }) { - let type = this.type; - let id = this.id; - let query = this.query; - const trelloParams = [ - "checkItems", - "checkItem_fields", - "filter", - "fields", - ]; - let p = this; + const { + app, + type, + boardId, + cardId, + query, + checkItems, + checkItemFields, + fields, + } = this; + let checklists = null; let matches = []; - const queryString = trelloParams.filter((param) => p[param]).map((param) => `${param}=${p[param]}`) - .join("&"); - - if (type == "board") { - checklists = await axios($, { - url: `https://api.trello.com/1/boards/${id}/checklists?${queryString}`, - method: "GET", - }, { - token: { - key: this.trello.$auth.oauth_access_token, - secret: this.trello.$auth.oauth_refresh_token, - }, - oauthSignerUri: this.trello.$auth.oauth_signer_uri, + if (type === "board") { + checklists = await app.listBoardChecklists({ + $, + boardId, }); - } else if (type == "card") { - checklists = await axios($, { - url: `https://api.trello.com/1/cards/${id}/checklists?${queryString}`, - method: "GET", - }, { - token: { - key: this.trello.$auth.oauth_access_token, - secret: this.trello.$auth.oauth_refresh_token, + + } else if (type === "card") { + checklists = await app.listCardChecklists({ + $, + cardId, + params: { + checkItems, + checkItem_fields: checkItemFields?.length && checkItemFields.join(), + fields: fields?.length && fields.join(), }, - oauthSignerUri: this.trello.$auth.oauth_signer_uri, }); } + if (checklists) { - checklists.forEach(function(checklist) { - if (checklist.name.includes(query)) + checklists.forEach((checklist) => { + if (checklist.name?.toLowerCase().includes(query.toLowerCase())) matches.push(checklist); }); } + if (matches?.length) { + $.export("$summary", `Found ${matches.length} matching checklist${matches.length === 1 + ? "" + : "s"}`); + } return matches; }, }; diff --git a/components/trello/actions/search-members/search-members.mjs b/components/trello/actions/search-members/search-members.mjs index da4e556d42a1d..8e8aeada74d78 100644 --- a/components/trello/actions/search-members/search-members.mjs +++ b/components/trello/actions/search-members/search-members.mjs @@ -1,61 +1,71 @@ -// legacy_hash_id: a_8KiV84 -import { axios } from "@pipedream/platform"; +import app from "../../trello.app.mjs"; export default { key: "trello-search-members", name: "Search Members", - description: "Search for Trello members.", - version: "0.1.2", + description: "Search for Trello members. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-search/#api-search-members-get).", + version: "0.2.1", type: "action", props: { - trello: { - type: "app", - app: "trello", - }, + app, query: { type: "string", + label: "Search Query", description: "Search query 1 to 16384 characters long", }, - limit: { - type: "integer", - description: "The maximum number of results to return. Maximum of 20.", - optional: true, - }, idBoard: { - type: "string", + label: "Board ID", + description: "The ID of the board to search for members.", optional: true, + propDefinition: [ + app, + "board", + ], }, idOrganization: { type: "string", + label: "Organization ID", + description: "The ID of the organization to search for members.", optional: true, + propDefinition: [ + app, + "idOrganizations", + ], }, - onlyOrgMembers: { - type: "boolean", + limit: { + type: "integer", + label: "Limit", + description: "The maximum number of results to return. Maximum of 20.", optional: true, }, }, async run({ $ }) { - const trelloParams = [ - "query", - "limit", - "idBoard", - "idOrganization", - "onlyOrgMembers", - ]; - let p = this; + const { + query, + limit, + idBoard, + idOrganization, + } = this; - const queryString = trelloParams.filter((param) => p[param]).map((param) => `${param}=${p[param]}`) - .join("&"); + const onlyOrgMembers = idBoard || idOrganization; - return await axios($, { - url: `https://api.trello.com/1/search/members?${queryString}`, - method: "GET", - }, { - token: { - key: this.trello.$auth.oauth_access_token, - secret: this.trello.$auth.oauth_refresh_token, + const response = await this.app.searchMembers({ + $, + params: { + query, + limit, + idBoard, + idOrganization, + onlyOrgMembers, }, - oauthSignerUri: this.trello.$auth.oauth_signer_uri, }); + + if (response?.length) { + $.export("$summary", `Successfully found ${response.length} member${response.length === 1 + ? "" + : "s"}`); + } + + return response; }, }; diff --git a/components/trello/actions/update-card/update-card.mjs b/components/trello/actions/update-card/update-card.mjs index fa094c39cd0a2..bc3a6e1f2a761 100644 --- a/components/trello/actions/update-card/update-card.mjs +++ b/components/trello/actions/update-card/update-card.mjs @@ -1,25 +1,24 @@ -import common from "../common.mjs"; -import pickBy from "lodash.pickby"; -import pick from "lodash.pick"; +import app from "../../trello.app.mjs"; +import pickBy from "lodash-es/pickBy.js"; +import pick from "lodash-es/pick.js"; export default { - ...common, key: "trello-update-card", name: "Update Card", - description: "Updates a card. [See the docs here](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-put)", - version: "0.1.3", + description: "Updates a card. [See the documentation](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-put).", + version: "0.2.1", type: "action", props: { - ...common.props, + app, idBoard: { propDefinition: [ - common.props.trello, + app, "board", ], }, - idCard: { + cardId: { propDefinition: [ - common.props.trello, + app, "cards", (c) => ({ board: c.idBoard, @@ -32,14 +31,14 @@ export default { }, name: { propDefinition: [ - common.props.trello, + app, "name", ], description: "The new name for the card.", }, desc: { propDefinition: [ - common.props.trello, + app, "desc", ], description: "The new description for the card.", @@ -49,10 +48,11 @@ export default { label: "Archived", description: "Whether to archive the card", default: false, + optional: true, }, idMembers: { propDefinition: [ - common.props.trello, + app, "member", (c) => ({ board: c.idBoard, @@ -64,15 +64,20 @@ export default { optional: true, }, idAttachmentCover: { - type: "string", - label: "Cover", - description: - "Assign an attachment to be the cover image for the card", - optional: true, + propDefinition: [ + app, + "cardAttachmentId", + ({ cardId }) => ({ + cardId, + params: { + filter: "cover", + }, + }), + ], }, idList: { propDefinition: [ - common.props.trello, + app, "lists", (c) => ({ board: c.idBoard, @@ -84,7 +89,7 @@ export default { }, idLabels: { propDefinition: [ - common.props.trello, + app, "label", (c) => ({ board: c.idBoard, @@ -97,19 +102,20 @@ export default { }, pos: { propDefinition: [ - common.props.trello, + app, "pos", ], + description: "The position of the new card. `top`, `bottom`, or a positive float", }, due: { propDefinition: [ - common.props.trello, + app, "due", ], }, dueComplete: { propDefinition: [ - common.props.trello, + app, "dueComplete", ], description: "Whether the due date should be marked complete.", @@ -118,47 +124,123 @@ export default { subscribed: { type: "boolean", label: "Subscribed", - description: "Whether the member is should be subscribed to the card.", + description: "Whether the member should be subscribed to the card.", default: false, + optional: true, }, address: { propDefinition: [ - common.props.trello, + app, "address", ], }, locationName: { propDefinition: [ - common.props.trello, + app, "locationName", ], }, coordinates: { propDefinition: [ - common.props.trello, + app, "coordinates", ], }, + customFieldIds: { + propDefinition: [ + app, + "customFieldIds", + (c) => ({ + boardId: c.idBoard, + }), + ], + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + if (!this.customFieldIds?.length) { + return props; + } + for (const customFieldId of this.customFieldIds) { + const customField = await this.app.getCustomField({ + customFieldId, + }); + props[customFieldId] = { + type: "string", + label: `Value for Custom Field - ${customField.name}`, + }; + if (customField.type === "list") { + props[customFieldId].options = customField.options?.map((option) => ({ + value: option.id, + label: option.value.text, + })) || []; + } + } + return props; + }, + methods: { + async getCustomFieldItems($) { + const customFieldItems = []; + for (const customFieldId of this.customFieldIds) { + const customField = await this.app.getCustomField({ + $, + customFieldId, + }); + const customFieldItem = { + idCustomField: customFieldId, + }; + if (customField.type === "list") { + customFieldItem.idValue = this[customFieldId]; + } else if (customField.type === "checkbox") { + customFieldItem.value = { + checked: this[customFieldId], + }; + } else { + customFieldItem.value = { + [customField.type]: this[customFieldId], + }; + } + customFieldItems.push(customFieldItem); + } + return customFieldItems; + }, }, async run({ $ }) { - const opts = pickBy(pick(this, [ - "name", - "desc", - "closed", - "idMembers", - "idAttachmentCover", - "idList", - "idLabels", - "idBoard", - "pos", - "due", - "dueComplete", - "subscribed", - "address", - "locationName", - "coordinates", - ])); - const res = await this.trello.updateCard(this.idCard, opts, $); + const res = await this.app.updateCard({ + $, + cardId: this.cardId, + data: pickBy(pick(this, [ + "name", + "desc", + "closed", + "idMembers", + "idAttachmentCover", + "idList", + "idLabels", + "idBoard", + "pos", + "due", + "dueComplete", + "subscribed", + "address", + "locationName", + "coordinates", + ])), + }); + + if (this.customFieldIds) { + const customFieldItems = await this.getCustomFieldItems($); + const updatedCustomFields = await this.app.updateCustomFields({ + $, + cardId: this.cardId, + data: { + customFieldItems, + }, + }); + res.updatedCustomFields = updatedCustomFields; + } + $.export("$summary", `Successfully updated card ${res.name}`); return res; }, diff --git a/components/trello/common/constants.mjs b/components/trello/common/constants.mjs new file mode 100644 index 0000000000000..3af98f23e14a2 --- /dev/null +++ b/components/trello/common/constants.mjs @@ -0,0 +1,128 @@ +const POSITIONS = [ + "top", + "bottom", +]; + +const CARD_FILTERS = [ + "all", + "closed", + "none", + "open", + "visible", +]; + +const LIST_FILTERS = [ + "all", + "closed", + "none", + "open", +]; + +const POWER_UPS = [ + "all", + "calendar", + "cardAging", + "recap", + "voting", +]; + +const PREFS_PERMISSION_LEVELS = [ + "org", + "private", + "public", +]; + +const PREFS_VOTING = [ + "disabled", + "members", + "observers", + "org", + "public", +]; + +const PREFS_COMMENTS = [ + "disabled", + "members", + "observers", + "org", + "public", +]; + +const PREFS_INVITATIONS = [ + "admins", + "members", +]; + +const PREFS_BACKGROUNDS = [ + "blue", + "orange", + "green", + "red", + "purple", + "pink", + "lime", + "sky", + "grey", +]; + +const PREFS_CARD_AGING = [ + "pirate", + "regular", +]; + +const LABEL_COLORS = [ + "yellow", + "purple", + "blue", + "red", + "green", + "orange", + "black", + "sky", + "pink", + "lime", + "null", +]; + +const NOTIFICATION_TIMES = [ + "5 minutes", + "10 minutes", + "15 minutes", + "30 minutes", + "1 hour", + "2 hours", + "3 hours", + "6 hours", + "12 hours", + "1 day", + "2 days", + "3 days", + "1 week", +]; + +const CARD_KEEP_FROM_SOURCE_PROPERTIES = [ + "all", + "attachments", + "checklists", + "comments", + "due", + "labels", + "members", + "stickers", +]; + +export default { + POSITIONS, + CARD_FILTERS, + LIST_FILTERS, + POWER_UPS, + PREFS_PERMISSION_LEVELS, + PREFS_VOTING, + PREFS_COMMENTS, + PREFS_INVITATIONS, + PREFS_BACKGROUNDS, + PREFS_CARD_AGING, + LABEL_COLORS, + NOTIFICATION_TIMES, + CARD_KEEP_FROM_SOURCE_PROPERTIES, +}; diff --git a/components/trello/common/fields.mjs b/components/trello/common/fields.mjs index 7aea4568b90ab..678968a99d370 100644 --- a/components/trello/common/fields.mjs +++ b/components/trello/common/fields.mjs @@ -1,5 +1,6 @@ export default { board: [ + "all", "closed", "dateLastActivity", "dateLastView", diff --git a/components/trello/package.json b/components/trello/package.json index d7d0868b1e064..e2930002d5276 100644 --- a/components/trello/package.json +++ b/components/trello/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/trello", - "version": "0.3.12", + "version": "0.4.1", "description": "Pipedream Trello Components", "main": "trello.app.mjs", "keywords": [ @@ -10,11 +10,12 @@ "homepage": "https://pipedream.com/apps/trello", "author": "Pipedream (https://pipedream.com/)", "dependencies": { - "@pipedream/platform": "^1.5.1", + "@pipedream/platform": "^3.0.1", "crypto": "^1.0.1", - "lodash.pick": "^4.4.0", - "lodash.pickby": "^4.6.0", - "mime": "^3.0.0" + "form-data": "^4.0.0", + "lodash-es": "^4.17.21", + "mime": "^4.0.4", + "ms": "^2.1.3" }, "gitHead": "e12480b94cc03bed4808ebc6b13e7fdb3a1ba535", "publishConfig": { diff --git a/components/trello/sources/card-archived/card-archived.mjs b/components/trello/sources/card-archived/card-archived.mjs index bd5ebe68e8305..f896f86c02785 100644 --- a/components/trello/sources/card-archived/card-archived.mjs +++ b/components/trello/sources/card-archived/card-archived.mjs @@ -1,54 +1,58 @@ import common from "../common/common-webhook.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "trello-card-archived", - name: "Card Archived (Instant)", + name: "Card Archived (Instant)", /* eslint-disable-line pipedream/source-name */ description: "Emit new event for each card archived.", - version: "0.0.12", + version: "0.1.1", type: "source", props: { ...common.props, board: { propDefinition: [ - common.props.trello, + common.props.app, "board", ], }, lists: { propDefinition: [ - common.props.trello, + common.props.app, "lists", (c) => ({ board: c.board, }), ], + description: "If specified, events will only be emitted when a card in one of the selected lists is archived", }, }, methods: { ...common.methods, - async getSampleEvents() { - const cards = await this.trello.getFilteredCards(this.board, "closed"); - return { - sampleEvents: cards, - sortField: "dateLastActivity", - }; + getSampleEvents() { + return this.app.getFilteredCards({ + boardId: this.board, + filter: "closed", + }); }, - isCorrectEventType(event) { - const eventTranslationKey = event.body?.action?.display?.translationKey; - return eventTranslationKey === "action_archived_card"; + getSortField() { + return "dateLastActivity"; }, - async getResult(event) { - const cardId = event.body?.action?.data?.card?.id; - return this.trello.getCard(cardId); + isCorrectEventType({ display }) { + return display?.translationKey === "action_archived_card"; + }, + getResult({ data }) { + return this.app.getCard({ + cardId: data?.card?.id, + }); }, isRelevant({ result: card }) { return ( (!this.board || this.board === card.idBoard) && - (!this.lists || - this.lists.length === 0 || + (!this.lists?.length || this.lists.includes(card.idList)) ); }, }, + sampleEmit, }; diff --git a/components/trello/sources/card-archived/test-event.mjs b/components/trello/sources/card-archived/test-event.mjs new file mode 100644 index 0000000000000..df2857886721c --- /dev/null +++ b/components/trello/sources/card-archived/test-event.mjs @@ -0,0 +1,64 @@ +export default { + "id": "5f4ee830046a92139596173a", + "badges": { + "attachmentsByType": { + "trello": { + "board": 0, + "card": 0 + } + }, + "externalSource": null, + "location": false, + "votes": 0, + "viewingMemberVoted": false, + "subscribed": false, + "fogbugz": "", + "checkItems": 0, + "checkItemsChecked": 0, + "checkItemsEarliestDue": null, + "comments": 0, + "attachments": 0, + "description": false, + "due": null, + "dueComplete": false, + "start": null, + "lastUpdatedByAi": false + }, + "checkItemStates": [], + "closed": true, + "dueComplete": false, + "dateLastActivity": "2020-09-02T00:34:22.602Z", + "desc": "", + "descData": null, + "due": null, + "dueReminder": null, + "email": null, + "idBoard": "5f4d7be6c45c22583f75fa02", + "idChecklists": [], + "idList": "5f4d7f78bdd7ce4d2d25fdda", + "idMembers": [], + "idMembersVoted": [], + "idShort": 10, + "idAttachmentCover": null, + "labels": [], + "idLabels": [], + "manualCoverAttachment": false, + "name": "a new card", + "pinned": false, + "pos": 65535, + "shortLink": "eBapV0gL", + "shortUrl": "https://trello.com/c/eBapV0gL", + "start": null, + "subscribed": false, + "url": "https://trello.com/c/eBapV0gL/10-a-new-card", + "cover": { + "idAttachment": null, + "color": null, + "idUploadedBackground": null, + "size": "normal", + "brightness": "light", + "idPlugin": null + }, + "isTemplate": false, + "cardRole": null +} \ No newline at end of file diff --git a/components/trello/sources/card-due-date-reminder/card-due-date-reminder.mjs b/components/trello/sources/card-due-date-reminder/card-due-date-reminder.mjs index 7f39a1c3298df..67dfa5eb90fb4 100644 --- a/components/trello/sources/card-due-date-reminder/card-due-date-reminder.mjs +++ b/components/trello/sources/card-due-date-reminder/card-due-date-reminder.mjs @@ -1,54 +1,69 @@ -import common from "../common/common-polling.mjs"; +import taskScheduler from "../../../pipedream/sources/new-scheduled-tasks/new-scheduled-tasks.mjs"; +import trello from "../../trello.app.mjs"; +import ms from "ms"; +import constants from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; export default { - ...common, key: "trello-card-due-date-reminder", - name: "Card Due Date Reminder", + name: "Card Due Date Reminder", /* eslint-disable-line pipedream/source-name */ description: "Emit new event at a specified time before a card is due.", - version: "0.0.9", + version: "0.1.1", type: "source", dedupe: "unique", props: { - ...common.props, + pipedream: taskScheduler.props.pipedream, + trello, + db: "$.service.db", + http: "$.interface.http", board: { propDefinition: [ - common.props.trello, + trello, "board", ], }, timeBefore: { - type: "integer", + type: "string", label: "Time Before", - description: "How far before the due time the event should trigger.", - default: 5, + description: "How far before the due time the event should trigger. For example, `5 minutes`, `10 minutes`, `1 hour`.", + default: "5 minutes", + options: constants.NOTIFICATION_TIMES, + reloadProps: true, }, - timeBeforeUnit: { - type: "integer", - label: "Time Before (Unit)", - description: "Unit of time for Time Before.", - options: [ - { - label: "Minutes", - value: 60000, - }, - { - label: "Hours", - value: 3600000, - }, - { - label: "Days", - value: 86400000, - }, - { - label: "Weeks", - value: 604800000, + }, + async additionalProps() { + const props = {}; + if (this.timeBefore) { + props.timer = { + type: "$.interface.timer", + description: "Poll the API to schedule alerts for any newly created events", + default: { + intervalSeconds: ms(this.timeBefore) / 1000, }, - ], - default: 60000, + }; + } + return props; + }, + hooks: { + async deactivate() { + const ids = this._getScheduledEventIds(); + if (!ids?.length) { + return; + } + for (const id of ids) { + if (await this.deleteEvent({ + body: { + id, + }, + })) { + console.log("Cancelled scheduled event"); + } + } + this._setScheduledEventIds(); }, }, methods: { - ...common.methods, + ...taskScheduler.methods, generateMeta({ id, name: summary, }, now) { @@ -58,23 +73,87 @@ export default { ts: now, }; }, + _getScheduledEventIds() { + return this.db.get("scheduledEventIds"); + }, + _setScheduledEventIds(ids) { + this.db.set("scheduledEventIds", ids); + }, + _getScheduledCardIds() { + return this.db.get("scheduledCardIds"); + }, + _setScheduledCardIds(ids) { + this.db.set("scheduledCardIds", ids); + }, + _hasDeployed() { + const result = this.db.get("hasDeployed"); + this.db.set("hasDeployed", true); + return result; + }, emitEvent(card, now) { const meta = this.generateMeta(card, now); this.$emit(card, meta); }, }, async run(event) { - const boardId = this.board; const now = event.timestamp * 1000; - const cards = await this.trello.getCards(boardId); + // self subscribe only on the first time + if (!this._hasDeployed()) { + await this.selfSubscribe(); + } + + let scheduledEventIds = this._getScheduledEventIds() || []; + + // incoming scheduled event + if (event.$channel === this.selfChannel()) { + const remainingScheduledEventIds = scheduledEventIds.filter((id) => id !== event["$id"]); + this._setScheduledEventIds(remainingScheduledEventIds); + this.emitEvent(event, now); + return; + } + + // schedule new events + const scheduledCardIds = this._getScheduledCardIds() || {}; + const cards = await this.trello.getCards({ + boardId: this.board, + }); + for (const card of cards) { - if (!card.due) continue; - const due = Date.parse(card.due); - const notifyAt = due - this.timeBefore * this.timeBeforeUnit; - if (notifyAt <= now) { - this.emitEvent(card, now); + const dueDate = card.due + ? new Date(card.due) + : null; + if (!dueDate || dueDate.getTime() < Date.now()) { + delete scheduledCardIds[card.id]; + continue; + } + + const later = new Date(dueDate.getTime() - ms(this.timeBefore)); + + if (scheduledCardIds[card.id]) { + // reschedule if card's due date has changed + if (card.due !== scheduledCardIds[card.id].dueDate) { + await this.deleteEvent({ + body: { + id: scheduledCardIds[card.id].eventId, + }, + }); + scheduledEventIds = scheduledEventIds + .filter((id) => id !== scheduledCardIds[card.id].eventId); + } else { + continue; + } } + + const scheduledEventId = this.emitScheduleEvent(card, later); + scheduledEventIds.push(scheduledEventId); + scheduledCardIds[card.id] = { + eventId: scheduledEventId, + dueDate: card.due, + }; } + this._setScheduledEventIds(scheduledEventIds); + this._setScheduledCardIds(scheduledCardIds); }, + sampleEmit, }; diff --git a/components/trello/sources/card-due-date-reminder/test-event.mjs b/components/trello/sources/card-due-date-reminder/test-event.mjs new file mode 100644 index 0000000000000..35b396b457bda --- /dev/null +++ b/components/trello/sources/card-due-date-reminder/test-event.mjs @@ -0,0 +1,75 @@ +export default { + "id": "61818a303e545129af23695d", + "badges": { + "attachmentsByType": { + "trello": { + "board": 0, + "card": 0 + } + }, + "externalSource": null, + "location": false, + "votes": 0, + "viewingMemberVoted": false, + "subscribed": true, + "fogbugz": "", + "checkItems": 1, + "checkItemsChecked": 1, + "checkItemsEarliestDue": null, + "comments": 2, + "attachments": 1, + "description": false, + "due": "2024-10-01T19:20:00.000Z", + "dueComplete": false, + "start": null, + "lastUpdatedByAi": false + }, + "checkItemStates": [ + { + "idCheckItem": "6181a5f18e239e6aff9799a4", + "state": "complete" + } + ], + "closed": false, + "dueComplete": false, + "dateLastActivity": "2024-10-01T19:08:51.312Z", + "desc": "", + "descData": { + "emoji": {} + }, + "due": "2024-10-01T19:20:00.000Z", + "dueReminder": -1, + "email": null, + "idBoard": "5f4d7be6c45c22583f75fa02", + "idChecklists": [ + "6181a5ef401ae66c357732b4" + ], + "idList": "5f4d7f78bdd7ce4d2d25fdda", + "idMembers": [], + "idMembersVoted": [], + "idShort": 68, + "idAttachmentCover": "62c884dce32d46579ecfd5f0", + "labels": [], + "idLabels": [], + "manualCoverAttachment": false, + "name": "test", + "pinned": false, + "pos": 131071, + "shortLink": "QVLahShU", + "shortUrl": "https://trello.com/c/QVLahShU", + "start": null, + "subscribed": true, + "url": "https://trello.com/c/QVLahShU/68-sfsfdsfd", + "cover": { + "idAttachment": "62c884dce32d46579ecfd5f0", + "color": null, + "idUploadedBackground": null, + "size": "normal", + "brightness": "dark", + "idPlugin": null + }, + "isTemplate": false, + "cardRole": null, + "$channel": "self", + "$id": "62377726-40b9-41a8-85c7-b01f3f5f8fbe" +} \ No newline at end of file diff --git a/components/trello/sources/card-moved/card-moved.mjs b/components/trello/sources/card-moved/card-moved.mjs index c710975bb7136..39fa97d34d5e0 100644 --- a/components/trello/sources/card-moved/card-moved.mjs +++ b/components/trello/sources/card-moved/card-moved.mjs @@ -1,40 +1,46 @@ import common from "../common/common-webhook.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "trello-card-moved", - name: "Card Moved (Instant)", + name: "Card Moved (Instant)", /* eslint-disable-line pipedream/source-name */ description: "Emit new event each time a card is moved to a list.", - version: "0.0.11", + version: "0.1.1", type: "source", props: { ...common.props, board: { propDefinition: [ - common.props.trello, + common.props.app, "board", ], }, lists: { propDefinition: [ - common.props.trello, + common.props.app, "lists", (c) => ({ board: c.board, }), ], + description: "If specified, events will only be emitted if a card is moved to or from one of the selected lists", }, }, methods: { ...common.methods, async getSampleEvents() { - const cards = this.lists && this.lists.length > 0 - ? await this.trello.getCardsInList(this.lists[0]) - : await this.trello.getCards(this.board); - return { - sampleEvents: cards, - sortFilter: "dateLastActivity", - }; + const cards = this.lists?.length > 0 + ? await this.app.getCardsInList({ + listId: this.lists[0], + }) + : await this.app.getCards({ + boardId: this.board, + }); + return cards; + }, + getSortField() { + return "dateLastActivity"; }, _getListAfter() { return this.db.get("listAfter"); @@ -42,35 +48,31 @@ export default { _setListAfter(listAfter) { this.db.set("listAfter", listAfter); }, - isCorrectEventType(event) { - const eventTranslationKey = event.body?.action?.display?.translationKey; - return eventTranslationKey === "action_move_card_from_list_to_list"; + isCorrectEventType({ display }) { + return display?.translationKey === "action_move_card_from_list_to_list"; }, - async getResult(event) { - const cardId = event.body?.action?.data?.card?.id; - const listAfter = event.body?.action?.data?.listAfter?.name; + getResult({ data }) { /** Record listAfter to use in generateMeta() */ - this._setListAfter(listAfter); - return this.trello.getCard(cardId); + this._setListAfter(data?.listAfter?.name); + return this.app.getCard({ + cardId: data?.card?.id, + }); }, isRelevant({ - result: card, event, + result: card, action, }) { - const listIdAfter = event.body?.action?.data?.listAfter?.id; - const listIdBefore = event.body?.action?.data?.listBefore?.id; - return ( (!this.board || this.board === card.idBoard) && - (!this.lists || - this.lists.length === 0 || - this.lists.includes(listIdAfter) || - this.lists.includes(listIdBefore)) + (!this.lists?.length || + this.lists.includes(action?.data?.listAfter?.id) || + this.lists.includes(action?.data?.listBefore?.id)) ); }, generateMeta({ id, name, }) { const listAfter = this._getListAfter(); + name = name || id; const summary = listAfter ? `${name} - moved to ${listAfter}` : name; @@ -81,4 +83,5 @@ export default { }; }, }, + sampleEmit, }; diff --git a/components/trello/sources/card-moved/test-event.mjs b/components/trello/sources/card-moved/test-event.mjs new file mode 100644 index 0000000000000..44bc71e5f5b79 --- /dev/null +++ b/components/trello/sources/card-moved/test-event.mjs @@ -0,0 +1,78 @@ +export default { + "id": "60836bac2ed2ef78f4cf850d", + "badges": { + "attachmentsByType": { + "trello": { + "board": 0, + "card": 0 + } + }, + "externalSource": null, + "location": false, + "votes": 0, + "viewingMemberVoted": false, + "subscribed": true, + "fogbugz": "", + "checkItems": 1, + "checkItemsChecked": 0, + "checkItemsEarliestDue": null, + "comments": 3, + "attachments": 1, + "description": false, + "due": "2021-05-10T21:50:00.000Z", + "dueComplete": false, + "start": null, + "lastUpdatedByAi": false + }, + "checkItemStates": [], + "closed": false, + "dueComplete": false, + "dateLastActivity": "2021-11-02T23:02:44.876Z", + "desc": "", + "descData": null, + "due": "2021-05-10T21:50:00.000Z", + "dueReminder": 1440, + "email": null, + "idBoard": "5f4d7be6c45c22583f75fa02", + "idChecklists": [ + "608b2037ea7ac07da59c17be" + ], + "idList": "5f4ec9f69028408fed04c0b1", + "idMembers": [], + "idMembersVoted": [], + "idShort": 54, + "idAttachmentCover": "6099aece406c0b7a97e71535", + "labels": [ + { + "id": "608b2273586147300cc73046", + "idBoard": "5f4d7be6c45c22583f75fa02", + "idOrganization": "6047c17c2f558003144e04d7", + "name": "orange", + "nodeId": "ari:cloud:trello::label/workspace/6047c17c2f558003144e04d7/608b2273586147300cc73046", + "color": "orange", + "uses": 4 + }, + ], + "idLabels": [ + "608b2273586147300cc73046", + ], + "manualCoverAttachment": false, + "name": "new card", + "pinned": false, + "pos": 655359, + "shortLink": "fo2GH2y2", + "shortUrl": "https://trello.com/c/fo2GH2y2", + "start": null, + "subscribed": true, + "url": "https://trello.com/c/fo2GH2y2/54-new-card", + "cover": { + "idAttachment": "6099aece406c0b7a97e71535", + "color": null, + "idUploadedBackground": null, + "size": "normal", + "brightness": "dark", + "idPlugin": null + }, + "isTemplate": false, + "cardRole": null +} \ No newline at end of file diff --git a/components/trello/sources/card-updates/card-updates.mjs b/components/trello/sources/card-updates/card-updates.mjs index 57d4a6871158c..f83712527f312 100644 --- a/components/trello/sources/card-updates/card-updates.mjs +++ b/components/trello/sources/card-updates/card-updates.mjs @@ -1,23 +1,24 @@ import common from "../common/common-webhook.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "trello-card-updates", - name: "Card Updates (Instant)", + name: "Card Updated (Instant)", /* eslint-disable-line pipedream/source-name */ description: "Emit new event for each update to a Trello card.", - version: "0.0.11", + version: "0.1.1", type: "source", props: { ...common.props, board: { propDefinition: [ - common.props.trello, + common.props.app, "board", ], }, cards: { propDefinition: [ - common.props.trello, + common.props.app, "cards", (c) => ({ board: c.board, @@ -29,32 +30,45 @@ export default { ...common.methods, async getSampleEvents() { let cards = []; - if (this.cards && this.cards.length > 0) { + const params = { + customFieldItems: true, + }; + if (this.cards?.length > 0) { for (const cardId of this.cards) { - const card = await this.trello.getCard(cardId); + const card = await this.app.getCard({ + cardId, + params, + }); cards.push(card); } } else { - cards = await this.trello.getCards(this.board); + cards = await this.app.getCards({ + boardId: this.board, + params, + }); } - return { - sampleEvents: cards, - sortField: "dateLastActivity", - }; + return cards; + }, + getSortField() { + return "dateLastActivity"; }, - isCorrectEventType(event) { - const eventType = event.body?.action?.type; - return eventType === "updateCard"; + isCorrectEventType({ type }) { + return type === "updateCard"; }, - async getResult(event) { - const cardId = event.body?.action?.data?.card?.id; - return this.trello.getCard(cardId); + getResult({ data }) { + return this.app.getCard({ + cardId: data?.card?.id, + params: { + customFieldItems: true, + }, + }); }, isRelevant({ result: card }) { return ( (!this.board || this.board === card.idBoard) && - (!this.cards || this.cards.length === 0 || this.cards.includes(card.id)) + (!this.cards?.length || this.cards.includes(card.id)) ); }, }, + sampleEmit, }; diff --git a/components/trello/sources/card-updates/test-event.mjs b/components/trello/sources/card-updates/test-event.mjs new file mode 100644 index 0000000000000..e40f64d7737d3 --- /dev/null +++ b/components/trello/sources/card-updates/test-event.mjs @@ -0,0 +1,86 @@ +export default { + "id": "620eb14ebda9570d72fddead", + "badges": { + "attachmentsByType": { + "trello": { + "board": 0, + "card": 0 + } + }, + "externalSource": null, + "location": false, + "votes": 0, + "viewingMemberVoted": false, + "subscribed": false, + "fogbugz": "", + "checkItems": 1, + "checkItemsChecked": 1, + "checkItemsEarliestDue": null, + "comments": 3, + "attachments": 1, + "description": false, + "due": "2021-05-10T21:50:00.000Z", + "dueComplete": false, + "start": null, + "lastUpdatedByAi": false + }, + "checkItemStates": [ + { + "idCheckItem": "620eb14fbda9570d72fde031", + "state": "complete" + } + ], + "closed": false, + "dueComplete": false, + "dateLastActivity": "2024-09-30T16:31:35.215Z", + "desc": "", + "descData": { + "emoji": {} + }, + "due": "2021-05-10T21:50:00.000Z", + "dueReminder": null, + "email": null, + "idBoard": "5f4d7be6c45c22583f75fa02", + "idChecklists": [ + "620eb14fbda9570d72fde030" + ], + "idList": "5f4d7f78bdd7ce4d2d25fdda", + "idMembers": [], + "idMembersVoted": [], + "idShort": 85, + "idAttachmentCover": "620eb14ebda9570d72fdded7", + "labels": [ + { + "id": "5f4d7be6cdabcf46c027c792", + "idBoard": "5f4d7be6c45c22583f75fa02", + "idOrganization": "6047c17c2f558003144e04d7", + "name": "green label", + "nodeId": "ari:cloud:trello::label/workspace/6047c17c2f558003144e04d7/5f4d7be6cdabcf46c027c792", + "color": "green", + "uses": 9 + }, + ], + "idLabels": [ + "5f4d7be6cdabcf46c027c792", + ], + "manualCoverAttachment": false, + "name": "new card", + "pinned": false, + "pos": 360447, + "shortLink": "dho6bOdR", + "shortUrl": "https://trello.com/c/dho6bOdR", + "start": null, + "subscribed": false, + "url": "https://trello.com/c/dho6bOdR/85-new-card", + "cover": { + "idAttachment": "620eb14ebda9570d72fdded7", + "color": null, + "idUploadedBackground": null, + "size": "normal", + "brightness": "dark", + "idPlugin": null + }, + "isTemplate": false, + "cardRole": null, + "customFieldItems": [] +} \ No newline at end of file diff --git a/components/trello/sources/common/actions.mjs b/components/trello/sources/common/actions.mjs new file mode 100644 index 0000000000000..2f30e5e0d0950 --- /dev/null +++ b/components/trello/sources/common/actions.mjs @@ -0,0 +1,206 @@ +export default [ + { + label: "Accept Enterprise Join Request", + value: "acceptEnterpriseJoinRequest", + }, + { + label: "Add Attachment To Card", + value: "addAttachmentToCard", + }, + { + label: "Add Checklist To Card", + value: "addChecklistToCard", + }, + { + label: "Add Member To Board", + value: "addMemberToBoard", + }, + { + label: "Add Member To Card", + value: "addMemberToCard", + }, + { + label: "Add Member To Organization", + value: "addMemberToOrganization", + }, + { + label: "Add Organization To Enterprise", + value: "addOrganizationToEnterprise", + }, + { + label: "Add To Enterprise Plugin Whitelist", + value: "addToEnterprisePluginWhitelist", + }, + { + label: "Add To Organization Board", + value: "addToOrganizationBoard", + }, + { + label: "Comment Card", + value: "commentCard", + }, + { + label: "Convert To Card From Check Item", + value: "convertToCardFromCheckItem", + }, + { + label: "Copy Board", + value: "copyBoard", + }, + { + label: "Copy Card", + value: "copyCard", + }, + { + label: "Copy Comment Card", + value: "copyCommentCard", + }, + { + label: "Create Board", + value: "createBoard", + }, + { + label: "Create Card", + value: "createCard", + }, + { + label: "Create List", + value: "createList", + }, + { + label: "Create Organization", + value: "createOrganization", + }, + { + label: "Delete Board Invitation", + value: "deleteBoardInvitation", + }, + { + label: "Delete Card", + value: "deleteCard", + }, + { + label: "Delete Organization Invitation", + value: "deleteOrganizationInvitation", + }, + { + label: "Disable Enterprise Plugin Whitelist", + value: "disableEnterprisePluginWhitelist", + }, + { + label: "Disable Plugin", + value: "disablePlugin", + }, + { + label: "Disable Power Up", + value: "disablePowerUp", + }, + { + label: "Email Card", + value: "emailCard", + }, + { + label: "Enable Enterprise Plugin Whitelist", + value: "enableEnterprisePluginWhitelist", + }, + { + label: "Enable Plugin", + value: "enablePlugin", + }, + { + label: "Enable Power Up", + value: "enablePowerUp", + }, + { + label: "Make Admin Of Board", + value: "makeAdminOfBoard", + }, + { + label: "Make Normal Member Of Board", + value: "makeNormalMemberOfBoard", + }, + { + label: "Make Normal Member Of Organization", + value: "makeNormalMemberOfOrganization", + }, + { + label: "Make Observer Of Board", + value: "makeObserverOfBoard", + }, + { + label: "Member Joined Trello", + value: "memberJoinedTrello", + }, + { + label: "Move Card From Board", + value: "moveCardFromBoard", + }, + { + label: "Move Card To Board", + value: "moveCardToBoard", + }, + { + label: "Move List From Board", + value: "moveListFromBoard", + }, + { + label: "Move List To Board", + value: "moveListToBoard", + }, + { + label: "Remove Checklist From Card", + value: "removeChecklistFromCard", + }, + { + label: "Remove From Enterprise Plugin Whitelist", + value: "removeFromEnterprisePluginWhitelist", + }, + { + label: "Remove From Organization Board", + value: "removeFromOrganizationBoard", + }, + { + label: "Remove Member From Card", + value: "removeMemberFromCard", + }, + { + label: "Remove Organization From Enterprise", + value: "removeOrganizationFromEnterprise", + }, + { + label: "Unconfirmed Board Invitation", + value: "unconfirmedBoardInvitation", + }, + { + label: "Unconfirmed Organization Invitation", + value: "unconfirmedOrganizationInvitation", + }, + { + label: "Update Board", + value: "updateBoard", + }, + { + label: "Update Card", + value: "updateCard", + }, + { + label: "Update Check Item State On Card", + value: "updateCheckItemStateOnCard", + }, + { + label: "Update Checklist", + value: "updateChecklist", + }, + { + label: "Update List", + value: "updateList", + }, + { + label: "Update Member", + value: "updateMember", + }, + { + label: "Update Organization", + value: "updateOrganization", + }, +]; diff --git a/components/trello/sources/common/common-board-based.mjs b/components/trello/sources/common/common-board-based.mjs index 1dfded0235aac..a2b4de682973b 100644 --- a/components/trello/sources/common/common-board-based.mjs +++ b/components/trello/sources/common/common-board-based.mjs @@ -9,12 +9,12 @@ export default { ...base.props, board: { propDefinition: [ - base.props.trello, + base.props.app, "board", ], }, onlyEventsRelatedWithAuthenticatedUser: { - label: "Only Events Related With Me", + label: "Only Events Related To Me", description: "Only will emit events from the cards related with the authenticated user", type: "boolean", default: false, @@ -32,17 +32,24 @@ export default { return false; } - const member = await this.trello.getMember("me"); + if (this.lists?.length) { + const list = await this.app.getCardList({ + cardId: result.idCard, + }); + if (!this.lists.includes(list.id)) { + return false; + } + } + + const member = await this.app.getMember({ + memberId: "me", + }); - if ( + return !( this.onlyEventsRelatedWithAuthenticatedUser && result?.idMembers?.length && !result.idMembers.includes(member.id) - ) { - return false; - } - - return true; + ); }, }, }; diff --git a/components/trello/sources/common/common-webhook.mjs b/components/trello/sources/common/common-webhook.mjs index ecd936dd20c72..a209c1aaf8baf 100644 --- a/components/trello/sources/common/common-webhook.mjs +++ b/components/trello/sources/common/common-webhook.mjs @@ -1,3 +1,4 @@ +import { createHmac } from "crypto"; import common from "./common.mjs"; export default { @@ -8,52 +9,83 @@ export default { }, hooks: { async deploy() { - const { - sampleEvents, sortField, - } = await this.getSampleEvents(); - sampleEvents.sort((a, b) => (Date.parse(a[sortField]) > Date.parse(b[sortField])) - ? 1 - : -1); + let sampleEvents = await this.getSampleEvents(); + const sortField = this.getSortField(); + sampleEvents = this.sortItemsByDate(sampleEvents, sortField); for (const event of sampleEvents.slice(-25)) { this.emitEvent(event); } }, async activate() { const modelId = await this.getModelId(); - const { id } = await this.trello.createHook({ - id: modelId, - endpoint: this.http.endpoint, + const { id } = await this.createHook({ + params: { + idModel: modelId, + description: "Pipedream Source ID", + callbackURL: this.http.endpoint, + }, }); - this.db.set("hookId", id); + this._setHookId(id); }, async deactivate() { - const hookId = this.db.get("hookId"); - await this.trello.deleteHook({ - hookId, - }); + const hookId = this._getHookId(); + if (hookId) { + await this.deleteHook({ + hookId, + }); + } }, }, methods: { ...common.methods, + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getBase64Digest(data) { + const { secret } = this.app.getToken(); + return createHmac("sha1", secret) + .update(data) + .digest("base64"); + }, + // Do not remove the async keyword from this function + async isValidSignature({ + body, bodyRaw, headers, + }) { + const data = bodyRaw + body.webhook?.callbackURL; + const doubleHash = this.getBase64Digest(data); + const headerHash = headers["x-trello-webhook"]; + return doubleHash === headerHash; + }, + createHook(args = {}) { + return this.app.post({ + ...args, + path: "/webhooks/", + }); + }, + deleteHook({ + hookId, ...args + } = {}) { + return this.app.delete({ + ...args, + path: `/webhooks/${hookId}`, + }); + }, /** * Returns the ID of the current board selected. If no board is selected, returns * the id of the authenticated user. */ async getModelId() { - if (this.board) return this.board; - const member = await this.trello.getMember("me"); + if (this.board) { + return this.board; + } + const member = await this.app.getMember({ + memberId: "me", + }); return member.id; }, - /** - * Verifies that the event received was sent from Trello. - * @param {object} event - The event returned from a webhook - */ - verifyEvent(event) { - return ( - this.trello.verifyTrelloWebhookRequest(event, this.http.endpoint) && - event.body !== undefined - ); - }, /** * Default isCorrectEventType. Used in components to verify that the event received is * of the type that the component is watching for. @@ -68,22 +100,44 @@ export default { isRelevant() { return true; }, + getResult(action) { + return action; + }, getSampleEvents() { throw new Error("getSampleEvents not implemented"); }, + getSortField() { + throw new Error("getSortField is not implemented"); + }, }, async run(event) { - if (!this.verifyEvent(event)) { + if (event.body === undefined) { + console.log("Event body is undefined. Skipping..."); + return; + } + + if (!this.isValidSignature(event)) { console.log("The event failed the verification. Skipping..."); return; } - if (!this.isCorrectEventType(event)) return; - const result = await this.getResult(event); - if (!(await this.isRelevant({ + const { action } = event.body; + + if (!this.isCorrectEventType(action)) { + console.log("The event is not of the correct type. Skipping..."); + return; + } + + const result = await this.getResult(action); + const isRelevant = await this.isRelevant({ result, - event, - }))) return; + action, + }); + + if (!isRelevant) { + console.log("The event is not relevant. Skipping..."); + return; + } this.emitEvent(result); }, diff --git a/components/trello/sources/common/common.mjs b/components/trello/sources/common/common.mjs index 306d5d187747a..60cb5533fe07c 100644 --- a/components/trello/sources/common/common.mjs +++ b/components/trello/sources/common/common.mjs @@ -1,8 +1,8 @@ -import trello from "../../trello.app.mjs"; +import app from "../../trello.app.mjs"; export default { props: { - trello, + app, db: "$.service.db", }, methods: { @@ -13,11 +13,11 @@ export default { * @param {string} name - The name of the item of the book. */ generateMeta({ - id, name: summary, + id, name, }) { return { id, - summary, + summary: name || `${id}`, ts: Date.now(), }; }, @@ -30,5 +30,8 @@ export default { const meta = this.generateMeta(result); this.$emit(result, meta); }, + sortItemsByDate(items, sortField) { + return items.sort((a, b) => (Date.parse(a[sortField]) > Date.parse(b[sortField]))); + }, }, }; diff --git a/components/trello/common/events.mjs b/components/trello/sources/common/events.mjs similarity index 100% rename from components/trello/common/events.mjs rename to components/trello/sources/common/events.mjs diff --git a/components/trello/sources/custom-webhook-events/custom-webhook-events.mjs b/components/trello/sources/custom-webhook-events/custom-webhook-events.mjs index af24fc8bc9898..1ac3ca59b941e 100644 --- a/components/trello/sources/custom-webhook-events/custom-webhook-events.mjs +++ b/components/trello/sources/custom-webhook-events/custom-webhook-events.mjs @@ -1,29 +1,31 @@ import common from "../common/common-webhook.mjs"; +import events from "../common/events.mjs"; export default { ...common, key: "trello-custom-webhook-events", - name: "Custom Webhook Events (Instant)", + name: "Custom Webhook Events (Instant)", /* eslint-disable-line pipedream/source-name */ description: "Emit new events for activity matching a board, event types, lists and/or cards.", - version: "0.0.10", + version: "0.1.1", type: "source", props: { ...common.props, board: { propDefinition: [ - common.props.trello, + common.props.app, "board", ], }, eventTypes: { - propDefinition: [ - common.props.trello, - "eventTypes", - ], + type: "string[]", + label: "Event Types", + optional: true, + description: "Only emit events for the selected event types (e.g., `updateCard`).", + options: events, }, lists: { propDefinition: [ - common.props.trello, + common.props.app, "lists", (c) => ({ board: c.board, @@ -32,7 +34,7 @@ export default { }, cards: { propDefinition: [ - common.props.trello, + common.props.app, "cards", (c) => ({ board: c.board, @@ -40,69 +42,55 @@ export default { ], }, }, - hooks: { - ...common.hooks, - async deploy() { - const { - sampleEvents, sortField, - } = await this.getSampleEvents(); - sampleEvents.sort((a, b) => (Date.parse(a[sortField]) > Date.parse(b[sortField])) - ? 1 - : -1); - for (const action of sampleEvents.slice(-25)) { - this.emitEvent({ - action, - }); - } - }, - }, methods: { ...common.methods, - async getSampleEvents() { - const eventTypes = this.eventTypes && this.eventTypes.length > 0 + getSampleEvents() { + const eventTypes = this.eventTypes?.length ? this.eventTypes.join(",") : null; - const actions = await this.trello.getBoardActivity(this.board, eventTypes); - return { - sampleEvents: actions, - sortField: "date", - }; + return this.app.getBoardActivity({ + boardId: this.board, + params: { + filter: eventTypes, + }, + }); + }, + getSortField() { + return "date"; }, - isCorrectEventType(event) { - const eventType = event.body?.action?.type; + isCorrectEventType({ type }) { return ( - (eventType) && - (!this.eventTypes || - this.eventTypes.length === 0 || - this.eventTypes.includes(eventType)) + (type) && + (!this.eventTypes?.length || + this.eventTypes.includes(type)) ); }, - async getResult(event) { - return event.body; - }, async isRelevant({ result: body }) { let listId = body.action?.data?.list?.id; const cardId = body.action?.data?.card?.id; // If listId not returned, see if we can get it from the cardId - if (cardId && !listId) - listId = (await this.trello.getCardList(cardId)).id; + if (cardId && !listId) { + const res = await this.app.getCardList({ + cardId, + }); + listId = res.id; + } return ( - (!this.lists || - this.lists.length === 0 || + (!this.lists?.length || !listId || this.lists.includes(listId)) && - (!this.cards || this.cards.length === 0 || !cardId || this.cards.includes(cardId)) + (!this.cards?.length || !cardId || this.cards.includes(cardId)) ); }, - generateMeta({ action }) { + generateMeta(action) { const { id, - type: summary, + type, date, } = action; return { id, - summary, + summary: `New ${type} event`, ts: Date.parse(date), }; }, diff --git a/components/trello/sources/new-activity/new-activity.mjs b/components/trello/sources/new-activity/new-activity.mjs index 57df12203f5cb..194c848dffdfe 100644 --- a/components/trello/sources/new-activity/new-activity.mjs +++ b/components/trello/sources/new-activity/new-activity.mjs @@ -1,62 +1,55 @@ import common from "../common/common-webhook.mjs"; +import actions from "../common/actions.mjs"; export default { ...common, key: "trello-new-activity", - name: "New Activity (Instant)", + name: "New Board Activity (Instant)", description: "Emit new event for new activity on a board.", - version: "0.0.8", + version: "0.1.1", type: "source", props: { ...common.props, board: { propDefinition: [ - common.props.trello, + common.props.app, "board", ], }, - }, - hooks: { - ...common.hooks, - async deploy() { - const { - sampleEvents, sortField, - } = await this.getSampleEvents(); - sampleEvents.sort((a, b) => (Date.parse(a[sortField]) > Date.parse(b[sortField])) - ? 1 - : -1); - for (const action of sampleEvents.slice(-25)) { - this.emitEvent({ - action, - }); - } + activityTypes: { + type: "string[]", + label: "Activity Types", + description: "Filter incoming events by the activity type", + options: actions, + optional: true, }, }, methods: { ...common.methods, - async getSampleEvents() { - const actions = await this.trello.getBoardActivity(this.board); - return { - sampleEvents: actions, - sortField: "date", - }; + getSampleEvents() { + return this.app.getBoardActivity({ + boardId: this.board, + }); }, - async getResult(event) { - return event.body; + getSortField() { + return "date"; }, - isRelevant({ event }) { - const boardId = event.body?.action?.data?.board?.id; - return !this.board || this.board === boardId; + isRelevant({ action }) { + const { + data, type, + } = action; + return ((!this.board || this.board === data?.board?.id) + && (!this.activityTypes?.length || this.activityTypes.includes(type))); }, - generateMeta({ action }) { + generateMeta(action) { const { id, - type: summary, + type, date, } = action; return { id, - summary, + summary: `New ${type} activity`, ts: Date.parse(date), }; }, diff --git a/components/trello/sources/new-attachment/new-attachment.mjs b/components/trello/sources/new-attachment/new-attachment.mjs index ed382ebea4f63..ba77abdfe961c 100644 --- a/components/trello/sources/new-attachment/new-attachment.mjs +++ b/components/trello/sources/new-attachment/new-attachment.mjs @@ -1,60 +1,67 @@ import common from "../common/common-webhook.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "trello-new-attachment", name: "New Attachment (Instant)", - description: "Emit new event for new attachment on a board.", - version: "0.0.8", + description: "Emit new event when a new attachment is added on a board.", + version: "0.1.1", type: "source", props: { ...common.props, board: { propDefinition: [ - common.props.trello, + common.props.app, "board", ], }, - }, - hooks: { - ...common.hooks, - async deploy() { - const { - sampleEvents, sortField, - } = await this.getSampleEvents(); - sampleEvents.sort((a, b) => (Date.parse(a[sortField]) > Date.parse(b[sortField])) - ? 1 - : -1); - for (const action of sampleEvents.slice(-25)) { - this.$emit(action, { - id: action.id, - summary: action?.data?.attachment?.name, - ts: Date.parse(action.date), - }); - } + lists: { + propDefinition: [ + common.props.app, + "lists", + (c) => ({ + board: c.board, + }), + ], + description: "If specified, events will only be emitted when an attachment is added to a card in one of the specified lists", }, }, methods: { ...common.methods, async getSampleEvents() { - const actions = await this.trello.getBoardActivity(this.board, "addAttachmentToCard"); - return { - sampleEvents: actions, - sortField: "date", - }; + const cards = this.lists?.length + ? await this.app.getCardsInList({ + listId: this.lists[0], + }) + : await this.app.getCards({ + boardId: this.board, + }); + const attachments = []; + for (const card of cards) { + const cardAttachments = await this.app.listCardAttachments({ + cardId: card.id, + }); + attachments.push(...cardAttachments); + } + return attachments; + }, + getSortField() { + return "date"; }, - isCorrectEventType(event) { - const eventType = event.body?.action?.type; - return eventType === "addAttachmentToCard"; + isCorrectEventType({ type }) { + return type === "addAttachmentToCard"; }, - async getResult(event) { - const cardId = event.body?.action?.data?.card?.id; - const attachmentId = event.body?.action?.data?.attachment?.id; - return this.trello.getAttachment(cardId, attachmentId); + getResult({ data }) { + return this.app.getAttachment({ + cardId: data?.card?.id, + attachmentId: data?.attachment?.id, + }); }, - isRelevant({ event }) { - const boardId = event.body?.action?.data?.board?.id; - return !this.board || this.board === boardId; + isRelevant({ action }) { + return (!this.board || this.board === action?.data?.board?.id) + && (!this.lists?.length || this.lists.includes(action?.data?.list?.id)); }, }, + sampleEmit, }; diff --git a/components/trello/sources/new-attachment/test-event.mjs b/components/trello/sources/new-attachment/test-event.mjs new file mode 100644 index 0000000000000..c513590937437 --- /dev/null +++ b/components/trello/sources/new-attachment/test-event.mjs @@ -0,0 +1,69 @@ +export default { + "id": "608b1310a52eed3b040d98ca", + "bytes": 18820, + "date": "2021-04-29T20:12:00.760Z", + "edgeColor": "#bbac9c", + "idMember": "5df149d4fa80be39e64c9a43", + "isUpload": true, + "mimeType": "image/jpeg", + "name": "meowsalot.jpg", + "previews": [ + { + "id": "608b1311a52eed3b040d98d6", + "_id": "608b1311a52eed3b040d98d6", + "scaled": false, + "url": "", + "bytes": 1649, + "height": 50, + "width": 70 + }, + { + "id": "608b1311a52eed3b040d98d7", + "_id": "608b1311a52eed3b040d98d7", + "scaled": false, + "url": "", + "bytes": 9598, + "height": 150, + "width": 250 + }, + { + "id": "608b1311a52eed3b040d98d8", + "_id": "608b1311a52eed3b040d98d8", + "scaled": true, + "url": "", + "bytes": 4508, + "height": 98, + "width": 150 + }, + { + "id": "608b1311a52eed3b040d98d9", + "_id": "608b1311a52eed3b040d98d9", + "scaled": true, + "url": "", + "bytes": 13736, + "height": 195, + "width": 300 + }, + { + "id": "608b1311a52eed3b040d98da", + "_id": "608b1311a52eed3b040d98da", + "scaled": true, + "url": "", + "bytes": 20289, + "height": 260, + "width": 400 + }, + { + "id": "608b1311a52eed3b040d98db", + "_id": "608b1311a52eed3b040d98db", + "scaled": false, + "url": "", + "bytes": 18820, + "height": 260, + "width": 400 + } + ], + "url": "", + "pos": 16384, + "fileName": "meowsalot.jpg" +} \ No newline at end of file diff --git a/components/trello/sources/new-board/new-board.mjs b/components/trello/sources/new-board/new-board.mjs index 9a08daf1d36a6..147da5547d22d 100644 --- a/components/trello/sources/new-board/new-board.mjs +++ b/components/trello/sources/new-board/new-board.mjs @@ -1,29 +1,38 @@ import common from "../common/common-webhook.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "trello-new-board", name: "New Board (Instant)", description: "Emit new event for each new board added.", - version: "0.0.12", + version: "0.1.1", type: "source", dedupe: "unique", methods: { ...common.methods, async getSampleEvents() { - const boards = await this.trello.getBoards(); - return { - sampleEvents: boards, - sortField: "dateLastView", - }; + const boards = await this.app.getBoards(); + const allBoards = []; + for (const board of boards) { + const b = await this.app.getBoard({ + boardId: board.id, + }); + allBoards.push(b); + } + return allBoards; }, - isCorrectEventType(event) { - const eventType = event.body?.action?.type; - return eventType === "createBoard"; + getSortField() { + return "dateLastView"; }, - async getResult(event) { - const boardId = event.body?.action?.data?.board?.id; - return this.trello.getBoard(boardId); + isCorrectEventType({ type }) { + return type === "createBoard"; + }, + getResult({ data }) { + return this.app.getBoard({ + boardId: data?.board?.id, + }); }, }, + sampleEmit, }; diff --git a/components/trello/sources/new-board/test-event.mjs b/components/trello/sources/new-board/test-event.mjs new file mode 100644 index 0000000000000..8ba2d6bd2397d --- /dev/null +++ b/components/trello/sources/new-board/test-event.mjs @@ -0,0 +1,100 @@ +export default { + "id": "608b147275ba1f27e956a696", + "name": "New Board", + "desc": "", + "descData": null, + "closed": true, + "idOrganization": "6047c17c2f558003144e04d7", + "idEnterprise": null, + "pinned": false, + "url": "https://trello.com/b/RoxOjLDb/new-board", + "shortUrl": "https://trello.com/b/RoxOjLDb", + "prefs": { + "permissionLevel": "org", + "hideVotes": false, + "voting": "disabled", + "comments": "members", + "invitations": "members", + "selfJoin": true, + "cardCovers": true, + "cardCounts": false, + "isTemplate": false, + "cardAging": "regular", + "calendarFeedEnabled": false, + "hiddenPluginBoardButtons": [], + "switcherViews": [ + { + "viewType": "Board", + "enabled": true + }, + ], + "background": "608af97d7a474e8d98c2eef2", + "backgroundColor": null, + "backgroundImage": "", + "backgroundTile": false, + "backgroundBrightness": "dark", + "sharedSourceUrl": "", + "backgroundImageScaled": [ + { + "width": 140, + "height": 79, + "url": "" + }, + { + "width": 256, + "height": 144, + "url": "" + }, + { + "width": 480, + "height": 270, + "url": "" + }, + { + "width": 960, + "height": 540, + "url": "" + }, + { + "width": 1024, + "height": 576, + "url": "" + }, + { + "width": 1280, + "height": 720, + "url": "" + }, + { + "width": 1920, + "height": 1080, + "url": "" + }, + { + "width": 2048, + "height": 1152, + "url": "" + }, + { + "width": 2560, + "height": 1440, + "url": "" + } + ], + "backgroundBottomColor": "#231a15", + "backgroundTopColor": "#626667", + "canBePublic": true, + "canBeEnterprise": true, + "canBeOrg": true, + "canBePrivate": true, + "canInvite": true + }, + "labelNames": { + "green": "", + "yellow": "", + "orange": "", + "red": "", + "purple": "", + "blue": "", + } +} \ No newline at end of file diff --git a/components/trello/sources/new-card/new-card.mjs b/components/trello/sources/new-card/new-card.mjs index 0c495c3c29f07..5da07a2005014 100644 --- a/components/trello/sources/new-card/new-card.mjs +++ b/components/trello/sources/new-card/new-card.mjs @@ -1,58 +1,72 @@ import common from "../common/common-webhook.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "trello-new-card", name: "New Card (Instant)", description: "Emit new event for each new Trello card on a board.", - version: "0.0.11", + version: "0.1.1", type: "source", dedupe: "unique", props: { ...common.props, board: { propDefinition: [ - common.props.trello, + common.props.app, "board", ], }, lists: { propDefinition: [ - common.props.trello, + common.props.app, "lists", (c) => ({ board: c.board, }), ], + description: "If specified, events will only be emitted when a card is created in one of the specified lists", }, }, methods: { ...common.methods, async getSampleEvents() { - const cards = this.lists && this.lists.length > 0 - ? await this.trello.getCardsInList(this.lists[0]) - : await this.trello.getCards(this.board); - return { - sampleEvents: cards, - sortField: "dateLastActivity", + const params = { + customFieldItems: true, }; + const cards = this.lists?.length + ? await this.app.getCardsInList({ + listId: this.lists[0], + params, + }) + : await this.app.getCards({ + boardId: this.board, + params, + }); + return cards; }, - isCorrectEventType(event) { - const eventType = event.body?.action?.type; - return eventType === "createCard"; + getSortField() { + return "dateLastActivity"; }, - async getResult(event) { - const cardId = event.body?.action?.data?.card?.id; - return this.trello.getCard(cardId); + isCorrectEventType({ type }) { + return type === "createCard"; + }, + getResult({ data }) { + return this.app.getCard({ + cardId: data?.card?.id, + params: { + customFieldItems: true, + }, + }); }, isRelevant({ result: card }) { if (this.board && this.board !== card.idBoard) return false; return ( (!this.board || this.board === card.idBoard) && - (!this.lists || - this.lists.length === 0 || + (!this.lists?.length || this.lists.includes(card.idList)) ); }, }, + sampleEmit, }; diff --git a/components/trello/sources/new-card/test-event.mjs b/components/trello/sources/new-card/test-event.mjs new file mode 100644 index 0000000000000..7be076b9b8709 --- /dev/null +++ b/components/trello/sources/new-card/test-event.mjs @@ -0,0 +1,79 @@ +export default { + "id": "60836bac2ed2ef78f4cf850d", + "badges": { + "attachmentsByType": { + "trello": { + "board": 0, + "card": 0 + } + }, + "externalSource": null, + "location": false, + "votes": 0, + "viewingMemberVoted": false, + "subscribed": true, + "fogbugz": "", + "checkItems": 1, + "checkItemsChecked": 0, + "checkItemsEarliestDue": null, + "comments": 3, + "attachments": 1, + "description": false, + "due": "2021-05-10T21:50:00.000Z", + "dueComplete": false, + "start": null, + "lastUpdatedByAi": false + }, + "checkItemStates": [], + "closed": false, + "dueComplete": false, + "dateLastActivity": "2021-11-02T23:02:44.876Z", + "desc": "", + "descData": null, + "due": "2021-05-10T21:50:00.000Z", + "dueReminder": 1440, + "email": null, + "idBoard": "5f4d7be6c45c22583f75fa02", + "idChecklists": [ + "608b2037ea7ac07da59c17be" + ], + "idList": "5f4ec9f69028408fed04c0b1", + "idMembers": [], + "idMembersVoted": [], + "idShort": 54, + "idAttachmentCover": "6099aece406c0b7a97e71535", + "labels": [ + { + "id": "608b2273586147300cc73046", + "idBoard": "5f4d7be6c45c22583f75fa02", + "idOrganization": "6047c17c2f558003144e04d7", + "name": "orange", + "nodeId": "ari:cloud:trello::label/workspace/6047c17c2f558003144e04d7/608b2273586147300cc73046", + "color": "orange", + "uses": 4 + }, + ], + "idLabels": [ + "608b2273586147300cc73046", + ], + "manualCoverAttachment": false, + "name": "new card", + "pinned": false, + "pos": 655359, + "shortLink": "fo2GH2y2", + "shortUrl": "https://trello.com/c/fo2GH2y2", + "start": null, + "subscribed": true, + "url": "https://trello.com/c/fo2GH2y2/54-new-card", + "cover": { + "idAttachment": "6099aece406c0b7a97e71535", + "color": null, + "idUploadedBackground": null, + "size": "normal", + "brightness": "dark", + "idPlugin": null + }, + "isTemplate": false, + "cardRole": null, + "customFieldItems": [] +} \ No newline at end of file diff --git a/components/trello/sources/new-checklist/new-checklist.mjs b/components/trello/sources/new-checklist/new-checklist.mjs index 7b408f239c7d2..afc6291eac325 100644 --- a/components/trello/sources/new-checklist/new-checklist.mjs +++ b/components/trello/sources/new-checklist/new-checklist.mjs @@ -1,29 +1,58 @@ import common from "../common/common-board-based.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "trello-new-checklist", name: "New Checklist (Instant)", description: "Emit new event for each new checklist added to a board.", - version: "0.0.13", + version: "0.1.1", type: "source", dedupe: "unique", + props: { + ...common.props, + board: { + propDefinition: [ + common.props.app, + "board", + ], + }, + lists: { + propDefinition: [ + common.props.app, + "lists", + (c) => ({ + board: c.board, + }), + ], + description: "If specified, events will only be emitted when a checklist is added to a card in one of the specified lists", + }, + onlyEventsRelatedWithAuthenticatedUser: { + label: "Only Events Related To Me", + description: "Only will emit events from the cards related with the authenticated user", + type: "boolean", + default: false, + optional: true, + }, + }, methods: { ...common.methods, - async getSampleEvents() { - const checklists = await this.trello.listBoardChecklists(this.board); - return { - sampleEvents: checklists, - sortField: "id", - }; + getSampleEvents() { + return this.app.listBoardChecklists({ + boardId: this.board, + }); + }, + getSortField() { + return "id"; }, - isCorrectEventType(event) { - const eventType = event.body?.action?.type; - return eventType === "addChecklistToCard"; + isCorrectEventType({ type }) { + return type === "addChecklistToCard"; }, - async getResult(event) { - const checklistId = event.body?.action?.data?.checklist?.id; - return this.trello.getChecklist(checklistId); + getResult({ data }) { + return this.app.getChecklist({ + checklistId: data?.checklist?.id, + }); }, }, + sampleEmit, }; diff --git a/components/trello/sources/new-checklist/test-event.mjs b/components/trello/sources/new-checklist/test-event.mjs new file mode 100644 index 0000000000000..65dee447bd69c --- /dev/null +++ b/components/trello/sources/new-checklist/test-event.mjs @@ -0,0 +1,22 @@ +export default { + "id": "6181a5c4b843ed8eb8fc1482", + "name": "Checklist", + "idBoard": "5f4d7be6c45c22583f75fa02", + "idCard": "6099aff7ab0cc929f640e06e", + "pos": 16384, + "checkItems": [ + { + "id": "6181a5c685e54137db6951f5", + "name": "item", + "nameData": { + "emoji": {} + }, + "pos": 17027, + "state": "complete", + "due": null, + "dueReminder": null, + "idMember": null, + "idChecklist": "6181a5c4b843ed8eb8fc1482" + } + ] +} \ No newline at end of file diff --git a/components/trello/sources/new-comment-added-to-card/new-comment-added-to-card.mjs b/components/trello/sources/new-comment-added-to-card/new-comment-added-to-card.mjs index dbea1594452f7..2debee96fde93 100644 --- a/components/trello/sources/new-comment-added-to-card/new-comment-added-to-card.mjs +++ b/components/trello/sources/new-comment-added-to-card/new-comment-added-to-card.mjs @@ -1,84 +1,98 @@ import common from "../common/common-webhook.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "trello-new-comment-added-to-card", name: "New Comment Added to Card (Instant)", description: "Emit new event for each new comment added to a card.", - version: "0.1.1", + version: "0.2.1", type: "source", dedupe: "unique", props: { ...common.props, board: { propDefinition: [ - common.props.trello, + common.props.app, "board", ], }, + list: { + propDefinition: [ + common.props.app, + "lists", + (c) => ({ + board: c.board, + }), + ], + type: "string", + label: "List", + description: "If specified, events will only be emitted when a comment is added to a card in the specified list", + }, cards: { propDefinition: [ - common.props.trello, + common.props.app, "cards", (c) => ({ board: c.board, + list: c.list, }), ], - }, - }, - hooks: { - ...common.hooks, - async deploy() { - const { - sampleEvents, sortField, - } = await this.getSampleEvents(); - sampleEvents.sort((a, b) => (Date.parse(a[sortField]) > Date.parse(b[sortField])) - ? 1 - : -1); - for (const action of sampleEvents.slice(-25)) { - this.emitEvent(action); - } + description: "If specified, events will only be emitted when a comment is added to one of the specified cards", }, }, methods: { ...common.methods, async getSampleEvents() { - const cards = this.cards && this.cards.length > 0 + const cards = this.cards?.length ? this.cards - : (await this.trello.getCards(this.board)).map((card) => card.id); + : this.list + ? (await this.app.getCardsInList({ + listId: this.list, + })).map((card) => card.id) + : (await this.app.getCards({ + boardId: this.board, + })).map((card) => card.id); const actions = []; for (const card of cards) { - const activities = await this.trello.getCardActivity(card, "commentCard"); + const activities = await this.app.getCardActivity({ + cardId: card, + params: { + filter: "commentCard", + }, + }); for (const activity of activities) { actions.push(await this.getResult(activity)); } } - return { - sampleEvents: actions, - sortField: "date", - }; + return actions; + }, + getSortField() { + return "date"; }, - isCorrectEventType(event) { - const eventType = event.body?.action?.type; - return eventType === "commentCard"; + isCorrectEventType({ type }) { + return type === "commentCard"; }, - async getResult(event) { - const card = await this.trello.getCard(event?.body?.action?.data?.card?.id ?? - event?.data?.card?.id); - const member = await this.trello.getMember(event?.body?.action?.idMemberCreator ?? - event.idMemberCreator); + async getResult(action) { + const card = await this.app.getCard({ + cardId: action?.data?.card?.id, + }); + const member = await this.app.getMember({ + memberId: action?.idMemberCreator, + }); return { member, card, - event: event?.body ?? event, + event: action, }; }, isRelevant({ result: { card } }) { return ( (!this.board || this.board === card.idBoard) && - (!this.cards || this.cards.length === 0 || this.cards.includes(card.id)) + (!this.lists || this.list === card.idList) && + (!this.cards?.length || this.cards.includes(card.id)) ); }, generateMeta({ event }) { @@ -89,4 +103,5 @@ export default { }; }, }, + sampleEmit, }; diff --git a/components/trello/sources/new-comment-added-to-card/test-event.mjs b/components/trello/sources/new-comment-added-to-card/test-event.mjs new file mode 100644 index 0000000000000..9e2c2398e2582 --- /dev/null +++ b/components/trello/sources/new-comment-added-to-card/test-event.mjs @@ -0,0 +1,298 @@ +export default { + "member": { + "id": "5df149d4fa80be39e64c9a43", + "aaId": "5df149cf8998970e5b435642", + "activityBlocked": false, + "avatarHash": "5590d4f06fc7d09fc98b242248c9003e", + "avatarUrl": "https://trello-members.s3.amazonaws.com/5df149d4fa80be39e64c9a43/5590d4f06fc7d09fc98b242248c9003e", + "bio": "", + "bioData": null, + "confirmed": true, + "fullName": "Michelle Bergeron", + "idEnterprise": null, + "idEnterprisesDeactivated": [], + "idMemberReferrer": null, + "idPremOrgsAdmin": [], + "initials": "MB", + "memberType": "normal", + "nonPublic": {}, + "nonPublicAvailable": true, + "products": [], + "url": "https://trello.com/michellebergeron6", + "username": "michellebergeron6", + "status": "disconnected", + "aaBlockSyncUntil": null, + "aaEmail": null, + "aaEnrolledDate": null, + "avatarSource": "none", + "credentialsRemovedCount": 0, + "dateLastImpression": "2024-10-01T19:54:58.087Z", + "dateLastActive": "2024-10-01T19:55:05.070Z", + "domainClaimed": null, + "email": null, + "gravatarHash": "010b11df3bb34c405912830655ee130c", + "idBoards": [ + "5df14a131c0e51155e68ea62", + ], + "idOrganizations": [ + "6047c17c2f558003144e04d7" + ], + "idEnterprisesAdmin": [], + "limits": { + "boards": { + "totalPerMember": { + "status": "ok", + "disableAt": 4500, + "warnAt": 3600 + } + }, + "orgs": { + "totalPerMember": { + "status": "ok", + "disableAt": 850, + "warnAt": 680 + } + } + }, + "loginTypes": null, + "marketingOptIn": { + "optedIn": true, + "date": "2019-12-11T19:56:05.741Z" + }, + "messagesDismissed": [ + { + "_id": "6082199aab18525bdcbe18f5", + "name": "team-join-cta-banner-6047c17c2f558003144e04d7", + "count": 5, + "lastDismissed": "2021-04-23T00:49:31.975Z" + }, + { + "_id": "6082199a190169402dc6130f", + "name": "team-join-cta-banner-6047c17c2f558003144e04d7", + "count": 1, + "lastDismissed": "2021-04-23T00:49:30.689Z" + } + ], + "nodeId": "ari:cloud:trello::user/5df149d4fa80be39e64c9a43", + "oneTimeMessagesDismissed": [ + "create-first-board", + "close-menu-of-first-board", + "nusku.views-switcher-upsell-default-open", + "teamify-post-migration", + "ack-new-feature-ButlerOnBoardsV2-1619852400000", + "workspace-nav-enabled", + "workspace-nav-spotlight", + "team-template-picker-6047c17c2f558003144e04d7", + "free-trial-banner-confetti-6047c17c2f558003144e04d7", + "team-table-views-switcher-new-pill", + "timeline-view-suggestion-6047c17c2f558003144e04d7", + "calendar-view-suggestion-6047c17c2f558003144e04d7", + "free-trial-banner-6047c17c2f558003144e04d7", + "workspace-user-limit-banner", + "atlassian-intelligence-admin-6047c17c2f558003144e04d7", + "your-items-on-home" + ], + "sessionType": null, + "prefs": { + "sendSummaries": true, + "minutesBetweenSummaries": 60, + "minutesBeforeDeadlineToNotify": 60, + "colorBlind": false, + "locale": "en-US", + "privacy": { + "fullName": "public", + "avatar": "public" + } + }, + "trophies": [], + "uploadedAvatarHash": null, + "uploadedAvatarUrl": null, + "premiumFeatures": [], + "isAaMastered": true, + "ixUpdate": "151" + }, + "card": { + "id": "61818a303e545129af23695d", + "badges": { + "attachmentsByType": { + "trello": { + "board": 0, + "card": 2 + } + }, + "externalSource": null, + "location": false, + "votes": 0, + "viewingMemberVoted": false, + "subscribed": true, + "fogbugz": "", + "checkItems": 1, + "checkItemsChecked": 1, + "checkItemsEarliestDue": null, + "comments": 2, + "attachments": 3, + "description": false, + "due": "2024-10-01T19:20:00.000Z", + "dueComplete": false, + "start": null, + "lastUpdatedByAi": false + }, + "checkItemStates": [ + { + "idCheckItem": "6181a5f18e239e6aff9799a4", + "state": "complete" + } + ], + "closed": false, + "dueComplete": false, + "dateLastActivity": "2024-10-01T20:12:35.627Z", + "desc": "", + "descData": { + "emoji": {} + }, + "due": "2024-10-01T19:20:00.000Z", + "dueReminder": -1, + "email": null, + "idBoard": "5f4d7be6c45c22583f75fa02", + "idChecklists": [ + "6181a5ef401ae66c357732b4" + ], + "idList": "5f4d7f78bdd7ce4d2d25fdda", + "idMembers": [], + "idMembersVoted": [], + "idShort": 68, + "idAttachmentCover": "62c884dce32d46579ecfd5f0", + "labels": [], + "idLabels": [], + "manualCoverAttachment": false, + "name": "sfsfdsfd", + "pinned": false, + "pos": 131071, + "shortLink": "QVLahShU", + "shortUrl": "https://trello.com/c/QVLahShU", + "start": null, + "subscribed": true, + "url": "https://trello.com/c/QVLahShU/68-sfsfdsfd", + "cover": { + "idAttachment": "62c884dce32d46579ecfd5f0", + "color": null, + "idUploadedBackground": null, + "size": "normal", + "brightness": "dark", + "scaled": [ + { + "id": "62c884dde32d46579ecfd5ff", + "_id": "62c884dde32d46579ecfd5ff", + "scaled": false, + "url": "", + "bytes": 1649, + "height": 50, + "width": 70 + }, + { + "id": "62c884dde32d46579ecfd601", + "_id": "62c884dde32d46579ecfd601", + "scaled": true, + "url": "", + "bytes": 4508, + "height": 98, + "width": 150 + }, + { + "id": "62c884dde32d46579ecfd600", + "_id": "62c884dde32d46579ecfd600", + "scaled": false, + "url": "", + "bytes": 9598, + "height": 150, + "width": 250 + }, + { + "id": "62c884dde32d46579ecfd602", + "_id": "62c884dde32d46579ecfd602", + "scaled": true, + "url": "", + "bytes": 13736, + "height": 195, + "width": 300 + }, + { + "id": "62c884dde32d46579ecfd603", + "_id": "62c884dde32d46579ecfd603", + "scaled": true, + "url": "", + "bytes": 20289, + "height": 260, + "width": 400 + }, + { + "id": "62c884dde32d46579ecfd604", + "_id": "62c884dde32d46579ecfd604", + "scaled": false, + "url": "", + "bytes": 18820, + "height": 260, + "width": 400 + } + ], + "edgeColor": "#bbac9c", + "idPlugin": null + }, + "isTemplate": false, + "cardRole": null + }, + "event": { + "id": "66fc17543e3458edb8b3070d", + "idMemberCreator": "5df149d4fa80be39e64c9a43", + "data": { + "text": "hello world", + "textData": { + "emoji": {} + }, + "card": { + "id": "61818a303e545129af23695d", + "name": "card", + "idShort": 68, + "shortLink": "QVLahShU" + }, + "board": { + "id": "5f4d7be6c45c22583f75fa02", + "name": "Board", + "shortLink": "tiA9sOeK" + }, + "list": { + "id": "5f4d7f78bdd7ce4d2d25fdda", + "name": "List" + } + }, + "appCreator": null, + "type": "commentCard", + "date": "2024-10-01T15:37:56.522Z", + "limits": { + "reactions": { + "perAction": { + "status": "ok", + "disableAt": 900, + "warnAt": 720 + }, + "uniquePerAction": { + "status": "ok", + "disableAt": 17, + "warnAt": 14 + } + } + }, + "memberCreator": { + "id": "5df149d4fa80be39e64c9a43", + "activityBlocked": false, + "avatarHash": "5590d4f06fc7d09fc98b242248c9003e", + "avatarUrl": "", + "fullName": "", + "idMemberReferrer": null, + "initials": "", + "nonPublic": {}, + "nonPublicAvailable": true, + "username": "" + } + } +} \ No newline at end of file diff --git a/components/trello/sources/new-label-added-to-card/new-label-added-to-card.mjs b/components/trello/sources/new-label-added-to-card/new-label-added-to-card.mjs index af2f6283fb4fb..8b8bebb76810d 100644 --- a/components/trello/sources/new-label-added-to-card/new-label-added-to-card.mjs +++ b/components/trello/sources/new-label-added-to-card/new-label-added-to-card.mjs @@ -1,54 +1,62 @@ import common from "../common/common-webhook.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "trello-new-label-added-to-card", name: "New Label Added To Card (Instant)", description: "Emit new event for each label added to a card.", - version: "0.0.11", + version: "0.1.1", type: "source", props: { ...common.props, board: { propDefinition: [ - common.props.trello, + common.props.app, "board", ], }, - lists: { + list: { propDefinition: [ - common.props.trello, + common.props.app, "lists", (c) => ({ board: c.board, }), ], + type: "string", + label: "List", + description: "If specified, events will only be emitted when a label is added to a card in the specified lists", }, cards: { propDefinition: [ - common.props.trello, + common.props.app, "cards", (c) => ({ board: c.board, + list: c.list, }), ], + description: "If specified, events will only be emitted when a label is added to one of the specified cards", }, }, hooks: { ...common.hooks, async deploy() { - if (this.cards && this.cards.length > 0) { + if (this.cards?.length) { await this.emitLabelsFromCardIds(this.cards); return; } - if (this.lists && this.lists.length > 0) { - for (const listId of this.lists) { - const cards = await this.trello.getCardsInList(listId); - await this.emitLabelsFromCards(cards); - } + if (this.list) { + const cards = await this.app.getCardsInList({ + listId: this.list, + }); + await this.emitLabelsFromCards(cards); return; } - const cards = await this.trello.getCards(this.board); + const cards = await this.app.getCards({ + boardId: this.board, + }); await this.emitLabelsFromCards(cards); }, }, @@ -58,7 +66,9 @@ export default { for (const card of cards) { const labelIds = card.idLabels; for (const labelId of labelIds) { - const label = await this.trello.getLabel(labelId); + const label = await this.app.getLabel({ + labelId, + }); let summary = label.color; summary += label.name ? ` - ${label.name}` @@ -74,8 +84,10 @@ export default { }, async emitLabelsFromCardIds(cardIds) { const cards = []; - for (const id of cardIds) { - const card = await this.trello.getCard(id); + for (const cardId of cardIds) { + const card = await this.app.getCard({ + cardId, + }); cards.push(card); } await this.emitLabelsFromCards(cards); @@ -92,26 +104,22 @@ export default { _setLabelColor(labelColor) { this.db.set("labelColor", labelColor); }, - isCorrectEventType(event) { - const eventType = event.body?.action?.type; - return eventType === "addLabelToCard"; + isCorrectEventType({ type }) { + return type === "addLabelToCard"; }, - async getResult(event) { - const cardId = event.body?.action?.data?.card?.id; - const labelName = event.body?.action?.data?.label?.name; - const labelColor = event.body?.action?.data?.label?.color; + getResult({ data }) { /** Record labelName & labelColor to use in generateMeta() */ - this._setLabelName(labelName); - this._setLabelColor(labelColor); - return this.trello.getCard(cardId); + this._setLabelName(data?.label?.name); + this._setLabelColor(data?.label?.color); + return this.app.getCard({ + cardId: data?.card?.id, + }); }, isRelevant({ result: card }) { return ( (!this.board || this.board === card.idBoard) && - (!this.lists || - this.lists.length === 0 || - this.lists.includes(card.idList)) && - (!this.cards || this.cards.length === 0 || this.cards.includes(card.id)) + (!this.list || this.list === card.idList) && + (!this.cards?.length || this.cards.includes(card.id)) ); }, generateMeta({ @@ -135,4 +143,5 @@ export default { this.$emit(card, meta); }, }, + sampleEmit, }; diff --git a/components/trello/sources/new-label-added-to-card/test-event.mjs b/components/trello/sources/new-label-added-to-card/test-event.mjs new file mode 100644 index 0000000000000..0c03bce6bf924 --- /dev/null +++ b/components/trello/sources/new-label-added-to-card/test-event.mjs @@ -0,0 +1,85 @@ +export default { + "id": "620eb14ebda9570d72fddead", + "badges": { + "attachmentsByType": { + "trello": { + "board": 0, + "card": 0 + } + }, + "externalSource": null, + "location": false, + "votes": 0, + "viewingMemberVoted": false, + "subscribed": false, + "fogbugz": "", + "checkItems": 1, + "checkItemsChecked": 1, + "checkItemsEarliestDue": null, + "comments": 3, + "attachments": 1, + "description": false, + "due": "2021-05-10T21:50:00.000Z", + "dueComplete": false, + "start": null, + "lastUpdatedByAi": false + }, + "checkItemStates": [ + { + "idCheckItem": "620eb14fbda9570d72fde031", + "state": "complete" + } + ], + "closed": false, + "dueComplete": false, + "dateLastActivity": "2024-09-30T16:31:35.215Z", + "desc": "", + "descData": { + "emoji": {} + }, + "due": "2021-05-10T21:50:00.000Z", + "dueReminder": null, + "email": null, + "idBoard": "5f4d7be6c45c22583f75fa02", + "idChecklists": [ + "620eb14fbda9570d72fde030" + ], + "idList": "5f4d7f78bdd7ce4d2d25fdda", + "idMembers": [], + "idMembersVoted": [], + "idShort": 85, + "idAttachmentCover": "620eb14ebda9570d72fdded7", + "labels": [ + { + "id": "5f4d7be6cdabcf46c027c792", + "idBoard": "5f4d7be6c45c22583f75fa02", + "idOrganization": "6047c17c2f558003144e04d7", + "name": "green label", + "nodeId": "ari:cloud:trello::label/workspace/6047c17c2f558003144e04d7/5f4d7be6cdabcf46c027c792", + "color": "green", + "uses": 9 + }, + ], + "idLabels": [ + "5f4d7be6cdabcf46c027c792", + ], + "manualCoverAttachment": false, + "name": "new card", + "pinned": false, + "pos": 360447, + "shortLink": "dho6bOdR", + "shortUrl": "https://trello.com/c/dho6bOdR", + "start": null, + "subscribed": false, + "url": "https://trello.com/c/dho6bOdR/85-new-card", + "cover": { + "idAttachment": "620eb14ebda9570d72fdded7", + "color": null, + "idUploadedBackground": null, + "size": "normal", + "brightness": "dark", + "idPlugin": null + }, + "isTemplate": false, + "cardRole": null +} \ No newline at end of file diff --git a/components/trello/sources/new-label/new-label.mjs b/components/trello/sources/new-label/new-label.mjs index 1e75dd6012ace..6ce68168b1547 100644 --- a/components/trello/sources/new-label/new-label.mjs +++ b/components/trello/sources/new-label/new-label.mjs @@ -1,29 +1,31 @@ import common from "../common/common-board-based.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "trello-new-label", - name: "New Label (Instant)", + name: "New Label Created (Instant)", description: "Emit new event for each new label added to a board.", - version: "0.0.13", + version: "0.1.1", type: "source", dedupe: "unique", methods: { ...common.methods, - async getSampleEvents() { - const labels = await this.trello.findLabel(this.board); - return { - sampleEvents: labels, - sortField: "id", - }; + getSampleEvents() { + return this.app.findLabel({ + boardId: this.board, + }); + }, + getSortField() { + return "id"; }, - isCorrectEventType(event) { - const eventType = event.body?.action?.type; - return eventType === "createLabel"; + isCorrectEventType({ type }) { + return type === "createLabel"; }, - async getResult(event) { - const labelId = event.body?.action?.data?.label?.id; - return this.trello.getLabel(labelId); + getResult({ data }) { + return this.app.getLabel({ + labelId: data?.label?.id, + }); }, generateMeta({ id, name, color: summary, @@ -38,4 +40,5 @@ export default { }; }, }, + sampleEmit, }; diff --git a/components/trello/sources/new-label/test-event.mjs b/components/trello/sources/new-label/test-event.mjs new file mode 100644 index 0000000000000..d4c67a99019de --- /dev/null +++ b/components/trello/sources/new-label/test-event.mjs @@ -0,0 +1,7 @@ +export default { + "id": "5f4d7be6cdabcf46c027c794", + "idBoard": "5f4d7be6c45c22583f75fa02", + "name": "red label", + "color": "red", + "uses": 1 +} \ No newline at end of file diff --git a/components/trello/sources/new-list/new-list.mjs b/components/trello/sources/new-list/new-list.mjs deleted file mode 100644 index 26b6ceb5cbec8..0000000000000 --- a/components/trello/sources/new-list/new-list.mjs +++ /dev/null @@ -1,29 +0,0 @@ -import common from "../common/common-board-based.mjs"; - -export default { - ...common, - key: "trello-new-list", - name: "New List (Instant)", - description: "Emit new event for each new list added to a board.", - version: "0.0.13", - type: "source", - dedupe: "unique", - methods: { - ...common.methods, - async getSampleEvents() { - const lists = await this.trello.getLists(this.board); - return { - sampleEvents: lists, - sortField: "id", - }; - }, - isCorrectEventType(event) { - const eventType = event.body?.action?.type; - return eventType === "createList"; - }, - async getResult(event) { - const listId = event.body?.action?.data?.list?.id; - return await this.trello.getList(listId); - }, - }, -}; diff --git a/components/trello/sources/new-member-on-card/new-member-on-card.mjs b/components/trello/sources/new-member-on-card/new-member-on-card.mjs index f03438a9498b3..bd68eea59f5e8 100644 --- a/components/trello/sources/new-member-on-card/new-member-on-card.mjs +++ b/components/trello/sources/new-member-on-card/new-member-on-card.mjs @@ -1,40 +1,43 @@ import common from "../common/common-board-based.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, key: "trello-new-member-on-card", name: "New Member on Card (Instant)", description: "Emit new event for each member that join in a card.", - version: "0.0.14", + version: "0.1.1", type: "source", dedupe: "unique", methods: { ...common.methods, - async getSampleEvents() { - const cards = await this.trello.getMemberCards("me"); - return { - sampleEvents: cards, - sortField: "dateLastActivity", - }; + getSampleEvents() { + return this.app.getMemberCards({ + userId: "me", + }); + }, + getSortField() { + return "dateLastActivity"; }, - isCorrectEventType(event) { - const eventType = event.body?.action?.type; - return eventType === "addMemberToCard"; + isCorrectEventType({ type }) { + return type === "addMemberToCard"; }, - async getResult(event) { - const cardId = event.body?.action?.data?.card?.id; - return this.trello.getCard(cardId); + getResult({ data }) { + return this.app.getCard({ + cardId: data?.card?.id, + }); }, generateMeta({ - id, name: summary, dateLastActivity, + id, name, dateLastActivity, }) { return { - id: this.onlyEventsRelatedWithAuthenticatedUser ? - id : - `${id}${dateLastActivity}`, - summary, + id: this.onlyEventsRelatedWithAuthenticatedUser + ? id + : `${id}${dateLastActivity}`, + summary: name || id, ts: Date.now(), }; }, }, + sampleEmit, }; diff --git a/components/trello/sources/new-member-on-card/test-event.mjs b/components/trello/sources/new-member-on-card/test-event.mjs new file mode 100644 index 0000000000000..b07248ed2bb8c --- /dev/null +++ b/components/trello/sources/new-member-on-card/test-event.mjs @@ -0,0 +1,83 @@ +export default { + "id": "66f5aef006f1cca76bb3250f", + "badges": { + "attachmentsByType": { + "trello": { + "board": 0, + "card": 0 + } + }, + "externalSource": null, + "location": false, + "votes": 0, + "viewingMemberVoted": false, + "subscribed": true, + "attachments": 0, + "fogbugz": "", + "checkItems": 0, + "checkItemsChecked": 0, + "checkItemsEarliestDue": null, + "comments": 2, + "description": false, + "due": null, + "dueComplete": false, + "lastUpdatedByAi": false, + "start": null + }, + "checkItemStates": [], + "closed": false, + "dueComplete": false, + "dateLastActivity": "2024-09-30T16:22:46.307Z", + "desc": "", + "descData": { + "emoji": {} + }, + "due": null, + "dueReminder": null, + "email": null, + "idBoard": "5f4d7be6c45c22583f75fa02", + "idChecklists": [ + "66f5b2296cb1ec0c66c5a119", + "66f6f798f4c23c2020f2bbad" + ], + "idList": "5f4d7f78bdd7ce4d2d25fdda", + "idMembers": [ + "5df149d4fa80be39e64c9a43" + ], + "idMembersVoted": [], + "idShort": 98, + "idAttachmentCover": null, + "labels": [ + { + "id": "66f5c1fd51d74d4e66aa26e0", + "idBoard": "5f4d7be6c45c22583f75fa02", + "idOrganization": "6047c17c2f558003144e04d7", + "name": "subtle sky label", + "nodeId": "ari:cloud:trello::label/workspace/6047c17c2f558003144e04d7/66f5c1fd51d74d4e66aa26e0", + "color": "sky_light", + "uses": 1 + }, + ], + "idLabels": [ + "66f5c1fd51d74d4e66aa26e0", + ], + "manualCoverAttachment": false, + "name": "hello world", + "pinned": false, + "pos": 393215, + "shortLink": "bRoOO9Bh", + "shortUrl": "https://trello.com/c/bRoOO9Bh", + "start": null, + "subscribed": true, + "url": "https://trello.com/c/bRoOO9Bh/98-hello-world", + "cover": { + "idAttachment": null, + "color": null, + "idUploadedBackground": null, + "size": "normal", + "brightness": "dark", + "idPlugin": null + }, + "isTemplate": false, + "cardRole": null +} \ No newline at end of file diff --git a/components/trello/sources/new-notification/new-notification.mjs b/components/trello/sources/new-notification/new-notification.mjs index 66c7426d365fa..2d988e128fa71 100644 --- a/components/trello/sources/new-notification/new-notification.mjs +++ b/components/trello/sources/new-notification/new-notification.mjs @@ -4,9 +4,8 @@ export default { ...common, key: "trello-new-notification", name: "New Notification", - description: - "Emit new event for each new Trello notification for the authenticated user.", - version: "0.0.10", + description: "Emit new event for each new Trello notification for the authenticated user.", + version: "0.1.1", type: "source", dedupe: "unique", methods: { @@ -29,11 +28,13 @@ export default { }, async run() { const since = this._getLastNotificationId(); - const params = { - since, - }; - const notifications = await this.trello.getNotifications("me", params); + const notifications = await this.app.getNotifications({ + notificationId: "me", + params: { + since, + }, + }); const { length: notificationCount = 0 } = notifications; if (notificationCount <= 0) { diff --git a/components/trello/trello.app.mjs b/components/trello/trello.app.mjs index b7eb408a4abcd..5be4e8d1edf2e 100644 --- a/components/trello/trello.app.mjs +++ b/components/trello/trello.app.mjs @@ -1,44 +1,57 @@ import { axios } from "@pipedream/platform"; -import crypto from "crypto"; -import events from "./common/events.mjs"; import fields from "./common/fields.mjs"; -import mime from "mime"; +import constants from "./common/constants.mjs"; +import mime from "mime/types/standard.js"; export default { type: "app", app: "trello", description: "Pipedream Trello Components", propDefinitions: { - cards: { - type: "string[]", - label: "Cards", - description: "The Trello cards you wish to select", - optional: true, - async options(opts) { - const cards = await this.getCards(opts.board); - return cards.map((card) => ({ - label: card.name, - value: card.id, - })); - }, - }, board: { type: "string", label: "Board", description: "The Trello board you wish to select", async options() { const boards = await this.getBoards(); - const activeBoards = boards.filter((board) => board.closed === false); - return activeBoards.map((board) => ({ - label: board.name, - value: board.id, + return boards.filter(({ closed }) => closed === false) + .map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + cards: { + type: "string[]", + label: "Cards", + description: "The Trello cards you wish to select", + optional: true, + async options({ + board, list, checklistCardsOnly, + }) { + let cards = await this.getCards({ + boardId: board, + }); + if (list) { + cards = cards.filter(({ idList }) => idList === list); + } + if (checklistCardsOnly) { + cards = cards.filter(({ idChecklists }) => idChecklists?.length); + } + return cards.map(({ + id: value, name: label, + }) => ({ + label, + value, })); }, }, boardFields: { type: "string[]", label: "Boards Fields", - description: "`all` or a list of board [fields](https://developer.atlassian.com/cloud/trello/guides/rest-api/object-definitions/#board-object)", + description: "`all` or a list of board [fields](https://developer.atlassian.com/cloud/trello/guides/rest-api/object-definitions/#board-object) to be returned in the results", options: fields.board, default: [ "name", @@ -48,26 +61,21 @@ export default { cardFields: { type: "string[]", label: "Cards Fields", - description: "`all` or a list of card [fields](https://developer.atlassian.com/cloud/trello/guides/rest-api/object-definitions/#card-object)", + description: "`all` or a list of card [fields](https://developer.atlassian.com/cloud/trello/guides/rest-api/object-definitions/#card-object) to be returned in the results", options: fields.card, default: [ "all", ], }, - eventTypes: { - type: "string[]", - label: "Event Types", - optional: true, - description: "Only emit events for the selected event types (e.g., `updateCard`).", - options: events, - }, lists: { type: "string[]", label: "Lists", description: "The Trello lists you wish to select", optional: true, - async options(opts) { - const lists = await this.getLists(opts.board); + async options({ board }) { + const lists = await this.getLists({ + boardId: board, + }); return lists.map((list) => ({ label: list.name, value: list.id, @@ -84,9 +92,14 @@ export default { label: "Organization IDs", description: "Specify the organizations to search for boards in", async options() { - const orgs = await this.listOrganizations(this.$auth.oauth_uid); + const orgs = await this.listOrganizations({ + memberId: this.$auth.oauth_uid, + params: { + fields: "all", + }, + }); return orgs.map((org) => ({ - label: org.name || org.id, + label: org.displayName ?? org.name ?? org.id, value: org.id, })); }, @@ -108,11 +121,28 @@ export default { type: "string", label: "Label", description: "The ID of the Label to be added to the card", - async options(opts) { - const labels = await this.findLabel(opts.board); - return labels.map((label) => ({ - label: label.name, - value: label.id, + async options({ + board, card, excludeCardLabels, cardLabelsOnly, + }) { + let labels = await this.findLabel({ + boardId: board, + }); + if (card) { + const { idLabels } = await this.getCard({ + cardId: card, + }); + if (excludeCardLabels) { + labels = labels.filter(({ id }) => !idLabels.includes(id)); + } + if (cardLabelsOnly) { + labels = labels.filter(({ id }) => idLabels.includes(id)); + } + } + return labels.map(({ + name, color, id: value, + }) => ({ + label: name || color, + value, })); }, }, @@ -120,8 +150,18 @@ export default { type: "string", label: "Member", description: "The ID of the Member to be added to the card", - async options(opts) { - const members = await this.listMembers(opts.board); + async options({ + board, card, excludeCardMembers, + }) { + let members = await this.listMembers({ + boardId: board, + }); + if (card && excludeCardMembers) { + const { idMembers } = await this.getCard({ + cardId: card, + }); + members = members.filter(({ id }) => !idMembers.includes(id)); + } return members.map((member) => ({ label: member.fullName, value: member.id, @@ -132,27 +172,37 @@ export default { type: "string", label: "Checklist", description: "The ID of a checklist to copy into the new checklist", - async options(opts) { - const { - board, - card, - } = opts; + async options({ + board, card, + }) { const checklists = card ? - await this.listCardChecklists(card) : - await this.listBoardChecklists(board); + await this.listCardChecklists({ + cardId: card, + }) : + await this.listBoardChecklists({ + boardId: board, + }); return checklists.map((checklist) => ({ label: checklist.name, value: checklist.id, })); }, }, - mimeType: { - type: "string", - label: "File Attachment Type", - description: "Not required for URL attachment", + customFieldIds: { + type: "string[]", + label: "Custom Field Ids", + description: "An array of custom field Ids to create/update", optional: true, - options() { - return Object.values(mime._types); + async options({ boardId }) { + const customFields = await this.listCustomFields({ + boardId, + }); + return customFields?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; }, }, name: { @@ -166,6 +216,19 @@ export default { label: "File Attachment URL", description: "URL must start with `http://` or `https://`", }, + mimeType: { + type: "string", + label: "Mime Type", + description: "Mime type of the attached file. Eg. `application/pdf`", + options() { + return Object.keys(mime); + }, + }, + file: { + type: "string", + label: "File Attachment Path", + description: "The path to the file saved to the `/tmp` directory (e.g. `/tmp/example.pdf`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory)", + }, desc: { type: "string", label: "Description", @@ -175,11 +238,8 @@ export default { pos: { type: "string", label: "Position", - description: "The position of the new card, can be `top`, `bottom`, or a positive number", - options: [ - "top", - "bottom", - ], + description: "The position of the checklist on the card. One of: top, bottom, or a positive number.", + options: constants.POSITIONS, optional: true, }, due: { @@ -216,439 +276,290 @@ export default { type: "string", label: "Card Filter", description: "Filter to apply to Cards. Valid values: `all`, `closed`, `none`, `open`, `visible`", - options: [ - "all", - "closed", - "none", - "open", - "visible", - ], + options: constants.CARD_FILTERS, default: "all", }, listFilter: { type: "string", label: "List Filter", description: "Type of list to search for", + options: constants.LIST_FILTERS, + default: "all", + }, + customFieldItems: { + type: "boolean", + label: "Custom Field Items", + description: "Whether to include the customFieldItems", + default: false, + optional: true, + }, + checklistItemId: { + type: "string", + label: "Checklist Item ID", + description: "The ID of the checklist item.", + async options({ checklistId }) { + const checkItems = await this.listCheckItems({ + checklistId, + }); + return checkItems.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + cardAttachmentId: { + type: "string", + label: "Cover Attachment ID", + description: "Assign an attachment id to be the cover image for the card", + optional: true, + async options({ + cardId, params, + }) { + const attachments = await this.listCardAttachments({ + cardId, + params, + }); + return attachments.map(({ + name, url, id: value, + }) => ({ + label: name || url, + value, + })); + }, + }, + fileType: { + type: "string", + label: "File Attachment Type", + description: "Select whether to attach file from a path or URL", options: [ - "all", - "closed", - "none", - "open", + "path", + "url", ], - default: "all", }, }, methods: { - _getBaseUrl() { - return "https://api.trello.com/1/"; - }, - async _getAuthorizationHeader({ - data, method, url, - }, $) { - const requestData = { - data, - method, - url, - }; - const token = { - key: this.$auth.oauth_access_token, - secret: this.$auth.oauth_refresh_token, - }; - return axios($ ?? this, { - method: "POST", - url: this.$auth.oauth_signer_uri, - data: { - requestData, - token, - }, - }); + getSignerUri() { + return this.$auth.oauth_signer_uri; }, - async _makeRequest(args, $) { + getToken() { const { - method = "GET", - path, - ...otherArgs - } = args; - const config = { - method, - url: `${this._getBaseUrl()}${path}`, - ...otherArgs, - }; - const authorization = await this._getAuthorizationHeader(config, $); - config.headers = { - ...config.headers, - authorization, + oauth_access_token: key, + oauth_refresh_token: secret, + } = this.$auth; + return { + key, + secret, }; - try { - return await axios($ ?? this, config); - } catch (err) { - console.log(err); - } }, - /** - * Archives a card. - * - * @param {string} idCard - the ID of the Card to archive. - * @returns an updated card object with `closed` (archived) property set to true. - * See more at the API docs: - * https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-put - */ - async archiveCard(idCard, $) { - const config = { - path: `cards/${idCard}`, - method: "PUT", - data: { - closed: true, - }, - }; - return this._makeRequest(config, $); + getUrl(path) { + return `https://api.trello.com/1${path}`; }, - /** - * Create an Attachment to a Card - * - * @param {string} idCard - the ID of the Card to move. - * @param {Object} params - an object containing parameters for the API request - * @returns {array} an string array with the ID of all the Card's Attachments. - * See more at the API docs: - * https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-attachments-post - */ - async addAttachmentToCardViaUrl(idCard, params, $) { - const config = { - path: `cards/${idCard}/attachments`, - method: "POST", - params, + _makeRequest({ + $ = this, path, ...args + } = {}) { + const signConfig = { + token: this.getToken(), + oauthSignerUri: this.getSignerUri(), }; - return this._makeRequest(config, $); - }, - /** - * Adds an existing label to the specified card. - * - * @param {string} idCard - the ID of the Card to move. - * @param {Object} params - an object containing parameters for the API request - * @returns {array} an string array with the ID of all the Card's Labels. - * See more at the API docs: - * https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-idlabels-post - */ - async addExistingLabelToCard(idCard, params, $) { - const config = { - path: `cards/${idCard}/idLabels`, - method: "POST", - params, - }; - return this._makeRequest(config, $); - }, - /** - * Add a member to a card - * - * @param {string} idCard - the ID of the Card to move. - * @param {Object} params - an object containing parameters for the API request - * @returns {array} an string array with the ID of all the Card's Members. - * See more at the API docs: - * https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-idmembers-post - */ - async addMemberToCard(idCard, params, $) { - const config = { - path: `cards/${idCard}/idMembers`, - method: "POST", - params, - }; - return this._makeRequest(config, $); - }, - /** - * Creates a checklist on the specified card. - * - * @param {Object} params - an object containing parameters for the API request - * @returns an object with the created checklist. - * See more at the API docs: - * https://developer.atlassian.com/cloud/trello/rest/api-group-checklists/#api-checklists-post - */ - async createChecklist(params, $) { + const config = { - path: "checklists", - method: "POST", - params, + ...args, + url: this.getUrl(path), }; - return this._makeRequest(config, $); + + return axios($, config, signConfig); }, - /** - * Creates a comment on a card. - * - * @param {string} idCard - the ID of the Card that the comment should be created on. - * @param {Object} params - an object containing parameters for the API request - * @returns a object containing a summary of the related card, members, and other Trello - * entities related to the newly created comment. - * See more at the API docs: - * https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-actions-comments-post - */ - async createCommentOnCard(idCard, comment, $) { - const config = { - path: `cards/${idCard}/actions/comments`, + post(args = {}) { + return this._makeRequest({ method: "POST", - params: { - text: comment, - }, - }; - return this._makeRequest(config, $); + ...args, + }); }, - /** - * Closes a board. - * - * @param {string} boardId - the ID of the Board to close. - * @returns the updated board object with the `closed` property set to true. - * See more at the API docs: - * https://developer.atlassian.com/cloud/trello/rest/api-group-boards/#api-boards-id-put - */ - async closeBoard(boardId, $) { - const config = { - path: `boards/${boardId}`, + put(args = {}) { + return this._makeRequest({ method: "PUT", - data: { - closed: true, - }, - }; - return this._makeRequest(config, $); - }, - /** - * Creates a new card. - * - * @param {Object} opts - an object containing data for the API request - * @returns the created card object. See more at the API docs: - * https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-post - */ - async createCard(opts, $) { - const config = { - path: "cards", - method: "post", - data: opts, - }; - return this._makeRequest(config, $); + ...args, + }); }, - /** - * Deletes the specified checklist. - * - * @param {string} idChecklist - the ID of the checklist to delete. - * @returns {object} an empty `limits` object indicating the operation completed successfully. - */ - async deleteChecklist(idChecklist, $) { - const config = { - path: `checklists/${idChecklist}`, + delete(args = {}) { + return this._makeRequest({ method: "DELETE", - }; - return this._makeRequest(config, $); + ...args, + }); }, - /** - * Finds a label on a specific board. - * - * @param {string} boardId - unique identifier of the board to search for labels. - * @param {Object} params - an object containing parameters for the API request - * @returns {array} an array with label objects complying with the specified parameters. - */ - async findLabel(boardId, params, $) { - const config = { - path: `boards/${boardId}/labels`, - params, - }; - return this._makeRequest(config, $); + createBoard(args = {}) { + return this.post({ + path: "/boards", + ...args, + }); }, - /** - * Finds a list in the specified board. - * - * @param {string} - boardId unique identifier of the board to search for lists. - * @param {Object} params - an object containing parameters for the API request - * @returns {array} an array with list objects conforming with the specified parameters. - */ - async findList(boardId, params, $) { - const config = { - path: `boards/${boardId}/lists`, - params, - }; - return this._makeRequest(config, $); + updateBoard({ + boardId, ...args + } = {}) { + return this.put({ + path: `/boards/${boardId}`, + ...args, + }); }, - /** - * Moves a card to the specified board/list pair. - * - * @param {string} idCard the ID of the Card to move. - * @param {Object} data - an object containing data for the API request - * @returns an updated card object set to the specified board and list ids. - * See more at the API docs: - * https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-put - */ - async moveCardToList(idCard, data, $) { - const config = { - path: `cards/${idCard}`, - method: "PUT", - data, - }; - return this._makeRequest(config, $); + createCard(args = {}) { + return this.post({ + path: "/cards", + ...args, + }); }, - async verifyTrelloWebhookRequest(request, callbackURL) { - let secret = this.$auth.oauth_refresh_token; - const base64Digest = function (s) { - return crypto.createHmac("sha1", secret).update(s) - .digest("base64"); - }; - const content = JSON.stringify(request.body) + callbackURL; - const doubleHash = base64Digest(content); - const headerHash = request.headers["x-trello-webhook"]; - return doubleHash === headerHash; + updateCard({ + cardId, ...args + } = {}) { + return this.put({ + path: `/cards/${cardId}`, + ...args, + }); }, - async getBoardActivity(boardId, filter = null) { + findLabel({ + boardId, ...args + } = {}) { return this._makeRequest({ - path: `boards/${boardId}/actions`, - params: { - filter, - }, + path: `/boards/${boardId}/labels`, + ...args, }); }, - async getCardActivity(cardId, filter = null) { + getCardActivity({ + cardId, ...args + } = {}) { return this._makeRequest({ - path: `cards/${cardId}/actions`, - params: { - filter, - }, + path: `/cards/${cardId}/actions`, + ...args, }); }, - async getBoard(id) { + getBoardActivity({ + boardId, ...args + } = {}) { return this._makeRequest({ - path: `boards/${id}`, + path: `/boards/${boardId}/actions`, + ...args, }); }, - async getBoards(id = this.$auth.oauth_uid) { + getBoard({ + boardId, ...args + } = {}) { return this._makeRequest({ - path: `members/${id}/boards`, + path: `/boards/${boardId}`, + ...args, }); }, - /** - * Gets details of a card. - * - * @param {string} id - the ID of the card to get details of. - * @returns {object} a card object. See more at the API docs: - * https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-post - */ - async getCard(id) { + getBoards({ + boardId = this.$auth.oauth_uid, ...args + } = {}) { return this._makeRequest({ - path: `cards/${id}`, + path: `/members/${boardId}/boards`, + ...args, }); }, - async getCards(id) { + getCard({ + cardId, ...args + } = {}) { return this._makeRequest({ - path: `boards/${id}/cards`, + path: `/cards/${cardId}`, + ...args, }); }, - async getFilteredCards(boardId, filter) { + getCards({ + boardId, ...args + } = {}) { return this._makeRequest({ - path: `boards/${boardId}/cards`, - params: { - filter, - }, + path: `/boards/${boardId}/cards`, + ...args, }); }, - async getCardsInList(listId) { + getChecklist({ + checklistId, ...args + } = {}) { return this._makeRequest({ - path: `lists/${listId}/cards`, + path: `/checklists/${checklistId}`, + ...args, }); }, - async getMemberCards(userId) { + getFilteredCards({ + boardId, filter, ...args + } = {}) { return this._makeRequest({ - path: `members/${userId}/cards`, + path: `/boards/${boardId}/cards/${filter}`, + ...args, }); }, - async getChecklist(id) { + getCardList({ + cardId, ...args + } = {}) { return this._makeRequest({ - path: `checklists/${id}`, + path: `/cards/${cardId}/list`, + ...args, }); }, - async getLabel(id) { + getCardsInList({ + listId, ...args + } = {}) { return this._makeRequest({ - path: `labels/${id}`, + path: `/lists/${listId}/cards`, + ...args, }); }, - async getList(id) { + getLabel({ + labelId, ...args + } = {}) { return this._makeRequest({ - path: `lists/${id}`, + path: `/labels/${labelId}`, + ...args, }); }, - async getLists(id) { + getLists({ + boardId, ...args + } = {}) { return this._makeRequest({ - path: `boards/${id}/lists`, + path: `/boards/${boardId}/lists`, + ...args, }); }, - async getNotifications(id, params) { + getList({ + listId, ...args + } = {}) { return this._makeRequest({ - path: `members/${id}/notifications`, - params, + path: `/lists/${listId}`, + ...args, }); }, - async getMember(id) { + getMember({ + memberId, ...args + } = {}) { return this._makeRequest({ - path: `members/${id}`, + path: `/members/${memberId}`, + ...args, }); }, - async getAttachment(cardId, attachmentId) { + getMemberCards({ + userId, ...args + } = {}) { return this._makeRequest({ - path: `cards/${cardId}/attachments/${attachmentId}`, + path: `/members/${userId}/cards`, + ...args, }); }, - async getCardList(cardId) { - return this._makeRequest({ - path: `cards/${cardId}/list`, - }); - }, - async createHook({ - id, endpoint, - }) { - const resp = await this._makeRequest({ - method: "post", - path: "webhooks/", - headers: { - "Content-Type": "applicaton/json", - }, - params: { - idModel: id, - description: "Pipedream Source ID", //todo add ID - callbackURL: endpoint, - }, - }); - return resp; - }, - async deleteHook({ hookId }) { + getAttachment({ + cardId, attachmentId, ...args + } = {}) { return this._makeRequest({ - method: "delete", - path: `webhooks/${hookId}`, + path: `/cards/${cardId}/attachments/${attachmentId}`, + ...args, }); }, - /** - * Removes an existing label from the specified card. - * - * @param {string} idCard - the ID of the Card to remove the Label from. - * @param {string} idLabel - the ID of the Label to be removed from the card. - * @returns {object} an object with the null valued property `_value` indicating that - * there were no errors - */ - async removeLabelFromCard(idCard, idLabel, $) { - const config = { - path: `cards/${idCard}/idLabels/${idLabel}`, - method: "DELETE", - }; - return this._makeRequest(config, $); - }, - /** - * Renames the specified list - * - * @param {string} listId - the ID of the List to rename. - * @param {Object} data - an object containing data for the API request - * @returns {object} a list object with the `closed` property, indicated if the list is - * closed or archived, `id` the id of the renamed List, `idBoard` the id of the Board parent - * to the List, `name` with the new name of the List, and `pos` with the position of the List - * in the Board. - */ - async renameList(listId, data, $) { - const config = { - path: `lists/${listId}`, - method: "PUT", - data, - }; - return this._makeRequest(config, $); + getNotifications({ + notificationId, ...args + } = {}) { + return this._makeRequest({ + path: `/members/${notificationId}/notifications`, + ...args, + }); }, /** * Searches for members, cards, boards, and/or organizations matching the specified query. @@ -659,81 +570,172 @@ export default { * this case "cards"), `partial` the search `terms` as included in `query`, and other * `modifiers`. */ - async search(params, $) { - const config = { - path: "search", - params, - }; - return this._makeRequest(config, $); + search(args = {}) { + return this._makeRequest({ + path: "/search", + ...args, + }); }, - /** - * Searches for boards matching the specified query. - * - * @param {Object} opts - an object containing data for the API request - * @returns {cards: array, options: object} an array with the `cards` objects matching the - * specified `query`, and an object with the `options` for the search, such as `modelTypes` (in - * this case "cards"), `partial` the search `terms` as included in `query`, and other - * `modifiers`. - */ - async searchBoards(opts, $) { - const params = { - ...opts, - idOrganizations: opts.idOrganizations?.join(","), - }; - return this.search(params, $); + listMembers({ + boardId, ...args + } = {}) { + return this._makeRequest({ + path: `/boards/${boardId}/members`, + ...args, + }); }, - /** - * Searches for cards matching the specified query. - * - * @param {Object} opts - an object containing data for the API request - * @returns {cards: array, options: object} an array with the `cards` objects matching the - * specified `query`, and an object with the `options` for the search, such as `modelTypes` (in - * this case "cards"), `partial` the search `terms` as included in `query`, and other - * `modifiers`. - */ - async searchCards(opts, $) { - const params = { - ...opts, - idOrganizations: opts.idOrganizations?.join(","), - idCards: opts.idCards?.join(","), - }; - return this.search(params, $); + listBoardChecklists({ + boardId, ...args + } = {}) { + return this._makeRequest({ + path: `/boards/${boardId}/checklists`, + ...args, + }); }, - /** - * Updates a card. - * - * @param {string} idCard - the ID of the card to update - * @param {Object} params - an object containing parameters for the API request - * @returns the updated card object. See more at the API docs: - * https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-post - */ - async updateCard(idCard, - params, $) { - const config = { - path: `cards/${idCard}`, - method: "PUT", - params, - }; - return this._makeRequest(config, $); + listCardChecklists({ + cardId, ...args + } = {}) { + return this._makeRequest({ + path: `/cards/${cardId}/checklists`, + ...args, + }); }, - async listMembers(board) { + listOrganizations({ + memberId, ...args + } = {}) { return this._makeRequest({ - path: `boards/${board}/members`, + path: `/members/${memberId}/organizations`, + ...args, }); }, - async listBoardChecklists(board) { + getCustomField({ + customFieldId, ...args + } = {}) { return this._makeRequest({ - path: `boards/${board}/checklists`, + path: `/customFields/${customFieldId}`, + ...args, }); }, - async listCardChecklists(card) { + listCustomFields({ + boardId, ...args + } = {}) { return this._makeRequest({ - path: `cards/${card}/checklists`, + path: `/boards/${boardId}/customFields`, + ...args, + }); + }, + updateCustomFields({ + cardId, ...args + } = {}) { + return this.put({ + path: `/cards/${cardId}/customFields`, + ...args, + }); + }, + listCheckItems({ + checklistId, ...args + } = {}) { + return this._makeRequest({ + path: `/checklists/${checklistId}/checkItems`, + ...args, + }); + }, + listCardAttachments({ + cardId, ...args + } = {}) { + return this._makeRequest({ + path: `/cards/${cardId}/attachments`, + ...args, + }); + }, + addAttachmentToCard({ + cardId, ...args + } = {}) { + return this.post({ + path: `/cards/${cardId}/attachments`, + ...args, + }); + }, + addChecklist({ + cardId, ...args + } = {}) { + return this.post({ + path: `/cards/${cardId}/checklists`, + ...args, + }); + }, + addComment({ + cardId, ...args + } = {}) { + return this.post({ + path: `/cards/${cardId}/actions/comments`, + ...args, + }); + }, + addExistingLabelToCard({ + cardId, ...args + } = {}) { + return this.post({ + path: `/cards/${cardId}/idLabels`, + ...args, + }); + }, + removeLabelFromCard({ + cardId, labelId, ...args + } = {}) { + return this.delete({ + path: `/cards/${cardId}/idLabels/${labelId}`, + ...args, + }); + }, + addMemberToCard({ + cardId, ...args + } = {}) { + return this.post({ + path: `/cards/${cardId}/idMembers`, + ...args, + }); + }, + completeChecklistItem({ + cardId, checklistItemId, ...args + } = {}) { + return this.put({ + path: `/cards/${cardId}/checkItem/${checklistItemId}`, + ...args, + }); + }, + createChecklistItem({ + checklistId, ...args + } = {}) { + return this.post({ + path: `/checklists/${checklistId}/checkItems`, + ...args, + }); + }, + createLabel(args = {}) { + return this.post({ + path: "/labels", + ...args, + }); + }, + createList(args = {}) { + return this.post({ + path: "/lists", + ...args, + }); + }, + deleteChecklist({ + checklistId, ...args + } = {}) { + return this.delete({ + path: `/checklists/${checklistId}`, + ...args, }); }, - async listOrganizations(id) { + searchMembers(args = {}) { return this._makeRequest({ - path: `members/${id}/organizations?fields="id,name"`, + path: "/search/members", + ...args, }); }, }, diff --git a/components/tremendous/README.md b/components/tremendous/README.md index 358480413af50..53caa34503e91 100644 --- a/components/tremendous/README.md +++ b/components/tremendous/README.md @@ -1,21 +1,11 @@ # Overview -The Tremendous API offers incredible capabilities to help you build web -applications and connected devices. With it, you can create custom services, -build connected devices, and automate tasks. +The Tremendous API lets you automate the distribution of digital rewards and incentives, such as gift cards, prepaid Visa® cards, cash, and more. It's perfect for businesses wanting to scale their reward programs without manual intervention. With Pipedream, you can harness the Tremendous API to create dynamic, serverless workflows that trigger rewards based on various events, integrate with CRM systems for customer milestones, or streamline employee recognition processes. -Here are a few examples of the things you can build using the Tremendous API: +# Example Use Cases -- Custom Services: Create your own custom services and applications with the - Tremendous API. Leverage its robust features and integrations to create - something tailored to your needs. -- Smart Devices: Connect devices to the internet and control them remotely. - Automate tasks like scheduling and controlling lights, thermostats, and more. -- Automation: Automate web tasks and processes with a few clicks. Create - workflows to streamline your business processes. -- Cloud Integrations: Integrate 3rd party services like Stripe, Dropbox, - MongoDB, Slack, and many more through our Cloud integrations. -- Web Applications: Create modern, responsive web applications and dashboards - easily with the Tremendous API. -- Wearables & IoT: Connect wearables and IoT devices to the Tremendous platform - and track data in real-time. +- **Customer Loyalty Rewards**: Automatically send a digital gift card to customers who have completed a certain number of purchases. Use Pipedream to monitor your e-commerce platform, such as Shopify, for purchase events, and trigger the Tremendous API to dispatch the reward. + +- **Survey Participation Incentives**: Incentivize survey responses by offering a reward upon completion. Connect Typeform or Google Forms to Tremendous through Pipedream. When a survey is completed, a workflow triggers the Tremendous API to send an appropriate incentive. + +- **Employee Recognition Program**: Streamline employee rewards by integrating Tremendous with HR platforms like BambooHR. Set up a Pipedream workflow that listens for work anniversaries or performance achievements, then automatically sends a personalized reward via the Tremendous API. diff --git a/components/tremendous/actions/create-order-email-reward/create-order-email-reward.mjs b/components/tremendous/actions/create-order-email-reward/create-order-email-reward.mjs new file mode 100644 index 0000000000000..692a06c6b46ca --- /dev/null +++ b/components/tremendous/actions/create-order-email-reward/create-order-email-reward.mjs @@ -0,0 +1,107 @@ +import app from "../../tremendous.app.mjs"; +import { DELIVERY_METHOD_OPTIONS } from "../../common/constants.mjs"; + +export default { + name: "Create Order Email Reward", + version: "0.0.1", + key: "tremendous-create-order-email-reward", + description: "Create an order to send out a reward. [See the documentation](https://developers.tremendous.com/reference/create-order)", + type: "action", + props: { + app, + campaignId: { + propDefinition: [ + app, + "campaignId", + ], + optional: true, + }, + products: { + propDefinition: [ + app, + "products", + ], + optional: true, + }, + infoBox: { + type: "alert", + alertType: "info", + content: "Either `Products` or `Campaign ID` must be specified. [See the documentation](https://developers.tremendous.com/reference/create-order) for more information.", + }, + fundingSourceId: { + propDefinition: [ + app, + "fundingSourceId", + ], + default: "balance", + }, + externalId: { + type: "string", + label: "External ID", + description: "Reference for this order. If set, any subsequent requests with the same `External ID` will not create any further orders, and simply return the initially created order.", + optional: true, + }, + valueAmount: { + type: "string", + label: "Value Amount", + description: "Amount of the reward.", + }, + valueCurrencyCode: { + type: "string", + label: "Value Currency Code", + description: "Currency of the reward.", + }, + recipientName: { + type: "string", + label: "Recipient Name", + description: "Name of the recipient.", + }, + recipientEmail: { + type: "string", + label: "Recipient Email", + description: "Email address of the recipient.", + }, + recipientPhone: { + type: "string", + label: "Recipient Phone", + description: "Phone number of the recipient. For non-US phone numbers, specify the country code (prefixed with `+`).", + }, + deliveryMethod: { + type: "string", + label: "Delivery Method", + description: "How to deliver the reward to the recipient.", + options: DELIVERY_METHOD_OPTIONS, + }, + }, + async run({ $ }) { + const response = await this.app.createOrder({ + $, + data: { + external_id: this.externalId, + payment: { + funding_source_id: this.fundingSourceId, + }, + reward: { + campaign_id: this.campaignId, + products: this.products, + value: { + denomination: this.valueAmount, + currency_code: this.valueCurrencyCode, + }, + recipient: { + name: this.recipientName, + email: this.recipientEmail, + phone: this.recipientPhone, + }, + delivery: { + method: this.deliveryMethod, + }, + }, + }, + }); + + $.export("$summary", `Successfully created order (ID: ${response?.order?.id})`); + + return response; + }, +}; diff --git a/components/tremendous/common/constants.mjs b/components/tremendous/common/constants.mjs new file mode 100644 index 0000000000000..8fa569a6180b0 --- /dev/null +++ b/components/tremendous/common/constants.mjs @@ -0,0 +1,15 @@ +export const DELIVERY_METHOD_OPTIONS = [ + { + value: "EMAIL", + label: "Deliver the reward to the recipient by email", + }, + { + value: "LINK", + label: "Deliver the reward to the recipient via a link.", + }, + + { + value: "PHONE", + label: "Deliver the reward to the recipient by SMS", + }, +]; diff --git a/components/tremendous/package.json b/components/tremendous/package.json new file mode 100644 index 0000000000000..e502f2a48a695 --- /dev/null +++ b/components/tremendous/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/tremendous", + "version": "0.0.1", + "description": "Pipedream Tremendous Components", + "main": "tremendous.app.mjs", + "keywords": [ + "pipedream", + "tremendous" + ], + "homepage": "https://pipedream.com/apps/tremendous", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/tremendous/tremendous.app.mjs b/components/tremendous/tremendous.app.mjs index 6d0c587c8b121..6318f95bef4cc 100644 --- a/components/tremendous/tremendous.app.mjs +++ b/components/tremendous/tremendous.app.mjs @@ -1,11 +1,89 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "tremendous", - propDefinitions: {}, + propDefinitions: { + campaignId: { + type: "string", + label: "Campaign ID", + description: "ID of the campaign in your account, that defines the available products (different gift cards, charity, etc.) that the recipient can choose from.", + async options() { + const { campaigns } = await this.listCampaigns(); + return campaigns?.map(({ + id, name, + }) => ({ + label: name, + value: id, + })); + }, + }, + products: { + type: "string[]", + label: "Products", + description: "IDs of products (different gift cards, charity, etc.) that will be available to the recipient to choose from. If this and `Campaign ID` are specified, this will override the products made available by the campaign. It will not override other campaign attributes, like the message and customization of the look and feel.", + async options() { + const { products } = await this.listProducts(); + return products?.map(({ + id, name, + }) => ({ + label: name, + value: id, + })); + }, + }, + fundingSourceId: { + type: "string", + label: "Funding Source ID", + description: "Tremendous ID of the funding source that will be used to pay for the order. Use `balance` to use your Tremendous's balance.", + async options() { + const response = await this.listFundingSources(); + return response.funding_sources?.map(({ + id, method, + }) => ({ + label: `${id} - ${method}`, + value: id, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseRequest({ + $, headers, ...args + }) { + return axios($, { + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, + }, + baseURL: "https://testflight.tremendous.com/api/v2", + ...args, + }); + }, + createOrder(args) { + return this._baseRequest({ + method: "POST", + url: "/orders", + ...args, + }); + }, + listCampaigns() { + return this._baseRequest({ + method: "GET", + url: "/campaigns", + }); + }, + listProducts() { + return this._baseRequest({ + method: "GET", + url: "/products", + }); + }, + listFundingSources() { + return this._baseRequest({ + method: "GET", + url: "/funding_sources", + }); }, }, }; diff --git a/components/trengo/README.md b/components/trengo/README.md index 4bf4b4d71c212..e928cff44163b 100644 --- a/components/trengo/README.md +++ b/components/trengo/README.md @@ -1,17 +1,11 @@ # Overview -You can build amazing customer service experiences by using the Trengo API. -It's the platform to help teams work better together, and offers features to -optimize your customer conversation. For example, you can use Trengo to: +The Trengo API provides programmatic access to Trengo's multi-channel communication platform, allowing for the streamlining of customer interactions across various channels such as email, SMS, social media, and chat. By leveraging the Trengo API on Pipedream, you can automate customer support workflows, sync communication data with CRM systems, and trigger alerts or actions based on specific customer queries or events. -- Improve customer service by leveraging automation for fast and personalized - responses -- Increase efficiency by optimizing operational workflows -- Gather insights from customer conversations to improve customer service -- Automate conversations like welcome messages and error notifications -- Filter and manage incoming conversations to make sure the right person - provides the right answers +# Example Use Cases -These are just some of the examples of what you can do with the Trengo API. -With the Trengo platform, you can make sure your customer conversations deliver -the best possible outcomes, consistently. +- **Customer Support Ticket Automation**: Automate the creation of support tickets in Trengo based on customer inquiries from diverse channels. Use Pipedream to monitor incoming messages and automatically categorize and assign them to the appropriate support team or agent in Trengo. + +- **CRM Synchronization**: Synchronize customer messages and interactions from Trengo to a CRM platform like Salesforce. With Pipedream, set up a workflow that updates customer records in Salesforce with conversation threads and support ticket statuses from Trengo, ensuring a unified customer view. + +- **Real-Time Notifications and Alerts**: Trigger real-time notifications or alerts in apps like Slack or Microsoft Teams whenever a high-priority message or ticket is received on Trengo. Use Pipedream workflows to identify keywords or sentiment in incoming messages and escalate important customer issues instantly to the right stakeholders. diff --git a/components/trestle/README.md b/components/trestle/README.md new file mode 100644 index 0000000000000..21d90a6041489 --- /dev/null +++ b/components/trestle/README.md @@ -0,0 +1,11 @@ +# Overview + +The Trestle API provides a platform for real estate data integration, offering access to listings, open houses, and other relevant data. It's designed for developers building applications that require up-to-date real estate information. On Pipedream, you can harness this data to create automated workflows that respond to changes in listings, sync data between platforms, or generate real-time analytics. + +# Example Use Cases + +- **Real Estate Listing Notifications**: Create a workflow that monitors new listings on Trestle and sends out email alerts via a service like SendGrid. This can keep potential buyers or real estate agents informed about new properties that match their criteria. + +- **Property Data Sync to a CRM**: Implement an automation that periodically fetches updated property listings from Trestle and syncs them with customer records in a CRM like Salesforce. This ensures sales teams have the latest information at their fingertips. + +- **Market Analysis Reporting**: Build a workflow that gathers data on recent property sales from the Trestle API and feeds it into a tool like Google Sheets or Tableau for advanced analysis and visualization, providing insights into market trends. diff --git a/components/trestle/package.json b/components/trestle/package.json index 7e1341cc76b25..f814a6a9c2326 100644 --- a/components/trestle/package.json +++ b/components/trestle/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/tricentis_qtest/actions/create-requirement/create-requirement.mjs b/components/tricentis_qtest/actions/create-requirement/create-requirement.mjs new file mode 100644 index 0000000000000..a9a3543e7a14f --- /dev/null +++ b/components/tricentis_qtest/actions/create-requirement/create-requirement.mjs @@ -0,0 +1,61 @@ +import { + getFieldProps as additionalProps, getProperties, +} from "../../common/utils.mjs"; +import tricentisQtest from "../../tricentis_qtest.app.mjs"; + +export default { + key: "tricentis_qtest-create-requirement", + name: "Create Requirement", + description: "Create a new requirement. [See the documentation](https://documentation.tricentis.com/qtest/od/en/content/apis/apis/requirement_apis.htm#CreateARequirement)", + version: "0.0.1", + type: "action", + props: { + tricentisQtest, + projectId: { + propDefinition: [ + tricentisQtest, + "projectId", + ], + }, + parentId: { + propDefinition: [ + tricentisQtest, + "parentId", + ({ projectId }) => ({ + projectId, + }), + ], + reloadProps: true, + }, + name: { + type: "string", + label: "Name", + description: "Requirement name", + }, + }, + additionalProps, + methods: { + getDataFields() { + return this.tricentisQtest.getRequirementFields(this.projectId); + }, + getProperties, + }, + async run({ $ }) { + const { // eslint-disable-next-line no-unused-vars + tricentisQtest, projectId, parentId, name, getProperties, getDataFields, ...fields + } = this; + const response = await tricentisQtest.createRequirement({ + $, + projectId, + params: { + parentId, + }, + data: { + name, + properties: getProperties(fields), + }, + }); + $.export("$summary", `Successfully created requirement (ID: ${response.id})`); + return response; + }, +}; diff --git a/components/tricentis_qtest/actions/get-defect/get-defect.mjs b/components/tricentis_qtest/actions/get-defect/get-defect.mjs new file mode 100644 index 0000000000000..484cced52d4a1 --- /dev/null +++ b/components/tricentis_qtest/actions/get-defect/get-defect.mjs @@ -0,0 +1,38 @@ +import tricentisQtest from "../../tricentis_qtest.app.mjs"; + +export default { + key: "tricentis_qtest-get-defect", + name: "Get Defect", + description: "Get details of a defect. [See the documentation](https://documentation.tricentis.com/qtest/od/en/content/apis/apis/defect_apis.htm#GetRecentlyUpdatedDefects)", + version: "0.0.1", + type: "action", + props: { + tricentisQtest, + projectId: { + propDefinition: [ + tricentisQtest, + "projectId", + ], + }, + defectId: { + propDefinition: [ + tricentisQtest, + "defectId", + ({ projectId }) => ({ + projectId, + }), + ], + }, + }, + async run({ $ }) { + const { + tricentisQtest, ...args + } = this; + const response = await tricentisQtest.getDefect({ + $, + ...args, + }); + $.export("$summary", `Successfully fetched defect (ID: ${args.defectId})`); + return response; + }, +}; diff --git a/components/tricentis_qtest/actions/get-requirement/get-requirement.mjs b/components/tricentis_qtest/actions/get-requirement/get-requirement.mjs new file mode 100644 index 0000000000000..69ab98b6c3ff3 --- /dev/null +++ b/components/tricentis_qtest/actions/get-requirement/get-requirement.mjs @@ -0,0 +1,38 @@ +import tricentisQtest from "../../tricentis_qtest.app.mjs"; + +export default { + key: "tricentis_qtest-get-requirement", + name: "Get Requirement", + description: "Get details of a requirement. [See the documentation](https://documentation.tricentis.com/qtest/od/en/content/apis/apis/requirement_apis.htm#GetARequirementByItsID)", + version: "0.0.1", + type: "action", + props: { + tricentisQtest, + projectId: { + propDefinition: [ + tricentisQtest, + "projectId", + ], + }, + requirementId: { + propDefinition: [ + tricentisQtest, + "requirementId", + ({ projectId }) => ({ + projectId, + }), + ], + }, + }, + async run({ $ }) { + const { + tricentisQtest, ...args + } = this; + const response = await tricentisQtest.getRequirement({ + $, + ...args, + }); + $.export("$summary", `Successfully fetched requirement (ID: ${args.requirementId})`); + return response; + }, +}; diff --git a/components/tricentis_qtest/actions/submit-defect/submit-defect.mjs b/components/tricentis_qtest/actions/submit-defect/submit-defect.mjs new file mode 100644 index 0000000000000..c086243414e77 --- /dev/null +++ b/components/tricentis_qtest/actions/submit-defect/submit-defect.mjs @@ -0,0 +1,43 @@ +import { + getFieldProps as additionalProps, getProperties, +} from "../../common/utils.mjs"; +import tricentisQtest from "../../tricentis_qtest.app.mjs"; + +export default { + key: "tricentis_qtest-submit-defect", + name: "Submit Defect", + description: "Submit a new defect. [See the documentation](https://documentation.tricentis.com/qtest/od/en/content/apis/apis/defect_apis.htm#SubmitaDefect)", + version: "0.0.1", + type: "action", + props: { + tricentisQtest, + projectId: { + propDefinition: [ + tricentisQtest, + "projectId", + ], + reloadProps: true, + }, + }, + additionalProps, + methods: { + getDataFields() { + return this.tricentisQtest.getDefectFields(this.projectId); + }, + getProperties, + }, + async run({ $ }) { + const { // eslint-disable-next-line no-unused-vars + tricentisQtest, projectId, getProperties, getDataFields, ...fields + } = this; + const response = await tricentisQtest.createDefect({ + $, + projectId, + data: { + properties: getProperties(fields), + }, + }); + $.export("$summary", `Successfully submitted defect (ID: ${response.id})`); + return response; + }, +}; diff --git a/components/tricentis_qtest/actions/update-defect/update-defect.mjs b/components/tricentis_qtest/actions/update-defect/update-defect.mjs new file mode 100644 index 0000000000000..221b1df64ee5b --- /dev/null +++ b/components/tricentis_qtest/actions/update-defect/update-defect.mjs @@ -0,0 +1,53 @@ +import { + getFieldProps as additionalProps, getProperties, +} from "../../common/utils.mjs"; +import tricentisQtest from "../../tricentis_qtest.app.mjs"; + +export default { + key: "tricentis_qtest-update-defect", + name: "Update Defect", + description: "Update a defect. [See the documentation](https://documentation.tricentis.com/qtest/od/en/content/apis/apis/defect_apis.htm#UpdateADefect)", + version: "0.0.1", + type: "action", + props: { + tricentisQtest, + projectId: { + propDefinition: [ + tricentisQtest, + "projectId", + ], + }, + defectId: { + propDefinition: [ + tricentisQtest, + "defectId", + ({ projectId }) => ({ + projectId, + }), + ], + reloadProps: true, + }, + }, + additionalProps, + methods: { + getDataFields() { + return this.tricentisQtest.getDefectFields(this.projectId); + }, + getProperties, + }, + async run({ $ }) { + const { // eslint-disable-next-line no-unused-vars + tricentisQtest, projectId, defectId, getProperties, getDataFields, ...fields + } = this; + const response = await tricentisQtest.updateDefect({ + $, + projectId, + defectId, + data: { + properties: getProperties(fields), + }, + }); + $.export("$summary", `Successfully updated defect (ID: ${defectId})`); + return response; + }, +}; diff --git a/components/tricentis_qtest/actions/update-requirement/update-requirement.mjs b/components/tricentis_qtest/actions/update-requirement/update-requirement.mjs new file mode 100644 index 0000000000000..0b8daabe2263c --- /dev/null +++ b/components/tricentis_qtest/actions/update-requirement/update-requirement.mjs @@ -0,0 +1,65 @@ +import { + getFieldProps as additionalProps, getProperties, +} from "../../common/utils.mjs"; +import tricentisQtest from "../../tricentis_qtest.app.mjs"; + +export default { + key: "tricentis_qtest-update-requirement", + name: "Update Requirement", + description: "Update a requirement. [See the documentation](https://documentation.tricentis.com/qtest/od/en/content/apis/apis/requirement_apis.htm#UpdateARequirement)", + version: "0.0.1", + type: "action", + props: { + tricentisQtest, + projectId: { + propDefinition: [ + tricentisQtest, + "projectId", + ], + }, + requirementId: { + propDefinition: [ + tricentisQtest, + "requirementId", + ({ projectId }) => ({ + projectId, + }), + ], + reloadProps: true, + }, + name: { + type: "string", + label: "Name", + description: "Requirement name", + }, + }, + additionalProps, + methods: { + getDataFields() { + return this.tricentisQtest.getRequirementFields(this.projectId); + }, + getProperties, + }, + async run({ $ }) { + const { /* eslint-disable no-unused-vars */ + tricentisQtest, + projectId, + requirementId, + name, + getProperties, + getDataFields, + ...fields + } = this; /* eslint-enable no-unused-vars */ + const response = await tricentisQtest.updateRequirement({ + $, + projectId, + requirementId, + data: { + name, + properties: getProperties(fields), + }, + }); + $.export("$summary", `Successfully updated requirement (ID: ${requirementId})`); + return response; + }, +}; diff --git a/components/tricentis_qtest/common/utils.mjs b/components/tricentis_qtest/common/utils.mjs new file mode 100644 index 0000000000000..33b88936dc8c3 --- /dev/null +++ b/components/tricentis_qtest/common/utils.mjs @@ -0,0 +1,53 @@ +export async function getFieldProps() { + if (this.useFields === false) return {}; + + const fields = await this.getDataFields(); + + function getFieldType(type) { + switch (type) { + case "Number": + return "integer"; + case "ArrayNumber": + return "integer[]"; + default: + return "string"; + } + } + + const result = {}; + const isUpdate = !!(this.requirementId || this.defectId); + + fields?.forEach(({ + id, label, attribute_type: fieldType, allowed_values: options, required, + }) => { + const type = getFieldType(fieldType); + result[`field_${id}`] = { + label, + type, + description: `Field ID: ${id} (type: ${fieldType})`, + optional: isUpdate || !required, + ...(options && { + options: options.map(({ + label, value, + }) => ({ + label, + value: (type === "string" && typeof value !== "string") + ? value.toString() + : value, + })), + }), + }; + }); + + return result; +} + +export function getProperties(fields) { + return fields && Object.entries(fields).map(([ + id, + value, + ]) => ({ + field_id: id.split("_").pop(), + field_value: value, + })); +} diff --git a/components/tricentis_qtest/package.json b/components/tricentis_qtest/package.json new file mode 100644 index 0000000000000..6ca738085ce8e --- /dev/null +++ b/components/tricentis_qtest/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/tricentis_qtest", + "version": "0.1.0", + "description": "Pipedream Tricentis qTest Components", + "main": "tricentis_qtest.app.mjs", + "keywords": [ + "pipedream", + "tricentis_qtest" + ], + "homepage": "https://pipedream.com/apps/tricentis_qtest", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/tricentis_qtest/tricentis_qtest.app.mjs b/components/tricentis_qtest/tricentis_qtest.app.mjs new file mode 100644 index 0000000000000..a6fe4c2c92dbb --- /dev/null +++ b/components/tricentis_qtest/tricentis_qtest.app.mjs @@ -0,0 +1,196 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "tricentis_qtest", + propDefinitions: { + projectId: { + type: "string", + label: "Project ID", + description: "The ID of a project", + async options() { + const projects = await this.getProjects(); + return (projects ?? []).map(({ + id, name, + }) => ({ + label: name, + value: id, + })); + }, + }, + parentId: { + type: "string", + label: "Parent ID", + description: "The parent module which will contain the newly created requirement", + async options({ projectId }) { + const modules = await this.getModules(projectId); + return (modules ?? []).map(({ + id, name, + }) => ({ + label: name, + value: id, + })); + }, + }, + requirementId: { + type: "string", + label: "Requirement ID", + description: "The ID of a requirement", + async options({ + page = 0, projectId, + }) { + const requirements = await this.getRequirements({ + projectId, + params: { + page: page + 1, + }, + }); + return (requirements ?? []).map(({ + id, name, + }) => ({ + label: name, + value: id, + })); + }, + }, + defectId: { + type: "string", + label: "Defect ID", + description: "The ID of a defect. The listed options are defects that have been updated in the last 30 days.", + async options({ + page = 0, projectId, prevContext: { startTime }, + }) { + if (!startTime) { + const date = new Date(); + date.setDate(date.getDate() - 30); + startTime = date.toISOString(); + } + const fields = await this.getDefectFields(projectId); + const summaryId = fields.find(({ label }) => label === "Summary")?.id; + const defects = await this.getDefects({ + projectId, + params: { + page: page + 1, + startTime, + }, + }); + return { + options: (defects ?? []).map(({ + id, properties, + }) => ({ + label: properties.find((f) => f.field_id === summaryId)?.field_value ?? id, + value: id, + })), + context: { + startTime, + }, + }; + }, + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.qtest_base_uri}/api/v3`; + }, + async _makeRequest({ + $ = this, headers, ...otherOpts + }) { + return axios($, { + ...otherOpts, + baseURL: this._baseUrl(), + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + getProjects() { + return this._makeRequest({ + url: "/projects", + }); + }, + getModules(projectId) { + return this._makeRequest({ + url: `/projects/${projectId}/modules`, + }); + }, + createRequirement({ + projectId, ...args + }) { + return this._makeRequest({ + method: "POST", + url: `/projects/${projectId}/requirements`, + ...args, + }); + }, + getRequirement({ + projectId, requirementId, ...args + }) { + return this._makeRequest({ + url: `/projects/${projectId}/requirements/${requirementId}`, + ...args, + }); + }, + getRequirements({ + projectId, ...args + }) { + return this._makeRequest({ + url: `/projects/${projectId}/requirements`, + ...args, + }); + }, + updateRequirement({ + projectId, requirementId, ...args + }) { + return this._makeRequest({ + method: "PUT", + url: `/projects/${projectId}/requirements/${requirementId}`, + ...args, + }); + }, + getRequirementFields(projectId) { + return this._makeRequest({ + url: `/projects/${projectId}/settings/requirements/fields`, + }); + }, + createDefect({ + projectId, ...args + }) { + return this._makeRequest({ + method: "POST", + url: `/projects/${projectId}/defects`, + ...args, + }); + }, + getDefect({ + projectId, defectId, ...args + }) { + return this._makeRequest({ + url: `/projects/${projectId}/defects/${defectId}`, + ...args, + }); + }, + getDefects({ + projectId, ...args + }) { + return this._makeRequest({ + url: `/projects/${projectId}/defects/last-change`, + ...args, + }); + }, + updateDefect({ + projectId, defectId, ...args + }) { + return this._makeRequest({ + method: "PUT", + url: `/projects/${projectId}/defects/${defectId}`, + ...args, + }); + }, + getDefectFields(projectId) { + return this._makeRequest({ + url: `/projects/${projectId}/settings/defects/fields`, + }); + }, + }, +}; diff --git a/components/triggercmd/README.md b/components/triggercmd/README.md index fdc64245e97da..41530bf8df63f 100644 --- a/components/triggercmd/README.md +++ b/components/triggercmd/README.md @@ -1,31 +1,11 @@ # Overview -TRIGGERcmd is an innovative cloud-based automation platform with a -user-friendly API that allows you to control your digital world from anywhere. -With the TRIGGERcmd API, you can access everything from medical equipment to -home entertainment systems, and even software development tools. No matter what -type of system you're trying to manage, TRIGGERcmd can help you get it done -quickly and easily. Here are just a few of the possibilities: +TRIGGERcmd is a cloud service that allows you to execute commands on your computers remotely through a REST API or via voice command with smart home integrators. With Pipedream's integration capabilities, you can create custom workflows to automate tasks across various apps and services. For instance, you could set up a serverless workflow to trigger scripts on your computer when specific events occur in other apps, such as receiving an email, a new GitHub commit, or a scheduled timer. -- Automate backups of your data: Backing up your data has never been easier. - The TRIGGERcmd API allows you to quickly set up automated backups of all your - important files and documents. -- Connect to home entertainment systems: If you've ever wanted to control your - home entertainment system remotely, then the TRIGGERcmd API is for you. You - can quickly connect your system to the cloud and start controlling it from - anywhere. -- Control medical equipment remotely: With the TRIGGERcmd API, you can control - your medical equipment from any location to keep patients safe and healthy. -- Access software development tools: With the TRIGGERcmd API, you can access a - variety of software development tools to speed up development and improve - product quality. -- Automate tasks: Automating tedious tasks has never been simpler. The - TRIGGERcmd API makes it easy to set up automated tasks that save you time and - energy. -- Control appliances: With the TRIGGERcmd API, you can easily control any - appliance in your home remotely, letting you adjust settings and access - features on the fly. +# Example Use Cases -You can use the TRIGGERcmd API to take control of your digital world. Whether -you're a medical professional, a software developer, or a home entertainment -enthusiast, the TRIGGERcmd API can help you get the most out of your systems. +- **Remote System Administration**: Trigger virus scans or system updates on your remote machines when a security alert is received from an app like SentinelOne. This ensures that your systems are always up to date with the latest security patches without manual intervention. + +- **DevOps Automation**: Automatically run deployment scripts on your server when a new commit is pushed to a specific branch in your GitHub repository. This workflow can streamline your CI/CD pipeline, making the deployment process more efficient and error-free. + +- **Smart Home Integration**: Link TRIGGERcmd with smart home apps like SmartThings to execute commands on your PC in response to smart home triggers, such as turning off your computer when your smart thermostat detects you've left home, or starting a backup when your smart lock is activated at night. diff --git a/components/triggre/README.md b/components/triggre/README.md new file mode 100644 index 0000000000000..1d9afcc5868a1 --- /dev/null +++ b/components/triggre/README.md @@ -0,0 +1,11 @@ +# Overview + +The Triggre API offers a gateway to interact with the Triggre platform, enabling you to automate processes, manage data, and integrate with other services. On Pipedream, you can harness this API to create powerful serverless workflows, connecting Triggre with numerous other apps to streamline operations, trigger actions based on events, and manipulate data in real-time. This empowers you to build custom, automated solutions that bridge the gap between Triggre and the rest of your tech stack. + +# Example Use Cases + +- **Automate Order Processing**: When an order is placed in Triggre, a Pipedream workflow can automatically send the order details to a fulfillment service like ShipStation, update inventory in a system like Shopify, and send a confirmation email to the customer using SendGrid. + +- **Sync Customer Data**: On a new customer signup in your Triggre app, trigger a Pipedream workflow to add or update the customer's information in a CRM like Salesforce, subscribe them to a Mailchimp campaign, and send a personalized welcome message through Twilio SMS. + +- **Handle Support Tickets**: For every new support request submitted via Triggre, use a Pipedream workflow to create a ticket in a helpdesk system like Zendesk, notify the support team on Slack, and log the issue in a Google Sheets document for tracking and analysis. diff --git a/components/trint/README.md b/components/trint/README.md new file mode 100644 index 0000000000000..9e6f7732f4d6b --- /dev/null +++ b/components/trint/README.md @@ -0,0 +1,11 @@ +# Overview + +The Trint API allows you to harness the power of automated transcription and translation. With Trint, you can convert audio and video files into searchable, editable text. It supports multiple languages and provides features like speaker identification and timestamping. Using the Trint API within Pipedream, you can create automated workflows that process media files, extract valuable insights, and integrate transcribed content into databases, content management systems, or other third-party services. + +# Example Use Cases + +- **Automated Podcast Transcription**: Set up a workflow where each new podcast episode uploaded to a cloud storage (like Dropbox or Google Drive) triggers a Pipedream event, which then sends the file to Trint for transcription. Once transcribed, the text can be automatically formatted and published on your blog or website. + +- **Video Content Analysis and Archiving**: Whenever a new video is added to a CMS, Pipedream can catch this event, send the video to Trint for transcription and translation, and then store the results in a database. This can be linked with AI services to analyze sentiment or extract topics, enriching your video archive with searchable metadata. + +- **Multilingual Support for Customer Service Calls**: Record customer service calls and use Pipedream to send these recordings to Trint for transcription and translation. Integrate this with a CRM system to append transcripts to customer profiles, aiding in support and ensuring clear communication across different languages. diff --git a/components/tripadvisor_content_api/README.md b/components/tripadvisor_content_api/README.md new file mode 100644 index 0000000000000..021e266f3cc6b --- /dev/null +++ b/components/tripadvisor_content_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Tripadvisor Content API allows you to tap into a rich repository of travel content, offering access to a vast array of information such as hotel details, reviews, photos, and ratings. With this API on Pipedream, you can automate the process of gathering travel-related insights, dynamically updating travel offerings, or responding to customer sentiment in real-time. The possibilities extend from integrating travel data into your services, monitoring brand reputation, to enriching user experiences with fresh, curated content. + +# Example Use Cases + +- **Sync Tripadvisor Reviews with CRM**: Automatically fetch new reviews for specific properties or destinations from Tripadvisor and store them in your Customer Relationship Management (CRM) software. This keeps customer service teams informed and ready to react to client feedback promptly. + +- **Monitor Hotel Ratings for Competitive Analysis**: Track the ratings and rankings of hotels or destinations to maintain competitive analysis dashboards. This data can be integrated with tools like Google Sheets or Tableau, providing your business intelligence team with real-time insights to inform strategic decisions. + +- **Automate Content Updates on Travel Platforms**: Use the Tripadvisor Content API to periodically update your travel platform's listings with the latest reviews, amenities, and photos. This ensures that users are always seeing the most accurate and up-to-date information, which can improve user engagement and trust in your service. diff --git a/components/tripadvisor_content_api/actions/location-details/location-details.mjs b/components/tripadvisor_content_api/actions/location-details/location-details.mjs new file mode 100644 index 0000000000000..52446efdba796 --- /dev/null +++ b/components/tripadvisor_content_api/actions/location-details/location-details.mjs @@ -0,0 +1,37 @@ +import app from "../../tripadvisor_content_api.app.mjs"; + +export default { + key: "tripadvisor_content_api-location-details", + name: "Get Location Details", + description: "Returns comprehensive information about a location. [See the documentation](https://tripadvisor-content-api.readme.io/reference/getlocationdetails)", + version: "0.0.1", + type: "action", + props: { + app, + searchQuery: { + propDefinition: [ + app, + "searchQuery", + ], + }, + locationId: { + propDefinition: [ + app, + "locationId", + (c) => ({ + searchQuery: c.searchQuery, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.app.getLocationDetails({ + $, + locationId: this.locationId, + }); + + $.export("$summary", `Successfully returned the details for location '${response.name}'`); + + return response; + }, +}; diff --git a/components/tripadvisor_content_api/actions/location-reviews/location-reviews.mjs b/components/tripadvisor_content_api/actions/location-reviews/location-reviews.mjs new file mode 100644 index 0000000000000..e34a112ea09ce --- /dev/null +++ b/components/tripadvisor_content_api/actions/location-reviews/location-reviews.mjs @@ -0,0 +1,37 @@ +import app from "../../tripadvisor_content_api.app.mjs"; + +export default { + key: "tripadvisor_content_api-location-reviews", + name: "Get Location Reviews", + description: "Returns up to 5 of the most recent reviews for a specific location. [See the documentation](https://tripadvisor-content-api.readme.io/reference/getlocationreviews)", + version: "0.0.1", + type: "action", + props: { + app, + searchQuery: { + propDefinition: [ + app, + "searchQuery", + ], + }, + locationId: { + propDefinition: [ + app, + "locationId", + (c) => ({ + searchQuery: c.searchQuery, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.app.getLocationReviews({ + $, + locationId: this.locationId, + }); + + $.export("$summary", `Successfully listed ${response.data.length} of the most recent reviews for the specified location`); + + return response; + }, +}; diff --git a/components/tripadvisor_content_api/actions/location-search/location-search.mjs b/components/tripadvisor_content_api/actions/location-search/location-search.mjs new file mode 100644 index 0000000000000..f549fb12e9a9c --- /dev/null +++ b/components/tripadvisor_content_api/actions/location-search/location-search.mjs @@ -0,0 +1,44 @@ +import app from "../../tripadvisor_content_api.app.mjs"; + +export default { + key: "tripadvisor_content_api-location-search", + name: "Search Locations", + description: "Returns up to 10 locations found by the given search query. [See the documentation](https://tripadvisor-content-api.readme.io/reference/searchforlocations)", + version: "0.0.1", + type: "action", + props: { + app, + searchQuery: { + propDefinition: [ + app, + "searchQuery", + ], + }, + category: { + propDefinition: [ + app, + "category", + ], + }, + address: { + propDefinition: [ + app, + "address", + ], + }, + }, + async run({ $ }) { + const response = await this.app.searchLocations({ + $, + params: { + searchQuery: this.searchQuery, + category: this.category, + address: this.address, + }, + }); + + $.export("$summary", `Found ${response.data.length} location(s)`); + + return response; + }, +}; diff --git a/components/tripadvisor_content_api/package.json b/components/tripadvisor_content_api/package.json new file mode 100644 index 0000000000000..a8ede8c12b0bc --- /dev/null +++ b/components/tripadvisor_content_api/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/tripadvisor_content_api", + "version": "0.1.0", + "description": "Pipedream Tripadvisor (Content API) Components", + "main": "tripadvisor_content_api.app.mjs", + "keywords": [ + "pipedream", + "tripadvisor_content_api" + ], + "homepage": "https://pipedream.com/apps/tripadvisor_content_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/tripadvisor_content_api/tripadvisor_content_api.app.mjs b/components/tripadvisor_content_api/tripadvisor_content_api.app.mjs new file mode 100644 index 0000000000000..4cc75253f9c1c --- /dev/null +++ b/components/tripadvisor_content_api/tripadvisor_content_api.app.mjs @@ -0,0 +1,92 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "tripadvisor_content_api", + propDefinitions: { + locationId: { + type: "string", + label: "Location ID", + description: "The ID of the location", + async options({ searchQuery }) { + const { data: resources } = await this.searchLocations({ + params: { + searchQuery, + }, + }); + return resources.map(({ + location_id, name, + }) => ({ + value: location_id, + label: name, + })); + }, + }, + searchQuery: { + type: "string", + label: "Search Query", + description: "The search query to find locations", + }, + address: { + type: "string", + label: "Location Address", + description: "The address of the location", + optional: true, + }, + category: { + type: "string", + label: "Location Category", + description: "The category of the location", + optional: true, + options: [ + "hotels", + "attractions", + "restaurants", + "geos", + ], + }, + }, + methods: { + _baseUrl() { + return "https://api.content.tripadvisor.com/api/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + params: { + ...params, + key: `${this.$auth.api_key}`, + }, + }); + }, + getLocationDetails({ + locationId, ...args + }) { + return this._makeRequest({ + ...args, + path: `/location/${locationId}/details`, + }); + }, + getLocationReviews({ + locationId, ...args + }) { + return this._makeRequest({ + ...args, + path: `/location/${locationId}/reviews`, + }); + }, + searchLocations(args = {}) { + return this._makeRequest({ + ...args, + path: "/location/search", + }); + }, + }, +}; diff --git a/components/truss/README.md b/components/truss/README.md new file mode 100644 index 0000000000000..8d588b5bbbdaf --- /dev/null +++ b/components/truss/README.md @@ -0,0 +1,11 @@ +# Overview + +The Truss API enables automation and integration of project management functionalities. With Truss, you can manage tasks, projects, and resources within your organization by automating workflows and connecting to other services. By leveraging Pipedream's capabilities, you can create complex automations that respond to events in Truss, manipulate data, and trigger actions in other apps, all without the need to manage infrastructure. + +# Example Use Cases + +- **Sync Truss Projects with Google Calendar**: Automate the synchronization of project deadlines and milestones from Truss to a Google Calendar. When a new project is created or a deadline is updated in Truss, a corresponding event is automatically added or modified in a specific Google Calendar, ensuring all team members have the most up-to-date schedule. + +- **Create Truss Tasks from Email**: Convert incoming emails to tasks in Truss. Using Pipedream's email trigger, you can parse emails for specific keywords or from certain senders and automatically create a new task in Truss with the relevant details, streamlining the process of tracking correspondence and action items. + +- **Aggregate Truss Data for Reporting**: Build a workflow that periodically fetches data from Truss projects and tasks, then compiles and formats the information into a report. This report could then be sent to Slack, email, or even saved to a Google Sheet for shared access, providing teams with regular updates on project status and progress. diff --git a/components/truss/package.json b/components/truss/package.json index c7384583a319e..d49e2a391ebef 100644 --- a/components/truss/package.json +++ b/components/truss/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/trust/actions/create-testimonial/create-testimonial.mjs b/components/trust/actions/create-testimonial/create-testimonial.mjs new file mode 100644 index 0000000000000..cf20a040d83c3 --- /dev/null +++ b/components/trust/actions/create-testimonial/create-testimonial.mjs @@ -0,0 +1,129 @@ +import app from "../../trust.app.mjs"; + +export default { + key: "trust-create-testimonial", + name: "Create Testimonial", + description: "Create a new testimonial within the Trust platform.", + version: "0.0.1", + type: "action", + props: { + app, + brandId: { + propDefinition: [ + app, + "brandId", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + videoToken: { + propDefinition: [ + app, + "videoToken", + ], + }, + videoUrl: { + propDefinition: [ + app, + "videoUrl", + ], + }, + firstName: { + propDefinition: [ + app, + "firstName", + ], + }, + lastName: { + propDefinition: [ + app, + "lastName", + ], + }, + title: { + propDefinition: [ + app, + "title", + ], + }, + imageUrl: { + propDefinition: [ + app, + "imageUrl", + ], + }, + testimonialText: { + propDefinition: [ + app, + "testimonialText", + ], + }, + gaveConsent: { + propDefinition: [ + app, + "gaveConsent", + ], + }, + published: { + propDefinition: [ + app, + "published", + ], + }, + stars: { + propDefinition: [ + app, + "stars", + ], + }, + }, + methods: { + createTestimonial(args = {}) { + return this.app.post({ + path: "/testimonial", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createTestimonial, + testimonialText, + brandId, + imageUrl, + email, + videoUrl, + videoToken, + firstName, + lastName, + title, + gaveConsent, + published, + stars, + } = this; + + const response = await createTestimonial({ + $, + data: { + brandId, + testimonialText, + imageUrl, + email, + videoUrl, + videoToken, + firstname: firstName, + lastname: lastName, + title, + gaveConsent, + published, + stars, + }, + }); + $.export("$summary", `Successfully created testimonial with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/trust/actions/delete-testimonial/delete-testimonial.mjs b/components/trust/actions/delete-testimonial/delete-testimonial.mjs new file mode 100644 index 0000000000000..b5929530f8d65 --- /dev/null +++ b/components/trust/actions/delete-testimonial/delete-testimonial.mjs @@ -0,0 +1,54 @@ +import app from "../../trust.app.mjs"; + +export default { + key: "trust-delete-testimonial", + name: "Delete Testimonial", + description: "Deletes an existing testimonial from the Trust platform. [See the documentation](https://api-docs.usetrust.io/)", + version: "0.0.1", + type: "action", + props: { + app, + brandId: { + propDefinition: [ + app, + "brandId", + ], + }, + testimonialId: { + propDefinition: [ + app, + "testimonialId", + ({ brandId }) => ({ + brandId, + }), + ], + }, + }, + methods: { + deleteTestimonial({ + testimonialId, ...args + } = {}) { + return this.app.delete({ + path: `/testimonial/${testimonialId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + deleteTestimonial, + testimonialId, + } = this; + + await deleteTestimonial({ + $, + testimonialId, + }); + + $.export("$summary", "Successfully deleted testimonial."); + + return { + success: true, + }; + }, +}; diff --git a/components/trust/actions/update-testimonial/update-testimonial.mjs b/components/trust/actions/update-testimonial/update-testimonial.mjs new file mode 100644 index 0000000000000..4b936721863e7 --- /dev/null +++ b/components/trust/actions/update-testimonial/update-testimonial.mjs @@ -0,0 +1,142 @@ +import app from "../../trust.app.mjs"; + +export default { + key: "trust-update-testimonial", + name: "Update Testimonial", + description: "Update an existing testimonial within the Trust platform. [See the documentation](https://api-docs.usetrust.io/)", + version: "0.0.1", + type: "action", + props: { + app, + brandId: { + propDefinition: [ + app, + "brandId", + ], + }, + testimonialId: { + propDefinition: [ + app, + "testimonialId", + ({ brandId }) => ({ + brandId, + }), + ], + }, + firstName: { + propDefinition: [ + app, + "firstName", + ], + }, + lastName: { + propDefinition: [ + app, + "lastName", + ], + }, + title: { + propDefinition: [ + app, + "title", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + imageUrl: { + propDefinition: [ + app, + "imageUrl", + ], + }, + testimonialText: { + propDefinition: [ + app, + "testimonialText", + ], + }, + videoToken: { + propDefinition: [ + app, + "videoToken", + ], + }, + videoUrl: { + propDefinition: [ + app, + "videoUrl", + ], + }, + gaveConsent: { + propDefinition: [ + app, + "gaveConsent", + ], + }, + published: { + propDefinition: [ + app, + "published", + ], + }, + stars: { + propDefinition: [ + app, + "stars", + ], + }, + }, + methods: { + updateTestimonial({ + testimonialId, ...args + } = {}) { + return this.app.put({ + path: `/testimonial/${testimonialId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + updateTestimonial, + brandId, + testimonialId, + testimonialText, + imageUrl, + email, + videoUrl, + videoToken, + firstName, + lastName, + title, + gaveConsent, + published, + stars, + } = this; + + const response = await updateTestimonial({ + $, + testimonialId, + data: { + brandId, + testimonialText, + imageUrl, + email, + videoUrl, + videoToken, + firstname: firstName, + lastname: lastName, + title, + gaveConsent, + published, + stars, + }, + }); + $.export("$summary", `Successfully updated testimonial with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/trust/actions/upload-video/upload-video.mjs b/components/trust/actions/upload-video/upload-video.mjs new file mode 100644 index 0000000000000..d243efd5196b7 --- /dev/null +++ b/components/trust/actions/upload-video/upload-video.mjs @@ -0,0 +1,56 @@ +import fs from "fs"; +import FormData from "form-data"; +import app from "../../trust.app.mjs"; + +export default { + key: "trust-upload-video", + name: "Upload Video", + description: "Upload a video to the Trust platform. [See the documentation](https://api-docs.usetrust.io/uploads-a-video-to-be-used-for-testimonials).", + version: "0.0.1", + type: "action", + props: { + app, + brandId: { + propDefinition: [ + app, + "brandId", + ], + }, + filePath: { + type: "string", + label: "File Path", + description: "File path of a file previously downloaded in Pipedream E.g. (`/tmp/my-file.mp4`). [Download a file to the `/tmp` directory](https://pipedream.com/docs/code/nodejs/http-requests/#download-a-file-to-the-tmp-directory)", + }, + }, + methods: { + uploadVideo({ + brandId, ...args + } = {}) { + return this.app.post({ + path: `/media/upload-video/${brandId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + uploadVideo, + brandId, + filePath, + } = this; + + const data = new FormData(); + data.append("file", fs.createReadStream(filePath)); + + const response = await uploadVideo({ + $, + brandId, + data, + headers: data.getHeaders(), + }); + + $.export("$summary", "Successfully uploaded video."); + + return response; + }, +}; diff --git a/components/trust/common/constants.mjs b/components/trust/common/constants.mjs new file mode 100644 index 0000000000000..54e8a6e4caec2 --- /dev/null +++ b/components/trust/common/constants.mjs @@ -0,0 +1,7 @@ +const BASE_URL = "https://api.usetrust.app"; +const VERSION_PATH = "/v1"; + +export default { + BASE_URL, + VERSION_PATH, +}; diff --git a/components/trust/package.json b/components/trust/package.json new file mode 100644 index 0000000000000..7086e869f0386 --- /dev/null +++ b/components/trust/package.json @@ -0,0 +1,20 @@ +{ + "name": "@pipedream/trust", + "version": "0.1.0", + "description": "Pipedream Trust Components", + "main": "trust.app.mjs", + "keywords": [ + "pipedream", + "trust" + ], + "homepage": "https://pipedream.com/apps/trust", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0", + "form-data": "^4.0.0", + "fs": "^0.0.1-security" + } +} diff --git a/components/trust/sources/common/polling.mjs b/components/trust/sources/common/polling.mjs new file mode 100644 index 0000000000000..c2f71517cb084 --- /dev/null +++ b/components/trust/sources/common/polling.mjs @@ -0,0 +1,59 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../trust.app.mjs"; + +export default { + props: { + app, + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + sortFn() { + return; + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + async processResources(resources) { + const { + sortFn, + processResource, + } = this; + + return Array.from(resources) + .sort(sortFn) + .forEach(processResource); + }, + }, + async run() { + const { + getResourcesFn, + getResourcesFnArgs, + processResources, + } = this; + + const resourcesFn = getResourcesFn(); + const resources = await resourcesFn(getResourcesFnArgs()); + + processResources(resources); + }, +}; diff --git a/components/trust/sources/new-testimonial/new-testimonial.mjs b/components/trust/sources/new-testimonial/new-testimonial.mjs new file mode 100644 index 0000000000000..6e3841d8b312d --- /dev/null +++ b/components/trust/sources/new-testimonial/new-testimonial.mjs @@ -0,0 +1,43 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "trust-new-testimonial", + name: "New Testimonial Created", + description: "Emit new event when a new testimonial is created. [See the documentation](https://api-docs.usetrust.io/get-a-list-with-testimonials-for-a-specific-website-via-brand-id).", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + brandId: { + propDefinition: [ + common.props.app, + "brandId", + ], + }, + }, + methods: { + ...common.methods, + sortFn(a, b) { + return new Date(b.created_at) - new Date(a.created_at); + }, + getResourcesFn() { + return this.app.listTestimonials; + }, + getResourcesFnArgs() { + return { + brandId: this.brandId, + }; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Testimonial: ${resource.id}`, + ts: Date.parse(resource.created), + }; + }, + }, + sampleEmit, +}; diff --git a/components/trust/sources/new-testimonial/test-event.mjs b/components/trust/sources/new-testimonial/test-event.mjs new file mode 100644 index 0000000000000..7b6fbef8915f3 --- /dev/null +++ b/components/trust/sources/new-testimonial/test-event.mjs @@ -0,0 +1,21 @@ +export default { + "id": "07be6cab-8c40-4855-945c-77ec27ddaffa", + "brandId": "dd493aea-1289-488d-aeb4-c633717758fd", + "created": "2024-07-27T00:20:45.933", + "firstname": null, + "lastname": null, + "title": null, + "subtitle": null, + "company": null, + "email": "test@test.com", + "imageUrl": null, + "testimonialText": "Always fantastic support. I love that Trust keep bringing out new features!", + "externalVideoHtml": null, + "videoVideoUrl": null, + "socialMediaProfiles": "[]", + "stars": 0, + "gaveConsent": false, + "consentDateTime": null, + "published": false, + "questionsAndAnswers": "[]" +}; diff --git a/components/trust/trust.app.mjs b/components/trust/trust.app.mjs new file mode 100644 index 0000000000000..b37044d0ebd89 --- /dev/null +++ b/components/trust/trust.app.mjs @@ -0,0 +1,168 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "trust", + propDefinitions: { + brandId: { + type: "string", + label: "Brand ID", + description: "The brand if for a registered website / brand (use GET of Websites API endpoint first). It must be of GUID format. Eg. `9d885046-1b7b-4b7b-8b7b-9d8850461b7b`", + async options() { + const response = await this.listWebsitesAndBrands(); + return response?.brands?.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + testimonialId: { + type: "string", + label: "Testimonial ID", + description: "The unique identifier for the testimonial to be operated on.", + async options({ brandId }) { + const testimonials = await this.listTestimonials({ + brandId, + }); + return testimonials?.map(({ + id: value, email, firstname, lastname, + }) => ({ + label: [ + firstname, + lastname, + email, + ].filter(Boolean).join(" ") || value, + value, + })); + }, + }, + testimonialText: { + type: "string", + label: "Testimonial Text", + description: "Text content of the testimonial. Eg. `Always fantastic support. I love that Trust keep bringing out new features!`", + }, + email: { + type: "string", + label: "Email", + description: "The email address of the author of the testimonial.", + }, + videoUrl: { + type: "string", + label: "Video URL", + description: "URL of uploaded video. Required when using an uploaded video along with **Video Token**.", + }, + videoToken: { + type: "string", + label: "Video Token", + description: "Token of uploaded video. Required when using an uploaded video along with **Video URL**.", + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the author of the testimonial.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the author of the testimonial.", + optional: true, + }, + title: { + type: "string", + label: "Title", + description: "The title of the testimonial.", + optional: true, + }, + imageUrl: { + type: "string", + label: "Image URL", + description: "The URL of the image to be associated with the testimonial.", + optional: true, + }, + gaveConsent: { + type: "boolean", + label: "Gave Consent?", + description: "Whether the author of the testimonial has given consent for it to be published.", + optional: true, + }, + published: { + type: "boolean", + label: "Is Published?", + description: "Is testimonial published to show via any widget?", + optional: true, + }, + stars: { + type: "string", + label: "Stars", + description: "The number of stars given in the testimonial. Eg. `5`", + optional: true, + options: [ + "1", + "2", + "3", + "4", + "5", + ], + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getAuth() { + return { + username: "x", + password: this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + auth: this.getAuth(), + headers: { + ...headers, + accept: "application/json", + }, + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + put(args = {}) { + return this._makeRequest({ + method: "PUT", + ...args, + }); + }, + listTestimonials({ + brandId, ...args + } = {}) { + return this._makeRequest({ + path: `/testimonial/all/${brandId}`, + ...args, + }); + }, + listWebsitesAndBrands(args = {}) { + return this._makeRequest({ + path: "/websites", + ...args, + }); + }, + }, +}; diff --git a/components/trustpilot/README.md b/components/trustpilot/README.md new file mode 100644 index 0000000000000..07f0f8f3fe56c --- /dev/null +++ b/components/trustpilot/README.md @@ -0,0 +1,11 @@ +# Overview + +The Trustpilot (Customer) API lets you tap into the rich pool of customer review data on Trustpilot. You can use it to automate the process of collecting and managing reviews, responding to feedback, and analyzing customer sentiment. With Pipedream's integration, you can trigger workflows based on new reviews, aggregate review data for analysis, and sync Trustpilot data with other services like CRMs, support tickets, and marketing tools. + +# Example Use Cases + +- **Manage New Reviews in Real Time**: When a new review is posted on Trustpilot, use Pipedream to trigger a workflow that sends a notification to a Slack channel or an email to the customer service team. This enables immediate acknowledgment or action concerning the customer's feedback. + +- **Aggregate Reviews for Reporting**: Schedule a daily workflow on Pipedream that collects all the new reviews, summarizes the sentiment and star rating, and creates a report in Google Sheets. This allows for easy monitoring of customer satisfaction trends over time. + +- **Automate Review Response**: Craft a workflow that, upon receiving a new review, checks the star rating. If it's below a certain threshold, automatically draft a response or create a support ticket in Zendesk to ensure that a customer service rep engages with the unhappy customer promptly. diff --git a/components/trustpilot/package.json b/components/trustpilot/package.json index e4768748fd737..4f83c4cbf38e1 100644 --- a/components/trustpilot/package.json +++ b/components/trustpilot/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/tubular/README.md b/components/tubular/README.md new file mode 100644 index 0000000000000..a9d4bb10fe65d --- /dev/null +++ b/components/tubular/README.md @@ -0,0 +1,11 @@ +# Overview + +The Tubular API furnishes you with the tools to craft intricate customer relationship management workflows within Pipedream. Utilize it to automate processes, track sales pipeline progress, manage contacts, and gain insights from your sales data. With Tubular's API, you can seamlessly integrate sales data into other systems, establish notifications for sales updates, and much more, making it simpler for teams to collaborate and stay informed on sales activities. + +# Example Use Cases + +- **Sales Lead Scoring Automation**: When a new lead is added to Tubular, use Pipedream to score the lead based on predefined criteria. The scored leads can then be automatically sorted or tagged in Tubular for prioritization by your sales team. + +- **New Deal Notification**: Configure a workflow that sends real-time notifications via Slack, email, or SMS whenever a new deal is created in Tubular. This keeps your team alerted and can prompt immediate action, ensuring no opportunity slips through the cracks. + +- **Scheduled Sales Report Generation**: Leverage Pipedream to schedule weekly or monthly sales reports. The workflow can extract data from Tubular, format it into a comprehensive report, and then send it to Google Sheets or email it to stakeholders, providing regular insights into sales performance. diff --git a/components/tuesday/package.json b/components/tuesday/package.json new file mode 100644 index 0000000000000..965879ad6e51a --- /dev/null +++ b/components/tuesday/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/tuesday", + "version": "0.0.1", + "description": "Pipedream Tuesday Components", + "main": "tuesday.app.mjs", + "keywords": [ + "pipedream", + "tuesday" + ], + "homepage": "https://pipedream.com/apps/tuesday", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/tuesday/tuesday.app.mjs b/components/tuesday/tuesday.app.mjs new file mode 100644 index 0000000000000..d448255862932 --- /dev/null +++ b/components/tuesday/tuesday.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "tuesday", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/tumblr/README.md b/components/tumblr/README.md index 5d50720970398..6a81a7bc3db7b 100644 --- a/components/tumblr/README.md +++ b/components/tumblr/README.md @@ -1,21 +1,11 @@ # Overview -The Tumblr API allows developers to create applications that integrate with the -popular blogging platform. With this tool, you can make all kinds of -interesting projects and apps that upload, store, and display content from the -Tumblr platform. Here are some examples of what you can build with the Tumblr -API: +The Tumblr API on Pipedream allows you to automate interactions with your Tumblr account, like posting content, managing posts, and interacting with followers. By leveraging this API, you can craft workflows that streamline your social media strategies, analyze engagement, and connect your Tumblr activities with other tools and platforms. Whether you're looking to auto-post content across different networks, analyze post metrics, or curate content based on specific criteria, the Tumblr API paired with Pipedream’s serverless execution model paves the way for powerful automations. -- Photo galleries and image sharing sites -- Social media aggregation and monitoring apps -- Video streaming services -- Music streaming services -- Real-time notifications -- Gamified experiences -- Content distribution networks -- GIF and meme search engines -- Self-publishing apps and mini-sites -- Virtual reality experiences -- Map-based experiences -- Personalized media consumption experiences -- Customizable content displays +# Example Use Cases + +- **Content Syndication Across Platforms**: Automatically post Tumblr content to other social media platforms like Twitter or Facebook when a new post is published. This ensures your content has broader reach without manual reposting. + +- **Post Analytics Dashboard**: Collect data on likes, reblogs, and comments for each Tumblr post and send this data to Google Sheets or a BI tool like Tableau. Use this info to create a dashboard that tracks engagement trends and post performance. + +- **Automated Content Curation**: Set up a workflow that monitors specific tags or searches on Tumblr and automatically reblogs posts that match certain criteria. This helps maintain an active presence and engage with trends or topics relevant to your audience. diff --git a/components/tumblr/package.json b/components/tumblr/package.json new file mode 100644 index 0000000000000..f42f68b689653 --- /dev/null +++ b/components/tumblr/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/tumblr", + "version": "0.6.0", + "description": "Pipedream tumblr Components", + "main": "tumblr.app.mjs", + "keywords": [ + "pipedream", + "tumblr" + ], + "homepage": "https://pipedream.com/apps/tumblr", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/turbohire/README.md b/components/turbohire/README.md index ff35decde4a60..51eb8d9d1eb57 100644 --- a/components/turbohire/README.md +++ b/components/turbohire/README.md @@ -1,24 +1,11 @@ # Overview -The TurboHire API is a powerful tool for businesses to help them hire better -and faster. This API provides tools and services that help to efficiently and -quickly identify, engage, and onboard new hires. Using this API, businesses can -create custom-made experiences that provide engaging job postings and recruiter -communication, along with automated onboarding processes. +TurboHire is a talent acquisition platform that streamlines the hiring process using automation and AI. With its API, you can enrich candidate profiles, automate communication, and trigger actions based on recruitment stages. On Pipedream, you can leverage TurboHire’s API to create powerful automations by connecting it to a multitude of services, thus enhancing the hiring workflow, maintaining candidate databases, and ensuring timely interactions. -The TurboHire API is designed to fit the needs of businesses of all sizes. It's -customizable and easy to use, allowing businesses to quickly implement their -recruitment process and start hiring smarter. +# Example Use Cases -Here are some examples of what you can build using the TurboHire API: +- **Automated Candidate Outreach**: When a candidate is moved to a new stage in TurboHire, use Pipedream to send personalized emails via SendGrid. This keeps candidates informed and engaged without manual intervention. -- Automate job postings, profile analysis, and candidate engagement. -- Create custom recruiting experiences with real-time, candidate-centric job - postings. -- Generate comprehensive analytics on recruitment efforts. -- Automate onboarding process, such as sending welcome emails, collecting - paperwork, and scheduling onboarding calls. -- Easily measure the success of recruiting campaigns using powerful metrics and - analytics. -- Streamline and automate applicant tracking system (ATS) integration with - existing processes. +- **Interview Scheduling**: Sync TurboHire with Google Calendar using Pipedream to automatically schedule interviews when a candidate reaches the interview stage. This can include sending calendar invites to both the candidate and the interviewers. + +- **Candidate Feedback Collection**: After an interview, trigger a feedback form to be sent to the interviewer using Typeform through Pipedream. Collect and store responses in a Google Sheet for easy analysis and decision-making. diff --git a/components/turbot_pipes/README.md b/components/turbot_pipes/README.md new file mode 100644 index 0000000000000..7e6788cae604a --- /dev/null +++ b/components/turbot_pipes/README.md @@ -0,0 +1,11 @@ +# Overview + +The Turbot Pipes API allows for sophisticated data integration and workflow automation across a variety of sources and services. With Turbot Pipes, you can design data pipelines that connect your systems, automate data transformations, and orchestrate tasks to streamline your operations. The API provides methods to create, manage, and execute these data pipelines, which can be especially powerful when leveraged through Pipedream's serverless platform. You can connect Turbot Pipes with numerous other apps available on Pipedream to automate data flows, sync information, and react to events in real-time. + +# Example Use Cases + +- **Automated Data Backup Workflow**: Create a workflow on Pipedream that triggers a Turbot Pipes pipeline to back up your critical data from a primary database to a secondary storage system, such as Amazon S3. This can be scheduled or triggered by specific events, ensuring that your data is consistently and safely backed up. + +- **Real-time CRM and Support Ticket Integration**: Use Turbot Pipes to build an integration between a CRM platform and a support ticketing system. On Pipedream, design a workflow that captures new customer support tickets and uses the Turbot Pipes API to enrich CRM profiles with the latest support interactions, providing a 360-degree customer view to your sales team. + +- **Marketing Analytics Aggregation**: Employ Turbot Pipes through Pipedream to aggregate marketing data from various sources like Google Ads, social media platforms, and email campaigns. You can create a pipeline that collects this data, transforms it into a unified analytics format, and pushes it to a data warehouse or BI tool for in-depth analysis. diff --git a/components/turbot_pipes/package.json b/components/turbot_pipes/package.json index ff248a9d8de0c..82c46d2498da5 100644 --- a/components/turbot_pipes/package.json +++ b/components/turbot_pipes/package.json @@ -15,4 +15,4 @@ "dependencies": { "@pipedream/platform": "^1.6.0" } -} \ No newline at end of file +} diff --git a/components/turso/actions/create-database/create-database.mjs b/components/turso/actions/create-database/create-database.mjs new file mode 100644 index 0000000000000..4ddc8c6c3eea3 --- /dev/null +++ b/components/turso/actions/create-database/create-database.mjs @@ -0,0 +1,47 @@ +import app from "../../turso.app.mjs"; + +export default { + key: "turso-create-database", + name: "Create Database", + description: "Creates a new database in a group for the organization or user. [See the documentation](https://docs.turso.tech/api-reference/databases/create)", + version: "0.0.3", + type: "action", + props: { + app, + organizationName: { + propDefinition: [ + app, + "organizationName", + ], + }, + groupName: { + propDefinition: [ + app, + "groupName", + (c) => ({ + organizationName: c.organizationName, + }), + ], + }, + databaseName: { + propDefinition: [ + app, + "databaseName", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createDatabase({ + $, + organizationName: this.organizationName, + data: { + name: this.databaseName, + groupName: this.groupName, + }, + }); + + $.export("$summary", `Successfully created the database '${this.databaseName}'`); + + return response; + }, +}; diff --git a/components/turso/actions/execute-query/execute-query.mjs b/components/turso/actions/execute-query/execute-query.mjs new file mode 100644 index 0000000000000..f10a7e6f066b4 --- /dev/null +++ b/components/turso/actions/execute-query/execute-query.mjs @@ -0,0 +1,31 @@ +import turso from "../../turso.app.mjs"; + +export default { + key: "turso-execute-query", + name: "Execute SQL Query", + description: "Execute a custom SQLite query. See [our docs](https://pipedream.com/docs/databases/working-with-sql) to learn more about working with SQL in Pipedream.", + type: "action", + version: "0.0.2", + props: { + turso, + // eslint-disable-next-line pipedream/props-description + sql: { + type: "sql", + auth: { + app: "turso", + }, + label: "SQL Query", + }, + }, + async run({ $ }) { + const args = this.turso.executeQueryAdapter(this.sql); + const data = await this.turso.executeQuery({ + $, + ...args, + }); + $.export("$summary", `Returned ${data.rows.length} ${data.rows.length === 1 + ? "row" + : "rows"}`); + return data; + }, +}; diff --git a/components/turso/actions/get-databases/get-databases.mjs b/components/turso/actions/get-databases/get-databases.mjs new file mode 100644 index 0000000000000..01c04500844e8 --- /dev/null +++ b/components/turso/actions/get-databases/get-databases.mjs @@ -0,0 +1,28 @@ +import app from "../../turso.app.mjs"; + +export default { + key: "turso-get-databases", + name: "Get Databases", + description: "Returns a list of databases belonging to the organization or user. [See the documentation](https://docs.turso.tech/api-reference/databases/list)", + version: "0.0.3", + type: "action", + props: { + app, + organizationName: { + propDefinition: [ + app, + "organizationName", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getDatabases({ + $, + organizationName: this.organizationName, + }); + + $.export("$summary", `Successfully retrieved '${response.databases.length}' database(s)`); + + return response; + }, +}; diff --git a/components/turso/actions/get-groups/get-groups.mjs b/components/turso/actions/get-groups/get-groups.mjs new file mode 100644 index 0000000000000..8b668d549b28b --- /dev/null +++ b/components/turso/actions/get-groups/get-groups.mjs @@ -0,0 +1,28 @@ +import app from "../../turso.app.mjs"; + +export default { + key: "turso-get-groups", + name: "Get Groups", + description: "Returns a list of groups belonging to the organization or user. [See the documentation](https://docs.turso.tech/api-reference/groups/list)", + version: "0.0.3", + type: "action", + props: { + app, + organizationName: { + propDefinition: [ + app, + "organizationName", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getGroups({ + $, + organizationName: this.organizationName, + }); + + $.export("$summary", `Successfully retrieved '${response.groups.length}' group(s)`); + + return response; + }, +}; diff --git a/components/turso/actions/get-organizations/get-organizations.mjs b/components/turso/actions/get-organizations/get-organizations.mjs new file mode 100644 index 0000000000000..175eebb7bc107 --- /dev/null +++ b/components/turso/actions/get-organizations/get-organizations.mjs @@ -0,0 +1,21 @@ +import app from "../../turso.app.mjs"; + +export default { + key: "turso-get-organizations", + name: "Get Organizations", + description: "Returns a list of organizations the authenticated user owns or is a member of. [See the documentation](https://docs.turso.tech/api-reference/organizations/list)", + version: "0.0.3", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.getOrganizations({ + $, + }); + + $.export("$summary", `Successfully retrieved '${response.length}' organization(s)`); + + return response; + }, +}; diff --git a/components/turso/package.json b/components/turso/package.json new file mode 100644 index 0000000000000..9fa4e477ff883 --- /dev/null +++ b/components/turso/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/turso", + "version": "0.2.1", + "description": "Pipedream Turso Components", + "main": "turso.app.mjs", + "keywords": [ + "pipedream", + "turso" + ], + "homepage": "https://pipedream.com/apps/turso", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/turso/turso.app.mjs b/components/turso/turso.app.mjs new file mode 100644 index 0000000000000..4032d41719cdf --- /dev/null +++ b/components/turso/turso.app.mjs @@ -0,0 +1,204 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "turso", + propDefinitions: { + organizationName: { + type: "string", + label: "Organization Name", + description: "Name of the organization", + async options() { + const orgNames = await this.getOrganizations({}); + return orgNames.map(({ slug }) => ({ + value: slug, + })); + }, + }, + groupName: { + type: "string", + label: "Group Name", + description: "Name of the group", + async options({ organizationName }) { + const response = await this.getGroups({ + organizationName, + }); + const groupsNames = response.groups; + return groupsNames.map(({ name }) => ({ + value: name, + })); + }, + }, + databaseName: { + type: "string", + label: "Database Name", + description: "Name of the database", + }, + }, + methods: { + _baseUrl() { + return "https://api.turso.tech/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_token}`, + }, + }); + }, + createDatabase({ + organizationName, ...args + }) { + return this._makeRequest({ + method: "post", + path: `/organizations/${organizationName}/databases`, + ...args, + }); + }, + getDatabases({ + organizationName, ...args + }) { + return this._makeRequest({ + path: `/organizations/${organizationName}/databases`, + ...args, + }); + }, + getGroups({ + organizationName, ...args + }) { + return this._makeRequest({ + path: `/organizations/${organizationName}/groups`, + ...args, + }); + }, + getOrganizations(args = {}) { + return this._makeRequest({ + path: "/organizations", + ...args, + }); + }, + /** + * A helper method to get the schema of the database. Used by other features + * (like the `sql` prop) to enrich the code editor and provide the user with + * auto-complete and fields suggestion. + * + * @returns {DbInfo} The schema of the database, which is a + * JSON-serializable object. + */ + async getSchema() { + const { rows } = await this.executeQuery({ + sql: `SELECT m.name AS tableName, ti.name AS columnName, ti.type AS dataType, ti.[notnull] AS isNullable, ti.[dflt_value] AS columnDefault + FROM sqlite_master AS m + JOIN pragma_table_info(m.name) AS ti + WHERE m.type = 'table' ORDER BY m.name, ti.cid`, + }); + const schema = {}; + for (const row of rows) { + const { rows: count } = await this.executeQuery({ + sql: `SELECT COUNT (*) as count FROM ${row.tableName}`, + }); + schema[row.tableName] = { + metadata: { + rowCount: count[0].count, + }, + schema: { + ...schema[row.tableName]?.schema, + [row.columnName]: { + columnDefault: row.columnDefault, + dataType: row.dataType, + isNullable: row.isNullable, + }, + }, + }; + } + return schema; + }, + /** + * A method that performs the inverse transformation of `proxyAdapter`. + * + * @param {object} proxyArgs - The output of `proxyAdapter`. + * @param {string} proxyArgs.query - The SQL query to be executed. + * @param {string[]} proxyArgs.params - The values to replace in the SQL + * query. + * @returns {object} - The adapted query and parameters, compatible with + * `executeQuery`. + */ + executeQueryAdapter(proxyArgs = {}) { + let { query: sql } = proxyArgs; + const params = proxyArgs?.params || []; + for (const param of params) { + sql = sql.replace("?", param); + } + sql = sql.replaceAll("\n", " "); + return { + sql, + }; + }, + /** + * Executes a query against the MySQL database. This method takes care of + * connecting to the database, executing the query, and closing the + * connection. + * @param {object} preparedStatement - The prepared statement to be sent to the DB. + * @param {string} preparedStatement.sql - The prepared SQL query to be executed. + * @param {string[]} preparedStatement.values - The values to replace in the SQL query. + * @returns {object[]} - The rows returned by the DB as a result of the query. + */ + async executeQuery({ + sql, $ = this, + }) { + const { + organization_slug: organizationName, database_name: databaseName, oauth_access_token: jwt, + } = this.$auth; + const { results } = await axios($, { + method: "POST", + url: `https://${databaseName}-${organizationName}.turso.io/v2/pipeline`, + headers: { + Authorization: `Bearer ${jwt}`, + }, + data: { + requests: [ + { + type: "execute", + stmt: { + sql, + }, + }, + { + type: "close", + }, + ], + }, + }); + if (results[0].type === "error") { + throw new Error(`${results[0].error.message}`); + } + const { + cols = [], rows = [], ...data + } = results[0].response.result; + + // format response + const response = []; + const columnNames = cols.map(({ name }) => name); + rows.forEach((row) => { + const newRow = {}; + for (let i = 0; i < columnNames.length; i++) { + newRow[columnNames[i]] = row[i].value; + } + response.push(newRow); + }); + return { + rows: response, + ...data, + }; + }, + }, +}; diff --git a/components/tutor_lms/README.md b/components/tutor_lms/README.md new file mode 100644 index 0000000000000..17b0247bccb85 --- /dev/null +++ b/components/tutor_lms/README.md @@ -0,0 +1,11 @@ +# Overview + +The Tutor LMS API provides hooks into the Tutor LMS ecosystem, enabling you to automate actions and manage data around courses, lessons, quizzes, and results within the learning management system. With Pipedream, you can build workflows that react to events in Tutor LMS, such as new course enrollments, or that push data to Tutor LMS to create or update resources. Utilizing Pipedream's ability to connect to multiple services, you can synchronize Tutor LMS data with other apps, trigger notifications, and streamline administrative tasks. + +# Example Use Cases + +- **Course Enrollment Notifications**: Trigger a workflow whenever a new student enrolls in a course. This workflow could send an email via SendGrid to the course instructor with the student's information and enrollment details. + +- **Synchronize Course Data with Google Sheets**: Keep a Google Sheets spreadsheet updated with the latest course information. Whenever a course is added or updated in Tutor LMS, a Pipedream workflow can automatically push the changes to a dedicated Google Sheets document, ensuring easy access to up-to-date course listings. + +- **Automated Certificate Generation**: When a student completes a course, trigger a workflow to generate a certificate using a service like DocuSign or Adobe Sign. The signed certificate can then be emailed directly to the student or uploaded to their profile in Tutor LMS. diff --git a/components/twelve_data/README.md b/components/twelve_data/README.md index 189e840093f67..f061fa0623589 100644 --- a/components/twelve_data/README.md +++ b/components/twelve_data/README.md @@ -1,30 +1,11 @@ # Overview -The Twelve Data API is a powerful and flexible tool that enables you to quickly -access real-time and historical market data from global exchanges. With this -tool, you can build: +The Twelve Data API provides access to real-time and historical stock market data, making it a powerful tool for financial analysis and trading strategy development. With this API, users can retrieve stock prices, forex data, indicators, and more, delivering the necessary information to monitor market trends closely. On Pipedream, you can leverage this data to create custom, serverless workflows that react to market changes, automate trading analysis, or integrate financial data into other apps and services for informed decision-making. -- Custom charts for any financial and market data, with tools to compare the - data points side-by-side. -- Technical analysis tools such as trend lines and indicators, and -- Automated trading platforms and monitoring solutions to detect market trends - and important data points. +# Example Use Cases -The Twelve Data API provides access to real-time and historical market data for -numerous exchanges and asset types including stocks, commodities, currencies, -cryptocurrencies, indices, options, and more. It also supports streaming data -via WebSocket or HTTP. +- **Automated Trading Signals**: Set up a workflow that monitors specific indicators or stock price thresholds. When certain conditions are met, you could trigger trade execution through a brokerage API or send a notification to execute a manual trade. -Here are some examples of what you can build with Twelve Data API: +- **Market Trend Dashboard**: Create a workflow to fetch market data at regular intervals, updating a live dashboard. Integrate with Google Sheets or Data Studio to visualize the data for quick analysis, enabling you to spot trends or market movements as they happen. -- Data-driven apps such as portfolio analysis and tracking, investment - simulation and manipulation, and real-time position management. -- Real-time alerting systems that track traders’ positions and send - notifications in the event of a key market move. -- Indicator and trading system analysis to detect patterns and trends in the - market. -- Algorithmic trading systems or models to automate or optimize trades based on - various criteria. -- A mobile app for quickly and easily monitoring market data on the go. -- A web-based platform for visualizing and analyzing financial data. -- A chatbot for providing financial advice and portfolio management. +- **Sentiment Analysis Alerts**: Combine Twelve Data API with a sentiment analysis service to gauge market sentiment on particular stocks or the market overall. When sentiment shifts significantly, trigger alerts through email or messaging services like Slack to inform stakeholders or adjust trading strategies promptly. diff --git a/components/twenty/actions/create-update-delete-record/create-update-delete-record.mjs b/components/twenty/actions/create-update-delete-record/create-update-delete-record.mjs new file mode 100644 index 0000000000000..8ea2961cf67c0 --- /dev/null +++ b/components/twenty/actions/create-update-delete-record/create-update-delete-record.mjs @@ -0,0 +1,137 @@ +import { + camelCaseToWords, parseObject, +} from "../../common/utils.mjs"; +import twenty from "../../twenty.app.mjs"; + +export default { + key: "twenty-create-update-delete-record", + name: "Create, Update, or Delete a Record in Twenty", + description: "Create, update, or delete a single record in Twenty. This action allows for dynamic handling of records based on specified action type. [See the documentation](https://api.twenty.com/docs)", + version: "0.0.1", + type: "action", + props: { + twenty, + recordId: { + propDefinition: [ + twenty, + "recordId", + ], + reloadProps: true, + }, + actionType: { + propDefinition: [ + twenty, + "actionType", + ], + reloadProps: true, + }, + additionalProp: { + type: "object", + label: "Additional Prop", + description: "Any additional prop you want to fill.", + optional: true, + }, + }, + async additionalProps() { + const props = {}; + const recordId = this.recordId; + const actionType = this.actionType; + + if (recordId && this.actionType) { + if ([ + "delete", + "update", + ].includes(actionType)) { + props.id = { + type: "string", + label: `${camelCaseToWords(recordId)} Id`, + description: `The Id of ${recordId}`, + options: async () => { + const { + components: { schemas }, tags, + } = await this.twenty.listRecords(); + const index = Object.keys(schemas).findIndex((i) => i === this.recordId); + const { data } = await this.twenty.listRecordItems(tags[index + 1].name); + const response = data[tags[index + 1].name]; + return response.map((item) => ({ + label: item.name || item.title || item.id, + value: item.id, + })); + }, + }; + } + + if ([ + "create", + "update", + ].includes(actionType)) { + + const { components: { schemas } } = await this.twenty.listRecords(); + const properties = schemas[recordId].properties; + const required = schemas[recordId].required || []; + + for (const [ + key, + value, + ] of Object.entries(properties)) { + if ( + (key != "id") && + (key != "createdAt") && + (key != "updatedAt") && + (value.type) && + (value.type != "object") && + (value.type != "array") + ) { + props[key] = { + type: value["type"] === "number" + ? "integer" + : value["type"], + label: camelCaseToWords(key), + description: value["description"], + optional: !(required.includes === key), + }; + } + } + } + } + return props; + }, + async run({ $ }) { + const { + twenty, + id, + recordId, + actionType, + additionalProp, + ...data + } = this; + + let response; + + const { + components: { schemas }, tags, + } = await this.twenty.listRecords(); + const index = Object.keys(schemas).findIndex((i) => i === this.recordId); + + try { + response = await twenty.performAction({ + $, + id, + actionType: this.actionType, + recordName: tags[index + 1].name, + data: { + ...data, + ...(additionalProp + ? parseObject(additionalProp) + : {}), + }, + }); + + $.export("$summary", `Successfully performed ${actionType} ${recordId} on record with ID: ${id || response.data[`${actionType}${recordId}`].id}`); + + return response; + } catch (error) { + throw new Error(`Failed to ${actionType} record. Error: ${error.message}`); + } + }, +}; diff --git a/components/twenty/common/utils.mjs b/components/twenty/common/utils.mjs new file mode 100644 index 0000000000000..f097aeac6c1b2 --- /dev/null +++ b/components/twenty/common/utils.mjs @@ -0,0 +1,31 @@ +export const camelCaseToWords = (s) => { + const result = s.replace(/([A-Z])/g, " $1"); + return result.charAt(0).toUpperCase() + result.slice(1); +}; + +export const capitalizeFirstLetter = (string) => { + return string.charAt(0).toLowerCase() + string.slice(1); +}; + +export const parseObject = (obj) => { + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/twenty/package.json b/components/twenty/package.json new file mode 100644 index 0000000000000..17a8e53d2d811 --- /dev/null +++ b/components/twenty/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/twenty", + "version": "0.1.0", + "description": "Pipedream Twenty Components", + "main": "twenty.app.mjs", + "keywords": [ + "pipedream", + "twenty" + ], + "homepage": "https://pipedream.com/apps/twenty", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} + diff --git a/components/twenty/sources/new-record-modified-instant/new-record-modified-instant.mjs b/components/twenty/sources/new-record-modified-instant/new-record-modified-instant.mjs new file mode 100644 index 0000000000000..3209ad0503310 --- /dev/null +++ b/components/twenty/sources/new-record-modified-instant/new-record-modified-instant.mjs @@ -0,0 +1,45 @@ +import twenty from "../../twenty.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "twenty-new-record-modified-instant", + name: "New Record Modified (Instant)", + description: "Emit new event when a record is created, updated, or deleted.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + twenty, + http: "$.interface.http", + db: "$.service.db", + }, + hooks: { + async activate() { + const { data } = await this.twenty.createHook({ + data: { + targetUrl: this.http.endpoint, + operation: "*.*", + }, + }); + + this.db.set("webhookId", data.createWebhook.id); + }, + async deactivate() { + const webhookId = this.db.get("webhookId"); + await this.twenty.deleteHook(webhookId); + }, + }, + async run(event) { + const { body } = event; + + const eventType = body.eventType; + const eventName = eventType.split(".")[0]; + + this.$emit(body, { + id: `${body.objectMetadata.id}-${body.eventDate}`, + summary: `New ${body.objectMetadata.nameSingular} ${eventName}d with Id: ${body.objectMetadata.id}.`, + ts: Date.parse(body.eventDate) || Date.now(), + }); + }, + sampleEmit, +}; diff --git a/components/twenty/sources/new-record-modified-instant/test-event.mjs b/components/twenty/sources/new-record-modified-instant/test-event.mjs new file mode 100644 index 0000000000000..92f4ea7a28bb8 --- /dev/null +++ b/components/twenty/sources/new-record-modified-instant/test-event.mjs @@ -0,0 +1,61 @@ +export default { + "targetUrl": "http://webhook.url", + "eventType": "*.*", + "objectMetadata": { + "id": "12345678-1234-1234-1234-123456789012", + "nameSingular": "company" + }, + "workspaceId": "12345678-1234-1234-1234-123456789012", + "webhookId": "12345678-1234-1234-1234-123456789012", + "eventDate": "2024-05-07T14:16:39.810Z", + "record": { + "id": "12345678-1234-1234-1234-123456789012", + "name": "company test", + "people": { + "edges": [], + "__typename": "personConnection" + }, + "address": "", + "position": 1, + "createdAt": "2024-05-07T14:16:39.350276+00:00", + "employees": null, + "favorites": { + "edges": [], + "__typename": "favoriteConnection" + }, + "updatedAt": "2024-05-07T14:16:39.350276+00:00", + "__typename": "company", + "domainName": "", + "attachments": { + "edges": [], + "__typename": "attachmentConnection" + }, + "accountOwner": null, + "opportunities": { + "edges": [], + "__typename": "opportunityConnection" + }, + "accountOwnerId": null, + "activityTargets": { + "edges": [], + "__typename": "activityTargetConnection" + }, + "timelineActivities": { + "edges": [], + "__typename": "timelineActivityConnection" + }, + "idealCustomerProfile": false, + "xLink": { + "url": "", + "label": "" + }, + "linkedinLink": { + "url": "", + "label": "" + }, + "annualRecurringRevenue": { + "amountMicros": null, + "currencyCode": "" + } + } +} \ No newline at end of file diff --git a/components/twenty/twenty.app.mjs b/components/twenty/twenty.app.mjs new file mode 100644 index 0000000000000..f3e11a5cca4b9 --- /dev/null +++ b/components/twenty/twenty.app.mjs @@ -0,0 +1,139 @@ +import { axios } from "@pipedream/platform"; +import { + camelCaseToWords, capitalizeFirstLetter, +} from "./common/utils.mjs"; + +export default { + type: "app", + app: "twenty", + propDefinitions: { + recordId: { + type: "string", + label: "Record ID", + description: "The ID of the record to update or delete.", + async options() { + const { components: { schemas } } = await this.listRecords(); + + return Object.entries(schemas).filter(([ + key, + ]) => key != "Attachment") + .map(([ + key, + ]) => ({ + label: camelCaseToWords(key), + value: key, + })); + }, + }, + actionType: { + type: "string", + label: "Action Type", + description: "Specify the action to perform: create, update, or delete.", + options: [ + { + label: "Create", + value: "create", + }, + { + label: "Update", + value: "update", + }, + { + label: "Delete", + value: "delete", + }, + ], + default: "create", + }, + }, + methods: { + _baseUrl() { + return "https://api.twenty.com"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listRecords() { + return this._makeRequest({ + path: "/open-api/core", + }); + }, + listRecordItems(recordId) { + return this._makeRequest({ + path: `/rest/${capitalizeFirstLetter(recordId)}`, + }); + }, + createHook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/rest/webhooks", + ...opts, + }); + }, + deleteHook(hookId) { + return this._makeRequest({ + method: "DELETE", + path: `/rest/webhooks/${hookId}`, + }); + }, + createRecord({ + recordName, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/rest/${recordName}`, + ...opts, + }); + }, + updateRecord({ + recordName, id, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/rest/${recordName}/${id}`, + ...opts, + }); + }, + deleteRecord({ + id, recordName, + }) { + return this._makeRequest({ + method: "DELETE", + path: `/rest/${recordName}/${id}`, + }); + }, + performAction({ + actionType, id, recordName, ...opts + }) { + switch (actionType) { + case "create": + return this.createRecord({ + recordName, + ...opts, + }); + case "update": + return this.updateRecord({ + recordName, + id, + ...opts, + }); + case "delete": + return this.deleteRecord({ + id, + recordName, + }); + } + }, + }, +}; diff --git a/components/twilio/README.md b/components/twilio/README.md index ca70f2c70d3dd..d19342f233dfb 100644 --- a/components/twilio/README.md +++ b/components/twilio/README.md @@ -1,14 +1,11 @@ # Overview -With the Twilio API, you can build telephone applications that make and receive -phone calls, as well astext messaging applications that send and receive text -messages. - -Some examples of applications you could build include: - -- A phone call application that allows you to make and receive phone calls over - the internet -- A text messaging application that allows you to send and receive text - messages over the internet -- A voicemail application that allows you to leave and receive voicemails over - the internet +Twilio's API on Pipedream opens up a multitude of communication capabilities, allowing you to build robust, scalable, and automated workflows. With Twilio, you can send and receive SMS and MMS messages, make voice calls, and perform other communication functions programmatically. Leveraging Pipedream's seamless integration, you can connect these communications features with hundreds of other services to automate notifications, streamline customer interactions, and enhance operational efficiency. + +# Example Use Cases + +- **SMS Alert System for eCommerce Order Updates**: Connect Twilio with Shopify to send SMS updates to customers when their order status changes. Apply conditions to trigger messages for order confirmation, shipment tracking, and delivery notifications, adding a personal touch to the online shopping experience. + +- **Automated Customer Support Ticketing**: Use Twilio with Zendesk to create a support ticket when a customer sends a text message to your Twilio number. This workflow automates the ticket generation process, ensuring that customer issues are promptly recorded and queued for support staff attention without manual entry. + +- **Two-Factor Authentication (2FA) for App Security**: Integrate Twilio with your custom app's authentication system to send a one-time passcode via SMS for 2FA. Enhance security by incorporating an extra verification step, making it tougher for unauthorized users to gain access to sensitive information or accounts. diff --git a/components/twilio/actions/check-verification-token/check-verification-token.mjs b/components/twilio/actions/check-verification-token/check-verification-token.mjs index 4105ea089dba8..50b5c7d04f635 100644 --- a/components/twilio/actions/check-verification-token/check-verification-token.mjs +++ b/components/twilio/actions/check-verification-token/check-verification-token.mjs @@ -1,11 +1,12 @@ import app from "../../twilio.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; export default { key: "twilio-check-verification-token", name: "Check Verification Token", - description: "Check if user-provided token is correct. [See the documentation](https://www.twilio.com/docs/verify/api) for more information", + description: "Check if user-provided token is correct. [See the documentation](https://www.twilio.com/docs/verify/api)", type: "action", - version: "0.0.1", + version: "0.0.3", props: { app, serviceSid: { @@ -27,12 +28,21 @@ export default { }, }, async run({ $ }) { - const res = await this.app.checkVerificationToken( - this.serviceSid, - this.to, - this.code, - ); - $.export("$summary", `Successfully fetched SMS verification of "${this.to}"`); - return res; + try { + const res = await this.app.checkVerificationToken( + this.serviceSid, + this.to, + this.code, + ); + $.export("$summary", `Verification code is ${res.valid + ? "" + : "not"} valid`); + return res; + } catch (e) { + if (e.status === 404) { + throw new ConfigurationError("Verification ID not found. Twilio deletes the verification SID once it's expired (10 minutes), approved, or the max attempts to check a code have been reached."); + } + throw new ConfigurationError(JSON.stringify(e)); + } }, }; diff --git a/components/twilio/actions/create-verification-service/create-verification-service.mjs b/components/twilio/actions/create-verification-service/create-verification-service.mjs new file mode 100644 index 0000000000000..a21260daa8d01 --- /dev/null +++ b/components/twilio/actions/create-verification-service/create-verification-service.mjs @@ -0,0 +1,24 @@ +import twilio from "../../twilio.app.mjs"; + +export default { + key: "twilio-create-verification-service", + name: "Create Verification Service", + description: "Create a verification service for sending SMS verifications. [See the documentation](https://www.twilio.com/docs/verify/api/service#create-a-verification-service)", + type: "action", + version: "0.0.2", + props: { + twilio, + friendlyName: { + type: "string", + label: "Friendly Name", + description: "The name of the new verification service", + }, + }, + async run({ $ }) { + const response = await this.twilio.createVerificationService({ + friendlyName: this.friendlyName, + }); + $.export("$summary", `Successfully created verification service with SID: ${response.sid}"`); + return response; + }, +}; diff --git a/components/twilio/actions/delete-call/delete-call.mjs b/components/twilio/actions/delete-call/delete-call.mjs index db474a862f095..b1c2253b0de8c 100644 --- a/components/twilio/actions/delete-call/delete-call.mjs +++ b/components/twilio/actions/delete-call/delete-call.mjs @@ -3,8 +3,8 @@ import twilio from "../../twilio.app.mjs"; export default { key: "twilio-delete-call", name: "Delete Call", - description: "Remove a call record from your account. [See the docs](https://www.twilio.com/docs/voice/api/call-resource#delete-a-call-resource) for more information", - version: "0.1.2", + description: "Remove a call record from your account. [See the documentation](https://www.twilio.com/docs/voice/api/call-resource#delete-a-call-resource)", + version: "0.1.4", type: "action", props: { twilio, @@ -13,6 +13,7 @@ export default { twilio, "sid", ], + optional: false, }, }, async run({ $ }) { diff --git a/components/twilio/actions/delete-message/delete-message.mjs b/components/twilio/actions/delete-message/delete-message.mjs index 3a5dd9e6fe122..75d98ecbb7286 100644 --- a/components/twilio/actions/delete-message/delete-message.mjs +++ b/components/twilio/actions/delete-message/delete-message.mjs @@ -3,8 +3,8 @@ import twilio from "../../twilio.app.mjs"; export default { key: "twilio-delete-message", name: "Delete Message", - description: "Delete a message record from your account. [See the docs](https://www.twilio.com/docs/sms/api/message-resource#delete-a-message-resource) for more information", - version: "0.1.2", + description: "Delete a message record from your account. [See the documentation](https://www.twilio.com/docs/sms/api/message-resource#delete-a-message-resource)", + version: "0.1.4", type: "action", props: { twilio, @@ -13,6 +13,7 @@ export default { twilio, "messageId", ], + optional: false, }, }, async run({ $ }) { diff --git a/components/twilio/actions/download-recording-media/download-recording-media.mjs b/components/twilio/actions/download-recording-media/download-recording-media.mjs index 0849260fac1b2..33a9340e6f14a 100644 --- a/components/twilio/actions/download-recording-media/download-recording-media.mjs +++ b/components/twilio/actions/download-recording-media/download-recording-media.mjs @@ -1,14 +1,14 @@ import twilio from "../../twilio.app.mjs"; -import got from "got@13.0.0"; import stream from "stream"; import { promisify } from "util"; import fs from "fs"; +import { axios } from "@pipedream/platform"; export default { key: "twilio-download-recording-media", name: "Download Recording Media", - description: "Download a recording media file. [See the docs](https://www.twilio.com/docs/voice/api/recording#fetch-a-recording-media-file) for more information", - version: "0.1.4", + description: "Download a recording media file. [See the documentation](https://www.twilio.com/docs/voice/api/recording#fetch-a-recording-media-file)", + version: "0.1.6", type: "action", props: { twilio, @@ -27,7 +27,21 @@ export default { filePath: { type: "string", label: "File Path", - description: "The destination path in [`/tmp`](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory) for the downloaded the file (e.g., `/tmp/myFile.mp3`)", + description: "The destination path in [`/tmp`](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory) for the downloaded the file (e.g., `/tmp/myFile.mp3`). Make sure to include the file extension.", + }, + }, + methods: { + getFileStream({ + $, downloadUrl, + }) { + return axios($, { + url: downloadUrl, + auth: { + username: `${this.twilio.$auth.AccountSid}`, + password: `${this.twilio.$auth.AuthToken}`, + }, + responseType: "stream", + }); }, }, async run({ $ }) { @@ -39,10 +53,16 @@ export default { // Add chosen download format extension (e.g. ".mp3"), as specified in the Twilio API docs: // https://www.twilio.com/docs/voice/api/recording#fetch-a-recording-media-file const downloadUrl = uri + this.format; + const fileStream = await this.getFileStream({ + $, + downloadUrl, + }); const pipeline = promisify(stream.pipeline); const resp = await pipeline( - got.stream(downloadUrl), - fs.createWriteStream(this.filePath), + fileStream, + fs.createWriteStream(this.filePath.includes("/tmp") + ? this.filePath + : `/tmp/${this.filePath}`), ); $.export("$summary", `Successfully downloaded the recording media file to "${this.filePath}"`); return resp; diff --git a/components/twilio/actions/get-call/get-call.mjs b/components/twilio/actions/get-call/get-call.mjs index 66f73f4334812..a5c9c49f7eb8d 100644 --- a/components/twilio/actions/get-call/get-call.mjs +++ b/components/twilio/actions/get-call/get-call.mjs @@ -4,8 +4,8 @@ import { callToString } from "../../common/utils.mjs"; export default { key: "twilio-get-call", name: "Get Call", - description: "Return call resource of an individual call. [See the docs](https://www.twilio.com/docs/voice/api/call-resource#fetch-a-call-resource) for more information", - version: "0.1.2", + description: "Return call resource of an individual call. [See the documentation](https://www.twilio.com/docs/voice/api/call-resource#fetch-a-call-resource)", + version: "0.1.4", type: "action", props: { twilio, @@ -14,10 +14,62 @@ export default { twilio, "sid", ], + optional: false, + }, + includeTranscripts: { + type: "boolean", + label: "Include Transcripts", + description: "Set to `true` to include recording transcript(s) if available", + optional: true, + }, + }, + methods: { + async getTranscripts(callSid) { + const transcripts = []; + const recordings = await this.twilio.listRecordings({ + callSid, + }); + for (const recording of recordings) { + const recordingTranscripts = await this.getRecordingTranscripts(recording.sid); + if (recordingTranscripts?.length) { + transcripts.push(...recordingTranscripts); + } + } + return transcripts; + }, + async getRecordingTranscripts(sourceSid) { + const transcripts = await this.twilio.listTranscripts({ + sourceSid, + }); + const results = []; + for (const transcript of transcripts) { + const { + sentences, transcript: fullTranscript, + } = await this.twilio.getSentences(transcript.sid); + results.push({ + ...transcript, + _version: undefined, + sentences, + transcript: fullTranscript, + }); + } + return results; }, }, async run({ $ }) { - const resp = await this.twilio.getCall(this.sid); + let resp = await this.twilio.getCall(this.sid); + + if (this.includeTranscripts) { + const transcripts = await this.getTranscripts(this.sid); + if (transcripts?.length) { + resp = { + ...resp, + _version: undefined, + transcripts, + }; + } + } + $.export("$summary", `Successfully fetched the call, "${callToString(resp)}"`); return resp; }, diff --git a/components/twilio/actions/get-message/get-message.mjs b/components/twilio/actions/get-message/get-message.mjs index af73b53d15b4d..f5f65eed531f2 100644 --- a/components/twilio/actions/get-message/get-message.mjs +++ b/components/twilio/actions/get-message/get-message.mjs @@ -4,8 +4,8 @@ import { messageToString } from "../../common/utils.mjs"; export default { key: "twilio-get-message", name: "Get Message", - description: "Return details of a message. [See the docs](https://www.twilio.com/docs/sms/api/message-resource#fetch-a-message-resource) for more information", - version: "0.1.2", + description: "Return details of a message. [See the documentation](https://www.twilio.com/docs/sms/api/message-resource#fetch-a-message-resource)", + version: "0.1.4", type: "action", props: { twilio, @@ -14,6 +14,7 @@ export default { twilio, "messageId", ], + optional: false, }, }, async run({ $ }) { diff --git a/components/twilio/actions/get-transcripts/get-transcripts.mjs b/components/twilio/actions/get-transcripts/get-transcripts.mjs new file mode 100644 index 0000000000000..8fa68dcc6061d --- /dev/null +++ b/components/twilio/actions/get-transcripts/get-transcripts.mjs @@ -0,0 +1,40 @@ +import twilio from "../../twilio.app.mjs"; + +export default { + key: "twilio-get-transcripts", + name: "Get Transcripts", + description: "Retrieves full transcripts for the specified transcript SIDs. [See the documentation](https://www.twilio.com/docs/voice/intelligence/api/transcript-sentence-resource#get-transcript-sentences)", + version: "0.0.1", + type: "action", + props: { + twilio, + transcriptSids: { + propDefinition: [ + twilio, + "transcriptSids", + ], + }, + }, + async run({ $ }) { + const transcripts = []; + for (const sid of this.transcriptSids) { + transcripts.push(await this.twilio.getTranscript(sid)); + } + const results = []; + for (const transcript of transcripts) { + const { + sentences, transcript: fullTranscript, + } = await this.twilio.getSentences(transcript.sid); + results.push({ + ...transcript, + _version: undefined, + sentences, + transcript: fullTranscript, + }); + } + $.export("$summary", `Successfully fetched ${results.length} transcript${results.length === 1 + ? "" + : "s"}`); + return results; + }, +}; diff --git a/components/twilio/actions/list-calls/list-calls.mjs b/components/twilio/actions/list-calls/list-calls.mjs index 14ea713870a29..66276d614c32d 100644 --- a/components/twilio/actions/list-calls/list-calls.mjs +++ b/components/twilio/actions/list-calls/list-calls.mjs @@ -4,8 +4,8 @@ import { omitEmptyStringValues } from "../../common/utils.mjs"; export default { key: "twilio-list-calls", name: "List Calls", - description: "Return a list of calls associated with your account. [See the docs](https://www.twilio.com/docs/voice/api/call-resource#read-multiple-call-resources) for more information", - version: "0.1.2", + description: "Return a list of calls associated with your account. [See the documentation](https://www.twilio.com/docs/voice/api/call-resource#read-multiple-call-resources)", + version: "0.1.4", type: "action", props: { twilio, diff --git a/components/twilio/actions/list-message-media/list-message-media.mjs b/components/twilio/actions/list-message-media/list-message-media.mjs index d9c611feed2da..dddef1db1ec6e 100644 --- a/components/twilio/actions/list-message-media/list-message-media.mjs +++ b/components/twilio/actions/list-message-media/list-message-media.mjs @@ -4,8 +4,8 @@ import { omitEmptyStringValues } from "../../common/utils.mjs"; export default { key: "twilio-list-message-media", name: "List Message Media", - description: "Return a list of media associated with your message. [See the docs](https://www.twilio.com/docs/sms/api/media-resource#read-multiple-media-resources) for more information", - version: "0.1.2", + description: "Return a list of media associated with your message. [See the documentation](https://www.twilio.com/docs/sms/api/media-resource#read-multiple-media-resources)", + version: "0.1.4", type: "action", props: { twilio, @@ -14,6 +14,7 @@ export default { twilio, "messageId", ], + optional: false, }, limit: { propDefinition: [ diff --git a/components/twilio/actions/list-messages/list-messages.mjs b/components/twilio/actions/list-messages/list-messages.mjs index b356b5cc49c50..78da1e45289e5 100644 --- a/components/twilio/actions/list-messages/list-messages.mjs +++ b/components/twilio/actions/list-messages/list-messages.mjs @@ -5,8 +5,8 @@ import { omitEmptyStringValues } from "../../common/utils.mjs"; export default { key: "twilio-list-messages", name: "List Messages", - description: "Return a list of messages associated with your account. [See the docs](https://www.twilio.com/docs/sms/api/message-resource#read-multiple-message-resources) for more information", - version: "0.1.3", + description: "Return a list of messages associated with your account. [See the documentation](https://www.twilio.com/docs/sms/api/message-resource#read-multiple-message-resources)", + version: "0.1.5", type: "action", props: { twilio, diff --git a/components/twilio/actions/list-recording-transcriptions/list-recording-transcriptions.mjs b/components/twilio/actions/list-recording-transcriptions/list-recording-transcriptions.mjs deleted file mode 100644 index 530426e61aed0..0000000000000 --- a/components/twilio/actions/list-recording-transcriptions/list-recording-transcriptions.mjs +++ /dev/null @@ -1,37 +0,0 @@ -import twilio from "../../twilio.app.mjs"; -import { omitEmptyStringValues } from "../../common/utils.mjs"; - -export default { - key: "twilio-list-recording-transcriptions", - name: "List Recording Transcriptions", - description: "Return a set of transcriptions available for a recording. [See the docs](https://www.twilio.com/docs/voice/api/recording#fetch-a-recordings-transcriptions) for more information", - version: "0.1.2", - type: "action", - props: { - twilio, - recordingID: { - propDefinition: [ - twilio, - "recordingID", - ], - }, - limit: { - propDefinition: [ - twilio, - "limit", - ], - }, - }, - async run({ $ }) { - const resp = await this.twilio.listRecordingTranscriptions( - this.recordingID, - omitEmptyStringValues({ - limit: this.limit, - }), - ); - $.export("$summary", `Successfully fetched ${resp.length} recording transcription${resp.length === 1 - ? "" - : "s"} for the recording, "${this.recordingID}"`); - return resp; - }, -}; diff --git a/components/twilio/actions/list-transcripts/list-transcripts.mjs b/components/twilio/actions/list-transcripts/list-transcripts.mjs new file mode 100644 index 0000000000000..012970cf1740c --- /dev/null +++ b/components/twilio/actions/list-transcripts/list-transcripts.mjs @@ -0,0 +1,51 @@ +import twilio from "../../twilio.app.mjs"; +import { omitEmptyStringValues } from "../../common/utils.mjs"; + +export default { + key: "twilio-list-transcripts", + name: "List Transcripts", + description: "Return a list of transcripts. [See the documentation](https://www.twilio.com/docs/voice/intelligence/api/transcript-resource#fetch-multiple-transcripts)", + version: "0.0.2", + type: "action", + props: { + twilio, + includeTranscriptText: { + propDefinition: [ + twilio, + "includeTranscriptText", + ], + }, + limit: { + propDefinition: [ + twilio, + "limit", + ], + }, + }, + async run({ $ }) { + let results = await this.twilio.listTranscripts( + omitEmptyStringValues({ + limit: this.limit, + }), + ); + if (this.includeTranscriptText) { + const transcripts = []; + for (const result of results) { + const { + sentences, transcript, + } = await this.twilio.getSentences(result.sid); + transcripts.push({ + ...result, + _version: undefined, + sentences, + transcript, + }); + } + results = transcripts; + } + $.export("$summary", `Successfully fetched ${results.length} transcript${results.length === 1 + ? "" + : "s"}`); + return results; + }, +}; diff --git a/components/twilio/actions/make-phone-call/make-phone-call.mjs b/components/twilio/actions/make-phone-call/make-phone-call.mjs index 8921dff338435..5ba8a59c2bb12 100644 --- a/components/twilio/actions/make-phone-call/make-phone-call.mjs +++ b/components/twilio/actions/make-phone-call/make-phone-call.mjs @@ -5,8 +5,8 @@ import { callToString } from "../../common/utils.mjs"; export default { key: "twilio-make-phone-call", name: "Make a Phone Call", - description: "Make a phone call, passing text that Twilio will speak to the recipient of the call. [See the docs](https://www.twilio.com/docs/voice/api/call-resource#create-a-call-resource) for more information", - version: "0.1.3", + description: "Make a phone call passing text, a URL, or an application that Twilio will use to handle the call. [See the documentation](https://www.twilio.com/docs/voice/api/call-resource#create-a-call-resource)", + version: "0.1.5", type: "action", props: { twilio, @@ -22,11 +22,94 @@ export default { "to", ], }, + callType: { + type: "string", + label: "Call Type", + description: "Whether to use `text`, a `URL`, or an `application` to handle the call", + options: [ + { + label: "Enter text for Twilio to speak when the user picks up the phone", + value: "text", + }, + { + label: "Enter a URL that returns the TwiML instructions for the call", + value: "url", + }, + { + label: "Enter the SID of an Application resource that will handle the call", + value: "application", + }, + ], + reloadProps: true, + }, text: { label: "Text", type: "string", description: "The text you'd like Twilio to speak to the user when they pick up the phone.", + hidden: true, }, + url: { + type: "string", + label: "URL", + description: "The absolute URL that returns the TwiML instructions for the call", + hidden: true, + }, + applicationSid: { + propDefinition: [ + twilio, + "applicationSid", + ], + hidden: true, + }, + timeout: { + type: "integer", + label: "Timeout", + description: "The integer number of seconds that we should allow the phone to ring before assuming there is no answer. The default is `60` seconds and the maximum is `600` seconds.", + optional: true, + }, + record: { + type: "boolean", + label: "Record", + description: "Whether to record the call", + optional: true, + reloadProps: true, + }, + }, + async additionalProps(existingProps) { + const props = {}; + existingProps.text.hidden = !(this.callType === "text"); + existingProps.url.hidden = !(this.callType === "url"); + existingProps.applicationSid.hidden = !(this.callType === "application"); + if (this.record) { + props.trim = { + type: "string", + label: "Trim", + description: "Whether to trim any leading and trailing silence from the recording", + options: [ + "trim-silence", + "do-not-trim", + ], + optional: true, + }; + props.recordingTrack = { + type: "string", + label: "Recording Track", + description: "The audio track to record for the call. Default is `both`.", + options: [ + "inbound", + "outbound", + "both", + ], + optional: true, + }; + props.recordingCallbackUrl = { + type: "string", + label: "Recording Callback URL", + description: "The URL that we call when the recording is available to be accessed", + optional: true, + }; + } + return props; }, async run({ $ }) { // Parse the given number into its E.164 equivalent @@ -47,7 +130,14 @@ export default { const data = { to: toParsed.phoneNumber, from: fromParsed.phoneNumber, - twiml: `${this.text}`, + twiml: this.text && `${this.text}`, + url: this.url, + applicationSid: this.applicationSid, + timeout: this.timeout, + record: this.record, + trim: this.trim, + recordingTrack: this.recordingTrack, + recordingStatusCallback: this.recordingCallbackUrl, }; const resp = await this.twilio.getClient().calls.create(data); diff --git a/components/twilio/actions/phone-number-lookup/phone-number-lookup.mjs b/components/twilio/actions/phone-number-lookup/phone-number-lookup.mjs new file mode 100644 index 0000000000000..6867ea35e5b7b --- /dev/null +++ b/components/twilio/actions/phone-number-lookup/phone-number-lookup.mjs @@ -0,0 +1,41 @@ +import app from "../../twilio.app.mjs"; + +export default { + key: "twilio-phone-number-lookup", + name: "Phone Number Lookup", + description: "Lookup information about a phone number. [See the documentation](https://www.twilio.com/docs/lookup/v2-api/line-type-intelligence) for more information", + type: "action", + version: "0.0.2", + props: { + app, + sid: { + type: "string", + label: "Phone Number", + description: "The phone number to lookup", + }, + }, + methods: { + lineTypeLookup(sid) { + const client = this.app.getClient(); + return client.lookups.v2.phoneNumbers(sid).fetch({ + fields: "line_type_intelligence", + }); + }, + }, + async run({ $ }) { + const { + lineTypeLookup, + sid, + } = this; + + const response = await lineTypeLookup(sid); + + if (response.validationErrors?.length) { + $.export("$summary", `Failed to fetch phone number lookup: \`${JSON.stringify(response.validationErrors, null, 2)}\`.`); + } else { + $.export("$summary", `Successfully fetched phone number lookup of \`${sid}\``); + } + + return response; + }, +}; diff --git a/components/twilio/actions/send-message/send-message.mjs b/components/twilio/actions/send-message/send-message.mjs new file mode 100644 index 0000000000000..57c495861d802 --- /dev/null +++ b/components/twilio/actions/send-message/send-message.mjs @@ -0,0 +1,64 @@ +import { phone } from "phone"; +import twilio from "../../twilio.app.mjs"; +import { messageToString } from "../../common/utils.mjs"; + +export default { + key: "twilio-send-message", + name: "Send Message", + description: "Send an SMS text with optional media files. [See the documentation](https://www.twilio.com/docs/sms/api/message-resource#create-a-message-resource)", + type: "action", + version: "0.0.2", + props: { + twilio, + from: { + propDefinition: [ + twilio, + "from", + ], + }, + to: { + propDefinition: [ + twilio, + "to", + ], + }, + body: { + propDefinition: [ + twilio, + "body", + ], + }, + mediaUrl: { + propDefinition: [ + twilio, + "mediaUrl", + ], + }, + }, + async run({ $ }) { + // Parse the given number into its E.164 equivalent + // The E.164 phone number will be included in the first element + // of the array, but the array will be empty if parsing fails. + // See https://www.npmjs.com/package/phone + const toParsed = phone(this.to); + if (!toParsed || !toParsed.phoneNumber) { + throw new Error(`Phone number ${this.to} could not be parsed as a valid number.`); + } + + const fromParsed = phone(this.from); + if (!fromParsed || !fromParsed.phoneNumber) { + throw new Error(`Phone number ${this.from} could not be parsed as a valid number.`); + } + + const data = { + to: toParsed.phoneNumber, + from: fromParsed.phoneNumber, + body: this.body, + mediaUrl: this.mediaUrl, + }; + + const resp = await this.twilio.getClient().messages.create(data); + $.export("$summary", `Successfully sent a new message, "${messageToString(resp)}"`); + return resp; + }, +}; diff --git a/components/twilio/actions/send-mms/send-mms.mjs b/components/twilio/actions/send-mms/send-mms.mjs deleted file mode 100644 index 73eec1d3199ef..0000000000000 --- a/components/twilio/actions/send-mms/send-mms.mjs +++ /dev/null @@ -1,64 +0,0 @@ -import { phone } from "phone"; -import twilio from "../../twilio.app.mjs"; -import { messageToString } from "../../common/utils.mjs"; - -export default { - key: "twilio-send-mms", - name: "Send MMS", - description: "Send an SMS with text and media files. [See the docs](https://www.twilio.com/docs/sms/api/message-resource#create-a-message-resource) for more information", - type: "action", - version: "0.1.3", - props: { - twilio, - from: { - propDefinition: [ - twilio, - "from", - ], - }, - to: { - propDefinition: [ - twilio, - "to", - ], - }, - body: { - propDefinition: [ - twilio, - "body", - ], - }, - mediaUrl: { - propDefinition: [ - twilio, - "mediaUrl", - ], - }, - }, - async run({ $ }) { - // Parse the given number into its E.164 equivalent - // The E.164 phone number will be included in the first element - // of the array, but the array will be empty if parsing fails. - // See https://www.npmjs.com/package/phone - const toParsed = phone(this.to); - if (!toParsed || !toParsed.phoneNumber) { - throw new Error(`Phone number ${this.to} could not be parsed as a valid number.`); - } - - const fromParsed = phone(this.from); - if (!fromParsed || !fromParsed.phoneNumber) { - throw new Error(`Phone number ${this.from} could not be parsed as a valid number.`); - } - - const data = { - to: toParsed.phoneNumber, - from: fromParsed.phoneNumber, - body: this.body, - mediaUrl: this.mediaUrl, - }; - - const resp = await this.twilio.getClient().messages.create(data); - $.export("$summary", `Successfully sent a new MMS, "${messageToString(resp)}"`); - return resp; - }, -}; diff --git a/components/twilio/actions/send-sms-verification/send-sms-verification.mjs b/components/twilio/actions/send-sms-verification/send-sms-verification.mjs index bbb5805e22624..465c23a9af42c 100644 --- a/components/twilio/actions/send-sms-verification/send-sms-verification.mjs +++ b/components/twilio/actions/send-sms-verification/send-sms-verification.mjs @@ -3,9 +3,9 @@ import app from "../../twilio.app.mjs"; export default { key: "twilio-send-sms-verification", name: "Send SMS Verification", - description: "Send an SMS verification to a phone number. [See the documentation](https://www.twilio.com/docs/verify/api) for more information", + description: "Send an SMS verification to a phone number. [See the documentation](https://www.twilio.com/docs/verify/api)", type: "action", - version: "0.0.1", + version: "0.0.3", props: { app, serviceSid: { diff --git a/components/twilio/actions/send-sms/send-sms.mjs b/components/twilio/actions/send-sms/send-sms.mjs deleted file mode 100644 index 64c27f640c9d5..0000000000000 --- a/components/twilio/actions/send-sms/send-sms.mjs +++ /dev/null @@ -1,57 +0,0 @@ -import { phone } from "phone"; -import twilio from "../../twilio.app.mjs"; -import { messageToString } from "../../common/utils.mjs"; - -export default { - key: "twilio-send-sms", - name: "Send SMS", - description: "Send a simple text-only SMS. [See the docs](https://www.twilio.com/docs/sms/api/message-resource#create-a-message-resource) for more information", - type: "action", - version: "0.1.3", - props: { - twilio, - from: { - propDefinition: [ - twilio, - "from", - ], - }, - to: { - propDefinition: [ - twilio, - "to", - ], - }, - body: { - propDefinition: [ - twilio, - "body", - ], - }, - }, - async run({ $ }) { - // Parse the given number into its E.164 equivalent - // The E.164 phone number will be included in the first element - // of the array, but the array will be empty if parsing fails. - // See https://www.npmjs.com/package/phone - const toParsed = phone(this.to); - if (!toParsed || !toParsed.phoneNumber) { - throw new Error(`Phone number ${this.to} could not be parsed as a valid number.`); - } - - const fromParsed = phone(this.from); - if (!fromParsed || !fromParsed.phoneNumber) { - throw new Error(`Phone number ${this.from} could not be parsed as a valid number.`); - } - - const data = { - to: toParsed.phoneNumber, - from: fromParsed.phoneNumber, - body: this.body, - }; - - const resp = await this.twilio.getClient().messages.create(data); - $.export("$summary", `Successfully sent a new SMS, "${messageToString(resp)}"`); - return resp; - }, -}; diff --git a/components/twilio/common/utils.mjs b/components/twilio/common/utils.mjs index 17fdf5ddda4f6..5985df4e5371c 100644 --- a/components/twilio/common/utils.mjs +++ b/components/twilio/common/utils.mjs @@ -136,6 +136,7 @@ function recordingToString(recording) { } export { + formatTimeElapsed, timeBetween, omitEmptyStringValues, callToString, diff --git a/components/twilio/package.json b/components/twilio/package.json index 4bbc4922016d6..5ee746f484f20 100644 --- a/components/twilio/package.json +++ b/components/twilio/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/twilio", - "version": "0.3.11", + "version": "0.6.0", "description": "Pipedream Twilio Components", "main": "twilio.app.mjs", "keywords": [ @@ -10,8 +10,10 @@ "homepage": "https://pipedream.com/apps/twilio", "author": "Pipedream (https://pipedream.com/)", "dependencies": { - "@pipedream/platform": "^1.2.0", - "phone": "^3.1.29", + "@pipedream/platform": "^3.0.0", + "got": "^13.0.0", + "phone": "^3.1.49", + "stream": "^0.0.3", "twilio": "^3.54.2" }, "gitHead": "e12480b94cc03bed4808ebc6b13e7fdb3a1ba535", diff --git a/components/twilio/sources/common-polling.mjs b/components/twilio/sources/common-polling.mjs deleted file mode 100644 index 40280129f5c13..0000000000000 --- a/components/twilio/sources/common-polling.mjs +++ /dev/null @@ -1,45 +0,0 @@ -import twilio from "../twilio.app.mjs"; -import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; - -export default { - props: { - twilio, - db: "$.service.db", - timer: { - label: "Polling schedule", - description: "Pipedream polls Twilio for events on this schedule.", - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - }, - methods: { - _getCreatedAfter() { - return this.db.get("createdAfter"); - }, - _setCreatedAfter(createdAfter) { - this.db.set("createdAfter", createdAfter); - }, - emitEvent(result) { - const meta = this.generateMeta(result); - this.$emit(result, meta); - }, - }, - async run() { - let dateCreatedAfter = this._getCreatedAfter(); - const params = { - dateCreatedAfter, - }; - const results = await this.listResults(params); - for (const result of results) { - this.emitEvent(result); - if ( - !dateCreatedAfter || - Date.parse(result.dateCreated) > Date.parse(dateCreatedAfter) - ) - dateCreatedAfter = result.dateCreated; - } - this._setCreatedAfter(dateCreatedAfter); - }, -}; diff --git a/components/twilio/sources/common-webhook.mjs b/components/twilio/sources/common-webhook.mjs deleted file mode 100644 index ac489f0c33bb9..0000000000000 --- a/components/twilio/sources/common-webhook.mjs +++ /dev/null @@ -1,106 +0,0 @@ -import twilio from "../twilio.app.mjs"; - -export default { - props: { - twilio, - incomingPhoneNumber: { - propDefinition: [ - twilio, - "incomingPhoneNumber", - ], - }, - authToken: { - propDefinition: [ - twilio, - "authToken", - ], - }, - http: { - label: "HTTP Responder", - description: "Exposes a `respond()` method that lets the source issue HTTP responses", - type: "$.interface.http", - customResponse: true, - }, - }, - hooks: { - async activate() { - const createWebhookResp = await this.twilio.setWebhookURL({ - serviceType: this.getServiceType(), - phoneNumberSid: this.incomingPhoneNumber, - url: this.http.endpoint, - }); - console.log(createWebhookResp); - }, - async deactivate() { - // remove the webhook URL if url prop is not set - const deleteWebhookResp = await this.twilio.setWebhookURL({ - serviceType: this.getServiceType(), - phoneNumberSid: this.incomingPhoneNumber, - url: "", - }); - console.log(deleteWebhookResp); - }, - }, - methods: { - getServiceType() { - throw new Error("getServiceType() is not implemented!"); - }, - getResponseBody() { - return null; - }, - isRelevant() { - return true; - }, - emitEvent(body, headers) { - const meta = this.generateMeta(body, headers); - this.$emit(body, meta); - }, - }, - async run(event) { - let { - body, - headers, - } = event; - - const responseBody = this.getResponseBody(); - if (responseBody) { - this.http.respond({ - status: 200, - headers: { - "Content-Type": "text/xml", - }, - body: responseBody, - }); - } - - if (typeof body !== "object") { - body = Object.fromEntries(new URLSearchParams(body)); - } - - if (!this.isRelevant(body)) { - console.log("Event not relevant. Skipping..."); - return; - } - - const signature = headers["x-twilio-signature"]; - if (!signature) { - console.log("No x-twilio-signature header in request. Exiting."); - return; - } - - // The url must match the incoming URL exactly, which contains a `/` at the end - const isRequestValid = this.twilio.validateRequest({ - signature, - url: `${this.http.endpoint}/`, - params: body, - authToken: this.authToken, - }); - - if (!isRequestValid) { - console.log("Event could not be validated. Skipping..."); - return; - } - - this.emitEvent(body, headers); - }, -}; diff --git a/components/twilio/sources/common/common-polling.mjs b/components/twilio/sources/common/common-polling.mjs new file mode 100644 index 0000000000000..62574a603d290 --- /dev/null +++ b/components/twilio/sources/common/common-polling.mjs @@ -0,0 +1,50 @@ +import twilio from "../../twilio.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + twilio, + db: "$.service.db", + timer: { + label: "Polling schedule", + description: "Pipedream polls Twilio for events on this schedule.", + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getCreatedAfter() { + return this.db.get("createdAfter"); + }, + _setCreatedAfter(createdAfter) { + this.db.set("createdAfter", createdAfter); + }, + emitEvent(result) { + const meta = this.generateMeta(result); + delete result._version; + this.$emit(result, meta); + }, + }, + async run() { + const dateCreatedAfter = this._getCreatedAfter(); + let newDateCreatedAfter = dateCreatedAfter; + + const results = await this.listResults({ + dateCreatedAfter, + }); + + for (const result of results) { + const dateCreated = result.dateCreated; + const ts = Date.parse(dateCreated); + if ( !dateCreatedAfter || ts > Date.parse(dateCreatedAfter) ) { + this.emitEvent(result); + if (!dateCreatedAfter || ts > Date.parse(newDateCreatedAfter)) { + newDateCreatedAfter = dateCreated; + } + } + } + this._setCreatedAfter(newDateCreatedAfter); + }, +}; diff --git a/components/twilio/sources/common/common-webhook.mjs b/components/twilio/sources/common/common-webhook.mjs new file mode 100644 index 0000000000000..e2123cf9ddef2 --- /dev/null +++ b/components/twilio/sources/common/common-webhook.mjs @@ -0,0 +1,99 @@ +import twilio from "../../twilio.app.mjs"; + +export default { + props: { + twilio, + incomingPhoneNumber: { + propDefinition: [ + twilio, + "incomingPhoneNumber", + ], + }, + http: { + label: "HTTP Responder", + description: "Exposes a `respond()` method that lets the source issue HTTP responses", + type: "$.interface.http", + customResponse: true, + }, + }, + hooks: { + async activate() { + const createWebhookResp = await this.twilio.setWebhookURL({ + serviceType: this.getServiceType(), + phoneNumberSid: this.incomingPhoneNumber, + url: this.http.endpoint, + }); + console.log(createWebhookResp); + }, + async deactivate() { + // remove the webhook URL if url prop is not set + const deleteWebhookResp = await this.twilio.setWebhookURL({ + serviceType: this.getServiceType(), + phoneNumberSid: this.incomingPhoneNumber, + url: "", + }); + console.log(deleteWebhookResp); + }, + }, + methods: { + getServiceType() { + throw new Error("getServiceType() is not implemented!"); + }, + getResponseBody() { + return null; + }, + isRelevant() { + return true; + }, + emitEvent(body, headers) { + const meta = this.generateMeta(body, headers); + this.$emit(body, meta); + }, + }, + async run(event) { + let { + body, + headers, + } = event; + + const responseBody = this.getResponseBody(); + if (responseBody) { + this.http.respond({ + status: 200, + headers: { + "Content-Type": "text/xml", + }, + body: responseBody, + }); + } + + if (typeof body !== "object") { + body = Object.fromEntries(new URLSearchParams(body)); + } + + if (!this.isRelevant(body)) { + console.log("Event not relevant. Skipping..."); + return; + } + + const signature = headers["x-twilio-signature"]; + if (!signature) { + console.log("No x-twilio-signature header in request. Exiting."); + return; + } + + // The url must match the incoming URL exactly, which contains a `/` at the end + const isRequestValid = this.twilio.validateRequest({ + signature, + url: `${this.http.endpoint}/`, + params: body, + }); + + if (!isRequestValid) { + console.log("Event could not be validated. Skipping..."); + return; + } + + this.emitEvent(body, headers); + }, +}; diff --git a/components/twilio/sources/new-call/new-call.mjs b/components/twilio/sources/new-call/new-call.mjs index dc9255fc7c556..2036834890d80 100644 --- a/components/twilio/sources/new-call/new-call.mjs +++ b/components/twilio/sources/new-call/new-call.mjs @@ -1,4 +1,4 @@ -import common from "../common-webhook.mjs"; +import common from "../common/common-webhook.mjs"; import constants from "../../common/constants.mjs"; export default { @@ -6,7 +6,7 @@ export default { key: "twilio-new-call", name: "New Call (Instant)", description: "Emit new event each time a call to the phone number is completed. Configures a webhook in Twilio, tied to a phone number.", - version: "0.1.3", + version: "0.1.5", type: "source", dedupe: "unique", methods: { diff --git a/components/twilio/sources/new-incoming-sms/new-incoming-sms.mjs b/components/twilio/sources/new-incoming-sms/new-incoming-sms.mjs index e593f6a401e6c..8385831a82aae 100644 --- a/components/twilio/sources/new-incoming-sms/new-incoming-sms.mjs +++ b/components/twilio/sources/new-incoming-sms/new-incoming-sms.mjs @@ -1,5 +1,5 @@ import twilio from "twilio"; -import common from "../common-webhook.mjs"; +import common from "../common/common-webhook.mjs"; import constants from "../../common/constants.mjs"; const MessagingResponse = twilio.twiml.MessagingResponse; @@ -9,7 +9,7 @@ export default { key: "twilio-new-incoming-sms", name: "New Incoming SMS (Instant)", description: "Emit new event every time an SMS is sent to the phone number set. Configures a webhook in Twilio, tied to an incoming phone number.", - version: "0.1.3", + version: "0.1.5", type: "source", dedupe: "unique", props: { diff --git a/components/twilio/sources/new-phone-number/new-phone-number.mjs b/components/twilio/sources/new-phone-number/new-phone-number.mjs index f80d80306607e..84354bf6a8406 100644 --- a/components/twilio/sources/new-phone-number/new-phone-number.mjs +++ b/components/twilio/sources/new-phone-number/new-phone-number.mjs @@ -1,11 +1,11 @@ -import common from "../common-polling.mjs"; +import common from "../common/common-polling.mjs"; export default { ...common, key: "twilio-new-phone-number", name: "New Phone Number", description: "Emit new event when you add a new phone number to your account", - version: "0.1.4", + version: "0.1.6", type: "source", dedupe: "unique", methods: { diff --git a/components/twilio/sources/new-recording/new-recording.mjs b/components/twilio/sources/new-recording/new-recording.mjs index afeb7f46dc59f..32d00fae95610 100644 --- a/components/twilio/sources/new-recording/new-recording.mjs +++ b/components/twilio/sources/new-recording/new-recording.mjs @@ -1,11 +1,11 @@ -import common from "../common-polling.mjs"; +import common from "../common/common-polling.mjs"; export default { ...common, key: "twilio-new-recording", name: "New Recording", description: "Emit new event when a new call recording is created", - version: "0.1.4", + version: "0.1.6", type: "source", dedupe: "unique", methods: { diff --git a/components/twilio/sources/new-transcript-created/new-transcript-created.mjs b/components/twilio/sources/new-transcript-created/new-transcript-created.mjs new file mode 100644 index 0000000000000..66a7882d67c79 --- /dev/null +++ b/components/twilio/sources/new-transcript-created/new-transcript-created.mjs @@ -0,0 +1,52 @@ +import common from "../common/common-polling.mjs"; + +export default { + ...common, + key: "twilio-new-transcript-created", + name: "New Transcript Created", + description: "Emit new event when a new call transcript is created", + version: "0.0.2", + type: "source", + dedupe: "unique", + props: { + ...common.props, + includeTranscriptText: { + propDefinition: [ + common.props.twilio, + "includeTranscriptText", + ], + }, + }, + methods: { + ...common.methods, + async listResults(...args) { + const results = await this.twilio.listTranscripts(...args); + if (!this.includeTranscriptText) { + return results; + } + const transcripts = []; + for (const result of results) { + const { + sentences, transcript, + } = await this.twilio.getSentences(result.sid); + transcripts.push({ + ...result, + sentences, + transcript, + }); + } + return transcripts; + }, + generateMeta(transcript) { + const { + sid: id, + dateCreated, + } = transcript; + return { + id, + summary: `New transcript ${id}`, + ts: Date.parse(dateCreated), + }; + }, + }, +}; diff --git a/components/twilio/sources/new-transcription/new-transcription.mjs b/components/twilio/sources/new-transcription/new-transcription.mjs deleted file mode 100644 index 31e949f8b3ef8..0000000000000 --- a/components/twilio/sources/new-transcription/new-transcription.mjs +++ /dev/null @@ -1,28 +0,0 @@ -import common from "../common-polling.mjs"; - -export default { - ...common, - key: "twilio-new-transcription", - name: "New Transcription", - description: "Emit new event when a new call transcription is created", - version: "0.1.4", - type: "source", - dedupe: "unique", - methods: { - ...common.methods, - async listResults(...args) { - return await this.twilio.listTranscriptions(...args); - }, - generateMeta(transcription) { - const { - sid: id, - dateCreated, - } = transcription; - return { - id, - summary: `New transcription ${id}`, - ts: Date.parse(dateCreated), - }; - }, - }, -}; diff --git a/components/twilio/twilio.app.mjs b/components/twilio/twilio.app.mjs index 09bb77c5ff35c..65c95a08bce4e 100644 --- a/components/twilio/twilio.app.mjs +++ b/components/twilio/twilio.app.mjs @@ -1,5 +1,6 @@ import twilio from "twilio"; import { + formatTimeElapsed, callToString, messageToString, recordingToString, @@ -10,13 +11,6 @@ export default { type: "app", app: "twilio", propDefinitions: { - authToken: { - type: "string", - secret: true, - label: "Twilio Auth Token", - description: - "Your Twilio auth token, found [in your Twilio console](https://www.twilio.com/console). Required for validating Twilio events.", - }, body: { type: "string", label: "Message Body", @@ -72,7 +66,7 @@ export default { limit: { type: "integer", label: "Limit", - description: "The maximum number of results to be worked with during one execution cycle.", + description: "The maximum number of results to retrieve", optional: true, default: 50, }, @@ -166,10 +160,44 @@ export default { })); }, }, + includeTranscriptText: { + type: "boolean", + label: "Include Transcript Text?", + description: "Set to `true` to include the transcript sentences in the response", + optional: true, + }, + applicationSid: { + type: "string", + label: "Application SID", + description: "The SID of the Application resource", + async options() { + const applications = await this.listApplications(); + return applications?.map((application) => ({ + label: application.friendly_name, + value: application.sid, + })) || []; + }, + }, + transcriptSids: { + type: "string[]", + label: "Transcript SIDs", + description: "The unique SID identifiers of the Transcripts to retrieve", + async options({ page }) { + const transcripts = await this.listTranscripts({ + page, + }); + return transcripts?.map(({ + sid: value, dateCreated, duration, + }) => ({ + value, + label: `${dateCreated} for ${formatTimeElapsed(duration)}`, + })) || []; + }, + }, }, methods: { validateRequest({ - signature, url, params, authToken = this.$auth.authToken, + signature, url, params, authToken = this.$auth.AuthToken, } = {}) { // See https://www.twilio.com/docs/usage/webhooks/webhooks-security return twilio.validateRequest( @@ -240,6 +268,36 @@ export default { const client = this.getClient(); return client.transcriptions.list(params); }, + listTranscripts(params) { + const client = this.getClient(); + return client.intelligence.v2.transcripts.list(params); + }, + listTranscriptSentences(transcriptId, params = {}) { + const client = this.getClient(); + return client.intelligence.v2.transcripts(transcriptId).sentences.list(params); + }, + getTranscript(transcriptId) { + const client = this.getClient(); + return client.intelligence.v2.transcripts(transcriptId).fetch(); + }, + async getSentences(transcriptId) { + const sentences = await this.listTranscriptSentences(transcriptId, { + limit: 1000, + }); + const transcriptParts = sentences.map((sentence) => `Speaker ${sentence.mediaChannel} (Sentence ${sentence.sentenceIndex}):\n${sentence.transcript}\nConfidence: ${sentence.confidence}\n`); + return { + sentences, + transcript: transcriptParts.join("\n"), + }; + }, + listApplications(params) { + const client = this.getClient(); + return client.applications.list(params); + }, + createVerificationService(params) { + const client = this.getClient(); + return client.verify.v2.services.create(params); + }, /** * Returns a list of messages associated with your account. When getting the * list of all messages, results will be sorted on the DateSent field with @@ -357,7 +415,6 @@ export default { const client = this.getClient(); return client.recordings(sid).transcriptions.list(params); }, - /** * Send a verification code via sms * @@ -372,7 +429,6 @@ export default { channel: "sms", }); }, - /** * List all services * @@ -382,7 +438,6 @@ export default { const client = this.getClient(); return client.verify.v2.services.list(); }, - /** * Check whether the verification token is valid * diff --git a/components/twin/actions/browse/browse.mjs b/components/twin/actions/browse/browse.mjs new file mode 100644 index 0000000000000..9f3aab2ad61c3 --- /dev/null +++ b/components/twin/actions/browse/browse.mjs @@ -0,0 +1,70 @@ +import twin from "../../twin.app.mjs"; + +export default { + key: "twin-browse", + name: "Browse", + description: "Browse the internet with an AI web navigation agent that can find information for you. [See the documentation](https://docs.twin.so/api-reference/endpoint/browse)", + version: "0.0.1", + type: "action", + props: { + twin, + startUrl: { + type: "string", + label: "Start URL", + description: "The URL where the browsing task should begin", + }, + goal: { + type: "string", + label: "Goal", + description: "The goal or objective of the browsing task. Example: \"Find the latest price of AAPL stock.\"", + }, + outputType: { + type: "string", + label: "Output Type", + description: "The type of output expected from the task", + options: [ + "string", + "url", + "list[url]", + ], + default: "string", + optional: true, + }, + callbackWithRerun: { + type: "boolean", + label: "Callback With Rerun", + description: "Use the `$.flow.rerun` Node.js helper to rerun the step when the search is completed. This will increase execution time and credit usage as a result. [See the documentation](https://pipedream.com/docs/code/nodejs/rerun/#flow-rerun)", + optional: true, + }, + }, + async run({ $ }) { + let response, completionCallbackUrl; + const { run } = $.context; + if (run.runs === 1) { + if (this.callbackWithRerun) { + ({ resume_url: completionCallbackUrl } = $.flow.rerun(600000, null, 1)); + } + response = await this.twin.browse({ + $, + data: { + startUrl: this.startUrl, + goal: this.goal, + outputType: this.outputType, + completionCallbackUrl, + }, + }); + } + + if (run.callback_request) { + const { task_id: taskId } = run.callback_request.body; + response = await this.twin.getTask({ + taskId, + }); + } + + if (response.status === "COMPLETED") { + $.export("$summary", "Successfully completed browsing"); + } + return response; + }, +}; diff --git a/components/twin/actions/get-task-details/get-task-details.mjs b/components/twin/actions/get-task-details/get-task-details.mjs new file mode 100644 index 0000000000000..4d6ca1db3504c --- /dev/null +++ b/components/twin/actions/get-task-details/get-task-details.mjs @@ -0,0 +1,26 @@ +import twin from "../../twin.app.mjs"; + +export default { + key: "twin-get-task-details", + name: "Get Task Details", + description: "Retrieve details of a specific task. [See the documentation](https://docs.twin.so/api-reference/endpoint/get-task)", + version: "0.0.1", + type: "action", + props: { + twin, + taskId: { + propDefinition: [ + twin, + "taskId", + ], + }, + }, + async run({ $ }) { + const response = await this.twin.getTask({ + $, + taskId: this.taskId, + }); + $.export("$summary", `Successfully retrieved details for task with ID: ${this.taskId}`); + return response; + }, +}; diff --git a/components/twin/package.json b/components/twin/package.json new file mode 100644 index 0000000000000..8529cbe13f51e --- /dev/null +++ b/components/twin/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/twin", + "version": "0.1.0", + "description": "Pipedream Twin Components", + "main": "twin.app.mjs", + "keywords": [ + "pipedream", + "twin" + ], + "homepage": "https://pipedream.com/apps/twin", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/twin/twin.app.mjs b/components/twin/twin.app.mjs new file mode 100644 index 0000000000000..2b5ee9af7b0a6 --- /dev/null +++ b/components/twin/twin.app.mjs @@ -0,0 +1,64 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "twin", + propDefinitions: { + taskId: { + type: "string", + label: "Task ID", + description: "Identifier of a task", + async options() { + const tasks = await this.listTasks(); + return tasks?.map(({ + id: value, goal, + }) => ({ + value, + label: `${goal.slice(0, 50)}...`, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.twin.so"; + }, + _headers() { + return { + "x-api-key": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, + path, + ...args + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._headers(), + ...args, + }); + }, + browse(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/browse", + ...args, + }); + }, + getTask({ + taskId, ...args + }) { + return this._makeRequest({ + path: `/task/${taskId}`, + ...args, + }); + }, + listTasks(args = {}) { + return this._makeRequest({ + path: "/tasks", + ...args, + }); + }, + }, +}; diff --git a/components/twist/README.md b/components/twist/README.md index 5ed18af921a9b..4a6b50701cdc8 100644 --- a/components/twist/README.md +++ b/components/twist/README.md @@ -1,22 +1,11 @@ # Overview -With the Twist API, you can easily create an unlimited range of applications, -tools and integrations that work with the Twist platform. Whether you're -looking to build a bot, a custom integration, or an app to visualize data, the -Twist API offers the flexibility you need to turn your ideas into reality. +Twist is a communication app designed for team collaboration, with a focus on organized, threaded conversations. Using the Twist API on Pipedream, you can automate workflows to enhance productivity, streamline communication, and integrate with other tools. From automating notifications to syncing tasks across platforms, the API unlocks valuable use cases for teams needing to stay in sync without the noise of email or less structured chat apps. -The following is a list of examples of what you can build with the Twist API: +# Example Use Cases -- Custom bots that can automate simple tasks and interact with users in real - time, providing valuable feedback and information -- Integrations that allow you to easily sync data from external sources -- An app to visualize data from the Twist system in a variety of ways -- Custom notifications that alert users when specific events occur -- Automated workflows to streamline processes and enhance efficiency -- Reports to keep track of key performance metrics -- Custom dashboards to easily monitor the health of your organization -- A tool to generate customer insights from Twist conversations -- A tool to quickly search for specific messages and conversations across - multiple channels -- Connectors to allow users to easily post messages from 3rd party applications -- And much more! +- **Automated Project Updates:** Create a workflow that triggers whenever a new task is added to a project management tool like Asana or Trello. The update is then formatted and posted to a specific Twist channel to keep the team informed without leaving their communication hub. + +- **Support Ticket Notification:** Build an automation that listens for new support tickets from a platform like Zendesk. When a ticket is received, it triggers a Pipedream workflow that posts a summary and link to the ticket in a designated Twist channel, ensuring that the support team can react quickly. + +- **Daily Digests from Multiple Tools:** Set up a daily scheduled workflow that aggregates updates from various tools such as GitHub for commits, JIRA for issue updates, and Google Calendar for meeting schedules. The workflow compiles these into a single digest and posts it to Twist, giving the team a one-glance summary of the day’s agenda and activity. diff --git a/components/twitch/README.md b/components/twitch/README.md index 3a6ce8cb52b4f..3db5f88b1f238 100644 --- a/components/twitch/README.md +++ b/components/twitch/README.md @@ -1,9 +1,11 @@ # Overview -With the Twitch API, you can develop apps that: +The Twitch API unlocks a world of possibilities for engaging with live streaming communities and understanding audience behaviors. With Pipedream, you can harness this API to automate many aspects of Twitch interaction and analysis. From tracking stream stats to automating chat messages, the Twitch API lets you create workflows that interact with Twitch's vast live streaming ecosystem. Pipedream's serverless platform streamlines these tasks, making it simple to connect Twitch with other services for enhanced functionalities. -- Display a list of top Twitch channels -- Allow users to search for specific Twitch channels -- Show information about a specific Twitch channel -- Allow users to follow or unfollow a Twitch channel -- Notify users when their favorite Twitch channels go live +# Example Use Cases + +- **Automated Stream Alerts to Discord**: Automatically post custom messages to a Discord channel whenever you or your favorite streamer goes live on Twitch. This keeps your community informed in real time and drives traffic to your Twitch channel. + +- **Stream Performance Analytics**: After each streaming session, aggregate statistics such as view counts, new followers, and chat activity. Send this data to Google Sheets or BigQuery for further analysis, helping you to optimize future streams based on audience engagement insights. + +- **Automated Giveaways and Polls**: During a live stream, monitor chat for certain keywords to trigger events like raffles or polls. Use Pipedream to connect Twitch chat with a randomizer service to pick winners, or tally votes, and announce results in real-time. diff --git a/components/twitch/package-lock.json b/components/twitch/package-lock.json index d1652d16c0843..929f793ae5142 100644 --- a/components/twitch/package-lock.json +++ b/components/twitch/package-lock.json @@ -78,9 +78,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -295,9 +295,9 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" }, "form-data": { "version": "4.0.0", diff --git a/components/twitch_developer_app/README.md b/components/twitch_developer_app/README.md index 83e2244be7271..3e4e687c533ec 100644 --- a/components/twitch_developer_app/README.md +++ b/components/twitch_developer_app/README.md @@ -1,16 +1,11 @@ # Overview -With the Twitch Developer App API, you can create a variety of apps and -integrations to help make your broadcasting experience better. The API opens up -new possibilities in chatbot integrations, streaming services, and other app -development. Below are just a few examples of what you can do with the API: +The Twitch Developer App API lets you tap into the rich features of Twitch, enabling integration with the streaming platform's data. Using Pipedream, you can automate interactions with Twitch, such as managing streams, users, games, and more. With the ability to react to events in real-time, you can create automated workflows that enhance the streaming experience for broadcasters and viewers alike, such as notifying followers when a stream goes live, tracking metrics, or even moderating chat. -- Create chatbots that can help keep your chat conversations running smoothly -- Integrate Twitch with other third-party services for more functionality -- Create alerts for streamers and viewers when activities occur -- Create streaming services with more features than are available through the - Twitch app -- Develop apps that make it easier to manage stream broadcasts -- Show streamer viewer stats in real time -- Automate tasks related to stream broadcasts -- Tailor streaming experiences on any device +# Example Use Cases + +- **Stream Alerts to Discord**: Trigger a Pipedream workflow when a Twitch channel goes live. Use the Discord app to send a custom notification to a designated Discord server, alerting followers instantly that their favorite streamer is broadcasting. + +- **Automated Stream Analysis**: After a stream ends, kick off a workflow that aggregates streaming stats like viewership peaks, average watch time, and chat engagement. Connect to Google Sheets to log this data for performance tracking and long-term analysis. + +- **Dynamic Chat Moderation**: Monitor chat messages on Twitch in real-time. Use sentiment analysis from an app like Google Cloud Natural Language API to assess chat tone. Automatically timeout or ban users who violate chat policies by sending commands back to Twitch using the API. diff --git a/components/twitter/README.md b/components/twitter/README.md index 7d3bada28d1f9..b150eb675d55c 100644 --- a/components/twitter/README.md +++ b/components/twitter/README.md @@ -1,3 +1,8 @@ +# Overview + +The Twitter API on Pipedream enables you to automate interactions with Twitter, from posting tweets to analyzing social media trends. Pipedream's serverless platform provides the tools to create workflows that trigger on specific Twitter activities, process data, and connect with countless other apps for extensive automation scenarios. With Pipedream's integration, you can listen for events such as new tweets, mentions, or followers, and execute actions like tweeting, retweeting, or even leveraging sentiment analysis to gauge public perception. + + # Getting Started In order to connect your Twitter account to Pipedream, you'll need to create a developer project and app. 1. First, visit Twitter's [developer dashboard](https://developer.twitter.com/en/portal/dashboard) and sign in @@ -19,6 +24,14 @@ In order to connect your Twitter account to Pipedream, you'll need to create a d ![Consumer API key](https://res.cloudinary.com/dpenc2lit/image/upload/v1684365722/Screenshot_2023-05-17_at_4.19.52_PM_jlvbvw.png) +# Example Use Cases + +- **Automated Tweeting of Curated Content:** Create a workflow that monitors RSS feeds or other content sources. When new items are detected, format the content and automatically post it to your Twitter account, keeping your followers engaged with fresh and relevant content. + +- **Twitter Sentiment Analysis:** Set up a workflow that triggers on new mentions of your username on Twitter. Pass the tweet text to a sentiment analysis service like Google's Natural Language API to determine the emotion conveyed. Use this data to respond appropriately or to gather insights on public perception. + +- **Customer Support Ticketing:** Develop a workflow that starts when your company's Twitter account receives a direct message. Automatically create a ticket in a customer support platform like Zendesk or HubSpot, ensuring that your team promptly addresses customer inquiries and issues mentioned on Twitter. + # Troubleshooting Below are some of the most common issues we see. diff --git a/components/twitter/actions/add-user-to-list/add-user-to-list.ts b/components/twitter/actions/add-user-to-list/add-user-to-list.ts index 6b835bd3e52ef..b3193c03eb7e8 100644 --- a/components/twitter/actions/add-user-to-list/add-user-to-list.ts +++ b/components/twitter/actions/add-user-to-list/add-user-to-list.ts @@ -12,7 +12,7 @@ export default defineAction({ key: "twitter-add-user-to-list", name: "Add User To List", description: `Add a member to a list owned by the user. [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.0.8", type: "action", props: { ...common.props, diff --git a/components/twitter/actions/create-tweet/create-tweet.ts b/components/twitter/actions/create-tweet/create-tweet.ts index a0a52a7b1e620..551e2cbc988af 100644 --- a/components/twitter/actions/create-tweet/create-tweet.ts +++ b/components/twitter/actions/create-tweet/create-tweet.ts @@ -10,7 +10,7 @@ export default defineAction({ key: "twitter-create-tweet", name: "Create Tweet", description: `Create a new tweet. [See the documentation](${DOCS_LINK})`, - version: "2.1.5", + version: "2.1.6", type: "action", props: { app, diff --git a/components/twitter/actions/delete-tweet/delete-tweet.ts b/components/twitter/actions/delete-tweet/delete-tweet.ts index 5f2551884d79f..447fb69daaac3 100644 --- a/components/twitter/actions/delete-tweet/delete-tweet.ts +++ b/components/twitter/actions/delete-tweet/delete-tweet.ts @@ -11,7 +11,7 @@ export default defineAction({ key: "twitter-delete-tweet", name: "Delete Tweet", description: `Remove a posted tweet. [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.0.8", type: "action", props: { ...common.props, diff --git a/components/twitter/actions/follow-user/follow-user.ts b/components/twitter/actions/follow-user/follow-user.ts index 7f555010056d2..a7b5d18c5fd24 100644 --- a/components/twitter/actions/follow-user/follow-user.ts +++ b/components/twitter/actions/follow-user/follow-user.ts @@ -12,7 +12,7 @@ export default defineAction({ key: "twitter-follow-user", name: "Follow User", description: `Follow a user. [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.0.8", type: "action", props: { ...common.props, diff --git a/components/twitter/actions/get-tweet/get-tweet.ts b/components/twitter/actions/get-tweet/get-tweet.ts index 567540001ef1f..8b7619db806c2 100644 --- a/components/twitter/actions/get-tweet/get-tweet.ts +++ b/components/twitter/actions/get-tweet/get-tweet.ts @@ -15,7 +15,7 @@ export default defineAction({ key: "twitter-get-tweet", name: "Get Tweet", description: `Return a single tweet specified by ID. [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.0.8", type: "action", props: { ...common.props, diff --git a/components/twitter/actions/get-user/get-user.ts b/components/twitter/actions/get-user/get-user.ts index ba4af2b366b86..5969199d55479 100644 --- a/components/twitter/actions/get-user/get-user.ts +++ b/components/twitter/actions/get-user/get-user.ts @@ -17,7 +17,7 @@ export default defineAction({ key: "twitter-get-user", name: "Get User", description: `Get information about a user. [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.0.8", type: "action", props: { ...common.props, diff --git a/components/twitter/actions/like-tweet/like-tweet.ts b/components/twitter/actions/like-tweet/like-tweet.ts index 9b609f4db9f5f..c19eab42cfdc8 100644 --- a/components/twitter/actions/like-tweet/like-tweet.ts +++ b/components/twitter/actions/like-tweet/like-tweet.ts @@ -11,7 +11,7 @@ export default defineAction({ key: "twitter-like-tweet", name: "Like Tweet", description: `Like a tweet specified by its ID. [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.0.8", type: "action", props: { ...common.props, diff --git a/components/twitter/actions/list-favorites/list-favorites.ts b/components/twitter/actions/list-favorites/list-favorites.ts index 49d5888462a8a..3d518d77d7292 100644 --- a/components/twitter/actions/list-favorites/list-favorites.ts +++ b/components/twitter/actions/list-favorites/list-favorites.ts @@ -23,7 +23,7 @@ export default defineAction({ key: "twitter-list-favorites", name: "List Liked Tweets", description: `Return the most recent tweets liked by you or the specified user. [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.0.8", type: "action", props: { ...common.props, diff --git a/components/twitter/actions/list-followers/list-followers.ts b/components/twitter/actions/list-followers/list-followers.ts index 480d6e7542273..f777bb526fd0f 100644 --- a/components/twitter/actions/list-followers/list-followers.ts +++ b/components/twitter/actions/list-followers/list-followers.ts @@ -23,7 +23,7 @@ export default defineAction({ key: "twitter-list-followers", name: "List Followers", description: `Return a collection of user objects for users following the specified user. [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.0.8", type: "action", props: { ...common.props, diff --git a/components/twitter/actions/list-lists/list-lists.ts b/components/twitter/actions/list-lists/list-lists.ts index c84973871aff5..1aadc6fdd0aaf 100644 --- a/components/twitter/actions/list-lists/list-lists.ts +++ b/components/twitter/actions/list-lists/list-lists.ts @@ -23,7 +23,7 @@ export default defineAction({ key: "twitter-list-lists", name: "List Lists", description: `Get all lists owned by a user. [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.0.8", type: "action", props: { ...common.props, diff --git a/components/twitter/actions/list-mentions/list-mentions.ts b/components/twitter/actions/list-mentions/list-mentions.ts index 0fd86f3ca4948..154e29012a178 100644 --- a/components/twitter/actions/list-mentions/list-mentions.ts +++ b/components/twitter/actions/list-mentions/list-mentions.ts @@ -23,7 +23,7 @@ export default defineAction({ key: "twitter-list-mentions", name: "List Mentions", description: `Return the most recent mentions for the specified user. [See the documentation](${DOCS_LINK})`, - version: "2.0.8", + version: "2.0.9", type: "action", props: { ...common.props, diff --git a/components/twitter/actions/list-user-tweets/list-user-tweets.ts b/components/twitter/actions/list-user-tweets/list-user-tweets.ts index a1f4e9b809a5c..1bbed1747ee0f 100644 --- a/components/twitter/actions/list-user-tweets/list-user-tweets.ts +++ b/components/twitter/actions/list-user-tweets/list-user-tweets.ts @@ -23,7 +23,7 @@ export default defineAction({ key: "twitter-list-user-tweets", name: "List User Tweets", description: `Return a collection of the most recent tweets posted by a user. [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.0.8", type: "action", props: { ...common.props, diff --git a/components/twitter/actions/retweet/retweet.ts b/components/twitter/actions/retweet/retweet.ts index f9d82204a435b..3d1e4de0e9338 100644 --- a/components/twitter/actions/retweet/retweet.ts +++ b/components/twitter/actions/retweet/retweet.ts @@ -11,7 +11,7 @@ export default defineAction({ key: "twitter-retweet", name: "Retweet a tweet", description: `Retweet a tweet specified by ID. [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.0.8", type: "action", props: { ...common.props, diff --git a/components/twitter/actions/send-dm/send-dm.ts b/components/twitter/actions/send-dm/send-dm.ts index 98f1dd89f6df3..d72378d47f3ef 100644 --- a/components/twitter/actions/send-dm/send-dm.ts +++ b/components/twitter/actions/send-dm/send-dm.ts @@ -12,7 +12,7 @@ export default defineAction({ key: "twitter-send-dm", name: "Send Direct Message (DM)", description: `Send a message to a user. [See the documentation](${DOCS_LINK})`, - version: "1.0.5", + version: "1.0.6", type: "action", props: { ...common.props, diff --git a/components/twitter/actions/simple-search-in-list/simple-search-in-list.ts b/components/twitter/actions/simple-search-in-list/simple-search-in-list.ts index d1bbb3079f846..c7ab79977a47e 100644 --- a/components/twitter/actions/simple-search-in-list/simple-search-in-list.ts +++ b/components/twitter/actions/simple-search-in-list/simple-search-in-list.ts @@ -21,7 +21,7 @@ export default defineAction({ key: "twitter-simple-search-in-list", name: "Search Tweets in List", description: `Search Tweets by text in a list. [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.0.8", type: "action", props: { ...common.props, diff --git a/components/twitter/actions/simple-search/simple-search.ts b/components/twitter/actions/simple-search/simple-search.ts index 536786f909854..12ba5e5665315 100644 --- a/components/twitter/actions/simple-search/simple-search.ts +++ b/components/twitter/actions/simple-search/simple-search.ts @@ -21,7 +21,7 @@ export default defineAction({ key: "twitter-simple-search", name: "Search Tweets", description: `Retrieve Tweets from the last seven days that match a query. [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.0.8", type: "action", props: { ...common.props, diff --git a/components/twitter/actions/unfollow-user/unfollow-user.ts b/components/twitter/actions/unfollow-user/unfollow-user.ts index e48cc062c6dcc..284cbd19c551d 100644 --- a/components/twitter/actions/unfollow-user/unfollow-user.ts +++ b/components/twitter/actions/unfollow-user/unfollow-user.ts @@ -12,7 +12,7 @@ export default defineAction({ key: "twitter-unfollow-user", name: "Unfollow User", description: `Unfollow a user. [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.0.8", type: "action", props: { ...common.props, diff --git a/components/twitter/actions/unlike-tweet/unlike-tweet.ts b/components/twitter/actions/unlike-tweet/unlike-tweet.ts index 59004c791d6a8..5a0622fe22261 100644 --- a/components/twitter/actions/unlike-tweet/unlike-tweet.ts +++ b/components/twitter/actions/unlike-tweet/unlike-tweet.ts @@ -11,7 +11,7 @@ export default defineAction({ key: "twitter-unlike-tweet", name: "Unlike Tweet", description: `Unlike a tweet specified by its ID. [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.0.8", type: "action", props: { ...common.props, diff --git a/components/twitter/actions/upload-media/upload-media.ts b/components/twitter/actions/upload-media/upload-media.ts index 2d0e495aaf201..26ac322fd70a2 100644 --- a/components/twitter/actions/upload-media/upload-media.ts +++ b/components/twitter/actions/upload-media/upload-media.ts @@ -13,7 +13,7 @@ export default defineAction({ key: "twitter-upload-media", name: "Upload Media", description: `Upload new media. [See the documentation](${DOCS_LINK})`, - version: "0.0.11", + version: "0.0.12", type: "action", props: { ...common.props, diff --git a/components/twitter/common/addObjIncludes.ts b/components/twitter/common/addObjIncludes.ts new file mode 100644 index 0000000000000..b63675b7e3e64 --- /dev/null +++ b/components/twitter/common/addObjIncludes.ts @@ -0,0 +1,110 @@ +import { + IncludesIdCollection, + IncludesIdCollectionFlattened, +} from "./types/includeMapping"; +import { + DirectMessage, + List, + ResponseIncludes, + Tweet, + TwitterEntity, + User, +} from "./types/responseSchemas"; + +// https://developer.twitter.com/en/docs/twitter-api/expansions + +function flatClearIncludeIds( + obj: IncludesIdCollection, +): IncludesIdCollectionFlattened { + return Object.fromEntries( + Object.entries(obj).map(([ + key, + value, + ]) => [ + key, + value.filter((e) => e !== undefined).flat(), + ]), + ); +} + +export function getDirectMessageIncludeIds(obj: DirectMessage) { + return flatClearIncludeIds({ + userIds: [ + obj.sender_id, + obj.participant_ids, + ], + mediaKeys: [ + obj.attachments?.media_keys, + ], + tweetIds: [ + obj.referenced_tweets?.map(({ id }) => id), + ], + }); +} + +export function getListIncludeIds(obj: List) { + return flatClearIncludeIds({ + userIds: [ + obj.owner_id, + ], + }); +} + +export function getUserIncludeIds(obj: User) { + return flatClearIncludeIds({ + tweetIds: [ + obj.pinned_tweet_id, + ], + }); +} + +export function getTweetIncludeIds(obj: Tweet) { + return flatClearIncludeIds({ + userIds: [ + obj.author_id, + obj.in_reply_to_user_id, + ], + userNames: [ + obj.entities?.mentions?.map(({ username }) => username), + ], + tweetIds: [ + obj.edit_history_tweet_ids, + obj.referenced_tweets?.map(({ id }) => id), + ], + pollIds: [ + obj.attachments?.poll_ids, + ], + placeIds: [ + obj.geo?.place_id, + ], + mediaKeys: [ + obj.attachments?.media_keys, + ], + }); +} + +export function getObjIncludes( + obj: TwitterEntity, + includes: ResponseIncludes = {}, + getIncludeIds: (o: TwitterEntity) => IncludesIdCollectionFlattened, +) { + const { + userIds, userNames, tweetIds, pollIds, placeIds, mediaKeys, + } = + getIncludeIds(obj); + + const result: ResponseIncludes = { + media: includes.media?.filter((item) => mediaKeys?.includes(item.media_key)), + users: includes.users?.filter( + (item) => userIds?.includes(item.id) || userNames?.includes(item.username), + ), + places: includes.places?.filter((item) => placeIds?.includes(item.id)), + polls: includes.polls?.filter((item) => pollIds?.includes(item.id)), + tweets: includes.tweets?.filter((item) => tweetIds?.includes(item.id)), + }; + return Object.fromEntries( + Object.entries(result).filter(([ + , value, + ]) => value?.length > 0), + ); +} diff --git a/components/twitter/common/types/includeMapping.ts b/components/twitter/common/types/includeMapping.ts new file mode 100644 index 0000000000000..b3c8110b475e9 --- /dev/null +++ b/components/twitter/common/types/includeMapping.ts @@ -0,0 +1,11 @@ +type IncludesIdMap = { + mediaKeys?: T; + placeIds?: T; + pollIds?: T; + tweetIds?: T; + userIds?: T; + userNames?: T; +}; + +export type IncludesIdCollection = IncludesIdMap<(string | string[])[]>; +export type IncludesIdCollectionFlattened = IncludesIdMap; diff --git a/components/twitter/common/types/responseSchemas.ts b/components/twitter/common/types/responseSchemas.ts index 167b49463e285..bda4fea425cea 100644 --- a/components/twitter/common/types/responseSchemas.ts +++ b/components/twitter/common/types/responseSchemas.ts @@ -4,13 +4,16 @@ export interface TwitterEntity { export type TwitterEntityMap = Record; -export interface DirectMessage extends TwitterEntity { +export interface DirectMessage extends TwitterEntity, HasMediaAttachments, HasReferencedTweets { event_type: "MessageCreate"; text: string; + sender_id?: string; + participant_ids?: string[]; } export interface List extends TwitterEntity { name: string; + owner_id?: string; } interface MetricsFields { @@ -20,10 +23,32 @@ interface MetricsFields { promoted_metrics?: string; } -export interface Tweet extends TwitterEntity, MetricsFields { - text: string; - edit_history_tweet_ids: string[]; +interface HasReferencedTweets { referenced_tweets?: ReferencedTweet[]; +} +interface HasMediaAttachments { + attachments?: { + media_keys?: string[]; + }; +} + +export interface Tweet + extends TwitterEntity, + MetricsFields, + HasReferencedTweets { + text: string; + author_id?: string; + edit_history_tweet_ids?: string[]; + in_reply_to_user_id?: string; + attachments?: HasMediaAttachments["attachments"] & { + poll_ids: string[]; + }; + geo?: { + place_id: string; + }; + entities?: { + mentions?: User[]; + }; includes?: { tweets?: Tweet[]; }; @@ -37,11 +62,18 @@ export interface ReferencedTweet { export interface User extends TwitterEntity { name: string; username: string; + pinned_tweet_id?: string; } -interface ResponseIncludes { +type Poll = TwitterEntity; +type Place = TwitterEntity; + +export interface ResponseIncludes { + places?: Place[]; + polls?: Poll[]; users?: User[]; tweets?: Tweet[]; + media?: MediaItem[]; } interface ResponseBase { @@ -60,3 +92,7 @@ export type PaginatedResponseObject = ResponseBase & { result_count: number; }; }; + +interface MediaItem { + media_key: string; +} diff --git a/components/twitter/package.json b/components/twitter/package.json index 57761bce8879b..6b077ef4363d2 100644 --- a/components/twitter/package.json +++ b/components/twitter/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/twitter", - "version": "2.2.4", + "version": "2.3.0", "description": "Pipedream Twitter Components", "main": "dist/app/twitter.app.mjs", "keywords": [ diff --git a/components/twitter/sources/new-follower-of-user/new-follower-of-user.ts b/components/twitter/sources/new-follower-of-user/new-follower-of-user.ts index fb9f47559f9ba..114386806e7b4 100644 --- a/components/twitter/sources/new-follower-of-user/new-follower-of-user.ts +++ b/components/twitter/sources/new-follower-of-user/new-follower-of-user.ts @@ -8,13 +8,16 @@ import { } from "../../actions/list-followers/list-followers"; import { User } from "../../common/types/responseSchemas"; import cacheUserId from "../common/cacheUserId"; +import { + getObjIncludes, getUserIncludeIds, +} from "../../common/addObjIncludes"; export default defineSource({ ...common, key: "twitter-new-follower-of-user", name: "New Follower Received by User", description: `Emit new event when the specified User receives a Follower [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.1.0", type: "source", props: { ...common.props, @@ -43,7 +46,10 @@ export default defineSource({ userId, }; - const { data } = await this.app.getUserFollowers(params); + const { + data, includes, + } = await this.app.getUserFollowers(params); + data.forEach((user) => user.includes = getObjIncludes(user, includes, getUserIncludeIds)); return data; }, }, diff --git a/components/twitter/sources/new-list-followed/new-list-followed.ts b/components/twitter/sources/new-list-followed/new-list-followed.ts index 20c572319d442..765a385ffbe89 100644 --- a/components/twitter/sources/new-list-followed/new-list-followed.ts +++ b/components/twitter/sources/new-list-followed/new-list-followed.ts @@ -5,6 +5,9 @@ import { List } from "../../common/types/responseSchemas"; import { getListFields } from "../../common/methods"; import { GetUserFollowedListsParams } from "../../common/types/requestParams"; import cacheUserId from "../common/cacheUserId"; +import { + getListIncludeIds, getObjIncludes, +} from "../../common/addObjIncludes"; const DOCS_LINK = "https://developer.twitter.com/en/docs/twitter-api/lists/list-follows/api-reference/get-users-id-followed_lists"; const MAX_RESULTS_PER_PAGE = 100; @@ -14,7 +17,7 @@ export default defineSource({ key: "twitter-new-list-followed", name: "New List Followed by User", description: `Emit new event when the specified User follows a List [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.1.0", type: "source", props: { ...common.props, @@ -43,7 +46,10 @@ export default defineSource({ userId, }; - const { data } = await this.app.getUserFollowedLists(params); + const { + data, includes, + } = await this.app.getUserFollowedLists(params); + data.forEach((list) => list.includes = getObjIncludes(list, includes, getListIncludeIds)); return data; }, }, diff --git a/components/twitter/sources/new-mention-received-by-user/new-mention-received-by-user.ts b/components/twitter/sources/new-mention-received-by-user/new-mention-received-by-user.ts index 3b39f561072a5..6083d11bc626c 100644 --- a/components/twitter/sources/new-mention-received-by-user/new-mention-received-by-user.ts +++ b/components/twitter/sources/new-mention-received-by-user/new-mention-received-by-user.ts @@ -8,13 +8,16 @@ import { } from "../../actions/list-mentions/list-mentions"; import { Tweet } from "../../common/types/responseSchemas"; import cacheUserId from "../common/cacheUserId"; +import { + getObjIncludes, getTweetIncludeIds, +} from "../../common/addObjIncludes"; export default defineSource({ ...common, key: "twitter-new-mention-received-by-user", name: "New Mention Received by User", description: `Emit new event when the specified User is mentioned in a Tweet [See the documentation](${DOCS_LINK})`, - version: "0.0.5", + version: "0.1.0", type: "source", props: { ...common.props, @@ -43,7 +46,10 @@ export default defineSource({ userId, }; - const { data } = await this.app.getUserMentions(params); + const { + data, includes, + } = await this.app.getUserMentions(params); + data.forEach((tweet) => tweet.includes = getObjIncludes(tweet, includes, getTweetIncludeIds)); return data; }, }, diff --git a/components/twitter/sources/new-message/new-message.ts b/components/twitter/sources/new-message/new-message.ts index 47a019be45cf3..18f70ec1a7013 100644 --- a/components/twitter/sources/new-message/new-message.ts +++ b/components/twitter/sources/new-message/new-message.ts @@ -4,6 +4,9 @@ import { getTweetSummary as getItemSummary } from "../common/getItemSummary"; import { getMessageFields } from "../../common/methods"; import { GetDirectMessagesParams } from "../../common/types/requestParams"; import { DirectMessage } from "../../common/types/responseSchemas"; +import { + getDirectMessageIncludeIds, getObjIncludes, +} from "../../common/addObjIncludes"; const DOCS_LINK = "https://developer.twitter.com/en/docs/twitter-api/direct-messages/lookup/api-reference/get-dm_events"; const MAX_RESULTS_PER_PAGE = 100; @@ -13,7 +16,7 @@ export default defineSource({ key: "twitter-new-message", name: "New Message Received", description: `Emit new event when a new Direct Message (DM) is received [See the documentation](${DOCS_LINK})`, - version: "1.0.5", + version: "1.1.0", type: "source", methods: { ...common.methods, @@ -33,7 +36,10 @@ export default defineSource({ }, }; - const { data } = await this.app.getDirectMessages(params); + const { + data, includes, + } = await this.app.getDirectMessages(params); + data.forEach((msg) => msg.includes = getObjIncludes(msg, includes, getDirectMessageIncludeIds)); return data; }, }, diff --git a/components/twitter/sources/new-tweet-in-list/new-tweet-in-list.ts b/components/twitter/sources/new-tweet-in-list/new-tweet-in-list.ts index c53d09ba930c8..9688c58ae4b24 100644 --- a/components/twitter/sources/new-tweet-in-list/new-tweet-in-list.ts +++ b/components/twitter/sources/new-tweet-in-list/new-tweet-in-list.ts @@ -7,13 +7,16 @@ import { Tweet } from "../../common/types/responseSchemas"; import { DOCS_LINK, MAX_RESULTS_PER_PAGE, } from "../../actions/simple-search-in-list/simple-search-in-list"; +import { + getObjIncludes, getTweetIncludeIds, +} from "../../common/addObjIncludes"; export default defineSource({ ...common, key: "twitter-new-tweet-in-list", name: "New Tweet Posted in List", description: `Emit new event when a Tweet is posted in the specified list [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.1.0", type: "source", props: { ...common.props, @@ -40,7 +43,10 @@ export default defineSource({ params: this.getTweetFields(), }; - const { data } = await this.app.getListTweets(params); + const { + data, includes, + } = await this.app.getListTweets(params); + data.forEach((tweet) => tweet.includes = getObjIncludes(tweet, includes, getTweetIncludeIds)); return data; }, }, diff --git a/components/twitter/sources/new-tweet-liked-by-user/new-tweet-liked-by-user.ts b/components/twitter/sources/new-tweet-liked-by-user/new-tweet-liked-by-user.ts index d68133933d78e..4fac854dcded7 100644 --- a/components/twitter/sources/new-tweet-liked-by-user/new-tweet-liked-by-user.ts +++ b/components/twitter/sources/new-tweet-liked-by-user/new-tweet-liked-by-user.ts @@ -8,13 +8,16 @@ import { } from "../../actions/list-favorites/list-favorites"; import { Tweet } from "../../common/types/responseSchemas"; import cacheUserId from "../common/cacheUserId"; +import { + getObjIncludes, getTweetIncludeIds, +} from "../../common/addObjIncludes"; export default defineSource({ ...common, key: "twitter-new-tweet-liked-by-user", name: "New Tweet Liked by User", description: `Emit new event when a Tweet is liked by the specified User [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.1.0", type: "source", props: { ...common.props, @@ -43,7 +46,10 @@ export default defineSource({ userId, }; - const { data } = await this.app.getUserLikedTweets(params); + const { + data, includes, + } = await this.app.getUserLikedTweets(params); + data.forEach((tweet) => tweet.includes = getObjIncludes(tweet, includes, getTweetIncludeIds)); return data; }, }, diff --git a/components/twitter/sources/new-tweet-metrics/new-tweet-metrics.ts b/components/twitter/sources/new-tweet-metrics/new-tweet-metrics.ts index 9b822575ec228..a253372222e28 100644 --- a/components/twitter/sources/new-tweet-metrics/new-tweet-metrics.ts +++ b/components/twitter/sources/new-tweet-metrics/new-tweet-metrics.ts @@ -11,7 +11,7 @@ export default defineSource({ key: "twitter-new-tweet-metrics", name: "New Tweet Metrics", description: `Emit new event when a Tweet has new metrics [See the documentation](${DOCS_LINK})`, - version: "1.0.5", + version: "1.0.6", type: "source", props: { ...common.props, diff --git a/components/twitter/sources/new-tweet-posted-by-user/new-tweet-posted-by-user.ts b/components/twitter/sources/new-tweet-posted-by-user/new-tweet-posted-by-user.ts index a6055e5ea900c..79a0d537d677f 100644 --- a/components/twitter/sources/new-tweet-posted-by-user/new-tweet-posted-by-user.ts +++ b/components/twitter/sources/new-tweet-posted-by-user/new-tweet-posted-by-user.ts @@ -8,13 +8,16 @@ import { DOCS_LINK, MAX_RESULTS_PER_PAGE, } from "../../actions/list-user-tweets/list-user-tweets"; import cacheUserId from "../common/cacheUserId"; +import { + getObjIncludes, getTweetIncludeIds, +} from "../../common/addObjIncludes"; export default defineSource({ ...common, key: "twitter-new-tweet-posted-by-user", name: "New Tweet Posted by User", description: `Emit new event when the specified User posts a Tweet [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.1.0", type: "source", props: { ...common.props, @@ -43,7 +46,10 @@ export default defineSource({ userId, }; - const { data } = await this.app.getUserTweets(params); + const { + data, includes, + } = await this.app.getUserTweets(params); + data.forEach((tweet) => tweet.includes = getObjIncludes(tweet, includes, getTweetIncludeIds)); return data; }, }, diff --git a/components/twitter/sources/new-tweet-posted-matching-query/new-tweet-posted-matching-query.ts b/components/twitter/sources/new-tweet-posted-matching-query/new-tweet-posted-matching-query.ts index d247aea56b899..3129aed5b3eaf 100644 --- a/components/twitter/sources/new-tweet-posted-matching-query/new-tweet-posted-matching-query.ts +++ b/components/twitter/sources/new-tweet-posted-matching-query/new-tweet-posted-matching-query.ts @@ -7,13 +7,16 @@ import { Tweet } from "../../common/types/responseSchemas"; import { DOCS_LINK, MAX_RESULTS_PER_PAGE, } from "../../actions/simple-search/simple-search"; +import { + getObjIncludes, getTweetIncludeIds, +} from "../../common/addObjIncludes"; export default defineSource({ ...common, key: "twitter-new-tweet-posted-matching-query", name: "New Tweet Posted Matching Query", description: `Emit new event when a new tweet matching the specified query is posted [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.1.1", type: "source", props: { ...common.props, @@ -42,7 +45,10 @@ export default defineSource({ }, }; - const { data } = await this.app.searchTweets(params); + const { + data, includes, + } = await this.app.searchTweets(params); + data.forEach((tweet) => tweet.includes = getObjIncludes(tweet, includes, getTweetIncludeIds)); return data; }, }, diff --git a/components/twitter/sources/new-unfollower-of-user/new-unfollower-of-user.ts b/components/twitter/sources/new-unfollower-of-user/new-unfollower-of-user.ts index cc77fde4cde1d..efc1fa87dca69 100644 --- a/components/twitter/sources/new-unfollower-of-user/new-unfollower-of-user.ts +++ b/components/twitter/sources/new-unfollower-of-user/new-unfollower-of-user.ts @@ -8,13 +8,16 @@ import { import { User } from "../../common/types/responseSchemas"; import { GetUserFollowersParams } from "../../common/types/requestParams"; import cacheUserId from "../common/cacheUserId"; +import { + getObjIncludes, getUserIncludeIds, +} from "../../common/addObjIncludes"; export default defineSource({ ...common, key: "twitter-new-unfollower-of-user", name: "New Unfollower of User", description: `Emit new event when the specified User loses a Follower [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.1.0", type: "source", props: { ...common.props, @@ -47,7 +50,10 @@ export default defineSource({ userId, }; - const { data } = await this.app.getUserFollowers(params); + const { + data, includes, + } = await this.app.getUserFollowers(params); + data.forEach((user) => user.includes = getObjIncludes(user, includes, getUserIncludeIds)); return data; }, async getAndProcessData(emit = false) { diff --git a/components/twitter/sources/new-user-followed/new-user-followed.ts b/components/twitter/sources/new-user-followed/new-user-followed.ts index cf4e88fc0b0ca..5b9c4b9a5a223 100644 --- a/components/twitter/sources/new-user-followed/new-user-followed.ts +++ b/components/twitter/sources/new-user-followed/new-user-followed.ts @@ -5,6 +5,9 @@ import { getUserFields } from "../../common/methods"; import { GetUserFollowingParams } from "../../common/types/requestParams"; import { User } from "../../common/types/responseSchemas"; import cacheUserId from "../common/cacheUserId"; +import { + getObjIncludes, getUserIncludeIds, +} from "../../common/addObjIncludes"; const DOCS_LINK = "https://developer.twitter.com/en/docs/twitter-api/users/follows/api-reference/get-users-id-following"; const MAX_RESULTS_PER_PAGE = 1000; @@ -14,7 +17,7 @@ export default defineSource({ key: "twitter-new-user-followed", name: "New User Followed by User", description: `Emit new event when the specified User follows another User [See the documentation](${DOCS_LINK})`, - version: "2.0.7", + version: "2.1.0", type: "source", props: { ...common.props, @@ -43,7 +46,10 @@ export default defineSource({ userId, }; - const { data } = await this.app.getUserFollowing(params); + const { + data, includes, + } = await this.app.getUserFollowing(params); + data.forEach((user) => user.includes = getObjIncludes(user, includes, getUserIncludeIds)); return data; }, }, diff --git a/components/twitter/twitter.app.mjs b/components/twitter/twitter.app.mjs new file mode 100644 index 0000000000000..55d81a68bfa48 --- /dev/null +++ b/components/twitter/twitter.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "twitter", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/twitter_ads/README.md b/components/twitter_ads/README.md new file mode 100644 index 0000000000000..a9525a9e48e16 --- /dev/null +++ b/components/twitter_ads/README.md @@ -0,0 +1,11 @@ +# Overview + +The Twitter Ads API offers a powerful suite of tools for managing Twitter ad campaigns programmatically. With this API, you can automate ad operations, fetch performance metrics, manage audiences, and tailor ads to specific objectives or target demographics. Integrating this API with Pipedream amplifies its capabilities, allowing for seamless workflows that connect with other services, trigger actions based on data insights, and manage ad campaigns efficiently across multiple platforms. + +# Example Use Cases + +- **Automatic Campaign Reporting**: Generate regular reports on ad performance by pulling data from the Twitter Ads API and sending insights to Slack, email, or Google Sheets. This workflow can be scheduled to run at regular intervals, providing consistent updates to your marketing team. + +- **Dynamic Ad Adjustment**: Monitor your website's product inventory or pricing changes using a webhook or directly interfacing with an e-commerce platform like Shopify. When a change is detected, update your Twitter ad campaigns accordingly to keep promotions in sync with current offerings. + +- **Lead Management Integration**: After a user engages with an ad on Twitter, use Pipedream to capture these interactions and add the user's contact information to a CRM like Salesforce or HubSpot. This workflow can streamline lead generation and ensure prompt follow-up on potential sales opportunities. diff --git a/components/twitter_ads/package.json b/components/twitter_ads/package.json index fed015867aa33..1b4d03cb94a5b 100644 --- a/components/twitter_ads/package.json +++ b/components/twitter_ads/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/txt_werk/README.md b/components/txt_werk/README.md new file mode 100644 index 0000000000000..b6974c6b4dca6 --- /dev/null +++ b/components/txt_werk/README.md @@ -0,0 +1,11 @@ +# Overview + +The TXT Werk API specializes in advanced natural language processing, providing tools for text analysis such as entity recognition, sentiment analysis, and keyword extraction. Integrating TXT Werk with Pipedream allows users to automate workflows involving text data enrichment and analysis, transforming unstructured text into actionable insights. This integration can enhance applications in content moderation, market research, and customer feedback analysis by automatically processing text data from various sources. + +# Example Use Cases + +- **Customer Sentiment Analysis**: Automatically analyze customer reviews or feedback from different platforms using TXT Werk's sentiment analysis. Trigger a workflow on Pipedream when new feedback is collected via webhooks, process the text through TXT Werk to determine sentiment, and log results in a Google Sheets or send a summary via Slack to the customer service team. + +- **Content Moderation for User-Generated Content**: Use TXT Werk's entity recognition and keyword extraction to moderate content on forums or social media platforms. Set up a workflow on Pipedream that triggers whenever new posts are made, analyze the content with TXT Werk, and automatically flag or remove inappropriate or sensitive content based on predefined criteria. + +- **Market Research and Trend Analysis**: Extract and analyze key phrases and entities from news articles or social media posts to identify trends and insights with TXT Werk. Configure a Pipedream workflow to periodically fetch new content from news feeds or Twitter, process the content through TXT Werk, and aggregate the results in a dashboard tool like Google Data Studio for easy visualization and trend spotting. diff --git a/components/txt_werk/actions/analyze-text/analyze-text.mjs b/components/txt_werk/actions/analyze-text/analyze-text.mjs new file mode 100644 index 0000000000000..7c8d6bd910443 --- /dev/null +++ b/components/txt_werk/actions/analyze-text/analyze-text.mjs @@ -0,0 +1,44 @@ +import app from "../../txt_werk.app.mjs"; + +export default { + key: "txt_werk-analyze-text", + name: "Analyze Text", + description: "Send a text to be analyzed by TXTWerk. [See the documentation](https://services.txtwerk.de/ws/documentation/showRequest)", + version: "0.0.1", + type: "action", + props: { + app, + text: { + propDefinition: [ + app, + "text", + ], + }, + title: { + propDefinition: [ + app, + "title", + ], + }, + services: { + propDefinition: [ + app, + "services", + ], + }, + }, + async run({ $ }) { + const response = await this.app.analyzeText({ + $, + params: { + text: this.text, + title: this.title, + services: this.services.join(","), + }, + }); + + $.export("$summary", "Text successfully analyzed"); + + return response; + }, +}; diff --git a/components/txt_werk/common/constants.mjs b/components/txt_werk/common/constants.mjs new file mode 100644 index 0000000000000..d0dd499812aec --- /dev/null +++ b/components/txt_werk/common/constants.mjs @@ -0,0 +1,14 @@ +export default { + SERVICES: [ + "entities", + "tags", + "categories", + "dates", + "measures", + "authors", + "fingerprints", + "lexiconEntities", + "lexiconTags", + "nerEntities", + ], +}; diff --git a/components/txt_werk/package.json b/components/txt_werk/package.json new file mode 100644 index 0000000000000..9c913888bb415 --- /dev/null +++ b/components/txt_werk/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/txt_werk", + "version": "0.1.0", + "description": "Pipedream TXT Werk Components", + "main": "txt_werk.app.mjs", + "keywords": [ + "pipedream", + "txt_werk" + ], + "homepage": "https://pipedream.com/apps/txt_werk", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/txt_werk/txt_werk.app.mjs b/components/txt_werk/txt_werk.app.mjs new file mode 100644 index 0000000000000..a4bb2f7667c23 --- /dev/null +++ b/components/txt_werk/txt_werk.app.mjs @@ -0,0 +1,54 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "txt_werk", + propDefinitions: { + text: { + type: "string", + label: "Text", + description: "Text to be analyzed", + }, + title: { + type: "string", + label: "Title", + description: "Title of the document to be analyzed", + optional: true, + }, + services: { + type: "string[]", + label: "Services", + description: "List of services to request", + options: constants.SERVICES, + }, + }, + methods: { + _baseUrl() { + return "https://api.txtwerk.de/rest/txt"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "X-Api-Key": `${this.$auth.api_key}`, + }, + }); + }, + async analyzeText(args = {}) { + return this._makeRequest({ + method: "post", + path: "/analyzer", + ...args, + }); + }, + }, +}; diff --git a/components/typebot/README.md b/components/typebot/README.md index 87c925084bb57..71d1573623c01 100644 --- a/components/typebot/README.md +++ b/components/typebot/README.md @@ -1,18 +1,11 @@ # Overview -With the Typebot API, you can build conversational assistants for your app, -website, or SMS system. This includes a wide range of products, from simple -chatbot interactions to complex artificial intelligence. With the Typebot API, -you can easily create unique, intelligent experiences for your users. +Typebot is a conversational form builder that allows you to create interactive and engaging forms for data collection. With Typebot API on Pipedream, you can automate the processing of form submissions, synchronize data with other services, and trigger customized workflows. Utilize the data gathered from Typebot in real-time, enrich it with other services, and streamline processes like lead generation, surveys, and feedback collection. -Here are some examples of what you can build with the Typebot API: +# Example Use Cases -- Create a personalized, AI-powered conversational assistant for your website, - app, or SMS system. -- Automate customer service inquiries with chatbot conversations. -- Generate personalized and automated responses to customer inquiries. -- Create solutions that increase customer engagement. -- Create a conversational interface with natural language processing. -- Create dynamic content with natural language generation. -- Build integrations with other products such as Google Home, Alexa, and Slack. -- Deliver contextualized customer service with predictive analytics. +- **Automated Lead Qualification and CRM Entry**: When a new form submission from Typebot is detected, Pipedream can evaluate the responses to qualify leads. Based on specified criteria, it can automatically add qualified leads to a CRM like Salesforce or HubSpot, ensuring that your sales team focuses on the right prospects. + +- **Feedback Analysis and Ticket Creation**: Upon receiving customer feedback via a Typebot form, trigger a Pipedream workflow that performs sentiment analysis using a tool like MonkeyLearn. If negative feedback is detected, create a support ticket in a system like Zendesk or Jira to follow up proactively. + +- **Event Registration and Confirmation Emails**: Use Typebot forms for event registration. With Pipedream, when a submission is made, it can confirm the registration by adding the attendee to an event management app like Eventbrite, and send a personalized confirmation email using SendGrid or another email service provider. diff --git a/components/typeflowai/package.json b/components/typeflowai/package.json new file mode 100644 index 0000000000000..226dc9512cff4 --- /dev/null +++ b/components/typeflowai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/typeflowai", + "version": "0.1.0", + "description": "Pipedream TypeflowAI Components", + "main": "typeflowai.app.mjs", + "keywords": [ + "pipedream", + "typeflowai" + ], + "homepage": "https://pipedream.com/apps/typeflowai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/typeflowai/sources/common/base.mjs b/components/typeflowai/sources/common/base.mjs new file mode 100644 index 0000000000000..b6c396d586741 --- /dev/null +++ b/components/typeflowai/sources/common/base.mjs @@ -0,0 +1,54 @@ +import typeflowai from "../../typeflowai.app.mjs"; + +export default { + props: { + typeflowai, + db: "$.service.db", + http: "$.interface.http", + workflowIds: { + propDefinition: [ + typeflowai, + "workflowIds", + ], + }, + }, + hooks: { + async activate() { + const { data: { id } } = await this.typeflowai.createWebhook({ + data: { + url: this.http.endpoint, + triggers: this.getTriggers(), + workflowIds: this.workflowIds, + }, + }); + this._setHookId(id); + }, + async deactivate() { + const hookId = this._getHookId(); + if (hookId) { + await this.typeflowai.deleteWebhook({ + hookId, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getTriggers() { + throw new Error("getTriggers is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run(event) { + const { body } = event; + const meta = this.generateMeta(body.data); + this.$emit(body, meta); + }, +}; diff --git a/components/typeflowai/sources/new-response-created/new-response-created.mjs b/components/typeflowai/sources/new-response-created/new-response-created.mjs new file mode 100644 index 0000000000000..bb356a3c7a216 --- /dev/null +++ b/components/typeflowai/sources/new-response-created/new-response-created.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "typeflowai-new-response-created", + name: "New Response Created (Instant)", + description: "Emit new event when a response is created for a workflow in TypeflowAI. [See the documentation](https://typeflowai.com/docs/api/management/webhooks)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTriggers() { + return [ + "responseCreated", + ]; + }, + generateMeta(data) { + return { + id: data.id, + summary: `New Response Created with ID: ${data.id}`, + ts: Date.parse(data.createdAt), + }; + }, + }, + sampleEmit, +}; diff --git a/components/typeflowai/sources/new-response-created/test-event.mjs b/components/typeflowai/sources/new-response-created/test-event.mjs new file mode 100644 index 0000000000000..6dbd546896db6 --- /dev/null +++ b/components/typeflowai/sources/new-response-created/test-event.mjs @@ -0,0 +1,34 @@ +export default { + "webhookId": "clysvncl1000h3zacrgownemj", + "event": "responseCreated", + "data": { + "id": "clysvvfoy00016uyngkpnkfa6", + "createdAt": "2024-07-19T15:57:13.138Z", + "updatedAt": "2024-07-19T15:57:13.138Z", + "workflowId": "clyovrgyj00001xs7dp1q82pn", + "person": null, + "personAttributes": null, + "finished": false, + "data": { + "inputType": "text", + "lead-name": "Bob Ross" + }, + "ttc": { + "lead-name": 202776.6000000238 + }, + "notes": [], + "tags": [], + "meta": { + "source": "", + "url": "https://dashboard.typeflowai.com/s/clyovrgyj00001xs7dp1q82pn", + "userAgent": { + "browser": "Chrome", + "os": "Mac OS", + "device": "desktop" + }, + "country": "US" + }, + "singleUseId": null, + "language": "default" + } +} \ No newline at end of file diff --git a/components/typeflowai/sources/new-response-finished/new-response-finished.mjs b/components/typeflowai/sources/new-response-finished/new-response-finished.mjs new file mode 100644 index 0000000000000..caec512793343 --- /dev/null +++ b/components/typeflowai/sources/new-response-finished/new-response-finished.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "typeflowai-new-response-finished", + name: "New Response Finished (Instant)", + description: "Emit new event when a response is marked as finished. [See the documentation](https://typeflowai.com/docs/api/management/webhooks)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTriggers() { + return [ + "responseFinished", + ]; + }, + generateMeta(data) { + return { + id: data.id, + summary: `New Response Finished with ID: ${data.id}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/typeflowai/sources/new-response-finished/test-event.mjs b/components/typeflowai/sources/new-response-finished/test-event.mjs new file mode 100644 index 0000000000000..16fd830ab9133 --- /dev/null +++ b/components/typeflowai/sources/new-response-finished/test-event.mjs @@ -0,0 +1,35 @@ +export default { + "webhookId": "clysvosum000j3zacdfyu8214", + "event": "responseFinished", + "data": { + "id": "clysxxx5c000d6uynim7hydd4", + "createdAt": "2024-07-19T16:55:08.305Z", + "updatedAt": "2024-07-19T16:55:08.305Z", + "workflowId": "clysxvqnt0000147rgp14gxjs", + "person": null, + "personAttributes": null, + "finished": true, + "data": { + "inputType": "text", + "x45vvrnfd06yl46ay1iwtpj1": "hello world" + }, + "ttc": { + "_total": 9927.100000023842, + "x45vvrnfd06yl46ay1iwtpj1": 9927.100000023842 + }, + "notes": [], + "tags": [], + "meta": { + "source": "", + "url": "https://dashboard.typeflowai.com/s/clysxvqnt0000147rgp14gxjs", + "userAgent": { + "browser": "Chrome", + "os": "Mac OS", + "device": "desktop" + }, + "country": "US" + }, + "singleUseId": null, + "language": "default" + } +} \ No newline at end of file diff --git a/components/typeflowai/sources/new-response-updated/new-response-updated.mjs b/components/typeflowai/sources/new-response-updated/new-response-updated.mjs new file mode 100644 index 0000000000000..4f747e666a9e0 --- /dev/null +++ b/components/typeflowai/sources/new-response-updated/new-response-updated.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "typeflowai-new-response-updated", + name: "New Response Updated (Instant)", + description: "Emit new event when a response is updated within a workflow. [See the documentation](https://typeflowai.com/docs/api/management/webhooks)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTriggers() { + return [ + "responseUpdated", + ]; + }, + generateMeta(data) { + const ts = Date.parse(data.updatedAt); + return { + id: `${data.id}-${ts}`, + summary: `New Response Updated with ID: ${data.id}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/typeflowai/sources/new-response-updated/test-event.mjs b/components/typeflowai/sources/new-response-updated/test-event.mjs new file mode 100644 index 0000000000000..de7f65e2f004f --- /dev/null +++ b/components/typeflowai/sources/new-response-updated/test-event.mjs @@ -0,0 +1,36 @@ +export default { + "webhookId": "clysvo37m000i3zacvkle85xz", + "event": "responseUpdated", + "data": { + "id": "clysvvfoy00016uyngkpnkfa6", + "createdAt": "2024-07-19T15:57:13.138Z", + "updatedAt": "2024-07-19T15:57:29.219Z", + "workflowId": "clyovrgyj00001xs7dp1q82pn", + "person": null, + "personAttributes": null, + "finished": false, + "data": { + "inputType": "text", + "lead-name": "Bob Ross", + "lead-company-do": "Art" + }, + "ttc": { + "lead-name": 202776.6000000238, + "lead-company-do": 17130.09999999404 + }, + "notes": [], + "tags": [], + "meta": { + "source": "", + "url": "https://dashboard.typeflowai.com/s/clyovrgyj00001xs7dp1q82pn", + "userAgent": { + "browser": "Chrome", + "os": "Mac OS", + "device": "desktop" + }, + "country": "US" + }, + "singleUseId": null, + "language": "default" + } +} \ No newline at end of file diff --git a/components/typeflowai/typeflowai.app.mjs b/components/typeflowai/typeflowai.app.mjs new file mode 100644 index 0000000000000..2155b68c668f7 --- /dev/null +++ b/components/typeflowai/typeflowai.app.mjs @@ -0,0 +1,63 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "typeflowai", + propDefinitions: { + workflowIds: { + type: "string[]", + label: "Workflow IDs", + description: "List of workflow IDs that will trigger the webhook. If not provided, the webhook will be triggered for all workflows.", + async options() { + const { data } = await this.listWorkflows(); + return data?.map(({ + id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://dashboard.typeflowai.com/api/v1"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "x-api-key": this.$auth.api_key, + }, + }); + }, + listWorkflows(opts = {}) { + return this._makeRequest({ + path: "/management/workflows", + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteWebhook({ + hookId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${hookId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/typeform/README.md b/components/typeform/README.md index 6bf3c334d0c75..edd744149bc8d 100644 --- a/components/typeform/README.md +++ b/components/typeform/README.md @@ -1,13 +1,11 @@ # Overview -With the Typeform API, you can easily create beautiful, interactive forms that -allow people to share their information with you in an engaging way. Here are -some examples of what you can build with the Typeform API: - -- A contact form for your website that allows people to easily get in touch - with you -- An order form for your product or service that allows people to place orders - easily and securely -- A survey form that allows you to collect data and feedback from people -- A registration form for your event or conference that allows people to sign - up easily and securely +The Typeform API furnishes you with the means to create dynamic forms and collect user responses in real-time. By leveraging this API within Pipedream's serverless platform, you can automate workflows to process this data, integrate seamlessly with other services, and react to form submissions instantaneously. This empowers you to craft tailored responses, synchronize with databases, trigger email campaigns, or even manage event registrations without manual intervention. + +# Example Use Cases + +- **Customer Feedback to Slack Alerts**: Automate the collection of customer feedback by sending new Typeform responses to a designated Slack channel. This workflow enables real-time alerts for your team to swiftly address customer concerns or praises. + +- **Event Registration and Calendar Sync**: Streamline the event registration process by using Typeform to capture attendee details and automatically add new registrants to a Google Calendar event. This ensures a seamless and organized experience for both the planners and the participants. + +- **Lead Capture to CRM Integration**: Convert Typeform entries into leads by piping responses directly into a CRM platform like Salesforce. Automatically create contacts or update existing records, ensuring your sales team always has the latest lead information at their disposal. diff --git a/components/typefully/actions/create-draft/create-draft.mjs b/components/typefully/actions/create-draft/create-draft.mjs new file mode 100644 index 0000000000000..9f0c2a3827856 --- /dev/null +++ b/components/typefully/actions/create-draft/create-draft.mjs @@ -0,0 +1,61 @@ +import typefully from "../../typefully.app.mjs"; + +export default { + key: "typefully-create-draft", + name: "Create Draft", + description: "Creates a new draft in Typefully. [See the documentation](https://support.typefully.com/en/articles/8718287-typefully-api#h_df59629cbf)", + version: "0.0.1", + type: "action", + props: { + typefully, + content: { + propDefinition: [ + typefully, + "content", + ], + }, + threadify: { + propDefinition: [ + typefully, + "threadify", + ], + optional: true, + }, + share: { + propDefinition: [ + typefully, + "share", + ], + optional: true, + }, + autoRetweetEnabled: { + propDefinition: [ + typefully, + "autoRetweetEnabled", + ], + optional: true, + }, + autoPlugEnabled: { + propDefinition: [ + typefully, + "autoPlugEnabled", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.typefully.createDraft({ + $, + data: { + content: this.content, + threadify: this.threadify, + share: this.share, + auto_retweet_enabled: this.autoRetweetEnabled, + auto_plug_enabled: this.autoPlugEnabled, + }, + }); + + $.export("$summary", `Created draft with ID: ${response.id}.`); + return response; + }, +}; diff --git a/components/typefully/actions/schedule-draft-next-slot/schedule-draft-next-slot.mjs b/components/typefully/actions/schedule-draft-next-slot/schedule-draft-next-slot.mjs new file mode 100644 index 0000000000000..ade6b6b3c45f1 --- /dev/null +++ b/components/typefully/actions/schedule-draft-next-slot/schedule-draft-next-slot.mjs @@ -0,0 +1,62 @@ +import typefully from "../../typefully.app.mjs"; + +export default { + key: "typefully-schedule-draft-next-slot", + name: "Schedule Draft Next Slot", + description: "Schedules an existing draft for publication in the next available time slot. [See the documentation](https://support.typefully.com/en/articles/8718287-typefully-api#h_df59629cbf)", + version: "0.0.1", + type: "action", + props: { + typefully, + content: { + propDefinition: [ + typefully, + "content", + ], + }, + threadify: { + propDefinition: [ + typefully, + "threadify", + ], + optional: true, + }, + share: { + propDefinition: [ + typefully, + "share", + ], + optional: true, + }, + autoRetweetEnabled: { + propDefinition: [ + typefully, + "autoRetweetEnabled", + ], + optional: true, + }, + autoPlugEnabled: { + propDefinition: [ + typefully, + "autoPlugEnabled", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.typefully.createDraft({ + $, + data: { + "content": this.content, + "threadify": this.threadify, + "share": this.share, + "schedule-date": "next-free-slot", + "auto_retweet_enabled": this.autoRetweetEnabled, + "auto_plug_enabled": this.autoPlugEnabled, + }, + }); + + $.export("$summary", `Draft scheduled successfully with ID: ${response.id}.`); + return response; + }, +}; diff --git a/components/typefully/actions/schedule-draft/schedule-draft.mjs b/components/typefully/actions/schedule-draft/schedule-draft.mjs new file mode 100644 index 0000000000000..7cfa0aafe00c3 --- /dev/null +++ b/components/typefully/actions/schedule-draft/schedule-draft.mjs @@ -0,0 +1,68 @@ +import typefully from "../../typefully.app.mjs"; + +export default { + key: "typefully-schedule-draft", + name: "Schedule Draft", + description: "Schedules a draft for publication at a specific date and time. [See the documentation](https://support.typefully.com/en/articles/8718287-typefully-api#h_df59629cbf)", + version: "0.0.1", + type: "action", + props: { + typefully, + content: { + propDefinition: [ + typefully, + "content", + ], + }, + threadify: { + propDefinition: [ + typefully, + "threadify", + ], + optional: true, + }, + share: { + propDefinition: [ + typefully, + "share", + ], + optional: true, + }, + scheduleDate: { + propDefinition: [ + typefully, + "scheduleDate", + ], + }, + autoRetweetEnabled: { + propDefinition: [ + typefully, + "autoRetweetEnabled", + ], + optional: true, + }, + autoPlugEnabled: { + propDefinition: [ + typefully, + "autoPlugEnabled", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.typefully.createDraft({ + $, + data: { + "content": this.content, + "threadify": this.threadify, + "share": this.share, + "schedule-date": this.scheduleDate, + "auto_retweet_enabled": this.autoRetweetEnabled, + "auto_plug_enabled": this.autoPlugEnabled, + }, + }); + + $.export("$summary", `Draft scheduled successfully with ID: ${response.id}.`); + return response; + }, +}; diff --git a/components/typefully/package.json b/components/typefully/package.json new file mode 100644 index 0000000000000..04f55f3dd9362 --- /dev/null +++ b/components/typefully/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/typefully", + "version": "0.1.0", + "description": "Pipedream Typefully Components", + "main": "typefully.app.mjs", + "keywords": [ + "pipedream", + "typefully" + ], + "homepage": "https://pipedream.com/apps/typefully", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/typefully/sources/common/base.mjs b/components/typefully/sources/common/base.mjs new file mode 100644 index 0000000000000..39c8b4bb5420e --- /dev/null +++ b/components/typefully/sources/common/base.mjs @@ -0,0 +1,57 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import typefully from "../../typefully.app.mjs"; + +export default { + props: { + typefully, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + async emitEvent(maxResults = false) { + const lastId = this._getLastId(); + const fn = await this.getFunction(); + const response = await fn(); + + let responseArray = []; + for await (const item of response) { + if (item.id <= lastId) break; + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastId(responseArray[0].id); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(new Date()), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/typefully/sources/new-draft-published/new-draft-published.mjs b/components/typefully/sources/new-draft-published/new-draft-published.mjs new file mode 100644 index 0000000000000..79a6e2db5f90c --- /dev/null +++ b/components/typefully/sources/new-draft-published/new-draft-published.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "typefully-new-draft-published", + name: "New Draft Published", + description: "Emit new event when a draft is published to Twitter via Typefully.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.typefully.getRecentlyPublishedDrafts; + }, + getSummary(draft) { + return `Draft Published: ${draft.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/typefully/sources/new-draft-published/test-event.mjs b/components/typefully/sources/new-draft-published/test-event.mjs new file mode 100644 index 0000000000000..27370baa8303b --- /dev/null +++ b/components/typefully/sources/new-draft-published/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "id": 123, + "status": "published", + "html": "content test", + "num_tweets": 1, + "last_edited": null, + "scheduled_date": null, + "published_on": "2025-01-09T10:22:11Z", + "share_url": "https://typefully.com/t/nb9Lxx13df", + "twitter_url": null, + "linkedin_url": null, + "text_first_tweet": "content", + "html_first_tweet": "content", + "text_preview_linkedin": null +} \ No newline at end of file diff --git a/components/typefully/sources/new-draft-scheduled/new-draft-scheduled.mjs b/components/typefully/sources/new-draft-scheduled/new-draft-scheduled.mjs new file mode 100644 index 0000000000000..790400310a02a --- /dev/null +++ b/components/typefully/sources/new-draft-scheduled/new-draft-scheduled.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "typefully-new-draft-scheduled", + name: "New Draft Scheduled", + description: "Emit new event when a draft is scheduled for publication.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.typefully.getRecentlyScheduledDrafts; + }, + getSummary(draft) { + return `Scheduled draft: ${draft.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/typefully/sources/new-draft-scheduled/test-event.mjs b/components/typefully/sources/new-draft-scheduled/test-event.mjs new file mode 100644 index 0000000000000..d9274837f569e --- /dev/null +++ b/components/typefully/sources/new-draft-scheduled/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "id": 123, + "status": "scheduled", + "html": "content test", + "num_tweets": 1, + "last_edited": null, + "scheduled_date": "2025-01-09T10:22:11Z", + "published_on": null, + "share_url": "https://typefully.com/t/nb9Lxx13df", + "twitter_url": null, + "linkedin_url": null, + "text_first_tweet": "content", + "html_first_tweet": "content", + "text_preview_linkedin": null +} \ No newline at end of file diff --git a/components/typefully/typefully.app.mjs b/components/typefully/typefully.app.mjs new file mode 100644 index 0000000000000..42f7acff460b9 --- /dev/null +++ b/components/typefully/typefully.app.mjs @@ -0,0 +1,112 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "typefully", + propDefinitions: { + kind: { + type: "string", + label: "Kind", + description: "Filter notifications by kind", + async options() { + return [ + { + label: "Inbox", + value: "inbox", + }, + { + label: "Activity", + value: "activity", + }, + ]; + }, + }, + contentFilter: { + type: "string", + label: "Content Filter", + description: "Filter drafts by content type", + async options() { + return [ + { + label: "Threads", + value: "threads", + }, + { + label: "Tweets", + value: "tweets", + }, + ]; + }, + }, + content: { + type: "string", + label: "Content", + description: "You can split into multiple tweets by adding 4 consecutive newlines between tweets in the content", + }, + threadify: { + type: "boolean", + label: "Threadify", + description: "Content will be automatically split into multiple tweets", + }, + share: { + type: "boolean", + label: "Share", + description: "If true, returned payload will include a share_url", + }, + autoRetweetEnabled: { + type: "boolean", + label: "Auto Retweet Enabled", + description: "If true, the post will have an AutoRT enabled, according to the one set on Typefully for the account", + }, + autoPlugEnabled: { + type: "boolean", + label: "Auto Plug Enabled", + description: "If true, the post will have an AutoPlug enabled, according to the one set on Typefully for the account", + }, + scheduleDate: { + type: "string", + label: "Schedule Date", + description: "Date to schedule the draft (ISO format - YYYY-MM-DDTHH:MM:SSZ)", + }, + }, + methods: { + _baseUrl() { + return "https://api.typefully.com/v1"; + }, + _headers() { + return { + "x-api-key": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createDraft(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/drafts/", + ...opts, + }); + }, + getRecentlyScheduledDrafts(opts = {}) { + return this._makeRequest({ + method: "GET", + path: "/drafts/recently-scheduled/", + ...opts, + }); + }, + getRecentlyPublishedDrafts(opts = {}) { + return this._makeRequest({ + method: "GET", + path: "/drafts/recently-published/", + ...opts, + }); + }, + }, +}; diff --git a/components/typless/README.md b/components/typless/README.md index 5f4d9134c481a..112ac2982c3db 100644 --- a/components/typless/README.md +++ b/components/typless/README.md @@ -1,17 +1,11 @@ # Overview -With the Typless API, you can build a variety of applications that make -managing content easier. The Typless API provides a built-in library of -pre-formatted templates that enable you to quickly create types of content such -as documents, images, videos, and more. You can use the Typless API to automate -content processes, manage customer interactions, and create integrated -experiences. Here are just a few examples of what you can do with the Typless -API: +The Typless API offers a streamlined solution for extracting data from documents using machine learning. With its capability, you can automate the process of pulling out specific information from various document types, such as invoices, receipts, or identity documents, turning unstructured data into structured and actionable data points. -- Create beautiful documents with pre-formatted templates -- Automate content creation process -- Manage customer interactions through forms, surveys, and other channels -- Create data-driven tools, such as searchable databases -- Design interactive dashboards for tracking performance -- Integrate data from multiple sources into an unified view -- Build interactive games and experiences +# Example Use Cases + +- **Automated Invoice Processing**: Streamline your accounts payable by setting up a workflow that triggers when an invoice is received via email. Pipedream can capture the email, leverage the Typless API to extract invoice data, and then populate this data into an accounting software like QuickBooks, effectively cutting down manual data entry. + +- **Expense Management Automation**: Build a system where employees forward their receipts to a dedicated email address. Pipedream processes the incoming email, the Typless API extracts purchase details from the receipts, and then the data is inserted into an expense tracking tool like Expensify or a custom database, simplifying expense reporting. + +- **Identity Verification Workflow**: Enhance your user onboarding process with an identity check. When a user uploads an ID document, Pipedream can catch the upload event, use Typless API to extract the user's details, and then feed this information into a background check service or your internal user management system to confirm identity, ensuring compliance and security. diff --git a/components/u301/README.md b/components/u301/README.md new file mode 100644 index 0000000000000..4020ecaa22c49 --- /dev/null +++ b/components/u301/README.md @@ -0,0 +1,11 @@ +# Overview + +The U301 API provides a robust platform for URL management, including features like URL shortening, analytics, and secure redirection. This API is extremely useful for businesses or developers who need to manage and analyze web traffic through compact, trackable URLs. Using Pipedream, you can integrate the U301 API into automated workflows that enhance marketing efforts, streamline link management, and provide detailed insights into click-through behaviors. + +# Example Use Cases + +- **Marketing Campaign Tracking**: Automate the creation of shortened URLs for different marketing channels using the U301 API. Combine it with the Google Sheets app on Pipedream to log URL and associated campaign data in real-time. This helps in tracking the performance and engagement of each channel effectively. + +- **Scheduled Link Updates**: Use the U301 API to manage and update shortened URLs dynamically based on scheduled events or triggers. This can be linked with a CRM platform like HubSpot (available on Pipedream) to update marketing links in emails or landing pages based on customer interactions or campaign timelines. + +- **Real-Time Alerting for URL Metrics**: Set up a workflow that monitors analytics from the U301 API for specific URLs and triggers alerts via Slack (integrated with Pipedream) when certain thresholds are reached (e.g., number of clicks). This is ideal for real-time marketing adjustments and to gauge the impact of specific promotional activities. diff --git a/components/u301/actions/create-qrcode/create-qrcode.mjs b/components/u301/actions/create-qrcode/create-qrcode.mjs new file mode 100644 index 0000000000000..0f77a880930ab --- /dev/null +++ b/components/u301/actions/create-qrcode/create-qrcode.mjs @@ -0,0 +1,43 @@ +import app from "../../u301.app.mjs"; + +export default { + key: "u301-create-qrcode", + name: "Create QR Code", + description: "Create a QR Code from the shortened URL. [See the documentation](https://docs.u301.com/api-reference/endpoint/create-a-qrcode)", + version: "0.0.1", + type: "action", + props: { + app, + url: { + propDefinition: [ + app, + "url", + ], + }, + width: { + propDefinition: [ + app, + "width", + ], + }, + margin: { + propDefinition: [ + app, + "margin", + ], + }, + }, + async run({ $ }) { + const { + app, ...params + } = this; + const response = await app.createQrCode({ + $, + params, + }); + + $.export("$summary", `Successfully created the QR Code for the url: ${this.url}`); + + return response; + }, +}; diff --git a/components/u301/actions/list-domains/list-domains.mjs b/components/u301/actions/list-domains/list-domains.mjs new file mode 100644 index 0000000000000..eb4647924f156 --- /dev/null +++ b/components/u301/actions/list-domains/list-domains.mjs @@ -0,0 +1,37 @@ +import app from "../../u301.app.mjs"; + +export default { + key: "u301-list-domains", + name: "List Domains", + description: "List all available domains for URL shortening. [See the documentation](https://docs.u301.com/api-reference/endpoint/list-shorten-domains)", + version: "0.0.1", + type: "action", + props: { + app, + perPage: { + propDefinition: [ + app, + "perPage", + ], + }, + page: { + propDefinition: [ + app, + "page", + ], + }, + }, + async run({ $ }) { + const response = await this.app.listDomains({ + $, + params: { + perPage: this.perPage, + page: this.page, + }, + }); + + $.export("$summary", `Successfully retrieved ${response.data.length} domains`); + + return response; + }, +}; diff --git a/components/u301/actions/shorten-link/shorten-link.mjs b/components/u301/actions/shorten-link/shorten-link.mjs new file mode 100644 index 0000000000000..e26d0e0cba838 --- /dev/null +++ b/components/u301/actions/shorten-link/shorten-link.mjs @@ -0,0 +1,37 @@ +import app from "../../u301.app.mjs"; + +export default { + key: "u301-shorten-link", + name: "Shorten URL", + description: "Shorten a long link. [See the documentation](https://docs.u301.com/api-reference/endpoint/create)", + version: "0.0.1", + type: "action", + props: { + app, + url: { + propDefinition: [ + app, + "url", + ], + }, + title: { + propDefinition: [ + app, + "title", + ], + }, + }, + async run({ $ }) { + const response = await this.app.shortenLink({ + $, + params: { + url: this.url, + title: this.title, + }, + }); + + $.export("$summary", `Successfully shortened the URL: ${response.shortened}`); + + return response; + }, +}; diff --git a/components/u301/package.json b/components/u301/package.json new file mode 100644 index 0000000000000..ca28da78efc04 --- /dev/null +++ b/components/u301/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/u301", + "version": "0.1.0", + "description": "Pipedream U301 Components", + "main": "u301.app.mjs", + "keywords": [ + "pipedream", + "u301" + ], + "homepage": "https://pipedream.com/apps/u301", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/u301/u301.app.mjs b/components/u301/u301.app.mjs new file mode 100644 index 0000000000000..5229db36ac9bc --- /dev/null +++ b/components/u301/u301.app.mjs @@ -0,0 +1,79 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "u301", + propDefinitions: { + url: { + type: "string", + label: "URL", + description: "The URL to be shortened", + }, + title: { + type: "string", + label: "Title", + description: "The title of the URL", + }, + perPage: { + type: "integer", + label: "perPage", + description: "Results per page", + }, + page: { + type: "integer", + label: "Page", + description: "Page of the list", + }, + width: { + type: "integer", + label: "Width", + description: "QR code width", + optional: true, + }, + margin: { + type: "integer", + label: "Margin", + description: "QR code padding", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.u301.com/v2"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + async shortenLink(args = {}) { + return this._makeRequest({ + path: "/shorten", + ...args, + }); + }, + async listDomains(args = {}) { + return this._makeRequest({ + path: "/shorten-domains", + ...args, + }); + }, + async createQrCode(args = {}) { + return this._makeRequest({ + path: "/qrcode", + ...args, + }); + }, + }, +}; diff --git a/components/uber_direct/package.json b/components/uber_direct/package.json new file mode 100644 index 0000000000000..97a4285aeb5b5 --- /dev/null +++ b/components/uber_direct/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/uber_direct", + "version": "0.0.1", + "description": "Pipedream Uber Direct Components", + "main": "uber_direct.app.mjs", + "keywords": [ + "pipedream", + "uber_direct" + ], + "homepage": "https://pipedream.com/apps/uber_direct", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/uber_direct/uber_direct.app.mjs b/components/uber_direct/uber_direct.app.mjs new file mode 100644 index 0000000000000..c7734c0140eba --- /dev/null +++ b/components/uber_direct/uber_direct.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "uber_direct", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/uberduck/README.md b/components/uberduck/README.md new file mode 100644 index 0000000000000..3f8a974da3a07 --- /dev/null +++ b/components/uberduck/README.md @@ -0,0 +1,11 @@ +# Overview + +The Uberduck API offers text-to-speech capabilities with a variety of voice choices, from standard voices to celebrity impersonations. It enables you to convert text into lifelike speech, providing an API that can be used for creating audio content, voiceovers for videos, or for making interactive voice response systems more engaging. On Pipedream, you can build workflows that leverage this functionality, triggering voice synthesis with events from various sources and integrating with other apps for a seamless automation experience. + +# Example Use Cases + +- **Automatic Podcast Generation**: Create a workflow where you convert blog posts or written content into audio format using the Uberduck API. Trigger the process with new RSS feed items, use Uberduck for conversion, and then upload the resulting audio to a podcast hosting platform like Anchor. + +- **Dynamic Voice Notifications**: Build a system that generates voice notifications for regular updates or alerts. For instance, use Uberduck to turn Slack messages or Trello card updates into speech, which can then be sent as a voice file attachment back into Slack or to another messaging service. + +- **Interactive Voice Response (IVR) Enhancements**: Improve your IVR system by using the Uberduck API to generate dynamic and engaging responses based on user input. Integrate with Twilio to handle incoming calls, use conditional logic to process the request, and employ Uberduck to create a custom-tailored audio response. diff --git a/components/uchat/README.md b/components/uchat/README.md new file mode 100644 index 0000000000000..8cb5c846de05c --- /dev/null +++ b/components/uchat/README.md @@ -0,0 +1,11 @@ +# Overview + +The Uchat API allows for the automation and integration of chat services and customer interactions within the Uchat platform. By leveraging this API on Pipedream, you can create powerful workflows to enhance customer engagement, streamline communication processes, and analyze chat data for insights. Pipedream's serverless architecture simplifies connecting Uchat to various other apps and services, enabling you to build custom, scalable, and automated solutions. + +# Example Use Cases + +- **Customer Support Automation**: Trigger a Pipedream workflow whenever a new message is received on Uchat. Use this to automatically respond with predefined answers for common questions, or escalate complex issues by creating a ticket in a connected service like Zendesk. + +- **Chat Data Analysis**: Collect and store chat messages in a Pipedream data store. Periodically process and analyze this data to gain insights into common customer inquiries, sentiment, or trending topics. Integrate with Google Sheets or Data Studio for advanced reporting and visualization. + +- **Real-time Notifications**: Set up a workflow that monitors for specific keywords or phrases in Uchat conversations. When a match is found, send real-time alerts to a Slack channel or via email to quickly inform your team about important customer feedback or urgent issues. diff --git a/components/uipath/README.md b/components/uipath/README.md index d795a53125b31..5176d98e0f1a0 100644 --- a/components/uipath/README.md +++ b/components/uipath/README.md @@ -1,22 +1,11 @@ # Overview -Using the UiPath API, you can build automated and robotic process automation -(RPA) solutions that improve business operations. UiPath's easy-to-use, -powerful, and open tools allow developers to quickly and easily create -automated tasks and processes that are essential to many companies. +UiPath is a leader in Robotic Process Automation (RPA), enabling businesses to automate routine tasks with software robots. Their API provides the ability to manage these robots, processes, and queues programmatically. With UiPath's API on Pipedream, you can create sophisticated automation workflows that trigger actions in UiPath as part of broader, cross-application processes. This could involve initiating UiPath jobs based on incoming data from different sources, managing resources, or reacting to events from other applications or services to create a seamless automation ecosystem. -Some examples of what you can build using the UiPath API include: +# Example Use Cases -- Robotic process automation solutions that increase efficiency, reduce costs, - and eliminate errors in data processing activities. -- Automated tasks and processes to help automate business processes such as - accounts payable and receivable, payroll, and customer service. -- Automated systems to help with data entry, file organization, data cleansing, - and other data-related tasks. -- Automated business applications such as document management, CRM, and ERP - systems. -- Automated workflows to help manage approvals, business processes, and - workflows. -- Automated tools to help with debugging, testing, and deployment of - applications. -- Automated tools to help with monitoring and analyzing business data. +- **Invoice Processing Automation**: Automatically kick off UiPath processes for new invoices received via email. When a new email attachment is detected by an email service like Gmail, Pipedream can trigger a UiPath job to extract data from the invoice for further processing and entry into an ERP system like SAP. + +- **Scheduled Report Generation**: Use Pipedream to schedule and trigger UiPath bots to generate and distribute reports. Connect Pipedream to a time-based trigger to initiate UiPath workflows at specific intervals, pulling data from databases or CRM systems such as Salesforce, processing it, and then emailing the compiled reports to relevant stakeholders. + +- **Real-time Slack Notifications for UiPath Job Completion**: Set up a workflow on Pipedream where UiPath sends a web hook to signify the completion of a job. Pipedream can then parse this information and send a notification to a Slack channel, keeping the team updated on the status of robotic processes without manual monitoring. diff --git a/components/uipath/package.json b/components/uipath/package.json new file mode 100644 index 0000000000000..73258364d6a18 --- /dev/null +++ b/components/uipath/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/uipath", + "version": "0.6.0", + "description": "Pipedream uipath Components", + "main": "uipath.app.mjs", + "keywords": [ + "pipedream", + "uipath" + ], + "homepage": "https://pipedream.com/apps/uipath", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/uipath_automation_hub/README.md b/components/uipath_automation_hub/README.md new file mode 100644 index 0000000000000..c8703d23410ba --- /dev/null +++ b/components/uipath_automation_hub/README.md @@ -0,0 +1,11 @@ +# Overview + +The UiPath Automation Hub API allows you to interact with the automation lifecycle management platform. With this API, you can create, manage, and deploy automation tasks using UiPath's comprehensive toolset. On Pipedream, leverage this API to kick off robotic process automation (RPA) workflows, synchronize automation initiatives with your project management tools, or even gather valuable insights on your automations' performance and impact. + +# Example Use Cases + +- **Schedule RPA Tasks Based on External Triggers**: Integrate UiPath Automation Hub with cron triggers on Pipedream to schedule and initiate RPA workflows. Automate repetitive tasks like data entry, report generation, or email processing by triggering bots based on time or predefined conditions. + +- **Sync Automation Initiatives with Project Management Tools**: Connect UiPath Automation Hub to project management apps like Trello or Jira on Pipedream. Whenever a new RPA opportunity is identified within the Automation Hub, create corresponding tasks or stories in your project management tool to track the automation's development lifecycle. + +- **Monitor Automation Performance**: Combine UiPath Automation Hub with data visualization tools like Google Sheets or Tableau on Pipedream. Extract performance data and metrics from UiPath to create dashboards that display your automations' efficiency, success rates, and business impact, helping you make informed decisions. diff --git a/components/uk_gov_vehecle_enquiry_api/README.md b/components/uk_gov_vehecle_enquiry_api/README.md index 01f5f1bf0c4a7..80f9d79a8041c 100644 --- a/components/uk_gov_vehecle_enquiry_api/README.md +++ b/components/uk_gov_vehecle_enquiry_api/README.md @@ -1,23 +1,11 @@ # Overview -The UK gov Vehicle Enquiry API provides a range of data relating to vehicles -registered in the UK such as MOT, tax and driver & vehicle licence information. -With this API, developers can create tools and applications that allow the user -to search, store and retrieve vehicle-related data. +The UK gov Vehicle Enquiry API provides access to key vehicle data, allowing users to retrieve information about a vehicle's make, model, color, fuel type, CO2 emissions, and more by inputting its registration number. Leveraging this API within Pipedream opens up a realm of possibilities for automating tasks related to vehicle management, compliance checks, and data aggregation for businesses in the automotive industry or for public sector uses. -Using this comprehensive API, developers can build all sorts of powerful -applications and services which can be used to simplify the process of -determining the ownership of a vehicle, identify vehicle tax and MOT history, -check vehicle compatibility for renting or leasing, and validate customer -payment details, to name a few. +# Example Use Cases -Here are some of the many things developers can build with the UK gov Vehicle -Enquiry API: +- **Automated Vehicle Compliance Monitoring**: Trigger a workflow that periodically checks a fleet of vehicles for tax and MOT compliance by querying the Vehicle Enquiry API. If a vehicle is found non-compliant, the workflow can automatically notify the fleet manager or initiate a renewal process by connecting to a DMV scheduling app. -- Automated MOT and Tax Reminder Services -- Vehicle Tracking Systems -- Vehicle Rental and Leasing Platforms -- Vehicle Theft Detection Systems -- Vehicle Sale and Valuation Services -- Residual Value Calculators -- License Plate Recognition Systems +- **Second-hand Car Dealership Integration**: Upon receiving a new inventory list, trigger a Pipedream workflow that verifies each vehicle’s details with the Vehicle Enquiry API. The workflow can enrich a dealership’s database with accurate car data and flag potential discrepancies or issues before listing the vehicles for sale. + +- **Environmental Impact Analysis**: Use the API to collect data on fuel type and CO2 emissions for a set of vehicles. A Pipedream workflow can aggregate this information, analyze the environmental impact, and send a summarized report to stakeholders or integrate with a dashboard app for real-time monitoring. diff --git a/components/ultramsg/README.md b/components/ultramsg/README.md index d7ceea634560e..fde849467e91a 100644 --- a/components/ultramsg/README.md +++ b/components/ultramsg/README.md @@ -1,30 +1,11 @@ # Overview -Using UltraMsg API, you can build powerful messaging applications quickly and -easily. This API allows developers to quickly implement communication solutions -such as chat, messaging, voice & video calls, presence, notifications, and data -publishing/subscribing. With the UltraMsg API, it's easier than ever to add -robust features to your app. +UltraMsg API offers a suite of messaging capabilities that let you automate WhatsApp messaging. With it, you can send and receive messages, create WhatsApp chatbots, and manage media, enabling a wide range of automated interactions with users. Pipedream acts as a powerful accelerator here, letting you connect UltraMsg to hundreds of other apps to craft custom workflows. -These are some of the features you can achieve with UltraMsg API: +# Example Use Cases -- Secure messaging, voice & video calls and group management -- Presence, group management, and notifications -- Easy data publishing and subscribing -- Support for custom messages and rich message formats -- Message delivery and delivery notifications -- Reliable, secure and efficient delivery of messages -- Device-agnostic messaging including web, mobile, and desktop -- Easy integration with existing services -- Robust user and identity management +- **Customer Support Ticketing System Integration**: When a new WhatsApp message arrives via UltraMsg, trigger a Pipedream workflow that creates a new support ticket in a tool like Zendesk or Jira. Use data from the incoming message to populate the ticket details, ensuring customer queries are promptly and systematically addressed. -Below are some example applications that you can build using the UltraMsg API: +- **E-Commerce Order Updates**: Integrate UltraMsg with an e-commerce platform such as Shopify. Set up a Pipedream workflow that listens for order status changes, and automatically sends updates to customers on WhatsApp about their order progress, from confirmation to shipping and delivery. -- Chat applications -- Video conferencing and collaboration -- Mobile messaging and notifications -- Gaming platforms and chat lobbies -- IoT and sensor data streaming -- Enterprise notification systems -- Inter-application communication -- Real-time analytics and data collection +- **Event Reminder System**: Connect UltraMsg to Google Calendar via Pipedream. Whenever a new event is added to a specific calendar, trigger a workflow that sends out reminder messages to a list of participants on WhatsApp, ensuring timely attendance and engagement. diff --git a/components/unbounce/README.md b/components/unbounce/README.md new file mode 100644 index 0000000000000..006417f2c6570 --- /dev/null +++ b/components/unbounce/README.md @@ -0,0 +1,11 @@ +# Overview + +The Unbounce API lets you tap into the powerful landing page platform to automate tasks and sync data with other services. Use it within Pipedream workflows to capture, analyze, and act on leads generated through your landing pages. Think of fetching form submissions, updating leads in a CRM, or dynamically modifying landing page content. + +# Example Use Cases + +- **Sync Submissions to Google Sheets**: Automatically add new Unbounce form submissions to a Google Sheet for easy tracking and analysis. This workflow can help you keep a running log of leads without manual data entry. + +- **Trigger Email Campaigns with Mailchimp**: Once a visitor submits a form on your Unbounce page, trigger an email campaign in Mailchimp. This creates a seamless follow-up process to engage leads immediately. + +- **Update CRM Contacts in Salesforce**: When a new lead is captured via Unbounce, create or update a contact in Salesforce. This ensures your sales team has the latest information without manual updates. diff --git a/components/undetectable_ai/actions/list-documents/list-documents.mjs b/components/undetectable_ai/actions/list-documents/list-documents.mjs new file mode 100644 index 0000000000000..282097682a2ad --- /dev/null +++ b/components/undetectable_ai/actions/list-documents/list-documents.mjs @@ -0,0 +1,31 @@ +import app from "../../undetectable_ai.app.mjs"; + +export default { + key: "undetectable_ai-list-documents", + name: "List Documents", + description: "Retrieve the IDs of documents associated with your account. [See the documentation](https://docs.undetectable.ai/#list-documents)", + version: "0.0.1", + type: "action", + props: { + app, + page: { + propDefinition: [ + app, + "page", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.app.listDocuments({ + $, + data: { + page: this.page, + }, + }); + + $.export("$summary", `Successfully retrieved '${response.documents.length}' documents`); + + return response; + }, +}; diff --git a/components/undetectable_ai/actions/retrieve-document/retrieve-document.mjs b/components/undetectable_ai/actions/retrieve-document/retrieve-document.mjs new file mode 100644 index 0000000000000..598122b2e5d0e --- /dev/null +++ b/components/undetectable_ai/actions/retrieve-document/retrieve-document.mjs @@ -0,0 +1,30 @@ +import app from "../../undetectable_ai.app.mjs"; + +export default { + key: "undetectable_ai-retrieve-document", + name: "Retrieve Document", + description: "Retrieve the document object for a submitted piece of content. [See the documentation](https://docs.undetectable.ai/#retrieve-document)", + version: "0.0.1", + type: "action", + props: { + app, + id: { + propDefinition: [ + app, + "id", + ], + }, + }, + async run({ $ }) { + const response = await this.app.retrieveDocument({ + $, + data: { + id: this.id, + }, + }); + + $.export("$summary", `Successfully retrieved document with ID '${response.id}'`); + + return response; + }, +}; diff --git a/components/undetectable_ai/actions/submit-document/submit-document.mjs b/components/undetectable_ai/actions/submit-document/submit-document.mjs new file mode 100644 index 0000000000000..8b0910084df10 --- /dev/null +++ b/components/undetectable_ai/actions/submit-document/submit-document.mjs @@ -0,0 +1,51 @@ +import app from "../../undetectable_ai.app.mjs"; + +export default { + key: "undetectable_ai-submit-document", + name: "Submit Document", + description: "Submit document for humanization. [See the documentation](https://docs.undetectable.ai/#submit-document)", + version: "0.0.1", + type: "action", + props: { + app, + content: { + propDefinition: [ + app, + "content", + ], + }, + readability: { + propDefinition: [ + app, + "readability", + ], + }, + purpose: { + propDefinition: [ + app, + "purpose", + ], + }, + strength: { + propDefinition: [ + app, + "strength", + ], + }, + }, + async run({ $ }) { + const response = await this.app.submitDocument({ + $, + data: { + content: this.content, + readability: this.readability, + purpose: this.purpose, + strength: this.strength, + }, + }); + + $.export("$summary", `Successfully submited document for humanization. ID: '${response.id}'`); + + return response; + }, +}; diff --git a/components/undetectable_ai/common/constants.mjs b/components/undetectable_ai/common/constants.mjs new file mode 100644 index 0000000000000..848acf6791d4c --- /dev/null +++ b/components/undetectable_ai/common/constants.mjs @@ -0,0 +1,25 @@ +export default { + PURPOSE_OPTIONS: [ + "General Writing", + "Essay", + "Article", + "Marketing Material", + "Story", + "Cover Letter", + "Report", + "Business Material", + "Legal Material", + ], + READABILITY_OPTIONS: [ + "High School", + "University", + "Doctorate", + "Journalist", + "Marketing", + ], + STRENGTH_OPTIONS: [ + "Quality", + "Balanced", + "More Human", + ], +}; diff --git a/components/undetectable_ai/package.json b/components/undetectable_ai/package.json new file mode 100644 index 0000000000000..92c74f6b7ef3f --- /dev/null +++ b/components/undetectable_ai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/undetectable_ai", + "version": "0.1.0", + "description": "Pipedream Undetectable AI Components", + "main": "undetectable_ai.app.mjs", + "keywords": [ + "pipedream", + "undetectable_ai" + ], + "homepage": "https://pipedream.com/apps/undetectable_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/undetectable_ai/undetectable_ai.app.mjs b/components/undetectable_ai/undetectable_ai.app.mjs new file mode 100644 index 0000000000000..a4404e49cb28f --- /dev/null +++ b/components/undetectable_ai/undetectable_ai.app.mjs @@ -0,0 +1,85 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "undetectable_ai", + propDefinitions: { + content: { + type: "string", + label: "Content", + description: "The document you want to humanize. Minimum 50 characters, maximum 15,000 characters", + }, + readability: { + type: "string", + label: "Readability", + description: "Readability of the humanized document", + options: constants.READABILITY_OPTIONS, + }, + purpose: { + type: "string", + label: "Purpose", + description: "Purpose of the humanized document", + options: constants.PURPOSE_OPTIONS, + }, + strength: { + type: "string", + label: "Strength", + description: "Increases aggressiveness of humanization algorithms", + optional: true, + options: constants.STRENGTH_OPTIONS, + }, + id: { + type: "string", + label: "Document ID", + description: "The ID of the document object submitted for humanization", + }, + page: { + type: "string", + label: "Page", + description: "The page number for pagination. Starts at 0. Each page contains upto 250 documents", + }, + }, + methods: { + _baseUrl() { + return "https://api.undetectable.ai"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "api-key": `${this.$auth.api_key}`, + }, + }); + }, + async submitDocument(args = {}) { + return this._makeRequest({ + method: "post", + path: "/submit", + ...args, + }); + }, + async retrieveDocument(args = {}) { + return this._makeRequest({ + method: "post", + path: "/document", + ...args, + }); + }, + async listDocuments(args = {}) { + return this._makeRequest({ + method: "post", + path: "/list", + ...args, + }); + }, + }, +}; diff --git a/components/unione/README.md b/components/unione/README.md new file mode 100644 index 0000000000000..607027d9ae9e7 --- /dev/null +++ b/components/unione/README.md @@ -0,0 +1,11 @@ +# Overview + +UniOne is an email service provider that offers a broad range of features for sending and managing email campaigns. Through its API, you can programmatically send transactional emails, organize mailing lists, track email delivery statuses, and analyze recipient engagements. When integrated on Pipedream, UniOne becomes a part of your serverless workflow, enabling you to automate email operations with various triggers and actions from other apps. + +# Example Use Cases + +- **Transactional Email Automation**: Send transactional emails using UniOne API in response to events from your app. For example, you can trigger an email when a new user signs up on your website, automatically sending them a welcome message and login details. + +- **Mailing List Management**: Sync a CRM platform like HubSpot with UniOne to manage contacts and mailing lists. When a new contact is added to a HubSpot list, you could use a Pipedream workflow to add or update the contact in a UniOne mailing list. + +- **Email Performance Analysis**: Collect metrics on email campaigns from UniOne and send the data to a dashboard tool like Google Sheets or Data Studio. With Pipedream's workflow, you could automate the extraction of open rates, click rates, and bounce statistics, giving you insights into your email performance over time. diff --git a/components/unione/package.json b/components/unione/package.json index fd265a13e306c..3d12360a2672c 100644 --- a/components/unione/package.json +++ b/components/unione/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/unisender/README.md b/components/unisender/README.md index 9eeb7e180fe45..3930da46a8820 100644 --- a/components/unisender/README.md +++ b/components/unisender/README.md @@ -1,30 +1,11 @@ # Overview -Unisender offers a powerful API with a wide range of useful features and -possibilities. It enables developers to create powerful applications that take -advantage of the Unisender platform. With the Unisender API, you can easily -develop applications to interact with Unisender and perform tasks such as: +UniSender is a platform that empowers users to execute email and SMS marketing campaigns effectively. With the UniSender API on Pipedream, you can automate the process of managing contacts, designing emails, initiating campaigns, and analyzing the results. Implementing workflows that respond to subscriber actions, synchronize customer data across platforms, or dynamically update marketing lists becomes straightforward, enhancing the efficiency and personalization of your marketing efforts. -- Sending emails and newsletters -- Managing contacts (subscribers) -- Checking emails for unsubscribe requests -- Subscribing new contacts to lists -- Managing segmented lists -- Checking messages for clicks, opens, bounces -- Accessing statistics on campaigns +# Example Use Cases -These are just some of the possibilities the Unisender API gives you. With it, -you can build powerful applications with features suited to your specific needs -and business requirements. Below are some examples of applications that can be -built with Unisender API: +- **Automated Lead Follow-Up**: After a lead is captured through a form on your website, trigger a workflow that automatically adds the contact to UniSender, sends a personalized welcome email, and follows up with a series of tailored SMS messages to keep your brand top-of-mind. -- Email auto responder applications -- Opt-in marketing tools -- Subscriber segmentation tools -- Automated email list segmentation applications -- Campaign management tools -- Newsletter creation software -- Real-time message tracking applications -- Advanced email analytics tools -- Automated email marketing campaigns -- Lead generation tools +- **Dynamic List Segmentation**: Connect UniSender with your CRM (like Salesforce) to automate the segmentation process. When a contact's status updates in your CRM, a Pipedream workflow adjusts their corresponding UniSender mailing list membership to ensure they receive the most relevant content. + +- **Campaign Performance Dashboard**: After sending out an email campaign via UniSender, trigger a workflow that fetches campaign analytics and pushes this data to a Google Sheets spreadsheet. This can be used for real-time monitoring of campaign performance metrics, helping to make data-driven decisions for future campaigns. diff --git a/components/unity_cloud_build/README.md b/components/unity_cloud_build/README.md index 6e34aef64f04d..08664e2a75089 100644 --- a/components/unity_cloud_build/README.md +++ b/components/unity_cloud_build/README.md @@ -1,29 +1,11 @@ # Overview -The Unity Cloud Build API provides a powerful way to automate your builds and -take advantage of incremental cloud builds on multiple platforms. With this -API, developers can create a cloud-based build pipeline, enabling them to -create builds on the fly and maintain a consistently high-quality product. +The Unity Cloud Build API lets you automate the process of setting up and distributing game builds. It's a powerful suite of tools allowing developers to personalize and streamline build tasks. With Pipedream, you can harness this API to integrate build processes with your existing tools, notify teams, deploy builds to platforms, and react to events in Unity Cloud Build in real-time. -The Unity Cloud Build API is an ideal tool for streamlining the build process -and taking full advantage of the cloud. With the API, developers can: +# Example Use Cases -- Automate build processes and speed up time to deploy -- Generate multiple builds for different platforms, including Linux, Android, - iOS, WebGL, and UWP -- Track build progress and performance in real-time -- Create customized scripts for automated build tasks -- Access and manage build artifacts -- Create custom rules for automated builds +- **Automated Build Notifications via Slack**: Create a workflow on Pipedream that triggers when a new build is available in Unity Cloud Build. It can then send a message to a Slack channel to inform the team. This aids in keeping everyone up-to-date on the latest build status without manual checks. -Examples of products that can be built using the Unity Cloud Build API include: +- **GitHub Integration for Build Triggers**: Set up a Pipedream workflow that listens for GitHub push or pull request events. Upon detection, it can trigger a new build in Unity Cloud Build, effectively creating a continuous integration pipeline that ensures your game is always ready for the latest changes in your codebase. -- 3D games for mobile, console, web, and desktop -- Educational simulations -- Multi-player battle games -- Virtual reality applications -- Augmented reality experiences -- Visual visualizations and interactive data visualizations -- Interactive presentations and training materials -- Online marketplaces -- Location-based experiences +- **Discord Notifications for Build Failures**: Configure Pipedream to monitor Unity Cloud Build for failed build events. When a build fails, automatically send a detailed report to a specified Discord server to prompt immediate action from the development team. This workflow helps in quickly addressing and resolving build issues. diff --git a/components/unity_cloud_build/package.json b/components/unity_cloud_build/package.json new file mode 100644 index 0000000000000..41daead5219e7 --- /dev/null +++ b/components/unity_cloud_build/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/unity_cloud_build", + "version": "0.6.0", + "description": "Pipedream unity_cloud_build Components", + "main": "unity_cloud_build.app.mjs", + "keywords": [ + "pipedream", + "unity_cloud_build" + ], + "homepage": "https://pipedream.com/apps/unity_cloud_build", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/universal_summarizer_by_kagi/package.json b/components/universal_summarizer_by_kagi/package.json new file mode 100644 index 0000000000000..3c7c8e1a77085 --- /dev/null +++ b/components/universal_summarizer_by_kagi/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/universal_summarizer_by_kagi", + "version": "0.0.1", + "description": "Pipedream Universal Summarizer by Kagi Components", + "main": "universal_summarizer_by_kagi.app.mjs", + "keywords": [ + "pipedream", + "universal_summarizer_by_kagi" + ], + "homepage": "https://pipedream.com/apps/universal_summarizer_by_kagi", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/universal_summarizer_by_kagi/universal_summarizer_by_kagi.app.mjs b/components/universal_summarizer_by_kagi/universal_summarizer_by_kagi.app.mjs new file mode 100644 index 0000000000000..9c79fcd3d71f8 --- /dev/null +++ b/components/universal_summarizer_by_kagi/universal_summarizer_by_kagi.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "universal_summarizer_by_kagi", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/unsplash/README.md b/components/unsplash/README.md index 0884b4810bcdf..0072df42d9d39 100644 --- a/components/unsplash/README.md +++ b/components/unsplash/README.md @@ -1,25 +1,11 @@ # Overview -You can do a lot of amazing things with the Unsplash API! Whether you're -looking to build a photo-editing app, a wallpaper app, or a photo-sharing -website, you can use the Unsplash API to get access to over a million free, -high resolution photos. Here are some of the amazing things you can build with -the Unsplash API: +The Unsplash API provides programmatic access to a vast library of high-quality, royalty-free images. This enables developers to search for and retrieve photos based on keywords, collections, or featured content. With Pipedream's integration, you can automate tasks like updating social media banners, populating website content with dynamic images, or even analyzing photo metadata for insights. By leveraging Unsplash's API within Pipedream, you can create workflows that trigger on various events, process image data, and interact with countless other apps available on the platform, streamlining your digital asset management and content creation processes. -- Photo-editing apps: You can use the Unsplash API to access, search for, and - download high resolution photos so your users can use them as a starting - point for their own creations. -- Wallpaper apps: Why deal with copyright issues? With the Unsplash API you can - easily obtain millions of photos to use as wallpaper in your app. -- Photo-sharing websites: Why spend countless hours uploading and curating - photos? With the Unsplash API you can access, search for, and download high - resolution photos directly onto your website. -- Image recognition: With the Unsplash API, you can leverage the millions of - photos available to develop AI-powered image recognition and object detection - software. -- Advertisement websites: With the Unsplash API, you can easily integrate ads - into your website by providing high resolution photos and links to the - products in the photos. -- Image search engines: Thanks to the variety of photos available, you can - create a powerful image search engine to help your users find what they're - looking for quickly. +# Example Use Cases + +- **Social Media Content Update:** Automate the updating of your social media profiles with fresh, high-resolution images by setting a Pipedream workflow that periodically queries the Unsplash API for new photos based on specific search criteria, and then posts those images to platforms like Twitter, Facebook, or Instagram using their respective APIs. + +- **Dynamic Website Imagery:** Enhance your website's visual appeal by creating a Pipedream workflow that pulls a curated set of images from Unsplash based on tags or collections, then pushes these images to your website's CMS (such as WordPress) to keep the visual content fresh and engaging without manual intervention. + +- **Marketing Asset Management:** Build a Pipedream workflow that listens for new uploads from your company’s designated photographers to Unsplash, downloads the high-quality versions of these images, and then stores them in Google Drive or Dropbox. Integrate with Slack to send notifications to your marketing team so they can easily access and use the latest images in campaigns and collateral. diff --git a/components/unsplash/actions/get-photo/get-photo.mjs b/components/unsplash/actions/get-photo/get-photo.mjs new file mode 100644 index 0000000000000..b414abc44e060 --- /dev/null +++ b/components/unsplash/actions/get-photo/get-photo.mjs @@ -0,0 +1,41 @@ +import app from "../../unsplash.app.mjs"; + +export default { + key: "unsplash-get-photo", + name: "Get Photo", + description: "Get a specific photo from Unsplash. [See the documentation](https://unsplash.com/documentation#get-a-photo)", + version: "0.0.1", + type: "action", + props: { + app, + photoId: { + propDefinition: [ + app, + "photoId", + ], + }, + }, + methods: { + getPhoto({ + photoId, ...args + } = {}) { + return this.app._makeRequest({ + path: `/photos/${photoId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + getPhoto, + photoId, + } = this; + const response = await getPhoto({ + $, + photoId, + }); + + $.export("$summary", `Successfully retrieved photo with ID \`${response.id}\``); + return response; + }, +}; diff --git a/components/unsplash/actions/search-photos/search-photos.mjs b/components/unsplash/actions/search-photos/search-photos.mjs new file mode 100644 index 0000000000000..342586dfd9e3c --- /dev/null +++ b/components/unsplash/actions/search-photos/search-photos.mjs @@ -0,0 +1,74 @@ +import constants from "../../common/constants.mjs"; +import app from "../../unsplash.app.mjs"; + +export default { + key: "unsplash-search-photos", + name: "Search Photos", + description: "Get a single page of photo results for a query. [See the documentation](https://unsplash.com/documentation#search-photos)", + version: "0.0.1", + type: "action", + props: { + app, + query: { + type: "string", + label: "Query", + description: "Search terms.", + }, + contentFilter: { + type: "string", + label: "Content Filter", + description: "Limit results by content safety. Valid values are `low` and `high`.", + optional: true, + options: constants.CONTENT_FILTERS, + }, + color: { + type: "string", + label: "Color", + description: "Filter results by color. Valid values are: `black_and_white`, `black`, `white`, `yellow`, `orange`, `red`, `purple`, `magenta`, `green`, `teal`, and `blue`.", + optional: true, + options: constants.COLOR_OPTIONS, + }, + orientation: { + type: "string", + label: "Orientation", + description: "Filter by photo orientation. Optional. (Valid values: `landscape`, `portrait`, `squarish`)", + optional: true, + options: constants.ORIENTATION_OPTIONS, + }, + }, + methods: { + searchPhotos(args = {}) { + return this.app._makeRequest({ + path: "/search/photos", + ...args, + }); + }, + }, + async run({ $ }) { + const { + app, + searchPhotos, + query, + contentFilter, + color, + orientation, + } = this; + + const photos = await app.paginate({ + resourcesFn: searchPhotos, + resourcesFnArgs: { + $, + params: { + query, + content_filter: contentFilter, + color, + orientation, + }, + }, + resourceName: "results", + }); + + $.export("$summary", `Successfully retrieved \`${photos.length}\` photo(s).`); + return photos; + }, +}; diff --git a/components/unsplash/common/constants.mjs b/components/unsplash/common/constants.mjs new file mode 100644 index 0000000000000..a2bfe84cfba66 --- /dev/null +++ b/components/unsplash/common/constants.mjs @@ -0,0 +1,81 @@ +const BASE_URL = "https://api.unsplash.com"; +const API_VERSION = "v1"; +const DEFAULT_MAX = 100; +const DEFAULT_LIMIT = 100; + +const CONTENT_FILTERS = [ + "low", + "high", +]; + +const COLOR_OPTIONS = [ + { + label: "Black and White", + value: "black_and_white", + }, + { + label: "Black", + value: "black", + }, + { + label: "White", + value: "white", + }, + { + label: "Yellow", + value: "yellow", + }, + { + label: "Orange", + value: "orange", + }, + { + label: "Red", + value: "red", + }, + { + label: "Purple", + value: "purple", + }, + { + label: "Magenta", + value: "magenta", + }, + { + label: "Green", + value: "green", + }, + { + label: "Teal", + value: "teal", + }, + { + label: "Blue", + value: "blue", + }, +]; + +const ORIENTATION_OPTIONS = [ + { + label: "Landscape", + value: "landscape", + }, + { + label: "Portrait", + value: "portrait", + }, + { + label: "Squarish", + value: "squarish", + }, +]; + +export default { + BASE_URL, + API_VERSION, + DEFAULT_LIMIT, + DEFAULT_MAX, + CONTENT_FILTERS, + COLOR_OPTIONS, + ORIENTATION_OPTIONS, +}; diff --git a/components/unsplash/common/utils.mjs b/components/unsplash/common/utils.mjs new file mode 100644 index 0000000000000..67cc42c8d951e --- /dev/null +++ b/components/unsplash/common/utils.mjs @@ -0,0 +1,34 @@ +function parseLinkHeader(linkHeader) { + return linkHeader?.split(",") + .reduce((props, link) => { + const [ + url, + rel, + ] = link.split(";"); + const [ + , value, + ] = url.split("<"); + const [ + , key, + ] = rel.split("="); + const clearKey = key.replace(/"/g, ""); + const clearValue = value.replace(/>/g, ""); + return { + ...props, + [clearKey]: clearValue, + }; + }, {}); +} + +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +export default { + iterate, + parseLinkHeader, +}; diff --git a/components/unsplash/package.json b/components/unsplash/package.json new file mode 100644 index 0000000000000..928813186c861 --- /dev/null +++ b/components/unsplash/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/unsplash", + "version": "0.0.1", + "description": "Pipedream Unsplash Components", + "main": "unsplash.app.mjs", + "keywords": [ + "pipedream", + "unsplash" + ], + "homepage": "https://pipedream.com/apps/unsplash", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} diff --git a/components/unsplash/unsplash.app.mjs b/components/unsplash/unsplash.app.mjs index 0e3ab16e1e945..6209711a84f5f 100644 --- a/components/unsplash/unsplash.app.mjs +++ b/components/unsplash/unsplash.app.mjs @@ -1,11 +1,98 @@ +import { axios } from "@pipedream/platform"; +import utils from "./common/utils.mjs"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "unsplash", - propDefinitions: {}, + propDefinitions: { + photoId: { + type: "string", + label: "Photo ID", + description: "The ID of the photo to retrieve.", + async options({ page }) { + const photos = await this.listPhotos({ + params: { + page: page + 1, + per_page: constants.DEFAULT_LIMIT, + }, + }); + return photos.map(({ + id: value, slug: label, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `${constants.BASE_URL}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Accept-Version": constants.API_VERSION, + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + debug: true, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + listPhotos(args = {}) { + return this._makeRequest({ + path: "/photos", + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, resourceName, + max = constants.DEFAULT_MAX, + }) { + let page = 1; + let resourcesCount = 0; + + while (true) { + const response = + await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + page, + per_page: constants.DEFAULT_LIMIT, + }, + }); + + const nextResources = resourceName && response[resourceName] || response; + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + yield resource; + resourcesCount += 1; + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + page += 1; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); }, }, }; diff --git a/components/unstructured/package.json b/components/unstructured/package.json new file mode 100644 index 0000000000000..f2e582bb082bf --- /dev/null +++ b/components/unstructured/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/unstructured", + "version": "0.0.1", + "description": "Pipedream Unstructured Components", + "main": "unstructured.app.mjs", + "keywords": [ + "pipedream", + "unstructured" + ], + "homepage": "https://pipedream.com/apps/unstructured", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/unstructured/unstructured.app.mjs b/components/unstructured/unstructured.app.mjs new file mode 100644 index 0000000000000..74cc9bde39b66 --- /dev/null +++ b/components/unstructured/unstructured.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "unstructured", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/unthread/actions/create-conversation/create-conversation.mjs b/components/unthread/actions/create-conversation/create-conversation.mjs new file mode 100644 index 0000000000000..e9a83d0575116 --- /dev/null +++ b/components/unthread/actions/create-conversation/create-conversation.mjs @@ -0,0 +1,145 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { + PRIORITY_OPTIONS, + STATUS_OPTIONS, +} from "../../common/constants.mjs"; +import app from "../../unthread.app.mjs"; + +export default { + key: "unthread-create-conversation", + name: "Create Conversation", + description: "Create a new Conversation. [See the documentation](https://docs.unthread.io/api-introduction/using-api#create-conversation)", + version: "0.0.1", + type: "action", + props: { + app, + type: { + type: "string", + label: "Type", + description: "Type of the conversation", + options: [ + "triage", + "email", + ], + reloadProps: true, + }, + markdown: { + type: "string", + label: "Markdown", + description: "Markdown of the conversation", + }, + status: { + type: "string", + label: "Status", + description: "Status of the conversation", + options: STATUS_OPTIONS, + }, + assignedToUserId: { + propDefinition: [ + app, + "userId", + ], + optional: true, + }, + customerId: { + propDefinition: [ + app, + "customerId", + ], + optional: true, + }, + priority: { + type: "integer", + label: "Priority", + description: "Priority of the conversation", + options: PRIORITY_OPTIONS, + optional: true, + }, + triageChannelId: { + propDefinition: [ + app, + "triageChannelId", + ], + hidden: true, + }, + notes: { + type: "string", + label: "Notes", + description: "Notes of the conversation", + optional: true, + }, + title: { + type: "string", + label: "Title", + description: "Title of the conversation", + optional: true, + }, + excludeAnalytics: { + type: "boolean", + label: "Exclude Analytics", + description: "Exclude Analytics for this conversation", + optional: true, + }, + emailInboxId: { + type: "string", + label: "Email Inbox Id", + description: "ID of the Email Inbox", + hidden: true, + }, + onBehalfOfEmail: { + type: "string", + label: "On Behalf Of Email", + description: "Email on behalf of which the conversation is created", + optional: true, + }, + onBehalfOfName: { + type: "string", + label: "On Behalf Of Name", + description: "Name on behalf of which the conversation is created", + optional: true, + }, + onBehalfOfId: { + type: "string", + label: "On Behalf Of ID", + description: "ID on behalf of which the conversation is created", + optional: true, + }, + }, + async additionalProps(props) { + const isTriage = this.type === "triage"; + props.triageChannelId.hidden = !isTriage; + props.emailInboxId.hidden = isTriage; + + return {}; + }, + async run({ $ }) { + if (this.type === "email" && (!this.onBehalfOfEmail && !this.onBehalfOfId)) { + throw new ConfigurationError("You must provide either 'On Behalf Of Email' or 'On Behalf Of ID' when creating an email conversation"); + } + const { + app, + onBehalfOfEmail, + onBehalfOfName, + onBehalfOfId, + ...data + } = this; + + const onBehalfOf = {}; + + if (onBehalfOfEmail) onBehalfOf.email = onBehalfOfEmail; + if (onBehalfOfName) onBehalfOf.name = onBehalfOfName; + if (onBehalfOfId) onBehalfOf.id = onBehalfOfId; + + const response = await app.createConversation({ + $, + data: { + ...data, + onBehalfOf, + }, + }); + + $.export("$summary", `Successfully created Conversation with ID '${response.id}'`); + + return response; + }, +}; diff --git a/components/unthread/actions/create-customer/create-customer.mjs b/components/unthread/actions/create-customer/create-customer.mjs new file mode 100644 index 0000000000000..ba0f886121293 --- /dev/null +++ b/components/unthread/actions/create-customer/create-customer.mjs @@ -0,0 +1,43 @@ +import app from "../../unthread.app.mjs"; + +export default { + key: "unthread-create-customer", + name: "Create Customer", + description: "Create a new Customer. [See the documentation](https://docs.unthread.io/api-introduction/using-api#create-customer)", + version: "0.0.2", + type: "action", + props: { + app, + slackChannelId: { + propDefinition: [ + app, + "slackChannelId", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + emailDomains: { + propDefinition: [ + app, + "emailDomains", + ], + }, + }, + async run({ $ }) { + const { + app, ...data + } = this; + const response = await app.createCustomer({ + $, + data, + }); + + $.export("$summary", `Successfully created Customer with ID '${response.id}'`); + + return response; + }, +}; diff --git a/components/unthread/actions/delete-customer/delete-customer.mjs b/components/unthread/actions/delete-customer/delete-customer.mjs new file mode 100644 index 0000000000000..0c792e33537df --- /dev/null +++ b/components/unthread/actions/delete-customer/delete-customer.mjs @@ -0,0 +1,28 @@ +import app from "../../unthread.app.mjs"; + +export default { + key: "unthread-delete-customer", + name: "Delete Customer", + description: "Delete a Customer. [See the documentation](https://docs.unthread.io/api-introduction/using-api#delete-customer)", + version: "0.0.2", + type: "action", + props: { + app, + customerId: { + propDefinition: [ + app, + "customerId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.deleteCustomer({ + $, + customerId: this.customerId, + }); + + $.export("$summary", `Successfully deleted Customer with ID '${this.customerId}'`); + + return response; + }, +}; diff --git a/components/unthread/actions/update-customer/update-customer.mjs b/components/unthread/actions/update-customer/update-customer.mjs new file mode 100644 index 0000000000000..9653abe974938 --- /dev/null +++ b/components/unthread/actions/update-customer/update-customer.mjs @@ -0,0 +1,71 @@ +import app from "../../unthread.app.mjs"; + +export default { + key: "unthread-update-customer", + name: "Update Customer", + description: "Update a Customer. [See the documentation](https://docs.unthread.io/api-introduction/using-api#update-customer)", + version: "0.0.2", + type: "action", + props: { + app, + customerId: { + propDefinition: [ + app, + "customerId", + ], + }, + slackChannelId: { + propDefinition: [ + app, + "slackChannelId", + ], + optional: true, + }, + name: { + propDefinition: [ + app, + "name", + ], + optional: true, + }, + emailDomains: { + propDefinition: [ + app, + "emailDomains", + ], + optional: true, + }, + disableAutomatedTicketing: { + propDefinition: [ + app, + "disableAutomatedTicketing", + ], + }, + slackTeamId: { + propDefinition: [ + app, + "slackTeamId", + ], + }, + defaultTriageChannelId: { + propDefinition: [ + app, + "defaultTriageChannelId", + ], + }, + }, + async run({ $ }) { + const { + app, customerId, ...data + } = this; + const response = await app.updateCustomer({ + $, + customerId, + data, + }); + + $.export("$summary", `Successfully updated Customer with ID '${response.id}'`); + + return response; + }, +}; diff --git a/components/unthread/common/constants.mjs b/components/unthread/common/constants.mjs new file mode 100644 index 0000000000000..36c458f8c4d7c --- /dev/null +++ b/components/unthread/common/constants.mjs @@ -0,0 +1,27 @@ +export const LIMIT = 100; + +export const STATUS_OPTIONS = [ + "open", + "in_progress", + "on_hold", + "closed", +]; + +export const PRIORITY_OPTIONS = [ + { + label: "3", + value: 3, + }, + { + label: "5", + value: 5, + }, + { + label: "7", + value: 7, + }, + { + label: "9", + value: 9, + }, +]; diff --git a/components/unthread/package.json b/components/unthread/package.json new file mode 100644 index 0000000000000..d2f5821104c34 --- /dev/null +++ b/components/unthread/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/unthread", + "version": "0.2.0", + "description": "Pipedream Unthread Components", + "main": "unthread.app.mjs", + "keywords": [ + "pipedream", + "unthread" + ], + "homepage": "https://pipedream.com/apps/unthread", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/unthread/unthread.app.mjs b/components/unthread/unthread.app.mjs new file mode 100644 index 0000000000000..f84dfb16cf3df --- /dev/null +++ b/components/unthread/unthread.app.mjs @@ -0,0 +1,169 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "unthread", + propDefinitions: { + userId: { + type: "string", + label: "Assigned To User ID", + description: "ID of the User to whom the conversation is assigned", + async options({ prevContext }) { + const { + data, cursors, + } = await this.listUsers({ + data: { + limit: LIMIT, + cursor: prevContext.nextCursor, + }, + }); + + return { + options: data.map(({ + id: value, email: label, + }) => ({ + label, + value, + })), + context: { + nextCursor: cursors.next, + }, + }; + }, + }, + customerId: { + type: "string", + label: "Customer ID", + description: "ID of the Customer", + async options({ prevContext }) { + const { + data, cursors, + } = await this.listCustomers({ + data: { + limit: LIMIT, + cursor: prevContext.nextCursor, + }, + }); + + return { + options: data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })), + context: { + nextCursor: cursors.next, + }, + }; + }, + }, + name: { + type: "string", + label: "Name", + description: "Name of the customer", + }, + slackChannelId: { + type: "string", + label: "Slack Channel ID", + description: "ID the customer's Slack Channel", + }, + emailDomains: { + type: "string[]", + label: "Email Domains", + description: "Email Domains of the customer, i.e.: `gmail.com`", + }, + triageChannelId: { + type: "string", + label: "Triage Channel ID", + description: "ID the customer's Triage Channel. [See the documentation](https://docs.unthread.io/account-setup/connect-channels) for further information.", + }, + disableAutomatedTicketing: { + type: "boolean", + label: "Automated Ticketing", + description: "Disable Automated Ticketing for this customer", + optional: true, + }, + defaultTriageChannelId: { + type: "string", + label: "Default Triage Channel", + description: "ID of the default triage Channel of this customer", + optional: true, + }, + slackTeamId: { + type: "string", + label: "Slack Team ID", + description: "ID of the Slack Team", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.unthread.io/api"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "X-Api-Key": `${this.$auth.api_key}`, + }, + }); + }, + createConversation(args = {}) { + return this._makeRequest({ + method: "post", + path: "/conversations", + ...args, + }); + }, + createCustomer(args = {}) { + return this._makeRequest({ + method: "post", + path: "/customers", + ...args, + }); + }, + updateCustomer({ + customerId, ...args + }) { + return this._makeRequest({ + method: "patch", + path: `/customers/${customerId}`, + ...args, + }); + }, + deleteCustomer({ + customerId, ...args + }) { + return this._makeRequest({ + method: "delete", + path: `/customers/${customerId}`, + ...args, + }); + }, + listCustomers(args = {}) { + return this._makeRequest({ + method: "post", + path: "/customers/list", + ...args, + }); + }, + listUsers(args = {}) { + return this._makeRequest({ + method: "post", + path: "/users/list", + ...args, + }); + }, + }, +}; diff --git a/components/upbooks/actions/add-employee/add-employee.mjs b/components/upbooks/actions/add-employee/add-employee.mjs new file mode 100644 index 0000000000000..3d7e99fd1006b --- /dev/null +++ b/components/upbooks/actions/add-employee/add-employee.mjs @@ -0,0 +1,120 @@ +import upbooks from "../../upbooks.app.mjs"; + +export default { + key: "upbooks-add-employee", + name: "Add New Employee", + description: "Adds a new employee to Upbooks. [See the documentation](https://www.postman.com/scrrum/workspace/upbooks-io/request/13284127-a51a907a-0648-477d-96f6-f5a9e79262fd)", + version: "0.0.1", + type: "action", + props: { + upbooks, + name: { + type: "string", + label: "Name", + description: "Full name of the employee.", + }, + employeeNumber: { + type: "string", + label: "Employee Number", + description: "The identification number of the employee.", + optional: true, + }, + type: { + type: "string", + label: "Type", + description: "The employee's type.", + options: [ + "full-time", + "part-time", + ], + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "The employee's email.", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The employee's phone.", + optional: true, + }, + dob: { + type: "string", + label: "DOB", + description: "The employee's date of birth. Format: YYYY-MM-DD", + optional: true, + }, + dateOfJoining: { + type: "string", + label: "Date Of Joining", + description: "The employee's start date. Format: YYYY-MM-DDTHH:MM:SSZ", + optional: true, + }, + dateOfLeaving: { + type: "string", + label: "Date Of Leaving", + description: "The employee's end date. Format: YYYY-MM-DDTHH:MM:SSZ", + optional: true, + }, + ctc: { + type: "integer", + label: "CTC", + description: "Cost to company in cents.", + }, + salaryComponentId: { + propDefinition: [ + upbooks, + "salaryComponentId", + ], + }, + designation: { + type: "string", + label: "Designation", + description: "In which position the employee will work.", + optional: true, + }, + role: { + type: "string", + label: "Role", + description: "The identification of the employee's role.", + options: [ + "staff", + "admin", + "others", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.upbooks.addNewEmployee({ + $, + data: { + name: this.name, + employeeNumber: this.employeeNumber, + type: this.type, + email: this.email, + phone: this.phone, + dob: this.dob, + employment: [ + { + dateOfJoining: this.dateOfJoining, + dateOfLeaving: this.dateOfLeaving, + salary: { + ctc: (this.ctc / 100).toFixed(2), + componentGroup: { + id: this.salaryComponentId, + }, + }, + designation: this.designation, + role: this.role, + }, + ], + }, + }); + $.export("$summary", `Successfully added a new employee with Id: ${response.data._id}`); + return response; + }, +}; diff --git a/components/upbooks/actions/create-expense-category/create-expense-category.mjs b/components/upbooks/actions/create-expense-category/create-expense-category.mjs new file mode 100644 index 0000000000000..7b2234c62cd20 --- /dev/null +++ b/components/upbooks/actions/create-expense-category/create-expense-category.mjs @@ -0,0 +1,55 @@ +import upbooks from "../../upbooks.app.mjs"; + +export default { + key: "upbooks-create-expense-category", + name: "Create Expense Category", + description: "Creates a new expense category in UpBooks. [See the documentation](https://www.postman.com/scrrum/workspace/upbooks-io/request/13284127-a07ae2fc-f712-42aa-bcf5-6ce63c7a0929)", + version: "0.0.1", + type: "action", + props: { + upbooks, + title: { + type: "string", + label: "Title", + description: "The expense category's title.", + }, + subCategory: { + type: "string", + label: "Sub Category", + description: "subCategory", + options: [ + { + label: "Operating Expense", + value: "operating-expense", + }, + { + label: "Non Operating Expense", + value: "non-operating-expense", + }, + { + label: "Cost Of Goods Sold", + value: "cost-of-goods-sold", + }, + ], + }, + summary: { + type: "string", + label: "Summary", + description: "summary", + optional: true, + }, + }, + async run({ $ }) { + const { + upbooks, + ...data + } = this; + + const response = await upbooks.createExpenseCategory({ + $, + data, + }); + $.export("$summary", `Successfully created new expense category with Id: ${response.data._id}`); + return response; + }, +}; diff --git a/components/upbooks/actions/record-outward-payment/record-outward-payment.mjs b/components/upbooks/actions/record-outward-payment/record-outward-payment.mjs new file mode 100644 index 0000000000000..0ce4e31764532 --- /dev/null +++ b/components/upbooks/actions/record-outward-payment/record-outward-payment.mjs @@ -0,0 +1,88 @@ +import { CURRENCY_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import upbooks from "../../upbooks.app.mjs"; + +export default { + key: "upbooks-record-outward-payment", + name: "Record Outward Payment", + description: "Records an outward payment in UpBooks. [See the documentation](https://www.postman.com/scrrum/workspace/upbooks-io/request/13284127-3fc82d7a-2173-4b3a-a8ec-4c812c928810)", + version: "0.0.1", + type: "action", + props: { + upbooks, + mode: { + type: "string", + label: "Mode", + description: "The outward payment mode.", + options: [ + { + label: "Cash", + value: "cash", + }, + { + label: "Cheque", + value: "cheque", + }, + { + label: "Neft", + value: "neft", + }, + { + label: "Imps", + value: "imps", + }, + { + label: "Wire Transfer", + value: "wire transfer", + }, + ], + }, + amount: { + type: "string", + label: "Amount", + description: "The outwart payment amount in cents.", + }, + date: { + type: "string", + label: "Date", + description: "The date of the outward payment. Format: YYYY-MM-DD", + }, + expenseIds: { + propDefinition: [ + upbooks, + "expenseIds", + ], + }, + account: { + propDefinition: [ + upbooks, + "accountId", + ], + }, + currency: { + type: "string", + label: "Currency", + description: "The currency of the outward payment.", + options: CURRENCY_OPTIONS, + }, + }, + async run({ $ }) { + const currency = CURRENCY_OPTIONS.filter((item) => item.value === this.currency)[0]; + const response = await this.upbooks.recordOutwardPayment({ + $, + data: { + mode: this.mode, + amount: (this.amount / 100).toFixed(2), + date: this.date, + expenseIds: parseObject(this.expenseIds), + accountId: this.account, + currency: { + name: currency.label, + symbol: currency.value, + }, + }, + }); + $.export("$summary", `Successfully recorded outward payment with Id: ${response.data._id}`); + return response; + }, +}; diff --git a/components/upbooks/common/constants.mjs b/components/upbooks/common/constants.mjs new file mode 100644 index 0000000000000..46fd49769bbcd --- /dev/null +++ b/components/upbooks/common/constants.mjs @@ -0,0 +1,22 @@ +export const CURRENCY_OPTIONS = [ + { + label: "Indian Rupees", + value: "INR", + }, + { + label: "US Dollar", + value: "USD", + }, + { + label: "Euro", + value: "EUR", + }, + { + label: "Australian Dollar", + value: "AUD", + }, + { + label: "Emirati Dirham", + value: "AED", + }, +]; diff --git a/components/upbooks/common/utils.mjs b/components/upbooks/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/upbooks/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/upbooks/package.json b/components/upbooks/package.json new file mode 100644 index 0000000000000..88d23e10777c5 --- /dev/null +++ b/components/upbooks/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/upbooks", + "version": "0.1.0", + "description": "Pipedream UpBooks Components", + "main": "upbooks.app.mjs", + "keywords": [ + "pipedream", + "upbooks" + ], + "homepage": "https://pipedream.com/apps/upbooks", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} + diff --git a/components/upbooks/sources/new-data/new-data.mjs b/components/upbooks/sources/new-data/new-data.mjs new file mode 100644 index 0000000000000..9a8c97dbf48ed --- /dev/null +++ b/components/upbooks/sources/new-data/new-data.mjs @@ -0,0 +1,61 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import upbooks from "../../upbooks.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "upbooks-new-data", + name: "New Data Available", + description: "Emit new event when fresh data is available for a specific collection.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + upbooks, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01T00:00:00Z"; + }, + _setLastDate(created) { + this.db.set("lastDate", created); + }, + generateMeta(item) { + return { + id: item._id, + summary: `New ${item.title}`, + ts: item.occurredAt, + }; + }, + async startEvent(maxResults = 0) { + const lastDate = this._getLastDate(); + const { data } = await this.upbooks.listActivities({ + params: { + fromDate: lastDate, + }, + }); + + if (maxResults && maxResults.length > maxResults) maxResults.length = maxResults; + if (data.length) this._setLastDate(data[0].occurredAt); + + for (const item of data.reverse()) { + this.$emit(item, this.generateMeta(item)); + } + }, + }, + hooks: { + async deploy() { + await this.startEvent(25); + }, + }, + async run() { + await this.startEvent(); + }, + sampleEmit, +}; diff --git a/components/upbooks/sources/new-data/test-event.mjs b/components/upbooks/sources/new-data/test-event.mjs new file mode 100644 index 0000000000000..d0ab64f2805c9 --- /dev/null +++ b/components/upbooks/sources/new-data/test-event.mjs @@ -0,0 +1,31 @@ +export default { + "_id":"0d6f5e4038f7eb902bfa651b", + "user":{ + "_id":"0d6f5e4038f7eb902bfa651b", + "id":"0d6f5e4038f7eb902bfa651b", + "role":"organization-admin", + "name":"Sergio Wong", + "email":"sergio@pipekit.pro", + "ipAddress":"12.345.678.901", + }, + "organization":{ + "_id":"0d6f5e4038f7eb902bfa651b", + "organizationId":"123456" + }, + "occurredAt":"2024-05-24T21:15:59.347Z", + "eventCode":"EV-1234", + "eventCategory":"Outward-Payment", + "actionType":"Create", + "initiator":"User", + "title":"Outward Payment Recorded", + "body":"User recorded outward payment OP12345 of amount 1.00 USD", + "additional":{ + "link":{ + "text":"0d6f5e4038f7eb902bfa651b", + "url":"outward-payment/view/0d6f5e4038f7eb902bfa651b" + } + }, + "createdAt":"2024-05-24T21:15:59.356Z", + "updatedAt":"2024-05-24T21:15:59.356Z", + "__v":0 +} \ No newline at end of file diff --git a/components/upbooks/upbooks.app.mjs b/components/upbooks/upbooks.app.mjs new file mode 100644 index 0000000000000..ad657f1048feb --- /dev/null +++ b/components/upbooks/upbooks.app.mjs @@ -0,0 +1,161 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "upbooks", + propDefinitions: { + salaryComponentId: { + type: "string", + label: "Salary Component Id", + description: "The identification of the salary component.", + async options({ page }) { + const { data } = await this.listSalaryComponents({ + params: { + page, + }, + }); + + return data.map(({ + _id: value, groupName: label, + }) => ({ + label, + value, + })); + }, + }, + expenseIds: { + type: "string[]", + label: "Expense Ids", + description: "The identification of the expense.", + async options({ page }) { + const { data } = await this.listExpenses({ + params: { + page, + }, + }); + + return data.map(({ + _id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + accountId: { + type: "string", + label: "Account Id", + description: "The identification of the account.", + async options({ page }) { + const { data } = await this.listAccounts({ + params: { + page, + }, + }); + + return data.map(({ + _id: value, title, category, + }) => ({ + label: `${title} (${category.charAt(0).toUpperCase() + category.slice(1)})`, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.upbooks.io/api/v1"; + }, + _headers() { + return { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "API-KEY": `${this.$auth.api_key}`, + "Organization-ID": `${this.$auth.organization_id}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listAccounts(opts = {}) { + return this._makeRequest({ + path: "/account", + ...opts, + }); + }, + listActivities(opts = {}) { + return this._makeRequest({ + path: "/activity-log/all", + ...opts, + }); + }, + listExpenses(opts = {}) { + return this._makeRequest({ + path: "/expense", + ...opts, + }); + }, + listSalaryComponents(opts = {}) { + return this._makeRequest({ + path: "/salary-component", + ...opts, + }); + }, + addNewEmployee(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/employee", + ...opts, + }); + }, + createExpenseCategory(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/expense-category", + ...opts, + }); + }, + recordOutwardPayment(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/outward-payment", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const { + data, + meta: { + current_page, last_page, + }, + } = await fn({ + params, + ...opts, + }); + for (const d of data) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = !(current_page == last_page); + + } while (hasMore); + }, + }, +}; diff --git a/components/updown_io/README.md b/components/updown_io/README.md new file mode 100644 index 0000000000000..8b82d37dfebe9 --- /dev/null +++ b/components/updown_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The Updown.io API allows you to interact programmatically with your website monitoring service. With this API, you can automate uptime checks, retrieve the status of your websites, manage checks, receive downtime alerts, and gather performance metrics. Integrating the Updown.io API with Pipedream enables you to create automated workflows that can respond to events, monitor services continuously, and connect with other apps to trigger actions based on the health and performance of your websites. + +# Example Use Cases + +- **Automated Downtime Alerts to Slack**: Use Updown.io with Pipedream to monitor your website's uptime. Set up a workflow that triggers a notification in a Slack channel whenever Updown.io detects your site is down. This enables your team to react swiftly to any issues. + +- **Scheduled Performance Reports to Email**: Combine Updown.io with a service like SendGrid on Pipedream to send daily or weekly performance reports. Summarize uptime, downtime, and average response times of your websites and email them to stakeholders to keep them informed. + +- **Auto-Scaling Infrastructure Based on Uptime Metrics**: Link Updown.io with cloud provider APIs, like AWS or Google Cloud, on Pipedream. Create a workflow that scales your infrastructure up or down based on the performance metrics from Updown.io, optimizing cost and performance based on real-time data. diff --git a/components/updown_io/package.json b/components/updown_io/package.json new file mode 100644 index 0000000000000..3c9f2a07ca0c5 --- /dev/null +++ b/components/updown_io/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/updown_io", + "version": "0.1.0", + "description": "Pipedream Updown.io Components", + "main": "updown_io.app.mjs", + "keywords": [ + "pipedream", + "updown_io" + ], + "homepage": "https://pipedream.com/apps/updown_io", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/updown_io/sources/common/base.mjs b/components/updown_io/sources/common/base.mjs new file mode 100644 index 0000000000000..0345972f988fa --- /dev/null +++ b/components/updown_io/sources/common/base.mjs @@ -0,0 +1,65 @@ +import updown from "../../updown_io.app.mjs"; + +export default { + props: { + updown, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + }, + hooks: { + async activate() { + const { id } = await this.updown.createRecipient({ + data: { + type: "webhook", + value: this.http.endpoint, + }, + }); + this._setRecipientId(id); + }, + async deactivate() { + const recipientId = this._getRecipientId(); + if (recipientId) { + await this.updown.deleteRecipient({ + recipientId, + }); + } + }, + }, + methods: { + _getRecipientId() { + return this.db.get("recipientId"); + }, + _setRecipientId(recipientId) { + this.db.set("recipientId", recipientId); + }, + getEventTypes() { + return false; + }, + isRelevant() { + return true; + }, + generateMeta(event) { + const ts = Date.parse(event.time); + return { + id: `${event.event}${ts}`, + summary: event.description, + ts, + }; + }, + }, + async run({ body }) { + this.http.respond({ + status: 200, + }); + const eventTypes = this.getEventTypes(); + for (const event of body) { + if ((!eventTypes?.length || eventTypes.includes(event.event)) && this.isRelevant(event)) { + const meta = this.generateMeta(event); + this.$emit(event, meta); + } + } + }, +}; diff --git a/components/updown_io/sources/common/events.mjs b/components/updown_io/sources/common/events.mjs new file mode 100644 index 0000000000000..989d41d731d35 --- /dev/null +++ b/components/updown_io/sources/common/events.mjs @@ -0,0 +1,30 @@ +export default [ + { + value: "check.down", + label: "When a check goes down (after confirmation)", + }, + { + value: "check.up", + label: "When a check is back up (recovery following a check.down event)", + }, + { + value: "check.ssl_invalid", + label: "When the SSL certificate is considered invalid", + }, + { + value: "check.ssl_valid", + label: "When the SSL certificate is valid again (recovery after a check.ssl_invalid event)", + }, + { + value: "check.ssl_expiration", + label: "When your SSL certificate approaches expiration date (1, 7, 14, and 30 days before for 1y certs)", + }, + { + value: "check.ssl_renewed", + label: "When the SSL certificate was renewed close to expiration (recovery for check.ssl_expiration)", + }, + { + value: "check.performance_drop", + label: "When the Apex drops more than 30% below the lowest of the last 5 hours", + }, +]; diff --git a/components/updown_io/sources/new-down-alert-instant/new-down-alert-instant.mjs b/components/updown_io/sources/new-down-alert-instant/new-down-alert-instant.mjs new file mode 100644 index 0000000000000..beede3835d1d2 --- /dev/null +++ b/components/updown_io/sources/new-down-alert-instant/new-down-alert-instant.mjs @@ -0,0 +1,21 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "updown_io-new-down-alert-instant", + name: "New Down Alert (Instant)", + description: "Emit new event when a website check reports as down. [See the documentation](https://updown.io/api#webhooks)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventTypes() { + return [ + "check.down", + ]; + }, + }, + sampleEmit, +}; diff --git a/components/updown_io/sources/new-down-alert-instant/test-event.mjs b/components/updown_io/sources/new-down-alert-instant/test-event.mjs new file mode 100644 index 0000000000000..6cd22b04c65c2 --- /dev/null +++ b/components/updown_io/sources/new-down-alert-instant/test-event.mjs @@ -0,0 +1,42 @@ +export default { + "event": "check.down", + "time": "2024-03-15T21:07:54Z", + "description": "DOWN: https://updown.io/ since 21:02:54 (UTC), reason: 418 I'm a teapot", + "check": { + "token": "xyz0", + "url": "https://updown.io", + "alias": null, + "last_status": 418, + "uptime": 100, + "down": true, + "down_since": "2024-03-15T21:02:54Z", + "up_since": null, + "error": "418 I'm a teapot", + "period": 30, + "apdex_t": 0.25, + "string_match": "", + "enabled": true, + "published": true, + "disabled_locations": [], + "recipients": [ + "email:497723868", + "webhook:1300810874" + ], + "last_check_at": "2024-03-15T21:07:39Z", + "next_check_at": "2024-03-15T21:08:09Z", + "created_at": null, + "mute_until": null, + "favicon_url": "https://updown.io/favicon.png", + "custom_headers": {}, + "http_verb": "GET/HEAD", + "http_body": "" + }, + "downtime": { + "id": "65f4b8aa893e4abe43ed72cd", + "error": "418 I'm a teapot", + "started_at": "2024-03-15T21:02:54Z", + "ended_at": null, + "duration": null, + "partial": null + } +} \ No newline at end of file diff --git a/components/updown_io/sources/new-event-instant/new-event-instant.mjs b/components/updown_io/sources/new-event-instant/new-event-instant.mjs new file mode 100644 index 0000000000000..bfac11aff0cbe --- /dev/null +++ b/components/updown_io/sources/new-event-instant/new-event-instant.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; +import events from "../common/events.mjs"; + +export default { + ...common, + key: "updown_io-new-event-instant", + name: "New Event (Instant)", + description: "Emit new event when a new webhook event occurs. [See the documentation](https://updown.io/api#webhooks)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + eventTypes: { + type: "string[]", + label: "Event Types", + description: "Filter the incoming events by event type", + options: events, + optional: true, + }, + }, + methods: { + ...common.methods, + getEventTypes() { + return this.eventTypes; + }, + }, +}; diff --git a/components/updown_io/sources/ssl-expiration-instant/ssl-expiration-instant.mjs b/components/updown_io/sources/ssl-expiration-instant/ssl-expiration-instant.mjs new file mode 100644 index 0000000000000..9b23830c7dcbe --- /dev/null +++ b/components/updown_io/sources/ssl-expiration-instant/ssl-expiration-instant.mjs @@ -0,0 +1,40 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "updown_io-ssl-expiration-instant", + name: "SSL Expiration (Instant)", + description: "Emit new event when an SSL certificate expiration is detected [See the documentation](https://updown.io/api#webhooks)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + threshold: { + type: "string", + label: "Threshold", + description: "Days before SSL expiration to emit an event", + options: [ + "1", + "7", + "14", + "30", + ], + optional: true, + }, + }, + methods: { + ...common.methods, + getEventTypes() { + return [ + "check.ssl_expiration", + ]; + }, + isRelevant(event) { + return !this.threshold + || (event?.ssl && event.ssl.days_before_expiration === +this.threshold); + }, + }, + sampleEmit, +}; diff --git a/components/updown_io/sources/ssl-expiration-instant/test-event.mjs b/components/updown_io/sources/ssl-expiration-instant/test-event.mjs new file mode 100644 index 0000000000000..30ba9a0d22000 --- /dev/null +++ b/components/updown_io/sources/ssl-expiration-instant/test-event.mjs @@ -0,0 +1,45 @@ +export default { + "event": "check.ssl_expiration", + "time": "2024-03-15T21:12:08Z", + "description": "The SSL certificate served by updown.io will expire in 7 days", + "check": { + "token": "xyz0", + "url": "https://updown.io", + "alias": null, + "last_status": 200, + "uptime": 100, + "down": false, + "down_since": null, + "up_since": "2024-02-15T21:12:08Z", + "error": null, + "period": 30, + "apdex_t": 0.25, + "string_match": "", + "enabled": true, + "published": true, + "disabled_locations": [], + "recipients": [ + "email:497723868", + "webhook:1300810874", + "webhook:3808246510" + ], + "last_check_at": "2024-03-15T21:11:53Z", + "next_check_at": "2024-03-15T21:12:23Z", + "created_at": null, + "mute_until": null, + "favicon_url": "https://updown.io/favicon.png", + "custom_headers": {}, + "http_verb": "GET/HEAD", + "http_body": "" + }, + "ssl": { + "cert": { + "subject": "updown.io", + "issuer": "Let's Encrypt Authority X3 (Let's Encrypt)", + "from": "2018-09-08T21:00:18Z", + "to": "2018-12-07T21:00:18Z", + "algorithm": "SHA-256 with RSA encryption" + }, + "days_before_expiration": 7 + } +} \ No newline at end of file diff --git a/components/updown_io/updown_io.app.mjs b/components/updown_io/updown_io.app.mjs new file mode 100644 index 0000000000000..73d53864d5814 --- /dev/null +++ b/components/updown_io/updown_io.app.mjs @@ -0,0 +1,47 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "updown_io", + propDefinitions: {}, + methods: { + _baseUrl() { + return "https://updown.io/api"; + }, + _authParams(params) { + return { + ...params, + "api-key": `${this.$auth.api_key}`, + }; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + params: this._authParams(params), + }); + }, + createRecipient(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/recipients", + ...opts, + }); + }, + deleteRecipient({ + recipientId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/recipients/${recipientId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/upkeep/README.md b/components/upkeep/README.md index 08d01796bf0a1..61bc8858b8284 100644 --- a/components/upkeep/README.md +++ b/components/upkeep/README.md @@ -1,31 +1,11 @@ # Overview -The [UpKeep API](https://www.upkeep.com/) allows developers to build -Maintenance & Operations solutions for their customers and end users. The API -enables developers to access data on their users' assets, tasks, and -maintenance operations. With the UpKeep API, developers can build custom -solutions for asset management, equipment tracking, work order management, and -more. +The UpKeep API enables users to seamlessly integrate maintenance management tasks with Pipedream's serverless execution platform. By leveraging UpKeep's endpoints, you can automate workflows related to asset tracking, work order management, and preventive maintenance scheduling. The API's capabilities allow for real-time updates on equipment status, automated notifications for maintenance tasks, and data synchronization across maintenance teams and tools. -Some of the features that the UpKeep API provides include: +# Example Use Cases -- Asset Tracking: Track your assets with detailed information, such as location - and usage stats. -- Task Management: Keep track of tasks assigned to your users or users assigned - to tasks. -- Maintenance Operations: Record and track maintenance operations, such as - repairs and replacements. -- Work Order Management: Create and manage work orders, including assigning - technicians and tracking progress. +- **Automated Work Order Creation**: Trigger a new work order in UpKeep when a customer support ticket in Zendesk indicates equipment failure. This workflow can include pulling relevant customer data from the ticket, inputting it into the work order, and notifying the maintenance team in Slack. -Here are some examples of what you can build using the UpKeep API: +- **Preventive Maintenance Scheduling**: Use a cron job to schedule preventive maintenance tasks. The workflow can fetch equipment usage logs from an IoT platform, like Particle, evaluate if service is due, and create a maintenance request in UpKeep if thresholds are met. -- Maintenance & Operations Solutions: Create solutions that manage equipment, - plan and schedule maintenance, record maintenance operations, and track asset - usage. -- Fleet Management Solutions: Create solutions that track fleet assets, - including vehicles and equipment, and monitor usage. -- Task Management Solutions: Create solutions that allow users to assign and - manage tasks, as well as track their progress. -- Asset Tracking Solutions: Create solutions that track assets and their usage, - including their location, condition, and maintenance operations. +- **Inventory Management Alerts**: Connect UpKeep with an inventory management app like TradeGecko to monitor stock levels. When inventory falls below a certain threshold, the workflow can automatically generate a purchase order in UpKeep and notify the procurement team via email or a messaging app like Microsoft Teams. diff --git a/components/uplead/README.md b/components/uplead/README.md new file mode 100644 index 0000000000000..21aba7e3e81f4 --- /dev/null +++ b/components/uplead/README.md @@ -0,0 +1,11 @@ +# Overview + +The UpLead API provides access to a robust database of B2B contacts and companies, enabling users to enrich leads with detailed information like email addresses, phone numbers, company details, and more. Within Pipedream, you can integrate the UpLead API to automate lead enrichment, sync lead data with other tools, and build custom workflows that leverage UpLead's data for sales intelligence, marketing campaigns, and CRM data enhancement. + +# Example Use Cases + +- **Lead Enrichment in Real-Time**: Trigger a workflow in Pipedream whenever a new lead is captured via a web form. Use the UpLead API to enrich the lead's data and then automatically add the detailed lead information to a CRM like Salesforce or a marketing platform like Mailchimp for immediate follow-up or nurturing. + +- **Scheduled Data Sync for CRM**: Set up a scheduled workflow in Pipedream that runs at regular intervals (daily, weekly) to fetch new and updated contacts from UpLead. The workflow can cross-reference existing CRM records, update them with fresh data, and add new leads to ensure your sales team always has the latest information at their fingertips. + +- **Alerts for Target Account Changes**: Monitor specific companies or industries for changes in key data points, such as company size, funding rounds, or executive movements. Use the UpLead API in a Pipedream workflow to regularly check for these updates and send notifications via Slack or email to relevant stakeholders when important changes are detected. diff --git a/components/uplisting/README.md b/components/uplisting/README.md new file mode 100644 index 0000000000000..4be0ab06be9e1 --- /dev/null +++ b/components/uplisting/README.md @@ -0,0 +1,11 @@ +# Overview + +The Uplisting API allows property managers and hosts to automate and integrate their listing and booking management across various platforms. With this API on Pipedream, you can streamline operations like syncing bookings, updating listings, and managing guests' stays. Uplisting on Pipedream provides a canvas for building workflows that connect with other apps for a cohesive property management system, simplifying tasks such as communication, scheduling, and analytics. + +# Example Use Cases + +- **Automate Booking Confirmations**: When a new booking is made on Uplisting, trigger a Pipedream workflow that sends a personalized confirmation email to the guest using the SendGrid app. This can include details like check-in instructions and house rules. + +- **Sync Bookings with Google Calendar**: Create a workflow that adds new Uplisting bookings to a Google Calendar, helping you visually manage property availability. You can also set up reminders for upcoming check-ins or check-outs. + +- **Generate Monthly Revenue Reports**: With a Pipedream workflow, consolidate booking data from Uplisting at the end of each month, calculate earnings, and compile them into a report using Google Sheets. This can be set to email the report to stakeholders via Gmail or another email service. diff --git a/components/uploadcare/README.md b/components/uploadcare/README.md index 47407db7f476c..2ac4ebbb446d6 100644 --- a/components/uploadcare/README.md +++ b/components/uploadcare/README.md @@ -1,26 +1,11 @@ # Overview -Using the Uploadcare API, you can build a variety of applications that improve -the user experience while minimizing their reliance on third-party services. -With the API, you can: +Uploadcare is a file uploading, processing, and delivery platform that provides developers with tools to handle the entire file lifecycle with ease. With its robust API, you can upload files from any device, transform images and documents on-the-fly, and manage digital content with a comprehensive set of features. Integrating Uploadcare with Pipedream allows you to craft workflows that automate file operations and connect them seamlessly with other services, such as CRMs, marketing platforms, and data analysis tools. -- Create a fast and secure content delivery system. This is useful for - businesses that need to deploy large amounts of content quickly and securely. -- Design web and mobile applications that are optimized for fast loading and - providing the best user experience. -- Create a storage for media files, photos, and video that can be accessed from - any part of the world. -- Store files in the cloud, and provide users access to them from anywhere. -- Create advanced image editing tools, with advanced cropping and resizing - tools, filters, etc. -- Allow users to share files from anywhere on the web, adding social features - that let them create and share albums and stories. -- Create notifications for users when their uploads have completed, allowing - for quick feedback about the progress of their uploads. -- Allow users to edit documents in the cloud, providing advanced features such - as spell-check, format conversion, and more. -- Automate media content delivery, ensuring that users are always served the - most updated versions. -- Provide a secure authentication system, allowing businesses to easily manage - who has access to their media and applications. -- And much, much more! +# Example Use Cases + +- **Automated Image Moderation and Posting to Social Media**: When new images are uploaded to Uploadcare, trigger a Pipedream workflow that utilizes image recognition APIs to moderate content. Once approved, automatically resize and optimize the images using Uploadcare's processing capabilities, then post them to social media platforms like Twitter or Instagram. + +- **E-Commerce Order Fulfillment Automation**: For an e-commerce platform, use an Uploadcare upload widget to receive customer's customization files at checkout. Trigger a Pipedream workflow that processes these files, attaches them to the relevant order in your e-commerce system (like Shopify), and sends a notification to the fulfillment team with the order details and processed files. + +- **Event-Driven Backup to Cloud Storage**: Set up an event listener in Pipedream for new files uploaded to Uploadcare. Upon upload, initiate a workflow that performs any necessary file conversions, then automatically backs up the files to cloud storage solutions such as Amazon S3 or Google Drive, ensuring that business-critical files are always securely stored and accessible. diff --git a/components/upollo/README.md b/components/upollo/README.md new file mode 100644 index 0000000000000..bb38645b571af --- /dev/null +++ b/components/upollo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Upollo API offers real-time user behavior analysis to prevent fraud and account sharing. It's designed to detect and respond to suspicious activities by scoring user actions and sessions. In Pipedream, you can harness this API to craft workflows that automate responses to these activities, integrate with other services for enriched functionality, and streamline user management processes. + +# Example Use Cases + +- **User Risk Scoring and Alerting**: Create a workflow that triggers when a new user action occurs, uses the Upollo API to score the risk level, and sends alerts via email or Slack if the risk threshold is exceeded. + +- **Automated Fraud Detection and Account Flagging**: Build a workflow where, upon detecting a high-risk score from Upollo, it automatically flags the user account in your database and triggers additional verification steps through email or SMS. + +- **Enhanced Security with Two-Factor Authentication (2FA)**: Implement a workflow that, after receiving a high-risk assessment from Upollo, triggers a 2FA process with an app like Twilio, ensuring another layer of security before granting account access. diff --git a/components/upollo/package.json b/components/upollo/package.json index daf959f97e35d..589f215730367 100644 --- a/components/upollo/package.json +++ b/components/upollo/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/upstash_redis/actions/list-databases/list-databases.mjs b/components/upstash_redis/actions/list-databases/list-databases.mjs new file mode 100644 index 0000000000000..fd0d237276e7d --- /dev/null +++ b/components/upstash_redis/actions/list-databases/list-databases.mjs @@ -0,0 +1,19 @@ +import app from "../../upstash_redis.app.mjs"; + +export default { + key: "upstash_redis-list-databases", + name: "List Redis Databases", + description: "Lists all Redis databases. [See the documentation](https://upstash.com/docs/devops/developer-api/redis/list_databases)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.listDatabases({ + $, + }); + $.export("$summary", `Successfully listed \`${response.length}\` Redis database(s)`); + return response; + }, +}; diff --git a/components/upstash_redis/actions/post-command/post-command.mjs b/components/upstash_redis/actions/post-command/post-command.mjs new file mode 100644 index 0000000000000..234b1b57c809d --- /dev/null +++ b/components/upstash_redis/actions/post-command/post-command.mjs @@ -0,0 +1,49 @@ +import utils from "../../common/utils.mjs"; +import app from "../../upstash_redis.app.mjs"; + +export default { + key: "upstash_redis-post-command", + name: "Post Command", + description: "Post a command to a Redis database. [See the documentation](https://upstash.com/docs/redis/features/restapi#post-command-in-body)", + version: "0.0.1", + type: "action", + props: { + app, + command: { + type: "string", + label: "Command", + description: "The command to post to the database.", + options: [ + "SET", + "GET", + ], + }, + args: { + type: "string[]", + label: "Arguments", + description: "The arguments for the command. Eg. `[\"mykey\", \"myvalue\"]`", + }, + }, + methods: { + postCommand(args = {}) { + return this.app.post(args); + }, + }, + async run({ $ }) { + const { + postCommand, + command, + args, + } = this; + const response = await postCommand({ + $, + data: [ + command, + ...utils.parseArray(args), + ], + }); + + $.export("$summary", "Successfully posted command to the database."); + return response; + }, +}; diff --git a/components/upstash_redis/common/constants.mjs b/components/upstash_redis/common/constants.mjs new file mode 100644 index 0000000000000..97b1a13636a23 --- /dev/null +++ b/components/upstash_redis/common/constants.mjs @@ -0,0 +1,9 @@ +const BASE_URL = "https://api.upstash.com"; +const VERSION_PATH = "/v2/redis"; +const HTTPS_PROTOCOL = "https://"; + +export default { + BASE_URL, + VERSION_PATH, + HTTPS_PROTOCOL, +}; diff --git a/components/upstash_redis/common/utils.mjs b/components/upstash_redis/common/utils.mjs new file mode 100644 index 0000000000000..d9df0aa935aef --- /dev/null +++ b/components/upstash_redis/common/utils.mjs @@ -0,0 +1,28 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function parseArray(value) { + try { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +export default { + parseArray, +}; diff --git a/components/upstash_redis/package.json b/components/upstash_redis/package.json new file mode 100644 index 0000000000000..6b3c12220796f --- /dev/null +++ b/components/upstash_redis/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/upstash_redis", + "version": "0.1.0", + "description": "Pipedream Upstash Redis Components", + "main": "upstash_redis.app.mjs", + "keywords": [ + "pipedream", + "upstash_redis" + ], + "homepage": "https://pipedream.com/apps/upstash_redis", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} diff --git a/components/upstash_redis/upstash_redis.app.mjs b/components/upstash_redis/upstash_redis.app.mjs new file mode 100644 index 0000000000000..e970463b0a7db --- /dev/null +++ b/components/upstash_redis/upstash_redis.app.mjs @@ -0,0 +1,76 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "upstash_redis", + propDefinitions: { + databaseId: { + type: "string", + label: "Database ID", + description: "The ID of the database.", + async options() { + const databases = await this.listDatabases(); + return databases.map(({ + database_id: value, database_name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + getRedisUrl() { + const { upstash_redis_rest_url: url } = this.$auth; + return url.startsWith(constants.HTTPS_PROTOCOL) + ? url + : `${constants.HTTPS_PROTOCOL}${url}`; + }, + getUrl(path, managementApi) { + return managementApi + ? `${constants.BASE_URL}${constants.VERSION_PATH}${path}` + : this.getRedisUrl(); + }, + getHeaders(headers) { + return { + ...headers, + "Authorization": `Bearer ${this.$auth.uptash_redis_rest_token}`, + }; + }, + getAuth(managementApi) { + if (managementApi) { + const { + email: username, + management_api_key: password, + } = this.$auth; + return { + username, + password, + }; + } + }, + _makeRequest({ + $ = this, path, headers, managementApi, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path, managementApi), + headers: this.getHeaders(headers), + auth: this.getAuth(managementApi), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + listDatabases() { + return this._makeRequest({ + managementApi: true, + path: "/databases", + }); + }, + }, +}; diff --git a/components/uptime_robot/README.md b/components/uptime_robot/README.md index 41f129eae1be0..00d11a3cd85db 100644 --- a/components/uptime_robot/README.md +++ b/components/uptime_robot/README.md @@ -1,23 +1,11 @@ # Overview -Uptime Robot is a monitoring service provider that offers an API to help -developers and businesses monitor their websites and services. With the Uptime -Robot API, developers can create custom solutions to check and monitor their -systems, as well as measure performance and detect downtime. +The Uptime Robot API allows you to monitor the uptime of your websites and services from a central hub. On Pipedream, you can harness this API to create automated workflows that trigger on events like downtime, maintenance, or performance issues. This enables real-time alerts, auto-generated reports, and seamless integration with other tools for incident management or data analysis. -The Uptime Robot API can be used to build a variety of applications, from basic -website-monitoring tools to sophisticated alerting and reporting systems. Here -are some examples of the applications that you can build with the Uptime Robot -API: +# Example Use Cases -- Automated Website Monitoring: Monitor websites and web services for - availability and performance. Notify users when an issue is detected. -- Reporting Dashboards: Aggregate monitoring data and visualise it in custom - dashboards. -- Automated Notification System: Set up a system to be notified immediately - about downtimes or performance issues. -- Alerting System: Set custom alerts for various metrics and thresholds. -- Slack/Hipchat Integrations: Connect notifications to popular messaging - platforms. -- Custom Monitoring Solutions: Build custom solutions to check any external API - or system. +- **Downtime Alert to Slack**: Trigger a Pipedream workflow when Uptime Robot detects that a site is down. The workflow sends a custom alert to a designated Slack channel, ensuring your team responds swiftly to downtime incidents. + +- **Scheduled Performance Reports to Email**: Use Pipedream's built-in cron scheduler to regularly retrieve performance data from Uptime Robot. Format the data and automatically send a daily or weekly performance report to a list of stakeholders through email. + +- **Incident Management with Jira**: Create a Pipedream workflow that opens a new Jira ticket when Uptime Robot flags a site outage. Include details like the time of the incident and the affected URL, streamlining your incident response process. diff --git a/components/uptime_robot/package.json b/components/uptime_robot/package.json new file mode 100644 index 0000000000000..7d871f328c718 --- /dev/null +++ b/components/uptime_robot/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/uptime_robot", + "version": "0.6.0", + "description": "Pipedream uptime_robot Components", + "main": "uptime_robot.app.mjs", + "keywords": [ + "pipedream", + "uptime_robot" + ], + "homepage": "https://pipedream.com/apps/uptime_robot", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/uptimerobot/.gitignore b/components/uptimerobot/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/uptimerobot/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/uptimerobot/README.md b/components/uptimerobot/README.md index a1b7a4994efc5..6470840da5a54 100644 --- a/components/uptimerobot/README.md +++ b/components/uptimerobot/README.md @@ -1,21 +1,11 @@ # Overview -Using UptimeRobot API, you can build various tools and automation for server -monitoring, memory and performance tracking, and more. With fewer resources, -you can streamline the process of gathering insights to quickly identify -potential problems—freeing up more time to focus on delivering an optimal -service. +The UptimeRobot API allows you to monitor the uptime of websites and services, sending alerts for any downtime detected. With Pipedream, you can harness this API to create tailored notifications, generate uptime reports, and synchronize with incident management tools. By leveraging Pipedream's capabilities, you can integrate UptimeRobot with a myriad of services to automate responses to uptime changes, streamline communication processes, and maintain a pulse on your web infrastructure's health. -The UptimeRobot API can help you: +# Example Use Cases -- Monitor web servers and connected systems -- Receive real-time notifications of any potential problems -- Access detailed performance data such as response time, latency, number of - requests, and more -- Automate tests for alerts and record changes in states -- Analyse trends over time, such as memory or disk usage -- Automate backups or alerts when disk space is low -- Create automatic scaling policies for improved resource allocation -- Monitor system resources and usage over time to detect outages -- Analyze API or network performance across distributed regions -- Automate the rollout of new code releases with zero downtime +- **Downtime Incident Logging to Google Sheets**: Automate the logging of any downtime incidents detected by UptimeRobot directly into a Google Sheets spreadsheet. This workflow enables you to maintain a historical record of outages for analysis and reporting, helping you to spot trends and identify recurring issues. + +- **Slack Notification on Downtime**: When UptimeRobot flags a site as down, instantly trigger a notification to a designated Slack channel. This rapid communication ensures that your team can react quickly to resolve the issue, minimizing downtime and keeping everyone in the loop. + +- **Synchronize Downtime Events with Jira**: Create a Jira ticket automatically whenever UptimeRobot detects a website is down. This workflow streamlines the task management process, ensuring that each incident is tracked and assigned to a team member, facilitating a structured response to site outages. diff --git a/components/uptimerobot/actions/create-alert-contact/create-alert-contact.mjs b/components/uptimerobot/actions/create-alert-contact/create-alert-contact.mjs new file mode 100644 index 0000000000000..0c86a2de27b7f --- /dev/null +++ b/components/uptimerobot/actions/create-alert-contact/create-alert-contact.mjs @@ -0,0 +1,60 @@ +import constants from "../../common/constants.mjs"; +import app from "../../uptimerobot.app.mjs"; + +export default { + key: "uptimerobot-create-alert-contact", + name: "Create Alert Contact", + description: "Create a new alert contact. [See the documentation](https://uptimerobot.com/api/).", + version: "0.0.1", + type: "action", + props: { + app, + type: { + type: "string", + label: "Alert Contact Type", + description: "The type of the alert contact.", + options: Object.values(constants.ALERT_CONTACT_TYPE), + default: constants.ALERT_CONTACT_TYPE.EMAIL.value, + }, + friendlyName: { + description: "A friendly name for the alert contact.", + propDefinition: [ + app, + "friendlyName", + ], + }, + value: { + type: "string", + label: "Value", + description: "Alert contact's email address Eg. `user@uptimerobot.com`, phone Eg. `12345678910` (with country code), username, url Eg. `https://example.com/webhook/` or api key Eg. `dXB0aW1lcm9ib3Q=` depending on the alert contact type.", + }, + }, + methods: { + createAlertContact(args = {}) { + return this.app.post({ + path: "/newAlertContact", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createAlertContact, + type, + friendlyName, + value, + } = this; + + const response = await createAlertContact({ + $, + data: { + type, + friendly_name: friendlyName, + value, + }, + }); + + $.export("$summary", "Successfully created the alert contact."); + return response; + }, +}; diff --git a/components/uptimerobot/actions/create-monitor/create-monitor.mjs b/components/uptimerobot/actions/create-monitor/create-monitor.mjs new file mode 100644 index 0000000000000..de8fda64b38ed --- /dev/null +++ b/components/uptimerobot/actions/create-monitor/create-monitor.mjs @@ -0,0 +1,269 @@ +import constants from "../../common/constants.mjs"; +import app from "../../uptimerobot.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "uptimerobot-create-monitor", + name: "Create Monitor", + description: "Create a new monitor. [See the documentation](https://uptimerobot.com/api/).", + version: "0.0.1", + type: "action", + props: { + app, + type: { + type: "string", + label: "Monitor Type", + description: "The type of the monitor.", + reloadProps: true, + default: constants.MONITOR_TYPE.PING.value, + options: Object.values(constants.MONITOR_TYPE), + }, + friendlyName: { + description: "A friendly name for the monitor.", + propDefinition: [ + app, + "friendlyName", + ], + }, + url: { + type: "string", + label: "URL, IP Or Host", + description: "The URL, IP address or Host to monitor.", + }, + interval: { + type: "string", + label: "Monitor Interval", + description: "The interval for the monitoring check in seconds. It is recommended to use at least 1-minute checks [available in paid plans](https://app.uptimerobot.com/billing/pricing).", + default: String(5 * 60), + options: [ + { + label: "5 Minutes", + value: String(5 * 60), + }, + { + label: "30 Minutes", + value: String(30 * 60), + }, + { + label: "1 Hour", + value: String(60 * 60), + }, + { + label: "12 Hours", + value: String(12 * 60 * 60), + }, + { + label: "1 Day", + value: String(24 * 60 * 60), + }, + ], + }, + alertContacts: { + type: "string[]", + label: "Alert Contacts", + propDefinition: [ + app, + "alertContact", + ], + }, + }, + additionalProps() { + const { + type: monitorType, + subType, + } = this; + + const timeout = { + type: "string", + label: "Request Timeout", + description: "The request timeout. The shorter the timeout the earlier we mark website as down.", + default: "30", + options: [ + { + label: "1 Second", + value: "1", + }, + { + label: "15 Seconds", + value: "15", + }, + { + label: "30 Seconds", + value: "30", + }, + { + label: "45 Seconds", + value: "45", + }, + { + label: "1 Minute", + value: "60", + }, + ], + }; + + const authProps = { + httpAuthType: { + type: "string", + label: "HTTP Auth Type", + description: "The HTTP auth type for the monitor.", + optional: true, + options: [ + { + label: "HTTP Basic", + value: "1", + }, + { + label: "Digest", + value: "2", + }, + ], + }, + httpUsername: { + type: "string", + label: "HTTP Username", + description: "The HTTP username for the monitor.", + optional: true, + }, + httpPassword: { + type: "string", + label: "HTTP Password", + description: "The HTTP password for the monitor.", + optional: true, + }, + }; + + if (monitorType === constants.MONITOR_TYPE.PORT.value) { + return { + timeout, + subType: { + type: "string", + label: "Port Type", + description: "The type of the port.", + options: Object.values(constants.PORT_TYPE), + default: constants.PORT_TYPE.HTTP.value, + reloadProps: true, + }, + ...(subType === constants.PORT_TYPE.CUSTOM.value && { + port: { + type: "string", + label: "Port", + description: "The port number to monitor.", + }, + }), + }; + } + + if (monitorType === constants.MONITOR_TYPE.KEYWORD.value) { + return { + keywordValue: { + type: "string", + label: "Keyword Value", + description: "The keyword must be present in the response HTML source. You can use HTML markup, too. Eg. `apple` or `Out of stock`.", + }, + keywordType: { + type: "string", + label: "Keyword Type", + description: "The keyword type of the monitor.", + default: "1", + options: [ + { + label: "Start incident when keyword exists", + value: "1", + }, + { + label: "Start incident when keyword does not exist", + value: "2", + }, + ], + }, + keywordCaseType: { + type: "string", + label: "Keyword Case Type", + description: "The keyword case type of the monitor.", + default: "1", + options: [ + { + label: "Case Sensitive", + value: "0", + }, + { + label: "Case Insensitive", + value: "1", + }, + ], + }, + ...authProps, + }; + } + + if (monitorType === constants.MONITOR_TYPE.HTTPS.value) { + return { + timeout, + ...authProps, + }; + } + + return {}; + }, + methods: { + formatAlertContacts(alertContacts, useDefaultThresholdAndRecurrence = true) { + const threshold = 0; + const recurrence = 0; + return utils.parseArray(alertContacts) + ?.map((value) => useDefaultThresholdAndRecurrence + ? `${value}_${threshold}_${recurrence}` + : value) + .join("-"); + }, + createMonitor(args = {}) { + return this.app.post({ + path: "/newMonitor", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createMonitor, + formatAlertContacts, + type, + friendlyName, + url, + interval, + alertContacts, + timeout, + subType, + port, + keywordType, + keywordValue, + keywordCaseType, + httpUsername, + httpPassword, + httpAuthType, + } = this; + + const response = await createMonitor({ + $, + data: { + friendly_name: friendlyName, + url, + type, + interval, + alert_contacts: formatAlertContacts(alertContacts), + timeout, + sub_type: subType, + port, + keyword_type: keywordType, + keyword_value: keywordValue, + keyword_case_type: keywordCaseType, + http_username: httpUsername, + http_password: httpPassword, + http_auth_type: httpAuthType, + }, + }); + + $.export("$summary", `Successfully created monitor with ID \`${response.monitor.id}\`.`); + return response; + }, +}; diff --git a/components/uptimerobot/actions/update-monitor-status/update-monitor-status.mjs b/components/uptimerobot/actions/update-monitor-status/update-monitor-status.mjs new file mode 100644 index 0000000000000..83d9ca430fec4 --- /dev/null +++ b/components/uptimerobot/actions/update-monitor-status/update-monitor-status.mjs @@ -0,0 +1,41 @@ +import app from "../../uptimerobot.app.mjs"; + +export default { + key: "uptimerobot-update-monitor-status", + name: "Update Monitor Status", + description: "Update an existing monitor's status to pause or resume monitoring. [See the documentation](https://uptimerobot.com/api/).", + version: "0.0.1", + type: "action", + props: { + app, + monitorId: { + propDefinition: [ + app, + "monitorId", + ], + }, + status: { + propDefinition: [ + app, + "status", + ], + }, + }, + async run({ $ }) { + const { + app, + monitorId, + status, + } = this; + + const response = await app.updateMonitor({ + $, + data: { + id: monitorId, + status, + }, + }); + $.export("$summary", "Successfully updated the monitor status."); + return response; + }, +}; diff --git a/components/uptimerobot/app/uptimerobot.app.ts b/components/uptimerobot/app/uptimerobot.app.ts deleted file mode 100644 index 729cf8f05f3f3..0000000000000 --- a/components/uptimerobot/app/uptimerobot.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "uptimerobot", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); diff --git a/components/uptimerobot/common/constants.mjs b/components/uptimerobot/common/constants.mjs new file mode 100644 index 0000000000000..1801f3a48f4fd --- /dev/null +++ b/components/uptimerobot/common/constants.mjs @@ -0,0 +1,175 @@ +const BASE_URL = "https://api.uptimerobot.com"; +const VERSION_PATH = "/v2"; +const DEFAULT_LIMIT = 50; + +const MONITOR_TYPE = { + PING: { + label: "Ping", + value: "3", + }, + PORT: { + label: "Port", + value: "4", + }, + HTTPS: { + label: "HTTP(s)", + value: "1", + }, + KEYWORD: { + label: "Keyword", + value: "2", + }, +}; + +const ALERT_CONTACT_STATUS = { + NOT_ACTIVATED: { + label: "Not Activated", + value: "0", + }, + PAUSED: { + label: "Paused", + value: "1", + }, + ACTIVE: { + label: "Active", + value: "2", + }, +}; + +const PORT_TYPE = { + HTTP: { + label: "HTTP (80)", + value: "1", + }, + HTTPS: { + label: "HTTPS (443)", + value: "2", + }, + FTP: { + label: "FTP (21)", + value: "3", + }, + SMTP: { + label: "SMTP (25)", + value: "4", + }, + POP3: { + label: "POP3 (110)", + value: "5", + }, + IMAP: { + label: "IMAP (143)", + value: "6", + }, + CUSTOM: { + label: "Custom Port", + value: "99", + }, +}; + +const ALERT_CONTACT_TYPE = { + SMS: { + label: "SMS", + value: "1", + }, + EMAIL: { + label: "E-mail", + value: "2", + }, + TWITTER: { + label: "Twitter", + value: "3", + }, + WEB_HOOK: { + label: "Web-hook", + value: "5", + }, + PUSHBULLET: { + label: "Pushbullet", + value: "6", + }, + ZAPIER: { + label: "Zapier", + value: "7", + }, + PRO_SMS: { + label: "Pro SMS", + value: "8", + }, + PUSHOVER: { + label: "Pushover", + value: "9", + }, + SLACK: { + label: "Slack", + value: "11", + }, + VOICE_CALL: { + label: "Voice Call", + value: "14", + }, + SPLUNK: { + label: "Splunk", + value: "15", + }, + PAGERDUTY: { + label: "Pagerduty", + value: "16", + }, + OPSGENIE: { + label: "Opsgenie", + value: "17", + }, + MS_TEAMS: { + label: "MS Teams", + value: "20", + }, + GOOGLE_CHAT: { + label: "Google Chat", + value: "21", + }, + DISCORD: { + label: "Discord", + value: "23", + }, +}; + +const ALERT_CONTACT_TYPE_VALUE_MAP = { + "1": "sms", + "2": "e-mail", + "3": "twitter", + "5": "web-hook", + "6": "pushbullet", + "7": "zapier", + "8": "pro-sms", + "9": "pushover", + "11": "slack", + "14": "voice-call", + "15": "splunk", + "16": "pagerduty", + "17": "opsgenie", + "20": "ms-teams", + "21": "google-chat", + "23": "discord", +}; + +const PORT_VALUE_MAP = { + "1": 80, + "2": 443, + "3": 21, + "4": 25, + "5": 110, + "6": 143, +}; + +export default { + BASE_URL, + VERSION_PATH, + MONITOR_TYPE, + DEFAULT_LIMIT, + ALERT_CONTACT_TYPE, + ALERT_CONTACT_TYPE_VALUE_MAP, + ALERT_CONTACT_STATUS, + PORT_TYPE, + PORT_VALUE_MAP, +}; diff --git a/components/uptimerobot/common/utils.mjs b/components/uptimerobot/common/utils.mjs new file mode 100644 index 0000000000000..89f755940d3e2 --- /dev/null +++ b/components/uptimerobot/common/utils.mjs @@ -0,0 +1,52 @@ +import { ConfigurationError } from "@pipedream/platform"; + +const parseJson = (input) => { + const parse = (value) => { + if (typeof(value) === "string") { + try { + return parseJson(JSON.parse(value)); + } catch (e) { + return value; + } + } else if (typeof(value) === "object" && value !== null) { + return Object.entries(value) + .reduce((acc, [ + key, + val, + ]) => Object.assign(acc, { + [key]: parse(val), + }), {}); + } + return value; + }; + + return parse(input); +}; + +function parseArray(value) { + try { + if (!value) { + return; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +export default { + parseArrayAndMap: (value) => parseArray(value)?.map(parseJson), + parseArray, +}; diff --git a/components/uptimerobot/package.json b/components/uptimerobot/package.json index 7d4b1fe9a0fc7..5fbe385d85d28 100644 --- a/components/uptimerobot/package.json +++ b/components/uptimerobot/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/uptimerobot", - "version": "0.0.4", + "version": "0.1.0", "description": "Pipedream UptimeRobot Components", "main": "dist/app/uptimerobot.app.mjs", "keywords": [ @@ -14,5 +14,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" } } diff --git a/components/uptimerobot/uptimerobot.app.mjs b/components/uptimerobot/uptimerobot.app.mjs new file mode 100644 index 0000000000000..850abaad88bfb --- /dev/null +++ b/components/uptimerobot/uptimerobot.app.mjs @@ -0,0 +1,149 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "uptimerobot", + propDefinitions: { + alertContact: { + type: "string", + label: "Alert Contact", + description: "The alert contacts to be notified when the monitor goes up/down.", + async options({ + prevContext: { offset = 1 }, + filter = ({ status }) => String(status) === constants.ALERT_CONTACT_STATUS.ACTIVE.value, + }) { + if (offset === null) { + return []; + } + const { alert_contacts: alertContacts } = await this.getAlertContacts({ + data: { + limit: constants.DEFAULT_LIMIT, + offset, + }, + }); + return { + options: alertContacts + .filter(filter) + .map(({ + id, value, type: alertContactType, + }) => ({ + label: `${value} (${constants.ALERT_CONTACT_TYPE_VALUE_MAP[alertContactType]})`, + value: id, + })), + context: { + offset: alertContacts.length === constants.DEFAULT_LIMIT + ? offset + constants.DEFAULT_LIMIT + : null, + }, + }; + }, + }, + friendlyName: { + type: "string", + label: "Friendly Name", + description: "A friendly name for the monitor.", + }, + monitorId: { + type: "string", + label: "Monitor ID", + description: "The ID of the monitor.", + async options({ prevContext: { offset = 0 } }) { + if (offset === null) { + return []; + } + const { monitors } = await this.getMonitors({ + data: { + limit: constants.DEFAULT_LIMIT, + offset, + }, + }); + return { + options: monitors.map(({ + id: value, friendly_name: label, + }) => ({ + label, + value, + })), + context: { + offset: monitors.length === constants.DEFAULT_LIMIT + ? offset + constants.DEFAULT_LIMIT + : null, + }, + }; + }, + }, + status: { + type: "string", + label: "Status", + description: "The desired status to change the monitor to.", + options: [ + { + label: "Pause", + value: "0", + }, + { + label: "Resume", + value: "1", + }, + ], + }, + }, + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Cache-Control": "no-cache", + }; + }, + getAuthData(data) { + return { + ...data, + api_key: this.$auth.api_key, + }; + }, + async _makeRequest({ + $ = this, path, data, headers, ...args + } = {}) { + const response = await axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + data: this.getAuthData(data), + }); + + if (response.stat !== "ok") { + throw new Error(JSON.stringify(response, null, 2)); + } + + return response; + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + getAlertContacts(args = {}) { + return this.post({ + path: "/getAlertContacts", + ...args, + }); + }, + getMonitors(args = {}) { + return this.post({ + path: "/getMonitors", + ...args, + }); + }, + updateMonitor(args = {}) { + return this.post({ + path: "/editMonitor", + ...args, + }); + }, + }, +}; diff --git a/components/upviral/README.md b/components/upviral/README.md index 5924b62176420..47d9a79b9b02f 100644 --- a/components/upviral/README.md +++ b/components/upviral/README.md @@ -1,23 +1,11 @@ # Overview -UpViral provides amazing opportunities for businesses to quickly and easily -build viral marketing campaigns, referral programs, giveaways and more. It -helps marketers to acquire new leads, increase website traffic, build an -engaged customer base, and most importantly, earn more sales. The power of the -UpViral API allows you to create powerful, automated, comprehensive and -cost-effective marketing campaigns. +UpViral is a platform designed to create and run referral campaigns with ease. With the UpViral API, you can automate the collection of lead data, manage contests, and track the performance of your campaigns directly within Pipedream. This allows for a seamless integration of viral marketing efforts into your existing business processes. By leveraging the API, you can trigger actions in other apps based on campaign results, synchronize lead data with CRM systems, and even automate the distribution of rewards to contest winners. -Here is a list of some ideas of what you can do with UpViral API: +# Example Use Cases -- Create automated and comprehensive referral campaigns or as UpViral calls - them 'Viral Quests' -- Build a customer loyalty program -- Create interactive games and giveaways -- Design reward or contest campaigns -- Build a referral credit system -- Design customer onboarding programs -- Generate survey campaigns -- Develop sweepstakes -- Automatically award bonuses -- Create emails capture forms -- And many more! +- **Automated Lead Sync with CRM**: Sync new leads from UpViral campaigns directly to a CRM like Salesforce or Moskit. Each time a lead is captured in UpViral, a Pipedream workflow can automatically create or update a contact in the CRM, ensuring your sales team always has the latest information at their fingertips. + +- **Dynamic Email Follow-Ups**: Integrate UpViral with an email marketing service like SendGrid to send personalized follow-up emails. When someone enters your campaign, use Pipedream to trigger a series of targeted emails that encourage participants to share your campaign, keeping them engaged and amplifying your campaign's reach. + +- **Real-Time Analytics Dashboard**: Pipe campaign performance data from UpViral into a real-time analytics dashboard like Google Sheets or Data Studio. Use Pipedream to periodically fetch data such as referrals, leads, and conversion rates, then visualize this data to make informed decisions on the fly and adjust your marketing strategies accordingly. diff --git a/components/upwave/README.md b/components/upwave/README.md new file mode 100644 index 0000000000000..9b7ed1c2014a5 --- /dev/null +++ b/components/upwave/README.md @@ -0,0 +1,11 @@ +# Overview + +The Upwave API provides programmatic access to the Upwave project management platform, enabling users to automate tasks, sync data across various tools, and create custom integrations. On Pipedream, you can leverage this API to build powerful workflows that can add tasks, update boards, and track project progress, all in real time. With Pipedream's serverless architecture, you can set up event-driven workflows that respond to changes in Upwave or integrate with other apps to streamline your project management processes. + +# Example Use Cases + +- **Task Automation on Upwave**: Create a workflow that automatically adds new tasks to an Upwave board when a specific event occurs in another app, like a new issue being opened in GitHub or a new sales lead in Salesforce. + +- **Project Updates via Email**: Set up a Pipedream workflow to watch for changes on an Upwave board and automatically send an email notification using the Gmail app on Pipedream whenever a task status is updated, keeping team members in the loop. + +- **Daily Project Digest**: Build a daily summary generator that fetches the latest updates from Upwave, compiles them into a digest, and posts the summary to a Slack channel, ensuring everyone starts their day with the latest project developments. diff --git a/components/urlbae/README.md b/components/urlbae/README.md new file mode 100644 index 0000000000000..f48a49ef35dd7 --- /dev/null +++ b/components/urlbae/README.md @@ -0,0 +1,11 @@ +# Overview + +The UrlBae API lets you shorten URLs, making them easier to share and manage. You can generate compact links, track clicks, and gather data on link engagement within your workflows on Pipedream. By leveraging UrlBae in Pipedream, you can automate the process of creating, distributing, and analyzing short links across various platforms, enhancing both marketing efforts and user experience. + +# Example Use Cases + +- **Shorten URLs on Form Submission**: When a new form response is received via Typeform or Google Forms, automatically shorten any included URLs before storing them in a Google Sheet or sending them in an email digest. + +- **Tweet Shortened Links**: After publishing a new blog post, use the RSS Trigger to capture the URL, then shorten it with UrlBae before posting an update to Twitter with the compact link, making the tweet cleaner and tracking click-through rates. + +- **Aggregate Click Data**: Schedule a Pipedream job to periodically retrieve click statistics for your UrlBae short links and save them to a Data Store. Use this data to create a dashboard in Google Data Studio or Tableau, giving insights into link performance. diff --git a/components/urlbox_io/README.md b/components/urlbox_io/README.md new file mode 100644 index 0000000000000..0aa591a50e5c0 --- /dev/null +++ b/components/urlbox_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The Urlbox.io API lets you capture live, high-quality screenshots of web pages programmatically, offering a range of customization options such as setting the viewport size, format, and full-page capture. On Pipedream, this powerful tool can be integrated into workflows to automate screenshot capture for archiving, monitoring, and reporting tasks, or as part of a larger data collection and analysis pipeline. + +# Example Use Cases + +- **Website Change Monitoring**: Set up a Pipedream workflow that periodically triggers the Urlbox.io API to take screenshots of a website. Compare these snapshots to detect changes or updates, and automatically alert you via email using the Pipedream Email app when significant changes are detected. + +- **SEO and Archival Snapshots**: Create a workflow where Urlbox.io captures screenshots of your published content or competitors' pages. This can be used for SEO tracking or archiving purposes. Integrate with Google Sheets on Pipedream to log the screenshot URLs and timestamps for a growing historical record. + +- **Social Media Share Previews**: Automate the process of generating URL previews for social media posts. When a new article or product is added to your CMS (like WordPress), trigger a Urlbox.io screenshot and format it for optimal social sharing, then post directly to your social platforms such as Twitter using their respective Pipedream app integrations. diff --git a/components/uscreen/README.md b/components/uscreen/README.md new file mode 100644 index 0000000000000..7d8b5f20565c7 --- /dev/null +++ b/components/uscreen/README.md @@ -0,0 +1,11 @@ +# Overview + +The Uscreen API lets you programmatically access your video on demand and OTT (over-the-top) service. With this API, you can pull data about videos, users, and sales, and carry out actions like creating and updating users, and more. By leveraging the Uscreen API on Pipedream, you can automate workflows involving video content management, user engagement tracking, and sales monitoring. This can significantly cut down manual effort and streamlines operations within your Uscreen account. + +# Example Use Cases + +- **Automated User Onboarding**: Set up a workflow that listens for new user sign-ups via a webhook, then uses the Uscreen API to automatically send personalized welcome emails or SMS messages, perhaps integrating with SendGrid or Twilio for messaging capabilities. + +- **Content Syncing with Social Media**: Whenever you upload a new video to Uscreen, trigger a workflow that posts a tailored announcement with a link to the new content on your social media platforms like Twitter or Facebook, driving engagement and keeping your audience informed. + +- **Sales Reporting to Google Sheets**: Configure a scheduled workflow that regularly fetches sales and subscription data from Uscreen and populates a Google Sheets spreadsheet. This allows for easy tracking of revenue trends and the creation of visual reports without manual data entry. diff --git a/components/uscreen/package.json b/components/uscreen/package.json index db61d95fb4b8c..3cf7a60994aae 100644 --- a/components/uscreen/package.json +++ b/components/uscreen/package.json @@ -15,4 +15,4 @@ "dependencies": { "@pipedream/platform": "^1.5.1" } -} \ No newline at end of file +} diff --git a/components/user_com/README.md b/components/user_com/README.md index 92a102a600170..075365fb177cc 100644 --- a/components/user_com/README.md +++ b/components/user_com/README.md @@ -1,18 +1,11 @@ # Overview -The User.com API enables developers to build automated integrations with their -User.com CRM platform. It allows you to control the user data and use it in the -most efficient way for your product. With the User.com API you can build -innovative solutions for customer success, customer retention, and customer -engagement. +The User.com API offers a robust platform for automating personalized customer interactions and streamlining user engagement strategies. By employing this API on Pipedream, you can create intricate workflows that trigger actions based on user behavior, synchronize data across various platforms, and engage users with targeted communication. Whether you're aiming to automate marketing campaigns, optimize customer support, or create a dynamic user experience, the User.com API provides the tools to construct a tailored approach. -Here are some examples of what you can build using the User.com API: +# Example Use Cases -- Automated customer onboarding -- Advanced data segmentation -- Automated customer relationship management -- Custom workflows -- Automated customer support -- Multi-channel customer communication -- Custom account rights management -- Automated integrations with third-party services +- **User Onboarding Automation**: Automate the onboarding process by triggering tailored welcome emails through the User.com API when a new user signs up. Follow up with a series of educational emails or in-app messages, scheduled based on user actions, to enhance their understanding of your product. + +- **Customer Support Ticket Routing**: Integrate User.com with a ticketing system like Zendesk on Pipedream. When a user submits a support request via User.com, automatically create a ticket in Zendesk and tag it based on the user's profile and past interactions, ensuring that the ticket is handled by the most appropriate support team. + +- **Real-Time User Segmentation and Outreach**: Use the User.com API to segment users in real-time based on their in-app behavior. Connect with an email marketing service like Mailchimp on Pipedream to send targeted campaigns to those segments, such as special offers to users who viewed a product but did not make a purchase. diff --git a/components/userflow/README.md b/components/userflow/README.md new file mode 100644 index 0000000000000..9c34cb3232f61 --- /dev/null +++ b/components/userflow/README.md @@ -0,0 +1,11 @@ +# Overview + +The Userflow API allows you to automate and integrate the process of creating and managing in-app guides and walkthroughs. Using the API within Pipedream, you can programmatically trigger events, update user attributes, and manage flows, thereby creating a personalized user experience within your application. This opens up possibilities for syncing user data, customizing user onboarding experiences, and tracking user progress without manual intervention. + +# Example Use Cases + +- **Sync User Data from CRM to Userflow**: Automatically update user attributes in Userflow when a contact is updated in your CRM system like Hubspot or Salesforce. This keeps user data consistent across platforms and ensures personalized onboarding flows are based on the latest user information. + +- **Trigger Onboarding Flows Based on User Behavior**: Start a specific Userflow guide when a user performs a certain action within your app. For instance, if a user adds their first item to the cart in an e-commerce app, trigger a guide on how to complete their purchase. + +- **Track User Progress and Send Custom Notifications**: Monitor user progress through onboarding flows and send custom notifications or emails via apps like SendGrid or Twilio when they reach certain milestones. This can encourage users to fully engage with your app and provide additional support as needed. diff --git a/components/userlist/README.md b/components/userlist/README.md new file mode 100644 index 0000000000000..8e3c5bf0af923 --- /dev/null +++ b/components/userlist/README.md @@ -0,0 +1,11 @@ +# Overview + +The Userlist API offers a way to manage and engage with your application's user base through targeted messaging and campaigns. In Pipedream, you can leverage this API to automate user segmentation, push personalized messages, and track events to create a refined user experience. When you integrate Userlist with other apps on Pipedream, you unlock a powerful suite of functionalities for syncing user data, triggering personalized communication, and responding to user actions in real-time. + +# Example Use Cases + +- **Sync New Users from Your App to Userlist**: Automatically add new users from your database or another service like Auth0 to Userlist whenever a new user signs up. This keeps your Userlist contacts up-to-date without manual entry. + +- **Send Custom Event Triggers for User Actions**: When a user performs a specific action in your app, like upgrading their account or completing a tutorial, send a custom event to Userlist using Pipedream's HTTP request action. This can trigger targeted campaigns or in-app messages. + +- **Create Segments Based on Activity in Other Services**: Use data from services like Stripe for payment activities or Intercom for support interactions to create dynamic user segments in Userlist. For example, you might create a segment for users who recently upgraded their subscription or submitted a support ticket. diff --git a/components/usersketch/README.md b/components/usersketch/README.md new file mode 100644 index 0000000000000..f5366222ef9ca --- /dev/null +++ b/components/usersketch/README.md @@ -0,0 +1,11 @@ +# Overview + +The UserSketch API lets you pull user details like names, emails, locations, and job titles, to create realistic user personas for your apps or services. Integrating the UserSketch API with Pipedream unlocks the potential to automate the creation of these personas, enrich CRM data, or enhance user testing with minimal effort. Pipedream's serverless platform facilitates the creation of complex workflows, connecting UserSketch with a vast array of other apps and services to streamline your processes. + +# Example Use Cases + +- **Enrich CRM Contacts**: Automatically add realistic user personas from UserSketch to your CRM software. When a new lead is added, a Pipedream workflow can trigger, fetching detailed personas and appending them to the lead's profile to better inform sales strategies. + +- **Test User Scenario Generation**: Seamlessly integrate user personas into your testing environment. Each time a new feature is ready for testing, Pipedream can use the UserSketch API to generate a set of user personas, which can be used to simulate real-world usage scenarios in your app. + +- **Marketing Campaign Personalization**: Create targeted marketing campaigns using detailed personas. With each campaign initiation, a Pipedream workflow can be set to pull data from UserSketch to tailor the messaging and content for various user demographics, improving engagement rates. diff --git a/components/uservoice/README.md b/components/uservoice/README.md index 86f0759cb6464..2f3b0bc409670 100644 --- a/components/uservoice/README.md +++ b/components/uservoice/README.md @@ -1,23 +1,11 @@ # Overview -Using the UserVoice API, developers can build powerful customer service tools -to improve the service your organization provides to its customers. Here are -just a few of the possibilities: +The UserVoice API allows you to tap into customer feedback and support data to automate and enhance customer engagement processes. With the API, you can programmatically access UserVoice accounts to retrieve suggestions, tickets, and user data, allowing you to analyze customer trends, automate responses, and integrate with other customer success platforms. By leveraging Pipedream's capabilities, you can create event-driven workflows that react to UserVoice events, synchronize data across multiple services, and construct a more responsive customer feedback loop. -- Create a custom portal, allowing your customers to submit and manage their - requests, as well as track their progress, all within your own branded - domain. -- Integrate UserVoice with your existing customer service software, enabling - customers to better track their inquiries and quickly provide feedback on - their experiences. -- Design branded surveys to gather feedback from customers and gain insight - about how to improve your customer service. -- Automate customer service processes to ensure each customer's inquiries are - handled quickly and efficiently. -- Use data from the UserVoice API to gain valuable insights into your customer - service performance. +# Example Use Cases -These are just a few of the possibilities that can be accomplished through the -UserVoice API. With its comprehensive suite of features, your organization will -be able to provide exceptional customer service experiences and better serve -your customers. +- **Sync UserVoice Feedback to a CRM**: Keep your CRM up-to-date by creating a Pipedream workflow that listens for new UserVoice suggestions and automatically adds them as leads or notes to relevant contacts in Salesforce. This ensures your sales team always has the latest customer insights at their fingertips. + +- **Automate Support Ticket Escalation**: Build a workflow where Pipedream monitors UserVoice for high-severity support tickets and, upon detection, triggers an alert in Slack and assigns the ticket to a senior support agent in Jira. This expedites the resolution process for critical issues. + +- **UserVoice Analytics Dashboard**: Implement a workflow that periodically fetches data from UserVoice, such as ticket volume and resolution time, and sends it to Google Sheets. Use this data to populate a dashboard that provides real-time analytics, helping you to make informed decisions about your support strategy. diff --git a/components/usps/README.md b/components/usps/README.md new file mode 100644 index 0000000000000..b4d4f4f7d668b --- /dev/null +++ b/components/usps/README.md @@ -0,0 +1,11 @@ +# Overview + +The USPS API provides various functionalities such as tracking shipments, calculating shipping prices, and scheduling pickups, making it a valuable tool for automating logistics and e-commerce operations. Integrating the USPS API with Pipedream allows you to create serverless workflows that can enhance how you manage shipping tasks, alert systems, or update order statuses in real-time, without needing to manage infrastructure. + +# Example Use Cases + +- **Real-time Shipment Tracking Updates**: Set up a workflow that monitors the status of shipments using the USPS API and sends real-time updates via Slack or email. When a package's status changes, Pipedream triggers an event that can be routed to notify the customer or update an internal tracking system. + +- **Automated Shipping Cost Calculator**: Implement a workflow that receives order details from an e-commerce platform like Shopify, uses the USPS API to calculate shipping costs based on weight and destination, and then updates the order with the calculated shipping costs. This can streamline order processing and ensure accurate billing. + +- **Scheduled Pickup Automation**: Create a workflow that automatically schedules pickups for orders ready to ship. The workflow can trigger at specified intervals, collect ready orders from a database or service like Airtable, use the USPS API to schedule the pickups, and then log the details or notify the relevant team. diff --git a/components/usps/package.json b/components/usps/package.json new file mode 100644 index 0000000000000..3d56148748e76 --- /dev/null +++ b/components/usps/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/usps", + "version": "0.0.1", + "description": "Pipedream USPS Components", + "main": "usps.app.mjs", + "keywords": [ + "pipedream", + "usps" + ], + "homepage": "https://pipedream.com/apps/usps", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/usps/usps.app.mjs b/components/usps/usps.app.mjs new file mode 100644 index 0000000000000..9530ceecba590 --- /dev/null +++ b/components/usps/usps.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "usps", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/utradea/README.md b/components/utradea/README.md new file mode 100644 index 0000000000000..edc537e57a059 --- /dev/null +++ b/components/utradea/README.md @@ -0,0 +1,11 @@ +# Overview + +The Utradea API allows users to access financial analytics and social sentiment data for various investment opportunities. With this API, you can programmatically get insights into stock trends, market predictions, and community-driven investment analysis. When used within Pipedream, you have the ability to create automated workflows that can help you make informed investment decisions, track stock performance, or integrate with other financial tools. + +# Example Use Cases + +- **Automated Investment Alerts:** Create a workflow that triggers on specific Utradea sentiment indicators or analytics thresholds. When these are met, an alert could be sent via SMS or email, ensuring quick, informed actions on market changes. + +- **Portfolio Sync and Analysis:** Sync your investment portfolio with Utradea's analytics by periodically fetching data for your holdings. Combine this with Pipedream's ability to write to Google Sheets to track and analyze your portfolio performance over time. + +- **Social Sentiment-Driven Notifications:** Use Pipedream to listen for shifts in social sentiment or new investment ideas on Utradea. When a significant change is detected, broadcast this information through a Slack channel to keep a team or investment group informed. diff --git a/components/v7_darwin/actions/add-instructions/add-instructions.mjs b/components/v7_darwin/actions/add-instructions/add-instructions.mjs new file mode 100644 index 0000000000000..5d0defc778ca2 --- /dev/null +++ b/components/v7_darwin/actions/add-instructions/add-instructions.mjs @@ -0,0 +1,38 @@ +import app from "../../v7_darwin.app.mjs"; + +export default { + key: "v7_darwin-add-instructions", + name: "Add Instructions", + description: "Add annotator instructions. [See the documentation](https://docs.v7labs.com/reference/adding-instructions)", + version: "0.0.1", + type: "action", + props: { + app, + id: { + propDefinition: [ + app, + "id", + ], + }, + instructions: { + propDefinition: [ + app, + "instructions", + ], + description: "String of instructions. Written using HTML", + }, + }, + async run({ $ }) { + const response = await this.app.addInstructions({ + $, + dataset_id: this.id, + data: { + instructions: this.instructions, + }, + }); + + $.export("$summary", `Successfully created Instruction in dataset with ID: '${this.id}'`); + + return response; + }, +}; diff --git a/components/v7_darwin/actions/create-dataset/create-dataset.mjs b/components/v7_darwin/actions/create-dataset/create-dataset.mjs new file mode 100644 index 0000000000000..7071af13ddccd --- /dev/null +++ b/components/v7_darwin/actions/create-dataset/create-dataset.mjs @@ -0,0 +1,30 @@ +import app from "../../v7_darwin.app.mjs"; + +export default { + key: "v7_darwin-create-dataset", + name: "Create Dataset", + description: "Creates a new Dataset. [See the documentation](https://docs.v7labs.com/reference/create-dataset)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "name", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createDataset({ + $, + data: { + name: this.name, + }, + }); + + $.export("$summary", `Successfully created Dataset with ID: '${response.id}'`); + + return response; + }, +}; diff --git a/components/v7_darwin/actions/update-dataset/update-dataset.mjs b/components/v7_darwin/actions/update-dataset/update-dataset.mjs new file mode 100644 index 0000000000000..3bf581cd985a3 --- /dev/null +++ b/components/v7_darwin/actions/update-dataset/update-dataset.mjs @@ -0,0 +1,86 @@ +import app from "../../v7_darwin.app.mjs"; + +export default { + key: "v7_darwin-update-dataset", + name: "Update Dataset", + description: "Update a dataset with the specified ID. [See the documentation](https://docs.v7labs.com/reference/update-dataset)", + version: "0.0.1", + type: "action", + props: { + app, + id: { + propDefinition: [ + app, + "id", + ], + }, + annotatorsCanCreateTags: { + propDefinition: [ + app, + "annotatorsCanCreateTags", + ], + }, + annotatorsCanInstantiateWorkflows: { + propDefinition: [ + app, + "annotatorsCanInstantiateWorkflows", + ], + }, + anyoneCanDoubleAssign: { + propDefinition: [ + app, + "anyoneCanDoubleAssign", + ], + }, + instructions: { + propDefinition: [ + app, + "instructions", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + public: { + propDefinition: [ + app, + "public", + ], + }, + reviewersCanAnnotate: { + propDefinition: [ + app, + "reviewersCanAnnotate", + ], + }, + workPrioritization: { + propDefinition: [ + app, + "workPrioritization", + ], + }, + }, + async run({ $ }) { + const response = await this.app.updateDataset({ + $, + id: this.id, + data: { + annotators_can_create_tags: this.annotatorsCanCreateTags, + annotators_can_instantiate_workflows: this.annotatorsCanInstantiateWorkflows, + anyone_can_double_assign: this.anyoneCanDoubleAssign, + instructions: this.instructions, + name: this.name, + public: this.public, + reviewers_can_annotate: this.reviewersCanAnnotate, + work_prioritization: this.workPrioritization, + }, + }); + + $.export("$summary", `Successfully updated Dataset with ID: '${this.id}'`); + + return response; + }, +}; diff --git a/components/v7_darwin/common/constants.mjs b/components/v7_darwin/common/constants.mjs new file mode 100644 index 0000000000000..812517f9a0357 --- /dev/null +++ b/components/v7_darwin/common/constants.mjs @@ -0,0 +1,8 @@ +export default { + PRIORITY_OPTIONS: [ + "priority:asc", + "priority:desc", + "inserted_at:asc", + "inserted_at:asc", + ], +}; diff --git a/components/v7_darwin/package.json b/components/v7_darwin/package.json new file mode 100644 index 0000000000000..cc4bfdc856ed6 --- /dev/null +++ b/components/v7_darwin/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/v7_darwin", + "version": "0.1.0", + "description": "Pipedream V7 Darwin Components", + "main": "v7_darwin.app.mjs", + "keywords": [ + "pipedream", + "v7_darwin" + ], + "homepage": "https://pipedream.com/apps/v7_darwin", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/v7_darwin/v7_darwin.app.mjs b/components/v7_darwin/v7_darwin.app.mjs new file mode 100644 index 0000000000000..225e205b0a481 --- /dev/null +++ b/components/v7_darwin/v7_darwin.app.mjs @@ -0,0 +1,117 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "v7_darwin", + propDefinitions: { + id: { + type: "string", + label: "Dataset ID", + description: "ID of the Dataset", + async options() { + const endusersIds = await this.listDatasets(); + return endusersIds.map(({ + id, name, + }) => ({ + value: id, + label: name, + })); + }, + }, + name: { + type: "string", + label: "Name", + description: "The name of the new dataset", + }, + annotatorsCanCreateTags: { + type: "boolean", + label: "Annotators Can Create Tags", + description: "Flag to specify whether annotators are allowed to create tags for the specified dataset", + }, + annotatorsCanInstantiateWorkflows: { + type: "boolean", + label: "Annotators Can Instantiate Workflows", + description: "Flag to specify whether annotators can get assigned items in 'new' status", + }, + anyoneCanDoubleAssign: { + type: "boolean", + label: "Anyone Can Double Assign", + description: "Flag to specify whether users can be assigned to different stages in the same workflow", + }, + instructions: { + type: "string", + label: "Instructions", + description: "Dataset instructions for annotators", + }, + public: { + type: "boolean", + label: "Public", + description: "Flag to specify whether the dataset should be publicly accessible", + }, + reviewersCanAnnotate: { + type: "boolean", + label: "Reviewers Can Annotate", + description: "Flag to specify whether reviewers are allowed to create annotations in review stages", + }, + workPrioritization: { + type: "string", + label: "Work Prioritization", + description: "Specification for sorting items when annotators request new items", + options: constants.PRIORITY_OPTIONS, + }, + }, + methods: { + _baseUrl() { + return "https://darwin.v7labs.com/api"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "Authorization": `ApiKey ${this.$auth.api_key}`, + "Content-Type": "application/json", + }, + }); + }, + async createDataset(args = {}) { + return this._makeRequest({ + method: "post", + path: "/datasets", + ...args, + }); + }, + async updateDataset({ + id, ...args + }) { + return this._makeRequest({ + method: "put", + path: `/datasets/${id}`, + ...args, + }); + }, + async addInstructions({ + dataset_id, ...args + }) { + return this._makeRequest({ + method: "put", + path: `/datasets/${dataset_id}`, + ...args, + }); + }, + async listDatasets(args = {}) { + return this._makeRequest({ + path: "/datasets", + ...args, + }); + }, + }, +}; diff --git a/components/v7_go/actions/create-entity/create-entity.mjs b/components/v7_go/actions/create-entity/create-entity.mjs new file mode 100644 index 0000000000000..30d3d01c1f72b --- /dev/null +++ b/components/v7_go/actions/create-entity/create-entity.mjs @@ -0,0 +1,56 @@ +import v7Go from "../../v7_go.app.mjs"; + +export default { + key: "v7_go-create-entity", + name: "Create Entity", + description: "Triggers the creation of a new entity. [See the documentation](https://docs.go.v7labs.com/reference/create-entities-programmatically)", + version: "0.0.1", + type: "action", + props: { + v7Go, + workspaceId: { + propDefinition: [ + v7Go, + "workspaceId", + ], + }, + projectId: { + propDefinition: [ + v7Go, + "projectId", + ({ workspaceId }) => ({ + workspaceId, + }), + ], + reloadProps: true, + }, + }, + async additionalProps() { + if (this.projectId) { + return await this.v7Go.prepareProps({ + workspaceId: this.workspaceId, + projectId: this.projectId, + }); + } + }, + async run({ $ }) { + const { + v7Go, + workspaceId, + projectId, + ...fields + } = this; + + const response = await v7Go.createEntity({ + $, + workspaceId, + projectId, + data: { + fields: v7Go.parseObject(fields), + }, + }); + + $.export("$summary", `Successfully created entity with ID ${response.id}`); + return response; + }, +}; diff --git a/components/v7_go/actions/create-project/create-project.mjs b/components/v7_go/actions/create-project/create-project.mjs new file mode 100644 index 0000000000000..7c5ffeb0e3459 --- /dev/null +++ b/components/v7_go/actions/create-project/create-project.mjs @@ -0,0 +1,34 @@ +import v7Go from "../../v7_go.app.mjs"; + +export default { + key: "v7_go-create-project", + name: "Create Project", + description: "Initiates the creation of a new project with a unique project identifier. [See the documentation](https://docs.go.v7labs.com/reference/project-create)", + version: "0.0.1", + type: "action", + props: { + v7Go, + workspaceId: { + propDefinition: [ + v7Go, + "workspaceId", + ], + }, + name: { + type: "string", + label: "Name", + description: "The name of the project.", + }, + }, + async run({ $ }) { + const response = await this.v7Go.createProject({ + $, + workspaceId: this.workspaceId, + data: { + name: this.name, + }, + }); + $.export("$summary", `Successfully created project with ID ${response.id}`); + return response; + }, +}; diff --git a/components/v7_go/actions/update-entity/update-entity.mjs b/components/v7_go/actions/update-entity/update-entity.mjs new file mode 100644 index 0000000000000..f945d7a19132a --- /dev/null +++ b/components/v7_go/actions/update-entity/update-entity.mjs @@ -0,0 +1,71 @@ +import v7Go from "../../v7_go.app.mjs"; + +export default { + key: "v7_go-update-entity", + name: "Update Entity", + description: "Executes an update on an existing entity. [See the documentation](https://docs.go.v7labs.com/reference/entity-update-values)", + version: "0.0.1", + type: "action", + props: { + v7Go, + workspaceId: { + propDefinition: [ + v7Go, + "workspaceId", + ], + }, + projectId: { + propDefinition: [ + v7Go, + "projectId", + ({ workspaceId }) => ({ + workspaceId, + }), + ], + }, + entityId: { + propDefinition: [ + v7Go, + "entityId", + ({ + workspaceId, projectId, + }) => ({ + workspaceId, + projectId, + }), + ], + reloadProps: true, + }, + }, + async additionalProps() { + if (this.entityId) { + return await this.v7Go.prepareProps({ + workspaceId: this.workspaceId, + projectId: this.projectId, + entityId: this.entityId, + }); + } + }, + async run({ $ }) { + const { + v7Go, + workspaceId, + projectId, + entityId, + ...fields + } = this; + + const response = await v7Go.updateEntity({ + $, + workspaceId, + projectId, + entityId, + data: { + fields: v7Go.parseObject(fields), + }, + }); + + $.export("$summary", `Successfully updated entity with ID ${response.id}`); + return response; + }, +}; diff --git a/components/v7_go/common/constants.mjs b/components/v7_go/common/constants.mjs new file mode 100644 index 0000000000000..ea830c15a04cb --- /dev/null +++ b/components/v7_go/common/constants.mjs @@ -0,0 +1 @@ +export const LIMIT = 100; diff --git a/components/v7_go/package.json b/components/v7_go/package.json new file mode 100644 index 0000000000000..10257aacb4f1c --- /dev/null +++ b/components/v7_go/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/v7_go", + "version": "0.1.0", + "description": "Pipedream V7 Go Components", + "main": "v7_go.app.mjs", + "keywords": [ + "pipedream", + "v7_go" + ], + "homepage": "https://pipedream.com/apps/v7_go", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/v7_go/sources/common/base.mjs b/components/v7_go/sources/common/base.mjs new file mode 100644 index 0000000000000..4637d081dfda9 --- /dev/null +++ b/components/v7_go/sources/common/base.mjs @@ -0,0 +1,58 @@ +import v7Go from "../../v7_go.app.mjs"; + +export default { + props: { + v7Go, + http: { + type: "$.interface.http", + customResponse: false, + }, + db: "$.service.db", + workspaceId: { + propDefinition: [ + v7Go, + "workspaceId", + ], + }, + projectId: { + propDefinition: [ + v7Go, + "projectId", + ({ workspaceId }) => ({ + workspaceId, + }), + ], + }, + }, + hooks: { + async activate() { + const data = await this.v7Go.createWebhook({ + workspaceId: this.workspaceId, + data: { + action: { + type: "webhook", + url: this.http.endpoint, + }, + events: this.getEvents(), + project_id: this.projectId, + }, + }); + this.db.set("webhookId", data.id); + }, + async deactivate() { + const webhookId = this.db.get("webhookId"); + await this.v7Go.deleteWebhook({ + workspaceId: this.workspaceId, + webhookId, + }); + }, + }, + async run({ body }) { + const ts = Date.parse(new Date()); + this.$emit(body, { + id: `${body.entity.id}-${ts}`, + summary: this.getSummary(body), + ts: ts, + }); + }, +}; diff --git a/components/v7_go/sources/complete-entity-instant/complete-entity-instant.mjs b/components/v7_go/sources/complete-entity-instant/complete-entity-instant.mjs new file mode 100644 index 0000000000000..4d1ba5b2cc6ab --- /dev/null +++ b/components/v7_go/sources/complete-entity-instant/complete-entity-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "v7_go-complete-entity-instant", + name: "New Complete Entity (Instant)", + description: "Emit new event when all fields of an entity are completed in V7 Go.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return { + "entity.all_fields_completed": true, + }; + }, + getSummary({ entity }) { + return `Entity all fields completed: ${entity.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/v7_go/sources/complete-entity-instant/test-event.mjs b/components/v7_go/sources/complete-entity-instant/test-event.mjs new file mode 100644 index 0000000000000..b8c4e359b951c --- /dev/null +++ b/components/v7_go/sources/complete-entity-instant/test-event.mjs @@ -0,0 +1,25 @@ +export default { + "type": "entity.all_fields_completed", + "entity": { + "id": "12345678-1234-1234-1234-123456789012", + "fields": { + "text-field-test": { + "data": { + "value": "field value", + "value_type": "manual", + "additional_value": null, + "additional_value_type": null + }, + "status": "complete", + "error_message": null, + "inserted_at": "2024-07-23T14:28:53.480497Z", + "updated_at": "2024-07-23T14:28:55.255082Z", + "property_id": "12345678-1234-1234-1234-123456789012", + "property_type": "text" + } + }, + "inserted_at": "2024-07-23T14:28:53.479981Z", + "updated_at": "2024-07-23T14:28:53.479981Z", + "project_id": "12345678-1234-1234-1234-123456789012" + } +} \ No newline at end of file diff --git a/components/v7_go/sources/complete-field-instant/complete-field-instant.mjs b/components/v7_go/sources/complete-field-instant/complete-field-instant.mjs new file mode 100644 index 0000000000000..7fe501692cb5b --- /dev/null +++ b/components/v7_go/sources/complete-field-instant/complete-field-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "v7_go-complete-field-instant", + name: "New Field Completion (Instant)", + description: "Emit new event when a field within an entity is completed in V7 Go.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return { + "entity.field_completed": true, + }; + }, + getSummary({ entity }) { + return `Field completed for entity ${entity.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/v7_go/sources/complete-field-instant/test-event.mjs b/components/v7_go/sources/complete-field-instant/test-event.mjs new file mode 100644 index 0000000000000..073bb0ead5bc3 --- /dev/null +++ b/components/v7_go/sources/complete-field-instant/test-event.mjs @@ -0,0 +1,39 @@ +export default { + "type": "entity.field_completed", + "entity": { + "id": "12345678-1234-1234-1234-123456789012", + "fields": { + "text-field-test-1": { + "data": { + "value": "field value", + "value_type": "manual", + "additional_value": null, + "additional_value_type": null + }, + "status": "complete", + "error_message": null, + "inserted_at": "2024-07-23T14:35:21.061761Z", + "updated_at": "2024-07-23T14:35:37.057732Z", + "property_id": "12345678-1234-1234-1234-123456789012", + "property_type": "text" + }, + "text-field-test-2": { + "data": { + "value": "field value", + "value_type": "manual", + "additional_value": null, + "additional_value_type": null + }, + "status": "complete", + "error_message": null, + "inserted_at": "2024-07-23T14:28:53.480497Z", + "updated_at": "2024-07-23T14:28:55.255082Z", + "property_id": "12345678-1234-1234-1234-123456789012", + "property_type": "text" + } + }, + "inserted_at": "2024-07-23T14:28:53.479981Z", + "updated_at": "2024-07-23T14:28:53.479981Z", + "project_id": "12345678-1234-1234-1234-123456789012" + } +} \ No newline at end of file diff --git a/components/v7_go/sources/new-entity-instant/new-entity-instant.mjs b/components/v7_go/sources/new-entity-instant/new-entity-instant.mjs new file mode 100644 index 0000000000000..145e600432d58 --- /dev/null +++ b/components/v7_go/sources/new-entity-instant/new-entity-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "v7_go-new-entity-instant", + name: "New Entity Created (Instant)", + description: "Emit new event when an entity is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return { + "entity.created": true, + }; + }, + getSummary({ entity }) { + return `New entity created: ${entity.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/v7_go/sources/new-entity-instant/test-event.mjs b/components/v7_go/sources/new-entity-instant/test-event.mjs new file mode 100644 index 0000000000000..b2744b64d59d9 --- /dev/null +++ b/components/v7_go/sources/new-entity-instant/test-event.mjs @@ -0,0 +1,72 @@ +export default { + "type": "entity.created", + "entity": { + "id": "12345678-1234-1234-1234-123456789012", + "fields": { + "text-field-test-1": { + "data": { + "value": null, + "value_type": null, + "additional_value": null, + "additional_value_type": null + }, + "status": "idle", + "error_message": null, + "inserted_at": "2024-07-23T14:39:40.528059Z", + "updated_at": "2024-07-23T14:39:40.528059Z", + "property_id": "12345678-1234-1234-1234-123456789012", + "property_type": "text" + }, + "text-field-test-2": { + "data": { + "value": null, + "value_type": null, + "additional_value": null, + "additional_value_type": null + }, + "status": "idle", + "error_message": null, + "inserted_at": "2024-07-23T14:39:40.527282Z", + "updated_at": "2024-07-23T14:39:40.527282Z", + "property_id": "12345678-1234-1234-1234-123456789012", + "property_type": "text" + } + }, + "inserted_at": "2024-07-23T14:39:40.526627Z", + "updated_at": "2024-07-23T14:39:40.526627Z", + "project_id": "12345678-1234-1234-1234-123456789012" + }, + "project": { + "id": "12345678-1234-1234-1234-123456789012", + "name": "Project test 01", + "inserted_at": "2024-07-22T16:24:26.921584Z", + "updated_at": "2024-07-23T14:35:21.101723Z", + "properties": { + "text-field-test-1": { + "id": "12345678-1234-1234-1234-123456789012", + "name": "New property", + "type": "text", + "description": null, + "inserted_at": "2024-07-23T14:35:21.064456Z", + "updated_at": "2024-07-23T14:35:21.064456Z", + "inputs": [], + "tool": "gpt_4o", + "input_ids": [], + "slug": "text-field-test-1" + }, + "text-field-test-2": { + "id": "12345678-1234-1234-1234-123456789012", + "name": "Text Field", + "type": "text", + "description": null, + "inserted_at": "2024-07-22T17:43:13.126685Z", + "updated_at": "2024-07-22T17:50:56.293517Z", + "inputs": [], + "tool": "gpt_4o", + "input_ids": [], + "slug": "text-field-test-2" + } + }, + "workspace_id": "12345678-1234-1234-1234-123456789012" + } +} \ No newline at end of file diff --git a/components/v7_go/v7_go.app.mjs b/components/v7_go/v7_go.app.mjs new file mode 100644 index 0000000000000..af0b8063e007c --- /dev/null +++ b/components/v7_go/v7_go.app.mjs @@ -0,0 +1,253 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "v7_go", + propDefinitions: { + workspaceId: { + type: "string", + label: "Workspace ID", + description: "The ID of the workspace", + async options() { + const { data } = await this.listWorkspaces(); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + projectId: { + type: "string", + label: "Project ID", + description: "The ID of the project", + async options({ + page, workspaceId, + }) { + const { data } = await this.listProjects({ + workspaceId, + params: { + offset: LIMIT * page, + limit: LIMIT, + }, + }); + + return data.map(({ + id: value, name, + }) => ({ + label: name || value, + value, + })); + }, + }, + entityId: { + type: "string", + label: "Entity ID", + description: "The ID of the entity", + async options({ + page, workspaceId, projectId, + }) { + const { data } = await this.listEntities({ + workspaceId, + projectId, + params: { + offset: LIMIT * page, + limit: LIMIT, + }, + }); + + return data.map(({ id }) => id); + }, + }, + }, + methods: { + _baseUrl() { + return "https://go.v7labs.com/api"; + }, + _headers() { + return { + "X-API-KEY": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createProject({ + workspaceId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/workspaces/${workspaceId}/projects`, + ...opts, + }); + }, + createEntity({ + workspaceId, projectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/workspaces/${workspaceId}/projects/${projectId}/entities`, + ...opts, + }); + }, + getEntity({ + workspaceId, projectId, entityId, + }) { + return this._makeRequest({ + path: `/workspaces/${workspaceId}/projects/${projectId}/entities/${entityId}`, + }); + }, + getProject({ + workspaceId, projectId, + }) { + return this._makeRequest({ + path: `/workspaces/${workspaceId}/projects/${projectId}`, + }); + }, + updateEntity({ + workspaceId, projectId, entityId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/workspaces/${workspaceId}/projects/${projectId}/entities/${entityId}`, + ...opts, + }); + }, + listEntities({ + workspaceId, projectId, ...opts + }) { + return this._makeRequest({ + method: "GET", + path: `/workspaces/${workspaceId}/projects/${projectId}/entities`, + ...opts, + }); + }, + listProjects({ + workspaceId, ...opts + }) { + return this._makeRequest({ + method: "GET", + path: `/workspaces/${workspaceId}/projects`, + ...opts, + }); + }, + listWorkspaces() { + return this._makeRequest({ + method: "GET", + path: "/workspaces", + }); + }, + parseObject(object) { + const arrayKeys = Object.keys(object); + return arrayKeys.reduce((prev, curr) => { + let value = object[curr]; + const prefix = curr.split("_"); + + switch (prefix[0]) { + case "SINGLE" : value = { + options: [ + value, + ], + }; break; + case "MULTI" : value = { + options: value, + }; break; + case "URL" : value = { + url: value, + }; break; + case "FILE" : value = { + file_url: value, + }; break; + } + + return { + ...prev, + [prefix[1]]: value, + }; + }, {}); + }, + async prepareProps({ + projectId, workspaceId, entityId, + }) { + const props = {}; + let fields; + const { properties } = await this.getProject({ + workspaceId, + projectId, + }); + + if (entityId) { + const entityProps = await this.getEntity({ + workspaceId, + projectId, + entityId, + }); + fields = entityProps.fields; + } + + for (const prop of properties) { + if (prop.type === "collection") continue; + + const type = prop.type === "multi_select" + ? "string[]" + : "string"; + + let responseType = "STRING"; + + switch (prop.type) { + case "single_select": responseType = "SINGLE"; break; + case "multi_select": responseType = "MULTI"; break; + case "url": responseType = "URL"; break; + case "file": responseType = "FILE"; break; + } + + props[`${responseType}_${prop.id}`] = { + type, + label: prop.name, + optional: true, + }; + + if (entityId) { + if (fields[prop.slug].manual_value?.value) { + const value = fields[prop.slug].manual_value.value; + const parsedValue = prop.type === "single_select" + ? value.join() + : value; + props[`${responseType}_${prop.id}`].default = parsedValue; + } + } + + if (prop.config?.options) { + props[`${responseType}_${prop.id}`].options = prop.config.options.map(({ value }) => value); + } + } + return props; + }, + createWebhook({ + workspaceId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/workspaces/${workspaceId}/triggers`, + ...opts, + }); + }, + deleteWebhook({ + workspaceId, webhookId, + }) { + return this._makeRequest({ + method: "DELETE", + path: `/workspaces/${workspaceId}/triggers/${webhookId}`, + }); + }, + }, +}; diff --git a/components/vapi/README.md b/components/vapi/README.md new file mode 100644 index 0000000000000..19ed105b09e7f --- /dev/null +++ b/components/vapi/README.md @@ -0,0 +1,11 @@ +# Overview + +The Vapi API delivers voice automation capabilities, letting you build powerful voice response systems. With Vapi, you can automate calls, send voice messages, and create dynamic interactions through speech recognition and text-to-speech. Pipedream's serverless platform allows you to integrate Vapi's API with numerous other services to automate workflows, react to events, and orchestrate complex voice-enabled processes. + +# Example Use Cases + +- **Automated Customer Support Calls**: Trigger a Vapi-powered call flow when a customer submits a support ticket via your helpdesk platform. Use the call to collect additional information or provide immediate assistance, updating the ticket with the call details once complete. + +- **Voice Survey Campaigns**: Launch a survey campaign where responses are collected via phone calls. Set up a workflow that initiates Vapi calls to a list of contacts, captures their voice responses, and stores the results in a database for analysis. + +- **Event Reminder Calls**: Combine Vapi with a calendar app to send out voice call reminders a day before scheduled events. Include details such as event time and location, and offer recipients options to confirm attendance or reschedule. diff --git a/components/vapi/actions/create-call/create-call.mjs b/components/vapi/actions/create-call/create-call.mjs new file mode 100644 index 0000000000000..b229065b781e4 --- /dev/null +++ b/components/vapi/actions/create-call/create-call.mjs @@ -0,0 +1,63 @@ +import { ConfigurationError } from "@pipedream/platform"; +import vapi from "../../vapi.app.mjs"; + +export default { + key: "vapi-create-call", + name: "Create Call", + description: "Starts a new conversation with an assistant. [See the documentation](https://docs.vapi.ai/api-reference/calls/create)", + version: "0.0.1", + type: "action", + props: { + vapi, + name: { + type: "string", + label: "Conversation Name", + description: "Name of the new conversation", + optional: true, + }, + assistantId: { + propDefinition: [ + vapi, + "assistantId", + ], + optional: true, + }, + squadId: { + propDefinition: [ + vapi, + "squadId", + ], + optional: true, + }, + phoneNumberId: { + propDefinition: [ + vapi, + "phoneNumberId", + ], + }, + customerId: { + type: "string", + label: "Customer ID", + description: "ID of the customer for the conversation", + optional: true, + }, + }, + async run({ $ }) { + if (!this.assistantId && !this.squadId) { + throw new ConfigurationError("Specify either `Assistant Id` or `Squad Id`"); + } + + const response = await this.vapi.startConversation({ + $, + data: { + assistantId: this.assistantId, + squadId: this.squadId, + phoneNumberId: this.phoneNumberId, + name: this.name, + customerId: this.customerId, + }, + }); + $.export("$summary", `Conversation created with ID ${response.id}`); + return response; + }, +}; diff --git a/components/vapi/actions/update-assistant-settings/update-assistant-settings.mjs b/components/vapi/actions/update-assistant-settings/update-assistant-settings.mjs new file mode 100644 index 0000000000000..fd41e471c477b --- /dev/null +++ b/components/vapi/actions/update-assistant-settings/update-assistant-settings.mjs @@ -0,0 +1,265 @@ +import { + BACKGROUND_SOUND, + CLIENT_MESSAGE_OPTIONS, + FIRST_MESSAGE_MODE_OPTIONS, + SERVER_MESSAGE_OPTIONS, +} from "../../common/constants.mjs"; +import { + clearObj, + parseObject, +} from "../../common/utils.mjs"; +import vapi from "../../vapi.app.mjs"; + +export default { + key: "vapi-update-assistant-settings", + name: "Update Assistant Settings", + description: "Updates the configuration settings for a specific assistant. [See the documentation](https://docs.vapi.ai/api-reference/assistants/update)", + version: "0.0.1", + type: "action", + props: { + vapi, + assistantId: { + propDefinition: [ + vapi, + "assistantId", + ], + }, + transcriber: { + type: "object", + label: "Transcriber", + description: "A formatted JSON object for the assistant's transcriber. **Example: { \"provider\": \"talkscriber\", \"language\": \"en\", \"model\": \"whisper\" }**. [See the documentation](https://docs.vapi.ai/api-reference/assistants/update) for further details", + optional: true, + }, + model: { + type: "object", + label: "Model", + description: "A formatted JSON object for the assistant's LLM. **Example: {\"provider\": \"xai\", \"model\": \"grok-beta\", \"emotionRecognitionEnabled\": true, \"knowledgeBase\": {\"server\": {\"url\": \"url\", \"timeoutSeconds\": 20}}, \"knowledgeBaseId\": \"model\", \"maxTokens\": 1.1, \"messages\": [{\"role\": \"assistant\"}], \"numFastTurns\": 1.1, \"temperature\": 1.1, \"toolIds\": [\"model\"], \"tools\": [{\"type\": \"transferCall\", \"async\": false}]}**. [See the documentation](https://docs.vapi.ai/api-reference/assistants/update) for further details", + optional: true, + }, + voice: { + type: "object", + label: "Voice", + description: "A formatted JSON object for the assistant's voice. **Example: {\"provider\":\"tavus\",\"voiceId\":\"r52da2535a\",\"callbackUrl\":\"voice\",\"chunkPlan\":{\"enabled\":true,\"minCharacters\":30,\"punctuationBoundaries\":[\"。\",\",\",\".\",\"!\",\"?\",\";\",\"،\",\",\",\"।\",\"॥\",\"|\",\"||\",\",\",\":\"],\"formatPlan\":{\"enabled\":true,\"numberToDigitsCutoff\":2025}},\"conversationName\":\"voice\",\"conversationalContext\":\"voice\",\"customGreeting\":\"voice\",\"fallbackPlan\":{\"voices\":[{\"provider\":\"tavus\",\"voiceId\":\"r52da2535a\"}]},\"personaId\":\"voice\",\"properties\":{\"maxCallDuration\":1.1,\"participantLeftTimeout\":1.1,\"participantAbsentTimeout\":1.1,\"enableRecording\":true,\"enableTranscription\":true,\"applyGreenscreen\":true,\"language\":\"language\",\"recordingS3BucketName\":\"recordingS3BucketName\",\"recordingS3BucketRegion\":\"recordingS3BucketRegion\",\"awsAssumeRoleArn\":\"awsAssumeRoleArn\"}}**. [See the documentation](https://docs.vapi.ai/api-reference/assistants/update) for further details", + optional: true, + }, + firstMessage: { + type: "string", + label: "First Message", + description: "The first message the assistant will say or a URL to an audio file. If unspecified, assistant will wait for user to speak and use the model to respond once they speak.", + optional: true, + }, + firstMessageMode: { + type: "string", + label: "First Message Mode", + description: "Mode for the first message", + optional: true, + options: FIRST_MESSAGE_MODE_OPTIONS, + }, + hipaaEnabled: { + type: "boolean", + label: "HIPAA Enabled", + description: "When this is enabled, no logs, recordings, or transcriptions will be stored. At the end of the call, you will still receive an end-of-call-report message to store on your server.", + optional: true, + }, + clientMessages: { + type: "string[]", + label: "Client Messages", + description: "These are the messages that will be sent to your Client SDKs", + options: CLIENT_MESSAGE_OPTIONS, + optional: true, + }, + serverMessages: { + type: "string[]", + label: "Server Messages", + description: "These are the messages that will be sent to your Server URL", + options: SERVER_MESSAGE_OPTIONS, + optional: true, + }, + silenceTimeoutSeconds: { + type: "integer", + label: "Silence Timeout Seconds", + description: "How many seconds of silence to wait before ending the call.", + optional: true, + default: 30, + min: 10, + max: 3600, + }, + maxDurationSeconds: { + type: "integer", + label: "Max Duration Seconds", + description: "This is the maximum number of seconds that the call will last. When the call reaches this duration, it will be ended.", + optional: true, + default: 600, + min: 10, + max: 43200, + }, + backgroundSound: { + type: "string", + label: "Background Sound", + description: "This is the background sound in the call. Default for phone calls is 'office' and default for web calls is 'off'.", + optional: true, + options: BACKGROUND_SOUND, + }, + backgroundDenoisingEnabled: { + type: "boolean", + label: "Background Denoising Enabled", + description: "This enables filtering of noise and background speech while the user is talking. Default false while in beta.", + optional: true, + }, + modelOutputInMessagesEnabled: { + type: "boolean", + label: "Model Output in Messages Enabled", + description: "This determines whether the model's output is used in conversation history rather than the transcription of assistant's speech. Default false while in beta.", + optional: true, + }, + transportConfigurations: { + type: "string[]", + label: "Transport Configurations", + description: "These are the configurations to be passed to the transport providers of assistant's calls, like Twilio. You can store multiple configurations for different transport providers. For a call, only the configuration matching the call transport provider is used. **Example: [{\"provider\":\"twilio\",\"timeout\":60,\"record\":false,\"recordingChannels\":\"mono\"}]**. [See the documentation](https://docs.vapi.ai/api-reference/assistants/update) for further details", + optional: true, + }, + credentials: { + type: "string[]", + label: "Credentials", + description: "These are dynamic credentials that will be used for the assistant calls. By default, all the credentials are available for use in the call but you can supplement an additional credentials using this. Dynamic credentials override existing credentials. **Example: [{\"provider\":\"xai\",\"apiKey\":\"credentials\",\"name\":\"credentials\"}]**. [See the documentation](https://docs.vapi.ai/api-reference/assistants/update) for further details", + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "Name of the assistant. This is required when you want to transfer between assistants in a call.", + optional: true, + }, + voicemailDetection: { + type: "object", + label: "Voicemail Detection", + description: "These are the settings to configure or disable voicemail detection. Alternatively, voicemail detection can be configured using the model.tools=[VoicemailTool]. This uses Twilio's built-in detection while the VoicemailTool relies on the model to detect if a voicemail was reached. You can use neither of them, one of them, or both of them. By default, Twilio built-in detection is enabled while VoicemailTool is not. **Example: {\"provider\":\"twilio\",\"voicemailDetectionTypes\":[\"machine_end_beep\",\"machine_end_silence\"],\"enabled\":true,\"machineDetectionTimeout\":1.1,\"machineDetectionSpeechThreshold\":1.1,\"machineDetectionSpeechEndThreshold\":1.1,\"machineDetectionSilenceTimeout\":1.1}**. [See the documentation](https://docs.vapi.ai/api-reference/assistants/update) for further details", + optional: true, + }, + voicemailMessage: { + type: "string", + label: "Voicemail Message", + description: "This is the message that the assistant will say if the call is forwarded to voicemail. If unspecified, it will hang up", + optional: true, + }, + endCallMessage: { + type: "string", + label: "End Call Message", + description: "This is the message that the assistant will say if it ends the call. If unspecified, it will hang up without saying anything", + optional: true, + }, + endCallPhrases: { + type: "string[]", + label: "End Call Phrases", + description: "A list containing phrases that, if spoken by the assistant, will trigger the call to be hung up. Case insensitive.", + optional: true, + }, + metadata: { + type: "object", + label: "Metadata", + description: "This is for metadata you want to store on the assistant.", + optional: true, + }, + analysisPlan: { + type: "object", + label: "Analysis Plan", + description: "This is the plan for analysis of assistant's calls. Stored in `call.analysis`. **Example: {\"summaryPlan\":{\"messages\":[{\"key\":\"value\"}],\"enabled\":true,\"timeoutSeconds\":1.1},\"structuredDataPlan\":{\"messages\":[{\"key\":\"value\"}],\"enabled\":true,\"schema\":{\"type\":\"string\"},\"timeoutSeconds\":1.1},\"successEvaluationPlan\":{\"rubric\":\"NumericScale\",\"messages\":[{\"key\":\"value\"}],\"enabled\":true,\"timeoutSeconds\":1.1}}**. [See the documentation](https://docs.vapi.ai/api-reference/assistants/update) for further details", + optional: true, + }, + artifactPlan: { + type: "object", + label: "Artifact Plan", + description: "This is the plan for artifacts generated during assistant's calls. Stored in call.artifact. **Note:** `recordingEnabled` is currently at the root level. It will be moved to `artifactPlan` in the future, but will remain backwards compatible. **Example: {\"recordingEnabled\":true,\"videoRecordingEnabled\":false,\"transcriptPlan\":{\"enabled\":true,\"assistantName\":\"assistantName\",\"userName\":\"userName\"},\"recordingPath\":\"recordingPath\"}**. [See the documentation](https://docs.vapi.ai/api-reference/assistants/update) for further details", + optional: true, + }, + messagePlan: { + type: "object", + label: "Message Plan", + description: "This is the plan for static predefined messages that can be spoken by the assistant during the call, like idleMessages. **Note:** `firstMessage`, `voicemailMessage`, and `endCallMessage` are currently at the root level. They will be moved to `messagePlan` in the future, but will remain backwards compatible. **Example: {\"idleMessages\":[\"idleMessages\"],\"idleMessageMaxSpokenCount\":1.1,\"idleTimeoutSeconds\":1.1}**. [See the documentation](https://docs.vapi.ai/api-reference/assistants/update) for further details", + optional: true, + }, + startSpeakingPlan: { + type: "object", + label: "Start Speaking Plan", + description: "This is the plan for when the assistant should start talking. **Example: {\"waitSeconds\":0.4,\"smartEndpointingEnabled\":false,\"customEndpointingRules\":[{\"type\":\"both\",\"assistantRegex\":\"customEndpointingRules\",\"customerRegex\":\"customEndpointingRules\",\"timeoutSeconds\":1.1}],\"transcriptionEndpointingPlan\":{\"onPunctuationSeconds\":0.1,\"onNoPunctuationSeconds\":1.5,\"onNumberSeconds\":0.5}}**. [See the documentation](https://docs.vapi.ai/api-reference/assistants/update) for further details", + optional: true, + }, + stopSpeakingPlan: { + type: "object", + label: "Stop Speaking Plan", + description: "This is the plan for when assistant should stop talking on customer interruption. **Example: {\"numWords\":0,\"voiceSeconds\":0.2,\"backoffSeconds\":1}**. [See the documentation](https://docs.vapi.ai/api-reference/assistants/update) for further details", + optional: true, + }, + monitorPlan: { + type: "object", + label: "Monitor Plan", + description: "This is the plan for real-time monitoring of the assistant's calls. **Note:** `serverMessages`, `clientMessages`, `serverUrl` and `serverUrlSecret` are currently at the root level but will be moved to `monitorPlan` in the future. Will remain backwards compatible. **Example: {\"listenEnabled\":false,\"controlEnabled\":false}**. [See the documentation](https://docs.vapi.ai/api-reference/assistants/update) for further details", + optional: true, + }, + credentialIds: { + type: "string[]", + label: "Credential IDs", + description: "These are the credentials that will be used for the assistant calls. By default, all the credentials are available for use in the call but you can provide a subset using this.", + optional: true, + }, + server: { + type: "object", + label: "Server", + description: "This is where Vapi will send webhooks. You can find all webhooks available along with their shape in ServerMessage schema. **Example: {\"url\":\"url\",\"timeoutSeconds\":20,\"secret\":\"secret\",\"headers\":{\"key\":\"value\"}}**. [See the documentation](https://docs.vapi.ai/api-reference/assistants/update) for further details", + optional: true, + }, + }, + async run({ $ }) { + const { + vapi, + assistantId, + transcriber, + model, + voice, + clientMessages, + serverMessages, + transportConfigurations, + credentials, + voicemailDetection, + endCallPhrases, + metadata, + analysisPlan, + artifactPlan, + messagePlan, + startSpeakingPlan, + stopSpeakingPlan, + monitorPlan, + credentialIds, + server, + ...data + } = this; + + const response = await vapi.updateAssistant({ + $, + assistantId, + data: clearObj({ + ...data, + transcriber: parseObject(transcriber), + model: parseObject(model), + voice: parseObject(voice), + clientMessages: parseObject(clientMessages), + serverMessages: parseObject(serverMessages), + transportConfigurations: parseObject(transportConfigurations), + credentials: parseObject(credentials), + voicemailDetection: parseObject(voicemailDetection), + endCallPhrases: parseObject(endCallPhrases), + metadata: parseObject(metadata), + analysisPlan: parseObject(analysisPlan), + artifactPlan: parseObject(artifactPlan), + messagePlan: parseObject(messagePlan), + startSpeakingPlan: parseObject(startSpeakingPlan), + stopSpeakingPlan: parseObject(stopSpeakingPlan), + monitorPlan: parseObject(monitorPlan), + credentialIds: parseObject(credentialIds), + server: parseObject(server), + }), + }); + $.export("$summary", `Updated assistant ${this.assistantId} successfully`); + return response; + }, +}; diff --git a/components/vapi/actions/upload-file/upload-file.mjs b/components/vapi/actions/upload-file/upload-file.mjs new file mode 100644 index 0000000000000..cf6d9c49a77c2 --- /dev/null +++ b/components/vapi/actions/upload-file/upload-file.mjs @@ -0,0 +1,34 @@ +import FormData from "form-data"; +import fs from "fs"; +import { checkTmp } from "../../common/utils.mjs"; +import vapi from "../../vapi.app.mjs"; + +export default { + key: "vapi-upload-file", + name: "Upload File", + description: "Uploads a new file. [See the documentation](https://docs.vapi.ai/api-reference)", + version: "0.0.1", + type: "action", + props: { + vapi, + file: { + type: "string", + label: "File", + description: "The path to the file saved to the `/tmp` directory (e.g. `/tmp/example.txt`). [See the documentation](https://pipedream.com/docs/workflows/steps/code/nodejs/working-with-files/#the-tmp-directory).", + }, + }, + async run({ $ }) { + const formData = new FormData(); + const filePath = checkTmp(this.file); + + formData.append("file", fs.createReadStream(filePath)); + + const response = await this.vapi.uploadFile({ + $, + data: formData, + headers: formData.getHeaders(), + }); + $.export("$summary", `File uploaded successfully with ID ${response.id} and status ${response.status}`); + return response; + }, +}; diff --git a/components/vapi/common/constants.mjs b/components/vapi/common/constants.mjs new file mode 100644 index 0000000000000..98f73d4bb84be --- /dev/null +++ b/components/vapi/common/constants.mjs @@ -0,0 +1,65 @@ +export const LIMIT = 1000; + +export const FIRST_MESSAGE_MODE_OPTIONS = [ + { + label: "Assistant Speaks First", + value: "assistant-speaks-first", + }, + { + label: "Assistant Waits for User", + value: "assistant-waits-for-user", + }, + { + label: "Assistant Speaks First with Model Generated Message", + value: "assistant-speaks-first-with-model-generated-message", + }, +]; + +export const CLIENT_MESSAGE_OPTIONS = [ + "conversation-update", + "function-call", + "function-call-result", + "hang", + "language-changed", + "metadata", + "model-output", + "speech-update", + "status-update", + "transcript", + "tool-calls", + "tool-calls-result", + "transfer-update", + "user-interrupted", + "voice-input", +]; + +export const SERVER_MESSAGE_OPTIONS = [ + "conversation-update", + "end-of-call-report", + "function-call", + "hang", + "language-changed", + "language-change-detected", + "model-output", + "phone-call-control", + "speech-update", + "status-update", + "transcript", + "transcript[transcriptType=\"final\"]", + "tool-calls", + "transfer-destination-request", + "transfer-update", + "user-interrupted", + "voice-input", +]; + +export const BACKGROUND_SOUND = [ + { + label: "Office", + value: "office", + }, + { + label: "Off", + value: "off", + }, +]; diff --git a/components/vapi/common/utils.mjs b/components/vapi/common/utils.mjs new file mode 100644 index 0000000000000..9420d92839d7d --- /dev/null +++ b/components/vapi/common/utils.mjs @@ -0,0 +1,48 @@ +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; + +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; + +export const clearObj = (obj) => { + return Object.entries(obj) + .filter(([ + , + v, + ]) => (v != null && v != "" && JSON.stringify(v) != "{}")) + .reduce((acc, [ + k, + v, + ]) => ({ + ...acc, + [k]: (!Array.isArray(v) && v === Object(v)) + ? clearObj(v) + : v, + }), {}); +}; diff --git a/components/vapi/package.json b/components/vapi/package.json index 079d52bb54850..ab186fdbc6448 100644 --- a/components/vapi/package.json +++ b/components/vapi/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/vapi", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Vapi Components", "main": "vapi.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "form-data": "^4.0.1" } -} \ No newline at end of file +} diff --git a/components/vapi/sources/common/base.mjs b/components/vapi/sources/common/base.mjs new file mode 100644 index 0000000000000..77732a07557e5 --- /dev/null +++ b/components/vapi/sources/common/base.mjs @@ -0,0 +1,58 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import { LIMIT } from "../../common/constants.mjs"; +import vapi from "../../vapi.app.mjs"; + +export default { + props: { + vapi, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01"; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const fn = this.getFunction(); + + const response = await fn({ + params: { + createdAtGt: lastDate, + limit: LIMIT, + }, + }); + + if (response.length) { + if (maxResults && (response.length > maxResults)) { + response.length = maxResults; + } + this._setLastDate(response[0].createdAt); + } + + for (const item of response.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item.createdAt), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/vapi/sources/new-conversation/new-conversation.mjs b/components/vapi/sources/new-conversation/new-conversation.mjs new file mode 100644 index 0000000000000..4161a77b3bc9a --- /dev/null +++ b/components/vapi/sources/new-conversation/new-conversation.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "vapi-new-conversation", + name: "New Conversation Started", + description: "Emit new event when a voicebot starts a conversation.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.vapi.listCalls; + }, + getSummary(item) { + return `New Conversation: ${item.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/vapi/sources/new-conversation/test-event.mjs b/components/vapi/sources/new-conversation/test-event.mjs new file mode 100644 index 0000000000000..39ba5530a6b00 --- /dev/null +++ b/components/vapi/sources/new-conversation/test-event.mjs @@ -0,0 +1,336 @@ +export default { + "id": "id", + "orgId": "orgId", + "createdAt": "2024-01-15T09:30:00Z", + "updatedAt": "2024-01-15T09:30:00Z", + "type": "inboundPhoneCall", + "costs": [ + { + "type": "analysis", + "analysisType": "summary", + "completionTokens": 1.1, + "cost": 1.1, + "model": { + "key": "value" + }, + "promptTokens": 1.1 + } + ], + "messages": [ + { + "role": "role", + "message": "message", + "time": 1.1, + "endTime": 1.1, + "secondsFromStart": 1.1 + } + ], + "phoneCallProvider": "twilio", + "phoneCallTransport": "sip", + "status": "queued", + "endedReason": "assistant-not-invalid", + "destination": { + "type": "sip", + "sipUri": "destination", + "description": "destination", + "message": "destination", + "sipHeaders": { + "key": "value" + }, + "transferPlan": { + "mode": "blind-transfer" + } + }, + "startedAt": "2024-01-15T09:30:00Z", + "endedAt": "2024-01-15T09:30:00Z", + "cost": 1.1, + "costBreakdown": { + "transport": 1.1, + "stt": 1.1, + "llm": 1.1, + "tts": 1.1, + "vapi": 1.1, + "total": 1.1, + "llmPromptTokens": 1.1, + "llmCompletionTokens": 1.1, + "ttsCharacters": 1.1 + }, + "artifactPlan": { + "recordingEnabled": true, + "videoRecordingEnabled": false, + "transcriptPlan": { + "enabled": true + }, + "recordingPath": "recordingPath" + }, + "analysis": { + "summary": "summary", + "structuredData": { + "key": "value" + }, + "successEvaluation": "successEvaluation" + }, + "monitor": { + "listenUrl": "listenUrl", + "controlUrl": "controlUrl" + }, + "artifact": { + "messages": [ + { + "role": "role", + "message": "message", + "time": 1.1, + "endTime": 1.1, + "secondsFromStart": 1.1 + } + ], + "messagesOpenAIFormatted": [ + { + "role": "assistant" + } + ], + "recordingUrl": "recordingUrl", + "stereoRecordingUrl": "stereoRecordingUrl", + "videoRecordingUrl": "videoRecordingUrl", + "videoRecordingStartDelaySeconds": 1.1, + "transcript": "transcript" + }, + "transport": { + "provider": "twilio", + "assistantVideoEnabled": true + }, + "phoneCallProviderId": "phoneCallProviderId", + "assistantId": "assistantId", + "assistant": { + "transcriber": { + "provider": "talkscriber" + }, + "model": { + "provider": "xai", + "model": "grok-beta" + }, + "voice": { + "provider": "tavus", + "voiceId": "r52da2535a" + }, + "firstMessage": "Hello! How can I help you today?", + "firstMessageMode": "assistant-speaks-first", + "hipaaEnabled": false, + "clientMessages": [ + "conversation-update", + "function-call", + "hang", + "model-output", + "speech-update", + "status-update", + "transfer-update", + "transcript", + "tool-calls", + "user-interrupted", + "voice-input" + ], + "serverMessages": [ + "conversation-update", + "end-of-call-report", + "function-call", + "hang", + "speech-update", + "status-update", + "tool-calls", + "transfer-destination-request", + "user-interrupted" + ], + "silenceTimeoutSeconds": 30, + "maxDurationSeconds": 600, + "backgroundSound": "off", + "backgroundDenoisingEnabled": false, + "modelOutputInMessagesEnabled": false, + "transportConfigurations": [ + { + "provider": "twilio", + "timeout": 60, + "record": false + } + ], + "credentials": [ + { + "provider": "xai", + "apiKey": "credentials" + } + ], + "name": "name", + "voicemailDetection": { + "provider": "twilio" + }, + "voicemailMessage": "voicemailMessage", + "endCallMessage": "endCallMessage", + "endCallPhrases": [ + "endCallPhrases" + ], + "metadata": { + "key": "value" + }, + "artifactPlan": { + "recordingEnabled": true, + "videoRecordingEnabled": false + }, + "startSpeakingPlan": { + "waitSeconds": 0.4, + "smartEndpointingEnabled": false + }, + "stopSpeakingPlan": { + "numWords": 0, + "voiceSeconds": 0.2, + "backoffSeconds": 1 + }, + "monitorPlan": { + "listenEnabled": false, + "controlEnabled": false + }, + "credentialIds": [ + "credentialIds" + ], + "server": { + "url": "url", + "timeoutSeconds": 20 + } + }, + "assistantOverrides": { + "transcriber": { + "provider": "talkscriber" + }, + "model": { + "provider": "xai", + "model": "grok-beta" + }, + "voice": { + "provider": "tavus", + "voiceId": "r52da2535a" + }, + "firstMessage": "Hello! How can I help you today?", + "firstMessageMode": "assistant-speaks-first", + "hipaaEnabled": false, + "clientMessages": [ + "conversation-update", + "function-call", + "hang", + "model-output", + "speech-update", + "status-update", + "transfer-update", + "transcript", + "tool-calls", + "user-interrupted", + "voice-input" + ], + "serverMessages": [ + "conversation-update", + "end-of-call-report", + "function-call", + "hang", + "speech-update", + "status-update", + "tool-calls", + "transfer-destination-request", + "user-interrupted" + ], + "silenceTimeoutSeconds": 30, + "maxDurationSeconds": 600, + "backgroundSound": "off", + "backgroundDenoisingEnabled": false, + "modelOutputInMessagesEnabled": false, + "transportConfigurations": [ + { + "provider": "twilio", + "timeout": 60, + "record": false + } + ], + "credentials": [ + { + "provider": "xai", + "apiKey": "credentials" + } + ], + "variableValues": { + "key": "value" + }, + "name": "name", + "voicemailDetection": { + "provider": "twilio" + }, + "voicemailMessage": "voicemailMessage", + "endCallMessage": "endCallMessage", + "endCallPhrases": [ + "endCallPhrases" + ], + "metadata": { + "key": "value" + }, + "artifactPlan": { + "recordingEnabled": true, + "videoRecordingEnabled": false + }, + "startSpeakingPlan": { + "waitSeconds": 0.4, + "smartEndpointingEnabled": false + }, + "stopSpeakingPlan": { + "numWords": 0, + "voiceSeconds": 0.2, + "backoffSeconds": 1 + }, + "monitorPlan": { + "listenEnabled": false, + "controlEnabled": false + }, + "credentialIds": [ + "credentialIds" + ], + "server": { + "url": "url", + "timeoutSeconds": 20 + } + }, + "squadId": "squadId", + "squad": { + "members": [ + {} + ], + "name": "name", + "membersOverrides": { + "firstMessage": "Hello! How can I help you today?", + "hipaaEnabled": false, + "silenceTimeoutSeconds": 30, + "maxDurationSeconds": 600, + "backgroundDenoisingEnabled": false, + "modelOutputInMessagesEnabled": false + } + }, + "phoneNumberId": "phoneNumberId", + "phoneNumber": { + "twilioAccountSid": "twilioAccountSid", + "twilioAuthToken": "twilioAuthToken", + "twilioPhoneNumber": "twilioPhoneNumber", + "fallbackDestination": { + "type": "sip", + "sipUri": "fallbackDestination" + }, + "name": "name", + "assistantId": "assistantId", + "squadId": "squadId", + "server": { + "url": "url", + "timeoutSeconds": 20 + } + }, + "customerId": "customerId", + "customer": { + "numberE164CheckEnabled": true, + "extension": "extension", + "number": "number", + "sipUri": "sipUri", + "name": "name" + }, + "name": "name" +} \ No newline at end of file diff --git a/components/vapi/vapi.app.mjs b/components/vapi/vapi.app.mjs index 97279f197b9f4..feaa573240422 100644 --- a/components/vapi/vapi.app.mjs +++ b/components/vapi/vapi.app.mjs @@ -1,11 +1,160 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + export default { type: "app", app: "vapi", - propDefinitions: {}, + propDefinitions: { + assistantId: { + type: "string", + label: "Assistant ID", + description: "ID of the assistant to start a conversation with or update", + async options() { + const assistants = await this.listAssistants({ + params: { + limit: LIMIT, + }, + }); + return assistants.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + squadId: { + type: "string", + label: "Squad ID", + description: "ID of the squad to assign to the conversation", + async options() { + const squads = await this.listSquads({ + params: { + limit: LIMIT, + }, + }); + return squads.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + phoneNumberId: { + type: "string", + label: "Phone Number ID", + description: "ID of the phone number to use for the conversation", + async options() { + const phoneNumbers = await this.listPhoneNumbers({ + params: { + limit: LIMIT, + }, + }); + return phoneNumbers.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.vapi.ai"; + }, + _headers(headers = {}) { + return { + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, headers, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(headers), + ...opts, + }); + }, + startConversation(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/call", + ...opts, + }); + }, + listAssistants(opts = {}) { + return this._makeRequest({ + path: "/assistant", + ...opts, + }); + }, + listCalls(opts = {}) { + return this._makeRequest({ + path: "/call", + ...opts, + }); + }, + listSquads(opts = {}) { + return this._makeRequest({ + path: "/squad", + ...opts, + }); + }, + listPhoneNumbers(opts = {}) { + return this._makeRequest({ + path: "/phone-number", + ...opts, + }); + }, + updateAssistant({ + assistantId, ...opts + }) { + return this._makeRequest({ + method: "PATCH", + path: `/assistant/${assistantId}`, + ...opts, + }); + }, + uploadFile(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/file", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const { + data, + meta: { + current_page, last_page, + }, + } = await fn({ + params, + ...opts, + }); + for (const d of data) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = !(current_page == last_page); + + } while (hasMore); }, }, -}; \ No newline at end of file +}; diff --git a/components/vbout/README.md b/components/vbout/README.md index d86e5324806d1..1dc8b7549e48c 100644 --- a/components/vbout/README.md +++ b/components/vbout/README.md @@ -1,21 +1,11 @@ # Overview -The VBOUT API provides an easy and powerful way for developers to integrate -their applications with the VBOUT marketing automation platform. With the VBOUT -API, developers can create custom integrations and applications to manage and -automate customer communications in the most effective way possible. +The VBOUT API unlocks the potential to automate your marketing stack, leveraging Pipedream's capabilities for seamless integrations with other services. With VBOUT, you can manage contacts, automate email campaigns, track user actions, and analyze performance data. Pipedream enhances this by allowing you to capture events in real-time, process data with serverless code, and connect with countless apps to extend your marketing automation workflows. -Here are just a few examples of what you can build with the VBOUT API: +# Example Use Cases -- Create lead management workflows to capture leads, qualify leads, and pass - them onto other systems like Salesforce. -- Automatically segment contacts and set up automated email campaigns based on - their individual preferences. -- Track the performance of your marketing activities and optimize campaigns for - optimal results. -- Create personalized emails that are tailored to individual customer - interests. -- Utilize our analytics capabilities to gain insights into customer behavior - and uncover new opportunities to drive engagement and conversions. -- Leverage A/B testing capabilities to find out what works best with your - target audience. +- **Lead Scoring and Segmentation**: When a user performs a specific action on your website, captured by Google Analytics, Pipedream listens for this event and triggers a VBOUT workflow to update the lead score of the user. Based on the new score, the lead is then automatically segmented into a new email campaign in VBOUT for targeted follow-ups. + +- **Social Media Engagement to Email Campaigns**: A user's interaction with your brand's social media post, monitored by Pipedream's Twitter trigger, can initiate a sequence of events in VBOUT. Depending on the interaction type, a personalized email is crafted in VBOUT and sent to the user, encouraging them to explore related products or content, thus enhancing customer engagement. + +- **Customer Feedback Loop**: After a customer support ticket is resolved in Zendesk, an event is sent to Pipedream, triggering an automation that sends a follow-up survey email via VBOUT. The customer's survey response is then captured and logged back into VBOUT for sentiment analysis, helping to improve the support process and product offerings. diff --git a/components/vectera/README.md b/components/vectera/README.md index a4edc125c13c8..bd07d8dc0f8f5 100644 --- a/components/vectera/README.md +++ b/components/vectera/README.md @@ -1,20 +1,11 @@ # Overview -The Vectera API offers a wide range of features to enable powerful and -versatile video collaboration solutions. With the Vectera API, you can build -collaborative solutions that connect multiple users in real-time and make video -meetings more efficient. You can create applications that enable teams to share -screens, make documents and media securely, and collaborate in real-time. +The Vectera API enables users to streamline scheduling and video meeting management directly within their own platforms or through integration with other services. With the Vectera API, you can automate the creation of meeting rooms, manage contacts, and handle meeting recordings. Leverage Pipedream's capabilities to trigger workflows from various events, process data, and connect Vectera with other apps to create powerful automations. -Here are some examples of what you can build with the Vectera API: +# Example Use Cases -- Create online meeting tools that are secure, fast, and tailored to your - needs. -- Develop customized applications for live meetings and training sessions. -- Enable users to easily share private documents and other media. -- Create a whiteboard feature that allows users to collaborate in real-time. -- Develop visual collaboration tools to make online meetings more interactive. -- Create AI-powered chat bots that can help facilitate conversations during - meetings. -- Integrate with enterprise grade solutions to make them more secure and - efficient. +- **Automated Meeting Room Creation**: Trigger a workflow on Pipedream whenever a new event is scheduled in your calendar app (e.g., Google Calendar). The workflow creates a new Vectera meeting room and sends the details to participants via email or a messaging app like Slack. + +- **Contact Synchronization**: Sync contacts between Vectera and a CRM platform like Salesforce. Whenever a new contact is added to Salesforce, a corresponding contact can be automatically created in Vectera, ensuring that meeting invites are always sent to up-to-date email addresses. + +- **Meeting Recording Processing**: After a meeting ends, trigger a Pipedream workflow to download the recording from Vectera. The workflow can then upload the recording to a cloud storage service like Google Drive and send a notification with the link to participants or stakeholders. diff --git a/components/vend/README.md b/components/vend/README.md index 8d29a27e5b00c..0b16152d6d32b 100644 --- a/components/vend/README.md +++ b/components/vend/README.md @@ -1,30 +1,11 @@ # Overview -With the Vend API, you can streamline your customer experience and build -customer loyalty by leveraging customer data, product information and stock -control. +The Vend API offers programmatic access to retail management features, enabling users to automate processes related to inventory, sales, products, customers, and more within their retail business. Through Pipedream, you can harness this API to create custom workflows that trigger on various events within Vend, process data, integrate with other apps, and automate actions to streamline retail operations, enhance customer engagement, and optimize sales strategies. -The Vend API can help you create custom checkout experiences for customers that -improve the convenience and speed of their shopping experience. For example, -you can integrate your store’s website with the Vend API to enable customers -the ability to purchase goods directly from your website. You can also power -loyalty programs through the Vend API, allowing customers to earn and redeem -rewards points on purchases. +# Example Use Cases -In addition to connecting customers to your store, the Vend API can also be -used to help streamline product management and stock control. You can use the -Vend API to provide customers information about product availability, pricing, -and delivery options as well as helping you manage stock levels and reordering. +- **Real-time Inventory Updates to Google Sheets**: Track inventory levels by connecting Vend to Google Sheets via Pipedream. Whenever a sale is made or new stock is added in Vend, trigger a workflow to update the corresponding Google Sheet in real-time, ensuring inventory records are always current without manual intervention. -The opportunities are endless with Vend API, here are some examples of products -you can create: +- **Customer Follow-Up Emails with SendGrid**: After a purchase is completed in Vend, use Pipedream to trigger a workflow that sends a personalized follow-up email via SendGrid. This can include a thank you message, product care tips, or requests for feedback, enhancing post-purchase customer engagement. -- Custom checkout experiences -- Loyalty programs -- Product catalogs -- Stock control -- Reordering automation -- Delivery options -- Payment integration -- Faster checkout process -- Real-time product updates +- **Slack Notifications for Low Stock Alerts**: Set up a workflow on Pipedream that monitors Vend for low stock levels and automatically sends an alert to a designated Slack channel. This enables quick restocking decisions, preventing potential sales loss due to unavailability of popular items. diff --git a/components/vendasta/README.md b/components/vendasta/README.md new file mode 100644 index 0000000000000..1efe0932fee27 --- /dev/null +++ b/components/vendasta/README.md @@ -0,0 +1,11 @@ +# Overview + +The Vendasta API offers robust access to manage customer accounts, listings, and reviews within the Vendasta platform. Using Pipedream, you can automate workflows that integrate these capabilities with other apps, cutting down manual tasks and streamlining business operations. Pipedream's serverless execution model allows you to trigger actions based on events, schedule tasks, and manipulate data with built-in code steps. + +# Example Use Cases + +- **Sync Vendasta Customer Data with CRM**: Keep your CRM updated by pulling new customer data from Vendasta and pushing it to your CRM platform, such as Salesforce, HubSpot, or Zoho. Whenever a new customer is added in Vendasta, trigger a Pipedream workflow to create or update the customer's record in your CRM. + +- **Automate Reputation Management Responses**: Monitor reviews received on Vendasta and automate response workflows. For instance, when a new review is detected, trigger a Pipedream workflow to analyze the sentiment using AI services like Google Natural Language API, and then post a drafted response or alert your team to take action. + +- **Scheduled Listing Data Sync**: Maintain up-to-date listings across platforms by scheduling Pipedream workflows that periodically fetch listing information from Vendasta and update entries in directories such as Google My Business or Facebook Pages. This ensures information consistency and helps in SEO optimization. diff --git a/components/vendasta/package.json b/components/vendasta/package.json index 93fa4b562a348..73dd5e15ba06b 100644 --- a/components/vendasta/package.json +++ b/components/vendasta/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/venly/README.md b/components/venly/README.md new file mode 100644 index 0000000000000..ee7a10cb50d26 --- /dev/null +++ b/components/venly/README.md @@ -0,0 +1,11 @@ +# Overview + +The Venly (NFT API) provides a platform for managing and integrating Non-Fungible Tokens (NFTs) into applications. With this API, you can create and manage wallets, mint and transfer NFTs, and query blockchain data, among other features. Leveraging the Venly API within Pipedream allows developers to streamline NFT operations by creating automated workflows, connecting to other services, and processing blockchain events in real-time. + +# Example Use Cases + +- **Automated NFT Minting and Notification**: Streamline the NFT creation process by setting up a Pipedream workflow that monitors a specific trigger (like a form submission or a webhook event) and uses the Venly API to mint NFTs automatically. Once minted, it could send notifications via email, Slack, or Discord to inform the relevant stakeholders. + +- **NFT Sales Tracking and Analytics**: Create a Pipedream workflow that uses the Venly API to track NFT sales. Upon detection of a new sale, the workflow could log the data in a Google Sheets spreadsheet, send an analytical report to Google Analytics, and update a dashboard in real-time to provide insights into NFT sales performance. + +- **Cross-platform NFT Gallery Sync**: Develop a Pipedream workflow that syncs an NFT collection gallery across multiple platforms such as a personal website, Shopify store, or social media. Using Venly's API, the workflow can keep NFT listings updated on all platforms whenever a new NFT is minted or an existing one is transferred or sold. diff --git a/components/vercel/actions/cancel-deployment/cancel-deployment.mjs b/components/vercel/actions/cancel-deployment/cancel-deployment.mjs deleted file mode 100644 index 04c8b079f355b..0000000000000 --- a/components/vercel/actions/cancel-deployment/cancel-deployment.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import vercel from "../../vercel.app.mjs"; - -export default { - key: "vercel-cancel-deployment", - name: "Cancel Deployment", - description: "Cancel a deployment which is currently building. [See the docs](https://vercel.com/docs/rest-api#endpoints/deployments/cancel-a-deployment)", - version: "0.0.2", - type: "action", - props: { - vercel, - deployment: { - propDefinition: [ - vercel, - "deployment", - () => ({ - state: "BUILDING", - }), - ], - }, - team: { - propDefinition: [ - vercel, - "team", - ], - }, - }, - async run({ $ }) { - const params = { - teamId: this.team, - }; - const res = await this.vercel.cancelDeployment(this.deployment, params, $); - $.export("$summary", `Successfully canceled deployment ${this.deployment}`); - return res; - }, -}; diff --git a/components/vercel/actions/create-deployment/create-deployment.mjs b/components/vercel/actions/create-deployment/create-deployment.mjs deleted file mode 100644 index 6d475e23f842a..0000000000000 --- a/components/vercel/actions/create-deployment/create-deployment.mjs +++ /dev/null @@ -1,63 +0,0 @@ -import vercel from "../../vercel.app.mjs"; - -export default { - key: "vercel-create-deployment", - name: "Create Deployment", - description: "Create a new deployment from a GitHub repository. [See the docs](https://vercel.com/docs/rest-api#endpoints/deployments/create-a-new-deployment)", - version: "0.0.2", - type: "action", - props: { - vercel, - name: { - type: "string", - label: "Name", - description: "A string with the project name used in the deployment URL", - }, - project: { - propDefinition: [ - vercel, - "project", - ], - description: "The target project identifier in which the deployment will be created. When defined, this parameter overrides name", - }, - repoId: { - type: "string", - label: "Git Source Repository Id", - description: "Id of the source repository", - }, - branch: { - type: "string", - label: "Branch", - description: "Branch of repository to deploy to", - default: "main", - }, - team: { - propDefinition: [ - vercel, - "team", - ], - }, - public: { - type: "boolean", - label: "Public", - description: "Whether a deployment's source and logs are available publicly", - optional: true, - }, - }, - async run({ $ }) { - const data = { - name: this.name, - project: this.project, - teamId: this.team, - public: this.public, - gitSource: { - type: "github", - repoId: this.repoId, - ref: this.branch, - }, - }; - const res = await this.vercel.createDeployment(data, $); - $.export("$summary", "Successfully created new deployment"); - return res; - }, -}; diff --git a/components/vercel/actions/list-deployments/list-deployments.mjs b/components/vercel/actions/list-deployments/list-deployments.mjs deleted file mode 100644 index f415b7e0ebd36..0000000000000 --- a/components/vercel/actions/list-deployments/list-deployments.mjs +++ /dev/null @@ -1,48 +0,0 @@ -import vercel from "../../vercel.app.mjs"; - -export default { - key: "vercel-list-deployments", - name: "List Deployments", - description: "List deployments under the account corresponding to the API token. [See the docs](https://vercel.com/docs/rest-api#endpoints/deployments/list-deployments)", - version: "0.0.2", - type: "action", - props: { - vercel, - project: { - propDefinition: [ - vercel, - "project", - ], - }, - team: { - propDefinition: [ - vercel, - "team", - ], - }, - state: { - propDefinition: [ - vercel, - "state", - ], - }, - max: { - propDefinition: [ - vercel, - "max", - ], - }, - }, - async run({ $ }) { - const params = { - projectId: this.project, - state: this.state, - teamId: this.team, - }; - const res = await this.vercel.listDeployments(params, this.max, $); - $.export("$summary", `Found ${res.length} deployment${res.length !== 1 - ? "s" - : ""}`); - return res; - }, -}; diff --git a/components/vercel/package.json b/components/vercel/package.json deleted file mode 100644 index d250550846882..0000000000000 --- a/components/vercel/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@pipedream/vercel", - "version": "0.1.3", - "description": "Pipedream Vercel Components", - "main": "vercel.app.js", - "keywords": [ - "pipedream", - "vercel" - ], - "homepage": "https://pipedream.com/apps/vercel", - "author": "Pipedream (https://pipedream.com/)", - "publishConfig": { - "access": "public" - }, - "dependencies": { - "@pipedream/platform": "^1.2.0" - } -} diff --git a/components/vercel/sources/new-deployment/new-deployment.mjs b/components/vercel/sources/new-deployment/new-deployment.mjs deleted file mode 100644 index 670158c807d8d..0000000000000 --- a/components/vercel/sources/new-deployment/new-deployment.mjs +++ /dev/null @@ -1,87 +0,0 @@ -import vercel from "../../vercel.app.mjs"; -import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; - -export default { - key: "vercel-new-deployment", - name: "New Deployment", - description: "Emit new event when a deployment is created", - version: "0.0.4", - type: "source", - dedupe: "unique", - props: { - vercel, - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - project: { - propDefinition: [ - vercel, - "project", - ], - }, - state: { - propDefinition: [ - vercel, - "state", - ], - }, - max: { - propDefinition: [ - vercel, - "max", - ], - }, - }, - hooks: { - async deploy() { - await this.processEvent(10); - }, - }, - methods: { - _getFrom() { - return this.db.get("from"); - }, - _setFrom(from) { - this.db.set("from", from); - }, - generateMeta(deployment) { - const { - uid, - name, - state, - created, - } = deployment; - return { - id: uid, - summary: `${name ?? uid} ${state}`, - ts: created, - }; - }, - async processEvent(max) { - const params = { - projectId: this.project, - state: this.state, - }; - let from = this._getFrom(); - if (from) { - params.from = from; - } - const deployments = await this.vercel.listDeployments(params, max); - for (const deployment of deployments) { - if (!from || deployment.created > from) { - from = deployment.created; - } - const meta = this.generateMeta(deployment); - this.$emit(deployment, meta); - } - this._setFrom(from); - }, - }, - async run() { - await this.processEvent(this.max); - }, -}; diff --git a/components/vercel/vercel.app.mjs b/components/vercel/vercel.app.mjs deleted file mode 100644 index 2137289eb6da4..0000000000000 --- a/components/vercel/vercel.app.mjs +++ /dev/null @@ -1,157 +0,0 @@ -import { axios } from "@pipedream/platform"; -import constants from "./constants.mjs"; - -export default { - type: "app", - app: "vercel", - propDefinitions: { - project: { - type: "string", - label: "Project", - description: "Filter deployments from the given projectId", - optional: true, - async options() { - const projects = await this.listProjects(); - return projects?.map((project) => ({ - label: project.name, - value: project.id, - })) ?? []; - }, - }, - deployment: { - type: "string", - label: "Deployment", - description: "Select the deployment to cancel", - async options({ state }) { - const params = state - ? { - state, - } - : {}; - const deployments = await this.listDeployments(params); - return deployments?.map((deployment) => ({ - label: deployment.name, - value: deployment.uid, - })) ?? []; - }, - }, - team: { - type: "string", - label: "Team", - description: "The Team identifier or slug to perform the request on behalf of", - optional: true, - async options() { - try { - const teams = await this.listTeams(); - return teams?.map((team) => ({ - label: team.slug, - value: team.id, - })) ?? []; - } catch (e) { - throw new Error(e.message); - } - }, - }, - state: { - type: "string", - label: "State", - description: "Filter deployments based on their state", - optional: true, - options: constants.DEPLOYMENT_STATES, - }, - max: { - type: "integer", - label: "Max", - description: "Maximum number of results to return", - optional: true, - }, - }, - methods: { - _auth() { - return { - Authorization: `Bearer ${this.$auth.oauth_access_token}`, - }; - }, - async makeRequest(config, $) { - config = { - ...config, - url: `https://api.vercel.com/${config.endpoint}`, - headers: { - ...this._auth(), - "User-Agent": "@PipedreamHQ/pipedream v0.1", - }, - }; - delete config.endpoint; - return axios($ ?? this, config); - }, - /** - * Paginate through a list of resources in Vercel - * @params {String} resource - Resource type (e.g. "projects", "deployments", "teams"). - * The response from makeRequest() will contain an array of results with the specified - * resource as the key and the array as the value - * @params {Object} config - configuration variables for the request - * @params {Integer} max - the maximum number of results to return - * @returns {Array} An array of results - */ - async paginate(resource, config, max = constants.MAX_RESULTS, $) { - const allResults = []; - config.params = { - ...config.params, - limit: constants.PAGE_SIZE, - }; - let results; - do { - results = await this.makeRequest(config, $); - config.params.from = results?.pagination?.next; - config.params.since = results?.pagination?.next; - allResults.push(...results[resource]); - } while (results?.pagination?.count === config.limit && allResults.length < max); - if (allResults.length > max) { - allResults.length = max; - } - return allResults; - }, - async listProjects(max, $) { - const config = { - method: "GET", - endpoint: "v8/projects", - }; - return this.paginate("projects", config, max, $); - }, - async listDeployments(params, max, $) { - const config = { - method: "GET", - endpoint: "v6/deployments", - params, - }; - return this.paginate("deployments", config, max, $); - }, - async listTeams(max, $) { - const config = { - method: "GET", - endpoint: "v2/teams", - }; - return this.paginate("teams", config, max, $); - }, - async cancelDeployment(id, params, $) { - const config = { - method: "PATCH", - endpoint: `v12/deployments/${id}/cancel`, - params, - }; - return this.makeRequest(config, $); - }, - async createDeployment(data, $) { - const config = { - method: "POST", - endpoint: "v13/deployments", - data, - }; - if (data.teamId) { - config.endpoint += `?teamId=${data.teamId}`; - } - delete data.teamId; - return this.makeRequest(config, $); - }, - }, -}; diff --git a/components/vercel_token_auth/README.md b/components/vercel_token_auth/README.md index afbb6c617be8d..b3481c01fa7c7 100644 --- a/components/vercel_token_auth/README.md +++ b/components/vercel_token_auth/README.md @@ -1,18 +1,11 @@ # Overview - Vercel provides an API that allows you to authenticate users and manage access - to your applications and services. With the Vercel API, you can easily build - an array of applications, from complete single-page applications to scalable - applications and services. Here are a few examples of what you can build with - Vercel's API: +The Vercel API empowers developers to automate, manage, and interact with their Vercel projects and deployments directly through code. With the Vercel API on Pipedream, you can harness the power of serverless functions to create dynamic and responsive workflows. Automate deployment processes, sync deployment statuses with other tools, trigger notifications based on deployment events, or manage your domains and aliases—all within the seamless integration landscape of Pipedream. -- SSO (Single Sign-On) solutions that securely authenticate users across - multiple devices and applications -- Secure access control solutions to protect sensitive data and services -- APIs that provide easy access to third-party data -- User registration and management tools -- Content management solutions -- Data analysis tools and applications -- Back-end services for web applications -- Customizable dashboards for web applications -- Automated email notifications for applications and services +# Example Use Cases + +- **Deployment Trigger Notification**: Send a Slack message or an email via SendGrid whenever a new deployment is triggered in Vercel. This keeps teams instantly informed about development progress and deployment pipeline activity. + +- **Automated Backup on Deployment**: Whenever a deployment to production is successful, back up the production database by triggering a serverless function that creates a snapshot and stores it in Amazon S3 or another cloud storage service. This ensures that there's always a recent backup available, safeguarding against data loss. + +- **Performance Metrics Reporting**: After a deployment, invoke a workflow that fetches the latest deployment's performance metrics using the Vercel API and sends a report to DataDog, or a Google Sheet for further analysis and historical tracking. This helps in keeping a tab on the application's performance and the impact of each deployment. diff --git a/components/vercel_token_auth/actions/cancel-deployment/cancel-deployment.mjs b/components/vercel_token_auth/actions/cancel-deployment/cancel-deployment.mjs index 6bfb92368b50b..b93afcab43215 100644 --- a/components/vercel_token_auth/actions/cancel-deployment/cancel-deployment.mjs +++ b/components/vercel_token_auth/actions/cancel-deployment/cancel-deployment.mjs @@ -1,14 +1,36 @@ -/* eslint-disable pipedream/required-properties-name */ -/* eslint-disable pipedream/required-properties-description */ -/* eslint-disable pipedream/required-properties-version */ -/* eslint-disable pipedream/required-properties-type */ -import base from "../../../vercel/actions/cancel-deployment/cancel-deployment.mjs"; -import overrideApp from "../../common/override-app.mjs"; - -overrideApp(base); +import vercelTokenAuth from "../../vercel_token_auth.app.mjs"; export default { - ...base, key: "vercel_token_auth-cancel-deployment", - version: "0.0.2", + name: "Cancel Deployment", + description: "Cancel a deployment which is currently building. [See the documentation](https://vercel.com/docs/rest-api/endpoints/deployments#cancel-a-deployment)", + version: "0.0.4", + type: "action", + props: { + vercelTokenAuth, + team: { + propDefinition: [ + vercelTokenAuth, + "team", + ], + }, + deployment: { + propDefinition: [ + vercelTokenAuth, + "deployment", + (c) => ({ + teamId: c.team, + state: "BUILDING", + }), + ], + }, + }, + async run({ $ }) { + const params = { + teamId: this.team, + }; + const res = await this.vercelTokenAuth.cancelDeployment(this.deployment, params, $); + $.export("$summary", `Successfully canceled deployment ${this.deployment}`); + return res; + }, }; diff --git a/components/vercel_token_auth/actions/create-deployment/create-deployment.mjs b/components/vercel_token_auth/actions/create-deployment/create-deployment.mjs index 02db81c95afa8..3b8ca4131e72c 100644 --- a/components/vercel_token_auth/actions/create-deployment/create-deployment.mjs +++ b/components/vercel_token_auth/actions/create-deployment/create-deployment.mjs @@ -1,14 +1,91 @@ -/* eslint-disable pipedream/required-properties-name */ -/* eslint-disable pipedream/required-properties-description */ -/* eslint-disable pipedream/required-properties-version */ -/* eslint-disable pipedream/required-properties-type */ -import base from "../../../vercel/actions/create-deployment/create-deployment.mjs"; -import overrideApp from "../../common/override-app.mjs"; - -overrideApp(base); +import vercelTokenAuth from "../../vercel_token_auth.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; export default { - ...base, key: "vercel_token_auth-create-deployment", - version: "0.0.2", + name: "Create Deployment", + description: "Create a new deployment from a GitHub repository. [See the documentation](https://vercel.com/docs/rest-api/endpoints/deployments#create-a-new-deployment)", + version: "0.0.4", + type: "action", + props: { + vercelTokenAuth, + name: { + type: "string", + label: "Name", + description: "A string with the project name used in the deployment URL", + }, + team: { + propDefinition: [ + vercelTokenAuth, + "team", + ], + }, + project: { + propDefinition: [ + vercelTokenAuth, + "project", + (c) => ({ + teamId: c.team, + }), + ], + description: "The target project identifier in which the deployment will be created", + optional: false, + reloadProps: true, + }, + branch: { + type: "string", + label: "Branch", + description: "Branch of repository to deploy to", + }, + public: { + type: "boolean", + label: "Public", + description: "Whether a deployment's source and logs are available publicly", + optional: true, + }, + }, + async additionalProps(props) { + if (!this.project) { + return {}; + } + try { + const { link } = await this.vercelTokenAuth.getProject(this.project); + if (link) { + props.branch.description = `Branch of \`${link.repo}\` repository to deploy to`; + props.branch.default = link?.productionBranch || "main"; + } else { + props.branch.default = "main"; + } + } catch { + props.branch.default = "main"; + return {}; + } + return {}; + }, + async run({ $ }) { + if (!this.branch) { + throw new ConfigurationError("Branch prop is required"); + } + + const { link } = await this.vercelTokenAuth.getProject(this.project, $); + if (!link?.repoId) { + throw new ConfigurationError(`No linked repository found for project with ID: ${this.project}`); + } + const repoId = link.repoId; + + const data = { + name: this.name, + project: this.project, + teamId: this.team, + public: this.public, + gitSource: { + type: "github", + repoId, + ref: this.branch, + }, + }; + const res = await this.vercelTokenAuth.createDeployment(data, $); + $.export("$summary", "Successfully created new deployment"); + return res; + }, }; diff --git a/components/vercel_token_auth/actions/list-deployments/list-deployments.mjs b/components/vercel_token_auth/actions/list-deployments/list-deployments.mjs index 2a2423384571a..d3a456581a761 100644 --- a/components/vercel_token_auth/actions/list-deployments/list-deployments.mjs +++ b/components/vercel_token_auth/actions/list-deployments/list-deployments.mjs @@ -1,14 +1,51 @@ -/* eslint-disable pipedream/required-properties-name */ -/* eslint-disable pipedream/required-properties-description */ -/* eslint-disable pipedream/required-properties-version */ -/* eslint-disable pipedream/required-properties-type */ -import base from "../../../vercel/actions/list-deployments/list-deployments.mjs"; -import overrideApp from "../../common/override-app.mjs"; - -overrideApp(base); +import vercelTokenAuth from "../../vercel_token_auth.app.mjs"; export default { - ...base, key: "vercel_token_auth-list-deployments", - version: "0.0.2", + name: "List Deployments", + description: "List deployments under the account corresponding to the API token. [See the documentation](https://vercel.com/docs/rest-api/endpoints/deployments#list-deployments)", + version: "0.0.4", + type: "action", + props: { + vercelTokenAuth, + team: { + propDefinition: [ + vercelTokenAuth, + "team", + ], + }, + project: { + propDefinition: [ + vercelTokenAuth, + "project", + (c) => ({ + teamId: c.team, + }), + ], + }, + state: { + propDefinition: [ + vercelTokenAuth, + "state", + ], + }, + max: { + propDefinition: [ + vercelTokenAuth, + "max", + ], + }, + }, + async run({ $ }) { + const params = { + projectId: this.project, + state: this.state, + teamId: this.team, + }; + const res = await this.vercelTokenAuth.listDeployments(params, this.max, $); + $.export("$summary", `Found ${res.length} deployment${res.length !== 1 + ? "s" + : ""}`); + return res; + }, }; diff --git a/components/vercel/constants.mjs b/components/vercel_token_auth/common/constants.mjs similarity index 100% rename from components/vercel/constants.mjs rename to components/vercel_token_auth/common/constants.mjs diff --git a/components/vercel_token_auth/common/override-app.mjs b/components/vercel_token_auth/common/override-app.mjs deleted file mode 100644 index a75f354c13816..0000000000000 --- a/components/vercel_token_auth/common/override-app.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import app from "../vercel_token_auth.app.mjs"; -const appName = "vercel"; - -/** - * This function substitutes the app so that $auth and propDefinitions correspond to the correct app - * This is done to avoid duplicating code because all component props and methods are shared between - * gorgias_oauth and gorgias (api_key) apps - * @param {*} base app to inject - in this case, gorgias_oauth - */ -function overrideApp(base) { - base.props[appName] = app; - - Object.keys(base.props) - .filter((prop) => prop != appName) - .forEach((prop) => { - const { propDefinition } = base.props[prop]; - if (propDefinition?.length > 0) { - propDefinition[0] = app; - } - }); -} - -export default overrideApp; diff --git a/components/vercel_token_auth/package.json b/components/vercel_token_auth/package.json index 29805bac1a880..4902eddc8b216 100644 --- a/components/vercel_token_auth/package.json +++ b/components/vercel_token_auth/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/vercel_token_auth", - "version": "0.0.3", + "version": "0.0.5", "description": "Pipedream Vercel (token-based auth) Components", "main": "vercel_token_auth.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/vercel_token_auth/sources/new-deployment/new-deployment.mjs b/components/vercel_token_auth/sources/new-deployment/new-deployment.mjs index 570cb4eb8aa6e..9474844f7d640 100644 --- a/components/vercel_token_auth/sources/new-deployment/new-deployment.mjs +++ b/components/vercel_token_auth/sources/new-deployment/new-deployment.mjs @@ -1,14 +1,97 @@ -/* eslint-disable pipedream/required-properties-name */ -/* eslint-disable pipedream/required-properties-description */ -/* eslint-disable pipedream/required-properties-version */ -/* eslint-disable pipedream/required-properties-type */ -import base from "../../../vercel/sources/new-deployment/new-deployment.mjs"; -import overrideApp from "../../common/override-app.mjs"; - -overrideApp(base); +import vercelTokenAuth from "../../vercel_token_auth.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; export default { - ...base, key: "vercel_token_auth-new-deployment", - version: "0.0.2", + name: "New Deployment", + description: "Emit new event when a deployment is created", + version: "0.0.4", + type: "source", + dedupe: "unique", + props: { + vercelTokenAuth, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + team: { + propDefinition: [ + vercelTokenAuth, + "team", + ], + }, + project: { + propDefinition: [ + vercelTokenAuth, + "project", + (c) => ({ + teamId: c.team, + }), + ], + }, + state: { + propDefinition: [ + vercelTokenAuth, + "state", + ], + }, + max: { + propDefinition: [ + vercelTokenAuth, + "max", + ], + }, + }, + hooks: { + async deploy() { + await this.processEvent(10); + }, + }, + methods: { + _getFrom() { + return this.db.get("from"); + }, + _setFrom(from) { + this.db.set("from", from); + }, + generateMeta(deployment) { + const { + uid, + name, + state, + created, + } = deployment; + return { + id: uid, + summary: `${name ?? uid} ${state}`, + ts: created, + }; + }, + async processEvent(max) { + const params = { + teamId: this.team, + projectId: this.project, + state: this.state, + }; + let from = this._getFrom(); + if (from) { + params.from = from; + } + const deployments = await this.vercelTokenAuth.listDeployments(params, max); + for (const deployment of deployments) { + if (!from || deployment.created > from) { + from = deployment.created; + } + const meta = this.generateMeta(deployment); + this.$emit(deployment, meta); + } + this._setFrom(from); + }, + }, + async run() { + await this.processEvent(this.max); + }, }; diff --git a/components/vercel_token_auth/vercel_token_auth.app.mjs b/components/vercel_token_auth/vercel_token_auth.app.mjs index 63ce2c2fbb333..6da875a429ffb 100644 --- a/components/vercel_token_auth/vercel_token_auth.app.mjs +++ b/components/vercel_token_auth/vercel_token_auth.app.mjs @@ -1,15 +1,168 @@ -import base from "../vercel/vercel.app.mjs"; +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; export default { - ...base, type: "app", app: "vercel_token_auth", + propDefinitions: { + project: { + type: "string", + label: "Project", + description: "Filter deployments from the given projectId", + optional: true, + async options({ teamId }) { + const projects = await this.listProjects({ + teamId, + }); + return projects?.map((project) => ({ + label: project.name, + value: project.id, + })) ?? []; + }, + }, + deployment: { + type: "string", + label: "Deployment", + description: "Select the deployment to cancel", + async options({ + teamId, state, + }) { + const params = { + teamId, + }; + if (state) { + params.state = state; + } + const deployments = await this.listDeployments(params); + return deployments?.map((deployment) => ({ + label: deployment.name, + value: deployment.uid, + })) ?? []; + }, + }, + team: { + type: "string", + label: "Team", + description: "The Team identifier or slug to perform the request on behalf of", + async options() { + try { + const teams = await this.listTeams(); + return teams?.map((team) => ({ + label: team.slug, + value: team.id, + })) ?? []; + } catch (e) { + throw new Error(e.message); + } + }, + }, + state: { + type: "string", + label: "State", + description: "Filter deployments based on their state", + optional: true, + options: constants.DEPLOYMENT_STATES, + }, + max: { + type: "integer", + label: "Max", + description: "Maximum number of results to return", + optional: true, + }, + }, methods: { - ...base.methods, _auth() { return { Authorization: `Bearer ${this.$auth.personal_access_token}`, }; }, + async makeRequest(config, $) { + config = { + ...config, + url: `https://api.vercel.com/${config.endpoint}`, + headers: { + ...this._auth(), + "User-Agent": "@PipedreamHQ/pipedream v0.1", + }, + }; + delete config.endpoint; + return axios($ ?? this, config); + }, + /** + * Paginate through a list of resources in Vercel + * @params {String} resource - Resource type (e.g. "projects", "deployments", "teams"). + * The response from makeRequest() will contain an array of results with the specified + * resource as the key and the array as the value + * @params {Object} config - configuration variables for the request + * @params {Integer} max - the maximum number of results to return + * @returns {Array} An array of results + */ + async paginate(resource, config, max = constants.MAX_RESULTS, $) { + const allResults = []; + config.params = { + ...config.params, + limit: constants.PAGE_SIZE, + }; + let results; + do { + results = await this.makeRequest(config, $); + config.params.from = results?.pagination?.next; + config.params.since = results?.pagination?.next; + allResults.push(...results[resource]); + } while (results?.pagination?.count === config.limit && allResults.length < max); + if (allResults.length > max) { + allResults.length = max; + } + return allResults; + }, + async getProject(projectId, $) { + const config = { + endpoint: `v9/projects/${projectId}`, + }; + return this.makeRequest(config, $); + }, + async listProjects(params, max, $) { + const config = { + method: "GET", + endpoint: "v9/projects", + params, + }; + return this.paginate("projects", config, max, $); + }, + async listDeployments(params, max, $) { + const config = { + method: "GET", + endpoint: "v6/deployments", + params, + }; + return this.paginate("deployments", config, max, $); + }, + async listTeams(max, $) { + const config = { + method: "GET", + endpoint: "v2/teams", + }; + return this.paginate("teams", config, max, $); + }, + async cancelDeployment(id, params, $) { + const config = { + method: "PATCH", + endpoint: `v12/deployments/${id}/cancel`, + params, + }; + return this.makeRequest(config, $); + }, + async createDeployment(data, $) { + const config = { + method: "POST", + endpoint: "v13/deployments", + data, + }; + if (data.teamId) { + config.endpoint += `?teamId=${data.teamId}`; + } + delete data.teamId; + return this.makeRequest(config, $); + }, }, }; diff --git a/components/verdict_as_a_service/README.md b/components/verdict_as_a_service/README.md index f1542e6177419..c38859bcee105 100644 --- a/components/verdict_as_a_service/README.md +++ b/components/verdict_as_a_service/README.md @@ -1,24 +1,11 @@ # Overview -The Verdict as a Service (VAAS) API allows developers to create AI-driven -applications with confidence. With VAAS, developers can create a wide range of -AI-driven applications such as security and fraud detection, personalization -recommendations, next-best-action, risk assessment, and wayfinding. VAAS -enables developers to quickly, easily and cost-effectively build applications -that leverage the power of AI using the flexibility of a cloud-based platform. +Verdict as a Service (VaaS) API provides a powerful interface for automating the analysis of files and URLs for potential threats. Leveraging the capabilities of Pipedream, users can create sophisticated workflows that trigger actions based on the results of the threat analysis. From email attachments to submitted URLs, VaaS can be integrated into a variety of security and data processing pipelines to ensure safety and compliance. -Here are a few examples of applications you can build using the Verdict as a -Service API: +# Example Use Cases -- Fraud and security detection: With VAAS, developers can create applications - that can detect various kinds of fraud and security risks using AI-fueled - insights. -- Personalization recommendations: You can create personalized customer - experiences with AI-powered product and service recommendations. -- Next-best-action: AI-driven applications can recommend the next best action - for users to take, taking into account a variety of contextual information. -- Risk assessment: VAAS applications can provide tailored risk assessment based - on customer data, providing tailored insights and predictions. -- Wayfinding: Developers can create wayfinding applications that take into - account a variety of contextual information to provide real-time, - personalized directions. +- **Automated Threat Detection for Email Attachments**: Use Pipedream to monitor an email inbox for new messages. Extract attachments and pass them to the VaaS API for analysis. If a threat is detected, trigger an alert to the security team and move the email to a quarantine folder. + +- **Secure File Upload Processing**: Implement a workflow that scans files uploaded to a cloud storage service like Dropbox or Google Drive. Upon file upload, Pipedream sends the file to VaaS for security scanning. If the file is deemed safe, it is then made available to the team; otherwise, the uploader is notified of the issue and the file is removed. + +- **URL Analysis for User Submissions**: When users submit URLs through a form or helpdesk ticket, use Pipedream to automate the submission of those URLs to VaaS. Integrate with a CRM or ticketing system like Zendesk to update the ticket with the analysis results, ensuring only safe links are processed by support staff. diff --git a/components/verifalia/README.md b/components/verifalia/README.md index ed2f44d4647a2..4669be1b61c0d 100644 --- a/components/verifalia/README.md +++ b/components/verifalia/README.md @@ -1,25 +1,11 @@ # Overview -The Verifalia API is a powerful and secure suite of tools designed to validate -& clean data and email addresses. It can be used to help reduce fraud, improve -data quality, and boost marketing campaigns. +Verifalia's API provides robust email validation and verification services, ensuring that email addresses in your lists are accurate and deliverable. Leveraging Verifalia within Pipedream workflows can automate the process of cleaning up email lists, improve email marketing efficiency, and maintain communication channel integrity. By integrating Verifalia's capabilities, you can cut down on bounces, identify disposable email addresses, and segment lists based on quality scores. -Verifalia's API can be used to build the following: +# Example Use Cases -- Email Validation: Validate a list of email addresses and check for accuracy, - validity, and deliverability. -- Email Address Hygiene & Cleaning: Automatically clean a list of email - addresses to remove duplicates, typos, and invalid addresses. -- Email Address List Verification: Check a list of email addresses and verify - them against a specific domain. -- Email Spam Score & Analysis: Analyze a list of emails and determine the - associated spam level. -- Phone Number Validation: Automatically validate a list of phone numbers and - check for accuracy and improper formatting. -- Bulk Email Sending: Execute a bulk email campaign and track email delivery - rate in real-time. -- Private DNS Check & Domain Name Verification: Check a domain's reliability - and identity, as well as DNS records and MX records. -- Domain Catch-all Check & Delivery Confirmation: Determine if a domain is - flagged as a catch-all and confirm if emails sent to that domain will be - received by the intended recipient. +- **Automated Email List Cleaning**: Set up a workflow where new subscribers' email addresses, collected through a sign-up form, are automatically sent to Verifalia for validation. Once verified, the clean email addresses are saved to your CRM or email marketing platform, such as Mailchimp or HubSpot. + +- **Periodic Email Verification Scheduler**: Use Pipedream's cron job feature to schedule regular email verification checks. Email addresses from your database are periodically verified through Verifalia to ensure ongoing list hygiene, and invalid ones are tagged or removed to improve campaign performance. + +- **Instant Verification for New Leads**: When a lead is captured via a lead generation tool like Typeform or a landing page, trigger an immediate email verification process. Only add the leads with valid email addresses to your sales pipeline in a tool like Salesforce, ensuring your sales team works only with high-quality prospects. diff --git a/components/verifone/package.json b/components/verifone/package.json new file mode 100644 index 0000000000000..e2b7dcb398d6f --- /dev/null +++ b/components/verifone/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/verifone", + "version": "0.0.1", + "description": "Pipedream Verifone Components", + "main": "verifone.app.mjs", + "keywords": [ + "pipedream", + "verifone" + ], + "homepage": "https://pipedream.com/apps/verifone", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/verifone/verifone.app.mjs b/components/verifone/verifone.app.mjs new file mode 100644 index 0000000000000..c7e4ad1b4a029 --- /dev/null +++ b/components/verifone/verifone.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "verifone", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/verifybee/README.md b/components/verifybee/README.md index 521314e053199..8e8f888cfbb6e 100644 --- a/components/verifybee/README.md +++ b/components/verifybee/README.md @@ -1,16 +1,11 @@ # Overview -VerifyBee API provides powerful and secure authentication services to help keep -you and your customers safe. With VerifyBee API, you can build an array of -applications that require secure authentication and identity verification -procedures. Here are some examples of things you can build with VerifyBee API: +VerifyBee is a powerful tool designed for validating emails and phone numbers, ensuring that your contact lists are accurate and up-to-date. By integrating VerifyBee with Pipedream, you can automate the process of cleaning your contact data across various platforms, trigger communications with confidence, and maintain the integrity of your CRM or customer databases. The API allows developers to check the validity of emails and phone numbers, which can be crucial for reducing bounce rates and improving communication efficiency. -- Mobile Applications -- Online Registration Systems -- Login Security Systems -- Password Management Tools -- E-commerce Payment Systems -- Secure Digital Documents -- Anti-fraud and Risk Assessments -- High-risk Transactions Verification -- Multi-factor Authorization Solutions +# Example Use Cases + +- **Email Verification for New Subscribers:** Automate the process of verifying emails when new subscribers join your mailing list on platforms such as Mailchimp. If a subscriber's email is verified successfully, add them to a specific email campaign; if not, flag for follow-up or removal. + +- **Phone Number Validation for E-Commerce Checkouts:** Integrate VerifyBee with your e-commerce platform, like Shopify, to validate customer phone numbers during checkout. This can cut down on failed deliveries and improve customer service by ensuring that all contact information is correct before an order is confirmed. + +- **Automated Cleaning of CRM Data:** Schedule regular clean-ups of your CRM data by integrating VerifyBee with Salesforce or HubSpot. Automatically check the validity of contact information within your CRM, update status fields for invalid entries, and notify your sales or marketing team to take action. diff --git a/components/veriphone/README.md b/components/veriphone/README.md new file mode 100644 index 0000000000000..363abc9e9b823 --- /dev/null +++ b/components/veriphone/README.md @@ -0,0 +1,11 @@ +# Overview + +The Veriphone API is your go-to tool within Pipedream for validating and standardizing phone numbers across the globe. It checks phone numbers for correctness, formats them into a standardized E.164 format, and provides carrier and country details. By integrating Veriphone API into your Pipedream workflows, you can automate processes like user verification, data cleaning for marketing campaigns, and enhance the quality of your contact databases. + +# Example Use Cases + +- **User Signup Verification**: When a user signs up for your service, automate a workflow that uses the Veriphone API to validate the phone number they provide. Connect it to your user database (like MySQL or MongoDB) on Pipedream to store the standardized number and user details upon successful verification. + +- **CRM Data Cleaning**: Set up a periodic workflow that sifts through your CRM platform, such as HubSpot or Salesforce, to validate and update the phone numbers in your contact list. This ensures that your sales or customer service teams always have accurate and usable contact information. + +- **SMS Marketing Optimization**: Before sending out a bulk SMS campaign, use a workflow that integrates the Veriphone API to validate the recipients' phone numbers. Connect this to your messaging service, like Twilio, to send messages only to verified numbers, helping you cut costs and improve the delivery success rate. diff --git a/components/vero/README.md b/components/vero/README.md new file mode 100644 index 0000000000000..2651539933017 --- /dev/null +++ b/components/vero/README.md @@ -0,0 +1,11 @@ +# Overview + +Vero's API allows you to automate email marketing tasks such as managing campaigns, users, and events. Integrating Vero with Pipedream enables you to create workflows that respond to various triggers, like webhooks or schedules, and carry out actions such as sending emails, segmenting users, or tracking events. This integration can be a powerful tool to personalize your marketing efforts and respond in real-time to your customers' interactions. + +# Example Use Cases + +- **Customer Onboarding Campaign**: Trigger a Vero workflow in Pipedream when a new user signs up via your app. The workflow can send personalized welcome emails, track user activity, and tag users for segmentation, ensuring a tailored onboarding experience. + +- **Behavior-Based Email Trigger**: Create a Pipedream workflow that listens for specific user actions, like cart abandonment or product views, using webhooks. Use Vero's API to send targeted follow-up emails encouraging users to complete their purchase or explore related products. + +- **Real-Time User Segmentation**: Use Pipedream to process user interactions from your website or app, updating their profiles in Vero in real-time. This can include tagging users based on their activity or updating user attributes, allowing for dynamic segmentation and more effective marketing campaigns. diff --git a/components/verticalresponse/package.json b/components/verticalresponse/package.json new file mode 100644 index 0000000000000..fc7e9689833f7 --- /dev/null +++ b/components/verticalresponse/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/verticalresponse", + "version": "0.0.1", + "description": "Pipedream VerticalResponse Components", + "main": "verticalresponse.app.mjs", + "keywords": [ + "pipedream", + "verticalresponse" + ], + "homepage": "https://pipedream.com/apps/verticalresponse", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/verticalresponse/verticalresponse.app.mjs b/components/verticalresponse/verticalresponse.app.mjs new file mode 100644 index 0000000000000..e241bf98cc5e1 --- /dev/null +++ b/components/verticalresponse/verticalresponse.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "verticalresponse", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/vestaboard/README.md b/components/vestaboard/README.md new file mode 100644 index 0000000000000..031bea70631f6 --- /dev/null +++ b/components/vestaboard/README.md @@ -0,0 +1,11 @@ +# Overview + +The Vestaboard API allows you to control a networked, mechanical split-flap display, enabling you to automate the content it shows. By integrating with Pipedream, you can develop serverless workflows that respond to various triggers (like incoming webhooks, emails, or scheduled timers) and push updates to your Vestaboard. You can dynamically display information such as meeting reminders, real-time performance metrics, or custom messages based on events from other apps and services. + +# Example Use Cases + +- **Dynamic Performance Dashboard**: Use the Vestaboard API on Pipedream to create a live dashboard that displays key business metrics. Connect to apps like Google Sheets or Salesforce, fetch the latest data, and send it to your Vestaboard to keep your team informed and motivated. + +- **Meeting Room Schedule Display**: Create a workflow that integrates with calendar services like Google Calendar or Office 365. Extract upcoming events and format the data to display meeting schedules on your Vestaboard in real-time, ensuring everyone knows when and where their next meeting is. + +- **Slack-Powered Announcements**: Set up a Pipedream workflow that listens for specific commands or messages in Slack. When a trigger phrase is detected, capture the message content and automatically update your Vestaboard with the latest announcements or alerts for your team to see. diff --git a/components/vext/README.md b/components/vext/README.md new file mode 100644 index 0000000000000..bf34ad02e894d --- /dev/null +++ b/components/vext/README.md @@ -0,0 +1,11 @@ +# Overview + +The Vext API lets you automate interactions with the Vext application, which is designed for versatile text transformations, parsing, and encoding. On Pipedream, you can craft serverless workflows that utilize Vext API to process and manipulate text data on-the-fly, without the need for manual intervention. Imagine automating complex text parsing tasks, encoding/decoding data in various formats, or even integrating with other services to enhance data processing pipelines. + +# Example Use Cases + +- **Automate Text Transformation Workflows**: Set up a Pipedream workflow that listens for incoming webhooks containing text data, then uses the Vext API to transform this text according to predefined rules or templates. The result could then be sent to a database, logged, or used as input for further processing steps in the workflow. + +- **Encode/Decode Data on Demand**: Create a workflow that triggers on a schedule or via webhooks to encode or decode data using Vext. For instance, you might encode sensitive information before saving it to a cloud storage service, or decode data received in an encoded format before processing it further or presenting it to users. + +- **Integrate Text Parsing with Data Analysis Tools**: Leverage a Pipedream workflow that takes raw text data from sources like emails or form submissions, parses it using Vext for specific information extraction, and then sends the structured data to a tool like Google Sheets or a BI platform for analysis and reporting. diff --git a/components/vext/actions/send-query/send-query.mjs b/components/vext/actions/send-query/send-query.mjs new file mode 100644 index 0000000000000..01ebe20cf9840 --- /dev/null +++ b/components/vext/actions/send-query/send-query.mjs @@ -0,0 +1,44 @@ +import app from "../../vext.app.mjs"; + +export default { + key: "vext-send-query", + name: "Send Query", + description: "Send a query request to your LLM pipeline. [See the documentation](https://vext.readme.io/reference/http-request-query)", + version: "0.0.1", + type: "action", + props: { + app, + payload: { + propDefinition: [ + app, + "payload", + ], + }, + longPolling: { + propDefinition: [ + app, + "longPolling", + ], + }, + requestId: { + propDefinition: [ + app, + "requestId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.sendQuery({ + $, + data: { + payload: this.payload, + long_polling: this.longPolling, + request_id: this.requestId, + }, + }); + + $.export("$summary", `Query with ID '${response.request_id || response.text.request_id}' successfully sent`); + + return response; + }, +}; diff --git a/components/vext/package.json b/components/vext/package.json new file mode 100644 index 0000000000000..416447a66d1f3 --- /dev/null +++ b/components/vext/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/vext", + "version": "0.1.0", + "description": "Pipedream Vext Components", + "main": "vext.app.mjs", + "keywords": [ + "pipedream", + "vext" + ], + "homepage": "https://pipedream.com/apps/vext", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^2.0.0" + } +} diff --git a/components/vext/vext.app.mjs b/components/vext/vext.app.mjs new file mode 100644 index 0000000000000..b8a72e444820c --- /dev/null +++ b/components/vext/vext.app.mjs @@ -0,0 +1,53 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "vext", + propDefinitions: { + payload: { + type: "string", + label: "Payload", + description: "This is the input that you'll be sending to your LLM pipeline", + }, + longPolling: { + type: "boolean", + label: "Long Polling", + description: "If your pipeline includes several models and you're facing timeout issues, enabling this option will generate a 'request_id' for you", + }, + requestId: { + type: "string", + label: "Request ID", + description: "Include the long_polling request ID in the body to receive updates until the final result is delivered", + optional: true, + }, + }, + methods: { + _baseUrl() { + return `https://payload.vextapp.com/hook/${this.$auth.endpoint_id}/catch/${this.$auth.channel_token}`; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "Content-Type": "application/json", + "Apikey": `Api-key ${this.$auth.api_key}`, + }, + }); + }, + async sendQuery(args = {}) { + return this._makeRequest({ + method: "post", + path: "", + ...args, + }); + }, + }, +}; diff --git a/components/vida/actions/provide-context/provide-context.mjs b/components/vida/actions/provide-context/provide-context.mjs new file mode 100644 index 0000000000000..f769a2ec5447d --- /dev/null +++ b/components/vida/actions/provide-context/provide-context.mjs @@ -0,0 +1,33 @@ +import vida from "../../vida.app.mjs"; + +export default { + key: "vida-provide-context", + name: "Add Context", + description: "Uploads additional context for a conversation with your AI agent. Helpful when integrating data from external CRMs. [See the documentation](https://vida.io/docs/api-reference/knowledge/add-context)", + version: "0.0.1", + type: "action", + props: { + vida, + target: { + type: "string", + label: "Target", + description: "Phone number in E.164 format or VIDA username of the user.", + }, + context: { + type: "string", + label: "Context", + description: "Context information to inject", + }, + }, + async run({ $ }) { + const response = await this.vida.addContext({ + $, + data: { + target: this.target, + context: this.context, + }, + }); + $.export("$summary", `Successfully uploaded additional context for target ${this.target}`); + return response; + }, +}; diff --git a/components/vida/package.json b/components/vida/package.json new file mode 100644 index 0000000000000..a1d6419c2b19d --- /dev/null +++ b/components/vida/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/vida", + "version": "0.1.0", + "description": "Pipedream Vida Components", + "main": "vida.app.mjs", + "keywords": [ + "pipedream", + "vida" + ], + "homepage": "https://pipedream.com/apps/vida", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/vida/sources/common/base.mjs b/components/vida/sources/common/base.mjs new file mode 100644 index 0000000000000..c022d76a02b75 --- /dev/null +++ b/components/vida/sources/common/base.mjs @@ -0,0 +1,49 @@ +import vida from "../../vida.app.mjs"; + +export default { + props: { + vida, + http: { + type: "$.interface.http", + customResponse: false, + }, + db: "$.service.db", + label: { + type: "string", + label: "Label", + description: "Friendly label for webhook", + }, + }, + methods: { + filterEvent() { + return true; + }, + }, + hooks: { + async activate() { + await this.vida.createWebhook({ + data: { + url: this.http.endpoint, + label: this.label, + type: "conversation", + }, + }); + }, + async deactivate() { + await this.vida.deleteWebhook({ + data: { + url: this.http.endpoint, + }, + }); + }, + }, + async run({ body }) { + if (this.filterEvent(body)) { + this.$emit(body, { + id: body.uuid, + summary: this.getSummary(body), + ts: body.timestamp, + }); + } + }, +}; diff --git a/components/vida/sources/new-conversation-instant/new-conversation-instant.mjs b/components/vida/sources/new-conversation-instant/new-conversation-instant.mjs new file mode 100644 index 0000000000000..ebfceeee634ee --- /dev/null +++ b/components/vida/sources/new-conversation-instant/new-conversation-instant.mjs @@ -0,0 +1,19 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "vida-new-conversation-instant", + name: "New Conversation (Instant)", + description: "Emit new events after completion of any communication handled by your Vida AI agent, be it a call, text, or email.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSummary(event) { + return `New conversation created: ${event.uuid}`; + }, + }, + sampleEmit, +}; diff --git a/components/vida/sources/new-conversation-instant/test-event.mjs b/components/vida/sources/new-conversation-instant/test-event.mjs new file mode 100644 index 0000000000000..a628e5d61d40f --- /dev/null +++ b/components/vida/sources/new-conversation-instant/test-event.mjs @@ -0,0 +1,50 @@ +export default { + "from": 999999999999, + "fromUser": "integrations__test", + "to": 1269579, + "toUser": "integrations", + "rate": 0, + "usdRate": 0, + "timestamp": 1727728969, + "date": "2024-09-30T20:42:49.000Z", + "message": "Call from integrations", + "content-type": "text/plain", + "disposition-notification": null, + "roomId": "1269579:1269579", + "status": "success", + "attachments": [], + "isLive": false, + "aiAgent": false, + "aiReward": null, + "aiRewardUsd": null, + "aiLeadRating": null, + "aiLeadRatingReason": null, + "aiAgentOverride": null, + "gift": false, + "source": "call", + "uuid": "86547d26-9b89-41a6-afd1-292085c9b846", + "campaignId": "campa861346f6104e3e4762ef5b936e2984fb", + "fromNumber": "+1234567890", + "toNumber": "integrations", + "notify": false, + "targetInbox": "inbox", + "type": "call", + "duration": 1, + "direction": "outbound", + "missedCall": false, + "bypassAgent": false, + "cnamSpam": false, + "selfCall": true, + "callingUserIsContact": false, + "agentOutcome": null, + "summary": null, + "callDialog": [], + "campaign": true, + "diversion": null, + "voicemailRecording": null, + "eventType": "outbound-call", + "forcedCampaign": true, + "conversation": "", + "collectedData": null, + "functionsRun": [] +} \ No newline at end of file diff --git a/components/vida/sources/new-incoming-conversation-instant/new-incoming-conversation-instant.mjs b/components/vida/sources/new-incoming-conversation-instant/new-incoming-conversation-instant.mjs new file mode 100644 index 0000000000000..1415d0135ea83 --- /dev/null +++ b/components/vida/sources/new-incoming-conversation-instant/new-incoming-conversation-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "vida-new-incoming-conversation-instant", + name: "New Incoming Conversation (Instant)", + description: "Emit new event when an incoming call or message is received before answered by an agent. Useful for providing context about the caller or messenger to your agent before response.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSummary(event) { + return `New incoming ${event.body.communicationSource} from ${event.body.source}`; + }, + filterEvents(body) { + return body.direction === "inbound"; + }, + }, + sampleEmit, +}; diff --git a/components/vida/sources/new-incoming-conversation-instant/test-event.mjs b/components/vida/sources/new-incoming-conversation-instant/test-event.mjs new file mode 100644 index 0000000000000..3f99116e529d6 --- /dev/null +++ b/components/vida/sources/new-incoming-conversation-instant/test-event.mjs @@ -0,0 +1,50 @@ +export default { + "from": 999999999999, + "fromUser": "integrations__test", + "to": 1269579, + "toUser": "integrations", + "rate": 0, + "usdRate": 0, + "timestamp": 1727728969, + "date": "2024-09-30T20:42:49.000Z", + "message": "Call from integrations", + "content-type": "text/plain", + "disposition-notification": null, + "roomId": "1269579:1269579", + "status": "success", + "attachments": [], + "isLive": false, + "aiAgent": false, + "aiReward": null, + "aiRewardUsd": null, + "aiLeadRating": null, + "aiLeadRatingReason": null, + "aiAgentOverride": null, + "gift": false, + "source": "call", + "uuid": "86547d26-9b89-41a6-afd1-292085c9b846", + "campaignId": "campa861346f6104e3e4762ef5b936e2984fb", + "fromNumber": "+1234567890", + "toNumber": "integrations", + "notify": false, + "targetInbox": "inbox", + "type": "call", + "duration": 1, + "direction": "inbound", + "missedCall": false, + "bypassAgent": false, + "cnamSpam": false, + "selfCall": true, + "callingUserIsContact": false, + "agentOutcome": null, + "summary": null, + "callDialog": [], + "campaign": true, + "diversion": null, + "voicemailRecording": null, + "eventType": "inbound-call", + "forcedCampaign": true, + "conversation": "", + "collectedData": null, + "functionsRun": [] +} \ No newline at end of file diff --git a/components/vida/vida.app.mjs b/components/vida/vida.app.mjs new file mode 100644 index 0000000000000..c0baba25e0e57 --- /dev/null +++ b/components/vida/vida.app.mjs @@ -0,0 +1,47 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "vida", + methods: { + _baseUrl() { + return "https://api.vida.dev/api/v2"; + }, + _params(params = {}) { + return { + ...params, + token: `${this.$auth.api_token}`, + }; + }, + _makeRequest({ + $ = this, path, params, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + params: this._params(params), + ...opts, + }); + }, + addContext(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/context", + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteWebhook(opts = {}) { + return this._makeRequest({ + method: "DELETE", + path: "/webhooks", + ...opts, + }); + }, + }, +}; diff --git a/components/videoask/package.json b/components/videoask/package.json new file mode 100644 index 0000000000000..bc7b35ebf412c --- /dev/null +++ b/components/videoask/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/videoask", + "version": "0.0.1", + "description": "Pipedream VideoAsk Components", + "main": "videoask.app.mjs", + "keywords": [ + "pipedream", + "videoask" + ], + "homepage": "https://pipedream.com/apps/videoask", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/videoask/videoask.app.mjs b/components/videoask/videoask.app.mjs new file mode 100644 index 0000000000000..70c468c7d81a0 --- /dev/null +++ b/components/videoask/videoask.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "videoask", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/vies_api/README.md b/components/vies_api/README.md new file mode 100644 index 0000000000000..4c99b15565169 --- /dev/null +++ b/components/vies_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The VIES (VAT Information Exchange System) API lets you validate European VAT numbers. It's a tool for checking the validity of VAT numbers issued by any EU Member State, which is crucial for B2B transactions within the EU to ensure VAT compliance. In Pipedream, you can use the VIES API to automate workflows related to customer verification, invoicing, and compliance checks, streamlining the process of doing business across European borders. + +# Example Use Cases + +- **VAT Validation for New User Sign-ups**: When a new user signs up on your platform and provides a VAT number, trigger a Pipedream workflow to validate it via the VIES API. If it's valid, continue their onboarding process; if not, flag the account for review. + +- **Periodic VAT Number Verification**: Set up a scheduled workflow to periodically check the validity of VAT numbers stored in your customer database. This ensures ongoing compliance and can automatically notify customers if their VAT number becomes invalid. + +- **Automated VAT-Compliant Invoicing**: When creating invoices for EU customers, use the VIES API in a Pipedream workflow to validate VAT numbers and calculate the correct VAT amount. Integrate with accounting software like QuickBooks to automate the entire invoicing process. diff --git a/components/vies_api/actions/retrieve-vat-data/retrieve-vat-data.mjs b/components/vies_api/actions/retrieve-vat-data/retrieve-vat-data.mjs index ca62c8cc4e209..dbc4216622984 100644 --- a/components/vies_api/actions/retrieve-vat-data/retrieve-vat-data.mjs +++ b/components/vies_api/actions/retrieve-vat-data/retrieve-vat-data.mjs @@ -3,8 +3,8 @@ import app from "../../vies_api.app.mjs"; export default { key: "vies_api-retrieve-vat-data", name: "Retrieve VAT Data", - version: "0.0.1", - description: "Get firm data from VIES registry. [See the documentation](https://viesapi.eu/public/rest/viesapi.html#/API/getVIESData)", + version: "0.0.2", + description: "Get firm data from VIES registry. [See the documentation](https://viesapi.eu/public/rest/index.html#/API/getVIESData)", type: "action", props: { app, diff --git a/components/vies_api/package.json b/components/vies_api/package.json index 8512c16f8560c..03da49cb4a83a 100644 --- a/components/vies_api/package.json +++ b/components/vies_api/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/vies_api", - "version": "0.1.0", + "version": "0.1.1", "description": "Pipedream VIES API Components", "main": "vies_api.app.mjs", "keywords": [ diff --git a/components/vimeo/README.md b/components/vimeo/README.md index ce76cbbc67e6b..ac4a2dfb8beb6 100644 --- a/components/vimeo/README.md +++ b/components/vimeo/README.md @@ -1,23 +1,11 @@ # Overview -The Vimeo API is a powerful tool for developers to access a wide range of -features on the Vimeo platform. With the Vimeo API, you can build a variety of -applications to enhance the user experience sharing videos. Here are a few -examples of applications that can be built using the Vimeo API: +The Vimeo API equips you to interact programmatically with the Vimeo platform, allowing you to manage videos, users, and albums. With Pipedream, you can choreograph this functionality into automation, integrating Vimeo's capabilities with countless other services. Pipedream's serverless platform streamlines the process of setting up event-driven workflows, custom reactions to video uploads, or even coordinating marketing efforts that hinge on your video content. -- Video streaming applications: With the Vimeo API, developers can quickly - build streaming applications that offer users high-quality, secure, and - reliable video streaming services. -- Content discovery tools: Vimeo is home to a wide variety of content that you - can make more accessible with content discovery tools built with the Vimeo - API. -- Engagement platforms: With the Vimeo API, developers can craft engaging - platforms that allow users to easily find, watch, and interact with videos. -- Editing tools: With the Vimeo API, developers can easily create editing tools - that give users the ability to quickly and easily edit their video files. -- Video usage analytics: Vimeo’s API offers developers powerful video - analytics, providing users with insights into how their videos are being - watched, helping to make better decisions about their content. -- Video monetization: The Vimeo API is a great tool for monetizing videos, - allowing users to easily set up paywalls and offer subscriptions for - controlled access to their videos. +# Example Use Cases + +- **Automated Video Content Distribution**: When a new video is uploaded to Vimeo, trigger a workflow that shares the video link across multiple social media platforms like Twitter, LinkedIn, and Facebook, maximizing your audience reach without manual intervention. + +- **Content Moderation Workflow**: Set up a Pipedream workflow that scans comments on your Vimeo videos for specific keywords or sentiment indicators. If inappropriate content is detected, the workflow can automatically delete the comment and notify a moderator or take any other predefined action. + +- **Video Analytics Reporting**: Whenever a video is viewed on Vimeo, trigger a Pipedream workflow that logs analytics data to a Google Sheet or any database of your choice. This could include view counts, viewer location data, or engagement metrics, helping you develop a deeper understanding of your audience. diff --git a/components/vimeo/actions/add-video-to-album/add-video-to-album.mjs b/components/vimeo/actions/add-video-to-album/add-video-to-album.mjs new file mode 100644 index 0000000000000..b88cade4b5a67 --- /dev/null +++ b/components/vimeo/actions/add-video-to-album/add-video-to-album.mjs @@ -0,0 +1,39 @@ +import vimeo from "../../vimeo.app.mjs"; + +export default { + key: "vimeo-add-video-to-album", + name: "Add Video To Album", + description: "Adds an existing video to a user's album/showcase on Vimeo. [See the documentation](https://developer.vimeo.com/api/reference/showcases#update_showcases)", + version: "0.0.1", + type: "action", + props: { + vimeo, + videoId: { + propDefinition: [ + vimeo, + "videoId", + ], + }, + albumUri: { + propDefinition: [ + vimeo, + "albumUri", + ], + }, + }, + async run({ $ }) { + const response = await this.vimeo.addVideoToAlbum({ + $, + videoId: this.videoId, + data: { + add: [ + { + uri: this.albumUri, + }, + ], + }, + }); + $.export("$summary", `Successfully added video ${this.videoId} to album ${this.albumUri}`); + return response; + }, +}; diff --git a/components/vimeo/actions/delete-video/delete-video.mjs b/components/vimeo/actions/delete-video/delete-video.mjs new file mode 100644 index 0000000000000..1500593e8375a --- /dev/null +++ b/components/vimeo/actions/delete-video/delete-video.mjs @@ -0,0 +1,26 @@ +import vimeo from "../../vimeo.app.mjs"; + +export default { + key: "vimeo-delete-video", + name: "Delete Video", + description: "Permanently deletes a video from the user's Vimeo account. This action can't be undone. [See the documentation](https://developer.vimeo.com/api/reference/videos#delete_video)", + version: "0.0.1", + type: "action", + props: { + vimeo, + videoId: { + propDefinition: [ + vimeo, + "videoId", + ], + }, + }, + async run({ $ }) { + const response = await this.vimeo.deleteVideo({ + $, + videoId: this.videoId, + }); + $.export("$summary", `Successfully deleted video with ID: ${this.videoId}`); + return response; + }, +}; diff --git a/components/vimeo/actions/upload-video/upload-video.mjs b/components/vimeo/actions/upload-video/upload-video.mjs new file mode 100644 index 0000000000000..d467df54fab40 --- /dev/null +++ b/components/vimeo/actions/upload-video/upload-video.mjs @@ -0,0 +1,44 @@ +import vimeo from "../../vimeo.app.mjs"; + +export default { + key: "vimeo-upload-video", + name: "Upload Video", + description: "Uploads a video to the user's Vimeo account. Ensure you have enough storage quota on your account. [See the documentation](https://developer.vimeo.com/api/reference/videos#upload_video)", + version: "0.0.1", + type: "action", + props: { + vimeo, + videoUrl: { + type: "string", + label: "Video URL", + description: "URL of the video file to upload", + }, + name: { + type: "string", + label: "Name", + description: "The title of the video", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "The description of the video", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.vimeo.uploadVideo({ + $, + data: { + upload: { + approach: "pull", + link: this.videoUrl, + }, + name: this.name, + description: this.description, + }, + }); + $.export("$summary", `Successfully uploaded video with ID: ${response.uri.split("/").pop()}`); + return response; + }, +}; diff --git a/components/vimeo/common/constants.mjs b/components/vimeo/common/constants.mjs new file mode 100644 index 0000000000000..04b8ac8167611 --- /dev/null +++ b/components/vimeo/common/constants.mjs @@ -0,0 +1,5 @@ +const DEFAULT_LIMIT = 100; + +export default { + DEFAULT_LIMIT, +}; diff --git a/components/vimeo/package.json b/components/vimeo/package.json new file mode 100644 index 0000000000000..cde13aa4595d6 --- /dev/null +++ b/components/vimeo/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/vimeo", + "version": "0.0.1", + "description": "Pipedream Vimeo Components", + "main": "vimeo.app.mjs", + "keywords": [ + "pipedream", + "vimeo" + ], + "homepage": "https://pipedream.com/apps/vestaboard", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedreamhq/platform": "^0.8.1" + } +} diff --git a/components/vimeo/sources/common/base.mjs b/components/vimeo/sources/common/base.mjs new file mode 100644 index 0000000000000..ea4d7ddaf02c0 --- /dev/null +++ b/components/vimeo/sources/common/base.mjs @@ -0,0 +1,74 @@ +import vimeo from "../../vimeo.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + vimeo, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + generateMeta(video) { + return { + id: video.uri.split("/").pop(), + summary: this.getSummary(video), + ts: Date.parse(video.created_time), + }; + }, + emitEvent(video) { + const meta = this.generateMeta(video); + this.$emit(video, meta); + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + + const resourceFn = this.getResourceFn(); + const params = this.getParams(); + + const items = this.vimeo.paginate({ + resourceFn, + params, + max, + }); + + for await (const item of items) { + const ts = Date.parse(item.created_time); + if (ts >= lastTs) { + maxTs = Math.max(ts, maxTs); + this.emitEvent(item); + } + } + + this._setLastTs(maxTs); + }, + getParams() { + return {}; + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/vimeo/sources/new-video-by-search/new-video-by-search.mjs b/components/vimeo/sources/new-video-by-search/new-video-by-search.mjs new file mode 100644 index 0000000000000..a5928a7edc9c2 --- /dev/null +++ b/components/vimeo/sources/new-video-by-search/new-video-by-search.mjs @@ -0,0 +1,40 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "vimeo-new-video-by-search", + name: "New Video by Search", + description: "Emit new event each time a new video matching the search terms is added.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + searchTerm: { + type: "string", + label: "Search Term", + description: "The term to search for new videos", + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.vimeo.searchVideos; + }, + getParams() { + return { + query: this.searchTerm, + sort: "date", + direction: "desc", + }; + }, + getSummary(video) { + return `New video matching search: ${video.name}`; + }, + }, + async run() { + await this.processEvent(100); + }, + sampleEmit, +}; diff --git a/components/vimeo/sources/new-video-by-search/test-event.mjs b/components/vimeo/sources/new-video-by-search/test-event.mjs new file mode 100644 index 0000000000000..81e192ea5f724 --- /dev/null +++ b/components/vimeo/sources/new-video-by-search/test-event.mjs @@ -0,0 +1,723 @@ +export default { + "uri": "/videos/926042886", + "name": "pipedream testing", + "description": "pipedream testing", + "type": "video", + "link": "https://vimeo.com/926042886", + "player_embed_url": "https://player.vimeo.com/video/926042886?h=fa95111d95", + "duration": 30, + "width": 480, + "language": null, + "height": 270, + "embed": { + "html": "", + "badges": { + "hdr": false, + "live": { + "streaming": false, + "archived": false + }, + "staff_pick": { + "normal": false, + "best_of_the_month": false, + "best_of_the_year": false, + "premiere": false + }, + "vod": false, + "weekend_challenge": false + }, + "interactive": false, + "buttons": { + "watchlater": true, + "share": true, + "embed": true, + "hd": false, + "fullscreen": true, + "scaling": true, + "like": true + }, + "logos": { + "vimeo": true, + "custom": { + "active": false, + "url": null, + "link": null, + "use_link": false, + "sticky": false + } + }, + "play_button": { + "position": "auto" + }, + "title": { + "name": "user", + "owner": "user", + "portrait": "user" + }, + "end_screen": [], + "playbar": true, + "quality_selector": null, + "pip": true, + "autopip": true, + "volume": true, + "color": "00adef", + "colors": { + "color_one": "000000", + "color_two": "00adef", + "color_three": "ffffff", + "color_four": "000000" + }, + "event_schedule": true, + "has_cards": false, + "outro_type": "videos", + "show_timezone": false, + "cards": [], + "airplay": true, + "audio_tracks": true, + "chapters": true, + "chromecast": true, + "closed_captions": true, + "transcript": true, + "uri": null, + "email_capture_form": null, + "speed": true + }, + "created_time": "2024-03-21T22:03:01+00:00", + "modified_time": "2024-03-21T22:03:47+00:00", + "release_time": "2024-03-21T22:03:01+00:00", + "content_rating": [ + "unrated" + ], + "content_rating_class": "unrated", + "rating_mod_locked": false, + "license": null, + "privacy": { + "view": "anybody", + "embed": "public", + "download": false, + "add": true, + "comments": "anybody" + }, + "pictures": { + "uri": "/videos/926042886/pictures/1820037216", + "active": true, + "type": "custom", + "base_link": "https://i.vimeocdn.com/video/1820037216-12749fc0e022c8478c1fa894d654828aa04329dc5e075c0c30d5bc128bd1000a-d", + "sizes": [ + { + "width": 100, + "height": 75, + "link": "https://i.vimeocdn.com/video/1820037216-12749fc0e022c8478c1fa894d654828aa04329dc5e075c0c30d5bc128bd1000a-d_100x75?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1820037216-12749fc0e022c8478c1fa894d654828aa04329dc5e075c0c30d5bc128bd1000a-d_100x75&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 200, + "height": 150, + "link": "https://i.vimeocdn.com/video/1820037216-12749fc0e022c8478c1fa894d654828aa04329dc5e075c0c30d5bc128bd1000a-d_200x150?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1820037216-12749fc0e022c8478c1fa894d654828aa04329dc5e075c0c30d5bc128bd1000a-d_200x150&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 295, + "height": 166, + "link": "https://i.vimeocdn.com/video/1820037216-12749fc0e022c8478c1fa894d654828aa04329dc5e075c0c30d5bc128bd1000a-d_295x166?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1820037216-12749fc0e022c8478c1fa894d654828aa04329dc5e075c0c30d5bc128bd1000a-d_295x166&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 640, + "height": 360, + "link": "https://i.vimeocdn.com/video/1820037216-12749fc0e022c8478c1fa894d654828aa04329dc5e075c0c30d5bc128bd1000a-d_640x360?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1820037216-12749fc0e022c8478c1fa894d654828aa04329dc5e075c0c30d5bc128bd1000a-d_640x360&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 960, + "height": 540, + "link": "https://i.vimeocdn.com/video/1820037216-12749fc0e022c8478c1fa894d654828aa04329dc5e075c0c30d5bc128bd1000a-d_960x540?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1820037216-12749fc0e022c8478c1fa894d654828aa04329dc5e075c0c30d5bc128bd1000a-d_960x540&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 1280, + "height": 720, + "link": "https://i.vimeocdn.com/video/1820037216-12749fc0e022c8478c1fa894d654828aa04329dc5e075c0c30d5bc128bd1000a-d_1280x720?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1820037216-12749fc0e022c8478c1fa894d654828aa04329dc5e075c0c30d5bc128bd1000a-d_1280x720&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 1920, + "height": 1080, + "link": "https://i.vimeocdn.com/video/1820037216-12749fc0e022c8478c1fa894d654828aa04329dc5e075c0c30d5bc128bd1000a-d_1920x1080?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1820037216-12749fc0e022c8478c1fa894d654828aa04329dc5e075c0c30d5bc128bd1000a-d_1920x1080&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + } + ], + "resource_key": "287cc2640807ff28c15680d2430b29524245f479", + "default_picture": false + }, + "tags": [], + "stats": { + "plays": 0 + }, + "categories": [], + "uploader": { + "pictures": { + "uri": "/users/217071126/pictures/97277504", + "active": true, + "type": "custom", + "base_link": "https://i.vimeocdn.com/portrait/97277504", + "sizes": [ + { + "width": 30, + "height": 30, + "link": "https://i.vimeocdn.com/portrait/97277504_30x30" + }, + { + "width": 72, + "height": 72, + "link": "https://i.vimeocdn.com/portrait/97277504_72x72" + }, + { + "width": 75, + "height": 75, + "link": "https://i.vimeocdn.com/portrait/97277504_75x75" + }, + { + "width": 100, + "height": 100, + "link": "https://i.vimeocdn.com/portrait/97277504_100x100" + }, + { + "width": 144, + "height": 144, + "link": "https://i.vimeocdn.com/portrait/97277504_144x144" + }, + { + "width": 216, + "height": 216, + "link": "https://i.vimeocdn.com/portrait/97277504_216x216" + }, + { + "width": 288, + "height": 288, + "link": "https://i.vimeocdn.com/portrait/97277504_288x288" + }, + { + "width": 300, + "height": 300, + "link": "https://i.vimeocdn.com/portrait/97277504_300x300" + }, + { + "width": 360, + "height": 360, + "link": "https://i.vimeocdn.com/portrait/97277504_360x360" + } + ], + "resource_key": "c81484dfb71289acb2fd170037263a2dcaddefb8", + "default_picture": false + } + }, + "metadata": { + "connections": { + "comments": { + "uri": "/videos/926042886/comments", + "options": [ + "GET", + "POST" + ], + "total": 0 + }, + "credits": { + "uri": "/videos/926042886/credits", + "options": [ + "GET", + "POST" + ], + "total": 0 + }, + "likes": { + "uri": "/videos/926042886/likes", + "options": [ + "GET" + ], + "total": 0 + }, + "pictures": { + "uri": "/videos/926042886/pictures", + "options": [ + "GET", + "POST" + ], + "total": 1 + }, + "texttracks": { + "uri": "/videos/926042886/texttracks", + "options": [ + "GET", + "POST" + ], + "total": 0 + }, + "related": { + "uri": "/videos?query=pipedream&sort=date&direction=desc&page=1&per_page=100&offset=1", + "options": [ + "GET" + ] + }, + "recommendations": { + "uri": "/videos/926042886/recommendations", + "options": [ + "GET" + ] + }, + "albums": { + "uri": "/videos/926042886/albums", + "options": [ + "GET", + "PATCH" + ], + "total": 0 + }, + "available_albums": { + "uri": "/videos/926042886/available_albums", + "options": [ + "GET" + ], + "total": 1 + }, + "available_channels": { + "uri": "/videos/926042886/available_channels", + "options": [ + "GET" + ], + "total": 0 + }, + "versions": { + "uri": "/videos/926042886/versions", + "options": [ + "GET" + ], + "total": 1, + "current_uri": "/videos/926042886/versions/909075280", + "resource_key": "d1cdb1e5b2b824a8eb068fed178f858ce9f9646a", + "latest_incomplete_version": null + } + }, + "interactions": { + "watchlater": { + "uri": "/users/217071126/watchlater/926042886", + "options": [ + "GET", + "PUT", + "DELETE" + ], + "added": false, + "added_time": null + }, + "report": { + "uri": "/videos/926042886/report", + "options": [ + "POST" + ], + "reason": [ + "pornographic", + "harassment", + "ripoff", + "incorrect rating", + "spam", + "causes harm", + "csam" + ] + }, + "view_team_members": { + "uri": "/videos/926042886/teammembers", + "options": [ + "GET" + ] + }, + "edit": { + "uri": "/videos/926042886", + "options": [ + "PATCH" + ], + "blocked_fields": [ + "custom_url" + ] + }, + "edit_content_rating": { + "uri": "/videos/926042886", + "options": [ + "PATCH" + ], + "content_rating": [ + "language", + "drugs", + "violence", + "nudity", + "advertisement", + "safe", + "unrated" + ] + }, + "edit_privacy": { + "uri": "/videos/926042886", + "options": [ + "PATCH" + ], + "content_type": "application/vnd.vimeo.video", + "properties": [ + { + "name": "privacy.view", + "required": true, + "options": [ + "anybody", + "nobody", + "password", + "disable", + "unlisted" + ] + } + ] + }, + "delete": { + "uri": "/videos/926042886", + "options": [ + "DELETE" + ] + }, + "can_update_privacy_to_public": { + "uri": "/videos/926042886", + "options": [ + "PATCH" + ] + }, + "trim": { + "uri": "/videos/926042886/cliptrim", + "options": [ + "GET", + "POST" + ] + }, + "validate": { + "uri": "/videos/926042886/validate", + "options": [ + "PUT" + ] + } + }, + "is_vimeo_create": false, + "is_screen_record": false + }, + "manage_link": "/manage/videos/926042886", + "user": { + "uri": "/users/217071126", + "name": "Test User", + "link": "https://vimeo.com/user217071126", + "capabilities": { + "hasLiveSubscription": false, + "hasEnterpriseLihp": false, + "hasSvvTimecodedComments": false, + "hasSimplifiedEnterpriseAccount": false + }, + "location": "", + "gender": "", + "bio": null, + "short_bio": null, + "created_time": "2024-03-21T14:41:15+00:00", + "pictures": { + "uri": "/users/217071126/pictures/97277504", + "active": true, + "type": "custom", + "base_link": "https://i.vimeocdn.com/portrait/97277504", + "sizes": [ + { + "width": 30, + "height": 30, + "link": "https://i.vimeocdn.com/portrait/97277504_30x30" + }, + { + "width": 72, + "height": 72, + "link": "https://i.vimeocdn.com/portrait/97277504_72x72" + }, + { + "width": 75, + "height": 75, + "link": "https://i.vimeocdn.com/portrait/97277504_75x75" + }, + { + "width": 100, + "height": 100, + "link": "https://i.vimeocdn.com/portrait/97277504_100x100" + }, + { + "width": 144, + "height": 144, + "link": "https://i.vimeocdn.com/portrait/97277504_144x144" + }, + { + "width": 216, + "height": 216, + "link": "https://i.vimeocdn.com/portrait/97277504_216x216" + }, + { + "width": 288, + "height": 288, + "link": "https://i.vimeocdn.com/portrait/97277504_288x288" + }, + { + "width": 300, + "height": 300, + "link": "https://i.vimeocdn.com/portrait/97277504_300x300" + }, + { + "width": 360, + "height": 360, + "link": "https://i.vimeocdn.com/portrait/97277504_360x360" + } + ], + "resource_key": "c81484dfb71289acb2fd170037263a2dcaddefb8", + "default_picture": false + }, + "websites": [], + "metadata": { + "connections": { + "albums": { + "uri": "/users/217071126/albums", + "options": [ + "GET" + ], + "total": 1 + }, + "appearances": { + "uri": "/users/217071126/appearances", + "options": [ + "GET" + ], + "total": 0 + }, + "categories": { + "uri": "/users/217071126/categories", + "options": [ + "GET" + ], + "total": 0 + }, + "channels": { + "uri": "/users/217071126/channels", + "options": [ + "GET" + ], + "total": 0 + }, + "feed": { + "uri": "/users/217071126/feed", + "options": [ + "GET" + ] + }, + "followers": { + "uri": "/users/217071126/followers", + "options": [ + "GET" + ], + "total": 0 + }, + "following": { + "uri": "/users/217071126/following", + "options": [ + "GET" + ], + "total": 0 + }, + "groups": { + "uri": "/users/217071126/groups", + "options": [ + "GET" + ], + "total": 0 + }, + "likes": { + "uri": "/users/217071126/likes", + "options": [ + "GET" + ], + "total": 1 + }, + "membership": { + "uri": "/users/217071126/membership/", + "options": [ + "PATCH" + ] + }, + "moderated_channels": { + "uri": "/users/217071126/channels?filter=moderated", + "options": [ + "GET" + ], + "total": 0 + }, + "portfolios": { + "uri": "/users/217071126/portfolios", + "options": [ + "GET" + ], + "total": 0 + }, + "videos": { + "uri": "/users/217071126/videos", + "options": [ + "GET" + ], + "total": 2 + }, + "watchlater": { + "uri": "/users/217071126/watchlater", + "options": [ + "GET" + ], + "total": 0 + }, + "shared": { + "uri": "/users/217071126/shared/videos", + "options": [ + "GET" + ], + "total": 0 + }, + "pictures": { + "uri": "/users/217071126/pictures", + "options": [ + "GET", + "POST" + ], + "total": 1 + }, + "watched_videos": { + "uri": "/me/watched/videos", + "options": [ + "GET" + ], + "total": 0 + }, + "folders_root": { + "uri": "/users/217071126/folders/root", + "options": [ + "GET" + ] + }, + "folders": { + "uri": "/users/217071126/folders", + "options": [ + "GET", + "POST" + ], + "total": 0 + }, + "teams": { + "uri": "/users/217071126/teams", + "options": [ + "GET" + ], + "total": 1 + }, + "block": { + "uri": "/me/block", + "options": [ + "GET" + ], + "total": 0 + } + } + }, + "location_details": { + "formatted_address": "", + "latitude": null, + "longitude": null, + "city": null, + "state": null, + "neighborhood": null, + "sub_locality": null, + "state_iso_code": null, + "country": null, + "country_iso_code": null + }, + "skills": [], + "available_for_hire": false, + "can_work_remotely": false, + "preferences": { + "videos": { + "rating": [ + "unrated" + ], + "privacy": { + "view": "anybody", + "comments": "anybody", + "embed": "public", + "download": true, + "add": true, + "allow_share_link": true + } + }, + "webinar_registrant_lower_watermark_banner_dismissed": [] + }, + "content_filter": [ + "language", + "drugs", + "violence", + "nudity", + "safe", + "unrated" + ], + "upload_quota": { + "space": { + "free": 1072171800, + "max": 1073741824, + "used": 1570024, + "showing": "lifetime", + "unit": "video_size" + }, + "periodic": { + "period": null, + "unit": null, + "free": null, + "max": null, + "used": null, + "reset_date": null + }, + "lifetime": { + "unit": "video_size", + "free": 1072171800, + "max": 1073741824, + "used": 1570024 + } + }, + "resource_key": "5fc63eb4b96a33b6356ca60c25f0bad1afa5deb2", + "account": "free" + }, + "last_user_action_event_date": "2024-03-21T22:03:01+00:00", + "parent_folder": null, + "review_page": { + "active": true, + "link": "https://vimeo.com/user217071126/review/926042886/0d32b18cd6", + "is_shareable": true + }, + "app": { + "name": "Pipedream", + "uri": "/apps/187277" + }, + "play": { + "status": "playable" + }, + "status": "available", + "resource_key": "8b631da97ac69bc74325e5c0afbe2034acf81782", + "upload": { + "status": "complete", + "link": null, + "upload_link": null, + "form": null, + "approach": null, + "size": null, + "redirect_url": null + }, + "transcode": { + "status": "complete" + }, + "is_playable": true, + "has_audio": true +} \ No newline at end of file diff --git a/components/vimeo/sources/new-video-liked/new-video-liked.mjs b/components/vimeo/sources/new-video-liked/new-video-liked.mjs new file mode 100644 index 0000000000000..afbcacbfb2e0a --- /dev/null +++ b/components/vimeo/sources/new-video-liked/new-video-liked.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "vimeo-new-video-liked", + name: "New Video Liked", + description: "Emit new event each time the user likes a new video on Vimeo.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.vimeo.listLikedVideos; + }, + getSummary(video) { + return `New liked video: ${video.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/vimeo/sources/new-video-liked/test-event.mjs b/components/vimeo/sources/new-video-liked/test-event.mjs new file mode 100644 index 0000000000000..6389c9d6ec806 --- /dev/null +++ b/components/vimeo/sources/new-video-liked/test-event.mjs @@ -0,0 +1,1209 @@ +export default { + "uri": "/videos/917286232", + "name": "SUSPENDED IN SPACE", + "description": "Two skiers are suspended in space and time as the frozen winter scape of the Yukon Territory awakens their souls. \n\nThe film was made as a nationwide partnership to advocate the natural beauty of the Yukon. Thank you to our partners, Travel Yukon, Air North, HeliCat Canada, and Yukon Heliski. The film was captured in the territories of Taku River Tlingit First Nations (TRTFN), Carcross/Tagish First Nations (CTFN), and The Champagne and Aishihik First Nations (CAFN).\n\nDirector: Cameron Thuman\nProduction Company: NativeFour\nStarring Kajsa Larsson and Celeste Pomerantz\nProducer: Dilan Mistry\nDirector of Photography: Christopher Clark\nAerial Cinematographer: Zachary Moxley\nEditor: Christian Whittemore\nComposer: Liam Mour\nColor House: Company 3\nColorist: Parker Jarvie\nColor Producer: Dan Butler\nSound Studio: Defacto Sound\nAssociate Producer: Dan Counihan", + "type": "video", + "link": "https://vimeo.com/917286232", + "player_embed_url": "https://player.vimeo.com/video/917286232?h=3b017121a3", + "duration": 390, + "width": 3104, + "language": "en", + "height": 2160, + "embed": { + "html": "", + "badges": { + "hdr": false, + "live": { + "streaming": false, + "archived": false + }, + "staff_pick": { + "normal": true, + "best_of_the_month": false, + "best_of_the_year": false, + "premiere": false + }, + "vod": false, + "weekend_challenge": false + }, + "interactive": false + }, + "created_time": "2024-02-27T21:50:53+00:00", + "modified_time": "2024-03-21T21:29:09+00:00", + "release_time": "2024-02-27T21:50:53+00:00", + "content_rating": [ + "safe" + ], + "content_rating_class": "safe", + "rating_mod_locked": false, + "license": null, + "privacy": { + "view": "anybody", + "embed": "public", + "download": false, + "add": true, + "comments": "anybody" + }, + "pictures": { + "uri": "/videos/917286232/pictures/1805656785", + "active": true, + "type": "custom", + "base_link": "https://i.vimeocdn.com/video/1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d", + "sizes": [ + { + "width": 100, + "height": 75, + "link": "https://i.vimeocdn.com/video/1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_100x75?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_100x75&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 200, + "height": 150, + "link": "https://i.vimeocdn.com/video/1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_200x150?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_200x150&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 295, + "height": 166, + "link": "https://i.vimeocdn.com/video/1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_295x166?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_295x166&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 640, + "height": 360, + "link": "https://i.vimeocdn.com/video/1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_640x360?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_640x360&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 960, + "height": 540, + "link": "https://i.vimeocdn.com/video/1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_960x540?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_960x540&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 1280, + "height": 720, + "link": "https://i.vimeocdn.com/video/1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_1280x720?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_1280x720&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 1920, + "height": 1080, + "link": "https://i.vimeocdn.com/video/1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_1920x1080?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_1920x1080&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + } + ], + "resource_key": "eab0da371632f14a8e24bc543c56396ec23b4973", + "default_picture": false + }, + "tags": [ + { + "uri": "/tags/suspendedinspaceandtime", + "name": "Suspended in Space and Time", + "tag": "Suspended in Space and Time", + "canonical": "suspendedinspaceandtime", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/suspendedinspaceandtime/videos", + "options": [ + "GET" + ], + "total": 1 + } + } + }, + "resource_key": "010f9e1faa54feab79c10d38e35b2bb9709e7c12" + }, + { + "uri": "/tags/suspendedinspace", + "name": "Suspended in Space", + "tag": "Suspended in Space", + "canonical": "suspendedinspace", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/suspendedinspace/videos", + "options": [ + "GET" + ], + "total": 1 + } + } + }, + "resource_key": "92d5dac1b62f1be7bfaa54679276e3546a44f443" + }, + { + "uri": "/tags/whitehorse", + "name": "Whitehorse", + "tag": "Whitehorse", + "canonical": "whitehorse", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/whitehorse/videos", + "options": [ + "GET" + ], + "total": 1037 + } + } + }, + "resource_key": "06163f68ce4979c9911a5918254dcfe58c0cffb4" + }, + { + "uri": "/tags/canada’syukonterritory", + "name": "Canada’s Yukon territory", + "tag": "Canada’s Yukon territory", + "canonical": "canada’syukonterritory", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/canada’syukonterritory/videos", + "options": [ + "GET" + ], + "total": 1 + } + } + }, + "resource_key": "b26ff516e0ea322ccfe13f415c4cd6e40bcfd993" + }, + { + "uri": "/tags/yukoncanada", + "name": "Yukon Canada", + "tag": "Yukon Canada", + "canonical": "yukoncanada", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/yukoncanada/videos", + "options": [ + "GET" + ], + "total": 7 + } + } + }, + "resource_key": "6c9b7d4f0ee2a3c9b302dd114a57a2d50743480d" + }, + { + "uri": "/tags/travelyukon", + "name": "Travel Yukon", + "tag": "Travel Yukon", + "canonical": "travelyukon", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/travelyukon/videos", + "options": [ + "GET" + ], + "total": 8 + } + } + }, + "resource_key": "fed1e011859f6086e947d2dc05ad46aed32ee70e" + }, + { + "uri": "/tags/documentary", + "name": "Documentary", + "tag": "Documentary", + "canonical": "documentary", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/documentary/videos", + "options": [ + "GET" + ], + "total": 475523 + } + } + }, + "resource_key": "c30fecf7d6d5627b3b84ef149121de4af05e6132" + }, + { + "uri": "/tags/shortfilm", + "name": "Short film", + "tag": "Short film", + "canonical": "shortfilm", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/shortfilm/videos", + "options": [ + "GET" + ], + "total": 327527 + } + } + }, + "resource_key": "622974228860564011834cef822426380358fb5d" + }, + { + "uri": "/tags/yukon", + "name": "Yukon", + "tag": "Yukon", + "canonical": "yukon", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/yukon/videos", + "options": [ + "GET" + ], + "total": 4137 + } + } + }, + "resource_key": "ac5b667260598e5dbb5378d7d50994edb2aa4bce" + }, + { + "uri": "/tags/snow", + "name": "Snow", + "tag": "Snow", + "canonical": "snow", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/snow/videos", + "options": [ + "GET" + ], + "total": 134585 + } + } + }, + "resource_key": "f761d20fcb95e0befc3a453194a914104b2bc4e3" + }, + { + "uri": "/tags/winter", + "name": "Winter", + "tag": "Winter", + "canonical": "winter", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/winter/videos", + "options": [ + "GET" + ], + "total": 105543 + } + } + }, + "resource_key": "50fbc5abd4c038c02af840446d57013d34dd13ae" + }, + { + "uri": "/tags/red", + "name": "RED", + "tag": "RED", + "canonical": "red", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/red/videos", + "options": [ + "GET" + ], + "total": 125252 + } + } + }, + "resource_key": "3167acb83804f4f72c23e8055ec4b87f569f977f" + }, + { + "uri": "/tags/cameronthuman", + "name": "Cameron Thuman", + "tag": "Cameron Thuman", + "canonical": "cameronthuman", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/cameronthuman/videos", + "options": [ + "GET" + ], + "total": 44 + } + } + }, + "resource_key": "74f7a56a6d72f6d9cdd3ab377e7a96e9c46dbb97" + }, + { + "uri": "/tags/christopherclark", + "name": "Christopher Clark", + "tag": "Christopher Clark", + "canonical": "christopherclark", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/christopherclark/videos", + "options": [ + "GET" + ], + "total": 28 + } + } + }, + "resource_key": "18a3166441274374f7dc1c66d62f85faf5be3088" + }, + { + "uri": "/tags/celestepomerantz", + "name": "Celeste Pomerantz", + "tag": "Celeste Pomerantz", + "canonical": "celestepomerantz", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/celestepomerantz/videos", + "options": [ + "GET" + ], + "total": 1 + } + } + }, + "resource_key": "200744803252698431c85d3c01287e3dc557addb" + }, + { + "uri": "/tags/kajsalarsson", + "name": "Kajsa Larsson", + "tag": "Kajsa Larsson", + "canonical": "kajsalarsson", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/kajsalarsson/videos", + "options": [ + "GET" + ], + "total": 5 + } + } + }, + "resource_key": "77773f2ff4b9015bc628b4447ac4dcc541e5014e" + }, + { + "uri": "/tags/nativefour", + "name": "NativeFour", + "tag": "NativeFour", + "canonical": "nativefour", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/nativefour/videos", + "options": [ + "GET" + ], + "total": 1 + } + } + }, + "resource_key": "35d5270add2f9db242a30c8952883ca011b0d504" + }, + { + "uri": "/tags/zacharymoxley", + "name": "Zachary Moxley", + "tag": "Zachary Moxley", + "canonical": "zacharymoxley", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/zacharymoxley/videos", + "options": [ + "GET" + ], + "total": 5 + } + } + }, + "resource_key": "d5b1ef0b14e66dbe1a4028ee432da349829d38bb" + }, + { + "uri": "/tags/skiingmovie", + "name": "Skiing movie", + "tag": "Skiing movie", + "canonical": "skiingmovie", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/skiingmovie/videos", + "options": [ + "GET" + ], + "total": 11 + } + } + }, + "resource_key": "376eb494d7ddc02b74cebe0fa18cd84c67ffe742" + }, + { + "uri": "/tags/skiing", + "name": "Skiing", + "tag": "Skiing", + "canonical": "skiing", + "metadata": { + "connections": { + "videos": { + "uri": "/tags/skiing/videos", + "options": [ + "GET" + ], + "total": 78963 + } + } + }, + "resource_key": "afa8f1f043d15398ae196c33dcbf70580e1b2772" + } + ], + "stats": { + "plays": 10608 + }, + "categories": [ + { + "uri": "/categories/sports/subcategories/snow", + "name": "Snow", + "link": "https://vimeo.com/categories/sports/snow/videos", + "top_level": false, + "is_deprecated": true, + "pictures": { + "uri": "/videos/917286232/pictures/1805656785", + "active": true, + "type": "custom", + "base_link": "https://i.vimeocdn.com/video/1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d", + "sizes": [ + { + "width": 100, + "height": 75, + "link": "https://i.vimeocdn.com/video/1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_100x75?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_100x75&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 200, + "height": 150, + "link": "https://i.vimeocdn.com/video/1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_200x150?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_200x150&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 295, + "height": 166, + "link": "https://i.vimeocdn.com/video/1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_295x166?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_295x166&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 640, + "height": 360, + "link": "https://i.vimeocdn.com/video/1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_640x360?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_640x360&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 960, + "height": 540, + "link": "https://i.vimeocdn.com/video/1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_960x540?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_960x540&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 1280, + "height": 720, + "link": "https://i.vimeocdn.com/video/1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_1280x720?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_1280x720&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 1920, + "height": 1080, + "link": "https://i.vimeocdn.com/video/1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_1920x1080?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1805656785-b8c1a0981f3324cc5cf5d7589923ba332c9d54484146c2a2326776a58708db31-d_1920x1080&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + } + ], + "resource_key": "eab0da371632f14a8e24bc543c56396ec23b4973", + "default_picture": false + }, + "last_video_featured_time": "2024-03-15T17:15:40+00:00", + "parent": { + "uri": "/categories/sports", + "name": "Sports", + "link": "https://vimeo.com/categories/sports" + }, + "metadata": { + "connections": { + "channels": { + "uri": "/categories/sports/subcategories/snow/channels", + "options": [ + "GET" + ], + "total": 7 + }, + "groups": { + "uri": "/categories/sports/subcategories/snow/groups", + "options": [ + "GET" + ], + "total": 5 + }, + "users": { + "uri": "/categories/sports/subcategories/snow/users", + "options": [ + "GET" + ], + "total": 9527 + }, + "videos": { + "uri": "/categories/sports/subcategories/snow/videos", + "options": [ + "GET" + ], + "total": 12635 + } + }, + "interactions": { + "follow": { + "added": false, + "added_time": null, + "uri": "/users/217071126/categories/snow" + } + } + }, + "resource_key": "79498dd063733304a9fe2f989cb563119801f197" + }, + { + "uri": "/categories/sports", + "name": "Sports", + "link": "https://vimeo.com/categories/sports", + "top_level": true, + "is_deprecated": false, + "pictures": { + "uri": "/videos/348721737/pictures/799468351", + "active": true, + "type": "custom", + "base_link": "https://i.vimeocdn.com/video/799468351-4174bc52dc15d2f754a33b78e9091961eec86817fa9dcfd34f277166964909d5-d", + "sizes": [ + { + "width": 100, + "height": 75, + "link": "https://i.vimeocdn.com/video/799468351-4174bc52dc15d2f754a33b78e9091961eec86817fa9dcfd34f277166964909d5-d_100x75?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F799468351-4174bc52dc15d2f754a33b78e9091961eec86817fa9dcfd34f277166964909d5-d_100x75&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 200, + "height": 150, + "link": "https://i.vimeocdn.com/video/799468351-4174bc52dc15d2f754a33b78e9091961eec86817fa9dcfd34f277166964909d5-d_200x150?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F799468351-4174bc52dc15d2f754a33b78e9091961eec86817fa9dcfd34f277166964909d5-d_200x150&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 295, + "height": 166, + "link": "https://i.vimeocdn.com/video/799468351-4174bc52dc15d2f754a33b78e9091961eec86817fa9dcfd34f277166964909d5-d_295x166?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F799468351-4174bc52dc15d2f754a33b78e9091961eec86817fa9dcfd34f277166964909d5-d_295x166&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 640, + "height": 360, + "link": "https://i.vimeocdn.com/video/799468351-4174bc52dc15d2f754a33b78e9091961eec86817fa9dcfd34f277166964909d5-d_640x360?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F799468351-4174bc52dc15d2f754a33b78e9091961eec86817fa9dcfd34f277166964909d5-d_640x360&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 960, + "height": 540, + "link": "https://i.vimeocdn.com/video/799468351-4174bc52dc15d2f754a33b78e9091961eec86817fa9dcfd34f277166964909d5-d_960x540?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F799468351-4174bc52dc15d2f754a33b78e9091961eec86817fa9dcfd34f277166964909d5-d_960x540&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 1280, + "height": 720, + "link": "https://i.vimeocdn.com/video/799468351-4174bc52dc15d2f754a33b78e9091961eec86817fa9dcfd34f277166964909d5-d_1280x720?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F799468351-4174bc52dc15d2f754a33b78e9091961eec86817fa9dcfd34f277166964909d5-d_1280x720&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 1920, + "height": 1080, + "link": "https://i.vimeocdn.com/video/799468351-4174bc52dc15d2f754a33b78e9091961eec86817fa9dcfd34f277166964909d5-d_1920x1080?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F799468351-4174bc52dc15d2f754a33b78e9091961eec86817fa9dcfd34f277166964909d5-d_1920x1080&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + } + ], + "resource_key": "32c4c7ff495ea0729496de094afa19e0cf5959f4", + "default_picture": false + }, + "last_video_featured_time": "2024-03-21T21:17:16+00:00", + "parent": null, + "metadata": { + "connections": { + "channels": { + "uri": "/categories/sports/channels", + "options": [ + "GET" + ], + "total": 36556 + }, + "groups": { + "uri": "/categories/sports/groups", + "options": [ + "GET" + ], + "total": 9964 + }, + "users": { + "uri": "/categories/sports/users", + "options": [ + "GET" + ], + "total": 1610306 + }, + "videos": { + "uri": "/categories/sports/videos", + "options": [ + "GET" + ], + "total": 363949 + } + }, + "interactions": { + "follow": { + "added": false, + "added_time": null, + "uri": "/users/217071126/categories/sports" + } + } + }, + "subcategories": [], + "icon": { + "uri": "/categories/sports/icon", + "active": false, + "type": "custom", + "base_link": "https://i.vimeocdn.com/grab?sig=5344daf4664754d61aa83225b70021ac98ecc5e4a653c2928cddc17f64f1b325&v=1&s=https%3A%2F%2Ff.vimeocdn.com%2Fimages_v6%2Fcategories%2Firis_icon_sports", + "sizes": [ + { + "width": 20, + "height": 20, + "link": "https://i.vimeocdn.com/grab?sig=5344daf4664754d61aa83225b70021ac98ecc5e4a653c2928cddc17f64f1b325&v=1&s=https%3A%2F%2Ff.vimeocdn.com%2Fimages_v6%2Fcategories%2Firis_icon_sports_64.png%3Fdb62413a379bb41a73b7bd361fec5b9f157efd21%3Fv%3D2&r=pad&w=20&h=20&f=png" + }, + { + "width": 40, + "height": 40, + "link": "https://i.vimeocdn.com/grab?sig=5344daf4664754d61aa83225b70021ac98ecc5e4a653c2928cddc17f64f1b325&v=1&s=https%3A%2F%2Ff.vimeocdn.com%2Fimages_v6%2Fcategories%2Firis_icon_sports_64.png%3Fdb62413a379bb41a73b7bd361fec5b9f157efd21%3Fv%3D2&r=pad&w=40&h=40&f=png" + }, + { + "width": 60, + "height": 60, + "link": "https://i.vimeocdn.com/grab?sig=5344daf4664754d61aa83225b70021ac98ecc5e4a653c2928cddc17f64f1b325&v=1&s=https%3A%2F%2Ff.vimeocdn.com%2Fimages_v6%2Fcategories%2Firis_icon_sports_64.png%3Fdb62413a379bb41a73b7bd361fec5b9f157efd21%3Fv%3D2&r=pad&w=60&h=60&f=png" + }, + { + "width": 80, + "height": 80, + "link": "https://i.vimeocdn.com/grab?sig=5344daf4664754d61aa83225b70021ac98ecc5e4a653c2928cddc17f64f1b325&v=1&s=https%3A%2F%2Ff.vimeocdn.com%2Fimages_v6%2Fcategories%2Firis_icon_sports_64.png%3Fdb62413a379bb41a73b7bd361fec5b9f157efd21%3Fv%3D2&r=pad&w=80&h=80&f=png" + }, + { + "width": 100, + "height": 100, + "link": "https://i.vimeocdn.com/grab?sig=5344daf4664754d61aa83225b70021ac98ecc5e4a653c2928cddc17f64f1b325&v=1&s=https%3A%2F%2Ff.vimeocdn.com%2Fimages_v6%2Fcategories%2Firis_icon_sports_64.png%3Fdb62413a379bb41a73b7bd361fec5b9f157efd21%3Fv%3D2&r=pad&w=100&h=100&f=png" + }, + { + "width": 120, + "height": 120, + "link": "https://i.vimeocdn.com/grab?sig=5344daf4664754d61aa83225b70021ac98ecc5e4a653c2928cddc17f64f1b325&v=1&s=https%3A%2F%2Ff.vimeocdn.com%2Fimages_v6%2Fcategories%2Firis_icon_sports_64.png%3Fdb62413a379bb41a73b7bd361fec5b9f157efd21%3Fv%3D2&r=pad&w=120&h=120&f=png" + }, + { + "width": 140, + "height": 140, + "link": "https://i.vimeocdn.com/grab?sig=5344daf4664754d61aa83225b70021ac98ecc5e4a653c2928cddc17f64f1b325&v=1&s=https%3A%2F%2Ff.vimeocdn.com%2Fimages_v6%2Fcategories%2Firis_icon_sports_64.png%3Fdb62413a379bb41a73b7bd361fec5b9f157efd21%3Fv%3D2&r=pad&w=140&h=140&f=png" + }, + { + "width": 160, + "height": 160, + "link": "https://i.vimeocdn.com/grab?sig=5344daf4664754d61aa83225b70021ac98ecc5e4a653c2928cddc17f64f1b325&v=1&s=https%3A%2F%2Ff.vimeocdn.com%2Fimages_v6%2Fcategories%2Firis_icon_sports_64.png%3Fdb62413a379bb41a73b7bd361fec5b9f157efd21%3Fv%3D2&r=pad&w=160&h=160&f=png" + }, + { + "width": 180, + "height": 180, + "link": "https://i.vimeocdn.com/grab?sig=5344daf4664754d61aa83225b70021ac98ecc5e4a653c2928cddc17f64f1b325&v=1&s=https%3A%2F%2Ff.vimeocdn.com%2Fimages_v6%2Fcategories%2Firis_icon_sports_64.png%3Fdb62413a379bb41a73b7bd361fec5b9f157efd21%3Fv%3D2&r=pad&w=180&h=180&f=png" + } + ], + "resource_key": "73fb2a40655c2a497cc42463c07cf4637b0320f1", + "default_picture": false + }, + "resource_key": "24145eed0412385437c5a34fa732d3f90dd1dd3a" + } + ], + "uploader": { + "pictures": { + "uri": "/users/28470614/pictures/96542509", + "active": true, + "type": "custom", + "base_link": "https://i.vimeocdn.com/portrait/96542509?subrect=2151%2C611%2C2971%2C1431&r=cover", + "sizes": [ + { + "width": 30, + "height": 30, + "link": "https://i.vimeocdn.com/portrait/96542509_30x30?subrect=2151%2C611%2C2971%2C1431&r=cover" + }, + { + "width": 72, + "height": 72, + "link": "https://i.vimeocdn.com/portrait/96542509_72x72?subrect=2151%2C611%2C2971%2C1431&r=cover" + }, + { + "width": 75, + "height": 75, + "link": "https://i.vimeocdn.com/portrait/96542509_75x75?subrect=2151%2C611%2C2971%2C1431&r=cover" + }, + { + "width": 100, + "height": 100, + "link": "https://i.vimeocdn.com/portrait/96542509_100x100?subrect=2151%2C611%2C2971%2C1431&r=cover" + }, + { + "width": 144, + "height": 144, + "link": "https://i.vimeocdn.com/portrait/96542509_144x144?subrect=2151%2C611%2C2971%2C1431&r=cover" + }, + { + "width": 216, + "height": 216, + "link": "https://i.vimeocdn.com/portrait/96542509_216x216?subrect=2151%2C611%2C2971%2C1431&r=cover" + }, + { + "width": 288, + "height": 288, + "link": "https://i.vimeocdn.com/portrait/96542509_288x288?subrect=2151%2C611%2C2971%2C1431&r=cover" + }, + { + "width": 300, + "height": 300, + "link": "https://i.vimeocdn.com/portrait/96542509_300x300?subrect=2151%2C611%2C2971%2C1431&r=cover" + }, + { + "width": 360, + "height": 360, + "link": "https://i.vimeocdn.com/portrait/96542509_360x360?subrect=2151%2C611%2C2971%2C1431&r=cover" + } + ], + "resource_key": "21ec53605dbe8ad0485c5f44c3d5dba38473bb0a", + "default_picture": false + } + }, + "metadata": { + "connections": { + "comments": { + "uri": "/videos/917286232/comments", + "options": [ + "GET", + "POST" + ], + "total": 42 + }, + "credits": { + "uri": "/videos/917286232/credits", + "options": [ + "GET", + "POST" + ], + "total": 6 + }, + "likes": { + "uri": "/videos/917286232/likes", + "options": [ + "GET" + ], + "total": 507 + }, + "pictures": { + "uri": "/videos/917286232/pictures", + "options": [ + "GET", + "POST" + ], + "total": 1 + }, + "texttracks": { + "uri": "/videos/917286232/texttracks", + "options": [ + "GET", + "POST" + ], + "total": 1 + }, + "related": null, + "recommendations": { + "uri": "/videos/917286232/recommendations", + "options": [ + "GET" + ] + }, + "albums": { + "uri": "/videos/917286232/albums", + "options": [ + "GET", + "PATCH" + ], + "total": 0 + }, + "available_albums": { + "uri": "/videos/917286232/available_albums", + "options": [ + "GET" + ], + "total": 1 + }, + "available_channels": { + "uri": "/videos/917286232/available_channels", + "options": [ + "GET" + ], + "total": 0 + }, + "versions": { + "uri": "/videos/917286232/versions", + "options": [ + "GET" + ], + "total": 1, + "current_uri": "/videos/917286232/versions/899711563", + "resource_key": "630e03f6c6fb63ce15d47126bde639b458ac44d3", + "latest_incomplete_version": null + } + }, + "interactions": { + "watchlater": { + "uri": "/users/217071126/watchlater/917286232", + "options": [ + "GET", + "PUT", + "DELETE" + ], + "added": false, + "added_time": null + }, + "like": { + "uri": "/users/217071126/likes/917286232", + "options": [ + "GET", + "PUT", + "DELETE" + ], + "added": true, + "added_time": "2024-03-21T21:29:09+00:00", + "show_count": true + }, + "report": { + "uri": "/videos/917286232/report", + "options": [ + "POST" + ], + "reason": [ + "pornographic", + "harassment", + "ripoff", + "incorrect rating", + "spam", + "causes harm", + "csam" + ] + }, + "can_update_privacy_to_public": { + "uri": "/videos/917286232", + "options": [ + "PATCH" + ] + }, + "validate": { + "uri": "/videos/917286232/validate", + "options": [ + "PUT" + ] + } + }, + "is_vimeo_create": false, + "is_screen_record": false + }, + "user": { + "uri": "/users/28470614", + "name": "Cameron Thuman", + "link": "https://vimeo.com/cameronthuman", + "capabilities": { + "hasLiveSubscription": false, + "hasEnterpriseLihp": false, + "hasSvvTimecodedComments": true, + "hasSimplifiedEnterpriseAccount": false + }, + "location": "Los Angeles, CA, USA", + "gender": "n", + "bio": "Two-time Cannes Lions YDA nominee. Directed for the Olympics, Jeep, Ram, Anheuser-Busch, Mercedes-Benz, Air Canada, Saucony, and Lincoln.", + "short_bio": "Director", + "created_time": "2014-05-26T00:32:10+00:00", + "pictures": { + "uri": "/users/28470614/pictures/96542509", + "active": true, + "type": "custom", + "base_link": "https://i.vimeocdn.com/portrait/96542509?subrect=2151%2C611%2C2971%2C1431&r=cover", + "sizes": [ + { + "width": 30, + "height": 30, + "link": "https://i.vimeocdn.com/portrait/96542509_30x30?subrect=2151%2C611%2C2971%2C1431&r=cover" + }, + { + "width": 72, + "height": 72, + "link": "https://i.vimeocdn.com/portrait/96542509_72x72?subrect=2151%2C611%2C2971%2C1431&r=cover" + }, + { + "width": 75, + "height": 75, + "link": "https://i.vimeocdn.com/portrait/96542509_75x75?subrect=2151%2C611%2C2971%2C1431&r=cover" + }, + { + "width": 100, + "height": 100, + "link": "https://i.vimeocdn.com/portrait/96542509_100x100?subrect=2151%2C611%2C2971%2C1431&r=cover" + }, + { + "width": 144, + "height": 144, + "link": "https://i.vimeocdn.com/portrait/96542509_144x144?subrect=2151%2C611%2C2971%2C1431&r=cover" + }, + { + "width": 216, + "height": 216, + "link": "https://i.vimeocdn.com/portrait/96542509_216x216?subrect=2151%2C611%2C2971%2C1431&r=cover" + }, + { + "width": 288, + "height": 288, + "link": "https://i.vimeocdn.com/portrait/96542509_288x288?subrect=2151%2C611%2C2971%2C1431&r=cover" + }, + { + "width": 300, + "height": 300, + "link": "https://i.vimeocdn.com/portrait/96542509_300x300?subrect=2151%2C611%2C2971%2C1431&r=cover" + }, + { + "width": 360, + "height": 360, + "link": "https://i.vimeocdn.com/portrait/96542509_360x360?subrect=2151%2C611%2C2971%2C1431&r=cover" + } + ], + "resource_key": "21ec53605dbe8ad0485c5f44c3d5dba38473bb0a", + "default_picture": false + }, + "websites": [ + { + "uri": "/users/28470614/links/8555147", + "name": "Instagram", + "link": "http://Instagram.com/cameronthuman", + "type": "link", + "description": null + }, + { + "uri": "/users/28470614/links/8555153", + "name": null, + "link": "https://cameronthuman.com/", + "type": "link", + "description": null + } + ], + "metadata": { + "connections": { + "albums": { + "uri": "/users/28470614/albums", + "options": [ + "GET" + ], + "total": 0 + }, + "appearances": { + "uri": "/users/28470614/appearances", + "options": [ + "GET" + ], + "total": 0 + }, + "channels": { + "uri": "/users/28470614/channels", + "options": [ + "GET" + ], + "total": 30 + }, + "feed": { + "uri": "/users/28470614/feed", + "options": [ + "GET" + ] + }, + "followers": { + "uri": "/users/28470614/followers", + "options": [ + "GET" + ], + "total": 446 + }, + "following": { + "uri": "/users/28470614/following", + "options": [ + "GET" + ], + "total": 24 + }, + "groups": { + "uri": "/users/28470614/groups", + "options": [ + "GET" + ], + "total": 11 + }, + "likes": { + "uri": "/users/28470614/likes", + "options": [ + "GET" + ], + "total": 68 + }, + "membership": { + "uri": "/users/28470614/membership/", + "options": [ + "PATCH" + ] + }, + "moderated_channels": { + "uri": "/users/28470614/channels?filter=moderated", + "options": [ + "GET" + ], + "total": 1 + }, + "portfolios": { + "uri": "/users/28470614/portfolios", + "options": [ + "GET" + ], + "total": 0 + }, + "videos": { + "uri": "/users/28470614/videos", + "options": [ + "GET" + ], + "total": 21 + }, + "shared": { + "uri": "/users/28470614/shared/videos", + "options": [ + "GET" + ], + "total": 2 + }, + "pictures": { + "uri": "/users/28470614/pictures", + "options": [ + "GET", + "POST" + ], + "total": 1 + }, + "folders_root": { + "uri": "/users/28470614/folders/root", + "options": [ + "GET" + ] + }, + "teams": { + "uri": "/users/28470614/teams", + "options": [ + "GET" + ], + "total": 1 + }, + "permission_policies": { + "uri": "/users/28470614/permission_policies", + "options": [ + "GET" + ], + "total": 7 + } + }, + "interactions": { + "follow": { + "added": false, + "added_time": null, + "uri": "/users/217071126/following/28470614", + "options": [ + "GET", + "PUT", + "DELETE" + ] + }, + "block": { + "uri": "/me/block/28470614", + "options": [ + "PUT", + "DELETE" + ], + "added": false, + "added_time": null + }, + "report": { + "uri": "/users/28470614/report", + "options": [ + "POST" + ], + "reason": [ + "inappropriate avatar", + "spammy", + "bad videos", + "creepy", + "not playing nice", + "impersonation", + "inappropriate job post", + "spam emails" + ] + } + } + }, + "location_details": { + "formatted_address": "Los Angeles, CA, USA", + "latitude": 34.0522346, + "longitude": -118.2436829, + "city": "Los Angeles", + "state": "California", + "neighborhood": null, + "sub_locality": null, + "state_iso_code": "CA", + "country": "United States", + "country_iso_code": "US" + }, + "skills": [ + { + "uri": "/marketplace/skills/17", + "name": "Director" + } + ], + "available_for_hire": false, + "can_work_remotely": false, + "resource_key": "743603935c68658a242a4f05c9af4927153babd3", + "account": "pro_unlimited" + }, + "app": { + "name": "Vimeo Site", + "uri": "/apps/58479" + }, + "play": { + "status": "playable" + }, + "status": "available", + "resource_key": "f714f98c319946456d8abd47e6b330f6897d9d5a", + "upload": { + "status": "complete", + "link": null, + "upload_link": null, + "form": null, + "approach": null, + "size": null, + "redirect_url": null + }, + "transcode": { + "status": "complete" + }, + "is_playable": true, + "has_audio": true +} \ No newline at end of file diff --git a/components/vimeo/sources/new-video-of-mine/new-video-of-mine.mjs b/components/vimeo/sources/new-video-of-mine/new-video-of-mine.mjs new file mode 100644 index 0000000000000..5c29abd0600d4 --- /dev/null +++ b/components/vimeo/sources/new-video-of-mine/new-video-of-mine.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "vimeo-new-video-of-mine", + name: "New Video of Mine", + description: "Emit new event when the user adds a new video.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.vimeo.listVideos; + }, + getSummary(video) { + return `New video: ${video.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/vimeo/sources/new-video-of-mine/test-event.mjs b/components/vimeo/sources/new-video-of-mine/test-event.mjs new file mode 100644 index 0000000000000..5ee69d96417c5 --- /dev/null +++ b/components/vimeo/sources/new-video-of-mine/test-event.mjs @@ -0,0 +1,723 @@ +export default { + "uri": "/videos/925926407", + "name": "Video Title!", + "description": "Video description!", + "type": "video", + "link": "https://vimeo.com/925926407", + "player_embed_url": "https://player.vimeo.com/video/925926407?h=49a4e309cd", + "duration": 30, + "width": 480, + "language": null, + "height": 270, + "embed": { + "html": "", + "badges": { + "hdr": false, + "live": { + "streaming": false, + "archived": false + }, + "staff_pick": { + "normal": false, + "best_of_the_month": false, + "best_of_the_year": false, + "premiere": false + }, + "vod": false, + "weekend_challenge": false + }, + "interactive": false, + "buttons": { + "watchlater": true, + "share": true, + "embed": true, + "hd": false, + "fullscreen": true, + "scaling": true, + "like": true + }, + "logos": { + "vimeo": true, + "custom": { + "active": false, + "url": null, + "link": null, + "use_link": false, + "sticky": false + } + }, + "play_button": { + "position": "auto" + }, + "title": { + "name": "user", + "owner": "user", + "portrait": "user" + }, + "end_screen": [], + "playbar": true, + "quality_selector": null, + "pip": true, + "autopip": true, + "volume": true, + "color": "00adef", + "colors": { + "color_one": "000000", + "color_two": "00adef", + "color_three": "ffffff", + "color_four": "000000" + }, + "event_schedule": true, + "has_cards": false, + "outro_type": "videos", + "show_timezone": false, + "cards": [], + "airplay": true, + "audio_tracks": true, + "chapters": true, + "chromecast": true, + "closed_captions": true, + "transcript": true, + "uri": null, + "email_capture_form": null, + "speed": true + }, + "created_time": "2024-03-21T16:46:48+00:00", + "modified_time": "2024-03-21T16:47:52+00:00", + "release_time": "2024-03-21T16:46:48+00:00", + "content_rating": [ + "unrated" + ], + "content_rating_class": "unrated", + "rating_mod_locked": false, + "license": null, + "privacy": { + "view": "anybody", + "embed": "public", + "download": false, + "add": true, + "comments": "anybody" + }, + "pictures": { + "uri": "/videos/925926407/pictures/1819873931", + "active": true, + "type": "custom", + "base_link": "https://i.vimeocdn.com/video/1819873931-f62a6e2bc9ac123f4a7d0c1bcd4a13c17ee137cd790def4e63e723d0dcb61e53-d", + "sizes": [ + { + "width": 100, + "height": 75, + "link": "https://i.vimeocdn.com/video/1819873931-f62a6e2bc9ac123f4a7d0c1bcd4a13c17ee137cd790def4e63e723d0dcb61e53-d_100x75?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1819873931-f62a6e2bc9ac123f4a7d0c1bcd4a13c17ee137cd790def4e63e723d0dcb61e53-d_100x75&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 200, + "height": 150, + "link": "https://i.vimeocdn.com/video/1819873931-f62a6e2bc9ac123f4a7d0c1bcd4a13c17ee137cd790def4e63e723d0dcb61e53-d_200x150?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1819873931-f62a6e2bc9ac123f4a7d0c1bcd4a13c17ee137cd790def4e63e723d0dcb61e53-d_200x150&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 295, + "height": 166, + "link": "https://i.vimeocdn.com/video/1819873931-f62a6e2bc9ac123f4a7d0c1bcd4a13c17ee137cd790def4e63e723d0dcb61e53-d_295x166?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1819873931-f62a6e2bc9ac123f4a7d0c1bcd4a13c17ee137cd790def4e63e723d0dcb61e53-d_295x166&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 640, + "height": 360, + "link": "https://i.vimeocdn.com/video/1819873931-f62a6e2bc9ac123f4a7d0c1bcd4a13c17ee137cd790def4e63e723d0dcb61e53-d_640x360?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1819873931-f62a6e2bc9ac123f4a7d0c1bcd4a13c17ee137cd790def4e63e723d0dcb61e53-d_640x360&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 960, + "height": 540, + "link": "https://i.vimeocdn.com/video/1819873931-f62a6e2bc9ac123f4a7d0c1bcd4a13c17ee137cd790def4e63e723d0dcb61e53-d_960x540?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1819873931-f62a6e2bc9ac123f4a7d0c1bcd4a13c17ee137cd790def4e63e723d0dcb61e53-d_960x540&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 1280, + "height": 720, + "link": "https://i.vimeocdn.com/video/1819873931-f62a6e2bc9ac123f4a7d0c1bcd4a13c17ee137cd790def4e63e723d0dcb61e53-d_1280x720?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1819873931-f62a6e2bc9ac123f4a7d0c1bcd4a13c17ee137cd790def4e63e723d0dcb61e53-d_1280x720&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + }, + { + "width": 1920, + "height": 1080, + "link": "https://i.vimeocdn.com/video/1819873931-f62a6e2bc9ac123f4a7d0c1bcd4a13c17ee137cd790def4e63e723d0dcb61e53-d_1920x1080?r=pad", + "link_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1819873931-f62a6e2bc9ac123f4a7d0c1bcd4a13c17ee137cd790def4e63e723d0dcb61e53-d_1920x1080&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png" + } + ], + "resource_key": "77ecbd18f1b48136dcee57745450af1ccedc7590", + "default_picture": false + }, + "tags": [], + "stats": { + "plays": 0 + }, + "categories": [], + "uploader": { + "pictures": { + "uri": "/users/217071126/pictures/97277504", + "active": true, + "type": "custom", + "base_link": "https://i.vimeocdn.com/portrait/97277504", + "sizes": [ + { + "width": 30, + "height": 30, + "link": "https://i.vimeocdn.com/portrait/97277504_30x30" + }, + { + "width": 72, + "height": 72, + "link": "https://i.vimeocdn.com/portrait/97277504_72x72" + }, + { + "width": 75, + "height": 75, + "link": "https://i.vimeocdn.com/portrait/97277504_75x75" + }, + { + "width": 100, + "height": 100, + "link": "https://i.vimeocdn.com/portrait/97277504_100x100" + }, + { + "width": 144, + "height": 144, + "link": "https://i.vimeocdn.com/portrait/97277504_144x144" + }, + { + "width": 216, + "height": 216, + "link": "https://i.vimeocdn.com/portrait/97277504_216x216" + }, + { + "width": 288, + "height": 288, + "link": "https://i.vimeocdn.com/portrait/97277504_288x288" + }, + { + "width": 300, + "height": 300, + "link": "https://i.vimeocdn.com/portrait/97277504_300x300" + }, + { + "width": 360, + "height": 360, + "link": "https://i.vimeocdn.com/portrait/97277504_360x360" + } + ], + "resource_key": "c81484dfb71289acb2fd170037263a2dcaddefb8", + "default_picture": false + } + }, + "metadata": { + "connections": { + "comments": { + "uri": "/videos/925926407/comments", + "options": [ + "GET", + "POST" + ], + "total": 0 + }, + "credits": { + "uri": "/videos/925926407/credits", + "options": [ + "GET", + "POST" + ], + "total": 0 + }, + "likes": { + "uri": "/videos/925926407/likes", + "options": [ + "GET" + ], + "total": 0 + }, + "pictures": { + "uri": "/videos/925926407/pictures", + "options": [ + "GET", + "POST" + ], + "total": 1 + }, + "texttracks": { + "uri": "/videos/925926407/texttracks", + "options": [ + "GET", + "POST" + ], + "total": 0 + }, + "related": { + "uri": "/me/videos?page=1&per_page=100&offset=1", + "options": [ + "GET" + ] + }, + "recommendations": { + "uri": "/videos/925926407/recommendations", + "options": [ + "GET" + ] + }, + "albums": { + "uri": "/videos/925926407/albums", + "options": [ + "GET", + "PATCH" + ], + "total": 0 + }, + "available_albums": { + "uri": "/videos/925926407/available_albums", + "options": [ + "GET" + ], + "total": 1 + }, + "available_channels": { + "uri": "/videos/925926407/available_channels", + "options": [ + "GET" + ], + "total": 0 + }, + "versions": { + "uri": "/videos/925926407/versions", + "options": [ + "GET" + ], + "total": 1, + "current_uri": "/videos/925926407/versions/908948827", + "resource_key": "99bc66b2b469adec4cc04c7f7d135cb5fe45e2ff", + "latest_incomplete_version": null + } + }, + "interactions": { + "watchlater": { + "uri": "/users/217071126/watchlater/925926407", + "options": [ + "GET", + "PUT", + "DELETE" + ], + "added": false, + "added_time": null + }, + "report": { + "uri": "/videos/925926407/report", + "options": [ + "POST" + ], + "reason": [ + "pornographic", + "harassment", + "ripoff", + "incorrect rating", + "spam", + "causes harm", + "csam" + ] + }, + "view_team_members": { + "uri": "/videos/925926407/teammembers", + "options": [ + "GET" + ] + }, + "edit": { + "uri": "/videos/925926407", + "options": [ + "PATCH" + ], + "blocked_fields": [ + "custom_url" + ] + }, + "edit_content_rating": { + "uri": "/videos/925926407", + "options": [ + "PATCH" + ], + "content_rating": [ + "language", + "drugs", + "violence", + "nudity", + "advertisement", + "safe", + "unrated" + ] + }, + "edit_privacy": { + "uri": "/videos/925926407", + "options": [ + "PATCH" + ], + "content_type": "application/vnd.vimeo.video", + "properties": [ + { + "name": "privacy.view", + "required": true, + "options": [ + "anybody", + "nobody", + "password", + "disable", + "unlisted" + ] + } + ] + }, + "delete": { + "uri": "/videos/925926407", + "options": [ + "DELETE" + ] + }, + "can_update_privacy_to_public": { + "uri": "/videos/925926407", + "options": [ + "PATCH" + ] + }, + "trim": { + "uri": "/videos/925926407/cliptrim", + "options": [ + "GET", + "POST" + ] + }, + "validate": { + "uri": "/videos/925926407/validate", + "options": [ + "PUT" + ] + } + }, + "is_vimeo_create": false, + "is_screen_record": false + }, + "manage_link": "/manage/videos/925926407", + "user": { + "uri": "/users/217071126", + "name": "Test User", + "link": "https://vimeo.com/user217071126", + "capabilities": { + "hasLiveSubscription": false, + "hasEnterpriseLihp": false, + "hasSvvTimecodedComments": false, + "hasSimplifiedEnterpriseAccount": false + }, + "location": "", + "gender": "", + "bio": null, + "short_bio": null, + "created_time": "2024-03-21T14:41:15+00:00", + "pictures": { + "uri": "/users/217071126/pictures/97277504", + "active": true, + "type": "custom", + "base_link": "https://i.vimeocdn.com/portrait/97277504", + "sizes": [ + { + "width": 30, + "height": 30, + "link": "https://i.vimeocdn.com/portrait/97277504_30x30" + }, + { + "width": 72, + "height": 72, + "link": "https://i.vimeocdn.com/portrait/97277504_72x72" + }, + { + "width": 75, + "height": 75, + "link": "https://i.vimeocdn.com/portrait/97277504_75x75" + }, + { + "width": 100, + "height": 100, + "link": "https://i.vimeocdn.com/portrait/97277504_100x100" + }, + { + "width": 144, + "height": 144, + "link": "https://i.vimeocdn.com/portrait/97277504_144x144" + }, + { + "width": 216, + "height": 216, + "link": "https://i.vimeocdn.com/portrait/97277504_216x216" + }, + { + "width": 288, + "height": 288, + "link": "https://i.vimeocdn.com/portrait/97277504_288x288" + }, + { + "width": 300, + "height": 300, + "link": "https://i.vimeocdn.com/portrait/97277504_300x300" + }, + { + "width": 360, + "height": 360, + "link": "https://i.vimeocdn.com/portrait/97277504_360x360" + } + ], + "resource_key": "c81484dfb71289acb2fd170037263a2dcaddefb8", + "default_picture": false + }, + "websites": [], + "metadata": { + "connections": { + "albums": { + "uri": "/users/217071126/albums", + "options": [ + "GET" + ], + "total": 1 + }, + "appearances": { + "uri": "/users/217071126/appearances", + "options": [ + "GET" + ], + "total": 0 + }, + "categories": { + "uri": "/users/217071126/categories", + "options": [ + "GET" + ], + "total": 0 + }, + "channels": { + "uri": "/users/217071126/channels", + "options": [ + "GET" + ], + "total": 0 + }, + "feed": { + "uri": "/users/217071126/feed", + "options": [ + "GET" + ] + }, + "followers": { + "uri": "/users/217071126/followers", + "options": [ + "GET" + ], + "total": 0 + }, + "following": { + "uri": "/users/217071126/following", + "options": [ + "GET" + ], + "total": 0 + }, + "groups": { + "uri": "/users/217071126/groups", + "options": [ + "GET" + ], + "total": 0 + }, + "likes": { + "uri": "/users/217071126/likes", + "options": [ + "GET" + ], + "total": 0 + }, + "membership": { + "uri": "/users/217071126/membership/", + "options": [ + "PATCH" + ] + }, + "moderated_channels": { + "uri": "/users/217071126/channels?filter=moderated", + "options": [ + "GET" + ], + "total": 0 + }, + "portfolios": { + "uri": "/users/217071126/portfolios", + "options": [ + "GET" + ], + "total": 0 + }, + "videos": { + "uri": "/users/217071126/videos", + "options": [ + "GET" + ], + "total": 2 + }, + "watchlater": { + "uri": "/users/217071126/watchlater", + "options": [ + "GET" + ], + "total": 0 + }, + "shared": { + "uri": "/users/217071126/shared/videos", + "options": [ + "GET" + ], + "total": 0 + }, + "pictures": { + "uri": "/users/217071126/pictures", + "options": [ + "GET", + "POST" + ], + "total": 1 + }, + "watched_videos": { + "uri": "/me/watched/videos", + "options": [ + "GET" + ], + "total": 0 + }, + "folders_root": { + "uri": "/users/217071126/folders/root", + "options": [ + "GET" + ] + }, + "folders": { + "uri": "/users/217071126/folders", + "options": [ + "GET", + "POST" + ], + "total": 0 + }, + "teams": { + "uri": "/users/217071126/teams", + "options": [ + "GET" + ], + "total": 1 + }, + "block": { + "uri": "/me/block", + "options": [ + "GET" + ], + "total": 0 + } + } + }, + "location_details": { + "formatted_address": "", + "latitude": null, + "longitude": null, + "city": null, + "state": null, + "neighborhood": null, + "sub_locality": null, + "state_iso_code": null, + "country": null, + "country_iso_code": null + }, + "skills": [], + "available_for_hire": false, + "can_work_remotely": false, + "preferences": { + "videos": { + "rating": [ + "unrated" + ], + "privacy": { + "view": "anybody", + "comments": "anybody", + "embed": "public", + "download": true, + "add": true, + "allow_share_link": true + } + }, + "webinar_registrant_lower_watermark_banner_dismissed": [] + }, + "content_filter": [ + "language", + "drugs", + "violence", + "nudity", + "safe", + "unrated" + ], + "upload_quota": { + "space": { + "free": 1072171800, + "max": 1073741824, + "used": 1570024, + "showing": "lifetime", + "unit": "video_size" + }, + "periodic": { + "period": null, + "unit": null, + "free": null, + "max": null, + "used": null, + "reset_date": null + }, + "lifetime": { + "unit": "video_size", + "free": 1072171800, + "max": 1073741824, + "used": 1570024 + } + }, + "resource_key": "5fc63eb4b96a33b6356ca60c25f0bad1afa5deb2", + "account": "free" + }, + "last_user_action_event_date": "2024-03-21T16:46:48+00:00", + "parent_folder": null, + "review_page": { + "active": true, + "link": "https://vimeo.com/user217071126/review/925926407/d278fbe985", + "is_shareable": true + }, + "app": { + "name": "Pipedream", + "uri": "/apps/187277" + }, + "play": { + "status": "playable" + }, + "status": "available", + "resource_key": "ba212620356229199427508992fcf167c5c51f46", + "upload": { + "status": "complete", + "link": null, + "upload_link": null, + "form": null, + "approach": null, + "size": null, + "redirect_url": null + }, + "transcode": { + "status": "complete" + }, + "is_playable": true, + "has_audio": true +} \ No newline at end of file diff --git a/components/vimeo/vimeo.app.mjs b/components/vimeo/vimeo.app.mjs index c96e7bd4b3c10..4bc47e3d0c22e 100644 --- a/components/vimeo/vimeo.app.mjs +++ b/components/vimeo/vimeo.app.mjs @@ -1,11 +1,145 @@ +import { axios } from "@pipedreamhq/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "vimeo", - propDefinitions: {}, + propDefinitions: { + videoId: { + type: "string", + label: "Video ID", + description: "The ID of the video", + async options({ page }) { + const { data } = await this.listVideos({ + params: { + page: page + 1, + }, + }); + return data?.map(({ + uri, name: label, + }) => ({ + value: uri.split("/").pop(), + label, + })) || []; + }, + }, + albumUri: { + type: "string", + label: "Album URI", + description: "The URI of the album to add the video to", + async options({ page }) { + const { data } = await this.listAlbums({ + params: { + page: page + 1, + }, + }); + return data?.map(({ + uri: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.vimeo.com"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + ...headers, + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "Content-Type": "application/json", + }, + }); + }, + listVideos(opts = {}) { + return this._makeRequest({ + path: "/me/videos", + ...opts, + }); + }, + listAlbums(opts = {}) { + return this._makeRequest({ + path: "/me/albums", + ...opts, + }); + }, + listLikedVideos(opts = {}) { + return this._makeRequest({ + path: "/me/likes", + ...opts, + }); + }, + searchVideos(opts = {}) { + return this._makeRequest({ + path: "/videos", + ...opts, + }); + }, + uploadVideo(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/me/videos", + headers: { + Accept: "application/vnd.vimeo.*+json;version=3.4", + }, + ...opts, + }); + }, + addVideoToAlbum({ + videoId, ...opts + }) { + return this._makeRequest({ + method: "PATCH", + path: `/videos/${videoId}/albums`, + ...opts, + }); + }, + deleteVideo({ + videoId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/videos/${videoId}`, + ...opts, + }); + }, + async *paginate({ + resourceFn, + params = {}, + max, + }) { + params = { + ...params, + page: 1, + per_page: constants.DEFAULT_LIMIT, + }; + let total, count = 0; + do { + const { data } = await resourceFn({ + params, + }); + for (const item of data) { + yield item; + count++; + if (max && count >= max) { + return; + } + } + total = data?.length; + params.page++; + } while (total === params.per_page); }, }, }; diff --git a/components/viral_loops/README.md b/components/viral_loops/README.md new file mode 100644 index 0000000000000..4e48f606d73da --- /dev/null +++ b/components/viral_loops/README.md @@ -0,0 +1,11 @@ +# Overview + +The Viral Loops API enables you to tap into the potent capabilities of referral marketing within your applications. Using this API through Pipedream, you can automate campaign management, participant tracking, and reward allocation. You can also trigger actions based on referral achievements or new participant sign-ups, making it simpler to manage marketing campaigns and analyze their performance in real-time. + +# Example Use Cases + +- **Syncing New Referral Participants to a CRM**: Automatically add new participants from your Viral Loops campaigns to a CRM like HubSpot, ensuring your sales team has the latest leads to follow up on. + +- **Distributing Rewards with Stripe**: When a participant reaches a referral milestone, trigger a workflow to issue a discount or credit via Stripe, incentivizing further engagement and customer loyalty. + +- **Email Notifications for Campaign Updates**: Set up a workflow to send out email alerts through SendGrid whenever there's a significant update in your campaign's performance, such as reaching a participant threshold or achieving a certain number of referrals. diff --git a/components/virifi/package.json b/components/virifi/package.json new file mode 100644 index 0000000000000..044734265bfa1 --- /dev/null +++ b/components/virifi/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/virifi", + "version": "0.0.1", + "description": "Pipedream Virifi Components", + "main": "virifi.app.mjs", + "keywords": [ + "pipedream", + "virifi" + ], + "homepage": "https://pipedream.com/apps/virifi", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/virifi/virifi.app.mjs b/components/virifi/virifi.app.mjs new file mode 100644 index 0000000000000..ff6f3e6e342c8 --- /dev/null +++ b/components/virifi/virifi.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "virifi", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/virustotal/README.md b/components/virustotal/README.md index 52482b022f431..97f40c2676f51 100644 --- a/components/virustotal/README.md +++ b/components/virustotal/README.md @@ -1,23 +1,11 @@ # Overview -The VirusTotal API is a powerful online tool that allows developers to access -malware information, such as the prevalence of a specific file and the location -of any malicious URLs providers may have identified. With this API, users can -build a range of applications that can help warn their users of malicious -activity and protect them from viruses. +The VirusTotal API offers a powerful interface to automate various aspects of security analysis and threat intelligence. With the API, you can scan files, URLs, domains, and IP addresses for malicious activity. Pipedream's serverless platform allows you to create workflows that can leverage this API to build custom security tools, automate threat detection, and integrate with other services for enhanced monitoring and alerting. -Here are some of the things you can build with the VirusTotal API: +# Example Use Cases -- Monitor File Integrity: Visualize hashes of files over time, scan malicious - files before downloading, and detect possible malicious file behavior. -- Scan Malicious URLs: Monitor any suspicious URLs registered in the VirusTotal - database, and alert users when an URL is tagged as malicious. -- Uncover Malicious Behavior: Paired with other APIs, investigate events - reported as malicious and inform users on any malicious behavior. -- Create Firewall Rules: Deny access to untrusted IPs and urls, blacklist them - and analyze the source of any suspicious activity. -- Carry Out Instant Analysis: By using VirusTotal Scan, analyze suspicious - content and get immediate response on any malicious material. -- Automate Incident Response: Use the APIs with your own incident response - system to alert you when malicious content is identified and take immediate - action. +- **Automated Malware Scanning for Email Attachments**: Use Pipedream to monitor an email inbox for new messages. When a new attachment is detected, automatically send it to VirusTotal for scanning. If the attachment is found to be malicious, trigger an alert and move the email to a quarantine folder. + +- **Continuous URL Monitoring for Phishing Detection**: Create a Pipedream workflow that periodically checks URLs from a database or a list against the VirusTotal API. If a URL is flagged as a potential phishing site, automatically notify your team on Slack or through another communication app available on Pipedream. + +- **Integrating Threat Intelligence into SIEM Solutions**: Streamline your security information and event management (SIEM) by using Pipedream to send new threat data from VirusTotal directly to your SIEM service, such as Splunk or a similar app on Pipedream. Keep your threat intelligence up-to-date and enhance your security response capabilities. diff --git a/components/vision6/README.md b/components/vision6/README.md index 01ff834d979ce..c2464e3641461 100644 --- a/components/vision6/README.md +++ b/components/vision6/README.md @@ -1,26 +1,11 @@ # Overview -Vision6 offers a powerful API that can help you build a variety of projects. -Whether you're a developer, marketer, or business owner, you can use the -Vision6 API to get your products in front of customers and drive conversions. -With the Vision6 API, you can create complex automated marketing campaigns, -manage your customer data, and create powerful visualizations with ease. Here -are some examples of the ways you can use Vision6 for your digital marketing -needs: +The Vision6 API offers a powerful way to automate email marketing and audience engagement directly through Pipedream. With it, you can programmatically manage lists, contacts, and messages, and also track campaign performance. This API provides granular control over email workflows, enabling custom triggers, targeted actions, and detailed analytics retrieval. The combination of Vision6 with Pipedream's serverless platform unlocks endless possibilities for syncing data, personalizing communication, and optimizing marketing strategies without manual intervention. -- Automated marketing campaigns: You can create complex automated campaigns - that send emails or SMS messages based on customer behavior or usage data. - You can also create automated drip campaigns to nurture customers through - their purchase journey. -- Customer data management: The Vision6 API allows you to store, manage, and - analyze customer data. With customer data, you can personalize your messaging - and track conversions. -- Visualizations: You can use Vision6 to create compelling visualizations and - infographics that help you make better decisions and show customers trends in - your data. -- Integrations: You can connect third-party applications or services to the - Vision6 API, allowing you to leverage a variety of tools to create a seamless - customer experience. -- Surveys and feedback collection: With the Vision6 API, you can create surveys - and polls and collect feedback from customers. This feedback can help you - tailor your messaging and create more meaningful customer experiences. +# Example Use Cases + +- **Automated Contact Syncing Between Platforms**: Synchronize contacts from your CRM platform like Salesforce to Vision6 lists automatically. Upon a new contact entry or update in Salesforce, trigger a workflow in Pipedream that adds or updates the contact details in a specified Vision6 list, keeping your mailing list fresh and up-to-date. + +- **Dynamic Email Campaigns Based on User Behavior**: Trigger personalized email campaigns from Vision6 when a user performs a specific action on your website or app, tracked via Google Analytics. Pipedream can capture the event, analyze the action, and instruct Vision6 to send a tailored email to the user, creating an instantly responsive marketing strategy. + +- **Real-Time Data-Driven Product Recommendations**: Connect Vision6 with an e-commerce platform like Shopify. Use Pipedream to monitor purchase history or browsing behaviour and trigger targeted product recommendation emails via Vision6. This can increase customer engagement by offering relevant products and enhancing the shopping experience. diff --git a/components/visitor_queue/README.md b/components/visitor_queue/README.md index 9648fe5a7d4ff..dc55b8caec26e 100644 --- a/components/visitor_queue/README.md +++ b/components/visitor_queue/README.md @@ -1,24 +1,11 @@ # Overview -The Visitor Queue API is an application programming interface that simplifies -website visitor data capture, lead collection, and automation. With this API, -website owners can design, develop, and deploy more successful online -applications and experiences. +The Visitor Queue API lets you track and identify businesses that visit your website, providing valuable data for lead generation and marketing analytics. By leveraging this API on Pipedream, you can automate tasks such as scoring leads, syncing data with CRM systems, and engaging with potential customers through targeted outreach. The ability to connect with hundreds of apps on Pipedream allows for powerful, automated workflows that can enrich your sales pipeline and optimize your marketing strategies. -Using the Visitor Queue API, website owners can take full advantage of their -website visitor data to improve customer acquisition, sales, and more. +# Example Use Cases -Examples of things you can achieve with the Visitor Queue API: +- **Lead Prioritization Workflow**: Identify high-value leads using Visitor Queue's data and automatically score them based on custom criteria such as company size or industry. Use Pipedream to add these leads to a Google Sheets spreadsheet and flag them for immediate follow-up by your sales team. -- Something as simple as captures form information and send a tailored message - to the customer. -- Generate leads with an interactive web form. -- Track visitor behavior on a website and send user-specific alerts. -- Create and populate visitor profiles to measure user engagement. -- Automatically invite visitors to join a mailing list, schedule personalized - messages, or integrate with APIs and databases. -- Trigger custom, automated sales campaigns from visitor behavior. -- Detect, identify, and flag malicious website activity. -- Gather usage data in order to optimize the website experience for visitors. -- Create bespoke data visualizations and analytics to analyze the performance - and effectiveness of campaigns. +- **CRM Integration Workflow**: Sync Visitor Queue data with your CRM, like Salesforce or Hubspot, to keep your sales pipeline up-to-date. Set up a Pipedream workflow that creates or updates CRM records in real time as new visitors are identified, ensuring your sales team has the most current information at their fingertips. + +- **Real-Time Alerts Workflow**: Configure a workflow in Pipedream to send real-time notifications to Slack or Microsoft Teams when a targeted or repeat company visits your site. This allows your marketing and sales teams to react promptly, tailoring their outreach strategies to engage with these prospects effectively. diff --git a/components/visualping/README.md b/components/visualping/README.md new file mode 100644 index 0000000000000..4fb2ff11d42c9 --- /dev/null +++ b/components/visualping/README.md @@ -0,0 +1,11 @@ +# Overview + +VisualPing is a tool that lets you monitor web pages for changes. Using its API with Pipedream, you can automate reactions to those changes. For instance, you can set up workflows that notify you or your team when specific parts of a web page update, or use changes to trigger downstream actions in other apps. Pipedream's serverless platform enables you to connect VisualPing with a vast array of apps to create custom, automated workflows without writing a lot of code. + +# Example Use Cases + +- **Automate Slack Notifications on Web Changes**: Monitor your competitor's home page for changes and automatically send a notification to a designated Slack channel. This keeps your marketing or product team informed about competitor updates almost in real-time. + +- **Create Trello Cards for Price Changes**: Watch an e-commerce product page for price changes with VisualPing. When a change is detected, a Trello card is created with details of the change, helping your sales team adapt their strategies or your procurement team to act quickly. + +- **Sync Changes to Google Sheets**: Track changes to a set of curated web pages and append details of those changes to a Google Sheet. This is excellent for SEO professionals who want to monitor web page updates and consolidate their findings in an easily accessible format. diff --git a/components/vitally/README.md b/components/vitally/README.md new file mode 100644 index 0000000000000..f40c5a076d701 --- /dev/null +++ b/components/vitally/README.md @@ -0,0 +1,11 @@ +# Overview + +The Vitally API offers a window into customer success operations, enabling you to automate workflows, integrate with other tools, and manipulate customer data programmatically. With Pipedream, you can harness this API to create custom automations that react to events in Vitally, sync data between Vitally and other platforms, or maintain up-to-date customer profiles across your business stack. + +# Example Use Cases + +- **Sync Vitally Users with CRM**: Integrate Vitally with a CRM platform like Salesforce. Automatically create or update CRM contacts when a user is added or updated in Vitally, ensuring sales teams have the latest customer data. + +- **Automate Customer Health Score Updates**: Use a scheduled Pipedream workflow to fetch customer engagement data from tools like Segment or Mixpanel and update the health scores in Vitally. This can help maintain a current view of customer satisfaction and product usage. + +- **Trigger Email Campaigns Based on Vitally Events**: Connect Vitally with an email marketing service like Mailchimp. Trigger targeted email campaigns or educational drip sequences when a user's health score drops or they achieve a significant milestone within your product. diff --git a/components/vitel_phone/README.md b/components/vitel_phone/README.md new file mode 100644 index 0000000000000..de17f1fb3f214 --- /dev/null +++ b/components/vitel_phone/README.md @@ -0,0 +1,11 @@ +# Overview + +The Vitel Phone API allows you to integrate a suite of telephony services into your applications, enabling you to automate calls, manage voicemails, send text messages, and track communications. Leveraging Pipedream's serverless platform, you can create workflows that react to events from Vitel Phone and connect them with hundreds of other apps to streamline processes, alert teams, log activities, or even analyze call data for insights. + +# Example Use Cases + +- **Auto-Respond to Missed Calls**: If you miss a call on Vitel Phone, you can set up a Pipedream workflow that triggers an automatic text message or an email response to the caller. This way, you ensure prompt communication, even when you're not available to take the call. + +- **Log Calls and Voicemails to CRM**: Integrate Vitel Phone with your CRM platform, such as Salesforce or HubSpot. Every time you receive a call or voicemail, Pipedream can capture the details and automatically update the customer's record in the CRM, keeping track of all interactions in one place. + +- **Voice Command Actions**: Use Pipedream to trigger workflows based on specific voice commands during a call. For instance, when a keyword is mentioned, it could initiate a task in a project management tool like Trello or Asana, send a Slack notification, or log the event for future reference. diff --git a/components/vivifyscrum/README.md b/components/vivifyscrum/README.md index 7b190e1dd5b34..014fa6af05b8d 100644 --- a/components/vivifyscrum/README.md +++ b/components/vivifyscrum/README.md @@ -1,28 +1,11 @@ # Overview -The [VivifyScrum API](https://www.vivifyscrum.com/) enables developers to -create powerful and customised tools for their customers. By harnessing the -VivifyScrum API, users can access a wide range of options for building tools -for their business. +The VivifyScrum API unleashes the potential to automate your agile project management by interfacing with your scrum boards, backlogs, and work items. Through Pipedream's serverless platform, you can create workflows that respond to events within VivifyScrum, such as new tasks or status updates. This allows for synchronization of project data across various platforms, reporting, and triggering actions in other tools, thereby enhancing productivity and providing real-time insights. -VivifyScrum offers a range of tools for customising customer’s online -experiences. You can create applications that allows customers to manage -customer accounts, manage customer records, and manage data in general. -VivifyScrum also offers access to the customer’s customer relationship -management (CRM) system. +# Example Use Cases -Some of the other features of the VivifyScrum API include visual customisation, -data manipulation, and more. The API also provides support for integrations -with third-party services such as payment gateways, email services, and chat -applications. +**Automate Task Creation from Emails**: When you receive an email in Gmail with a specific subject or from a particular sender, use Pipedream to parse the email and automatically create a backlog item or task in VivifyScrum. This ensures that action items from emails are captured without manual entry. -These are just a few examples of the kinds of tools you can create with the -VivifyScrum API: +**Sync Tasks with Calendar**: Integrate VivifyScrum with Google Calendar to add due dates from tasks as events. Whenever a due date is assigned or updated in VivifyScrum, it triggers a Pipedream workflow that creates or updates a corresponding event in Google Calendar, helping to keep track of deadlines visually. -- Automated product recommendations -- Customised customer surveys -- Data-driven customer segmentation -- Automated lead qualification -- Seamless integrations with external systems and services -- Analytics and reporting of customer data -- Tools for managing customer relationships +**Slack Notifications for Board Updates**: Set up a Pipedream workflow that listens for changes on VivifyScrum boards and sends a notification to a designated Slack channel. This keeps the team informed in real-time when tasks are moved across stages, completed, or when comments are added, fostering collaboration and immediate response to updates. diff --git a/components/vivocalendar/README.md b/components/vivocalendar/README.md new file mode 100644 index 0000000000000..33fc133fb27c1 --- /dev/null +++ b/components/vivocalendar/README.md @@ -0,0 +1,11 @@ +# Overview + +Vivocalendar API lets you interact with the Vivocalendar platform to manage events, calendars, and reminders. Within Pipedream, you can use this API to automate tasks, sync data across different calendar services, or create personalized notifications that fit into your workflow. Leveraging Pipedream's ability to integrate with numerous services, you can build custom automations that trigger on calendar events, process data, and perform actions in response. + +# Example Use Cases + +- **Automate Event Creation from Emails**: Scan incoming Gmail for specific keywords and automatically create events in Vivocalendar. This workflow would use the Gmail integration on Pipedream to trigger upon receiving new emails, parse the content for details, and create an event via the Vivocalendar API. + +- **Sync Vivocalendar with Google Calendar**: Keep your Google Calendar in sync with Vivocalendar. Each new event added to Vivocalendar can be mirrored to Google Calendar, and vice versa. This ensures that you never miss an event because you were checking the wrong calendar. The workflow can be set up using Pipedream's built-in Google Calendar app alongside the Vivocalendar API. + +- **Daily Event Digest to Slack**: Send a daily digest of your Vivocalendar events to a Slack channel. Pipedream's cron scheduler can trigger this workflow every day, fetching the day's events from Vivocalendar and formatting a message that is then sent to your chosen Slack channel using the Slack app integration. diff --git a/components/vivocalendar/actions/cancel-appointment/cancel-appointment.mjs b/components/vivocalendar/actions/cancel-appointment/cancel-appointment.mjs new file mode 100644 index 0000000000000..d72cc50f85541 --- /dev/null +++ b/components/vivocalendar/actions/cancel-appointment/cancel-appointment.mjs @@ -0,0 +1,70 @@ +import app from "../../vivocalendar.app.mjs"; + +export default { + key: "vivocalendar-cancel-appointment", + name: "Cancel Appointment", + description: "Cancels an appointment. [See the documentation](https://app.vivocalendar.com/api-docs/index.html)", + version: "0.0.1", + type: "action", + props: { + app, + user: { + propDefinition: [ + app, + "staffUserId", + () => ({ + mapper: ({ + id, email, name: label, + }) => ({ + label, + value: `${id}:${email}`, + }), + }), + ], + }, + appointmentId: { + propDefinition: [ + app, + "appointmentId", + ({ user }) => { + const email = user.split(":")[1]; + return { + email, + }; + }, + ], + }, + }, + methods: { + cancelAppointment({ + appointmentId, ...args + } = {}) { + return this.app.delete({ + path: `/appointments/${appointmentId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + cancelAppointment, + user, + appointmentId, + } = this; + + const userId = user.split(":")[0]; + + const response = await cancelAppointment({ + $, + appointmentId, + data: { + appointment: { + user_id: userId, + }, + }, + }); + + $.export("$summary", "Successfully cancelled appointment"); + return response; + }, +}; diff --git a/components/vivocalendar/actions/create-appointment/create-appointment.mjs b/components/vivocalendar/actions/create-appointment/create-appointment.mjs new file mode 100644 index 0000000000000..1707a4ec88131 --- /dev/null +++ b/components/vivocalendar/actions/create-appointment/create-appointment.mjs @@ -0,0 +1,122 @@ +import app from "../../vivocalendar.app.mjs"; + +export default { + key: "vivocalendar-create-appointment", + name: "Create Appointment", + description: "Creates a new appointment. [See the documentation](https://app.vivocalendar.com/api-docs/index.html)", + version: "0.0.1", + type: "action", + props: { + app, + userId: { + propDefinition: [ + app, + "staffUserId", + ], + }, + serviceId: { + propDefinition: [ + app, + "serviceId", + ], + }, + startTime: { + propDefinition: [ + app, + "appointmentStartTime", + ], + }, + date: { + propDefinition: [ + app, + "appointmentDate", + ], + }, + endTime: { + propDefinition: [ + app, + "appointmentEndTime", + ], + }, + title: { + type: "string", + label: "Appointment Title", + description: "The title of the appointment", + optional: true, + }, + price: { + type: "integer", + label: "Appointment Price", + description: "The price of the appointment", + optional: true, + default: 0, + }, + duration: { + type: "integer", + label: "Appointment Duration", + description: "The duration of the appointment in minutes", + }, + description: { + type: "string", + label: "Appointment Description", + description: "The description of the appointment", + optional: true, + }, + customerName: { + propDefinition: [ + app, + "customerName", + ], + }, + customerEmail: { + propDefinition: [ + app, + "customerEmail", + ], + }, + }, + methods: { + createAppointment(args = {}) { + return this.app.post({ + path: "/appointments", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createAppointment, + userId, + serviceId, + startTime, + date, + endTime, + title, + description, + price, + duration, + customerName, + customerEmail, + } = this; + + const response = await createAppointment({ + $, + params: { + "appointment[user_id]": userId, + "appointment[service_id]": serviceId, + "appointment[appointment_start_time]": startTime, + "appointment[appointment_date]": date, + "appointment[appointment_end_time]": endTime, + "appointment[price]": price, + "appointment[title]": title, + "appointment[duration]": duration, + "appointment[description]": description, + "customer[name]": customerName, + "customer[email]": customerEmail, + }, + }); + + $.export("$summary", `Successfully created appointment with ID \`${response?.response?.appointment?.id}\``); + return response; + }, +}; diff --git a/components/vivocalendar/actions/create-customer/create-customer.mjs b/components/vivocalendar/actions/create-customer/create-customer.mjs new file mode 100644 index 0000000000000..84e0f6209e25f --- /dev/null +++ b/components/vivocalendar/actions/create-customer/create-customer.mjs @@ -0,0 +1,77 @@ +import app from "../../vivocalendar.app.mjs"; + +export default { + key: "vivocalendar-create-customer", + name: "Create Customer", + description: "Creates a new customer. [See the documentation](https://app.vivocalendar.com/api-docs/index.html)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "customerName", + ], + }, + email: { + propDefinition: [ + app, + "customerEmail", + ], + }, + phone: { + propDefinition: [ + app, + "customerPhone", + ], + optional: true, + }, + whatIsYourDOB: { + propDefinition: [ + app, + "customerBirthDate", + ], + }, + whatIsYourHobby: { + propDefinition: [ + app, + "customerHobby", + ], + }, + }, + methods: { + createCustomer(args = {}) { + return this.app.post({ + path: "/customers", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createCustomer, + name, + email, + phone, + whatIsYourDOB, + whatIsYourHobby, + } = this; + + const response = await createCustomer({ + $, + data: { + customer: { + name, + email, + phone, + What_is_your_DOB: whatIsYourDOB, + What_is_your_hobby: whatIsYourHobby, + }, + }, + }); + + $.export("$summary", `Successfully created customer with ID \`${response?.response?.customer?.id}\``); + return response; + }, +}; diff --git a/components/vivocalendar/common/constants.mjs b/components/vivocalendar/common/constants.mjs new file mode 100644 index 0000000000000..cf32f9dfd2c47 --- /dev/null +++ b/components/vivocalendar/common/constants.mjs @@ -0,0 +1,11 @@ +const BASE_URL = "https://app.vivocalendar.com"; +const VERSION_PATH = "/api/v3"; +const DEFAULT_MAX = 600; +const WEBHOOK_ID = "webhookId"; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_MAX, + WEBHOOK_ID, +}; diff --git a/components/vivocalendar/package.json b/components/vivocalendar/package.json index 7cef308945a2c..4f0f13ca04620 100644 --- a/components/vivocalendar/package.json +++ b/components/vivocalendar/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/vivocalendar", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Vivocalendar Components", "main": "vivocalendar.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" } -} \ No newline at end of file +} diff --git a/components/vivocalendar/sources/common/base.mjs b/components/vivocalendar/sources/common/base.mjs new file mode 100644 index 0000000000000..e46214caa7a79 --- /dev/null +++ b/components/vivocalendar/sources/common/base.mjs @@ -0,0 +1,14 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../vivocalendar.app.mjs"; + +export default { + props: { + app, + db: "$.service.db", + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + }, +}; diff --git a/components/vivocalendar/sources/common/events.mjs b/components/vivocalendar/sources/common/events.mjs new file mode 100644 index 0000000000000..83b585522f4f5 --- /dev/null +++ b/components/vivocalendar/sources/common/events.mjs @@ -0,0 +1,8 @@ +export default { + APPOINTMENT_CREATED: "appointment_created", + APPOINTMENT_UPDATED: "appointment_updated", + APPOINTMENT_DESTROYED: "appointment_destroyed", + CUSTOMER_CREATED: "customer_created", + CUSTOMER_UPDATED: "customer_updated", + CUSTOMER_DESTROYED: "customer_destroyed", +}; diff --git a/components/vivocalendar/sources/common/polling.mjs b/components/vivocalendar/sources/common/polling.mjs new file mode 100644 index 0000000000000..76d2496a9c26c --- /dev/null +++ b/components/vivocalendar/sources/common/polling.mjs @@ -0,0 +1,58 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import common from "./base.mjs"; + +export default { + ...common, + props: { + ...common.props, + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + ...common.methods, + getResourceName() { + throw new ConfigurationError("getResourceName is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + processResources(resources) { + Array.from(resources) + .reverse() + .forEach(this.processResource); + }, + }, + async run() { + const { + getResourcesFn, + getResourcesFnArgs, + getResourceName, + processResources, + } = this; + + const resourcesFn = getResourcesFn(); + const response = await resourcesFn(getResourcesFnArgs()); + const resourceName = getResourceName(); + + const keys = resourceName.split("."); + const resources = keys.reduce((acc, key) => acc[key], response); + + processResources(resources); + }, +}; diff --git a/components/vivocalendar/sources/common/webhook.mjs b/components/vivocalendar/sources/common/webhook.mjs new file mode 100644 index 0000000000000..e6eae19012cf6 --- /dev/null +++ b/components/vivocalendar/sources/common/webhook.mjs @@ -0,0 +1,78 @@ +import { ConfigurationError } from "@pipedream/platform"; +import common from "./base.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + ...common, + props: { + ...common.props, + http: "$.interface.http", + }, + hooks: { + async activate() { + const { + http: { endpoint }, + getEventType, + } = this; + + const eventType = getEventType(); + const response = + await this.createWebhook({ + params: { + "api_subscription[callback_url]": endpoint, + "api_subscription[event_type]": eventType, + }, + data: { + api_subscription: { + callback_url: endpoint, + event_type: eventType, + }, + }, + }); + + this.setWebhookId(response?.response?.subscription?.id); + }, + async deactivate() { + const webhookId = this.getWebhookId(); + if (webhookId) { + await this.deleteWebhook({ + webhookId, + }); + } + }, + }, + methods: { + ...common.methods, + setWebhookId(value) { + this.db.set(constants.WEBHOOK_ID, value); + }, + getWebhookId() { + return this.db.get(constants.WEBHOOK_ID); + }, + getEventType() { + throw new ConfigurationError("getEventType is not implemented"); + }, + processResource(resource) { + this.$emit(resource, this.generateMeta(resource?.object)); + }, + createWebhook(args = {}) { + return this.app.post({ + debug: true, + path: "/api_subscriptions", + ...args, + }); + }, + deleteWebhook({ + webhookId, ...args + } = {}) { + return this.app.delete({ + debug: true, + path: `/api_subscriptions/${webhookId}`, + ...args, + }); + }, + }, + async run({ body }) { + this.processResource(body); + }, +}; diff --git a/components/vivocalendar/sources/new-appointment-instant/new-appointment-instant.mjs b/components/vivocalendar/sources/new-appointment-instant/new-appointment-instant.mjs new file mode 100644 index 0000000000000..dbc94d4acbf12 --- /dev/null +++ b/components/vivocalendar/sources/new-appointment-instant/new-appointment-instant.mjs @@ -0,0 +1,25 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; + +export default { + ...common, + key: "vivocalendar-new-appointment-instant", + name: "New Appointment (Instant)", + description: "Emit new event when a new appointment is created. [See the documentation](https://app.vivocalendar.com/api-docs/index.html)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return events.APPOINTMENT_CREATED; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Appointment: ${resource.id}`, + ts: Date.parse(resource.created_at), + }; + }, + }, +}; diff --git a/components/vivocalendar/sources/new-staff-member-created/new-staff-member-created.mjs b/components/vivocalendar/sources/new-staff-member-created/new-staff-member-created.mjs new file mode 100644 index 0000000000000..40d3bf94b6047 --- /dev/null +++ b/components/vivocalendar/sources/new-staff-member-created/new-staff-member-created.mjs @@ -0,0 +1,30 @@ +import common from "../common/polling.mjs"; + +export default { + ...common, + key: "vivocalendar-new-staff-member-created", + name: "New Staff Member Created", + description: "Emit new event when a new staff member is created in VIVO Calendar. [See the documentation](https://app.vivocalendar.com/api-docs/index.html)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceName() { + return "response.staff_members"; + }, + getResourcesFn() { + return this.app.listStaffUsers; + }, + getResourcesFnArgs() { + return {}; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Member: ${resource.email}`, + ts: Date.parse(resource.created_at), + }; + }, + }, +}; diff --git a/components/vivocalendar/vivocalendar.app.mjs b/components/vivocalendar/vivocalendar.app.mjs index 501629feffc6d..a1b7c109265bc 100644 --- a/components/vivocalendar/vivocalendar.app.mjs +++ b/components/vivocalendar/vivocalendar.app.mjs @@ -1,11 +1,156 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "vivocalendar", - propDefinitions: {}, + propDefinitions: { + customerName: { + type: "string", + label: "Customer Name", + description: "The name of the customer", + }, + customerEmail: { + type: "string", + label: "Customer Email", + description: "The email address of the customer", + optional: true, + }, + customerPhone: { + type: "string", + label: "Customer Phone Number", + description: "The phone number of the customer", + optional: true, + }, + customerBirthDate: { + type: "string", + label: "Customer Date of Birth", + description: "The date of birth of the customer", + optional: true, + }, + customerHobby: { + type: "string", + label: "Customer Hobby", + description: "The hobby of the customer", + optional: true, + }, + staffUserId: { + type: "string", + label: "User ID", + description: "The unique identifier for the user", + async options({ + mapper = ({ + id: value, name: label, + }) => ({ + label, + value, + }), + }) { + const { response: { staff_members: users } } = await this.listStaffUsers(); + return users.map(mapper); + }, + }, + serviceId: { + type: "string", + label: "Service ID", + description: "The unique identifier for the service", + async options() { + const { response: { services } } = await this.listServices(); + return services.map(({ + id: value, service_name: label, + }) => ({ + label, + value, + })); + }, + }, + appointmentStartTime: { + type: "string", + label: "Appointment Start Time", + description: "The start time of the appointment. Eg `16:30`", + }, + appointmentDate: { + type: "string", + label: "Appointment Date", + description: "The date of the appointment. Eg `2024-01-01`", + }, + appointmentEndTime: { + type: "string", + label: "Appointment End Time", + description: "The end time of the appointment. Eg `17:30`", + }, + appointmentId: { + type: "string", + label: "Appointment ID", + description: "The unique identifier for the appointment", + async options({ + page, email, + }) { + const { response: { appointment } } = await this.listAppointments({ + params: { + page, + email, + }, + }); + return appointment.map(({ + id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Content-Type": "application/json", + "accept": "application/json", + "api-key": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + listStaffUsers(args = {}) { + return this._makeRequest({ + path: "/staff_members", + ...args, + }); + }, + listServices(args = {}) { + return this._makeRequest({ + path: "/services", + ...args, + }); + }, + listAppointments(args = {}) { + return this._makeRequest({ + path: "/appointments", + ...args, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/vivomeetings/README.md b/components/vivomeetings/README.md new file mode 100644 index 0000000000000..b6c3ef2e2b8c3 --- /dev/null +++ b/components/vivomeetings/README.md @@ -0,0 +1,11 @@ +# Overview + +The Vivomeetings API allows developers to integrate real-time video conferencing capabilities into their applications. With this API, users can create, manage, and customize video meetings directly through the Pipedream platform. Utilizing Pipedream's serverless execution model, you can interface with the Vivomeetings API to automate meeting setups, dynamically manage participants, and harness data from meeting events for analytics or enhanced user experiences. + +# Example Use Cases + +- **Automated Meeting Scheduling and Notifications**: Connect Vivomeetings with Google Calendar via Pipedream to automate the creation and scheduling of meetings based on calendar events. This workflow can listen for new events added to a Google Calendar, automatically create a meeting in Vivomeetings, and send custom notifications to participants via email or SMS. + +- **Dynamic Meeting Management Based on Attendance**: Integrate Vivomeetings with Slack using Pipedream. This workflow triggers when a scheduled meeting's start time is near, checking if the minimum number of participants have joined. If not, it can send reminders through Slack messages or postpone the meeting automatically, updating all participants via their preferred communication method. + +- **Post-Meeting Feedback Collection and Analysis**: After a Vivomeetings session ends, trigger a workflow that sends a feedback form to all participants using Typeform. Collect responses and store them in a Google Sheets spreadsheet for easy analysis and follow-up. This process helps in gathering valuable insights to improve future meetings and participant engagement. diff --git a/components/vivomeetings/actions/create-conference/create-conference.mjs b/components/vivomeetings/actions/create-conference/create-conference.mjs new file mode 100644 index 0000000000000..05cab4ace17d9 --- /dev/null +++ b/components/vivomeetings/actions/create-conference/create-conference.mjs @@ -0,0 +1,147 @@ +import { parseObject } from "../../common/utils.mjs"; +import vivomeetings from "../../vivomeetings.app.mjs"; + +export default { + key: "vivomeetings-create-conference", + name: "Create Conference", + description: "Creates a new conference in VivoMeetings. [See the documentation](https://docs.google.com/viewerng/viewer?url=https://vivomeetings.com/wp-content/uploads/2023/01/Partner-APIs-v1.41.docx-1.pdf)", + version: "0.0.1", + type: "action", + props: { + vivomeetings, + companyId: { + propDefinition: [ + vivomeetings, + "companyId", + ], + }, + hostId: { + propDefinition: [ + vivomeetings, + "hostId", + ], + }, + subject: { + propDefinition: [ + vivomeetings, + "subject", + ], + }, + agenda: { + propDefinition: [ + vivomeetings, + "agenda", + ], + }, + start: { + propDefinition: [ + vivomeetings, + "start", + ], + }, + timeZone: { + propDefinition: [ + vivomeetings, + "timeZone", + ], + }, + duration: { + propDefinition: [ + vivomeetings, + "duration", + ], + }, + autoRecord: { + propDefinition: [ + vivomeetings, + "autoRecord", + ], + }, + autoStream: { + propDefinition: [ + vivomeetings, + "autoStream", + ], + }, + autoTranscribe: { + propDefinition: [ + vivomeetings, + "autoTranscribe", + ], + }, + oneTimeAccessCode: { + propDefinition: [ + vivomeetings, + "oneTimeAccessCode", + ], + }, + secureUrl: { + propDefinition: [ + vivomeetings, + "secureUrl", + ], + }, + hostInitiatedRecording: { + propDefinition: [ + vivomeetings, + "hostInitiatedRecording", + ], + optional: true, + }, + securityPin: { + propDefinition: [ + vivomeetings, + "securityPin", + ], + }, + muteMode: { + propDefinition: [ + vivomeetings, + "muteMode", + ], + }, + participants: { + propDefinition: [ + vivomeetings, + "contactIds", + ({ hostId }) => ({ + hostId, + }), + ], + }, + participantEmails: { + propDefinition: [ + vivomeetings, + "participantEmails", + ], + }, + }, + async run({ $ }) { + const response = await this.vivomeetings.createConference({ + $, + data: { + host_id: this.hostId, + subject: this.subject, + agenda: this.agenda, + start: this.start, + time_zone: this.timeZone, + duration: this.duration, + auto_record: this.autoRecord, + auto_stream: this.autoStream, + auto_transcribe: this.autoTranscribe, + one_time_access_code: this.oneTimeAccessCode, + secure_url: this.secureUrl, + host_initiated_recording: this.hostInitiatedRecording, + security_pin: this.securityPin, + mute_mode: this.muteMode, + participants: this.participants, + participant_emails: parseObject(this.participantEmails)?.map((item) => ({ + email: item, + })), + }, + }); + + $.export("$summary", `Conference with Id: "${response.conference_id}" created successfully`); + return response; + }, +}; diff --git a/components/vivomeetings/actions/get-conference-recordings/get-conference-recordings.mjs b/components/vivomeetings/actions/get-conference-recordings/get-conference-recordings.mjs new file mode 100644 index 0000000000000..747e09d02e4da --- /dev/null +++ b/components/vivomeetings/actions/get-conference-recordings/get-conference-recordings.mjs @@ -0,0 +1,37 @@ +import vivomeetings from "../../vivomeetings.app.mjs"; + +export default { + key: "vivomeetings-get-conference-recordings", + name: "Get Conference Recordings", + description: "Fetches the recordings of a conference or webinar from VivoMeetings. [See the documentation](https://docs.google.com/viewerng/viewer?url=https://vivomeetings.com/wp-content/uploads/2023/01/Partner-APIs-v1.41.docx-1.pdf)", + version: "0.0.1", + type: "action", + props: { + vivomeetings, + hostId: { + propDefinition: [ + vivomeetings, + "hostId", + ], + }, + conferenceId: { + propDefinition: [ + vivomeetings, + "conferenceId", + ({ hostId }) => ({ + hostId, + }), + ], + }, + }, + async run({ $ }) { + const { recordings } = await this.vivomeetings.getConferenceDetails({ + $, + data: { + conference_id: this.conferenceId, + }, + }); + $.export("$summary", `Successfully fetched recordings for conference ID: ${this.conferenceId}`); + return recordings; + }, +}; diff --git a/components/vivomeetings/actions/update-conference/update-conference.mjs b/components/vivomeetings/actions/update-conference/update-conference.mjs new file mode 100644 index 0000000000000..4fc54d1e1f5e9 --- /dev/null +++ b/components/vivomeetings/actions/update-conference/update-conference.mjs @@ -0,0 +1,119 @@ +import vivomeetings from "../../vivomeetings.app.mjs"; + +export default { + key: "vivomeetings-update-conference", + name: "Update Conference", + description: "Updates an existing conference or webinar on VivoMeetings. [See the documentation](https://docs.google.com/viewerng/viewer?url=https://vivomeetings.com/wp-content/uploads/2023/01/Partner-APIs-v1.41.docx-1.pdf)", + version: "0.0.1", + type: "action", + props: { + vivomeetings, + hostId: { + propDefinition: [ + vivomeetings, + "hostId", + ], + }, + conferenceId: { + propDefinition: [ + vivomeetings, + "conferenceId", + ({ hostId }) => ({ + hostId, + }), + ], + }, + subject: { + propDefinition: [ + vivomeetings, + "subject", + ], + optional: true, + }, + agenda: { + propDefinition: [ + vivomeetings, + "agenda", + ], + optional: true, + }, + start: { + propDefinition: [ + vivomeetings, + "start", + ], + optional: true, + }, + timeZone: { + propDefinition: [ + vivomeetings, + "timeZone", + ], + optional: true, + }, + duration: { + propDefinition: [ + vivomeetings, + "duration", + ], + optional: true, + }, + autoRecord: { + propDefinition: [ + vivomeetings, + "autoRecord", + ], + }, + autoTranscribe: { + propDefinition: [ + vivomeetings, + "autoTranscribe", + ], + }, + oneTimeAccessCode: { + propDefinition: [ + vivomeetings, + "oneTimeAccessCode", + ], + optional: true, + }, + muteMode: { + propDefinition: [ + vivomeetings, + "muteMode", + ], + optional: true, + }, + participants: { + propDefinition: [ + vivomeetings, + "contactIds", + ({ hostId }) => ({ + hostId, + }), + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.vivomeetings.updateConference({ + $, + data: { + conference_id: this.conferenceId, + subject: this.subject, + agenda: this.agenda, + start: this.start, + time_zone: this.timeZone, + duration: this.duration, + auto_record: this.autoRecord, + auto_transcribe: this.autoTranscribe, + one_time_access_code: this.oneTimeAccessCode, + mute_mode: this.muteMode, + participants: this.participants, + }, + }); + + $.export("$summary", `Successfully updated the conference with ID ${this.conferenceId}`); + return response; + }, +}; diff --git a/components/vivomeetings/common/constants.mjs b/components/vivomeetings/common/constants.mjs new file mode 100644 index 0000000000000..09b4ce9f40a61 --- /dev/null +++ b/components/vivomeetings/common/constants.mjs @@ -0,0 +1,2411 @@ +export const AUTO_RECORD_OPTIONS = [ + "none", + "audio", + "video", +]; + +export const MUTE_MODE_OPTIONS = [ + { + label: "Default, all unmuted", + value: "conversation", + }, + { + label: "Participants muted, but can unmute", + value: "q&a", + }, + { + label: "Participants muted and can not unmute", + value: "presentation", + }, +]; + +export const TIMEZONE_OPTIONS = [ + { + label: "Africa/Abidjan +00:00", + value: "Africa/Abidjan", + }, + { + label: "Africa/Accra +00:00", + value: "Africa/Accra", + }, + { + label: "Africa/Addis_Ababa +03:00", + value: "Africa/Addis_Ababa", + }, + { + label: "Africa/Algiers +01:00", + value: "Africa/Algiers", + }, + { + label: "Africa/Asmara +03:00", + value: "Africa/Asmara", + }, + { + label: "Africa/Asmera +03:00", + value: "Africa/Asmera", + }, + { + label: "Africa/Bamako +00:00", + value: "Africa/Bamako", + }, + { + label: "Africa/Bangui +01:00", + value: "Africa/Bangui", + }, + { + label: "Africa/Banjul +00:00", + value: "Africa/Banjul", + }, + { + label: "Africa/Bissau +00:00", + value: "Africa/Bissau", + }, + { + label: "Africa/Blantyre +02:00", + value: "Africa/Blantyre", + }, + { + label: "Africa/Brazzaville +01:00", + value: "Africa/Brazzaville", + }, + { + label: "Africa/Bujumbura +02:00", + value: "Africa/Bujumbura", + }, + { + label: "Africa/Cairo +02:00", + value: "Africa/Cairo", + }, + { + label: "Africa/Casablanca +01:00", + value: "Africa/Casablanca", + }, + { + label: "Africa/Ceuta +01:00", + value: "Africa/Ceuta", + }, + { + label: "Africa/Conakry +00:00", + value: "Africa/Conakry", + }, + { + label: "Africa/Dakar +00:00", + value: "Africa/Dakar", + }, + { + label: "Africa/Dar_es_Salaam +03:00", + value: "Africa/Dar_es_Salaam", + }, + { + label: "Africa/Djibouti +03:00", + value: "Africa/Djibouti", + }, + { + label: "Africa/Douala +01:00", + value: "Africa/Douala", + }, + { + label: "Africa/El_Aaiun +01:00", + value: "Africa/El_Aaiun", + }, + { + label: "Africa/Freetown +00:00", + value: "Africa/Freetown", + }, + { + label: "Africa/Gaborone +02:00", + value: "Africa/Gaborone", + }, + { + label: "Africa/Harare +02:00", + value: "Africa/Harare", + }, + { + label: "Africa/Johannesburg +02:00", + value: "Africa/Johannesburg", + }, + { + label: "Africa/Juba +02:00", + value: "Africa/Juba", + }, + { + label: "Africa/Kampala +03:00", + value: "Africa/Kampala", + }, + { + label: "Africa/Khartoum +02:00", + value: "Africa/Khartoum", + }, + { + label: "Africa/Kigali +02:00", + value: "Africa/Kigali", + }, + { + label: "Africa/Kinshasa +01:00", + value: "Africa/Kinshasa", + }, + { + label: "Africa/Lagos +01:00", + value: "Africa/Lagos", + }, + { + label: "Africa/Libreville +01:00", + value: "Africa/Libreville", + }, + { + label: "Africa/Lome +00:00", + value: "Africa/Lome", + }, + { + label: "Africa/Luanda +01:00", + value: "Africa/Luanda", + }, + { + label: "Africa/Lubumbashi +02:00", + value: "Africa/Lubumbashi", + }, + { + label: "Africa/Lusaka +02:00", + value: "Africa/Lusaka", + }, + { + label: "Africa/Malabo +01:00", + value: "Africa/Malabo", + }, + { + label: "Africa/Maputo +02:00", + value: "Africa/Maputo", + }, + { + label: "Africa/Maseru +02:00", + value: "Africa/Maseru", + }, + { + label: "Africa/Mbabane +02:00", + value: "Africa/Mbabane", + }, + { + label: "Africa/Mogadishu +03:00", + value: "Africa/Mogadishu", + }, + { + label: "Africa/Monrovia +00:00", + value: "Africa/Monrovia", + }, + { + label: "Africa/Nairobi +03:00", + value: "Africa/Nairobi", + }, + { + label: "Africa/Ndjamena +01:00", + value: "Africa/Ndjamena", + }, + { + label: "Africa/Niamey +01:00", + value: "Africa/Niamey", + }, + { + label: "Africa/Nouakchott +00:00", + value: "Africa/Nouakchott", + }, + { + label: "Africa/Ouagadougou +00:00", + value: "Africa/Ouagadougou", + }, + { + label: "Africa/Porto-Novo +01:00", + value: "Africa/Porto-Novo", + }, + { + label: "Africa/Sao_Tome +00:00", + value: "Africa/Sao_Tome", + }, + { + label: "Africa/Timbuktu +00:00", + value: "Africa/Timbuktu", + }, + { + label: "Africa/Tripoli +02:00", + value: "Africa/Tripoli", + }, + { + label: "Africa/Tunis +01:00", + value: "Africa/Tunis", + }, + { + label: "Africa/Windhoek +02:00", + value: "Africa/Windhoek", + }, + { + label: "America/Adak -10:00", + value: "America/Adak", + }, + { + label: "America/Anchorage -09:00", + value: "America/Anchorage", + }, + { + label: "America/Anguilla -04:00", + value: "America/Anguilla", + }, + { + label: "America/Antigua -04:00", + value: "America/Antigua", + }, + { + label: "America/Araguaina -03:00", + value: "America/Araguaina", + }, + { + label: "America/Argentina/Buenos_Aires -03:00", + value: "America/Argentina/Buenos_Aires", + }, + { + label: "America/Argentina/Catamarca -03:00", + value: "America/Argentina/Catamarca", + }, + { + label: "America/Argentina/ComodRivadavia -03:00", + value: "America/Argentina/ComodRivadavia", + }, + { + label: "America/Argentina/Cordoba -03:00", + value: "America/Argentina/Cordoba", + }, + { + label: "America/Argentina/Jujuy -03:00", + value: "America/Argentina/Jujuy", + }, + { + label: "America/Argentina/La_Rioja -03:00", + value: "America/Argentina/La_Rioja", + }, + { + label: "America/Argentina/Mendoza -03:00", + value: "America/Argentina/Mendoza", + }, + { + label: "America/Argentina/Rio_Gallegos -03:00", + value: "America/Argentina/Rio_Gallegos", + }, + { + label: "America/Argentina/Salta -03:00", + value: "America/Argentina/Salta", + }, + { + label: "America/Argentina/San_Juan -03:00", + value: "America/Argentina/San_Juan", + }, + { + label: "America/Argentina/San_Luis -03:00", + value: "America/Argentina/San_Luis", + }, + { + label: "America/Argentina/Tucuman -03:00", + value: "America/Argentina/Tucuman", + }, + { + label: "America/Argentina/Ushuaia -03:00", + value: "America/Argentina/Ushuaia", + }, + { + label: "America/Aruba -04:00", + value: "America/Aruba", + }, + { + label: "America/Asuncion -04:00", + value: "America/Asuncion", + }, + { + label: "America/Atikokan -05:00", + value: "America/Atikokan", + }, + { + label: "America/Atka -10:00", + value: "America/Atka", + }, + { + label: "America/Bahia -03:00", + value: "America/Bahia", + }, + { + label: "America/Bahia_Banderas -06:00", + value: "America/Bahia_Banderas", + }, + { + label: "America/Barbados -04:00", + value: "America/Barbados", + }, + { + label: "America/Belem -03:00", + value: "America/Belem", + }, + { + label: "America/Belize -06:00", + value: "America/Belize", + }, + { + label: "America/Blanc-Sablon -04:00", + value: "America/Blanc-Sablon", + }, + { + label: "America/Boa_Vista -04:00", + value: "America/Boa_Vista", + }, + { + label: "America/Bogota -05:00", + value: "America/Bogota", + }, + { + label: "America/Boise -07:00", + value: "America/Boise", + }, + { + label: "America/Buenos_Aires -03:00", + value: "America/Buenos_Aires", + }, + { + label: "America/Cambridge_Bay -07:00", + value: "America/Cambridge_Bay", + }, + { + label: "America/Campo_Grande -04:00", + value: "America/Campo_Grande", + }, + { + label: "America/Cancun -05:00", + value: "America/Cancun", + }, + { + label: "America/Caracas -04:00", + value: "America/Caracas", + }, + { + label: "America/Catamarca -03:00", + value: "America/Catamarca", + }, + { + label: "America/Cayenne -03:00", + value: "America/Cayenne", + }, + { + label: "America/Cayman -05:00", + value: "America/Cayman", + }, + { + label: "America/Chicago -06:00", + value: "America/Chicago", + }, + { + label: "America/Chihuahua -06:00", + value: "America/Chihuahua", + }, + { + label: "America/Ciudad_Juarez -07:00", + value: "America/Ciudad_Juarez", + }, + { + label: "America/Coral_Harbour -05:00", + value: "America/Coral_Harbour", + }, + { + label: "America/Cordoba -03:00", + value: "America/Cordoba", + }, + { + label: "America/Costa_Rica -06:00", + value: "America/Costa_Rica", + }, + { + label: "America/Creston -07:00", + value: "America/Creston", + }, + { + label: "America/Cuiaba -04:00", + value: "America/Cuiaba", + }, + { + label: "America/Curacao -04:00", + value: "America/Curacao", + }, + { + label: "America/Danmarkshavn +00:00", + value: "America/Danmarkshavn", + }, + { + label: "America/Dawson -07:00", + value: "America/Dawson", + }, + { + label: "America/Dawson_Creek -07:00", + value: "America/Dawson_Creek", + }, + { + label: "America/Denver -07:00", + value: "America/Denver", + }, + { + label: "America/Detroit -05:00", + value: "America/Detroit", + }, + { + label: "America/Dominica -04:00", + value: "America/Dominica", + }, + { + label: "America/Edmonton -07:00", + value: "America/Edmonton", + }, + { + label: "America/Eirunepe -05:00", + value: "America/Eirunepe", + }, + { + label: "America/El_Salvador -06:00", + value: "America/El_Salvador", + }, + { + label: "America/Ensenada -08:00", + value: "America/Ensenada", + }, + { + label: "America/Fort_Nelson -07:00", + value: "America/Fort_Nelson", + }, + { + label: "America/Fort_Wayne -05:00", + value: "America/Fort_Wayne", + }, + { + label: "America/Fortaleza -03:00", + value: "America/Fortaleza", + }, + { + label: "America/Glace_Bay -04:00", + value: "America/Glace_Bay", + }, + { + label: "America/Godthab -02:00", + value: "America/Godthab", + }, + { + label: "America/Goose_Bay -04:00", + value: "America/Goose_Bay", + }, + { + label: "America/Grand_Turk -05:00", + value: "America/Grand_Turk", + }, + { + label: "America/Grenada -04:00", + value: "America/Grenada", + }, + { + label: "America/Guadeloupe -04:00", + value: "America/Guadeloupe", + }, + { + label: "America/Guatemala -06:00", + value: "America/Guatemala", + }, + { + label: "America/Guayaquil -05:00", + value: "America/Guayaquil", + }, + { + label: "America/Guyana -04:00", + value: "America/Guyana", + }, + { + label: "America/Halifax -04:00", + value: "America/Halifax", + }, + { + label: "America/Havana -05:00", + value: "America/Havana", + }, + { + label: "America/Hermosillo -07:00", + value: "America/Hermosillo", + }, + { + label: "America/Indiana/Indianapolis -05:00", + value: "America/Indiana/Indianapolis", + }, + { + label: "America/Indiana/Knox -06:00", + value: "America/Indiana/Knox", + }, + { + label: "America/Indiana/Marengo -05:00", + value: "America/Indiana/Marengo", + }, + { + label: "America/Indiana/Petersburg -05:00", + value: "America/Indiana/Petersburg", + }, + { + label: "America/Indiana/Tell_City -06:00", + value: "America/Indiana/Tell_City", + }, + { + label: "America/Indiana/Vevay -05:00", + value: "America/Indiana/Vevay", + }, + { + label: "America/Indiana/Vincennes -05:00", + value: "America/Indiana/Vincennes", + }, + { + label: "America/Indiana/Winamac -05:00", + value: "America/Indiana/Winamac", + }, + { + label: "America/Indianapolis -05:00", + value: "America/Indianapolis", + }, + { + label: "America/Inuvik -07:00", + value: "America/Inuvik", + }, + { + label: "America/Iqaluit -05:00", + value: "America/Iqaluit", + }, + { + label: "America/Jamaica -05:00", + value: "America/Jamaica", + }, + { + label: "America/Jujuy -03:00", + value: "America/Jujuy", + }, + { + label: "America/Juneau -09:00", + value: "America/Juneau", + }, + { + label: "America/Kentucky/Louisville -05:00", + value: "America/Kentucky/Louisville", + }, + { + label: "America/Kentucky/Monticello -05:00", + value: "America/Kentucky/Monticello", + }, + { + label: "America/Knox_IN -06:00", + value: "America/Knox_IN", + }, + { + label: "America/Kralendijk -04:00", + value: "America/Kralendijk", + }, + { + label: "America/La_Paz -04:00", + value: "America/La_Paz", + }, + { + label: "America/Lima -05:00", + value: "America/Lima", + }, + { + label: "America/Los_Angeles -08:00", + value: "America/Los_Angeles", + }, + { + label: "America/Louisville -05:00", + value: "America/Louisville", + }, + { + label: "America/Lower_Princes -04:00", + value: "America/Lower_Princes", + }, + { + label: "America/Maceio -03:00", + value: "America/Maceio", + }, + { + label: "America/Managua -06:00", + value: "America/Managua", + }, + { + label: "America/Manaus -04:00", + value: "America/Manaus", + }, + { + label: "America/Marigot -04:00", + value: "America/Marigot", + }, + { + label: "America/Martinique -04:00", + value: "America/Martinique", + }, + { + label: "America/Matamoros -06:00", + value: "America/Matamoros", + }, + { + label: "America/Mazatlan -07:00", + value: "America/Mazatlan", + }, + { + label: "America/Mendoza -03:00", + value: "America/Mendoza", + }, + { + label: "America/Menominee -06:00", + value: "America/Menominee", + }, + { + label: "America/Merida -06:00", + value: "America/Merida", + }, + { + label: "America/Metlakatla -09:00", + value: "America/Metlakatla", + }, + { + label: "America/Mexico_City -06:00", + value: "America/Mexico_City", + }, + { + label: "America/Miquelon -03:00", + value: "America/Miquelon", + }, + { + label: "America/Moncton -04:00", + value: "America/Moncton", + }, + { + label: "America/Monterrey -06:00", + value: "America/Monterrey", + }, + { + label: "America/Montevideo -03:00", + value: "America/Montevideo", + }, + { + label: "America/Montreal -05:00", + value: "America/Montreal", + }, + { + label: "America/Montserrat -04:00", + value: "America/Montserrat", + }, + { + label: "America/Nassau -05:00", + value: "America/Nassau", + }, + { + label: "America/New_York -05:00", + value: "America/New_York", + }, + { + label: "America/Nipigon -05:00", + value: "America/Nipigon", + }, + { + label: "America/Nome -09:00", + value: "America/Nome", + }, + { + label: "America/Noronha -02:00", + value: "America/Noronha", + }, + { + label: "America/North_Dakota/Beulah -06:00", + value: "America/North_Dakota/Beulah", + }, + { + label: "America/North_Dakota/Center -06:00", + value: "America/North_Dakota/Center", + }, + { + label: "America/North_Dakota/New_Salem -06:00", + value: "America/North_Dakota/New_Salem", + }, + { + label: "America/Nuuk -02:00", + value: "America/Nuuk", + }, + { + label: "America/Ojinaga -06:00", + value: "America/Ojinaga", + }, + { + label: "America/Panama -05:00", + value: "America/Panama", + }, + { + label: "America/Pangnirtung -05:00", + value: "America/Pangnirtung", + }, + { + label: "America/Paramaribo -03:00", + value: "America/Paramaribo", + }, + { + label: "America/Phoenix -07:00", + value: "America/Phoenix", + }, + { + label: "America/Port_of_Spain -04:00", + value: "America/Port_of_Spain", + }, + { + label: "America/Port-au-Prince -05:00", + value: "America/Port-au-Prince", + }, + { + label: "America/Porto_Acre -05:00", + value: "America/Porto_Acre", + }, + { + label: "America/Porto_Velho -04:00", + value: "America/Porto_Velho", + }, + { + label: "America/Puerto_Rico -04:00", + value: "America/Puerto_Rico", + }, + { + label: "America/Punta_Arenas -03:00", + value: "America/Punta_Arenas", + }, + { + label: "America/Rainy_River -06:00", + value: "America/Rainy_River", + }, + { + label: "America/Rankin_Inlet -06:00", + value: "America/Rankin_Inlet", + }, + { + label: "America/Recife -03:00", + value: "America/Recife", + }, + { + label: "America/Regina -06:00", + value: "America/Regina", + }, + { + label: "America/Resolute -06:00", + value: "America/Resolute", + }, + { + label: "America/Rio_Branco -05:00", + value: "America/Rio_Branco", + }, + { + label: "America/Rosario -03:00", + value: "America/Rosario", + }, + { + label: "America/Santa_Isabel -08:00", + value: "America/Santa_Isabel", + }, + { + label: "America/Santarem -03:00", + value: "America/Santarem", + }, + { + label: "America/Santiago -04:00", + value: "America/Santiago", + }, + { + label: "America/Santo_Domingo -04:00", + value: "America/Santo_Domingo", + }, + { + label: "America/Sao_Paulo -03:00", + value: "America/Sao_Paulo", + }, + { + label: "America/Scoresbysund -02:00", + value: "America/Scoresbysund", + }, + { + label: "America/Shiprock -07:00", + value: "America/Shiprock", + }, + { + label: "America/Sitka -09:00", + value: "America/Sitka", + }, + { + label: "America/St_Barthelemy -04:00", + value: "America/St_Barthelemy", + }, + { + label: "America/St_Johns -03:30", + value: "America/St_Johns", + }, + { + label: "America/St_Kitts -04:00", + value: "America/St_Kitts", + }, + { + label: "America/St_Lucia -04:00", + value: "America/St_Lucia", + }, + { + label: "America/St_Thomas -04:00", + value: "America/St_Thomas", + }, + { + label: "America/St_Vincent -04:00", + value: "America/St_Vincent", + }, + { + label: "America/Swift_Current -06:00", + value: "America/Swift_Current", + }, + { + label: "America/Tegucigalpa -06:00", + value: "America/Tegucigalpa", + }, + { + label: "America/Thule -04:00", + value: "America/Thule", + }, + { + label: "America/Thunder_Bay -05:00", + value: "America/Thunder_Bay", + }, + { + label: "America/Tijuana -08:00", + value: "America/Tijuana", + }, + { + label: "America/Toronto -05:00", + value: "America/Toronto", + }, + { + label: "America/Tortola -04:00", + value: "America/Tortola", + }, + { + label: "America/Vancouver -08:00", + value: "America/Vancouver", + }, + { + label: "America/Virgin -04:00", + value: "America/Virgin", + }, + { + label: "America/Whitehorse -07:00", + value: "America/Whitehorse", + }, + { + label: "America/Winnipeg -06:00", + value: "America/Winnipeg", + }, + { + label: "America/Yakutat -09:00", + value: "America/Yakutat", + }, + { + label: "America/Yellowknife -07:00", + value: "America/Yellowknife", + }, + { + label: "Antarctica/Casey +08:00", + value: "Antarctica/Casey", + }, + { + label: "Antarctica/Davis +07:00", + value: "Antarctica/Davis", + }, + { + label: "Antarctica/DumontDUrville +10:00", + value: "Antarctica/DumontDUrville", + }, + { + label: "Antarctica/Macquarie +10:00", + value: "Antarctica/Macquarie", + }, + { + label: "Antarctica/Mawson +05:00", + value: "Antarctica/Mawson", + }, + { + label: "Antarctica/McMurdo +12:00", + value: "Antarctica/McMurdo", + }, + { + label: "Antarctica/Palmer -03:00", + value: "Antarctica/Palmer", + }, + { + label: "Antarctica/Rothera -03:00", + value: "Antarctica/Rothera", + }, + { + label: "Antarctica/South_Pole +12:00", + value: "Antarctica/South_Pole", + }, + { + label: "Antarctica/Syowa +03:00", + value: "Antarctica/Syowa", + }, + { + label: "Antarctica/Troll +00:00", + value: "Antarctica/Troll", + }, + { + label: "Antarctica/Vostok +05:00", + value: "Antarctica/Vostok", + }, + { + label: "Arctic/Longyearbyen +01:00", + value: "Arctic/Longyearbyen", + }, + { + label: "Asia/Aden +03:00", + value: "Asia/Aden", + }, + { + label: "Asia/Almaty +05:00", + value: "Asia/Almaty", + }, + { + label: "Asia/Amman +03:00", + value: "Asia/Amman", + }, + { + label: "Asia/Anadyr +12:00", + value: "Asia/Anadyr", + }, + { + label: "Asia/Aqtau +05:00", + value: "Asia/Aqtau", + }, + { + label: "Asia/Aqtobe +05:00", + value: "Asia/Aqtobe", + }, + { + label: "Asia/Ashgabat +05:00", + value: "Asia/Ashgabat", + }, + { + label: "Asia/Ashkhabad +05:00", + value: "Asia/Ashkhabad", + }, + { + label: "Asia/Atyrau +05:00", + value: "Asia/Atyrau", + }, + { + label: "Asia/Baghdad +03:00", + value: "Asia/Baghdad", + }, + { + label: "Asia/Bahrain +03:00", + value: "Asia/Bahrain", + }, + { + label: "Asia/Baku +04:00", + value: "Asia/Baku", + }, + { + label: "Asia/Bangkok +07:00", + value: "Asia/Bangkok", + }, + { + label: "Asia/Barnaul +07:00", + value: "Asia/Barnaul", + }, + { + label: "Asia/Beirut +02:00", + value: "Asia/Beirut", + }, + { + label: "Asia/Bishkek +06:00", + value: "Asia/Bishkek", + }, + { + label: "Asia/Brunei +08:00", + value: "Asia/Brunei", + }, + { + label: "Asia/Calcutta +05:30", + value: "Asia/Calcutta", + }, + { + label: "Asia/Chita +09:00", + value: "Asia/Chita", + }, + { + label: "Asia/Choibalsan +08:00", + value: "Asia/Choibalsan", + }, + { + label: "Asia/Chongqing +08:00", + value: "Asia/Chongqing", + }, + { + label: "Asia/Chungking +08:00", + value: "Asia/Chungking", + }, + { + label: "Asia/Colombo +05:30", + value: "Asia/Colombo", + }, + { + label: "Asia/Dacca +06:00", + value: "Asia/Dacca", + }, + { + label: "Asia/Damascus +03:00", + value: "Asia/Damascus", + }, + { + label: "Asia/Dhaka +06:00", + value: "Asia/Dhaka", + }, + { + label: "Asia/Dili +09:00", + value: "Asia/Dili", + }, + { + label: "Asia/Dubai +04:00", + value: "Asia/Dubai", + }, + { + label: "Asia/Dushanbe +05:00", + value: "Asia/Dushanbe", + }, + { + label: "Asia/Famagusta +02:00", + value: "Asia/Famagusta", + }, + { + label: "Asia/Gaza +02:00", + value: "Asia/Gaza", + }, + { + label: "Asia/Harbin +08:00", + value: "Asia/Harbin", + }, + { + label: "Asia/Hebron +02:00", + value: "Asia/Hebron", + }, + { + label: "Asia/Ho_Chi_Minh +07:00", + value: "Asia/Ho_Chi_Minh", + }, + { + label: "Asia/Hong_Kong +08:00", + value: "Asia/Hong_Kong", + }, + { + label: "Asia/Hovd +07:00", + value: "Asia/Hovd", + }, + { + label: "Asia/Irkutsk +08:00", + value: "Asia/Irkutsk", + }, + { + label: "Asia/Istanbul +03:00", + value: "Asia/Istanbul", + }, + { + label: "Asia/Jakarta +07:00", + value: "Asia/Jakarta", + }, + { + label: "Asia/Jayapura +09:00", + value: "Asia/Jayapura", + }, + { + label: "Asia/Jerusalem +02:00", + value: "Asia/Jerusalem", + }, + { + label: "Asia/Kabul +04:30", + value: "Asia/Kabul", + }, + { + label: "Asia/Kamchatka +12:00", + value: "Asia/Kamchatka", + }, + { + label: "Asia/Karachi +05:00", + value: "Asia/Karachi", + }, + { + label: "Asia/Kashgar +06:00", + value: "Asia/Kashgar", + }, + { + label: "Asia/Kathmandu +05:45", + value: "Asia/Kathmandu", + }, + { + label: "Asia/Katmandu +05:45", + value: "Asia/Katmandu", + }, + { + label: "Asia/Khandyga +09:00", + value: "Asia/Khandyga", + }, + { + label: "Asia/Kolkata +05:30", + value: "Asia/Kolkata", + }, + { + label: "Asia/Krasnoyarsk +07:00", + value: "Asia/Krasnoyarsk", + }, + { + label: "Asia/Kuala_Lumpur +08:00", + value: "Asia/Kuala_Lumpur", + }, + { + label: "Asia/Kuching +08:00", + value: "Asia/Kuching", + }, + { + label: "Asia/Kuwait +03:00", + value: "Asia/Kuwait", + }, + { + label: "Asia/Macao +08:00", + value: "Asia/Macao", + }, + { + label: "Asia/Macau +08:00", + value: "Asia/Macau", + }, + { + label: "Asia/Magadan +11:00", + value: "Asia/Magadan", + }, + { + label: "Asia/Makassar +08:00", + value: "Asia/Makassar", + }, + { + label: "Asia/Manila +08:00", + value: "Asia/Manila", + }, + { + label: "Asia/Muscat +04:00", + value: "Asia/Muscat", + }, + { + label: "Asia/Nicosia +02:00", + value: "Asia/Nicosia", + }, + { + label: "Asia/Novokuznetsk +07:00", + value: "Asia/Novokuznetsk", + }, + { + label: "Asia/Novosibirsk +07:00", + value: "Asia/Novosibirsk", + }, + { + label: "Asia/Omsk +06:00", + value: "Asia/Omsk", + }, + { + label: "Asia/Oral +05:00", + value: "Asia/Oral", + }, + { + label: "Asia/Phnom_Penh +07:00", + value: "Asia/Phnom_Penh", + }, + { + label: "Asia/Pontianak +07:00", + value: "Asia/Pontianak", + }, + { + label: "Asia/Pyongyang +09:00", + value: "Asia/Pyongyang", + }, + { + label: "Asia/Qatar +03:00", + value: "Asia/Qatar", + }, + { + label: "Asia/Qostanay +05:00", + value: "Asia/Qostanay", + }, + { + label: "Asia/Qyzylorda +05:00", + value: "Asia/Qyzylorda", + }, + { + label: "Asia/Rangoon +06:30", + value: "Asia/Rangoon", + }, + { + label: "Asia/Riyadh +03:00", + value: "Asia/Riyadh", + }, + { + label: "Asia/Saigon +07:00", + value: "Asia/Saigon", + }, + { + label: "Asia/Sakhalin +11:00", + value: "Asia/Sakhalin", + }, + { + label: "Asia/Samarkand +05:00", + value: "Asia/Samarkand", + }, + { + label: "Asia/Seoul +09:00", + value: "Asia/Seoul", + }, + { + label: "Asia/Shanghai +08:00", + value: "Asia/Shanghai", + }, + { + label: "Asia/Singapore +08:00", + value: "Asia/Singapore", + }, + { + label: "Asia/Srednekolymsk +11:00", + value: "Asia/Srednekolymsk", + }, + { + label: "Asia/Taipei +08:00", + value: "Asia/Taipei", + }, + { + label: "Asia/Tashkent +05:00", + value: "Asia/Tashkent", + }, + { + label: "Asia/Tbilisi +04:00", + value: "Asia/Tbilisi", + }, + { + label: "Asia/Tehran +03:30", + value: "Asia/Tehran", + }, + { + label: "Asia/Tel_Aviv +02:00", + value: "Asia/Tel_Aviv", + }, + { + label: "Asia/Thimbu +06:00", + value: "Asia/Thimbu", + }, + { + label: "Asia/Thimphu +06:00", + value: "Asia/Thimphu", + }, + { + label: "Asia/Tokyo +09:00", + value: "Asia/Tokyo", + }, + { + label: "Asia/Tomsk +07:00", + value: "Asia/Tomsk", + }, + { + label: "Asia/Ujung_Pandang +08:00", + value: "Asia/Ujung_Pandang", + }, + { + label: "Asia/Ulaanbaatar +08:00", + value: "Asia/Ulaanbaatar", + }, + { + label: "Asia/Ulan_Bator +08:00", + value: "Asia/Ulan_Bator", + }, + { + label: "Asia/Urumqi +06:00", + value: "Asia/Urumqi", + }, + { + label: "Asia/Ust-Nera +10:00", + value: "Asia/Ust-Nera", + }, + { + label: "Asia/Vientiane +07:00", + value: "Asia/Vientiane", + }, + { + label: "Asia/Vladivostok +10:00", + value: "Asia/Vladivostok", + }, + { + label: "Asia/Yakutsk +09:00", + value: "Asia/Yakutsk", + }, + { + label: "Asia/Yangon +06:30", + value: "Asia/Yangon", + }, + { + label: "Asia/Yekaterinburg +05:00", + value: "Asia/Yekaterinburg", + }, + { + label: "Asia/Yerevan +04:00", + value: "Asia/Yerevan", + }, + { + label: "Atlantic/Azores -01:00", + value: "Atlantic/Azores", + }, + { + label: "Atlantic/Bermuda -04:00", + value: "Atlantic/Bermuda", + }, + { + label: "Atlantic/Canary +00:00", + value: "Atlantic/Canary", + }, + { + label: "Atlantic/Cape_Verde -01:00", + value: "Atlantic/Cape_Verde", + }, + { + label: "Atlantic/Faeroe +00:00", + value: "Atlantic/Faeroe", + }, + { + label: "Atlantic/Faroe +00:00", + value: "Atlantic/Faroe", + }, + { + label: "Atlantic/Jan_Mayen +01:00", + value: "Atlantic/Jan_Mayen", + }, + { + label: "Atlantic/Madeira +00:00", + value: "Atlantic/Madeira", + }, + { + label: "Atlantic/Reykjavik +00:00", + value: "Atlantic/Reykjavik", + }, + { + label: "Atlantic/South_Georgia -02:00", + value: "Atlantic/South_Georgia", + }, + { + label: "Atlantic/St_Helena +00:00", + value: "Atlantic/St_Helena", + }, + { + label: "Atlantic/Stanley -03:00", + value: "Atlantic/Stanley", + }, + { + label: "Australia/ACT +10:00", + value: "Australia/ACT", + }, + { + label: "Australia/Adelaide +09:30", + value: "Australia/Adelaide", + }, + { + label: "Australia/Brisbane +10:00", + value: "Australia/Brisbane", + }, + { + label: "Australia/Broken_Hill +09:30", + value: "Australia/Broken_Hill", + }, + { + label: "Australia/Canberra +10:00", + value: "Australia/Canberra", + }, + { + label: "Australia/Currie +10:00", + value: "Australia/Currie", + }, + { + label: "Australia/Darwin +09:30", + value: "Australia/Darwin", + }, + { + label: "Australia/Eucla +08:45", + value: "Australia/Eucla", + }, + { + label: "Australia/Hobart +10:00", + value: "Australia/Hobart", + }, + { + label: "Australia/LHI +10:30", + value: "Australia/LHI", + }, + { + label: "Australia/Lindeman +10:00", + value: "Australia/Lindeman", + }, + { + label: "Australia/Lord_Howe +10:30", + value: "Australia/Lord_Howe", + }, + { + label: "Australia/Melbourne +10:00", + value: "Australia/Melbourne", + }, + { + label: "Australia/North +09:30", + value: "Australia/North", + }, + { + label: "Australia/NSW +10:00", + value: "Australia/NSW", + }, + { + label: "Australia/Perth +08:00", + value: "Australia/Perth", + }, + { + label: "Australia/Queensland +10:00", + value: "Australia/Queensland", + }, + { + label: "Australia/South +09:30", + value: "Australia/South", + }, + { + label: "Australia/Sydney +10:00", + value: "Australia/Sydney", + }, + { + label: "Australia/Tasmania +10:00", + value: "Australia/Tasmania", + }, + { + label: "Australia/Victoria +10:00", + value: "Australia/Victoria", + }, + { + label: "Australia/West +08:00", + value: "Australia/West", + }, + { + label: "Australia/Yancowinna +09:30", + value: "Australia/Yancowinna", + }, + { + label: "Brazil/Acre -05:00", + value: "Brazil/Acre", + }, + { + label: "Brazil/DeNoronha -02:00", + value: "Brazil/DeNoronha", + }, + { + label: "Brazil/East -03:00", + value: "Brazil/East", + }, + { + label: "Brazil/West -04:00", + value: "Brazil/West", + }, + { + label: "Canada/Atlantic -04:00", + value: "Canada/Atlantic", + }, + { + label: "Canada/Central -06:00", + value: "Canada/Central", + }, + { + label: "Canada/Eastern -05:00", + value: "Canada/Eastern", + }, + { + label: "Canada/Mountain -07:00", + value: "Canada/Mountain", + }, + { + label: "Canada/Newfoundland -03:30", + value: "Canada/Newfoundland", + }, + { + label: "Canada/Pacific -08:00", + value: "Canada/Pacific", + }, + { + label: "Canada/Saskatchewan -06:00", + value: "Canada/Saskatchewan", + }, + { + label: "Canada/Yukon -07:00", + value: "Canada/Yukon", + }, + { + label: "CET +01:00", + value: "CET", + }, + { + label: "Chile/Continental -04:00", + value: "Chile/Continental", + }, + { + label: "Chile/EasterIsland -06:00", + value: "Chile/EasterIsland", + }, + { + label: "CST6CDT -06:00", + value: "CST6CDT", + }, + { + label: "Cuba -05:00", + value: "Cuba", + }, + { + label: "EET +02:00", + value: "EET", + }, + { + label: "Egypt +02:00", + value: "Egypt", + }, + { + label: "Eire +01:00", + value: "Eire", + }, + { + label: "EST -05:00", + value: "EST", + }, + { + label: "EST5EDT -05:00", + value: "EST5EDT", + }, + { + label: "Etc/GMT +00:00", + value: "Etc/GMT", + }, + { + label: "Etc/GMT-0 +00:00", + value: "Etc/GMT-0", + }, + { + label: "Etc/GMT-1 +01:00", + value: "Etc/GMT-1", + }, + { + label: "Etc/GMT-2 +02:00", + value: "Etc/GMT-2", + }, + { + label: "Etc/GMT-3 +03:00", + value: "Etc/GMT-3", + }, + { + label: "Etc/GMT-4 +04:00", + value: "Etc/GMT-4", + }, + { + label: "Etc/GMT-5 +05:00", + value: "Etc/GMT-5", + }, + { + label: "Etc/GMT-6 +06:00", + value: "Etc/GMT-6", + }, + { + label: "Etc/GMT-7 +07:00", + value: "Etc/GMT-7", + }, + { + label: "Etc/GMT-8 +08:00", + value: "Etc/GMT-8", + }, + { + label: "Etc/GMT-9 +09:00", + value: "Etc/GMT-9", + }, + { + label: "Etc/GMT-10 +10:00", + value: "Etc/GMT-10", + }, + { + label: "Etc/GMT-11 +11:00", + value: "Etc/GMT-11", + }, + { + label: "Etc/GMT-12 +12:00", + value: "Etc/GMT-12", + }, + { + label: "Etc/GMT-13 +13:00", + value: "Etc/GMT-13", + }, + { + label: "Etc/GMT-14 +14:00", + value: "Etc/GMT-14", + }, + { + label: "Etc/GMT+0 +00:00", + value: "Etc/GMT+0", + }, + { + label: "Etc/GMT+1 -01:00", + value: "Etc/GMT+1", + }, + { + label: "Etc/GMT+2 -02:00", + value: "Etc/GMT+2", + }, + { + label: "Etc/GMT+3 -03:00", + value: "Etc/GMT+3", + }, + { + label: "Etc/GMT+4 -04:00", + value: "Etc/GMT+4", + }, + { + label: "Etc/GMT+5 -05:00", + value: "Etc/GMT+5", + }, + { + label: "Etc/GMT+6 -06:00", + value: "Etc/GMT+6", + }, + { + label: "Etc/GMT+7 -07:00", + value: "Etc/GMT+7", + }, + { + label: "Etc/GMT+8 -08:00", + value: "Etc/GMT+8", + }, + { + label: "Etc/GMT+9 -09:00", + value: "Etc/GMT+9", + }, + { + label: "Etc/GMT+10 -10:00", + value: "Etc/GMT+10", + }, + { + label: "Etc/GMT+11 -11:00", + value: "Etc/GMT+11", + }, + { + label: "Etc/GMT+12 -12:00", + value: "Etc/GMT+12", + }, + { + label: "Etc/GMT0 +00:00", + value: "Etc/GMT0", + }, + { + label: "Etc/Greenwich +00:00", + value: "Etc/Greenwich", + }, + { + label: "Etc/UCT +00:00", + value: "Etc/UCT", + }, + { + label: "Etc/Universal +00:00", + value: "Etc/Universal", + }, + { + label: "Etc/UTC +00:00", + value: "Etc/UTC", + }, + { + label: "Etc/Zulu +00:00", + value: "Etc/Zulu", + }, + { + label: "Europe/Amsterdam +01:00", + value: "Europe/Amsterdam", + }, + { + label: "Europe/Andorra +01:00", + value: "Europe/Andorra", + }, + { + label: "Europe/Astrakhan +04:00", + value: "Europe/Astrakhan", + }, + { + label: "Europe/Athens +02:00", + value: "Europe/Athens", + }, + { + label: "Europe/Belfast +00:00", + value: "Europe/Belfast", + }, + { + label: "Europe/Belgrade +01:00", + value: "Europe/Belgrade", + }, + { + label: "Europe/Berlin +01:00", + value: "Europe/Berlin", + }, + { + label: "Europe/Bratislava +01:00", + value: "Europe/Bratislava", + }, + { + label: "Europe/Brussels +01:00", + value: "Europe/Brussels", + }, + { + label: "Europe/Bucharest +02:00", + value: "Europe/Bucharest", + }, + { + label: "Europe/Budapest +01:00", + value: "Europe/Budapest", + }, + { + label: "Europe/Busingen +01:00", + value: "Europe/Busingen", + }, + { + label: "Europe/Chisinau +02:00", + value: "Europe/Chisinau", + }, + { + label: "Europe/Copenhagen +01:00", + value: "Europe/Copenhagen", + }, + { + label: "Europe/Dublin +01:00", + value: "Europe/Dublin", + }, + { + label: "Europe/Gibraltar +01:00", + value: "Europe/Gibraltar", + }, + { + label: "Europe/Guernsey +00:00", + value: "Europe/Guernsey", + }, + { + label: "Europe/Helsinki +02:00", + value: "Europe/Helsinki", + }, + { + label: "Europe/Isle_of_Man +00:00", + value: "Europe/Isle_of_Man", + }, + { + label: "Europe/Istanbul +03:00", + value: "Europe/Istanbul", + }, + { + label: "Europe/Jersey +00:00", + value: "Europe/Jersey", + }, + { + label: "Europe/Kaliningrad +02:00", + value: "Europe/Kaliningrad", + }, + { + label: "Europe/Kiev +02:00", + value: "Europe/Kiev", + }, + { + label: "Europe/Kirov +03:00", + value: "Europe/Kirov", + }, + { + label: "Europe/Kyiv +02:00", + value: "Europe/Kyiv", + }, + { + label: "Europe/Lisbon +00:00", + value: "Europe/Lisbon", + }, + { + label: "Europe/Ljubljana +01:00", + value: "Europe/Ljubljana", + }, + { + label: "Europe/London +00:00", + value: "Europe/London", + }, + { + label: "Europe/Luxembourg +01:00", + value: "Europe/Luxembourg", + }, + { + label: "Europe/Madrid +01:00", + value: "Europe/Madrid", + }, + { + label: "Europe/Malta +01:00", + value: "Europe/Malta", + }, + { + label: "Europe/Mariehamn +02:00", + value: "Europe/Mariehamn", + }, + { + label: "Europe/Minsk +03:00", + value: "Europe/Minsk", + }, + { + label: "Europe/Monaco +01:00", + value: "Europe/Monaco", + }, + { + label: "Europe/Moscow +03:00", + value: "Europe/Moscow", + }, + { + label: "Europe/Nicosia +02:00", + value: "Europe/Nicosia", + }, + { + label: "Europe/Oslo +01:00", + value: "Europe/Oslo", + }, + { + label: "Europe/Paris +01:00", + value: "Europe/Paris", + }, + { + label: "Europe/Podgorica +01:00", + value: "Europe/Podgorica", + }, + { + label: "Europe/Prague +01:00", + value: "Europe/Prague", + }, + { + label: "Europe/Riga +02:00", + value: "Europe/Riga", + }, + { + label: "Europe/Rome +01:00", + value: "Europe/Rome", + }, + { + label: "Europe/Samara +04:00", + value: "Europe/Samara", + }, + { + label: "Europe/San_Marino +01:00", + value: "Europe/San_Marino", + }, + { + label: "Europe/Sarajevo +01:00", + value: "Europe/Sarajevo", + }, + { + label: "Europe/Saratov +04:00", + value: "Europe/Saratov", + }, + { + label: "Europe/Simferopol +03:00", + value: "Europe/Simferopol", + }, + { + label: "Europe/Skopje +01:00", + value: "Europe/Skopje", + }, + { + label: "Europe/Sofia +02:00", + value: "Europe/Sofia", + }, + { + label: "Europe/Stockholm +01:00", + value: "Europe/Stockholm", + }, + { + label: "Europe/Tallinn +02:00", + value: "Europe/Tallinn", + }, + { + label: "Europe/Tirane +01:00", + value: "Europe/Tirane", + }, + { + label: "Europe/Tiraspol +02:00", + value: "Europe/Tiraspol", + }, + { + label: "Europe/Ulyanovsk +04:00", + value: "Europe/Ulyanovsk", + }, + { + label: "Europe/Uzhgorod +02:00", + value: "Europe/Uzhgorod", + }, + { + label: "Europe/Vaduz +01:00", + value: "Europe/Vaduz", + }, + { + label: "Europe/Vatican +01:00", + value: "Europe/Vatican", + }, + { + label: "Europe/Vienna +01:00", + value: "Europe/Vienna", + }, + { + label: "Europe/Vilnius +02:00", + value: "Europe/Vilnius", + }, + { + label: "Europe/Volgograd +03:00", + value: "Europe/Volgograd", + }, + { + label: "Europe/Warsaw +01:00", + value: "Europe/Warsaw", + }, + { + label: "Europe/Zagreb +01:00", + value: "Europe/Zagreb", + }, + { + label: "Europe/Zaporozhye +02:00", + value: "Europe/Zaporozhye", + }, + { + label: "Europe/Zurich +01:00", + value: "Europe/Zurich", + }, + { + label: "Factory +00:00", + value: "Factory", + }, + { + label: "GB +00:00", + value: "GB", + }, + { + label: "GB-Eire +00:00", + value: "GB-Eire", + }, + { + label: "GMT +00:00", + value: "GMT", + }, + { + label: "GMT-0 +00:00", + value: "GMT-0", + }, + { + label: "GMT+0 +00:00", + value: "GMT+0", + }, + { + label: "GMT0 +00:00", + value: "GMT0", + }, + { + label: "Greenwich +00:00", + value: "Greenwich", + }, + { + label: "Hongkong +08:00", + value: "Hongkong", + }, + { + label: "HST -10:00", + value: "HST", + }, + { + label: "Iceland +00:00", + value: "Iceland", + }, + { + label: "Indian/Antananarivo +03:00", + value: "Indian/Antananarivo", + }, + { + label: "Indian/Chagos +06:00", + value: "Indian/Chagos", + }, + { + label: "Indian/Christmas +07:00", + value: "Indian/Christmas", + }, + { + label: "Indian/Cocos +06:30", + value: "Indian/Cocos", + }, + { + label: "Indian/Comoro +03:00", + value: "Indian/Comoro", + }, + { + label: "Indian/Kerguelen +05:00", + value: "Indian/Kerguelen", + }, + { + label: "Indian/Mahe +04:00", + value: "Indian/Mahe", + }, + { + label: "Indian/Maldives +05:00", + value: "Indian/Maldives", + }, + { + label: "Indian/Mauritius +04:00", + value: "Indian/Mauritius", + }, + { + label: "Indian/Mayotte +03:00", + value: "Indian/Mayotte", + }, + { + label: "Indian/Reunion +04:00", + value: "Indian/Reunion", + }, + { + label: "Iran +03:30", + value: "Iran", + }, + { + label: "Israel +02:00", + value: "Israel", + }, + { + label: "Jamaica -05:00", + value: "Jamaica", + }, + { + label: "Japan +09:00", + value: "Japan", + }, + { + label: "Kwajalein +12:00", + value: "Kwajalein", + }, + { + label: "Libya +02:00", + value: "Libya", + }, + { + label: "MET +01:00", + value: "MET", + }, + { + label: "Mexico/BajaNorte -08:00", + value: "Mexico/BajaNorte", + }, + { + label: "Mexico/BajaSur -07:00", + value: "Mexico/BajaSur", + }, + { + label: "Mexico/General -06:00", + value: "Mexico/General", + }, + { + label: "MST -07:00", + value: "MST", + }, + { + label: "MST7MDT -07:00", + value: "MST7MDT", + }, + { + label: "Navajo -07:00", + value: "Navajo", + }, + { + label: "NZ +12:00", + value: "NZ", + }, + { + label: "NZ-CHAT +12:45", + value: "NZ-CHAT", + }, + { + label: "Pacific/Apia +13:00", + value: "Pacific/Apia", + }, + { + label: "Pacific/Auckland +12:00", + value: "Pacific/Auckland", + }, + { + label: "Pacific/Bougainville +11:00", + value: "Pacific/Bougainville", + }, + { + label: "Pacific/Chatham +12:45", + value: "Pacific/Chatham", + }, + { + label: "Pacific/Chuuk +10:00", + value: "Pacific/Chuuk", + }, + { + label: "Pacific/Easter -06:00", + value: "Pacific/Easter", + }, + { + label: "Pacific/Efate +11:00", + value: "Pacific/Efate", + }, + { + label: "Pacific/Enderbury +13:00", + value: "Pacific/Enderbury", + }, + { + label: "Pacific/Fakaofo +13:00", + value: "Pacific/Fakaofo", + }, + { + label: "Pacific/Fiji +12:00", + value: "Pacific/Fiji", + }, + { + label: "Pacific/Funafuti +12:00", + value: "Pacific/Funafuti", + }, + { + label: "Pacific/Galapagos -06:00", + value: "Pacific/Galapagos", + }, + { + label: "Pacific/Gambier -09:00", + value: "Pacific/Gambier", + }, + { + label: "Pacific/Guadalcanal +11:00", + value: "Pacific/Guadalcanal", + }, + { + label: "Pacific/Guam +10:00", + value: "Pacific/Guam", + }, + { + label: "Pacific/Honolulu -10:00", + value: "Pacific/Honolulu", + }, + { + label: "Pacific/Johnston -10:00", + value: "Pacific/Johnston", + }, + { + label: "Pacific/Kanton +13:00", + value: "Pacific/Kanton", + }, + { + label: "Pacific/Kiritimati +14:00", + value: "Pacific/Kiritimati", + }, + { + label: "Pacific/Kosrae +11:00", + value: "Pacific/Kosrae", + }, + { + label: "Pacific/Kwajalein +12:00", + value: "Pacific/Kwajalein", + }, + { + label: "Pacific/Majuro +12:00", + value: "Pacific/Majuro", + }, + { + label: "Pacific/Marquesas -09:30", + value: "Pacific/Marquesas", + }, + { + label: "Pacific/Midway -11:00", + value: "Pacific/Midway", + }, + { + label: "Pacific/Nauru +12:00", + value: "Pacific/Nauru", + }, + { + label: "Pacific/Niue -11:00", + value: "Pacific/Niue", + }, + { + label: "Pacific/Norfolk +11:00", + value: "Pacific/Norfolk", + }, + { + label: "Pacific/Noumea +11:00", + value: "Pacific/Noumea", + }, + { + label: "Pacific/Pago_Pago -11:00", + value: "Pacific/Pago_Pago", + }, + { + label: "Pacific/Palau +09:00", + value: "Pacific/Palau", + }, + { + label: "Pacific/Pitcairn -08:00", + value: "Pacific/Pitcairn", + }, + { + label: "Pacific/Pohnpei +11:00", + value: "Pacific/Pohnpei", + }, + { + label: "Pacific/Ponape +11:00", + value: "Pacific/Ponape", + }, + { + label: "Pacific/Port_Moresby +10:00", + value: "Pacific/Port_Moresby", + }, + { + label: "Pacific/Rarotonga -10:00", + value: "Pacific/Rarotonga", + }, + { + label: "Pacific/Saipan +10:00", + value: "Pacific/Saipan", + }, + { + label: "Pacific/Samoa -11:00", + value: "Pacific/Samoa", + }, + { + label: "Pacific/Tahiti -10:00", + value: "Pacific/Tahiti", + }, + { + label: "Pacific/Tarawa +12:00", + value: "Pacific/Tarawa", + }, + { + label: "Pacific/Tongatapu +13:00", + value: "Pacific/Tongatapu", + }, + { + label: "Pacific/Truk +10:00", + value: "Pacific/Truk", + }, + { + label: "Pacific/Wake +12:00", + value: "Pacific/Wake", + }, + { + label: "Pacific/Wallis +12:00", + value: "Pacific/Wallis", + }, + { + label: "Pacific/Yap +10:00", + value: "Pacific/Yap", + }, + { + label: "Poland +01:00", + value: "Poland", + }, + { + label: "Portugal +00:00", + value: "Portugal", + }, + { + label: "PRC +08:00", + value: "PRC", + }, + { + label: "PST8PDT -08:00", + value: "PST8PDT", + }, + { + label: "ROC +08:00", + value: "ROC", + }, + { + label: "ROK +09:00", + value: "ROK", + }, + { + label: "Singapore +08:00", + value: "Singapore", + }, + { + label: "Turkey +03:00", + value: "Turkey", + }, + { + label: "UCT +00:00", + value: "UCT", + }, + { + label: "Universal +00:00", + value: "Universal", + }, + { + label: "US/Alaska -09:00", + value: "US/Alaska", + }, + { + label: "US/Aleutian -10:00", + value: "US/Aleutian", + }, + { + label: "US/Arizona -07:00", + value: "US/Arizona", + }, + { + label: "US/Central -06:00", + value: "US/Central", + }, + { + label: "US/East-Indiana -05:00", + value: "US/East-Indiana", + }, + { + label: "US/Eastern -05:00", + value: "US/Eastern", + }, + { + label: "US/Hawaii -10:00", + value: "US/Hawaii", + }, + { + label: "US/Indiana-Starke -06:00", + value: "US/Indiana-Starke", + }, + { + label: "US/Michigan -05:00", + value: "US/Michigan", + }, + { + label: "US/Mountain -07:00", + value: "US/Mountain", + }, + { + label: "US/Pacific -08:00", + value: "US/Pacific", + }, + { + label: "US/Samoa -11:00", + value: "US/Samoa", + }, + { + label: "UTC +00:00", + value: "UTC", + }, + { + label: "W-SU +03:00", + value: "W-SU", + }, + { + label: "WET +00:00", + value: "WET", + }, + { + label: "Zulu +00:00", + value: "Zulu", + }, +]; diff --git a/components/vivomeetings/common/utils.mjs b/components/vivomeetings/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/vivomeetings/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/vivomeetings/package.json b/components/vivomeetings/package.json new file mode 100644 index 0000000000000..7ee93acffe476 --- /dev/null +++ b/components/vivomeetings/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/vivomeetings", + "version": "0.1.0", + "description": "Pipedream Vivomeetings Components", + "main": "vivomeetings.app.mjs", + "keywords": [ + "pipedream", + "vivomeetings" + ], + "homepage": "https://pipedream.com/apps/vivomeetings", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/vivomeetings/sources/new-meeting/new-meeting.mjs b/components/vivomeetings/sources/new-meeting/new-meeting.mjs new file mode 100644 index 0000000000000..58ff2e9d315b3 --- /dev/null +++ b/components/vivomeetings/sources/new-meeting/new-meeting.mjs @@ -0,0 +1,69 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import vivomeetings from "../../vivomeetings.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "vivomeetings-new-meeting", + name: "New VivoMeeting or Webinar Created", + description: "Emit new event when a new VivoMeeting or webinar is created. [See the documentation](https://vivomeetings.com/api-developer-guide/)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + vivomeetings, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + hostId: { + propDefinition: [ + vivomeetings, + "hostId", + ], + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + async emitEvent(maxResults = false) { + const lastId = this._getLastId(); + const response = await this.vivomeetings.listConferences({ + data: { + host_id: this.hostId, + }, + }); + + const filteredArray = response.filter((item) => item.conference_id > lastId).reverse(); + if (filteredArray.length) { + if (maxResults && (filteredArray.length > maxResults)) { + filteredArray.length = maxResults; + } + this._setLastId(filteredArray[0].conference_id); + } + + for (const item of filteredArray.reverse()) { + this.$emit(item, { + id: item.conference_id, + summary: `New Meeting: ${item.subject}`, + ts: Date.parse(new Date()), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, + sampleEmit, +}; diff --git a/components/vivomeetings/sources/new-meeting/test-event.mjs b/components/vivomeetings/sources/new-meeting/test-event.mjs new file mode 100644 index 0000000000000..638738f8000ed --- /dev/null +++ b/components/vivomeetings/sources/new-meeting/test-event.mjs @@ -0,0 +1,22 @@ +export default { + "conference_id": 183, + "subject": "Test Conference", + "agenda": "An agenda", + "start": "2020-09-19 13:00:00", + "end_time": "2020-09-19 14:00:00", + "active": false, + "time_zone": "Europe/London", + "duration": 60, + "auto_record": "none", + "auto_transcribe": false, + "one_time_access_code": 987654321, + "security_pin": "123456", + "mute_mode": "conversation", + "room_url": + "https://subdomain.domain.tld/conf/call/987654321", + "recordings": [], + "participants": [ 1, 2, 3 ], + "moderators": [ 1 ], + "is_streaming": false, + "moderator_token": "cnzrvyEeJp3H" +} \ No newline at end of file diff --git a/components/vivomeetings/vivomeetings.app.mjs b/components/vivomeetings/vivomeetings.app.mjs new file mode 100644 index 0000000000000..6e38cc2bcb3e5 --- /dev/null +++ b/components/vivomeetings/vivomeetings.app.mjs @@ -0,0 +1,216 @@ +import { axios } from "@pipedream/platform"; +import { + AUTO_RECORD_OPTIONS, + MUTE_MODE_OPTIONS, + TIMEZONE_OPTIONS, +} from "./common/constants.mjs"; + +export default { + type: "app", + app: "vivomeetings", + propDefinitions: { + conferenceId: { + type: "string", + label: "Conference ID", + description: "ID of the conference.", + async options({ hostId }) { + const data = await this.listConferences({ + data: { + host_id: hostId, + }, + }); + + return data.map(({ + conference_id: value, subject: label, + }) => ({ + label, + value, + })); + }, + }, + companyId: { + type: "string", + label: "Company Id", + description: "The Id of the company.", + }, + hostId: { + type: "string", + label: "Host Id", + description: "The Id of the host.", + async options({ companyId }) { + const data = await this.listHosts({ + data: { + companyId, + }, + }); + + return data.map(({ + host_id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + contactIds: { + type: "string[]", + label: "Contact Ids", + description: "The unique identification of the contacts.", + async options({ hostId }) { + const data = await this.listContacts({ + data: { + host_id: hostId, + }, + }); + + return data.map(({ + contact_id: value, name, email, + }) => ({ + label: `${name || email}`, + value, + })); + }, + }, + subject: { + type: "string", + label: "Subject", + description: "The subject of the meeting.", + }, + agenda: { + type: "string", + label: "Agenda", + description: "The description of the meeting.", + }, + start: { + type: "string", + label: "Start", + description: "Start time, this works in conjunction with the time_zone to set the correct time.", + }, + timeZone: { + type: "string", + label: "Time Zone", + description: "Time zone associated with the start time. In the IANA Time Zone database (https://www.iana.org/time-zones)", + options: TIMEZONE_OPTIONS, + }, + duration: { + type: "integer", + label: "Duration", + description: "Duration in minutes.", + }, + autoRecord: { + type: "string", + label: "Auto Record", + description: "Indicates whether the conference should be recorded automatically and the format. If omitted will default to none. Both record and stream must be the same value if recording and streaming together.", + options: AUTO_RECORD_OPTIONS, + optional: true, + }, + autoStream: { + type: "string", + label: "Auto Stream", + description: "Indicates whether the conference should be streamed automatically and the format. If omitted will default to none. Both record and stream must be the same value if recording and streaming together.", + options: AUTO_RECORD_OPTIONS, + optional: true, + }, + autoTranscribe: { + type: "boolean", + label: "Auto Transcribe", + description: "Whether the recording should automatically be transcribed. Valid values are true or false. If omitted will default to false.", + optional: true, + }, + oneTimeAccessCode: { + type: "boolean", + label: "One Time Access Code", + description: "When set to true an access code will be returned in the response.", + }, + secureUrl: { + type: "boolean", + label: "Secure URL", + description: "When set to true the room_url will contain an encrypted access code. Best when used with one_time_access_code.", + optional: true, + }, + hostInitiatedRecording: { + type: "boolean", + label: "Host Initiated Recording", + description: "Providing this parameter will change the trigger for automatically starting recording or streaming to also include the host joining the call.", + optional: true, + }, + securityPin: { + type: "string", + label: "Security Pin", + description: "Pin required to access the conference.", + }, + muteMode: { + type: "string", + label: "Mute Mode", + description: "Sets the initial participant mute settings.", + options: MUTE_MODE_OPTIONS, + }, + participantEmails: { + type: "string[]", + label: "Participant Emails", + description: "Alternatively participants can be listed via emails with moderators specified. If a participant’s emails is not on the host’s contact list, it will be added.", + optional: true, + }, + }, + methods: { + _baseUrl() { + return `${this.$auth.url}`; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path = "", ...opts + }) { + return axios($, { + ...opts, + url: this._baseUrl() + path, + headers: this._headers(), + }); + }, + listHosts(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/enterprise_api/host/fetch_all", + ...opts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/enterprise_api/contact/fetch_all", + ...opts, + }); + }, + listConferences(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/enterprise_api/conference/fetch_all", + ...opts, + }); + }, + createConference(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/enterprise_api/conference/create", + ...opts, + }); + }, + updateConference(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/enterprise_api/conference/update", + ...opts, + }); + }, + getConferenceDetails(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/enterprise_api/conference/fetch", + ...opts, + }); + }, + }, +}; diff --git a/components/vk/README.md b/components/vk/README.md index e9a273e5c6f54..382d11147891c 100644 --- a/components/vk/README.md +++ b/components/vk/README.md @@ -1,29 +1,11 @@ # Overview -VK, or formerly known as Vkontakte, is a popular Russian-based social -networking website that offers an expansive set of web APIs for developers. -Using the VK API, developers can create a number of applications and services -that can interact with VK users and resources. With the VK APIs, developers -can create a variety of services ranging from traditional social networking -features to more complex applications and games. +The VK API lets you tap into the rich functionality of VKontakte, Russia's largest social network. With Pipedream, you can automate interactions with VK's platform, such as posting content, managing communities, and fetching user data. This enables you to create custom social media dashboards, automate posting schedules, or integrate VK actions into your broader app ecosystem. Pipedream's serverless platform simplifies these tasks with event-driven workflows, allowing you to focus on building powerful applications without managing servers. -Here are some of the things that can be built with the VK API: +# Example Use Cases -- Social Networking Platforms: Enable users to make connections, send messages, - share media, and create and manage groups and events. -- Music and Video Streaming Platforms: Create streaming music and video - platforms with support for streaming popular audio and video content from VK. -- Games: Implement fully featured game applications that can be integrated with - VK for leaderboard categories, gaming communities, and more. -- Chatbots: Create their own personal chatbot for VK users, allowing them to - communicate with other VK users in an automated manner. -- E-commerce: Use the VK APIs to create online marketplaces and e-commerce - stores with VK integration. -- Location-based Services: Develop applications that can be used to locate and - share events, locations and points of interest with VK users. -- Analytics and Tracking: Collect data and implement analytical tools for VK - users and businesses. -- Advertising and Promotion: Develop campaigns and promotional activities with - VK integration. -- Customization: Enable users to customize their VK experience with a variety - of methods, including custom skins, page layouts, and more. +- **Automated Community Management**: Automatically approve new group members, post scheduled content, and track community engagement in VK groups. By integrating with Slack via Pipedream, instantly relay member inquiries to a customer service channel for a swift response. + +- **Content Distribution Network**: Sync your VK posts with other social media platforms like Twitter or Facebook for a streamlined cross-posting experience. Use Pipedream to listen for new VK posts and automatically share them on other platforms, expanding your content's reach with minimal effort. + +- **User Behavior Analysis**: Leverage Pipedream to collect and aggregate user interactions with your VK posts or ads. Send this data to Google Sheets or your preferred analytics tool for detailed analysis and insights, enabling data-driven decision-making for your marketing strategy. diff --git a/components/voice/README.md b/components/voice/README.md index 58e06f96812ab..eb6e76fd16316 100644 --- a/components/voice/README.md +++ b/components/voice/README.md @@ -1,38 +1,11 @@ # Overview -Voiceflow is an API designed to help you quickly develop and deploy -conversational and voice interface applications to popular platforms like -Amazon Alexa and Google Home. With the Voiceflow API, you can build a wide -variety of applications such as voice-driven virtual assistants, voice shopping -and ordering systems, voice-driven games, AI-driven digital advertisement -platforms, and more. +The Voiceflow API provides programmatic access to Voiceflow's conversational AI platform, enabling users to create, update, and deploy voice and chat applications. Through Pipedream, developers can automate workflows involving Voiceflow projects, such as syncing conversation designs with external tools, triggering customer support messages based on user interactions, or compiling analytics from user sessions. -Here are some examples of what you can build with Voice Flow: +# Example Use Cases -- Virtual Assistants: Create applications that provide a virtual assistant - service to answer customer questions, offer suggestions about products and - services, provide conversational AI for customer service, and more. -- Voice Shopping and Ordering System: Build applications to let users search - and purchase products through voice commands. -- Voice-driven Games: Build games that are fun and engaging for users to play - through their voice commands. -- AI-driven Digital Advertisement Platforms: Create applications that leverage - AI and natural language processing technologies to offer personalized - advertisements and content recommendations tailored to individual users. -- Multi-Channel Applications: Build applications with the ability to work on - multiple voice platforms, such as Amazon Alexa and Google Home, as well as - other non-voice interfaces like web, mobile, or chatbot. -- Smart Home Platforms: Create applications that allow users to control their - smart home devices through their voice commands. -- Voice Conversational Agents: Create conversational agents that can understand - and respond to customer queries with natural language processing. -- Voice-Enabled Customer Service: Create customer service applications that - allow users to find answers to their questions, probe for more information, - adjust settings, offer customer interaction, and more. -- Speech Recognition Systems: Build applications that utilize speech - recognition technologies to transcribe user speech into text and output an - appropriate response. +- **Automated Syncing of Voiceflow Projects**: Integrate GitHub with Voiceflow using Pipedream to automatically commit changes in your Voiceflow projects to a repository. Each time a project is updated in Voiceflow, Pipedream triggers a workflow that pushes the latest version to GitHub, ensuring version control and backup for your conversational apps. -There’s so much more you can do with the Voiceflow API. With the right -combination of features, you can create a powerful and engaging voice interface -with amazing potential. +- **User Interaction Triggered Support Tickets**: Connect Voiceflow with Zendesk via Pipedream. When a user encounters an issue or requests help in a Voiceflow-powered chatbot, Pipedream captures the event and creates a support ticket in Zendesk with the user's query and contact details, streamlining customer support follow-ups. + +- **Voiceflow Analytics to Google Sheets**: Use Pipedream to send session data from Voiceflow to Google Sheets. After a conversation ends in a Voiceflow application, Pipedream can automatically extract the session details and append them to a Google Sheet, providing easy-to-analyze data for improving user experiences. diff --git a/components/voice/voice.app.mjs b/components/voice/voice.app.mjs new file mode 100644 index 0000000000000..97fd6f5ebd420 --- /dev/null +++ b/components/voice/voice.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "voice", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/voice_monkey/README.md b/components/voice_monkey/README.md index 30b5aa07006cb..f6f48f8c0dbec 100644 --- a/components/voice_monkey/README.md +++ b/components/voice_monkey/README.md @@ -1,25 +1,9 @@ # Overview -Voice Monkey's Custom API allows you to build voice applications to extend your -business. Here are some examples of what you can create: +The Voice Monkey API provides a bridge between Alexa and Pipedream, enabling you to send custom announcements, notifications, or commands to your Alexa devices. With this API, you can trigger Alexa to speak a custom message or execute routines, leveraging the power of voice interaction in your automated workflows. It's particularly useful for smart home enthusiasts, productivity hackers, and businesses looking to integrate voice notifications into their services. -- Virtual Assistants: Create a virtual assistant that can answer customer - queries, provide relevant information and services, or even be a personal - helper. -- Home Automation: Transform your home with Voice Monkey's Custom API by adding - voice control to lights, security systems, and other devices. -- Interactive Voice Response (IVR): Streamline customer service with an - automated system, allowing customers to interact with your business through - voice commands. -- Voice User Interfaces: Build interactive applications with natural language - understanding and cognitive capabilities. -- Smart Home Applications: Develop tools to monitor, control and automate your - home. -- Smart Robotic Applications: Create robots that respond to voice commands and - operate autonomously with the help of Voice Monkey's Custom API. -- Conversational Agents: Create AI-powered chatbots that can interact with - customers and provide personalized information or services. -- Content Aggregators: Pull content from multiple sources and create a unified, - natural language interface. -- Voice-controlled Games: Develop games powered by the Voice Monkey's Custom - API, allowing users to interact using voice commands. +# Example Use Cases + +- **Alexa Morning Briefing Automation:** Kickstart your day with an automated morning briefing. When your calendar shows a scheduled event, Pipedream can trigger the Voice Monkey API to have Alexa read out the day's agenda, weather forecast, and important news. +- **Smart Home Alert System:** Enhance your home security with voice alerts. Connect smart sensors to Pipedream, so when a sensor detects movement or opens a door, it prompts Alexa to announce a security alert through the Voice Monkey API. +- **Customer Support Notifications:** Integrate the Voice Monkey API with a customer support ticketing system like Zendesk. Whenever a high-priority support ticket is received, use Pipedream to trigger an announcement from Alexa to ensure immediate attention from your support team. diff --git a/components/voilanorbert/README.md b/components/voilanorbert/README.md index 8ed49dcd91377..855b804375ece 100644 --- a/components/voilanorbert/README.md +++ b/components/voilanorbert/README.md @@ -1,20 +1,11 @@ # Overview -VoilaNorbert is the easiest way to find the most accurate email addresses and -contact info in a few seconds. Thanks to its powerful API, you can quickly -access information and create custom applications to help you get the data you -need and make the best use of that information. +VoilaNorbert is a potent tool for finding and verifying email addresses. With its API, you can automate processes like enriching leads with verified contact information, streamlining your email outreach, and maintaining the health of your email lists. The API's main features include searching for email addresses based on names and domains, verifying email deliverability, and managing your contacts. When used within Pipedream, you can leverage these capabilities to create workflows that respond to events, integrate with other services, and process data in real time. -The VoilaNorbert API can help you with many tasks, including: +# Example Use Cases -- Building contact pages and customer lists -- Generating personalized lead reports -- Creating targeted campaigns for customers and prospects -- Improving customer segmentation and list management -- Validating customer contact information -- Enhancing customer data for use in marketing -- Automating lead verification processes -- Integrating email address data into customer relationship management (CRM) - systems -- Identifying customer contacts from web interactions -- Enhancing existing customer contact databases with additional details +- **Lead Enrichment Workflow**: Triggered by a new entry on a Google Sheet (containing names and companies), this workflow uses VoilaNorbert to find corresponding email addresses, verifies them to ensure deliverability, and then updates the Google Sheet with the new data. This automation keeps your lead database fresh and actionable. + +- **Email Verification on Signup**: When a new user signs up through a website form, VoilaNorbert is employed to verify the email address provided. If verified, the user's information is added to a CRM like Salesforce, and a welcome email is sent via SendGrid. This process ensures that you're capturing genuine leads without manual vetting. + +- **Periodic Email List Cleaning**: A scheduled workflow runs monthly, fetching email addresses from an email marketing platform like Mailchimp. It uses VoilaNorbert to verify and clean the list, removing invalid emails automatically. The cleaned list is then re-uploaded to Mailchimp, optimizing your email campaign's deliverability and engagement rates. diff --git a/components/vonage/package.json b/components/vonage/package.json new file mode 100644 index 0000000000000..5ded9a310e270 --- /dev/null +++ b/components/vonage/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/vonage", + "version": "0.0.1", + "description": "Pipedream Vonage Components", + "main": "vonage.app.mjs", + "keywords": [ + "pipedream", + "vonage" + ], + "homepage": "https://pipedream.com/apps/vonage", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/vonage/vonage.app.mjs b/components/vonage/vonage.app.mjs new file mode 100644 index 0000000000000..9fc3d18a42fa2 --- /dev/null +++ b/components/vonage/vonage.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "vonage", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/voodoo_sms/README.md b/components/voodoo_sms/README.md index d5bda6eb99e3b..33b458f93b336 100644 --- a/components/voodoo_sms/README.md +++ b/components/voodoo_sms/README.md @@ -1,27 +1,11 @@ # Overview -The Voodoo SMS API provides developers with an awesome way to unleash their -creativity. Whether you're looking to create powerful marketing campaigns, -payment notifications, or customer engagement initiatives, the Voodoo SMS API -can help you get the job done. +Voodoo SMS is a powerful gateway API that enables you to send and receive SMS messages programmatically. When integrated with Pipedream, Voodoo SMS becomes a part of diverse automation workflows to engage customers, confirm transactions, and provide timely notifications. By leveraging Pipedream's platform, you can connect Voodoo SMS to a vast array of APIs, services, and apps, creating custom, event-driven workflows that streamline communication processes. -Here are some examples of what you can build using the Voodoo SMS API: +# Example Use Cases -- An SMS notification system to alert customers of upcoming discounts, special - offers, and product launches -- Payment reminders sent to customers via SMS -- A bulk SMS sending system to reach customers with timely and accessible - notifications -- A two-way SMS chatbot to provide a seamless customer service experience -- An SMS order confirmation system to inform customers of their purchases -- Surveys and customer feedback via SMS -- An automated SMS marketing campaign to send promotional offers -- An event reminder system to send reminders about upcoming events -- A two-way SMS conversation system to provide a better customer experience -- An automated customer service system to handle customer questions and - inquiries -- An SMS coupon delivery system to send promotional codes and discounts -- A welcome SMS system to greet new customers with engaging messages -- A text-to-voice system to allow customers to interact with your application - via SMS -- An SMS advertising system to reach customers with targeted ads +- **Customer Support Ticket Alert System**: Integrate Voodoo SMS with a support ticketing system like Zendesk. When a new high-priority ticket is created, trigger a Pipedream workflow to send an SMS notification to the support manager. This ensures immediate awareness and timely responses to critical customer issues. + +- **E-Commerce Order Confirmation and Updates**: Combine Voodoo SMS with an e-commerce platform like Shopify. Set up a Pipedream workflow to send an SMS confirmation when a customer places an order, and follow-up messages for order dispatch and delivery status. This workflow keeps customers informed and enhances their shopping experience. + +- **Appointment Reminder Service**: Link Voodoo SMS to Google Calendar via Pipedream. Whenever a new appointment is scheduled, trigger a workflow that sends a text reminder to the participant a day before the event. This service helps reduce no-shows and improves participation rates. diff --git a/components/voodoo_sms/package.json b/components/voodoo_sms/package.json new file mode 100644 index 0000000000000..8be6350d0c582 --- /dev/null +++ b/components/voodoo_sms/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/voodoo_sms", + "version": "0.6.0", + "description": "Pipedream voodoo_sms Components", + "main": "voodoo_sms.app.mjs", + "keywords": [ + "pipedream", + "voodoo_sms" + ], + "homepage": "https://pipedream.com/apps/voodoo_sms", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/vosfactures/README.md b/components/vosfactures/README.md index 0b26e50345c73..ebb9bad0dd038 100644 --- a/components/vosfactures/README.md +++ b/components/vosfactures/README.md @@ -1,19 +1,11 @@ # Overview -The Vosfactures API is a powerful tool for businesses to take care of their -accounting operations. With this API, businesses can perform a wide range of -accounting operations such as creating, editing and sending invoices, managing -bills, producing reports and other financial data, and more. +The Vosfactures API offers robust features for managing invoices, quotes, and orders within your business. With Pipedream, you can automate workflows that leverage these capabilities, streamlining your accounting processes and integrating them with other apps. From auto-generating invoices based on specific triggers to syncing your financials with accounting software or CRM systems, the opportunities for automation are vast. -Using the Vosfactures API, businesses can easily build robust accounting -solutions tailored to their organization's specific needs. Here are some -examples of what businesses can build using the Vosfactures API: +# Example Use Cases -- Invoice Processing – Easily create, manage, and send invoices to customers. -- Expense Reports – Automatically generate and track expense reports. -- Reporting – Generate financial reports for reporting purposes. -- Billing Management – Automatically create and manage billing entries. -- Accounting System Integration – Integrate with other accounting systems for - streamlined accounting operation. -- Payment Processing – Process payment from customers and manage accounts - receivable. +- **Automated Invoice Generation from E-commerce Sales**: When a new sale is processed on an e-commerce platform like Shopify, Pipedream can trigger a workflow that creates an invoice in Vosfactures. This ensures that every sale is instantly reflected in your accounting. + +- **Scheduled Financial Reporting**: Set up a Pipedream workflow to retrieve financial data from Vosfactures at regular intervals. This data can be compiled into reports and sent to stakeholders via email, Slack, or other communication platforms, keeping everyone informed without manual effort. + +- **CRM Integration for Client Onboarding**: On the creation of a new contact in a CRM like HubSpot, a Pipedream workflow can automatically create a client profile within Vosfactures. This keeps client data in sync and streamlines the onboarding process for new business. diff --git a/components/vryno/README.md b/components/vryno/README.md new file mode 100644 index 0000000000000..640bdad2b17b2 --- /dev/null +++ b/components/vryno/README.md @@ -0,0 +1,11 @@ +# Overview + +The Vryno API provides functionality to access a platform that specializes in various services including but not limited to real estate, healthcare, and educational services. By integrating this API into Pipedream workflows, developers can automate interactions and data exchanges between Vryno and other platforms, enhancing efficiency and enabling innovative service delivery models. + +# Example Use Cases + +- **Automated Property Listing Sync**: Automatically sync property listings from Vryno to real estate platforms like Zillow or Realtor.com. Every time a new listing is added on Vryno, the workflow triggers an event in Pipedream that posts these listings to other platforms, keeping all listings up-to-date across multiple channels. + +- **Appointment Scheduling Integration**: Connect Vryno's healthcare appointment scheduling features with Google Calendar. When a new appointment is scheduled in Vryno, a Pipedream workflow automatically creates a corresponding event in the doctor's Google Calendar, ensuring they never miss an appointment and optimize their daily schedule. + +- **Automated Student Enrollment Notifications**: When a new student enrolls in a course via Vryno, use Pipedream to trigger an email notification through services like SendGrid or Mailgun. This workflow could also update a Slack channel used by educational administrators, keeping everyone informed in real time about new enrollments. diff --git a/components/vryno/actions/create-unique-lead/create-unique-lead.mjs b/components/vryno/actions/create-unique-lead/create-unique-lead.mjs new file mode 100644 index 0000000000000..d1dc5499980df --- /dev/null +++ b/components/vryno/actions/create-unique-lead/create-unique-lead.mjs @@ -0,0 +1,221 @@ +import { ConfigurationError } from "@pipedream/platform"; +import vryno from "../../vryno.app.mjs"; + +export default { + key: "vryno-create-unique-lead", + name: "Create Unique Lead", + description: "Creates a unique lead in the Vryno system, ensuring no duplication of lead details. [See the documentation](https://vrynotest.ti2.in/docs/api-documentation/how-to-create-a-record-in-any-module-in-vryno-crm/)", + version: "0.0.1", + type: "action", + props: { + vryno, + firstName: { + type: "string", + label: "First Name", + description: "The lead's first name.", + optional: true, + }, + name: { + type: "string", + label: "Last Name", + description: "The lead's last name.", + }, + email: { + type: "string", + label: "Email", + description: "The lead's email.", + optional: true, + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The lead's phone number.", + optional: true, + }, + company: { + type: "string", + label: "Company", + description: "The company the lead works for.", + optional: true, + }, + website: { + type: "string", + label: "Website", + description: "The lead's website.", + optional: true, + }, + ownerId: { + type: "string", + label: "Owner Id", + description: "The user Id related to the lead.You can find the user IDs in your account -> settings -> users & controls -> users, click on some user and the ID will be in URL.", + }, + score: { + type: "integer", + label: "Score", + description: "The lead's score.", + optional: true, + }, + expectedRevenue: { + type: "integer", + label: "Expected Revenue", + description: "Expected revenue for the lead.", + optional: true, + }, + numberOfEmployees: { + type: "integer", + label: "Number Of Employees", + description: "Number of employees at the lead company.", + optional: true, + }, + billingAddress: { + type: "string", + label: "Billing Address", + description: "The lead's billing address.", + optional: true, + }, + billingCity: { + type: "string", + label: "Billing City", + description: "The lead's billing city.", + optional: true, + }, + billingState: { + type: "string", + label: "Billing State", + description: "The lead's billing state.", + optional: true, + }, + billingCountry: { + type: "string", + label: "Billing Country", + description: "The lead's billing country.", + optional: true, + }, + billingZipcode: { + type: "string", + label: "Billing Zipcode", + description: "The lead's billing zipcode", + optional: true, + }, + shippingAddress: { + type: "string", + label: "Shipping Address", + description: "The lead's shipping address.", + optional: true, + }, + shippingCity: { + type: "string", + label: "Shipping City", + description: "The lead's shipping city.", + optional: true, + }, + shippingState: { + type: "string", + label: "Shipping State", + description: "The lead's shipping state.", + optional: true, + }, + shippingCountry: { + type: "string", + label: "Shipping Country", + description: "The lead's shipping country.", + optional: true, + }, + shippingZipcode: { + type: "string", + label: "Shipping Zipcode", + description: "The lead's shipping zipcode", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "A brief description about the lead.", + optional: true, + }, + }, + async run({ $ }) { + if (!this.email && !this.phoneNumber) { + throw new ConfigurationError("You must provide at least either **Email** or **Phone Number**."); + } + const duplicateCheck = await this.vryno.post({ + data: { + query: `query { + fetchLead(filters:[${this.email + ? `{name: "email", operator:"eq",value:["${this.email}"]},` + : ""}${this.phoneNumber + ? `{name: "phoneNumber", operator:"eq",value:["${this.phoneNumber}"]}` + : ""}], + expression:"( ( a ) and b)"){ + code + status + message + messageKey + count + data { + id + } + } + }`, + }, + }); + + if (duplicateCheck.data?.fetchLead?.data?.length) { + $.export("$summary", "A lead with the same email and phone number already exists."); + return duplicateCheck.data; + } + + const { + vryno, + ...data + } = this; + + let query = `mutation { + createLead(input: { + `; + + for (const [ + field, + value, + ] of Object.entries(data)) { + query += `${field}:`; + if ([ + "score", + "expectedRevenue", + "numberOfEmployees", + ].includes(field)) { + query += ` ${value} + `; + } else { + query += ` "${value}" + `; + } + } + + query += `}) { + code + message + status + messageKey + data { + id + } + errors + } + }`; + + const response = await vryno.post({ + $, + data: { + query, + }, + }); + + if (response.data.createLead.code != 200) { + throw new ConfigurationError(response.data.createLead.message); + } + + $.export("$summary", `Successfully created new lead with Id: ${response.data.createLead.data.id}`); + return response; + }, +}; diff --git a/components/vryno/package.json b/components/vryno/package.json new file mode 100644 index 0000000000000..a0a0886d5967a --- /dev/null +++ b/components/vryno/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/vryno", + "version": "0.1.0", + "description": "Pipedream Vryno Components", + "main": "vryno.app.mjs", + "keywords": [ + "pipedream", + "vryno" + ], + "homepage": "https://pipedream.com/apps/vryno", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} + diff --git a/components/vryno/vryno.app.mjs b/components/vryno/vryno.app.mjs new file mode 100644 index 0000000000000..e737cd4ca0345 --- /dev/null +++ b/components/vryno/vryno.app.mjs @@ -0,0 +1,33 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "vryno", + methods: { + _baseUrl() { + return `https://${this.$auth.company_instance_name}.ms.vryno.com`; + }, + _headers() { + return { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "Content-Type": "application/json", + }; + }, + _makeRequest({ + $ = this, path, ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: this._headers(), + }); + }, + post(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/api/graphql/crm", + ...opts, + }); + }, + }, +}; diff --git a/components/vtiger_crm/README.md b/components/vtiger_crm/README.md new file mode 100644 index 0000000000000..26f7d1a5e16b1 --- /dev/null +++ b/components/vtiger_crm/README.md @@ -0,0 +1,11 @@ +# Overview + +The Vtiger CRM API lets you tap into your customer relationship data to create, read, update, and delete records across various entities such as contacts, leads, and opportunities. In Pipedream, you can leverage this API to automate workflows, sync data across platforms, and react to events in real-time, all without the need for dedicated backend code. You can create dynamic workflows that respond to webhooks, schedule tasks, and integrate with 1000+ apps readily available on Pipedream. + +# Example Use Cases + +- **Lead Scoring Automation**: Automatically score and qualify leads in Vtiger CRM by setting up a workflow on Pipedream that processes incoming leads, evaluates them against predefined criteria, and updates their score in the CRM. + +- **Sync Contacts with a Newsletter Service**: Keep your Vtiger CRM contacts in sync with a newsletter service like Mailchimp. When a new contact is added to Vtiger CRM, a Pipedream workflow can trigger that adds the contact to a specific Mailchimp audience, ensuring your mailing lists are always up-to-date. + +- **Customer Support Ticketing Integration**: Integrate Vtiger CRM with a customer support platform such as Zendesk. Whenever a support ticket is resolved in Zendesk, a Pipedream workflow can update the related customer record in Vtiger CRM, adding notes or changing the status based on the outcome of the support ticket. diff --git a/components/vybit/README.md b/components/vybit/README.md new file mode 100644 index 0000000000000..467daa5cfe343 --- /dev/null +++ b/components/vybit/README.md @@ -0,0 +1,11 @@ +# Overview + +The Vybit API allows you to create and manage notifications with custom sounds. Using this API on Pipedream opens up possibilities for integrating audio notifications into a variety of workflows. You can dynamically generate alerts based on triggers from numerous apps, streamlining the way users receive and acknowledge important information. + +# Example Use Cases + +- **Dynamic Alert System for E-commerce Sales**: Set up a workflow that monitors your e-commerce platform for new sales. Each time a sale is made, the Vybit API is triggered to send a custom sound notification, giving you an immediate and pleasant audible alert. + +- **Task Completion Notifications for Project Management**: Integrate the Vybit API with your project management tool. When a task is marked as completed, trigger a Vybit notification to the team's channel or specific members, providing a satisfying sound cue to signify completion. + +- **Real-time Alerts for Social Media Mentions**: Create a workflow that connects to a social media platform's API. When your brand gets mentioned, use Vybit to play a unique sound, allowing your social media team to respond quickly and engage with the audience. diff --git a/components/vybit/actions/send-vybit/send-vybit.mjs b/components/vybit/actions/send-vybit/send-vybit.mjs new file mode 100644 index 0000000000000..f45f512bce840 --- /dev/null +++ b/components/vybit/actions/send-vybit/send-vybit.mjs @@ -0,0 +1,58 @@ +import app from "../../vybit.app.mjs"; + +export default { + key: "vybit-send-vybit", + name: "Send Vybit", + description: "Triggers a vybit, with optional customizations available. [See the documentation](https://www.vybit.net/#trigger)", + version: "0.0.1", + type: "action", + props: { + app, + vybitId: { + propDefinition: [ + app, + "vybitId", + ], + }, + message: { + type: "string", + label: "Message", + description: "Content that will override the default Vybit message and appear in the Vybit log.", + optional: true, + }, + imageUrl: { + type: "string", + label: "Image URL", + description: "An image to attach to the notification (will override the default setting)", + optional: true, + }, + linkUrl: { + type: "string", + label: "Link URL", + description: "A redirect URL when the notification is tapped (will override the default setting)", + optional: true, + }, + log: { + type: "string", + label: "Log", + description: "Content to append to the Vybit log (supports hyperlinks, such as `Example`)", + optional: true, + }, + }, + async run({ $ }) { + const { + app, vybitId, ...data + } = this; + const response = await app.sendVybit({ + $, + vybitId, + data, + }); + + const summary = response.result + ? `Successfully sent Vybit (event ID ${response.plk})` + : "Vybit could not be sent"; + $.export("$summary", summary); + return response; + }, +}; diff --git a/components/vybit/package.json b/components/vybit/package.json new file mode 100644 index 0000000000000..f32b3b5ece300 --- /dev/null +++ b/components/vybit/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/vybit", + "version": "0.1.0", + "description": "Pipedream Vybit Components", + "main": "vybit.app.mjs", + "keywords": [ + "pipedream", + "vybit" + ], + "homepage": "https://pipedream.com/apps/vybit", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/vybit/vybit.app.mjs b/components/vybit/vybit.app.mjs new file mode 100644 index 0000000000000..da30766b21a73 --- /dev/null +++ b/components/vybit/vybit.app.mjs @@ -0,0 +1,52 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "vybit", + propDefinitions: { + vybitId: { + type: "string", + label: "Vybit ID", + description: "Select a Vybit, or provide a Vybit ID.", + async options() { + const item = await this.listVybits(); + return item?.map(({ + triggerKey, name, + }) => ({ + label: name, + value: triggerKey, + })); + }, + }, + }, + methods: { + async _makeRequest({ + $ = this, headers, ...otherOpts + }) { + return axios($, { + ...otherOpts, + headers: { + ...headers, + "Content-Type": "application/json", + "Accept": "application/json", + "Accept-Charset": "utf-8", + "Authorization": `${this.$auth.oauth_access_token}`, + }, + }); + }, + async listVybits() { + return this._makeRequest({ + url: "https://app.vybit.net/rest/vybits", + }); + }, + async sendVybit({ + vybitId, ...args + }) { + return this._makeRequest({ + method: "POST", + url: `https://vybit.net/trigger/${vybitId}`, + ...args, + }); + }, + }, +}; diff --git a/components/waboxapp/README.md b/components/waboxapp/README.md index e58525eabaafb..5a2fbc184894a 100644 --- a/components/waboxapp/README.md +++ b/components/waboxapp/README.md @@ -1,20 +1,11 @@ # Overview -Building a two-way chatbot application is now easy and enjoyable thanks to the -Waboxapp API. With its simple and straightforward API, developers and -businesses can create a powerful bot that communicates effortlessly with -customers, automatically providing support, information and more. +The Waboxapp API is a powerful tool allowing enhanced interaction with WhatsApp, enabling automated messages, and message monitoring. With this API on Pipedream, you can create sophisticated workflows that send alerts, synchronize with customer service platforms, or even trigger events in other apps based on received WhatsApp messages. By leveraging serverless execution, these automations can run on-demand or on a schedule without the need for dedicated infrastructure. -The API can be used to build a wide range of applications, such as: +# Example Use Cases -- Chatbots that can send and receive messages -- Customer service bots for handling customer support issues and providing - support information -- Bots for ordering products, tracking shipments and receiving payments -- Chatbots for scheduling, reminders and tasks -- Bots that can manage calendars and to-do lists -- Intelligent bots for natural language processing and machine learning -- Bots for intelligent notifications and alerts -- Bots for providing personalized recommendations and offers -- Bots for gathering real-time data and insights -- Bots for providing content updates and news. +- **Customer Support Ticket Creation**: Automatically generate customer support tickets in helpdesk software like Zendesk whenever a WhatsApp message is received. This workflow can parse the incoming message for critical information, create a new ticket in Zendesk, and reply to the customer with a ticket number. + +- **Order Confirmation and Updates**: Connect Waboxapp to an e-commerce platform like Shopify to send order confirmations and shipping updates. When a new order is placed, trigger a workflow that sends a personalized WhatsApp message to the customer confirming their order, and later, notify them when their order has shipped. + +- **Event Reminder System**: Integrate Waboxapp with Google Calendar to send event reminders. Set up a workflow that monitors a specific calendar for upcoming events and automatically sends a WhatsApp message to attendees with event details a day before the event occurs. diff --git a/components/waboxapp/package.json b/components/waboxapp/package.json new file mode 100644 index 0000000000000..8e721b82bcc11 --- /dev/null +++ b/components/waboxapp/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/waboxapp", + "version": "0.6.0", + "description": "Pipedream waboxapp Components", + "main": "waboxapp.app.mjs", + "keywords": [ + "pipedream", + "waboxapp" + ], + "homepage": "https://pipedream.com/apps/waboxapp", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/wachete/README.md b/components/wachete/README.md new file mode 100644 index 0000000000000..b69fda0222200 --- /dev/null +++ b/components/wachete/README.md @@ -0,0 +1,11 @@ +# Overview + +The Wachete API lets you track changes in web content and get notified when updates happen. This is handy for monitoring websites for new content, price changes, or availability of products, without manually checking the sites. Using Pipedream, you can integrate Wachete with other apps to automate reactions to these updates. Think about monitoring your competitors’ sites, keeping tabs on industry news, or even watching for job postings. With Wachete on Pipedream, these tasks become set-and-forget operations. + +# Example Use Cases + +- **Content Change Notification to Slack**: Monitor your favorite news outlet for the latest articles. When Wachete detects a new post, use Pipedream to send a formatted message with the update's details straight to a Slack channel. This keeps your team in the know, instantly. + +- **E-commerce Price Tracking to Google Sheets**: Keep an eye on product prices across different e-commerce platforms. Set Wachete to watch for price changes, and with Pipedream’s help, log those changes in a Google Sheets spreadsheet. You can analyze trends, adjust your pricing strategies, or find the best time to buy. + +- **Job Listing Alerts via Email**: Use Wachete to monitor career pages for new job listings. When a new opportunity pops up, Pipedream can trigger an email to you, or post to a job board you manage. This makes sure you or your audience don't miss out on new career opportunities. diff --git a/components/waitless/package.json b/components/waitless/package.json new file mode 100644 index 0000000000000..a753ae935efab --- /dev/null +++ b/components/waitless/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/waitless", + "version": "0.0.1", + "description": "Pipedream Waitless Components", + "main": "waitless.app.mjs", + "keywords": [ + "pipedream", + "waitless" + ], + "homepage": "https://pipedream.com/apps/waitless", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/waitless/waitless.app.mjs b/components/waitless/waitless.app.mjs new file mode 100644 index 0000000000000..6cc312bb80494 --- /dev/null +++ b/components/waitless/waitless.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "waitless", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/waitlist/package.json b/components/waitlist/package.json new file mode 100644 index 0000000000000..414adf3421baa --- /dev/null +++ b/components/waitlist/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/waitlist", + "version": "0.1.0", + "description": "Pipedream Waitlist Components", + "main": "waitlist.app.mjs", + "keywords": [ + "pipedream", + "waitlist" + ], + "homepage": "https://pipedream.com/apps/waitlist", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/waitlist/sources/common/base.mjs b/components/waitlist/sources/common/base.mjs new file mode 100644 index 0000000000000..bfd05af0e8ab8 --- /dev/null +++ b/components/waitlist/sources/common/base.mjs @@ -0,0 +1,62 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import waitlist from "../../waitlist.app.mjs"; + +export default { + props: { + waitlist, + db: "$.service.db", + timer: { + label: "Polling interval", + description: "Pipedream will poll the Waitlist API on this schedule", + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastValue() { + return this.db.get("lastValue") || 0; + }, + _setLastValue(lastValue) { + this.db.set("lastValue", lastValue); + }, + getProps() { + return {}; + }, + async startEvent(maxResults = 0) { + const lastValue = this._getLastValue(); + const fn = this.getFunction(); + const field = this.getField(); + + const items = await fn({ + ...this.getProps(), + }); + + const filteredResponse = items.filter((item) => this.getFilter(item[field], lastValue)); + + if (filteredResponse.length) { + if (maxResults && filteredResponse.length > maxResults) { + filteredResponse.length = maxResults; + } + this._setLastValue(filteredResponse[filteredResponse.length - 1][field]); + } + + for (const item of filteredResponse) { + this.$emit( item, { + id: item.id || item.uuid, + summary: this.getSummary(item), + ts: Date.parse(item.created_at), + }); + } + }, + }, + hooks: { + async deploy() { + await this.startEvent(25); + }, + }, + async run() { + await this.startEvent(); + }, +}; diff --git a/components/waitlist/sources/new-list/new-list.mjs b/components/waitlist/sources/new-list/new-list.mjs new file mode 100644 index 0000000000000..c2a32f635ee80 --- /dev/null +++ b/components/waitlist/sources/new-list/new-list.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "waitlist-new-list", + name: "New List Created", + description: "Emit new event each time a waitlist is created. [See the documentation](https://getwaitlist.com/docs/api-docs/waitlist)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.waitlist.listWaitlists; + }, + getSummary(item) { + return `New waitlist with Id: ${item.id}`; + }, + getField() { + return "id"; + }, + getFilter(item, lastValue) { + return item > lastValue; + }, + }, + sampleEmit, +}; diff --git a/components/waitlist/sources/new-list/test-event.mjs b/components/waitlist/sources/new-list/test-event.mjs new file mode 100644 index 0000000000000..c82b16c2b75e1 --- /dev/null +++ b/components/waitlist/sources/new-list/test-event.mjs @@ -0,0 +1,40 @@ +export default { + "id": 213, + "configuration_style_json": { + "social_links": { + "facebook": "", + "instagram": "", + "linkedin": "", + "pinterest": "", + "twitter": "https://twitter.com/WaitlistAPI" + }, + "status_description": "Thanks for signing up!", + "status_font_color": "#000000", + "status_main_color": "#222222", + "widget_background_color": "#4937E7", + "widget_button_color": "#000000", + "widget_font_color": "#000000" + }, + "logo": null, + "spots_to_move_upon_referral": 3, + "uses_firstname_lastname": false, + "uses_leaderboard": true, + "uses_signup_verification": false, + "waitlist_name": "Title", + "waitlist_url_location": "https://getwaitlist.com", + "statistics": { + 'total_signups': 2200, + 'current_signups': 2200, + }, + "title": null, + "success_title": null, + "required_contact_detail": "EMAIL", + "widget_shows_social_links": false, + "signup_button_title": "Sign Up", + "hide_counts": false, + "leaderboard_length": 5, + "remove_widget_headers": false, + "questions": [{'question_value': "What is your favorite animal?", 'optional': false, "answer_value": ["Cat", "Dog", "Duck", "Other"]}], + "twitter_message": "", + "organization_uuid_fk": "30120c24-0ddc-4f35-9bc6-f5e3c7b09257", +} diff --git a/components/waitlist/sources/new-subscriber/new-subscriber.mjs b/components/waitlist/sources/new-subscriber/new-subscriber.mjs new file mode 100644 index 0000000000000..43286931a82ae --- /dev/null +++ b/components/waitlist/sources/new-subscriber/new-subscriber.mjs @@ -0,0 +1,44 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "waitlist-new-subscriber", + name: "New Subscriber Added", + description: "Emit new event each time a subscriber is added. [See the documentation](https://getwaitlist.com/docs/api-docs/waitlist)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + waitlistId: { + propDefinition: [ + common.props.waitlist, + "waitlistId", + ], + }, + }, + methods: { + ...common.methods, + getFunction() { + return this.waitlist.listSignups; + }, + getProps() { + return { + waitlistId: this.waitlistId, + }; + }, + getSummary(item) { + return `New signup with Id: ${item.uuid}`; + }, + getField() { + return "created_at"; + }, + getFilter(item, lastValue) { + let parseDate = item.split("_"); + const itemDate = `${parseDate[0]}T${parseDate[1].replace(/-/g, ":")}`; + return Date.parse(itemDate) > Date.parse(lastValue); + }, + }, + sampleEmit, +}; diff --git a/components/waitlist/sources/new-subscriber/test-event.mjs b/components/waitlist/sources/new-subscriber/test-event.mjs new file mode 100644 index 0000000000000..8892bfbe7006f --- /dev/null +++ b/components/waitlist/sources/new-subscriber/test-event.mjs @@ -0,0 +1,19 @@ +export default { + "amount_referred": 0, + "created_at": "2022-04-10_18-34-28", + "email": "maya@getwaitlist.com", + "priority": 4985, + "referral_link": "https://getwaitlist.com?ref_id=4F0BTBMAB", + "referral_token": "4F0BTBMAB", + "referred_by_signup_token": null, + "removed_date": null, + "removed_priority": null, + "uuid": "c60ff9f2-1a58-4551-87ea-414991184fba", + "verified": false, + "answers": [{'question_value': "What is your favorite animal?", 'optional': false, "answer_value": "Cat"}], + "phone": null, + "first_name": "Maya", + "last_name": "Kyler", + "metadata": {}, + "waitlist_id": 1234 +} diff --git a/components/waitlist/waitlist.app.mjs b/components/waitlist/waitlist.app.mjs new file mode 100644 index 0000000000000..238191563350c --- /dev/null +++ b/components/waitlist/waitlist.app.mjs @@ -0,0 +1,57 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "waitlist", + propDefinitions: { + waitlistId: { + type: "string", + label: "Waitlist Id", + description: "The ID of your waitlist.", + async options() { + const waitlists = await this.listWaitlists(); + + return waitlists.map(({ + id: value, waitlist_name: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.getwaitlist.com/api/v1"; + }, + _headers() { + return { + "Content-Type": "application/json", + "api-key": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listWaitlists(opts = {}) { + return this._makeRequest({ + path: "/waitlist", + ...opts, + }); + }, + listSignups({ + waitlistId, ...opts + }) { + return this._makeRequest({ + path: `/signup/waitlist/${waitlistId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/waitwhile/README.md b/components/waitwhile/README.md index 466f60892d65f..79b2abb837e0b 100644 --- a/components/waitwhile/README.md +++ b/components/waitwhile/README.md @@ -1,25 +1,11 @@ # Overview -With the Waitwhile API, you can create a variety of powerful -customer-engagement tools to manage your business and how it interacts with -customers. +Waitwhile is a queue management and scheduling platform that lets you optimize appointments and waitlists, enhancing the customer experience. By leveraging the Waitwhile API on Pipedream, you can automate processes such as updating waitlists, syncing customer data, or triggering notifications when a customer's status changes. This seamless integration can streamline operations for businesses like restaurants, healthcare providers, or service centers where queue management is crucial. -The API provides the following benefits: +# Example Use Cases -- Create customer profiles -- Schedule appointments -- Track customer activity -- Collect customer feedback +- **Automated Customer Check-in**: When a customer signs in using a custom form or app, trigger a workflow on Pipedream to add them to the Waitwhile waitlist automatically. Connect to a CRM like Salesforce to log the visit and update the customer's profile with the wait time and service details. -Here are some examples of what you can build with the Waitwhile API: +- **Real-time Waitlist Syncing**: Sync Waitwhile's waitlist with Google Sheets in real-time. Each time a customer's status changes or they're added/removed from the list, a Pipedream workflow updates the corresponding sheet, keeping all stakeholders informed without manual data entry. -- Customized customer experiences - Customize the booking process and include - unique details that create comprehensive customer profiles. -- Automated appointment management - Automatically manage and book appointments - using the Waitwhile API. -- Scalable customer engagement - Ensure customer engagement and increase - customer loyalty with features like automated customer-engagement surveys. -- Customized notifications - Create personalized messages to customers based on - their activity and profile. -- Customized communication - Enhance customer communication with customized - email blasts, texts, and push notifications. +- **Appointment Reminder System**: Set up a workflow to send SMS reminders to customers about their upcoming appointments. When Waitwhile schedules an appointment, Pipedream can trigger an SMS via Twilio, reminding the customer of their appointment time and providing options to confirm or reschedule. diff --git a/components/waiverfile/README.md b/components/waiverfile/README.md new file mode 100644 index 0000000000000..c57d1cdeb1c81 --- /dev/null +++ b/components/waiverfile/README.md @@ -0,0 +1,11 @@ +# Overview + +With the WaiverFile API, you can automate interactions with your WaiverFile account directly from Pipedream. This API provides access to manage and retrieve waivers, customer data, and events, allowing you to integrate WaiverFile with other services for streamlined workflows. You could automate the sending of waiver links, synchronize signed waivers with a database, or trigger actions based on waiver completions. The flexibility of the WaiverFile API means you can create powerful automations tailored to your business needs on Pipedream's serverless platform. + +# Example Use Cases + +- **Automatic Waiver Follow-up Emails**: When a customer signs a waiver, trigger an automated email through an email service like SendGrid or Mailgun, thanking them or providing additional information. This improves customer experience without manual intervention. + +- **Sync Waivers to Google Sheets for Record-Keeping**: Automatically send completed waivers to a Google Sheet. This keeps records up-to-date and makes it easy to monitor and analyze waiver submissions in real-time, without manual data entry. + +- **Slack Notifications for New Waivers**: Set up a workflow that sends a notification to a specific Slack channel whenever a waiver is signed. This is useful for real-time updates, allowing team members to promptly take any necessary follow-up actions. diff --git a/components/waiverforever/README.md b/components/waiverforever/README.md index 070e973f6e666..9c0d38876899b 100644 --- a/components/waiverforever/README.md +++ b/components/waiverforever/README.md @@ -1,21 +1,11 @@ # Overview -WaiverForever is an efficient digital waiver solution that can help you -automate your waiver signing process. With their powerful API, developers can -create custom apps and services that leverage WaiverForever’s features, -allowing you to quickly and securely manage digital waivers. +WaiverForever offers an API to digitally manage waiver documents, enabling users to streamline the process of creating, signing, and organizing waivers. This API can be a game-changer for businesses that need to handle liability waivers, consent forms, or other types of agreements efficiently. By leveraging Pipedream's capabilities, you can automate workflows around these waiver processes, such as triggering actions when a waiver is signed, syncing signed waivers with other databases, or setting up alerts for incomplete documents. -The WaiverForever API provides several endpoints that you can use to integrate -your applications with the product. You can develop custom applications to do -everything from signing and managing waiver documents to tracking customer -data, offering better performance and minimizing manual processes. Here are -just some of the things you can build with the WaiverForever API: +# Example Use Cases -- Create and send online waivers to customers -- Automatically store and organize signed waivers -- Automate customer data updates -- Create reports and export data in a multitude of formats -- Manage payment processing securely -- Scan and compare signatures -- Receive notifications when a waiver is signed -- Access customizable templates for waivers and forms +- **Automated Waiver Confirmation Emails**: When a waiver is signed through WaiverForever, trigger a Pipedream workflow that sends a personalized confirmation email to the signee using an email service like SendGrid. This ensures immediate acknowledgment and offers a digital receipt of their signed document. + +- **CRM Integration for New Signees**: Integrate WaiverForever with a Customer Relationship Management (CRM) platform such as Salesforce. Each time a waiver is completed, automatically create or update the customer profile in the CRM with their latest information, ensuring your customer records are always up-to-date. + +- **Slack Notifications for Team Awareness**: Set up a Pipedream workflow that posts a notification in a specified Slack channel whenever a new waiver is signed. This is especially useful for teams needing instant updates, like event organizers or activity centers that require real-time awareness of participant sign-ins. diff --git a/components/wakatime/README.md b/components/wakatime/README.md index 5c6014f357f16..b4384e781b1c8 100644 --- a/components/wakatime/README.md +++ b/components/wakatime/README.md @@ -1,16 +1,11 @@ # Overview -The WakaTime API enables developers to easily track, analyze, and display -real-time coding activity and metrics. With the WakaTime API, you can build -apps that help you measure and improve programming productivity. Here are a few -examples of the type of applications and features you can create with the -WakaTime API: +WakaTime offers insights into your coding activity, enabling you to track the time you spend on programming projects. With the WakaTime API on Pipedream, you can automate the extraction of this data to trigger actions in other apps, generate reports, and monitor your development workflow. Pipedream's serverless platform allows you to connect WakaTime to hundreds of other services without writing comprehensive code, creating opportunities to streamline your productivity and gain data-driven insights into your coding habits. -- Dashboards to visualize coding activity -- Reports about programming languages and tools used -- Alerts for specific coding activities -- Integration with collaboration tools (e.g. Slack, Trello) -- Tools to recommend coding best practices -- Add-ons to better track project progress and deadlines -- Metrics for managing remote programming teams -- Automated metrics for software projects and releases +# Example Use Cases + +- **Daily Coding Summary to Slack**: Using Pipedream's workflow, you can set up an automation that sends a daily summary of your coding activity from WakaTime to a designated Slack channel. This keeps your team informed about project progress and individual contributions without manual updates. + +- **Project Time Tracking to Google Sheets**: With Pipedream, you can create a workflow that automatically logs the time spent on different projects in WakaTime to a Google Sheets spreadsheet. This enables easy tracking of time allocation across multiple projects and simplifies generating timesheets or invoices. + +- **Github Commit Triggered Coding Breakdown**: Set up a Pipedream workflow where a GitHub commit triggers a detailed breakdown of your coding activity in WakaTime for the time period leading up to the commit. This allows you to correlate coding time with specific development milestones and understand your productivity patterns. diff --git a/components/wappalyzer/README.md b/components/wappalyzer/README.md new file mode 100644 index 0000000000000..90e31e42e5a69 --- /dev/null +++ b/components/wappalyzer/README.md @@ -0,0 +1,11 @@ +# Overview + +The Wappalyzer API lets you uncover the technology stack of any website. On Pipedream, you can integrate this capability into workflows to automate tech stack detection. Here's what you can do: identify the software used by your competitors, track technology adoption, and get insights into your target market's tech preferences. By leveraging Pipedream's robust platform, you can connect Wappalyzer with other services to create custom workflows that trigger actions based on the tech stack data you collect. + +# Example Use Cases + +- **Lead Generation for Web Development Services**: Detect tech stacks of potential leads' websites. If a site uses outdated frameworks, trigger an email through the SendGrid app, offering your services to modernize their web presence. + +- **Market Research Automation**: Schedule daily checks on top industry websites, capture their tech stacks, and save the data in a Google Sheets spreadsheet for trend analysis and report generation. + +- **Competitor Monitoring Alert System**: Monitor competitor websites for tech stack changes. Use Pipedream's built-in Cron Scheduler to run the workflow weekly, and if new technologies are detected, post a notification in a Slack channel to keep your team informed. diff --git a/components/warpcast/README.md b/components/warpcast/README.md new file mode 100644 index 0000000000000..7242c4d9fc1bb --- /dev/null +++ b/components/warpcast/README.md @@ -0,0 +1,11 @@ +# Overview + +The Warpcast API unlocks the potential for creating dynamic and interactive video experiences. With Pipedream, you can automate interactions with Warpcast, such as managing video content, analyzing viewer data, and integrating with other services. Pipedream's serverless platform facilitates building workflows that trigger on specific events, process data, and connect to countless other APIs, all with minimal setup and maintenance. + +# Example Use Cases + +- **Automate Video Uploads**: When new videos are ready for publishing from a cloud storage service like Dropbox, a Pipedream workflow can automatically upload them to Warpcast. Set up a trigger for new files in Dropbox, then use the Warpcast API to add the video to your channel or playlist. + +- **Sync Viewer Data with CRM**: Keep track of viewer engagement by syncing viewing data from Warpcast to a CRM like Salesforce. When Warpcast reports a viewing event, use Pipedream to parse the data and update the contact record in Salesforce, helping to personalize customer journeys. + +- **Moderate Comments with Sentiment Analysis**: Connect Warpcast with a Natural Language Processing (NLP) service like Google Cloud's Natural Language API. When new comments are posted on your videos, analyze their sentiment in real-time. If negative sentiment is detected, flag the comment for review or notify a team member. diff --git a/components/watchsignals/README.md b/components/watchsignals/README.md new file mode 100644 index 0000000000000..4423550b15563 --- /dev/null +++ b/components/watchsignals/README.md @@ -0,0 +1,11 @@ +# Overview + +The WatchSignals API offers access to a rich database of luxury watch market data, including price tracking, brand details, and watch specifications. By integrating WatchSignals API with Pipedream, you can automate various tasks such as monitoring market trends, updating pricing in your inventory system, or even alerting customers to changes in watch prices or new arrivals. Pipedream's serverless platform allows you to create these workflows quickly, leveraging hundreds of built-in services without managing infrastructure. + +# Example Use Cases + +- **Market Trend Analysis**: Trigger a Pipedream workflow on a schedule to fetch the latest luxury watch market data from WatchSignals. Analyze the data within Pipedream using built-in code steps or send it to a data visualization tool like Google Sheets or Data Studio for further examination and report generation. + +- **Inventory Price Sync**: Whenever a watch price updates on WatchSignals, use a Pipedream workflow to automatically reflect this change in your own e-commerce platform's inventory system. This can be achieved by setting up a webhook to listen for price change events from WatchSignals and then using an API request to update the corresponding items in your e-commerce database. + +- **Customer Alerts for Watch Collectors**: Create a Pipedream workflow that subscribes to price changes or new additions of specific luxury watch models. When a change is detected, automatically send personalized email alerts to customers who expressed interest in those models using an email service like SendGrid or Mailgun. diff --git a/components/wati/.gitignore b/components/wati/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/wati/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/wati/README.md b/components/wati/README.md new file mode 100644 index 0000000000000..7c51148cebf1c --- /dev/null +++ b/components/wati/README.md @@ -0,0 +1,11 @@ +# Overview + +The WATI API allows you to power up your customer engagement by leveraging WhatsApp messaging. With Pipedream's capabilities, you can create serverless workflows that integrate WATI to automate personalized notifications, process inbound messages, and manage contacts. This can help scale your customer service, marketing campaigns, and streamline communications with WhatsApp's wide user base. + +# Example Use Cases + +- **Customer Support Automation**: Automate responses to common queries received on WhatsApp by using WATI with Pipedream's built-in code steps or pre-built actions. Tag and route complex issues to human agents or update your CRM with the conversation context. + +- **Appointment Reminders**: Combine WATI with Google Calendar on Pipedream to send automated WhatsApp reminders for appointments. Trigger the workflow when a new event is added or an existing one is nearing its start time, reducing no-shows and enhancing customer experience. + +- **Order Updates and Feedback Collection**: Use WATI with a Shopify trigger in Pipedream to notify customers of their order status. Follow up with a satisfaction survey through WhatsApp after order delivery, collecting feedback directly into a Google Sheet or your preferred analytics tool. diff --git a/components/wati/actions/add-contact/add-contact.mjs b/components/wati/actions/add-contact/add-contact.mjs new file mode 100644 index 0000000000000..ec617afa928f9 --- /dev/null +++ b/components/wati/actions/add-contact/add-contact.mjs @@ -0,0 +1,53 @@ +import { ConfigurationError } from "@pipedream/platform"; +import wati from "../../wati.app.mjs"; + +export default { + key: "wati-add-contact", + name: "Add Contact", + description: "Adds a new contact on the WATI platform. [See the documentation](https://docs.wati.io/reference/post_api-v1-addcontact-whatsappnumber)", + version: "0.0.1", + type: "action", + props: { + wati, + whatsappNumber: { + propDefinition: [ + wati, + "whatsappNumber", + ], + }, + name: { + type: "string", + label: "Name", + description: "The name of the contact", + optional: true, + }, + customParams: { + propDefinition: [ + wati, + "customParams", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.wati.addContact({ + $, + whatsappNumber: this.whatsappNumber, + data: { + name: this.name, + customParams: this.customParams && Object.entries(this.customParams).map(([ + key, + value, + ]) => ({ + name: key, + value, + })), + }, + }); + if (!response.result) { + throw new ConfigurationError(response.info); + } + $.export("$summary", `Successfully added contact with phone number: ${this.whatsappNumber}`); + return response; + }, +}; diff --git a/components/wati/actions/send-template-message/send-template-message.mjs b/components/wati/actions/send-template-message/send-template-message.mjs new file mode 100644 index 0000000000000..3c02cc3974538 --- /dev/null +++ b/components/wati/actions/send-template-message/send-template-message.mjs @@ -0,0 +1,63 @@ +import { ConfigurationError } from "@pipedream/platform"; +import wati from "../../wati.app.mjs"; + +export default { + key: "wati-send-template-message", + name: "Send WhatsApp Template Message", + description: "Enables sending of WhatsApp messages using a pre-approved template. [See the documentation](https://docs.wati.io/reference/post_api-v2-sendtemplatemessage)", + version: "0.0.1", + type: "action", + props: { + wati, + whatsappNumber: { + propDefinition: [ + wati, + "whatsappNumber", + ], + }, + customParams: { + propDefinition: [ + wati, + "customParams", + ], + label: "Parameters", + description: "An object with template's custom params.", + }, + templateName: { + propDefinition: [ + wati, + "templateName", + ], + }, + broadcastName: { + type: "string", + label: "Broadcast Name", + description: "The name of broadcast.", + }, + }, + async run({ $ }) { + const response = await this.wati.sendTemplateMessage({ + $, + params: { + whatsappNumber: this.whatsappNumber, + }, + data: { + parameters: this.customParams && Object.entries(this.customParams).map(([ + key, + value, + ]) => ({ + name: key, + value, + })), + template_name: this.templateName, + broadcast_name: this.broadcastName, + }, + }); + if (!response.result) { + throw new ConfigurationError(response.info); + } + + $.export("$summary", `Successfully sent template message to ${this.whatsappNumber}`); + return response; + }, +}; diff --git a/components/wati/actions/update-contact-attribute/update-contact-attribute.mjs b/components/wati/actions/update-contact-attribute/update-contact-attribute.mjs new file mode 100644 index 0000000000000..d1254ea3183bc --- /dev/null +++ b/components/wati/actions/update-contact-attribute/update-contact-attribute.mjs @@ -0,0 +1,47 @@ +import { ConfigurationError } from "@pipedream/platform"; +import wati from "../../wati.app.mjs"; + +export default { + key: "wati-update-contact-attribute", + name: "Update Contact Attribute", + description: "Allows updating attributes/tags related to an existing contact. [See the documentation](https://docs.wati.io/reference/post_api-v1-updatecontactattributes-whatsappnumber)", + version: "0.0.1", + type: "action", + props: { + wati, + whatsappNumber: { + propDefinition: [ + wati, + "whatsappNumber", + ], + }, + customParams: { + propDefinition: [ + wati, + "customParams", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.wati.updateContactAttributes({ + $, + whatsappNumber: this.whatsappNumber, + data: { + customParams: this.customParams && Object.entries(this.customParams).map(([ + key, + value, + ]) => ({ + name: key, + value, + })), + }, + }); + if (!response.result) { + throw new ConfigurationError(response.info); + } + + $.export("$summary", `Successfully updated attributes for contact ${this.whatsappNumber}`); + return response; + }, +}; diff --git a/components/wati/app/wati.app.ts b/components/wati/app/wati.app.ts deleted file mode 100644 index 208ee0d199f21..0000000000000 --- a/components/wati/app/wati.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "wati", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/wati/package.json b/components/wati/package.json index 031b83cd95569..90b471a941a4e 100644 --- a/components/wati/package.json +++ b/components/wati/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/wati", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream WATI Components", - "main": "dist/app/wati.app.mjs", + "main": "wati.app.mjs", "keywords": [ "pipedream", "wati" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/wati", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } -} \ No newline at end of file +} diff --git a/components/wati/sources/common/base.mjs b/components/wati/sources/common/base.mjs new file mode 100644 index 0000000000000..8e10715b02532 --- /dev/null +++ b/components/wati/sources/common/base.mjs @@ -0,0 +1,61 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import wati from "../../wati.app.mjs"; + +export default { + props: { + wati, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + prepareData(data) { + return data; + }, + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const dateField = this.getDateField(); + + const response = this.wati.paginate( + this.getPaginateOpts(maxResults), + ); + + let responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + responseArray = this.prepareData(responseArray, lastDate, maxResults); + + if (responseArray.length) { + this._setLastDate(responseArray[0][dateField]); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item[dateField]), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/wati/sources/new-contact-created/new-contact-created.mjs b/components/wati/sources/new-contact-created/new-contact-created.mjs new file mode 100644 index 0000000000000..0b5c33e00917d --- /dev/null +++ b/components/wati/sources/new-contact-created/new-contact-created.mjs @@ -0,0 +1,33 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "wati-new-contact-created", + name: "New Contact Created", + description: "Emit new event when a contact is created from an incoming WhatsApp message.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getPaginateOpts(maxResults) { + return { + fn: this.wati.listContacts, + itemsField: "result", + optsField: "data", + maxResults, + }; + }, + getDateField() { + return "created"; + }, + checkBreak(item, lastDate) { + return Date.parse(item.created) < lastDate; + }, + getSummary(item) { + return `New contact created: ${item.wAid}`; + }, + }, + sampleEmit, +}; diff --git a/components/wati/sources/new-contact-created/test-event.mjs b/components/wati/sources/new-contact-created/test-event.mjs new file mode 100644 index 0000000000000..3a696c5a7d163 --- /dev/null +++ b/components/wati/sources/new-contact-created/test-event.mjs @@ -0,0 +1,33 @@ +export default { + "id": "670934c1d464c11dd46c3b7f", + "wAid": "17759865200", + "firstName": "+17759865200", + "fullName": "+17759865200", + "phone": "17759865200", + "source": null, + "contactStatus": "VALID", + "photo": null, + "created": "Oct-11-2024", + "customParams": [ + { + "name": "name", + "value": "+17759865200" + }, + { + "name": "phone", + "value": "17759865200" + } + ], + "optedIn": false, + "isDeleted": false, + "lastUpdated": "2024-10-11T15:09:36.047Z", + "allowBroadcast": true, + "allowSMS": true, + "teamIds": [ + "6708393ad464c11dd46b3d73" + ], + "isInFlow": false, + "lastFlowId": null, + "currentFlowNodeId": null, + "selectedHubspotId": null +} \ No newline at end of file diff --git a/components/wati/sources/new-incoming-message/new-incoming-message.mjs b/components/wati/sources/new-incoming-message/new-incoming-message.mjs new file mode 100644 index 0000000000000..448456aa51882 --- /dev/null +++ b/components/wati/sources/new-incoming-message/new-incoming-message.mjs @@ -0,0 +1,52 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "wati-new-incoming-message", + name: "New Incoming Message", + description: "Emit new event when there is an incoming message on your number.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + contactId: { + propDefinition: [ + common.props.wati, + "contactId", + ], + }, + }, + methods: { + ...common.methods, + getPaginateOpts() { + return { + fn: this.wati.listContactMessages, + whatsappNumber: `+${this.contactId}`, + itemsField: [ + "messages", + ], + optsField: "params", + }; + }, + getDateField() { + return "timestamp"; + }, + prepareData(data, lastDate, maxResults) { + data = data + .filter((item) => item.statusString === "SENT" && Date.parse(item.created) > lastDate) + .sort((a, b) => Date.parse(b.created) - Date.parse(a.created)); + + if (maxResults && data.length > maxResults) data.length = maxResults; + return data; + }, + checkBreak(item, lastDate) { + return Date.parse(item.timestamp) < lastDate; + }, + getSummary(item) { + return `New message: ${item.text || "No content"}`; + }, + }, + sampleEmit, +}; diff --git a/components/wati/sources/new-incoming-message/test-event.mjs b/components/wati/sources/new-incoming-message/test-event.mjs new file mode 100644 index 0000000000000..c478a5e1f2f00 --- /dev/null +++ b/components/wati/sources/new-incoming-message/test-event.mjs @@ -0,0 +1,26 @@ +export default { + "replySourceMessage": null, + "messageReferral": null, + "text": "Know the Pricing", + "type": "text", + "data": null, + "timestamp": "1728656646", + "owner": false, + "statusString": "SENT", + "avatarUrl": null, + "assignedId": null, + "operatorName": null, + "localMessageId": null, + "failedDetail": null, + "referenceOrderId": null, + "contacts": null, + "messageProducts": null, + "orderProducts": null, + "interactiveData": null, + "orderDetailsViewModel": null, + "id": "67093506d464c11dd46c3bcf", + "created": "2024-10-11T14:24:06.891Z", + "conversationId": "670934f5d464c11dd46c3bc6", + "ticketId": "67093506d464c11dd46c3bcc", + "eventType": "message" +} \ No newline at end of file diff --git a/components/wati/wati.app.mjs b/components/wati/wati.app.mjs new file mode 100644 index 0000000000000..295552c153504 --- /dev/null +++ b/components/wati/wati.app.mjs @@ -0,0 +1,144 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "wati", + propDefinitions: { + contactId: { + type: "string", + label: "Contact Id", + description: "The Id of the contact.", + async options({ page }) { + const { result: { items } } = await this.listContacts({ + data: { + pageSize: 100, + pageNumber: page, + }, + }); + + return items.map(({ wAid }) => wAid); + }, + }, + whatsappNumber: { + type: "string", + label: "WhatsApp Number", + description: "Your WhatsApp number with country code.", + }, + customParams: { + type: "object", + label: "Custom Params", + description: "An object with contact's custom fields.", + }, + templateName: { + type: "string", + label: "Template Name", + description: "The name of template.", + async options({ page }) { + const { messageTemplates: data } = await this.listTemplates({ + params: { + pageSize: page + 1, + }, + }); + + return data.map(({ elementName }) => elementName); + }, + }, + }, + methods: { + _baseUrl() { + return `${this.$auth.api_endpoint}/api/v1`; + }, + _headers() { + return { + Authorization: `${this.$auth.access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contacts", + ...opts, + }); + }, + listContactMessages({ + whatsappNumber, ...opts + }) { + return this._makeRequest({ + path: `/getMessages/${whatsappNumber}`, + ...opts, + }); + }, + listTemplates(opts = {}) { + return this._makeRequest({ + path: "/getMessageTemplates", + ...opts, + }); + }, + addContact({ + whatsappNumber, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/addContact/${whatsappNumber}`, + ...opts, + }); + }, + sendTemplateMessage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/sendTemplateMessage", + ...opts, + }); + }, + updateContactAttributes({ + whatsappNumber, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/updateContactAttributes/${whatsappNumber}`, + ...opts, + }); + }, + async *paginate({ + fn, itemsField, optsField, maxResults = null, data = {}, params = {}, ...otherOpts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + const opts = { + data, + params, + ...otherOpts, + }; + + opts[optsField].pageSize = 100; + + do { + opts[optsField].pageNumber = page++; + const response = await fn(opts); + const items = response[itemsField].items; + + for (const d of items) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = items.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/watsonx_ai/README.md b/components/watsonx_ai/README.md new file mode 100644 index 0000000000000..b89f2af01fe90 --- /dev/null +++ b/components/watsonx_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +The WatsonX AI API offers a robust suite of AI capabilities from IBM, designed to integrate advanced machine learning and cognitive computing into various applications. With these APIs, you can enhance your apps with AI-driven features such as language understanding, speech to text conversion, and visual recognition. Using Pipedream, these capabilities can be seamlessly integrated into automated workflows, allowing you to connect WatsonX AI with other services and automate complex tasks efficiently. + +# Example Use Cases + +- **Customer Support Automation**: Automate responses in a customer support system by integrating WatsonX AI with a ticketing system like Zendesk. WatsonX AI can analyze incoming support tickets, understand the sentiment and content, and automatically draft appropriate responses or escalate issues based on urgency and sentiment. + +- **Content Recommendations System**: Build a content recommendation engine by leveraging WatsonX AI’s natural language understanding. Analyze user interactions and feedback on various contents through Pipedream, then use WatsonX AI to suggest personalized content to users on platforms like WordPress or Shopify. + +- **Real-time Speech Analytics**: Enhance a call center application by integrating WatsonX AI’s speech-to-text capabilities. Convert call audio to text in real-time, analyze the text to extract insights, and use these insights to provide real-time assistance to agents or direct calls to appropriate departments. diff --git a/components/watsonx_ai/package.json b/components/watsonx_ai/package.json new file mode 100644 index 0000000000000..4cffa5a0f40aa --- /dev/null +++ b/components/watsonx_ai/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/watsonx_ai", + "version": "0.0.1", + "description": "Pipedream WatsonX AI Components", + "main": "watsonx_ai.app.mjs", + "keywords": [ + "pipedream", + "watsonx_ai" + ], + "homepage": "https://pipedream.com/apps/watsonx_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/watsonx_ai/watsonx_ai.app.mjs b/components/watsonx_ai/watsonx_ai.app.mjs new file mode 100644 index 0000000000000..b6dd99803fb78 --- /dev/null +++ b/components/watsonx_ai/watsonx_ai.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "watsonx_ai", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/wave/README.md b/components/wave/README.md new file mode 100644 index 0000000000000..96b7628a5ef75 --- /dev/null +++ b/components/wave/README.md @@ -0,0 +1,11 @@ +# Overview + +Wave is a financial software that simplifies accounting, invoicing, and receipt scanning for small businesses. With the Wave API, you can automate various financial tasks directly within Pipedream. You can create invoices, manage customers, process payments, and extract financial reports. Integrating Wave with Pipedream allows you to connect your accounting workflow with other apps, triggering actions based on financial events or scheduling regular financial operations. + +# Example Use Cases + +- **Automated Invoice Creation on New Customer Signup**: When a new customer registers via your app (e.g., using the Shopify API), automatically create and send a customized invoice in Wave. + +- **Expense Tracking with Receipt Scanning**: Send an email with a receipt attachment (using the Gmail API) to a designated Pipedream inbox, which triggers a workflow that scans and records the receipt in Wave for expense tracking. + +- **Monthly Financial Summary Reports**: Schedule a monthly Pipedream workflow to retrieve financial reports from Wave and send a digest via Slack to your finance team for review. diff --git a/components/wavecell/README.md b/components/wavecell/README.md index d18d2077dd1f3..9fd7bc6f76524 100644 --- a/components/wavecell/README.md +++ b/components/wavecell/README.md @@ -1,24 +1,11 @@ # Overview -The 8x8 API enables developers to create powerful communication solutions and -seamlessly integrate them with their applications and products. With just a few -lines of code, you can make and receive phone calls, send and receive text -messages, and securely manage you users’ contact directory. +The 8x8 (Wavecell) API offers robust capabilities for voice and SMS messaging services, enabling you to engage customers through automated communications. Whether it's sending out timely notifications, verifying user identities with OTPs, or streamlining customer support processes, the API provides a versatile platform for enhancing your communication workflows. With Pipedream, you can harness this potential by crafting serverless integrations that trigger 8x8 actions based on events from countless other apps, or even custom logic. -Some of the many things that can be built with the 8x8 API include: +# Example Use Cases -- Cloud Call Centers: Create inbound and outbound contact centers that can - handle any amount of customer inquiries. -- Multi Channel Communication Platforms: Build out a suite of messaging - services that covers the major platforms like SMS, WhatsApp, and Facebook - Messenger. -- Customer Interactions: Set up communication channels in your app or website - that can be used to increase user engagement. -- Artificial Intelligence and Machine Learning: Utilize the 8x8 API to create - solutions that use AI and ML to automate communication processes. -- Unified Communications: Create seamless communication systems for customers - that are accessible through a variety of communication channels. -- DataSecurity: Add multiple layers of security to ensure that user’s data is - secure and protected. -- Reporting: Streamlined reporting tools that allow you to quickly pull data - and insights on customer interactions. +- **Automated Customer Support Tickets Alerting**: Integrate 8x8 with a customer support platform like Zendesk to automatically send SMS updates to customers when their support ticket status changes. This ensures they're kept in the loop, improving customer satisfaction and engagement. + +- **E-Commerce Order Confirmation and Updates**: For each new order placed on an e-commerce platform like Shopify, trigger an 8x8 workflow to send a confirmation SMS to the customer, and follow up with delivery status updates. This proactive communication can enhance the customer experience and reduce inbound inquiry volumes. + +- **Event-Driven Notification System for IT Monitoring**: Combine 8x8 with an IT monitoring tool like Datadog. Set up a workflow to send an SMS to your IT team when critical alerts or incidents are detected. Quick notifications mean faster response times, minimizing downtime and operational disruptions. diff --git a/components/wavecell/package.json b/components/wavecell/package.json new file mode 100644 index 0000000000000..5a2f371a8830e --- /dev/null +++ b/components/wavecell/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/wavecell", + "version": "0.6.0", + "description": "Pipedream wavecell Components", + "main": "wavecell.app.mjs", + "keywords": [ + "pipedream", + "wavecell" + ], + "homepage": "https://pipedream.com/apps/wavecell", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/wealthbox/README.md b/components/wealthbox/README.md new file mode 100644 index 0000000000000..8101f660460ad --- /dev/null +++ b/components/wealthbox/README.md @@ -0,0 +1,11 @@ +# Overview + +The Wealthbox API allows for the management and automation of customer relationship management (CRM) tasks within Wealthbox's platform. By leveraging this API on Pipedream, you can create workflows that automate updates, manage contacts, and sync data across multiple applications. This can greatly enhance productivity, reduce manual entry errors, and keep data consistent across platforms. + +# Example Use Cases + +- **Contact Synchronization**: Automatically sync new contacts from Wealthbox to Google Contacts. Every time a new contact is added to Wealthbox, a Pipedream workflow triggers and adds that contact to Google Contacts, ensuring your address books are always up-to-date. + +- **Task Management Integration**: Create tasks in project management tools like Trello or Asana when a new task is added in Wealthbox. This workflow helps keep track of CRM tasks alongside other project-related activities, streamlining the management process across different platforms. + +- **Email Marketing Automation**: When a new contact is labeled with a specific tag in Wealthbox, enroll them in a targeted email campaign in Mailchimp. This ensures that your marketing efforts are tailored and consistent, engaging leads with relevant content at the right time. diff --git a/components/weatherbit_io/README.md b/components/weatherbit_io/README.md new file mode 100644 index 0000000000000..16b15f0f1fb94 --- /dev/null +++ b/components/weatherbit_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The Weatherbit.io API offers real-time weather data, forecasts, and historical weather information. By integrating this API with Pipedream, you can automate tasks based on weather conditions, set up alerts, or enrich your app with weather data. This can range from triggering workflows when certain weather thresholds are met, to augmenting business data with weather insights, or automating smart home devices in response to the weather. + +# Example Use Cases + +- **Weather-Triggered Notifications**: Automatically send a message through Slack or email using the Pipedream Slack app or the SMTP service when the Weatherbit.io API reports weather conditions like rain or high temperatures in a specified location. It's handy for event planning or sending reminders to bring an umbrella. + +- **Dynamic Advertising Based on Weather**: Adjust advertising campaigns on platforms like Google Ads or Facebook Ads by triggering changes in ad copy or spending when the Weatherbit.io API forecasts specific weather conditions. This could maximize engagement for products that are weather-sensitive, like clothing or travel services. + +- **Smart Home Automation**: Integrate with smart home platforms like Philips Hue or Nest using their respective Pipedream apps to adjust lighting or temperature settings based on real-time weather data or forecasts from Weatherbit.io. This creates personalized and energy-efficient home environments. diff --git a/components/weaviate/README.md b/components/weaviate/README.md new file mode 100644 index 0000000000000..e6181340fd4fd --- /dev/null +++ b/components/weaviate/README.md @@ -0,0 +1,11 @@ +# Overview + +Weaviate is a cloud-native, modular, real-time vector search engine that enables scalable, high-performance semantic search. It's built for a wide range of applications, from autocomplete and similar object suggestions to full-text search and automatic categorization. With the Weaviate API, you can index and search through large amounts of data using machine learning models to understand the content and context of the data. On Pipedream, you can leverage this API to create serverless workflows that automate data ingestion, enrichment, and search capabilities, enhancing your apps with intelligent search functions. + +# Example Use Cases + +- **Automated Data Ingestion Workflow**: Ingest data from various sources, transform and enrich it using functions, and index it in Weaviate to create a powerful search database. Connect with sources like RSS feeds, webhooks, or databases on Pipedream to automate the flow of information into Weaviate. + +- **Sentiment Analysis and Indexing**: Use Weaviate in conjunction with a sentiment analysis API, like the one provided by Google Cloud Natural Language, to process user reviews or social media posts. Analyze the sentiment of the text and then store the results in Weaviate for searching based on sentiment scores. + +- **Real-time Alerting System**: Implement a system that monitors and searches new data entries in Weaviate for specific criteria. When a match is found, trigger notifications or actions through apps like Slack, email, or SMS. This could be used for monitoring brand mentions, compliance, or other critical data points. diff --git a/components/weaviate/package.json b/components/weaviate/package.json new file mode 100644 index 0000000000000..69219b7e928bc --- /dev/null +++ b/components/weaviate/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/weaviate", + "version": "0.0.1", + "description": "Pipedream Weaviate Components", + "main": "weaviate.app.mjs", + "keywords": [ + "pipedream", + "weaviate" + ], + "homepage": "https://pipedream.com/apps/weaviate", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/weaviate/weaviate.app.mjs b/components/weaviate/weaviate.app.mjs new file mode 100644 index 0000000000000..c0bc9cc789d62 --- /dev/null +++ b/components/weaviate/weaviate.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "weaviate", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/webflow/README.md b/components/webflow/README.md index feea7e8d37348..e7cb48f07c1ef 100644 --- a/components/webflow/README.md +++ b/components/webflow/README.md @@ -1,11 +1,16 @@ # Overview -With the Webflow API, you can build a number of different applications that can -help you manage your website more effectively. Some examples of applications -that can be built using the Webflow API include: - -- A website management tool that can help you keep track of your website's - content and performance -- A tool that can help you manage your website's SEO -- A tool that can help you monitor your website's traffic -- A tool that can help you track your website's conversion rate +The Webflow API empowers developers to programmatically interact with their Webflow site, enabling automation, data synchronization, and complex integrations with other apps and services. With Pipedream's serverless platform, you can harness this API to craft custom workflows that react to events, manage content dynamically, or extend the capabilities of your Webflow projects by linking them with a vast array of other applications. + +You can connect your Webflow account in a few different ways: +1. Directly within the workflow builder, from a trigger or an action step. Search for the Webflow app, and connect your account. +2. From the Pipedream Accounts [page](https://pipedream.com/accounts): Go to Accounts > Connect an app > Search for "Webflow". +3. Run the example Node JS code below by clicking "Connect Webflow and run", and choose a project and create an example workflow to interact with the Webflow API. + +# Example Use Cases + +- **Content Sync from CMS to Webflow**: Automatically update your Webflow site when a new post is added to a headless CMS like Contentful. Once the CMS triggers an event, Pipedream can capture it and push the content to Webflow, ensuring your site remains up-to-date without manual intervention. + +- **E-commerce Order Processing**: When a new order is placed on your Webflow e-commerce site, use Pipedream to capture the order details and integrate with an app like QuickBooks to handle invoicing and accounting. This streamlines the order-to-cash process, keeping financials in sync. + +- **Customer Support Ticket Creation**: Connect Webflow form submissions to a customer support platform like Zendesk. Whenever a form is submitted on your Webflow site, Pipedream can automatically create a support ticket, helping your support team stay efficient and responsive. diff --git a/components/webflow/actions/create-collection-item/create-collection-item.mjs b/components/webflow/actions/create-collection-item/create-collection-item.mjs index cf3c48fda0cd5..46f983614c632 100644 --- a/components/webflow/actions/create-collection-item/create-collection-item.mjs +++ b/components/webflow/actions/create-collection-item/create-collection-item.mjs @@ -1,61 +1,71 @@ -import webflow from "../../webflow.app.mjs"; +import app from "../../webflow.app.mjs"; export default { key: "webflow-create-collection-item", name: "Create Collection Item", - description: "Create new collection item. [See the docs here](https://developers.webflow.com/#create-new-collection-item)", - version: "0.1.6", + description: "Create new collection item. [See the documentation](https://developers.webflow.com/data/reference/cms/collection-items/staged-items/create-item)", + version: "2.0.0", type: "action", props: { - webflow, + app, siteId: { propDefinition: [ - webflow, + app, "sites", ], }, collectionId: { propDefinition: [ - webflow, + app, "collections", (c) => ({ siteId: c.siteId, }), ], + reloadProps: true, }, - live: { - label: "Live", - description: "Indicate if the item should be published to the live site", - type: "boolean", - default: false, - }, - name: { - label: "Name", - description: "Name given to the Item.", - type: "string", - }, - slug: { - label: "Slug", - description: "URL structure of the Item in your site.", - type: "string", - }, + }, + async additionalProps() { + const props = {}; + if (!this.collectionId) { + return props; + } + const { fields } = await this.app.getCollection(this.collectionId); + for (const field of fields) { + if (field.isEditable && field.slug !== "isArchived" && field.slug !== "isDraft") { + props[field.slug] = { + type: "string", + label: field.name, + description: field.slug === "name" + ? "Name given to the Item." + : field.slug === "slug" + ? "URL structure of the Item in your site." + : "See the documentation for additional information about [Field Types & Item Values](https://developers.webflow.com/reference/field-types-item-values).", + optional: !field.isRequired, + }; + } + } + return props; }, async run({ $ }) { - const webflow = this.webflow._createApiClient(); + const { + app, + // eslint-disable-next-line no-unused-vars + siteId, + collectionId, + ...fieldData + } = this; - const response = await webflow.createItem({ - collectionId: this.collectionId, - fields: { - name: this.name, - slug: this.slug, - _archived: false, - _draft: false, + const response = await app.createCollectionItem( + collectionId, + { + fieldData, + isArchived: false, + isDraft: false, }, - }, { - live: this.live, - }); + ); - $.export("$summary", `Successfully created collection item ${this.name}`); + $.export("$summary", `Successfully created collection item ${this.name ?? ""}`); return response; }, diff --git a/components/webflow/actions/delete-collection-item/delete-collection-item.mjs b/components/webflow/actions/delete-collection-item/delete-collection-item.mjs index cf329275da710..63d1deb609e0e 100644 --- a/components/webflow/actions/delete-collection-item/delete-collection-item.mjs +++ b/components/webflow/actions/delete-collection-item/delete-collection-item.mjs @@ -1,22 +1,22 @@ -import webflow from "../../webflow.app.mjs"; +import app from "../../webflow.app.mjs"; export default { key: "webflow-delete-collection-item", name: "Delete Collection Item", - description: "Delete Item of a Collection. [See the docs here](https://developers.webflow.com/#remove-collection-item)", - version: "0.0.5", + description: "Delete Item of a Collection. [See the documentation](https://developers.webflow.com/data/reference/cms/collection-items/staged-items/delete-item)", + version: "2.0.0", type: "action", props: { - webflow, + app, siteId: { propDefinition: [ - webflow, + app, "sites", ], }, collectionId: { propDefinition: [ - webflow, + app, "collections", (c) => ({ siteId: c.siteId, @@ -25,7 +25,7 @@ export default { }, itemId: { propDefinition: [ - webflow, + app, "items", (c) => ({ collectionId: c.collectionId, @@ -34,12 +34,10 @@ export default { }, }, async run({ $ }) { - const webflow = this.webflow._createApiClient(); - - const response = await webflow.removeItem({ - collectionId: this.collectionId, - itemId: this.itemId, - }); + const { + collectionId, itemId, + } = this; + const response = await this.app.deleteCollectionItem(collectionId, itemId); $.export("$summary", "Successfully deleted item"); diff --git a/components/webflow/actions/fulfill-order/fulfill-order.mjs b/components/webflow/actions/fulfill-order/fulfill-order.mjs index 860cd41233a97..7d5580f580134 100644 --- a/components/webflow/actions/fulfill-order/fulfill-order.mjs +++ b/components/webflow/actions/fulfill-order/fulfill-order.mjs @@ -1,22 +1,22 @@ -import webflow from "../../webflow.app.mjs"; +import app from "../../webflow.app.mjs"; export default { key: "webflow-fulfill-order", name: "Fulfill Order", - description: "Fulfill an order. [See the docs here](https://developers.webflow.com/#fulfill-order)", - version: "0.0.4", + description: "Fulfill an order. [See the documentation](https://developers.webflow.com/data/reference/ecommerce/orders/update-fulfill)", + version: "2.0.0", type: "action", props: { - webflow, + app, siteId: { propDefinition: [ - webflow, + app, "sites", ], }, orderId: { propDefinition: [ - webflow, + app, "orders", ], }, @@ -28,13 +28,10 @@ export default { }, }, async run({ $ }) { - const apiClient = this.webflow._createApiClient(); - - const response = await apiClient.post(`/sites/${this.siteId}/order/${this.orderId}/fulfill`, { - data: { - sendOrderFulfilledEmail: this.sendOrderFulfilledEmail, - }, - }); + const { + app, siteId, orderId, ...data + } = this; + const response = await app.fulfillOrder(siteId, orderId, data); $.export("$summary", "Successfully fulfilled order"); diff --git a/components/webflow/actions/get-collection-item/get-collection-item.mjs b/components/webflow/actions/get-collection-item/get-collection-item.mjs index 55875f7e15b38..c5a279f08fd2b 100644 --- a/components/webflow/actions/get-collection-item/get-collection-item.mjs +++ b/components/webflow/actions/get-collection-item/get-collection-item.mjs @@ -1,22 +1,22 @@ -import webflow from "../../webflow.app.mjs"; +import app from "../../webflow.app.mjs"; export default { key: "webflow-get-collection-item", name: "Get Collection Item", - description: "Get a Collection Item. [See the docs here](https://developers.webflow.com/#get-single-item)", - version: "0.1.7", + description: "Get a Collection Item. [See the documentation](https://developers.webflow.com/data/reference/cms/collection-items/staged-items/get-item)", + version: "2.0.0", type: "action", props: { - webflow, + app, siteId: { propDefinition: [ - webflow, + app, "sites", ], }, collectionId: { propDefinition: [ - webflow, + app, "collections", (c) => ({ siteId: c.siteId, @@ -25,7 +25,7 @@ export default { }, itemId: { propDefinition: [ - webflow, + app, "items", (c) => ({ collectionId: c.collectionId, @@ -34,12 +34,7 @@ export default { }, }, async run({ $ }) { - const webflow = this.webflow._createApiClient(); - - const response = await webflow.item({ - collectionId: this.collectionId, - itemId: this.itemId, - }); + const response = await this.app.getCollectionItem(this.collectionId, this.itemId); $.export("$summary", "Successfully retrieved collection item"); diff --git a/components/webflow/actions/get-collection/get-collection.mjs b/components/webflow/actions/get-collection/get-collection.mjs index 6464bb1601ee8..0bac66a1a2a8d 100644 --- a/components/webflow/actions/get-collection/get-collection.mjs +++ b/components/webflow/actions/get-collection/get-collection.mjs @@ -1,22 +1,22 @@ -import webflow from "../../webflow.app.mjs"; +import app from "../../webflow.app.mjs"; export default { key: "webflow-get-collection", name: "Get Collection", - description: "Get a collection. [See the docs here](https://developers.webflow.com/#get-collection-with-full-schema)", - version: "0.0.5", + description: "Get a collection. [See the documentation](https://developers.webflow.com/data/reference/cms/collections/get)", + version: "2.0.0", type: "action", props: { - webflow, + app, siteId: { propDefinition: [ - webflow, + app, "sites", ], }, collectionId: { propDefinition: [ - webflow, + app, "collections", (c) => ({ siteId: c.siteId, @@ -25,7 +25,7 @@ export default { }, }, async run({ $ }) { - const response = await this.webflow.getCollection(this.collectionId); + const response = await this.app.getCollection(this.collectionId); $.export("$summary", "Successfully retrieved collection"); diff --git a/components/webflow/actions/get-item-inventory/get-item-inventory.mjs b/components/webflow/actions/get-item-inventory/get-item-inventory.mjs index 5dace536d6139..cc5f3469efc76 100644 --- a/components/webflow/actions/get-item-inventory/get-item-inventory.mjs +++ b/components/webflow/actions/get-item-inventory/get-item-inventory.mjs @@ -1,22 +1,22 @@ -import webflow from "../../webflow.app.mjs"; +import app from "../../webflow.app.mjs"; export default { key: "webflow-get-item-inventory", name: "Get Item Inventory", - description: "Get the inventory of a specific item. [See the docs here](https://developers.webflow.com/#item-inventory)", - version: "0.0.5", + description: "Get the inventory of a specific item. [See the documentation](https://developers.webflow.com/data/reference/ecommerce/inventory/list)", + version: "2.0.0", type: "action", props: { - webflow, + app, siteId: { propDefinition: [ - webflow, + app, "sites", ], }, collectionId: { propDefinition: [ - webflow, + app, "collections", (c) => ({ siteId: c.siteId, @@ -25,7 +25,7 @@ export default { }, itemId: { propDefinition: [ - webflow, + app, "items", (c) => ({ collectionId: c.collectionId, @@ -34,9 +34,7 @@ export default { }, }, async run({ $ }) { - const apiClient = this.webflow._createApiClient(); - - const response = await apiClient.apiClient.get(`/collections/${this.collectionId}/items/${this.itemId}/inventory`); + const response = await this.app.getCollectionItemInventory(this.collectionId, this.itemId); $.export("$summary", "Successfully retrieved item inventory"); diff --git a/components/webflow/actions/get-order/get-order.mjs b/components/webflow/actions/get-order/get-order.mjs index 25785b53fbfb1..f33a1a1478f06 100644 --- a/components/webflow/actions/get-order/get-order.mjs +++ b/components/webflow/actions/get-order/get-order.mjs @@ -1,31 +1,28 @@ -import webflow from "../../webflow.app.mjs"; +import app from "../../webflow.app.mjs"; export default { key: "webflow-get-order", name: "Get Order", - description: "Get an order. [See the docs here](https://developers.webflow.com/#get-order)", - version: "0.0.4", + description: "Get info on an order. [See the documentation](https://developers.webflow.com/data/reference/ecommerce/orders/get)", + version: "2.0.0", type: "action", props: { - webflow, + app, siteId: { propDefinition: [ - webflow, + app, "sites", ], }, orderId: { propDefinition: [ - webflow, + app, "orders", ], }, }, async run({ $ }) { - const response = await this.webflow.getOrder({ - siteId: this.siteId, - orderId: this.orderId, - }); + const response = await this.app.getOrder(this.siteId, this.orderId); $.export("$summary", "Successfully retrieved order"); diff --git a/components/webflow/actions/get-site/get-site.mjs b/components/webflow/actions/get-site/get-site.mjs index 2af7c05bdbad0..0f306c77db22e 100644 --- a/components/webflow/actions/get-site/get-site.mjs +++ b/components/webflow/actions/get-site/get-site.mjs @@ -1,22 +1,22 @@ -import webflow from "../../webflow.app.mjs"; +import app from "../../webflow.app.mjs"; export default { key: "webflow-get-site", name: "Get Site", - description: "Get a site. [See the docs here](https://developers.webflow.com/#get-specific-site)", - version: "0.0.4", + description: "Get a site. [See the documentation](https://developers.webflow.com/data/reference/sites/get)", + version: "2.0.0", type: "action", props: { - webflow, + app, siteId: { propDefinition: [ - webflow, + app, "sites", ], }, }, async run({ $ }) { - const response = await this.webflow.getSite(this.siteId); + const response = await this.app.getSite(this.siteId); $.export("$summary", "Successfully retrieved site"); diff --git a/components/webflow/actions/list-collection-items/list-collection-items.mjs b/components/webflow/actions/list-collection-items/list-collection-items.mjs index 12bcf43885ce3..eb1bde0e045c9 100644 --- a/components/webflow/actions/list-collection-items/list-collection-items.mjs +++ b/components/webflow/actions/list-collection-items/list-collection-items.mjs @@ -1,22 +1,22 @@ -import webflow from "../../webflow.app.mjs"; +import app from "../../webflow.app.mjs"; export default { key: "webflow-list-collection-items", name: "List Collection Items", - description: "List Items of a collection. [See the docs here](https://developers.webflow.com/#get-all-items-for-a-collection)", - version: "0.0.5", + description: "List Items of a collection. [See the documentation](https://developers.webflow.com/data/reference/cms/collection-items/bulk-items/list-items)", + version: "2.0.0", type: "action", props: { - webflow, + app, siteId: { propDefinition: [ - webflow, + app, "sites", ], }, collectionId: { propDefinition: [ - webflow, + app, "collections", (c) => ({ siteId: c.siteId, @@ -25,9 +25,9 @@ export default { }, }, async run({ $ }) { - const response = await this.webflow.getItems(0, this.collectionId); + const response = await this.app.listCollectionItems(0, this.collectionId); - $.export("$summary", "Successfully retrieved collections items"); + $.export("$summary", "Successfully retrieved collection's items"); return response; }, diff --git a/components/webflow/actions/list-collections/list-collections.mjs b/components/webflow/actions/list-collections/list-collections.mjs index 84077c56f1f6d..3b65a5d6b1caa 100644 --- a/components/webflow/actions/list-collections/list-collections.mjs +++ b/components/webflow/actions/list-collections/list-collections.mjs @@ -1,22 +1,22 @@ -import webflow from "../../webflow.app.mjs"; +import app from "../../webflow.app.mjs"; export default { key: "webflow-list-collections", name: "List Collections", - description: "List collections. [See the docs here](https://developers.webflow.com/#list-collections)", - version: "0.0.4", + description: "List collections. [See the documentation](https://developers.webflow.com/data/reference/cms/collections/list)", + version: "2.0.0", type: "action", props: { - webflow, + app, siteId: { propDefinition: [ - webflow, + app, "sites", ], }, }, async run({ $ }) { - const response = await this.webflow.getCollections(this.siteId); + const response = await this.app.listCollections(this.siteId); $.export("$summary", "Successfully retrieved collections"); diff --git a/components/webflow/actions/list-orders/list-orders.mjs b/components/webflow/actions/list-orders/list-orders.mjs index 2d489ebeca791..bba6e64124f20 100644 --- a/components/webflow/actions/list-orders/list-orders.mjs +++ b/components/webflow/actions/list-orders/list-orders.mjs @@ -1,35 +1,35 @@ -import webflow from "../../webflow.app.mjs"; +import app from "../../webflow.app.mjs"; import constants from "../common/constants.mjs"; export default { key: "webflow-list-orders", name: "List Orders", - description: "List orders. [See the docs here](https://developers.webflow.com/#get-all-orders)", - version: "0.0.4", + description: "List orders. [See the documentation](https://developers.webflow.com/data/reference/ecommerce/orders/list)", + version: "2.0.0", type: "action", props: { - webflow, + app, siteId: { propDefinition: [ - webflow, + app, "sites", ], }, status: { label: "Status", - description: "The status to filter the orders.", + description: "If specified, only orders with this status will be listed.", type: "string", options: constants.ORDER_STATUSES, optional: true, }, }, async run({ $ }) { - const response = await this.webflow.getOrders({ - siteId: this.siteId, - status: this.status, - }); + const { + app, ...data + } = this; + const response = await app.listOrders(data); - $.export("$summary", "Successfully retrieved orders"); + $.export("$summary", `Successfully retrieved ${response?.length} orders`); return response; }, diff --git a/components/webflow/actions/list-sites/list-sites.mjs b/components/webflow/actions/list-sites/list-sites.mjs index 6ef0ec3ef218d..850fad3e4b9a1 100644 --- a/components/webflow/actions/list-sites/list-sites.mjs +++ b/components/webflow/actions/list-sites/list-sites.mjs @@ -1,16 +1,16 @@ -import webflow from "../../webflow.app.mjs"; +import app from "../../webflow.app.mjs"; export default { key: "webflow-list-sites", name: "List Sites", - description: "List sites. [See the docs here](https://developers.webflow.com/#list-sites)", - version: "0.0.4", + description: "List sites. [See the documentation](https://developers.webflow.com/data/reference/sites/list)", + version: "2.0.0", type: "action", props: { - webflow, + app, }, async run({ $ }) { - const response = await this.webflow.getSites(); + const response = await this.app.listSites(); $.export("$summary", "Successfully retrieved sites"); diff --git a/components/webflow/actions/publish-site/publish-site.mjs b/components/webflow/actions/publish-site/publish-site.mjs index 152c3f3162bd1..3a55cdec20a51 100644 --- a/components/webflow/actions/publish-site/publish-site.mjs +++ b/components/webflow/actions/publish-site/publish-site.mjs @@ -1,22 +1,22 @@ -import webflow from "../../webflow.app.mjs"; +import app from "../../webflow.app.mjs"; export default { key: "webflow-publish-site", name: "Publish Site", - description: "Get a site in a specific domain. [See the docs here](https://developers.webflow.com/#publish-site)", - version: "0.0.4", + description: "Publish a site. [See the documentation](https://developers.webflow.com/data/reference/sites/publish)", + version: "2.0.0", type: "action", props: { - webflow, + app, siteId: { propDefinition: [ - webflow, + app, "sites", ], }, domains: { propDefinition: [ - webflow, + app, "domains", (c) => ({ siteId: c.siteId, @@ -25,12 +25,7 @@ export default { }, }, async run({ $ }) { - const webflow = this.webflow._createApiClient(); - - const response = await webflow.publishSite({ - siteId: this.siteId, - domains: this.domains, - }); + const response = await this.app.publishSite(this.siteId, this.domains); $.export("$summary", "Successfully published site"); diff --git a/components/webflow/actions/refund-order/refund-order.mjs b/components/webflow/actions/refund-order/refund-order.mjs index d5b9e536072a8..b35dcd6f9d5d6 100644 --- a/components/webflow/actions/refund-order/refund-order.mjs +++ b/components/webflow/actions/refund-order/refund-order.mjs @@ -1,30 +1,28 @@ -import webflow from "../../webflow.app.mjs"; +import app from "../../webflow.app.mjs"; export default { key: "webflow-refund-order", name: "Refund Order", - description: "Refund an order. [See the docs here](https://developers.webflow.com/#refund-order)", - version: "0.0.4", + description: "Refund an order. [See the documentation](https://developers.webflow.com/data/reference/ecommerce/orders/refund)", + version: "2.0.0", type: "action", props: { - webflow, + app, siteId: { propDefinition: [ - webflow, + app, "sites", ], }, orderId: { propDefinition: [ - webflow, + app, "orders", ], }, }, async run({ $ }) { - const apiClient = this.webflow._createApiClient(); - - const response = apiClient.get(`/sites/${this.siteId}/order/${this.orderId}/refund`); + const response = await this.app.refundOrder(this.siteId, this.orderId); $.export("$summary", "Successfully refunded order"); diff --git a/components/webflow/actions/unfulfill-order/unfulfill-order.mjs b/components/webflow/actions/unfulfill-order/unfulfill-order.mjs index 040d6b762ee40..af4bbfafe859a 100644 --- a/components/webflow/actions/unfulfill-order/unfulfill-order.mjs +++ b/components/webflow/actions/unfulfill-order/unfulfill-order.mjs @@ -1,30 +1,28 @@ -import webflow from "../../webflow.app.mjs"; +import app from "../../webflow.app.mjs"; export default { key: "webflow-unfulfill-order", name: "Unfulfill Order", - description: "Unfulfill an order. [See the docs here](https://developers.webflow.com/#unfulfill-order)", - version: "0.0.4", + description: "Unfulfill an order. [See the documentation](https://developers.webflow.com/data/reference/ecommerce/orders/update-unfulfill)", + version: "2.0.0", type: "action", props: { - webflow, + app, siteId: { propDefinition: [ - webflow, + app, "sites", ], }, orderId: { propDefinition: [ - webflow, + app, "orders", ], }, }, async run({ $ }) { - const apiClient = this.webflow._createApiClient(); - - const response = apiClient.post(`/sites/${this.siteId}/order/${this.orderId}/unfulfill`); + const response = await this.app.unfulfillOrder(this.siteId, this.orderId); $.export("$summary", "Successfully unfulfilled order"); diff --git a/components/webflow/actions/update-collection-item/update-collection-item.mjs b/components/webflow/actions/update-collection-item/update-collection-item.mjs index 5ad131a1f4313..e1c980d96c23f 100644 --- a/components/webflow/actions/update-collection-item/update-collection-item.mjs +++ b/components/webflow/actions/update-collection-item/update-collection-item.mjs @@ -1,37 +1,38 @@ -import webflow from "../../webflow.app.mjs"; +import app from "../../webflow.app.mjs"; export default { key: "webflow-update-collection-item", name: "Update Collection Item", - description: "Update collection item. [See the documentation](https://developers.webflow.com/#update-collection-item)", - version: "0.1.7", + description: + "Update collection item. [See the documentation](https://developers.webflow.com/data/reference/cms/collection-items/bulk-items/update-items)", + version: "2.0.0", type: "action", props: { - webflow, + app, siteId: { propDefinition: [ - webflow, + app, "sites", ], }, collectionId: { propDefinition: [ - webflow, + app, "collections", (c) => ({ siteId: c.siteId, }), ], - reloadProps: true, }, itemId: { propDefinition: [ - webflow, + app, "items", (c) => ({ collectionId: c.collectionId, }), ], + reloadProps: true, }, }, async additionalProps() { @@ -39,17 +40,22 @@ export default { if (!this.collectionId) { return props; } - const { fields } = await this.webflow.getCollection(this.collectionId); + const { fields } = await this.app.getCollection(this.collectionId); for (const field of fields) { - if (field.editable && field.slug !== "_archived" && field.slug !== "_draft") { + if ( + field.isEditable && + field.slug !== "isArchived" && + field.slug !== "isDraft" + ) { props[field.slug] = { type: "string", label: field.name, - description: field.slug === "name" - ? "Name given to the Item." - : field.slug === "slug" - ? "URL structure of the Item in your site. Note: Updates to an item slug will break all links referencing the old slug." - : "See the documentation for additional information about [Field Types & Item Values](https://developers.webflow.com/reference/field-types-item-values).", + description: + field.slug === "name" + ? "Name given to the Item." + : field.slug === "slug" + ? "URL structure of the Item in your site. Note: Updates to an item slug will break all links referencing the old slug." + : "See the documentation for additional information about [Field Types & Item Values](https://developers.webflow.com/reference/field-types-item-values).", optional: true, }; } @@ -59,7 +65,7 @@ export default { }, async run({ $ }) { const { - webflow, + app, // eslint-disable-next-line no-unused-vars siteId, collectionId, @@ -69,21 +75,17 @@ export default { ...customFields } = this; - const webflowClient = webflow._createApiClient(); - - const item = await webflowClient.item({ - collectionId, - itemId, - }); + const item = await app.getCollectionItem(collectionId, itemId); - const response = await webflowClient.updateItem({ - collectionId, - itemId, - name: name || item.name, - slug: slug || item.slug, - _archived: false, - _draft: false, - ...customFields, + const response = await app.updateCollectionItem(collectionId, itemId, { + id: itemId, + isArchived: false, + isDraft: false, + fieldData: { + ...customFields, + name: name || item.fieldData.name, + slug: slug || item.fieldData.slug, + }, }); $.export("$summary", "Successfully updated collection item"); diff --git a/components/webflow/actions/update-item-inventory/update-item-inventory.mjs b/components/webflow/actions/update-item-inventory/update-item-inventory.mjs index 443d8ddc897aa..e902028d75233 100644 --- a/components/webflow/actions/update-item-inventory/update-item-inventory.mjs +++ b/components/webflow/actions/update-item-inventory/update-item-inventory.mjs @@ -1,22 +1,22 @@ -import webflow from "../../webflow.app.mjs"; +import app from "../../webflow.app.mjs"; export default { key: "webflow-update-item-inventory", name: "Update Item Inventory", - description: "Update the inventory of a specific item. [See the docs here](https://developers.webflow.com/#update-item-inventory)", - version: "0.0.5", + description: "Update the inventory of a specific item. [See the documentation](https://developers.webflow.com/data/reference/ecommerce/inventory/update)", + version: "2.0.0", type: "action", props: { - webflow, + app, siteId: { propDefinition: [ - webflow, + app, "sites", ], }, collectionId: { propDefinition: [ - webflow, + app, "collections", (c) => ({ siteId: c.siteId, @@ -25,7 +25,7 @@ export default { }, itemId: { propDefinition: [ - webflow, + app, "items", (c) => ({ collectionId: c.collectionId, @@ -43,35 +43,28 @@ export default { }, quantity: { label: "Quantity", - description: "The quantity will be seted with this value. This just can be used with `finite` option selected and without `updateQuantity` value.", + description: "If specified, sets quantity to this value. Can only be used with the `finite` inventory type, and if `Update Quantity` is not specified.", type: "integer", optional: true, }, updateQuantity: { label: "Update Quantity", - description: "This value will be added to the quantity. This just can be used with `finite` option selected and without `quantity` value.", + description: "If specified, adds this value to the current quantity. Can only be used with the `finite` inventory type, and if `Quantity` is not specified.", type: "integer", optional: true, }, }, async run({ $ }) { - const apiClient = this.webflow._createApiClient(); - const { - inventoryType, - quantity, - updateQuantity, + app, + // eslint-disable-next-line no-unused-vars + siteId, + collectionId, + itemId, + ...data } = this; - const response = await apiClient.patch(`/collections/${this.collectionId}/items/${this.itemId}/inventory`, { - data: { - fields: { - inventoryType, - quantity, - updateQuantity, - }, - }, - }); + const response = await app.updateCollectionItemInventory(collectionId, itemId, data); $.export("$summary", "Successfully updated item inventory"); diff --git a/components/webflow/actions/update-order/update-order.mjs b/components/webflow/actions/update-order/update-order.mjs index 74a22137dcb1b..10bd3c2b2ecc9 100644 --- a/components/webflow/actions/update-order/update-order.mjs +++ b/components/webflow/actions/update-order/update-order.mjs @@ -1,22 +1,22 @@ -import webflow from "../../webflow.app.mjs"; +import app from "../../webflow.app.mjs"; export default { key: "webflow-update-order", name: "Update Order", - description: "Update an order. [See the docs here](https://developers.webflow.com/#update-order)", - version: "0.0.4", + description: "Update an order. [See the documentation](https://developers.webflow.com/data/reference/ecommerce/orders/update)", + version: "2.0.0", type: "action", props: { - webflow, + app, siteId: { propDefinition: [ - webflow, + app, "sites", ], }, orderId: { propDefinition: [ - webflow, + app, "orders", ], }, @@ -40,23 +40,11 @@ export default { }, }, async run({ $ }) { - const apiClient = this.webflow._createApiClient(); - const { - comment, - shippingProvider, - shippingTracking, + app, siteId, orderId, ...data } = this; - const response = await apiClient.post(`/sites/${this.siteId}/order/${this.orderId}`, { - data: { - fields: { - comment, - shippingProvider, - shippingTracking, - }, - }, - }); + const response = await app.updateOrder(siteId, orderId, data); $.export("$summary", "Successfully updated order"); diff --git a/components/webflow/package.json b/components/webflow/package.json index f7536f2551a16..2db71e58b1bd1 100644 --- a/components/webflow/package.json +++ b/components/webflow/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/webflow", - "version": "0.4.5", + "version": "1.0.1", "description": "Pipedream Webflow Components", "main": "webflow.app.mjs", "keywords": [ @@ -10,10 +10,9 @@ "homepage": "https://pipedream.com/apps/webflow", "author": "Pipedream (https://pipedream.com/)", "dependencies": { - "@pipedream/platform": "^1.1.0", - "webflow-api": "1.3.1" + "@pipedream/platform": "^3.0.3", + "webflow-api": "2.4.2" }, - "gitHead": "e12480b94cc03bed4808ebc6b13e7fdb3a1ba535", "publishConfig": { "access": "public" } diff --git a/components/webflow/sources/changed-collection-item/changed-collection-item.mjs b/components/webflow/sources/changed-collection-item/changed-collection-item.mjs index 0e818f3537291..6506124f24a01 100644 --- a/components/webflow/sources/changed-collection-item/changed-collection-item.mjs +++ b/components/webflow/sources/changed-collection-item/changed-collection-item.mjs @@ -1,11 +1,11 @@ -import common from "../common/collection-common.mjs"; +import common from "../common/common.mjs"; export default { type: "source", key: "webflow-changed-collection-item", - name: "New Changed Collection Item", - description: "Emit new event when a collection item is changed. [See the docs here](https://developers.webflow.com/#model16)", - version: "0.2.3", + name: "Collection Item Updated", + description: "Emit new event when a collection item is changed. [See the documentation](https://developers.webflow.com/data/reference/webhooks/events/collection-item-changed)", + version: "2.0.0", ...common, methods: { ...common.methods, @@ -13,11 +13,14 @@ export default { return "collection_item_changed"; }, generateMeta(data) { - const ts = Date.parse(data["updated-on"]); + const { + id, fieldData, lastUpdated, + } = data; + const ts = Date.parse(lastUpdated); return { - id: `${data._id}-${ts}`, - summary: `Collection ${data.slug} item changed`, + id: `${id}-${ts}`, + summary: `Item updated: ${fieldData?.slug ?? fieldData?.name ?? id}`, ts, }; }, diff --git a/components/webflow/sources/changed-ecommerce-inventory/changed-ecommerce-inventory.mjs b/components/webflow/sources/changed-ecommerce-inventory/changed-ecommerce-inventory.mjs index 8e04702ce88b6..60fc751434fbf 100644 --- a/components/webflow/sources/changed-ecommerce-inventory/changed-ecommerce-inventory.mjs +++ b/components/webflow/sources/changed-ecommerce-inventory/changed-ecommerce-inventory.mjs @@ -3,9 +3,9 @@ import common from "../common/common.mjs"; export default { type: "source", key: "webflow-changed-ecommerce-inventory", - name: "New Changed E-commerce Inventory", - description: "Emit new event when an e-commerce inventory level changes. [See the docs here](https://developers.webflow.com/#item-inventory)", - version: "0.2.3", + name: "E-commerce Inventory Updated", + description: "Emit new event when an e-commerce inventory level changes. [See the documentation](https://developers.webflow.com/data/reference/webhooks/events/ecomm-inventory-changed)", + version: "2.0.0", ...common, methods: { ...common.methods, @@ -13,12 +13,13 @@ export default { return "ecomm_inventory_changed"; }, generateMeta(data) { - const now = Date.now(); + const ts = Date.now(); + const { id } = data; return { - id: `${data._id}-${now}`, - summary: `E-commerce ${data._id} inventory changed`, - ts: now, + id: `${id}-${ts}`, + summary: `E-comm inventory updated: ${id}`, + ts, }; }, }, diff --git a/components/webflow/sources/changed-ecommerce-order/changed-ecommerce-order.mjs b/components/webflow/sources/changed-ecommerce-order/changed-ecommerce-order.mjs index 6f1a80be6af21..c54f17dd383f5 100644 --- a/components/webflow/sources/changed-ecommerce-order/changed-ecommerce-order.mjs +++ b/components/webflow/sources/changed-ecommerce-order/changed-ecommerce-order.mjs @@ -3,22 +3,21 @@ import common from "../common/common.mjs"; export default { type: "source", key: "webflow-changed-ecommerce-order", - name: "New Changed E-commerce Order", - description: "Emit new event when an e-commerce order is changed. [See the docs here](https://developers.webflow.com/#order-model)", - version: "0.2.3", + name: "E-commerce Order Updated", + description: "Emit new event when an e-commerce order is changed. [See the documentation](https://developers.webflow.com/data/reference/webhooks/events/ecomm-order-changed)", + version: "2.0.0", ...common, methods: { ...common.methods, getWebhookTriggerType() { return "ecomm_order_changed"; }, - generateMeta(data) { - const now = Date.now(); - + generateMeta({ orderId }) { + const ts = Date.now(); return { - id: `${data.orderId}-${now}`, - summary: `E-commerce order ${data.orderId} changed`, - ts: now, + id: `${orderId}-${ts}`, + summary: `E-comm order updated: ${orderId}`, + ts, }; }, }, diff --git a/components/webflow/sources/common/common.mjs b/components/webflow/sources/common/common.mjs index 8bbc816812e0f..880a0ea6fadc2 100644 --- a/components/webflow/sources/common/common.mjs +++ b/components/webflow/sources/common/common.mjs @@ -1,15 +1,14 @@ -import webflow from "../../webflow.app.mjs"; +import app from "../../webflow.app.mjs"; import { v4 as uuid } from "uuid"; -import { axios } from "@pipedream/platform"; import constants from "../../common/constants.mjs"; export default { dedupe: "unique", props: { - webflow, + app, siteId: { propDefinition: [ - webflow, + app, "sites", ], }, @@ -17,16 +16,6 @@ export default { http: "$.interface.http", }, methods: { - async _makeRequest(path, params = {}) { - return axios(this, { - url: "https://api.webflow.com" + path, - headers: { - "Authorization": `Bearer ${this.webflow.$auth.oauth_access_token}`, - "Accept-Version": "1.0.0", - }, - params, - }); - }, _getWebhookId() { return this.db.get("webhookId"); }, @@ -36,12 +25,6 @@ export default { getWebhookTriggerType() { throw new Error("getWebhookTriggerType is not implemented"); }, - getWebhookFilter() { - return {}; - }, - isEventRelevant(event) { - if (event) return true; - }, generateMeta(data) { return { id: data.id || uuid(), @@ -50,13 +33,9 @@ export default { }; }, processEvent(event) { - if (!this.isEventRelevant(event)) { - return; - } - - const { body } = event; - const meta = this.generateMeta(body); - this.$emit(body, meta); + const { body: { payload } } = event; + const meta = this.generateMeta(payload); + this.$emit(payload, meta); }, emitHistoricalEvents(events, limit = constants.DEPLOY_OFFSET) { for (const event of events.slice(0, limit)) { @@ -67,18 +46,15 @@ export default { }, hooks: { async activate() { - const { endpoint } = this.http; - const triggerType = this.getWebhookTriggerType(); - const filter = this.getWebhookFilter(); - const webhook = await this.webflow.createWebhook( - this.siteId, endpoint, triggerType, filter, - ); + const webhook = await this.app.createWebhook(this.siteId, { + url: this.http.endpoint, + triggerType: this.getWebhookTriggerType(), + }); - this._setWebhookId(webhook._id); + this._setWebhookId(webhook?.id); }, async deactivate() { - const webhookId = this._getWebhookId(); - await this.webflow.removeWebhook(this.siteId, webhookId); + await this.app.removeWebhook(this._getWebhookId()); }, }, async run(event) { diff --git a/components/webflow/sources/new-collection-item/new-collection-item.mjs b/components/webflow/sources/new-collection-item/new-collection-item.mjs index 5364ff0f38e22..259286fad8250 100644 --- a/components/webflow/sources/new-collection-item/new-collection-item.mjs +++ b/components/webflow/sources/new-collection-item/new-collection-item.mjs @@ -1,56 +1,25 @@ -import constants from "../../common/constants.mjs"; -import common from "../common/collection-common.mjs"; +import common from "../common/common.mjs"; export default { type: "source", key: "webflow-new-collection-item", - name: "New Collection Item", - description: "Emit new event when a collection item is created. [See the docs here](https://developers.webflow.com/#item-model)", - version: "0.2.3", + name: "New Collection Item Created", + description: "Emit new event when a collection item is created. [See the documentation](https://developers.webflow.com/data/reference/webhooks/events/collection-item-created)", + version: "2.0.0", ...common, - hooks: { - ...common.hooks, - async deploy() { - if (this.collectionIds?.length !== 1) { - console.log("Skipping retrieval of historical events for multiple or no Collection ID"); - return; - } - - const path = `/collections/${this.collectionIds[0]}/items`; - console.log("Retrieving historical events..."); - - let { - total, - items: events, - } = await this._makeRequest(path); - - if (total > constants.LIMIT) { - const offset = Math.floor(total / constants.LIMIT); - - events = (await this._makeRequest(path, { - offset, - })).items.reverse(); - - events.push(...(await this._makeRequest(path, { - offset: offset - 1, - })).items.reverse()); - } else { - events.reverse(); - } - - this.emitHistoricalEvents(events); - }, - }, methods: { ...common.methods, getWebhookTriggerType() { return "collection_item_created"; }, generateMeta(data) { + const { + id, fieldData, + } = data; return { - id: data._id, - summary: `New collection item ${data.slug} created`, - ts: Date.parse(data["created-on"]), + id, + summary: `New item: ${fieldData?.slug ?? fieldData?.name ?? id}`, + ts: Date.parse(data["createdOn"]), }; }, }, diff --git a/components/webflow/sources/new-deleted-collection-item/new-deleted-collection-item.mjs b/components/webflow/sources/new-deleted-collection-item/new-deleted-collection-item.mjs index 169c29d0203ff..529d6cddb94e3 100644 --- a/components/webflow/sources/new-deleted-collection-item/new-deleted-collection-item.mjs +++ b/components/webflow/sources/new-deleted-collection-item/new-deleted-collection-item.mjs @@ -3,9 +3,9 @@ import common from "../common/common.mjs"; export default { type: "source", key: "webflow-new-deleted-collection-item", - name: "New Deleted Collection Item", - description: "Emit new event when a collection item is deleted. [See the docs here](https://developers.webflow.com/#item-model)", - version: "0.2.3", + name: "Collection Item Deleted", + description: "Emit new event when a collection item is deleted. [See the documentation](https://developers.webflow.com/data/reference/webhooks/events/collection-item-deleted)", + version: "2.0.0", ...common, methods: { ...common.methods, @@ -13,10 +13,11 @@ export default { return "collection_item_deleted"; }, generateMeta(data) { + const { id } = data; return { - id: data.itemId, - summary: `Collection item ${data.itemId} deleted.`, - ts: Date.parse(data["created-on"]), + id, + summary: `Item deleted: ${id}`, + ts: Date.now(), }; }, }, diff --git a/components/webflow/sources/new-ecommerce-order/new-ecommerce-order.mjs b/components/webflow/sources/new-ecommerce-order/new-ecommerce-order.mjs index d7c3e709439c0..0fb8a407b33f3 100644 --- a/components/webflow/sources/new-ecommerce-order/new-ecommerce-order.mjs +++ b/components/webflow/sources/new-ecommerce-order/new-ecommerce-order.mjs @@ -4,63 +4,21 @@ export default { type: "source", key: "webflow-new-ecommerce-order", name: "New E-commerce Order", - description: "Emit new event when an e-commerce order is created. [See the docs here](https://developers.webflow.com/#order-model)", - version: "0.2.3", + description: + "Emit new event when an e-commerce order is created. [See the documentation](https://developers.webflow.com/data/reference/webhooks/events/ecomm-new-order)", + version: "2.0.0", ...common, - props: { - ...common.props, - historicalEventsNumber: { - type: "integer", - label: "Number of Historical Events to Emit", - description: "Defaults to `0`. Number of historical events to fetch and emit. Maximum is `100`.", - optional: true, - default: 0, - min: 0, - max: 100, - }, - emitMostRecent: { - type: "boolean", - label: "Emit Most Recent Events", - description: "Defaults to `false`. **Warning**: if `true`, will need to request all orders to extract the most recent ones.", - default: false, - }, - }, hooks: { ...common.hooks, async deploy() { - if (!this.historicalEventsNumber) { - return; - } - - const path = `/sites/${this.siteId}/orders`; + const { siteId } = this; console.log("Retrieving historical events..."); - if (!this.emitMostRecent) { - const events = await this._makeRequest(path, { - limit: this.historicalEventsNumber, - }); - this.emitHistoricalEvents(events); - return; - } - - let toEmit = []; - let events = []; - const params = { - offset: 0, - }; - - do { - events = await this._makeRequest(path, params); - - if (toEmit.push(...events) > 100) { - toEmit = toEmit.slice(toEmit.length - 100, toEmit.length); - } - - params.offset += 1; - } while (events.length > 0); - - toEmit.reverse(); - this.emitHistoricalEvents(toEmit, this.historicalEventsNumber); + const events = await this.app.listOrders({ + siteId, + limit: 10, + }); + this.emitHistoricalEvents(events); }, }, methods: { diff --git a/components/webflow/sources/new-form-submission/new-form-submission.mjs b/components/webflow/sources/new-form-submission/new-form-submission.mjs index d29791ef41bbd..2465374dd2f28 100644 --- a/components/webflow/sources/new-form-submission/new-form-submission.mjs +++ b/components/webflow/sources/new-form-submission/new-form-submission.mjs @@ -5,8 +5,8 @@ export default { type: "source", key: "webflow-new-form-submission", name: "New Form Submission", - description: "Emit new event when a new form is submitted. [See the docs here](https://developers.webflow.com/#trigger-types)", - version: "0.2.4", + description: "Emit new event when a form is submitted. [See the documentation](https://developers.webflow.com/data/reference/webhooks/events/form-submission)", + version: "2.0.0", ...common, methods: { ...common.methods, @@ -14,10 +14,13 @@ export default { return "form_submission"; }, generateMeta(data) { + const { + name, id, submittedAt, + } = data; return { - id: data._id, - summary: `New form ${data._id} submission`, - ts: Date.parse(data.date), + id, + summary: `Form submitted: ${name ?? id}`, + ts: Date.parse(submittedAt), }; }, }, diff --git a/components/webflow/sources/new-site-published/new-site-published.mjs b/components/webflow/sources/new-site-published/new-site-published.mjs index 571cec7662565..92ad1006b87ae 100644 --- a/components/webflow/sources/new-site-published/new-site-published.mjs +++ b/components/webflow/sources/new-site-published/new-site-published.mjs @@ -1,44 +1,24 @@ -import constants from "../../common/constants.mjs"; import common from "../common/common.mjs"; export default { type: "source", key: "webflow-new-site-published", - name: "New Site Published", - description: "Emit new event when a site is published. [See the docs here](https://developers.webflow.com/#trigger-types)", - version: "0.2.3", + name: "Site Published", + description: "Emit new event when a site is published. [See the documentation](https://developers.webflow.com/data/reference/webhooks/events/site-publish)", + version: "2.0.0", ...common, - hooks: { - ...common.hooks, - async deploy() { - console.log("Retrieving historical events..."); - - const sites = await this._makeRequest("/sites", { - limit: this.historicalEventsNumber, - }); - - const filtered = sites.filter((site) => site.lastPublished); - const sliced = filtered.slice( - filtered.length - constants.DEPLOY_OFFSET, - constants.DEPLOY_OFFSET, - ); - const sorted = sliced.sort((a, b) => Date.parse(a.lastPublished) > Date.parse(b.lastPublished) - ? -1 - : 1); - - this.emitHistoricalEvents(sorted); - }, - }, methods: { ...common.methods, getWebhookTriggerType() { return "site_publish"; }, - generateMeta(data) { + generateMeta({ + siteId, publishedOn, + }) { return { - id: `${data.site}-${data.publishTime}`, - summary: `The site ${data.site} has been published.`, - ts: data.publishTime, + id: `${siteId}-${publishedOn}`, + summary: `Site published: ${siteId}`, + ts: Date.parse(publishedOn), }; }, }, diff --git a/components/webflow/webflow.app.mjs b/components/webflow/webflow.app.mjs index 74375c31d64f0..e604219812030 100644 --- a/components/webflow/webflow.app.mjs +++ b/components/webflow/webflow.app.mjs @@ -1,7 +1,4 @@ -// Webflow version support here: https://www.npmjs.com/package/webflow-api?activeTab=versions -// @note: this is pinned to Webflow 1.3.1 -// because the upgrade to version 2 requires a new app -import Webflow from "webflow-api@1.3.1"; +import { WebflowClient } from "webflow-api"; import constants from "./common/constants.mjs"; export default { @@ -9,64 +6,66 @@ export default { app: "webflow", propDefinitions: { domains: { - label: "Domain", - description: "The list of domains.", + label: "Custom Domains", + description: "Select one or more custom domains to publish.", type: "string[]", async options({ siteId }) { - const domains = await this.getDomains(siteId); - - return domains.map((domain) => domain.name); + const domains = await this.listDomains(siteId); + return domains.map((id, url) => ({ + label: url, + id, + })); }, }, sites: { label: "Site", - description: "The list of sites", + description: "Select a site or provide a custom site ID.", type: "string", async options() { - const sites = await this.getSites(); + const sites = await this.listSites(); return sites.map((site) => ({ - label: site.name, - value: site._id, + label: site.displayName || site.shortName, + value: site.id, })); }, }, collections: { label: "Collection", - description: "The list of collection of a site", + description: "Select a collection or provide a custom collection ID.", type: "string", async options({ siteId }) { - const collections = await this.getCollections(siteId); + const collections = await this.listCollections(siteId); return collections.map((collection) => ({ - label: collection.name, - value: collection._id, + label: collection.displayName || collection.slug, + value: collection.id, })); }, }, items: { label: "Item", - description: "The list of items of a collection", + description: "Select an item or provide a custom item ID.", type: "string", async options({ collectionId, page, }) { - const items = await this.getItems(page, collectionId); + const items = await this.listCollectionItems(page, collectionId); return items.map((item) => ({ - label: item.name, - value: item._id, + label: item.fieldData?.name || item.fieldData?.slug, + value: item.id, })); }, }, orders: { label: "Order", - description: "The list of orders of a site", + description: "Select an order, or provide a custom order ID.", type: "string", async options({ siteId, page, }) { - const items = await this.getOrders({ + const items = await this.listOrders({ page, siteId, }); @@ -76,179 +75,96 @@ export default { }, }, methods: { - /** - * Get the auth access token; - * - * @returns {string} The base auth access token. - */ _authToken() { return this.$auth.oauth_access_token; }, - /** - * Create a Webflow API client; - * - * @returns {params} The Webflow API client. - */ - _createApiClient() { - return new Webflow({ - token: this._authToken(), + webflowClient() { + return new WebflowClient({ + accessToken: this._authToken(), }); }, - /** - * Create a Webflow webhook; - * - * @param {siteId} ID of the site to be monitored. - * @param {url} URL to webhook return. - * @param {triggerType} Type of event that will be triggered. - * @param {filter} Filters to be applied in webhook. - * - * @returns {params} The Webflow webhook. - */ - async createWebhook(siteId, url, triggerType, filter = {}) { - const apiClient = this._createApiClient(); - - return apiClient.createWebhook({ - siteId, - triggerType, - url, - filter, - }); + async createWebhook(siteId, data) { + return this.webflowClient().webhooks.create(siteId, data); }, - /** - * Remove a Webflow webhook; - * - * @param {siteId} ID of the site. - * @param {webhookId} ID of the webhook. - */ - async removeWebhook(siteId, webhookId) { - const apiClient = this._createApiClient(); - return apiClient.removeWebhook({ - siteId, - webhookId, - }); + async removeWebhook(webhookId) { + return this.webflowClient().webhooks.delete(webhookId); }, - /** - * Get an order; - * - * @param {options} Options to filter the order. - * - * @returns {params} An order. - */ - async getOrder({ - siteId, orderId, - }) { - const apiClient = this._createApiClient(); - - return apiClient.get(`/sites/${siteId}/order/${orderId}`); - }, - /** - * Get a list of orders; - * - * @param {options} Options to filter the orders. - * - * @returns {params} A list of orders. - */ - async getOrders({ - page, siteId, status, + async getOrder(siteId, orderId) { + return this.webflowClient().orders.get(siteId, orderId); + }, + async listOrders({ + page: offset = 0, siteId, status, }) { - const apiClient = this._createApiClient(); - - return apiClient.get(`/sites/${siteId}/orders`, { - status: status, - offset: page ?? 0, - limit: constants.LIMIT, + const response = await this.webflowClient().orders.list(siteId, { + offset, + status, }); + return response?.orders; }, - /** - * Get a list of domains; - * - * @param {options} Options to filter the domains. - * - * @returns {params} A list of domains. - */ - async getDomains(siteId) { - const webflow = this._createApiClient(); - - return await webflow.domains({ - siteId, - }); + async listDomains(siteId) { + const response = await this.webflowClient().sites.getCustomDomain(siteId); + return response?.customDomains; }, - /** - * Get a site; - * - * @param {options} Options to filter the site. - * - * @returns {params} A site. - */ - async getSite(siteId) { - const webflow = this._createApiClient(); - - return await webflow.site({ - siteId, - }); + getSite(siteId) { + return this.webflowClient().sites.get(siteId); }, - /** - * Get a list of sites; - * - * @param {options} Options to filter the sites. - * - * @returns {params} A list of sites. - */ - async getSites() { - const webflow = this._createApiClient(); - - return await webflow.sites(); - }, - /** - * Get a collection; - * - * @param {options} Options to filter the collection. - * - * @returns {params} A collection. - */ - async getCollection(collectionId) { - const webflow = this._createApiClient(); - - return await webflow.collection({ - collectionId, - }); + async listSites() { + const response = await this.webflowClient().sites.list(); + return response?.sites; }, - /** - * Get a list of collections; - * - * @param {options} Options to filter the collections. - * - * @returns {params} A list of collections. - */ - async getCollections(siteId) { - const webflow = this._createApiClient(); - + getCollection(collectionId) { + return this.webflowClient().collections.get(collectionId); + }, + async listCollections(siteId) { if (!siteId) return []; - return await webflow.collections({ - siteId: siteId, - }); + const response = await this.webflowClient().collections.list(siteId); + return response?.collections; }, - /** - * Get a list of items; - * - * @param {options} Options to filter the items. - * - * @returns {params} A list of items. - */ - async getItems(page = 0, collectionId) { - const webflow = this._createApiClient(); - + async listCollectionItems(page = 0, collectionId) { if (!collectionId) return []; - const response = await webflow.items({ - collectionId, - }, { + const response = await this.webflowClient().collections.items.listItems(collectionId, { limit: constants.LIMIT, offset: page, }); - return response; + return response?.items; + }, + getCollectionItem(collectionId, itemId) { + return this.webflowClient().collections.items.getItem(collectionId, itemId); + }, + deleteCollectionItem(collectionId, itemId) { + return this.webflowClient().collections.items.deleteItem(collectionId, itemId); + }, + createCollectionItem(collectionId, data) { + return this.webflowClient().collections.items.createItem(collectionId, data); + }, + updateCollectionItem(collectionId, itemId, data) { + return this.webflowClient().collections.items.updateItem(collectionId, itemId, data); + }, + getCollectionItemInventory(collectionId, itemId) { + return this.webflowClient().inventory.list(collectionId, itemId); + }, + updateCollectionItemInventory(collectionId, itemId, data) { + return this.webflowClient().inventory.update(collectionId, itemId, data); + }, + publishSite(siteId, customDomains) { + return this.webflowClient().sites.publish(siteId, { + customDomains, + }); + }, + fulfillOrder(siteId, orderId, data) { + return this.webflowClient().orders.updateFulfill(siteId, orderId, data); + }, + unfulfillOrder(siteId, orderId) { + return this.webflowClient().orders.updateUnfulfill(siteId, orderId); + }, + refundOrder(siteId, orderId) { + return this.webflowClient().orders.refund(siteId, orderId); + }, + updateOrder(siteId, orderId, data) { + return this.webflowClient().orders.update(siteId, orderId, data); }, }, }; diff --git a/components/webinarfuel/README.md b/components/webinarfuel/README.md new file mode 100644 index 0000000000000..282b1de16d902 --- /dev/null +++ b/components/webinarfuel/README.md @@ -0,0 +1,11 @@ +# Overview + +The WebinarFuel API allows users to automate their webinar marketing efforts directly within Pipedream. By integrating WebinarFuel's functionalities, users can manage registrations, send reminders, or handle attendee data in real-time. This can be particularly useful for enriching attendee data, syncing webinar information with other business tools, and managing the entire webinar lifecycle programmatically. + +# Example Use Cases + +- **Automate Webinar Registrations**: Register attendees for webinars automatically when they sign up through your website or landing page. Use the WebinarFuel API to send registration details to WebinarFuel and confirm their slot within Pipedream. + +- **Sync Attendee Data with CRM**: After a webinar, sync attendee information and engagement metrics with a CRM like Salesforce or HubSpot. This could involve sending attendee data from WebinarFuel to the CRM, updating contact records, or triggering follow-up campaigns based on attendee behavior. + +- **Webinar Reminder Engine**: Build an automated reminder system that sends personalized email or SMS reminders to attendees before the webinar starts. This workflow could integrate WebinarFuel with Twilio for SMS or SendGrid for email, ensuring attendees are well-informed and more likely to attend. diff --git a/components/webinargeek/README.md b/components/webinargeek/README.md index 5747b8a2ec015..d3052517d0e54 100644 --- a/components/webinargeek/README.md +++ b/components/webinargeek/README.md @@ -1,26 +1,11 @@ # Overview -You can build a lot of things with the WebinarGeek API! WebinarGeek enables you -to effortlessly create and manage successful webinars and online events, -featuring all the necessary features – streaming, engagement, and analytics. +The WebinarGeek API lets you automate and streamline your webinar management tasks. You can create, update, and delete webinars, manage registrations, send out emails, and gather analytics. By harnessing this API within Pipedream, you can construct workflows that react to a variety of triggers, such as new registrant data, and then take actions, like updating a CRM or sending personalized follow-up emails through your email provider. -Using the WebinarGeek API, you can create features and services to increase -your webinar's reach and engagement. Here are some examples: +# Example Use Cases -- Automated Messages: You could use the API to create automated messages, - allowing you to instantly respond to your audience and schedule reminders for - upcoming webinars. -- Gamification: Create gamification elements to keep your attendees engaged, - such as quizzes, interactive polls, and leaderboards. -- Custom Themes: Customize the look and feel of your webinars with the API to - create an immersive experience for your audience. -- Webinar Scheduling: Automatically schedule your events with the API. -- Analytics and Reports: Use the API to generate real-time reporting data, - giving you better insight into how your webinars are performing. -- Integration with Third-Party Channels: Connect with social media networks and - other third-party services with the API to give your webinars a boost. +- **Automated Webinar Follow-up Emails**: After a webinar ends, trigger a workflow that gathers the list of attendees from WebinarGeek and sends personalized thank-you emails through SendGrid, including a link to the webinar recording. -Whether you want to create automated messages, gamification elements, and -custom themes, or harness the analytics data and integrate third-party -channels, the WebinarGeek API has the features you need to create a unique and -successful webinar. +- **CRM Integration for New Registrants**: When a new user registers for a webinar, automatically add their details to your Salesforce CRM and send them a calendar invite via Google Calendar, ensuring that they're reminded of the event. + +- **Real-Time Slack Notifications for Webinar Engagement**: Set up a workflow that sends a notification to a designated Slack channel when someone asks a question during a webinar, enabling real-time engagement and allowing your team to provide immediate answers or follow-ups. diff --git a/components/webinarjam/README.md b/components/webinarjam/README.md index b22dde62bd58c..a2bdb2d077d64 100644 --- a/components/webinarjam/README.md +++ b/components/webinarjam/README.md @@ -1,17 +1,11 @@ # Overview -WebinarJam is an online event platform that provides users with powerful APIs -to create engaging virtual events. With the WebinarJam API, users can easily -build innovative and powerful virtual experiences that drive audience -engagement and increase conversions. Here are some of the many things users can -build using the WebinarJam API: +The WebinarJam API enables the automation of webinar management tasks, allowing users to seamlessly integrate their webinar data with other systems and services. With this API, you can programmatically create and update webinars, register participants, send custom notifications, and more. Leveraging Pipedream's serverless integration platform, you can create workflows that react to WebinarJam events, process data in real-time, and interact with countless other apps to optimize your webinar operations. -- Create an online webinar, with advanced features and customizations -- Automate email signups and notifications -- Integrate custom payment gateways -- Build a powerful, interactive Q&A system -- Create quizzes and surveys during events -- Track user engagement during events -- Create automated follow-up messages and surveys -- Customize event-specific landing pages -- Track and analyze historical data from past webinars +# Example Use Cases + +- **Webinar Registrant Sync to CRM**: Automatically register new WebinarJam attendees to a CRM platform like Salesforce. When a user signs up for a webinar, the workflow triggers and adds their details to your CRM, ensuring your sales team has the latest leads without manual entry. + +- **Post-Webinar Email Follow-Up**: Trigger a personalized email sequence via SendGrid or Mailchimp after a webinar ends. This workflow can segment the participants based on their engagement, sending different follow-up content to those who attended live versus those who missed the session. + +- **Webinar Analytics Dashboard Update**: After every webinar, automatically compile key metrics such as attendee count, duration, and engagement levels to a Google Sheets or Data Studio dashboard. This workflow helps maintain real-time insights into webinar performance, aiding in quick decision-making. diff --git a/components/webinarjam/package.json b/components/webinarjam/package.json new file mode 100644 index 0000000000000..7dab19c62f3d3 --- /dev/null +++ b/components/webinarjam/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/webinarjam", + "version": "0.6.0", + "description": "Pipedream webinarjam Components", + "main": "webinarjam.app.mjs", + "keywords": [ + "pipedream", + "webinarjam" + ], + "homepage": "https://pipedream.com/apps/webinarjam", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/webinarkit/README.md b/components/webinarkit/README.md new file mode 100644 index 0000000000000..6e6e241f54afc --- /dev/null +++ b/components/webinarkit/README.md @@ -0,0 +1,11 @@ +# Overview + +The WebinarKit API provides programmable hooks into WebinarKit's suite, enabling automation of webinar management tasks like creating webinars, registering participants, and sending out reminders. With Pipedream, you can craft workflows that react to various triggers, such as new registrant sign-ups or attendee data, and connect these events to hundreds of other apps. This integration can vastly improve the efficiency of your webinar operations by automating repetitive tasks, collecting data, and nurturing leads with minimal manual intervention. + +# Example Use Cases + +- **Automate Webinar Registrations**: When a new attendee signs up for a webinar via a third-party form, you can use Pipedream to automatically register them in WebinarKit, ensuring all attendees are captured in one centralized location. + +- **Post-Webinar Engagement**: After a webinar concludes, trigger an automated workflow in Pipedream that segments attendees based on their interaction during the webinar and sends personalized follow-up emails through an email marketing service like Mailchimp. + +- **Sync Webinar Data to CRM**: Use Pipedream to listen for new webinar sign-ups or attendee engagement and automatically update contact records in a CRM like Salesforce, attaching webinar participation as a custom field to help sales teams tailor their outreach. diff --git a/components/webling/package.json b/components/webling/package.json new file mode 100644 index 0000000000000..58975d1823ab4 --- /dev/null +++ b/components/webling/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/webling", + "version": "0.0.1", + "description": "Pipedream Webling Components", + "main": "webling.app.mjs", + "keywords": [ + "pipedream", + "webling" + ], + "homepage": "https://pipedream.com/apps/webling", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/webling/webling.app.mjs b/components/webling/webling.app.mjs new file mode 100644 index 0000000000000..96573b33a8080 --- /dev/null +++ b/components/webling/webling.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "webling", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/webmerge/README.md b/components/webmerge/README.md index d5886132b10a5..81e42dc170da0 100644 --- a/components/webmerge/README.md +++ b/components/webmerge/README.md @@ -1,19 +1,11 @@ # Overview -The WebMerge API allows you to create automated documents and business -applications without the need to code. It's flexible, powerful, and easy to -use. You can use the WebMerge API to build a variety of applications to -streamline your day-to-day workflows. +WebMerge is an API that automates the creation of PDFs, Word documents, Excel files, and more, by merging data into custom-built templates. With Pipedream, you can harness this functionality to create dynamic documents fueled by data from various sources. Think auto-generated reports from CRM leads, personalized contracts from form submissions, or automated invoices post-sale. Pipedream's serverless platform allows you to create intricate workflows that connect WebMerge with other apps, triggering document creation with real-time data. -Here are some examples of what you can build using the WebMerge API: +# Example Use Cases -- Automatically generate professional contracts, agreements, or invoices -- Automate document delivery -- Send personalized communications to large customer bases -- Create secure web-based forms -- Create web-based member directories -- Create PDF bookmarks -- Convert web forms into PDF documents -- Automate PDF assembly tasks -- Generate customized bulk marketing material -- Securely export data to cloud storage solutions +- **Automated Contract Generation from CRM Deals**: When a deal reaches a certain stage in a CRM like Salesforce, trigger a Pipedream workflow that sends deal data to WebMerge, which then generates a tailored contract and emails it directly to the client for signing. + +- **Dynamic Report Creation from Analytics Platforms**: Set up a scheduled workflow in Pipedream to pull the latest data from analytics tools like Google Analytics. Use WebMerge to turn that data into a polished, comprehensive report, and then distribute it to stakeholders via a platform like Slack or by emailing through SendGrid. + +- **Invoice Processing Post-Purchase**: Capture new payment information from a payment gateway such as Stripe. Trigger a Pipedream workflow to populate an invoice template in WebMerge with the payment details, then save the invoice to a cloud storage service like Dropbox and notify your finance team through a messaging app. diff --git a/components/webmerge/package.json b/components/webmerge/package.json new file mode 100644 index 0000000000000..ad2b52c1e3f14 --- /dev/null +++ b/components/webmerge/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/webmerge", + "version": "0.6.0", + "description": "Pipedream webmerge Components", + "main": "webmerge.app.mjs", + "keywords": [ + "pipedream", + "webmerge" + ], + "homepage": "https://pipedream.com/apps/webmerge", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/webscraper_io/README.md b/components/webscraper_io/README.md new file mode 100644 index 0000000000000..624c6e8ee70cc --- /dev/null +++ b/components/webscraper_io/README.md @@ -0,0 +1,11 @@ +# Overview + +The WebScraper.IO API allows you to programmatically perform web scraping tasks, extracting structured data from websites. With the API, you can automate the gathering of web content for analysis, monitoring, and integration with other data sources. In Pipedream, you can leverage this API to build workflows that process, analyze, and act on the data you scrape without writing code for backend infrastructure. + +# Example Use Cases + +- **Automated Content Aggregation**: Build a workflow that triggers at regular intervals to scrape content from multiple news sites or blogs, then compile the results into a single data store within Pipedream. You could then use this aggregated data for content analysis or send a daily digest via email. + +- **Price Monitoring System**: Create a Pipedream workflow that uses WebScraper.IO to monitor product prices on e-commerce websites. The workflow can trigger notifications or take action when prices drop below a certain threshold. Connect the workflow with the Twilio app to send SMS alerts to subscribers or stakeholders. + +- **Competitor Analysis Pipeline**: Develop a pipeline that scrapes competitor websites for new product launches or content updates using WebScraper.IO. Integrate the scraped data with Google Sheets or Airtable on Pipedream to maintain an up-to-date comparison database, which can inform strategic business decisions. diff --git a/components/webscraping_ai/.gitignore b/components/webscraping_ai/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/webscraping_ai/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/webscraping_ai/README.md b/components/webscraping_ai/README.md index f4f36ce7b27b7..fef38f130a0f8 100644 --- a/components/webscraping_ai/README.md +++ b/components/webscraping_ai/README.md @@ -1,16 +1,11 @@ # Overview -WebScraping.AI is an API designed to make it easy to access and extract online -content from websites. Using the WebScraping.AI API, you can quickly and easily -build a range of automated data retrieval applications, such as: +WebScraping.AI API provides powerful tools for extracting data from websites, enabling users to retrieve structured information without the hassle of setting up a custom scraper. It handles proxy rotation, browsers, and CAPTCHAs, allowing you to focus on data collection. With Pipedream, you can harness this capability to create automated workflows that trigger on various events, process web content, and connect with countless other apps to feed data pipelines, monitor changes, or populate databases. -- Web data scraping for lead generation -- Automated web scraping to build market intelligence systems -- Automated web scraping to monitor web content for competitive intelligence -- Extracting data for specific products or services -- Image/logo recognition for scraping -- Automated web scraping for eCommerce monitoring -- Automated web scraping for pricing information -- Extracting data from job postings -- Monitoring social media for marketing trends -- Extracting data from financial statements +# Example Use Cases + +- **Market Research Automation**: Gather pricing, product descriptions, and ratings from e-commerce sites. Use WebScraping.AI with Pipedream to scrape data at regular intervals, and connect to apps like Google Sheets to organize the data for analysis, or send it to a business intelligence tool for real-time dashboards. + +- **Content Change Detection**: Monitor competitors' websites for content updates or changes. Set up a Pipedream workflow that scrapes specific pages daily with WebScraping.AI, compares it with the previous version stored in Pipedream's built-in data store, and sends alerts via Slack or Email when changes are detected. + +- **Lead Generation Engine**: Extract contact information from online directories or professional networking sites. Use WebScraping.AI within a Pipedream workflow to capture leads and automatically add them to a CRM like HubSpot or Salesforce, or send the data to a marketing automation tool to initiate contact sequences. diff --git a/components/webscraping_ai/actions/ask-question/ask-question.mjs b/components/webscraping_ai/actions/ask-question/ask-question.mjs new file mode 100644 index 0000000000000..8edd1ca4bac69 --- /dev/null +++ b/components/webscraping_ai/actions/ask-question/ask-question.mjs @@ -0,0 +1,126 @@ +import webscrapingAI from "../../webscraping_ai.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "webscraping_ai-ask-question", + name: "Ask Question about Webpage", + description: "Gets an answer to a question about a given webpage. [See the documentation](https://webscraping.ai/docs#tag/AI/operation/getQuestion)", + version: "0.0.1", + type: "action", + props: { + webscrapingAI, + targetUrl: { + propDefinition: [ + webscrapingAI, + "targetUrl", + ], + }, + question: { + type: "string", + label: "Question", + description: "The question to ask about the given webpage. E.g. `What is the summary of this page content?`", + }, + headers: { + propDefinition: [ + webscrapingAI, + "headers", + ], + }, + timeout: { + propDefinition: [ + webscrapingAI, + "timeout", + ], + }, + js: { + propDefinition: [ + webscrapingAI, + "js", + ], + }, + jsTimeout: { + propDefinition: [ + webscrapingAI, + "jsTimeout", + ], + }, + waitFor: { + propDefinition: [ + webscrapingAI, + "waitFor", + ], + }, + proxy: { + propDefinition: [ + webscrapingAI, + "proxy", + ], + }, + country: { + propDefinition: [ + webscrapingAI, + "country", + ], + }, + customProxy: { + propDefinition: [ + webscrapingAI, + "customProxy", + ], + }, + device: { + propDefinition: [ + webscrapingAI, + "device", + ], + }, + errorOn404: { + propDefinition: [ + webscrapingAI, + "errorOn404", + ], + }, + errorOnRedirect: { + propDefinition: [ + webscrapingAI, + "errorOnRedirect", + ], + }, + jsScript: { + propDefinition: [ + webscrapingAI, + "jsScript", + ], + }, + format: { + propDefinition: [ + webscrapingAI, + "format", + ], + }, + }, + async run({ $ }) { + const response = await this.webscrapingAI.getAnswerToQuestion({ + $, + params: { + url: this.targetUrl, + question: this.question, + headers: utils.stringifyHeaders(this.headers), + timeout: this.timeout, + js: this.js, + js_timeout: this.jsTimeout, + wait_for: this.waitFor, + proxy: this.proxy, + country: this.country, + custom_proxy: this.customProxy, + device: this.device, + error_on_404: this.errorOn404, + error_on_redirect: this.errorOnRedirect, + js_script: this.jsScript, + format: this.format, + }, + }); + $.export("$summary", "Successfully retrieved answer to question"); + return response; + }, +}; diff --git a/components/webscraping_ai/actions/scrape-website-html/scrape-website-html.mjs b/components/webscraping_ai/actions/scrape-website-html/scrape-website-html.mjs new file mode 100644 index 0000000000000..491062855566e --- /dev/null +++ b/components/webscraping_ai/actions/scrape-website-html/scrape-website-html.mjs @@ -0,0 +1,127 @@ +import webscrapingAI from "../../webscraping_ai.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "webscraping_ai-scrape-website-html", + name: "Scrape Website HTML", + description: "Returns the full HTML content of a webpage specified by the URL. [See the documentation](https://webscraping.ai/docs#tag/HTML/operation/getHTML):", + version: "0.0.1", + type: "action", + props: { + webscrapingAI, + targetUrl: { + propDefinition: [ + webscrapingAI, + "targetUrl", + ], + }, + headers: { + propDefinition: [ + webscrapingAI, + "headers", + ], + }, + timeout: { + propDefinition: [ + webscrapingAI, + "timeout", + ], + }, + js: { + propDefinition: [ + webscrapingAI, + "js", + ], + }, + jsTimeout: { + propDefinition: [ + webscrapingAI, + "jsTimeout", + ], + }, + waitFor: { + propDefinition: [ + webscrapingAI, + "waitFor", + ], + }, + proxy: { + propDefinition: [ + webscrapingAI, + "proxy", + ], + }, + country: { + propDefinition: [ + webscrapingAI, + "country", + ], + }, + customProxy: { + propDefinition: [ + webscrapingAI, + "customProxy", + ], + }, + device: { + propDefinition: [ + webscrapingAI, + "device", + ], + }, + errorOn404: { + propDefinition: [ + webscrapingAI, + "errorOn404", + ], + }, + errorOnRedirect: { + propDefinition: [ + webscrapingAI, + "errorOnRedirect", + ], + }, + jsScript: { + propDefinition: [ + webscrapingAI, + "jsScript", + ], + }, + format: { + propDefinition: [ + webscrapingAI, + "format", + ], + }, + returnScriptResult: { + type: "boolean", + label: "Return Script Result", + description: "Return result of the custom JavaScript code (`js_script` parameter) execution on the target page (`false` by default, page HTML will be returned).", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.webscrapingAI.pageHtmlByUrl({ + $, + params: { + url: this.targetUrl, + headers: utils.stringifyHeaders(this.headers), + timeout: this.timeout, + js: this.js, + js_timeout: this.jsTimeout, + wait_for: this.waitFor, + proxy: this.proxy, + country: this.country, + custom_proxy: this.customProxy, + device: this.device, + error_on_404: this.errorOn404, + error_on_redirect: this.errorOnRedirect, + js_script: this.jsScript, + format: this.format, + return_script_result: this.returnScriptResult, + }, + }); + $.export("$summary", `Successfully scraped HTML of URL ${this.targetUrl}`); + return response; + }, +}; diff --git a/components/webscraping_ai/actions/scrape-website-text/scrape-website-text.mjs b/components/webscraping_ai/actions/scrape-website-text/scrape-website-text.mjs new file mode 100644 index 0000000000000..c5fd18a86a61f --- /dev/null +++ b/components/webscraping_ai/actions/scrape-website-text/scrape-website-text.mjs @@ -0,0 +1,133 @@ +import webscrapingAI from "../../webscraping_ai.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "webscraping_ai-scrape-website-text", + name: "Scrape Website Text", + description: "Returns the visible text content of a webpage specified by the URL. [See the documentation](https://webscraping.ai/docs#tag/Text/operation/getText).", + version: "0.0.1", + type: "action", + props: { + webscrapingAI, + targetUrl: { + propDefinition: [ + webscrapingAI, + "targetUrl", + ], + }, + headers: { + propDefinition: [ + webscrapingAI, + "headers", + ], + }, + timeout: { + propDefinition: [ + webscrapingAI, + "timeout", + ], + }, + js: { + propDefinition: [ + webscrapingAI, + "js", + ], + }, + jsTimeout: { + propDefinition: [ + webscrapingAI, + "jsTimeout", + ], + }, + waitFor: { + propDefinition: [ + webscrapingAI, + "waitFor", + ], + }, + proxy: { + propDefinition: [ + webscrapingAI, + "proxy", + ], + }, + country: { + propDefinition: [ + webscrapingAI, + "country", + ], + }, + customProxy: { + propDefinition: [ + webscrapingAI, + "customProxy", + ], + }, + device: { + propDefinition: [ + webscrapingAI, + "device", + ], + }, + errorOn404: { + propDefinition: [ + webscrapingAI, + "errorOn404", + ], + }, + errorOnRedirect: { + propDefinition: [ + webscrapingAI, + "errorOnRedirect", + ], + }, + jsScript: { + propDefinition: [ + webscrapingAI, + "jsScript", + ], + }, + textFormat: { + type: "string", + label: "Text Format", + description: "The format of the returned text content. Default: `json`", + options: [ + "plain", + "xml", + "json", + ], + default: "json", + optional: true, + }, + returnLinks: { + type: "boolean", + label: "Return Links", + description: "Whether to include links in the returned text content. Works only when Text Format is `json`.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.webscrapingAI.pageTextByUrl({ + $, + params: { + url: this.targetUrl, + headers: utils.stringifyHeaders(this.headers), + timeout: this.timeout, + js: this.js, + js_timeout: this.jsTimeout, + wait_for: this.waitFor, + proxy: this.proxy, + country: this.country, + custom_proxy: this.customProxy, + device: this.device, + error_on_404: this.errorOn404, + error_on_redirect: this.errorOnRedirect, + js_script: this.jsScript, + text_format: this.textFormat, + return_links: this.returnLinks, + }, + }); + $.export("$summary", `Successfully scraped text from ${this.targetUrl}`); + return response; + }, +}; diff --git a/components/webscraping_ai/app/webscraping_ai.app.ts b/components/webscraping_ai/app/webscraping_ai.app.ts deleted file mode 100644 index ef05d2cd90d7f..0000000000000 --- a/components/webscraping_ai/app/webscraping_ai.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "webscraping_ai", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); diff --git a/components/webscraping_ai/common/utils.mjs b/components/webscraping_ai/common/utils.mjs new file mode 100644 index 0000000000000..0093b6517ff63 --- /dev/null +++ b/components/webscraping_ai/common/utils.mjs @@ -0,0 +1,12 @@ +function stringifyHeaders(headers) { + if (!headers) { + return undefined; + } + return typeof headers === "string" + ? headers + : JSON.stringify(headers); +} + +export default { + stringifyHeaders, +}; diff --git a/components/webscraping_ai/package.json b/components/webscraping_ai/package.json index 6cc412b15c6c8..26e6a2e4a629c 100644 --- a/components/webscraping_ai/package.json +++ b/components/webscraping_ai/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/webscraping_ai", - "version": "0.0.3", + "version": "0.1.0", "description": "Pipedream WebScraping.AI Components", - "main": "dist/app/webscraping_ai.app.mjs", + "main": "webscraping_ai.app.mjs", "keywords": [ "pipedream", "webscraping_ai" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/webscraping_ai", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/webscraping_ai/webscraping_ai.app.mjs b/components/webscraping_ai/webscraping_ai.app.mjs new file mode 100644 index 0000000000000..1bfb8b639bdad --- /dev/null +++ b/components/webscraping_ai/webscraping_ai.app.mjs @@ -0,0 +1,155 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "webscraping_ai", + propDefinitions: { + targetUrl: { + type: "string", + label: "Target URL", + description: "The URL of the webpage to scrape.", + }, + headers: { + type: "object", + label: "Headers", + description: "HTTP headers to pass to the target page", + optional: true, + }, + timeout: { + type: "integer", + label: "Timeout", + description: "Maximum web page retrieval time in ms. Increase it in case of timeout errors (10000 by default, maximum is 30000).", + optional: true, + }, + js: { + type: "boolean", + label: "JS", + description: "Execute on-page JavaScript using a headless browser (`true` by default)", + optional: true, + }, + jsTimeout: { + type: "integer", + label: "JS Timeout", + description: "Maximum JavaScript rendering time in ms. Default: `2000`", + optional: true, + }, + waitFor: { + type: "string", + label: "Wait For", + description: "CSS selector to wait for before returning the page content. Useful for pages with dynamic content loading. Overrides js_timeout.", + optional: true, + }, + proxy: { + type: "string", + label: "Proxy", + description: "Type of proxy, use residential proxies if your site restricts traffic from datacenters (`datacenter` by default). Note that residential proxy requests are more expensive than datacenter, see the pricing page for details.", + options: [ + "datacenter", + "residential", + ], + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "Country of the proxy to use (`us` by default)", + options: [ + "us", + "gb", + "de", + "it", + "fr", + "ca", + "es", + "ru", + "jp", + "kr", + "in", + ], + optional: true, + }, + customProxy: { + type: "string", + label: "Custom Proxy", + description: "Your own proxy URL to use instead of our built-in proxy pool in \"http://user:password@host:port\" format ([Smartproxy](https://webscraping.ai/proxies/smartproxy) for example).", + optional: true, + }, + device: { + type: "string", + label: "Device", + description: "Type of device emulation. Default is `desktop`", + options: [ + "desktop", + "mobile", + "tablet", + ], + optional: true, + }, + errorOn404: { + type: "boolean", + label: "Error on 404", + description: "Return error on 404 HTTP status on the target page (`false` by default)", + optional: true, + }, + errorOnRedirect: { + type: "boolean", + label: "Error on Redirect", + description: "Return error on redirect on the target page (`false` by default)", + optional: true, + }, + jsScript: { + type: "string", + label: "JS Script", + description: "Custom JavaScript code to execute on the target page. Example: `document.querySelector('button').click();`", + optional: true, + }, + format: { + type: "string", + label: "Format", + description: "Format of the response (`text` by default). `json` will return a JSON object with the response, `text` will return a plain text/HTML response.", + options: [ + "json", + "text", + ], + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.webscraping.ai"; + }, + _makeRequest({ + $ = this, + path, + params, + ...otherOpts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + params: { + ...params, + api_key: this.$auth.api_key, + }, + ...otherOpts, + }); + }, + pageHtmlByUrl(opts = {}) { + return this._makeRequest({ + path: "/html", + ...opts, + }); + }, + pageTextByUrl(opts = {}) { + return this._makeRequest({ + path: "/text", + ...opts, + }); + }, + getAnswerToQuestion(opts = {}) { + return this._makeRequest({ + path: "/ai/question", + ...opts, + }); + }, + }, +}; diff --git a/components/webvizio/README.md b/components/webvizio/README.md new file mode 100644 index 0000000000000..d3d3e2f348a1f --- /dev/null +++ b/components/webvizio/README.md @@ -0,0 +1,11 @@ +# Overview + +The Webvizio API provides a platform for website project management, enabling users to manage tasks, collaborate on web development projects, and review website changes in real-time. With Pipedream, you can harness this API to automate feedback loops, sync tasks with other project management tools, and create a seamless integration between your website updates and your project tracking system. + +# Example Use Cases + +- **Automate Feedback Collection to Task Management**: Gather feedback from Webvizio and automatically create tasks in a connected project management tool like Trello or Asana. This ensures that web feedback translates directly into actionable tasks. + +- **Sync Comments with Communication Platforms**: When new comments are added in Webvizio, trigger notifications and share the details in communication platforms such as Slack or Microsoft Teams. This keeps the entire team instantly informed about new developments or required changes. + +- **Periodic Website Review and Reporting**: Set up a scheduled Pipedream workflow that periodically checks for website changes or comments in Webvizio, compiles a report, and emails it to stakeholders using a service like SendGrid. This ensures regular updates to the team or clients about the status of the website project. diff --git a/components/weglot/README.md b/components/weglot/README.md new file mode 100644 index 0000000000000..e9e40e51ef73d --- /dev/null +++ b/components/weglot/README.md @@ -0,0 +1,11 @@ +# Overview + +The Weglot API allows for real-time translation of website content, simplifying the process of making a website multilingual. On Pipedream, you can use the Weglot API to craft workflows that automate translation tasks, manage language databases, or even synchronize content across different platforms. This API could be a game changer for businesses looking to globalize their online presence without the manual overhead. + +# Example Use Cases + +- **Automated Content Translation for New Posts**: When a new blog post is published on your CMS (like WordPress), you could use Pipedream to trigger a workflow that automatically sends the content to Weglot for translation. The translated content can then be pushed back to your CMS, creating a multilingual post without any manual intervention. + +- **Sync Product Descriptions Across Platforms**: If you run an e-commerce site using Shopify, you could create a Pipedream workflow that monitors your product listings for changes. When a change is detected, the workflow sends the new or updated product descriptions to Weglot for translation, then updates the translated content on other marketplaces like eBay or Amazon. + +- **Real-time Support Ticket Translation**: Integrate Weglot with a customer support platform such as Zendesk through Pipedream. This workflow can translate incoming support tickets into your support team's preferred language and outgoing responses back into the customer's language, enabling seamless support for a global customer base. diff --git a/components/welcome/common/constants.mjs b/components/welcome/common/constants.mjs new file mode 100644 index 0000000000000..7c49e045702bb --- /dev/null +++ b/components/welcome/common/constants.mjs @@ -0,0 +1,15 @@ +const BASE_URL = "https://app.experiencewelcome.com"; +const VERSION_PATH = "/api/v1"; +const LAST_DATE_AT = "lastDateAt"; +const IS_FIRST_RUN = "isFirstRun"; +const DEFAULT_MAX = 100; +const DEFAULT_LIMIT = 25; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_MAX, + DEFAULT_LIMIT, + LAST_DATE_AT, + IS_FIRST_RUN, +}; diff --git a/components/welcome/common/utils.mjs b/components/welcome/common/utils.mjs new file mode 100644 index 0000000000000..de7ee6c4a4692 --- /dev/null +++ b/components/welcome/common/utils.mjs @@ -0,0 +1,17 @@ +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +function getNestedProperty(obj, propertyString) { + const properties = propertyString.split("."); + return properties.reduce((prev, curr) => prev?.[curr], obj); +} + +export default { + iterate, + getNestedProperty, +}; diff --git a/components/welcome/package.json b/components/welcome/package.json new file mode 100644 index 0000000000000..6869b4f901b01 --- /dev/null +++ b/components/welcome/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/welcome", + "version": "0.1.0", + "description": "Pipedream Welcome Components", + "main": "welcome.app.mjs", + "keywords": [ + "pipedream", + "welcome" + ], + "homepage": "https://pipedream.com/apps/welcome", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/welcome/sources/common/polling.mjs b/components/welcome/sources/common/polling.mjs new file mode 100644 index 0000000000000..7b2cf3c7f3adf --- /dev/null +++ b/components/welcome/sources/common/polling.mjs @@ -0,0 +1,112 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../welcome.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + this.setIsFirstRun(true); + }, + }, + methods: { + setIsFirstRun(value) { + this.db.set(constants.IS_FIRST_RUN, value); + }, + getIsFirstRun() { + return this.db.get(constants.IS_FIRST_RUN); + }, + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + setLastDateAt(value) { + this.db.set(constants.LAST_DATE_AT, value); + }, + getLastDateAt() { + return this.db.get(constants.LAST_DATE_AT); + }, + getDateField() { + throw new ConfigurationError("getDateField is not implemented"); + }, + getResourceName() { + throw new ConfigurationError("getResourceName is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + processResources(resources) { + Array.from(resources) + .forEach(this.processResource); + }, + }, + async run() { + const { + app, + getDateField, + getLastDateAt, + getResourcesFn, + getResourcesFnArgs, + getResourceName, + processResources, + getIsFirstRun, + setIsFirstRun, + setLastDateAt, + } = this; + + const isFirstRun = getIsFirstRun(); + const dateField = getDateField(); + const lastDateAt = getLastDateAt(); + + const otherArgs = isFirstRun + ? { + max: constants.DEFAULT_LIMIT, + } + : { + dateField, + lastDateAt, + }; + + const resources = await app.paginate({ + resourcesFn: getResourcesFn(), + resourcesFnArgs: getResourcesFnArgs(), + resourceName: getResourceName(), + ...otherArgs, + }); + + if (isFirstRun && resources.length) { + const [ + firstResource, + ] = Array.from(resources).reverse(); + if (firstResource) { + setLastDateAt(firstResource[dateField]); + } + } + + processResources(resources); + + if (isFirstRun) { + setIsFirstRun(false); + } + }, +}; diff --git a/components/welcome/sources/event-created/event-created.mjs b/components/welcome/sources/event-created/event-created.mjs new file mode 100644 index 0000000000000..2566759515efa --- /dev/null +++ b/components/welcome/sources/event-created/event-created.mjs @@ -0,0 +1,37 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "welcome-event-created", + name: "New Event Created", + description: "Emit new event when a new event is created in Welcome. [See the documentation](https://app.experiencewelcome.com/api-docs/index.html)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getDateField() { + return "updatedAt"; + }, + getResourceName() { + return "events"; + }, + getResourcesFn() { + return this.app.listEvents; + }, + getResourcesFnArgs() { + return { + debug: true, + }; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Event: ${resource.name}`, + ts: Date.parse(resource.updatedAt), + }; + }, + }, + sampleEmit, +}; diff --git a/components/welcome/sources/event-created/test-event.mjs b/components/welcome/sources/event-created/test-event.mjs new file mode 100644 index 0000000000000..b1aa6b24ddb73 --- /dev/null +++ b/components/welcome/sources/event-created/test-event.mjs @@ -0,0 +1,257 @@ +export default { + "primaryLoungeSessionOpen": false, + "eventSeries": null, + "stage": { + "isRecording": false, + "streamingToMobile": false, + "streamingActive": false, + "recordingInstanceReady": true, + "agenda": { + "data": { + "id": "22415", + "type": "agenda", + "attributes": { + "id": 22415, + "hashid": "Y0fAke", + "published": null, + "stageId": 28334, + "unassignedAssetsOrderingOverride": [ + { + "order": 0, + "assetId": "55565", + "assetType": "DynamicEventAsset" + }, + { + "order": 1, + "assetId": "55566", + "assetType": "DynamicEventAsset" + }, + { + "order": 2, + "assetId": "233792", + "assetType": "EventAsset" + }, + { + "order": 3, + "assetId": "55567", + "assetType": "DynamicEventAsset" + } + ], + "durationsEnabled": false, + "agendaItems": { + "data": [] + } + } + } + }, + "id": 28334, + "hashid": "yVfbBW", + "eventId": 26778, + "name": "Test 1", + "description": "", + "status": "closed", + "startTime": "2024-08-16T14:00:00.000Z", + "endTime": "2024-08-16T15:00:00.000Z", + "timeZone": "America/Bogota", + "registerDisplayName": "Register", + "alreadyRegisteredDisplayName": "Already Registered?", + "auditoriumDisplayName": "Auditorium", + "breakoutDisplayName": "Breakout", + "loungeDisplayName": "Lounge", + "primaryInfoDisplayName": "Info", + "secondaryInfoDisplayName": "Secondary Info", + "staffDisplayName": "staff", + "speakerDisplayName": "speaker", + "attendeeDisplayName": "attendee", + "defaultLoungeViewMode": "card", + "speakerGreetingText": null, + "raiseHandEnabled": true, + "primaryInfoPageUrl": null, + "primaryInfoImageUrl": null, + "secondaryInfoPageUrl": null, + "secondaryInfoImageUrl": null, + "stageUrl": "/events/mqubWG/stages/yVfbBW", + "logoUrl": null, + "iconUrl": null, + "coverImageUrl": "https://d30kg4wnmf2rd.cloudfront.net/event_assets/mqubWG/cover/people-and-things.gif-b02f1732-d8aa-4db4-a3e9-a460f68f5bc1", + "publicChatEnabled": true, + "publicChatSlowModeEnabled": false, + "qnaEnabled": true, + "qnaModerationEnabled": false, + "qnaHiddenNamesEnabled": false, + "qnaAllHiddenNamesEnabled": false, + "peopleListEnabled": true, + "peopleListShowCountEnabled": true, + "pollsShowVoteCountEnabled": true, + "directMessagesEnabled": true, + "videoCallEnabled": true, + "highChatTrafficExpected": false, + "breakoutsEnabled": true, + "assetPreviewPopperEnabled": true, + "agendaVisibleEnabled": true, + "closedCaptionsEnabled": true, + "lowerThirdEnabled": true, + "speakerBubblesEnabled": true, + "speakerNamesEnabled": true, + "overlayFadeEnabled": true, + "activeLoungeSessionOpen": false, + "nameTagsAutoDismiss": false, + "translationServiceUrl": null, + "primaryColor": "#000000", + "secondaryColor": "#000000", + "signedStageId": "eyJfcmFpbHMiOnsibWVzc2FnZSI6Ik1qZ3pNelE9IiwiZXhwIjpudWxsLCJwdXIiOiJzdGFnZS93cyJ9fQ==--c4097b23f5d8f62e14d448a621b94114d473b5a2abd1f8bd36467d735a6d192b" + }, + "stages": [ + { + "isRecording": false, + "streamingToMobile": false, + "streamingActive": false, + "recordingInstanceReady": true, + "agenda": { + "data": { + "id": "22415", + "type": "agenda", + "attributes": { + "id": 22415, + "hashid": "Y0fAke", + "published": null, + "stageId": 28334, + "unassignedAssetsOrderingOverride": [ + { + "order": 0, + "assetId": "55565", + "assetType": "DynamicEventAsset" + }, + { + "order": 1, + "assetId": "55566", + "assetType": "DynamicEventAsset" + }, + { + "order": 2, + "assetId": "233792", + "assetType": "EventAsset" + }, + { + "order": 3, + "assetId": "55567", + "assetType": "DynamicEventAsset" + } + ], + "durationsEnabled": false, + "agendaItems": { + "data": [] + } + } + } + }, + "id": 28334, + "hashid": "yVfbBW", + "eventId": 26778, + "name": "Test 1", + "description": "", + "status": "closed", + "startTime": "2024-08-16T14:00:00.000Z", + "endTime": "2024-08-16T15:00:00.000Z", + "timeZone": "America/Bogota", + "registerDisplayName": "Register", + "alreadyRegisteredDisplayName": "Already Registered?", + "auditoriumDisplayName": "Auditorium", + "breakoutDisplayName": "Breakout", + "loungeDisplayName": "Lounge", + "primaryInfoDisplayName": "Info", + "secondaryInfoDisplayName": "Secondary Info", + "staffDisplayName": "staff", + "speakerDisplayName": "speaker", + "attendeeDisplayName": "attendee", + "defaultLoungeViewMode": "card", + "speakerGreetingText": null, + "raiseHandEnabled": true, + "primaryInfoPageUrl": null, + "primaryInfoImageUrl": null, + "secondaryInfoPageUrl": null, + "secondaryInfoImageUrl": null, + "stageUrl": "/events/mqubWG/stages/yVfbBW", + "logoUrl": null, + "iconUrl": null, + "coverImageUrl": "https://d30kg4wnmf2rd.cloudfront.net/event_assets/mqubWG/cover/people-and-things.gif-b02f1732-d8aa-4db4-a3e9-a460f68f5bc1", + "publicChatEnabled": true, + "publicChatSlowModeEnabled": false, + "qnaEnabled": true, + "qnaModerationEnabled": false, + "qnaHiddenNamesEnabled": false, + "qnaAllHiddenNamesEnabled": false, + "peopleListEnabled": true, + "peopleListShowCountEnabled": true, + "pollsShowVoteCountEnabled": true, + "directMessagesEnabled": true, + "videoCallEnabled": true, + "highChatTrafficExpected": false, + "breakoutsEnabled": true, + "assetPreviewPopperEnabled": true, + "agendaVisibleEnabled": true, + "closedCaptionsEnabled": true, + "lowerThirdEnabled": true, + "speakerBubblesEnabled": true, + "speakerNamesEnabled": true, + "overlayFadeEnabled": true, + "activeLoungeSessionOpen": false, + "nameTagsAutoDismiss": false, + "translationServiceUrl": null, + "primaryColor": "#000000", + "secondaryColor": "#000000", + "signedStageId": "eyJfcmFpbHMiOnsibWVzc2FnZSI6Ik1qZ3pNelE9IiwiZXhwIjpudWxsLCJwdXIiOiJzdGFnZS93cyJ9fQ==--c4097b23f5d8f62e14d448a621b94114d473b5a2abd1f8bd36467d735a6d192b" + } + ], + "restrictedSigninTypes": [ + "google", + "linkedin", + "outlook", + "email" + ], + "id": 26778, + "hashid": "mqubWG", + "name": "Test 1", + "description": "", + "updatedAt": "2024-08-15T22:11:38.060Z", + "startTime": "2024-08-16T14:00:00.000Z", + "endTime": "2024-08-16T15:00:00.000Z", + "timeZone": "America/Bogota", + "primaryColor": "#000000", + "secondaryColor": "#000000", + "logoUrl": null, + "iconUrl": null, + "coverImageUrl": "https://d30kg4wnmf2rd.cloudfront.net/event_assets/mqubWG/cover/people-and-things.gif-b02f1732-d8aa-4db4-a3e9-a460f68f5bc1", + "accessType": "registration_required", + "registrationsCount": 1, + "eventUrl": "https://app.experiencewelcome.com/events/mqubWG/stages/yVfbBW", + "eventLoginUrl": "https://app.experiencewelcome.com/events/mqubWG", + "loungeUrl": "/events/mqubWG/stages/yVfbBW?initialStageArea=lounge", + "alwaysShowWelcomeRegForm": false, + "demoMode": false, + "quickRegEnabled": true, + "emailReminder24hPriorEnabled": false, + "emailReminder1hPriorEnabled": false, + "emailPostEventEnabled": false, + "recordingsAvailable": false, + "recordingsAutoPublish": true, + "recordingsRegistrationRequired": false, + "agendaRegistrationRequired": false, + "registerDisplayName": "Register", + "alreadyRegisteredDisplayName": "Already Registered?", + "staffDisplayName": "staff", + "speakerDisplayName": "speaker", + "attendeeDisplayName": "attendee", + "defaultLoungeViewMode": "card", + "speakerGreetingText": null, + "breakoutsEnabled": true, + "speakersCanModerate": false, + "postEventSurvey": true, + "noEmailConfirmation": true, + "clipsCustomLinkText": null, + "clipsCustomLinkUrl": null, + "postEventMarketingUrl": null, + "eventType": "live", + "primaryStageId": 28334, + "welcomeRegRequired": false +}; diff --git a/components/welcome/sources/event-updated/event-updated.mjs b/components/welcome/sources/event-updated/event-updated.mjs new file mode 100644 index 0000000000000..b72240453e988 --- /dev/null +++ b/components/welcome/sources/event-updated/event-updated.mjs @@ -0,0 +1,38 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "welcome-event-updated", + name: "Event Updated", + description: "Emit new event when an event is updated. [See the documentation](https://app.experiencewelcome.com/api-docs/index.html)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getDateField() { + return "updatedAt"; + }, + getResourceName() { + return "events"; + }, + getResourcesFn() { + return this.app.listEvents; + }, + getResourcesFnArgs() { + return { + debug: true, + }; + }, + generateMeta(resource) { + const ts = Date.parse(resource.updatedAt); + return { + id: `${resource.id}-${ts}`, + summary: `Event Updated: ${resource.name}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/welcome/sources/event-updated/test-event.mjs b/components/welcome/sources/event-updated/test-event.mjs new file mode 100644 index 0000000000000..b1aa6b24ddb73 --- /dev/null +++ b/components/welcome/sources/event-updated/test-event.mjs @@ -0,0 +1,257 @@ +export default { + "primaryLoungeSessionOpen": false, + "eventSeries": null, + "stage": { + "isRecording": false, + "streamingToMobile": false, + "streamingActive": false, + "recordingInstanceReady": true, + "agenda": { + "data": { + "id": "22415", + "type": "agenda", + "attributes": { + "id": 22415, + "hashid": "Y0fAke", + "published": null, + "stageId": 28334, + "unassignedAssetsOrderingOverride": [ + { + "order": 0, + "assetId": "55565", + "assetType": "DynamicEventAsset" + }, + { + "order": 1, + "assetId": "55566", + "assetType": "DynamicEventAsset" + }, + { + "order": 2, + "assetId": "233792", + "assetType": "EventAsset" + }, + { + "order": 3, + "assetId": "55567", + "assetType": "DynamicEventAsset" + } + ], + "durationsEnabled": false, + "agendaItems": { + "data": [] + } + } + } + }, + "id": 28334, + "hashid": "yVfbBW", + "eventId": 26778, + "name": "Test 1", + "description": "", + "status": "closed", + "startTime": "2024-08-16T14:00:00.000Z", + "endTime": "2024-08-16T15:00:00.000Z", + "timeZone": "America/Bogota", + "registerDisplayName": "Register", + "alreadyRegisteredDisplayName": "Already Registered?", + "auditoriumDisplayName": "Auditorium", + "breakoutDisplayName": "Breakout", + "loungeDisplayName": "Lounge", + "primaryInfoDisplayName": "Info", + "secondaryInfoDisplayName": "Secondary Info", + "staffDisplayName": "staff", + "speakerDisplayName": "speaker", + "attendeeDisplayName": "attendee", + "defaultLoungeViewMode": "card", + "speakerGreetingText": null, + "raiseHandEnabled": true, + "primaryInfoPageUrl": null, + "primaryInfoImageUrl": null, + "secondaryInfoPageUrl": null, + "secondaryInfoImageUrl": null, + "stageUrl": "/events/mqubWG/stages/yVfbBW", + "logoUrl": null, + "iconUrl": null, + "coverImageUrl": "https://d30kg4wnmf2rd.cloudfront.net/event_assets/mqubWG/cover/people-and-things.gif-b02f1732-d8aa-4db4-a3e9-a460f68f5bc1", + "publicChatEnabled": true, + "publicChatSlowModeEnabled": false, + "qnaEnabled": true, + "qnaModerationEnabled": false, + "qnaHiddenNamesEnabled": false, + "qnaAllHiddenNamesEnabled": false, + "peopleListEnabled": true, + "peopleListShowCountEnabled": true, + "pollsShowVoteCountEnabled": true, + "directMessagesEnabled": true, + "videoCallEnabled": true, + "highChatTrafficExpected": false, + "breakoutsEnabled": true, + "assetPreviewPopperEnabled": true, + "agendaVisibleEnabled": true, + "closedCaptionsEnabled": true, + "lowerThirdEnabled": true, + "speakerBubblesEnabled": true, + "speakerNamesEnabled": true, + "overlayFadeEnabled": true, + "activeLoungeSessionOpen": false, + "nameTagsAutoDismiss": false, + "translationServiceUrl": null, + "primaryColor": "#000000", + "secondaryColor": "#000000", + "signedStageId": "eyJfcmFpbHMiOnsibWVzc2FnZSI6Ik1qZ3pNelE9IiwiZXhwIjpudWxsLCJwdXIiOiJzdGFnZS93cyJ9fQ==--c4097b23f5d8f62e14d448a621b94114d473b5a2abd1f8bd36467d735a6d192b" + }, + "stages": [ + { + "isRecording": false, + "streamingToMobile": false, + "streamingActive": false, + "recordingInstanceReady": true, + "agenda": { + "data": { + "id": "22415", + "type": "agenda", + "attributes": { + "id": 22415, + "hashid": "Y0fAke", + "published": null, + "stageId": 28334, + "unassignedAssetsOrderingOverride": [ + { + "order": 0, + "assetId": "55565", + "assetType": "DynamicEventAsset" + }, + { + "order": 1, + "assetId": "55566", + "assetType": "DynamicEventAsset" + }, + { + "order": 2, + "assetId": "233792", + "assetType": "EventAsset" + }, + { + "order": 3, + "assetId": "55567", + "assetType": "DynamicEventAsset" + } + ], + "durationsEnabled": false, + "agendaItems": { + "data": [] + } + } + } + }, + "id": 28334, + "hashid": "yVfbBW", + "eventId": 26778, + "name": "Test 1", + "description": "", + "status": "closed", + "startTime": "2024-08-16T14:00:00.000Z", + "endTime": "2024-08-16T15:00:00.000Z", + "timeZone": "America/Bogota", + "registerDisplayName": "Register", + "alreadyRegisteredDisplayName": "Already Registered?", + "auditoriumDisplayName": "Auditorium", + "breakoutDisplayName": "Breakout", + "loungeDisplayName": "Lounge", + "primaryInfoDisplayName": "Info", + "secondaryInfoDisplayName": "Secondary Info", + "staffDisplayName": "staff", + "speakerDisplayName": "speaker", + "attendeeDisplayName": "attendee", + "defaultLoungeViewMode": "card", + "speakerGreetingText": null, + "raiseHandEnabled": true, + "primaryInfoPageUrl": null, + "primaryInfoImageUrl": null, + "secondaryInfoPageUrl": null, + "secondaryInfoImageUrl": null, + "stageUrl": "/events/mqubWG/stages/yVfbBW", + "logoUrl": null, + "iconUrl": null, + "coverImageUrl": "https://d30kg4wnmf2rd.cloudfront.net/event_assets/mqubWG/cover/people-and-things.gif-b02f1732-d8aa-4db4-a3e9-a460f68f5bc1", + "publicChatEnabled": true, + "publicChatSlowModeEnabled": false, + "qnaEnabled": true, + "qnaModerationEnabled": false, + "qnaHiddenNamesEnabled": false, + "qnaAllHiddenNamesEnabled": false, + "peopleListEnabled": true, + "peopleListShowCountEnabled": true, + "pollsShowVoteCountEnabled": true, + "directMessagesEnabled": true, + "videoCallEnabled": true, + "highChatTrafficExpected": false, + "breakoutsEnabled": true, + "assetPreviewPopperEnabled": true, + "agendaVisibleEnabled": true, + "closedCaptionsEnabled": true, + "lowerThirdEnabled": true, + "speakerBubblesEnabled": true, + "speakerNamesEnabled": true, + "overlayFadeEnabled": true, + "activeLoungeSessionOpen": false, + "nameTagsAutoDismiss": false, + "translationServiceUrl": null, + "primaryColor": "#000000", + "secondaryColor": "#000000", + "signedStageId": "eyJfcmFpbHMiOnsibWVzc2FnZSI6Ik1qZ3pNelE9IiwiZXhwIjpudWxsLCJwdXIiOiJzdGFnZS93cyJ9fQ==--c4097b23f5d8f62e14d448a621b94114d473b5a2abd1f8bd36467d735a6d192b" + } + ], + "restrictedSigninTypes": [ + "google", + "linkedin", + "outlook", + "email" + ], + "id": 26778, + "hashid": "mqubWG", + "name": "Test 1", + "description": "", + "updatedAt": "2024-08-15T22:11:38.060Z", + "startTime": "2024-08-16T14:00:00.000Z", + "endTime": "2024-08-16T15:00:00.000Z", + "timeZone": "America/Bogota", + "primaryColor": "#000000", + "secondaryColor": "#000000", + "logoUrl": null, + "iconUrl": null, + "coverImageUrl": "https://d30kg4wnmf2rd.cloudfront.net/event_assets/mqubWG/cover/people-and-things.gif-b02f1732-d8aa-4db4-a3e9-a460f68f5bc1", + "accessType": "registration_required", + "registrationsCount": 1, + "eventUrl": "https://app.experiencewelcome.com/events/mqubWG/stages/yVfbBW", + "eventLoginUrl": "https://app.experiencewelcome.com/events/mqubWG", + "loungeUrl": "/events/mqubWG/stages/yVfbBW?initialStageArea=lounge", + "alwaysShowWelcomeRegForm": false, + "demoMode": false, + "quickRegEnabled": true, + "emailReminder24hPriorEnabled": false, + "emailReminder1hPriorEnabled": false, + "emailPostEventEnabled": false, + "recordingsAvailable": false, + "recordingsAutoPublish": true, + "recordingsRegistrationRequired": false, + "agendaRegistrationRequired": false, + "registerDisplayName": "Register", + "alreadyRegisteredDisplayName": "Already Registered?", + "staffDisplayName": "staff", + "speakerDisplayName": "speaker", + "attendeeDisplayName": "attendee", + "defaultLoungeViewMode": "card", + "speakerGreetingText": null, + "breakoutsEnabled": true, + "speakersCanModerate": false, + "postEventSurvey": true, + "noEmailConfirmation": true, + "clipsCustomLinkText": null, + "clipsCustomLinkUrl": null, + "postEventMarketingUrl": null, + "eventType": "live", + "primaryStageId": 28334, + "welcomeRegRequired": false +}; diff --git a/components/welcome/welcome.app.mjs b/components/welcome/welcome.app.mjs new file mode 100644 index 0000000000000..48860832c1379 --- /dev/null +++ b/components/welcome/welcome.app.mjs @@ -0,0 +1,100 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; + +export default { + type: "app", + app: "welcome", + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Accept": "application/json", + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + patch(args = {}) { + return this._makeRequest({ + method: "PATCH", + ...args, + }); + }, + listEvents(args = {}) { + return this._makeRequest({ + path: "/events", + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, resourceName, + lastDateAt, dateField, + max = constants.DEFAULT_MAX, + }) { + let page = 1; + let resourcesCount = 0; + + while (true) { + const response = + await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + page, + per_page: constants.DEFAULT_LIMIT, + }, + }); + + const nextResources = utils.getNestedProperty(response, resourceName); + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + const isDateGreater = + lastDateAt + && Date.parse(resource[dateField]) >= Date.parse(lastDateAt); + + if (!lastDateAt || isDateGreater) { + yield resource; + resourcesCount += 1; + } + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + if (nextResources.length < constants.DEFAULT_LIMIT) { + console.log("No next page"); + return; + } + + page += 1; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); + }, + }, +}; diff --git a/components/wesupply/README.md b/components/wesupply/README.md new file mode 100644 index 0000000000000..6956d877b18df --- /dev/null +++ b/components/wesupply/README.md @@ -0,0 +1,11 @@ +# Overview + +The WeSupply API serves as a bridge connecting your ecommerce platforms to WeSupply's order tracking and logistics features. By utilizing the API within Pipedream, you unlock the potential to automate order updates, streamline returns, and enhance customer service interactions. Pipedream's serverless platform allows you to create workflows that trigger based on certain events, process data, and connect to countless other APIs and services—essentially turning your WeSupply data into actionable insights and automated tasks without the need for a dedicated backend infrastructure. + +# Example Use Cases + +- **Order Status Sync to Customer Support Platform**: Integrate WeSupply with a customer support platform like Zendesk. When an order status updates in WeSupply, the workflow triggers, fetching the order details and updating the corresponding support ticket in Zendesk. This keeps your support team and customers informed about the latest shipping updates or delays. + +- **Returns Processing Automation**: Automate the returns process by connecting WeSupply to an email service like SendGrid. When a return is initiated in WeSupply, Pipedream triggers a workflow that sends an email with a return label and instructions, streamlining the return process for customers and reducing manual work for your team. + +- **Inventory Level Webhook Notifications**: Pair WeSupply with Slack for real-time inventory updates. Set up a workflow where low inventory levels in WeSupply send an alert to a designated Slack channel. This helps your procurement team stay on top of stock levels and react promptly to avoid stockouts. diff --git a/components/weworkbook/README.md b/components/weworkbook/README.md index a4dbe42d33f58..1b3204711f8fe 100644 --- a/components/weworkbook/README.md +++ b/components/weworkbook/README.md @@ -1,32 +1,11 @@ # Overview -Let’s explore what you can do with the weworkbook API. The [we workbook -API](https://weworkbook.com/) makes it easy to integrate business solutions -with your own applications and tools. With it you can quickly and easily access -core features of our service such as task management, messaging, and analytics. +The Weworkbook API provides access to a suite of tools for educational businesses, such as student management, course planning, and finance tracking. Leveraging this API on Pipedream allows for the automation of these tasks and the integration of data with other apps and services. By combining workflows on Pipedream, you can streamline operations, enhance student engagement, and improve reporting capabilities, among other potential benefits. -Using the weworkbook API, you can develop solutions that make work easier and -more efficient by: +# Example Use Cases -- Managing tasks - Automating tasks, assigning them to individuals or groups, - and tracking progress in real-time. -- Sending messages - Create conversations and participate in threads, add or - remove recipients, hide conversations, and mark messages as read or unread. -- Analyzing data - Analyze data from multiple sources, visualize data, create - reports and dashboards, identify trends, and locate insights. -- Accessing third-party APIs - Connect with a wide range of third-party - applications and data sources to extend the capabilities of the WeWorkbook - platform. +- **Automated Student Enrollment Notifications**: Trigger a workflow when a new student enrolls in a course using Weworkbook. Use the Pipedream's built-in email service or connect to a messaging app like Slack to notify staff instantly, ensuring that no enrollment goes unnoticed and staff can take immediate action. -In addition, with the WeWorkbook API you can build: +- **Course Attendance to Google Sheets**: Each time a student checks into a class, the API can send data to Pipedream, which then logs this attendance in a Google Sheet. This provides an up-to-date and accessible record for educators and administrators to track attendance and participation trends over time. -- Common applications - Create web or mobile applications for team - collaboration and collaboration with external partners. -- Custom solutions - Build interface solutions such as custom links, - multi-screen applications, and integrated tools. -- Reporting tools - Build reporting and visualization tools to gain insights - and track performance. -- Automation solutions - Deliver automated solutions such as automation of - processes and workflows. -- Chat bots - Construct bots that understand natural language and respond to - interactions consistently and intelligently. +- **Dynamic Invoicing with QuickBooks**: When a new invoice is created in Weworkbook, trigger a workflow that captures invoice details and creates a corresponding invoice in QuickBooks. This ensures financial data remains synchronized across both platforms, reducing manual entry and the potential for human error. diff --git a/components/what_are_those/actions/find-sneakers-by-sku/find-sneakers-by-sku.mjs b/components/what_are_those/actions/find-sneakers-by-sku/find-sneakers-by-sku.mjs new file mode 100644 index 0000000000000..61f1296aca567 --- /dev/null +++ b/components/what_are_those/actions/find-sneakers-by-sku/find-sneakers-by-sku.mjs @@ -0,0 +1,31 @@ +import fs from "fs"; +import { checkTmp } from "../../common/utils.mjs"; +import app from "../../what_are_those.app.mjs"; + +export default { + key: "what_are_those-find-sneakers-by-sku", + name: "Find Sneakers by SKU", + description: "Identifies sneakers from a size tag photo and returns sneaker name and details. [See the documentation](https://documenter.getpostman.com/view/3847098/2sAY4rDQDs#4f6a49f9-3393-42cd-8474-3856a79888af)", + version: "0.0.1", + type: "action", + props: { + app, + sizeTagImage: { + type: "string", + label: "Size Tag Image", + description: "The path to the size tag image in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp).", + }, + }, + async run({ $ }) { + const data = fs.readFileSync(checkTmp(this.sizeTagImage)); + const base64Image = Buffer.from(data, "binary").toString("base64"); + + const response = await this.app.identifySneakersFromSizeTag({ + $, + data: base64Image, + }); + + $.export("$summary", `Identified sneaker: ${response}`); + return response; + }, +}; diff --git a/components/what_are_those/actions/grade-sneakers-condition/grade-sneakers-condition.mjs b/components/what_are_those/actions/grade-sneakers-condition/grade-sneakers-condition.mjs new file mode 100644 index 0000000000000..2382e7e4e40dd --- /dev/null +++ b/components/what_are_those/actions/grade-sneakers-condition/grade-sneakers-condition.mjs @@ -0,0 +1,78 @@ +import FormData from "form-data"; +import fs from "fs"; +import { checkTmp } from "../../common/utils.mjs"; +import app from "../../what_are_those.app.mjs"; + +export default { + key: "what_are_those-grade-sneakers-condition", + name: "Grade and Authenticate Sneakers", + description: "Grades and authenticates sneakers using provided images. [See the documentation](https://documenter.getpostman.com/view/3847098/2sAY4rDQDs#13d527e8-5d8f-4511-857c-b40b8dd921b8)", + version: "0.0.1", + type: "action", + props: { + app, + frontImage: { + type: "string", + label: "Front Image", + description: "The path to the front image image in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp).", + }, + leftImage: { + type: "string", + label: "Left Image", + description: "The path to the left image image in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp).", + }, + rightImage: { + type: "string", + label: "Right Image", + description: "The path to the right image image in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp).", + }, + soleImage: { + type: "string", + label: "Sole Image", + description: "The path to the sole image image in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp).", + }, + insoleImage: { + type: "string", + label: "Insole Image", + description: "The path to the insole image image in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp).", + }, + sizeTagImage: { + type: "string", + label: "Size Tag Image", + description: "The path to the sizeTag image image in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp).", + }, + type: { + type: "string", + label: "Use Type", + description: "the type parameter to see specific types of data.", + options: [ + "grading", + "authentication", + ], + optional: true, + }, + }, + async run({ $ }) { + const data = new FormData(); + data.append("image1", fs.createReadStream(checkTmp(this.frontImage))); + data.append("image2", fs.createReadStream(checkTmp(this.leftImage))); + data.append("image3", fs.createReadStream(checkTmp(this.rightImage))); + data.append("image4", fs.createReadStream(checkTmp(this.soleImage))); + data.append("image5", fs.createReadStream(checkTmp(this.insoleImage))); + data.append("image6", fs.createReadStream(checkTmp(this.sizeTagImage))); + + const response = await this.app.gradeAuthenticateSneakers({ + headers: { + ...data.getHeaders(), + }, + data: data, + maxBodyLength: Infinity, + params: { + type: this.type, + }, + timeout: 120000, + }); + $.export("$summary", "Successfully graded and authenticated sneakers."); + return response; + }, +}; diff --git a/components/what_are_those/actions/identify-sneakers-from-photo/identify-sneakers-from-photo.mjs b/components/what_are_those/actions/identify-sneakers-from-photo/identify-sneakers-from-photo.mjs new file mode 100644 index 0000000000000..1ff6a6585f5c2 --- /dev/null +++ b/components/what_are_those/actions/identify-sneakers-from-photo/identify-sneakers-from-photo.mjs @@ -0,0 +1,34 @@ +import FormData from "form-data"; +import fs from "fs"; +import { checkTmp } from "../../common/utils.mjs"; +import app from "../../what_are_those.app.mjs"; + +export default { + key: "what_are_those-identify-sneakers-from-photo", + name: "Identify Sneakers from Photo", + description: "Identifies sneakers from an uploaded image and returns details such as name, links, images, prices, and confidence scores. [See the documentation](https://documenter.getpostman.com/view/3847098/2sAY4rDQDs#957c900c-501f-4c8f-9b8b-71655a8cfb5d).", + version: "0.0.1", + type: "action", + props: { + app, + image: { + type: "string", + label: "Image", + description: "The path to the size tag image in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp).", + }, + }, + async run({ $ }) { + const data = new FormData(); + data.append("image1", fs.createReadStream(checkTmp(this.image))); + + const response = await this.app.identifySneakers({ + headers: { + ...data.getHeaders(), + }, + data: data, + }); + + $.export("$summary", `Identified ${response.names} sneakers successfully`); + return response; + }, +}; diff --git a/components/what_are_those/common/utils.mjs b/components/what_are_those/common/utils.mjs new file mode 100644 index 0000000000000..1a5e36f32a603 --- /dev/null +++ b/components/what_are_those/common/utils.mjs @@ -0,0 +1,6 @@ +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; diff --git a/components/what_are_those/package.json b/components/what_are_those/package.json new file mode 100644 index 0000000000000..16b409180cf3b --- /dev/null +++ b/components/what_are_those/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/what_are_those", + "version": "0.1.0", + "description": "Pipedream What Are Those Components", + "main": "what_are_those.app.mjs", + "keywords": [ + "pipedream", + "what_are_those" + ], + "homepage": "https://pipedream.com/apps/what_are_those", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/what_are_those/what_are_those.app.mjs b/components/what_are_those/what_are_those.app.mjs new file mode 100644 index 0000000000000..b578d247d61eb --- /dev/null +++ b/components/what_are_those/what_are_those.app.mjs @@ -0,0 +1,43 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "what_are_those", + methods: { + _headers(headers = {}) { + return { + "x-api-key": this.$auth.api_key, + ...headers, + }; + }, + _makeRequest({ + $ = this, headers, ...opts + }) { + return axios($, { + headers: this._headers(headers), + ...opts, + }); + }, + identifySneakers(opts = {}) { + return this._makeRequest({ + method: "POST", + url: "https://ayq6s37rv6.execute-api.us-east-1.amazonaws.com/Prod/rec?data_type=multi", + ...opts, + }); + }, + gradeAuthenticateSneakers(opts = {}) { + return this._makeRequest({ + method: "POST", + url: "https://6mdt6kw7ig.execute-api.us-east-1.amazonaws.com/Prod/list?data_type=multi", + ...opts, + }); + }, + identifySneakersFromSizeTag(opts = {}) { + return this._makeRequest({ + method: "POST", + url: "https://0blrzg7ahc.execute-api.us-east-1.amazonaws.com/Prod/sku", + ...opts, + }); + }, + }, +}; diff --git a/components/what_converts/package.json b/components/what_converts/package.json new file mode 100644 index 0000000000000..cd05a068d982b --- /dev/null +++ b/components/what_converts/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/what_converts", + "version": "0.6.0", + "description": "Pipedream what_converts Components", + "main": "what_converts.app.mjs", + "keywords": [ + "pipedream", + "what_converts" + ], + "homepage": "https://pipedream.com/apps/what_converts", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/whatconverts/README.md b/components/whatconverts/README.md new file mode 100644 index 0000000000000..721c69fda52da --- /dev/null +++ b/components/whatconverts/README.md @@ -0,0 +1,11 @@ +# Overview + +The WhatConverts API allows you to track, manage, and report on leads and conversions from various marketing channels. With this API, you can automate the ingestion of lead data into your CRM, generate custom reports, and receive real-time alerts for new leads. When you implement this API within Pipedream, you can create powerful, serverless workflows that respond to events in WhatConverts, or trigger actions based on data from other apps. + +# Example Use Cases + +- **Sync Leads to Google Sheets**: When a new lead is tracked in WhatConverts, use Pipedream to add the lead details to a Google Sheets spreadsheet. This workflow enables a live overview of leads for team members who manage them in Google Sheets. + +- **Create CRM Tasks for New Leads**: For each new lead captured by WhatConverts, create a task in a CRM like Salesforce or HubSpot. This ensures timely follow-up by sales teams, and keeps all lead information centralized. + +- **Send Slack Notifications for Qualified Leads**: Set up a Pipedream workflow that sends a message to a designated Slack channel when a lead meets certain qualification criteria in WhatConverts. This can help sales teams prioritize and respond quickly to high-potential leads. diff --git a/components/whatsable/README.md b/components/whatsable/README.md new file mode 100644 index 0000000000000..f253e061e61ff --- /dev/null +++ b/components/whatsable/README.md @@ -0,0 +1,11 @@ +# Overview + +The WhatsAble API enables automated interactions with WhatsApp, allowing you to send messages, create groups, and manage contacts programmatically. Integrating WhatsAble with Pipedream, you can craft serverless workflows that react to various triggers and perform actions on WhatsApp, like sending notifications or processing incoming messages. Pipedream's capability to connect with hundreds of apps opens up a multitude of automation possibilities, streamlining communication tasks and linking WhatsApp with your digital ecosystem. + +# Example Use Cases + +- **Automated Customer Support Messages**: Trigger a workflow in Pipedream when a new ticket is created in your helpdesk software (like Zendesk). The workflow can send an automated WhatsApp message to the customer, acknowledging the ticket and providing an estimated response time. + +- **E-Commerce Order Updates**: Set up a workflow that listens for new orders from an e-commerce platform like Shopify. Once an order is placed, automatically send an order confirmation and updates on shipping status directly to the customer's WhatsApp. + +- **Survey Responses Collector**: After a user completes a survey on Typeform, trigger a Pipedream workflow that sends a thank-you message via WhatsApp and stores the survey response in a Google Sheet for data analysis and record-keeping. diff --git a/components/whatsable/package.json b/components/whatsable/package.json index 365eec743717d..ff1a8db42ff65 100644 --- a/components/whatsable/package.json +++ b/components/whatsable/package.json @@ -15,4 +15,4 @@ "dependencies": { "@pipedream/platform": "^1.6.0" } -} \ No newline at end of file +} diff --git a/components/whatsapp_business/README.md b/components/whatsapp_business/README.md new file mode 100644 index 0000000000000..18f225d68f874 --- /dev/null +++ b/components/whatsapp_business/README.md @@ -0,0 +1,11 @@ +# Overview + +The WhatsApp Business API on Pipedream is a powerful avenue for automating interactions with customers on WhatsApp. You can send messages, set up automated responses, and manage conversations on a large scale. With Pipedream's serverless platform, you can create workflows that trigger from various events and connect WhatsApp Business with numerous other apps to automate tasks, notify teams, sync data, and more, all without managing infrastructure. + +# Example Use Cases + +- **Customer Support Automation**: Automatically respond to common customer inquiries received on WhatsApp by integrating with a knowledge base or FAQ data store. This can cut down on response time and free up human agents for more complex queries. + +- **Order Confirmation and Updates**: Send automated order confirmations and delivery status updates to customers. This could integrate with your e-commerce platform, like Shopify, to pull in real-time order information and send timely messages directly through WhatsApp. + +- **Event Reminder Notifications**: Sync with a calendar service like Google Calendar to send reminders for appointments or events. When a calendar event is nearing, trigger a WhatsApp message to ensure the participant has all the necessary details. diff --git a/components/whatsapp_business/actions/list-message-templates/list-message-templates.mjs b/components/whatsapp_business/actions/list-message-templates/list-message-templates.mjs index ea5e6480f9b8e..487b4a075ed0d 100644 --- a/components/whatsapp_business/actions/list-message-templates/list-message-templates.mjs +++ b/components/whatsapp_business/actions/list-message-templates/list-message-templates.mjs @@ -6,7 +6,7 @@ export default { key: "whatsapp_business-list-message-templates", name: "List Message Templates", description: `Lists message templates. [See the docs.](${docLink})`, - version: "0.0.2", + version: "0.0.3", type: "action", props: { whatsapp, diff --git a/components/whatsapp_business/actions/send-text-message/send-text-message.mjs b/components/whatsapp_business/actions/send-text-message/send-text-message.mjs index cc3a8826cfc62..f14062eeff901 100644 --- a/components/whatsapp_business/actions/send-text-message/send-text-message.mjs +++ b/components/whatsapp_business/actions/send-text-message/send-text-message.mjs @@ -6,7 +6,7 @@ export default { key: "whatsapp_business-send-text-message", name: "Send Text Message", description: `Sends a text message. [See the docs.](${docLink})`, - version: "0.0.2", + version: "0.0.3", type: "action", props: { whatsapp, diff --git a/components/whatsapp_business/actions/send-text-using-template/send-text-using-template.mjs b/components/whatsapp_business/actions/send-text-using-template/send-text-using-template.mjs index 46ccbeaed4785..741127d614b71 100644 --- a/components/whatsapp_business/actions/send-text-using-template/send-text-using-template.mjs +++ b/components/whatsapp_business/actions/send-text-using-template/send-text-using-template.mjs @@ -7,7 +7,7 @@ export default { key: "whatsapp_business-send-text-using-template", name: "Send Text Using Template", description: `Send a text message using a pre-defined template. Variables can be sent only as text. [See the docs.](${docLink})`, - version: "0.0.4", + version: "0.0.5", type: "action", props: { whatsapp, diff --git a/components/whatsapp_business/actions/send-voice-message/send-voice-message.mjs b/components/whatsapp_business/actions/send-voice-message/send-voice-message.mjs new file mode 100644 index 0000000000000..8058a155efaf2 --- /dev/null +++ b/components/whatsapp_business/actions/send-voice-message/send-voice-message.mjs @@ -0,0 +1,77 @@ +import whatsapp from "../../whatsapp_business.app.mjs"; +import FormData from "form-data"; +import fs from "fs"; + +export default { + key: "whatsapp_business-send-voice-message", + name: "Send Voice Message", + description: "Sends a voice message. [See the documentation](https://developers.facebook.com/docs/whatsapp/cloud-api/messages/audio-messages)", + version: "0.0.1", + type: "action", + props: { + whatsapp, + phoneNumberId: { + propDefinition: [ + whatsapp, + "phoneNumberId", + ], + }, + recipientPhoneNumber: { + propDefinition: [ + whatsapp, + "recipientPhoneNumber", + ], + }, + filePath: { + type: "string", + label: "File Path", + description: "The path to a media file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + }, + type: { + type: "string", + label: "Type", + description: "The mime-type of media file being uploaded", + options: [ + "audio/aac", + "audio/mp4", + "audio/mpeg", + "audio/amr", + "audio/ogg", + "audio/opus", + ], + }, + }, + async run({ $ }) { + // upload media file + const formData = new FormData(); + const content = fs.createReadStream(this.filePath.includes("tmp/") + ? this.filePath + : `/tmp/${this.filePath}`); + formData.append("file", content); + formData.append("type", this.type); + formData.append("messaging_product", "whatsapp"); + const { id: mediaId } = await this.whatsapp.uploadMedia({ + $, + phoneNumberId: this.phoneNumberId, + data: formData, + headers: formData.getHeaders(), + }); + + // send voice message + const response = await this.whatsapp.sendVoiceMessage({ + $, + phoneNumberId: this.phoneNumberId, + data: { + messaging_product: "whatsapp", + recipient_type: "individual", + to: this.recipientPhoneNumber, + type: "audio", + audio: { + id: mediaId, + }, + }, + }); + $.export("$summary", `Sent message successfully to +${this.recipientPhoneNumber}`); + return response; + }, +}; diff --git a/components/whatsapp_business/package.json b/components/whatsapp_business/package.json index 2798c66a31d79..f92aa41447c0c 100644 --- a/components/whatsapp_business/package.json +++ b/components/whatsapp_business/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/whatsapp_business", - "version": "0.0.9", + "version": "0.0.10", "description": "Pipedream WhatsApp Business Components", "main": "whatsapp_business.app.mjs", "keywords": [ @@ -13,6 +13,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.2.0" + "@pipedream/platform": "^1.2.0", + "form-data": "^4.0.0" } } diff --git a/components/whatsapp_business/sources/new-message-sent/new-message-sent.mjs b/components/whatsapp_business/sources/new-message-sent/new-message-sent.mjs index b3f9414e9acae..cd1ff5866c995 100644 --- a/components/whatsapp_business/sources/new-message-sent/new-message-sent.mjs +++ b/components/whatsapp_business/sources/new-message-sent/new-message-sent.mjs @@ -5,7 +5,7 @@ export default { key: "whatsapp_business-new-message-sent", name: "New Message Sent", description: "Emit new event when a new message is sent or received. A Webhook subscribed to field \"messages\" must be set up from the App Dashboard of your [Facebook Developer Account](https://developers.facebook.com/). See [documentation](https://developers.facebook.com/docs/graph-api/webhooks/getting-started#configure-webhooks-product) for more information about Webhook setup.", - version: "0.0.3", + version: "0.0.4", type: "source", dedupe: "unique", props: { diff --git a/components/whatsapp_business/sources/new-message-sent/test-event.mjs b/components/whatsapp_business/sources/new-message-sent/test-event.mjs index 80f3e65275f19..5c0288dcb78f9 100644 --- a/components/whatsapp_business/sources/new-message-sent/test-event.mjs +++ b/components/whatsapp_business/sources/new-message-sent/test-event.mjs @@ -1,39 +1,39 @@ export default { "object": "message_app_business_account", "entry": [ - { - "id": "206552730230215", - "changes": [ - { - "value": { - "messaging_product": "message_app", - "metadata": { - "display_phone_number": "20556478963", - "phone_number_id": "206994163982106" - }, - "contacts": [ + { + "id": "206552730230215", + "changes": [ { - "profile": { - "name": "Charlie" - }, - "wa_id": "19234895230" - } - ], - "messages": [ - { - "from": "19234895230", - "id": "msageid.GCbOTkyMzQ4OTUyMzAVAgASGBR2RDA3RjBCNTQ3OTU3Njg3MzM1QgA=", - "timestamp": "1789256532", - "text": { - "body": "Check" - }, - "type": "text" - } - ] - }, - "field": "messages" - } - ] - } - ] - } \ No newline at end of file + "value": { + "messaging_product": "message_app", + "metadata": { + "display_phone_number": "20556478963", + "phone_number_id": "206994163982106", + }, + "contacts": [ + { + "profile": { + "name": "Charlie", + }, + "wa_id": "19234895230", + }, + ], + "messages": [ + { + "from": "19234895230", + "id": "msageid.GCbOTkyMzQ4OTUyMzAVAgASGBR2RDA3RjBCNTQ3OTU3Njg3MzM1QgA=", + "timestamp": "1789256532", + "text": { + "body": "Check", + }, + "type": "text", + }, + ], + }, + "field": "messages", + }, + ], + }, + ], +}; diff --git a/components/whatsapp_business/whatsapp_business.app.mjs b/components/whatsapp_business/whatsapp_business.app.mjs index 4b36511baa7a0..b3685557b2170 100644 --- a/components/whatsapp_business/whatsapp_business.app.mjs +++ b/components/whatsapp_business/whatsapp_business.app.mjs @@ -101,7 +101,7 @@ export default { return data; }, async _makeRequest({ - $ = this, path, ...opts + $ = this, path, headers, ...opts }) { return axios($, { ...opts, @@ -109,6 +109,7 @@ export default { headers: { "Authorization": `Bearer ${this._auth()}`, "Content-Type": "application/json", + ...headers, }, }); }, @@ -196,5 +197,23 @@ export default { }, }); }, + uploadMedia({ + phoneNumberId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/${phoneNumberId}/media`, + ...opts, + }); + }, + sendVoiceMessage({ + phoneNumberId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/${phoneNumberId}/messages`, + ...opts, + }); + }, }, }; diff --git a/components/whautomate/actions/assign-tags-contact/assign-tags-contact.mjs b/components/whautomate/actions/assign-tags-contact/assign-tags-contact.mjs new file mode 100644 index 0000000000000..46d248b7facdf --- /dev/null +++ b/components/whautomate/actions/assign-tags-contact/assign-tags-contact.mjs @@ -0,0 +1,45 @@ +import { parseObject } from "../../common/utils.mjs"; +import whautomate from "../../whautomate.app.mjs"; + +export default { + key: "whautomate-assign-tags-contact", + name: "Assign Tags to Contact", + description: "Assign one or more tags to an existing contact. [See the documentation](https://help.whautomate.com/product-guides/whautomate-rest-api/contacts#/v1-contacts-contactid-1)", + version: "0.0.1", + type: "action", + props: { + whautomate, + contactId: { + propDefinition: [ + whautomate, + "contactId", + ], + }, + contactTags: { + propDefinition: [ + whautomate, + "contactTags", + ], + }, + }, + async run({ $ }) { + const contact = await this.whautomate.getContact(this.contactId); + const response = await this.whautomate.updateContact({ + $, + contactId: this.contactId, + data: { + ...contact, + tags: [ + ...new Set([ + ...(contact.tags + ? contact.tags + : []), + ...parseObject(this.contactTags), + ]), + ], + }, + }); + $.export("$summary", `Successfully assigned tags to contact ${this.contactId}`); + return response; + }, +}; diff --git a/components/whautomate/actions/create-contact/create-contact.mjs b/components/whautomate/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..831f33ea41246 --- /dev/null +++ b/components/whautomate/actions/create-contact/create-contact.mjs @@ -0,0 +1,80 @@ +import { STAGE_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import whautomate from "../../whautomate.app.mjs"; + +export default { + key: "whautomate-create-contact", + name: "Create Contact", + description: "Create a new contact associated with a WhatsApp number. [See the documentation](https://help.whautomate.com/product-guides/whautomate-rest-api/contacts#/v1-contacts-1)", + version: "0.0.1", + type: "action", + props: { + whautomate, + name: { + type: "string", + label: "Name", + description: "The name of the contact", + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The WhatsApp phone number of the contact. Format: +15555555555", + }, + locationId: { + propDefinition: [ + whautomate, + "locationId", + ], + }, + stage: { + type: "string", + label: "Stage", + description: "The Contact Stage", + optional: true, + options: STAGE_OPTIONS, + }, + tags: { + propDefinition: [ + whautomate, + "contactTags", + ], + optional: true, + }, + customFields: { + type: "object", + label: "Custom Fields", + description: "The contact custom fields.", + optional: true, + }, + notes: { + type: "string", + label: "Notes", + description: "The contact notes.", + optional: true, + }, + }, + async run({ $ }) { + const { + whautomate, + locationId, + tags, + customFields, + ...data + } = this; + + const response = await whautomate.createContact({ + $, + data: { + ...data, + location: { + id: locationId, + }, + tags: parseObject(tags), + customFields: parseObject(customFields), + }, + }); + + $.export("$summary", `Successfully created contact with ID ${response.id}`); + return response; + }, +}; diff --git a/components/whautomate/actions/send-whatsapp-template-message/send-whatsapp-template-message.mjs b/components/whautomate/actions/send-whatsapp-template-message/send-whatsapp-template-message.mjs new file mode 100644 index 0000000000000..3ab987bbd1810 --- /dev/null +++ b/components/whautomate/actions/send-whatsapp-template-message/send-whatsapp-template-message.mjs @@ -0,0 +1,86 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import whautomate from "../../whautomate.app.mjs"; + +export default { + key: "whautomate-send-whatsapp-template-message", + name: "Send WhatsApp Template Message", + description: "Send a pre-defined WhatsApp message template to a contact. [See the documentation](https://help.whautomate.com/product-guides/whautomate-rest-api/messages)", + version: "0.0.1", + type: "action", + props: { + whautomate, + contactId: { + propDefinition: [ + whautomate, + "contactId", + ], + }, + templateName: { + type: "string", + label: "Template Name", + description: "The WhatsApp Template from your Whautomate Account.", + }, + templateLanguage: { + type: "string", + label: "Template Language", + description: "The language of the WhatsApp Template.", + }, + locationId: { + propDefinition: [ + whautomate, + "locationId", + ], + }, + headerMediaUrl: { + type: "string", + label: "Header Media URL", + description: "The URL of the header media.", + optional: true, + }, + headerTextParameters: { + type: "string[]", + label: "Header Text Parameters", + description: "The variables used in the header of your WhatsApp Template.", + optional: true, + }, + bodyTextParameters: { + type: "string[]", + label: "Body Text Parameters", + description: "The variables used in the body of your WhatsApp Template.", + optional: true, + }, + buttonUrlParameters: { + type: "string[]", + label: "Button URL Parameters", + description: "The placeholders used in the buttons of your WhatsApp Template.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.whautomate.sendWhatsAppMessageTemplate({ + $, + data: { + contact: { + id: this.contactId, + }, + template: { + name: this.templateName, + language: this.templateLanguage, + }, + location: { + id: this.locationId, + }, + headerMediaUrl: this.headerMediaUrl, + headerTextParameters: parseObject(this.headerTextParameters), + bodyTextParameters: parseObject(this.bodyTextParameters), + buttonUrlParameters: parseObject(this.buttonUrlParameters), + }, + }); + + if (response.error) throw new ConfigurationError(response.error); + + $.export("$summary", `Successfully sent template message to ${this.locationId}`); + return response; + }, +}; diff --git a/components/whautomate/common/constants.mjs b/components/whautomate/common/constants.mjs new file mode 100644 index 0000000000000..b8c0e3ab5cb54 --- /dev/null +++ b/components/whautomate/common/constants.mjs @@ -0,0 +1,10 @@ +export const STAGE_OPTIONS = [ + "Subscriber", + "Lead", + "Marketing qualified lead (MQL)", + "Sales qualified lead (SQL)", + "Opportunity", + "Customer", + "Evangelist", + "Other", +]; diff --git a/components/whautomate/common/utils.mjs b/components/whautomate/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/whautomate/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/whautomate/package.json b/components/whautomate/package.json new file mode 100644 index 0000000000000..4222001e9221f --- /dev/null +++ b/components/whautomate/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/whautomate", + "version": "0.1.0", + "description": "Pipedream Whautomate Components", + "main": "whautomate.app.mjs", + "keywords": [ + "pipedream", + "whautomate" + ], + "homepage": "https://pipedream.com/apps/whautomate", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} + diff --git a/components/whautomate/sources/common/base.mjs b/components/whautomate/sources/common/base.mjs new file mode 100644 index 0000000000000..74f2ca6a544f4 --- /dev/null +++ b/components/whautomate/sources/common/base.mjs @@ -0,0 +1,58 @@ +import whautomate from "../../whautomate.app.mjs"; + +export default { + props: { + whautomate, + http: { + type: "$.interface.http", + customResponse: false, + }, + db: "$.service.db", + name: { + type: "string", + label: "Name", + description: "The name of the webhook.", + }, + }, + methods: { + _setWehookId(webhookId) { + this.db.set("webhookId", webhookId); + }, + _getWebhookId() { + return this.db.get("webhookId"); + }, + }, + hooks: { + async activate() { + const data = await this.whautomate.createWebhook({ + data: { + webhookHeaders: [ + { + "key": "Content-Type", + "value": "application/json", + }, + ], + events: this.getEvent(), + name: this.name, + serverUrl: this.http.endpoint, + active: true, + }, + }); + this._setWehookId(data.id); + }, + async deactivate() { + const webhookId = this._getWebhookId(); + await this.whautomate.deleteWebhook(webhookId); + }, + }, + async run({ body }) { + if (body.event) { + const ts = Date.parse(body.event.timeStamp); + this.$emit(body, { + id: body.event.id, + summary: this.getSummary(body), + ts, + }); + } + }, +}; diff --git a/components/whautomate/sources/new-appointment-cancelled-instant/new-appointment-cancelled-instant.mjs b/components/whautomate/sources/new-appointment-cancelled-instant/new-appointment-cancelled-instant.mjs new file mode 100644 index 0000000000000..40608ad75898f --- /dev/null +++ b/components/whautomate/sources/new-appointment-cancelled-instant/new-appointment-cancelled-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "whautomate-new-appointment-cancelled-instant", + name: "New Appointment Cancelled (Instant)", + description: "Emit new event when an appointment is cancelled in Whautomate.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return [ + "appointment_cancelled", + ]; + }, + getSummary(body) { + return `Appointment cancelled: ${body.appointment.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/whautomate/sources/new-appointment-cancelled-instant/test-event.mjs b/components/whautomate/sources/new-appointment-cancelled-instant/test-event.mjs new file mode 100644 index 0000000000000..9bd10ee184ee4 --- /dev/null +++ b/components/whautomate/sources/new-appointment-cancelled-instant/test-event.mjs @@ -0,0 +1,48 @@ +export default { + "event": { + "id": "nmgGHJrt6RTG", + "type": "appointment_cancelled", + "key": "nmgGHJrt6RTG", + "timeStamp": "2024-08-05T19:40:05.316Z" + }, + "appointment": { + "id": "nmgGHJrt6RTG", + "location": { + "id": "nmgGHJrt6RTG", + "title": "Test" + }, + "client": { + "id": "nmgGHJrt6RTG", + "fullName": "Client test", + "countryCode": "+1", + "phone": "12345678901", + "email": "" + }, + "staff": { + "id": "nmgGHJrt6RTG", + "name": "Staff Name" + }, + "timezone": "America/Sao_Paulo", + "overrideTimeSlotValidation": false, + "date": "2024-08-05", + "time": "04:39 PM", + "startTimeUTC": "2024-08-05T19:39:00.000Z", + "startTime": "2024-08-05T16:39:00.000Z", + "endTimeUTC": "2024-08-05T20:09:00.000Z", + "endTime": "2024-08-05T17:09:00.000Z", + "service": { + "id": "nmgGHJrt6RTG", + "name": "Consultation", + "durationMinutes": 30, + "sellingPrice": 100.27 + }, + "addOnServices": [], + "bookedFrom": "Admin Portal", + "status": "Cancelled", + "appointmentType": "LOCATION_VISIT", + "notes": "", + "paymentStatus": "", + "createdAt": "2024-08-05T19:39:22.511Z", + "updatedAt": "2024-08-05T19:40:04.442Z" + } +} \ No newline at end of file diff --git a/components/whautomate/sources/new-appointment-scheduled-instant/new-appointment-scheduled-instant.mjs b/components/whautomate/sources/new-appointment-scheduled-instant/new-appointment-scheduled-instant.mjs new file mode 100644 index 0000000000000..109571479cdb6 --- /dev/null +++ b/components/whautomate/sources/new-appointment-scheduled-instant/new-appointment-scheduled-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "whautomate-new-appointment-scheduled-instant", + name: "New Appointment Scheduled (Instant)", + description: "Emit new event when a new appointment is scheduled in Whautomate.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return [ + "appointment_scheduled", + ]; + }, + getSummary(body) { + return `New appointment scheduled for client ${body.appointment.client.fullName}`; + }, + }, + sampleEmit, +}; diff --git a/components/whautomate/sources/new-appointment-scheduled-instant/test-event.mjs b/components/whautomate/sources/new-appointment-scheduled-instant/test-event.mjs new file mode 100644 index 0000000000000..64bbe24d411a2 --- /dev/null +++ b/components/whautomate/sources/new-appointment-scheduled-instant/test-event.mjs @@ -0,0 +1,48 @@ +export default { + "event": { + "id": "nmgGHJrt6RTG", + "type": "appointment_scheduled", + "key": "nmgGHJrt6RTG", + "timeStamp": "2024-08-05T19:39:22.761Z" + }, + "appointment": { + "id": "nmgGHJrt6RTG", + "location": { + "id": "nmgGHJrt6RTG", + "title": "Test" + }, + "client": { + "id": "nmgGHJrt6RTG", + "fullName": "Client test", + "countryCode": "+1", + "phone": "1245678901", + "email": "" + }, + "staff": { + "id": "nmgGHJrt6RTG", + "name": "Staff Name" + }, + "timezone": "America/Sao_Paulo", + "overrideTimeSlotValidation": false, + "date": "2024-08-05", + "time": "04:39 PM", + "startTimeUTC": "2024-08-05T19:39:00.000Z", + "startTime": "2024-08-05T16:39:00.000Z", + "endTimeUTC": "2024-08-05T20:09:00.000Z", + "endTime": "2024-08-05T17:09:00.000Z", + "service": { + "id": "nmgGHJrt6RTG", + "name": "Consultation", + "durationMinutes": 30, + "sellingPrice": 100.27 + }, + "addOnServices": [], + "bookedFrom": "Admin Portal", + "status": "Booked", + "appointmentType": "LOCATION_VISIT", + "notes": "", + "paymentStatus": "", + "createdAt": "2024-08-05T19:39:22.511Z", + "updatedAt": "2024-08-05T19:39:22.511Z" + } +} \ No newline at end of file diff --git a/components/whautomate/sources/new-client-created-instant/new-client-created-instant.mjs b/components/whautomate/sources/new-client-created-instant/new-client-created-instant.mjs new file mode 100644 index 0000000000000..d8f9f4ef9d869 --- /dev/null +++ b/components/whautomate/sources/new-client-created-instant/new-client-created-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "whautomate-new-client-created-instant", + name: "New Client Created (Instant)", + description: "Emit new event when a new client is created in Whautomate.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return [ + "client_created", + ]; + }, + getSummary(body) { + return `New client created: ${body.client.fullName}`; + }, + }, + sampleEmit, +}; diff --git a/components/whautomate/sources/new-client-created-instant/test-event.mjs b/components/whautomate/sources/new-client-created-instant/test-event.mjs new file mode 100644 index 0000000000000..04cac65838ed8 --- /dev/null +++ b/components/whautomate/sources/new-client-created-instant/test-event.mjs @@ -0,0 +1,39 @@ +export default { + "event": { + "id": "nCVBNhfgYU", + "type": "client_created", + "key": "nCVBNhfgYU", + "timeStamp": "2024-08-05T19:32:15.324Z" + }, + "client": { + "id": "nCVBNhfgYU", + "clientId": "C2", + "fullName": "client test", + "preferredName": "", + "contactType": "MOBILE", + "countryCode": "+1", + "phone": "12345678901", + "email": "", + "registrationDate": "2024-08-05T19:31:52.562Z", + "dob": null, + "addressType": "", + "address": "", + "identificationNumber": "", + "gender": "", + "preferredLanguage": "", + "maritalStatus": "", + "tags": [], + "notes": "", + "referralSource": "", + "primaryLocation": { + "id": "nCVBNhfgYU", + "title": "Test" + }, + "emergencyRelationType": "", + "emergencyName": "", + "emergencyCountryCode": "+52", + "emergencyPhone": "", + "createdAt": "2024-08-05T19:32:15.046Z", + "updatedAt": "2024-08-05T19:32:15.046Z" + } +} \ No newline at end of file diff --git a/components/whautomate/whautomate.app.mjs b/components/whautomate/whautomate.app.mjs new file mode 100644 index 0000000000000..bd017eea1e4fd --- /dev/null +++ b/components/whautomate/whautomate.app.mjs @@ -0,0 +1,138 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "whautomate", + propDefinitions: { + contactId: { + type: "string", + label: "Contact ID", + description: "The unique identifier of the contact", + async options({ page }) { + const data = await this.listContacts({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + contactTags: { + type: "string[]", + label: "Contact Tags", + description: "An array of contact tags.", + async options({ page }) { + const data = await this.listContactTags({ + params: { + page: page + 1, + }, + }); + + return data.map(({ name }) => name); + }, + }, + locationId: { + type: "string", + label: "Location Id", + description: "The location id of the contact", + async options({ page }) { + const data = await this.listLocations({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.whautomate.com/v1"; + }, + _headers() { + return { + "x-api-key": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contacts", + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + }); + }, + getContact(contactId) { + return this._makeRequest({ + path: `/contacts/${contactId}`, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts", + ...opts, + }); + }, + listContactTags(opts = {}) { + return this._makeRequest({ + path: "/contactTags", + ...opts, + }); + }, + listLocations(opts = {}) { + return this._makeRequest({ + path: "/locations", + ...opts, + }); + }, + updateContact({ + contactId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/contacts/${contactId}`, + ...opts, + }); + }, + sendWhatsAppMessageTemplate(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/messages/whatsapp/sendtemplate", + ...opts, + }); + }, + }, +}; diff --git a/components/white_swan/README.md b/components/white_swan/README.md new file mode 100644 index 0000000000000..02c0e8ecf9d46 --- /dev/null +++ b/components/white_swan/README.md @@ -0,0 +1,11 @@ +# Overview + +The White Swan API provides predictive analytics for mitigating risks in financial portfolios using artificial intelligence. By leveraging the White Swan API in Pipedream, you can automate the process of gathering insights, monitoring market conditions, and integrating advanced risk analysis into your existing financial systems. With Pipedream's serverless platform, you can construct workflows to react in real-time to data from White Swan, send alerts, and even automate trades or adjustments based on risk thresholds or predictive signals. + +# Example Use Cases + +- **Automated Risk Alerts**: Use White Swan's predictive analytics to monitor portfolio risk and set up a Pipedream workflow that sends you real-time notifications via email, SMS, or Slack when certain risk thresholds are crossed. This immediate feedback allows you to take swift action to mitigate potential losses. + +- **Dynamic Portfolio Rebalancing**: Create a Pipedream workflow that automatically triggers a rebalancing of your investment portfolio using White Swan's risk predictions. Integrate with a trading platform like Alpaca to execute trades that align your portfolio with the desired risk profile, based on the latest AI-driven insights. + +- **Market Sentiment Analysis**: Combine White Swan's market sentiment data with sentiment analysis from Twitter or news sources using Pipedream. Correlate the different sentiment indicators and use the results to adjust your investment strategy, or to feed into machine learning models for more nuanced decision-making. diff --git a/components/whoisfreaks/README.md b/components/whoisfreaks/README.md new file mode 100644 index 0000000000000..02a045d5df7e0 --- /dev/null +++ b/components/whoisfreaks/README.md @@ -0,0 +1,11 @@ +# Overview + +The WhoisFreaks API offers a way to dig into the riches of domain registration data. With this API, you can retrieve extensive WHOIS records, including registrar details, domain status, creation and expiration dates, and registrant information, among other data. When integrated with Pipedream's serverless platform, you unlock the potential to automate domain research, track domain registrations, monitor changes, and connect insights with other apps for a multitude of purposes. + +# Example Workflows + +- **Domain Registration Monitoring**: Build a workflow on Pipedream that tracks domain registrations for specific keywords. When a new domain containing the keyword is registered, use WhoisFreaks to fetch WHOIS data and send alerts via Slack or email to the interested parties. + +- **Trademark Protection**: Set up a Pipedream workflow to monitor the registration of domains similar to your trademarks. WhoisFreaks can be used to identify potential trademark infringements, and you can connect to a CRM like Salesforce to initiate a follow-up action or legal response. + +- **Competitive Intelligence Gathering**: Create a Pipedream workflow to perform regular searches for domains registered by your competitors. Utilize WhoisFreaks API to extract relevant data and compile it into a report, which can be periodically sent to Google Sheets for analysis and shared with your team. diff --git a/components/whoisfreaks/actions/domain-lookup/domain-lookup.mjs b/components/whoisfreaks/actions/domain-lookup/domain-lookup.mjs new file mode 100644 index 0000000000000..b1ae422b07809 --- /dev/null +++ b/components/whoisfreaks/actions/domain-lookup/domain-lookup.mjs @@ -0,0 +1,45 @@ +import whoisfreaks from "../../whoisfreaks.app.mjs"; + +export default { + key: "whoisfreaks-domain-lookup", + name: "Domain Lookup", + description: "Retrieve details about a domain name. [See the documentation](https://whoisfreaks.com/products/whois-api#live_lookup)", + version: "0.0.1", + type: "action", + props: { + whoisfreaks, + domainName: { + propDefinition: [ + whoisfreaks, + "domainName", + ], + }, + lookupType: { + type: "string", + label: "Lookup Type", + description: "Whether to perform a `live` or `historical` lookup", + options: [ + "live", + "historical", + ], + }, + format: { + propDefinition: [ + whoisfreaks, + "format", + ], + }, + }, + async run({ $ }) { + const response = await this.whoisfreaks.domainLookup({ + $, + params: { + domainName: this.domainName, + whois: this.lookupType, + format: this.format, + }, + }); + $.export("$summary", `Successfully performed lookup for domain ${this.domainName}`); + return response; + }, +}; diff --git a/components/whoisfreaks/actions/ip-lookup/ip-lookup.mjs b/components/whoisfreaks/actions/ip-lookup/ip-lookup.mjs new file mode 100644 index 0000000000000..1ddf8a5d085eb --- /dev/null +++ b/components/whoisfreaks/actions/ip-lookup/ip-lookup.mjs @@ -0,0 +1,34 @@ +import whoisfreaks from "../../whoisfreaks.app.mjs"; + +export default { + key: "whoisfreaks-ip-lookup", + name: "IP Lookup", + description: "Retrieve information about an IP address. [See the documentation](https://whoisfreaks.com/products/whois-api#ip_lookup)", + version: "0.0.1", + type: "action", + props: { + whoisfreaks, + ip: { + type: "string", + label: "IP", + description: "IPv4 or IPv6 address for the requested whois", + }, + format: { + propDefinition: [ + whoisfreaks, + "format", + ], + }, + }, + async run({ $ }) { + const response = await this.whoisfreaks.ipLookup({ + $, + params: { + ip: this.ip, + format: this.format, + }, + }); + $.export("$summary", `Successfully performed lookup for IP ${this.ip}`); + return response; + }, +}; diff --git a/components/whoisfreaks/actions/reverse-lookup/reverse-lookup.mjs b/components/whoisfreaks/actions/reverse-lookup/reverse-lookup.mjs new file mode 100644 index 0000000000000..631b81e25030d --- /dev/null +++ b/components/whoisfreaks/actions/reverse-lookup/reverse-lookup.mjs @@ -0,0 +1,76 @@ +import whoisfreaks from "../../whoisfreaks.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "whoisfreaks-reverse-lookup", + name: "Reverse Lookup", + description: "Retrieve details about a domain by keyword, email, registrant name or company. [See the documentation](https://whoisfreaks.com/products/whois-api#reverse_lookup)", + version: "0.0.1", + type: "action", + props: { + whoisfreaks, + keyword: { + type: "string", + label: "Keyword", + description: "Keyword search utilizes case-insensitive Pattern Matching search technique to search in our historical database. For example, `whoisfreaks` matches with any keyword that have the searched pattern like `mywhoisfreaks` and `whoisfreakscom`.", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "Email search uses case-insensitive exact matching technique to search in our historical database. For example, `support@whoisfreaks.com` matches only with `support@whoisfreaks.com`.", + optional: true, + }, + owner: { + type: "string", + label: "Owner", + description: "The owner name for requested whois", + optional: true, + }, + company: { + type: "string", + label: "Company", + description: "Registrant name or Company search use full-text search technique to search in our historical database. For example, `whoisfreaks` matched with `whoisfreaks`, `whoisfreak`, `whois`, `mywhoisfreaks` and `whoisfreakscom`.", + optional: true, + }, + format: { + propDefinition: [ + whoisfreaks, + "format", + ], + }, + page: { + type: "integer", + label: "Page", + description: "For getting next or desired page of whois info. Default: `1`", + default: 1, + optional: true, + }, + }, + async run({ $ }) { + if ([ + this.keyword, + this.email, + this.owner, + this.company, + ].filter((v) => v !== undefined).length !== 1) { + throw new ConfigurationError("Must enter one and only one of `keyword`, `email`, `owner`, or `company`"); + } + + const response = await this.whoisfreaks.domainLookup({ + $, + params: { + keyword: this.keyword, + email: this.email, + owner: this.owner, + company: this.company, + whois: "reverse", + format: this.format, + page: this.page, + }, + }); + + $.export("$summary", "Successfully performed reverse lookup"); + return response; + }, +}; diff --git a/components/whoisfreaks/package.json b/components/whoisfreaks/package.json new file mode 100644 index 0000000000000..db1a590a2491b --- /dev/null +++ b/components/whoisfreaks/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/whoisfreaks", + "version": "0.1.0", + "description": "Pipedream WhoisFreaks Components", + "main": "whoisfreaks.app.mjs", + "keywords": [ + "pipedream", + "whoisfreaks" + ], + "homepage": "https://pipedream.com/apps/whoisfreaks", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/whoisfreaks/whoisfreaks.app.mjs b/components/whoisfreaks/whoisfreaks.app.mjs new file mode 100644 index 0000000000000..14e41129ac779 --- /dev/null +++ b/components/whoisfreaks/whoisfreaks.app.mjs @@ -0,0 +1,55 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "whoisfreaks", + propDefinitions: { + domainName: { + type: "string", + label: "Domain Name", + description: "The domain name to lookup", + }, + format: { + type: "string", + label: "Format", + description: "Two formats are available JSON, XML. If you don't specify the 'format' parameter, the default format will be JSON.", + options: [ + "JSON", + "XML", + ], + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.whoisfreaks.com/v1.0"; + }, + _makeRequest({ + $ = this, + path, + params, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + params: { + ...params, + apiKey: this.$auth.api_key, + }, + ...opts, + }); + }, + domainLookup(opts = {}) { + return this._makeRequest({ + path: "/whois", + ...opts, + }); + }, + ipLookup(opts = {}) { + return this._makeRequest({ + path: "/ip-whois", + ...opts, + }); + }, + }, +}; diff --git a/components/whop/README.md b/components/whop/README.md new file mode 100644 index 0000000000000..73f9f2fbe195e --- /dev/null +++ b/components/whop/README.md @@ -0,0 +1,11 @@ +# Overview + +The Whop API provides access to a marketplace for buying and selling software companies. On Pipedream, you can leverage the Whop API to craft serverless workflows that automate tasks like tracking sales, managing memberships, and integrating with other services for a comprehensive business management solution. It’s great for creating custom alerts, syncing data across platforms, and much more, all without writing a line of server-side code. + +# Example Use Cases + +- **Automated Membership Updates**: Use the Whop API to monitor changes in memberships. When a new user signs up or a membership is canceled, trigger a workflow that updates your CRM or sends a personalized welcome or cancellation email via SendGrid. + +- **Real-time Sales Notifications**: Set up a workflow to receive instant notifications through Slack or SMS using Twilio whenever a sale occurs. This allows you to stay on top of your business performance and engage with your team instantly about sales milestones. + +- **Sync User Data with Google Sheets**: Automatically update a Google Sheets spreadsheet with user data from Whop whenever a new user registers. This can help with reporting, analytics, or to feed data into other marketing automation tools. diff --git a/components/whosonlocation/README.md b/components/whosonlocation/README.md new file mode 100644 index 0000000000000..dff1b891aadb2 --- /dev/null +++ b/components/whosonlocation/README.md @@ -0,0 +1,11 @@ +# Overview + +The WhosOnLocation API allows you to manage and track people's presence in various locations, such as office buildings, worksites, and campuses. In Pipedream, you can leverage this API to create automations that streamline visitor management, employee attendance, and emergency roll-calls. By crafting serverless workflows, you can respond to events in real-time, synchronize data with other systems, and enhance security and compliance processes. + +# Example Use Cases + +- **Visitor Registration Automation**: Automate the visitor sign-in process by integrating WhosOnLocation with Pipedream. Create a workflow that triggers when a visitor fills out a form in your preferred app, such as Typeform. The workflow captures the submission, registers the visitor in WhosOnLocation, and can even send personalized entry instructions via email or SMS through integrations with SendGrid or Twilio. + +- **Real-Time Occupancy Alerting**: Set up a workflow that monitors the number of people on-site using WhosOnLocation's API. When occupancy levels hit certain thresholds, Pipedream can trigger notifications to Slack to inform facilities managers in real-time. This ensures that safety and social distancing regulations are adhered to and allows for swift action when needed. + +- **Emergency Roll-Call Check-In**: In emergencies, quickly account for everyone's safety with a workflow that interfaces with WhosOnLocation during an evacuation. The workflow could be configured to send an automated message via an app like Twilio to all individuals on-site, asking them to check in. Responses are then collected and updated in WhosOnLocation, helping emergency responders and company officials to track everyone's status efficiently. diff --git a/components/wicked_reports/README.md b/components/wicked_reports/README.md new file mode 100644 index 0000000000000..092265f438d97 --- /dev/null +++ b/components/wicked_reports/README.md @@ -0,0 +1,11 @@ +# Overview + +The Wicked Reports API offers analytical insights into marketing performance, tracking customer journeys, sales, and ROI over time. On Pipedream, you can harness these insights to trigger actions, feed data into other apps, or customize reporting. By integrating Wicked Reports API, you can automate data collection, react to new marketing data in real-time, and build workflows that connect marketing performance to other business processes. + +# Example Use Cases + +- **Sync Marketing Data with CRM**: Automatically update your CRM records when a new sale or lead is detected in Wicked Reports. This keeps your sales team informed with the latest conversion data. + +- **Email Campaign Analysis**: After sending out an email campaign, use the Wicked Reports API to gather campaign performance data and send it to a Google Sheet for easy analysis and visualization. + +- **Real-time Alerts for Marketing Performance**: Set up a workflow that monitors key performance indicators from Wicked Reports. When thresholds are crossed (like a sudden drop in ROI or a spike in lead generation), trigger notifications via Slack or email to prompt immediate action. diff --git a/components/wicked_reports/package.json b/components/wicked_reports/package.json index 7c683c0079c79..442ce3d655d12 100644 --- a/components/wicked_reports/package.json +++ b/components/wicked_reports/package.json @@ -13,4 +13,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/wildapricot/README.md b/components/wildapricot/README.md new file mode 100644 index 0000000000000..3ccaf10b7705a --- /dev/null +++ b/components/wildapricot/README.md @@ -0,0 +1,11 @@ +# Overview + +The WildApricot API opens up a world of possibilities for managing your membership-driven organization efficiently by automating tasks and integrating with other services. With this API on Pipedream, you can craft workflows that manipulate member data, automate event creation and management, handle financial operations such as invoicing, and engage with members through tailored communication. Pipedream's serverless platform simplifies the process of setting up these automations with pre-built components and easy-to-use triggers and actions for the WildApricot API. + +# Example Use Cases + +- **Automated New Member Onboarding**: Set up a workflow that triggers when a new member is added to WildApricot. The workflow could send a welcome email, create a new contact in a CRM like Salesforce, and add the member to a Slack channel for real-time team updates. + +- **Event Registration and Follow-Up**: Create an automation that triggers each time a member registers for an event on WildApricot. The workflow can register the member for a Zoom webinar, send a calendar invite via Google Calendar, and follow up with an email containing event details and resources. + +- **Membership Renewal Reminders**: Construct a workflow that monitors membership expiration dates. When a membership is nearing its end, automate a reminder email to the member with renewal instructions, log a follow-up task in a tool like Trello, and update a Google Sheet tracking renewals. diff --git a/components/wildapricot/actions/add-update-contact-member/add-update-contact-member.mjs b/components/wildapricot/actions/add-update-contact-member/add-update-contact-member.mjs new file mode 100644 index 0000000000000..b2058335ca0b6 --- /dev/null +++ b/components/wildapricot/actions/add-update-contact-member/add-update-contact-member.mjs @@ -0,0 +1,80 @@ +import wildapricot from "../../wildapricot.app.mjs"; + +export default { + key: "wildapricot-add-update-contact-member", + name: "Add or Update Contact or Member", + description: "Adds or updates a contact or member details in the user's WildApricot database. [See the documentation](https://app.swaggerhub.com/apis-docs/WildApricot/wild-apricot_public_api/7.24.0#/Contacts/CreateContact)", + version: "0.0.1", + type: "action", + props: { + wildapricot, + accountId: { + propDefinition: [ + wildapricot, + "accountId", + ], + reloadProps: true, + }, + contactId: { + propDefinition: [ + wildapricot, + "contactId", + (c) => ({ + accountId: c.accountId, + }), + ], + optional: true, + }, + }, + async additionalProps() { + const props = {}; + const fields = await this.wildapricot.listContactFields({ + accountId: this.accountId, + }); + for (const field of fields) { + if (field.IsEditable && field.Access === "Public") { + props[field.Id] = { + type: "string", + label: field.FieldName, + optional: true, + }; + } + } + return props; + }, + async run({ $ }) { + const fields = await this.wildapricot.listContactFields({ + $, + accountId: this.accountId, + }); + const fieldValues = []; + for (const field of fields) { + if (this[field.Id]) { + fieldValues.push({ + FieldName: field.FieldName, + Value: this[field.Id], + }); + } + } + const args = { + $, + accountId: this.accountId, + data: { + FieldValues: fieldValues, + }, + }; + if (this.contactId) { + args.data.Id = this.contactId; + } + const response = this.contactId + ? await this.wildapricot.updateContact({ + contactId: this.contactId, + ...args, + }) + : await this.wildapricot.createContact(args); + $.export("$summary", `Successfully ${this.contactId + ? "updated" + : "created"} contact/member with ID ${response.Id}`); + return response; + }, +}; diff --git a/components/wildapricot/actions/add-update-event-registration/add-update-event-registration.mjs b/components/wildapricot/actions/add-update-event-registration/add-update-event-registration.mjs new file mode 100644 index 0000000000000..9d0f3dc765b4b --- /dev/null +++ b/components/wildapricot/actions/add-update-event-registration/add-update-event-registration.mjs @@ -0,0 +1,114 @@ +import wildapricot from "../../wildapricot.app.mjs"; + +export default { + key: "wildapricot-add-update-event-registration", + name: "Add or Update Event Registration", + description: "Searches event registrations using a contact email. If a match is found, the registration details are updated. If not, a new registration is added to the event. [See the documentation](https://app.swaggerhub.com/apis-docs/WildApricot/wild-apricot_public_api/7.24.0#/Events.EventRegistrations/CreateEventRegistration)", + version: "0.0.1", + type: "action", + props: { + wildapricot, + accountId: { + propDefinition: [ + wildapricot, + "accountId", + ], + }, + contactEmail: { + type: "string", + label: "Contact Email", + description: "The email of the contact to search for event registrations", + }, + eventId: { + propDefinition: [ + wildapricot, + "eventId", + (c) => ({ + accountId: c.accountId, + }), + ], + }, + eventRegistrationTypeId: { + propDefinition: [ + wildapricot, + "eventRegistrationTypeId", + (c) => ({ + accountId: c.accountId, + eventId: c.eventId, + }), + ], + }, + isCheckedIn: { + type: "boolean", + label: "Is Checked In", + description: "Indicates if registrant is already checked in to the event", + optional: true, + }, + memo: { + type: "string", + label: "Memo", + description: "Additional notes about this registration", + optional: true, + }, + }, + methods: { + async getContactId($, email) { + const contacts = await this.wildapricot.listContacts({ + $, + accountId: this.accountId, + params: { + "SimpleQuery": email, + }, + }); + if (!contacts?.length || !contacts[0].Id) { + throw new Error(`Contact with email ${email} not found`); + } + return contacts[0].Id; + }, + }, + async run({ $ }) { + const contactId = await this.getContactId($, this.contactEmail); + const eventRegistrations = await this.wildapricot.listEventRegistrations({ + $, + accountId: this.accountId, + params: { + contactId, + eventId: this.eventId, + }, + }); + const data = { + Event: { + Id: this.eventId, + }, + Contact: { + Id: contactId, + }, + RegistrationTypeId: this.eventRegistrationTypeId, + IsCheckedIn: this.isCheckedIn, + Memo: this.memo, + }; + let response; + if (!eventRegistrations?.length) { + response = await this.wildapricot.createEventRegistration({ + $, + accountId: this.accountId, + data, + }); + } else { + const eventRegistrationId = eventRegistrations[0].Id; + response = await this.wildapricot.updateEventRegistration({ + $, + accountId: this.accountId, + eventRegistrationId: eventRegistrationId, + data: { + ...data, + Id: eventRegistrationId, + }, + }); + } + $.export("$summary", `Successfully ${eventRegistrations?.length + ? "updated" + : "created"} event registration with ID ${response.Id}`); + return response; + }, +}; diff --git a/components/wildapricot/actions/delete-event-registration/delete-event-registration.mjs b/components/wildapricot/actions/delete-event-registration/delete-event-registration.mjs new file mode 100644 index 0000000000000..d25d4bbf6a9ce --- /dev/null +++ b/components/wildapricot/actions/delete-event-registration/delete-event-registration.mjs @@ -0,0 +1,58 @@ +import wildapricot from "../../wildapricot.app.mjs"; + +export default { + key: "wildapricot-delete-event-registration", + name: "Delete Event Registration", + description: "Removes an event registration from the user's WildApricot database. [See the documentation](https://app.swaggerhub.com/apis-docs/WildApricot/wild-apricot_public_api/7.24.0#/Events.EventRegistrations/DeleteEventRegistration)", + version: "0.0.1", + type: "action", + props: { + wildapricot, + accountId: { + propDefinition: [ + wildapricot, + "accountId", + ], + }, + contactId: { + propDefinition: [ + wildapricot, + "contactId", + (c) => ({ + accountId: c.accountId, + }), + ], + optional: true, + }, + eventId: { + propDefinition: [ + wildapricot, + "eventId", + (c) => ({ + accountId: c.accountId, + }), + ], + optional: true, + }, + eventRegistrationId: { + propDefinition: [ + wildapricot, + "eventRegistrationId", + (c) => ({ + accountId: c.accountId, + contactId: c.contactId, + eventId: c.eventId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.wildapricot.deleteEventRegistration({ + $, + accountId: this.accountId, + eventRegistrationId: this.eventRegistrationId, + }); + $.export("$summary", `Successfully deleted event registration with ID: ${this.eventRegistrationId}`); + return response; + }, +}; diff --git a/components/wildapricot/common/constants.mjs b/components/wildapricot/common/constants.mjs new file mode 100644 index 0000000000000..7f4081138acc1 --- /dev/null +++ b/components/wildapricot/common/constants.mjs @@ -0,0 +1,5 @@ +const DEFAULT_LIMIT = 20; + +export default { + DEFAULT_LIMIT, +}; diff --git a/components/wildapricot/package.json b/components/wildapricot/package.json new file mode 100644 index 0000000000000..90c003b593c2a --- /dev/null +++ b/components/wildapricot/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/wildapricot", + "version": "0.1.0", + "description": "Pipedream WildApricot Components", + "main": "wildapricot.app.mjs", + "keywords": [ + "pipedream", + "wildapricot" + ], + "homepage": "https://pipedream.com/apps/wildapricot", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.2" + } +} diff --git a/components/wildapricot/sources/common/base.mjs b/components/wildapricot/sources/common/base.mjs new file mode 100644 index 0000000000000..82f9a8fb73289 --- /dev/null +++ b/components/wildapricot/sources/common/base.mjs @@ -0,0 +1,29 @@ +import wildapricot from "../../wildapricot.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + wildapricot, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + accountId: { + propDefinition: [ + wildapricot, + "accountId", + ], + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + }, +}; diff --git a/components/wildapricot/sources/new-contact-or-member-updated/new-contact-or-member-updated.mjs b/components/wildapricot/sources/new-contact-or-member-updated/new-contact-or-member-updated.mjs new file mode 100644 index 0000000000000..e88dc8b05d3e1 --- /dev/null +++ b/components/wildapricot/sources/new-contact-or-member-updated/new-contact-or-member-updated.mjs @@ -0,0 +1,52 @@ +import common from "../common/base.mjs"; +import constants from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "wildapricot-new-contact-or-member-updated", + name: "New Contact or Member Updated", + description: "Emit new event when a contact or member in WildApricot is updated", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + generateMeta(contact) { + const ts = Date.parse(contact.ProfileLastUpdated); + return { + id: `${contact.Id}-${ts}`, + summary: `New or Updated Contact ${contact.Id}`, + ts, + }; + }, + }, + async run() { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + const limit = constants.DEFAULT_LIMIT; + const params = { + "$top": limit, + "$skip": 0, + }; + let total; + do { + const contacts = await this.wildapricot.listContacts({ + accountId: this.accountId, + params, + }); + for (const contact of contacts) { + const ts = Date.parse(contact.ProfileLastUpdated); + if (ts >= lastTs) { + maxTs = Math.max(ts, maxTs); + const meta = this.generateMeta(contact); + this.$emit(contact, meta); + } + } + params["$skip"] += limit; + total = contacts?.length; + } while (total === limit); + this._setLastTs(maxTs); + }, + sampleEmit, +}; diff --git a/components/wildapricot/sources/new-contact-or-member-updated/test-event.mjs b/components/wildapricot/sources/new-contact-or-member-updated/test-event.mjs new file mode 100644 index 0000000000000..0595f62fec08b --- /dev/null +++ b/components/wildapricot/sources/new-contact-or-member-updated/test-event.mjs @@ -0,0 +1,149 @@ +export default { + "FirstName": "", + "LastName": "", + "Email": "org@email.com", + "DisplayName": "Org", + "Organization": "Org", + "ProfileLastUpdated": "2024-04-03T11:57:11.133-04:00", + "FieldValues": [ + { + "FieldName": "Archived", + "Value": false, + "SystemCode": "IsArchived" + }, + { + "FieldName": "Donor", + "Value": false, + "SystemCode": "IsDonor" + }, + { + "FieldName": "Event registrant", + "Value": true, + "SystemCode": "IsEventAttendee" + }, + { + "FieldName": "Member", + "Value": false, + "SystemCode": "IsMember" + }, + { + "FieldName": "Suspended member", + "Value": false, + "SystemCode": "IsSuspendedMember" + }, + { + "FieldName": "Event announcements", + "Value": true, + "SystemCode": "ReceiveEventReminders" + }, + { + "FieldName": "Member emails and newsletters", + "Value": true, + "SystemCode": "ReceiveNewsletters" + }, + { + "FieldName": "Email delivery disabled", + "Value": false, + "SystemCode": "EmailDisabled" + }, + { + "FieldName": "Email delivery disabled automatically", + "Value": false, + "SystemCode": "EmailingDisabledAutomatically" + }, + { + "FieldName": "Receiving emails disabled", + "Value": false, + "SystemCode": "RecievingEMailsDisabled" + }, + { + "FieldName": "Balance", + "Value": 0, + "SystemCode": "Balance" + }, + { + "FieldName": "Total donated", + "Value": 0, + "SystemCode": "TotalDonated" + }, + { + "FieldName": "Registered for specific event", + "Value": null, + "SystemCode": "RegistredForEvent" + }, + { + "FieldName": "Profile last updated", + "Value": "2024-04-03T15:57:11.133+00:00", + "SystemCode": "LastUpdated" + }, + { + "FieldName": "Profile last updated by", + "Value": 74400049, + "SystemCode": "LastUpdatedBy" + }, + { + "FieldName": "Creation date", + "Value": "2024-04-03T15:57:10+00:00", + "SystemCode": "CreationDate" + }, + { + "FieldName": "Last login date", + "Value": null, + "SystemCode": "LastLoginDate" + }, + { + "FieldName": "Administrator role", + "Value": [], + "SystemCode": "AdminRole" + }, + { + "FieldName": "Notes", + "Value": "", + "SystemCode": "Notes" + }, + { + "FieldName": "Terms of use accepted", + "Value": false, + "SystemCode": "SystemRulesAndTermsAccepted" + }, + { + "FieldName": "User ID", + "Value": 74443226, + "SystemCode": "MemberId" + }, + { + "FieldName": "First name", + "Value": "", + "SystemCode": "FirstName" + }, + { + "FieldName": "Last name", + "Value": "", + "SystemCode": "LastName" + }, + { + "FieldName": "Organization", + "Value": "Org", + "SystemCode": "Organization" + }, + { + "FieldName": "Email", + "Value": "org@email.com", + "SystemCode": "Email" + }, + { + "FieldName": "Phone", + "Value": "", + "SystemCode": "Phone" + }, + { + "FieldName": "Privacy Policy", + "Value": false, + "SystemCode": "custom-16165339" + } + ], + "Id": 74443226, + "Url": "https://api.wildapricot.org/v2/accounts/467183/Contacts/74443226", + "IsAccountAdministrator": false, + "TermsOfUseAccepted": false +} \ No newline at end of file diff --git a/components/wildapricot/sources/new-event-registration-created/new-event-registration-created.mjs b/components/wildapricot/sources/new-event-registration-created/new-event-registration-created.mjs new file mode 100644 index 0000000000000..c658761443d1a --- /dev/null +++ b/components/wildapricot/sources/new-event-registration-created/new-event-registration-created.mjs @@ -0,0 +1,74 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + ...common, + key: "wildapricot-new-event-registration-created", + name: "New Event Registration Created", + description: "Emit new event when a registration to an existing event in WildApricot is newly created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + contactId: { + propDefinition: [ + common.props.wildapricot, + "contactId", + (c) => ({ + accountId: c.accountId, + }), + ], + optional: true, + }, + eventId: { + propDefinition: [ + common.props.wildapricot, + "eventId", + (c) => ({ + accountId: c.accountId, + }), + ], + optional: true, + }, + }, + hooks: { + async deploy() { + if (!this.eventId && !this.contactId) { + throw new ConfigurationError("Must provide one of Contact ID or Event ID"); + } + }, + }, + methods: { + ...common.methods, + generateMeta(registration) { + return { + id: registration.Id, + summary: `New Registration ${registration.Id}`, + ts: Date.parse(registration.RegistrationDate), + }; + }, + }, + async run() { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + const eventRegistrations = await this.wildapricot.listEventRegistrations({ + accountId: this.accountId, + params: { + contactId: this.contactId, + eventId: this.eventId, + }, + }); + for (const registration of eventRegistrations) { + const ts = Date.parse(registration.RegistrationDate); + if (ts >= lastTs) { + maxTs = Math.max(ts, maxTs); + const meta = this.generateMeta(registration); + this.$emit(registration, meta); + } + } + this._setLastTs(maxTs); + }, + sampleEmit, +}; diff --git a/components/wildapricot/sources/new-event-registration-created/test-event.mjs b/components/wildapricot/sources/new-event-registration-created/test-event.mjs new file mode 100644 index 0000000000000..22a61869b6a32 --- /dev/null +++ b/components/wildapricot/sources/new-event-registration-created/test-event.mjs @@ -0,0 +1,65 @@ +export default { + "Id": 52091764, + "Url": "https://api.wildapricot.org/v2.2/accounts/467183/EventRegistrations/52091764", + "Event": { + "Id": 5683098, + "Url": "https://api.wildapricot.org/v2.2/accounts/467183/Events/5683098", + "Location": "", + "StartDate": "2024-04-04T00:00:00-04:00", + "EndDate": "2024-04-04T00:00:00-04:00", + "Name": "Event1" + }, + "Contact": { + "Id": 74443226, + "Url": "https://api.wildapricot.org/v2.2/accounts/467183/Contacts/74443226", + "Name": "Org" + }, + "DisplayName": "Org", + "Organization": "Org", + "IsCheckedIn": false, + "RegistrationFee": 0, + "PaidSum": 0, + "RegistrationTypeId": 9041891, + "IsPaid": true, + "RegistrationFields": [ + { + "FieldName": "First name", + "Value": "", + "SystemCode": "FirstName" + }, + { + "FieldName": "Last name", + "Value": "", + "SystemCode": "LastName" + }, + { + "FieldName": "Organization", + "Value": "Org", + "SystemCode": "Organization" + }, + { + "FieldName": "Email", + "Value": "org@email.com", + "SystemCode": "Email" + }, + { + "FieldName": "Phone", + "Value": "", + "SystemCode": "Phone" + }, + { + "FieldName": "Privacy Policy", + "Value": false, + "SystemCode": "custom-16165339" + } + ], + "ShowToPublic": false, + "RegistrationDate": "2024-04-03T16:24:59-04:00", + "RegistrationType": { + "Id": 9041891, + "Name": "ticket1" + }, + "IsGuestRegistration": false, + "OnWaitlist": false, + "Status": "Free" +} \ No newline at end of file diff --git a/components/wildapricot/sources/new-payments-received/new-payments-received.mjs b/components/wildapricot/sources/new-payments-received/new-payments-received.mjs new file mode 100644 index 0000000000000..7f10bd6fdc4b1 --- /dev/null +++ b/components/wildapricot/sources/new-payments-received/new-payments-received.mjs @@ -0,0 +1,51 @@ +import common from "../common/base.mjs"; +import constants from "../../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "wildapricot-new-payments-received", + name: "New Payments Received", + description: "Emit new event each time a new payment is received in WildApricot", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + generateMeta(payment) { + return { + id: payment.Id, + summary: `New Payment Recieved ${payment.Id}`, + ts: Date.parse(payment.CreatedDate), + }; + }, + }, + async run() { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + const limit = constants.DEFAULT_LIMIT; + const params = { + "$top": limit, + "$skip": 0, + }; + let total; + do { + const { Payments: payments } = await this.wildapricot.listPayments({ + accountId: this.accountId, + params, + }); + for (const payment of payments) { + const ts = Date.parse(payment.CreatedDate); + if (ts >= lastTs) { + maxTs = Math.max(ts, maxTs); + const meta = this.generateMeta(payment); + this.$emit(payment, meta); + } + } + params["$skip"] += limit; + total = payments?.length; + } while (total === limit); + this._setLastTs(maxTs); + }, + sampleEmit, +}; diff --git a/components/wildapricot/sources/new-payments-received/test-event.mjs b/components/wildapricot/sources/new-payments-received/test-event.mjs new file mode 100644 index 0000000000000..4900de400ebf9 --- /dev/null +++ b/components/wildapricot/sources/new-payments-received/test-event.mjs @@ -0,0 +1,25 @@ +export default { + "Id": 93346951, + "Url": "https://api.wildapricot.org/v2.2/accounts/467183/Payments/93346951", + "Tender": null, + "Comment": "", + "PublicComment": "", + "AllocatedValue": 0, + "RefundedAmount": 0, + "Type": "InvoicePayment", + "Donation": null, + "Value": 1, + "DocumentDate": "2024-04-03T17:08:35+00:00", + "Contact": { + "Id": 74400049, + "Url": "https://api.wildapricot.org/v2.2/accounts/467183/Contacts/74400049", + "Name": "Test User" + }, + "CreatedDate": "2024-04-03T17:08:35", + "CreatedBy": { + "Id": 74400049, + "Url": "https://api.wildapricot.org/v2.2/accounts/467183/Contacts/74400049" + }, + "UpdatedDate": null, + "UpdatedBy": null +} \ No newline at end of file diff --git a/components/wildapricot/wildapricot.app.mjs b/components/wildapricot/wildapricot.app.mjs new file mode 100644 index 0000000000000..09eeb37313747 --- /dev/null +++ b/components/wildapricot/wildapricot.app.mjs @@ -0,0 +1,256 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "wildapricot", + propDefinitions: { + accountId: { + type: "string", + label: "Account ID", + description: "The unique identifier for the account", + async options() { + const accounts = await this.listAccounts(); + return accounts?.map(({ + Id: value, Name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + contactId: { + type: "string", + label: "Contact ID", + description: "The unique identifier for the contact", + async options({ + accountId, page, + }) { + const limit = constants.DEFAULT_LIMIT; + const contacts = await this.listContacts({ + accountId, + params: { + "$top": limit, + "$skip": page * limit, + }, + }); + return contacts?.map(({ + Id: value, DisplayName: label, + }) => ({ + value, + label, + })) || []; + }, + }, + eventId: { + type: "string", + label: "Event ID", + description: "The unique identifier for the event", + async options({ + accountId, page, + }) { + const limit = constants.DEFAULT_LIMIT; + const { Events: events } = await this.listEvents({ + accountId, + params: { + "$top": limit, + "$skip": page * limit, + }, + }); + return events?.map(({ + Id: value, Name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + eventRegistrationId: { + type: "string", + label: "Event Registration ID", + description: "The unique identifier for the event registration", + async options({ + accountId, eventId, contactId, + }) { + if (!eventId && !contactId) { + throw new Error("Must provide one of Contact ID or Event ID"); + } + const eventRegistrations = await this.listEventRegistrations({ + accountId, + params: { + contactId, + eventId, + }, + }); + return eventRegistrations?.map(({ + Id: value, DisplayName: label, + }) => ({ + value, + label, + })) || []; + }, + }, + eventRegistrationTypeId: { + type: "string", + label: "Event Registration Type ID", + description: "The unique identifier for the event registration type", + async options({ + accountId, eventId, + }) { + const types = await this.listEventRegistrationTypes({ + accountId, + params: { + eventId, + }, + }); + return types?.map(({ + Id: value, Name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.wildapricot.org/v2.2"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + listAccounts(opts = {}) { + return this._makeRequest({ + path: "/accounts", + ...opts, + }); + }, + async listContacts({ + accountId, params, ...opts + }) { + const path = `/accounts/${accountId}/contacts`; + const { ResultId: resultId } = await this._makeRequest({ + path, + params: { + ...params, + "$async": true, + }, + ...opts, + }); + // poll until contacts are retrieved + const timer = (ms) => new Promise((res) => setTimeout(res, ms)); + let complete, results; + do { + let { + Contacts: contacts, State: state, + } = await this._makeRequest({ + path, + accountId, + params: { + resultId, + }, + }); + results = contacts; + complete = state === "Complete"; + await timer(1000); + } while (!complete); + return results; + }, + listContactFields({ + accountId, ...opts + }) { + return this._makeRequest({ + path: `/accounts/${accountId}/contactfields`, + ...opts, + }); + }, + listEvents({ + accountId, ...opts + }) { + return this._makeRequest({ + path: `/accounts/${accountId}/events`, + ...opts, + }); + }, + listEventRegistrations({ + accountId, ...opts + }) { + return this._makeRequest({ + path: `/accounts/${accountId}/eventregistrations`, + ...opts, + }); + }, + listEventRegistrationTypes({ + accountId, ...opts + }) { + return this._makeRequest({ + path: `/accounts/${accountId}/EventRegistrationTypes`, + ...opts, + }); + }, + listPayments({ + accountId, ...opts + }) { + return this._makeRequest({ + path: `/accounts/${accountId}/payments`, + ...opts, + }); + }, + createContact({ + accountId, ...opts + }) { + return this._makeRequest({ + path: `/accounts/${accountId}/contacts`, + method: "POST", + ...opts, + }); + }, + updateContact({ + accountId, contactId, ...opts + }) { + return this._makeRequest({ + path: `/accounts/${accountId}/contacts/${contactId}`, + method: "PUT", + ...opts, + }); + }, + createEventRegistration({ + accountId, ...opts + }) { + return this._makeRequest({ + path: `/accounts/${accountId}/eventregistrations`, + method: "POST", + ...opts, + }); + }, + updateEventRegistration({ + accountId, eventRegistrationId, ...opts + }) { + return this._makeRequest({ + path: `/accounts/${accountId}/eventregistrations/${eventRegistrationId}`, + method: "PUT", + ...opts, + }); + }, + deleteEventRegistration({ + accountId, eventRegistrationId, ...opts + }) { + return this._makeRequest({ + path: `/accounts/${accountId}/eventregistrations/${eventRegistrationId}`, + method: "DELETE", + ...opts, + }); + }, + }, +}; diff --git a/components/wildberries/README.md b/components/wildberries/README.md index 22d2881d230de..79bd5e3506b3c 100644 --- a/components/wildberries/README.md +++ b/components/wildberries/README.md @@ -1,21 +1,11 @@ # Overview -Using the Wildberries API, developers can create amazing applications for -interacting with Wildberries products. This powerful API provides a powerful -set of tools for searching, buying, and managing products and provides a wide -range of samples and related data to make it easier to build applications. Here -are a few examples of what developers can build using the Wildberries API: +The Wildberries API offers a gateway to interact with one of Russia's largest online retailers. With this API, you can automate various aspects of the e-commerce process, including catalog management, order processing, and inventory tracking. Harnessing the Wildberries API within Pipedream's ecosystem allows you to create sophisticated, serverless workflows that respond in real-time to events like new orders, updated listings, or stock changes. These integrations can streamline operations, improve customer service, and enhance data analysis for better decision-making. -- Mobile applications that allow users to browse products, purchase items, and - track orders -- Dynamic pricing applications that quickly adjust prices according to - real-time changes in demand -- Search engines that allow users to quickly find the exact product they are - looking for -- Tools that assist admin in managing products and analyzing customer behavior -- Shopping bots that help automated product searches and purchases -- Recommendation engines that suggest products to customers based on their past - purchases -- Data-driven applications for monitoring product pricing and stock levels -- Marketplace apps that allow vendors to advertise products and buyers to - purchase them +# Example Use Cases + +- **Real-time Order Processing**: Automate the order fulfillment process by integrating the Wildberries API with a fulfillment service or in-house inventory management system. When a new order is placed on Wildberries, trigger a Pipedream workflow to update inventory levels and send order details to the fulfillment center, reducing manual intervention and speeding up delivery times. + +- **Product Listings Sync**: Keep product information consistent across platforms by syncing updates from Wildberries with other e-commerce platforms like Shopify or WooCommerce. When product details or prices change on Wildberries, use a Pipedream workflow to propagate these changes to other marketplaces, ensuring uniformity and preventing customer confusion. + +- **Customer Service Automation**: Improve response times and personalize customer interaction by linking Wildberries with customer support tools such as Zendesk or Intercom. Set up a Pipedream workflow to create a support ticket or send a personalized message whenever there is an order update or return request on Wildberries, enhancing the overall customer experience. diff --git a/components/winston_ai/README.md b/components/winston_ai/README.md new file mode 100644 index 0000000000000..87d19e60280c5 --- /dev/null +++ b/components/winston_ai/README.md @@ -0,0 +1,11 @@ +# Overview + +The Winston AI API offers robust content detection, enabling developers to analyze text for various purposes such as sentiment analysis, spam detection, and more. Within Pipedream, you can harness this API to build serverless workflows that react to content, integrate with numerous other apps, and automate responses or actions based on the API's insights. You might craft workflows that filter user-generated content, trigger alerts on negative sentiments, or enhance data collection with AI-driven context. + +# Example Use Cases + +- **Content Moderation for Community Platforms**: Use Winston AI to scan messages posted on platforms like Slack, Discord, or forums. If it detects harmful or unwanted content, automatically remove the message and notify moderators. + +- **Customer Feedback Analysis**: Connect Winston AI with a customer service app like Zendesk. Analyze incoming feedback for sentiment and categorize tickets based on urgency or potential churn risk, improving response times and customer satisfaction. + +- **Social Media Monitoring**: Monitor brand mentions on social media platforms like Twitter using Winston AI. Evaluate sentiment and highlight positive testimonials or quickly respond to and address negative feedback. diff --git a/components/wire2air/README.md b/components/wire2air/README.md new file mode 100644 index 0000000000000..669eaf9c4ee27 --- /dev/null +++ b/components/wire2air/README.md @@ -0,0 +1,11 @@ +# Overview + +The Wire2Air API provides the ability to integrate SMS capabilities into your applications, allowing for sending and receiving text messages, managing contacts, and automating communication processes. On Pipedream, you can leverage these features to create powerful serverless workflows, connecting the Wire2Air API with a multitude of other services for various use cases, such as notifications, marketing, and customer support. + +# Example Use Cases + +- **SMS Notifications for E-commerce Orders**: Trigger a workflow on Pipedream when a new order is placed on an e-commerce platform like Shopify. Use the Wire2Air API to send a confirmation SMS to the customer with order details. + +- **Automated Customer Support Messages**: Combine Wire2Air with a CRM app like HubSpot. When a new ticket is created, automatically send an SMS to the customer acknowledging the receipt of their query and providing an estimated response time. + +- **Event Reminder Service**: Use Google Calendar events to trigger SMS reminders. Before an event starts, a Pipedream workflow can send a text message reminder to participants with the Wire2Air API. diff --git a/components/wise/README.md b/components/wise/README.md index 4f4f31ddc8ff6..3a60c9e6c34f1 100644 --- a/components/wise/README.md +++ b/components/wise/README.md @@ -1,24 +1,11 @@ # Overview -Using Wise's API, you can build a variety of applications that leverage the -company's international payments infrastructure. The API provides a unified, -hardware-agnostic interface to bank networks around the world and allows -merchants, marketplaces, and financial services to quickly and easily accept -payments with multi-currency support. +The Wise API enables seamless integration of international bank transfers into applications, offering real-time currency exchange rates, multi-currency account management, and compliance checks. With Pipedream, you can leverage Wise to automate financial workflows, sync transaction data with accounting software, and trigger actions based on payment events, all while maintaining security and reducing the manual overhead often involved in international finances. -Wise also provides powerful tools and features to monitor, analyze, and -optimize payments. With the API, developers can easily access currency -conversion, health checks, transaction reports, and more. With the help of the -API, developers can easily integrate into the platform to create a custom -payments experience. +# Example Use Cases -The following are some examples of applications that can be built with Wise's -API: +- **Automated Invoice Payments**: Set up a workflow where invoices from your accounting platform (like QuickBooks) trigger international payments through Wise. When an invoice is marked as due, Pipedream can automatically initiate a payment in the required currency, streamlining your accounts payable process. -- Payment platforms -- E-commerce applications -- International money transfers -- Merchant payment processing -- Currency exchange applications -- Multi-currency wallets -- Payment analysis and optimization tools +- **Real-time Currency Exchange Alerts**: Create a Pipedream workflow that monitors currency exchange rates between your preferred currency pairs on Wise. When the exchange rate hits a specified threshold, the workflow could notify you via email or a messaging app like Slack, enabling timely financial decisions. + +- **Synchronized Financial Reporting**: Integrate Wise with a data visualization tool like Google Data Studio. Each time a new transaction occurs, Pipedream can extract the transaction details and feed them into a live report, giving you up-to-date insight into your financial status across different currencies and countries. diff --git a/components/wisepops/README.md b/components/wisepops/README.md index a0e289758f9f0..83484e99b7e7e 100644 --- a/components/wisepops/README.md +++ b/components/wisepops/README.md @@ -1,19 +1,11 @@ # Overview -WisePops' API allows webmasters and developers to easily create, deploy, and -manage powerful dynamic popups and surveys. +The WisePops API unlocks potent possibilities for automating pop-up and banners management on your website. By integrating WisePops with Pipedream, you can streamline workflows such as syncing lead data to CRM platforms, personalizing visitor engagement based on real-time analytics, and dynamically updating marketing campaigns. The API's ability to fetch, update, create, and delete pop-ups enables developers to orchestrate sophisticated marketing automation that reacts to customer behavior and data-driven insights. -Using the WisePops API, you can build a variety of rich interactive experiences -that increase user engagement and generate leads. Here are just some examples -of the types of popups and surveys you can build: +# Example Use Cases -- Email Capture Popups -- Abandoned Cart Popups -- Exit Intent Popups -- Quiz Popups -- Inline Forms -- Polls -- Onboarding Surveys -- Rating & Feedback Forms -- Outcome-Based Survey -- User Satisfaction Surveys +- **Dynamic Pop-Up Content Based on User Behavior**: Use customer interaction data from your website to dynamically update WisePops pop-up content. By connecting the WisePops API to website analytics on Pipedream, you can tailor pop-up messages based on user behavior, such as displaying a discount code to a user who has visited multiple product pages. + +- **Synchronized Leads Management**: After capturing leads through WisePops pop-ups, automatically feed this data into your CRM like Salesforce or HubSpot using Pipedream. This workflow can enrich lead profiles, trigger follow-up emails, and score leads based on the interaction with the pop-up, ensuring seamless lead nurturing. + +- **Real-Time Campaign Performance Adjustment**: Link WisePops with a real-time analytics tool, such as Google Analytics, via Pipedream to enable immediate responses to campaign performance data. If a pop-up underperforms, trigger an A/B test with an alternate version or adjust its targeting criteria, all automated through Pipedream workflows. diff --git a/components/wishpond/README.md b/components/wishpond/README.md index e309f03b7db40..9f4626ae4ea7e 100644 --- a/components/wishpond/README.md +++ b/components/wishpond/README.md @@ -1,15 +1,11 @@ # Overview -The Wishpond API makes it easy to create powerful marketing campaigns such as -lead captures, email campaigns, SMS campaigns, and more. With the Wishpond API, -you can easily automate your marketing campaigns, allowing you to focus on -other tasks. Here are a few of the things you can do: - -- Create customized landing pages, lead forms, and email campaigns -- Generate leads and track their progress throughout the sales funnel -- Automate follow-up emails, SMS messages, and other communications -- Automate A/B testing to choose the best versions of your campaigns and funnel - steps -- Segment and target marketing campaigns for the highest engagement -- Monitor social media campaigns for performance and engagement -- Track customer activity and use insights to craft personalized email content +Wishpond's API unlocks a world where digital marketing efforts can be automated and customized at scale. With this powerful tool at your disposal on Pipedream, you can create, manage, and track marketing campaigns effortlessly. Leverage Wishpond to capture leads, nurture them through personalized emails, and analyze the performance of your marketing strategies. By integrating with Pipedream, you capitalize on the ability to connect Wishpond with hundreds of apps, triggering actions based on campaign events, syncing leads to CRMs, and automating follow-up tasks without writing a single line of code. + +# Example Use Cases + +- **Lead Score Updating**: Capture leads from your Wishpond campaigns and automatically update their score based on interactions, such as email opens or page visits, using Pipedream's built-in code steps. Sync updated leads to a CRM like Salesforce for real-time sales team alerts. + +- **Dynamic Email Sequences**: Combine Wishpond with SendGrid on Pipedream to create dynamic email sequences. Trigger the sequence when a user signs up for a webinar, sending them personalized content based on their interests or behavior, and follow up post-webinar with tailored resources. + +- **Real-time Slack Notifications**: Instantly notify your sales or support team in Slack when a high-value lead completes a form or interacts with a key piece of content. Use Pipedream's Slack integration to filter and send custom alerts, enabling immediate engagement and potentially higher conversion rates. diff --git a/components/wistia/README.md b/components/wistia/README.md new file mode 100644 index 0000000000000..dd34bf58ccb94 --- /dev/null +++ b/components/wistia/README.md @@ -0,0 +1,11 @@ +# Overview + +The Wistia API lets you dive deep into video analytics, manage your media library, and automate video workflows. It's great for developers aiming to extend the functionality of their Wistia-hosted content. When used within Pipedream, you can integrate Wistia with countless other apps and services to create robust automations. You could, for instance, update a CRM with video view data, send custom email alerts based on viewer behavior, or even synchronize video uploads with a content management system. + +# Example Use Cases + +- **Automate Video Publication Announcements**: When a new video is uploaded to Wistia, you can automatically post a message to Slack or send an email via SendGrid to notify your team or subscribers, keeping everyone informed about new content without manual intervention. + +- **Sync Video Engagement Data with a CRM**: Capture engagement data from your viewers and feed it into Salesforce or HubSpot. This could enable sales teams to better understand customer interests based on which videos they're watching and how they interact with the content, helping to tailor follow-up communications more effectively. + +- **Trigger Workflows from Video Events**: Use video events like a play or pause action to trigger specific workflows. For example, if a viewer pauses a video at a certain point, it could trigger an email through Mailgun with more information on the topic discussed at that time stamp, offering a more interactive and responsive viewing experience. diff --git a/components/wit_ai/README.md b/components/wit_ai/README.md index ec215a11f5384..669a233147635 100644 --- a/components/wit_ai/README.md +++ b/components/wit_ai/README.md @@ -1,20 +1,11 @@ # Overview -Build amazing voice and text-based applications with the wit.ai API. With -wit.ai, you can easily create an application that can understand natural -language and help you in your daily tasks. +Wit.ai API enables your apps to process natural language and understand human commands contextually. With it, you can build conversational interfaces, decipher intent from text or voice inputs, and train your model to improve recognition. Pipedream provides a seamless platform to trigger workflows using this API, allowing the automation of tasks based on language cues. It's perfect for crafting custom bots, enhancing customer service, or streamlining internal operations by interpreting human requests and taking action accordingly. -Using the wit.ai API, you can create a wide range of different applications, -some of which include: +# Example Use Cases -- Virtual Assistants (Voice and Text): Create virtual assistants that - understand natural language and can be used to answer questions and provide - various kinds of help. -- Chatbots: Create interactive chatbots that can understand natural language - and provide conversations with users. -- Home Automation: Use the wit.ai to create a voice-controlled home automation - system that can control various devices like lights and sensors. -- Healthcare Solutions: Create healthcare applications that can understand - natural language, make assessments, and offer recommendations. -- AI-Powered Games: Create games that make use of wit.ai’s capabilities to - understand natural language and create intelligent interactions with players. +- **Customer Support Automation**: Use Wit.ai’s language understanding to interpret customer inquiries received via email or chat. Combine it with a CRM like Salesforce on Pipedream to automatically categorize and assign tickets, or even generate and send basic responses to common questions. + +- **Voice-Controlled Tasks**: Build a system where voice commands captured from IoT devices are sent to Wit.ai. Analyze the intent and execute related smart home actions, like adjusting thermostats or controlling lights, by connecting to home automation services like Philips Hue or Nest on Pipedream. + +- **Content Categorization**: Implement an automated content analysis system that uses Wit.ai to understand and categorize user-generated content or customer feedback. Link it with a data visualization tool like Google Sheets on Pipedream to track trends, sentiment, and topics for marketing insights or product improvements. diff --git a/components/wit_ai/package.json b/components/wit_ai/package.json new file mode 100644 index 0000000000000..a38857247dcbe --- /dev/null +++ b/components/wit_ai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/wit_ai", + "version": "0.6.0", + "description": "Pipedream wit_ai Components", + "main": "wit_ai.app.mjs", + "keywords": [ + "pipedream", + "wit_ai" + ], + "homepage": "https://pipedream.com/apps/wit_ai", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/withings/README.md b/components/withings/README.md index b4d92c8a0968b..c43e4f22f1ec0 100644 --- a/components/withings/README.md +++ b/components/withings/README.md @@ -1,29 +1,11 @@ # Overview -The Withings API allows developers to access and leverage Withings health data -to create innovative and valuable applications. With the Withings API, users -are able to build applications that give them access to data from Withings’ -connected health devices such as activity trackers, digital scales, and blood -pressure and thermometer devices. +Withings API lets you tap into health and wellness data from Withings devices like smart scales, watches, and sleep trackers. The API enables you to fetch measurements such as weight, activity, sleep, and heart rate data. This data can inform personal health apps, trigger notifications for health milestones, or integrate with health coaching platforms. By leveraging Pipedream's power, you can automate workflows that react to this data, enriching user profiles, informing coaching decisions, or driving wellness-focused communities and challenges. -With the Withings API, it is possible to build applications that help users -connect to their data and use it effectively and efficiently. Here are a few -examples of what can be achieved with the Withings API: +# Example Use Cases -- Activity tracker applications to help users visualize and understand their - health data, such as fitness and sleep tracking apps. -- Digital scales applications that can collect a user’s body weight data over - time and show trends. -- Blood pressure monitor applications to measure the user’s blood pressure and - store heart rate data. -- Thermometer applications to show trends in body temperature. -- Analytical and dashboard applications that allow users to generate reports, - set goals, and access data from multiple sources. -- Visualization and notification apps that allow users to set up customized - notifications and alarms. -- Automation apps that allow users to set up automated actions based on their - health data. +- **Health Dashboard Integration**: Sync Withings health metrics to a custom dashboard on a regular basis. Use the API to pull the latest user data and display it through widgets or graphs. This can help users track progress over time and provide insights into their health trends. -By leveraging the Withings API, developers are able to unlock valuable, -actionable data to create innovative and useful applications that can improve -users’ lives. +- **Fitness Challenge Automation**: Create a fitness challenge app where participants' progress is updated based on Withings activity data. Use Pipedream to watch for new Withings activity data and automatically update a leaderboard hosted on Google Sheets or a web app. + +- **Wellness Notification Service**: Set up a service that sends out personalized messages or emails when a user reaches certain health milestones. With Pipedream, you can listen for specific changes in Withings data, like weight loss or increased daily step count, and trigger congratulatory notifications via Twilio for SMS or SendGrid for email. diff --git a/components/wix_api_key/README.md b/components/wix_api_key/README.md new file mode 100644 index 0000000000000..b6e30016a2951 --- /dev/null +++ b/components/wix_api_key/README.md @@ -0,0 +1,11 @@ +# Overview + +The Wix API allows for the management and automation of various aspects of Wix-based web applications. Through Pipedream, you can harness this capability to create, update, retrieve, and delete information from your Wix site, such as managing site content, handling e-commerce orders, and engaging with users. With Pipedream, you can also trigger workflows based on events in Wix, connect with hundreds of other services, and process data with custom logic. + +# Example Use Cases + +- **Sync Wix Store Orders to Google Sheets**: Automate the process of capturing new Wix store orders and appending them to a Google Sheets spreadsheet. This is ideal for maintaining order records, performing analysis, or sharing order data with your team in real-time. + +- **Send Welcome Emails via SendGrid**: Trigger an email through SendGrid to new Wix site members when they register. This workflow can be customized to include personalized content, offering a professional touch to your onboarding process. + +- **Wix Blog Post to Social Media Announcement**: When a new blog post is published on your Wix site, automatically share it across social media platforms like Twitter or Facebook to increase visibility and engagement with your audience. diff --git a/components/wiza/README.md b/components/wiza/README.md new file mode 100644 index 0000000000000..3fcef9a1bec2b --- /dev/null +++ b/components/wiza/README.md @@ -0,0 +1,11 @@ +# Overview + +The Wiza API offers robust tools for scraping LinkedIn profiles and turning them into structured data such as emails, names, and company details. This data can be pivotal for sales and marketing professionals to build lead lists, validate contact information, and enrich existing databases directly through API integration. Leveraging this API on Pipedream allows users to automate the entire process of lead generation and management by connecting Wiza to a myriad of other services like CRMs, email marketing platforms, and data analysis tools. + +# Example Use Cases + +- **Lead Generation Automation**: Automatically extract LinkedIn data using Wiza, then use this data to create or update leads in Salesforce. This workflow can trigger whenever a new profile matches predefined LinkedIn search criteria, ensuring up-to-date lead information is always available for sales teams. + +- **Email Marketing Integration**: Use Wiza to gather detailed contact information, and then feed this data into Mailchimp to automate the creation of targeted email campaigns. This can also include automated segmentation based on the professional details collected, such as industry or job role, to tailor marketing efforts. + +- **CRM Enrichment**: Enhance the utility of a CRM like HubSpot by automatically enriching contacts with additional data fetched via Wiza. This workflow can periodically update contact information in the CRM, ensuring that sales and marketing teams always have the most relevant and detailed information at their fingertips. diff --git a/components/wiza/actions/create-list/create-list.mjs b/components/wiza/actions/create-list/create-list.mjs new file mode 100644 index 0000000000000..26df2cd6a46ee --- /dev/null +++ b/components/wiza/actions/create-list/create-list.mjs @@ -0,0 +1,78 @@ +import app from "../../wiza.app.mjs"; + +export default { + key: "wiza-create-list", + name: "Create List", + description: "Create a list of people to enrich. [See the documentation](https://wiza.co/api-docs#/paths/~1api~1lists/post)", + version: "0.0.1", + type: "action", + props: { + app, + name: { + propDefinition: [ + app, + "name", + ], + }, + enrichmentLevel: { + propDefinition: [ + app, + "enrichmentLevel", + ], + }, + acceptGeneric: { + propDefinition: [ + app, + "acceptGeneric", + ], + }, + acceptPersonal: { + propDefinition: [ + app, + "acceptPersonal", + ], + }, + acceptWork: { + propDefinition: [ + app, + "acceptWork", + ], + }, + fullName: { + propDefinition: [ + app, + "fullName", + ], + }, + company: { + propDefinition: [ + app, + "company", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createList({ + $, + data: { + name: this.description, + enrichmentLevel: this.enrichmentLevel, + email_options: { + accept_generic: this.acceptGeneric, + accept_personal: this.acceptPersonal, + accept_work: this.acceptWork, + }, + items: [ + { + full_name: this.fullName, + company: this.company, + }, + ], + }, + }); + + $.export("$summary", `'${response.status.message}', your list's ID is '${response.data.id}'`); + + return response; + }, +}; diff --git a/components/wiza/actions/get-contacts/get-contacts.mjs b/components/wiza/actions/get-contacts/get-contacts.mjs new file mode 100644 index 0000000000000..412bc4f369666 --- /dev/null +++ b/components/wiza/actions/get-contacts/get-contacts.mjs @@ -0,0 +1,37 @@ +import app from "../../wiza.app.mjs"; + +export default { + key: "wiza-get-contacts", + name: "Get Contacts", + description: "Get contacts for a list. [See the documentation](https://wiza.co/api-docs#/paths/~1api~1lists~1%7Bid%7D/get)", + version: "0.0.1", + type: "action", + props: { + app, + id: { + propDefinition: [ + app, + "id", + ], + }, + segment: { + propDefinition: [ + app, + "segment", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getContacts({ + $, + id: this.id, + params: { + segment: this.segment, + }, + }); + + $.export("$summary", "Successfully retrieved the list's contacts"); + + return response; + }, +}; diff --git a/components/wiza/actions/get-list/get-list.mjs b/components/wiza/actions/get-list/get-list.mjs new file mode 100644 index 0000000000000..8178c4dfef8ee --- /dev/null +++ b/components/wiza/actions/get-list/get-list.mjs @@ -0,0 +1,28 @@ +import app from "../../wiza.app.mjs"; + +export default { + key: "wiza-get-list", + name: "Get List", + description: "Get the list with the given id. [See the documentation](https://wiza.co/api-docs#/paths/~1api~1lists~1%7Bid%7D/get)", + version: "0.0.1", + type: "action", + props: { + app, + id: { + propDefinition: [ + app, + "id", + ], + }, + }, + async run({ $ }) { + const response = await this.app.getList({ + $, + id: this.id, + }); + + $.export("$summary", `The status of your list is: '${response.data.status}'`); + + return response; + }, +}; diff --git a/components/wiza/common/constants.mjs b/components/wiza/common/constants.mjs new file mode 100644 index 0000000000000..26fad292c8ab3 --- /dev/null +++ b/components/wiza/common/constants.mjs @@ -0,0 +1,11 @@ +export default { + SEGMENTS: [ + "people", + "valid", + "risky", + ], + ENRICHMENT_LEVELS: [ + "partial", + "full", + ], +}; diff --git a/components/wiza/package.json b/components/wiza/package.json new file mode 100644 index 0000000000000..b14fd374063dc --- /dev/null +++ b/components/wiza/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/wiza", + "version": "0.1.0", + "description": "Pipedream Wiza Components", + "main": "wiza.app.mjs", + "keywords": [ + "pipedream", + "wiza" + ], + "homepage": "https://pipedream.com/apps/wiza", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} diff --git a/components/wiza/wiza.app.mjs b/components/wiza/wiza.app.mjs new file mode 100644 index 0000000000000..eb16094d1028c --- /dev/null +++ b/components/wiza/wiza.app.mjs @@ -0,0 +1,100 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "wiza", + propDefinitions: { + acceptWork: { + type: "boolean", + label: "Work Email", + description: "Accept professional email address? i.e. 'tim.cooke@apple.com'", + }, + acceptPersonal: { + type: "boolean", + label: "Personal Email", + description: "Accept personal email address? i.e. 'tcooke1960@gmail.com'", + }, + acceptGeneric: { + type: "boolean", + label: "Generic Email", + description: "Accept generic email address? i.e. 'hello@apple.com'", + }, + enrichmentLevel: { + type: "string", + label: "Enrichment Level", + description: "Enrichment level of the list.", + options: constants.ENRICHMENT_LEVELS, + }, + name: { + type: "string", + label: "Name", + description: "Name of the list", + }, + fullName: { + type: "string", + label: "Full Name", + description: "Full name of the contact", + }, + company: { + type: "string", + label: "Company", + description: "Name of the company", + }, + id: { + type: "string", + label: "List ID", + description: "ID of the list", + }, + segment: { + type: "string", + label: "Segment", + description: "Specify the segment of contacts to return", + options: constants.SEGMENTS, + }, + }, + methods: { + _baseUrl() { + return "https://wiza.co/api"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + async getList({ + args, id, + }) { + return this._makeRequest({ + path: `/lists/${id}`, + ...args, + }); + }, + async getContacts({ + id, ...args + }) { + return this._makeRequest({ + path: `/lists/${id}/contacts`, + ...args, + }); + }, + async createList(args = {}) { + return this._makeRequest({ + method: "post", + path: "/lists", + ...args, + }); + }, + }, +}; diff --git a/components/wolfram_alpha/actions/perform-computation/perform-computation.mjs b/components/wolfram_alpha/actions/perform-computation/perform-computation.mjs new file mode 100644 index 0000000000000..daffdc3f8e428 --- /dev/null +++ b/components/wolfram_alpha/actions/perform-computation/perform-computation.mjs @@ -0,0 +1,81 @@ +import app from "../../wolfram_alpha.app.mjs"; + +export default { + key: "wolfram_alpha-perform-computation", + name: "Perform Computation", + description: "Executes a computation query using the Wolfram Alpha API. [See the documentation](https://products.wolframalpha.com/api/documentation)", + version: "0.0.1", + type: "action", + props: { + app, + input: { + type: "string", + label: "Input", + description: "Text specifying the input string.", + }, + width: { + type: "string", + label: "Width", + description: "Specify an approximate width limit for text and tables. Eg. `200`, `500`. This parameter does not affect plots or graphics. Width values are approximate; behavior may vary for different content.", + optional: true, + }, + maxWidth: { + type: "string", + label: "Max Width", + description: "Specify an extended maximum width for large objects. Eg. `200`, `500`. This parameter does not affect plots or graphics. Width values are approximate; behavior may vary for different content.", + optional: true, + }, + plotWidth: { + type: "string", + label: "Plot Width", + description: "Specify an approximate width limit for plots and graphics. Eg. `100`, `200`. This parameter does not affect text or tables. Width values are approximate; behavior may vary for different content.", + optional: true, + }, + mag: { + type: "string", + label: "Magnification", + description: "Specify magnification of objects within a pod. Eg. `0.5`, `1.0`, `2.0`. Changing this parameter does not affect the overall size of pods.", + optional: true, + }, + assumption: { + type: "string", + label: "Assumption", + description: "Specifies an assumption, such as the meaning of a word or the value of a formula variable. Eg. `*C.pi-_*Movie`, `DateOrder_**Day.Month.Year--`. Assumptions made implicitly by the API. Values for this parameter are given by the input properties of `` subelements of `` elements in XML results.", + optional: true, + }, + }, + methods: { + performComputation(args = {}) { + return this.app.makeRequest({ + path: "/llm-api", + ...args, + }); + }, + }, + async run({ $ }) { + const { + performComputation, + input, + width, + maxWidth, + plotWidth, + mag, + assumption, + } = this; + + const response = await performComputation({ + $, + params: { + input, + width, + maxwidth: maxWidth, + plotwidth: plotWidth, + mag, + assumption, + }, + }); + + $.export("$summary", "Computation performed successfully"); + return response; + }, +}; diff --git a/components/wolfram_alpha/common/constants.mjs b/components/wolfram_alpha/common/constants.mjs new file mode 100644 index 0000000000000..3b9f30472c918 --- /dev/null +++ b/components/wolfram_alpha/common/constants.mjs @@ -0,0 +1,7 @@ +const BASE_URL = "https://www.wolframalpha.com"; +const VERSION_PATH = "/api/v1"; + +export default { + BASE_URL, + VERSION_PATH, +}; diff --git a/components/wolfram_alpha/package.json b/components/wolfram_alpha/package.json new file mode 100644 index 0000000000000..29e6de1510245 --- /dev/null +++ b/components/wolfram_alpha/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/wolfram_alpha", + "version": "0.1.0", + "description": "Pipedream Wolfram Alpha Components", + "main": "wolfram_alpha.app.mjs", + "keywords": [ + "pipedream", + "wolfram_alpha" + ], + "homepage": "https://pipedream.com/apps/wolfram_alpha", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/wolfram_alpha/wolfram_alpha.app.mjs b/components/wolfram_alpha/wolfram_alpha.app.mjs new file mode 100644 index 0000000000000..98cac1a0d57e3 --- /dev/null +++ b/components/wolfram_alpha/wolfram_alpha.app.mjs @@ -0,0 +1,27 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "wolfram_alpha", + methods: { + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getAuthParams(params) { + return { + ...params, + appid: this.$auth.app_id, + }; + }, + makeRequest({ + $ = this, path, params, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + params: this.getAuthParams(params), + }); + }, + }, +}; diff --git a/components/wolfram_alpha_api/package.json b/components/wolfram_alpha_api/package.json new file mode 100644 index 0000000000000..92a5dbf8690b5 --- /dev/null +++ b/components/wolfram_alpha_api/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/wolfram_alpha_api", + "version": "0.0.1", + "description": "Pipedream Wolfram Alpha API Components", + "main": "wolfram_alpha_api.app.mjs", + "keywords": [ + "pipedream", + "wolfram_alpha_api" + ], + "homepage": "https://pipedream.com/apps/wolfram_alpha_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/wolfram_alpha_api/wolfram_alpha_api.app.mjs b/components/wolfram_alpha_api/wolfram_alpha_api.app.mjs new file mode 100644 index 0000000000000..7671b73e18ad5 --- /dev/null +++ b/components/wolfram_alpha_api/wolfram_alpha_api.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "wolfram_alpha_api", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/wonderchat/README.md b/components/wonderchat/README.md new file mode 100644 index 0000000000000..431b9b3bd5390 --- /dev/null +++ b/components/wonderchat/README.md @@ -0,0 +1,11 @@ +# Overview + +The Wonderchat API lets you automate interactions with your Wonderchat account, streamlining conversations and enhancing customer engagement. On Pipedream, you can build workflows that trigger on specific events and automate tasks like sending messages, managing contacts, or integrating with CRMs. By coupling the Wonderchat API with Pipedream’s capabilities, you get a powerful synergy where you can seamlessly connect various apps and services, run serverless code, and manipulate data with ease. + +# Example Use Cases + +- **Automate Welcome Messages**: When a new contact is added in Wonderchat, trigger a Pipedream workflow that sends a personalized welcome message. This small touch can significantly impact user experience and retention. + +- **Sync Contacts with CRM**: Whenever a contact is updated or tagged in Wonderchat, use a Pipedream workflow to sync these changes to a CRM like Salesforce. Keeping customer information up-to-date across platforms enhances sales and support efforts. + +- **Customer Support Ticketing**: Integrate Wonderchat with a ticketing system such as Zendesk. When a conversation on Wonderchat reaches a certain threshold or keyword is detected, create a support ticket in Zendesk to ensure no query goes unnoticed. diff --git a/components/wonderchat/package.json b/components/wonderchat/package.json index dd4a4e124c333..e34131a03e4f1 100644 --- a/components/wonderchat/package.json +++ b/components/wonderchat/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/woocommerce/README.md b/components/woocommerce/README.md index 1cc513c55565b..cb7b81097235d 100644 --- a/components/woocommerce/README.md +++ b/components/woocommerce/README.md @@ -1,16 +1,61 @@ # Overview -The WooCommerce API allows developers to interact with WooCommerce sites in a -number of ways. With the API, developers can create, read, update, and delete -data such as products, orders, customers, etc. WooCommerce also provides a -number of API endpoints for retrieving information about the site, such as -settings, currencies, etc. - -Here are some examples of what you can build using the WooCommerce API: - -- A mobile app for browsing and ordering products from a WooCommerce store -- A desktop application for managing a WooCommerce store -- A web application for displaying real-time information about a WooCommerce - store (sales, stock levels, etc) -- An API extension for WooCommerce that allows third-party applications to - access data in a WooCommerce store +WooCommerce is a customizable, open-source eCommerce platform built on WordPress. With the WooCommerce API, you can tap into the heart of your eCommerce store to read, create, update, and delete products, orders, and customers. On Pipedream, you can harness this API to automate routine tasks, sync data across platforms, and enhance customer experiences. By connecting WooCommerce to a wide array of apps and services, you can streamline operations, trigger personalized marketing, and analyze your sales data with greater ease. + +# Example Use Cases + +- **Sync New Orders to Google Sheets**: When a new order is placed on your WooCommerce store, automatically add its details to a Google Sheets spreadsheet. This process aids in real-time order tracking and inventory management without manually exporting data. + +- **Automated Order Confirmation Emails**: Use the WooCommerce API to monitor new orders and trigger personalized confirmation emails through a service like SendGrid. This workflow can include order details, expected delivery dates, and upsell opportunities. + +- **Customer Support Ticket Creation**: On receiving a new customer inquiry or support request via WooCommerce, instantly generate a ticket in a customer support platform like Zendesk. This ensures no customer query goes unnoticed and helps maintain high service standards. + +# Getting Started + +To connect your WooCommerce store to Pipedream, create a REST API key. + +## Creating a WooCommerce REST API Key + +Open the **WooCommerce** plugin in your WordPress admin dashboard and select the Advanced tab. Navigate to the **REST API** section and click **Create an API key**. + +![Creating an API key for WooCommerce from within your WordPress admin dashboard](https://res.cloudinary.com/pipedreamin/image/upload/v1715009382/marketplace/apps/woocommerce/CleanShot_2024-05-06_at_11.25.25_2x_tf9j1w.png) + +We recommend naming this API key "Pipedream" to easily remember its purpose. Select a user account that this API key should be tied to, ideally one with at least store manager access. + +Next, choose the level of permission you’d like Pipedream workflows to have. You can choose between: + +- **Read** - your Pipedream workflows can only read data, not update or insert new data. +- **Write** - your Pipedream workflows can update or insert data like orders and products, but cannot read them. +- **Read/Write** - your Pipedream workflows can both read and write data on your WooCommerce store. + +![Choose the user, name and the permissions level for your WooCommerce API key](https://res.cloudinary.com/pipedreamin/image/upload/v1715009382/marketplace/apps/woocommerce/CleanShot_2024-05-06_at_11.26.05_2x_rfyt9k.png) + +After generating the API key, you’ll receive a **Consumer Key** and **Consumer Secret**. Copy these values into Pipedream under the respective **Key** and **Secret** fields. + +Finally, enter your domain name. For example, for `https://example-store.com`, simply enter `example-store.com`. If your store is hosted on a subpath, like `https://my-site.com/store`, enter `my-site.com/store`. + +Double-check your store’s home URL under the **Settings area in WordPress.** + +# Troubleshooting + +## Unable to connect to the WooCommerce REST API + +### Check your Permalink structure + +![Make sure your WordPress store has the Post Name URL structure for Pipedream to be able to connect to it correctly.](https://res.cloudinary.com/pipedreamin/image/upload/v1715010321/marketplace/apps/woocommerce/CleanShot_2024-05-06_at_11.42.43_2x_qe3qu8.png) + +To enable Pipedream's access to your WooCommerce store’s REST API, ensure your WordPress site’s Permalink Structure is set to `Post name`. Open the **Settings** area in WordPress, navigate to the **Permalink** section, verify that the **Permalink** setting is set to `Post name`, and click **Save** to apply the change. + +### Check your WordPress Home URL + +If you still cannot connect your WooCommerce store to Pipedream, ensure that your site’s Home URL structure is not on a subdomain or subpath. You can find your WordPress site’s Site URL under the Settings > General section. + +If your site’s URL is `https://example.com`, then the Pipedream `domain` field for your WooCommerce connected account should be `example.com`. If your WordPress site’s home URL is under a different subdomain, such as `https://store.example.com`, then enter `store.example.com` for the `domain` field for your connected account. + +### Firewall issues + +By default, Pipedream Workflows originate from dynamic IP addresses within the `us-east-1` region in AWS. To allow Pipedream connections, set up a Pipedream VPC to assign a static IP address to your workflows. + +## Permissions issues + +Ensure the user associated with your WooCommerce API key has the necessary permissions to read or write specific resources like products or orders. Additionally, double-check that your API key has read and/or write permissions. diff --git a/components/woodpecker_co/README.md b/components/woodpecker_co/README.md index 653319fc4ed3f..0a802fc775c7b 100644 --- a/components/woodpecker_co/README.md +++ b/components/woodpecker_co/README.md @@ -1,19 +1,11 @@ # Overview -Woodpecker.co's API is a powerful tool that enables developers and -entrepreneurs to build powerful applications and tools for sending automated -emails and communication. With the Woodpecker.co API, you can create custom -automated emails and other communications for your online customers and website -visitors. +The Woodpecker.co API lets you automate your email outreach and follow-up processes. With it, you can manage contacts, campaigns, and emails programmatically, creating a seamless bridge between your email campaigns and your data. This API, integrated with Pipedream, unlocks powerful workflows that can save time, personalize communication at scale, and keep your data in sync across various platforms. -Here are some examples of things you can build using the Woodpecker.co API: +# Example Use Cases -- Automated Welcome Emails for Newly Registered Users -- Automated Reminders for Scheduled Appointments -- Automated Follow-up Emails for Prospective Customers -- Automated Customer Feedback Forms -- Customizable Email Templates for Outreach -- Automated Newsletters -- Automated Notifications for Special Offers and Promotions -- Automated Surveys and Polls -- Automated Response Management and Tracking +- **Automated Lead Nurturing Workflow**: Trigger a sequence of personalized follow-up emails to new leads captured from your CRM like Salesforce or HubSpot. When a new lead is added to your CRM, use Pipedream to automatically add their contact information to a Woodpecker.co campaign, ensuring they receive timely, targeted communication that moves them down the sales funnel. + +- **Feedback Collection Automation**: After closing a deal or completing a project, automatically send out a feedback request email using Woodpecker.co. Connect it to a form app like Typeform on Pipedream, so when a client fills out a feedback form, their responses trigger a thank-you email and the data is stored for analysis, helping you to improve services or products continuously. + +- **Webinar Attendee Follow-Up**: Enhance engagement with webinar attendees by setting up a post-webinar follow-up sequence. Once Pipedream detects a new attendee registration in your webinar platform, such as Zoom, add the attendee to a dedicated Woodpecker.co campaign that sends out a thank-you email, additional resources, and even sets reminders for future webinars, all aimed at keeping your brand at the top of their mind. diff --git a/components/woopra/README.md b/components/woopra/README.md new file mode 100644 index 0000000000000..b02a4eafc28b8 --- /dev/null +++ b/components/woopra/README.md @@ -0,0 +1,11 @@ +# Overview + +The Woopra API lets you track customer interactions in real time, analyze customer behavior, and automate responses based on that behavior. Using Pipedream, you can create workflows that trigger on various events, push or pull data to/from Woopra, and connect Woopra with other apps to enhance your data-driven decision-making processes. + +# Example Use Cases + +- **Real-time Customer Activity Alerting**: Whenever a customer performs a significant action, like a purchase or a subscription upgrade, Pipedream can catch this event from Woopra and send notifications through email, Slack, or SMS to the relevant team members instantly. + +- **Customer Segmentation and Tagging**: Using Pipedream, you can segment customers based on their behavior as reported by Woopra and then tag them in your CRM for targeted marketing campaigns. This can be done by connecting Woopra with a CRM tool like Salesforce to automate the segmentation process. + +- **Event-Driven Email Campaigns**: Trigger personalized email campaigns with Mailchimp or SendGrid when Woopra detects specific customer behaviors, like abandoning a cart or spending a certain amount of time on a product page. This automates the process of re-engaging customers based on their interactions with your site. diff --git a/components/woovi/README.md b/components/woovi/README.md new file mode 100644 index 0000000000000..f9beadd1f32a2 --- /dev/null +++ b/components/woovi/README.md @@ -0,0 +1,11 @@ +# Overview + +The Woovi API provides capabilities to interact with Woovi's video streaming services. In Pipedream, you can leverage this API to automate workflows related to video content management, distribution, and analytics. Custom integrations can help you streamline operations, such as automating video uploads, syncing metadata, or triggering actions based on viewer data. + +# Example Use Cases + +- **Automated Video Uploads**: Build a workflow where videos are automatically uploaded to Woovi from a cloud storage provider like Dropbox or Google Drive. Once a new video is detected in a specified folder, Pipedream can handle the upload to Woovi, saving you time and minimizing manual intervention. + +- **Content Metadata Sync**: Maintain consistent metadata across platforms by creating a workflow that syncs metadata from your CMS to Woovi. Whenever content is updated in your CMS, Pipedream can trigger an update to ensure that video titles, descriptions, and tags are mirrored on Woovi. + +- **Viewer Analytics Trigger**: Use Pipedream to set up a workflow that listens for new viewer analytics from Woovi and triggers actions based on that data. For example, if a video reaches a certain view count, you could automatically send an email alert or post a congratulatory message on social media. diff --git a/components/wordpress_com/README.md b/components/wordpress_com/README.md index c05e6628dca0a..46f51cadf9b2e 100644 --- a/components/wordpress_com/README.md +++ b/components/wordpress_com/README.md @@ -1,24 +1,11 @@ # Overview -WordPress.com allows developers to create powerful applications and websites, -using the WordPress.com API. With the WordPress.com API, developers can create -custom applications and websites that integrate with the WordPress.com platform -in meaningful ways. +The Wordpress.com API empowers developers to extend and integrate their website's capabilities with custom automations and connections to a multitude of apps. Through Pipedream's serverless platform, you have the ability to automate content management tasks such as posting new articles, managing comments, and synchronizing users. This can streamline content workflows, enhance user engagement, and keep your site's data in sync with other services like social media, email marketing platforms, and analytics tools. -The WordPress.com API is accessible via REST, which allows developers to -interact with WordPress.com in a variety of ways. Additionally, developers can -extend the WordPress.com platform by creating custom plugins and themes. +# Example Use Cases -Here are some examples of what you can build with the Wordpress.com API: +- **Automated Content Distribution**: When a new post is published on Wordpress.com, trigger a Pipedream workflow to share the post across various social media platforms like Twitter, Facebook, and LinkedIn. This workflow can grab the post's title, excerpt, and link, then use platform-specific APIs to create new social media posts, maximizing the reach of your content without manual effort. -- Custom blog or website -- Online store -- Custom dashboard for managing content -- Custom user profiles -- Analytics and reporting tools -- Social media integration -- Search engine optimization -- Email campaigns -- Custom payment gateway solutions -- Content marketing platforms -- Advertising networks +- **Comment Moderation Alerts**: Set up a Pipedream workflow that monitors Wordpress.com for new comments. When a comment is detected, analyze its content for specific keywords or sentiment using a service like Google's Natural Language API. If the comment requires attention (e.g., contains flagged words or negative sentiment), send an immediate alert to a Slack channel or via email to prompt review and moderation, keeping your community healthy and engaged. + +- **User Synchronization and Engagement**: Create a workflow that triggers when a new user registers on your Wordpress.com site. This workflow can add the user to a CRM like HubSpot or Salesforce, subscribe them to a Mailchimp email list, and even send a personalized welcome email via SendGrid. This ensures your user data is consistent across platforms and kickstarts the user engagement process from the moment they sign up. diff --git a/components/wordpress_com/package.json b/components/wordpress_com/package.json new file mode 100644 index 0000000000000..0d41d964ec38f --- /dev/null +++ b/components/wordpress_com/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/wordpress_com", + "version": "0.6.0", + "description": "Pipedream wordpress_com Components", + "main": "wordpress_com.app.mjs", + "keywords": [ + "pipedream", + "wordpress_com" + ], + "homepage": "https://pipedream.com/apps/wordpress_com", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/wordpress_org/README.md b/components/wordpress_org/README.md index 0ffd8f77b4a4b..2446f19efdd3e 100644 --- a/components/wordpress_org/README.md +++ b/components/wordpress_org/README.md @@ -1,32 +1,11 @@ # Overview -WordPress API can help you create a variety of different projects with greater -efficiency and functionality. Whether it’s developing and creating a custom -WordPress site, creating an API for a product, or developing a plugin for a -program, the WordPress API offers more possibilities than ever to customize and -enhance your online experience. Here are a few examples of things you can build -using the WordPress API: +The WordPress.org API offers a wide range of capabilities for content management, theme and plugin information, and community engagement. With Pipedream, you can harness this API to create automated workflows that react to events in WordPress, sync content across platforms, or even manage your site's appearance and functionality programmatically. Whether you're looking to streamline your publishing process, enhance user interaction, or keep everything in sync, the WordPress.org API on Pipedream offers a powerful toolset to craft custom solutions. -- Custom WordPress Sites: With WordPress API, you can create specialized and - customized sites that fit your exact needs and vision. From e-commerce stores - to magazine websites and anything else imaginable, WordPress API can help you - create and customize a website that’s tailored to your exact needs. -- Product API: If you’re a developer and you have products that require an API, - WordPress API can help you easily create one. You can create a custom API - that helps you manage your products and make them available to other - applications and websites. -- Plugin Creation: If you need a plugin for something you’re developing, the - WordPress API can help you create something tailored to your exact needs. - From custom shortcodes to mini programs and full-fledged plugins, the - WordPress API has what you need. -- Widgets: Widgets are small pieces of code that you can use on your website to - display different types of content. You can use the WordPress API to create - custom widgets that help you display plugins, videos, images, and more. -- Custom Social Network Platforms: The WordPress API offers more possibilities - than ever to create custom social network platforms. With access to the API, - you can create specialized networks for your website, unfettered by - third-party restrictions. -- Mobile Applications:Do you need to create a mobile application that interacts - with your website? The WordPress API can provide the tools and features you - need to create a smooth and seamless integration between your website and - your app. +# Example Use Cases + +- **Automated Content Syndication**: Publish new WordPress posts automatically to social media platforms like Twitter or Facebook. Whenever you push a new post, Pipedream can capture this event and send out tweets or posts with links back to your article, driving traffic and engagement. + +- **Dynamic Content Updates**: Update content across multiple WordPress sites in response to a single trigger. For instance, when you update a post on your main site, Pipedream can propagate these changes to other WordPress sites you manage, ensuring content consistency. + +- **Scheduled Theme Changes**: Swap WordPress themes based on time or specific triggers, such as holidays or promotional events. With Pipedream, you can schedule these changes in advance, so your site reflects the right mood or branding at just the right time. diff --git a/components/workamajig/README.md b/components/workamajig/README.md new file mode 100644 index 0000000000000..5abf01381410e --- /dev/null +++ b/components/workamajig/README.md @@ -0,0 +1,11 @@ +# Overview + +The Workamajig API allows for automation and integration of project management tasks within the Workamajig platform. Using Pipedream, developers can create workflows to manage projects, resources, and client accounts by triggering events, syncing data, and automating processes. Pipedream's serverless platform enables the execution of these workflows on demand or on a schedule, making it possible to connect Workamajig with other apps and services to streamline operations. + +# Example Use Cases + +- **Project Status Updates to Slack**: Sync Workamajig project updates to a Slack channel. Whenever a project status changes in Workamajig, a Pipedream workflow triggers and sends a notification with details to a designated Slack channel, keeping the team informed in real-time. + +- **Sync Workamajig Tasks with Google Calendar**: Automatically create or update Google Calendar events based on tasks in Workamajig. When a new task is assigned or updated in Workamajig, Pipedream triggers a workflow that reflects these changes in Google Calendar, aiding in time management and scheduling. + +- **Automate Invoice Creation and Delivery**: Generate invoices in Workamajig and deliver them via email. With Pipedream, a workflow can be set to create invoices for completed projects and use an email service like SendGrid to send the invoices to clients, streamlining the billing process. diff --git a/components/workast/README.md b/components/workast/README.md index edec00ae55267..db109ce99c0bd 100644 --- a/components/workast/README.md +++ b/components/workast/README.md @@ -1,24 +1,14 @@ # Overview -The Workast API allows developers to create powerful applications for -streamlining the process of task management. With the Workast API, you can -build applications that will help users automate their personal and -professional task management workflow. +The Workast API enables the automation of task management in your projects, allowing the creation, update, and tracking of tasks without leaving the context of your current workflow. With the Workast API on Pipedream, you can tap into this functionality to build seamless integrations and automate repetitive actions, such as syncing tasks across multiple platforms, generating reports, or triggering reminders and notifications based on specific task events. -Here are some examples of what you can build using the Workast API: +# Example Use Cases -- Task Management Systems: You can build applications to help users manage - their tasks, reminders, and subtasks. -- Collaboration Platforms: With the Workast API, you can create an application - to help teams collaborate on tasks and share updates in real-time. -- Project Dashboards: You can create an application that will help users - visualize the progress of their projects. -- Scheduling: Applications that integrate with calendars, to help users create - and manage their schedules. -- Reporting: With the Workast API, you can build application with reporting - capabilities, to help users track their task progress and keep track of their - team’s progress. -- Database Management: You can create an application to help users store and - organize their data. -- Time Tracking: Create an application that helps users keep track of their - time and track the progress of their tasks. +- **Task Synchronization Across Platforms** + Automate the synchronization of tasks between Workast and a project management tool like Trello or Asana. When a task is created or updated in Workast, trigger a workflow on Pipedream that replicates the task in Trello or Asana, keeping all team members aligned across different platforms. + +- **Automated Task Reporting** + Generate daily or weekly task reports by setting up a Pipedream workflow that pulls completed tasks from Workast, formats them into a digestible report, and sends it via email using a service like SendGrid or directly to a Slack channel to keep the team informed on progress. + +- **Dynamic Notifications Based on Task Activity** + Create a workflow that listens for specific task updates in Workast, such as a due date change or a task completion, and then triggers personalized notifications. Use Pipedream to send these alerts through SMS with Twilio, Slack messages, or even update a Google Sheet used for tracking project milestones. diff --git a/components/workboard/README.md b/components/workboard/README.md index 00ee55c48ea45..b8759c584a476 100644 --- a/components/workboard/README.md +++ b/components/workboard/README.md @@ -1,24 +1,11 @@ # Overview -With the Workboard API, you can easily build solutions that can help you -automate parts of your organization's workflow. Here are a few examples of -solutions you can create with the Workboard API: +The Workboard API enables you to interact with Workboard's platform programmatically, allowing for the automation of tasks related to goal setting, action item tracking, and performance metrics. By deploying workflows on Pipedream, you can connect Workboard to a multitude of other apps, triggering actions and exchanging data to streamline your organization's operations and decision-making processes. A well-integrated system can save time, reduce errors, and provide real-time insights into team performance and project progress. -- Automatically create a Stream of tasks based on a client’s data. -- Create Streams that capture different types of progress notifications. -- Create Streams that integrate data from other applications into Workboard. -- Consolidate Teams and their tasks into a single timeline view. -- Automatically update task status and assign tasks to Teams. -- Automatically generate reports with data from multiple Streams. -- Create reports to track the performance of different Teams in realtime. -- Generate graphs and charts to help visualize trends in your organization. -- Automatically create alert notifications for tasks and Streams. -- Provide personalized notifications for different users and for specific - tasks. -- Automatically provision and de-provision user accounts. -- Connect different systems (like ticketing, customer support, etc.) to - Workboard. -- Automate the creation and management of Streams. -- Automatically create project plans and milestones. -- Automatically transfer data between Streams. -- Create custom widgets and views to help visualize complex data. +# Example Use Cases + +- **Automated Meeting Summaries**: After a meeting concludes in your calendar app, a Pipedream workflow can trigger to pull key results and action items from Workboard, format them into a concise summary, and then send the summary via Slack or email to all participants. + +- **Project Progress Tracking**: Integrate Workboard with a project management tool like Jira. When updates are made in Jira, a workflow on Pipedream can automatically update the corresponding goals and action items in Workboard, ensuring that all team members have the latest project status. + +- **Performance Dashboard Updates**: Use Pipedream to connect Workboard with a data visualization tool like Google Sheets or Tableau. As team members update their goals and metrics in Workboard, the workflow triggers to populate these changes into a dashboard, providing real-time performance updates to stakeholders. diff --git a/components/workboard/package.json b/components/workboard/package.json new file mode 100644 index 0000000000000..23513ccb39aec --- /dev/null +++ b/components/workboard/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/workboard", + "version": "0.6.0", + "description": "Pipedream workboard Components", + "main": "workboard.app.mjs", + "keywords": [ + "pipedream", + "workboard" + ], + "homepage": "https://pipedream.com/apps/workboard", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/workbooks_crm/README.md b/components/workbooks_crm/README.md index 0850882874f59..1cfaf19ee09fa 100644 --- a/components/workbooks_crm/README.md +++ b/components/workbooks_crm/README.md @@ -1,25 +1,9 @@ # Overview -Using Workbook CRM's powerful API, you have the opportunity to build powerful -business applications like never before. With its intuitive interface, -streamlined processes and wraparound features, the Workbooks CRM API offers -sophisticated software solutions to meet a variety of organizational needs. +The Workbooks CRM API unlocks a treasure trove of possibilities for automating customer relationship management tasks. With this API, you can seamlessly sync contact info, manage sales leads, automate follow-up tasks, and build custom reports to unlock actionable insights. Harnessing the power of the Workbooks CRM API on Pipedream allows you to supercharge these functionalities, triggering workflows on custom events, and connecting to numerous other apps to streamline your CRM processes even further. -From custom tailored customer Account and Contact Management dashboards to -project tracking and sales order automation, the Workbooks CRM API enables you -to create, store and access data seamlessly. With its secure, cloud-based -architecture, you can create, update, delete and query data, ensuring that only -authorized personnel can access the necessary materials. +# Example Use Cases -The areas you can build with the Workbooks CRM API, include: - -- Customizable customer account and contact management -- Real-time project tracking and asset management -- Sales order automation -- Accurate data collection and analysis -- Automated CRM reporting -- Powerful marketing automation tools -- Robust service management and support -- Streamlined customer onboarding processes -- Comprehensive B2B and B2C portals -- User-friendly lead and opportunity management +- **Sales Lead Auto-Assignment**: Automatically assign new leads to sales reps based on custom criteria such as geographic location or industry. When a new lead is created in Workbooks CRM, a Pipedream workflow can parse the lead details and assign it to the correct sales rep in your team, sending them a notification via Slack or email. +- **Customer Onboarding Sequence**: Kick off a customer onboarding sequence when a deal is marked as won. This could involve sending a series of welcome emails via SendGrid, creating tasks for account managers in Asana, and scheduling a check-in call using the Zoom API. +- **Support Ticket Integration**: Integrate customer support by creating a ticket in a service like Zendesk whenever there is a significant update on a customer's account in Workbooks CRM. Use Pipedream to monitor for changes in account status or other key fields, and automatically create or update tickets in Zendesk to ensure timely customer support. diff --git a/components/workflow_max/package.json b/components/workflow_max/package.json new file mode 100644 index 0000000000000..f2f7f8bca8e90 --- /dev/null +++ b/components/workflow_max/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/workflow_max", + "version": "0.0.1", + "description": "Pipedream Workflow Max Components", + "main": "workflow_max.app.mjs", + "keywords": [ + "pipedream", + "workflow_max" + ], + "homepage": "https://pipedream.com/apps/workflow_max", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/workflow_max/workflow_max.app.mjs b/components/workflow_max/workflow_max.app.mjs new file mode 100644 index 0000000000000..55595e2ee87f7 --- /dev/null +++ b/components/workflow_max/workflow_max.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "workflow_max", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/workiom/README.md b/components/workiom/README.md index 45f0c20506173..8637278072b5f 100644 --- a/components/workiom/README.md +++ b/components/workiom/README.md @@ -1,21 +1,11 @@ # Overview -The Workiom API enables you to build powerful custom applications to customize -and streamline your business. With the Workiom API, you can design and develop -customized applications that leverage the powerful Workiom platform to increase -productivity and collaboration. +Workiom is a no-code platform that helps teams build custom work management apps. The Workiom API allows for extensive customization and automation, enabling you to streamline business processes by connecting your custom apps with a multitude of other services. With the API, you can create, read, update, and delete records, manage lists, and automate tasks within your Workiom apps. By leveraging Pipedream's capabilities, you can integrate Workiom with hundreds of other apps to automate workflows, sync data across platforms, and trigger actions based on events from any connected service. -For example, with the Workiom API, you can: +# Example Use Cases -- Create custom applications to automate and streamline workflows -- Export and transform data into more actionable insights -- Access the powerful Workiom CRM, querying contacts, creating leads, and - tracking customer data. -- Integrate with third-party services, such as Social Media, Email, and Surveys -- Securely manage collaboration, sharing, and access permissions -- Track time and project progress -- Utilize Artificial Intelligence (AI) to assist in decision making and - planning -- Allow users to collaborate on documents and databases in real-time -- Integrate with billing and invoicing systems to create a complete financial - workflow. +- **Project Management Automation**: Automate project setup by triggering a workflow on Pipedream when a new project is created in Workiom. The workflow can create corresponding tasks in a project management tool like Trello or Asana, set up a Slack channel for project communication, and add events to a Google Calendar. + +- **Customer Onboarding**: Enhance your customer onboarding experience by triggering a Pipedream workflow when a new customer is added to Workiom. This workflow can send a welcome email via SendGrid, grant access to a scheduling app like Calendly, and initiate a series of educational drip emails in Mailchimp to guide the customer through your product or service. + +- **Inventory Tracking**: Keep your inventory in sync by using a Pipedream workflow to monitor changes in inventory levels within Workiom. When a product's stock dips below a certain threshold, the workflow can reorder stock by creating a purchase order in an app like QuickBooks, notify the responsible team member via a message in Microsoft Teams, and update a dashboard in Google Sheets with current inventory status. diff --git a/components/workiz/README.md b/components/workiz/README.md new file mode 100644 index 0000000000000..bb102c6ba7d5e --- /dev/null +++ b/components/workiz/README.md @@ -0,0 +1,11 @@ +# Overview + +The Workiz API opens up a world of possibilities for managing field service operations. By tapping into Workiz with Pipedream, you can automate workflows, sync data across apps, and react to events in real-time. Whether you're automating job scheduling, dispatch notifications, or customer follow-ups, integrating Workiz with Pipedream allows you to streamline operations, reduce manual entry, and keep your team in sync. + +# Example Use Cases + +- **Job Status Webhook Trigger**: When a job status updates in Workiz, trigger a Pipedream workflow to send a custom email or SMS to the customer. Combine this with a service like SendGrid or Twilio on Pipedream for seamless communication. + +- **Dynamic Scheduling Assistant**: Integrate Workiz with Google Calendar via Pipedream to automatically schedule jobs and update both systems in real-time. Whenever a new job is created in Workiz, a corresponding event can be added to a Google Calendar, making sure your team never misses an appointment. + +- **Inventory Management**: Use Workiz's API to monitor inventory levels within Pipedream. If stock drops below a certain threshold, automatically generate a purchase order and send it to your supplier through an app like QuickBooks or send a notification to the responsible manager via Slack. diff --git a/components/worksnaps/README.md b/components/worksnaps/README.md index 0c8a1d06ce716..2430f2c19eaa4 100644 --- a/components/worksnaps/README.md +++ b/components/worksnaps/README.md @@ -1,24 +1,11 @@ # Overview -Using the Worksnaps API, you can build applications to help teams better -understand and track their work. These applications can provide insight into -how to improve project management, increase employee engagement and -productivity, and evaluate productivity goals. +Worksnaps is a time-tracking service designed for remote work that offers detailed project and user activity insights. With Pipedream, you can automate actions based on time-tracking data, trigger workflows with specific user activities, or sync time logs with other management tools. Pipedream's serverless platform allows you to connect the Worksnaps API with countless apps to streamline project management, payroll processing, and productivity analysis. -The Worksnaps API allows you to build a range of applications, including: +# Example Use Cases -- Timesheet Tracking and Management System: Track employee time and project - progress in real-time, view reports on employee and team performance, - generate activity reports and invoices. -- Employee Engagement and Performance Tracking: Track employee performance and - engagement, analyze how individual and team performance is affected by events - and changes in the environment, and provide feedback and motivation to - employees. -- Automated Task and Project Management: Create custom workflows that are - optimized for team performance and client satisfaction, manage team task - lists and deadlines, organize projects in a shared workspace and assign tasks - to individual employees. -- Analytics and Insights: Generate interactive insights into team and employee - performance, track projects and tasks over time, discover patterns in project - progress, analyze trends in employee engagement and productivity, and create - custom reports to identify performance issues and opportunities. +- **Project Time Tracking to Google Sheets**: Automate the export of detailed time logs from Worksnaps to a Google Sheets spreadsheet for real-time project tracking and reporting. This workflow triggers whenever new time entries are logged, keeping all stakeholders updated with the latest data. + +- **Slack Alerts for Project Milestones**: Set up notifications in a Slack channel when team members reach certain milestones or when specific projects accumulate a certain number of hours. This workflow helps in celebrating team achievements and keeping the momentum going. + +- **Invoice Generation with QuickBooks**: Create invoices in QuickBooks based on time logged in Worksnaps. This workflow can be scheduled to run at the end of each billing period, ensuring that clients are invoiced accurately and promptly for the hours worked. diff --git a/components/workstack/README.md b/components/workstack/README.md index cfc8743827cf8..2546a0705ba22 100644 --- a/components/workstack/README.md +++ b/components/workstack/README.md @@ -1,17 +1,11 @@ # Overview -The Workstack API is an powerful set of tools for automating your workflows, -managing projects, and building custom applications. With its intuitive and -robust set of features and easy-to-use approach, the Workstack API can be used -to create many different types of projects and applications. +Workstack is a task and project management tool that helps teams organize their workloads efficiently. With its API, you can automate project and task creation, update timelines, and sync across multiple tools, ensuring your team stays on top of their deliverables. The Workstack API on Pipedream opens up possibilities to connect with hundreds of other apps, triggering actions in Workstack or vice versa, based on events from other tools, leading to a more integrated workflow and improved productivity. -Some of the different types of projects you can build with the Workstack API -include: +# Example Use Cases -- Automation tools to streamline your tasks and processes. -- Project management platforms to coordinate teams and collaborate on tasks. -- Custom applications to meet your business’s unique needs. -- Tracking platforms to monitor progress and productivity. -- Scheduling tools to plan projects and resources. -- Dashboards and reporting systems to generate meaningful insights. -- Integration tools for connecting external systems. +- **Project Creation Automation**: Automatically create a new project in Workstack when a deal is won in a CRM like Salesforce. This workflow can include setting up initial tasks, assigning team members, and defining deadlines, ensuring that new client projects are set up for success from the start. + +- **Task Synchronization with Google Calendar**: Sync Workstack tasks with a Google Calendar to provide visibility into deadlines and ensure that time is blocked off for critical project tasks. Whenever a task's due date or details are updated in Workstack, the corresponding Google Calendar event is automatically updated. + +- **Resource Allocation Updates from Slack Commands**: Use Slack commands to update task assignments and resource allocations in Workstack. For instance, a team lead can type a command in Slack to reassign a task to a different team member in Workstack, streamlining the process of task management without needing to switch between apps. diff --git a/components/workstack/package.json b/components/workstack/package.json new file mode 100644 index 0000000000000..a0f2b0fe9ec55 --- /dev/null +++ b/components/workstack/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/workstack", + "version": "0.6.0", + "description": "Pipedream workstack Components", + "main": "workstack.app.mjs", + "keywords": [ + "pipedream", + "workstack" + ], + "homepage": "https://pipedream.com/apps/workstack", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/world_news_api/README.md b/components/world_news_api/README.md new file mode 100644 index 0000000000000..604c9767f38cc --- /dev/null +++ b/components/world_news_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The World News API provides access to news articles from around the world in real-time. With it, you can fetch the latest news by category, country, language, and keyword. In Pipedream, this powerful API integrates into workflows, allowing you to automate actions based on current events, monitor specific topics, and even enrich your applications with timely content. + +# Example Use Cases + +- **News Alert System**: Trigger a workflow on Pipedream with new articles matching specific keywords. Use this to send alerts through email, SMS, or chat applications like Slack whenever news emerges on your topics of interest. + +- **Content Aggregator**: Collect and aggregate news from various countries and categories, then store them in a Pipedream data store. Periodically, you could run a workflow that pushes this aggregated content to a web service or a CMS like WordPress. + +- **Sentiment Analysis**: Conduct sentiment analysis on news headlines or content fetched by the World News API. Connect to a sentiment analysis service like MonkeyLearn within a Pipedream workflow to gauge public sentiment on the latest news stories. diff --git a/components/woxo/README.md b/components/woxo/README.md index d75ec18e85e36..c9911909c35a8 100644 --- a/components/woxo/README.md +++ b/components/woxo/README.md @@ -1,34 +1,11 @@ # Overview -WOXO is an easy-to-use API that enables developers to build amazing mobile -applications quickly and efficiently. It can be used to create everything from -mobile shopping applications to mobile gaming and interactive experiences. With -its advanced technology and customization capabilities, developers can bring -powerful ideas to life quickly and easily. +The WOXO API enables automation of social media content creation and management, transforming spreadsheets into social media posts, widgets, and more. With this API, you can dynamically generate marketing content, schedule posts, and update widgets in real-time, all based on data you control in a spreadsheet. This marries data-driven decision-making with social media management, offering a highly efficient way to engage with your audience. -Here is a list of what you can build with the WOXO API: +# Example Use Cases -- Shopping Applications - Create intuitive shopping experiences for customers - to browse, select and buy items easily. -- Social Networking Applications - Create a platform for users to share - content, interact and stay connected. -- Gaming Applications - Create captivating gaming experiences with exciting - visuals and user interaction. -- Virtual Assistance Applications - Create virtual assistants that can help - users complete tasks quickly and easily. -- Event Planning Applications - Create mobile platforms that help users find, - create and plan events. -- Messaging Applications - Create secure messaging experiences that enable - users to communicate easily. -- Gambling Applications - Create applications that enable users to gamble - without having to visit a casino. -- Education Applications - Create user-friendly educational experiences that - help enhance learning. -- Travel Applications - Create applications that enable users to quickly and - easily find and book flights and hotels. -- Payment Applications - Create secure, easy-to-use payment processing - applications that enable users to make payments anywhere. -- Health and Fitness Applications - Create applications that enable users to - easily track and manage their health and fitness goals. -- Analytics Applications - Create powerful analytics applications that enable - users to better understand and manage their data. +- **Automated Social Media Campaigns from Spreadsheet Data**: Create a Pipedream workflow that monitors a Google Sheets document for changes. Whenever new data is added, the WOXO API generates and schedules social media posts across multiple platforms, ensuring your campaigns stay fresh and relevant without manual intervention. + +- **Real-time Social Media Metrics Dashboard**: Set up a workflow where the WOXO API fetches your latest social media performance metrics and updates a dashboard widget on your website. Combine this with a cron-triggered Pipedream workflow to refresh the data at regular intervals, giving you and your audience up-to-date insights. + +- **Dynamic Content Widgets for E-commerce**: Connect WOXO with your e-commerce platform's API, like Shopify, to turn new product listings into attractive web widgets. When you add products to your Shopify store, Pipedream triggers the WOXO API to create a widget showcasing the latest additions, which can then be embedded on various online channels. diff --git a/components/wp_maps/README.md b/components/wp_maps/README.md new file mode 100644 index 0000000000000..6e603fcbb0ad1 --- /dev/null +++ b/components/wp_maps/README.md @@ -0,0 +1,11 @@ +# Overview + +The WP Maps API is a tool that enables you to integrate interactive maps into WordPress sites. You can use it to display custom markers, layers, and various map providers within your WordPress pages. With Pipedream, you can create serverless workflows that leverage the WP Maps API to automate map-related tasks. You could, for example, synchronize map data with external databases, send notifications when a new marker is added, or dynamically update maps based on user interactions or other triggers. + +# Example Use Cases + +- **Sync Map Markers with CRM**: Automate the process of updating your WP Maps with new locations as they are added to your Customer Relationship Management (CRM) system. Each time a new contact is added to the CRM, a workflow could be triggered that adds a marker to a map on your WordPress site, representing the contact's location. + +- **Send Alerts for New Map Interactions**: Whenever a user adds a comment or rating to a marker on your WP Maps, trigger a workflow that sends a notification to a Slack channel or an email to the admin. This allows for quick moderation or acknowledgment of user interactions. + +- **Automate Content Creation Based on Map Activity**: Generate new posts or update existing content on your WordPress site in response to changes in map data. For instance, if a certain number of users visit a marker or a new area is highlighted on the map, a workflow could create a post detailing popular locations or events in the area. diff --git a/components/wpforms/README.md b/components/wpforms/README.md new file mode 100644 index 0000000000000..cd0f66a723fe5 --- /dev/null +++ b/components/wpforms/README.md @@ -0,0 +1,11 @@ +# Overview + +The WPForms API allows users to interact with WPForms data programmatically. With Pipedream, you can harness this API to automate tasks like managing form entries, updating forms, and triggering workflows based on form submissions. This connectivity opens up a wealth of possibilities for integrating with other services, like CRMs, email marketing tools, and databases, to streamline your processes and make data work for you. + +# Example Use Cases + +- **Instant Slack Notifications for New WPForms Submissions**: Trigger a workflow in Pipedream when a new form submission is received in WPForms. The workflow can then send a message to a designated Slack channel with the submission details, ensuring that your team promptly receives the information. + +- **Automate Contact Creation in Salesforce from WPForms Entries**: Set up a workflow where each new WPForms submission is automatically added as a new contact in Salesforce. This eliminates the need for manual data entry and keeps your customer information synchronized across your marketing and sales platforms. + +- **Sync WPForms Submissions to Google Sheets for Reporting**: Every time a form is submitted via WPForms, trigger a Pipedream workflow to append the submission data to a Google Sheet. This can be useful for creating real-time reports, data backups, or sharing form data with team members who prefer to work within spreadsheets. diff --git a/components/wpforms/package.json b/components/wpforms/package.json index 6552ea87a9b94..feebe4e7acdb3 100644 --- a/components/wpforms/package.json +++ b/components/wpforms/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/wrike/README.md b/components/wrike/README.md index 309e122f2774a..361216316ee1c 100644 --- a/components/wrike/README.md +++ b/components/wrike/README.md @@ -1,19 +1,11 @@ # Overview -The Wrike API is an easy-to-use Application Programming Interface that allows -users to build custom apps and other solutions to integrate with the Wrike -platform. With the Wrike API, developers can access and leverage Wrike features -and data to create a wide range of applications and integrations. +Wrike API on Pipedream exposes the robust task and project management features of Wrike, enabling you to automate workflows across various apps and services. With it, you can programmatically access and manipulate tasks, folders, projects, and users within Wrike. This API lends itself to a multitude of automation possibilities, like synchronizing project updates across platforms, streamlining notifications and reporting, or even managing resource allocation based on project workload. -The possibilities are almost endless when using the Wrike API. Here are some -examples: +# Example Use Cases -- Automatically sync your CRM with Wrike -- Build a custom time-tracking integration to show project- Or task-level - metrics -- Create an AI-powered automation tool to route tasks to specific users -- Develop a project and task management app for internal teams or external - clients -- Generate custom reports to keep an eye on project progress -- Create a custom search engine and analysis tool to monitor user behavior -- Develop custom tools to automate and streamline task allocation +- **Project Status Updates to Slack**: Automatically send project status updates from Wrike to a designated Slack channel. When a Wrike task reaches a certain status, trigger a workflow that posts a message in Slack, keeping your team informed and aligned without manual intervention. + +- **Sync Wrike Tasks with Google Calendar**: Create a workflow that adds new Wrike tasks to Google Calendar as events. When a new task is created in Wrike, Pipedream can capture that event, and use the Google Calendar API to schedule it, ensuring your schedule is always up-to-date with your task list. + +- **GitHub Issues to Wrike Tasks**: Convert new GitHub issues into Wrike tasks to streamline bug tracking and feature requests. When a new issue is reported on GitHub, trigger a Pipedream workflow that creates a corresponding task in Wrike, tagged and categorized for your dev team to tackle. diff --git a/components/wrike/actions/new-task/new-task.mjs b/components/wrike/actions/new-task/new-task.mjs index d18320fe19359..6d508001f408c 100644 --- a/components/wrike/actions/new-task/new-task.mjs +++ b/components/wrike/actions/new-task/new-task.mjs @@ -4,8 +4,8 @@ import _ from "lodash"; export default { key: "wrike-new-task", name: "New Task", - description: "Create a Wrike task under a specified folder ID. [See the docs](https://developers.wrike.com/api/v4/tasks/#create-task)", - version: "0.3.0", + description: "Create a Wrike task under a specified folder ID. [See the documentation](https://developers.wrike.com/api/v4/tasks/#create-task)", + version: "0.3.1", type: "action", props: { wrike, diff --git a/components/wrike/actions/update-task-custom-fields/update-task-custom-fields.mjs b/components/wrike/actions/update-task-custom-fields/update-task-custom-fields.mjs index 87805559f7b41..c3937fcca7fd1 100644 --- a/components/wrike/actions/update-task-custom-fields/update-task-custom-fields.mjs +++ b/components/wrike/actions/update-task-custom-fields/update-task-custom-fields.mjs @@ -4,8 +4,8 @@ import { ConfigurationError } from "@pipedream/platform"; export default { key: "wrike-update-task-custom-fields", name: "Update Task Custom Fields", - description: "Update the custom fields for a task. [See the docs](https://developers.wrike.com/api/v4/tasks/#modify-tasks)", - version: "0.0.1", + description: "Update the custom fields for a task. [See the documentation](https://developers.wrike.com/api/v4/tasks/#modify-tasks)", + version: "0.0.2", type: "action", props: { wrike, diff --git a/components/wrike/package.json b/components/wrike/package.json index 2a48749d28e6e..e361eb45ae766 100644 --- a/components/wrike/package.json +++ b/components/wrike/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/wrike", - "version": "0.0.2", + "version": "0.0.3", "description": "Pipedream Wrike Components", "main": "wrike.app.mjs", "keywords": [ @@ -14,7 +14,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.3.0", + "@pipedream/platform": "^3.0.3", "lodash": "^4.17.21" } } diff --git a/components/wrike/sources/new-folder-created/new-folder-created.mjs b/components/wrike/sources/new-folder-created/new-folder-created.mjs index 70259569cb16d..886efed7dfd76 100644 --- a/components/wrike/sources/new-folder-created/new-folder-created.mjs +++ b/components/wrike/sources/new-folder-created/new-folder-created.mjs @@ -7,7 +7,7 @@ export default { name: "New Folder Created", description: "Emit new event when a folder is created", type: "source", - version: "0.0.1", + version: "0.0.2", props: { ...base.props, folderId: { @@ -17,7 +17,6 @@ export default { ], description: "Receive notifications for folders in a folder and, optionally, in its subfolders. Leave blank to receive notifications for all folders in the account", optional: true, - reloadProps: true, }, spaceId: { propDefinition: [ @@ -26,7 +25,6 @@ export default { ], description: "Receive notifications for changes to folders within a space. Leave blank to receive notifications for all folders in the account", optional: true, - reloadProps: true, }, recursive: { type: "boolean", diff --git a/components/wrike/sources/new-subtask-created/new-subtask-created.mjs b/components/wrike/sources/new-subtask-created/new-subtask-created.mjs index 1da225a321cd8..0c5a6291fa042 100644 --- a/components/wrike/sources/new-subtask-created/new-subtask-created.mjs +++ b/components/wrike/sources/new-subtask-created/new-subtask-created.mjs @@ -7,28 +7,32 @@ export default { name: "New Subtask Created", description: "Emit new event when a subtask is created", type: "source", - version: "0.0.1", + version: "0.0.2", props: { ...base.props, - taskId: { + taskIds: { propDefinition: [ base.props.wrike, "taskId", - (c) => ({ - folderId: c.folderId, - spaceId: c.spaceId, - }), ], - description: "Receive notifications for subtasks of a task", + type: "string[]", + label: "Task IDs", + description: "Receive notifications for subtasks of the specified task(s)", + optional: true, }, }, hooks: { ...base.hooks, async deploy() { console.log("Retrieving historical events..."); - const subtasks = await this.wrike.getSubtasks({ - taskId: this.taskId, - }); + const taskIds = this.taskIds || (await this.wrike.listTasks({}))?.map(({ id }) => id) || []; + const subtasks = []; + for (const taskId of taskIds) { + const taskSubtasks = await this.wrike.getSubtasks({ + taskId, + }); + subtasks.push(...taskSubtasks); + } for (const subtask of subtasks.slice(-constants.DEPLOY_LIMIT)) { this.emitEvent(subtask); } @@ -55,7 +59,7 @@ export default { const task = await this.wrike.getTask({ taskId: data.taskId, }); - if (task.superTaskIds.includes(this.taskId)) { + if (!this.taskIds || task.superTaskIds.some((id) => this.taskIds.includes(id))) { this.emitEvent(task); } } diff --git a/components/wrike/sources/new-task-created/new-task-created.mjs b/components/wrike/sources/new-task-created/new-task-created.mjs index b9c2416e36457..eb14112ef97ea 100644 --- a/components/wrike/sources/new-task-created/new-task-created.mjs +++ b/components/wrike/sources/new-task-created/new-task-created.mjs @@ -7,7 +7,7 @@ export default { name: "New Task Created", description: "Emit new event when a task is created", type: "source", - version: "0.0.1", + version: "0.0.2", props: { ...base.props, folderId: { @@ -17,7 +17,6 @@ export default { ], description: "Receive notifications for tasks in a folder and, optionally, in its subfolders. Leave blank to receive notifications for all tasks in the account", optional: true, - reloadProps: true, }, spaceId: { propDefinition: [ @@ -26,7 +25,6 @@ export default { ], description: "Receive notifications for changes to tasks, folders, and projects within a space. Leave blank to receive notifications for all tasks in the account", optional: true, - reloadProps: true, }, recursive: { type: "boolean", diff --git a/components/wrike/wrike.app.mjs b/components/wrike/wrike.app.mjs index 05f773aa3a8bc..557475dcca6e0 100644 --- a/components/wrike/wrike.app.mjs +++ b/components/wrike/wrike.app.mjs @@ -72,7 +72,7 @@ export default { }, methods: { _baseUrl() { - return "https://www.wrike.com/api/v4"; + return `https://${this.$auth.host}/api/v4`; }, _buildPath({ basePath, folderId, spaceId, diff --git a/components/writer/.gitignore b/components/writer/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/writer/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/writer/README.md b/components/writer/README.md new file mode 100644 index 0000000000000..38938e9f523f5 --- /dev/null +++ b/components/writer/README.md @@ -0,0 +1,11 @@ +# Overview + +Writer API lets you automate content creation, ensuring your text adheres to certain style guides and writing rules. With Pipedream, you can build workflows integrating Writer to check grammar, enhance clarity, and maintain brand voice across various platforms. By connecting Writer with numerous apps supported by Pipedream, you can streamline content workflows, enforce consistency, and save time on content editing and approval processes. + +# Example Use Cases + +- **Content Quality Control for Blog Posts**: Automate the proofreading process for blog posts. When a new post is added to your CMS (like WordPress), trigger a Pipedream workflow that sends the content to Writer API. The API can analyze writing quality, suggest improvements, and enforce style guidelines. Revised content can be sent back for review or automatically updated in the CMS. + +- **Email Campaign Review**: Before sending out an email campaign via an email service like Mailchimp, use Writer API in a Pipedream workflow to check the email copy for brand voice and clarity. After the review, the workflow can either flag emails for manual revision or approve them for distribution, streamlining your email marketing process. + +- **Social Media Content Consistency**: Create a workflow where social media posts from a scheduling tool like Buffer are vetted through Writer API before going live. This ensures all posts meet your brand's writing standards. If the content doesn't align, the workflow can alert the team for manual intervention or suggest automatic edits, maintaining a cohesive brand presence online. diff --git a/components/writer/actions/send-prompt/send-prompt.mjs b/components/writer/actions/send-prompt/send-prompt.mjs new file mode 100644 index 0000000000000..80aa55d83a000 --- /dev/null +++ b/components/writer/actions/send-prompt/send-prompt.mjs @@ -0,0 +1,50 @@ +import app from "../../writer.app.mjs"; + +export default { + key: "writer-send-prompt", + name: "Send Prompt", + description: "Generate a chat completion based on the provided messages. [See the documentation](https://dev.writer.com/api-guides/api-reference/completion-api/chat-completion)", + version: "0.0.1", + type: "action", + props: { + app, + messages: { + propDefinition: [ + app, + "messages", + ], + }, + logprobs: { + propDefinition: [ + app, + "logprobs", + ], + }, + maxTokens: { + propDefinition: [ + app, + "maxTokens", + ], + }, + number: { + propDefinition: [ + app, + "number", + ], + }, + }, + async run({ $ }) { + const response = await this.app.sendPrompt({ + $, + data: { + model: "palmyra-x-004", + messages: JSON.parse(this.messages), + logprobs: this.logprobs, + max_tokens: this.maxTokens, + n: this.number, + }, + }); + $.export("$summary", "Successfully generated chat completion"); + return response; + }, +}; diff --git a/components/writer/app/writer.app.ts b/components/writer/app/writer.app.ts deleted file mode 100644 index ec1a7ad0a19b4..0000000000000 --- a/components/writer/app/writer.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "writer", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/writer/package.json b/components/writer/package.json index 249993b0f78f3..59d4f49f3d0a2 100644 --- a/components/writer/package.json +++ b/components/writer/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/writer", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream Writer Components", - "main": "dist/app/writer.app.mjs", + "main": "writer.app.mjs", "keywords": [ "pipedream", "writer" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/writer", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/writer/writer.app.mjs b/components/writer/writer.app.mjs new file mode 100644 index 0000000000000..25173c9091a92 --- /dev/null +++ b/components/writer/writer.app.mjs @@ -0,0 +1,61 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "writer", + propDefinitions: { + messages: { + type: "string", + label: "Messages", + description: "Array of messages to be used for generating completion. Use the format: `[{ \"role\": \"user\", \"content\": \"Hello\" }]`. The possible roles are `user`, `asssistant`, 'system' and 'tool'", + }, + logprobs: { + type: "boolean", + label: "Log Probabilities", + description: "Specifies whether to return log probabilities of the output tokens", + optional: true, + }, + maxTokens: { + type: "integer", + label: "Max Tokens", + description: "Defines the maximum number of tokens that the model can generate in the response", + optional: true, + }, + number: { + type: "integer", + label: "Number", + description: "Specifies the number of completions to generate", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.writer.com/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + "Authorization": `Bearer ${this.$auth.api_key}`, + "Content-Type": "application/json", + ...headers, + }, + }); + }, + + async sendPrompt(args = {}) { + return this._makeRequest({ + path: "/chat", + method: "post", + ...args, + }); + }, + }, +}; diff --git a/components/writesonic/README.md b/components/writesonic/README.md new file mode 100644 index 0000000000000..c053f20a172bc --- /dev/null +++ b/components/writesonic/README.md @@ -0,0 +1,11 @@ +# Overview + +The Writesonic API taps into the prowess of AI to craft compelling written content, from blog posts to marketing copy. By integrating this API with Pipedream, you can automate content creation, enrich your applications with dynamic text, and streamline various writing tasks. It’s perfect for when you need high-quality writing done swiftly and at scale. + +# Example Use Cases + +- **Automated Blog Post Generation**: Trigger a workflow on Pipedream to use Writesonic API to generate articles. Once a week, you might have a workflow that fetches trending keywords from Google Trends using a pre-built Pipedream component, then sends these to Writesonic to create a fresh blog post, which could then be automatically posted to your CMS like WordPress. + +- **Dynamic Product Descriptions**: If you run an e-commerce site with a regularly changing inventory, a Pipedream workflow could use Writesonic to generate unique product descriptions. Hook into your Shopify or WooCommerce product addition event, send product details to Writesonic, and get attractive descriptions that boost your SEO and conversion rates. + +- **Email Campaign Content Creation**: For those running frequent email campaigns, use Pipedream to integrate Writesonic with your email service provider, like Mailchimp. When you're ready to send a new campaign, trigger a workflow that generates compelling copy based on your campaign theme or product offering, then automatically populate and send emails through your chosen platform. diff --git a/components/wubook_ratechecker/README.md b/components/wubook_ratechecker/README.md new file mode 100644 index 0000000000000..05790750af339 --- /dev/null +++ b/components/wubook_ratechecker/README.md @@ -0,0 +1,11 @@ +# Overview + +The WuBook RateChecker API allows users to retrieve detailed pricing information and rates for various hotels. This powerful tool can be integrated into Pipedream workflows to create automated processes for monitoring hotel prices, analyzing market trends, or updating pricing strategies. With Pipedream's capabilities, you can trigger actions based on the data fetched from WuBook, connect to various apps, and build complex workflows without managing servers or infrastructure. + +# Example Use Cases + +- **Monitor Competitor Pricing**: Set up a Pipedream workflow that regularly checks the rates of competitor hotels and sends alerts via email or Slack when there's a significant change. This can help in staying competitive in the market. + +- **Dynamic Pricing Strategy**: Create a workflow where WuBook RateChecker API data influences the pricing of your own hotel listings on platforms like Airbnb or Booking.com. Utilize other Pipedream integrations to adjust prices automatically based on market demand. + +- **Market Analysis Reports**: Generate regular market analysis reports by collecting rate data over time. Use the WuBook RateChecker API to fetch rates, then store the data in a Google Sheets document for easy visualization and sharing with stakeholders. diff --git a/components/wuf/package.json b/components/wuf/package.json index 0f91d316502fe..3da06d97da505 100644 --- a/components/wuf/package.json +++ b/components/wuf/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/wufoo/README.md b/components/wufoo/README.md new file mode 100644 index 0000000000000..6283c6e2560a0 --- /dev/null +++ b/components/wufoo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Wufoo API allows you to interact with the Wufoo platform, where you can create custom online forms for data collection and surveys. With this API on Pipedream, you can automate form submissions, retrieve form data, and integrate with other services to streamline workflows. Use cases include synchronizing form entries to a database, triggering email notifications upon form submission, and analyzing survey responses for insights. + +# Example Use Cases + +- **Synchronize Form Submissions to Google Sheets**: When a new form submission is received in Wufoo, the workflow triggers an action to append the submission data to a Google Sheets document. This allows for real-time data collection and analysis, keeping your spreadsheets automatically updated with the latest information. + +- **Automated Slack Notifications for New Entries**: Set up a workflow that sends a message to a designated Slack channel whenever a new Wufoo form submission comes in. This can help teams immediately engage with new leads, customer inquiries, or feedback without manual oversight. + +- **Email Acknowledgement After Submission**: Craft a workflow where, after a form is submitted on Wufoo, an email is automatically sent to the submitter via SendGrid, thanking them for their input and providing them with a summary of their submission or additional steps if necessary. diff --git a/components/x_ai/actions/create-embeddings/create-embeddings.mjs b/components/x_ai/actions/create-embeddings/create-embeddings.mjs new file mode 100644 index 0000000000000..0090c3e80c394 --- /dev/null +++ b/components/x_ai/actions/create-embeddings/create-embeddings.mjs @@ -0,0 +1,57 @@ +import app from "../../x_ai.app.mjs"; + +export default { + key: "x_ai-create-embeddings", + name: "Create Embedding", + description: "Create an embedding vector representation corresponding to the input text. [See the documentation](https://docs.x.ai/api/endpoints#create-embeddings)", + version: "0.0.2", + type: "action", + props: { + app, + embeddingModel: { + propDefinition: [ + app, + "embeddingModel", + ], + }, + input: { + propDefinition: [ + app, + "input", + ], + }, + dimensions: { + propDefinition: [ + app, + "dimensions", + ], + }, + encodingFormat: { + propDefinition: [ + app, + "encodingFormat", + ], + }, + user: { + propDefinition: [ + app, + "user", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.createEmbeddings({ + $, + data: { + dimensions: this.dimensions, + encoding_format: this.encodingFormat, + input: this.input, + model: this.embeddingModel, + user: this.user, + }, + }); + $.export("$summary", "Successfully created embedding"); + return response; + }, +}; diff --git a/components/x_ai/actions/get-model/get-model.mjs b/components/x_ai/actions/get-model/get-model.mjs new file mode 100644 index 0000000000000..10655a84d0189 --- /dev/null +++ b/components/x_ai/actions/get-model/get-model.mjs @@ -0,0 +1,27 @@ +import app from "../../x_ai.app.mjs"; + +export default { + key: "x_ai-get-model", + name: "Get Model", + description: "List all language and embedding models available. [See the documentation](https://docs.x.ai/api/endpoints#get-model)", + version: "0.0.2", + type: "action", + props: { + app, + model: { + propDefinition: [ + app, + "model", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.getModel({ + $, + model: this.model, + }); + $.export("$summary", `Successfully retrieved the '${this.model}' model`); + return response; + }, +}; diff --git a/components/x_ai/actions/post-chat-completion/post-chat-completion.mjs b/components/x_ai/actions/post-chat-completion/post-chat-completion.mjs new file mode 100644 index 0000000000000..a9ed57210752d --- /dev/null +++ b/components/x_ai/actions/post-chat-completion/post-chat-completion.mjs @@ -0,0 +1,111 @@ +import app from "../../x_ai.app.mjs"; + +export default { + key: "x_ai-post-chat-completion", + name: "Post Chat Completion", + description: "Create a language model response for a chat conversation. [See the documentation](https://docs.x.ai/api/endpoints#chat-completions)", + version: "0.0.2", + type: "action", + props: { + app, + model: { + propDefinition: [ + app, + "model", + ], + }, + message: { + propDefinition: [ + app, + "message", + ], + }, + frequencyPenalty: { + propDefinition: [ + app, + "frequencyPenalty", + ], + }, + logprobs: { + propDefinition: [ + app, + "logprobs", + ], + }, + maxTokens: { + propDefinition: [ + app, + "maxTokens", + ], + }, + n: { + propDefinition: [ + app, + "n", + ], + }, + presencePenalty: { + propDefinition: [ + app, + "presencePenalty", + ], + }, + seed: { + propDefinition: [ + app, + "seed", + ], + }, + stream: { + propDefinition: [ + app, + "stream", + ], + }, + temperature: { + propDefinition: [ + app, + "temperature", + ], + }, + topP: { + propDefinition: [ + app, + "topP", + ], + }, + user: { + propDefinition: [ + app, + "user", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.postChatCompletion({ + $, + data: { + model: this.model, + messages: [ + { + role: "user", + content: this.message, + }, + ], + frequency_penalty: Number(this.frequencyPenalty), + logprobs: this.logprobs, + max_tokens: this.maxTokens, + n: this.n, + presence_penalty: Number(this.presencePenalty), + seed: this.seed, + stream: this.stream, + temperature: Number(this.temperature), + top_p: Number(this.topP), + user: this.user, + }, + }); + $.export("$summary", `Successfully sent message to the model '${this.model}'`); + return response; + }, +}; diff --git a/components/x_ai/actions/post-completion/post-completion.mjs b/components/x_ai/actions/post-completion/post-completion.mjs new file mode 100644 index 0000000000000..e6ae9fdda99b2 --- /dev/null +++ b/components/x_ai/actions/post-completion/post-completion.mjs @@ -0,0 +1,120 @@ +import app from "../../x_ai.app.mjs"; + +export default { + key: "x_ai-post-completion", + name: "Post Completion", + description: "Create a language model response for a given prompt. [See the documentation](https://docs.x.ai/api/endpoints#completions)", + version: "0.0.2", + type: "action", + props: { + app, + model: { + propDefinition: [ + app, + "model", + ], + }, + prompt: { + propDefinition: [ + app, + "prompt", + ], + }, + echo: { + propDefinition: [ + app, + "echo", + ], + }, + frequencyPenalty: { + propDefinition: [ + app, + "frequencyPenalty", + ], + }, + logprobs: { + propDefinition: [ + app, + "logprobs", + ], + }, + maxTokens: { + propDefinition: [ + app, + "maxTokens", + ], + }, + n: { + propDefinition: [ + app, + "n", + ], + }, + presencePenalty: { + propDefinition: [ + app, + "presencePenalty", + ], + }, + seed: { + propDefinition: [ + app, + "seed", + ], + }, + stream: { + propDefinition: [ + app, + "stream", + ], + }, + suffix: { + propDefinition: [ + app, + "suffix", + ], + }, + temperature: { + propDefinition: [ + app, + "temperature", + ], + }, + topP: { + propDefinition: [ + app, + "topP", + ], + }, + user: { + propDefinition: [ + app, + "user", + ], + }, + }, + + async run({ $ }) { + const response = await this.app.postCompletion({ + $, + data: { + model: this.model, + prompt: this.prompt, + echo: this.echo, + frequency_penalty: Number(this.frequencyPenalty), + logprobs: this.logprobs, + max_tokens: this.maxTokens, + n: this.n, + presence_penalty: Number(this.presencePenalty), + seed: this.seed, + stream: this.stream, + suffix: this.suffix, + temperature: Number(this.temperature), + top_p: Number(this.topP), + user: this.user, + }, + }); + $.export("$summary", `Successfully sent prompt to the model '${this.model}'`); + return response; + }, +}; diff --git a/components/x_ai/common/constants.mjs b/components/x_ai/common/constants.mjs new file mode 100644 index 0000000000000..ecde3a72419cb --- /dev/null +++ b/components/x_ai/common/constants.mjs @@ -0,0 +1,6 @@ +export default { + ENCODING_FORMATS: [ + "float", + "base64", + ], +}; diff --git a/components/x_ai/package.json b/components/x_ai/package.json new file mode 100644 index 0000000000000..f5bc02777b51e --- /dev/null +++ b/components/x_ai/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/x_ai", + "version": "0.1.1", + "description": "Pipedream Chat Data Components", + "main": "x_ai.app.mjs", + "keywords": [ + "pipedream", + "x_ai" + ], + "homepage": "https://pipedream.com/apps/x_ai", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/x_ai/x_ai.app.mjs b/components/x_ai/x_ai.app.mjs new file mode 100644 index 0000000000000..56a75ffe909b4 --- /dev/null +++ b/components/x_ai/x_ai.app.mjs @@ -0,0 +1,195 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "x_ai", + propDefinitions: { + model: { + type: "string", + label: "Model", + description: "ID of the embedding model to use", + async options() { + const response = await this.getModels(); + const modelsIds = response.data; + return modelsIds.map(({ id }) => ({ + value: id, + })); + }, + }, + embeddingModel: { + type: "string", + label: "Embedding Models", + description: "ID of the embedding model to use", + async options() { + const response = await this.getEmbeddingModels(); + const embeddingModelsIds = response.models; + return embeddingModelsIds.map(({ id }) => ({ + value: id, + })); + }, + }, + prompt: { + type: "string", + label: "Prompt", + description: "Prompt for the request", + }, + message: { + type: "string", + label: "Message", + description: "Message for the chat completion", + }, + echo: { + type: "boolean", + label: "Echo", + description: "Option to include the original prompt in the response along with the generated completion", + optional: true, + }, + frequencyPenalty: { + type: "string", + label: "Frequency Penalty", + 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", + optional: true, + }, + logprobs: { + type: "boolean", + label: "Log Probabilities", + description: "Include the log probabilities on the `logprobs` most likely output tokens, as well the chosen tokens", + optional: true, + }, + maxTokens: { + type: "integer", + label: "Max Tokens", + description: "Limits the number of tokens that can be produced in the output", + optional: true, + }, + n: { + type: "integer", + label: "Completion Number", + description: "Determines how many completion sequences to produce for each prompt. Be cautious with its use due to high token consumption", + optional: true, + }, + presencePenalty: { + type: "string", + label: "Presence Penalty", + 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 model's likelihood to talk about new topics", + optional: true, + }, + seed: { + type: "integer", + label: "Seed", + description: "If specified, our system will make a best effort to sample deterministically", + optional: true, + }, + stream: { + type: "boolean", + label: "Stream", + description: "Whether to stream back partial progress. If set, tokens will be sent as data-only server-sent events as they become available", + optional: true, + }, + suffix: { + type: "string", + label: "Suffix", + description: "Optional string to append after the generated text", + optional: true, + }, + temperature: { + type: "string", + label: "Temperature", + description: "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic", + optional: true, + }, + topP: { + type: "string", + label: "Nucleus Sampling", + 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", + optional: true, + }, + user: { + type: "string", + label: "User", + description: "A unique identifier representing your end-user, which can help xAI to monitor and detect abuse", + optional: true, + }, + dimensions: { + type: "integer", + label: "Dimensions", + description: "The number of dimensions the resulting output embeddings should have", + optional: true, + }, + encodingFormat: { + type: "string", + label: "Encoding Format", + description: "The format to return the embeddings in", + optional: true, + options: constants.ENCODING_FORMATS, + }, + input: { + type: "string[]", + label: "Input", + description: "Text input to be converted into an embedding", + }, + }, + methods: { + _baseUrl() { + return "https://api.x.ai"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + async postChatCompletion(args = {}) { + return this._makeRequest({ + path: "/v1/chat/completions", + method: "post", + ...args, + }); + }, + async postCompletion(args = {}) { + return this._makeRequest({ + path: "/v1/completions", + method: "post", + ...args, + }); + }, + async createEmbeddings(args = {}) { + return this._makeRequest({ + path: "/v1/embeddings", + method: "post", + ...args, + }); + }, + async getModel({ + model, ...args + }) { + return this._makeRequest({ + path: `/v1/models/${model}`, + ...args, + }); + }, + async getModels(args = {}) { + return this._makeRequest({ + path: "/v1/models", + ...args, + }); + }, + async getEmbeddingModels(args = {}) { + return this._makeRequest({ + path: "/v1/embedding-models", + ...args, + }); + }, + }, +}; diff --git a/components/xata/.gitignore b/components/xata/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/xata/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/xata/README.md b/components/xata/README.md new file mode 100644 index 0000000000000..d71e146220407 --- /dev/null +++ b/components/xata/README.md @@ -0,0 +1,11 @@ +# Overview + +The Xata API offers a serverless, scalable database service that supports building and deploying data-driven applications with ease. With its API-first approach, developers can perform CRUD operations on their databases, manage schemas, and leverage powerful search capabilities. In Pipedream, you can harness the Xata API to automate database operations, sync data across various platforms, and trigger workflows based on database events. + +# Example Use Cases + +- **Automated Data Backup**: Create a Pipedream workflow that periodically triggers to back up specific tables or the entire Xata database to a cloud storage service like Amazon S3 or Google Cloud Storage, ensuring data redundancy and safety. + +- **Real-time Data Sync with Third-party Services**: Craft a workflow where each new record in a Xata database table triggers a Pipedream action to create or update a corresponding contact in a CRM like Salesforce or HubSpot, keeping sales data aligned across platforms. + +- **Issue Tracking Integration**: Build a Pipedream workflow that connects Xata with GitHub. Whenever a new issue is created in GitHub, a corresponding record is added to a Xata database, allowing for enhanced issue tracking and project management. diff --git a/components/xata/actions/common/common.mjs b/components/xata/actions/common/common.mjs new file mode 100644 index 0000000000000..ee4e6a430dd27 --- /dev/null +++ b/components/xata/actions/common/common.mjs @@ -0,0 +1,108 @@ +import app from "../../xata.app.mjs"; + +export default { + props: { + app, + endpoint: { + propDefinition: [ + app, + "endpoint", + ], + }, + workspace: { + propDefinition: [ + app, + "workspace", + ], + }, + database: { + propDefinition: [ + app, + "database", + (c) => ({ + workspace: c.workspace, + }), + ], + }, + branch: { + propDefinition: [ + app, + "branch", + (c) => ({ + endpoint: c.endpoint, + database: c.database, + }), + ], + }, + table: { + propDefinition: [ + app, + "table", + (c) => ({ + endpoint: c.endpoint, + database: c.database, + branch: c.branch, + }), + ], + reloadProps: true, + }, + }, + async additionalProps(props) { + const { + endpoint, + database, + branch, + table, + } = this; + + const description = "The keys and values of the data that will be recorded in the database."; + + if (endpoint && database && branch && table) { + const { columns } = await this.app.listColumns({ + endpoint, + database, + branch, + table, + }); + if (columns?.length) { + let descriptionWithColumns = `${description} Available Columns:`; + for (const column of columns) { + descriptionWithColumns += ` \`${column.name}\``; + } + props.recordData.description = descriptionWithColumns; + return {}; + } + } + props.recordData.description = description; + return {}; + }, + methods: { + async formatRecordData() { + const recordData = this.recordData; + const { columns } = await this.app.listColumns({ + endpoint: this.endpoint, + database: this.database, + branch: this.branch, + table: this.table, + }); + if (!columns?.length) { + return this.recordData; + } + for (const column of columns) { + if (!recordData[column.name] || typeof recordData[column.name] !== "string") { + continue; + } + if ((column.type === "int" || column.type === "float")) { + recordData[column.name] = +recordData[column.name]; + } + if (column.type === "bool") { + recordData[column.name] = !(recordData[column.name] === "false" || recordData[column.name === "0"]); + } + if (column.type === "multiple" || column.type === "vector") { + recordData[column.name] = JSON.parse(recordData[column.name]); + } + } + return recordData; + }, + }, +}; diff --git a/components/xata/actions/create-record/create-record.mjs b/components/xata/actions/create-record/create-record.mjs new file mode 100644 index 0000000000000..e56cdbd7bd03b --- /dev/null +++ b/components/xata/actions/create-record/create-record.mjs @@ -0,0 +1,31 @@ +import common from "../common/common.mjs"; + +export default { + ...common, + key: "xata-create-record", + name: "Create Record", + description: "Create a new Record in the specified database. [See the documentation](https://xata.io/docs/api-reference/db/db_branch_name/tables/table_name/data#insert-record)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + recordData: { + propDefinition: [ + common.props.app, + "recordData", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createRecord({ + $, + endpoint: this.endpoint, + database: this.database, + branch: this.branch, + table: this.table, + data: await this.formatRecordData(), + }); + $.export("$summary", `Successfully created Record with ID: '${response.id}'`); + return response; + }, +}; diff --git a/components/xata/actions/list-branches/list-branches.mjs b/components/xata/actions/list-branches/list-branches.mjs new file mode 100644 index 0000000000000..38ff41d7a83ed --- /dev/null +++ b/components/xata/actions/list-branches/list-branches.mjs @@ -0,0 +1,42 @@ +import app from "../../xata.app.mjs"; + +export default { + key: "xata-list-branches", + name: "List Branches", + description: "List branches of the specified database. [See the documentation](https://xata.io/docs/api-reference/dbs/db_name#list-branches)", + version: "0.0.1", + type: "action", + props: { + app, + endpoint: { + propDefinition: [ + app, + "endpoint", + ], + }, + workspace: { + propDefinition: [ + app, + "workspace", + ], + }, + database: { + propDefinition: [ + app, + "database", + (c) => ({ + workspace: c.workspace, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.app.listBranches({ + $, + endpoint: this.endpoint, + database: this.database, + }); + $.export("$summary", `Successfully retrieved '${response.branches.length}' branches`); + return response; + }, +}; diff --git a/components/xata/actions/replace-record/replace-record.mjs b/components/xata/actions/replace-record/replace-record.mjs new file mode 100644 index 0000000000000..2f15fad5a2822 --- /dev/null +++ b/components/xata/actions/replace-record/replace-record.mjs @@ -0,0 +1,44 @@ +import common from "../common/common.mjs"; + +export default { + ...common, + key: "xata-replace-record", + name: "Replace Record", + description: "Replace a record with the specified ID. [See the documentation](https://xata.io/docs/api-reference/db/db_branch_name/tables/table_name/data/record_id#insert-record-with-id)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + recordId: { + propDefinition: [ + common.props.app, + "recordId", + (c) => ({ + endpoint: c.endpoint, + database: c.database, + table: c.table, + branch: c.branch, + }), + ], + }, + recordData: { + propDefinition: [ + common.props.app, + "recordData", + ], + }, + }, + async run({ $ }) { + const response = await this.app.replaceRecord({ + $, + endpoint: this.endpoint, + database: this.database, + branch: this.branch, + table: this.table, + recordId: this.recordId, + data: await this.formatRecordData(), + }); + $.export("$summary", `Successfully replaced Record with ID: '${response.id}'`); + return response; + }, +}; diff --git a/components/xata/actions/update-record/update-record.mjs b/components/xata/actions/update-record/update-record.mjs new file mode 100644 index 0000000000000..cff581a4eded7 --- /dev/null +++ b/components/xata/actions/update-record/update-record.mjs @@ -0,0 +1,44 @@ +import common from "../common/common.mjs"; + +export default { + ...common, + key: "xata-update-record", + name: "Update Record", + description: "Update or create a record with the specified ID. [See the documentation](https://xata.io/docs/api-reference/db/db_branch_name/tables/table_name/data/record_id#upsert-record-with-id)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + recordId: { + propDefinition: [ + common.props.app, + "recordId", + (c) => ({ + endpoint: c.endpoint, + database: c.database, + table: c.table, + branch: c.branch, + }), + ], + }, + recordData: { + propDefinition: [ + common.props.app, + "recordData", + ], + }, + }, + async run({ $ }) { + const response = await this.app.updateRecord({ + $, + endpoint: this.endpoint, + database: this.database, + branch: this.branch, + table: this.table, + recordId: this.recordId, + data: await this.formatRecordData(), + }); + $.export("$summary", `Successfully updated/created Record with ID: '${response.id}'`); + return response; + }, +}; diff --git a/components/xata/app/xata.app.ts b/components/xata/app/xata.app.ts deleted file mode 100644 index a73142b8024ce..0000000000000 --- a/components/xata/app/xata.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "xata", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); diff --git a/components/xata/package.json b/components/xata/package.json index 4c2a8b50f0adc..0d78426c25eb2 100644 --- a/components/xata/package.json +++ b/components/xata/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/xata", - "version": "0.0.2", - "description": "Pipedream Xata Components", - "main": "dist/app/xata.app.mjs", + "version": "0.1.0", + "description": "Pipedream xata Components", + "main": "xata.app.mjs", "keywords": [ "pipedream", "xata" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/xata", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.2" } } diff --git a/components/xata/xata.app.mjs b/components/xata/xata.app.mjs new file mode 100644 index 0000000000000..0713529ec5226 --- /dev/null +++ b/components/xata/xata.app.mjs @@ -0,0 +1,197 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "xata", + propDefinitions: { + recordId: { + type: "string", + label: "Record ID", + description: "ID of the record to create or update", + async options({ + endpoint, database, branch, table, + }) { + const response = await this.listRecords({ + endpoint, + database, + branch, + table, + }); + const recordIds = response.records; + return recordIds.map(({ id }) => ({ + value: id, + })); + }, + }, + table: { + type: "string", + label: "Table Name", + description: "Name of the table", + async options({ + endpoint, database, branch, + }) { + const { schema: { tables } } = await this.getBranchSchema({ + endpoint, + database, + branch, + }); + return tables?.map(({ name }) => name ) || []; + }, + }, + endpoint: { + type: "string", + label: "HTTP Endpoint", + description: "The endpoint of your database, i.e.: `https://my-workspace-123456.us-east-1.xata.sh`. You can find your workspace domain by navigating to the Configuration tab in the Xata Web UI", + }, + recordData: { + type: "object", + label: "Record Data", + description: "The keys and values of the data that will be recorded in the database", + }, + workspace: { + type: "string", + label: "Workspace ID", + description: "ID of your workspace", + async options() { + const response = await this.listWorkspaces(); + const workspaceIds = response.workspaces; + return workspaceIds.map(({ + id, name, + }) => ({ + value: id, + label: name, + })); + }, + }, + database: { + type: "string", + label: "Database Name", + description: "Name of the database. Must be **NON POSTGRES ENABLED**.", + async options({ workspace }) { + const response = await this.listDatabases({ + workspace, + }); + const databaseNames = response.databases.filter(({ postgresEnabled }) => !postgresEnabled); + return databaseNames.map(({ name }) => ({ + value: name, + label: name, + })); + }, + }, + branch: { + type: "string", + label: "Branch Name", + description: "Name of the branch", + async options({ + endpoint, database, + }) { + const response = await this.listBranches({ + endpoint, + database, + }); + const branchNames = response.branches; + return branchNames.map(({ name }) => ({ + value: name, + label: name, + })); + }, + }, + }, + methods: { + _baseUrl() { + return "https://api.xata.io"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + url, + headers, + ...otherOpts + } = opts; + const finalUrl = url || `${this._baseUrl()}${path}`; + return axios($, { + ...otherOpts, + url: finalUrl, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + createRecord({ + endpoint, database, branch, table, ...args + }) { + return this._makeRequest({ + method: "post", + url: `${endpoint}/db/${database}:${branch}/tables/${table}/data`, + ...args, + }); + }, + replaceRecord({ + endpoint, database, branch, table, recordId, ...args + }) { + return this._makeRequest({ + method: "put", + url: `${endpoint}/db/${database}:${branch}/tables/${table}/data/${recordId}`, + ...args, + }); + }, + updateRecord({ + endpoint, database, branch, table, recordId, ...args + }) { + return this._makeRequest({ + method: "post", + url: `${endpoint}/db/${database}:${branch}/tables/${table}/data/${recordId}`, + ...args, + }); + }, + listRecords({ + endpoint, database, branch, table, ...args + }) { + return this._makeRequest({ + method: "post", + url: `${endpoint}/db/${database}:${branch}/tables/${table}/query`, + ...args, + }); + }, + listWorkspaces(args = {}) { + return this._makeRequest({ + path: "/workspaces", + ...args, + }); + }, + listDatabases({ + workspace, ...args + }) { + return this._makeRequest({ + path: `/workspaces/${workspace}/dbs`, + ...args, + }); + }, + listBranches({ + endpoint, database, ...args + }) { + return this._makeRequest({ + url: `${endpoint}/dbs/${database}`, + ...args, + }); + }, + listColumns({ + endpoint, database, branch, table, ...args + }) { + return this._makeRequest({ + url: `${endpoint}/db/${database}:${branch}/tables/${table}/columns`, + ...args, + }); + }, + getBranchSchema({ + endpoint, database, branch, ...args + }) { + return this._makeRequest({ + url: `${endpoint}/db/${database}:${branch}`, + ...args, + }); + }, + }, +}; diff --git a/components/xeggex/README.md b/components/xeggex/README.md new file mode 100644 index 0000000000000..b9559a6ad58a9 --- /dev/null +++ b/components/xeggex/README.md @@ -0,0 +1,11 @@ +# Overview + +The XeggeX API offers a gateway to cryptocurrency data, providing access to real-time info on prices, trades, and markets. Integrating this API into Pipedream workflows allows you to automate various crypto-related tasks, such as monitoring price changes, alerting on trading opportunities, or compiling market analysis. With Pipedream’s serverless platform, these workflows can run on events or schedules, interact with other services, and require minimal setup. + +# Example Use Cases + +- **Crypto Price Alert System**: Set up a workflow on Pipedream that checks cryptocurrency prices through the XeggeX API at regular intervals. When a specified price threshold is crossed, it triggers an alert. Use Pipedream’s built-in connectors to send notifications via email, SMS, or messaging platforms like Slack or Discord. + +- **Automated Trading Strategy**: Create a Pipedream workflow that listens for webhook events from XeggeX API indicating market changes. When certain conditions are met, the workflow could place trades on your behalf using a connected trading platform. Ensure risk management by incorporating logic to limit orders based on customizable criteria. + +- **Market Analysis Reports**: Use Pipedream to schedule daily retrieval of market data from the XeggeX API. Aggregate and analyze this data within the workflow, then compile it into a report. Connect to apps like Google Sheets or Data Studio to store and visualize the results, or automate distribution to stakeholders through email or cloud storage platforms. diff --git a/components/xeggex/package.json b/components/xeggex/package.json new file mode 100644 index 0000000000000..4d693b2d99b67 --- /dev/null +++ b/components/xeggex/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/xeggex", + "version": "0.0.1", + "description": "Pipedream XeggeX Components", + "main": "xeggex.app.mjs", + "keywords": [ + "pipedream", + "xeggex" + ], + "homepage": "https://pipedream.com/apps/xeggex", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/xeggex/xeggex.app.mjs b/components/xeggex/xeggex.app.mjs new file mode 100644 index 0000000000000..f120c07991f11 --- /dev/null +++ b/components/xeggex/xeggex.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "xeggex", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/xendit/README.md b/components/xendit/README.md index f1d415216aae0..dbf05b30f3df6 100644 --- a/components/xendit/README.md +++ b/components/xendit/README.md @@ -1,25 +1,11 @@ # Overview -The Xendit API is a powerful suite of APIs and SDKs that enable businesses, -developers, and individuals to make and receive secure payments across hundreds -of countries, currencies, and payment methods. From collecting payment for -ecommerce sites to processing donations for nonprofits and making mass -payments, the Xendit API can help users easily achieve these goals with its -robust tools and secure processes. +Leveraging the Xendit API on Pipedream, you can automate an array of financial tasks related to payments, disbursements, and accounting. Xendit simplifies handling transactions in Southeast Asia, offering services such as direct debit, credit card processing, and real-time payment tracking. By connecting it with Pipedream, you can create powerful workflows that trigger actions based on payment events, sync transaction data with other business systems, and streamline notification processes for financial operations. -Whether you're an experienced developer or just getting started, with the -Xendit API you can build a variety of payment solutions: +# Example Use Cases -- Accept debit and credit card payments within your app -- Collect payments from customers with invoices -- Create a marketplace with payouts to vendors -- Have mass disbursements to employees and clients -- Offer online checkout and shopping carts -- Setup recurring payments -- Create and accept virtual cards -- Send money to hundreds of countries with virtual accounts -- Securely store customer details for future payments +- **Automate Invoice Creation on Payment**: When a customer completes a payment through Xendit, automatically generate an invoice using a service like QuickBooks and email it to the customer using SendGrid. -By taking advantage of the Xendit API and its secure payment solutions, -businesses, developers, and individuals have the potential to reduce costs and -build efficient, comprehensive payment solutions quickly and securely. +- **Sync Payments with CRM**: Once a payment is received via Xendit, update the customer's payment history in a CRM like Salesforce or HubSpot, ensuring sales records are always current. + +- **Real-time Fraud Detection Alerts**: Monitor transactions processed by Xendit for unusual patterns. Use a tool like Slack or Twilio to send real-time alerts if potential fraud is detected, enabling immediate action. diff --git a/components/xero_accounting_api/README.md b/components/xero_accounting_api/README.md index 3fa3ab2cda8fb..15376051f339e 100644 --- a/components/xero_accounting_api/README.md +++ b/components/xero_accounting_api/README.md @@ -1,4 +1,10 @@ -# Using Webhooks +# Overview + +The Xero Accounting API offers a powerful gateway to access and manipulate financial data within Xero. Leveraging Pipedream's capabilities, developers can build custom workflows that streamline accounting processes, sync financial data with external systems, and trigger actions based on financial events. This API allows for the automation of tasks such as invoicing, bank reconciliation, bill payments, and reporting, which can lead to significant time savings and enhanced data accuracy. + + +# Getting Started +## Using Webhooks Xero supports webhooks for instant notifications of specific event changes. @@ -17,3 +23,11 @@ To create and use a webhook with Pipedream: For more information, please read Xero's [Creating a Webhook Guide](https://developer.xero.com/documentation/guides/webhooks/creating-webhooks/). If you have issues with this integration, please join our public Slack and ask for help. + +# Example Use Cases + +- **Automated Invoice Processing**: Trigger a workflow when new invoices are created in Xero to send email notifications using SendGrid, thus keeping stakeholders informed in real-time about billing activities. + +- **Expense Tracking and Approval Workflow**: Integrate Xero with Slack using Pipedream to automatically post messages to a designated channel for new expense claims, allowing for quick review and approval by the finance team. + +- **Synchronized Customer Management**: When a new contact is added to Xero, use Pipedream to trigger a workflow that creates or updates that contact in a CRM like Salesforce, ensuring consistent client information across business platforms. \ No newline at end of file diff --git a/components/xperiencify/README.md b/components/xperiencify/README.md index 4be3d623cca3e..bca58cdeee744 100644 --- a/components/xperiencify/README.md +++ b/components/xperiencify/README.md @@ -1,24 +1,11 @@ # Overview -Xperiencify is an API that allows developers to quickly and easily create web -applications, websites, and mobile games without having to code. With -Xperiencify, developers can focus on creating a great product while leaving the -complexity of coding to the Xperiencify team. +Xperiencify is a platform designed to create engaging online courses that motivate students through gamification and psychological triggers. The Xperiencify API allows course creators to manage and automate tasks related to courses, students, and their progress. With Pipedream, you can harness this API to create dynamic workflows that respond to course enrollments, progress milestones, and other student interactions. Automate notifications, gather analytics, synchronize with other platforms, and more, creating a seamless integration between Xperiencify and your digital ecosystem. -Xperiencify allows developers to explore the possibilities of creating custom -projects such as interactive web applications, dynamic websites, and mobile -games. Through the Xperiencify API, developers have access to a variety of -tools and resources that are designed to support and simplify the process of -creating web and mobile applications. +# Example Use Cases -Examples of projects you can build with the Xperiencify API include: +- **Course Enrollment Trigger**: When a new student enrolls in a course on Xperiencify, trigger a Pipedream workflow that sends a personalized welcome email via SendGrid and logs the enrollment in a Google Sheets spreadsheet for tracking. -- Java web applications -- Interactive ecommerce websites -- Responsive mobile websites -- HTML5 mobile games -- Mobile messaging applications -- Social networking applications -- Cloud managed services -- Data-driven web applications -- Mobile game server architecture +- **Progress Milestone Notifications**: Set up a Pipedream workflow that listens to progress updates from Xperiencify. When a student hits a significant milestone, automatically post a congratulatory message on their Slack channel and unlock a bonus resource by updating the course content on Xperiencify. + +- **Student Engagement Insights**: Use Pipedream to aggregate data on student interactions and progress from Xperiencify. Analyze the data to gain insights into engagement levels, and trigger a series of targeted emails through Mailchimp to re-engage inactive students or offer additional help to those struggling. diff --git a/components/xverify/actions/verify-address/verify-address.mjs b/components/xverify/actions/verify-address/verify-address.mjs new file mode 100644 index 0000000000000..cd087458fa198 --- /dev/null +++ b/components/xverify/actions/verify-address/verify-address.mjs @@ -0,0 +1,119 @@ +import app from "../../xverify.app.mjs"; + +export default { + key: "xverify-verify-address", + name: "Verify Address", + description: "Sends an address verification request. [See the documentation](https://apidocs.xverify.com/#address-verification-api-endpoint).", + version: "0.0.1", + type: "action", + props: { + app, + // eslint-disable-next-line pipedream/props-label, pipedream/props-description + info: { + type: "alert", + alertType: "info", + content: "**Note**: The `city`, `state`, and `zip` fields are optional. However, you must provide at least one of these fields to verify an address. If you provide all three fields, the API will use the city and state to verify the address.", + }, + domain: { + propDefinition: [ + app, + "domain", + ], + }, + address1: { + type: "string", + label: "Street Address 1", + description: "The street address to be verified.", + }, + address2: { + type: "string", + label: "Street Address 2", + description: "The second line of the street address to be verified.", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "The city of the address to be verified.", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "The state of the address to be verified.", + optional: true, + }, + zip: { + type: "string", + label: "Zip Code", + description: "The postal code of the address to be verified. **Required** if city and state are not provided.", + optional: true, + }, + urbanization: { + type: "string", + label: "Urbanization", + description: "A component of certain addresses in Puerto Rico.", + optional: true, + }, + parse: { + type: "boolean", + label: "Parse Address", + description: "Set to `true` if you want the street address to be parsed into individual elements in the response.", + optional: true, + }, + aff: { + propDefinition: [ + app, + "aff", + ], + }, + subaff: { + propDefinition: [ + app, + "subaff", + ], + }, + }, + methods: { + verifyAddress(args = {}) { + return this.app._makeRequest({ + path: "/av", + ...args, + }); + }, + }, + async run({ $ }) { + const { + verifyAddress, + domain, + address1, + address2, + city, + state, + zip, + urbanization, + parse, + aff, + subaff, + } = this; + + const response = await verifyAddress({ + $, + params: { + domain, + address1, + address2, + city, + state, + zip, + urbanization, + parse, + aff, + subaff, + }, + }); + + $.export("$summary", `Successfully sent address verification request with status \`${response.status}\`.`); + return response; + }, +}; diff --git a/components/xverify/actions/verify-email/verify-email.mjs b/components/xverify/actions/verify-email/verify-email.mjs new file mode 100644 index 0000000000000..f8992f5acf079 --- /dev/null +++ b/components/xverify/actions/verify-email/verify-email.mjs @@ -0,0 +1,82 @@ +import app from "../../xverify.app.mjs"; + +export default { + key: "xverify-verify-email", + name: "Verify Email", + description: "Sends an email verification request. [See the documentation](https://apidocs.xverify.com/#email-verification-api-endpoint).", + version: "0.0.1", + type: "action", + props: { + app, + domain: { + propDefinition: [ + app, + "domain", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + ip: { + type: "string", + label: "IP Address", + description: "IP address from which a user submitted an email address. Can be used to detect fraudulent or risky submissions.", + optional: true, + }, + ua: { + type: "string", + label: "User Agent", + description: "User Agent of the browser that submitted the email address. Can be used to detect fraudlent or risky data.", + optional: true, + }, + aff: { + propDefinition: [ + app, + "aff", + ], + }, + subaff: { + propDefinition: [ + app, + "subaff", + ], + }, + }, + methods: { + verifyEmail(args = {}) { + return this.app._makeRequest({ + path: "/ev", + ...args, + }); + }, + }, + async run({ $ }) { + const { + verifyEmail, + domain, + email, + ip, + ua, + aff, + subaff, + } = this; + + const response = await verifyEmail({ + $, + params: { + domain, + email, + ip, + ua, + aff, + subaff, + }, + }); + + $.export("$summary", `Successfully sent email verification with status \`${response.status}\`.`); + return response; + }, +}; diff --git a/components/xverify/actions/verify-phone/verify-phone.mjs b/components/xverify/actions/verify-phone/verify-phone.mjs new file mode 100644 index 0000000000000..75c68fe8a7c16 --- /dev/null +++ b/components/xverify/actions/verify-phone/verify-phone.mjs @@ -0,0 +1,66 @@ +import app from "../../xverify.app.mjs"; + +export default { + key: "xverify-verify-phone", + name: "Verify Phone", + description: "Sends a phone verification request. [See the documentation](https://apidocs.xverify.com/#phone-verification-api-endpoint).", + version: "0.0.1", + type: "action", + props: { + app, + domain: { + propDefinition: [ + app, + "domain", + ], + }, + phone: { + propDefinition: [ + app, + "phone", + ], + }, + aff: { + propDefinition: [ + app, + "aff", + ], + }, + subaff: { + propDefinition: [ + app, + "subaff", + ], + }, + }, + methods: { + verifyPhone(args = {}) { + return this.app._makeRequest({ + path: "/pv", + ...args, + }); + }, + }, + async run({ $ }) { + const { + verifyPhone, + domain, + phone, + aff, + subaff, + } = this; + + const response = await verifyPhone({ + $, + params: { + domain, + phone, + aff, + subaff, + }, + }); + + $.export("$summary", `Successfully sent verification with status \`${response.status}\`.`); + return response; + }, +}; diff --git a/components/xverify/package.json b/components/xverify/package.json new file mode 100644 index 0000000000000..0b97c4be8fa85 --- /dev/null +++ b/components/xverify/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/xverify", + "version": "0.1.1", + "description": "Pipedream Xverify Components", + "main": "xverify.app.mjs", + "keywords": [ + "pipedream", + "xverify" + ], + "homepage": "https://pipedream.com/apps/xverify", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/xverify/xverify.app.mjs b/components/xverify/xverify.app.mjs new file mode 100644 index 0000000000000..734fc0ebbc0d4 --- /dev/null +++ b/components/xverify/xverify.app.mjs @@ -0,0 +1,60 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "xverify", + propDefinitions: { + domain: { + type: "string", + label: "Domain", + description: "The domain you have configured in your Xverify settings under which this query should be processed. See below.", + options() { + return [ + this.$auth.domain, + ]; + }, + }, + email: { + type: "string", + label: "Email Address", + description: "The email address to be validated (and optionally corrected).", + }, + aff: { + type: "string", + label: "Affiliate ID", + description: "The ID you define to identify the affiliate or source of the email for reporting or potential blocking.", + optional: true, + }, + subaff: { + type: "string", + label: "Sub-Affiliate ID", + description: "The sub-identifier you define for the affiliate or source of the email for reporting or potential blocking.", + optional: true, + }, + phone: { + type: "string", + label: "Phone Number", + description: "The phone number to be verified.", + }, + }, + methods: { + getUrl(path) { + return `https://api.xverify.com/v2${path}`; + }, + getParams(params) { + return { + ...params, + api_key: this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, params, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + params: this.getParams(params), + }); + }, + }, +}; diff --git a/components/y_gy/README.md b/components/y_gy/README.md new file mode 100644 index 0000000000000..fc0b782e12f00 --- /dev/null +++ b/components/y_gy/README.md @@ -0,0 +1,13 @@ +markdown + +# Overview + +The y.gy API allows users to shorten URLs dynamically, providing a way to manage and track URL performance in applications or marketing campaigns. Using this API on Pipedream, you can automate URL shortening and integrate these activities with other services like databases, analytics tools, or CRM systems. This capability is particularly valuable in scenarios involving high-volume link generation, targeted content distribution, and real-time link performance analysis. + +# Example Use Cases + +- **Automated URL Shortening for Social Media Posts**: Automatically shorten URLs when posting to social media platforms like Twitter or Facebook. Whenever content is scheduled or published from a CMS, trigger a workflow in Pipedream that shortens the URL using y.gy and posts the new, shortened link along with the content automatically to the targeted social media. + +- **Dynamic Link Generation for Email Marketing Campaigns**: Enhance email marketing efforts by using Pipedream to integrate y.gy with email platforms like SendGrid or Mailchimp. Automatically shorten URLs when sending out bulk or personalized emails, ensuring that links are trackable and tidy. This workflow can be set to generate links as emails are being drafted, or dynamically as they are sent. + +- **Real-Time Analytics for Shortened URLs**: Connect y.gy with analytics tools like Google Analytics through Pipedream. Set up a workflow that captures data on who clicks the shortened URLs and when, and feed this data into analytics tools for real-time analysis. This can be used for A/B testing different URLs in marketing campaigns to see which performs better in real-time. diff --git a/components/y_gy/actions/create-short-link/create-short-link.mjs b/components/y_gy/actions/create-short-link/create-short-link.mjs new file mode 100644 index 0000000000000..382ccffcb7533 --- /dev/null +++ b/components/y_gy/actions/create-short-link/create-short-link.mjs @@ -0,0 +1,51 @@ +import app from "../../y_gy.app.mjs"; + +export default { + key: "y_gy-create-short-link", + name: "Create Short Link", + description: "Create new short links with y.gy. [See the documentation](https://app.y.gy/docs/api-docs/links#create-a-short-link)", + version: "0.0.1", + type: "action", + props: { + app, + destinationUrl: { + propDefinition: [ + app, + "destinationUrl", + ], + }, + domain: { + propDefinition: [ + app, + "domain", + ], + }, + suffix: { + propDefinition: [ + app, + "suffix", + ], + }, + password: { + propDefinition: [ + app, + "password", + ], + }, + }, + async run({ $ }) { + const response = await this.app.createShortLink({ + $, + data: { + destination_url: this.destinationUrl, + domain: this.domain, + suffix: this.suffix, + password: this.password, + }, + }); + + $.export("$summary", `The URL was successfully shortened: '${response.url}'`); + + return response; + }, +}; diff --git a/components/y_gy/actions/get-links/get-links.mjs b/components/y_gy/actions/get-links/get-links.mjs new file mode 100644 index 0000000000000..9891444613ac9 --- /dev/null +++ b/components/y_gy/actions/get-links/get-links.mjs @@ -0,0 +1,21 @@ +import app from "../../y_gy.app.mjs"; + +export default { + key: "y_gy-get-links", + name: "Get Links", + description: "Get a list of the links created by the authenticated account [See the documentation](https://app.y.gy/docs/api-docs/)", + version: "0.0.1", + type: "action", + props: { + app, + }, + async run({ $ }) { + const response = await this.app.getLinks({ + $, + }); + + $.export("$summary", `Successfully retrieved ${response.links.length} link(s)`); + + return response; + }, +}; diff --git a/components/y_gy/package.json b/components/y_gy/package.json new file mode 100644 index 0000000000000..cf18fce71ef6b --- /dev/null +++ b/components/y_gy/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/y_gy", + "version": "0.1.0", + "description": "Pipedream y.gy Components", + "main": "y_gy.app.mjs", + "keywords": [ + "pipedream", + "y_gy" + ], + "homepage": "https://pipedream.com/apps/y_gy", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5" + } +} diff --git a/components/y_gy/y_gy.app.mjs b/components/y_gy/y_gy.app.mjs new file mode 100644 index 0000000000000..6edf39551f07f --- /dev/null +++ b/components/y_gy/y_gy.app.mjs @@ -0,0 +1,65 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "y_gy", + propDefinitions: { + destinationUrl: { + type: "string", + label: "Destination URL", + description: "The short link will redirect to this destination URL", + }, + domain: { + type: "string", + label: "Domain", + description: "This is the root domain of the short link. You can add a custom link, but this has to be verified via the y.gy dashboard first.", + optional: true, + }, + suffix: { + type: "string", + label: "Suffix", + description: "The end of the domain. If the suffix is 123, the short link might look like 'y.gy/123'", + optional: true, + }, + password: { + type: "string", + label: "Password", + description: "A password to protect you page", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.y.gy/api/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + "api-key": `${this.$auth.api_key}`, + }, + }); + }, + async createShortLink(args = {}) { + return this._makeRequest({ + method: "post", + path: "/link", + ...args, + }); + }, + async getLinks(args = {}) { + return this._makeRequest({ + path: "/link", + ...args, + }); + }, + }, +}; diff --git a/components/yahoo_fantasy_sports/README.md b/components/yahoo_fantasy_sports/README.md index 67574339fbd7e..4a889d1749b86 100644 --- a/components/yahoo_fantasy_sports/README.md +++ b/components/yahoo_fantasy_sports/README.md @@ -1,24 +1,14 @@ # Overview -The Yahoo! Fantasy Sports API is an application programming interface (API) -publicly available for connecting with Yahoo's fantasy sports platform. It -provides the necessary tools for developers to create multi-tenant -applications, as well as for individual users to build custom applications -faster and easier than ever before. +The Yahoo! Fantasy Sports API opens a realm of possibilities for sports enthusiasts and developers, allowing them to tap into fantasy leagues, player stats, and real-time scoring. Leverage this data source within Pipedream to automate team management, notifications, and data analysis. You can create dynamic workflows that react to league updates, automate team roster changes, or aggregate statistics for insightful analytics, all with real-time triggers and seamless integrations with hundreds of apps. -The Yahoo! Fantasy Sports API can be used to build a variety of applications -and websites related to fantasy sports. Some of the possible applications built -with the Yahoo! Fantasy Sports API include: +# Example Use Cases -- Fantasy sports mobile apps, such as mobile versions of fantasy football, - basketball, baseball and hockey -- Websites for creating and tracking fantasy sports teams -- Websites for fantasy sports leagues -- Tools for calculating fantasy sports player rankings -- Tools for managing fantasy drafts -- Tools for analyzing and visualizing fantasy sports performance and trends -- Integrations with third party services such as scoring and stat tracking - services -- Widgets and plugins for fantasy sports websites and applications -- Bots and automation tools for fantasy sports -- Search tools for finding players in fantasy sports leagues +**Fantasy Team Performance Tracker** +Automatically collect your fantasy team's performance data after each game. Aggregate and store this data in Google Sheets using Pipedream's Google Sheets integration. Analyze trends over time, making data-driven decisions to optimize your roster. + +**Real-time Injury Alerts** +Set up a Pipedream workflow that monitors player statuses in real-time and sends a push notification via Twilio or Slack when a player on your fantasy team is listed as injured. This enables quick roster adjustments to avoid points loss. + +**Automated Trade Proposals** +Create a workflow where Pipedream assesses player stats and performance metrics, then automatically generates and sends out trade proposals to other managers in your league via Gmail when it identifies a beneficial trade opportunity. diff --git a/components/yahoo_finance_by_apidojo/package.json b/components/yahoo_finance_by_apidojo/package.json new file mode 100644 index 0000000000000..22d3e435f78b9 --- /dev/null +++ b/components/yahoo_finance_by_apidojo/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/yahoo_finance_by_apidojo", + "version": "0.6.0", + "description": "Pipedream yahoo_finance_by_apidojo Components", + "main": "yahoo_finance_by_apidojo.app.mjs", + "keywords": [ + "pipedream", + "yahoo_finance_by_apidojo" + ], + "homepage": "https://pipedream.com/apps/yahoo_finance_by_apidojo", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/yanado/README.md b/components/yanado/README.md index d02cf4a6926fa..40af53ba49a05 100644 --- a/components/yanado/README.md +++ b/components/yanado/README.md @@ -1,26 +1,11 @@ # Overview -The Yanado API is a powerful tool that enables developers to build custom -project, task and relationship management solutions quickly and easily. By -integrating it into your existing application, you can vastly improve overall -productivity. Here are some of the useful applications you can create: +Yanado turns your Gmail into a powerful collaboration tool, allowing you to manage tasks and projects directly within your inbox. With the Yanado API, you can automate task creation, project management, task updates, and notifications, syncing your email activities with project workflows. Pipedream's platform empowers you to integrate Yanado with a myriad of other apps, enabling seamless automation of complex workflows that bridge your email communications with task management, CRM updates, calendar scheduling, and more. -- Automated project workflows: Automate repetitive project steps and processes, - assign tasks to team members and receive reminders as they finish their - tasks. -- Resource management: Monitor, manage and update your team's time and - resources to optimize their performance. -- Collaboration tools: Coordinate team collaboration, receive timely - notifications and customer feedback with the help of user friendly - collaboration tools. -- Reporting & visuals: Generate automated reports with real-time data visuals, - analyze and review current project metrics with easy to interpret visuals. -- Custom fields & rules: Customize the Yanado API to match specific business - needs, create custom rules and monitor their success. -- API integrations: Connect the Yanado API to popular third-party apps to - further extend its functionality. -- Scheduling & tracking: Automate and track critical schedules, tasks and - events quickly and easily with intuitive scheduling components. +# Example Use Cases -With the Yanado API, you can expect to build custom solutions faster and with -greater ease. So start building your application today! +- **Email to Task Conversion**: When you receive an email from a new client, Pipedream can automatically create a task in Yanado. Link this to CRM software like Salesforce or HubSpot; when an email is tagged with "new-client", Pipedream creates a new client profile in your CRM and schedules a follow-up task in Yanado. + +- **Project Status Updates**: Set up a workflow on Pipedream where updates to tasks in Yanado trigger notifications in Slack. When a task reaches a certain status, such as "In Review", Pipedream sends a message to the designated Slack channel, keeping the team informed without leaving their workspace. + +- **Automated Task Prioritization**: Connect Yanado to Google Calendar via Pipedream, so when a meeting is scheduled that pertains to a specific project, a high-priority task is automatically created in Yanado. This ensures that action items from meetings are captured and prioritized correctly, without manual input. diff --git a/components/yelp/README.md b/components/yelp/README.md index 6decefaa92d61..02e664178ea49 100644 --- a/components/yelp/README.md +++ b/components/yelp/README.md @@ -1,29 +1,11 @@ # Overview -Yelp's API enables developers to access Yelp's vast collection of business -data, including descriptions, user reviews, and more. With the help of the Yelp -API, you can leverage the wealth of information within the Yelp network to -create powerful applications and services. +The Yelp API provides access to rich data on local businesses across various categories, including restaurants, bars, and service providers. Leveraging Yelp's API on Pipedream allows you to automate the retrieval of business information, ratings, and customer reviews, which can be instrumental in making data-driven decisions for market analysis, customer insights, or even personal use. By stitching the Yelp API into Pipedream workflows, you unlock potential for real-time monitoring, data collection, and cross-application synergy, enhancing your business intelligence and operational efficiency. -There are countless possibilities for what you can create with the Yelp API, a -few notable examples include: +# Example Use Cases -- Restaurant recommendations app - Make use of the Yelp API to create a - restaurant recommendation app for users. It can offer suggestions of nearby - dining options based on user's current location, preferences, budget and so - on. -- Business Finder App - Create a business-finding application using the Yelp - API's information about business owners, locations, reviews and more. It can - quickly source results according to a user's search query. -- Custom Review Aggregator - With the Yelp API, you can create a custom review - aggregator that takes reviews from Yelp, along with other review sources, and - organizes them into one single place. -- Benchmarking Reports - Create detailed reports with the help of the Yelp API - and visualize the comparative success of a user's business to their - competitors. -- Visual Search - Another interesting application that can be created with the - Yelp API is a visual search engine. It can take an image and provide search - results related to the image. -- Trip Planning Tool - Utilize the Yelp API to build a trip planning tool that - provides all the details about the places to see, things to do and places to - visit on a particular trip. +- **Local Restaurant Insights Dashboard**: Aggregate data from Yelp for all restaurants in a specific area, categorize them based on ratings and reviews, and then pipe this data into Google Sheets using Pipedream. This creates a real-time dashboard that updates automatically, allowing users to quickly identify top-rated restaurants for market analysis or personal culinary exploration. + +- **Notification System for New Reviews**: Monitor Yelp for new reviews on a particular set of businesses. When a new review is detected, use Pipedream's workflow to trigger an email or a Slack message to the business owner or the customer service team, providing them with immediate feedback for quality control and rapid response management. + +- **Competitive Analysis Automation**: Set up a Pipedream workflow that periodically fetches ratings and reviews of competitors and compares them against your own business metrics. By integrating this data with a tool like Airtable, you can maintain a live database of competitive intelligence, helping in strategic business planning and marketing efforts. diff --git a/components/yespo/README.md b/components/yespo/README.md new file mode 100644 index 0000000000000..634dfd455667a --- /dev/null +++ b/components/yespo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Yespo API offers a platform for personalized customer engagement through multichannel marketing automation. By integrating Yespo with Pipedream, you can create custom workflows that trigger based on customer interactions or data changes. This allows for dynamic campaign management, customer segmentation, and real-time analytics, enabling you to automate and scale personalized marketing efforts efficiently. + +# Example Use Cases + +- **Automate Lead Nurturing Campaigns**: Trigger a sequence of personalized emails or messages on Yespo when a new lead is added to your CRM. You can use Pipedream to listen for new entries in your CRM and use the Yespo API to start communication with leads based on their interests or behaviors. + +- **Sync Customer Data for Targeted Campaigns**: Keep your customer segments up-to-date by syncing data from your database to Yespo on a schedule. With Pipedream, you can create a workflow that periodically fetches customer data from your SQL database, processes it, and updates Yespo's customer segments for more targeted campaigns. + +- **Real-time Engagement Tracking**: Monitor customer interactions with your campaigns in real-time by connecting Yespo to a dashboard like Google Sheets. Set up a Pipedream workflow that captures webhooks from Yespo on email opens or link clicks and logs this data into Google Sheets for live monitoring and analysis. diff --git a/components/yext/package.json b/components/yext/package.json new file mode 100644 index 0000000000000..95766716fade5 --- /dev/null +++ b/components/yext/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/yext", + "version": "0.0.1", + "description": "Pipedream Yext Components", + "main": "yext.app.mjs", + "keywords": [ + "pipedream", + "yext" + ], + "homepage": "https://pipedream.com/apps/yext", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/yext/yext.app.mjs b/components/yext/yext.app.mjs new file mode 100644 index 0000000000000..9011467b0fbe4 --- /dev/null +++ b/components/yext/yext.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "yext", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/yoast_seo/README.md b/components/yoast_seo/README.md new file mode 100644 index 0000000000000..eb76521788e2f --- /dev/null +++ b/components/yoast_seo/README.md @@ -0,0 +1,11 @@ +# Overview + +The Yoast SEO API enables automation of SEO tasks and retrieval of data to optimize website content for search engines directly through Pipedream. Connect the Yoast SEO API to analyze pages, manage metadata, and improve search rankings. Pipedream's serverless platform makes it easy to build complex workflows linking Yoast SEO with other services, triggering actions based on content changes, SEO scores, or other criteria. + +# Example Use Cases + +- **Content Audit Workflow**: Automatically fetch the SEO scores of published blog posts from Yoast SEO and create a report in Google Sheets. Use this workflow to identify posts that need optimization and track changes over time. + +- **SEO Alerts via Slack**: Set up a workflow where Pipedream monitors your site's SEO health through Yoast SEO API. If a critical issue is detected, such as a drop in keyword rankings or missing metadata, an alert is sent to a designated Slack channel, prompting immediate action. + +- **Automated SEO Recommendations**: Combine Yoast SEO with a machine learning API to analyze content and generate SEO recommendations. When a new post is drafted in WordPress, trigger a Pipedream workflow that runs it through Yoast SEO and a machine learning model to suggest improvements before publication. diff --git a/components/yodiz/README.md b/components/yodiz/README.md index 9f42edf15d0cb..11f2bc2bcc1d3 100644 --- a/components/yodiz/README.md +++ b/components/yodiz/README.md @@ -1,15 +1,11 @@ # Overview -Using the Yodiz API, you can build all sorts of applications and integrations -that can help you stay organized and focused on your work. Here are some -examples of what you can build using the Yodiz API: - -- Tracking for features, tasks and bugs -- Automated tracking of progress and performance against goals -- Connecting external services such as Google Calendar -- Receive notifications on any project updates -- Easily pull project data (like backlogs and reports) into dashboards -- Multiple roles and privilege levels -- Customized views of project data -- Comprehensive analytics and reporting -- Integration with existing enterprise tools +The Yodiz API offers powerful ways to automate project management tasks, streamline issue tracking, and enhance team collaboration. By leveraging Pipedream's capabilities, you can create intricate workflows that react to project updates, synchronize data with other apps, and manage tasks without manual intervention. With access to Yodiz's endpoints, you can automate notifications, integrate with external databases, or even orchestrate complex cross-application workflows. It's a playground for developers looking to optimize their project management operations. + +# Example Use Cases + +- **Automated Task Creation from Customer Support Tickets**: When a support ticket is flagged as a bug in your customer support platform (like Zendesk), a workflow on Pipedream can capture this event and automatically create a corresponding issue in Yodiz, ensuring that your development team can prioritize and track bug fixes efficiently. + +- **Daily Stand-up Preparation**: Set up a workflow that runs every morning before your team's stand-up meeting. It can fetch the latest updates on tasks and user stories from Yodiz, compile a concise report, and post it to a Slack channel. This way, the team starts the day informed and ready to discuss the most relevant items. + +- **Sync with Git Repositories**: For teams using GitHub or Bitbucket, a Pipedream workflow can observe commits and pull requests, then update Yodiz items with relevant information such as commit messages or branch status. This bridge between code repository and project management tool keeps everyone aligned on progress and changes. diff --git a/components/yodiz/package.json b/components/yodiz/package.json new file mode 100644 index 0000000000000..5381ad024ed30 --- /dev/null +++ b/components/yodiz/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/yodiz", + "version": "0.6.0", + "description": "Pipedream yodiz Components", + "main": "yodiz.app.mjs", + "keywords": [ + "pipedream", + "yodiz" + ], + "homepage": "https://pipedream.com/apps/yodiz", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/yoplanning/README.md b/components/yoplanning/README.md new file mode 100644 index 0000000000000..30541143eaa1a --- /dev/null +++ b/components/yoplanning/README.md @@ -0,0 +1,11 @@ +# Overview + +YoPlanning API offers tools for event planning, with features such as creating and managing event schedules, attendee lists, and budgets. Leveraging this API in Pipedream enables automating event-related workflows, syncing event data with other services, and triggering actions based on event updates or attendee responses. It's a powerful way to streamline event management tasks, ensuring that nothing slips through the cracks as you orchestrate everything from small meetings to large conferences. + +# Example Use Cases + +- **Automated Event Creation and Notification**: Create events in YoPlanning via Pipedream when a new form submission is received in Google Forms. Trigger an email notification using the Gmail service to all attendees with event details. + +- **Sync Attendee Lists with CRM**: When a new attendee registers for an event in YoPlanning, automatically add their details to a CRM like Salesforce or HubSpot. Keep sales and marketing teams informed about potential leads attending the events. + +- **Budget Tracking and Alerts**: Monitor the budget for an event in YoPlanning and set up a workflow in Pipedream to send an alert to Slack when expenses approach the budget limit. This helps to ensure financial controls are in place. diff --git a/components/yoprint/README.md b/components/yoprint/README.md new file mode 100644 index 0000000000000..a54df98d38b65 --- /dev/null +++ b/components/yoprint/README.md @@ -0,0 +1,11 @@ +# Overview + +The YoPrint API offers a suite of functionalities to streamline printing business operations, allowing users to automate tasks related to order management, customer interactions, and production processes. By integrating YoPrint with Pipedream, businesses can create automated workflows to connect their print management with other services, enhancing efficiency and data consistency. With Pipedream's serverless platform, you can trigger actions based on various events, process data, and integrate with countless other apps. + +# Example Use Cases + +- **Sync Orders with Accounting Software**: Automatically push new orders from YoPrint to your accounting software, ensuring that your financial records are always up-to-date. This workflow can tag completed orders in YoPrint and then create corresponding invoices in apps like QuickBooks or Xero. + +- **Order Status Notifications**: Keep your customers informed by sending them real-time updates on their order status. When an order status changes in YoPrint, use Pipedream to send a notification via email, SMS, or a messaging platform like Slack, keeping everyone in the loop. + +- **Inventory Management**: Monitor your inventory levels by connecting YoPrint with an inventory management system. Set up a workflow that alerts you or updates your inventory when new orders come in, or when you're running low on supplies, using apps like Airtable or Google Sheets for tracking. diff --git a/components/yoprint/package.json b/components/yoprint/package.json index e4df7e3f1973a..c0858337ca16a 100644 --- a/components/yoprint/package.json +++ b/components/yoprint/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/yotpo/README.md b/components/yotpo/README.md index 52fa7096b6dbd..c7097582bb3d3 100644 --- a/components/yotpo/README.md +++ b/components/yotpo/README.md @@ -1,26 +1,11 @@ # Overview -With Yotpo’s Reviews & Ratings API, you can create applications to make it -easier than ever to connect your customers to your products. This comprehensive -API allows you to access, display, manage, and analyze all your reviews, -ratings and customer interactions, including user-generated content across -websites, in-app and mobile experiences. With Yotpo’s Reviews & Ratings API, -you can: +Yotpo - Reviews & Ratings API empowers businesses to leverage customer feedback by managing and utilizing user-generated content. With this API, you can programmatically fetch reviews, respond to customer feedback, and analyze sentiment to improve products and services. Integrating it with Pipedream allows for automation of these tasks, streamlining the process of collecting and acting on customer insights. -- Create custom marketing campaigns to boost customer loyalty and engagement -- Track customer sentiment by monitoring reviews, ratings and user-generated - content -- Gather feedback from customers to improve product design and features -- Gain deeper insights into customer behaviour from review and ratings data -- Integrate data from multiple sources for a more complete view of the customer - experience -- Build applications to drive customer conversation and engagement +# Example Use Cases -Examples of applications or experiences that can be built using the Yotpo -Reviews & Ratings API include: +- **Sync Reviews to a Google Sheet for Analysis**: Automatically transfer new reviews from Yotpo to a Google Sheets document using a Pipedream workflow. This enables easy monitoring and analysis of customer feedback trends over time without manual data entry. -- An interactive ratings and reviews page -- A product comparison page with user-generated ratings and reviews -- An automated system to send a survey to customers after a purchase -- A widget to display product ratings and reviews on a website -- A dashboard to view customer profile data, ratings and reviews +- **Trigger Email Campaigns Based on Review Ratings**: Set up a workflow that triggers a specific email campaign from a marketing platform like Mailchimp when a review with either a high or low rating is received. High ratings could trigger a "thank you" campaign, while low ratings might start a customer service follow-up sequence. + +- **Post Top Reviews to Social Media**: Create a Pipedream workflow that filters and posts 5-star reviews to your company's social media channels, such as Twitter or Facebook, using their respective APIs. This can automatically highlight positive customer experiences, boosting social proof and brand reputation. diff --git a/components/yotpo_loyalty_referrals/README.md b/components/yotpo_loyalty_referrals/README.md index 419160d45b432..39e3be382be04 100644 --- a/components/yotpo_loyalty_referrals/README.md +++ b/components/yotpo_loyalty_referrals/README.md @@ -1,19 +1,11 @@ # Overview -Yotpo's Loyalty & Referrals API makes it easy to create incentives and rewards -for your customers, driving relationships and creating loyalty within your -brand. Implementing Yotpo's API can help you build programs that nurture the -customer relationships and encourage them to re-engage with your store. By -linking customer data, you can create personalize programs that cater to each -customer's individual needs, strengthening the relationship and driving -conversions. +The Yotpo - Loyalty & Referrals API unlocks the potential to craft custom loyalty and referral programs by integrating with your tech stack through Pipedream. With this API, you can manage rewards, referrals, and customer loyalty data in real-time. Automate the process of rewarding customers for their engagement, track referral sources, and analyze program effectiveness directly through Pipedream's serverless platform. -With Yotpo's Loyalty & Referrals API you can: +# Example Use Cases -- Create personalized reward and loyalty programs -- Customize and track customer rewards -- Set up referral campaigns -- Integrate loyalty and referral programs with other marketing efforts -- Increase customer engagement and loyalty -- Monitor customer satisfaction and loyalty -- Generate reports of customers’ activity and behaviors. +- **Sync New Referral Sign-ups to CRM:** When a user signs up through a referral link, trigger a workflow to add the new customer details to your CRM system. This maintains an updated list of leads generated via referrals, allowing for targeted follow-ups or engagement campaigns. + +- **Reward Points Balance Updates to Email Marketing Tool:** Automate the process of updating customer reward points balance in an email marketing tool like Mailchimp. Once points are updated or redeemed, Pipedream can trigger a personalized email campaign to encourage further purchases or reward redemptions. + +- **Aggregate Customer Loyalty Data for Analytics:** Collect and send customer loyalty data points to a service like Google Sheets or a BI tool. Use this aggregated data to analyze trends, measure program ROI, and adjust strategies for maximizing engagement and retention. diff --git a/components/yotpo_loyalty_referrals/package.json b/components/yotpo_loyalty_referrals/package.json new file mode 100644 index 0000000000000..0d8891948647b --- /dev/null +++ b/components/yotpo_loyalty_referrals/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/yotpo_loyalty_referrals", + "version": "0.6.0", + "description": "Pipedream yotpo_loyalty_referrals Components", + "main": "yotpo_loyalty_referrals.app.mjs", + "keywords": [ + "pipedream", + "yotpo_loyalty_referrals" + ], + "homepage": "https://pipedream.com/apps/yotpo_loyalty_referrals", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/you_can_book_me/README.md b/components/you_can_book_me/README.md new file mode 100644 index 0000000000000..51589c05b05b1 --- /dev/null +++ b/components/you_can_book_me/README.md @@ -0,0 +1,11 @@ +# Overview + +The You Can Book Me API lets you automate scheduling by interacting with your booking data. On Pipedream, you can connect this API to create dynamic workflows that trigger actions in other apps when new bookings are made, changes occur, or reminders need to be sent. Pipedream enables these interactions while handling the backend logic and API requests, so you can focus on crafting the user experience and flow. + +# Example Use Cases + +- **Sync Bookings with Google Calendar**: Automatically create or update events in Google Calendar when new bookings are made through You Can Book Me. Ensure your schedule is always up-to-date across platforms. + +- **Send Custom Email Confirmations**: Trigger a customized email confirmation using a service like SendGrid whenever a booking is made. Include details specific to the booking, or add personalized content for your client. + +- **Manage Booking Data With Airtable**: On each new booking, create a new record in an Airtable base. Use this to track, manage, and analyze your bookings or customer interactions over time. diff --git a/components/you_need_a_budget/README.md b/components/you_need_a_budget/README.md index 13c67f59dd6c0..c3be130d55a99 100644 --- a/components/you_need_a_budget/README.md +++ b/components/you_need_a_budget/README.md @@ -1,22 +1,11 @@ # Overview -The You Need a Budget (YNAB) API provides APIs to interact with YNAB users’ -budgets, transactions, and more. With the YNAB API, developers have the ability -to build applications that are tailored to an individual's budget and financial -lifestyle. +The You Need a Budget (YNAB) API offers a direct line into your budgeting data, allowing you to read and write transaction details, access budget categories, update account balances, and more. By leveraging this API on Pipedream, you can automate your financial tracking and synchronize your budget with other aspects of your financial life. This interface is particularly powerful for those looking to streamline their budgeting process, ensure real-time updates across platforms, and generate custom financial reports. -Whether you're looking to create an app to assist with budgeting and tracking -expenses, generating reports, or getting a real-time view of an individual's -budget and movements, the YNAB API provides the tools to do it. The YNAB API is -easy to use, secure, and versatile. +# Example Use Cases -Here are some examples of how you can use the YNAB API: +- **Automated Transaction Recording**: When you make a purchase with a credit card that's tracked by another service like Plaid, Pipedream can catch the transaction via a webhook. It then automatically logs this new transaction in the appropriate YNAB budget category, keeping your budget up to date without manual entry. -- Build an application to track expenses and generate reports -- Monitor the balances of multiple budgets -- Generate a budget snapshot based on accounts -- Create custom visualizations and analytics tools -- Create notifications when specific budget values change -- Automate budgeting tasks, such as transfers and payments between accounts -- Create interactive dashboards and track real-time trends -- Enforce budgeting rules, such as threshold limits or budget goal tracking +- **Category Balance Alerts**: Set up a daily scheduled workflow on Pipedream to check your YNAB category balances. If any category's balance falls below a predefined threshold, Pipedream sends an alert via SMS using Twilio or an email through SendGrid, helping you to stay on top of your budgeting goals. + +- **Expense Report Generation**: At the end of each month, a Pipedream workflow compiles transactions and budget category data from YNAB and formats a custom expense report. The report could then be sent to your email or uploaded to Google Drive for easy access and sharing with family or financial advisors. diff --git a/components/youcanbook_me/README.md b/components/youcanbook_me/README.md index 980e78e8c8a5b..a2ce32d7930e6 100644 --- a/components/youcanbook_me/README.md +++ b/components/youcanbook_me/README.md @@ -1,28 +1,11 @@ # Overview -The YouCanBook.me API allows developers to easily integrate booking and -appointment-scheduling capabilities into any website or application. With the -API, developers can programmatically build bookable experiences that let -customers book available appointments and see a personalized, real-time view of -their calendars. +The YouCanBook.Me API allows for automating appointment scheduling by integrating with your calendar. It can trigger actions when new bookings are made, modified, or canceled. Building on Pipedream's platform enables developers to create robust workflows that can interact with other services. For example, you can confirm appointments, sync with other calendars, send custom emails, or update CRM records based on booking activities. -The API lets developers customize the booking experience in a wide variety of -ways. You can easily adjust the button design, create custom booking forms, -define how and when customers will receive a booking confirmation, add -additional fields, provide access to members-only appointments, and plenty -more. +# Example Use Cases -With the YouCanBook.me API, you can build the following: +- **Appointment Confirmation and Reminder Workflow**: When a booking is made through YouCanBook.Me, trigger a Pipedream workflow that sends a personalized confirmation email to the customer. Schedule a follow-up reminder email 24 hours before the appointment using the Delay action. Connect to a service like Twilio to send an SMS reminder as well. -- Scheduling Widgets: Create widgets with simple embed codes that can be - custom-styled to fit the look and feel of your website. -- Automated Appointment Reminders: Send automated reminder emails or text - messages to customers when an appointment is approaching. -- Online Payment Portals: Link the WooCommerce or Shopify payment gateways to - the YouCanBook.me API for secure online payments. -- Messaging Integrations: Create automated messaging experiences with other - providers like WhatsApp, Telegram, Facebook Messenger, and more. -- Automated Notifications: Send automatic notifications to customers and staff - when bookings are made or cancelled. -- Calendar Connections: Connect YouCanBook.Me with 3rd-party calendaring - applications, like iCalendar and Google Calendar. +- **CRM Integration for New Bookings**: After a new appointment is scheduled, use Pipedream to automatically create or update a contact in a CRM like Salesforce or HubSpot. The workflow can fetch additional details from the booking and log the appointment as an event or activity associated with the customer's record in the CRM. + +- **Resource Allocation Based on Bookings**: Use booking details from YouCanBook.Me to manage resource allocation in project management tools like Asana or Trello. When a new booking is made, a Pipedream workflow can create a new task or update an existing project, assigning team members and setting due dates to ensure resources are ready for the appointment. diff --git a/components/youtube_analytics_api/README.md b/components/youtube_analytics_api/README.md index 6b841c7af7479..a2cf34f95ee7f 100644 --- a/components/youtube_analytics_api/README.md +++ b/components/youtube_analytics_api/README.md @@ -1,23 +1,11 @@ # Overview -Google's YouTube (Analytics API) allows developers to extract insights and -valuable analytics data from their YouTube channels. By using the YouTube -Analytics API, developers can create comprehensive reports and analysis tools -that show how content is performing on YouTube. With this powerful API, -developers can create applications that measure audience engagement, understand -how videos impact an audience’s behavior, and analyze how audience demographic -influences viewership. +The YouTube Analytics API enables you to pull complex, insightful data regarding your YouTube channel's performance, audience demographics, and engagement metrics. It's a goldmine for content creators looking to refine their content strategy based on solid data. Using Pipedream, you can automate the extraction of these analytics, set up real-time alerts, or synchronize this data with other tools for enhanced reporting and decision-making. -Below are just a few examples of the powerful and comprehensive analytics -solutions you can build with the YouTube (Analytics API): +# Example Use Cases -- Track and segment user engagements with videos and channels. -- Monitor and compare user engagement with different types of content. -- See the demographic breakdown of your viewers. -- Measure audience retention and monitor the impact of different strategies. -- Analyze how content virality affects overall viewership. -- Insight into viewer attention and viewership for individual videos and - channels. -- Detect anomalies and gauge overall performance of content over time. -- Predict how various changes (e.g. content optimizations) will impact - viewership. +- **Automated Report Generation**: Schedule a Pipedream workflow to fetch YouTube Analytics data daily, weekly, or monthly, then format this data into a report and send it via email or save it to Google Sheets. For instance, you can create a workflow that summarizes view counts, watch time, and new subscribers, offering a regular snapshot of channel growth without manual effort. + +- **Real-time Subscriber Milestone Alerts**: Create a Pipedream workflow that checks your subscriber count at regular intervals. Once you hit a certain milestone, it can trigger a celebratory post on your social media accounts, or notify you through Slack or another messaging platform. This automatic notification helps engage with your community at the right moments. + +- **Content Performance Dashboard Integration**: Build a dashboard in a tool like Tableau or Google Data Studio displaying your YouTube channel's analytics. Set up a Pipedream workflow that pushes YouTube Analytics data to these platforms periodically, ensuring your dashboard always reflects up-to-date information to guide your content strategies. diff --git a/components/youtube_analytics_api/actions/common/reports-query.mjs b/components/youtube_analytics_api/actions/common/reports-query.mjs new file mode 100644 index 0000000000000..542f55179389c --- /dev/null +++ b/components/youtube_analytics_api/actions/common/reports-query.mjs @@ -0,0 +1,119 @@ +import app from "../../youtube_analytics_api.app.mjs"; +import constants from "../../common/constants.mjs"; +import utils from "../../common/utils.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; + +export default { + props: { + app, + reloader: { + type: "boolean", + label: "Hidden Reloader", + description: "This prop is used to reload the props when the step gets created.", + hidden: true, + reloadProps: true, + }, + startDate: { + propDefinition: [ + app, + "startDate", + ], + }, + endDate: { + propDefinition: [ + app, + "endDate", + ], + }, + dimensions: { + propDefinition: [ + app, + "dimensions", + ], + }, + sort: { + propDefinition: [ + app, + "sort", + ], + }, + maxResults: { + propDefinition: [ + app, + "maxResults", + ], + }, + }, + methods: { + getIdsProps() { + const { idType } = this; + + if (idType === constants.ID_TYPE.CHANNEL.value) { + return { + idType: propsFragments.idType, + }; + } + + if (idType === constants.ID_TYPE.CONTENT_OWNER.value) { + return { + idType: propsFragments.idType, + ids: { + type: "string", + label: "Content Owner Name", + description: "The content owner name for the user. Eg. `MyContentOwnerName`.", + }, + }; + } + + if (idType === constants.ID_TYPE.CHANNEL_ID.value) { + return { + idType: propsFragments.idType, + ids: { + type: "string", + label: "Channel ID", + description: "The channel ID for the user. Eg. `UC_x5XG1OV2P6uZZ5FSM9Ttw`. You can find the ID using the [YouTube Data API](https://developers.google.com/youtube/v3/docs/channels/list).", + }, + }; + } + + return { + idType: propsFragments.idType, + }; + }, + getIdsParam() { + const { + idType, + ids, + } = this; + if (idType === constants.ID_TYPE.CHANNEL.value) { + return "channel==MINE"; + } + if (idType === constants.ID_TYPE.CONTENT_OWNER.value) { + return `contentOwner==${ids}`; + } + if (idType === constants.ID_TYPE.CHANNEL_ID.value) { + return `channel==${ids}`; + } + }, + getFiltersParam() { + const { filters } = this; + const filtersObj = utils.parseJson(filters); + + if (!filtersObj) { + return; + } + + return utils.arrayToCommaSeparatedList( + Object.entries(filtersObj) + .reduce((acc, [ + key, + val, + ]) => [ + ...acc, + `${key}==${val}`, + ], []), + ";", + ); + }, + }, +}; diff --git a/components/youtube_analytics_api/actions/get-video-metrics/get-video-metrics.mjs b/components/youtube_analytics_api/actions/get-video-metrics/get-video-metrics.mjs new file mode 100644 index 0000000000000..e8a66998f4cc7 --- /dev/null +++ b/components/youtube_analytics_api/actions/get-video-metrics/get-video-metrics.mjs @@ -0,0 +1,54 @@ +import common from "../common/reports-query.mjs"; +import utils from "../../common/utils.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; + +export default { + ...common, + key: "youtube_analytics_api-get-video-metrics", + name: "Get Video Metrics", + description: "Retrieve detailed analytics for a specific video. [See the documentation](https://developers.google.com/youtube/analytics/reference/reports/query)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + videoId: { + type: "string", + label: "Video ID", + description: "The ID of the video for which you want to retrieve metrics. Eg. `pd1FJh59zxQ`.", + }, + metrics: propsFragments.metrics, + }, + additionalProps() { + return this.getIdsProps(); + }, + async run({ $ }) { + const { + app, + videoId, + getIdsParam, + startDate, + endDate, + metrics, + dimensions, + sort, + maxResults, + } = this; + + const response = await app.reportsQuery({ + $, + params: { + ids: getIdsParam(), + startDate, + endDate, + metrics: utils.arrayToCommaSeparatedList(metrics), + dimensions: utils.arrayToCommaSeparatedList(dimensions), + sort: utils.arrayToCommaSeparatedList(sort), + maxResults, + filters: `video==${videoId}`, + }, + }); + + $.export("$summary", "Successfully fetched video metrics."); + return response; + }, +}; diff --git a/components/youtube_analytics_api/actions/list-channel-reports/list-channel-reports.mjs b/components/youtube_analytics_api/actions/list-channel-reports/list-channel-reports.mjs new file mode 100644 index 0000000000000..41eccc38e1d25 --- /dev/null +++ b/components/youtube_analytics_api/actions/list-channel-reports/list-channel-reports.mjs @@ -0,0 +1,106 @@ +import common from "../common/reports-query.mjs"; +import constants from "../../common/constants.mjs"; +import utils from "../../common/utils.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; + +export default { + ...common, + key: "youtube_analytics_api-list-channel-reports", + name: "List Channel Reports", + description: "Fetch summary analytics reports for a specified youtube channel. Optional filters include date range and report type. [See the documentation](https://developers.google.com/youtube/analytics/reference/reports/query)", + version: "0.0.1", + type: "action", + additionalProps() { + const { + getIdsProps, + getReportTypeProps, + } = this; + + return { + ...getIdsProps(), + ...getReportTypeProps(), + }; + }, + methods: { + ...common.methods, + getReportTypeProps() { + const { channelReportType } = this; + const { + VIDEO_BASIC_USER_ACTIVITY_STATS, + PLAYLIST_BASIC_STATS, + } = constants.CHANNEL_REPORT_TYPE; + + if (channelReportType === VIDEO_BASIC_USER_ACTIVITY_STATS.value) { + const supportedFilters = VIDEO_BASIC_USER_ACTIVITY_STATS.metadata.filters + .reduce((acc, filter) => ({ + ...acc, + [filter]: "", + }), {}); + + return { + channelReportType: propsFragments.channelReportType, + metrics: { + ...propsFragments.metrics, + options: VIDEO_BASIC_USER_ACTIVITY_STATS.metadata.metrics, + }, + filters: { + ...propsFragments.filters, + description: `**Supported filters: \`${JSON.stringify(supportedFilters)}\`**. ${propsFragments.filters.description}`, + }, + }; + } + + if (channelReportType === PLAYLIST_BASIC_STATS.value) { + const supportedFilters = PLAYLIST_BASIC_STATS.metadata.filters + .reduce((acc, filter) => ({ + ...acc, + [filter]: "", + }), {}); + + return { + channelReportType: propsFragments.channelReportType, + metrics: { + ...propsFragments.metrics, + options: PLAYLIST_BASIC_STATS.metadata.metrics, + }, + filters: { + ...propsFragments.filters, + description: `**Supported filters: \`${JSON.stringify(supportedFilters)}\`**. ${propsFragments.filters.description}`, + }, + }; + } + + return { + channelReportType: propsFragments.channelReportType, + }; + }, + }, + async run({ $ }) { + const { + app, + getIdsParam, + getFiltersParam, + startDate, + endDate, + metrics, + sort, + maxResults, + } = this; + + const response = await app.reportsQuery({ + $, + params: { + ids: getIdsParam(), + startDate, + endDate, + metrics: utils.arrayToCommaSeparatedList(metrics), + filters: getFiltersParam(), + sort: utils.arrayToCommaSeparatedList(sort), + maxResults, + }, + }); + + $.export("$summary", "Successfully fetched channel reports."); + return response; + }, +}; diff --git a/components/youtube_analytics_api/actions/query-custom-analytics/query-custom-analytics.mjs b/components/youtube_analytics_api/actions/query-custom-analytics/query-custom-analytics.mjs new file mode 100644 index 0000000000000..55c8765cd3d4a --- /dev/null +++ b/components/youtube_analytics_api/actions/query-custom-analytics/query-custom-analytics.mjs @@ -0,0 +1,50 @@ +import common from "../common/reports-query.mjs"; +import utils from "../../common/utils.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; + +export default { + ...common, + key: "youtube_analytics_api-query-custom-analytics", + name: "Query Custom Analytics", + description: "Execute a custom analytics query using specified metrics, dimensions, filters, and date ranges. Requires query parameters to configure. [See the documentation](https://developers.google.com/youtube/analytics/reference/reports/query).", + version: "0.0.1", + type: "action", + props: { + ...common.props, + metrics: propsFragments.metrics, + filters: propsFragments.filters, + }, + additionalProps() { + return this.getIdsProps(); + }, + async run({ $ }) { + const { + app, + getIdsParam, + getFiltersParam, + startDate, + endDate, + metrics, + dimensions, + sort, + maxResults, + } = this; + + const response = await app.reportsQuery({ + $, + params: { + ids: getIdsParam(), + startDate, + endDate, + metrics: utils.arrayToCommaSeparatedList(metrics), + dimensions: utils.arrayToCommaSeparatedList(dimensions), + filters: getFiltersParam(), + sort: utils.arrayToCommaSeparatedList(sort), + maxResults, + }, + }); + + $.export("$summary", "Successfully fetched custom analytics data."); + return response; + }, +}; diff --git a/components/youtube_analytics_api/common/constants.mjs b/components/youtube_analytics_api/common/constants.mjs new file mode 100644 index 0000000000000..71be1063c4036 --- /dev/null +++ b/components/youtube_analytics_api/common/constants.mjs @@ -0,0 +1,164 @@ +const METRIC = { + AD_IMPRESSIONS: "adImpressions", + ANNOTATION_CLICKABLE_IMPRESSIONS: "annotationClickableImpressions", + ANNOTATION_CLICKS: "annotationClicks", + ANNOTATION_CLICK_THROUGH_RATE: "annotationClickThroughRate", + ANNOTATION_CLOSABLE_IMPRESSIONS: "annotationClosableImpressions", + ANNOTATION_CLOSES: "annotationCloses", + ANNOTATION_CLOSE_RATE: "annotationCloseRate", + ANNOTATION_IMPRESSIONS: "annotationImpressions", + AUDIENCE_WATCH_RATIO: "audienceWatchRatio", + AVERAGE_VIEW_DURATION: "averageViewDuration", + AVERAGE_VIEW_PERCENTAGE: "averageViewPercentage", + CARD_CLICK_RATE: "cardClickRate", + CARD_CLICKS: "cardClicks", + CARD_IMPRESSIONS: "cardImpressions", + CARD_TEASER_CLICK_RATE: "cardTeaserClickRate", + CARD_TEASER_CLICKS: "cardTeaserClicks", + CARD_TEASER_IMPRESSIONS: "cardTeaserImpressions", + COMMENTS: "comments", + CPM: "cpm", + DISLIKES: "dislikes", + ESTIMATED_AD_REVENUE: "estimatedAdRevenue", + ESTIMATED_MINUTES_WATCHED: "estimatedMinutesWatched", + ESTIMATED_REVENUE: "estimatedRevenue", + GROSS_REVENUE: "grossRevenue", + LIKES: "likes", + MONETIZED_PLAYBACKS: "monetizedPlaybacks", + PLAYBACK_BASED_CPM: "playbackBasedCpm", + PLAYLIST_STARTS: "playlistStarts", + SAVES_ADDED: "savesAdded", + SAVES_REMOVED: "savesRemoved", + SHARES: "shares", + SUBSCRIBERS_GAINED: "subscribersGained", + SUBSCRIBERS_LOST: "subscribersLost", + VIDEOS_ADDED_TO_PLAYLISTS: "videosAddedToPlaylists", + VIDEOS_REMOVED_FROM_PLAYLISTS: "videosRemovedFromPlaylists", + VIEWER_PERCENTAGE: "viewerPercentage", + VIEWS: "views", +}; + +const DIMENSION = { + AD_TYPE: "adType", + AGE_GROUP: "ageGroup", + ASSET: "asset", + AUDIENCE_TYPE: "audienceType", + CHANNEL: "channel", + CLAIMED_STATUS: "claimedStatus", + CONTENT_OWNER: "contentOwner", + COUNTRY: "country", + DAY: "day", + DEVICE_TYPE: "deviceType", + ELAPSED_VIDEO_TIME_RATIO: "elapsedVideoTimeRatio", + GENDER: "gender", + INSIGHT_PLAYBACK_LOCATION_DETAIL: "insightPlaybackLocationDetail", + INSIGHT_PLAYBACK_LOCATION_TYPE: "insightPlaybackLocationType", + INSIGHT_TRAFFIC_SOURCE_DETAIL: "insightTrafficSourceDetail", + INSIGHT_TRAFFIC_SOURCE_TYPE: "insightTrafficSourceType", + LIVE_OR_ON_DEMAND: "liveOrOnDemand", + OPERATING_SYSTEM: "operatingSystem", + PLAYLIST: "playlist", + PROVINCE: "province", + SHARING_SERVICE: "sharingService", + SUBSCRIBED_STATUS: "subscribedStatus", + SUBTITLE_LANGUAGE: "subtitleLanguage", + UPLOADER_TYPE: "uploaderType", + VIDEO: "video", +}; + +const ID_TYPE = { + CHANNEL: { + label: "My Channel", + value: "MINE", + }, + CHANNEL_ID: { + label: "Channel ID", + value: "channelId", + }, + CONTENT_OWNER: { + label: "Content Owner", + value: "contentOwner", + }, +}; + +const CHANNEL_REPORT_TYPE = { + VIDEO_BASIC_USER_ACTIVITY_STATS: { + label: "Basic User Activity Statistics For Video", + value: "basicUserActivityStatsForVideo", + metadata: { + metrics: [ + "views", + "redViews", + "comments", + "likes", + "dislikes", + "videosAddedToPlaylists", + "videosRemovedFromPlaylists", + "shares", + "estimatedMinutesWatched", + "estimatedRedMinutesWatched", + "averageViewDuration", + "averageViewPercentage", + "annotationClickThroughRate", + "annotationCloseRate", + "annotationImpressions", + "annotationClickableImpressions", + "annotationClosableImpressions", + "annotationClicks", + "annotationCloses", + "cardClickRate", + "cardTeaserClickRate", + "cardImpressions", + "cardTeaserImpressions", + "cardClicks", + "cardTeaserClicks", + "subscribersGained", + "subscribersLost", + "estimatedRevenue*", + "estimatedAdRevenue*", + "grossRevenue*", + "estimatedRedPartnerRevenue*", + "monetizedPlaybacks*", + "playbackBasedCpm*", + "adImpressions*", + "cpm*", + ], + filters: [ + "country", + "continent", + "subContinent", + "video", + "group", + ], + }, + }, + PLAYLIST_BASIC_STATS: { + label: "Basic Statistics For Playlist", + value: "basicStatsForPlaylist", + metadata: { + metrics: [ + "views", + "estimatedMinutesWatched", + "averageViewDuration", + "averageTimeInPlaylist", + "playlistAverageViewDuration", + "playlistEstimatedMinutesWatched", + "playlistSaves", + "playlistStarts", + "playlistViews", + "viewsPerPlaylistStart", + ], + filters: [ + "playlist", + "group", + ], + }, + }, +}; + +export default { + METRIC, + DIMENSION, + ID_TYPE, + CHANNEL_REPORT_TYPE, +}; diff --git a/components/youtube_analytics_api/common/props-fragments.mjs b/components/youtube_analytics_api/common/props-fragments.mjs new file mode 100644 index 0000000000000..098547f79d376 --- /dev/null +++ b/components/youtube_analytics_api/common/props-fragments.mjs @@ -0,0 +1,37 @@ +import constants from "./constants.mjs"; + +export default { + idType: { + type: "string", + label: "ID Type", + description: "The type of ID to use for the query. This can be either `My Channel`, `Channel ID`, or `Content Owner`.", + options: Object.values(constants.ID_TYPE), + default: constants.ID_TYPE.CHANNEL.value, + reloadProps: true, + }, + channelReportType: { + type: "string", + label: "Channel Report Type", + description: "The type of report to fetch for the specified YouTube Channel. This selects default dimensions, metrics and filters.", + options: Object.values(constants.CHANNEL_REPORT_TYPE) + .map(({ + // eslint-disable-next-line no-unused-vars + metadata, + ...rest + }) => rest), + default: constants.CHANNEL_REPORT_TYPE.VIDEO_BASIC_USER_ACTIVITY_STATS.value, + reloadProps: true, + }, + metrics: { + type: "string[]", + label: "Metrics", + description: "Metrics, such as `views` or `likes`, `dislikes`. See the documentation for [channel reports](https://developers.google.com/youtube/analytics/channel_reports) or [content owner reports](https://developers.google.com/youtube/analytics/content_owner_reports) for a list of the reports that you can retrieve and the metrics available in each report. (The [Metrics](https://developers.google.com/youtube/reporting#metrics) document contains definitions for all of the metrics.).", + options: Object.values(constants.METRIC), + }, + filters: { + type: "object", + label: "Filters", + description: "A list of filters that should be applied when retrieving YouTube Analytics data. The documentation for [channel reports](https://developers.google.com/youtube/analytics/channel_reports) and [content owner reports](https://developers.google.com/youtube/analytics/content_owner_reports) identifies the dimensions that can be used to filter each report, and the [Dimensions](https://developers.google.com/youtube/analytics/dimsmets/dims) document defines those dimensions.\n\nIf a request uses multiple filters the returned result table will satisfy both filters. For example, a filters parameter value of `{\"video\":\"dMH0bHeiRNg\",\"country\":\"IT\"}` restricts the result set to include data for the given video in Italy.\n\nSpecifying multiple values for a filter\nThe API supports the ability to specify multiple values for the [video](https://developers.google.com/youtube/reporting#supported-reports), [playlist](https://developers.google.com/youtube/reporting#supported-reports), and [channel](https://developers.google.com/youtube/reporting#supported-reports) filters. To do so, specify a separated list of the video, playlist, or channel IDs for which the API response should be filtered. For example, a filters parameter value of `{\"video\":\"pd1FJh59zxQ,Zhawgd0REhA\",\"country\":\"IT\"}` restricts the result set to include data for the given videos in Italy. The parameter value can specify up to 500 IDs. For more details on the filters parameter, see the filters parameter in [Parameters](https://developers.google.com/youtube/analytics/reference/reports/query#Parameters) section.", + optional: true, + }, +}; diff --git a/components/youtube_analytics_api/common/utils.mjs b/components/youtube_analytics_api/common/utils.mjs new file mode 100644 index 0000000000000..2f7e4f69c0054 --- /dev/null +++ b/components/youtube_analytics_api/common/utils.mjs @@ -0,0 +1,57 @@ +import { ConfigurationError } from "@pipedream/platform"; + +const parseJson = (input) => { + const parse = (value) => { + if (typeof(value) === "string") { + try { + return parseJson(JSON.parse(value)); + } catch (e) { + return value; + } + } else if (typeof(value) === "object" && value !== null) { + return Object.entries(value) + .reduce((acc, [ + key, + val, + ]) => Object.assign(acc, { + [key]: parse(val), + }), {}); + } + return value; + }; + + return parse(input); +}; + +function parseArray(value) { + try { + if (!value) { + return; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +function arrayToCommaSeparatedList(array, char = ",") { + return parseArray(array)?.join(char); +} + +export default { + parseJson, + parseArray, + arrayToCommaSeparatedList, +}; diff --git a/components/youtube_analytics_api/package.json b/components/youtube_analytics_api/package.json new file mode 100644 index 0000000000000..c19490cbb9ffd --- /dev/null +++ b/components/youtube_analytics_api/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/youtube_analytics_api", + "version": "0.0.1", + "description": "Pipedream Youtube Analytics API Components", + "main": "youtube_analytics_api.app.mjs", + "keywords": [ + "pipedream", + "youtube_analytics_api" + ], + "homepage": "https://pipedream.com/apps/youtube_analytics_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/youtube_analytics_api/youtube_analytics_api.app.mjs b/components/youtube_analytics_api/youtube_analytics_api.app.mjs index 75ef088ab620a..ed86d861eb08d 100644 --- a/components/youtube_analytics_api/youtube_analytics_api.app.mjs +++ b/components/youtube_analytics_api/youtube_analytics_api.app.mjs @@ -1,11 +1,64 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "youtube_analytics_api", - propDefinitions: {}, + propDefinitions: { + endDate: { + type: "string", + label: "End Date", + description: "The end date for fetching YouTube Analytics data. The value should be in `YYYY-MM-DD` format. The API response contains data up until the last day for which all metrics in the query are available at the time of the query. So, for example, if the request specifies an end date of July 5, 2017, and values for all of the requested metrics are only available through July 3, 2017, that will be the last date for which data is included in the response. (That is true even if data for some of the requested metrics is available for July 4, 2017.)", + }, + startDate: { + type: "string", + label: "Start Date", + description: "The start date for fetching YouTube Analytics data. The value should be in `YYYY-MM-DD` format.", + }, + dimensions: { + type: "string[]", + label: "Dimensions", + description: "A list of YouTube Analytics dimensions, such as `video` or `ageGroup`, `gender`. See the documentation for [channel reports](https://developers.google.com/youtube/analytics/channel_reports) or [content owner reports](https://developers.google.com/youtube/analytics/content_owner_reports) for a list of the reports that you can retrieve and the dimensions used for those reports. (The [Dimensions](https://developers.google.com/youtube/reporting#dimensions) document contains definitions for all of the dimensions.).", + optional: true, + options: Object.values(constants.DIMENSION), + }, + sort: { + type: "string[]", + label: "Sort", + description: "A list of dimensions or metrics that determine the sort order for YouTube Analytics data. By default the sort order is ascending. The `-` prefix causes descending sort order. Eg. `-views`.", + optional: true, + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "The maximum number of rows to include in the response.", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `https://youtubeanalytics.googleapis.com/v2${path}`; + }, + getHeaders() { + return { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "Accept": "application/json", + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + reportsQuery(args = {}) { + return this._makeRequest({ + path: "/reports", + ...args, + }); }, }, }; diff --git a/components/youtube_analytics_api_custom_app/README.md b/components/youtube_analytics_api_custom_app/README.md index 3bf8f2371f49e..89dc219757dd4 100644 --- a/components/youtube_analytics_api_custom_app/README.md +++ b/components/youtube_analytics_api_custom_app/README.md @@ -1,16 +1,11 @@ # Overview -The YouTube Analytics API is an incredibly powerful tool for developers and -businesses. This API allows developers to access rich statistics about their -YouTube channel and videos, allowing them to create custom applications and -tools to better understand their viewers, customers, and markets. With the -YouTube Analytics API, you can build: +The YouTube (Analytics API) - Custom App on Pipedream enables content creators, marketers, and developers to deeply understand the performance of their YouTube channels and videos through automated data retrieval. By tapping into metrics like view counts, likes, comments, and watch time, users can craft strategies to optimize their content and increase engagement. With Pipedream's serverless platform, these insights can trigger workflows, inform content creators in real-time, and integrate smoothly with other apps for a seamless data processing experience. -- Analytics dashboards that display insights from various YouTube data sources -- Custom reports with data from specific YouTube searches or channels -- Interactive data visualizations to better understand critical trends -- Automated tools for monitoring and measuring success -- Alerts for tracking changes in channel engagement -- Applications to identify and track influencers in the market -- Machine learning models to detect trends based on user data -- And much more. +# Example Use Cases + +- **Channel Performance Dashboard Automation**: Automate the collection of YouTube Analytics data to feed into a Google Sheets dashboard. Use the YouTube Analytics API to retrieve daily stats on video performance, channel views, and subscriber counts. Use Google Sheets actions in Pipedream to insert this data into a spreadsheet, providing a real-time overview of channel health. + +- **Subscriber Engagement Alerting**: Set up a workflow to monitor new comments and likes on your most recent videos. The YouTube Analytics API can fetch these metrics, and with Pipedream's conditional logic, you can trigger email notifications using the SendGrid app when certain engagement thresholds are met, keeping you closely connected to your audience's pulse. + +- **Content Optimization Insights**: Combine the YouTube Analytics data with natural language processing (NLP) to analyze comments sentiment on your videos. Use the YouTube Analytics API to collect comments, then pass them to an NLP service like Google Cloud's Natural Language API via Pipedream. Integrate the results back into your content strategy to produce more of what your audience loves. diff --git a/components/youtube_analytics_api_custom_app/package.json b/components/youtube_analytics_api_custom_app/package.json new file mode 100644 index 0000000000000..29927cf3b30fb --- /dev/null +++ b/components/youtube_analytics_api_custom_app/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/youtube_analytics_api_custom_app", + "version": "0.6.0", + "description": "Pipedream youtube_analytics_api_custom_app Components", + "main": "youtube_analytics_api_custom_app.app.mjs", + "keywords": [ + "pipedream", + "youtube_analytics_api_custom_app" + ], + "homepage": "https://pipedream.com/apps/youtube_analytics_api_custom_app", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/youtube_data_api/README.md b/components/youtube_data_api/README.md index 0ed9378d48367..f25340b0d7b09 100644 --- a/components/youtube_data_api/README.md +++ b/components/youtube_data_api/README.md @@ -1,10 +1,11 @@ # Overview -There are lots of things you can build using the YouTube API! Here are just a -few examples: - -- A YouTube video player -- A YouTube search engine -- A tool to help you find popular videos on YouTube -- A way to keep track of your favorite YouTube videos -- A way to find new and interesting YouTube channels to watch +The YouTube Data API lets you incorporate functions normally executed on the YouTube website into your own website or application. You can perform operations like searching for videos, retrieving channel data, and managing playlists. When integrated with Pipedream's serverless platform, this API can be part of automations that react to events, synchronize YouTube data with other services, or generate custom reports. + +# Example Use Cases + +- **Automated Video Reporting**: Create a workflow that triggers weekly to fetch new videos from a specific YouTube channel using the YouTube Data API and compiles a report with views and engagement data. This report can then be sent to an email or a Slack channel. + +- **Dynamic Playlist Management**: Build an automation that monitors a Google Sheet for new rows containing video URLs, and using the YouTube Data API, adds those videos to a designated YouTube playlist. This is ideal for collaborative video collection or content curation projects. + +- **Social Media Cross-Posting**: Design a workflow that watches for new videos on your YouTube channel, then uses the YouTube Data API to get the video details and post them to Twitter, LinkedIn, or another social platform, broadening your content's reach. diff --git a/components/youtube_data_api_custom_app/README.md b/components/youtube_data_api_custom_app/README.md index 6b5eeb69d3361..0607295ad435b 100644 --- a/components/youtube_data_api_custom_app/README.md +++ b/components/youtube_data_api_custom_app/README.md @@ -1,16 +1,11 @@ # Overview - What can I build using YouTube's Data API? +The YouTube (Data API) - Custom App API on Pipedream lets you wield the vast capabilities of YouTube's platform directly within your automated workflows. Leverage this API to manage channels, playlists, subscriptions, and videos. You can automate video uploads, sync channel data with other platforms, analyze metrics, and engage with your audience without manual intervention. Utilize the power of serverless and event-driven architecture to respond to video events in real-time, enrich your marketing strategies, and maintain an active, data-informed YouTube presence. -The YouTube Data API (also known as the YouTube Custom App API) is a powerful tool that enables developers to build applications that interact with YouTube's platform. With it, developers can create apps that display data about the videos and channels on YouTube, fetch metadata about specific videos, modify existing videos, and much more. +# Example Use Cases -Some of the many possibilities enabled by the YouTube Data API include: +- **Video Upload Automation**: Automate the process of video uploads. Once a new video file is dropped into a designated cloud storage service like Google Drive or Dropbox, Pipedream can detect the new file, trigger a workflow, and use the YouTube Data API to upload the video to your channel, setting titles, descriptions, and tags based on predefined templates or metadata from the file. -- Building applications that search for relevant videos and channels within YouTube -- Displaying detailed information about specific videos and channels -- Modifying existing videos -- Uploading videos to YouTube -- Constructing an application that categorizes videos and channels by topics -- Constructing an application that shows video comments -- Creating an object detection detection application that can detect objects present in a video -- Fetching metadata about videos and channels +- **YouTube Channel Analytics Dashboard**: Create a custom analytics dashboard by pulling data from YouTube's API to track views, likes, comments, and subscriber counts. With Pipedream, you can schedule this as a periodic workflow that extracts the latest metrics from your YouTube channel and pushes them to a Google Sheet or a dashboard app like Geckoboard, keeping you constantly informed with up-to-date channel performance insights. + +- **Automated Comment Moderation**: Set up a workflow that monitors new comments on your YouTube videos for specific keywords or phrases that may indicate spam or inappropriate content. Use the YouTube Data API with Pipedream to automatically flag, hide, or delete these comments, and if necessary, notify you via email or messaging apps like Slack for manual review, ensuring your community remains positive and engaging. diff --git a/components/yr/README.md b/components/yr/README.md new file mode 100644 index 0000000000000..b83b3717c0945 --- /dev/null +++ b/components/yr/README.md @@ -0,0 +1,11 @@ +# Overview + +The Yr API provides meteorological data, allowing you to access weather forecasts, historical data, and various meteorological elements for locations worldwide. On Pipedream, you can leverage this data in serverless workflows, creating custom automations that react to weather changes, integrate with other services, or provide timely weather updates. + +# Example Use Cases + +- **Weather-Based Content Publishing**: Automatically post weather updates or warnings to social media platforms like Twitter or Facebook when the Yr API reports certain conditions, like severe weather alerts or significant temperature changes. + +- **Smart Home Automation**: Integrate the Yr API with smart home devices through Pipedream workflows. For instance, close smart blinds or adjust thermostats in response to the current weather conditions or forecasts retrieved from the Yr API. + +- **Event Planning Notifications**: Use Pipedream to monitor weather forecasts and send email or SMS notifications to event attendees if the weather might impact an outdoor event. This can be done by integrating with email services like SendGrid or messaging apps like Twilio. diff --git a/components/yumpu/README.md b/components/yumpu/README.md new file mode 100644 index 0000000000000..c16e321163ecc --- /dev/null +++ b/components/yumpu/README.md @@ -0,0 +1,11 @@ +# Overview + +The Yumpu API allows you to integrate magazine and flipbook functionalities into your apps and workflows. Using this API, you can upload PDFs, create magazines, manage subscriptions, and access detailed statistics about your publications. With Pipedream, you can automate interactions with the Yumpu API, triggering workflows with events from your apps or scheduled jobs, and connecting Yumpu to a multitude of other services for a seamless data flow. + +# Example Use Cases + +- **Automated Flipbook Creation from New PDF Uploads**: Trigger a workflow in Pipedream when a new PDF is added to a Dropbox folder. The workflow will upload the PDF to Yumpu, creating a new flipbook, and then post the flipbook link to a Slack channel to notify your team. + +- **Subscriber Sync and Email Updates**: When a new subscriber is added to your Mailchimp list, trigger a Pipedream workflow to add that subscriber to a Yumpu subscription. Additionally, send a personalized welcome email with the latest Yumpu magazine link using SendGrid. + +- **Flipbook Performance Reporting**: Schedule a daily Pipedream workflow to fetch statistics from Yumpu for your latest publications. Format the data and insert it into a Google Sheets spreadsheet for easy tracking and visualization. diff --git a/components/z_api/README.md b/components/z_api/README.md new file mode 100644 index 0000000000000..bbce074fbcf46 --- /dev/null +++ b/components/z_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Z-API API facilitates WhatsApp messaging automation, allowing users to send and receive messages, manage contacts, and orchestrate chatbots within the popular messaging platform. On Pipedream, you can leverage this API to create powerful serverless workflows that trigger actions in response to WhatsApp events, automate message flows, and integrate with myriad other services without managing infrastructure. + +# Example Use Cases + +- **Automated Customer Support Responses**: Connect Z-API to a CRM tool like HubSpot on Pipedream. When a new message comes into WhatsApp, trigger a workflow that checks the customer's details in HubSpot and sends a personalized reply or escalation based on their history. + +- **Order Confirmation and Updates**: Sync Z-API with an e-commerce platform such as Shopify. After a customer places an order, use Pipedream to send an order confirmation via WhatsApp, and further send real-time order status updates as they occur. + +- **Event Reminder Notifications**: Combine Z-API with Google Calendar on Pipedream. Set up a workflow that sends reminders to participants via WhatsApp for upcoming events or meetings, and updates them about any changes in schedule or location. diff --git a/components/z_api/package.json b/components/z_api/package.json new file mode 100644 index 0000000000000..fc6c56ed77e1e --- /dev/null +++ b/components/z_api/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/z_api", + "version": "0.0.1", + "description": "Pipedream Z-API Components", + "main": "z_api.app.mjs", + "keywords": [ + "pipedream", + "z_api" + ], + "homepage": "https://pipedream.com/apps/z_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/z_api/z_api.app.mjs b/components/z_api/z_api.app.mjs new file mode 100644 index 0000000000000..f588a73c6ba21 --- /dev/null +++ b/components/z_api/z_api.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "z_api", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/zagomail/README.md b/components/zagomail/README.md new file mode 100644 index 0000000000000..54ff4f9362c6c --- /dev/null +++ b/components/zagomail/README.md @@ -0,0 +1,11 @@ +# Overview + +The Zagomail API enables you to automate email marketing tasks, manage contacts, and streamline campaigns directly from Pipedream. With this API, you can trigger actions based on events, update subscriber lists, send targeted emails, and track campaign performance. Pipedream's serverless platform lets you connect Zagomail with hundreds of other apps to create bespoke workflows, without the need to manage infrastructure. + +# Example Use Cases + +- **Automated Welcome Email Sequence**: Trigger a welcome email sequence in Zagomail when a new user signs up on your platform, using Pipedream's ability to listen for new database entries or form submissions. Ensure a timely and personalized onboarding experience with minimal manual effort. + +- **Dynamic Campaign Management**: Use Pipedream to watch for changes in your CRM, like updated lead statuses, and automatically update subscriber segments in Zagomail. Kick off targeted email campaigns based on these segments to engage customers with relevant content. + +- **E-commerce Abandoned Cart Reminders**: Connect Pipedream with an e-commerce platform, such as Shopify. When a cart is abandoned, trigger a workflow that sends a reminder email through Zagomail, encouraging the customer to complete their purchase. This can significantly improve conversion rates by bringing potential customers back to your store. diff --git a/components/zagomail/package.json b/components/zagomail/package.json index 50d93a9d49255..b1766a2348aca 100644 --- a/components/zagomail/package.json +++ b/components/zagomail/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/zamzar/README.md b/components/zamzar/README.md new file mode 100644 index 0000000000000..7bdaf9b0c8865 --- /dev/null +++ b/components/zamzar/README.md @@ -0,0 +1,11 @@ +# Overview + +The Zamzar API on Pipedream provides powerful file conversion capabilities, allowing you to easily automate the process of converting files between different formats. With Pipedream's serverless platform, you can set up workflows that trigger conversions, handle the completed files, and integrate with other services for a seamless automation experience. From document conversion for data analysis to prepping media files for different platforms, Zamzar's API coupled with Pipedream's robust functionality opens up a world of possibilities for streamlining tasks. + +# Example Use Cases + +- **Automated Document Conversion for Cloud Storage**: When a new document is uploaded to Google Drive, a Pipedream workflow can automatically trigger Zamzar to convert it to a different format (e.g., DOCX to PDF). Once the conversion is finished, the PDF can be saved back to Google Drive or another cloud storage service. + +- **Image Format Standardization for Web Applications**: For a web app that requires images in a specific format, use Pipedream to watch for image uploads. Upon upload, trigger Zamzar to convert images to the required format (e.g., BMP to JPG) and then store the standardized images in an AWS S3 bucket. + +- **Audio File Processing for Podcast Publishing**: Set up a Pipedream workflow to monitor a Dropbox folder for new audio files. Utilize Zamzar to convert these files from various formats to a uniform format (e.g., WAV to MP3) preferred for podcast distribution, and then automatically upload the processed files to a podcast hosting platform. diff --git a/components/zamzar/actions/create-content-file/create-content-file.mjs b/components/zamzar/actions/create-content-file/create-content-file.mjs index 845494c1195f9..8716563a1ed72 100644 --- a/components/zamzar/actions/create-content-file/create-content-file.mjs +++ b/components/zamzar/actions/create-content-file/create-content-file.mjs @@ -5,7 +5,7 @@ export default { key: "zamzar-create-content-file", name: "Create Content File", description: "Creates a file from the provided content. [See the documentation](https://developers.zamzar.com/docs)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { app, diff --git a/components/zamzar/actions/retrieve-job/retrieve-job.mjs b/components/zamzar/actions/retrieve-job/retrieve-job.mjs index edfa9304f37d4..d60d51ba4f5f3 100644 --- a/components/zamzar/actions/retrieve-job/retrieve-job.mjs +++ b/components/zamzar/actions/retrieve-job/retrieve-job.mjs @@ -4,7 +4,7 @@ export default { key: "zamzar-retrieve-job", name: "Retrieve Job", description: "Finds the file that has been processed under the specified job id. [See the documentation](https://developers.zamzar.com/docs)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { app, diff --git a/components/zamzar/actions/start-job-from-file/start-job-from-file.mjs b/components/zamzar/actions/start-job-from-file/start-job-from-file.mjs index 0d49329a5d29d..9c8a66a3cd67d 100644 --- a/components/zamzar/actions/start-job-from-file/start-job-from-file.mjs +++ b/components/zamzar/actions/start-job-from-file/start-job-from-file.mjs @@ -5,7 +5,7 @@ export default { key: "zamzar-start-job-from-file", name: "Start Job From File", description: "Starts a conversion job and upload a source file in a single request. [See the documentation](https://developers.zamzar.com/docs)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { app, diff --git a/components/zamzar/common/utils.mjs b/components/zamzar/common/utils.mjs index 18a39fd07b749..c812fe7712329 100644 --- a/components/zamzar/common/utils.mjs +++ b/components/zamzar/common/utils.mjs @@ -9,7 +9,7 @@ function buildFormData(formData, data, parentKey) { buildFormData(formData, data[key], parentKey && `${parentKey}[${key}]` || key); }); - } else if (data && constants.FILE_PROP_NAMES.some((prop) => prop.includes(parentKey))) { + } else if (data && constants.FILE_PROP_NAMES.some((prop) => parentKey.includes(prop))) { formData.append(parentKey, createReadStream(data)); } else if (data) { diff --git a/components/zamzar/package.json b/components/zamzar/package.json index 403211653050e..1422f5753baed 100644 --- a/components/zamzar/package.json +++ b/components/zamzar/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/zamzar", - "version": "0.1.0", + "version": "0.1.1", "description": "Pipedream Zamzar Components", "main": "zamzar.app.mjs", "keywords": [ @@ -13,7 +13,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.5.1", + "@pipedream/platform": "^1.6.2", "form-data": "^4.0.0", "fs": "^0.0.1-security" } diff --git a/components/zamzar/sources/new-conversion-job/new-conversion-job.mjs b/components/zamzar/sources/new-conversion-job/new-conversion-job.mjs index 4d209da7a0ff4..e7a57991c2169 100644 --- a/components/zamzar/sources/new-conversion-job/new-conversion-job.mjs +++ b/components/zamzar/sources/new-conversion-job/new-conversion-job.mjs @@ -6,7 +6,7 @@ export default { key: "zamzar-new-conversion-job", name: "New Conversion Job", description: "Emit new event when a conversion job has completed. [See the documentation](https://developers.zamzar.com/docs)", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "last", props: { diff --git a/components/zamzar/sources/new-file/new-file.mjs b/components/zamzar/sources/new-file/new-file.mjs index 91007d583a99d..4d3b23d13bcfc 100644 --- a/components/zamzar/sources/new-file/new-file.mjs +++ b/components/zamzar/sources/new-file/new-file.mjs @@ -6,7 +6,7 @@ export default { key: "zamzar-new-file", name: "New File", description: "Emit new event when a file has been converted successfully. [See the documentation](https://developers.zamzar.com/docs)", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "greatest", methods: { diff --git a/components/zamzar/sources/watch-completed-conversion-jobs/watch-completed-conversion-jobs.mjs b/components/zamzar/sources/watch-completed-conversion-jobs/watch-completed-conversion-jobs.mjs index 3738eb19b8c17..73220b397af08 100644 --- a/components/zamzar/sources/watch-completed-conversion-jobs/watch-completed-conversion-jobs.mjs +++ b/components/zamzar/sources/watch-completed-conversion-jobs/watch-completed-conversion-jobs.mjs @@ -6,7 +6,7 @@ export default { key: "zamzar-watch-completed-conversion-jobs", name: "Watch Completed Conversion Jobs", description: "Emit new event as soon as a conversion job has been completed. [See the documentation](https://developers.zamzar.com/docs)", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "greatest", methods: { diff --git a/components/zendesk/README.md b/components/zendesk/README.md index 4be7045813997..2a422dac991c1 100644 --- a/components/zendesk/README.md +++ b/components/zendesk/README.md @@ -1,19 +1,94 @@ # Overview -Using the Zendesk API, you can build custom apps and integrations to automate -processes and help your teams build better customer relationships. - -The API provides a range of methods to interact with your customer help desk, -customer data, and customer communication tools. This enables you to create -custom customer experiences that are tailored to your business needs. - -Some examples of what you can build using the Zendesk API include: - -- Create a custom customer feedback widget -- Automate customer service processes -- Identify customer trends and insights -- Help manage customer data in real-time -- Create a custom customer profile and segmentation tool -- Trigger automated messages based on customer events -- Integrate customer service processes with your company’s backend system -- Create a custom customer self-service portal +The Zendesk API enables seamless integration of Zendesk's customer service platform with your existing business processes and third-party applications. By leveraging this API with Pipedream, you can automate ticket tracking, sync customer data, escalate issues, and streamline communication across multiple channels. This can significantly increase efficiency, accelerate response times, and enhance the overall customer experience. Automations can range from simple notifications to complex workflows involving data transformation and multi-step actions across various services. + +# Example Use Cases + +**Ticket Management Automation** +Automatically create Zendesk tickets from emails, chat messages, or form submissions captured in other apps like Gmail or Slack. Use Pipedream to parse the incoming information and create a ticket in Zendesk with the appropriate tags, priorities, and assignments. + +**Customer Feedback Loop** +After a ticket is resolved, trigger a workflow to send a follow-up survey using a platform like Typeform. Record responses back in Zendesk to ensure customer feedback influences service quality. An automated workflow could tag the ticket with the feedback score or add notes for support agents. + +**Real-Time Notifications for Critical Issues** +Set up a Pipedream workflow that monitors Zendesk for tickets with 'Urgent' priority or specific keywords and sends instant notifications to a dedicated Slack channel or via SMS through Twilio. This ensures that critical issues are promptly addressed by support teams. + +# Getting Started + +First, log in to your [Pipedream workspace](https://pipedream.com), then connect Zendesk either through a step or trigger in a workflow, or directly from the Connected Accounts page in Pipedream. + +You'll first be prompted to enter your Zendesk subdomain. You can find this in the URL after logging into Zendesk. + +The subdomain is the portion of the URL *before* `zendesk.com`. + +For example, if the subdomain is `pipedream1903`, that's what you would enter in Pipedream. + +![Example of finding the Zendesk subdomain from the URL while logged into Zendesk](https://res.cloudinary.com/pipedreamin/image/upload/v1715183755/marketplace/apps/zendesk/CleanShot_2024-05-08_at_11.44.08_2x_ogzhhj.png) + +Next, you'll be prompted to connect your Zendesk account. Zendesk will ask if you'd like to grant Pipedream permission to perform actions on your account; accept these permissions to continue. + +And that's it! You can now automate Zendesk actions from within Pipedream workflows. + +# Troubleshooting + +## Status Codes +Responses may have the status codes described in the following sections. + +### 200 range +The request was successful. The status is 200 for successful GET and PUT requests, 201 for most POST requests, and 204 for DELETE requests. + +### 400 range +The request was not successful. The content type of the response may be text/plain for API-level error messages such as trying to call the API without SSL. The content type is application/json for business-level error messages because the response includes a JSON object with information about the error: + +```json +{ + "details": { + "value": [ + { + "type": "blank", + "description": "can't be blank" + }, + { + "type": "invalid", + "description": " is not properly formatted" + } + ] + }, + "description": "RecordValidation errors", + "error": "RecordInvalid" +} +``` + +If you see a response from a known endpoint that looks like plain text, you probably made a syntax error in your request. This type of response commonly occurs when making a request to a nonexistent Zendesk Support instance. + +### **403** + +A 403 response means the server has determined the user or the account doesn’t have the required permissions to use the API. + +### **409** + +A 409 response indicates a conflict with the resource you're trying to create or update. + +409 errors typically occur when two or more requests try to create or change the same resource simultaneously. While Zendesk APIs can handle concurrent requests, requests shouldn't change the same resource at the same time. To avoid 409 errors, serialize requests when possible. If you receive a 409 error, you can retry your request after resolving the conflict. + +The Zendesk Ticketing API provides specific parameters to prevent conflicts when updating tickets. For more information, see Protecting against ticket update collisions. + +### **422 Unprocessable Entity** + +A 422 response means that the content type and the syntax of the request entity are correct, but the content itself is not processable by the server. This is usually due to the request entity not being relevant to the resource that it's trying to create or update. Example: Trying to close a ticket that's already closed. + +### **429** + +A 429 error indicates that a usage limit has been exceeded. See the [Zendesk Rate limits](https://developer.zendesk.com/api-reference/introduction/rate-limits/). + +### **500 range** + +If you ever experience responses with status codes in the 500 range, the Zendesk API may be experiencing internal issues or having a scheduled maintenance during which you might receive a 503 Service Unavailable status code. + +A 503 response with a `Retry-After` header indicates a database timeout or deadlock. You can retry your request after the number of seconds specified in the `Retry-After` header. + +If the 503 response doesn't have a Retry-After header, Zendesk Support may be experiencing internal issues or undergoing scheduled maintenance. In such cases, check `@zendeskops` and our status page for any known issues. + +When building an API client, we recommend treating any 500 status codes as a warning or temporary state. However, if the status persists and if Zendesk doesn't have a publicly announced maintenance or service disruption, contact the [Zendesk Customer Support](https://support.zendesk.com/hc/en-us/articles/360026614173). + +If submitting a ticket to Zendesk, provide the `X-Zendesk-Request-Id` header included in the HTTP response. This helps the Support team track down the request in the logs more quickly. diff --git a/components/zendesk_sell/actions/create-contact/create-contact.mjs b/components/zendesk_sell/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..d4e2c8fd37a18 --- /dev/null +++ b/components/zendesk_sell/actions/create-contact/create-contact.mjs @@ -0,0 +1,91 @@ +import zendeskSell from "../../zendesk_sell.app.mjs"; + +export default { + key: "zendesk_sell-create-contact", + name: "Create Contact", + description: "Creates a new contact. [See the documentation](https://developer.zendesk.com/api-reference/sales-crm/resources/contacts/#create-a-contact).", + type: "action", + version: "0.0.1", + props: { + zendeskSell, + isOrganization: { + propDefinition: [ + zendeskSell, + "isOrganization", + ], + reloadProps: true, + }, + status: { + propDefinition: [ + zendeskSell, + "status", + ], + }, + title: { + propDefinition: [ + zendeskSell, + "title", + ], + }, + description: { + propDefinition: [ + zendeskSell, + "description", + ], + }, + email: { + propDefinition: [ + zendeskSell, + "email", + ], + }, + phone: { + propDefinition: [ + zendeskSell, + "phone", + ], + }, + }, + async additionalProps() { + const props = {}; + if (this.isOrganization) { + props.name = { + type: "string", + label: "Name", + description: "Name of the contact", + }; + } else { + props.firstName = { + type: "string", + label: "First Name", + description: "First name of the contact", + }; + props.lastName = { + type: "string", + label: "Last Name", + description: "Last name of the contact", + }; + } + return props; + }, + async run({ $ }) { + const response = await this.zendeskSell.createContact({ + $, + data: { + data: { + is_organization: this.isOrganization, + name: this.name, + first_name: this.firstName, + last_name: this.lastName, + customer_status: this.status, + title: this.title, + description: this.description, + email: this.email, + phone: this.phone, + }, + }, + }); + $.export("$summary", `Successfully created contact with ID ${response.data.id}`); + return response; + }, +}; diff --git a/components/zendesk_sell/actions/create-lead/create-lead.mjs b/components/zendesk_sell/actions/create-lead/create-lead.mjs new file mode 100644 index 0000000000000..42a855752168f --- /dev/null +++ b/components/zendesk_sell/actions/create-lead/create-lead.mjs @@ -0,0 +1,96 @@ +import zendeskSell from "../../zendesk_sell.app.mjs"; + +export default { + key: "zendesk_sell-create-lead", + name: "Create Lead", + description: "Creates a new lead. [See the documentation](https://developer.zendesk.com/api-reference/sales-crm/resources/leads/#create-a-lead).", + type: "action", + version: "0.0.1", + props: { + zendeskSell, + isOrganization: { + propDefinition: [ + zendeskSell, + "isOrganization", + ], + description: "Indicator of whether or not this lead refers to an organization or an individual", + reloadProps: true, + }, + status: { + propDefinition: [ + zendeskSell, + "status", + ], + description: "The status of the lead", + }, + title: { + propDefinition: [ + zendeskSell, + "title", + ], + description: "The lead’s job title", + }, + description: { + propDefinition: [ + zendeskSell, + "description", + ], + description: "The lead’s description", + }, + email: { + propDefinition: [ + zendeskSell, + "email", + ], + description: "The lead’s email address", + }, + phone: { + propDefinition: [ + zendeskSell, + "phone", + ], + description: "The lead’s phone number", + }, + }, + async additionalProps() { + const props = {}; + if (this.isOrganization) { + props.name = { + type: "string", + label: "Name", + description: "Name of the lead", + }; + } else { + props.firstName = { + type: "string", + label: "First Name", + description: "First name of the lead", + }; + props.lastName = { + type: "string", + label: "Last Name", + description: "Last name of the lead", + }; + } + return props; + }, + async run({ $ }) { + const response = await this.zendeskSell.createLead({ + $, + data: { + data: { + first_name: this.firstName, + last_name: this.lastName, + organization_name: this.name, + status: this.status, + title: this.title, + description: this.description, + email: this.email, + phone: this.phone, + }, + }, + }); + $.export("$summary", `Successfully created lead with ID ${response.data.id}`); + return response; + }, +}; diff --git a/components/zendesk_sell/actions/create-task/create-task.mjs b/components/zendesk_sell/actions/create-task/create-task.mjs new file mode 100644 index 0000000000000..dac6db39968b2 --- /dev/null +++ b/components/zendesk_sell/actions/create-task/create-task.mjs @@ -0,0 +1,86 @@ +import zendeskSell from "../../zendesk_sell.app.mjs"; + +export default { + key: "zendesk_sell-create-task", + name: "Create Task", + description: "Creates a new task. [See the documentation](https://developer.zendesk.com/api-reference/sales-crm/resources/tasks/#create-a-task).", + type: "action", + version: "0.0.1", + props: { + zendeskSell, + resourceType: { + type: "string", + label: "Resource Type", + description: "Name of the resource type the task is attached to", + options: [ + "contact", + "lead", + "deal", + ], + reloadProps: true, + }, + contactId: { + propDefinition: [ + zendeskSell, + "contactId", + ], + hidden: true, + }, + leadId: { + propDefinition: [ + zendeskSell, + "leadId", + ], + hidden: true, + }, + dealId: { + propDefinition: [ + zendeskSell, + "dealId", + ], + hidden: true, + }, + content: { + type: "string", + label: "Content", + description: "Content of the task", + }, + completed: { + type: "boolean", + label: "Completed", + description: "Indicator of whether the task is completed or not", + optional: true, + }, + dueDate: { + type: "string", + label: "Due Date", + description: "Date and time the task is due in UTC (ISO8601 format)", + optional: true, + }, + }, + async additionalProps(props) { + props.contactId.hidden = this.resourceType !== "contact"; + props.leadId.hidden = this.resourceType !== "lead"; + props.dealId.hidden = this.resourceType !== "deal"; + return {}; + }, + async run({ $ }) { + const response = await this.zendeskSell.createTask({ + $, + data: { + data: { + resource_type: this.resourceType, + resource_id: this.resourceType === "contact" + ? this.contactId + : this.resourceType === "lead" + ? this.leadId + : this.dealId, + content: this.content, + completed: this.completed, + due_date: this.dueDate, + }, + }, + }); + return response; + }, +}; diff --git a/components/zendesk_sell/package.json b/components/zendesk_sell/package.json new file mode 100644 index 0000000000000..c445f120a18a7 --- /dev/null +++ b/components/zendesk_sell/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/zendesk_sell", + "version": "0.1.0", + "description": "Pipedream Zendesk Sell Components", + "main": "zendesk_sell.app.mjs", + "keywords": [ + "pipedream", + "zendesk_sell" + ], + "homepage": "https://pipedream.com/apps/zendesk_sell", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/zendesk_sell/sources/common/base.mjs b/components/zendesk_sell/sources/common/base.mjs new file mode 100644 index 0000000000000..97b9a92952d86 --- /dev/null +++ b/components/zendesk_sell/sources/common/base.mjs @@ -0,0 +1,86 @@ +import zendeskSell from "../../zendesk_sell.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + zendeskSell, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + getTsField() { + return "created_at"; + }, + getParams() { + return { + sort_by: `${this.getTsField()}:desc`, + }; + }, + generateMeta(item) { + return { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item[this.getTsField()]), + }; + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + const fn = this.getResourceFn(); + const params = this.getParams(); + const tsField = this.getTsField(); + + const results = this.zendeskSell.paginate({ + fn, + params, + max, + }); + + const items = []; + for await (const result of results) { + const { data: item } = result; + const ts = Date.parse(item[tsField]); + if (ts >= lastTs) { + items.push(item); + } else { + break; + } + } + + if (!items?.length) { + return; + } + + this._setLastTs(Date.parse(items[0][tsField])); + + items.forEach((item) => { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/zendesk_sell/sources/new-contact-created/new-contact-created.mjs b/components/zendesk_sell/sources/new-contact-created/new-contact-created.mjs new file mode 100644 index 0000000000000..f9b978d665624 --- /dev/null +++ b/components/zendesk_sell/sources/new-contact-created/new-contact-created.mjs @@ -0,0 +1,20 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "zendesk_sell-new-contact-created", + name: "New Contact Created", + description: "Emit new event when a new contact is created in Zendesk Sell.", + type: "source", + version: "0.0.1", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.zendeskSell.listContacts; + }, + getSummary(contact) { + return `New Contact ID: ${contact.id}`; + }, + }, +}; diff --git a/components/zendesk_sell/sources/new-deal-created/new-deal-created.mjs b/components/zendesk_sell/sources/new-deal-created/new-deal-created.mjs new file mode 100644 index 0000000000000..317231798499c --- /dev/null +++ b/components/zendesk_sell/sources/new-deal-created/new-deal-created.mjs @@ -0,0 +1,20 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "zendesk_sell-new-deal-created", + name: "New Deal Created", + description: "Emit new event when a new deal is created in Zendesk Sell.", + type: "source", + version: "0.0.1", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.zendeskSell.listDeals; + }, + getSummary(deal) { + return `New Deal ID: ${deal.id}`; + }, + }, +}; diff --git a/components/zendesk_sell/sources/new-lead-created/new-lead-created.mjs b/components/zendesk_sell/sources/new-lead-created/new-lead-created.mjs new file mode 100644 index 0000000000000..7af498231679a --- /dev/null +++ b/components/zendesk_sell/sources/new-lead-created/new-lead-created.mjs @@ -0,0 +1,20 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "zendesk_sell-new-lead-created", + name: "New Lead Created", + description: "Emit new event when a new lead is created in Zendesk Sell.", + type: "source", + version: "0.0.1", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.zendeskSell.listLeads; + }, + getSummary(lead) { + return `New Lead ID: ${lead.id}`; + }, + }, +}; diff --git a/components/zendesk_sell/zendesk_sell.app.mjs b/components/zendesk_sell/zendesk_sell.app.mjs new file mode 100644 index 0000000000000..09c30bf5a7820 --- /dev/null +++ b/components/zendesk_sell/zendesk_sell.app.mjs @@ -0,0 +1,173 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "zendesk_sell", + propDefinitions: { + contactId: { + type: "string", + label: "Contact ID", + description: "Identifier of a contact", + async options({ page }) { + const { items } = await this.listContacts({ + page: page + 1, + }); + return items?.map(({ data }) => ({ + value: data.id, + label: data.name || (`${data.first_name} ${data.last_name}`).trim(), + })) || []; + }, + }, + leadId: { + type: "string", + label: "Lead ID", + description: "Identifier of a lead", + async options({ page }) { + const { items } = await this.listLeads({ + page: page + 1, + }); + return items?.map(({ data }) => ({ + value: data.id, + label: data.organization_name || (`${data.first_name} ${data.last_name}`).trim(), + })) || []; + }, + }, + dealId: { + type: "string", + label: "Deal ID", + description: "Identifier of a deal", + async options({ page }) { + const { items } = await this.listDeals({ + page: page + 1, + }); + return items?.map(({ data }) => ({ + value: data.id, + label: data.name, + })) || []; + }, + }, + isOrganization: { + type: "boolean", + label: "Is Organization", + description: "Indicator of whether or not this contact refers to an organization or an individual", + }, + status: { + type: "string", + label: "Status", + description: "The customer status of the contact", + options: [ + "none", + "current", + "past", + ], + optional: true, + }, + title: { + type: "string", + label: "Title", + description: "The contact’s job title", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "The contact’s description", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "The contact’s email address", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The contact’s phone number", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://api.getbase.com/v2"; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + accept: "application/json", + }, + ...opts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts", + ...opts, + }); + }, + listLeads(opts = {}) { + return this._makeRequest({ + path: "/leads", + ...opts, + }); + }, + listDeals(opts = {}) { + return this._makeRequest({ + path: "/deals", + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contacts", + ...opts, + }); + }, + createLead(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/leads", + ...opts, + }); + }, + createTask(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/tasks", + ...opts, + }); + }, + async *paginate({ + fn, + params, + max, + }) { + params = { + ...params, + per_page: 100, + page: 1, + }; + let total, count = 0; + do { + const { items } = await fn({ + params, + }); + for (const item of items) { + yield item; + if (max && ++count >= max) { + return; + } + } + total = items?.length; + params.page++; + } while (total); + }, + }, +}; diff --git a/components/zenkit/README.md b/components/zenkit/README.md index 3bb2098e95982..fb0e17c67ca60 100644 --- a/components/zenkit/README.md +++ b/components/zenkit/README.md @@ -1,21 +1,11 @@ # Overview -Zenkit is a powerful online database and project management tool used by -thousands of businesses and organizations worldwide. It provides a wide range -of tools and data management capabilities, along with its own API that allows -developers and companies to build custom applications and integrations. With -the Zenkit API, you can build: +Zenkit is a versatile project management and collaboration tool that lets you organize your data and build custom workflows to streamline your business and personal projects. With the Zenkit API, you can create, read, update, and delete items in your collections, manage users, and automate notifications or actions based on changes within your datasets. Combining Zenkit's API with Pipedream's capability unlocks powerful automation opportunities, allowing you to interconnect your project data with numerous other services and internal systems to enhance productivity, data consistency, and event-driven processes. -- Collaborative to-do lists and productivity apps -- Custom reports and dashboards -- Event booking platforms -- Team collaboration tools -- Automated notifications and alerts -- Task management systems -- Data mining and analysis software -- Inventory tracking systems -- CRM and customer data management platforms -- Online shopping carts and payment systems -- Cloud-based file storage and sharing services -- Mobile application development platforms -- Digital marketing and analytics solutions +# Example Use Cases + +- **Sync Tasks with Calendar**: Create a workflow on Pipedream that takes new or updated tasks from Zenkit and syncs them with Google Calendar. Any changes in task due dates or descriptions in Zenkit would automatically update the corresponding calendar event, ensuring your schedule is always up-to-date. + +- **Customer Support Ticketing**: Build an automation where new support tickets submitted via a Zenkit form are instantly posted to a Slack channel. This allows for rapid response by your support team. Additionally, the workflow could update the ticket status in Zenkit once it has been acknowledged or resolved by the team. + +- **Sales Lead Tracking**: Design a Pipedream workflow that connects Zenkit with a CRM system like Salesforce. When a new sales lead is entered into a Zenkit collection, it triggers the workflow, which then creates or updates the lead information in Salesforce. This ensures that your sales team has the latest data without manual entry, leading to more efficient lead management. diff --git a/components/zenler/README.md b/components/zenler/README.md index f8b4de616d172..84e9fe7ba9308 100644 --- a/components/zenler/README.md +++ b/components/zenler/README.md @@ -1,25 +1,11 @@ # Overview -With the Zenler API, you can transform the way you and your learners access, -experience, and interact with content. Here are just some of the things you can -build with Zenler: +The Zenler API lets you tap into your online course platform to automate tasks, streamline student engagement, and track course performance. By harnessing the power of the Zenler API on Pipedream, you can create dynamic serverless workflows that respond to course interactions, manage users, and analyze educational content effectiveness, all in real-time. Whether you're looking to enhance the learning experience, or make your course administration more efficient, the Zenler API on Pipedream offers the tools to make it happen. -- Interactive online learning experiences: Create online lessons, modules, and - courses with text, videos, and audio, as well as quizzes, tests, and surveys - that engage your learners. -- Learning Paths: Create personalized learning paths for each learner based on - their interests, progress and learning goals. -- Self-paced Learning: Provide self-paced learning pathways to deliver content - to learners on their own. -- Mobile Learning: Deploy your content to mobile devices, including phones, and - tablets. -- Gamified Learning: Reward learners for engagement, such as completing modules - or earning badges and achievements. -- Virtual Classrooms: Engage learners in real-time or asynchronous discussions - with live audio, video, and chat. -- Offline Support: Download content for offline use and create projects for - learners to work on or collaborate with others in an offline setting. -- Advanced Analytics: Use data-driven optimization to track the usage and - testing results for each individual learner. -- Integration: Integrate with third-party systems and APIs to access and manage - your content with existing solutions. +# Example Use Cases + +- **Automated Student Welcome Messages**: Trigger a personalized welcome email via SendGrid whenever a new user enrolls in a course on Zenler. This workflow ensures that every student receives all the necessary information they need to get started, while also adding a personal touch. + +- **Course Completion Certificates**: Generate and send a certificate using a service like PDF.co when a student completes a course. This workflow can monitor course completions, create a personalized certificate, and then email it to the student, all without manual intervention. + +- **Real-time Course Progress Tracking**: Integrate with a tool like Google Sheets to track student progress in real-time. Each time a student completes a lesson or a module, append their progress to a spreadsheet. This allows for at-a-glance monitoring and can be used to identify students who might need additional support or encouragement. diff --git a/components/zenrows/README.md b/components/zenrows/README.md new file mode 100644 index 0000000000000..724c06c33a4d9 --- /dev/null +++ b/components/zenrows/README.md @@ -0,0 +1,11 @@ +# Overview + +ZenRows API specializes in web scraping and handles issues like CAPTCHAs, JavaScript rendering, and rotating proxies to ensure successful data extraction. In Pipedream, you can pair the ZenRows API with numerous other services to create automated workflows that respond to events, process and analyze scraped data, or even trigger actions based on the data collected. Whether you need to monitor changes on web pages, aggregate content for analysis, or feed scraped data into other applications, ZenRows' integration on Pipedream simplifies these tasks. + +# Example Use Cases + +- **Data Monitoring and Alerting**: Create a Pipedream workflow that uses ZenRows API to regularly scrape data from a target website. Analyze the extracted data for specific criteria or changes, and use Pipedream's built-in actions to send alerts via email, SMS, or chat applications like Slack when those conditions are met. + +- **Content Aggregation and Database Insertion**: Leverage ZenRows API to scrape articles, forum posts, or product listings. Process the scraped content in Pipedream, and then insert the data into a database like MySQL or a cloud platform such as Airtable. This can be useful for market research, competitive analysis, or content curation. + +- **SEO Monitoring and Google Sheets Reporting**: Utilize ZenRows API within Pipedream to scrape SEO-relevant information from web pages such as meta tags, headings, and keyword density. Feed the results into a Google Sheets document for easy reporting and visualization. This workflow can help in tracking SEO performance and making data-driven decisions for website optimization. diff --git a/components/zenscrape/README.md b/components/zenscrape/README.md new file mode 100644 index 0000000000000..edbb36cf1626d --- /dev/null +++ b/components/zenscrape/README.md @@ -0,0 +1,11 @@ +# Overview + +The Zenscrape API offers a versatile way to extract structured data from any webpage without the hassle of managing proxies or parsing HTML. By leveraging Zenscrape on Pipedream, you can automate data extraction, receive real-time results, and integrate scraped data with hundreds of other services. Create workflows that trigger on schedules or events, process the data, and perform actions like updating databases, sending alerts, or generating reports. + +# Example Use Cases + +- **Content Change Detection and Notification**: Use Zenscrape to monitor website changes. When a target web page updates, trigger a Pipedream workflow, scrape the new content, and send notifications via Slack or email to relevant parties. + +- **Competitor Price Monitoring**: Set up a Pipedream workflow that periodically uses Zenscrape to extract pricing information from competitor websites. Analyze the data to adjust your own pricing strategy and automatically update prices in your e-commerce platform like Shopify. + +- **Lead Generation Automation**: Implement a Pipedream workflow that uses Zenscrape to scrape contact information from online directories or industry websites. Then, feed this data into a CRM like Salesforce or send it to Google Sheets for further processing and outreach campaigns. diff --git a/components/zenscrape/actions/get-credit-status/get-credit-status.mjs b/components/zenscrape/actions/get-credit-status/get-credit-status.mjs new file mode 100644 index 0000000000000..22ff1bf4b05d8 --- /dev/null +++ b/components/zenscrape/actions/get-credit-status/get-credit-status.mjs @@ -0,0 +1,19 @@ +import zenscrape from "../../zenscrape.app.mjs"; + +export default { + key: "zenscrape-get-credit-status", + name: "Get Credit Status", + description: "Retrieve the number of remaining credits in Zenscrape. [See the documentation](https://app.zenscrape.com/documentation)", + version: "0.0.1", + type: "action", + props: { + zenscrape, + }, + async run({ $ }) { + const response = await this.zenscrape.getStatus({ + $, + }); + $.export("$summary", "Successfully retrieved credit status."); + return response; + }, +}; diff --git a/components/zenscrape/actions/get-website-content/get-website-content.mjs b/components/zenscrape/actions/get-website-content/get-website-content.mjs new file mode 100644 index 0000000000000..dc1195f15d070 --- /dev/null +++ b/components/zenscrape/actions/get-website-content/get-website-content.mjs @@ -0,0 +1,56 @@ +import zenscrape from "../../zenscrape.app.mjs"; + +export default { + key: "zenscrape-get-website-content", + name: "Get Website Content", + description: "Retrieve the content of a website. [See the documentation](https://app.zenscrape.com/documentation)", + version: "0.0.1", + type: "action", + props: { + zenscrape, + url: { + propDefinition: [ + zenscrape, + "url", + ], + }, + premium: { + propDefinition: [ + zenscrape, + "premium", + ], + }, + location: { + propDefinition: [ + zenscrape, + "location", + ], + }, + keepHeaders: { + propDefinition: [ + zenscrape, + "keepHeaders", + ], + }, + render: { + propDefinition: [ + zenscrape, + "render", + ], + }, + }, + async run({ $ }) { + const response = await this.zenscrape.getContent({ + $, + params: { + url: this.url, + premium: this.premium, + location: this.location, + keep_headers: this.keepHeaders, + render: this.render, + }, + }); + $.export("$summary", `Successfully scraped website \`${this.url}.\``); + return response; + }, +}; diff --git a/components/zenscrape/package.json b/components/zenscrape/package.json index 71b652aa0ff86..71477b31cfd7b 100644 --- a/components/zenscrape/package.json +++ b/components/zenscrape/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/zenscrape", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Zenscrape Components", "main": "zenscrape.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "md5": "^2.3.0" } -} \ No newline at end of file +} diff --git a/components/zenscrape/zenscrape.app.mjs b/components/zenscrape/zenscrape.app.mjs index 4ac729c665b47..0b59e381202fe 100644 --- a/components/zenscrape/zenscrape.app.mjs +++ b/components/zenscrape/zenscrape.app.mjs @@ -1,11 +1,67 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "zenscrape", - propDefinitions: {}, + propDefinitions: { + url: { + type: "string", + label: "URL", + description: "The target site you want to scrape", + }, + premium: { + type: "boolean", + label: "Premium", + description: "Uses residential proxies, unlocks sites that are hard to scrape. Counts as 20 credits towards your quota.", + optional: true, + }, + location: { + type: "string", + label: "Location", + description: "If premium=`false` possible locations are 'na' (North America) and 'eu' (Europe). If premium=`true` you can choose a location from Zenscrape's [list of 230+ countries](https://app.zenscrape.com/documentation#proxyLocationList)", + optional: true, + }, + keepHeaders: { + type: "boolean", + label: "Keep Headers", + description: "Allow to pass through forward headers (e.g. user agents, cookies)", + optional: true, + }, + render: { + type: "boolean", + label: "Render", + description: "Use a headless browser to fetch content that relies on javascript. Counts as 5 credits towards your quota.", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://app.zenscrape.com/api/v1"; + }, + _makeRequest({ + $ = this, + path, + ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + apikey: this.$auth.api_key, + }, + ...opts, + }); + }, + getContent(opts = {}) { + return this._makeRequest({ + path: "/get", + ...opts, + }); + }, + getStatus(opts = {}) { + return this._makeRequest({ + path: "/status", + ...opts, + }); }, }, }; diff --git a/components/zenserp/README.md b/components/zenserp/README.md new file mode 100644 index 0000000000000..6c1cae1c24cbf --- /dev/null +++ b/components/zenserp/README.md @@ -0,0 +1,11 @@ +# Overview + +The Zenserp API enables you to automate search engine queries, parsing SERPs (Search Engine Results Pages) to extract valuable data such as search results, location-based results, and even Google image searches. Within Pipedream's platform, you can harness this API to create workflows that react to various triggers, like webhooks or schedules, to perform automated searches and process the resulting data. This can be powerful for SEO analysis, market research, and content monitoring. Pipedream's serverless architecture makes it seamless to integrate Zenserp with other apps to augment your workflows. + +# Example Use Cases + +- **Automated SEO Monitoring**: Trigger a Pipedream workflow on a schedule to use the Zenserp API to check search rankings for specific keywords. After obtaining the results, the workflow could analyze changes in ranking over time and notify you via Slack or email if significant fluctuations are detected. + +- **Content Change Detection**: Set up a Pipedream workflow that utilizes Zenserp to regularly search for your brand or specific content. If new or unexpected results are found, indicating changes in SERP or new content mentioning your brand, the workflow could alert you or log the information in a Google Sheet for further review. + +- **Market Trend Analysis**: Use a Zenserp-powered workflow in Pipedream to perform daily searches for industry-specific terms to identify trending topics or emerging competitors. The results can then be piped into a data visualization tool like Tableau or a database like Airtable for trend tracking and analysis over time. diff --git a/components/zenserp/package.json b/components/zenserp/package.json new file mode 100644 index 0000000000000..c76e1fd3cb040 --- /dev/null +++ b/components/zenserp/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/zenserp", + "version": "0.0.1", + "description": "Pipedream Zenserp Components", + "main": "zenserp.app.mjs", + "keywords": [ + "pipedream", + "zenserp" + ], + "homepage": "https://pipedream.com/apps/zenserp", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/zenserp/zenserp.app.mjs b/components/zenserp/zenserp.app.mjs new file mode 100644 index 0000000000000..a290d51c2b58f --- /dev/null +++ b/components/zenserp/zenserp.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "zenserp", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/components/zenventory/actions/create-customer-order/create-customer-order.mjs b/components/zenventory/actions/create-customer-order/create-customer-order.mjs new file mode 100644 index 0000000000000..b2d4f6545f087 --- /dev/null +++ b/components/zenventory/actions/create-customer-order/create-customer-order.mjs @@ -0,0 +1,345 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { POSTAGE_ACCOUNT_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import zenventory from "../../zenventory.app.mjs"; + +export default { + key: "zenventory-create-customer-order", + name: "Create Customer Order", + description: "Creates a new customer order. [See the documentation](https://docs.zenventory.com/#tag/customer_order/paths/~1customer-orders/post)", + version: "0.0.1", + type: "action", + props: { + zenventory, + orderNumber: { + type: "string", + label: "Order Number", + description: "Order number for the customer order.", + optional: true, + }, + clientId: { + propDefinition: [ + zenventory, + "clientId", + ], + optional: true, + }, + clientName: { + propDefinition: [ + zenventory, + "clientName", + ], + optional: true, + }, + customerId: { + type: "integer", + label: "Customer Id", + description: "Id of the customer. If none is provided, will attempt to find an existing customer based on the other customer fields and shipping address. Other fields not used if id is provided.", + optional: true, + }, + title: { + type: "string", + label: "Customer Title", + description: "The title of the customer.", + optional: true, + }, + name: { + type: "string", + label: "Customer Name", + description: "A combination of name and surname is required if the customer is new.", + optional: true, + }, + surname: { + type: "string", + label: "Customer Surname", + description: "A combination of name and surname is required if the customer is new.", + optional: true, + }, + email: { + type: "string", + label: "Customer Email", + description: "The email of the customer.", + optional: true, + }, + customerCompany: { + type: "string", + label: "Customer Company", + description: "The company of the customer.", + optional: true, + }, + accountNumber: { + type: "string", + label: "Customer Account Number", + description: "Will only match a customer on this field if provided.", + optional: true, + }, + shippingAddressId: { + type: "integer", + label: "Shipping Address Id", + description: "Id of the shipping address. **Other Shipping fields will not be used if provided.**", + optional: true, + }, + shippingAddressCode: { + type: "string", + label: "Shipping Address Code", + description: "Used to find an existing address. Other fields not used if an address is successfully found.", + optional: true, + }, + shippingAddressCompany: { + type: "string", + label: "Shipping Address Company", + description: "The company of the shipping address.", + optional: true, + }, + shippingAddressName: { + type: "string", + label: "Shipping Address Name", + description: "The name of the shipping address.", + optional: true, + }, + shippingAddressline1: { + type: "string", + label: "Shipping Address Line 1", + description: "The shipping address line 1.", + optional: true, + }, + shippingAddressline2: { + type: "string", + label: "Shipping Address Line 2", + description: "The shipping address line 2.", + optional: true, + }, + shippingAddressline3: { + type: "string", + label: "Shipping Address Line 3", + description: "The shipping address line 3.", + optional: true, + }, + shippingAddressCity: { + type: "string", + label: "Shipping Address City", + description: "The city of the shipping address.", + optional: true, + }, + shippingAddressState: { + type: "string", + label: "Shipping Address State", + description: "The state of the shipping address.", + optional: true, + }, + shippingAddressZip: { + type: "string", + label: "Shipping Address Zip", + description: "The zip of the shipping address.", + optional: true, + }, + shippingAddressCountryCode: { + type: "string", + label: "Shipping Address Country Code", + description: "The country code of the shipping address. [See the ISO 3166 codes](https://www.iban.com/country-codes).", + optional: true, + }, + shippingAddressPhone: { + type: "string", + label: "Shipping Address Phone", + description: "The phone of the shipping address.", + optional: true, + }, + sameAsShipping: { + type: "boolean", + label: "Same As Shipping", + description: "True if the billing address is the same as the shipping address.", + reloadProps: true, + optional: true, + }, + shipFromWarehouseId: { + type: "string", + label: "Ship From Warehouse Id", + description: "Id of the warehouse the ordered items will be allocated from. If no warehouse parameters are given, then the user's current warehouse will be used.", + optional: true, + }, + shipFromWarehouseName: { + type: "string", + label: "Ship From Warehouse Name", + description: "Name of the warehouse the ordered items will be allocated from. Ignored if warehouseId is provided.", + optional: true, + }, + shipVia: { + type: "string", + label: "Ship Via", + description: "Code of the carrier or service to use for shipping.", + optional: true, + }, + postageAccount: { + type: "string", + label: "Postage Account", + description: "Who to bill for shipping.", + options: POSTAGE_ACCOUNT_OPTIONS, + optional: true, + }, + items: { + type: "string[]", + label: "Items", + description: "An array of objects of ordered items. **Example: {\"itemId\": \"123\", \"sku\": \"SKU123\", \"quantity\": 1}** [See the documentation](https://docs.zenventory.com/#tag/customer_order/paths/~1customer-orders/post) fro further information.", + optional: true, + }, + }, + async additionalProps() { + const props = {}; + + if (!this.sameAsShipping) { + props.billingAddressId = { + type: "integer", + label: "Billing Address Id", + description: "Id of the billing address. **Other Billing fields will not be used if provided.**", + optional: true, + }; + props.billingAddressCode = { + type: "string", + label: "Billing Address Code", + description: "Used to find an existing address. Other fields not used if an address is successfully found.", + optional: true, + }; + props.billingAddressCompany = { + type: "string", + label: "Billing Address Company", + description: "The company of the billing address.", + optional: true, + }; + props.billingAddressName = { + type: "string", + label: "Billing Address Name", + description: "The name of the billing address.", + optional: true, + }; + props.billingAddressline1 = { + type: "string", + label: "Billing Address Line 1", + description: "The billing address line 1.", + optional: true, + }; + props.billingAddressline2 = { + type: "string", + label: "Billing Address Line 2", + description: "The billing address line 2.", + optional: true, + }; + props.billingAddressline3 = { + type: "string", + label: "Billing Address Line 3", + description: "The billing address line 3.", + optional: true, + }; + props.billingAddressCity = { + type: "string", + label: "Billing Address City", + description: "The city of the billing address.", + optional: true, + }; + props.billingAddressState = { + type: "string", + label: "Billing Address State", + description: "The state of the billing address.", + optional: true, + }; + props.billingAddressZip = { + type: "string", + label: "Billing Address Zip", + description: "The zip of the billing address.", + optional: true, + }; + props.billingAddressCountryCode = { + type: "string", + label: "Billing Address Country Code", + description: "The country code of the billing address. [See the ISO 3166 codes](https://www.iban.com/country-codes).", + optional: true, + }; + props.billingAddressPhone = { + type: "string", + label: "Billing Address Phone", + description: "The phone of the billing address.", + optional: true, + }; + } + return props; + }, + async run({ $ }) { + if (!this.customerId && + !this.title && + !this.name && + !this.surname && + !this.email && + !this.customerCompany && + !this.accountNumber) { + throw new ConfigurationError("You must provide at least 'Customer Id', 'Customer Title', 'Customer Name', 'Customer Surname', 'Customer Email', 'Customer Company' or 'Customer Account Number'."); + } + + if (!this.shippingAddressId && + !this.shippingAddressline1 && + !this.shippingAddressCountryCode) { + throw new ConfigurationError("You must provide at least 'Shipping Address Id' or 'Shipping Address Line1' and 'Shipping Address Country Code'."); + } + + if (!this.sameAsShipping && + !this.billingAddressId && + !this.billingAddressline1 && + !this.billingAddressCountryCode) { + throw new ConfigurationError("When 'Same As Shipping' is set **False** you must provide at least 'Billing Address Id' or 'Billing Address Line1' and 'Billing Address Country Code'."); + } + + const response = await this.zenventory.createCustomerOrder({ + $, + data: { + orderNumber: this.orderNumber, + clientId: this.clientId, + clientName: this.clientName, + customer: { + id: this.customerId, + title: this.title, + name: this.name, + surname: this.surname, + email: this.email, + company: this.customerCompany, + accountNumber: this.accountNumber, + }, + shippingAddress: { + id: this.shippingAddressId, + code: this.shippingAddressCode, + company: this.shippingAddressCompany, + name: this.shippingAddressName, + line1: this.shippingAddressline1, + line2: this.shippingAddressline2, + line3: this.shippingAddressline3, + city: this.shippingAddressCity, + state: this.shippingAddressState, + zip: this.shippingAddressZip, + countryCode: this.shippingAddressCountryCode, + phone: this.shippingAddressPhone, + }, + billingAddress: { + sameAsShipping: this.sameAsShipping, + id: this.billingAddressId, + code: this.billingAddressCode, + company: this.billingAddressCompany, + name: this.billingAddressName, + line1: this.billingAddressline1, + line2: this.billingAddressline2, + line3: this.billingAddressline3, + city: this.billingAddressCity, + state: this.billingAddressState, + zip: this.billingAddressZip, + countryCode: this.billingAddressCountryCode, + phone: this.billingAddressPhone, + }, + shipFromWarehouseId: this.shipFromWarehouseId, + shipFromWarehouseName: this.shipFromWarehouseName, + shipVia: this.shipVia, + postageAccount: this.postageAccount, + items: parseObject(this.items), + }, + }); + + $.export("$summary", `Successfully created customer order with ID ${response.id}`); + return response; + }, +}; diff --git a/components/zenventory/actions/create-item/create-item.mjs b/components/zenventory/actions/create-item/create-item.mjs new file mode 100644 index 0000000000000..23c74e727ba66 --- /dev/null +++ b/components/zenventory/actions/create-item/create-item.mjs @@ -0,0 +1,284 @@ +import zenventory from "../../zenventory.app.mjs"; + +export default { + key: "zenventory-create-item", + name: "Create Item", + description: "Generates a new item. [See the documentation](https://docs.zenventory.com/#tag/items/paths/~1items/post)", + version: "0.0.1", + type: "action", + props: { + zenventory, + sku: { + type: "string", + label: "SKU", + description: "The item's SKU.", + }, + clientId: { + propDefinition: [ + zenventory, + "clientId", + ], + description: "Id of the client that the item belongs to. Defaults to the user's client id.", + optional: true, + }, + clientName: { + propDefinition: [ + zenventory, + "clientName", + ], + description: "Name of the client that the item belongs to. Ignored if clientId is provided and is nonzero.", + optional: true, + }, + upc: { + type: "string", + label: "UPC", + description: "The item's UPC.", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "The item's description.", + optional: true, + }, + category: { + type: "string", + label: "Category", + description: "The item's category to be grouped by.", + optional: true, + }, + baseUom: { + type: "string", + label: "Base UOM", + description: "The item's base unit of measurement.", + optional: true, + }, + unitCost: { + type: "string", + label: "Unit Cost", + description: "How much the item costs you to stock.", + optional: true, + }, + leadTime: { + type: "integer", + label: "Lead Time", + description: "The item's lead time.", + optional: true, + }, + defaultEconOrder: { + type: "integer", + label: "Default Econ Order", + description: "The item's default order quantity for purchase orders.", + optional: true, + }, + orderLimit: { + type: "integer", + label: "Order Limit", + description: "The item's order limit.", + optional: true, + }, + rrp: { + type: "string", + label: "RRP", + description: "The item's recommended retail price.", + optional: true, + }, + price: { + type: "string", + label: "Price", + description: "How much the item is sold for.", + optional: true, + }, + active: { + type: "boolean", + label: "Active", + description: "True if the item is active. Inactive items are hidden from most processes.", + optional: true, + }, + kit: { + type: "boolean", + label: "Kit", + description: "True if the item is a kit item made up of other items. An item cannot both be a kit and an assembly item.", + optional: true, + }, + assembly: { + type: "boolean", + label: "Assembly", + description: "True if the item is stocked by combining other items together. An item cannot both be a kit and an assembly item.", + optional: true, + }, + perishable: { + type: "boolean", + label: "Perishable", + description: "True if the item has an expiration date. A non-inventory or serialized item cannot also be this.", + optional: true, + }, + trackLot: { + type: "boolean", + label: "Track Lot", + description: "True if the item is tracked by lot numbers. A non-inventory or serialized item cannot also be this.", + optional: true, + }, + serialized: { + type: "boolean", + label: "Serialized", + description: "True if the item is tracked by serial numbers. A non-inventory item cannot be serialized.", + optional: true, + }, + nonInventory: { + type: "boolean", + label: "Non Inventory", + description: "True if the item does not have inventory. A kit or assembly cannot also be non-inventory.", + optional: true, + }, + weight: { + type: "string", + label: "Weight", + description: "The item's weight.", + optional: true, + }, + storageLength: { + type: "string", + label: "Storage Length", + description: "Part of the dimensions the item takes to store.", + optional: true, + }, + storageWidth: { + type: "string", + label: "Storage Width", + description: "Part of the dimensions the item takes to store.", + optional: true, + }, + storageHeight: { + type: "string", + label: "Storage Height", + description: "Part of the dimensions the item takes to store.", + optional: true, + }, + safetyStock: { + type: "integer", + label: "Safety Stock", + description: "How much stock should be withheld when reporting stock levels to marketplace integrations.", + optional: true, + }, + userField1: { + type: "string", + label: "User Field 1", + description: "User defined field for the item.", + optional: true, + }, + userField2: { + type: "string", + label: "User Field 2", + description: "User defined field for the item.", + optional: true, + }, + userField3: { + type: "string", + label: "User Field 3", + description: "User defined field for the item.", + optional: true, + }, + userField4: { + type: "string", + label: "User Field 4", + description: "User defined field for the item.", + optional: true, + }, + userField5: { + type: "string", + label: "User Field 5", + description: "User defined field for the item.", + optional: true, + }, + userField6: { + type: "string", + label: "User Field 6", + description: "User defined field for the item.", + optional: true, + }, + notes: { + type: "string", + label: "Notes", + description: "Notes for the item.", + optional: true, + }, + assignToAllWarehouses: { + type: "boolean", + label: "Assign To All Warehouses", + description: "True to assign to all warehouses on creation.", + optional: true, + }, + assignToWarehouse: { + type: "boolean", + label: "Assign To Warehouse", + description: "True to assign the item to a specific warehouse.", + optional: true, + }, + warehouseId: { + type: "integer", + label: "Warehouse Id", + description: "Id of the warehouse the item will be assigned to. If no warehouse parameters are given, then the user's current warehouse will be used.", + optional: true, + }, + warehouseName: { + type: "string", + label: "Warehouse Name", + description: "Name of the warehouse the item will be assigned to. Ignored if warehouseId is provided.", + optional: true, + }, + reorderLevel: { + type: "integer", + label: "Reorder Level", + description: "Reorder level for the item's warehouse assignment.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.zenventory.createItem({ + $, + data: { + sku: this.sku, + clientId: this.clientId, + clientName: this.clientName, + upc: this.upc, + description: this.description, + category: this.category, + baseUom: this.baseUom, + unitCost: this.unitCost && parseFloat(this.unitCost), + leadTime: this.leadTime, + defaultEconOrder: this.defaultEconOrder, + orderLimit: this.orderLimit, + rrp: this.rrp && parseFloat(this.rrp), + price: this.price && parseFloat(this.price), + active: this.active, + kit: this.kit, + assembly: this.assembly, + perishable: this.perishable, + trackLot: this.trackLot, + serialized: this.serialized, + nonInventory: this.nonInventory, + weight: this.weight && parseFloat(this.weight), + storageLength: this.storageLength && parseFloat(this.storageLength), + storageWidth: this.storageWidth && parseFloat(this.storageWidth), + storageHeight: this.storageHeight && parseFloat(this.storageHeight), + safetyStock: this.safetyStock, + userField1: this.userField1, + userField2: this.userField2, + userField3: this.userField3, + userField4: this.userField4, + userField5: this.userField5, + userField6: this.userField6, + notes: this.notes, + assignToAllWarehouses: this.assignToAllWarehouses, + assignToWarehouse: this.assignToWarehouse, + warehouseId: this.warehouseId, + warehouseName: this.warehouseName, + reorderLevel: this.reorderLevel, + }, + }); + + $.export("$summary", `Successfully created purchase order with ID ${response.id}`); + return response; + }, +}; diff --git a/components/zenventory/actions/create-purchase-order/create-purchase-order.mjs b/components/zenventory/actions/create-purchase-order/create-purchase-order.mjs new file mode 100644 index 0000000000000..f69431adca7a5 --- /dev/null +++ b/components/zenventory/actions/create-purchase-order/create-purchase-order.mjs @@ -0,0 +1,112 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import zenventory from "../../zenventory.app.mjs"; + +export default { + key: "zenventory-create-purchase-order", + name: "Create Purchase Order", + description: "Generates a new purchase order. [See the documentation](https://docs.zenventory.com/)", + version: "0.0.1", + type: "action", + props: { + zenventory, + supplierId: { + type: "integer", + label: "Supplier Id", + description: "Id of the supplier that is being ordered from.", + optional: true, + }, + supplierName: { + type: "string", + label: "Supplier Name", + description: "Name of the supplier that is being ordered from. Ignored if supplierId is provided.", + optional: true, + }, + warehouseId: { + type: "integer", + label: "Warehouse Id", + description: "Id of the warehouse the items will be delivered to. If no warehouse parameters are given, then the user's current warehouse will be used.", + optional: true, + }, + warehouseName: { + type: "string", + label: "Warehouse Name", + description: "Name of the warehouse the items will be delivered to. Ignored if warehouseId is provided.", + optional: true, + }, + clientId: { + type: "integer", + label: "Client Id", + description: "Id of the client that the purchase order is for. Defaults to the user's client id.", + optional: true, + }, + clientName: { + type: "string", + label: "Client Name", + description: "Name of the client that the purchase order is for. Ignored if clientId is provided and is nonzero.", + optional: true, + }, + orderNumber: { + type: "string", + label: "Order Number", + description: "Order number for the purchase order. If blank, one will automatically be generated.", + optional: true, + }, + draft: { + type: "boolean", + label: "Draft", + description: "True if the purchase order should be created as a draft to allow future editing.", + optional: true, + }, + requiredByDate: { + type: "string", + label: "Required By.", + description: "The date of the purchase. **Format: YYYY-MM-DD**", + optional: true, + }, + projectNumber: { + type: "string", + label: "Project Number", + description: "The number of the project.", + optional: true, + }, + notes: { + type: "string", + label: "Notes", + description: "A note of the purchase.", + optional: true, + }, + items: { + type: "string[]", + label: "Items", + description: "A list of object of ordered items. **Example: {\"itemId\": 123, \"sku\": \"SKU123\", \"description\": \"description\", \"quantity\": 1}**. [See the documentation](https://docs.zenventory.com/#tag/purchase_order/paths/~1purchase-orders/post) for further information.", + optional: true, + }, + }, + async run({ $ }) { + if (!this.supplierId && !this.supplierName) { + throw new ConfigurationError("You must provide at least 'Supplier Id' or 'Supplier Name'."); + } + + const response = await this.zenventory.createPurchaseOrder({ + $, + data: { + supplierId: this.supplierId, + supplierName: this.supplierName, + warehouseId: this.warehouseId, + warehouseName: this.warehouseName, + clientId: this.clientId, + clientName: this.clientName, + orderNumber: this.orderNumber, + draft: this.draft, + requiredByDate: this.requiredByDate, + projectNumber: this.projectNumber, + notes: this.notes, + items: parseObject(this.items), + }, + }); + + $.export("$summary", `Successfully created purchase order with ID ${response.id}`); + return response; + }, +}; diff --git a/components/zenventory/common/constants.mjs b/components/zenventory/common/constants.mjs new file mode 100644 index 0000000000000..a3deffb7fea4d --- /dev/null +++ b/components/zenventory/common/constants.mjs @@ -0,0 +1,5 @@ +export const POSTAGE_ACCOUNT_OPTIONS = [ + "sender", + "client", + "recipient", +]; diff --git a/components/zenventory/common/utils.mjs b/components/zenventory/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/zenventory/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/zenventory/package.json b/components/zenventory/package.json new file mode 100644 index 0000000000000..472c998de416a --- /dev/null +++ b/components/zenventory/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/zenventory", + "version": "0.1.0", + "description": "Pipedream Zenventory Components", + "main": "zenventory.app.mjs", + "keywords": [ + "pipedream", + "zenventory" + ], + "homepage": "https://pipedream.com/apps/zenventory", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/zenventory/sources/common/base.mjs b/components/zenventory/sources/common/base.mjs new file mode 100644 index 0000000000000..bd8177f772a9f --- /dev/null +++ b/components/zenventory/sources/common/base.mjs @@ -0,0 +1,72 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import zenventory from "../../zenventory.app.mjs"; + +export default { + props: { + zenventory, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01T00:00:01"; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + prepareData({ + responseArray, fieldDate, + }) { + if (responseArray.length) { + this._setLastDate(responseArray[0][fieldDate]); + } + return responseArray; + }, + emitData(data) { + for (const item of data.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(new Date()), + }); + } + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const fieldDate = this.getFieldDate(); + + const response = this.zenventory.paginate({ + fn: this.getFunction(), + params: this.getParams(lastDate), + dataField: this.getDataField(), + maxResults, + }); + + let responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + const preparedData = this.prepareData({ + responseArray, + fieldDate, + maxResults, + }); + + this.emitData(preparedData); + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/zenventory/sources/new-customer-order/new-customer-order.mjs b/components/zenventory/sources/new-customer-order/new-customer-order.mjs new file mode 100644 index 0000000000000..62da2a2003e41 --- /dev/null +++ b/components/zenventory/sources/new-customer-order/new-customer-order.mjs @@ -0,0 +1,46 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "zenventory-new-customer-order", + name: "New Customer Order Created", + description: "Emit new event when a new customer order is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + prepareData({ + responseArray, fieldDate, maxResults, + }) { + responseArray.reverse(); + if (responseArray.length) { + if (maxResults && responseArray.length > maxResults) { + responseArray.length = maxResults; + } + this._setLastDate(responseArray[0][fieldDate]); + } + return responseArray; + }, + getParams(orderedDate) { + return { + orderedDate, + orderedDateConditional: "on_or_after", + }; + }, + getFunction() { + return this.zenventory.listCustomerOrders; + }, + getDataField() { + return "customerOrders"; + }, + getFieldDate() { + return "orderedDate"; + }, + getSummary(item) { + return `New Customer Order: ${item.orderNumber}`; + }, + }, + sampleEmit, +}; diff --git a/components/zenventory/sources/new-customer-order/test-event.mjs b/components/zenventory/sources/new-customer-order/test-event.mjs new file mode 100644 index 0000000000000..aa1d145c03e70 --- /dev/null +++ b/components/zenventory/sources/new-customer-order/test-event.mjs @@ -0,0 +1,109 @@ +export default { + "id": 103717, + "orderNumber": "Test-Order-001", + "orderReference": "", + "customer": { + "id": 7089, + "title": "", + "name": "Test Customer 1", + "surname": "", + "email": "testcustomer1@testserver.com", + "company": "Test Company", + "accountNumber": "" + }, + "client": null, + "orderedDate": "2021-12-13T00:00:00-07:00", + "createdDate": "2024-09-13T13:02:58-07:00", + "modifiedDate": "2024-09-13T13:02:58-07:00", + "orderPlaced": true, + "createdBy": { + "id": 520, + "name": "Pipedream Support" + }, + "completed": false, + "completedDate": "", + "cancelled": false, + "cancelledDate": "", + "cancelledReason": "", + "cancelledBy": null, + "onHold": false, + "onHoldUntil": "", + "postageAccount": "sender", + "buyerPaidShipping": 0, + "shippingAddress": { + "id": 12517, + "company": "", + "name": "", + "line1": "515 E Grant St", + "line2": "", + "line3": "", + "city": "Phoenix", + "state": "AZ", + "zip": "85004", + "country": "United States", + "countryCode": "US", + "phone": "", + "code": "", + "verifiedStatus": "awaiting", + "verifiedDate": "", + "verifiedMessage": "" + }, + "billingAddress": { + "id": 12518, + "company": "", + "name": "", + "line1": "N/A", + "line2": "", + "line3": "", + "city": "", + "state": "", + "zip": "", + "country": "United States", + "countryCode": "US", + "phone": "", + "code": "", + "verifiedStatus": "awaiting", + "verifiedDate": "", + "verifiedMessage": "" + }, + "shipVia": "Standard", + "shipViaPackaging": "", + "shipViaConfirmation": "", + "dryIceWeight": 0, + "packageSku": "", + "shipFromWarehouse": { + "id": 187, + "name": "Main Warehouse" + }, + "projectNumber": "", + "discountPercentage": 0, + "orderSource": "CSV Import", + "internalNote": "", + "noteFromCustomer": "", + "noteToCustomer": "", + "notificationEmail": "", + "userField1": "", + "userField2": "", + "userField3": "", + "myList1": "", + "myList2": "", + "tags": [], + "items": [ + { + "id": 214405, + "itemId": 12063, + "sku": "TEST-SKU-101", + "description": "Baseball Cap", + "orderQuantity": 1, + "uom": "Each", + "quantity": 1, + "allocatedQuantity": 1, + "pickedQuantity": 0, + "price": 49, + "discountPercentage": 0, + "partOfKit": false, + "componentOf": 0, + "additionalFields": null + } + ] +} \ No newline at end of file diff --git a/components/zenventory/sources/new-item-created/new-item-created.mjs b/components/zenventory/sources/new-item-created/new-item-created.mjs new file mode 100644 index 0000000000000..9e8d9319da152 --- /dev/null +++ b/components/zenventory/sources/new-item-created/new-item-created.mjs @@ -0,0 +1,56 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "zenventory-new-item-created", + name: "New Item Created", + description: "Emit new event when a new item is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFieldDate() { + return "createdDate"; + }, + getSummary(item) { + return `New Item: ${item.sku}`; + }, + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + async emitEvent(maxResults = false) { + const lastId = this._getLastId(); + + const response = this.zenventory.paginate({ + fn: this.zenventory.listItems, + params: { + orderBy: "id", + orderDir: "desc", + }, + dataField: "items", + maxResults, + }); + + let responseArray = []; + for await (const item of response) { + if (item.id <= lastId) break; + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && responseArray.length > maxResults) { + responseArray.length = maxResults; + } + this._setLastId(responseArray[0].id); + } + + this.emitData(responseArray); + }, + }, + sampleEmit, +}; diff --git a/components/zenventory/sources/new-item-created/test-event.mjs b/components/zenventory/sources/new-item-created/test-event.mjs new file mode 100644 index 0000000000000..bde5e051fb52e --- /dev/null +++ b/components/zenventory/sources/new-item-created/test-event.mjs @@ -0,0 +1,44 @@ +export default { + "id": 2306, + "sku": "FumoFumo-002", + "upc": "", + "description": "Marisa Kirisame", + "category": "Fumo", + "client": null, + "baseUom": "Each", + "unitCost": 10, + "leadTime": 0, + "defaultEconOrder": 0, + "rrp": 0, + "price": 50, + "active": true, + "kit": false, + "assembly": false, + "perishable": false, + "trackLot": false, + "serialized": false, + "nonInventory": false, + "weight": 0, + "storageLength": 0, + "storageWidth": 0, + "storageHeight": 0, + "storageVolume": 0, + "safetyStock": 0, + "userField1": "", + "userField2": "", + "userField3": "", + "userField4": "", + "userField5": "", + "userField6": "", + "createdDate": "2017-08-06T10:45:53-07:00", + "modifiedDate": "2017-08-06T10:45:53-07:00", + "notes": "", + "additionalFields": { + "units": [ + { + "name": "Each", + "quantity": 1 + } + ] + } +} \ No newline at end of file diff --git a/components/zenventory/sources/new-purchase-order/new-purchase-order.mjs b/components/zenventory/sources/new-purchase-order/new-purchase-order.mjs new file mode 100644 index 0000000000000..9c979313317de --- /dev/null +++ b/components/zenventory/sources/new-purchase-order/new-purchase-order.mjs @@ -0,0 +1,35 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "zenventory-new-purchase-order", + name: "New Purchase Order Created", + description: "Emit new event when a new purchase order is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getParams(lastDate) { + return { + createdFrom: lastDate, + orderBy: "createdDate", + orderDir: "desc", + }; + }, + getFunction() { + return this.zenventory.listPurchaseOrders; + }, + getDataField() { + return "purchaseOrders"; + }, + getFieldDate() { + return "createdDate"; + }, + getSummary(item) { + return `New Purchase Order: ${item.orderNumber}`; + }, + }, + sampleEmit, +}; diff --git a/components/zenventory/sources/new-purchase-order/test-event.mjs b/components/zenventory/sources/new-purchase-order/test-event.mjs new file mode 100644 index 0000000000000..5e0b55e3aed9e --- /dev/null +++ b/components/zenventory/sources/new-purchase-order/test-event.mjs @@ -0,0 +1,32 @@ +export default { + "id": 3858, + "orderNumber": "000000", + "supplier": { + "id": 569, + "name": "Default Supplier" + }, + "warehouse": { + "id": 187, + "name": "Main Warehouse" + }, + "client": null, + "user": { + "id": 123, + "name": "User Name" + }, + "draft": true, + "completed": false, + "deleted": false, + "createdDate": "2024-10-07T11:00:31-07:00", + "preparedDate": "", + "requiredByDate": "2024-10-07", + "completedDate": "", + "projectNumber": "", + "notes": "", + "shipMethod": "", + "terms": "", + "userField1": "", + "userField2": "", + "userField3": "", + "items": [] +} \ No newline at end of file diff --git a/components/zenventory/zenventory.app.mjs b/components/zenventory/zenventory.app.mjs new file mode 100644 index 0000000000000..325b26763d724 --- /dev/null +++ b/components/zenventory/zenventory.app.mjs @@ -0,0 +1,103 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "zenventory", + propDefinitions: { + clientId: { + type: "integer", + label: "Client Id", + description: "Id of the client that the customer order is for.", + }, + clientName: { + type: "string", + label: "Client Name", + description: "Name of the client that the customer order is for. **Ignored if clientId is provided and is nonzero.**", + }, + }, + methods: { + _baseUrl() { + return "https://app.zenventory.com/rest"; + }, + _auth() { + return { + username: `${this.$auth.api_key}`, + password: `${this.$auth.api_secret}`, + }; + }, + _makeRequest({ + $ = this, method = "GET", path, ...opts + }) { + return axios($, { + method, + url: this._baseUrl() + path, + auth: this._auth(), + ...opts, + }); + }, + createItem(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/items", + ...opts, + }); + }, + createCustomerOrder(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/customer-orders", + ...opts, + }); + }, + createPurchaseOrder(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/purchase-orders", + ...opts, + }); + }, + listCustomerOrders(opts = {}) { + return this._makeRequest({ + path: "/customer-orders", + ...opts, + }); + }, + listPurchaseOrders(opts = {}) { + return this._makeRequest({ + path: "/purchase-orders", + ...opts, + }); + }, + listItems(opts = {}) { + return this._makeRequest({ + path: "/items", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, dataField, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const response = await fn({ + params, + ...opts, + }); + for (const d of response[dataField]) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = page < response.meta.totalPages; + + } while (hasMore); + }, + }, +}; diff --git a/components/zep/actions/add-memory/add-memory.mjs b/components/zep/actions/add-memory/add-memory.mjs new file mode 100644 index 0000000000000..b4111f01e514e --- /dev/null +++ b/components/zep/actions/add-memory/add-memory.mjs @@ -0,0 +1,55 @@ +import zep from "../../zep.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "zep-add-memory", + name: "Add Memory to Session", + description: "Adds memory to an existing session in Zep. [See the documentation](https://help.getzep.com/api-reference/memory/add)", + version: "0.0.1", + type: "action", + props: { + zep, + sessionId: { + propDefinition: [ + zep, + "sessionId", + ], + }, + messages: { + type: "string[]", + label: "Messages", + description: "An array of message objects, where each message contains a role (`norole`, `system`, `assistant`, `user`, `function`, or `tool`) and content. Example: `[{ \"content\": \"content\", \"role_type\": \"norole\" }]` [See the documentation](https://help.getzep.com/api-reference/memory/add) for more information", + }, + factInstruction: { + type: "string", + label: "Fact Instruction", + description: "Additional instruction for generating the facts", + optional: true, + }, + returnContext: { + type: "boolean", + label: "Return Context", + description: "Optionally return memory context relevant to the most recent messages", + optional: true, + }, + summaryInstruction: { + type: "string", + label: "Summary Instructions", + description: "Additional instruction for generating the summary", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.zep.addMemoryToSession({ + sessionId: this.sessionId, + data: { + messages: utils.parseArray(this.messages), + factInstruction: this.factInstruction, + returnContext: this.returnContext, + summaryInstruction: this.summaryInstruction, + }, + }); + $.export("$summary", `Added memory to session ${this.sessionId}`); + return response; + }, +}; diff --git a/components/zep/actions/add-user/add-user.mjs b/components/zep/actions/add-user/add-user.mjs new file mode 100644 index 0000000000000..85e30feec2b86 --- /dev/null +++ b/components/zep/actions/add-user/add-user.mjs @@ -0,0 +1,64 @@ +import zep from "../../zep.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "zep-add-user", + name: "Add User", + description: "Adds a user in Zep. [See the documentation](https://help.getzep.com/api-reference/user/add)", + version: "0.0.1", + type: "action", + props: { + zep, + userId: { + type: "string", + label: "User ID", + description: "The unique identifier of the new user", + }, + email: { + type: "string", + label: "Email", + description: "Email address of the user", + optional: true, + }, + firstName: { + type: "string", + label: "First Name", + description: "First name of the new user", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the new user", + optional: true, + }, + factRatingInstructions: { + propDefinition: [ + zep, + "factRatingInstructions", + ], + }, + metadata: { + propDefinition: [ + zep, + "metadata", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.zep.createUser({ + $, + data: { + email: this.email, + first_name: this.firstName, + last_name: this.lastName, + fact_rating_instructions: utils.parseObject(this.factRatingInstructions), + metadata: utils.parseObject(this.metadata), + user_id: this.userId, + }, + }); + $.export("$summary", `Successfully added user with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/zep/actions/create-session/create-session.mjs b/components/zep/actions/create-session/create-session.mjs new file mode 100644 index 0000000000000..3700067d25cbf --- /dev/null +++ b/components/zep/actions/create-session/create-session.mjs @@ -0,0 +1,50 @@ +import zep from "../../zep.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "zep-create-session", + name: "Create Session", + description: "Creates a new session in Zep. [See the documentation](https://help.getzep.com/api-reference/memory/add-session)", + version: "0.0.1", + type: "action", + props: { + zep, + sessionId: { + type: "string", + label: "Session ID", + description: "The unique identifier of the session", + }, + userId: { + propDefinition: [ + zep, + "userId", + ], + }, + factRatingInstructions: { + propDefinition: [ + zep, + "factRatingInstructions", + ], + }, + metadata: { + propDefinition: [ + zep, + "metadata", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.zep.createSession({ + $, + data: { + session_id: this.sessionId, + user_id: this.userId, + fact_rating_instructions: utils.parseObject(this.factRatingInstructions), + metadata: utils.parseObject(this.metadata), + }, + }); + $.export("$summary", `Created session with ID ${this.sessionId}`); + return response; + }, +}; diff --git a/components/zep/actions/update-session/update-session.mjs b/components/zep/actions/update-session/update-session.mjs new file mode 100644 index 0000000000000..093227a3de2dd --- /dev/null +++ b/components/zep/actions/update-session/update-session.mjs @@ -0,0 +1,44 @@ +import zep from "../../zep.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "zep-update-session", + name: "Update Session", + description: "Updates an existing session in Zep. [See the documentation](https://help.getzep.com/api-reference/memory/update-session)", + version: "0.0.1", + type: "action", + props: { + zep, + sessionId: { + propDefinition: [ + zep, + "sessionId", + ], + }, + metadata: { + propDefinition: [ + zep, + "metadata", + ], + description: "An object of key/value pairs representing the metadata to add to the session", + }, + factRatingInstructions: { + propDefinition: [ + zep, + "factRatingInstructions", + ], + }, + }, + async run({ $ }) { + const response = await this.zep.updateSession({ + $, + sessionId: this.sessionId, + data: { + fact_rating_instructions: utils.parseObject(this.factRatingInstructions), + metadata: utils.parseObject(this.metadata), + }, + }); + $.export("$summary", `Successfully updated ssession with ID ${this.sessionId}`); + return response; + }, +}; diff --git a/components/zep/common/utils.mjs b/components/zep/common/utils.mjs new file mode 100644 index 0000000000000..32476c3373b72 --- /dev/null +++ b/components/zep/common/utils.mjs @@ -0,0 +1,30 @@ +function parseObject(obj) { + if (!obj) { + return undefined; + } + + return typeof obj === "string" + ? JSON.parse(obj) + : obj; +} + +function parseArray(arr) { + if (!arr) { + return undefined; + } + + if (typeof arr === "string") { + return JSON.parse(arr); + } + + if (Array.isArray(arr)) { + return arr.map((item) => parseObject(item)); + } + + return arr; +} + +export default { + parseObject, + parseArray, +}; diff --git a/components/zep/package.json b/components/zep/package.json new file mode 100644 index 0000000000000..5f49cb70ac437 --- /dev/null +++ b/components/zep/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/zep", + "version": "0.1.0", + "description": "Pipedream Zep Components", + "main": "zep.app.mjs", + "keywords": [ + "pipedream", + "zep" + ], + "homepage": "https://pipedream.com/apps/zep", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/zep/sources/common/base.mjs b/components/zep/sources/common/base.mjs new file mode 100644 index 0000000000000..d186ee9b3ee88 --- /dev/null +++ b/components/zep/sources/common/base.mjs @@ -0,0 +1,80 @@ +import zep from "../../zep.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + zep, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + const results = await this.getNewResults(lastTs, max); + results.forEach((item) => this.emitEvent(item)); + }, + async getSessions({ + lastTs, orderBy, max, updateLastTs = true, + }) { + const params = { + page_size: max || 1000, + order_by: orderBy, + asc: false, + }; + + const { sessions: results } = await this.zep.listSessions({ + params, + }); + + const sessions = []; + for (const session of results) { + const ts = Date.parse(session[orderBy]); + if (ts >= lastTs) { + sessions.push(session); + } else { + break; + } + if (max && sessions.length >= max) { + break; + } + } + + if (!sessions.length) { + return []; + } + if (updateLastTs) { + this._setLastTs(Date.parse(sessions[0][orderBy])); + } + return sessions.reverse(); + }, + emitEvent(item) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }, + getNewResults() { + throw new Error("getNewResults is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/zep/sources/new-message/new-message.mjs b/components/zep/sources/new-message/new-message.mjs new file mode 100644 index 0000000000000..72869259ea2ec --- /dev/null +++ b/components/zep/sources/new-message/new-message.mjs @@ -0,0 +1,80 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "zep-new-message", + name: "New Message in Session", + description: "Emit new event when a message is added to a session. [See the documentation](https://help.getzep.com/api-reference/memory/get-session-messages)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + async getNewResults(lastTs, max) { + const sessionIds = await this.getRecentlyUpdatedSessionIds(lastTs); + let messages = []; + let maxTs = lastTs; + + for (const sessionId of sessionIds) { + const results = this.paginateMessages(sessionId); + + for await (const message of results) { + const ts = Date.parse(message.created_at); + if (ts >= lastTs) { + messages.push({ + ...message, + session_id: sessionId, + }); + maxTs = Math.max(maxTs, ts); + } + } + } + + this._setLastTs(maxTs); + + // sort by created_at + messages = messages.sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at)); + + if (max) { + messages = messages.slice(-1 * max); + } + + return messages; + }, + async getRecentlyUpdatedSessionIds(lastTs) { + const sessions = await this.getSessions({ + lastTs, + orderBy: "updated_at", + updateLastTs: false, + max: 100, + }); + return sessions?.map(({ session_id: id }) => id) || []; + }, + async *paginateMessages(sessionId) { + const params = { + limit: 1000, + cursor: 1, + }; + let total; + + do { + const { messages } = await this.zep.listMessages({ + sessionId, + params, + }); + for (const message of messages) { + yield message; + } + total = messages?.length; + params.cursor++; + } while (total); + }, + generateMeta(message) { + return { + id: message.uuid, + summary: `New Message: ${message.content}`, + ts: Date.parse(message.created_at), + }; + }, + }, +}; diff --git a/components/zep/sources/new-session/new-session.mjs b/components/zep/sources/new-session/new-session.mjs new file mode 100644 index 0000000000000..49842ba20f6bc --- /dev/null +++ b/components/zep/sources/new-session/new-session.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "zep-new-session", + name: "New Session Created", + description: "Emit new event when a new session is created in Zep. [See the documentation](https://help.getzep.com/api-reference/memory/list-sessions)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getNewResults(lastTs, max) { + return this.getSessions({ + lastTs, + max, + orderBy: "created_at", + }); + }, + generateMeta(session) { + return { + id: session.session_id, + summary: `New Session: ${session.session_id}`, + ts: Date.parse(session.created_at), + }; + }, + }, +}; diff --git a/components/zep/sources/session-updated/session-updated.mjs b/components/zep/sources/session-updated/session-updated.mjs new file mode 100644 index 0000000000000..08e7955f228b8 --- /dev/null +++ b/components/zep/sources/session-updated/session-updated.mjs @@ -0,0 +1,29 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "zep-session-updated", + name: "Session Updated", + description: "Emit new event when an existing session is updated. [See the documentation](https://help.getzep.com/api-reference/memory/list-sessions)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getNewResults(lastTs, max) { + return this.getSessions({ + lastTs, + max, + orderBy: "updated_at", + }); + }, + generateMeta(session) { + const ts = Date.parse(session.updated_at); + return { + id: `${session.session_id}${ts}`, + summary: `Updated Session: ${session.session_id}`, + ts, + }; + }, + }, +}; diff --git a/components/zep/zep.app.mjs b/components/zep/zep.app.mjs new file mode 100644 index 0000000000000..4ea32d1fb7b45 --- /dev/null +++ b/components/zep/zep.app.mjs @@ -0,0 +1,124 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "zep", + propDefinitions: { + sessionId: { + type: "string", + label: "Session ID", + description: "The ID of the session", + async options({ page }) { + const { sessions } = await this.listSessions({ + params: { + page_number: page, + order_by: "updated_at", + asc: false, + }, + }); + return sessions?.map(({ session_id }) => session_id) || []; + }, + }, + userId: { + type: "string", + label: "User ID", + description: "The ID of the user", + async options({ page }) { + const { users } = await this.listUsers({ + params: { + pageNumber: page, + }, + }); + return users?.map(({ + user_id: value, first_name: firstName, last_name: lastName, email, + }) => ({ + value, + label: firstName || lastName + ? (`${firstName} ${lastName}`).trim() + : email || value, + })) || []; + }, + }, + factRatingInstructions: { + type: "object", + label: "Fact Rating Instructions", + description: "Instructions to use for the fact rating consisting of examples and instruction. Example: `{ \"examples\": { \"high\": \"high\", \"low\": \"low\", \"medium\": \"medium\" }, \"instruction\": \"instruction\" }`. [See the documentation](https://help.getzep.com/api-reference/memory/add-session) for more info.", + optional: true, + }, + metadata: { + type: "object", + label: "Metadata", + description: "An object of key/value pairs representing the metadata associated with the session", + }, + }, + methods: { + _baseUrl() { + return "https://api.getzep.com/api/v2"; + }, + _makeRequest({ + $ = this, + path, + ...otherOpts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + "authorization": `Api-Key ${this.$auth.api_key}`, + }, + ...otherOpts, + }); + }, + listSessions(opts = {}) { + return this._makeRequest({ + path: "/sessions-ordered", + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users-ordered", + ...opts, + }); + }, + listMessages({ + sessionId, ...opts + }) { + return this._makeRequest({ + path: `/sessions/${sessionId}/messages`, + ...opts, + }); + }, + createSession(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/sessions", + ...opts, + }); + }, + createUser(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/users", + ...opts, + }); + }, + addMemoryToSession({ + sessionId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/sessions/${sessionId}/memory`, + ...opts, + }); + }, + updateSession({ + sessionId, ...opts + }) { + return this._makeRequest({ + method: "PATCH", + path: `/sessions/${sessionId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/zerobounce/.gitignore b/components/zerobounce/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/zerobounce/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/zerobounce/README.md b/components/zerobounce/README.md new file mode 100644 index 0000000000000..0e9675e32d769 --- /dev/null +++ b/components/zerobounce/README.md @@ -0,0 +1,11 @@ +# Overview + +The ZeroBounce API provides email verification services that help improve email deliverability by removing invalid or risky email addresses from your lists. When integrated with Pipedream, you can automate workflows to clean your mailing lists, validate subscribers in real-time, and enrich your contacts. Pipedream's serverless platform facilitates the running of code that interacts with the ZeroBounce API to execute these tasks based on various triggers and actions from other integrated services. + +# Example Use Cases + +- **Email List Cleaning Workflow**: Automatically validate and clean email lists on a schedule. By setting up a timed trigger, the workflow can retrieve email lists from a data storage app, send them to the ZeroBounce API for validation, and receive the cleaned list, which can be stored back or sent to an email marketing service like Mailchimp. + +- **Real-time Email Verification**: Implement real-time email verification on sign-up forms. When a new user submits their email through a form, a Pipedream workflow can instantly verify it with ZeroBounce. Depending on the result, the workflow can either proceed with the user registration or prompt for a valid email, ensuring only legitimate users are onboarded. + +- **Enrichment and Segmentation**: Enrich and segment contacts based on ZeroBounce's data append feature. The workflow can take an email address, use ZeroBounce to append additional data such as the user's location, gender, or IP address, and subsequently use this info to segment the contacts into specific marketing campaigns within a CRM such as Salesforce. diff --git a/components/zerobounce/actions/ai-scoring/ai-scoring.mjs b/components/zerobounce/actions/ai-scoring/ai-scoring.mjs new file mode 100644 index 0000000000000..d191cea4053af --- /dev/null +++ b/components/zerobounce/actions/ai-scoring/ai-scoring.mjs @@ -0,0 +1,27 @@ +import zerobounce from "../../zerobounce.app.mjs"; + +export default { + key: "zerobounce-ai-scoring", + name: "AI Scoring", + description: "Estimates a reliability score based on ZeroBounce's AI for the provided email. [See the documentation](https://www.zerobounce.net/docs/ai-scoring-api/#single_email_scoring)", + version: "0.0.1", + type: "action", + props: { + zerobounce, + email: { + type: "string", + label: "Email", + description: "The email address that you want to retrieve Scoring data for", + }, + }, + async run({ $ }) { + const response = await this.zerobounce.getReliabilityScore({ + $, + params: { + email: this.email, + }, + }); + $.export("$summary", `Successfully estimated reliability score for email: ${this.email}`); + return response; + }, +}; diff --git a/components/zerobounce/actions/file-validation/file-validation.mjs b/components/zerobounce/actions/file-validation/file-validation.mjs new file mode 100644 index 0000000000000..92384e9928ff1 --- /dev/null +++ b/components/zerobounce/actions/file-validation/file-validation.mjs @@ -0,0 +1,128 @@ +import zerobounce from "../../zerobounce.app.mjs"; +import fs from "fs"; +import FormData from "form-data"; +import path from "path"; + +export default { + key: "zerobounce-file-validation", + name: "Validate Emails in File", + description: "Performs email validation on all the addresses contained in a provided file. [See the documentation](https://www.zerobounce.net/docs/email-validation-api-quickstart/)", + version: "0.0.1", + type: "action", + props: { + zerobounce, + filePath: { + type: "string", + label: "File Path", + description: "The path to a csv or txt file in the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + }, + emailAddressColumn: { + type: "integer", + label: "Email Address Column", + description: "The column index of the email address in the file. Index starts from 1.", + }, + firstNameColumn: { + type: "integer", + label: "First Name Column", + description: "The column index of the first name column. Index starts from 1.", + optional: true, + }, + lastNameColumn: { + type: "integer", + label: "Last Name Column", + description: "The column index of the last name column. Index starts from 1.", + optional: true, + }, + ipAddressColumn: { + type: "integer", + label: "IP Address Column", + description: "The IP Address the email signed up from. Index starts from 1", + optional: true, + }, + hasHeaderRow: { + type: "boolean", + label: "Has Header Row", + description: "If the first row from the submitted file is a header row", + optional: true, + }, + removeDuplicates: { + type: "boolean", + label: "Remove Duplicates", + description: "If you want the system to remove duplicate emails. Default is `true`. Please note that if we remove more than 50% of the lines because of duplicates (parameter is true), system will return a 400 bad request error as a safety net to let you know that more than 50% of the file has been modified.", + optional: true, + }, + returnUrl: { + type: "string", + label: "Return URL", + description: "The URL will be used to call back when the validation is completed", + optional: true, + }, + callbackWithRerun: { + type: "boolean", + label: "Callback With Rerun", + description: "Use the `$.flow.rerun` Node.js helper to rerun the step when the validation is completed. Overrides the `rerunUrl` prop. This will increase execution time and credit usage as a result. [See the documentation](https://pipedream.com/docs/code/nodejs/rerun/#flow-rerun)", + optional: true, + }, + }, + async run({ $ }) { + let response, summary; + const { run } = $.context; + + if (run.runs === 1) { + let returnUrl = this.returnUrl; + if (this.callbackWithRerun) { + ({ resume_url: returnUrl } = $.flow.rerun(600000, null, 1)); + } + + const filePath = this.filePath.includes("tmp/") + ? this.filePath + : `/tmp/${this.filePath}`; + const fileName = path.basename(filePath); + const fileContent = fs.readFileSync(filePath); + + const formData = new FormData(); + formData.append("file", fileContent, fileName); + formData.append("email_address_column", this.emailAddressColumn); + formData.append("api_key", this.zerobounce.$auth.api_key); + if (this.firstNameColumn) { + formData.append("first_name_column", this.firstNameColumn); + } + if (this.lastNameColumn) { + formData.append("last_name_column", this.lastNameColumn); + } + if (this.ipAddressColumn) { + formData.append("ip_address_column", this.ipAddressColumn); + } + if (this.hasHeaderRow) { + formData.append("has_header_row", this.hasHeaderRow + ? "true" + : "false"); + } + if (this.removeDuplicates) { + formData.append("remove_duplicate", this.removeDuplicates + ? "true" + : "false"); + } + if (returnUrl) { + formData.append("return_url", returnUrl); + } + + response = await this.zerobounce.validateEmailsInFile({ + $, + data: formData, + headers: { + ...formData.getHeaders(), + }, + }); + summary = "Successfully sent file for validation"; + } + + if (run.callback_request) { + response = run.callback_request.body; + summary = "Successfully validated emails in file"; + } + + $.export("$summary", summary); + return response; + }, +}; diff --git a/components/zerobounce/actions/get-validation-results-file/get-validation-results-file.mjs b/components/zerobounce/actions/get-validation-results-file/get-validation-results-file.mjs new file mode 100644 index 0000000000000..a4fb5146ee7db --- /dev/null +++ b/components/zerobounce/actions/get-validation-results-file/get-validation-results-file.mjs @@ -0,0 +1,64 @@ +import zerobounce from "../../zerobounce.app.mjs"; +import fs from "fs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "zerobounce-get-validation-results-file", + name: "Get Validation Results File", + description: "Downloads the validation results for a file submitted using sendfile API. [See the documentation](https://www.zerobounce.net/docs/email-validation-api-quickstart/#get_file__v2__)", + version: "0.0.1", + type: "action", + props: { + zerobounce, + fileId: { + type: "string", + label: "File ID", + description: "The file_id returned when sending the file for validation. Can be found on your Zerobounce \"Validate\" tab under Results next to each filename. Click on the ID circle.", + }, + fileName: { + type: "string", + label: "File Name", + description: "The filename to save the file as in the \"/tmp\" directory", + }, + }, + methods: { + getResultsFile({ + $, ...opts + }) { + return this.zerobounce.getResultsFile({ + $, + params: { + file_id: this.fileId, + }, + ...opts, + }); + }, + async validateFileId({ $ }) { + try { + return await this.getResultsFile({ + $, + }); + } catch { + throw new ConfigurationError("File not found. Make sure the File ID is correct"); + } + }, + }, + async run({ $ }) { + if (!(await this.validateFileId({ + $, + }))) { + return; + } + const response = await this.getResultsFile({ + $, + responseType: "arraybuffer", + }); + + const filePath = `/tmp/${this.fileName}`; + fs.writeFileSync(filePath, response); + + $.export("$summary", `File saved to ${filePath}`); + + return filePath; + }, +}; diff --git a/components/zerobounce/actions/validate-email/validate-email.mjs b/components/zerobounce/actions/validate-email/validate-email.mjs new file mode 100644 index 0000000000000..45399108a0c80 --- /dev/null +++ b/components/zerobounce/actions/validate-email/validate-email.mjs @@ -0,0 +1,41 @@ +import zerobounce from "../../zerobounce.app.mjs"; + +export default { + key: "zerobounce-validate-email", + name: "Validate Email", + description: "Validates a specific email. [See the documentation](https://www.zerobounce.net/docs/email-validation-api-quickstart/#validate_emails__v2__)", + version: "0.0.1", + type: "action", + props: { + zerobounce, + email: { + type: "string", + label: "Email", + description: "The email address to be validated", + }, + ipAddress: { + type: "string", + label: "IP Address", + description: "The IP Address the email signed up from", + optional: true, + }, + activityData: { + type: "boolean", + label: "Activity Data", + description: "If set to `true`, Activity Data information will be appended to the validation result", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.zerobounce.validateEmail({ + $, + params: { + email: this.email, + ip_address: this.ipAddress || "", + activity_data: this.activityData, + }, + }); + $.export("$summary", `Successfully validated email: ${this.email}`); + return response; + }, +}; diff --git a/components/zerobounce/app/zerobounce.app.ts b/components/zerobounce/app/zerobounce.app.ts deleted file mode 100644 index 56554ac30099a..0000000000000 --- a/components/zerobounce/app/zerobounce.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "zerobounce", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/zerobounce/package.json b/components/zerobounce/package.json index b94be62eec373..cbe8e85ba7dc8 100644 --- a/components/zerobounce/package.json +++ b/components/zerobounce/package.json @@ -1,16 +1,20 @@ { "name": "@pipedream/zerobounce", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream ZeroBounce Components", - "main": "dist/app/zerobounce.app.mjs", + "main": "zerobounce.app.mjs", "keywords": [ "pipedream", "zerobounce" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/zerobounce", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "form-data": "^4.0.1", + "path": "^0.12.7" } } diff --git a/components/zerobounce/zerobounce.app.mjs b/components/zerobounce/zerobounce.app.mjs new file mode 100644 index 0000000000000..88ecf05bc55dd --- /dev/null +++ b/components/zerobounce/zerobounce.app.mjs @@ -0,0 +1,53 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "zerobounce", + methods: { + _baseUrl() { + return "https://api.zerobounce.net/v2"; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + url, + params, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: url || `${this._baseUrl()}${path}`, + params: { + ...params, + api_key: this.$auth.api_key, + }, + }); + }, + validateEmail(opts = {}) { + return this._makeRequest({ + path: "/validate", + ...opts, + }); + }, + getReliabilityScore(opts = {}) { + return this._makeRequest({ + path: "/scoring", + ...opts, + }); + }, + validateEmailsInFile(opts = {}) { + return this._makeRequest({ + method: "POST", + url: "https://bulkapi.zerobounce.net/v2/sendfile", + ...opts, + }); + }, + getResultsFile(opts = {}) { + return this._makeRequest({ + url: "https://bulkapi.zerobounce.net/v2/getfile", + ...opts, + }); + }, + }, +}; diff --git a/components/zerotier/README.md b/components/zerotier/README.md index 4eb0a98c0cb66..055c6951d65d0 100644 --- a/components/zerotier/README.md +++ b/components/zerotier/README.md @@ -1,21 +1,11 @@ # Overview -With the ZeroTier API, you can create powerful zero-trust networks across -applications, subsystems, devices, and end-users. The API lets you control and -monitor all of the different components that make up a ZeroTier network, -empowering you to build powerful networking solutions. Below are some examples -of what you can build using the ZeroTier API: +ZeroTier is a smart Ethernet switch for Earth. It's a global distributed network that connects devices, VMs, and apps with a simple, secure, and ubiquitous network fabric. The ZeroTier API allows you to manage networks, members, and access rules programmatically. With Pipedream's capabilities, you can automate interactions with ZeroTier, integrating it seamlessly with other apps and services to orchestrate complex workflows, react to events, or sync data across platforms. -- Secure communication infrastructure, connecting nodes, services, and users - inside and outside of corporate networks -- Secure sites and services, linked securely for public or private access -- Fault-tolerant load-balancing and service-mesh capabilities for microservices - applications -- Securely manage deployments, allowing for remote access and control from - anywhere -- Network-based authentication, providing additional layers of security to - different types of systems -- Data streaming networks, connecting edge-devices to central systems in - real-time -- Scalable peer-to-peer networks, simplifying distributed computing - applications +# Example Use Cases + +- **Automated Network Provisioning**: When new employees are onboarded, you could use the ZeroTier API on Pipedream to automatically provision access to necessary networks. This could be triggered by an HR system like BambooHR, where a new user creation event kicks off a workflow that adds the user to specified ZeroTier networks. + +- **Dynamic Access Control Based on External Events**: Imagine a security system like Okta issuing a command to restrict network access in response to a potential threat. Using Pipedream, you could listen for such events and use the ZeroTier API to modify network access rules, removing or altering member permissions in real-time, thus enhancing your overall security posture. + +- **Sync Network Status with Monitoring Tools**: Use Pipedream to connect ZeroTier with monitoring tools like Datadog. You can set up a workflow that periodically checks the status of your ZeroTier networks and members, then logs this info into Datadog for analysis. This way, you can keep an eye on network health, usage patterns, and preemptively spot issues before they escalate. diff --git a/components/zerys/README.md b/components/zerys/README.md index 1671b596025a6..b6fd019531dfa 100644 --- a/components/zerys/README.md +++ b/components/zerys/README.md @@ -1,28 +1,11 @@ # Overview -The Zerys API is a powerful software development platform that can be used to -create a variety of applications. With Zerys, developers can create innovative -software Tools, Applications and APIs (Application Programming Interfaces) -tailored to specific needs. +The Zerys API offers a platform for content marketing and production, enabling programmatic interactions with Zerys content services. With Pipedream, you can automate content ordering, manage projects, and streamline the content creation pipeline by integrating with other business tools. This API could be a powerhouse for those looking to automate their content operations, ensuring consistent quality and timely delivery. -Zerys provides businesses with a fast and reliable cloud platform for creating -high quality software solutions at scale. The platform supports a wide range of -well-defined and intuitive APIs that make integrating applications with other -services and products effortless. Zerys provides a comprehensive set of tools -for quickly designing, building, and testing new applications. +# Example Use Cases -Furthermore, Zerys enables developers to rapidly deploy hosted applications -using fully-managed services, allowing businesses to take advantage of the -latest technologies quickly and efficiently. +- **Content Request Automation**: Automate the process of requesting new content as soon as your inventory drops below a certain threshold. Trigger a Zerys API call to create a new content project when a stock management system, like Shopify, indicates you need more content for product descriptions or blog posts. -Below are a few examples of what you can create using the Zerys API: +- **Project Management Sync**: Sync Zerys projects with project management tools like Trello or Asana. Whenever a new task is added in these tools that requires content creation, trigger a workflow to create a corresponding content order in Zerys, keeping your project plans and content needs aligned. -- Content Management Systems -- Social Media Tools -- Mobile Applications -- Custom Dashboards -- Automation Workflows -- Online Stores -- Data Analysis Platforms -- Interactivity Integrations -- Serverless Applications +- **Feedback Loop Integration**: After content is delivered through Zerys, automatically send it to a review platform like Google Docs or Grammarly. Once reviewed and approved, trigger an event to update the content status in Zerys and notify your team via Slack or email that the content is ready for publishing. diff --git a/components/zerys/package.json b/components/zerys/package.json new file mode 100644 index 0000000000000..10e43bb30a4c6 --- /dev/null +++ b/components/zerys/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/zerys", + "version": "0.6.0", + "description": "Pipedream zerys Components", + "main": "zerys.app.mjs", + "keywords": [ + "pipedream", + "zerys" + ], + "homepage": "https://pipedream.com/apps/zerys", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/zest/README.md b/components/zest/README.md new file mode 100644 index 0000000000000..fbf01d49ade7d --- /dev/null +++ b/components/zest/README.md @@ -0,0 +1,11 @@ +# Overview + +The Zest API offers a platform to curate content for marketing professionals, providing access to high-quality articles, podcasts, and videos. With Pipedream, you can leverage this API to automate content discovery, analysis, and distribution, without the need for manual intervention. By connecting Zest with other apps, you can create custom, serverless workflows that save time, enhance content strategy, and maintain a consistent online presence. + +# Example Use Cases + +- **Content Curation and Distribution Workflow**: Automatically fetch the latest high-engagement articles from Zest and share them on your company's Slack channel, ensuring your team stays updated with the latest marketing trends. + +- **Social Media Integration**: Curate content through Zest and post it directly to your Twitter or LinkedIn profiles with Pipedream's scheduled workflows, engaging your followers with industry-leading insights on autopilot. + +- **Email Newsletter Automation**: Build an automated system that selects top content from Zest every week and compiles it into a Mailchimp campaign, streamlining your newsletter creation process and providing subscribers with valuable resources. diff --git a/components/zip_archive_api/README.md b/components/zip_archive_api/README.md new file mode 100644 index 0000000000000..edf3ff80346f0 --- /dev/null +++ b/components/zip_archive_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Zip Archive API allows you to manage zip archives programmatically. You can create, extract, and modify zip files, making it ideal for automating routine file management tasks. Integrating this API with Pipedream enables you to automate workflows that involve file compression or decompression, combine it with triggers from other apps, and process large batches of files efficiently, saving time and reducing manual effort. + +# Example Use Cases + +- **Automated Backup and Compression for Cloud Storage**: Automatically zip files uploaded to a Dropbox folder and then store the compressed archive in Google Drive. This can be useful for archiving documents and saving space in cloud storage. + +- **Email Attachments Compression**: Monitor an email inbox for new messages with attachments using the Gmail trigger, compress the attachments into a zip file, and upload the archive to a specific FTP server or cloud storage service. This is particularly useful for businesses that regularly receive large attachments and want to streamline storage. + +- **Website Asset Management**: Automatically compress website assets like CSS, JS, and images whenever they are updated in a GitHub repository. After compression, the zip archive could be pushed back to another branch within the repo or deployed to a web server. This workflow helps in optimizing the loading speed of web pages. diff --git a/components/zip_archive_api/actions/compress-files/compress-files.mjs b/components/zip_archive_api/actions/compress-files/compress-files.mjs new file mode 100644 index 0000000000000..0bee12c774fe2 --- /dev/null +++ b/components/zip_archive_api/actions/compress-files/compress-files.mjs @@ -0,0 +1,82 @@ +import app from "../../zip_archive_api.app.mjs"; +import fs from "fs"; +import FormData from "form-data"; + +export default { + key: "zip_archive_api-compress-files", + name: "Compress Files", + description: "Compress files provided through URLs into a zip folder. [See the documentation](https://archiveapi.com/rest-api/file-compression/)", + version: "0.0.1", + type: "action", + props: { + app, + uploadType: { + propDefinition: [ + app, + "uploadType", + ], + }, + archiveName: { + propDefinition: [ + app, + "archiveName", + ], + }, + compressionLevel: { + propDefinition: [ + app, + "compressionLevel", + ], + }, + password: { + propDefinition: [ + app, + "password", + ], + }, + files: { + propDefinition: [ + app, + "files", + ], + }, + }, + async run({ $ }) { + let data = { + files: this.files, + archiveName: this.archiveName, + compressionLevel: this.compressionLevel, + password: this.password, + }; + + if (this.uploadType === "File") { + data = new FormData(); + + if (this.password) data.append("Password", this.password); + if (this.compressionLevel) data.append("CompressionLevel", this.compressionLevel); + data.append("ArchiveName", this.archiveName); + + for (const file of this.files) { + data.append("Files", fs.createReadStream(file)); + } + } + + const headers = this.uploadType === "File" + ? data.getHeaders() + : {}; + + const response = await this.app.compressFiles({ + $, + data, + headers, + responseType: "arraybuffer", + }); + + const tmpPath = `/tmp/${this.archiveName}`; + fs.writeFileSync(tmpPath, response); + + $.export("$summary", `Successfully compressed the files into '${tmpPath}'`); + + return tmpPath; + }, +}; diff --git a/components/zip_archive_api/common/constants.mjs b/components/zip_archive_api/common/constants.mjs new file mode 100644 index 0000000000000..9cdf9ad9797c2 --- /dev/null +++ b/components/zip_archive_api/common/constants.mjs @@ -0,0 +1,6 @@ +export default { + UPLOAD_TYPES: [ + "URL", + "File", + ], +}; diff --git a/components/zip_archive_api/package.json b/components/zip_archive_api/package.json new file mode 100644 index 0000000000000..c6158a2dd99c4 --- /dev/null +++ b/components/zip_archive_api/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pipedream/zip_archive_api", + "version": "0.1.0", + "description": "Pipedream Zip Archive API Components", + "main": "zip_archive_api.app.mjs", + "keywords": [ + "pipedream", + "zip_archive_api" + ], + "homepage": "https://pipedream.com/apps/zip_archive_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.5", + "form-data": "^4.0.0" + } +} diff --git a/components/zip_archive_api/zip_archive_api.app.mjs b/components/zip_archive_api/zip_archive_api.app.mjs new file mode 100644 index 0000000000000..b2fbbb503abc0 --- /dev/null +++ b/components/zip_archive_api/zip_archive_api.app.mjs @@ -0,0 +1,71 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "zip_archive_api", + propDefinitions: { + uploadType: { + type: "string", + label: "Upload Type", + description: "The upload type of the file", + options: constants.UPLOAD_TYPES, + }, + archiveName: { + type: "string", + label: "Archive Name", + description: "Compressed archive name", + }, + compressionLevel: { + type: "integer", + label: "Compression Level", + description: "Archive compression level. Value range: 1-9", + optional: true, + }, + password: { + type: "string", + label: "Password", + description: "The compressed ZIP archive password", + optional: true, + }, + files: { + type: "string[]", + label: "Files URLs", + description: "The URLs or path of the files to be compressed", + }, + file: { + type: "string", + label: "File URL", + description: "The URL or path of the archive to extract the files from", + }, + }, + methods: { + _baseUrl() { + return "https://api.archiveapi.com"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + params, + ...otherOpts + } = opts; + + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + params: { + ...params, + secret: `${this.$auth.secret}`, + }, + }); + }, + async compressFiles(args = {}) { + return this._makeRequest({ + method: "post", + path: "/zip", + ...args, + }); + }, + }, +}; diff --git a/components/zixflow/README.md b/components/zixflow/README.md new file mode 100644 index 0000000000000..ad199ffd62479 --- /dev/null +++ b/components/zixflow/README.md @@ -0,0 +1,11 @@ +# Overview + +The Zixflow API allows for the automation of business process workflows, enabling the creation, management, and optimization of tasks across various functions. Utilizing this API in Pipedream, one can automate repetitive tasks, integrate with other services, and trigger actions based on specific conditions, helping to streamline business operations and increase efficiency. + +# Example Use Cases + +- **Automate Task Creation**: Upon receiving a new customer inquiry via a CRM like Salesforce, automatically create a task in Zixflow to follow up, ensuring no lead is missed and response times are fast. + +- **Sync Project Updates**: When a project status is updated in Zixflow, trigger a workflow that posts this update to a Slack channel, keeping the team informed in real-time without manual intervention. + +- **Dynamic Reporting**: Set up a scheduled workflow in Pipedream to retrieve data from Zixflow on a regular basis, compile a report, and email it using SendGrid, ensuring stakeholders stay updated with the latest project metrics and progress. diff --git a/components/zixflow/actions/common/constants.mjs b/components/zixflow/actions/common/constants.mjs new file mode 100644 index 0000000000000..bf3c027be826d --- /dev/null +++ b/components/zixflow/actions/common/constants.mjs @@ -0,0 +1,100 @@ +export default { + ICON_TYPES: [ + { + label: "Emoji", + value: "emoji", + }, + { + label: "Interaction", + value: "interaction", + }, + { + label: "Messaging App", + value: "messaging_app", + }, + ], + INTERACTION_TYPES: [ + { + label: "Call", + value: "call", + }, + { + label: "Meeting", + value: "meeting", + }, + { + label: "Message", + value: "message", + }, + { + label: "Coffee", + value: "coffee", + }, + { + label: "Lunch", + value: "lunch", + }, + { + label: "Event", + value: "event", + }, + { + label: "Drink", + value: "drink", + }, + ], + MESSAGING_TYPES: [ + { + label: "Whatsapp", + value: "whatsapp", + }, + { + label: "Twitter", + value: "twitter", + }, + { + label: "LinkedIn", + value: "linkedin", + }, + { + label: "Hangout", + value: "hangout", + }, + { + label: "Skype", + value: "skype", + }, + { + label: "Slack", + value: "slack", + }, + { + label: "iMessage", + value: "imessage", + }, + { + label: "Facebook Messenger", + value: "facebook_messenger", + }, + { + label: "Signal", + value: "signal", + }, + { + label: "Discord", + value: "discord", + }, + { + label: "WeChat", + value: "wechat", + }, + { + label: "Telegram", + value: "telegram", + }, + { + label: "Viber", + value: "viber", + }, + ], +}; diff --git a/components/zixflow/actions/create-activity/create-activity.mjs b/components/zixflow/actions/create-activity/create-activity.mjs new file mode 100644 index 0000000000000..a139e53f98858 --- /dev/null +++ b/components/zixflow/actions/create-activity/create-activity.mjs @@ -0,0 +1,101 @@ +import app from "../../zixflow.app.mjs"; +import constants from "../common/constants.mjs"; + +export default { + key: "zixflow-create-activity", + name: "Create Activity", + description: "Creates a new activity or task within Zixflow. [See the documentation](https://docs.zixflow.com/api-reference/activity-list/create)", + version: "0.0.1", + type: "action", + props: { + app, + iconType: { + propDefinition: [ + app, + "iconType", + ], + reloadProps: true, + }, + iconValue: { + type: "string", + label: "Icon Value", + description: "Defines the specific value of the icon based on the iconType", + }, + scheduleAt: { + propDefinition: [ + app, + "scheduleAt", + ], + }, + name: { + propDefinition: [ + app, + "name", + ], + }, + description: { + propDefinition: [ + app, + "description", + ], + }, + statusId: { + propDefinition: [ + app, + "statusId", + ], + optional: true, + }, + associationType: { + propDefinition: [ + app, + "associationType", + ], + optional: true, + }, + associatedId: { + propDefinition: [ + app, + "associatedId", + (c) => ({ + associationType: c.associationType, + }), + ], + optional: true, + }, + }, + async additionalProps() { + const props = { + iconValue: { + type: "string", + label: "Icon Value", + description: "Defines the specific value of the icon based on the iconType", + }, + }; + + if (this.iconType !== "emoji") { + props.iconValue.options = this.iconType === "interaction" + ? constants.INTERACTION_TYPES + : constants.MESSAGING_TYPES; + } + + return props; + }, + async run({ $ }) { + const response = await this.app.createActivity({ + $, + data: { + iconType: this.iconType, + iconValue: this.iconValue, + scheduleAt: this.scheduleAt, + name: this.name, + description: this.description, + status: this.statusId, + associated: this.associatedId, + }, + }); + + $.export("$summary", `Successfully created activity with title '${this.name}'`); + return response; + }, +}; diff --git a/components/zixflow/actions/delete-activity/delete-activity.mjs b/components/zixflow/actions/delete-activity/delete-activity.mjs new file mode 100644 index 0000000000000..4b11c47d905ce --- /dev/null +++ b/components/zixflow/actions/delete-activity/delete-activity.mjs @@ -0,0 +1,27 @@ +import app from "../../zixflow.app.mjs"; + +export default { + key: "zixflow-delete-activity", + name: "Delete Activity", + description: "Deletes an existing activity or task from Zixflow. [See the documentation](https://docs.zixflow.com/api-reference/activity-list/delete)", + version: "0.0.1", + type: "action", + props: { + app, + activityId: { + propDefinition: [ + app, + "activityId", + ], + }, + }, + async run({ $ }) { + const response = await this.app.deleteActivity({ + $, + activityId: this.activityId, + }); + + $.export("$summary", `Successfully deleted activity with ID: ${this.activityId}`); + return response; + }, +}; diff --git a/components/zixflow/actions/get-activities/get-activities.mjs b/components/zixflow/actions/get-activities/get-activities.mjs new file mode 100644 index 0000000000000..1974d0cccff7a --- /dev/null +++ b/components/zixflow/actions/get-activities/get-activities.mjs @@ -0,0 +1,58 @@ +import app from "../../zixflow.app.mjs"; + +export default { + key: "zixflow-get-activities", + name: "Get Activities", + description: "Retrieve a list of activities. [See the documentation](https://docs.zixflow.com/api-reference/activity-list/get#body)", + version: "0.0.1", + type: "action", + props: { + app, + filter: { + propDefinition: [ + app, + "filter", + ], + }, + sort: { + propDefinition: [ + app, + "sort", + ], + }, + limit: { + propDefinition: [ + app, + "limit", + ], + }, + offset: { + propDefinition: [ + app, + "offset", + ], + }, + }, + async run({ $ }) { + const filter = typeof this.filter === "string" + ? JSON.parse(this.filter) + : this.filter; + + const sort = typeof this.sort === "string" + ? JSON.parse(this.sort) + : this.sort; + + const response = await this.app.getActivities({ + $, + data: { + sort, + filter, + limit: this.limit, + offset: this.offset, + }, + }); + $.export("$summary", `Successfully retrieved ${response.data.length} activities`); + + return response; + }, +}; diff --git a/components/zixflow/actions/update-activity/update-activity.mjs b/components/zixflow/actions/update-activity/update-activity.mjs new file mode 100644 index 0000000000000..3fed3bcd0d9fb --- /dev/null +++ b/components/zixflow/actions/update-activity/update-activity.mjs @@ -0,0 +1,118 @@ +import app from "../../zixflow.app.mjs"; +import constants from "../common/constants.mjs"; + +export default { + key: "zixflow-update-activity", + name: "Update Activity", + description: "Updates an existing activity or task in Zixflow. [See the documentation](https://docs.zixflow.com/api-reference/activity-list/edit)", + version: "0.0.1", + type: "action", + props: { + app, + activityId: { + propDefinition: [ + app, + "activityId", + ], + reloadProps: true, + }, + iconType: { + propDefinition: [ + app, + "iconType", + ], + optional: true, + reloadProps: true, + }, + iconValue: { + propDefinition: [ + app, + "iconValue", + (c) => ({ + iconType: c.iconType, + }), + ], + optional: true, + }, + scheduleAt: { + propDefinition: [ + app, + "scheduleAt", + ], + optional: true, + }, + name: { + propDefinition: [ + app, + "name", + ], + optional: true, + }, + description: { + propDefinition: [ + app, + "description", + ], + optional: true, + }, + statusId: { + propDefinition: [ + app, + "statusId", + ], + optional: true, + }, + associationType: { + propDefinition: [ + app, + "associationType", + ], + optional: true, + }, + associatedId: { + propDefinition: [ + app, + "associatedId", + (c) => ({ + associationType: c.associationType, + }), + ], + optional: true, + }, + }, + async additionalProps() { + const props = { + iconValue: { + type: "string", + label: "Icon Value", + description: "Defines the specific value of the icon based on the iconType", + }, + }; + + if (this.iconType !== "emoji") { + props.iconValue.options = this.iconType === "interaction" + ? constants.INTERACTION_TYPES + : constants.MESSAGING_TYPES; + } + + return props; + }, + async run({ $ }) { + const response = await this.app.updateActivity({ + $, + activityId: this.activityId, + data: { + iconType: this.iconType, + iconValue: this.iconValue, + scheduleAt: this.scheduleAt, + name: this.name, + description: this.description, + status: this.statusId, + associated: this.associatedId, + }, + }); + + $.export("$summary", `Successfully updated activity with ID ${this.activityId}`); + return response; + }, +}; diff --git a/components/zixflow/package.json b/components/zixflow/package.json index b129f4383bd46..1e7f3faf5ba77 100644 --- a/components/zixflow/package.json +++ b/components/zixflow/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/zixflow", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Zixflow Components", "main": "zixflow.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.4" } -} \ No newline at end of file +} diff --git a/components/zixflow/zixflow.app.mjs b/components/zixflow/zixflow.app.mjs index a5fbe1cec22e5..e3a566bbac42b 100644 --- a/components/zixflow/zixflow.app.mjs +++ b/components/zixflow/zixflow.app.mjs @@ -1,11 +1,225 @@ +import { axios } from "@pipedream/platform"; +import constants from "./actions/common/constants.mjs"; + export default { type: "app", app: "zixflow", - propDefinitions: {}, + propDefinitions: { + activityId: { + type: "string", + label: "Activity ID", + description: "The unique identifier for the activity or task", + async options({ page }) { + const { data: resources } = await this.getActivities({ + data: { + offset: page * 100, + limit: 100, + }, + }); + + return resources.map(({ + _id, name, + }) => ({ + value: _id, + label: name, + })); + }, + }, + associationType: { + type: "string", + label: "Association Type", + description: "The association type.", + async options({ page }) { + const { data: resources } = await this.getCollections({ + data: { + offset: page * 100, + limit: 100, + }, + }); + + return resources.map(({ + _id, name, + }) => ({ + value: _id, + label: name, + })); + }, + }, + associatedId: { + type: "string", + label: "Associated ID", + description: "The ID of the collection record associated with the activity.", + async options({ + page, associationType, + }) { + const { data: resources } = await this.getCollectionsRecords({ + associationType, + data: { + offset: page * 100, + limit: 100, + }, + }); + + return resources.map(({ + _id, name, + }) => ({ + value: _id, + label: name, + })); + }, + }, + statusId: { + type: "string", + label: "Status ID", + description: "The unique identifier for the status attribute. To get the Status ID, please use Get Activities action to get all activities, then investigate the `status` field in each record", + async options({ page }) { + const { data: resources } = await this.getActivities({ + data: { + offset: page * 100, + limit: 100, + }, + }); + + return resources.filter((r) => !!r.status).map(({ + status: { + _id, name, + }, + }) => ({ + value: _id, + label: name, + })); + }, + }, + iconType: { + type: "string", + label: "Icon Type", + description: "Specifies the type of icon associated with the activity", + options: constants.ICON_TYPES, + }, + iconValue: { + type: "string", + label: "Icon Value", + description: "Defines the specific value of the icon based on the iconType", + async options({ iconType }) { + if (iconType === "interaction") return constants.INTERACTION_TYPES; + if (iconType === "messaging_app") return constants.INTERACTION_TYPES; + + return []; + }, + }, + name: { + type: "string", + label: "Activity Name", + description: "The name or title of the activity", + }, + scheduleAt: { + type: "string", + label: "Schedule At", + description: "Specifies the scheduled time for the activity in the format `YYYY-MM-DDTHH:mm:ss.SSSZ`", + }, + description: { + type: "string", + label: "Description", + description: "A description providing additional details about the activity", + optional: true, + }, + status: { + type: "string", + label: "Description", + description: "A description providing additional details about the activity", + optional: true, + }, + filter: { + type: "string", + label: "Filter", + description: "An array that will eventually allow users to define specific criteria for filtering data. Currently, it is an empty array, indicating that no filtering is applied at this time.", + optional: true, + }, + sort: { + type: "string", + label: "Sort", + description: "An array that will eventually enable users to specify sorting criteria for the data. Like the filter array, it is currently empty, implying that no sorting is applied in the current context.", + optional: true, + }, + limit: { + type: "integer", + label: "Limit", + description: "The number of records to be returned, set to 10 in this instance. This parameter restricts the response to a specific quantity of records.", + optional: true, + }, + offset: { + type: "integer", + label: "Offset", + description: "The starting point from which the records are to be fetched within the entire dataset. In this case, it is set to 0, indicating that retrieval should commence from the beginning of the dataset.", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.zixflow.com/api/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + async createActivity(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/collection-records/activity-list", + ...args, + }); + }, + async getCollections(args = {}) { + return this._makeRequest({ + path: "/collections", + ...args, + }); + }, + async getCollectionsRecords({ + associationType, ...args + }) { + return this._makeRequest({ + path: `/collection-records/${associationType}/query`, + method: "post", + ...args, + }); + }, + async getActivities(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/collection-records/activity-list/query", + ...args, + }); + }, + async updateActivity({ + activityId, ...args + }) { + return this._makeRequest({ + method: "PATCH", + path: `/collection-records/activity-list/${activityId}`, + ...args, + }); + }, + async deleteActivity({ + activityId, ...args + }) { + return this._makeRequest({ + method: "DELETE", + path: `/collection-records/activity-list/${activityId}`, + ...args, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/zoho_analytics/README.md b/components/zoho_analytics/README.md new file mode 100644 index 0000000000000..b6e33db4473b9 --- /dev/null +++ b/components/zoho_analytics/README.md @@ -0,0 +1,26 @@ +# Overview + +The Zoho Analytics API lets you harness the power of your data by automating complex analytics tasks. With this API, you can create, access, and manage reports, dashboards, and KPI widgets programmatically. Integrate with Pipedream's serverless platform to trigger actions based on events, sync data across apps, and automate workflows without managing servers. Whether it's updating datasets in real-time or sending reports to your team, the possibilities stretch as wide as your data does. + +# Example Use Cases + +- **Sync New Leads to Zoho Analytics**: Whenever a new lead is added to your CRM, like Salesforce, use Pipedream to automatically push this data into Zoho Analytics. This keeps your datasets fresh and your insights accurate. + +- **Automate Weekly Sales Reports**: Schedule a weekly event in Pipedream to fetch sales data from Zoho Analytics, format it, and send a well-crafted report via Gmail or Slack to keep your team informed. + +- **Track Social Media Campaign Performance**: Connect your social media platforms, like Facebook Ads, to Zoho Analytics through Pipedream. Analyze campaign data and automatically adjust bids or budgets based on performance metrics. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). + + + diff --git a/components/zoho_assist/README.md b/components/zoho_assist/README.md new file mode 100644 index 0000000000000..9a35b7e09bd6e --- /dev/null +++ b/components/zoho_assist/README.md @@ -0,0 +1,23 @@ +# Overview + +The Zoho Assist API provides remote support and access functionalities, enabling automated session management, customer support, and IT operations. By integrating it with Pipedream, you can craft serverless workflows that respond to various triggers and perform actions like creating support sessions, fetching session details, or managing users. The API's versatility in Pipedream allows for streamlined coordination between support requests and responses, facilitating seamless IT support and management tasks. + +# Example Use Cases + +- **Automated Support Session Creation**: Automate the creation of support sessions in response to customer tickets from a helpdesk platform like Zendesk. When a new ticket is received, Pipedream can trigger a Zoho Assist session and link the session info back to the ticket for quick resolution. + +- **Session Status Monitoring**: Monitor ongoing Zoho Assist sessions and log their status to a Google Sheet. Use this for reporting, auditing, or triggering follow-ups based on session outcomes, ensuring nothing falls through the cracks. + +- **User Management Sync**: Sync user accounts between Zoho Assist and your internal systems, such as an HR platform. Whenever a new employee is onboarded, automatically create their Zoho Assist account to grant them the necessary access privileges. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_bigin/README.md b/components/zoho_bigin/README.md new file mode 100644 index 0000000000000..299e74bfa01ef --- /dev/null +++ b/components/zoho_bigin/README.md @@ -0,0 +1,23 @@ +# Overview + +The Zoho Bigin API provides access to Zoho Bigin's CRM features, allowing you to manage contacts, deals, and tasks programmatically. With Pipedream, you can harness this API to create no-code, serverless workflows that automate tasks between Zoho Bigin and multiple other apps. You can sync data, respond to events in real-time, and augment the capabilities of Zoho Bigin by connecting to over 300 other apps supported by Pipedream. + +# Example Use Cases + +- **Sync New Bigin Contacts to Google Sheets**: When a new contact is added in Zoho Bigin, this workflow automatically appends the contact's details to a Google Sheets spreadsheet. This is useful for maintaining backup data on contacts or providing other teams with real-time access to contact information. + +- **Create Bigin Contacts from Shopify Orders**: Whenever a new order is placed on Shopify, this workflow creates a corresponding contact in Zoho Bigin, ensuring your CRM is always up to date with customer information from your e-commerce platform. + +- **Send Slack Notifications for New Bigin Deals**: Set up a workflow that listens for new deals in Zoho Bigin and sends a notification with the deal details to a designated Slack channel. This keeps your team informed immediately when potential revenue is on the horizon. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_bookings/README.md b/components/zoho_bookings/README.md new file mode 100644 index 0000000000000..c73321ebfd48d --- /dev/null +++ b/components/zoho_bookings/README.md @@ -0,0 +1,23 @@ +# Overview + +The Zoho Bookings API lets you integrate your booking system with other apps and services, automating tasks like scheduling, rescheduling, and cancelling appointments. On Pipedream, you can use this API to trigger workflows, process booking data, synchronize schedules across platforms, and create custom notifications. It's a powerful tool for service-based businesses looking to streamline their operations and enhance customer interaction. + +# Example Use Cases + +- **Sync Bookings with Google Calendar**: Automatically add new bookings from Zoho Bookings to a Google Calendar, allowing for seamless calendar management. Any changes or cancellations in Zoho Bookings can also update Google Calendar in real time, ensuring all schedules are in sync. + +- **Send Personalized Notifications**: Craft and send personalized email or SMS notifications through Twilio or SendGrid when a new booking is made, or a booking is rescheduled. Include details like the time, service, and any special instructions, creating a tailored experience for each customer. + +- **Manage Customer Follow-ups**: Connect Zoho Bookings with a CRM platform like Salesforce or HubSpot. When an appointment concludes, trigger a workflow that creates a follow-up task in the CRM, ensuring no customer is overlooked and opportunities for additional services or feedback are captured. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_bookings/package.json b/components/zoho_bookings/package.json index 2e80becdf0560..2dd51d8d6ae0a 100644 --- a/components/zoho_bookings/package.json +++ b/components/zoho_bookings/package.json @@ -16,4 +16,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/zoho_books/README.md b/components/zoho_books/README.md index 6017450996d88..d409947d613b0 100644 --- a/components/zoho_books/README.md +++ b/components/zoho_books/README.md @@ -1,32 +1,8 @@ # Overview -Zoho Books is an online accounting software that helps you manage your -finances, get paid faster, and work collaboratively with your clients and -teams. The Zoho Books API provides a wide variety of integration possibilities -to connect with your accounting software and build powerful, efficient -applications. With the API, you can quickly and easily add functionality that -simplifies your complex tasks, such as invoicing, payments, bank -reconciliation, reporting, and more. - -Below are some of the features, tasks, and applications that can be built using -the Zoho Books API: - -- Create, update, and delete customers -- Manage invoice payments -- Automate bank reconciliations -- Generate financial statements and reports -- Track time and billable expenses -- Sync data across multiple applications -- Integrate with online payment platforms -- Automate GST compliance reporting -- Post journal entries -- Link tax payments to invoices -- View cash flow reports -- Generate client statements -- Implement multi-currency support -- Monitor inventory and sales performance - -## Getting Started +Zoho Books API unlocks the potential to automate and streamline accounting tasks by integrating with Pipedream's serverless platform. With this powerful combo, you can automate invoicing, manage your accounts, reconcile bank transactions, and handle contacts and items without manual input. By setting up event-driven workflows, you can ensure data consistency across platforms, trigger notifications, and generate reports, all while saving time and reducing human error. + +# Getting Started 1. First, sign up for Pipedream at [https://pipedream.com](https://pipedream.com). 2. [Create a new workflow](https://pipedream.com/new). @@ -35,3 +11,24 @@ the Zoho Books API: 5. Once you've added a step, press the **Connect Zoho Books** button near the top. If this is your first time authorizing Pipedream's access to your Zoho Books account, you'll be prompted to accept that access, and Pipedream will store the authorization grant to enable the workflow to access the API. If you've already linked a Zoho Books account via Pipedream, pressing **Connect Zoho Books** will list any existing accounts you've linked. Once you've connected your account, you can run your workflow and fetch data from the API. You can change any of the code associated with this step, changing the API endpoint you'd like to retrieve data from, or modifying the results in any way. + +# Example Use Cases + +- **Invoice Automation**: Create or update invoices in Zoho Books whenever a new order is placed on your e-commerce platform like Shopify. This can trigger an email to the customer with the invoice attached, streamlining the billing process. + +- **Expense Tracking**: Connect Zoho Books to your expense management app like Expensify. Whenever a new expense report is approved in Expensify, create a corresponding expense record in Zoho Books, keeping your accounts up to date. + +- **Customer Synchronization**: Sync new customer data from a CRM like Salesforce to Zoho Books. When a new contact is added in Salesforce, automatically create or update the customer information in Zoho Books, ensuring accurate and consistent customer data across business tools. + + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). diff --git a/components/zoho_books/actions/create-customer-payment/create-customer-payment.mjs b/components/zoho_books/actions/create-customer-payment/create-customer-payment.mjs index bd019dddc916d..4d94c8e00c936 100644 --- a/components/zoho_books/actions/create-customer-payment/create-customer-payment.mjs +++ b/components/zoho_books/actions/create-customer-payment/create-customer-payment.mjs @@ -1,131 +1,137 @@ // legacy_hash_id: a_XziR2J -import { axios } from "@pipedream/platform"; +import { PAYMENT_MODE_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import zohoBooks from "../../zoho_books.app.mjs"; export default { key: "zoho_books-create-customer-payment", name: "Create Customer Payment", - description: "Creates a new payment.", - version: "0.2.1", + description: "Creates a new payment. [See the documentation](https://www.zoho.com/books/api/v3/customer-payments/#create-a-payment)", + version: "0.3.0", type: "action", props: { - zoho_books: { - type: "app", - app: "zoho_books", - }, - organization_id: { - type: "string", - description: "In Zoho Books, your business is termed as an organization. If you have multiple businesses, you simply set each of those up as an individual organization. Each organization is an independent Zoho Books Organization with it's own organization ID, base currency, time zone, language, contacts, reports, etc.\n\nThe parameter `organization_id` should be sent in with every API request to identify the organization.\n\nThe `organization_id` can be obtained from the GET `/organizations` API's JSON response. Alternatively, it can be obtained from the **Manage Organizations** page in the admin console.", - }, - customer_id: { - type: "string", - description: "Customer ID of the customer involved in the payment.", - }, - payment_mode: { - type: "string", - description: "Mode through which payment is made. This can be `check`, `cash`, `creditcard`, `banktransfer`, `bankremittance`, `autotransaction` or `others`. Max-length [100]", - options: [ - "check", - "cash", - "creditcard", - "banktransfer", - "bankremittance", - "autotransaction", - "others", + zohoBooks, + customerId: { + propDefinition: [ + zohoBooks, + "customerId", ], }, - invoices: { - type: "any", - description: "List of invoices associated with the payment. Each invoice object contains `invoice_id`, `invoice_number`, `date`, `invoice_amount`, `amount_applied` and `balance_amount`.", + paymentMode: { + type: "string", + label: "Payment Mode", + description: "Mode through which payment is made.", + options: PAYMENT_MODE_OPTIONS, }, amount: { type: "string", + label: "Amount", description: "Amount paid in the respective payment.", }, date: { type: "string", + label: "Date", description: "Date on which payment is made. Format [yyyy-mm-dd]", }, - reference_number: { + referenceNumber: { type: "string", + label: "Reference Number", description: "Reference number generated for the payment. A string of your choice can also be used as the reference number. Max-length [100]", optional: true, }, description: { type: "string", + label: "Description", description: "Description about the payment.", optional: true, }, - exchange_rate: { + invoices: { + type: "string[]", + label: "Invoices", + description: "List of invoice objects associated with the payment. Each invoice object contains `invoice_id`, `invoice_number`, `date`, `invoice_amount`, `amount_applied` and `balance_amount`. **Example: {\"invoice_id\": \"90300000079426\", \"amount_applied\": 450}**", + }, + exchangeRate: { type: "string", + label: "Exchange Rate", description: "Exchange rate for the currency used in the invoices and customer's currency. The payment amount would be the multiplicative product of the original amount and the exchange rate. Default is 1.", optional: true, }, - bank_charges: { + bankCharges: { type: "string", + label: "Bank Charges", description: "Denotes any additional bank charges.", optional: true, }, - custom_fields: { - type: "any", - description: "Additional fields for the payments.", + customFields: { + propDefinition: [ + zohoBooks, + "customFields", + ], + description: "A list of Additional field objects for the payments. **Example: {\"label\": \"label\", \"value\": 129890}**", optional: true, }, - invoice_id: { - type: "string", - description: "ID of the invoice to get payments of.", - optional: true, + invoiceId: { + propDefinition: [ + zohoBooks, + "invoiceId", + ({ customerId }) => ({ + customerId, + }), + ], }, - amount_applied: { + amountApplied: { type: "string", + label: "Amount Applied", description: "Amount paid for the invoice.", optional: true, }, - tax_amount_withheld: { + taxAmountWithheld: { type: "string", + label: "Tax Amount Withheld", description: "Amount withheld for tax.", optional: true, }, - account_id: { - type: "string", - description: "ID of the cash/bank account the payment has to be deposited.", + accountId: { + propDefinition: [ + zohoBooks, + "accountId", + ], optional: true, }, - contact_persons: { - type: "string", - description: "IDs of the contact personsthe thank you mail has to be triggered.", + contactPersons: { + propDefinition: [ + zohoBooks, + "contactPersons", + ({ customerId }) => ({ + customerId, + }), + ], optional: true, }, }, async run({ $ }) { - //See the API docs: https://www.zoho.com/books/api/v3/#Customer-Payments_Create_a_payment - - if (!this.organization_id || !this.customer_id || !this.payment_mode || !this.invoices || !this.amount || !this.date) { - throw new Error("Must provide organization_id, customer_id, payment_mode, invoices, amount, and date parameters."); - } - - return await axios($, { - method: "post", - url: `https://books.${this.zoho_books.$auth.base_api_uri}/api/v3/customerpayments?organization_id=${this.organization_id}`, - headers: { - Authorization: `Zoho-oauthtoken ${this.zoho_books.$auth.oauth_access_token}`, - }, + const response = await this.zohoBooks.createCustomerPayment({ + $, data: { - customer_id: this.customer_id, - payment_mode: this.payment_mode, + customer_id: this.customerId, + payment_mode: this.paymentMode, amount: this.amount, date: this.date, - reference_number: this.reference_number, + reference_number: this.referenceNumber, description: this.description, - invoices: this.invoices, - exchange_rate: this.exchange_rate, - bank_charges: this.bank_charges, - custom_fields: this.custom_fields, - invoice_id: this.invoice_id, - amount_applied: this.amount_applied, - tax_amount_withheld: this.tax_amount_withheld, - account_id: this.account_id, - contact_persons: this.contact_persons, + invoices: parseObject(this.invoices), + exchange_rate: this.exchangeRate && parseFloat(this.exchangeRate), + bank_charges: this.bankCharges && parseFloat(this.bankCharges), + custom_fields: parseObject(this.customFields), + invoice_id: this.invoiceId, + amount_applied: this.amountApplied && parseFloat(this.amountApplied), + tax_amount_withheld: this.taxAmountWithheld && parseFloat(this.taxAmountWithheld), + account_id: this.accountId, + contact_persons: parseObject(this.contactPersons), }, }); + + $.export("$summary", `Customer payment successfully created with Id: ${response.payment.payment_id}`); + return response; }, }; diff --git a/components/zoho_books/actions/create-customer/create-customer.mjs b/components/zoho_books/actions/create-customer/create-customer.mjs new file mode 100644 index 0000000000000..2bce073964637 --- /dev/null +++ b/components/zoho_books/actions/create-customer/create-customer.mjs @@ -0,0 +1,166 @@ +// legacy_hash_id: a_Xzi1qo +import { + CUSTOMER_SUB_TYPE_OPTIONS, + LANGUAGE_CODE_OPTIONS, +} from "../../common/constants.mjs"; +import { + clearObj, + parseObject, +} from "../../common/utils.mjs"; +import zohoBooks from "../../zoho_books.app.mjs"; + +export default { + key: "zoho_books-create-customer", + name: "Create Customer", + description: "Creates a new customer. [See the documentation](https://www.zoho.com/books/api/v3/items/#create-an-item)", + version: "0.0.1", + type: "action", + props: { + zohoBooks, + contactName: { + type: "string", + label: "Contact Name", + description: "Display Name of the contact. Max-length [200].", + }, + companyName: { + type: "string", + label: "Company Name", + description: "Company Name of the contact. Max-length [200].", + optional: true, + }, + website: { + type: "string", + label: "Website", + description: "Website of the contact.", + optional: true, + }, + languageCode: { + type: "string", + label: "Language Code", + description: "The language of a contact.", + options: LANGUAGE_CODE_OPTIONS, + optional: true, + }, + customerSubType: { + type: "string", + label: "Customer Sub Type", + description: "Type of the customer.", + options: CUSTOMER_SUB_TYPE_OPTIONS, + optional: true, + }, + creditLimit: { + type: "string", + label: "Credit Limit", + description: "Credit limit for a customer.", + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "An array of tag objects. **Example: {\"tag_id\":\"124567890\",\"tag_option_id\":\"1234567890\"}**", + optional: true, + }, + isPortalEnabled: { + type: "boolean", + label: "Is Portal Enabled", + description: "To enable client portal for the contact.", + optional: true, + }, + currencyId: { + propDefinition: [ + zohoBooks, + "currencyId", + ], + optional: true, + }, + paymentTerms: { + propDefinition: [ + zohoBooks, + "paymentTerms", + ], + description: "Net payment term for the customer.", + optional: true, + }, + paymentTermsLabel: { + propDefinition: [ + zohoBooks, + "paymentTermsLabel", + ], + description: "Label for the paymet due details.", + optional: true, + }, + notes: { + propDefinition: [ + zohoBooks, + "notes", + ], + description: "Commennts about the payment made by the contact.", + optional: true, + }, + exchangeRate: { + propDefinition: [ + zohoBooks, + "exchangeRate", + ], + description: "Exchange rate for the opening balance.", + optional: true, + }, + vatTreatment: { + propDefinition: [ + zohoBooks, + "vatTreatment", + ], + optional: true, + }, + gstNo: { + propDefinition: [ + zohoBooks, + "gstNo", + ], + optional: true, + }, + avataxUseCode: { + propDefinition: [ + zohoBooks, + "avataxUseCode", + ], + optional: true, + }, + taxId: { + propDefinition: [ + zohoBooks, + "taxId", + ], + description: "ID of the tax to be associated to the estimate.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.zohoBooks.createContact({ + $, + data: clearObj({ + contact_name: this.contactName, + company_name: this.companyName, + website: this.website, + language_code: this.languageCode, + contact_type: "customer", + customer_sub_type: this.customerSubType, + credit_limit: this.creditLimit, + tags: parseObject(this.tags), + is_portal_enabled: this.isPortalEnabled, + currency_id: this.currencyId, + payment_terms: this.paymentTerms, + payment_terms_label: this.paymentTermsLabel, + notes: this.notes, + exchange_rate: this.exchangeRate && parseFloat(this.exchangeRate), + vat_treatment: this.vatTreatment, + gst_no: this.gstNo, + avatax_use_code: this.avataxUseCode, + tax_id: this.taxId, + }), + }); + + $.export("$summary", `Contact successfully created with Id: ${response.contact.contact_id}`); + return response; + }, +}; diff --git a/components/zoho_books/actions/create-employee/create-employee.mjs b/components/zoho_books/actions/create-employee/create-employee.mjs index f11fe759efd11..9c0c005a67db9 100644 --- a/components/zoho_books/actions/create-employee/create-employee.mjs +++ b/components/zoho_books/actions/create-employee/create-employee.mjs @@ -1,47 +1,35 @@ // legacy_hash_id: a_rJiaL2 -import { axios } from "@pipedream/platform"; +import zohoBooks from "../../zoho_books.app.mjs"; export default { key: "zoho_books-create-employee", name: "Create Employee", - description: "Creates an employee for an expense.", - version: "0.2.1", + description: "Creates an employee for an expense. [See the documentation](https://www.zoho.com/books/api/v3/expenses/#create-an-employee)", + version: "0.3.0", type: "action", props: { - zoho_books: { - type: "app", - app: "zoho_books", - }, - organization_id: { - type: "string", - description: "In Zoho Books, your business is termed as an organization. If you have multiple businesses, you simply set each of those up as an individual organization. Each organization is an independent Zoho Books Organization with it's own organization ID, base currency, time zone, language, contacts, reports, etc.\n\nThe parameter `organization_id` should be sent in with every API request to identify the organization.\n\nThe `organization_id` can be obtained from the GET `/organizations` API's JSON response. Alternatively, it can be obtained from the **Manage Organizations** page in the admin console.", - }, + zohoBooks, name: { type: "string", + label: "Name", description: "Name of the employee.", }, email: { type: "string", + label: "Email", description: "Email of the employee.", }, }, async run({ $ }) { - // See the API docs: https://www.zoho.com/books/api/v3/#Expenses_Create_an_employee - - if (!this.organization_id || !this.name || !this.email) { - throw new Error("Must provide organization_id, name, and email parameters."); - } - - return await axios($, { - method: "post", - url: `https://books.${this.zoho_books.$auth.base_api_uri}/api/v3/employees?organization_id=${this.organization_id}`, - headers: { - Authorization: `Zoho-oauthtoken ${this.zoho_books.$auth.oauth_access_token}`, - }, + const response = await this.zohoBooks.createEmployee({ + $, data: { name: this.name, email: this.email, }, }); + + $.export("$summary", `Employee successfully created with Id: ${response.employee.employee_id}`); + return response; }, }; diff --git a/components/zoho_books/actions/create-estimate/create-estimate.mjs b/components/zoho_books/actions/create-estimate/create-estimate.mjs new file mode 100644 index 0000000000000..2e137819a4d85 --- /dev/null +++ b/components/zoho_books/actions/create-estimate/create-estimate.mjs @@ -0,0 +1,391 @@ +// legacy_hash_id: a_Xzi1qo +import { parseObject } from "../../common/utils.mjs"; +import zohoBooks from "../../zoho_books.app.mjs"; + +export default { + key: "zoho_books-create-estimate", + name: "Create Estimate", + description: "Creates a new estimate. [See the documentation](https://www.zoho.com/books/api/v3/estimates/#create-an-estimate)", + version: "0.0.1", + type: "action", + props: { + zohoBooks, + customerId: { + propDefinition: [ + zohoBooks, + "customerId", + ], + }, + currencyId: { + propDefinition: [ + zohoBooks, + "currencyId", + ], + optional: true, + }, + contactPersons: { + propDefinition: [ + zohoBooks, + "contactPersons", + ({ customerId }) => ({ + customerId, + }), + ], + description: "Array if contact person(S) for whom estimate has to be sent.", + optional: true, + }, + templateId: { + propDefinition: [ + zohoBooks, + "pdfTemplateId", + ], + optional: true, + }, + placeOfSupply: { + propDefinition: [ + zohoBooks, + "placeOfSupply", + ], + optional: true, + }, + gstTreatment: { + propDefinition: [ + zohoBooks, + "gstTreatment", + ], + optional: true, + }, + gstNo: { + propDefinition: [ + zohoBooks, + "gstNo", + ], + optional: true, + }, + estimateNumber: { + type: "string", + label: "Estimate Number", + description: "Search estimates by estimate number.", + optional: true, + }, + referenceNumber: { + type: "string", + label: "Reference Number", + description: "Search estimates by reference number.", + optional: true, + }, + date: { + type: "string", + label: "Date", + description: "Search estimates by estimate date.", + optional: true, + }, + expiryDate: { + type: "string", + label: "Expiry Date", + description: "The date of expiration of the estimates.", + optional: true, + }, + exchangeRate: { + propDefinition: [ + zohoBooks, + "exchangeRate", + ], + optional: true, + }, + discount: { + propDefinition: [ + zohoBooks, + "discount", + ], + optional: true, + description: "Discount applied to the invoice. It can be either in % or in amount. e.g. 12.5% or 190. Max-length [100]", + }, + isDiscountBeforeTax: { + propDefinition: [ + zohoBooks, + "isDiscountBeforeTax", + ], + optional: true, + }, + discountType: { + propDefinition: [ + zohoBooks, + "discountType", + ], + optional: true, + }, + isInclusiveTax: { + propDefinition: [ + zohoBooks, + "isInclusiveTax", + ], + optional: true, + }, + customBody: { + propDefinition: [ + zohoBooks, + "customBody", + ], + optional: true, + }, + customSubject: { + propDefinition: [ + zohoBooks, + "customSubject", + ], + optional: true, + }, + salespersonName: { + propDefinition: [ + zohoBooks, + "salespersonName", + ], + optional: true, + }, + customFields: { + propDefinition: [ + zohoBooks, + "customFields", + ], + description: "A list of Additional field objects for the payments. **Example: {\"index\": 1, \"value\": \"value\"}**", + optional: true, + }, + lineItems: { + propDefinition: [ + zohoBooks, + "lineItems", + ], + description: "A list of line items objects of an estimate. **Example: {\"item_id\": \"1352827000000156060\", \"name\": \"name\", \"description\": \"description\", \"quantity\": \"1\" }** [See the documentation](https://www.zoho.com/books/api/v3/sales-order/#create-a-sales-order) for further details.", + }, + notes: { + propDefinition: [ + zohoBooks, + "notes", + ], + description: "The notes added below expressing gratitude or for conveying some information.", + optional: true, + }, + terms: { + propDefinition: [ + zohoBooks, + "terms", + ], + optional: true, + }, + shippingCharge: { + propDefinition: [ + zohoBooks, + "shippingCharge", + ], + optional: true, + }, + adjustment: { + propDefinition: [ + zohoBooks, + "adjustment", + ], + optional: true, + }, + adjustmentDescription: { + propDefinition: [ + zohoBooks, + "adjustmentDescription", + ], + optional: true, + }, + taxId: { + propDefinition: [ + zohoBooks, + "taxId", + ], + description: "ID of the tax to be associated to the estimate.", + optional: true, + }, + taxExemptionId: { + propDefinition: [ + zohoBooks, + "taxExemptionId", + ], + optional: true, + }, + taxAuthorityId: { + propDefinition: [ + zohoBooks, + "taxAuthorityId", + ], + optional: true, + }, + avataxUseCode: { + propDefinition: [ + zohoBooks, + "avataxUseCode", + ], + optional: true, + }, + avataxExemptNo: { + propDefinition: [ + zohoBooks, + "taxExemptionId", + ], + optional: true, + }, + vatTreatment: { + propDefinition: [ + zohoBooks, + "vatTreatment", + ], + optional: true, + }, + taxTreatment: { + propDefinition: [ + zohoBooks, + "taxTreatment", + ], + description: "VAT treatment for the estimate.", + optional: true, + }, + isReverseChargeApplied: { + type: "boolean", + label: "Is Reverse Charge Applied.", + description: "Used to specify whether the transaction is applicable for Domestic Reverse Charge (DRC) or not.", + optional: true, + }, + itemId: { + propDefinition: [ + zohoBooks, + "itemId", + ], + optional: true, + }, + lineItemId: { + type: "string", + label: "Line Item Id", + description: "ID of the line item. Mandatory, if the existing line item has to be updated. If empty, a new line item will be created.", + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "Name of the line item.", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "Description of the line item.", + optional: true, + }, + rate: { + type: "string", + label: "Rate", + description: "Rate of the line item.", + optional: true, + }, + unit: { + type: "string", + label: "Unit", + description: "Unit of the line item. E.g. kgs, Nos", + optional: true, + }, + quantity: { + type: "integer", + label: "Quantity", + description: "The quantity of line item.", + optional: true, + }, + projectId: { + type: "string", + label: "Project Id", + description: "Id of the project", + optional: true, + }, + acceptRetainer: { + type: "boolean", + label: "Accept Retainer", + description: "The \"Accept Retainer\" node should be passed for the retainer invoice to be created automatically, provided that the customer has accepted the quote.", + optional: true, + }, + retainerPercentage: { + type: "integer", + label: "Retainer Percentage", + description: "Pass the \"Retainer Percentage\" node to create the retainer invoice automatically.", + min: 1, + max: 100, + optional: true, + }, + send: { + propDefinition: [ + zohoBooks, + "send", + ], + optional: true, + }, + ignoreAutoNumberGeneration: { + propDefinition: [ + zohoBooks, + "ignoreAutoNumberGeneration", + ], + optional: true, + description: "Ignore auto estimate number generation for this estimate. This mandates the estimate number.", + }, + }, + async run({ $ }) { + const response = await this.zohoBooks.createEstimate({ + $, + params: { + send: this.send, + ignore_auto_number_generation: this.ignoreAutoNumberGeneration, + }, + data: { + customer_id: this.customerId, + currency_id: this.currencyId, + contact_persons: parseObject(this.contactPersons), + template_id: this.templateId, + place_of_supply: this.placeOfSupply, + gst_treatment: this.gstTreatment, + gst_no: this.gstNo, + estimate_number: this.estimateNumber, + reference_number: this.referenceNumber, + date: this.date, + expiry_date: this.expiryDate, + exchange_rate: this.exchangeRate && parseFloat(this.exchangeRate), + discount: this.discount && parseFloat(this.discount), + is_discount_before_tax: this.isDiscountBeforeTax, + discount_type: this.discountType, + is_inclusive_tax: this.isInclusiveTax, + custom_body: this.customBody, + custom_subject: this.customSubject, + salesperson_name: this.salespersonName, + custom_fields: parseObject(this.customFields), + line_items: parseObject(this.lineItems), + notes: this.notes, + terms: this.terms, + shipping_charge: this.shippingCharge, + adjustment: this.adjustment && parseFloat(this.adjustment), + adjustment_description: this.adjustmentDescription, + tax_id: this.taxId, + tax_exemption_id: this.taxExemptionId, + tax_authority_id: this.taxAuthorityId, + avatax_use_code: this.avataxUseCode, + avatax_exempt_no: this.avataxExemptNo, + vat_treatment: this.vatTreatment, + tax_treatment: this.taxTreatment, + is_reverse_charge_applied: this.isReverseChargeApplied, + item_id: this.itemId, + line_item_id: this.lineItemId, + name: this.name, + description: this.description, + rate: this.rate && parseFloat(this.rate), + unit: this.unit, + quantity: this.quantity, + project_id: this.projectId, + accept_retainer: this.acceptRetainer, + retainer_percentage: this.retainerPercentage, + }, + }); + + $.export("$summary", `Item successfully created with Id: ${response.estimate.estimate_id}`); + return response; + }, +}; diff --git a/components/zoho_books/actions/create-invoice/create-invoice.mjs b/components/zoho_books/actions/create-invoice/create-invoice.mjs index 1ffdc682743b7..bcfe3c3f3f043 100644 --- a/components/zoho_books/actions/create-invoice/create-invoice.mjs +++ b/components/zoho_books/actions/create-invoice/create-invoice.mjs @@ -1,317 +1,334 @@ // legacy_hash_id: a_EVix1V -import { axios } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import zohoBooks from "../../zoho_books.app.mjs"; export default { key: "zoho_books-create-invoice", name: "Create Invoice", - description: "Creates an invoice for your customer.", - version: "0.2.1", + description: "Creates an invoice for your customer. [See the documentation](https://www.zoho.com/books/api/v3/invoices/#create-an-invoice)", + version: "0.3.0", type: "action", props: { - zoho_books: { - type: "app", - app: "zoho_books", - }, - organization_id: { - type: "string", - description: "In Zoho Books, your business is termed as an organization. If you have multiple businesses, you simply set each of those up as an individual organization. Each organization is an independent Zoho Books Organization with it's own organization ID, base currency, time zone, language, contacts, reports, etc.\n\nThe parameter `organization_id` should be sent in with every API request to identify the organization.\n\nThe `organization_id` can be obtained from the GET `/organizations` API's JSON response. Alternatively, it can be obtained from the **Manage Organizations** page in the admin console.", - }, - customer_id: { - type: "string", + zohoBooks, + customerId: { + propDefinition: [ + zohoBooks, + "customerId", + ], description: "ID of the customer the invoice has to be created.", }, - line_items: { - type: "any", - description: "Line items of an estimate.", - optional: true, - }, - contact_persons: { - type: "any", - description: "Array of contact person(s) for whom invoice has to be sent.", - optional: true, - }, - invoice_number: { - type: "string", - description: "Search invoices by invoice number.Variants: `invoice_number_startswith` and `invoice_number_contains`. Max-length [100]", - optional: true, - }, - place_of_supply: { + invoiceNumber: { type: "string", - description: "Place where the goods/services are supplied to. (If not given, `place of contact` given for the contact will be taken)\nSupported codes for UAE emirates are :\nAbu Dhabi - AB`,\nAjman - `AJ`,\nDubai - `DU`,\nFujairah - `FU`,\nRas al-Khaimah - `RA`,\nSharjah - `SH`,\nUmm al-Quwain - `UM`\nSupported codes for the GCC countries are :\nUnited Arab Emirates - `AE`,\nSaudi Arabia - `SA`,\nBahrain - `BH`,\nKuwait - `KW`,\nOman - `OM`,\nQatar - `QA`.", + label: "Invoice Number", + description: "Search invoices by invoice number. Max-length [100]", optional: true, }, - vat_treatment: { - type: "string", - description: "Enter vat treatment.", - optional: true, - }, - tax_treatment: { - type: "string", - description: "VAT treatment for the invoice .Choose whether the contact falls under: `vat_registered`, `vat_not_registered`, `gcc_vat_not_registered`, `gcc_vat_registered`, `non_gcc` , `dz_vat_registered` and `dz_vat_not_registered` supported only for UAE.", + placeOfSupply: { + propDefinition: [ + zohoBooks, + "placeOfSupply", + ], optional: true, }, - gst_treatment: { - type: "string", - description: "Choose whether the contact is GST registered/unregistered/consumer/overseas. Allowed values are `business_gst`, `business_none`, `overseas`, `consumer`.", - optional: true, - options: [ - "business_gst", - "business_none", - "overseas", - "consumer", + vatTreatment: { + propDefinition: [ + zohoBooks, + "vatTreatment", ], - }, - gst_no: { - type: "string", - description: "15 digit GST identification number of the customer.", optional: true, }, - reference_number: { + referenceNumber: { type: "string", + label: "Reference Number", description: "The reference number of the invoice.", optional: true, }, - template_id: { - type: "string", - description: "ID of the pdf template associated with the invoice.", + templateId: { + propDefinition: [ + zohoBooks, + "templateId", + ], optional: true, }, date: { type: "string", - description: "Search invoices by invoice date. Default date format is yyyy-mm-dd. `Variants: due_date_start, due_date_end, due_date_before and due_date_after.`", + label: "Date", + description: "Search invoices by invoice date. Default date format is yyyy-mm-dd.", optional: true, }, - payment_terms: { - type: "string", - description: "Payment terms in days e.g. 15, 30, 60. Invoice due date will be calculated based on this. Max-length [100]", + paymentTerms: { + propDefinition: [ + zohoBooks, + "paymentTerms", + ], optional: true, }, - payment_terms_label: { - type: "boolean", - description: "Used to override the default payment terms label. Default value for 15 days is \"Net 15 Days\". Max-length [100]", + paymentTermsLabel: { + propDefinition: [ + zohoBooks, + "paymentTermsLabel", + ], optional: true, }, - due_date: { + dueDate: { type: "string", + label: "Due Date", description: "Search invoices by due date. Default date format is yyyy-mm-dd. `Variants: due_date_start, due_date_end, due_date_before and due_date_after`", optional: true, - options: [ - "entity_level", - "item_level", - ], }, discount: { - type: "boolean", - description: "Discount applied to the invoice. It can be either in % or in amount. e.g. 12.5% or 190. Max-length [100]", + propDefinition: [ + zohoBooks, + "discount", + ], optional: true, + description: "Discount applied to the invoice. It can be either in % or in amount. e.g. 12.5% or 190. Max-length [100]", }, - is_discount_before_tax: { - type: "boolean", - description: "Used to specify how the discount has to applied. Either before or after the calculation of tax.", + isDiscountBeforeTax: { + propDefinition: [ + zohoBooks, + "isDiscountBeforeTax", + ], optional: true, }, - discount_type: { - type: "string", - description: "How the discount is specified. Allowed values: `entity_level` and `item_level`.", + discountType: { + propDefinition: [ + zohoBooks, + "discountType", + ], optional: true, - options: [ - "entity_level", - "item_level", + }, + isInclusiveTax: { + propDefinition: [ + zohoBooks, + "isInclusiveTax", ], + optional: true, }, - is_inclusive_tax: { + exchangeRate: { type: "string", - description: "Used to specify whether the line item rates are inclusive or exclusivr of tax.", + label: "Exchange Rate", + description: "Exchange rate of the currency.", optional: true, }, - exchange_rate: { - type: "any", - description: "Exchange rate of the currency.", + recurringInvoiceId: { + propDefinition: [ + zohoBooks, + "recurringInvoiceId", + ({ customerId }) => ({ + customerId, + }), + ], optional: true, }, - recurring_invoice_id: { + invoicedEstimateId: { type: "string", - description: "ID of the recurring invoice from which the invoice is created.", + label: "Invoiced Estimate Id", + description: "ID of the invoice from which the invoice is created.", optional: true, }, - invoiced_estimate_id: { - type: "string", - description: "The notes added below expressing gratitude or for conveying some information.", + salespersonName: { + propDefinition: [ + zohoBooks, + "salespersonName", + ], optional: true, }, - salesperson_name: { - type: "string", - description: "ID of the invoice from which the invoice is created.", + customFields: { + propDefinition: [ + zohoBooks, + "customFields", + ], + description: "A list of custom fields objects for an invoice. **Example: {\"customfield_id\": 123123, \"value\": \"value\"}**", optional: true, }, - custom_fields: { - type: "any", - description: "Custom fields for an invoice.", + lineItems: { + propDefinition: [ + zohoBooks, + "lineItems", + ], optional: true, + description: "A list of line items objects of an estimate. **Example: {\"item_id\": \"1352827000000156060\", \"notes\": \"note\", \"name\": \"Item name\", \"quantity\": \"1\" }** [See the documentation](https://www.zoho.com/books/api/v3/invoices/#create-an-invoice) for further details.", }, - payment_options: { + paymentOptions: { type: "object", - description: "Payment options for the invoice, online payment gateways and bank accounts. Will be displayed in the pdf.", + label: "Payment Options", + description: "Payment options for the invoice, online payment gateways and bank accounts. Will be displayed in the pdf. **Example: {\"payment_gateways\": [\"configured\": true, \"additional_field1\": \"standard\", \"gateway_name\": \"paypal\"]}** [See the documentation](https://www.zoho.com/books/api/v3/invoices/#create-an-invoice) for further details.", optional: true, }, - allow_partial_payments: { + allowPartialPayments: { type: "boolean", + label: "Allow Partial Payments", description: "Boolean to check if partial payments are allowed for the contact", optional: true, }, - custom_body: { - type: "string", + customBody: { + propDefinition: [ + zohoBooks, + "customBody", + ], optional: true, }, - custom_subject: { - type: "string", + customSubject: { + propDefinition: [ + zohoBooks, + "customSubject", + ], optional: true, }, notes: { - type: "string", + propDefinition: [ + zohoBooks, + "notes", + ], description: "The notes added below expressing gratitude or for conveying some information.", optional: true, }, terms: { - type: "string", - description: "The terms added below expressing gratitude or for conveying some information.", + propDefinition: [ + zohoBooks, + "terms", + ], optional: true, }, - shipping_charge: { - type: "string", - description: "Shipping charges applied to the invoice. Max-length [100]", + shippingCharge: { + propDefinition: [ + zohoBooks, + "shippingCharge", + ], optional: true, }, adjustment: { - type: "string", + propDefinition: [ + zohoBooks, + "adjustment", + ], optional: true, }, - adjustment_description: { - type: "string", - description: "Adjustments made to the invoice.", + adjustmentDescription: { + propDefinition: [ + zohoBooks, + "adjustmentDescription", + ], optional: true, }, reason: { type: "string", + label: "Reason", optional: true, }, - tax_authority_id: { - type: "string", - description: "ID of the tax authority. Tax authority depends on the location of the customer. For example, if the customer is located in NY, then the tax authority is NY tax authority.", - optional: true, - }, - tax_exemption_id: { - type: "string", - description: "ID of the tax exemption.", - optional: true, - }, - avatax_use_code: { - type: "string", - description: "Used to group like customers for exemption purposes. It is a custom value that links customers to a tax rule. Select from Avalara [standard codes][1] or enter a custom code. Max-length [25]", + taxAuthorityId: { + propDefinition: [ + zohoBooks, + "taxAuthorityId", + ], optional: true, }, - avatax_exempt_no: { - type: "string", - description: "Exemption certificate number of the customer. Max-length [25]", + taxExemptionId: { + propDefinition: [ + zohoBooks, + "taxExemptionId", + ], optional: true, }, - tax_id: { - type: "string", - description: "ID of the tax.", + taxId: { + propDefinition: [ + zohoBooks, + "taxId", + ], optional: true, }, - expense_id: { - type: "string", + expenseId: { + propDefinition: [ + zohoBooks, + "expenseId", + ], optional: true, }, - salesorder_item_id: { + salesorderItemId: { type: "string", + label: "Salesorder Item Id", description: "ID of the sales order line item which is invoices.", optional: true, }, - avatax_tax_code: { + avataxTaxCode: { type: "string", + label: "Avatax Tax Code", description: "A tax code is a unique label used to group Items (products, services, or charges) together. Refer the [link][2] for more deails. Max-length [25]", optional: true, }, - time_entry_ids: { - type: "any", - description: "IDs of the time entries associated with the project.", + timeEntryIds: { + propDefinition: [ + zohoBooks, + "timeEntryIds", + ], optional: true, }, send: { - type: "boolean", - description: "Send the estimate to the contact person(s) associated with the estimate.Allowed Values: `true` and `false`", + propDefinition: [ + zohoBooks, + "send", + ], optional: true, }, - ignore_auto_number_generation: { - type: "boolean", - description: "Ignore auto estimate number generation for this estimate. This mandates the estimate number.", + ignoreAutoNumberGeneration: { + propDefinition: [ + zohoBooks, + "ignoreAutoNumberGeneration", + ], optional: true, + description: "Ignore auto estimate number generation for this estimate. This mandates the estimate number.", }, }, async run({ $ }) { - //See the API docs: https://www.zoho.com/books/api/v3/#Invoices_Create_an_invoice - - if (!this.organization_id || !this.customer_id || !this.line_items) { - throw new Error("Must provide organization_id, customer_id, and line_items parameters."); - } - - return await axios($, { - method: "post", - url: `https://books.${this.zoho_books.$auth.base_api_uri}/api/v3/invoices?organization_id=${this.organization_id}`, - headers: { - Authorization: `Zoho-oauthtoken ${this.zoho_books.$auth.oauth_access_token}`, + const response = await this.zohoBooks.createInvoice({ + $, + params: { + send: this.send, + ignore_auto_number_generation: this.ignoreAutoNumberGeneration, }, data: { - customer_id: this.customer_id, - contact_persons: this.contact_persons, - invoice_number: this.invoice_number, - place_of_supply: this.place_of_supply, - vat_treatment: this.vat_treatment, - tax_treatment: this.tax_treatment, - gst_treatment: this.gst_treatment, - gst_no: this.gst_no, - reference_number: this.reference_number, - template_id: this.template_id, + customer_id: this.customerId, + invoice_number: this.invoiceNumber, + place_of_supply: this.placeOfSupply, + vat_treatment: this.vatTreatment, + reference_number: this.referenceNumber, + template_id: this.templateId, date: this.date, - payment_terms: this.payment_terms, - payment_terms_label: this.payment_terms_label, - due_date: this.due_date, - discount: this.discount, - is_discount_before_tax: this.is_discount_before_tax, - discount_type: this.discount_type, - is_inclusive_tax: this.is_inclusive_tax, - exchange_rate: this.exchange_rate, - recurring_invoice_id: this.recurring_invoice_id, - invoiced_estimate_id: this.invoiced_estimate_id, - salesperson_name: this.salesperson_name, - custom_fields: this.custom_fields, - line_items: this.line_items, - payment_options: this.payment_options, - allow_partial_payments: this.allow_partial_payments, - custom_body: this.custom_body, - custom_subject: this.custom_subject, + payment_terms: this.paymentTerms, + payment_terms_label: this.paymentTermsLabel, + due_date: this.dueDate, + discount: this.discount && parseFloat(this.discount), + is_discount_before_tax: this.isDiscountBeforeTax, + discount_type: this.discountType, + is_inclusive_tax: this.isInclusiveTax, + exchange_rate: this.exchangeRate && parseFloat(this.exchangeRate), + recurring_invoice_id: this.recurringInvoiceId, + invoiced_estimate_id: this.invoicedEstimateId, + salesperson_name: this.salespersonName, + custom_fields: parseObject(this.customFields), + line_items: parseObject(this.lineItems), + payment_options: parseObject(this.paymentOptions), + allow_partial_payments: this.allowPartialPayments, + custom_body: this.customBody, + custom_subject: this.customSubject, notes: this.notes, terms: this.terms, - shipping_charge: this.shipping_charge, - adjustment: this.adjustment, - adjustment_description: this.adjustment_description, + shipping_charge: this.shippingCharge && parseFloat(this.shippingCharge), + adjustment: this.adjustment && parseFloat(this.adjustment), + adjustment_description: this.adjustmentDescription, reason: this.reason, - tax_authority_id: this.tax_authority_id, - tax_exemption_id: this.tax_exemption_id, - avatax_use_code: this.avatax_use_code, - avatax_exempt_no: this.avatax_exempt_no, - tax_id: this.tax_id, - expense_id: this.expense_id, - salesorder_item_id: this.salesorder_item_id, - avatax_tax_code: this.avatax_tax_code, - time_entry_ids: this.time_entry_ids, - }, - params: { - send: this.send, - ignore_auto_number_generation: this.ignore_auto_number_generation, + tax_authority_id: this.taxAuthorityId, + tax_exemption_id: this.taxExemptionId, + tax_id: this.taxId, + expense_id: this.expenseId, + salesorder_item_id: this.salesorderItemId, + avatax_tax_code: this.avataxTaxCode, + time_entry_ids: parseObject(this.timeEntryIds), }, }); + + $.export("$summary", `Invoice successfully created with Id: ${response.invoice.invoice_id}`); + return response; }, }; diff --git a/components/zoho_books/actions/create-item/create-item.mjs b/components/zoho_books/actions/create-item/create-item.mjs index aaf62faf17741..85f792da09a8e 100644 --- a/components/zoho_books/actions/create-item/create-item.mjs +++ b/components/zoho_books/actions/create-item/create-item.mjs @@ -1,183 +1,178 @@ // legacy_hash_id: a_Xzi1qo -import { axios } from "@pipedream/platform"; +import { + ITEM_TYPE_OPTIONS, PRODUCT_TYPE_OPTIONS, +} from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import zohoBooks from "../../zoho_books.app.mjs"; export default { key: "zoho_books-create-item", name: "Create Item", - description: "Creates a new item.", - version: "0.2.1", + description: "Creates a new item. [See the documentation](https://www.zoho.com/books/api/v3/items/#create-an-item)", + version: "0.3.0", type: "action", props: { - zoho_books: { - type: "app", - app: "zoho_books", - }, - organization_id: { - type: "string", - description: "In Zoho Books, your business is termed as an organization. If you have multiple businesses, you simply set each of those up as an individual organization. Each organization is an independent Zoho Books Organization with it's own organization ID, base currency, time zone, language, contacts, reports, etc.\n\nThe parameter `organization_id` should be sent in with every API request to identify the organization.\n\nThe `organization_id` can be obtained from the GET `/organizations` API's JSON response. Alternatively, it can be obtained from the **Manage Organizations** page in the admin console.", - }, + zohoBooks, name: { type: "string", + label: "Name", description: "Name of the item. Max-length [100]", }, rate: { type: "string", + label: "Rate", description: "Price of the item.", }, description: { type: "string", + label: "Description", description: "Description for the item. Max-length [2000]", optional: true, }, - tax_id: { - type: "string", + taxId: { + propDefinition: [ + zohoBooks, + "taxId", + ], description: "ID of the tax to be associated to the item.", optional: true, }, - tax_percentage: { + taxPercentage: { type: "string", + label: "Tax Percentage", description: "Percent of the tax.", optional: true, }, sku: { type: "string", + label: "SKU", description: "SKU value of item,should be unique throughout the product", optional: true, }, - product_type: { + productType: { type: "string", - description: "Specify the type of an item. Allowed values: `goods` or `service` or `digital_service`.", + label: "Product Type", + description: "Specify the type of an item.", optional: true, - options: [ - "goods", - "service", - "digital_service", - ], + options: PRODUCT_TYPE_OPTIONS, }, - hsn_or_sac: { + hsnOrSac: { type: "string", + label: "HSN Or SAC", description: "HSN Code.", optional: true, }, - is_taxable: { + isTaxable: { type: "boolean", + label: "Is Taxable", description: "Boolean to track the taxability of the item.", optional: true, }, - tax_exemption_id: { + taxExemptionId: { type: "string", + label: "Tax Exemption Id", description: "ID of the tax exemption. Mandatory, if is_taxable is false.", optional: true, }, - account_id: { - type: "string", + accountId: { + propDefinition: [ + zohoBooks, + "accountId", + ], description: "ID of the account to which the item has to be associated with.", optional: true, }, - avatax_tax_code: { + itemType: { type: "string", - description: "A tax code is a unique label used to group Items (products, services, or charges) together. Max-length [25]", + label: "Item Type", + description: "Type of the item. Default value will be sales.", optional: true, + options: ITEM_TYPE_OPTIONS, }, - avatax_use_code: { - type: "string", - description: "Used to group like customers for exemption purposes. It is a custom value that links customers to a tax rule. Select from Avalara [standard codes][1] or enter a custom code. Max-length [25]", - optional: true, - }, - item_type: { - type: "string", - description: "Type of the item. Allowed values: `sales`,`purchases`,`sales_and_purchases` and `inventory`. Default value will be sales.", - optional: true, - options: [ - "sales", - "purchases", - "sales_and_purchases", - "inventory", - ], - }, - purchase_description: { + purchaseDescription: { type: "string", + label: "Purchase Description", description: "Purchase description for the item.", optional: true, }, - purchase_rate: { + purchaseRate: { type: "string", + label: "Purchase Rate", description: "Purchase price of the item.", optional: true, }, - purchase_account_id: { + purchaseAccountId: { type: "string", + label: "Purchase Account Id", description: "ID of the COGS account to which the item has to be associated with. Mandatory, if item_type is purchase / sales and purchase / inventory.", optional: true, }, - inventory_account_id: { + inventoryAccountId: { type: "string", + label: "Inventory Account Id", description: "ID of the stock account to which the item has to be associated with. Mandatory, if item_type is inventory.", optional: true, }, - vendor_id: { + vendorId: { type: "string", + label: "Vendor Id", description: "Preferred vendor ID.", optional: true, }, - reorder_level: { + reorderLevel: { type: "string", + label: "Reorder Level", description: "Reorder level of the item.", optional: true, }, - initial_stock: { + initialStock: { type: "string", + label: "Initial Stock", description: "Opening stock of the item.", optional: true, }, - initial_stock_rate: { + initialStockRate: { type: "string", + label: "Initial Stock Rate", description: "Unit price of the opening stock.", optional: true, }, - item_tax_preferences: { - type: "any", + itemTaxPreferences: { + type: "string[]", + label: "Item Tax Preferences", + description: "A list of item tax objects. **Format: {\"tax_id\":\"12312312031200\",\"tax_specification\":\"intra\"}**", optional: true, }, }, async run({ $ }) { - //See the API docs: https://www.zoho.com/books/api/v3/#Items_Create_an_Item - - if (!this.organization_id || !this.name || !this.rate) { - throw new Error("Must provide organization_id, name, and rate parameters."); - } - - return await axios($, { - method: "post", - url: `https://books.${this.zoho_books.$auth.base_api_uri}/api/v3/items?organization_id=${this.organization_id}`, - headers: { - Authorization: `Zoho-oauthtoken ${this.zoho_books.$auth.oauth_access_token}`, - }, + const response = await this.zohoBooks.createItem({ + $, data: { name: this.name, - rate: this.rate, + rate: this.rate && parseFloat(this.rate), description: this.description, - tax_id: this.tax_id, - tax_percentage: this.tax_percentage, + tax_id: this.taxId, + tax_percentage: this.taxPercentage, sku: this.sku, - product_type: this.product_type, - hsn_or_sac: this.hsn_or_sac, - is_taxable: this.is_taxable, - tax_exemption_id: this.tax_exemption_id, - account_id: this.account_id, - avatax_tax_code: this.avatax_tax_code, - avatax_use_code: this.avatax_use_code, - item_type: this.item_type, - purchase_description: this.purchase_description, - purchase_rate: this.purchase_rate, - purchase_account_id: this.purchase_account_id, - inventory_account_id: this.inventory_account_id, - vendor_id: this.vendor_id, - reorder_level: this.reorder_level, - initial_stock: this.initial_stock, - initial_stock_rate: this.initial_stock_rate, - item_tax_preferences: this.item_tax_preferences, + product_type: this.productType, + hsn_or_sac: this.hsnOrSac, + is_taxable: this.isTaxable, + tax_exemption_id: this.taxExemptionId, + account_id: this.accountId, + item_type: this.itemType, + purchase_description: this.purchaseDescription, + purchase_rate: this.purchaseRate, + purchase_account_id: this.purchaseAccountId, + inventory_account_id: this.inventoryAccountId, + vendor_id: this.vendorId, + reorder_level: this.reorderLevel, + initial_stock: this.initialStock, + initial_stock_rate: this.initialStockRate, + item_tax_preferences: parseObject(this.itemTaxPreferences), }, }); + + $.export("$summary", `Item successfully created with Id: ${response.item.item_id}`); + return response; }, }; diff --git a/components/zoho_books/actions/create-salesorder/create-salesorder.mjs b/components/zoho_books/actions/create-salesorder/create-salesorder.mjs index 5536a9d17774d..47105c681f75b 100644 --- a/components/zoho_books/actions/create-salesorder/create-salesorder.mjs +++ b/components/zoho_books/actions/create-salesorder/create-salesorder.mjs @@ -1,333 +1,438 @@ // legacy_hash_id: a_WYi46K -import { axios } from "@pipedream/platform"; +import { + clearObj, parseObject, +} from "../../common/utils.mjs"; +import zohoBooks from "../../zoho_books.app.mjs"; export default { key: "zoho_books-create-salesorder", name: "Create Sales Order", - description: "Creates a sales order.", - version: "0.2.1", + description: "Creates a sales order. [See the documentation](https://www.zoho.com/books/api/v3/sales-order/#create-a-sales-order)", + version: "0.3.0", type: "action", props: { - zoho_books: { - type: "app", - app: "zoho_books", - }, - organization_id: { - type: "string", - description: "In Zoho Books, your business is termed as an organization. If you have multiple businesses, you simply set each of those up as an individual organization. Each organization is an independent Zoho Books Organization with it's own organization ID, base currency, time zone, language, contacts, reports, etc.\n\nThe parameter `organization_id` should be sent in with every API request to identify the organization.\n\nThe `organization_id` can be obtained from the GET `/organizations` API's JSON response. Alternatively, it can be obtained from the **Manage Organizations** page in the admin console.", - }, - customer_id: { - type: "string", + zohoBooks, + customerId: { + propDefinition: [ + zohoBooks, + "customerId", + ], description: "ID of the customer to whom the sales order has to be created.", }, - line_items: { - type: "any", - description: "Line items of a sales order.", - optional: true, - }, - contact_persons: { - type: "any", - description: "Array of contact person(s) for whom sales order has to be sent.", + contactPersons: { + propDefinition: [ + zohoBooks, + "contactPersons", + ({ customerId }) => ({ + customerId, + }), + ], + description: "A list of contact person(s) for whom sales order has to be sent.", optional: true, }, date: { - type: "string", - description: "The date, the sales order is created.", + propDefinition: [ + zohoBooks, + "date", + ], optional: true, }, - shipment_date: { - type: "string", - description: "Shipping date of sales order.", + shipmentDate: { + propDefinition: [ + zohoBooks, + "shipmentDate", + ], optional: true, }, - custom_fields: { - type: "any", - description: "Custom fields for a sales order.", + customFields: { + propDefinition: [ + zohoBooks, + "customFields", + ], optional: true, }, - place_of_supply: { - type: "string", - description: "Place where the goods/services are supplied to. (If not given, `place of contact` given for the contact will be taken)\nSupported codes for UAE emirates are :\nAbu Dhabi - AB`,\nAjman - `AJ`,\nDubai - `DU`,\nFujairah - `FU`,\nRas al-Khaimah - `RA`,\nSharjah - `SH`,\nUmm al-Quwain - `UM`\nSupported codes for the GCC countries are :\nUnited Arab Emirates - `AE`,\nSaudi Arabia - `SA`,\nBahrain - `BH`,\nKuwait - `KW`,\nOman - `OM`,\nQatar - `QA`.", + placeOfSupply: { + propDefinition: [ + zohoBooks, + "placeOfSupply", + ], optional: true, }, - salesperson_id: { - type: "string", - description: "ID of the salesperson.", + salespersonId: { + propDefinition: [ + zohoBooks, + "salespersonId", + ], optional: true, }, - merchant_id: { - type: "string", - description: "ID of the merchant.", + merchantId: { + propDefinition: [ + zohoBooks, + "merchantId", + ], optional: true, }, - gst_treatment: { - type: "string", - description: "Choose whether the contact is GST registered/unregistered/consumer/overseas. Allowed values are `business_gst`, `business_none`, `overseas`, `consumer`.", + gstTreatment: { + propDefinition: [ + zohoBooks, + "gstTreatment", + ], optional: true, }, - gst_no: { - type: "string", - description: "15 digit GST identification number of the customer.", + gstNo: { + propDefinition: [ + zohoBooks, + "gstNo", + ], optional: true, }, - is_inclusive_tax: { - type: "boolean", - description: "Used to specify whether the line item rates are inclusive or exclusive of tax.", + isInclusiveTax: { + propDefinition: [ + zohoBooks, + "isInclusiveTax", + ], optional: true, }, + lineItems: { + propDefinition: [ + zohoBooks, + "lineItems", + ], + }, notes: { - type: "string", - description: "Notes for this Sales Order.", + propDefinition: [ + zohoBooks, + "notes", + ], optional: true, }, terms: { - type: "string", + propDefinition: [ + zohoBooks, + "terms", + ], optional: true, }, - billing_address_id: { - type: "string", - description: "ID of the Billing Address", + billingAddressId: { + propDefinition: [ + zohoBooks, + "billingAddressId", + ], optional: true, }, - shipping_address_id: { - type: "string", - description: "ID of the Shipping Address.", + shippingAddressId: { + propDefinition: [ + zohoBooks, + "shippingAddressId", + ], optional: true, }, - crm_owner_id: { - type: "string", + crmOwnerId: { + propDefinition: [ + zohoBooks, + "crmOwnerId", + ], optional: true, }, - crm_custom_reference_id: { - type: "string", + crmCustomReferenceId: { + propDefinition: [ + zohoBooks, + "crmCustomReferenceId", + ], optional: true, }, - vat_treatment: { - type: "string", - description: "Enter vat treatment.", + vatTreatment: { + propDefinition: [ + zohoBooks, + "vatTreatment", + ], optional: true, }, - tax_treatment: { - type: "string", - description: "VAT treatment for the invoice .Choose whether the contact falls under: `vat_registered`, `vat_not_registered`, `gcc_vat_not_registered`, `gcc_vat_registered`, `non_gcc` , `dz_vat_registered` and `dz_vat_not_registered` supported only for UAE.", + taxTreatment: { + propDefinition: [ + zohoBooks, + "taxTreatment", + ], optional: true, }, - salesorder_number: { - type: "string", - description: "Mandatory if auto number generation is disabled.", + salesorderNumber: { + propDefinition: [ + zohoBooks, + "salesorderNumber", + ], optional: true, }, - reference_number: { - type: "string", - description: "**For Customer Only** : If a contact is assigned to any particular user, that user can manage transactions for the contact", + referenceNumber: { + propDefinition: [ + zohoBooks, + "referenceNumber", + ], optional: true, }, - is_update_customer: { - type: "boolean", - description: "Boolean to update billing address of customer.", + isUpdateCustomer: { + propDefinition: [ + zohoBooks, + "isUpdateCustomer", + ], optional: true, }, discount: { - type: "string", - description: "Discount applied to the sales order. It can be either in % or in amount. e.g. 12.5% or 190.", + propDefinition: [ + zohoBooks, + "discount", + ], optional: true, }, - exchange_rate: { - type: "string", - description: "Exchange rate of the currency.", + exchangeRate: { + propDefinition: [ + zohoBooks, + "exchangeRate", + ], optional: true, }, - salesperson_name: { - type: "string", - description: "Name of the sales person.", + salespersonName: { + propDefinition: [ + zohoBooks, + "salespersonName", + ], optional: true, }, - notes_default: { - type: "string", - description: "Default Notes for the Sales Order", + notesDefault: { + propDefinition: [ + zohoBooks, + "notesDefault", + ], optional: true, }, - terms_default: { - type: "string", - description: "Default Terms of the Sales Order.", + termsDefault: { + propDefinition: [ + zohoBooks, + "termsDefault", + ], optional: true, }, - tax_id: { - type: "string", - description: "Tax ID for the Sales Order.", + taxId: { + propDefinition: [ + zohoBooks, + "taxId", + ], optional: true, }, - tax_authority_id: { - type: "string", - description: "ID of the tax authority. Tax authority depends on the location of the customer.", + taxAuthorityId: { + propDefinition: [ + zohoBooks, + "taxAuthorityId", + ], optional: true, }, - tax_exemption_id: { - type: "string", - description: "ID of the tax exemption applied.", + taxExemptionId: { + propDefinition: [ + zohoBooks, + "taxExemptionId", + ], optional: true, }, - tax_authority_name: { - type: "string", - description: "Tax Authority's name.", + taxAuthorityName: { + propDefinition: [ + zohoBooks, + "taxAuthorityName", + ], optional: true, }, - tax_exemption_code: { - type: "string", - description: "Code of Tax Exemption that is applied.", + taxExemptionCode: { + propDefinition: [ + zohoBooks, + "taxExemptionCode", + ], optional: true, }, - avatax_exempt_no: { - type: "string", - description: "Exemption certificate number of the customer.", + avataxExemptNo: { + propDefinition: [ + zohoBooks, + "taxExemptionId", + ], optional: true, }, - avatax_use_code: { - type: "string", - description: "Used to group like customers for exemption purposes. It is a custom value that links customers to a tax rule.", + avataxUseCode: { + propDefinition: [ + zohoBooks, + "avataxUseCode", + ], optional: true, }, - shipping_charge: { - type: "string", + shippingCharge: { + propDefinition: [ + zohoBooks, + "shippingCharge", + ], optional: true, }, adjustment: { - type: "string", + propDefinition: [ + zohoBooks, + "adjustment", + ], optional: true, }, - delivery_method: { - type: "string", + adjustmentDescription: { + propDefinition: [ + zohoBooks, + "adjustmentDescription", + ], optional: true, }, - estimate_id: { - type: "string", - description: "ID of the estimate associated with the Sales Order.", + deliveryMethod: { + propDefinition: [ + zohoBooks, + "deliveryMethod", + ], optional: true, }, - is_discount_before_tax: { - type: "boolean", - description: "Used to specify how the discount has to applied. Either before or after the calculation of tax.", + estimateId: { + type: "string", + label: "Estimate Id", + description: "ID of the estimate associated with the Sales Order.", optional: true, }, - discount_type: { - type: "string", - description: "How the discount is specified. Allowed values are entity_level or item_level. Allowed Values: `entity_level` and `item_level`.", + isDiscountBeforeTax: { + propDefinition: [ + zohoBooks, + "isDiscountBeforeTax", + ], optional: true, }, - adjustment_description: { - type: "string", + discountType: { + propDefinition: [ + zohoBooks, + "discountType", + ], optional: true, }, - pricebook_id: { - type: "string", + pricebookId: { + propDefinition: [ + zohoBooks, + "pricebookId", + ], optional: true, }, - template_id: { - type: "string", - description: "ID of the pdf template.", + templateId: { + propDefinition: [ + zohoBooks, + "pdfTemplateId", + ], optional: true, }, documents: { - type: "any", + propDefinition: [ + zohoBooks, + "documents", + ], optional: true, }, - zcrm_potential_id: { - type: "string", + zcrmPotentialId: { + propDefinition: [ + zohoBooks, + "zcrmPotentialId", + ], optional: true, }, - zcrm_potential_name: { - type: "string", + zcrmPotentialName: { + propDefinition: [ + zohoBooks, + "zcrmPotentialName", + ], optional: true, }, - ignore_auto_number_generation: { - type: "string", - description: "Ignore auto sales order number generation for this sales order. This mandates the sales order number.", + ignoreAutoNumberGeneration: { + propDefinition: [ + zohoBooks, + "ignoreAutoNumberGeneration", + ], optional: true, }, - can_send_in_mail: { - type: "string", - description: "Can the file be sent in mail.", + canSendInMail: { + propDefinition: [ + zohoBooks, + "canSendInMail", + ], optional: true, }, totalFiles: { - type: "string", - description: "Total number of files.", + propDefinition: [ + zohoBooks, + "totalFiles", + ], optional: true, }, doc: { - type: "string", - description: "Document that is to be attached", + propDefinition: [ + zohoBooks, + "doc", + ], optional: true, }, }, async run({ $ }) { - //See the API docs: https://www.zoho.com/books/api/v3/#Sales-Order_Create_a_sales_order - - if (!this.organization_id || !this.customer_id || !this.line_items) { - throw new Error("Must provide organization_id, customer_id, and line_items parameters."); - } - - return await axios($, { - method: "post", - url: `https://books.${this.zoho_books.$auth.base_api_uri}/api/v3/salesorders?organization_id=${this.organization_id}`, - headers: { - Authorization: `Zoho-oauthtoken ${this.zoho_books.$auth.oauth_access_token}`, - }, - data: { - customer_id: this.customer_id, - contact_persons: this.contact_persons, + const response = await this.zohoBooks.createSalesorder({ + $, + params: clearObj({ + ignore_auto_number_generation: this.ignoreAutoNumberGeneration, + can_send_in_mail: this.canSendInMail, + totalFiles: this.totalFiles, + doc: this.doc, + }), + data: clearObj({ + customer_id: this.customerId, + contact_persons: this.contactPersons, date: this.date, - shipment_date: this.shipment_date, - custom_fields: this.custom_fields, - place_of_supply: this.place_of_supply, - salesperson_id: this.salesperson_id, - merchant_id: this.merchant_id, - gst_treatment: this.gst_treatment, - gst_no: this.gst_no, - is_inclusive_tax: this.is_inclusive_tax, - line_items: this.line_items, + shipment_date: this.shipmentDate, + custom_fields: parseObject(this.customFields), + place_of_supply: this.placeOfSupply, + salesperson_id: this.salespersonId, + merchant_id: this.merchantId, + gst_treatment: this.gstTreatment, + gst_no: this.gstNo, + is_inclusive_tax: this.isInclusiveTax, + line_items: parseObject(this.lineItems), notes: this.notes, terms: this.terms, - billing_address_id: this.billing_address_id, - shipping_address_id: this.shipping_address_id, - crm_owner_id: this.crm_owner_id, - crm_custom_reference_id: this.crm_custom_reference_id, - vat_treatment: this.vat_treatment, - tax_treatment: this.tax_treatment, - salesorder_number: this.salesorder_number, - reference_number: this.reference_number, - is_update_customer: this.is_update_customer, + billing_address_id: this.billingAddressId, + shipping_address_id: this.shippingAddressId, + crm_owner_id: this.crmOwnerId, + crm_custom_reference_id: this.crmCustomReferenceId, + vat_treatment: this.vatTreatment, + tax_treatment: this.taxTreatment, + salesorder_number: this.salesorderNumber, + reference_number: this.referenceNumber, + is_update_customer: this.isUpdateCustomer, discount: this.discount, - exchange_rate: this.exchange_rate, - salesperson_name: this.salesperson_name, - notes_default: this.notes_default, - terms_default: this.terms_default, - tax_id: this.tax_id, - tax_authority_id: this.tax_authority_id, - tax_exemption_id: this.tax_exemption_id, - tax_authority_name: this.tax_authority_name, - tax_exemption_code: this.tax_exemption_code, - avatax_exempt_no: this.avatax_exempt_no, - avatax_use_code: this.avatax_use_code, - shipping_charge: this.shipping_charge, - adjustment: this.adjustment, - delivery_method: this.delivery_method, - estimate_id: this.estimate_id, - is_discount_before_tax: this.is_discount_before_tax, - discount_type: this.discount_type, - adjustment_description: this.adjustment_description, - pricebook_id: this.pricebook_id, - template_id: this.template_id, - documents: this.documents, - zcrm_potential_id: this.zcrm_potential_id, - zcrm_potential_name: this.zcrm_potential_name, - }, - params: { - ignore_auto_number_generation: this.ignore_auto_number_generation, - can_send_in_mail: this.can_send_in_mail, - totalFiles: this.totalFiles, - doc: this.doc, - }, + exchange_rate: this.exchangeRate && parseFloat(this.exchangeRate), + salesperson_name: this.salespersonName, + notes_default: this.notesDefault, + terms_default: this.termsDefault, + tax_id: this.taxId, + tax_authority_id: this.taxAuthorityId, + tax_exemption_id: this.taxExemptionId, + tax_authority_name: this.taxAuthorityName, + tax_exemption_code: this.taxExemptionCode, + avatax_exempt_no: this.avataxExemptNo, + avatax_use_code: this.avataxUseCode, + shipping_charge: this.shippingCharge && parseFloat(this.shippingCharge), + adjustment: this.adjustment && parseFloat(this.adjustment), + delivery_method: this.deliveryMethod, + estimate_id: this.estimateId, + is_discount_before_tax: this.isDiscountBeforeTax, + discount_type: this.discountType, + adjustment_description: this.adjustmentDescription, + pricebook_id: this.pricebookId, + template_id: this.templateId, + documents: parseObject(this.documents), + zcrm_potential_id: this.zcrmPotentialId, + zcrm_potential_name: this.zcrmPotentialName, + }), }); + + $.export("$summary", `Salesorder successfully created with Id: ${response.salesorder.salesorder_id}`); + return response; }, }; diff --git a/components/zoho_books/actions/delete-contact/delete-contact.mjs b/components/zoho_books/actions/delete-contact/delete-contact.mjs index b1cd312c7baaa..1813944967478 100644 --- a/components/zoho_books/actions/delete-contact/delete-contact.mjs +++ b/components/zoho_books/actions/delete-contact/delete-contact.mjs @@ -1,39 +1,29 @@ // legacy_hash_id: a_Lgiern -import { axios } from "@pipedream/platform"; +import zohoBooks from "../../zoho_books.app.mjs"; export default { key: "zoho_books-delete-contact", name: "Delete Contact", - description: "Deletes an existing contact.", - version: "0.2.1", + description: "Deletes an existing contact. [See the documentation](https://www.zoho.com/books/api/v3/contacts/#delete-a-contact)", + version: "0.3.0", type: "action", props: { - zoho_books: { - type: "app", - app: "zoho_books", - }, - organization_id: { - type: "string", - description: "In Zoho Books, your business is termed as an organization. If you have multiple businesses, you simply set each of those up as an individual organization. Each organization is an independent Zoho Books Organization with it's own organization ID, base currency, time zone, language, contacts, reports, etc.\n\nThe parameter `organization_id` should be sent in with every API request to identify the organization.\n\nThe `organization_id` can be obtained from the GET `/organizations` API's JSON response. Alternatively, it can be obtained from the **Manage Organizations** page in the admin console.", - }, - contact_id: { - type: "string", - description: "ID of the contact to delete.", + zohoBooks, + customerId: { + propDefinition: [ + zohoBooks, + "customerId", + ], + description: "The Id of the contact which will be deleted.", }, }, async run({ $ }) { - // See the API docs: https://www.zoho.com/books/api/v3/#Contacts_Delete_a_Contact - - if (!this.organization_id || !this.contact_id) { - throw new Error("Must provide organization_id, and contact_id parameters."); - } - - return await axios($, { - method: "delete", - url: `https://books.${this.zoho_books.$auth.base_api_uri}/api/v3/contacts/${this.contact_id}?organization_id=${this.organization_id}`, - headers: { - Authorization: `Zoho-oauthtoken ${this.zoho_books.$auth.oauth_access_token}`, - }, + const response = await this.zohoBooks.deleteContact({ + $, + customerId: this.customerId, }); + + $.export("$summary", `Contact successfully deleted with Id: ${this.customerId}`); + return response; }, }; diff --git a/components/zoho_books/actions/get-invoice/get-invoice.mjs b/components/zoho_books/actions/get-invoice/get-invoice.mjs index 103806175eaef..fd5862f4c8b3d 100644 --- a/components/zoho_books/actions/get-invoice/get-invoice.mjs +++ b/components/zoho_books/actions/get-invoice/get-invoice.mjs @@ -1,64 +1,41 @@ // legacy_hash_id: a_Mdie64 -import { axios } from "@pipedream/platform"; +import { PRINT_OPTIONS } from "../../common/constants.mjs"; +import zohoBooks from "../../zoho_books.app.mjs"; export default { key: "zoho_books-get-invoice", name: "Get Invoice", - description: "Gets the details of an invoice.", - version: "0.2.1", + description: "Gets the details of an invoice. [See the documentation](https://www.zoho.com/books/api/v3/invoices/#get-an-invoice)", + version: "0.3.0", type: "action", props: { - zoho_books: { - type: "app", - app: "zoho_books", - }, - organization_id: { - type: "string", - description: "In Zoho Books, your business is termed as an organization. If you have multiple businesses, you simply set each of those up as an individual organization. Each organization is an independent Zoho Books Organization with it's own organization ID, base currency, time zone, language, contacts, reports, etc.\n\nThe parameter `organization_id` should be sent in with every API request to identify the organization.\n\nThe `organization_id` can be obtained from the GET `/organizations` API's JSON response. Alternatively, it can be obtained from the **Manage Organizations** page in the admin console.", - }, - invoice_id: { - type: "string", + zohoBooks, + invoiceId: { + propDefinition: [ + zohoBooks, + "invoiceId", + ], description: "ID of the invoice to get details.", }, print: { type: "string", - description: "Print the exported pdf. Allowed Values: `true`, `false`, `on` and `off`", - optional: true, - options: [ - "true", - "false", - "on", - "off", - ], - }, - accept: { - type: "string", - description: "Get the details of a particular invoice in formats such as json/ pdf/ html. Default format is json. Allowed values: `json`, `pdf`, and `html`.", + label: "Print", + description: "Print the exported pdf.", + options: PRINT_OPTIONS, optional: true, - options: [ - "json", - "pdf", - "html", - ], }, }, async run({ $ }) { - //See the API docs: https://www.zoho.com/books/api/v3/#Invoices_Get_an_invoice - - if (!this.organization_id || !this.invoice_id) { - throw new Error("Must provide organization_id, and invoice_id parameters."); - } - - return await axios($, { - method: "get", - url: `https://books.${this.zoho_books.$auth.base_api_uri}/api/v3/invoices/${this.invoice_id}?organization_id=${this.organization_id}`, - headers: { - Authorization: `Zoho-oauthtoken ${this.zoho_books.$auth.oauth_access_token}`, - }, + const response = await this.zohoBooks.getInvoice({ + $, + invoiceId: this.invoiceId, params: { print: this.print, - accept: this.accept, + accept: "json", }, }); + + $.export("$summary", `Successfully fetched invoice with Id: ${this.invoiceId}`); + return response; }, }; diff --git a/components/zoho_books/actions/get-item/get-item.mjs b/components/zoho_books/actions/get-item/get-item.mjs index 5320d0a209d31..1e80975355f28 100644 --- a/components/zoho_books/actions/get-item/get-item.mjs +++ b/components/zoho_books/actions/get-item/get-item.mjs @@ -1,39 +1,28 @@ // legacy_hash_id: a_wdiVqz -import { axios } from "@pipedream/platform"; +import zohoBooks from "../../zoho_books.app.mjs"; export default { key: "zoho_books-get-item", name: "Get Item", - description: "Gets the details of an existing item.", - version: "0.2.1", + description: "Gets the details of an existing item. [See the documentation](https://www.zoho.com/books/api/v3/items/#get-an-item)", + version: "0.3.0", type: "action", props: { - zoho_books: { - type: "app", - app: "zoho_books", - }, - organization_id: { - type: "string", - description: "In Zoho Books, your business is termed as an organization. If you have multiple businesses, you simply set each of those up as an individual organization. Each organization is an independent Zoho Books Organization with it's own organization ID, base currency, time zone, language, contacts, reports, etc.\n\nThe parameter `organization_id` should be sent in with every API request to identify the organization.\n\nThe `organization_id` can be obtained from the GET `/organizations` API's JSON response. Alternatively, it can be obtained from the **Manage Organizations** page in the admin console.", - }, - item_id: { - type: "string", - description: "ID of the item to get details.", + zohoBooks, + itemId: { + propDefinition: [ + zohoBooks, + "itemId", + ], }, }, async run({ $ }) { - //See the API docs: https://www.zoho.com/books/api/v3/#Items_Get_an_item - - if (!this.organization_id || !this.item_id) { - throw new Error("Must provide organization_id, and item_id parameters."); - } - - return await axios($, { - method: "get", - url: `https://books.${this.zoho_books.$auth.base_api_uri}/api/v3/items/${this.item_id}?organization_id=${this.organization_id}`, - headers: { - Authorization: `Zoho-oauthtoken ${this.zoho_books.$auth.oauth_access_token}`, - }, + const response = await this.zohoBooks.getItem({ + $, + itemId: this.itemId, }); + + $.export("$summary", `Successfully fetched item with Id: ${this.itemId}`); + return response; }, }; diff --git a/components/zoho_books/actions/list-contacts/list-contacts.mjs b/components/zoho_books/actions/list-contacts/list-contacts.mjs index 8ff3a06bef155..ce1d8e17da69f 100644 --- a/components/zoho_books/actions/list-contacts/list-contacts.mjs +++ b/components/zoho_books/actions/list-contacts/list-contacts.mjs @@ -1,45 +1,27 @@ // legacy_hash_id: a_RAiV28 -import { axios } from "@pipedream/platform"; +import zohoBooks from "../../zoho_books.app.mjs"; export default { key: "zoho_books-list-contacts", name: "List Contacts", - description: "Lists all contacts given the organization_id.", - version: "0.4.1", + description: "Lists all contacts given the organization_id. [See the documentation](https://www.zoho.com/books/api/v3/contacts/#list-contacts)", + version: "0.5.0", type: "action", props: { - zoho_books: { - type: "app", - app: "zoho_books", - }, - organization_id: { - type: "string", - description: "In Zoho Books, your business is termed as an organization. If you have multiple businesses, you simply set each of those up as an individual organization. Each organization is an independent Zoho Books Organization with it's own organization ID, base currency, time zone, language, contacts, reports, etc.\n\nThe parameter `organization_id` should be sent in with every API request to identify the organization.\n\nThe `organization_id` can be obtained from the GET `/organizations` API's JSON response. Alternatively, it can be obtained from the **Manage Organizations** page in the admin console.", - }, - page: { - type: "string", - description: "By default first page will be listed. For navigating through pages, use the `page` parameter.", - optional: true, - }, - per_page: { - type: "string", - description: "The `per_page` parameter can be used to set the number of records that you want to receive in response.", - optional: true, - }, + zohoBooks, }, async run({ $ }) { - //See the API docs: https://www.zoho.com/books/api/v3/#Contacts_List_Contacts - - return await axios($, { - method: "get", - url: `https://books.${this.zoho_books.$auth.base_api_uri}/api/v3/contacts?organization_id=${this.organization_id}`, - headers: { - Authorization: `Zoho-oauthtoken ${this.zoho_books.$auth.oauth_access_token}`, - }, - params: { - page: this.page, - per_page: this.per_page, - }, + const response = this.zohoBooks.paginate({ + fn: this.zohoBooks.listContacts, + fieldName: "contacts", }); + + const responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + $.export("$summary", `Successfully fetched ${responseArray.length} item(s)`); + return responseArray; }, }; diff --git a/components/zoho_books/actions/list-expenses/list-expenses.mjs b/components/zoho_books/actions/list-expenses/list-expenses.mjs index 676f559832910..66fd2fac864b0 100644 --- a/components/zoho_books/actions/list-expenses/list-expenses.mjs +++ b/components/zoho_books/actions/list-expenses/list-expenses.mjs @@ -1,163 +1,142 @@ -// legacy_hash_id: a_gnir81 -import { axios } from "@pipedream/platform"; +// legacy_hash_id: a_gnir81// legacy_hash_id: a_RAiV28 +import { + FILTER_BY_OPTIONS, + SORT_COLUMN_OPTIONS, + STATUS_OPTIONS, +} from "../../common/constants.mjs"; +import zohoBooks from "../../zoho_books.app.mjs"; export default { key: "zoho_books-list-expenses", name: "List Expenses", - description: "List all the Expenses.", - version: "0.2.1", + description: "List all the Expenses. [See the documentation](https://www.zoho.com/books/api/v3/expenses/#list-expenses)", + version: "0.3.0", type: "action", props: { - zoho_books: { - type: "app", - app: "zoho_books", - }, - organization_id: { - type: "string", - description: "In Zoho Books, your business is termed as an organization. If you have multiple businesses, you simply set each of those up as an individual organization. Each organization is an independent Zoho Books Organization with it's own organization ID, base currency, time zone, language, contacts, reports, etc.\n\nThe parameter `organization_id` should be sent in with every API request to identify the organization.\n\nThe `organization_id` can be obtained from the GET `/organizations` API's JSON response. Alternatively, it can be obtained from the **Manage Organizations** page in the admin console.", - }, + zohoBooks, description: { type: "string", + label: "Description", description: "Search expenses by description.Variants `description_startswith` and `description_contains`. Max-length [100]", optional: true, }, - reference_number: { + referenceNumber: { type: "string", + label: "Reference Number", description: "Search expenses by reference number. Variants `reference_number_startswith` and `reference_number_contains`. Max-length [100]", optional: true, }, date: { type: "string", + label: "Date", description: "Search expenses by expense date. Variants `date_start`, `date_end`, `date_before` and `date_after`. Format [yyyy-mm-dd]", optional: true, }, status: { type: "string", - description: "Search expenses by expense status. Allowed Values `unbilled`, `invoiced`, `reimbursed`, `non-billable` and `billable`.", + label: "Status", + description: "Search expenses by expense status.", + options: STATUS_OPTIONS, optional: true, - options: [ - "unbilled", - "invoiced", - "reimbursed", - "non-billable", - "billable", - ], }, amount: { type: "string", - description: "Search expenses by amount. Variants: `amount_less_than`, `amount_less_equals`, `amount_greater_than` and `amount_greater_than`.", + label: "Amount", + description: "Search expenses by amount.", optional: true, }, - account_name: { + accountName: { type: "string", - description: "Search expenses by expense account name. Variants `account_name_startswith` and `account_name_contains`. Max-length [100", + label: "Account Name", + description: "Search expenses by expense account name. Max-length [100]", optional: true, }, - customer_name: { + customerName: { type: "string", - description: "Search expenses by customer name. Variants: `customer_name_startswith` and `customer_name_contains`. Max-length [100]", + label: "Customer Name", + description: "Search expenses by customer name. Max-length [100]", optional: true, }, - vendor_name: { + vendorName: { type: "string", - description: "Search expenses by vendor name. Variants: `vendor_name_startswith` and `vendor_name_contains`.", + label: "Vendor Name", + description: "Search expenses by vendor name.", optional: true, }, - customer_id: { + customerId: { type: "string", + label: "Customer Id", description: "ID of the expense account.", optional: true, }, - vendor_id: { + vendorId: { type: "string", + label: "Vendor Id", description: "ID of the vendor the expense is made.", optional: true, }, - recurring_expense_id: { + recurringExpenseId: { type: "string", + label: "Recurring Expense Id", description: "Search expenses by recurring expense id.", optional: true, }, - paid_through_account_id: { + paidThroughAccountId: { type: "string", + label: "Paid Through Account Id", description: "Search expenses by paid through account id.", optional: true, }, - search_text: { + searchText: { type: "string", + label: "Search Text", description: "Search expenses by account name or description or `customer name` or `vendor name`. Max-length [100]", optional: true, }, - sort_column: { - type: "string", - description: "Sort expenses.Allowed Values `date`, `account_name`, `total`, `bcy_total`, `reference_number`, `customer_name` and `created_time`.", - optional: true, - options: [ - "date", - "account_name", - "total", - "bcy_total", - "reference_number", - "customer_name", - "created_time", - ], - }, - filter_by: { - type: "string", - description: "Filter expenses by expense status. Allowed Values `Status.All`, `Status.Billable`, `Status.Nonbillable`, `Status.Reimbursed`, `Status.Invoiced` and `Status.Unbilled`.", - optional: true, - options: [ - "Status.All", - "Status.Billable", - "Status.Nonbillable", - "Status.Reimbursed", - "Status.Invoiced", - "Status.Unbilled", - ], - }, - page: { + sortColumn: { type: "string", - description: "By default first page will be listed. For navigating through `pages`, use the `page` parameter.", + label: "Sort Column", + description: "Sort expenses.", optional: true, + options: SORT_COLUMN_OPTIONS, }, - per_page: { + filterBy: { type: "string", - description: "The `per_page` parameter can be used to set the number of records that you want to receive in response.", + label: "Filter By", + description: "Filter expenses by expense status.", optional: true, + options: FILTER_BY_OPTIONS, }, }, async run({ $ }) { - //See the API docs: https://www.zoho.com/books/api/v3/#Expenses_List_Expenses - - if (!this.organization_id) { - throw new Error("Must provide organization_id parameters."); - } - - return await axios($, { - method: "get", - url: `https://books.${this.zoho_books.$auth.base_api_uri}/api/v3/expenses?organization_id=${this.organization_id}`, - headers: { - Authorization: `Zoho-oauthtoken ${this.zoho_books.$auth.oauth_access_token}`, - }, + const response = this.zohoBooks.paginate({ + fn: this.zohoBooks.listExpenses, + fieldName: "expenses", params: { description: this.description, - reference_number: this.reference_number, + reference_number: this.referenceNumber, date: this.date, status: this.status, amount: this.amount, - account_name: this.account_name, - customer_name: this.customer_name, - vendor_name: this.vendor_name, - customer_id: this.customer_id, - vendor_id: this.vendor_id, - recurring_expense_id: this.recurring_expense_id, - paid_through_account_id: this.paid_through_account_id, - search_text: this.search_text, - sort_column: this.sort_column, - filter_by: this.filter_by, - page: this.page, - per_page: this.per_page, + account_name: this.accountName, + customer_name: this.customerName, + vendor_name: this.vendorName, + customer_id: this.customerId, + vendor_id: this.vendorId, + recurring_expense_id: this.recurringExpenseId, + paid_through_account_id: this.paidThroughAccountId, + search_text: this.searchText, + sort_column: this.sortColumn, + filter_by: this.filterBy, }, }); + + const responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + $.export("$summary", `Successfully fetched ${responseArray.length} expenses(s)`); + return responseArray; }, }; diff --git a/components/zoho_books/actions/list-invoices/list-invoices.mjs b/components/zoho_books/actions/list-invoices/list-invoices.mjs index 48fd06679d9d1..ac176923441c9 100644 --- a/components/zoho_books/actions/list-invoices/list-invoices.mjs +++ b/components/zoho_books/actions/list-invoices/list-invoices.mjs @@ -1,193 +1,169 @@ -// legacy_hash_id: a_PNin2N -import { axios } from "@pipedream/platform"; +// legacy_hash_id: a_PNin2N// legacy_hash_id: a_RAiV28 +import { + INVOICE_FILTER_BY_OPTIONS, + INVOICE_SORT_COLUMN_OPTIONS, + INVOICE_STATUS_OPTIONS, +} from "../../common/constants.mjs"; +import zohoBooks from "../../zoho_books.app.mjs"; export default { key: "zoho_books-list-invoices", name: "List Invoices", - description: "Lists all invoices with pagination.", - version: "0.2.1", + description: "Lists all invoices. [See the documentation](https://www.zoho.com/books/api/v3/contacts/#list-contacts)", + version: "0.3.0", type: "action", props: { - zoho_books: { - type: "app", - app: "zoho_books", - }, - organization_id: { - type: "string", - description: "In Zoho Books, your business is termed as an organization. If you have multiple businesses, you simply set each of those up as an individual organization. Each organization is an independent Zoho Books Organization with it's own organization ID, base currency, time zone, language, contacts, reports, etc.\n\nThe parameter `organization_id` should be sent in with every API request to identify the organization.\n\nThe `organization_id` can be obtained from the GET `/organizations` API's JSON response. Alternatively, it can be obtained from the **Manage Organizations** page in the admin console.", - }, - invoice_number: { + zohoBooks, + invoiceNumber: { type: "string", - description: "Search invoices by invoice number.Variants: `invoice_number_startswith` and `invoice_number_contains`. Max-length [100]", + label: "Invoice Number", + description: "Search invoices by invoice number. Max-length [100]", optional: true, }, - item_name: { + itemName: { type: "string", - description: "Search invoices by item name. Variants: `item_name_startswith` and `item_name_contains`. Max-length [100]", + label: "Item Name", + description: "Search invoices by item name. Max-length [100]", optional: true, }, - item_id: { + itemId: { type: "string", + label: "Item Id", description: "Search invoices by item id.", optional: true, }, - item_description: { + itemDescription: { type: "string", - description: "Search invoices by item description. Variants: `item_description_startswith` and `item_description_contains`. Max-length [100]", + label: "Item Description", + description: "Search invoices by item description. Max-length [100]", optional: true, }, - reference_number: { + referenceNumber: { type: "string", + label: "Reference Number", description: "The reference number of the invoice.", optional: true, }, - customer_name: { + customerName: { type: "string", + label: "Customer Name", description: "The name of the customer. Max-length [100]", optional: true, }, - recurring_invoice_id: { + recurringInvoiceId: { type: "string", + label: "Recurring Invoice Id", description: "ID of the recurring invoice from which the invoice is created.", optional: true, }, email: { type: "string", + label: "Email", description: "Search contacts by email id. Max-length [100]", optional: true, }, total: { type: "string", + label: "Total", description: "The total amount to be paid.", optional: true, }, balance: { type: "string", + label: "Balance", description: "The unpaid amount.", optional: true, }, - custom_field: { + customField: { type: "string", - description: "Search invoices by custom fields. Variants: `custom_field_startswith` and `custom_field_contains`.", + label: "Custom Field", + description: "Search invoices by custom fields.", optional: true, }, date: { type: "string", - description: "Search invoices by invoice date. Default date format is `yyyy-mm-dd`. Variants: `due_date_start`, `due_date_end`, `due_date_before` and `due_date_after`.", + label: "Date", + description: "Search invoices by invoice date. Default date format is `yyyy-mm-dd`.", optional: true, }, - due_date: { + dueDate: { type: "string", - description: "Search invoices by due date. Default date format is `yyyy-mm-dd`. Variants: `due_date_start`, `due_date_end`, `due_date_before` and `due_date_after`", + label: "Due Date", + description: "Search invoices by due date. Default date format is `yyyy-mm-dd`.", optional: true, }, - last_modified_time: { + lastModifiedTime: { type: "string", + label: "Last Modified Time", optional: true, }, status: { type: "string", - description: "Search invoices by invoice status. Allowed Values: `sent`, `draft`, `overdue`, `paid`, `void`, `unpaid`, `partially_paid` and `viewed`", + label: "Status", + description: "Search invoices by invoice status.", + options: INVOICE_STATUS_OPTIONS, optional: true, - options: [ - "sent", - "draft", - "overdue", - "paid", - "void", - "unpaid", - "partially_paid", - "viewed", - ], }, - customer_id: { + customerId: { type: "string", + label: "Customer Id", description: "ID of the customer the invoice has to be created.", optional: true, }, - filter_by: { + filterBy: { type: "string", - description: "Filter invoices by any status or payment expected date.Allowed Values: `Status.All`, `Status.Sent`, `Status.Draft`, `Status.OverDue`, `Status.Paid`, `Status.Void`, `Status.Unpaid`, `Status.PartiallyPaid`, `Status.Viewed` and `Date.PaymentExpectedDate`", + label: "Filter By", + description: "Filter invoices by any status or payment expected date.", optional: true, - options: [ - "Status.All", - "Status.Sent", - "Status.Draft", - "Status.OverDue", - "Status.Paid", - "Status.Void", - "Status.Unpaid", - "Status.PartiallyPaid", - "Status.Viewed", - "Date.PaymentExpectedDate", - ], + options: INVOICE_FILTER_BY_OPTIONS, }, - search_text: { + searchText: { type: "string", + label: "Search Text", description: "Search invoices by invoice number or purchase order or customer name. Max-length [100]", optional: true, }, - sort_column: { - type: "string", - description: "Sort invoices. Allowed Values: `customer_name`, `invoice_number`, `date`, `due_date`, `total`, `balance` and `created_time`.", - optional: true, - options: [ - "customer_name", - "invoice_number", - "date", - "due_date", - "total", - "balance", - "created_time", - ], - }, - page: { - type: "string", - description: "By default first page will be listed. For navigating through pages, use the `page` parameter.", - optional: true, - }, - per_page: { + sortColumn: { type: "string", - description: "The `per_page` parameter can be used to set the number of records that you want to receive in response.", + label: "Sort Column", + description: "Sort invoices.", + options: INVOICE_SORT_COLUMN_OPTIONS, optional: true, }, }, async run({ $ }) { - //See the API docs: https://www.zoho.com/books/api/v3/#Invoices_List_invoices - - if (!this.organization_id) { - throw new Error("Must provide organization_id parameter."); - } - - return await axios($, { - method: "get", - url: `https://books.${this.zoho_books.$auth.base_api_uri}/api/v3/invoices?organization_id=${this.organization_id}`, - headers: { - Authorization: `Zoho-oauthtoken ${this.zoho_books.$auth.oauth_access_token}`, - }, + const response = this.zohoBooks.paginate({ + fn: this.zohoBooks.listInvoices, + fieldName: "invoices", params: { - invoice_number: this.invoice_number, - item_name: this.item_name, - item_id: this.item_id, - item_description: this.item_description, - reference_number: this.reference_number, - customer_name: this.customer_name, - recurring_invoice_id: this.recurring_invoice_id, + invoice_number: this.invoiceNumber, + item_name: this.itemName, + item_id: this.itemId, + item_description: this.itemDescription, + reference_number: this.referenceNumber, + customer_name: this.customerName, + recurring_invoice_id: this.recurringInvoiceId, email: this.email, total: this.total, balance: this.balance, - custom_field: this.custom_field, + custom_field: this.customField, date: this.date, - due_date: this.due_date, - last_modified_time: this.last_modified_time, + due_date: this.dueDate, + last_modified_time: this.lastModifiedTime, status: this.status, - customer_id: this.customer_id, - filter_by: this.filter_by, - search_text: this.search_text, - sort_column: this.sort_column, - page: this.page, - per_page: this.per_page, + customer_id: this.customerId, + filter_by: this.filterBy, + search_text: this.searchText, + sort_column: this.sortColumn, }, }); + + const responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + $.export("$summary", `Successfully fetched ${responseArray.length} invoices(s)`); + return responseArray; }, }; diff --git a/components/zoho_books/actions/make-api-call/make-api-call.mjs b/components/zoho_books/actions/make-api-call/make-api-call.mjs index 77fa7606da69c..7c1d0c4d5c928 100644 --- a/components/zoho_books/actions/make-api-call/make-api-call.mjs +++ b/components/zoho_books/actions/make-api-call/make-api-call.mjs @@ -1,24 +1,24 @@ // legacy_hash_id: a_PNinw1 -import { axios } from "@pipedream/platform"; +import { checkUrl } from "../../common/utils.mjs"; +import zohoBooks from "../../zoho_books.app.mjs"; export default { key: "zoho_books-make-api-call", name: "Make API Call", description: "Makes an aribitrary call to Zoho Books API", - version: "0.3.1", + version: "0.4.0", type: "action", props: { - zoho_books: { - type: "app", - app: "zoho_books", - }, - query_string: { + zohoBooks, + queryString: { type: "string", + label: "Query String", description: "Query string of the request.", optional: true, }, - request_method: { + requestMethod: { type: "string", + label: "Request Method", description: "Http method to use in the request.", options: [ "get", @@ -28,30 +28,35 @@ export default { "delete", ], }, - relative_url: { + relativeUrl: { type: "string", - description: "A path relative to Zoho Books to send the request against.", + label: "Relative Url", + description: "A path relative to Zoho Books to send the request against. For example, use `/expenses` for [List Expenses API](https://www.zoho.com/books/api/v3/expenses/#list-expenses). [See the documentation](https://www.zoho.com/books/api/v3/introduction/)", }, headers: { type: "object", + label: "Headers", description: "Headers to send in the request.", + optional: true, }, - request_body: { + requestBody: { type: "object", + label: "Request Body", description: "Body of the request.", optional: true, }, }, async run({ $ }) { - // See the API docs: https://www.zoho.com/books/api/v3/#introduction - - this.query_string = this.query_string || ""; - - return await axios($, { - method: this.request_method, - url: `https://books.${this.zoho_books.$auth.base_api_uri}/api/v3/${this.relative_url}${this.query_string}`, + const response = await this.zohoBooks._makeRequest({ + $, + path: checkUrl(this.relativeUrl), + params: this.queryString, + method: this.requestMethod, headers: this.headers, - data: this.request_body, + data: this.requestBody, }); + + $.export("$summary", `Successfully called the path: ${this.relativeUrl}`); + return response; }, }; diff --git a/components/zoho_books/actions/update-salesorder/update-salesorder.mjs b/components/zoho_books/actions/update-salesorder/update-salesorder.mjs index 7b261d66c93ae..0f9b94ed28650 100644 --- a/components/zoho_books/actions/update-salesorder/update-salesorder.mjs +++ b/components/zoho_books/actions/update-salesorder/update-salesorder.mjs @@ -1,350 +1,453 @@ // legacy_hash_id: a_m8iYLJ -import { axios } from "@pipedream/platform"; +import { ConfigurationError } from "@pipedream/platform"; +import { + clearObj, parseObject, +} from "../../common/utils.mjs"; +import zohoBooks from "../../zoho_books.app.mjs"; export default { key: "zoho_books-update-salesorder", name: "Update Sales Order", - description: "Updates an existing sales order.", - version: "0.2.1", + description: "Updates an existing sales order. [See the documentation](https://www.zoho.com/books/api/v3/sales-order/#update-a-sales-order)", + version: "0.3.0", type: "action", props: { - zoho_books: { - type: "app", - app: "zoho_books", - }, - organization_id: { - type: "string", - description: "In Zoho Books, your business is termed as an organization. If you have multiple businesses, you simply set each of those up as an individual organization. Each organization is an independent Zoho Books Organization with it's own organization ID, base currency, time zone, language, contacts, reports, etc.\n\nThe parameter `organization_id` should be sent in with every API request to identify the organization.\n\nThe `organization_id` can be obtained from the GET `/organizations` API's JSON response. Alternatively, it can be obtained from the **Manage Organizations** page in the admin console.", - }, - salesorder_id: { - type: "string", - description: "ID of the sales order to update.", + zohoBooks, + salesorderId: { + propDefinition: [ + zohoBooks, + "salesorderId", + ], }, - customer_id: { - type: "string", - description: "ID of the customer to whom the sales order has to be updated.", + customerId: { + propDefinition: [ + zohoBooks, + "customerId", + ], + description: "ID of the customer to whom the sales order has to be created.", + optional: true, }, - contact_persons: { - type: "any", - description: "Array of contact person(s) for whom sales order has to be sent.", + contactPersons: { + propDefinition: [ + zohoBooks, + "contactPersons", + ({ customerId }) => ({ + customerId, + }), + ], + description: "A list of contact person(s) for whom sales order has to be sent.", optional: true, }, date: { - type: "string", - description: "The date, the sales order is created.", + propDefinition: [ + zohoBooks, + "date", + ], optional: true, }, - shipment_date: { - type: "string", - description: "Shipping date of sales order.", + shipmentDate: { + propDefinition: [ + zohoBooks, + "shipmentDate", + ], optional: true, }, - custom_fields: { - type: "any", - description: "Custom fields for a sales order.", + customFields: { + propDefinition: [ + zohoBooks, + "customFields", + ], optional: true, }, - place_of_supply: { - type: "string", - description: "Place where the goods/services are supplied to. (If not given, `place of contact` given for the contact will be taken)\nSupported codes for UAE emirates are :\nAbu Dhabi - AB`,\nAjman - `AJ`,\nDubai - `DU`,\nFujairah - `FU`,\nRas al-Khaimah - `RA`,\nSharjah - `SH`,\nUmm al-Quwain - `UM`\nSupported codes for the GCC countries are :\nUnited Arab Emirates - `AE`,\nSaudi Arabia - `SA`,\nBahrain - `BH`,\nKuwait - `KW`,\nOman - `OM`,\nQatar - `QA`.", + placeOfSupply: { + propDefinition: [ + zohoBooks, + "placeOfSupply", + ], optional: true, }, - salesperson_id: { - type: "string", - description: "ID of the salesperson.", + salespersonId: { + propDefinition: [ + zohoBooks, + "salespersonId", + ], optional: true, }, - merchant_id: { - type: "string", - description: "ID of the merchant.", + merchantId: { + propDefinition: [ + zohoBooks, + "merchantId", + ], optional: true, }, - gst_treatment: { - type: "string", - description: "Choose whether the contact is GST registered/unregistered/consumer/overseas. Allowed values are `business_gst`, `business_none`, `overseas`, `consumer`.", - optional: true, - options: [ - "business_gst", - "business_none", - "overseas", - "consumer", + gstTreatment: { + propDefinition: [ + zohoBooks, + "gstTreatment", ], + optional: true, }, - gst_no: { - type: "string", - description: "15 digit GST identification number of the customer.", + gstNo: { + propDefinition: [ + zohoBooks, + "gstNo", + ], optional: true, }, - is_inclusive_tax: { - type: "boolean", - description: "Used to specify whether the line item rates are inclusive or exclusive of tax.", + isInclusiveTax: { + propDefinition: [ + zohoBooks, + "isInclusiveTax", + ], optional: true, }, - line_items: { - type: "any", - description: "Line items of a sales order. To delete a line item just remove it from this line_items list.", + lineItems: { + propDefinition: [ + zohoBooks, + "lineItems", + ], optional: true, }, notes: { - type: "string", - description: "Notes for this Sales Order.", + propDefinition: [ + zohoBooks, + "notes", + ], optional: true, }, terms: { - type: "string", + propDefinition: [ + zohoBooks, + "terms", + ], optional: true, }, - billing_address_id: { - type: "string", - description: "ID of the Billing Address", + billingAddressId: { + propDefinition: [ + zohoBooks, + "billingAddressId", + ], optional: true, }, - shipping_address_id: { - type: "string", - description: "ID of the Shipping Address.", + shippingAddressId: { + propDefinition: [ + zohoBooks, + "shippingAddressId", + ], optional: true, }, - crm_owner_id: { - type: "string", + crmOwnerId: { + propDefinition: [ + zohoBooks, + "crmOwnerId", + ], optional: true, }, - crm_custom_reference_id: { - type: "string", + crmCustomReferenceId: { + propDefinition: [ + zohoBooks, + "crmCustomReferenceId", + ], optional: true, }, - vat_treatment: { - type: "string", - description: "Enter vat treatment.", + vatTreatment: { + propDefinition: [ + zohoBooks, + "vatTreatment", + ], optional: true, }, - tax_treatment: { - type: "string", - description: "VAT treatment for the invoice .Choose whether the contact falls under: `vat_registered`, `vat_not_registered`, `gcc_vat_not_registered`, `gcc_vat_registered`, `non_gcc` , `dz_vat_registered` and `dz_vat_not_registered` supported only for UAE.", - optional: true, - options: [ - "vat_registered", - "vat_not_registered", - "gcc_vat_not_registered", - "gcc_vat_registered", - "non_gcc", - "dz_vat_registered", - "dz_vat_not_registered", + taxTreatment: { + propDefinition: [ + zohoBooks, + "taxTreatment", ], + optional: true, }, - salesorder_number: { - type: "string", - description: "Mandatory if auto number generation is disabled.", + salesorderNumber: { + propDefinition: [ + zohoBooks, + "salesorderNumber", + ], optional: true, }, - reference_number: { - type: "string", - description: "**For Customer Only** : If a contact is assigned to any particular user, that user can manage transactions for the contact", + referenceNumber: { + propDefinition: [ + zohoBooks, + "referenceNumber", + ], optional: true, }, - is_update_customer: { - type: "boolean", - description: "Boolean to update billing address of customer.", + isUpdateCustomer: { + propDefinition: [ + zohoBooks, + "isUpdateCustomer", + ], optional: true, }, discount: { - type: "string", - description: "Discount applied to the sales order. It can be either in % or in amount. e.g. 12.5% or 190.", + propDefinition: [ + zohoBooks, + "discount", + ], optional: true, }, - exchange_rate: { - type: "string", - description: "Exchange rate of the currency.", + exchangeRate: { + propDefinition: [ + zohoBooks, + "exchangeRate", + ], optional: true, }, - salesperson_name: { - type: "string", - description: "Name of the sales person.", + salespersonName: { + propDefinition: [ + zohoBooks, + "salespersonName", + ], optional: true, }, - notes_default: { - type: "string", - description: "Default Notes for the Sales Order.", + notesDefault: { + propDefinition: [ + zohoBooks, + "notesDefault", + ], optional: true, }, - terms_default: { - type: "string", - description: "Default Terms of the Sales Order.", + termsDefault: { + propDefinition: [ + zohoBooks, + "termsDefault", + ], optional: true, }, - tax_id: { - type: "string", - description: "Tax ID for the Sales Order.", + taxId: { + propDefinition: [ + zohoBooks, + "taxId", + ], optional: true, }, - tax_authority_id: { - type: "string", - description: "ID of the tax authority. Tax authority depends on the location of the customer.", + taxAuthorityId: { + propDefinition: [ + zohoBooks, + "taxAuthorityId", + ], optional: true, }, - tax_exemption_id: { - type: "string", - description: "ID of the tax exemption applied.", + taxExemptionId: { + propDefinition: [ + zohoBooks, + "taxExemptionId", + ], optional: true, }, - tax_authority_name: { - type: "string", - description: "Tax Authority's name.", + taxAuthorityName: { + propDefinition: [ + zohoBooks, + "taxAuthorityName", + ], optional: true, }, - tax_exemption_code: { - type: "string", - description: "Code of Tax Exemption that is applied.", + taxExemptionCode: { + propDefinition: [ + zohoBooks, + "taxExemptionCode", + ], optional: true, }, - avatax_exempt_no: { - type: "string", - description: "Exemption certificate number of the customer.", + avataxExemptNo: { + propDefinition: [ + zohoBooks, + "taxExemptionId", + ], optional: true, }, - avatax_use_code: { - type: "string", - description: "Used to group like customers for exemption purposes. It is a custom value that links customers to a tax rule.", + avataxUseCode: { + propDefinition: [ + zohoBooks, + "avataxUseCode", + ], optional: true, }, - shipping_charge: { - type: "string", + shippingCharge: { + propDefinition: [ + zohoBooks, + "shippingCharge", + ], optional: true, }, adjustment: { - type: "string", + propDefinition: [ + zohoBooks, + "adjustment", + ], optional: true, }, - delivery_method: { - type: "string", + deliveryMethod: { + propDefinition: [ + zohoBooks, + "deliveryMethod", + ], optional: true, }, - is_discount_before_tax: { - type: "boolean", - description: "Used to specify how the discount has to applied. Either before or after the calculation of tax.", + estimateId: { + type: "string", + label: "Estimate Id", + description: "ID of the estimate associated with the Sales Order.", optional: true, }, - discount_type: { - type: "string", - description: "How the discount is specified. Allowed values are entity_level or item_level.", + isDiscountBeforeTax: { + propDefinition: [ + zohoBooks, + "isDiscountBeforeTax", + ], optional: true, - options: [ - "entity_level", - "item_level", + }, + discountType: { + propDefinition: [ + zohoBooks, + "discountType", ], + optional: true, }, - adjustment_description: { - type: "string", + adjustmentDescription: { + propDefinition: [ + zohoBooks, + "adjustmentDescription", + ], optional: true, }, - pricebook_id: { - type: "string", + pricebookId: { + propDefinition: [ + zohoBooks, + "pricebookId", + ], optional: true, }, - template_id: { - type: "string", - description: "ID of the pdf template.", + templateId: { + propDefinition: [ + zohoBooks, + "pdfTemplateId", + ], optional: true, }, documents: { - type: "any", + propDefinition: [ + zohoBooks, + "documents", + ], optional: true, }, - zcrm_potential_id: { - type: "string", + zcrmPotentialId: { + propDefinition: [ + zohoBooks, + "zcrmPotentialId", + ], optional: true, }, - zcrm_potential_name: { - type: "string", + zcrmPotentialName: { + propDefinition: [ + zohoBooks, + "zcrmPotentialName", + ], optional: true, }, - ignore_auto_number_generation: { - type: "string", - description: "Ignore auto sales order number generation for this sales order. This mandates the sales order number.", + ignoreAutoNumberGeneration: { + propDefinition: [ + zohoBooks, + "ignoreAutoNumberGeneration", + ], optional: true, }, - can_send_in_mail: { - type: "string", - description: "Can the file be sent in mail.", + canSendInMail: { + propDefinition: [ + zohoBooks, + "canSendInMail", + ], optional: true, }, totalFiles: { - type: "string", - description: "Total number of files.", + propDefinition: [ + zohoBooks, + "totalFiles", + ], optional: true, }, doc: { - type: "string", - description: "Document that is to be attached", + propDefinition: [ + zohoBooks, + "doc", + ], optional: true, }, }, async run({ $ }) { - //See the API docs: https://www.zoho.com/books/api/v3/#Sales-Order_Update_a_sales_order + const data = clearObj({ + customer_id: this.customerId, + contact_persons: this.contactPersons, + date: this.date, + shipment_date: this.shipmentDate, + custom_fields: parseObject(this.customFields), + place_of_supply: this.placeOfSupply, + salesperson_id: this.salespersonId, + merchant_id: this.merchantId, + gst_treatment: this.gstTreatment, + gst_no: this.gstNo, + is_inclusive_tax: this.isInclusiveTax, + line_items: parseObject(this.lineItems), + notes: this.notes, + terms: this.terms, + billing_address_id: this.billingAddressId, + shipping_address_id: this.shippingAddressId, + crm_owner_id: this.crmOwnerId, + crm_custom_reference_id: this.crmCustomReferenceId, + vat_treatment: this.vatTreatment, + tax_treatment: this.taxTreatment, + salesorder_number: this.salesorderNumber, + reference_number: this.referenceNumber, + is_update_customer: this.isUpdateCustomer, + discount: this.discount, + exchange_rate: this.exchangeRate && parseFloat(this.exchangeRate), + salesperson_name: this.salespersonName, + notes_default: this.notesDefault, + terms_default: this.termsDefault, + tax_id: this.taxId, + tax_authority_id: this.taxAuthorityId, + tax_exemption_id: this.taxExemptionId, + tax_authority_name: this.taxAuthorityName, + tax_exemption_code: this.taxExemptionCode, + avatax_exempt_no: this.avataxExemptNo, + avatax_use_code: this.avataxUseCode, + shipping_charge: this.shippingCharge && parseFloat(this.shippingCharge), + adjustment: this.adjustment && parseFloat(this.adjustment), + delivery_method: this.deliveryMethod, + estimate_id: this.estimateId, + is_discount_before_tax: this.isDiscountBeforeTax, + discount_type: this.discountType, + adjustment_description: this.adjustmentDescription, + pricebook_id: this.pricebookId, + template_id: this.templateId, + documents: parseObject(this.documents), + zcrm_potential_id: this.zcrmPotentialId, + zcrm_potential_name: this.zcrmPotentialName, + }); - if (!this.organization_id || !this.salesorder_id) { - throw new Error("Must provide organization_id, salesorder_id parameters."); + if (!Object.keys(data).length) { + throw new ConfigurationError("You must provide at least one field."); } - - return await axios($, { - method: "put", - url: `https://books.${this.zoho_books.$auth.base_api_uri}/api/v3/salesorders/${this.salesorder_id}?organization_id=${this.organization_id}`, - headers: { - Authorization: `Zoho-oauthtoken ${this.zoho_books.$auth.oauth_access_token}`, - }, - data: { - customer_id: this.customer_id, - contact_persons: this.contact_persons, - date: this.date, - shipment_date: this.shipment_date, - custom_fields: this.custom_fields, - place_of_supply: this.place_of_supply, - salesperson_id: this.salesperson_id, - merchant_id: this.merchant_id, - gst_treatment: this.gst_treatment, - gst_no: this.gst_no, - is_inclusive_tax: this.is_inclusive_tax, - line_items: this.line_items, - notes: this.notes, - terms: this.terms, - billing_address_id: this.billing_address_id, - shipping_address_id: this.shipping_address_id, - crm_owner_id: this.crm_owner_id, - crm_custom_reference_id: this.crm_custom_reference_id, - vat_treatment: this.vat_treatment, - tax_treatment: this.tax_treatment, - salesorder_number: this.salesorder_number, - reference_number: this.reference_number, - is_update_customer: this.is_update_customer, - discount: this.discount, - exchange_rate: this.exchange_rate, - salesperson_name: this.salesperson_name, - notes_default: this.notes_default, - terms_default: this.terms_default, - tax_id: this.tax_id, - tax_authority_id: this.tax_authority_id, - tax_exemption_id: this.tax_exemption_id, - tax_authority_name: this.tax_authority_name, - tax_exemption_code: this.tax_exemption_code, - avatax_exempt_no: this.avatax_exempt_no, - avatax_use_code: this.avatax_use_code, - shipping_charge: this.shipping_charge, - adjustment: this.adjustment, - delivery_method: this.delivery_method, - is_discount_before_tax: this.is_discount_before_tax, - discount_type: this.discount_type, - adjustment_description: this.adjustment_description, - pricebook_id: this.pricebook_id, - template_id: this.template_id, - documents: this.documents, - zcrm_potential_id: this.zcrm_potential_id, - zcrm_potential_name: this.zcrm_potential_name, - }, - params: { - ignore_auto_number_generation: this.ignore_auto_number_generation, - can_send_in_mail: this.can_send_in_mail, + const response = await this.zohoBooks.updateSalesorder({ + $, + salesorderId: this.salesorderId, + params: clearObj({ + ignore_auto_number_generation: this.ignoreAutoNumberGeneration, + can_send_in_mail: this.canSendInMail, totalFiles: this.totalFiles, doc: this.doc, - }, + }), + data, }); + + $.export("$summary", `Salesorder successfully updated with Id: ${this.salesorderId}`); + return response; }, }; diff --git a/components/zoho_books/common/constants.mjs b/components/zoho_books/common/constants.mjs new file mode 100644 index 0000000000000..673ea47bcc472 --- /dev/null +++ b/components/zoho_books/common/constants.mjs @@ -0,0 +1,196 @@ +export const PAYMENT_MODE_OPTIONS = [ + "check", + "cash", + "creditcard", + "banktransfer", + "bankremittance", + "autotransaction", + "others", +]; + +export const PLACE_OF_SUPPLY_OPTIONS = [ + { + label: "Abu Dhabi (UAE Supported)", + value: "AB", + }, + { + label: "Ajman (UAE Supported)", + value: "AJ", + }, + { + label: "Dubai (UAE Supported)", + value: "DU", + }, + { + label: "Fujairah (UAE Supported)", + value: "FU", + }, + { + label: "Ras al-Khaimah (UAE Supported)", + value: "RA", + }, + { + label: "Sharjah (UAE Supported)", + value: "SH", + }, + { + label: "Umm al-Quwain (UAE Supported)", + value: "UM", + }, + { + label: "United Arab Emirates (GCC Supported)", + value: "AE", + }, + { + label: "Saudi Arabia (GCC Supported)", + value: "SA", + }, + { + label: "Bahrain (GCC Supported)", + value: "BH", + }, + { + label: "Kuwait (GCC Supported)", + value: "KW", + }, + { + label: "Oman (GCC Supported)", + value: "OM", + }, + { + label: "Qatar (GCC Supported)", + value: "QA", + }, +]; + +export const GST_TREATMENT_OPTIONS = [ + "business_gst", + "business_none", + "overseas", + "consumer", +]; + +export const DISCOUNT_TYPE_OPTIONS = [ + "entity_level", + "item_level", +]; + +export const PRODUCT_TYPE_OPTIONS = [ + "goods", + "service", + "digital_service", +]; + +export const ITEM_TYPE_OPTIONS = [ + "sales", + "purchases", + "sales_and_purchases", + "inventory", +]; + +export const TAX_TREATMENT_OPTIONS = [ + "vat_registered", + "vat_not_registered", + "gcc_vat_not_registered", + "gcc_vat_registered", + "non_gcc", + "dz_vat_registered", + "dz_vat_not_registered", + "home_country_mexico", + "border_region_mexico", + "non_mexico", + "vat_registered ", + "vat_not_registered ", + "non_kenya", + "vat_registered", + "vat_not_registered", + "overseas", +]; + +export const PRINT_OPTIONS = [ + "true", + "false", + "on", + "off", +]; + +export const STATUS_OPTIONS = [ + "unbilled", + "invoiced", + "reimbursed", + "non-billable", + "billable", +]; + +export const SORT_COLUMN_OPTIONS = [ + "date", + "account_name", + "total", + "bcy_total", + "reference_number", + "customer_name", + "created_time", +]; + +export const FILTER_BY_OPTIONS = [ + "Status.All", + "Status.Billable", + "Status.Nonbillable", + "Status.Reimbursed", + "Status.Invoiced", + "Status.Unbilled", +]; + +export const INVOICE_STATUS_OPTIONS = [ + "sent", + "draft", + "overdue", + "paid", + "void", + "unpaid", + "partially_paid", + "viewed", +]; + +export const INVOICE_FILTER_BY_OPTIONS = [ + "Status.All", + "Status.Sent", + "Status.Draft", + "Status.OverDue", + "Status.Paid", + "Status.Void", + "Status.Unpaid", + "Status.PartiallyPaid", + "Status.Viewed", + "Date.PaymentExpectedDate", +]; + +export const INVOICE_SORT_COLUMN_OPTIONS = [ + "customer_name", + "invoice_number", + "date", + "due_date", + "total", + "balance", + "created_time", +]; + +export const LANGUAGE_CODE_OPTIONS = [ + "de", + "en", + "es", + "fr", + "it", + "ja", + "nl", + "pt", + "pt_br", + "sv", + "zh", + "en_gb", +]; + +export const CUSTOMER_SUB_TYPE_OPTIONS = [ + "business", + "individual", +]; diff --git a/components/zoho_books/common/utils.mjs b/components/zoho_books/common/utils.mjs new file mode 100644 index 0000000000000..e03aac64cb2e5 --- /dev/null +++ b/components/zoho_books/common/utils.mjs @@ -0,0 +1,51 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; + +export const clearObj = (obj) => { + return Object.entries(obj) + .filter(([ + , + v, + ]) => (v != null && v != "" && JSON.stringify(v) != "{}")) + .reduce((acc, [ + k, + v, + ]) => ({ + ...acc, + [k]: (!Array.isArray(v) && v === Object(v)) + ? clearObj(v) + : v, + }), {}); +}; + +export const checkUrl = (url) => { + if (url.startsWith("/books/v3")) { + return url.substring(9); + } + if (url.startsWith("/v3")) { + return url.substring(3); + } + return url; +}; diff --git a/components/zoho_books/package.json b/components/zoho_books/package.json new file mode 100644 index 0000000000000..6c5d0cca37ec9 --- /dev/null +++ b/components/zoho_books/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/zoho_books", + "version": "0.2.0", + "description": "Pipedream Zoho Books Components", + "main": "zoho_books.app.mjs", + "keywords": [ + "pipedream", + "zoho_books" + ], + "homepage": "https://pipedream.com/apps/zoho_books", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/zoho_books/sources/common/base.mjs b/components/zoho_books/sources/common/base.mjs new file mode 100644 index 0000000000000..11929a8bb4406 --- /dev/null +++ b/components/zoho_books/sources/common/base.mjs @@ -0,0 +1,60 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import zohoBooks from "../../zoho_books.app.mjs"; + +export default { + props: { + zohoBooks, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + + const response = this.zohoBooks.paginate({ + fn: this.getFunction(), + fieldName: this.getFieldName(), + }); + + let responseArray = []; + for await (const item of response) { + if (item.created_time <= lastDate) break; + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastDate(Date.parse(responseArray[0].created_time)); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item[this.getFieldId()], + summary: this.getSummary(item), + ts: Date.parse(item.created_time), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/zoho_books/sources/new-customer/new-customer.mjs b/components/zoho_books/sources/new-customer/new-customer.mjs new file mode 100644 index 0000000000000..42e7b48b08a30 --- /dev/null +++ b/components/zoho_books/sources/new-customer/new-customer.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "zoho_books-new-customer", + name: "New Customer", + description: "Emit new event when a new customer is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.zohoBooks.listContacts; + }, + getFieldName() { + return "contacts"; + }, + getFieldId() { + return "contact_id"; + }, + getSummary(item) { + return `New Customer: ${item.contact_name}`; + }, + }, + sampleEmit, +}; diff --git a/components/zoho_books/sources/new-customer/test-event.mjs b/components/zoho_books/sources/new-customer/test-event.mjs new file mode 100644 index 0000000000000..2f6b157db1e12 --- /dev/null +++ b/components/zoho_books/sources/new-customer/test-event.mjs @@ -0,0 +1,20 @@ +export default { + "contact_id": 460000000026049, + "contact_name": "Bowman and Co", + "company_name": "Bowman and Co", + "contact_type": "customer", + "status": "active", + "payment_terms": 15, + "payment_terms_label": "Net 15", + "currency_id": 460000000000097, + "currency_code": "USD", + "outstanding_receivable_amount": 250, + "unused_credits_receivable_amount": 1369.66, + "first_name": "Will", + "last_name": "Smith", + "email": "willsmith@bowmanfurniture.com", + "phone": "+1-925-921-9201", + "mobile": "+1-4054439562", + "created_time": "2013-08-05T12:06:10+0530", + "last_modified_time": "2013-10-07T18:24:51+0530" +} \ No newline at end of file diff --git a/components/zoho_books/sources/new-expense/new-expense.mjs b/components/zoho_books/sources/new-expense/new-expense.mjs new file mode 100644 index 0000000000000..799e4cf4b6cd4 --- /dev/null +++ b/components/zoho_books/sources/new-expense/new-expense.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "zoho_books-new-expense", + name: "New Expense", + description: "Emit new event when a new expense is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.zohoBooks.listExpenses; + }, + getFieldName() { + return "expenses"; + }, + getFieldId() { + return "expense_id"; + }, + getSummary(item) { + return `New Expense: ${item.expense_id}`; + }, + }, + sampleEmit, +}; diff --git a/components/zoho_books/sources/new-expense/test-event.mjs b/components/zoho_books/sources/new-expense/test-event.mjs new file mode 100644 index 0000000000000..75a5cccedbbdd --- /dev/null +++ b/components/zoho_books/sources/new-expense/test-event.mjs @@ -0,0 +1,25 @@ +export default { + "expense_id": 982000000030049, + "date": "2013-11-18", + "account_name": "Rent", + "description": "Marketing", + "currency_id": 982000000567001, + "currency_code": "USD", + "bcy_total": 100, + "bcy_total_without_tax": 100, + "total": 100, + "total_without_tax": 100, + "is_billable": true, + "reference_number": null, + "customer_id": 982000000567001, + "customer_name": "Bowman & Co", + "status": "unbilled", + "created_time": "2013-11-18T00:00:00.000Z", + "last_modified_time": " ", + "expense_receipt_name": " ", + "mileage_rate": " ", + "mileage_unit": " ", + "expense_type": "non-mileage", + "start_reading": " ", + "end_reading": " " +} \ No newline at end of file diff --git a/components/zoho_books/sources/new-sales-order/new-sales-order.mjs b/components/zoho_books/sources/new-sales-order/new-sales-order.mjs new file mode 100644 index 0000000000000..5078c8b6074be --- /dev/null +++ b/components/zoho_books/sources/new-sales-order/new-sales-order.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "zoho_books-new-sales-order", + name: "New Sales Order", + description: "Emit new event when a new sales order is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.zohoBooks.listSalesorders; + }, + getFieldName() { + return "salesorders"; + }, + getFieldId() { + return "salesorder_id"; + }, + getSummary(item) { + return `New Sales Order: ${item.salesorder_number}`; + }, + }, + sampleEmit, +}; diff --git a/components/zoho_books/sources/new-sales-order/test-event.mjs b/components/zoho_books/sources/new-sales-order/test-event.mjs new file mode 100644 index 0000000000000..4386460147a04 --- /dev/null +++ b/components/zoho_books/sources/new-sales-order/test-event.mjs @@ -0,0 +1,30 @@ +export default { + "salesorder_id": "460000000039129", + "zcrm_potential_id": "460000000033001", + "zcrm_potential_name": "string", + "customer_name": "SAF Instruments Inc", + "customer_id": "460000000017138", + "status": "open", + "salesorder_number": "SO-00001", + "reference_number": "REF-001", + "date": "2014-07-28", + "shipment_date": "string", + "shipment_days": 2, + "currency_id": "460000000000097", + "currency_code": "USD", + "total": 12400, + "sub_total": 11800, + "bcy_total": 400, + "created_time": "2014-07-28T08:29:07+0530", + "last_modified_time": "2014-08-25T11:23:26+0530", + "is_emailed": false, + "has_attachment": false, + "custom_fields": [ + { + "customfield_id": "460000000639129", + "index": 1, + "value": "Normal", + "label": "Priority" + } + ] +} \ No newline at end of file diff --git a/components/zoho_books/zoho_books.app.mjs b/components/zoho_books/zoho_books.app.mjs index 3ae9249b20ece..09c5f6ec1cb58 100644 --- a/components/zoho_books/zoho_books.app.mjs +++ b/components/zoho_books/zoho_books.app.mjs @@ -1,11 +1,747 @@ +import { axios } from "@pipedream/platform"; +import { + DISCOUNT_TYPE_OPTIONS, + GST_TREATMENT_OPTIONS, + PLACE_OF_SUPPLY_OPTIONS, + TAX_TREATMENT_OPTIONS, +} from "./common/constants.mjs"; + export default { type: "app", app: "zoho_books", - propDefinitions: {}, + propDefinitions: { + accountId: { + type: "string", + label: "Account Id", + description: "ID of the cash/bank account the payment has to be deposited.", + async options({ page }) { + const { bankaccounts } = await this.listBankAccounts({ + params: { + page: page + 1, + }, + }); + + return bankaccounts.map(({ + account_id: value, account_name: label, + }) => ({ + label, + value, + })); + }, + }, + contactPersons: { + type: "string[]", + label: "Contact Persons", + description: "IDs of the contact persons the thank you mail has to be triggered.", + async options({ + page, customerId, + }) { + const { contact_persons: data } = await this.listContactPersons({ + customerId, + params: { + page: page + 1, + }, + }); + + return data.map(({ + contact_person_id: value, first_name: fName, last_name: lName, email, + }) => ({ + label: `${fName} ${lName} ${email}`, + value, + })); + }, + }, + currencyId: { + type: "string", + label: "Currency Id", + description: "The id of the customer.", + async options({ page }) { + const { currencies } = await this.listCurrencies({ + params: { + page: page + 1, + }, + }); + return currencies.map(({ + currency_id: value, currency_name: label, + }) => ({ + label, + value, + })); + }, + }, + customerId: { + type: "string", + label: "Customer Id", + description: "Customer ID of the customer involved in the payment.", + async options({ page }) { + const { contacts } = await this.listContacts({ + params: { + page: page + 1, + }, + }); + return contacts.map(({ + contact_id: value, contact_name: label, + }) => ({ + label, + value, + })); + }, + }, + expenseId: { + type: "string", + label: "Expense Id", + async options({ page }) { + const { expenses } = await this.listExpenses({ + params: { + page: page + 1, + }, + }); + return expenses.map(({ + expense_id: value, description: label, + }) => ({ + label, + value, + })); + }, + }, + invoiceId: { + type: "string", + label: "Invoice Id", + description: "ID of the invoice to get payments of.", + async options({ + page, customerId, + }) { + const { invoices } = await this.listInvoices({ + params: { + page: page + 1, + customer_id: customerId, + }, + }); + + return invoices.map(({ + invoice_id: value, invoice_number: inv, customer_name: name, + }) => ({ + label: `${inv} - ${name}`, + value, + })); + }, + }, + itemId: { + type: "string", + label: "Item Id", + description: "ID of the item to get details.", + async options({ page }) { + const { items } = await this.listItems({ + params: { + page: page + 1, + }, + }); + + return items.map(({ + item_id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + recurringInvoiceId: { + type: "string", + label: "Recurring Invoice Id", + description: "ID of the recurring invoice from which the invoice is created.", + async options({ + page, customerId, + }) { + const { recurring_invoices: data } = await this.listRecurringInvoices({ + params: { + page: page + 1, + customerId, + }, + }); + + return data.map(({ + recurring_invoice_id: value, recurrence_name: label, + }) => ({ + label, + value, + })); + }, + }, + salesorderId: { + type: "string", + label: "Salesorder Id", + description: "ID of the sales order to update.", + async options({ page }) { + const { salesorders } = await this.listSalesorders({ + params: { + page: page + 1, + }, + }); + + return salesorders.map(({ + salesorder_id: value, salesorder_number: label, + }) => ({ + label, + value, + })); + }, + }, + taxAuthorityId: { + type: "string", + label: "Tax Authority Id", + description: "ID of the tax authority. Tax authority depends on the location of the customer. For example, if the customer is located in NY, then the tax authority is NY tax authority.", + async options({ page }) { + const { tax_authorities: data } = await this.listTaxAuthorities({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + tax_authority_id: value, tax_authority_name: label, + }) => ({ + label, + value, + })); + }, + }, + taxExemptionId: { + type: "string", + label: "Tax Exemption Id", + description: "ID of the tax exemption.", + async options({ page }) { + const { tax_exemptions: data } = await this.listTaxExemptions({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + tax_exemption_id: value, tax_exemption_code: label, + }) => ({ + label, + value, + })); + }, + }, + taxId: { + type: "string", + label: "Tax Id", + description: "ID of the tax.", + async options({ page }) { + const { taxes } = await this.listTaxes({ + params: { + page: page + 1, + }, + }); + + return taxes.map(({ + tax_id: value, tax_name: label, + }) => ({ + label, + value, + })); + }, + }, + templateId: { + type: "string", + label: "Template Id", + description: "ID of the pdf template associated with the invoice.", + async options({ page }) { + const { templates } = await this.listTemplates({ + params: { + page: page + 1, + }, + }); + + return templates.map(({ + template_id: value, template_name: label, + }) => ({ + label, + value, + })); + }, + }, + timeEntryIds: { + type: "string[]", + label: "Time Entry Ids", + description: "IDs of the time entries associated with the project.", + async options({ page }) { + const { time_entries: data } = await this.listTimeEntries({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + time_entry_id: value, task_name: tName, user_name: uName, + }) => ({ + label: `${tName} - ${uName}`, + value, + })); + }, + }, + date: { + type: "string", + label: "date", + description: "The date, the sales order is created.", + }, + shipmentDate: { + type: "string", + label: "Shipment Date", + description: "Shipping date of sales order.", + }, + customFields: { + type: "string[]", + label: "Custom Fields", + description: "A list of custom field objects for a sales order. **Example: {\"customfield_id\": \"1352827000000156060\", \"value\": \"value\" }** [See the documentation](https://www.zoho.com/books/api/v3/sales-order/#create-a-sales-order) for further details.", + }, + placeOfSupply: { + type: "string", + label: "Place Of Supply", + description: "Place where the goods/services are supplied to. (If not given, `place of contact` given for the contact will be taken).", + options: PLACE_OF_SUPPLY_OPTIONS, + }, + salespersonId: { + type: "string", + label: "Salesperson Id", + description: "ID of the salesperson.", + }, + merchantId: { + type: "string", + label: "Merchant Id", + description: "ID of the merchant.", + }, + gstTreatment: { + type: "string", + label: "GST Treatment", + description: "Choose whether the contact is GST registered/unregistered/consumer/overseas.", + options: GST_TREATMENT_OPTIONS, + }, + gstNo: { + type: "string", + label: "GST No", + description: "15 digit GST identification number of the customer.", + }, + isInclusiveTax: { + type: "boolean", + label: "Is Inclusive Tax", + description: "Used to specify whether the line item rates are inclusive or exclusive of tax.", + }, + lineItems: { + type: "string[]", + label: "Line Items", + description: "A list of line items objects of a sales order. **Example: {\"item_order\": \"1352827000000156060\", \"name\": \"name\", \"description\": \"description\", \"quantity\": \"1\" }** [See the documentation](https://www.zoho.com/books/api/v3/sales-order/#create-a-sales-order) for further details.", + }, + notes: { + type: "string", + label: "Notes", + description: "Notes for this Sales Order.", + }, + terms: { + type: "string", + label: "Terms", + description: "The terms added below expressing gratitude or for conveying some information.", + }, + billingAddressId: { + type: "string", + label: "Billing Address Id", + description: "ID of the Billing Address", + }, + shippingAddressId: { + type: "string", + label: "Shipping Address Id", + description: "ID of the Shipping Address.", + }, + crmOwnerId: { + type: "string", + label: "CRM Owner Id", + }, + crmCustomReferenceId: { + type: "string", + label: "CRM Custom Reference Id", + }, + vatTreatment: { + type: "string", + label: "VAT Treatment", + description: "Enter vat treatment.", + }, + taxTreatment: { + type: "string", + label: "Tax Treatment", + description: "VAT treatment for the invoice.", + options: TAX_TREATMENT_OPTIONS, + }, + salesorderNumber: { + type: "string", + label: "Salesorder Number", + description: "Mandatory if auto number generation is disabled.", + }, + referenceNumber: { + type: "string", + label: "Reference Number", + description: "**For Customer Only** : If a contact is assigned to any particular user, that user can manage transactions for the contact", + }, + isUpdateCustomer: { + type: "boolean", + label: "Is Update Customer", + description: "Boolean to update billing address of customer.", + }, + discount: { + type: "string", + label: "Discount", + description: "Discount applied to the sales order. It can be either in % or in amount. e.g. 12.5% or 190.", + }, + exchangeRate: { + type: "string", + label: "Exchange Rate", + description: "Exchange rate of the currency.", + }, + salespersonName: { + type: "string", + label: "Salesperson Name", + description: "Name of the sales person.", + }, + notesDefault: { + type: "string", + label: "Notes Default", + description: "Default Notes for the Sales Order", + }, + termsDefault: { + type: "string", + label: "Terms Default", + description: "Default Terms of the Sales Order.", + }, + taxAuthorityName: { + type: "string", + label: "Tax Authority Name", + description: "Tax Authority's name.", + }, + taxExemptionCode: { + type: "string", + label: "Tax Exemption Code", + description: "Code of Tax Exemption that is applied.", + }, + avataxExemptNo: { + type: "string", + label: "Avatax Exempt No", + description: "Exemption certificate number of the customer.", + }, + avataxUseCode: { + type: "string", + label: "Avatax Use Code", + description: "Used to group like customers for exemption purposes. It is a custom value that links customers to a tax rule.", + }, + shippingCharge: { + type: "string", + label: "Shipping Charge", + description: "Shipping charges applied to the invoice.", + }, + adjustment: { + type: "string", + label: "Adjustment", + }, + deliveryMethod: { + type: "string", + label: "Delivery Method", + }, + isDiscountBeforeTax: { + type: "boolean", + label: "Is Discount Before Tax", + description: "Used to specify how the discount has to applied. Either before or after the calculation of tax.", + }, + discountType: { + type: "string", + label: "Discount Type", + description: "How the discount is specified. Allowed values are entity_level or item_level.", + options: DISCOUNT_TYPE_OPTIONS, + }, + adjustmentDescription: { + type: "string", + label: "Adjustment Description", + }, + pricebookId: { + type: "string", + label: "Pricebook Id", + }, + pdfTemplateId: { + type: "string", + label: "Template Id", + description: "ID of the PDF template.", + }, + documents: { + type: "string[]", + label: "Documents", + description: "A list of documents.", + }, + zcrmPotentialId: { + type: "string", + label: "ZCRM Potential Id", + }, + zcrmPotentialName: { + type: "string", + label: "ZCRM Potential Name", + }, + ignoreAutoNumberGeneration: { + type: "boolean", + label: "Ignore Auto Number Generation", + description: "Ignore auto sales order number generation for this sales order. This mandates the sales order number.", + default: false, + }, + canSendInMail: { + type: "string", + label: "Can Send In Mail", + description: "Can the file be sent in mail.", + }, + totalFiles: { + type: "string", + label: "Total Files", + description: "Total number of files.", + }, + doc: { + type: "string", + label: "doc", + description: "Document that is to be attached", + }, + customBody: { + type: "string", + label: "Custom Body", + }, + customSubject: { + type: "string", + label: "Custom Subject", + optional: true, + }, + send: { + type: "boolean", + label: "Send", + description: "Send the estimate to the contact person(s) associated with the estimate.", + }, + paymentTerms: { + type: "integer", + label: "Payment Terms", + description: "Payment terms in days e.g. 15, 30, 60. Invoice due date will be calculated based on this. Max-length [100]", + }, + paymentTermsLabel: { + type: "string", + label: "Payment Terms Label", + description: "Used to override the default payment terms label. Default value for 15 days is \"Net 15 Days\". Max-length [100]", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `${this.$auth.api_domain}/books/v3`; + }, + _headers(headers = {}) { + return { + ...headers, + Authorization: `Zoho-oauthtoken ${this.$auth.oauth_access_token}`, + }; + }, + _params(params = {}) { + return { + ...params, + organization_id: `${this.$auth.organization_id}`, + }; + }, + _makeRequest({ + $ = this, path, headers, params, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(headers), + params: this._params(params), + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contacts", + ...opts, + }); + }, + createCustomerPayment(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/customerpayments", + ...opts, + }); + }, + createEmployee(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/employees", + ...opts, + }); + }, + createEstimate(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/estimates", + ...opts, + }); + }, + createInvoice(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/invoices", + ...opts, + }); + }, + createItem(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/items", + ...opts, + }); + }, + createSalesorder(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/salesorders", + ...opts, + }); + }, + deleteContact({ + customerId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/contacts/${customerId}`, + ...opts, + }); + }, + getInvoice({ + invoiceId, ...opts + }) { + return this._makeRequest({ + path: `/invoices/${invoiceId}`, + ...opts, + }); + }, + getItem({ + itemId, ...opts + }) { + return this._makeRequest({ + path: `/items/${itemId}`, + ...opts, + }); + }, + listBankAccounts(opts = {}) { + return this._makeRequest({ + path: "/bankaccounts", + ...opts, + }); + }, + listContactPersons({ + customerId, ...opts + }) { + return this._makeRequest({ + path: `/contacts/${customerId}/contactpersons`, + ...opts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts", + ...opts, + }); + }, + listCurrencies(opts = {}) { + return this._makeRequest({ + path: "/settings/currencies", + ...opts, + }); + }, + listExpenses(opts = {}) { + return this._makeRequest({ + path: "/expenses", + ...opts, + }); + }, + listInvoices(opts = {}) { + return this._makeRequest({ + path: "/invoices", + ...opts, + }); + }, + listItems(opts = {}) { + return this._makeRequest({ + path: "/items", + ...opts, + }); + }, + listRecurringInvoices(opts = {}) { + return this._makeRequest({ + path: "/recurringinvoices", + ...opts, + }); + }, + listSalesorders(opts = {}) { + return this._makeRequest({ + path: "/salesorders", + ...opts, + }); + }, + listTaxAuthorities(opts = {}) { + return this._makeRequest({ + path: "/settings/taxauthorities", + ...opts, + }); + }, + listTaxes(opts = {}) { + return this._makeRequest({ + path: "/settings/taxes", + ...opts, + }); + }, + listTaxExemptions(opts = {}) { + return this._makeRequest({ + path: "/settings/taxexemptions", + ...opts, + }); + }, + listTemplates(opts = {}) { + return this._makeRequest({ + path: "/invoices/templates", + ...opts, + }); + }, + listTimeEntries(opts = {}) { + return this._makeRequest({ + path: "/projects/timeentries", + ...opts, + }); + }, + updateSalesorder({ + salesorderId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/salesorders/${salesorderId}`, + ...opts, + }); + }, + async *paginate({ + fn, params = {}, fieldName, ...opts + }) { + let hasMore = false; + let page = 0; + + do { + params.page = ++page; + const data = await fn({ + params, + ...opts, + }); + for (const d of data[fieldName]) { + yield d; + } + + hasMore = data[fieldName].length; + + } while (hasMore); }, }, }; diff --git a/components/zoho_bugtracker/README.md b/components/zoho_bugtracker/README.md new file mode 100644 index 0000000000000..2dbf822055034 --- /dev/null +++ b/components/zoho_bugtracker/README.md @@ -0,0 +1,23 @@ +# Overview + +The Zoho BugTracker API allows you to interact programmatically with Zoho BugTracker, a tool designed for tracking and fixing bugs in your software projects. With this API, you can create, read, update, and delete information related to bugs, projects, users, and more. On Pipedream, you can harness this API to automate workflows, sync data across apps, and optimize bug management processes. Whether it's triggering actions based on bug updates or collating bug reports for analysis, Pipedream’s serverless platform simplifies integrating Zoho BugTracker with hundreds of other apps for seamless automation. + +# Example Use Cases + +- **Automated Bug Tracking Alert System**: Send real-time notifications to a Slack channel whenever a new bug is reported or an existing bug is updated in Zoho BugTracker. This keeps your development team instantly informed about bugs that need attention. + +- **Sync Bugs with Project Management Tools**: Automatically create or update tasks in Trello or Asana when a bug is reported in Zoho BugTracker. This helps in syncing development and project management efforts, ensuring that the team is aligned and can track bug resolution progress. + +- **Daily Bug Report Digest**: Compile a daily summary of bugs reported, resolved, or escalated within Zoho BugTracker and send it via email using the SendGrid app. This provides stakeholders with a snapshot of the day's bug management activities. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_calendar/README.md b/components/zoho_calendar/README.md new file mode 100644 index 0000000000000..79965a6ee6b9f --- /dev/null +++ b/components/zoho_calendar/README.md @@ -0,0 +1,23 @@ +# Overview + +Zoho Calendar API allows for the integration and manipulation of calendar events, giving you the power to automate workflows involving scheduling, reminders, and event management right within Pipedream. With this API, you can create, read, update, or delete events, and also respond to invites. By leveraging Pipedream's capabilities, you can connect Zoho Calendar with various other services to streamline your calendar management and synchronize events with other apps, or trigger actions based on calendar events. + +# Example Use Cases + +- **Sync Zoho Calendar Events with Google Sheets**: Automate the export of your Zoho Calendar events into a Google Sheet for advanced data analysis and record-keeping. Every time a new event is added to your Zoho Calendar, Pipedream triggers a workflow that appends the event details to a Google Sheet. This is great for reporting and archival purposes. + +- **Create Slack Notifications for Upcoming Events**: Get timely reminders for upcoming events on your Zoho Calendar by setting up a workflow that sends notifications to a Slack channel. Whenever an event is about to start, Pipedream uses the Zoho Calendar API to fetch the event details and posts a message in Slack, ensuring you never miss an important meeting or appointment. + +- **Trigger Email Campaigns from Event Sign-Ups**: Kick-off email campaigns whenever someone signs up for an event on your Zoho Calendar. Pipedream can detect new event attendees and automatically trigger an email sequence via an email marketing platform, like Mailchimp, providing them with event details, reminders, or related promotional content. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_campaigns/README.md b/components/zoho_campaigns/README.md new file mode 100644 index 0000000000000..9e5990959e45c --- /dev/null +++ b/components/zoho_campaigns/README.md @@ -0,0 +1,23 @@ +# Overview + +The Zoho Campaigns API opens up a world of possibilities for email marketing automation within Pipedream, allowing you to manage contacts, campaigns, and reports programmatically. You can connect Zoho Campaigns with other apps to create workflows that automate various tasks, such as synchronizing subscriber lists, triggering campaigns based on specific actions or events, and analyzing campaign performance with custom metrics. + +# Example Use Cases + +- **Sync New Users to Zoho Campaigns**: Automatically add new users from a CRM like Salesforce to a Zoho Campaigns mailing list whenever a new lead is captured. This ensures your marketing efforts reach all potential customers. + +- **Trigger Email Campaigns after E-commerce Transactions**: Launch Zoho Campaigns email sequences when a customer completes a purchase in an e-commerce platform like Shopify. Use this workflow to send order confirmations, request reviews, or offer post-purchase discounts. + +- **Dynamic Campaign Reporting**: Compile campaign performance data from Zoho Campaigns and send it to a Google Sheets spreadsheet for advanced analysis and visualization. This workflow can help you make data-driven decisions to optimize your email marketing campaigns. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_campaigns/actions/add-contact-to-mailing-list/add-contact-to-mailing-list.mjs b/components/zoho_campaigns/actions/add-contact-to-mailing-list/add-contact-to-mailing-list.mjs index 04e2532c2dac0..9cec859c26f4a 100644 --- a/components/zoho_campaigns/actions/add-contact-to-mailing-list/add-contact-to-mailing-list.mjs +++ b/components/zoho_campaigns/actions/add-contact-to-mailing-list/add-contact-to-mailing-list.mjs @@ -4,7 +4,7 @@ export default { type: "action", key: "zoho_campaigns-add-contact-to-mailing-list", name: "Add Contact to Mailing List", - version: "0.0.1", + version: "0.0.2", description: "You can use this API to add contacts to your mailing lists. [See the documentation](https://www.zoho.com/campaigns/help/developers/contact-subscribe.html)", props: { app, diff --git a/components/zoho_campaigns/actions/create-campaign/create-campaign.mjs b/components/zoho_campaigns/actions/create-campaign/create-campaign.mjs index 89be361bcf62e..b5d3854616983 100644 --- a/components/zoho_campaigns/actions/create-campaign/create-campaign.mjs +++ b/components/zoho_campaigns/actions/create-campaign/create-campaign.mjs @@ -4,7 +4,7 @@ export default { type: "action", key: "zoho_campaigns-create-campaign", name: "Create Campaign", - version: "0.0.1", + version: "0.0.2", description: "You can create a campaign using this API. Using this API, you can set the campaign name, subject line, sender address; choose the intended mailing list.. [See the documentation](https://www.zoho.com/campaigns/help/developers/create-campaign.html)", props: { app, diff --git a/components/zoho_campaigns/package.json b/components/zoho_campaigns/package.json index b2dc507d0c4e8..9bb6a41a6c766 100644 --- a/components/zoho_campaigns/package.json +++ b/components/zoho_campaigns/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/zoho_campaigns", - "version": "0.1.0", + "version": "0.1.1", "description": "Pipedream Zoho Campaigns Components", "main": "dist/app/zoho_campaigns.app.mjs", "keywords": [ diff --git a/components/zoho_campaigns/sources/new-campaign/new-campaign.mjs b/components/zoho_campaigns/sources/new-campaign/new-campaign.mjs index 1ce14990d18e5..042101ad582c9 100644 --- a/components/zoho_campaigns/sources/new-campaign/new-campaign.mjs +++ b/components/zoho_campaigns/sources/new-campaign/new-campaign.mjs @@ -6,7 +6,7 @@ export default { key: "zoho_campaigns-new-campaign", name: "New Campaign", description: "Emit new event when a new campaign is created.", - version: "0.0.1", + version: "0.0.2", methods: { ...common.methods, _emitEvent(campaign) { diff --git a/components/zoho_campaigns/sources/new-contact/new-contact.mjs b/components/zoho_campaigns/sources/new-contact/new-contact.mjs index e9a99679dbe35..9020c30027055 100644 --- a/components/zoho_campaigns/sources/new-contact/new-contact.mjs +++ b/components/zoho_campaigns/sources/new-contact/new-contact.mjs @@ -5,7 +5,7 @@ export default { key: "zoho_campaigns-new-contact", name: "New Contact", description: "Emit new event when a new contact is created.", - version: "0.0.1", + version: "0.0.2", props: { ...common.props, mailingList: { diff --git a/components/zoho_campaigns/zoho_campaigns.app.mjs b/components/zoho_campaigns/zoho_campaigns.app.mjs index 32dc320a431e7..a14c967b92ca3 100644 --- a/components/zoho_campaigns/zoho_campaigns.app.mjs +++ b/components/zoho_campaigns/zoho_campaigns.app.mjs @@ -40,7 +40,7 @@ export default { return this.$auth.oauth_access_token; }, _getBaseUrl() { - return "https://campaigns.zoho.com/api/v1.1"; + return `https://campaigns.${this.$auth.base_api_url}/api/v1.1`; }, _getHeaders() { return { diff --git a/components/zoho_catalyst/README.md b/components/zoho_catalyst/README.md new file mode 100644 index 0000000000000..8ed755b331cc6 --- /dev/null +++ b/components/zoho_catalyst/README.md @@ -0,0 +1,23 @@ +# Overview + +Zoho Catalyst is a cloud-based backend for building and hosting serverless applications. With its API, you can create, read, update, and delete records in Catalyst Data Store, run Catalyst Functions, manage files in Catalyst File Store, and orchestrate various backend processes. Integrating Zoho Catalyst with Pipedream allows you to seamlessly connect these backend operations with other services and automate workflows. For example, you can trigger a function when you receive an email, process data from webhooks, or sync information between Zoho Catalyst and other platforms. + +# Example Use Cases + +- **Sync New Users to a Mailing List**: When new users sign up in a Zoho Catalyst app, you can automatically add their contact information to a mailing list in Mailchimp. This keeps your marketing efforts in sync and ensures that you're reaching out to the latest users of your app. + +- **Automate File Backup to Cloud Storage**: Whenever a new file is uploaded to Zoho Catalyst File Store, you can create a workflow that automatically backs up the file to Google Drive, Dropbox, or another cloud storage service. This redundancy ensures that you always have a backup of critical files. + +- **Process Payments and Update Inventory**: After a payment is processed via a platform like Stripe, you can use Pipedream to call Zoho Catalyst Functions that update inventory counts and generate a sales record in the Catalyst Data Store. This automation streamlines order fulfillment and inventory management. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_cliq/README.md b/components/zoho_cliq/README.md new file mode 100644 index 0000000000000..d437447d14773 --- /dev/null +++ b/components/zoho_cliq/README.md @@ -0,0 +1,23 @@ +# Overview + +The Zoho Cliq API allows you to automate and integrate your team communication within Zoho Cliq. With this API, you can create bots to send messages, manage channels and users, and streamline notifications to your team about events from other applications or services. Using Pipedream, you can harness these capabilities to create powerful workflows that trigger actions within Zoho Cliq or react to events happening in Cliq to perform tasks in other apps. + +# Example Use Cases + +- **Automate Welcome Messages for New Users**: When a new user joins your organization on Zoho Cliq, you can set up a Pipedream workflow that automatically sends a personalized welcome message or onboarding instructions from a bot. + +- **Sync Zoho Cliq Channels with External Tools**: Configure a workflow on Pipedream that listens to webhooks from apps like JIRA, GitHub, or Trello. Whenever a new issue, commit, or card is created, the workflow can post updates to a specific Zoho Cliq channel to keep your team in the loop. + +- **Create Daily Summary Reports**: Build a Pipedream workflow that gathers data from various sources like sales figures from Zoho CRM, support ticket status from Zendesk, or website analytics from Google Analytics. Then, compile a daily summary report and post it in a Zoho Cliq channel every morning. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_commerce/README.md b/components/zoho_commerce/README.md new file mode 100644 index 0000000000000..f38f8ee4e84fd --- /dev/null +++ b/components/zoho_commerce/README.md @@ -0,0 +1,23 @@ +# Overview + +The Zoho Commerce API enables you to interact programmatically with Zoho's e-commerce platform, allowing for the automation of various online store operations. From managing products, orders, and customer data to generating reports, this API opens up possibilities for syncing your e-commerce data with other business tools, setting up automated marketing campaigns based on customer behavior, or even creating custom analytics dashboards to monitor your store's performance. + +# Example Use Cases + +- **Automate Order Fulfillment**: When a new order comes in via Zoho Commerce, use Pipedream to trigger a workflow that sends the order details to a fulfillment service like ShipStation. The workflow could then update the order status in Zoho Commerce and notify the customer of the shipment progress. + +- **Sync Customer Data with CRM**: Whenever a new customer signs up or updates their profile on your Zoho Commerce store, automatically sync their details to a CRM like Salesforce. This keeps your sales team informed and ensures marketing efforts are based on the latest customer information. + +- **Dynamic Inventory Management**: Set up a Pipedream workflow to monitor inventory levels in Zoho Commerce. If a product's stock falls below a certain threshold, the workflow can automatically reorder stock from suppliers and send alerts to your management team. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_creator/README.md b/components/zoho_creator/README.md index d6bd857524d64..032e887c53d23 100644 --- a/components/zoho_creator/README.md +++ b/components/zoho_creator/README.md @@ -1,22 +1,23 @@ # Overview -The Zoho Creator API lets you build powerful applications and custom components -quickly, easily, and securely. It's a platform designed to let you quickly -develop custom components and applications, without having to worry about -server-side code or hosting. You can use it to create your own integrated -applications and custom components, and to securely incorporate them into your -existing business applications. - -Using the Zoho Creator API, you can create: - -- Custom business applications, such as helpdesks, invoicing, or asset - management -- Custom single-page web apps -- AutonDBS integration -- Business rules and trigger-based automation -- Data-driven user interfaces -- Reporting and data visualizations -- Custom integrations and plugins -- Custom API endpoints -- Third-party API integration -- Desktop and mobile app support +Zoho Creator is a low-code platform that enables the building of custom applications tailored to business needs without extensive coding knowledge. Via its API, you can automate processes, manage data, and integrate with other services. Pipedream amplifies this capability by offering a serverless platform where events from various apps trigger workflows. You can use Zoho Creator’s API on Pipedream to create, read, update, and delete records, automate data flows, and link your custom apps with a myriad of other services for efficient, automated workflows. + +# Example Use Cases + +- **Lead Capture Automation**: When a new lead is captured in Zoho Creator, trigger a Pipedream workflow to add the lead to a CRM like Salesforce, send a personalized welcome email through SendGrid, and notify the sales team on Slack. This workflow ensures timely engagement with potential customers and streamlines the lead nurture process. + +- **Inventory Management**: Set up a workflow that monitors inventory levels in Zoho Creator. When stock for a particular item falls below a predefined threshold, Pipedream can automatically reorder the item from a supplier through an API like Supplier's API, update the inventory in Zoho Creator, and notify the inventory manager via an email or a message on Microsoft Teams. This keeps stock levels optimal with minimal manual intervention. + +- **Customer Support Ticket Routing**: Whenever a new support ticket is created in Zoho Creator, use Pipedream to analyze the ticket content using a Natural Language Processing (NLP) service like Google Cloud Natural Language API. Based on the analysis, categorize and assign the ticket to the right department, update the ticket with the category in Zoho Creator, and post ticket details to a department-specific Trello board or channel in Teams. This ensures that tickets are addressed by the most suitable team, improving response times and customer satisfaction. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_creator/package.json b/components/zoho_creator/package.json index 92d85381d2b31..390180ff57cd1 100644 --- a/components/zoho_creator/package.json +++ b/components/zoho_creator/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/zoho_creator", - "version": "0.3.6", + "version": "0.3.7", "description": "Pipedream Zoho_creator Components", "main": "zoho_creator.app.mjs", "keywords": [ diff --git a/components/zoho_creator/sources/common.mjs b/components/zoho_creator/sources/common.mjs index fb8ea40e2aac6..0921c7f7bcd41 100644 --- a/components/zoho_creator/sources/common.mjs +++ b/components/zoho_creator/sources/common.mjs @@ -21,6 +21,20 @@ export default { ], }, }, + hooks: { + async deploy() { + const { data } = await this.zohoCreator.getRecords({ + appLinkName: this.appLinkName, + reportLinkName: this.reportLinkName, + params: { + limit: 1, + }, + }); + if (data?.length) { + this.validateRecord(data[0]); + } + }, + }, methods: { getLastTimestamp() { return this.db.get(constants.LAST_TIMESTAMP); diff --git a/components/zoho_creator/sources/new-or-updated-record/new-or-updated-record.mjs b/components/zoho_creator/sources/new-or-updated-record/new-or-updated-record.mjs index 6618b7d03a1fe..fb4f37cf44dc3 100644 --- a/components/zoho_creator/sources/new-or-updated-record/new-or-updated-record.mjs +++ b/components/zoho_creator/sources/new-or-updated-record/new-or-updated-record.mjs @@ -1,6 +1,7 @@ import common from "../common.mjs"; import utils from "../../common/utils.mjs"; import constants from "../../constants.mjs"; +import { ConfigurationError } from "@pipedream/platform"; const { zohoCreator } = common.props; const { toSingleLineString } = utils; @@ -18,7 +19,7 @@ export default { `), type: "source", name: "New or Updated Record", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", props: { ...common.props, @@ -41,16 +42,16 @@ export default { methods: { ...common.methods, getMetadata(record) { - const id = JSON.stringify(record); + const ts = Date.parse(record[constants.MODIFIED_TIME_FIELD]); return { - id, - summary: id, - ts: Date.parse(record[constants.MODIFIED_TIME_FIELD]), + id: `${record.ID}-${ts}`, + summary: JSON.stringify(record), + ts, }; }, validateRecord(record) { if (!record[constants.MODIFIED_TIME_FIELD]) { - throw new Error("Record is missing the \"Modified Time\" field. Add the \"Modified Time\" Grouping field in the Zoho Creator record properties for the Report."); + throw new ConfigurationError("Record is missing the \"Modified Time\" field. Add the \"Modified Time\" Grouping field in the Zoho Creator record properties for the Report."); } }, processEvent(record) { diff --git a/components/zoho_creator/sources/new-record/new-record.mjs b/components/zoho_creator/sources/new-record/new-record.mjs index 2023eb6966fb3..1391ef6bd163e 100644 --- a/components/zoho_creator/sources/new-record/new-record.mjs +++ b/components/zoho_creator/sources/new-record/new-record.mjs @@ -1,6 +1,7 @@ import common from "../common.mjs"; import utils from "../../common/utils.mjs"; import constants from "../../constants.mjs"; +import { ConfigurationError } from "@pipedream/platform"; const { zohoCreator } = common.props; const { toSingleLineString } = utils; @@ -18,7 +19,7 @@ export default { `), type: "source", name: "New Record", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", props: { ...common.props, @@ -49,7 +50,7 @@ export default { }, validateRecord(record) { if (!record[constants.ADDED_TIME_FIELD]) { - throw new Error("Record is missing the \"Added Time\" field. Add the \"Added Time\" Grouping field in the Zoho Creator record properties for the Report."); + throw new ConfigurationError("Record is missing the \"Added Time\" field. Add the \"Added Time\" Grouping field in the Zoho Creator record properties for the Report."); } }, processEvent(record) { diff --git a/components/zoho_creator/zoho_creator.app.mjs b/components/zoho_creator/zoho_creator.app.mjs index b4697e438aa32..62167af77c237 100644 --- a/components/zoho_creator/zoho_creator.app.mjs +++ b/components/zoho_creator/zoho_creator.app.mjs @@ -36,8 +36,12 @@ export default { }, }, methods: { - getAccount() { - return this.$auth.oauth_uid; + async getAccount(appLinkName) { + const { applications = [] } = await this.getApplications(); + const { workspace_name: account } = applications.find( + ({ link_name: link }) => link === appLinkName, + ); + return account; }, isRetriableStatusCode(statusCode) { return constants.RETRIABLE_STATUS_CODE.includes(statusCode); @@ -114,7 +118,7 @@ export default { }) { return this.makeRequest({ $, - path: `/${this.getAccount()}/${appLinkName}/reports`, + path: `/${await this.getAccount(appLinkName)}/${appLinkName}/reports`, params, }); }, @@ -123,7 +127,7 @@ export default { }) { return this.makeRequest({ $, - path: `/${this.getAccount()}/${appLinkName}/report/${reportLinkName}`, + path: `/${await this.getAccount(appLinkName)}/${appLinkName}/report/${reportLinkName}`, params, }); }, @@ -194,8 +198,8 @@ export default { addedTime, modifiedTime, }) { return [ - addedTime && `${constants.ADDED_TIME_FIELD} > '${addedTime}'`, - modifiedTime && `${constants.MODIFIED_TIME_FIELD} > '${modifiedTime}'`, + addedTime && `${constants.ADDED_TIME_FIELD} >= '${addedTime}'`, + modifiedTime && `${constants.MODIFIED_TIME_FIELD} >= '${modifiedTime}'`, ].reduce( (reduction, criteria) => (criteria && reduction.concat(criteria) || reduction), diff --git a/components/zoho_crm/README.md b/components/zoho_crm/README.md index 844012dbe65a5..6e50025d77735 100644 --- a/components/zoho_crm/README.md +++ b/components/zoho_crm/README.md @@ -1,35 +1,8 @@ # Overview -Zoho CRM API is a powerful and versatile tool to create business applications. -It provides an easy to use platform for developers to create customer-facing -applications for businesses of all sizes. With the Zoho CRM API, developers can -create applications for sales, marketing, customer support, and even financial -data management. +The Zoho CRM API enables the manipulation and retrieval of data within Zoho CRM, a platform for managing your sales, marketing, support, and inventory in a single system. Leveraging this on Pipedream, you can automate tasks like syncing contacts, updating lead statuses, or creating custom CRM operations that trigger actions in other apps. Pipedream's serverless platform allows for real-time data processing, transforming, and orchestrating workflows that respond to events in Zoho CRM with minimal latency. -The Zoho CRM API provides access to a comprehensive set of features that allow -developers to build efficient, reliable and intuitive business solutions. These -features include: - -- Customer Relationship Management (CRM) -- Automation Tools -- Reporting and Analytics -- Customization and Deployment - -Here are some examples of what developers can build using the Zoho CRM API: - -- Automate customer interactions and manage customer relationship workflows -- Send targeted emails based on customer segmentation or customer behavior -- Leverage the reporting and analytics capabilities of the Zoho CRM to gain - insights into customer data -- Create dynamic dashboards and reports to better understand customer - engagement and performance -- Develop customer relationship management apps to capture customer information - and interact with customers -- Integrate the Zoho CRM with external applications and services to build - comprehensive customer-facing applications. - - -## Getting Started +# Getting Started 1. First, sign up for Pipedream at [https://pipedream.com](https://pipedream.com). 2. [Create a new workflow](https://pipedream.com/new). @@ -38,3 +11,23 @@ Here are some examples of what developers can build using the Zoho CRM API: 5. Once you've added a step, press the **Connect Zoho CRM** button near the top. If this is your first time authorizing Pipedream's access to your Zoho CRM account, you'll be prompted to accept that access, and Pipedream will store the authorization grant to enable the workflow to access the API. If you've already linked a Zoho CRM account via Pipedream, pressing **Connect Zoho CRM** will list any existing accounts you've linked. Once you've connected your account, you can run your workflow and fetch data from the API. You can change any of the code associated with this step, changing the API endpoint you'd like to retrieve data from, or modifying the results in any way. + +# Example Use Cases + +- **Lead Qualification and Assignment Workflow**: When a new lead is entered into Zoho CRM, Pipedream can automatically qualify the lead based on predefined criteria (like job title or company size) and assign it to the appropriate sales representative. This can be extended by notifying the assigned rep via Slack or email, ensuring prompt follow-ups. + +- **Contact Sync Across Platforms**: Keep contact information in sync between Zoho CRM and other platforms such as Google Contacts, Mailchimp, or Shopify. Whenever a contact is updated in Zoho CRM, Pipedream can propagate those changes to the other platforms in real-time, maintaining consistency and accuracy across your sales and marketing tools. + +- **Automated Support Ticket Creation**: Integrate Zoho CRM with a customer support platform like Zendesk. When a deal reaches a certain stage in Zoho CRM indicating potential issues, Pipedream can automatically generate a support ticket in Zendesk with the relevant deal and customer information, streamlining the support process and ensuring issues are addressed promptly. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_crm/actions/common/common-objects.mjs b/components/zoho_crm/actions/common/common-objects.mjs index 4079cd9b9cc98..b754d19e2f7a4 100644 --- a/components/zoho_crm/actions/common/common-objects.mjs +++ b/components/zoho_crm/actions/common/common-objects.mjs @@ -28,23 +28,33 @@ export default { && !field.system_mandatory && field.operation_type[`api_${type}`] && ![ - "picklist", "lookup", "ownerlookup", "profileimage", ].includes(field.data_type)); }, - getType(dataType) { + getType({ + data_type: dataType, json_type: jsonType, + }) { + let type; switch (dataType) { case "boolean": - return "boolean"; + type = "boolean"; + break; case "integer": - return "integer"; + type = "integer"; + break; case "bigint": - return "integer"; + type = "integer"; + break; default: - return "string"; + type = "string"; + break; + }; + if (jsonType === "jsonarray") { + return `${type}[]`; } + return type; }, getRequiredProps(moduleType, type = "create") { let props = {}; @@ -138,12 +148,40 @@ export default { const { fields } = await this.listFields(moduleType); for (const field of this.filterFields(fields, type)) { props[field.api_name] = { - type: this.getType(field.data_type), + type: this.getType(field), label: field.display_label, optional: true, }; + if (field.pick_list_values?.length) { + props[field.api_name].options = field.pick_list_values.map(({ + display_value: label, actual_value: value, + }) => ({ + value, + label, + })); + } } return props; }, + parseFields(fields) { + if (!fields) { + return; + } + for (const [ + key, + value, + ] of Object.entries(fields)) { + try { + if (typeof value === "string") { + fields[key] = typeof JSON.parse(value) === "number" + ? value + : JSON.parse(value); + } + } catch { + fields[key] = value; + } + } + return fields; + }, }, }; diff --git a/components/zoho_crm/actions/create-object/create-object.mjs b/components/zoho_crm/actions/create-object/create-object.mjs index ec29e8c3f4c38..bcc73a73b04f6 100644 --- a/components/zoho_crm/actions/create-object/create-object.mjs +++ b/components/zoho_crm/actions/create-object/create-object.mjs @@ -1,11 +1,12 @@ import common from "../common/common-objects.mjs"; +import { ConfigurationError } from "@pipedream/platform"; export default { ...common, key: "zoho_crm-create-object", name: "Create Object", description: "Create a new object/module entry. [See the documentation](https://www.zoho.com/crm/developer/docs/api/v2/insert-records.html)", - version: "0.3.2", + version: "0.3.3", type: "action", async additionalProps() { const requiredProps = this.getRequiredProps(this.moduleType); @@ -53,12 +54,18 @@ export default { }); const objectData = { data: [ - object, + this.parseFields(object), ], }; const res = await zohoCrm.createObject(moduleType, objectData, $); - if (res.data[0].details.id) { + + if (res.data[0].code === "SUCCESS") { $.export("$summary", `Successfully created new object with ID ${res.data[0].details.id}.`); + } else { + if (res.data[0].code === "INVALID_DATA") { + throw new ConfigurationError(`Error: Invalid data for field '${res.data[0].details.api_name}'. Expected data type: ${res.data[0].details.expected_data_type}`); + } + throw new ConfigurationError(res.data[0].message); } return res; }, diff --git a/components/zoho_crm/actions/update-object/update-object.mjs b/components/zoho_crm/actions/update-object/update-object.mjs index 67200f81ce3eb..e46dc7ad0c05b 100644 --- a/components/zoho_crm/actions/update-object/update-object.mjs +++ b/components/zoho_crm/actions/update-object/update-object.mjs @@ -1,12 +1,13 @@ import zohoCrm from "../../zoho_crm.app.mjs"; import common from "../common/common-objects.mjs"; +import { ConfigurationError } from "@pipedream/platform"; export default { ...common, key: "zoho_crm-update-object", name: "Update Object", description: "Updates existing entities in the module. [See the documentation](https://www.zoho.com/crm/developer/docs/api/v2/update-records.html)", - version: "0.3.2", + version: "0.3.3", type: "action", props: { ...common.props, @@ -58,13 +59,21 @@ export default { ...otherProps, }); - const response = await zohoCrm.updateObject( + const res = await zohoCrm.updateObject( moduleType, recordId, - object, + this.parseFields(object), $, ); - $.export("$summary", `Successfully updated object with ID ${recordId}.`); - return response; + + if (res.data[0].code === "SUCCESS") { + $.export("$summary", `Successfully updated object with ID ${recordId}.`); + } else { + if (res.data[0].code === "INVALID_DATA") { + throw new ConfigurationError(`Error: Invalid data for field '${res.data[0].details.api_name}'. Expected data type: ${res.data[0].details.expected_data_type}`); + } + throw new ConfigurationError(res.data[0].message); + } + return res; }, }; diff --git a/components/zoho_crm/package.json b/components/zoho_crm/package.json index 24b380430bc5d..5354b3dc2d8c2 100644 --- a/components/zoho_crm/package.json +++ b/components/zoho_crm/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/zoho_crm", - "version": "0.5.2", + "version": "0.5.3", "description": "Pipedream Zoho CRM Components", "main": "zoho_crm.app.mjs", "keywords": [ @@ -10,7 +10,7 @@ "homepage": "https://pipedream.com/apps/zoho_crm", "author": "Pipedream (https://pipedream.com/)", "dependencies": { - "@pipedream/platform": "^1.2.0", + "@pipedream/platform": "^3.0.3", "crypto": "^1.0.1", "lodash.sortby": "^4.7.0" }, diff --git a/components/zoho_desk/README.md b/components/zoho_desk/README.md index ab6dafa775fff..9a258ebfaa76e 100644 --- a/components/zoho_desk/README.md +++ b/components/zoho_desk/README.md @@ -1,18 +1,26 @@ # Overview -Zoho Desk's API enables you to build automated workflows and increase the -efficiency of your customer service operations. With the API you can build -integrations, create custom solutions for customer service, or quickly create a -tool for custom reporting. Here are some examples of what you can build using -the Zoho Desk API: - -- Create custom ticket management flows. -- Create custom customer account management systems. -- Integrate with popular third-party messaging and collaboration tools. -- Automate customer onboarding processes. -- Generate custom reports and analytics. -- Automate internal processes and escalate tasks. -- Customize the customer service dashboard. -- Create custom customer analytics dashboard to measure customer satisfaction. -- Integrate Zoho Desk with other business systems. -- Create custom workflow management systems. +Zoho Desk API empowers you to streamline and automate customer service processes. With Pipedream, you can tap into Zoho Desk's capabilities to manage tickets, fetch customer info, and coordinate your support team's efforts. Imagine syncing support tickets to your internal systems, triggering alerts for high-priority issues, or even analyzing support trends over time. Pipedream's serverless platform lets you connect Zoho Desk to hundreds of other apps with minimal hassle, transforming how you attend to customer needs. + +# Example Use Cases + +**Automated Ticket Prioritization Workflow** +Automatically prioritize incoming Zoho Desk tickets based on keywords, customer tier, or issue severity. Use Pipedream to route high-priority tickets to senior support agents and tag them for immediate attention, while grouping lower priority issues for batch processing. This can ensure a quicker response time for critical issues. + +**Customer Satisfaction Follow-up** +After a ticket is marked as resolved in Zoho Desk, trigger an automated follow-up email or survey to gauge customer satisfaction. Pipedream can integrate Zoho Desk with email platforms like SendGrid or survey tools like Typeform to help maintain high levels of customer service and gather feedback for continuous improvement. + +**Support Dashboard Sync** +Sync Zoho Desk data to a business intelligence tool like Google Data Studio to visualize customer support performance. With Pipedream, you can set up a workflow that periodically extracts ticket metrics and pushes them to your dashboard, providing your team with real-time insights into support operations and customer satisfaction trends. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_desk/actions/add-ticket-attachment/add-ticket-attachment.mjs b/components/zoho_desk/actions/add-ticket-attachment/add-ticket-attachment.mjs index 0ed4839f9dadc..b20ee80d44a3f 100644 --- a/components/zoho_desk/actions/add-ticket-attachment/add-ticket-attachment.mjs +++ b/components/zoho_desk/actions/add-ticket-attachment/add-ticket-attachment.mjs @@ -6,7 +6,7 @@ export default { name: "Add Ticket Attachment", description: "Attaches a file to a ticket. [See the docs here](https://desk.zoho.com/DeskAPIDocument#TicketAttachments#TicketAttachments_CreateTicketattachment)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoDesk, orgId: { diff --git a/components/zoho_desk/actions/add-ticket-comment/add-ticket-comment.mjs b/components/zoho_desk/actions/add-ticket-comment/add-ticket-comment.mjs index a2e7c9262d21f..dcc8d294bd657 100644 --- a/components/zoho_desk/actions/add-ticket-comment/add-ticket-comment.mjs +++ b/components/zoho_desk/actions/add-ticket-comment/add-ticket-comment.mjs @@ -5,7 +5,7 @@ export default { name: "Add Ticket Comment", description: "Adds a comment to a ticket. [See the docs here](https://desk.zoho.com/DeskAPIDocument#TicketsComments#TicketsComments_Createticketcomment)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoDesk, orgId: { diff --git a/components/zoho_desk/actions/create-account/create-account.mjs b/components/zoho_desk/actions/create-account/create-account.mjs index 26c5667d66150..fdf4577482eab 100644 --- a/components/zoho_desk/actions/create-account/create-account.mjs +++ b/components/zoho_desk/actions/create-account/create-account.mjs @@ -5,7 +5,7 @@ export default { name: "Create Account", description: "Creates an account in your help desk portal. [See the docs here](https://desk.zoho.com/DeskAPIDocument#Accounts#Accounts_CreateAccount)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoDesk, orgId: { diff --git a/components/zoho_desk/actions/create-contact/create-contact.mjs b/components/zoho_desk/actions/create-contact/create-contact.mjs index e13a0ac6b9d1d..1da75403d1ee6 100644 --- a/components/zoho_desk/actions/create-contact/create-contact.mjs +++ b/components/zoho_desk/actions/create-contact/create-contact.mjs @@ -5,7 +5,7 @@ export default { name: "Create Contact", description: "Creates a contact in your help desk portal. [See the docs here](https://desk.zoho.com/DeskAPIDocument#Contacts#Contacts_CreateContact)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoDesk, orgId: { diff --git a/components/zoho_desk/actions/create-ticket/create-ticket.mjs b/components/zoho_desk/actions/create-ticket/create-ticket.mjs index 9d10314fdb40a..6c324e1e0699b 100644 --- a/components/zoho_desk/actions/create-ticket/create-ticket.mjs +++ b/components/zoho_desk/actions/create-ticket/create-ticket.mjs @@ -5,7 +5,7 @@ export default { name: "Create Ticket", description: "Creates a ticket in your helpdesk. [See the docs here](https://desk.zoho.com/DeskAPIDocument#Tickets#Tickets_Createaticket)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoDesk, orgId: { diff --git a/components/zoho_desk/actions/find-contact/find-contact.mjs b/components/zoho_desk/actions/find-contact/find-contact.mjs index f01d8af241ce1..6f7c96da39277 100644 --- a/components/zoho_desk/actions/find-contact/find-contact.mjs +++ b/components/zoho_desk/actions/find-contact/find-contact.mjs @@ -5,7 +5,7 @@ export default { name: "Find Contact", description: "Searches for contacts in your help desk portal. [See the docs here](https://desk.zoho.com/DeskAPIDocument#Search#Search_SearchContacts)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoDesk, orgId: { diff --git a/components/zoho_desk/actions/find-or-create-contact/find-or-create-contact.mjs b/components/zoho_desk/actions/find-or-create-contact/find-or-create-contact.mjs index d0e6f5815c735..8e10ac83b48cd 100644 --- a/components/zoho_desk/actions/find-or-create-contact/find-or-create-contact.mjs +++ b/components/zoho_desk/actions/find-or-create-contact/find-or-create-contact.mjs @@ -5,7 +5,7 @@ export default { name: "Find or Create Contact", description: "Finds or create a contact. [See the docs here](https://desk.zoho.com/DeskAPIDocument#Contacts#Contacts_CreateContact)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoDesk, orgId: { diff --git a/components/zoho_desk/actions/search-ticket/search-ticket.mjs b/components/zoho_desk/actions/search-ticket/search-ticket.mjs index 83940bdecd619..a49cf2877db13 100644 --- a/components/zoho_desk/actions/search-ticket/search-ticket.mjs +++ b/components/zoho_desk/actions/search-ticket/search-ticket.mjs @@ -5,7 +5,7 @@ export default { name: "Search Ticket", description: "Searches for tickets in your help desk. [See the docs here](https://desk.zoho.com/DeskAPIDocument#Search_TicketsSearchAPI)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoDesk, orgId: { diff --git a/components/zoho_desk/actions/send-email-reply/send-email-reply.mjs b/components/zoho_desk/actions/send-email-reply/send-email-reply.mjs index 96f568eeddbc5..b9a99c00b022b 100644 --- a/components/zoho_desk/actions/send-email-reply/send-email-reply.mjs +++ b/components/zoho_desk/actions/send-email-reply/send-email-reply.mjs @@ -5,7 +5,7 @@ export default { name: "Send E-Mail Reply", description: "Sends an email reply. [See the docs here](https://desk.zoho.com/DeskAPIDocument#Threads#Threads_SendEmailReply)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoDesk, orgId: { diff --git a/components/zoho_desk/actions/update-contact/update-contact.mjs b/components/zoho_desk/actions/update-contact/update-contact.mjs index 2e868ccd0d740..1e08be6bef34c 100644 --- a/components/zoho_desk/actions/update-contact/update-contact.mjs +++ b/components/zoho_desk/actions/update-contact/update-contact.mjs @@ -5,7 +5,7 @@ export default { name: "Update Contact", description: "Updates details of an existing contact. [See the docs here](https://desk.zoho.com/DeskAPIDocument#Contacts#Contacts_Updateacontact)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoDesk, orgId: { diff --git a/components/zoho_desk/actions/update-ticket/update-ticket.mjs b/components/zoho_desk/actions/update-ticket/update-ticket.mjs index 8e2c6dc1d365e..4c3ae72b89c49 100644 --- a/components/zoho_desk/actions/update-ticket/update-ticket.mjs +++ b/components/zoho_desk/actions/update-ticket/update-ticket.mjs @@ -5,7 +5,7 @@ export default { name: "Update Ticket", description: "Updates an existing ticket. [See the docs here](https://desk.zoho.com/DeskAPIDocument#Tickets#Tickets_Updateaticket)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoDesk, orgId: { diff --git a/components/zoho_desk/common/utils.mjs b/components/zoho_desk/common/utils.mjs index 6bb7d096b2464..21b502910e075 100644 --- a/components/zoho_desk/common/utils.mjs +++ b/components/zoho_desk/common/utils.mjs @@ -18,7 +18,7 @@ function buildFormData(formData, data, parentKey) { buildFormData(formData, data[key], parentKey && `${parentKey}[${key}]` || key); }); - } else if (data && constants.FILE_PROP_NAMES.some((prop) => prop.includes(parentKey))) { + } else if (data && constants.FILE_PROP_NAMES.some((prop) => parentKey.includes(prop))) { formData.append(parentKey, createReadStream(data)); } else if (data) { diff --git a/components/zoho_desk/package.json b/components/zoho_desk/package.json index 243a5b28f594a..9f0e0ff2849d1 100644 --- a/components/zoho_desk/package.json +++ b/components/zoho_desk/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/zoho_desk", - "version": "0.0.3", + "version": "0.0.4", "description": "Pipedream Zoho_desk Components", "main": "zoho_desk.app.mjs", "keywords": [ @@ -10,7 +10,10 @@ "homepage": "https://pipedream.com/apps/zoho_desk", "author": "Pipedream (https://pipedream.com/)", "dependencies": { - "@pipedream/platform": "^1.2.0" + "@pipedream/platform": "^1.6.2", + "async-retry": "^1.3.3", + "form-data": "^4.0.0", + "fs": "^0.0.1-security" }, "publishConfig": { "access": "public" diff --git a/components/zoho_desk/sources/changed-ticket-status/changed-ticket-status.mjs b/components/zoho_desk/sources/changed-ticket-status/changed-ticket-status.mjs index 5af1a405b78eb..be41226f0d2e0 100644 --- a/components/zoho_desk/sources/changed-ticket-status/changed-ticket-status.mjs +++ b/components/zoho_desk/sources/changed-ticket-status/changed-ticket-status.mjs @@ -6,7 +6,7 @@ export default { name: "New Ticket Status Change", description: "Emit new event when a status ticket is changed. [See the docs here](https://desk.zoho.com/DeskAPIDocument#Tickets#Tickets_Listalltickets)", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", props: { ...common.props, diff --git a/components/zoho_desk/sources/new-account/new-account.mjs b/components/zoho_desk/sources/new-account/new-account.mjs index e82f1b0ac733c..bf768c6f0afce 100644 --- a/components/zoho_desk/sources/new-account/new-account.mjs +++ b/components/zoho_desk/sources/new-account/new-account.mjs @@ -6,7 +6,7 @@ export default { name: "New Account", description: "Emit new event when a new account is created. [See the docs here](https://desk.zoho.com/DeskAPIDocument#Accounts#Accounts_Listaccounts)", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", props: { ...common.props, diff --git a/components/zoho_desk/sources/new-agent/new-agent.mjs b/components/zoho_desk/sources/new-agent/new-agent.mjs index 3469550913f79..ed02e33ccd8eb 100644 --- a/components/zoho_desk/sources/new-agent/new-agent.mjs +++ b/components/zoho_desk/sources/new-agent/new-agent.mjs @@ -6,7 +6,7 @@ export default { name: "New Agent", description: "Emit new event when a new agent is created. [See the docs here](https://desk.zoho.com/DeskAPIDocument#Agents#Agents_Listagents)", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", props: { ...common.props, diff --git a/components/zoho_desk/sources/new-contact/new-contact.mjs b/components/zoho_desk/sources/new-contact/new-contact.mjs index ec2bcd5fdc2cd..9a9a5f79e22c4 100644 --- a/components/zoho_desk/sources/new-contact/new-contact.mjs +++ b/components/zoho_desk/sources/new-contact/new-contact.mjs @@ -6,7 +6,7 @@ export default { name: "New Contact", description: "Emit new event when a new contact is created. [See the docs here](https://desk.zoho.com/DeskAPIDocument#Contacts#Contacts_Listcontacts)", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", props: { ...common.props, diff --git a/components/zoho_desk/sources/new-ticket-attachment/new-ticket-attachment.mjs b/components/zoho_desk/sources/new-ticket-attachment/new-ticket-attachment.mjs index 92d9235dd762f..36635f0a09f3c 100644 --- a/components/zoho_desk/sources/new-ticket-attachment/new-ticket-attachment.mjs +++ b/components/zoho_desk/sources/new-ticket-attachment/new-ticket-attachment.mjs @@ -6,7 +6,7 @@ export default { name: "New Ticket Attachment", description: "Emit new event when a new ticket attachment is created. [See the docs here](https://desk.zoho.com/DeskAPIDocument#TicketAttachments#TicketAttachments_Listticketattachments)", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", props: { ...common.props, diff --git a/components/zoho_desk/sources/new-ticket-comment/new-ticket-comment.mjs b/components/zoho_desk/sources/new-ticket-comment/new-ticket-comment.mjs index 775225b5ff18a..0e3958c132a0e 100644 --- a/components/zoho_desk/sources/new-ticket-comment/new-ticket-comment.mjs +++ b/components/zoho_desk/sources/new-ticket-comment/new-ticket-comment.mjs @@ -6,7 +6,7 @@ export default { name: "New Ticket Comment", description: "Emit new event when a new ticket comment is created. [See the docs here](https://desk.zoho.com/DeskAPIDocument#TicketsComments#TicketsComments_Listallticketcomments)", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", props: { ...common.props, diff --git a/components/zoho_desk/sources/new-ticket-message/new-ticket-message.mjs b/components/zoho_desk/sources/new-ticket-message/new-ticket-message.mjs index b9144dd803396..15b747c59ef8e 100644 --- a/components/zoho_desk/sources/new-ticket-message/new-ticket-message.mjs +++ b/components/zoho_desk/sources/new-ticket-message/new-ticket-message.mjs @@ -6,7 +6,7 @@ export default { name: "New Ticket Message", description: "Emit new event when a message ticket is created. [See the docs here](https://desk.zoho.com/DeskAPIDocument#Threads#Threads_Listallthreads)", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", props: { ...common.props, diff --git a/components/zoho_desk/sources/new-ticket/new-ticket.mjs b/components/zoho_desk/sources/new-ticket/new-ticket.mjs index c672422dd9ed9..79bf3211e2295 100644 --- a/components/zoho_desk/sources/new-ticket/new-ticket.mjs +++ b/components/zoho_desk/sources/new-ticket/new-ticket.mjs @@ -6,7 +6,7 @@ export default { name: "New Ticket", description: "Emit new event when a new ticket is created. [See the docs here](https://desk.zoho.com/DeskAPIDocument#Tickets#Tickets_Listalltickets)", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", props: { ...common.props, diff --git a/components/zoho_desk/sources/updated-ticket/updated-ticket.mjs b/components/zoho_desk/sources/updated-ticket/updated-ticket.mjs index 92a341f68a57b..d989a2ac4b5c2 100644 --- a/components/zoho_desk/sources/updated-ticket/updated-ticket.mjs +++ b/components/zoho_desk/sources/updated-ticket/updated-ticket.mjs @@ -6,7 +6,7 @@ export default { name: "New Updated Ticket", description: "Emit new event when a ticket is updated. [See the docs here](https://desk.zoho.com/DeskAPIDocument#Tickets#Tickets_Listalltickets)", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", props: { ...common.props, diff --git a/components/zoho_docs/README.md b/components/zoho_docs/README.md index ea2376cee46ca..5b63602e5bcaa 100644 --- a/components/zoho_docs/README.md +++ b/components/zoho_docs/README.md @@ -1,17 +1,23 @@ # Overview -Zoho Docs is an all-in-one platform that enables users to store,create, -collaborate, and share documents. By integrating the Zoho Docs API, developers -can build custom applications that enable teams to securely collaborate on -projects in real time. Here are a few examples of what you can create using the -Zoho Docs API: - -- Online document storage and sharing platform -- Real-time collaboration tools -- Project tracking and task management apps -- Version control systems -- Secure document scanning and signatures -- Workflows for managing documents -- Custom user authentication systems -- Automated document generation systems -- Automated document classification systems +The Zoho Docs API unlocks the power to seamlessly manage documents in the cloud by integrating with Pipedream. With Pipedream's serverless platform, you can automate document sharing, conversions, and syncing files with other apps or databases. By connecting Zoho Docs to other services, you can streamline workflows, simplify collaboration, and maintain document organization with minimal manual intervention. + +# Example Use Cases + +- **Automated Document Backups**: Create a workflow that triggers at regular intervals to back up important documents from Zoho Docs to a cloud storage service like Google Drive or Dropbox. This ensures your files are safely archived without manual effort. + +- **Team Collaboration Enhancer**: Set up an automation that watches for new files or updates in a Zoho Docs folder and notifies team members via Slack or sends an email through Gmail. Keep everyone in sync without the need to check for updates manually. + +- **CRM Sync**: Whenever a new contract or customer document is added to Zoho Docs, use Pipedream to add a corresponding record in a CRM like Salesforce or HubSpot. This maintains consistency across business platforms and saves time on data entry. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_docs/package.json b/components/zoho_docs/package.json new file mode 100644 index 0000000000000..9de063db24057 --- /dev/null +++ b/components/zoho_docs/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/zoho_docs", + "version": "0.6.0", + "description": "Pipedream zoho_docs Components", + "main": "zoho_docs.app.mjs", + "keywords": [ + "pipedream", + "zoho_docs" + ], + "homepage": "https://pipedream.com/apps/zoho_docs", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/zoho_expense/README.md b/components/zoho_expense/README.md new file mode 100644 index 0000000000000..d8569aa77d868 --- /dev/null +++ b/components/zoho_expense/README.md @@ -0,0 +1,23 @@ +# Overview + +The Zoho Expense API allows for streamlined management of expense reporting and tracking. With Pipedream, you can automate various tasks like submitting expenses, approving reports, or syncing expense data with other accounting tools. Pipedream's serverless platform enables you to create workflows that react to new expense submissions, scheduled report generation, and much more, without the hassle of managing infrastructure. + +# Example Use Cases + +- **Expense Approval Automation**: Create a workflow that triggers when a new expense report is submitted. Use Pipedream to automate the approval process based on custom criteria, such as expense limits or category checks. If approved, the workflow can send notifications to the finance team or update the report status in Zoho Expense. + +- **Receipt Integration with Cloud Storage**: Set up a Pipedream workflow that detects newly uploaded receipts in Zoho Expense and backs them up to a cloud storage service like Google Drive or Dropbox. This ensures that all receipts are stored redundantly and can be accessed from different platforms for accounting purposes. + +- **Expense Data Sync with Accounting Software**: Build a workflow on Pipedream that periodically fetches new expense entries from Zoho Expense and pushes them to accounting software like QuickBooks or Xero. This can help keep your books up to date automatically and reduce manual data entry errors. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_forms/README.md b/components/zoho_forms/README.md new file mode 100644 index 0000000000000..2fccbeb4f79bb --- /dev/null +++ b/components/zoho_forms/README.md @@ -0,0 +1,26 @@ +# Overview + +Zoho Forms API allows you to automate interactions with your forms and the data you collect. Leveraging Pipedream, you can harness this API to trigger workflows upon new form submissions, manipulate form entries, and sync data with other services. Pipedream’s serverless platform simplifies integrating Zoho Forms with hundreds of other apps, empowering you to create custom, scalable workflows without hosting or managing servers. + +# Example Use Cases + +- **Automate Lead Capture to CRM** + Capture leads using Zoho Forms and instantly add them to a CRM like Salesforce. When a new form submission occurs, the workflow triggers, extracting the submission data and creating a new lead record in Salesforce, ensuring that no lead falls through the cracks. + +- **Sync Form Data to Google Sheets** + Keep a real-time Google Sheets log of every Zoho Forms submission. Each time a form is submitted, Pipedream's workflow adds a new row to your designated Google Sheet. This is ideal for sharing submission data with team members who prefer working within spreadsheets. + +- **Form Submission Notifications via Slack** + Get instant notifications in a Slack channel whenever a form is submitted. This Pipedream workflow listens for new submissions on Zoho Forms and then posts a custom message with the submission details to your chosen Slack channel, keeping your team updated in real-time. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_inventory/README.md b/components/zoho_inventory/README.md index c2f3271354130..eeca450979816 100644 --- a/components/zoho_inventory/README.md +++ b/components/zoho_inventory/README.md @@ -1,27 +1,23 @@ # Overview -The Zoho Inventory API gives developers the ability to build innovative and -dynamic applications that enable businesses to manage the selling, buying, and -organizing of inventory. With this API, developers can tailor their -applications with the features to enable businesses to make the most of their -inventory. - -The Zoho Inventory API has many features, including: - -- Creation and management of vendors, customers, and items -- Ability to keep track of inventory stock in warehouses, sale orders, and - purchase orders -- Automation of sales, purchases, and warehousing operations -- Ability to manage taxes, currency exchange rates, ships, carriers, and more -- Access to real-time reports on sale and purchase orders - -With the Zoho Inventory API, you can create the following applications: - -- Customized e-commerce website for customers -- Inventory management system for warehouses -- Dynamic order processing and invoicing system -- Automated purchasing system for vendors -- Robust billing system for customers -- Mobile app for sales and inventory tracking -- Real-time reporting system for analyzing sales and purchases -- Comprehensive e-commerce platform that integrates with other apps +Zoho Inventory API on Pipedream opens the door to smart inventory management by allowing you to integrate with a plethora of apps, automate inventory tracking, and streamline order processing. You can sync your inventory levels across multiple sales channels, automatically update stock quantities, process sales orders, manage returns, and generate detailed reports, all in real-time. With Pipedream's serverless platform, crafting workflows becomes a breeze, letting you focus on scaling your business while reducing manual overhead. + +# Example Use Cases + +- **Automated Order Fulfillment**: When a new order is placed on your e-commerce platform, Pipedream triggers a workflow that creates a sales order in Zoho Inventory, allocates stock, and updates inventory levels. The process can be extended to send a notification to your logistics team or directly to a shipping service like ShipStation to initiate the delivery process. + +- **Real-time Stock Sync Across Channels**: Keep your inventory levels in sync across all sales channels, such as Shopify, Amazon, or eBay. Set up a Pipedream workflow that listens for stock level changes in Zoho Inventory and automatically updates the respective channel's inventory, ensuring you never oversell and always display the correct stock information to your customers. + +- **Automated Reordering Alerts**: Configure a workflow to monitor stock thresholds and automatically send purchase orders to suppliers when inventory runs low. This could be coupled with Slack or email notifications to keep the purchasing team informed, ensuring your best-selling items are always in stock. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_inventory/actions/create-contact/create-contact.mjs b/components/zoho_inventory/actions/create-contact/create-contact.mjs index ccd7683e48ea4..1bbc9beef9071 100644 --- a/components/zoho_inventory/actions/create-contact/create-contact.mjs +++ b/components/zoho_inventory/actions/create-contact/create-contact.mjs @@ -4,7 +4,7 @@ export default { key: "zoho_inventory-create-contact", name: "Create Contact", description: "Create a new contact in Zoho Inventory. [See the docs here](https://www.zoho.com/inventory/api/v1/contacts/#create-a-contact)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { zohoInventory, diff --git a/components/zoho_inventory/actions/create-order/create-order.mjs b/components/zoho_inventory/actions/create-order/create-order.mjs index 51e066b6781a3..2f47ddb0c6a3a 100644 --- a/components/zoho_inventory/actions/create-order/create-order.mjs +++ b/components/zoho_inventory/actions/create-order/create-order.mjs @@ -4,7 +4,7 @@ export default { key: "zoho_inventory-create-order", name: "Create Sales Order", description: "Create a new sales order in Zoho Inventory. [See the docs here](https://www.zoho.com/inventory/api/v1/salesorders/#create-a-sales-order)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { zohoInventory, diff --git a/components/zoho_inventory/package.json b/components/zoho_inventory/package.json index 992ffbdedcc33..31985f465cec4 100644 --- a/components/zoho_inventory/package.json +++ b/components/zoho_inventory/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/zoho_inventory", - "version": "0.0.3", + "version": "0.0.4", "description": "Pipedream Zoho Inventory Components", "main": "zoho_inventory.app.mjs", "keywords": [ diff --git a/components/zoho_inventory/sources/new-contact/new-contact.mjs b/components/zoho_inventory/sources/new-contact/new-contact.mjs index 2383a969c31e6..e5761d3bbbe14 100644 --- a/components/zoho_inventory/sources/new-contact/new-contact.mjs +++ b/components/zoho_inventory/sources/new-contact/new-contact.mjs @@ -5,7 +5,7 @@ export default { key: "zoho_inventory-new-contact", name: "New Contact", description: "Emit new event each time a new contact is created in Zoho Inventory", - version: "0.0.2", + version: "0.0.3", type: "source", dedupe: "unique", methods: { diff --git a/components/zoho_inventory/sources/new-invoice/new-invoice.mjs b/components/zoho_inventory/sources/new-invoice/new-invoice.mjs index 36e28f2aa4d72..357b03fc374f3 100644 --- a/components/zoho_inventory/sources/new-invoice/new-invoice.mjs +++ b/components/zoho_inventory/sources/new-invoice/new-invoice.mjs @@ -5,7 +5,7 @@ export default { key: "zoho_inventory-new-invoice", name: "New Invoice", description: "Emit new event each time a new invoice is created in Zoho Inventory", - version: "0.0.2", + version: "0.0.3", type: "source", dedupe: "unique", methods: { diff --git a/components/zoho_inventory/sources/new-item/new-item.mjs b/components/zoho_inventory/sources/new-item/new-item.mjs index 906712b1341b2..c7ef24ffa3068 100644 --- a/components/zoho_inventory/sources/new-item/new-item.mjs +++ b/components/zoho_inventory/sources/new-item/new-item.mjs @@ -5,7 +5,7 @@ export default { key: "zoho_inventory-new-item", name: "New Item", description: "Emit new event each time a new item is created in Zoho Inventory", - version: "0.0.2", + version: "0.0.3", type: "source", dedupe: "unique", methods: { diff --git a/components/zoho_inventory/sources/new-order/new-order.mjs b/components/zoho_inventory/sources/new-order/new-order.mjs index b05090cf49275..c811f8f670104 100644 --- a/components/zoho_inventory/sources/new-order/new-order.mjs +++ b/components/zoho_inventory/sources/new-order/new-order.mjs @@ -5,7 +5,7 @@ export default { key: "zoho_inventory-new-order", name: "New Sales Order", description: "Emit new event each time a new sales order is created in Zoho Inventory", - version: "0.0.2", + version: "0.0.3", type: "source", dedupe: "unique", methods: { diff --git a/components/zoho_inventory/zoho_inventory.app.mjs b/components/zoho_inventory/zoho_inventory.app.mjs index a9fdb1684e5a9..893198675fdf6 100644 --- a/components/zoho_inventory/zoho_inventory.app.mjs +++ b/components/zoho_inventory/zoho_inventory.app.mjs @@ -71,7 +71,7 @@ export default { }, methods: { _getBaseUrl() { - return `https://inventory.${this.$auth.base_api_uri}/api/v1/`; + return `${this.$auth.api_domain}/inventory/v1/`; }, _getHeaders() { return { diff --git a/components/zoho_invoice/README.md b/components/zoho_invoice/README.md new file mode 100644 index 0000000000000..5bfa87c3dff81 --- /dev/null +++ b/components/zoho_invoice/README.md @@ -0,0 +1,23 @@ +# Overview + +The Zoho Invoice API offers a suite of tools to automate the invoicing and billing processes. With this API, you can create and manage customers, invoices, and payments. On Pipedream, you can harness these capabilities to build workflows that trigger on specific events, such as new invoice creation, or scheduled tasks that could, for example, follow up on unpaid invoices. Due to its serverless architecture, Pipedream ensures each step of your workflow runs only when needed, saving resources and time. + +# Example Use Cases + +- **Automate Invoice Creation for New CRM Leads**: When a new lead is added to a CRM like Salesforce, automatically create a customer and a draft invoice in Zoho Invoice, ensuring the billing process begins as soon as a sales opportunity is identified. + +- **Slack Notifications for Overdue Invoices**: Set up a workflow that checks for overdue invoices daily and sends a summary message to a designated Slack channel, keeping the team informed and ready to take action on outstanding payments. + +- **Sync Invoices with Accounting Software**: Automatically export new and updated invoices from Zoho Invoice to accounting software like QuickBooks. This ensures your financial records are always in sync, reducing manual data entry and the chance of errors. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_mail/README.md b/components/zoho_mail/README.md index 00e0582056dca..14facb3555670 100644 --- a/components/zoho_mail/README.md +++ b/components/zoho_mail/README.md @@ -1,28 +1,16 @@ # Overview -The Zoho Mail API enables developers to integrate their applications with the -Zoho Mail platform, allowing them access to a wide range of email and -collaboration features. With the API, developers can build sophisticated email -applications that manage mailing lists, send and receive automated emails, -analyze customer data, manage customer accounts, and more. - -The following are a few examples of what you can build using the Zoho Mail API: - -- Create mailing lists and manage contacts -- Set up automated mailing sequences -- Analyze customer engagement data -- Access customer accounts and manage subscriptions -- Automate the sending of outgoing emails -- Receive and manage incoming emails -- Track email delivery rates and performance -- Connect with 3rd party applications and integrate with other services -- Create and manage custom customer fields and profiles -- Streamline customer interactions with automated follow up emails -- Automatically respond to incoming messages -- Create and manage user groups -- Create and manage custom address books and contact lists - -## Getting Started +The Zoho Mail API equips you with the ability to automate actions on emails, manage mailboxes, and interact with your Zoho Mail account programmatically. With Pipedream, you can trigger workflows on new emails, send emails automatically, and connect Zoho Mail to thousands of other services, streamlining communication processes, enhancing productivity, and enabling efficient data management. + +# Example Use Cases + +- **Email to Task Conversion**: Create tasks in project management tools like Trello or Asana when receiving emails with specific keywords or from certain senders. This can automate the process of tracking important requests or action items coming through your email. + +- **Support Ticket Generation**: On receiving an email with a support query or issue report, instantly generate a ticket in a customer support platform like Zendesk or Freshdesk. This helps in ensuring that customer issues are addressed promptly and not lost in the shuffle of an inbox. + +- **Email Campaign Analytics**: After sending out a marketing campaign, you might want to track responses and engagement. Use the API to tag and categorize responses, then send this data to analytics tools like Google Sheets or a BI platform to measure campaign effectiveness. + +# Getting Started 1. First, sign up for Pipedream at [https://pipedream.com](https://pipedream.com). 2. [Create a new workflow](https://pipedream.com/new). @@ -31,3 +19,15 @@ The following are a few examples of what you can build using the Zoho Mail API: 5. Once you've added a step, press the **Connect Zoho Mail** button near the top. If this is your first time authorizing Pipedream's access to your Zoho Mail account, you'll be prompted to accept that access, and Pipedream will store the authorization grant to enable the workflow to access the API. If you've already linked a Zoho Mail account via Pipedream, pressing **Connect Zoho Mail** will list any existing accounts you've linked. Once you've connected your account, you can run your workflow and fetch data from the API. You can change any of the code associated with this step, changing the API endpoint you'd like to retrieve data from, or modifying the results in any way. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_meeting/README.md b/components/zoho_meeting/README.md new file mode 100644 index 0000000000000..1ceeeaa09f44b --- /dev/null +++ b/components/zoho_meeting/README.md @@ -0,0 +1,23 @@ +# Overview + +The Zoho Meeting API lets you automate various aspects of managing online meetings and webinars. You can create, update, or cancel sessions, list all your meetings, fetch details of specific meetings, and more. Integrating the Zoho Meeting API with Pipedream enables you to connect your meeting management flow with other apps and services, streamlining your workflow and automating repetitive tasks. + +# Example Use Cases + +- **Automate Meeting Creation and Notifications**: Create meetings automatically in Zoho Meeting based on triggers from other apps, such as new event bookings in Calendly. Then use Pipedream to send custom email invites or Slack messages to the participants with the meeting details. + +- **Sync Meetings with Google Calendar**: Whenever a new meeting is scheduled in Zoho Meeting, use Pipedream to create a corresponding event in Google Calendar, and vice versa, ensuring your schedule is always up-to-date across platforms. + +- **Meeting Insights with Data Processing**: After a meeting ends, use Pipedream to fetch the meeting details and attendance report from Zoho Meeting. Process this data to gain insights or update CRM records in Salesforce, and finally, store the processed data in a Google Sheets document for record-keeping and further analysis. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_notebook/README.md b/components/zoho_notebook/README.md new file mode 100644 index 0000000000000..4d2739e6d9663 --- /dev/null +++ b/components/zoho_notebook/README.md @@ -0,0 +1,23 @@ +# Overview + +The Zoho Notebook API allows for the creation, management, and retrieval of digital notes within the Zoho Notebook service. On Pipedream, you can harness this API to automate your note-taking processes, synchronize content across different platforms, and trigger actions based on updates to your notes. With Pipedream's capability to integrate various apps, you can connect Zoho Notebook with other services to streamline workflows and increase productivity. + +# Example Use Cases + +- **Automated Note Syncing with Google Drive**: Whenever you create a new note in Zoho Notebook, a Pipedream workflow can automatically replicate the note to a designated folder in Google Drive. This ensures that your notes are backed up and accessible from the cloud storage service of your choice. + +- **Task Creation in Project Management Tools from Notes**: When a note containing specific keywords or tags (like "TODO" or "#ProjectX") is added to Zoho Notebook, Pipedream can parse the note and create corresponding tasks in a project management app such as Trello or Asana, linking back to the original note for reference. + +- **Email Digest of Daily Notes**: Compile a daily digest email of all new notes created in Zoho Notebook during the day. Using Pipedream's scheduled triggers, you can collate notes and send a summary to yourself or your team to keep everyone informed and aligned. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_notebook/actions/create-notebook/create-notebook.mjs b/components/zoho_notebook/actions/create-notebook/create-notebook.mjs new file mode 100644 index 0000000000000..44dc5b4dbc428 --- /dev/null +++ b/components/zoho_notebook/actions/create-notebook/create-notebook.mjs @@ -0,0 +1,71 @@ +import zohoNotebook from "../../zoho_notebook.app.mjs"; + +export default { + key: "zoho_notebook-create-notebook", + name: "Create Notebook", + description: "Creates a new notebook.", + version: "0.0.1", + type: "action", + props: { + zohoNotebook, + name: { + type: "string", + label: "Name", + description: "Name of the new notebook", + }, + coverImageId: { + type: "string", + label: "Cover Image ID", + description: "ID of the cover image that is to be used by the notebook", + optional: true, + }, + location: { + type: "string", + label: "Location", + description: "Name of the place where the notebook is made", + optional: true, + }, + longitude: { + type: "string", + label: "Longitude", + description: "Longitude coordinate", + optional: true, + }, + latitude: { + type: "string", + label: "Latitude", + description: "Latitude coordinate", + optional: true, + }, + isDefault: { + type: "boolean", + label: "Is Default", + description: "Specifies if this Notebook is the default notebook", + optional: true, + }, + isLocked: { + type: "boolean", + label: "Is Locked", + description: "Specifies if this Notebook is locked", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.zohoNotebook.createNotebook({ + $, + params: { + JSONString: JSON.stringify({ + name: this.name, + cover_image_id: this.coverImageId, + location: this.location, + longitude: +this.longitude, + latitude: +this.latitude, + is_default: this.isDefault, + is_locked: this.isLocked, + }), + }, + }); + $.export("$summary", `Successfully created notebook with ID ${response.notebook_id}`); + return response; + }, +}; diff --git a/components/zoho_notebook/actions/list-notebooks/list-notebooks.mjs b/components/zoho_notebook/actions/list-notebooks/list-notebooks.mjs new file mode 100644 index 0000000000000..7684a5ea7b8b3 --- /dev/null +++ b/components/zoho_notebook/actions/list-notebooks/list-notebooks.mjs @@ -0,0 +1,56 @@ +import zohoNotebook from "../../zoho_notebook.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "zoho_notebook-list-notebooks", + name: "List Notebooks", + description: "Retrieve a list of all notebooks created by the user.", + version: "0.0.1", + type: "action", + props: { + zohoNotebook, + sortColumn: { + type: "string", + label: "Sort Column", + description: "Column to sort the results by. If not specified, last saved order is used to fetch the list", + options: constants.SORT_COLUMNS, + optional: true, + }, + includeCoverImage: { + type: "boolean", + label: "Include Cover Image", + description: "Whether to include the cover image in the results", + optional: true, + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "Maximum number of results to return", + optional: true, + }, + }, + async run({ $ }) { + const items = this.zohoNotebook.paginate({ + resourceFn: this.zohoNotebook.listNotebooks, + args: { + $, + params: { + sort_column: this.sortColumn, + include_cover_img: this.includeCoverImage, + }, + }, + resourceType: "notebooks", + max: this.maxResults, + }); + + const notebooks = []; + for await (const item of items) { + notebooks.push(item); + } + + $.export("$summary", `Successfully listed ${notebooks.length} notebook${notebooks.length === 1 + ? "" + : "s"}`); + return notebooks; + }, +}; diff --git a/components/zoho_notebook/common/constants.mjs b/components/zoho_notebook/common/constants.mjs new file mode 100644 index 0000000000000..6a58533db7243 --- /dev/null +++ b/components/zoho_notebook/common/constants.mjs @@ -0,0 +1,45 @@ +const DEFAULT_LIMIT = 50; + +const SORT_COLUMNS = [ + { + label: "Custom Order (ASC)", + value: "Notebook.CustomOrder", + }, + { + label: "Created Time (ASC)", + value: "Notebook.CreatedTime", + }, + { + label: "Created Time (DESC)", + value: "-Notebook.CreatedTime", + }, + { + label: "Last Modified Time (ASC)", + value: "Notebook.LastModifiedTime", + }, + { + label: "Last Modified Time (DESC)", + value: "-Notebook.LastModifiedTime", + }, + { + label: "Last Accessed (ASC)", + value: "Notebook.LastAccessed", + }, + { + label: "Last Accessed (DESC)", + value: "-Notebook.LastAccessed", + }, + { + label: "Alphabet (ASC)", + value: "Notebook.Alphabet", + }, + { + label: "Alphabet (DESC)", + value: "-Notebook.Alphabet", + }, +]; + +export default { + DEFAULT_LIMIT, + SORT_COLUMNS, +}; diff --git a/components/zoho_notebook/package.json b/components/zoho_notebook/package.json new file mode 100644 index 0000000000000..4d741a4b1aaca --- /dev/null +++ b/components/zoho_notebook/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/zoho_notebook", + "version": "0.1.0", + "description": "Pipedream Zoho Notebook Components", + "main": "zoho_notebook.app.mjs", + "keywords": [ + "pipedream", + "zoho_notebook" + ], + "homepage": "https://pipedream.com/apps/zoho_notebook", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^1.6.0" + } +} diff --git a/components/zoho_notebook/sources/common/base.mjs b/components/zoho_notebook/sources/common/base.mjs new file mode 100644 index 0000000000000..5932a3f5ab63b --- /dev/null +++ b/components/zoho_notebook/sources/common/base.mjs @@ -0,0 +1,99 @@ +import zohoNotebook from "../../zoho_notebook.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + zohoNotebook, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + notebookId: { + propDefinition: [ + zohoNotebook, + "notebookId", + ], + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + getResourceFn() { + return this.zohoNotebook.listNotecards; + }, + getArgs() { + return { + notebookId: this.notebookId, + }; + }, + getResourceType() { + return "notecards"; + }, + isRelevant() { + return true; + }, + emitEvent(item) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }, + generateMeta(item) { + return { + id: item.notecard_id, + summary: this.getSummary(item), + ts: Date.parse(item.created_at), + }; + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + const resourceFn = this.getResourceFn(); + const args = this.getArgs(); + const resourceType = this.getResourceType(); + const items = this.zohoNotebook.paginate({ + resourceFn, + args: { + ...args, + params: { + ...args.params, + sort_column: "-Notecard.CreatedTime", + }, + }, + resourceType, + max, + }); + const results = []; + for await (const item of items) { + const ts = Date.parse(item.created_time); + if (ts >= lastTs) { + maxTs = Math.max(maxTs, ts); + if (this.isRelevant(item)) { + results.push(item); + } + } else { + break; + } + } + results.reverse().forEach((item) => this.emitEvent(item)); + this._setLastTs(maxTs); + }, + }, + async run() { + await this.processEvent(); + }, +}; + diff --git a/components/zoho_notebook/sources/new-audio-card/new-audio-card.mjs b/components/zoho_notebook/sources/new-audio-card/new-audio-card.mjs new file mode 100644 index 0000000000000..4e655e92be9a8 --- /dev/null +++ b/components/zoho_notebook/sources/new-audio-card/new-audio-card.mjs @@ -0,0 +1,20 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "zoho_notebook-new-audio-card", + name: "New Audio Card", + description: "Emit new event when a new audio card is created in Zoho Notebook", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + isRelevant(item) { + return item.type === "note/audio"; + }, + getSummary(item) { + return `New Audio Card ID: ${item.notecard_id}`; + }, + }, +}; diff --git a/components/zoho_notebook/sources/new-checklist-card/new-checklist-card.mjs b/components/zoho_notebook/sources/new-checklist-card/new-checklist-card.mjs new file mode 100644 index 0000000000000..3c3bf35157f82 --- /dev/null +++ b/components/zoho_notebook/sources/new-checklist-card/new-checklist-card.mjs @@ -0,0 +1,20 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "zoho_notebook-new-checklist-card", + name: "New Checklist Card", + description: "Emit new event when a new checklist card is created in Zoho Notebook.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + isRelevant(item) { + return item.type === "note/checklist"; + }, + getSummary(item) { + return `New Checklist Card ID: ${item.notecard_id}`; + }, + }, +}; diff --git a/components/zoho_notebook/sources/new-notecard-in-notebook/new-notecard-in-notebook.mjs b/components/zoho_notebook/sources/new-notecard-in-notebook/new-notecard-in-notebook.mjs new file mode 100644 index 0000000000000..6e12c05a74350 --- /dev/null +++ b/components/zoho_notebook/sources/new-notecard-in-notebook/new-notecard-in-notebook.mjs @@ -0,0 +1,17 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "zoho_notebook-new-notecard-in-notebook", + name: "New Note Card in Notebook", + description: "Emit new event when a new notecard is created in a specific notebook in Zoho Notebook.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSummary(item) { + return `New Notecard ID: ${item.notecard_id}`; + }, + }, +}; diff --git a/components/zoho_notebook/zoho_notebook.app.mjs b/components/zoho_notebook/zoho_notebook.app.mjs new file mode 100644 index 0000000000000..55225c2af9296 --- /dev/null +++ b/components/zoho_notebook/zoho_notebook.app.mjs @@ -0,0 +1,102 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "zoho_notebook", + propDefinitions: { + notebookId: { + type: "string", + label: "Notebook ID", + description: "The identifier of the notebook where the notecard will be created", + async options({ page }) { + const { notebooks } = await this.listNotebooks({ + params: { + limit: constants.DEFAULT_LIMIT, + offset: page * constants.DEFAULT_LIMIT, + }, + }); + return notebooks?.map(({ + notebook_id: value, name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + }, + methods: { + _baseUrl() { + return `https://notebook.${this.$auth.base_api_uri}/api/v1`; + }, + _makeRequest(opts = {}) { + const { + $ = this, + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + ...headers, + "Authorization": `Zoho-oauthtoken ${this.$auth.oauth_access_token}`, + }, + }); + }, + listNotebooks(opts = {}) { + return this._makeRequest({ + path: "/notebooks", + ...opts, + }); + }, + listNotecards({ + notebookId, ...opts + }) { + return this._makeRequest({ + path: `/notebooks/${notebookId}/notecards`, + ...opts, + }); + }, + createNotebook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/notebooks", + ...opts, + }); + }, + async *paginate({ + resourceFn, + args = {}, + resourceType, + max, + }) { + args = { + ...args, + params: { + ...args.params, + limit: constants.DEFAULT_LIMIT, + offset: 0, + }, + }; + let total, count = 0; + do { + const results = await resourceFn(args); + const items = results[resourceType]; + if (!items) { + return; + } + for (const item of items) { + yield item; + count++; + if (max && count >= max) { + return; + } + } + total = items?.length; + args.params.offset += args.params.limit; + } while (total === args.params.limit); + }, + }, +}; diff --git a/components/zoho_people/README.md b/components/zoho_people/README.md new file mode 100644 index 0000000000000..3b33c87f32f09 --- /dev/null +++ b/components/zoho_people/README.md @@ -0,0 +1,23 @@ +# Overview + +The Zoho People API lets you interface with Zoho People, a human resource management platform. With this API on Pipedream, you can automate HR processes, sync employee data across systems, manage leave records, and more. Use Pipedream's no-code platform to integrate Zoho People with hundreds of other apps for custom workflows that fit your business needs. + +# Example Use Cases + +- **Employee Onboarding Automation**: Trigger a workflow in Pipedream when a new employee is added to Zoho People. The workflow can send a welcome email through SendGrid, create a new user in your Active Directory, and add the employee to relevant Slack channels automatically. + +- **Leave Request Approval**: Set up a Pipedream workflow where leave requests in Zoho People trigger an approval process. The request can be pushed to a Slack channel for a manager's approval, and once approved, it can update the leave status in Zoho People and notify the employee via Twilio SMS. + +- **Daily HR Report Generation**: Configure a daily scheduled workflow in Pipedream that fetches the previous day's employee activity from Zoho People, compiles a report, and then sends this report through Gmail to the HR department, providing insights into attendance, time-off, and other HR metrics. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_people/package.json b/components/zoho_people/package.json index 592dc98a8c385..90c8ebb68f0f4 100644 --- a/components/zoho_people/package.json +++ b/components/zoho_people/package.json @@ -11,4 +11,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/zoho_projects/README.md b/components/zoho_projects/README.md index 6f5eac092f120..d8eca18e97aa3 100644 --- a/components/zoho_projects/README.md +++ b/components/zoho_projects/README.md @@ -1,33 +1,23 @@ # Overview -The Zoho Projects API is a powerful tool that allows developers to build custom -applications that enhance the functionality of the Zoho Projects platform. With -the API, developers have access to all the functionalities of the project such -as tasks, resources, timesheets, documents, deep integration with various -third-party applications and more. Developers can build applications for a -variety of needs including: - -- Integration with custom workflows: Devlopers can build custom workflows that - enhance the user experience and increase productivity. -- Online portals: Create an online portal or dashboard for users to manage - their projects, tasks and resources. -- Task management: Create applications to track and manage tasks with detailed - reporting capabilities. -- Resource management: Build applications to track and control resources, - assign tasks and manage budgeting. -- Reports and analytics: Create comprehensive dashboards to analyse data and - develop meaningful insights. -- Mobile applications: Create native apps for iOS and Android that allow users - to access the project and its data on the go. - -Here are some examples of things you can build using the Zoho Projects API: - -- Automated Gantt charts -- Task and resource tracking -- Project calendar tracking -- Task and project budgeting -- Project management dashboards -- Time management -- Workflow automation -- Task completion alerts -- Integrations with third-party applications and services +The Zoho Projects API lets you harness the full potential of project management by enabling seamless integration with other tools and automating routine tasks. With it, you can create projects, manage tasks, track time, and customize your workflow. On Pipedream, you can leverage this API to build robust automations that connect Zoho Projects with a plethora of other apps, streamlining project tracking and collaboration workflows, all without writing a single line of code. + +# Example Use Cases + +- **Project Status Updates to Slack**: Trigger a workflow in Pipedream when the status of a project in Zoho Projects changes. Then, post an update to a designated Slack channel to keep your team informed in real time about the project's progress. + +- **New Task Assignment Email Notifications**: When a new task is created in Zoho Projects, use Pipedream to automatically send out an email notification to the assigned team member or client. This could be done by integrating with an email service like SendGrid, ensuring immediate communication and clarity on new assignments. + +- **GitHub Commit Trigger for Time Tracking**: Start a time entry in Zoho Projects whenever a new commit is made to a specific GitHub repository. This allows for precise tracking of development time, providing valuable insights for project management and billing purposes. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_projects/actions/add-log-time/add-log-time.mjs b/components/zoho_projects/actions/add-log-time/add-log-time.mjs index af3aeeeed1502..37ae39bf2062c 100644 --- a/components/zoho_projects/actions/add-log-time/add-log-time.mjs +++ b/components/zoho_projects/actions/add-log-time/add-log-time.mjs @@ -6,7 +6,7 @@ export default { name: "Add Log Time", description: "Add Time for a General Log. Adds the time log to other tasks. [See the docs here](https://www.zoho.com/projects/help/rest-api/log-time.html#alink11)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoProjects, portalId: { diff --git a/components/zoho_projects/actions/create-bug/create-bug.mjs b/components/zoho_projects/actions/create-bug/create-bug.mjs index 8559d46a0d621..2374ee11a88bb 100644 --- a/components/zoho_projects/actions/create-bug/create-bug.mjs +++ b/components/zoho_projects/actions/create-bug/create-bug.mjs @@ -6,7 +6,7 @@ export default { name: "Create Bug", description: "Creates a bug. [See the docs here](https://www.zoho.com/projects/help/rest-api/bugs-api.html#alink3)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoProjects, portalId: { diff --git a/components/zoho_projects/actions/create-milestone/create-milestone.mjs b/components/zoho_projects/actions/create-milestone/create-milestone.mjs index 07e6185606072..b5ae10871dc6e 100644 --- a/components/zoho_projects/actions/create-milestone/create-milestone.mjs +++ b/components/zoho_projects/actions/create-milestone/create-milestone.mjs @@ -6,7 +6,7 @@ export default { name: "Create Milestone", description: "Creates a milestone. [See the docs here](https://www.zoho.com/projects/help/rest-api/milestones-api.html#alink3)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoProjects, portalId: { diff --git a/components/zoho_projects/actions/create-project/create-project.mjs b/components/zoho_projects/actions/create-project/create-project.mjs index bdfe2a0f661ce..ce46df5db628e 100644 --- a/components/zoho_projects/actions/create-project/create-project.mjs +++ b/components/zoho_projects/actions/create-project/create-project.mjs @@ -6,7 +6,7 @@ export default { name: "Create Project", description: "Creates a project. [See the docs here](https://www.zoho.com/projects/help/rest-api/projects-api.html#alink5)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoProjects, portalId: { diff --git a/components/zoho_projects/actions/create-task-list/create-task-list.mjs b/components/zoho_projects/actions/create-task-list/create-task-list.mjs index baf88a7439be4..f2e6d46232be2 100644 --- a/components/zoho_projects/actions/create-task-list/create-task-list.mjs +++ b/components/zoho_projects/actions/create-task-list/create-task-list.mjs @@ -6,7 +6,7 @@ export default { name: "Create Task List", description: "Creates a task list. [See the docs here](https://www.zoho.com/projects/help/rest-api/tasklists-api.html#alink2)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoProjects, portalId: { diff --git a/components/zoho_projects/actions/create-task/create-task.mjs b/components/zoho_projects/actions/create-task/create-task.mjs index 85311a2661a9a..ca0c53179b6fd 100644 --- a/components/zoho_projects/actions/create-task/create-task.mjs +++ b/components/zoho_projects/actions/create-task/create-task.mjs @@ -6,7 +6,7 @@ export default { name: "Create Task", description: "Creates a task. [See the docs here](https://www.zoho.com/projects/help/rest-api/tasks-api.html#alink4)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoProjects, portalId: { diff --git a/components/zoho_projects/actions/find-project/find-project.mjs b/components/zoho_projects/actions/find-project/find-project.mjs index 1c048d18baa25..f3509643c0b1e 100644 --- a/components/zoho_projects/actions/find-project/find-project.mjs +++ b/components/zoho_projects/actions/find-project/find-project.mjs @@ -6,7 +6,7 @@ export default { name: "Find Project", description: "Lists the modules across the portal based on the search term. The API returns both active and archived projects for the users having admin privileges. [See the docs here](https://www.zoho.com/projects/help/rest-api/search-api.html#alink1)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoProjects, portalId: { diff --git a/components/zoho_projects/actions/update-project/update-project.mjs b/components/zoho_projects/actions/update-project/update-project.mjs index 68bc9bf3a6543..5402f34d8706d 100644 --- a/components/zoho_projects/actions/update-project/update-project.mjs +++ b/components/zoho_projects/actions/update-project/update-project.mjs @@ -6,7 +6,7 @@ export default { name: "Update Project", description: "Updates a project. [See the docs here](https://www.zoho.com/projects/help/rest-api/projects-api.html#alink6)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoProjects, portalId: { diff --git a/components/zoho_projects/actions/upload-file/upload-file.mjs b/components/zoho_projects/actions/upload-file/upload-file.mjs index 092cf3f4f0767..b1ff31a9eb61a 100644 --- a/components/zoho_projects/actions/upload-file/upload-file.mjs +++ b/components/zoho_projects/actions/upload-file/upload-file.mjs @@ -6,7 +6,7 @@ export default { name: "Upload File", description: "Add a document. [See the docs here](https://www.zoho.com/projects/help/rest-api/documents-api.html#alink3)", type: "action", - version: "0.0.1", + version: "0.0.2", props: { zohoProjects, portalId: { diff --git a/components/zoho_projects/common/utils.mjs b/components/zoho_projects/common/utils.mjs index a0d7c3c985e17..7554d45ea3fb2 100644 --- a/components/zoho_projects/common/utils.mjs +++ b/components/zoho_projects/common/utils.mjs @@ -16,7 +16,7 @@ function buildFormData(formData, data, parentKey) { buildFormData(formData, data[key], parentKey && `${parentKey}[${key}]` || key); }); - } else if (data && constants.FILE_PROP_NAMES.some((prop) => prop.includes(parentKey))) { + } else if (data && constants.FILE_PROP_NAMES.some((prop) => parentKey.includes(prop))) { formData.append(parentKey, createReadStream(data)); } else if (data) { diff --git a/components/zoho_projects/package.json b/components/zoho_projects/package.json index 35af76656c73f..92fd27a311cde 100644 --- a/components/zoho_projects/package.json +++ b/components/zoho_projects/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/zoho_projects", - "version": "0.0.3", + "version": "0.0.4", "description": "Pipedream Zoho Projects Components", "main": "zoho_projects.app.mjs", "keywords": [ @@ -13,6 +13,9 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.2.0" + "@pipedream/platform": "^1.6.2", + "async-retry": "^1.3.3", + "form-data": "^4.0.0", + "fs": "^0.0.1-security" } } diff --git a/components/zoho_projects/sources/new-bug/new-bug.mjs b/components/zoho_projects/sources/new-bug/new-bug.mjs index 8ee8d92d131a3..702504ea157b8 100644 --- a/components/zoho_projects/sources/new-bug/new-bug.mjs +++ b/components/zoho_projects/sources/new-bug/new-bug.mjs @@ -7,7 +7,7 @@ export default { name: "New Bug", description: "Emit new event when a new bug is created. [See the docs here](https://www.zoho.com/projects/help/rest-api/bugs-api.html#alink1)", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", props: { ...common.props, diff --git a/components/zoho_projects/sources/new-log-time/new-log-time.mjs b/components/zoho_projects/sources/new-log-time/new-log-time.mjs index d2d4742d3ab3e..2f4791ea9c1e3 100644 --- a/components/zoho_projects/sources/new-log-time/new-log-time.mjs +++ b/components/zoho_projects/sources/new-log-time/new-log-time.mjs @@ -6,7 +6,7 @@ export default { name: "New Log Time", description: "Emit new event when a log time is created. [See the docs here](https://www.zoho.com/projects/help/rest-api/log-time.html#alink1)", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", props: { ...common.props, diff --git a/components/zoho_projects/sources/new-milestone/new-milestone.mjs b/components/zoho_projects/sources/new-milestone/new-milestone.mjs index 4dd0bac2def47..1872e56cc3af8 100644 --- a/components/zoho_projects/sources/new-milestone/new-milestone.mjs +++ b/components/zoho_projects/sources/new-milestone/new-milestone.mjs @@ -7,7 +7,7 @@ export default { name: "New Milestone", description: "Emit new event when a new milestone is created. [See the docs here](https://www.zoho.com/projects/help/rest-api/milestones-api.html#alink1)", type: "source", - version: "0.0.2", + version: "0.0.3", dedupe: "unique", props: { ...common.props, diff --git a/components/zoho_projects/sources/new-project/new-project.mjs b/components/zoho_projects/sources/new-project/new-project.mjs index 51ae679388d07..a2cb53904d559 100644 --- a/components/zoho_projects/sources/new-project/new-project.mjs +++ b/components/zoho_projects/sources/new-project/new-project.mjs @@ -7,7 +7,7 @@ export default { name: "New Project", description: "Emit new event when a new project is created. [See the docs here](https://www.zoho.com/projects/help/rest-api/projects-api.html#alink1)", type: "source", - version: "0.0.2", + version: "0.0.3", props: { ...common.props, portalId: { diff --git a/components/zoho_projects/sources/new-task-list/new-task-list.mjs b/components/zoho_projects/sources/new-task-list/new-task-list.mjs index bf5bec4791795..c3b94195f0a19 100644 --- a/components/zoho_projects/sources/new-task-list/new-task-list.mjs +++ b/components/zoho_projects/sources/new-task-list/new-task-list.mjs @@ -7,7 +7,7 @@ export default { name: "New Task List", description: "Emit new event when a task list is created. [See the docs here](https://www.zoho.com/projects/help/rest-api/tasklists-api.html#alink1)", type: "source", - version: "0.0.2", + version: "0.0.3", props: { ...common.props, portalId: { diff --git a/components/zoho_projects/sources/new-task/new-task.mjs b/components/zoho_projects/sources/new-task/new-task.mjs index dc3d3cc1ec743..c20ea62899317 100644 --- a/components/zoho_projects/sources/new-task/new-task.mjs +++ b/components/zoho_projects/sources/new-task/new-task.mjs @@ -7,7 +7,7 @@ export default { name: "New Task", description: "Emit new event when a new task is created. [See the docs here](https://www.zoho.com/projects/help/rest-api/tasks-api.html#alink1)", type: "source", - version: "0.0.2", + version: "0.0.3", props: { ...common.props, portalId: { diff --git a/components/zoho_recruit/README.md b/components/zoho_recruit/README.md new file mode 100644 index 0000000000000..68e3015af066a --- /dev/null +++ b/components/zoho_recruit/README.md @@ -0,0 +1,23 @@ +# Overview + +The Zoho Recruit API lets you access the Zoho Recruit ATS functionalities programmatically, enabling integrations with other services, automation of tasks, and enhancement of the recruitment process. With this API in Pipedream, you can trigger workflows based on events in Zoho Recruit, manipulate candidate data, post jobs, schedule interviews, and automate communication. The seamless connection between Zoho Recruit and other apps through Pipedream creates a powerful ecosystem to streamline your hiring workflows. + +# Example Use Cases + +- **Automate Candidate Follow-Up Emails**: When a candidate's status changes in Zoho Recruit, automatically send personalized follow-up emails using Gmail or another email service. This keeps candidates engaged and informed throughout the recruitment process. + +- **Sync New Candidates with HR Software**: Whenever a new candidate is added to Zoho Recruit, add their info to your HR software, like BambooHR, to maintain an updated database across platforms. This ensures all teams have access to the latest candidate information without manual data entry. + +- **Post Jobs to Social Media**: Automatically share new job openings from Zoho Recruit to your company's LinkedIn and Facebook pages. This increases visibility of your job postings and attracts a broader range of applicants. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_salesiq/README.md b/components/zoho_salesiq/README.md new file mode 100644 index 0000000000000..61a69fe710441 --- /dev/null +++ b/components/zoho_salesiq/README.md @@ -0,0 +1,23 @@ +# Overview + +The Zoho SalesIQ API offers a wealth of possibilities for engaging with customers and streamlining sales processes. By leveraging this API in Pipedream, you can craft automated workflows to react to events, manage visitors, and integrate customer interaction data with other services. This could range from triggering actions based on visitor activities to syncing chat transcripts with CRM tools. Pipedream's ability to connect with hundreds of apps allows for creative and powerful automations that can save time and provide valuable insights. + +# Example Use Cases + +- **Visitor Activity to Slack Notification**: When a visitor performs a specific action on your website, such as viewing a pricing page, use Zoho SalesIQ to trigger a workflow that sends a custom alert to a designated Slack channel. This enables your sales team to respond promptly to potential leads. + +- **Support Ticket Creation in Zendesk**: Upon ending a chat with a customer in Zoho SalesIQ, automatically create a support ticket in Zendesk with the chat transcript and customer details. This keeps support teams aligned and ensures follow-ups are timely and informed. + +- **Syncing Chat Leads to Mailchimp**: Convert leads captured through Zoho SalesIQ chats into subscribers in your Mailchimp list. Set up a workflow that takes the email addresses collected from chats and adds them to a Mailchimp campaign, triggering a sequence of targeted emails. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_sheet/.gitignore b/components/zoho_sheet/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/zoho_sheet/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/zoho_sheet/README.md b/components/zoho_sheet/README.md index 68d7b68404bb8..740122466f82c 100644 --- a/components/zoho_sheet/README.md +++ b/components/zoho_sheet/README.md @@ -1,18 +1,23 @@ # Overview -Using the Zoho Sheet API, you can create a wide variety of applications that -work with spreadsheets. With it, you can build extensions, automate workflow, -create custom interfaces, and much more. Below are some examples of projects -you could build with the Zoho Sheet API. - -- Create custom interfaces to view, organize, and manipulate spreadsheets. -- Automate spreadsheet functions like data sorting, formula calculation, and - data updating. -- Create an extension that interacts with multiple Zoho Sheets accounts. -- Manage and combine data from multiple sources. -- Develop tools for team collaboration on sheets. -- Create charts, graphs, and visualizations for insights into the data. -- Migrate spreadsheets from one platform to another. -- Generate personalized reports from sheets with data. -- Develop applications that integrate with other products and services. -- Automate notifications for timelines, checklists, and other tasks. +The Zoho Sheet API allows you to manipulate spreadsheet data programmatically. Imagine harnessing this functionality within Pipedream's ecosystem, where you can automate data flows, sync information across platforms, and generate reports with ease. With Pipedream, you can trigger workflows using events from numerous apps, fetch or push data to Zoho Sheet, analyze or transform this data, and even automate notifications based on the results. + +# Example Use Cases + +- **Automated Invoice Generation**: Trigger a workflow when a new sale is logged in your CRM (like Salesforce). The Zoho Sheet API adds sale details to a spreadsheet, calculates totals, and generates an invoice. Then, use an email service like SendGrid to send the invoice to the customer. + +- **Marketing Campaign Tracking**: When a new campaign is launched, a workflow is triggered to create a new sheet for tracking. As campaign data flows in from platforms like Google Analytics or Facebook Ads, it's logged in the sheet. Use Pipedream to process this data and update the sheet with key performance metrics. + +- **Inventory Management**: Set up a workflow that triggers on a schedule (e.g., daily). The workflow fetches inventory data from an e-commerce platform like Shopify, updates the Zoho Sheet with current stock levels, and applies conditional formatting to highlight items with low stock. Additionally, it could create purchase orders for items below threshold levels. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_sheet/actions/create-row/create-row.mjs b/components/zoho_sheet/actions/create-row/create-row.mjs new file mode 100644 index 0000000000000..882a067ba3c02 --- /dev/null +++ b/components/zoho_sheet/actions/create-row/create-row.mjs @@ -0,0 +1,54 @@ +import { parseObject } from "../../common/utils.mjs"; +import zohoSheet from "../../zoho_sheet.app.mjs"; + +export default { + key: "zoho_sheet-create-row", + name: "Create Row", + description: "Creates a new row in the specified worksheet. [See the documentation](https://www.zoho.com/sheet/help/api/v2/)", + version: "0.0.1", + type: "action", + props: { + zohoSheet, + workbookId: { + propDefinition: [ + zohoSheet, + "workbookId", + ], + }, + worksheet: { + propDefinition: [ + zohoSheet, + "worksheet", + ({ workbookId }) => ({ + workbookId, + }), + ], + }, + headerRow: { + type: "integer", + label: "Header Row", + description: "Default value is 1. This can be mentioned if the table header is not in the first row of the worksheet.", + optional: true, + }, + data: { + propDefinition: [ + zohoSheet, + "data", + ], + }, + }, + async run({ $ }) { + const response = await this.zohoSheet.createRow({ + $, + workbookId: this.workbookId, + data: { + worksheet_id: this.worksheet, + header_row: this.headerRow || 1, + json_data: JSON.stringify(parseObject(this.data)), + }, + }); + + $.export("$summary", `Successfully created a row in the worksheet: ${response.sheet_name}`); + return response; + }, +}; diff --git a/components/zoho_sheet/actions/search-delete-row/search-delete-row.mjs b/components/zoho_sheet/actions/search-delete-row/search-delete-row.mjs new file mode 100644 index 0000000000000..da2705b497491 --- /dev/null +++ b/components/zoho_sheet/actions/search-delete-row/search-delete-row.mjs @@ -0,0 +1,88 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import zohoSheet from "../../zoho_sheet.app.mjs"; + +export default { + key: "zoho_sheet-search-delete-row", + name: "Search and Delete Row", + description: "Searches for a row based on provided criteria and deletes it. [See the documentation](https://www.zoho.com/sheet/help/api/v2/)", + version: "0.0.1", + type: "action", + props: { + zohoSheet, + workbookId: { + propDefinition: [ + zohoSheet, + "workbookId", + ], + }, + worksheet: { + propDefinition: [ + zohoSheet, + "worksheet", + ({ workbookId }) => ({ + workbookId, + }), + ], + }, + headerRow: { + propDefinition: [ + zohoSheet, + "headerRow", + ], + optional: true, + }, + criteria: { + propDefinition: [ + zohoSheet, + "criteria", + ], + optional: true, + }, + rowArray: { + type: "integer[]", + label: "Row Array", + description: "Array of row indexs, which needs to be deleted.", + optional: true, + }, + firstMatchOnly: { + propDefinition: [ + zohoSheet, + "firstMatchOnly", + ], + }, + isCaseSensitive: { + propDefinition: [ + zohoSheet, + "isCaseSensitive", + ], + }, + deleteRows: { + type: "boolean", + label: "Delete Rows", + description: "If true it will delete the rows completely, otherwise the records are only erased by default.", + default: false, + }, + }, + async run({ $ }) { + if (!this.criteria && !this.rowArray) { + throw new ConfigurationError("You must provide at least **Criteria** or **Row Array** to process this request."); + } + const response = await this.zohoSheet.deleteRow({ + $, + workbookId: this.workbookId, + data: { + worksheet_id: this.worksheet, + header_row: this.headerRow, + criteria: this.criteria, + row_array: JSON.stringify(parseObject(this.rowArray)), + first_match_only: this.firstMatchOnly, + is_case_sensitive: this.isCaseSensitive, + delete_rows: this.deleteRows, + }, + }); + + $.export("$summary", `Row matching criteria deleted successfully from worksheet ${this.worksheet}`); + return response; + }, +}; diff --git a/components/zoho_sheet/actions/update-row/update-row.mjs b/components/zoho_sheet/actions/update-row/update-row.mjs new file mode 100644 index 0000000000000..4dfd8bdf696e6 --- /dev/null +++ b/components/zoho_sheet/actions/update-row/update-row.mjs @@ -0,0 +1,81 @@ +import { parseObject } from "../../common/utils.mjs"; +import zohoSheet from "../../zoho_sheet.app.mjs"; + +export default { + key: "zoho_sheet-update-row", + name: "Update Row", + description: "Finds a specific row by its index and updates its content. [See the documentation](https://www.zoho.com/sheet/help/api/v2/)", + version: "0.0.1", + type: "action", + props: { + zohoSheet, + workbookId: { + propDefinition: [ + zohoSheet, + "workbookId", + ], + }, + worksheet: { + propDefinition: [ + zohoSheet, + "worksheet", + ({ workbookId }) => ({ + workbookId, + }), + ], + }, + headerRow: { + propDefinition: [ + zohoSheet, + "headerRow", + ], + optional: true, + }, + criteria: { + propDefinition: [ + zohoSheet, + "criteria", + ], + description: "If criteria is not set all available rows will get updated. Mention the criteria as described above.", + optional: true, + }, + firstMatchOnly: { + propDefinition: [ + zohoSheet, + "firstMatchOnly", + ], + description: "If true and if there are multiple records on the specified criteria, records will be updated for first match alone. Otherwise, all the matched records will be updated.", + }, + isCaseSensitive: { + propDefinition: [ + zohoSheet, + "isCaseSensitive", + ], + }, + data: { + propDefinition: [ + zohoSheet, + "data", + ], + type: "object", + description: "The JSON data that needs to be updated. Example:{\"Month\":\"May\",\"Amount\":50}", + }, + }, + async run({ $ }) { + const response = await this.zohoSheet.updateRow({ + $, + workbookId: this.workbookId, + data: { + worksheet_id: this.worksheet, + header_row: this.headerRow, + criteria: this.criteria, + first_match_only: this.firstMatchOnly, + is_case_sensitive: this.isCaseSensitive, + data: JSON.stringify(parseObject(this.data)), + }, + }); + + $.export("$summary", `Successfully updated ${response.no_of_affected_rows} row(s) in worksheet ${this.worksheet}`); + return response; + }, +}; diff --git a/components/zoho_sheet/app/zoho_sheet.app.ts b/components/zoho_sheet/app/zoho_sheet.app.ts deleted file mode 100644 index 7b57f52e4f210..0000000000000 --- a/components/zoho_sheet/app/zoho_sheet.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "zoho_sheet", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); diff --git a/components/zoho_sheet/common/utils.mjs b/components/zoho_sheet/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/zoho_sheet/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/zoho_sheet/package.json b/components/zoho_sheet/package.json index dd257a1f16d70..7c6ae868bb86f 100644 --- a/components/zoho_sheet/package.json +++ b/components/zoho_sheet/package.json @@ -1,18 +1,18 @@ { "name": "@pipedream/zoho_sheet", - "version": "0.0.3", + "version": "0.1.0", "description": "Pipedream Zoho Sheet Components", - "main": "dist/app/zoho_sheet.app.mjs", + "main": "zoho_sheet.app.mjs", "keywords": [ "pipedream", "zoho_sheet" ], - "files": [ - "dist" - ], "homepage": "https://pipedream.com/apps/zoho_sheet", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/zoho_sheet/sources/common/base.mjs b/components/zoho_sheet/sources/common/base.mjs new file mode 100644 index 0000000000000..68b830a1fb559 --- /dev/null +++ b/components/zoho_sheet/sources/common/base.mjs @@ -0,0 +1,47 @@ +import zohoSheet from "../../zoho_sheet.app.mjs"; + +export default { + props: { + zohoSheet, + http: "$.interface.http", + db: "$.service.db", + serviceName: { + type: "string", + label: "Service Name", + description: "The name of the webhook.", + }, + }, + methods: { + getExtraData() { + return {}; + }, + }, + hooks: { + async activate() { + await this.zohoSheet.createWebhook({ + data: { + service_name: this.serviceName.replace(/\s/g, ""), + target_url: this.http.endpoint, + event: this.getEvent(), + ...this.getExtraData(), + }, + }); + }, + async deactivate() { + await this.zohoSheet.deleteWebhook({ + data: { + target_url: this.http.endpoint, + ...this.getExtraData(), + }, + }); + }, + }, + async run({ body }) { + const ts = Date.parse(new Date()); + this.$emit(body, { + id: `${ts}`, + summary: this.getSummary(body), + ts: ts, + }); + }, +}; diff --git a/components/zoho_sheet/sources/new-or-updated-row-instant/new-or-updated-row-instant.mjs b/components/zoho_sheet/sources/new-or-updated-row-instant/new-or-updated-row-instant.mjs new file mode 100644 index 0000000000000..645345c7f31e3 --- /dev/null +++ b/components/zoho_sheet/sources/new-or-updated-row-instant/new-or-updated-row-instant.mjs @@ -0,0 +1,54 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "zoho_sheet-new-or-updated-row-instant", + name: "New or Updated Row (Instant)", + description: "Emit new event whenever a row is added or modified.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + workbookId: { + propDefinition: [ + common.props.zohoSheet, + "workbookId", + ], + }, + worksheetId: { + propDefinition: [ + common.props.zohoSheet, + "worksheet", + ({ workbookId }) => ({ + workbookId, + }), + ], + withLabel: true, + }, + alert: { + type: "alert", + alertType: "info", + content: "**New row** will be triggered only after the entire row is completed.", + }, + }, + methods: { + ...common.methods, + getEvent() { + return "update_worksheet"; + }, + getExtraData() { + return { + resource_id: this.workbookId, + worksheet_id: this.worksheetId.value, + }; + }, + getSummary({ updated_rows }) { + return `Row ${updated_rows[0].row_type === "NEW" + ? "created" + : "updated"} in worksheet ${this.worksheetId.label}`; + }, + }, + sampleEmit, +}; diff --git a/components/zoho_sheet/sources/new-or-updated-row-instant/test-event.mjs b/components/zoho_sheet/sources/new-or-updated-row-instant/test-event.mjs new file mode 100644 index 0000000000000..23b39924bf5be --- /dev/null +++ b/components/zoho_sheet/sources/new-or-updated-row-instant/test-event.mjs @@ -0,0 +1,17 @@ +export default { + "updated_rows" : [ + { + "Name": "John", + "Age": 24, + "Marks": 34, + "row_index": 3, + "row_type": "update" + } + ], + "header_row_index": 1, + "start_column": 1, + "end_column": 3, + "webhook_id": "", + "service_name": "", + "event": "update_worksheet" +} \ No newline at end of file diff --git a/components/zoho_sheet/sources/new-row-instant/new-row-instant.mjs b/components/zoho_sheet/sources/new-row-instant/new-row-instant.mjs new file mode 100644 index 0000000000000..7502ed121f841 --- /dev/null +++ b/components/zoho_sheet/sources/new-row-instant/new-row-instant.mjs @@ -0,0 +1,47 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "zoho_sheet-new-row-instant", + name: "New Row Created (Instant)", + description: "Emit new event each time a new row is created in a Zoho Sheet worksheet.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + workbookId: { + propDefinition: [ + common.props.zohoSheet, + "workbookId", + ], + }, + worksheetId: { + propDefinition: [ + common.props.zohoSheet, + "worksheet", + ({ workbookId }) => ({ + workbookId, + }), + ], + withLabel: true, + }, + }, + methods: { + ...common.methods, + getEvent() { + return "new_row"; + }, + getExtraData() { + return { + resource_id: this.workbookId, + worksheet_id: this.worksheetId.value, + }; + }, + getSummary() { + return `New row in worksheet ${this.worksheetId.label}`; + }, + }, + sampleEmit, +}; diff --git a/components/zoho_sheet/sources/new-row-instant/test-event.mjs b/components/zoho_sheet/sources/new-row-instant/test-event.mjs new file mode 100644 index 0000000000000..42946d13d2528 --- /dev/null +++ b/components/zoho_sheet/sources/new-row-instant/test-event.mjs @@ -0,0 +1,17 @@ +export default { + "updated_rows" : [ + { + "Name": "John", + "Age": 24, + "Marks": 34, + "row_index": 3, + "row_type": "update" + } + ], + "header_row_index": 1, + "start_column": 1, + "end_column": 3, + "webhook_id": "", + "service_name": "", + "event": "new_row" +} \ No newline at end of file diff --git a/components/zoho_sheet/sources/new-workbook-instant/new-workbook-instant.mjs b/components/zoho_sheet/sources/new-workbook-instant/new-workbook-instant.mjs new file mode 100644 index 0000000000000..7de4a80469a0d --- /dev/null +++ b/components/zoho_sheet/sources/new-workbook-instant/new-workbook-instant.mjs @@ -0,0 +1,22 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "zoho_sheet-new-workbook-instant", + name: "New Workbook Created (Instant)", + description: "Emit new event whenever a new workbook is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return "new_workbook"; + }, + getSummary(event) { + return `New workbook: ${event.workbook_name} (${event.resource_id})`; + }, + }, + sampleEmit, +}; diff --git a/components/zoho_sheet/sources/new-workbook-instant/test-event.mjs b/components/zoho_sheet/sources/new-workbook-instant/test-event.mjs new file mode 100644 index 0000000000000..f591adf0eb33b --- /dev/null +++ b/components/zoho_sheet/sources/new-workbook-instant/test-event.mjs @@ -0,0 +1,8 @@ +export default { + "workbook_url": "", + "resource_id": "", + "workbook_name": "", + "webhook_id": "", + "service_name": "", + "event": "new_workbook" +} \ No newline at end of file diff --git a/components/zoho_sheet/zoho_sheet.app.mjs b/components/zoho_sheet/zoho_sheet.app.mjs new file mode 100644 index 0000000000000..12e2617345c12 --- /dev/null +++ b/components/zoho_sheet/zoho_sheet.app.mjs @@ -0,0 +1,147 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "zoho_sheet", + propDefinitions: { + workbookId: { + type: "string", + label: "Worksheet", + description: "The ID of the workbook (Spreadsheet)", + async options() { + const { workbooks } = await this.listWorkbooks({}); + return workbooks.map(({ + resource_id: value, workbook_name: label, + }) => ({ + label, + value, + })); + }, + }, + worksheet: { + type: "string", + label: "Worksheet", + description: "The ID of the worksheet (Sheet of the Spreadsheet)", + async options({ workbookId }) { + const { worksheet_names: worksheets } = await this.listWorksheets({ + workbookId, + }); + return worksheets.map(({ + worksheet_id: value, worksheet_name: label, + }) => ({ + label, + value, + })); + }, + }, + data: { + type: "string[]", + label: "JSON Data", + description: "An array of objects of the data for the row content, including the column headers as keys. **Example: {\"Name\":\"Joe\",\"Region\":\"South\",\"Units\":284}**", + }, + headerRow: { + type: "integer", + label: "Header Row", + description: "By default, first row of the worksheet is considered as header row. This can be used if tabular data starts from any row other than the first row..", + }, + criteria: { + type: "string", + label: "Criteria", + description: "Conditions to locate the row to delete. **Example: \"Month\"=\"March\" and \"Amount\">50**", + }, + firstMatchOnly: { + type: "boolean", + label: "First Match Only", + description: "If true and if there are multiple records on the specified criteria, records will be deleted for first match alone. Otherwise, all the matched records will be deleted. This parameter will be ignored if criteria is not mentioned.", + default: false, + }, + isCaseSensitive: { + type: "boolean", + label: "Is Case Sensitive", + description: "Can be set as false for case insensitive search.", + default: true, + }, + }, + methods: { + _baseUrl() { + return `https://sheet.${this.$auth.base_api_uri}/api/v2`; + }, + _headers() { + return { + "Authorization": `Zoho-oauthtoken ${this.$auth.oauth_access_token}`, + "Content-Type": "application/x-www-form-urlencoded", + }; + }, + _makeRequest({ + $ = this, path, data, method, ...opts + }) { + return axios($, { + method: "POST", + url: this._baseUrl() + path, + headers: this._headers(), + data: { + ...data, + method, + }, + ...opts, + }); + }, + listWorkbooks(opts = {}) { + return this._makeRequest({ + path: "/workbooks", + method: "workbook.list", + ...opts, + }); + }, + listWorksheets({ + workbookId, ...opts + }) { + return this._makeRequest({ + path: `/${workbookId}`, + method: "worksheet.list", + ...opts, + }); + }, + createRow({ + workbookId, ...opts + }) { + return this._makeRequest({ + path: `/${workbookId}`, + method: "worksheet.records.add", + ...opts, + }); + }, + deleteRow({ + workbookId, ...opts + }) { + return this._makeRequest({ + path: `/${workbookId}`, + method: "worksheet.records.delete", + ...opts, + }); + }, + updateRow({ + workbookId, ...opts + }) { + return this._makeRequest({ + path: `/${workbookId}`, + method: "worksheet.records.update", + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + path: "/webhook", + method: "webhook.subscribe", + ...opts, + }); + }, + deleteWebhook(opts = {}) { + return this._makeRequest({ + path: "/webhook", + method: "webhook.unsubscribe", + ...opts, + }); + }, + }, +}; diff --git a/components/zoho_sign/README.md b/components/zoho_sign/README.md new file mode 100644 index 0000000000000..f45585cbdc6b8 --- /dev/null +++ b/components/zoho_sign/README.md @@ -0,0 +1,23 @@ +# Overview + +Zoho Sign API lets you automate document signing processes. Integrate with Pipedream to create or manage documents, send them for signatures, and track status updates in real-time. Sync data with other apps, trigger workflows based on document actions, or automate follow-ups once signatures are received. + +# Example Use Cases + +- **Automate Document Workflow**: Upon a new sale in your CRM, automatically generate a sales contract using Zoho Sign, send it to the customer for signing, and receive notifications when the document is signed. + +- **Document Status Tracking**: Set up a workflow to monitor document status on Zoho Sign. When a document gets signed or declined, update the status in a Google Sheet and notify relevant team members via Slack. + +- **Contract Renewal Reminders**: Use Zoho Sign to detect contracts nearing expiry. Trigger emails to customers with renewal links and pre-filled documents for hassle-free re-signing. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_sprints/README.md b/components/zoho_sprints/README.md new file mode 100644 index 0000000000000..7b801e2d45b26 --- /dev/null +++ b/components/zoho_sprints/README.md @@ -0,0 +1,23 @@ +# Overview + +Zoho Sprints is a versatile agile project management tool that enables teams to plan, track, and iterate their work in sprints. With the Zoho Sprints API, you can automate your agile workflows, sync projects across different platforms, and create custom dashboards to monitor progress. Using Pipedream, you can connect Zoho Sprints with hundreds of apps to streamline processes, trigger actions based on sprint changes, and manipulate sprint data programmatically. + +# Example Use Cases + +- **Automated Sprint Reporting**: Generate sprint reports automatically by connecting Zoho Sprints to a data visualization tool like Google Sheets. Whenever a sprint ends, Pipedream can trigger a workflow that compiles key metrics and populates them into a predefined Google Sheets template, giving stakeholders real-time insights. + +- **Sprint Change Notifications**: Stay updated with sprint changes by creating a workflow that sends real-time notifications. When a sprint is updated in Zoho Sprints, Pipedream can trigger a notification to be sent through Slack or email, ensuring that team members are always informed about the latest sprint developments. + +- **Issue Tracking Integration**: Integrate Zoho Sprints with an external issue tracking system like Jira. When a new issue is created in Jira, Pipedream can automate the creation of a corresponding work item in Zoho Sprints, keeping your backlogs synchronized and making cross-platform collaboration seamless. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_subscriptions/README.md b/components/zoho_subscriptions/README.md new file mode 100644 index 0000000000000..545d96742a14b --- /dev/null +++ b/components/zoho_subscriptions/README.md @@ -0,0 +1,23 @@ +# Overview + +The Zoho Subscriptions API allows you to manage various aspects of subscription-based billing services. With this API, you can automate tasks such as creating subscriptions, handling customer billing info, and managing invoices. In Pipedream, you can harness this API to build workflows that respond to events in Zoho Subscriptions or to perform actions based on triggers from other apps. This enables seamless automation of billing operations and integration with your broader app ecosystem. + +# Example Use Cases + +- **Automate Invoice Reminders**: Use Zoho Subscriptions to monitor subscription statuses and send invoice reminders to customers with pending payments. Combine this with an email service like SendGrid to automate the reminder process. + +- **Sync New Subscribers to CRM**: When a new subscriber is added in Zoho Subscriptions, automatically add their details to a CRM system, like Salesforce or HubSpot, for sales follow-up and customer relationship management. + +- **Create Slack Notifications for Subscription Changes**: Set up a workflow where any update to a subscription in Zoho Subscriptions triggers a notification in a designated Slack channel. This can keep your team informed about new sign-ups, cancellations, or payment issues. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_subscriptions/actions/create-customer/create-customer.mjs b/components/zoho_subscriptions/actions/create-customer/create-customer.mjs index be5f8533f5e96..bd78d6dd473d6 100644 --- a/components/zoho_subscriptions/actions/create-customer/create-customer.mjs +++ b/components/zoho_subscriptions/actions/create-customer/create-customer.mjs @@ -3,8 +3,8 @@ import zohoSubscriptions from "../../zoho_subscriptions.app.mjs"; export default { key: "zoho_subscriptions-create-customer", name: "Create Customer", - version: "0.0.1", - description: "Create a new customer. [See the documentation](https://www.zoho.com/subscriptions/api/v1/customers/#create-a-customer)", + version: "0.0.2", + description: "Create a new customer. [See the documentation](https://www.zoho.com/billing/api/v1/customers/#create-a-customer)", type: "action", props: { zohoSubscriptions, @@ -80,97 +80,97 @@ export default { }, billingAddressAttention: { type: "string", - label: "Attention", + label: "Billing Address - Attention", description: "Attention of the customer's billing address.", optional: true, }, billingAddressStreet: { type: "string", - label: "Street", + label: "Billing Address - Street", description: "Street of the customer's billing address.", optional: true, }, billingAddressCity: { type: "string", - label: "City", + label: "Billing Address - City", description: "City of the customer's billing address.", optional: true, }, billingAddressState: { type: "string", - label: "State", + label: "Billing Address - State", description: "State of the customer's billing address.", optional: true, }, billingAddressZip: { type: "string", - label: "Zip", + label: "Billing Address - Zip", description: "Zip of the customer's billing address.", optional: true, }, billingAddressCountry: { type: "string", - label: "Country", + label: "Billing Address - Country", description: "Country of the customer's billing address.", optional: true, }, billingAddressStateCode: { type: "string", - label: "State Code", + label: "Billing Address - State Code", description: "State Code of the customer's billing address.", optional: true, }, billingAddressFax: { type: "string", - label: "Fax", + label: "Billing Address - Fax", description: "Fax of the customer's billing address.", optional: true, }, shippingAddressAttention: { type: "string", - label: "Attention", + label: "Shipping Address - Attention", description: "Attention of the customer's shipping address.", optional: true, }, shippingAddressStreet: { type: "string", - label: "Street", + label: "Shipping Address - Street", description: "Street of the customer's shipping address.", optional: true, }, shippingAddressCity: { type: "string", - label: "City", + label: "Shipping Address - City", description: "City of the customer's shipping address.", optional: true, }, shippingAddressState: { type: "string", - label: "State", + label: "Shipping Address - State", description: "State of the customer's shipping address.", optional: true, }, shippingAddressZip: { type: "string", - label: "Zip", + label: "Shipping Address - Zip", description: "Zip of the customer's shipping address.", optional: true, }, shippingAddressCountry: { type: "string", - label: "Country", + label: "Shipping Address - Country", description: "Country of the customer's shipping address.", optional: true, }, shippingAddressStateCode: { type: "string", - label: "State Code", + label: "Shipping Address - State Code", description: "State Code of the customer's shipping address.", optional: true, }, shippingAddressFax: { type: "string", - label: "Fax", + label: "Shipping Address - Fax", description: "Fax of the customer's shipping address.", optional: true, }, diff --git a/components/zoho_subscriptions/actions/create-subscription/create-subscription.mjs b/components/zoho_subscriptions/actions/create-subscription/create-subscription.mjs index 83e4de39cc071..64dbd3423b41a 100644 --- a/components/zoho_subscriptions/actions/create-subscription/create-subscription.mjs +++ b/components/zoho_subscriptions/actions/create-subscription/create-subscription.mjs @@ -4,8 +4,8 @@ import zohoSubscriptions from "../../zoho_subscriptions.app.mjs"; export default { key: "zoho_subscriptions-create-subscription", name: "Create Subscription", - version: "0.0.1", - description: "Create a new subscription. [See the documentation](https://www.zoho.com/subscriptions/api/v1/subscription/#create-a-subscription)", + version: "0.0.2", + description: "Create a new subscription. [See the documentation](https://www.zoho.com/billing/api/v1/subscription/#create-a-subscription)", type: "action", props: { zohoSubscriptions, @@ -29,6 +29,7 @@ export default { organizationId, }), ], + reloadProps: true, }, paymentTerms: { propDefinition: [ @@ -322,6 +323,35 @@ export default { optional: true, }, }, + async additionalProps(existingProps) { + const props = {}; + if (!this.customerId) { + return props; + } + try { + const { customer } = await this.zohoSubscriptions.getCustomer({ + customerId: this.customerId, + }); + const { contact_person: contactperson } = await this.zohoSubscriptions.getContactPerson({ + customerId: this.customerId, + contactpersonId: customer.primary_contactperson_id, + }); + if (!customer.email && !contactperson.email) { + existingProps.contactpersons.hidden = true; + props.contactEmail = { + type: "string", + label: "Contact Email", + }; + } + } catch { + props.contactEmail = { + type: "string", + label: "Contact Email", + optional: true, + }; + } + return props; + }, async run({ $ }) { const { zohoSubscriptions, @@ -367,14 +397,15 @@ export default { cfdiUsage, allowPartialPayments, accountId, + contactEmail, } = this; if (autoCollect && !cardId) { - throw new ConfigurationError("By setting Auto-Collect to `true`, you must to fill in the card Id."); + throw new ConfigurationError("If setting Auto-Collect to `true`, you must fill in the card Id."); } if (autoCollect && !accountId) { - throw new ConfigurationError("By setting Auto-Collect to `true`, you must to fill in the Account Id."); + throw new ConfigurationError("If setting Auto-Collect to `true`, you must fill in the Account Id."); } let exchangeRateFloat = null; @@ -404,6 +435,27 @@ export default { } } + const { customer } = await zohoSubscriptions.getCustomer({ + customerId, + }); + if (contactEmail) { + await zohoSubscriptions.updateCustomer({ + customerId, + data: { + display_name: customer.display_name, + email: contactEmail, + }, + }); + } else { + const { contact_person: contactperson } = await zohoSubscriptions.getContactPerson({ + customerId, + contactpersonId: customer.primary_contactperson_id, + }); + if (!customer.email && !contactperson.email) { + throw new ConfigurationError("Customer must have an email address"); + } + } + const response = await zohoSubscriptions.createSubscription({ $, organizationId, diff --git a/components/zoho_subscriptions/package.json b/components/zoho_subscriptions/package.json index 6ab6dc147349b..5638c7d5ff781 100644 --- a/components/zoho_subscriptions/package.json +++ b/components/zoho_subscriptions/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/zoho_subscriptions", - "version": "0.1.0", + "version": "0.1.1", "description": "Pipedream Zoho Subscriptions Components", "main": "zoho_subscriptions.app.mjs", "keywords": [ @@ -13,6 +13,7 @@ "access": "public" }, "dependencies": { + "@pipedream/platform": "^3.0.3", "moment": "^2.29.4" } } diff --git a/components/zoho_subscriptions/sources/payment-failure/payment-failure.mjs b/components/zoho_subscriptions/sources/payment-failure/payment-failure.mjs index 075a280f000be..04ec2e056303b 100644 --- a/components/zoho_subscriptions/sources/payment-failure/payment-failure.mjs +++ b/components/zoho_subscriptions/sources/payment-failure/payment-failure.mjs @@ -5,7 +5,7 @@ import zohoSubscriptions from "../../zoho_subscriptions.app.mjs"; export default { key: "zoho_subscriptions-payment-failure", name: "New Payment Failure", - version: "0.0.1", + version: "0.0.2", description: "Emit new event when a payment fails to process.", type: "source", dedupe: "unique", diff --git a/components/zoho_subscriptions/sources/subscription-created/subscription-created.mjs b/components/zoho_subscriptions/sources/subscription-created/subscription-created.mjs index 582b3774da36b..ed2da52f00052 100644 --- a/components/zoho_subscriptions/sources/subscription-created/subscription-created.mjs +++ b/components/zoho_subscriptions/sources/subscription-created/subscription-created.mjs @@ -5,7 +5,7 @@ import zohoSubscriptions from "../../zoho_subscriptions.app.mjs"; export default { key: "zoho_subscriptions-subscription-created", name: "New Subscription Created", - version: "0.0.1", + version: "0.0.2", description: "Emit new event when a new subscription is created.", type: "source", dedupe: "unique", diff --git a/components/zoho_subscriptions/zoho_subscriptions.app.mjs b/components/zoho_subscriptions/zoho_subscriptions.app.mjs index c806b505c00bc..1b6500f8ed732 100644 --- a/components/zoho_subscriptions/zoho_subscriptions.app.mjs +++ b/components/zoho_subscriptions/zoho_subscriptions.app.mjs @@ -8,7 +8,7 @@ export default { cardId: { type: "string", label: "Card Id", - description: "Enter the card Id of the card which has to be updated..", + description: "Enter the card Id of the card which is to be updated.", async options({ page, customerId, organizationId, }) { @@ -134,7 +134,7 @@ export default { }, methods: { _apiUrl() { - return `${this.$auth.api_domain}/subscriptions/v1`; + return `${this.$auth.api_domain}/billing/v1`; }, _getHeaders(organizationId = null) { const headers = { @@ -157,6 +157,22 @@ export default { return axios($, clearObj(config)); }, + getCustomer({ + customerId, ...args + }) { + return this._makeRequest({ + path: `customers/${customerId}`, + ...args, + }); + }, + getContactPerson({ + customerId, contactpersonId, ...args + }) { + return this._makeRequest({ + path: `customers/${customerId}/contactpersons/${contactpersonId}`, + ...args, + }); + }, createCustomer(args = {}) { return this._makeRequest({ method: "POST", @@ -164,6 +180,15 @@ export default { ...args, }); }, + updateCustomer({ + customerId, ...args + }) { + return this._makeRequest({ + method: "PUT", + path: `customers/${customerId}`, + ...args, + }); + }, createSubscription(args = {}) { return this._makeRequest({ method: "POST", diff --git a/components/zoho_survey/README.md b/components/zoho_survey/README.md new file mode 100644 index 0000000000000..9b5e3543b2b14 --- /dev/null +++ b/components/zoho_survey/README.md @@ -0,0 +1,23 @@ +# Overview + +The Zoho Survey API enables you to integrate your surveys and their data with other applications, automating your workflows and allowing you to manage surveys and responses efficiently. Within Pipedream, you can harness this API to trigger workflows, process survey responses, create or update surveys, and sync your survey data with other services. This streamlined interaction facilitates real-time data collection and analysis, leading to informed decision-making and proactive engagement with your audience. + +# Example Use Cases + +- **Automate Response Collection to Google Sheets**: Collect survey responses in real-time and automatically add them to a Google Sheets spreadsheet. This is ideal for data analysis and sharing insights with teams who depend on quick access to fresh data. + +- **Send Survey Invitations via Email or SMS**: Use Pipedream to trigger an action whenever a new contact is added to your CRM like Salesforce. The workflow would send an automated email or SMS invitation to participate in a Zoho Survey to the new contact, ensuring consistent engagement with prospective clients or stakeholders. + +- **Sync Survey Data with Slack**: Set up a workflow where new survey responses are instantly posted to a Slack channel. This keeps your team updated on customer feedback or employee satisfaction without them needing to leave their communication hub. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_survey/actions/send-email-invitation/send-email-invitation.mjs b/components/zoho_survey/actions/send-email-invitation/send-email-invitation.mjs index 215e897a6245d..91bd5b5a8a93b 100644 --- a/components/zoho_survey/actions/send-email-invitation/send-email-invitation.mjs +++ b/components/zoho_survey/actions/send-email-invitation/send-email-invitation.mjs @@ -4,7 +4,7 @@ export default { key: "zoho_survey-send-email-invitation", name: "Send Email Invitation", description: "Sends an email invitation with Zoho Survey.", - version: "0.0.1", + version: "0.0.4", type: "action", props: { zohoSurvey, diff --git a/components/zoho_survey/package.json b/components/zoho_survey/package.json index b00232ad03920..6c52f3c1a0ff2 100644 --- a/components/zoho_survey/package.json +++ b/components/zoho_survey/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/zoho_survey", - "version": "0.1.0", + "version": "0.1.5", "description": "Pipedream Zoho Survey Components", "main": "zoho_survey.app.mjs", "keywords": [ @@ -13,6 +13,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^1.5.1" + "@pipedream/platform": "^3.0.3", + "html-entities-decoder": "^1.0.5" } } diff --git a/components/zoho_survey/sources/common/base.mjs b/components/zoho_survey/sources/common/base.mjs index d28303ef9daee..edac5b5fa0ca6 100644 --- a/components/zoho_survey/sources/common/base.mjs +++ b/components/zoho_survey/sources/common/base.mjs @@ -30,6 +30,11 @@ export default { }), ], }, + triggerName: { + type: "string", + label: "Trigger Name", + description: "Trigger name to be displayed within Zoho Survey (Maximum 255 characters)", + }, }, hooks: { async activate() { @@ -38,7 +43,7 @@ export default { groupId: this.groupId, surveyId: this.surveyId, data: { - name: "Pipedream Webhook", + name: this.triggerName, event: this.getEvent(), callbackUrl: this.http.endpoint, responseContentType: "json", @@ -68,13 +73,17 @@ export default { _setHookId(hookId) { this.db.set("hookId", hookId); }, + formatResponse(body) { + return body; + }, generateMeta() { throw new Error("generateMeta is not implemented"); }, }, async run(event) { const { body } = event; - const meta = this.generateMeta(body); - this.$emit(body, meta); + const response = await this.formatResponse(body); + const meta = this.generateMeta(response); + this.$emit(response, meta); }, }; diff --git a/components/zoho_survey/sources/new-survey-response/new-survey-response.mjs b/components/zoho_survey/sources/new-survey-response/new-survey-response.mjs index 1436439d24ad6..a1e4c452e466c 100644 --- a/components/zoho_survey/sources/new-survey-response/new-survey-response.mjs +++ b/components/zoho_survey/sources/new-survey-response/new-survey-response.mjs @@ -1,4 +1,5 @@ import common from "../common/base.mjs"; +import decode from "html-entities-decoder"; import sampleEmit from "./test-event.mjs"; export default { @@ -6,7 +7,7 @@ export default { key: "zoho_survey-new-survey-response", name: "New Survey Response (Instant)", description: "Emit new event when a new survey response is received in Zoho Surveys.", - version: "0.0.1", + version: "0.0.6", type: "source", dedupe: "unique", methods: { @@ -15,12 +16,77 @@ export default { return "response_completed"; }, generateMeta(response) { + const ts = Date.now(); return { - id: response.RESPONSE_ID, - summary: `New Response ${response.RESPONSE_ID}`, - ts: Date.now(), + id: `${response["RESPONSE_ID"].value}${ts}`, + summary: `New Response ${response["RESPONSE_ID"].value}`, + ts, }; }, + collectFieldLabels(obj) { + const labels = {}; + function recursiveSearch(obj, primaryLabel) { + if (Array.isArray(obj)) { + obj.forEach((question) => recursiveSearch(question, primaryLabel)); + } + if ("variables" in obj) { + recursiveSearch(obj.variables, `${primaryLabel + ? primaryLabel + " - " + : ""}${obj.label + ? obj.label + : ""}`); + } + if ("label" in obj && "key" in obj) { + labels[obj.key] = `${primaryLabel + ? primaryLabel + " - " + : ""}${obj.label}`; + } + } + recursiveSearch(obj); + return labels; + }, + formatValue(value) { + return typeof value === "string" + ? decode(value) + : Array.isArray(value) + ? value.map((v) => v + ? decode(v) + : "") + : ""; + }, + async formatResponse(body) { + const { variables } = await this.zohoSurvey.listSurveyFields({ + portalId: this.portalId, + groupId: this.groupId, + surveyId: this.surveyId, + }); + const questions = variables.flatMap((v) => v.variables); + const labels = this.collectFieldLabels(questions); + const response = {}; + for (const [ + key, + value, + ] of Object.entries(body)) { + response[key] = labels[key] + ? { + label: decode(labels[key]), + value: this.formatValue(value), + } + : this.formatValue(value); + } + for (const [ + key, + value, + ] of Object.entries(labels)) { + if (!response[key]) { + response[key] = { + label: decode(value), + value: "", + }; + } + } + return response; + }, }, sampleEmit, }; diff --git a/components/zoho_survey/sources/new-survey-response/test-event.mjs b/components/zoho_survey/sources/new-survey-response/test-event.mjs index 3507124572e86..fe58d49841f49 100644 --- a/components/zoho_survey/sources/new-survey-response/test-event.mjs +++ b/components/zoho_survey/sources/new-survey-response/test-event.mjs @@ -1,11 +1,46 @@ export default { - "COLLECTOR_NAME": "eCommerce", - "RESPONSE_STATUS": "COMPLETED", - "Q-A": "Yes", - "COLLECTOR_ID": "ShdJpz", - "RESPONSE_STARTTIME": "Nov 30, 2023 09:51:19", - "RESPONSE_ID": "x7DjTWhf", - "IP_ADDRESS": "47.225.32.185", - "RESPONSE_ENDTIME": "Nov 30, 2023 09:51:21", - "RESPONSE_TIMETAKEN": "2 secs " + "COLLECTOR_NAME": { + "label": "Collector Name", + "value": "Customer Satisfaction Survey" + }, + "RESPONSE_STATUS": { + "label": "Response Status", + "value": "COMPLETED" + }, + "Q-A": { + "label": "What is your name?", + "value": "Pipedream" + }, + "COLLECTOR_ID": { + "label": "Collector ID", + "value": "MUDHdQ" + }, + "RESPONSE_STARTTIME": { + "label": "Response Start Time", + "value": "Aug 02, 2024 12:37:11" + }, + "RESPONSE_ID": { + "label": "Response ID", + "value": "3cD3Ky2Z" + }, + "IP_ADDRESS": { + "label": "IP Address", + "value": "47.225.32.185" + }, + "RESPONSE_ENDTIME": { + "label": "Response End Time", + "value": "Aug 02, 2024 12:37:29" + }, + "RESPONSE_TIMETAKEN": { + "label": "Time Taken to Respond", + "value": "17 secs " + }, + "Q-B": { + "label": "Email address", + "value": "" + }, + "RESPONSE_URL": { + "label": "Response URL", + "value": "" + } } \ No newline at end of file diff --git a/components/zoho_survey/zoho_survey.app.mjs b/components/zoho_survey/zoho_survey.app.mjs index 2b0e255c5944a..fceda26cc59bb 100644 --- a/components/zoho_survey/zoho_survey.app.mjs +++ b/components/zoho_survey/zoho_survey.app.mjs @@ -1,4 +1,5 @@ import { axios } from "@pipedream/platform"; +import decode from "html-entities-decoder"; const DEFAULT_LIMIT = 50; export default { @@ -15,14 +16,14 @@ export default { portalId: value, portalName: label, }) => ({ value, - label, + label: decode(label), })) || []; }, }, groupId: { type: "string", - label: "Group", - description: "Unique identifier of a group/department", + label: "Department", + description: "Unique identifier of a department", async options({ portalId }) { const portals = await this.listPortals(); const portal = portals.find((portal) => portal.portalId === portalId); @@ -30,7 +31,7 @@ export default { groupUniqueId: value, name: label, }) => ({ value, - label, + label: decode(label), })); }, }, @@ -43,6 +44,7 @@ export default { }) { const limit = DEFAULT_LIMIT; const params = { + filterby: "published", offset: limit, range: (limit * page) + 1, }; @@ -55,7 +57,7 @@ export default { id: value, name: label, }) => ({ value, - label, + label: decode(label), })) || []; }, }, @@ -75,13 +77,13 @@ export default { id: value, name: label, }) => ({ value, - label, + label: decode(label), })) || []; }, }, distributionId: { type: "string", - label: "Distribution", + label: "Trigger Invitation", description: "Identifier of a distribution", async options({ portalId, groupId, surveyId, collectorId, @@ -99,7 +101,7 @@ export default { id: value, name: label, }) => ({ value, - label, + label: decode(label), })) || []; }, }, @@ -169,6 +171,11 @@ export default { }) { return this._makeRequest({ path: `/portals/${portalId}/departments/${groupId}/surveys/${surveyId}/collectors/metainfo`, + params: { + ...args?.params, + status: "open", + fromservice: "pipedream", + }, ...args, }); }, @@ -197,5 +204,13 @@ export default { ...args, }); }, + listSurveyFields({ + portalId, groupId, surveyId, ...args + }) { + return this._makeRequest({ + path: `/portals/${portalId}/departments/${groupId}/surveys/${surveyId}/integration/trigger/variables`, + ...args, + }); + }, }, }; diff --git a/components/zoho_tables/package.json b/components/zoho_tables/package.json new file mode 100644 index 0000000000000..e2676395ce3b1 --- /dev/null +++ b/components/zoho_tables/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/zoho_tables", + "version": "0.0.1", + "description": "Pipedream Zoho Tables Components", + "main": "zoho_tables.app.mjs", + "keywords": [ + "pipedream", + "zoho_tables" + ], + "homepage": "https://pipedream.com/apps/zoho_tables", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/zoho_tables/zoho_tables.app.mjs b/components/zoho_tables/zoho_tables.app.mjs new file mode 100644 index 0000000000000..42dfb670457a5 --- /dev/null +++ b/components/zoho_tables/zoho_tables.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "zoho_tables", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; diff --git a/components/zoho_workdrive/README.md b/components/zoho_workdrive/README.md index 577d5e2f307b4..e1f82671e6c8f 100644 --- a/components/zoho_workdrive/README.md +++ b/components/zoho_workdrive/README.md @@ -1,23 +1,23 @@ # Overview -Zoho WorkDrive API is a powerful tool for managing and sharing content -collaboration, storage and documents online. With the API, you can build -solutions to handle document organization, store, process and share enterprise -data securely, in a cloud-based environment. Here are a few examples of what -you can do with the API: - -- Create and store new documents, spreadsheets, presentations and other files -- Automate document sharing and processing -- Architecture document storage solutions to suit your organization -- Access and sync documents and folders across multiple devices -- Integrate with other third-party applications -- Create and share link-based documents and folders -- Add metadata to documents and files in the cloud -- Share documents with users or groups -- Focus content search results with filters -- Secure documents with user authentication -- Enable authentication policies and control access -- Monitor user activity to ensure compliance -- Create automated backup for documents and files -- Track changes and version control for documents and files -- Create reports and analytics for documents and folders +The Zoho WorkDrive API interacts with Zoho's cloud-based file management system, enabling automated file and folder operations, team management, and content collaboration. With Pipedream, you can harness this API to create workflows that trigger on specific events, manipulate files and folders, and integrate with other services for a seamless productivity boost. + +# Example Use Cases + +- **Automated Data Backup**: Use Pipedream to monitor a specific folder in Zoho WorkDrive for new files. When a new file is detected, automatically back it up to an external storage service like Dropbox or Google Drive, ensuring redundancy and data safety. + +- **Content Approval Pipeline**: Create a workflow where documents uploaded to a 'Pending Approval' folder in WorkDrive trigger an approval request in Slack to designated team members. Once approved, move the document to an 'Approved' folder and notify the team via email through a service like SendGrid. + +- **Team Onboarding**: Set up an onboarding workflow that creates a new folder structure in Zoho WorkDrive for each new team member added in an HR platform like BambooHR. Populate the folders with necessary onboarding documents and share access with the new member, streamlining the onboarding process. + +# Troubleshooting + +**Issues Connecting My Account: IP Allowlist** + +If your Zoho security policy includes an IP Allowlist, update it to connect your account: + +1. Add this IP Range: `44.223.89.56` - `44.223.89.63`. +2. Include your current IP Address because the initial OAuth authorization request originates from your browser. +3. Set up a [Virtual Private Cloud (VPC) on Pipedream](https://pipedream.com/docs/workflows/vpc#create-a-new-vpc). Add the [static IP address](https://pipedream.com/docs/workflows/vpc#find-the-static-outbound-ip-address-for-a-vpc) to Zoho's IP Allowlist. Note: VPCs are available with Pipedream's **Business Plan** or higher. See [pricing](https://pipedream.com/pricing) for details. + +After connecting your account, make sure to [run the workflow within a VPC](https://pipedream.com/docs/workflows/vpc#run-workflows-within-a-vpc). \ No newline at end of file diff --git a/components/zoho_workdrive/actions/download-file/download-file.mjs b/components/zoho_workdrive/actions/download-file/download-file.mjs index be1657194eadc..9080869675251 100644 --- a/components/zoho_workdrive/actions/download-file/download-file.mjs +++ b/components/zoho_workdrive/actions/download-file/download-file.mjs @@ -7,7 +7,7 @@ export default { key: "zoho_workdrive-download-file", name: "Download File to Tmp Direcory", description: "Download a file to the /tmp directory. [See the documentation](https://workdrive.zoho.com/apidocs/v1/filesfolders/downloadserverfile)", - version: "0.0.1", + version: "0.0.3", type: "action", props: { app, @@ -17,12 +17,21 @@ export default { "teamId", ], }, + folderType: { + propDefinition: [ + app, + "folderType", + ], + }, folderId: { propDefinition: [ app, "parentId", - ({ teamId }) => ({ + ({ + teamId, folderType, + }) => ({ teamId, + folderType, }), ], label: "Folder Id", @@ -69,11 +78,12 @@ export default { }, }, async run({ $ }) { - const fileName = this.fileName || this.fileId.label; + const fileId = this.fileId?.value ?? this.fileId; + const fileName = this.fileName ?? this.fileId?.label ?? "file"; const filePath = getFilePath(fileName); const fileContent = await this.downloadFile({ - fileId: this.fileId.value, + fileId, }); fs.writeFileSync(filePath, fileContent); diff --git a/components/zoho_workdrive/actions/upload-file/upload-file.mjs b/components/zoho_workdrive/actions/upload-file/upload-file.mjs index 07d941a928e9f..4024bfbf34555 100644 --- a/components/zoho_workdrive/actions/upload-file/upload-file.mjs +++ b/components/zoho_workdrive/actions/upload-file/upload-file.mjs @@ -7,7 +7,7 @@ import app from "../../zoho_workdrive.app.mjs"; export default { key: "zoho_workdrive-upload-file", name: "Upload File", - version: "0.0.2", + version: "0.0.3", description: "Upload a new file to your WorkDrive account. [See the documentation](https://workdrive.zoho.com/apidocs/v1/chunkupload/chunkuploadcreatesession)", type: "action", props: { @@ -18,12 +18,21 @@ export default { "teamId", ], }, + folderType: { + propDefinition: [ + app, + "folderType", + ], + }, parentId: { propDefinition: [ app, "parentId", - ({ teamId }) => ({ + ({ + teamId, folderType, + }) => ({ teamId, + folderType, }), ], }, diff --git a/components/zoho_workdrive/package.json b/components/zoho_workdrive/package.json index f935d215a702f..d361bdf9bb8b2 100644 --- a/components/zoho_workdrive/package.json +++ b/components/zoho_workdrive/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/zoho_workdrive", - "version": "0.2.0", + "version": "0.2.2", "description": "Pipedream Zoho WorkDrive Components", "main": "zoho_workdrive.app.mjs", "keywords": [ diff --git a/components/zoho_workdrive/sources/new-file-in-folder/new-file-in-folder.mjs b/components/zoho_workdrive/sources/new-file-in-folder/new-file-in-folder.mjs index 4903ae67edbe7..040f34a92f1e9 100644 --- a/components/zoho_workdrive/sources/new-file-in-folder/new-file-in-folder.mjs +++ b/components/zoho_workdrive/sources/new-file-in-folder/new-file-in-folder.mjs @@ -5,7 +5,7 @@ import sampleEmit from "./test-event.mjs"; export default { key: "zoho_workdrive-new-file-in-folder", name: "New File In Folder", - version: "0.0.2", + version: "0.0.3", description: "Emit new event when a new file is created in a specific folder.", type: "source", dedupe: "unique", @@ -26,12 +26,21 @@ export default { "teamId", ], }, + folderType: { + propDefinition: [ + app, + "folderType", + ], + }, folderId: { propDefinition: [ app, "parentId", - ({ teamId }) => ({ + ({ + teamId, folderType, + }) => ({ teamId, + folderType, }), ], label: "Folder Id", @@ -52,6 +61,7 @@ export default { } = this; const lastDate = this._getLastDate(); + let maxDate = lastDate; const items = app.paginate({ fn: app.listFiles, maxResults, @@ -63,10 +73,15 @@ export default { let responseArray = []; for await (const item of items) { - if (new Date(item.created_time) <= new Date(lastDate)) break; - responseArray.push(item); + const createdTime = item.attributes.created_time; + if (new Date(createdTime) > new Date(lastDate)) { + responseArray.push(item); + if (new Date(createdTime) > new Date(maxDate)) { + maxDate = createdTime; + } + } } - if (responseArray.length) this._setLastDate(responseArray[0].created_time); + this._setLastDate(maxDate); for (const item of responseArray) { this.$emit( @@ -74,7 +89,7 @@ export default { { id: item.id, summary: `A new file with id: "${item.id}" was created!`, - ts: item.created_time, + ts: item.attributes.created_time, }, ); } diff --git a/components/zoho_workdrive/sources/new-folder/new-folder.mjs b/components/zoho_workdrive/sources/new-folder/new-folder.mjs index cd709242f5efa..6fd5fc6b6dddf 100644 --- a/components/zoho_workdrive/sources/new-folder/new-folder.mjs +++ b/components/zoho_workdrive/sources/new-folder/new-folder.mjs @@ -5,7 +5,7 @@ import sampleEmit from "./test-event.mjs"; export default { key: "zoho_workdrive-new-folder", name: "New Folder", - version: "0.0.2", + version: "0.0.3", description: "Emit new event when a new folder is created in a specific folder.", type: "source", dedupe: "unique", @@ -26,12 +26,21 @@ export default { "teamId", ], }, + folderType: { + propDefinition: [ + app, + "folderType", + ], + }, folderId: { propDefinition: [ app, "parentId", - ({ teamId }) => ({ + ({ + teamId, folderType, + }) => ({ teamId, + folderType, }), ], label: "Folder Id", @@ -52,6 +61,7 @@ export default { } = this; const lastDate = this._getLastDate(); + let maxDate = lastDate; const items = app.paginate({ fn: app.listFiles, maxResults, @@ -63,10 +73,15 @@ export default { let responseArray = []; for await (const item of items) { - if (new Date(item.created_time) <= new Date(lastDate)) break; - responseArray.push(item); + const createdTime = item.attributes.created_time; + if (new Date(createdTime) > new Date(lastDate)) { + responseArray.push(item); + if (new Date(createdTime) > new Date(maxDate)) { + maxDate = createdTime; + } + } } - if (responseArray.length) this._setLastDate(responseArray[0].created_time); + this._setLastDate(maxDate); for (const item of responseArray) { this.$emit( @@ -74,7 +89,7 @@ export default { { id: item.id, summary: `A new folder with id: "${item.id}" was created!`, - ts: item.created_time, + ts: item.attributes.created_time, }, ); } diff --git a/components/zoho_workdrive/zoho_workdrive.app.mjs b/components/zoho_workdrive/zoho_workdrive.app.mjs index 945fccc4badac..fb5cb2dd28152 100644 --- a/components/zoho_workdrive/zoho_workdrive.app.mjs +++ b/components/zoho_workdrive/zoho_workdrive.app.mjs @@ -28,15 +28,27 @@ export default { label: "Parent Id", description: "The unique ID of the folder where files are to be uploaded.", async options({ - page, teamId, + page, teamId, folderType, }) { return await this.listRootFolders({ + folderType, teamId, limit: LIMIT, offset: LIMIT * page, }); }, }, + folderType: { + type: "string", + label: "Folder Type", + description: "Whether to retrieve team folders or privatespace folders", + async options() { + return [ + "My Folders", + "Team Folders", + ]; + }, + }, }, methods: { _apiUrl(path, params = "") { @@ -94,7 +106,7 @@ export default { }); }, async listRootFolders({ - teamId, limit, offset, params = {}, ...args + teamId, folderType, limit, offset, params = {}, ...args }) { const { data: { id: teamCurrentUserId } } = await this.getTeamCurrentUser({ teamId, @@ -112,7 +124,9 @@ export default { params["sort"] = "name"; const reponseArray = []; const { data: rootFolders } = await this._makeRequest({ - path: `privatespace/${privateSpaceId}/folders`, + path: folderType === "Team Folders" + ? `/teams/${teamId}/teamfolders` + : `privatespace/${privateSpaceId}/folders`, params: new URLSearchParams(params).toString(), ...args, }); diff --git a/components/zonka_feedback/README.md b/components/zonka_feedback/README.md index 23170fa8ad3e6..1d3f4c715540c 100644 --- a/components/zonka_feedback/README.md +++ b/components/zonka_feedback/README.md @@ -1,20 +1,11 @@ # Overview -With the Zonka Feedback API, you can create powerful tools for collecting -customer feedback. Here are just a few examples of the types of things you can -build with their API: +Zonka Feedback API allows for the collection and management of customer feedback data. Through Pipedream, you can automate the process of capturing this feedback, analyze the data for insights, and integrate with other services to act on the information collected. With Pipedream's serverless platform, you can create complex workflows that trigger actions in other apps, based on feedback received, without writing extensive code. -- Create a customer feedback questionaire for your products or services -- Build an in-store experience feedback form -- Create customer satisfaction surveys -- Integrate customer feedback with your existing CRM system -- Collect employee engagement and satisfaction feedback -- Develop real-time customer support systems -- Put together custom analytics reports -- Develop customer satisfaction scorecards -- Create custom satisfaction scoring models -- Create customer feedback trend reports -- Create surveys for product or service launches -- Design customer loyalty systems -- Monitor sales and customer experiences to pinpoint areas for improvement -- Develop customer segmentation and targeting models +# Example Use Cases + +- **Automated Feedback Analysis and Reporting**: Set up a workflow that triggers whenever new feedback is received via Zonka. Automatically analyze the sentiment of the feedback using a natural language processing service like Google Cloud Natural Language API. Summarize the results and send a weekly report via email with Gmail or SMTP by Pipedream. + +- **Real-time Feedback Notification**: Create a workflow that immediately notifies the customer service team on Slack or sends an SMS via Twilio when a customer leaves negative feedback. This enables your team to take swift action, potentially turning a negative experience into a positive one. + +- **Feedback-Driven Task Creation**: Construct a workflow that creates a new task in a project management tool like Trello or Asana when specific feedback criteria are met. For example, if feedback mentions a bug or feature request, automatically create a card or task so the product team can prioritize it in their workflow. diff --git a/components/zoom/README.md b/components/zoom/README.md index 48d0d539fabce..f2f06175181de 100644 --- a/components/zoom/README.md +++ b/components/zoom/README.md @@ -1,14 +1,9 @@ # Overview - -Connecting Zoom to any app using Pipedream +The Zoom API lets you tap into a rich set of functionalities to enhance the video conferencing experience within your own app or workflow. With the Zoom API on Pipedream, you can automatically create meetings, manage users, send meeting notifications, and more, orchestrating these actions within a broader automation. This allows for seamless integration with other services, enabling both data collection and action triggers based on Zoom events. **Pipedream [workflows](/workflows/) allow you to run any Node.js code that connects to the Zoom API**. Just [create a new workflow](https://pipedream.com/new), then add prebuilt Zoom [actions](/components#actions) (create a meeting, send a chat message, etc.) or [write your own code](/code/). These workflows can be triggered by HTTP requests, timers, email, or on any app-based event (new tweets, a Github PR, Zoom events, etc). - - - - -## Getting Started +# Getting Started 1. First, sign up for Pipedream at [https://pipedream.com](https://pipedream.com). 2. Visit [https://pipedream.com/accounts](https://pipedream.com/accounts). @@ -18,11 +13,9 @@ This will open up a new window prompting you to authorize Pipedream's access to your Zoom account. Once you authorize access, you should see your Zoom account listed among your apps. 1. [Create a new workflow](https://pipedream.com/new), [add a new step](/workflows/steps/), search for "Zoom" or "Zoom Admin". Once you've selected either app, you can choose to either "Run Node.js code" or select one of the prebuilt actions for performing common API operations. -2. At this stage, you'll be asked to link the Zoom account you connected above, authorizing the request to the Zoom API with your credentials: +2. At this stage, you'll be asked to link the Zoom account you connected above, authorizing the request to the Zoom API with your credentials. -Connect Zoom Account - -### Zoom vs Zoom Admin app +## Zoom vs Zoom Admin app Zoom users can be classified into two groups: non-admins and admins. Admins have account-level permissions that users do not, and Zoom has corresponding admin-level scopes that aren't relevant for normal users. Therefore, Pipedream exposes two apps — **Zoom** and **Zoom Admin** — to serve the two groups. @@ -33,3 +26,11 @@ Non-admins have [permissions](https://marketplace.zoom.us/docs/guides/authorizat Zoom admins have [permissions](https://marketplace.zoom.us/docs/guides/authorization/permissions#account-level-scopes) to manage account-level resources, like users and reports. They can also manage webinars and meetings across their organization. **If you're an admin and need to manage these resources via API, you'll want to use the Zoom Admin app**. The [Zoom API docs on permissions](https://marketplace.zoom.us/docs/guides/authorization/permissions) provide detailed information on these permissions and their associated OAuth scopes. + +# Example Use Cases + +- **Automated Meeting Scheduling and Notifications**: With Pipedream, you can create a workflow that listens for upcoming calendar events in Google Calendar. Once it detects a new event labeled "Zoom Meeting," it can trigger the Zoom API to create a meeting and then automatically send custom email notifications with the meeting details to all the attendees using SendGrid. + +- **Zoom Webinar Attendee Management**: Build a workflow where new sign-ups from an event management platform like Eventbrite trigger the addition of these attendees to a Zoom webinar. Post-webinar, send a follow-up email via Mailgun with a link to the webinar recording, which you can upload to a cloud storage platform like Dropbox. + +- **Meeting Analytics and Reporting**: Combine Zoom's meeting ended webhook with Pipedream's capabilities to create a workflow that captures meeting details upon conclusion. With this data, you can send a summary email to the host, update a Google Sheet with attendance information, and even push the data to a BI tool like Tableau for more in-depth analysis. \ No newline at end of file diff --git a/components/zoom/actions/add-meeting-registrant/add-meeting-registrant.mjs b/components/zoom/actions/add-meeting-registrant/add-meeting-registrant.mjs index 968af7856c43d..7dcf8652c73cc 100644 --- a/components/zoom/actions/add-meeting-registrant/add-meeting-registrant.mjs +++ b/components/zoom/actions/add-meeting-registrant/add-meeting-registrant.mjs @@ -4,7 +4,7 @@ export default { key: "zoom-add-meeting-registrant", name: "Add Meeting Registrant", description: "Registers a participant for a meeting. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/methods/#operation/meetingRegistrantCreate)", - version: "0.3.0", + version: "0.3.3", type: "action", props: { app, diff --git a/components/zoom/actions/add-webinar-registrant/add-webinar-registrant.mjs b/components/zoom/actions/add-webinar-registrant/add-webinar-registrant.mjs index b2e179a3e6e4b..795954d9082ef 100644 --- a/components/zoom/actions/add-webinar-registrant/add-webinar-registrant.mjs +++ b/components/zoom/actions/add-webinar-registrant/add-webinar-registrant.mjs @@ -4,7 +4,7 @@ export default { key: "zoom-add-webinar-registrant", name: "Add Webinar Registrant", description: "Registers a participant for a webinar. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinarregistrantcreate).", - version: "0.3.0", + version: "0.3.3", type: "action", props: { app, diff --git a/components/zoom/actions/create-meeting/create-meeting.mjs b/components/zoom/actions/create-meeting/create-meeting.mjs index 08175aba6d39b..9b7c916de2810 100644 --- a/components/zoom/actions/create-meeting/create-meeting.mjs +++ b/components/zoom/actions/create-meeting/create-meeting.mjs @@ -5,7 +5,7 @@ export default { key: "zoom-create-meeting", name: "Create Meeting", description: "Creates a meeting for a user. A maximum of 100 meetings can be created for a user in a day.", - version: "0.1.1", + version: "0.1.4", type: "action", props: { zoom: { diff --git a/components/zoom/actions/create-user/create-user.mjs b/components/zoom/actions/create-user/create-user.mjs index fe24e092d87c1..51589d52c94a1 100644 --- a/components/zoom/actions/create-user/create-user.mjs +++ b/components/zoom/actions/create-user/create-user.mjs @@ -5,7 +5,7 @@ export default { key: "zoom-create-user", name: "Create User", description: "Creates a new user in your account.", - version: "0.2.1", + version: "0.2.4", type: "action", props: { zoom: { diff --git a/components/zoom/actions/delete-user/delete-user.mjs b/components/zoom/actions/delete-user/delete-user.mjs index 7c4b6e5f8d3eb..6c2c02865cb9b 100644 --- a/components/zoom/actions/delete-user/delete-user.mjs +++ b/components/zoom/actions/delete-user/delete-user.mjs @@ -5,7 +5,7 @@ export default { key: "zoom-delete-user", name: "Delete User", description: "Disassociates (unlinks) a user from the associated account or permanently deletes a user.", - version: "0.2.1", + version: "0.2.4", type: "action", props: { zoom: { diff --git a/components/zoom/actions/get-meeting-details/get-meeting-details.mjs b/components/zoom/actions/get-meeting-details/get-meeting-details.mjs index 3e409abaaf558..43c90ac28435b 100644 --- a/components/zoom/actions/get-meeting-details/get-meeting-details.mjs +++ b/components/zoom/actions/get-meeting-details/get-meeting-details.mjs @@ -5,7 +5,7 @@ export default { key: "zoom-get-meeting-details", name: "Get Meeting Details", description: "Retrieves the details of a meeting.", - version: "0.3.1", + version: "0.3.4", type: "action", props: { zoom: { diff --git a/components/zoom/actions/get-webinar-details/get-webinar-details.mjs b/components/zoom/actions/get-webinar-details/get-webinar-details.mjs index 7dcbaa163fe88..7a1bb41da51f0 100644 --- a/components/zoom/actions/get-webinar-details/get-webinar-details.mjs +++ b/components/zoom/actions/get-webinar-details/get-webinar-details.mjs @@ -4,7 +4,7 @@ export default { key: "zoom-get-webinar-details", name: "Get Webinar Details", description: "Gets details of a scheduled webinar. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/methods/#operation/webinar).", - version: "0.3.0", + version: "0.3.3", type: "action", props: { app, diff --git a/components/zoom/actions/list-call-recordings/list-call-recordings.mjs b/components/zoom/actions/list-call-recordings/list-call-recordings.mjs new file mode 100644 index 0000000000000..33ed8a47ef7b9 --- /dev/null +++ b/components/zoom/actions/list-call-recordings/list-call-recordings.mjs @@ -0,0 +1,67 @@ +import zoom from "../../zoom.app.mjs"; + +export default { + name: "List Call Recordings", + description: "Get your account's call recordings. [See the documentation](https://developers.zoom.us/docs/api/rest/reference/phone/methods/#operation/getPhoneRecordings)", + key: "zoom-list-call-recordings", + version: "0.0.1", + type: "action", + props: { + zoom, + infoBox: { + type: "alert", + alertType: "info", + content: "The Zoom API returns calls from the last 30 days by default. You can use the `Start Date` and `End Date` props to change this.", + }, + startDate: { + type: "string", + label: "Start Date", + description: "The start date/time in `yyyy-mm-dd` or `yyyy-MM-ddTHH:mm:ssZ` format. If `End Date` is not specified, calls made within a 30-day period starting on `Start Date` will be retrieved.", + optional: true, + }, + endDate: { + type: "string", + label: "End Date", + description: "The end date/time in `yyyy-mm-dd` or `yyyy-MM-ddTHH:mm:ssZ` format. Calls made after `Start Date` and before `End Date` will be retrieved. Date range should be a maximum of 30 days.", + optional: true, + }, + max: { + propDefinition: [ + zoom, + "max", + ], + default: 30, + min: 1, + max: 300, + }, + }, + async run ({ $ }) { + const { + zoom, startDate, endDate, max, + } = this; + let to = endDate; + if (startDate && !endDate) { + const date = new Date(startDate); + if (isNaN(date.valueOf())) { + throw new Error("Invalid format for `Start Date`. Please use `yyyy-mm-dd` or `yyyy-MM-ddTHH:mm:ssZ`."); + } + date.setDate(date.getDate() - 30); + to = date.toISOString(); + if (!startDate.split("T")[1]) { + to = to.split("T")[0]; + } + } + const { recordings } = await zoom.listCallRecordings({ + step: $, + params: { + page_size: max, + from: startDate, + to, + }, + }); + + $.export("$summary", `Successfully fetched ${recordings.length} call recordings`); + + return recordings; + }, +}; diff --git a/components/zoom/actions/list-channels/list-channels.mjs b/components/zoom/actions/list-channels/list-channels.mjs index d397ec8ca10de..3b4fd19ca3caa 100644 --- a/components/zoom/actions/list-channels/list-channels.mjs +++ b/components/zoom/actions/list-channels/list-channels.mjs @@ -5,7 +5,7 @@ export default { key: "zoom-list-channels", name: "List Channels", description: "List a user's chat channels.", - version: "0.1.1", + version: "0.1.4", type: "action", props: { zoom: { diff --git a/components/zoom/actions/list-past-meeting-participants/list-past-meeting-participants.mjs b/components/zoom/actions/list-past-meeting-participants/list-past-meeting-participants.mjs index 443c2d4ca3620..0ce260757b655 100644 --- a/components/zoom/actions/list-past-meeting-participants/list-past-meeting-participants.mjs +++ b/components/zoom/actions/list-past-meeting-participants/list-past-meeting-participants.mjs @@ -4,7 +4,7 @@ export default { key: "zoom-list-past-meeting-participants", name: "List Past Meeting Participants", description: "Retrieve information on participants from a past meeting. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/methods/#operation/pastMeetingParticipants).", - version: "0.2.0", + version: "0.2.3", type: "action", props: { app, diff --git a/components/zoom/actions/list-past-webinar-qa/list-past-webinar-qa.mjs b/components/zoom/actions/list-past-webinar-qa/list-past-webinar-qa.mjs index 4119b9c927367..e68c2835bc92b 100644 --- a/components/zoom/actions/list-past-webinar-qa/list-past-webinar-qa.mjs +++ b/components/zoom/actions/list-past-webinar-qa/list-past-webinar-qa.mjs @@ -5,7 +5,7 @@ export default { key: "zoom-list-past-webinar-qa", name: "List Past Webinar Q&A", description: "The feature for Webinars allows attendees to ask questions during the Webinar and for the panelists, co-hosts and host to answer their questions. Use this API to list Q&A of a specific Webinar.", - version: "0.1.1", + version: "0.1.4", type: "action", props: { zoom: { diff --git a/components/zoom/actions/list-user-call-logs/list-user-call-logs.mjs b/components/zoom/actions/list-user-call-logs/list-user-call-logs.mjs new file mode 100644 index 0000000000000..ed1e019071576 --- /dev/null +++ b/components/zoom/actions/list-user-call-logs/list-user-call-logs.mjs @@ -0,0 +1,31 @@ +import zoom from "../../zoom.app.mjs"; + +export default { + name: "List User's Call Logs", + description: "Gets a user's Zoom phone call logs. [See the documentation](https://developers.zoom.us/docs/zoom-phone/apis/#operation/phoneUserCallLogs)", + key: "zoom-list-user-call-logs", + version: "0.0.3", + type: "action", + props: { + zoom, + userId: { + propDefinition: [ + zoom, + "userId", + ], + }, + }, + async run ({ $ }) { + const data = await this.zoom.getResourcesStream({ + resourceFn: this.zoom.listCallLogs, + resourceFnArgs: { + userId: this.userId, + }, + resourceName: "call_logs", + }); + + $.export("$summary", `Successfully fetched ${data.length} call log(s)`); + + return data; + }, +}; diff --git a/components/zoom/actions/list-webinar-participants-report/list-webinar-participants-report.mjs b/components/zoom/actions/list-webinar-participants-report/list-webinar-participants-report.mjs index d07bf86075382..c2c45609732c3 100644 --- a/components/zoom/actions/list-webinar-participants-report/list-webinar-participants-report.mjs +++ b/components/zoom/actions/list-webinar-participants-report/list-webinar-participants-report.mjs @@ -1,11 +1,11 @@ -import app from "../../zoom.app.mjs"; import utils from "../../common/utils.mjs"; +import app from "../../zoom.app.mjs"; export default { key: "zoom-list-webinar-participants-report", name: "List Webinar Participants Report", description: "Retrieves detailed report on each webinar attendee. You can get webinar participant reports for the last 6 months. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/methods/#operation/reportWebinarParticipants).", - version: "0.0.1", + version: "0.0.4", type: "action", props: { app, diff --git a/components/zoom/actions/send-chat-message/send-chat-message.mjs b/components/zoom/actions/send-chat-message/send-chat-message.mjs index dc338018e2a44..ea0b793c900d7 100644 --- a/components/zoom/actions/send-chat-message/send-chat-message.mjs +++ b/components/zoom/actions/send-chat-message/send-chat-message.mjs @@ -5,7 +5,7 @@ export default { key: "zoom-send-chat-message", name: "Send Chat Message", description: "Send chat messages on Zoom to either an individual user who is in your contact list or to a of which you are a member.", - version: "0.1.1", + version: "0.1.4", type: "action", props: { zoom: { diff --git a/components/zoom/actions/update-meeting/update-meeting.mjs b/components/zoom/actions/update-meeting/update-meeting.mjs index 8e0d547431142..2cb8d6cfc1d00 100644 --- a/components/zoom/actions/update-meeting/update-meeting.mjs +++ b/components/zoom/actions/update-meeting/update-meeting.mjs @@ -5,7 +5,7 @@ export default { key: "zoom-update-meeting", name: "Update Meeting", description: "Updates an existing Zoom meeting", - version: "0.1.1", + version: "0.1.4", type: "action", props: { zoom: { diff --git a/components/zoom/actions/update-webinar/update-webinar.mjs b/components/zoom/actions/update-webinar/update-webinar.mjs index 072e2d6e7ffaf..a9f99f4dd39a2 100644 --- a/components/zoom/actions/update-webinar/update-webinar.mjs +++ b/components/zoom/actions/update-webinar/update-webinar.mjs @@ -5,7 +5,7 @@ export default { key: "zoom-update-webinar", name: "Update Webinar", description: "Update a webinar's topic, start time, or other settings", - version: "0.1.1", + version: "0.1.4", type: "action", props: { zoom: { diff --git a/components/zoom/actions/view-user/view-user.mjs b/components/zoom/actions/view-user/view-user.mjs index 14877c9294037..ef5c829ffdf7a 100644 --- a/components/zoom/actions/view-user/view-user.mjs +++ b/components/zoom/actions/view-user/view-user.mjs @@ -5,7 +5,7 @@ export default { key: "zoom-view-user", name: "View User", description: "View your user information", - version: "0.1.1", + version: "0.1.4", type: "action", props: { zoom: { diff --git a/components/zoom/package.json b/components/zoom/package.json index 9e0d81808c2c4..5b926e64ad543 100644 --- a/components/zoom/package.json +++ b/components/zoom/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/zoom", - "version": "0.4.0", + "version": "0.6.0", "description": "Pipedream Zoom Components", "main": "zoom.app.mjs", "keywords": [ diff --git a/components/zoom/sources/custom-event/custom-event.mjs b/components/zoom/sources/custom-event/custom-event.mjs index 86dfe464c3d85..4e49a9de0a773 100644 --- a/components/zoom/sources/custom-event/custom-event.mjs +++ b/components/zoom/sources/custom-event/custom-event.mjs @@ -6,7 +6,7 @@ export default { key: "zoom-custom-event", name: "Custom Events (Instant)", description: "Emit new events tied to your Zoom user or resources you own", - version: "0.1.0", + version: "0.1.3", type: "source", dedupe: "unique", props: { diff --git a/components/zoom/sources/meeting-created/meeting-created.mjs b/components/zoom/sources/meeting-created/meeting-created.mjs index 0ae5e4392acf4..01ad09d3271a5 100644 --- a/components/zoom/sources/meeting-created/meeting-created.mjs +++ b/components/zoom/sources/meeting-created/meeting-created.mjs @@ -6,7 +6,7 @@ export default { key: "zoom-meeting-created", name: "Meeting Created (Instant)", description: "Emit new event each time a meeting is created where you're the host", - version: "0.1.0", + version: "0.1.3", type: "source", dedupe: "unique", props: { diff --git a/components/zoom/sources/meeting-deleted/meeting-deleted.mjs b/components/zoom/sources/meeting-deleted/meeting-deleted.mjs index f2828972d3792..05a0964752c14 100644 --- a/components/zoom/sources/meeting-deleted/meeting-deleted.mjs +++ b/components/zoom/sources/meeting-deleted/meeting-deleted.mjs @@ -6,7 +6,7 @@ export default { key: "zoom-meeting-deleted", name: "Meeting Deleted (Instant)", description: "Emit new event each time a meeting is deleted where you're the host", - version: "0.1.0", + version: "0.1.3", type: "source", dedupe: "unique", props: { diff --git a/components/zoom/sources/meeting-ended/meeting-ended.mjs b/components/zoom/sources/meeting-ended/meeting-ended.mjs index 5adff1553cfef..9b81ad8009810 100644 --- a/components/zoom/sources/meeting-ended/meeting-ended.mjs +++ b/components/zoom/sources/meeting-ended/meeting-ended.mjs @@ -6,7 +6,7 @@ export default { key: "zoom-meeting-ended", name: "Meeting Ended (Instant)", description: "Emit new event each time a meeting ends where you're the host", - version: "0.1.0", + version: "0.1.3", type: "source", dedupe: "unique", props: { diff --git a/components/zoom/sources/meeting-started/meeting-started.mjs b/components/zoom/sources/meeting-started/meeting-started.mjs index dfd0b657a3a61..2b6d807d212e9 100644 --- a/components/zoom/sources/meeting-started/meeting-started.mjs +++ b/components/zoom/sources/meeting-started/meeting-started.mjs @@ -6,7 +6,7 @@ export default { key: "zoom-meeting-started", name: "Meeting Started (Instant)", description: "Emit new event each time a meeting starts where you're the host", - version: "0.1.0", + version: "0.1.4", type: "source", dedupe: "unique", props: { diff --git a/components/zoom/sources/meeting-updated/meeting-updated.mjs b/components/zoom/sources/meeting-updated/meeting-updated.mjs index 9acf436e88285..435b805ea8876 100644 --- a/components/zoom/sources/meeting-updated/meeting-updated.mjs +++ b/components/zoom/sources/meeting-updated/meeting-updated.mjs @@ -6,7 +6,7 @@ export default { key: "zoom-meeting-updated", name: "Meeting Updated (Instant)", description: "Emit new event each time a meeting is updated where you're the host", - version: "0.1.0", + version: "0.1.3", type: "source", dedupe: "unique", props: { diff --git a/components/zoom/sources/phone-event/phone-event.mjs b/components/zoom/sources/phone-event/phone-event.mjs index 882f223113a18..110dcedaa94f2 100644 --- a/components/zoom/sources/phone-event/phone-event.mjs +++ b/components/zoom/sources/phone-event/phone-event.mjs @@ -6,7 +6,7 @@ export default { key: "zoom-phone-event", name: "Zoom Phone Events (Instant)", description: "Emit new Zoom Phone event tied to your Zoom user or resources you own", - version: "0.1.0", + version: "0.1.3", type: "source", props: { ...common.props, diff --git a/components/zoom/sources/recording-completed/recording-completed.mjs b/components/zoom/sources/recording-completed/recording-completed.mjs index 93c0ac8c0d577..b69fcfa413961 100644 --- a/components/zoom/sources/recording-completed/recording-completed.mjs +++ b/components/zoom/sources/recording-completed/recording-completed.mjs @@ -6,7 +6,7 @@ export default { key: "zoom-recording-completed", name: "Recording Completed (Instant)", description: "Emit new event each time a new recording completes for a meeting or webinar where you're the host", - version: "0.1.0", + version: "0.1.3", type: "source", dedupe: "unique", props: { diff --git a/components/zoom/sources/webinar-created/webinar-created.mjs b/components/zoom/sources/webinar-created/webinar-created.mjs index 4668ff767ddf1..10a3acecaadd3 100644 --- a/components/zoom/sources/webinar-created/webinar-created.mjs +++ b/components/zoom/sources/webinar-created/webinar-created.mjs @@ -6,7 +6,7 @@ export default { key: "zoom-webinar-created", name: "Webinar Created (Instant)", description: "Emit new event each time a webinar is created where you're the host", - version: "0.1.0", + version: "0.1.3", type: "source", dedupe: "unique", props: { diff --git a/components/zoom/sources/webinar-deleted/webinar-deleted.mjs b/components/zoom/sources/webinar-deleted/webinar-deleted.mjs index 8226639386408..4cc07aa90a641 100644 --- a/components/zoom/sources/webinar-deleted/webinar-deleted.mjs +++ b/components/zoom/sources/webinar-deleted/webinar-deleted.mjs @@ -6,7 +6,7 @@ export default { key: "zoom-webinar-deleted", name: "Webinar Deleted (Instant)", description: "Emit new event each time a webinar is deleted where you're the host", - version: "0.1.0", + version: "0.1.3", type: "source", dedupe: "unique", props: { diff --git a/components/zoom/sources/webinar-ended/webinar-ended.mjs b/components/zoom/sources/webinar-ended/webinar-ended.mjs index 1ec441c0999c8..396ebbbf2c0fe 100644 --- a/components/zoom/sources/webinar-ended/webinar-ended.mjs +++ b/components/zoom/sources/webinar-ended/webinar-ended.mjs @@ -6,7 +6,7 @@ export default { key: "zoom-webinar-ended", name: "Webinar Ended (Instant)", description: "Emit new event each time a webinar ends where you're the host", - version: "0.1.0", + version: "0.1.3", type: "source", dedupe: "unique", props: { diff --git a/components/zoom/sources/webinar-started/webinar-started.mjs b/components/zoom/sources/webinar-started/webinar-started.mjs index b63503f0840ee..70de57c9b206b 100644 --- a/components/zoom/sources/webinar-started/webinar-started.mjs +++ b/components/zoom/sources/webinar-started/webinar-started.mjs @@ -6,7 +6,7 @@ export default { key: "zoom-webinar-started", name: "Webinar Started (Instant)", description: "Emit new event each time a webinar starts where you're the host", - version: "0.1.0", + version: "0.1.3", type: "source", dedupe: "unique", props: { diff --git a/components/zoom/sources/webinar-updated/webinar-updated.mjs b/components/zoom/sources/webinar-updated/webinar-updated.mjs index a1380e4b5297a..ed97346a4c062 100644 --- a/components/zoom/sources/webinar-updated/webinar-updated.mjs +++ b/components/zoom/sources/webinar-updated/webinar-updated.mjs @@ -6,7 +6,7 @@ export default { key: "zoom-webinar-updated", name: "Webinar Updated (Instant)", description: "Emit new event each time a webinar is updated where you're the host", - version: "0.1.0", + version: "0.1.3", type: "source", dedupe: "unique", props: { diff --git a/components/zoom/zoom.app.mjs b/components/zoom/zoom.app.mjs index d71b12eac66b5..b31436af7c60c 100644 --- a/components/zoom/zoom.app.mjs +++ b/components/zoom/zoom.app.mjs @@ -54,6 +54,31 @@ export default { }, optional: true, }, + userId: { + type: "string", + label: "User Id", + description: "The user ID or email address of the user.", + async options({ prevContext }) { + const { nextPageToken } = prevContext; + const response = await this.listUsers({ + params: { + next_page_token: nextPageToken, + }, + }); + + return { + options: response.users.map(({ + name, email, id: value, + }) => ({ + label: `${name} - ${email}`, + value, + })), + context: { + nextPageToken: response.next_page_token, + }, + }; + }, + }, includeAudioRecordings: { type: "boolean", label: "Include Audio Recordings", @@ -251,6 +276,26 @@ export default { ...args, }); }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/phone/users", + ...opts, + }); + }, + listCallLogs({ + userId, ...args + }) { + return this._makeRequest({ + path: `/phone/users/${userId}/call_logs`, + ...args, + }); + }, + listCallRecordings(args = {}) { + return this._makeRequest({ + path: "/phone/recordings", + ...args, + }); + }, async *getResourcesStream({ resourceFn, resourceFnArgs, diff --git a/components/zoom_admin/README.md b/components/zoom_admin/README.md index 1d7d8659fba39..3d30b51c24544 100644 --- a/components/zoom_admin/README.md +++ b/components/zoom_admin/README.md @@ -1,11 +1,34 @@ +# Overview + +The Zoom Admin API lets you harness the extensive capabilities of Zoom for automation and integration, right within Pipedream. Automate user management, track Zoom rooms, monitor webinars and meetings, and customize your workflow to respond dynamically to events like new participants or ended meetings. With these APIs and the power of Pipedream, you can streamline administrative tasks, extract valuable insights, and sync Zoom activities with other services. + +# Example Use Cases + +- **Automated User Provisioning and Deprovisioning**: Sync user data from your HR platform (e.g., BambooHR) with Zoom. When a new employee is added in BambooHR, automatically create a Zoom user account for them. Conversely, when an employee leaves, trigger a workflow to deactivate their Zoom account. + +- **Meeting Analytics and Reporting**: Connect Zoom Admin with a data visualization tool like Tableau. Each time a meeting ends, aggregate meeting statistics and send them to Tableau to create real-time dashboards that help management analyze usage patterns and meeting effectiveness. + +- **Webinar Attendee Follow-Up**: Link Zoom webinars with email platforms such as SendGrid. After a webinar ends, send a customized follow-up email to all attendees with a survey or additional resources, and update your CRM (like Salesforce) with attendee engagement data. + +# Getting Started +## Zoom vs Zoom Admin app + +Zoom users can be classified into two groups: non-admins and admins. Admins have account-level permissions that users do not, and Zoom has corresponding admin-level scopes that aren't relevant for normal users. Therefore, Pipedream exposes two apps — **Zoom** and **Zoom Admin** — to serve the two groups. + +In the Zoom Marketplace, these apps are named [Pipedream](https://marketplace.zoom.us/apps/jGaV-kRrT3igAYnn-J5v2g), and [Pipedream for Zoom Admins](https://marketplace.zoom.us/apps/tZvUsiucR96SqtvfBsemXg), respectively. + +Non-admins have [permissions](https://marketplace.zoom.us/docs/guides/authorization/permissions#user-managed-scopes) to manage standard Zoom resources in their account: meetings, webinars, recordings, and more. **If you're a non-admin, you'll want to use the Zoom app**. + +Zoom admins have [permissions](https://marketplace.zoom.us/docs/guides/authorization/permissions#account-level-scopes) to manage account-level resources, like users and reports. They can also manage webinars and meetings across their organization. **If you're an admin and need to manage these resources via API, you'll want to use the Zoom Admin app**. + +The [Zoom API docs on permissions](https://marketplace.zoom.us/docs/guides/authorization/permissions) provide detailed information on these permissions and their associated OAuth scopes. + ## Zoom Admin This directory contains [event sources](https://docs.pipedream.com/event-sources/) that operate on data from the [Zoom API](https://marketplace.zoom.us/docs/api-reference/introduction). **These event sources work with the Zoom Admin app in Pipedream**, specifically meant for Zoom admins operating on data across their account. Event sources let you turn any API into an event stream. For example, the [`recording-completed.js`](recording-completed.js) event source polls the Zoom API for new meeting or webinar recordings tied to your user, and [emits](https://github.com/PipedreamHQ/pipedream/blob/master/COMPONENT-API.md#emit) a new event for every new recording it finds. You can access these events in real-time using a [private SSE stream](https://docs.pipedream.com/api/sse/) tied to your source, or in batch using the [REST API](https://docs.pipedream.com/api/rest/). Or you can trigger [Pipedream workflows](#pipedream-workflows) on every new event. -[Read more in the Zoom + Pipedream docs](https://docs.pipedream.com/apps/zoom/). - ### Pipedream workflows You can trigger a [Pipedream workflow](https://docs.pipedream.com/workflows/) — hosted Node.js code — on every new event from any Zoom source. You can find a few example workflows below. diff --git a/components/zoom_admin/actions/add-meeting-registrant/add-meeting-registrant.mjs b/components/zoom_admin/actions/add-meeting-registrant/add-meeting-registrant.mjs index e69b9f642f4de..7b9838f7d56e2 100644 --- a/components/zoom_admin/actions/add-meeting-registrant/add-meeting-registrant.mjs +++ b/components/zoom_admin/actions/add-meeting-registrant/add-meeting-registrant.mjs @@ -1,13 +1,13 @@ -import zoomAdmin from "../../zoom_admin.app.mjs"; +import { axios } from "@pipedream/platform"; import get from "lodash/get.js"; import isArray from "lodash/isArray.js"; -import { axios } from "@pipedream/platform"; +import zoomAdmin from "../../zoom_admin.app.mjs"; export default { name: "Add meeting registrant", description: "Register a participant for a meeting. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingregistrantcreate)", key: "zoom_admin-add-meeting-registrant", - version: "0.1.3", + version: "0.1.5", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/add-webinar-panelist/add-webinar-panelist.mjs b/components/zoom_admin/actions/add-webinar-panelist/add-webinar-panelist.mjs index 84f02f403225a..88c40fac92d7f 100644 --- a/components/zoom_admin/actions/add-webinar-panelist/add-webinar-panelist.mjs +++ b/components/zoom_admin/actions/add-webinar-panelist/add-webinar-panelist.mjs @@ -1,12 +1,12 @@ -import zoomAdmin from "../../zoom_admin.app.mjs"; -import get from "lodash/get.js"; import { axios } from "@pipedream/platform"; +import get from "lodash/get.js"; +import zoomAdmin from "../../zoom_admin.app.mjs"; export default { name: "Add webinar panelist", description: "Register a panelist for a webinar. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinarpanelistcreate)", key: "zoom_admin-add-webinar-panelist", - version: "0.1.3", + version: "0.1.6", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/add-webinar-registrant/add-webinar-registrant.mjs b/components/zoom_admin/actions/add-webinar-registrant/add-webinar-registrant.mjs index 220ee4514c9b8..00b7bb7c353f1 100644 --- a/components/zoom_admin/actions/add-webinar-registrant/add-webinar-registrant.mjs +++ b/components/zoom_admin/actions/add-webinar-registrant/add-webinar-registrant.mjs @@ -1,13 +1,13 @@ -import zoomAdmin from "../../zoom_admin.app.mjs"; +import { axios } from "@pipedream/platform"; import get from "lodash/get.js"; import isArray from "lodash/isArray.js"; -import { axios } from "@pipedream/platform"; +import zoomAdmin from "../../zoom_admin.app.mjs"; export default { name: "Add webinar registrant", description: "Register a participant for a webinar. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinarregistrantcreate)", key: "zoom_admin-add-webinar-registrant", - version: "0.1.3", + version: "0.1.5", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/create-meeting/create-meeting.mjs b/components/zoom_admin/actions/create-meeting/create-meeting.mjs index 2a1c2d8ee49da..1a1c5117733ca 100644 --- a/components/zoom_admin/actions/create-meeting/create-meeting.mjs +++ b/components/zoom_admin/actions/create-meeting/create-meeting.mjs @@ -1,7 +1,7 @@ +import { axios } from "@pipedream/platform"; +import consts from "../../consts.mjs"; import zoomAdmin from "../../zoom_admin.app.mjs"; import tzs from "../../zoom_tzs.mjs"; -import consts from "../../consts.mjs"; -import { axios } from "@pipedream/platform"; const { MEETING_TYPE_OPTIONS } = consts; @@ -9,7 +9,7 @@ export default { name: "Create a meeting", description: "Create a new room in zoom. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate)", key: "zoom_admin-create-meeting", - version: "0.1.3", + version: "0.1.5", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/create-webinar/create-webinar.mjs b/components/zoom_admin/actions/create-webinar/create-webinar.mjs index 57f009172cadb..5840418f1319a 100644 --- a/components/zoom_admin/actions/create-webinar/create-webinar.mjs +++ b/components/zoom_admin/actions/create-webinar/create-webinar.mjs @@ -1,7 +1,7 @@ +import { axios } from "@pipedream/platform"; +import consts from "../../consts.mjs"; import zoomAdmin from "../../zoom_admin.app.mjs"; import tzs from "../../zoom_tzs.mjs"; -import consts from "../../consts.mjs"; -import { axios } from "@pipedream/platform"; const { RECURRENCE_TYPE_OPTIONS } = consts; @@ -9,7 +9,7 @@ export default { name: "Create Webinar", description: "Create a webinar for an user. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinarcreate)", key: "zoom_admin-create-webinar", - version: "0.1.3", + version: "0.1.5", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/delete-cloud-recording/delete-cloud-recording.mjs b/components/zoom_admin/actions/delete-cloud-recording/delete-cloud-recording.mjs index d40875cbd4e7d..18bcf06f74fe3 100644 --- a/components/zoom_admin/actions/delete-cloud-recording/delete-cloud-recording.mjs +++ b/components/zoom_admin/actions/delete-cloud-recording/delete-cloud-recording.mjs @@ -1,13 +1,13 @@ -import zoomAdmin from "../../zoom_admin.app.mjs"; -import isObject from "lodash/isObject.js"; import { axios } from "@pipedream/platform"; +import isObject from "lodash/isObject.js"; import consts from "../../consts.mjs"; +import zoomAdmin from "../../zoom_admin.app.mjs"; export default { name: "Delete Cloud Recording", description: "Remove a recording from a meeting or webinar. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/cloud-recording/recordingdeleteone)", key: "zoom_admin-delete-cloud-recording", - version: "0.1.3", + version: "0.1.5", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/delete-meeting/delete-meeting.mjs b/components/zoom_admin/actions/delete-meeting/delete-meeting.mjs index 9ef6f68d00672..74e7b44478083 100644 --- a/components/zoom_admin/actions/delete-meeting/delete-meeting.mjs +++ b/components/zoom_admin/actions/delete-meeting/delete-meeting.mjs @@ -1,12 +1,12 @@ -import zoomAdmin from "../../zoom_admin.app.mjs"; -import get from "lodash/get.js"; import { axios } from "@pipedream/platform"; +import get from "lodash/get.js"; +import zoomAdmin from "../../zoom_admin.app.mjs"; export default { name: "Delete meeting", description: "Delete a meeting. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingdelete)", key: "zoom_admin-delete-meeting", - version: "0.1.3", + version: "0.1.5", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/delete-webinar-panelist/delete-webinar-panelist.mjs b/components/zoom_admin/actions/delete-webinar-panelist/delete-webinar-panelist.mjs index d1d81e0cdcbbe..6e3564a9f92c7 100644 --- a/components/zoom_admin/actions/delete-webinar-panelist/delete-webinar-panelist.mjs +++ b/components/zoom_admin/actions/delete-webinar-panelist/delete-webinar-panelist.mjs @@ -1,12 +1,12 @@ -import zoomAdmin from "../../zoom_admin.app.mjs"; -import get from "lodash/get.js"; import { axios } from "@pipedream/platform"; +import get from "lodash/get.js"; +import zoomAdmin from "../../zoom_admin.app.mjs"; export default { name: "Delete webinar panelist", description: "Remove a panelist from a webinar. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinarpanelistdelete)", key: "zoom_admin-delete-webinar-panelist", - version: "0.1.3", + version: "0.1.5", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/delete-webinar/delete-webinar.mjs b/components/zoom_admin/actions/delete-webinar/delete-webinar.mjs index 7c72114fe0b44..5416b365831b5 100644 --- a/components/zoom_admin/actions/delete-webinar/delete-webinar.mjs +++ b/components/zoom_admin/actions/delete-webinar/delete-webinar.mjs @@ -1,12 +1,12 @@ -import zoomAdmin from "../../zoom_admin.app.mjs"; -import get from "lodash/get.js"; import { axios } from "@pipedream/platform"; +import get from "lodash/get.js"; +import zoomAdmin from "../../zoom_admin.app.mjs"; export default { name: "Delete webinar", description: "Delete a webinar. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinardelete)", key: "zoom_admin-delete-webinar", - version: "0.1.3", + version: "0.1.5", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/end-meeting/end-meeting.mjs b/components/zoom_admin/actions/end-meeting/end-meeting.mjs index 23c9ea2e94234..02446347a0c08 100644 --- a/components/zoom_admin/actions/end-meeting/end-meeting.mjs +++ b/components/zoom_admin/actions/end-meeting/end-meeting.mjs @@ -1,12 +1,12 @@ -import zoomAdmin from "../../zoom_admin.app.mjs"; -import get from "lodash/get.js"; import { axios } from "@pipedream/platform"; +import get from "lodash/get.js"; +import zoomAdmin from "../../zoom_admin.app.mjs"; export default { name: "End meeting", description: "End a meeting for a user. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingstatus)", key: "zoom_admin-end-meeting", - version: "0.1.3", + version: "0.1.5", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/get-meeting/get-meeting.mjs b/components/zoom_admin/actions/get-meeting/get-meeting.mjs index 6393e482fae59..076e414c40aac 100644 --- a/components/zoom_admin/actions/get-meeting/get-meeting.mjs +++ b/components/zoom_admin/actions/get-meeting/get-meeting.mjs @@ -1,12 +1,12 @@ -import zoomAdmin from "../../zoom_admin.app.mjs"; -import get from "lodash/get.js"; import { axios } from "@pipedream/platform"; +import get from "lodash/get.js"; +import zoomAdmin from "../../zoom_admin.app.mjs"; export default { name: "Get Meeting", description: "Retrieve the details of a meeting. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meeting)", key: "zoom_admin-get-meeting", - version: "0.1.4", + version: "0.1.6", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/get-webinar/get-webinar.mjs b/components/zoom_admin/actions/get-webinar/get-webinar.mjs index 2ee5224cccae3..c5e2b2042da65 100644 --- a/components/zoom_admin/actions/get-webinar/get-webinar.mjs +++ b/components/zoom_admin/actions/get-webinar/get-webinar.mjs @@ -1,12 +1,12 @@ -import zoomAdmin from "../../zoom_admin.app.mjs"; -import get from "lodash/get.js"; import { axios } from "@pipedream/platform"; +import get from "lodash/get.js"; +import zoomAdmin from "../../zoom_admin.app.mjs"; export default { name: "Get Webinar", description: "Retrieve the details of a webinar. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinar)", key: "zoom_admin-get-webinar", - version: "0.1.3", + version: "0.1.5", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/list-account-call-logs/list-account-call-logs.mjs b/components/zoom_admin/actions/list-account-call-logs/list-account-call-logs.mjs new file mode 100644 index 0000000000000..8be5c321acfac --- /dev/null +++ b/components/zoom_admin/actions/list-account-call-logs/list-account-call-logs.mjs @@ -0,0 +1,23 @@ +import { paginate } from "../../common/pagination.mjs"; +import zoomAdmin from "../../zoom_admin.app.mjs"; + +export default { + name: "List Account Call Logs", + description: "Returns an account's new edition call logs. [See the documentation](https://developers.zoom.us/docs/zoom-phone/apis/#operation/accountCallHistory)", + key: "zoom_admin-list-account-call-logs", + version: "0.0.2", + type: "action", + props: { + zoomAdmin, + }, + async run ({ $ }) { + const data = await paginate( + this.zoomAdmin.listAccountCallLogs, + "call_logs", + ); + + $.export("$summary", `Successfully fetched ${data.length} call log(s)`); + + return data; + }, +}; diff --git a/components/zoom_admin/actions/list-cloud-recordings/list-cloud-recordings.mjs b/components/zoom_admin/actions/list-cloud-recordings/list-cloud-recordings.mjs index e4917170d3735..ff5f7559b42ad 100644 --- a/components/zoom_admin/actions/list-cloud-recordings/list-cloud-recordings.mjs +++ b/components/zoom_admin/actions/list-cloud-recordings/list-cloud-recordings.mjs @@ -1,12 +1,12 @@ -import zoomAdmin from "../../zoom_admin.app.mjs"; -import consts from "../../consts.mjs"; import { paginate } from "../../common/pagination.mjs"; +import consts from "../../consts.mjs"; +import zoomAdmin from "../../zoom_admin.app.mjs"; export default { name: "List Cloud Recordings", description: "Search cloud recordings from a meeting or webinar. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/cloud-recording/recordingslist)", key: "zoom_admin-list-cloud-recordings", - version: "0.2.0", + version: "0.2.2", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/list-meeting-registrants/list-meeting-registrants.mjs b/components/zoom_admin/actions/list-meeting-registrants/list-meeting-registrants.mjs index 4faff23b1d667..5111e58a48f95 100644 --- a/components/zoom_admin/actions/list-meeting-registrants/list-meeting-registrants.mjs +++ b/components/zoom_admin/actions/list-meeting-registrants/list-meeting-registrants.mjs @@ -1,13 +1,13 @@ -import zoomAdmin from "../../zoom_admin.app.mjs"; import get from "lodash/get.js"; -import consts from "../../consts.mjs"; import { paginate } from "../../common/pagination.mjs"; +import consts from "../../consts.mjs"; +import zoomAdmin from "../../zoom_admin.app.mjs"; export default { name: "List meeting registrants", description: "List all users who have registered for a meeting. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingregistrants)", key: "zoom_admin-list-meeting-registrants", - version: "0.2.0", + version: "0.2.2", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/list-meetings/list-meetings.mjs b/components/zoom_admin/actions/list-meetings/list-meetings.mjs index 13ba8b89c33d4..2972359e6801d 100644 --- a/components/zoom_admin/actions/list-meetings/list-meetings.mjs +++ b/components/zoom_admin/actions/list-meetings/list-meetings.mjs @@ -1,12 +1,12 @@ -import zoomAdmin from "../../zoom_admin.app.mjs"; -import consts from "../../consts.mjs"; import { paginate } from "../../common/pagination.mjs"; +import consts from "../../consts.mjs"; +import zoomAdmin from "../../zoom_admin.app.mjs"; export default { name: "List meetings", description: "List all meetings. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetings)", key: "zoom_admin-list-meetings", - version: "0.2.0", + version: "0.2.2", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/list-webinar-participants/list-webinar-participants.mjs b/components/zoom_admin/actions/list-webinar-participants/list-webinar-participants.mjs index bebac057fb874..60a7db312ed98 100644 --- a/components/zoom_admin/actions/list-webinar-participants/list-webinar-participants.mjs +++ b/components/zoom_admin/actions/list-webinar-participants/list-webinar-participants.mjs @@ -5,7 +5,7 @@ export default { name: "List Webinar Participants", description: "Use this API to list all the participants who attended a webinar hosted in the past. [See the documentation](https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/listWebinarParticipants)", key: "zoom_admin-list-webinar-participants", - version: "0.2.0", + version: "0.2.2", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/list-webinar-registrants/list-webinar-registrants.mjs b/components/zoom_admin/actions/list-webinar-registrants/list-webinar-registrants.mjs index bbb12822ddbaf..902414a3d1b09 100644 --- a/components/zoom_admin/actions/list-webinar-registrants/list-webinar-registrants.mjs +++ b/components/zoom_admin/actions/list-webinar-registrants/list-webinar-registrants.mjs @@ -1,12 +1,12 @@ -import zoomAdmin from "../../zoom_admin.app.mjs"; -import consts from "../../consts.mjs"; import { paginate } from "../../common/pagination.mjs"; +import consts from "../../consts.mjs"; +import zoomAdmin from "../../zoom_admin.app.mjs"; export default { name: "List webinar registrants", description: "List all users that have registered for a webinar. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinarregistrants)", key: "zoom_admin-list-webinar-registrants", - version: "0.2.0", + version: "0.2.3", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/list-webinars/list-webinars.mjs b/components/zoom_admin/actions/list-webinars/list-webinars.mjs index ca4e03235eb52..463a310f3f7ef 100644 --- a/components/zoom_admin/actions/list-webinars/list-webinars.mjs +++ b/components/zoom_admin/actions/list-webinars/list-webinars.mjs @@ -5,7 +5,7 @@ export default { name: "List Webinars", description: "List all webinars for a user. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinars)", key: "zoom_admin-list-webinars", - version: "0.2.0", + version: "0.2.2", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/update-meeting/update-meeting.mjs b/components/zoom_admin/actions/update-meeting/update-meeting.mjs index e8b76493451ed..c65df35193fd4 100644 --- a/components/zoom_admin/actions/update-meeting/update-meeting.mjs +++ b/components/zoom_admin/actions/update-meeting/update-meeting.mjs @@ -1,8 +1,8 @@ -import zoomAdmin from "../../zoom_admin.app.mjs"; -import tzs from "../../zoom_tzs.mjs"; +import { axios } from "@pipedream/platform"; import get from "lodash/get.js"; import consts from "../../consts.mjs"; -import { axios } from "@pipedream/platform"; +import zoomAdmin from "../../zoom_admin.app.mjs"; +import tzs from "../../zoom_tzs.mjs"; const { MEETING_TYPE_OPTIONS } = consts; @@ -10,7 +10,7 @@ export default { name: "Update a meeting", description: "Update the details of a meeting. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingupdate)", key: "zoom_admin-update-meeting", - version: "0.1.3", + version: "0.1.5", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/update-webinar-registrant-status/update-webinar-registrant-status.mjs b/components/zoom_admin/actions/update-webinar-registrant-status/update-webinar-registrant-status.mjs index 03ec5a4f1c155..2f961620e5c67 100644 --- a/components/zoom_admin/actions/update-webinar-registrant-status/update-webinar-registrant-status.mjs +++ b/components/zoom_admin/actions/update-webinar-registrant-status/update-webinar-registrant-status.mjs @@ -1,14 +1,14 @@ -import zoomAdmin from "../../zoom_admin.app.mjs"; +import { axios } from "@pipedream/platform"; import get from "lodash/get.js"; import isObject from "lodash/isObject.js"; import consts from "../../consts.mjs"; -import { axios } from "@pipedream/platform"; +import zoomAdmin from "../../zoom_admin.app.mjs"; export default { name: "Update Webinar Registrant Status", description: "Update registrant status for a webinar. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingregistrantstatus)", key: "zoom_admin-update-webinar-registrant-status", - version: "0.1.3", + version: "0.1.5", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/actions/update-webinar/update-webinar.mjs b/components/zoom_admin/actions/update-webinar/update-webinar.mjs index 787f4a643c139..a81e71ddec7ec 100644 --- a/components/zoom_admin/actions/update-webinar/update-webinar.mjs +++ b/components/zoom_admin/actions/update-webinar/update-webinar.mjs @@ -1,8 +1,8 @@ -import zoomAdmin from "../../zoom_admin.app.mjs"; -import tzs from "../../zoom_tzs.mjs"; +import { axios } from "@pipedream/platform"; import get from "lodash/get.js"; import consts from "../../consts.mjs"; -import { axios } from "@pipedream/platform"; +import zoomAdmin from "../../zoom_admin.app.mjs"; +import tzs from "../../zoom_tzs.mjs"; const { RECURRENCE_TYPE_OPTIONS, @@ -13,7 +13,7 @@ export default { name: "Update Webinar", description: "Update the details of a webinar. [See the docs here](https://marketplace.zoom.us/docs/api-reference/zoom-api/webinars/webinarupdate)", key: "zoom_admin-update-webinar", - version: "0.1.3", + version: "0.1.5", type: "action", props: { zoomAdmin, diff --git a/components/zoom_admin/package.json b/components/zoom_admin/package.json index 20a3b5ca659a7..6e96fe1a2d039 100644 --- a/components/zoom_admin/package.json +++ b/components/zoom_admin/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/zoom_admin", - "version": "0.6.0", + "version": "0.7.2", "description": "Pipedream Zoom_admin Components", "main": "zoom_admin.app.js", "keywords": [ diff --git a/components/zoom_admin/sources/account-created/account-created.mjs b/components/zoom_admin/sources/account-created/account-created.mjs index faab46264081e..bc7cd9ac06551 100644 --- a/components/zoom_admin/sources/account-created/account-created.mjs +++ b/components/zoom_admin/sources/account-created/account-created.mjs @@ -5,7 +5,7 @@ export default { type: "source", name: "Account Created", description: "Emits an event each time a sub-account is created in your master account", - version: "0.1.3", + version: "0.1.5", dedupe: "unique", // Dedupe based on account ID props: { zoomAdmin, diff --git a/components/zoom_admin/sources/account-settings-updated/account-settings-updated.mjs b/components/zoom_admin/sources/account-settings-updated/account-settings-updated.mjs index a3a7f4a335e6c..b6098adf66de2 100644 --- a/components/zoom_admin/sources/account-settings-updated/account-settings-updated.mjs +++ b/components/zoom_admin/sources/account-settings-updated/account-settings-updated.mjs @@ -5,7 +5,7 @@ export default { type: "source", name: "Account Settings Updated", description: "Emits an event each time your master account or sub-account settings are updated", - version: "0.1.3", + version: "0.1.5", props: { zoomAdmin, zoomApphook: { diff --git a/components/zoom_admin/sources/account-updated/account-updated.mjs b/components/zoom_admin/sources/account-updated/account-updated.mjs index 32284afce9f00..54369b05d9e6d 100644 --- a/components/zoom_admin/sources/account-updated/account-updated.mjs +++ b/components/zoom_admin/sources/account-updated/account-updated.mjs @@ -5,7 +5,7 @@ export default { type: "source", name: "Account Updated", description: "Emits an event each time your master account or sub-account profile is updated", - version: "0.1.3", + version: "0.1.5", props: { zoomAdmin, zoomApphook: { diff --git a/components/zoom_admin/sources/custom-events/custom-events.mjs b/components/zoom_admin/sources/custom-events/custom-events.mjs index 1a468f9fd6d34..d7f47b27a3ebe 100644 --- a/components/zoom_admin/sources/custom-events/custom-events.mjs +++ b/components/zoom_admin/sources/custom-events/custom-events.mjs @@ -5,7 +5,7 @@ export default { type: "source", name: "Custom Events", description: "Listen for any events tied to your Zoom account", - version: "0.1.3", + version: "0.1.5", props: { zoomAdmin, eventNameOptions: { diff --git a/components/zoom_admin/sources/meeting-created/meeting-created.mjs b/components/zoom_admin/sources/meeting-created/meeting-created.mjs index 6dc6eca7f8ce6..6ba127da2f6d0 100644 --- a/components/zoom_admin/sources/meeting-created/meeting-created.mjs +++ b/components/zoom_admin/sources/meeting-created/meeting-created.mjs @@ -5,7 +5,7 @@ export default { type: "source", name: "Meeting Created", description: "Emits an event each time a meeting is created in your Zoom account", - version: "0.1.3", + version: "0.1.5", dedupe: "unique", // Dedupe based on meeting ID props: { zoomAdmin, diff --git a/components/zoom_admin/sources/meeting-deleted/meeting-deleted.mjs b/components/zoom_admin/sources/meeting-deleted/meeting-deleted.mjs index 6d8e9d8f1c789..643002f5636da 100644 --- a/components/zoom_admin/sources/meeting-deleted/meeting-deleted.mjs +++ b/components/zoom_admin/sources/meeting-deleted/meeting-deleted.mjs @@ -5,7 +5,7 @@ export default { type: "source", name: "Meeting Deleted", description: "Emits an event each time a meeting is deleted in your Zoom account", - version: "0.1.3", + version: "0.1.5", dedupe: "unique", // Dedupe based on meeting ID props: { zoomAdmin, diff --git a/components/zoom_admin/sources/meeting-ended/meeting-ended.mjs b/components/zoom_admin/sources/meeting-ended/meeting-ended.mjs index 6780d2a443051..3d9908a73bde9 100644 --- a/components/zoom_admin/sources/meeting-ended/meeting-ended.mjs +++ b/components/zoom_admin/sources/meeting-ended/meeting-ended.mjs @@ -5,7 +5,7 @@ export default { type: "source", name: "Meeting Ended", description: "Emits an event each time a meeting ends in your Zoom account", - version: "0.1.3", + version: "0.1.5", dedupe: "unique", // Dedupe based on meeting ID props: { zoomAdmin, diff --git a/components/zoom_admin/sources/meeting-started/meeting-started.mjs b/components/zoom_admin/sources/meeting-started/meeting-started.mjs index 22d8174ae1dcc..cc3cd366089d7 100644 --- a/components/zoom_admin/sources/meeting-started/meeting-started.mjs +++ b/components/zoom_admin/sources/meeting-started/meeting-started.mjs @@ -1,12 +1,12 @@ -import zoomAdmin from "../../zoom_admin.app.mjs"; import { v4 as uuidv4 } from "uuid"; +import zoomAdmin from "../../zoom_admin.app.mjs"; export default { key: "zoom_admin-meeting-started", type: "source", name: "Meeting Started", description: "Emits an event each time a meeting starts in your Zoom account", - version: "0.1.3", + version: "0.1.5", dedupe: "unique", // Dedupe based on meeting ID props: { zoomAdmin, diff --git a/components/zoom_admin/sources/meeting-updated/meeting-updated.mjs b/components/zoom_admin/sources/meeting-updated/meeting-updated.mjs index cf5d3c42b7990..bd861abc2559f 100644 --- a/components/zoom_admin/sources/meeting-updated/meeting-updated.mjs +++ b/components/zoom_admin/sources/meeting-updated/meeting-updated.mjs @@ -5,7 +5,7 @@ export default { type: "source", name: "Meeting Updated", description: "Emits an event each time a meeting is updated in your Zoom account", - version: "0.1.3", + version: "0.1.5", dedupe: "unique", // dedupe on the meeting ID + timestamp props: { zoomAdmin, diff --git a/components/zoom_admin/sources/recording-completed/recording-completed.mjs b/components/zoom_admin/sources/recording-completed/recording-completed.mjs index 9a19891b1efa7..10ecd3ecb1d11 100644 --- a/components/zoom_admin/sources/recording-completed/recording-completed.mjs +++ b/components/zoom_admin/sources/recording-completed/recording-completed.mjs @@ -6,7 +6,7 @@ export default { type: "source", name: "Recording Completed", description: "Emits an event each time a recording is ready for viewing in your Zoom account", - version: "0.1.3", + version: "0.1.6", dedupe: "unique", // Dedupe events based on the ID of the recording file props: { zoomAdmin, diff --git a/components/zoom_admin/sources/user-activated/user-activated.mjs b/components/zoom_admin/sources/user-activated/user-activated.mjs index 265fdad7bef9e..12d0ba01064ad 100644 --- a/components/zoom_admin/sources/user-activated/user-activated.mjs +++ b/components/zoom_admin/sources/user-activated/user-activated.mjs @@ -3,9 +3,9 @@ import zoomAdmin from "../../zoom_admin.app.mjs"; export default { key: "zoom_admin-user-activated", type: "source", - name: "User Activated", - description: "Emits an event each time a user is activated in your Zoom account", - version: "0.1.3", + name: "New User Activated", + description: "Emit new event each time a user is activated in your Zoom account", + version: "0.1.5", dedupe: "unique", // Dedupe based on user ID props: { zoomAdmin, diff --git a/components/zoom_admin/sources/user-created/user-created.mjs b/components/zoom_admin/sources/user-created/user-created.mjs index 40dc3c3823b5f..fff60664895fe 100644 --- a/components/zoom_admin/sources/user-created/user-created.mjs +++ b/components/zoom_admin/sources/user-created/user-created.mjs @@ -5,7 +5,7 @@ export default { type: "source", name: "User Created", description: "Emits an event each time a user is created in your Zoom account", - version: "0.1.3", + version: "0.1.5", dedupe: "unique", // Dedupe based on user ID props: { zoomAdmin, diff --git a/components/zoom_admin/sources/user-deactivated/user-deactivated.mjs b/components/zoom_admin/sources/user-deactivated/user-deactivated.mjs index 8aa56bcc0d0fa..8eaa720b3343c 100644 --- a/components/zoom_admin/sources/user-deactivated/user-deactivated.mjs +++ b/components/zoom_admin/sources/user-deactivated/user-deactivated.mjs @@ -5,7 +5,7 @@ export default { type: "source", name: "User Deactivated", description: "Emits an event each time a user is deactivated in your Zoom account", - version: "0.1.3", + version: "0.1.5", dedupe: "unique", // Dedupe based on user ID props: { zoomAdmin, diff --git a/components/zoom_admin/sources/user-deleted/user-deleted.mjs b/components/zoom_admin/sources/user-deleted/user-deleted.mjs index 66392b97e3140..01902ff6bfd8c 100644 --- a/components/zoom_admin/sources/user-deleted/user-deleted.mjs +++ b/components/zoom_admin/sources/user-deleted/user-deleted.mjs @@ -5,7 +5,7 @@ export default { type: "source", name: "User Deleted", description: "Emits an event each time a user is deleted in your Zoom account", - version: "0.1.3", + version: "0.1.5", dedupe: "unique", // Dedupe based on user ID props: { zoomAdmin, diff --git a/components/zoom_admin/sources/user-invitation-accepted/user-invitation-accepted.mjs b/components/zoom_admin/sources/user-invitation-accepted/user-invitation-accepted.mjs index d3fab4ff076e2..920601f892059 100644 --- a/components/zoom_admin/sources/user-invitation-accepted/user-invitation-accepted.mjs +++ b/components/zoom_admin/sources/user-invitation-accepted/user-invitation-accepted.mjs @@ -5,7 +5,7 @@ export default { type: "source", name: "User Invitation Accepted", description: "Emits an event each time a user accepts an invite to your Zoom account", - version: "0.1.3", + version: "0.1.5", dedupe: "unique", // Dedupe based on user ID props: { zoomAdmin, diff --git a/components/zoom_admin/sources/user-updated/user-updated.mjs b/components/zoom_admin/sources/user-updated/user-updated.mjs index d13e3212df10c..1560baa4d66eb 100644 --- a/components/zoom_admin/sources/user-updated/user-updated.mjs +++ b/components/zoom_admin/sources/user-updated/user-updated.mjs @@ -5,7 +5,7 @@ export default { type: "source", name: "User Updated", description: "Emits an event each time a user's settings are updated in your Zoom account", - version: "0.1.3", + version: "0.1.5", dedupe: "unique", // Dedupe based on user ID props: { zoomAdmin, diff --git a/components/zoom_admin/sources/webinar-changes-to-panelists/webinar-changes-to-panelists.mjs b/components/zoom_admin/sources/webinar-changes-to-panelists/webinar-changes-to-panelists.mjs index cd1490470951c..ce071b7997756 100644 --- a/components/zoom_admin/sources/webinar-changes-to-panelists/webinar-changes-to-panelists.mjs +++ b/components/zoom_admin/sources/webinar-changes-to-panelists/webinar-changes-to-panelists.mjs @@ -1,14 +1,14 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; import crypto from "crypto"; import difference from "lodash/difference.js"; -import zoomAdmin from "../../zoom_admin.app.mjs"; import { sanitizedArray } from "../../utils.mjs"; -import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import zoomAdmin from "../../zoom_admin.app.mjs"; export default { type: "source", name: "Changes to Webinar Panelists", key: "zoom_admin-webinar-changes-to-panelists", - version: "0.1.3", + version: "0.1.5", description: "Emit new event every time a panelist is added or removed from a webinar, or any time their details change", dedupe: "unique", props: { diff --git a/components/zoom_admin/sources/webinar-created/webinar-created.mjs b/components/zoom_admin/sources/webinar-created/webinar-created.mjs index df68f58760e54..8d803dae4e763 100644 --- a/components/zoom_admin/sources/webinar-created/webinar-created.mjs +++ b/components/zoom_admin/sources/webinar-created/webinar-created.mjs @@ -6,7 +6,7 @@ export default { name: "Webinar Created", description: "Emits an event each time a webinar is created in your Zoom account", - version: "0.1.3", + version: "0.1.5", dedupe: "unique", // Dedupe based on webinar ID props: { zoomAdmin, diff --git a/components/zoom_admin/sources/webinar-deleted/webinar-deleted.mjs b/components/zoom_admin/sources/webinar-deleted/webinar-deleted.mjs index 1e0eb163426f5..b4526bcbcb01f 100644 --- a/components/zoom_admin/sources/webinar-deleted/webinar-deleted.mjs +++ b/components/zoom_admin/sources/webinar-deleted/webinar-deleted.mjs @@ -5,7 +5,7 @@ export default { name: "Webinar Deleted", description: "Emits an event each time a webinar is deleted in your Zoom account", - version: "0.1.3", + version: "0.1.5", type: "source", dedupe: "unique", // Dedupe based on webinar ID props: { diff --git a/components/zoom_admin/sources/webinar-ended/webinar-ended.mjs b/components/zoom_admin/sources/webinar-ended/webinar-ended.mjs index ae0f0a07a0d9b..a983cf2195a0f 100644 --- a/components/zoom_admin/sources/webinar-ended/webinar-ended.mjs +++ b/components/zoom_admin/sources/webinar-ended/webinar-ended.mjs @@ -5,7 +5,7 @@ export default { type: "source", name: "Webinar Ended", description: "Emits an event each time a webinar ends in your Zoom account", - version: "0.1.3", + version: "0.1.5", dedupe: "unique", // Dedupe based on webinar ID props: { zoomAdmin, diff --git a/components/zoom_admin/sources/webinar-started/webinar-started.mjs b/components/zoom_admin/sources/webinar-started/webinar-started.mjs index f4eadfc28e239..a819c886c2226 100644 --- a/components/zoom_admin/sources/webinar-started/webinar-started.mjs +++ b/components/zoom_admin/sources/webinar-started/webinar-started.mjs @@ -5,7 +5,7 @@ export default { type: "source", name: "Webinar Started", description: "Emits an event each time a webinar starts in your Zoom account", - version: "0.1.3", + version: "0.1.7", dedupe: "unique", // Dedupe based on webinar ID props: { zoomAdmin, diff --git a/components/zoom_admin/sources/webinar-updated/webinar-updated.mjs b/components/zoom_admin/sources/webinar-updated/webinar-updated.mjs index 8498450f621a2..3873638a6ec8f 100644 --- a/components/zoom_admin/sources/webinar-updated/webinar-updated.mjs +++ b/components/zoom_admin/sources/webinar-updated/webinar-updated.mjs @@ -6,7 +6,7 @@ export default { name: "Webinar Updated", description: "Emits an event each time a webinar is updated in your Zoom account", - version: "0.1.3", + version: "0.1.5", dedupe: "unique", // Dedupe based on webinar ID props: { zoomAdmin, diff --git a/components/zoom_admin/zoom_admin.app.mjs b/components/zoom_admin/zoom_admin.app.mjs index 27692e448aaa8..6aef5aeac4c63 100644 --- a/components/zoom_admin/zoom_admin.app.mjs +++ b/components/zoom_admin/zoom_admin.app.mjs @@ -1,10 +1,10 @@ /* eslint-disable camelcase */ import { axios } from "@pipedream/platform"; +import flatten from "lodash/flatten.js"; import get from "lodash/get.js"; import sortBy from "lodash/sortBy.js"; -import flatten from "lodash/flatten.js"; -import zoomCountries from "./zoom_countries.mjs"; import consts from "./consts.mjs"; +import zoomCountries from "./zoom_countries.mjs"; export default { type: "app", @@ -285,6 +285,14 @@ export default { }); return data; }, + listAccountCallLogs(nextPageToken) { + return this._makeRequest({ + path: "/phone/call_history", + params: { + next_page_token: nextPageToken, + }, + }); + }, async listWebinarPanelists(webinarID, nextPageToken) { const { data } = await this._makeRequest({ path: `/webinars/${webinarID}/panelists`, diff --git a/components/zoom_chatbot/package.json b/components/zoom_chatbot/package.json new file mode 100644 index 0000000000000..893bcb215ab20 --- /dev/null +++ b/components/zoom_chatbot/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/zoom_chatbot", + "version": "0.6.0", + "description": "Pipedream zoom_chatbot Components", + "main": "zoom_chatbot.app.mjs", + "keywords": [ + "pipedream", + "zoom_chatbot" + ], + "homepage": "https://pipedream.com/apps/zoom_chatbot", + "author": "Pipedream support@pipedream.com (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/zulip/.gitignore b/components/zulip/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/zulip/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/zulip/README.md b/components/zulip/README.md index ca87966f5d26d..8182a64898a7d 100644 --- a/components/zulip/README.md +++ b/components/zulip/README.md @@ -1,24 +1,11 @@ # Overview -The Zulip API allows developers to create powerful applications that integrate -with their Zulip workspaces. This can be used to build advanced automation, -custom integrations, and other features not built into the Zulip platform. +Zulip is a powerful chat platform designed for productive and threaded group conversations. With the Zulip API, you can automate and extend the capabilities of your Zulip instance directly within Pipedream. This includes managing streams, users, messages, and events. Automations can help streamline communication, respond to specific triggers, and integrate chat data with other business tools to centralize workflows. -With the Zulip API you can create automated bots, build custom notification -systems, manage users and groups, and even build advanced analytics. The API -provides access to the same data stored in the Zulip web and mobile -applications, allowing applications to use the same data and features. +# Example Use Cases -Here are some examples of things you can build with the Zulip API: +- **Automated Support Ticket Creation**: When a message tagged with #support is sent on Zulip, trigger a workflow that creates a ticket in a tool like Zendesk or Jira. This ensures that customer queries are promptly turned into trackable support tickets. -- Automated bots: Create automated bots which respond to messages according to - logic you define. -- Custom notifications: Send notifications to specific users in a stream or - send bulk notifications to an entire organization or group. -- Manage users and groups: Automatically create and manage users and groups. -- Advanced analytics: Analyze patterns and trends in conversations and build - visualizations to present the data. -- Custom integrations: Integrate other applications and services like social - media, project management tools, and more. -- Create custom apps: Create your own web applications that interact with - Zulip. +- **Real-time CRM Updates**: Use Pipedream to detect when a user mentions a sales deal in Zulip. Extract the details and update the corresponding opportunity in a CRM like Salesforce, ensuring the sales team has the latest information. + +- **Daily Standup Summaries**: Schedule a Pipedream workflow to collect the previous day's standup messages from a designated Zulip stream. Compile and send a summary email to the team using a service like SendGrid, keeping everyone in sync even if they missed the live standup. diff --git a/components/zulip/actions/send-message/send-message.mjs b/components/zulip/actions/send-message/send-message.mjs new file mode 100644 index 0000000000000..8ad4faad987cb --- /dev/null +++ b/components/zulip/actions/send-message/send-message.mjs @@ -0,0 +1,76 @@ +import app from "../../zulip.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "zulip-send-message", + name: "Send Message", + description: "Send a direct or channel message. [See the documentation](https://zulip.com/api/send-message)", + version: "0.0.1", + type: "action", + props: { + app, + type: { + propDefinition: [ + app, + "type", + ], + }, + userId: { + propDefinition: [ + app, + "userId", + ], + }, + channelId: { + propDefinition: [ + app, + "channelId", + ], + }, + topic: { + propDefinition: [ + app, + "topic", + (c) => ({ + channelId: c.channelId, + }), + ], + }, + content: { + propDefinition: [ + app, + "content", + ], + }, + }, + + async run({ $ }) { + const isDirect = this.type === "direct" || this.type === "private"; + const isChannel = this.type === "channel" || this.type === "stream"; + + if (isDirect && (!this.userId || this.userId.length === 0)) { + throw new ConfigurationError("You must provide at least one User ID when the type is 'direct' or 'private'."); + } + + if (isChannel && !this.channelId) { + throw new ConfigurationError("You must provide a Channel ID when the type is 'channel' or 'stream'."); + } + + if ((isDirect && this.channelId) || (isChannel && this.userId?.length > 0)) { + throw new ConfigurationError(`Invalid input: '${this.type}' messages require only ${isDirect + ? "User ID(s)" + : "a Channel ID"}.`); + } + const response = await this.app.sendMessage({ + $, + params: { + type: this.type, + to: this.channelId || `[${this.userId.join(",")}]`, + topic: this.topic, + content: this.content, + }, + }); + $.export("$summary", `Sucessfully sent message with ID ${response.id}`); + return response; + }, +}; diff --git a/components/zulip/app/zulip.app.ts b/components/zulip/app/zulip.app.ts deleted file mode 100644 index 78240e4699b20..0000000000000 --- a/components/zulip/app/zulip.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "zulip", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); diff --git a/components/zulip/common/constants.mjs b/components/zulip/common/constants.mjs new file mode 100644 index 0000000000000..586df711cca4b --- /dev/null +++ b/components/zulip/common/constants.mjs @@ -0,0 +1,8 @@ +export default { + TYPE_OPTIONS: [ + "direct", + "channel", + "stream", + "private", + ], +}; diff --git a/components/zulip/package.json b/components/zulip/package.json index 20ec668014095..d07c23fb1d8e1 100644 --- a/components/zulip/package.json +++ b/components/zulip/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/zulip", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream Zulip Components", - "main": "dist/app/zulip.app.mjs", + "main": "zulip.app.mjs", "keywords": [ "pipedream", "zulip" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/zulip", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/zulip/zulip.app.mjs b/components/zulip/zulip.app.mjs new file mode 100644 index 0000000000000..26c8275ad0b61 --- /dev/null +++ b/components/zulip/zulip.app.mjs @@ -0,0 +1,117 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "zulip", + propDefinitions: { + type: { + type: "string", + label: "Type", + description: "The type of message to be sent", + options: constants.TYPE_OPTIONS, + }, + userId: { + type: "integer[]", + label: "User ID", + description: "ID of the Users recipients of the message. Required when type is `direct` or `private`", + optional: true, + async options() { + const response = await this.getUsers(); + const usersIds = response.members; + return usersIds.map(({ + user_id, full_name, + }) => ({ + label: full_name, + value: user_id, + })); + }, + }, + channelId: { + type: "integer", + label: "Channel ID", + description: "ID of the Channel recipient of the message. Required when type is `channel` or `stream`", + optional: true, + async options() { + const response = await this.getChannels(); + const channelsIds = response.streams; + return channelsIds.map(({ + stream_id, name, + }) => ({ + label: name, + value: stream_id, + })); + }, + }, + topic: { + type: "string", + label: "Topic", + description: "The topic of the message. Required when type is `channel` or `stream`", + optional: true, + async options({ channelId }) { + const response = await this.getTopics({ + channelId, + }); + const topics = response.topics; + return topics.map(({ name }) => ({ + label: name, + value: name, + })); + }, + }, + content: { + type: "string", + label: "Content", + description: "The content of the message", + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.domain}.zulipchat.com/api/v1`; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + path, + auth, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + url: this._baseUrl() + path, + auth: { + username: `${this.$auth.email}`, + password: `${this.$auth.api_key}`, + ...auth, + }, + }); + }, + async getUsers(args = {}) { + return this._makeRequest({ + path: "/users", + ...args, + }); + }, + async getChannels(args = {}) { + return this._makeRequest({ + path: "/streams", + ...args, + }); + }, + async getTopics({ + channelId, ...args + }) { + return this._makeRequest({ + path: `/users/me/${channelId}/topics`, + ...args, + }); + }, + async sendMessage(args = {}) { + return this._makeRequest({ + path: "/messages", + method: "post", + ...args, + }); + }, + }, +}; diff --git a/components/zuora/README.md b/components/zuora/README.md index 0a70d895ef7c3..8953238c2b8e0 100644 --- a/components/zuora/README.md +++ b/components/zuora/README.md @@ -1,19 +1,11 @@ # Overview -The Zuora Billing API provides a comprehensive set of APIs to manage customer -billing lifecycles, from order processing to invoicing and collections. With -the Zuora Billing API, you can build powerful applications that automate -business processes, manage customer billing accounts, and generate financial -insights. Here are a few examples of what you can build with the Zuora Billing -API: +Zuora Billing API grants the power to automate complex billing processes, manage subscriptions, and handle payments with ease. On Pipedream, you can craft workflows that react to events in Zuora, synchronize data across multiple platforms, and perform actions based on specific triggers. For instance, you can update a CRM record when a subscription is renewed, send out custom email alerts on payment failures, or generate detailed financial reports by aggregating billing data. -- Automate the creation, modification, and retrieval of customer billing - accounts. -- Create orders with installments and discounts. -- Generate comprehensive invoices and payment reminders. -- Configure payment gateways and automate payment processing. -- Post customer refunds, fees and reversals. -- Automate the collections process, including dunning. -- Track and visualize financial performance. -- Generate detailed reporting insights. -- Streamline customer onboarding. +# Example Use Cases + +- **Subscription Status Webhook to Slack Notification**: When a subscription status changes in Zuora, a Pipedream workflow can catch this event and post a notification to a designated Slack channel. This keeps teams instantly informed about customer subscription lifecycles without manual checks. + +- **Failed Payment Retry Logic**: Create a workflow that listens for failed payment events from Zuora. Use conditional logic within Pipedream to determine if a retry should occur based on predefined criteria (e.g., number of attempts, type of error). If a retry is warranted, Pipedream can automatically initiate the payment process again in Zuora. + +- **Monthly Financial Summary to Google Sheets**: At the end of each month, trigger a Pipedream workflow to query Zuora for all closed invoices. Aggregate the data to calculate key financial metrics and then append the results to a Google Sheet for easy access and analysis by your finance team. diff --git a/components/zylvie/common/constants.mjs b/components/zylvie/common/constants.mjs new file mode 100644 index 0000000000000..9a4dc25888918 --- /dev/null +++ b/components/zylvie/common/constants.mjs @@ -0,0 +1,5 @@ +const BASE_URL = "https://api.zylvie.com"; + +export default { + BASE_URL, +}; diff --git a/components/zylvie/package.json b/components/zylvie/package.json new file mode 100644 index 0000000000000..1d67105bf8e5b --- /dev/null +++ b/components/zylvie/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/zylvie", + "version": "0.1.0", + "description": "Pipedream Zylvie Components", + "main": "zylvie.app.mjs", + "keywords": [ + "pipedream", + "zylvie" + ], + "homepage": "https://pipedream.com/apps/zylvie", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.0" + } +} diff --git a/components/zylvie/sources/common/triggers.mjs b/components/zylvie/sources/common/triggers.mjs new file mode 100644 index 0000000000000..1cfda1acc1dbe --- /dev/null +++ b/components/zylvie/sources/common/triggers.mjs @@ -0,0 +1,7 @@ +export default { + SALE: "sale", + LEAD: "lead", + AFFILIATE: "affiliate", + SUBSCRIPTION: "subscription", + CANCEL: "cancel", +}; diff --git a/components/zylvie/sources/common/webhook.mjs b/components/zylvie/sources/common/webhook.mjs new file mode 100644 index 0000000000000..f0cc6c2c43b35 --- /dev/null +++ b/components/zylvie/sources/common/webhook.mjs @@ -0,0 +1,67 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../zylvie.app.mjs"; + +export default { + props: { + app, + http: "$.interface.http", + }, + hooks: { + async activate() { + const { + createWebhook, + getTriggerName, + http: { endpoint }, + } = this; + await createWebhook({ + debug: true, + data: { + trigger: getTriggerName(), + webhook_url: endpoint, + }, + }); + }, + async deactivate() { + const { + deleteWebhook, + http: { endpoint }, + } = this; + await deleteWebhook({ + debug: true, + data: { + webhook_url: endpoint, + }, + }); + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getTriggerName() { + throw new ConfigurationError("getTriggerName is not implemented"); + }, + processResource(resource) { + this.$emit(resource, this.generateMeta(resource)); + }, + createWebhook(args = {}) { + return this.app.post({ + path: "/webhooks/subscribe", + ...args, + }); + }, + deleteWebhook(args = {}) { + return this.app.delete({ + path: "/webhooks/unsubscribe", + ...args, + }); + }, + }, + async run({ body }) { + this.http.respond({ + status: 200, + }); + + this.processResource(body); + }, +}; diff --git a/components/zylvie/sources/new-affiliate-sign-up-instant/new-affiliate-sign-up-instant.mjs b/components/zylvie/sources/new-affiliate-sign-up-instant/new-affiliate-sign-up-instant.mjs new file mode 100644 index 0000000000000..a59d225fa6828 --- /dev/null +++ b/components/zylvie/sources/new-affiliate-sign-up-instant/new-affiliate-sign-up-instant.mjs @@ -0,0 +1,29 @@ +import common from "../common/webhook.mjs"; +import triggers from "../common/triggers.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "zylvie-new-affiliate-sign-up-instant", + name: "New Affiliate Sign Up (Instant)", + description: "Emit new event when a visitor signs up to be an affiliate or when they accept an invitation to be an affiliate. [See the documentation](https://developers.zylvie.com/webhooks/subscribe).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTriggerName() { + return triggers.AFFILIATE; + }, + generateMeta(payload) { + const { data: resource } = payload; + const ts = Date.parse(resource.accepted_at); + return { + id: resource.id, + summary: `New Affiliate: ${resource.affiliate.name || resource.affiliate.email}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/zylvie/sources/new-affiliate-sign-up-instant/test-event.mjs b/components/zylvie/sources/new-affiliate-sign-up-instant/test-event.mjs new file mode 100644 index 0000000000000..3f72b3ebb0301 --- /dev/null +++ b/components/zylvie/sources/new-affiliate-sign-up-instant/test-event.mjs @@ -0,0 +1,21 @@ +export default { + "event": "affiliate", + "data": { + "accepted_at": "2025-03-23T03:12:35Z", + "affiliate": { + "username": "stavros.flatley", + "name": "Stavros Flatley", + "email": "stavros@greek.com" + }, + "commission_percentage": 35.0, + "products": { + "allowed_for_all_products": false, + "allowed_products": [ + { + "title": "The Ultimate Business Book", + "price": 0.0 + } + ] + } + } +}; diff --git a/components/zylvie/sources/new-lead-instant/new-lead-instant.mjs b/components/zylvie/sources/new-lead-instant/new-lead-instant.mjs new file mode 100644 index 0000000000000..ef448dde155a3 --- /dev/null +++ b/components/zylvie/sources/new-lead-instant/new-lead-instant.mjs @@ -0,0 +1,28 @@ +import common from "../common/webhook.mjs"; +import triggers from "../common/triggers.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "zylvie-new-lead-instant", + name: "New Lead (Instant)", + description: "Emit new event when a user submits their name and email to receive a free product/lead magnet. [See the documentation](https://developers.zylvie.com/webhooks/subscribe).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTriggerName() { + return triggers.LEAD; + }, + generateMeta(payload) { + const { data: resource } = payload; + return { + id: resource.transaction_id, + summary: `New Lead: ${resource.transaction_id}`, + ts: Date.parse(resource.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/zylvie/sources/new-lead-instant/test-event.mjs b/components/zylvie/sources/new-lead-instant/test-event.mjs new file mode 100644 index 0000000000000..f3d360a205233 --- /dev/null +++ b/components/zylvie/sources/new-lead-instant/test-event.mjs @@ -0,0 +1,25 @@ +export default { + "event": "lead", + "data": { + "transaction_id": "free_1711078342965279", + "created_at": "2025-03-23T03:12:35Z", + "buyer": { + "name": "Stavros Flatley", + "email": "stavros@greek.com", + "permission_to_send_emails": true + }, + "products": [ + { + "title": "The Ultimate Business Book", + "price": 0.0, + "quantity": 1 + } + ], + "custom_fields": [ + { + "name": "Tax identification number", + "value": "TIN-5252445767" + } + ] + } +}; diff --git a/components/zylvie/sources/new-subscription-instant/new-subscription-instant.mjs b/components/zylvie/sources/new-subscription-instant/new-subscription-instant.mjs new file mode 100644 index 0000000000000..da8eee17f7c28 --- /dev/null +++ b/components/zylvie/sources/new-subscription-instant/new-subscription-instant.mjs @@ -0,0 +1,28 @@ +import common from "../common/webhook.mjs"; +import triggers from "../common/triggers.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "zylvie-new-subscription-instant", + name: "New Subscription (Instant)", + description: "Emit new event when a user subscribes to a subscription product, whether free trial or otherwise. [See the documentation](https://developers.zylvie.com/webhooks/subscribe).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTriggerName() { + return triggers.SUBSCRIPTION; + }, + generateMeta(payload) { + const { data: resource } = payload; + return { + id: resource.subscription_id, + summary: `New Subscription: ${resource.subscription_id}`, + ts: Date.parse(resource.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/zylvie/sources/new-subscription-instant/test-event.mjs b/components/zylvie/sources/new-subscription-instant/test-event.mjs new file mode 100644 index 0000000000000..859fdfcc5f3eb --- /dev/null +++ b/components/zylvie/sources/new-subscription-instant/test-event.mjs @@ -0,0 +1,51 @@ +export default { + "event": "subscription", + "data": { + "subscription_id": "sub_1OoXxPF18w81dttLN6XyvuBl", + "created_at": "2025-02-27T21:12:59Z", + "cancel_at": null, + "canceled_at": null, + "trial_end": "2025-03-22T21:12:59Z", + "trial_start": "2025-02-27T21:12:59Z", + "currency": "usd", + "amount": 99.99, + "interval": "month", + "interval_count": 1, + "stripe_price_id": "price_1OkfZsF18w81dttL2OgdOtEj", + "status": "active", + "product": { + "title": "Life Coaching (Pro Tier)" + }, + "buyer": { + "name": "Stavros Flatley", + "email": "stavros@greek.com", + "permission_to_send_emails": true, + "phone": "+353850163326", + "line1": "349 Navan Road", + "line2": "Ashtown", + "postal_code": "D07 R2C3", + "city": "Dublin 7", + "state": "County Dublin", + "country": "IE", + "payment_method_type": "card", + "card_brand": "visa", + "card_last4": "0005" + }, + "coupon": { + "code": "20OFF", + "type": "percentage", + "amount": 20.0 + }, + "referrer": { + "email": "john.cena@zlappo.com" + }, + "custom_fields": [ + { + "name": "Tax identification number", + "value": "TIN-5252445767" + } + ], + "commission_earned": 41.99, + "commission_paid": false + } +}; diff --git a/components/zylvie/zylvie.app.mjs b/components/zylvie/zylvie.app.mjs new file mode 100644 index 0000000000000..a1895f07ef18c --- /dev/null +++ b/components/zylvie/zylvie.app.mjs @@ -0,0 +1,39 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +export default { + type: "app", + app: "zylvie", + methods: { + getUrl(path) { + return `${constants.BASE_URL}${path}`; + }, + getHeaders(headers) { + return { + "Authorization": `Bearer ${this.$auth.api_key}`, + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + }, +}; diff --git a/components/zyte_api/README.md b/components/zyte_api/README.md new file mode 100644 index 0000000000000..37650a1b0805c --- /dev/null +++ b/components/zyte_api/README.md @@ -0,0 +1,11 @@ +# Overview + +The Zyte API provides programmatic access to web data extraction services, allowing you to pull structured data from websites efficiently. Within Pipedream, you can leverage the Zyte API to create powerful serverless workflows that automate data collection, monitor web content changes, or enrich your datasets with web-sourced information. By connecting Zyte to other apps on Pipedream, you can easily integrate web scraping into your data processing pipelines, event-driven applications, and more, with minimal setup and no server maintenance. + +# Example Use Cases + +- **Automated Market Research**: Trigger a workflow on a schedule to use Zyte API for scraping product information from e-commerce sites. Process this data and send it to a Google Sheets spreadsheet for analysis, helping businesses track competitor pricing and inventory changes in real-time. + +- **Content Change Detection**: Set up a Pipedream workflow that uses the Zyte API to periodically check a set of web pages for content updates or changes. If changes are detected, automatically notify your team via Slack or email, ensuring that you are always informed about critical updates to competitors' websites or industry news portals. + +- **Lead Generation Pipeline**: Create a workflow on Pipedream that utilizes Zyte API to extract contact information from business directories or professional social networks. Enrich this data with additional details and automatically add it to a CRM system like Salesforce, streamlining your lead generation and outreach efforts. diff --git a/components/zyte_api/package.json b/components/zyte_api/package.json new file mode 100644 index 0000000000000..9ef1a47e4bc6e --- /dev/null +++ b/components/zyte_api/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pipedream/zyte_api", + "version": "0.0.1", + "description": "Pipedream Zyte API Components", + "main": "zyte_api.app.mjs", + "keywords": [ + "pipedream", + "zyte_api" + ], + "homepage": "https://pipedream.com/apps/zyte_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + } +} diff --git a/components/zyte_api/zyte_api.app.mjs b/components/zyte_api/zyte_api.app.mjs new file mode 100644 index 0000000000000..2761caa99b7f8 --- /dev/null +++ b/components/zyte_api/zyte_api.app.mjs @@ -0,0 +1,11 @@ +export default { + type: "app", + app: "zyte_api", + propDefinitions: {}, + methods: { + // this.$auth contains connected account data + authKeys() { + console.log(Object.keys(this.$auth)); + }, + }, +}; \ No newline at end of file diff --git a/docs-v2/.eslintrc.js b/docs-v2/.eslintrc.js deleted file mode 100644 index bd3badf80f46b..0000000000000 --- a/docs-v2/.eslintrc.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - // Enables TypeScript support - parser: "@typescript-eslint/parser", - // Specifies the ESLint parser - parserOptions: { - ecmaFeatures: { - jsx: true, // Allows for the parsing of JSX - }, - ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features - sourceType: "module", // Allows for the use of imports - }, - settings: { - react: { - version: "detect", // Tells eslint-plugin-react to automatically detect the version of React to use - }, - }, - extends: [ - "next", - ], -}; diff --git a/docs-v2/.npmrc b/docs-v2/.npmrc new file mode 100644 index 0000000000000..9dc7c37f87031 --- /dev/null +++ b/docs-v2/.npmrc @@ -0,0 +1,8 @@ +auto-install-peers=true +enable-pre-post-scripts=true +save-workspace-protocol=false +link-workspace-packages=false +shamefully-hoist=true +node-linker=hoisted +shared-workspace-lockfile=true +engine-strict=false \ No newline at end of file diff --git a/docs-v2/.tool-versions b/docs-v2/.tool-versions index c2ca3d3d25e33..fdb1cb75b15d8 100644 --- a/docs-v2/.tool-versions +++ b/docs-v2/.tool-versions @@ -1 +1,2 @@ nodejs 20.9.0 +pnpm 9.14.2 diff --git a/docs-v2/README.md b/docs-v2/README.md index c8db482025261..6a004b71b2f59 100644 --- a/docs-v2/README.md +++ b/docs-v2/README.md @@ -6,8 +6,7 @@ ```bash asdf install -yarn install -yarn dev +pnpm dev ``` Open [http://localhost:3000/docs](http://localhost:3000/docs). @@ -23,7 +22,7 @@ We're moving from VuePress to Nextra. Here's what's the same and what's differen - New docs are here, in the `docs-v2/` directory. - All docs are still Markdown. Now MDX files instead of just Markdown, so extensions have been renamed to `.mdx`. - Keep images in `public/images/foo.png`, reference with `![alt text](/images/foo.png)` -- Leaf nodes in VuePress were at files like `/docs/workspaces/sso/google/README.md`. Nextra allows leaf nodes to be non-index files like `/docs/workspaces/sso/google.mdx`, so there's no need for the final directory, which simplifies things. +- Leaf nodes in VuePress were at files like `/docs/workflows/workspaces/sso/google/README.md`. Nextra allows leaf nodes to be non-index files like `/docs/workflows/workspaces/sso/google.mdx`, so there's no need for the final directory, which simplifies things. - VuePress used Vue, Nextra uses React. All of the custom components used in MDX files are either [built-in Nextra components](https://nextra.site/docs/guide/built-ins) or Pipedream-specific components in `components/`, ported from Vue. - Learn the [built-in Nextra components](https://nextra.site/docs/guide/built-ins). All the `::: tip`, `::: warning`, or other special VuePress components have an equivalent Nextra component. @@ -51,11 +50,11 @@ pages workspaces/ index.mdx # /docs/workspaces sso/ - index.mdx # /docs/workspaces/sso - google.mdx # /docs/workspaces/sso/google + index.mdx # /docs/workflows/workspaces/sso + google.mdx # /docs/workflows/workspaces/sso/google ``` -The `_meta.json` files in each directory defines a mapping between the labels in the sidebar and the files in that directory. If you want to add an item to the sidebar, you'll probably want to edit the `_meta.json` file. [See the Nextra docs](https://nextra.site/docs/docs-theme/page-configuration) for more info. +The `_meta.tsx` files in each directory defines a mapping between the labels in the sidebar and the files in that directory. If you want to add an item to the sidebar, you'll probably want to edit the `_meta.tsx` file. [See the Nextra docs](https://nextra.site/docs/docs-theme/page-configuration) for more info. ## Custom components @@ -64,3 +63,7 @@ Use (and create!) custom components in `components/`. You'll see a lot of refere ## Redirects If you need to add any custom redirects, e.g. when you move pages to a different directory, add then to the `vercel.json` file in this directory. + +## Adding new versions + +Add a new version of docs by creating a new feature branch in the format of `docs-v{number}-pathing`. diff --git a/docs-v2/components/ArcadeEmbed.tsx b/docs-v2/components/ArcadeEmbed.tsx new file mode 100644 index 0000000000000..dd303a2ba0d21 --- /dev/null +++ b/docs-v2/components/ArcadeEmbed.tsx @@ -0,0 +1,28 @@ +import React from "react"; + +interface ArcadeEmbedProps { + src: string; + title?: string; +} + +const ArcadeEmbed: React.FC = ({ + src, title, +}) => { + return ( +
+ +
+ ); +}; + +export default ArcadeEmbed; diff --git a/docs-v2/components/DocSearch.tsx b/docs-v2/components/DocSearch.tsx index 3e83adb23c432..5c707a2d5686b 100644 --- a/docs-v2/components/DocSearch.tsx +++ b/docs-v2/components/DocSearch.tsx @@ -12,6 +12,13 @@ function Search() { facetFilters: [ "version:latest", ], + indexName: process.env.ALGOLIA_INDEX_NAME, + }} + transformItems={(items) => { + return items.map((item) => ({ + ...item, + content: item.url, + })); }} /> ); diff --git a/docs-v2/components/LanguageLink.tsx b/docs-v2/components/LanguageLink.tsx index 4696a0a952d23..e2876ef78208b 100644 --- a/docs-v2/components/LanguageLink.tsx +++ b/docs-v2/components/LanguageLink.tsx @@ -1,8 +1,14 @@ import React from "react"; +type LanguageLinkProps = { + icon: string; + name: string; + link: string; +}; + function LanguageLink({ icon, name, link, -}) { +}: LanguageLinkProps) { return (
diff --git a/docs-v2/components/PipedreamCode.tsx b/docs-v2/components/PipedreamCode.tsx index 996e6dd7dc8ef..853a532ace90e 100644 --- a/docs-v2/components/PipedreamCode.tsx +++ b/docs-v2/components/PipedreamCode.tsx @@ -16,7 +16,7 @@ const fira = Fira_Code({ }); const PipedreamCode = ({ children }: PipedreamCodeProps) => <> - {children} + {children} ; export default PipedreamCode; diff --git a/docs-v2/components/PythonMappings.tsx b/docs-v2/components/PythonMappings.tsx index 363a244a834dc..0da362ff00aa2 100644 --- a/docs-v2/components/PythonMappings.tsx +++ b/docs-v2/components/PythonMappings.tsx @@ -72,7 +72,7 @@ const PythonMappings = () => { {`# pipedream add-package ${mapping[0]}`} diff --git a/docs-v2/components/VideoPlayer.tsx b/docs-v2/components/VideoPlayer.tsx index 52eb92042375b..798d6574930d7 100644 --- a/docs-v2/components/VideoPlayer.tsx +++ b/docs-v2/components/VideoPlayer.tsx @@ -1,10 +1,16 @@ import React from "react"; +type VideoPlayerProps = { + src: string; + title: string; + startAt?: number; +}; + const VideoPlayer = ({ src, title, startAt, -}) => { - const embedUrl = `${src}${startAt - ? `?start=${startAt}` +}: VideoPlayerProps) => { + const embedUrl = `${src}?rel=0${startAt + ? `&start=${startAt}` : ""}`; return ( diff --git a/docs-v2/eslint.config.js b/docs-v2/eslint.config.js deleted file mode 100644 index 1eb1b43f43f32..0000000000000 --- a/docs-v2/eslint.config.js +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-check - -import eslint from "@eslint/js"; -import tseslint from "typescript-eslint"; - -export default tseslint.config( - eslint.configs.recommended, - ...tseslint.configs.recommended, -); diff --git a/docs-v2/next.config.js b/docs-v2/next.config.js deleted file mode 100644 index 420aa6dc76b78..0000000000000 --- a/docs-v2/next.config.js +++ /dev/null @@ -1,61 +0,0 @@ -const withNextra = require("nextra")({ - theme: "nextra-theme-docs", - themeConfig: "./theme.config.tsx", - defaultShowCopyCode: true, -}); - -module.exports = withNextra({ - basePath: "/docs", - images: { - remotePatterns: [ - { - protocol: "https", - hostname: "res.cloudinary.com", - }, - ], - }, - env: { - PIPEDREAM_NODE_VERSION: "20", - PIPEDREAM_BASE_URL: "https://pipedream.com", - API_BASE_URL: "https://api.pipedream.com/v1", - SQL_API_BASE_URL: "https://rt.pipedream.com/sql", - ENDPOINT_BASE_URL: "*.m.pipedream.net", - PAYLOAD_SIZE_LIMIT: "512KB", - MEMORY_LIMIT: "256MB", - MEMORY_ABSOLUTE_LIMIT: "10GB", - EMAIL_PAYLOAD_SIZE_LIMIT: "30MB", - MAX_WORKFLOW_EXECUTION_LIMIT: "750", - BASE_CREDITS_PRICE_MEMORY: "256", - BASE_CREDITS_PRICE_SECONDS: "30", - DATA_STORES_MAX_KEYS: "1,024", - DAILY_CREDITS_LIMIT: "25", - DAILY_TESTING_LIMIT: "30 minutes", - INSPECTOR_EVENT_EXPIRY_DAYS: "365", - FUNCTION_PAYLOAD_LIMIT: "6MB", - DAILY_INVOCATIONS_LIMIT: "333", - FREE_ORG_DAILY_INVOCATIONS_LIMIT: "66", - PRICE_PER_INVOCATION: "0.0002", - FREE_MONTHLY_INVOCATIONS: "10,000", - PRO_MONTHLY_INVOCATIONS: "20,000", - TEAM_MONTHLY_INVOCATIONS: "20,000", - TEAM_MEMBER_LIMIT: "5", - PRO_MONTHLY_PRICE: "$19", - TEAM_MONTHLY_PRICE: "$19", - DEFAULT_WORKFLOW_QUEUE_SIZE: "100", - MAX_WORKFLOW_QUEUE_SIZE: "10,000", - PYTHON_VERSION: "3.12", - GO_LANG_VERSION: "1.21.5", - CONFIGURED_PROPS_SIZE_LIMIT: "64KB", - SERVICE_DB_SIZE_LIMIT: "60KB", - TMP_SIZE_LIMIT: "2GB", - DELAY_MIN_MAX_TIME: - "You can pause your workflow for as little as one millisecond, or as long as one year", - PUBLIC_APPS: "1,900", - FREE_INSPECTOR_EVENT_LIMIT: "7 days of events", - WARM_WORKERS_INTERVAL: "10 minutes", - WARM_WORKERS_CREDITS_PER_INTERVAL: "5", - ALGOLIA_APP_ID: "XY28M447C5", - ALGOLIA_SEARCH_API_KEY: "9d9169458128b3d60c22bb04da4431c7", - ALGOLIA_INDEX_NAME: "pipedream", - }, -}); diff --git a/docs-v2/next.config.mjs b/docs-v2/next.config.mjs new file mode 100644 index 0000000000000..b839a06250981 --- /dev/null +++ b/docs-v2/next.config.mjs @@ -0,0 +1,483 @@ +import nextra from "nextra"; + +const withNextra = nextra({ + theme: "nextra-theme-docs", + themeConfig: "./theme.config.tsx", + defaultShowCopyCode: true, +}); + +export default withNextra({ + basePath: "/docs", + trailingSlash: true, + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "res.cloudinary.com", + }, + ], + }, + async redirects() { + return [ + { + source: "/v3/", + destination: "/", + permanent: true, + }, + { + source: "/v3/:path*/", + destination: "/:path*/", + permanent: true, + }, + { + source: "/what-is-pipedream/", + destination: "/", + permanent: true, + }, + { + source: "/apps/", + destination: "/integrations/apps/", + permanent: true, + }, + { + source: "/apps/app-partners/", + destination: "/integrations/app-partners/", + permanent: true, + }, + { + source: "/apps/guide/requesting-additional-oauth-scopes/", + destination: "/integrations/oauth-clients/", + permanent: true, + }, + { + source: "/apps/contributing/", + destination: "/workflows/contributing/", + permanent: true, + }, + { + source: "/apps/all-apps/", + destination: "https://pipedream.com/apps/", + permanent: true, + }, + { + source: "/apps/:path*/", + destination: "https://pipedream.com/apps/:path*/", + permanent: true, + }, + { + source: "/support/", + destination: "https://pipedream.com/support/", + permanent: true, + }, + { + source: "/security/", + destination: "/privacy-and-security/", + permanent: true, + }, + { + source: "/user-settings/", + destination: "/account/user-settings/", + permanent: true, + }, + { + source: "/quickstart/run-workflow-on-a-schedule/", + destination: "/quickstart/", + permanent: true, + }, + { + source: "/quickstart/github-sync/", + destination: "/workflows/git/", + permanent: true, + }, + { + source: "/cron/", + destination: "/workflows/building-workflows/triggers/", + permanent: true, + }, + { + source: "/notebook/", + destination: "/workflows/", + permanent: true, + }, + { + source: "/notebook/actions/", + destination: "/workflows/building-workflows/actions/", + permanent: true, + }, + { + source: "/notebook/fork/", + destination: "/workflows/building-workflows/sharing/", + permanent: true, + }, + { + source: "/notebook/inspector/", + destination: "/workflows/building-workflows/inspect/", + permanent: true, + }, + { + source: "/notebook/destinations/snowflake/", + destination: "/workflows/data-management/databases/", + permanent: true, + }, + { + source: "/notebook/destinations/:path*/", + destination: "/workflows/data-management/destinations/:path*/", + permanent: true, + }, + { + source: "/notebook/destinations/", + destination: "/workflows/data-management/destinations/", + permanent: true, + }, + { + source: "/notebook/code/", + destination: "/workflows/building-workflows/code/", + permanent: true, + }, + { + source: "/notebook/observability/", + destination: "/workflows/building-workflows/inspect/", + permanent: true, + }, + { + source: "/notebook/sources/", + destination: "/workflows/building-workflows/triggers/", + permanent: true, + }, + { + source: "/notebook/sql/", + destination: "/workflows/data-management/databases/working-with-sql/", + permanent: true, + }, + { + source: "/sources/", + destination: "/workflows/building-workflows/triggers/", + permanent: true, + }, + { + source: "/projects/", + destination: "/workflows/projects/", + permanent: true, + }, + { + source: "/projects/git/", + destination: "/workflows/git/", + permanent: true, + }, + { + source: "/projects/file-stores/", + destination: "/workflows/data-management/file-stores/", + permanent: true, + }, + { + source: "/projects/file-stores/:path*/", + destination: "/workflows/data-management/file-stores/:path*/", + permanent: true, + }, + { + source: "/projects/:path*/", + destination: "/workflows/projects/:path*/", + permanent: true, + }, + { + source: "/event-history/", + destination: "/workflows/event-history/", + permanent: true, + }, + { + source: "/workflows/building-workflows/", + destination: "/workflows/", + permanent: true, + }, + { + source: "/workflows/concurrency-and-throttling/", + destination: + "/workflows/building-workflows/settings/concurrency-and-throttling/", + permanent: true, + }, + { + source: "/workflows/steps/", + destination: "/workflows/#steps", + permanent: true, + }, + { + source: "/workflows/fork/", + destination: "/workflows/building-workflows/sharing/", + permanent: true, + }, + { + source: "/workflows/steps/code/async/", + destination: "/workflows/building-workflows/code/nodejs/async/", + permanent: true, + }, + { + source: "/workflows/steps/code/state/", + destination: "/workflows/#step-exports", + permanent: true, + }, + { + source: "/workflows/steps/params/", + destination: "/workflows/building-workflows/using-props/", + permanent: true, + }, + { + source: "/workflows/events/cold-starts/", + destination: + "/workflows/building-workflows/settings/#eliminate-cold-starts", + permanent: true, + }, + { + source: "/workflows/examples/waiting-to-execute-next-step-of-workflow/", + destination: "/workflows/building-workflows/code/nodejs/delay/", + permanent: true, + }, + { + source: "/workflows/networking/", + destination: "/workflows/vpc/", + permanent: true, + }, + { + source: "/workflows/built-in-functions/", + destination: "/workflows/building-workflows/actions/", + permanent: true, + }, + { + source: "/workflows/events/inspect/", + destination: "/workflows/building-workflows/inspect/", + permanent: true, + }, + { + source: "/workflows/triggers/", + destination: "/workflows/building-workflows/triggers/", + permanent: true, + }, + { + source: "/workflows/steps/triggers/", + destination: "/workflows/building-workflows/triggers/", + permanent: true, + }, + { + source: "/workflows/actions/", + destination: "/workflows/building-workflows/actions/", + permanent: true, + }, + { + source: "/workflows/steps/actions/", + destination: "/workflows/building-workflows/actions/", + permanent: true, + }, + { + source: "/workflows/flow-control/", + destination: "/workflows/building-workflows/control-flow/", + permanent: true, + }, + { + source: "/workflows/control-flow/", + destination: "/workflows/building-workflows/control-flow/", + permanent: true, + }, + { + source: "/code/", + destination: "/workflows/building-workflows/code/", + permanent: true, + }, + { + source: "/code/:path*/", + destination: "/workflows/building-workflows/code/:path*/", + permanent: true, + }, + { + source: "/http/", + destination: "/workflows/building-workflows/http/", + permanent: true, + }, + { + source: "/environment-variables/", + destination: "/workflows/environment-variables/", + permanent: true, + }, + { + source: "/components/quickstart/nodejs/actions/", + destination: "/workflows/contributing/components/actions-quickstart/", + permanent: true, + }, + { + source: "/components/", + destination: "/workflows/contributing/components/", + permanent: true, + }, + { + source: "/components/:path*/", + destination: "/workflows/contributing/components/:path*/", + permanent: true, + }, + { + source: "/github-sync/", + destination: "/workflows/git/", + permanent: true, + }, + { + source: "/workspaces/", + destination: "/workflows/workspaces/", + permanent: true, + }, + { + source: "/workspaces/okta/", + destination: "/workflows/workspaces/sso/okta/", + permanent: true, + }, + { + source: "/workspaces/google/", + destination: "/workflows/workspaces/sso/google/", + permanent: true, + }, + { + source: "/workspaces/saml/", + destination: "/workflows/workspaces/sso/saml/", + permanent: true, + }, + { + source: "/workspaces/:path*/", + destination: "/workflows/workspaces/:path*/", + permanent: true, + }, + { + source: "/workspaces-and-credits-faq/", + destination: "/pricing/faq/", + permanent: true, + }, + { + source: "/connected-accounts/api/", + destination: "/connect/api/#accounts/", + permanent: true, + }, + { + source: "/connected-accounts/", + destination: "/integrations/connected-accounts/", + permanent: true, + }, + { + source: "/connected-accounts/:path*/", + destination: "/integrations/connected-accounts/:path*/", + permanent: true, + }, + { + source: "/api/", + destination: "/rest-api/", + permanent: true, + }, + { + source: "/api/:path*/", + destination: "/rest-api/:path*/", + permanent: true, + }, + { + source: "/data-stores/", + destination: "/workflows/data-management/data-stores/", + permanent: true, + }, + { + source: "/databases/", + destination: "/workflows/data-management/databases/", + permanent: true, + }, + { + source: "/databases/:path*/", + destination: "/workflows/data-management/databases/:path*/", + permanent: true, + }, + { + source: "/cli/", + destination: "/workflows/cli/reference/", + permanent: true, + }, + { + source: "/connect/connect-link/", + destination: "/connect/managed-auth/connect-link/", + permanent: true, + }, + { + source: "/connect/customize-your-app/", + destination: "/connect/managed-auth/customization/", + permanent: true, + }, + { + source: "/connect/oauth-clients/", + destination: "/connect/managed-auth/oauth-clients/", + permanent: true, + }, + { + source: "/connect/quickstart/", + destination: "/connect/managed-auth/quickstart/", + permanent: true, + }, + { + source: "/connect/tokens/", + destination: "/connect/managed-auth/tokens/", + permanent: true, + }, + { + source: "/connect/webhooks/", + destination: "/connect/managed-auth/webhooks/", + permanent: true, + }, + ]; + }, + async rewrites() { + return [ + { + source: "/abuse", + destination: "/hidden/abuse", + }, + { + source: "/scheduling-future-tasks", + destination: "/hidden/scheduling-future-tasks", + }, + { + source: "/status", + destination: "/hidden/status", + }, + { + source: "/subprocessors", + destination: "/hidden/subprocessors", + }, + ]; + }, + env: { + PIPEDREAM_NODE_VERSION: "20", + PIPEDREAM_BASE_URL: "https://pipedream.com", + API_BASE_URL: "https://api.pipedream.com/v1", + SQL_API_BASE_URL: "https://rt.pipedream.com/sql", + ENDPOINT_BASE_URL: "*.m.pipedream.net", + PAYLOAD_SIZE_LIMIT: "512KB", + MEMORY_LIMIT: "256MB", + MEMORY_ABSOLUTE_LIMIT: "10GB", + EMAIL_PAYLOAD_SIZE_LIMIT: "30MB", + MAX_WORKFLOW_EXECUTION_LIMIT: "750", + BASE_CREDITS_PRICE_MEMORY: "256", + BASE_CREDITS_PRICE_SECONDS: "30", + DAILY_TESTING_LIMIT: "30 minutes", + INSPECTOR_EVENT_EXPIRY_DAYS: "365", + FUNCTION_PAYLOAD_LIMIT: "6MB", + DEFAULT_WORKFLOW_QUEUE_SIZE: "100", + MAX_WORKFLOW_QUEUE_SIZE: "10,000", + PYTHON_VERSION: "3.12", + GO_LANG_VERSION: "1.21.5", + CONFIGURED_PROPS_SIZE_LIMIT: "64KB", + SERVICE_DB_SIZE_LIMIT: "60KB", + TMP_SIZE_LIMIT: "2GB", + DELAY_MIN_MAX_TIME: + "You can pause your workflow for as little as one millisecond, or as long as one year", + PUBLIC_APPS: "2,500", + REGISTRY_ACTIONS: "5,300", + REGISTRY_SOURCES: "2,500", + REGISTRY_COMPONENTS: "8,000", + FREE_INSPECTOR_EVENT_LIMIT: "7 days of events", + WARM_WORKERS_INTERVAL: "10 minutes", + WARM_WORKERS_CREDITS_PER_INTERVAL: "5", + ALGOLIA_APP_ID: "XY28M447C5", + ALGOLIA_SEARCH_API_KEY: "a7d274c84696bac04e14cc87139d9eaf", + ALGOLIA_INDEX_NAME: "pipedream", + PD_EGRESS_IP_RANGE: "44.223.89.56/29", + }, +}); diff --git a/docs-v2/package.json b/docs-v2/package.json index 4dfe05a8207b9..40699353f29d9 100644 --- a/docs-v2/package.json +++ b/docs-v2/package.json @@ -2,43 +2,40 @@ "name": "@pipedream/docs", "version": "0.0.1", "description": "Pipedream docs", + "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start" }, + "packageManager": "pnpm@9.14.2", + "engines": { + "pnpm": "9.14.2", + "node": "20.9.0" + }, "author": "Pipedream", "license": "MIT", "homepage": "https://pipedream.com", "dependencies": { - "@docsearch/react": "3", - "@typescript-eslint/eslint-plugin": "^7.0.2", - "@typescript-eslint/parser": "^7.0.2", - "@vercel/analytics": "^1.2.2", - "eslint": "^8.56.0", - "eslint-plugin-jest": "^27.9.0", - "eslint-plugin-jsonc": "^2.13.0", - "eslint-plugin-pipedream": "^0.2.4", - "eslint-plugin-putout": "^22.4.0", + "@docsearch/react": "^3.6.1", + "@vercel/analytics": "^1.3.1", + "chalk": "^5.3.0", "jest": "^29.7.0", - "lint-staged": "^15.2.2", - "next": "^13.0.6", - "nextra": "latest", - "nextra-theme-docs": "latest", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "lint-staged": "^15.2.7", + "next": "^14.2.5", + "nextra": "^3", + "nextra-theme-docs": "^3", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-markdown": "^9.0.1", - "sharp": "^0.33.2" + "sharp": "^0.33.4" }, "devDependencies": { - "@next/eslint-plugin-next": "^14.1.0", "@types/node": "18.11.10", - "autoprefixer": "^10.4.17", - "eslint-config-next": "^14.1.0", - "postcss": "^8.4.35", - "prettier": "^3.2.5", - "tailwindcss": "^3.4.1", - "typescript": "^5.3.3", - "typescript-eslint": "^7.0.2" + "autoprefixer": "^10.4.19", + "postcss": "^8.4.40", + "prettier": "^3.3.3", + "tailwindcss": "^3.4.7", + "typescript": "^5.5.4" } } diff --git a/docs-v2/pages/_meta.json b/docs-v2/pages/_meta.json deleted file mode 100644 index 4c2817881118a..0000000000000 --- a/docs-v2/pages/_meta.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "index": "What is Pipedream?", - "quickstart": { - "title": "Quickstart" - }, - "workspaces": { - "title": "Workspaces" - }, - "projects": { - "title": "Projects" - }, - "workflows": { - "title": "Workflows" - }, - "connected-accounts": { - "title": "Connected Accounts" - }, - "code": { - "title": "Code" - }, - "data-stores": "Data Stores", - "components": "Components", - "sources": "Sources", - "event-history": "Event History", - "http": "HTTP", - "apps": "Integrations", - "rest-api": "API Reference", - "cli": "CLI", - "environment-variables": { - "title": "Environment Variables" - }, - "destinations": "Destinations", - "user-settings": "User and Billing Settings", - "troubleshooting": "Troubleshooting", - "pricing": "Pricing FAQ", - "privacy-and-security": { - "title": "Security" - }, - "limits": "Limits", - "support": { - "title": "Support", - "type": "page", - "href": "https://pipedream.com/support", - "newWindow": true - }, - "pricing-page": { - "title": "Pricing", - "href": "https://pipedream.com/pricing", - "newWindow": true, - "type": "page" - }, - "statuspage": { - "title": "Status", - "type": "page", - "href": "https://status.pipedream.com", - "newWindow": true - }, - "workspaces-and-credits-faq": { - "display": "children" - }, - "abuse": { - "display": "children" - }, - "airtable": { - "display": "children" - }, - "examples": { - "display": "children" - }, - "migrate-from-v1": { - "display": "children" - }, - "new-feature-or-bug": { - "display": "children" - }, - "nodejs20-faq-2024-02": { - "display": "children" - }, - "scheduling-future-tasks": { - "display": "children" - }, - "shopify-faq-2023-10": { - "display": "children" - }, - "subprocessors": { - "display": "children" - }, - "pipedream-axios": { - "display": "children" - }, - "status": { - "display": "children" - } -} diff --git a/docs-v2/pages/_meta.tsx b/docs-v2/pages/_meta.tsx new file mode 100644 index 0000000000000..475593add603a --- /dev/null +++ b/docs-v2/pages/_meta.tsx @@ -0,0 +1,37 @@ +export default { + "index": "What is Pipedream?", + "quickstart": "Quickstart", + "integrations": "Integrations", + "workflows": "Workflows", + "connect": "Connect", + "rest-api": "REST API", + "pricing": "Pricing", + "account": "Account", + "privacy-and-security": "Security", + "troubleshooting": "Troubleshooting", + "glossary": "Glossary of Terms", + "support": { + title: "Support", + type: "page", + href: "https://pipedream.com/support", + newWindow: true, + }, + "pricing-page": { + title: "Pricing", + href: "https://pipedream.com/pricing", + newWindow: true, + type: "page", + }, + "statuspage": { + title: "Status", + type: "page", + href: "https://status.pipedream.com", + newWindow: true, + }, + "deprecated": { + display: "hidden", + }, + "hidden": { + display: "hidden", + }, +} as const diff --git a/docs-v2/pages/abuse/_meta.json b/docs-v2/pages/abuse/_meta.json deleted file mode 100644 index 2073540dec46b..0000000000000 --- a/docs-v2/pages/abuse/_meta.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "index": { - "display": "hidden" - } -} - diff --git a/docs-v2/pages/account/_meta.tsx b/docs-v2/pages/account/_meta.tsx new file mode 100644 index 0000000000000..ac5cbfd10a9dc --- /dev/null +++ b/docs-v2/pages/account/_meta.tsx @@ -0,0 +1,4 @@ +export default { + "user-settings": "User Settings", + "billing-settings": "Billing Settings", +} as const diff --git a/docs-v2/pages/account/billing-settings.mdx b/docs-v2/pages/account/billing-settings.mdx new file mode 100644 index 0000000000000..f1a498395a51a --- /dev/null +++ b/docs-v2/pages/account/billing-settings.mdx @@ -0,0 +1,43 @@ +import Callout from '@/components/Callout' + +# Billing Settings + +You'll find information on your usage data (for specific [Pipedream limits](/workflows/limits/)) in your [Billing Settings](https://pipedream.com/settings/billing). You can also upgrade to [paid plans](https://pipedream.com/pricing) from this page. + +## Subscription + +You can upgrade to [paid plans](https://pipedream.com/pricing) from this section. + +If you've already upgraded, you'll see an option to **Manage Subscription** here, which directs you to your personal Stripe portal. Here, you can change your payment method, review the details of previous invoices, and more. + +## Usage + +[Credits](/pricing/#credits) are Pipedream's billable unit, and users on the [free tier](/pricing/#free-tier) are limited on the number of daily free credits allocated. The **Usage** section displays a chart of the daily credits across a historical range of time to provide insight into your usage patterns. + +Hover over a specific column in the chart to see the number of credits run for that specific day: + +![Daily credits tooltip](/images/account/daily-invocations-tooltip.png) + +_Click_ on a specific column to see credits for that day, broken out by workflow / source: + +![Credits broken out by workflow / source](/images/account/usage-by-resource.png) + +Users on the free tier will see the last 30 days of usage in this chart. Users on [paid plans](https://pipedream.com/pricing) will see the cumulative usage tied to their current billing period. + +## Compute Budget + +Control the maximum number of compute credits permitted on your account with an _Credit Budget_. + +This will restrict your account-wide usage to the specified number of [credits](/pricing/#credits) on a monthly or daily basis. The compute budget does not apply to credits incurred by [dedicated workers](/workflows/building-workflows/settings/#eliminate-cold-starts). + +To enable this feature, _click_ on the toggle and define your maximum number of credits in the period. + + +Due to how credits are accrued, there may be cases where your credit usage may _slightly_ go over the cap. + +In an example scenario, with a cap set at 20 credits and a long-running workflow that uses 10 credits per run, it's possible that two concurrent events trigger the workflow, and the cap won't apply until after the concurrent events are processed. + + +## Limits + +For users on the [Free tier](/pricing/#free-tier), this section displays your usage towards your [credits quota](/workflows/limits/#daily-credits-limit) for the current UTC day. diff --git a/docs-v2/pages/account/user-settings.mdx b/docs-v2/pages/account/user-settings.mdx new file mode 100644 index 0000000000000..665b625b84cc0 --- /dev/null +++ b/docs-v2/pages/account/user-settings.mdx @@ -0,0 +1,85 @@ +import Callout from '@/components/Callout' +import VideoPlayer from '@/components/VideoPlayer' + +# User Settings + +You can find important account details, text editor configuration, and more in your [User Settings](https://pipedream.com/user). + +## Changing your email + +Pipedream sends system emails to the email address tied to your Pipedream login. You can change the email address to which these emails are delivered by modifying the **Email** in your Account Settings. Once changed, an email will be delivered to the new address requesting you verify it. + +Pipedream marketing emails may still be sent to the original email address you used when signing up for Pipedream. To change the email address tied to marketing emails, please [reach out to our team](https://pipedream.com/support). + +## Two-Factor Authentication + + + +Two-factor authentication (2FA) adds an additional layer of security for your Pipedream account and is recommended for all users. + +### Configuring 2FA + +1. Open your [Account Settings](https://pipedream.com/user) +2. Click **Configure** under **Two-Factor Authentication** +3. Scan the QR code in an authenticator app like [1Password](https://1password.com/) or Google Authenticator (available on [iOS](https://apps.apple.com/us/app/google-authenticator/id388497605) and [Android](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en_US&gl=US)) +4. If you're unable to scan the QR code, you can view the setup key to configure 2FA manually in your authenticator app +5. Enter the one-time-password (OTP) from your authenticator app +6. **Save your recovery codes in a secure location**. You'll need these to access your Pipedream account in the case you lose access to your authenticator app. + + +Save your recovery codes + +If you lose access to your authenticator app and your recovery codes, you will permanently lose access to your Pipedream account. **Pipedream Support cannot recover these accounts.** + + +### Signing in with 2FA + +1. You'll be prompted to enter your OTP the next time you sign in to Pipedream +2. When prompted, you can enter the OTP from your authenticator app or a recovery code + + +Using recovery codes + +Each recovery code is a one-time-use code, so make sure to generate new recovery codes in your [Account Settings](https://pipedream.com/user) when you need to. All previously generated recovery codes expire when you generate new ones. + + + +2FA is not currently supported with Single Sign On +Pipedream recommends enabling 2FA with your identity provider. + + +### Requiring 2-Factor Authentication + +Workspaces on the Business plan can [require all workspace members to configure 2FA](/workflows/workspaces/#requiring-two-factor-authentication) in order to log in to Pipedream. + +If you are a member of any workspace where 2FA is required, you cannot disable 2FA, but you can still reconfigure it in your [account settings](https://pipedream.com/account/) if necessary. + + +Admins and Owners control 2FA settings + +Only workspace owner and admin members can enable or disable 2FA for an entire workspace. + + +## Pipedream API Key + +Pipedream provides a [REST API](/rest-api/) for interacting with Pipedream programmatically. You'll find your API key here, which you use to [authorize requests to the API](/rest-api/auth/). + +You can revoke and regenerate your API key from here at any time. + +## Delete Account + +You can delete your Pipedream account at any time by visiting your Account Settings and pressing the **Delete your Account** button. Account deletion is immediately and irreversible. + +## Application + +You can change how the Pipedream app displays data, and basic text editor config, in your [Application Settings](https://pipedream.com/settings/app). + +For example, you can: + +- Change the clock format to 12-hour or 24-hour mode +- Enable Vim keyboard shortcuts in the Pipedream text editor, or enable word wrap +- Set the number of spaces that will be added in the editor when pressing `Tab` + +## Environment Variables + +Environment variables allow you to securely store secrets or other config values that you can access in Pipedream workflows via `process.env`. [Read more about environment variables here](/workflows/environment-variables/). diff --git a/docs-v2/pages/airtable/_meta.json b/docs-v2/pages/airtable/_meta.json deleted file mode 100644 index fbd4ce3ee87ca..0000000000000 --- a/docs-v2/pages/airtable/_meta.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "oauth-migration-2024-02": { - "display": "children" - } -} - diff --git a/docs-v2/pages/airtable/oauth-migration-2024-02/_meta.json b/docs-v2/pages/airtable/oauth-migration-2024-02/_meta.json deleted file mode 100644 index 2073540dec46b..0000000000000 --- a/docs-v2/pages/airtable/oauth-migration-2024-02/_meta.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "index": { - "display": "hidden" - } -} - diff --git a/docs-v2/pages/api/_meta.json b/docs-v2/pages/api/_meta.json deleted file mode 100644 index fdd9321f04fbe..0000000000000 --- a/docs-v2/pages/api/_meta.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "index": "Overview", - "auth": "Authorizing API requests", - "rest": "REST API", - "sse": "SSE API" -} diff --git a/docs-v2/pages/apps/_meta.json b/docs-v2/pages/apps/_meta.json deleted file mode 100644 index 6f84a35764b44..0000000000000 --- a/docs-v2/pages/apps/_meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "index": "Integrated Apps", - "contributing": "Contributing" -} diff --git a/docs-v2/pages/apps/contributing.mdx b/docs-v2/pages/apps/contributing.mdx deleted file mode 100644 index 10e003ff07c6d..0000000000000 --- a/docs-v2/pages/apps/contributing.mdx +++ /dev/null @@ -1,99 +0,0 @@ -import Callout from '@/components/Callout' -import { FileTree } from 'nextra/components' - -# Pipedream Registry - -When developing workflows with pre-built actions and triggers, under the hood you're using [components](/components/) from the [Pipedream Registry Github Repository](https://github.com/pipedreamhq/pipedream). - -Components contributed to the [Pipedream Registry Github Repository](https://github.com/pipedreamhq/pipedream) are published to the [Pipedream marketplace](https://pipedream.com/apps) and are listed in -the Pipedream UI when building workflows. - - -What is a component? - -If you haven't yet, we recommend starting with our Component Development Quickstart Guides for [sources](/components/quickstart/nodejs/sources/) -and [actions](/components/quickstart/nodejs/actions/) to learn how to build components and privately publish them to your account. - - -## Registry Components Structure - -All Pipedream registry components live in [this GitHub repo](https://github.com/PipedreamHQ/pipedream) under the [`components`](https://github.com/PipedreamHQ/pipedream/tree/master/components) directory. - -Every integrated app on Pipedream has a corresponding directory that defines the actions and sources available for that app. Below is a simplified version of the [Airtable app directory](https://github.com/PipedreamHQ/pipedream/tree/master/components/airtable) within the registry: - - - - - - - - - - - - - - - - - - - - - - -In the example above, the `components/airtable/actions/get-record/get-record.mjs` component is published as the **Get Record** action under the **Airtable** app within the workflow builder in Pipedream. - - -The repository is missing the app directory I'd like to add components for - -You can request to have new apps integrated into Pipedream. - -Once the Pipedream team integrates the app, we'll create a directory for the app in the [`components`](https://github.com/PipedreamHQ/pipedream/tree/master/components) directory of the GitHub repo. - - -## Contribution Process - -Anyone from the community can build [sources](/sources/) and [actions](/components#actions) for integrated apps. - -To submit new components or update existing components: - -1. Fork the public [Pipedream Registry Github Repository](https://github.com/pipedreamhq/pipedream). -2. Create a new component within the corresponding app's directory within the `components` directory (if applicable). -3. [Create a PR for the Pipedream team to review](https://github.com/PipedreamHQ/pipedream/compare). -4. Address any feedback provided by Pipedream based on the best practice [Component Guidelines & Patterns](/components/guidelines/). -5. Once the review is complete and approved, Pipedream will merge the PR to the `master` branch -6. The component will be available for use within workflows for all Pipedream developers! 🎉 - -### Component Development Discussion - -Join the discussion with other Pipedream component developers at the [#contribute channel](https://pipedream-users.slack.com/archives/C01E5KCTR16) in Slack or [on Discourse](https://pipedream.com/community/c/dev/11). - - -Not sure what to build? - -Need inspiration? Check out [sources](https://github.com/PipedreamHQ/pipedream/issues?q=is%3Aissue+is%3Aopen+%5BSOURCE%5D+in%3Atitle) -and [actions](https://github.com/PipedreamHQ/pipedream/issues?q=is%3Aissue+is%3Aopen+%5BACTION%5D+in%3Atitle+) requested by the community! - - -## Reference Components - -The following components arfor developing sources and -actions for Pipedream's registry. - -### Reference Sources - -| Name | App | Type | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------- | -| [New Card](https://github.com/pipedreamhq/pipedream/blob/master/components/trello/sources/new-card/new-card.mjs) | Trello | Webhook | -| [New or Modified Files](https://github.com/pipedreamhq/pipedream/blob/master/components/google_drive/sources/new-or-modified-files/new-or-modified-files.mjs) | Google Drive | Webhook + Polling | -| [New Submission](https://github.com/pipedreamhq/pipedream/blob/master/components/jotform/sources/new-submission/new-submission.mjs) | Jotform | Webhook (with no unique hook ID) | - -### Reference Actions - -| Name | App | -| ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| [Add Multiple Rows](https://github.com/PipedreamHQ/pipedream/blob/master/components/google_sheets/actions/add-multiple-rows/add-multiple-rows.mjs) | Google Sheets | -| [Send Message](https://github.com/PipedreamHQ/pipedream/blob/master/components/discord_webhook/actions/send-message/send-message.mjs) | Discord | -| [Append Text](https://github.com/PipedreamHQ/pipedream/blob/master/components/google_docs/actions/append-text/append-text.mjs) | Google Docs | -| [`GET` request](https://github.com/PipedreamHQ/pipedream/blob/master/components/http/actions/get-request/get-request.mjs) | HTTP | diff --git a/docs-v2/pages/apps/index.mdx b/docs-v2/pages/apps/index.mdx deleted file mode 100644 index cb09d27ecc91b..0000000000000 --- a/docs-v2/pages/apps/index.mdx +++ /dev/null @@ -1,79 +0,0 @@ -import Callout from '@/components/Callout' - -# Integrated Apps - -Pipedream has built-in integrations with more than {process.env.PUBLIC_APPS} apps. Since you can [write any code](/code/nodejs/) on Pipedream, and pass API keys or credentials using [environment variables](/environment-variables/), you can connect to virtually any service, so the list is not exhaustive. - -But Pipedream-integrated apps provide a few benefits: - -- You can [connect the app once](/connected-accounts/) and [link that connected account to any step of a workflow](/connected-accounts/#connecting-accounts) -- Pipedream provides [pre-built actions](/components#actions) that wrap common operations for the app. You shouldn't have to write the code to send a message to Slack, or add a new row to a Google Sheet, so actions make that easy. Actions are just code, so you can fork and modify them, or even [publish your own to the Pipedream community](/apps/contributing/). -- [You have access to your API keys and access tokens in code steps](/code/nodejs/auth/), so you can write any code to authorize custom requests to these apps. - -## Premium Apps - -The vast majority of integrated apps on Pipedream are free to use in your workflows across any plan. However, in order to use any of the below apps in an active workflow, your workspace will need to have access to [Premium Apps](https://pipedream.com/pricing): - -- [ActiveCampaign](https://pipedream.com/apps/activecampaign) -- [ADP](https://pipedream.com/apps/adp) -- [Amazon Advertising](https://pipedream.com/apps/amazon_advertising) -- [Asana](https://pipedream.com/apps/asana) -- [AWS](https://pipedream.com/apps/aws) -- [Azure OpenAI Service](https://pipedream.com/apps/azure-openai-service) -- [BigCommerce](https://pipedream.com/apps/bigcommerce) -- [Cisco Webex](https://pipedream.com/apps/cisco-webex) -- [Cisco Webex (Custom App)](https://pipedream.com/apps/cisco-webex-custom-app) -- [Close](https://pipedream.com/apps/close) -- [Cloudinary](https://pipedream.com/apps/cloudinary) -- [Customer.io](https://pipedream.com/apps/customer-io) -- [Datadog](https://pipedream.com/apps/datadog) -- [dbt Cloud](https://pipedream.com/apps/dbt) -- [ERPNext](https://pipedream.com/apps/erpnext) -- [Exact](https://pipedream.com/apps/exact) -- [Freshdesk](https://pipedream.com/apps/freshdesk) -- [Google Cloud](https://pipedream.com/apps/google-cloud) -- [Gorgias](https://pipedream.com/apps/gorgias-oauth) -- [HubSpot](https://pipedream.com/apps/hubspot) -- [Intercom](https://pipedream.com/apps/intercom) -- [Jira](https://pipedream.com/apps/jira) -- [Jira Service Desk](https://pipedream.com/apps/jira-service-desk) -- [Klaviyo](https://pipedream.com/apps/klaviyo) -- [Linkedin](https://pipedream.com/apps/linkedin) -- [Linkedin Ads](https://pipedream.com/apps/linkedin-ads) -- [Mailchimp](https://pipedream.com/apps/mailchimp) -- [Mailgun](https://pipedream.com/apps/mailgun) -- [MongoDB](https://pipedream.com/apps/mongodb) -- [Outreach](https://pipedream.com/apps/outreach) -- [PagerDuty](https://pipedream.com/apps/pagerduty) -- [Pinterest](https://pipedream.com/apps/pinterest) -- [Pipedrive](https://pipedream.com/apps/pipedrive) -- [Pipefy](https://pipedream.com/apps/pipefy) -- [Propeller](https://pipedream.com/apps/propeller) -- [Quickbooks](https://pipedream.com/apps/quickbooks) -- [Rebrandly](https://pipedream.com/apps/rebrandly) -- [ReCharge](https://pipedream.com/apps/recharge) -- [Salesforce (REST API)](https://pipedream.com/apps/salesforce_rest_api) -- [Segment](https://pipedream.com/apps/segment) -- [SendinBlue](https://pipedream.com/apps/sendinblue) -- [ServiceNow](https://pipedream.com/apps/servicenow) -- [ShipStation](https://pipedream.com/apps/shipstation) -- [Shopify](https://pipedream.com/apps/shopify) -- [Snowflake](https://pipedream.com/apps/snowflake) -- [Stripe](https://pipedream.com/apps/stripe) -- [Twilio SendGrid](https://pipedream.com/apps/sendgrid) -- [WhatsApp Business](https://pipedream.com/apps/whatsapp-business) -- [WooCommerce](https://pipedream.com/apps/woocommerce) -- [Xero Accounting](https://pipedream.com/apps/xero_accounting_api) -- [Zendesk](https://pipedream.com/apps/zendesk) -- [Zoom Admin](https://pipedream.com/apps/zoom_admin) -- [Zoho Books](https://pipedream.com/apps/zoho_books) -- [Zoho CRM](https://pipedream.com/apps/zoho_crm) -- [Zoho People](https://pipedream.com/apps/zoho_people) -- [Zoho SalesIQ](https://pipedream.com/apps/zoho_salesiq) - - -Missing an integration? -If we don't have an integration for an app that you'd like to see, please [request let us know](https://pipedream.com/support) or [contribute it to the source available Pipedream registry](/apps/contributing/). - - -**Check out the full list of integrated apps [here](https://pipedream.com/apps).** diff --git a/docs-v2/pages/cli/_meta.json b/docs-v2/pages/cli/_meta.json deleted file mode 100644 index 1aed52464a260..0000000000000 --- a/docs-v2/pages/cli/_meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "reference": "CLI Reference", - "install": "Install", - "login": "Login" -} diff --git a/docs-v2/pages/cli/login.mdx b/docs-v2/pages/cli/login.mdx deleted file mode 100644 index c792a8c7502ef..0000000000000 --- a/docs-v2/pages/cli/login.mdx +++ /dev/null @@ -1,58 +0,0 @@ -# Logging into the CLI - -To start using the Pipedream CLI, you'll need to link it to your Pipedream account. If you don't have a Pipedream account, you can sign up from the CLI. - - - -## Existing Pipedream account - -If you already have a Pipedream account, run - -``` -pd login -``` - -This will open up a new window in your default browser. If you're already logged into your Pipedream account in this browser, this will immediately link the CLI to your account, writing your API key for that account to your [`pd` config file](/cli/reference/#cli-config-file). - -Otherwise, you'll be asked to login. - -Once you're done, go back to your shell and you should see confirmation that your account is linked: - -``` -> pd login -Logged in as dylburger (dylan@pipedream.com) -``` - -Then [follow this guide](/cli/reference/#creating-a-profile-for-a-workspace) to learn how to find your workspace ID and associate it with a `pd` profile. - -## Signing up for Pipedream via the CLI - -If you haven't signed up for a Pipedream account, you can create an account using the CLI: - -``` -pd signup -``` - -This will open up a new window in your default browser. You'll be asked to sign up for Pipedream here. Once you do, your account will be linked to the CLI, writing your API key for that account to your [`pd` config file](/cli/reference/#cli-config-file). - -Once you're done, go back to your shell and you should see confirmation that your account is linked: - -``` -> pd signup -Logged in as dylburger (dylan@pipedream.com) -``` - -## Logging out of the CLI - -You can log out of the CLI by running: - -``` -pd logout -``` - -This will remove your API key from the [`pd` config file](/cli/reference/#cli-config-file). - -## Using the CLI to manage multiple accounts - -If you have multiple Pipedream accounts, you can use [profiles](/cli/reference/#profiles) to ensure the CLI can manage resources for each. - diff --git a/docs-v2/pages/cli/reference.mdx b/docs-v2/pages/cli/reference.mdx deleted file mode 100644 index 7db254b9ea54c..0000000000000 --- a/docs-v2/pages/cli/reference.mdx +++ /dev/null @@ -1,355 +0,0 @@ -# CLI Reference - -## Installing the CLI - -[See the CLI installation docs](/cli/install/) to learn how to install the CLI for your OS / architecture. - -## Command Reference - -Run `pd` to see a list of all commands with basic usage info, or run `pd help ` to display help docs for a specific command. - -We've also documented each command below, with usage examples for each. - -### General Notes - -Everywhere you can refer to a specific component as an argument, you can use the component's ID _or_ its name slug. For example, to retrieve details about a specific source using `pd describe`, you can use either of the following commands: - -```bash -> pd describe dc_abc123 - - id: dc_abc123 - name: http - endpoint: https://myendpoint.m.pipedream.net - -> pd describe http -Searching for sources matching http - - id: dc_abc123 - name: http - endpoint: https://myendpoint.m.pipedream.net -``` - -### `pd delete` - -Deletes an event source. Run: - -```bash -pd delete -``` - -Run `pd list so` to display a list of your event sources. - -### `pd deploy` - -Deploy an event source from local or remote code. - -Running `pd deploy`, without any arguments, brings up an interactive menu asking you select a source. This list of sources is retrieved from the registry of public sources [published to Github](https://github.com/PipedreamHQ/pipedream/tree/master/components). - -When you select a source, we'll deploy it and start listening for new events. - -You can also deploy a specific source via the source's `key` (defined in the component file for the source): - -```bash -pd deploy http-new-requests -``` - -or author a component locally and deploy that local file: - -```bash -pd deploy http.js -``` - -[Read more about authoring your own event sources](/components/quickstart/nodejs/sources/). - -### `pd describe` - -Display the details for a source: its id, name, and other configuration details: - -```bash -pd describe SOURCE_ID_OR_NAME -``` - -### `pd dev` - -`pd dev` allows you to interactively develop a source from a local file.`pd dev` will link your local file with the deployed component and watch your local file for changes. When you save changes to your local file, your component will automatically be updated on Pipedream. - -```bash -pd dev FILE_OR_NAME -``` - -If you quit `pd dev` and want to link the same deployed source to your local file, you can pass the deployed component ID using the `--dc` flag: - -```bash -pd dev --dc SOURCE_ID FILE_OR_NAME -``` - -### `pd events` - -Returns historical events sent to a source, and streams emitted events directly to the CLI. - -```bash -pd events SOURCE_ID -``` - -By default, `pd events` prints (up to) the last 10 events sent to your source. - -```bash -pd events -n 100 SOURCE_ID_OR_NAME -``` - -`pd events -n N` retrieves the last `N` events sent to your source. We store the last 100 events sent to a source, so you can retrieve a max of 100 events using this command. - -```bash -pd events -f SOURCE_ID_OR_NAME -``` - -`pd events -f` connects to the [SSE stream tied to your source](/api/sse/) and displays events as the source produces them. - -```bash -pd events -n N -f SOURCE_ID_OR_NAME -``` - -You can combine the `-n` and `-f` options to list historical events _and_ follow the source for new events. - -### `pd help` - -Displays help for any command. Run `pd help events`, `pd help describe`, etc. - -### `pd init` - -Generate new app and component files from templates. - -#### `pd init app` - -Creates a directory and [an app file](/components/guidelines/#app-files) from a template - -```bash -# Creates google_calendar/ directory and google_calendar.mjs file -pd init app google_calendar -``` - -#### `pd init action` - -Creates a new directory and [a component action](/components#actions) from a template. - -```bash -# Creates add-new-event/ directory and add-new-event.mjs file -pd init action add-new-event -``` - -#### `pd init source` - -Creates a new directory and [an event source](/sources/) from a template. - -```bash -# Creates cancelled-event/ directory and cancelled_event.mjs file -pd init source cancelled-event -``` - -You can attach [database](/components/api/#db), [HTTP](/components/api/#http), or [Timer](/components/api/#timer) props to your template using the following flags: - -| Prop type | Flag | -| --------- | --------- | -| Database | `--db` | -| HTTP | `--http` | -| Timer | `--timer` | - -For example, running: - -```bash -pd init source cancelled-event --db --http --timer -``` - -will include the following props in your new event source: - -```javascript -props: { - db: "$.service.db", - http: "$.interface.http", - timer: "$.interface.timer", -} -``` - -### `pd list` - -Lists Pipedream sources running in your account. Running `pd list` without any arguments prompts you to select the type of resource you'd like to list. - -You can also list specific resource types directly: - -```bash -pd list components -``` - -```bash -pd list streams -``` - -`sources` and `streams` have shorter aliases, too: - -```bash -pd list so -``` - -```bash -pd list st -``` - -### `pd login` - -Log in to Pipedream CLI and persist API key locally. See [Logging into the CLI](/cli/login/) for more information. - -### `pd logout` - -Unsets the local API key tied to your account. - -Running `pd logout` without any arguments removes the default API key from your [config file](/cli/reference/#cli-config-file). - -You can remove the API key for a specific profile by running: - -```bash -pd logout -p PROFILE -``` - -### `pd logs` - -Event sources produce logs that can be useful for troubleshooting issues with that source. `pd logs` displays logs for a source. - -Running `pd logs ` connects to the [SSE logs stream tied to your source](/sources/logs/), displaying new logs as the source produces them. - -Any errors thrown by the source will also appear here. - -### `pd publish` - -To publish an action, use the `pd publish` command. - -```bash -pd publish -``` - -For example: - -```bash -pd publish my-action.js -``` - -### `pd signup` - -Sign up for Pipedream via the CLI and persist your API key locally. See the docs on [Signing up for Pipedream via the CLI](/cli/login/#signing-up-for-pipedream-via-the-cli) for more information. - -### `pd unpublish` - -Unpublish a component you've published to your account. If you publish a source or action that you no longer need, you can unpublish it by component `key`: - -``` -pd unpublish component -``` - -### `pd update` - -Updates the code, props, or metadata for an event source. - -If you deployed a source from Github, for example, someone might publish an update to that source, and you may want to run the updated code. - -```bash -pd update SOURCE_ID_OR_NAME \ - --code https://github.com/PipedreamHQ/pipedream/blob/master/components/http/sources/new-requests/new-requests.js -``` - -You can change the name of a source: - -```bash -pd update SOURCE_ID_OR_NAME --name NEW_NAME -``` - -You can deactivate a source if you want to stop it from running: - -```bash -pd update SOURCE_ID_OR_NAME --deactivate -``` - -or activate a source you previously deactivated: - -```bash -pd update SOURCE_ID_OR_NAME --activate -``` - -## Profiles - -Profiles allow you to work with multiple, named Pipedream accounts via the CLI. - -### Creating a new profile - -When you [login to the CLI](/cli/login/), the CLI writes the API key for that account to your config file, in the `api_key` field: - -```bash -api_key = abc123 -``` - -You can set API keys for other, named profiles, too. Run - -```bash -pd login -p -``` - -`` can be any string of shell-safe characters that you'd like to use to identify this new profile. The CLI opens up a browser asking you to login to your target Pipedream account, then writes the API key to a section of the config file under this profile: - -```bash -[your_profile] -api_key = def456 -``` - -You can also run `pd signup -p ` if you'd like to sign up for a new Pipedream account via the CLI and set a named profile for that account. - -### Creating a profile for a workspace - -If you're working with resources in an [workspace](/workspaces/), you'll need to add an `org_id` to your profile. - -1. [Retrieve your workspaces's ID](/workspaces/#finding-your-workspace-s-id) -2. Open up your [Pipedream config file](#cli-config-file) and create a new [profile](#profiles) with the following information: - -```bash -[profile_name] -api_key = -org_id = -``` - -When using the CLI, pass `--profile ` when running any command. For example, if you named your profile `workspace`, you'd run this command to publish a component: - -```bash -pd publish file.js --profile workspace -``` - -### Using profiles - -You can set a profile on any `pd` command by setting the `-p` or `--profile` flag. For example, to list the sources in a specific account, run: - -```bash -pd list sources --profile PROFILE -``` - -## Version - -To get the current version of the `pd` CLI, run - -```bash -pd --version -``` - -## Auto-upgrade - -The CLI is configured to check for new versions automatically. This ensures you're always running the most up-to-date version. - -## CLI config file - -The `pd` config file contains your Pipedream API keys (tied to your default account, or other [profiles](#profiles)) and other configuration used by the CLI. - -If the `XDG_CONFIG_HOME` env var is set, the config file will be found in `$XDG_CONFIG_HOME/pipedream`. - -Otherwise, it will be found in `$HOME/.config/pipedream`. - -## Analytics - -Pipedream tracks CLI usage data to report errors and usage stats. We use this data exclusively for the purpose of internal analytics (see [our privacy policy](https://pipedream.com/privacy) for more information). - -If you'd like to opt-out of CLI analytics, set the `PD_CLI_DO_NOT_TRACK` environment variable to `true` or `1`. - diff --git a/docs-v2/pages/code/_meta.json b/docs-v2/pages/code/_meta.json deleted file mode 100644 index eb0fa447021a3..0000000000000 --- a/docs-v2/pages/code/_meta.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "index": "Overview", - "nodejs": "Node.js", - "python": "Python", - "go": "Go", - "bash": "Bash" -} diff --git a/docs-v2/pages/code/index.mdx b/docs-v2/pages/code/index.mdx deleted file mode 100644 index b821a27e31d4c..0000000000000 --- a/docs-v2/pages/code/index.mdx +++ /dev/null @@ -1,25 +0,0 @@ -import icons from '@/utils/icons' -import LanguageLink from "@/components/LanguageLink"; -import VideoPlayer from "@/components/VideoPlayer"; - -# Overview - - - -Pipedream comes with thousands of prebuilt [triggers](/workflows/steps/triggers/) and [actions](/components#actions) for [hundreds of apps](https://pipedream.com/apps). Often, these will be sufficient for building simple workflows. - -But sometimes you need to run your own custom logic. You may need to make an API request to fetch additional metadata about the event, transform data into a custom format, or end the execution of a workflow early under some conditions. **Code steps let you do this and more**. - -Code steps let you execute [Node.js v{process.env.PIPEDREAM_NODE_VERSION}](https://nodejs.org/) (JavaScript) code, Python, Go or even Bash right in a workflow. - -Choose a language to get started: - -
- - - - -
- -If you'd like to see another, specific language supported, please [let us know](https://pipedream.com/community). - diff --git a/docs-v2/pages/code/nodejs/_meta.json b/docs-v2/pages/code/nodejs/_meta.json deleted file mode 100644 index 645efc771e403..0000000000000 --- a/docs-v2/pages/code/nodejs/_meta.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "index": "Running Node.js in workflows", - "ai-code-generation": "Use AI to generate code", - "auth": "Connect apps", - "http-requests": "HTTP requests", - "working-with-files": "Files", - "using-data-stores": "Data stores", - "delay": "Delaying steps", - "rerun": "Pause, resume, and rerun steps", - "async": "Running asynchronous code", - "sharing-code": "Sharing code across workflows", - "browser-automation": "Browser automation" -} diff --git a/docs-v2/pages/code/nodejs/auth.mdx b/docs-v2/pages/code/nodejs/auth.mdx deleted file mode 100644 index e1cc0d376ffb3..0000000000000 --- a/docs-v2/pages/code/nodejs/auth.mdx +++ /dev/null @@ -1,123 +0,0 @@ -import VideoPlayer from '@/components/VideoPlayer'; - -# Connecting apps in Node.js - - - -When you use [prebuilt actions](/components#actions) tied to apps, you don't need to write the code to authorize API requests. Just [connect your account](/connected-accounts/#connecting-accounts) for that app and run your workflow. - -But sometimes you'll need to [write your own code](/code/nodejs/). You can also connect apps to custom code steps, using the auth information to authorize requests to that app. - -For example, you may want to send a Slack message from a step. We use Slack's OAuth integration to authorize sending messages from your workflows. - -To wire up a Slack account to a workflow, define it as a `prop` to the workflow. - -```javascript -import { WebClient } from '@slack/web-api' - -export default defineComponent({ - props: { - // This creates a connection called "slack" that connects a Slack account. - slack: { - type: 'app', - app: 'slack' - } - }, - async run({ steps, $ }) { - const web = new WebClient(this.slack.$auth.oauth_access_token) - - return await web.chat.postMessage({ - text: "Hello, world!", - channel: "#general", - }) - } -}); -``` - -Now the step in the workflow builder will allow you to connect your Slack account: - -![Connect a Slack account to a Node.js code step using a prop](/images/code/nodejs/auth/slack-auth-prop-example.png) - -## Accessing connected account data with `this.appName.$auth` - -In our Slack example above, we created a Slack `WebClient` using the Slack OAuth access token: - -```javascript -const web = new WebClient(this.slack.$auth.oauth_access_token); -``` - -Where did `this.slack` come from? Good question. It was generated by the definition we made in `props`: - -```javascript{4,6,8} -export default defineComponent({ - props: { - // the name of the app from the key of the prop, in this case it's "slack" - slack: { - // define that this prop is an app - type: 'app', - // define that this app connects to Slack - app: 'slack' - } - } - // ... rest of the Node.js step -``` - -The Slack access token is generated by Pipedream, and is available to this step in the `this.slack.$auth` object: - -```javascript -export default defineComponent({ - props: { - slack: { - type: 'app', - app: 'slack' - } - }, - async run({ steps, $ }) { - async (steps, $) => { - // Authentication details for all of your apps are accessible under the special $ variable: - console.log(this.slack.$auth); - } - }) -}); -``` - -`this.appName.$auth` contains named properties for each account you connect to the associated step. Here, we connected Slack, so `this.slack.$auth` contains the Slack auth info (the `oauth_access_token`). - -The names of the properties for each connected account will differ with the account. Pipedream typically exposes OAuth access tokens as `oauth_access_token`, and API keys under the property `api_key`. But if there's a service-specific name for the tokens (for example, if the service calls it `server_token`), we prefer that name, instead. - -To list the `this.[app name].$auth` properties available to you for a given app, run `Object.keys` on the app: - -```javascript -console.log(Object.keys(this.slack.$auth)) // Replace this.slack with your app's name -``` - -and run your workflow. You'll see the property names in the logs below your step. - -## Writing custom steps to use `this.appName.$auth` - -You can write code that utilizes connected accounts in a few different ways: - -### Using the code templates tied to apps - -When you write custom code that connects to an app, you can start with a code snippet Pipedream provides for each app. This is called the **test request**. - -When you search for an app in a step: - -1. Click the **+** button below any step. -2. Search for the app you're looking for and select it from the list. -3. Select the option to **Use any \ API**. - -This code operates as a template you can extend, and comes preconfigured with the connection to the target app and the code for authorizing requests to the API. You can modify this code however you'd like. - -### Manually connecting apps to steps - -See the Connected Accounts docs for [connecting an account to a code step](/connected-accounts/#from-a-code-step). - -## Custom auth tokens / secrets - -When you want to connect to a 3rd party service that isn't supported by Pipedream, you can store those secrets in [Environment Variables](/environment-variables/). - -## Learn more about `props` - -Not only can `props` be used to connect apps to workflow steps, but they can also be used to [collect properties collected from user input](/code/nodejs/#passing-props-to-code-steps) and [save data between workflow runs](/code/nodejs/using-data-stores/). - diff --git a/docs-v2/pages/code/nodejs/delay.mdx b/docs-v2/pages/code/nodejs/delay.mdx deleted file mode 100644 index 6a0bb4af06e9e..0000000000000 --- a/docs-v2/pages/code/nodejs/delay.mdx +++ /dev/null @@ -1,142 +0,0 @@ -import Callout from '@/components/Callout' -import VideoPlayer from '@/components/VideoPlayer' - -# Delaying a workflow - - - -Use `$.flow.delay` to [delay a step in a workflow](/workflows/flow-control/#delay). - -These docs show you how to write Node.js code to handle delays. If you don't need to write code, see [our built-in delay actions](/workflows/flow-control/#delay-actions). - - - -## Using `$.flow.delay` - -`$.flow.delay` takes one argument: the number of **milliseconds** you'd like to pause your workflow until the next step executes. {process.env.DELAY_MIN_MAX_TIME}. - -Note that [delays happen at the end of the step where they're called](#when-delays-happen). - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - // Delay a workflow for 60 seconds (60,000 ms) - $.flow.delay(60 * 1000) - - // Delay a workflow for 15 minutes - $.flow.delay(15 * 60 * 1000) - - // Delay a workflow based on the value of incoming event data, - // or default to 60 seconds if that variable is undefined - $.flow.delay(steps.trigger.event?.body?.delayMs ?? 60 * 1000) - - // Delay a workflow a random amount of time - $.flow.delay(Math.floor(Math.random() * 1000)) - } -}); -``` - - -Paused workflow state - -When `$.flow.delay` is executed in a Node.js step, the workflow itself will enter a **Paused** state. - -While the workflow is paused, it will not incur any credits towards compute time. You can also [view all paused workflows in the Event History](/event-history/#filtering-by-status). - - -### Credit usage - -The length of time a workflow is delayed from `$.flow.delay` does _not_ impact your credit usage. For example, delaying a 256 megabyte workflow for five minutes will **not** incur ten credits. - -However, using `$.flow.delay` in a workflow will incur two credits. - -One credit is used to initially start the workflow, then the second credit is used when the workflow resumes after its pause period has ended. - - -Exact credit usage depends on duration and memory configuration - -If your workflow's [execution timeout limit](/workflows/settings/#execution-timeout-limit) is set to longer than [default limit](/limits/#time-per-execution), it may incur more than two [credits](/pricing/#credits) when using `pd.flow.delay`. - - -## `cancel_url` and `resume_url` - -Both the built-in **Delay** actions and `$.flow.delay` return a `cancel_url` and `resume_url` that lets you cancel or resume paused executions. - -These URLs are specific to a single execution of your workflow. While the workflow is paused, you can load these in your browser or send an HTTP request to either: - -- Hitting the `cancel_url` will immediately cancel that execution -- Hitting the `resume_url` will immediately resume that execution early - -[Since Pipedream pauses your workflow at the _end_ of the step where you run call `$.flow.delay`](#when-delays-happen), you can send these URLs to third party systems, via email, or anywhere else you'd like to control the execution of your workflow. - -```javascript -import axios from 'axios' - -export default defineComponent({ - async run({ steps, $ }) { - const { cancel_url, resume_url } = $.flow.delay(15 * 60 * 1000) - - // Send the URLs to a system you own - await axios({ - method: "POST", - url: `https://example.com`, - data: { cancel_url, resume_url }, - }); - - // Email yourself the URLs. Click on the links to cancel / resume - $.send.email({ - subject: `Workflow execution ${steps.trigger.context.id}`, - text: `Cancel your workflow here: ${cancel_url} . Resume early here: ${resume_url}`, - }); - } -}); - -// Delay happens at the end of this step -``` - -## When delays happen - -**Pipedream pauses your workflow at the _end_ of the step where you call `$.flow.delay`**. This lets you [send the `cancel_url` and `resume_url` to third-party systems](#cancel-url-and-resume-url). - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - const { cancel_url, resume_url } = $.flow.delay(15 * 60 * 1000) - // ... run any code you want here - } -}); - -// Delay happens at the end of this step -``` - -## Delays and HTTP responses - -You cannot run `$.respond` after running `$.flow.delay`. Pipedream ends the original execution of the workflow when `$.flow.delay` is called and issues the following response to the client to indicate this state: - -> $.respond() not called for this invocation - -If you need to set a delay on an HTTP request triggered workflow, consider using [`setTimeout`](#settimeout) instead. - -## `setTimeout` - -Alternatively, you can use `setTimeout` instead of using `$.flow.delay` to delay individual workflow steps. - -However, there are some drawbacks to using `setTimeout` instead of `$.flow.delay`. `setTimeout` will count towards your workflow's compute time, for example: - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - // delay this step for 30 seconds - const delay = 30000; - - return new Promise((resolve, reject) => { - setTimeout(() => { - resolve('timer ended') - }, delay) - }) - } -}); - -``` - -The Node.js step above will hold the workflow's execution for this step for 30 seconds; however, 30 seconds will also _contribute_ to your credit usage. Also consider that workflows have a hard limit of {process.env.MAX_WORKFLOW_EXECUTION_LIMIT} seconds. diff --git a/docs-v2/pages/code/nodejs/http-requests.mdx b/docs-v2/pages/code/nodejs/http-requests.mdx deleted file mode 100644 index 6f6a7332bc373..0000000000000 --- a/docs-v2/pages/code/nodejs/http-requests.mdx +++ /dev/null @@ -1,667 +0,0 @@ -import Callout from '@/components/Callout' -import { Tabs } from 'nextra/components' - -# Make HTTP Requests with Node.js - -HTTP requests are fundamental to working with APIs or other web services. You can make HTTP requests to retrieve data from APIs, fetch HTML from websites, or do pretty much anything your web browser can do. - -**Below, we'll review how to make HTTP requests using Node.js code on Pipedream.** - -We'll use the [`axios`](https://github.com/axios/axios) and [`got`](https://github.com/sindresorhus/got) HTTP clients in the examples below, but [you can use any npm package you'd like](/code/nodejs/#using-npm-packages) on Pipedream, so feel free to experiment with other clients, too. - -If you're developing Pipedream components, you may find the [`@pipedream/platform` version of `axios`](/pipedream-axios/) helpful for displaying error data clearly in the Pipedream UI. - -If you're new to HTTP, see our [glossary of HTTP terms](https://requestbin.com/blog/working-with-webhooks/#webhooks-glossary-common-terms) for a helpful introduction. - -## Basic `axios` usage notes - -To use `axios` on Pipedream, you'll just need to import the `axios` npm package: - -```javascript -import axios from "axios"; -``` - -You make HTTP requests by passing a [JavaScript object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects) to `axios` that defines the parameters of the request. For example, you'll typically want to define the HTTP method and the URL you're sending data to: - -```javascript -{ - method: "GET", - url: `https://swapi.dev/api/films/` -} -``` - -`axios` returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises), which is just a fancy way of saying that it makes the HTTP request in the background (asynchronously) while the rest of your code runs. On Pipedream, [all asynchronous code must be run synchronously](/code/nodejs/async/), which means you'll need to wait for the HTTP request to finish before moving on to the next step. You do this by adding an `await` in front of the call to `axios`. - -**Putting all of this together, here's how to make a basic HTTP request on Pipedream:** - -```javascript -const resp = await axios({ - method: "GET", - url: `https://swapi.dev/api/films/`, -}); -``` - -The response object `resp` contains a lot of information about the response: its data, headers, and more. Typically, you just care about the data, which you can access in the `data` property of the response: - -```javascript -const resp = await axios({ - method: "GET", - url: `https://swapi.dev/api/films/`, -}); - -// HTTP response data is in the data property -const data = resp.data; -``` - -Alternatively, you can access the data using [object destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring), which is equivalent to the above and preferred in modern JavaScript: - -```javascript -const { data } = resp; -``` - -## Send a `GET` request to fetch data - -Make a request to retrieve Star Wars films from the Star Wars API: - - - -``` javascript -import axios from "axios"; - -export default defineComponent({ - async run({ steps, $ }) { - // Make an HTTP GET request using axios - const res = await axios({ - method: "GET", - url: `https://swapi.dev/api/films/`, - }); - - // Retrieve just the data from the response - const { data } = res; - } -}); -``` - - -``` javascript -export default defineComponent({ - props: { - httpRequest: { - type: "http_request", - label: "Star Wars API request", - default: { - method: "GET", - url: "https://swapi.dev/api/films/" - } - }, - }, - async run({ steps, $ }) { - // Make an HTTP GET request using the http-request - const res = await this.httpRequest.execute(); - - // Retrieve just the data from the response - const { data } = res; - }, -}) -``` - -**Produces** - -![With the http-request prop](https://res.cloudinary.com/pipedreamin/image/upload/v1649961271/docs/components/CleanShot_2022-04-14_at_14.34.16_2x_c0urph.png) - - - -## Send a `POST` request to submit data - -POST sample JSON to [JSONPlaceholder](https://jsonplaceholder.typicode.com/), a free mock API service: - - - -``` javascript -import axios from "axios"; - -export default defineComponent({ - async run({ steps, $ }) { - // Make an HTTP POST request using axios - const resp = await axios({ - method: "POST", - url: `https://jsonplaceholder.typicode.com/posts`, - data: { - name: "Luke", - }, - }); - - // Retrieve just the data from the response - const { data } = resp; - } -}); -``` - - -``` javascript -export default defineComponent({ - props: { - httpRequest: { - type: "http_request", - label: "JSON Placeholder API request", - default: { - method: "POST", - url: "https://jsonplaceholder.typicode.com/posts", - body: { - contentType: "application/json", - fields: [{ name: "Luke" }] - } - } - }, - }, - async run({ steps, $ }) { - // Make an HTTP GET request using the http-request - const res = await this.httpRequest.execute(); - - // Retrieve just the data from the response - const { data } = res; - }, -}) -``` - - - -When you make a `POST` request, you pass `POST` as the `method`, and include the data you'd like to send in the `data` object. - -## Pass query string parameters to a `GET` request - -Retrieve fake comment data on a specific post using [JSONPlaceholder](https://jsonplaceholder.typicode.com/), a free mock API service. Here, you fetch data from the `/comments` resource, retrieving data for a specific post by query string parameter: `/comments?postId=1`. - - - -``` javascript -import axios from "axios"; - -export default defineComponent({ - async run({ steps, $ }) { - // Make an HTTP GET request using axios - const resp = await axios({ - method: "GET", - url: `https://jsonplaceholder.typicode.com/comments`, - params: { - postId: 1, - }, - }); - - // Retrieve just the data from the response - const { data } = resp; - } -}); -``` - - -``` javascript -export default defineComponent({ - props: { - httpRequest: { - type: "http_request", - label: "JSON Placeholder API request", - default: { - method: "GET", - url: "https://jsonplaceholder.typicode.com/comments", - params: { - fields: [{ postId: 1 }] - } - } - }, - }, - async run({ steps, $ }) { - // Make an HTTP GET request using the http-request - const res = await this.httpRequest.execute(); - - // Retrieve just the data from the response - const { data } = res; - }, -}) -``` - -You should pass query string parameters using the `params` object, like above. When you do, `axios` automatically [URL-encodes](https://www.w3schools.com/tags/ref_urlencode.ASP) the parameters for you, which you'd otherwise have to do manually. - - - -## Send a request with HTTP headers - -You pass HTTP headers in the `headers` object of the `axios` request: - -```javascript -import axios from "axios"; - -// Make an HTTP POST request using axios -const resp = await axios({ - method: "POST", - url: `https://jsonplaceholder.typicode.com/posts`, - headers: { - "Content-Type": "application/json", - }, - data: { - name: "Luke", - }, -}); -``` - -## Send a request with a secret or API key - -Most APIs require you authenticate HTTP requests with an API key or other token. **Please review the docs for your service to understand how they accept this data.** - -Here's an example showing an API key passed in an HTTP header: - -```javascript -import axios from "axios"; - -// Make an HTTP POST request using axios -const resp = await axios({ - method: "POST", - url: `https://jsonplaceholder.typicode.com/posts`, - headers: { - "Content-Type": "application/json", - "X-API-Key": "123", // API KEY - }, - data: { - name: "Luke", - }, -}); -``` - -[Copy this workflow to run this code on Pipedream](https://pipedream.com/@dylburger/send-an-http-request-with-headers-p_q6ClzO/edit). - -## Send multiple HTTP requests in sequence - -There are many ways to make multiple HTTP requests. This code shows you a simple example that sends the numbers `1`, `2`, and `3` in the body of an HTTP POST request: - -```javascript -import axios from "axios"; - -export default defineComponent({ - async run({ steps, $ }) { - // We'll store each response and return them in this array - const responses = []; - - for await (const num of [1, 2, 3]) { - const resp = await axios({ - method: "POST", - url: "https://example.com", - data: { - num, // Will send the current value of num in the loop - }, - }); - responses.push(resp.data); - } - - return responses; - }, -}); -``` - -This sends each HTTP request _in sequence_, one after another, and returns an array of response data returned from the URL to which you send the POST request. If you need to make requests _in parallel_, [see these docs](#send-multiple-http-requests-in-parallel). - -[Copy this workflow](https://pipedream.com/@dylburger/iterate-over-a-pipedream-step-export-sending-multiple-http-requests-p_ljCAPN/edit) and fill in your destination URL to see how this works. **This workflow iterates over the value of a Pipedream [step export](/workflows/steps/#step-exports)** - data returned from a previous step. Since you often want to iterate over data returned from a Pipedream action or other code step, this is a common use case. - -## Send multiple HTTP requests in parallel - -Sometimes you'll want to make multiple HTTP requests in parallel. If one request doesn't depend on the results of another, this is a nice way to save processing time in a workflow. It can significantly cut down on the time you spend waiting for one request to finish, and for the next to begin. - -To make requests in parallel, you can use two techniques. By default, we recommend using [`promise.allSettled`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled), which makes all HTTP requests and returns data on their success / failure. If an HTTP request fails, all other requests will proceed. - -```javascript -import axios from "axios"; - -export default defineComponent({ - async run({ steps, $ }) { - const arr = [ - "https://www.example.com", - "https://www.cnn.com", - "https://www.espn.com", - ]; - const promises = arr.map((url) => axios.get(url)); - return Promise.allSettled(promises); - }, -}); -``` - -First, we generate an array of `axios.get` requests (which are all [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)), and then call `Promise.allSettled` to run them in parallel. - -When you want to stop future requests when _one_ of the requests fails, you can use [`Promise.all`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all), instead: - -```javascript -import axios from "axios"; - -export default defineComponent({ - async run({ steps, $ }) { - const arr = [ - "https://www.example.com", - "https://www.cnn.com", - "https://www.espn.com", - ]; - const promises = arr.map((url) => axios.get(url)); - return Promise.all(promises); - }, -}); -``` - -The Mozilla docs expand on the difference between these methods, and when you may want to use one or the other: - -> The `Promise.allSettled()` method returns a promise that resolves after all of the given promises have either fulfilled or rejected, with an array of objects that each describes the outcome of each promise.
-> It is typically used when you have multiple asynchronous tasks that are not dependent on one another to complete successfully, or you'd always like to know the result of each promise.
-> In comparison, the Promise returned by `Promise.all()` may be more appropriate if the tasks are dependent on each other / if you'd like to immediately reject upon any of them rejecting. - -## Send a `multipart/form-data` request - - - - -``` javascript -import axios from "axios"; -import FormData from "form-data"; - -export default defineComponent({ - async run({ steps, $ }) { - const formData = new FormData(); - formData.append("name", "Luke Skywalker"); - - const headers = formData.getHeaders(); - const config = { - method: "POST", - url: "https://example.com", - headers, - data: formData, - }; - return await axios(config); - } -}); -``` - - - - -``` javascript -export default defineComponent({ - props: { - httpRequest: { - type: "http_request", - label: "Example Multipart Form Request", - default: { - method: "POST", - url: "https://example.com", - headers: { - contentType: "multipart/form-data", - fields: [{ name: "Luke Skywalker" }] - } - } - }, - }, - async run({ steps, $ }) { - // Make an HTTP GET request using the http-request - const res = await this.httpRequest.execute(); - - // Retrieve just the data from the response - const { data } = res; - }, -}) -``` - - - -[Copy this workflow](https://pipedream.com/@dylburger/send-a-multipart-form-data-request-p_WxCQRyr/edit) to run this example. - -## Download a file to the `/tmp` directory - -This example shows you how to download a file to a file in [the `/tmp` directory](/code/nodejs/working-with-files/). This can be especially helpful for downloading large files: it streams the file to disk, minimizing the memory the workflow uses when downloading the file. - -```javascript -import { pipeline } from "stream/promises"; -import fs from "fs"; -import got from "got"; - -export default defineComponent({ - async run({ steps, $ }) { - // Download the webpage HTML file to /tmp - return await pipeline( - got.stream("https://example.com"), - fs.createWriteStream('/tmp/file.html') - ); - } -}) -``` - -[Copy this workflow](https://pipedream.com/new?h=tch_wqKfoW) to run this example. - -## Upload a file from the `/tmp` directory - -This example shows you how to make a `multipart/form-data` request with a file as a form part. You can store and read any files from [the `/tmp` directory](/code/nodejs/working-with-files/#the-tmp-directory). - -This can be especially helpful for uploading large files: it streams the file from disk, minimizing the memory the workflow uses when uploading the file. - -```javascript -import axios from "axios"; -import fs from "fs"; -import FormData from "form-data"; - -export default defineComponent({ - async run({ steps, $ }) { - const formData = new FormData(); - formData.append("file", fs.createReadStream('/tmp/file.pdf')); - const headers = formData.getHeaders(); - - const config = { - method: "POST", - url: "https://example.com", - headers, - data: formData, - }; - return await axios(config); - } -}); -``` - -[Copy this workflow](https://pipedream.com/new?h=tch_Oknf4r) to run this example. - -## IP addresses for HTTP requests made from Pipedream workflows - -By default, [HTTP requests made from Pipedream can come from a large range of IP addresses](/privacy-and-security/#hosting-details). **If you need to restrict the IP addresses HTTP requests come from, you have two options**: - -- [Use a Pipedream VPC](/workflows/vpc/) to route all outbound HTTP requests through a single IP address -- If you don't need to access the HTTP response data, you can [use `$send.http()`](/destinations/http/) to send requests from a [limited set of IP addresses](/destinations/http/#ip-addresses-for-pipedream-http-requests). - - -## Use an HTTP proxy to proxy requests through another host - -By default, HTTP requests made from Pipedream can come from a range of IP addresses. **If you need to make requests from a single IP address, you can route traffic through an HTTP proxy**: - -```javascript -import axios from "axios"; -import httpsProxyAgent from "https-proxy-agent"; - -export default defineComponent({ - props: { - user: { - type: 'string', - label: 'Username', - description: 'The username for the HTTP proxy authentication', - }, - pass: { - type: 'string', - label: 'Password', - secret: true, - description: 'The password for the HTTP proxy authentication', - }, - host: { - type: 'string', - label: "HTTP Proxy Host", - description: "The URL for the HTTP proxy", - }, - port: { - type: "string", - label: "Port", - description: "The port the HTTP proxy is accepting requests at", - }, - target_host: { - type: 'string', - label: "Target Host", - description: "The URL for the end target to reach through the proxy", - }, - method: { - type: 'string', - default: 'GET', - label: "HTTP method", - description: "The HTTP method to use to reach the end target host" - }, - body: { - type: 'object', - label: "HTTP body", - description: "The HTTP body payload to send to the end target host" - } - }, - async run({ steps, $ }) { - const { user, pass, host, port, target_host, method } = this; - const agent = new httpsProxyAgent(`http://${user}:${pass}@${host}:${port}`); - - const config = { - method, - url: target_host, - body, - httpsAgent: agent, - }; - - return await axios.request(config); - } -}); -``` - -[Copy this workflow to run this code on Pipedream](https://pipedream.com/new?h=tch_mypfby). - -## Stream a downloaded file directly to another URL - -Sometimes you need to upload a downloaded file directly to another service, without processing the downloaded file. You could [download the file](#download-a-file-to-the-tmp-directory) and then [upload it](#upload-a-file-from-the-tmp-directory) to the other URL, but these intermediate steps are unnecessary: you can just stream the download to the other service directly, without saving the file to disk. - -This method is especially effective for large files that exceed the [limits of the `/tmp` directory](/limits/#disk). - -[Copy this workflow](https://pipedream.com/@dylburger/stream-download-to-upload-p_5VCLoa1/edit) or paste this code into a [new Node.js code step](/code/nodejs/): - -```javascript -import stream from "stream"; -import { promisify } from "util"; -import got from "got"; - -export default defineComponent({ - async run({ steps, $ }) { - const pipeline = promisify(stream.pipeline); - - await pipeline( - got.stream("https://example.com"), - got.stream.post("https://example2.com") - ); - } -}); -``` - -You'll want to replace `https://example.com` with the URL you'd like to stream data from, and replace `https://example2.com` with the URL you'd like to send the data _to_. `got` streams the content directly, downloading the file using a `GET` request and uploading it as a `POST` request. - -If you need to modify this behavior, [see the `got` Stream API](https://github.com/sindresorhus/got#gotstreamurl-options). - -## Catch and process HTTP errors - -By default, `axios` throws an error when the HTTP response code is in the 400-500 range (a client or server error). If you'd like to process the error data instead of throwing an error, you can pass a custom function to the `validateStatus` property: - -```javascript -import axios from "axios"; - -export default defineComponent({ - async run({ steps, $ }) { - const resp = await axios({ - url: "https://httpstat.us/400", - validateStatus: () => true, // will not throw error when axios gets a 400+ status code (the default behavior) - }); - if (resp.status >= 400) { - this.debug = resp; - throw new Error(JSON.stringify(resp.data)); // This can be modified to throw any error you'd like - } - return resp; - } -}); -``` - -See [the `axios` docs](https://github.com/axios/axios#request-config) for more details. - -## Paginating API requests - -When you fetch data from an API, the API may return records in "pages". For example, if you're trying to fetch a list of 1,000 records, the API might return those in groups of 100 items. - -Different APIs paginate data in different ways. You'll need to consult the docs of your API provider to see how they suggest you paginate through records. - -## Send GraphQL request - -Make a GraphQL request using the `graphql-request` NPM package: - -```javascript -import { graphql } from 'graphql' -import { request, gql } from 'graphql-request' - -export default defineComponent({ - async run({ steps, $ }) { - const document = gql` - query samplePokeAPIquery { - generations: pokemon_v2_generation { - name - pokemon_species: pokemon_v2_pokemonspecies_aggregate { - aggregate { - count - } - } - } - } - ` - return await request('https://beta.pokeapi.co/graphql/v1beta', document) - }, -}) -``` - - -The graphql package is required - -The `graphql` package is required for popular GraphQL clients to function, like `graphql-request` and `urql`. - -Even though you will not need to use the `graphql` code itself in your code step, it's required to import it in order for `graphql-request` to function. - - -### Send an authenticated GraphQL request - -Authenticate your connected accounts in Pipedream with GraphQL requests using the `app` prop: - -```javascript -import { graphql } from 'graphql' -import { GraphQLClient, gql } from 'graphql-request' - -export default defineComponent({ - props: { - github: { - type: 'app', - app: 'github' - } - }, - async run({ steps, $ }) { - const me = gql` - query { - viewer { - login - } - } - ` - - const client = new GraphQLClient('https://api.github.com/graphql', { - headers: { - authorization: `Bearer ${this.github.$auth.oauth_access_token}`, - }, - }) - - return await client.request(me) - }, -}) - -``` - -Alternatively, you can use Environment Variables as well for simple API key based GraphQL APIs. diff --git a/docs-v2/pages/code/nodejs/images/console-dir.png b/docs-v2/pages/code/nodejs/images/console-dir.png deleted file mode 100644 index b2e8a5129ee1d..0000000000000 Binary files a/docs-v2/pages/code/nodejs/images/console-dir.png and /dev/null differ diff --git a/docs-v2/pages/code/nodejs/images/console-log-error.png b/docs-v2/pages/code/nodejs/images/console-log-error.png deleted file mode 100644 index 2a598bf115498..0000000000000 Binary files a/docs-v2/pages/code/nodejs/images/console-log-error.png and /dev/null differ diff --git a/docs-v2/pages/code/nodejs/images/dollar-end.png b/docs-v2/pages/code/nodejs/images/dollar-end.png deleted file mode 100644 index f513aa05bae28..0000000000000 Binary files a/docs-v2/pages/code/nodejs/images/dollar-end.png and /dev/null differ diff --git a/docs-v2/pages/code/nodejs/images/exception-in-code-cell.png b/docs-v2/pages/code/nodejs/images/exception-in-code-cell.png deleted file mode 100644 index cea2fe586fcb4..0000000000000 Binary files a/docs-v2/pages/code/nodejs/images/exception-in-code-cell.png and /dev/null differ diff --git a/docs-v2/pages/code/nodejs/images/exception.png b/docs-v2/pages/code/nodejs/images/exception.png deleted file mode 100644 index a2d724888bc61..0000000000000 Binary files a/docs-v2/pages/code/nodejs/images/exception.png and /dev/null differ diff --git a/docs-v2/pages/code/nodejs/images/new-button.png b/docs-v2/pages/code/nodejs/images/new-button.png deleted file mode 100644 index 7281a37aa97a1..0000000000000 Binary files a/docs-v2/pages/code/nodejs/images/new-button.png and /dev/null differ diff --git a/docs-v2/pages/code/nodejs/images/new-code-step.png b/docs-v2/pages/code/nodejs/images/new-code-step.png deleted file mode 100644 index 7907282d68ac1..0000000000000 Binary files a/docs-v2/pages/code/nodejs/images/new-code-step.png and /dev/null differ diff --git a/docs-v2/pages/code/nodejs/images/syntax-error.png b/docs-v2/pages/code/nodejs/images/syntax-error.png deleted file mode 100644 index 8557e0d96e796..0000000000000 Binary files a/docs-v2/pages/code/nodejs/images/syntax-error.png and /dev/null differ diff --git a/docs-v2/pages/code/nodejs/images/this-checkpoint-observability.png b/docs-v2/pages/code/nodejs/images/this-checkpoint-observability.png deleted file mode 100644 index c30947d4621da..0000000000000 Binary files a/docs-v2/pages/code/nodejs/images/this-checkpoint-observability.png and /dev/null differ diff --git a/docs-v2/pages/code/nodejs/index.mdx b/docs-v2/pages/code/nodejs/index.mdx deleted file mode 100644 index b2133593170a5..0000000000000 --- a/docs-v2/pages/code/nodejs/index.mdx +++ /dev/null @@ -1,484 +0,0 @@ -import Callout from '@/components/Callout' -import VideoPlayer from "@/components/VideoPlayer"; - -# Running Node.js in workflows - -Pipedream supports writing Node.js v{process.env.PIPEDREAM_NODE_VERSION} at any point of a workflow. - -Anything you can do with Node.js, you can do in a workflow. This includes using most of [npm's 400,000+ packages](#using-npm-packages). JavaScript is one of the [most used](https://insights.stackoverflow.com/survey/2019#technology-_-programming-scripting-and-markup-languages) [languages](https://github.blog/2018-11-15-state-of-the-octoverse-top-programming-languages/) in the world, with a thriving community and extensive package ecosystem. If you work on websites and know JavaScript well, Pipedream makes you a full stack engineer. If you've never used JavaScript, see the [resources below](#new-to-javascript). - - -It's important to understand the core difference between Node.js and the JavaScript that runs in your web browser: **Node doesn't have access to some of the things a browser expects, like the HTML on the page, or its URL**. If you haven't used Node before, be aware of this limitation as you search for JavaScript examples on the web. - - -## Adding a code step - -1. Click the **+** button below any step of your workflow. -2. Select the option to **Run custom code**. - -Note that new code steps will default to Node.js v{process.env.PIPEDREAM_NODE_VERSION}. You can add any Node.js code in the editor that appears. For example, try: - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - console.log("This is Node.js code"); - $.export("test", "Some test data"); - return "Test data"; - }, -}); -``` - -Code steps use the same editor ([Monaco](https://microsoft.github.io/monaco-editor/)) used in Microsoft's [VS Code](https://code.visualstudio.com/), which supports syntax highlighting, automatic indentation, and more. - -## Sharing data between steps - -A Node.js step can use data from other steps using [step exports](/workflows/steps/#step-exports), it can also export data for other steps to use. - -### Using data from another step - -In Node.js steps, data from the initial workflow trigger and other steps are available in the `steps` argument passed to the `run({ steps, $ })` function. - -In this example, we'll pretend this data is coming into our HTTP trigger via POST request. - -```json -{ - "id": 1, - "name": "Bulbasaur", - "type": "plant" -} -``` - -In our Node.js step, we can access this data in the `steps` variable Specifically, this data from the POST request into our workflow is available in the `trigger` property. - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - const pokemonName = steps.trigger.event.name; - const pokemonType = steps.trigger.event.type; - - console.log(`${pokemonName} is a ${pokemonType} type Pokemon`); - }, -}); -``` - -### Sending data downstream to other steps - -To share data created, retrieved, transformed or manipulated by a step to others downstream you can simply `return` it. - -```javascript -// This step is named "code" in the workflow -import axios from "axios"; - -export default defineComponent({ - async run({ steps, $ }) { - const response = await axios.get( - "https://pokeapi.co/api/v2/pokemon/charizard" - ); - // Store the response's JSON contents into a variable called "pokemon" - const pokemon = response.data; - - // Expose the pokemon data downstream to other steps in the $return_value from this step - return pokemon; - }, -}); -``` - -### Using $.export - - - -Alternatively, use the built in `$.export` helper instead of returning data. The `$.export` creates a _named_ export with the given value. - -```javascript -// This step is named "code" in the workflow -import axios from "axios"; - -export default defineComponent({ - async run({ steps, $ }) { - const response = await axios.get( - "https://pokeapi.co/api/v2/pokemon/charizard" - ); - // Store the response's JSON contents into a variable called "pokemon" - const pokemon = response.data; - - // Expose the pokemon data downstream to other steps in the pokemon export from this step - $.export("pokemon", pokemon); - }, -}); -``` - -Now this `pokemon` data is accessible to downstream steps within `steps.code.pokemon` - - -You can only export JSON-serializable data from steps. Things like: - -- strings -- numbers -- objects - -You cannot export functions or other complex objects that don't serialize to JSON. [You can save that data to a file in the `/tmp` directory](/code/nodejs/working-with-files/). - - -## Passing props to code steps - - - -You can make code steps reusable by allowing them to accept props. Instead of hard-coding the values of variables within the code itself, you can pass them to the code step as arguments or parameters _entered in the workflow builder_. - -For example, let's define a `firstName` prop. This will allow us to freely enter text from the workflow builder. - -```javascript -export default defineComponent({ - props: { - firstName: { - type: "string", - label: "Your first name", - default: "Dylan", - }, - }, - async run({ steps, $ }) { - console.log( - `Hello ${this.firstName}, congrats on crafting your first prop!` - ); - }, -}); -``` - -The workflow builder now can accept text input to populate the `firstName` to this particular step only: - -![Workflow builder displaying the input visually as a text input field](/images/code/nodejs/user-input-props-example.png) - -Accepting a single string is just one example, you can build a step to accept arrays of strings through a dropdown presented in the workflow builder. - -[Read the props reference for the full list of options](/components/api/#props). - -## How Pipedream Node.js components work - - - -When you add a new Node.js code step or use the examples in this doc, you'll notice a common structure to the code: - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - // this Node.js code will execute when the step runs - }, -}); -``` - -This defines [a Node.js component](/components/api/). Components let you: - -- Pass input to steps using [props](/code/nodejs/#passing-props-to-code-steps) -- [Connect an account to a step](/connected-accounts/#from-a-code-step) -- [Issue HTTP responses](/workflows/steps/triggers/#http-responses) -- Perform workflow-level flow control, like [ending a workflow early](#ending-a-workflow-early) - -When the step runs, Pipedream executes the `run` method: - -- Any asynchronous code within a code step [**must** be run synchronously](/code/nodejs/async/), using the `await` keyword or with a Promise chain, using `.then()`, `.catch()`, and related methods. -- Pipedream passes the `steps` variable to the run method. `steps` is also an object, and contains the [data exported from previous steps](/workflows/steps/#step-exports) in your workflow. -- You also have access to the `$` variable, which gives you access to methods like `$.respond`, `$.export`, [and more](/components/api/#actions). - -If you're using [props](/code/nodejs/#passing-props-to-code-steps) or [connect an account to a step](/connected-accounts/#from-a-code-step), the component exposes them in the variable `this`, which refers to the current step: - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - // `this` refers to the running component. Props, connected accounts, etc. are exposed here - console.log(this); - }, -}); -``` - -When you [connect an account to a step](/connected-accounts/#from-a-code-step), Pipedream exposes the auth info in the variable [`this.appName.$auth`](/code/nodejs/auth/#accessing-connected-account-data-with-this-appname-auth). - -## Logs - -You can call `console.log` or `console.error` to add logs to the execution of a code step. - -These logs will appear just below the associated step. `console.log` messages appear in black, `console.error` in red. - -### `console.dir` - -If you need to print the contents of JavaScript objects, use `console.dir`: - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - console.dir({ - name: "Luke", - }); - }, -}); -``` - -## Syntax errors - -Pipedream will attempt to catch syntax errors when you're writing code, highlighting the lines where the error occurred in red. - - -While you can save a workflow with syntax errors, it's unlikely to run correctly on new events. Make sure to fix syntax errors before running your workflow. - - -## Using `npm` packages - - - -[npm](https://www.npmjs.com/) hosts JavaScript packages: libraries of code someone else wrote and packaged for others to use. npm has over 400,000 packages and counting. - -### Just `import` it - -To use an npm package on Pipedream, simply `import` it: - -```javascript -import axios from "axios"; -``` - -By default, workflows don't have any packages installed. Just import any package in this manner to make it available in the step. - -If a package only supports the [CommonJS module format](https://nodejs.org/api/modules.html), you may have to `require` it: - -```javascript -const axios = require("axios"); -``` - -**Within a single step, you can only use `import` or `require` statements, not both**. See [this section](#require-is-not-defined) for more details. - -When Pipedream runs your workflow, we download the associated npm package for you before running your code steps. - -If you've used Node before, you'll notice there's no `package.json` file to upload or edit. We want to make package management simple, so just `import` or `require` the module like you would in your code, after package installation, and get to work. - -### Third-party package limitations - -Some packages require access to a web browser to run, and don't work with Node.js. Often this limitation is documented on the package `README`, but often it's not. If you're not sure and need to use it, we recommend just trying to `import` or `require` it. - -Other packages require access to binaries or system libraries that aren't installed in the Pipedream execution environment. - -If you're seeing any issues with a specific package, please [let us know](https://pipedream.com/support/) and we'll try to help you make it work. - -### Pinning package versions - -Each time you deploy a workflow with Node.js code, Pipedream downloads the npm packages you `import` in your step. **By default, Pipedream deploys the latest version of the npm package each time you deploy a change**. - -There are many cases where you may want to specify the version of the packages you're using. If you'd like to use a _specific_ version of a package in a workflow, you can add that version in the `import` string, for example: - -```javascript -import axios from "axios@0.19.2"; -``` - -You can also pass the version specifiers used by npm to support [semantic version](https://semver.org/) upgrades. For example, to allow for future patch version upgrades: - -```javascript -import axios from "axios@~0.20.0"; -``` - -To allow for patch and minor version upgrades, use: - -```javascript -import got from "got@^11.0.0"; -``` - - -The behavior of the caret (`^`) operator is different for 0.x versions, for which it will only match patch versions, and not minor versions. - - -You can also specify different versions of the same package in different steps. Each step will used the associated version. Note that this also increases the size of your deployment, which can affect cold start times. - -### CommonJS vs. ESM imports - -In Node.js, you may be used to importing third-party packages using the `require` statement: - -```javascript -const axios = require("axios"); -``` - -In this example, we're including the `axios` [CommonJS module](https://nodejs.org/api/modules.html) published to npm. You import CommonJS modules using the `require` statement. - -But you may encounter this error in workflows: - -`Error Must use import to load ES Module` - -This means that the package you're trying to `require` uses a different format to export their code, called [ECMAScript modules](https://nodejs.org/api/esm.html#esm_modules_ecmascript_modules) (**ESM**, or **ES modules**, for short). With ES modules, you instead need to `import` the package: - -```javascript -import got from "got"; -``` - -Most package publish both CommonJS and ESM versions, so **if you always use `import`, you're less likely to have problems**. In general, refer to the documentation for your package for instructions on importing it correctly. - -### `require` is not defined - -This error means that you cannot use CommonJS and ESM imports in the same step. For example, if you run code like this: - -```javascript -import fetch from "node-fetch"; -const axios = require("axios"); -``` - -your workflow will throw a `require is not defined` error. There are two solutions: - -1. Try converting your CommonJS `require` statement into an ESM `import` statement. For example, convert this: - -```javascript -const axios = require("axios"); -``` - -to this: - -```javascript -import axios from "axios"; -``` - -2. If the `import` syntax fails to work, separate your imports into different steps, using only CommonJS requires in one step, and only ESM imports in another. - -## Variable scope - -Any variables you create within a step are scoped to that step. That is, they cannot be referenced in any other step. - -Within a step, the [normal rules of JavaScript variable scope](https://developer.mozilla.org/en-US/docs/Glossary/Scope) apply. - -**When you need to share data across steps, use [step exports](/workflows/steps/).** - -## Making HTTP requests from your workflow - -There are two ways to make HTTP requests in code steps: - -- Use any HTTP client that works with Node.js. [See this example guide for how to use `axios` to make HTTP requests](/code/nodejs/http-requests/). -- [Use `$.send.http()`](/destinations/http/#using-send-http-in-workflows), a Pipedream-provided method for making asynchronous HTTP requests. - -In general, if you just need to make an HTTP request but don't care about the response, [use `$.send.http()`](/destinations/http/#using-send-http-in-workflows). If you need to operate on the data in the HTTP response in the rest of your workflow, [use `axios`](/code/nodejs/http-requests/). - -## Returning HTTP responses - -You can return HTTP responses from [HTTP-triggered workflows](/workflows/steps/triggers/#http) using the [`$.respond()` function](/workflows/steps/triggers/#http-responses). - -## Ending a workflow early - - - -Sometimes you want to end your workflow early, or otherwise stop or cancel the execution or a workflow under certain conditions. For example: - -- You may want to end your workflow early if you don't receive all the fields you expect in the event data. -- You only want to run your workflow for 5% of all events sent from your source. -- You only want to run your workflow for users in the United States. If you receive a request from outside the U.S., you don't want the rest of the code in your workflow to run. -- You may use the `user_id` contained in the event to look up information in an external API. If you can't find data in the API tied to that user, you don't want to proceed. - -**In any code step, calling `return $.flow.exit()` will end the execution of the workflow immediately.** No remaining code in that step, and no code or destination steps below, will run for the current event. - - -It's a good practice to use `return $.flow.exit()` to immediately exit the workflow. -In contrast, `$.flow.exit()` on its own will end the workflow only after executing all remaining code in the step. - - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - return $.flow.exit(); - console.log( - "This code will not run, since $.flow.exit() was called above it" - ); - }, -}); -``` - -You can pass any string as an argument to `$.flow.exit()`: - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - return $.flow.exit("End message"); - }, -}); -``` - -Or exit the workflow early within a conditional: - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - // Flip a coin, running $.flow.exit() for 50% of events - if (Math.random() > 0.5) { - return $.flow.exit(); - } - console.log("This code will only run 50% of the time"); - }, -}); -``` - -## Errors - -[Errors](https://nodejs.org/dist/latest-v10.x/docs/api/errors.html#errors_errors) raised in a code step will stop the execution of code or destinations that follow. - -### Configuration Error - -Throwing a `ConfigurationError` in a Node.js step will display the error message in a dedicated area. - -This is useful for providing feedback during validation of `props`. In the example below, a required Header value is missing from the Google Sheets action: - -![Example of an ConfigurationError](https://res.cloudinary.com/pipedreamin/image/upload/v1651680315/docs/components/CleanShot_2022-05-04_at_12.04.38_2x_vf8jny.png) - -Or you can use it for validating the format of a given `email` prop: - -```javascript -import { ConfigurationError } from "@pipedream/platform"; - -export default defineComponent({ - props: { - email: { type: "string" }, - }, - async run({ steps, $ }) { - // if the email address doesn't include a @, it's not valid - if (!this.email.includes("@")) { - throw new ConfigurationError("Provide a valid email address"); - } - }, -}); -``` - -## Using secrets in code - -Workflow code is private. Still, we recommend you don't include secrets — API keys, tokens, or other sensitive values — directly in code steps. - -Pipedream supports [environment variables](/environment-variables/) for keeping secrets separate from code. Once you create an environment variable in Pipedream, you can reference it in any workflow using `process.env.VARIABLE_NAME`. The values of environment variables are private. - -See the [Environment Variables](/environment-variables/) docs for more information. - -## Limitations of code steps - -Code steps operate within the [general constraints on workflows](/limits/#workflows). As long as you stay within those limits and abide by our [acceptable use policy](/limits/#acceptable-use), you can add any number of code steps in a workflow to do virtually anything you'd be able to do in Node.js. - -If you're trying to run code that doesn't work or you have questions about any limits on code steps, [please reach out](https://pipedream.com/support/). - -## Editor settings - -We use the [Monaco Editor](https://microsoft.github.io/monaco-editor/), which powers VS Code and other web-based editors. - -We also let you customize the editor. For example, you can enable Vim mode, and change the default tab size for indented code. Visit your [Settings](https://pipedream.com/settings) to modify these settings. - -## Keyboard Shortcuts - -We use the [Monaco Editor](https://microsoft.github.io/monaco-editor/), which powers VS Code. As a result, many of the VS Code [keyboard shortcuts](https://code.visualstudio.com/docs/getstarted/keybindings) should work in the context of the editor. - -For example, you can use shortcuts to search for text, format code, and more. - -## New to JavaScript? - -We understand many of you might be new to JavaScript, and provide resources for you to learn the language below. - -When you're searching for how to do something in JavaScript, some of the code you try might not work in Pipedream. This could be because the code expects to run in a browser, not a Node.js environment. The same goes for [npm packages](#using-npm-packages). - -### I'm new to programming - -Many of the most basic JavaScript tutorials are geared towards writing code for a web browser to run. This is great for learning — a webpage is one of the coolest things you can build with code. We recommend starting with these general JavaScript tutorials and trying the code you learn on Pipedream: - -- [JavaScript For Cats](http://jsforcats.com/) -- [Mozilla - JavaScript First Steps](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/First_steps) -- [StackOverflow](https://stackoverflow.com/) operates a programming Q&A site that typically has the first Google result when you're searching for something specific. It's a great place to find answers to common questions. - -### I know how to code, but don't know JavaScript - -- [A re-introduction to JavaScript (JS tutorial)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/A_re-introduction_to_JavaScript) -- [MDN language overview](https://developer.mozilla.org/en-US/docs/Web/JavaScript) -- [Eloquent Javascript](https://eloquentjavascript.net/) -- [Node School](https://nodeschool.io/) -- [You Don't Know JS](https://github.com/getify/You-Dont-Know-JS) diff --git a/docs-v2/pages/code/nodejs/rerun.mdx b/docs-v2/pages/code/nodejs/rerun.mdx deleted file mode 100644 index fe94e1b7646cc..0000000000000 --- a/docs-v2/pages/code/nodejs/rerun.mdx +++ /dev/null @@ -1,266 +0,0 @@ -import Callout from "@/components/Callout"; -import VideoPlayer from "@/components/VideoPlayer"; - -# Pause, resume, and rerun a workflow - -You can use `$.flow.suspend` and `$.flow.rerun` to pause a workflow and resume it later. - -This is useful when you want to: - -- Pause a workflow until someone manually approves it -- Poll an external API until some job completes, and proceed with the workflow when it's done -- Trigger an external API to start a job, pause the workflow, and resume it when the external API sends an HTTP callback - -We'll cover all of these examples below. - -## `$.flow.suspend` - -Use `$.flow.suspend` when you want to pause a workflow and proceed with the remaining steps only when manually approved or cancelled. - -For example, you can suspend a workflow and send yourself a link to manually resume or cancel the rest of the workflow: - -```javascript -export default defineComponent({ - async run({ $ }) { - const { resume_url, cancel_url } = $.flow.suspend(); - $.send.email({ - subject: "Please approve this important workflow", - text: `Click here to approve the workflow: ${resume_url}, and cancel here: ${cancel_url}`, - }); - // Pipedream suspends your workflow at the end of the step - }, -}); -``` - -You'll receive an email like this: - -
-approve this workflow -
- -And can resume or cancel the rest of the workflow by clicking on the appropriate link. - -### `resume_url` and `cancel_url` - -In general, calling `$.flow.suspend` returns a `cancel_url` and `resume_url` that lets you cancel or resume paused executions. Since Pipedream pauses your workflow at the _end_ of the step, you can pass these URLs to any external service before the workflow pauses. If that service accepts a callback URL, it can trigger the `resume_url` when its work is complete. - -These URLs are specific to a single execution of your workflow. While the workflow is paused, you can load these in your browser or send any HTTP request to them: - -- Sending an HTTP request to the `cancel_url` will cancel that execution -- Sending an HTTP request to the `resume_url` will resume that execution - -If you resume a workflow, any data sent in the HTTP request is passed to the workflow and returned in the `$resume_data` [step export](/workflows/steps/#step-exports) of the suspended step. For example, if you call `$.flow.suspend` within a step named `code`, the `$resume_data` export should contain the data sent in the `resume_url` request: - -
-resume data step export -
- -Requests to the `resume_url` have [the same limits as any HTTP request to Pipedream](/limits/#http-request-body-size), but you can send larger payloads using our [large payload](/workflows/steps/triggers/#sending-large-payloads) or [large file](/workflows/steps/triggers/#large-file-support) interfaces. - -### Default timeout of 24 hours - -By default, `$.flow.suspend` will automatically resume the workflow after 24 hours. You can set your own timeout (in milliseconds) as the first argument: - -```javascript -export default defineComponent({ - async run({ $ }) { - // 7 days - const TIMEOUT = 1000 * 60 * 60 * 24 * 7; - $.flow.suspend(TIMEOUT); - }, -}); -``` - -## `$.flow.rerun` - - - -Use `$.flow.rerun` when you want to run a specific step of a workflow multiple times. This is useful when you need to start a job in an external API and poll for its completion, or have the service call back to the step and let you process the HTTP request within the step. - -### Retrying a Failed API Request - -`$.flow.rerun` can be used to conditionally retry a failed API request due to a service outage or rate limit reached. Place the `$.flow.rerun` call within a `catch` block to only retry the API request if an error is thrown: - -```javascript -import { axios } from "@pipedream/platform"; - -export default defineComponent({ - props: { - openai: { - type: "app", - app: "openai", - }, - }, - async run({ steps, $ }) { - try { - return await axios($, { - url: `https://api.openai.com/v1/completions`, - method: "post", - headers: { - Authorization: `Bearer ${this.openai.$auth.api_key}`, - }, - data: { - model: "text-davinci-003", - prompt: "Say this is a test", - max_tokens: 7, - temperature: 0, - }, - }); - } catch (error) { - const MAX_RETRIES = 3; - const DELAY = 1000 * 30; - - // Retry the request every 30 seconds, for up to 3 times - $.flow.rerun(DELAY, null, MAX_RETRIES); - } - }, -}); -``` - -### Polling for the status of an external job - -Sometimes you need to poll for the status of an external job until it completes. `$.flow.rerun` lets you rerun a specific step multiple times: - -```javascript -import axios from "axios"; - -export default defineComponent({ - async run({ $ }) { - const MAX_RETRIES = 3; - // 10 seconds - const DELAY = 1000 * 10; - const { run } = $.context; - // $.context.run.runs starts at 1 and increments when the step is rerun - if (run.runs === 1) { - // $.flow.rerun(delay, context (discussed below), max retries) - $.flow.rerun(DELAY, null, MAX_RETRIES); - } else if (run.runs === MAX_RETRIES + 1) { - throw new Error("Max retries exceeded"); - } else { - // Poll external API for status - const { data } = await axios({ - method: "GET", - url: "https://example.com/status", - }); - // If we're done, continue with the rest of the workflow - if (data.status === "DONE") return data; - - // Else retry later - $.flow.rerun(DELAY, null, MAX_RETRIES); - } - }, -}); -``` - -`$.flow.rerun` accepts the following arguments: - -```javascript -$.flow.rerun( - delay, // The number of milliseconds until the step will be rerun - context, // JSON-serializable data you need to pass between runs - maxRetries // The total number of times the step will rerun. Defaults to 10 -); -``` - -### Accept an HTTP callback from an external service - -When you trigger a job in an external service, and that service can send back data in an HTTP callback to Pipedream, you can process that data within the same step using `$.flow.rerun`: - -```javascript -import axios from "axios"; - -export default defineComponent({ - async run({ steps, $ }) { - const TIMEOUT = 86400 * 1000; - const { run } = $.context; - // $.context.run.runs starts at 1 and increments when the step is rerun - if (run.runs === 1) { - const { cancel_url, resume_url } = $.flow.rerun(TIMEOUT, null, 1); - - // Send resume_url to external service - await axios({ - method: "POST", - url: "your callback URL", - data: { - resume_url, - cancel_url, - }, - }); - } - - // When the external service calls back into the resume_url, you have access to - // the callback data within $.context.run.callback_request - else if (run.callback_request) { - return run.callback_request; - } - }, -}); -``` - -### Passing `context` to `$.flow.rerun` - -Within a Node.js code step, `$.context.run.context` contains the `context` passed from the prior call to `rerun`. This lets you pass data from one run to another. For example, if you call: - -```javascript -$.flow.rerun(1000, { hello: "world" }); -``` - -`$.context.run.context` will contain: - -
-resume data step export -
- -### `maxRetries` - -By default, `maxRetries` is **10**. - -When you exceed `maxRetries`, the workflow proceeds to the next step. If you need to handle this case with an exception, `throw` an error from the step: - -```javascript -export default defineComponent({ - async run({ $ }) { - const MAX_RETRIES = 3; - const { run } = $.context; - if (run.runs === 1) { - $.flow.rerun(1000, null, MAX_RETRIES); - } else if (run.runs === MAX_RETRIES + 1) { - throw new Error("Max retries exceeded"); - } - }, -}); -``` - -## Behavior when testing - -When you're building a workflow and test a step with `$.flow.suspend` or `$.flow.rerun`, it will not suspend the workflow, and you'll see a message like the following: - -> Workflow execution canceled — this may be due to `$.flow.suspend()` usage (not supported in test) - -These functions will only suspend and resume when run in production. - -## Credits when using `suspend` / `rerun` - -You are not charged for the time your workflow is suspended during a `$.flow.rerun` or `$.flow.suspend`. Only when workflows are resumed will compute time count toward [credit usage](/pricing/#credits). - - -When a suspended workflow reawakens, it will reset the credit counter. - -Each rerun or reawakening from a suspension will count as a new fresh credit. - - diff --git a/docs-v2/pages/code/nodejs/using-data-stores.mdx b/docs-v2/pages/code/nodejs/using-data-stores.mdx deleted file mode 100644 index bf755ddcc678b..0000000000000 --- a/docs-v2/pages/code/nodejs/using-data-stores.mdx +++ /dev/null @@ -1,247 +0,0 @@ -import Callout from '@/components/Callout' - -# Using Data Stores - -In Node.js (Javascript) code steps, you can also store and retrieve data within code steps without connecting a 3rd party database. - -Add data stores to steps as props. By adding the store as a prop, it's available under `this`. - -For example, you can define a data store as a data prop, and reference it at `this.data`: - -```javascript -export default defineComponent({ - props: { - // Define that the "data" variable in our component is a data store - data: { type: "data_store" }, - }, - async run({ steps, $ }) { - // Now we can access the data store at "this.data" - await this.data.get("email"); - }, -}); -``` - - -**`props` injects variables into `this`**. See how we declared the `data` prop in the `props` object, and then accessed it at `this.data` in the `run` method. - - - -All data store operations are asynchronous, so must be `await`ed. - - -## Using the data store - -Once you've defined a data store prop for your component, then you'll be able to create a new data store or use an existing one from your account. - -![Create a new data store or choose another one from your account for your component](https://res.cloudinary.com/pipedreamin/image/upload/v1649270361/docs/components/data_store_scaffolding_bluivn.png) - -## Saving data - -Data Stores are key-value stores. Save data within a Data Store using the `this.data.set` method. The first argument is the _key_ where the data should be held, and the second argument is the _value_ assigned to that key. - -```javascript -export default defineComponent({ - props: { - data: { type: "data_store" }, - }, - async run({ steps, $ }) { - // Store a timestamp each time this step is executed in the workflow - await this.data.set("lastRanAt", new Date()); - }, -}); -``` - -## Retrieving keys - -Fetch all the keys in a given Data Store using the `keys` method: - -```javascript -export default defineComponent({ - props: { - data: { type: "data_store" }, - }, - async run({ steps, $ }) { - // Return a list of all the keys in a given Data Store - return await this.data.keys(); - }, -}); -``` - -## Checking for the existence of specific keys - -If you need to check whether a specific `key` exists in a Data Store, you can pass the `key` to the `has` method to get back a `true` or `false`: - -```javascript -export default defineComponent({ - props: { - data: { type: "data_store" }, - }, - async run({ steps, $ }) { - // Check if a specific key exists in your Data Store - return await this.data.has("lastRanAt"); - }, -}); -``` - -## Retrieving data - -You can retrieve data with the Data Store using the `get` method. Pass the _key_ to the `get` method to retrieve the content that was stored there with `set`. - -```javascript -export default defineComponent({ - props: { - data: { type: "data_store" }, - }, - async run({ steps, $ }) { - // Check if the lastRanAt key exists - const lastRanAt = await this.data.get("lastRanAt"); - }, -}); -``` - -## Deleting or updating values within a record - -To delete or update the _value_ of an individual record, use the `set` method for an existing `key` and pass either the new value or `''` as the second argument to remove the value but retain the key. - -```javascript -export default defineComponent({ - props: { - data: { type: "data_store" }, - }, - async run({ steps, $ }) { - // Update the value associated with the key, myKey - await this.data.set("myKey", "newValue"); - - // Remove the value but retain the key - await this.data.set("myKey", ""); - }, -}); -``` - -## Deleting specific records - -To delete individual records in a Data Store, use the `delete` method for a specific `key`: - -```javascript -export default defineComponent({ - props: { - data: { type: "data_store" }, - }, - async run({ steps, $ }) { - // Delete the lastRanAt record - const lastRanAt = await this.data.delete("lastRanAt"); - }, -}); -``` - -## Deleting all records from a specific Data Store - -If you need to delete all records in a given Data Store, you can use the `clear` method. **Note that this is an irreversible change, even when testing code in the workflow builder.** - -```javascript -export default defineComponent({ - props: { - data: { type: "data_store" }, - }, - async run({ steps, $ }) { - // Delete all records from a specific Data Store - return await this.data.clear(); - }, -}); -``` - -## Viewing store data - -You can view the contents of your data stores in your [Pipedream dashboard](https://pipedream.com/stores). - -From here you can also manually edit your data store's data, rename stores, delete stores or create new stores. - -## Using multiple data stores in a single code step - -It is possible to use multiple data stores in a single code step, just make a unique name per store in the `props` definition. Let's define 2 separate `customers` and `orders` data sources and leverage them in a single code step: - -```javascript -export default defineComponent({ - props: { - customers: { type: "data_store" }, - orders: { type: "data_store" }, - }, - async run({ steps, $ }) { - // Retrieve the customer from our customer store - const customer = await this.customer.get(steps.trigger.event.customer_id); - // Retrieve the order from our order data store - const order = await this.orders.get(steps.trigger.event.order_id); - }, -}); -``` - -## Workflow counter example - -You can use a data store as a counter. For example, this code counts the number of times the workflow runs: - -```javascript -export default defineComponent({ - props: { - data: { type: "data_store" }, - }, - async run({ steps, $ }) { - // By default, all database entries are undefined. - // It's wise to set a default value so our code as an initial value to work with - const counter = (await this.data.get("counter")) ?? 0; - - // On the first run "counter" will be 0 and we'll increment it to 1 - // The next run will increment the counter to 2, and so forth - await this.data.set("counter", counter + 1); - }, -}); -``` - -## Dedupe data example - -Data Stores are also useful for storing data from prior runs to prevent acting on duplicate data, or data that's been seen before. - -For example, this workflow's trigger contains an email address from a potential new customer. But we want to track all emails collected so we don't send a welcome email twice: - -```javascript -export default defineComponent({ - props: { - data: { type: "data_store" }, - }, - async run({ steps, $ }) { - const email = steps.trigger.event.body.new_customer_email; - // Retrieve the past recorded emails from other runs - const emails = (await this.data.get("emails")) ?? []; - - // If the current email being passed from our webhook is already in our list, exit early - if (emails.includes(email)) { - return $.flow.exit("Already welcomed this user"); - } - - // Add the current email to the list of past emails so we can detect it in the future runs - await this.data.set("emails", [...emails, email]); - }, -}); -``` - -## Data store limitations - -Data Stores are only currently available in Node.js and Python steps. They are not yet available [Bash](/code/bash/) or [Go](/code/go/). - -### Supported data types - -Data stores can hold any JSON-serializable data within the storage limits. This includes data types including: - -- Strings -- Objects -- Arrays -- Dates -- Integers -- Floats - -But you cannot serialize Functions, Classes, or other more complex objects. - -### Querying records - -You can retrieve up to {process.env.DATA_STORES_MAX_KEYS} records within a single query. - -The `this.data.entries()` and `this.data.keys()` functions allow you to retrieve all keys and records from your data store. However, using these methods with a data store with over {process.env.DATA_STORES_MAX_KEYS} keys will result in a 426 error. diff --git a/docs-v2/pages/code/nodejs/working-with-files.mdx b/docs-v2/pages/code/nodejs/working-with-files.mdx deleted file mode 100644 index dbcf6d9d1ca8d..0000000000000 --- a/docs-v2/pages/code/nodejs/working-with-files.mdx +++ /dev/null @@ -1,101 +0,0 @@ ---- -short_description: Store and read files with Node.js in workflows. -thumbnail: https://res.cloudinary.com/pipedreamin/image/upload/v1646763737/docs/icons/icons8-opened-folder_y60u9l.svg ---- - -# Working with the filesystem in Node.js - -You'll commonly need to work with files in a workflow, for example: downloading content from some service to upload to another. This doc explains how to work with files in Pipedream workflows and provides some sample code for common operations. - - - -## The `/tmp` directory - -Within a workflow, you have full read-write access to the `/tmp` directory. You have {process.env.TMP_SIZE_LIMIT} of available space in `/tmp` to save any file. - -### Managing `/tmp` across workflow runs - -The `/tmp` directory is stored on the virtual machine that runs your workflow. We call this the execution environment ("EE"). More than one EE may be created to handle high-volume workflows. And EEs can be destroyed at any time (for example, after about 10 minutes of receiving no events). This means that you should not expect to have access to files across executions. At the same time, files _may_ remain, so you should clean them up to make sure that doesn't affect your workflow. **Use [the `tmp-promise` package](https://github.com/benjamingr/tmp-promise) to cleanup files after use, or [delete the files manually](#delete-a-file).** - -### Reading a file from `/tmp` - -This example uses [step exports](/workflows/steps/#step-exports) to return the contents of a test file saved in `/tmp` as a string: - -```javascript -import fs from "fs"; - -export default defineComponent({ - async run({ steps, $ }) { - return (await fs.promises.readFile('/tmp/your-file')).toString() - } -}); -``` - -### Writing a file to `/tmp` - -Use the [`fs` module](https://nodejs.org/api/fs.html) to write data to `/tmp`: - -```javascript -import fs from "fs" -import { file } from 'tmp-promise' - -export default defineComponent({ - async run({ steps, $ }) { - const { path, cleanup } = await file(); - await fs.promises.appendFile(path, Buffer.from("hello, world")) - await cleanup(); - } -}); -``` - -### Listing files in `/tmp` - -Return a list of the files saved in `/tmp`: - -```javascript -import fs from "fs"; - -export default defineComponent({ - async run({ steps, $ }) { - return fs.readdirSync("/tmp"); - } -}); -``` - -### Delete a file - -```javascript -import fs from "fs"; - -export default defineComponent({ - async run({ steps, $ }) { - return await fs.promises.unlink('/tmp/your-file'); - } -}); -``` - -### Download a file to `/tmp` - -[See this example](/code/nodejs/http-requests/#download-a-file-to-the-tmp-directory) to learn how to download a file to `/tmp`. - -### Upload a file from `/tmp` - -[See this example](/code/nodejs/http-requests/#upload-a-file-from-the-tmp-directory) to learn how to upload a file from `/tmp` in an HTTP request. - -### Download a file, uploading it in another `multipart/form-data` request - -[This workflow](https://pipedream.com/@dylburger/download-file-then-upload-file-via-multipart-form-data-request-p_QPCx7p/edit) provides an example of how to download a file at a specified **Download URL**, uploading that file to an **Upload URL** as form data. - -### Download email attachments to `/tmp`, upload to Amazon S3 - -[This workflow](https://pipedream.com/@dylan/upload-email-attachments-to-s3-p_V9CGAQ/edit) is triggered by incoming emails. When copied, you'll get a workflow-specific email address you can send any email to. This workflow takes any attachments included with inbound emails, saves them to `/tmp`, and uploads them to Amazon S3. - -You should also be aware of the [inbound payload limits](/limits/#email-triggers) associated with the email trigger. - -### Downloading and uploading files from File Stores - -Within Node.js code steps, you can download files from a File Store to the `/tmp` directory and vice versa. - -The `$.files` helper includes methods to upload and download files from the Project's File Store. - -[Read the File Stores `$.files` helper documentation.](/projects/file-stores/#managing-file-stores-from-workflows) diff --git a/docs-v2/pages/code/python/_meta.json b/docs-v2/pages/code/python/_meta.json deleted file mode 100644 index bd55dc2ca3c74..0000000000000 --- a/docs-v2/pages/code/python/_meta.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "index": "Running Python in workflows", - "auth": "Connect apps", - "http-requests": "HTTP requests", - "working-with-files": "Files", - "using-data-stores": "Data stores", - "delay": "Delaying steps", - "rerun": "Pause, resume, and rerun steps", - "import-mappings": "Different PyPI package name and import name", - "faqs": "Python FAQs" -} diff --git a/docs-v2/pages/code/python/auth.mdx b/docs-v2/pages/code/python/auth.mdx deleted file mode 100644 index 73237d6e7623a..0000000000000 --- a/docs-v2/pages/code/python/auth.mdx +++ /dev/null @@ -1,92 +0,0 @@ -# Connecting apps in Python - -When you use [prebuilt actions](/components#actions) tied to apps, you don't need to write the code to authorize API requests. Just [connect your account](/connected-accounts/#connecting-accounts) for that app and run your workflow. - -But sometimes you'll need to [write your own code](/code/python/). You can also connect apps to custom code steps, using the auth information to authorize requests to that app. - -For example, you may want to send a Slack message from a step. We use Slack's OAuth integration to authorize sending messages from your workflows. - -Add Slack as an app on the Python step, then connect your Slack account. - -![Add your Slack account to a Python code step by adding it](https://res.cloudinary.com/pipedreamin/image/upload/v1658954165/docs/components/CleanShot_2022-07-27_at_16.35.37_ytofp2.gif) - -Then within the Python code step, `pd.inputs["slack"]["$auth"]["oauth_access_token"]` will contain your Slack account OAuth token. - -With that token, you can make authenticated API calls to Slack: - -```python -from slack_sdk import WebClient - -def handler(pd: "pipedream"): - # Your Slack OAuth token is available under pd.inputs - token = pd.inputs["slack"]["$auth"]["oauth_access_token"] - - # Instantiate a new Slack client with your token - client = WebClient(token=token) - - # Use the client to send messages to Slack channels - response = client.chat_postMessage( - channel='#general', - text='Hello from Pipedream!' - ) - - # Export the Slack response payload for use in future steps - pd.export("response", response.data) -``` - - - -## Accessing connected account data with `pd.inputs[appName]["$auth"]` - -In our Slack example above, we created a Slack `WebClient` using the Slack OAuth access token: - -```python -# Instantiate a new Slack client with your token -client = WebClient(token=token) -``` - -Where did `pd.inputs["slack"]` come from? Good question. It was generated when we connected Slack to our Python step. - -![The Slack app generates the pd.inputs["slack"] data](https://res.cloudinary.com/pipedreamin/image/upload/v1658954351/docs/components/CleanShot_2022-07-27_at_16.38.42_f08ocy.png) - -The Slack access token is generated by Pipedream, and is available to this step in the `pd.inputs[appName]["$auth"]` object: - -```python -from slack_sdk import WebClient - -def handler(pd: "pipedream"): - token = pd.inputs["slack"]["$auth"]["oauth_access_token"] - # Authentication details for all of your apps are accessible under the special pd.inputs["slack"] variable: - console.log(pd.inputs["slack"]["$auth"]) -``` - -`pd.inputs["slack"]["$auth"]` contains named properties for each account you connect to the associated step. Here, we connected Slack, so `this.slack.$auth` contains the Slack auth info (the `oauth_access_token`). - -The names of the properties for each connected account will differ with the account. Pipedream typically exposes OAuth access tokens as `oauth_access_token`, and API keys under the property `api_key`. But if there's a service-specific name for the tokens (for example, if the service calls it `server_token`), we prefer that name, instead. - -To list the `pd.inputs["slack"]["$auth"]` properties available to you for a given app, just print the contents of the `$auth` property: - -```python -print(pd.inputs["slack"]["$auth"]) # Replace "slack" with your app's name -``` - -and run your workflow. You'll see the property names in the logs below your step. - -### Using the code templates tied to apps - -When you write custom code that connects to an app, you can start with a code snippet Pipedream provides for each app. This is called the **test request**. - -When you search for an app in a step: - -1. Click the **+** button below any step. -2. Search for the app you're looking for and select it from the list. -3. Select the option to **Run Python with any [app] API**. - -![Create Python API scaffolding for any app](https://res.cloudinary.com/pipedreamin/image/upload/v1658954462/docs/components/CleanShot_2022-07-27_at_16.40.41_isy1e6.png) - -This code operates as a template you can extend, and comes preconfigured with the connection to the target app and the code for authorizing requests to the API. You can modify this code however you'd like. - -## Custom auth tokens / secrets - -When you want to connect to a 3rd party service that isn't supported by Pipedream, you can store those secrets in [Environment Variables](/environment-variables/). - diff --git a/docs-v2/pages/code/python/delay.mdx b/docs-v2/pages/code/python/delay.mdx deleted file mode 100644 index c40b73623de29..0000000000000 --- a/docs-v2/pages/code/python/delay.mdx +++ /dev/null @@ -1,129 +0,0 @@ -import Callout from '@/components/Callout' - -# Delaying a workflow - -Use `pd.flow.delay` to [delay a step in a workflow](/workflows/flow-control/#delay). - -These docs show you how to write Python code to handle delays. If you don't need to write code, see [our built-in delay actions](/workflows/flow-control/#delay-actions). - -## Using `pd.flow.delay` - -`pd.flow.delay` takes one argument: the number of **milliseconds** you'd like to pause your workflow until the next step executes. {process.env.DELAY_MIN_MAX_TIME}. - -Note that [delays happen at the end of the step where they're called](#when-delays-happen). - -```python -import random - -def handler(pd: 'pipedream'): - # Delay a workflow for 60 seconds (60,000 ms) - pd.flow.delay(60 * 1000) - - # Delay a workflow for 15 minutes - pd.flow.delay(15 * 60 * 1000) - - # Delay a workflow based on the value of incoming event data, - # or default to 60 seconds if that variable is undefined - default = 60 * 1000 - delayMs = pd.steps["trigger"].get("event", {}).get("body", {}).get("delayMs", default) - pd.flow.delay(delayMs) - - # Delay a workflow a random amount of time - pd.flow.delay(random.randint(0, 999)) -``` - - -Paused workflow state - -When `pd.flow.delay` is executed in a Python step, the workflow itself will enter a **Paused** state. - -While the workflow is paused, it will not incur any credits towards compute time. You can also [view all paused workflows in the Event History](/event-history/#filtering-by-status). - - -### Credit usage - -The length of time a workflow is delayed from `pd.flow.delay` does _not_ impact your credit usage. For example, delaying a 256 megabyte workflow for five minutes will **not** incur ten credits. - -However, using `pd.flow.delay` in a workflow will incur two credits. - -One credit is used to initially start the workflow, then the second credit is used when the workflow resumes after its pause period has ended. - - -Exact credit usage depends on duration and memory configuration - -If your workflow's [execution timeout limit](/workflows/settings/#execution-timeout-limit) is set to longer than [default limit](/limits/#time-per-execution), it may incur more than two [credits](/pricing/#credits) when using `pd.flow.delay`. - - -## `cancel_url` and `resume_url` - -Both the built-in **Delay** actions and `pd.flow.delay` return a `cancel_url` and `resume_url` that lets you cancel or resume paused executions. - -These URLs are specific to a single execution of your workflow. While the workflow is paused, you can load these in your browser or send an HTTP request to either: - -- Hitting the `cancel_url` will immediately cancel that execution -- Hitting the `resume_url` will immediately resume that execution early - -[Since Pipedream pauses your workflow at the _end_ of the step where you run call `pd.flow.delay`](#when-delays-happen), you can send these URLs to third party systems, via email, or anywhere else you'd like to control the execution of your workflow. - -```python -import requests - -def handler(pd: 'pipedream'): - links = pd.flow.delay(15 * 60 * 1000) - # links contains a dictionary with two entries: resume_url and cancel_url - - # Send the URLs to a system you own - requests.post("https://example.com", json=links) - - # Email yourself the URLs. Click on the links to cancel / resume - pd.send.email( - subject=f"Workflow execution {pd.steps['trigger']['context']['id']}", - text=f"Cancel your workflow here: {links['cancel_url']} . Resume early here: {links['resume_url']}", - html=None - ) - - # Delay happens at the end of this step -``` - - -In `pd.send.email`, the `html` argument defaults to `""`, so it overrides the email `text` unless explicitly set to `None`. - - -## When delays happen - -**Pipedream pauses your workflow at the _end_ of the step where you call `pd.flow.delay`**. This lets you [send the `cancel_url` and `resume_url` to third-party systems](#cancel-url-and-resume-url). - -```python -def handler(pd: 'pipedream'): - urls = pd.flow.delay(15 * 60 * 1000) - cancel_url, resume_url = urls["cancel_url"], urls["resume_url"] - # ... run any code you want here - - # Delay happens at the end of this step -``` - -## Delays and HTTP responses - -You cannot run `pd.respond` after running `pd.flow.delay`. Pipedream ends the original execution of the workflow when `pd.flow.delay` is called and issues the following response to the client to indicate this state: - -> $.respond() not called for this invocation - -If you need to set a delay on an HTTP request triggered workflow, consider using [`time.sleep`](#time-sleep) instead. - -## `time.sleep` - -Alternatively, you can use `time.sleep` instead of using `pd.flow.delay` to delay individual workflow steps. - -However, there are some drawbacks to using `time.sleep` instead of `pd.flow.delay`. `time.sleep` will count towards your workflow's compute time, for example: - -```python -import time - -def handler(pd: 'pipedream'): - # delay this step for 30 seconds - delay = 30 - - time.sleep(delay) -``` - -The Python step above will hold the workflow's execution for this step for 30 seconds; however, 30 seconds will also _contribute_ to your credit usage. Also consider that workflows have a hard limit of {process.env.MAX_WORKFLOW_EXECUTION_LIMIT} seconds. diff --git a/docs-v2/pages/code/python/faqs.mdx b/docs-v2/pages/code/python/faqs.mdx deleted file mode 100644 index 08d8d427bebaf..0000000000000 --- a/docs-v2/pages/code/python/faqs.mdx +++ /dev/null @@ -1,23 +0,0 @@ -# Frequently Asked Questions about Python - -## What's the difference between `def handler(pd)` and the `pipedream` package for Python code steps? - -The pd object passed to the handler method lets you exit the [workflow early](/code/python/#ending-a-workflow-early), [integrate a Data Store](/code/python/using-data-stores/), and [use connected accounts](/code/python/auth/) into your Python code steps. - -However, at this time there are issues with our Python interpreter that is causing an `ECONNRESET` error. - -If you need [to use data from other steps](/code/python/#using-data-from-another-step) or [export data to other steps](/code/python/#sending-data-downstream-to-other-steps) in your workflow, we recommend using the `pipedream` package module. - -If you need to use a Data Store in your workflow, we recommend using a [pre-built action](/data-stores/) to retrieve or store data or [Node.js's Data Store](/code/nodejs/using-data-stores/) capabilities. - -## I've tried installing a Python package with a normal import and the magic comment system, but I still can't. What can I do? - -Some Python packages require binaries present within the environment in order to function properly. Or they include binaries but those binaries are not compatible with the Pipedream workflow environment. - -Unfortunately we cannot support these types of packages at this time, but if you have an issue importing a PyPI package into a Python code step [please open a issue](https://github.com/PipedreamHQ/pipedream/issues/new/choose). - -## Can I publish my Python code as a reusable pre-built action or trigger like you can with Node.js? - -Not at this time. Pipedream only supports Python as a code step language. The Components system only supports Node.js at this time. - -You can still duplicate Python code steps within the same workflow, but to reuse a code step, you'll need to copy and paste the Python code to another workflow. diff --git a/docs-v2/pages/code/python/http-requests.mdx b/docs-v2/pages/code/python/http-requests.mdx deleted file mode 100644 index 0ec4281fd944d..0000000000000 --- a/docs-v2/pages/code/python/http-requests.mdx +++ /dev/null @@ -1,292 +0,0 @@ -import Callout from '@/components/Callout' - -# Making HTTP Requests with Python - -HTTP requests are fundamental to working with APIs or other web services. You can make HTTP requests to retrieve data from APIs, fetch HTML from websites, or do pretty much anything your web browser can do. - -**Below, we'll review how to make HTTP requests using Python code on Pipedream.** - -We recommend using the popular `requests` HTTP client package available in Python to send HTTP requests, but [you can use any PyPi package you'd like on Pipedream](/code/python/#using-third-party-packages). - - - -## Basic `requests` usage notes - -No need to run `pip install`, just `import requests` at the top of your step's code and it's available for your code to use. - -To use `requests` on Pipedream, you'll just need to import the `requests` PyPi package: - -```python -import requests -``` - -You make HTTP requests by passing a URL and optional request parameters to one of [Requests' 7 HTTP request methods](https://requests.readthedocs.io/en/latest/api/#main-interface). - -**Here's how to make a basic HTTP request on Pipedream:** - -```python -r = requests.get('https://swapi.dev/api/films/') -``` - -The [Response](https://requests.readthedocs.io/en/latest/api/#requests.Response) object `r` contains a lot of information about the response: its content, headers, and more. Typically, you just care about the content, which you can access in the `text` property of the response: - -```python -r = requests.get('https://swapi.dev/api/films/') - -# HTTP response content is in the text property -r.text -``` - -Requests automatically decodes the content of the response based on its encoding, `r.encoding`, which is determined based on the HTTP headers. - -If you're dealing with JSON data, you can call `r.json()` to decode the content as JSON: - -```python -r = requests.get('https://swapi.dev/api/films/') - -# The json-encoded content of a response, if any -r.json() -``` - -If JSON decoding fails, `r.json()` raises an exception. - -## Making a `GET` request - -GET requests typically are for retrieving data from an API. Below is an example. - -```python -import requests - -def handler(pd: "pipedream"): - url = "https://swapi.dev/api/people/1" - - r = requests.get(url) - - # The response is logged in your Pipedream step results: - print(r.text) - - # The response status code is logged in your Pipedream step results: - print(r.status_code) -``` - -## Making a `POST` request - -```python -import requests - -def handler(pd: "pipedream"): - # This a POST request to this URL will echo back whatever data we send to it - url = "https://postman-echo.com/post" - - data = {"name": "Bulbasaur"} - - r = requests.post(url, data=data) - - # The response is logged in your Pipedream step results: - print(r.text) - - # The response status code is logged in your Pipedream step results: - print(r.status_code) -``` - -When you make a `POST` request, pass a dictionary with the data you'd like to send to the `data` argument. Requests automatically form-encodes the data when the request is made. - - -The code example above will NOT set the `Content-Type` header, meaning it will NOT be set to `application/json`. - -If you want the header set to `application/json` and don't want to encode the `dict` yourself, you can pass it using the `json` parameter and it will be encoded automatically: - -```python - url = "https://postman-echo.com/post" - data = {"name": "Bulbasaur"} - r = requests.post(url, json=data) -``` - - -## Passing query string parameters to a `GET` request - -Retrieve fake comment data on a specific post using [JSONPlaceholder](https://jsonplaceholder.typicode.com/), a free mock API service. Here, you fetch data from the `/comments` resource, retrieving data for a specific post by query string parameter: `/comments?postId=1`. - -```python -import requests - -def handler(pd: "pipedream"): - url = "https://jsonplaceholder.typicode.com/comments" - params = {"postId": 1} - - # Make an HTTP GET request using requests - r = requests.get(url, params=params) - - # Retrieve the content of the response - data = r.text -``` - -You should pass query string parameters as a dictionary using the `params` keyword argument, like above. When you do, `requests` automatically [URL-encodes](https://www.w3schools.com/tags/ref_urlencode.ASP) the parameters for you, which you'd otherwise have to do manually. - -## Sending a request with HTTP headers - -To add HTTP headers to a request, pass a dictionary to the `headers` parameter: - -```python -import requests -import json - -def handler(pd: "pipedream"): - url = "https://jsonplaceholder.typicode.com/posts" - headers = {"Content-Type": "application/json"} - data = {"name": "Luke"} - - # Make an HTTP POST request using requests - r = requests.post(url, headers=headers, data=json.dumps(data)) -``` - -## Sending a request with a secret or API key - -Most APIs require you authenticate HTTP requests with an API key or other token. **Please review the docs for your service to understand how they accept this data.** - -Here's an example showing an API key passed in an HTTP header: - -```python -import requests - -def handler(pd: "pipedream"): - url = "https://jsonplaceholder.typicode.com/posts" - headers = {"X-API-KEY": "123"} # API KEY - data = {"name": "Luke"} - - # Make an HTTP POST request using requests - r = requests.post(url, headers=headers, json=data) -``` - -## Sending files - -An example of sending a previously stored file in the workflow's `/tmp` directory: - -```python -import requests - -def handler(pd: "pipedream"): - # Retrieving a previously saved file from workflow storage - files = {"image": open("/tmp/python-logo.png", "rb")} - - r = requests.post(url="https://api.imgur.com/3/image", files=files) -``` - -## Downloading a file to the `/tmp` directory - -This example shows you how to download a file to a file in [the `/tmp` directory](/code/python/working-with-files/). This can be especially helpful for downloading large files: it streams the file to disk, minimizing the memory the workflow uses when downloading the file. - -```python -import requests - -def handler(pd: "pipedream"): - # Download the webpage HTML file to /tmp - with requests.get("https://example.com", stream=True) as response: - # Check if the request was successful - response.raise_for_status() - - # Open the new file /tmp/file.html in binary write mode - with open("/tmp/file.html", "wb") as file: - for chunk in response.iter_content(chunk_size=8192): - # Write the chunk to file - file.write(chunk) -``` - -## Uploading a file from the `/tmp` directory - -This example shows you how to make a `multipart/form-data` request with a file as a form part. You can store and read any files from [the `/tmp` directory](/code/python/working-with-files/#the-tmp-directory). - -This can be especially helpful for uploading large files: it streams the file from disk, minimizing the memory the workflow uses when uploading the file. - -```python -import requests -from requests_toolbelt.multipart.encoder import MultipartEncoder - -def handler(pd: "pipedream"): - m = MultipartEncoder(fields={ - 'file': ('filename', open('/tmp/file.pdf', 'rb'), 'application/pdf') - }) - - r = requests.post("https://example.com", data=m, - headers={'Content-Type': m.content_type}) -``` - -## IP addresses for HTTP requests made from Pipedream workflows - -By default, [HTTP requests made from Pipedream can come from a large range of IP addresses](/privacy-and-security/#hosting-details). **If you need to restrict the IP addresses HTTP requests come from, you can [Use a Pipedream VPC](/workflows/vpc/) to route all outbound HTTP requests through a single IP address.** - -## Using an HTTP proxy to proxy requests through another host - -By default, HTTP requests made from Pipedream can come from a range of IP addresses. **If you need to make requests from a single IP address, you can route traffic through an HTTP proxy**: - -```python -import requests - -def handler(pd: "pipedream"): - user = "USERNAME" # Replace with your HTTP proxy username - password = "PASSWORD" # Replace with your HTTP proxy password - host = "10.10.1.10" # Replace with the HTTP proxy URL - port = 1080 # Replace with the port of the HTTP proxy - proxies = { - "https": f"http://{user}:{password}@{host}:{port}", - } - - r = requests.request("GET", "https://example.com", proxies=proxies) -``` - -## Paginating API requests - -When you fetch data from an API, the API may return records in "pages". For example, if you're trying to fetch a list of 1,000 records, the API might return those in groups of 100 items. - -Different APIs paginate data in different ways. You'll need to consult the docs of your API provider to see how they suggest you paginate through records. - -## Sending a GraphQL request - -Construct a GraphQL query as a string and then using the requests library to send it to the GraphQL server: - -```python -import requests - -def handler(pd: "pipedream"): - url = "https://beta.pokeapi.co/graphql/v1beta" - - query = """ -query samplePokeAPIquery { - generations: pokemon_v2_generation { - name - pokemon_species: pokemon_v2_pokemonspecies_aggregate { - aggregate { - count - } - } - } -} - """ - - r = requests.post(url, json={"query": query}) - return r.json() -``` - -### Sending an authenticated GraphQL request - -Authenticate your connected accounts in Pipedream with GraphQL requests using `pd.inputs[appName]["$auth"]`: - -```python -import requests - -def handler(pd: "pipedream"): - url = "https://api.github.com/graphql" - query = """ -query { - viewer { - login - } -} - """ - token = pd.inputs["github"]["$auth"]["oauth_access_token"] - headers = {"authorization": f"Bearer {token}"} - r = requests.post(url, json={"query": query}, headers=headers) - return r.json() -``` - -Alternatively, you can use Environment Variables as well for simple API key based GraphQL APIs. diff --git a/docs-v2/pages/code/python/images/print-logs.png b/docs-v2/pages/code/python/images/print-logs.png deleted file mode 100644 index 49f35ea46bf0b..0000000000000 Binary files a/docs-v2/pages/code/python/images/print-logs.png and /dev/null differ diff --git a/docs-v2/pages/code/python/index.mdx b/docs-v2/pages/code/python/index.mdx deleted file mode 100644 index b7eaa0436eac3..0000000000000 --- a/docs-v2/pages/code/python/index.mdx +++ /dev/null @@ -1,298 +0,0 @@ -import Callout from '@/components/Callout' -import VideoPlayer from '@/components/VideoPlayer' - -# Python - -Pipedream supports [Python v{process.env.PYTHON_VERSION}](https://www.python.org) in workflows. Run any Python code, use any [PyPI package](https://pypi.org/), connect to APIs, and more. - -## Adding a Python code step - -1. Click the + icon to add a new step -2. Click **Custom Code** -3. In the new step, select the `python` language runtime in language dropdown - -## Python Code Step Structure - -A new Python Code step will have the following structure: - -```python -def handler(pd: "pipedream"): - # Reference data from previous steps - print(pd.steps["trigger"]["context"]["id"]) - # Return data for use in future steps - return {"foo": {"test": True}} -``` - -You can also perform more complex operations, including [leveraging your connected accounts to make authenticated API requests](/code/python/auth/), [accessing Data Stores](/code/python/using-data-stores/) and [installing PyPI packages](/code/python/#using-third-party-packages). - -- [Install PyPI Packages](/code/python/#using-third-party-packages) -- [Import data exported from other steps](/code/python/#using-data-from-another-step) -- [Export data to downstream steps](/code/python/#sending-data-downstream-to-other-steps) -- [Retrieve data from a data store](/code/python/using-data-stores/#retrieving-data) -- [Store data into a data store](/code/python/using-data-stores/#saving-data) -- [Access API credentials from connected accounts](/code/python/auth/) - -## Logging and debugging - -You can use `print` at any time in a Python code step to log information as the script is running. - -The output for the `print` **logs** will appear in the `Results` section just beneath the code editor. - -![Python print log output in the results](./images/print-logs.png) - -## Using third party packages - - - -You can use any packages from [PyPI](https://pypi.org) in your Pipedream workflows. This includes popular choices such as: - -- [`requests` for making HTTP requests](https://pypi.org/project/requests/) -- [`sqlalchemy`for retrieving or inserting data in a SQL database](https://pypi.org/project/sqlalchemy/) -- [`pandas` for working with complex datasets](https://pypi.org/project/pandas/) - -To use a PyPI package, just include it in your step's code: - -```python -import requests -``` - -And that's it. No need to update a `requirements.txt` or specify elsewhere in your workflow of which packages you need. Pipedream will automatically install the dependency for you. - -### If your package's `import` name differs from its PyPI package name - -Pipedream's package installation uses [the `pipreqs` package](https://github.com/bndr/pipreqs) to detect package imports and install the associated package for you. Some packages, like [`python-telegram-bot`](https://python-telegram-bot.org/), use an `import` name that differs from their PyPI name: - -```bash -pip install python-telegram-bot -``` - -vs. - -```python -import telegram -``` - -Use the built in [magic comment system to resolve these mismatches](/code/python/import-mappings/): - -```python -# pipedream add-package python-telegram-bot -import telegram -``` - -### Pinning package versions - -Each time you deploy a workflow with Python code, Pipedream downloads the PyPi packages you `import` in your step. **By default, Pipedream deploys the latest version of the PyPi package each time you deploy a change**. - -There are many cases where you may want to specify the version of the packages you're using. If you'd like to use a _specific_ version of a package in a workflow, you can add that version in a [magic comment](/code/python/import-mappings/), for example: - -```python -# pipedream add-package pandas==2.0.0 -import pandas -``` - - -Currently, you cannot use different versions of the same package in different steps in a workflow. - - -## Making an HTTP request - -We recommend using the popular `requests` HTTP client package available in Python to send HTTP requests. - -No need to run `pip install`, just `import requests` at the top of your step's code and it's available for your code to use. - -See the [Making HTTP Requests with Python](/code/python/http-requests/) docs for more information. - -## Returning HTTP responses - -You can return HTTP responses from [HTTP-triggered workflows](/workflows/steps/triggers/#http) using the `pd.respond()` method: - -```python -def handler(pd: "pipedream"): - pd.respond({ - "status": 200, - "body": { - "message": "Everything is ok" - } - }) -``` - -Please note to always include at least the `body` and `status` keys in your `pd.respond` argument. The `body` must also be a JSON serializable object or dictionary. - - -Unlike the [Node.js equivalent](https://pipedream.com/docs/workflows/steps/triggers/#http-responses), the Python `pd.respond` helper does not yet support responding with Streams. - - - -_Don't forget_ to [configure your workflow's HTTP trigger to allow a custom response](/workflows/steps/triggers/#http-responses). Otherwise your workflow will return the default response. - - -## Sharing data between steps - -A step can accept data from other steps in the same workflow, or pass data downstream to others. - -### Using data from another step - -In Python steps, data from the initial workflow trigger and other steps are available in the `pd.steps` object. - -In this example, we'll pretend this data is coming into our workflow's HTTP trigger via POST request. - -```json -// POST .m.pipedream.net -{ - "id": 1, - "name": "Bulbasaur", - "type": "plant" -} -``` - -In our Python step, we can access this data in the `pd.steps` object passed into the `handler`. Specifically, this data from the POST request into our workflow is available in the `trigger` dictionary item. - -```python -def handler(pd: "pipedream"): - # retrieve the data from the HTTP request in the initial workflow trigger - pokemon_name = pd.steps["trigger"]["event"]["name"] - pokemon_type = pd.steps["trigger"]["event"]["type"] - - print(f"{pokemon_name} is a {pokemon_type} type Pokemon") -``` - -### Sending data downstream to other steps - -To share data created, retrieved, transformed or manipulated by a step to others downstream, `return` the data in the `handler` function: - -```python -# This step is named "code" in the workflow -import requests - -def handler(pd: "pipedream"): - r = requests.get("https://pokeapi.co/api/v2/pokemon/charizard") - # Store the JSON contents into a variable called "pokemon" - pokemon = r.json() - - # Expose the data to other steps in the "pokemon" key from this step - return { - "pokemon": pokemon - } -``` - -Now this `pokemon` data is accessible to downstream steps within `pd.steps["code"]["pokemon"]` - - -You can only export JSON-serializable data from steps. Things like: - -- strings -- numbers -- lists -- dictionaries - - -## Using environment variables - -You can leverage any [environment variables defined in your Pipedream account](/environment-variables/#environment-variables) in a Python step. This is useful for keeping your secrets out of code as well as keeping them flexible to swap API keys without having to update each step individually. - -To access them, use the `os` module. - -```python -import os - -def handler(pd: "pipedream"): - token = os.environ["AIRTABLE_API_KEY"] - - print(token) -``` - -Or an even more useful example, using the stored environment variable to make an authenticated API request. - -### Using API key authentication - -If an particular service requires you to use an API key, you can pass it via the headers of the request. - -This proves your identity to the service so you can interact with it: - -```python -import requests -import os - -def handler(pd: "pipedream"): - token = os.environ["AIRTABLE_API_KEY"] - - url = "https://api.airtable.com/v0/your-airtable-base/your-table" - - headers = { "Authorization": f"Bearer {token}"} - r = requests.get(url, headers=headers) - - print(r.text) -``` - - -There are 2 different ways of using the `os` module to access your environment variables. - -`os.environ["ENV_NAME_HERE"]` will raise an error that stops your workflow if that key doesn't exist in your Pipedream account. - -Whereas `os.environ.get("ENV_NAME_HERE")` will _not_ throw an error and instead returns an empty string. - -If your code relies on the presence of a environment variable, consider using `os.environ["ENV_NAME_HERE"]` instead. - - -## Handling errors - -You may need to exit a workflow early. In a Python step, just a `raise` an error to halt a step's execution. - -```python -raise NameError("Something happened that should not. Exiting early.") -``` - -All exceptions from your Python code will appear in the **logs** area of the results. - -## Ending a workflow early - -Sometimes you want to end your workflow early, or otherwise stop or cancel the execution of a workflow under certain conditions. For example: - -- You may want to end your workflow early if you don't receive all the fields you expect in the event data. -- You only want to run your workflow for 5% of all events sent from your source. -- You only want to run your workflow for users in the United States. If you receive a request from outside the U.S., you don't want the rest of the code in your workflow to run. -- You may use the `user_id` contained in the event to look up information in an external API. If you can't find data in the API tied to that user, you don't want to proceed. - -**In any code step, calling `return pd.flow.exit()` will end the execution of the workflow immediately.** No remaining code in that step, and no code or destination steps below, will run for the current event. - - -It's a good practice to use `return pd.flow.exit()` to immediately exit the workflow. -In contrast, `pd.flow.exit()` on its own will end the workflow only after executing all remaining code in the step. - - -```python -def handler(pd: "pipedream"): - return pd.flow.exit("reason") - print("This code will not run, since pd.flow.exit() was called above it") -``` - -You can pass any string as an argument to `pd.flow.exit()`: - -```python -def handler(pd: "pipedream"): - return pd.flow.exit("Exiting early. Goodbye.") - print("This code will not run, since pd.flow.exit() was called above it") -``` - -Or exit the workflow early within a conditional: - -```python -import random - -def handler(pd: "pipedream"): - # Flip a coin, running pd.flow.exit() for 50% of events - if random.randint(0, 100) <= 50: - return pd.flow.exit("reason") - - print("This code will only run 50% of the time"); -``` - -## File storage - -You can also store and read files with Python steps. This means you can upload photos, retrieve datasets, accept files from an HTTP request and more. - -The `/tmp` directory is accessible from your workflow steps for saving and retrieving files. - -You have full access to read and write both files in `/tmp`. - -See the [Working with the filesystem in Python](/code/python/working-with-files/) docs for more information. diff --git a/docs-v2/pages/code/python/rerun.mdx b/docs-v2/pages/code/python/rerun.mdx deleted file mode 100644 index cf182f36c0989..0000000000000 --- a/docs-v2/pages/code/python/rerun.mdx +++ /dev/null @@ -1,179 +0,0 @@ -import Callout from '@/components/Callout' - -# Pause, resume, and rerun a workflow - -You can use `pd.flow.suspend` and `pd.flow.rerun` to pause a workflow and resume it later. - -This is useful when you want to: - -- Pause a workflow until someone manually approves it -- Poll an external API until some job completes, and proceed with the workflow when it's done -- Trigger an external API to start a job, pause the workflow, and resume it when the external API sends an HTTP callback - -We'll cover all of these examples below. - -## `pd.flow.suspend` - -Use `pd.flow.suspend` when you want to pause a workflow and proceed with the remaining steps only when manually approved or cancelled. - -For example, you can suspend a workflow and send yourself a link to manually resume or cancel the rest of the workflow: - -```python -def handler(pd: 'pipedream'): - urls = pd.flow.suspend() - pd.send.email( - subject="Please approve this important workflow", - text=f"Click here to approve the workflow: ${urls["resume_url"]}, and cancel here: ${urls["cancel_url"]}" - ) - # Pipedream suspends your workflow at the end of the step -``` - -You'll receive an email like this: - -![Approve this workflow](https://res.cloudinary.com/pipedreamin/image/upload/v1655272047/docs/approve-workflow_oc06k3.png) - -And can resume or cancel the rest of the workflow by clicking on the appropriate link. - -### `resume_url` and `cancel_url` - -In general, calling `pd.flow.suspend` returns a `cancel_url` and `resume_url` that lets you cancel or resume paused executions. Since Pipedream pauses your workflow at the _end_ of the step, you can pass these URLs to any external service before the workflow pauses. If that service accepts a callback URL, it can trigger the `resume_url` when its work is complete. - -These URLs are specific to a single execution of your workflow. While the workflow is paused, you can load these in your browser or send any HTTP request to them: - -- Sending an HTTP request to the `cancel_url` will cancel that execution -- Sending an HTTP request to the `resume_url` will resume that execution - -If you resume a workflow, any data sent in the HTTP request is passed to the workflow and returned in the `$resume_data` [step export](/workflows/steps/#step-exports) of the suspended step. For example, if you call `pd.flow.suspend` within a step named `code`, the `$resume_data` export should contain the data sent in the `resume_url` request: - -![resume data step export](https://res.cloudinary.com/pipedreamin/image/upload/v1655271815/docs/resume_data_lafhxr.png) - -### Default timeout of 24 hours - -By default, `pd.flow.suspend` will automatically resume the workflow after 24 hours. You can set your own timeout (in milliseconds) as the first argument: - -```python -def handler(pd: 'pipedream'): - # 7 days - TIMEOUT = 1000 * 60 * 60 * 24 * 7 - pd.flow.suspend(TIMEOUT) -``` - -## `pd.flow.rerun` - -Use `pd.flow.rerun` when you want to run a specific step of a workflow multiple times. This is useful when you need to start a job in an external API and poll for its completion, or have the service call back to the step and let you process the HTTP request within the step. - -### Polling for the status of an external job - -Sometimes you need to poll for the status of an external job until it completes. `pd.flow.rerun` lets you rerun a specific step multiple times: - -```python -import requests - -def handler(pd: 'pipedream'): - MAX_RETRIES = 3 - # 10 seconds - DELAY = 1000 * 10 - run = pd.context['run'] - print(pd.context) - # pd.context.run.runs starts at 1 and increments when the step is rerun - if run['runs'] == 1: - # pd.flow.rerun(delay, context (discussed below), max retries) - pd.flow.rerun(DELAY, None, MAX_RETRIES) - - elif run['runs'] == MAX_RETRIES + 1: - raise Exception("Max retries exceeded") - - else: - # Poll external API for status - response = requests.get("https://example.com/status") - # If we're done, continue with the rest of the workflow - if response.json().status == "DONE": - return response.json() - - # Else retry later - pd.flow.rerun(DELAY, None, MAX_RETRIES) -``` - -`pd.flow.rerun` accepts the following arguments: - -```python -pd.flow.rerun( - delay, # The number of milliseconds until the step will be rerun - context, # JSON-serializable data you need to pass between runs - maxRetries, # The total number of times the step will rerun. Defaults to 10 -) -``` - -### Accept an HTTP callback from an external service - -When you trigger a job in an external service, and that service can send back data in an HTTP callback to Pipedream, you can process that data within the same step using `pd.flow.retry`: - -```python -import requests - -def handler(pd: 'pipedream'): - TIMEOUT = 86400 * 1000 - run = pd.context['run'] - # pd.context['run']['runs'] starts at 1 and increments when the step is rerun - if run['runs'] == 1: - links = pd.flow.rerun(TIMEOUT, None, 1) - # links contains a dictionary with two entries: resume_url and cancel_url - - # Send resume_url to external service - await request.post("your callback URL", json=links) - - # When the external service calls back into the resume_url, you have access to - # the callback data within pd.context.run['callback_request'] - elif 'callback_request' in run: - return run['callback_request'] - -``` - -### Passing `context` to `pd.flow.rerun` - -Within a Python code step, `pd.context.run.context` contains the `context` passed from the prior call to `rerun`. This lets you pass data from one run to another. For example, if you call: - -```python -pd.flow.rerun(1000, { "hello": "world" }) -``` - -`pd.context.run.context` will contain: - -
-resume data step export -
- -### `maxRetries` - -By default, `maxRetries` is **10**. - -When you exceed `maxRetries`, the workflow proceeds to the next step. If you need to handle this case with an exception, `raise` an Exception from the step: - -```python -def handler(pd: 'pipedream'): - MAX_RETRIES = 3 - run = pd.context['run'] - if run['runs'] == 1: - pd.flow.rerun(1000, None, MAX_RETRIES) - - else if (run['runs'] == MAX_RETRIES + 1): - raise Exception("Max retries exceeded") -``` - -## Behavior when testing - -When you're building a workflow and test a step with `pd.flow.suspend` or `pd.flow.rerun`, it will not suspend the workflow, and you'll see a message like the following: - -> Workflow execution canceled — this may be due to `pd.flow.suspend()` usage (not supported in test) - -These functions will only suspend and resume when run in production. - -## Credits usage when using `pd.flow.suspend` / `pd.flow.rerun` - -You are not charged for the time your workflow is suspended during a `pd.flow.suspend` or `pd.flow.rerun`. Only when workflows are resumed will compute time count toward [credit usage](/pricing/#credits). - - -When a suspended workflow reawakens, it will reset the credit counter. - -Each rerun or reawakening from a suspension will count as a new fresh credit. - diff --git a/docs-v2/pages/code/python/using-data-stores.mdx b/docs-v2/pages/code/python/using-data-stores.mdx deleted file mode 100644 index 15206dabe2e74..0000000000000 --- a/docs-v2/pages/code/python/using-data-stores.mdx +++ /dev/null @@ -1,311 +0,0 @@ -import Callout from '@/components/Callout' - -# Using Data Stores - -You can store and retrieve data from [Data stores](/data-stores/) in Python without connecting to a 3rd party database. - -Add a data store as a input to a Python step, then access it in your Python `handler` with `pd.inputs["data_store"]`. - -```python -def handler(pd: "pipedream"): - # Access the data store under the pd.inputs - data_store = pd.inputs["data_store"] - - # Store a value under a key - data_store["key"] = "Hello World" - - # Retrieve the value and print it to the step's Logs - print(data_store["key"]) - -``` - -## Adding a Data Store - -Click _Add Data Store_ near the top of a Python step: - -![Adding a data store to a Python step](https://res.cloudinary.com/pipedreamin/image/upload/v1658954673/docs/components/CleanShot_2022-07-27_at_16.44.16_olfejo.gif) - -This will add the selected data store to your Python code step. - -## Saving data - -Data stores are key-value stores. Saving data within a data store is just like setting a property on a dictionary: - -```python -from datetime import datetime - -def handler(pd: "pipedream"): - # Access the data store under the pd.inputs - data_store = pd.inputs["data_store"] - - # Store a timestamp - data_store["last_ran_at"] = datetime.now().isoformat() -``` - -## Retrieving keys - -Fetch all the keys in a given data store using the `keys` method: - -```python -def handler(pd: "pipedream"): - # Access the data store under the pd.inputs - data_store = pd.inputs["data_store"] - - # Retrieve all keys in the data store - keys = pd.inputs["data_store"].keys() - - # Print a comma separated string of all keys - print(*keys, sep=",") -``` - -## Checking for the existence of specific keys - -If you need to check whether a specific `key` exists in a data store, use `if` and `in` as a conditional: - -```python -def handler(pd: "pipedream"): - # Access the data store under the pd.inputs - data_store = pd.inputs["data_store"] - - # Search for a key in a conditional - if "last_ran_at" in data_store: - print(f"Last ran at {data_store['last_ran_at']}") -``` - -## Retrieving data - -Data stores are very performant at retrieving single records by keys. However you can also use key iteration to retrieve all records within a Data Store as well. - - -Data stores are intended to be a fast and convienent data storage option for quickly adding data storage capability to your workflows without adding another database dependency. - -However, if you need more advanced querying capabilities for querying records with nested dictionaries or filtering based on a record value - consider using a full fledged database. Pipedream can integrate with MySQL, Postgres, DynamoDb, MongoDB and more. - - -### Get a single record - -You can retrieve single records from a data store by key: - -```python -def handler(pd: "pipedream"): - # Access the data store under the pd.inputs - data_store = pd.inputs["data_store"] - - # Retrieve the timestamp value by the key name - last_ran_at = data_store["last_ran_at"] - - # Print the timestamp - print(f"Last ran at {last_ran_at}") -``` - -Alternatively, use the `data_store.get()` method to retrieve a specific key's contents: - -```python -def handler(pd: "pipedream"): - # Access the data store under the pd.inputs - data_store = pd.inputs["data_store"] - - # Retrieve the timestamp value by the key name - last_ran_at = data_store.get("last_ran_at") - - # Print the timestamp - print(f"Last ran at {last_ran_at}") -``` - - -What's the difference between `data_store["key"]` and `data_store.get("key")`? - -- `data_store["key"]` will throw a `TypeError` if the key doesn't exist in the data store. -- `data_store.get("key")` will instead return `None` if the key doesn't exist in the data store. -- `data_store.get("key", "default_value")` will return `"default_value"` if the key doesn't exist on the data store. - - -### Retrieving all records - -You can retrieve all records within a data store by iterating over all keys within the data store. - -For example, use the `data_store.keys()` method to retrieve all keys, then iterate over each to build a dictionary of records: - -```python -def handler(pd: "pipedream"): - data_store = pd.inputs['data_store'] - - records = {} - keys = data_store.keys() - - # iterate through all keys within the Data Store to generate a new dictionary - for key in keys: - records[key] = data_store[key] - - return records -``` - -This code step example exports all records within the data store as a dictionary. - - -The `datastore.keys()` method does not return a list, but instead it returns a `Keys` iterable object. You cannot export a `data_store` or `data_store.keys()` from a Python code step at this time. - -Instead build a dictionary or list using the `data_store.keys()` method. - - -## Deleting or updating values within a record - -To delete or update the _value_ of an individual record, assign `key` a new value or `''` to remove the value but retain the key. - -```python -def handler(pd: "pipedream"): - # Access the data store under the pd.inputs - data_store = pd.inputs["data_store"] - - # Assign a new value to the key - data_store["myKey"] = "newValue" - - # Remove the value but retain the key - data_store["myKey"] = "" -``` - -### Working with nested dictionaries - -You can store dictionaries within a record. This allows you to create complex records. - -However, to update specific attributes within a nested dictionary, you'll need to replace the record entirely. - -For example, the code the below will **not** update the `name` attribute on the stored dictionary stored under the key `pokemon`: - -```python -def handler(pd: "pipedream"): - # The current dictionary looks like this: - # pokemon: { - # "name": "Charmander" - # "type": "fire" - # } - - # You'll see "Charmander" in the logs - print(pd.inputs['data_store']['pokemon']['name']) - - # attempting to overwrite the pokemon's name will not apply - pd.inputs['data_store']['pokemon']['name'] = 'Bulbasaur' - - # Exports "Charmander" - return pd.inputs['data_store']['pokemon']['name'] -``` - -Instead, _overwrite_ the entire record to modify attributes: - -```python -def handler(pd: "pipedream"): - # retrieve the record item by it's key first - pokemon = pd.inputs['data_store']['pokemon'] - - # now update the record's attribute - pokemon['name'] = 'Bulbasaur' - - # and out right replace the record with the new modified dictionary - pd.inputs['data_store']['pokemon'] = pokemon - - # Now we'll see "Bulbasaur" exported - return pd.inputs['data_store']['pokemon']['name'] -``` - -## Deleting specific records - -To delete individual records in a data store, use the `del` operation for a specific `key`: - -```python -def handler(pd: "pipedream"): - # Access the data store under the pd.inputs - data_store = pd.inputs["data_store"] - - # Delete the last_ran_at timestamp key - del data_store["last_ran_at"] -``` - -## Deleting all records from a specific data store - -If you need to delete all records in a given data store, you can use the `clear` method. - -```python -def handler(pd: "pipedream"): - # Access the data store under the pd.inputs - data_store = pd.inputs["data_store"] - - # Delete the entire contents of the datas store - data_store.clear() -``` - - -`data_store.clear()` is an **irreversible** change, **even when testing code** in the workflow builder. - - -## Viewing store data - -You can view the contents of your data stores in your [Pipedream dashboard](https://pipedream.com/stores). - -From here you can also manually edit your data store's data, rename stores, delete stores or create new stores. - -## Workflow counter example - -You can use a data store as a counter. For example, this code counts the number of times the workflow runs: - -```python -def handler(pd: "pipedream"): - # Access the data store under the pd.inputs - data_store = pd.inputs["data_store"] - - # if the counter doesn't exist yet, start it at one - if data_store.get("counter") == None: - data_store["counter"] = 1 - - # Otherwise, increment it by one - else: - count = data_store["counter"] - data_store["counter"] = count + 1 -``` - -## Dedupe data example - -Data Stores are also useful for storing data from prior runs to prevent acting on duplicate data, or data that's been seen before. - -For example, this workflow's trigger contains an email address from a potential new customer. But we want to track all emails collected so we don't send a welcome email twice: - -```python -def handler(pd: "pipedream"): - # Access the data store - data_store = pd.inputs["data_store"] - - # Reference the incoming email from the HTTP request - new_email = pd.steps["trigger"]["event"]["body"]["new_customer_email"] - - # Retrieve the emails stored in our data store - emails = data_store.get('emails', []) - - # If this email has been seen before, exit early - if new_email in emails: - print(f"Already seen {new_email}, exiting") - return False - - # This email is new, append it to our list - else: - print(f"Adding new email to data store {new_email}") - emails.append(new_email) - data_store["emails"] = emails - return new_email -``` - -### Supported data types - -Data stores can hold any JSON-serializable data within the storage limits. This includes data types including: - -- Strings -- Dictionaries -- Lists -- Integers -- Floats - -But you cannot serialize Modules, Functions, Classes, or other more complex objects. - -### Querying records - -You can retrieve up to {process.env.DATA_STORES_MAX_KEYS} records within a single query. - -The `pd.inputs["data_store"].keys()` function allow you to retrieve all keys from your data store. However, using this method with a data store with over {process.env.DATA_STORES_MAX_KEYS} keys will result in a 426 error. diff --git a/docs-v2/pages/code/python/working-with-files.mdx b/docs-v2/pages/code/python/working-with-files.mdx deleted file mode 100644 index d6ca893b8f290..0000000000000 --- a/docs-v2/pages/code/python/working-with-files.mdx +++ /dev/null @@ -1,115 +0,0 @@ -import Callout from '@/components/Callout' - -# Working with the filesystem in Python - -You can work with files within a workflow. For instance, downloading content from some service to upload to another. Here are some sample code for common file operations. - -## The `/tmp` directory - -Within a workflow, you have full read-write access to the `/tmp` directory. You have {process.env.TMP_SIZE_LIMIT} of available space in `/tmp` to save any file. - -### Managing `/tmp` across workflow runs - -The `/tmp` directory is stored on the virtual machine that runs your workflow. We call this the execution environment ("EE"). More than one EE may be created to handle high-volume workflows. And EEs can be destroyed at any time (for example, after about 10 minutes of receiving no events). This means that you should not expect to have access to files across executions. At the same time, files _may_ remain, so you should clean them up to make sure that doesn't affect your workflow. **Use [the `tempfile` module](https://docs.python.org/3/library/tempfile.html) to cleanup files after use, or [delete the files manually](#deleting-a-file).** - -## Writing a file to `/tmp` - -```python -import requests - -def handler(pd: "pipedream"): - # Download the Python logo - r = requests.get("https://www.python.org/static/img/python-logo@2x.png") - - # Create a new file python-logo.png in the /tmp/data directory - with open("/tmp/python-logo.png", "wb") as f: - # Save the content of the HTTP response into the file - f.write(r.content) -``` - -Now `/tmp/python-logo.png` holds the official Python logo. - -## Reading a file from `/tmp` - -You can also open files you have previously stored in the `/tmp` directory. Let's open the `python-logo.png` file. - -```python -import os - -def handler(pd: "pipedream"): - with open("/tmp/python-logo.png") as f: - # Store the contents of the file into a variable - file_data = f.read() -``` - -## Listing files in `/tmp` - -If you need to check what files are currently in `/tmp` you can list them and print the results to the **Logs** section of **Results**: - -```python -import os - -def handler(pd: "pipedream"): - # Prints the files in the tmp directory - print(os.listdir("/tmp")) -``` - -## Deleting a file - -```python -import os - -def handler(pd: "pipedream"): - print(os.unlink("/tmp/your-file")) -``` - -## Downloading a file to `/tmp` - -[See this example](/code/python/http-requests/#downloading-a-file-to-the-tmp-directory) to learn how to download a file to `/tmp`. - -## Uploading a file from `/tmp` - -[See this example](/code/python/http-requests/#uploading-a-file-from-the-tmp-directory) to learn how to upload a file from `/tmp` in an HTTP request. - -## Downloading a file, uploading it in another `multipart/form-data` request - -```python -import requests -from requests_toolbelt.multipart.encoder import MultipartEncoder -import os - -def handler(pd: "pipedream"): - download_url = "https://example.com" - upload_url = "http://httpbin.org/post" - file_path = "/tmp/index.html" - content_type = "text/html" - - # DOWNLOAD - with requests.get(download_url, stream=True) as response: - response.raise_for_status() - with open(file_path, "wb") as file: - for chunk in response.iter_content(chunk_size=8192): - file.write(chunk) - - # UPLOAD - multipart_data = MultipartEncoder(fields={ - 'file': (os.path.basename(file_path), open(file_path, 'rb'), content_type) - }) - response = requests.post( - upload_url, - data=multipart_data, - headers={'Content-Type': multipart_data.content_type} - ) -``` - -## `/tmp` limitations - -The `/tmp` directory can store up to {process.env.TMP_SIZE_LIMIT} of storage. Also the storage may be wiped or may not exist between workflow executions. - -To avoid errors, assume that the `/tmp` directory is empty between workflow runs. Please refer to the [disk limits](/limits/#disk) for details. - - -Are File Stores helpers available for Python to download, upload and manage files? - -At this time no, only Node.js includes a helper to interact with the [File Store](/projects/file-stores/) programmatically within workflows. - diff --git a/docs-v2/pages/components/_meta.json b/docs-v2/pages/components/_meta.json deleted file mode 100644 index 32696cfa2d8fe..0000000000000 --- a/docs-v2/pages/components/_meta.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "index": "Overview", - "api": "Component API", - "actions-quickstart": "Quickstart — Actions", - "sources-quickstart": "Quickstart — Sources", - "guidelines": "Guidelines", - "typescript": "TypeScript components" -} diff --git a/docs-v2/pages/components/api.mdx b/docs-v2/pages/components/api.mdx deleted file mode 100644 index d6346d06c308b..0000000000000 --- a/docs-v2/pages/components/api.mdx +++ /dev/null @@ -1,1212 +0,0 @@ -import Callout from '@/components/Callout' - -# Component API Reference - - -Our TypeScript component API is in **beta**. If you're interested in developing TypeScript components and providing feedback, [see our TypeScript docs](/components/typescript/). - - -This document was created to help developers author and use [Pipedream components](/components/). Not only can you develop [sources](/components/quickstart/nodejs/sources/) (workflow triggers) and [actions](/components/quickstart/nodejs/actions/) using the component API, but you can also develop [Node.js steps](/code/nodejs/) right in your workflows - without leaving your browser! You can publish components to your account for private use, or [contribute them to the Pipedream registry](/apps/contributing/) for anyone to run. - -While sources and actions share the same core component API, they differ in both how they're used and written, so certain parts of the component API apply only to one or the other. [This section of the docs](#differences-between-sources-and-actions) explains the core differences. When this document uses the term "component", the corresponding feature applies to both sources and actions. If a specific feature applies to only sources _or_ actions, the correct term will be used. - -If you have any questions about component development, please reach out [in our community](https://pipedream.com/community/c/dev/11). - -## Overview - -### What is a component? - -Components are Node.js modules that run on Pipedream's serverless infrastructure. - -- Trigger Node.js code on HTTP requests, timers, cron schedules, or manually -- Emit data on each event to inspect it. Trigger Pipedream hosted workflows or access it outside of Pipedream via API -- Accept user input on deploy via [CLI](/cli/reference/#pd-deploy), [API](/api/rest/#overview), or [UI](https://pipedream.com/sources) -- Connect to [{process.env.PUBLIC_APPS}+ apps](https://pipedream.com/apps) using Pipedream managed auth -- Use most npm packages with no `npm install` or `package.json` required -- Store and retrieve state using the [built-in key-value store](#db) - -### Quickstarts - -To help you get started, we created a step-by-step walkthrough for developing both [sources](/components/quickstart/nodejs/sources/) and [actions](/components/quickstart/nodejs/actions/). We recommend starting with those docs and using the API reference below as you develop. - -### Differences between sources and actions - -Sources and actions share the same component API. However, certain features of the API only apply to one or the other: - -- Actions are defined with `type: action` ([see the docs on the `type` property](#component-structure)). Sources require no `type` property be set. Components without a `type` are considered sources. - -- Sources emit events [using `this.$emit`](#emit), which trigger linked workflows. Any features associated with emitting events (e.g., [dedupe strategies](#dedupe-strategies)) can only be used with sources. Actions [return data using `return` or `$.export`](#returning-data-from-steps), which is made available to future steps of the associated workflow. - -- Sources have access to [lifecycle hooks](#lifecycle-hooks), which are often required to configure the source to listen for new events. Actions do not have access to these lifecycle hooks. - -- Actions have access to [a special `$` variable](#actions), passed as a parameter to the `run` method. This variable exposes functions that allow you to send data to destinations, export data from the action, return HTTP responses, and more. - -- Sources can be developed iteratively using `pd dev`. Actions currently cannot (please follow [this issue](https://github.com/PipedreamHQ/pipedream/issues/1437) to be notified of updates). - -- You use `pd deploy` to deploy sources to your account. You use `pd publish` to publish actions, making them available for use in workflows. - -- You can attach [interfaces](#interface-props) (like HTTP endpoints, or timers) to sources. This defines how the source is invoked. Actions do not have interfaces, since they're run step-by-step as a part of the associated workflow. - -### Getting Started with the CLI - -Several examples below use the Pipedream CLI. To install it, [follow the instructions for your OS / architecture](/cli/install/). - -See the [CLI reference](/cli/reference/) for detailed usage and examples beyond those covered below. - -### Example Components - -You can find hundreds of example components in the `components/` directory of the [`PipedreamHQ/pipedream` repo](https://github.com/PipedreamHQ/pipedream). - -## Component API - -### Component Structure - -Pipedream components export an object with the following properties: - -```javascript -export default { - name: "", - key: "", - type: "", - version: "", - description: "", - props: {}, - methods: {}, - hooks: { - async activate() {}, - async deactivate() {}, - async deploy() {}, - }, - dedupe: "", - async run(event) { - this.$emit(event); - }, -}; -``` - -| Property | Type | Required? | Description | -| ------------- | -------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `name` | `string` | required | The name of the component, a string which identifies components deployed to users' accounts. This name will show up in the Pipedream UI, in CLI output (for example, from `pd list` commands), etc. It will also be converted to a unique slug on deploy to reference a specific component instance (it will be auto-incremented if not unique within a user account). | -| `key` | `string` | recommended | The `key` uniquely identifies a component within a namespace. The default namespace for components is your account.

When publishing components to the Pipedream registry, the `key` must be unique across registry components and should follow the pattern:

`app_name_slug`-`slugified-component-name` | -| `type` | `string` | required | When publishing an action, `type: "action"` is required. When publishing a source, use `type: "source"`. | -| `version` | `string` | required | The component version. There are no constraints on the version, but [semantic versioning](https://semver.org/) is required for any components published to the [Pipedream registry](/components/guidelines/). | -| `description` | `string` | recommended | The description will appear in the Pipedream UI to aid in discovery and to contextualize instantiated components | -| `props` | `object` | optional | [Props](#props) are custom attributes you can register on a component. When a value is passed to a prop attribute, it becomes a property on that component instance. You can reference these properties in component code using `this` (e.g., `this.propName`). | -| `methods` | `object` | optional | Define component methods for the component instance. They can be referenced via `this` (e.g., `this.methodName()`). | -| `hooks` | `object` | optional (sources only) | [Hooks](#hooks) are functions that are executed when specific component lifecycle events occur. | -| `dedupe` | `string` | optional (sources only) | You may specify a [dedupe strategy](#dedupe-strategies) to be applied to emitted events | -| `run` | `method` | required | Each time a component is invoked (for example, via HTTP request), [its `run` method](#run) is called. The event that triggered the component is passed to `run`, so that you can access it within the method. Events are emitted using `this.$emit()`. | - -### Props - -Props are custom attributes you can register on a component. When a value is passed to a prop attribute, it becomes a property on that component instance. You can reference these properties in component code using `this` (e.g., `this.propName`). - -| Prop Type | Description | -| ------------------------------- | --------------------------------------------------------------------------------------------- | -| [User Input](#user-input-props) | Enable components to accept input on deploy | -| [Interface](#interface-props) | Attaches a Pipedream interface to your component (e.g., an HTTP interface or timer) | -| [Service](#service-props) | Attaches a Pipedream service to your component (e.g., a key-value database to maintain state) | -| [App](#app-props) | Enables managed auth for a component | -| [Data Store](/data-stores/#using-data-stores-in-code-steps) | Provides access to a Pipedream [data store](/data-stores/) | -| [HTTP Request](#http-request-prop)| Enables components to execute HTTP requests based on user input | -| [Alert](#alert-prop)| Renders an informational alert in the prop form to help users configure the source or action | - -#### User Input Props - -User input props allow components to accept input on deploy. When deploying a component, users will be prompted to enter values for these props, setting the behavior of the component accordingly. - -##### General - -**Definition** - -```javascript -props: { - myPropName: { - type: "", - label: "", - description: "", - options: [], // OR async options() {} to return dynamic options - optional: true || false, - propDefinition: [], - default: "", - secret: true || false, - min: , - max: , - disabled: true || false, - hidden: true || false - }, -}, -``` - -| Property | Type | Required? | Description | -| ---------------- | ------------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `type` | `string` | required | Value must be set to a valid `PropType` (see below). Suffix with `[]` (e.g. `string[]`) to denote array of that type (if supported). | -| `label` | `string` | optional | A friendly label to show to user for this prop. If a label is not provided, the `propName` is displayed to the user. | -| `description` | `string` | optional | Displayed near the prop input. Typically used to contextualize the prop or provide instructions to help users input the correct value. Markdown is supported. | -| `options` | `string[]` or `object[]` or `method` | optional | Provide an array to display options to a user in a drop down menu.
 
**`[]` Basic usage**
Array of strings. E.g.,
`['option 1', 'option 2']`
 
**`object[]` Define Label and Value**
`[{ label: 'Label 1', value: 'label1'}, { label: 'Label 2', value: 'label2'}]`
 
**`method` Dynamic Options**
You can generate options dynamically (e.g., based on real-time API requests with pagination). See configuration details below. | -| `useQuery` | `boolean` | optional | Use in conjunction with **Dynamic Options**. If set to `true`, the prop accepts a real-time query that can be used by the `options` method to obtain results according to that query. | -| `optional` | `boolean` | optional | Set to `true` to make this prop optional. Defaults to `false`. | -| `propDefinition` | `[]` | optional | Re-use a prop defined in an app file. When you include a prop definition, the prop will inherit values for all the properties listed here. However, you can override those values by redefining them for a given prop instance. See **propDefinitions** below for usage. | -| `default` | `string` | optional | Define a default value if the field is not completed. Can only be defined for optional fields (required fields require explicit user input). | -| `secret` | `boolean` | optional | If set to `true`, this field will hide your input in the browser like a password field, and its value will be encrypted in Pipedream's database. The value will be decrypted when the component is run in [the execution environment](/privacy-and-security/#execution-environment). Defaults to `false`. Only allowed for `string` props. | -| `min` | `integer` | optional | Minimum allowed integer value. Only allowed for `integer` props.. | -| `max` | `integer` | optional | Maximum allowed integer value . Only allowed for `integer` props. | -| `disabled` | `boolean` | optional | Set to `true` to disable usage of this prop. Defaults to `false`. | -| `hidden` | `boolean` | optional | Set to `true` to hide this field. Defaults to `false`. | - -**Prop Types** - -| Prop Type | Array Supported | Supported in Sources? | Supported in Actions? | Custom properties | -| ------------------- | --------------- | --------------------- | --------------------- | :---------------------------------------------------------------------------------------------------------- | -| `app` | | ✓ | ✓ | See [App Props](#app-props) below | -| `boolean` | ✓ | ✓ | ✓ | -| `integer` | ✓ | ✓ | ✓ | - `min` (`integer`): Minimum allowed integer value.
- `max` (`integer`): Maximum allowed integer value. | -| `string` | ✓ | ✓ | ✓ | - `secret` (`boolean`): Whether to treat the value as a secret. | -| `object` | | ✓ | ✓ | -| `any` | | | ✓ | -| `$.interface.http` | | ✓ | | -| `$.interface.timer` | | ✓ | | -| `$.service.db` | | ✓ | | -| `data_store` | | | ✓ | -| `http_request` | | | ✓ | -| `alert` | | ✓ | ✓ | See [Alert Prop](#alert-prop) below - -**Usage** - -| Code | Description | Read Scope | Write Scope | -| ----------------- | ---------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------- | -| `this.myPropName` | Returns the configured value of the prop | `run()` `hooks` `methods` | n/a (input props may only be modified on component deploy or update via UI, CLI or API) | - -**Example** - -Following is an example source that demonstrates how to capture user input via a prop and emit it on each event: - -```javascript -export default { - name: "User Input Prop Example", - version: "0.1", - props: { - msg: { - type: "string", - label: "Message", - description: "Enter a message to `console.log()`", - }, - }, - async run() { - this.$emit(this.msg); - }, -}; -``` - -To see more examples, explore the [curated components in Pipedream's GitHub repo](#example-components). - -##### Advanced Configuration - -##### Async Options ([example](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/github.app.mjs)) - -Async options allow users to select prop values that can be programmatically-generated (e.g., based on a real-time API response). - -```javascript -async options({ - page, - prevContext, - query, -}) {}, -``` - -| Property | Type | Required? | Description | -| ------------- | --------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `options()` | `method` | optional | Typically returns an array of values matching the prop type (e.g., `string`) or an array of object that define the `label` and `value` for each option. The `page` and `prevContext` input parameter names are reserved for pagination (see below).
 
When using `prevContext` for pagination, it must return an object with an `options` array and a `context` object with a `nextPageToken` key. E.g., `{ options, context: { nextPageToken }, }` | -| `page` | `integer` | optional | Returns a `0` indexed page number. Use with APIs that accept a numeric page number for pagination. | -| `prevContext` | `string` | optional | Returns a string representing the context for the previous `options` execution. Use with APIs that accept a token representing the last record for pagination. | -| `query` | `string` | optional | Returns a string with the user input if the prop has the `useQuery` property set to `true`. Use with APIs that return items based on a query or search parameter. | - -Following is an example source demonstrating the usage of async options: - -```javascript -export default { - name: "Async Options Example", - version: "0.1", - props: { - msg: { - type: "string", - label: "Message", - description: "Select a message to `console.log()`", - async options() { - // write any node code that returns a string[] or object[] (with label/value keys) - return ["This is option 1", "This is option 2"]; - }, - }, - }, - async run() { - this.$emit(this.msg); - }, -}; -``` - -##### Prop Definitions ([example](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-commit/new-commit.mjs)) - -Prop definitions enable you to reuse props that are defined in another object. A common use case is to enable re-use of props that are defined for a specific app. - -```javascript -props: { - myPropName: { - propDefinition: [ - app, - "propDefinitionName", - inputValues - ] - }, -}, - -``` - -| Property | Type | Required? | Description | -| -------------------- | -------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `propDefinition` | `array` | optional | An array of options that define a reference to a `propDefinitions` within the `propDefinitions` for an `app` | -| `app` | `object` | required | An app object | -| `propDefinitionName` | `string` | required | The name of a specific `propDefinition` defined in the corresponding `app` object | -| `inputValues` | `object` | optional | Values to pass into the prop definition. To reference values from previous props, use an arrow function. E.g.,:
 
`c => ({ variableName: c.previousPropName })`

[See these docs](#referencing-values-from-previous-props) for more information. | - -Following is an example source that demonstrates how to use `propDefinitions`. - -```javascript -const rss = { - type: "app", - app: "rss", - propDefinitions: { - urlDef: { - type: "string", - label: "RSS URL", - description: "Enter a URL for an RSS feed.", - }, - }, -}; - -export default { - name: "Prop Definition Example", - description: `This component captures an RSS URL and logs it`, - version: "0.1", - props: { - rss, - url: { propDefinition: [rss, "urlDef"] }, - }, - async run() { - console.log(this.url); - }, -}; -``` - -##### Referencing values from previous props - -When you define a prop in an app file, and that prop depends on the value of another prop, you'll need to pass the value of the previous props in a special way. Let's review an example from [Trello](https://trello.com), a task manager. - -You create Trello _boards_ for new projects. Boards contain _lists_. For example, this **Active** board contains two lists: - -![Trello board example](./images/trello-board-example.png) - -In Pipedream, users can choose from lists on a specific board: - -![Trello board and lists props](./images/trello-props.png) - -Both **Board** and **Lists** are defined in the Trello app file: - -```javascript -board: { - type: "string", - label: "Board", - async options(opts) { - const boards = await this.getBoards(this.$auth.oauth_uid); - const activeBoards = boards.filter((board) => board.closed === false); - return activeBoards.map((board) => { - return { label: board.name, value: board.id }; - }); - }, -}, -lists: { - type: "string[]", - label: "Lists", - optional: true, - async options(opts) { - const lists = await this.getLists(opts.board); - return lists.map((list) => { - return { label: list.name, value: list.id }; - }); - }, -} -``` - -In the `lists` prop, notice how `opts.board` references the board. You can pass `opts` to the prop's `options` method when you reference `propDefinitions` in specific components: - -```javascript -board: { propDefinition: [trello, "board"] }, -lists: { - propDefinition: [ - trello, - "lists", - (configuredProps) => ({ board: configuredProps.board }), - ], -}, -``` - -`configuredProps` contains the props the user previously configured (the board). This allows the `lists` prop to use it in the `options` method. - -##### Dynamic props - -Some prop definitions must be computed dynamically, after the user configures another prop. We call these **dynamic props**, since they are rendered on-the-fly. This technique is used in [the Google Sheets **Add Single Row** action](https://github.com/PipedreamHQ/pipedream/blob/master/components/google_sheets/actions/add-single-row/add-single-row.mjs), which we'll use as an example below. - -First, determine the prop whose selection should render dynamic props. In the Google Sheets example, we ask the user whether their sheet contains a header row. If it does, we display header fields as individual props: - -![Google Sheets Additional props example - header columns loading as props](https://res.cloudinary.com/pipedreamin/image/upload/v1654129371/docs/additional-props_lx5jtv.gif) - -To load dynamic props, the header prop must have the `reloadProps` field set to `true`: - -```javascript -hasHeaders: { - type: "string", - label: "Does the first row of the sheet have headers?", - description: "If the first row of your document has headers we'll retrieve them to make it easy to enter the value for each column.", - options: [ - "Yes", - "No", - ], - reloadProps: true, -}, -``` - -When a user chooses a value for this prop, Pipedream runs the `additionalProps` component method to render props: - -```javascript -async additionalProps() { - const sheetId = this.sheetId?.value || this.sheetId; - const props = {}; - if (this.hasHeaders === "Yes") { - const { values } = await this.googleSheets.getSpreadsheetValues(sheetId, `${this.sheetName}!1:1`); - if (!values[0]?.length) { - throw new ConfigurationError("Cound not find a header row. Please either add headers and click \"Refresh fields\" or adjust the action configuration to continue."); - } - for (let i = 0; i < values[0]?.length; i++) { - props[`col_${i.toString().padStart(4, "0")}`] = { - type: "string", - label: values[0][i], - optional: true, - }; - } - } else if (this.hasHeaders === "No") { - props.myColumnData = { - type: "string[]", - label: "Values", - description: "Provide a value for each cell of the row. Google Sheets accepts strings, numbers and boolean values for each cell. To set a cell to an empty value, pass an empty string.", - }; - } - return props; -}, -``` - -The signature of this function is: - -```javascript -async additionalProps(previousPropDefs) -``` - -where `previousPropDefs` are the full set of props (props merged with the previous `additionalProps`). When the function is executed, `this` is bound similar to when the `run` function is called, where you can access the values of the props as currently configured, and call any `methods`. The return value of `additionalProps` will replace any previous call, and that return value will be merged with props to define the final set of props. - -Following is an example that demonstrates how to use `additionalProps` to dynamically change a prop's `disabled` and `hidden` properties: - -```javascript -async additionalProps(previousPropDefs) { - if (this.myCondition === "Yes") { - previousPropDefs.myPropName.disabled = true; - previousPropDefs.myPropName.hidden = true; - } else { - previousPropDefs.myPropName.disabled = false; - previousPropDefs.myPropName.hidden = false; - } - return previousPropDefs; -}, -``` - -Dynamic props can have any one of the following prop types: - -- `app` -- `boolean` -- `integer` -- `string` -- `object` -- `any` -- `$.interface.http` -- `$.interface.timer` -- `data_store` -- `http_request` - -#### Interface Props - -Interface props are infrastructure abstractions provided by the Pipedream platform. They declare how a source is invoked — via HTTP request, run on a schedule, etc. — and therefore define the shape of the events it processes. - -| Interface Type | Description | -| --------------- | --------------------------------------------------------------- | -| [Timer](#timer) | Invoke your source on an interval or based on a cron expression | -| [HTTP](#http) | Invoke your source on HTTP requests | - -#### Timer - -To use the timer interface, declare a prop whose value is the string `$.interface.timer`: - -**Definition** - -```javascript -props: { - myPropName: { - type: "$.interface.timer", - default: {}, - }, -} -``` - -| Property | Type | Required? | Description | -| --------- | -------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -| `type` | `string` | required | Must be set to `$.interface.timer` | -| `default` | `object` | optional | **Define a default interval**
`{ intervalSeconds: 60, },`
 
**Define a default cron expression**
`{ cron: "0 0 * * *", },` | - -**Usage** - -| Code | Description | Read Scope | Write Scope | -| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------- | ------------------------------------------------------------------------------------------- | -| `this.myPropName` | Returns the type of interface configured (e.g., `{ type: '$.interface.timer' }`) | `run()` `hooks` `methods` | n/a (interface props may only be modified on component deploy or update via UI, CLI or API) | -| `event` | Returns an object with the execution timestamp and interface configuration (e.g., `{ "timestamp": 1593937896, "interval_seconds": 3600 }`) | `run(event)` | n/a (interface props may only be modified on source deploy or update via UI, CLI or API) | - -**Example** - -Following is a basic example of a source that is triggered by a `$.interface.timer` and has default defined as a cron expression. - -```javascript -export default { - name: "Cron Example", - version: "0.1", - props: { - timer: { - type: "$.interface.timer", - default: { - cron: "0 0 * * *", // Run job once a day - }, - }, - }, - async run() { - console.log("hello world!"); - }, -}; -``` - -Following is an example source that's triggered by a `$.interface.timer` and has a `default` interval defined. - -```javascript -export default { - name: "Interval Example", - version: "0.1", - props: { - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: 60 * 60 * 24, // Run job once a day - }, - }, - }, - async run() { - console.log("hello world!"); - }, -}; -``` - -##### HTTP - -To use the HTTP interface, declare a prop whose value is the string `$.interface.http`: - -```javascript -props: { - myPropName: { - type: "$.interface.http", - customResponse: true, // optional: defaults to false - }, -} -``` - -**Definition** - -| Property | Type | Required? | Description | -| --------- | -------- | --------- | ------------------------------------------------------------------------------------------------------------ | -| `type` | `string` | required | Must be set to `$.interface.http` | -| `respond` | `method` | required | The HTTP interface exposes a `respond()` method that lets your component issue HTTP responses to the client. | - -**Usage** - -| Code | Description | Read Scope | Write Scope | -| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `this.myPropName` | Returns an object with the unique endpoint URL generated by Pipedream (e.g., `{ endpoint: 'https://abcde.m.pipedream.net' }`) | `run()` `hooks` `methods` | n/a (interface props may only be modified on source deploy or update via UI, CLI or API) | -| `event` | Returns an object representing the HTTP request (e.g., `{ method: 'POST', path: '/', query: {}, headers: {}, bodyRaw: '', body: {}, }`) | `run(event)` | The shape of `event` corresponds with the the HTTP request you make to the endpoint generated by Pipedream for this interface | -| `this.myPropName.respond()` | Returns an HTTP response to the client (e.g., `this.http.respond({status: 200})`). | n/a | `run()` | - -###### Responding to HTTP requests - -The HTTP interface exposes a `respond()` method that lets your source issue HTTP responses. You may run `this.http.respond()` to respond to the client from the `run()` method of a source. In this case you should also pass the `customResponse: true` parameter to the prop. - -| Property | Type | Required? | Description | -| --------- | -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ | -| `status` | `integer` | required | An integer representing the HTTP status code. Return `200` to indicate success. Standard status codes range from `100` - `599` | -| `headers` | `object` | optional | Return custom key-value pairs in the HTTP response | -| `body` | `string` `object` `buffer` | optional | Return a custom body in the HTTP response. This can be any string, object, or Buffer. | - -###### HTTP Event Shape - -Following is the shape of the event passed to the `run()` method of your source: - -```javascript -{ - method: 'POST', - path: '/', - query: {}, - headers: {}, - bodyRaw: '', - body: -} -``` - -**Example** - -Following is an example source that's triggered by `$.interface.http` and returns `{ 'msg': 'hello world!' }` in the HTTP response. On deploy, Pipedream will generate a unique URL for this source: - -```javascript -export default { - name: "HTTP Example", - version: "0.0.1", - props: { - http: { - type: "$.interface.http", - customResponse: true, - }, - }, - async run(event) { - this.http.respond({ - status: 200, - body: { - msg: "hello world!", - }, - headers: { - "content-type": "application/json", - }, - }); - console.log(event); - }, -}; -``` - -#### Service Props - -| Service | Description | -| ------- | ---------------------------------------------------------------------------------------------------- | -| _DB_ | Provides access to a simple, component-specific key-value store to maintain state across executions. | - -##### DB - -**Definition** - -```javascript -props: { - myPropName: "$.service.db", -} -``` - -**Usage** - -| Code | Description | Read Scope | Write Scope | -| ----------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------- | -------------------------------------- | -| `this.myPropName.get('key')` | Method to get a previously set value for a key. Returns `undefined` if a key does not exist. | `run()` `hooks` `methods` | Use the `set()` method to write values | -| `this.myPropName.set('key', value)` | Method to set a value for a key. Values must be JSON-serializable data. | Use the `get()` method to read values | `run()` `hooks` `methods` | - -#### App Props - -App props are normally defined in an [app file](/components/guidelines/#app-files), separate from individual components. See [the `components/` directory of the pipedream GitHub repo](https://github.com/PipedreamHQ/pipedream/tree/master/components) for example app files. - -**Definition** - -```javascript -props: { - myPropName: { - type: "app", - app: "", - propDefinitions: {} - methods: {}, - }, -}, -``` - -| Property | Type | Required? | Description | -| ----------------- | -------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `type` | `string` | required | Value must be `app` | -| `app` | `string` | required | Value must be set to the name slug for an app registered on Pipedream. [App files](/components/guidelines/#app-files) are programmatically generated for all integrated apps on Pipedream. To find your app's slug, visit the `components` directory of [the Pipedream GitHub repo](https://github.com/PipedreamHQ/pipedream/tree/master/components), find the app file (the file that ends with `.app.mjs`), and find the `app` property at the root of that module. If you don't see an app listed, please [open an issue here](https://github.com/PipedreamHQ/pipedream/issues/new?assignees=&labels=app%2C+enhancement&template=app---service-integration.md&title=%5BAPP%5D). | -| `propDefinitions` | `object` | optional | An object that contains objects with predefined user input props. See the section on User Input Props above to learn about the shapes that can be defined and how to reference in components using the `propDefinition` property | -| `methods` | `object` | optional | Define app-specific methods. Methods can be referenced within the app object context via `this` (e.g., `this.methodName()`) and within a component via `this.myAppPropName` (e.g., `this.myAppPropName.methodName()`). | - -**Usage** - -| Code | Description | Read Scope | Write Scope | -| --------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------- | ----------- | -| `this.$auth` | Provides access to OAuth tokens and API keys for Pipedream managed auth | **App Object:** `methods` | n/a | -| `this.myAppPropName.$auth` | Provides access to OAuth tokens and API keys for Pipedream managed auth | **Parent Component:** `run()` `hooks` `methods` | n/a | -| `this.methodName()` | Execute a common method defined for an app within the app definition (e.g., from another method) | **App Object:** `methods` | n/a | -| `this.myAppPropName.methodName()` | Execute a common method defined for an app from a component that includes the app as a prop | **Parent Component:** `run()` `hooks` `methods` | n/a | - -> **Note:** The specific `$auth` keys supported for each app will be published in the near future. - -#### HTTP Request Prop - -**Usage** - -| Code | Description | Read Scope | Write Scope | -| --------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------- | ----------- | -| `this.myPropName.execute()` | Execute an HTTP request as configured | n/a | `run()` `methods` | - -**Example** - -Following is an example action that demonstrates how to accept an HTTP request configuration as input and execute the request when the component is run: - -```javascript -export default { - name: "HTTP Request Example", - version: "0.0.1", - props: { - httpRequest: { - type: "http_request", - label: "API Request", - default: { - method: "GET", - url: "https://jsonplaceholder.typicode.com/posts", - } - }, - }, - async run() { - const { data } = await this.httpRequest.execute(); - return data; - }, -}; -``` - -For more examples, see the [docs on making HTTP requests with Node.js](/code/nodejs/http-requests/#send-a-get-request-to-fetch-data). - - -#### Alert Prop - -Sometimes you may need to surface contextual information to users within the prop form. This might be information that's not directly related to a specific prop, so it doesn't make sense to include in a prop description, but rather, it may be related to the overall configuration of the prop form. - -**Usage** - -| Property | Type | Required? | Description | -| - | - | - | - | -| `type` | `string` | required | Set to `alert` | -| `alertType` | `string` | required | Determines the color and UI presentation of the alert prop. Can be one of `info`, `neutral`, `warning`, `error`. | -| `content` | `string` | required | Determines the text that is rendered in the alert. Both plain text and markdown are supported. | - -```javascript -export default defineComponent({ - props: { - alert: { - type: "alert", - alertType: "info", - content: "Admin rights on the repo are required in order to register webhooks. In order to continue setting up your source, configure a polling interval below to check for new events.", - } - }, -}) -``` - -Refer to GitHub's component sources in the `pipedream` repo for an [example implementation](https://github.com/PipedreamHQ/pipedream/blob/b447d71f658d10d6a7432e8f5153bbda56ba9810/components/github/sources/common/common-flex.mjs#L27). - -![Info alert prop in GitHub source](./images/info-alert-prop-github.png) - -#### Limits on props - -When a user configures a prop with a value, it can hold at most {process.env.CONFIGURED_PROPS_SIZE_LIMIT} data. Consider this when accepting large input in these fields (such as a base64 string). - -The {process.env.CONFIGURED_PROPS_SIZE_LIMIT} limit applies only to static values entered as raw text. In workflows, users can pass expressions (referencing data in a prior step). In that case the prop value is simply the text of the expression, for example `{{steps.nodejs.$return_value}}`, well below the limit. The value of these expressions is evaluated at runtime, and are subject to [different limits](/limits/). - -### Methods - -You can define helper functions within the `methods` property of your component. You have access to these functions within the [`run` method](#run), or within other methods. - -Methods can be accessed using `this.`. For example, a `random` method: - -```javascript -methods: { - random() { - return Math.random() - }, -} -``` - -can be run like so: - -```javascript -const randomNum = this.random(); -``` - -### Hooks - -```javascript -hooks: { - async deploy() {}, - async activate() {}, - async deactivate() {}, -}, -``` - -| Property | Type | Required? | Description | -| ------------ | -------- | --------- | ----------------------------------------------------- | -| `deploy` | `method` | optional | Executed each time a component is deployed | -| `activate` | `method` | optional | Executed each time a component is deployed or updated | -| `deactivate` | `method` | optional | Executed each time a component is deactivated | - -### Dedupe Strategies - -> **IMPORTANT:** To use a dedupe strategy, you must emit an `id` as part of the event metadata (dedupe strategies are applied to the submitted `id`) - -| Strategy | Description | -| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `unique` | Pipedream maintains a cache of 100 emitted `id` values. Events with `id` values that are not in the cache are emitted, and the `id` value is added to the cache. After 100 events, `id` values are purged from the cache based on the order received (first in, first out). A common use case for this strategy is an RSS feed which typically does not exceed 100 items | -| `greatest` | Pipedream caches the largest `id` value (must be numeric). Only events with larger `id` values are emitted, and the cache is updated to match the new, largest value.. | -| `last` | Pipedream caches the ID associated with the last emitted event. When new events are emitted, only events after the matching `id` value will be emitted as events. If no `id` values match, then all events will be emitted. | - -### Run - -Each time a component is invoked, its `run` method is called. Sources are invoked by their [interface](#interface-props) (for example, via HTTP request). Actions are run when their parent workflow is triggered. - -You can reference `this` within the `run` method. `this` refers to the component, and provides access to [props](#props), [methods](#methods), and more. - -#### Sources - -When a source is invoked, the event that triggered the source is passed to `run`, so that you can access it within the method: - -```javascript -async run(event) { - console.log(event) -} -``` - -##### \$emit - -`this.$emit()` is a method in scope for the `run` method of a source - -```javascript -this.$emit(event, { - id, - name, - summary, - ts, -}); -``` - -| Property | Type | Required? | Description | -| --------- | ---------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `event` | JSON serializable data | optional | The data to emit as the event | -| `id` | `string` or `number` | Required if a dedupe strategy is applied | A value to uniquely identify this event. Common `id` values may be a 3rd party ID, a timestamp, or a data hash | -| `name` | `string` | optional | The name of the "channel" you'd like to emit the event to. By default, events are emitted to the `default` channel. If you set a different channel here, listening sources or workflows can subscribe to events on this channel, running the source or workflow only on events emitted to that channel. | -| `summary` | `string` | optional | Define a summary to customize the data displayed in the events list to help differentiate events at a glance | -| `ts` | `integer` | optional | Accepts an epoch timestamp in **milliseconds**. If you submit a timestamp, events will automatically be ordered and emitted from oldest to newest. If using the `last` dedupe strategy, the value cached as the `last` event for an execution will correspond to the event with the newest timestamp. | - -Following is a basic example that emits an event on each component execution. - -```javascript -export default { - name: "this.$emit() example", - description: "Deploy and run this component manually via the Pipedream UI", - async run() { - this.$emit({ message: "hello world!" }); - }, -}; -``` - -##### Logs - -You can view logs produced by a source's `run` method in the **Logs** section of the [Pipedream source UI](https://pipedream.com/sources), or using the `pd logs` CLI command: - -```bash -pd logs -``` - -##### Events - -If the `run` method emits events using `this.$emit`, you can access the events in the **EVENTS** section of the Pipedream UI for the component, or using the `pd events` CLI command: - -```bash -pd events -``` - -#### Actions - -When an action is run in a workflow, Pipedream passes an object with a `$` variable that gives you access to special functions, outlined below: - -```javascript -async run({ $ }) { - // You have access to $ within your action -} -``` - -##### Returning data from steps - -By default, variables declared within an action are scoped to that action. To return data from a step, you have two options: 1) use the `return` keyword, or 2) use `$.export` to return a named export from a step. - -**`return`** - -Use `return` to return data from an action: - -```javascript -async run({ $ }) { - return "data" -} -``` - -When you use return, the exported data will appear at `steps.[STEP NAME].$return_value`. For example, if you ran the code above in a step named `nodejs`, you'd reference the returned data using `steps.nodejs.$return_value`. - -**`$.export`** - -You can also use `$.export` to return named exports from an action. `$.export` takes the name of the export as the first argument, and the value to export as the second argument: - -```javascript -async run({ $ }) { - $.export("name", "value") -} -``` - -When your workflow runs, you'll see the named exports appear below your step, with the data you exported. You can reference these exports in other steps using `steps.[STEP NAME].[EXPORT NAME]`. - -##### Returning HTTP responses with `$.respond` - -`$.respond` lets you issue HTTP responses from your workflow. [See the full `$.respond` docs for more information](/workflows/steps/triggers/#customizing-the-http-response). - -```javascript -async run({ $ }) { - $.respond({ - status: 200, - body: "hello, world" - }) -} -``` - -##### Ending steps early with `return $.flow.exit` - -`return $.flow.exit` terminates the entire workflow. It accepts a single argument: a string that tells the workflow why the workflow terminated, which is displayed in the Pipedream UI. - -```javascript -async run({ $ }) { - return $.flow.exit("reason") -} -``` - -##### `$.summary` - -`$.summary` is used to surface brief, user-friendly summaries about what happened when an action step succeeds. For example, when [adding items to a Spotify playlist](https://github.com/PipedreamHQ/pipedream/blob/master/components/spotify/actions/add-items-to-playlist/add-items-to-playlist.mjs#L51): - -![Spotify example with $summary](./images/spotify-$summary-example.png) - -Example implementation: - -```javascript -const data = [1, 2]; -const playlistName = "Cool jams"; -$.export( - "$summary", - `Successfully added ${data.length} ${ - data.length == 1 ? "item" : "items" - } to "${playlistName}"` -); -``` - -##### `$.send` - -`$.send` allows you to send data to [Pipedream destinations](/destinations/). - -**`$.send.http`** - -[See the HTTP destination docs](/destinations/http/#using-send-http-in-component-actions). - -**`$.send.email`** - -[See the Email destination docs](/destinations/email/#using-send-email-in-component-actions). - -**`$.send.s3`** - -[See the S3 destination docs](/destinations/s3/#using-send-s3-in-component-actions). - -**`$.send.emit`** - -[See the Emit destination docs](/destinations/emit/#using-send-emit-in-component-actions). - -**`$.send.sse`** - -[See the SSE destination docs](/destinations/sse/#using-send-sse-in-component-actions). - -##### `$.context` - -`$.context` exposes [the same properties as `steps.trigger.context`](/workflows/events/#steps-trigger-context), and more. Action authors can use it to get context about the calling workflow and the execution. - -All properties from [`steps.trigger.context`](/workflows/events/#steps-trigger-context) are exposed, as well as: - -| Property | Description | -| ---------- | :-----------------------------------------------------------------------------------------------------------------------------------------------------: | -| `deadline` | An epoch millisecond timestamp marking the point when the workflow is configured to [timeout](/limits/#time-per-execution). | -| `JIT` | Stands for "just in time" (environment). `true` if the user is testing the step, `false` if the step is running in production. | -| `run` | An object containing metadata about the current run number. See [the docs on `$.flow.rerun`](/workflows/events/#steps-trigger-context) for more detail. | - -### Environment variables - -[Environment variables](/environment-variables/) are not accessible within sources or actions directly. Since components can be used by anyone, you cannot guarantee that a user will have a specific variable set in their environment. - -In sources, you can use [`secret` props](#props) to reference sensitive data. - -In actions, you'll see a list of your environment variables in the object explorer when selecting a variable to pass to a step: - -### Using npm packages - -To use an npm package in a component, just require it. There is no `package.json` or `npm install` required. - -```javascript -import axios from "axios"; -``` - -When you deploy a component, Pipedream downloads the latest versions of these packages and bundles them with your deployment. - -Some packages that rely on large dependencies or on unbundled binaries — may not work on Pipedream. Please [reach out](https://pipedream.com/support) if you encounter a specific issue. - -#### Referencing a specific version of a package - -_This currently applies only to sources_. - -If you'd like to use a _specific_ version of a package in a source, you can add that version in the `require` string, for example: `require("axios@0.19.2")`. Moreover, you can pass the same version specifiers that npm and other tools allow to specify allowed [semantic version](https://semver.org/) upgrades. For example: - -- To allow for future patch version upgrades, use `require("axios@~0.20.0")` -- To allow for patch and minor version upgrades, use `require("axios@^0.20.0")` - -## Managing Components - -Sources and actions are developed and deployed in different ways, given the different functions they serve in the product. - -- [Managing Sources](#managing-sources) -- [Managing Actions](#managing-actions) - -### Managing Sources - -#### CLI - Development Mode - ---- - -The easiest way to develop and test sources is with the `pd dev` command. `pd dev` deploys a local file, attaches it to a component, and automatically updates the component on each local save. To deploy a new component with `pd dev`, run: - -```bash -pd dev -``` - -To attach to an existing deployed component, run: - -```bash -pd dev --dc -``` - -#### CLI - Deploy - -##### From Local Code - -To deploy a source via CLI, use the `pd deploy` command. - -```bash -pd deploy -``` - -E.g., - -```bash -pd deploy my-source.js -``` - -##### From Pipedream Github Repo - -You can explore the components available to deploy in [Pipedream's GitHub repo](https://github.com/pipedreamhq/pipedream/tree/master/components). - -```bash -pd deploy -``` - -E.g., - -```bash -pd deploy http-new-requests -``` - -##### From Any URL - -```bash -pd deploy -``` - -E.g., - -```bash -pd deploy https://raw.githubusercontent.com/PipedreamHQ/pipedream/master/components/http/sources/new-requests/new-requests.js -``` - -#### CLI - Update - -View the [CLI command reference](/cli/reference/#command-reference). - -#### CLI - Delete - -View the [CLI command reference](/cli/reference/#command-reference). - -#### UI - Deploy - -You can find and deploy curated components at [https://pipedream.com/sources/new](https://pipedream.com/sources/new), or you can deploy code via the UI using following URL patterns. - -##### From Pipedream Github Repo - -```bash -https://pipedream.com/sources?action=create&key= -``` - -E.g., - -```bash -https://pipedream.com/sources?action=create&key=http-new-requests -``` - -##### From Any URL - -```bash -https://pipedream.com/sources?action=create&url= -``` - -E.g., - -```bash -https://pipedream.com/sources?action=create&url=https%3A%2F%2Fraw.githubusercontent.com%2FPipedreamHQ%2Fpipedream%2Fmaster%2Fcomponents%2Fhttp%2Fhttp.js -``` - -#### UI - Update - -You can update the code and props for a component from the **Configuration** tab for a source in the Pipedream UI. - -#### UI - Delete - -You can delete a component via the UI at [https://pipedream.com/sources](https://pipedream.com/sources). - -#### API - -See the [REST API docs](/api/rest/). - -### Managing Actions - -#### CLI - Publish - -To publish an action, use the `pd publish` command. - -```bash -pd publish FILENAME -``` - -E.g., - -```bash -pd publish my-action.js -``` - -## Source Lifecycle - -### Lifecycle hooks - -Pipedream sources support the following hooks. The code for these hooks are defined within the component. Learn more about the [component structure](#component-structure) and [hook usage](#hooks). - -#### `deploy` - -The `deploy()` hook is automatically invoked by Pipedream when a source is deployed. A common use case for the deploy hook is to create webhook subscriptions when the source is deployed, but you can run any Node.js code within the `deploy` hook. To learn more about the `deploy()` hook, refer to the [API documentation](#hooks). - -#### `activate` - -The `activate()` hook is automatically invoked by Pipedream when a source is deployed or updated. For example, this hook will be run when users update component props, so you can run code here that handles those changes. To learn more about defining a custom `activate()` hook, refer to the [API documentation](#hooks). - -#### `deactivate` - -The `deactivate()` hook is automatically invoked by Pipedream when a source is updated or deleted. A common use case for the deactivate hook is to automatically delete a webhook subscription when a component is deleted, but you can run any Node.js code within the `deactivate` hook. To learn more about the `deactivate()` hook, refer to the [API documentation](#hooks). - -### States - -#### Saved Component - -A saved component is non-instantiated component code that has previously been deployed to Pipedream. Each saved component has a unique saved component ID. Saved components cannot be invoked directly — they must first be deployed. - -#### Deployed Component - -A deployed component is an instance of a saved component that can be invoked. Deployed components can be active or inactive. On deploy, Pipedream instantiates a saved component and invokes the `activate()` hook. - -#### Deleted Component - -On delete, Pipedream invokes the `deactivate()` hook and then deletes the deployed component instance. - -### Operations - -#### Deploy - -On deploy, Pipedream creates an instance of a saved component and invokes the optional `deploy()` and `activate()` hooks. A unique deployed component ID is generated for the component. - -You can deploy a component via the CLI, UI or API. - -#### Update - -On update, Pipedream, invokes the optional `deactivate()` hook, updates the code and props for a deployed component, and then invokes the optional `activate()` hook. The deployed component ID is not changed by an update operation. - -#### Delete - -On delete, Pipedream invokes the optional `deactivate()` hook and deletes the component instance. - -## Source Event Lifecycle - -The event lifecycle applies to deployed sources. Learn about the [source lifecycle](#source-lifecycle). - -### Diagram - -![Pipedream Components Event Lifecycle Diagram](https://res.cloudinary.com/pipedreamin/image/upload/v1683089643/d0iiggokfkwnmt4kckb5.png) - -### Triggering Sources - -Sources are triggered when you manually run them (e.g., via the **RUN NOW** button in the UI) or when one of their [interfaces](#interface-props) is triggered. Pipedream sources currently support **HTTP** and **Timer** interfaces. - -When a source is triggered, the `run()` method of the component is executed. Standard output and errors are surfaced in the **Logs** tab. - -### Emitting Events from Sources - -Sources can emit events via `this.$emit()`. If you define a [dedupe strategy](#dedupe-strategies) for a source, Pipedream automatically dedupes the events you emit. - -> **TIP:** if you want to use a dedupe strategy, be sure to pass an `id` for each event. Pipedream uses this value for deduping purposes. - -### Consuming Events from Sources - -Pipedream makes it easy to consume events via: - -- The UI -- Workflows -- APIs -- CLI - -#### UI - -When you navigate to your source [in the UI](https://pipedream.com/sources), you'll be able to select and inspect the most recent 100 events (i.e., an event bin). For example, if you send requests to a simple HTTP source, you will be able to inspect the events (i.e., a request bin). - -#### Workflows - -[Trigger hosted Node.js workflows](/workflows/) on each event. Integrate with {process.env.PUBLIC_APPS}+ apps including Google Sheets, Discord, Slack, AWS, and more! - -#### API - -Events can be retrieved using the [REST API](/api/rest/) or [SSE stream tied to your component](/api/sse/). This makes it easy to retrieve data processed by your component from another app. Typically, you'll want to use the [REST API](/api/rest/) to retrieve events in batch, and connect to the [SSE stream](/api/sse/) to process them in real time. - -#### CLI - -Use the `pd events` command to retrieve the last 10 events via the CLI: - -```bash -pd events -n 10 -``` diff --git a/docs-v2/pages/components/guidelines.mdx b/docs-v2/pages/components/guidelines.mdx deleted file mode 100644 index 8cbeaa0019c2b..0000000000000 --- a/docs-v2/pages/components/guidelines.mdx +++ /dev/null @@ -1,677 +0,0 @@ -import Callout from '@/components/Callout' - -# Components Guidelines & Patterns - -For a component to be accepted into the Pipedream registry, it should follow these guidelines below. These guidelines help ensure components are high quality, are intutive for both Pipedream users and component developers to use and extend. - - -Questions about best practices? - -Join the discussion with fellow Pipedream component developers at the [#contribute channel](https://pipedream-users.slack.com/archives/C01E5KCTR16) in Slack or [on Discourse](https://pipedream.com/community/c/dev/11). - - -## Local Checks - -When submitting pull requests, the new code will run through a series of -automated checks like linting the code. If you want to run those checks locally -for quicker feedback you must have [pnpm](https://pnpm.io/) installed and -run the following commands at the root of the project: - -1. To install all the project's dependencies (only needed once): - -```shell -pnpm install -``` - -2. To install all required dependencies: - -```shell -npx pnpm install -r -``` - -3. To run the linter checks against your code (assuming that your changes are located at `components/foo` for example): - -```shell -npx eslint components/foo -``` - -4. Optionally, you can automatically fix any linter issues by running the following command: - -```shell -npx eslint --fix components/foo -``` - -Keep in mind that not all issues can be automatically fixed by the linter -since they could alter the behaviour of the code. - - -## General - -### Components should be ES modules - -The Node.js community has started publishing [ESM-only](https://flaviocopes.com/es-modules/) packages that do not work with [CommonJS modules](https://nodejs.org/docs/latest/api/modules.html#modules_modules_commonjs_modules). This means you must `import` the package. You can't use `require`. - -You also cannot mix ESM with CJS. This will **not** work: - -```javascript -// ESM -import axios from "axios"; - -// CommonJS - this should be `export default` -module.exports = { - ... -} -``` - -Therefore, all components should be written as ES modules: - -```javascript -import axios from "axios"; - -export default { - ... -} -``` - -**You'll need to use [the `.mjs` file extension](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#aside_%E2%80%94_.mjs_versus_.js) for any components written as ES modules**. - -You'll notice that many of the existing components are written as CommonJS modules. Please fix these and submit a pull request as you refactor related code. For example, if you're developing new Spotify actions, and you notice the existing event sources use CommonJS, change them to ESM: - -1. Rename the file extension from `.js` to `.mjs` using `git mv` (e.g. `git mv source.js source.mjs`). -2. Change all `require` statements to `import`s. -3. Change instances of `module.exports` to `export default`. - -### Component Scope - -Create components to address specific use cases whenever possible. For example, -when a user subscribes to a Github webhook to listen for “star” activity, events -can be generated when users star or unstar a repository. The “New Star” source -filters events for only new star activity so the user doesn't have to. - -There may be cases where it's valuable to create a generic component that -provides users with broad latitude (e.g., see the [custom -webhook](https://github.com/pipedreamhq/pipedream/blob/master/components/github/sources/custom-webhook-events) -event source for GitHub). However, as a general heuristic, we found that tightly -scoped components are easier for users to understand and use. - -### Required Metadata - -Registry [components](/components/api/#component-structure) require a unique `key` and -`version`, and a friendly `name` and `description`. Action components require a -`type` field to be set to `action` (sources will require a type to be set in the -future). - -```javascript -export default { - key: "google_drive-new-shared-drive", - name: "New Shared Drive", - description: "Emits a new event any time a shared drive is created.", - version: "0.0.1", -}; -``` - -### Component Key Pattern - -When publishing components to the Pipedream registry, the `key` must be unique -across registry components and should follow the pattern: - -`app_name_slug`-`slugified-component-name` - -**Source** keys should use past tense verbs that describe the event that occurred (e.g., `linear_app-issue-created-instant`). For **action** keys, use active verbs to describe the action that will occur, (e.g., `linear_app-create-issue`). - -### Versioning - -When you first publish a component to the registry, set its version to `0.0.1`. - -Pipedream registry components try to follow [semantic versioning](https://semver.org/). From their site: - -Given a version number `MAJOR.MINOR.PATCH`, increment the: - -1. `MAJOR` version when you make incompatible API changes, -2. `MINOR` version when you add functionality in a backwards compatible manner, and -3. `PATCH` version when you make backwards compatible bug fixes. - -When you're developing actions locally, and you've incremented the version in your account multiple times, make sure to set it to the version it should be at in the registry prior to submitting your PR. For example, when you add an action to the registry, the version should be `0.0.1`. If the action was at version `0.1.0` and you've fixed a bug, change it to `0.1.1` when committing your final code. - -### Folder Structure - -Registry components are organized by app in the `components` directory of the -`pipedreamhq/pipedream` repo. - -```text -/components - /[app-name-slug] - /[app-name-slug].app.js - /actions - /[action-name-slug] - /[action-name-slug].js - /sources - /[source-name-slug] - /[source-name-slug].js -``` - -- The name of each app folder corresponds with the name slug for each app -- The app file should be in the root of the app folder (e.g., - `/components/[app_slug]/[app_slug].app.js`) -- Components for each app are organized into `/sources` and `/actions` - subfolders -- Each component should be placed in its own subfolder (with the name of the - folder and the name of the `js` file equivalent to the slugified component - name). For example, the path for the "Search Mentions" source for Twitter is - `/components/twitter/sources/search-mentions/search-mentions.js`. -- Aside from `app_slug`, words in folder and file names are separated by dashes - (-) (i.e., in kebab case) - -You can explore examples in the [components -directory](https://github.com/pipedreamhq/pipedream/tree/master/components). - -#### Using APIs vs Client Libraries - -If the app has a well-supported [Node.js client -library](/components/api/#using-npm-packages), feel free to use that instead of manually -constructing API requests. - -### `package.json` - -Each app should have a `package.json` in its root folder. If one doesn't exist, run `npm init` in the app's root folder and customize the file using [this `package.json`](https://github.com/PipedreamHQ/pipedream/blob/55236b3aa993cbcb545e245803d8654c6358b0a2/components/stripe/package.json) as a template. - -Each time you change the code for an app file, or change the dependencies for any app component, modify the package `version`. - -Save any dependencies in the component app directory: - -```bash -npm i --save package -npm i --save-dev package -``` - -#### Error-handling and input validation - -When you use the SDK of a popular API, the SDK might raise clear errors to the user. For example, if the user is asked to pass an email address, and that email address doesn't validate, the library might raise that in the error message. - -But other libraries will _not_ raise clear errors. In these cases, you may need to `throw` your own custom error that wraps the error from the API / lib. [See the Airtable components](https://github.com/PipedreamHQ/pipedream/blob/9e4e400cda62335dfabfae384d9224e04a585beb/components/airtable/airtable.app.js#L70) for an example of custom error-handling and input validation. - -In general, **imagine you are a user troubleshooting an issue. Is the error easy-to-understand? If not, `throw` a better error**. - -### `README` files - -New actions and sources should include `README.md` files within the same directory to describe how to use the action or source to users. - -Here's an example `README.md` structure: - -```markdown - -# Overview - -# Getting Started - -# Troubleshooting - -``` - -These sections will appear within the correponding app, source and action page, along with any subheadings and content. - -Here's an example of an [app `README.md` within the `discord` component on the Pipedream registry](https://github.com/PipedreamHQ/pipedream/blob/master/components/discord/README.md). That same content is rendered within the [Pipedream integration page for the Discord app](https://pipedream.com/apps/discord). - -You can add additional subheadings to each of the top level `Overview`, `Getting Started` and `Troubleshooting` headings: - -```markdown -# Overview - -## Limitations - -Perhaps there are some limitations about the API that users should know about. - -# Getting Started - -## Generating an API key - -Instructions on how to generate an API key from within the service's dashboard. - -# Troubleshooting - -## Required OAuth scopes - -Please take note, you'll need to have sufficient privileges in order to complete authentication. - -``` - - -Only these three top level headings `Overview`, `Getting Starting` and `Troubleshooting` will appear within the corresponding App Marketplace page. All other headings will be ignored. - - -#### Pagination - -When making API requests, handle pagination to ensure all data/events are -processed. Moreover, if the underlying account experiences and/or generates too -much data paginating through the entire collection of records, it might cause -out-of-memory or timeout issues (or both!), so as a rule of thumb the pagination -logic should: - -- Be encapsulated as a [generator](https://mzl.la/37z6Sh6) so that the component - can start processing records after the very first API call. As an example, you - can check the [Microsoft OneDrive - methods](https://github.com/PipedreamHQ/pipedream/tree/master/components/microsoft_onedrive/microsoft_onedrive.app.mjs) - to list files. -- Accept a "next token/page/ID" whenever possible, so that API calls do not - retrieve the entire collection of records during every execution but rather - from a recent point in time. The `scanDeltaItems` generator method in the - example above follows this pattern. -- Persist the last page number, token or record ID right after processing, so - that following executions of the component process new records to minimize the - amount of duplicate events, execution time and delayed events. Following the - same Microsoft OneDrive example, check the `processEvent` method [in this - component](https://github.com/PipedreamHQ/pipedream/tree/master/components/microsoft_onedrive/sources/new-file/new-file.mjs) - for an example. - - -#### Capturing Sensitive Data - -If users are required to enter sensitive data, always use -[secret](/components/api/#general) props. - -### Promoting Reusability - -#### App Files - -App files contain components that declare the app and include prop definitions -and methods that may be reused across components. App files should adhere to the -following naming convention: `[app_name_slug].app.js`. If an app file does not -exist for your app, please [reach -out](https://pipedream.com/community/c/dev/11). - -##### Prop Definitions - -Whenever possible, reuse existing [prop definitions](/components/api/#prop-definitions-example). - -If a prop definition does not exist and you are adding an app-specific prop that -may be reused in future components, add it as a prop definition to the app file. -Prop definitions will also be surfaced for apps the Pipedream marketplace. - -##### Methods - -Whenever possible, reuse -[methods](/components/api/#methods) -defined in the app file. If you need to use an API for which a method is not -defined and it may be used in future components, define a new method in the app -file. - -Use the [JS Docs](https://jsdoc.app/about-getting-started.html) pattern for -lightweight documentation of each method in the app file. Provide a description -and define @params and @returns block tags (with default values if applicable — -e.g., `[foo=bar]`). This data will both help with reusability and will be -surfaced in documentation for apps in the Pipedream marketplace. For example: - -```javascript -export default { - methods: { - /** - * Get the most recently liked Tweets for a user - * - * @params {Object} opts - An object representing the configuration options - * for this method - * @params {String} opts.screenName - The user's Twitter screen name (e.g., - * `pipedream`) - * @params {String} [opts.count=200] - The maximum number of Tweets to - * return - * @params {String} [opts.tweetMode=extended] - Use the default of - * `extended` to return non-truncated Tweets - * @returns {Array} Array of most recent Tweets liked by the specified user - */ - async getLikedTweets(opts = {}) { - const { screenName, count = 200, tweetMode = "extended" } = opts; - const { data } = await this._makeRequest({ - url: "https://api.twitter.com/1.1/favorites/list.json", - params: { - screen_name: screenName, - count, - tweet_mode: tweetMode, - }, - }); - return data; - }, - }, -}; -``` - -#### Testing - -Pipedream does not currently support unit tests to validate that changes to app -files are backwards compatible with existing components. Therefore, if you make -changes to an app file that may impact other sources, you must currently test -potentially impacted components to confirm their functionality is not negatively -affected. We expect to support a testing framework in the future. - -### Common Files (optional) - -An optional pattern to improve reusability is to use a `common` module to -abstract elements that are used across to multiple components. The trade-off -with this approach is that it increases complexity for end-users who have the -option of customizing the code for components within Pipedream. When using this -approach, the general pattern is: - -- The `.app.js` module contains the logic related to making the actual API calls - (e.g. calling `axios.get`, encapsulate the API URL and token, etc). -- The `common.js` module contains logic and structure that is not specific to - any single component. Its structure is equivalent to a component, except that - it doesn't define attributes such as `version`, `dedupe`, `key`, `name`, etc - (those are specific to each component). It defines the main logic/flow and - relies on calling its methods (which might not be implemented by this - component) to get any necessary data that it needs. In OOP terms, it would be - the equivalent of a base abstract class. -- The component module of each action would inherit/extend the `common.js` - component by setting additional attributes (e.g. `name`, `description`, `key`, - etc) and potentially redefining any inherited methods. - -See [Google -Drive](https://github.com/PipedreamHQ/pipedream/tree/master/components/google_drive) for an -example of this pattern. When using this approach, prop definitions should still -be maintained in the app file. - -Please note that the name `common` is just a convention and depending on each -case it might make sense to name any common module differently. For example, the -[AWS -sources](https://github.com/PipedreamHQ/pipedream/tree/master/components/aws) -contains a `common` directory instead of a `common.js` file, and the directory -contains several modules that are shared between different event sources. - -## Props - -As a general rule of thumb, we should strive to only incorporate the 3-4 most relevant options from a given API as props. This is not a hard limit, but the goal is to optimize for usability. We should aim to solve specific use cases as simply as possible. - -### Labels - -Use [prop](/components/api/#user-input-props) labels to customize the name of a prop or -propDefinition (independent of the variable name in the code). The label should -mirror the name users of an app are familiar with; i.e., it should mirror the -equivalent label in the app's UI. This applies to usage in labels, descriptions, -etc. E.g., the Twitter API property for search keywords is “q”, but but label is -set to “Search Term”. - -### Descriptions - -Include a description for [props](/components/api/#user-input-props) if it helps the -user understand what they need to do. Use Markdown as appropriate -to improve the clarity of the description or instructions. When using Markdown: - -- Enclose sample input values in backticks (`` ` ``) -- Refer to other props using **bold** by surrounding with double asterisks (\*) -- Use Markdown links with descriptive text rather than displaying a full URL. -- If the description isn't self-explanatory, link to the API docs of the relevant method to further clarify how the prop works. When the value of the prop is complex (for example, an object with many properties), link to the section of the API docs that include details on this format. Users may pass values from previous steps using expressions, so they'll need to know how to structure the input data. - -Examples: - -- The async option to select an Airtable Base is self-explanatory so includes no - description: - - ![image-20210326151557417](https://res.cloudinary.com/pipedreamin/image/upload/v1672810770/ixb3aozdijmz0zfqxmvy.png) - -- The “Search Term” prop for Twitter includes a description that helps the user - understand what values they can enter, with specific values highlighted using - backticks and links to external content. - - ![image-20210326151706682](./images/image-20210326151706682.png) - -### Optional vs Required Props - -Use optional [props](/components/api/#user-input-props) whenever possible to minimize the -input fields required to use a component. - -For example, the Twitter search mentions source only requires that a user -connect their account and enter a search term. The remaining fields are optional -for users who want to filter the results, but they do not require any action to -activate the source: - -![image-20210326151930885](./images/image-20210326151930885.png) - -### Default Values - -Provide [default values](/components/api/#user-input-props) whenever possible. NOTE: the -best default for a source doesn’t always map to the default recommended by the -app. For example, Twitter defaults search results to an algorithm that balances -recency and popularity. However, the best default for the use case on Pipedream -is recency. - -### Async Options - -Avoid asking users to enter ID values. Use [async -options](/components/api/#async-options-example) (with label/value definitions) so users -can make selections from a drop down menu. For example, Todoist identifies -projects by numeric IDs (e.g., 12345). The async option to select a project -displays the name of the project as the label, so that’s the value the user sees -when interacting with the source (e.g., “My Project”). The code referencing the -selection receives the numeric ID (12345). - -Async options should also support [pagination](/components/api/#async-options-example) -(so users can navigate across multiple pages of options for long lists). See -[Hubspot](https://github.com/PipedreamHQ/pipedream/blob/a9b45d8be3b84504dc22bb2748d925f0d5c1541f/components/hubspot/hubspot.app.mjs#L136) -for an example of offset-based pagination. See -[Twitter](https://github.com/PipedreamHQ/pipedream/blob/d240752028e2a17f7cca1a512b40725566ea97bd/components/twitter/twitter.app.mjs#L200) -for an example of cursor-based pagination. - -### Dynamic Props - -[Dynamic props](/components/api/#dynamic-props) can improve the user experience for components. They let you render props in the Pipedream dynamically, based on the value of other props, and can be used to collect more specific information that can make it easier to use the component. See the Google Sheets example in the linked component API docs. - -### Interface & Service Props - -In the interest of consistency, use the following naming patterns when defining -[interface](/components/api/#interface-props) and -[service](/components/api/#service-props) props in source components: - -| Prop | **Recommended Prop Variable Name** | -| ------------------- | ---------------------------------- | -| `$.interface.http` | `http` | -| `$.interface.timer` | `timer` | -| `$.service.db` | `db` | - -Use getters and setters when dealing with `$.service.db` to avoid potential -typos and leverage encapsulation (e.g., see the [Search -Mentions](https://github.com/PipedreamHQ/pipedream/blob/master/components/twitter/sources/search-mentions/search-mentions.mjs#L83-L88) -event source for Twitter). - -## Source Guidelines - -These guidelines are specific to [source](/sources/) development. - -### Webhook vs Polling Sources - -Create subscription webhooks sources (vs polling sources) whenever possible. -Webhook sources receive/emit events in real-time and typically use less compute -time from the user’s account. Note: In some cases, it may be appropriate to -support webhook and polling sources for the same event. For example, Calendly -supports subscription webhooks for their premium users, but non-premium users -are limited to the REST API. A webhook source can be created to emit new -Calendly events for premium users, and a polling source can be created to -support similar functionality for non-premium users. - -### Source Name - -Source name should be a singular, title-cased name and should start with "New" -(unless emits are not limited to new items). Name should not be slugified and -should not include the app name. NOTE: Pipedream does not currently distinguish -real-time event sources for end-users automatically. The current pattern to -identify a real-time event source is to include “(Instant)” in the source name. -E.g., “New Search Mention” or “New Submission (Instant)”. - -### Source Description - -Enter a short description that provides more detail than the name alone. -Typically starts with "Emit new". E.g., “Emit new Tweets that matches your -search criteria”. - -### Emit a Summary - -Always [emit a summary](/components/api/#emit) for each event. For example, the summary -for each new Tweet emitted by the Search Mentions source is the content of the -Tweet itself. - -If no sensible summary can be identified, submit the event payload in string -format as the summary. - -### Deduping - -Use built-in [deduping strategies](/components/api/#dedupe-strategies) whenever possible -(`unique`, `greatest`, `last`) vs developing custom deduping code. Develop -custom deduping code if the existing strategies do not support the requirements -for a source. - -### Surfacing Test Events - -In order to provide users with source events that they can immediately reference when building their workflow, we should implement 2 strategies whenever possible: - -#### Emit Events on First Run: -- Polling sources should always emit events on the first run (see the [Spotify: New Playlist](https://github.com/PipedreamHQ/pipedream/blob/master/components/spotify/sources/new-playlist/new-playlist.mjs) source as an example) -- Webhook-based sources should attempt to fetch existing events in the `deploy()` hook during source creation (see the [Jotform: New Submission](https://github.com/PipedreamHQ/pipedream/blob/master/components/jotform/sources/new-submission/new-submission.mjs) source) - -_Note – make sure to emit the most recent events (considering pagination), and limit the count to no more than 50 events._ - -#### Include a static sample event: -There are times where there may not be any historical events available (think about sources that emit less frequently, like "New Customer" or "New Order", etc). In these cases, we should include a static sample event so users can see the event shape and reference it while building their workflow, even if it's using fake data. - -To do this, -1. Copy the JSON output from the source's emit (what you get from `steps.trigger.event`) and **make sure to remove or scrub any sensitive or personal data** (you can also copy this from the app's API docs) -2. Add a new file called `test-event.mjs` in the same directory as the component source and export the JSON event via `export default` ([example](https://github.com/PipedreamHQ/pipedream/blob/master/components/jotform/sources/new-submission/test-event.mjs)) -3. In the source component code, make sure to import that file as `sampleEmit` ([example](https://github.com/PipedreamHQ/pipedream/blob/master/components/jotform/sources/new-submission/new-submission.mjs#L2)) -4. And finally, export the `sampleEmit` object ([example](https://github.com/PipedreamHQ/pipedream/blob/master/components/jotform/sources/new-submission/new-submission.mjs#L96)) - -This will render a "Generate Test Event" button in the UI for users to emit that sample event: - -![generate-sample-event](https://res.cloudinary.com/pipedreamin/image/upload/v1690488844/generate-test-event_drjykm.gif) - -### Polling Sources - -#### Default Timer Interval - -As a general heuristic, set the default timer interval to 15 minutes. However, -you may set a custom interval (greater or less than 15 minutes) if appropriate -for the specific source. Users may also override the default value at any time. - -For polling sources in the Pipedream registry, the default polling interval is set as a global config. Individual sources can access that default within the props definition: - -``` javascript -import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; - -export default { - props: { - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - }, - // rest of component -} -``` - -#### Rate Limit Optimization - -When building a polling source, cache the most recently processed ID or -timestamp using `$.service.db` whenever the API accepts a `since_id` or "since -timestamp" (or equivalent). Some apps (e.g., Github) do not count requests that -do not return new results against a user’s API quota. - -If the service has a well-supported Node.js client library, it'll often build in -retries for issues like rate limits, so using the client lib (when available) -should be preferred. In the absence of that, -[Bottleneck](https://www.npmjs.com/package/bottleneck) can be useful for -managing rate limits. 429s should be handled with exponential backoff (instead -of just letting the error bubble up). - -### Webhook Sources - -#### Hooks - -[Hooks](/components/api/#hooks) are methods that are automatically invoked by Pipedream -at different stages of the [component lifecycle](/components/api/#source-lifecycle). -Webhook subscriptions are typically created when components are instantiated or -activated via the `activate()` hook, and deleted when components are deactivated -or deleted via the `deactivate()` hook. - -#### Helper Methods - -Whenever possible, create methods in the app file to manage [creating and -deleting webhook subscriptions](/components/api/#hooks). - -| **Description** | **Method Name** | -| --------------------------------------- | --------------- | -| Method to create a webhook subscription | `createHook()` | -| Method to delete a webhook subscription | `deleteHook()` | - -#### Storing the 3rd Party Webhook ID - -After subscribing to a webhook, save the ID for the hook returned by the 3rd -party service to the `$.service.db` for a source using the key `hookId`. This ID -will be referenced when managing or deleting the webhook. Note: some apps may -not return a unique ID for the registered webhook (e.g., Jotform). - -#### Signature Validation - -Subscription webhook components should always validate the incoming event -signature if the source app supports it. - -#### Shared Secrets - -If the source app supports shared secrets, implement support transparent to the -end user. Generate and use a GUID for the shared secret value, save it to a -`$.service.db` key, and use the saved value to validate incoming events. - -## Action Guidelines - -### Action Name - -Like [source name](#source-name), action name should be a singular, title-cased -name, should not be slugified, and should not include the app name. - -As a general pattern, articles are not included in the action name. For example, -instead of "Create a Post", use "Create Post". - -#### Use `@pipedream/platform` axios for all HTTP requests - -By default, the standard `axios` package doesn't return useful debugging data to the user when it `throw`s errors on HTTP 4XX and 5XX status codes. This makes it hard for the user to troubleshoot the issue. - -Instead, [use `@pipedream/platform` axios](/pipedream-axios/). - -#### Return JavaScript objects - -When you `return` data from an action, it's exposed as a [step export](/workflows/steps/#step-exports) for users to reference in future steps of their workflow. Return JavaScript objects in all cases, unless there's a specific reason not to. - -For example, some APIs return XML responses. If you return XML from the step, it's harder for users to parse and reference in future steps. Convert the XML to a JavaScript object, and return that, instead. - -### "List" actions - -#### Return an array of objects - -To simplify using results from "list"/"search" actions in future steps of a -workflow, return an array of the items being listed rather than an object with a -nested array. [See this example for -Airtable](https://github.com/PipedreamHQ/pipedream/blob/cb4b830d93e1495d8622b0c7dbd80cd3664e4eb3/components/airtable/actions/common-list.js#L48-L63). - -#### Handle pagination - -For actions that return a list of items, the common use case is to retrieve all -items. Handle pagination within the action to remove the complexity of needing -to paginate from users. We may revisit this in the future and expose the -pagination / offset params directly to the user. - -In some cases, it may be appropriate to limit the number of API requests made or -records returned in an action. For example, some Twitter actions optionally -limit the number of API requests that are made per execution (using a -[`maxRequests` -prop](https://github.com/PipedreamHQ/pipedream/blob/cb4b830d93e1495d8622b0c7dbd80cd3664e4eb3/components/twitter/twitter.app.mjs#L52)) -to avoid exceeding Twitter's rate limits. [See the Airtable -components](https://github.com/PipedreamHQ/pipedream/blob/e2bb7b7bea2fdf5869f18e84644f5dc61d9c22f0/components/airtable/airtable.app.js#L14) -for an example of using a `maxRecords` prop to optionally limit the maximum -number of records to return. - -### Use `$.summary` to summarize what happened - -[Describe what happened](/components/api/#returning-data-from-steps) when an action succeeds by following these guidelines: - -- Use plain language and provide helpful and contextually relevant information (especially the count of items) -- Whenever possible, use names and titles instead of IDs -- Basic structure: _Successfully [action performed (like added, removed, updated)] “[relevant destination]”_ - -### Don't export data you know will be large - -Browsers can crash when users load large exports (many MBs of data). When you know the content being returned is likely to be large – e.g. files — don't export the full content. Consider writing the data to the `/tmp` directory and exporting a reference to the file. diff --git a/docs-v2/pages/components/index.mdx b/docs-v2/pages/components/index.mdx deleted file mode 100644 index c6bacc9dd0a5b..0000000000000 --- a/docs-v2/pages/components/index.mdx +++ /dev/null @@ -1,136 +0,0 @@ -import Callout from '@/components/Callout' -import VideoPlayer from '@/components/VideoPlayer' - -# Overview - -## What are Components? - -Components are [Node.js modules](api/#component-structure) that run on Pipedream's serverless infrastructure. They can use Pipedream managed auth for [{process.env.PUBLIC_APPS}+ apps](https://pipedream.com/explore) (for both OAuth and key-based APIs) and [use most npm packages](api/#using-npm-packages) with no `npm install` or `package.json` required. - -Components are most commonly used as the building blocks of Pipedream workflows, but they can also be used like typical serverless functions. You can explore curated components for popular apps in Pipedream's [Marketplace](https://pipedream.com/explore) and [GitHub repo](https://github.com/pipedreamhq/pipedream/tree/master/components) or you can author and share your own. - - -Our TypeScript component API is in **beta**. If you're interested in developing TypeScript components and providing feedback, [see our TypeScript docs](/components/typescript/). - - -## Component Types - -Pipedream supports two types of components — [sources](#sources) and [actions](#actions). - -### Sources - -[Sources](/sources/) must be instantiated and they run as independent resources on Pipedream. They are commonly used as workflow triggers (but can also be used as standalone serverless functions). - -**Capabilities** - -- Accept user input on deploy via `props` -- [Trigger](api/#interface-props) on HTTP requests, timers, cron schedules, or manually -- Emit events that can be inspected, trigger Pipedream [workflows](/workflows/) and that can be consumed in your own app via [API](/api/) -- Store and retrieve state using the [built-in key-value store](api/#db) -- Use any of Pipedream's built-in [deduping strategies](api/#dedupe-strategies) -- Deploy via Pipedream's UI, CLI or API - -**Example** - -The [New Files (Instant)](https://github.com/PipedreamHQ/pipedream/blob/master/components/google_drive/sources/new-files-instant/new-files-instant.mjs) source for Google Drive is a prebuilt component in Pipedream's registry that can be deployed in seconds and emits an event every time a new file is added to the user's Google Drive, and can also be configured to watch for changes to a specific folder within that drive. Each new event that is emitted can be used to trigger a workflow. - -### Actions - -Actions are components that may be used as steps in a workflow. Unlike sources, actions cannot run independently (outside of a workflow). - -**Capabilities** - -- Accept user input via `props` -- May `return` JSON serializable data - -**Example** - -The Add Single Row action for Google Sheets is a prebuilt component in Pipedream's registry that can be added to a workflow and configured in seconds. Users can configure it in seconds and send workflow data to Google Sheets without having to learn the Google Sheets API. - -## Using Components - -Components may be instantiated or added to workflows via Pipedream's UI. - -- Sources may be instantiated and consumed via [UI](https://pipedream.com/sources/new), [CLI](/cli/reference/#pd-deploy) or API -- Actions may be added to [workflows](https://pipedream.com/new) - -### Using Private Actions - -Private action components published from the [CLI](/cli/reference/#pd-publish) or from a [Node.js Code Step](/code/nodejs/sharing-code) are available for use across your workflows. - -To use a published action, add a new step to your workflow and click **My Actions**. Your privately published action components will appear in this list. - -![Use the "My Actions" section in a new step to include your private actions](https://res.cloudinary.com/pipedreamin/image/upload/v1618550730/docs/components/image-20210411165325045_ia5sd5.png) - -### Using Private Sources - -Private source components deployed from your account via the [CLI](/cli/reference/#pd-deploy) will automatically create a new Source in your account with the prop configuration you specified. - -You can also deploy new instances of a source from the [Components dashboard](https://res.cloudinary.com/pipedreamin/image/upload/v1618550730/docs/components/image-20210411165325045_ia5sd5.png). To deploy a new instance of a source, click the menu on the right hand side and select **Create source**. - -![Creating a source from the Component dashboard](https://res.cloudinary.com/pipedreamin/image/upload/v1666106571/docs/CleanShot_2022-10-18_at_11.22.02_ajjopm.gif) - -## Developing Components - -Develop components locally using your preferred code editor (and maintain your code in your own GitHub repo) and deploy or publish using Pipedream's [CLI](/cli/reference/#pd-deploy). - -- Sources may be deployed directly from local code or published to your account and instantiated via Pipedream's UI -- Actions may only be published — published actions may be added to workflows via Pipedream's UI - -Published components are only available to your own account by default. If published to a team account, the component (source or action) may be discovered and selected by any member of the team. - -### Prerequisites - -- A free [Pipedream](https://pipedream.com) account -- A free [GitHub](https://github.com) account -- Basic proficiency with Node.js or Javascript -- Pipedream [CLI](/cli/reference/) - -Finally, the target app must be integrated with Pipedream. You can explore all apps supported by Pipedream in the [marketplace](https://pipedream.com/explore). If your app is not listed, please [create a GitHub issue](https://github.com/PipedreamHQ/pipedream/issues/new?assignees=&labels=app%2C+enhancement&template=app---service-integration.md&title=%5BAPP%5D) to request it and [reach out](https://pipedream.com/community/c/dev/11) to our team to let us know that you're blocked on source or action development. - -### Quickstart Guides - -- [Sources](quickstart/nodejs/sources/) -- [Actions](quickstart/nodejs/actions/) - -### Component API Reference - -After getting familiar with source/action development using the quickstart guides, check out [the Component API Reference](/components/api/) and [examples on GitHub](https://github.com/pipedreamhq/pipedream/tree/master/components) to learn more. - -## Managing Privately Published Components - -Components published to your workspace are available in the [Components](https://pipedream.com/components) section of the dashboard. - -Your private components published from the CLI or from Node.js code steps are listed here. - -### Unpublishing Privately Published Components - - - -To unpublish components belonging to your workspace, open the menu on the right hand side of the component details and select **Unpublish Component**. - -A prompt will open to confirm the action, click **Confirm** to unpublish your action. - -![Unpublish a component from your account by opening the menu on the right hand side](https://res.cloudinary.com/pipedreamin/image/upload/v1666103082/docs/components/CleanShot_2022-10-18_at_10.22.45_vdhoq7.gif) - - -Unpublishing a component is a permanent action, please be careful to ensure you still have access to the source code. - - -## Sharing Components - -Contribute to the Pipedream community by publishing and sharing new components, and contributing to the maintenance of existing components. - -### Verified Components - -Pipedream maintains a source-available registry of components (sources and actions) that have been curated for the community. Registered components are verified by Pipedream through the [GitHub PR process](/apps/contributing/#contribution-process) and: - -- Can be trusted by end users -- Follow consistent patterns for usability -- Are supported by Pipedream if issues arise - -Registered components also appear in the Pipedream marketplace and are listed in Pipedream's UI when building workflows. - -### Community Components - -Developers may create, deploy and share components that do not conform to these guidelines, but they will not be eligible to be listed in the curated registry (e.g., they may be hosted in a Github repo). If you develop a component that does not adhere to these guidelines, but you believe there is value to the broader community, please [reach out in our community forum](https://pipedream.com/community/c/dev/11). diff --git a/docs-v2/pages/components/migrating/index.mdx b/docs-v2/pages/components/migrating/index.mdx deleted file mode 100644 index bbdcbc1594ce1..0000000000000 --- a/docs-v2/pages/components/migrating/index.mdx +++ /dev/null @@ -1,183 +0,0 @@ -# Migrating from Legacy Actions to Component Actions - - - -## Overview -This document is for developers who created legacy actions in [Pipedream's UI](https://pipedream.com/actions). The purpose is to help users migrate legacy actions to Pipedream's new [component model](/components/). - -## Key Changes - -**Capture user input via `props` instead of `params`** - -The component model does not support `params`. You need to migrate `params` references to [`props`](/components/api/#props). Unlike `params`, `props` must be explicitly declared and defined prior to using them in code (in the old model, an input form was automatically generated when `params` were used in code — `params` were not explicitly declared). - -**Declare app `props` to use managed auth** - -The model for linking an app to legacy actions as well as the syntax for referencing credentials is different with Pipedream components. In the old model, apps were linked to steps in Pipedream's workflow builder UI, and credentials were referenced via the `auths` object. - -When using the component model, apps are defined as `props` and credentials are referenced as properties of the app. For example, to use managed auth for GitHub, the component `props` must contain a key (`gh` in the example below) with an app definition for the value (the app definition is an object): - -```javascript -gh: { - type: "app", - app: "github" -} -``` - -The component's `run()` method can then reference the credentials for GitHub via `this.gh.$auth.oauth_access_token`. - -**Develop locally and host on GitHub** -Actions are no longer developed in Pipedream's UI. Develop actions locally using your preferred editor, publish to Pipedream via CLI and maintain the code in your own GitHub repo. - -**Update with a click** -When you publish a new version of an action, you can update actions used in workflows with a click (updating legacy actions in workflows requires action steps to be deleted, re-added and re-configured). - -**Support for async options** -Async options allow action authors to render a paginated drop down menu allowing users to select from values that are programmatically-generated. The most common use case is to populate the drop down based on results of an API request (e.g., to list Google Sheets in a user's drive). - -**Simplified discovery** -Actions you publish are now grouped under **My Actions** when adding a step to a workflow. NOTE: this option will appear in the workflow builder *after* you publish your first action. - -## Getting Started - -Ready to develop your first component action? We recommend starting with our [quickstart guide](/components/quickstart/nodejs/actions/). Then review both our [component API reference](/components/api/) and [actions published to Pipedream’s GitHub repo](https://github.com/PipedreamHQ/pipedream/tree/master/components). - -## Migration Example - -Let's walk through an example that migrates code for a legacy action to a Pipedream component. - -### Legacy Code Example - -Following is the code for the legacy action to get a GitHub repo (GitHub was linked to this action via Pipedream's UI, so it's not declared in the code): - -```javascript -const config = { - url: `https://api.github.com/repos/${params.owner}/${params.repo}`, - headers: { - Authorization: `Bearer ${auths.github.oauth_access_token}`, - } -} -return await require("@pipedreamhq/platform").axios(this, config) -``` - -Also, following is the associated JSON schema that defines metadata for the `params` inputs: - -```json -{ - "type": "object", - "properties": { - "owner": { - "type": "string", - "description": "Name of repository owner." - }, - "repo": { - "type": "string", - "description": "Name of repository." - } - }, - "required": [ - "owner", - "repo" - ] -} -``` - -### Converting to the Component Model - -To convert the code above to the component model, we need to: - -1. Link the GitHub app to the component using `props` (so we can use Pipedream managed auth for GitHub) -2. Define `props` for `owner` and `repo` so we can capture user input. The definition for each prop includes the `type` and `description` metadata. Additionally, since all the fields are required, we do not need to set the `optional` property (set `optional` to `true` for optional `props`). This metadata was previously captured in the JSON schema. -3. Replace references to `params` in the `run()` method. `props` are bound to `this` (e.g., `this.owner` and `this.repo`). -4. Update the reference to the GitHub OAuth token from `auths.github.oauth_access_token` to `this.github.$auth.oauth_access_token` (note: `github` in this context references the name of the prop, not the name of the app; if the prop was named `gh` then the auth would be referenced via `this.gh.$auth.oauth_access_token`). -5. Replace the `@pipedreamhq/platform` npm package with the standard `axios` package. - -```javascript -const axios = require("axios") - -module.exports = { - type: "action", - name: "Get Repo Example", - key: "github_get-repo-example", - version: "0.0.1", - props: { - github: { - type: "app", - app: "github", - }, - owner: { - type: "string", - description: "Name of repository owner.", - }, - repo: { - type: "string", - description: "Name of repository.", - } - }, - async run() { - const config = { - url: `https://api.github.com/repos/${this.owner}/${this.repo}`, - headers: { - Authorization: `Bearer ${this.github.$auth.oauth_access_token}`, - } - } - - return (await axios(config)).data - }, -} -``` - -### Advanced: Using Async Options - -Next, let's take the example one step further. Instead of asking users to enter the owner and repo name, let's use `async options` so users can select the repo from a drop-down menu. To do that, we'll: - -1. Remove the `owner` and `repo` props -2. Add a `repoFullName` prop that makes a request to `https://api.github.com/user/repos` to retrieve a list of (paginated) repos -3. Update the `run()` function to use the `repoFullName` prop - -```javascript -const axios = require("axios") - -module.exports = { - type: "action", - name: "Get Repo Example", - key: "github_get-repo-example", - version: "0.0.2", - props: { - github: { - type: "app", - app: "github", - }, - repoFullName: { - type: "string", - label: "Repo", - async options(page) { - const repos = (await axios({ - url: "https://api.github.com/user/repos", - params: { - page, - per_page: 100, - }, - headers: { - Authorization: `Bearer ${this.github.$auth.oauth_access_token}`, - } - })).data - return repos.map((repo) => repo.full_name); - }, - }, - }, - async run() { - const config = { - url: `https://api.github.com/repos/${this.repoFullName}`, - headers: { - Authorization: `Bearer ${this.github.$auth.oauth_access_token}`, - } - } - return (await axios(config)).data - }, -} -``` - -### Publishing and Using Actions - -In the legacy model, actions were published via Pipedream's UI. To learn how to publish component-based actions using Pipedream's CLI and use them in workflows, review our [quickstart guide](/components/quickstart/nodejs/actions/). To contribute actions to the Pipedream registry, review our [guidelines](/components/guidelines/). diff --git a/docs-v2/pages/connect/_meta.tsx b/docs-v2/pages/connect/_meta.tsx new file mode 100644 index 0000000000000..a53b7c0718ea4 --- /dev/null +++ b/docs-v2/pages/connect/_meta.tsx @@ -0,0 +1,32 @@ +export default { + "index": { + "title": "Overview", + }, + "use-cases": { + "title": "Use cases", + }, + "managed-auth": { + "title": "Managed auth", + }, + "components": { + "title": "Pre-built tools", + }, + "api-proxy": { + "title": "API proxy", + }, + "workflows": { + "title": "Workflows", + }, + "environments": { + "title": "Environments", + }, + "api": { + "title": "API & SDK reference", + }, + "troubleshooting": { + "title": "Troubleshooting", + }, + "migrating-from-project-keys-to-oauth": { + "display": "hidden", + }, +} as const diff --git a/docs-v2/pages/connect/api-proxy.mdx b/docs-v2/pages/connect/api-proxy.mdx new file mode 100644 index 0000000000000..c29f1216b741f --- /dev/null +++ b/docs-v2/pages/connect/api-proxy.mdx @@ -0,0 +1,170 @@ +import Callout from '@/components/Callout' + +# Connect API Proxy + +Pipedream Connect provides a proxy API that you can use to send authenticated requests to any integrated API on behalf of your users, which is useful in a few scenarios: + +1. You need code-level control and you want to use [Pipedream's OAuth](/connect/managed-auth/oauth-clients/#using-pipedream-oauth) instead of [your own OAuth client](/connect/managed-auth/oauth-clients/#using-a-custom-oauth-client) +2. There isn't a [pre-built tool](/connect/components/) (action) for the app, or you need to modify the request +3. You want to avoid storing end user credentials in your app + +## Overview + +The Connect proxy enables you to interface with any integrated API and make authenticated requests on behalf of your users, without dealing with OAuth or storing end user credentials. + +1. You send a request to the proxy and identify the end user you want to act on behalf of +2. The proxy sends the request to the downstream API and dynamically inserts your end user's auth credentials +3. The proxy returns the response from the downstream API back to you + +![Connect API proxy visualization](https://res.cloudinary.com/pipedreamin/image/upload/v1740098219/pd-connect-proxy-viz-updated_jb7wfu.png) + + +Before getting started with the Connect proxy, make sure you've already gone through the [managed auth quickstart](/connect/managed-auth/quickstart/) for Pipedream Connect. + + +## Getting started + +You can send requests to the Connect proxy using the [Pipedream SDK](/connect/sdk/) with a fetch-style interface, or by making a request to the [REST API](/rest-api/connect/proxy/). + +- A [Pipedream OAuth client](/rest-api/auth/#oauth) to make authenticated requests to Pipedream's API +- Connect [environment](/connect/environments/) (ex, `production` or `development`) +- The [external user ID](/connect/api/#external-users) for your end user (ex, `abc-123`) +- The [account ID](/connect/api/#accounts) for your end user's connected account (ex, `apn_1234567`) + +Refer to the full Connect API [here](/connect/api/). + +### Authenticating on behalf of your user + +Most API integrations that use OAuth to authenticate requests require that you pass a user's access token in the `Authorization` header with the `Bearer` prefix. For these apps, the Connect proxy will automatically handle that for you — you don't need to pass any reference to their OAuth access token in this case. + +For apps that require a different authentication method, you should include the necessary headers with the value surrounded by `{{ }}` in your request to the proxy, and Pipedream will automatically replace the macro with the real values and forward to the downstream API. For example: + +```javascript +/* +OpenAI requires that you pass the API key +in the `Authorization` header with the `Bearer` prefix: +*/ + +headers: { + authorization: "Bearer {{api_key}}", +} + +// Pipedream will replace the `{{api_key}}` macro with the actual API key +``` + +```javascript +/* +Zoho apps require that you pass the OAuth access token +in the `Authorization` header with a custom `Zoho-oauthtoken` prefix: +*/ + +headers: { + authorization: "Zoho-oauthtoken {{oauth_access_token}}", +} + +// Pipedream will replace the `{{oauth_access_token}}` macro with the actual token +``` + + +Refer to the relevant API's developer documentation for the correct way to authenticate requests. + + +### Making a request + +#### Using the Pipedream SDK + +You can use the [Pipedream SDK](https://www.npmjs.com/package/@pipedream/sdk) to send a fetch-style request: + +```javascript +import { createBackendClient } from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: {development | production}, + projectId: {your_projectId}, + credentials: { + clientId: {your_oauth_client_id}, + clientSecret: {your_oauth_client_secret} + }, +}); + + +const resp = await pd.makeProxyRequest( + { + searchParams: { + account_id: "{account_id}", // The account ID for your end user (ex, apn_1234567) + external_user_id: "{external_user_id}", // The external user ID for your end user + } + }, + { + url: "https://slack.com/api/chat.postMessage", // Include any query params you need; no need to Base64 encode the URL if using the SDK + options: { + method: "POST", + headers: { + hello: "world!" // Include any headers you need to send to the downstream API + }, + body: { + text: "hello, world", + channel: "C03NA8B4VA9" + }, + }, + } +) + +// Parse and return the data you need +console.log(resp); +``` + +#### Using the REST API + +You can also send a request to the Connect REST API with the below config: + +**URL** + +- The URL of the API you want to call (ex, `https://slack.com/api/chat.postMessage`) +- When using the REST API, this should be an URL-safe Base64 encoded string (ex, `aHR0cHM6Ly9zbGFjay5jb20vYXBpL2NoYXQucG9zdE1lc3NhZ2U`) + +**HTTP method** + +- Use the HTTP method required by the downstream API + +**Body** + +- Optionally include a body to send to the downstream API + +**Headers** + +- If using the REST API, include the `Authorization` header with your Pipedream OAuth access token (`Bearer {access_token}`) +- Headers that contain the prefix `x-pd-proxy` will get forwarded to the downstream API +- If the downstream API requires [custom authorization headers](#authenticating-on-behalf-of-your-user), make sure to prepend with `x-pd-proxy` and include the macro `{{ }}` that Pipedream will replace with the actual value. For example, + +```javascript +"x-pd-proxy-apiKey: {{api_key}}" +``` + +```bash +# First, obtain an OAuth access token to authenticate to the Pipedream API + +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{your_oauth_client_id}", + "client_secret": "{your_oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. + +curl -X POST "https://api.pipedream.com/v1/connect/{your_project_id}/proxy/{url_safe_base64_encoded_url}?external_user_id={external_user_id}&account_id={apn_xxxxxxx}" \ + -H "Authorization: Bearer {access_token}" \ + -H "x-pd-environment: {development | production}" \ + -d '{ + "text": "hello, world", + "channel": "C03NA8B4VA9" + }' + +# Parse and return the data you need +``` + +## Rate limits + +The Connect proxy limits API requests to **100 per minute per project**. [Let us know](https://pipedream.com/support) if you need higher limits. diff --git a/docs-v2/pages/connect/api.mdx b/docs-v2/pages/connect/api.mdx new file mode 100644 index 0000000000000..cdc7e94f8c8a5 --- /dev/null +++ b/docs-v2/pages/connect/api.mdx @@ -0,0 +1,3503 @@ +import Callout from '@/components/Callout' +import { Tabs } from 'nextra/components' + +# Connect API & SDK reference + +Pipedream provides a TypeScript SDK and a REST API to interact with the Connect service. You'll find examples using the SDK and the REST API in multiple languages below. + +## REST API base URL + +Pipedream Connect resources are scoped to [projects](/workflows/projects/), so you'll need to pass [the project's ID](/workflows/projects/#finding-your-projects-id) as a part of the base URL: + +``` +https://api.pipedream.com/v1/connect/{project_id} +``` + +## Installing the TypeScript SDK + +Pipedream's SDK will work in any browser or server that can run JavaScript. + +### npm + +To install [the SDK](https://www.npmjs.com/package/@pipedream/sdk) from npm, run: + +```bash +npm i --save @pipedream/sdk +``` + +### ` +``` + +or a specific version: + +```html + +``` + +## Authentication + +See the [REST API Authentication docs](/rest-api/auth/). + +### TypeScript SDK (server) + +Most of your interactions with the Connect API will happen on the server, to protect API requests and user credentials. You'll use the SDK in [your frontend](#typescript-sdk-browser) to let users connect accounts. Once connected, you'll use the SDK on the server to retrieve credentials, invoke workflows on their behalf, and more. + +[Create a Pipedream OAuth client](/rest-api/auth/#oauth) and instantiate the SDK with your client ID and secret: + +```typescript +import { createBackendClient } from "@pipedream/sdk/server"; + +// These secrets should be saved securely and passed to your environment +const pd = createBackendClient({ + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + } +}); + +// The `pd` client provides methods to interact with the Connect API — see below +``` + +### TypeScript SDK (browser) + +You'll primarily use the browser SDK to let your users securely connect apps from your frontend. Here, you + +1. [Create a short-lived token on your server](#create-a-new-token) +2. Initiate auth with that token to securely connect an account for a specific user + +Here's a Next.js example [from our quickstart](/connect/managed-auth/quickstart/): + +```typescript +import { createFrontendClient } from "@pipedream/sdk/browser" +// Example from our Next.js app +import { serverConnectTokenCreate } from "./server" + +const { token, expires_at } = await serverConnectTokenCreate({ + external_user_id: externalUserId // The end user's ID in your system +}); + +export default function Home() { + const pd = createFrontendClient() + function connectAccount() { + pd.connectAccount({ + app: appSlug, // Pass the app name slug of the app you want to integrate + oauthAppId: appId, // For OAuth apps, pass the OAuth app ID; omit this param to use Pipedream's OAuth client or for key-based apps + token, // The token you received from your server above + onSuccess: ({ id: accountId }) => { + console.log(`Account successfully connected: ${accountId}`) + } + }) + } + + return ( +
+ +
+ ) +} +``` + +## Environment + +Some API endpoints accept an [environment](/connect/environments/) parameter. This lets you specify the environment (`production` or `development`) where resources will live in your project. + +Always set the environment when you create the SDK client: + +```typescript +import { createBackendClient } from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "your-oauth-client-id", + clientSecret: "your-oauth-client-secret", + } +}); +``` + +or pass the `X-PD-Environment` header in HTTP requests: + +```bash +curl -X POST https://api.pipedream.com/v1/connect/{project_id}/tokens \ + -H "Content-Type: application/json" \ + -H "X-PD-Environment: development" \ + -H "Authorization: Bearer {access_token}" \ + -d '{ + "external_user_id": "your-external-user-id" + }' +``` + +## External users + +When you use the Connect API, you'll pass an `external_user_id` parameter when initiating account connections and retrieving credentials. This is your user's ID, in your system — whatever you use to uniquely identify them. + +Pipedream associates this ID with user accounts, so you can retrieve credentials for a specific user, and invoke workflows on their behalf. + +External User IDs are limited to 250 characters. + +## Rate limits + +### Pipedream rate limits + +| API Endpoint | Rate Limit | +|----------------------------|------------------------------------------------------| +| `POST /tokens` | 100 requests per minute per `external_user_id` | +| `GET /accounts`| 100 requests per minute per project

The sum of requests across all `*/accounts/*` endpoints must not exceed 100 requests per minute. This includes requests to,
• `/accounts`
• `/apps/:app_id/accounts`
• `/accounts/:account_id` | +| `/proxy` | 100 requests per minute per project | + +If you need higher rate limits, please [reach out](https://pipedream.com/support). + +### Developer rate limits + +- You can optionally set rate limits for your users to control their usage of the Connect API from within your application, to prevent runaway use or abuse. +- Specify a time window in seconds and how many requests to allow in that window. The API will give you a `rate_limit_token` that you'll need to include in future `/connect/` requests: + +``` +POST /rate_limits +``` + +**Body parameters** + +`window_size_seconds` **integer** + +Define the size of the time window in seconds. + +--- + +`requests_per_window` **integer** + +Define the number of requests you want to allow per time window. + +**Example request** + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. +# Define the rate limit parameters + +curl -X POST https://api.pipedream.com/v1/connect/rate_limits \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer {access_token}" \ + -d '{ + "window_size_seconds": 10, + "requests_per_window": 1000 + }' +``` + +**Example response** +```json +{ + "token": "CiKpqRdTmNwLfhzSvYxBjAkMnVbXuQrWeZyHgPtJsDcEvFpLnE" +} +``` + +**Example usage** + +```bash +# The response will include a rate limit token. +# Pass it as a header in your downstream requests to the Connect API. +# Below is an example request that runs the "List Commits" action for the Gitlab app. + +curl -X POST "https://api.pipedream.com/v1/connect/{your_project_id}/actions/run" \ + -H "Authorization: Bearer {access_token}" \ + -H "Content-Type: application/json" \ + -H "x-pd-rate-limit: {rate_limit_token}" \ # Pass the rate limit token in the header + -d '{ + "external_user_id": "jverce", + "id": "gitlab-list-commits", + "configured_props": { + "gitlab": { + "authProvisionId": "apn_kVh9AoD" + }, + "projectId": 45672541, + "refName": "main" + } + }' +``` + +## API Reference + +### Invoke workflows + +You can use the SDK to [invoke workflows on behalf of any end user](/connect/workflows/). **Write one workflow, run it for all of your users**. + +### Tokens + +Your app will initiate the account connection flow for your end users in your frontend. To securely scope connection to a specific end user, on your server, **you retrieve a short-lived token for that user**, and return that token to your frontend. + +See [the Connect tokens docs](/connect/tokens/) for more information. + +#### Create a new token + +``` +POST /{project_id}/tokens +``` + +##### Path parameters + +`project_id` **string** + +[The project's ID](/workflows/projects/#finding-your-projects-id) + +##### Body parameters + +`external_user_id` **string** + +The ID of your end user. Use whatever ID uniquely identifies the user in your system. + +--- + +`allowed_origins` **string array** + +When using the Connect API to make requests from a client environment like a browser, you must specify the allowed origins for the token. Otherwise, this field is optional. This is a list of URLs that are allowed to make requests with the token. For example: + +```json +{ + "allowed_origins": ["http://localhost:3000", "https://example.com"] +} +``` +--- + +`success_redirect_uri` **string** (_optional_) + +When using [Connect Link](/connect/connect-link/), you can optionally redirect your end user to the `success_redirect_uri` on successful completion of the auth flow. + +--- + +`error_redirect_uri` **string** (_optional_) + +When using [Connect Link](/connect/connect-link/), you can optionally redirect your end user to the `error_redirect_uri` on any errors in the auth flow. This lets you handle errors in whatever way you want in your own app. + +--- + +`webhook_uri` **string** (_optional_) + +Pipedream will send events on successful auth, or any errors, to this URL via webhook. [See the webhooks docs](/connect/webhooks/) for more information. + +##### Examples + +To create a short-lived token via TypeScript / JavaScript SDK, you'll need to create a Pipedream API client and call the `createConnectToken` method. In our example app, this code is in `app/server.ts`. + +In other languages, you'll need to make an HTTP POST request to the `/tokens` endpoint to create a token, then return the token to your frontend. Click into other tabs to see examples in additional languages. + + + +```typescript +import { + createBackendClient, + type BackendClient, + type BackendClientOpts, + type ConnectAPIResponse, + type ConnectTokenCreateOpts, + type ConnectTokenResponse, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}; +const pd: BackendClient = createBackendClient(clientOpts); + +const requestOpts: ConnectTokenCreateOpts = { + // The end user's ID in your system + external_user_id: "{your_external_user_id}", + // The allowed origins for the token (required for client-side requests) + allowed_origins: ["http://localhost:3000", "https://example.com"], +}; +const response: ConnectTokenResponse = await pd.createConnectToken(requestOpts); + +const { + token, // The token you'll pass to the frontend + expires_at, // The token's expiration date + connect_link_url, // The URL to redirect the user to for the Connect Link flow +}: { + token: string, + expires_at: string, + connect_link_url: string, +} = response; +``` + + +```javascript +import { createBackendClient } from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +const { token, expires_at } = await pd.createConnectToken({ + external_user_id: "{your_external_user_id}", // The end user's ID in your system + allowed_origins: ["http://localhost:3000", "https://example.com"], // The allowed origins for the token (required for client-side requests) +}); +``` + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. + +curl -X POST https://api.pipedream.com/v1/connect/{project_id}/tokens \ + -H "Content-Type: application/json" \ + -H "X-PD-Environment: development" \ + -H "Authorization: Bearer {access_token}" \ + -d '{ + "external_user_id": "{your_external_user_id}", + "allowed_origins": [ + "http://localhost:3000", + "https://example.com" + ], + }' +``` + + + +### Accounts + +#### List all accounts + +List all connected accounts for all end users within a project. + +``` +GET /{project_id}/accounts/ +``` + +##### Path parameters + +`project_id` **string** + +[The project's ID](/workflows/projects/#finding-your-projects-id) + +##### Query parameters + +`app` **string** (_optional_) + +The ID or name slug the app you'd like to retrieve. For example, Slack's unique app ID is `app_OkrhR1`, and its name slug is `slack`. + +You can find the app's ID in the response from the [List apps](/rest-api/#list-apps) endpoint, and the name slug under the **Authentication** section of any [app page](https://pipedream.com/apps). + +--- + +`oauth_app_id` **string** (_optional_) + +The ID of the [OAuth app](/connect/managed-auth/quickstart/#create-a-pipedream-oauth-client) you'd like to retrieve accounts for. + +--- + +`external_user_id` **string** (_optional_) + +[The external user ID](/connect/api/#external-users) in your system that you want to retrieve accounts for. + +--- + +`include_credentials` **boolean** (_optional_) + +Pass `include_credentials=true` as a query-string parameter to include the account credentials in the response. + + +Never return user credentials to the client + + + +To retrieve the credentials for any account in `production` for OAuth apps (Slack, Google Sheets, etc), the connected account must be using [your own OAuth client](/connect/managed-auth/oauth-clients/#using-a-custom-oauth-client). You can only retrieve end user credentials for accounts that are using Pipedream's OAuth clients in `development`. [Learn more here](/connect/managed-auth/oauth-clients/#using-pipedream-oauth). + + +##### Examples + + + +```typescript +import { + createBackendClient, + type Account, + type BackendClientOpts, + type BackendClient, + type GetAccountOpts, + type GetAccountsResponse, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}; +const pd: BackendClient = createBackendClient(clientOpts); + +const requestOpts: GetAccountOpts = { + app: "github", // optional, filter by app + external_user_id: "user-abc-123", // optional, filter by external user ID + include_credentials: true, // optional, set to true to include credentials +}; +const response: GetAccountsResponse = await pd.getAccounts(requestOpts); + +// These may contain credentials, which you should never return to the client +const { + data +}: { + data: Account[] +} = response; +``` + + +```javascript +import { createBackendClient } from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +const accounts = await pd.getAccounts({ + app: "github", // optional, filter by app + external_user_id: "user-abc-123", // optional, filter by external user ID + include_credentials: true, // optional, set to true to include credentials +}); + +// Parse and return the data you need. These may contain credentials, +// which you should never return to the client +``` + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. + +curl -X GET \ + -G \ + "https://api.pipedream.com/v1/connect/{project_id}/accounts" \ + -H "Authorization: Bearer {access_token}" \ + -d "app=github" \ # optional, filter by app + -d "external_user_id=user-abc-123" \ # optional, filter by external user ID + -d "include_credentials=true" # optional, include credentials + + +# Parse and return the data you need. These may contain credentials, +# which you should never return to the client +``` + + + +##### Example response (without credentials) + +```json +{ + "page_info": { + "total_count": 5, + "count": 5, + "start_cursor": "YXBuX0JtaEJKSm0", + "end_cursor": "YXBuX1YxaE1lTE0", + }, + "data": { + "accounts": [ + { + "id": "apn_XehyZPr", + "name": null, + "external_id": "user-abc-123", + "healthy": true, + "dead": false, + "app": { + "id": "app_OkrhR1", + "name": "Slack" + }, + "created_at": "2024-07-30T22:52:48.000Z", + "updated_at": "2024-08-01T03:44:17.000Z" + }, + { + "id": "apn_b6h9QDK", + "name": null, + "external_id": "user-abc-123", + "healthy": true, + "dead": false, + "app": { + "id": "app_OrZhaO", + "name": "GitHub" + }, + "created_at": "2024-07-31T02:49:18.000Z", + "updated_at": "2024-08-01T03:58:17.000Z" + }, + { + "id": "apn_0WhJYxv", + "name": null, + "external_id": "user-abc-123", + "healthy": true, + "dead": false, + "app": { + "id": "app_OrZhaO", + "name": "GitHub" + }, + "created_at": "2024-07-31T20:28:16.000Z", + "updated_at": "2024-08-01T03:47:30.000Z" + }, + { + "id": "apn_kVh9PJx", + "name": null, + "external_id": "user-abc-123", + "healthy": true, + "dead": false, + "app": { + "id": "app_OrZhaO", + "name": "GitHub" + }, + "created_at": "2024-07-31T21:17:03.000Z", + "updated_at": "2024-08-01T03:43:23.000Z" + }, + { + "id": "apn_WYhMlrz", + "name": null, + "external_id": "user-abc-123", + "healthy": true, + "dead": false, + "app": { + "id": "app_XBxhAl", + "name": "Airtable" + }, + "created_at": "2024-08-01T04:04:03.000Z", + "updated_at": "2024-08-01T04:04:03.000Z" + } + ] + } +} +``` + +##### Example response (with credentials) + +```json +{ + "page_info": { + "total_count": 1, + "count": 1, + "start_cursor": "YXBuX0JtaEJKSm0", + "end_cursor": "YXBuX1YxaE1lTE0", + }, + "data": { + "accounts":[ + { + "id": "apn_MGhvgnX", + "name": "gcostanza", + "external_id": "user-abc-123", + "healthy": true, + "dead": null, + "app": { + "id": "oa_aPXiQd", + "name_slug": "github", + "name": "GitHub", + "auth_type": "oauth", + "description": "Where the world builds software. Millions of developers and companies build, ship, and maintain their software on GitHub—the largest and most advanced development platform in the world.", + "img_src": "https://assets.pipedream.net/s.v0/app_OrZhaO/logo/orig", + "custom_fields_json": "[]", + "categories": [ + "Developer Tools" + ] + }, + "created_at": "2024-12-03T04:26:38.000Z", + "updated_at": "2024-12-11T17:59:28.000Z", + "credentials": { + "oauth_client_id": "xyz789...", + "oauth_access_token": "xxx_abc123...", + "oauth_uid": "123456789" + }, + "expires_at": null, + "error": null, + "last_refreshed_at": "2024-12-11T17:59:28.000Z", + "next_refresh_at": "2024-12-11T18:56:28.000Z" + } + ] + } +} +``` + +#### Retrieve account details by ID + +Retrieve the account details for a specific account based on the account ID + +``` +GET /{project_id}/accounts/{account_id} +``` + +##### Path parameters + +`project_id` **string** + +[The project's ID](/workflows/projects/#finding-your-projects-id) + +--- + +`account_id` **string** + +The ID of the account you want to retrieve + +##### Parameters + +`include_credentials` **boolean** (_optional_) + +Pass `include_credentials=true` as a query-string parameter to include the account credentials in the response. + + +Never return user credentials to the client + + + +To retrieve the credentials for any account in `production` for OAuth apps (Slack, Google Sheets, etc), the connected account must be using [your own OAuth client](/connect/managed-auth/oauth-clients/#using-a-custom-oauth-client). You can only retrieve end user credentials for accounts that are using Pipedream's OAuth clients in `development`. [Learn more here](/connect/managed-auth/oauth-clients/#using-pipedream-oauth). + + +##### Examples + + + +```typescript +import { + createBackendClient, + type Account, + type BackendClient, + type BackendClientOpts, + type GetAccountOpts, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}; +const pd: BackendClient = createBackendClient(clientOpts); + +const accountId: string = "{account_id}"; // Replace with your account ID +const requestOpts: GetAccountOpts = { + include_credentials: true, // optional, set to true to include credentials +}; +// These may contain credentials, which you should never return to the client. +const account: Account = await pd.getAccountById(accountId, requestOpts); +``` + + +```javascript +import { createBackendClient } from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +const accountId = "{account_id}"; // Replace with your account ID + +const account = await pd.getAccountById(accountId, { + include_credentials: true, // optional, set to true to include credentials +}); + +// Parse and return the data you need. These may contain credentials, +// which you should never return to the client +``` + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. + +curl -X GET \ + -G \ + "https://api.pipedream.com/v1/connect/{project_id}/accounts/{account_id}" \ + -H "Authorization: Bearer {access_token}" \ + -d "app=github" \ # optional, filter by app + -d "external_user_id=user-abc-123" \ # optional, filter by external user ID + -d "include_credentials=true" # optional, include credentials + +# Parse and return the data you need. These may contain credentials, +# which you should never return to the client +``` + + + +##### Example response (without account credentials) + +```json +{ + "data": { + "id": "apn_WYhMlrz", + "name": null, + "external_id": "user-abc-123", + "healthy": true, + "dead": false, + "app": { + "id": "oa_aw4ib2", + "name_slug": "airtable_oauth", + "name": "Airtable", + "auth_type": "oauth", + "description": "Airtable is a low-code platform to build next-gen apps. Move beyond rigid tools, operationalize your critical data, and reimagine workflows with AI.", + "img_src": "https://assets.pipedream.net/s.v0/app_XBxhAl/logo/orig", + "custom_fields_json": "[]", + "categories": ["Productivity"] + }, + "created_at": "2024-08-01T04:04:03.000Z", + "updated_at": "2024-08-01T04:04:03.000Z" + } +} +``` + +##### Example response (with `include_credentials=true`) + +```json +{ + "data": { + "id": "apn_WYhMlrz", + "name": null, + "external_id": "user-abc-123", + "healthy": true, + "dead": false, + "app": { + "id": "app_XBxhAl", + "name": "Airtable" + }, + "created_at": "2024-08-01T04:04:03.000Z", + "updated_at": "2024-08-01T04:04:03.000Z", + "credentials": { + "oauth_client_id": "dd7a26ca-ba11-4f80-8667-xxxxxxxx", + "oauth_access_token": "oaaLa2Ob1umiregWa.v1.xxxxxxxx.xxxxxxxx", + "oauth_refresh_token": "oaaLa2Ob1umiregWa.v1.refresh.xxxxxxxx", + "oauth_uid": "usrnbIhrxxxxxxxx" + }, + "expires_at": "2024-08-01T05:04:03.000Z", + "project_id": 279440, + "user_id": "gcostanza", + "error": null, + "last_refreshed_at": null, + "next_refresh_at": "2024-08-01T04:17:33.000Z" + } +} +``` + +#### Delete connected account + +Delete a specific connected account for an end user, and any deployed triggers. + +``` +DELETE /{project_id}/accounts/{account_id} +``` + +##### Path parameters + +`project_id` **string** + +[The project's ID](/workflows/projects/#finding-your-projects-id) + +--- + +`account_id` **string** + +##### Examples + + + +```typescript +import { + createBackendClient, + type BackendClient, + type BackendClientOpts, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}; +const pd: BackendClient = createBackendClient(clientOpts); + +const accountId: string = "{account_id}"; // Replace with your account ID +await pd.deleteAccount(accountId); + +// You can return a message or handle any post-deletion logic here. +``` + + +```javascript +import { createBackendClient } from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +const accountId = "{account_id}"; // Replace with your account ID +await pd.deleteAccount(accountId); + +// You can return a message or handle any post-deletion logic here. +``` + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. + +curl -X DELETE "https://api.pipedream.com/v1/connect/{project_id}/accounts/{account_id}" \ + -H "Authorization: Bearer {access_token}" +``` + + + +##### Response + +Pipedream returns a `204 No Content` response on successful account deletion + +#### Delete all connected accounts for an app + +Delete all connected accounts for a specific app + +``` +DELETE /{project_id}/apps/{app_id}/accounts +``` + +##### Path parameters + +`project_id` **string** + +[The project's ID](/workflows/projects/#finding-your-projects-id) + +--- + +`app_id` **string** + +The app ID for which you want to delete all connected accounts. `app_id` can be `oauth_app_id` for [OAuth apps](/connect/managed-auth/quickstart/#create-a-pipedream-oauth-client) or name slug for key-based apps, which you can find under the **Authentication** section of any [app page](https://pipedream.com/apps) + +##### Examples + + + +```typescript +import { + createBackendClient, + type BackendClient, + type BackendClientOpts, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}; +const pd: BackendClient = createBackendClient(clientOpts); + +const appId: string = "{app_id}"; // Replace with the app ID +await pd.deleteAccountsByApp(appId); + +// You can return a message or handle any post-deletion logic here. +``` + + +```javascript +import { createBackendClient } from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +const appId = "{app_id}"; // Replace with the app ID +await pd.deleteAccountsByApp(appId); + +// You can return a message or handle any post-deletion logic here. +``` + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. + +curl -X DELETE "https://api.pipedream.com/v1/connect/{project_id}/accounts/{account_id}" \ + -H "Authorization: Bearer {access_token}" +``` + + + +##### Response + +Pipedream returns a `204 No Content` response on successful account deletion + +#### Delete an end user + +Delete an end user, all their connected accounts, and any deployed triggers. + +``` +DELETE /{project_id}/users/{external_user_id} +``` + +##### Path parameters + +`project_id` **string** + +[The project's ID](/workflows/projects/#finding-your-projects-id) + +--- + +`external_user_id` **string** + +[The external user ID](#external-users) in your system + +##### Examples + + + +```typescript +import { + createBackendClient, + type BackendClient, + type BackendClientOpts, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}; +const pd: BackendClient = createBackendClient(clientOpts); + +const externalId: string = "{external_user_id}"; // Replace with your external user ID +await pd.deleteExternalUser(externalId); + +// You can return a message or handle any post-deletion logic here. +``` + + +```javascript +import { createBackendClient } from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +const externalId = "{external_user_id}"; // Replace with your external user ID +await pd.deleteExternalUser(externalId); + +// You can return a message or handle any post-deletion logic here. +``` + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. + +curl -X DELETE "https://api.pipedream.com/v1/connect/{project_id}/users/{external_user_id}" \ + -H "Authorization: Bearer {access_token}" +``` + + + +##### Response + +Pipedream returns a `204 No Content` response on successful account deletion + + +### Components + +#### List components + +List all the components in the Pipedream registry. + +``` +GET /{component_type} +``` + +##### Path parameters + +`component_type` **string** + +Either `triggers`, `actions`, or `components`. + +##### Query parameters + +`app` **string** (_optional_) + +The ID or name slug the app you'd like to retrieve. For example, Slack's unique +app ID is `app_OkrhR1`, and its name slug is `slack`. + +You can find the app's ID in the response from the [List +apps](/rest-api/#list-apps) endpoint, and the name slug under the +**Authentication** section of any [app page](https://pipedream.com/apps). + +--- + +`q` **string** (_optional_) + +A search query to filter the components by key (see the [component structure +table](/workflows/contributing/components/api/#component-structure)). + +##### Examples + + + + +```typescript +import { + createBackendClient, + type BackendClient, + type BackendClientOpts, + type GetComponentsOpts, + type GetComponentsResponse, + type V1Component, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}; +const pd: BackendClient = createBackendClient(clientOpts); + +// Retrieve components containing the word "issue" in their name, belonging to +// the Gitlab app +const requestOpts: GetComponentsOpts = { + app: "gitlab", + q: "issue", +}; +const response: GetComponentsResponse = await pd.getComponents(requestOpts); + +const { + data // The list of components for the Gitlab app and query `q` +}: { + data: Omit[]; +} = response; +``` + + + +```javascript +import { createBackendClient } from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +// Retrieve components containing the word "issue" in their name, belonging to +// the Gitlab app +const { data: components } = await pd.getComponents({ + app: "gitlab", + q: "issue", +}); + +// Parse and return the data you need +``` + + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. +# This request will fetch the first 3 components for the Gitlab app that contain +# the word "issue" in their name +curl -X GET "https://api.pipedream.com/v1/connect/{your_project_id}/components?app=gitlab&limit=3&q=issue" \ + -H "Authorization: Bearer {access_token}" +``` + + + + +##### Example response +```json +{ + "page_info": { + "total_count": 5, + "count": 3, + "start_cursor": "c2NfM3ZpanpRcg", + "end_cursor": "c2NfQjVpTkJBcA" + }, + "data": [ + { + "name": "New Issue (Instant)", + "version": "0.1.2", + "key": "gitlab-new-issue" + }, + { + "name": "Update Issue", + "version": "0.0.2", + "key": "gitlab-update-issue" + }, + { + "name": "Search Issues", + "version": "0.0.3", + "key": "gitlab-search-issues" + } + ] +} +``` + + +#### Retrieve a component + +Retrieve a specific component from the Pipedream registry. + +``` +GET /{component_type}/{component_key} +``` + +##### Path parameters + +`component_type` **string** + +Either `triggers`, `actions`, or `components`. + +--- + +`component_key` **string** + +The key that identifies the component (see the [component structure +table](/workflows/contributing/components/api/#component-structure)). + +##### Examples + + + + +```typescript +import { + createBackendClient, + type BackendClient, + type BackendClientOpts, + type GetComponentOpts, + type GetComponentResponse, + type V1Component, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}; +const pd: BackendClient = createBackendClient(clientOpts); + +// Retrieve the "New Issue (Instant)" component for the Gitlab app +const requestOpts: GetComponentOpts = { + key: "gitlab-new-issue", +}; +const response: GetComponentResponse = await pd.getComponent(requestOpts); + +const { + data // The "New Issue (Instant)" component for the Gitlab app +}: { + data: V1Component; +} = response; +``` + + + +```javascript +import { createBackendClient } from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +// Retrieve the "New Issue (Instant)" component for the Gitlab app +const { data: component } = await pd.getComponent({ + key: "gitlab-new-issue", +}); + +// Parse and return the data you need +``` + + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. +# This request will fetch the "New Issue (Instant)" component for the Gitlab app. +curl -X GET "https://api.pipedream.com/v1/connect/{your_project_id}/components/gitlab-new-issue" \ + -H "Authorization: Bearer {access_token}" +``` + + + +##### Example response + +```json +{ + "data": { + "name": "New Issue (Instant)", + "version": "0.1.2", + "key": "gitlab-new-issue", + "configurable_props": [ + { + "name": "gitlab", + "type": "app", + "app": "gitlab" + }, + { + "name": "db", + "type": "$.service.db" + }, + { + "name": "http", + "type": "$.interface.http", + "customResponse": true + }, + { + "name": "projectId", + "type": "integer", + "label": "Project ID", + "description": "The project ID, as displayed in the main project page", + "remoteOptions": true + } + ] + } +} +``` + + + + +#### Configure a component + +Configure the a component's prop, based on the current component's +configuration. This endpoint will use the component's configuration to retrieve +the list of options that can be used to configure the prop indicated in the +request. + +```text +GET /{component_type}/configure +``` + +##### Path parameters + +`component_type` **string** + +Either `triggers`, `actions`, or `components`. + +##### Body parameters + +`configured_props` **object** + +The props that have already been configured for the component. This is a +JSON-serializable object with the prop names as keys and the configured values +as values. + +--- + +`external_user_id` **string** + +[The external user ID](/connect/api/#external-users) in your system that you +want to retrieve accounts for. + +--- + +`id` **string** + +The key that identifies the component (see the [component structure +table](/workflows/contributing/components/api/#component-structure)). + +--- + +`prop_name` **string** + +The name of the component's prop to configure. + +##### Examples + + + + +```typescript +import { + createBackendClient, + type BackendClient, + type BackendClientOpts, + type ConfigureComponentOpts, + type ConfigureComponentResponse, + type PropOption, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}; +const pd: BackendClient = createBackendClient(clientOpts); + +// Retrieve the configuration options for the "projectId" prop of the "List +// Commits" component for the Gitlab app. +const requestOpts: ConfigureComponentOpts = { + componentId: { + key: "gitlab-list-commits", + }, + configuredProps: { + gitlab: { + authProvisionId: "apn_kVh9AoD", + }, + }, + externalUserId: "jverce", + propName: "projectId", +}; +const response: ConfigureComponentResponse = await pd.configureComponent(requestOpts); + +const { + options, // The list of options for the "projectId" prop + stringOptions, // The list of string options for the "projectId" prop + errors, // Any errors that occurred during the configuration +}: { + options: PropOption[]; + stringOptions: string[]; + errors: string[]; +} = response; +``` + + + +```javascript +import { createBackendClient } from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +// Retrieve the configuration options for the "projectId" prop of the "List +// Commits" component for the Gitlab app. +const { options } = await pd.configureComponent({ + componentId: { + key: "gitlab-list-commits", + }, + configuredProps: { + gitlab: { + authProvisionId: "apn_kVh9AoD", + }, + }, + externalUserId: "jverce", + propName: "projectId", +}); + +// Parse and return the data you need +``` + + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. +# This request will fetch the configuration options for the "projectId" prop of +# the "List Commits" component for the Gitlab app. + +echo '{ + "external_user_id": "jverce", + "id": "gitlab-list-commits", + "prop_name": "projectId", + "configured_props": { + "gitlab": { + "authProvisionId": "apn_kVh9AoD" + } + } +}' > data.json + +curl -X POST "https://api.pipedream.com/v1/connect/{your_project_id}/components/configure" \ + -H "Authorization: Bearer {access_token}" \ + -H "Content-Type: application/json" \ + -d @data.json +``` + + + + +##### Example response + +```json +{ + "observations": [], + "context": null, + "options": [ + { + "label": "jverce/foo-massive-10231-1", + "value": 45672541 + }, + { + "label": "jverce/foo-massive-10231", + "value": 45672514 + }, + { + "label": "jverce/foo-massive-14999-2", + "value": 45672407 + }, + { + "label": "jverce/foo-massive-14999-1", + "value": 45672382 + }, + { + "label": "jverce/foo-massive-14999", + "value": 45672215 + }, + { + "label": "jverce/gitlab-development-kit", + "value": 21220953 + }, + { + "label": "jverce/gitlab", + "value": 21208123 + } + ], + "errors": [], + "timings": { + "api_to_sidekiq": 1734043172355.1042, + "sidekiq_received": 1734043172357.867, + "sidekiq_to_lambda": 1734043172363.6812, + "sidekiq_done": 1734043173461.6406, + "lambda_configure_prop_called": 1734043172462, + "lambda_done": 1734043173455 + }, + "stringOptions": null +} +``` + +#### Reload a component's props (i.e. configure dynamic props) + +Reload the component's props after configuring a dynamic prop, based on the +current component's configuration. This endpoint will use the component's +configuration to retrieve a new list of props depending on the value of the +props that were configured so far. See the [Dynamic +Props](/connect/workflows/contributing/components#dynamic-props-optional) section for more information. + +```text +GET /{component_type}/props +``` + +##### Path parameters + +`component_type` **string** + +Either `triggers`, `actions`, or `components`. + +##### Body parameters + +`configured_props` **object** + +The props that have already been configured for the component. This is a +JSON-serializable object with the prop names as keys and the configured values +as values. + +--- + +`external_user_id` **string** + +[The external user ID](/connect/api/#external-users) in your system that you +want to retrieve accounts for. + +--- + +`id` **string** + +The key that identifies the component (see the [component structure +table](/workflows/contributing/components/api/#component-structure)). + +--- + +`dynamic_props_id` **string** (_optional_) + +The ID of the last prop reconfiguration (or none when reconfiguring the props +for the first time). + +##### Examples + + + + +```typescript +import { + createBackendClient, + type BackendClient, + type BackendClientOpts, + type ConfigurableProps, + type ReloadComponentPropsOpts, + type ReloadComponentPropsResponse, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}; +const pd: BackendClient = createBackendClient(clientOpts); + +// Retrieve the configuration options for the "Add Single Row" component for +// the Google Sheets app. Note that the `sheetId` prop is a dynamic prop. +const requestOpts: ReloadComponentPropsOpts = { + componentId: { + key: "google_sheets-add-single-row", + }, + configuredProps: { + googleSheets: { + authProvisionId: "apn_V1hMoE7", + }, + sheetId: "1BfWjFF2dTW_YDiLISj5N9nKCUErShgugPS434liyytg", + }, + externalUserId: "jay", +}; +const response: ReloadComponentPropsResponse = await pd.reloadComponentProps(requestOpts); + +const { + configurableProps, // The new set of props + id: dynamicPropsId, // The ID of the last prop reconfiguration +}: { + configurableProps: ConfigurableProps, + id: string, +} = response.dynamicProps; +``` + + + +```javascript +import { createBackendClient } from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +// Retrieve the configuration options for the "Add Single Row" component for +// the Google Sheets app. Note that the `sheetId` prop is a dynamic prop. +const { dynamicProps } = await pd.reloadComponentProps({ + componentId: { + key: "google_sheets-add-single-row", + }, + configuredProps: { + googleSheets: { + authProvisionId: "apn_V1hMoE7", + }, + sheetId: "1BfWjFF2dTW_YDiLISj5N9nKCUErShgugPS434liyytg", + }, + externalUserId: "jay", +}); + +const { + configurableProps, // The new set of props + id: dynamicPropsId, // The ID of the last prop reconfiguration +} = dynamicProps; + +// Parse and return the data you need +``` + + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. +# This request will fetch the new set of props for the "Add Single Row" component +# for the Google Sheets app. Note that the `sheetId` prop is a dynamic prop. + +echo '{ + "external_user_id": "jay", + "id": "google_sheets-add-single-row", + "configured_props": { + "googleSheets": { + "authProvisionId": "apn_V1hMoE7" + }, + "sheetId": "1BfWjFF2dTW_YDiLISj5N9nKCUErShgugPS434liyytg" + } +}' > data.json + +curl -X POST "https://api.pipedream.com/v1/connect/{your_project_id}/components/props" \ + -H "Authorization: Bearer {access_token}" \ + -H "Content-Type: application/json" \ + -d @data.json +``` + + + + +##### Example response + +```json +{ + "observations": [], + "errors": [], + "dynamicProps": { + "id": "dyp_6xUyVgQ", + "configurableProps": [ + { + "name": "googleSheets", + "type": "app", + "app": "google_sheets" + }, + { + "name": "drive", + "type": "string", + "label": "Drive", + "description": "Defaults to `My Drive`. To select a [Shared Drive](https://support.google.com/a/users/answer/9310351) instead, select it from this list.", + "optional": true, + "default": "My Drive", + "remoteOptions": true + }, + { + "name": "sheetId", + "type": "string", + "label": "Spreadsheet", + "description": "The Spreadsheet ID", + "useQuery": true, + "remoteOptions": true, + "reloadProps": true + }, + { + "name": "worksheetId", + "type": "string[]", + "label": "Worksheet(s)", + "description": "Select a worksheet or enter a custom expression. When referencing a spreadsheet dynamically, you must provide a custom expression for the worksheet.", + "remoteOptions": true, + "reloadProps": true + }, + { + "name": "hasHeaders", + "type": "boolean", + "label": "Does the first row of the sheet have headers?", + "description": "If the first row of your document has headers, we'll retrieve them to make it easy to enter the value for each column. Note: When using a dynamic reference for the worksheet ID (e.g. `{{steps.foo.$return_value}}`), this setting is ignored.", + "reloadProps": true + }, + { + "name": "myColumnData", + "type": "string[]", + "label": "Values", + "description": "Provide a value for each cell of the row. Google Sheets accepts strings, numbers and boolean values for each cell. To set a cell to an empty value, pass an empty string." + } + ] + } +} +``` + +#### Invoke an action + +Invoke an action component for a Pipedream Connect user in a project. + +```text +POST /actions/run +``` + +##### Body parameters + +`id` **string** + +The key that identifies the action component (see the [component structure +table](/workflows/contributing/components/api/#component-structure)). + +--- + +`configured_props` **object** + +The props that have already been configured for the component. This is a +JSON-serializable object with the prop names as keys and the configured values +as values. + +--- + +`external_user_id` **string** + +[The external user ID](/connect/api/#external-users) in your system on behalf of +which you want to execute the action. + +--- + +`dynamic_props_id` **string** (_optional_) + +The ID of the last prop reconfiguration (if applicable). + +##### Examples + + + + +```typescript +import { + createBackendClient, + type BackendClient, + type BackendClientOpts, + type RunActionOpts, + type RunActionResponse, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}; +const pd: BackendClient = createBackendClient(clientOpts); + +// Run the "List Commits" action for the Gitlab app +const requestOpts: RunActionOpts = { + actionId: { + key: "gitlab-list-commits", + }, + configuredProps: { + gitlab: { + authProvisionId: "apn_kVh9AoD", + }, + projectId: 45672541, + refName: "main" + }, + externalUserId: "jverce", +}; +const response: RunActionResponse = await pd.runAction(requestOpts); + +const { + exports, // The named exports produced by the action + os, // The observations produced by the action + ret, // The value returned by the action +}: { + exports: unknown, + os: unknown[], + ret: unknown, +} = response; +``` + + + +```javascript +import { createBackendClient } from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +// Run the "List Commits" action for the Gitlab app +const { + exports, // The named exports produced by the action + os, // The observations produced by the action + ret, // The value returned by the action +} = await pd.runAction({ + actionId: { + key: "gitlab-list-commits", + }, + configuredProps: { + gitlab: { + authProvisionId: "apn_kVh9AoD", + }, + projectId: 45672541, + refName: "main" + }, + externalUserId: "jverce", +}); + +// Parse and return the data you need +``` + + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. +# This request will run the "List Commits" action for the Gitlab app. + +echo '{ + "external_user_id": "jverce", + "id": "gitlab-list-commits", + "configured_props": { + "gitlab": { + "authProvisionId": "apn_kVh9AoD" + }, + "projectId": 45672541, + "refName": "main" + } +}' > data.json + +curl -X POST "https://api.pipedream.com/v1/connect/{your_project_id}/actions/run" \ + -H "Authorization: Bearer {access_token}" \ + -H "Content-Type: application/json" \ + -d @data.json +``` + + + + +##### Example response + +```json +{ + "exports": { + "$summary": "Retrieved 1 commit" + }, + "os": [], + "ret": [ + { + "id": "387262aea5d4a6920ac76c1e202bc9fd0841fea5", + "short_id": "387262ae", + "created_at": "2023-05-03T03:03:25.000+00:00", + "parent_ids": [], + "title": "Initial commit", + "message": "Initial commit", + "author_name": "Jay Vercellone", + "author_email": "nope@pipedream.com", + "authored_date": "2023-05-03T03:03:25.000+00:00", + "committer_name": "Jay Vercellone", + "committer_email": "nope@pipedream.com", + "committed_date": "2023-05-03T03:03:25.000+00:00", + "trailers": {}, + "extended_trailers": {}, + "web_url": "https://gitlab.com/jverce/foo-massive-10231-1/-/commit/387262aea5d4a6920ac76c1e202bc9fd0841fea5" + } + ] +} +``` + +#### Deploy a trigger + +Deploy a trigger component that will emit events to a webhook or workflow for a Pipedream Connect user. + +```text +POST /triggers/deploy +``` + +##### Body parameters + +`id` **string** + +The key that identifies the action component (see the [component structure +table](/workflows/contributing/components/api/#component-structure)). + +--- + +`configured_props` **object** + +The props that have already been configured for the component. This is a +JSON-serializable object with the prop names as keys and the configured values +as values. + +--- + +`external_user_id` **string** + +[The external user ID](/connect/api/#external-users) in your system on behalf of +which you want to execute the action. + +--- + +`webhook_url` **string** (_optional_) + +The URL to which the trigger will send events. + +--- + +`workflow_url` **string** (_optional_) + +The Pipedream workflow ID to which you want to emit events (ex, `p_1234567`). + + +The workflow must be in the same Pipedream project as the trigger. + + +--- + +`dynamic_props_id` **string** (_optional_) + +The ID of the last prop reconfiguration (if applicable). + +##### Examples + + + + +```typescript +import { + createBackendClient, + type BackendClient, + type BackendClientOpts, + type DeployTriggerOpts, + type DeployTriggerResponse, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}; +const pd: BackendClient = createBackendClient(clientOpts); + +// Deploy the "New Issue (Instant)" trigger for the Gitlab app +const requestOpts: DeployTriggerOpts = { + triggerId: { + key: "gitlab-new-issue", + }, + configuredProps: { + gitlab: { + authProvisionId: "apn_kVh9AoD", + }, + projectId: 45672541, + }, + externalUserId: "jverce", + webhookUrl: "https://events.example.com/gitlab-new-issue", +}; +const response: DeployTriggerResponse = await pd.deployTrigger(requestOpts); + +const { + id: triggerId, // The unique ID of the deployed trigger + name: triggerName, // The name of the deployed trigger + owner_id: userId, // The unique ID in Pipedream of your user +}: { + id: string, + name: string, + owner_id: string, +} = response.data; +``` + + + +```javascript +import { createBackendClient } from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +// Deploy the "New Issue (Instant)" trigger for the Gitlab app +const { data: deployedTrigger } = await pd.deployTrigger({ + triggerId: { + key: "gitlab-new-issue", + }, + configuredProps: { + gitlab: { + authProvisionId: "apn_kVh9AoD", + }, + projectId: 45672541, + }, + externalUserId: "jverce", + webhookUrl: "https://events.example.com/gitlab-new-issue", +}); + +const { + id: triggerId, // The unique ID of the deployed trigger + name: triggerName, // The name of the deployed trigger + owner_id: userId, // The unique ID in Pipedream of your user +} = deployedTrigger; + +// Parse and return the data you need +``` + + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. +# This request will deploy the "New Issue (Instant)" trigger for the Gitlab app. + +echo '{ + "external_user_id": "jverce", + "id": "gitlab-new-issue", + "configured_props": { + "gitlab": { + "authProvisionId": "apn_kVh9AoD" + }, + "projectId": 45672541, + }, + "webhook_url": "https://events.example.com/gitlab-new-issue" +}' > data.json + +curl -X POST "https://api.pipedream.com/v1/connect/{your_project_id}/triggers/deploy" \ + -H "Authorization: Bearer {access_token}" \ + -H "Content-Type: application/json" \ + -d @data.json +``` + + + + +##### Example response + +```json +{ + "data": { + "id": "dc_dAuGmW7", + "owner_id": "exu_oedidz", + "component_id": "sc_3vijzQr", + "configurable_props": [ + { + "name": "gitlab", + "type": "app", + "app": "gitlab" + }, + { + "name": "db", + "type": "$.service.db" + }, + { + "name": "http", + "type": "$.interface.http", + "customResponse": true + }, + { + "name": "projectId", + "type": "integer", + "label": "Project ID", + "description": "The project ID, as displayed in the main project page", + "remoteOptions": true + } + ], + "configured_props": { + "gitlab": { + "authProvisionId": "apn_kVh9AoD" + }, + "db": { + "type": "$.service.db" + }, + "http": { + "endpoint_url": "https://xxxxxxxxxx.m.pipedream.net" + }, + "projectId": 45672541 + }, + "active": true, + "created_at": 1734028283, + "updated_at": 1734028283, + "name": "My first project - exu_oedidz", + "name_slug": "my-first-project---exu-oedidz-2" + } +} +``` + +#### List deployed triggers + +List all of the deployed triggers for a given user. + +```text +GET /deployed-triggers/ +``` + +##### Query parameters + +`external_user_id` **string** + +[The external user ID](/connect/api/#external-users) in your system on behalf of +which you want to deploy the trigger. + + +##### Examples + + + + +```typescript +import { + createBackendClient, + type BackendClient, + type BackendClientOpts, + type GetTriggersOpts, + type GetTriggersResponse, + type V1DeployedComponent, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}; +const pd: BackendClient = createBackendClient(clientOpts); + +// List all deployed triggers for the specified user +const requestOpts: GetTriggersOpts = { + externalUserId: "jverce", +}; +const response: GetTriggersResponse = await pd.getTriggers(requestOpts); + +const { + data: triggers, // The list of deployed triggers +}: { + data: V1DeployedComponent[], +} = response; +``` + + + +```javascript +import { + createBackendClient, +} from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +// List all deployed triggers for the specified user +const { data: triggers } = await pd.getTriggers({ + externalUserId: "jverce" +}); + +// Parse and return the data you need +``` + + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. +# This request will list all deployed triggers for the specified user. + +curl -X GET \ + -G \ + "https://api.pipedream.com/v1/connect/{your_project_id}/deployed-triggers" \ + -H "Authorization: Bearer {access_token}" \ + -H "Content-Type: application/json" \ + -H "x-pd-environment: development" \ + -d "external_user_id={external_user_id}" +``` + + + +##### Example response + +```json +{ + "page_info": { + "total_count": 2, + "count": 2, + "start_cursor": "ZGNfZ3p1bUsyZQ", + "end_cursor": "ZGNfdjN1QllYZw" + }, + "data": [ + { + "id": "dc_gzumK2e", + "owner_id": "exu_2LniLm", + "component_id": "sc_r1ixBpL", + "configurable_props": [ + { + "name": "googleDrive", + "type": "app", + "app": "google_drive" + }, + { + "name": "db", + "type": "$.service.db" + }, + { + "name": "http", + "type": "$.interface.http" + }, + { + "name": "drive", + "type": "string", + "label": "Drive", + "description": "Defaults to My Drive. To select a [Shared Drive](https://support.google.com/a/users/answer/9310351) instead, select it from this list.", + "optional": false, + "default": "My Drive", + "remoteOptions": true + }, + { + "name": "timer", + "label": "Push notification renewal schedule", + "description": "The Google Drive API requires occasional renewal of push notification subscriptions. **This runs in the background, so you should not need to modify this schedule**.", + "type": "$.interface.timer", + "static": { + "intervalSeconds": 82080 + } + }, + { + "name": "folders", + "type": "string[]", + "label": "Folders", + "description": "(Optional) The folders you want to watch. Leave blank to watch for any new file in the Drive.", + "optional": true, + "default": [], + "remoteOptions": true + } + ], + "configured_props": { + "googleDrive": { + "authProvisionId": "apn_V1hMeLM" + }, + "db": { + "type": "$.service.db" + }, + "http": { + "endpoint_url": "https://xxxxxxxxxx.m.pipedream.net" + }, + "drive": "My Drive", + "timer": { + "cron": null, + "interval_seconds": 82080 + } + }, + "active": true, + "created_at": 1733512889, + "updated_at": 1733512889, + "name": "Danny Connect - exu_2LniLm", + "name_slug": "danny-connect---exu-2-lni-lm-3" + }, + { + "id": "dc_K0u2OEQ", + "owner_id": "exu_2LniLm", + "component_id": "sc_ogiRveN", + "configurable_props": [ + { + "name": "app", + "type": "app", + "app": "basecamp" + }, + { + "name": "db", + "type": "$.service.db" + }, + { + "name": "accountId", + "type": "string", + "label": "Account Id", + "description": "The ID of the account.", + "remoteOptions": true + }, + { + "name": "projectId", + "type": "string", + "label": "Project Id", + "description": "The ID of the project.", + "remoteOptions": true + }, + { + "name": "http", + "type": "$.interface.http" + } + ], + "configured_props": { + "app": { + "authProvisionId": "apn_EOh4dP0" + }, + "db": { + "type": "$.service.db" + }, + "accountId": { + "__lv": { + "label": "Pipedream", + "value": 5871996 + } + }, + "projectId": { + "__lv": { + "label": "Getting Started", + "value": 39996142 + } + }, + "http": { + "endpoint_url": "https://xxxxxxxxxx.m.pipedream.net" + } + }, + "active": true, + "created_at": 1733198039, + "updated_at": 1733198039, + "name": "Danny Connect - exu_2LniLm", + "name_slug": "danny-connect---exu-2-lni-lm-1" + } + ] +} +``` + +#### Retrieve a deployed trigger + +Retrieve a single deployed trigger for a given user. + +```text +GET /deployed-triggers/{deployed_component_id}/ +``` + +##### Path parameters + +`dcid` **string** + +The deployed component ID for the trigger you'd like to retrieve. + +##### Query parameters + +`external_user_id` **string** + +[The external user ID](/connect/api/#external-users) in your system on behalf of +which you want to deploy the trigger. + + +##### Examples + + + + +```typescript +import { + createBackendClient, + type BackendClient, + type BackendClientOpts, + type GetTriggerOpts, + type GetTriggerResponse, + type V1DeployedComponent, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}; +const pd: BackendClient = createBackendClient(clientOpts); + +// Retrieve the deployed trigger for the specified user +const requestOpts: GetTriggerOpts = { + id: "dc_gzumK2e", + externalUserId: "jverce", +}; +const response: GetTriggerResponse = await pd.getTrigger(requestOpts); + +const { + data: trigger, // The deployed trigger +}: { + data: V1DeployedComponent, +} = response; +``` + + + +```javascript +import { + createBackendClient, +} from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +// Retrieve the deployed trigger for the specified user +const { data: trigger } = await pd.getTrigger({ + id: "dc_gzumK2e", + externalUserId: "jverce" +}); + +// Parse and return the data you need +``` + + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. +# This request will list all deployed triggers for the specified user. + +curl -X GET \ + -G \ + "https://api.pipedream.com/v1/connect/{your_project_id}/deployed-triggers/{dcid}/" \ + -H "Authorization: Bearer {access_token}" \ + -H "Content-Type: application/json" \ + -H "x-pd-environment: development" \ + -d "external_user_id={external_user_id}" +``` + + + +##### Example response + +```json +{ + "id": "dc_gzumK2e", + "owner_id": "exu_2LniLm", + "component_id": "sc_r1ixBpL", + "configurable_props": [ + { + "name": "googleDrive", + "type": "app", + "app": "google_drive" + }, + { + "name": "db", + "type": "$.service.db" + }, + { + "name": "http", + "type": "$.interface.http" + }, + { + "name": "drive", + "type": "string", + "label": "Drive", + "description": "Defaults to My Drive. To select a [Shared Drive](https://support.google.com/a/users/answer/9310351) instead, select it from this list.", + "optional": false, + "default": "My Drive", + "remoteOptions": true + }, + { + "name": "timer", + "label": "Push notification renewal schedule", + "description": "The Google Drive API requires occasional renewal of push notification subscriptions. **This runs in the background, so you should not need to modify this schedule**.", + "type": "$.interface.timer", + "static": { + "intervalSeconds": 82080 + } + }, + { + "name": "folders", + "type": "string[]", + "label": "Folders", + "description": "(Optional) The folders you want to watch. Leave blank to watch for any new file in the Drive.", + "optional": true, + "default": [], + "remoteOptions": true + } + ], + "configured_props": { + "googleDrive": { + "authProvisionId": "apn_V1hMeLM" + }, + "db": { + "type": "$.service.db" + }, + "http": { + "endpoint_url": "https://xxxxxxxxxx.m.pipedream.net" + }, + "drive": "My Drive", + "timer": { + "cron": null, + "interval_seconds": 82080 + } + }, + "active": true, + "created_at": 1733512889, + "updated_at": 1733512889, + "name": "Danny Connect - exu_2LniLm", + "name_slug": "danny-connect---exu-2-lni-lm-3" +} +``` + +#### Delete a deployed trigger + +Delete deployed trigger for a given user. + +```text +DELETE /deployed-triggers/{deployed_component_id}/ +``` + +##### Path parameters + +`dcid` **string** + +The deployed component ID for the trigger you'd like to retrieve. + +##### Query parameters + +`external_user_id` **string** + +[The external user ID](/connect/api/#external-users) in your system on behalf of +which you want to deploy the trigger. + + +##### Examples + + + + +```typescript +import { + createBackendClient, + type BackendClient, + type BackendClientOpts, + type DeleteTriggerOpts, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}; +const pd: BackendClient = createBackendClient(clientOpts); + +// Delete the deployed trigger for the specified user. +const requestOpts: DeleteTriggerOpts = { + id: "dc_gzumK2e", + externalUserId: "jverce", +}; + +// The method doesn't return any data. +await pd.deleteTrigger(requestOpts); +``` + + + +```javascript +import { + createBackendClient, +} from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +// Delete the deployed trigger for the specified user. The method doesn't return +// any data. +await pd.deleteTrigger({ + id: "dc_gzumK2e", + externalUserId: "jverce" +}); +``` + + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. +# This request will list all deployed triggers for the specified user. + +curl -X DELETE \ + -G \ + "https://api.pipedream.com/v1/connect/{your_project_id}/deployed-triggers/{dcid}/" \ + -H "Authorization: Bearer {access_token}" \ + -H "Content-Type: application/json" \ + -H "x-pd-environment: development" \ + -d "external_user_id={external_user_id}" +``` + + + +##### Response + +Pipedream returns a `204 No Content` response on successful deletion + + +#### Retrieve the events emitted by a deployed trigger + +Retrieve a list of the last events that a deployed trigger emitted. + +```text +GET /deployed-triggers/{deployed_component_id}/events/ +``` + +##### Path parameters + +`dcid` **string** + +The deployed component ID for the trigger you'd like to retrieve. + +##### Query parameters + +`external_user_id` **string** + +[The external user ID](/connect/api/#external-users) in your system on behalf of +which you want to deploy the trigger. + +`n` **number** (_optional_) + +The number of events to retrieve. Defaults to 10, and it's capped at 100. + + +##### Examples + + + + +```typescript +import { + createBackendClient, + type BackendClient, + type BackendClientOpts, + type GetTriggerEventsOpts, + type GetTriggerEventsResponse, + type V1EmittedEvent, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}; +const pd: BackendClient = createBackendClient(clientOpts); + +// List the last 10 events emitted by the deployed trigger of the specified user +const requestOpts: GetTriggerEventsOpts = { + id: "dc_gzumK2e", + externalUserId: "jverce", +}; +const response: GetTriggerEventsResponse = await pd.getTriggerEvents(requestOpts); + +const { + data: events, // The list of events emitted by the trigger +}: { + data: V1EmittedEvent[], +} = response; +``` + + + +```javascript +import { + createBackendClient, +} from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +// List the last 10 events emitted by the deployed trigger of the specified user +const { data: events } = await pd.getTriggerEvents({ + id: "dc_gzumK2e", + externalUserId: "jverce" +}); + +// Parse and return the data you need +``` + + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. +# This request will list all events emitted by the specified deployed trigger. + +curl -X GET \ + -G \ + "https://api.pipedream.com/v1/connect/{your_project_id}/deployed-triggers/{dcid}/events" \ + -H "Authorization: Bearer {access_token}" \ + -H "Content-Type: application/json" \ + -H "x-pd-environment: development" \ + -d "external_user_id={external_user_id}" +``` + + + +##### Example response + +```json +{ + "data": [ + { + "e": { + "method": "PUT", + "path": "/", + "query": {}, + "client_ip": "127.0.0.1", + "url": "http://6c367a3dcffce4d01a7b691e906f8982.m.d.pipedream.net/", + "headers": { + "host": "6c367a3dcffce4d01a7b691e906f8982.m.d.pipedream.net", + "connection": "close", + "user-agent": "curl/8.7.1", + "accept": "*/*" + } + }, + "k": "emit", + "ts": 1737155977519, + "id": "1737155977519-0" + }, + { + "e": { + "method": "PUT", + "path": "/", + "query": {}, + "client_ip": "127.0.0.1", + "url": "http://6c367a3dcffce4d01a7b691e906f8982.m.d.pipedream.net/", + "headers": { + "host": "6c367a3dcffce4d01a7b691e906f8982.m.d.pipedream.net", + "connection": "close", + "user-agent": "curl/8.7.1", + "accept": "*/*" + } + }, + "k": "emit", + "ts": 1737062972013, + "id": "1737062972013-0" + }, + { + "e": { + "method": "POST", + "path": "/", + "query": {}, + "client_ip": "127.0.0.1", + "url": "http://6c367a3dcffce4d01a7b691e906f8982.m.d.pipedream.net/", + "headers": { + "host": "6c367a3dcffce4d01a7b691e906f8982.m.d.pipedream.net", + "connection": "close", + "user-agent": "curl/8.7.1", + "accept": "*/*" + } + }, + "k": "emit", + "ts": 1737047546217, + "id": "1737047546217-0" + }, + { + "e": { + "method": "GET", + "path": "/", + "query": {}, + "client_ip": "127.0.0.1", + "url": "http://6c367a3dcffce4d01a7b691e906f8982.m.d.pipedream.net/", + "headers": { + "host": "6c367a3dcffce4d01a7b691e906f8982.m.d.pipedream.net", + "connection": "close", + "user-agent": "curl/8.7.1", + "accept": "*/*" + } + }, + "k": "emit", + "ts": 1736985883016, + "id": "1736985883016-0" + } + ] +} +``` + +#### Retrieve the webhooks listening to a deployed trigger + +Retrieve the list of webhook URLs listening to a deployed trigger. + +```text +GET /deployed-triggers/{deployed_component_id}/webhooks/ +``` + +##### Path parameters + +`dcid` **string** + +The deployed component ID for the trigger you'd like to retrieve. + +##### Query parameters + +`external_user_id` **string** + +[The external user ID](/connect/api/#external-users) in your system on behalf of +which you want to deploy the trigger. + + +##### Examples + + + + +```typescript +import { + createBackendClient, + type BackendClient, + type BackendClientOpts, + type GetTriggerWebhooksOpts, + type GetTriggerWebhooksResponse, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}; +const pd: BackendClient = createBackendClient(clientOpts); + +// Retrieve the list of webhook URLs listening to the deployed trigger for the +// specified user +const requestOpts: GetTriggerWebhooksOpts = { + id: "dc_gzumK2e", + externalUserId: "jverce", +}; +const response: GetTriggerWebhooksResponse = await pd.getTriggerWebhooks(requestOpts); + +const { + webhook_urls: urls, // The list of webhook URLs listening to the deployed trigger +}: { + webhook_urls: string[], +} = response; +``` + + + +```javascript +import { + createBackendClient, +} from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +// Retrieve the list of webhook URLs listening to the deployed trigger for the +// specified user +const { webhook_urls: urls } = await pd.getTriggerWebhooks({ + id: "dc_gzumK2e", + externalUserId: "jverce" +}); +``` + + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. +# This request will list all webhook URLs listening to the specified deployed trigger. + +curl -X GET \ + -G \ + "https://api.pipedream.com/v1/connect/{your_project_id}/deployed-triggers/{dcid}/webhooks/" \ + -H "Authorization: Bearer {access_token}" \ + -H "Content-Type: application/json" \ + -H "x-pd-environment: development" \ + -d "external_user_id={external_user_id}" +``` + + + +##### Example response + +```json +{ + "webhook_urls": [ + "https://events.example.com/gitlab-new-issue", + ] +} +``` + +#### Update the webhooks listening to a deployed trigger + +Update the list of webhook URLs that will listen to a deployed trigger. + +```text +PUT /deployed-triggers/{deployed_component_id}/webhooks/ +``` + +##### Path parameters + +`dcid` **string** + +The deployed component ID for the trigger you'd like to retrieve. + +##### Query parameters + +`external_user_id` **string** + +[The external user ID](/connect/api/#external-users) in your system on behalf of +which you want to deploy the trigger. + +##### Body parameters + +`webhook_urls` **string[]** + +The list of webhook URLs that will listen to the deployed trigger. + +##### Examples + + + + +```typescript +import { + createBackendClient, + type BackendClient, + type BackendClientOpts, + type UpdateTriggerWebhooksOpts, + type UpdateTriggerWebhooksResponse, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}; +const pd: BackendClient = createBackendClient(clientOpts); + +// The new list of webhook URLs that will listen to the deployed trigger +const webhookUrls: string[] = [ + "https://events.example.com/gitlab-new-issue", +]; + +const requestOpts: UpdateTriggerWebhooksOpts = { + id: "dc_gzumK2e", + externalUserId: "jverce", + webhookUrls, +}; + +// Update the list of webhook URLs listening to the deployed trigger +const response: UpdateTriggerWebhooksResponse = await pd.updateTriggerWebhooks(requestOpts); + +// `webhookUrls` will match `confirmedUrls` if the update was successful +const { + webhook_urls: confirmedUrls, +}: { + webhook_urls: string[], +} = response; +``` + + + +```javascript +import { + createBackendClient, +} from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +// The new list of webhook URLs that will listen to the deployed trigger +const webhookUrls = [ + "https://events.example.com/gitlab-new-issue", +]; + +// Update the list of webhook URLs listening to the deployed trigger +const { webhook_urls: confirmedUrls } = await pd.updateTriggerWebhooks({ + id: "dc_gzumK2e", + externalUserId: "jverce", + webhookUrls, +}); + +// `webhookUrls` will match `confirmedUrls` if the update was successful +``` + + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. +# This request will update the list of webhook URLs listening to the specified deployed trigger. + +curl -X PUT \ + "https://api.pipedream.com/v1/connect/{your_project_id}/deployed-triggers/{dcid}/webhooks/" \ + -H "Authorization: Bearer {access_token}" \ + -H "Content-Type: application/json" \ + -H "x-pd-environment: development" \ + -d '{"external_user_id": "{external_user_id}", "webhook_urls": ["https://events.example.com/gitlab-new-issue"]}' +``` + + + +##### Example response + +```json +{ + "webhook_urls": [ + "https://events.example.com/gitlab-new-issue", + ] +} +``` + +#### Retrieve the workflows listening to a deployed trigger + +Retrieve the list of workflow IDs listening to a deployed trigger. + +```text +GET /deployed-triggers/{deployed_component_id}/workflows/ +``` + +##### Path parameters + +`dcid` **string** + +The deployed component ID for the trigger you'd like to retrieve. + +##### Query parameters + +`external_user_id` **string** + +[The external user ID](/connect/api/#external-users) in your system on behalf of +which you want to deploy the trigger. + + +##### Examples + + + + +```typescript +import { + createBackendClient, + type BackendClient, + type BackendClientOpts, + type GetTriggerWorkflowsOpts, + type GetTriggerWorkflowsResponse, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}; +const pd: BackendClient = createBackendClient(clientOpts); + +// Retrieve the list of workflow IDs listening to the deployed trigger for the +// specified user +const requestOpts: GetTriggerWorkflowsOpts = { + id: "dc_gzumK2e", + externalUserId: "jverce", +}; +const response: GetTriggerWorkflowsResponse = await pd.getTriggerWorkflows(requestOpts); + +const { + workflow_ids: ids, // The list of workflow IDs listening to the deployed trigger +}: { + workflow_ids: string[], +} = response; +``` + + + +```javascript +import { + createBackendClient, +} from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +// Retrieve the list of workflow IDs listening to the deployed trigger for the +// specified user +const { workflow_ids: ids } = await pd.getTriggerWorkflows({ + id: "dc_gzumK2e", + externalUserId: "jverce" +}); +``` + + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. +# This request will list all workflow IDs listening to the specified deployed trigger. + +curl -X GET \ + -G \ + "https://api.pipedream.com/v1/connect/{your_project_id}/deployed-triggers/{dcid}/workflows/" \ + -H "Authorization: Bearer {access_token}" \ + -H "Content-Type: application/json" \ + -H "x-pd-environment: development" \ + -d "external_user_id={external_user_id}" +``` + + + +##### Example response + +```json +{ + "workflow_ids": [ + "p_ERRCzw", + "p_2LniLm", + ] +} +``` + +#### Update the workflows listening to a deployed trigger + +Update the list of workflows that will listen to a deployed trigger. + +```text +PUT /deployed-triggers/{deployed_component_id}/workflows/ +``` + +##### Path parameters + +`dcid` **string** + +The deployed component ID for the trigger you'd like to retrieve. + +##### Query parameters + +`external_user_id` **string** + +[The external user ID](/connect/api/#external-users) in your system on behalf of +which you want to deploy the trigger. + +##### Body parameters + +`workflow_ids` **string[]** + +The list of workflow IDs that will listen to the deployed trigger. + +##### Examples + + + + +```typescript +import { + createBackendClient, + type BackendClient, + type BackendClientOpts, + type UpdateTriggerWorkflowsOpts, + type UpdateTriggerWorkflowsResponse, +} from "@pipedream/sdk/server"; + +const clientOpts: BackendClientOpts = { + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}", +}; +const pd: BackendClient = createBackendClient(clientOpts); + +// The new list of workflow IDs that will listen to the deployed trigger +const workflowIds: string[] = [ + "p_ERRCzw", + "p_2LniLm", +]; +const requestOpts: UpdateTriggerWorkflowsOpts = { + id: "dc_gzumK2e", + externalUserId: "jverce", + workflowIds, +}; + +// Update the list of workflows listening to the deployed trigger +const response: UpdateTriggerWorkflowsResponse = await pd.updateTriggerWorkflows(requestOpts); + +// `workflowIds` will match `confirmedIds` if the update was successful +const { + workflow_ids: confirmedIds, +}: { + workflow_ids: string[], +} = response; +``` + + + +```javascript +import { + createBackendClient, +} from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +// The new list of workflow IDs that will listen to the deployed trigger +const workflowIds = [ + "p_ERRCzw", + "p_2LniLm", +]; + +// Update the list of workflows listening to the deployed trigger +const { workflow_ids: confirmedIds } = await pd.updateTriggerWorkflows({ + id: "dc_gzumK2e", + externalUserId: "jverce", + workflowIds, +}); + +// `workflowIds` will match `confirmedIds` if the update was successful +``` + + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. +# This request will update the list of webhook URLs listening to the specified deployed trigger. + +curl -X PUT \ + "https://api.pipedream.com/v1/connect/{your_project_id}/deployed-triggers/{dcid}/webhooks/" \ + -H "Authorization: Bearer {access_token}" \ + -H "Content-Type: application/json" \ + -H "x-pd-environment: development" \ + -d '{"external_user_id": "{external_user_id}", "workflow_ids": ["p_ERRCzw", "p_2LniLm"]}' +``` + + + +##### Example response + +```json +{ + "workflow_ids": [ + "p_ERRCzw", + "p_2LniLm", + ] +} +``` diff --git a/docs-v2/pages/connect/components.mdx b/docs-v2/pages/connect/components.mdx new file mode 100644 index 0000000000000..641a6693c50cd --- /dev/null +++ b/docs-v2/pages/connect/components.mdx @@ -0,0 +1,814 @@ +import { Steps, Tabs } from 'nextra/components' +import Callout from '@/components/Callout' + +# Pre-built tools for your app or agent + +Pipedream Connect provides APIs to embed pre-built tools ([triggers and actions](/workflows/contributing/components/)) directly in your application +or AI agent, enabling access to 10,000+ built-in API operations. Enable [your end users](/connect/api/#external-users) to +configure, deploy, and invoke Pipedream triggers and actions for more than {process.env.PUBLIC_APPS} APIs. + +## What are triggers and actions? + +In Pipedream, we call triggers and actions [components](/workflows/contributing/components/), which are self-contained executable units of code. Your end users configure the inputs and these components produce a +result that's exported as output. These components are developed and maintained by Pipedream +and our community and their source code is available in our [public Github repo](https://github.com/PipedreamHQ/pipedream/tree/master/components). + +## Implementation + +### Use Pipedream's frontend SDK +- Pipedream provides a frontend React SDK to enable your users to configure and run triggers and actions in your app's UI +- Style the UI components however you want to match the design of your app, and you can also fork the SDK +- Refer to the [SDK](https://github.com/PipedreamHQ/pipedream/blob/master/packages/connect-react/README.md) to get started + + +Check out the [public demo app](https://pdrm.co/connect) to see the API and SDK in action. You can also [run it locally and explore the code](https://github.com/PipedreamHQ/pipedream-connect-examples/tree/master/connect-react-demo). + + +### Use your own frontend +- See below to get started with the REST API +- Refer to the [full API reference](/connect/api/#components) for supported SDK methods as well + +## Getting started + + +Refer to the [Connect API docs](/connect/api/) for the full API reference. Below is a quickstart with a few specific examples. + +You can skip steps 1 and 2 if you already know the component you want to use or if you'd prefer to [pass a natural language prompt to Pipedream's component search API](/rest-api/#search-for-registry-components). + + + + +### Authenticate to the Pipedream API + +Before sending requests to the API, make sure to [authenticate using a Pipedream OAuth client](/rest-api/auth/#oauth): + + + +```javascript +// Initialize the Pipedream client with the SDK + +import { createBackendClient } from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development | production", + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); +``` + + +```bash +# Get an access token for the REST API + +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{your_oauth_client_id}", + "client_secret": "{your_oauth_client_secret}" + }' +``` + + + + +All subsequent examples assume that you've either initialized the SDK client or have a valid access token. + + +### Find the app you want to use + +To find the right trigger or action to configure and run, first find the app. In this example, we'll search for `gitlab`. + + + +```javascript +const apps = await pd.getApps({ q: "gitlab" }); + +// Parse and return the data you need +``` + + +```bash +curl -X https://api.pipedream.com/v1/apps?q=gitlab \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer {access_token}" + +# Parse and return the data you need +``` + + + +Here's the response: + +```json +{ + "page_info": { + "total_count": 1, + "count": 1, + "start_cursor": "Z2l0bGFi", + "end_cursor": "Z2l0bGFi" + }, + "data": [ + { + "id": "app_1Z2hw1", + "name_slug": "gitlab", + "name": "GitLab", + "auth_type": "oauth", + "description": "GitLab is the most comprehensive AI-powered DevSecOps Platform.", + "img_src": "https://assets.pipedream.net/s.v0/app_1Z2hw1/logo/orig", + "custom_fields_json": "[{\"name\":\"base_api_url\",\"label\":\"Base API URL\",\"description\":\"The Base API URL defaults to `gitlab.com`. If you are using self-hosted Gitlab, enter your base url here, e.g. `gitlab.example.com`\",\"default\":\"gitlab.com\",\"optional\":null}]", + "categories": [ + "Developer Tools" + ] + } + ] +} +``` + +### List the available components for the app + +Once you have the app you want to use, now you can list the triggers and/or actions for that app. We'll list the actions for Gitlab and we'll pass the `name_slug` `gitlab` as the `app`. + + + +```javascript +const components = await pd.getComponents({ q: "gitlab" }); + +// Parse and return the data you need +``` + + +```bash +curl -X https://api.pipedream.com/v1/connect/{project_id}/actions?app=gitlab \ + -H "Content-Type: application/json" \ + -H "X-PD-Environment: development" \ + -H "Authorization: Bearer {access_token}" + +# Parse and return the data you need +``` + + + +Here's the response: + +```json +{ + "page_info": { + "total_count": 20, + "count": 10, + "start_cursor": "c2NfbHlpRThkQQ", + "end_cursor": "c2NfQjVpTkJBTA" + }, + "data": [ + { + "name": "List Commits", + "version": "0.0.2", + "key": "gitlab_developer_app-list-commits" + }, + { + "name": "Update Issue", + "version": "0.0.1", + "key": "gitlab_developer_app-update-issue" + }, + { + "name": "Update Epic", + "version": "0.0.1", + "key": "gitlab_developer_app-update-epic" + }, + { + "name": "Search Issues", + "version": "0.0.1", + "key": "gitlab_developer_app-search-issues" + }, + { + "name": "List Repo Branches", + "version": "0.0.1", + "key": "gitlab_developer_app-list-repo-branches" + }, + { + "name": "Get Repo Branch", + "version": "0.0.1", + "key": "gitlab_developer_app-get-repo-branch" + }, + { + "name": "Get Issue", + "version": "0.0.1", + "key": "gitlab_developer_app-get-issue" + }, + { + "name": "Create issue", + "version": "0.0.1", + "key": "gitlab_developer_app-create-issue" + }, + { + "name": "Create Epic", + "version": "0.0.1", + "key": "gitlab_developer_app-create-epic" + }, + { + "name": "Create Branch", + "version": "0.0.1", + "key": "gitlab_developer_app-create-branch" + } + ] +} +``` +### Retrieve the component's definition + +To configure and run a component for your end users, you need to understand +the component's definition. Now that you have the component's key from the previous step, +you can retrieve its structure from the Pipedream API. See the [component +structure](/workflows/contributing/components/api/#component-structure) section in our docs for more +details. + +As an example, the following API call will return the structure of the **List +Commits** action for Gitlab: + + + +```javascript +const component = await pd.getComponent({ key: "gitlab-list-commits" }); + +// Parse and return the data you need +``` + + +```bash +curl -X https://api.pipedream.com/v1/connect/{project_id}/components/gitlab-list-commits \ + -H "Content-Type: application/json" \ + -H "X-PD-Environment: development" \ + -H "Authorization: Bearer {access_token}" + +# Parse and return the data you need +``` + + + +The response will contain the component's structure, including its user-friendly name, +version, and most importantly, the configuration options the component accepts +(also known as [props](/workflows/contributing/components/api/#props) or "properties"). +Here's an example of the response for the component in the example above: + +```json +{ + "data": { + "name": "List Commits", + "version": "0.0.3", + "key": "gitlab-list-commits", + "configurable_props": [ + { + "name": "gitlab", + "type": "app", + "app": "gitlab" + }, + { + "name": "projectId", + "type": "integer", + "label": "Project ID", + "description": "The project ID, as displayed in the main project page", + "remoteOptions": true + }, + { + "name": "refName", + "type": "string", + "label": "Branch Name", + "description": "The name of the branch", + "remoteOptions": true, + "optional": true + }, + { + "name": "max", + "type": "integer", + "label": "Max Results", + "description": "Max number of results to return. Default value is `100`", + "optional": true, + "default": 100 + } + ] + } +} +``` + +Using this information, you can now drive the configuration of the component for +your end users, as described in the next section. + +### Configure the component + +Component execution on behalf of your end users requires a few preliminary +steps, focused on getting the right input parameters (aka +[props](/workflows/building-workflows/using-props/)) to the component. + +Configuring each prop for a component often involves an API call to retrieve the possible values, +unless the values that a prop can take are static or free-form. The endpoint is accessible at: + +Typically, the options for a prop are linked to a specific user's account. Each +of these props implements an `options` method that retrieves the necessary +options from the third-party API, formats them, and sends them back in the +response for the end user to select. Examples are listing Slack channels, Google Sheets, etc. + +The payload for the configuration API call must contain the following fields: + +1. `external_user_id`: the ID of your user on your end +2. `id`: the component's unique ID (aka **key**) +3. `prop_name`: the name of the prop you want to configure +4. `configured_props`: an object containing the props that have already been + configured. The initial configuration call must contain the ID of the account + (aka `authProvisionId`) that your user has connected to the target app (see + [this section](workflows#configure-accounts-to-use-your-end-users-auth) for + more details on how to create these accounts). + +We'll use the [**List Commits** component for +Gitlab](https://github.com/PipedreamHQ/pipedream/blob/master/components/gitlab/actions/list-commits/list-commits.mjs#L4) +as an example, to illustrate a call that retrieves the options for the +`projectId` prop of that component: + + + +```javascript +const { options } = await pd.configureComponent({ + externalUserId: "abc-123", + id: "gitlab-list-commits", + propName: "projectId", + configuredProps: { + gitlab: { + authProvisionId: "apn_kVh9AoD", + } + } +}); + +// Parse and return the data you need +``` + + +```bash +curl -X POST https://api.pipedream.com/v1/connect/{project_id}/components/configure \ + -H "Content-Type: application/json" \ + -H "X-PD-Environment: development" \ + -H "Authorization: Bearer {access_token}" \ + -d '{ + "external_user_id": "abc-123", + "id": "gitlab-list-commits", + "prop_name": "projectId", + "configured_props": { + "gitlab": { + "authProvisionId": "apn_kVh9AoD" + } + } + }' +# Parse and return the data you need +``` + + + + +The response contains the possible values (and their human-readable labels +when applicable) for the prop, as well as any possible errors that might have +occurred. The response for the request above would look like this: + +```json +{ + "observations": [], + "context": null, + "options": [ + { + "label": "jverce/foo-massive-10231-1", + "value": 45672541 + }, + { + "label": "jverce/foo-massive-10231", + "value": 45672514 + }, + { + "label": "jverce/foo-massive-14999-2", + "value": 45672407 + }, + { + "label": "jverce/foo-massive-14999-1", + "value": 45672382 + }, + { + "label": "jverce/foo-massive-14999", + "value": 45672215 + }, + { + "label": "jverce/gitlab-development-kit", + "value": 21220953 + }, + { + "label": "jverce/gitlab", + "value": 21208123 + } + ], + "errors": [], + "timings": { + "api_to_sidekiq": 1734043172355.1042, + "sidekiq_received": 1734043172357.867, + "sidekiq_to_lambda": 1734043172363.6812, + "sidekiq_done": 1734043173461.6406, + "lambda_configure_prop_called": 1734043172462, + "lambda_done": 1734043173455 + }, + "stringOptions": null +} +``` + + + +Fields inside `configured_props` are written in camel case since they refer to +the names of props as they appear in the component's code, they are not +attributes that the API itself consumes. + + + +You configure props one-by-one, making a call to the component configuration API +for each new prop. Subsequent prop configuration calls will be identical to the +one above: + +1. Add the prop you currently want to configure as the `prop_name` +2. Include the names and values of all previously-configured props in the + `configured_props` object. Keep this object in your app's local state, add a + prop once you or the end user selects a value, and pass it to the + `configured_props` API param. + +For example, to retrieve the configuration options for the `refName` prop: + +```json +{ + "async_handle": "IyvFeE5oNpYd", + "external_user_id": "demo-34c13d13-a31e-4a3d-8b63-0ac954671095", + "id": "gitlab-list-commits", + "prop_name": "refName", + "configured_props": { + "gitlab": { + "authProvisionId": "apn_oOhaBlD" + }, + "projectId": 21208123 + } +} +``` + +### Configure dynamic props (optional) + +The set of props that a component can accept might not be static, and may change +depending on the values of prior props. Props that behave this way are called +[dynamic props](/workflows/contributing/components/api/#dynamic-props), and they need to be configured +in a different way. Props that are dynamic will have a `reloadProps` attribute +set to `true` in the component's definition. + +After configuring a dynamic prop, the set of subsequent props must be recomputed +(or reloaded), which is possible using the following API call: + +```text +POST /v1/connect/components/props +``` + +The payload is similar to the one used for the configuration API, but it +excludes the `prop_name` field since the goal of this call is to reload and +retrieve the new set of props, not to configure a specific one. + +Using the [Add Single Row action for Google Sheets](https://pipedream.com/apps/google-sheets/actions/add-single-row) as an example, the +request payload would look like this: + +```json +{ + "async_handle": "PL41Yf3PuX61", + "external_user_id": "demo-25092fa8-86c0-4d46-86c9-9dc9bde3b964", + "id": "google_sheets-add-single-row", + "configured_props": { + "googleSheets": { + "authProvisionId": "apn_V1hMoE7" + }, + "sheetId": "1BfWjFF2dTW_YDiLISj5N9nKCUErShgugPS434liyytg" + } +} +``` + +In this case, the `sheetId` prop is dynamic, and so after configuring it, the +set of props must be reloaded. The response will contain the new set of props +and their definition, similar to when the [component information was first +retrieved](#retrieving-a-components-definition). The response will also contain +an ID that can be used to reference the new set of props in subsequent +configuration calls. If this is ID is not provided, the set of props will be +based on the definition of the component that was retrieved initially. + +To illustrate, the response for the request above would look like this: + +```json +{ + "observations": [], + "errors": [], + "dynamicProps": { + "id": "dyp_6xUyVgQ", + "configurableProps": [ + { + "name": "googleSheets", + "type": "app", + "app": "google_sheets" + }, + { + "name": "drive", + "type": "string", + "label": "Drive", + "description": "Defaults to `My Drive`. To select a [Shared Drive](https://support.google.com/a/users/answer/9310351) instead, select it from this list.", + "optional": true, + "default": "My Drive", + "remoteOptions": true + }, + { + "name": "sheetId", + "type": "string", + "label": "Spreadsheet", + "description": "The Spreadsheet ID", + "useQuery": true, + "remoteOptions": true, + "reloadProps": true + }, + { + "name": "worksheetId", + "type": "string[]", + "label": "Worksheet(s)", + "description": "Select a worksheet or enter a custom expression. When referencing a spreadsheet dynamically, you must provide a custom expression for the worksheet.", + "remoteOptions": true, + "reloadProps": true + }, + { + "name": "hasHeaders", + "type": "boolean", + "label": "Does the first row of the sheet have headers?", + "description": "If the first row of your document has headers, we'll retrieve them to make it easy to enter the value for each column. Note: When using a dynamic reference for the worksheet ID (e.g. `{{steps.foo.$return_value}}`), this setting is ignored.", + "reloadProps": true + }, + { + "name": "myColumnData", + "type": "string[]", + "label": "Values", + "description": "Provide a value for each cell of the row. Google Sheets accepts strings, numbers and boolean values for each cell. To set a cell to an empty value, pass an empty string." + } + ] + } +} +``` + +### Execution + +Once all the props have been configured, the component can be invoked. Pipedream +supports two types of components: [actions](/workflows/contributing/components/api/#actions) and +[sources](/workflows/contributing/components/api/#sources) (aka triggers). + +Actions are components that perform a task by taking an input either during +[configuration](#configure-the-component) and/or during invocation (usually both), and +produces a result. Sources are very similar, but the difference is that they are +not invoked directly by end users directly, but rather by events that happen on a +third-party service. For example, the "New File" source for Google Drive will be +triggered every time a new file is created in a specific folder in Google Drive, +then will emit an event for you to consume. + +All this means is that actions can be invoked manually on demand, while sources +are instead deployed and run automatically when the event they are listening for +occurs. + +#### Invoking an action + +At the end of the configuration process for an action, you'll end up with a +payload that you can use to invoke the action. The payload is similar to the one +used for configuring a prop, with the exception of the `prop_name` attribute +(because we're not configuring any props at this point): + +```json +{ + "async_handle": "xFfBakdTGTkI", + "external_user_id": "abc-123", + "id": "gitlab-list-commits", + "configured_props": { + "gitlab": { + "authProvisionId": "apn_kVh9AoD" + }, + "projectId": 45672541, + "refName": "main" + } +} +``` + +To run the action with this configuration, simply send it as the request payload +to the following endpoint: + + + +```javascript +const resp = await pd.runAction({ + externalUserId: "abc-123", + id: "gitlab-list-commits", + configuredProps: { + gitlab: { + authProvisionId: "apn_kVh9AoD", + }, + projectId: 45672541, + refName: "main" + } +}); + +// Parse and return the data you need +``` + + +```bash +curl -X POST https://api.pipedream.com/v1/connect/{project_id}/actions/run \ + -H "Content-Type: application/json" \ + -H "X-PD-Environment: development" \ + -H "Authorization: Bearer {access_token}" \ + -d '{ + "external_user_id": "abc-123", + "id": "gitlab-list-commits", + "prop_name": "projectId", + "configured_props": { + "gitlab": { + "authProvisionId": "apn_kVh9AoD" + } + } + }' + +# Parse and return the data you need +``` + + + +The output of executing the action will be a JSON object containing the +following fields: + +1. `exports`: all the named exports produced by the action, like when calling + [`$.export` in a Node.js](/workflows/building-workflows/code/nodejs/#using-export) component. +2. `os`: a list of observations produced by the action (e.g. logs, errors, etc). +3. `ret`: the return value of the action, if any. + +The following (abbreviated) example shows the output of running the action +above: + +```json +{ + "exports": { + "$summary": "Retrieved 1 commit" + }, + "os": [], + "ret": [ + { + "id": "387262aea5d4a6920ac76c1e202bc9fd0841fea5", + "short_id": "387262ae", + "created_at": "2023-05-03T03:03:25.000+00:00", + "parent_ids": [], + "title": "Initial commit", + "message": "Initial commit", + "author_name": "Jay Vercellone", + "author_email": "nope@pipedream.com", + "authored_date": "2023-05-03T03:03:25.000+00:00", + "committer_name": "Jay Vercellone", + "committer_email": "nope@pipedream.com", + "committed_date": "2023-05-03T03:03:25.000+00:00", + "trailers": {}, + "extended_trailers": {}, + "web_url": "https://gitlab.com/jverce/foo-massive-10231-1/-/commit/387262aea5d4a6920ac76c1e202bc9fd0841fea5" + } + ] +} +``` + +#### Deploying a source + +Because sources are exercised by events that happen on a third-party service, +their semantics are different from actions. Once a source is configured, it must +be deployed to start listening for events. When deploying a source, you +can define either a webhook URL or a Pipedream workflow ID to consume those events. + +Deploying a source is done by sending a payload similar to the one used for +running an action, with the addition of the webhook URL or workflow ID. Using +the **New Issue (Instant)** source for Gitlab as an example, the payload would +look something like this: + +```json +{ + "external_user_id": "abc-123", + "id": "gitlab-new-issue", + "prop_name": "http", + "configured_props": { + "gitlab": { + "authProvisionId": "apn_kVh9AoD" + }, + "projectId": 45672541 + }, + "webhook_url": "https://events.example.com/gitlab-new-issue" +} +``` + +Deploy a source for your users: + + + +```javascript +const { data: deployedTrigger } = await pd.deployTrigger({ + externalUserId: "abc-123", + id: "gitlab-new-issue", + propName: "projectId", + configuredProps: { + gitlab: { + authProvisionId: "apn_kVh9AoD", + } + }, + webhookUrl: "https://events.example.com/gitlab-new-issue" +}); + +const { + id: triggerId, // The unique ID of the deployed trigger + name: triggerName, // The name of the deployed trigger + owner_id: userId, // The unique ID in Pipedream of your user +} = deployedTrigger; + +// Parse and return the data you need +``` + + +```bash +curl -X POST https://api.pipedream.com/v1/connect/{project_id}/components/triggers/deploy \ + -H "Content-Type: application/json" \ + -H "X-PD-Environment: development" \ + -H "Authorization: Bearer {access_token}" \ + -d '{ + "external_user_id": "abc-123", + "id": "gitlab-new-issue", + "prop_name": "projectId", + "configured_props": { + "gitlab": { + "authProvisionId": "apn_kVh9AoD" + } + }, + "webhook_url": "https://events.example.com/gitlab-new-issue" + }' +# Parse and return the data you need +``` + + + +If the source deployment succeeds, the response will contain the information +regarding the state of the source, including all the component's props metadata, +as well as their values. It will also contain its name, creation date, owner, +and most importantly its unique ID, which can be used to manage the source in +the future (e.g. delete it). The response for the request above would look like +this: + +```json +{ + "data": { + "id": "dc_dAuGmW7", + "owner_id": "exu_oedidz", + "component_id": "sc_3vijzQr", + "configurable_props": [ + { + "name": "gitlab", + "type": "app", + "app": "gitlab" + }, + { + "name": "db", + "type": "$.service.db" + }, + { + "name": "http", + "type": "$.interface.http", + "customResponse": true + }, + { + "name": "projectId", + "type": "integer", + "label": "Project ID", + "description": "The project ID, as displayed in the main project page", + "remoteOptions": true + } + ], + "configured_props": { + "gitlab": { + "authProvisionId": "apn_kVh9AoD" + }, + "db": { + "type": "$.service.db" + }, + "http": { + "endpoint_url": "https://xxxxxxxxxx.m.pipedream.net" + }, + "projectId": 45672541 + }, + "active": true, + "created_at": 1734028283, + "updated_at": 1734028283, + "name": "My first project - exu_oedidz", + "name_slug": "my-first-project---exu-oedidz-2" + } +} +``` + +In the example above, the source ID is `dc_dAuGmW7`, which can be used to delete, +retrieve, or update the source in the future. + +Refer to the [full Connect API reference](/connect/api#components) for questions and additional examples. + + diff --git a/docs-v2/pages/connect/environments.mdx b/docs-v2/pages/connect/environments.mdx new file mode 100644 index 0000000000000..6daf5b2b3162d --- /dev/null +++ b/docs-v2/pages/connect/environments.mdx @@ -0,0 +1,49 @@ +import Callout from '@/components/Callout' +import Image from 'next/image' + +# Environments + +Pipedream Connect projects support two environments: `development` and `production`. Connected accounts and credentials stored in `development` remain separate from `production`. + + +You can use all of the Connect features in `development` mode **on any plan**. **[Get in touch with our Sales team](https://pipedream.com/pricing?plan=Enterprise)** when you're ready to ship to production. + + + +## How to specify the environment + +You specify the environment when [creating a new Connect token](/connect/api/#create-a-new-token) with the Pipedream SDK or API. When users succesfully connect their account, Pipedream saves the account credentials (API key, access token, etc) for that `external_user_id` in the specified environment. + +Always set the environment when you create the SDK client: + +```typescript +import { createBackendClient } from "@pipedream/sdk/server"; + +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "your-oauth-client-id", + clientSecret: "your-oauth-client-secret", + } +}); +``` + +or pass the `X-PD-Environment` header in HTTP requests: + +```bash +curl -X POST https://api.pipedream.com/v1/connect/{project_id}/tokens \ + -H "Content-Type: application/json" \ + -H "X-PD-Environment: development" \ + -H "Authorization: Bearer {access_token}" \ + -d '{ + "external_user_id": "your-external-user-id" + }' +``` + + +When connecting an account in `development`, make sure you're signed in to pipedream.com in the same browser where you're connecting your account. This is only a requirement for the `development` environment. **You should only use `development` with your own accounts when testing and developing, and not with your end users.** + + +
+ +Connect in development mode diff --git a/docs-v2/pages/connect/index.mdx b/docs-v2/pages/connect/index.mdx new file mode 100644 index 0000000000000..a6018294c854d --- /dev/null +++ b/docs-v2/pages/connect/index.mdx @@ -0,0 +1,68 @@ +import Callout from '@/components/Callout' +import { Steps, Tabs } from 'nextra/components' +import Image from 'next/image' +import VideoPlayer from "@/components/VideoPlayer"; + +# Pipedream Connect + +**Connect provides a developer toolkit that lets you add {process.env.PUBLIC_APPS}+ integrations to your app or AI agent.** You can build AI agents, in-app messaging, CRM syncs, [and much more](/connect/use-cases/), all in a few minutes. You have full, code-level control over how these integrations work in your app. You handle your product, Pipedream simplifies the integration. + +![Connect visualization](https://res.cloudinary.com/pipedreamin/image/upload/v1738731467/pd-connect-viz_cep0uq.png) + +## Use managed auth + +- Handle authorization or accept API keys on behalf of your users, for any of Pipedream's [{process.env.PUBLIC_APPS}+ APIs](https://pipedream.com/apps) +- Use the [Client SDK](https://github.com/PipedreamHQ/pipedream/tree/master/packages/sdk) or [Connect Link](/connect/managed-auth/quickstart/#or-use-connect-link) to accept auth in minutes +- Ship new integrations quickly with Pipedream's approved OAuth clients, or use your own + + +## Act on behalf of your users + +- Retrieve OAuth access tokens and API keys for your end users with Pipedream's [REST API](/connect/api/) +- Add 10k pre-built tools and triggers from {process.env.PUBLIC_APPS}+ APIs to your AI agent or embed them directly in your SaaS app +- Develop and deploy complex multi-step [workflows](/connect/workflows/) in our best-in-class [visual builder](/workflows/building-workflows/) +- Send custom API requests while still avoiding dealing with customer credentials with the [Connect proxy](/connect/api-proxy/) + +{/* Pipedream Connect overview */} + +## Use cases + +Pipedream Connect lets you build any API integration into your product in minutes. Our customers build: + +- **AI products**: Talk to any AI API or LLM, interacting with your users or running AI-driven asynchronous tasks +- **In-app messaging**: Send messages to Slack, Discord, Microsoft Teams, or any app directly from your product. +- **CRM syncs**: Sync data between your app and Salesforce, HubSpot, or any CRM +- **Spreadsheet integrations**: Sync data between your app and Google Sheets, Airtable, or any spreadsheet + +[and much more](/connect/use-cases/). + +## Getting started + +Visit [the managed auth quickstart](/connect/quickstart/) to build your first integration. + +## Plans and pricing + +**Managed authentication with Connect is free to use for up to 1,000 connected accounts for any workspace**. Check out our [pricing page](https://pipedream.com/pricing?plan=Enterprise) to get in touch with our Sales team for details on using the rest of the Connect features in production. + +## Security + +Pipedream takes the security of our products seriously. See [details on Connect security](/privacy-and-security/#pipedream-connect) and [our general security docs](/privacy-and-security/). Please send us any questions or [suspected vulnerabilities](/privacy-and-security/#reporting-a-vulnerability). You can also get a copy of our [SOC 2 Type 2 report](/privacy-and-security/#soc-2), [sign HIPAA BAAs](/privacy-and-security/#hipaa), and get information on other practices and controls. + +### Storing user credentials, token refresh + +All credentials and tokens are sent to Pipedream securely over HTTPS, and encrypted at rest. [See our security docs on credentials](/privacy-and-security/#third-party-oauth-grants-api-keys-and-environment-variables) for more information. + +### How to secure your Connect apps + +- **Secure all secrets** — Secure your Pipedream OAuth client credentials, and especially any user credentials. Never expose secrets in your client-side code. Make all requests to Pipedream's API and third-party APIs from your server-side code. +- **Use HTTPS** — Always use HTTPS to secure your connections between your client and server. Requests to Pipedream's API will be automatically redirected to HTTPS. +- **Use secure, session-based auth between your client and server** — authorize all requests from your client to your server using a secure, session-based auth mechanism. Use well-known identity providers with services like [Clerk](https://clerk.com/), [Firebase](https://firebase.google.com/), or [Auth0](https://auth0.com/) to securely generate and validate authentication tokens. The same follows for Pipedream workflows — if you trigger Pipedream workflows from your client or server, validate all requests in the workflow before executing workflow code. +- **Secure your workflows** — See our [standard security practices](/privacy-and-security/best-practices) for recommendations on securing your Pipedream workflows. + +## Glossary of terms + +- **App**: GitHub, Notion, Slack, Google Sheets, and more. The app is the API you want your users to connect to in your product. See the [full list here](https://pipedream.com/apps). +- **Developer**: This is probably you, the Pipedream customer who's developing an app and wants to use Connect to make API requests on behalf of your end users. +- **End User**: Your customer or user, whose data you want to access on their behalf. End users are identifed via the `external_user_id` param in the Connect SDK and API. +- **Connected Account**: The account your end user connects. [Read more about connected accounts](/integrations/connected-accounts). +- **OAuth Client**: This is admittedly a bit of an overloaded term and refers both to [custom OAuth clients](/connect/managed-auth/oauth-clients/) you create in Pipedream to use when your end users authorize access to their account, as well as [OAuth clients to authenticate to Pipedream's API](/rest-api/auth/#oauth). diff --git a/docs-v2/pages/connect/managed-auth/_meta.tsx b/docs-v2/pages/connect/managed-auth/_meta.tsx new file mode 100644 index 0000000000000..692e9c6c53693 --- /dev/null +++ b/docs-v2/pages/connect/managed-auth/_meta.tsx @@ -0,0 +1,9 @@ +export default { + "quickstart": "Quickstart", + "users": "Users", + "tokens": "Connect Tokens", + "connect-link": "Connect Link", + "oauth-clients": "OAuth Clients", + "webhooks": "Webhooks", + "customization": "Customizing the Auth Flow", +} as const diff --git a/docs-v2/pages/connect/managed-auth/connect-link.mdx b/docs-v2/pages/connect/managed-auth/connect-link.mdx new file mode 100644 index 0000000000000..212213868667a --- /dev/null +++ b/docs-v2/pages/connect/managed-auth/connect-link.mdx @@ -0,0 +1,32 @@ +# Connect Link + +Connect Link provides a Pipedream-hosted link that lets you connect third-party accounts without any frontend implementation in your app. + +## When to use Connect Link + +If you aren't able to execute JavaScript or open an iFrame in your frontend, or you want to send users a URL to connect accounts via email or SMS, use Connect Link. That URL opens a Pipedream-hosted page that guides the user through the account connection flow. The URL is scoped to the specific end user, and expires after 4 hours. + +## How to generate a link + +See [the Connect quickstart](/connect/managed-auth/quickstart/) for a full tutorial for getting Connect up and running. + +Here's a quick overview of how to generate a Connect Link URL: + +1. First, [generate a token](/connect/managed-auth/quickstart/#generate-a-short-lived-token) for your users. +2. Extract the `connect_link_url` from the token response. +3. Before returning the URL to your user, add an `app` parameter to the end of the query string: + +``` +https://pipedream.com/_static/connect.html?token={token}&connectLink=true&app={appSlug} +``` + +4. Redirect your users to this URL, or send it to them via email, SMS, and more. + +**To test this code, check out this workflow:** +[https://pipedream.com/new?h=tch_4RfjXN](https://pipedream.com/new?h=tch_4RfjXN) + +## Success and error redirect URLs + +To automatically redirect users somewhere after they complete the connection flow (or if an error occurs), define the `success_redirect_uri` and `error_redirect_uri` parameters during token creation. [See the API docs](/connect/api/#create-a-new-token) for details. + +In the absence of these URLs, Pipedream will redirect the user to a Pipedream-hosted success or error page at the end of the connection flow. diff --git a/docs-v2/pages/connect/managed-auth/customization.mdx b/docs-v2/pages/connect/managed-auth/customization.mdx new file mode 100644 index 0000000000000..b4f842cf3f0c8 --- /dev/null +++ b/docs-v2/pages/connect/managed-auth/customization.mdx @@ -0,0 +1,39 @@ +import ArcadeEmbed from '@/components/ArcadeEmbed' +import Callout from '@/components/Callout' + +# Customizing the Auth Flow + + + + + +By default, your end users will see a primarly Pipedream branded experience when they connect their account. To customize this screen to highlight your application, you can configure your [app's name](#application-name), [support email](#support-email), and [logo](#logo) in the Pipedream UI. + + +## Customizing your application details + +Open your project in the Pipedream UI: [https://pipedream.com/projects](https://pipedream.com/projects) + +1. Once you've opened your project, click the **Connect** tab in the left sidebar +2. From there, select the **Configuration** tab + +![Pipedream Connect Configuration](https://res.cloudinary.com/pipedreamin/image/upload/v1731045468/connect-app-config_th9fqo.png) + +### Application name +By default, your end users will see: +>We use Pipedream to connect your account + +Enter the name of your application that you'd like to show instead, so it reads: +>\{Application Name\} uses Pipedream to connect your account + +### Support email +In the case of any errors during the account connection flow, by default your users will see: +>Connection failed. Please retry or contact support. + +To give your end users an email address to seek support, enter your support email. We'll display it: +>Connection failed. Please retry or contact support [help@example.com](mailto:help@example.com). + +### Logo +By default we'll show Pipedream's logo alongside the app your user is connecting to. If you'd like to show your own logo instead, upload it here. \ No newline at end of file diff --git a/docs-v2/pages/connect/managed-auth/oauth-clients.mdx b/docs-v2/pages/connect/managed-auth/oauth-clients.mdx new file mode 100644 index 0000000000000..bd66dcbbe0316 --- /dev/null +++ b/docs-v2/pages/connect/managed-auth/oauth-clients.mdx @@ -0,0 +1,42 @@ +import Image from 'next/image' +import Callout from '@/components/Callout' + +# OAuth Clients + +When connecting an account for any OAuth app via Pipedream Connect, we'll default to using Pipedream's official OAuth client, which enables you to quickly get up and running. [Read more about OAuth clients in Pipedream here](/integrations/connected-accounts/oauth-clients). + +## Using Pipedream OAuth + +There are two types of apps in Pipedream: + +1. **Key-based**: These apps require static credentials, like API keys. Pipedream stores these credentials securely and exposes them via API. +2. **OAuth**: These apps require OAuth authorization. Pipedream manages the OAuth flow for these apps, ensuring you always have a fresh access token for requests. + + +For any OAuth app that supports it, **you can always use your own client.** Your ability to use Pipedream's OAuth clients in production depends on the use case. + + +
+ +
+| Operation | Details | Environment | +|--------|---------|------------------------------| +| Retrieve user credentrials | [Fetch the credentials](/connect/api#accounts) for your end user from Pipedream's API to use in your app | ✅ `development`
❌ `production` | +| Invoke workflows | [Trigger any Pipedream workflow](/connect/workflows) and use the connected account of your end users | ✅ `development`
❌ `production` | +| Embed prebuilt tools | [Run any action and deploy any trigger](/connect/components) directly from your AI agent or app | ✅ `development`
✅ `production` | +| Proxy API requests | [Write custom code to interface with any integrated API](/connect/api-proxy) while avoiding dealing with user auth | ✅ `development`
✅ `production` | +
+ +## Using a custom OAuth client + +1. Follow the steps [here](/integrations/oauth-clients#configuring-custom-oauth-clients) to create an OAuth client in Pipedream. +2. When connecting an account either via the [frontend SDK](/connect/managed-auth/quickstart/#use-the-pipedream-sdk-in-your-frontend), make sure to include the `oauthAppId` in `pd.connectAccount()`. +3. If using [Connect Link](/connect/managed-auth/quickstart/#or-use-connect-link), make sure to include the `oauthAppId` in the URL. + +### Finding your OAuth app ID + +[Create your OAuth client in Pipedream](https://pipedream.com/@/accounts/oauth-clients) then click the arrow to the left of the client name to expand the details. + +
+ +Copy OAuth App ID diff --git a/docs-v2/pages/connect/managed-auth/quickstart.mdx b/docs-v2/pages/connect/managed-auth/quickstart.mdx new file mode 100644 index 0000000000000..d7e0e4a4964aa --- /dev/null +++ b/docs-v2/pages/connect/managed-auth/quickstart.mdx @@ -0,0 +1,176 @@ +import Callout from '@/components/Callout' +import { Steps, Tabs } from 'nextra/components' +import Image from 'next/image' +import VideoPlayer from "@/components/VideoPlayer"; + +# Managed Auth Quickstart + +Pipedream Connect is the easiest way for your users to connect to [over {process.env.PUBLIC_APPS}+ APIs](https://pipedream.com/apps), **right in your product**. You can build in-app messaging, CRM syncs, AI agents, [and much more](/connect/use-cases/), all in a few minutes. + +## Visual overview + +Here's a high-level overview of how Connect works with your app: + +
+Pipedream Connect overview + +Here's how Connect sits in your frontend and backend, and communicates with Pipedream's API: + +
+Connect developer flow + + +## Getting started + + + +### Run the Pipedream example app or configure your own + +You'll need to do two things to add Pipedream Connect to your app: + +1. [Connect to the Pipedream API from your server](#generate-a-short-lived-token). This lets you make secure calls to the Pipedream API to initiate the account connection flow and retrieve account credentials. If you're running a JavaScript framework like Node.js on your server, you can use the Pipedream SDK. +2. [Add the Pipedream SDK to your frontend](#connect-your-users-account) or redirect your users to [a Pipedream-hosted URL](/connect/connect-link/) to start the account connection flow. + +We'll walk through these steps below, using [an example Next.js app](https://github.com/PipedreamHQ/pipedream-connect-examples/tree/master/managed-auth-basic-next-app/). To follow along, clone [the repo](https://github.com/PipedreamHQ/pipedream-connect-examples/) and follow the instructions in [the app's `README`](https://github.com/PipedreamHQ/pipedream-connect-examples/tree/master/managed-auth-basic-next-app/). That will run the app on `localhost:3000`. + + +The Next.js app is just an example. You can build Connect apps in any framework, using any language. We've provided examples in Python, Ruby, and others below. + + +First, copy the `.env.example` file to `.env.local`: + +```bash +cp .env.example .env.local +``` + +and fill the `.env.local` file with your project and app details: + +```bash +# Used by `app/server.ts` to authorize requests to the Pipedream API — see below +PIPEDREAM_CLIENT_ID=your_client_id +PIPEDREAM_CLIENT_SECRET=your_client_secret +PIPEDREAM_PROJECT_ENVIRONMENT=development +PIPEDREAM_PROJECT_ID=your_project_id +``` + +If you're building your own app, you'll need to provide these values to the environment, or retrieve them from your secrets store. + +### Create a project in Pipedream + +1. Open an existing Pipedream project or create a new one at [pipedream.com/projects](https://pipedream.com/projects). +2. Click the **Settings** tab, then copy your **Project ID**. + +### Create a Pipedream OAuth client + +Pipedream uses OAuth to authorize requests to the REST API. To create an OAuth client: + +1. Visit the [API settings](https://pipedream.com/settings/api) for your workspace. +2. Click the **New OAuth Client** button. +3. Name your client and click **Create**. +4. Copy the client secret. **It will not be accessible again**. Click **Close**. +5. Copy the client ID from the list. + +You'll need these when configuring the SDK and making API requests. + +### Generate a short-lived token + +To securely initiate account connections for your users, you'll need to generate a short-lived token for your users and use that in the [account connection flow](#connect-your-users-account). See [the docs on Connect tokens](/connect/tokens/) for a general overview of why we need to create tokens and scope them to end users. + +In the Next.js example here, we're running [Next server components](https://nextjs.org/docs/app/building-your-application/rendering/server-components) in `app/server.ts`. We call the `serverConnectTokenCreate` method from the frontend to retrieve a token **for a specific user**. + +```typescript +import { serverConnectTokenCreate } from "./server" + +const { token, expires_at } = await serverConnectTokenCreate({ + external_user_id: externalUserId // The end user's ID in your system +}); +``` + +If you're using a different server / API framework, you'll need to make secure calls to that API to create a new token for your users. + +Once you have a token, return it to your frontend to start the account connection flow for the user, or redirect them to a Pipedream-hosted URL with [Connect Link](#or-use-connect-link). + + +Refer to the API docs for [full set of parameters you can pass](/connect/api/#create-a-new-token) in the `ConnectTokenCreate` call. + + +### Connect your user's account + +To connect a third-party account for a user, you have two options: + +1. [Use the Pipedream SDK](#use-the-pipedream-sdk-in-your-frontend) in your frontend +2. [Use Connect Link](#or-use-connect-link) to deliver a hosted URL to your user + +#### Use the Pipedream SDK in your frontend + +Use this method when you want to handle the account connection flow yourself, in your app. For example, you might want to show a **Connect Slack** button in your app that triggers the account connection flow. + +First, install the [Pipedream SDK](https://www.npmjs.com/package/@pipedream/sdk) in your frontend: + +```bash +npm i --save @pipedream/sdk +``` + +When the user connects an account in your product, [pass the token from your backend](#generate-a-short-lived-token) and call `connectAccount`. This opens a Pipedream iFrame that guides the user through the account connection. + +In our example, `app/page.tsx` calls the `connectAccount` method from the Pipedream SDK when the user clicks the **Connect your account** button. + +
+Connect your account button + +```typescript +import { createFrontendClient } from "@pipedream/sdk/browser" + +export default function Home() { + const pd = createFrontendClient() + function connectAccount() { + pd.connectAccount({ + app: appSlug, // Pass the app name slug of the app you want to integrate + oauthAppId: appId, // For OAuth apps, pass the OAuth app ID; omit this param to use Pipedream's OAuth client or for key-based apps + token: "YOUR_TOKEN", // The token you received from your server above + onSuccess: ({ id: accountId }) => { + console.log(`Account successfully connected: ${accountId}`) + } + }) + } + + return ( +
+ +
+ ) +} +``` + +Press that button to connect an account for the app you configured. + +#### Or use Connect Link + +Use this option when you can't execute JavaScript or open an iFrame in your environment (e.g. mobile apps), or when you want to offload the account connection flow to Pipedream and avoid frontend work. You can also send these links via email or SMS. + +The Connect Link opens a Pipedream-hosted page, guiding users through the account connection process. The URL is specific to the user and expires after 4 hours. + +1. First, [generate a token](#generate-a-short-lived-token) for your users. +2. Extract the `connect_link_url` from the token response. +3. Before returning the URL to your user, add an `app` parameter to the end of the query string: + +``` +https://pipedream.com/_static/connect.html?token={token}&connectLink=true&app={appSlug}&oauthAppId={oauthAppId} +``` + +4. Redirect your users to this URL, or send it to them via email, SMS, and more. + +### Make authenticated requests + +Now that your users have connected an account, you can use their auth in one of a few ways: + +1. [Retrieve their credentials from the REST API](/connect/api/#accounts) to use in your backend application +2. [Use Pipedream's visual workflow builder](/connect/workflows/) to define complex logic to run on behalf of your users +3. [Embed Pipedream components directly in your app](/connect/components/) to run actions and triggers on their behalf + +### Deploy your app to production + +- Test end to end in [development](/connect/environments/) +- Ship to production! + +
diff --git a/docs-v2/pages/connect/managed-auth/tokens.mdx b/docs-v2/pages/connect/managed-auth/tokens.mdx new file mode 100644 index 0000000000000..338bdf3de475c --- /dev/null +++ b/docs-v2/pages/connect/managed-auth/tokens.mdx @@ -0,0 +1,32 @@ +import Callout from '@/components/Callout' + +# Connect Tokens + +When you initiate account connection for your end users, you must either: + +1. Generate a secure, short-lived token scoped to the end user, or +2. Use the [Connect Link](/connect/connect-link/) feature to generate a URL that guides the user through the account connection flow without any frontend work on your side. + +Here, we'll show you how to generate tokens for your users and return that to your frontend, passing that to the account connection flow. + +Use tokens when you want to handle the account connection flow yourself, in your app's UI. For example, you might want to show a **Connect Slack** button in your app that triggers the account connection flow for Slack, or launch the flow in a modal. + + +Connect tokens currently have a 4-hour expiry, and can only be used once. + + +## Creating a token + +See docs on [the `/tokens` endpoint](/connect/api/#create-a-new-token) to create new tokens. + +## Webhooks + +When you generate a token, you can specify a `webhook_uri` where Pipedream will deliver updates on the account connection flow. This is useful if you want to update your UI based on the status of the account connection flow, get a log of errors, and more. + +[See the webhooks docs](/connect/webhooks/) for more information. + +## Tokens are scoped to end users and environments + +When you [create a new Connect token](/connect/api/#create-a-new-token), you pass an `external_user_id` and an `environment`. See the docs on [environments](/connect/environments/) for more information on passing environment in the SDK and API. + +Tokens are scoped to this user and environment. When the user successfully connects an account with that token, it will be saved for that `external_user_id` in the specified environment. diff --git a/docs-v2/pages/connect/managed-auth/users.mdx b/docs-v2/pages/connect/managed-auth/users.mdx new file mode 100644 index 0000000000000..810f2fcd2ce7d --- /dev/null +++ b/docs-v2/pages/connect/managed-auth/users.mdx @@ -0,0 +1,17 @@ +import Callout from '@/components/Callout' + +# Users + +To view or delete your users' connected accounts: + +1. Open your project in Pipedream +2. Click the **Connect** tab on the left +3. Click the **Users** tab at the top + +You'll see a list of all users, their connected accounts, and the option to delete any accounts from the UI. You can also retrieve and delete all your users via the API ([see the docs for reference](/connect/api/)). + + +Connect currently supports one connected account per user, app, environment combination. + +So if user `abc-123` in your application connects their Slack account in `production`, then that same user connects a different Slack workspace (also in `production`), the first connected account will get overwritten in Pipedream and replaced by the second. + diff --git a/docs-v2/pages/connect/managed-auth/webhooks.mdx b/docs-v2/pages/connect/managed-auth/webhooks.mdx new file mode 100644 index 0000000000000..82b444cb80742 --- /dev/null +++ b/docs-v2/pages/connect/managed-auth/webhooks.mdx @@ -0,0 +1,54 @@ +# Connect Webhooks + +When you [generate a Connect token](/connect/managed-auth/quickstart/#generate-a-short-lived-token), you can pass a `webhook_uri` parameter. Pipedream will send a POST request to this URL when the user completes the connection flow, or if an error occurs at any point. [See the API docs](/connect/api/#create-a-new-token) for details. + +## Webhook events + +- `CONNECTION_SUCCESS` - Sent when the user successfully connects their account +- `CONNECTION_ERROR` - Sent when an error occurs during the connection flow + +## Webhook payload + +### Successful connection + +Please note that user credentials are not sent in the webhook request. To retrieve credentials, use the [Connect API to fetch the account](/connect/api/#retrieve-account-details-by-id) using the `account.id` provided in the webhook payload. + +```json +{ + "event": "CONNECTION_SUCCESS", + "connect_token": "abc123", + "environment": "production", + "connect_session_id": 123, + "account": { + "id": "apn_abc123", + "name": "My Slack workspace", + "external_id": "U123456", + "healthy": true, + "dead": false, + "app": { + "id": "app_abc123", + "name_slug": "slack", + "name": "Slack", + "auth_type": "oauth", + "description": "Slack is a channel-based messaging platform", + "img_src": "https://assets.pipedream.net/icons/slack.svg", + "custom_fields_json": [], + "categories": "Communication", + }, + "created_at": "2021-09-01T00:00:00Z", + "updated_at": "2021-09-01T00:00:00Z", + } +} +``` + +### Error + +```json +{ + "event": "CONNECTION_ERROR", + "connect_token": "abc123", + "environment": "production", + "connect_session_id": 123, + "error": "You've hit your limit on the number of external users you can connect." +} +``` diff --git a/docs-v2/pages/connect/migrating-from-project-keys-to-oauth.mdx b/docs-v2/pages/connect/migrating-from-project-keys-to-oauth.mdx new file mode 100644 index 0000000000000..d7f1a2d254daf --- /dev/null +++ b/docs-v2/pages/connect/migrating-from-project-keys-to-oauth.mdx @@ -0,0 +1,103 @@ +import Callout from '@/components/Callout' +import { Steps, Tabs } from 'nextra/components' + +# Migrating to the 1.0 SDK + + +This guide is only relevant if: + +- You used the `0.x` version of the JavaScript SDK +- You authenticated with the Pipedream API using project keys + + +## What changed + +- In the `0.x` version of the SDK and the original Connect API, you could authenticate with keys scoped to a specific project. In the `1.x` version of the SDK, you need to authenticate with [OAuth clients](/rest-api/auth/#oauth). +- The `createClient` method from both the browser and Node.js SDKs has been replaced with separate methods: `createFrontendClient` and `createBackendClient`, respectively. +- The `connectTokenCreate` method has been renamed `createConnectToken` +- New SDK methods: `projectInfo`, `invokeWorkflow`, and more + +## How to migrate + + + +### Create an OAuth client + +Follow the instructions [here](/rest-api/auth/#oauth) to create an OAuth client. + +### Update your SDK version + +Change the `@pipedream/sdk` version in your `package.json`: + +```json +{ + "dependencies": { + "@pipedream/sdk": "^1.0.0" + } +} +``` + +Then run + +```bash +npm install +``` + +### Update your project key references to use your OAuth client + +You may have been referencing project keys in environment variables or other config. Replace these with references to the OAuth client you created in step 1. + +For example, `process.env.PIPEDREAM_PROJECT_KEY` should be replaced with `process.env.PIPEDREAM_OAUTH_CLIENT_ID`, and `process.env.PIPEDREAM_PROJECT_SECRET` should be replaced with `process.env.PIPEDREAM_OAUTH_CLIENT_SECRET`. + +### Update your SDK code + +#### Frontend client + +You'll need to make two changes to your frontend client code: + +1. Replace references to `@pipedream/sdk/browser`. Instead, import directly from `@pipedream/sdk`. +2. Change the `createClient` method to `createFrontendClient`. + +```typescript +import { createFrontendClient } from "@pipedream/sdk/browser" +const pd = createFrontendClient() +``` + +#### Backend client + +You'll need to make three changes to your backend code: + +1. Change the `createClient` method to `createBackendClient`. +2. Pass your OAuth client ID and secret to the `createBackendClient` method, removing references to project keys. +3. Change any token create method references from `connectTokenCreate` to `createConnectToken`. + + + +```typescript +import { createBackendClient, HTTPAuthType } from "@pipedream/sdk/server"; + +// These secrets should be saved securely and passed to your environment +const pd = createBackendClient({ + credentials: { + clientId: "YOUR_CLIENT_ID", + clientSecret: "YOUR_CLIENT_SECRET", + }, +}); +``` + + +```javascript +import { createBackendClient } from "@pipedream/sdk/server"; + +// These secrets should be saved securely and passed to your environment +const pd = createBackendClient({ + credentials: { + clientId: "YOUR_CLIENT_ID", + clientSecret: "YOUR_CLIENT_SECRET", + }, +}); +``` + + + + diff --git a/docs-v2/pages/connect/troubleshooting.mdx b/docs-v2/pages/connect/troubleshooting.mdx new file mode 100644 index 0000000000000..c21c987b4a6a6 --- /dev/null +++ b/docs-v2/pages/connect/troubleshooting.mdx @@ -0,0 +1,51 @@ +import Callout from '@/components/Callout' + +# Troubleshooting + +Below are some common errors when connecting your users' accounts via Pipedream Connect. + +### Error creating a Connect token + +>Error creating token: Error: Failed to obtain OAuth token: Response Error: 401 Unauthorized + +Authorization to the Pipedream API failed when creating the Connect token. Double-check the client ID or secret for your [Pipedream OAuth client](/connect/api/#authentication). + +### Error connecting an account + +Most errors when connecting an account are related to the [Connect token](/connect/tokens/), which Pipedream validates from the Connect iFrame. + +#### Common errors + +>This link has expired. Please request a new one from the app developer. + +>This session has expired. Please refresh the page to try again. + +#### Troubleshooting steps + +Pipedream typically returns an explicit error message in the HTTP response of the token validation network call directly from the iFrame in the client. To check for errors, start the account connection flow in a browser and open the developer console to view the network requests. + +Filter for requests to + +``` +https://api.pipedream.com/v1/connect/tokens +``` + +and check the response for error messages. + +#### Token validation errors + +>The Pipedream Connect token is invalid. Please generate a new one and try again. + +Connect tokens expire, and are only able to be used once. Try generating a new token and try again. + +>App not found. Please check your app id. + +Double-check the app slug you're passing [when connecting your user's account](/connect/managed-auth/quickstart/#connect-your-users-account). + +### Connection failed. Please retry or contact support. + +The user may have closed the OAuth popup window without completing authorization. + + +If you're still have trouble or hitting an error that isn't listed here, [get in touch with us](https://pipedream.com/support). We'd love to help. + diff --git a/docs-v2/pages/connect/use-cases.mdx b/docs-v2/pages/connect/use-cases.mdx new file mode 100644 index 0000000000000..68b55a9f33527 --- /dev/null +++ b/docs-v2/pages/connect/use-cases.mdx @@ -0,0 +1,41 @@ +# Pipedream Connect use cases + +Developers use Pipedream Connect to build customer-facing API integrations into their products. It lets you build [in-app messaging](#in-app-messaging), [CRM syncs](#crm-syncs), [AI-driven products](#ai-products), and much more, all in a few minutes. + +## Core value to app developers + +In 20 years of building software, we've seen a common theme. No matter the product, your customers end up needing to connect your app to third-party APIs. + +You might build real-time notifications with messaging apps, export customer data to databases or spreadsheets, ingest data from CRMs, or connect to any of the thousands of APIs and SaaS services your customers are using. These features are often tied to large contracts and Enterprise customers. + +But it's hard to justify the engineering effort required for these integrations. They're a distraction from the core product. Once built, they're hard to maintain. You have to securely manage auth, learn the nuances of each API, and improve the integration as your customers ask for new features. Managing these integrations is a huge context switch for any engineer. Most teams have trouble scaling this. + +At Pipedream, our customers tell us a variant of this story every day. Pipedream Connect helps you build these features **in minutes**, for any app. + +Once you add the core integration UI to your app, non-technical employees can also help to manage [the workflows](/workflows/building-workflows/) that drive the backend logic. For example, if you're building [in-app messaging](#in-app-messaging), once you add the UI to let users connect Slack, Discord, and other tools, anyone on your team can build workflows that format and deliver messages to your customers. This is a huge plus for many orgs: you still get to build a bespoke UI, directly in your app, suited to your customer need. But anyone in the company can collaborate on the workflows that power it. + +## Value to your customers + +Shipping new customer-facing integrations can happen in minutes. + +## How customers are using Connect + +### In-app messaging + +Most apps build email notifications, since it's easy. But most teams work in Slack, Discord, Microsoft Teams, or a variety of other messaging apps. Sometimes you want to send messages via SMS or push notifications. It's hard to maintain integrations for all the apps your customers are using. Pipedream makes this simple. + +### CRM syncs + +Sync data between your app and Salesforce, HubSpot, or any CRM. Pipedream lets your customers connect their accounts directly from your UI, define the sync logic, and run it on Pipedream's infrastructure. Pull data from your customers' CRMs in real-time, or push data from your app. + +### AI products + +Talk to any AI API or LLM. Build chat apps or interact in real-time with your users. Or run asynchronous tasks in the background, like image classification, article summarization, or other tasks you want to offload to an AI agent. You can use built-in functions like [`$.flow.suspend`](/workflows/building-workflows/code/nodejs/rerun/#flowsuspend) to send a message to your team, or directly to the user, to approve specific actions. + +### Spreadsheet integrations + +Sync data between your app and Google Sheets, Airtable, or any spreadsheet. Pipedream Connect lets your users auth with any app, select the sheet, and define custom sync logic. + +### And much more + +Building an app with Pipedream and want to be profiled here (anonymously or otherwise)? Email connect@pipedream.com to let us know! diff --git a/docs-v2/pages/connect/workflows.mdx b/docs-v2/pages/connect/workflows.mdx new file mode 100644 index 0000000000000..cdf7eff66b0a2 --- /dev/null +++ b/docs-v2/pages/connect/workflows.mdx @@ -0,0 +1,414 @@ +import { Steps, Tabs } from 'nextra/components' +import ArcadeEmbed from '@/components/ArcadeEmbed' +import Callout from '@/components/Callout' +import Image from 'next/image' + +# Running workflows for your end users + +Just like you can build and run internal [workflows](/workflows/building-workflows/) for your team, **you can run workflows for [your end users](/connect/api/#external-users), too**. + +Whether you're building well-defined integrations or autonomous AI agents, workflows provide a powerful set of tools for running [code](/workflows/building-workflows/code/) or [pre-defined actions](/workflows/building-workflows/actions/) on behalf of your users. Pipedream's UI makes it easy to build, test, and [debug](/workflows/building-workflows/inspect/) workflows. + +## What are workflows? + +
+ +
+ +Workflows are sequences of [steps](/workflows/#steps) [triggered by an event](/workflows/building-workflows/triggers/), like an HTTP request, or new rows in a Google sheet. + +You can use [pre-built actions](/workflows/building-workflows/actions/) or custom [Node.js](/workflows/building-workflows/code/nodejs/), [Python](/workflows/building-workflows/code/python/), [Golang](/workflows/building-workflows/code/go/), or [Bash](/workflows/building-workflows/code/bash/) code in workflows and connect to any of our {process.env.PUBLIC_APPS} integrated apps. + +Workflows also have built-in: + +- [Flow control](/workflows/building-workflows/control-flow/) +- [Concurrency and throttling](/workflows/building-workflows/settings/concurrency-and-throttling/) +- [Key-value stores](/workflows/data-management/data-stores/) +- [Error handling](/workflows/building-workflows/errors/) +- [VPCs](/workflows/vpc/) +- [And more](https://pipedream.com/pricing) + +Read [the quickstart](/quickstart/) to learn more. + +## Getting started + + + +### Create a workflow + +[Create a new workflow](/workflows/building-workflows/) or open an existing one. + +### Add an HTTP trigger + +To get started building workflows for your end users: + +1. Add an [HTTP trigger](/workflows/building-workflows/triggers/#http) to your workflow +2. Generate a test event with the required headers: + - `x-pd-environment: development` + - `x-pd-external-user-id: {your_external_user_id}` + +See the [Triggering your workflow](#triggering-your-workflow) section below for details on securing your workflow with OAuth and deploying triggers on behalf of your end users. + +### Configure accounts to use your end users' auth + +When you configure [pre-built actions](/workflows/building-workflows/actions/) or [custom code that connects to third-party APIs](/workflows/building-workflows/code/nodejs/auth/), you can link accounts in one of two ways: + +1. **Use your own account**: If you're connecting to an API that uses your own API key or developer account — for example, a workflow that connects to the OpenAI API or a PostgreSQL database — click the **Connect account** button to link your own, static account. + +
+ +
+ +2. **Use your end users' auth**: If you're building a workflow that connects to your end users' accounts — for example, a workflow that sends a message with your user's Slack account — you can select the option to **Use end user's auth via Connect**: + +
+ +
+ +When you trigger the workflow, Pipedream will look up the corresponding account for the end user whose user ID you provide [when invoking the workflow](#invoke-the-workflow). + +### Connect a test account + +To run an end-to-end test as an end user, you need to have users and connected accounts in your project. If you already have a **development** account linked, you can skip this step. + +If you don't, the fastest way to do this is [on the **Users** tab](/connect/managed-auth/users/) in your Pipedream project: +- You'll see there's a button to **Connect account** +- Go through the flow and make sure to create the account in **development** mode +- Note the **external user ID** of the account you just connected, you'll need it in the next step + + + + +### Generate a test request + +Test events are critical for developing workflows effectively. Without a test event, you won't be able to test your workflow end to end in the builder, see the shape of the event data that triggers the workflow, and the lookup to use your end user's auth won't work. + +To generate a test event, click **Send Test Event** in the trigger, and fill in the event data. This will trigger the workflow and allow you to test the workflow end to end in the builder. + + +Make sure to include these headers in your test request: +- `x-pd-environment: development` +- `x-pd-external-user-id: {your_external_user_id}` + + +
+ +Invoke workflow headers + +### Deploy your workflow + +When you're done with the workflow, click **Deploy** at the top right. + +### Invoke the workflow + +If you're using TypeScript or a JavaScript runtime, [install the Pipedream SDK](/connect/api/#installing-the-typescript-sdk). Pipedream also provides an HTTP API for invoking workflows (see example below). + +```bash +npm i @pipedream/sdk +``` + +To invoke workflows, you'll need: + +1. The OAuth client ID and secret from your OAuth client in **step 2 above** (if configured) +2. Your [Project ID](/workflows/projects/#finding-your-projects-id) +3. Your workflow's HTTP endpoint URL +4. The [external user ID](/connect/api/#external-users) of the user you'd like to run the workflow for +5. The [Connect environment](/connect/environments/) tied to the user's account + +Then invoke the workflow like so: + + + +```typescript +import { createBackendClient, HTTPAuthType } from "@pipedream/sdk/server"; + +// These secrets should be saved securely and passed to your environment +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +await pd.invokeWorkflowForExternalUser( + "{your_endpoint_url}", // pass the endpoint ID or full URL here + "{your_external_user_id}" // The end user's ID in your system + { + method: "POST", + body: { + message: "Hello World" + } + }, + HTTPAuthType.OAuth // Will automatically send the Authorization header with a fresh token +) +``` + + +```javascript +import { createBackendClient } from "@pipedream/sdk/server"; + +// These secrets should be saved securely and passed to your environment +const client = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}" + }, + projectId: "{your_project_id}" +}); + +const response = await client.invokeWorkflowForExternalUser( + "{your_endpoint_url}", // pass the endpoint ID or full URL here + "{your_external_user_id}" // The end user's ID in your system + { + method: "POST", + body: { + message: "Hello World" + } + } +) +``` + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. + +curl -X POST https://{your-endpoint-url} \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer {access_token}" \ + -H 'X-PD-External-User-ID: {your_external_user_id}' \ + -H 'X-PD-Environment: development' \ # 'development' or 'production' + -d '{ + "message": "Hello, world" + }' +``` + + +
+ +## Configuring workflow steps + +When configuring a workflow that's using your end user's auth instead of your own, you'll need to define most configuration fields manually in each step. + +For example, normally when you connect your own Google Sheets account directly in the builder, you can dynamically list all of the available sheets from a dropdown. + +
+ +
+ +However, when running workflows on behalf of your end users, that UI configuration doesn't work, since the Google Sheets account to use is determined at the time of workflow execution. So instead, you'll need to configure these fields manually. + +- Either make sure to pass all required configuration data when invoking the workflow, or add a step to your workflow that retrieve it from your database, etc. For example: + +```bash +curl -X POST https://{your-endpoint-url} \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer {access_token}" \ + -H 'X-PD-External-User-ID: {your_external_user_id}' \ + -H 'X-PD-Environment: development' \ # 'development' or 'production' + -d '{ + "slackChannel": "#general", + "messageText": "Hello, world!", + "gitRepo": "AcmeOrg/acme-repo", + "issueTitle": "Test Issue" + }' \ +``` + +- Then in the Slack and GitHub steps, you'd reference those fields directly: + +
+ +
+ + +We plan to improve this interface in the future, and potentially allow developers to store end user metadata and configuration data alongside the connected account for your end users, so you won't need to pass the data at runtime. [Let us know](https://pipedream.com/support) if that's a feature you'd like to see. + + +## Testing + +To test a step using the connected account of one of your end users in the builder, you'll need a few things to be configured so that your workflow knows which account to use. + +**Make sure you have an external user with the relevant connected account(s) saved to your project:** +- Go to the **[Users tab](/connect/managed-auth/users/)** in the **Connect** section of your project to confirm +- If not, either connect one from your application or [directly in the UI](#connect-a-test-account) + +**Pass the environment and external user ID:** +1. Once you've added an HTTP trigger to the workflow, click **Generate test event** +Generate test event +2. Click on the **Headers** tab +3. Make sure `x-pd-environment` is set (you'll likely want to `development`) +4. Make sure to also pass `x-pd-external-user-id` with the external user ID of the user you'd like to test with + +Include required headers + +## Triggering your workflow + +You have two options for triggering workflows that run on behalf of your end users: + +1. [Invoke via HTTP webhook](#http-webhook) +2. [Deploy an event source](#deploy-an-event-source) (Slack, Gmail, etc.) + +### HTTP Webhook + +The most common way to trigger workflows is via HTTP webhook. We strongly recommend [creating a Pipedream OAuth client](/rest-api/auth#creating-an-oauth-client) and authenticating inbound requests to your workflows. + +To get started, you'll need: + +- [OAuth client ID and secret](/rest-api/auth#creating-an-oauth-client) (optional but recommended) +- Your [project ID](/workflows/projects/#finding-your-projects-id) +- Your workflow's HTTP endpoint URL +- The [external user ID](/connect/api/#external-users) of your end user +- The [Connect environment](/connect/environments/) + + + +```typescript +import { createBackendClient, HTTPAuthType } from "@pipedream/sdk/server"; + +// These secrets should be saved securely and passed to your environment +const pd = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, + projectId: "{your_project_id}" +}); + +await pd.invokeWorkflowForExternalUser( + "{your_endpoint_url}", // pass the endpoint ID or full URL here + "{your_external_user_id}" // The end user's ID in your system + { + method: "POST", + body: { + message: "Hello World" + } + }, + HTTPAuthType.OAuth // Will automatically send the Authorization header with a fresh token +) +``` + + +```javascript +import { createBackendClient } from "@pipedream/sdk/server"; + +// These secrets should be saved securely and passed to your environment +const client = createBackendClient({ + environment: "development", // change to production if running for a test production account, or in production + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}" + }, + projectId: "{your_project_id}" +}); + +const response = await client.invokeWorkflowForExternalUser( + "{your_endpoint_url}", // pass the endpoint ID or full URL here + "{your_external_user_id}" // The end user's ID in your system + { + method: "POST", + body: { + message: "Hello World" + } + } +) +``` + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. + +curl -X POST https://{your-endpoint-url} \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer {access_token}" \ + -H 'X-PD-External-User-ID: {your_external_user_id}' \ + -H 'X-PD-Environment: development' \ # 'development' or 'production' + -d '{ + "message": "Hello, world" + }' +``` + + + +### Deploy an event source + +You can [programmatically deploy triggers via the API](/connect/api/#deploy-a-trigger) to have events from integrated apps (like [new Slack messages](https://pipedream.com/apps/slack/triggers/new-message-in-channels) or [new emails in Gmail](https://pipedream.com/apps/gmail/triggers/new-email-received)) trigger your workflow. This allows you to: + +- Deploy triggers for specific users from your application +- Configure trigger parameters per-user +- Manage deployed triggers via the API + +See the [API documentation](/connect/api/#deploy-a-trigger) for detailed examples of deploying and managing triggers. + +## Troubleshooting + +For help debugging issues with your workflow, you can return verbose error messages to the caller by configuring the HTTP trigger to **Return a custom response from your workflow**. + +Configure custom response + +With that setting enabled on the trigger, below is an example of [this](/connect/workflows/#required-account-not-found-for-external-user-id) error: + +```bash +curl -X POST https://{your-endpoint-url} \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer {access_token}' \ + -H "x-pd-environment: development" \ + -H "x-pd-external-user-id: abc-123" \ + -d '{ + "slackChannel": "#general", + "messageText": "Hello, world! (sent via curl)", + "hubSpotList": "prospects", + "contactEmail": "foo@example.com" + }' \ +Pipedream Connect Error: Required account for hubspot not found for external user ID abc-123 in development +``` + + +### Common errors +#### No external user ID passed, but one or more steps require it +- One or more steps in the workflow are configured to **Use end user's auth via Connect**, but no external user ID was passed when invoking the workflow. +- [Refer to the docs](#invoke-the-workflow) to make sure you're passing external user ID correctly when invoking the workflow. + +#### No matching external user ID +- There was an external user ID passed, but it didn't match any users in the project. +- Double-check that the external user ID that you passed when invoking the workflow matches one either [in the UI](/connect/managed-auth/users/) or [via the API](/connect/api/#accounts). + +#### Required account not found for external user ID +- The external user ID was passed when invoking the workflow, but the user doesn't have a connected account for one or more of the apps that are configured to use it in this workflow execution. +- You can check which connected accounts are available for that user [in the UI](/connect/managed-auth/users/) or [via the API](/connect/api/#accounts). + +#### Running workflows for your users in production requires a higher tier plan +- Anyone is able to run workflows for your end users in `development`. The Business plan is required to run on behalf of `production` users. +- Schedule a call with our sales team and learn more about pricing [here](https://pipedream.com/pricing?plan=Enterprise). + +## Known limitations + +#### Workflows can only use a single external user's auth per execution +- Right now you cannot invoke a workflow to loop through many external user IDs within a single execution. +- You can only run a workflow for a single external user ID at a time (for now). + +#### The external user ID to use during execution must be passed in the triggering event +- You can't run a workflow on a timer for example, and look up the external user ID to use at runtime. +- The external user ID must be passed in the triggering event, typically via [HTTP trigger](#invoke-the-workflow). diff --git a/docs-v2/pages/connected-accounts/_meta.json b/docs-v2/pages/connected-accounts/_meta.json deleted file mode 100644 index dec8bd2088c04..0000000000000 --- a/docs-v2/pages/connected-accounts/_meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "index": "Connected Accounts" -} diff --git a/docs-v2/pages/connected-accounts/api.mdx b/docs-v2/pages/connected-accounts/api.mdx deleted file mode 100644 index d45f515fc3748..0000000000000 --- a/docs-v2/pages/connected-accounts/api.mdx +++ /dev/null @@ -1,176 +0,0 @@ -import Callout from '@/components/Callout' -import { Tabs } from 'nextra/components' - -# Accessing credentials via API - -When you [connect an account](/connected-accounts/#connecting-accounts), you can use its credentials in workflows, authorizing requests to any app. Pipedream manages the OAuth process for [OAuth apps](/connected-accounts/#oauth), ensuring you always have a fresh access token for requests. - -You can also access account credentials from the Pipedream API, using them in any other tool or app where you need auth. - - -The credentials API is in beta - -Accessing credentials via API is in **beta**, and we're looking for feedback. Please [let us know](https://pipedream.com/support) how you're using it, what's not working, and what else you'd like to see. - -During the beta: - -- All API calls to `/v1/accounts/*` are free. -- The API is subject to change. - - -## Using the credentials API - -1. [Connect your account](/connected-accounts/#connecting-a-new-account) -2. On [https://pipedream.com/accounts](https://pipedream.com/accounts), find your account and click the `...` to the right of the account, -3. **Copy Account ID** - -![Copy Account ID](https://res.cloudinary.com/pipedreamin/image/upload/v1707622922/docs/Screenshot_2024-02-10_at_7.10.59_PM_zxfpkt.png) - -4. Make a request to the [**Get account credentials** endpoint](/api/rest/#get-account). - - - - -```javascript -export default defineComponent({ - props: { - pipedream: { - type: "app", - app: "pipedream", - }, - accountId: { - type: "string", - label: "Account ID", - }, - }, - async run({ steps, $ }) { - const url = `https://api.pipedream.com/v1/accounts/${this.accoundId}?include_credentials=1`; - const headers = { - "Content-Type": "application/json", - Authorization: `Bearer ${this.pipedream.$auth.api_key}`, - }; - - const response = await fetch(url, { - method: "GET", - headers, - }); - if (!response.ok) throw new Error(`API request failed: ${response.status}`); - return await response.json(); - }, -}); -``` - - - - -```python -import requests - -def handler(pd: "pipedream"): - headers = { - 'Content-Type': 'application/json', - 'Authorization': f'Bearer {pd.inputs["pipedream"]["$auth"]["api_key"]}', - }; - r = requests.get("https://api.pipedream.com/v1/accounts/?include_credentials=1", headers=headers) - return r.json() -``` - - - - -```bash -curl 'https://api.pipedream.com/v1/accounts/?include_credentials=1' \ - -H "Authorization: Bearer " -``` - - - - -```javascript -const url = `https://api.pipedream.com/v1/accounts/?include_credentials=1`; -const headers = { - "Content-Type": "application/json", - Authorization: `Bearer `, -}; - -const response = await fetch(url, { - method: "GET", - headers, -}); -if (!response.ok) throw new Error(`API request failed: ${response.status}`); - -// Credentials are in data.credentials -const { data } = await response.json(); -``` - -::: - - - - -```python -import requests - -headers = { - 'Content-Type': 'application/json', - 'Authorization': f'Bearer ', -}; -r = requests.get("https://api.pipedream.com/v1/accounts/?include_credentials=1", headers=headers) - -# Credentials are in data.data.credentials -data = r.json() -``` - - - - -5. It's not necessary to fetch credentials on every request to your app. You should [cache credentials in your own DB](#caching-credentials), refreshing them only when necessary. - -## Caching credentials - -Access tokens for most OAuth services are valid for at least one hour. Some tokens have a longer expiry, and some are static until rotated or revoked. - -The response from `/v1/accounts/:id?include_credentials=1` contains an `expires_at` field, an ISO timestamp representing the expiry of the current token. This maps to the `expires_at` timestamp in the OAuth access token response. While tokens are not guaranteed to be valid until expiry, they are for most APIs in practice. We recommend caching credentials with the `expires_at` timestamp, using the tokens until expiry, retrieving new credentials only after expiry or when you encounter auth errors when making your API request. - -If `expires_at` is missing or `null`, the credentials do not expire. - -```javascript -/*** PSEUDO-CODE — use this as a guide ***/ - -// Fetch the most-recent token and expiry from cache / DB -const cache = Cache(); -const { oauth_access_token, expires_at } = await cache.get("credentials"); - -// Only fetch new tokens when expired -let token = oauth_access_token; -if (expires_at && new Date(expires_at) < new Date()) { - const url = `https://api.pipedream.com/v1/accounts/?include_credentials=1`; - const headers = { - "Content-Type": "application/json", - Authorization: `Bearer `, - }; - - const response = await fetch(url, { - method: "GET", - headers, - }); - if (!response.ok) - throw new Error(`Unable to fetch token: ${response.status}`); - - const { data } = await response.json(); - const newToken = data?.credentials?.oauth_access_token; - if (!newToken) throw new Error("No access token"); - token = newToken; -} - -// Use `token` in API requests. Handle credentials errors and -// fetch new credentials from Pipedream as necessary -``` - -Since these credentials are sensitive, you should encrypt them in the DB or data store you're using, decrypting them only at runtime when authorizing API requests. - -## Security - -[See our security docs](/privacy-and-security/#third-party-oauth-grants-api-keys-and-environment-variables) for details on how Pipedream secures your credentials. - -Connected accounts are private by default. You can [manage access control](/connected-accounts/#managing-access) to expose them to other members of your workspace. diff --git a/docs-v2/pages/connected-accounts/external-auth.mdx b/docs-v2/pages/connected-accounts/external-auth.mdx deleted file mode 100644 index 32ae3a1b00e25..0000000000000 --- a/docs-v2/pages/connected-accounts/external-auth.mdx +++ /dev/null @@ -1,62 +0,0 @@ -import Callout from '@/components/Callout' - -# Passing external credentials at runtime - -If you use a secrets store like [HashiCorp Vault](https://www.vaultproject.io/) or [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/), store credentials in a database, or use a service like [Nango](https://www.nango.dev/) to manage auth, you can retrieve these secrets at runtime and pass them to any step. - -There are two ways to pass external auth at runtime: - -1. [Pass it in an HTTP request](#pass-credentials-via-http) -2. [Fetch credentials from a DB or secrets store](#fetch-credentials-from-a-db-or-secrets-store) within a workflow step - - -External auth is in beta -Passing external credentials at runtime is in **beta**, and we're looking for feedback. Please [let us know](https://pipedream.com/support) how you're using it, what's not working, and what else you'd like to see. - - -## Pass credentials via HTTP - -1. If not already configured, [add an HTTP trigger](/workflows/steps/triggers/#http) to your workflow. -2. From your app, retrieve credentials and send them in an HTTP request to the endpoint with the rest of the payload. -3. In the step of your workflow where you'd like to pass these credentials, select the **Use external authentication** option at the bottom-right of the account selector: - -![Select "External Auth"](./images/select-external-auth.png) - -4. You'll be prompted for all required credentials for the app, often just an `oauth_access_token` or `api_key`. [Find the variable that contains your credentials](/workflows/events/#copying-references-to-event-data) and pass them to each field: - -![External auth](https://res.cloudinary.com/pipedreamin/image/upload/v1707630112/docs/Screenshot_2024-02-10_at_9.40.54_PM_hynkvq.png) - -Most steps require additional, user-specific configuration. For example, the Slack **Send a Message** action requires a **Channel ID**, which may be specific to the end user's workspace. You'll need to fetch these values from another step and reference them here. - -![Configure additional params](https://res.cloudinary.com/pipedreamin/image/upload/v1707630112/docs/Screenshot_2024-02-10_at_9.40.54_PM_hynkvq.png) - - -Default logging - -When you return credentials from workflow steps, Pipedream stores it with the rest of the workflow execution data. Workflow events are retained according to the default retention policy for your plan and any [data retention controls](/workflows/settings/#data-retention-controls) you've configured. - -You can set [the `pd-nostore` flag](/workflows/steps/triggers/#x-pd-nostore) to `1` on requests with credentials to disable logging for those requests only. - - -## Fetch credentials from a DB or secrets store - -1. Add a step to your workflow to fetch credentials from your DB or secrets store. -2. In the step of your workflow where you'd like to pass these credentials, select the **Use external authentication** option at the bottom-right of the account selector: - -![Select "External Auth"](./images/select-external-auth.png) - -3. You'll be prompted for all required credentials for the app, often just an `oauth_access_token` or `api_key`. [Find the variable that contains your credentials](/workflows/events/#copying-references-to-event-data) and pass them to each field: - -![External auth](https://res.cloudinary.com/pipedreamin/image/upload/v1707630112/docs/Screenshot_2024-02-10_at_9.40.54_PM_hynkvq.png) - -Most steps require additional, user-specific configuration. For example, the Slack **Send a Message** action requires a **Channel ID**, which may be specific to the end user's workspace. You'll need to fetch these values from another step and reference them here. - -![Configure additional params](https://res.cloudinary.com/pipedreamin/image/upload/v1707630112/docs/Screenshot_2024-02-10_at_9.40.54_PM_hynkvq.png) - - -Default logging - -When you return credentials from workflow steps, Pipedream stores it with the rest of the workflow execution data. Workflow events are retained according to the default retention policy for your plan and any [data retention controls](/workflows/settings/#data-retention-controls) you've configured. - -You can set [the `pd-nostore` flag](/workflows/steps/triggers/#x-pd-nostore) to `1` on requests with credentials to disable logging for those requests only. - diff --git a/docs-v2/pages/connected-accounts/index.mdx b/docs-v2/pages/connected-accounts/index.mdx deleted file mode 100644 index 74ccde5f58f25..0000000000000 --- a/docs-v2/pages/connected-accounts/index.mdx +++ /dev/null @@ -1,259 +0,0 @@ -import Callout from '@/components/Callout' -import VideoPlayer from '@/components/VideoPlayer' - -# Connected Accounts - - - -Pipedream provides native integrations for [{process.env.PUBLIC_APPS}+ APIs](https://pipedream.com/apps). Once you connect an account, you can - -- [Link that account to any step of a workflow](#connecting-accounts), using the associated credentials to make API requests to any service. -- [Manage permissions](#managing-connected-accounts), limiting access to sensitive accounts -- [Access credentials via API](#accessing-credentials-via-api), letting you build services anywhere and use Pipedream to handle auth - -Pipedream handles OAuth for you, ensuring you always have a fresh access token to authorize requests, and [credentials are tightly-secured](/privacy-and-security/#third-party-oauth-grants-api-keys-and-environment-variables). - -If you use an existing secrets store, or manage credentials in a database, you can also [pass those to Pipedream at runtime](/connected-accounts/external-auth/) instead of connecting accounts in the UI. - -## Supported Apps - -Pipedream supports [{process.env.PUBLIC_APPS}+ apps](https://pipedream.com/apps), and we're adding more every day. - -If you don't see an integration for a service you need, you can [request the integration here](#requesting-a-new-app-or-service), or [use environment variables](/environment-variables) to manage custom credentials. - -## Types of Integrations - -### OAuth - -For services that support OAuth, Pipedream operates an OAuth application that mediates access to the service so you don't have to maintain your own app, store refresh and access tokens, and more. - -When you connect an account, you'll see a new window open where you authorize the Pipedream application to access data in your account. Pipedream stores the OAuth refresh token tied to your authorization grant, automatically generating access tokens you can use to authorized requests to the service's API. You can [access these tokens in code steps](/code/nodejs/auth/). - -### Key-based - -We also support services that use API keys or other long-lived tokens to authorize requests. - -For those services, you'll have to create your keys in the service itself, then add them to your connected accounts in Pipedream. - -For example, if you add a new connected account for **Sendgrid**, you'll be asked to add your Sendgrid API key. - -## Connecting accounts - -### From an action - -Prebuilt actions that connect to a specific service require you connect your account for that service before you run your workflow. Click the **Connect [APP]** button to get started. - -Depending on the integration, this will either: - -- Open the OAuth flow for the target service, prompting you to authorize Pipedream to access your account, or -- Open a modal asking for your API credentials for key-based services - -If you've already connected an account for this app, you'll also see a list of existing accounts to select from. - -### From the HTTP Request action - -Craft a custom HTTP request in a workflow with a connected account _without code_. - -In a new step, select the **Send any HTTP Request** to start a new HTTP Request action. - -![Starting a new HTTP request action in a workflow](https://res.cloudinary.com/pipedreamin/image/upload/v1672947285/docs/CleanShot_2023-01-05_at_14.34.25_wi8rcc.png) - -Then, within the new HTTP request, open the **Authorization Type** dropdown to select a **Select an app**: - -![Opening the HTTP Request Authorization Type dropdown](https://res.cloudinary.com/pipedreamin/image/upload/v1673535917/docs/CleanShot_2023-01-12_at_10.05.02_vmttbf.png) - -This will open a new prompt to select an app to connect with. Once you select an app, the HTTP request will be updated with the correct headers to authenticate with that app's API. - -![Select an account](https://res.cloudinary.com/pipedreamin/image/upload/v1673536044/docs/CleanShot_2023-01-12_at_10.07.06_rejzyy.gif) - -Once you connect the selected app account Pipedream will autmatically include your account's authentication keys in the request in the headers, as well as update the URL to match the selected service. - -Now you can modify the request path, method, body or query params to perform an action on the endpoint with your authenticated account. - -### From a code step - -You can connect accounts to code steps by using an `app` prop. Refer to the [connecting apps in Node.js documentation](/code/nodejs/auth/). - -For example, you can connect to Slack from Pipedream (via their OAuth integration), and use the access token Pipedream generates to authorize requests: - -```javascript -import { WebClient } from '@slack/web-api'; - -// Sends a message to a Slack Channel -export default defineComponent({ - props: { - slack: { - type: 'app', - app: 'slack' - } - }, - async run({ steps, $ }) { - const web = new WebClient(this.slack.$auth.oauth_access_token) - return await web.chat.postMessage({ - text: "Hello, world!", - channel: "#general", - }) - } -}); -``` - -## Managing Connected Accounts - -Visit your [Accounts Page](https://pipedream.com/accounts) to see a list of all your connected accounts. - -On this page you can: - -- Connect your account for any integrated app -- [View and manage access](#access-control) for your connected accounts -- Delete a connected account -- Reconnect an account -- Change the nickname associated with an account - -You'll also see some data associated with these accounts: - -- For many OAuth apps, we'll list the scopes for which you've granted Pipedream access -- The workflows that are using the account - -### Connecting a new account - -1. Visit [https://pipedream.com/accounts](https://pipedream.com/accounts) -2. Click the **Connect an app** button at the top-right. -3. Select the app you'd like to connect. - -### Reconnecting an account - -If you encounter errors in a step that appear to be related to credentials or authorization, you can reconnect your account: - -1. Visit [https://pipedream.com/accounts](https://pipedream.com/accounts) -2. Search for your account -3. Click on the _..._ next to your account, on the right side of the page -4. Select the option to **Reconnect** your account - -## Access Control - -**New connected accounts are private by default** and can only be used by the person who added it. - - -Accounts connected before August 2023 - -Prior to this change, all connected accounts were accessible to all workspace members. Accounts connected before 2023 will be accessible to all workspace members by default. You can [make them private](#managing-access) at any time. - - -### Managing access - -- Find the account on the Accounts page and click the 3 dots on the far right of the row -- Select **Manage Access** - -![Selecting Manage Access](https://res.cloudinary.com/pipedreamin/image/upload/v1691617725/manage-access-button_bgnebz.png) - -- You may be prompted to reconnect your account first to verify ownership of the account -- You can enable access to the entire workspace or individual members - -![Managing Access for a Connected Account](https://res.cloudinary.com/pipedreamin/image/upload/v1691614603/manage-access-modal_crmx3f.gif) - -### Collaborating with others - -Even if a workspace member doesn't have access to a private connected account, you can still collaborate together on the same workflows. - -Workspace members who don't have access to a connected account **can perform the following actions** on workflows: - -- Reference step exports -- Inspect prop inputs, step logs, and errors -- Test any step, so they can effectively develop and debug workflows end to end - -Workspace members who do **not** have access to a given connected account **cannot modify prop inputs or edit any code** with that account. - -![Read only action](https://res.cloudinary.com/pipedreamin/image/upload/v1691622307/read-only-action_uvdh1p.png) - -![Read only code step](https://res.cloudinary.com/pipedreamin/image/upload/v1691621275/read-only-code-step_ijqvjc.png) - -To make changes to steps that are locked in read-only mode, you can: - -- Ask the account owner to [grant access](#managing-access) -- Click **More Actions** and change the connected account to one that you have access to (note that this may remove some prop configurations) - -### Explanation of access - -Access to connected accounts is enforced at the step-level within workflows and is designed with security and control in mind. - -When you connect an account in Pipedream, you are the owner of that connected account, and you always have full access. You can: - -- Manage access -- Delete -- Reconnect -- Add to any step or trigger - -How workspace members can use connected accounts that are **private**: - -
- -
- -| Operations | Workspace Owner & Admin | Other Members | -| -------------------------------------------------- | :---------------------: | :----------------: | -| View on [Accounts](https://pipedream.com/accounts) | ✅ | ❌ | -| Add to a new trigger or step | ❌ | ❌ | -| Modify existing steps | ❌ | ❌ | -| Test exising steps | ✅ | ✅ | -| Manage access | ❌ | ❌ | -| Reconnect | ❌ | ❌ | -| Delete | ✅ | ❌ | - -How workspace members can use connected accounts that are **shared**: - -
- -
- -| Operations | Workspace Owner & Admin | Other Members | -| -------------------------------------------------- | :---------------------: | :----------------: | -| View on [Accounts](https://pipedream.com/accounts) | ✅ | ✅ | -| Add to a new trigger or step | ✅ | ✅ | -| Modify existing steps | ✅ | ✅ | -| Test exising steps | ✅ | ✅ | -| Manage access | ❌ | ❌ | -| Reconnect | ❌ | ❌ | -| Delete | ✅ | ❌ | - -### Frequently Asked Questions - - -Why isn't my connected account showing up in the legacy workflow builder? - -In order to use a connected account in the legacy (v1) workflow builder, the account must be shared with the entire workspace. Private accounts are accessible in the latest version of the workflow builder. - - -#### What is the "Owner" column? - -The owner column on the Accounts page indicates who in the workspace originally connected the account (that is the only person who has permissions to manage access). - -#### Why is there no "Owner" for certain connected accounts? - -Accounts that were connected before August 2023 don't have an owner associated with them, and are shared with the entire workspace. In order to manage access for any of those accounts, we'll first prompt you to reconnect. - -#### How can I restrict access to a connected account shared with the workspace? - -See above for info on [managing access](#managing-access). - -#### Can I still work with other people on a single workflow, even if I don't want them to have access to my connected account? - -Yes, see the section on [collaborating with others](#collaborating-with-others). - -## Accessing credentials via API - -You can access credentials for any connected account via API, letting you build services anywhere and use Pipedream to handle auth. See [the guide for accessing credentials via API](/connected-accounts/api/) for more details. - -## Passing external credentials at runtime - -If you use a secrets store like [HashiCorp Vault](https://www.vaultproject.io/) or [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/), store credentials in a database, or use a service like [Nango](https://www.nango.dev/) to manage auth, you can retrieve these secrets at runtime and pass them to any step. [See the full guide here](/connected-accounts/external-auth/). - -## Account security - -[See our security docs](/privacy-and-security/#third-party-oauth-grants-api-keys-and-environment-variables) for details on how Pipedream secures your connected accounts. - -## Requesting a new app or service - -1. Visit [https://pipedream.com/support](https://pipedream.com/support) -2. Scroll to the bottom, where you'll see a Support form. -3. Select **App / Integration questions** and submit the request. - diff --git a/docs-v2/pages/deprecated/airtable/_meta.tsx b/docs-v2/pages/deprecated/airtable/_meta.tsx new file mode 100644 index 0000000000000..1d008b16d92f2 --- /dev/null +++ b/docs-v2/pages/deprecated/airtable/_meta.tsx @@ -0,0 +1,6 @@ +export default { + "oauth-migration-2024-02": { + display: "children", + }, +} as const + diff --git a/docs-v2/pages/deprecated/airtable/oauth-migration-2024-02/_meta.tsx b/docs-v2/pages/deprecated/airtable/oauth-migration-2024-02/_meta.tsx new file mode 100644 index 0000000000000..0f563ee9fd4e2 --- /dev/null +++ b/docs-v2/pages/deprecated/airtable/oauth-migration-2024-02/_meta.tsx @@ -0,0 +1,6 @@ +export default { + "index": { + "display": "hidden", + }, +} as const + diff --git a/docs-v2/pages/airtable/oauth-migration-2024-02/index.mdx b/docs-v2/pages/deprecated/airtable/oauth-migration-2024-02/index.mdx similarity index 100% rename from docs-v2/pages/airtable/oauth-migration-2024-02/index.mdx rename to docs-v2/pages/deprecated/airtable/oauth-migration-2024-02/index.mdx diff --git a/docs-v2/pages/deprecated/migrate-from-v1/_meta.tsx b/docs-v2/pages/deprecated/migrate-from-v1/_meta.tsx new file mode 100644 index 0000000000000..4705b04ff8795 --- /dev/null +++ b/docs-v2/pages/deprecated/migrate-from-v1/_meta.tsx @@ -0,0 +1,5 @@ +export default { + "index": { + "display": "hidden", + }, +} as const diff --git a/docs-v2/pages/deprecated/migrate-from-v1/index.mdx b/docs-v2/pages/deprecated/migrate-from-v1/index.mdx new file mode 100644 index 0000000000000..67320a96de2e4 --- /dev/null +++ b/docs-v2/pages/deprecated/migrate-from-v1/index.mdx @@ -0,0 +1,353 @@ +import Callout from '@/components/Callout' + +# Migrate from v1 + + +Never used Pipedream v1? You can skip this migration guide and read on about [Steps](/workflows/#steps). + + +We are excited to announce that we have launched a new version (v2) of Pipedream to all new and existing users! + +We have re-imagined the UX from the ground up, made the product much easier to use and have improved performance. In addition, we are introducing powerful new features including: + +- **Edit & test** your workflows in separate editing mode without impacting live workflows +- **Support for multiple languages** including [Node.js](/workflows/building-workflows/code/nodejs/), [Python](/workflows/building-workflows/code/python/), [Bash](/workflows/building-workflows/code/bash/) and [Go](/workflows/building-workflows/code/go/) +- **Granular testing** including the ability to test individual steps and more +- **Multiple triggers** are now supported per workflow +- **Improved** forms for easier configuration and streamlined building + +_Get Started_ + +- Read our [quickstart](/quickstart/), [docs](/), and/or [FAQ](#faq) +- Have questions? Ask here or on [Discourse](https://pipedream.com/community) +- As a reminder, all integration components are source-available and [hosted on GitHub](https://github.com/PipedreamHQ/pipedream). You can [contribute your own components](/workflows/contributing/) or improve existing ones. + +Watch a demo: + + + +And this is just the beginning — we have an exciting roadmap planned for 2022 including workflow serialization and GitHub integration. + +## New Builder Overview + +Fundamentally, the new version of the workflow builder gives you the same abilities to build, test and deploy your workflows. However, you'll notice some differences in how to build workflows. + +### Building vs Inspecting + +In v1, building your workflow and inspecting past events were visible in the same view. The new v2 builder has improved this by separating the workflow **Builder** from the workflow events **Inspector**. + +Switch between these contexts using the menu in the top right of the workflow builder. + +![Switching between the Builder & Inspector contexts in the v2 workflow builder](/images/migrate-from-v1/new-builder-context-switcher.gif) + +When you first open a deployed workflow, you're presented with the **Inspector** version of the workflow. In this view you can see logs of past events, and select them to see the results of each step in the workflow. + +![Example of events listed in the Inspector](/images/migrate-from-v1/inspector-sample.png) + +To edit the workflow, click the **Edit** button in the top right hand corner. This will close the inspector and allow you to edit your workflow without the distraction of logs from the production flow. + +![Example of the new dedicated Builder mode](/images/migrate-from-v1/builder-mode-sample.png) + +### Testing Changes + +In the v1 workflow builder, you had to deploy the whole workflow to test changes to any step. To make changes to a deployed workflow, you had to made edits on the live version. + +We've improved this flow. Now you can test your changes with a new **Test** button without effecting the live version of the workflow. + +In addition to testing single steps, you can now selectively test portions of your workflow (e.g. all steps above or below the selected step): + +![Selectively pick testing your workflow above or below the current step is now available.](/images/migrate-from-v1/test-workflow-portions.png) + +#### Testing individual events + +Not only can you test portions of your workflow in isolation, but you can also select a specific event to run against your workflow. + +In the **Test Trigger** portion of your trigger, you can select a past event seen by the workflow and build your steps against it - without having to re-trigger it manually: + +![Test your workflow with a specific event](/images/migrate-from-v1/testing-individual-events.gif) + +### Deploying Changes + +After you're happy with your changes, **deploy** them to your production workflow. Just click the **Deploy** button in the top right hand corner of the screen. + +After deploying your changes, your workflow is now live, and any changes you made will run against incoming events. + +## Node.js Code Step Changes + +There are a few changes to the Node.js code steps that you should know about. Some functions have been renamed for more clarity, and we've aligned the Node.js code steps closer to the [Component API](/workflows/contributing/components/). + +### Code Scaffolding Format + +In v1, the Node.js steps would automatically scaffold new Node.js steps in this format: + +```javascript +async (event, steps) { + // your code could be entered in here +} +``` + +In v2, the new scaffolding is wrapped with a new `defineComponent` function: + +```javascript +defineComponent({ + async run({ steps, $ }) { + // your code can be entered here + }, +}); +``` + +1. The `event` from the trigger step is still available, but exposed in `steps.trigger.event` instead. +2. The `$` variable has been passed into the `run` function where your code is executed. + +You can think of the `$` as the entry point to built in Pipedream functions. In v1, this special functions included `$end`, `$respond`, etc. In v2, these have been remapped to `$.flow.exit` and `$.respond` respectively. + +These changes unify workflow development to the [Component API](/workflows/contributing/components/api/) used by pre-built actions and also allows the [defining of props](#params-vs-props) from within your code steps. + +### Using 3rd party packages + +In v1, you had to define your imports of 3rd party packages within the scaffolded function: + +```javascript +async (event, steps) { + const axios = require('axios'); + // your code could be entered in here +} +``` + +Now, in v2 workflows you can `import` your packages in the top of the step, just like a normal Node.js module: + +```javascript +import axios from "axios"; + +defineComponent({ + async run({ steps, $ }) { + // your code can be entered here + }, +}); +``` + +Allowing all of the scaffolding to be edited opens up the ability to [pass props](/workflows/building-workflows/code/nodejs/#passing-props-to-code-steps) into your Node.js code steps, which we'll cover later. + +### Step Exports + +In v1, you could assign arbitrary properties to `this` within a Node.js step and the properties would be available as step exports: + +```javascript +// this step's name is get_customer_data +async (event, steps) { + this.name = 'Dylan'; + // downstream steps could use steps.get_customer_data.name to retrieve 'Dylan' +} +``` + +In v2 you use $.export to export data, instead:: + +```javascript +// this step's name is get_customer_data +defineComponent({ + async run({ steps, $ }) { + $.export("name", "Dylan"); + // downstream steps can use steps.get_customer_data.name to retrieve 'Dylan' + }, +}); +``` + + +Using `return` to export data is the same from v1 to v2. You can still `return` data, and it will be available to other steps with `steps.[stepName].$return_value. + + +### Exiting a workflow early + +In v1, the `$end` function can be called to exit a flow early: + +```javascript +async (event, steps) { + $end('Exiting the whole workflow early'); + console.log('I will never run'); +} +``` + +In v2, this same function is available, but under `$.flow.exit`: + +```javascript +defineComponent({ + async run({ steps, $ }) { + return $.flow.exit("Exiting the workflow early"); + console.log("I will never run"); + }, +}); +``` + +### Params vs Props + +In the v1 builder, you could pass input to steps using `params`. In the v2 builder, you pass input using [props](/workflows/contributing/components/api/#component-api). + +You can still enter free text and select data from other steps in pre-built actions. Also can add your own custom props that accept input like strings, numbers and more just like in v1. + +#### Defining params + +In the v1 workflow builder, params could be structured or unstructured. The params schema builder allowed you to add your own custom params to steps. + +In v2, you can add your own custom props without leaving the code editor. + +```javascript +export default defineComponent({ + props: { + firstName: { + type: "string", + label: "Your first name", + }, + }, + async run({ steps, $ }) { + console.log(this.firstName); + }, +}); +``` + +In the example, you added a firstName string prop. The value assigned to this prop in the workflow builder. + +Additionally, Pipedream renders a visual component in the step **Configuration** to accept this input: + +![Custom props render in the Configuration portion of the code step.](/images/migrate-from-v1/custom-string-prop.png) + +### Connecting apps + +In the v2 builder, you can connect apps with your code using [props](/workflows/contributing/components/api/#props). + +Above the `run` function, define an app prop that your Node.js step integrates with: + +```javascript +import { axios } from "@pipedream/platform"; + +export default defineComponent({ + props: { + slack: { + type: "app", + app: "slack", + }, + }, + async run({ steps, $ }) { + return await axios($, { + url: `https://slack.com/api/users.profile.get`, + headers: { + Authorization: `Bearer ${this.slack.$auth.oauth_access_token}`, + }, + }); + }, +}); +``` + +After testing the step, you'll see the Slack app will appear in the **Configuration** section on the left hand side. In this section you can choose which Slack account you'd like to use in the step. + +![Example of adding an app connection to a v2 Node.js step](/images/migrate-from-v1/app-props-example.png) + +### HTTP Response + +You can still return an HTTP response from an HTTP-triggered workflow. + +Use [`$.respond`](/workflows/building-workflows/triggers/#http) to send a JSON or string response from the HTTP call that triggered the workflow. + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + $.respond({ + status: 200, + headers: {}, + body: { + message: "hello world!", + }, + }); + }, +}); +``` + +Please note, you'll also need to configure the HTTP trigger step to also allow custom responses. Use the dropdown in the **HTTP Response** section of the HTTP trigger to select the **Return a custom response from your workflow** option: + +![Select the option to allow your workflow to send it's own HTTP responses](/images/migrate-from-v1/custom-http-response-option.png) + +## Known Gaps & Limitations + +However, some features from the original builder are not currently available in v2. The Pipedream team is working to quickly address these items, but if you have feedback that isn't listed here, please [reach out](https://pipedream.com/support). + +### Sharing workflows + +At this time, sharing is not yet implemented in v2 of the workflow builder. As workaround, create your workflows in a organization which make workflows available to your team members. + +If you need assistance transferring workflows across accounts, [please contact us](https://pipedream.com/support). + +### `$checkpoint` + +The `$checkpoint` functionality to save data between workflow runs is not supported in v2, and has been replaced by [Data Stores](/workflows/building-workflows/code/nodejs/using-data-stores/). + +### Public workflows + +At this time, all v2 workflows are private. Unfortunately at this time there is no workaround. We'll announce when a workaround for this limitation is available. + +If you're working with Pipedream support to troubleshoot your workflow, you can share it with the support team under your workflow's **Settings**. + +### Rolling back a specific version + +In v2, you can test and save your progress on a workflow _without_ deploying it. + +However, after deploying it's not possible to rollback to a prior version of a deployed workflow. + +You can still edit a deployed workflow, just like in v1 but automatic version rollbacks are not currently possible. + +### Replaying production events + +In the v2 builder, you can still view individual events that trigger your v2 workflows in the **Inspector** events log. You can delete specific events or all of them in one click as well. + +To replay past events against your deploy v2 workflows, open the event's menu and click **Replay Event**. This will rerun your workflow with this same event. + +## FAQ + +### What are the benefits of the new (v2) workflow builder? + +- **Edit & test** your workflows in separate editing mode without impacting live workflows +- **Support for multiple languages** including Node, Python, Golang & bash +- **Granular testing** including the ability to test individual steps and more +- **Multiple triggers** are now supported per workflow +- **Improved** forms for easier configuration and streamlined building + +### What are the limitations of the new (v2) workflow builder? + +- `$checkpoint` has been removed from v2 workflows, but [Data Stores](/workflows/building-workflows/code/nodejs/using-data-stores/) provides a similar API. +- Sharing workflows is not supported +- Making workflows public is not supported + +### Are v2 workflows backwards compatible? + +No, v2 workflows are not currently compatible with the v1 builder. + +However, pre-built component actions are still compatible across both versions. If you do encounter a gap from v1 actions in the v2 builder, [reach out to us](https://pipedream.com/support). + +### Is the Component API changing as well? Will I need to rewrite Components? + +No. Any components in the public registry or any private components you have published in your account are compatible with v2. + +The v2 workflow builder utilizes the same Component API allowing you to create components from within your workflows, which was not possible in v1. + +### Will I still be able to open and edit v1 workflows? + +Yes, absolutely you will still be able to view and edit v1 workflows. There is no need to immediately change your workflows from v1 to v2. + +### How do I migrate v1 workflows to v2 workflows? + +At this time we do not have an automated process to change v1 to v2. To create a v2 equivalent workflow, you can recompose your v1 workflow in the v2 builder. + +However, if it uses custom Node.js code steps, be sure to [follow the changes we describe in the guide above](/deprecated/migrate-from-v1/#nodejs-code-step-changes). + +### When will the new (v2) workflow builder be the default builder for all customers? + +By default, existing users will still default to the v1 builder. You can create new v2 workflows from the dropdown menu next to the New workflow button. + +if you'd like to default to the v2 builder when creating new workflows, you can change the **Builder Version** in [your account settings](https://pipedream.com/settings/account). + +### When will I no longer be able to create v1 workflows? + +There is currently no deprecation date for v1 workflows. We will continue to support of v1 workflows until we have feature parity with v2. + +When this date becomes clear we will provide assistance to automatically and assist migrate v1 to v2 workflows for you. diff --git a/docs-v2/pages/deprecated/nodejs20-faq-2024-02/_meta.tsx b/docs-v2/pages/deprecated/nodejs20-faq-2024-02/_meta.tsx new file mode 100644 index 0000000000000..4705b04ff8795 --- /dev/null +++ b/docs-v2/pages/deprecated/nodejs20-faq-2024-02/_meta.tsx @@ -0,0 +1,5 @@ +export default { + "index": { + "display": "hidden", + }, +} as const diff --git a/docs-v2/pages/deprecated/nodejs20-faq-2024-02/index.mdx b/docs-v2/pages/deprecated/nodejs20-faq-2024-02/index.mdx new file mode 100644 index 0000000000000..e6097925a2f03 --- /dev/null +++ b/docs-v2/pages/deprecated/nodejs20-faq-2024-02/index.mdx @@ -0,0 +1,314 @@ +import Callout from '@/components/Callout' + +# Update to the Node.js runtime on Pipedream (February 2024) + +Effective 2024-02-01, the Node.js runtime will no longer load Amazon-specific certificate authority (CA) certificates by default. + + + +### Will this impact my workflows? + +**Existing workflows that are not re-deployed will NOT be impacted.** + +Only workflow that meet all of following criteria will be impacted: + +- Is re-deployed or deployed after 2024-02-01 +- Connects to an [AWS RDS](https://aws.amazon.com/rds/)-managed database (e.g., PostgreSQL, MySQL, or Microsoft SQL Server) +- Has server identity verification enabled (e.g., the `rejectUnauthorized` connection option is set to `true`) + + +Workflows deployed on or after 2024-02-01 that do *not* integrate with AWS RDS will not be impacted. + + +### Why are Amazon-specific CA certificates no longer loaded by default? + +Pipedream's runtime, which is based on the [Amazon Web Services](https://aws.amazon.com/) (AWS) Lambda Node.js runtime, is being upgraded to support Node.js 20. Starting with Node.js 20, Lambda no longer loads and validates Amazon-specific CA certificates by default. This improves cold start performance. + +For more information, see [Amazon's blog post](https://aws.amazon.com/blogs/compute/node-js-20-x-runtime-now-available-in-aws-lambda/). + +### How will this impact my workflows? + +Starting 2024-02-01, relevant database connection attempts will return a message like this: + +![Missing CA Certificate](https://res.cloudinary.com/pipedreamin/image/upload/v1705428110/self-signed-cert-in-cert-chain-error_fkvph0.png) + +### What do I need to do? + +If you are not planning to update and re-deploy a workflow [impacted by this update](#will-this-impact-my-workflows), you do not need to do anything. + +Otherwise, to successfully connect to your database, you can disable server identity verification or include CA certificates for your database. + +For more information on secure connections and CAs, see the AWS docs: [Using SSL/TLS to encrypt a connection to a DB instance](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html) + +Below are instructions for updating a workflow that connects to a [**MySQL**](#mysql), [**PostgreSQL**](#postgresql), or [**Microsoft SQL Server**](#microsoft-sql-server) DB instance. + +#### MySQL + +**Using a Pipedream action**: + +- Option A: Disable server identity verification + 1. If given the option, update the action to the latest version by clicking the "Update" button in the workflow step. The lastest version of MySQL actions disable server identity verification by default. + + ![Update Action Button](https://res.cloudinary.com/pipedreamin/image/upload/v1705519878/update-action-button-mysql_fr549p.png) + +- Option B: Use the [MySQL (SSL)](https://pipedream.com/apps/mysql-ssl) app + 1. Replace your MySQL action with the corresponding MySQL (SSL) action. + 2. [Connect](/integrations/connected-accounts/#connecting-accounts) your MySQL (SSL) account, specifying the `key`, `cert`, `ca`, and `rejectUnauthorized` connection options. + +**Using a custom code step**: + +- Option A: Disable server identity verification + 1. Set the `rejectUnauthorized` connection option to `false`. For example: + + ```javascript + const connection = await mysql.createConnection({ + host, + user, + password, + database, + ssl: { + rejectUnauthorized: false, + } + }); + ``` + +- Option B: Use the [MySQL (SSL)](https://pipedream.com/apps/mysql-ssl) app + 1. Replace the `mysql` app prop with a `mysql_ssl` app prop. + 2. Use the SSL connection options contained in the `$auth` object. + + Here's an example of an updated code step that uses the **`mysql_ssl`** app and the [`mysql2` npm package](https://www.npmjs.com/package/mysql2): + + ```javascript + import mysql from 'mysql2/promise'; + export default defineComponent({ + props: { + mysql: { + type: "app", + app: "mysql_ssl", + } + }, + async run({steps, $}) { + const { host, port, username, password, database, ca, cert, key, rejectUnauthorized } = this.mysql.$auth; + const connection = await mysql.createConnection({ + host, + port, + user: username, + password, + database, + ssl: { + rejectUnauthorized, + ca, + cert, + key, + } + }); + const [rows] = await connection.execute('SELECT NOW()'); + return rows; + }, + }) + ``` + +#### PostgreSQL + +**Using a Pipedream action**: + +1. If given the option, update the action to the latest version by clicking the "Update" button in the workflow step. + + ![Update Action Button](https://res.cloudinary.com/pipedreamin/image/upload/v1705519996/update-action-button-postgresql_aadmqm.png) + +2. Set the **Reject Unauthorized** field to `false`. + + ![Reject Unauthorized Field](https://res.cloudinary.com/pipedreamin/image/upload/v1705520053/postgresql-reject-unauthorized-field_ualjtm.png) + +**Using a custom code step**: + +- Option A: Disable server identity verification + 1. Set the `rejectUnauthorized` connection option to `false`. For example: + + ```javascript + const client = new Client({ + host, + database, + user, + password, + ssl: { + rejectUnauthorized: false, + }, + }); + ``` + +- Option B: Include all Amazon CA certificates + 1. Read the certificate file with all Amazon CA certificates located at `/var/runtime/ca-cert.pem`. + 2. Include the CA certificates in the database connection options. + + Here's an example code step that uses the [`pg` npm package](https://www.npmjs.com/package/pg): + + ```javascript + import { Client } from "pg"; + import fs from "fs"; + export default defineComponent({ + props: { + postgresql: { + type: "app", + app: "postgresql", + } + }, + async run({steps, $}) { + const { host, user, password, port, database } = this.postgresql.$auth; + const client = new Client({ + host, + database, + user, + password, + port, + ssl: { + rejectUnauthorized: true, + ca: fs.readFileSync("/var/runtime/ca-cert.pem") + }, + }); + await client.connect(); + const results = (await client.query("SELECT NOW()")).rows; + $.export("results", results); + await client.end(); + }, + }); + ``` + +- Option C: Include your region's certificate bundle + + 1. Download the [certificate bundle for your AWS region](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.RegionCertificates). + 2. Import the certificate bundle into your workflow using [the file store](/workflows/projects/file-stores). + 3. Include the CA certificates in the database connection options. + + Here's an example code step that uses the `pg` npm package: + + ```javascript + import { Client } from "pg"; + import fs from "fs"; + export default defineComponent({ + props: { + postgresql: { + type: "app", + app: "postgresql", + } + }, + async run({steps, $}) { + const { host, user, password, port, database } = this.postgresql.$auth; + const client = new Client({ + host, + database, + user, + password, + port, + ssl: { + rejectUnauthorized: true, + ca: fs.readFileSync(steps.trigger.context.attachments["-bundle.pem"]).toString(), + }, + }); + await client.connect(); + const results = (await client.query("SELECT NOW()")).rows; + $.export("results", results); + await client.end(); + }, + }); + ``` + +#### Microsoft SQL Server + +**Using a Pipedream action**: + +1. [Reconnect](/integrations/connected-accounts/#reconnecting-an-account) your Microsoft SQL Server account, setting the **trustServerCertificate** field to `true`. + +**Using a custom code step**: + +- Option A: Disable server identity verification + 1. Set the `trustServerCertificate` connection option to `true`. For example: + + ```javascript + const config = { + server, + database, + user, + password, + options: { + trustServerCertificate: true, + }, + }; + ``` + +- Option B: Include all Amazon CA certificates + 1. Read the certificate file with all Amazon certificates located at `/var/runtime/ca-cert.pem`. + 2. Include the certificates in the database connection options. + + Here's an example code step that uses the [`mssql` npm package](https://www.npmjs.com/package/mssql): + + ```javascript + import sql from "mssql"; + import fs from "fs"; + export default defineComponent({ + props: { + microsoft_sql_server: { + type: "app", + app: "microsoft_sql_server", + } + }, + async run({steps, $}) { + const { host, username, password, port, database, encrypt, trustServerCertificate } = this.microsoft_sql_server.$auth; + const config = { + server: host, + port: parseInt(port, 10), + database, + user: username, + password, + options: { + encrypt, + trustServerCertificate, + cryptoCredentialsDetails: { + ca: fs.readFileSync("/var/runtime/ca-cert.pem").toString(), + }, + }, + }; + await sql.connect(config); + return await sql.query`SELECT GETDATE()`; + }, + }); + ``` + +- Option C: Include your region's certificate bundle + 1. Download the [certificate bundle for your AWS region](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.RegionCertificates). + 2. Import the certificate bundle into your workflow using [the file store](/workflows/projects/file-stores). + 3. Include the certificates in the database connection options. + + Here's an example code step that uses the `mssql` npm package: + + ```javascript + import sql from "mssql"; + import fs from "fs"; + export default defineComponent({ + props: { + microsoft_sql_server: { + type: "app", + app: "microsoft_sql_server", + } + }, + async run({steps, $}) { + const { host, username, password, port, database, encrypt, trustServerCertificate } = this.microsoft_sql_server.$auth; + const config = { + server: host, + port: parseInt(port, 10), + database, + user: username, + password, + options: { + encrypt, + trustServerCertificate, + cryptoCredentialsDetails: { + ca: fs.readFileSync(steps.trigger.context.attachments["-bundle.pem"]).toString(), + }, + }, + }; + await sql.connect(config); + return await sql.query`SELECT GETDATE()`; + }, + }) + ``` diff --git a/docs-v2/pages/deprecated/shopify-faq-2023-10/_meta.tsx b/docs-v2/pages/deprecated/shopify-faq-2023-10/_meta.tsx new file mode 100644 index 0000000000000..4705b04ff8795 --- /dev/null +++ b/docs-v2/pages/deprecated/shopify-faq-2023-10/_meta.tsx @@ -0,0 +1,5 @@ +export default { + "index": { + "display": "hidden", + }, +} as const diff --git a/docs-v2/pages/shopify-faq-2023-10/index.mdx b/docs-v2/pages/deprecated/shopify-faq-2023-10/index.mdx similarity index 100% rename from docs-v2/pages/shopify-faq-2023-10/index.mdx rename to docs-v2/pages/deprecated/shopify-faq-2023-10/index.mdx diff --git a/docs-v2/pages/destinations/_meta.json b/docs-v2/pages/destinations/_meta.json deleted file mode 100644 index 9e737f1c7e117..0000000000000 --- a/docs-v2/pages/destinations/_meta.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "index": "Overview", - "email": "Email", - "http": "HTTP", - "emit": "Emit", - "sse": "SSE", - "s3": "S3" -} diff --git a/docs-v2/pages/destinations/http.mdx b/docs-v2/pages/destinations/http.mdx deleted file mode 100644 index 083f9eb2211a8..0000000000000 --- a/docs-v2/pages/destinations/http.mdx +++ /dev/null @@ -1,124 +0,0 @@ -import Callout from '@/components/Callout' -import PublicIPs from '@/components/PublicIPs' - -# HTTP - -HTTP Destinations allow you to send data to another HTTP endpoint URL outside of Pipedream. This can be an endpoint you own and operate, or a URL tied to a service you use (for example, a [Slack Incoming Webhook](https://api.slack.com/incoming-webhooks)). - -## Using `$.send.http` in workflows - -You can send HTTP requests in [Node.js code steps](/code/nodejs/) using `$.send.http()`. - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - $.send.http({ - method: "POST", - url: "[YOUR URL HERE]", - data: { - name: "Luke Skywalker", - }, - }); - } -}); -``` - -`$.send.http()` accepts an object with all of the following properties: - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - $.send.http({ - method, // Required, HTTP method, a string, e.g. POST, GET - url, // Required, the URL to send the HTTP request to - data, // HTTP payload - headers, // An object containing custom headers, e.g. { "Content-Type": "application/json" } - params, // An object containing query string parameters as key-value pairs - auth, // An object that contains a username and password property, for HTTP basic auth - }); - } -}); - -``` - -**Destination delivery is asynchronous**: the HTTP requests are sent after your workflow finishes. This means **you cannot write code that operates on the HTTP response**. These HTTP requests **do not** count against your workflow's compute time. - -If you iterate over an array of values and send an HTTP request for each: - -```javascript - -export default defineComponent({ - async run({ steps, $ }) { - const names = ["Luke", "Han", "Leia", "Obi Wan"]; - names.forEach((name) => { - $.send.http({ - method: "POST", - url: "[YOUR URL HERE]", - data: { - name, - }, - }); - }); - } -}); -``` - -you won't have to `await` the execution of the HTTP requests in your workflow. We'll collect every `$.send.http()` call and defer those HTTP requests, sending them after your workflow finishes. - -## Using `$.send.http` in component actions - -If you're authoring a [component action](/components#actions), you can deliver data to an HTTP destination using `$.send.http`. - -`$.send.http` functions the same as [`$.send.http` in workflow code steps](#using-send-http-in-workflows): - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - $.send.http({ - method: "GET", - url: "https://example.com" - }) - } -}); -``` - -## HTTP Destination delivery - -HTTP Destination delivery is handled asynchronously, separate from the execution of a workflow. However, we deliver the specified payload to HTTP destinations for every event sent to your workflow. - -Generally, this means it should only take a few seconds for us to send the event to the destination you specify. In some cases, delivery will take longer. - -The time it takes to make HTTP requests sent with `$.send.http()` does not count against your workflow quota. - -## HTTP request and response logs - -Below your code step, you'll see both the data that was sent in the HTTP request, and the HTTP response that was issued. If you issue multiple HTTP requests, we'll show the request and response data for each. - -## What if I need to access the HTTP response in my workflow? - -Since HTTP requests sent with `$.send.http()` are sent asynchronously, after your workflow runs, **you cannot access the HTTP response in your workflow**. - -If you need to access the HTTP response data in your workflow, [use `axios`](/code/nodejs/http-requests/) or another HTTP client. - -## Timeout - -The timeout on HTTP request sent with `$.send.http()` is currently **5 seconds**. This time includes DNS resolution, connecting to the host, writing the request body, server processing, and reading the response body. - -Any requests that exceed 5 seconds will yield a `timeout` error. - -## Retries - -Currently, Pipedream will not retry any failed request. If your HTTP destination endpoint is down, or returns an error response, we'll display that response in the observability associated with the Destination in the relevant step. - -## IP addresses for Pipedream HTTP requests - -When you make an HTTP request using `$.send.http()`, the traffic will come from one of the following IP addresses: - - - -This list may change over time. If you've previously whitelisted these IP addresses and are having trouble sending HTTP requests to your target service, please check to ensure this list matches your firewall rules. - - -These IP addresses are tied specifically to the `$.send.http()` service. If you send traffic directly from a workflow, it will be sent from one of Pipedream's general range of IP addresses. [See our hosting docs for more information](/privacy-and-security/#hosting-details). - - diff --git a/docs-v2/pages/destinations/http/images/http-request-response.png b/docs-v2/pages/destinations/http/images/http-request-response.png deleted file mode 100644 index 5284528c68bb2..0000000000000 Binary files a/docs-v2/pages/destinations/http/images/http-request-response.png and /dev/null differ diff --git a/docs-v2/pages/destinations/http/images/three-http-requests.png b/docs-v2/pages/destinations/http/images/three-http-requests.png deleted file mode 100644 index 7ca0cea79235a..0000000000000 Binary files a/docs-v2/pages/destinations/http/images/three-http-requests.png and /dev/null differ diff --git a/docs-v2/pages/destinations/http/images/webhook-dest-params.png b/docs-v2/pages/destinations/http/images/webhook-dest-params.png deleted file mode 100644 index c3b8286116c95..0000000000000 Binary files a/docs-v2/pages/destinations/http/images/webhook-dest-params.png and /dev/null differ diff --git a/docs-v2/pages/destinations/http/images/webhook-destination.png b/docs-v2/pages/destinations/http/images/webhook-destination.png deleted file mode 100644 index d3ebfef6c8961..0000000000000 Binary files a/docs-v2/pages/destinations/http/images/webhook-destination.png and /dev/null differ diff --git a/docs-v2/pages/destinations/images/new-code-step.png b/docs-v2/pages/destinations/images/new-code-step.png deleted file mode 100644 index 52d32efb8f0d5..0000000000000 Binary files a/docs-v2/pages/destinations/images/new-code-step.png and /dev/null differ diff --git a/docs-v2/pages/destinations/images/new-code.png b/docs-v2/pages/destinations/images/new-code.png deleted file mode 100644 index 63f234e40ad2f..0000000000000 Binary files a/docs-v2/pages/destinations/images/new-code.png and /dev/null differ diff --git a/docs-v2/pages/destinations/images/webhook-action-params.png b/docs-v2/pages/destinations/images/webhook-action-params.png deleted file mode 100644 index 0bc68405d6fa8..0000000000000 Binary files a/docs-v2/pages/destinations/images/webhook-action-params.png and /dev/null differ diff --git a/docs-v2/pages/destinations/images/webhook-action.png b/docs-v2/pages/destinations/images/webhook-action.png deleted file mode 100644 index 8823b1c212d04..0000000000000 Binary files a/docs-v2/pages/destinations/images/webhook-action.png and /dev/null differ diff --git a/docs-v2/pages/destinations/index.mdx b/docs-v2/pages/destinations/index.mdx deleted file mode 100644 index 6c67db9d1c533..0000000000000 --- a/docs-v2/pages/destinations/index.mdx +++ /dev/null @@ -1,100 +0,0 @@ -# Destinations - -**Destinations**, like [actions](/components#actions), abstract the delivery and connection logic required to send events to services like Amazon S3, or targets like HTTP and email. - -However, Destinations are different than actions in two ways: - -- Events are delivered to the Destinations asynchronously, after your workflow completes. This means you don't wait for network I/O (e.g. for HTTP requests or connection overhead for data warehouses) within your workflow code, so you can process more events faster. -- In the case of data stores like S3, you typically don't want to send every event on its own. This can be costly and carries little benefit. Instead, you typically want to batch a collection of events together, sending the batch at some frequency. Destinations handle that batching for relevant services. - -The docs below discuss features common to all Destinations. See the [docs for a given destination](#available-destinations) for information specific to those destinations. - - - -## Available Destinations - -- [HTTP](/destinations/http/) -- [Email](/destinations/email/) -- [S3](/destinations/s3/) -- [SSE](/destinations/sse/) -- [Emit to another listener](/destinations/emit/) - -## Using destinations - -### Using destinations in workflows - -You can send data to Destinations in [Node.js code steps](/code/nodejs/), too, using `$.send` functions. - -`$.send` is an object provided by Pipedream that exposes destination-specific functions like `$.send.http()`, `$.send.s3()`, and more. This allows you to send data to destinations programmatically, if you need more control than the default actions provide. - -Let's use `$.send.http()` to send an HTTP POST request like we did in the Action example above. [Add a new action](/workflows/steps/actions), then search for "**Run custom code**": - -Create a new HTTP endpoint URL (try creating a new Pipedream workflow and adding an HTTP trigger), and add the code below to your code step, with the URL you created: - -```javascript -export default defineComponent({ - async run({ steps, $}) { - $.send.http({ - method: "POST", - url: "[YOUR URL HERE]", - data: { - name: "Luke Skywalker", - }, - }); - } -}) -``` - -See the docs for the [HTTP destination](/destinations/http/) to learn more about all the options you can pass to the `$.send.http()` function. - -Again, it's important to remember that **Destination delivery is asynchronous**. If you iterate over an array of values and send an HTTP request for each: - -```javascript -export default defineComponent({ - async run({ steps, $}) { - const names = ["Luke", "Han", "Leia", "Obi Wan"]; - for (const name of names) { - $.send.http({ - method: "POST", - url: "[YOUR URL HERE]", - data: { - name, - }, - }); - } - } -}) -``` - -you won't have to `await` the execution of the HTTP requests in your workflow. We'll collect every `$.send.http()` call and defer those HTTP requests, sending them after your workflow finishes. - -### Using destinations in actions - -If you're authoring a [component action](/components#actions), you can deliver data to destinations, too. `$.send` isn't directly available to actions like it is for workflow code steps. Instead, you use `$.send` to access the destination-specific functions: - -```javascript -export default { - name: "Action Demo", - key: "action_demo", - version: "0.0.1", - type: "action", - async run({ $ }) { - $.send.http({ - method: "POST", - url: "[YOUR URL HERE]", - data: { - name: "Luke Skywalker", - }, - }); - } -} -``` - -[See the component action API docs](/components/api/#actions) for more details. - -## Asynchronous Delivery - -Events are delivered to destinations _asynchronously_ — that is, separate from the execution of your workflow. **This means you're not waiting for network or connection I/O in the middle of your function, which can be costly**. - -Some destination payloads, like HTTP, are delivered within seconds. For other destinations, like S3 and SQL, we collect individual events into a batch and send the batch to the destination. See the [docs for a specific destination](#available-destinations) for the relevant batch delivery frequency. - diff --git a/docs-v2/pages/destinations/s3/images/s3-action.png b/docs-v2/pages/destinations/s3/images/s3-action.png deleted file mode 100644 index 08f63ecd60200..0000000000000 Binary files a/docs-v2/pages/destinations/s3/images/s3-action.png and /dev/null differ diff --git a/docs-v2/pages/destinations/s3/images/s3-dest-params.png b/docs-v2/pages/destinations/s3/images/s3-dest-params.png deleted file mode 100644 index 664eb0afe9bc7..0000000000000 Binary files a/docs-v2/pages/destinations/s3/images/s3-dest-params.png and /dev/null differ diff --git a/docs-v2/pages/event-history.mdx b/docs-v2/pages/event-history.mdx deleted file mode 100644 index 7eb58ab8cff30..0000000000000 --- a/docs-v2/pages/event-history.mdx +++ /dev/null @@ -1,101 +0,0 @@ -import Callout from '@/components/Callout' - -# Event History - -Monitor all workflow events and their stack traces in one centralized view under the [**Event History**](https://pipedream.com/event-history) section in the dashboard. - -Within the **Event History**, you'll be able to filter your events by workflow, execution status, within a specific time range. - - - -## Filtering Events - -The filters at the top of the screen allow you to search all events processed by your workflows. - -You can filter by the event's **Status**, **time of initiation** or by the **Workflow name**. - - -The filters are scoped to the current [workspace](/workspaces/). - -If you're not seeing the events or workflow you're expecting, try [switching workspaces](/workspaces/#switching-between-workspaces). - - -### Filtering by status - -The **Status** filter controls which events are shown by their status. For example selecting the **Success** status, you'll be shown all events that were successfully executed by workflows. - -![Only showing workflow events by current execution status](https://res.cloudinary.com/pipedreamin/image/upload/v1689875830/docs/docs/event%20histories/image_44_g2sabg.png) - -#### All failed workflow executions - -You can view all failed workflow executions by applying the **Error** status filter. - -This will only display the failed workflow executions in the selected time period. - -This view in particular is helpful for identifying trends of errors, or workflows with persistent problems. - -![Viewing all failed workflow executions by the filter in the Event History](https://res.cloudinary.com/pipedreamin/image/upload/v1689876111/docs/docs/event%20histories/CleanShot_2023-07-20_at_14.01.43_2x_nksdxd.png) - -#### All paused workflow executions - -Workflows that are paused from `$.flow.delay` or `$.flow.suspend` will be shown when this filter is activated. - -![Only showing workflows that are currently paused](https://res.cloudinary.com/pipedreamin/image/upload/v1689875506/docs/docs/event%20histories/CleanShot_2023-07-20_at_13.51.11_2x_kn2dpw.png) - - -If you're using `setTimeout` or `sleep` in Node.js or Python steps, the event will not be considered **Paused**. Using those language native execution holding controls leaves your workflow in a **Executing** state. - - -### Within a time frame - -Filtering by time frame will only include workflow events _started_ within the defined range. - -Using this dropdown, you can select between convenient time ranges, or specify a custom range on the right side. - -![How to filter events by a timerange](https://res.cloudinary.com/pipedreamin/image/upload/v1683747452/docs/docs/event%20histories/CleanShot_2023-05-10_at_15.37.01_2x_oxb07m.png) - - -Long running workflows - -The time range filter depends on when the execution event first started. If a long running workflow starts within the specified time range but it's execution continues _past_ the filtered timeframe, it will still be included in the results. - - -### Filtering by workflow - -You can also filter events by a specific workflow. You can search by the workflow's name in the search bar in the top right. - -![Search by workflow name in the search bar](https://res.cloudinary.com/pipedreamin/image/upload/v1683747588/docs/docs/event%20histories/CleanShot_2023-05-10_at_15.39.30_2x_yoa1k6.png) - -Alternatively, you can filter by workflow from a specific event. First, open the menu on the far right, then select **Filter By Workflow**. Then only events processed by that workflow will appear. - -![Filtering events by workflow by selecting the workflow on the right](https://res.cloudinary.com/pipedreamin/image/upload/v1683747695/docs/docs/event%20histories/CleanShot_2023-05-10_at_15.41.20_2x_ulvdns.png) - -## Inspecting events - -Clicking on an individual event will open a panel that displays the steps executed, their individual configurations, as well as the overall performance and results of the entire workflow. - -At this time it is not possible to edit the workflow with the selected event history. Only events shown in the [event inspector](/workflows/events/inspect/#the-inspector) can be choosen for the workflow builder. - -The top of the event history details will display details including the overall status of that particular event execution and errors if any. - -If there is an error message, the link at the bottom of the error message will link to the corresponding workflow step that threw the error. - -![Viewing individual event executions](https://res.cloudinary.com/pipedreamin/image/upload/v1683748495/docs/docs/event%20histories/CleanShot_2023-05-10_at_15.53.44_2x_t30gsb.png) - - -The event log is missing a workflow step, or the configuration is not what I expect - -Changing a workflow _after_ a particular event's execution will not update it's event history with that new workflow change. - -You'll need to replay an event to execute the workflow with the latest changes. - - -## Limits - -The number of events recorded and available for viewing in the Event History depends on your plan. [Please see the pricing page](https://pipedream.com/pricing) for more details. - -## Frequently asked questions - -### Are event histories available on all plans? - -Yes, event history is available for all workspace plans, including free plans. However, the length of searchable or viewable history changes depending on your plan. [Please see the pricing page](https://pipedream.com/pricing) for more details. diff --git a/docs-v2/pages/examples/_meta.json b/docs-v2/pages/examples/_meta.json deleted file mode 100644 index 3c5fe53ec1708..0000000000000 --- a/docs-v2/pages/examples/_meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "waiting-to-execute-next-step-of-workflow": { - "display": "hidden" - } -} diff --git a/docs-v2/pages/examples/waiting-to-execute-next-step-of-workflow.mdx b/docs-v2/pages/examples/waiting-to-execute-next-step-of-workflow.mdx deleted file mode 100644 index 99ad1ff93526d..0000000000000 --- a/docs-v2/pages/examples/waiting-to-execute-next-step-of-workflow.mdx +++ /dev/null @@ -1,76 +0,0 @@ -# Example: Delay a workflow before the next step runs - -At a certain point in a workflow, you may want to delay a step from running for some period of time. For example, if you've built a workflow to process new user sign ups on your site, you may want to wait one day to send the user an email asking them for feedback. - -Pipedream doesn't yet provide a built-in step to pause / delay a specific workflow step, but [we're tracking that here](https://github.com/PipedreamHQ/pipedream/issues/187). **This guide shows you a workaround for implementing this delay behavior**. - -## Step 1 - Create a Task Scheduler event source - -[Click here to create a Pipedream Task Scheduler source](https://pipedream.com/sources/new?key=pipedream-new-scheduled-tasks). This [event source](/sources/) allows you to schedule a message to trigger a workflow at a specific time (for example, "run this workflow one day in the future, with this event data"). _This is how we'll delay our step_. - -You'll need to connect your Pipedream API key to run this source. You'll find this in [your settings](https://pipedream.com/settings/account). Next, enter a secret value in the **Secret** field - this ensures only users with this secret can schedule tasks. We'll use this in **Step 3** below. - -![Create Task Scheduler](./images/create-task-scheduler.gif) - -Keep this event source open in a tab / window - you'll reference it later. - -## Step 2 - Review your workflow / delay logic - -In our example new user signup workflow, we'll implement the following logic: - -1. Workflow is triggered on an HTTP request when a new user signs up -2. Wait one day -3. Send the user a welcome email - -To delay the email by one day, we'll need to separate our logic into _two_ workflows: - -1. [Workflow #1](https://pipedream.com/@dylburger/delay-example-workflow-1-receive-http-request-with-user-email-schedule-task-one-day-in-future-p_vQCgj35/edit) receives the HTTP request when the new user signs up. Then, **it schedules a new task in our Task Scheduler event source one day in the future**. -2. [Workflow #2](https://pipedream.com/@dylburger/delay-example-workflow-2-send-email-after-delay-p_RRCzdLR/edit) will be triggered on scheduled tasks emitted by our Task Scheduler. In this example, our Task Scheduler receives the scheduled task from workflow #1, **waits one day**, and triggers workflow #2, which sends the user a welcome email. - -Let's see how to implement this. - -## Step 3 - Add the delay step to Workflow #1 - -Identify the step(s) in your workflow you'd like to delay. In our example, we want to delay the welcome email. **Above that step, click the `+` button to add a step to your workflow, select the `Search All Actions` label, and find the `Pipedream Task Scheduler - Schedule Task` step**: - -![Find Task Scheduler step](./images/find-task-scheduler-step.gif) - -Visit the Task Scheduler source you created in **Step 1** and copy its **Endpoint**: - -![Task Scheduler endpoint](./images/endpoint.png) - -Then, fill in the params of the task scheduler step: - -- To schedule a message one day in the future, enter `86400` in the **Num Seconds** field. To schedule two minutes in the future, enter `120`, etc. -- Add the **Endpoint** from your task scheduler as the **Task Scheduler URL** of your action. -- The **Message** is the data you'd like to send to Workflow #2. In this example, we add `{{event.body}}` - the HTTP payload that triggered our original workflow - so we have access to the same data in Workflow #2. -- If you configured a **Secret** when creating your Task Scheduler, click on the optional `secret` label at the bottom of the step and add it. - -![Configured task scheduler step](./images/configured-task-scheduler-step.png) - -[See this workflow for an example](https://pipedream.com/@dylburger/delay-example-workflow-1-receive-http-request-with-user-email-schedule-task-one-day-in-future-p_vQCgj35/edit). - -## Step 4 - Move the steps you'd like to delay to Workflow #2 - -Now that you've implemented the delay step in Workflow #1, **you'll need to remove any steps below that delay step from your Workflow #1. Then, you'll move these steps to a new workflow triggered by your Task Scheduler event source**. - -First, [create a new workflow](https://pipedream.com/new). In the trigger step, click the label to **Use one of your existing sources**, and select your Task Scheduler source: - -![Select Task Scheduler source](./images/select-task-scheduler-as-trigger.gif) - -Then, add the step(s) you wanted to delay from your original workflow. In our example, we wanted to send the user a welcome email after one day, so we moved the email step from Workflow #1 to Workflow #2. - -When Workflow #1 is triggered, it sends the **Message** you included to the Task Scheduler. **Num Seconds** in the future (in our example, one day), the Task Scheduler triggers Workflow #2, and includes the **Message** in its event data, accessible in the variable `event.message`: - -![Task Scheduler event data](./images/task-scheduler-event.png) - -For example, if your original HTTP payload contained an `email` property and you passed `{{event.body}}` to the Task Scheduler, you can reference that in Workflow #2 using `{{event.message.email}}`: - -![event.message.email reference](./images/task-scheduler-event.png) - -Finally, toggle your trigger step **On**: - -![Toggle trigger step on](./images/toggle-trigger-step-on.gif) - -[See this workflow for an example](https://pipedream.com/@dylburger/delay-example-workflow-2-send-email-after-delay-p_RRCzdLR/edit). - diff --git a/docs-v2/pages/glossary.mdx b/docs-v2/pages/glossary.mdx new file mode 100644 index 0000000000000..d25f57abaed92 --- /dev/null +++ b/docs-v2/pages/glossary.mdx @@ -0,0 +1,493 @@ +# Pipedream Glossary + +Below you'll find a glossary of Pipedream-specific terms. We use these in the product, docs, and other content, so if you're seeing a term for the first time, you'll probably find it below. + +All terms that aren't in this doc hold their standard technical meaning. If you see a term missing, please [reach out](https://pipedream.com/support). + +[0-9](#0---9) | [A](#a) | [B](#b) | [C](#c) | [D](#d) | [E](#e) | [F](#f) | [G](#g) | [H](#h) | [I](#i) | [J](#j) | [K](#k) | [L](#l) | [M](#m) | [N](#n) | [O](#o) | [P](#p) | [Q](#q) | [R](#r) | [S](#s) | [T](#t) | [U](#u) | [V](#v) | [W-Z](#w-z) + +## 0 - 9 + +### 2FA + +Short for [two-factor authentication](#two-factor-authentication-2fa). + +## A + +### Account + +Synonym for [connected account](#connected-account). + +### Action + +Actions are reusable code steps, written as [Pipedream components](#component). + +### Advanced plan + +Pipedream's plan for individuals and teams running production workflows. [See the pricing page](https://pipedream.com/pricing) for more details. + +### Auto-retry + +[A workflow setting](/workflows/building-workflows/settings/#auto-retry-errors) that lets you automatically retry an execution from the failed step when it encounters an error. + +## B + +### Bash runtime + +Pipedream's internal code in the [execution environment](#execution-environment) responsible for running Bash code. + +### Basic plan + +Pipedream's plan for individuals who need higher limits and the option to scale usage. [See the pricing page](https://pipedream.com/pricing) for more details. + +### Bi-directional GitHub sync + +When you configure [GitHub Sync](#github-sync), you can make changes in Pipedream and push them to GitHub, or make changes locally, push to GitHub, and deploy to Pipedream. Since changes can be made in each system and communicated to the other, the sync is bi-directional. + +### Branch + +Short for [Git branch](https://git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshell). When using [Pipedream GitHub Sync](#github-sync), you can sync a GitHub repository to a Pipedream project and manage changes to code in a branch. + +### Builder + +The Pipedream UI where you build, edit, and test workflows. + +### Business plan + +Pipedream's plan for teams with security, compliance, and support needs. [See the pricing page](https://pipedream.com/pricing) for more details. + +## C + +### Changelog + +Synonym for [project changelog](#project-changelog). + +### Code step + +[Steps](#step) that let users run [custom code](/workflows/building-workflows/code/) in a workflow. + +### Cold start + +A cold start refers to the delay between the invocation of workflow and the execution of the workflow code. Cold starts happen when Pipedream spins up a new [execution environment](#execution-environment) to handle incoming events. + +### Commit + +Short for [Git commit](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository). When using [Pipedream GitHub Sync](#github-sync), you commit changes to a branch before deploying the workflow to production. + +### Component + +Components are Node.js modules that run on Pipedream's serverless infrastructure. [Sources](#source) and [actions](#action) are two types of components. See [the component API](/workflows/contributing/components/api/) for more details. + +### Component API + +The programming interface for creating [components](#component) in Pipedream. + +### Component guidelines + +Guidelines applied to components submitted to [the Pipedream component registry](#component-registry). + +### Component registry + +The public registry of [components](#component) available to Pipedream users, [available on GitHub](https://github.com/PipedreamHQ/pipedream). + +### Concurrency + +[A workflow setting](/workflows/building-workflows/settings/concurrency-and-throttling/#concurrency) that lets users configure the number of concurrent [workers](#worker) available to process events. + +### Connected account + +A specific account or credentials used to connect to a Pipedream [integration](#integrations). If both you and your team member have an account with OpenAI, for example, you would connect each account as a distinct connected account. [See the docs](/integrations/connected-accounts) for more details. + +### Connected account access control + +You can restrict access to connected accounts to specific individuals or share with the entire workspace. [See the docs](/integrations/connected-accounts#access-control) for more details. + +### Credit + +Pipedream charges one credit per 30 seconds of compute time at 256MB megabytes of memory (the default) per workflow execution. Credits are also charged for [dedicated workers](#dedicated-workers). [See the docs](/pricing/#credits) for more details. + +### Custom domain + +By default, [HTTP endpoints](#http-endpoint) are served from the `*.m.pipedream.net` domain. You can configure a [custom domain](/workflows/domains/) if you want to host that endpoint on your own domain. + +### Custom source + +An [event source](#event-source) that you create using custom code, or by modifying a [registry source](#registry-source). + +## D + +### Data retention + +A workflow setting that allows you to configure how long Pipedream stores event data and logs associated with [executions](#execution). [See the docs](/workflows/building-workflows/settings/#data-retention-controls) for more details. + +### Dedicated workers + +[Workers](#worker) that remain available to process events, even when the workflow is not running. This can help reduce [cold starts](#cold-start) and improve performance for workflows that require low latency. [See the docs](/workflows/building-workflows/settings/#eliminate-cold-starts) for more details. + +### Deduper + +[Event sources](#event-source) can receive duplicate requests tied to the same event. Pipedream's infrastructure supports [deduplication](/workflows/contributing/components/api/#dedupe-strategies) to ensure that only unique events are emitted by a source. + +### Delay + +[A built-in service](/workflows/building-workflows/control-flow/delay/) that lets you pause a workflow for a specified amount of time. You can delay workflows using pre-built actions, or delay in code. + +### Destination + +[Destinations](/workflows/data-management/destinations/) are built-in services that abstract the delivery and connection logic required to send events to services like Amazon S3, or targets like HTTP and email. + +### Domain + +Synonum for [custom domain](#custom-domain). + +### Data store + +[Data stores](/workflows/data-management/data-stores/) are Pipedream's built-in key-value store. + +### Deploy key + +When you configure [GitHub Sync](#github-sync), you can use a deploy key to authenticate Pipedream with your GitHub repository. [See the docs](/workflows/git/#create-a-new-project-and-enable-github-sync) for more details. + +## E + +### Editor + +The built-in code editor in the [builder](#builder). + +### Email trigger + +A [workflow trigger](#trigger) that listens for incoming email. This trigger exposes a workflow-specific email address that you can use to send email to the workflow. + +### Emit + +[Event sources](#event-source), [workflow triggers](#trigger), and even workflows themselves can emit [events](#event) that trigger other [listeners](#listener). Since sources have a built-in [deduper](#deduper), not all requests are emitted as events. + +### Emitter + +A resource that [emits](#emit) [events](#event). Emitters can be [event sources](#event-source), [workflow triggers](#trigger), or even workflows themselves. + +### Error notification + +When a workflow execution encounters an error, Pipedream sends an [error notification](/workflows/building-workflows/errors/) to the configured error [listeners](#listener). + +### Environment variable + +Pipedream supports two types of environment variables: + +- [Project variables](#project-variable), available within a specific project +- [Workspace variables](#workspace-variable), available across all projects in a workspace + +### Event + +Events are emitted by [sources](#event-source) and consumed by workflows. Events can be triggered by a variety of sources, including HTTP requests, cron schedules, and third-party APIs. Events can be passed to actions, which can process the event data and perform a variety of operations, including making HTTP requests, sending emails, and interacting with third-party APIs. + +### Event context + +Metadata about a workflow execution, including the timestamp of the event, the event ID, and more. Exposed in [`steps.trigger.context`](/workflows/building-workflows/triggers/#stepstriggercontext). + +### Event data + +The content of the event, exposed in [`steps.trigger.event`](/workflows/building-workflows/triggers/). + +### Event history + +A log of all workflow events and executions, available in the [event inspector](#inspector) or the global [event history UI](/workflows/event-history/). + +### Event queue + +When using built-in [concurrency](#concurrency) or [throttling](#throttling) controls, events are queued in a workflow-specific queue and processed by available [workers](#worker). + +### Event source + +[Components](#component) that watch for events from a third-party data source, emitting those events to [listeners](#listener). + +### Execution + +When a workflow is triggered by an event, the running instance of the workflow on that event is called an execution. + +### Execution environment + +[The virtual machine](/privacy-and-security/#execution-environment) and internal Pipedream platform code that runs a workflow execution. An instance of an execution environment is called a [worker](#worker). + +### Execution rate controls + +The workflow setting that allows users to configure the number of executions a workflow can process per unit time. Also known as throttling. [See the docs](/workflows/building-workflows/settings/concurrency-and-throttling/#throttling) for more details. + +### Export + +Depending on the context, **export** can function as a noun or verb: + +- **Noun**: A synonym for [step export](#step-export) +- **Verb**: The act of exporting data from a step using Pipedream primitives like [`$.export`](/workflows/building-workflows/code/nodejs/#using-export) or `return`. + +### Expression + +In programming, expressions are code that resolve to a value. In Pipedream, [you can use expressions within props forms](/workflows/building-workflows/using-props/#entering-expressions) to reference prior steps or compute custom values at runtime. + +### External credentials + +[Connected accounts](#connected-account) are accounts that users link directly in Pipedream. External credentials are credentials that users store in their own database or service, and reference in Pipedream at runtime. [See the docs](/integrations/connected-accounts/external-auth) for more details. + +## F + +### File store + +[File stores](/workflows/projects/file-stores) are filesystems scoped to projects. Any files stored in the file store are available to all workflows in the project. + +### Filter + +[Built-in actions](https://pipedream.com/apps/filter) that let you continue or stop a workflow based on a condition. + +### Folder + +Within projects, you can organize workflows into folders. + +### Free plan + +Pipedream's free plan. [See the limits docs](/workflows/limits/) for more details. + +## G + +### Global search + +Press `Ctrl + K` or `Cmd + K` to open the global search bar in the Pipedream UI. + +### GitHub Sync + +When enabled on a [project](#project), GitHub Sync syncs the project's workflow code with a GitHub repository. [See the docs](/workflows/git/) for more details. + +### Golang runtime + +Pipedream's internal code in the [execution environment](#execution-environment) responsible for running Go code. + +## H + +### Helper functions + +[Built-in actions](https://pipedream.com/apps/helper-functions) that convert data types, format dates, and more. + +### Hooks + +[Hooks](/workflows/contributing/components/api/#hooks) are functions executed as a part of the [event source](#event-source) lifecycle. They can be used to perform setup tasks before the source is deployed, or teardown tasks after the source is destroyed. + +### HTTP endpoint + +The URL tied to a [workflow HTTP trigger](#http-trigger) or HTTP-triggered [event source](#event-source). + +### HTTP trigger + +A [workflow trigger](#trigger) that listens for incoming HTTP requests. This trigger exposes a unique URL that you can use to send HTTP requests to the workflow. + +## I + +### Inspector + +The Pipedream UI that displays a specific workflow's event history. [See the docs](/workflows/building-workflows/inspect/) for more details. + +### Integrations + +When Pipedream adds a new third-party service to our marketplace of apps, we often have to handle details of the OAuth process and authentication, and build [sources](#event-source) and [actions](#action) for the API. These details are abstracted from the user, and the app configuration is referred to as an **integration**. + +## J + +## K + +### Key-based account + +A [connected account](#connected-account) that uses static credentials, like API keys. + +## L + +### Listener + +A resource that listens for events emitted by [emitters](#emitter). Listeners can be [workflows](#workflow), [event sources](#event-source), webhook URLs, and more. + +### Logs + +Standard output and error logs generated by steps during a workflow execution. Logs are available as a part of the step execution details in the [event inspector](#inspector) or the global [event history UI](/workflows/event-history/). + +## M + +### Merge + +When you configure [GitHub Sync](#github-sync), you can merge changes from a branch into the production branch of your GitHub repository, deploying those changes to Pipedream. + +## N + +### Node.js runtime + +Pipedream's internal code in the [execution environment](#execution-environment) responsible for running Node.js code. + +## O + +### Organization + +Synonym for [workspaces](#workspace). + +### OAuth account + +A [connected account](#connected-account) that uses OAuth to authenticate with a third-party service. + +## P + +### Premium apps + +Pipedream's built-in [integrations](#integrations) that require a paid plan to use. [See the pricing page](https://pipedream.com/pricing) for more details and the [full list of premium apps](/integrations/apps#premium-apps). + +### Project + +A container for workflows, secrets, and other resources in Pipedream. Projects can be synced with a GitHub repository using [GitHub Sync](#github-sync). [See the docs](/workflows/projects/) for more details. + +### Project-based access control + +You can restrict access to projects to specific individuals or share with the entire workspace. [See the docs](/workflows/projects/access-controls) for more details. + +### Project changelog + +When using [Pipedream GitHub Sync](#github-sync), the changelog shows the history of changes made to a project. + +### Project file + +A file stored in a [file store](#file-store). + +### Project secret + +Users can add both standard project variables and secrets to a project. The values of secrets are encrypted and cannot be read from the UI once added. + +### Project settings + +Configure GitHub Sync and other project-specific configuration in a project's settings. + +### Project variable + +Project-specific environment variables, available to all workflows in a project. + +### Props + +[Props](/workflows/building-workflows/using-props/) allow you to pass input to [components](#component). + +### Python runtime + +Pipedream's internal code in the [execution environment](#execution-environment) responsible for running Python code. + +### Object explorer + +The [builder](#builder) UI that allows you to search objects [exported](#export) from prior steps. [See the docs](/workflows/building-workflows/using-props/#use-the-object-explorer) for more details. + +## Q + +## R + +### Registry + +Synonym for [component registry](#component-registry). + +### Registry source + +An [event source](#event-source) available in the [component registry](#component-registry). Registry sources are reviewed and approved by Pipedream. + +## S + +### Schedule trigger + +A [workflow trigger](#trigger) that runs on a schedule. This trigger exposes a cron-like syntax that you can use to schedule the workflow. + +### Single sign-on (SSO) + +Users can [configure SSO](/workflows/workspaces/sso/) to authenticate with Pipedream using their identity provider. + +### Source + +Synonym for [event source](#event-source). + +### Step + +[Steps](/workflows/#steps) are the building blocks used to create workflows. Steps can be [triggers](#trigger), [actions](#action), or [code steps](#code-step). + +### Step export + +JSON-serializable data returned from steps, available in future steps of a workflow. [See the docs](/workflows/#step-exports) for more details. + +### Step notes + +[Step notes](/workflows/#step-notes) are Markdown notes you can add to a step to document its purpose. + +### Subscription + +A connection between a [listener](#listener) and an [emitter](#emitter) that allows the listener to receive events from the emitter. + +### Suspend + +Workflow [executions](#execution) are suspended when you [delay](#delay) or use functions like [`$.flow.suspend`](/workflows/building-workflows/code/nodejs/rerun/#flowsuspend) to pause the workflow. + +## T + +### Throttling + +Synonym for [execution rate controls](#execution-rate-controls). + +### Timeout + +All workflows have [a default timeout](/workflows/limits/#time-per-execution). You can configure a custom timeout in the [workflow settings](/workflows/building-workflows/settings/#execution-timeout-limit). + +### `/tmp` directory + +A directory available to the workflow's [execution environment](#execution-environment) for storing files. Files stored in `/tmp` are only guaranteed to be available for the duration of the workflow execution, and are not accessible across [workers](#worker). + +### Trigger + +Triggers process data from third-party APIs and [emit](#emit) [events](#event) that run workflows. Triggers can be [HTTP triggers](#http-trigger), [schedule triggers](#schedule-trigger), [email triggers](#email-trigger), [event sources](#event-source), and more. + +### Two-factor authentication (2FA) + +Two-factor authentication. [Configure 2FA](/account/user-settings/#two-factor-authentication) to add an extra layer of security to your Pipedream account. + +## U + +## V + +### VPC (Virtual Private Cloud) + +VPCs are customer-specific private networks where workflows can run. [See the docs](/workflows/vpc/) for more details. + +## W-Z + +### Worker + +An instance of a workflow [execution environment](#execution-environment) available to processes [events](#event). + +### Workspace + +You create a workspace when you sign up for Pipedream. Workspaces contain projects, workflows, and other resources. [See the docs](/workflows/workspaces) for more details. + +### Workspace admin + +A workspace can have multiple [admins](/workflows/workspaces/#promoting-a-member-to-admin), who can administer the workspace, manage billing, and more. + +### Workspace member + +A user invited to a workspace. Members can create projects, workflows, and other resources in the workspace, but cannot manage billing or administer the workspace. + +### Workspace owner + +The user who created the workspace. + +### Workflow serialization + +When you use [GitHub Sync](#github-sync), Pipedream serializes the workflow configuration to a YAML file. Optionally, if your workflow contains custom code, Pipedream serializes the code to a separate file. + +### Workspace settings + +[Workspace settings](#workspace-settings) let [workspace admins](#workspace-admin) configure settings like membership, [SSO](#single-sign-on-sso), and more. + +### Workflow template + +When you [share a workflow](/workflows/building-workflows/sharing/), you create a template that anyone can copy and run. + +### Workspace variable + +An environment variable available across all projects in a workspace. + +### Workflow + +Workflows are the primary resource in Pipedream. They process events from [triggers](#trigger) and run [steps](#step) to perform actions like making HTTP requests, sending emails, and more. diff --git a/docs-v2/pages/hidden/abuse/_meta.tsx b/docs-v2/pages/hidden/abuse/_meta.tsx new file mode 100644 index 0000000000000..1926dd0089996 --- /dev/null +++ b/docs-v2/pages/hidden/abuse/_meta.tsx @@ -0,0 +1,5 @@ +export default { + index: { + display: "hidden", + }, +} as const diff --git a/docs-v2/pages/abuse/index.mdx b/docs-v2/pages/hidden/abuse/index.mdx similarity index 100% rename from docs-v2/pages/abuse/index.mdx rename to docs-v2/pages/hidden/abuse/index.mdx diff --git a/docs-v2/pages/hidden/scheduling-future-tasks/_meta.tsx b/docs-v2/pages/hidden/scheduling-future-tasks/_meta.tsx new file mode 100644 index 0000000000000..4705b04ff8795 --- /dev/null +++ b/docs-v2/pages/hidden/scheduling-future-tasks/_meta.tsx @@ -0,0 +1,5 @@ +export default { + "index": { + "display": "hidden", + }, +} as const diff --git a/docs-v2/pages/hidden/scheduling-future-tasks/index.mdx b/docs-v2/pages/hidden/scheduling-future-tasks/index.mdx new file mode 100644 index 0000000000000..ea3a59262e799 --- /dev/null +++ b/docs-v2/pages/hidden/scheduling-future-tasks/index.mdx @@ -0,0 +1,180 @@ +# Scheduling future tasks + +Pipedream includes an event source which exposes an HTTP API for scheduling one-time tasks. You can schedule tasks at any timestamp, with second-level precision, up to one year in the future. + +[Click here to create this source](https://pipedream.com/sources?action=create&key=pipedream-new-scheduled-tasks), or visit [https://pipedream.com/sources/new](https://pipedream.com/sources/new), select the **Pipedream** app, and the **New Scheduled Tasks** source. + +To [schedule a new task](#scheduling-a-task), just send an HTTP `POST` request to your source's endpoint, at the `/schedule` path, with the following format: + +```javascript +{ + "timestamp": "2020-08-21T04:29:00.951Z", // timestamp: an ISO 8601 timestamp + "message": { "name": "Luke" } // message: any object or string +} +``` + +When the timestamp arrives and the task is invoked, the source will emit the payload passed in your original, scheduled request. This allows you to trigger [a Pipedream workflow](/workflows/building-workflows/) at the scheduled time, passing the `message` and `timestamp` to the workflow as an [incoming event](/workflows/building-workflows/triggers/). + +You can also listen for these events in your own app / infra, by [subscribing to your source's SSE stream](/workflows/data-management/destinations/sse/). Each time a scheduled task is emitted from your Pipedream source, it also emits a message to that SSE stream. Any application (a Docker container, a Rails app, etc.) listening to that SSE stream can react to that message to run whatever code you'd like. + + + +## HTTP API + +This source exposes an HTTP endpoint where you can send `POST` requests to schedule new tasks. Your endpoint URL should appear as the **Endpoint** in your source's details, in the **Events** tab. + +### Scheduling a task + +``` +POST /schedule +``` + +#### Pipedream built-in action + +Use the **Pipedream Task Scheduler - Schedule Task** Helper Functions action to schedule a new task. + +#### Node.js + +Use the following code in a Node.js code step: + +```javascript +import { axios } from "@pipedream/platform" + +export default defineComponent({ + props: { + numSeconds: { + type: "integer", + label: "Num Seconds", + description: "How many seconds in the future would you like to schedule the task?", + }, + secret: { + type: "string", + optional: true, + }, + taskSchedulerURL: { + type: "string", + label: "Task Scheduler URL", + description: "Enter the URL as it appears in the **Endpoint** field of your Task Scheduler source", + }, + message: { + type: "string", + description: "The message / payload to send to your task scheduler. Can be any string or JavaScript object. This message will be emitted by the task scheduler at the specified number of seconds in the future.", + }, + }, + async run({ $ }) { + const timestamp = (new Date(+new Date() + (this.numSeconds * 1000))).toISOString() + + const headers = { + "Content-Type": "application/json", + }; + if (this.secret) { + headers["x-pd-secret"] = this.secret; + } + + return await axios($, { + url: `${this.taskSchedulerURL}/schedule`, + method: "POST", + headers, + data: { + timestamp, + message: this.message, + }, + }); + }, +}) +``` + +#### `cURL` + +To schedule a new task, `POST` a JSON object with an [ISO 8601](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) `timestamp` and a `message` to the **`/schedule` path** of your source's HTTP endpoint: + +```javascript +{ + "timestamp": "2020-08-21T04:29:00.951Z", // timestamp: an ISO 8601 timestamp + "message": { "name": "Luke" } // message can be any object or string +} +``` + +Optionally, if you configured a secret in your source, you'll need to pass that in the `x-pd-secret` header. For example: + +```bash +curl -X POST \ + -H 'Content-Type: application/json' \ + -H 'x-pd-secret: 123' \ + -d '{ "timestamp": "2020-09-18T04:40:59Z", "message": "foo" }' \ + https://endpoint.m.pipedream.net/schedule +``` + +Successful task schedule requests yield a `200 OK` response, noting the task was successfully scheduled. + +### Cancelling a scheduled task + +``` +POST /cancel +``` + +When you schedule a task, you'll receive a unique ID assigned to that task in the `id` field of the HTTP response body. That `id` can be passed to the `/cancel` endpoint to cancel that task before its scheduled time arrives. + +#### Node.js + +Cancel tasks using the following code in a Node.js code step: + +```javascript +import { axios } from "@pipedream/platform" + +export default defineComponent({ + props: { + secret: { + type: "string", + optional: true, + }, + taskSchedulerURL: { + type: "string", + label: "Task Scheduler URL", + description: "Enter the URL as it appears in the **Endpoint** field of your Task Scheduler source", + }, + taskId: { + type: "string", + description: "The ID of the task you'd like to cancel", + }, + }, + async run({ $ }) { + const headers = { + "Content-Type": "application/json", + }; + if (this.secret) { + headers["x-pd-secret"] = this.secret; + } + + return await axios($, { + url: `${this.taskSchedulerURL}/cancel`, + method: "DELETE", + headers, + data: { + id: this.taskId, + }, + }); + }, +}) +``` + +#### `cURL` + +```bash +curl -X POST \ + -H 'Content-Type: application/json' \ + -H 'x-pd-secret: 123' \ + -d '{ "id": "8fceb45b-0241-4d04-9f3f-334679586370" }' \ + https://endpoint.m.pipedream.net/cancel +``` + +## Processing scheduled tasks + +Scheduled tasks are emitted by the event source as events, which you can consume with + +- [Pipedream workflows](/workflows/building-workflows/) +- [A source-specific SSE stream](/workflows/data-management/destinations/sse/) +- [The Pipedream REST API](/rest-api/) +- [The Pipedream CLI](/workflows/cli/reference/#installing-the-cli) + +[See the docs on consuming events from sources](/workflows/building-workflows/sources/#consuming-events-from-sources) for more information. diff --git a/docs-v2/pages/hidden/status/_meta.tsx b/docs-v2/pages/hidden/status/_meta.tsx new file mode 100644 index 0000000000000..4705b04ff8795 --- /dev/null +++ b/docs-v2/pages/hidden/status/_meta.tsx @@ -0,0 +1,5 @@ +export default { + "index": { + "display": "hidden", + }, +} as const diff --git a/docs-v2/pages/hidden/status/index.mdx b/docs-v2/pages/hidden/status/index.mdx new file mode 100644 index 0000000000000..03952467f7d6d --- /dev/null +++ b/docs-v2/pages/hidden/status/index.mdx @@ -0,0 +1,5 @@ +# Service Status + +Pipedream operates a status page at [https://status.pipedream.com](https://status.pipedream.com/). That page displays the uptime history and current status of every Pipedream service. + +When incidents occur, updates are published to the **#incidents** channel of [Pipedream's Slack Community](https://pipedream.com/support) and to the [@PipedreamStatus](https://twitter.com/PipedreamStatus) account on Twitter. On the status page itself, you can also subscribe to updates directly. diff --git a/docs-v2/pages/hidden/subprocessors/_meta.tsx b/docs-v2/pages/hidden/subprocessors/_meta.tsx new file mode 100644 index 0000000000000..4705b04ff8795 --- /dev/null +++ b/docs-v2/pages/hidden/subprocessors/_meta.tsx @@ -0,0 +1,5 @@ +export default { + "index": { + "display": "hidden", + }, +} as const diff --git a/docs-v2/pages/subprocessors/index.mdx b/docs-v2/pages/hidden/subprocessors/index.mdx similarity index 100% rename from docs-v2/pages/subprocessors/index.mdx rename to docs-v2/pages/hidden/subprocessors/index.mdx diff --git a/docs-v2/pages/http.mdx b/docs-v2/pages/http.mdx deleted file mode 100644 index c8d65aa732cf5..0000000000000 --- a/docs-v2/pages/http.mdx +++ /dev/null @@ -1,167 +0,0 @@ -import Callout from '@/components/Callout' - -# HTTP - -Integrate and automate any API using Pipedream workflows. Use app specific pre-built actions, or an HTTP request action for a no code interface. If you need more control over error handling, then use your same connected accounts with code in Node.js or Python. - -## Pre-built actions - -Pre-built actions are the most convenient option for integrating your workflow with an API. Pre-built actions can use your connected accounts to perform API requests, and are configured through props. - -Pre-built actions are the fastest way to get started building workflows, but they may not fit your use case if a prop is missing or is handling data in a way that doesn't fit your needs. - -For example, to send a message using Slack just search for Slack and use the **Send Message to a Public Channel** action: - -![Finding the Slack - Send Message to a Public Channel action](https://res.cloudinary.com/pipedreamin/image/upload/v1684263132/docs/docs/CleanShot_2023-05-16_at_14.36.58_et1kg2.png) - -Then connect your Slack account, select a channel and write your message: - -![Configuring a Slack - Send Message to a Public Channel action](https://res.cloudinary.com/pipedreamin/image/upload/v1684263134/docs/docs/CleanShot_2023-05-16_at_14.37.29_qngd26.png) - -Now with a few clicks and some text you've integrated Slack into a Pipedream workflow. - - -Pre-built actions are open source - -All pre-built actions are published from the [Pipedream Component Registry](/apps/contributing/), so you can read and modify their source code. You can even publish your own from [Node.js code steps privately to your own workspace](/code/nodejs/sharing-code/). - - -## HTTP Request Action - -The HTTP request action is the next most convenient option. Use a Postman-like interface to configure an HTTP request - including the headers, body, and even connecting an account. - -![Finding the HTTP request builder action](https://res.cloudinary.com/pipedreamin/image/upload/v1684420462/docs/docs/http/CleanShot_2023-05-18_at_10.33.50_vigdf7.png) - -Once you connect your account to the step, it will automatically configure the authorization headers to match. - -![Selecting a connected account for a HTTP request step](https://res.cloudinary.com/pipedreamin/image/upload/v1684259987/docs/docs/event%20histories/CleanShot_2023-05-16_at_13.50.53_fv3caw.png) - -Then you can choose **Slack** as the service to connect the HTTP request with: - -![Connecting the HTTP Request step to Slack](https://res.cloudinary.com/pipedreamin/image/upload/v1684420551/docs/docs/http/CleanShot_2023-05-18_at_10.35.41_dxyipl.png) - -The HTTP request action will automatically be configured with the Slack connection, you'll just need to select your account to finish the authentication. - -Then it's simply updating the URL to send a message which is [`https://slack.com/api/chat.postMessage`](https://api.slack.com/methods/chat.postMessage): - -![Defining the Slack API endpoint URL and connecting an account](https://res.cloudinary.com/pipedreamin/image/upload/v1684263130/docs/docs/CleanShot_2023-05-16_at_14.35.05_mame6o.png) - -Finally modify the body of the request to specify the `channel` and `message` for the request: - -![Configuring the Slack body for sending the request](https://res.cloudinary.com/pipedreamin/image/upload/v1684263128/docs/docs/CleanShot_2023-05-16_at_14.34.56_kpk2vp.png) - -HTTP Request actions can be used to quickly scaffold API requests, but are not as flexible as code for a few reasons: - -* Conditionally sending requests - The HTTP request action will always request, to send requests conditionally you'll need to use code. -* Workflow execution halts - if an HTTP request fails, the entire workflow cancels -* Automatically retrying - `$.flow.retry` isn't available in the HTTP Request action to retry automatically if the request fails -* Error handling - It's not possible to set up a secondary action if an HTTP request fails. - -## HTTP Requests in code - -When you need more control, use code. You can use your connected accounts with Node.js or Python code steps. - -This gives you the flexibility to catch errors, use retries, or send multiple API requests in a single step. - -First, connect your account to the code step: - -* [Connecting any account to a Node.js step](/code/nodejs/auth/#accessing-connected-account-data-with-this-appname-auth) -* [Connecting any account to a Python step](/code/python/auth/) - -### Conditionally sending an API Request - -You may only want to send a Slack message on a certain condition, in this example we'll only send a Slack message if the HTTP request triggering the workflow passes a special variable: `steps.trigger.event.body.send_message` - -```javascript -import { axios } from "@pipedream/platform" - -export default defineComponent({ - props: { - slack: { - type: "app", - app: "slack", - } - }, - async run({steps, $}) { - // only send the Slack message if the HTTP request has a `send_message` property in the body - if(steps.trigger.body.send_message) { - return await axios($, { - headers: { - Authorization: `Bearer ${this.slack.$auth.oauth_access_token}`, - }, - - url: `https://slack.com/api/chat.postMessage`, - - method: 'post', - data: { - channel: 'C123456', - text: 'Hi from a Pipedream Node.js code step' - } - }) - } - }, -}) - -``` - -### Error Handling - -The other advantage of using code is handling error messages using `try...catch` blocks. In this example, we'll only send a Slack message if another API request fails: - -```javascript -import { axios } from "@pipedream/platform" - -export default defineComponent({ - props: { - openai: { - type: "app", - app: "openai" - }, - slack: { - type: "app", - app: "slack", - } - }, - async run({steps, $}) { - try { - return await axios($, { - url: `https://api.openai.com/v1/completions`, - method: 'post', - headers: { - Authorization: `Bearer ${this.openai.$auth.api_key}`, - }, - data: { - "model": "text-davinci-003", - "prompt": "Say this is a test", - "max_tokens": 7, - "temperature": 0 - } - }) - } catch(error) { - return await axios($, { - url: `https://slack.com/api/chat.postMessage`, - method: 'post', - headers: { - Authorization: `Bearer ${this.slack.$auth.oauth_access_token}`, - }, - data: { - channel: 'C123456', - text: `OpenAI returned an error: ${error}` - } - }) - } - }, -}) -``` - - -Subscribing to all errors - -[You can use a subscription](/api/rest/#subscriptions) to subscribe a workflow to all errors through the `$errors` channel, instead of handling each error individually. - - -### Automatically retrying an HTTP request - -You can leverage `$.flow.rerun` within a `try...catch` block in order to retry a failed API request. - -[See the example in the `$.flow.rerun` docs](/code/nodejs/rerun/#pause-resume-and-rerun-a-workflow) for Node.js. diff --git a/docs-v2/pages/images/add-new-env-var.png b/docs-v2/pages/images/add-new-env-var.png deleted file mode 100644 index 7cdfea9eae72a..0000000000000 Binary files a/docs-v2/pages/images/add-new-env-var.png and /dev/null differ diff --git a/docs-v2/pages/images/add-remove-env-var.png b/docs-v2/pages/images/add-remove-env-var.png deleted file mode 100644 index 5d6ce43249499..0000000000000 Binary files a/docs-v2/pages/images/add-remove-env-var.png and /dev/null differ diff --git a/docs-v2/pages/images/app-based-trigger.png b/docs-v2/pages/images/app-based-trigger.png deleted file mode 100644 index bfc7afe7fbb2c..0000000000000 Binary files a/docs-v2/pages/images/app-based-trigger.png and /dev/null differ diff --git a/docs-v2/pages/images/edit-environment.png b/docs-v2/pages/images/edit-environment.png deleted file mode 100644 index 860998686487b..0000000000000 Binary files a/docs-v2/pages/images/edit-environment.png and /dev/null differ diff --git a/docs-v2/pages/images/env-var-error.png b/docs-v2/pages/images/env-var-error.png deleted file mode 100644 index 401992d6b1ff3..0000000000000 Binary files a/docs-v2/pages/images/env-var-error.png and /dev/null differ diff --git a/docs-v2/pages/images/env-vars-object-explorer.png b/docs-v2/pages/images/env-vars-object-explorer.png deleted file mode 100644 index 2e7fef119a0cf..0000000000000 Binary files a/docs-v2/pages/images/env-vars-object-explorer.png and /dev/null differ diff --git a/docs-v2/pages/images/env-vars.gif b/docs-v2/pages/images/env-vars.gif deleted file mode 100644 index 24b4efd38596b..0000000000000 Binary files a/docs-v2/pages/images/env-vars.gif and /dev/null differ diff --git a/docs-v2/pages/images/env-vars/add-new-var-v2.png b/docs-v2/pages/images/env-vars/add-new-var-v2.png deleted file mode 100644 index 83fd57d4f7b91..0000000000000 Binary files a/docs-v2/pages/images/env-vars/add-new-var-v2.png and /dev/null differ diff --git a/docs-v2/pages/images/env-vars/add-var-modal-v2.png b/docs-v2/pages/images/env-vars/add-var-modal-v2.png deleted file mode 100644 index bded43f05c6c2..0000000000000 Binary files a/docs-v2/pages/images/env-vars/add-var-modal-v2.png and /dev/null differ diff --git a/docs-v2/pages/images/env-vars/autocomplete-env-vars.png b/docs-v2/pages/images/env-vars/autocomplete-env-vars.png deleted file mode 100644 index bc18a8e294495..0000000000000 Binary files a/docs-v2/pages/images/env-vars/autocomplete-env-vars.png and /dev/null differ diff --git a/docs-v2/pages/images/env-vars/edit-env-var.png b/docs-v2/pages/images/env-vars/edit-env-var.png deleted file mode 100644 index a8d02f05c0cc9..0000000000000 Binary files a/docs-v2/pages/images/env-vars/edit-env-var.png and /dev/null differ diff --git a/docs-v2/pages/images/env-vars/env-vars-object-explorer-v2.png b/docs-v2/pages/images/env-vars/env-vars-object-explorer-v2.png deleted file mode 100644 index 10a8a01aef9fa..0000000000000 Binary files a/docs-v2/pages/images/env-vars/env-vars-object-explorer-v2.png and /dev/null differ diff --git a/docs-v2/pages/images/env-vars/project-vars.png b/docs-v2/pages/images/env-vars/project-vars.png deleted file mode 100644 index c1418f775b93c..0000000000000 Binary files a/docs-v2/pages/images/env-vars/project-vars.png and /dev/null differ diff --git a/docs-v2/pages/images/getting-started.png b/docs-v2/pages/images/getting-started.png deleted file mode 100644 index 46a82599a33ee..0000000000000 Binary files a/docs-v2/pages/images/getting-started.png and /dev/null differ diff --git a/docs-v2/pages/images/getting-started2.png b/docs-v2/pages/images/getting-started2.png deleted file mode 100644 index 4e05fba075b0d..0000000000000 Binary files a/docs-v2/pages/images/getting-started2.png and /dev/null differ diff --git a/docs-v2/pages/images/getting-started3.png b/docs-v2/pages/images/getting-started3.png deleted file mode 100644 index 37c906db2b92b..0000000000000 Binary files a/docs-v2/pages/images/getting-started3.png and /dev/null differ diff --git a/docs-v2/pages/images/getting-started5.png b/docs-v2/pages/images/getting-started5.png deleted file mode 100644 index 540dac6bf7d4d..0000000000000 Binary files a/docs-v2/pages/images/getting-started5.png and /dev/null differ diff --git a/docs-v2/pages/images/params-hamburger-menu.png b/docs-v2/pages/images/params-hamburger-menu.png deleted file mode 100644 index 6f070d61b64ed..0000000000000 Binary files a/docs-v2/pages/images/params-hamburger-menu.png and /dev/null differ diff --git a/docs-v2/pages/images/timeout-err-cell.png b/docs-v2/pages/images/timeout-err-cell.png deleted file mode 100644 index 0405767dbf563..0000000000000 Binary files a/docs-v2/pages/images/timeout-err-cell.png and /dev/null differ diff --git a/docs-v2/pages/images/timeout-err-inspector.png b/docs-v2/pages/images/timeout-err-inspector.png deleted file mode 100644 index fae08f64a416f..0000000000000 Binary files a/docs-v2/pages/images/timeout-err-inspector.png and /dev/null differ diff --git a/docs-v2/pages/index.mdx b/docs-v2/pages/index.mdx index 726da80c3c87f..3c3f6e71812ac 100644 --- a/docs-v2/pages/index.mdx +++ b/docs-v2/pages/index.mdx @@ -2,18 +2,17 @@ import VideoPlayer from "@/components/VideoPlayer"; # Introduction to Pipedream -Pipedream is the fastest way to automate any process that connects APIs. Build and run workflows with code-level control when you need it, and no code when you don't. +Pipedream is the fastest way to automate any process that connects APIs. Build and run workflows with code-level control when you need it, and no code when you don't. Easily add thousands of customer-facing integrations to your app or AI agent in minutes. The Pipedream platform includes: -- A [serverless runtime](/code/) and [workflow service](/workflows/) -- Source-available [triggers](/workflows/steps/triggers/) and [actions](/workflows/steps/actions/) for [hundreds of integrated apps](https://pipedream.com/explore/) -- One-click [OAuth and key-based authentication](/connected-accounts/) for more than {process.env.PUBLIC_APPS} APIs (use tokens directly in code or with pre-built actions) - -Watch a demo or review our [quickstart guide](/quickstart/): +- A [serverless runtime](/workflows/building-workflows/code/) and [workflow service](/workflows/building-workflows/) +- SDK to handle [customer authentication](/connect/) for {process.env.PUBLIC_APPS}+ APIs +- Source-available pre-built [triggers](/workflows/building-workflows/triggers/) and [actions](/workflows/building-workflows/actions/) for [thousands of integrated apps](https://pipedream.com/explore/) +- One-click [OAuth and key-based authentication](/integrations/connected-accounts/) for more than {process.env.PUBLIC_APPS} APIs (use tokens directly in code or with pre-built actions) @@ -25,13 +24,13 @@ To get started, [sign up for a free account](https://pipedream.com/auth/signup) Once you understand the basics of workflow development, learn how to get more out of Pipedream: -- [Use code in workflows](/code/) -- [Develop custom actions](/components/quickstart/nodejs/actions/) -- [Develop custom triggers](/components/quickstart/nodejs/sources/) +- [Use code in workflows](/workflows/building-workflows/code/) +- [Develop custom actions](/workflows/contributing/components/quickstart/nodejs/actions/) +- [Develop custom triggers](/workflows/contributing/components/quickstart/nodejs/sources/) ## Use Cases -Pipedream supports use cases from prototype to production and is trusted by 200k+ developers from startups to Fortune 500 companies: +Pipedream supports use cases from prototype to production and is trusted by 800k+ developers from startups to Fortune 500 companies: ![logos](https://res.cloudinary.com/pipedreamin/image/upload/v1612919944/homepage/logos_kcbviz.png) @@ -41,7 +40,7 @@ Our [community](https://pipedream.com/community) uses Pipedream for a wide varie - Connecting SaaS apps - General API orchestration and automation -- Database automations ([reach out](https://pipedream.com/community) to learn about connecting to resources behind a firewall) +- Database automations ([learn about connecting to resources behind a firewall](/workflows/data-management/databases/)) - Custom notifications and alerting - Mobile and JAMstack backends - Rate limiting, request smoothing @@ -51,13 +50,13 @@ Our [community](https://pipedream.com/community) uses Pipedream for a wide varie ## Source-available -Pipedream maintains a [source-available component registry](https://github.com/pipedreamhq/pipedream/) on GitHub so you can avoid writing boilerplate code for common API integrations. Use components as no code building blocks in workflows, or use them to scaffold code that you can customize. You can also [create a PR to contribute new components](/apps/contributing/#contribution-process) via GitHub. +Pipedream maintains a [source-available component registry](https://github.com/PipedreamHQ/pipedream) on GitHub so you can avoid writing boilerplate code for common API integrations. Use components as no code building blocks in workflows, or use them to scaffold code that you can customize. You can also [create a PR to contribute new components](/workflows/contributing/#contribution-process) via GitHub. ## Contributing We hope is that by providing a generous free tier, you will not only get value from Pipedream, but you will give back to help us improve the product for the entire community and grow the platform by: -- [Contributing components](/apps/contributing/) to the [Pipedream registry](https://github.com/pipedreamhq/pipedream) or sharing via your own GitHub repo +- [Contributing components](/workflows/contributing/) to the [Pipedream registry](https://github.com/PipedreamHQ/pipedream) or sharing via your own GitHub repo - Asking and answering questions in our [public community](https://pipedream.com/community/) - [Reporting bugs](https://pipedream.com/community/c/bugs/9) and [requesting features](https://github.com/PipedreamHQ/pipedream/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=%5BFEATURE%5D+) that help us build a better product - Following us on [Twitter](https://twitter.com/pipedream), starring our [GitHub repo](https://github.com/PipedreamHQ/pipedream) and subscribing to our [YouTube channel](https://www.youtube.com/c/pipedreamhq) @@ -67,10 +66,10 @@ Learn about [all the ways you can contribute](https://pipedream.com/contributing ## Support & Community -If you have any questions or feedback, please [reach out in our community forum](https://pipedream.com/community). +If you have any questions or feedback, please [reach out in our community forum](https://pipedream.com/community) or [to our support team](https://pipedream.com/support). ## Service Status -Pipedream operates a status page at [https://status.pipedream.com](https://status.pipedream.com). That page displays the uptime history and current status of every Pipedream service. +Pipedream operates a status page at [https://status.pipedream.com](https://status.pipedream.com/). That page displays the uptime history and current status of every Pipedream service. When incidents occur, updates are published to the **#incidents** channel of [Pipedream's Slack Community](https://pipedream.com/support) and to the [@PipedreamStatus](https://twitter.com/PipedreamStatus) account on Twitter. On the status page itself, you can also subscribe to updates directly. diff --git a/docs-v2/pages/integrations/_meta.tsx b/docs-v2/pages/integrations/_meta.tsx new file mode 100644 index 0000000000000..de53e70fccddc --- /dev/null +++ b/docs-v2/pages/integrations/_meta.tsx @@ -0,0 +1,7 @@ +export default { + "apps": "Integrated Apps", + "connected-accounts": "Connected Accounts", + "oauth-clients": "OAuth Clients", + "external-auth": "External Auth", + "app-partners": "App Partners", +} as const diff --git a/docs-v2/pages/integrations/app-partners.mdx b/docs-v2/pages/integrations/app-partners.mdx new file mode 100644 index 0000000000000..11382e1a08236 --- /dev/null +++ b/docs-v2/pages/integrations/app-partners.mdx @@ -0,0 +1,23 @@ +# App Partners + +By integrating your app with Pipedream, your users will be able to connect your app with over {process.env.PUBLIC_APPS} supported apps on our platform. You gain access and exposure to a community of over 800,000 developers, and can spend more time building your product and less time navigating app integrations. + +## Benefits of Integrating With Pipedream + +- **End-to-End Development:** Pipedream will handle the entire development process of the integration, from managing authentication, setting up an official Pipedream OAuth client where applicable, and final QA. +- **Custom Triggers and Actions:** We will create up to three triggers and three actions, based on the methods available within your APIs. +- **Extensive Developer Exposure:** Your app will be accessible to a large and growing community of developers. +- **Dedicated App Page:** Control and customize your dedicated app page to market your app to potential users, and share example use cases or workflow templates that can be built to help users get started. + +## Integration Process + +Integrating with Pipedream is a straightforward process: + +1. Pipedream requires an account for testing and development along with API documentation in order to get started. We will build the initial integration, and run test requests to test the connection. +2. Our team will build no-code triggers and actions that make the most sense from a workflow development perspective - if you have specific triggers and actions in mind to start with, we’ll start there. +3. Our QA team will thoroughly test the no-code components to ensure that they work as intended, and then we will release and announce the completed integration in our public Slack with over 5,000 active members. + +## Get Started + +Are you ready to integrate with Pipedream? [Contact our integrations team](https://pipedream.com/support) today to get started. + diff --git a/docs-v2/pages/integrations/apps.mdx b/docs-v2/pages/integrations/apps.mdx new file mode 100644 index 0000000000000..e23e9c228534a --- /dev/null +++ b/docs-v2/pages/integrations/apps.mdx @@ -0,0 +1,79 @@ +import Callout from '@/components/Callout' + +# Integrated Apps + +Pipedream has built-in integrations with more than {process.env.PUBLIC_APPS} apps. Since you can [write any code](/workflows/building-workflows/code/nodejs/) on Pipedream, and pass API keys or credentials using [environment variables](/workflows/environment-variables/), you can connect to virtually any service, so the list is not exhaustive. + +But Pipedream-integrated apps provide a few benefits: + +- You can [connect the app once](/integrations/connected-accounts/) and [link that connected account to any step of a workflow](/integrations/connected-accounts/#connecting-accounts) +- Pipedream provides [pre-built actions](/workflows/contributing/components/#actions) that wrap common operations for the app. You shouldn't have to write the code to send a message to Slack, or add a new row to a Google Sheet, so actions make that easy. Actions are just code, so you can fork and modify them, or even [publish your own to the Pipedream community](/workflows/contributing/). +- [You have access to your API keys and access tokens in code steps](/workflows/building-workflows/code/nodejs/auth/), so you can write any code to authorize custom requests to these apps. + +## Premium Apps + +The vast majority of integrated apps on Pipedream are free to use in your workflows across any plan. However, in order to use any of the below apps in an active workflow, your workspace will need to have access to [Premium Apps](https://pipedream.com/pricing): + +- [ActiveCampaign](https://pipedream.com/apps/activecampaign) +- [ADP](https://pipedream.com/apps/adp) +- [Amazon Advertising](https://pipedream.com/apps/amazon_advertising) +- [Asana](https://pipedream.com/apps/asana) +- [AWS](https://pipedream.com/apps/aws) +- [Azure OpenAI Service](https://pipedream.com/apps/azure-openai-service) +- [BigCommerce](https://pipedream.com/apps/bigcommerce) +- [Cisco Webex](https://pipedream.com/apps/cisco-webex) +- [Cisco Webex (Custom App)](https://pipedream.com/apps/cisco-webex-custom-app) +- [Close](https://pipedream.com/apps/close) +- [Cloudinary](https://pipedream.com/apps/cloudinary) +- [Customer.io](https://pipedream.com/apps/customer-io) +- [Datadog](https://pipedream.com/apps/datadog) +- [dbt Cloud](https://pipedream.com/apps/dbt) +- [ERPNext](https://pipedream.com/apps/erpnext) +- [Exact](https://pipedream.com/apps/exact) +- [Freshdesk](https://pipedream.com/apps/freshdesk) +- [Google Cloud](https://pipedream.com/apps/google-cloud) +- [Gorgias](https://pipedream.com/apps/gorgias-oauth) +- [HubSpot](https://pipedream.com/apps/hubspot) +- [Intercom](https://pipedream.com/apps/intercom) +- [Jira](https://pipedream.com/apps/jira) +- [Jira Service Desk](https://pipedream.com/apps/jira-service-desk) +- [Klaviyo](https://pipedream.com/apps/klaviyo) +- [Linkedin](https://pipedream.com/apps/linkedin) +- [Linkedin Ads](https://pipedream.com/apps/linkedin-ads) +- [Mailchimp](https://pipedream.com/apps/mailchimp) +- [Mailgun](https://pipedream.com/apps/mailgun) +- [MongoDB](https://pipedream.com/apps/mongodb) +- [Outreach](https://pipedream.com/apps/outreach) +- [PagerDuty](https://pipedream.com/apps/pagerduty) +- [Pinterest](https://pipedream.com/apps/pinterest) +- [Pipedrive](https://pipedream.com/apps/pipedrive) +- [Pipefy](https://pipedream.com/apps/pipefy) +- [Propeller](https://pipedream.com/apps/propeller) +- [Quickbooks](https://pipedream.com/apps/quickbooks) +- [Rebrandly](https://pipedream.com/apps/rebrandly) +- [ReCharge](https://pipedream.com/apps/recharge) +- [Salesforce (REST API)](https://pipedream.com/apps/salesforce_rest_api) +- [Segment](https://pipedream.com/apps/segment) +- [SendinBlue](https://pipedream.com/apps/sendinblue) +- [ServiceNow](https://pipedream.com/apps/servicenow) +- [ShipStation](https://pipedream.com/apps/shipstation) +- [Shopify](https://pipedream.com/apps/shopify) +- [Snowflake](https://pipedream.com/apps/snowflake) +- [Stripe](https://pipedream.com/apps/stripe) +- [Twilio SendGrid](https://pipedream.com/apps/sendgrid) +- [WhatsApp Business](https://pipedream.com/apps/whatsapp-business) +- [WooCommerce](https://pipedream.com/apps/woocommerce) +- [Xero Accounting](https://pipedream.com/apps/xero_accounting_api) +- [Zendesk](https://pipedream.com/apps/zendesk) +- [Zoom Admin](https://pipedream.com/apps/zoom_admin) +- [Zoho Books](https://pipedream.com/apps/zoho_books) +- [Zoho CRM](https://pipedream.com/apps/zoho_crm) +- [Zoho People](https://pipedream.com/apps/zoho_people) +- [Zoho SalesIQ](https://pipedream.com/apps/zoho_salesiq) + + +Missing an integration? +If we don't have an integration for an app that you'd like to see, please [let us know](https://pipedream.com/support) or [contribute it to the source available Pipedream registry](/workflows/contributing/). + + +**Check out the full list of integrated apps [here](https://pipedream.com/apps).** diff --git a/docs-v2/pages/integrations/connected-accounts.mdx b/docs-v2/pages/integrations/connected-accounts.mdx new file mode 100644 index 0000000000000..f998acd3146ad --- /dev/null +++ b/docs-v2/pages/integrations/connected-accounts.mdx @@ -0,0 +1,261 @@ +import Callout from '@/components/Callout' +import VideoPlayer from '@/components/VideoPlayer' + +# Connected Accounts + + + +Pipedream provides native integrations for [{process.env.PUBLIC_APPS}+ APIs](https://pipedream.com/apps). Once you connect an account, you can + +- [Link that account to any step of a workflow](#connecting-accounts), using the associated credentials to make API requests to any service. +- [Manage permissions](#managing-connected-accounts), limiting access to sensitive accounts + +Pipedream handles OAuth for you, ensuring you always have a fresh access token to authorize requests, and [credentials are tightly-secured](/privacy-and-security/#third-party-oauth-grants-api-keys-and-environment-variables). + +If you use an existing secrets store, or manage credentials in a database, you can also [pass those to Pipedream at runtime](/integrations/connected-accounts/external-auth/) instead of connecting accounts in the UI. + +## Supported Apps + +Pipedream supports [{process.env.PUBLIC_APPS}+ apps](https://pipedream.com/apps), and we're adding more every day. + +If you don't see an integration for a service you need, you can [request the integration here](#requesting-a-new-app-or-service), or [use environment variables](/workflows/environment-variables/) to manage custom credentials. + +## Types of Integrations + +### OAuth + +For services that support OAuth, Pipedream operates an OAuth application that mediates access to the service so you don't have to maintain your own app, store refresh and access tokens, and more. + +When you connect an account, you'll see a new window open where you authorize the Pipedream application to access data in your account. Pipedream stores the OAuth refresh token tied to your authorization grant, automatically generating access tokens you can use to authorized requests to the service's API. You can [access these tokens in code steps](/workflows/building-workflows/code/nodejs/auth/). + +### Key-based + +We also support services that use API keys or other long-lived tokens to authorize requests. + +For those services, you'll have to create your keys in the service itself, then add them to your connected accounts in Pipedream. + +For example, if you add a new connected account for **Sendgrid**, you'll be asked to add your Sendgrid API key. + +## Connecting accounts + + +This section discusses connecting **your own account** within the Pipedream UI. If you're looking to use the connected accounts for your customers, check out the [Connect docs](/connect/). + + +### From an action + +Prebuilt actions that connect to a specific service require you connect your account for that service before you run your workflow. Click the **Connect [APP]** button to get started. + +Depending on the integration, this will either: + +- Open the OAuth flow for the target service, prompting you to authorize Pipedream to access your account, or +- Open a modal asking for your API credentials for key-based services + +If you've already connected an account for this app, you'll also see a list of existing accounts to select from. + +### From the HTTP Request action + +Craft a custom HTTP request in a workflow with a connected account _without code_. + +In a new step, select the **Send any HTTP Request** to start a new HTTP Request action. + +![Starting a new HTTP request action in a workflow](https://res.cloudinary.com/pipedreamin/image/upload/v1672947285/docs/CleanShot_2023-01-05_at_14.34.25_wi8rcc.png) + +Then, within the new HTTP request, open the **Authorization Type** dropdown to select a **Select an app**: + +![Opening the HTTP Request Authorization Type dropdown](https://res.cloudinary.com/pipedreamin/image/upload/v1673535917/docs/CleanShot_2023-01-12_at_10.05.02_vmttbf.png) + +This will open a new prompt to select an app to connect with. Once you select an app, the HTTP request will be updated with the correct headers to authenticate with that app's API. + +![Select an account](https://res.cloudinary.com/pipedreamin/image/upload/v1673536044/docs/CleanShot_2023-01-12_at_10.07.06_rejzyy.gif) + +Once you connect the selected app account Pipedream will autmatically include your account's authentication keys in the request in the headers, as well as update the URL to match the selected service. + +Now you can modify the request path, method, body or query params to perform an action on the endpoint with your authenticated account. + +### From a code step + +You can connect accounts to code steps by using an `app` prop. Refer to the [connecting apps in Node.js documentation](/workflows/building-workflows/code/nodejs/auth/). + +For example, you can connect to Slack from Pipedream (via their OAuth integration), and use the access token Pipedream generates to authorize requests: + +```javascript +import { WebClient } from '@slack/web-api'; + +// Sends a message to a Slack Channel +export default defineComponent({ + props: { + slack: { + type: 'app', + app: 'slack' + } + }, + async run({ steps, $ }) { + const web = new WebClient(this.slack.$auth.oauth_access_token) + return await web.chat.postMessage({ + text: "Hello, world!", + channel: "#general", + }) + } +}); +``` + +## Managing Connected Accounts + +Visit your [Accounts Page](https://pipedream.com/accounts) to see a list of all your connected accounts. + +On this page you can: + +- Connect your account for any integrated app +- [View and manage access](#access-control) for your connected accounts +- Delete a connected account +- Reconnect an account +- Change the nickname associated with an account + +You'll also see some data associated with these accounts: + +- For many OAuth apps, we'll list the scopes for which you've granted Pipedream access +- The workflows that are using the account + +### Connecting a new account + +1. Visit [https://pipedream.com/accounts](https://pipedream.com/accounts) +2. Click the **Connect an app** button at the top-right. +3. Select the app you'd like to connect. + +### Reconnecting an account + +If you encounter errors in a step that appear to be related to credentials or authorization, you can reconnect your account: + +1. Visit [https://pipedream.com/accounts](https://pipedream.com/accounts) +2. Search for your account +3. Click on the _..._ next to your account, on the right side of the page +4. Select the option to **Reconnect** your account + +## Access Control + +**New connected accounts are private by default** and can only be used by the person who added it. + + +Connected accounts created prior to August 2023 were accessible to all workspace members by default. You can [restrict access](#managing-access) at any time. + + +### Managing access + +- Find the account on the Accounts page and click the 3 dots on the far right of the row +- Select **Manage Access** + +![Selecting Manage Access](https://res.cloudinary.com/pipedreamin/image/upload/v1710869643/connected-account-manage-access_wsn98i.png) + +- You may be prompted to reconnect your account first to verify ownership of the account +- You can enable access to the entire workspace or individual members + +![Managing Access for a Connected Account](https://res.cloudinary.com/pipedreamin/image/upload/v1691614603/manage-access-modal_crmx3f.gif) + +### Collaborating with others + +Even if a workspace member doesn't have access to a private connected account, you can still collaborate together on the same workflows. + +Workspace members who don't have access to a connected account **can perform the following actions** on workflows: + +- Reference step exports +- Inspect prop inputs, step logs, and errors +- Test any step, so they can effectively develop and debug workflows end to end + +Workspace members who do **not** have access to a given connected account **cannot modify prop inputs or edit any code** with that account. + +![Read only action](https://res.cloudinary.com/pipedreamin/image/upload/v1691622307/read-only-action_uvdh1p.png) + +![Read only code step](https://res.cloudinary.com/pipedreamin/image/upload/v1691621275/read-only-code-step_ijqvjc.png) + +To make changes to steps that are locked in read-only mode, you can: + +- Ask the account owner to [grant access](#managing-access) +- Click **More Actions** and change the connected account to one that you have access to (note that this may remove some prop configurations) + +### Access + +Access to connected accounts is enforced at the step-level within workflows and is designed with security and control in mind. + +When you connect an account in Pipedream, you are the owner of that connected account, and you always have full access. You can: + +- Manage access +- Delete +- Reconnect +- Add to any step or trigger + +For connected accounts that are **not** shared with other workspace members: + +| Operation | Workspace Owner & Admin | Other Members | +| -------------------------------------------------- | :---------------------: | :----------------: | +| View on [Accounts](https://pipedream.com/accounts) | ✅ | ❌ | +| Add to a new trigger or step | ❌ | ❌ | +| Modify existing steps | ❌ | ❌ | +| Test exising steps | ✅ | ✅ | +| Manage access | ✅ | ❌ | +| Reconnect | ✅ | ❌ | +| Delete | ✅ | ❌ | + +For connected accounts that **are** shared with other workspace members: + +| Operations | Workspace Owner & Admin | Other Members | +| -------------------------------------------------- | :---------------------: | :----------------: | +| View on [Accounts](https://pipedream.com/accounts) | ✅ | ✅ | +| Add to a new trigger or step | ✅ | ✅ | +| Modify existing steps | ✅ | ✅ | +| Test exising steps | ✅ | ✅ | +| Manage access | ✅ | ❌ | +| Reconnect | ✅ | ❌ | +| Delete | ✅ | ❌ | + +### FAQ + +#### Why isn't my connected account showing up in the legacy workflow builder? + +In order to use a connected account in the legacy (v1) workflow builder, the account must be shared with the entire workspace. Private accounts are accessible in the latest version of the workflow builder. + +#### What is the "Owner" column? + +The owner column on the Accounts page indicates who in the workspace originally connected the account (that is the only person who has permissions to manage access). + +#### Why is there no "Owner" for certain connected accounts? + +Accounts that were connected before August 2023 don't have an owner associated with them, and are shared with the entire workspace. In order to manage access for any of those accounts, we'll first prompt you to reconnect. + +#### How can I restrict access to a connected account shared with the workspace? + +See above for info on [managing access](#managing-access). + +#### Can I still work with other people on a single workflow, even if I don't want them to have access to my connected account? + +Yes, see the section on [collaborating with others](#collaborating-with-others). + +## Accessing credentials via API + +You can access credentials for any connected account via API, letting you build services anywhere and use Pipedream to handle auth. See [the guide for accessing credentials via API](/connect/api/#accounts) for more details. + +## Passing external credentials at runtime + +If you use a secrets store like [Pipedream Connect](/connect/) or [HashiCorp Vault](https://www.vaultproject.io/), or if you store credentials in a database, you can retrieve these secrets at runtime and pass them to any step. [See the full guide here](/integrations/connected-accounts/external-auth/). + +## Connecting to apps with IP restrictions + + +These IP addresses are tied to **app connections only**, not workflows or other Pipedream services. To whitelist requests from Pipedream workflows, [use VPCs](/workflows/vpc/). + + +If you're connecting to an app that enforces IP restrictions, you may need to whitelist the Pipedream API's IP addresses: + +
+  {process.env.PD_EGRESS_IP_RANGE}
+
+ +## Account security + +[See our security docs](/privacy-and-security/#third-party-oauth-grants-api-keys-and-environment-variables) for details on how Pipedream secures your connected accounts. + +## Requesting a new app or service + +1. Visit [https://pipedream.com/support](https://pipedream.com/support) +2. Scroll to the bottom, where you'll see a Support form. +3. Select **App / Integration questions** and submit the request. diff --git a/docs-v2/pages/integrations/external-auth.mdx b/docs-v2/pages/integrations/external-auth.mdx new file mode 100644 index 0000000000000..f8f52fae944c5 --- /dev/null +++ b/docs-v2/pages/integrations/external-auth.mdx @@ -0,0 +1,61 @@ +import Callout from '@/components/Callout' + +# Passing external credentials at runtime + +If you use a secrets store like [HashiCorp Vault](https://www.vaultproject.io/) or [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/), store credentials in a database, or use a service like [Nango](https://www.nango.dev/) to manage auth, you can retrieve these secrets at runtime and pass them to any step. + +There are two ways to pass external auth at runtime: + +1. [Pass it in an HTTP request](#pass-credentials-via-http) +2. [Fetch credentials from a DB or secrets store](#fetch-credentials-from-a-db-or-secrets-store) within a workflow step + + +Passing external credentials at runtime is a feature that's available to customers on the [Business plan](https://pipedream.com/pricing?plan=Enterprise). If you have a use case that you'd like to discuss, [we'd love to hear about it](https://pipedream.com/support)! + + +## Pass credentials via HTTP + +1. If not already configured, [add an HTTP trigger](/workflows/building-workflows/triggers/#http) to your workflow. +2. From your app, retrieve credentials and send them in an HTTP request to the endpoint with the rest of the payload. +3. In the step of your workflow where you'd like to pass these credentials, select the **Use external authentication** option at the bottom-right of the account selector: + +![Select "External Auth"](/images/integrations/select-external-auth.png) + +4. You'll be prompted for all required credentials for the app, often just an `oauth_access_token` or `api_key`. [Find the variable that contains your credentials](/workflows/building-workflows/triggers/#copying-references-to-event-data) and pass them to each field: + +![External auth](https://res.cloudinary.com/pipedreamin/image/upload/v1707630112/docs/Screenshot_2024-02-10_at_9.40.54_PM_hynkvq.png) + +Most steps require additional, user-specific configuration. For example, the Slack **Send a Message** action requires a **Channel ID**, which may be specific to the end user's workspace. You'll need to fetch these values from another step and reference them here. + +![Configure additional params](https://res.cloudinary.com/pipedreamin/image/upload/v1707630112/docs/Screenshot_2024-02-10_at_9.40.54_PM_hynkvq.png) + + +Default logging + +When you return credentials from workflow steps, Pipedream stores it with the rest of the workflow execution data. Workflow events are retained according to the default retention policy for your plan and any [data retention controls](/workflows/building-workflows/settings/#data-retention-controls) you've configured. + +You can set [the `pd-nostore` flag](/workflows/building-workflows/triggers/#x-pd-nostore) to `1` on requests with credentials to disable logging for those requests only. + + +## Fetch credentials from a DB or secrets store + +1. Add a step to your workflow to fetch credentials from your DB or secrets store. +2. In the step of your workflow where you'd like to pass these credentials, select the **Use external authentication** option at the bottom-right of the account selector: + +![Select "External Auth"](/images/integrations/select-external-auth.png) + +3. You'll be prompted for all required credentials for the app, often just an `oauth_access_token` or `api_key`. [Find the variable that contains your credentials](/workflows/building-workflows/triggers/#copying-references-to-event-data) and pass them to each field: + +![External auth](https://res.cloudinary.com/pipedreamin/image/upload/v1707630112/docs/Screenshot_2024-02-10_at_9.40.54_PM_hynkvq.png) + +Most steps require additional, user-specific configuration. For example, the Slack **Send a Message** action requires a **Channel ID**, which may be specific to the end user's workspace. You'll need to fetch these values from another step and reference them here. + +![Configure additional params](https://res.cloudinary.com/pipedreamin/image/upload/v1707630112/docs/Screenshot_2024-02-10_at_9.40.54_PM_hynkvq.png) + + +Default logging + +When you return credentials from workflow steps, Pipedream stores it with the rest of the workflow execution data. Workflow events are retained according to the default retention policy for your plan and any [data retention controls](/workflows/building-workflows/settings/#data-retention-controls) you've configured. + +You can set [the `pd-nostore` flag](/workflows/building-workflows/triggers/#x-pd-nostore) to `1` on requests with credentials to disable logging for those requests only. + diff --git a/docs-v2/pages/integrations/oauth-clients.mdx b/docs-v2/pages/integrations/oauth-clients.mdx new file mode 100644 index 0000000000000..e0846b39db168 --- /dev/null +++ b/docs-v2/pages/integrations/oauth-clients.mdx @@ -0,0 +1,57 @@ +import Callout from '@/components/Callout' +import ArcadeEmbed from '@/components/ArcadeEmbed' +import { Steps } from 'nextra/components' + +# OAuth Clients +By default, OAuth apps in Pipedream use our official OAuth client. When you connect an account for these apps, you grant Pipedream the requested permissions (scopes) on OAuth authorization. + +Pipedream apps solve for a broad range of use cases, which means the scopes our OAuth client requests may include a different set than your specific use case. To define the exact scope of access you'd like to grant, you can configure a custom OAuth client. + +## Configuring custom OAuth clients + + + + + +### Create an OAuth client in the relevant app +For example, if you want to use a custom OAuth client for GitHub, you'll need to locate [their documentation](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) and create an OAuth app in your developer settings. + +### Navigate to the OAuth Clients page in Pipedream +Open the [OAuth Clients page in your Pipedream account](https://pipedream.com/@/accounts/oauth-clients) and click **New OAuth Client**. + +### Select the app +Choose the app you need. If you can't find what you're looking for, feel free to [submit an integration request](https://pipedream.com/support). + +### Enter the required fields +- **Name:** Give the OAuth client a name so it's easy to identify +- **Description:** Optionally add a brief description for additional context +- **Client ID and Secret**: Paste these values from the app's settings that you're configuring (the client secret is sensitive – we'll encrypt and hide it from the UI) +- **Redirect URI:** Copy this Redirect URI and paste it into the app's settings +- **Scopes:** We'll list the scopes from Pipedream's official OAuth client by default. Add or remove scopes as needed based on your use case. + +And finally, click **Save**. + + +Make sure to include all the scopes you need based on your use case. You can modify the scopes later (you'll need to reconnect your account for changes to take effect). Refer to the app's API documentation for information on what scopes you'll need. + + + + +## Connecting your account with with a custom OAuth client +Once you've created the OAuth client, anyone in your workspace can connect their account: + +
+ + + + +Now you're ready to use the connected account in any workflow, just like any other account in Pipedream: + +![Connected accounts with OAuth client labels](https://res.cloudinary.com/pipedreamin/image/upload/v1717104725/oauth-clients-dropdown-labels_zzmycx.png) + +### Limitations +- The vast majority of OAuth apps in Pipedream support custom OAuth clients. However, due to the unique integration requirements for certain apps, custom OAuth clients are not supported in **triggers** for these apps (custom OAuth clients work in actions and code steps): [Discord](https://pipedream.com/apps/discord/), [Dropbox](https://pipedream.com/apps/dropbox/), [Slack](https://pipedream.com/apps/slack/), and [Zoom](https://pipedream.com/apps/zoom/). \ No newline at end of file diff --git a/docs-v2/pages/migrate-from-v1/_meta.json b/docs-v2/pages/migrate-from-v1/_meta.json deleted file mode 100644 index 5e6a93c3a56e9..0000000000000 --- a/docs-v2/pages/migrate-from-v1/_meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "index": { - "display": "hidden" - } -} diff --git a/docs-v2/pages/migrate-from-v1/images/demo-poster.png b/docs-v2/pages/migrate-from-v1/images/demo-poster.png deleted file mode 100644 index 75190d0b4dec5..0000000000000 Binary files a/docs-v2/pages/migrate-from-v1/images/demo-poster.png and /dev/null differ diff --git a/docs-v2/pages/migrate-from-v1/index.mdx b/docs-v2/pages/migrate-from-v1/index.mdx deleted file mode 100644 index 9f9d0b257dc9c..0000000000000 --- a/docs-v2/pages/migrate-from-v1/index.mdx +++ /dev/null @@ -1,353 +0,0 @@ -import Callout from '@/components/Callout' - -# Migrate from v1 - - -Never used Pipedream v1? You can skip this migration guide and read on about [Steps](/workflows/steps/). - - -We are excited to announce that we have launched a new version (v2) of Pipedream to all new and existing users! - -We have re-imagined the UX from the ground up, made the product much easier to use and have improved performance. In addition, we are introducing powerful new features including: - -- **Edit & test** your workflows in separate editing mode without impacting live workflows -- **Support for multiple languages** including [Node.js](/code/nodejs), [Python](/code/python), [Bash](/code/bash) and [Go](/code/go) -- **Granular testing** including the ability to test individual steps and more -- **Multiple triggers** are now supported per workflow -- **Improved** forms for easier configuration and streamlined building - -_Get Started_ - -- Read our [quickstart](/quickstart/), [docs](/), and/or [FAQ](#faqs) -- Have questions? Ask here or on [Discourse](https://pipedream.com/community) -- As a reminder, all integration components are source-available and [hosted on GitHub](https://github.com/PipedreamHQ/pipedream). You can [contribute your own components](/apps/contributing/) or improve existing ones. - -Watch a demo: - - - -And this is just the beginning — we have an exciting roadmap planned for 2022 including workflow serialization and GitHub integration. - -## New Builder Overview - -Fundamentally, the new version of the workflow builder gives you the same abilities to build, test and deploy your workflows. However, you'll notice some differences in how to build workflows. - -### Building vs Inspecting - -In v1, building your workflow and inspecting past events were visible in the same view. The new v2 builder has improved this by separating the workflow **Builder** from the workflow events **Inspector**. - -Switch between these contexts using the menu in the top right of the workflow builder. - -![Switching between the Builder & Inspector contexts in the v2 workflow builder](./images/new-builder-context-switcher.gif) - -When you first open a deployed workflow, you're presented with the **Inspector** version of the workflow. In this view you can see logs of past events, and select them to see the results of each step in the workflow. - -![Example of events listed in the Inspector](./images/inspector-sample.png) - -To edit the workflow, click the **Edit** button in the top right hand corner. This will close the inspector and allow you to edit your workflow without the distraction of logs from the production flow. - -![Example of the new dedicated Builder mode](./images/builder-mode-sample.png) - -### Testing Changes - -In the v1 workflow builder, you had to deploy the whole workflow to test changes to any step. To make changes to a deployed workflow, you had to made edits on the live version. - -We've improved this flow. Now you can test your changes with a new **Test** button without effecting the live version of the workflow. - -In addition to testing single steps, you can now selectively test portions of your workflow (e.g. all steps above or below the selected step): - -![Selectively pick testing your workflow above or below the current step is now available.](./images/test-workflow-portions.png) - -#### Testing individual events - -Not only can you test portions of your workflow in isolation, but you can also select a specific event to run against your workflow. - -In the **Test Trigger** portion of your trigger, you can select a past event seen by the workflow and build your steps against it - without having to re-trigger it manually: - -![Test your workflow with a specific event](./images/testing-individual-events.gif) - -### Deploying Changes - -After you're happy with your changes, **deploy** them to your production workflow. Just click the **Deploy** button in the top right hand corner of the screen. - -After deploying your changes, your workflow is now live, and any changes you made will run against incoming events. - -## Node.js Code Step Changes - -There are a few changes to the Node.js code steps that you should know about. Some functions have been renamed for more clarity, and we've aligned the Node.js code steps closer to the [Component API](/components/). - -### Code Scaffolding Format - -In v1, the Node.js steps would automatically scaffold new Node.js steps in this format: - -```javascript -async (event, steps) { - // your code could be entered in here -} -``` - -In v2, the new scaffolding is wrapped with a new `defineComponent` function: - -```javascript -defineComponent({ - async run({ steps, $ }) { - // your code can be entered here - }, -}); -``` - -1. The `event` from the trigger step is still available, but exposed in `steps.trigger.event` instead. -2. The `$` variable has been passed into the `run` function where your code is executed. - -You can think of the `$` as the entry point to built in Pipedream functions. In v1, this special functions included `$end`, `$respond`, etc. In v2, these have been remapped to `$.flow.exit` and `$.respond` respectively. - -These changes unify workflow development to the [Component API](/components/api) used by pre-built actions and also allows the [defining of props](#params-vs-props) from within your code steps. - -### Using 3rd party packages - -In v1, you had to define your imports of 3rd party packages within the scaffolded function: - -```javascript -async (event, steps) { - const axios = require('axios'); - // your code could be entered in here -} -``` - -Now, in v2 workflows you can `import` your packages in the top of the step, just like a normal Node.js module: - -```javascript -import axios from "axios"; - -defineComponent({ - async run({ steps, $ }) { - // your code can be entered here - }, -}); -``` - -Allowing all of the scaffolding to be edited opens up the ability to [pass props](/code/nodejs/#passing-props-to-code-steps) into your Node.js code steps, which we'll cover later. - -### Step Exports - -In v1, you could assign arbitrary properties to `this` within a Node.js step and the properties would be available as step exports: - -```javascript -// this step's name is get_customer_data -async (event, steps) { - this.name = 'Dylan'; - // downstream steps could use steps.get_customer_data.name to retrieve 'Dylan' -} -``` - -In v2 you use $.export to export data, instead:: - -```javascript -// this step's name is get_customer_data -defineComponent({ - async run({ steps, $ }) { - $.export("name", "Dylan"); - // downstream steps can use steps.get_customer_data.name to retrieve 'Dylan' - }, -}); -``` - - -Using `return` to export data is the same from v1 to v2. You can still `return` data, and it will be available to other steps with `steps.[stepName].$return_value. - - -### Exiting a workflow early - -In v1, the `$end` function can be called to exit a flow early: - -```javascript -async (event, steps) { - $end('Exiting the whole workflow early'); - console.log('I will never run'); -} -``` - -In v2, this same function is available, but under `$.flow.exit`: - -```javascript -defineComponent({ - async run({ steps, $ }) { - return $.flow.exit("Exiting the workflow early"); - console.log("I will never run"); - }, -}); -``` - -### Params vs Props - -In the v1 builder, you could pass input to steps using `params`. In the v2 builder, you pass input using [props](/components/api/#component-api). - -You can still enter free text and select data from other steps in pre-built actions. Also can add your own custom props that accept input like strings, numbers and more just like in v1. - -#### Defining params - -In the v1 workflow builder, params could be structured or unstructured. The [params schema builder](https://pipedream.com/docs/v1/workflows/steps/params/#configuring-custom-params) allowed you to add your own custom params to steps. - -In v2, you can add your own custom props without leaving the code editor. - -```javascript -export default defineComponent({ - props: { - firstName: { - type: "string", - label: "Your first name", - }, - }, - async run({ steps, $ }) { - console.log(this.firstName); - }, -}); -``` - -In the example, you added a firstName string prop. The value assigned to this prop in the workflow builder. - -Additionally, Pipedream renders a visual component in the step **Configuration** to accept this input: - -![Custom props render in the Configuration portion of the code step.](./images/custom-string-prop.png) - -### Connecting apps - -In the v2 builder, you can connect apps with your code using [props](/components/api/#props). - -Above the `run` function, define an app prop that your Node.js step integrates with: - -```javascript -import { axios } from "@pipedream/platform"; - -export default defineComponent({ - props: { - slack: { - type: "app", - app: "slack", - }, - }, - async run({ steps, $ }) { - return await axios($, { - url: `https://slack.com/api/users.profile.get`, - headers: { - Authorization: `Bearer ${this.slack.$auth.oauth_access_token}`, - }, - }); - }, -}); -``` - -After testing the step, you'll see the Slack app will appear in the **Configuration** section on the left hand side. In this section you can choose which Slack account you'd like to use in the step. - -![Example of adding an app connection to a v2 Node.js step](./images/app-props-example.png) - -### HTTP Response - -You can still return an HTTP response from an HTTP-triggered workflow. - -Use [`$.respond`](/workflows/steps/triggers/#http) to send a JSON or string response from the HTTP call that triggered the workflow. - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - $.respond({ - status: 200, - headers: {}, - body: { - message: "hello world!", - }, - }); - }, -}); -``` - -Please note, you'll also need to configure the HTTP trigger step to also allow custom responses. Use the dropdown in the **HTTP Response** section of the HTTP trigger to select the **Return a custom response from your workflow** option: - -![Select the option to allow your workflow to send it's own HTTP responses](./images/custom-http-response-option.png) - -## Known Gaps & Limitations - -However, some features from the original builder are not currently available in v2. The Pipedream team is working to quickly address these items, but if you have feedback that isn't listed here, please [reach out](https://pipedream.com/support). - -### Sharing workflows - -At this time, sharing is not yet implemented in v2 of the workflow builder. As workaround, create your workflows in a organization which make workflows available to your team members. - -If you need assistance transferring workflows across accounts, [please contact us](https://pipedream.com/support). - -### `$checkpoint` - -The `$checkpoint` functionality to save data between workflow runs is not supported in v2, and has been replaced by [Data Stores](/code/nodejs/using-data-stores/). - -### Public workflows - -At this time, all v2 workflows are private. Unfortunately at this time there is no workaround. We'll announce when a workaround for this limitation is available. - -If you're working with Pipedream support to troubleshoot your workflow, you can share it with the support team under your workflow's **Settings**. - -### Rolling back a specific version - -In v2, you can test and save your progress on a workflow _without_ deploying it. - -However, after deploying it's not possible to rollback to a prior version of a deployed workflow. - -You can still edit a deployed workflow, just like in v1 but automatic version rollbacks are not currently possible. - -### Replaying production events - -In the v2 builder, you can still view individual events that trigger your v2 workflows in the **Inspector** events log. You can delete specific events or all of them in one click as well. - -To replay past events against your deploy v2 workflows, open the event's menu and click **Replay Event**. This will rerun your workflow with this same event. - -## FAQs - -### What are the benefits of the new (v2) workflow builder? - -- **Edit & test** your workflows in separate editing mode without impacting live workflows -- **Support for multiple languages** including Node, Python, Golang & bash -- **Granular testing** including the ability to test individual steps and more -- **Multiple triggers** are now supported per workflow -- **Improved** forms for easier configuration and streamlined building - -### What are the limitations of the new (v2) workflow builder? - -- `$checkpoint` has been removed from v2 workflows, but [Data Stores](/code/nodejs/using-data-stores/) provides a similar API. -- Sharing workflows is not supported -- Making workflows public is not supported - -### Are v2 workflows backwards compatible? - -No, v2 workflows are not currently compatible with the v1 builder. - -However, pre-built component actions are still compatible across both versions. If you do encounter a gap from v1 actions in the v2 builder, [reach out to us](https://pipedream.com/support). - -### Is the Component API changing as well? Will I need to rewrite Components? - -No. Any components in the public registry or any private components you have published in your account are compatible with v2. - -The v2 workflow builder utilizes the same Component API allowing you to create components from within your workflows, which was not possible in v1. - -### Will I still be able to open and edit v1 workflows? - -Yes, absolutely you will still be able to view and edit v1 workflows. There is no need to immediately change your workflows from v1 to v2. - -### How do I migrate v1 workflows to v2 workflows? - -At this time we do not have an automated process to change v1 to v2. To create a v2 equivalent workflow, you can recompose your v1 workflow in the v2 builder. - -However, if it uses custom Node.js code steps, be sure to [follow the changes we describe in the guide above](/migrate-from-v1/#node-js-code-step-changes). - -### When will the new (v2) workflow builder be the default builder for all customers? - -By default, existing users will still default to the v1 builder. You can create new v2 workflows from the dropdown menu next to the New workflow button. - -if you'd like to default to the v2 builder when creating new workflows, you can change the **Builder Version** in [your account settings](https://pipedream.com/settings/account). - -### When will I no longer be able to create v1 workflows? - -There is currently no deprecation date for v1 workflows. We will continue to support of v1 workflows until we have feature parity with v2. - -When this date becomes clear we will provide assistance to automatically and assist migrate v1 to v2 workflows for you. diff --git a/docs-v2/pages/new-feature-or-bug/_meta.json b/docs-v2/pages/new-feature-or-bug/_meta.json deleted file mode 100644 index 5e6a93c3a56e9..0000000000000 --- a/docs-v2/pages/new-feature-or-bug/_meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "index": { - "display": "hidden" - } -} diff --git a/docs-v2/pages/new-feature-or-bug/index.mdx b/docs-v2/pages/new-feature-or-bug/index.mdx deleted file mode 100644 index 766506155d3ee..0000000000000 --- a/docs-v2/pages/new-feature-or-bug/index.mdx +++ /dev/null @@ -1,4 +0,0 @@ - -# New Features / Bugs - -To request a new feature or app integration, or to report a new bug, please [see the instructions here](https://github.com/PipedreamHQ/pipedream#found-a-bug-have-a-feature-to-suggest). diff --git a/docs-v2/pages/nodejs20-faq-2024-02/_meta.json b/docs-v2/pages/nodejs20-faq-2024-02/_meta.json deleted file mode 100644 index 5e6a93c3a56e9..0000000000000 --- a/docs-v2/pages/nodejs20-faq-2024-02/_meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "index": { - "display": "hidden" - } -} diff --git a/docs-v2/pages/nodejs20-faq-2024-02/index.mdx b/docs-v2/pages/nodejs20-faq-2024-02/index.mdx deleted file mode 100644 index 5752b88070c0d..0000000000000 --- a/docs-v2/pages/nodejs20-faq-2024-02/index.mdx +++ /dev/null @@ -1,313 +0,0 @@ -import Callout from '@/components/Callout' - -# Update to the Node.js runtime on Pipedream (February 2024) - -Effective 2024-02-01, the Node.js runtime will no longer load Amazon-specific certificate authority (CA) certificates by default. - - - -### Will this impact my workflows? - -**Existing workflows that are not re-deployed will NOT be impacted.** - -Only workflow that meet all of following criteria will be impacted: - -- Is re-deployed or deployed after 2024-02-01 -- Connects to an [AWS RDS](https://aws.amazon.com/rds/)-managed database (e.g., PostgreSQL, MySQL, or Microsoft SQL Server) -- Has server identity verification enabled (e.g., the `rejectUnauthorized` connection option is set to `true`) - - -Workflows deployed on or after 2024-02-01 that do *not* integrate with AWS RDS will not be impacted. - - -### Why are Amazon-specific CA certificates no longer loaded by default? - -Pipedream's runtime, which is based on the [Amazon Web Services](https://aws.amazon.com/) (AWS) Lambda Node.js runtime, is being upgraded to support Node.js 20. Starting with Node.js 20, Lambda no longer loads and validates Amazon-specific CA certificates by default. This improves cold start performance. - -For more information, see [Amazon's blog post](https://aws.amazon.com/blogs/compute/node-js-20-x-runtime-now-available-in-aws-lambda/). - -### How will this impact my workflows? - -Starting 2024-02-01, relevant database connection attempts will return a message like this: - -![Missing CA Certificate](https://res.cloudinary.com/pipedreamin/image/upload/v1705428110/self-signed-cert-in-cert-chain-error_fkvph0.png) - -### What do I need to do? - -If you are not planning to update and re-deploy a workflow [impacted by this update](#will-this-impact-my-workflows), you do not need to do anything. - -Otherwise, to successfully connect to your database, you can disable server identity verification or include CA certificates for your database. - -For more information on secure connections and CAs, see the AWS docs: [Using SSL/TLS to encrypt a connection to a DB instance](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html) - -Below are instructions for updating a workflow that connects to a [**MySQL**](#mysql), [**PostgreSQL**](#postgresql), or [**Microsoft SQL Server**](#microsoft-sql-server) DB instance. - -#### MySQL - -**Using a Pipedream action**: - -- Option A: Disable server identity verification - 1. If given the option, update the action to the latest version by clicking the "Update" button in the workflow step. The lastest version of MySQL actions disable server identity verification by default. - - ![Update Action Button](https://res.cloudinary.com/pipedreamin/image/upload/v1705519878/update-action-button-mysql_fr549p.png) - -- Option B: Use the [MySQL (SSL)](https://pipedream.com/apps/mysql-ssl) app - 1. Replace your MySQL action with the corresponding MySQL (SSL) action. - 2. [Connect](/connected-accounts/#connecting-accounts) your MySQL (SSL) account, specifying the `key`, `cert`, `ca`, and `rejectUnauthorized` connection options. - -**Using a custom code step**: - -- Option A: Disable server identity verification - 1. Set the `rejectUnauthorized` connection option to `false`. For example: - - ```javascript - const connection = await mysql.createConnection({ - host, - user, - password, - database, - ssl: { - rejectUnauthorized: false, - } - }); - ``` - -- Option B: Use the [MySQL (SSL)](https://pipedream.com/apps/mysql-ssl) app - 1. Replace the `mysql` app prop with a `mysql_ssl` app prop. - 2. Use the SSL connection options contained in the `$auth` object. - - Here's an example of an updated code step that uses the **`mysql_ssl`** app and the [`mysql2` npm package](https://www.npmjs.com/package/mysql2): - - ```javascript - import mysql from 'mysql2/promise'; - export default defineComponent({ - props: { - mysql: { - type: "app", - app: "mysql_ssl", - } - }, - async run({steps, $}) { - const { host, port, username, password, database, ca, cert, key, rejectUnauthorized } = this.mysql.$auth; - const connection = await mysql.createConnection({ - host, - port, - user: username, - password, - database, - ssl: { - rejectUnauthorized, - ca, - cert, - key, - } - }); - const [rows] = await connection.execute('SELECT NOW()'); - return rows; - }, - }) - ``` - -#### PostgreSQL - -**Using a Pipedream action**: - -1. If given the option, update the action to the latest version by clicking the "Update" button in the workflow step. - - ![Update Action Button](https://res.cloudinary.com/pipedreamin/image/upload/v1705519996/update-action-button-postgresql_aadmqm.png) - -2. Set the **Reject Unauthorized** field to `false`. - - ![Reject Unauthorized Field](https://res.cloudinary.com/pipedreamin/image/upload/v1705520053/postgresql-reject-unauthorized-field_ualjtm.png) - -**Using a custom code step**: - -- Option A: Disable server identity verification - 1. Set the `rejectUnauthorized` connection option to `false`. For example: - - ```javascript - const client = new Client({ - host, - database, - user, - password, - ssl: { - rejectUnauthorized: false, - }, - }); - ``` - -- Option B: Include all Amazon CA certificates - 1. Read the certificate file with all Amazon CA certificates located at `/var/runtime/ca-cert.pem`. - 2. Include the CA certificates in the database connection options. - - Here's an example code step that uses the [`pg` npm package](https://www.npmjs.com/package/pg): - - ```javascript - import { Client } from "pg"; - import fs from "fs"; - export default defineComponent({ - props: { - postgresql: { - type: "app", - app: "postgresql", - } - }, - async run({steps, $}) { - const { host, user, password, port, database } = this.postgresql.$auth; - const client = new Client({ - host, - database, - user, - password, - port, - ssl: { - rejectUnauthorized: true, - ca: fs.readFileSync("/var/runtime/ca-cert.pem") - }, - }); - await client.connect(); - const results = (await client.query("SELECT NOW()")).rows; - $.export("results", results); - await client.end(); - }, - }); - ``` - -- Option C: Include your region's certificate bundle - 1. Download the [certificate bundle for your AWS region](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.RegionCertificates). - 2. Import the certificate bundle into your workflow as an [attachment](/workflows/settings/#attachments). - 3. Include the CA certificates in the database connection options. - - Here's an example code step that uses the `pg` npm package: - - ```javascript - import { Client } from "pg"; - import fs from "fs"; - export default defineComponent({ - props: { - postgresql: { - type: "app", - app: "postgresql", - } - }, - async run({steps, $}) { - const { host, user, password, port, database } = this.postgresql.$auth; - const client = new Client({ - host, - database, - user, - password, - port, - ssl: { - rejectUnauthorized: true, - ca: fs.readFileSync(steps.trigger.context.attachments["-bundle.pem"]).toString(), - }, - }); - await client.connect(); - const results = (await client.query("SELECT NOW()")).rows; - $.export("results", results); - await client.end(); - }, - }); - ``` - -#### Microsoft SQL Server - -**Using a Pipedream action**: - -1. [Reconnect](/connected-accounts/#reconnecting-an-account) your Microsoft SQL Server account, setting the **trustServerCertificate** field to `true`. - -**Using a custom code step**: - -- Option A: Disable server identity verification - 1. Set the `trustServerCertificate` connection option to `true`. For example: - - ```javascript - const config = { - server, - database, - user, - password, - options: { - trustServerCertificate: true, - }, - }; - ``` - -- Option B: Include all Amazon CA certificates - 1. Read the certificate file with all Amazon certificates located at `/var/runtime/ca-cert.pem`. - 2. Include the certificates in the database connection options. - - Here's an example code step that uses the [`mssql` npm package](https://www.npmjs.com/package/mssql): - - ```javascript - import sql from "mssql"; - import fs from "fs"; - export default defineComponent({ - props: { - microsoft_sql_server: { - type: "app", - app: "microsoft_sql_server", - } - }, - async run({steps, $}) { - const { host, username, password, port, database, encrypt, trustServerCertificate } = this.microsoft_sql_server.$auth; - const config = { - server: host, - port: parseInt(port, 10), - database, - user: username, - password, - options: { - encrypt, - trustServerCertificate, - cryptoCredentialsDetails: { - ca: fs.readFileSync("/var/runtime/ca-cert.pem").toString(), - }, - }, - }; - await sql.connect(config); - return await sql.query`SELECT GETDATE()`; - }, - }); - ``` - -- Option C: Include your region's certificate bundle - 1. Download the [certificate bundle for your AWS region](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.RegionCertificates). - 2. Import the certificate bundle into your workflow as an [attachment](/workflows/settings/#attachments). - 3. Include the certificates in the database connection options. - - Here's an example code step that uses the `mssql` npm package: - - ```javascript - import sql from "mssql"; - import fs from "fs"; - export default defineComponent({ - props: { - microsoft_sql_server: { - type: "app", - app: "microsoft_sql_server", - } - }, - async run({steps, $}) { - const { host, username, password, port, database, encrypt, trustServerCertificate } = this.microsoft_sql_server.$auth; - const config = { - server: host, - port: parseInt(port, 10), - database, - user: username, - password, - options: { - encrypt, - trustServerCertificate, - cryptoCredentialsDetails: { - ca: fs.readFileSync(steps.trigger.context.attachments["-bundle.pem"]).toString(), - }, - }, - }; - await sql.connect(config); - return await sql.query`SELECT GETDATE()`; - }, - }) - ``` diff --git a/docs-v2/pages/pipedream-axios/_meta.json b/docs-v2/pages/pipedream-axios/_meta.json deleted file mode 100644 index 5e6a93c3a56e9..0000000000000 --- a/docs-v2/pages/pipedream-axios/_meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "index": { - "display": "hidden" - } -} diff --git a/docs-v2/pages/pipedream-axios/index.mdx b/docs-v2/pages/pipedream-axios/index.mdx deleted file mode 100644 index 73a7d4026a159..0000000000000 --- a/docs-v2/pages/pipedream-axios/index.mdx +++ /dev/null @@ -1,49 +0,0 @@ -# `@pipedream/platform` axios - -## Why `@pipedream/platform` axios? - -`axios` is an HTTP client for Node.js ([see these docs](/code/nodejs/http-requests/) for usage examples). - -`axios` has a simple programming API and works well for most use cases. But its default error handling behavior isn't easy to use. When you make an HTTP request and the server responds with an error code in the 4XX or 5XX range of status codes, `axios` returns this stack trace: - -![default axios stack trace](./images/default-axios-stack.png) - -This only communicates the error code, and not any other information (like the body or headers) returned from the server. - -Pipedream publishes an `axios` wrapper as a part of [the `@pipedream/platform` package](https://github.com/PipedreamHQ/platform). This presents the same programming API as `axios`, but implements two helpful features: - -1. When the HTTP request succeeds (response code < `400`), it returns only the `data` property of the response object — the HTTP response body. This is typically what users want to see when they make an HTTP request: - -![pipedream axios success case](./images/pipedream-axios-success.png) - -2. When the HTTP request _fails_ (response code >= `400`), it displays a detailed error message in the Pipedream UI (the HTTP response body), and returns the whole `axios` response object so users can review details on the HTTP request and response: - -![pipedream axios error case](./images/pipedream-axios-stack.png) - -## Using `@pipedream/platform` axios in component actions - -To use `@pipedream/platform` axios in component actions, import it: - -```javascript -import { axios } from "@pipedream/platform" -``` - -`@pipedream/platform` axios uses methods [provided by the `$` object](/components/api/#actions), so you'll need to pass that as the first argument to `axios` when making HTTP requests, and pass the [standard `axios` request config](https://github.com/axios/axios#request-config) as the second argument. - -Here's an example action: - -```javascript -import { axios } from "@pipedream/platform" - -export default { - key: "my-test-component", - name: "My Test component", - version: "0.0.1", - type: "action", - async run({ $ }) { - return await axios($, { - url: "https://httpstat.us/200", - }) - } -} -``` \ No newline at end of file diff --git a/docs-v2/pages/pricing.mdx b/docs-v2/pages/pricing.mdx deleted file mode 100644 index 0829b4087103f..0000000000000 --- a/docs-v2/pages/pricing.mdx +++ /dev/null @@ -1,233 +0,0 @@ -import Callout from '@/components/Callout' - -# Plans and Pricing - -We believe anyone should be able to run simple, low-volume workflows at no cost. We also hope that you share your [sources](/components#sources), [workflows](/workflows), [actions](/components#actions), and other integration components so that other Pipedream users benefit from your work. - -To support these goals, Pipedream offers a generous [free tier](#free-tier), and you can **[request a free trial of our Advanced or Business plan](https://pipedream.com/pricing)**. You can run sources and workflows for free within the limits of the free tier. If you hit these limits, you can upgrade to one of our [paid tiers](#paid-tiers). - -[Read more about our plans and pricing here](https://pipedream.com/pricing). - -## Free Tier - -Free Tiers have access to all pre-built actions and triggers, and all of the workflow building capabilites as other paid tiers. - -But Free account have a [daily limit of free credits](/limits#daily-credits-limit) that cannot be exceed. Standard [Pipedream platform limits](/limits/) apply to Free Accounts as well. - -To lift the daily credit limit and increase the number of workflows and connected accounts [upgrade to a paid tier](https://pipedream.com/pricing). - -### Free Tier Connected Accounts - -Free Tier accounts can connect up to 3 different service accounts like Twitter, Discord, Google Sheets, or any of the thousands of available connections. - -### Free Tier Workflow Limitations - -Free Tier accounts have a [daily credit limit](/limits#daily-credits-limit) and have limits on the number of active workflows. Upgrading to a [paid tier](https://pipedream.com/pricing) will increase the number of available active workflows and connected accounts. - -### Free Tier Polling Interval Limitations - -Free Tier account triggers powered by polling are limited to the longest interval. Paid tiers have an option to polling at a substantially higher frequency. - -### Free Tier Support options - -Users on the Free Tier have access to community support, on [our forum](https://pipedream.com/community) and Slack. [Visit out Support page](https://pipedream.com/support) for more information. - -## Paid Tiers - -[Visit our pricing page](https://pipedream.com/pricing) to learn more about our paid plans. - -All paid plans vary features based on tier, but each paid plan option will: - -- Remove the daily {process.env.DAILY_CREDITS_LIMIT} [credits](#credits) limit -- Increase the number of active workflows available -- Increase the number of connected accounts - -## Definition of Terms - -Pipedream uses a number of terms to describe platform metrics and details of our plans. See the definitions of key terms below. - -### Credits - -Pipedream charges one credit per 30 seconds of compute time at 256MB megabytes of memory (the default) per workflow execution. Credits are also charged for [dedicated workers](/workflows/settings/#eliminate-cold-starts). - -**Most workflow executions use a single credit**, regardless of the number of steps (unlike some other platforms, Pipedream does not charge for usage based on the number of steps). - -Credits are not charged for workflows during development or testing. - -Adding additional memory capacity to workflows will increase credit usage in intervals of 256 megabytes. For example, doubling the memory of a workflow from 256MB to 512MB will double the cost of credits in the same execution time. - -#### Scenarios - - -A workflow that executes once for less than 30 seconds total - -This is the most common scenario. Regardless of the number of steps within the workflow, if it completes it's execution under 30 seconds then only one credit is incurred. - - - -A workflow that executes 5 times with 1 second per run - -5 credits are incurred, because the workflow ran for a total of 5 executions and under the 30 second threshold (5 executions at 1 credit each). - - - -A workflow that executes 2 times with 35 seconds per run - -4 credits are incurred, because each workflow execution exceeded 1 credit time limit of 30 seconds by 5 seconds. - - - -Developing a workflow with test events in the Pipedream workflow builder - -0 credits are incurred. Developing and testing your workflows is free. - -Execution time used to develop a workflow in the builder does not count towards your credit usage. - - - -An active standard workflow that isn't executed at all in a billing period - -0 credits are incurred. Pipedream only charges credits for workflow executions. - - - -A workflow with 512 megabytes of memory executing for 5 seconds - -2 credits are incurred, because the workflow executed for a total of 5 seconds at 512 megabytes of memory. - - -#### Source Credit Usage - -When an [event source](/sources) triggers a workflow, **the source execution is included for free.** This includes workspaces on the [Free Tier](/pricing/#free-tier). - -When a source is configured as a workflow trigger, the core value is in the workflow. You won't be charged for two credits (one to run the source, one to run the workflow) when the workflow contains the core logic. - - -This free credit per execution **only** applies to sources from the [Pipedream public registry](/sources). If you deploy a private custom source to your account, then all computation time including the inital 30 seconds for that private source counts toward credits. - - - -A polling source finishing under 30 seconds per execution - -For example, a source that polls an API for new events like [Airtable - New Row Added](https://pipedream.com/apps/airtable/triggers/new-records) only takes ~5 seconds to poll and emit events to subscribing workflows. - -This would result in **0 credits** per run because the **Airtable - New Row Added** source is a [publicly available component](https://pipedream.com/apps/airtable/triggers/new-records). - - - -A polling source finishing over 30 seconds per execution - -Consider a source (like **RSS - New Item in Feed** for instance) that takes 60 seconds total to finish polling, per execution. - -Each execution of this source would result in **0 credits** because the **RSS - New Item in Feed** source is a [publicly available component](https://pipedream.com/apps/rss/triggers/new-item-in-feed). - - - -A custom source that finished under 30 seconds per execution - -This would result in **1 credit** per execution because the initial free credit only applies to Pipedream Public Registry sources attached to at least one workflow. - - -### Billing Period - -Many of the usage statistics for paid users are tied to a **billing period**. Your billing period starts when you sign up for a paid plan, and recurs roughly once a month for the duration of your subscription. - -For example, if you sign up on Jan 1st, your first billing period will last one month, ending around Feb 1st, at which point you'll start a new billing period. - -Your invoices are tied to your billing period. [Read more about invoicing / billing here](#when-am-i-invoiced-billed-for-paid-plans). - -### Included Credits - -When you sign up for a paid plan, you pay a platform fee at the start of each [billing period](#billing-period). This minimum monthly charge grants you a base of included credits that you can use for the rest of your billing period (see your [Billing and Usage Settings](https://pipedream.com/settings/billing) for your exact quota). If you have been granted any additional credit increases by Pipedream, that is added to the included credits. - -### Additional Credits - -Any credits you run over your [included credit](/limits/#daily-credits-limit) are called **additional credits**. This usage is added to the invoice for your next [billing period](#billing-period), according to the [invoicing cycle described here](#when-am-i-invoiced-billed-for-paid-plans). - -### Data Store Keys - -A Data Store key represents a single record in a Data Store. - -In the example below, there are two records in the Data Store, and therefore there are two keys total. - -![Example of a Data Store with two keys](https://res.cloudinary.com/pipedreamin/image/upload/v1673537163/docs/CleanShot_2023-01-12_at_10.25.25_z6yg8t.png) - -## Managing my plan - -To cancel, upgrade or downgrade your plan, open the [pricing page](https://pipedream.com/pricing). - -To update your billing details, such as your VAT number, email address, etc. use the **Manage Billing Information** button in your [workspace billing settings](https://pipedream.com/settings/billing) to change your plan. Within this portal you can cancel, upgrade or downgrade your plan at any time. - -### Upgrading behavior - -Upgrading your subscription instantly activates the features available to your workspace. For example, if you upgrade your workspace from Free to Basic, that workspace will be able to activate more workflows and connected accounts. - -### Downgrading behavior - -Downgrades will apply at the end of your billing cycle, and any workflows that use features outside of the new billing plan will be automatically disabled. - -For example, if your workspace downgrades from Advanced to Basic and a workflow uses an Advanced feature such as [auto-retries](/workflows/settings/#auto-retry-errors), then this workflow will be disabled because the workspace plan no longer qualifies for that feature. - -Additionally, resource limits such as the number of active workflows and connected accounts will also be enforced at this same time. - -### Cancellation behavior - -To cancel your plan, open the [pricing page](https://pipedream.com/pricing) and click **Cancel** beneath your current plan. - -Cancelling your subscription will apply at the end of your current billing period. Workflows, connected accounts and sources will be deactivated from newest to oldest until the Free limits have been reached. - -## FAQ - -### How does workflow memory affect credits? - -Pipedream charges credits proportional to the memory configuration. If you run your workflow at the default memory of {process.env.MEMORY_LIMIT}, you are charged one credit each time your workflow executes for 30 seconds. But if you configure your workflow with `1024MB` of memory, for example, you're charged **four** credits, since you're using `4x` the default memory. - -### Are there any limits on paid tiers? - -**You can run any number of credits for any amount of compute time** on any paid tier. [Other platform limits](/limits/) apply. - -### When am I invoiced / billed for paid plans? - -When you upgrade to a paid tier, Stripe will immediately charge your payment method on file for the platform fee tied to your plan (see [https://pipedream.com/pricing](https://pipedream.com/pricing)) - -If you accrue any [additional credits](#additional-credits), that usage is reported to Stripe throughout the [billing period](/pricing/#billing-period). That overage, as well as the next platform fee, is charged at the start of the _next_ billing period. - -### Do any plans support payment by invoice, instead of credit / debit card? - -Yes, Pipedream can issue invoices on the Enterprise Plan. Invoices are paid annually. - -### How does Pipedream secure my credit card data? - -Pipedream stores no information on your payment method and uses Stripe as our payment processor. [See our security docs](/privacy-and-security/#payment-processor) for more information. - -### Are unused credits rolled over from one period to the next? - -**No**. On the Free tier, unused included daily credits under the daily limit are **not** rolled over to the next day. - -On paid tiers, unused included credits are also **not** rolled over to the next month. - -### How do I change my billing payment method? - -Please visit your [Stripe customer portal](https://pipedream.com/settings/billing?rtsbp=1) to change your payment method. - -### How can I view my past invoices? - -Invoices are emailed to your billing email address. You can also visit your [Stripe customer portal](https://pipedream.com/settings/billing?rtsbp=1) to view past invoices. - -### Can I retrieve my billing information via API? - -Yes. You can retrieve your usage and billing metadata from the [/users/me](/api/rest/#get-current-user-info) endpoint in the Pipedream REST API. - -### How do I cancel my paid plan? - -You can cancel your plan in your [Billing and Usage Settings](https://pipedream.com/settings/billing). You will have access to your paid plan through the end of your current billing period. Pipedream does not prorate plans cancelled within a billing period. - -If you'd like to process your cancellation immediately, and downgrade to the free tier, please [reach out](https://pipedream.com/support). - -### How do I change the billing email, VAT, or other company details tied to my invoice? - -You can update your billing information in your [Stripe customer portal](https://pipedream.com/settings/billing?rtsbp=1). - -### How do I contact the Pipedream team with other questions? - -You can start a support ticket [on our support page](https://pipedream.com/support). Select the **Billing Issues** category to start a billing related ticket. diff --git a/docs-v2/pages/pricing/_meta.tsx b/docs-v2/pages/pricing/_meta.tsx new file mode 100644 index 0000000000000..f07074c72382a --- /dev/null +++ b/docs-v2/pages/pricing/_meta.tsx @@ -0,0 +1,4 @@ +export default { + "index": "Overview", + "faq": "FAQ", +} as const diff --git a/docs-v2/pages/pricing/faq.mdx b/docs-v2/pages/pricing/faq.mdx new file mode 100644 index 0000000000000..1106ba4fd1550 --- /dev/null +++ b/docs-v2/pages/pricing/faq.mdx @@ -0,0 +1,55 @@ +# FAQ + +## How does workflow memory affect credits? + +Pipedream charges credits proportional to the memory configuration. If you run your workflow at the default memory of {process.env.MEMORY_LIMIT}, you are charged one credit each time your workflow executes for 30 seconds. But if you configure your workflow with `1024MB` of memory, for example, you're charged **four** credits, since you're using `4x` the default memory. + +## Are there any limits on paid tiers? + +**You can run any number of credits for any amount of compute time** on any paid tier. [Other platform limits](/workflows/limits/) apply. + +## When am I billed for paid plans? + +When you upgrade to a paid tier, Stripe will immediately charge your payment method on file for the platform fee tied to your plan (see [https://pipedream.com/pricing](https://pipedream.com/pricing)) + +If you accrue any [additional credits](/pricing/#additional-credits), that usage is reported to Stripe throughout the [billing period](/pricing/#billing-period). That overage, as well as the next platform fee, is charged at the start of the _next_ billing period. + +## Do any plans support payment by invoice, instead of credit / debit card? + +Yes, Pipedream can issue invoices on the Business Plan. [Please reach out to support](https://pipedream.com/support) + +## How does Pipedream secure my credit card data? + +Pipedream stores no information on your payment method and uses Stripe as our payment processor. [See our security docs](/privacy-and-security/#payment-processor) for more information. + +## Are unused credits rolled over from one period to the next? + +**No**. On the Free tier, unused included daily credits under the daily limit are **not** rolled over to the next day. + +On paid tiers, unused included credits are also **not** rolled over to the next month. + +## How do I change my billing payment method? + +Please visit your [Stripe customer portal](https://pipedream.com/settings/billing?rtsbp=1) to change your payment method. + +## How can I view my past invoices? + +Invoices are emailed to your billing email address. You can also visit your [Stripe customer portal](https://pipedream.com/settings/billing?rtsbp=1) to view past invoices. + +## Can I retrieve my billing information via API? + +Yes. You can retrieve your usage and billing metadata from the [/users/me](/rest-api/#get-current-user-info) endpoint in the Pipedream REST API. + +## How do I cancel my paid plan? + +You can cancel your plan in your [Billing and Usage Settings](https://pipedream.com/settings/billing). You will have access to your paid plan through the end of your current billing period. Pipedream does not prorate plans cancelled within a billing period. + +If you'd like to process your cancellation immediately, and downgrade to the free tier, please [reach out](https://pipedream.com/support). + +## How do I change the billing email, VAT, or other company details tied to my invoice? + +You can update your billing information in your [Stripe customer portal](https://pipedream.com/settings/billing?rtsbp=1). + +## How do I contact the Pipedream team with other questions? + +You can start a support ticket [on our support page](https://pipedream.com/support). Select the **Billing Issues** category to start a billing related ticket. diff --git a/docs-v2/pages/pricing/index.mdx b/docs-v2/pages/pricing/index.mdx new file mode 100644 index 0000000000000..4f3fcbb246f5d --- /dev/null +++ b/docs-v2/pages/pricing/index.mdx @@ -0,0 +1,167 @@ +import Callout from '@/components/Callout' + +# Plans and Pricing + +We believe anyone should be able to run simple, low-volume workflows at no cost. We also hope that you share your [sources](/workflows/contributing/components/#sources), [workflows](/workflows/building-workflows/), [actions](/workflows/contributing/components/#actions), and other integration components so that other Pipedream users benefit from your work. + +To support these goals, Pipedream offers a generous [free tier](#free-tier), and you can **[request a free trial of our Advanced or Business plan](https://pipedream.com/pricing)**. You can run sources and workflows for free within the limits of the free tier. If you hit these limits, you can upgrade to one of our [paid tiers](#paid-tiers). + +[Read more about our plans and pricing here](https://pipedream.com/pricing). + +## Free Tier + +Free Tiers have access to all pre-built actions and triggers, and all of the workflow building capabilites as other paid tiers. + +But Free account have a [daily limit of free credits](/workflows/limits/#daily-credits-limit) that cannot be exceed. Standard [Pipedream platform limits](/workflows/limits/) apply to Free Accounts as well. + +To lift the daily credit limit and increase the number of workflows and connected accounts [upgrade to a paid tier](https://pipedream.com/pricing). + +### Free Tier Connected Accounts + +Free Tier accounts can connect up to 3 different service accounts like Twitter, Discord, Google Sheets, or any of the thousands of available connections. + +### Free Tier Workflow Limitations + +Free Tier accounts have a [daily credit limit](/workflows/limits/#daily-credits-limit) and have limits on the number of active workflows. Upgrading to a [paid tier](https://pipedream.com/pricing) will increase the number of available active workflows and connected accounts. + +### Free Tier Polling Interval Limitations + +Free Tier account triggers powered by polling are limited to the longest interval. Paid tiers have an option to polling at a substantially higher frequency. + +### Free Tier Support options + +Users on the Free Tier have access to community support, on [our forum](https://pipedream.com/community) and Slack. [Visit out Support page](https://pipedream.com/support) for more information. + +## Paid Tiers + +[Visit our pricing page](https://pipedream.com/pricing) to learn more about our paid plans. + +All paid plans vary features based on tier, but each paid plan option will: + +- Remove the daily [credits](#credits) limit +- Increase the number of active workflows available +- Increase the number of connected accounts + +## Definition of Terms + +Pipedream uses a number of terms to describe platform metrics and details of our plans. See the definitions of key terms below. + +### Credits + +Pipedream charges one credit per 30 seconds of compute time at 256MB megabytes of memory (the default) per [workflow segment](/workflows/building-workflows/control-flow/#workflow-segments). Credits are also charged for [dedicated workers](/workflows/building-workflows/settings/#eliminate-cold-starts). + +Unlike some other platforms, Pipedream does not charge for usage based on the number of steps. Credits are not charged for workflows during development or testing. + +Adding additional memory capacity to workflows will increase credit usage in intervals of 256 megabytes. For example, doubling the memory of a workflow from 256MB to 512MB will double the cost of credits in the same execution time. + +#### Scenarios + + +Developing a workflow with test events in the Pipedream workflow builder is free. No credit usage is incurred. + + + +If an active workflow isn't executed in a billing period no credit usage is incurred. Pipedream only charges credits for workflow executions. + + +##### Workflow segments configured to use 256MB memory (default) + +| Scenario | Credits Used | +| --- | --- | +| Simple linear workflow - 1 second of compute | 1 credit | +| Simple linear workflow - 15 seconds of compute | 1 credit | +| Simple linear workflow - 35 seconds of compute | 2 credits | +| Linear workflow with a delay- 15 seconds before the delay- 15 seconds after execution resumes | 2 credits | +| Workflow with a branch - 3 seconds before the branch- 15 seconds within the executed branch | 2 credits | +| Workflow with a branch - 3 seconds before the branch, 15 seconds within the executed branch, 3 seconds after the branch in the parent flow | 3 credits | + +##### Workflow segments configured to use 1GB memory + +| Scenario | Credits Used | +| --- | --- | +| Simple linear workflow - 1 second of compute | 4 credit | +| Simple linear workflow - 15 seconds of compute | 4 credit | +| Simple linear workflow - 35 seconds of compute | 8 credits | +| Linear workflow with a delay- 15 seconds before the delay- 15 seconds after execution resumes | 8 credits | +| Workflow with a branch - 3 seconds before the branch- 15 seconds within the executed branch | 8 credits | +| Workflow with a branch - 3 seconds before the branch, 15 seconds within the executed branch, 3 seconds after the branch in the parent flow | 24 credits | + +#### Source Credit Usage + +When an [event source](/workflows/building-workflows/triggers/) triggers a workflow, **the source execution is included for free.** This includes workspaces on the [Free Tier](/pricing/#free-tier). + +When a source is configured as a workflow trigger, the core value is in the workflow. You won't be charged for two credits (one to run the source, one to run the workflow) when the workflow contains the core logic. + + +This free credit per execution **only** applies to sources from the [Pipedream public registry](/workflows/building-workflows/triggers/). If you deploy a private custom source to your account, then all computation time including the inital 30 seconds for that private source counts toward credits. + + + +A polling source finishing under 30 seconds per execution + +For example, a source that polls an API for new events like [Airtable - New Row Added](https://pipedream.com/apps/airtable/triggers/new-records) only takes ~5 seconds to poll and emit events to subscribing workflows. + +This would result in **0 credits** per run because the **Airtable - New Row Added** source is a [publicly available component](https://pipedream.com/apps/airtable/triggers/new-records). + + + +A polling source finishing over 30 seconds per execution + +Consider a source (like **RSS - New Item in Feed** for instance) that takes 60 seconds total to finish polling, per execution. + +Each execution of this source would result in **0 credits** because the **RSS - New Item in Feed** source is a [publicly available component](https://pipedream.com/apps/rss/triggers/new-item-in-feed). + + + +A custom source that finished under 30 seconds per execution + +This would result in **1 credit** per execution because the initial free credit only applies to Pipedream Public Registry sources attached to at least one workflow. + + +### Billing Period + +Many of the usage statistics for paid users are tied to a **billing period**. Your billing period starts when you sign up for a paid plan, and recurs roughly once a month for the duration of your subscription. + +For example, if you sign up on Jan 1st, your first billing period will last one month, ending around Feb 1st, at which point you'll start a new billing period. + +Your invoices are tied to your billing period. [Read more about invoicing / billing here](/pricing/faq/#when-am-i-billed-for-paid-plans). + +### Included Credits + +When you sign up for a paid plan, you pay a platform fee at the start of each [billing period](#billing-period). This minimum monthly charge grants you a base of included credits that you can use for the rest of your billing period (see your [Billing and Usage Settings](https://pipedream.com/settings/billing) for your exact quota). If you have been granted any additional credit increases by Pipedream, that is added to the included credits. + +### Additional Credits + +Any credits you run over your [included credit](/workflows/limits/#daily-credits-limit) are called **additional credits**. This usage is added to the invoice for your next [billing period](#billing-period), according to the [invoicing cycle described here](/pricing/faq/#when-am-i-billed-for-paid-plans). + +### Data Store Keys + +A Data Store key represents a single record in a Data Store. + +In the example below, there are two records in the Data Store, and therefore there are two keys total. + +![Example of a Data Store with two keys](https://res.cloudinary.com/pipedreamin/image/upload/v1673537163/docs/CleanShot_2023-01-12_at_10.25.25_z6yg8t.png) + +## Managing my plan + +To cancel, upgrade or downgrade your plan, open the [pricing page](https://pipedream.com/pricing). + +To update your billing details, such as your VAT number, email address, etc. use the **Manage Billing Information** button in your [workspace billing settings](https://pipedream.com/settings/billing) to change your plan. Within this portal you can cancel, upgrade or downgrade your plan at any time. + +### Upgrading behavior + +Upgrading your subscription instantly activates the features available to your workspace. For example, if you upgrade your workspace from Free to Basic, that workspace will be able to activate more workflows and connected accounts. + +### Downgrading behavior + +Downgrades will apply at the end of your billing cycle, and any workflows that use features outside of the new billing plan will be automatically disabled. + +For example, if your workspace downgrades from Advanced to Basic and a workflow uses an Advanced feature such as [auto-retries](/workflows/building-workflows/settings/#auto-retry-errors), then this workflow will be disabled because the workspace plan no longer qualifies for that feature. + +Additionally, resource limits such as the number of active workflows and connected accounts will also be enforced at this same time. + +### Cancellation behavior + +To cancel your plan, open the [pricing page](https://pipedream.com/pricing) and click **Cancel** beneath your current plan. + +Cancelling your subscription will apply at the end of your current billing period. Workflows, connected accounts and sources will be deactivated from newest to oldest until the Free limits have been reached. diff --git a/docs-v2/pages/privacy-and-security/_meta.json b/docs-v2/pages/privacy-and-security/_meta.json deleted file mode 100644 index 85be1e4e815af..0000000000000 --- a/docs-v2/pages/privacy-and-security/_meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "index": "Privacy and Security", - "best-practices": "Security best-practices", - "pgp-key": "PGP key" -} diff --git a/docs-v2/pages/privacy-and-security/_meta.tsx b/docs-v2/pages/privacy-and-security/_meta.tsx new file mode 100644 index 0000000000000..853d3c0df56ae --- /dev/null +++ b/docs-v2/pages/privacy-and-security/_meta.tsx @@ -0,0 +1,6 @@ +export default { + "index": "Privacy and Security", + "best-practices": "Security best-practices", + "hipaa": "HIPAA compliance", + "pgp-key": "PGP key", +} as const diff --git a/docs-v2/pages/privacy-and-security/best-practices.mdx b/docs-v2/pages/privacy-and-security/best-practices.mdx index 5e5dd02a183cf..10c2febec2bd1 100644 --- a/docs-v2/pages/privacy-and-security/best-practices.mdx +++ b/docs-v2/pages/privacy-and-security/best-practices.mdx @@ -1,35 +1,31 @@ # Security Best Practices -Pipedream implements a range of [privacy and security measures](/privacy-and-security/) meant to protect your data from unauthorized access. Since Pipedream [workflows](/workflows/), [event sources](/sources/), and other resources can run any Node.js code and process any event data, you also have a responsibility to ensure you handle that code and data securely. We've outlined a handful of best practices for that below. - - +Pipedream implements a range of [privacy and security measures](/privacy-and-security/) meant to protect your data from unauthorized access. Since Pipedream [workflows](/workflows/building-workflows/), [event sources](/workflows/building-workflows/triggers/), and other resources can run any code and process any event data, you also have a responsibility to ensure you handle that code and data securely. We've outlined a handful of best practices for that below. ## Store secrets as Pipedream connected accounts or environment variables -Even if your workflow code is private, you should never store secrets like API keys in code. These secrets should be stored in one of two ways: +Never store secrets like API keys directly in code. These secrets should be stored in one of two ways: -- [If Pipedream integrates with the app](https://pipedream.com/apps), use [connected accounts](/connected-accounts/) to link your apps / APIs. -- If you need to store credentials for an app Pipedream doesn't support, or you need to store arbitrary configuration data, use [environment variables](/environment-variables/). +- [If Pipedream integrates with the app](https://pipedream.com/apps), use [connected accounts](/integrations/connected-accounts/) to link your apps / APIs. +- If you need to store credentials for an app Pipedream doesn't support, or you need to store arbitrary configuration data, use [environment variables](/workflows/environment-variables/). Read more about how Pipedream secures connected accounts / environment variables [here](/privacy-and-security/#third-party-oauth-grants-api-keys-and-environment-variables). ## Deliver data to Pipedream securely -Whenever possible, encrypt data in transit to Pipedream. For example, use HTTPS endpoints when sending HTTP traffic to a workflow. +Always send data over HTTPS to Pipedream endpoints. ## Send data out of Pipedream securely When you connect to APIs in a workflow, or deliver data to third-party destinations, encrypt that data in transit. For example, use HTTPS endpoints when sending HTTP traffic to third parties. -## Add authentication to incoming event data - -You can add many public-facing triggers to your workflows. For example, when you add an HTTP trigger to your workflow, anyone with the associated trigger URL can invoke it. You should protect your workflow with an authentication mechanism like [Basic Auth](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication), JWT, or others. +## Require authorization for HTTP triggers -The easiest way to do this is to use the [Validate Webhook Auth action](https://pipedream.com/apps/http/actions/validate-webhook-auth). This supports common auth options, and you don't have to write any code to configure it. +HTTP triggers are public by default, and require no authorization or token to invoke. -If you need to implement custom logic in code, see [this workflow](https://pipedream.com/new?h=tch_OaJfNv) for a shared API key example. This reads the header `x-api-key` and compares it to the [environment variable](/environment-variables/) called `YOUR_WEBHOOK_API_KEY`. If the `x-api-key` header does not match this variable, it returns a `401 Unauthorized` error and exits the workflow early. +For many workflows, you should [configure authorization](/workflows/building-workflows/triggers/#authorizing-http-requests) to ensure that only authorized parties can invoke your HTTP trigger. -This pattern is typical for protecting workflows: add the authentication logic in a step at the top of your workflow, ending early if auth fails. If auth succeeds, Pipedream runs the remaining steps of your workflow. +For third-party services like webhooks, that authorize requests using their own mechanism, use the [Validate Webhook Auth action](https://pipedream.com/apps/http/actions/validate-webhook-auth). This supports common auth options, and you don't have to write any code to configure it. ## Validate signatures for incoming events, where available @@ -37,7 +33,7 @@ Many apps pass a **signature** with event data delivered via webhooks (or other Signatures are specific to the app sending the data, and the app should provide instructions for signature validation. **Not all apps compute signatures, but when they do, you should always verify them**. -When you use a Pipedream [event source](/sources/) as your workflow trigger, Pipedream should verify the signature for you. You can always [audit the code behind the event source](#audit-code-or-packages-you-use-within-a-workflow) to confirm this, and suggest further security improvements that you find. +When you use a Pipedream [event source](/workflows/building-workflows/triggers/) as your workflow trigger, Pipedream should verify the signature for you. You can always [audit the code behind the event source](#audit-code-or-packages-you-use-within-a-workflow) to confirm this, and suggest further security improvements that you find. See [Stripe's signature docs](https://stripe.com/docs/webhooks/signatures) for a real-world example. Pipedream's Stripe event source [verifies this signature for you](https://github.com/PipedreamHQ/pipedream/blob/bb1ebedf8cbcc6f1f755a8878c759522b8cc145b/components/stripe/sources/custom-webhook-events/custom-webhook-events.js#L49). @@ -51,9 +47,9 @@ The same follows for [npm](https://www.npmjs.com/) packages. Before you use a ne ## Limit what you log and return from steps -[Pipedream retains a limited history of event data](/limits/#event-execution-history) and associated logs for event sources and workflows. But if you cannot log specific data in Pipedream for privacy / security reasons, or if you want to limit risk, remember that **Pipedream only stores data returned from or logged in steps**. Specifically, Pipedream will only store: +[Pipedream retains a limited history of event data](/workflows/limits/#event-history) and associated logs for event sources and workflows. But if you cannot log specific data in Pipedream for privacy / security reasons, or if you want to limit risk, remember that **Pipedream only stores data returned from or logged in steps**. Specifically, Pipedream will only store: - The event data emitted from event sources, and any `console` logs / errors -- The event data that triggers your workflow, any `console` logs / errors, [step exports](/workflows/steps/#step-exports), and any data included in error stack traces. +- The event data that triggers your workflow, any `console` logs / errors, [step exports](/workflows/#step-exports), and any data included in error stack traces. Variables stored in memory that aren't logged or returned from steps are not included in Pipedream logs. Since you can modify any code in your Pipedream workflow, if you want to limit what gets logged from a Pipedream action or other step, you can adjust the code accordingly, removing any logs or step exports. diff --git a/docs-v2/pages/privacy-and-security/hipaa.mdx b/docs-v2/pages/privacy-and-security/hipaa.mdx new file mode 100644 index 0000000000000..73e218e9c12c1 --- /dev/null +++ b/docs-v2/pages/privacy-and-security/hipaa.mdx @@ -0,0 +1,38 @@ +# HIPAA compliance + +Pipedream can [sign Business Associate Addendums (BAAs)](#signing-a-business-associate-addendum) for Business customers intending to pass PHI to Pipedream. We can also provide a third-party SOC 2 report detailing our HIPAA-related controls. + +## HIPAA-eligible services + +- [Workflows](/workflows/building-workflows/) +- [Event sources](/workflows/building-workflows/triggers/) +- [Data stores](/workflows/data-management/data-stores/) +- [Destinations](/workflows/data-management/destinations/) + +### Ineligible services + +Any service not listed in the [HIPAA-eligible services](#hipaa-eligible-services) section is not eligible for use with PHI under HIPAA. Please reach out to [Pipedream support](https://pipedream.com/support) if you have questions about a specific service. + +The following services are explicitly not eligible for use with PHI under HIPAA. + +- [v1 workflows](/deprecated/migrate-from-v1/) +- [File stores](/workflows/data-management/file-stores/) + +## Your obligations as a customer + +If you are a covered entity or business associate under HIPAA, you must ensure that [you have a BAA in place with Pipedream](#signing-a-business-associate-addendum) before passing PHI to Pipedream. + +You must also ensure that you are using Pipedream in a manner that complies with HIPAA. This includes: + +- You may only use [HIPAA-eligible services](#hipaa-eligible-services) to process or store PHI +- You may not include PHI in Pipedream resource names, like the names of projects or workflows + +## Signing a Business Associate Addendum + +Pipedream is considered a Business Associate under HIPAA regulations. If you are a Covered Entity or Business Associate under HIPAA, you must have a Business Associate Agreement (BAA) in place with Pipedream before passing PHI to Pipedream. This agreement is an addendum to our standard terms, and outlines your obligations as a customer and Pipedream's obligations as a Business Associate under HIPAA. + +Please request a BAA by visiting [https://pipedream.com/support](https://pipedream.com/support). + +## Requesting information on HIPAA controls + +Please request compliance reports from [https://pipedream.com/support](https://pipedream.com/support). Pipedream can provide a SOC 2 Type II report covering Security controls, and a SOC 2 Type I report for Confidentiality and Availability. In 2025, Pipedream plans to include Confidentiality and Availability controls in our standard Type II audit. diff --git a/docs-v2/pages/privacy-and-security/index.mdx b/docs-v2/pages/privacy-and-security/index.mdx index e54ed92eb7c54..b785a09d03cbd 100644 --- a/docs-v2/pages/privacy-and-security/index.mdx +++ b/docs-v2/pages/privacy-and-security/index.mdx @@ -22,7 +22,7 @@ If you suspect Pipedream resources are being used for illegal purposes, or other ### SOC 2 -Pipedream undergoes regular third-party audits. We have demonstrated SOC 2 compliance and can provide a SOC 2 Type 2 report upon request. Please reach out to support@pipedream.com to request the latest report. +Pipedream undergoes annual third-party audits. We have demonstrated SOC 2 compliance and can provide a SOC 2 Type 2 report upon request. Please reach out to support@pipedream.com to request the latest report. We use [Drata](https://drata.com) to continuosly monitor our infrastructure's compliance with standards like SOC 2, and you can visit our [Security Report](https://app.drata.com/security-report/b45c2f79-1968-496b-8a10-321115b55845/27f61ebf-57e1-4917-9536-780faed1f236) to see a list of policies and processes we implement and track within Drata. @@ -42,10 +42,14 @@ You can find a list of Pipedream subprocessors [here](/subprocessors/). #### Submitting a GDPR deletion request -When you [delete your account](/user-settings/#delete-account), Pipedream deletes all personal data we hold on you in our system and our vendors. +When you [delete your account](/account/user-settings/#delete-account), Pipedream deletes all personal data we hold on you in our system and our vendors. If you need to delete data on behalf of one of your users, you can delete the event data yourself in your workflow or event source (for example, by deleting the events, or by removing the data from data stores). Your customer event data is automatically deleted from Pipedream subprocessors. +### HIPAA + +Pipedream can sign Business Associate Addendum (BAAs) for customers intending to pass PHI to Pipedream. We can also provide a third-party SOC 2 report detailing our HIPAA-related controls. See our [dedicated HIPAA docs](/privacy-and-security/hipaa) for more details. + ## Hosting Details Pipedream is hosted on the [Amazon Web Services](https://aws.amazon.com/) (AWS) platform in the `us-east-1` region. The physical hardware powering Pipedream, and the data stored by our platform, is hosted in data centers controlled and secured by AWS. You can read more about AWS’s security practices and compliance certifications [here](https://aws.amazon.com/security/). @@ -60,7 +64,7 @@ Pipedream reacts to potential threats quickly based on [our incident response po ## User Accounts, Authentication and Authorization -When you sign up for a Pipedream account, you can choose to link your Pipedream login to either an existing [Google](https://google.com) or [Github](https://github.com) account, or create an account directly with Pipedream. Pipedream also supports [single-sign on](/workspaces/#configuring-single-sign-on-sso). +When you sign up for a Pipedream account, you can choose to link your Pipedream login to either an existing [Google](https://google.com) or [Github](https://github.com) account, or create an account directly with Pipedream. Pipedream also supports [single-sign on](/workflows/workspaces/#configuring-single-sign-on-sso). When you link your Pipedream login to an existing identity provider, Pipedream does not store any passwords tied to your user account — that information is secured with the identity provider. We recommend you configure two-factor authentication in the provider to further protect access to your Pipedream account. @@ -78,17 +82,72 @@ Pipedream encrypts all OAuth grants, key-based credentials, and environment vari When you link credentials to a specific source or workflow, the credentials are loaded into that program's [execution environment](#execution-environment), which runs in its own virtual machine, with access to RAM and disk isolated from other users' code. -No credentials are logged in your source or workflow by default. If you log their values or [export data from a step](/workflows/steps/#step-exports), you can always delete the data for that execution from your source or workflow. These logs will also be deleted automatically based on the [event retention](https://pipedream.com/docs/limits/#event-execution-history) for your account. +No credentials are logged in your source or workflow by default. If you log their values or [export data from a step](/workflows/#step-exports), you can always delete the data for that execution from your source or workflow. These logs will also be deleted automatically based on the [event retention](/workflows/limits#event-history) for your account. You can delete your OAuth grants or key-based credentials at any time by visiting [https://pipedream.com/accounts](https://pipedream.com/accounts). Deleting OAuth grants within Pipedream **do not** revoke Pipedream's access to your account. You must revoke that access wherever you manage OAuth grants in your third party application. +## Pipedream REST API security, OAuth clients + +The Pipedream API supports two methods of authentication: [OAuth](/rest-api/auth/#oauth) and [User API keys](/rest-api/auth/#user-api-keys). **We recommend using OAuth clients** for a few reasons: + +✅ OAuth clients are tied to the workspace, administered by workspace admins
+✅ Tokens are short-lived
+✅ OAuth clients support scopes, limiting access to specific operations
+ +When testing the API or using the CLI, you can use your user API key. This key is tied to your user account and provides full access to any resources your user has access to, across workspaces. + +### OAuth clients + +Pipedream supports client credentials OAuth clients, which exchange a client ID and client secret for a short-lived access token. These clients are not tied to individual end users, and are meant to be used server-side. You must store these credentials securely on your server, never allowing them to be exposed in client-side code. + +Client secrets are salted and hashed before being saved to the database. The hashed secret is encrypted at rest. Pipedream does not store the client secret in plaintext. + +You can revoke a specific client secret at any time by visiting [https://pipedream.com/settings/api](https://pipedream.com/settings/api). + +### OAuth tokens + +Since Pipedream uses client credentials grants, access tokens must not be shared with end users or stored anywhere outside of your server environment. + +Access tokens are issued as JWTs, signed with an ED25519 private key. The public key used to verify these tokens is available at https://api.pipedream.com/.well-known/jwks.json. See [this workflow template](https://pipedream.com/new?h=tch_rBf76M) for example code you can use to validate these tokens. + +Access tokens are hashed before being saved in the Pipedream database, and are encrypted at rest. + +Access tokens expire after 1 hour. Tokens can be revoked at any time. + +## Pipedream Connect + +[Pipedream Connect](/connect/) is the easiest way for your users to connect to [over {process.env.PUBLIC_APPS}+ APIs](https://pipedream.com/apps), **right in your product**. + +### Client-side SDK + +Pipedream provides a [client-side SDK](/connect/api/#typescript-sdk-browser) to initiate authorization or accept API keys on behalf of your users in environments that can run JavaScript. You can see the code for that SDK [here](https://github.com/PipedreamHQ/pipedream/tree/master/packages/sdk). + +When you initiate authorization, you must: + +1. [Create a server-side token for a specific end user](/connect/api/#create-a-new-token) +2. Initiate auth with that token, connecting an account for a specific user + +These tokens can only initiate the auth connection flow. They have no permissions to access credentials or perform other operations against the REST API. They are meant to be scoped to a specific user, for use in clients that need to initiate auth flows. + +Tokens expire after 4 hours, at which point you must create a new token for that specific user. + +### Connect Link + +You can also use [Connect Link](/connect/connect-link/) to generate a URL that initiates the authorization flow for a specific user. This is useful when you want to initiate the auth flow from a client-side environment that can't run JavaScript, or include the link in an email, chat message, etc. + +Like tokens, Connect Links are coupled to specific users, and expire after 4 hours. + +### REST API + +The Pipedream Connect API is a subset of the [Pipedream REST API](/rest-api/). See the [REST API Security](#pipedream-rest-api-security-oauth-clients) section for more information on how we secure the API. + ## Execution environment The **execution environment** refers to the environment in which your sources, workflows, and other Pipedream code is executed. Each version of a source or workflow is deployed to its own virtual machine in AWS. This means your execution environment has its own RAM and disk, isolated from other users' environments. You can read more about the details of the virtualization and isolation mechanisms used to secure your execution environment [here](https://firecracker-microvm.github.io/). -Instances of running VMs are called **workers**. If Pipedream spins up three VMs to handle multiple, concurrent requests for a single workflow, we're running three **workers**. Each worker runs the same Pipedream execution environment. Workers are ephemeral — AWS will shut them down within ~5 minutes of inactivity — but you can configure [dedicated workers](/workflows/settings/#eliminate-cold-starts) to ensure workers are always available to handle incoming requests. +Instances of running VMs are called **workers**. If Pipedream spins up three VMs to handle multiple, concurrent requests for a single workflow, we're running three **workers**. Each worker runs the same Pipedream execution environment. Workers are ephemeral — AWS will shut them down within ~5 minutes of inactivity — but you can configure [dedicated workers](/workflows/building-workflows/settings/#eliminate-cold-starts) to ensure workers are always available to handle incoming requests. ## Controlling egress traffic from Pipedream @@ -162,7 +221,7 @@ Pipedream provides annual security training to all employees. Developers go thro Pipedream retains data only for as long as necessary to provide the core service. Pipedream stores your workflow code, data in data stores, and other data indefinitely, until you choose to delete it. -Event data and the logs associated with workflow executions are stored according to [the retention rules on your account](/limits/#event-execution-history). +Event data and the logs associated with workflow executions are stored according to [the retention rules on your account](/workflows/limits/#event-history). Pipedream deletes most internal application logs and logs tied to subprocessors within 30 days. We retain a subset of logs for longer periods where required for security investigations. diff --git a/docs-v2/pages/projects/_meta.json b/docs-v2/pages/projects/_meta.json deleted file mode 100644 index c99dac293cb6e..0000000000000 --- a/docs-v2/pages/projects/_meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "index": "Projects", - "git": "GitHub Sync", - "file-stores": "File Stores" -} diff --git a/docs-v2/pages/projects/file-stores/_meta.json b/docs-v2/pages/projects/file-stores/_meta.json deleted file mode 100644 index 4c7bd765fe0d3..0000000000000 --- a/docs-v2/pages/projects/file-stores/_meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "index": "File Stores" -} diff --git a/docs-v2/pages/projects/file-stores/index.mdx b/docs-v2/pages/projects/file-stores/index.mdx deleted file mode 100644 index ecf0ffef5fa96..0000000000000 --- a/docs-v2/pages/projects/file-stores/index.mdx +++ /dev/null @@ -1,333 +0,0 @@ -import Callout from '@/components/Callout' - -# File Stores - - -In Preview - -File Stores are available in Preview. There may be changes to allowed limits in the future. - -If you have any feedback on File Stores, please let us know in our [community](https://pipedream.com/support). - - -File Stores are a filesystem that are scoped to a Project. All workflows within the same Project have access to the File Stores. - -You can interact with these files through the Pipedream Dashboard or programmatically through your Project's workflows. - -Unlike files stored within a workflow's `/tmp` directory which are subject to deletion between executions, File Stores are separate cloud storage. Files within a File Store can be long term storage accessible by your workflows. - -![File Stores are scoped to Projects, and workflows within the Project can interact with files stored](https://res.cloudinary.com/pipedreamin/image/upload/v1700156062/docs/docs/Project%20Files/Untitled_7_cn9njj.png) - - - -## Managing File Stores from the Dashboard - -You can access a File Store by opening the Project and selecting the *File Store* on the left hand navigation menu. - -![Opening a project's file store in the Pipedream Dashboard.](https://res.cloudinary.com/pipedreamin/image/upload/v1698934897/docs/docs/Project%20Files/CleanShot_2023-11-02_at_10.21.15_2x_z5q5nt.png) - -### Uploading files to the File Store - -To upload a file, select *New* then select *File*: - -![Opening the new file pop-up in a Project's File Store](https://res.cloudinary.com/pipedreamin/image/upload/v1698934655/docs/docs/Project%20Files/CleanShot_2023-11-02_at_10.16.13_fqjuv4.gif) - -Then in the new pop-up, you can either drag and drop or browser your computer to stage a file for uploading: - -![Choose to either drag and drop a file to upload or browse your local filesystem to upload the file](https://res.cloudinary.com/pipedreamin/image/upload/v1698934655/docs/docs/Project%20Files/CleanShot_2023-11-02_at_10.16.19_2x_w7z8wv.png) - -Now that the file(s) are staged for uploaded. Click *Upload* to upload them: - -![Confirm the upload to the file store](https://res.cloudinary.com/pipedreamin/image/upload/v1698934657/docs/docs/Project%20Files/CleanShot_2023-11-02_at_10.16.49_2x_ha4scn.png) - -Finally, click *Done* to close the upload pop-up: - -![Closing the file store upload modal](https://res.cloudinary.com/pipedreamin/image/upload/v1698934659/docs/docs/Project%20Files/CleanShot_2023-11-02_at_10.17.01_2x_xmfqfi.png) - -You should now see your file is uploaded and available for use within your Project: - -![The file is now uploaded to the File Store](https://res.cloudinary.com/pipedreamin/image/upload/v1698935114/docs/docs/Project%20Files/CleanShot_2023-11-02_at_10.24.56_2x_ogoh5t.png) - -### Deleting files from the File Store - -You can delete individual files from a File Store by clicking the three dot menu on the far right of the file and selecting *Delete*. - -![Deleting a Project File from the Dashboard](https://res.cloudinary.com/pipedreamin/image/upload/v1698855610/docs/docs/Project%20Files/CleanShot_2023-11-01_at_12.19.20_lb4ddt.png) - -After confirming that you want to delete the file, it will be permanently deleted. - - -File deletion is permanent - -Once a file is deleted, it's not possible to recover it. Please take care when deleting files from File Stores. - - -## Managing File Stores from Workflows - -Files uploaded to a File Store are accessible by workflows within that same project. - -You can access these files programmatically using the `$.files` helper within Node.js code steps. - - - -File Stores are scoped to Projects - -Only workflows within the same project as the File Store can access the files. Workflows outside of the project will not be able to access that project's File Store. - - -### Listing files in the File Store - -The `$.files.dir()` method allows you to list files and directories within the Project's File Store. By default it will list the files at the root directory. - -Here's an example of how to iterate over the files in the root directory and open them as `File` instances: - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - // list all contents of the root File Stores directory in this project - const dirs = $.files.dir(); - let files = []; - - for await(const dir of dirs) { - // if this is a file, let's open it - if(dir.isFile()) { - files.push(dir.path) - } - } - - return files - }, -}) -``` - -### Opening files - -To interact with a file uploaded to the File Store, you'll first need to open it. - -Given there's a file in the File Store called `example.png`, you can open it using the `$.files.open()` method: - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - // Open the file by it's path in the File Store - const file = $.files.open('example.png') - // Log the S3 url to access the file publicly - return await file.toUrl() - }, -}) -``` - -Once the file has been opened, you can [read, write, delete the file and more](/projects/file-stores/reference/). - -### Uploading files to File Stores - -You can upload files using Node.js code in your workflows, either from URLs, from the `/tmp` directory in your workflows or directly from streams for high memory efficency. - -#### Uploading files from URLs - -`File.fromUrl()` can upload a file from a public URL to the File Store. - -First open a new file at a specific path in the File Store, and then pass a URL to the `fromUrl` method on that new file: - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - // Upload a file to the File Store by a URL - const file = await $.files.open('pipedream.png').fromUrl('https://res.cloudinary.com/pipedreamin/image/upload/t_logo48x48/v1597038956/docs/HzP2Yhq8_400x400_1_sqhs70.jpg') - - // display the uploaded file's URL from the File Store: - console.log(await file.toUrl()) - }, -}) -``` - -#### Uploading files from the workflow's `/tmp` directory - -`File.fromFile()` can upload a file stored within the workflow's `/tmp` directory to the File Store. - -First open a new file at a specific path in the File Store, and then pass a URL to the `fromFile` method on that new file: - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - // Upload a file to the File Store from the local /tmp/ directory - const file = await $.files.open('recording.mp3').fromFile('/tmp/recording.mp3') - - // Display the URL to the File Store hosted file - console.log(await file.toUrl()) - }, -}) -``` - -#### Uploading files using streams - -File Stores also support streaming to write large files. `File.createWriteStream()` creates a write stream for the file to upload to. Then you can pair this stream with a download stream from another remote location: - -```javascript -import { pipeline } from 'stream/promises'; -import got from 'got' - -export default defineComponent({ - async run({ steps, $ }) { - const writeStream = await $.files.open('logo.png').createWriteStream() - - const readStream = got.stream('https://pdrm.co/logo') - - await pipeline(readStream, writeStream); - }, -}) -``` - -Additionally, you can pass a `ReadableStream` instance directly to a File instance: - -```javascript -import got from 'got' - -export default defineComponent({ - async run({ steps, $ }) { - // Start a new read stream - const readStream = got.stream('https://pdrm.co/logo') - - // Populate the file's content from the read stream - await $.files.open("logo.png").fromReadableStream(readStream) - }, -}) -``` - - -(Recommended) Pass the contentLength if possible - -If possible, pass a `contentLength` argument, then File Store will be able to efficiently stream to use less memory. Without a `contentLength` argument, the entire file will need to be downloaded to `/tmp/` until it can be uploaded to the File store. - - - -### Downloading files - -File Stores live in cloud storage by default, but files can be downloaded to your workflows individually. - -#### Downloading files to the workflow's `/tmp` directory - -First open a new file at a specific path in the File Store, and then call the `toFile()` method to download the file to the given path: - -```javascript -import fs from 'fs'; - -export default defineComponent({ - async run({ steps, $ }) { - // Download a file from the File Store to the local /tmp/ directory - const file = await $.files.open('recording.mp3').toFile('/tmp/README.md') - - // read the file version of the file stored in /tmp - return (await fs.promises.readFile('/tmp/README.md')).toString() - }, -}) -``` - - -Only the `/tmp/` directory is readable and writable - -Make sure that your path to `toFile(path)` includes - - -### Passing files between steps - -Files can be passed between steps. Pipedream will automatically serialize the file as a JSON _description_ of the file. Then when you access the file as a step export as a prop in a Node.js code step, then you can interact with the `File` instance directly. - -For example, if you have a file stored at the path `logo.png` within your File Store, then within a Node.js code step you can open it: - -```javascript -// "open_file" Node.js code step -export default defineComponent({ - async run({ steps, $ }) { - // Return data to use it in future steps - const file = $.files.open('logo.png') - - return file - }, -}) -``` - -Then in a downstream code step, you can use it via the `steps` path: - -```javascript -// "get_file_url" Node.js code step -export default defineComponent({ - async run({ steps, $ }) { - // steps.open_file.$return_value is automatically parsed back into a File instance: - return await steps.open_file.$return_value.toUrl() - }, -}) -``` - - -Files descriptions are compatible with other workflow helpers - -Files can also be used with `$.suspend` and `$.delay`. - - -#### Handling lists of files - -One limitation of the automatic parsing of files between steps is that it currently doesn't automatically handle lists of files between steps. - -For example, if you have a step that returns an array of `File` instances: - -```javascript -// "open_files" Node.js code step -export default defineComponent({ - async run({ steps, $ }) { - // Return data to use it in future steps - const file1 = $.files.open('vue-logo.svg') - const file2 = $.files.open('react-logo.svg') - - return [file1, file] - }, -}) -``` - -Then you'll need to use `$.files.openDescriptor` to parse the JSON definition of the files back into `File` instances: - -```javascript -// "parse_files" Node.js code step -export default defineComponent({ - async run({ steps, $ }) { - const files = steps.open_files.$return_value.map(object => $.files.openDescriptor(object)) - - // log the URL to the first File - console.log(await files[0].toUrl()); - }, -}) -``` - -### Deleting files - -You can call `delete()` on the file to delete it from the File Store. - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - // Open the file and delete it - const file = await $.files.open('example.png').delete() - console.log('File deleted.') - }, -}) -``` - - -Deleting files is irreversible - -It's not possible to restore deleted files. Please take care when deleting files. - - -## Frequently Asked Questions - -### Are there size limits for files within File Stores? - -At this time no, but File Stores are in preview and are subject to change. - -### Are helpers available for Python to download, upload and manage files? - -At this time no, only Node.js includes a helper to interact with the File Store programmatically within workflows. - -### Are File Stores generally available? - -At this time File Stores are only available to Advanced plan and above subscribed workspaces. You can change your plan within the [pricing page](https://pipedream.com/pricing). diff --git a/docs-v2/pages/projects/file-stores/reference.mdx b/docs-v2/pages/projects/file-stores/reference.mdx deleted file mode 100644 index d9a86ab98de9a..0000000000000 --- a/docs-v2/pages/projects/file-stores/reference.mdx +++ /dev/null @@ -1,247 +0,0 @@ -import Callout from '@/components/Callout' - -# File Stores Node.js Reference - -The File Stores Node.js helper allows you to manage files within Code Steps and Action components. - - - -## `$.files` - -The `$.files` helper is the main module to interact with the Project's File Store. It can instatiate new files, open files from descriptors and list the contents of the File Store. - -### `$.files.open(path)` - -*Sync.* Opens a file from the relative `path`. If the file doesn't exist, a new empty file is created. - -### `$.files.openDescriptor(fileDescriptor)` - -*Sync.* Creates a new `File` from the JSON friendly description of a file. Useful for recreating a `File` from a step export. - -For example, export a `File` as a step export which will render the `File` as JSON: - -```javascript -// create_file -// Creates a new Project File and uploads an image to it -export default defineComponent({ - async run({ steps, $ }) { - // create the new file and upload the contents to it from a URL - const file = await $.files.open("imgur.png").fromUrl("https://i.imgur.com/TVIPgNq.png") - // return the file as a step export - return file - }, -} -``` - -Then in a downstream step recreate the `File` instance from the step export friendly _description_: - -```javascript -// download_file -// Opens a file downloaded from a previous step, and saves it. -export default defineComponent({ - async run({ steps, $ }) { - // Convert the the description of the file back into a File instance - const file = $.files.openDescriptor(steps.create_file.$return_value) - // Download the file to the local /tmp directory - await $.file.download('/tmp/example.png') - console.log("File downloaded to /tmp") - }, -}) - -``` - -### `$.files.dir(?path)` - -*Sync.* Lists the files & directories at the given `path`. By default it will list the files at the root directory. - -Here's an example of how to iterate over the files in the root directory and open them as `File` instances: - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - // list all contents of the root File Stores directory in this project - const dirs = $.files.dir(); - let files = []; - - for await(const dir of dirs) { - // if this is a file, let's open it - if(dir.isFile()) { - files.push(await $.files.open(dir.path)) - } - } - - return files - }, -}) -``` - -Each iteratee of `$.files.dir()` will contain the following properties: - -* `isDirectory()` - `true` if this instance is a directory. -* `isFile()` - `true` if this instance is a file. -* `path` - The path to the file. -* `size` - The size of the file in bytes. -* `modifiedAt` - The last modified at timestamp. - -## `File` - -This class describes an instance of a single file within a File Store. - -When using `$.files.open` or `$.files.openDescriptor`, you'll create a new instance of a `File` with helper methods that give you more flexibility to perform programatic actions with the file. - -### `File.toUrl()` - -*Async.* The pre-signed GET URL to retrieve the file. - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - // Retrieve the pre-signed GET URL for logo.png - const url = await $.files.open('logo.png').toUrl() - - return url - }, -}) - -``` - - -Pre-signed GET URLs are short lived. - -The `File.toUrl()` will expire after 30 minutes. - - -### `File.toFile(path)` - -*Async.* Downloads the file to the local path in the current workflow. If the file doesn't exist, a new one will be created at the path specified. - - -Only `/tmp` is writable in workflow environments - -Only the `/tmp` directory is writable in your workflow's exection environment. So you must download your file to the `/tmp` directory. - - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - // Download the file in the File Store to the workflow's /tmp/ directory - await $.files.open('logo.png').toFile("/tmp/logo.png") - }, -}) - -``` - -### `File.toBuffer()` - -*Async.* Downloads the file as a Buffer to create readable or writeable streams. - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - // opens a file at the path "hello.txt" and downloads it as a Buffer - const buffer = await $.files.open('hello.txt').toBuffer() - // Logs the contents of the Buffer as a string - console.log(buffer.toString()) - }, -}) -``` - -### `File.fromFile(localFilePath, ?contentType)` - -*Async.* Uploads a file from the file at the `/tmp` local path. For example, if `localFilePath` is given `/tmp/recording.mp3`, it will upload that file to the current File Store File instance. - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - // Upload a file to the File Store from the local /tmp/ directory - const file = await $.files.open('recording.mp3').fromFile('/tmp/recording.mp3') - - console.log(file.url) - }, -}) -``` - -### `File.fromUrl(url)` - -*Async.* Accepts a `url` to read from. - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - // Upload a file to the File Store by a URL - const file = await $.files.open('pipedream.png').fromUrl('https://res.cloudinary.com/pipedreamin/image/upload/t_logo48x48/v1597038956/docs/HzP2Yhq8_400x400_1_sqhs70.jpg') - - console.log(file.url) - }, -}) -``` - -### `File.createWriteStream(?contentType, ?contentLength)` - -*Async.* Creates a write stream to populate the file with. - - -Pass the content length if possible - -The `contentLength` argument is optional, however we do recommend passing it. Otherwise the entire file will need to be written to the local `/tmp` before it can be uploaded to the File store. - - -```javascript -import { pipeline } from 'stream/promises'; -import got from 'got' - -export default defineComponent({ - async run({ steps, $ }) { - const writeStream = await $.files.open('logo.png').createWriteStream("image/png", 2153) - - const readStream = got.stream('https://pdrm.co/logo') - - await pipeline(readStream, writeStream); - }, -}) -``` - -### `File.fromReadableStream(?contentType, ?contentLength)` - -*Async.* Populates a file's contents from the `ReadableStream`. - - -Pass the content length if possible - -The `contentLength` argument is optional, however we do recommend passing it. Otherwise the entire file will need to be written to the local `/tmp` before it can be uploaded to the File store. - - -```javascript -import got from 'got' - -export default defineComponent({ - async run({ steps, $ }) { - // Start a new read stream - const readStream = got.stream('https://pdrm.co/logo') - - // Populate the file's content from the read stream - await $.files.open("logo.png").fromReadableStream(readStream, "image/png", 2153) - }, -}) -``` - -### `File.delete()` - -*Async.* Deletes the Project File. - -```javascript -export default defineComponent({ - async run({ steps, $ }) { - // Open the Project File and delete it - const file = await $.files.open('example.png').delete() - - console.log('File deleted.') - }, -}) -``` - - -Deleting files is irreversible - -It's not possible to restore deleted files. Please take care when deleting files. - diff --git a/docs-v2/pages/projects/git.mdx b/docs-v2/pages/projects/git.mdx deleted file mode 100644 index bc9dd6411787f..0000000000000 --- a/docs-v2/pages/projects/git.mdx +++ /dev/null @@ -1,222 +0,0 @@ -import Callout from '@/components/Callout' - -# Github Sync - -When Github Syncing is enabled on your project, Pipedream will serialize your workflows and synchronize changes to a GitHub repo. - -Capabilities include: - -- Bi-directional GitHub sync (push and pull changes) -- Edit in development branches -- Track commit and merge history -- Link users to commits -- Merge from Pipedream or create PRs and merge from GitHub -- Edit in Pipedream or use a local editor and synchronize via GitHub (e.g., edit code, find and replace across multiple steps or workflows) -- Organize workflows into projects with support for nested folders - - -Follow our [quickstart guide](/quickstart/github-sync/) to start building projects on Pipedream using GitHub Sync. - - -## Getting Started - -### Create a new project and enable GitHub Sync - -A project may contain one or more workflows and may be further organized using nested folders. Each project may be synchronized to a single GitHub repo. - -- Go to `https://pipedream.com/projects` -- Create a new project -- Enter a project name and check the box to **Configure GitHub Sync** - - To use **OAuth** - - Select a connected account, GitHub scope and repo name - - Pipedream will automatically create a new, empty repo in GitHub - - ![Enabling Github on a Pipedream project](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F6b546c6b-2f90-4ec4-9188-320c01576259%2FUntitled.png?id=5db64a5f-e762-431e-bfb7-cdd495ad458c&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=860&userId=&cache=v2) - - To use **Deploy Keys** - - Create a new repo in GitHub - - Follow the instructions to configure the deploy key - - Test your setup and create a new project - - ![Enabling Github sync with a Deploy Key](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F6be64329-bb8b-43eb-a278-a11ad93113c0%2FUntitled.png?id=37f9afd8-ba14-431b-bd40-1846421440b6&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=860&userId=&cache=v2) - -### Create a branch to edit a project - - -Branches are required to make changes - -All changes to resources in a project must be made in a development branch. - -Examples of changes include creating, editing, deleting, enabling, disabling and renaming workflows. This also includes changing workflow settings like concurrency, VPC assignment and auto-retries. - - -To edit a git-backed project you must create a development branch by clicking **Edit > Create Branch** - -![Creating a new git backed development branch in a workflow](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fcf1e386b-7674-4843-8709-f1d5eef8ef00%2FUntitled.png?id=3af32b86-6ca2-4051-98cc-de31940eb609&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) - -Next, name the branch and click **Create**: - -![Enter your desired branch name, and click create to create the branch](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F5d404d86-3f35-4db8-a2e8-e8d7bdb3e2c0%2FUntitled.png?id=33ebf62f-cde3-43fb-a76b-5c869338226f&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) - -To exit development mode without merging to production, click **Exit Development Mode**: - -![Exiting the branch without committing the changes](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F7e72331c-54b8-453a-ae47-36f4b2355fac%2FUntitled.png?id=a55ea908-d904-4218-bfd3-72e568fee6ea&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) - -Your changes will be saved to the branch, if you choose to revisit them later. - -### Merge changes to production - -Once you've committed your changes, you can deploy your changes by merging them into the `production` branch through the Pipedream UI or GitHub. - -When you merge a Git-backed project to production, all modified resources in the project will be deployed. Multiple workflows may be deployed, modified, or deleted in production through a single merge action. - -#### Merge via the Pipedream UI - -To merge changes to production, click on **Merge to production:** - -![Click on the Merge to production button in the top right of the UI to merge the changes](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F7ffe8211-90de-4512-824a-f3cc1b5a9382%2FUntitled.png?id=0043c13f-ade3-4e0e-b9ae-a98b9f293885&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) - -Pipedream will present a diff between the development branch and the `production`. Validate your changes and click **Merge to production** to complete the merge: - -![In the confirmation modal, click Merge to production to confirm the changes](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fac27c067-f948-424c-9d6e-6360a759730c%2FUntitled.png?id=19f8d51c-82fb-47d6-a183-b84e2173b72d&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) - -#### Create a Pull Request in Github - -To create a pull request in GitHub, either choose Open GitHub pull request from the git-actions menu in Pipedream or in GitHub: - -Opening a PR request in Pipedream - -You can also review and merge changes directly from GitHub using the standard pull request process. - - -Pull request reviews cannot be required - -PR reviews cannot be required. That feature is on the roadmap for the Business tier. - - -### Commit changes - -To commit changes without merging to production, select **Commit Changes** from the Git Actions menu: - -![Select commit changes from the dropdown menu to make a commit](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F2523c3fb-d832-4e99-b5cc-5c3b7275c9fe%2FUntitled.png?id=9f871c2d-12f4-4484-a76f-b0d83c4d8ee9&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) - -You can review the diff and enter a commit message: - -![Preview your changes and approve them with a commit message](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F01f0b4f8-36d6-43a8-8568-99183a7a1d4c%2FUntitled.png?id=77f5b85a-c14d-47aa-8cbe-2e3d5ea64786&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) - -### Pull changes and resolve conflicts - -If remote changes are detected, you'll be prompted to pull the changes: - -![Click the Pull `` to pull in the latest changes to your current branch](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fac13c163-7816-4e67-bb4a-ccbfb97471c4%2FUntitled.png?id=434ad559-bf3c-4e9c-96a3-c784122793a9&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) - -Pipedream will attempt to automatically merge changes. If there are conflicts, you will be prompted to manually resolve it: - -![Example of a commit that requires manual resolution to continue with the commit](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F7849edd0-ee5f-47e9-9a23-b245435faf2b%2FUntitled.png?id=35308854-6572-4575-8fef-6531986fdb7f&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) - -### Move existing workflows to projects - - -Not available for v1 workflows - -Legacy (v1) workflows are not supported in projects. [Follow this guide to migrate your v1 workflows to v2 workflows](/migrate-from-v1/). - - -First, select the workflow(s) you want to move from the [workflows listing page](https://pipedream.com/workflows) and click **Move** in the top action menu: - -![Select your workflows you'd like to transfer to a project, then click the Move button in the top right](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F70c4da87-2aa3-435d-9226-c29fcc1cd881%2FUntitled.png?id=10fbba0c-2a92-49da-b7f1-22b1b46fb96c&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) - -Then, select the project to move the selected workflows to: - -![Select which project to move the selected workflows into in the dropdown in the top right of the screen](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F5fcb3357-9957-4307-aac9-e28ed59f85b0%2FUntitled.png?id=7fca27aa-28ec-4bcc-940d-9b66d1d692be&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) - - -Undeployed changes are automatically assigned a development branch - -If any moved workflows have undeployed changes, those changes will staged in a branch prefixed with `undeployed-changes` (e.g., `undeployed-changes-27361`). - - -### Use the changelog - -The changelog tracks all git activity (for projects with GitHub sync enabled). If you encounter an error merging your project, go to the changelog and explore the log details to help you troubleshoot issues in your workflows: - -![Opening the changelog of commits on the left hand menu](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fe7043a8c-597c-4722-86f6-464f582faf6f%2FUntitled.png?id=6e2f00f1-c768-4d13-9800-d6361afbe26d&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) - -### Local development - -Projects that use GitHub sync may be edited outside of Pipedream. You can edit and commit directly via GitHub’s UI or clone the repo locally and use your preferred editor (e.g., VSCode). - -To test external edits in Pipedream: - -1. Commit and push local changes to your development branch in GitHub -2. Open the project in Pipedream's UI and load your development branch -3. Use the Git Actions menu to pull changes from GitHub - -## Known Issues - -Below are a list of known issues that do not currently have solutions, but are in progress: - -- Project branches on Pipedream cannot be deleted. -- If a workflow uses an action that has been deprecated, merging to production will fail. -- Legacy (v1) workflows are not supported in projects. -- Self-hosted GitHub Server instances are not yet supported. [Please contact us for help](https://pipedream.com/support). -- Workflow attachments are not supported - -## Github Enterprise Cloud - -If your repository hosted on an Github Enterprise account, you can allow the static Pipedream IP address to sync your project changes. - -[Follow the directions here](https://docs.github.com/en/enterprise-cloud@latest/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/managing-allowed-ip-addresses-for-your-organization) to add an IP address. - -Then add this static IP address `3.214.142.179` to allow Pipedream to sync changes. - - -Github Sync is available on Business and above plans - -To use this public IP address and connect to Github Enterprise Cloud hosted repositories, you'll need to have a Pipedream Business plan. [View our plans](https://pipedream.com/pricing). - - -## Frequently Asked Questions - -### How are Pipedream workflows synchronized to Github? - -Pipedream will serialize your project's workflows and their configuration into a standard YAML format for storage in Github. - -Then Pipedream will commit your changes to your connected Github account. - -### Do you have a definition of this YAML? - -Not yet, please stay tuned! - -### Can I sync multiple workflows to a single Github Repository? - -Yes, _projects_ are synced to a single Github Repository which allows you to store multiple workflows into a single Github Repository for easier organization and management. - -### Can I use this feature to develop workflows locally? - -Yes, you can use the Github Syncing feature to develop your workflows from YAML files checked into your Pipedream connected Github Repository. - -Then pushing changes to the `production` branch will trigger a deploy for your Pipedream workflows. - -### Why am I seeing an error about "private auth mismatch" when trying to merge a branch to production? -![Private Auth Mismatch](https://res.cloudinary.com/pipedreamin/image/upload/v1704258143/private_auth_mismatch_kzdd7e.png) - -This error occurs when **both** of the below conditions are met: -1. The referenced workflow is using a connected account that's not shared with the entire workspace -2. The change was merged from outside the Pipedream UI (via github.com or locally) - -Since Pipedream can't verify the person who merged that change should have access to use the connected account in a workflow in this case, we block these deploys. - -To resolve this error: -1. Make sure all the connected accounts in the project's workflows are [accessible to the entire workspace](/connected-accounts/#access-control) -2. Re-trigger a sync with Pipedream by making a nominal change to the workflow **from outside the Pipedream UI** (via github.com or locally), then merge that change to production - -### Can I sync an existing GitHub Repository with workflows to a new Pipedream Project? - -No, at this time it's not possible because of how resources are connected during the bootstrapping process from the workflow YAML specification. -However, this is on our roadmap, [please subscribe to this issue](https://github.com/PipedreamHQ/pipedream/issues/9255) for the latest details. - -### How does the `production` branch work? - -Anything merged to the `production` branch will be deployed to your production workflows on Pipedream. - -From a design perspective, we want to let you manage any branching strategy on your end, since you may be making commits to the repo outside of Pipedream. Once we support managing Pipedream workflows in a monorepo, where you may have other changes, we wanted to use a branch that didn't conflict with a conventional main branch (like `main` or `master`). - -In the future, we also plan to support you changing the default branch name. diff --git a/docs-v2/pages/projects/index.mdx b/docs-v2/pages/projects/index.mdx deleted file mode 100644 index 5fa050b609ec0..0000000000000 --- a/docs-v2/pages/projects/index.mdx +++ /dev/null @@ -1,172 +0,0 @@ -import Callout from "@/components/Callout"; - -# Projects - -A workspace can contain one or more _projects_. Projects are a way to organize your workflows into specific groupings or categories. - -
-
- How workspaces are organized -
- -## Getting started with projects - -### Creating projects - -To create a new project, first [open the Projects section in the dashboard](https://pipedream.com/projects). - -Then click **Create project** to start a new project. - -Enter in your desired name for the project in the prompt, then click **Create**. - -That's it, you now have a dedicated new project created within your workspace. Now you can create workflows within this project, or move workflows into it or create folders for further organization. - -### Creating folders and workflows in projects - -Within a given project, you can create folders for your workflows. - -Open your project, and then click the **New** button for a dropdown to create a workflow in your current project. - - -Helpful hotkeys to speed up your development - -- `C then F` creates a new folder. -- `C then W` creates a new workflow. - - - -Folders can also contain sub-folders, which allows you to create a filing system to organize your workflows. - -### Moving workflows into folders - -To move workflows into folders, simply drag and drop the workflow into the folder. - -You can move workflows or folders up a level by dragging and dropping the workflow to the folder icon at the top of the list. - -### Importing workflows into projects - - - This only applies to Pipedream accounts that created workflows before the - projects feature was released. - - -To import a workflow from the general **Workflows** area of your dashboard into a project, first open up the specific project you'd like to import the workflow into. - -Then at the top right of the project's page, click the **Import** button. - -You'll be prompted to select a workflow from your **Workflows** area to import. - -### Moving workflows between projects - -To move a workflow from one project to another project, first check the workflow and then click **Move** to open a dropdown of projects. Select the project to move this workflow to, and click **Move** once more to complete the move. - -![How to move workflows from one project to another in the Pipedream dashboard.](https://res.cloudinary.com/pipedreamin/image/upload/v1695662665/docs/docs/projects/CleanShot_2023-09-25_at_13.23.38_2x_dyrtlv.png) - - -Github Sync limitation - -At this time it's not possible to move workflows out of GitHub Synchronized Projects. - - - -## Access Controls - -The [projects list view](https://pipedream.com/projects) contains **Owner** and **Access** columns. - -**Owner** indicates who within the workspace owns each project. This is typically the person who created the project. - -![Project Listing (Owner)](./images/project-listing-owner.png) - - - Projects created before February 2024 don't automatically have owners, which - has no functional impact. - - -**Access** indicates which workspace members have access to each project, and this can be displayed as "me", "Workspace", or "N members". - -![Project Listing (Access)](./images/project-listing-access.png) - -### Permissions - -Workspace owners and admins are able to perform all actions in projects, whereas workspace members are restricted from performing certain actions in projects. - -| Operation | Project creator | Workspace members | -| ------------------------------------------------------------ | :-------------: | :---------------: | -| View in [projects listing](https://pipedream.com/projects) | ✅ | ✅ | -| View in [Event History](https://pipedream.com/event-history) | ✅ | ✅ | -| View in global search | ✅ | ✅ | -| Manage project workflows | ✅ | ✅ | -| Manage project files | ✅ | ✅ | -| Manage project variables | ✅ | ✅ | -| Manage member access | ✅ | ❌ | -| Manage GitHub Sync settings | ✅ | ❌ | -| Delete project | ✅ | ❌ | - - - **Workspace admins and owners have the same permissions as project creators - for all projects in the workspace.** - - -### Managing access - - - By default, all projects are accessible to all workspace members. Workspaces - on the [Business plan](https://pipedream.com/pricing) can restrict access for - individual projects to specific workspace members. - - -You can easily modify the access rules for a project directly from the [project list view](https://pipedream.com/projects), either by clicking the access badge in the project row (fig 1) or clicking the 3 dots to open the action menu, then selecting **Manage Access** (fig 2). - -Via the access badge (fig 1): - -![Click the access badge to manage access](./images/access-badge-click.png) - -Via the action menu (fig 2): - -![Click manage access from the action menu](./images/manage-access-overflow-menu.png) - -From here, a slideout drawer reveals the access management configuration: - -![Manage access slideout workspace access](./images/slideout-workspace-share.png) - -Toggle the **Restrict access to this project** switch to manage access: - -![Manage access slideout restricted](./images/slideout-restricted.png) - -Select specific members of the workspace to grant access: - -![Manage access slideout showing member dropdown](./images/slideout-member-dropdown.png) - -You can always see who has access and remove access if necessary: - -![Manage access showing members with access](./images/slideout-member-list.png) - -## Project variables and secrets - -Environment variables defined at the global workspace level are accessible to all workspace members and workflows within the workspace. To restrict access to sensitive variables or secrets, define them at the project-level and [configure access controls for the project](/projects/#managing-access). - -[See here](/environment-variables) for info on creating, managing, and using environment variables and secrets. - - - **Project variables override workspace variables**. When the same variable is - defined at both the workspace and project levels (for example, - `process.env.BASE_DOMAIN`), the **project** variable takes precedence. - - -## Frequently Asked Questions - -### Can sources and connected accounts be organized into projects as well? - -At this time no, but that is on our roadmap. Projects will eventually contain all the resources needed for a complete system of workflows to work together. - -### Can projects be synchronized to a Github repository? - -Yes, [please read here](/projects/git/) for more information on the Github synchronization feature for projects. - -### Can I configure which members in my workspace have access to individual projects? - -Yes! Learn about managing access [here](/projects/#managing-access). diff --git a/docs-v2/pages/quickstart.mdx b/docs-v2/pages/quickstart.mdx new file mode 100644 index 0000000000000..9e819f669a172 --- /dev/null +++ b/docs-v2/pages/quickstart.mdx @@ -0,0 +1,253 @@ +import Callout from '@/components/Callout' +import { Steps } from 'nextra/components' + +# Workflow Development + +Sign up for a [free Pipedream account](https://pipedream.com/auth/signup) (no credit card required) and complete this quickstart guide to learn the basic patterns for workflow development: + + +### Create a project + +Workflows must be created in **Projects**. Projects make it easy to organize your workflows and collaborate with your team. + +Go to [https://pipedream.com/projects](https://pipedream.com/projects) and click on **Create Project**. + +![create project](/images/quickstart/create-project.png) + +Next, enter a project name and click **Create Project**. For this example, we'll name our project **Getting Started**. You may also click the icon to the right to generate a random project name. + +![configure project](/images/quickstart/configure-project.png) + + +[Configure GitHub Sync](/workflows/git/) for projects to enable git-based version control and unlock the ability to develop in branches, commit to or pull changes from GitHub, view diffs, create PRs and more. + + +### Create a workflow + +After the project is created, use the **New** button to create a new workflow. + +![Create a new workflow](/images/quickstart/new-workflow.png) + +Name the workflow and click **Create Workflow** to use the default settings. For this example, we'll name the workflow **Pipedream Quickstart**. + +![Configure new workflow](/images/quickstart/configure-workflow.png) + +### Add an HTTP / Webhook trigger + +Next, Pipedream will launch the workflow builder and prompt you to add a trigger. + +![Create a new Trigger for the workflow](/images/quickstart/add-trigger.png) + +Clicking the trigger opens a new menu to select the trigger. For this example, select **New HTTP / Webhook Requests**. + +![Add HTTP trigger](/images/quickstart/create-http-trigger.png) + +Click **Save and continue** in the step editor on the right to accept the default settings. + +![Save trigger](/images/quickstart/save-http-trigger.png) + +Pipedream will generate a unique URL to trigger this workflow. Once your workflow is deployed, your workflow will run on every request to this URL. + +![Unique URL](/images/quickstart/unique-url.png) + +### Generate a test event + +Next, generate a test event to help you build the workflow. + + +The test event will be used to provide autocomplete suggestion as you build your workflow. The data will also be used when testing later steps. You may generate or select a different test event at any time when building a workflow. + + +For this example, let's use the following test event data: + +```json +{ + "message": "Pipedream is awesome!" +} +``` + +Pipedream makes it easy to generate test events for your HTTP trigger. Click on **Generate Test Event** to open the HTTP request builder. Copy and paste the JSON data above into the **Raw Request Body** field and click **Send HTTP Request**. + +![Generate Test Event](/images/quickstart/generate-test-event.png) + +Pipedream will automatically select and display the contents of the selected event. Validate that the `message` was received as part the event `body`. + +![Inspect trigger event](/images/quickstart/inspect-trigger-event.png) + + + +You may also send live data to the unique URL for your workflow using your favorite HTTP tool or by running a `cURL` command, e.g., + +```bash +curl -d '{"message": "Pipedream is awesome!"}' \ + -H "Content-Type: application/json" \ + YOUR_ENDPOINT_URL +``` + + +### Enrich trigger data using Node.js and npm + +Before we send data to Google Sheets, let's use the npm [`sentiment`](https://www.npmjs.com/package/sentiment) package to generate a sentiment score for our message. To do that, click **Continue** or the **+** button. + +![Continue from trigger](/images/quickstart/trigger-continue.png) + +That will open the **Add a step** menu. Select **Run custom code**. + +![Add step](/images/quickstart/step-selector-code.png) + +Pipedream will add a Node.js code step to the workflow. + +![The new code step is added to the workflow after the HTTP trigger](/images/quickstart/new-code-step-added.png) + + +Rename the step to **sentiment**. + +![Rename step to sentiment](/images/quickstart/rename-code-step.gif) + +Next, add the following code to the code step: + +```javascript +import Sentiment from "sentiment" + +export default defineComponent({ + async run({ steps, $ }) { + let sentiment = new Sentiment() + return sentiment.analyze(steps.trigger.event.body.message) + }, +}) +``` + +This code imports the npm package, passes the message we sent to our trigger to the `analyze()` function by referencing `steps.trigger.event.body.message` and then returns the result. + + +To use any npm package on Pipedream, just `import` it. There's no `npm install` or `package.json` required. + + + +Any data you `return` from a step is exported so it can be inspected and referenced it in future steps via the `steps` object. In this example, return values will be exported to `steps.sentiment.$return_value` because we renamed the step to **sentiment** . + + +Your code step should now look like the screenshot below. To run the step and test the code, click the **Test** button. + +![Test code step](/images/quickstart/test-code-step.png) + +You should see the results of the sentiment analysis when the test is complete. + +![Test results](/images/quickstart/sentiment-results.png) + + + +When you **Test** a step, only the current step is executed. Use the caret to test different ranges of steps including the entire workflow. + + +### Save data to Google Sheets + +Next, create a Google Sheet and add **Timestamp**, **Message** and **Sentiment Score** to the first row. These labels act as our column headers amd will help us configure the Google Sheets step of the workflow. + +![Empty Sheet](/images/quickstart/empty-sheet.png) + +Next, let's add a step to the workflow to send the data to Google Sheets. First, click **+** after the `sentiment` code step and select the **Google Sheets** app. + +![Step Selector - Google Sheets](/images/quickstart/select-google-sheets-app.png) + +Then select the **Add Single Row** action. + +![Select Add Single Row](/images/quickstart/select-add-single-row.png) + +Click to connect you Google Sheets account to Pipedream (or select it from the dropdown if you previously connected an account). + +![Connect Google Sheets](/images/quickstart/connect-google-sheets-account.png) + +Pipedream will open Google's sign in flow in a new window. Sign in with the account you want to connect. + +![Google OAuth Window](/images/quickstart/google-oauth.png) + + +If prompted, you must check the box for Pipedream to **See, edit, create and delete all of your Google Drive files**. These permissions are required for configure and use the pre-built actions for Google Sheets. + + +![Google Permissions](/images/quickstart/google-permissions.png) + +Learn more about Pipedream's [privacy and security policy](/privacy-and-security/). + +When you complete connecting your Google account, the window should close and you should return to Pipedream. Your connected account should automatically be selected. Next, select your spreadsheet from the dropdown menu: + +![Select Spreadsheet](/images/quickstart/v3/select-spreadsheet.png) + +Then select the sheet name (the default sheet name in Google Sheets is **Sheet1**): + +![Select Sheet Name](/images/quickstart/v3/select-spreadsheet-name.png) + +Next, select if the spreadsheet has headers in the first row. When a header row exists, Pipedream will automatically retrieve the header labels to make it easy to enter data (if not, you can manually construct an array of values). Since the sheet for this example contains headers, select **Yes**. + +![Has headers](/images/quickstart/v3/select-headers.png) + +Pipedream will retrieve the headers and generate a form to enter data in your sheet: + +![Additional props](/images/quickstart/v3/async-props.png) + +First, let's use the object explorer to pass the timestamp for the workflow event as the value for the first column. This data can be found in the context object on the trigger. + +When you click into the **Timestamp** field, Pipedream will display an object explorer to make it easy to find data. Scroll or search to find the `ts` key under `steps.trigger.context`. + +![Add reference to timestamp](/images/quickstart/v3/select-ts-export.png) + +Click **select path** to insert a reference to steps.trigger.context.ts: + +![Timestamp added](/images/quickstart/v3/timestamp.png) + +Next, let's use autocomplete to enter a value for the **Message** column. First, add double braces `{{` — Pipedream will automatically add the closing braces `}}`. + +Then, type `steps.trigger.event.body.message` between the pairs of braces. Pipedream will provide autocomplete suggestions as you type. Press **Tab** to use a suggestion and then click `.` to get suggestions for the next key. The final value in the **Message** field should be `steps.trigger.event.body.message`. + +![Add reference to message](/images/quickstart/v3/autocomplete-export.png) + +Finally, let's copy a reference from a previous step. Click on the `sentiment` step to open the results in the editor: + +![Expand sentiment results](/images/quickstart/v3/select-sentiment-step.png) + +Next, click the **Copy Path** link next to the score. + +![Copy path to sentiment score](/images/quickstart/v3/copy-sentiment-path.png) + +Click the Google Steps step or click the open tab in the editor. Then paste the value into the **Sentiment Score** field — Pipedream will automatically wrap the reference in double braces `{{ }}`. + +![Paste reference to sentiment score](/images/quickstart/v3/paste-sentiment-score.png) + +Now that the configuration is complete, click **Test** to validate the configuration for this step. When the test is complete, you will see a success message and a summary of the action performed: + +![Action results](/images/quickstart/v3/successful-test-results.png) + +If you load your spreadsheet, you should see the data Pipedream inserted. + +![Data inserted into sheets](/images/quickstart/data-inserted.png) + +Next, return to your workflow and click **Deploy** to run your workflow on every trigger event. + +![Deploy Workflow](/images/quickstart/v3/deploy-workflow.png) + +When your workflow deploys, you will be redirected to the **Inspector**. Your workflow is now live. + +![Deployed workflow](/images/quickstart/deployed-workflow.png) + +To validate your workflow is working as expected, send a new request to your workflow: You can edit and run the following `cURL` command: + +```bash +curl -d '{ "message": "Pipedream is awesome!" }' \ + -H "Content-Type: application/json" \ + YOUR-TRIGGER-URL +``` + +The event will instantly appear in the event list. Select it to inspect the workflow execution. + +![Inspect executions](/images/quickstart/inspect-executions.png) + +Finally, you can return to Google Sheets to validate that the new data was automatically inserted. + +![Live data inserted into sheets](/images/quickstart/live-test.png) + + + +## Next Steps + +Congratulations! You completed the quickstart and should now understand the basic patterns for workflow development. Next, try creating your own [workflows](/workflows/building-workflows/), learn how to [build and run workflows for your users](/connect/workflows/) or check out the rest of the [docs](/)! diff --git a/docs-v2/pages/quickstart/_meta.json b/docs-v2/pages/quickstart/_meta.json deleted file mode 100644 index 7f59ea18f74fd..0000000000000 --- a/docs-v2/pages/quickstart/_meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "index": "Build a workflow", - "github-sync": "Use GitHub Sync" -} diff --git a/docs-v2/pages/quickstart/get-started.png b/docs-v2/pages/quickstart/get-started.png deleted file mode 100644 index 9bd9f18000fbb..0000000000000 Binary files a/docs-v2/pages/quickstart/get-started.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/github-sync.mdx b/docs-v2/pages/quickstart/github-sync.mdx deleted file mode 100644 index 6019ec6e7df7c..0000000000000 --- a/docs-v2/pages/quickstart/github-sync.mdx +++ /dev/null @@ -1,158 +0,0 @@ -import Callout from '@/components/Callout' -import { Steps } from 'nextra/components' - -# Quickstart - Github Sync - -The purpose of this guide is to help you start building projects on Pipedream using **GitHub Sync**. - -This guide assumes you: - -- Have a Pipedream account -- Have a GitHub account -- Understand the basics of workflow development on Pipedream -- Are familiar with git-concepts (branches, commits, merges, etc) -- Are on an [eligible plan](https://pipedream.com/pricing/) - - -If you are new to Pipedream, we recommend starting with the [basics of workflow development](/quickstart/). - - - -Users on the Free and Basic plan may preview GitHub Sync by enabling the feature **when creating a new project.** You will not be able to merge to production without upgrading. When you upgrade, you may enable or disable GitHub sync for projects at any time. - - - -### Create a new project - -Follow the steps below to create a new project and configure GitHub Sync. - - -This example uses Pipedream's OAuth integration with GitHub which will automaticallly create a a new, empty repo in GitHub. The OAuth integration requires the granting of extensive scopes so Pipedream can create and manage the integration. If you prefer not to use OAuth and configure the integration manually, follow the steps in Pipedream's docs to configure the integration using deploy keys. Deploy keys allow you to restrict Pipedream's access to a specific repo. - - -Visit [https://pipedream.com/projects](https://pipedream.com/projects) and click on **Create Project**. - -![github](./images/create_project.png) - -Enter a project name or click the icon to generate a random name and check the box to **Configure GitHub Sync**. - -![github](./images/configure_project_1.png) - -Select or connect a GitHub account, username and repo name and click **Create Project**. - -![github](./images/configure_project_2.png) - - -### Create a branch to make edits - -Once your project is ready, click on the **Edit** button. - -![github](./images/edit_1.png) - -Since this is a new project, you will prompted to create a branch. You may accept the default branch name or customize it. - -![github](./images/edit_2.png) - -In the future, when you click **Edit**, Pipedream will load development mode for the last branch you were editing for the project. You may also create or select a different branch from the drop down menu. - - -When GitHub Sync is enabled, project resources may only be edited in a branch. - - -### Create a workflow in the editable branch - -Once the branch is created, click the **New** button to create a workflow. - -![github](./images/new_workflow.png) - -For this example, name the workflow and click **Create Workflow** to use the default settings. - -![github](./images/configure_workflow.png) - -Build a simple workflow with an HTTP trigger and Node code step. - -![github](./images/basic_workflow.png) - - -At this point, your workflow is saved, but changes have not been commited to GitHub. To do that, click on the dropdown menu and select **Commit**. - -![github](./images/commit_changes_1.png) - -You may add an option commit message and complete the commit. - -![github](./images/commit_diff_1.png) - -Next, make an edit to the code (e.g., add a comment). Commit your changes again. Notice that the commit diff highlights the specific changes that will be committed. - -![github](./images/commit_diff_2.png) - -### View your project on GitHub - -Open the Git actions menu and select **View branch on GitHub**. - -![github](./images/view_branch_on_github_1.png) - -You can explore the files created, including the YAML file with the workflow definition and the javascript file with the code for the Node step. - -![github](./images/view_branch_on_github_2.png) - - -Optionally navigate to the file with the code step and make an edit to the file and commit your changes to the branch (e.g., add another comment). When you return to Pipedream, you'll notice the git actions button will prompt you to pull changes. Pipedream's integration with GitHub is bi-directional, so you can make edits outside of Pipedream (including locally) and sync them via GitHub. Note: there are currently restrictions on local development (e.g., you can only edit existing resources -- you can't create new workflows locally). - - -### Merge your changes to production - -Merge to production to deploy the workflow to Pipedream. - -![github](./images/merge_to_production_1.png) - -Pipedream will show you the full diff of all commited changes and the current state of Production. When your changes are merged, Pipedream will route you to the `production` branch for the project where you can view and inspect live, running workflows and events. - -![github](./images/merge_to_production_2.png) - -NOTE: When you merge, changes for all resources in the project will be merged. That means one or more workflows may be updated, enabled, disabled, etc. - -### Edit a previously merged branch - -To edit a previously merged branch, you may create a new branch or click **Edit** when viewing the `production`to continue editing the last used branch. - -![github](./images/edit_production.png) - -Open the workflow in Pipedream's editor and add an HTTP request action to the end of the workflow. Configure the action to `GET` the URL `https://example.com`. - -![github](./images/add_action.png) - -This time, we'll merge directly to production without commiting separately. Click **Merge to Production** to view the diff, commit and merge. - -![github](./images/action_diff.png) - -You may also view the repo on GitHub to see how the action configuration is represented in the YAML of the workflow definition. Try other actions, including actions that use connected accounts and explore how they're represented in the GitHub code. - -### Make an edit and merge from GitHub - -As the final step of this quickstart, view the repository on GitHub and navigate to the code step. Click the edit icon and made a simple change (e.g., add a comment). - -![github](./images/edit_in_github.png) - -Commit and create a PR. - -![github](./images/create_pr.png) - -Then open the PR, review the changes and merge. - -![github](./images/merge_pr.png) - -When you return to Pipedream, the changes will be merged to `production` and deployed. - -![github](./images/pr_deployed.png) - -### Use the changelog - -If you have any issues, including errors when merging or deploying, check the changelog. It communicates the state of all operations from both Pipedream and GitHub and will surface errors and issues (including where actions were initiated from and by whom). - -![github](./images/changelog.png) - - -## More resources - -We hope that helps you get started syncing projects with GitHub. Next, we suggest reading the docs and exploring the feature in more depth. If you have any issues or feedback, please [reach out](https://pipedream.com/support). \ No newline at end of file diff --git a/docs-v2/pages/quickstart/image-20220123213645943.png b/docs-v2/pages/quickstart/image-20220123213645943.png deleted file mode 100644 index 29717d7bb0c53..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220123213645943.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220123213843066.png b/docs-v2/pages/quickstart/image-20220123213843066.png deleted file mode 100644 index 006fd8d7b3017..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220123213843066.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220123214244795.png b/docs-v2/pages/quickstart/image-20220123214244795.png deleted file mode 100644 index fb64ae656dcaf..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220123214244795.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220123214505923.png b/docs-v2/pages/quickstart/image-20220123214505923.png deleted file mode 100644 index e0f9f3fc66349..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220123214505923.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220123215443936.png b/docs-v2/pages/quickstart/image-20220123215443936.png deleted file mode 100644 index b133eba0c772c..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220123215443936.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220123215558412.png b/docs-v2/pages/quickstart/image-20220123215558412.png deleted file mode 100644 index 7de2323b5702e..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220123215558412.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220123220018170.png b/docs-v2/pages/quickstart/image-20220123220018170.png deleted file mode 100644 index 5dc6ec9c83d72..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220123220018170.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220123220128877.png b/docs-v2/pages/quickstart/image-20220123220128877.png deleted file mode 100644 index b9a6d23dbdfd7..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220123220128877.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220123221849231.png b/docs-v2/pages/quickstart/image-20220123221849231.png deleted file mode 100644 index 44523dcc949bd..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220123221849231.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220123222247217.png b/docs-v2/pages/quickstart/image-20220123222247217.png deleted file mode 100644 index 2b2f610b8e32f..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220123222247217.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220123222340361.png b/docs-v2/pages/quickstart/image-20220123222340361.png deleted file mode 100644 index 713f6e8fa8fd3..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220123222340361.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220123222643042.png b/docs-v2/pages/quickstart/image-20220123222643042.png deleted file mode 100644 index a0c43ff3be22f..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220123222643042.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125184754078.png b/docs-v2/pages/quickstart/image-20220125184754078.png deleted file mode 100644 index ddaa1e9184045..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125184754078.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125185156527.png b/docs-v2/pages/quickstart/image-20220125185156527.png deleted file mode 100644 index b393cd434b447..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125185156527.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125185305043.png b/docs-v2/pages/quickstart/image-20220125185305043.png deleted file mode 100644 index b3152d40db080..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125185305043.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125185354469.png b/docs-v2/pages/quickstart/image-20220125185354469.png deleted file mode 100644 index 232b3bfec345e..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125185354469.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125185544800.png b/docs-v2/pages/quickstart/image-20220125185544800.png deleted file mode 100644 index 727fa5c0f830a..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125185544800.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125185952120.png b/docs-v2/pages/quickstart/image-20220125185952120.png deleted file mode 100644 index 4428ff8faa277..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125185952120.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125190643112.png b/docs-v2/pages/quickstart/image-20220125190643112.png deleted file mode 100644 index 66bdff163689d..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125190643112.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125190740937.png b/docs-v2/pages/quickstart/image-20220125190740937.png deleted file mode 100644 index c718f90838ac3..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125190740937.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125191025880.png b/docs-v2/pages/quickstart/image-20220125191025880.png deleted file mode 100644 index 5a949e35c55ae..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125191025880.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125191155907.png b/docs-v2/pages/quickstart/image-20220125191155907.png deleted file mode 100644 index 6cbafcf8d5a38..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125191155907.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125191627775.png b/docs-v2/pages/quickstart/image-20220125191627775.png deleted file mode 100644 index c81e86623f1b5..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125191627775.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125191907876.png b/docs-v2/pages/quickstart/image-20220125191907876.png deleted file mode 100644 index ae21a3da2fd08..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125191907876.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125192301634.png b/docs-v2/pages/quickstart/image-20220125192301634.png deleted file mode 100644 index f35a50e97991c..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125192301634.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125192410390.png b/docs-v2/pages/quickstart/image-20220125192410390.png deleted file mode 100644 index bf08516829abd..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125192410390.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125192709058.png b/docs-v2/pages/quickstart/image-20220125192709058.png deleted file mode 100644 index 7df5e20d116a8..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125192709058.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125192818879.png b/docs-v2/pages/quickstart/image-20220125192818879.png deleted file mode 100644 index ba55c705518e4..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125192818879.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125192944518.png b/docs-v2/pages/quickstart/image-20220125192944518.png deleted file mode 100644 index f50607fb6476b..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125192944518.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125193340378.png b/docs-v2/pages/quickstart/image-20220125193340378.png deleted file mode 100644 index dae1d3d213630..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125193340378.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125193450007.png b/docs-v2/pages/quickstart/image-20220125193450007.png deleted file mode 100644 index dae1d3d213630..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125193450007.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125193507453.png b/docs-v2/pages/quickstart/image-20220125193507453.png deleted file mode 100644 index 14b1b8133de2d..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125193507453.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125194354113.png b/docs-v2/pages/quickstart/image-20220125194354113.png deleted file mode 100644 index 85efb88a20040..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125194354113.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125194510308.png b/docs-v2/pages/quickstart/image-20220125194510308.png deleted file mode 100644 index 08107205a8629..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125194510308.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/image-20220125200445675.png b/docs-v2/pages/quickstart/image-20220125200445675.png deleted file mode 100644 index 4737f631221e0..0000000000000 Binary files a/docs-v2/pages/quickstart/image-20220125200445675.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/images/add-trigger.png b/docs-v2/pages/quickstart/images/add-trigger.png deleted file mode 100644 index 1530f85e0bb42..0000000000000 Binary files a/docs-v2/pages/quickstart/images/add-trigger.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/images/connect-google-sheets-account.png b/docs-v2/pages/quickstart/images/connect-google-sheets-account.png deleted file mode 100644 index caa2c79bded47..0000000000000 Binary files a/docs-v2/pages/quickstart/images/connect-google-sheets-account.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/images/create_project.png b/docs-v2/pages/quickstart/images/create_project.png deleted file mode 100644 index b494829dadd36..0000000000000 Binary files a/docs-v2/pages/quickstart/images/create_project.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/images/generate-test-event.png b/docs-v2/pages/quickstart/images/generate-test-event.png deleted file mode 100644 index bc682594f69d0..0000000000000 Binary files a/docs-v2/pages/quickstart/images/generate-test-event.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/images/inspect-trigger-event.png b/docs-v2/pages/quickstart/images/inspect-trigger-event.png deleted file mode 100644 index be4105db565eb..0000000000000 Binary files a/docs-v2/pages/quickstart/images/inspect-trigger-event.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/images/save-trigger.png b/docs-v2/pages/quickstart/images/save-trigger.png deleted file mode 100644 index ac5131d287862..0000000000000 Binary files a/docs-v2/pages/quickstart/images/save-trigger.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/images/select-add-single-row.png b/docs-v2/pages/quickstart/images/select-add-single-row.png deleted file mode 100644 index 312c991d33e9b..0000000000000 Binary files a/docs-v2/pages/quickstart/images/select-add-single-row.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/images/select-google-sheets-app.png b/docs-v2/pages/quickstart/images/select-google-sheets-app.png deleted file mode 100644 index 55a17bd78cec3..0000000000000 Binary files a/docs-v2/pages/quickstart/images/select-google-sheets-app.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/images/sentiment-results.png b/docs-v2/pages/quickstart/images/sentiment-results.png deleted file mode 100644 index 3aef07456a996..0000000000000 Binary files a/docs-v2/pages/quickstart/images/sentiment-results.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/images/step-selector-code.png b/docs-v2/pages/quickstart/images/step-selector-code.png deleted file mode 100644 index 0db2ae3062292..0000000000000 Binary files a/docs-v2/pages/quickstart/images/step-selector-code.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/images/test-code-step.png b/docs-v2/pages/quickstart/images/test-code-step.png deleted file mode 100644 index 5c720d256d56f..0000000000000 Binary files a/docs-v2/pages/quickstart/images/test-code-step.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/images/trigger-continue.png b/docs-v2/pages/quickstart/images/trigger-continue.png deleted file mode 100644 index b105c873b7cf7..0000000000000 Binary files a/docs-v2/pages/quickstart/images/trigger-continue.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/images/unique-url.png b/docs-v2/pages/quickstart/images/unique-url.png deleted file mode 100644 index e820405d736a8..0000000000000 Binary files a/docs-v2/pages/quickstart/images/unique-url.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/index.mdx b/docs-v2/pages/quickstart/index.mdx deleted file mode 100644 index 58f7bfd1d87d9..0000000000000 --- a/docs-v2/pages/quickstart/index.mdx +++ /dev/null @@ -1,244 +0,0 @@ -import Callout from '@/components/Callout' -import { Steps } from 'nextra/components' - -# Quickstart - Workflow Development - -Sign up for a [free Pipedream account](https://pipedream.com/auth/signup) (no credit card required) and complete this quickstart guide to learn the basic patterns for workflow development: - - -### Create a project - -Workflows must be created in **Projects**. Projects make it easy to organize your workflows and collaborate with your team. - -Go to [https://pipedream.com/projects](https://pipedream.com/projects) and click on **Create Project**. - -![create project](./images/create-project.png) - -Next, enter a project name and click **Create Project**. For this example, we'll name our project **Getting Started**. You may also click the icon to the right to generate a random project name. - -![configure project](./images/configure-project.png) - - -Configure GitHub Sync for projects to enable git-based version control and unlock the ability to develop in branches, commit to or pull changes from GitHub, view diffs, create PRs and more. To learn more, complete this quickstart guide to learn the basics of workflow development and then follow the [quickstart guide for GitHub Sync](./github-sync/). - - -### Create a workflow - -After the project is created, use the **New** button to create a new workflow. - -![Create a new workflow](./images/new-workflow.png) - -Name the workflow and click **Create Workflow** to use the default settings. For this example, we'll name the workflow **Pipedream Quickstart**. - -![Configure new workflow](./images/configure-workflow.png) - -### Add an HTTP / Webhook trigger - -Next, Pipedream will launch the workflow builder and prompt you to add a trigger. For this example, select **HTTP / Webhook Requests**. - -![Add HTTP trigger](./images/add-trigger.png) - -Click **Save and continue** to accept the default settings. - -![Save trigger](./images/save-trigger.png) - -Pipedream will generate a unique URL to trigger this workflow. Once your workflow is deployed, your workflow will run on every request to this URL. - -![Unique URL](./images/unique-url.png) - -### Generate a test event - -Next, generate a test event to help you build the workflow. - - -The test event will be used to provide autocomplete suggestion as you build your workflow. The data will also be used when testing later steps. You may generate or select a different test event at any time when building a workflow. - - -For this example, let's use the following test event data: - -```json -{ - "message": "Pipedream is awesome!" -} -``` - -Pipedream makes it easy to generate test events for your HTTP trigger. Click on **Generate Test Event** to open the HTTP request builder. Copy and paste the JSON data above into the **Raw Request Body** field and click **Send HTTP Request**. - -![Generate Test Event](./images/generate-test-event.png) - -Pipedream will automatically select and display the contents of the selected event. Validate that the `message` was received as part the event `body`. - -![Inspect trigger event](./images/inspect-trigger-event.png) - - - -You may also send live data to the unique URL for your workflow using your favorite HTTP tool or by running a `cURL` command, e.g., - -```bash -curl -d '{"message": "Pipedream is awesome!"}' \ - -H "Content-Type: application/json" \ - YOUR_ENDPOINT_URL -``` - - -### Enrich trigger data using Node.js and npm - -Before we send data to Google Sheets, let's use the npm [`sentiment`](https://www.npmjs.com/package/sentiment) package to generate a sentiment score for our message. To do that, click **Continue** or the **+** button. - -![Continue from trigger](./images/trigger-continue.png) - -That will open the **Add a step** menu. Select **Run custom code**. - -![Add step](./images/step-selector-code.png) - -Pipedream will add a Node.js code step. Rename the step to **sentiment**. - -![Rename step to sentiment](./images/rename-code-step.png) - -Next, add the following code to the code step: - -```javascript -import Sentiment from "sentiment" - -export default defineComponent({ - async run({ steps, $ }) { - let sentiment = new Sentiment() - return sentiment.analyze(steps.trigger.event.body.message) - }, -}) -``` - -This code imports the npm package, passes the message we sent to our trigger to the `analyze()` function by referencing `steps.trigger.event.body.message` and then returns the result. - - -To use any npm package on Pipedream, just `import` it. There's no `npm install` or `package.json` required. - - - -Any data you `return` from a step is exported so it can be inspected and referenced it in future steps via the `steps` object. In this example, return values will be exported to `steps.sentiment.$return_value` because we renamed the step to **sentiment** . - - -Your code step should now look like the screenshot below. To run the step and test the code, click the **Test** button. - -![Test code step](./images/test-code-step.png) - -You should see the results of the sentiment analysis when the test is complete. - -![Test results](./images/sentiment-results.png) - - - -When you **Test** a step, only the current step is executed. Use the caret to test different ranges of steps including the entire workflow. - - -### Save data to Google Sheets - -Next, create a Google Sheet and add **Timestamp**, **Message** and **Sentiment Score** to the first row. These labels act as our column headers amd will help us configure the Google Sheets step of the workflow. - -![Empty Sheet](./images/empty-sheet.png) - -Next, let's add a step to the workflow to send the data to Google Sheets. First, click **+** after the `sentiment` code step and select the **Google Sheets** app. - -![Step Selector - Google Sheets](./images/select-google-sheets-app.png) - -Then select the **Add Single Row** action. - -![Select Add Single Row](./images/select-add-single-row.png) - -Click to connect you Google Sheets account to Pipedream (or select it from the dropdown if you previously connected an account). - -![Connect Google Sheets](./images/connect-google-sheets-account.png) - -Pipedream will open Google's sign in flow in a new window. Sign in with the account you want to connect. - -![Google OAuth Window](./images/google-oauth.png) - - -If prompted, you must check the box for Pipedream to **See, edit, create and delete all of your Google Drive files**. These permissions are required for configure and use the pre-built actions for Google Sheets. - - -![Google Permissions](./images/google-permissions.png) - -Learn more about Pipedream's [privacy and security policy](/privacy-and-security/). - -When you complete connecting your Google account, the window should close and you should return to Pipedream. Your connected account should automatically be selected. Next, select your spreadsheet from the dropdown menu: - -![Select Spreadsheet](./images/select-spreadsheet.png) - -Then select the sheet name (the default sheet name in Google Sheets is **Sheet1**): - -![Select Sheet Name](./images/select-sheet-name.png) - -Next, select if the spreadsheet has headers in the first row. When a header row exists, Pipedream will automatically retrieve the header labels to make it easy to enter data (if not, you can manually construct an array of values). Since the sheet for this example contains headers, select **Yes**. - -![Has headers](./images/has-headers.png) - -Pipedream will retrieve the headers and generate a form to enter data in your sheet: - -![Additional props](./images/additional-props.png) - -First, let's use the object explorer to pass the timestamp for the workflow event as the value for the first column. This data can be found in the context object on the trigger. - -When you click into the **Timestamp** field, Pipedream will display an object explorer to make it easy to find data. Scroll or search to find the `ts` key under `steps.trigger.context`. - -![Add reference to timestamp](./images/add-timestamp.png) - -Click **select path** to insert a reference to steps.trigger.context.ts: - -![Timestamp added](./images/timestamp-added.png) - -Next, let's use autocomplete to enter a value for the **Message** column. First, add double braces `{{` — Pipedream will automatically add the closing braces `}}`. - -Then, type `steps.trigger.event.body.message` between the pairs of braces. Pipedream will provide autocomplete suggestions as you type. Press **Tab** to use a suggestion and then click `.` to get suggestions for the next key. The final value in the **Message** field should be `steps.trigger.event.body.message`. - -![Add reference to message](./images/autocomplete-message.png) - -Finally, let's copy a reference from a previous step. Scroll up to the `sentiment` step and expand the results: - -![Expand sentiment results](./images/expand-sentiment-results.png) - -Next, click the **Copy Path** link next to the score. - -![Copy path to sentiment score](./images/copy-path.png) - -Paste the value into the **Sentiment Score** field — Pipedream will automatically wrap the reference in double braces `{{ }}`. - -![Paste reference to sentiment score](./images/action-configuration-complete.png) - -Now that the configuration is complete, click **Test** to validate the configuration for this step. When the test is complete, you will see a success message and a summary of the action performed: - -![Action results](./images/test-successful.png) - -If you load your spreadsheet, you should see the data Pipedream inserted. - -![Data inserted into sheets](./images/data-inserted.png) - -Next, return to your workflow and click **Deploy** to run your workflow on every trigger event. - -![Deploy Workflow](./images/deploy-workflow.png) - -When your workflow deploys, you will be redirected to the **Inspector**. Your workflow is now live. - -![Deployed workflow](./images/deployed-workflow.png) - -To validate your workflow is working as expected, send a new request to your workflow: You can edit and run the following `cURL` command: - -```bash -curl -d '{ "message": "Pipedream is awesome!" }' \ - -H "Content-Type: application/json" \ - YOUR-TRIGGER-URL -``` - -The event will instantly appear in the event list. Select it to inspect the workflow execution. - -![Inspect executions](./images/inspect-executions.png) - -Finally, you can return to Google Sheets to validate that the new data was automatically inserted. - -![Live data inserted into sheets](./images/live-test.png) - - - -## Next Steps - -Congratulations! You completed the quickstart and should now understand the basic patterns for workflow development. Next, try creating your own workflows, explore the quickstart for [GitHub Sync](./github-sync/) and check out the rest of the [docs](/)! diff --git a/docs-v2/pages/quickstart/next.png b/docs-v2/pages/quickstart/next.png deleted file mode 100644 index 90ca885001dec..0000000000000 Binary files a/docs-v2/pages/quickstart/next.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329003353166.png b/docs-v2/pages/quickstart/v2/README/image-20220329003353166.png deleted file mode 100644 index fce70118c917c..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329003353166.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329003440947.png b/docs-v2/pages/quickstart/v2/README/image-20220329003440947.png deleted file mode 100644 index dda533905cae3..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329003440947.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329003539441.png b/docs-v2/pages/quickstart/v2/README/image-20220329003539441.png deleted file mode 100644 index c9568719338bd..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329003539441.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329003748162.png b/docs-v2/pages/quickstart/v2/README/image-20220329003748162.png deleted file mode 100644 index 25c35ae2fe107..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329003748162.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329003918421.png b/docs-v2/pages/quickstart/v2/README/image-20220329003918421.png deleted file mode 100644 index 3ab548331a24d..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329003918421.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329004022863.png b/docs-v2/pages/quickstart/v2/README/image-20220329004022863.png deleted file mode 100644 index 9c2efff743147..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329004022863.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329004301119.png b/docs-v2/pages/quickstart/v2/README/image-20220329004301119.png deleted file mode 100644 index f0371643d6f74..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329004301119.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329004604153.png b/docs-v2/pages/quickstart/v2/README/image-20220329004604153.png deleted file mode 100644 index 0c382e5cdbdd4..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329004604153.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329004656744.png b/docs-v2/pages/quickstart/v2/README/image-20220329004656744.png deleted file mode 100644 index e8441a3bf75d4..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329004656744.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329004810702.png b/docs-v2/pages/quickstart/v2/README/image-20220329004810702.png deleted file mode 100644 index cf7eae2007c0b..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329004810702.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329004904474.png b/docs-v2/pages/quickstart/v2/README/image-20220329004904474.png deleted file mode 100644 index c0f59a1defd2b..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329004904474.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329005354310.png b/docs-v2/pages/quickstart/v2/README/image-20220329005354310.png deleted file mode 100644 index 43bddd3cd17f8..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329005354310.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329005435230.png b/docs-v2/pages/quickstart/v2/README/image-20220329005435230.png deleted file mode 100644 index 5644505c598e2..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329005435230.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329005510143.png b/docs-v2/pages/quickstart/v2/README/image-20220329005510143.png deleted file mode 100644 index 8927756ab333b..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329005510143.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329005808138.png b/docs-v2/pages/quickstart/v2/README/image-20220329005808138.png deleted file mode 100644 index 4e81f48e531bb..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329005808138.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329010012345.png b/docs-v2/pages/quickstart/v2/README/image-20220329010012345.png deleted file mode 100644 index 4e81f48e531bb..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329010012345.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329010103998.png b/docs-v2/pages/quickstart/v2/README/image-20220329010103998.png deleted file mode 100644 index 989e7ee3932f8..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329010103998.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329010133943.png b/docs-v2/pages/quickstart/v2/README/image-20220329010133943.png deleted file mode 100644 index 9b608f120f648..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329010133943.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329010200367.png b/docs-v2/pages/quickstart/v2/README/image-20220329010200367.png deleted file mode 100644 index 1f272dc2e671c..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329010200367.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329010234048.png b/docs-v2/pages/quickstart/v2/README/image-20220329010234048.png deleted file mode 100644 index c559f4fa01be6..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329010234048.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329010348936.png b/docs-v2/pages/quickstart/v2/README/image-20220329010348936.png deleted file mode 100644 index a1453f204bcd3..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329010348936.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329010418111.png b/docs-v2/pages/quickstart/v2/README/image-20220329010418111.png deleted file mode 100644 index a1453f204bcd3..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329010418111.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329010525727.png b/docs-v2/pages/quickstart/v2/README/image-20220329010525727.png deleted file mode 100644 index 942c10cee108a..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329010525727.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329010637137.png b/docs-v2/pages/quickstart/v2/README/image-20220329010637137.png deleted file mode 100644 index 171c303d122ee..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329010637137.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329010721164.png b/docs-v2/pages/quickstart/v2/README/image-20220329010721164.png deleted file mode 100644 index 9522380c0dec9..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329010721164.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329010858873.png b/docs-v2/pages/quickstart/v2/README/image-20220329010858873.png deleted file mode 100644 index c439a4f8008d8..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329010858873.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329011023749.png b/docs-v2/pages/quickstart/v2/README/image-20220329011023749.png deleted file mode 100644 index e51a4b9a6390d..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329011023749.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329011056106.png b/docs-v2/pages/quickstart/v2/README/image-20220329011056106.png deleted file mode 100644 index 8e356bd0119cb..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329011056106.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329011135355.png b/docs-v2/pages/quickstart/v2/README/image-20220329011135355.png deleted file mode 100644 index 0212402065201..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329011135355.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329011205036.png b/docs-v2/pages/quickstart/v2/README/image-20220329011205036.png deleted file mode 100644 index 280ad1339b7d4..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329011205036.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329011311120.png b/docs-v2/pages/quickstart/v2/README/image-20220329011311120.png deleted file mode 100644 index 280ad1339b7d4..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329011311120.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329011323514.png b/docs-v2/pages/quickstart/v2/README/image-20220329011323514.png deleted file mode 100644 index 45380c3e161b1..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329011323514.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329011500931.png b/docs-v2/pages/quickstart/v2/README/image-20220329011500931.png deleted file mode 100644 index 880f0ac035f2c..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329011500931.png and /dev/null differ diff --git a/docs-v2/pages/quickstart/v2/README/image-20220329011543065.png b/docs-v2/pages/quickstart/v2/README/image-20220329011543065.png deleted file mode 100644 index 8d5e36481e042..0000000000000 Binary files a/docs-v2/pages/quickstart/v2/README/image-20220329011543065.png and /dev/null differ diff --git a/docs-v2/pages/rest-api/_meta.json b/docs-v2/pages/rest-api/_meta.json deleted file mode 100644 index 91d8fea95d603..0000000000000 --- a/docs-v2/pages/rest-api/_meta.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "index": "API Reference", - "auth": "Authentication", - "workflows": "Example: Create a workflow", - "rss": "Example: Create an RSS source", - "webhooks": "Example: Webhooks" -} diff --git a/docs-v2/pages/rest-api/_meta.tsx b/docs-v2/pages/rest-api/_meta.tsx new file mode 100644 index 0000000000000..b05fdbc325523 --- /dev/null +++ b/docs-v2/pages/rest-api/_meta.tsx @@ -0,0 +1,7 @@ +export default { + "index": "API Reference", + "auth": "Authentication", + "workflows": "Example: Create a workflow", + "rss": "Example: Create an RSS source", + "webhooks": "Example: Webhooks", +} as const diff --git a/docs-v2/pages/rest-api/auth.mdx b/docs-v2/pages/rest-api/auth.mdx index 4447e67c0712d..f0b6263c8a77e 100644 --- a/docs-v2/pages/rest-api/auth.mdx +++ b/docs-v2/pages/rest-api/auth.mdx @@ -1,16 +1,103 @@ # Authentication -## Pipedream API Key +The Pipedream API supports two methods of authentication: [OAuth](#oauth) and [User API keys](#user-api-keys). -When you sign up for Pipedream, an API key is automatically generated for your account. You can use this key to authorize requests to the API. +**We recommend OAuth** for a few reasons: + +✅ OAuth clients are tied to the workspace, administered by workspace admins
+✅ Tokens are short-lived
+✅ OAuth clients support scopes, limiting access to specific operations (coming soon!)
+✅ Limit access to specific Pipedream projects (coming soon!)
+ +When testing the API or using the CLI, you can use your user API key. This key is tied to your user account and provides full access to any resources your user has access to, across workspaces. + +## OAuth + +Workspace administrators can create OAuth clients in your workspace's [API settings](https://pipedream.com/settings/api). + +Since API requests are meant to be made server-side, and since grants are not tied to individual end users, all OAuth clients are [**Client Credentials** applications](https://www.oauth.com/oauth2-servers/access-tokens/client-credentials/). + +### Creating an OAuth client + +1. Visit the [API settings](https://pipedream.com/settings/api) for your workspace. +2. Click the **New OAuth Client** button. +3. Name your client and click **Create**. +4. Copy the client secret. **It will not be accessible again**. Click **Close**. +5. Copy the client ID from the list. + +### How to get an access token + +In the client credentials model, you exchange your OAuth client ID and secret for an access token. Then you use the access token to make API requests. + +If you're running a server that executes JavaScript, we recommend using [the Pipedream SDK](/connect/api/#installing-the-typescript-sdk), which automatically refreshes tokens for you. + +```javascript +import { createBackendClient } from "@pipedream/sdk/server"; + +// These secrets should be saved securely and passed to your environment +const pd = createBackendClient({ + credentials: { + clientId: "YOUR_CLIENT_ID", + clientSecret: "YOUR_CLIENT_SECRET", + }, + projectId: "YOUR_PROJECT_ID", // This is typically required for most Connect API endpoints +}); + +// Use the SDK's helper methods to make requests +const accounts = await pd.getAccounts({ include_credentials: 1 }); + +// Or make any Pipedream API request with the fresh token +const accounts = await pd.makeAuthorizedRequest("/accounts", { + method: "GET" + params: { + include_credentials: 1, + } +}); +``` + +You can also manage this token refresh process yourself, using the `/oauth/token` API endpoint: + +```bash +curl https://api.pipedream.com/v1/oauth/token \ + -H 'Content-Type: application/json' \ + -d '{ "grant_type": "client_credentials", "client_id": "", "client_secret": "" }' +``` + +Access tokens expire after 1 hour. Store access tokens securely, server-side. + +### Revoking a client secret + +1. Visit your workspace's [API settings](https://pipedream.com/settings/api). +2. Click the **...** button to the right of the OAuth client whose secret you want to revoke, then click **Rotate client secret**. +3. Copy the new client secret. **It will not be accessible again**. + +### OAuth security + +See [the OAuth section of the security docs](/privacy-and-security/#pipedream-rest-api-security-oauth-clients) for more information on how Pipedream secures OAuth credentials. + +## User API keys + +When you sign up for Pipedream, an API key is automatically generated for your user account. You can use this key to authorize requests to the API. You'll find this API key in your [User Settings](https://pipedream.com/user) (**My Account** -> **API Key**). +**Use user API keys when testing the API or using the CLI**. This key is tied to your user account and provides full access to any resources your user has access to, across workspaces. + +### Revoking your API key + +You can revoke your API key in your [Account Settings](https://pipedream.com/settings/account) (**Settings** -> **Account**). Click on the **REVOKE** button directly to the right of your API key. + +This will revoke your original API key, generating a new one. Any API requests made with the original token will yield a `401 Unauthorized` error. + ## Authorizing API requests -Pipedream uses [Bearer Authentication](https://oauth.net/2/bearer-tokens/) to authorize your access to the API or SSE event streams. When you make API requests, pass an `Authorization` header of the following format: +Whether you use OAuth access tokens or user API keys, Pipedream uses [Bearer Authentication](https://oauth.net/2/bearer-tokens/) to authorize your access to the API or SSE event streams. When you make API requests, pass an `Authorization` header of the following format: ``` +# OAuth access token +Authorization: Bearer + +# User API key Authorization: Bearer ``` @@ -23,10 +110,4 @@ curl 'https://api.pipedream.com/v1/users/me' \ ## Using the Pipedream CLI -You can [link the CLI to your Pipedream account](/cli/login/), which will automatically pass your API key in the `Authorization` header with every API request. - -## Revoking your API key - -You can revoke your API key in your [Account Settings](https://pipedream.com/settings/account) (**Settings** -> **Account**). Click on the **REVOKE** button directly to the right of your API key. - -This will revoke your original API key, generating a new one. Any API requests made with the original token will yield a `401 Unauthorized` error. +You can [link the CLI to your Pipedream account](/workflows/cli/login/), which will automatically pass your API key in the `Authorization` header with every API request. diff --git a/docs-v2/pages/rest-api/index.mdx b/docs-v2/pages/rest-api/index.mdx index 19fe9eb049310..6bf81659e7240 100644 --- a/docs-v2/pages/rest-api/index.mdx +++ b/docs-v2/pages/rest-api/index.mdx @@ -4,7 +4,7 @@ import Callout from "@/components/Callout"; ## Overview -Use the REST API to create and manage sources, workflows, subscriptions, and more. +Use the REST API to create workflows, manage event sources, handle subscriptions, and more. ## Base URL @@ -12,36 +12,35 @@ The base URL for all requests is [https://api.pipedream.com/v1](https://api.pipe ## Authentication -You authenticate to the REST API using your [Pipedream API -key](/rest-api/auth/#pipedream-api-key). When you make API requests, pass an -`Authorization` header of the following format: +The Pipedream API supports two methods of authentication: [OAuth](/rest-api/auth/#oauth) and [User API keys](/rest-api/auth/#user-api-keys). **Pipedream recommends using OAuth for most use cases**. -``` -Authorization: Bearer -``` - -For example, here's how you can use `cURL` to fetch profile information for the -authenticated user: +All credentials are passed as a Bearer token in the `Authorization` header. For example: ```shell -curl 'https://api.pipedream.com/v1/users/me' \ - -H 'Authorization: Bearer ' +curl https://api.pipedream.com/v1/accounts \ + -H "Authorization Bearer " ``` -Learn more about [API authentication](/rest-api/auth/) +Learn more in the [Authentication docs](/rest-api/auth/). + +### Authenticating as a workspace vs. a user + +Pipedream recommends using [OAuth](/rest-api/auth/#oauth) to auth against the Pipedream API. OAuth tokens are associated with a workspace, and the API will automatically use the workspace associated with the token. + +When you authenticate with a user API key, you must [specify the workspace ID in the `org_id` parameter](#common-parameters) when making requests to specific endpoints. ## Required headers -The `Authorization` header is required on all endpoints for authentication. +The `Authorization` header is required on all endpoints, to authenticate API requests. `POST` or `PUT` requests that accept JSON payloads also require a `Content-Type` header set to `application/json`. For example: ```shell curl https://api.pipedream.com/v1/components \ - -H "Authorization: Bearer " \ + -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ - -d '{"component_url": "https://github.com/PipedreamHQ/pipedream/blob/master/components/rss/sources/new-item-in-feed/new-item-in-feed.js"}' + -d '{"component_url": "https://github.com/PipedreamHQ/pipedream/blob/master/components/rss/sources/new-item-in-feed/new-item-in-feed.ts"}' ``` ## Common Parameters @@ -70,25 +69,18 @@ including all fields). Pass as a string of comma-separated values: --- -`workspace_id` **string** - -Some endpoints require you to specify [your workspace ID](/workspaces/#finding-your-workspace-s-id) you want the operation to take effect in. For example, if you're creating a new event source in a specific workspace, you'll want to pass the workspace ID in the `workspace_id` query string parameter. - -[Find your workspace's ID here](/workspaces/#finding-your-workspace-s-id). +`org_id` **string** - - If your organization is on one of our legacy plans like the Free Teams or - Teams plan, the `workspace_id` is synonymous with your `org_id`. Just pass - your organization ID as the same parameter. + +The `org_id` parameter is only required when using [User API keys](/rest-api/auth/#user-api-keys). When authenticating with OAuth tokens, the API will automatically use the workspace associated with the token. -## Working with resources owned by a workspace +When using [User API keys](/rest-api/auth/#user-api-keys), some endpoints require you to specify [your workspace ID](/workflows/workspaces/#finding-your-workspaces-id) you want the operation to take effect in: -If you're interacting with resources owned by a [workspace](/workspaces/), you may need to specify the workspace ID as a part of the request's query string parameter or route: +- When _fetching_ specific resources (for example, when you [retrieve events for a specific source](#get-source-events)), **you should not need to pass `org_id`**. If your user is a part of the workspace, and you have access to that resource, and the API will return the details of the resource. +- When _creating_ new resources, you'll need to specify the `org_id` in which you want to create the resource. -- When fetching specific resources (for example, when you [retrieve events for a specific source](#get-source-events)), you should not need to pass your workspace's ID. If your user is a part of the workspace, you should have access to that resource, and the API will return the details of the resource. -- When _creating_ new resources, you'll need to specify the `org_id` where you want the resource to live as a query string parameter (`?org_id=o_abc123`). Read more about the `org_id` parameter in the [Common Parameters section](#common-parameters). -- When _listing_ resources, use [the workspace-specific endpoints here](#workspaces). +[Find your workspace / org ID here](/workflows/workspaces/#finding-your-workspaces-id). ## Pagination @@ -124,8 +116,6 @@ A cursor, specifying you'd like to retrieve items _before_ this cursor. Cursor strings are returned with all paginated responses. ---- - ### Example Paginated Request This request fetches a page of 5 sources in the authenticated account, after a @@ -165,19 +155,79 @@ The response from the request above will have a shape that looks like: ## Errors Pipedream uses conventional HTTP response codes to indicate the success or -failure of an API request. Codes in the **2xx** range indicate success. Codes in -the **4xx** range indicate an error that failed (e.g., a required parameter was -omitted). Codes in the **5xx** range indicate an error with Pipedream’s server. +failure of an API request: + +- Codes in the `2xx` range indicate success. +- Codes in the `4xx` range indicate an error that failed (e.g., a required parameter was omitted). +- Codes in the `5xx` range indicate an error with Pipedream's server. ## Accounts -[Connected accounts](/connected-accounts/) let you manage credentials for integrated APIs. + +These docs discuss connected accounts **for Pipedream users**. To retrieve accounts for your end users via Connect, refer to the [Connect API docs](/connect/api/#accounts). + -### Get account +[Connected accounts](/integrations/connected-accounts/) let you manage credentials for integrated APIs. + +### List accounts + +List connected accounts accessible by the authenticated user or workspace. + +``` +GET /accounts/ +``` + +#### Parameters + +`app` **string** (_optional_) + +The ID or name slug the app you'd like to retrieve. For example, Slack's unique app ID is `app_OkrhR1`, and its name slug is `slack`. + +You can find the app's ID in the response from the [List apps](#list-apps) endpoint, and the name slug under the **Authentication** section of any [app page](https://pipedream.com/apps). + +--- + +`oauth_app_id` **string** (_optional_) + +The ID of the custom [OAuth app](/connect/managed-auth/quickstart/#create-a-pipedream-oauth-client) you'd like to retrieve accounts for. --- -By default, this route returns metadata for a specific connected account. Set `include_credentials=1` to return credentials that you can use in any app where you need auth. [See this guide](/connected-accounts/api/) to learn more. +`include_credentials` **boolean** (_optional_) + +Pass `include_credentials=true` as a query-string parameter to include the account credentials in the response + +#### Example Request — Get account metadata + +```bash +curl 'https://api.pipedream.com/v1/accounts' \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" +``` + +#### Example Response — Get account metadata + +```json +{ + "data": [ + { + "id": "apn_abc123", + "created_at": "2022-07-27T20:37:52.000Z", + "updated_at": "2024-02-11T04:18:46.000Z", + "name": "Google Sheets — pipedream.com", // account nickname, if set + "app": { + "id": "app_abc123", + "name": "Google Sheets" + }, + "healthy": true // true if Pipedream can make a successful test request + } + ] +} +``` + +### Get account + +By default, this route returns metadata for a specific connected account. Set `include_credentials=true` to return credentials that you can use in any app where you need the actual credentials (API key or OAuth access token for example). #### Endpoint @@ -187,13 +237,11 @@ GET /accounts/{account_id} #### Parameters ---- - `account_id` **string** To retrieve your account ID: -1. [Connect your account](/connected-accounts/#connecting-a-new-account) +1. [Connect your account](/integrations/connected-accounts/#connecting-a-new-account) 2. On [https://pipedream.com/accounts](https://pipedream.com/accounts), find your account and click the `...` to the right of the account, 3. **Copy Account ID** @@ -201,21 +249,21 @@ To retrieve your account ID:
-`include_credentials` **number** +--- -Default `0`: Response only contains account metadata, no credentials. Pass `1` to include `credentials`. +`include_credentials` **boolean** (_optional_) ---- +Pass `include_credentials=true` as a query-string parameter to include the account credentials in the response -#### Example Request — Get account metadata +#### Example Request — Get account metadata ```bash curl 'https://api.pipedream.com/v1/accounts/' \ - -H "Authorization: Bearer " \ + -H "Authorization: Bearer " \ -H "Content-Type: application/json" ``` -#### Example Response — Get account metadata +#### Example Response — Get account metadata ```json { @@ -223,7 +271,7 @@ curl 'https://api.pipedream.com/v1/accounts/' \ "id": "apn_abc123", "created_at": "2022-07-27T20:37:52.000Z", "updated_at": "2024-02-11T04:18:46.000Z", - "name": "Google Sheets — pipedream.com", // account nickname, if set + "name": "Google Sheets — pipedream.com", // account nickname, if set "app": { "id": "app_abc123", "name": "Google Sheets" @@ -236,8 +284,8 @@ curl 'https://api.pipedream.com/v1/accounts/' \ #### Example Request — Get account credentials ```bash -curl 'https://api.pipedream.com/v1/accounts/?include_credentials=1' \ - -H "Authorization: Bearer " \ +curl 'https://api.pipedream.com/v1/accounts/?include_credentials=true' \ + -H "Authorization: Bearer " \ -H "Content-Type: application/json" ``` @@ -250,7 +298,7 @@ curl 'https://api.pipedream.com/v1/accounts/?include_credentials=1' "created_at": "2022-07-27T20:37:52.000Z", "updated_at": "2024-02-11T04:18:46.000Z", "expires_at": "2024-02-11T05:18:46.000Z", - "name": "Google Sheets — pipedream.com", // account nickname, if set + "name": "Google Sheets — pipedream.com", // account nickname, if set "app": { "id": "app_abc123", "name": "Google Sheets" @@ -268,20 +316,156 @@ The properties of the `credentials` object are specific to the app. All OAuth apps expose the following properties: -- `oauth_access_token` — A fresh OAuth access token -- `oauth_client_id` — The client ID of the OAuth app -- `oauth_refresh_token` — The latest OAuth refresh token for your grant -- `oauth_uid` — A unique identifier in the third party API's system, typically a user ID or email address +- `oauth_access_token` — A fresh OAuth access token +- `oauth_client_id` — The client ID of the OAuth app +- `oauth_refresh_token` — The latest OAuth refresh token for your grant +- `oauth_uid` — A unique identifier in the third party API's system, typically a user ID or email address Apps with static credentials expose fields specific to the API, e.g. `api_key`. Review the response for specific apps to see the app-specific response. +## Apps + +### List Apps + +--- + +Retrieve a list of all apps available on Pipedream. + +#### Endpoint + +``` +GET /apps +``` + +#### Parameters + +`q` **string** (_optional_) + +A query string to filter the list of apps. For example, to search for apps that **contain** the string "Slack", pass `q=Slack`. + +--- + +`has_components` **string** (_optional_) + +Pass `1` to filter the list of apps to only those with public triggers or actions. + +--- + +`has_actions` **string** (_optional_) + +Pass `1` to filter the list of apps to only those with public actions. + +--- + +`has_triggers` **string** (_optional_) + +Pass `1` to filter the list of apps to only those with public triggers. + +#### Example Request + +```shell +curl https://api.pipedream.com/v1/apps + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" +``` + +#### Example Response + +```json +{ + "page_info": { + "total_count": 2, + "count": 2, + "start_cursor": "c2xhY2s", + "end_cursor": "c2xhY2tfYm90" + }, + "data": [ + { + "id": "app_OkrhR1", + "name_slug": "slack", + "name": "Slack", + "auth_type": "oauth", + "description": "Slack is a channel-based messaging platform. With Slack, people can work together more effectively, connect all their software tools and services, and find the information they need to do their best work — all within a secure, enterprise-grade environment.", + "img_src": "https://assets.pipedream.net/s.v0/app_OkrhR1/logo/orig", + "custom_fields_json": "[]", + "categories": [ + "Communication" + ] + }, + { + "id": "app_mWnheL", + "name_slug": "slack_bot", + "name": "Slack Bot", + "auth_type": "keys", + "description": "Interact with Slack with your own bot user", + "img_src": "https://assets.pipedream.net/s.v0/app_mWnheL/logo/orig", + "custom_fields_json": "[{\"name\":\"bot_token\",\"label\":\"Bot Token\",\"description\":null,\"default\":null,\"optional\":null,\"type\":\"password\"}]", + "categories": [ + "Communication" + ] + } + ] +} +``` + +### Get an App + +Retrieve metadata for a specific app. + +#### Endpoint + +``` +GET /apps/{app_id} +``` + +#### Path Parameters + +`app_id` **string** + +The ID or name slug the app you'd like to retrieve. For example, Slack's unique app ID is `app_OkrhR1`, and its name slug is `slack`. + +You can find the app's ID in the response from the [List apps](#list-apps) endpoint, and the name slug under the **Authentication** section of any [app page](https://pipedream.com/apps). + +#### Example Request + +```bash +curl https://api.pipedream.com/v1/apps/app_OkrhR1 \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" +``` + +#### Example Response + +```json +"data": [ + { + "id": "app_OkrhR1", + "name_slug": "slack", + "name": "Slack", + "auth_type": "oauth", + "description": "Slack is a channel-based messaging platform. With Slack, people can work together more effectively, connect all their software tools and services, and find the information they need to do their best work — all within a secure, enterprise-grade environment.", + "img_src": "https://assets.pipedream.net/s.v0/app_OkrhR1/logo/orig", + "custom_fields_json": "[]", + "categories": [ + "Communication" + ] + } +] +``` + ## Components + +These docs discuss the management of Pipedream components. To run components on behalf of your end users in your application, refer to the [Connect API docs](/connect/api/#components). + + Components are objects that represent the code for an [event source](#sources). ### Create a component ---- + +`/components` endpoints are only available when using [user API keys](/rest-api/auth/#user-api-keys), not yet for workspace [OAuth tokens](/rest-api/auth/#oauth). + Before you can create a source using the REST API, you must first create a **component** - the code for the source. @@ -297,11 +481,9 @@ POST /components #### Parameters ---- - `component_code` **string** (_optional_) -The full code for a [Pipedream component](/components/api/). +The full code for a [Pipedream component](/workflows/contributing/components/api/). --- @@ -310,7 +492,7 @@ The full code for a [Pipedream component](/components/api/). A reference to the URL where the component is hosted. For example, to create an RSS component, pass -`https://github.com/PipedreamHQ/pipedream/blob/master/components/rss/sources/new-item-in-feed/new-item-in-feed.js`. +`https://github.com/PipedreamHQ/pipedream/blob/master/components/rss/sources/new-item-in-feed/new-item-in-feed.ts`. --- @@ -324,9 +506,9 @@ Here's an example of how to create an RSS component from a Github URL: ```shell curl https://api.pipedream.com/v1/components \ - -H "Authorization: Bearer " \ + -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ - -d '{"component_url": "https://github.com/PipedreamHQ/pipedream/blob/master/components/rss/sources/new-item-in-feed/new-item-in-feed.js"}' + -d '{"component_url": "https://github.com/PipedreamHQ/pipedream/blob/master/components/rss/sources/new-item-in-feed/new-item-in-feed.ts"}' ``` #### Example Response @@ -375,8 +557,6 @@ GET /components/{key|id} #### Parameters ---- - `key` **string** The component key (identified by the `key` property within the component's @@ -388,13 +568,11 @@ source code) you'd like to fetch metadata for (example: `my-component`) The saved component ID you'd like to fetch metadata for (example: `sc_JDi8EB`) ---- - #### Example Request ```shell curl https://api.pipedream.com/v1/components/my-component \ - -H "Authorization: Bearer " + -H "Authorization: Bearer " ``` #### Example Response @@ -444,20 +622,16 @@ GET /components/registry/{key} #### Parameters ---- - `key` **string** The component key (identified by the `key` property within the component's source code) you'd like to fetch metadata for (example: `my-component`) ---- - #### Example Request ```shell curl https://api.pipedream.com/v1/components/registry/github-new-repository \ - -H "Authorization: Bearer " + -H "Authorization: Bearer " ``` #### Example Response @@ -491,12 +665,73 @@ curl https://api.pipedream.com/v1/components/registry/github-new-repository \ } ``` -## Events +### Search for registry components -### Get Source Events +Search for components in the global registry with natural language. Pipedream will use AI to match your query to the most relevant components. + +#### Endpoint + +``` +GET /components/search +``` + +#### Parameters + +`query` **string** + +The query string to search for components in the global registry, e.g. "Send a message to Slack on new Hubspot contacts" --- +`app` **string** (_optional_) + +The name slug the app you'd like to filter results for. For example, Slack's name slug is `slack`. Returned sources and actions are filtered to only those tied to the specified app. + +You can find the name slug under the **Authentication** section of any [app page](https://pipedream.com/apps). + +--- + +`similarity_threshold` **number** (_optional_) + +The minimum similarity score required for a component to be returned. The similarity score is a number between 0 and 1, where 1 is a perfect match. Similarity here is computed as the cosine distance between the embedding of the user query and the embedding of the component's metadata. + +--- + +`debug` **boolean** (_optional_) + +Pass `debug=true` to return additional data in the response, useful for inspecting the results. + +#### Example Request + +```shell +curl https://api.pipedream.com/v1/components/search\?query\="When a new Hubspot contact is added, send me an SMS"&limit=1 \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" +``` + +### Example Response + +```json +{ + "sources": [ + "hubspot-new-contact" + ], + "actions": [ + "twilio-send-sms" + ] +} +``` + +## Connect + +[Pipedream Connect](/connect/) is the easiest way for your users to connect to [over {process.env.PUBLIC_APPS}+ APIs](https://pipedream.com/apps), **right in your product**. You can build in-app messaging, CRM syncs, AI-driven products, [and much more](/connect/use-cases/), all in a few minutes. Visit [the quickstart](/connect/quickstart/) to build your first integration. + +Read more about the Connect API in the [Connect API docs](/connect/api/). + +## Events + +### Get Source Events + Retrieve up to the last 100 events emitted by a source. #### Endpoint @@ -523,8 +758,6 @@ GET /sources/{id}/event_summaries?limit=10 ### Delete source events ---- - Deletes all events, or a specific set of events, tied to a source. By default, making a `DELETE` request to this endpoint deletes **all** events @@ -532,7 +765,7 @@ associated with a source. To delete a specific event, or a range of events, you can use the `start_id` and `end_id` parameters. These IDs can be retrieved by using the [`GET /sources/{id}/event_summaries` -endpoint](/api/rest/#get-source-events), and are tied to the timestamp at which +endpoint](/rest-api/#get-source-events), and are tied to the timestamp at which the event was emitted — e.g. `1589486981597-0`. They are therefore naturally ordered by time. @@ -544,8 +777,6 @@ DELETE /sources/{id}/events #### Parameters ---- - `start_id` **string** The event ID from which you'd like to start deleting events. @@ -591,8 +822,6 @@ curl -X DELETE \ The request will delete the **first two events**. ---- - #### Example Request You can delete a single event by passing its event ID in both the value of the @@ -609,100 +838,112 @@ curl -X DELETE \ Deletion happens asynchronously, so you'll receive a `202 Accepted` HTTP status code in response to any deletion requests. -### List Projects +## OAuth + +### Get a new access token -Programmatically list the workspace's projects. +Exchanges a client ID and client secret for a new access token. #### Endpoint ``` -GET /v1/workspaces//projects +POST /oauth/token ``` -#### Path Parameters +#### Parameters -`workspaces_id` **string** +`grant_type` **string** -[Switch to your workspace's context](/workspaces/#switching-between-workspaces) and [find your org's ID](/workspaces/#finding-your-workspace-s-id). +The OAuth grant type. For Pipedream, this is always `client_credentials`. -#### Example Responses +--- -``` - "data": [ - { - "id": "proj_kYRs18", - "hid": "proj_kYRs18", - "name": "Sample Project", - "repository_id": null, - "repository_path": null, - "created_at": "2024-02-05T21:47:24.000Z", - "updated_at": "2024-02-06T16:13:07.000Z", - "org_id": 1, - "exceptions": null, - "allow_support": false - } - ] +`client_id` **string** + +The client ID of the OAuth app. + +--- + +`client_secret` **string** + +The client secret of the OAuth app. + +#### Example Request + +```bash +curl https://api.pipedream.com/v1/oauth/token \ + -H 'Content-Type: application/json' \ + -d '{ "grant_type": "client_credentials", "client_id": "", "client_secret": "" }' ``` -## Sources +#### Example Response -Event sources run code to collect events from an API, or receive events via -webhooks, emitting those events for use on Pipedream. Event sources can function -as workflow triggers. [Read more here](/sources/). +```json +{ + "access_token": "", + "token_type": "Bearer", + "expires_in": 3600, + "created_at": 1645142400 +} +``` -### List Current User Sources +### Revoke an access token ---- +Revokes an access token, rendering it invalid for future requests. #### Endpoint ``` -GET /users/me/sources/ +POST /oauth/revoke ``` #### Parameters -_No parameters_ +`token` **string** + +The access token to revoke. + +--- + +`client_id` **string** + +The client ID of the OAuth app. + +--- + +`client_secret` **string** + +The client secret of the OAuth app. + +--- #### Example Request -```shell -curl 'https://api.pipedream.com/v1/users/me/sources' \ - -H 'Authorization: Bearer ' +```bash +curl https://api.pipedream.com/v1/oauth/revoke \ + -H 'Content-Type: application/json' \ + -d '{ "token": "", "client_id": "", "client_secret": "" }' ``` #### Example Response +This endpoint will return a `200 OK` response with an empty body if the token was successfully revoked: + ```json -{ - "page_info": { - "total_count": 19, - "count": 10, - "start_cursor": "ZGNfSzB1QWVl", - "end_cursor": "ZGNfeUx1alJx" - }, - "data": [ - { - "id": "dc_abc123", - "component_id": "sc_def456", - "configured_props": { - "http": { - "endpoint_url": "https://myendpoint.m.pipedream.net" - } - }, - "active": true, - "created_at": 1587679599, - "updated_at": 1587764467, - "name": "test", - "name_slug": "test" - } - ] -} +{} ``` +## Sources + +Event sources run code to collect events from an API, or receive events via +webhooks, emitting those events for use on Pipedream. Event sources can function +as workflow triggers. [Read more here](/workflows/building-workflows/triggers/). + ### Create a Source ---- + +This endpoint is only available when using [user API keys](/rest-api/auth/#user-api-keys), not yet for workspace [OAuth tokens](/rest-api/auth/#oauth). + #### Endpoint @@ -712,18 +953,16 @@ POST /sources/ #### Parameters ---- - `component_id` **string** (_optional_) The ID of a component previously created in your account. [See the component -endpoints](/api/rest/#components) for information on how to retrieve this ID. +endpoints](/rest-api/#components) for information on how to retrieve this ID. --- `component_code` **string** (_optional_) -The full code for a [Pipedream component](/components/api/). +The full code for a [Pipedream component](/workflows/contributing/components/api/). --- @@ -732,7 +971,7 @@ The full code for a [Pipedream component](/components/api/). A reference to the URL where the component is hosted. For example, to create an RSS component, pass -`https://github.com/PipedreamHQ/pipedream/blob/master/components/rss/sources/new-item-in-feed/new-item-in-feed.js`. +`https://github.com/PipedreamHQ/pipedream/blob/master/components/rss/sources/new-item-in-feed/new-item-in-feed.ts`. --- @@ -747,16 +986,16 @@ as metadata to identify the location of the code. The name of the source. If absent, this defaults to using the [name -slug](/components/api/#component-structure) +slug](/workflows/contributing/components/api/#component-structure) of the component used to create the source. #### Example Request ```shell curl https://api.pipedream.com/v1/sources \ - -H "Authorization: Bearer " \ + -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ - -d '{"component_url": "https://github.com/PipedreamHQ/pipedream/blob/master/components/rss/sources/new-item-in-feed/new-item-in-feed.js", "name": "your-name-here", "configured_props": { "url": "https://rss.m.pipedream.net", "timer": { "intervalSeconds": 60 }}}' + -d '{"component_url": "https://github.com/PipedreamHQ/pipedream/blob/master/components/rss/sources/new-item-in-feed/new-item-in-feed.ts", "name": "your-name-here", "configured_props": { "url": "https://rss.m.pipedream.net", "timer": { "intervalSeconds": 60 }}}' ``` #### Example Response @@ -785,12 +1024,8 @@ Example response from creating an RSS source that runs once a minute: } ``` ---- - ### Update a source ---- - #### Endpoint ``` @@ -799,19 +1034,17 @@ PUT /sources/{id} #### Parameters ---- - `component_id` **string** (_optional_) The ID of a component previously created in your account. [See the component -endpoints](/api/rest/#components) for information on how to retrieve this ID. +endpoints](/rest-api/#components) for information on how to retrieve this ID. --- `component_code` **string** (_optional_) The full code for a [Pipedream -component](/components/api/). +component](/workflows/contributing/components/api/). --- @@ -820,7 +1053,7 @@ component](/components/api/). A reference to the URL where the component is hosted. For example, to create an RSS component, pass -`https://github.com/PipedreamHQ/pipedream/blob/master/components/rss/sources/new-item-in-feed/new-item-in-feed.js`. +`https://github.com/PipedreamHQ/pipedream/blob/master/components/rss/sources/new-item-in-feed/new-item-in-feed.ts`. --- @@ -834,7 +1067,7 @@ as metadata to identify the location of the code. The name of the source. -If absent, this defaults to using the [name slug](/components/api/#component-structure) +If absent, this defaults to using the [name slug](/workflows/contributing/components/api/#component-structure) of the component used to create the source. --- @@ -848,8 +1081,6 @@ Default: `true`. ### Delete a source ---- - #### Endpoint ``` @@ -858,17 +1089,17 @@ DELETE /sources/{id} ## Subscriptions -### Listen for events from another source or workflow + + The Subscriptions API is currently incompatible with projects that have [GitHub Sync](/workflows/git/) enabled. To [trigger another workflow](/workflows/building-workflows/code/nodejs/#invoke-another-workflow), use `$.flow.trigger` instead. + ---- +### Listen for events from another source or workflow You can configure a source or workflow to receive events from any number of other workflows or sources. For example, if you want a single workflow to run on 10 different RSS sources, you can configure the workflow to _listen_ for events from those 10 sources. ---- - #### Endpoint ``` @@ -877,8 +1108,6 @@ POST /subscriptions?emitter_id={emitting_component_id}&event_name={event_name}&l #### Parameters ---- - `emitter_id` **string** The ID of the workflow or component emitting events. Events from this component @@ -890,7 +1119,7 @@ workflows or components: - `p_*`: Listen to events from all workflows - `dc_*`: Listen to events from all event sources -[See the component endpoints](/api/rest/#components) for information on how to +[See the component endpoints](/rest-api/#components) for information on how to retrieve the ID of existing components. You can retrieve the ID of your workflow in your workflow's URL - it's the string `p_2gCPml` in `https://pipedream.com/@dylan/example-rss-sql-workflow-p_2gCPml/edit`. @@ -905,7 +1134,7 @@ in your workflow's URL - it's the string `p_2gCPml` in event_name= ``` -See [the `this.$emit` docs](/components/api/#emit) for more information on how to emit events on custom channels. +See [the `this.$emit` docs](/workflows/contributing/components/api/#emit) for more information on how to emit events on custom channels. Pipedream also exposes channels for logs and errors: @@ -919,13 +1148,11 @@ Pipedream also exposes channels for logs and errors: The ID of the component or workflow you'd like to receive events. -[See the component endpoints](/api/rest/#components) for information on how to +[See the component endpoints](/rest-api/#components) for information on how to retrieve the ID of existing components. You can retrieve the ID of your workflow in your workflow's URL - it's the string `p_2gCPml` in `https://pipedream.com/@dylan/example-rss-sql-workflow-p_2gCPml/edit`. ---- - #### Example Request You can configure workflow `p_abc123` to listen to events from the source @@ -934,14 +1161,12 @@ You can configure workflow `p_abc123` to listen to events from the source ```shell curl "https://api.pipedream.com/v1/subscriptions?emitter_id=dc_def456&listener_id=p_abc123" \ -X POST \ - -H "Authorization: Bearer " \ + -H "Authorization: Bearer " \ -H "Content-Type: application/json" ``` ### Automatically subscribe a listener to events from new workflows / sources ---- - You can use this endpoint to automatically receive events, like workflow errors, in another listening workflow or event source. Once you setup the auto-subscription, any new workflows or event sources you create will @@ -955,8 +1180,6 @@ endpoint](#listen-for-events-from-another-source-or-workflow). **Currently, this feature is enabled only on the API. The Pipedream UI will not display the sources configured as listeners using this API**. ---- - #### Endpoint ``` @@ -965,8 +1188,6 @@ POST /auto_subscriptions?event_name={event_name}&listener_id={receiving_source_i #### Parameters ---- - `event_name` **string** The name of the event stream whose events you'd like to receive: @@ -981,13 +1202,11 @@ The name of the event stream whose events you'd like to receive: The ID of the component or workflow you'd like to receive events. -[See the component endpoints](/api/rest/#components) for information on how to +[See the component endpoints](/rest-api/#components) for information on how to retrieve the ID of existing components. You can retrieve the ID of your workflow in your workflow's URL - it's the string `p_2gCPml` in `https://pipedream.com/@dylan/example-rss-sql-workflow-p_2gCPml/edit`. ---- - #### Example Request You can configure workflow `p_abc123` to listen to events from the source @@ -996,21 +1215,17 @@ You can configure workflow `p_abc123` to listen to events from the source ```shell curl "https://api.pipedream.com/v1/auto_subscriptions?event_name=$errors&listener_id=p_abc123" \ -X POST \ - -H "Authorization: Bearer " \ + -H "Authorization: Bearer " \ -H "Content-Type: application/json" ``` ### Delete a subscription ---- - Use this endpoint to delete an existing subscription. This endpoint accepts the same parameters as the [`POST /subscriptions` endpoint](#listen-for-events-from-another-source-or-workflow) for creating subscriptions. ---- - #### Endpoint ``` @@ -1019,8 +1234,6 @@ DELETE /subscriptions?emitter_id={emitting_component_id}&listener_id={receiving_ #### Parameters ---- - `emitter_id` **string** The ID of the workflow or component emitting events. Events from this component @@ -1032,7 +1245,7 @@ workflows or components: - `p_*`: Listen to events from all workflows - `dc_*`: Listen to events from all event sources -[See the component endpoints](/api/rest/#components) for information on how to +[See the component endpoints](/rest-api/#components) for information on how to retrieve the ID of existing components. You can retrieve the ID of your workflow in your workflow's URL - it's the string `p_2gCPml` in `https://pipedream.com/@dylan/example-rss-sql-workflow-p_2gCPml/edit`. @@ -1043,7 +1256,7 @@ in your workflow's URL - it's the string `p_2gCPml` in The ID of the component or workflow you'd like to receive events. -[See the component endpoints](/api/rest/#components) for information on how to +[See the component endpoints](/rest-api/#components) for information on how to retrieve the ID of existing components. You can retrieve the ID of your workflow in your workflow's URL - it's the string `p_2gCPml` in `https://pipedream.com/@dylan/example-rss-sql-workflow-p_2gCPml/edit`. @@ -1056,7 +1269,7 @@ The name of the event stream tied to your subscription. **If you didn't specify an `event_name` when creating your subscription, pass `event_name=`**. You'll find the `event_name` that's tied to your subscription when [listing your -subscriptions](#get-current-user-s-subscriptions): +subscriptions](#get-current-users-subscriptions): ```javascript { @@ -1073,8 +1286,6 @@ subscriptions](#get-current-user-s-subscriptions): } ``` ---- - #### Example Request You can delete a subscription you configured for workflow `p_abc123` to listen @@ -1083,15 +1294,17 @@ to events from the source `dc_def456` using the following command: ```shell curl "https://api.pipedream.com/v1/subscriptions?emitter_id=dc_def456&listener_id=p_abc123" \ -X DELETE \ - -H "Authorization: Bearer " \ + -H "Authorization: Bearer " \ -H "Content-Type: application/json" ``` ## Users -### Get Current User Info + +These endpoints only work when using [user API keys](/rest-api/auth/#user-api-keys), and will not work with workspace-level OAuth clients. + ---- +### Get Current User Info Retrieve information on the authenticated user. @@ -1109,7 +1322,7 @@ _No parameters_ ```bash curl 'https://api.pipedream.com/v1/users/me' \ - -H 'Authorization: Bearer ' + -H 'Authorization: Bearer ' ``` #### Example Response @@ -1168,119 +1381,18 @@ Paid user: } ``` -### Get Current User's Subscriptions - ---- - -Retrieve all the [subscriptions](#subscriptions) configured for the -authenticated user. - -#### Endpoint - -``` -GET /users/me/subscriptions -``` - -#### Parameters - -_No parameters_ - -#### Example Request - -```shell -curl 'https://api.pipedream.com/v1/users/me/subscriptions' \ - -H 'Authorization: Bearer ' -``` - -#### Example Response - -```json -{ - "data": [ - { - "id": "sub_abc123", - "emitter_id": "dc_abc123", - "listener_id": "p_abc123", - "event_name": "" - }, - { - "id": "sub_def456", - "emitter_id": "dc_def456", - "listener_id": "p_def456", - "event_name": "" - } - ] -} -``` - -### Get Current User's Webhooks - ---- - -Retrieve all the [webhooks](#webhooks) configured for the authenticated user. - -#### Endpoint - -``` -GET /users/me/webhooks -``` - -#### Parameters - -_No parameters_ - -#### Example Request - -```shell -curl 'https://api.pipedream.com/v1/users/me/webhooks' \ - -H 'Authorization: Bearer ' -``` - -#### Example Response - -```json -{ - "page_info": { - "total_count": 2, - "count": 2, - "start_cursor": "d2hfMjlsdUd6", - "end_cursor": "d2hfb3dHdWVv" - }, - "data": [ - { - "id": "wh_abc123", - "name": null, - "description": null, - "url": "https://endpoint.m.pipedream.net", - "active": true, - "created_at": 1611964025, - "updated_at": 1611964025 - }, - { - "id": "wh_def456", - "name": "Test webhook", - "description": "just a test", - "url": "https://endpoint2.m.pipedream.net", - "active": true, - "created_at": 1605835136, - "updated_at": 1605835136 - } - ] -} -``` - ## Webhooks Pipedream supports webhooks as a way to deliver events to a endpoint you own. Webhooks are managed at an account-level, and you send data to these webhooks using [subscriptions](#subscriptions). -For example, you can run a Twitter [event source](/sources/) that listens +For example, you can run a Twitter [event source](/workflows/building-workflows/triggers/) that listens for new tweets. If you [subscribe](#subscriptions) the webhook to this source, Pipedream will deliver those tweets directly to your webhook's URL without running a workflow. -[**See these tutorials**](/api/rest/webhooks/) for examples. +[**See these tutorials**](/rest-api/webhooks) for examples. ### Create a webhook @@ -1295,8 +1407,6 @@ POST /webhooks?url={your_endpoint_url}&name={name}&description={description} #### Parameters ---- - `url` **string** The endpoint URL where you'd like to deliver events. Any events sent to this @@ -1326,14 +1436,13 @@ mysql://user:pass@host:port `name` **string** The name you'd like to assign to this webhook, which will appear when [listing -your webhooks](#get-current-user-s-webhooks). +your webhooks](#get-current-users-webhooks). --- `description` **string** -The description you'd like to assign to this webhook, which will appear when -[listing your webhooks](#get-current-user-s-webhooks). +The description you'd like to assign to this webhook. #### Example Request @@ -1343,7 +1452,7 @@ You can create a webhook that delivers events to ```shell curl "https://api.pipedream.com/v1/webhooks?url=https://endpoint.m.pipedream.net&name=name&description=description" \ -X POST \ - -H "Authorization: Bearer " \ + -H "Authorization: Bearer " \ -H "Content-Type: application/json" ``` @@ -1368,15 +1477,8 @@ creating [subscriptions](#subscriptions). } ``` -### List webhooks - -You can list webhooks you've created in your account using the -[`/users/me/webhooks` endpoint](#get-current-user-s-webhooks) - ### Delete a webhook ---- - Use this endpoint to delete a webhook in your account. #### Endpoint @@ -1387,8 +1489,6 @@ DELETE /webhooks/{id} #### Path Parameters ---- - `id` **string** The ID of a webhook in your account. @@ -1400,15 +1500,21 @@ The ID of a webhook in your account. ```shell curl "https://api.pipedream.com/v1/webhooks/wh_abc123" \ -X DELETE \ - -H "Authorization: Bearer " \ + -H "Authorization: Bearer " \ -H "Content-Type: application/json" ``` ## Workflows +### Invoke workflow + +You can invoke workflows by making an HTTP request to a workflow's HTTP trigger. [See the docs on authorizing requests and invoking workflows](/workflows/building-workflows/triggers/#authorizing-http-requests) for more detail. + ### Create a Workflow ---- + +This endpoint is only available when using [user API keys](/rest-api/auth/#user-api-keys), not yet for workspace [OAuth tokens](/rest-api/auth/#oauth). + Creates a new workflow within an organization's project. This endpoint allows defining workflow steps, triggers, and settings, based on a supplied template. @@ -1422,7 +1528,7 @@ POST /workflows `org_id` **string** -[Switch to your workspace's context](/workspaces/#switching-between-workspaces) and [find your org's ID](/workspaces/#finding-your-workspace-s-id). +[Switch to your workspace's context](/workflows/workspaces/#switching-between-workspaces) and [find your org's ID](/workflows/workspaces/#finding-your-workspaces-id). --- @@ -1432,7 +1538,7 @@ The ID of the project where the new workflow will be created. To find your proje Click on the project where you'd like to create the new workflow, and the project ID can be found in the URL, starting with `proj_`. -If the URL is [https://pipedream.com/@pd-testing/projects/proj_GzsRY5N/tree](https://pipedream.com/@pd-testing/projects/proj_GzsRY5N/tree), your `project_id` is `proj_GzsRY5N`. +If the URL is [https://pipedream.com/@pd-testing/workflows/projects/proj_GzsRY5N/tree](https://pipedream.com/@pd-testing/workflows/projects/proj_GzsRY5N/tree), your `project_id` is `proj_GzsRY5N`. --- @@ -1674,7 +1780,9 @@ The ID of the workflow template to base the workflow on. To find a workflow's `t ### Update a Workflow ---- + +This endpoint is only available when using [user API keys](/rest-api/auth/#user-api-keys), not yet for workspace [OAuth tokens](/rest-api/auth/#oauth). + Updates the workflow's activation status. If you need to modify the workflow's steps, triggers, or connected accounts [consider making a new workflow](#create-a-workflow). @@ -1698,19 +1806,20 @@ If the URL is [https://pipedream.com/@michael-testing/api-p_13CDnxK/inspect](htt `active` **boolean** The activation status of a workflow. Set to `true` to activate the workflow, or `false` to deactivate it. +`org_id` **string** +[Find your org's ID](/workflows/workspaces/#finding-your-workspaces-id). + #### Example Request ```bash curl -X PUT 'https://api.pipedream.com/v1/workflows/p_abc123' \ - -H 'Authorization: Bearer ' \ + -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ - -d '{"active": false}' + -d '{"active": false, "org_id": "o_BYDI5y"}' ``` ### Get a Workflow's details ---- - Retrieves the details of a specific workflow within an organization's project. #### Endpoint @@ -1727,7 +1836,7 @@ GET /workflows/{workflow_id} ```bash curl 'https://api.pipedream.com/v1/workflows/p_abc123?org_id=o_abc123' \ - -H 'Authorization: Bearer ' + -H 'Authorization: Bearer ' ``` #### Example Response @@ -1780,10 +1889,8 @@ curl 'https://api.pipedream.com/v1/workflows/p_abc123?org_id=o_abc123' \ ### Get Workflow Emits ---- - Retrieve up to the last 100 events emitted from a workflow using -[`$send.emit()`](/destinations/emit/#emit-events). +[`$send.emit()`](/workflows/data-management/destinations/emit/#emit-events). #### Endpoint @@ -1811,7 +1918,7 @@ GET /v1/workflows/{workflow_id}/event_summaries?expand=event&limit=1 ```shell curl 'https://api.pipedream.com/v1/workflows/p_abc123/event_summaries?expand=event&limit=1' \ - -H 'Authorization: Bearer ' + -H 'Authorization: Bearer ' ``` #### Example Response @@ -1844,12 +1951,8 @@ curl 'https://api.pipedream.com/v1/workflows/p_abc123/event_summaries?expand=eve } ``` ---- - ### Get Workflow Errors ---- - Retrieve up to the last 100 events for a workflow that threw an error. The details of the error, along with the original event data, will be included @@ -1879,7 +1982,7 @@ GET /v1/workflows/{workflow_id}/$errors/event_summaries?expand=event&limit=1 ```shell curl 'https://api.pipedream.com/v1/workflows/p_abc123/$errors/event_summaries?expand=event&limit=1' \ - -H 'Authorization: Bearer ' + -H 'Authorization: Bearer ' ``` #### Example Response @@ -1930,7 +2033,7 @@ curl 'https://api.pipedream.com/v1/workflows/p_abc123/$errors/event_summaries?ex ## Workspaces -[Workspaces](/workspaces/) provide your team a way to manage resources in a shared workspace. Any resources created by the workspace are owned by the workspace and accessible to its members. +[Workspaces](/workflows/workspaces/) provide your team a way to manage resources in a shared workspace. Any resources created by the workspace are owned by the workspace and accessible to its members. ### Get a Workspace @@ -1939,14 +2042,14 @@ Programmatically view your workspace's current credit usage for the billing peri #### Endpoint ``` -GET /v1/workspaces/ +GET /v1/workflows/workspaces/ ``` #### Path Parameters `workspaces_id` **string** -[Switch to your workspace's context](/workspaces/#switching-between-workspaces) and [find your org's ID](/workspaces/#finding-your-workspace-s-id). +[Switch to your workspace's context](/workflows/workspaces/#switching-between-workspaces) and [find your org's ID](/workflows/workspaces/#finding-your-workspaces-id). #### Example Response @@ -1965,21 +2068,19 @@ GET /v1/workspaces/ ### Get Workspaces's Connected Accounts ---- - Retrieve all the connected accounts for a specific workspace. #### Endpoint ``` -GET /workspaces//accounts +GET /workflows/workspaces//accounts ``` #### Path Parameters -`workspace_id` **string** +`org_id` **string** -[Switch to your workspace's context](/workspaces/#switching-between-workspaces) and [find your org's ID](/workspaces/#finding-your-workspace-s-id). +[Switch to your workspace's context](/workflows/workspaces/#switching-between-workspaces) and [find your org's ID](/workflows/workspaces/#finding-your-workspaces-id). #### Query Parameters @@ -1990,8 +2091,8 @@ To look up the connected account information for a specific app, use the `query` #### Example Request ```shell -curl 'https://api.pipedream.com/v1/workspaces/o_abc123/accounts?query=google_sheets' \ - -H 'Authorization: Bearer ' +curl 'https://api.pipedream.com/v1/workflows/workspaces/o_abc123/accounts?query=google_sheets' \ + -H 'Authorization: Bearer ' ``` #### Example Response @@ -2015,27 +2116,25 @@ curl 'https://api.pipedream.com/v1/workspaces/o_abc123/accounts?query=google_she ### Get Workspaces's Subscriptions ---- - Retrieve all the [subscriptions](#subscriptions) configured for a specific workspace. #### Endpoint ``` -GET /workspaces//subscriptions +GET /workflows/workspaces//subscriptions ``` #### Path Parameters `workspaces_id` **string** -[Switch to your workspace's context](/workspaces/#switching-between-workspaces) and [find your org's ID](/workspaces/#finding-your-workspace-s-id). +[Switch to your workspace's context](/workflows/workspaces/#switching-between-workspaces) and [find your org's ID](/workflows/workspaces/#finding-your-workspaces-id). #### Example Request ```shell -curl 'https://api.pipedream.com/v1/workspaces/o_abc123/subscriptions' \ - -H 'Authorization: Bearer ' +curl 'https://api.pipedream.com/v1/workflows/workspaces/o_abc123/subscriptions' \ + -H 'Authorization: Bearer ' ``` #### Example Response @@ -2061,27 +2160,25 @@ curl 'https://api.pipedream.com/v1/workspaces/o_abc123/subscriptions' \ ### Get Workspaces's Sources ---- - Retrieve all the [event sources](#sources) configured for a specific workspace. #### Endpoint ``` -GET /orgs//sources +GET /orgs//sources ``` #### Path Parameters `org_id` **string** -[Switch to your workspace's context](/workspaces/#switching-between-workspaces) and [find your org's ID](/workspaces/#finding-your-workspace-s-id). +[Switch to your workspace's context](/workflows/workspaces/#switching-between-workspaces) and [find your org's ID](/workflows/workspaces/#finding-your-workspaces-id). #### Example Request ```shell curl 'https://api.pipedream.com/v1/orgs/o_abc123/sources' \ - -H 'Authorization: Bearer ' + -H 'Authorization: Bearer ' ``` #### Example Response diff --git a/docs-v2/pages/rest-api/rss.mdx b/docs-v2/pages/rest-api/rss.mdx index 2053dbfcb7c47..306d69ebcf7af 100644 --- a/docs-v2/pages/rest-api/rss.mdx +++ b/docs-v2/pages/rest-api/rss.mdx @@ -1,8 +1,8 @@ # REST API example: Create an RSS source -Here, we'll walk through an example of how to create an RSS [event source](/sources/) and retrieve events from that source using the [REST API](/api/rest/). +Here, we'll walk through an example of how to create an RSS [event source](/workflows/building-workflows/triggers/) and retrieve events from that source using the [REST API](/rest-api/). -Before you begin, you'll need your [Pipedream API Key](/rest-api/auth/#pipedream-api-key). +Before you begin, you'll need your [Pipedream API Key](/rest-api/auth/#user-api-keys). ## Find the details of the source you'd like to create @@ -13,7 +13,7 @@ To create an event source using Pipedream's REST API, you'll need two things: You can find the `key` by reviewing the code for the source, [in Pipedream's Github repo](https://github.com/PipedreamHQ/pipedream/tree/master/components). -In the `components/` directory, you'll see a list of apps. Navigate to the app-specific directory for your source, then visit the `sources/` directory in that dir to find your source. For example, to create an RSS source, visit the [`components/rss/sources/new-item-in-feed/new-item-in-feed.js` source](https://github.com/PipedreamHQ/pipedream/blob/master/components/rss/sources/new-item-in-feed/new-item-in-feed.js). +In the `components/` directory, you'll see a list of apps. Navigate to the app-specific directory for your source, then visit the `sources/` directory in that dir to find your source. For example, to create an RSS source, visit the [`components/rss/sources/new-item-in-feed/new-item-in-feed.js` source](https://github.com/PipedreamHQ/pipedream/blob/master/components/rss/sources/new-item-in-feed/new-item-in-feed.ts). The `key` is a globally unique identifier for the source. You'll see the `key` for this source near the top of the file: @@ -118,7 +118,7 @@ curl -H "Authorization: Bearer " \ "https://api.pipedream.com/sources/dc_abc123/sse" ``` -[See the SSE docs for more detail on this interface](/api/sse/). +[See the SSE docs for more detail on this interface](/workflows/data-management/destinations/sse/). ### REST API @@ -129,4 +129,4 @@ curl -H "Authorization: Bearer " \ "https://api.pipedream.com/v1/sources/dc_BVuN2Q/event_summaries" ``` -[See the docs on the `/event_summaries` endpoint](/api/rest/#get-source-events) for more details on the parameters it accepts. For example, you can pass a `limit` param to return only `N` results per page, and paginate over results using the `before` and `after` cursors described in the [pagination docs](/api/rest/#pagination). +[See the docs on the `/event_summaries` endpoint](/rest-api/#get-source-events) for more details on the parameters it accepts. For example, you can pass a `limit` param to return only `N` results per page, and paginate over results using the `before` and `after` cursors described in the [pagination docs](/rest-api/#pagination). diff --git a/docs-v2/pages/rest-api/webhooks.mdx b/docs-v2/pages/rest-api/webhooks.mdx index cd06271ec60c9..c7b974d427876 100644 --- a/docs-v2/pages/rest-api/webhooks.mdx +++ b/docs-v2/pages/rest-api/webhooks.mdx @@ -1,28 +1,28 @@ # REST API Example: Webhooks -Pipedream supports webhooks as a way to deliver events to a endpoint you own. Webhooks are managed at an account-level, and you send data to these webhooks using [subscriptions](/api/rest/#subscriptions). +Pipedream supports webhooks as a way to deliver events to an endpoint you own. Webhooks are managed at an account-level, and you send data to these webhooks using [subscriptions](/rest-api/#subscriptions). -For example, you can run a Twitter [event source](/sources/) that listens for new tweets. If you [subscribe](/api/rest/#subscriptions) the webhook to this source, Pipedream will deliver those tweets directly to your webhook's URL without running a workflow. +For example, you can run a Twitter [event source](/workflows/building-workflows/triggers/) that listens for new tweets. If you [subscribe](/rest-api/#subscriptions) the webhook to this source, Pipedream will deliver those tweets directly to your webhook's URL without running a workflow. ## Send events from an existing event source to a webhook -[Event sources](/sources/) source data from a service / API, emitting events that can trigger Pipedream workflows. For example, you can run a Github event source that emits an event anytime someone stars your repo, triggering a workflow on each new star. +[Event sources](/workflows/building-workflows/triggers/) source data from a service / API, emitting events that can trigger Pipedream workflows. For example, you can run a Github event source that emits an event anytime someone stars your repo, triggering a workflow on each new star. **You can also send the events emitted by an event source to a webhook**. -![Github stars to Pipedream](./images/webhook-proxy.png) +![Github stars to Pipedream](/images/rest-api/webhook-proxy.png) ### Step 1 - retrieve the source's ID First, you'll need the ID of your source. You can visit [https://pipedream.com/sources](https://pipedream.com/sources), select a source, and copy its ID from the URL. It's the string that starts with `dc_`: -![Source ID](./images/source-id.png) +![Source ID](/images/rest-api/source-id.png) -You can also find the ID by running `pd list sources` using [the CLI](/cli/reference/#pd-list). +You can also find the ID by running `pd list sources` using [the CLI](/workflows/cli/reference/#pd-list). ### Step 2 - Create a webhook -You can create a webhook using the [`POST /webhooks` endpoint](/api/rest/#create-a-webhook). The endpoint accepts 3 params: +You can create a webhook using the [`POST /webhooks` endpoint](/rest-api/#create-a-webhook). The endpoint accepts 3 params: - `url`: the endpoint to which you'd like to deliver events - `name`: a name to assign to the webhook, for your own reference @@ -50,9 +50,9 @@ Successful API responses contain a webhook ID in `data.id` — the string that s ### Step 3 - Create a subscription -[Subscriptions](/api/rest/#subscriptions) allow you to deliver events from one Pipedream resource to another. In the language of subscriptions, the webhook will **listen** for events **emitted** by the event source. +[Subscriptions](/rest-api/#subscriptions) allow you to deliver events from one Pipedream resource to another. In the language of subscriptions, the webhook will **listen** for events **emitted** by the event source. -You can make a request to the [`POST /subscriptions` endpoint](/api/rest/#listen-for-events-from-another-source-or-workflow) to create this subscription. This endpoint requires two params: +You can make a request to the [`POST /subscriptions` endpoint](/rest-api/#listen-for-events-from-another-source-or-workflow) to create this subscription. This endpoint requires two params: - `emitter_id`: the source ID from **Step 1** - `listener_id`: the webhook ID from **Step 2** @@ -66,7 +66,7 @@ curl "https://api.pipedream.com/v1/subscriptions?emitter_id=dc_abc123&listener_i -H "Content-Type: application/json" ``` -If successful, this endpoint should return a `200 OK` with metadata on the subscription. You can also [list your subscriptions](/api/rest/#get-current-user-s-subscriptions) to confirm that it's active. +If successful, this endpoint should return a `200 OK` with metadata on the subscription. ### Step 4 - Trigger an event @@ -74,7 +74,6 @@ Trigger an event in your source (for example, send a tweet, star a Github repo, ## Extending these ideas -You can configure _any_ events to be delivered to a webhook: events emitted by event source, or those [emitted by a workflow](/destinations/emit/). +You can configure _any_ events to be delivered to a webhook: events emitted by event source, or those [emitted by a workflow](/workflows/data-management/destinations/emit/). You can also configure an event to be delivered to _multiple_ webhooks by creating multiple webhooks / subscriptions. - diff --git a/docs-v2/pages/rest-api/workflows.mdx b/docs-v2/pages/rest-api/workflows.mdx index ce418941f6cd9..30fcc40b6d6de 100644 --- a/docs-v2/pages/rest-api/workflows.mdx +++ b/docs-v2/pages/rest-api/workflows.mdx @@ -3,17 +3,17 @@ import { Steps } from 'nextra/components' # Example: Create a Workflow -Here, we'll walk through an example of how to create a [workflow](/workflows/) programmatically using the [create workflow endpoint](/api/rest/#create-a-workflow) from a [workflow share link](/workflows/sharing/), and pass your own connected accounts, step and trigger props as configuration. +Here, we'll walk through an example of how to create a [workflow](/workflows/building-workflows/) programmatically using the [create workflow endpoint](/rest-api/#create-a-workflow) from a [workflow share link](/workflows/building-workflows/sharing/), and pass your own connected accounts, step and trigger props as configuration. -Before you begin, you'll need your [Pipedream API Key](/rest-api/auth/#pipedream-api-key). +Before you begin, you'll need your [Pipedream API Key](/rest-api/auth/#user-api-keys). ## Creating a new workflow from a template -Workflows can be shared as templates using a [Workflow Share Link](/workflows/sharing/). When you share a workflow, a unique key is created that represents that workflow's triggers, steps and settings. +Workflows can be shared as templates using a [Workflow Share Link](/workflows/building-workflows/sharing/). When you share a workflow, a unique key is created that represents that workflow's triggers, steps and settings. However, opening workflow share link with a browser will not include sharing private resources - such as connected accounts, sources and data stores. Connections to your private resources have to be populated by hand. -The [create workflow endpoint](/api/rest/#create-a-workflow) allows you to programmatically assign your own connected accounts, props within the workflow, and even deploy the workflow in a single API request. +The [create workflow endpoint](/rest-api/#create-a-workflow) allows you to programmatically assign your own connected accounts, props within the workflow, and even deploy the workflow in a single API request. @@ -29,7 +29,7 @@ https://pipedream.com/new?h=tch_abc123 The `tch_abc123` portion of the URL represents the unique workflow template ID. Copy this, you'll need it in the following steps. - + **You can create workflows from any workflow template** You're not limited to creating new workflows from your own templates, you can create your own workflows using this endpoint with any workflow share link. @@ -37,7 +37,7 @@ You're not limited to creating new workflows from your own templates, you can cr This guide will also work for any workflow share link, although we recommend copying the workflow to your account first so you can view the workflow's available configurable props. -### Create the workflow, and view the parameters +### Create the workflow, and view the parameters You'll need to view the original workflow's configuration so you can identify the props you'll need to provide for the new version of the workflow. @@ -48,7 +48,7 @@ In the Get Workflow API response, you'll see two properties: * `triggers` - represents the triggers for the workflow. * `steps` - represents the series of steps within your workflow -`triggers` and `steps` contain [props](/workflows/steps/using-props/) that define the connected accounts as well as configuration. +`triggers` and `steps` contain [props](/workflows/building-workflows/using-props/) that define the connected accounts as well as configuration. The next step is to learn how we can pass our specific connected accounts to app based props in the `steps` and/or `triggers` of the workflow template. @@ -109,7 +109,7 @@ For the example workflow above, the RSS feed trigger has a `url` property, and t Now that we have the names of the configurable props for both the `triggers` and `steps` of the workflow, let's design the payload for creating a new instance of the workflow. -First, populate the `project_id` and `org_id` where you'd like this new workflow to be instantiated under. Please refer to the [**Create Workflow** parameters documentation](/api/rest/#create-a-workflow) on how to find these values. +First, populate the `project_id` and `org_id` where you'd like this new workflow to be instantiated under. Please refer to the [**Create Workflow** parameters documentation](/rest-api/#create-a-workflow) on how to find these values. The `template_id` for your workflow can be found from the URL of the workflow share link you created in **Step 1** of this guide. @@ -131,7 +131,7 @@ The `trigger` as a `url` prop, so let's provide it with a specific URL (`https:/ } ``` - + **Triggers are addressable by index** You may have noticed that we didn't include the `namespace` argument to the trigger in our payload. This is because triggers are ordered sequentially, whereas steps need a `namespace` argument for proper addressing. @@ -139,7 +139,7 @@ You may have noticed that we didn't include the `namespace` argument to the trig If we were to send this payload to the **Create Workflow** endpoint now, it will populate the *RSS - New Item in Feed* trigger with the feed we provided. -You can also populate the `steps` props. +You can also populate the `steps` props. The **Slack - Send message in a Public Channel** step requires a `channelId`, `message` and the connected Slack account (`slack`). Let's start with connecting the Slack account. @@ -147,12 +147,12 @@ The **Slack - Send message in a Public Channel** step requires a `channelId`, `m To connect your accounts to the workflow, you'll need to find the specific IDs for each of the accounts you'd like to connect. -You can find your connected account IDs by using the [List Accounts endpoint](/api/rest/#get-workspaces-s-connected-accounts). +You can find your connected account IDs by using the [List Accounts endpoint](/rest-api/#get-workspacess-connected-accounts). You can filter your accounts by using the `query` query parameter. For example, if you want to find your connected Slack accounts to your workspace, then add `slack` to the query param: ``` -GET /workspaces//accounts?query=slack +GET /workflows/workspaces//accounts?query=slack ``` This request will narrow down the results to your connected Slack accounts, for easier finding. @@ -252,4 +252,4 @@ Finally, send the request to create this new workflow with this payload we've de You should see the new workflow within your Pipedream dashboard under the workspace and project defined in the payload. You can use this request to dynamically create new instances of the same workflow with different props, connected accounts and settings. - \ No newline at end of file + diff --git a/docs-v2/pages/scheduling-future-tasks/_meta.json b/docs-v2/pages/scheduling-future-tasks/_meta.json deleted file mode 100644 index 5e6a93c3a56e9..0000000000000 --- a/docs-v2/pages/scheduling-future-tasks/_meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "index": { - "display": "hidden" - } -} diff --git a/docs-v2/pages/scheduling-future-tasks/index.mdx b/docs-v2/pages/scheduling-future-tasks/index.mdx deleted file mode 100644 index bc9b0c8eac531..0000000000000 --- a/docs-v2/pages/scheduling-future-tasks/index.mdx +++ /dev/null @@ -1,180 +0,0 @@ -# Scheduling future tasks - -Pipedream includes an event source which exposes an HTTP API for scheduling one-time tasks. You can schedule tasks at any timestamp, with second-level precision, up to one year in the future. - -[Click here to create this source](https://pipedream.com/sources?action=create&key=pipedream-new-scheduled-tasks), or visit [https://pipedream.com/sources/new](https://pipedream.com/sources/new), select the **Pipedream** app, and the **New Scheduled Tasks** source. - -To [schedule a new task](#scheduling-a-task), just send an HTTP `POST` request to your source's endpoint, at the `/schedule` path, with the following format: - -```javascript -{ - "timestamp": "2020-08-21T04:29:00.951Z", // timestamp: an ISO 8601 timestamp - "message": { "name": "Luke" } // message: any object or string -} -``` - -When the timestamp arrives and the task is invoked, the source will emit the payload passed in your original, scheduled request. This allows you to trigger [a Pipedream workflow](/workflows/) at the scheduled time, passing the `message` and `timestamp` to the workflow as an [incoming event](/workflows/events/). - -You can also listen for these events in your own app / infra, by [subscribing to your source's SSE stream](/api/sse/). Each time a scheduled task is emitted from your Pipedream source, it also emits a message to that SSE stream. Any application (a Docker container, a Rails app, etc.) listening to that SSE stream can react to that message to run whatever code you'd like. - - - -## HTTP API - -This source exposes an HTTP endpoint where you can send `POST` requests to schedule new tasks. Your endpoint URL should appear as the **Endpoint** in your source's details, in the **Events** tab. - -### Scheduling a task - -``` -POST /schedule -``` - -#### Pipedream built-in action - -Use the **Pipedream Task Scheduler - Schedule Task** Helper Functions action to schedule a new task. - -#### Node.js - -Use the following code in a Node.js code step: - -```javascript -import { axios } from "@pipedream/platform" - -export default defineComponent({ - props: { - numSeconds: { - type: "integer", - label: "Num Seconds", - description: "How many seconds in the future would you like to schedule the task?", - }, - secret: { - type: "string", - optional: true, - }, - taskSchedulerURL: { - type: "string", - label: "Task Scheduler URL", - description: "Enter the URL as it appears in the **Endpoint** field of your Task Scheduler source", - }, - message: { - type: "string", - description: "The message / payload to send to your task scheduler. Can be any string or JavaScript object. This message will be emitted by the task scheduler at the specified number of seconds in the future.", - }, - }, - async run({ $ }) { - const timestamp = (new Date(+new Date() + (this.numSeconds * 1000))).toISOString() - - const headers = { - "Content-Type": "application/json", - }; - if (this.secret) { - headers["x-pd-secret"] = this.secret; - } - - return await axios($, { - url: `${this.taskSchedulerURL}/schedule`, - method: "POST", - headers, - data: { - timestamp, - message: this.message, - }, - }); - }, -}) -``` - -#### `cURL` - -To schedule a new task, `POST` a JSON object with an [ISO 8601](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) `timestamp` and a `message` to the **`/schedule` path** of your source's HTTP endpoint: - -```javascript -{ - "timestamp": "2020-08-21T04:29:00.951Z", // timestamp: an ISO 8601 timestamp - "message": { "name": "Luke" } // message can be any object or string -} -``` - -Optionally, if you configured a secret in your source, you'll need to pass that in the `x-pd-secret` header. For example: - -```bash -curl -X POST \ - -H 'Content-Type: application/json' \ - -H 'x-pd-secret: 123' \ - -d '{ "timestamp": "2020-09-18T04:40:59Z", "message": "foo" }' \ - https://endpoint.m.pipedream.net/schedule -``` - -Successful task schedule requests yield a `200 OK` response, noting the task was successfully scheduled. - -### Cancelling a scheduled task - -``` -POST /cancel -``` - -When you schedule a task, you'll receive a unique ID assigned to that task in the `id` field of the HTTP response body. That `id` can be passed to the `/cancel` endpoint to cancel that task before its scheduled time arrives. - -#### Node.js - -Cancel tasks using the following code in a Node.js code step: - -```javascript -import { axios } from "@pipedream/platform" - -export default defineComponent({ - props: { - secret: { - type: "string", - optional: true, - }, - taskSchedulerURL: { - type: "string", - label: "Task Scheduler URL", - description: "Enter the URL as it appears in the **Endpoint** field of your Task Scheduler source", - }, - taskId: { - type: "string", - description: "The ID of the task you'd like to cancel", - }, - }, - async run({ $ }) { - const headers = { - "Content-Type": "application/json", - }; - if (this.secret) { - headers["x-pd-secret"] = this.secret; - } - - return await axios($, { - url: `${this.taskSchedulerURL}/cancel`, - method: "DELETE", - headers, - data: { - id: this.taskId, - }, - }); - }, -}) -``` - -#### `cURL` - -```bash -curl -X POST \ - -H 'Content-Type: application/json' \ - -H 'x-pd-secret: 123' \ - -d '{ "id": "8fceb45b-0241-4d04-9f3f-334679586370" }' \ - https://endpoint.m.pipedream.net/cancel -``` - -## Processing scheduled tasks - -Scheduled tasks are emitted by the event source as events, which you can consume with - -- [Pipedream workflows](/workflows/) -- [A source-specific SSE stream](/api/sse/) -- [The Pipedream REST API](/api/rest/) -- [The Pipedream CLI](/cli/reference/#installing-the-cli) - -[See the docs on consuming events from sources](/sources/#consuming-events-from-sources) for more information. \ No newline at end of file diff --git a/docs-v2/pages/shopify-faq-2023-10/_meta.json b/docs-v2/pages/shopify-faq-2023-10/_meta.json deleted file mode 100644 index 5e6a93c3a56e9..0000000000000 --- a/docs-v2/pages/shopify-faq-2023-10/_meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "index": { - "display": "hidden" - } -} diff --git a/docs-v2/pages/status/_meta.json b/docs-v2/pages/status/_meta.json deleted file mode 100644 index 5e6a93c3a56e9..0000000000000 --- a/docs-v2/pages/status/_meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "index": { - "display": "hidden" - } -} diff --git a/docs-v2/pages/status/index.mdx b/docs-v2/pages/status/index.mdx deleted file mode 100644 index ff72f4cd94252..0000000000000 --- a/docs-v2/pages/status/index.mdx +++ /dev/null @@ -1,6 +0,0 @@ -# Service Status - -Pipedream operates a status page at [https://status.pipedream.com](https://status.pipedream.com). That page displays the uptime history and current status of every Pipedream service. - -When incidents occur, updates are published to the **#incidents** channel of [Pipedream's Slack Community](https://pipedream.com/support) and to the [@PipedreamStatus](https://twitter.com/PipedreamStatus) account on Twitter. On the status page itself, you can also subscribe to updates directly. - diff --git a/docs-v2/pages/subprocessors/_meta.json b/docs-v2/pages/subprocessors/_meta.json deleted file mode 100644 index 5e6a93c3a56e9..0000000000000 --- a/docs-v2/pages/subprocessors/_meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "index": { - "display": "hidden" - } -} diff --git a/docs-v2/pages/troubleshooting.mdx b/docs-v2/pages/troubleshooting.mdx deleted file mode 100644 index 9f1c7439be9ae..0000000000000 --- a/docs-v2/pages/troubleshooting.mdx +++ /dev/null @@ -1,261 +0,0 @@ -import Callout from '@/components/Callout' - -# Troubleshooting Common Issues - -This doc describes some common solutions for fixing issues with [pipedream.com](https://pipedream.com) or with a specific workflow. - -## A feature isn't working on pipedream.com - -If you're seeing an issue with [pipedream.com](https://pipedream.com) (for example, the site won't load, or you think you've found a bug), try each of the following steps, checking to see if they fix the problem: - -1. [Hard refresh](https://fabricdigital.co.nz/blog/how-to-hard-refresh-your-browser-and-clear-cache) pipedream.com in your browser. - -2. Log out of your pipedream.com account, and log back in. - -3. [Disable your browser extensions](https://www.computerhope.com/issues/ch001411.htm) or use features like [Chrome Guest mode](https://support.google.com/chrome/answer/6130773?hl=en&co=GENIE.Platform%3DAndroid) to browse pipedream.com without any existing extensions / cookies / cache. - -If you're still seeing the issue after trying these steps, please [report a bug](https://github.com/PipedreamHQ/pipedream/issues/new?assignees=&labels=bug&template=bug_report.md&title=%5BBUG%5D+). - -## My workflow isn't working - -If you're encountering a specific issue in a workflow, try the following steps, checking to see if they fix the problem: - -1. Make a trivial change to your workflow, and **Deploy** your workflow again. - -2. Try searching [the community](https://pipedream.com/support) or [the `pipedream` GitHub repo](https://github.com/PipedreamHQ/pipedream/issues) to see if anyone else has solved the same issue. - -If you're still seeing the issue after trying these steps, please reach out in [the community](https://pipedream.com/support). - -## Why is my trigger not emitting events? - -Most Pipedream sources fall into one of two categories: webhook-based or timer-based. - -### Webhook-based instant sources - -These sources will get triggered immediately. But because events come in in real-time, most will **not** automatically fetch historical events upon creation. To surface test events in your workflow while building, you'll need to generate an eligible event in the selected app. - -For example, if you've configured the "[Message Updates (Instant)](https://pipedream.com/apps/telegram-bot-api/triggers/message-updates) Telegram source, you'll need to send a message in the Telegram account you've selected in order for an event to appear. - -![Select an event](https://res.cloudinary.com/pipedreamin/image/upload/v1653434586/docs/webhook-triggers-select-event_qj7nlp.png) - -Sources for apps like [Telegram](https://pipedream.com/apps/telegram-bot-api/triggers/message-updates) and [Google Sheets](https://pipedream.com/apps/google-sheets/triggers/new-row-added) use webhooks and get triggered immediately. - -### Timer-based polling sources - -These sources will fetch new events on a regular interval, based on a schedule you specify in the trigger configuration. - -![Configure polling timer](https://res.cloudinary.com/pipedreamin/image/upload/v1653434591/docs/polling-triggers-timer_ooz26c.png) - -In most cases, Pipedream will automatically fetch recent historical events to help enable easier workflow development. Sources for apps like [Twitter](https://pipedream.com/apps/twitter/triggers/search-mentions) and [Spotify](https://pipedream.com/apps/spotify/triggers/new-playlist) require we poll their endpoints in order to fetch new events. - -## Where do I find my workflow's ID? - -Open [https://pipedream.com](https://pipedream.com) and visit your workflow. Copy the URL that appears in your browser's address bar. For example: - -``` -https://pipedream.com/@dylburger/p_abc123/edit -``` - -Your workflow's ID is the value that starts with `p_`. In this example: `p_abc123`. - -## Where do I find my event source's ID? - -Open [https://pipedream.com/sources](https://pipedream.com/sources) and click on your event source. Copy the URL that appears in your browser's address bar. For example: - -``` -https://pipedream.com/sources/dc_abc123 -``` - -Your source's ID is the value that starts with `dc_`. In this example: `dc_abc123`. - -## Warnings - -Pipedream displays warnings below steps in certain conditions. These warnings do not stop the execution of your workflow, but can signal an issue you should be aware of. - -## Limit Exceeded Errors - -Pipedream sets [limits](/limits/) on runtime, memory, and other execution-related properties. If you exceed these limits, you'll receive one of the errors below. [See the limits doc](/limits/) for details on specific limits. - -### Quota Exceeded - -On the Free tier, Pipedream imposes a limit on the [daily credits](/limits/#daily-credits-limit) across all workflows and sources. If you hit this limit, you'll see a **Quota Exceeded** error. - -Paid plans have no credit limit. [Upgrade here](https://pipedream.com/pricing). - -### Runtime Quota Exceeded - -You **do not** use credits testing workflows, but workspaces on the **Free** plan are limited to {process.env.DAILY_TESTING_LIMIT} of test runtime per day. If you exceed this limit when testing in the builder, you'll see a **Runtime Quota Exceeded** error. - -### Timeout - -Event sources and workflows have a [default time limit on a given execution](/limits/#time-per-execution). If your code exceeds that limit, you may encounter a **Timeout** error. - -To address timeouts, you'll either need to: - -1. Figure out why your code is running for longer than expected. It's important to note that **timeouts are not an issue with Pipedream — they are specific to your workflow**. Often, you're making a request to a third party API that doesn't respond in the time you expect, or you're processing a large amount of data in your workflow, and it doesn't complete before you hit the execution limit. -2. If it's expected that your code is taking a long time to run, you can raise the execution limit of a workflow in your [workflow's settings](/workflows/settings/#execution-timeout-limit). If you need to change the execution limit for an event source, please [reach out to our team](https://pipedream.com/support/). - -### Out of Memory - -Pipedream [limits the default memory](/limits/#memory) available to workflows and event sources. If you exceed this memory, you'll see an **Out of Memory** error. **You can raise the memory of your workflow [in your workflow's Settings](/workflows/settings/#memory)**. - -This can happen for two main reasons: - -1. When you load a large file or object into the workflow's memory (e.g. when you save the content in a variable). Where possible, consider streaming the file to / from disk, instead of storing it in memory, using a [technique like this](/code/nodejs/http-requests/#download-a-file-to-the-tmp-directory). -2. When you have many steps in your Pipedream workflow. When your workflow runs, Pipedream runs a separate process for each step in your workflow. That incurs some memory overhead. Typically this happens when you have more than 8-10 steps. When you see an OOM error on a workflow with many steps, try increasing the memory. - -### Rate Limit Exceeded - -Pipedream limits the number of events that can be processed by a given interface (e.g. HTTP endpoints) during a given interval. This limit is most commonly reached for HTTP interfaces - see the [QPS limits documentation](/limits/#qps-queries-per-second) for more information on that limit. - -**This limit can be raised for HTTP endpoints**. [Reach out to our team](https://pipedream.com/support/) to request an increase. - -### Request Entity Too Large - -By default, Pipedream limits the size of incoming HTTP payloads. If you exceed this limit, you'll see a **Request Entity Too Large** error. - -Pipedream supports two different ways to bypass this limit. Both of these interfaces support uploading data up to `5TB`, though you may encounter other [platform limits](/limits/). - -- You can send large HTTP payloads by passing the `pipedream_upload_body=1` query string or an `x-pd-upload-body: 1` HTTP header in your HTTP request. [Read more here](/workflows/steps/triggers/#sending-large-payloads). -- You can upload multiple large files, like images and videos, using the [large file upload interface](/workflows/steps/triggers/#large-file-support). - -### Function Payload Limit Exceeded - -The total size of `console.log()` statements, [step exports](/workflows/steps/#step-exports), and the original event data sent to workflows and sources cannot exceed a combined size of {process.env.FUNCTION_PAYLOAD_LIMIT}. If you produce logs or step exports larger than this - for example, passing around large API responses, CSVs, or other data - you may encounter a **Function Payload Limit Exceeded** in your workflow. - -Often, this occurs when you pass large data between steps using [step exports](/workflows/steps/#step-exports). You can avoid this error by [writing that data to the `/tmp` directory](/code/nodejs/working-with-files/#writing-a-file-to-tmp) in one step, and [reading the data into another step](/code/nodejs/working-with-files/#reading-a-file-from-tmp), which avoids the use of step exports and should keep you under the payload limit. - -Pipedream also compresses the function payload from your workflow, which can yield roughly a 2x-3x increase in payload size (somewhere between `12MB` and `18MB`), depending on the data. - -### JSON Nested Property Limit Exceeded - -Working with nested JavaScript objects that have more than 256 nested objects will trigger a **JSON Nested Property Limit Exceeded** error. - -Often, objects with this many nested objects result from a programming error that explodes the object in an unexpected way. Please confirm the code you're using to convert data into an object is correctly parsing the object. - -### Event Queue Full - -Workflows have a maximum event queue size when using concurrency and throttling controls. If the number of unprocessed events exceeds the [maximum queue size](/workflows/concurrency-and-throttling/#increasing-the-queue-size-for-a-workflow), you may encounter an **Event Queue Full** error. - -[Paid plans](https://pipedream.com/pricing) can [increase their queue size up to {process.env.MAX_WORKFLOW_QUEUE_SIZE}](/workflows/concurrency-and-throttling/#increasing-the-queue-size-for-a-workflow) for a given workflow. - -### Credit Budget Exceeded - -Credit Budgets are configurable limits on your credit usage at the account or workspace level. - -If you're receiving this warning on a source or workflow, this means your allocated Credit Budget has been reached for the defined period. - -You can increase this limit at any time in the [billing area of your settings](https://pipedream.com/settings/billing). - -## Pipedream Internal Error - -A `Pipedream Internal Error` is thrown whenever there's an exception during the building or executing of a workflow that's outside the scope of the code for the individual components (steps or actions). - -There are a few known ways this can be caused and how to solve them. - -### Out of date actions or sources - -Pipedream components are updated continously. But when new versions of actions and sources are published to the Pipedream Component Registry, your workflows are not updated by default. - -[An **Update** prompt](/workflows/steps/actions/#updating-actions-to-the-latest-version) is shown in the in the top right of the action if the component has a new version available. - -Sources do not feature an update button at this time, to receive the latest version, you'll need to create a new source, then attach it to your workflow. - -### New package versions issues - -If an NPM or PyPI package throws an error during either the building of the workflow or during it's execution, it may cause a `Pipedream Internal Error`. - -By default, Pipedream automatically updates NPM and PyPI packages to the latest version available. This is designed to make sure your workflows receive the latest package updates automatically. - -However, if a new package version includes bugs, or changes it's export signature, then this may cause a `Pipedream Internal Error`. - -You can potentially fix this issue by downgrading packages by pinning in [your Node.js](/code/nodejs/#pinning-package-versions) or [Python code steps](/code/python/#pinning-package-versions) to the last known working version. - -Alternatively, if the error is due to a major release that changes the import signature of a package, then modifying your code to match the signature may help. - - -Some Pipedream components use NPM packages - -Some Pipedream components like pre-built [actions and triggers for Slack use NPM packages](https://github.com/PipedreamHQ/pipedream/blob/9aea8653dc65d438d968971df72e95b17f52d51c/components/slack/slack.app.mjs#L1). - -In order to downgrade these packages, you'll need to fork the Pipedream Github Repository and deploy your own changes to test them privately. Then you can [contribute the fix back into the main Pipedream Repository](https://pipedream.com/docs/apps/contributing/#contribution-process). - - -### Packages consuming all available storage - -A `Pipedream Internal Error` could be the result of NPM or PyPI packages using the entireity of the workflow's storage capacity. - -The `lodash` library for example will import the entire package if individual modules are imported with this type of signature: - -```javascript -// This style of import will cause the entire lodash package to be installed, not just the pick module -import { pick } from "lodash" -``` - -Instead, use the specific package that exports the `pick` module alone: - -```javascript -// This style imports only the pick module, since the lodash.pick package only contains this module -import pick from "lodash.pick" -``` - -## Code was still running when the step ended - -This error occurs when Promises or asynchronous code is not properly finished before the next step begins execution. - -See the [Asynchronous section of the Node.js documentation](/code/nodejs/async/#the-problem) for more details. - - -## How do I contact Pipedream Support? - -Start by filling out the request form at [https://pipedream.com/support](https://pipedream.com/support), providing detailed information about your issue. - -### How do I share my workflow with Support? - -First, navigate to your **Project Settings** and share your project with Pipedream Support. - -If your workflow is _not_ part of a Project, go to the **Workflow Settings** to grant access to us. - -When filling out the request form at [https://pipedream.com/support](https://pipedream.com/support), please provide detailed information along with the URL from your browser's address bar, which should look something like: - -``` -https://pipedream.com/@yourworkspace/projects/yourproject/test-workflow-pabc123 -``` - -## Frequently-asked questions - -### How do I resolve the error "Undeployed changes — You have made changes to this workflow. Deploy the latest version from the editor" - -On workflows that are not [synced with GitHub](/projects/git/), you may notice the following warning at the top of your workflow: - -> **Undeployed changes** — You have made changes to this workflow. Deploy the latest version from the editor - -This means that you've made some changes to your workflow that you haven't yet deployed. To see a diff of what's changed, we recommend [enabling GitHub sync](/projects/git/), where you'll get a full commit history of changes made to your workflows, synced to your own GitHub repo. - -### Is there a way to replay workflow events programmatically? - -Not today. Please upvote and add your feedback to [this GitHub issue](https://github.com/PipedreamHQ/pipedream/issues/2784). - -### How do I store and retrieve data across workflow executions? - -If you operate your own database or data store, you can connect to it directly in Pipedream. - -Pipedream also operates a [built-in key-value store](/data-stores/) that you can use to get and set data across workflow executions and different workflows. - -### How do I delay the execution of a workflow? - -Use Pipedream's [built-in Delay actions](/workflows/flow-control/#delay) to delay a workflow at any step. - -### How can I save common functions as steps? - -You can create your own custom triggers and actions ("components") on Pipedream using [the Component API](/components/api/). These components are private to your account and can be used in any workflow. - -You can also publish common functions in your own package on a public registry like [npm](https://www.npmjs.com/) or [PyPI](https://pypi.org/). - -### Is Puppeteer supported in Pipedream? - -Yes, see [our Puppeteer docs](/code/nodejs/browser-automation/#puppeteer) for more detail. - -### Is Playwright supported in Pipedream? - -Yes, see [our Puppeteer docs](/code/nodejs/browser-automation/#playwright) for more detail. diff --git a/docs-v2/pages/troubleshooting/_meta.tsx b/docs-v2/pages/troubleshooting/_meta.tsx new file mode 100644 index 0000000000000..f07074c72382a --- /dev/null +++ b/docs-v2/pages/troubleshooting/_meta.tsx @@ -0,0 +1,4 @@ +export default { + "index": "Overview", + "faq": "FAQ", +} as const diff --git a/docs-v2/pages/troubleshooting/faq.mdx b/docs-v2/pages/troubleshooting/faq.mdx new file mode 100644 index 0000000000000..e60f70e81405f --- /dev/null +++ b/docs-v2/pages/troubleshooting/faq.mdx @@ -0,0 +1,50 @@ + +# FAQ + +## How do I resolve the error "Undeployed changes — You have made changes to this workflow. Deploy the latest version from the editor" + +On workflows that are not [synced with GitHub](/workflows/git/), you may notice the following warning at the top of your workflow: + +> **Undeployed changes** — You have made changes to this workflow. Deploy the latest version from the editor + +This means that you've made some changes to your workflow that you haven't yet deployed. To see a diff of what's changed, we recommend [enabling GitHub sync](/workflows/git/), where you'll get a full commit history of changes made to your workflows, synced to your own GitHub repo. + +## Is there a way to replay workflow events programmatically? + +Not today. Please upvote and add your feedback to [this GitHub issue](https://github.com/PipedreamHQ/pipedream/issues/2784). + +## How do I store and retrieve data across workflow executions? + +If you operate your own database or data store, you can connect to it directly in Pipedream. + +Pipedream also operates a [built-in key-value store](/workflows/data-management/data-stores/) that you can use to get and set data across workflow executions and different workflows. + +## How do I delay the execution of a workflow? + +Use Pipedream's [built-in Delay actions](/workflows/building-workflows/control-flow/delay/) to delay a workflow at any step. + +## How can my workflow run faster? + +Here are a few things that can help your workflow execute faster: + +1. **Increase memory:** Increase your [workflow memory](/workflows/building-workflows/settings/#memory) to at least 512 MB. Raising the memory limit will proportionally increase CPU resources, leading to improved performance and reduced latency. + +2. **Return static HTTP responses:** If your workflow is triggered by an HTTP source, return a [static HTTP response](/workflows/building-workflows/triggers/#http-responses) directly from the trigger configuration. This ensures the HTTP response is sent to the caller immediately, before the rest of the workflow steps are executed. + +3. **Simplify your workflow:** Reduce the number of [steps](/workflows/#code-actions) and [segments](/workflows/building-workflows/control-flow/#workflow-segments) in your workflow, combining multiple steps into one, if possible. This lowers the overhead involved in managing step execution and exports. + +4. **Activate warm workers:** Use [warm workers](/workflows/building-workflows/settings/#eliminate-cold-starts) to reduce the startup time of workflows. Set [as many warm workers](/workflows/building-workflows/settings/#how-many-workers-should-i-configure) as you want for high-volume traffic. + +## How can I save common functions as steps? + +You can create your own custom triggers and actions ("components") on Pipedream using [the Component API](/workflows/contributing/components/api/). These components are private to your account and can be used in any workflow. + +You can also publish common functions in your own package on a public registry like [npm](https://www.npmjs.com/) or [PyPI](https://pypi.org/). + +## Is Puppeteer supported in Pipedream? + +Yes, see [our Puppeteer docs](/workflows/building-workflows/code/nodejs/browser-automation/#puppeteer) for more detail. + +## Is Playwright supported in Pipedream? + +Yes, see [our Playwright docs](/workflows/building-workflows/code/nodejs/browser-automation/#playwright) for more detail. diff --git a/docs-v2/pages/troubleshooting/index.mdx b/docs-v2/pages/troubleshooting/index.mdx new file mode 100644 index 0000000000000..1c12f51ec1f05 --- /dev/null +++ b/docs-v2/pages/troubleshooting/index.mdx @@ -0,0 +1,239 @@ +import Callout from '@/components/Callout' + +# Troubleshooting Common Issues + +This doc describes some common solutions for fixing issues with [pipedream.com](https://pipedream.com) or with a specific workflow. + +## How do I contact Pipedream Support? + +Start by filling out the request form at [https://pipedream.com/support](https://pipedream.com/support), providing detailed information about your issue. + +## A feature isn't working on pipedream.com + +If you're seeing an issue with [pipedream.com](https://pipedream.com) (for example, the site won't load, or you think you've found a bug), try each of the following steps, checking to see if they fix the problem: + +1. [Hard refresh](https://fabricdigital.co.nz/blog/how-to-hard-refresh-your-browser-and-clear-cache) pipedream.com in your browser. + +2. Log out of your pipedream.com account, and log back in. + +3. [Disable your browser extensions](https://www.computerhope.com/issues/ch001411.htm) or use features like [Chrome Guest mode](https://support.google.com/chrome/answer/6130773?hl=en&co=GENIE.Platform%3DAndroid) to browse pipedream.com without any existing extensions / cookies / cache. + +If you're still seeing the issue after trying these steps, please [report a bug](https://github.com/PipedreamHQ/pipedream/issues/new?assignees=&labels=bug&template=bug_report.md&title=%5BBUG%5D+). + +## My workflow isn't working + +If you're encountering a specific issue in a workflow, try the following steps, checking to see if they fix the problem: + +1. Make a trivial change to your workflow, and **Deploy** your workflow again. + +2. Try searching [the community](https://pipedream.com/support) or [the `pipedream` GitHub repo](https://github.com/PipedreamHQ/pipedream/issues) to see if anyone else has solved the same issue. + +If you're still seeing the issue after trying these steps, please reach out in [the community](https://pipedream.com/support). + +## I need help with my GitHub Synced Project + +[See FAQ here](/workflows/git/#faq). + +## Why is my trigger not emitting events? + +Most Pipedream sources fall into one of two categories: webhook-based or timer-based. + +### Webhook-based instant sources + +These sources will get triggered immediately. But because events come in in real-time, most will **not** automatically fetch historical events upon creation. To surface test events in your workflow while building, you'll need to generate an eligible event in the selected app. + +For example, if you've configured the "[Message Updates (Instant)](https://pipedream.com/apps/telegram-bot-api/triggers/message-updates) Telegram source, you'll need to send a message in the Telegram account you've selected in order for an event to appear. + +![Select an event](https://res.cloudinary.com/pipedreamin/image/upload/v1653434586/docs/webhook-triggers-select-event_qj7nlp.png) + +Sources for apps like [Telegram](https://pipedream.com/apps/telegram-bot-api/triggers/message-updates) and [Google Sheets](https://pipedream.com/apps/google-sheets/triggers/new-row-added) use webhooks and get triggered immediately. + +### Timer-based polling sources + +These sources will fetch new events on a regular interval, based on a schedule you specify in the trigger configuration. + +![Configure polling timer](https://res.cloudinary.com/pipedreamin/image/upload/v1653434591/docs/polling-triggers-timer_ooz26c.png) + +In most cases, Pipedream will automatically fetch recent historical events to help enable easier workflow development. Sources for apps like [Twitter](https://pipedream.com/apps/twitter/triggers/search-mentions) and [Spotify](https://pipedream.com/apps/spotify/triggers/new-playlist) require we poll their endpoints in order to fetch new events. + +## Where do I find my workflow's ID? + +Open [https://pipedream.com](https://pipedream.com) and visit your workflow. Copy the URL that appears in your browser's address bar. For example: + +``` +https://pipedream.com/@dylburger/p_abc123/edit +``` + +Your workflow's ID is the value that starts with `p_`. In this example: `p_abc123`. + +## How do I invoke another workflow? + +We provide a [Trigger Workflow](https://pipedream.com/apps/helper-functions/actions/trigger-workflow) action in the [Helper Functions](https://pipedream.com/apps/helper-functions) app. [See more here](/workflows/building-workflows/code/nodejs/#invoke-another-workflow). + +Another option is to make an HTTP request to a Pipedream HTTP webhook trigger. + +## Where do I find my event source's ID? + +Open [https://pipedream.com/sources](https://pipedream.com/sources) and click on your event source. Copy the URL that appears in your browser's address bar. For example: + +``` +https://pipedream.com/sources/dc_abc123 +``` + +Your source's ID is the value that starts with `dc_`. In this example: `dc_abc123`. + +## Why is my trigger paused? + +Pipedream automatically disables sources with a 100% error rate in the past 5 days for accounts on the Free plan. + +To troubleshoot, you can look at the errors in the [source](/workflows/building-workflows/triggers/) logs, and may need to reconnect your account and re-enable the source for it to run again. If the issue persists, please reach out in [the community](https://pipedream.com/support). + +## Warnings + +Pipedream displays warnings below steps in certain conditions. These warnings do not stop the execution of your workflow, but can signal an issue you should be aware of. + +## Limit Exceeded Errors + +Pipedream sets [limits](/workflows/limits/) on runtime, memory, and other execution-related properties. If you exceed these limits, you'll receive one of the errors below. [See the limits doc](/workflows/limits/) for details on specific limits. + + +## Quota Exceeded + +On the Free tier, Pipedream imposes a limit on the [daily credits](/workflows/limits/#daily-credits-limit) across all workflows and sources. If you hit this limit, you'll see a **Quota Exceeded** error. + +Paid plans have no credit limit. [Upgrade here](https://pipedream.com/pricing). + +## Runtime Quota Exceeded + +You **do not** use credits testing workflows, but workspaces on the **Free** plan are limited to {process.env.DAILY_TESTING_LIMIT} of test runtime per day. If you exceed this limit when testing in the builder, you'll see a **Runtime Quota Exceeded** error. + +## Timeout + +Event sources and workflows have a [default time limit on a given execution](/workflows/limits/#time-per-execution). If your code exceeds that limit, you may encounter a **Timeout** error. + +To address timeouts, you'll either need to: + +1. Figure out why your code is running for longer than expected. It's important to note that **timeouts are not an issue with Pipedream — they are specific to your workflow**. Often, you're making a request to a third party API that doesn't respond in the time you expect, or you're processing a large amount of data in your workflow, and it doesn't complete before you hit the execution limit. +2. If it's expected that your code is taking a long time to run, you can raise the execution limit of a workflow in your [workflow's settings](/workflows/building-workflows/settings/#execution-timeout-limit). If you need to change the execution limit for an event source, please [reach out to our team](https://pipedream.com/support/). + +## Out of Memory + +Pipedream [limits the default memory](/workflows/limits/#memory) available to workflows and event sources. If you exceed this memory, you'll see an **Out of Memory** error. **You can raise the memory of your workflow [in your workflow's Settings](/workflows/building-workflows/settings/#memory)**. + +This can happen for two main reasons: + +1. When you load a large file or object into the workflow's memory (e.g. when you save the content in a variable). Where possible, consider streaming the file to / from disk, instead of storing it in memory, using a [technique like this](/workflows/building-workflows/code/nodejs/http-requests/#download-a-file-to-the-tmp-directory). +2. When you have many steps in your Pipedream workflow. When your workflow runs, Pipedream runs a separate process for each step in your workflow. That incurs some memory overhead. Typically this happens when you have more than 8-10 steps. When you see an OOM error on a workflow with many steps, try increasing the memory. + +## Rate Limit Exceeded + +Pipedream limits the number of events that can be processed by a given interface (e.g. HTTP endpoints) during a given interval. This limit is most commonly reached for HTTP interfaces - see the [QPS limits documentation](/workflows/limits/#qps-queries-per-second) for more information on that limit. + +**This limit can be raised for HTTP endpoints**. [Reach out to our team](https://pipedream.com/support/) to request an increase. + +## Request Entity Too Large + +By default, Pipedream limits the size of incoming HTTP payloads. If you exceed this limit, you'll see a **Request Entity Too Large** error. + +Pipedream supports two different ways to bypass this limit. Both of these interfaces support uploading data up to `5TB`, though you may encounter other [platform limits](/workflows/limits/). + +- You can send large HTTP payloads by passing the `pipedream_upload_body=1` query string or an `x-pd-upload-body: 1` HTTP header in your HTTP request. [Read more here](/workflows/building-workflows/triggers/#sending-large-payloads). +- You can upload multiple large files, like images and videos, using the [large file upload interface](/workflows/building-workflows/triggers/#large-file-support). + +## Function Payload Limit Exceeded + +The total size of `console.log()` statements, [step exports](/workflows/#step-exports), and the original event data sent to workflows and sources cannot exceed a combined size of {process.env.FUNCTION_PAYLOAD_LIMIT}. If you produce logs or step exports larger than this - for example, passing around large API responses, CSVs, or other data - you may encounter a **Function Payload Limit Exceeded** in your workflow. + +Often, this occurs when you pass large data between steps using [step exports](/workflows/#step-exports). You can avoid this error by [writing that data to the `/tmp` directory](/workflows/building-workflows/code/nodejs/working-with-files/#writing-a-file-to-tmp) in one step, and [reading the data into another step](/workflows/building-workflows/code/nodejs/working-with-files/#reading-a-file-from-tmp), which avoids the use of step exports and should keep you under the payload limit. + +Pipedream also compresses the function payload from your workflow, which can yield roughly a 2x-3x increase in payload size (somewhere between `12MB` and `18MB`), depending on the data. + +## JSON Nested Property Limit Exceeded + +Working with nested JavaScript objects that have more than 256 nested objects will trigger a **JSON Nested Property Limit Exceeded** error. + +Often, objects with this many nested objects result from a programming error that explodes the object in an unexpected way. Please confirm the code you're using to convert data into an object is correctly parsing the object. + +## Event Queue Full + +Workflows have a maximum event queue size when using concurrency and throttling controls. If the number of unprocessed events exceeds the [maximum queue size](/workflows/building-workflows/settings/concurrency-and-throttling/#increasing-the-queue-size-for-a-workflow), you may encounter an **Event Queue Full** error. + +[Paid plans](https://pipedream.com/pricing) can [increase their queue size up to {process.env.MAX_WORKFLOW_QUEUE_SIZE}](/workflows/building-workflows/settings/concurrency-and-throttling/#increasing-the-queue-size-for-a-workflow) for a given workflow. + +## Credit Budget Exceeded + +Credit Budgets are configurable limits on your credit usage at the account or workspace level. + +If you're receiving this warning on a source or workflow, this means your allocated Credit Budget has been reached for the defined period. + +You can increase this limit at any time in the [billing area of your settings](https://pipedream.com/settings/billing). + +## Pipedream Internal Error + +A `Pipedream Internal Error` is thrown whenever there's an exception during the building or executing of a workflow that's outside the scope of the code for the individual components (steps or actions). + +There are a few known ways this can be caused and how to solve them. + +## Out of date actions or sources + +Pipedream components are updated continously. But when new versions of actions and sources are published to the Pipedream Component Registry, your workflows are not updated by default. + +[An **Update** prompt](/workflows/building-workflows/actions/#updating-actions-to-the-latest-version) is shown in the in the top right of the action if the component has a new version available. + +Sources do not feature an update button at this time, to receive the latest version, you'll need to create a new source, then attach it to your workflow. + +### New package versions issues + +If an NPM or PyPI package throws an error during either the building of the workflow or during it's execution, it may cause a `Pipedream Internal Error`. + +By default, Pipedream automatically updates NPM and PyPI packages to the latest version available. This is designed to make sure your workflows receive the latest package updates automatically. + +However, if a new package version includes bugs, or changes it's export signature, then this may cause a `Pipedream Internal Error`. + +You can potentially fix this issue by downgrading packages by pinning in [your Node.js](/workflows/building-workflows/code/nodejs/#pinning-package-versions) or [Python code steps](/workflows/building-workflows/code/python/#pinning-package-versions) to the last known working version. + +Alternatively, if the error is due to a major release that changes the import signature of a package, then modifying your code to match the signature may help. + + +Some Pipedream components use NPM packages + +Some Pipedream components like pre-built [actions and triggers for Slack use NPM packages](https://github.com/PipedreamHQ/pipedream/blob/9aea8653dc65d438d968971df72e95b17f52d51c/components/slack/slack.app.mjs#L1). + +In order to downgrade these packages, you'll need to fork the Pipedream Github Repository and deploy your own changes to test them privately. Then you can [contribute the fix back into the main Pipedream Repository](/workflows/contributing/#contribution-process). + + +### Packages consuming all available storage + +A `Pipedream Internal Error` could be the result of NPM or PyPI packages using the entireity of the workflow's storage capacity. + +The `lodash` library for example will import the entire package if individual modules are imported with this type of signature: + +```javascript +// This style of import will cause the entire lodash package to be installed, not just the pick module +import { pick } from "lodash" +``` + +Instead, use the specific package that exports the `pick` module alone: + +```javascript +// This style imports only the pick module, since the lodash.pick package only contains this module +import pick from "lodash.pick" +``` + +## Code was still running when the step ended + +This error occurs when Promises or asynchronous code is not properly finished before the next step begins execution. + +See the [Asynchronous section of the Node.js documentation](/workflows/building-workflows/code/nodejs/async/#the-problem) for more details. + +### How do I share my workflow with Support? + +First, navigate to your **Project Settings** and share your project with Pipedream Support. + +If your workflow is _not_ part of a Project, go to the **Workflow Settings** to grant access to us. + +When filling out the request form at [https://pipedream.com/support](https://pipedream.com/support), please provide detailed information along with the URL from your browser's address bar, which should look something like: + +``` +https://pipedream.com/@yourworkspace/workflows/projects/yourproject/test-workflow-pabc123 +``` diff --git a/docs-v2/pages/user-settings.mdx b/docs-v2/pages/user-settings.mdx deleted file mode 100644 index 942f7330b4248..0000000000000 --- a/docs-v2/pages/user-settings.mdx +++ /dev/null @@ -1,132 +0,0 @@ -import Callout from '@/components/Callout' -import VideoPlayer from '@/components/VideoPlayer' - -# User and Billing Settings - -You can find important account details, text editor configuration, and more in your [User Settings](https://pipedream.com/user). - -## Account - -You'll find your Pipedream email and other basic account details in your [Account Settings](https://pipedream.com/user). - -### Changing your email - -Pipedream sends system emails to the email address tied to your Pipedream login. You can change the email address to which these emails are delivered by modifying the **Email** in your Account Settings. Once changed, an email will be delivered to the new address requesting you verify it. - -Pipedream marketing emails may still be sent to the original email address you used when signing up for Pipedream. To change the email address tied to marketing emails, please [reach out to our team](https://pipedream.com/support). - -### Two-Factor Authentication - - - -Two-factor authentication (2FA) adds an additional layer of security for your Pipedream account and is recommended for all users. - -#### Configuring 2FA - -1. Open your [Account Settings](https://pipedream.com/user) -2. Click **Configure** under **Two-Factor Authentication** -3. Scan the QR code in an authenticator app like [1Password](https://1password.com/) or Google Authenticator (available on [iOS](https://apps.apple.com/us/app/google-authenticator/id388497605) and [Android](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en_US&gl=US)) -4. If you're unable to scan the QR code, you can view the setup key to configure 2FA manually in your authenticator app -5. Enter the one-time-password (OTP) from your authenticator app -6. **Save your recovery codes in a secure location**. You'll need these to access your Pipedream account in the case you lose access to your authenticator app. - - -Save your recovery codes - -If you lose access to your authenticator app and your recovery codes, you will permanently lose access to your Pipedream account. **Pipedream Support cannot recover these accounts.** - - -#### Signing in with 2FA - -1. You'll be prompted to enter your OTP the next time you sign in to Pipedream -2. When prompted, you can enter the OTP from your authenticator app or a recovery code - - -Using recovery codes - -Each recovery code is a one-time-use code, so make sure to generate new recovery codes in your [Account Settings](https://pipedream.com/user) when you need to. All previously generated recovery codes expire when you generate new ones. - - - -2FA is not currently supported with Single Sign On -Pipedream recommends enabling 2FA with your identity provider. - - -#### Requiring 2-Factor Authentication - -Workspaces on the Business and Enterprise plans can [require all workspace members to configure 2FA](/workspaces/#requiring-two-factor-authentication) in order to log in to Pipedream. - -If you are a member of any workspace where 2FA is required, you cannot disable 2FA, but you can still reconfigure it in your [account settings](https://pipedream.com/account/) if necessary. - - -Admins and Owners control 2FA settings - -Only workspace owner and admin members can enable or disable 2FA for an entire workspace. - - -### Pipedream API Key - -Pipedream provides a [REST API](/api/) for interacting with Pipedream programmatically. You'll find your API key here, which you use to [authorize requests to the API](/rest-api/auth/). - -You can revoke and regenerate your API key from here at any time. - -### Delete Account - -You can delete your Pipedream account at any time by visiting your Account Settings and pressing the **Delete your Account** button. Account deletion is immediately and irreversible. - -## Application - -You can change how the Pipedream app displays data, and basic text editor config, in your [Application Settings](https://pipedream.com/settings/app). - -For example, you can: - -- Change the clock format to 12-hour or 24-hour mode -- Enable Vim keyboard shortcuts in the Pipedream text editor, or enable word wrap -- Set the number of spaces that will be added in the editor when pressing `Tab` - -## Environment Variables - -Environment variables allow you to securely store secrets or other config values that you can access in Pipedream workflows via `process.env`. [Read more about environment variables here](/environment-variables/). - -## Billing and Usage - -You'll find information on your usage data (for specific [Pipedream limits](/limits/)) in your [Billing Settings](https://pipedream.com/settings/billing). You can also upgrade to [paid plans](https://pipedream.com/pricing) from this page. - -### Subscription - -You can upgrade to [paid plans](https://pipedream.com/pricing) from this section. - -If you've already upgraded, you'll see an option to **Manage Subscription** here, which directs you to your personal Stripe portal. Here, you can change your payment method, review the details of previous invoices, and more. - -### Usage - -[Credits](/pricing/#credits) are Pipedream's billable unit, and users on the [free tier](/pricing/#free-tier) are limited on the number of daily free credits allocated. The **Usage** section displays a chart of the daily credits across a historical range of time to provide insight into your usage patterns. - -Hover over a specific column in the chart to see the number of credits run for that specific day: - -![Daily credits tooltip](./images/daily-invocations-tooltip.png) - -_Click_ on a specific column to see credits for that day, broken out by workflow / source: - -![Credits broken out by workflow / source](./images/usage-by-resource.png) - -Users on the free tier will see the last 30 days of usage in this chart. Users on [paid plans](https://pipedream.com/pricing) will see the cumulative usage tied to their current billing period. - -### Compute Budget - -Control the maximum number of compute credits permitted on your account with an _Credit Budget_. - -This will restrict your account-wide usage to the specified number of [credits](/pricing/#credits) on a monthly or daily basis. The compute budget does not apply to credits incurred by [dedicated workers](/workflows/settings/#eliminate-cold-starts). - -To enable this feature, _click_ on the toggle and define your maximum number of credits in the period. - - -Due to how credits are accrued, there may be cases where your credit usage may _slightly_ go over the cap. - -In an example scenario, with cap set at 20 credits and long running workflow that uses 10 credits per run; it's possible that two concurrent events trigger the workflow, and the cap will won't apply until after the concurrent events are processed. - - -### Limits - -For users on the [Free tier](/pricing/#free-tier), this section displays your usage towards your [credits quota](/limits/#daily-credits-limit) for the current UTC day. - diff --git a/docs-v2/pages/workflows/_meta.json b/docs-v2/pages/workflows/_meta.json deleted file mode 100644 index cc843e4a1bc11..0000000000000 --- a/docs-v2/pages/workflows/_meta.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "index": "What are workflows?", - "steps": "Steps", - "triggers": "Triggers", - "actions": "Actions", - "using-props": "Using Props", - "events": "Events", - "inspect": "Inspect events", - "flow-control": "Flow control", - "errors": "Handling errors", - "concurrency-and-throttling": "Concurrency and Throttling", - "settings": "Settings", - "vpc": "Virtual Private Clouds", - "domains": "Custom Domains", - "sharing": "Sharing workflows", - "build-and-run": { - "display": "hidden" - } -} diff --git a/docs-v2/pages/workflows/_meta.tsx b/docs-v2/pages/workflows/_meta.tsx new file mode 100644 index 0000000000000..9d2c035d944cf --- /dev/null +++ b/docs-v2/pages/workflows/_meta.tsx @@ -0,0 +1,15 @@ +export default { + "index": "Overview", + "building-workflows": "Building Workflows", + "projects": "Projects", + "workspaces": "Workspaces", + "data-management": "Data Management", + "event-history": "Event History", + "environment-variables": "Environment Variables", + "git": "GitHub Sync", + "domains": "Custom Domains", + "vpc": "Virtual Private Clouds", + "limits": "Limits", + "contributing": "Contributing", + "cli": "CLI", +} as const diff --git a/docs-v2/pages/workflows/actions.mdx b/docs-v2/pages/workflows/actions.mdx deleted file mode 100644 index c46ebd149efd5..0000000000000 --- a/docs-v2/pages/workflows/actions.mdx +++ /dev/null @@ -1,39 +0,0 @@ -# Actions - -Actions are reusable code steps that integrate your apps, data and APIs. For example, you can send HTTP requests to an external service using our HTTP actions, or use our Google Sheets actions to add new data. You can use thousands of actions across {process.env.PUBLIC_APPS}+ apps today. - -Typically, integrating with these services requires custom code to manage connection logic, error handling, and more. Actions handle that for you. You only need to specify the parameters required for the action. For example, the HTTP `GET` Request action requires you enter the URL whose data you want to retrieve. - -You can also [create your own actions](#creating-your-own-actions) that can be shared across your own workflows, or published to all Pipedream users. - -## Using Existing Actions - -Adding existing actions to your workflow is easy: - -1. Click the **+** button below any step. -2. Search for the app you're looking for and select it from the list. -3. Search for the action and select it to add it to your workflow. - -For example, here's how to add the HTTP `GET` Request action: - -![Adding HTTP GET request action](https://res.cloudinary.com/pipedreamin/image/upload/v1647959419/docs/components/CleanShot_2022-03-22_at_10.29.34_s60bdr.gif) - -## Updating actions to the latest version - -When you use existing actions or create your own, you'll often want to update an action you added to a workflow to the newest version. For example, the community might publish a new feature or bug fix that you want to use. - -In your code steps with out of date actions, you'll see a button appear that will update your action to the latest version. Click on this button to update your code step: - -![Updating an action to the latest version](https://res.cloudinary.com/pipedreamin/image/upload/v1647969837/docs/components/CleanShot_2022-03-22_at_13.23.41_2x_isis5z.png) - -## Creating your own actions - -You can author your own actions on Pipedream, too. Anytime you need to reuse the same code across steps, consider making that an action. - -Start with our [action development quickstart](/components/quickstart/nodejs/actions/). You can read more about all the capabilities of actions in [our API docs](/components/api/), and review [example actions here](/components/api/#example-components). - -You can also publish actions to [the Pipedream registry](/apps/contributing/), which makes them available for anyone on Pipedream to use. - -## Reporting a bug / feature request - -If you'd like to report a bug, request a new action, or submit a feature request for an existing action, [open an issue in our GitHub repo](https://github.com/pipedreamhq/pipedream). diff --git a/docs-v2/pages/workflows/build-and-run.mdx b/docs-v2/pages/workflows/build-and-run.mdx deleted file mode 100644 index 5e96809df672c..0000000000000 --- a/docs-v2/pages/workflows/build-and-run.mdx +++ /dev/null @@ -1,4 +0,0 @@ -# Building - -Workflows can currently be built in Pipedream's UI. Local development support and Github integrations are on the roadmap for 2022. - diff --git a/docs-v2/pages/workflows/building-workflows/_meta.tsx b/docs-v2/pages/workflows/building-workflows/_meta.tsx new file mode 100644 index 0000000000000..42bee16c86848 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/_meta.tsx @@ -0,0 +1,16 @@ +export default { + "triggers": "Triggers", + "sources": { + "title": "Sources", + "display": "hidden", + }, + "actions": "Actions", + "http": "HTTP", + "using-props": "Using Props", + "code": "Write your own code", + "control-flow": "Control Flow", + "inspect": "Inspect events", + "errors": "Handling errors", + "sharing": "Sharing workflows", + "settings": "Settings", +} as const diff --git a/docs-v2/pages/workflows/building-workflows/actions.mdx b/docs-v2/pages/workflows/building-workflows/actions.mdx new file mode 100644 index 0000000000000..18684555e64e8 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/actions.mdx @@ -0,0 +1,39 @@ +# Actions + +Actions are reusable code steps that integrate your apps, data and APIs. For example, you can send HTTP requests to an external service using our HTTP actions, or use our Google Sheets actions to add new data. You can use thousands of actions across {process.env.PUBLIC_APPS}+ apps today. + +Typically, integrating with these services requires custom code to manage connection logic, error handling, and more. Actions handle that for you. You only need to specify the parameters required for the action. For example, the HTTP `GET` Request action requires you enter the URL whose data you want to retrieve. + +You can also [create your own actions](#creating-your-own-actions) that can be shared across your own workflows, or published to all Pipedream users. + +## Using Existing Actions + +Adding existing actions to your workflow is easy: + +1. Click the **+** button below any step. +2. Search for the app you're looking for and select it from the list. +3. Search for the action and select it to add it to your workflow. + +For example, here's how to add the HTTP `GET` Request action: + +![Adding HTTP GET request action](https://res.cloudinary.com/pipedreamin/image/upload/v1710509483/docs/docs/workflows/CleanShot_2024-03-15_at_09.30.51_lquom2.gif) + +## Updating actions to the latest version + +When you use existing actions or create your own, you'll often want to update an action you added to a workflow to the newest version. For example, the community might publish a new feature or bug fix that you want to use. + +In your code steps with out of date actions, you'll see a button appear that will update your action to the latest version. Click on this button to update your code step: + +![Updating an action to the latest version](/images/actions/update-action-button.png) + +## Creating your own actions + +You can author your own actions on Pipedream, too. Anytime you need to reuse the same code across steps, consider making that an action. + +Start with our [action development quickstart](/workflows/contributing/components/quickstart/nodejs/actions/). You can read more about all the capabilities of actions in [our API docs](/workflows/contributing/components/api/), and review [example actions here](/workflows/contributing/components/api/#example-components). + +You can also publish actions to [the Pipedream registry](/workflows/contributing/), which makes them available for anyone on Pipedream to use. + +## Reporting a bug / feature request + +If you'd like to report a bug, request a new action, or submit a feature request for an existing action, [open an issue in our GitHub repo](https://github.com/PipedreamHQ/pipedream). diff --git a/docs-v2/pages/workflows/building-workflows/code/_meta.tsx b/docs-v2/pages/workflows/building-workflows/code/_meta.tsx new file mode 100644 index 0000000000000..540119f98bc88 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/code/_meta.tsx @@ -0,0 +1,7 @@ +export default { + "index": "Overview", + "nodejs": "Node.js", + "python": "Python", + "go": "Go", + "bash": "Bash", +} as const diff --git a/docs-v2/pages/code/bash.mdx b/docs-v2/pages/workflows/building-workflows/code/bash.mdx similarity index 86% rename from docs-v2/pages/code/bash.mdx rename to docs-v2/pages/workflows/building-workflows/code/bash.mdx index 47bb54a532c6e..fb4a1384c950b 100644 --- a/docs-v2/pages/code/bash.mdx +++ b/docs-v2/pages/workflows/building-workflows/code/bash.mdx @@ -4,7 +4,7 @@ import Callout from '@/components/Callout' Prefer to write quick scripts in Bash? We've got you covered. You can run any Bash in a Pipedream step within your workflows. -Within a Bash step, you can [share data between steps](/code/bash/#sharing-data-between-steps) and [access environment variables](/code/bash/#using-environment-variables). But you can't connect accounts, return HTTP responses, or take advantage of other features available in the [Node.js](/code/nodejs/) environment at this time. +Within a Bash step, you can [share data between steps](/workflows/building-workflows/code/bash/#sharing-data-between-steps) and [access environment variables](/workflows/building-workflows/code/bash/#using-environment-variables). But you can't connect accounts, return HTTP responses, or take advantage of other features available in the [Node.js](/workflows/building-workflows/code/nodejs/) environment at this time. ## Adding a Bash code step @@ -61,7 +61,7 @@ cat $PIPEDREAM_STEPS | jq .trigger.event # Results in { id: 1, name: "Bulbasaur", type: "plant" } ``` - + The period (`.`) in front the `trigger.event` in the example is not a typo. This is to define the starting point for `jq` to traverse down the JSON in the HTTP response. @@ -78,7 +78,7 @@ EXPORT="key:json=${DATA}" echo $EXPORT >> $PIPEDREAM_EXPORTS ``` - + Not all data types can be stored in the `$PIPEDREAM_EXPORTS` data shared between workflow steps. You can only export JSON-serializable data from Bash steps. @@ -86,7 +86,7 @@ You can only export JSON-serializable data from Bash steps. ## Using environment variables -You can leverage any [environment variables defined in your Pipedream account](/environment-variables/#environment-variables) in a bash step. This is useful for keeping your secrets out of code as well as keeping them flexible to swap API keys without having to update each step individually. +You can leverage any [environment variables defined in your Pipedream account](/workflows/environment-variables/) in a bash step. This is useful for keeping your secrets out of code as well as keeping them flexible to swap API keys without having to update each step individually. To access them, just append the `$` in front of the environment variable name. @@ -113,7 +113,7 @@ echo $WEATHER # San Francisco: 🌫 +48°F ``` - + Use the `--silent` flag with `curl` to suppress extra extra diagnostic information that `curl` produces when making requests. This enables you to only worry about the body of the response so you can visualize it with tools like `echo` or `jq`. @@ -153,7 +153,7 @@ echo "Exiting now!" 1>&2 exit 1 ``` - + Using `exit` to quit a Bash step early _won't_ stop the execution of the rest of the workflow. Exiting a Bash step will only apply that particular step in the workflow. @@ -177,6 +177,6 @@ curl --silent https://wttr.in/Cleveland.png --output /tmp/weather.png ls /tmp ``` - -The `/tmp` directory does not have unlimited storage. Please refer to the [disk limits](/limits/#disk) for details. + +The `/tmp` directory does not have unlimited storage. Please refer to the [disk limits](/workflows/limits/#disk) for details. diff --git a/docs-v2/pages/code/go.mdx b/docs-v2/pages/workflows/building-workflows/code/go.mdx similarity index 93% rename from docs-v2/pages/code/go.mdx rename to docs-v2/pages/workflows/building-workflows/code/go.mdx index 4947819432263..7f9b4aab7c5cb 100644 --- a/docs-v2/pages/code/go.mdx +++ b/docs-v2/pages/workflows/building-workflows/code/go.mdx @@ -4,7 +4,7 @@ import Callout from '@/components/Callout' Pipedream supports [Go v{process.env.GO_LANG_VERSION}](https://go.dev) in workflows. You can use any of the [Go packages available](https://pkg.go.dev/) with a simple `import` — no `go get` needed. -When you write Go code on Pipedream, you can [share data between steps](/code/bash/#sharing-data-between-steps) and [access environment variables](/code/bash/#using-environment-variables). However, you can't connect accounts, return HTTP responses, or take advantage of other features available in the [Node.js](/code/nodejs/) environment at this time. +When you write Go code on Pipedream, you can [share data between steps](/workflows/building-workflows/code/bash/#sharing-data-between-steps) and [access environment variables](/workflows/building-workflows/code/bash/#using-environment-variables). However, you can't connect accounts, return HTTP responses, or take advantage of other features available in the [Node.js](/workflows/building-workflows/code/nodejs/) environment at this time. If you have any feedback on the Go runtime, please let us know in [our community](https://pipedream.com/support). @@ -20,7 +20,7 @@ You can use `fmt.Println` at any time to log information as the script is runnin The output for the `fmt.Println` **Logs** will appear in the `Results` section just beneath the code editor. - + Don't forget to import the `fmt` package in order to run `fmt.Println`. ```go {3} @@ -162,7 +162,7 @@ func main() { Now this `pokemon` data is accessible to downstream steps within `pd.Steps["code"]["pokemon"]` - + Not all data types can be stored in the `Steps` data shared between workflow steps. For the best experience, we recommend only [exporting structs that can be marshalled into JSON](https://go.dev/blog/json). @@ -170,7 +170,7 @@ For the best experience, we recommend only [exporting structs that can be marsha ## Using environment variables -You can leverage any [environment variables defined in your Pipedream account](/environment-variables/#environment-variables) in a Go step. This is useful for keeping your secrets out of code as well as keeping them flexible to swap API keys without having to update each step individually. +You can leverage any [environment variables defined in your Pipedream account](/workflows/environment-variables/) in a Go step. This is useful for keeping your secrets out of code as well as keeping them flexible to swap API keys without having to update each step individually. To access them, use the `os` package. @@ -277,7 +277,7 @@ func main() { // The response is logged in your Pipedream step results: sb := string(body) - log.Println(sb) + log.Println(sb) } ``` diff --git a/docs-v2/pages/workflows/building-workflows/code/index.mdx b/docs-v2/pages/workflows/building-workflows/code/index.mdx new file mode 100644 index 0000000000000..c0a130f1e8150 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/code/index.mdx @@ -0,0 +1,24 @@ +import icons from '@/utils/icons' +import LanguageLink from "@/components/LanguageLink"; +import VideoPlayer from "@/components/VideoPlayer"; + +# Overview + + + +Pipedream comes with thousands of prebuilt [triggers](/workflows/building-workflows/triggers/) and [actions](/workflows/contributing/components/#actions) for [hundreds of apps](https://pipedream.com/apps). Often, these will be sufficient for building simple workflows. + +But sometimes you need to run your own custom logic. You may need to make an API request to fetch additional metadata about the event, transform data into a custom format, or end the execution of a workflow early under some conditions. **Code steps let you do this and more**. + +Code steps let you execute [Node.js v{process.env.PIPEDREAM_NODE_VERSION}](https://nodejs.org/) (JavaScript) code, Python, Go or even Bash right in a workflow. + +Choose a language to get started: + +
+ + + + +
+ +If you'd like to see another, specific language supported, please [let us know](https://pipedream.com/community). diff --git a/docs-v2/pages/workflows/building-workflows/code/nodejs/_meta.tsx b/docs-v2/pages/workflows/building-workflows/code/nodejs/_meta.tsx new file mode 100644 index 0000000000000..b311680cc1055 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/code/nodejs/_meta.tsx @@ -0,0 +1,13 @@ +export default { + "index": "Running Node.js in workflows", + "ai-code-generation": "Use AI to generate code", + "auth": "Connect apps", + "http-requests": "HTTP requests", + "working-with-files": "Files", + "using-data-stores": "Data stores", + "delay": "Delaying steps", + "rerun": "Pause, resume, and rerun steps", + "async": "Running asynchronous code", + "sharing-code": "Sharing code across workflows", + "browser-automation": "Browser automation", +} as const diff --git a/docs-v2/pages/code/nodejs/ai-code-generation.mdx b/docs-v2/pages/workflows/building-workflows/code/nodejs/ai-code-generation.mdx similarity index 80% rename from docs-v2/pages/code/nodejs/ai-code-generation.mdx rename to docs-v2/pages/workflows/building-workflows/code/nodejs/ai-code-generation.mdx index 186f8a4dcdad9..a6716d123c291 100644 --- a/docs-v2/pages/code/nodejs/ai-code-generation.mdx +++ b/docs-v2/pages/workflows/building-workflows/code/nodejs/ai-code-generation.mdx @@ -6,9 +6,9 @@ import VideoPlayer from "@/components/VideoPlayer"; Tell Pipedream the code you want, we generate it for you. -![Generate code with AI](https://res.cloudinary.com/pipedreamin/image/upload/v1690472142/docs/2023-07-27_08.34.55_ic1i9m.gif) +![Generate code with AI](https://res.cloudinary.com/pipedreamin/image/upload/v1710515666/docs/docs/workflows/building-workflows/code/nodejs/ai-code-generation/CleanShot_2024-03-15_at_11.13.07_mjgmdc.gif) -Pipedream's [built-in actions](/workflows/steps/actions/) are great for running common API operations without having to write code, but sometimes you need code-level control in a workflow. You can [write this code yourself](/code/), or you can let Pipedream generate it for you with AI. +Pipedream's [built-in actions](/workflows/building-workflows/actions/) are great for running common API operations without having to write code, but sometimes you need code-level control in a workflow. You can [write this code yourself](/workflows/building-workflows/code/), or you can let Pipedream generate it for you with AI. This feature is new, and [we welcome feedback](https://pipedream.com/support). Please let us know what we can improve or add to make this more useful for you. @@ -16,7 +16,7 @@ This feature is new, and [we welcome feedback](https://pipedream.com/support). P Access the feature either from within a Node.js code cell or from any app in the step selector. -![Use AI with the Slack API](https://res.cloudinary.com/pipedreamin/image/upload/v1685132186/docs/docs/Screenshot_2023-05-26_at_1.15.14_PM_c4p2qw.png) +![Use AI with the Slack API](/images/nodejs/ai-code-generation/generating-slack-actions-with-ai.png) A window should pop up and ask for your prompt. Write exactly what you want to do within that step. **Be verbose** and see our tips for [getting the best results](#getting-the-best-results). @@ -37,11 +37,11 @@ Edit the code however you'd like. Once you're done, test the code. You'll see th You can also edit existing code with AI. Click the **Edit with AI** button at the top-right of any Node.js code step. You'll see the code gen window appear with the original code from your step. Enter a prompt to suggest an edit, and we'll give you the modified code. -![Edit code with AI](https://res.cloudinary.com/pipedreamin/image/upload/v1690472898/docs/2023-07-27_08.46.19_ixiikh.gif) +![Edit code with AI](https://res.cloudinary.com/pipedreamin/image/upload/v1710515922/docs/docs/workflows/building-workflows/code/nodejs/ai-code-generation/CleanShot_2024-03-15_at_11.17.20_pumcgn.gif) ## Getting the best results -**Generating code works best with clear, precise, and detailed instructions of what you want to do in your step.** The code gen service understands the [Pipedream component API](/components/api/) and references the API docs of [integrated apps](https://pipedream.com/apps). For example, you can tell it to include specific [props](/components/api/#props) (input) or [async options](/components/api/#async-options-example), and reference specific API endpoints you want to use for the selected app. +**Generating code works best with clear, precise, and detailed instructions of what you want to do in your step.** The code gen service understands the [Pipedream component API](/workflows/contributing/components/api/) and references the API docs of [integrated apps](https://pipedream.com/apps). For example, you can tell it to include specific [props](/workflows/contributing/components/api/#props) (input) or [async options](/workflows/contributing/components/api/#async-options-example), and reference specific API endpoints you want to use for the selected app. ### Examples diff --git a/docs-v2/pages/code/nodejs/async.mdx b/docs-v2/pages/workflows/building-workflows/code/nodejs/async.mdx similarity index 95% rename from docs-v2/pages/code/nodejs/async.mdx rename to docs-v2/pages/workflows/building-workflows/code/nodejs/async.mdx index 385f736c4b9aa..4a68ab72c7444 100644 --- a/docs-v2/pages/code/nodejs/async.mdx +++ b/docs-v2/pages/workflows/building-workflows/code/nodejs/async.mdx @@ -10,7 +10,7 @@ If you're not familiar with asynchronous programming concepts like [callback fun **Any asynchronous code within a Node.js code step must complete before the next step runs**. This ensures future steps have access to its data. If Pipedream detects that code is still running by the time the step completes, you'll see the following warning below the code step: -> **This step was still trying to run code when the step ended. Make sure you await all Promises, or promisify callback functions.** +> **This step was still trying to run code when the step ended. Make sure you await all Promises, or promisify callback functions.** As the warning notes, this often arises from one of two issues: @@ -21,7 +21,7 @@ As the warning notes, this often arises from one of two issues: ### `await` all Promises -Most Node.js packages that run async code return Promises as the result of method calls. For example, [`axios`](/code/nodejs/http-requests/#basic-axios-usage-notes) is an HTTP client. If you make an HTTP request like this in a Pipedream code step: +Most Node.js packages that run async code return Promises as the result of method calls. For example, [`axios`](/workflows/building-workflows/code/nodejs/http-requests/#basic-axios-usage-notes) is an HTTP client. If you make an HTTP request like this in a Pipedream code step: ```javascript const resp = axios({ diff --git a/docs-v2/pages/workflows/building-workflows/code/nodejs/auth.mdx b/docs-v2/pages/workflows/building-workflows/code/nodejs/auth.mdx new file mode 100644 index 0000000000000..87524102b9e88 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/code/nodejs/auth.mdx @@ -0,0 +1,126 @@ +import VideoPlayer from '@/components/VideoPlayer'; + +# Connecting apps in Node.js + + + +When you use [prebuilt actions](/workflows/contributing/components/#actions) tied to apps, you don't need to write the code to authorize API requests. Just [connect your account](/integrations/connected-accounts/#connecting-accounts) for that app and run your workflow. + +But sometimes you'll need to [write your own code](/workflows/building-workflows/code/nodejs/). You can also connect apps to custom code steps, using the auth information to authorize requests to that app. + +For example, you may want to send a Slack message from a step. We use Slack's OAuth integration to authorize sending messages from your workflows. + +To wire up a Slack account to a workflow, define it as a `prop` to the workflow. + +```javascript +import { WebClient } from '@slack/web-api' + +export default defineComponent({ + props: { + // This creates a connection called "slack" that connects a Slack account. + slack: { + type: 'app', + app: 'slack' + } + }, + async run({ steps, $ }) { + const web = new WebClient(this.slack.$auth.oauth_access_token) + + return await web.chat.postMessage({ + text: "Hello, world!", + channel: "#general", + }) + } +}); +``` + +Then click the **Refresh fields** button in the editor to render the Slack field based on the `slack` prop: + +![Refresh the prop defintions to rebuild the steps fields](/images/auth/refresh-fields-after-connecting-slack.png) + +Now the step in the workflow builder will allow you to connect your Slack account: + +![Connect a Slack account to a Node.js code step using a prop](/images/auth/slack-field-rendered.png) + +## Accessing connected account data with `this.appName.$auth` + +In our Slack example above, we created a Slack `WebClient` using the Slack OAuth access token: + +```javascript +const web = new WebClient(this.slack.$auth.oauth_access_token); +``` + +Where did `this.slack` come from? Good question. It was generated by the definition we made in `props`: + +```javascript {4-9} +export default defineComponent({ + props: { + // the name of the app from the key of the prop, in this case it's "slack" + slack: { + // define that this prop is an app + type: 'app', + // define that this app connects to Slack + app: 'slack' + } + } + // ... rest of the Node.js step +``` + +The Slack access token is generated by Pipedream, and is available to this step in the `this.slack.$auth` object: + +```javascript {11} +export default defineComponent({ + props: { + slack: { + type: 'app', + app: 'slack' + } + }, + async run({ steps, $ }) { + async (steps, $) => { + // Authentication details for all of your apps are accessible under the special $ variable: + console.log(this.slack.$auth); + } + }) +}); +``` + +`this.appName.$auth` contains named properties for each account you connect to the associated step. Here, we connected Slack, so `this.slack.$auth` contains the Slack auth info (the `oauth_access_token`). + +The names of the properties for each connected account will differ with the account. Pipedream typically exposes OAuth access tokens as `oauth_access_token`, and API keys under the property `api_key`. But if there's a service-specific name for the tokens (for example, if the service calls it `server_token`), we prefer that name, instead. + +To list the `this.[app name].$auth` properties available to you for a given app, run `Object.keys` on the app: + +```javascript +console.log(Object.keys(this.slack.$auth)) // Replace this.slack with your app's name +``` + +and run your workflow. You'll see the property names in the logs below your step. + +## Writing custom steps to use `this.appName.$auth` + +You can write code that utilizes connected accounts in a few different ways: + +### Using the code templates tied to apps + +When you write custom code that connects to an app, you can start with a code snippet Pipedream provides for each app. This is called the **test request**. + +When you search for an app in a step: + +1. Click the **+** button below any step. +2. Search for the app you're looking for and select it from the list. +3. Select the option to **Use any \ API**. + +This code operates as a template you can extend, and comes preconfigured with the connection to the target app and the code for authorizing requests to the API. You can modify this code however you'd like. + +### Manually connecting apps to steps + +See the Connected Accounts docs for [connecting an account to a code step](/integrations/connected-accounts/#from-a-code-step). + +## Custom auth tokens / secrets + +When you want to connect to a 3rd party service that isn't supported by Pipedream, you can store those secrets in [Environment Variables](/workflows/environment-variables/). + +## Learn more about `props` + +Not only can `props` be used to connect apps to workflow steps, but they can also be used to [collect properties collected from user input](/workflows/building-workflows/code/nodejs/#passing-props-to-code-steps) and [save data between workflow runs](/workflows/building-workflows/code/nodejs/using-data-stores/). diff --git a/docs-v2/pages/code/nodejs/browser-automation.mdx b/docs-v2/pages/workflows/building-workflows/code/nodejs/browser-automation.mdx similarity index 96% rename from docs-v2/pages/code/nodejs/browser-automation.mdx rename to docs-v2/pages/workflows/building-workflows/code/nodejs/browser-automation.mdx index 66c1af9a14f76..931bcccc343d9 100644 --- a/docs-v2/pages/code/nodejs/browser-automation.mdx +++ b/docs-v2/pages/workflows/building-workflows/code/nodejs/browser-automation.mdx @@ -32,7 +32,7 @@ Then using this browser you can open new [Pages](https://pptr.dev/api/puppeteer. export default defineComponent({ async run({steps, $}) { const browser = await puppeteer.browser(); - + // Interact with the web page programmatically // See Puppeeter's Page documentation for available methods: // https://pptr.dev/api/puppeteer.page @@ -61,7 +61,7 @@ Puppeteer can take a full screenshot of a webpage rendered with Chromium. For fu Save a screenshot within the local `/tmp` directory: ```javascript -import { puppeteer } from '@pipedreamhq/platform'; +import { puppeteer } from '@pipedream/browsers'; export default defineComponent({ async run({ $ }) { @@ -107,7 +107,7 @@ Puppeteer can render a PDF of a webpage. For full options [see the Puppeteer Scr Save the PDF locally to `/tmp`: ```javascript -import { puppeteer } from '@pipedreamhq/platform'; +import { puppeteer } from '@pipedream/browsers'; export default defineComponent({ async run({ $ }) { @@ -259,7 +259,7 @@ import { playwright } from '@pipedream/browsers'; export default defineComponent({ async run({steps, $}) { const browser = await playwright.browser(); - + // Interact with the web page programmatically // See Playwright's Page documentation for available methods: // https://playwright.dev/docs/api/class-page @@ -279,7 +279,7 @@ export default defineComponent({ }) ``` - + **Don't forget to close the browser context** Playwright differs from Puppeteer slightly in that you have to close the page's browser context before closing the browser itself. @@ -302,7 +302,7 @@ Playwright can take a full screenshot of a webpage rendered with Chromium. For f Save a screenshot within the local `/tmp` directory: ```javascript -import { playwright } from '@pipedreamhq/platform'; +import { playwright } from '@pipedream/browsers'; export default defineComponent({ async run({ $ }) { @@ -355,7 +355,7 @@ Playwright can render a PDF of a webpage. For full options [see the Playwright S Save a PDF locally to `/tmp`: ```javascript -import { playwright } from '@pipedreamhq/platform'; +import { playwright } from '@pipedream/browsers'; export default defineComponent({ async run({ $ }) { @@ -511,15 +511,15 @@ export default defineComponent({ -## Frequently Asked Questions +## FAQ ### Can I use this package in sources or actions? -Yes, the same `@pipedream/browsers` package can be used in [actions](https://pipedream.com/docs/components/quickstart/nodejs/actions/) as well as [sources](https://pipedream.com/docs/components/quickstart/nodejs/sources/). +Yes, the same `@pipedream/browsers` package can be used in [actions](/workflows/contributing/components/actions-quickstart/) as well as [sources](/workflows/contributing/components/sources-quickstart/). The steps are the same as usage in Node.js code. Open a browser, create a page, and close the browser at the end of the code step. - + **Memory limits** At this time it's not possible to configure the allocated memory to a Source. You may experience a higher rate of Out of Memory errors on Sources that use Puppeteer or Playwright due to the high usage of memory required by Chromium. @@ -531,7 +531,7 @@ Remember to close the browser instance _before_ the step finishes. Otherwise, th ### Out of memory errors or slow starts -For best results, we recommend increasing the amount of memory available to your workflow to 2 gigabytes. You can adjust the available memory in the [workflow settings](https://pipedream.com/docs/workflows/settings/#memory). +For best results, we recommend increasing the amount of memory available to your workflow to 2 gigabytes. You can adjust the available memory in the [workflow settings](/workflows/building-workflows/settings/#memory). ### Which browser are these packages using? diff --git a/docs-v2/pages/workflows/building-workflows/code/nodejs/delay.mdx b/docs-v2/pages/workflows/building-workflows/code/nodejs/delay.mdx new file mode 100644 index 0000000000000..9671f792a5e80 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/code/nodejs/delay.mdx @@ -0,0 +1,140 @@ +import Callout from '@/components/Callout' +import VideoPlayer from '@/components/VideoPlayer' + +# Delaying a workflow + + + +Use `$.flow.delay` to [delay a step in a workflow](/workflows/building-workflows/control-flow/delay/). + +These docs show you how to write Node.js code to handle delays. If you don't need to write code, see [our built-in delay actions](/workflows/building-workflows/control-flow/delay/#delay-actions). + +## Using `$.flow.delay` + +`$.flow.delay` takes one argument: the number of **milliseconds** you'd like to pause your workflow until the next step executes. {process.env.DELAY_MIN_MAX_TIME}. + +Note that [delays happen at the end of the step where they're called](#when-delays-happen). + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + // Delay a workflow for 60 seconds (60,000 ms) + $.flow.delay(60 * 1000) + + // Delay a workflow for 15 minutes + $.flow.delay(15 * 60 * 1000) + + // Delay a workflow based on the value of incoming event data, + // or default to 60 seconds if that variable is undefined + $.flow.delay(steps.trigger.event?.body?.delayMs ?? 60 * 1000) + + // Delay a workflow a random amount of time + $.flow.delay(Math.floor(Math.random() * 1000)) + } +}); +``` + + +Paused workflow state + +When `$.flow.delay` is executed in a Node.js step, the workflow itself will enter a **Paused** state. + +While the workflow is paused, it will not incur any credits towards compute time. You can also [view all paused workflows in the Event History](/workflows/event-history/#filtering-by-status). + + +### Credit usage + +The length of time a workflow is delayed from `$.flow.delay` does _not_ impact your credit usage. For example, delaying a 256 megabyte workflow for five minutes will **not** incur ten credits. + +However, using `$.flow.delay` in a workflow will incur two credits. + +One credit is used to initially start the workflow, then the second credit is used when the workflow resumes after its pause period has ended. + + +Exact credit usage depends on duration and memory configuration + +If your workflow's [execution timeout limit](/workflows/building-workflows/settings/#execution-timeout-limit) is set to longer than [default limit](/workflows/limits/#time-per-execution), it may incur more than two [credits](/pricing/#credits) when using `pd.flow.delay`. + + +## `cancel_url` and `resume_url` + +Both the built-in **Delay** actions and `$.flow.delay` return a `cancel_url` and `resume_url` that lets you cancel or resume paused executions. + +These URLs are specific to a single execution of your workflow. While the workflow is paused, you can load these in your browser or send an HTTP request to either: + +- Hitting the `cancel_url` will immediately cancel that execution +- Hitting the `resume_url` will immediately resume that execution early + +[Since Pipedream pauses your workflow at the _end_ of the step where you run call `$.flow.delay`](#when-delays-happen), you can send these URLs to third party systems, via email, or anywhere else you'd like to control the execution of your workflow. + +```javascript +import axios from 'axios' + +export default defineComponent({ + async run({ steps, $ }) { + const { cancel_url, resume_url } = $.flow.delay(15 * 60 * 1000) + + // Send the URLs to a system you own + await axios({ + method: "POST", + url: `https://example.com`, + data: { cancel_url, resume_url }, + }); + + // Email yourself the URLs. Click on the links to cancel / resume + $.send.email({ + subject: `Workflow execution ${steps.trigger.context.id}`, + text: `Cancel your workflow here: ${cancel_url} . Resume early here: ${resume_url}`, + }); + } +}); + +// Delay happens at the end of this step +``` + +## When delays happen + +**Pipedream pauses your workflow at the _end_ of the step where you call `$.flow.delay`**. This lets you [send the `cancel_url` and `resume_url` to third-party systems](#cancel_url-and-resume_url). + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + const { cancel_url, resume_url } = $.flow.delay(15 * 60 * 1000) + // ... run any code you want here + } +}); + +// Delay happens at the end of this step +``` + +## Delays and HTTP responses + +You cannot run `$.respond` after running `$.flow.delay`. Pipedream ends the original execution of the workflow when `$.flow.delay` is called and issues the following response to the client to indicate this state: + +> $.respond() not called for this invocation + +If you need to set a delay on an HTTP request triggered workflow, consider using [`setTimeout`](#settimeout) instead. + +## `setTimeout` + +Alternatively, you can use `setTimeout` instead of using `$.flow.delay` to delay individual workflow steps. + +However, there are some drawbacks to using `setTimeout` instead of `$.flow.delay`. `setTimeout` will count towards your workflow's compute time, for example: + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + // delay this step for 30 seconds + const delay = 30000; + + return new Promise((resolve, reject) => { + setTimeout(() => { + resolve('timer ended') + }, delay) + }) + } +}); + +``` + +The Node.js step above will hold the workflow's execution for this step for 30 seconds; however, 30 seconds will also _contribute_ to your credit usage. Also consider that workflows have a hard limit of {process.env.MAX_WORKFLOW_EXECUTION_LIMIT} seconds. diff --git a/docs-v2/pages/workflows/building-workflows/code/nodejs/http-requests.mdx b/docs-v2/pages/workflows/building-workflows/code/nodejs/http-requests.mdx new file mode 100644 index 0000000000000..172912e04462b --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/code/nodejs/http-requests.mdx @@ -0,0 +1,667 @@ +import Callout from '@/components/Callout' +import { Tabs } from 'nextra/components' + +# Make HTTP Requests with Node.js + +HTTP requests are fundamental to working with APIs or other web services. You can make HTTP requests to retrieve data from APIs, fetch HTML from websites, or do pretty much anything your web browser can do. + +**Below, we'll review how to make HTTP requests using Node.js code on Pipedream.** + +We'll use the [`axios`](https://github.com/axios/axios) and [`got`](https://github.com/sindresorhus/got) HTTP clients in the examples below, but [you can use any npm package you'd like](/workflows/building-workflows/code/nodejs/#using-npm-packages) on Pipedream, so feel free to experiment with other clients, too. + +If you're developing Pipedream components, you may find the [`@pipedream/platform` version of `axios`](/workflows/building-workflows/http/#platform-axios) helpful for displaying error data clearly in the Pipedream UI. + +If you're new to HTTP, see our [glossary of HTTP terms](https://requestbin.com/blog/working-with-webhooks/#webhooks-glossary-common-terms) for a helpful introduction. + +## Basic `axios` usage notes + +To use `axios` on Pipedream, you'll just need to import the `axios` npm package: + +```javascript +import axios from "axios"; +``` + +You make HTTP requests by passing a [JavaScript object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects) to `axios` that defines the parameters of the request. For example, you'll typically want to define the HTTP method and the URL you're sending data to: + +```javascript +{ + method: "GET", + url: `https://swapi.dev/api/films/` +} +``` + +`axios` returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises), which is just a fancy way of saying that it makes the HTTP request in the background (asynchronously) while the rest of your code runs. On Pipedream, [all asynchronous code must be run synchronously](/workflows/building-workflows/code/nodejs/async/), which means you'll need to wait for the HTTP request to finish before moving on to the next step. You do this by adding an `await` in front of the call to `axios`. + +**Putting all of this together, here's how to make a basic HTTP request on Pipedream:** + +```javascript +const resp = await axios({ + method: "GET", + url: `https://swapi.dev/api/films/`, +}); +``` + +The response object `resp` contains a lot of information about the response: its data, headers, and more. Typically, you just care about the data, which you can access in the `data` property of the response: + +```javascript +const resp = await axios({ + method: "GET", + url: `https://swapi.dev/api/films/`, +}); + +// HTTP response data is in the data property +const data = resp.data; +``` + +Alternatively, you can access the data using [object destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring), which is equivalent to the above and preferred in modern JavaScript: + +```javascript +const { data } = resp; +``` + +## Send a `GET` request to fetch data + +Make a request to retrieve Star Wars films from the Star Wars API: + + + +``` javascript +import axios from "axios"; + +export default defineComponent({ + async run({ steps, $ }) { + // Make an HTTP GET request using axios + const res = await axios({ + method: "GET", + url: `https://swapi.dev/api/films/`, + }); + + // Retrieve just the data from the response + const { data } = res; + } +}); +``` + + +``` javascript +export default defineComponent({ + props: { + httpRequest: { + type: "http_request", + label: "Star Wars API request", + default: { + method: "GET", + url: "https://swapi.dev/api/films/" + } + }, + }, + async run({ steps, $ }) { + // Make an HTTP GET request using the http-request + const res = await this.httpRequest.execute(); + + // Retrieve just the data from the response + const { data } = res; + }, +}) +``` + +**Produces** + +![With the http-request prop](https://res.cloudinary.com/pipedreamin/image/upload/v1649961271/docs/components/CleanShot_2022-04-14_at_14.34.16_2x_c0urph.png) + + + +## Send a `POST` request to submit data + +POST sample JSON to [JSONPlaceholder](https://jsonplaceholder.typicode.com/), a free mock API service: + + + +``` javascript +import axios from "axios"; + +export default defineComponent({ + async run({ steps, $ }) { + // Make an HTTP POST request using axios + const resp = await axios({ + method: "POST", + url: `https://jsonplaceholder.typicode.com/posts`, + data: { + name: "Luke", + }, + }); + + // Retrieve just the data from the response + const { data } = resp; + } +}); +``` + + +``` javascript +export default defineComponent({ + props: { + httpRequest: { + type: "http_request", + label: "JSON Placeholder API request", + default: { + method: "POST", + url: "https://jsonplaceholder.typicode.com/posts", + body: { + contentType: "application/json", + fields: [{ name: "Luke" }] + } + } + }, + }, + async run({ steps, $ }) { + // Make an HTTP GET request using the http-request + const res = await this.httpRequest.execute(); + + // Retrieve just the data from the response + const { data } = res; + }, +}) +``` + + + +When you make a `POST` request, you pass `POST` as the `method`, and include the data you'd like to send in the `data` object. + +## Pass query string parameters to a `GET` request + +Retrieve fake comment data on a specific post using [JSONPlaceholder](https://jsonplaceholder.typicode.com/), a free mock API service. Here, you fetch data from the `/comments` resource, retrieving data for a specific post by query string parameter: `/comments?postId=1`. + + + +``` javascript +import axios from "axios"; + +export default defineComponent({ + async run({ steps, $ }) { + // Make an HTTP GET request using axios + const resp = await axios({ + method: "GET", + url: `https://jsonplaceholder.typicode.com/comments`, + params: { + postId: 1, + }, + }); + + // Retrieve just the data from the response + const { data } = resp; + } +}); +``` + + +``` javascript +export default defineComponent({ + props: { + httpRequest: { + type: "http_request", + label: "JSON Placeholder API request", + default: { + method: "GET", + url: "https://jsonplaceholder.typicode.com/comments", + params: { + fields: [{ postId: 1 }] + } + } + }, + }, + async run({ steps, $ }) { + // Make an HTTP GET request using the http-request + const res = await this.httpRequest.execute(); + + // Retrieve just the data from the response + const { data } = res; + }, +}) +``` + +You should pass query string parameters using the `params` object, like above. When you do, `axios` automatically [URL-encodes](https://www.w3schools.com/tags/ref_urlencode.ASP) the parameters for you, which you'd otherwise have to do manually. + + + +## Send a request with HTTP headers + +You pass HTTP headers in the `headers` object of the `axios` request: + +```javascript +import axios from "axios"; + +// Make an HTTP POST request using axios +const resp = await axios({ + method: "POST", + url: `https://jsonplaceholder.typicode.com/posts`, + headers: { + "Content-Type": "application/json", + }, + data: { + name: "Luke", + }, +}); +``` + +## Send a request with a secret or API key + +Most APIs require you authenticate HTTP requests with an API key or other token. **Please review the docs for your service to understand how they accept this data.** + +Here's an example showing an API key passed in an HTTP header: + +```javascript +import axios from "axios"; + +// Make an HTTP POST request using axios +const resp = await axios({ + method: "POST", + url: `https://jsonplaceholder.typicode.com/posts`, + headers: { + "Content-Type": "application/json", + "X-API-Key": "123", // API KEY + }, + data: { + name: "Luke", + }, +}); +``` + +[Copy this workflow to run this code on Pipedream](https://pipedream.com/@dylburger/send-an-http-request-with-headers-p_q6ClzO/edit). + +## Send multiple HTTP requests in sequence + +There are many ways to make multiple HTTP requests. This code shows you a simple example that sends the numbers `1`, `2`, and `3` in the body of an HTTP POST request: + +```javascript +import axios from "axios"; + +export default defineComponent({ + async run({ steps, $ }) { + // We'll store each response and return them in this array + const responses = []; + + for await (const num of [1, 2, 3]) { + const resp = await axios({ + method: "POST", + url: "https://example.com", + data: { + num, // Will send the current value of num in the loop + }, + }); + responses.push(resp.data); + } + + return responses; + }, +}); +``` + +This sends each HTTP request _in sequence_, one after another, and returns an array of response data returned from the URL to which you send the POST request. If you need to make requests _in parallel_, [see these docs](#send-multiple-http-requests-in-parallel). + +[Copy this workflow](https://pipedream.com/@dylburger/iterate-over-a-pipedream-step-export-sending-multiple-http-requests-p_ljCAPN/edit) and fill in your destination URL to see how this works. **This workflow iterates over the value of a Pipedream [step export](/workflows/#step-exports)** - data returned from a previous step. Since you often want to iterate over data returned from a Pipedream action or other code step, this is a common use case. + +## Send multiple HTTP requests in parallel + +Sometimes you'll want to make multiple HTTP requests in parallel. If one request doesn't depend on the results of another, this is a nice way to save processing time in a workflow. It can significantly cut down on the time you spend waiting for one request to finish, and for the next to begin. + +To make requests in parallel, you can use two techniques. By default, we recommend using [`promise.allSettled`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled), which makes all HTTP requests and returns data on their success / failure. If an HTTP request fails, all other requests will proceed. + +```javascript +import axios from "axios"; + +export default defineComponent({ + async run({ steps, $ }) { + const arr = [ + "https://www.example.com", + "https://www.cnn.com", + "https://www.espn.com", + ]; + const promises = arr.map((url) => axios.get(url)); + return Promise.allSettled(promises); + }, +}); +``` + +First, we generate an array of `axios.get` requests (which are all [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)), and then call `Promise.allSettled` to run them in parallel. + +When you want to stop future requests when _one_ of the requests fails, you can use [`Promise.all`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all), instead: + +```javascript +import axios from "axios"; + +export default defineComponent({ + async run({ steps, $ }) { + const arr = [ + "https://www.example.com", + "https://www.cnn.com", + "https://www.espn.com", + ]; + const promises = arr.map((url) => axios.get(url)); + return Promise.all(promises); + }, +}); +``` + +The Mozilla docs expand on the difference between these methods, and when you may want to use one or the other: + +> The `Promise.allSettled()` method returns a promise that resolves after all of the given promises have either fulfilled or rejected, with an array of objects that each describes the outcome of each promise.
+> It is typically used when you have multiple asynchronous tasks that are not dependent on one another to complete successfully, or you'd always like to know the result of each promise.
+> In comparison, the Promise returned by `Promise.all()` may be more appropriate if the tasks are dependent on each other / if you'd like to immediately reject upon any of them rejecting. + +## Send a `multipart/form-data` request + + + + +``` javascript +import axios from "axios"; +import FormData from "form-data"; + +export default defineComponent({ + async run({ steps, $ }) { + const formData = new FormData(); + formData.append("name", "Luke Skywalker"); + + const headers = formData.getHeaders(); + const config = { + method: "POST", + url: "https://example.com", + headers, + data: formData, + }; + return await axios(config); + } +}); +``` + + + + +``` javascript +export default defineComponent({ + props: { + httpRequest: { + type: "http_request", + label: "Example Multipart Form Request", + default: { + method: "POST", + url: "https://example.com", + headers: { + contentType: "multipart/form-data", + fields: [{ name: "Luke Skywalker" }] + } + } + }, + }, + async run({ steps, $ }) { + // Make an HTTP GET request using the http-request + const res = await this.httpRequest.execute(); + + // Retrieve just the data from the response + const { data } = res; + }, +}) +``` + + + +[Copy this workflow](https://pipedream.com/@dylburger/send-a-multipart-form-data-request-p_WxCQRyr/edit) to run this example. + +## Download a file to the `/tmp` directory + +This example shows you how to download a file to a file in [the `/tmp` directory](/workflows/building-workflows/code/nodejs/working-with-files/). This can be especially helpful for downloading large files: it streams the file to disk, minimizing the memory the workflow uses when downloading the file. + +```javascript +import { pipeline } from "stream/promises"; +import fs from "fs"; +import got from "got"; + +export default defineComponent({ + async run({ steps, $ }) { + // Download the webpage HTML file to /tmp + return await pipeline( + got.stream("https://example.com"), + fs.createWriteStream('/tmp/file.html') + ); + } +}) +``` + +[Copy this workflow](https://pipedream.com/new?h=tch_wqKfoW) to run this example. + +## Upload a file from the `/tmp` directory + +This example shows you how to make a `multipart/form-data` request with a file as a form part. You can store and read any files from [the `/tmp` directory](/workflows/building-workflows/code/nodejs/working-with-files/#the-tmp-directory). + +This can be especially helpful for uploading large files: it streams the file from disk, minimizing the memory the workflow uses when uploading the file. + +```javascript +import axios from "axios"; +import fs from "fs"; +import FormData from "form-data"; + +export default defineComponent({ + async run({ steps, $ }) { + const formData = new FormData(); + formData.append("file", fs.createReadStream('/tmp/file.pdf')); + const headers = formData.getHeaders(); + + const config = { + method: "POST", + url: "https://example.com", + headers, + data: formData, + }; + return await axios(config); + } +}); +``` + +[Copy this workflow](https://pipedream.com/new?h=tch_Oknf4r) to run this example. + +## IP addresses for HTTP requests made from Pipedream workflows + +By default, [HTTP requests made from Pipedream can come from a large range of IP addresses](/privacy-and-security/#hosting-details). **If you need to restrict the IP addresses HTTP requests come from, you have two options**: + +- [Use a Pipedream VPC](/workflows/vpc/) to route all outbound HTTP requests through a single IP address +- If you don't need to access the HTTP response data, you can [use `$send.http()`](/workflows/data-management/destinations/http/) to send requests from a [limited set of IP addresses](/workflows/data-management/destinations/http/#ip-addresses-for-pipedream-http-requests). + + +## Use an HTTP proxy to proxy requests through another host + +By default, HTTP requests made from Pipedream can come from a range of IP addresses. **If you need to make requests from a single IP address, you can route traffic through an HTTP proxy**: + +```javascript +import axios from "axios"; +import httpsProxyAgent from "https-proxy-agent"; + +export default defineComponent({ + props: { + user: { + type: 'string', + label: 'Username', + description: 'The username for the HTTP proxy authentication', + }, + pass: { + type: 'string', + label: 'Password', + secret: true, + description: 'The password for the HTTP proxy authentication', + }, + host: { + type: 'string', + label: "HTTP Proxy Host", + description: "The URL for the HTTP proxy", + }, + port: { + type: "string", + label: "Port", + description: "The port the HTTP proxy is accepting requests at", + }, + target_host: { + type: 'string', + label: "Target Host", + description: "The URL for the end target to reach through the proxy", + }, + method: { + type: 'string', + default: 'GET', + label: "HTTP method", + description: "The HTTP method to use to reach the end target host" + }, + body: { + type: 'object', + label: "HTTP body", + description: "The HTTP body payload to send to the end target host" + } + }, + async run({ steps, $ }) { + const { user, pass, host, port, target_host, method } = this; + const agent = new httpsProxyAgent(`http://${user}:${pass}@${host}:${port}`); + + const config = { + method, + url: target_host, + body, + httpsAgent: agent, + }; + + return await axios.request(config); + } +}); +``` + +[Copy this workflow to run this code on Pipedream](https://pipedream.com/new?h=tch_mypfby). + +## Stream a downloaded file directly to another URL + +Sometimes you need to upload a downloaded file directly to another service, without processing the downloaded file. You could [download the file](#download-a-file-to-the-tmp-directory) and then [upload it](#upload-a-file-from-the-tmp-directory) to the other URL, but these intermediate steps are unnecessary: you can just stream the download to the other service directly, without saving the file to disk. + +This method is especially effective for large files that exceed the [limits of the `/tmp` directory](/workflows/limits/#disk). + +[Copy this workflow](https://pipedream.com/@dylburger/stream-download-to-upload-p_5VCLoa1/edit) or paste this code into a [new Node.js code step](/workflows/building-workflows/code/nodejs/): + +```javascript +import stream from "stream"; +import { promisify } from "util"; +import got from "got"; + +export default defineComponent({ + async run({ steps, $ }) { + const pipeline = promisify(stream.pipeline); + + await pipeline( + got.stream("https://example.com"), + got.stream.post("https://example2.com") + ); + } +}); +``` + +You'll want to replace `https://example.com` with the URL you'd like to stream data from, and replace `https://example2.com` with the URL you'd like to send the data _to_. `got` streams the content directly, downloading the file using a `GET` request and uploading it as a `POST` request. + +If you need to modify this behavior, [see the `got` Stream API](https://github.com/sindresorhus/got#gotstreamurl-options). + +## Catch and process HTTP errors + +By default, `axios` throws an error when the HTTP response code is in the 400-500 range (a client or server error). If you'd like to process the error data instead of throwing an error, you can pass a custom function to the `validateStatus` property: + +```javascript +import axios from "axios"; + +export default defineComponent({ + async run({ steps, $ }) { + const resp = await axios({ + url: "https://httpstat.us/400", + validateStatus: () => true, // will not throw error when axios gets a 400+ status code (the default behavior) + }); + if (resp.status >= 400) { + this.debug = resp; + throw new Error(JSON.stringify(resp.data)); // This can be modified to throw any error you'd like + } + return resp; + } +}); +``` + +See [the `axios` docs](https://github.com/axios/axios#request-config) for more details. + +## Paginating API requests + +When you fetch data from an API, the API may return records in "pages". For example, if you're trying to fetch a list of 1,000 records, the API might return those in groups of 100 items. + +Different APIs paginate data in different ways. You'll need to consult the docs of your API provider to see how they suggest you paginate through records. + +## Send GraphQL request + +Make a GraphQL request using the `graphql-request` NPM package: + +```javascript +import { graphql } from 'graphql' +import { request, gql } from 'graphql-request' + +export default defineComponent({ + async run({ steps, $ }) { + const document = gql` + query samplePokeAPIquery { + generations: pokemon_v2_generation { + name + pokemon_species: pokemon_v2_pokemonspecies_aggregate { + aggregate { + count + } + } + } + } + ` + return await request('https://beta.pokeapi.co/graphql/v1beta', document) + }, +}) +``` + + +The graphql package is required + +The `graphql` package is required for popular GraphQL clients to function, like `graphql-request` and `urql`. + +Even though you will not need to use the `graphql` code itself in your code step, it's required to import it in order for `graphql-request` to function. + + +### Send an authenticated GraphQL request + +Authenticate your connected accounts in Pipedream with GraphQL requests using the `app` prop: + +```javascript +import { graphql } from 'graphql' +import { GraphQLClient, gql } from 'graphql-request' + +export default defineComponent({ + props: { + github: { + type: 'app', + app: 'github' + } + }, + async run({ steps, $ }) { + const me = gql` + query { + viewer { + login + } + } + ` + + const client = new GraphQLClient('https://api.github.com/graphql', { + headers: { + authorization: `Bearer ${this.github.$auth.oauth_access_token}`, + }, + }) + + return await client.request(me) + }, +}) + +``` + +Alternatively, you can use Environment Variables as well for simple API key based GraphQL APIs. diff --git a/docs-v2/pages/workflows/building-workflows/code/nodejs/index.mdx b/docs-v2/pages/workflows/building-workflows/code/nodejs/index.mdx new file mode 100644 index 0000000000000..f633f65e9700c --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/code/nodejs/index.mdx @@ -0,0 +1,505 @@ +import Callout from '@/components/Callout' +import VideoPlayer from "@/components/VideoPlayer"; + +# Running Node.js in workflows + +Pipedream supports writing Node.js v{process.env.PIPEDREAM_NODE_VERSION} at any point of a workflow. + +Anything you can do with Node.js, you can do in a workflow. This includes using most of [npm's 400,000+ packages](#using-npm-packages). JavaScript is one of the [most used](https://insights.stackoverflow.com/survey/2019#technology-_-programming-scripting-and-markup-languages) [languages](https://github.blog/2018-11-15-state-of-the-octoverse-top-programming-languages/) in the world, with a thriving community and extensive package ecosystem. If you work on websites and know JavaScript well, Pipedream makes you a full stack engineer. If you've never used JavaScript, see the [resources below](#new-to-javascript). + + +It's important to understand the core difference between Node.js and the JavaScript that runs in your web browser: **Node doesn't have access to some of the things a browser expects, like the HTML on the page, or its URL**. If you haven't used Node before, be aware of this limitation as you search for JavaScript examples on the web. + + +## Adding a code step + +1. Click the **+** button below any step of your workflow. +2. Select the option to **Run custom code**. + +Note that new code steps will default to Node.js v{process.env.PIPEDREAM_NODE_VERSION}. You can add any Node.js code in the editor that appears. For example, try: + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + console.log("This is Node.js code"); + $.export("test", "Some test data"); + return "Test data"; + }, +}); +``` + +Code steps use the same editor ([Monaco](https://microsoft.github.io/monaco-editor/)) used in Microsoft's [VS Code](https://code.visualstudio.com/), which supports syntax highlighting, automatic indentation, and more. + +## Sharing data between steps + +A Node.js step can use data from other steps using [step exports](/workflows/#step-exports), it can also export data for other steps to use. + +### Using data from another step + +In Node.js steps, data from the initial workflow trigger and other steps are available in the `steps` argument passed to the `run({ steps, $ })` function. + +In this example, we'll pretend this data is coming into our HTTP trigger via POST request. + +```json +{ + "id": 1, + "name": "Bulbasaur", + "type": "plant" +} +``` + +In our Node.js step, we can access this data in the `steps` variable Specifically, this data from the POST request into our workflow is available in the `trigger` property. + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + const pokemonName = steps.trigger.event.name; + const pokemonType = steps.trigger.event.type; + + console.log(`${pokemonName} is a ${pokemonType} type Pokemon`); + }, +}); +``` + +### Sending data downstream to other steps + +To share data created, retrieved, transformed or manipulated by a step to others downstream you can simply `return` it. + +```javascript +// This step is named "code" in the workflow +import axios from "axios"; + +export default defineComponent({ + async run({ steps, $ }) { + const response = await axios.get( + "https://pokeapi.co/api/v2/pokemon/charizard" + ); + // Store the response's JSON contents into a variable called "pokemon" + const pokemon = response.data; + + // Expose the pokemon data downstream to other steps in the $return_value from this step + return pokemon; + }, +}); +``` + +### Using $.export + + + +Alternatively, use the built in `$.export` helper instead of returning data. The `$.export` creates a _named_ export with the given value. + +```javascript +// This step is named "code" in the workflow +import axios from "axios"; + +export default defineComponent({ + async run({ steps, $ }) { + const response = await axios.get( + "https://pokeapi.co/api/v2/pokemon/charizard" + ); + // Store the response's JSON contents into a variable called "pokemon" + const pokemon = response.data; + + // Expose the pokemon data downstream to other steps in the pokemon export from this step + $.export("pokemon", pokemon); + }, +}); +``` + +Now this `pokemon` data is accessible to downstream steps within `steps.code.pokemon` + + +You can only export JSON-serializable data from steps. Things like: + +- strings +- numbers +- objects + +You cannot export functions or other complex objects that don't serialize to JSON. [You can save that data to a file in the `/tmp` directory](/workflows/building-workflows/code/nodejs/working-with-files/). + + +## Passing props to code steps + + + +You can make code steps reusable by allowing them to accept props. Instead of hard-coding the values of variables within the code itself, you can pass them to the code step as arguments or parameters _entered in the workflow builder_. + +For example, let's define a `firstName` prop. This will allow us to freely enter text from the workflow builder. + +```javascript +export default defineComponent({ + props: { + firstName: { + type: "string", + label: "Your first name", + default: "Dylan", + }, + }, + async run({ steps, $ }) { + console.log( + `Hello ${this.firstName}, congrats on crafting your first prop!` + ); + }, +}); +``` + +The workflow builder now can accept text input to populate the `firstName` to this particular step only: + +![Workflow builder displaying the input visually as a text input field](/images/nodejs/first-name-prop-example.png) + +Accepting a single string is just one example, you can build a step to accept arrays of strings through a dropdown presented in the workflow builder. + +[Read the props reference for the full list of options](/workflows/contributing/components/api/#props). + +## How Pipedream Node.js components work + + + +When you add a new Node.js code step or use the examples in this doc, you'll notice a common structure to the code: + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + // this Node.js code will execute when the step runs + }, +}); +``` + +This defines [a Node.js component](/workflows/contributing/components/api/). Components let you: + +- Pass input to steps using [props](/workflows/building-workflows/code/nodejs/#passing-props-to-code-steps) +- [Connect an account to a step](/integrations/connected-accounts/#from-a-code-step) +- [Issue HTTP responses](/workflows/building-workflows/triggers/#http-responses) +- Perform workflow-level flow control, like [ending a workflow early](#ending-a-workflow-early) + +When the step runs, Pipedream executes the `run` method: + +- Any asynchronous code within a code step [**must** be run synchronously](/workflows/building-workflows/code/nodejs/async/), using the `await` keyword or with a Promise chain, using `.then()`, `.catch()`, and related methods. +- Pipedream passes the `steps` variable to the run method. `steps` is also an object, and contains the [data exported from previous steps](/workflows/#step-exports) in your workflow. +- You also have access to the `$` variable, which gives you access to methods like `$.respond`, `$.export`, [and more](/workflows/contributing/components/api/#actions). + +If you're using [props](/workflows/building-workflows/code/nodejs/#passing-props-to-code-steps) or [connect an account to a step](/integrations/connected-accounts/#from-a-code-step), the component exposes them in the variable `this`, which refers to the current step: + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + // `this` refers to the running component. Props, connected accounts, etc. are exposed here + console.log(this); + }, +}); +``` + +When you [connect an account to a step](/integrations/connected-accounts/#from-a-code-step), Pipedream exposes the auth info in the variable [`this.appName.$auth`](/workflows/building-workflows/code/nodejs/auth/#accessing-connected-account-data-with-thisappnameauth). + +## Logs + +You can call `console.log` or `console.error` to add logs to the execution of a code step. + +These logs will appear just below the associated step. `console.log` messages appear in black, `console.error` in red. + +### `console.dir` + +If you need to print the contents of JavaScript objects, use `console.dir`: + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + console.dir({ + name: "Luke", + }); + }, +}); +``` + +## Syntax errors + +Pipedream will attempt to catch syntax errors when you're writing code, highlighting the lines where the error occurred in red. + + +While you can save a workflow with syntax errors, it's unlikely to run correctly on new events. Make sure to fix syntax errors before running your workflow. + + +## Using `npm` packages + + + +[npm](https://www.npmjs.com/) hosts JavaScript packages: libraries of code someone else wrote and packaged for others to use. npm has over 400,000 packages and counting. + +### Just `import` it + +To use an npm package on Pipedream, simply `import` it: + +```javascript +import axios from "axios"; +``` + +By default, workflows don't have any packages installed. Just import any package in this manner to make it available in the step. + +If a package only supports the [CommonJS module format](https://nodejs.org/api/modules.html), you may have to `require` it: + +```javascript +const axios = require("axios"); +``` + +**Within a single step, you can only use `import` or `require` statements, not both**. See [this section](#require-is-not-defined) for more details. + +When Pipedream runs your workflow, we download the associated npm package for you before running your code steps. + +If you've used Node before, you'll notice there's no `package.json` file to upload or edit. We want to make package management simple, so just `import` or `require` the module like you would in your code, after package installation, and get to work. + +### Third-party package limitations + +Some packages require access to a web browser to run, and don't work with Node.js. Often this limitation is documented on the package `README`, but often it's not. If you're not sure and need to use it, we recommend just trying to `import` or `require` it. + +Other packages require access to binaries or system libraries that aren't installed in the Pipedream execution environment. + +If you're seeing any issues with a specific package, please [let us know](https://pipedream.com/support/) and we'll try to help you make it work. + +### Pinning package versions + +Each time you deploy a workflow with Node.js code, Pipedream downloads the npm packages you `import` in your step. **By default, Pipedream deploys the latest version of the npm package each time you deploy a change**. + +There are many cases where you may want to specify the version of the packages you're using. If you'd like to use a _specific_ version of a package in a workflow, you can add that version in the `import` string, for example: + +```javascript +import axios from "axios@0.19.2"; +``` + +You can also pass the version specifiers used by npm to support [semantic version](https://semver.org/) upgrades. For example, to allow for future patch version upgrades: + +```javascript +import axios from "axios@~0.20.0"; +``` + +To allow for patch and minor version upgrades, use: + +```javascript +import got from "got@^11.0.0"; +``` + + +The behavior of the caret (`^`) operator is different for 0.x versions, for which it will only match patch versions, and not minor versions. + + +You can also specify different versions of the same package in different steps. Each step will used the associated version. Note that this also increases the size of your deployment, which can affect cold start times. + +### CommonJS vs. ESM imports + +In Node.js, you may be used to importing third-party packages using the `require` statement: + +```javascript +const axios = require("axios"); +``` + +In this example, we're including the `axios` [CommonJS module](https://nodejs.org/api/modules.html) published to npm. You import CommonJS modules using the `require` statement. + +But you may encounter this error in workflows: + +`Error Must use import to load ES Module` + +This means that the package you're trying to `require` uses a different format to export their code, called [ECMAScript modules](https://nodejs.org/api/esm.html#esm_modules_ecmascript_modules) (**ESM**, or **ES modules**, for short). With ES modules, you instead need to `import` the package: + +```javascript +import got from "got"; +``` + +Most package publish both CommonJS and ESM versions, so **if you always use `import`, you're less likely to have problems**. In general, refer to the documentation for your package for instructions on importing it correctly. + +### `require` is not defined + +This error means that you cannot use CommonJS and ESM imports in the same step. For example, if you run code like this: + +```javascript +import fetch from "node-fetch"; +const axios = require("axios"); +``` + +your workflow will throw a `require is not defined` error. There are two solutions: + +1. Try converting your CommonJS `require` statement into an ESM `import` statement. For example, convert this: + +```javascript +const axios = require("axios"); +``` + +to this: + +```javascript +import axios from "axios"; +``` + +2. If the `import` syntax fails to work, separate your imports into different steps, using only CommonJS requires in one step, and only ESM imports in another. + +## Variable scope + +Any variables you create within a step are scoped to that step. That is, they cannot be referenced in any other step. + +Within a step, the [normal rules of JavaScript variable scope](https://developer.mozilla.org/en-US/docs/Glossary/Scope) apply. + +**When you need to share data across steps, use [step exports](/workflows/#step-exports).** + +## Making HTTP requests from your workflow + +There are two ways to make HTTP requests in code steps: + +- Use any HTTP client that works with Node.js. [See this example guide for how to use `axios` to make HTTP requests](/workflows/building-workflows/code/nodejs/http-requests/). +- [Use `$.send.http()`](/workflows/data-management/destinations/http/#using-sendhttp-in-workflows), a Pipedream-provided method for making asynchronous HTTP requests. + +In general, if you just need to make an HTTP request but don't care about the response, [use `$.send.http()`](/workflows/data-management/destinations/http/#using-sendhttp-in-workflows). If you need to operate on the data in the HTTP response in the rest of your workflow, [use `axios`](/workflows/building-workflows/code/nodejs/http-requests/). + +## Returning HTTP responses + +You can return HTTP responses from [HTTP-triggered workflows](/workflows/building-workflows/triggers/#http) using the [`$.respond()` function](/workflows/building-workflows/triggers/#http-responses). + +## Invoke another workflow + + +This is an alpha feature and is subject to change without prior notice. + + +You can invoke another workflow in your workspace with `$.flow.trigger`: + +``` +await $.flow.trigger( + workflowId, // your Pipedream workflow ID, e.g. p_abc123 + payload, // any JSON-serializable data +) +``` + +[Find your workflow's ID here.](/troubleshooting/#where-do-i-find-my-workflows-id) + +This invokes the workflow directly -- you don't need to configure a trigger, and the request does not leave the platform. + +We also provide a [Trigger Workflow](https://pipedream.com/apps/helper-functions/actions/trigger-workflow) action in the [Helper Functions](https://pipedream.com/apps/helper-functions) app so you don't need to do it by code! + +## Ending a workflow early + + + +Sometimes you want to end your workflow early, or otherwise stop or cancel the execution or a workflow under certain conditions. For example: + +- You may want to end your workflow early if you don't receive all the fields you expect in the event data. +- You only want to run your workflow for 5% of all events sent from your source. +- You only want to run your workflow for users in the United States. If you receive a request from outside the U.S., you don't want the rest of the code in your workflow to run. +- You may use the `user_id` contained in the event to look up information in an external API. If you can't find data in the API tied to that user, you don't want to proceed. + +**In any code step, calling `return $.flow.exit()` will end the execution of the workflow immediately.** No remaining code in that step, and no code or destination steps below, will run for the current event. + + +It's a good practice to use `return $.flow.exit()` to immediately exit the workflow. +In contrast, `$.flow.exit()` on its own will end the workflow only after executing all remaining code in the step. + + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + return $.flow.exit(); + console.log( + "This code will not run, since $.flow.exit() was called above it" + ); + }, +}); +``` + +You can pass any string as an argument to `$.flow.exit()`: + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + return $.flow.exit("End message"); + }, +}); +``` + +Or exit the workflow early within a conditional: + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + // Flip a coin, running $.flow.exit() for 50% of events + if (Math.random() > 0.5) { + return $.flow.exit(); + } + console.log("This code will only run 50% of the time"); + }, +}); +``` + +## Errors + +[Errors](https://nodejs.org/dist/latest-v10.x/docs/api/errors.html#errors_errors) raised in a code step will stop the execution of code or destinations that follow. + +### Configuration Error + +Throwing a `ConfigurationError` in a Node.js step will display the error message in a dedicated area. + +This is useful for providing feedback during validation of `props`. In the example below, a required Header value is missing from the Google Sheets action: + +![Example of an ConfigurationError](/images/nodejs/configuration-error-example.png) + +Or you can use it for validating the format of a given `email` prop: + +```javascript +import { ConfigurationError } from "@pipedream/platform"; + +export default defineComponent({ + props: { + email: { type: "string" }, + }, + async run({ steps, $ }) { + // if the email address doesn't include a @, it's not valid + if (!this.email.includes("@")) { + throw new ConfigurationError("Provide a valid email address"); + } + }, +}); +``` + +## Using secrets in code + +Workflow code is private. Still, we recommend you don't include secrets — API keys, tokens, or other sensitive values — directly in code steps. + +Pipedream supports [environment variables](/workflows/environment-variables/) for keeping secrets separate from code. Once you create an environment variable in Pipedream, you can reference it in any workflow using `process.env.VARIABLE_NAME`. The values of environment variables are private. + +See the [Environment Variables](/workflows/environment-variables/) docs for more information. + +## Limitations of code steps + +Code steps operate within the [general constraints on workflows](/workflows/limits/#workflows). As long as you stay within those limits and abide by our [acceptable use policy](/workflows/limits/#acceptable-use), you can add any number of code steps in a workflow to do virtually anything you'd be able to do in Node.js. + +If you're trying to run code that doesn't work or you have questions about any limits on code steps, [please reach out](https://pipedream.com/support/). + +## Editor settings + +We use the [Monaco Editor](https://microsoft.github.io/monaco-editor/), which powers VS Code and other web-based editors. + +We also let you customize the editor. For example, you can enable Vim mode, and change the default tab size for indented code. Visit your [Settings](https://pipedream.com/settings) to modify these settings. + +## Keyboard Shortcuts + +We use the [Monaco Editor](https://microsoft.github.io/monaco-editor/), which powers VS Code. As a result, many of the VS Code [keyboard shortcuts](https://code.visualstudio.com/docs/getstarted/keybindings) should work in the context of the editor. + +For example, you can use shortcuts to search for text, format code, and more. + +## New to JavaScript? + +We understand many of you might be new to JavaScript, and provide resources for you to learn the language below. + +When you're searching for how to do something in JavaScript, some of the code you try might not work in Pipedream. This could be because the code expects to run in a browser, not a Node.js environment. The same goes for [npm packages](#using-npm-packages). + +### I'm new to programming + +Many of the most basic JavaScript tutorials are geared towards writing code for a web browser to run. This is great for learning — a webpage is one of the coolest things you can build with code. We recommend starting with these general JavaScript tutorials and trying the code you learn on Pipedream: + +- [JavaScript For Cats](http://jsforcats.com/) +- [Mozilla - JavaScript First Steps](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/First_steps) +- [StackOverflow](https://stackoverflow.com/) operates a programming Q&A site that typically has the first Google result when you're searching for something specific. It's a great place to find answers to common questions. + +### I know how to code, but don't know JavaScript + +- [A re-introduction to JavaScript (JS tutorial)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/A_re-introduction_to_JavaScript) +- [MDN language overview](https://developer.mozilla.org/en-US/docs/Web/JavaScript) +- [Eloquent Javascript](https://eloquentjavascript.net/) +- [Node School](https://nodeschool.io/) +- [You Don't Know JS](https://github.com/getify/You-Dont-Know-JS) diff --git a/docs-v2/pages/workflows/building-workflows/code/nodejs/rerun.mdx b/docs-v2/pages/workflows/building-workflows/code/nodejs/rerun.mdx new file mode 100644 index 0000000000000..877f73609258e --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/code/nodejs/rerun.mdx @@ -0,0 +1,266 @@ +import Callout from "@/components/Callout"; +import VideoPlayer from "@/components/VideoPlayer"; + +# Pause, resume, and rerun a workflow + +You can use `$.flow.suspend` and `$.flow.rerun` to pause a workflow and resume it later. + +This is useful when you want to: + +- Pause a workflow until someone manually approves it +- Poll an external API until some job completes, and proceed with the workflow when it's done +- Trigger an external API to start a job, pause the workflow, and resume it when the external API sends an HTTP callback + +We'll cover all of these examples below. + +## `$.flow.suspend` + +Use `$.flow.suspend` when you want to pause a workflow and proceed with the remaining steps only when manually approved or cancelled. + +For example, you can suspend a workflow and send yourself a link to manually resume or cancel the rest of the workflow: + +```javascript +export default defineComponent({ + async run({ $ }) { + const { resume_url, cancel_url } = $.flow.suspend(); + $.send.email({ + subject: "Please approve this important workflow", + text: `Click here to approve the workflow: ${resume_url}, and cancel here: ${cancel_url}`, + }); + // Pipedream suspends your workflow at the end of the step + }, +}); +``` + +You'll receive an email like this: + +
+approve this workflow +
+ +And can resume or cancel the rest of the workflow by clicking on the appropriate link. + +### `resume_url` and `cancel_url` + +In general, calling `$.flow.suspend` returns a `cancel_url` and `resume_url` that lets you cancel or resume paused executions. Since Pipedream pauses your workflow at the _end_ of the step, you can pass these URLs to any external service before the workflow pauses. If that service accepts a callback URL, it can trigger the `resume_url` when its work is complete. + +These URLs are specific to a single execution of your workflow. While the workflow is paused, you can load these in your browser or send any HTTP request to them: + +- Sending an HTTP request to the `cancel_url` will cancel that execution +- Sending an HTTP request to the `resume_url` will resume that execution + +If you resume a workflow, any data sent in the HTTP request is passed to the workflow and returned in the `$resume_data` [step export](/workflows/#step-exports) of the suspended step. For example, if you call `$.flow.suspend` within a step named `code`, the `$resume_data` export should contain the data sent in the `resume_url` request: + +
+resume data step export +
+ +Requests to the `resume_url` have [the same limits as any HTTP request to Pipedream](/workflows/limits/#http-request-body-size), but you can send larger payloads using our [large payload](/workflows/building-workflows/triggers/#sending-large-payloads) or [large file](/workflows/building-workflows/triggers/#large-file-support) interfaces. + +### Default timeout of 24 hours + +By default, `$.flow.suspend` will automatically cancel the workflow after 24 hours. You can set your own timeout (in milliseconds) as the first argument: + +```javascript +export default defineComponent({ + async run({ $ }) { + // 7 days + const TIMEOUT = 1000 * 60 * 60 * 24 * 7; + $.flow.suspend(TIMEOUT); + }, +}); +``` + +## `$.flow.rerun` + + + +Use `$.flow.rerun` when you want to run a specific step of a workflow multiple times. This is useful when you need to start a job in an external API and poll for its completion, or have the service call back to the step and let you process the HTTP request within the step. + +### Retrying a Failed API Request + +`$.flow.rerun` can be used to conditionally retry a failed API request due to a service outage or rate limit reached. Place the `$.flow.rerun` call within a `catch` block to only retry the API request if an error is thrown: + +```javascript +import { axios } from "@pipedream/platform"; + +export default defineComponent({ + props: { + openai: { + type: "app", + app: "openai", + }, + }, + async run({ steps, $ }) { + try { + return await axios($, { + url: `https://api.openai.com/v1/completions`, + method: "post", + headers: { + Authorization: `Bearer ${this.openai.$auth.api_key}`, + }, + data: { + model: "text-davinci-003", + prompt: "Say this is a test", + max_tokens: 7, + temperature: 0, + }, + }); + } catch (error) { + const MAX_RETRIES = 3; + const DELAY = 1000 * 30; + + // Retry the request every 30 seconds, for up to 3 times + $.flow.rerun(DELAY, null, MAX_RETRIES); + } + }, +}); +``` + +### Polling for the status of an external job + +Sometimes you need to poll for the status of an external job until it completes. `$.flow.rerun` lets you rerun a specific step multiple times: + +```javascript +import axios from "axios"; + +export default defineComponent({ + async run({ $ }) { + const MAX_RETRIES = 3; + // 10 seconds + const DELAY = 1000 * 10; + const { run } = $.context; + // $.context.run.runs starts at 1 and increments when the step is rerun + if (run.runs === 1) { + // $.flow.rerun(delay, context (discussed below), max retries) + $.flow.rerun(DELAY, null, MAX_RETRIES); + } else if (run.runs === MAX_RETRIES + 1) { + throw new Error("Max retries exceeded"); + } else { + // Poll external API for status + const { data } = await axios({ + method: "GET", + url: "https://example.com/status", + }); + // If we're done, continue with the rest of the workflow + if (data.status === "DONE") return data; + + // Else retry later + $.flow.rerun(DELAY, null, MAX_RETRIES); + } + }, +}); +``` + +`$.flow.rerun` accepts the following arguments: + +```javascript +$.flow.rerun( + delay, // The number of milliseconds until the step will be rerun + context, // JSON-serializable data you need to pass between runs + maxRetries // The total number of times the step will rerun. Defaults to 10 +); +``` + +### Accept an HTTP callback from an external service + +When you trigger a job in an external service, and that service can send back data in an HTTP callback to Pipedream, you can process that data within the same step using `$.flow.rerun`: + +```javascript +import axios from "axios"; + +export default defineComponent({ + async run({ steps, $ }) { + const TIMEOUT = 86400 * 1000; + const { run } = $.context; + // $.context.run.runs starts at 1 and increments when the step is rerun + if (run.runs === 1) { + const { cancel_url, resume_url } = $.flow.rerun(TIMEOUT, null, 1); + + // Send resume_url to external service + await axios({ + method: "POST", + url: "your callback URL", + data: { + resume_url, + cancel_url, + }, + }); + } + + // When the external service calls back into the resume_url, you have access to + // the callback data within $.context.run.callback_request + else if (run.callback_request) { + return run.callback_request; + } + }, +}); +``` + +### Passing `context` to `$.flow.rerun` + +Within a Node.js code step, `$.context.run.context` contains the `context` passed from the prior call to `rerun`. This lets you pass data from one run to another. For example, if you call: + +```javascript +$.flow.rerun(1000, { hello: "world" }); +``` + +`$.context.run.context` will contain: + +
+resume data step export +
+ +### `maxRetries` + +By default, `maxRetries` is **10**. + +When you exceed `maxRetries`, the workflow proceeds to the next step. If you need to handle this case with an exception, `throw` an error from the step: + +```javascript +export default defineComponent({ + async run({ $ }) { + const MAX_RETRIES = 3; + const { run } = $.context; + if (run.runs === 1) { + $.flow.rerun(1000, null, MAX_RETRIES); + } else if (run.runs === MAX_RETRIES + 1) { + throw new Error("Max retries exceeded"); + } + }, +}); +``` + +## Behavior when testing + +When you're building a workflow and test a step with `$.flow.suspend` or `$.flow.rerun`, it will not suspend the workflow, and you'll see a message like the following: + +> Workflow execution canceled — this may be due to `$.flow.suspend()` usage (not supported in test) + +These functions will only suspend and resume when run in production. + +## Credits when using `suspend` / `rerun` + +You are not charged for the time your workflow is suspended during a `$.flow.rerun` or `$.flow.suspend`. Only when workflows are resumed will compute time count toward [credit usage](/pricing/#credits). + + +When a suspended workflow reawakens, it will reset the credit counter. + +Each rerun or reawakening from a suspension will count as a new fresh credit. + + diff --git a/docs-v2/pages/code/nodejs/sharing-code.mdx b/docs-v2/pages/workflows/building-workflows/code/nodejs/sharing-code.mdx similarity index 84% rename from docs-v2/pages/code/nodejs/sharing-code.mdx rename to docs-v2/pages/workflows/building-workflows/code/nodejs/sharing-code.mdx index 9be3708ef9c39..ab4da049c1a8a 100644 --- a/docs-v2/pages/code/nodejs/sharing-code.mdx +++ b/docs-v2/pages/workflows/building-workflows/code/nodejs/sharing-code.mdx @@ -3,13 +3,13 @@ import VideoPlayer from '@/components/VideoPlayer' # Sharing code across workflows -[Actions](/components#actions) are reusable steps. When you author an action, you can add it to your workflow like you would other actions, by clicking the **+** button below any step. +[Actions](/workflows/contributing/components/#actions) are reusable steps. When you author an action, you can add it to your workflow like you would other actions, by clicking the **+** button below any step. Pipedream provides two ways to share code across workflows: -- **Publish an action from a Node.js code step**. [Publish any Node.js code step as a reusable action](/code/nodejs/sharing-code/#publish-an-action-from-a-node-js-code-step) from the Pipedream dashboard. +- **Publish an action from a Node.js code step**. [Publish any Node.js code step as a reusable action](/workflows/building-workflows/code/nodejs/sharing-code/#publish-an-action-from-a-nodejs-code-step) from the Pipedream dashboard. -- **Create an action from code**. Develop your action code on your local filesystem and [publish to your Pipedream account using the Pipedream CLI](/components/quickstart/nodejs/actions/). +- **Create an action from code**. Develop your action code on your local filesystem and [publish to your Pipedream account using the Pipedream CLI](/workflows/contributing/components/quickstart/nodejs/actions/). ## Publish an action from a Node.js code step @@ -61,14 +61,14 @@ Then open the menu in the top righthand corner of the code step and select **Pub And now you've successfully saved a custom Node.js code step to your account. You'll be able to use this code step in any of your workflows. - + **Why can't I use the `steps` variable in published Node.js code steps?** The `steps` variable contains the _workflows_ step exports. When you publish a Node.js code step as an action, it becomes reusable across many workflows. -This means that the step exports available vary depending on the workflow it's running on. +This means that the step exports available vary depending on the workflow it's running on. Defining props is a way to map inputs to actions and allow individual workflows to define which exports should be used. @@ -121,7 +121,7 @@ export default defineComponent({ Finally use the **Publish to My Actions** button in the right hand side menu to publish this new version. - + **I'm not seeing an **Edit Action** button option in my step** The **Edit Action** button is only available for actions that are published from Node.js code steps. @@ -129,15 +129,13 @@ The **Edit Action** button is only available for actions that are published from Actions submitted to the public component registry can contain multiple files. At this time it's not possible to edit multi-file components direct in a code step. - + **Will publishing a new version of an action automatically update all other steps using it?** -No, a new version of an action doesn't automatically update all instances of the same action across your workflows. - -This gives you the control to gradually update. Learn how to [update steps to the newest action versions here](https://pipedream.com/docs/workflows/steps/actions/#updating-actions-to-the-latest-version). +No, a new version of an action doesn't automatically update all instances of the same action across your workflows. This gives you the control to gradually update. -After publishing a new version, all other steps using this same action will have the option to [update to the latest version](/workflows/steps/actions/#updating-actions-to-the-latest-version). +After publishing a new version, all other steps using this same action will have the option to [update to the latest version](/workflows/building-workflows/actions/#updating-actions-to-the-latest-version). ## Differences between publishing actions from workflow Node.js code steps and directly from code @@ -145,6 +143,6 @@ Publishing reusable actions from Node.js code steps allows you to quickly scaffo However, there are some differences. -1. Node.js code step actions cannot make use of [app files to further reduce redundancy](/components/guidelines/#promoting-reusability). -2. Node.js code step actions cannot be published to the [Pipedream Component Registry](/apps/contributing/). -3. Node.js code step actions have a slightly different structure than [action components](/components/api/#component-api). +1. Node.js code step actions cannot make use of [app files to further reduce redundancy](/workflows/contributing/components/guidelines/#promoting-reusability). +2. Node.js code step actions cannot be published to the [Pipedream Component Registry](/workflows/contributing/). +3. Node.js code step actions have a slightly different structure than [action components](/workflows/contributing/components/api/#component-api). diff --git a/docs-v2/pages/workflows/building-workflows/code/nodejs/using-data-stores.mdx b/docs-v2/pages/workflows/building-workflows/code/nodejs/using-data-stores.mdx new file mode 100644 index 0000000000000..92c298eae44c3 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/code/nodejs/using-data-stores.mdx @@ -0,0 +1,260 @@ +import Callout from '@/components/Callout' + +# Using Data Stores + +In Node.js (Javascript) code steps, you can also store and retrieve data within code steps without connecting a 3rd party database. + +Add data stores to steps as props. By adding the store as a prop, it's available under `this`. + +For example, you can define a data store as a data prop, and reference it at `this.data`: + +```javascript +export default defineComponent({ + props: { + // Define that the "data" variable in our component is a data store + data: { type: "data_store" }, + }, + async run({ steps, $ }) { + // Now we can access the data store at "this.data" + await this.data.get("email"); + }, +}); +``` + + +**`props` injects variables into `this`**. See how we declared the `data` prop in the `props` object, and then accessed it at `this.data` in the `run` method. + + + +All data store operations are asynchronous, so must be `await`ed. + + +## Using the data store + +Once you've defined a data store prop for your component, then you'll be able to create a new data store or use an existing one from your account. + +![Create a new data store or choose another one from your account for your component](/images/data-stores/nodejs-example.png) + +## Saving data + +Data Stores are key-value stores. Save data within a Data Store using the `this.data.set` method. The first argument is the _key_ where the data should be held, and the second argument is the _value_ assigned to that key. + +```javascript +export default defineComponent({ + props: { + data: { type: "data_store" }, + }, + async run({ steps, $ }) { + // Store a timestamp each time this step is executed in the workflow + await this.data.set("lastRanAt", new Date()); + }, +}); +``` + +## Retrieving keys + +Fetch all the keys in a given Data Store using the `keys` method: + +```javascript +export default defineComponent({ + props: { + data: { type: "data_store" }, + }, + async run({ steps, $ }) { + // Return a list of all the keys in a given Data Store + return await this.data.keys(); + }, +}); +``` + +## Checking for the existence of specific keys + +If you need to check whether a specific `key` exists in a Data Store, you can pass the `key` to the `has` method to get back a `true` or `false`: + +```javascript +export default defineComponent({ + props: { + data: { type: "data_store" }, + }, + async run({ steps, $ }) { + // Check if a specific key exists in your Data Store + return await this.data.has("lastRanAt"); + }, +}); +``` + +## Retrieving data + +You can retrieve data with the Data Store using the `get` method. Pass the _key_ to the `get` method to retrieve the content that was stored there with `set`. + +```javascript +export default defineComponent({ + props: { + data: { type: "data_store" }, + }, + async run({ steps, $ }) { + // Check if the lastRanAt key exists + const lastRanAt = await this.data.get("lastRanAt"); + }, +}); +``` + +## Retrieving all records + +Use an async iterator to efficiently retrieve all records or keys in your data store: + +```javascript +export default defineComponent({ + props: { + data: { type: "data_store" }, + }, + async run({ steps, $ }) { + const records = {}; + for await (const [k,v] of this.data) { + records[k] = v; + } + return records; + }, +}); +``` + +## Deleting or updating values within a record + +To delete or update the _value_ of an individual record, use the `set` method for an existing `key` and pass either the new value or `''` as the second argument to remove the value but retain the key. + +```javascript +export default defineComponent({ + props: { + data: { type: "data_store" }, + }, + async run({ steps, $ }) { + // Update the value associated with the key, myKey + await this.data.set("myKey", "newValue"); + + // Remove the value but retain the key + await this.data.set("myKey", ""); + }, +}); +``` + +## Deleting specific records + +To delete individual records in a Data Store, use the `delete` method for a specific `key`: + +```javascript +export default defineComponent({ + props: { + data: { type: "data_store" }, + }, + async run({ steps, $ }) { + // Delete the lastRanAt record + const lastRanAt = await this.data.delete("lastRanAt"); + }, +}); +``` + +## Deleting all records from a specific Data Store + +If you need to delete all records in a given Data Store, you can use the `clear` method. **Note that this is an irreversible change, even when testing code in the workflow builder.** + +```javascript +export default defineComponent({ + props: { + data: { type: "data_store" }, + }, + async run({ steps, $ }) { + // Delete all records from a specific Data Store + return await this.data.clear(); + }, +}); +``` + +## Viewing store data + +You can view the contents of your data stores in your [Pipedream dashboard](https://pipedream.com/stores). + +From here you can also manually edit your data store's data, rename stores, delete stores or create new stores. + +## Using multiple data stores in a single code step + +It is possible to use multiple data stores in a single code step, just make a unique name per store in the `props` definition. Let's define 2 separate `customers` and `orders` data sources and leverage them in a single code step: + +```javascript +export default defineComponent({ + props: { + customers: { type: "data_store" }, + orders: { type: "data_store" }, + }, + async run({ steps, $ }) { + // Retrieve the customer from our customer store + const customer = await this.customer.get(steps.trigger.event.customer_id); + // Retrieve the order from our order data store + const order = await this.orders.get(steps.trigger.event.order_id); + }, +}); +``` + +## Workflow counter example + +You can use a data store as a counter. For example, this code counts the number of times the workflow runs: + +```javascript +export default defineComponent({ + props: { + data: { type: "data_store" }, + }, + async run({ steps, $ }) { + // By default, all database entries are undefined. + // It's wise to set a default value so our code as an initial value to work with + const counter = (await this.data.get("counter")) ?? 0; + + // On the first run "counter" will be 0 and we'll increment it to 1 + // The next run will increment the counter to 2, and so forth + await this.data.set("counter", counter + 1); + }, +}); +``` + +## Dedupe data example + +Data Stores are also useful for storing data from prior runs to prevent acting on duplicate data, or data that's been seen before. + +For example, this workflow's trigger contains an email address from a potential new customer. But we want to track all emails collected so we don't send a welcome email twice: + +```javascript +export default defineComponent({ + props: { + data: { type: "data_store" }, + }, + async run({ steps, $ }) { + const email = steps.trigger.event.body.new_customer_email; + // Retrieve the past recorded emails from other runs + const emails = (await this.data.get("emails")) ?? []; + + // If the current email being passed from our webhook is already in our list, exit early + if (emails.includes(email)) { + return $.flow.exit("Already welcomed this user"); + } + + // Add the current email to the list of past emails so we can detect it in the future runs + await this.data.set("emails", [...emails, email]); + }, +}); +``` + +## Data store limitations + +Data Stores are only currently available in Node.js and Python steps. They are not yet available [Bash](/workflows/building-workflows/code/bash/) or [Go](/workflows/building-workflows/code/go/). + +### Supported data types + +Data stores can hold any JSON-serializable data within the storage limits. This includes data types including: + +- Strings +- Objects +- Arrays +- Dates +- Integers +- Floats + +But you cannot serialize Functions, Classes, or other more complex objects. diff --git a/docs-v2/pages/workflows/building-workflows/code/nodejs/working-with-files.mdx b/docs-v2/pages/workflows/building-workflows/code/nodejs/working-with-files.mdx new file mode 100644 index 0000000000000..126f024bc0187 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/code/nodejs/working-with-files.mdx @@ -0,0 +1,94 @@ +# Working with the filesystem in Node.js + +You'll commonly need to work with files in a workflow, for example: downloading content from some service to upload to another. This doc explains how to work with files in Pipedream workflows and provides some sample code for common operations. + +## The `/tmp` directory + +Within a workflow, you have full read-write access to the `/tmp` directory. You have {process.env.TMP_SIZE_LIMIT} of available space in `/tmp` to save any file. + +### Managing `/tmp` across workflow runs + +The `/tmp` directory is stored on the virtual machine that runs your workflow. We call this the execution environment ("EE"). More than one EE may be created to handle high-volume workflows. And EEs can be destroyed at any time (for example, after about 10 minutes of receiving no events). This means that you should not expect to have access to files across executions. At the same time, files _may_ remain, so you should clean them up to make sure that doesn't affect your workflow. **Use [the `tmp-promise` package](https://github.com/benjamingr/tmp-promise) to cleanup files after use, or [delete the files manually](#delete-a-file).** + +### Reading a file from `/tmp` + +This example uses [step exports](/workflows/#step-exports) to return the contents of a test file saved in `/tmp` as a string: + +```javascript +import fs from "fs"; + +export default defineComponent({ + async run({ steps, $ }) { + return (await fs.promises.readFile('/tmp/your-file')).toString() + } +}); +``` + +### Writing a file to `/tmp` + +Use the [`fs` module](https://nodejs.org/api/fs.html) to write data to `/tmp`: + +```javascript +import fs from "fs" +import { file } from 'tmp-promise' + +export default defineComponent({ + async run({ steps, $ }) { + const { path, cleanup } = await file(); + await fs.promises.appendFile(path, Buffer.from("hello, world")) + await cleanup(); + } +}); +``` + +### Listing files in `/tmp` + +Return a list of the files saved in `/tmp`: + +```javascript +import fs from "fs"; + +export default defineComponent({ + async run({ steps, $ }) { + return fs.readdirSync("/tmp"); + } +}); +``` + +### Delete a file + +```javascript +import fs from "fs"; + +export default defineComponent({ + async run({ steps, $ }) { + return await fs.promises.unlink('/tmp/your-file'); + } +}); +``` + +### Download a file to `/tmp` + +[See this example](/workflows/building-workflows/code/nodejs/http-requests/#download-a-file-to-the-tmp-directory) to learn how to download a file to `/tmp`. + +### Upload a file from `/tmp` + +[See this example](/workflows/building-workflows/code/nodejs/http-requests/#upload-a-file-from-the-tmp-directory) to learn how to upload a file from `/tmp` in an HTTP request. + +### Download a file, uploading it in another `multipart/form-data` request + +[This workflow](https://pipedream.com/@dylburger/download-file-then-upload-file-via-multipart-form-data-request-p_QPCx7p/edit) provides an example of how to download a file at a specified **Download URL**, uploading that file to an **Upload URL** as form data. + +### Download email attachments to `/tmp`, upload to Amazon S3 + +[This workflow](https://pipedream.com/@dylan/upload-email-attachments-to-s3-p_V9CGAQ/edit) is triggered by incoming emails. When copied, you'll get a workflow-specific email address you can send any email to. This workflow takes any attachments included with inbound emails, saves them to `/tmp`, and uploads them to Amazon S3. + +You should also be aware of the [inbound payload limits](/workflows/limits/#email-triggers) associated with the email trigger. + +### Downloading and uploading files from File Stores + +Within Node.js code steps, you can download files from a File Store to the `/tmp` directory and vice versa. + +The `$.files` helper includes methods to upload and download files from the Project's File Store. + +[Read the File Stores `$.files` helper documentation.](/workflows/data-management/file-stores/#managing-file-stores-from-workflows) diff --git a/docs-v2/pages/workflows/building-workflows/code/python/_meta.tsx b/docs-v2/pages/workflows/building-workflows/code/python/_meta.tsx new file mode 100644 index 0000000000000..9c81cb2fdc407 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/code/python/_meta.tsx @@ -0,0 +1,10 @@ +export default { + "index": "Running Python in workflows", + "auth": "Connect apps", + "http-requests": "HTTP requests", + "working-with-files": "Files", + "using-data-stores": "Data stores", + "delay": "Delaying steps", + "rerun": "Pause, resume, and rerun steps", + "import-mappings": "Different PyPI package name and import name", +} as const diff --git a/docs-v2/pages/workflows/building-workflows/code/python/auth.mdx b/docs-v2/pages/workflows/building-workflows/code/python/auth.mdx new file mode 100644 index 0000000000000..56fa21dc8bf02 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/code/python/auth.mdx @@ -0,0 +1,91 @@ +# Connecting apps in Python + +When you use [prebuilt actions](/workflows/contributing/components/#actions) tied to apps, you don't need to write the code to authorize API requests. Just [connect your account](/integrations/connected-accounts/#connecting-accounts) for that app and run your workflow. + +But sometimes you'll need to [write your own code](/workflows/building-workflows/code/python/). You can also connect apps to custom code steps, using the auth information to authorize requests to that app. + +For example, you may want to send a Slack message from a step. We use Slack's OAuth integration to authorize sending messages from your workflows. + +Add Slack as an app on the Python step, then connect your Slack account. + +![Add your Slack account to a Python code step by adding it](https://res.cloudinary.com/pipedreamin/image/upload/v1710517970/docs/docs/workflows/building-workflows/code/pythonauth/CleanShot_2024-03-15_at_11.51.53_u3ld0i.gif) + +Then within the Python code step, `pd.inputs["slack"]["$auth"]["oauth_access_token"]` will contain your Slack account OAuth token. + +With that token, you can make authenticated API calls to Slack: + +```python +from slack_sdk import WebClient + +def handler(pd: "pipedream"): + # Your Slack OAuth token is available under pd.inputs + token = pd.inputs["slack"]["$auth"]["oauth_access_token"] + + # Instantiate a new Slack client with your token + client = WebClient(token=token) + + # Use the client to send messages to Slack channels + response = client.chat_postMessage( + channel='#general', + text='Hello from Pipedream!' + ) + + # Export the Slack response payload for use in future steps + pd.export("response", response.data) +``` + + + +## Accessing connected account data with `pd.inputs[appName]["$auth"]` + +In our Slack example above, we created a Slack `WebClient` using the Slack OAuth access token: + +```python +# Instantiate a new Slack client with your token +client = WebClient(token=token) +``` + +Where did `pd.inputs["slack"]` come from? Good question. It was generated when we connected Slack to our Python step. + +![The Slack app generates the pd.inputs["slack"] data](/images/python/auth/connected-slack-account.png) + +The Slack access token is generated by Pipedream, and is available to this step in the `pd.inputs[appName]["$auth"]` object: + +```python +from slack_sdk import WebClient + +def handler(pd: "pipedream"): + token = pd.inputs["slack"]["$auth"]["oauth_access_token"] + # Authentication details for all of your apps are accessible under the special pd.inputs["slack"] variable: + console.log(pd.inputs["slack"]["$auth"]) +``` + +`pd.inputs["slack"]["$auth"]` contains named properties for each account you connect to the associated step. Here, we connected Slack, so `this.slack.$auth` contains the Slack auth info (the `oauth_access_token`). + +The names of the properties for each connected account will differ with the account. Pipedream typically exposes OAuth access tokens as `oauth_access_token`, and API keys under the property `api_key`. But if there's a service-specific name for the tokens (for example, if the service calls it `server_token`), we prefer that name, instead. + +To list the `pd.inputs["slack"]["$auth"]` properties available to you for a given app, just print the contents of the `$auth` property: + +```python +print(pd.inputs["slack"]["$auth"]) # Replace "slack" with your app's name +``` + +and run your workflow. You'll see the property names in the logs below your step. + +### Using the code templates tied to apps + +When you write custom code that connects to an app, you can start with a code snippet Pipedream provides for each app. This is called the **test request**. + +When you search for an app in a step: + +1. Click the **+** button below any step. +2. Search for the app you're looking for and select it from the list. +3. Select the option to **Run Python with any [app] API**. + +![Create Python API scaffolding for any app](/images/python/auth/step-selector-python-example.png) + +This code operates as a template you can extend, and comes preconfigured with the connection to the target app and the code for authorizing requests to the API. You can modify this code however you'd like. + +## Custom auth tokens / secrets + +When you want to connect to a 3rd party service that isn't supported by Pipedream, you can store those secrets in [Environment Variables](/workflows/environment-variables/). diff --git a/docs-v2/pages/workflows/building-workflows/code/python/delay.mdx b/docs-v2/pages/workflows/building-workflows/code/python/delay.mdx new file mode 100644 index 0000000000000..568cf5cbd3fb4 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/code/python/delay.mdx @@ -0,0 +1,129 @@ +import Callout from '@/components/Callout' + +# Delaying a workflow + +Use `pd.flow.delay` to [delay a step in a workflow](/workflows/building-workflows/control-flow/delay/). + +These docs show you how to write Python code to handle delays. If you don't need to write code, see [our built-in delay actions](/workflows/building-workflows/control-flow/delay/#delay-actions). + +## Using `pd.flow.delay` + +`pd.flow.delay` takes one argument: the number of **milliseconds** you'd like to pause your workflow until the next step executes. {process.env.DELAY_MIN_MAX_TIME}. + +Note that [delays happen at the end of the step where they're called](#when-delays-happen). + +```python +import random + +def handler(pd: 'pipedream'): + # Delay a workflow for 60 seconds (60,000 ms) + pd.flow.delay(60 * 1000) + + # Delay a workflow for 15 minutes + pd.flow.delay(15 * 60 * 1000) + + # Delay a workflow based on the value of incoming event data, + # or default to 60 seconds if that variable is undefined + default = 60 * 1000 + delayMs = pd.steps["trigger"].get("event", {}).get("body", {}).get("delayMs", default) + pd.flow.delay(delayMs) + + # Delay a workflow a random amount of time + pd.flow.delay(random.randint(0, 999)) +``` + + +Paused workflow state + +When `pd.flow.delay` is executed in a Python step, the workflow itself will enter a **Paused** state. + +While the workflow is paused, it will not incur any credits towards compute time. You can also [view all paused workflows in the Event History](/workflows/event-history/#filtering-by-status). + + +### Credit usage + +The length of time a workflow is delayed from `pd.flow.delay` does _not_ impact your credit usage. For example, delaying a 256 megabyte workflow for five minutes will **not** incur ten credits. + +However, using `pd.flow.delay` in a workflow will incur two credits. + +One credit is used to initially start the workflow, then the second credit is used when the workflow resumes after its pause period has ended. + + +Exact credit usage depends on duration and memory configuration + +If your workflow's [execution timeout limit](/workflows/building-workflows/settings/#execution-timeout-limit) is set to longer than [default limit](/workflows/limits/#time-per-execution), it may incur more than two [credits](/pricing/#credits) when using `pd.flow.delay`. + + +## `cancel_url` and `resume_url` + +Both the built-in **Delay** actions and `pd.flow.delay` return a `cancel_url` and `resume_url` that lets you cancel or resume paused executions. + +These URLs are specific to a single execution of your workflow. While the workflow is paused, you can load these in your browser or send an HTTP request to either: + +- Hitting the `cancel_url` will immediately cancel that execution +- Hitting the `resume_url` will immediately resume that execution early + +[Since Pipedream pauses your workflow at the _end_ of the step where you run call `pd.flow.delay`](#when-delays-happen), you can send these URLs to third party systems, via email, or anywhere else you'd like to control the execution of your workflow. + +```python +import requests + +def handler(pd: 'pipedream'): + links = pd.flow.delay(15 * 60 * 1000) + # links contains a dictionary with two entries: resume_url and cancel_url + + # Send the URLs to a system you own + requests.post("https://example.com", json=links) + + # Email yourself the URLs. Click on the links to cancel / resume + pd.send.email( + subject=f"Workflow execution {pd.steps['trigger']['context']['id']}", + text=f"Cancel your workflow here: {links['cancel_url']} . Resume early here: {links['resume_url']}", + html=None + ) + + # Delay happens at the end of this step +``` + + +In `pd.send.email`, the `html` argument defaults to `""`, so it overrides the email `text` unless explicitly set to `None`. + + +## When delays happen + +**Pipedream pauses your workflow at the _end_ of the step where you call `pd.flow.delay`**. This lets you [send the `cancel_url` and `resume_url` to third-party systems](cancel_url-and-resume_url). + +```python +def handler(pd: 'pipedream'): + urls = pd.flow.delay(15 * 60 * 1000) + cancel_url, resume_url = urls["cancel_url"], urls["resume_url"] + # ... run any code you want here + + # Delay happens at the end of this step +``` + +## Delays and HTTP responses + +You cannot run `pd.respond` after running `pd.flow.delay`. Pipedream ends the original execution of the workflow when `pd.flow.delay` is called and issues the following response to the client to indicate this state: + +> $.respond() not called for this invocation + +If you need to set a delay on an HTTP request triggered workflow, consider using [`time.sleep`](#timesleep) instead. + +## `time.sleep` + +Alternatively, you can use `time.sleep` instead of using `pd.flow.delay` to delay individual workflow steps. + +However, there are some drawbacks to using `time.sleep` instead of `pd.flow.delay`. `time.sleep` will count towards your workflow's compute time, for example: + +```python +import time + +def handler(pd: 'pipedream'): + # delay this step for 30 seconds + delay = 30 + + time.sleep(delay) +``` + +The Python step above will hold the workflow's execution for this step for 30 seconds; however, 30 seconds will also _contribute_ to your credit usage. Also consider that workflows have a hard limit of {process.env.MAX_WORKFLOW_EXECUTION_LIMIT} seconds. diff --git a/docs-v2/pages/workflows/building-workflows/code/python/http-requests.mdx b/docs-v2/pages/workflows/building-workflows/code/python/http-requests.mdx new file mode 100644 index 0000000000000..7ae57ea425ef8 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/code/python/http-requests.mdx @@ -0,0 +1,292 @@ +import Callout from '@/components/Callout' + +# Making HTTP Requests with Python + +HTTP requests are fundamental to working with APIs or other web services. You can make HTTP requests to retrieve data from APIs, fetch HTML from websites, or do pretty much anything your web browser can do. + +**Below, we'll review how to make HTTP requests using Python code on Pipedream.** + +We recommend using the popular `requests` HTTP client package available in Python to send HTTP requests, but [you can use any PyPi package you'd like on Pipedream](/workflows/building-workflows/code/python/#using-third-party-packages). + + + +## Basic `requests` usage notes + +No need to run `pip install`, just `import requests` at the top of your step's code and it's available for your code to use. + +To use `requests` on Pipedream, you'll just need to import the `requests` PyPi package: + +```python +import requests +``` + +You make HTTP requests by passing a URL and optional request parameters to one of [Requests' 7 HTTP request methods](https://requests.readthedocs.io/en/latest/api/#main-interface). + +**Here's how to make a basic HTTP request on Pipedream:** + +```python +r = requests.get('https://swapi.dev/api/films/') +``` + +The [Response](https://requests.readthedocs.io/en/latest/api/#requests.Response) object `r` contains a lot of information about the response: its content, headers, and more. Typically, you just care about the content, which you can access in the `text` property of the response: + +```python +r = requests.get('https://swapi.dev/api/films/') + +# HTTP response content is in the text property +r.text +``` + +Requests automatically decodes the content of the response based on its encoding, `r.encoding`, which is determined based on the HTTP headers. + +If you're dealing with JSON data, you can call `r.json()` to decode the content as JSON: + +```python +r = requests.get('https://swapi.dev/api/films/') + +# The json-encoded content of a response, if any +r.json() +``` + +If JSON decoding fails, `r.json()` raises an exception. + +## Making a `GET` request + +GET requests typically are for retrieving data from an API. Below is an example. + +```python +import requests + +def handler(pd: "pipedream"): + url = "https://swapi.dev/api/people/1" + + r = requests.get(url) + + # The response is logged in your Pipedream step results: + print(r.text) + + # The response status code is logged in your Pipedream step results: + print(r.status_code) +``` + +## Making a `POST` request + +```python +import requests + +def handler(pd: "pipedream"): + # This a POST request to this URL will echo back whatever data we send to it + url = "https://postman-echo.com/post" + + data = {"name": "Bulbasaur"} + + r = requests.post(url, data=data) + + # The response is logged in your Pipedream step results: + print(r.text) + + # The response status code is logged in your Pipedream step results: + print(r.status_code) +``` + +When you make a `POST` request, pass a dictionary with the data you'd like to send to the `data` argument. Requests automatically form-encodes the data when the request is made. + + +The code example above will NOT set the `Content-Type` header, meaning it will NOT be set to `application/json`. + +If you want the header set to `application/json` and don't want to encode the `dict` yourself, you can pass it using the `json` parameter and it will be encoded automatically: + +```python + url = "https://postman-echo.com/post" + data = {"name": "Bulbasaur"} + r = requests.post(url, json=data) +``` + + +## Passing query string parameters to a `GET` request + +Retrieve fake comment data on a specific post using [JSONPlaceholder](https://jsonplaceholder.typicode.com/), a free mock API service. Here, you fetch data from the `/comments` resource, retrieving data for a specific post by query string parameter: `/comments?postId=1`. + +```python +import requests + +def handler(pd: "pipedream"): + url = "https://jsonplaceholder.typicode.com/comments" + params = {"postId": 1} + + # Make an HTTP GET request using requests + r = requests.get(url, params=params) + + # Retrieve the content of the response + data = r.text +``` + +You should pass query string parameters as a dictionary using the `params` keyword argument, like above. When you do, `requests` automatically [URL-encodes](https://www.w3schools.com/tags/ref_urlencode.ASP) the parameters for you, which you'd otherwise have to do manually. + +## Sending a request with HTTP headers + +To add HTTP headers to a request, pass a dictionary to the `headers` parameter: + +```python +import requests +import json + +def handler(pd: "pipedream"): + url = "https://jsonplaceholder.typicode.com/posts" + headers = {"Content-Type": "application/json"} + data = {"name": "Luke"} + + # Make an HTTP POST request using requests + r = requests.post(url, headers=headers, data=json.dumps(data)) +``` + +## Sending a request with a secret or API key + +Most APIs require you authenticate HTTP requests with an API key or other token. **Please review the docs for your service to understand how they accept this data.** + +Here's an example showing an API key passed in an HTTP header: + +```python +import requests + +def handler(pd: "pipedream"): + url = "https://jsonplaceholder.typicode.com/posts" + headers = {"X-API-KEY": "123"} # API KEY + data = {"name": "Luke"} + + # Make an HTTP POST request using requests + r = requests.post(url, headers=headers, json=data) +``` + +## Sending files + +An example of sending a previously stored file in the workflow's `/tmp` directory: + +```python +import requests + +def handler(pd: "pipedream"): + # Retrieving a previously saved file from workflow storage + files = {"image": open("/tmp/python-logo.png", "rb")} + + r = requests.post(url="https://api.imgur.com/3/image", files=files) +``` + +## Downloading a file to the `/tmp` directory + +This example shows you how to download a file to a file in [the `/tmp` directory](/workflows/building-workflows/code/python/working-with-files/). This can be especially helpful for downloading large files: it streams the file to disk, minimizing the memory the workflow uses when downloading the file. + +```python +import requests + +def handler(pd: "pipedream"): + # Download the webpage HTML file to /tmp + with requests.get("https://example.com", stream=True) as response: + # Check if the request was successful + response.raise_for_status() + + # Open the new file /tmp/file.html in binary write mode + with open("/tmp/file.html", "wb") as file: + for chunk in response.iter_content(chunk_size=8192): + # Write the chunk to file + file.write(chunk) +``` + +## Uploading a file from the `/tmp` directory + +This example shows you how to make a `multipart/form-data` request with a file as a form part. You can store and read any files from [the `/tmp` directory](/workflows/building-workflows/code/python/working-with-files/#the-tmp-directory). + +This can be especially helpful for uploading large files: it streams the file from disk, minimizing the memory the workflow uses when uploading the file. + +```python +import requests +from requests_toolbelt.multipart.encoder import MultipartEncoder + +def handler(pd: "pipedream"): + m = MultipartEncoder(fields={ + 'file': ('filename', open('/tmp/file.pdf', 'rb'), 'application/pdf') + }) + + r = requests.post("https://example.com", data=m, + headers={'Content-Type': m.content_type}) +``` + +## IP addresses for HTTP requests made from Pipedream workflows + +By default, [HTTP requests made from Pipedream can come from a large range of IP addresses](/privacy-and-security/#hosting-details). **If you need to restrict the IP addresses HTTP requests come from, you can [Use a Pipedream VPC](/workflows/vpc/) to route all outbound HTTP requests through a single IP address.** + +## Using an HTTP proxy to proxy requests through another host + +By default, HTTP requests made from Pipedream can come from a range of IP addresses. **If you need to make requests from a single IP address, you can route traffic through an HTTP proxy**: + +```python +import requests + +def handler(pd: "pipedream"): + user = "USERNAME" # Replace with your HTTP proxy username + password = "PASSWORD" # Replace with your HTTP proxy password + host = "10.10.1.10" # Replace with the HTTP proxy URL + port = 1080 # Replace with the port of the HTTP proxy + proxies = { + "https": f"http://{user}:{password}@{host}:{port}", + } + + r = requests.request("GET", "https://example.com", proxies=proxies) +``` + +## Paginating API requests + +When you fetch data from an API, the API may return records in "pages". For example, if you're trying to fetch a list of 1,000 records, the API might return those in groups of 100 items. + +Different APIs paginate data in different ways. You'll need to consult the docs of your API provider to see how they suggest you paginate through records. + +## Sending a GraphQL request + +Construct a GraphQL query as a string and then using the requests library to send it to the GraphQL server: + +```python +import requests + +def handler(pd: "pipedream"): + url = "https://beta.pokeapi.co/graphql/v1beta" + + query = """ +query samplePokeAPIquery { + generations: pokemon_v2_generation { + name + pokemon_species: pokemon_v2_pokemonspecies_aggregate { + aggregate { + count + } + } + } +} + """ + + r = requests.post(url, json={"query": query}) + return r.json() +``` + +### Sending an authenticated GraphQL request + +Authenticate your connected accounts in Pipedream with GraphQL requests using `pd.inputs[appName]["$auth"]`: + +```python +import requests + +def handler(pd: "pipedream"): + url = "https://api.github.com/graphql" + query = """ +query { + viewer { + login + } +} + """ + token = pd.inputs["github"]["$auth"]["oauth_access_token"] + headers = {"authorization": f"Bearer {token}"} + r = requests.post(url, json={"query": query}, headers=headers) + return r.json() +``` + +Alternatively, you can use Environment Variables as well for simple API key based GraphQL APIs. diff --git a/docs-v2/pages/code/python/import-mappings.mdx b/docs-v2/pages/workflows/building-workflows/code/python/import-mappings.mdx similarity index 100% rename from docs-v2/pages/code/python/import-mappings.mdx rename to docs-v2/pages/workflows/building-workflows/code/python/import-mappings.mdx diff --git a/docs-v2/pages/workflows/building-workflows/code/python/index.mdx b/docs-v2/pages/workflows/building-workflows/code/python/index.mdx new file mode 100644 index 0000000000000..3a37256d2a88d --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/code/python/index.mdx @@ -0,0 +1,322 @@ +import Callout from '@/components/Callout' +import VideoPlayer from '@/components/VideoPlayer' + +# Python + +Pipedream supports [Python v{process.env.PYTHON_VERSION}](https://www.python.org) in workflows. Run any Python code, use any [PyPI package](https://pypi.org/), connect to APIs, and more. + +## Adding a Python code step + +1. Click the + icon to add a new step +2. Click **Custom Code** +3. In the new step, select the `python` language runtime in language dropdown + +## Python Code Step Structure + +A new Python Code step will have the following structure: + +```python +def handler(pd: "pipedream"): + # Reference data from previous steps + print(pd.steps["trigger"]["context"]["id"]) + # Return data for use in future steps + return {"foo": {"test": True}} +``` + +You can also perform more complex operations, including [leveraging your connected accounts to make authenticated API requests](/workflows/building-workflows/code/python/auth/), [accessing Data Stores](/workflows/building-workflows/code/python/using-data-stores/) and [installing PyPI packages](/workflows/building-workflows/code/python/#using-third-party-packages). + +- [Install PyPI Packages](/workflows/building-workflows/code/python/#using-third-party-packages) +- [Import data exported from other steps](/workflows/building-workflows/code/python/#using-data-from-another-step) +- [Export data to downstream steps](/workflows/building-workflows/code/python/#sending-data-downstream-to-other-steps) +- [Retrieve data from a data store](/workflows/building-workflows/code/python/using-data-stores/#retrieving-data) +- [Store data into a data store](/workflows/building-workflows/code/python/using-data-stores/#saving-data) +- [Access API credentials from connected accounts](/workflows/building-workflows/code/python/auth/) + +## Logging and debugging + +You can use `print` at any time in a Python code step to log information as the script is running. + +The output for the `print` **logs** will appear in the `Results` section just beneath the code editor. + +![Python print log output in the results](/images/python/print-logs.png) + +## Using third party packages + + + +You can use any packages from [PyPI](https://pypi.org) in your Pipedream workflows. This includes popular choices such as: + +- [`requests` for making HTTP requests](https://pypi.org/project/requests/) +- [`sqlalchemy`for retrieving or inserting data in a SQL database](https://pypi.org/project/sqlalchemy/) +- [`pandas` for working with complex datasets](https://pypi.org/project/pandas/) + +To use a PyPI package, just include it in your step's code: + +```python +import requests +``` + +And that's it. No need to update a `requirements.txt` or specify elsewhere in your workflow of which packages you need. Pipedream will automatically install the dependency for you. + +### If your package's `import` name differs from its PyPI package name + +Pipedream's package installation uses [the `pipreqs` package](https://github.com/bndr/pipreqs) to detect package imports and install the associated package for you. Some packages, like [`python-telegram-bot`](https://python-telegram-bot.org/), use an `import` name that differs from their PyPI name: + +```bash +pip install python-telegram-bot +``` + +vs. + +```python +import telegram +``` + +Use the built in [magic comment system to resolve these mismatches](/workflows/building-workflows/code/python/import-mappings/): + +```python +# pipedream add-package python-telegram-bot +import telegram +``` + +### Pinning package versions + +Each time you deploy a workflow with Python code, Pipedream downloads the PyPi packages you `import` in your step. **By default, Pipedream deploys the latest version of the PyPi package each time you deploy a change**. + +There are many cases where you may want to specify the version of the packages you're using. If you'd like to use a _specific_ version of a package in a workflow, you can add that version in a [magic comment](/workflows/building-workflows/code/python/import-mappings/), for example: + +```python +# pipedream add-package pandas==2.0.0 +import pandas +``` + + +Currently, you cannot use different versions of the same package in different steps in a workflow. + + +## Making an HTTP request + +We recommend using the popular `requests` HTTP client package available in Python to send HTTP requests. + +No need to run `pip install`, just `import requests` at the top of your step's code and it's available for your code to use. + +See the [Making HTTP Requests with Python](/workflows/building-workflows/code/python/http-requests/) docs for more information. + +## Returning HTTP responses + +You can return HTTP responses from [HTTP-triggered workflows](/workflows/building-workflows/triggers/#http) using the `pd.respond()` method: + +```python +def handler(pd: "pipedream"): + pd.respond({ + "status": 200, + "body": { + "message": "Everything is ok" + } + }) +``` + +Please note to always include at least the `body` and `status` keys in your `pd.respond` argument. The `body` must also be a JSON serializable object or dictionary. + + +Unlike the Node.js equivalent, the Python `pd.respond` helper does not yet support responding with Streams. + + + +_Don't forget_ to [configure your workflow's HTTP trigger to allow a custom response](/workflows/building-workflows/triggers/#http-responses). Otherwise your workflow will return the default response. + + +## Sharing data between steps + +A step can accept data from other steps in the same workflow, or pass data downstream to others. + +### Using data from another step + +In Python steps, data from the initial workflow trigger and other steps are available in the `pd.steps` object. + +In this example, we'll pretend this data is coming into our workflow's HTTP trigger via POST request. + +```json +// POST .m.pipedream.net +{ + "id": 1, + "name": "Bulbasaur", + "type": "plant" +} +``` + +In our Python step, we can access this data in the `pd.steps` object passed into the `handler`. Specifically, this data from the POST request into our workflow is available in the `trigger` dictionary item. + +```python +def handler(pd: "pipedream"): + # retrieve the data from the HTTP request in the initial workflow trigger + pokemon_name = pd.steps["trigger"]["event"]["name"] + pokemon_type = pd.steps["trigger"]["event"]["type"] + + print(f"{pokemon_name} is a {pokemon_type} type Pokemon") +``` + +### Sending data downstream to other steps + +To share data created, retrieved, transformed or manipulated by a step to others downstream, `return` the data in the `handler` function: + +```python +# This step is named "code" in the workflow +import requests + +def handler(pd: "pipedream"): + r = requests.get("https://pokeapi.co/api/v2/pokemon/charizard") + # Store the JSON contents into a variable called "pokemon" + pokemon = r.json() + + # Expose the data to other steps in the "pokemon" key from this step + return { + "pokemon": pokemon + } +``` + +Now this `pokemon` data is accessible to downstream steps within `pd.steps["code"]["pokemon"]` + + +You can only export JSON-serializable data from steps. Things like: + +- strings +- numbers +- lists +- dictionaries + + +## Using environment variables + +You can leverage any [environment variables defined in your Pipedream account](/workflows/environment-variables/) in a Python step. This is useful for keeping your secrets out of code as well as keeping them flexible to swap API keys without having to update each step individually. + +To access them, use the `os` module. + +```python +import os + +def handler(pd: "pipedream"): + token = os.environ["AIRTABLE_API_KEY"] + + print(token) +``` + +Or an even more useful example, using the stored environment variable to make an authenticated API request. + +### Using API key authentication + +If an particular service requires you to use an API key, you can pass it via the headers of the request. + +This proves your identity to the service so you can interact with it: + +```python +import requests +import os + +def handler(pd: "pipedream"): + token = os.environ["AIRTABLE_API_KEY"] + + url = "https://api.airtable.com/v0/your-airtable-base/your-table" + + headers = { "Authorization": f"Bearer {token}"} + r = requests.get(url, headers=headers) + + print(r.text) +``` + + +There are 2 different ways of using the `os` module to access your environment variables. + +`os.environ["ENV_NAME_HERE"]` will raise an error that stops your workflow if that key doesn't exist in your Pipedream account. + +Whereas `os.environ.get("ENV_NAME_HERE")` will _not_ throw an error and instead returns an empty string. + +If your code relies on the presence of a environment variable, consider using `os.environ["ENV_NAME_HERE"]` instead. + + +## Handling errors + +You may need to exit a workflow early. In a Python step, just a `raise` an error to halt a step's execution. + +```python +raise NameError("Something happened that should not. Exiting early.") +``` + +All exceptions from your Python code will appear in the **logs** area of the results. + +## Ending a workflow early + +Sometimes you want to end your workflow early, or otherwise stop or cancel the execution of a workflow under certain conditions. For example: + +- You may want to end your workflow early if you don't receive all the fields you expect in the event data. +- You only want to run your workflow for 5% of all events sent from your source. +- You only want to run your workflow for users in the United States. If you receive a request from outside the U.S., you don't want the rest of the code in your workflow to run. +- You may use the `user_id` contained in the event to look up information in an external API. If you can't find data in the API tied to that user, you don't want to proceed. + +**In any code step, calling `return pd.flow.exit()` will end the execution of the workflow immediately.** No remaining code in that step, and no code or destination steps below, will run for the current event. + + +It's a good practice to use `return pd.flow.exit()` to immediately exit the workflow. +In contrast, `pd.flow.exit()` on its own will end the workflow only after executing all remaining code in the step. + + +```python +def handler(pd: "pipedream"): + return pd.flow.exit("reason") + print("This code will not run, since pd.flow.exit() was called above it") +``` + +You can pass any string as an argument to `pd.flow.exit()`: + +```python +def handler(pd: "pipedream"): + return pd.flow.exit("Exiting early. Goodbye.") + print("This code will not run, since pd.flow.exit() was called above it") +``` + +Or exit the workflow early within a conditional: + +```python +import random + +def handler(pd: "pipedream"): + # Flip a coin, running pd.flow.exit() for 50% of events + if random.randint(0, 100) <= 50: + return pd.flow.exit("reason") + + print("This code will only run 50% of the time"); +``` + +## File storage + +You can also store and read files with Python steps. This means you can upload photos, retrieve datasets, accept files from an HTTP request and more. + +The `/tmp` directory is accessible from your workflow steps for saving and retrieving files. + +You have full access to read and write both files in `/tmp`. + +See the [Working with the filesystem in Python](/workflows/building-workflows/code/python/working-with-files/) docs for more information. + +## FAQ + +### What's the difference between `def handler(pd)` and the `pipedream` package for Python code steps? + +The pd object passed to the handler method lets you exit the [workflow early](/workflows/building-workflows/code/python/#ending-a-workflow-early), [integrate a Data Store](/workflows/building-workflows/code/python/using-data-stores/), and [use connected accounts](/workflows/building-workflows/code/python/auth/) into your Python code steps. + +However, at this time there are issues with our Python interpreter that is causing an `ECONNRESET` error. + +If you need [to use data from other steps](/workflows/building-workflows/code/python/#using-data-from-another-step) or [export data to other steps](/workflows/building-workflows/code/python/#sending-data-downstream-to-other-steps) in your workflow, we recommend using the `pipedream` package module. + +If you need to use a Data Store in your workflow, we recommend using a [pre-built action](/workflows/data-management/data-stores/) to retrieve or store data or [Node.js's Data Store](/workflows/building-workflows/code/nodejs/using-data-stores/) capabilities. + +### I've tried installing a Python package with a normal import and the magic comment system, but I still can't. What can I do? + +Some Python packages require binaries present within the environment in order to function properly. Or they include binaries but those binaries are not compatible with the Pipedream workflow environment. + +Unfortunately we cannot support these types of packages at this time, but if you have an issue importing a PyPI package into a Python code step [please open a issue](https://github.com/PipedreamHQ/pipedream/issues/new/choose). + +### Can I publish my Python code as a reusable pre-built action or trigger like you can with Node.js? + +Not at this time. Pipedream only supports Python as a code step language. The Components system only supports Node.js at this time. + +You can still duplicate Python code steps within the same workflow, but to reuse a code step, you'll need to copy and paste the Python code to another workflow. diff --git a/docs-v2/pages/workflows/building-workflows/code/python/rerun.mdx b/docs-v2/pages/workflows/building-workflows/code/python/rerun.mdx new file mode 100644 index 0000000000000..01f9463bbf8d1 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/code/python/rerun.mdx @@ -0,0 +1,179 @@ +import Callout from '@/components/Callout' + +# Pause, resume, and rerun a workflow + +You can use `pd.flow.suspend` and `pd.flow.rerun` to pause a workflow and resume it later. + +This is useful when you want to: + +- Pause a workflow until someone manually approves it +- Poll an external API until some job completes, and proceed with the workflow when it's done +- Trigger an external API to start a job, pause the workflow, and resume it when the external API sends an HTTP callback + +We'll cover all of these examples below. + +## `pd.flow.suspend` + +Use `pd.flow.suspend` when you want to pause a workflow and proceed with the remaining steps only when manually approved or cancelled. + +For example, you can suspend a workflow and send yourself a link to manually resume or cancel the rest of the workflow: + +```python +def handler(pd: 'pipedream'): + urls = pd.flow.suspend() + pd.send.email( + subject="Please approve this important workflow", + text=f"Click here to approve the workflow: ${urls["resume_url"]}, and cancel here: ${urls["cancel_url"]}" + ) + # Pipedream suspends your workflow at the end of the step +``` + +You'll receive an email like this: + +![Approve this workflow](https://res.cloudinary.com/pipedreamin/image/upload/v1655272047/docs/approve-workflow_oc06k3.png) + +And can resume or cancel the rest of the workflow by clicking on the appropriate link. + +### `resume_url` and `cancel_url` + +In general, calling `pd.flow.suspend` returns a `cancel_url` and `resume_url` that lets you cancel or resume paused executions. Since Pipedream pauses your workflow at the _end_ of the step, you can pass these URLs to any external service before the workflow pauses. If that service accepts a callback URL, it can trigger the `resume_url` when its work is complete. + +These URLs are specific to a single execution of your workflow. While the workflow is paused, you can load these in your browser or send any HTTP request to them: + +- Sending an HTTP request to the `cancel_url` will cancel that execution +- Sending an HTTP request to the `resume_url` will resume that execution + +If you resume a workflow, any data sent in the HTTP request is passed to the workflow and returned in the `$resume_data` [step export](/workflows/#step-exports) of the suspended step. For example, if you call `pd.flow.suspend` within a step named `code`, the `$resume_data` export should contain the data sent in the `resume_url` request: + +![resume data step export](https://res.cloudinary.com/pipedreamin/image/upload/v1655271815/docs/resume_data_lafhxr.png) + +### Default timeout of 24 hours + +By default, `pd.flow.suspend` will automatically cancel the workflow after 24 hours. You can set your own timeout (in milliseconds) as the first argument: + +```python +def handler(pd: 'pipedream'): + # 7 days + TIMEOUT = 1000 * 60 * 60 * 24 * 7 + pd.flow.suspend(TIMEOUT) +``` + +## `pd.flow.rerun` + +Use `pd.flow.rerun` when you want to run a specific step of a workflow multiple times. This is useful when you need to start a job in an external API and poll for its completion, or have the service call back to the step and let you process the HTTP request within the step. + +### Polling for the status of an external job + +Sometimes you need to poll for the status of an external job until it completes. `pd.flow.rerun` lets you rerun a specific step multiple times: + +```python +import requests + +def handler(pd: 'pipedream'): + MAX_RETRIES = 3 + # 10 seconds + DELAY = 1000 * 10 + run = pd.context['run'] + print(pd.context) + # pd.context.run.runs starts at 1 and increments when the step is rerun + if run['runs'] == 1: + # pd.flow.rerun(delay, context (discussed below), max retries) + pd.flow.rerun(DELAY, None, MAX_RETRIES) + + elif run['runs'] == MAX_RETRIES + 1: + raise Exception("Max retries exceeded") + + else: + # Poll external API for status + response = requests.get("https://example.com/status") + # If we're done, continue with the rest of the workflow + if response.json().status == "DONE": + return response.json() + + # Else retry later + pd.flow.rerun(DELAY, None, MAX_RETRIES) +``` + +`pd.flow.rerun` accepts the following arguments: + +```python +pd.flow.rerun( + delay, # The number of milliseconds until the step will be rerun + context, # JSON-serializable data you need to pass between runs + maxRetries, # The total number of times the step will rerun. Defaults to 10 +) +``` + +### Accept an HTTP callback from an external service + +When you trigger a job in an external service, and that service can send back data in an HTTP callback to Pipedream, you can process that data within the same step using `pd.flow.retry`: + +```python +import requests + +def handler(pd: 'pipedream'): + TIMEOUT = 86400 * 1000 + run = pd.context['run'] + # pd.context['run']['runs'] starts at 1 and increments when the step is rerun + if run['runs'] == 1: + links = pd.flow.rerun(TIMEOUT, None, 1) + # links contains a dictionary with two entries: resume_url and cancel_url + + # Send resume_url to external service + await request.post("your callback URL", json=links) + + # When the external service calls back into the resume_url, you have access to + # the callback data within pd.context.run['callback_request'] + elif 'callback_request' in run: + return run['callback_request'] + +``` + +### Passing `context` to `pd.flow.rerun` + +Within a Python code step, `pd.context.run.context` contains the `context` passed from the prior call to `rerun`. This lets you pass data from one run to another. For example, if you call: + +```python +pd.flow.rerun(1000, { "hello": "world" }) +``` + +`pd.context.run.context` will contain: + +
+resume data step export +
+ +### `maxRetries` + +By default, `maxRetries` is **10**. + +When you exceed `maxRetries`, the workflow proceeds to the next step. If you need to handle this case with an exception, `raise` an Exception from the step: + +```python +def handler(pd: 'pipedream'): + MAX_RETRIES = 3 + run = pd.context['run'] + if run['runs'] == 1: + pd.flow.rerun(1000, None, MAX_RETRIES) + + else if (run['runs'] == MAX_RETRIES + 1): + raise Exception("Max retries exceeded") +``` + +## Behavior when testing + +When you're building a workflow and test a step with `pd.flow.suspend` or `pd.flow.rerun`, it will not suspend the workflow, and you'll see a message like the following: + +> Workflow execution canceled — this may be due to `pd.flow.suspend()` usage (not supported in test) + +These functions will only suspend and resume when run in production. + +## Credits usage when using `pd.flow.suspend` / `pd.flow.rerun` + +You are not charged for the time your workflow is suspended during a `pd.flow.suspend` or `pd.flow.rerun`. Only when workflows are resumed will compute time count toward [credit usage](/pricing/#credits). + + +When a suspended workflow reawakens, it will reset the credit counter. + +Each rerun or reawakening from a suspension will count as a new fresh credit. + diff --git a/docs-v2/pages/workflows/building-workflows/code/python/using-data-stores.mdx b/docs-v2/pages/workflows/building-workflows/code/python/using-data-stores.mdx new file mode 100644 index 0000000000000..240f78313d9fe --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/code/python/using-data-stores.mdx @@ -0,0 +1,298 @@ +import Callout from '@/components/Callout' + +# Using Data Stores + +You can store and retrieve data from [Data stores](/workflows/data-management/data-stores/) in Python without connecting to a 3rd party database. + +Add a data store as a input to a Python step, then access it in your Python `handler` with `pd.inputs["data_store"]`. + +```python +def handler(pd: "pipedream"): + # Access the data store under the pd.inputs + data_store = pd.inputs["data_store"] + + # Store a value under a key + data_store["key"] = "Hello World" + + # Retrieve the value and print it to the step's Logs + print(data_store["key"]) + +``` + +## Adding a Data Store + +Click _Add Data Store_ near the top of a Python step: + +![Adding a data store to a Python step](https://res.cloudinary.com/pipedreamin/image/upload/v1710518388/docs/docs/workflows/building-workflows/code/pythondata-stores/CleanShot_2024-03-15_at_11.58.53_ognvbc.gif) + +This will add the selected data store to your Python code step. + +## Saving data + +Data stores are key-value stores. Saving data within a data store is just like setting a property on a dictionary: + +```python +from datetime import datetime + +def handler(pd: "pipedream"): + # Access the data store under the pd.inputs + data_store = pd.inputs["data_store"] + + # Store a timestamp + data_store["last_ran_at"] = datetime.now().isoformat() +``` + +## Retrieving keys + +Fetch all the keys in a given data store using the `keys` method: + +```python +def handler(pd: "pipedream"): + # Access the data store under the pd.inputs + data_store = pd.inputs["data_store"] + + # Retrieve all keys in the data store + keys = pd.inputs["data_store"].keys() + + # Print a comma separated string of all keys + print(*keys, sep=",") +``` + + +The `datastore.keys()` method does not return a list, but instead it returns a `Keys` iterable object. You cannot export a `data_store` or `data_store.keys()` from a Python code step at this time. + +Instead, build a dictionary or list when using the `data_store.keys()` method. + + +## Checking for the existence of specific keys + +If you need to check whether a specific `key` exists in a data store, use `if` and `in` as a conditional: + +```python +def handler(pd: "pipedream"): + # Access the data store under the pd.inputs + data_store = pd.inputs["data_store"] + + # Search for a key in a conditional + if "last_ran_at" in data_store: + print(f"Last ran at {data_store['last_ran_at']}") +``` + +## Retrieving data + +Data stores are very performant at retrieving single records by keys. However you can also use key iteration to retrieve all records within a Data Store as well. + + +Data stores are intended to be a fast and convienent data storage option for quickly adding data storage capability to your workflows without adding another database dependency. + +However, if you need more advanced querying capabilities for querying records with nested dictionaries or filtering based on a record value - consider using a full fledged database. Pipedream can integrate with MySQL, Postgres, DynamoDb, MongoDB and more. + + +### Get a single record + +You can retrieve single records from a data store by key: + +```python +def handler(pd: "pipedream"): + # Access the data store under the pd.inputs + data_store = pd.inputs["data_store"] + + # Retrieve the timestamp value by the key name + last_ran_at = data_store["last_ran_at"] + + # Print the timestamp + print(f"Last ran at {last_ran_at}") +``` + +Alternatively, use the `data_store.get()` method to retrieve a specific key's contents: + +```python +def handler(pd: "pipedream"): + # Access the data store under the pd.inputs + data_store = pd.inputs["data_store"] + + # Retrieve the timestamp value by the key name + last_ran_at = data_store.get("last_ran_at") + + # Print the timestamp + print(f"Last ran at {last_ran_at}") +``` + + +What's the difference between `data_store["key"]` and `data_store.get("key")`? + +- `data_store["key"]` will throw a `TypeError` if the key doesn't exist in the data store. +- `data_store.get("key")` will instead return `None` if the key doesn't exist in the data store. +- `data_store.get("key", "default_value")` will return `"default_value"` if the key doesn't exist on the data store. + + +### Retrieving all records + +You can retrieve all records within a data store by using an async iterator: + +```python +def handler(pd: "pipedream"): + data_store = pd.inputs["data_store"] + records = {} + for k,v in data_store.items(): + records[k] = v + return records +``` + +This code step example exports all records within the data store as a dictionary. + +## Deleting or updating values within a record + +To delete or update the _value_ of an individual record, assign `key` a new value or `''` to remove the value but retain the key. + +```python +def handler(pd: "pipedream"): + # Access the data store under the pd.inputs + data_store = pd.inputs["data_store"] + + # Assign a new value to the key + data_store["myKey"] = "newValue" + + # Remove the value but retain the key + data_store["myKey"] = "" +``` + +### Working with nested dictionaries + +You can store dictionaries within a record. This allows you to create complex records. + +However, to update specific attributes within a nested dictionary, you'll need to replace the record entirely. + +For example, the code the below will **not** update the `name` attribute on the stored dictionary stored under the key `pokemon`: + +```python +def handler(pd: "pipedream"): + # The current dictionary looks like this: + # pokemon: { + # "name": "Charmander" + # "type": "fire" + # } + + # You'll see "Charmander" in the logs + print(pd.inputs['data_store']['pokemon']['name']) + + # attempting to overwrite the pokemon's name will not apply + pd.inputs['data_store']['pokemon']['name'] = 'Bulbasaur' + + # Exports "Charmander" + return pd.inputs['data_store']['pokemon']['name'] +``` + +Instead, _overwrite_ the entire record to modify attributes: + +```python +def handler(pd: "pipedream"): + # retrieve the record item by it's key first + pokemon = pd.inputs['data_store']['pokemon'] + + # now update the record's attribute + pokemon['name'] = 'Bulbasaur' + + # and out right replace the record with the new modified dictionary + pd.inputs['data_store']['pokemon'] = pokemon + + # Now we'll see "Bulbasaur" exported + return pd.inputs['data_store']['pokemon']['name'] +``` + +## Deleting specific records + +To delete individual records in a data store, use the `del` operation for a specific `key`: + +```python +def handler(pd: "pipedream"): + # Access the data store under the pd.inputs + data_store = pd.inputs["data_store"] + + # Delete the last_ran_at timestamp key + del data_store["last_ran_at"] +``` + +## Deleting all records from a specific data store + +If you need to delete all records in a given data store, you can use the `clear` method. + +```python +def handler(pd: "pipedream"): + # Access the data store under the pd.inputs + data_store = pd.inputs["data_store"] + + # Delete the entire contents of the datas store + data_store.clear() +``` + + +`data_store.clear()` is an **irreversible** change, **even when testing code** in the workflow builder. + + +## Viewing store data + +You can view the contents of your data stores in your [Pipedream dashboard](https://pipedream.com/stores). + +From here you can also manually edit your data store's data, rename stores, delete stores or create new stores. + +## Workflow counter example + +You can use a data store as a counter. For example, this code counts the number of times the workflow runs: + +```python +def handler(pd: "pipedream"): + # Access the data store under the pd.inputs + data_store = pd.inputs["data_store"] + + # if the counter doesn't exist yet, start it at one + if data_store.get("counter") == None: + data_store["counter"] = 1 + + # Otherwise, increment it by one + else: + count = data_store["counter"] + data_store["counter"] = count + 1 +``` + +## Dedupe data example + +Data Stores are also useful for storing data from prior runs to prevent acting on duplicate data, or data that's been seen before. + +For example, this workflow's trigger contains an email address from a potential new customer. But we want to track all emails collected so we don't send a welcome email twice: + +```python +def handler(pd: "pipedream"): + # Access the data store + data_store = pd.inputs["data_store"] + + # Reference the incoming email from the HTTP request + new_email = pd.steps["trigger"]["event"]["body"]["new_customer_email"] + + # Retrieve the emails stored in our data store + emails = data_store.get('emails', []) + + # If this email has been seen before, exit early + if new_email in emails: + print(f"Already seen {new_email}, exiting") + return False + + # This email is new, append it to our list + else: + print(f"Adding new email to data store {new_email}") + emails.append(new_email) + data_store["emails"] = emails + return new_email +``` + +### Supported data types + +Data stores can hold any JSON-serializable data within the storage limits. This includes data types including: + +- Strings +- Dictionaries +- Lists +- Integers +- Floats + +But you cannot serialize Modules, Functions, Classes, or other more complex objects. diff --git a/docs-v2/pages/workflows/building-workflows/code/python/working-with-files.mdx b/docs-v2/pages/workflows/building-workflows/code/python/working-with-files.mdx new file mode 100644 index 0000000000000..3a37a213a0087 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/code/python/working-with-files.mdx @@ -0,0 +1,115 @@ +import Callout from '@/components/Callout' + +# Working with the filesystem in Python + +You can work with files within a workflow. For instance, downloading content from some service to upload to another. Here are some sample code for common file operations. + +## The `/tmp` directory + +Within a workflow, you have full read-write access to the `/tmp` directory. You have {process.env.TMP_SIZE_LIMIT} of available space in `/tmp` to save any file. + +### Managing `/tmp` across workflow runs + +The `/tmp` directory is stored on the virtual machine that runs your workflow. We call this the execution environment ("EE"). More than one EE may be created to handle high-volume workflows. And EEs can be destroyed at any time (for example, after about 10 minutes of receiving no events). This means that you should not expect to have access to files across executions. At the same time, files _may_ remain, so you should clean them up to make sure that doesn't affect your workflow. **Use [the `tempfile` module](https://docs.python.org/3/library/tempfile.html) to cleanup files after use, or [delete the files manually](#deleting-a-file).** + +## Writing a file to `/tmp` + +```python +import requests + +def handler(pd: "pipedream"): + # Download the Python logo + r = requests.get("https://www.python.org/static/img/python-logo@2x.png") + + # Create a new file python-logo.png in the /tmp/data directory + with open("/tmp/python-logo.png", "wb") as f: + # Save the content of the HTTP response into the file + f.write(r.content) +``` + +Now `/tmp/python-logo.png` holds the official Python logo. + +## Reading a file from `/tmp` + +You can also open files you have previously stored in the `/tmp` directory. Let's open the `python-logo.png` file. + +```python +import os + +def handler(pd: "pipedream"): + with open("/tmp/python-logo.png") as f: + # Store the contents of the file into a variable + file_data = f.read() +``` + +## Listing files in `/tmp` + +If you need to check what files are currently in `/tmp` you can list them and print the results to the **Logs** section of **Results**: + +```python +import os + +def handler(pd: "pipedream"): + # Prints the files in the tmp directory + print(os.listdir("/tmp")) +``` + +## Deleting a file + +```python +import os + +def handler(pd: "pipedream"): + print(os.unlink("/tmp/your-file")) +``` + +## Downloading a file to `/tmp` + +[See this example](/workflows/building-workflows/code/python/http-requests/#downloading-a-file-to-the-tmp-directory) to learn how to download a file to `/tmp`. + +## Uploading a file from `/tmp` + +[See this example](/workflows/building-workflows/code/python/http-requests/#uploading-a-file-from-the-tmp-directory) to learn how to upload a file from `/tmp` in an HTTP request. + +## Downloading a file, uploading it in another `multipart/form-data` request + +```python +import requests +from requests_toolbelt.multipart.encoder import MultipartEncoder +import os + +def handler(pd: "pipedream"): + download_url = "https://example.com" + upload_url = "http://httpbin.org/post" + file_path = "/tmp/index.html" + content_type = "text/html" + + # DOWNLOAD + with requests.get(download_url, stream=True) as response: + response.raise_for_status() + with open(file_path, "wb") as file: + for chunk in response.iter_content(chunk_size=8192): + file.write(chunk) + + # UPLOAD + multipart_data = MultipartEncoder(fields={ + 'file': (os.path.basename(file_path), open(file_path, 'rb'), content_type) + }) + response = requests.post( + upload_url, + data=multipart_data, + headers={'Content-Type': multipart_data.content_type} + ) +``` + +## `/tmp` limitations + +The `/tmp` directory can store up to {process.env.TMP_SIZE_LIMIT} of storage. Also the storage may be wiped or may not exist between workflow executions. + +To avoid errors, assume that the `/tmp` directory is empty between workflow runs. Please refer to the [disk limits](/workflows/limits/#disk) for details. + + +Are File Stores helpers available for Python to download, upload and manage files? + +At this time no, only Node.js includes a helper to interact with the [File Store](/workflows/projects/file-stores/) programmatically within workflows. + diff --git a/docs-v2/pages/workflows/building-workflows/control-flow/_meta.tsx b/docs-v2/pages/workflows/building-workflows/control-flow/_meta.tsx new file mode 100644 index 0000000000000..ebd3a73ede551 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/control-flow/_meta.tsx @@ -0,0 +1,9 @@ +export default { + "index": "Overview", + "ifelse": "Branching - If/Else", + "switch": "Branching - Switch", + "parallel": "Branching - Parallel", + "delay": "Delay", + "filter": "Filter", + "end-workflow": "End Workflow", +} as const diff --git a/docs-v2/pages/workflows/building-workflows/control-flow/delay.mdx b/docs-v2/pages/workflows/building-workflows/control-flow/delay.mdx new file mode 100644 index 0000000000000..ad19ca0919333 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/control-flow/delay.mdx @@ -0,0 +1,50 @@ +import VideoPlayer from "@/components/VideoPlayer"; + +### Delay + + + +Sometimes you need to wait a specific amount of time before the next step of your workflow proceeds. Pipedream supports this in one of two ways: + +1. The built-in **Delay** actions +2. The `$.flow.delay` function in Node.js + +{process.env.DELAY_MIN_MAX_TIME}. For example, we at Pipedream use this functionality to delay welcome emails to our customers until they've had a chance to use the product. + +#### Delay actions + +You can pause your workflow without writing code using the **Delay** actions: + +1. Click the **+** button below any step +2. Search for the **Delay** app +3. Select the **Delay Workflow** action +4. Configure the action to delay any amount of time, up to one year + +![Workflow delay step](/images/control-flow/delay-step-props.png) + +#### `$.flow.delay` + +If you need to delay a workflow within Node.js code, or you need detailed control over how delays occur, [see the docs on `$.flow.delay`](/workflows/building-workflows/code/nodejs/delay/). + +#### The state of delayed executions + +Delayed executions can hold one of three states: + +- **Paused**: The execution is within the delay window, and the workflow is still paused +- **Resumed**: The workflow has been resumed at the end of its delay window automatically, or resumed manually +- **Cancelled**: The execution was cancelled manually + +You'll see the current state of an execution by [viewing its event data](/workflows/building-workflows/triggers/inspect/). + +#### Cancelling or resuming execution manually + +The [**Delay** actions](#delay-actions) and [`$.flow.delay`](/workflows/building-workflows/code/nodejs/delay/) return two URLs each time they run: + +![Cancel and resume URLs](https://res.cloudinary.com/pipedreamin/image/upload/v1651551860/docs/Screen_Shot_2022-05-02_at_9.16.11_PM_ahw7tu.png) + +These URLs are specific to a single execution of your workflow. While the workflow is paused, you can load these in your browser or send an HTTP request to either: + +- Hitting the `cancel_url` will immediately cancel that execution +- Hitting the `resume_url` will immediately resume that execution early + +If you use [`$.flow.delay`](/workflows/building-workflows/code/nodejs/delay/), you can send these URLs to your own system to handle cancellation / resumption. You can even email your customers to let them cancel / resume workflows that run on their behalf. diff --git a/docs-v2/pages/workflows/building-workflows/control-flow/end-workflow.mdx b/docs-v2/pages/workflows/building-workflows/control-flow/end-workflow.mdx new file mode 100644 index 0000000000000..0123b8e25dc83 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/control-flow/end-workflow.mdx @@ -0,0 +1,21 @@ +# End Workflow + +To terminate the workflow prior to the last step, use the **End Workflow** pre-built action or `$.flow.exit()` in code. + +![End Workflow](/images/control-flow/end_workflow.png) + +## End Workflow Using a Pre-Built Action + +- Select and configure the End Workflow action from the step selector +- When the step runs, the workflow execution will stop +- You may configure an optional reason for ending the workflow execution. This reason will be surfaced when inspecting the event execution. + + ![Reason](/images/control-flow/reason.png) + + +## End Workflow in Code + +Check the reference for your preferred language to learn how to end the workflow execution in code. + +- [Ending a workflow in Node.js](/workflows/building-workflows/code/nodejs/#ending-a-workflow-early) +- [Ending a workflow in Python](/workflows/building-workflows/code/python/#ending-a-workflow-early) diff --git a/docs-v2/pages/workflows/building-workflows/control-flow/filter.mdx b/docs-v2/pages/workflows/building-workflows/control-flow/filter.mdx new file mode 100644 index 0000000000000..f92c8bc630cca --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/control-flow/filter.mdx @@ -0,0 +1,21 @@ +import VideoPlayer from "@/components/VideoPlayer"; + +### Filter + + + +Use the Filter action to quickly stop or continue workflows on certain conditions. + +![Create a filter action](https://res.cloudinary.com/pipedreamin/image/upload/v1651603927/docs/animations/CleanShot_2022-05-03_at_13.51.17_za1skw.gif) + +Add a filter action to your workflow by searching for the **Filter** app in a new step. + +The **Filter** app includes several built-in actions: Continue Workflow on Condition, Exit Workflow on Condition and Exit Workflow on Custom Condition. + +In each of these actions, the **Value** is the subject of the condition, and the **Condition** is the operand to compare the value against. + +For example, to only process orders with a `status = ready` + +#### Continue Workflow on Condition + +With this action, only when values that _pass_ a set condition will the workflow continue to execute steps after this filter. diff --git a/docs-v2/pages/workflows/building-workflows/control-flow/ifelse.mdx b/docs-v2/pages/workflows/building-workflows/control-flow/ifelse.mdx new file mode 100644 index 0000000000000..b2f42498a0d4e --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/control-flow/ifelse.mdx @@ -0,0 +1,86 @@ +import Callout from '@/components/Callout' +import VideoPlayer from "@/components/VideoPlayer"; +import { Steps } from 'nextra/components' + +# If/Else + +## Overview + +**If/Else** is single path branching operator. You can create multiple execution branches, but Pipedream will execute the **first** branch that matches the configured rules. The order in which rules are defined will affect the path of execution. + +If/Else operator is useful when you need to branch based on the value of multiple input variables. You must define both the input variable and condition to evaluate for every rule. If you only need to test for the value of a single input variable (e.g., if you are branching based on the path of an inbound request), the [Switch operator](./switch/) may be a better choice. + +![if/else configuration](/images/control-flow/ifelse-configuration.png) + +## Capabilities + +- Define rules to conditionally execute one of many branches +- Evaluate one or more expressions for each condition (use boolean operators to combine muliple rules) +- Use the **Else** condition as a fallback +- Merge and continue execution in the parent flow after the branching operation + + +If you disable the **Else** branch and there are no matching cases, the workflow will continue execution in the parent workflow after the **end** phase of the If/Else block + + + +The If/Else operator is a control flow **Block** with **start** and **end** phases. [Learn more about Blocks](./#blocks). + + +## Demo + + + +## Getting Started + + + +### Generate a test event + +Add a trigger and generate an event to help you build and test your workflow: + +![trigger.gif](/images/control-flow/trigger.gif) + +### Add the If/Else control flow block + +Click the + button to add a step to the canvas and select If/Else from the Control Flow section on the right. In the “start” phase, configure rules for each branch (optionally toggle the else branch) and then test the step. + +![add if else.gif](/images/control-flow/add_if_else.gif) + + +**IMPORTANT:** If you disable the **Else** condition and an event does not match any of the rules, the workflow will continue to the next step after the **If/Else** section. If you want to end workflow execution if no other conditions evaluate to `true`, enable the Else condition and add a **Terminate Workflow** action. + + +### Build and test along the execution path + +Add a step to the success branch and test it + +![add step to branch.gif](/images/control-flow/add_step_to_branch.gif) + +### Merge and continue the parent flow after the branching operation + +Test the end phase to export results from the If/Else control flow block. + +![test end phase.gif](/images/control-flow/test_end_phase.gif) + +Add a step and reference the exports from `ifelse` using the steps object. + +![reference end exports.gif](/images/control-flow/reference_end_exports.gif) + +### Build and test alternate paths + +Generate or select an alternate event to generate data to help you test other branches as you build. When you select a new event, the steps in the root workflow segments go stale. Steps in control flow blocks will only go stale if they are in the known path of execution; i.e., if you test a start phase, the steps in the success path will become stale. + +![select different event.gif](/images/control-flow/select_different_event.gif) + +Build, test and deploy the workflow. + +![test and deploy.gif](/images/control-flow/test_and_deploy.gif) + +### Test the deployed workflow + +Generate test events to trigger the deployed workflow and inspect the executions. + +![Inspect.gif](/images/control-flow/Inspect.gif) + + diff --git a/docs-v2/pages/workflows/building-workflows/control-flow/index.mdx b/docs-v2/pages/workflows/building-workflows/control-flow/index.mdx new file mode 100644 index 0000000000000..623c69ca71a60 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/control-flow/index.mdx @@ -0,0 +1,261 @@ +import VideoPlayer from "@/components/VideoPlayer"; + +# Overview + +Pipedream is adding powerful control flow operators so you can build and run non-linear workflows to unlock use cases that require advanced orchestration. + + + +## Operators + +| Operator | Description | +| :--- | :--- | +| [If/Else (beta)](./control-flow/ifelse/) | Supports single-path, logical branching orchestration. | +| [Delay](./control-flow/delay/) | Add a delay from 1 millisecond to 1 year before the next step of your workflow proceeds. | +| [Filter](./control-flow/filter/) | Define rules to stop or continue workflow execution. | +| [End Workflow](./control-flow/end-workflow/) | Terminate the workflow prior to the last step. | + +More operators (including parallel and looping) are coming soon. + +## Key Capabilities + +- Orchestrate execution of linear and non-linear workflows +- Normalize results and continue after a non-linear operation +- Nest control flow operators for advanced use cases +- Return HTTP responses during or after most non-linear operations +- Execute long running workflows (workflow timeout resets at each control flow boundary) + +## Execution Path + +### Context + +The execution path represents the specific steps (and the order of steps) that run when a workflow is triggered. + +- Simple linear workflows are executed from top to bottom — every step is in the execution path. + + ![Linear workflow](/images/control-flow/execution_path_a_linear.png) + +- With the introduction of non-linear workflows, steps may or may not be executed depending on the rules configured for control flow operators and the results exported from prior steps. + + ![Non-linear workflow](/images/control-flow/execution_path_b_nonlinear.png) + + Therefore, we introduced new patterns to signal the execution path and help you build, test and inspect workflows. + + +### Executed Path + +Step borders, backgrounds and connectors now highlight the **executed path** — the steps that are executed on the execution path. If a non-execution path step is tested, it will not be reflected as being on the execution path. + +![Linear workflow](/images/control-flow/executed_path.png) + +### Building and Testing in an Unknown or Non-Execution Path + +You may add and test steps in any path. However, Pipedream highlights that the results may not be reliable if the step is outside the executed path; the results may not match the outcome if the steps were in a known execution path and may lead to invalid or misleading results. + +![Unkonwn execution path](/images/control-flow/unknown_execution_path.png) + +### Signaling Steps are “Out of Date” + +If prior steps in a workflow are modified or retested, Pipedream marks later steps in the execution path as _stale_ to signal that the results may be out of date. In the non-linear model, Pipedream only marks steps that are in the confirmed execution path as stale. + +- If a change is made to a prior step, then the executed path is cleared. + + ![Out of date](/images/control-flow/out_of_date.gif) + +- Steps in the known execution path are immediately marked as stale +- State within conditional blocks is not updated until the start phase is tested and execution path is identified. + + ![Change in state](/images/control-flow/change_in_state.gif) + + +### Test State vs Execution Path + +Steps may be tested whether or not they are in the execution path. The test state for a step reflects whether a step was successfully tested or needs attention (the step may have errored, the results may be out of date, etc) and is denoted by the icon at the top left of each step. + +- Last test was successful + + ![Success](/images/control-flow/state_success.png) + +- Results may be stale, step may be untested, etc + + ![Stale](/images/control-flow/state_stale.png) + +- **Step has an error or is not configured** + + ![Error](/images/control-flow/state_error.png) + + +## Workflow Segments + +### Context + +Workflow segments are a linear series of steps that with no control flow operators. + +- A simple linear workflow is composed of a single workflow segment. + + ![Success](/images/control-flow/state_success.png) + +- When a control flow operator is introduced, then the workflow contains multiple segments. For example, when a Delay operator is added to the simple linear workflow above the workflow goes from 1 to 2 segements. + + ![Linear Segment](/images/control-flow/segment_linear.png) + +- The following example using If/Else contains 5 workflow segments. However, since only 1 branch within the If/Else control flow block is run on each workflow execution, the maximum number of segments that will be executed for each trigger event is 3. + + ![Non-Linear Segment](/images/control-flow/segment_non_linear.png) + + +### Billing + +Pipedream compiles each workflow segment into an executable function to optimize performance and reduce credit usage; credit usage is calculated independently for each workflow segment independent of the number of steps (rather than per step like many other platforms). + +- For example, the two workflow segments below both use a single credit: + - **Trigger + 1 step workflow segment (1 credit)** + + ![1 credit](/images/control-flow/billing_1credit_a.png) + + - **Trigger + 5 step workflow segment (1 credit)** + + ![1 credit](/images/control-flow/billing_1credit_b.png) + +- The If/Else example above that contains 5 workflow segments, but only 3 workflow segments in any given execution path will only incur 3 credits of usage per execution. + + ![1 credit](/images/control-flow/billing_3credits.png) + + +### Timeout and Memory + +For the preview, all workflow segments inherit the global timeout and memory settings. In the future, Pipedream will support customization of timeout and memory settings for each segment. For example, if you need expanded memory for select steps, you will be able to restrict higher memory execution to that segment instead of having to run the entire workflow with the higher memory settings. This can help you reduce credit usage. + +### Long Running Workflows + +Users may create long running workflows that greatly exceed the upper bound timeout of 12 minutes for current workflow (each workflow segment has an upper bound of 12 minutes). + +### `/tmp` directory access + +`tmp` directory access is scoped per workflow segment (since each segment is executed as an independent function). If you need to persist files across multiple segments (or workflow executions) use File Stores. + +### Warm Workers + +Warm workers are globally configured per segment. For example, if you have a workflow with 3 segments and you configure your workflow to use 1 warm worker per segment, 3 warm workers will be used. + +### Segment Relationships + +Steps may only reference prior steps in the same workflow segment or it’s direct ancestors. + +| Type | Description | +| --- | --- | +| Root | The root segment is the top level for a workflow — it may have children but no parents. If you do not include any control flow blocks in your workflow, your entire workflow definition is contained within the root segment. | +| Parent | A segment that has a child. | +| Child | A flow that has a parent. | + + +## Blocks + +### Context + +**Blocks** are compound steps that are composed of a **start** and an **end** phase. Blocks may contain one or more workflow segments between the phases. + +- Most non-linear control flow operators will be structured as blocks (vs. standard steps) +- You may add steps or blocks to [workflow segments](#workflow-segments) between start and end phases of a block +- The start and end phases are independently testable + - The start phase evaluates the rules/configuration for a block; the results may influence the execution path + - The end phase exports results from the control flow block that can be referenced in future workflow steps + - For example, for the If/Else control flow operator, the start phase evaluates the branching rules while the end phase exports the results from the executed branch. + +### Testing + +When building a workflow with a control flow block, we recommend testing the start phase, followed by steps in the execution path followed by the end phase. + +![Testing](/images/control-flow/2024-07-21_20.51.37.gif) + +For a conditional operator like if/else, we then recommend generating events that trigger alternate conditions and testing those. + +![Testing](/images/control-flow/2024-07-21_20.55.09.gif) + +#### Passing data to steps in a control flow block + +Steps may only reference prior steps in the same workflow segment or it’s direct ancestors. In the following example, `step_c` and `step_d` (within the if/else control flow block) can directly reference any exports from `trigger`, `step_a`, or `step_b` (in the parent/root workflow segment) via the steps object. `step_c` and `step_d` are siblings and cannot reference exports from each other. + +![Passing data](/images/control-flow/passing_data.png) + +#### Referencing data from steps in a previous block + +Steps after the end phase may not directly reference steps within a control flow block (between the start and end phases). E.g., in the following workflow there are two branches: + +![Passing data](/images/control-flow/referencing_data.png) + +In this example, `step_f` is executed after a control flow block. It can directly reference prior steps in the root workflow segment (`trigger`, `step_a` and `step_b` using the `steps` object). + +However, `step_f` cannot reference directly reference data exported by `step_c` or `step_d`. The reason is that due to the non-linear execution, `step_c` and `step_d` are not guaranteed to execute for every event. **To reference data from a control flow block, reference the exports of the end phase.** Refer to the documentation to understand how data is exported for each control flow operator (e.g., for if/else, the exports of the last step in the branch are returned as the exports for the end phase; you can easily normalize the results across branches using a code step). + +In this example, `step_f` can reference the exported data for an executed branch by referencing `steps.ifelse.$return_value`. + +### Nesting + +Control flow blocks may be nested within other control flow blocks: + +- If/Else blocks may be nested within other If/Else blocks +- In the future, Loops may be nested within If/Else blocks and vice versa. + +There is currently no limit on the number of nested elements. + +![Passing data](/images/control-flow/nesting.png) + +## Rule Builder + +### Context + +Pipedream is introducing a rule builder for comparative operations. The rule builder is currently only supported by the If/Else operator, but it will be extended to other operators including Switch and Filter. + +![Rule builder overview](/images/control-flow/rule_builder_overview.png) + +### Simple conditions + +Compare values using supported operators. + +![Rule builder simple](/images/control-flow/rule_builder_simple.png) + +### Combine multiple conditions using AND / OR + +Click “Add condition” using the menu on the right to add multiple conditions. Click on AND / OR to toggle the operator. + +![Rule builder multiple](/images/control-flow/rule_builder_multiple.png) + +### Test for multiple conditions using Groups + +Create and manage groups using the menu options to “Add group”, “Make condition into group”, “Nest group” and “Remove group”. + +![Rule builder groups](/images/control-flow/rule_builder_groups.png) + +### Supported Operators + +- Exists +- Doesn’t exist +- String + - Equals + - Doesn’t equal + - Is blank + - Is not blank + - Starts with + - Contains + - Ends with +- Number + - Equals + - Does not equal + - Is greater than + - Is greater than or equal to + - Is less than + - Is less than or equal to +- Boolean (equals) +- Type checks + - Is null + - Is not null + - Is string + - Is not a string + - Is a number + - Is not a number + - Is a boolean + - Is not a boolean +- Value checks + - Is true + - Is false diff --git a/docs-v2/pages/workflows/building-workflows/control-flow/parallel.mdx b/docs-v2/pages/workflows/building-workflows/control-flow/parallel.mdx new file mode 100644 index 0000000000000..7f9469ca5be50 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/control-flow/parallel.mdx @@ -0,0 +1,99 @@ +import Callout from '@/components/Callout' +import VideoPlayer from "@/components/VideoPlayer"; +import { Steps } from 'nextra/components' + +# Parallel + +## Overview + +**Parallel** is multi-path branching operator. It allows you to create multiple execution branches with optional filtering rules and Pipedream will execute **all** matching branches. Unlike [Switch](./switch/) and [If/Else](./ifelse/), the order in which rules are defined will not affect the path of execution. + +![Parallel](/images/control-flow/parallel/parallel.png) + +## Capabilities + +- Create non-linear workflows that execute steps in parallel branches +- Define when branches run — always, conditionally or never (to disable a branch) +- Merge and continue execution in the parent flow after the branching operation + + +The Parallel operator is a control flow **Block** with **start** and **end** phases. [Learn more about Blocks](./#blocks). + + +### Add Parallel operator to workflow + +Select **Parallel** from the **Control Flow** section of the step selector: + +![add parallel block](/images/control-flow/parallel/add_parallel_block.png) + +### Create Branches + +To create new branches, click the `+` button: + +![add branch](/images/control-flow/parallel/add_branch.png) + +### Rename Branches + +Edit the branch's nameslug on the canvas or in the right pane after selecting the **Start** phase of the parallel block. The nameslug communicates the branch's purpose and affects workflow execution—the end phase exports an object, with each key corresponding to a branch name. + +![rename branch](/images/control-flow/parallel/rename_branch.png) + +### Export Data to the Parent Flow + +You can export data from a parallel operation and continue execution in the parent flow. +- The parallel block exports data as a JSON object +- Branch exports are assigned to a key corresponding to the branch name slug (in the object exported from the block) +- Only the exports from the last step of each executed branch are included in the parallel block's return value +- To preview the exported data, test the **End** phase of the parallel block + +### Beta Limitations + +Workflow queue settings (concurrency, execution rate) may not work as expected with workflows using the parallel operator. + +## Getting Started + + + +### Generate a test event + +Add a trigger and generate an event to help you build and test your workflow: + +![trigger.gif](/images/control-flow/parallel/01_trigger.gif) + +### Add the Parallel control flow block + +Click the + button to add a step to the canvas and select Parallel from the Control Flow section on the right. You can optionally add or remove branches and configure conditions defining when each branch should run. + +![add parallel.gif](/images/control-flow/parallel/02_add_parallel.gif) + +### Test to identify the execution path(s) + +Test the **Start** phase to identify which branches will execute for the current event. + +![configure and test.gif](/images/control-flow/parallel/03_configure_and_test.gif) + +### Add steps to branches + +Add steps to the branches. These steps will be executed in parallel when the workflow runs. + +![add steps.gif](/images/control-flow/parallel/04_add_steps.gif) + +### Optionally merge and continue to the parent flow + +Test the **End** phase to export the results of the last step of each branch that was executed. This makes data from the branches available to reference in the parent flow. + +![export_data_to_parent](/images/control-flow/parallel/05_test_end_phase.gif) + +### Use exports in parent flow + +Optionally add steps after the parallel block and use data from individual branches by referencing the return value of the **End** phase. + +![use exports in parent flow](/images/control-flow/parallel/06_use_exports_in_parent_flow.gif) + +### Deploy and test the live workflow + +Deploy the workflow and trigger it to inspect the executions. + +![Inspect](/images/control-flow/parallel/07_deploy_and_run.gif) + + diff --git a/docs-v2/pages/workflows/building-workflows/control-flow/switch.mdx b/docs-v2/pages/workflows/building-workflows/control-flow/switch.mdx new file mode 100644 index 0000000000000..5e812b495ff46 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/control-flow/switch.mdx @@ -0,0 +1,86 @@ +import Callout from '@/components/Callout' +import VideoPlayer from "@/components/VideoPlayer"; +import { Steps } from 'nextra/components' + +# Switch + +## Overview + +**Switch** is single path branching operator. You can create multiple execution branches, but Pipedream will execute the **first** branch that matches the configured rules. The order in which rules are defined will affect the path of execution. + +Switch is useful when you need to make a branching decision based on the value of a single input variable (e.g., based on the path of an inbound request). You can define the input variable once and then branch based on the value(s). If you need to branch based on the values of multiple input variables use the [If/Else operator](./ifelse/). + +![Switch Configuration](/images/control-flow/switch/switch-configuration.png) + +## Capabilities + +- Define cases to conditionally execute one of many branches +- Define the expression to evaluate once and configure cases to compare values (use boolean operators to combine muliple rules for each case) +- Use the **Default** case as a fallback +- Merge and continue execution in the parent flow after the branching operation + + +If you disable the **Default** branch and there are no matching cases, the workflow will continue execution in the parent workflow after the **end** phase of the Switch block + + + +The Switch operator is a control flow **Block** with **start** and **end** phases. [Learn more about Blocks](./#blocks). + + +## Getting Started + + + +### Generate a test event + +Add a trigger and generate an event to help you build and test your workflow: + +![trigger.gif](/images/control-flow/switch/trigger.gif) + +### Add the Switch control flow block + +Click the + button to add a step to the canvas and select Switch from the Control Flow section on the right. In the “start” phase, configure rules for a case. + +![add switch.gif](/images/control-flow/switch/add_switch.gif) + + +**IMPORTANT:** If you disable the **Default** condition and an event does not match any of the rules, the workflow will continue to the next step after the **Switch** section. If you want to end workflow execution if no other conditions evaluate to `true`, enable the Default condition and add a **Terminate Workflow** action. + + +### Optionally add additional cases + +To add additional cases, click the **+** button. + +![add another case.gif](/images/control-flow/switch/add_another_case.gif) + +### Test and build along the execution path + +Test the **start** phase and add a step to the branch in the execution path, + +![test_and_build_success_path.gif](/images/control-flow/switch/test_and_build_success_path.gif) + +### Optionally merge and continue the parent flow + +Test the **end** phase to export the results of the last step in the execution path. This makes them available to reference in the parent flow. + +![export_data_to_parent.gif](/images/control-flow/switch/export_data_to_parent.gif) + +### Scaffold alternate paths + +You may add steps to alternate paths and test them. Pipedream will signal that the results may not be reliable if the branch is not in the execution path. + +![build_and_test_alternate_paths.gif](/images/control-flow/switch/build_and_test_alternate_paths.gif) + +### Validate alternate paths + +Generate or select alternate events to trigger and validate alternate paths. + +![test_different_trigger_events.gif](/images/control-flow/switch/test_different_trigger_events.gif) + +### Deploy and test the live workflow + +Deploy the workflow and trigger it to inspect the executions. + +![Inspect.gif](/images/control-flow/switch/deploy_and_test.gif) + + diff --git a/docs-v2/pages/workflows/building-workflows/errors.mdx b/docs-v2/pages/workflows/building-workflows/errors.mdx new file mode 100644 index 0000000000000..ee0b9e9b902cb --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/errors.mdx @@ -0,0 +1,147 @@ +import VideoPlayer from "@/components/VideoPlayer"; + +# Handling errors + +Two types of errors are raised in Pipedream workflows: + +- **Workflow errors** — Errors in the workflow execution environment, like [Timeouts](/troubleshooting/#timeout) or [Out of Memory](/troubleshooting/#out-of-memory) errors. Often, you can change your workflow's configuration to fix them. You can find more details on these errors [in our troubleshooting guide](/troubleshooting). +- **Step errors** — Errors raised by individual [code](/workflows/building-workflows/code/) or [action](/workflows/building-workflows/actions/) steps. These can be syntax errors, errors raised by the Node or Python runtime, errors with input data, and more. Pipedream will surface details about the error and the stack trace, and you can even [debug these errors with AI](#debug-with-ai). + +Both types of errors will trigger [error notifications](#error-notifications), can be handled by [custom error handlers](#handle-errors-with-custom-logic), and will show up in [the REST API](#poll-the-rest-api-for-workflow-errors). + +## Auto-retry + +You can [automatically retry events](/workflows/building-workflows/settings/#auto-retry-errors) that yield an error. This can help for transient errors that occur when making API requests, like when a service is down or your request times out. + +## Apply conditional logic + +Many errors result from the data you're processing. You might only receive certain data from a webhook under certain conditions, or have malformed data in the payload that causes an error. + +You can apply conditional logic in code, or using the [If / Else operator](/workflows/building-workflows/control-flow/ifelse/), handling these conditions accordingly. + +## Error notifications + +By default, [Pipedream sends an email](#default-system-emails) when a workflow throws an unhandled error. But you can + +- Send error notifications to Slack +- Handle errors from one workflow in a specific way +- Fetch errors asynchronously using the REST API, instead of handling the event in real-time + +These docs describe the default error behavior, and how to handle custom use cases like these. + +Before you jump into the examples below, remember that all Pipedream workflows are just code. You can always use the built-in error handling logic native to your programming language, for example: using JavaScript's [`try / catch` statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch). In the future, Pipedream plans to support this kind of error-handling for built-in actions, as well. + +### Default system emails + +Any time your workflow throws an unhandled error, you'll receive an email like this: + +![Example error email](https://res.cloudinary.com/pipedreamin/image/upload/v1656630943/docs/Screen_Shot_2022-06-29_at_6.20.42_PM_kmsjbr.png) + +This email includes a link to the error so you can see the data and logs associated with the run. When you inspect the data in the Pipedream UI, you'll see details on the error below the step that threw the error, e.g. the full stack trace. + +#### Duplicate errors do not trigger duplicate emails + +High-volume errors can lead to lots of notifications, so Pipedream only sends at most one email, per error, per workflow, per 24 hour period. + +For example, if your workflow throws a `TypeError`, we'll send you an email, but if it continues to throw that same `TypeError`, we won't email you about the duplicate errors until the next day. If a different workflow throws a `TypeError`, you **will** receive an email about that. + +### Test mode vs. live mode + +When you're editing and testing your workflow, any unhandled errors will **not** raise errors as emails, nor are they forwarded to [error listeners](#handle-errors-with-custom-logic). Error notifications are only sent when a deployed workflow encounters an error on a live event. + +## Debug with AI + +You can debug errors in [code](/workflows/building-workflows/code/) or [action](/workflows/building-workflows/actions/) steps with AI by pressing the **Debug with AI** button at the bottom of any error. + +### Data we send with errors + +When you debug an error with AI, Pipedream sends the following information to OpenAI: + +- The error code, message, and stack trace +- The step's code +- The input added to the step configuration. This **does not** contain the event data that triggered your workflow, just the static input entered in the step configuration, like the URL of an HTTP request, or the names of [step exports](/workflows/#step-exports). + +We explicitly **do not** send the event data that triggered the error, or any other information about your account or workflow. + +## Handle errors with custom logic + +Pipedream exposes a global stream of all errors, raised from all workflows. You can subscribe to this stream, triggering a workflow on every event. This lets you handle errors in a custom way. Instead of sending all errors to email, you can send them to Slack, Discord, AWS, or any other service, and handle them in any custom way. + +To do this: + +1. Create a new workflow. +2. Add a new trigger. Search for the `Pipedream` app. +3. Select the custom source `Workspace $error events`. +4. Generate an error in a live version of any workflow (errors raised while you're testing your workflow [do not send errors to the `$errors` stream](#test-mode-vs-live-mode)). You should see this error trigger the workflow in step #1. From there, you can build any logic you want to handle errors across workflows. + +### Duplicate errors _do_ trigger duplicate error events on custom workflows + +Unlike [the default system emails](#duplicate-errors-do-not-trigger-duplicate-emails), duplicate errors are sent to any workflow listeners. + +## Poll the REST API for workflow errors + +Pipedream provides a REST API endpoint to [list the most recent 100 workflow errors](/rest-api/#get-workflow-errors) for any given workflow. For example, to list the errors from workflow `p_abc123`, run: + +```bash +curl 'https://api.pipedream.com/v1/workflows/p_abc123/$errors/event_summaries?expand=event' \ + -H 'Authorization: Bearer ' +``` + +By including the `expand=event` query string param, Pipedream will return the full error data, along with the original event that triggered your workflow: + +```json +{ + "page_info": { + "total_count": 100, + "start_cursor": "1606370816223-0", + "end_cursor": "1606370816223-0", + "count": 1 + }, + "data": [ + { + "id": "1606370816223-0", + "indexed_at_ms": 1606370816223, + "event": { + "original_event": { + "name": "Luke", + "title": "Jedi" + }, + "original_context": { + "id": "2po8fyMMKF4SZFrOThm0Ex4zv6M", + "ts": "2024-12-05T17:52:54.117Z", + "pipeline_id": null, + "workflow_id": "p_abc1234", + "deployment_id": "d_abc1234", + "source_type": "COMPONENT", + "verified": false, + "hops": null, + "test": false, + "replay": false, + "owner_id": "o_abc1234", + "platform_version": "3.50.4", + "workflow_name": "error", + "resume": null, + "emitter_id": "hi_abc1234", + "external_user_id": null, + "external_user_environment": null, + "trace_id": "2po8fwtzKHVr0VZpJc3EUmdTAms", + "project_id": "proj_abc1234" + }, + "error": { + "code": "InternalFailure", + "cellId": "c_abc123", + "ts": "2020-11-26T06:06:56.077Z", + "stack": " at Request.extractError ..." + } + }, + "metadata": { + "emitter_id": "p_abc123", + "emit_id": "1kodKnAdWGeJyhqYbqyW6lEXVAo", + "name": "$errors" + } + } + ] +} +``` + +By listing these errors, you may be able to replay them against your workflow programmatically. For example, if your workflow is triggered by HTTP requests, you can send an HTTP request with the data found in `event.original_event` (see the example above) for every event that errored. diff --git a/docs-v2/pages/workflows/building-workflows/http.mdx b/docs-v2/pages/workflows/building-workflows/http.mdx new file mode 100644 index 0000000000000..33b0dc19bc008 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/http.mdx @@ -0,0 +1,235 @@ +import Callout from '@/components/Callout' + +# HTTP + +Integrate and automate any API using Pipedream workflows. Use app specific pre-built actions, or an HTTP request action for a no code interface. If you need more control over error handling, then use your same connected accounts with code in Node.js or Python. + +## Pre-built actions + +Pre-built actions are the most convenient option for integrating your workflow with an API. Pre-built actions can use your connected accounts to perform API requests, and are configured through props. + +Pre-built actions are the fastest way to get started building workflows, but they may not fit your use case if a prop is missing or is handling data in a way that doesn't fit your needs. + +For example, to send a message using Slack just search for Slack and use the **Send Message to a Public Channel** action: + +![Finding the Slack - Send Message to a Public Channel action](/images/http/selecting-pre-built-actions.png) + +Then connect your Slack account, select a channel and write your message: + +![Configuring a Slack - Send Message to a Public Channel action](/images/http/configure-slack-pre-built-action-props.png) + +Now with a few clicks and some text you've integrated Slack into a Pipedream workflow. + + +Pre-built actions are open source + +All pre-built actions are published from the [Pipedream Component Registry](/workflows/contributing/), so you can read and modify their source code. You can even publish your own from [Node.js code steps privately to your own workspace](/workflows/building-workflows/code/nodejs/sharing-code/). + + +## HTTP Request Action + +The HTTP request action is the next most convenient option. Use a Postman-like interface to configure an HTTP request - including the headers, body, and even connecting an account. + +![Finding the HTTP request builder action](/images/http/selecting-the-slack-api-http-request-builder.png) + +Selecting this action will display an HTTP request builder, with the Slack app slot to connect your account with. + +![Connecting a Slack account to the http request action builder](/images/http/connect-slack-account-to-http-request-action.png) + +Once you connect your account to the step, it will automatically configure the authorization headers to match. + +For example, the Slack API expects a Bearer token with the `Authorization` header. So Pipedream automatically configures this HTTP request to pass your token to that specific header: + +![Showing how Pipedream passes the managed auth token to the header](/images/http/viewing-authorization-configuration.png) + +The configuration of the request and management of your token is automatically handled for you. So you can simply modify the request to match the API endpoint you're seeking to interact with. + +### Adding apps to an HTTP request builder action + +You can also attach apps to the *Send any HTTP Request* action from the action selection menu. After adding a new step to your workflow, select the *Send any HTTP Request* action: + +![Selecting the HTTP request builder action from the step selector menu](/images/http/selecting-the-http-request-builder-action.png) + +Then within the HTTP request builder, click the *Autorization Type* dropdown to select a method, and click **Select an app**: + +![Selecting a connected account for a HTTP request step](https://res.cloudinary.com/pipedreamin/image/upload/v1684259987/docs/docs/event%20histories/CleanShot_2023-05-16_at_13.50.53_fv3caw.png) + +Then you can choose **Slack** as the service to connect the HTTP request with: + +![Connecting the HTTP Request step to Slack](https://res.cloudinary.com/pipedreamin/image/upload/v1684420551/docs/docs/http/CleanShot_2023-05-18_at_10.35.41_dxyipl.png) + +The HTTP request action will automatically be configured with the Slack connection, you'll just need to select your account to finish the authentication. + +Then it's simply updating the URL to send a message which is [`https://slack.com/api/chat.postMessage`](https://api.slack.com/methods/chat.postMessage): + +![Defining the Slack API endpoint URL and connecting an account](https://res.cloudinary.com/pipedreamin/image/upload/v1684263130/docs/docs/CleanShot_2023-05-16_at_14.35.05_mame6o.png) + +Finally modify the body of the request to specify the `channel` and `message` for the request: + +![Configuring the Slack body for sending the request](https://res.cloudinary.com/pipedreamin/image/upload/v1684263128/docs/docs/CleanShot_2023-05-16_at_14.34.56_kpk2vp.png) + +HTTP Request actions can be used to quickly scaffold API requests, but are not as flexible as code for a few reasons: + +* Conditionally sending requests - The HTTP request action will always request, to send requests conditionally you'll need to use code. +* Workflow execution halts - if an HTTP request fails, the entire workflow cancels +* Automatically retrying - `$.flow.retry` isn't available in the HTTP Request action to retry automatically if the request fails +* Error handling - It's not possible to set up a secondary action if an HTTP request fails. + +## HTTP Requests in code + +When you need more control, use code. You can use your connected accounts with Node.js or Python code steps. + +This gives you the flexibility to catch errors, use retries, or send multiple API requests in a single step. + +First, connect your account to the code step: + +* [Connecting any account to a Node.js step](/workflows/building-workflows/code/nodejs/auth/#accessing-connected-account-data-with-thisappnameauth) +* [Connecting any account to a Python step](/workflows/building-workflows/code/python/auth/) + +### Conditionally sending an API Request + +You may only want to send a Slack message on a certain condition, in this example we'll only send a Slack message if the HTTP request triggering the workflow passes a special variable: `steps.trigger.event.body.send_message` + +```javascript +import { axios } from "@pipedream/platform" + +export default defineComponent({ + props: { + slack: { + type: "app", + app: "slack", + } + }, + async run({steps, $}) { + // only send the Slack message if the HTTP request has a `send_message` property in the body + if(steps.trigger.body.send_message) { + return await axios($, { + headers: { + Authorization: `Bearer ${this.slack.$auth.oauth_access_token}`, + }, + + url: `https://slack.com/api/chat.postMessage`, + + method: 'post', + data: { + channel: 'C123456', + text: 'Hi from a Pipedream Node.js code step' + } + }) + } + }, +}) + +``` + +### Error Handling + +The other advantage of using code is handling error messages using `try...catch` blocks. In this example, we'll only send a Slack message if another API request fails: + +```javascript +import { axios } from "@pipedream/platform" + +export default defineComponent({ + props: { + openai: { + type: "app", + app: "openai" + }, + slack: { + type: "app", + app: "slack", + } + }, + async run({steps, $}) { + try { + return await axios($, { + url: `https://api.openai.com/v1/completions`, + method: 'post', + headers: { + Authorization: `Bearer ${this.openai.$auth.api_key}`, + }, + data: { + "model": "text-davinci-003", + "prompt": "Say this is a test", + "max_tokens": 7, + "temperature": 0 + } + }) + } catch(error) { + return await axios($, { + url: `https://slack.com/api/chat.postMessage`, + method: 'post', + headers: { + Authorization: `Bearer ${this.slack.$auth.oauth_access_token}`, + }, + data: { + channel: 'C123456', + text: `OpenAI returned an error: ${error}` + } + }) + } + }, +}) +``` + + +Subscribing to all errors + +[You can use a subscription](/rest-api/#subscriptions) to subscribe a workflow to all errors through the `$errors` channel, instead of handling each error individually. + + +### Automatically retrying an HTTP request + +You can leverage `$.flow.rerun` within a `try...catch` block in order to retry a failed API request. + +[See the example in the `$.flow.rerun` docs](/workflows/building-workflows/code/nodejs/rerun/#pause-resume-and-rerun-a-workflow) for Node.js. + +## Platform axios + +### Why `@pipedream/platform` axios? + +`axios` is an HTTP client for Node.js ([see these docs](/workflows/building-workflows/code/nodejs/http-requests/) for usage examples). + +`axios` has a simple programming API and works well for most use cases. But its default error handling behavior isn't easy to use. When you make an HTTP request and the server responds with an error code in the 4XX or 5XX range of status codes, `axios` returns this stack trace: + +![default axios stack trace](/images/http/default-axios-stack.png) + +This only communicates the error code, and not any other information (like the body or headers) returned from the server. + +Pipedream publishes an `axios` wrapper as a part of [the `@pipedream/platform` package](https://github.com/PipedreamHQ/pipedream/tree/master/platform). This presents the same programming API as `axios`, but implements two helpful features: + +1. When the HTTP request succeeds (response code < `400`), it returns only the `data` property of the response object — the HTTP response body. This is typically what users want to see when they make an HTTP request: + +![pipedream axios success case](/images/http/pipedream-axios-success.png) + +2. When the HTTP request _fails_ (response code >= `400`), it displays a detailed error message in the Pipedream UI (the HTTP response body), and returns the whole `axios` response object so users can review details on the HTTP request and response: + +![pipedream axios error case](/images/http/pipedream-axios-stack.png) + +### Using `@pipedream/platform` axios in component actions + +To use `@pipedream/platform` axios in component actions, import it: + +```javascript +import { axios } from "@pipedream/platform" +``` + +`@pipedream/platform` axios uses methods [provided by the `$` object](/workflows/contributing/components/api/#actions), so you'll need to pass that as the first argument to `axios` when making HTTP requests, and pass the [standard `axios` request config](https://github.com/axios/axios#request-config) as the second argument. + +Here's an example action: + +```javascript +import { axios } from "@pipedream/platform" + +export default { + key: "my-test-component", + name: "My Test component", + version: "0.0.1", + type: "action", + async run({ $ }) { + return await axios($, { + url: "https://httpstat.us/200", + }) + } +} +``` diff --git a/docs-v2/pages/workflows/building-workflows/inspect.mdx b/docs-v2/pages/workflows/building-workflows/inspect.mdx new file mode 100644 index 0000000000000..9fc08103d7eda --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/inspect.mdx @@ -0,0 +1,39 @@ +import VideoPlayer from "@/components/VideoPlayer"; + +# Inspect Events + + + +[The inspector](#the-inspector) lists the events you send to a [workflow](/workflows/building-workflows/). Once you choose a [trigger](/workflows/building-workflows/triggers/) and send events to it, you'll see those events in the inspector, to the left of your workflow. + +Clicking on an event from the list lets you [review the incoming event data and workflow execution logs](/workflows/building-workflows/triggers/#examining-event-data) for that event. + +You can use the inspector to replay events, delete them, and more. + +## The inspector + +The inspector lists your workflow's events: + +![Event inspector in a deployed workflow](https://res.cloudinary.com/pipedreamin/image/upload/v1648759565/docs/components/CleanShot_2022-03-31_at_16.45.52_vwwhaj.png) + +## Event Duration + +The duration shown when clicking an individual event notes the time it took to run your code, in addition to the time it took Pipedream to handle the execution of that code and manage its output. Specifically, + +**Duration = Time Your Code Ran + Pipedream Execution Time** + +## Replaying and deleting events + +Hover over an event, and you'll see two buttons: + +![Replaying and deleting events](https://res.cloudinary.com/pipedreamin/image/upload/v1648759778/docs/components/CleanShot_2022-03-31_at_16.49.24_ska5vo.gif) + +The blue button with the arrow **replays** the event against the newest version of your workflow. The red button with the X **deletes** the event. + +## Messages + +Any `console.log()` statements or other output of code steps is attached to the associated code cells. But [`$.flow.exit()`](/workflows/building-workflows/code/nodejs/#ending-a-workflow-early) or [errors](/workflows/building-workflows/code/nodejs/#errors) end a workflow's execution, so their details appear in the inspector. + +## Limits + +Pipedream retains a limited history of events for a given workflow. See the [limits docs](/workflows/limits/#event-history) for more information. diff --git a/docs-v2/pages/workflows/building-workflows/settings/_meta.tsx b/docs-v2/pages/workflows/building-workflows/settings/_meta.tsx new file mode 100644 index 0000000000000..0c9157a1cadfe --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/settings/_meta.tsx @@ -0,0 +1,4 @@ +export default { + "index": "Overview", + "concurrency-and-throttling": "Concurrency and Throttling", +} as const diff --git a/docs-v2/pages/workflows/concurrency-and-throttling.mdx b/docs-v2/pages/workflows/building-workflows/settings/concurrency-and-throttling.mdx similarity index 95% rename from docs-v2/pages/workflows/concurrency-and-throttling.mdx rename to docs-v2/pages/workflows/building-workflows/settings/concurrency-and-throttling.mdx index 16a488bcc5cb4..a42512de18866 100644 --- a/docs-v2/pages/workflows/concurrency-and-throttling.mdx +++ b/docs-v2/pages/workflows/building-workflows/settings/concurrency-and-throttling.mdx @@ -1,3 +1,5 @@ +import Callout from "@/components/Callout"; + # Concurrency and Throttling Pipedream makes it easy to manage the concurrency and rate at which events trigger your workflow code using execution controls. @@ -24,6 +26,10 @@ While the first example resulted in only two rows of data in Google Sheets, this If your workflow integrates with any APIs, then you may need to limit the rate at which your workflow executes to avoid hitting rate limits from your API provider. Since event-driven workflows are stateless, you can't manage the rate of execution from within your workflow code. Pipedream's execution controls solve this problem by allowing you to control the maximum number of times a workflow may be invoked over a specific period of time (e.g., up to 1 event every second). + +Pipedream controls the frequency of workflow invocations over a specified time interval using fixed window throttling. For example, if the execution rate limit is set at 1 execution every 5 seconds, this means that your workflow will be invoked no more than once within a fixed 5-second time box. This doesn’t mean that executions will occur 5 seconds apart. + + ## Usage Events emitted from a source to a workflow are placed in a queue, and Pipedream triggers your workflow with events from the queue based on your concurrency and throttling settings. These settings may be customized per workflow (so the same events may be processed at different rates by different workflows). diff --git a/docs-v2/pages/workflows/building-workflows/settings/index.mdx b/docs-v2/pages/workflows/building-workflows/settings/index.mdx new file mode 100644 index 0000000000000..48cd68e03788b --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/settings/index.mdx @@ -0,0 +1,216 @@ +import Callout from "@/components/Callout"; + +# Settings + +You can control workflow-specific settings in your workflow's **Settings**: + +1. Visit your workflow +2. Click on Workflow Settings on the top left: + +
+Click on Workflow Settings on the top left +
+ + +You can also open the workflow settings using `CMD` + `,` on Mac, or `Ctrl` + `,` on Windows. + + +## Enable Workflow + +If you'd like to pause your workflow from executing completely, you can disable it or reenable it here. + +## Error Handling + +By default, you'll receive notifications when your workflow throws an unhandled error. See the [error docs](/workflows/building-workflows/errors/) for more detail on these notifications. + +You can disable these notifications for your workflow by disabling the **Notify me on errors** toggle: + +Notify me on errors toggle + +
+Notify me on errors toggle +
+ +## Auto-retry Errors + + +**Out of Memory and Timeout Errors** + +Pipedream will not automatically retry if an execution fails due to an Out of Memory (OOM) error or a timeout. If you encounter these errors frequently, consider increasing the configuration settings for your workflow. + + +Customers on the [Advanced Plan](https://pipedream.com/pricing) can automatically retry workflows on errors. If any step in your workflow throws an error, Pipedream will retry the workflow from that failed step, re-rerunning the step up to 8 times over a 10 hour span with an [exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) strategy. + +On error, the step will export a `$summary` property that tells you how many times the step has been retried, and an `$attempt` object with the following properties: + +1. `error` — All the details of the error the step threw — the error, the stack, etc. +2. `cancel_url` — You can call this URL to cancel the retry +3. `rerun_url` — You can call this URL to proceed with the execution immediately +4. `resume_ts` — An ISO 8601 timestamp that tells you the timestamp of the next retry + +
+Step exports for failed auto-retry +
+ +If the step execution succeeds during any retry, the execution will proceed to the next step of the workflow. + +If the step fails on all 8 retries and throws a final error, you'll receive [an error notification](/workflows/building-workflows/errors/) through your standard notification channel. + +### Send error notifications on the first error + +By default, if a step fails on all 8 retries, and throws a final error, you'll receive [an error notification](/workflows/building-workflows/errors/) through your standard notification channel. But sometimes you need to investigate errors as soon as they happen. If you're connecting to your database, and receive an error that the DB is down, you may want to investigate that immediately. + +On any workflow with auto-retry enabled, you can optionally choose to **Send notification on first error**. This is disabled by default so you don't get emails for transient errors, but you can enable for critical workflows where you want visibility into all errors. + +For custom control over error handling, you can implement error logic in code steps (e.g. `try` / `catch` statements in Node.js code), or [create your own custom error workflow](/workflows/building-workflows/errors/#handle-errors-with-custom-logic). + +## Data Retention Controls + +By default, Pipedream stores exports, logs, and other data tied to workflow executions. You can view these logs in two places: + +1. [The workflow inspector](/workflows/building-workflows/inspect/#the-inspector) +2. [Event History](/workflows/event-history/) + +But if you're processing sensitive data, you may not want to store those logs. You can **Disable data retention** in your workflow settings to disable **all** logging. Since Pipedream stores no workflow logs with this setting enabled, you'll see no logs in the inspector or event history UI. + +![Disable data retention setting](/images/settings/disable-data-retention-settings.png) + +Refer to our [pricing page](https://pipedream.com/pricing) to understand the latest limits based on your plan. + +### Constraints + +- **Data Retention Controls do not apply to sources**: Even with data retention disabled on your workflow, Pipedream will still log inbound events for the source. +- **No events will be shown in the UI**: When data retention is disabled for your workflow, the Pipedream UI will not show any new events in the inspector or Event History for that workflow. + + +**Avoid surfacing events in the builder** + +Even with data retention disabled on your workflow, the builder will still surface inbound events when in build mode. To avoid surfacing potentially sensitive data here as well, refer to [these docs](/workflows/building-workflows/triggers/#pipedream-specific-request-parameters). + + + +## Execution Controls + +### Execution Timeout Limit + +Workflows have a default [execution limit](/workflows/limits/#time-per-execution), which defines the time the workflow can run for a single execution until it's timed out. + +If your workflow times out, and needs to run for longer than the [default limit](/workflows/limits/#time-per-execution), you can change that limit here. + +### Memory + +By default, workflows run with {process.env.MEMORY_LIMIT} of memory. If you're processing a lot of data in memory, you might need to raise that limit. Here, you can increase the memory of your workflow up to {process.env.MEMORY_ABSOLUTE_LIMIT}. + +Increasing your workflow's memory gives you a proportional increase in CPU, so increasing your workflow's memory can reduce its overall runtime and make it more performant. + + +**How can my workflow run faster?** + +See [our guide on running workflows faster](/troubleshooting/faq/#how-can-my-workflow-run-faster). + + +**Pipedream charges credits proportional to your memory configuration**. When you modify your memory settings, Pipedream will show you the number of credits you'll be charged per execution. [Read more here](/pricing/faq/#how-does-workflow-memory-affect-credits). + +### Concurrency and Throttling + +[Manage the concurrency and rate](/workflows/building-workflows/settings/concurrency-and-throttling/) at which events from a source trigger your workflow code. + +## Eliminate cold starts + +A **cold start** refers to the delay between the invocation of workflow and the execution of the workflow code. Cold starts happen when Pipedream spins up a new [execution environment](/privacy-and-security/#execution-environment) to handle incoming events. + +Specifically, cold starts occur on the first request to your workflow after a period of inactivity (roughly 5 minutes), or if your initial worker is already busy and a new worker needs to be initialized. In these cases, Pipedream creates a new execution environment to process your event. **Initializing this environment takes a few seconds, which delays the execution of this first event**. + +You can reduce cold starts by configuring a number of dedicated **workers**: + +1. Visit your workflow's **Settings** +2. Under **Execution Controls**, select the toggle to **Eliminate cold starts** +3. Configure [the appropriate number of workers](#how-many-workers-should-i-configure) for your use case + +
+Warm workers configuration +
+ +When you configure workers for a specific workflow, Pipedream initializes dedicated workers — virtual machines that run Pipedream's [execution environment](/privacy-and-security/#execution-environment). [It can take a few minutes](#how-long-does-it-take-to-spin-up-a-dedicated-worker) for new dedicated workers to deploy. Once deployed, these workers are available at all times to respond to workflow executions, with no cold starts. + + +You may need to configure [multiple dedicated workers](#how-many-workers-should-i-configure) to handle multiple, concurrent requests. + +Pipedream also performs some initialization operations on new workflow runs, so you may still observe a small startup time (typically around 50ms per workflow step) on dedicated workers. + + + +### When should I configure dedicated workers? + +You should configure dedicated workers when you need to process requests as soon as possible, with no latency. + +For example, you may build an HTTP-triggered workflow that returns a synchronous HTTP response to a user, without delay. Or you may be building a Slack bot and need to respond to Slack's webhook within a few seconds. Since these workflows need to respond quickly, they're good cases to use dedicated workers. + +### How many workers should I configure? + +Incoming requests are handled by a single worker, one at a time. If you only receive one request a minute, and the workflow finishes execution in a few seconds, you may only need one worker. + +But you might have a higher-volume app that receives two concurrent requests. In that case, Pipedream spins up **two** workers to handle each request. + +For many user-facing (even internal) applications, the number of requests over time can be modeled with a [Poisson distrubution](https://en.wikipedia.org/wiki/Poisson_distribution). You can use that distribution to estimate the number of workers you need at an average time, or set it higher if you want to ensure a specific percentage of requests hit a dedicated worker. You can also save a record of all workflow runs to your own database, with the timestamp they ran ([see `steps.trigger.context.ts`](/workflows/building-workflows/triggers/#stepstriggercontext)), and look at your own pattern of requests, to compute the optimal number of workers. + +### Do compute budgets apply to dedicated workers? + +No, compute budgets do not apply to dedicated workers, they only apply to credits incurred by compute from running workflows, sources, etc. + +### How long does it take to spin up a dedicated worker? + +It can take 5-10 minutes for Pipedream to fully configure a new dedicated worker. Before that time, you may still observe cold starts with new incoming requests. + +### Pricing for dedicated workers + +You're charged {process.env.WARM_WORKERS_CREDITS_PER_INTERVAL} credits for every {process.env.WARM_WORKERS_INTERVAL} a dedicated worker is live, per worker, per {process.env.MEMORY_LIMIT} memory. You can view the credits used by dedicated workers [in your billing settings](https://pipedream.com/settings/billing): + +
+Warm workers credit usage in billing +
+ +For example, if you run a single dedicated worker for 24 hours, that would cost 720 credits: + +``` +5 credits per 10 min +* 6 10-min periods per hour +* 24 hours += 720 credits +``` + +{process.env.WARM_WORKERS_INTERVAL} is the _minimum_ interval that Pipedream charges for usage. If you have a dedicated worker live for 1 minute, Pipedream will still charge {process.env.WARM_WORKERS_CREDITS_PER_INTERVAL} credits. + +Additionally, any change to dedicated worker configuration, (including worklow deploys) will result in an extra {process.env.WARM_WORKERS_CREDITS_PER_INTERVAL} credits of usage. + +
+Dedicated worker overlap +
+ +### Limits + +Each attachment is limited to `25MB` in size. The total size of all attachments within a single workflow cannot exceed `200MB`. diff --git a/docs-v2/pages/workflows/building-workflows/sharing.mdx b/docs-v2/pages/workflows/building-workflows/sharing.mdx new file mode 100644 index 0000000000000..93ad5aded0514 --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/sharing.mdx @@ -0,0 +1,144 @@ +import { Cards, Steps } from 'nextra/components' +import Callout from '@/components/Callout' +import RocketShip from '@/components/RocketShip' +import Image from 'next/image' + +# Sharing Workflows + +Pipedream provides a few ways to share your workflow: +1. [Share a workflow as a link with anyone](#creating-a-share-link-for-a-workflow) +2. [Publish as a public template](#publish-to-the-templates-gallery) + +You can share your workflows as templates with other Pipedream accounts with a unique shareable link. + +Creating a share link for your workflow will allow anyone with the link to create a template version of your workflow in their own Pipedream account. This will allow others to use your workflow with their own Pipedream account and also their own connected accounts. + +[Here's an example of a workflow](https://pipedream.com/new?h=tch_OYWfjz) that sends you a daily SMS message with today's schedule: + +![Daily Schedule SMS Reminder workflow](https://res.cloudinary.com/pipedreamin/image/upload/v1685116771/docs/docs/share%20workflows/New_Project_6_n63kju.png) + +Click **Deploy to Pipedream** below to create a copy of this workflow in your own Pipedream account. + + + } title="Deploy to Pipedream" href="https://pipedream.com/new?h=tch_OYWfjz" /> + + +The copied workflow includes the same trigger, steps, and connected account configuration, but it has a separate event history and versioning from the original. + +## Creating a share link for a workflow + +To share a workflow, open the workflow in your browser. Then in the top right menu, select **Create Share Link**. + +Click Create Share Link + +Now you can define which prop values should be included in this shareable link. + +### Including props + +Optionally, you can include the actual individual prop configurations as well. This helps speed up workflow development if the workflow relies on specific prop values to function properly. + +You can choose to **Include all** prop values if you'd like, or only select specific props. + +For the daily schedule reminder workflow, we included the props for filtering Google Calendar events, but we did _not_ include the SMS number to send the message to. This is because the end user of this workflow will use their own phone number instead: + +
+ +Sharing a workflow that will send a daily SMS message of your Google Calendar schedule for today + + +**Your connected accounts are not shared.** When other users configure your workflow from the shared link they'll be prompted to connect their own accounts. + + +### Versioning + +- When you create a shared link for your workflow, that link is frozen to the version of your workflow at the time the link was created +- If you make changes to the original workflow, those changes will _not_ be included in the shared workflow link, nor in any workflows copied from the original shared link +- To push out new changes to a workflow, you'll need to generate a new share link + + +**Share links persist**. You can create multiple share links for the same workflow with different prop configurations, or even different steps. Share links do not expire, nor do newly created link overwrite existing ones. + + +## Publish to the templates gallery + +We're excited to highlight the various use cases our community and partners have enabled using Pipedream. To do this, we've created a [Templates Gallery](https://pipedream.com/templates/) with a growing number of high-quality templates to help you discover your next workflow. + +The process to publish your own workflow to the Templates Gallery is similar to [creating a share link](#creating-a-share-link-for-a-workflow): + + + +### Open the workflow you'd like to share + +To get started, open the workflow in your browser. Then in the top right menu, select **Publish as a template**. + +
+ +Click Publish as a template + +### Configure which prop values to include in the template + +Follow the same steps as above to select the prop input values you'd like to include in the template, then click **Continue** + +### Enter the required information for the template title, description, and tags + +On the next screen, you'll be prompted for additional information about your template: + +- **Developer name**: This is probably you — this name will be displayed as the author of the template. +- **Template name**: The name of the template. +- **Brief description**: A short description of the template, which will be displayed on the listing page (maximum 256 characters). [See here](https://pipedream.com/templates) for examples. +- **Longer description**: Use Markdown to create a more in-depth description. We recommend including distinct sections as H2s, for you to provide an **Overview**, **Steps**, and **Use Cases**. This will be displayed on the details page for the template. Here's an example: [Notion Voice Notes (Google Drive)](https://pipedream.com/templates/notion-voice-notes-google-drive-version-mpt_2WRFKY). +- **Use cases**: Select one or more categories that align with the use cases for your template to help users discover it. +- **Affiliate token**: If you're a [Pipedream affiliate](https://pipedream.com/affiliates), you can enter your unique token here to earn commissions on any users who sign up for Pipedream after using your template. + +### Click **Submit** + +- Once you've filled out the required information, click **Submit**. +- We'll review your template and will email you once it goes live! + +
+ +## FAQ + +### If changes are made to the original workflow, will copied versions of the workflow from the shared link also change? + +No, workflows copied from a shared link will have separate version histories from the original workflow. You can modify your original workflow and it will not affect copied workflows. + +### Will my connected accounts be shared with the workflow? + +No, your connected accounts are not shared. Instead, copied workflows display a slot in actions that require a connected account, so the user of the copied workflow can provide their own accounts instead. + +For example, if one of your steps relies on a Slack connected account to send a message, then the copied workflow will display the need to connect a Slack account. + +### I haven't made any changes to my workflow, but if I generate another shared link will it override my original link? + +No, if the steps and prop configuration of the workflow is exactly the same, then the shared link URL will also be exactly the same. + +The shared workflow link is determined by the configuration of your workflow, it's not a randomly generated ID. + +### Will generating new shared links disable or delete old links? + +No, each link you generate will be available even if you create new versions based on changes or included props from the original workflow. + +### What plan is this feature available on? + +Sharing workflows via link is available on all plans, including the Free tier plans. + +### Do users of my workflow need to have a subscription? + +To copy a workflow, a subscription is not required. However, the copied workflow is subject to the current workspace's plan limits. + +For example, if a workflow requires more connected accounts than what's available on the [Free tier](/pricing/#free-tier), then users of your workflow will require a plan to run the workflow properly. + +### Will copies of my workflow use my credits? + +No. Copied workflows have entirely separate versioning, connected accounts, and billing. Sharing workflow copies is free, and the user of the copy usage is responsible for credit usage. Your original workflow is entirely separate from the copy. + +### How can I transfer all of my workflows from one account to another? + +It's only possible to share a single workflow at time with a link at this time. + +If you're trying to migrate all resources from one workspace to another [please contact us for help](mailto:support@pipedream.com). + +### Are step notes included when I share a workflow? + +Yes any [step notes](/workflows/#step-notes) you've added to your workflow are included in the copied version. diff --git a/docs-v2/pages/sources.mdx b/docs-v2/pages/workflows/building-workflows/sources.mdx similarity index 81% rename from docs-v2/pages/sources.mdx rename to docs-v2/pages/workflows/building-workflows/sources.mdx index 2454d533324d8..00d29d9af8fd7 100644 --- a/docs-v2/pages/sources.mdx +++ b/docs-v2/pages/workflows/building-workflows/sources.mdx @@ -5,14 +5,14 @@ import VideoPlayer from '@/components/VideoPlayer' -Event sources operate primarily as workflow triggers. When you add a new app-based [trigger](/workflows/steps/triggers/) to your workflow, you're creating an event source. +Event sources operate primarily as workflow triggers. When you add a new app-based [trigger](/workflows/building-workflows/triggers/) to your workflow, you're creating an event source. -![New app-based trigger](./images/app-based-trigger.png) +![Adding a new trigger to a workflow](https://res.cloudinary.com/pipedreamin/image/upload/v1710512637/docs/sources/CleanShot_2024-03-15_at_10.23.07_tyjswb.png) Event sources run as their own resources, separate from workflows, for two reasons: - A single event sources can trigger more than one workflow. If you have a data source that you want to run _multiple_ workflows, you can create an event source once and use it as the trigger for each workflow. -- If you need to consume events emitted by event sources in your own application, you don't need to run a workflow: you can use Pipedream's [REST API](/api/rest/) or a [private, real-time SSE stream](/api/sse/) to access the event data directly. +- If you need to consume events emitted by event sources in your own application, you don't need to run a workflow: you can use Pipedream's [REST API](/rest-api/) or a [private, real-time SSE stream](/workflows/data-management/destinations/sse/) to access the event data directly. You can view your event sources at [https://pipedream.com/sources](https://pipedream.com/sources). Here, you'll see the events a specific source has emitted, as well as the logs and configuration for that source. @@ -32,11 +32,11 @@ You can create event sources from the Pipedream UI or CLI. Visit [https://pipedream.com/sources](https://pipedream.com/sources) and click the **New +** button at the top right to create a new event source. You'll see a list of sources tied to apps (like Twitter and Github) and generic interfaces (like HTTP). Select your source, and you'll be asked to connect any necessary accounts (for example, the Twitter source requires you authorize Pipedream access to your Twitter account), and enter the values for any configuration settings tied to the source. -Once you've created a source, you can use it to trigger [Pipedream workflows](/workflows/) or [consume its events](#consuming-events-from-sources) using Pipedream's APIs. +Once you've created a source, you can use it to trigger [Pipedream workflows](/workflows/building-workflows/) or [consume its events](#consuming-events-from-sources) using Pipedream's APIs. ### Creating a source from the CLI -[Download the CLI](/cli/install/) and run: +[Download the CLI](/workflows/cli/install/) and run: ```bash pd deploy @@ -44,19 +44,19 @@ pd deploy This will bring up an interactive menu prompting you to select a source. Once selected, you'll be asked to connect any necessary accounts (for example, the Twitter source requires you authorize Pipedream access to your Twitter account), and enter the values for any configuration settings tied to the source. -Once you've created a source, you can use it to trigger [Pipedream workflows](/workflows/) or [consume its events](#consuming-events-from-sources) using Pipedream's APIs. +Once you've created a source, you can use it to trigger [Pipedream workflows](/workflows/building-workflows/) or [consume its events](#consuming-events-from-sources) using Pipedream's APIs. ## Consuming events from sources You can view the events for a source in the sources UI, under the **Events** section of that source. -You can also trigger a [Pipedream workflow](/workflows/) every time your source emits a new event. This lets you run workflows for every new tweet, every new item in an RSS feed, and more. +You can also trigger a [Pipedream workflow](/workflows/building-workflows/) every time your source emits a new event. This lets you run workflows for every new tweet, every new item in an RSS feed, and more. Finally, you can consume events programmatically, outside the Pipedream platform, in a few different ways: -- In real-time, using the [SSE stream](/api/sse/) linked to your source -- In real-time, via the CLI's [`pd events` command](/api/sse/#subscribe-to-new-events-using-the-pipedream-cli) -- In batch, using the [REST API](/api/rest/) +- In real-time, using the [SSE stream](/workflows/data-management/destinations/sse/) linked to your source +- In real-time, via the CLI's [`pd events` command](/workflows/cli/reference/#pd-events) +- In batch, using the [REST API](/rest-api/) ## Example: HTTP source @@ -70,13 +70,13 @@ When you create an HTTP source: HTTP sources are essentially [request bins](https://requestbin.com) that can be managed via API. -HTTP sources are a good example of how you can turn an event stream into an API: the HTTP requests are the **event stream**, generated from your application, client browsers, webhooks, etc. Then, you can retrieve HTTP requests via Pipedream's [**REST API**](/api/rest/), or stream them directly to other apps using the [SSE interface](/api/sse/). +HTTP sources are a good example of how you can turn an event stream into an API: the HTTP requests are the **event stream**, generated from your application, client browsers, webhooks, etc. Then, you can retrieve HTTP requests via Pipedream's [**REST API**](/rest-api/), or stream them directly to other apps using the [SSE interface](/workflows/data-management/destinations/sse/). [**See the Github quickstart for more information on HTTP event sources**](https://github.com/PipedreamHQ/pipedream/tree/master/components/http#quickstart). ## Example: Cron jobs -You can also use event sources to run any Node code on a schedule, allowing you to poll a service or API for data and emit that data as an event. The emitted events can trigger Pipedream workflows, and can be retrieved using Pipedream's [**REST API**](/api/rest/) or [SSE interface](/api/sse/). +You can also use event sources to run any Node code on a schedule, allowing you to poll a service or API for data and emit that data as an event. The emitted events can trigger Pipedream workflows, and can be retrieved using Pipedream's [**REST API**](/rest-api/) or [SSE interface](/workflows/data-management/destinations/sse/). ## Example: RSS @@ -95,7 +95,7 @@ Test events can be sourced from recent data, generated by real actions, or gener ### Recent past events During creation, most sources will attempt to fetch up to 50 of the most recent events. - + Not all sources will fetch events automatically Not all sources support pre-fetched events currently, but you can still generate test events yourself by performing the action that triggers the source. @@ -105,14 +105,14 @@ Not all sources support pre-fetched events currently, but you can still generate ![Select an event](https://res.cloudinary.com/pipedreamin/image/upload/v1692160381/select-event_jhogxp.gif) ### Generate a real event manually -If there aren't any existing events for the source to fetch, the next best way to produce a test event for your workflow is to generate one in the relevant application. +If there aren't any existing events for the source to fetch, the next best way to produce a test event for your workflow is to generate one in the relevant application. For example, if your workflow is triggered by [New Files in Dropbox](https://pipedream.com/apps/dropbox/triggers/new-file), try adding a file to your Dropbox account. -![Waiting for test event](https://res.cloudinary.com/pipedreamin/image/upload/v1692160381/waiting-for-event_kfdsdv.gif) +![Waiting for test event](https://res.cloudinary.com/pipedreamin/image/upload/v1710512807/docs/sources/CleanShot_2024-03-15_at_10.26.11_hsreft.gif) ### Generate a sample test event -Certain event sources support the ability to generate test events using static sample event data. +Certain event sources support the ability to generate test events using static sample event data. #### Hardcoded sample events For example, the [New Message from Discord source](https://pipedream.com/apps/discord/triggers/new-message) references [this static event](https://github.com/PipedreamHQ/pipedream/blob/master/components/discord/sources/new-message/test-event.mjs), which is a hardcoded file with purely representative data. The goal for this feature is to provide your workflow with the **expected event shape** to expect, but **without the expected values**. @@ -133,7 +133,7 @@ To reset the custom event JSON back to the sample event, click the "Reset" butto ![Resetting the Sample Event](https://res.cloudinary.com/pipedreamin/image/upload/v1692387426/Google_Chrome_-_Untitled_Workflow_-_Pipedream_2023-08-18_at_12.33.18_PM_kimrh9.png) - + Sample data may contain inconsistencies Whenever possible, we recommend using a recent real event, either retrieved automatically or generated manually by an action. @@ -143,14 +143,13 @@ Generating test data manually can result in typos or missing fields that your wo ## Publishing a new event source, or modifying an existing source -Anyone can create an event source or edit an existing one. The code for all event sources is public, and kept in the [`PipedreamHQ/pipedream` repo](https://github.com/PipedreamHQ/pipedream). [Read this quickstart](/components/quickstart/nodejs/sources/) and see the [event source API docs](/components/api/) to learn more about the source development process. +Anyone can create an event source or edit an existing one. The code for all event sources is public, and kept in the [`PipedreamHQ/pipedream` repo](https://github.com/PipedreamHQ/pipedream). [Read this quickstart](/workflows/contributing/components/quickstart/nodejs/sources/) and see the [event source API docs](/workflows/contributing/components/api/) to learn more about the source development process. You can chat about source development with the Pipedream team in the `#contribute` channel of our [public Slack](https://join.slack.com/t/pipedream-users/shared_invite/zt-ernlymsn-UHfPg~Dfp08uGkAd8dpkww), and in the `#dev` topic in the [Pipedream community](https://pipedream.com/community/c/dev/11). ## Limits -Event sources are subject to the [same limits as Pipedream workflows](/limits/), except: +Event sources are subject to the [same limits as Pipedream workflows](/workflows/limits/), except: - Sources have a default timeout of 5 min. - Memory is fixed at 256MB. - diff --git a/docs-v2/pages/workflows/building-workflows/triggers.mdx b/docs-v2/pages/workflows/building-workflows/triggers.mdx new file mode 100644 index 0000000000000..80872ac96332e --- /dev/null +++ b/docs-v2/pages/workflows/building-workflows/triggers.mdx @@ -0,0 +1,853 @@ +import Image from 'next/image' +import Callout from '@/components/Callout' +import { Tabs } from 'nextra/components' +import PipedreamImg from '@/components/PipedreamImg' +import VideoPlayer from "@/components/VideoPlayer"; + +# Triggers + + + +**Triggers** define the type of event that runs your workflow. For example, HTTP triggers expose a URL where you can send any HTTP requests. Pipedream will run your workflow on each request. The Scheduler trigger runs your workflow on a schedule, like a cron job. + +Today, we support the following triggers: + +- [Triggers for apps like Twitter, GitHub, and more](#app-based-triggers) +- [HTTP / Webhook](#http) +- [Schedule](#schedule) +- [Email](#email) +- [RSS](#rss) + +If there's a specific trigger you'd like supported, please [let us know](https://pipedream.com/support/). + +## App-based Triggers + +You can trigger a workflow on events from apps like Twitter, Google Calendar, and more using [event sources](/workflows/building-workflows/triggers/). Event sources run as separate resources from your workflow, which allows you to trigger _multiple_ workflows using the same source. Here, we'll refer to event sources and workflow triggers interchangeably. + +When you create a workflow, click **Add Trigger** to view the available triggers: + +![Add Trigger button on a new workflow](/images/triggers/add-trigger-button.png) + +This will open a new menu to search and choose a trigger for your workflow: + +![App triggers](/images/triggers/select-a-trigger.png) + +Search by **app name** to find triggers associated with your app. For Google Calendar, for example, you can run your workflow every time a new event is **added** to your calendar, each time an event **starts**, **ends**, and more: + +![Google Calendar sources](/images/triggers/gcal-triggers.png) + +Once you select your trigger, you'll be asked to connect any necessary accounts (for example, Google Calendar sources require you authorize Pipedream access to your Google account), and enter the values for any configuration settings. + +Some sources are configured to retrieve an initial set of events when they're created. Others require you to generate events in the app to trigger your workflow. If your source generates an initial set of events, you'll see them appear in the **Select events** dropdown in the **Select Event** step: + +![Choose an event to test your workflow steps against](https://res.cloudinary.com/pipedreamin/image/upload/v1647957381/docs/components/CleanShot_2022-03-22_at_09.55.57_2x_upj35r.png) + +Then you can select a specific test event and manually trigger your workflow with that event data by clicking **Send Test Event**. Now you're ready to build your workflow with the selected test event. + +### What's the difference between an event source and a trigger? + +You'll notice the docs use the terms **event source** and **trigger** interchangeably above. It's useful to clarify the distinction in the context of workflows. + +[Event sources](/workflows/building-workflows/triggers/) run code that collects events from some app or service and emits events as the source produces them. An event source can be used to **trigger** any number of workflows. + +For example, you might create a single source to listen for new Twitter mentions for a keyword, then trigger multiple workflows each time a new tweet is found: one to [send new tweets to Slack](https://pipedream.com/@pravin/twitter-mentions-slack-p_dDCA5e/edit), another to [save those tweets to an Amazon S3 bucket](https://pipedream.com/@dylan/twitter-to-s3-p_KwCZGA/readme), etc. + +**This model allows you to separate the data produced by a service (the event source) from the logic to process those events in different contexts (the workflow)**. + +Moreover, you can access events emitted by sources using Pipedream's [SSE](/workflows/data-management/destinations/sse/) and [REST APIs](/rest-api/). This allows you to access these events in your own app, outside Pipedream's platform. + +### Can I add multiple triggers to a workflow? + +Yes, you can add any number of triggers to a workflow. Click the top right menu in the trigger step and select **Add trigger**. + +![Add multiple triggers to a workflow](/images/triggers/add-multiple-triggers.png) + +### Shape of the `steps.trigger.event` object + +In all workflows, you have access to [event data](/workflows/building-workflows/triggers/#event-format) using the variable `steps.trigger.event`. + +The shape of the event is specific to the source. For example, RSS sources produce events with a `url` and `title` property representing the data provided by new items from a feed. Google Calendar sources produce events with a meeting title, start date, etc. + +## HTTP + +When you select the **HTTP** trigger: + +![HTTP API Trigger](/images/triggers/select-http-trigger.png) + +Pipedream creates a URL endpoint specific to your workflow: + +![HTTP API trigger URL](/images/triggers/http-trigger-url.png) + +You can send any HTTP requests to this endpoint, from anywhere on the web. You can configure the endpoint as the destination URL for a webhook or send HTTP traffic from your application - we'll accept any [valid HTTP request](#valid-requests). + + + +Pipedream also supports [custom domains](/workflows/domains/). This lets you host endpoints on `https://endpoint.yourdomain.com` instead of the default \`{process.env.ENDPOINT_BASE_URL}\` domain. + + +### Accessing HTTP request data + +You can access properties of the HTTP request, like the method, payload, headers, and more, in [the `event` object](/workflows/building-workflows/triggers/#event-format), accessible in any [code](/workflows/building-workflows/code/) or [action](/workflows/contributing/components/#actions) step. + +### Valid Requests + +You can send a request to your endpoint using any valid HTTP method: `GET`, `POST`, `HEAD`, and more. + +We default to generating HTTPS URLs in the UI, but will accept HTTP requests against the same endpoint URL. + +You can send data to any path on this host, with any query string parameters. You can access the full URL in the `event` object if you'd like to write code that interprets requests with different URLs differently. + +You can send data of any [Media Type](https://www.iana.org/assignments/media-types/media-types.xhtml) in the body of your request. + +The primary limit we impose is on the size of the request body: we'll issue a `413 Payload Too Large` status when the body [exceeds our specified limit](#request-entity-too-large). + +### Authorizing HTTP requests + +By default, HTTP triggers are public and require no authorization to invoke. Anyone with the endpoint URL can trigger your workflow. When possible, we recommend adding authorization. + +HTTP triggers support two built-in authorization types in the **Authorization** section of the HTTP trigger configuration: a [static, custom token](#custom-token) and [OAuth](#oauth). + +#### Custom token + +To configure a static, custom token for HTTP auth: + +1. Open the **Configure** section of the HTTP trigger +2. Select **Custom token**. +3. Enter whatever secret you'd like and click **Save and Continue**. + +![Custom token auth](https://res.cloudinary.com/pipedreamin/image/upload/v1729791152/Google_Chrome_-_Untitled_Workflow_-_10-24-2024_10-30_AM_-_Build_-_Pipedream_2024-10-24_at_10.31.01_AM_pkh8dk.png) + +When making HTTP requests, pass the custom token as a `Bearer` token in the `Authorization` header: + +```bash +curl -H 'Authorization: Bearer ' https://myendpoint.m.pipedream.net +``` + +#### OAuth + +You can also authorize requests using [Pipedream OAuth clients](/rest-api/auth/#oauth): + +1. Open the **Configure** section of the HTTP trigger. +2. Select **OAuth**. +3. If you don't have an existing OAuth client, [create one in your workspace's API settings](/rest-api/auth#creating-an-oauth-client). + +![OAuth authorization](https://res.cloudinary.com/pipedreamin/image/upload/v1729791415/Google_Chrome_-_Untitled_Workflow_-_10-24-2024_10-30_AM_-_Build_-_Pipedream_2024-10-24_at_10.36.04_AM_ujx34e.png) + +Next, you'll need to [generate an OAuth access token](/rest-api/auth/#how-to-get-an-access-token). + +When making HTTP requests, pass the OAuth access token as a `Bearer` token in the `Authorization` header: + +```bash +curl -H 'Authorization: Bearer ' https://myendpoint.m.pipedream.net +``` + +You can use the Pipedream SDK to automatically refresh access tokens and invoke workflows, or make HTTP requests directly to the workflow's URL: + + + +```typescript +import { createBackendClient, HTTPAuthType } from "@pipedream/sdk/server"; + +// These secrets should be saved securely and passed to your environment +const pd = createBackendClient({ + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, +}); + +await pd.invokeWorkflow( + "enabc123", // pass the endpoint ID or full URL here + { + method: "POST", + body: { + key: "value", + } + }, + HTTPAuthType.OAuth // Will automatically send the Authorization header with a fresh token +) +``` + + +```javascript +import { createBackendClient } from "@pipedream/sdk/server"; + +// These secrets should be saved securely and passed to your environment +const pd = createBackendClient({ + credentials: { + clientId: "{oauth_client_id}", + clientSecret: "{oauth_client_secret}", + }, +}); + +await pd.invokeWorkflow( + "enabc123", // pass the endpoint ID or full URL here + { + method: "POST", + body: { + key: "value", + } + }, + "oauth" // Will automatically send the Authorization header with a fresh token +) +``` + + +```bash +# First, obtain an OAuth access token +curl -X POST https://api.pipedream.com/v1/oauth/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "client_credentials", + "client_id": "{oauth_client_id}", + "client_secret": "{oauth_client_secret}" + }' + +# The response will include an access_token. Use it in the Authorization header below. + +curl -X POST https://{your-endpoint-url} \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer {access_token}" \ + -d '{ + "message": "Hello, world" + }' +``` + + + + +#### Implement your own authorization logic + +Since you have access to the entire request object, and can issue any HTTP response from a workflow, you can implement custom logic to validate requests. + +For example, you could require JWT tokens and validate those tokens using the [`jsonwebtoken` package](https://www.npmjs.com/package/jsonwebtoken) at the start of your workflow. + +### Custom domains + +To configure endpoints on your own domain, e.g. `endpoint.yourdomain.com` instead of the default `*.m.pipedream.net` domain, see the [custom domains](/workflows/domains/) docs. + +### How Pipedream handles JSON payloads + +When you send JSON in the HTTP payload, or when JSON data is sent in the payload from a webhook provider, **Pipedream converts that JSON to its equivalent JavaScript object**. The trigger data can be referenced using [the `steps` object](#shape-of-the-stepstriggerevent-object). + +In the [Inspector](/workflows/building-workflows/inspect/), we present `steps.trigger.event` cleanly, indenting nested properties, to make the payload easy to read. Since `steps.trigger.event` is a JavaScript object, it's easy to reference and manipulate properties of the payload using dot-notation. + +### How Pipedream handles `multipart/form-data` + +When you send [form data](https://ec.haxx.se/http/http-multipart) to Pipedream using a `Content-Type` of `multipart/form-data`, Pipedream parses the payload and converts it to a JavaScript object with a property per form field. For example, if you send a request with two fields: + +```bash +curl -F 'name=Leia' -F 'title=General' https://myendpoint.m.pipedream.net +``` + +Pipedream will convert that to a JavaScript object, `event.body`, with the following shape: + +```js +{ + name: "Leia", + title: "General", +} +``` + +### How Pipedream handles HTTP headers + +HTTP request headers will be available in the `steps.trigger.event.headers` steps export in your downstream steps. + +Pipedream will automatically lowercase header keys for consistency. + +### Pipedream-specific request parameters + +These params can be set as headers or query string parameters on any request to a Pipedream HTTP endpoint. + +#### `x-pd-nostore` + +Set to `1` to prevent logging any data for this execution. Pipedream will execute all steps of the workflow, but no data will be logged to Pipedream. No event will show up in the inspector or the Event History UI. + +If you need to disable logging for _all_ requests, use the workflow's [Data Retention controls](/workflows/building-workflows/settings/#data-retention-controls), instead. + +#### `x-pd-notrigger` + +Set to `1` to send an event to the workflow for testing. Pipedream will **not** trigger the production version of the workflow, but will display the event in the [list of test events](/workflows/building-workflows/triggers/#selecting-a-test-event) on the HTTP trigger. + +#### Limits + +You can send any content, up to the [HTTP payload size limit](/workflows/limits/#http-request-body-size), as a part of the form request. The content of uploaded images or other binary files does not contribute to this limit — the contents of the file will be uploaded at a Pipedream URL you have access to within your source or workflow. See the section on [Large File Support](#large-file-support) for more detail. + +### Sending large payloads + +_If you're uploading files, like images or videos, you should use the [large file upload interface](#large-file-support), instead_. + +By default, the body of HTTP requests sent to a source or workflow is limited to {process.env.PAYLOAD_SIZE_LIMIT}. **But you can send an HTTP payload of any size to a [workflow](/workflows/building-workflows/) or an [event source](/workflows/building-workflows/triggers/) by including the `pipedream_upload_body=1` query string or an `x-pd-upload-body: 1` HTTP header in your request**. + +```bash +curl -d '{ "name": "Yoda" }' \ + https://endpoint.m.pipedream.net\?pipedream_upload_body\=1 + +curl -d '{ "name": "Yoda" }' \ + -H "x-pd-upload-body: 1" \ + https://endpoint.m.pipedream.net +``` + +In workflows, Pipedream saves the raw payload data in a file whose URL you can reference in the variable `steps.trigger.event.body.raw_body_url`. + +![Raw body URL in the event data under steps.trigger.event.body.raw_body_url](https://res.cloudinary.com/pipedreamin/image/upload/v1647895357/docs/components/CleanShot_2022-03-21_at_16.42.01_2x_w6dmqk.png) + +Within your workflow, you can download the contents of this data using the **Send HTTP Request** action, or [by saving the data as a file to the `/tmp` directory](/workflows/building-workflows/code/nodejs/working-with-files/). + +#### Example: Download the HTTP payload using the Send HTTP Request action + +_Note: you can only download payloads at most {process.env.FUNCTION_PAYLOAD_LIMIT} in size using this method. Otherwise, you may encounter a [Function Payload Limit Exceeded](/troubleshooting/#function-payload-limit-exceeded) error._ + +You can download the large HTTP payload using the **Send HTTP Request** action. [Copy this workflow to see how this works](https://pipedream.com/new?h=tch_egfAby). + +The payload from the trigger of the workflow is exported to the variable `steps.retrieve_large_payload.$return_value`: + +![HTTP response data](/images/triggers/send_http_request_action.png) + +#### Example: Download the HTTP payload to the `/tmp` directory + +[This workflow](https://pipedream.com/new?h=tch_5ofXkX) downloads the HTTP payload, saving it as a file to the [`/tmp` directory](/workflows/building-workflows/code/nodejs/working-with-files/#the-tmp-directory). + +```javascript +import stream from "stream"; +import { promisify } from "util"; +import fs from "fs"; +import got from "got"; + +export default defineComponent({ + async run({ steps, $ }) { + const pipeline = promisify(stream.pipeline); + await pipeline( + got.stream(steps.trigger.event.body.raw_body_url), + fs.createWriteStream(`/tmp/raw_body`) + ); + }, +}) +``` + +You can [read this file](/workflows/building-workflows/code/nodejs/working-with-files/#reading-a-file-from-tmp) in subsequent steps of your workflow. + +#### How the payload data is saved + +Your raw payload is saved to a Pipedream-owned [Amazon S3 bucket](https://aws.amazon.com/s3/). Pipedream generates a [signed URL](https://docs.aws.amazon.com/AmazonS3/latest/dev/ShareObjectPreSignedURL.html) that allows you to access to that file for up to 30 minutes. After 30 minutes, the signed URL will be invalidated, and the file will be deleted. + +#### Limits + +**You can upload payloads up to 5TB in size**. However, payloads that large may trigger [other Pipedream limits](/workflows/limits/). Please [reach out](https://pipedream.com/support/) with any specific questions or issues. + +### Large File Support + +_This interface is best used for uploading large files, like images or videos. If you're sending JSON or other data directly in the HTTP payload, and encountering a **Request Entity Too Large** error, review the section above for [sending large payloads](#sending-large-payloads)_. + +You can upload any file to a [workflow](/workflows/building-workflows/) or an [event source](/workflows/building-workflows/triggers/) by making a `multipart/form-data` HTTP request with the file as one of the form parts. **Pipedream saves that file to a Pipedream-owned [Amazon S3 bucket](https://aws.amazon.com/s3/), generating a [signed URL](https://docs.aws.amazon.com/AmazonS3/latest/dev/ShareObjectPreSignedURL.html) that allows you to access to that file for up to 30 minutes**. After 30 minutes, the signed URL will be invalidated, and the file will be deleted. + +In workflows, these file URLs are provided in the `steps.trigger.event.body` variable, so you can download the file using the URL within your workflow, or pass the URL on to another third-party system for it to process. + +![Raw file URL in event data](/images/triggers/file-upload-urls.png) + +Within your workflow, you can download the contents of this data using the **Send HTTP Request** action, or [by saving the data as a file to the `/tmp` directory](/workflows/building-workflows/code/nodejs/working-with-files/). + +#### Example: upload a file using `cURL` + +For example, you can upload an image to a workflow using `cURL`: + +```bash +curl -F 'image=@my_image.png' https://myendpoint.m.pipedream.net +``` + +The `-F` tells `cURL` we're sending form data, with a single "part": a field named `image`, with the content of the image as the value (the `@` allows `cURL` to reference a local file). + +When you send this image to a workflow, Pipedream [parses the form data](#how-pipedream-handles-multipartform-data) and converts it to a JavaScript object, `event.body`. Select the event from the [inspector](/workflows/building-workflows/inspect/#the-inspector), and you'll see the `image` property under `event.body`: + +![Image form data](/images/triggers/image_form_data.png) + +When you upload a file as a part of the form request, Pipedream saves it to a Pipedream-owned [Amazon S3 bucket](https://aws.amazon.com/s3/), generating a [signed URL](https://docs.aws.amazon.com/AmazonS3/latest/dev/ShareObjectPreSignedURL.html) that allows you to access to that file for up to 30 minutes. After 30 minutes, the signed URL will be invalidated, and the file will be deleted. + +Within the `image` property of `event.body`, you'll see the value of this URL in the `url` property, along with the `filename` and `mimetype` of the file. Within your workflow, you can download the file, or pass the URL to a third party system to handle, and more. + +#### Example: Download this file to the `/tmp` directory + +[This workflow](https://pipedream.com/@dylburger/example-download-an-image-to-tmp-p_KwC2Ad/edit) downloads an image passed in the `image` field in the form request, saving it to the [`/tmp` directory](/workflows/building-workflows/code/nodejs/working-with-files/#the-tmp-directory). + +```javascript +import stream from "stream"; +import { promisify } from "util"; +import fs from "fs"; +import got from "got"; + +const pipeline = promisify(stream.pipeline); +await pipeline( + got.stream(steps.trigger.event.body.image.url), + fs.createWriteStream(`/tmp/${steps.trigger.event.body.image.filename}`) +); +``` + +#### Example: Upload image to your own Amazon S3 bucket + +[This workflow](https://pipedream.com/@dylburger/example-save-uploaded-file-to-amazon-s3-p_o7Cm9z/edit) streams the uploaded file to an Amazon S3 bucket you specify, allowing you to save the file to long-term storage. + +#### Limits + +Since large files are uploaded using a `Content-Type` of `multipart/form-data`, the limits that apply to [form data](#how-pipedream-handles-multipartform-data) also apply here. + +The content of the file itself does not contribute to the HTTP payload limit imposed for forms. **You can upload files up to 5TB in size**. However, files that large may trigger [other Pipedream limits](/workflows/limits/). Please [reach out](https://pipedream.com/support/) with any specific questions or issues. + +### Cross-Origin HTTP Requests + +We return the following headers on HTTP `OPTIONS` requests: + +``` +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE +``` + +Thus, your endpoint will accept [cross-origin HTTP requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) from any domain, using any standard HTTP method. + +### HTTP Responses + +#### Default HTTP response + +By default, when you send a [valid HTTP request](#valid-requests) to your endpoint URL, you should expect to receive a `200 OK` status code with the following payload: + +``` +

Success!

+

To customize this response, check out our docs here

+``` + +When you're processing HTTP requests, you often don't need to issue any special response to the client. We issue this default response so you don't have to write any code to do it yourself. + + + +**How can my workflow run faster?** + +See [our guide on running workflows faster](/troubleshooting/faq/#how-can-my-workflow-run-faster). + + +#### Customizing the HTTP response + +If you need to issue a custom HTTP response from a workflow, you can either: + +- Use the **Return HTTP response** action, available on the **HTTP / Webhook** app, or +- **Use the `$.respond()` function in a Code or Action step**. + +#### Using the HTTP Response Action + +The HTTP Response action lets you return HTTP responses without the need to write code. You can customize the response status code, and optionally specify response headers and body. + +This action uses `$.respond()` and will always [respond immediately](#returning-a-response-immediately) when called in your workflow. A [response error](#errors-with-http-responses) will still occur if your workflow throws an Error before this action runs. + +#### Using custom code with `$.respond()` + +You can return HTTP responses in Node.js code with the `$.respond()` function. + +`$.respond()` takes a single argument: an object with properties that specify the body, headers, and HTTP status code you'd like to respond with: + +```javascript +defineComponent({ + async run({ steps, $ }) { + await $.respond({ + status: 200, + headers: { "my-custom-header": "value" }, + body: { message: "My custom response" }, // This can be any string, object, Buffer, or Readable stream + }); + }, +}); +``` + +The value of the `body` property can be either a string, object, a [Buffer](https://nodejs.org/api/buffer.html#buffer_buffer) (binary data), or a [Readable stream](https://nodejs.org/api/stream.html#stream_readable_streams). Attempting to return any other data may yield an error. + +In the case where you return a Readable stream: + +- You must `await` the `$.respond` function (`await $.respond({ ... }`) +- The stream must close and be finished reading within your [workflow execution timeout](/workflows/limits/#time-per-execution). +- You cannot return a Readable and use the [`immediate: true`](#returning-a-response-immediately) property of `$.respond`. + +#### Timing of `$.respond()` execution + +You may notice some response latency calling workflows that use `$.respond()` from your HTTP client. By default, `$.respond()` is called at the end of your workflow, after all other code is done executing, so it may take some time to issue the response back. + +If you need to issue an HTTP response in the middle of a workflow, see the section on [returning a response immediately](#returning-a-response-immediately). + +#### Returning a response immediately + +You can issue an HTTP response within a workflow, and continue the rest of the workflow execution, by setting the `immediate` property to `true`: + +```javascript +defineComponent({ + async run({ steps, $ }) { + await $.respond({ + immediate: true, + status: 200, + headers: { "my-custom-header": "value" }, + body: { message: "My custom response" }, + }); + }, +}); +``` + +Passing `immediate: true` tells `$.respond()` to issue a response back to the client at this point in the workflow. After the HTTP response has been issued, the remaining code in your workflow runs. + +This can be helpful, for example, when you're building a Slack bot. When you send a message to a bot, Slack requires a `200 OK` response be issued immediately, to confirm receipt: + +```javascript +defineComponent({ + async run({ steps, $ }) { + await $.respond({ + immediate: true, + status: 200, + body: "", + }); + }, +}); +``` + +Once you issue the response, you'll probably want to process the message from the user and respond back with another message or data requested by the user. + +[Here's an example workflow](https://pipedream.com/@dylburger/issue-http-response-immediately-continue-running-workflow-p_pWCWGJ) that shows how to use `immediate: true` and run code after the HTTP response is issued. + +#### Errors with HTTP Responses + +If you use `$.respond()` in a workflow, **you must always make sure `$.respond()` is called in your code**. If you make an HTTP request to a workflow, and run code where `$.respond()` is _not_ called, your endpoint URL will issue a `400 Bad Request` error with the following body: + +``` +No $.respond called in workflow +``` + +This might happen if: + +- You call `$.respond()` conditionally, where it does not run under certain conditions. +- Your workflow throws an Error before you run `$.respond()`. +- You return data in the `body` property that isn't a string, object, or Buffer. + +If you can't handle the `400 Bad Request` error in the application calling your workflow, you can implement `try` / `finally` logic to ensure `$.respond()` always gets called with some default message. For example: + +```javascript +defineComponent({ + async run({ steps, $ }) { + try { + // Your code here that might throw an exception or not run + throw new Error("Whoops, something unexpected happened."); + } finally { + await $.respond({ + status: 200, + body: { + msg: "Default response", + }, + }); + } + }, +}); +``` + +### Errors + +Occasionally, you may encounter errors when sending requests to your endpoint: + +#### Request Entity Too Large + +The endpoint will issue a `413 Payload Too Large` status code when the body of your request exceeds {process.env.PAYLOAD_SIZE_LIMIT}. + +In this case, the request will still appear in the inspector, with information on the error. + +#### API key does not exist + +Your API key is the host part of the endpoint, e.g. the `eniqtww30717` in `eniqtww30717.m.pipedream.net`. If you attempt to send a request to an endpoint that does not exist, we'll return a `404 Not Found` error. + +We'll also issue a 404 response on workflows with an HTTP trigger that have been disabled. + +#### Too Many Requests + +If you send too many requests to your HTTP source within a small period of time, we may issue a `429 Too Many Requests` response. [Review our limits](/workflows/limits/) to understand the conditions where you might be throttled. + +You can also [reach out](https://pipedream.com/support/) to inquire about raising this rate limit. + +If you control the application sending requests, you should implement [a backoff strategy](https://medium.com/clover-platform-blog/conquering-api-rate-limiting-dcac5552714d) to temporarily slow the rate of events. + +## Schedule + +Pipedream allows you to run hosted scheduled jobs — commonly-referred to as a "cron job" — [for free](/pricing/). You can think of workflows like scripts that run on a schedule. + +You can write scheduled job to send an HTTP request, send a scheduled email, run any Node.js or Python code, connect to any API, and much more. Pipedream manages the servers where these jobs run, so you don't have to worry about setting up a server of your own or operating some service just to run code on a schedule. You write the workflow, we take care of the rest. + +### Choosing a Schedule trigger + +To create a new scheduled job, create a new workflow and search for the **Schedule** trigger: + +![Cron Scheduler source](/images/triggers/select-schedule-trigger.png) + +By default, your trigger will be turned **Off**. **To enable it, select either of the scheduling options**: + +- **Every** : run the job every N days, hours, minutes (e.g. every 1 day, every 3 hours). +- **Cron Expression** : schedule your job using a cron expression. For example, the expression `0 0 * * *` will run the job every day at midnight. Cron expressions can be tied to any timezone. + +### Testing a scheduled job + +If you're running a scheduled job once a day, you probably don't want to wait until the next day's run to test your new code. You can manually run the workflow associated with a scheduled job at any time by pressing the **Run Now** button. + +### Job History + +You'll see the history of job executions under the **Job History** section of the [Inspector](/workflows/building-workflows/inspect/). + +Clicking on a specific job shows the execution details for that job — all the logs and observability associated with that run of the workflow. + +### Trigger a notification to an external service (email, Slack, etc.) + +You can send yourself a notification — for example, an email or a Slack message — at any point in a workflow by using the relevant [Action](/workflows/contributing/components/#actions) or [Destination](/workflows/data-management/destinations/). + +If you'd like to email yourself when a job finishes successfully, you can use the [Email Destination](/workflows/data-management/destinations/email/). You can send yourself a Slack message using the Slack Action, or trigger an [HTTP request](/workflows/data-management/destinations/http/) to an external service. + +You can also [write code](/workflows/building-workflows/code/) to trigger any complex notification logic you'd like. + +### Troubleshooting your scheduled jobs + +When you run a scheduled job, you may need to troubleshoot errors or other execution issues. Pipedream offers built-in, step-level logs that show you detailed execution information that should aid troubleshooting. + +Any time a scheduled job runs, you'll see a new execution appear in the [Inspector](/workflows/building-workflows/inspect/). This shows you when the job ran, how long it took to run, and any errors that might have occurred. **Click on any of these lines in the Inspector to view the details for a given run**. + +Code steps show [logs](/workflows/building-workflows/code/nodejs/#logs) below the step itself. Any time you run `console.log()` or other functions that print output, you should see the logs appear directly below the step where the code ran. + +[Actions](/workflows/contributing/components/#actions) and [Destinations](/workflows/data-management/destinations/) also show execution details relevant to the specific Action or Destination. For example, when you use the [HTTP Destination](/workflows/data-management/destinations/http/) to make an HTTP request, you'll see the HTTP request and response details tied to that Destination step: + +## Email + +When you select the **Email** trigger: + +![Selecting the new email trigger](/images/triggers/select-email-trigger.png) + +Pipedream creates an email address specific to your workflow. Any email sent to this address triggers your workflow: + +![Custom workflow email address](/images/triggers/email-trigger.png) + +As soon as you send an email to the workflow-specific address, Pipedream parses its body, headers, and attachments into a JavaScript object it exposes in the `steps.trigger.event` variable that you can access within your workflow. This transformation can take a few seconds to perform. Once done, Pipedream will immediately trigger your workflow with the transformed payload. + +[Read more about the shape of the email trigger event](/workflows/building-workflows/triggers/#email). + +### Sending large emails + +By default, you can send emails up to {process.env.EMAIL_PAYLOAD_SIZE_LIMIT} in total size (content, headers, attachments). Emails over this size will be rejected, and you will not see them appear in your workflow. + +**You can send emails up to `30MB` in size by sending emails to `[YOUR EMAIL ENDPOINT]@upload.pipedream.net`**. If your workflow-specific email address is `endpoint@pipedream.net`, your "large email address" is `endpoint@upload.pipedream.net`. + +Emails delivered to this address are uploaded to a private URL you have access to within your workflow, at the variable `steps.trigger.event.mail.content_url`. You can download and parse the email within your workflow using that URL. This content contains the _raw_ email. Unlike the standard email interface, you must parse this email on your own - see the examples below. + +#### Example: Download the email using the Send HTTP Request action + +_Note: you can only download emails at most {process.env.FUNCTION_PAYLOAD_LIMIT} in size using this method. Otherwise, you may encounter a [Function Payload Limit Exceeded](/troubleshooting/#function-payload-limit-exceeded) error._ + +You can download the email using the **Send HTTP Request** action. [Copy this workflow to see how this works](https://pipedream.com/new?h=tch_1AfMyl). + +This workflow also parses the contents of the email and exposes it as a JavaScript object using the [`mailparser` library](https://nodemailer.com/extras/mailparser/): + +```javascript +import { simpleParser } from "mailparser"; + +export default defineComponent({ + async run({ steps, $ }) { + return await simpleParser(steps.get_large_email_content.$return_value); + }, +}); +``` + +#### Example: Download the email to the `/tmp` directory, read it and parse it + +[This workflow](https://pipedream.com/new?h=tch_jPfaEJ) downloads the email, saving it as a file to the [`/tmp` directory](/workflows/building-workflows/code/nodejs/working-with-files/#the-tmp-directory). Then it reads the same file (as an example), and parses it using the [`mailparser` library](https://nodemailer.com/extras/mailparser/): + +```javascript +import stream from "stream"; +import { promisify } from "util"; +import fs from "fs"; +import got from "got"; +import { simpleParser } from "mailparser"; + +// To use previous step data, pass the `steps` object to the run() function +export default defineComponent({ + async run({ steps, $ }) { + const pipeline = promisify(stream.pipeline); + await pipeline( + got.stream(steps.trigger.event.mail.content_url), + fs.createWriteStream(`/tmp/raw_email`) + ); + + // Now read the file and parse its contents into the `parsed` variable + // See https://nodemailer.com/extras/mailparser/ for parsing options + const f = fs.readFileSync(`/tmp/raw_email`); + return await simpleParser(f); + }, +}); +``` + +#### How the email is saved + +Your email is saved to a Pipedream-owned [Amazon S3 bucket](https://aws.amazon.com/s3/). Pipedream generates a [signed URL](https://docs.aws.amazon.com/AmazonS3/latest/dev/ShareObjectPreSignedURL.html) that allows you to access to that file for up to 30 minutes. After 30 minutes, the signed URL will be invalidated, and the file will be deleted. + +### Email attachments + +You can attach any files to your email, up to [the total email size limit](/workflows/limits/#email-triggers). + +Attachments are stored in `steps.trigger.event.attachments`, which provides an array of attachment objects. Each attachment in that array exposes key properties: + +- `contentUrl`: a URL that hosts your attachment. You can [download this file to the `/tmp` directory](/workflows/building-workflows/code/nodejs/http-requests/#download-a-file-to-the-tmp-directory) and process it in your workflow. +- `content`: If the attachment contains text-based content, Pipedream renders the attachment in `content`, up to 10,000 bytes. +- `contentTruncated`: `true` if the attachment contained text-based content larger than 10,000 bytes. If `true`, the data in `content` will be truncated, and you should fetch the full attachment from `contentUrl`. + +### Appending metadata to the incoming email address with `+data` + +Pipedream provides a way to append metadata to incoming emails by adding a `+` sign to the incoming email key, followed by any arbitrary string: + +``` +myemailaddr+test@pipedream.net +``` + +Any emails sent to your workflow-specific email address will resolve to that address, triggering your workflow, no matter the data you add after the `+` sign. Sending an email to both of these addresses triggers the workflow with the address `myemailaddr@pipedream.net`: + +``` +myemailaddr+test@pipedream.net +myemailaddr+unsubscribe@pipedream.net +``` + +This allows you implement conditional logic in your workflow based on the data in that string. + +### Troubleshooting + +#### I'm receiving an `Expired Token` error when trying to read an email attachment + +Email attachments are saved to S3, and are accessible in your workflows over [pre-signed URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html). + +If the presigned URL for the attachment has expired, then you'll need to send another email to create a brand new pre-signed URL. + +If you're using email attachments in combination with [`$.flow.delay`](/workflows/building-workflows/code/nodejs/delay/) or [`$.flow.rerun`](/workflows/building-workflows/code/nodejs/rerun/) which introduces a gap of time between steps in your workflow, then there's a chance the email attachment's URL will expire. + +To overcome this, we suggest uploading your email attachments to your Project's [File Store](/workflows/projects/file-stores/) for persistent storage. + +## RSS + +Choose the RSS trigger to watch an RSS feed for new items: + +![Selecting the RSS feed trigger](/images/triggers/select-rss-trigger.png) + +This will create an RSS [event source](/workflows/building-workflows/triggers/) that polls the feed for new items on the schedule you select. Every time a new item is found, your workflow will run. + +## Events + +Events trigger workflow executions. The event that triggers your workflow depends on the trigger you select for your workflow: + +- [HTTP triggers](/workflows/building-workflows/triggers/#http) invoke your workflow on HTTP requests. +- [Cron triggers](/workflows/building-workflows/triggers/#schedule) invoke your workflow on a time schedule (e.g., on an interval). +- [Email triggers](/workflows/building-workflows/triggers/#email) invoke your workflow on inbound emails. +- [Event sources](/workflows/building-workflows/triggers/#app-based-triggers) invoke your workflow on events from apps like Twitter, Google Calendar, and more. + +### Selecting a test event + +When you test any step in your workflow, Pipedream passes the test event you select in the trigger step: + +![Selecting a test event in the trigger](/images/triggers/select-an-event.png) + +You can select any event you've previously sent to your trigger as your test event, or send a new one. + +### Examining event data + +When you select an event, you'll see [the incoming event data](#event-format) and the [event context](#stepstriggercontext) for that event: + +![The event and context in a trigger initiation](https://res.cloudinary.com/pipedreamin/image/upload/v1648759141/docs/components/CleanShot_2022-03-31_at_16.30.37_jwwwdy.png) + +Pipedream parses your incoming data and exposes it in the variable [`steps.trigger.event`](#event-format), which you can access in any [workflow step](/workflows/#steps). + +### Copying references to event data + +When you're [examining event data](#examining-event-data), you'll commonly want to copy the name of the variable that points to the data you need to reference in another step. + +Hover over the property whose data you want to reference, and click the **Copy Path** button to its right: + +![Copy an event path](https://res.cloudinary.com/pipedreamin/image/upload/v1648759215/docs/components/CleanShot_2022-03-31_at_16.39.56_lsus2o.gif) + +### Copying the values of event data + +You can also copy the value of specific properties of your event data. Hover over the property whose data you want to copy, and click the **Copy Value** button to its right: + +![Copy event attribute value](https://res.cloudinary.com/pipedreamin/image/upload/v1648759275/docs/components/CleanShot_2022-03-31_at_16.41.02_xgzcsa.gif) + +### Event format + +When you send an event to your workflow, Pipedream takes the trigger data — for example, the HTTP payload, headers, etc. — and adds our own Pipedream metadata to it. + +**This data is exposed in the `steps.trigger.event` variable. You can reference this variable in any step of your workflow**. + +You can reference your event data in any [code](/workflows/building-workflows/code/) or [action](/workflows/contributing/components/#actions) step. See those docs or the general [docs on passing data between steps](/workflows/#steps) for more information. + +The specific shape of `steps.trigger.event` depends on the trigger type: + +#### HTTP + +| Property | Description | +| ----------- | :---------------------------------------------------: | +| `body` | A string or object representation of the HTTP payload | +| `client_ip` | IP address of the client that made the request | +| `headers` | HTTP headers, represented as an object | +| `method` | HTTP method | +| `path` | HTTP request path | +| `query` | Query string | +| `url` | Request host + path | + +#### Cron Scheduler + +| Property | Description | +| --------------------- | :---------------------------------------------------------------------------------------------: | +| `interval_seconds` | The number of seconds between scheduled executions | +| `cron` | When you've configured a custom cron schedule, the cron string | +| `timestamp` | The epoch timestamp when the workflow ran | +| `timezone_configured` | An object with formatted datetime data for the given execution, tied to the schedule's timezone | +| `timezone_utc` | An object with formatted datetime data for the given execution, tied to the UTC timezone | + +#### Email + +We use Amazon SES to receive emails for the email trigger. You can find the shape of the event in the [SES docs](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-notifications-contents.html). + +### `steps.trigger.context` + +`steps.trigger.event` contain your event's **data**. `steps.trigger.context` contains _metadata_ about the workflow and the execution tied to this event. + +You can use the data in `steps.trigger.context` to uniquely identify the Pipedream event ID, the timestamp at which the event invoked the workflow, and more: + +| Property | Description | +| ------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------: | +| `deployment_id` | A globally-unique string representing the current version of the workflow | +| `emitter_id` | The ID of the workflow trigger that emitted this event, e.g. the [event source](/workflows/building-workflows/triggers/) ID. | +| `id` | A unique, Pipedream-provided identifier for the event that triggered this workflow | +| `owner_id` | The Pipedream-assigned [workspace ID](/workflows/workspaces/#finding-your-workspaces-id) that owns the workflow | +| `platform_version` | The version of the Pipedream execution environment this event ran on | +| `replay` | A boolean, whether the event was replayed via the UI | +| `trace_id` | Holds the same value for all executions tied to an original event. [See below for more details](#how-do-i-retrieve-the-execution-id-for-a-workflow). | +| `ts` | The ISO 8601 timestamp at which the event invoked the workflow | +| `workflow_id` | The workflow ID | +| `workflow_name` | The workflow name | + +#### How do I retrieve the execution ID for a workflow? + +Pipedream exposes two identifies for workflow executions: one for the execution, and one for the "trace". + +`steps.trigger.context.id` should be unique for every execution of a workflow. + +`steps.trigger.context.trace_id` will hold the same value for all executions tied to the same original event, e.g. if you have auto-retry enabled and it retries a workflow three times, the `id` will change, but the `trace_id` will remain the same. For example, if you call `$.flow.suspend()` on a workflow, we run a new execution after the suspend, so you'd see two total executions: `id` will be unique before and after the suspend, but `trace_id` will be the same. + +You may notice other properties in `context`. These are used internally by Pipedream, and are subject to change. + +### Event retention + +On the Free and Basic plans, each workflow retains at most 100 events or 7 days of history. + +- After 100 events have been processed, Pipedream will delete the oldest event data as new events arrive, keeping only the last 100 events. +- Or if an event is older than 7 days, Pipedream will delete the event data. + +Other paid plans have longer retention. [See the pricing page](https://pipedream.com/pricing) for details. + +Events are also stored in [event history](/workflows/event-history/) for up to 30 days, depending on your plan. [See the pricing page](https://pipedream.com/pricing) for the retention on your plan. + +Events that are [delayed](/workflows/building-workflows/control-flow/delay/) or [suspended](/glossary/#suspend) are retained for the duration of the delay. After the delay, the workflow is executed, and the event data is retained according to the rules above. + + +For an extended history of events across all of your workflows, included processed events, with the ability to filter by status and time range, please see the [Event History](/workflows/event-history/). + + +## Don't see a trigger you need? + +If you don't see a trigger you'd like us to support, please [let us know](https://pipedream.com/support/). diff --git a/docs-v2/pages/workflows/using-props.mdx b/docs-v2/pages/workflows/building-workflows/using-props.mdx similarity index 93% rename from docs-v2/pages/workflows/using-props.mdx rename to docs-v2/pages/workflows/building-workflows/using-props.mdx index ca695a63c5244..f4d29c87c9031 100644 --- a/docs-v2/pages/workflows/using-props.mdx +++ b/docs-v2/pages/workflows/building-workflows/using-props.mdx @@ -24,7 +24,7 @@ When you click into a prop field, an object explorer expands below the input. Yo To manually enter or edit an expression, just enter or edit a value between double curly braces `{{ }}`. Pipedream provides auto-complete support as soon as you type. -![Manually entering an expression as a param](https://res.cloudinary.com/pipedreamin/image/upload/v1649169533/docs/components/CleanShot_2022-04-05_at_10.38.16_qokasr.gif) +![Manually entering an expression as a param](https://res.cloudinary.com/pipedreamin/image/upload/v1710509983/docs/docs/props/CleanShot_2024-03-15_at_09.38.09_topm8f.gif) You can also run Node.js code in `{{ }}`. For example, if `event.foo` is a JSON object and you want to pass it to a param as a string, you can run `{{JSON.stringify(event.foo)}}`. @@ -32,4 +32,4 @@ You can also run Node.js code in `{{ }}`. For example, if `event.foo` is a JSON To paste a reference from a step export, find the reference you want to use, click **Copy Path** and then paste it into the input. -![Copying the path to from a step export and pasting it as a prop in another step](https://res.cloudinary.com/pipedreamin/image/upload/v1649169707/docs/components/CleanShot_2022-04-05_at_10.40.42_e34xoj.gif) +![Copying the path to from a step export and pasting it as a prop in another step](https://res.cloudinary.com/pipedreamin/image/upload/v1710509995/docs/docs/props/CleanShot_2024-03-15_at_09.39.18_mfd5wa.gif) diff --git a/docs-v2/pages/workflows/cli/_meta.tsx b/docs-v2/pages/workflows/cli/_meta.tsx new file mode 100644 index 0000000000000..4047d022328b2 --- /dev/null +++ b/docs-v2/pages/workflows/cli/_meta.tsx @@ -0,0 +1,5 @@ +export default { + "reference": "CLI Reference", + "install": "Install", + "login": "Login", +} as const diff --git a/docs-v2/pages/cli/install.mdx b/docs-v2/pages/workflows/cli/install.mdx similarity index 84% rename from docs-v2/pages/cli/install.mdx rename to docs-v2/pages/workflows/cli/install.mdx index 7a7a438dff1f4..ef80352484eba 100644 --- a/docs-v2/pages/cli/install.mdx +++ b/docs-v2/pages/workflows/cli/install.mdx @@ -10,6 +10,7 @@ import VideoPlayer from '@/components/VideoPlayer' ### Homebrew ```bash +brew tap pipedreamhq/pd-cli brew install pipedreamhq/pd-cli/pipedream ``` @@ -21,7 +22,7 @@ Run the following command: curl https://cli.pipedream.com/install | sh ``` -This will automatically download and install the `pd` CLI to your Mac. You can also [download the macOS build](https://cli.pipedream.com/darwin/amd64/latest/pd.zip), unzip that archive, and place the `pd` binary somewhere in [your `PATH`](https://opensource.com/article/17/6/set-path-linux). +This will automatically download and install the `pd` CLI to your Mac. You can also [download the macOS build](https://cli.pipedream.com/darwin/amd64/latest/pd.zip), unzip that archive, and place the `pd` binary somewhere in [your `PATH`](https://opensource.com/article/17/6/set-path-linux). If this returns a permissions error, you may need to run: @@ -29,7 +30,7 @@ If this returns a permissions error, you may need to run: curl https://cli.pipedream.com/install | sudo sh ``` - + If you encounter the error `bad CPU type in executable: pd`, you will need to install Rosetta 2 on your Mac by running the following command: ```bash @@ -51,7 +52,7 @@ Download the appropriate [Linux CLI build](#cli-builds) for your architecture. U ## CLI Builds -Pipedream publishes the following builds of the CLI. If you need to use the CLI on another OS or architecture, [please reach out](https://docs.pipedream.com/support/). +Pipedream publishes the following builds of the CLI. If you need to use the CLI on another OS or architecture, [please reach out](https://pipedream.com/support/). | Operating System | Architecture | link | | ---------------- | ------------ | ----------------------------------------------------------------- | @@ -62,7 +63,18 @@ Pipedream publishes the following builds of the CLI. If you need to use the CLI | macOS | amd64 | [download](https://cli.pipedream.com/darwin/amd64/latest/pd.zip) | | Windows | amd64 | [download](https://cli.pipedream.com/windows/amd64/latest/pd.zip) | -Run `pd` to see a list of all commands, or `pd help ` to display help docs for a specific command. +## Community Libraries + + +Please note that Pipedream does not verify the correctness or security of these community libraries. Use them at your own risk. + + +### [Nix](https://nixos.org/) -See the [CLI reference](/cli/reference/) for detailed usage and examples for each command. +The `pd` binary is available via Nix flake [here](https://github.com/planet-a-ventures/pipedream-cli) + +## Help + +Run `pd` to see a list of all commands, or `pd help ` to display help docs for a specific command. +See the [CLI reference](/workflows/cli/reference/) for detailed usage and examples for each command. diff --git a/docs-v2/pages/workflows/cli/login.mdx b/docs-v2/pages/workflows/cli/login.mdx new file mode 100644 index 0000000000000..0abfcbbad0b3d --- /dev/null +++ b/docs-v2/pages/workflows/cli/login.mdx @@ -0,0 +1,57 @@ +# Logging into the CLI + +To start using the Pipedream CLI, you'll need to link it to your Pipedream account. If you don't have a Pipedream account, you can sign up from the CLI. + + + +## Existing Pipedream account + +If you already have a Pipedream account, run + +``` +pd login +``` + +This will open up a new window in your default browser. If you're already logged into your Pipedream account in this browser, this will immediately link the CLI to your account, writing your API key for that account to your [`pd` config file](/workflows/cli/reference/#cli-config-file). + +Otherwise, you'll be asked to login. + +Once you're done, go back to your shell and you should see confirmation that your account is linked: + +``` +> pd login +Logged in as dylburger (dylan@pipedream.com) +``` + +Then [follow this guide](/workflows/cli/reference/#creating-a-profile-for-a-workspace) to learn how to find your workspace ID and associate it with a `pd` profile. + +## Signing up for Pipedream via the CLI + +If you haven't signed up for a Pipedream account, you can create an account using the CLI: + +``` +pd signup +``` + +This will open up a new window in your default browser. You'll be asked to sign up for Pipedream here. Once you do, your account will be linked to the CLI, writing your API key for that account to your [`pd` config file](/workflows/cli/reference/#cli-config-file). + +Once you're done, go back to your shell and you should see confirmation that your account is linked: + +``` +> pd signup +Logged in as dylburger (dylan@pipedream.com) +``` + +## Logging out of the CLI + +You can log out of the CLI by running: + +``` +pd logout +``` + +This will remove your API key from the [`pd` config file](/workflows/cli/reference/#cli-config-file). + +## Using the CLI to manage multiple accounts + +If you have multiple Pipedream accounts, you can use [profiles](/workflows/cli/reference/#profiles) to ensure the CLI can manage resources for each. diff --git a/docs-v2/pages/workflows/cli/reference.mdx b/docs-v2/pages/workflows/cli/reference.mdx new file mode 100644 index 0000000000000..fbf27e8000cc0 --- /dev/null +++ b/docs-v2/pages/workflows/cli/reference.mdx @@ -0,0 +1,354 @@ +# CLI Reference + +## Installing the CLI + +[See the CLI installation docs](/workflows/cli/install/) to learn how to install the CLI for your OS / architecture. + +## Command Reference + +Run `pd` to see a list of all commands with basic usage info, or run `pd help ` to display help docs for a specific command. + +We've also documented each command below, with usage examples for each. + +### General Notes + +Everywhere you can refer to a specific component as an argument, you can use the component's ID _or_ its name slug. For example, to retrieve details about a specific source using `pd describe`, you can use either of the following commands: + +```bash +> pd describe dc_abc123 + + id: dc_abc123 + name: http + endpoint: https://myendpoint.m.pipedream.net + +> pd describe http +Searching for sources matching http + + id: dc_abc123 + name: http + endpoint: https://myendpoint.m.pipedream.net +``` + +### `pd delete` + +Deletes an event source. Run: + +```bash +pd delete +``` + +Run `pd list so` to display a list of your event sources. + +### `pd deploy` + +Deploy an event source from local or remote code. + +Running `pd deploy`, without any arguments, brings up an interactive menu asking you select a source. This list of sources is retrieved from the registry of public sources [published to Github](https://github.com/PipedreamHQ/pipedream/tree/master/components). + +When you select a source, we'll deploy it and start listening for new events. + +You can also deploy a specific source via the source's `key` (defined in the component file for the source): + +```bash +pd deploy http-new-requests +``` + +or author a component locally and deploy that local file: + +```bash +pd deploy http.js +``` + +[Read more about authoring your own event sources](/workflows/contributing/components/quickstart/nodejs/sources/). + +### `pd describe` + +Display the details for a source: its id, name, and other configuration details: + +```bash +pd describe SOURCE_ID_OR_NAME +``` + +### `pd dev` + +`pd dev` allows you to interactively develop a source from a local file.`pd dev` will link your local file with the deployed component and watch your local file for changes. When you save changes to your local file, your component will automatically be updated on Pipedream. + +```bash +pd dev FILE_OR_NAME +``` + +If you quit `pd dev` and want to link the same deployed source to your local file, you can pass the deployed component ID using the `--dc` flag: + +```bash +pd dev --dc SOURCE_ID FILE_OR_NAME +``` + +### `pd events` + +Returns historical events sent to a source, and streams emitted events directly to the CLI. + +```bash +pd events SOURCE_ID +``` + +By default, `pd events` prints (up to) the last 10 events sent to your source. + +```bash +pd events -n 100 SOURCE_ID_OR_NAME +``` + +`pd events -n N` retrieves the last `N` events sent to your source. We store the last 100 events sent to a source, so you can retrieve a max of 100 events using this command. + +```bash +pd events -f SOURCE_ID_OR_NAME +``` + +`pd events -f` connects to the [SSE stream tied to your source](/workflows/data-management/destinations/sse/) and displays events as the source produces them. + +```bash +pd events -n N -f SOURCE_ID_OR_NAME +``` + +You can combine the `-n` and `-f` options to list historical events _and_ follow the source for new events. + +### `pd help` + +Displays help for any command. Run `pd help events`, `pd help describe`, etc. + +### `pd init` + +Generate new app and component files from templates. + +#### `pd init app` + +Creates a directory and [an app file](/workflows/contributing/components/guidelines/#app-files) from a template + +```bash +# Creates google_calendar/ directory and google_calendar.mjs file +pd init app google_calendar +``` + +#### `pd init action` + +Creates a new directory and [a component action](/workflows/contributing/components/#actions) from a template. + +```bash +# Creates add-new-event/ directory and add-new-event.mjs file +pd init action add-new-event +``` + +#### `pd init source` + +Creates a new directory and [an event source](/workflows/building-workflows/triggers/) from a template. + +```bash +# Creates cancelled-event/ directory and cancelled_event.mjs file +pd init source cancelled-event +``` + +You can attach [database](/workflows/contributing/components/api/#db), [HTTP](/workflows/contributing/components/api/#http), or [Timer](/workflows/contributing/components/api/#timer) props to your template using the following flags: + +| Prop type | Flag | +| --------- | --------- | +| Database | `--db` | +| HTTP | `--http` | +| Timer | `--timer` | + +For example, running: + +```bash +pd init source cancelled-event --db --http --timer +``` + +will include the following props in your new event source: + +```javascript +props: { + db: "$.service.db", + http: "$.interface.http", + timer: "$.interface.timer", +} +``` + +### `pd list` + +Lists Pipedream sources running in your account. Running `pd list` without any arguments prompts you to select the type of resource you'd like to list. + +You can also list specific resource types directly: + +```bash +pd list components +``` + +```bash +pd list streams +``` + +`sources` and `streams` have shorter aliases, too: + +```bash +pd list so +``` + +```bash +pd list st +``` + +### `pd login` + +Log in to Pipedream CLI and persist API key locally. See [Logging into the CLI](/workflows/cli/login/) for more information. + +### `pd logout` + +Unsets the local API key tied to your account. + +Running `pd logout` without any arguments removes the default API key from your [config file](/workflows/cli/reference/#cli-config-file). + +You can remove the API key for a specific profile by running: + +```bash +pd logout -p PROFILE +``` + +### `pd logs` + +Event sources produce logs that can be useful for troubleshooting issues with that source. `pd logs` displays logs for a source. + +Running `pd logs ` connects to the [SSE logs stream tied to your source](/workflows/building-workflows/triggers/), displaying new logs as the source produces them. + +Any errors thrown by the source will also appear here. + +### `pd publish` + +To publish an action, use the `pd publish` command. + +```bash +pd publish +``` + +For example: + +```bash +pd publish my-action.js +``` + +### `pd signup` + +Sign up for Pipedream via the CLI and persist your API key locally. See the docs on [Signing up for Pipedream via the CLI](/workflows/cli/login/#signing-up-for-pipedream-via-the-cli) for more information. + +### `pd unpublish` + +Unpublish a component you've published to your account. If you publish a source or action that you no longer need, you can unpublish it by component `key`: + +``` +pd unpublish component +``` + +### `pd update` + +Updates the code, props, or metadata for an event source. + +If you deployed a source from Github, for example, someone might publish an update to that source, and you may want to run the updated code. + +```bash +pd update SOURCE_ID_OR_NAME \ + --code https://github.com/PipedreamHQ/pipedream/blob/master/components/http/sources/new-requests/new-requests.js +``` + +You can change the name of a source: + +```bash +pd update SOURCE_ID_OR_NAME --name NEW_NAME +``` + +You can deactivate a source if you want to stop it from running: + +```bash +pd update SOURCE_ID_OR_NAME --deactivate +``` + +or activate a source you previously deactivated: + +```bash +pd update SOURCE_ID_OR_NAME --activate +``` + +## Profiles + +Profiles allow you to work with multiple, named Pipedream accounts via the CLI. + +### Creating a new profile + +When you [login to the CLI](/workflows/cli/login/), the CLI writes the API key for that account to your config file, in the `api_key` field: + +```bash +api_key = abc123 +``` + +You can set API keys for other, named profiles, too. Run + +```bash +pd login -p +``` + +`` can be any string of shell-safe characters that you'd like to use to identify this new profile. The CLI opens up a browser asking you to login to your target Pipedream account, then writes the API key to a section of the config file under this profile: + +```bash +[your_profile] +api_key = def456 +``` + +You can also run `pd signup -p ` if you'd like to sign up for a new Pipedream account via the CLI and set a named profile for that account. + +### Creating a profile for a workspace + +If you're working with resources in an [workspace](/workflows/workspaces/), you'll need to add an `org_id` to your profile. + +1. [Retrieve your workspaces's ID](/workflows/workspaces/#finding-your-workspaces-id) +2. Open up your [Pipedream config file](#cli-config-file) and create a new [profile](#profiles) with the following information: + +```bash +[profile_name] +api_key = +org_id = +``` + +When using the CLI, pass `--profile ` when running any command. For example, if you named your profile `workspace`, you'd run this command to publish a component: + +```bash +pd publish file.js --profile workspace +``` + +### Using profiles + +You can set a profile on any `pd` command by setting the `-p` or `--profile` flag. For example, to list the sources in a specific account, run: + +```bash +pd list sources --profile PROFILE +``` + +## Version + +To get the current version of the `pd` CLI, run + +```bash +pd --version +``` + +## Auto-upgrade + +The CLI is configured to check for new versions automatically. This ensures you're always running the most up-to-date version. + +## CLI config file + +The `pd` config file contains your Pipedream API keys (tied to your default account, or other [profiles](#profiles)) and other configuration used by the CLI. + +If the `XDG_CONFIG_HOME` env var is set, the config file will be found in `$XDG_CONFIG_HOME/pipedream`. + +Otherwise, it will be found in `$HOME/.config/pipedream`. + +## Analytics + +Pipedream tracks CLI usage data to report errors and usage stats. We use this data exclusively for the purpose of internal analytics (see [our privacy policy](https://pipedream.com/privacy) for more information). + +If you'd like to opt-out of CLI analytics, set the `PD_CLI_DO_NOT_TRACK` environment variable to `true` or `1`. diff --git a/docs-v2/pages/workflows/contributing/_meta.tsx b/docs-v2/pages/workflows/contributing/_meta.tsx new file mode 100644 index 0000000000000..84706974aef5d --- /dev/null +++ b/docs-v2/pages/workflows/contributing/_meta.tsx @@ -0,0 +1,4 @@ +export default { + "index": "Overview", + "components": "Components", +} as const diff --git a/docs-v2/pages/workflows/contributing/components/_meta.tsx b/docs-v2/pages/workflows/contributing/components/_meta.tsx new file mode 100644 index 0000000000000..dd8f1829093a5 --- /dev/null +++ b/docs-v2/pages/workflows/contributing/components/_meta.tsx @@ -0,0 +1,8 @@ +export default { + "index": "Overview", + "api": "Component API", + "actions-quickstart": "Quickstart — Actions", + "sources-quickstart": "Quickstart — Sources", + "guidelines": "Guidelines", + "typescript": "TypeScript components", +} as const diff --git a/docs-v2/pages/components/actions-quickstart.mdx b/docs-v2/pages/workflows/contributing/components/actions-quickstart.mdx similarity index 90% rename from docs-v2/pages/components/actions-quickstart.mdx rename to docs-v2/pages/workflows/contributing/components/actions-quickstart.mdx index 2e1eac47ee68c..709269fede014 100644 --- a/docs-v2/pages/components/actions-quickstart.mdx +++ b/docs-v2/pages/workflows/contributing/components/actions-quickstart.mdx @@ -6,7 +6,7 @@ import Callout from '@/components/Callout' ## Overview -This document is intended for developers who want to author and edit [Pipedream Actions](/components/#actions). After completing this quickstart, you'll understand how to: +This document is intended for developers who want to author and edit [Pipedream Actions](/workflows/contributing/components/#actions). After completing this quickstart, you'll understand how to: - Develop Pipedream components - Publish private actions and use them in workflows @@ -15,17 +15,17 @@ This document is intended for developers who want to author and edit [Pipedream - Use npm packages - Use Pipedream managed auth for a 3rd party app - -If you previously developed actions using Pipedream's UI, we recommend reviewing our [migration guide](/components/migrating/) after completing this quickstart. + +If you previously developed actions using Pipedream's UI, we recommend reviewing our [migration guide](/workflows/contributing/components/migrating/) after completing this quickstart. ## Prerequisites - Create a free account at [https://pipedream.com](https://pipedream.com) -- Download and install the [Pipedream CLI](/cli/install/) -- Once the CLI is installed, [link your Pipedream account](/cli/login/#existing-pipedream-account) to the CLI by running `pd login` in your terminal +- Download and install the [Pipedream CLI](/workflows/cli/install/) +- Once the CLI is installed, [link your Pipedream account](/workflows/cli/login/#existing-pipedream-account) to the CLI by running `pd login` in your terminal -> **NOTE:** See the [CLI reference](/cli/reference/) for detailed usage and examples beyond those covered below. +> **NOTE:** See the [CLI reference](/workflows/cli/reference/) for detailed usage and examples beyond those covered below. ## Walkthrough @@ -57,7 +57,7 @@ We recommend that you complete the examples below in order. ### hello world! -The following code represents a simple component that can be published as an action ([learn more](/components/api/) about the component structure). When used in a workflow, it will export `hello world!` as the return value for the step. +The following code represents a simple component that can be published as an action ([learn more](/workflows/contributing/components/api/) about the component structure). When used in a workflow, it will export `hello world!` as the return value for the step. ```javascript export default { @@ -96,7 +96,7 @@ To test the action: 3. Click the **+** button to add a step to your workflow 4. Click on **My Actions** and then select the **Action Demo** action to add it to your workflow. - ![image-20210411165325045](https://res.cloudinary.com/pipedreamin/image/upload/v1618550730/docs/components/image-20210411165325045_ia5sd5.png) + ![Click on the My Actions button to show all of your privately published actions](/images/components/v3/using-private-actions.png) 5. Deploy your workflow 6. Click **RUN NOW** to execute your workflow and action @@ -109,7 +109,7 @@ Keep the browser tab open. We'll return to this workflow in the rest of the exam ### hello [name]! -Next, let's update the component to capture some user input. First, add a `string` [prop](/components/api/#props) called `name` to the component. +Next, let's update the component to capture some user input. First, add a `string` [prop](/workflows/contributing/components/api/#props) called `name` to the component. ```java export default { @@ -221,7 +221,7 @@ export default { }; ``` - + To use most npm packages on Pipedream, just `import` or `require` them — there is no `package.json` or `npm install` required. @@ -340,7 +340,7 @@ export default { }; ``` -Then add an [app prop](/components/api/#app-props) to use Pipedream managed auth with this component. For this example, we'll add an app prop for Github: +Then add an [app prop](/workflows/contributing/components/api/#app-props) to use Pipedream managed auth with this component. For this example, we'll add an app prop for Github: ```javascript import { Octokit } from "@octokit/rest"; @@ -361,7 +361,7 @@ export default { }; ``` - + The value for the `app` property is the name slug for the app in Pipedream. This is not currently discoverable, but it will be in the near future on app pages in the [Pipedream Marketplace](https://pipedream.com/explore). For the time being, if you want to know how to reference an app, please please [reach out](https://pipedream.com/community). @@ -397,7 +397,7 @@ export default { }; ``` -In order to help users understand what's happening with each action step, we recommend surfacing a brief summary with `$summary` ([read more](/components/api/#actions) about exporting data using `$.export`). +In order to help users understand what's happening with each action step, we recommend surfacing a brief summary with `$summary` ([read more](/workflows/contributing/components/api/#actions) about exporting data using `$.export`). ```javascript import { Octokit } from "@octokit/rest"; @@ -486,6 +486,6 @@ Select an existing account or connect a new one, and then deploy your workflow a ## What's Next? -You're ready to start authoring and publishing actions on Pipedream! You can also check out the [detailed component reference](/components/api/#component-api) at any time! +You're ready to start authoring and publishing actions on Pipedream! You can also check out the [detailed component reference](/workflows/contributing/components/api/#component-api) at any time! If you have any questions or feedback, please [reach out](https://pipedream.com/community)! diff --git a/docs-v2/pages/workflows/contributing/components/api.mdx b/docs-v2/pages/workflows/contributing/components/api.mdx new file mode 100644 index 0000000000000..4177c1d6f2923 --- /dev/null +++ b/docs-v2/pages/workflows/contributing/components/api.mdx @@ -0,0 +1,1212 @@ +import Callout from '@/components/Callout' + +# Component API Reference + + +Our TypeScript component API is in **beta**. If you're interested in developing TypeScript components and providing feedback, [see our TypeScript docs](/workflows/contributing/components/typescript/). + + +This document was created to help developers author and use [Pipedream components](/workflows/contributing/components/). Not only can you develop [sources](/workflows/contributing/components/quickstart/nodejs/sources/) (workflow triggers) and [actions](/workflows/contributing/components/quickstart/nodejs/actions/) using the component API, but you can also develop [Node.js steps](/workflows/building-workflows/code/nodejs/) right in your workflows - without leaving your browser! You can publish components to your account for private use, or [contribute them to the Pipedream registry](/workflows/contributing/) for anyone to run. + +While sources and actions share the same core component API, they differ in both how they're used and written, so certain parts of the component API apply only to one or the other. [This section of the docs](#differences-between-sources-and-actions) explains the core differences. When this document uses the term "component", the corresponding feature applies to both sources and actions. If a specific feature applies to only sources _or_ actions, the correct term will be used. + +If you have any questions about component development, please reach out [in our community](https://pipedream.com/community/c/dev/11). + +## Overview + +### What is a component? + +Components are Node.js modules that run on Pipedream's serverless infrastructure. + +- Trigger Node.js code on HTTP requests, timers, cron schedules, or manually +- Emit data on each event to inspect it. Trigger Pipedream hosted workflows or access it outside of Pipedream via API +- Accept user input on deploy via [CLI](/workflows/cli/reference/#pd-deploy), [API](/workflows/rest-api), or [UI](https://pipedream.com/sources) +- Connect to [{process.env.PUBLIC_APPS}+ apps](https://pipedream.com/apps) using Pipedream managed auth +- Use most npm packages with no `npm install` or `package.json` required +- Store and retrieve state using the [built-in key-value store](#db) + +### Quickstarts + +To help you get started, we created a step-by-step walkthrough for developing both [sources](/workflows/contributing/components/quickstart/nodejs/sources/) and [actions](/workflows/contributing/components/quickstart/nodejs/actions/). We recommend starting with those docs and using the API reference below as you develop. + +### Differences between sources and actions + +Sources and actions share the same component API. However, certain features of the API only apply to one or the other: + +- Actions are defined with `type: action` ([see the docs on the `type` property](#component-structure)). Sources require no `type` property be set. Components without a `type` are considered sources. + +- Sources emit events [using `this.$emit`](#emit), which trigger linked workflows. Any features associated with emitting events (e.g., [dedupe strategies](#dedupe-strategies)) can only be used with sources. Actions [return data using `return` or `$.export`](#returning-data-from-steps), which is made available to future steps of the associated workflow. + +- Sources have access to [lifecycle hooks](#lifecycle-hooks), which are often required to configure the source to listen for new events. Actions do not have access to these lifecycle hooks. + +- Actions have access to [a special `$` variable](#actions), passed as a parameter to the `run` method. This variable exposes functions that allow you to send data to destinations, export data from the action, return HTTP responses, and more. + +- Sources can be developed iteratively using `pd dev`. Actions currently cannot (please follow [this issue](https://github.com/PipedreamHQ/pipedream/issues/1437) to be notified of updates). + +- You use `pd deploy` to deploy sources to your account. You use `pd publish` to publish actions, making them available for use in workflows. + +- You can attach [interfaces](#interface-props) (like HTTP endpoints, or timers) to sources. This defines how the source is invoked. Actions do not have interfaces, since they're run step-by-step as a part of the associated workflow. + +### Getting Started with the CLI + +Several examples below use the Pipedream CLI. To install it, [follow the instructions for your OS / architecture](/workflows/cli/install/). + +See the [CLI reference](/workflows/cli/reference/) for detailed usage and examples beyond those covered below. + +### Example Components + +You can find hundreds of example components in the `components/` directory of the [`PipedreamHQ/pipedream` repo](https://github.com/PipedreamHQ/pipedream). + +## Component API + +### Component Structure + +Pipedream components export an object with the following properties: + +```javascript +export default { + name: "", + key: "", + type: "", + version: "", + description: "", + props: {}, + methods: {}, + hooks: { + async activate() {}, + async deactivate() {}, + async deploy() {}, + }, + dedupe: "", + async run(event) { + this.$emit(event); + }, +}; +``` + +| Property | Type | Required? | Description | +| ------------- | -------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | `string` | required | The name of the component, a string which identifies components deployed to users' accounts. This name will show up in the Pipedream UI, in CLI output (for example, from `pd list` commands), etc. It will also be converted to a unique slug on deploy to reference a specific component instance (it will be auto-incremented if not unique within a user account). | +| `key` | `string` | recommended | The `key` uniquely identifies a component within a namespace. The default namespace for components is your account.

When publishing components to the Pipedream registry, the `key` must be unique across registry components and should follow the pattern:

`app_name_slug`-`slugified-component-name` | +| `type` | `string` | required | When publishing an action, `type: "action"` is required. When publishing a source, use `type: "source"`. | +| `version` | `string` | required | The component version. There are no constraints on the version, but [semantic versioning](https://semver.org/) is required for any components published to the [Pipedream registry](/workflows/contributing/components/guidelines/). | +| `description` | `string` | recommended | The description will appear in the Pipedream UI to aid in discovery and to contextualize instantiated components | +| `props` | `object` | optional | [Props](#props) are custom attributes you can register on a component. When a value is passed to a prop attribute, it becomes a property on that component instance. You can reference these properties in component code using `this` (e.g., `this.propName`). | +| `methods` | `object` | optional | Define component methods for the component instance. They can be referenced via `this` (e.g., `this.methodName()`). | +| `hooks` | `object` | optional (sources only) | [Hooks](#hooks) are functions that are executed when specific component lifecycle events occur. | +| `dedupe` | `string` | optional (sources only) | You may specify a [dedupe strategy](#dedupe-strategies) to be applied to emitted events | +| `run` | `method` | required | Each time a component is invoked (for example, via HTTP request), [its `run` method](#run) is called. The event that triggered the component is passed to `run`, so that you can access it within the method. Events are emitted using `this.$emit()`. | + +### Props + +Props are custom attributes you can register on a component. When a value is passed to a prop attribute, it becomes a property on that component instance. You can reference these properties in component code using `this` (e.g., `this.propName`). + +| Prop Type | Description | +| ------------------------------- | --------------------------------------------------------------------------------------------- | +| [User Input](#user-input-props) | Enable components to accept input on deploy | +| [Interface](#interface-props) | Attaches a Pipedream interface to your component (e.g., an HTTP interface or timer) | +| [Service](#service-props) | Attaches a Pipedream service to your component (e.g., a key-value database to maintain state) | +| [App](#app-props) | Enables managed auth for a component | +| [Data Store](/workflows/data-management/data-stores/#using-data-stores-in-code-steps) | Provides access to a Pipedream [data store](/workflows/data-management/data-stores/) | +| [HTTP Request](#http-request-prop)| Enables components to execute HTTP requests based on user input | +| [Alert](#alert-prop)| Renders an informational alert in the prop form to help users configure the source or action | + +#### User Input Props + +User input props allow components to accept input on deploy. When deploying a component, users will be prompted to enter values for these props, setting the behavior of the component accordingly. + +##### General + +**Definition** + +```javascript +props: { + myPropName: { + type: "", + label: "", + description: "", + options: [], // OR async options() {} to return dynamic options + optional: true || false, + propDefinition: [], + default: "", + secret: true || false, + min: , + max: , + disabled: true || false, + hidden: true || false + }, +}, +``` + +| Property | Type | Required? | Description | +| ---------------- | ------------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `type` | `string` | required | Value must be set to a valid `PropType` (see below). Suffix with `[]` (e.g. `string[]`) to denote array of that type (if supported). | +| `label` | `string` | optional | A friendly label to show to user for this prop. If a label is not provided, the `propName` is displayed to the user. | +| `description` | `string` | optional | Displayed near the prop input. Typically used to contextualize the prop or provide instructions to help users input the correct value. Markdown is supported. | +| `options` | `string[]` or `object[]` or `method` | optional | Provide an array to display options to a user in a drop down menu.
 
**`[]` Basic usage**
Array of strings. E.g.,
`['option 1', 'option 2']`
 
**`object[]` Define Label and Value**
`[{ label: 'Label 1', value: 'label1'}, { label: 'Label 2', value: 'label2'}]`
 
**`method` Dynamic Options**
You can generate options dynamically (e.g., based on real-time API requests with pagination). See configuration details below. | +| `useQuery` | `boolean` | optional | Use in conjunction with **Dynamic Options**. If set to `true`, the prop accepts a real-time query that can be used by the `options` method to obtain results according to that query. | +| `optional` | `boolean` | optional | Set to `true` to make this prop optional. Defaults to `false`. | +| `propDefinition` | `[]` | optional | Re-use a prop defined in an app file. When you include a prop definition, the prop will inherit values for all the properties listed here. However, you can override those values by redefining them for a given prop instance. See **propDefinitions** below for usage. | +| `default` | `string` | optional | Define a default value if the field is not completed. Can only be defined for optional fields (required fields require explicit user input). | +| `secret` | `boolean` | optional | If set to `true`, this field will hide your input in the browser like a password field, and its value will be encrypted in Pipedream's database. The value will be decrypted when the component is run in [the execution environment](/privacy-and-security/#execution-environment). Defaults to `false`. Only allowed for `string` props. | +| `min` | `integer` | optional | Minimum allowed integer value. Only allowed for `integer` props.. | +| `max` | `integer` | optional | Maximum allowed integer value . Only allowed for `integer` props. | +| `disabled` | `boolean` | optional | Set to `true` to disable usage of this prop. Defaults to `false`. | +| `hidden` | `boolean` | optional | Set to `true` to hide this field. Defaults to `false`. | + +**Prop Types** + +| Prop Type | Array Supported | Supported in Sources? | Supported in Actions? | Custom properties | +| ------------------- | --------------- | --------------------- | --------------------- | :---------------------------------------------------------------------------------------------------------- | +| `app` | | ✓ | ✓ | See [App Props](#app-props) below | +| `boolean` | ✓ | ✓ | ✓ | +| `integer` | ✓ | ✓ | ✓ | - `min` (`integer`): Minimum allowed integer value.
- `max` (`integer`): Maximum allowed integer value. | +| `string` | ✓ | ✓ | ✓ | - `secret` (`boolean`): Whether to treat the value as a secret. | +| `object` | | ✓ | ✓ | +| `any` | | | ✓ | +| `$.interface.http` | | ✓ | | +| `$.interface.timer` | | ✓ | | +| `$.service.db` | | ✓ | | +| `data_store` | | | ✓ | +| `http_request` | | | ✓ | +| `alert` | | ✓ | ✓ | See [Alert Prop](#alert-prop) below + +**Usage** + +| Code | Description | Read Scope | Write Scope | +| ----------------- | ---------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------- | +| `this.myPropName` | Returns the configured value of the prop | `run()` `hooks` `methods` | n/a (input props may only be modified on component deploy or update via UI, CLI or API) | + +**Example** + +Following is an example source that demonstrates how to capture user input via a prop and emit it on each event: + +```javascript +export default { + name: "User Input Prop Example", + version: "0.1", + props: { + msg: { + type: "string", + label: "Message", + description: "Enter a message to `console.log()`", + }, + }, + async run() { + this.$emit(this.msg); + }, +}; +``` + +To see more examples, explore the [curated components in Pipedream's GitHub repo](#example-components). + +##### Advanced Configuration + +##### Async Options ([example](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/github.app.mjs)) + +Async options allow users to select prop values that can be programmatically-generated (e.g., based on a real-time API response). + +```javascript +async options({ + page, + prevContext, + query, +}) {}, +``` + +| Property | Type | Required? | Description | +| ------------- | --------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `options()` | `method` | optional | Typically returns an array of values matching the prop type (e.g., `string`) or an array of object that define the `label` and `value` for each option. The `page` and `prevContext` input parameter names are reserved for pagination (see below).
 
When using `prevContext` for pagination, it must return an object with an `options` array and a `context` object with a `nextPageToken` key. E.g., `{ options, context: { nextPageToken }, }` | +| `page` | `integer` | optional | Returns a `0` indexed page number. Use with APIs that accept a numeric page number for pagination. | +| `prevContext` | `string` | optional | Returns a string representing the context for the previous `options` execution. Use with APIs that accept a token representing the last record for pagination. | +| `query` | `string` | optional | Returns a string with the user input if the prop has the `useQuery` property set to `true`. Use with APIs that return items based on a query or search parameter. | + +Following is an example source demonstrating the usage of async options: + +```javascript +export default { + name: "Async Options Example", + version: "0.1", + props: { + msg: { + type: "string", + label: "Message", + description: "Select a message to `console.log()`", + async options() { + // write any node code that returns a string[] or object[] (with label/value keys) + return ["This is option 1", "This is option 2"]; + }, + }, + }, + async run() { + this.$emit(this.msg); + }, +}; +``` + +##### Prop Definitions ([example](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/new-commit/new-commit.mjs)) + +Prop definitions enable you to reuse props that are defined in another object. A common use case is to enable re-use of props that are defined for a specific app. + +```javascript +props: { + myPropName: { + propDefinition: [ + app, + "propDefinitionName", + inputValues + ] + }, +}, + +``` + +| Property | Type | Required? | Description | +| -------------------- | -------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `propDefinition` | `array` | optional | An array of options that define a reference to a `propDefinitions` within the `propDefinitions` for an `app` | +| `app` | `object` | required | An app object | +| `propDefinitionName` | `string` | required | The name of a specific `propDefinition` defined in the corresponding `app` object | +| `inputValues` | `object` | optional | Values to pass into the prop definition. To reference values from previous props, use an arrow function. E.g.,:
 
`c => ({ variableName: c.previousPropName })`

[See these docs](#referencing-values-from-previous-props) for more information. | + +Following is an example source that demonstrates how to use `propDefinitions`. + +```javascript +const rss = { + type: "app", + app: "rss", + propDefinitions: { + urlDef: { + type: "string", + label: "RSS URL", + description: "Enter a URL for an RSS feed.", + }, + }, +}; + +export default { + name: "Prop Definition Example", + description: `This component captures an RSS URL and logs it`, + version: "0.1", + props: { + rss, + url: { propDefinition: [rss, "urlDef"] }, + }, + async run() { + console.log(this.url); + }, +}; +``` + +##### Referencing values from previous props + +When you define a prop in an app file, and that prop depends on the value of another prop, you'll need to pass the value of the previous props in a special way. Let's review an example from [Trello](https://trello.com), a task manager. + +You create Trello _boards_ for new projects. Boards contain _lists_. For example, this **Active** board contains two lists: + +![Trello board example](/images/components/trello-board-example.png) + +In Pipedream, users can choose from lists on a specific board: + +![Trello board and lists props](/images/components/trello-props.png) + +Both **Board** and **Lists** are defined in the Trello app file: + +```javascript +board: { + type: "string", + label: "Board", + async options(opts) { + const boards = await this.getBoards(this.$auth.oauth_uid); + const activeBoards = boards.filter((board) => board.closed === false); + return activeBoards.map((board) => { + return { label: board.name, value: board.id }; + }); + }, +}, +lists: { + type: "string[]", + label: "Lists", + optional: true, + async options(opts) { + const lists = await this.getLists(opts.board); + return lists.map((list) => { + return { label: list.name, value: list.id }; + }); + }, +} +``` + +In the `lists` prop, notice how `opts.board` references the board. You can pass `opts` to the prop's `options` method when you reference `propDefinitions` in specific components: + +```javascript +board: { propDefinition: [trello, "board"] }, +lists: { + propDefinition: [ + trello, + "lists", + (configuredProps) => ({ board: configuredProps.board }), + ], +}, +``` + +`configuredProps` contains the props the user previously configured (the board). This allows the `lists` prop to use it in the `options` method. + +##### Dynamic props + +Some prop definitions must be computed dynamically, after the user configures another prop. We call these **dynamic props**, since they are rendered on-the-fly. This technique is used in [the Google Sheets **Add Single Row** action](https://github.com/PipedreamHQ/pipedream/blob/master/components/google_sheets/actions/add-single-row/add-single-row.mjs), which we'll use as an example below. + +First, determine the prop whose selection should render dynamic props. In the Google Sheets example, we ask the user whether their sheet contains a header row. If it does, we display header fields as individual props: + +![Google Sheets Additional props example - header columns loading as props](https://res.cloudinary.com/pipedreamin/image/upload/v1654129371/docs/additional-props_lx5jtv.gif) + +To load dynamic props, the header prop must have the `reloadProps` field set to `true`: + +```javascript +hasHeaders: { + type: "string", + label: "Does the first row of the sheet have headers?", + description: "If the first row of your document has headers we'll retrieve them to make it easy to enter the value for each column.", + options: [ + "Yes", + "No", + ], + reloadProps: true, +}, +``` + +When a user chooses a value for this prop, Pipedream runs the `additionalProps` component method to render props: + +```javascript +async additionalProps() { + const sheetId = this.sheetId?.value || this.sheetId; + const props = {}; + if (this.hasHeaders === "Yes") { + const { values } = await this.googleSheets.getSpreadsheetValues(sheetId, `${this.sheetName}!1:1`); + if (!values[0]?.length) { + throw new ConfigurationError("Cound not find a header row. Please either add headers and click \"Refresh fields\" or adjust the action configuration to continue."); + } + for (let i = 0; i < values[0]?.length; i++) { + props[`col_${i.toString().padStart(4, "0")}`] = { + type: "string", + label: values[0][i], + optional: true, + }; + } + } else if (this.hasHeaders === "No") { + props.myColumnData = { + type: "string[]", + label: "Values", + description: "Provide a value for each cell of the row. Google Sheets accepts strings, numbers and boolean values for each cell. To set a cell to an empty value, pass an empty string.", + }; + } + return props; +}, +``` + +The signature of this function is: + +```javascript +async additionalProps(previousPropDefs) +``` + +where `previousPropDefs` are the full set of props (props merged with the previous `additionalProps`). When the function is executed, `this` is bound similar to when the `run` function is called, where you can access the values of the props as currently configured, and call any `methods`. The return value of `additionalProps` will replace any previous call, and that return value will be merged with props to define the final set of props. + +Following is an example that demonstrates how to use `additionalProps` to dynamically change a prop's `disabled` and `hidden` properties: + +```javascript +async additionalProps(previousPropDefs) { + if (this.myCondition === "Yes") { + previousPropDefs.myPropName.disabled = true; + previousPropDefs.myPropName.hidden = true; + } else { + previousPropDefs.myPropName.disabled = false; + previousPropDefs.myPropName.hidden = false; + } + return previousPropDefs; +}, +``` + +Dynamic props can have any one of the following prop types: + +- `app` +- `boolean` +- `integer` +- `string` +- `object` +- `any` +- `$.interface.http` +- `$.interface.timer` +- `data_store` +- `http_request` + +#### Interface Props + +Interface props are infrastructure abstractions provided by the Pipedream platform. They declare how a source is invoked — via HTTP request, run on a schedule, etc. — and therefore define the shape of the events it processes. + +| Interface Type | Description | +| --------------- | --------------------------------------------------------------- | +| [Timer](#timer) | Invoke your source on an interval or based on a cron expression | +| [HTTP](#http) | Invoke your source on HTTP requests | + +#### Timer + +To use the timer interface, declare a prop whose value is the string `$.interface.timer`: + +**Definition** + +```javascript +props: { + myPropName: { + type: "$.interface.timer", + default: {}, + }, +} +``` + +| Property | Type | Required? | Description | +| --------- | -------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `type` | `string` | required | Must be set to `$.interface.timer` | +| `default` | `object` | optional | **Define a default interval**
`{ intervalSeconds: 60, },`
 
**Define a default cron expression**
`{ cron: "0 0 * * *", },` | + +**Usage** + +| Code | Description | Read Scope | Write Scope | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------- | ------------------------------------------------------------------------------------------- | +| `this.myPropName` | Returns the type of interface configured (e.g., `{ type: '$.interface.timer' }`) | `run()` `hooks` `methods` | n/a (interface props may only be modified on component deploy or update via UI, CLI or API) | +| `event` | Returns an object with the execution timestamp and interface configuration (e.g., `{ "timestamp": 1593937896, "interval_seconds": 3600 }`) | `run(event)` | n/a (interface props may only be modified on source deploy or update via UI, CLI or API) | + +**Example** + +Following is a basic example of a source that is triggered by a `$.interface.timer` and has default defined as a cron expression. + +```javascript +export default { + name: "Cron Example", + version: "0.1", + props: { + timer: { + type: "$.interface.timer", + default: { + cron: "0 0 * * *", // Run job once a day + }, + }, + }, + async run() { + console.log("hello world!"); + }, +}; +``` + +Following is an example source that's triggered by a `$.interface.timer` and has a `default` interval defined. + +```javascript +export default { + name: "Interval Example", + version: "0.1", + props: { + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: 60 * 60 * 24, // Run job once a day + }, + }, + }, + async run() { + console.log("hello world!"); + }, +}; +``` + +##### HTTP + +To use the HTTP interface, declare a prop whose value is the string `$.interface.http`: + +```javascript +props: { + myPropName: { + type: "$.interface.http", + customResponse: true, // optional: defaults to false + }, +} +``` + +**Definition** + +| Property | Type | Required? | Description | +| --------- | -------- | --------- | ------------------------------------------------------------------------------------------------------------ | +| `type` | `string` | required | Must be set to `$.interface.http` | +| `respond` | `method` | required | The HTTP interface exposes a `respond()` method that lets your component issue HTTP responses to the client. | + +**Usage** + +| Code | Description | Read Scope | Write Scope | +| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `this.myPropName` | Returns an object with the unique endpoint URL generated by Pipedream (e.g., `{ endpoint: 'https://abcde.m.pipedream.net' }`) | `run()` `hooks` `methods` | n/a (interface props may only be modified on source deploy or update via UI, CLI or API) | +| `event` | Returns an object representing the HTTP request (e.g., `{ method: 'POST', path: '/', query: {}, headers: {}, bodyRaw: '', body: {}, }`) | `run(event)` | The shape of `event` corresponds with the the HTTP request you make to the endpoint generated by Pipedream for this interface | +| `this.myPropName.respond()` | Returns an HTTP response to the client (e.g., `this.http.respond({status: 200})`). | n/a | `run()` | + +###### Responding to HTTP requests + +The HTTP interface exposes a `respond()` method that lets your source issue HTTP responses. You may run `this.http.respond()` to respond to the client from the `run()` method of a source. In this case you should also pass the `customResponse: true` parameter to the prop. + +| Property | Type | Required? | Description | +| --------- | -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ | +| `status` | `integer` | required | An integer representing the HTTP status code. Return `200` to indicate success. Standard status codes range from `100` - `599` | +| `headers` | `object` | optional | Return custom key-value pairs in the HTTP response | +| `body` | `string` `object` `buffer` | optional | Return a custom body in the HTTP response. This can be any string, object, or Buffer. | + +###### HTTP Event Shape + +Following is the shape of the event passed to the `run()` method of your source: + +```javascript +{ + method: 'POST', + path: '/', + query: {}, + headers: {}, + bodyRaw: '', + body: +} +``` + +**Example** + +Following is an example source that's triggered by `$.interface.http` and returns `{ 'msg': 'hello world!' }` in the HTTP response. On deploy, Pipedream will generate a unique URL for this source: + +```javascript +export default { + name: "HTTP Example", + version: "0.0.1", + props: { + http: { + type: "$.interface.http", + customResponse: true, + }, + }, + async run(event) { + this.http.respond({ + status: 200, + body: { + msg: "hello world!", + }, + headers: { + "content-type": "application/json", + }, + }); + console.log(event); + }, +}; +``` + +#### Service Props + +| Service | Description | +| ------- | ---------------------------------------------------------------------------------------------------- | +| _DB_ | Provides access to a simple, component-specific key-value store to maintain state across executions. | + +##### DB + +**Definition** + +```javascript +props: { + myPropName: "$.service.db", +} +``` + +**Usage** + +| Code | Description | Read Scope | Write Scope | +| ----------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------- | -------------------------------------- | +| `this.myPropName.get('key')` | Method to get a previously set value for a key. Returns `undefined` if a key does not exist. | `run()` `hooks` `methods` | Use the `set()` method to write values | +| `this.myPropName.set('key', value)` | Method to set a value for a key. Values must be JSON-serializable data. | Use the `get()` method to read values | `run()` `hooks` `methods` | + +#### App Props + +App props are normally defined in an [app file](/workflows/contributing/components/guidelines/#app-files), separate from individual components. See [the `components/` directory of the pipedream GitHub repo](https://github.com/PipedreamHQ/pipedream/tree/master/components) for example app files. + +**Definition** + +```javascript +props: { + myPropName: { + type: "app", + app: "", + propDefinitions: {} + methods: {}, + }, +}, +``` + +| Property | Type | Required? | Description | +| ----------------- | -------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `type` | `string` | required | Value must be `app` | +| `app` | `string` | required | Value must be set to the name slug for an app registered on Pipedream. [App files](/workflows/contributing/components/guidelines/#app-files) are programmatically generated for all integrated apps on Pipedream. To find your app's slug, visit the `components` directory of [the Pipedream GitHub repo](https://github.com/PipedreamHQ/pipedream/tree/master/components), find the app file (the file that ends with `.app.mjs`), and find the `app` property at the root of that module. If you don't see an app listed, please [open an issue here](https://github.com/PipedreamHQ/pipedream/issues/new?assignees=&labels=app%2C+enhancement&template=app---service-integration.md&title=%5BAPP%5D). | +| `propDefinitions` | `object` | optional | An object that contains objects with predefined user input props. See the section on User Input Props above to learn about the shapes that can be defined and how to reference in components using the `propDefinition` property | +| `methods` | `object` | optional | Define app-specific methods. Methods can be referenced within the app object context via `this` (e.g., `this.methodName()`) and within a component via `this.myAppPropName` (e.g., `this.myAppPropName.methodName()`). | + +**Usage** + +| Code | Description | Read Scope | Write Scope | +| --------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------- | ----------- | +| `this.$auth` | Provides access to OAuth tokens and API keys for Pipedream managed auth | **App Object:** `methods` | n/a | +| `this.myAppPropName.$auth` | Provides access to OAuth tokens and API keys for Pipedream managed auth | **Parent Component:** `run()` `hooks` `methods` | n/a | +| `this.methodName()` | Execute a common method defined for an app within the app definition (e.g., from another method) | **App Object:** `methods` | n/a | +| `this.myAppPropName.methodName()` | Execute a common method defined for an app from a component that includes the app as a prop | **Parent Component:** `run()` `hooks` `methods` | n/a | + +> **Note:** The specific `$auth` keys supported for each app will be published in the near future. + +#### HTTP Request Prop + +**Usage** + +| Code | Description | Read Scope | Write Scope | +| --------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------- | ----------- | +| `this.myPropName.execute()` | Execute an HTTP request as configured | n/a | `run()` `methods` | + +**Example** + +Following is an example action that demonstrates how to accept an HTTP request configuration as input and execute the request when the component is run: + +```javascript +export default { + name: "HTTP Request Example", + version: "0.0.1", + props: { + httpRequest: { + type: "http_request", + label: "API Request", + default: { + method: "GET", + url: "https://jsonplaceholder.typicode.com/posts", + } + }, + }, + async run() { + const { data } = await this.httpRequest.execute(); + return data; + }, +}; +``` + +For more examples, see the [docs on making HTTP requests with Node.js](/workflows/building-workflows/code/nodejs/http-requests/#send-a-get-request-to-fetch-data). + + +#### Alert Prop + +Sometimes you may need to surface contextual information to users within the prop form. This might be information that's not directly related to a specific prop, so it doesn't make sense to include in a prop description, but rather, it may be related to the overall configuration of the prop form. + +**Usage** + +| Property | Type | Required? | Description | +| - | - | - | - | +| `type` | `string` | required | Set to `alert` | +| `alertType` | `string` | required | Determines the color and UI presentation of the alert prop. Can be one of `info`, `neutral`, `warning`, `error`. | +| `content` | `string` | required | Determines the text that is rendered in the alert. Both plain text and markdown are supported. | + +```javascript +export default defineComponent({ + props: { + alert: { + type: "alert", + alertType: "info", + content: "Admin rights on the repo are required in order to register webhooks. In order to continue setting up your source, configure a polling interval below to check for new events.", + } + }, +}) +``` + +Refer to GitHub's component sources in the `pipedream` repo for an [example implementation](https://github.com/PipedreamHQ/pipedream/blob/b447d71f658d10d6a7432e8f5153bbda56ba9810/components/github/sources/common/common-flex.mjs#L27). + +![Info alert prop in GitHub source](/images/components/info-alert-prop-github.png) + +#### Limits on props + +When a user configures a prop with a value, it can hold at most {process.env.CONFIGURED_PROPS_SIZE_LIMIT} data. Consider this when accepting large input in these fields (such as a base64 string). + +The {process.env.CONFIGURED_PROPS_SIZE_LIMIT} limit applies only to static values entered as raw text. In workflows, users can pass expressions (referencing data in a prior step). In that case the prop value is simply the text of the expression, for example `{{steps.nodejs.$return_value}}`, well below the limit. The value of these expressions is evaluated at runtime, and are subject to [different limits](/workflows/limits/). + +### Methods + +You can define helper functions within the `methods` property of your component. You have access to these functions within the [`run` method](#run), or within other methods. + +Methods can be accessed using `this.`. For example, a `random` method: + +```javascript +methods: { + random() { + return Math.random() + }, +} +``` + +can be run like so: + +```javascript +const randomNum = this.random(); +``` + +### Hooks + +```javascript +hooks: { + async deploy() {}, + async activate() {}, + async deactivate() {}, +}, +``` + +| Property | Type | Required? | Description | +| ------------ | -------- | --------- | ----------------------------------------------------- | +| `deploy` | `method` | optional | Executed each time a component is deployed | +| `activate` | `method` | optional | Executed each time a component is deployed or updated | +| `deactivate` | `method` | optional | Executed each time a component is deactivated | + +### Dedupe Strategies + +> **IMPORTANT:** To use a dedupe strategy, you must emit an `id` as part of the event metadata (dedupe strategies are applied to the submitted `id`) + +| Strategy | Description | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `unique` | Pipedream maintains a cache of 100 emitted `id` values. Events with `id` values that are not in the cache are emitted, and the `id` value is added to the cache. After 100 events, `id` values are purged from the cache based on the order received (first in, first out). A common use case for this strategy is an RSS feed which typically does not exceed 100 items | +| `greatest` | Pipedream caches the largest `id` value (must be numeric). Only events with larger `id` values are emitted, and the cache is updated to match the new, largest value.. | +| `last` | Pipedream caches the ID associated with the last emitted event. When new events are emitted, only events after the matching `id` value will be emitted as events. If no `id` values match, then all events will be emitted. | + +### Run + +Each time a component is invoked, its `run` method is called. Sources are invoked by their [interface](#interface-props) (for example, via HTTP request). Actions are run when their parent workflow is triggered. + +You can reference `this` within the `run` method. `this` refers to the component, and provides access to [props](#props), [methods](#methods), and more. + +#### Sources + +When a source is invoked, the event that triggered the source is passed to `run`, so that you can access it within the method: + +```javascript +async run(event) { + console.log(event) +} +``` + +##### \$emit + +`this.$emit()` is a method in scope for the `run` method of a source + +```javascript +this.$emit(event, { + id, + name, + summary, + ts, +}); +``` + +| Property | Type | Required? | Description | +| --------- | ---------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `event` | JSON serializable data | optional | The data to emit as the event | +| `id` | `string` or `number` | Required if a dedupe strategy is applied | A value to uniquely identify this event. Common `id` values may be a 3rd party ID, a timestamp, or a data hash | +| `name` | `string` | optional | The name of the "channel" you'd like to emit the event to. By default, events are emitted to the `default` channel. If you set a different channel here, listening sources or workflows can subscribe to events on this channel, running the source or workflow only on events emitted to that channel. | +| `summary` | `string` | optional | Define a summary to customize the data displayed in the events list to help differentiate events at a glance | +| `ts` | `integer` | optional | Accepts an epoch timestamp in **milliseconds**. If you submit a timestamp, events will automatically be ordered and emitted from oldest to newest. If using the `last` dedupe strategy, the value cached as the `last` event for an execution will correspond to the event with the newest timestamp. | + +Following is a basic example that emits an event on each component execution. + +```javascript +export default { + name: "this.$emit() example", + description: "Deploy and run this component manually via the Pipedream UI", + async run() { + this.$emit({ message: "hello world!" }); + }, +}; +``` + +##### Logs + +You can view logs produced by a source's `run` method in the **Logs** section of the [Pipedream source UI](https://pipedream.com/sources), or using the `pd logs` CLI command: + +```bash +pd logs +``` + +##### Events + +If the `run` method emits events using `this.$emit`, you can access the events in the **EVENTS** section of the Pipedream UI for the component, or using the `pd events` CLI command: + +```bash +pd events +``` + +#### Actions + +When an action is run in a workflow, Pipedream passes an object with a `$` variable that gives you access to special functions, outlined below: + +```javascript +async run({ $ }) { + // You have access to $ within your action +} +``` + +##### Returning data from steps + +By default, variables declared within an action are scoped to that action. To return data from a step, you have two options: 1) use the `return` keyword, or 2) use `$.export` to return a named export from a step. + +**`return`** + +Use `return` to return data from an action: + +```javascript +async run({ $ }) { + return "data" +} +``` + +When you use return, the exported data will appear at `steps.[STEP NAME].$return_value`. For example, if you ran the code above in a step named `nodejs`, you'd reference the returned data using `steps.nodejs.$return_value`. + +**`$.export`** + +You can also use `$.export` to return named exports from an action. `$.export` takes the name of the export as the first argument, and the value to export as the second argument: + +```javascript +async run({ $ }) { + $.export("name", "value") +} +``` + +When your workflow runs, you'll see the named exports appear below your step, with the data you exported. You can reference these exports in other steps using `steps.[STEP NAME].[EXPORT NAME]`. + +##### Returning HTTP responses with `$.respond` + +`$.respond` lets you issue HTTP responses from your workflow. [See the full `$.respond` docs for more information](/workflows/building-workflows/triggers/#customizing-the-http-response). + +```javascript +async run({ $ }) { + $.respond({ + status: 200, + body: "hello, world" + }) +} +``` + +##### Ending steps early with `return $.flow.exit` + +`return $.flow.exit` terminates the entire workflow. It accepts a single argument: a string that tells the workflow why the workflow terminated, which is displayed in the Pipedream UI. + +```javascript +async run({ $ }) { + return $.flow.exit("reason") +} +``` + +##### `$.summary` + +`$.summary` is used to surface brief, user-friendly summaries about what happened when an action step succeeds. For example, when [adding items to a Spotify playlist](https://github.com/PipedreamHQ/pipedream/blob/master/components/spotify/actions/add-items-to-playlist/add-items-to-playlist.mjs#L51): + +![Spotify example with $summary](/images/components/spotify-$summary-example.png) + +Example implementation: + +```javascript +const data = [1, 2]; +const playlistName = "Cool jams"; +$.export( + "$summary", + `Successfully added ${data.length} ${ + data.length == 1 ? "item" : "items" + } to "${playlistName}"` +); +``` + +##### `$.send` + +`$.send` allows you to send data to [Pipedream destinations](/workflows/data-management/destinations/). + +**`$.send.http`** + +[See the HTTP destination docs](/workflows/data-management/destinations/http/#using-sendhttp-in-component-actions). + +**`$.send.email`** + +[See the Email destination docs](/workflows/data-management/destinations/email/#using-sendemail-in-component-actions). + +**`$.send.s3`** + +[See the S3 destination docs](/workflows/data-management/destinations/s3/#using-sends3-in-component-actions). + +**`$.send.emit`** + +[See the Emit destination docs](/workflows/data-management/destinations/emit/#using-sendemit-in-component-actions). + +**`$.send.sse`** + +[See the SSE destination docs](/workflows/data-management/destinations/sse/#using-sendsse-in-component-actions). + +##### `$.context` + +`$.context` exposes [the same properties as `steps.trigger.context`](/workflows/building-workflows/triggers/#stepstriggercontext), and more. Action authors can use it to get context about the calling workflow and the execution. + +All properties from [`steps.trigger.context`](/workflows/building-workflows/triggers/#stepstriggercontext) are exposed, as well as: + +| Property | Description | +| ---------- | :-----------------------------------------------------------------------------------------------------------------------------------------------------: | +| `deadline` | An epoch millisecond timestamp marking the point when the workflow is configured to [timeout](/workflows/limits/#time-per-execution). | +| `JIT` | Stands for "just in time" (environment). `true` if the user is testing the step, `false` if the step is running in production. | +| `run` | An object containing metadata about the current run number. See [the docs on `$.flow.rerun`](/workflows/building-workflows/triggers/#stepstriggercontext) for more detail. | + +### Environment variables + +[Environment variables](/workflows/building-workflows/environment-variables/) are not accessible within sources or actions directly. Since components can be used by anyone, you cannot guarantee that a user will have a specific variable set in their environment. + +In sources, you can use [`secret` props](#props) to reference sensitive data. + +In actions, you'll see a list of your environment variables in the object explorer when selecting a variable to pass to a step: + +### Using npm packages + +To use an npm package in a component, just require it. There is no `package.json` or `npm install` required. + +```javascript +import axios from "axios"; +``` + +When you deploy a component, Pipedream downloads the latest versions of these packages and bundles them with your deployment. + +Some packages that rely on large dependencies or on unbundled binaries — may not work on Pipedream. Please [reach out](https://pipedream.com/support) if you encounter a specific issue. + +#### Referencing a specific version of a package + +_This currently applies only to sources_. + +If you'd like to use a _specific_ version of a package in a source, you can add that version in the `require` string, for example: `require("axios@0.19.2")`. Moreover, you can pass the same version specifiers that npm and other tools allow to specify allowed [semantic version](https://semver.org/) upgrades. For example: + +- To allow for future patch version upgrades, use `require("axios@~0.20.0")` +- To allow for patch and minor version upgrades, use `require("axios@^0.20.0")` + +## Managing Components + +Sources and actions are developed and deployed in different ways, given the different functions they serve in the product. + +- [Managing Sources](#managing-sources) +- [Managing Actions](#managing-actions) + +### Managing Sources + +#### CLI - Development Mode + +--- + +The easiest way to develop and test sources is with the `pd dev` command. `pd dev` deploys a local file, attaches it to a component, and automatically updates the component on each local save. To deploy a new component with `pd dev`, run: + +```bash +pd dev +``` + +To attach to an existing deployed component, run: + +```bash +pd dev --dc +``` + +#### CLI - Deploy + +##### From Local Code + +To deploy a source via CLI, use the `pd deploy` command. + +```bash +pd deploy +``` + +E.g., + +```bash +pd deploy my-source.js +``` + +##### From Pipedream Github Repo + +You can explore the components available to deploy in [Pipedream's GitHub repo](https://github.com/PipedreamHQ/pipedream/tree/master/components). + +```bash +pd deploy +``` + +E.g., + +```bash +pd deploy http-new-requests +``` + +##### From Any URL + +```bash +pd deploy +``` + +E.g., + +```bash +pd deploy https://raw.githubusercontent.com/PipedreamHQ/pipedream/master/components/http/sources/new-requests/new-requests.js +``` + +#### CLI - Update + +View the [CLI command reference](/workflows/cli/reference/#command-reference). + +#### CLI - Delete + +View the [CLI command reference](/workflows/cli/reference/#command-reference). + +#### UI - Deploy + +You can find and deploy curated components at [https://pipedream.com/sources/new](https://pipedream.com/sources/new), or you can deploy code via the UI using following URL patterns. + +##### From Pipedream Github Repo + +```bash +https://pipedream.com/sources?action=create&key= +``` + +E.g., + +```bash +https://pipedream.com/sources?action=create&key=http-new-requests +``` + +##### From Any URL + +```bash +https://pipedream.com/sources?action=create&url= +``` + +E.g., + +```bash +https://pipedream.com/sources?action=create&url=https%3A%2F%2Fraw.githubusercontent.com%2FPipedreamHQ%2Fpipedream%2Fmaster%2Fcomponents%2Fhttp%2Fhttp.js +``` + +#### UI - Update + +You can update the code and props for a component from the **Configuration** tab for a source in the Pipedream UI. + +#### UI - Delete + +You can delete a component via the UI at [https://pipedream.com/sources](https://pipedream.com/sources). + +#### API + +See the [REST API docs](/workflows/building-workflows/rest-api/). + +### Managing Actions + +#### CLI - Publish + +To publish an action, use the `pd publish` command. + +```bash +pd publish FILENAME +``` + +E.g., + +```bash +pd publish my-action.js +``` + +## Source Lifecycle + +### Lifecycle hooks + +Pipedream sources support the following hooks. The code for these hooks are defined within the component. Learn more about the [component structure](#component-structure) and [hook usage](#hooks). + +#### `deploy` + +The `deploy()` hook is automatically invoked by Pipedream when a source is deployed. A common use case for the deploy hook is to create webhook subscriptions when the source is deployed, but you can run any Node.js code within the `deploy` hook. To learn more about the `deploy()` hook, refer to the [API documentation](#hooks). + +#### `activate` + +The `activate()` hook is automatically invoked by Pipedream when a source is deployed or updated. For example, this hook will be run when users update component props, so you can run code here that handles those changes. To learn more about defining a custom `activate()` hook, refer to the [API documentation](#hooks). + +#### `deactivate` + +The `deactivate()` hook is automatically invoked by Pipedream when a source is updated or deleted. A common use case for the deactivate hook is to automatically delete a webhook subscription when a component is deleted, but you can run any Node.js code within the `deactivate` hook. To learn more about the `deactivate()` hook, refer to the [API documentation](#hooks). + +### States + +#### Saved Component + +A saved component is non-instantiated component code that has previously been deployed to Pipedream. Each saved component has a unique saved component ID. Saved components cannot be invoked directly — they must first be deployed. + +#### Deployed Component + +A deployed component is an instance of a saved component that can be invoked. Deployed components can be active or inactive. On deploy, Pipedream instantiates a saved component and invokes the `activate()` hook. + +#### Deleted Component + +On delete, Pipedream invokes the `deactivate()` hook and then deletes the deployed component instance. + +### Operations + +#### Deploy + +On deploy, Pipedream creates an instance of a saved component and invokes the optional `deploy()` and `activate()` hooks. A unique deployed component ID is generated for the component. + +You can deploy a component via the CLI, UI or API. + +#### Update + +On update, Pipedream, invokes the optional `deactivate()` hook, updates the code and props for a deployed component, and then invokes the optional `activate()` hook. The deployed component ID is not changed by an update operation. + +#### Delete + +On delete, Pipedream invokes the optional `deactivate()` hook and deletes the component instance. + +## Source Event Lifecycle + +The event lifecycle applies to deployed sources. Learn about the [source lifecycle](#source-lifecycle). + +### Diagram + +![Pipedream Components Event Lifecycle Diagram](https://res.cloudinary.com/pipedreamin/image/upload/v1683089643/d0iiggokfkwnmt4kckb5.png) + +### Triggering Sources + +Sources are triggered when you manually run them (e.g., via the **RUN NOW** button in the UI) or when one of their [interfaces](#interface-props) is triggered. Pipedream sources currently support **HTTP** and **Timer** interfaces. + +When a source is triggered, the `run()` method of the component is executed. Standard output and errors are surfaced in the **Logs** tab. + +### Emitting Events from Sources + +Sources can emit events via `this.$emit()`. If you define a [dedupe strategy](#dedupe-strategies) for a source, Pipedream automatically dedupes the events you emit. + +> **TIP:** if you want to use a dedupe strategy, be sure to pass an `id` for each event. Pipedream uses this value for deduping purposes. + +### Consuming Events from Sources + +Pipedream makes it easy to consume events via: + +- The UI +- Workflows +- APIs +- CLI + +#### UI + +When you navigate to your source [in the UI](https://pipedream.com/sources), you'll be able to select and inspect the most recent 100 events (i.e., an event bin). For example, if you send requests to a simple HTTP source, you will be able to inspect the events (i.e., a request bin). + +#### Workflows + +[Trigger hosted Node.js workflows](/workflows/building-workflows/) on each event. Integrate with {process.env.PUBLIC_APPS}+ apps including Google Sheets, Discord, Slack, AWS, and more! + +#### API + +Events can be retrieved using the [REST API](/rest-api/) or [SSE stream tied to your component](/workflows/data-management/destinations/sse/). This makes it easy to retrieve data processed by your component from another app. Typically, you'll want to use the [REST API](/rest-api/) to retrieve events in batch, and connect to the [SSE stream](/workflows/data-management/destinations/sse/) to process them in real time. + +#### CLI + +Use the `pd events` command to retrieve the last 10 events via the CLI: + +```bash +pd events -n 10 +``` diff --git a/docs-v2/pages/workflows/contributing/components/guidelines.mdx b/docs-v2/pages/workflows/contributing/components/guidelines.mdx new file mode 100644 index 0000000000000..e02b8c95da32a --- /dev/null +++ b/docs-v2/pages/workflows/contributing/components/guidelines.mdx @@ -0,0 +1,899 @@ +import Callout from '@/components/Callout' + +# Components Guidelines & Patterns + +For a component to be accepted into the Pipedream registry, it should follow +these guidelines below. These guidelines help ensure components are high +quality, are intuitive for both Pipedream users and component developers to use +and extend. + + +Questions about best practices? + +Join the discussion with fellow Pipedream component developers at the +[#contribute channel](https://pipedream-users.slack.com/archives/C01E5KCTR16) in +Slack or [on Discourse](https://pipedream.com/community/c/dev/11). + + + +## Local Checks + +When submitting pull requests, the new code will run through a series of +automated checks like linting the code. If you want to run those checks locally +for quicker feedback you must have [pnpm](https://pnpm.io/) installed and run +the following commands at the root of the project: + +1. To install all the project's dependencies (only needed once): + + ```shell + pnpm install + ``` + +2. To install all required dependencies: + + ```shell + npx pnpm install -r + ``` + +3. To run the linter checks against your code (assuming that your changes are + located at `components/foo` for example): + + ```shell + npx eslint components/foo + ``` + +4. Optionally, you can automatically fix any linter issues by running the + following command: + + ```shell + npx eslint --fix components/foo + ``` + +Keep in mind that not all issues can be automatically fixed by the linter since +they could alter the behaviour of the code. + +## General + +### Components Should Be ES Modules + +The Node.js community has started publishing +[ESM-only](https://flaviocopes.com/es-modules/) packages that do not work with +[CommonJS +modules](https://nodejs.org/docs/latest/api/modules.html#modules_modules_commonjs_modules). +This means you must `import` the package. You can't use `require`. + +You also cannot mix ESM with CJS. This will **not** work: + +```javascript +// ESM +import axios from "axios"; + +// CommonJS - this should be `export default` +module.exports = { + // ... +} +``` + +Therefore, all components should be written as ES modules: + +```javascript +import axios from "axios"; + +export default { + //... +} +``` + +**You'll need to use [the `.mjs` file +extension](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#aside_%E2%80%94_.mjs_versus_.js) +for any components written as ES modules**. + +You'll notice that many of the existing components are written as CommonJS +modules. Please fix these and submit a pull request as you refactor related +code. For example, if you're developing new Spotify actions, and you notice the +existing event sources use CommonJS, change them to ESM: + +1. Rename the file extension from `.js` to `.mjs` using `git mv` (e.g. `git mv + source.js source.mjs`). +2. Change all `require` statements to `import`s. +3. Change instances of `module.exports` to `export default`. + +### Component Scope + +Create components to address specific use cases whenever possible. For example, +when a user subscribes to a Github webhook to listen for “star” activity, events +can be generated when users star or unstar a repository. The “New Star” source +filters events for only new star activity so the user doesn't have to. + +There may be cases where it's valuable to create a generic component that +provides users with broad latitude (e.g., see the [custom +webhook](https://github.com/PipedreamHQ/pipedream/blob/master/components/github/sources/custom-webhook-events) +event source for GitHub). However, as a general heuristic, we found that tightly +scoped components are easier for users to understand and use. + +### Required Metadata + +Registry [components](/workflows/contributing/components/api/#component-structure) require a unique +`key` and `version`, and a friendly `name` and `description`. Action components +require a `type` field to be set to `action` (sources will require a type to be +set in the future). + +```javascript +export default { + key: "google_drive-new-shared-drive", + name: "New Shared Drive", + description: "Emits a new event any time a shared drive is created.", + version: "0.0.1", +}; +``` + +### Component Key Pattern + +When publishing components to the Pipedream registry, the `key` must be unique +across registry components and should follow the pattern: + +`app_name_slug`-`slugified-component-name` + +**Source** keys should use past tense verbs that describe the event that +occurred (e.g., `linear_app-issue-created-instant`). For **action** keys, use +active verbs to describe the action that will occur, (e.g., +`linear_app-create-issue`). + +### Versioning + +When you first publish a component to the registry, set its version to `0.0.1`. + +Pipedream registry components try to follow [semantic +versioning](https://semver.org/). From their site: + +Given a version number `MAJOR.MINOR.PATCH`, increment the: + +1. `MAJOR` version when you make incompatible API changes, +2. `MINOR` version when you add functionality in a backwards compatible manner, + and +3. `PATCH` version when you make backwards compatible bug fixes. + +When you're developing actions locally, and you've incremented the version in +your account multiple times, make sure to set it to the version it should be at +in the registry prior to submitting your PR. For example, when you add an action +to the registry, the version should be `0.0.1`. If the action was at version +`0.1.0` and you've fixed a bug, change it to `0.1.1` when committing your final +code. + +### Folder Structure + +Registry components are organized by app in the `components` directory of the +`pipedreamhq/pipedream` repo. + +```text +/components + /[app-name-slug] + /[app-name-slug].app.js + /actions + /[action-name-slug] + /[action-name-slug].js + /sources + /[source-name-slug] + /[source-name-slug].js +``` + +- The name of each app folder corresponds with the name slug for each app +- The app file should be in the root of the app folder (e.g., + `/components/[app_slug]/[app_slug].app.js`) +- Components for each app are organized into `/sources` and `/actions` + subfolders +- Each component should be placed in its own subfolder (with the name of the + folder and the name of the `js` file equivalent to the slugified component + name). For example, the path for the "Search Mentions" source for Twitter is + `/components/twitter/sources/search-mentions/search-mentions.js`. +- Aside from `app_slug`, words in folder and file names are separated by dashes + (-) (i.e., in kebab case) + +You can explore examples in the [components +directory](https://github.com/PipedreamHQ/pipedream/tree/master/components). + +#### Using APIs vs Client Libraries + +If the app has a well-supported [Node.js client +library](/workflows/contributing/components/api/#using-npm-packages), feel free to use that instead of +manually constructing API requests. + +### `package.json` + +Each app should have a `package.json` in its root folder. If one doesn't exist, +run `npm init` in the app's root folder and customize the file using [this +`package.json`](https://github.com/PipedreamHQ/pipedream/blob/55236b3aa993cbcb545e245803d8654c6358b0a2/components/stripe/package.json) +as a template. + +Each time you change the code for an app file, or change the dependencies for +any app component, modify the package `version`. + +Save any dependencies in the component app directory: + +```bash +npm i --save package +npm i --save-dev package +``` + +#### Error-Handling and Input Validation + +When you use the SDK of a popular API, the SDK might raise clear errors to the +user. For example, if the user is asked to pass an email address, and that email +address doesn't validate, the library might raise that in the error message. + +But other libraries will _not_ raise clear errors. In these cases, you may need +to `throw` your own custom error that wraps the error from the API / lib. [See +the Airtable +components](https://github.com/PipedreamHQ/pipedream/blob/9e4e400cda62335dfabfae384d9224e04a585beb/components/airtable/airtable.app.js#L70) +for an example of custom error-handling and input validation. + +In general, **imagine you are a user troubleshooting an issue. Is the error +easy-to-understand? If not, `throw` a better error**. + +### `README` files + +New actions and sources should include `README.md` files within the same +directory to describe how to use the action or source to users. + +Here's an example `README.md` structure: + +```markdown + +# Overview + +# Example Use Cases + +# Getting Started + +# Troubleshooting + +``` + +These sections will appear within the correponding app, source and action page, +along with any subheadings and content. + +Here's an example of an [app `README.md` within the `discord` component on the +Pipedream +registry](https://github.com/PipedreamHQ/pipedream/blob/master/components/discord/README.md). +That same content is rendered within the [Pipedream integration page for the +Discord app](https://pipedream.com/apps/discord). + +You can add additional subheadings to each of the top level `Overview`, `Example +Use Cases`, `Getting Started` and `Troubleshooting` headings: + +```markdown +# Overview + +## Limitations + +Perhaps there are some limitations about the API that users should know about. + +# Example Use Cases + +1. Sync data in real time +2. Automate tedious actions +3. Introduce A.I. into the workflow + +# Getting Started + +## Generating an API Key + +Instructions on how to generate an API key from within the service's dashboard. + +# Troubleshooting + +## Required OAuth Scopes + +Please take note, you'll need to have sufficient privileges in order to complete +authentication. +``` + + +Only these three top level headings `Overview`, `Getting Starting` and +`Troubleshooting` will appear within the corresponding App Marketplace page. All +other headings will be ignored. + + +#### Pagination + +When making API requests, handle pagination to ensure all data/events are +processed. Moreover, if the underlying account experiences and/or generates too +much data paginating through the entire collection of records, it might cause +out-of-memory or timeout issues (or both!), so as a rule of thumb the pagination +logic should: + +- Be encapsulated as a [generator](https://mzl.la/37z6Sh6) so that the component + can start processing records after the very first API call. As an example, you + can check the [Microsoft OneDrive + methods](https://github.com/PipedreamHQ/pipedream/tree/master/components/microsoft_onedrive/microsoft_onedrive.app.mjs) + to list files. +- Accept a "next token/page/ID" whenever possible, so that API calls do not + retrieve the entire collection of records during every execution but rather + from a recent point in time. The `scanDeltaItems` generator method in the + example above follows this pattern. +- Persist the last page number, token or record ID right after processing, so + that following executions of the component process new records to minimize the + amount of duplicate events, execution time and delayed events. Following the + same Microsoft OneDrive example, check the `processEvent` method [in this + component](https://github.com/PipedreamHQ/pipedream/tree/master/components/microsoft_onedrive/sources/new-file/new-file.mjs) + for an example. + +#### Capturing Sensitive Data + +If users are required to enter sensitive data, always use +[secret](/workflows/contributing/components/api/#general) props. + +### Promoting Reusability + +#### App Files + +App files contain components that declare the app and include prop definitions +and methods that may be reused across components. App files should adhere to the +following naming convention: `[app_name_slug].app.js`. If an app file does not +exist for your app, please [reach +out](https://pipedream.com/community/c/dev/11). + +##### Prop Definitions + +Whenever possible, reuse existing [prop +definitions](/workflows/contributing/components/api/#prop-definitions-example). + +If a prop definition does not exist and you are adding an app-specific prop that +may be reused in future components, add it as a prop definition to the app file. +Prop definitions will also be surfaced for apps the Pipedream marketplace. + +##### Methods + +Whenever possible, reuse [methods](/workflows/contributing/components/api/#methods) defined in the app +file. If you need to use an API for which a method is not defined and it may be +used in future components, define a new method in the app file. + +Use the [JS Docs](https://jsdoc.app/about-getting-started.html) pattern for +lightweight documentation of each method in the app file. Provide a description +and define @params and @returns block tags (with default values if applicable — +e.g., `[foo=bar]`). This data will both help with reusability and will be +surfaced in documentation for apps in the Pipedream marketplace. For example: + +```javascript +export default { + methods: { + /** + * Get the most recently liked Tweets for a user + * + * @params {Object} opts - An object representing the configuration options + * for this method + * @params {String} opts.screenName - The user's Twitter screen name (e.g., + * `pipedream`) + * @params {String} [opts.count=200] - The maximum number of Tweets to + * return + * @params {String} [opts.tweetMode=extended] - Use the default of + * `extended` to return non-truncated Tweets + * @returns {Array} Array of most recent Tweets liked by the specified user + */ + async getLikedTweets(opts = {}) { + const { screenName, count = 200, tweetMode = "extended" } = opts; + const { data } = await this._makeRequest({ + url: "https://api.twitter.com/1.1/favorites/list.json", + params: { + screen_name: screenName, + count, + tweet_mode: tweetMode, + }, + }); + return data; + }, + }, +}; +``` + +#### Testing + +Pipedream does not currently support unit tests to validate that changes to app +files are backwards compatible with existing components. Therefore, if you make +changes to an app file that may impact other sources, you must currently test +potentially impacted components to confirm their functionality is not negatively +affected. We expect to support a testing framework in the future. + +### Common Files (Optional) + +An optional pattern to improve reusability is to use a `common` module to +abstract elements that are used across to multiple components. The trade-off +with this approach is that it increases complexity for end-users who have the +option of customizing the code for components within Pipedream. When using this +approach, the general pattern is: + +- The `.app.js` module contains the logic related to making the actual API calls + (e.g. calling `axios.get`, encapsulate the API URL and token, etc). +- The `common.js` module contains logic and structure that is not specific to + any single component. Its structure is equivalent to a component, except that + it doesn't define attributes such as `version`, `dedupe`, `key`, `name`, etc + (those are specific to each component). It defines the main logic/flow and + relies on calling its methods (which might not be implemented by this + component) to get any necessary data that it needs. In OOP terms, it would be + the equivalent of a base abstract class. +- The component module of each action would inherit/extend the `common.js` + component by setting additional attributes (e.g. `name`, `description`, `key`, + etc) and potentially redefining any inherited methods. + +See [Google +Drive](https://github.com/PipedreamHQ/pipedream/tree/master/components/google_drive) +for an example of this pattern. When using this approach, prop definitions +should still be maintained in the app file. + +Please note that the name `common` is just a convention and depending on each +case it might make sense to name any common module differently. For example, the +[AWS +sources](https://github.com/PipedreamHQ/pipedream/tree/master/components/aws) +contains a `common` directory instead of a `common.js` file, and the directory +contains several modules that are shared between different event sources. + +## Props + +As a general rule of thumb, we should strive to only incorporate the 3-4 most +relevant options from a given API as props. This is not a hard limit, but the +goal is to optimize for usability. We should aim to solve specific use cases as +simply as possible. + +### Labels + +Use [prop](/workflows/contributing/components/api/#user-input-props) labels to customize the name of a +prop or propDefinition (independent of the variable name in the code). The label +should mirror the name users of an app are familiar with; i.e., it should mirror +the equivalent label in the app’s UI. This applies to usage in labels, +descriptions, etc. E.g., the Twitter API property for search keywords is “q”, +but its label is set to “Search Term”. + +### Descriptions + +Include a description for [props](/workflows/contributing/components/api/#user-input-props) if it helps +the user understand what they need to do. Use Markdown as appropriate to improve +the clarity of the description or instructions. When using Markdown: + +- Enclose sample input values in backticks (`` ` ``) +- Refer to other props using **bold** by surrounding with double asterisks (\*) +- Use Markdown links with descriptive text rather than displaying a full URL. +- If the description isn't self-explanatory, link to the API docs of the + relevant method to further clarify how the prop works. When the value of the + prop is complex (for example, an object with many properties), link to the + section of the API docs that include details on this format. Users may pass + values from previous steps using expressions, so they'll need to know how to + structure the input data. + +Examples: + +- The async option to select an Airtable Base is self-explanatory so includes no + description: + + ![image-20210326151557417](https://res.cloudinary.com/pipedreamin/image/upload/v1672810770/ixb3aozdijmz0zfqxmvy.png) + +- The “Search Term” prop for Twitter includes a description that helps the user + understand what values they can enter, with specific values highlighted using + backticks and links to external content. + + ![image-20210326151706682](/images/components/image-20210326151706682.png) + +### Optional vs Required Props + +Use optional [props](/workflows/contributing/components/api/#user-input-props) whenever possible to +minimize the input fields required to use a component. + +For example, the Twitter search mentions source only requires that a user +connect their account and enter a search term. The remaining fields are optional +for users who want to filter the results, but they do not require any action to +activate the source: + +![image-20210326151930885](/images/components/image-20210326151930885.png) + +### Default Values + +Provide [default values](/workflows/contributing/components/api/#user-input-props) whenever possible. +NOTE: the best default for a source doesn’t always map to the default +recommended by the app. For example, Twitter defaults search results to an +algorithm that balances recency and popularity. However, the best default for +the use case on Pipedream is recency. + +### Async Options + +Avoid asking users to enter ID values. Use [async +options](/workflows/contributing/components/api/#async-options-example) (with label/value definitions) +so users can make selections from a drop down menu. For example, Todoist +identifies projects by numeric IDs (e.g., 12345). The async option to select a +project displays the name of the project as the label, so that’s the value the +user sees when interacting with the source (e.g., “My Project”). The code +referencing the selection receives the numeric ID (12345). + +Async options should also support +[pagination](/workflows/contributing/components/api/#async-options-example) (so users can navigate +across multiple pages of options for long lists). See +[Hubspot](https://github.com/PipedreamHQ/pipedream/blob/a9b45d8be3b84504dc22bb2748d925f0d5c1541f/components/hubspot/hubspot.app.mjs#L136) +for an example of offset-based pagination. See +[Twitter](https://github.com/PipedreamHQ/pipedream/blob/d240752028e2a17f7cca1a512b40725566ea97bd/components/twitter/twitter.app.mjs#L200) +for an example of cursor-based pagination. + +### Dynamic Props + +[Dynamic props](/workflows/contributing/components/api/#dynamic-props) can improve the user experience +for components. They let you render props in Pipedream dynamically, based on the +value of other props, and can be used to collect more specific information that +can make it easier to use the component. See the Google Sheets example in the +linked component API docs. + +### Interface & Service Props + +In the interest of consistency, use the following naming patterns when defining +[interface](/workflows/contributing/components/api/#interface-props) and +[service](/workflows/contributing/components/api/#service-props) props in source components: + +| Prop | **Recommended Prop Variable Name** | +| ------------------- | ---------------------------------- | +| `$.interface.http` | `http` | +| `$.interface.timer` | `timer` | +| `$.service.db` | `db` | + +Use getters and setters when dealing with `$.service.db` to avoid potential +typos and leverage encapsulation (e.g., see the [Search +Mentions](https://github.com/PipedreamHQ/pipedream/blob/master/components/twitter/sources/search-mentions/search-mentions.mjs#L83-L88) +event source for Twitter). + +## Source Guidelines + +These guidelines are specific to [source](/workflows/building-workflows/triggers/) development. + +### Webhook vs Polling Sources + +Create subscription webhooks sources (vs polling sources) whenever possible. +Webhook sources receive/emit events in real-time and typically use less compute +time from the user’s account. Note: In some cases, it may be appropriate to +support webhook and polling sources for the same event. For example, Calendly +supports subscription webhooks for their premium users, but non-premium users +are limited to the REST API. A webhook source can be created to emit new +Calendly events for premium users, and a polling source can be created to +support similar functionality for non-premium users. + +### Source Name + +Source name should be a singular, title-cased name and should start with "New" +(unless emits are not limited to new items). Name should not be slugified and +should not include the app name. NOTE: Pipedream does not currently distinguish +real-time event sources for end-users automatically. The current pattern to +identify a real-time event source is to include “(Instant)” in the source name. +E.g., “New Search Mention” or “New Submission (Instant)”. + +### Source Description + +Enter a short description that provides more detail than the name alone. +Typically starts with "Emit new". E.g., “Emit new Tweets that matches your +search criteria”. + +### Emit a Summary + +Always [emit a summary](/workflows/contributing/components/api/#emit) for each event. For example, the +summary for each new Tweet emitted by the Search Mentions source is the content +of the Tweet itself. + +If no sensible summary can be identified, submit the event payload in string +format as the summary. + +### Deduping + +Use built-in [deduping strategies](/workflows/contributing/components/api/#dedupe-strategies) whenever +possible (`unique`, `greatest`, `last`) vs developing custom deduping code. +Develop custom deduping code if the existing strategies do not support the +requirements for a source. + +### Surfacing Test Events + +In order to provide users with source events that they can immediately reference +when building their workflow, we should implement 2 strategies whenever +possible: + +#### Emit Events on First Run + +- Polling sources should always emit events on the first run (see the [Spotify: + New + Playlist](https://github.com/PipedreamHQ/pipedream/blob/master/components/spotify/sources/new-playlist/new-playlist.mjs) + source as an example) +- Webhook-based sources should attempt to fetch existing events in the + `deploy()` hook during source creation (see the [Jotform: New + Submission](https://github.com/PipedreamHQ/pipedream/blob/master/components/jotform/sources/new-submission/new-submission.mjs) + source) + +_Note – make sure to emit the most recent events (considering pagination), and +limit the count to no more than 50 events._ + +#### Include a Static Sample Event + +There are times where there may not be any historical events available (think +about sources that emit less frequently, like "New Customer" or "New Order", +etc). In these cases, we should include a static sample event so users can see +the event shape and reference it while building their workflow, even if it's +using fake data. + +To achieve this, follow these steps: + +1. Copy the JSON output from the source's emit (what you get from + `steps.trigger.event`) and **make sure to remove or scrub any sensitive or + personal data** (you can also copy this from the app's API docs) +2. Add a new file called `test-event.mjs` in the same directory as the component + source and export the JSON event via `export default` + ([example](https://github.com/PipedreamHQ/pipedream/blob/master/components/jotform/sources/new-submission/test-event.mjs)) +3. In the source component code, make sure to import that file as `sampleEmit` + ([example](https://github.com/PipedreamHQ/pipedream/blob/master/components/jotform/sources/new-submission/new-submission.mjs#L2)) +4. And finally, export the `sampleEmit` object + ([example](https://github.com/PipedreamHQ/pipedream/blob/master/components/jotform/sources/new-submission/new-submission.mjs#L96)) + +This will render a "Generate Test Event" button in the UI for users to emit that +sample event: + +![generate-sample-event](https://res.cloudinary.com/pipedreamin/image/upload/v1690488844/generate-test-event_drjykm.gif) + +### Polling Sources + +#### Default Timer Interval + +As a general heuristic, set the default timer interval to 15 minutes. However, +you may set a custom interval (greater or less than 15 minutes) if appropriate +for the specific source. Users may also override the default value at any time. + +For polling sources in the Pipedream registry, the default polling interval is +set as a global config. Individual sources can access that default within the +props definition: + +``` javascript +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + // rest of component... +} +``` + +#### Rate Limit Optimization + +When building a polling source, cache the most recently processed ID or +timestamp using `$.service.db` whenever the API accepts a `since_id` or "since +timestamp" (or equivalent). Some apps (e.g., Github) do not count requests that +do not return new results against a user’s API quota. + +If the service has a well-supported Node.js client library, it'll often build in +retries for issues like rate limits, so using the client lib (when available) +should be preferred. In the absence of that, +[Bottleneck](https://www.npmjs.com/package/bottleneck) can be useful for +managing rate limits. 429s should be handled with exponential backoff (instead +of just letting the error bubble up). + +### Webhook Sources + +#### Hooks + +[Hooks](/workflows/contributing/components/api/#hooks) are methods that are automatically invoked by +Pipedream at different stages of the [component +lifecycle](/workflows/contributing/components/api/#source-lifecycle). Webhook subscriptions are +typically created when components are instantiated or activated via the +`activate()` hook, and deleted when components are deactivated or deleted via +the `deactivate()` hook. + +#### Helper Methods + +Whenever possible, create methods in the app file to manage [creating and +deleting webhook subscriptions](/workflows/contributing/components/api/#hooks). + +| **Description** | **Method Name** | +| --------------------------------------- | --------------- | +| Method to create a webhook subscription | `createHook()` | +| Method to delete a webhook subscription | `deleteHook()` | + +#### Storing the 3rd Party Webhook ID + +After subscribing to a webhook, save the ID for the hook returned by the 3rd +party service to the `$.service.db` for a source using the key `hookId`. This ID +will be referenced when managing or deleting the webhook. Note: some apps may +not return a unique ID for the registered webhook (e.g., Jotform). + +#### Signature Validation + +Subscription webhook components should always validate the incoming event +signature if the source app supports it. + +#### Shared Secrets + +If the source app supports shared secrets, implement support transparent to the +end user. Generate and use a GUID for the shared secret value, save it to a +`$.service.db` key, and use the saved value to validate incoming events. + +## Action Guidelines + +### Action Name + +Like [source name](#source-name), action name should be a singular, title-cased +name, should not be slugified, and should not include the app name. + +As a general pattern, articles are not included in the action name. For example, +instead of "Create a Post", use "Create Post". + +#### Use `@pipedream/platform` axios for all HTTP Requests + +By default, the standard `axios` package doesn't return useful debugging data to +the user when it `throw`s errors on HTTP 4XX and 5XX status codes. This makes it +hard for the user to troubleshoot the issue. + +Instead, [use `@pipedream/platform` axios](/workflows/building-workflows/http/#platform-axios). + +#### Return JavaScript Objects + +When you `return` data from an action, it's exposed as a [step +export](/workflows/#step-exports) for users to reference in future steps +of their workflow. Return JavaScript objects in all cases, unless there's a +specific reason not to. + +For example, some APIs return XML responses. If you return XML from the step, +it's harder for users to parse and reference in future steps. Convert the XML to +a JavaScript object, and return that, instead. + +### "List" Actions + +#### Return an Array of Objects + +To simplify using results from "list"/"search" actions in future steps of a +workflow, return an array of the items being listed rather than an object with a +nested array. [See this example for +Airtable](https://github.com/PipedreamHQ/pipedream/blob/cb4b830d93e1495d8622b0c7dbd80cd3664e4eb3/components/airtable/actions/common-list.js#L48-L63). + +#### Handle Pagination + +For actions that return a list of items, the common use case is to retrieve all +items. Handle pagination within the action to remove the complexity of needing +to paginate from users. We may revisit this in the future and expose the +pagination / offset params directly to the user. + +In some cases, it may be appropriate to limit the number of API requests made or +records returned in an action. For example, some Twitter actions optionally +limit the number of API requests that are made per execution (using a +[`maxRequests` +prop](https://github.com/PipedreamHQ/pipedream/blob/cb4b830d93e1495d8622b0c7dbd80cd3664e4eb3/components/twitter/twitter.app.mjs#L52)) +to avoid exceeding Twitter's rate limits. [See the Airtable +components](https://github.com/PipedreamHQ/pipedream/blob/e2bb7b7bea2fdf5869f18e84644f5dc61d9c22f0/components/airtable/airtable.app.js#L14) +for an example of using a `maxRecords` prop to optionally limit the maximum +number of records to return. + +### Use `$.summary` to Summarize What Happened + +[Describe what happened](/workflows/contributing/components/api/#returning-data-from-steps) when an +action succeeds by following these guidelines: + +- Use plain language and provide helpful and contextually relevant information + (especially the count of items) +- Whenever possible, use names and titles instead of IDs +- Basic structure: _Successfully [action performed (like added, removed, + updated)] “[relevant destination]”_ + +### Don't Export Data You Know Will Be Large + +Browsers can crash when users load large exports (many MBs of data). When you +know the content being returned is likely to be large – e.g. files — don't +export the full content. Consider writing the data to the `/tmp` directory and +exporting a reference to the file. + +## Database Components + +Pipedream supports a special category of apps called ["databases"](/workflows/data-management/databases/), +such as +[MySQL](https://github.com/PipedreamHQ/pipedream/tree/master/components/mysql), +[PostgreSQL](https://github.com/PipedreamHQ/pipedream/tree/master/components/postgresql), +[Snowflake](https://github.com/PipedreamHQ/pipedream/tree/master/components/snowflake), +etc. Components tied to these apps offer unique features _as long as_ they +comply with some requirements. The most important features are: + +1. A built-in SQL editor that allows users to input a SQL query to be run + against their DB +2. Proxied execution of commands against a DB, which guarantees that such + requests are always being made from the same range of static IPs (see the + [shared static IPs docs](/workflows/data-management/databases#send-requests-from-a-shared-static-ip)) + +When dealing with database components, the Pipedream runtime performs certain +actions internally to make these features work. For this reason, these +components must implement specific interfaces that allows the runtime to +properly interact with their code. These interfaces are usually defined in the +[`@pipedream/platform`](https://github.com/PipedreamHQ/pipedream/tree/master/platform) +package. + +### SQL Editor + +This code editor is rendered specifically for props of type `sql`, and it uses +(whenever possible) the underlying's database schema information to provide +auto-complete suggestions. Each database engine not only has its own SQL +dialect, but also its own way of inspecting the schemas and table information it +stores. For this reason, each app file must implement the logic that's +applicable to the target engine. + +To support the schema retrieval, the app file must implement a method called +`getSchema` that takes no parameters, and returns a data structure with a format +like this: + +```javascript +{ + users: { // The entries at the root correspond to table names + metadata: { + rowCount: 100, + }, + schema: { + id: { // The entries under `schema` correspond to column names + columnDefault: null, + dataType: "number", + isNullable: false, + tableSchema: "public", + }, + email: { + columnDefault: null, + dataType: "varchar", + isNullable: false, + tableSchema: "public", + }, + dateOfBirth: { + columnDefault: null, + dataType: "varchar", + isNullable: true, + tableSchema: "public", + }, + }, + }, +} +``` + +The +[`lib/sql-prop.ts`](https://github.com/PipedreamHQ/pipedream/blob/master/platform/lib/sql-prop.ts) +file in the `@pipedream/platform` package define the schema format and the +signature of the `getSchema` method. You can also check out existing examples in +the +[MySQL](https://github.com/PipedreamHQ/pipedream/blob/master/components/mysql/mysql.app.mjs), +[PostgreSQL](https://github.com/PipedreamHQ/pipedream/blob/master/components/postgresql/postgresql.app.mjs) +and +[Snowflake](https://github.com/PipedreamHQ/pipedream/blob/master/components/snowflake/snowflake.app.mjs) +components. + +### Shared Static IPs + +When a user runs a SQL query against a database, the request is proxied through +a separate internal service that's guaranteed to always use the same range of +static IPs when making outbound requests. This is important for users that have +their databases protected behind a firewall, as they can whitelist these IPs to +allow Pipedream components to access their databases. + +To make this work, the app file must implement the interface defined in the +[`lib/sql-proxy.ts`](https://github.com/PipedreamHQ/pipedream/blob/master/platform/lib/sql-proxy.ts) +file in the `@pipedream/platform` package. This interface defines the following +methods: + +1. **`getClientConfiguration`**: This method takes no parameters and returns an + object that can be fed directly to the database's client library to + initialize/establish a connection to the database. This guarantees that both + the component and the proxy service use the same connection settings, **so + make sure the component uses this method when initializing the client**. +2. **`executeQuery`**: This method takes a query object and returns the result + of executing the query against the database. The Pipedream runtime will + replace this method with a call to the proxy service, so **every component + must make use of this method in order to support this feature**. +3. **`proxyAdapter`**: This method allows the proxy service to take the + arguments passed to the `executeQuery` method and transform them into a + "generic" query object that the service can then use. The expected format + looks something like this: + ```javascript + { + query: "SELECT * FROM users WHERE id = ?", + params: [42], + } + ``` + +You can check out these example pull requests that allowed components to support +this proxy feature: + +- [#11201 (MySQL)](https://github.com/PipedreamHQ/pipedream/pull/11201) +- [#11202 (PostgreSQL)](https://github.com/PipedreamHQ/pipedream/pull/11202) +- [#12511 (Snowflake)](https://github.com/PipedreamHQ/pipedream/pull/12511) diff --git a/docs-v2/pages/workflows/contributing/components/index.mdx b/docs-v2/pages/workflows/contributing/components/index.mdx new file mode 100644 index 0000000000000..123f403f316ef --- /dev/null +++ b/docs-v2/pages/workflows/contributing/components/index.mdx @@ -0,0 +1,140 @@ +import Callout from '@/components/Callout' +import VideoPlayer from '@/components/VideoPlayer' + +# Overview + +## What are Components? + +Components are [Node.js modules](api/#component-structure) that run on Pipedream's serverless infrastructure. They can use Pipedream managed auth for [{process.env.PUBLIC_APPS}+ apps](https://pipedream.com/explore) (for both OAuth and key-based APIs) and [use most npm packages](api/#using-npm-packages) with no `npm install` or `package.json` required. + +Components are most commonly used as the building blocks of Pipedream workflows, but they can also be used like typical serverless functions. You can explore curated components for popular apps in Pipedream's [Marketplace](https://pipedream.com/explore) and [GitHub repo](https://github.com/PipedreamHQ/pipedream/tree/master/components) or you can author and share your own. + + +Our TypeScript component API is in **beta**. If you're interested in developing TypeScript components and providing feedback, [see our TypeScript docs](/workflows/contributing/components/typescript/). + + +## Component Types + +Pipedream supports two types of components — [sources](#sources) and [actions](#actions). + +### Sources + +[Sources](/workflows/building-workflows/triggers/) must be instantiated and they run as independent resources on Pipedream. They are commonly used as workflow triggers (but can also be used as standalone serverless functions). + +**Capabilities** + +- Accept user input on deploy via `props` +- [Trigger](api/#interface-props) on HTTP requests, timers, cron schedules, or manually +- Emit events that can be inspected, trigger Pipedream [workflows](/workflows/building-workflows/) and that can be consumed in your own app via [API](/rest-api/) +- Store and retrieve state using the [built-in key-value store](api/#db) +- Use any of Pipedream's built-in [deduping strategies](api/#dedupe-strategies) +- Deploy via Pipedream's UI, CLI or API + +**Example** + +The [New Files (Instant)](https://github.com/PipedreamHQ/pipedream/blob/master/components/google_drive/sources/new-files-instant/new-files-instant.mjs) source for Google Drive is a prebuilt component in Pipedream's registry that can be deployed in seconds and emits an event every time a new file is added to the user's Google Drive, and can also be configured to watch for changes to a specific folder within that drive. Each new event that is emitted can be used to trigger a workflow. + +### Actions + +Actions are components that may be used as steps in a workflow. Unlike sources, actions cannot run independently (outside of a workflow). + +**Capabilities** + +- Accept user input via `props` +- May `return` JSON serializable data + +**Example** + +The Add Single Row action for Google Sheets is a prebuilt component in Pipedream's registry that can be added to a workflow and configured in seconds. Users can configure it in seconds and send workflow data to Google Sheets without having to learn the Google Sheets API. + +## Using Components + +Components may be instantiated or added to workflows via Pipedream's UI. + +- Sources may be instantiated and consumed via [UI](https://pipedream.com/sources/new), [CLI](/workflows/cli/reference/#pd-deploy) or API +- Actions may be added to [workflows](https://pipedream.com/new) + +### Using Private Actions + +Private action components published from the [CLI](/workflows/cli/reference/#pd-publish) or from a [Node.js Code Step](/workflows/building-workflows/code/nodejs/sharing-code/) are available for use across your workflows. + +To use a published action, add a new step to your workflow and click **My Actions**. Your privately published action components will appear in this list. + +![Use the "My Actions" section in a new step to include your private actions](/images/components/v3/using-private-actions.png) + +### Using Private Sources + +Private source components deployed from your account via the [CLI](/workflows/cli/reference/#pd-deploy) will automatically create a new Source in your account with the prop configuration you specified. + +Then in the workflow builder, when creating the trigger, select the *Existing* sources tab in the upper right corner to select your deployed source: + +![Selecting a pre-exisiting deployed source as the trigger for the workflow](/images/components/v3/using-private-sources.png) + +You can also deploy new instances of a source from the [Components dashboard](https://res.cloudinary.com/pipedreamin/image/upload/v1618550730/docs/components/image-20210411165325045_ia5sd5.png). To deploy a new instance of a source, click the menu on the right hand side and select **Create source**. + +![Creating a source from the Component dashboard](https://res.cloudinary.com/pipedreamin/image/upload/v1666106571/docs/CleanShot_2022-10-18_at_11.22.02_ajjopm.gif) + +## Developing Components + +Develop components locally using your preferred code editor (and maintain your code in your own GitHub repo) and deploy or publish using Pipedream's [CLI](/workflows/cli/reference/#pd-deploy). + +- Sources may be deployed directly from local code or published to your account and instantiated via Pipedream's UI +- Actions may only be published — published actions may be added to workflows via Pipedream's UI + +Published components are only available to your own account by default. If published to a team account, the component (source or action) may be discovered and selected by any member of the team. + +### Prerequisites + +- A free [Pipedream](https://pipedream.com) account +- A free [GitHub](https://github.com) account +- Basic proficiency with Node.js or Javascript +- Pipedream [CLI](/workflows/cli/reference/) + +Finally, the target app must be integrated with Pipedream. You can explore all apps supported by Pipedream in the [marketplace](https://pipedream.com/explore). If your app is not listed, please [create a GitHub issue](https://github.com/PipedreamHQ/pipedream/issues/new?assignees=&labels=app%2C+enhancement&template=app---service-integration.md&title=%5BAPP%5D) to request it and [reach out](https://pipedream.com/community/c/dev/11) to our team to let us know that you're blocked on source or action development. + +### Quickstart Guides + +- [Sources](/workflows/contributing/components/sources-quickstart/) +- [Actions](/workflows/contributing/components/actions-quickstart/) + +### Component API Reference + +After getting familiar with source/action development using the quickstart guides, check out [the Component API Reference](/workflows/contributing/components/api/) and [examples on GitHub](https://github.com/PipedreamHQ/pipedream/tree/master/components) to learn more. + +## Managing Privately Published Components + +Components published to your workspace are available in the [Components](https://pipedream.com/components) section of the dashboard. + +Your private components published from the CLI or from Node.js code steps are listed here. + +### Unpublishing Privately Published Components + + + +To unpublish components belonging to your workspace, open the menu on the right hand side of the component details and select **Unpublish Component**. + +A prompt will open to confirm the action, click **Confirm** to unpublish your action. + +![Unpublish a component from your account by opening the menu on the right hand side](https://res.cloudinary.com/pipedreamin/image/upload/v1666103082/docs/components/CleanShot_2022-10-18_at_10.22.45_vdhoq7.gif) + + +Unpublishing a component is a permanent action, please be careful to ensure you still have access to the source code. + + +## Sharing Components + +Contribute to the Pipedream community by publishing and sharing new components, and contributing to the maintenance of existing components. + +### Verified Components + +Pipedream maintains a source-available registry of components (sources and actions) that have been curated for the community. Registered components are verified by Pipedream through the [GitHub PR process](/workflows/contributing/#contribution-process) and: + +- Can be trusted by end users +- Follow consistent patterns for usability +- Are supported by Pipedream if issues arise + +Registered components also appear in the Pipedream marketplace and are listed in Pipedream's UI when building workflows. + +### Community Components + +Developers may create, deploy and share components that do not conform to these guidelines, but they will not be eligible to be listed in the curated registry (e.g., they may be hosted in a Github repo). If you develop a component that does not adhere to these guidelines, but you believe there is value to the broader community, please [reach out in our community forum](https://pipedream.com/community/c/dev/11). diff --git a/docs-v2/pages/components/sources-quickstart.mdx b/docs-v2/pages/workflows/contributing/components/sources-quickstart.mdx similarity index 96% rename from docs-v2/pages/components/sources-quickstart.mdx rename to docs-v2/pages/workflows/contributing/components/sources-quickstart.mdx index 2e708471b88eb..29b50b8c12611 100644 --- a/docs-v2/pages/components/sources-quickstart.mdx +++ b/docs-v2/pages/workflows/contributing/components/sources-quickstart.mdx @@ -36,15 +36,15 @@ We recommend that you execute the examples in order — each one builds on the c **Step 1.** Create a free account at [https://pipedream.com](https://pipedream.com). Just sign in with your Google or Github account. -**Step 2.** [Download and install the Pipedream CLI](/cli/install/). +**Step 2.** [Download and install the Pipedream CLI](/workflows/cli/install/). -**Step 3.** Once the CLI is installed, [link your Pipedream account to the CLI](/cli/login/#existing-pipedream-account): +**Step 3.** Once the CLI is installed, [link your Pipedream account to the CLI](/workflows/cli/login/#existing-pipedream-account): ```bash pd login ``` -See the [CLI reference](/cli/reference/) for detailed usage and examples beyond those covered below. +See the [CLI reference](/workflows/cli/reference/) for detailed usage and examples beyond those covered below. ## CLI Development Mode @@ -67,7 +67,7 @@ If you need to update a deployed instance of a source, pass it's ID to the `dc` pd dev --dc dc_123456 components/sources/my-source.mjs ``` -See the [CLI reference](/cli/reference/) for detailed usage and examples beyond those covered below. +See the [CLI reference](/workflows/cli/reference/) for detailed usage and examples beyond those covered below. ## Hello World! @@ -106,7 +106,7 @@ Open the URL returned by the CLI (`https://pipedream.com/sources/dc_v3uXKz` in t Then click **RUN NOW** to invoke your source. Your event will appear in real-time, and you can select it to inspect the emitted data. -![source](./images/quickstart/hello-world-1.gif) +![source](/images/components/quickstart/hello-world-1.gif) ### Maintain state across executions @@ -165,7 +165,7 @@ export default { Save the changes to your local file. Your component on Pipedream should automatically update. Return to the Pipedream UI and press **RUN NOW** — you should see the execution count appear in the event list. -![source](./images/quickstart/hello-world-2.gif) +![source](/images/components/quickstart/hello-world-2.gif) ### Invoke your code on a schedule @@ -212,7 +212,7 @@ export default { Save the changes to your file (your component on Pipedream should automatically update). and then, return to the Pipedream UI and **reload the page**. You should now see the timer settings in the summary and a countdown to the next execution (you can still run your component manually). Your component will now run every 15 minutes. -![source](./images/quickstart/hello-world-3.gif) +![source](/images/components/quickstart/hello-world-3.gif) **Note**: if you'd like to change the schedule of your deployed component, visit the **Configuration** tab in the Pipedream UI and change the schedule accordingly. Changing the value of `intervalSeconds` within the component's code will not change the schedule of the running instance of the component. You can also set one value as the default `intervalSeconds` in the component's code, but run @@ -298,7 +298,7 @@ curl -d '{ "message": "hello world!" }' \ "INSERT-YOUR-ENDPOINT-URL-HERE" ``` -![source](./images/quickstart/hello-world-4.gif) +![source](/images/components/quickstart/hello-world-4.gif) ## Emit new RSS items on a schedule (~10 mins) @@ -679,6 +679,6 @@ Save and reload your source in the Pipedream UI. You should now see a countdown ## What's Next? -You're ready to start authoring and deploying components on Pipedream! You can also check out the [detailed component reference](/components/api/) at any time! +You're ready to start authoring and deploying components on Pipedream! You can also check out the [detailed component reference](/workflows/contributing/components/api/) at any time! -If you have any questions or feedback, please join our [public Slack](https://pipedream.com/support). \ No newline at end of file +If you have any questions or feedback, please join our [public Slack](https://pipedream.com/support). diff --git a/docs-v2/pages/components/typescript.mdx b/docs-v2/pages/workflows/contributing/components/typescript.mdx similarity index 87% rename from docs-v2/pages/components/typescript.mdx rename to docs-v2/pages/workflows/contributing/components/typescript.mdx index bcbcd7e22f151..d2cf6fa10d317 100644 --- a/docs-v2/pages/components/typescript.mdx +++ b/docs-v2/pages/workflows/contributing/components/typescript.mdx @@ -2,7 +2,7 @@ import Callout from '@/components/Callout' # TypeScript components - + 🎉 Calling all TypeScript developers 🎉 TypeScript components are in **beta**, and we're looking for feedback. Please see our list of [known issues](#known-issues), start writing TypeScript components, and give us feedback in [our community](https://pipedream.com/support). @@ -14,19 +14,19 @@ During the beta, the `@pipedream/types` package and other TypeScript configurati ## Why TypeScript? -Most Pipedream components in [the registry](https://github.com/PipedreamHQ/pipedream/) are written in Node.js. Writing components in TypeScript can reduce bugs and speed up development, with very few changes to your code. +Most Pipedream components in [the registry](https://github.com/PipedreamHQ/pipedream) are written in Node.js. Writing components in TypeScript can reduce bugs and speed up development, with very few changes to your code. If you haven't written TypeScript, start with [this tutorial](https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html). ## Quickstart - -If you've never developed Pipedream components before, [start here](/components/). + +If you've never developed Pipedream components before, [start here](/workflows/contributing/components/). ### Developing TypeScript components in the `PipedreamHQ/pipedream` registry -1. [Fork and clone the repo](https://github.com/PipedreamHQ/pipedream/). +1. [Fork and clone the repo](https://github.com/PipedreamHQ/pipedream). 2. Run `pnpm install` to install dependencies. @@ -38,19 +38,19 @@ If you've never developed Pipedream components before, [start here](/components/ ```bash npm run build -``` +``` The build process should print the compiled JS files to your console and produce them at the `/dist` directory. For example, if you compile a TypeScript file at `pipedream/components/rss/sources/new-item-in-feed/new-item-in-feed.ts`, the corresponding JS file will be produced at `pipedream/components/rss/dist/sources/new-item-in-feed/new-item-in-feed.js`. -6. Use [the Pipedream CLI](/cli/reference/) to `pd publish` or `pd dev` the JavaScript components emitted by step 5 by the full path to the file. +6. Use [the Pipedream CLI](/workflows/cli/reference/) to `pd publish` or `pd dev` the JavaScript components emitted by step 5 by the full path to the file. ```bash pd publish pipedream/components/rss/dist/sources/new-item-in-feed/new-item-in-feed.js ``` - + Don't forget to use the dist directory If you attempt to deploy the TypeScript component directly, you'll receive a 500 error from the publish endpoint. Instead deploy the JavaScript file produced within the `/dist` directory as described in step 5. @@ -77,10 +77,10 @@ yarn add --dev @pipedream/types You'll need a minimal configuration to compile TypeScript components in your application. In the Pipedream registry, we use this setup: -- The `tsconfig.json` in the root of the repo contains [references](https://www.typescriptlang.org/docs/handbook/project-references.html) to component app directories. For example, the root config provides a reference to the `components/rss` directory, which contains its own `tsconfig.json` file. +- The `tsconfig.json` in the root of the repo contains [references](https://www.typescriptlang.org/docs/handbook/project-references.html) to component app directories. For example, the root config provides a reference to the `components/rss` directory, which contains its own `tsconfig.json` file. - `npm run build` compiles the TypeScript in all directories in `references`. - The `tsconfig.json` in each component app directory contains the app-specific TypeScript configuration. -- The GitHub actions in `.github/workflows` compile and publish our components. +- The GitHub actions in `.github/workflows` compile and publish our components. See [the RSS sources and actions](https://github.com/PipedreamHQ/pipedream/tree/master/components/rss) for an example app configuration. @@ -88,7 +88,7 @@ See [the RSS sources and actions](https://github.com/PipedreamHQ/pipedream/tree/ We welcome PRs in [the `PipedreamHQ/pipedream` repo](https://github.com/PipedreamHQ/pipedream), where we store all sources and actions, the `@pipedream/types` package, these docs, and other Pipedream code. Here are a few known issues durin the **beta**: -- `this` is strictly-typed within `methods`, `run`, `hooks`, and everywhere you have access to `this` in [the component API](/components/api/). But this typing can be improved. For example, we don't yet map props to their appropriate TypeScript type (everything is typed with `any`). +- `this` is strictly-typed within `methods`, `run`, `hooks`, and everywhere you have access to `this` in [the component API](/workflows/contributing/components/api/). But this typing can be improved. For example, we don't yet map props to their appropriate TypeScript type (everything is typed with `any`). - The compile -> publish lifecycle hasn't been fully-automated when you're developing in the `pipedream` repo. Currently, you have to run `npm run build` from the repo root, then use the `pd` CLI to publish components after compilation. It would be nice to run `tsc-watch` and have that compile and publish the new version of the component using the `--onSuccess` flag, publishing any sources or actions accordingly. - We should add a linter (like `dtslint`) to all TypeScript components). Currently, `dtslint` is configured only for the `@pipedream/types` package. diff --git a/docs-v2/pages/workflows/contributing/index.mdx b/docs-v2/pages/workflows/contributing/index.mdx new file mode 100644 index 0000000000000..d450cc5f33a41 --- /dev/null +++ b/docs-v2/pages/workflows/contributing/index.mdx @@ -0,0 +1,99 @@ +import Callout from '@/components/Callout' +import { FileTree } from 'nextra/components' + +# Pipedream Registry + +When developing workflows with pre-built actions and triggers, under the hood you're using [components](/workflows/contributing/components/) from the [Pipedream Registry Github Repository](https://github.com/PipedreamHQ/pipedream). + +Components contributed to the [Pipedream Registry Github Repository](https://github.com/PipedreamHQ/pipedream) are published to the [Pipedream marketplace](https://pipedream.com/apps) and are listed in +the Pipedream UI when building workflows. + + +What is a component? + +If you haven't yet, we recommend starting with our Component Development Quickstart Guides for [sources](/workflows/contributing/components/quickstart/nodejs/sources/) +and [actions](/workflows/contributing/components/quickstart/nodejs/actions/) to learn how to build components and privately publish them to your account. + + +## Registry Components Structure + +All Pipedream registry components live in [this GitHub repo](https://github.com/PipedreamHQ/pipedream) under the [`components`](https://github.com/PipedreamHQ/pipedream/tree/master/components) directory. + +Every integrated app on Pipedream has a corresponding directory that defines the actions and sources available for that app. Below is a simplified version of the [Airtable app directory](https://github.com/PipedreamHQ/pipedream/tree/master/components/airtable) within the registry: + + + + + + + + + + + + + + + + + + + + + + +In the example above, the `components/airtable/actions/get-record/get-record.mjs` component is published as the **Get Record** action under the **Airtable** app within the workflow builder in Pipedream. + + +The repository is missing the app directory I'd like to add components for + +You can request to have new apps integrated into Pipedream. + +Once the Pipedream team integrates the app, we'll create a directory for the app in the [`components`](https://github.com/PipedreamHQ/pipedream/tree/master/components) directory of the GitHub repo. + + +## Contribution Process + +Anyone from the community can build [sources](/workflows/building-workflows/triggers/) and [actions](/workflows/contributing/components/#actions) for integrated apps. + +To submit new components or update existing components: + +1. Fork the public [Pipedream Registry Github Repository](https://github.com/PipedreamHQ/pipedream). +2. Create a new component within the corresponding app's directory within the `components` directory (if applicable). +3. [Create a PR for the Pipedream team to review](https://github.com/PipedreamHQ/pipedream/compare). +4. Address any feedback provided by Pipedream based on the best practice [Component Guidelines & Patterns](/workflows/contributing/components/guidelines/). +5. Once the review is complete and approved, Pipedream will merge the PR to the `master` branch +6. The component will be available for use within workflows for all Pipedream developers! 🎉 + +### Component Development Discussion + +Join the discussion with other Pipedream component developers at the [#contribute channel](https://pipedream-users.slack.com/archives/C01E5KCTR16) in Slack or [on Discourse](https://pipedream.com/community/c/dev/11). + + +Not sure what to build? + +Need inspiration? Check out [sources](https://github.com/PipedreamHQ/pipedream/issues?q=is%3Aissue+is%3Aopen+%5BSOURCE%5D+in%3Atitle) +and [actions](https://github.com/PipedreamHQ/pipedream/issues?q=is%3Aissue+is%3Aopen+%5BACTION%5D+in%3Atitle+) requested by the community! + + +## Reference Components + +The following components are references for developing sources and +actions for Pipedream's registry. + +### Reference Sources + +| Name | App | Type | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------- | +| [New Card](https://github.com/PipedreamHQ/pipedream/blob/master/components/trello/sources/new-card/new-card.mjs) | Trello | Webhook | +| [New or Modified Files](https://github.com/PipedreamHQ/pipedream/blob/master/components/google_drive/sources/new-or-modified-files/new-or-modified-files.mjs) | Google Drive | Webhook + Polling | +| [New Submission](https://github.com/PipedreamHQ/pipedream/blob/master/components/jotform/sources/new-submission/new-submission.mjs) | Jotform | Webhook (with no unique hook ID) | + +### Reference Actions + +| Name | App | +| ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| [Add Multiple Rows](https://github.com/PipedreamHQ/pipedream/blob/master/components/google_sheets/actions/add-multiple-rows/add-multiple-rows.mjs) | Google Sheets | +| [Send Message](https://github.com/PipedreamHQ/pipedream/blob/master/components/discord_bot/actions/send-message/send-message.mjs) | Discord | +| [Append Text](https://github.com/PipedreamHQ/pipedream/blob/master/components/google_docs/actions/append-text/append-text.mjs) | Google Docs | +| [`GET` request](https://github.com/PipedreamHQ/pipedream/blob/master/components/http/actions/get-request/get-request.mjs) | HTTP | diff --git a/docs-v2/pages/workflows/data-management/_meta.tsx b/docs-v2/pages/workflows/data-management/_meta.tsx new file mode 100644 index 0000000000000..aad88f99440fd --- /dev/null +++ b/docs-v2/pages/workflows/data-management/_meta.tsx @@ -0,0 +1,6 @@ +export default { + "data-stores": "Data Stores", + "file-stores": "File Stores", + "destinations": "Destinations", + "databases": "Databases", +} as const diff --git a/docs-v2/pages/data-stores.mdx b/docs-v2/pages/workflows/data-management/data-stores.mdx similarity index 77% rename from docs-v2/pages/data-stores.mdx rename to docs-v2/pages/workflows/data-management/data-stores.mdx index 6f54511f50079..a6763e5710ae3 100644 --- a/docs-v2/pages/data-stores.mdx +++ b/docs-v2/pages/workflows/data-management/data-stores.mdx @@ -20,7 +20,7 @@ Data stores are useful for: You can connect to the same data store across workflows, so they're also great for sharing state across different services. -You can use pre-built, no-code actions to store, update, and clear data, or interact with data stores programmatically in [Node.js](/code/nodejs/using-data-stores/) or [Python](/code/python/using-data-stores/). +You can use pre-built, no-code actions to store, update, and clear data, or interact with data stores programmatically in [Node.js](/workflows/building-workflows/code/nodejs/using-data-stores/) or [Python](/workflows/building-workflows/code/python/using-data-stores/). ## Using pre-built Data Store actions @@ -34,7 +34,7 @@ To insert data into a data store: 2. Search for the **Data Stores** app and select it. 3. Select the **Add or update a single record** pre-built action. -![Insert a single row into a data store](https://res.cloudinary.com/pipedreamin/image/upload/v1648060286/docs/components/CleanShot_2022-03-23_at_14.31.05_2x_swrdrh.png) +![Insert a single row into a data store](/images/data-stores/add-or-update-a-single-record.png) Configure the action: @@ -42,13 +42,13 @@ Configure the action: 2. **Key** - the unique ID for this data that you'll use for lookup later 3. **Value** - The data to store at the specified `key` -![Configure the action](https://res.cloudinary.com/pipedreamin/image/upload/v1648063057/docs/components/CleanShot_2022-03-23_at_15.17.30_2x_snunyz.png) +![Configure the action](/images/data-stores/configuring-data-store-update-action.png) For example, to store the timestamp when the workflow was initially triggered, set the **Key** to **Triggered At** and the **Value** to `{{steps.trigger.context.ts}}`. -The **Key** must evaluate to a string. You can pass a static string, reference [exports](/workflows/steps/#step-exports) from a previous step, or use [any valid expression](/workflows/steps/using-props/#entering-expressions). +The **Key** must evaluate to a string. You can pass a static string, reference [exports](/workflows/#step-exports) from a previous step, or use [any valid expression](/workflows/building-workflows/using-props/#entering-expressions). -![Workflow trigger example](https://res.cloudinary.com/pipedreamin/image/upload/v1649270704/docs/components/add_update_record_action_eh7dpz.png) +![Workflow trigger example](/images/data-stores/update-data-store-key-by-reference.png) Need to store multiple records in one action? Use the **Add or update multiple @@ -63,7 +63,7 @@ The **Get record** action will retrieve the latest value of a data point in one 2. Search for the **Data Stores** app and select it. 3. Select the **Add or update a single record** pre-built action. -![Create a get record action](https://res.cloudinary.com/pipedreamin/image/upload/v1648062096/docs/components/CleanShot_2022-03-23_at_14.53.54_2x_aqpwx2.png) +![Create a get record action](/images/data-stores/get-a-record-action-selection.png) Configure the action: @@ -72,13 +72,13 @@ Configure the action: 3. **Create new record if key is not found** - if the specified key isn't found, you can create a new record 4. **Value** - The data to store at the specified `key` -![Get record action](https://res.cloudinary.com/pipedreamin/image/upload/v1648853992/docs/components/data_stores_get_record_yqazfk.png) +![Get record action](/images/data-stores/configure-data-store-retrieve-record.png) ### Deleting Data To delete a single record from your data store, use the **Delete a single record** action in a step: -![Select the delete record step](https://res.cloudinary.com/pipedreamin/image/upload/v1648064099/docs/components/CleanShot_2022-03-23_at_15.34.44_2x_pk9idz.png) +![Select the delete record step](/images/data-stores/select-delete-a-record.png) Then configure the action: @@ -87,7 +87,7 @@ Then configure the action: For example, you can delete the data at the **Triggered At** key that we've created in the steps above: -![Delete a record example](https://res.cloudinary.com/pipedreamin/image/upload/v1648064157/docs/components/CleanShot_2022-03-23_at_15.35.48_2x_ay1bbw.png) +![Delete a record example](/images/data-stores/delete-a-single-record.png) Deleting a record does not delete the entire data store. [To delete an entire data store, use the Pipedream Data Stores Dashboard](#deleting-data-stores). @@ -110,7 +110,7 @@ You can delete a data store from this dashboard as well. On the far right in the **Deleting a data store is irreversible**. - + If the **Delete** option is greyed out and unclickable, you have workflows using the data store in a step. Click the **>** to the left of the data store's name to expand the linked workflows.
@@ -126,7 +126,7 @@ Then remove the data store from any linked steps. ## Using data stores in code steps -Refer to the [Node.js](/code/nodejs/using-data-stores/) and [Python](/code/python/using-data-stores/) data store docs to learn how to use data stores in code steps. You can get, set, delete and perform any other data store operations in code. You cannot use data stores in [Bash](/code/bash/) or [Go](/code/go/) code steps. +Refer to the [Node.js](/workflows/building-workflows/code/nodejs/using-data-stores/) and [Python](/workflows/building-workflows/code/python/using-data-stores/) data store docs to learn how to use data stores in code steps. You can get, set, delete and perform any other data store operations in code. You cannot use data stores in [Bash](/workflows/building-workflows/code/bash/) or [Go](/workflows/building-workflows/code/go/) code steps. ## Compression @@ -144,11 +144,16 @@ You'll find your workspace's limits in the **Data Stores** section of usage dash

+## Atomic operations + +Data store operations are not atomic or transactional, which can lead to race conditions. +To ensure atomic operations, be sure to limit access to a data store key to a [single workflow with a single worker](/workflows/building-workflows/settings/concurrency-and-throttling/) or use a service that supports atomic operations from among our [integrated apps](https://pipedream.com/apps). + ## Supported data types Data stores can hold any JSON-serializable data within the storage limits. This includes data types including: @@ -162,28 +167,22 @@ Data stores can hold any JSON-serializable data within the storage limits. This But you cannot serialize functions, classes, sets, maps, or other complex objects. -## Retrieving a large number of keys - -You can retrieve up to {process.env.DATA_STORES_MAX_KEYS} keys from a data store in a single query. - -If you're using a pre-built action or code to retrieve all records or keys, and your data store contains more than {process.env.DATA_STORES_MAX_KEYS} records, you'll receive a 426 error. - ## Exporting data to an external service In order to stay within the [data store limits](#data-store-limits), you may need to export the data in your data store to an external service. The following Node.js example action will export the data in chunks via an HTTP POST request. You may need to adapt the code to your needs. Click on [this link](https://pipedream.com/new?h=tch_egfAMv) to create a copy of the workflow in your workspace. - + If the data contained in each key is large, consider lowering the number of `chunkSize`. -- Adjust your [workflow memory and timeout settings](/workflows/settings/) according to the size of the data in your data store. Set the memory at 512 MB and timeout to 60 seconds and adjust higher if needed. +- Adjust your [workflow memory and timeout settings](/workflows/building-workflows/settings/) according to the size of the data in your data store. Set the memory at 512 MB and timeout to 60 seconds and adjust higher if needed. - Monitor the exports of this step after each execution for any potential errors preventing a full export. Run the step as many times as needed until all your data is exported. - + This action deletes the keys that were successfully exported. It is advisable to first run a test without deleting the keys. In case of any unforeseen errors, your data will still be safe. diff --git a/docs-v2/pages/workflows/data-management/databases/_meta.tsx b/docs-v2/pages/workflows/data-management/databases/_meta.tsx new file mode 100644 index 0000000000000..5614ce8184d62 --- /dev/null +++ b/docs-v2/pages/workflows/data-management/databases/_meta.tsx @@ -0,0 +1,4 @@ +export default { + "index": "Connecting to Databases", + "working-with-sql": "Working with SQL", +} as const diff --git a/docs-v2/pages/workflows/data-management/databases/index.mdx b/docs-v2/pages/workflows/data-management/databases/index.mdx new file mode 100644 index 0000000000000..e5c58704aea65 --- /dev/null +++ b/docs-v2/pages/workflows/data-management/databases/index.mdx @@ -0,0 +1,41 @@ +import Callout from '@/components/Callout' + +# Connecting to Databases +Connecting to a database is essential for developing production workflows. Whether you're storing application data, querying user information, or analyzing event logs, most workflows and serverless functions require querying data at some point. + + +Pipedream workflows run in the AWS `us-east-1` network, sending requests from standard AWS IP ranges. + + +## Connecting to Restricted Databases +**Unless your database is publicly accessible, you'll likely need to add specific IPs to its allow-list.** To do this, you can configure your database connection to use either a shared or dedicated static IP address from Pipedream: + +### Create a Dedicated Static IP for Outbound Traffic +- [Virtual Private Clouds (VPCs)](/workflows/vpc/) in Pipedream let you deploy any workflow to a private network and is the most secure and recommended approach to using a static IP. +- Once configured, the VPC will give you a dedicated egress IP that's unique to your workspace, and is available to any workflow within your workspace. + +### Send Requests from a Shared Static IP +- When configuring your database connection as a [connected account](/integrations/connected-accounts) to Pipedream, you can choose to route network requests through a static IP block for [any app that's supported by Pipedream's SQL Proxy](#supported-databases) +- Pipedream's SQL Proxy routes requests to your database from the IP block below. + +#### Supported Databases +Pipedream's SQL Proxy, which enables the shared static IP, currently supports [MySQL](https://pipedream.com/apps/mysql), [PostgreSQL](https://pipedream.com/apps/postgresql), and [Snowflake](https://pipedream.com/apps/snowflake). Please let us know if you'd like to see support for other database types. + +#### Enabling the Shared Static IP +Connect your account for one of the [supported database apps](#supported-databases) and set **Use Shared Static IP** to **TRUE**, then click **Test connection** to ensure Pipedream can successfully connect to your database. + +![Connect and test your account](https://res.cloudinary.com/pipedreamin/image/upload/v1717387003/test-connection-mysql_bizpqk.png) + +#### Shared Static IP Block +Add the following IP block to your database allow-list: +``` +44.223.89.56/29 +``` + + +## FAQ + +### What's the difference between using a shared static IP with the SQL Proxy vs a dedicated IP using a VPC? +Both the SQL Proxy and VPCs enable secure database connections from a static IP. +- VPCs offer enhanced isolation and security by providing a **dedicated** static IP for workflows within your workspace +- The SQL proxy routes requests to your database connections through a set of **shared** static IPs diff --git a/docs-v2/pages/workflows/data-management/databases/working-with-sql.mdx b/docs-v2/pages/workflows/data-management/databases/working-with-sql.mdx new file mode 100644 index 0000000000000..8f5bdc5e547ce --- /dev/null +++ b/docs-v2/pages/workflows/data-management/databases/working-with-sql.mdx @@ -0,0 +1,79 @@ +import Callout from '@/components/Callout' +import { Steps } from 'nextra/components' +import ArcadeEmbed from '@/components/ArcadeEmbed' + +# Working with SQL +Pipedream makes it easy to interact with SQL databases within your workflows. You can securely connect to your database and use either pre-built no-code triggers and actions to interact with your database, or execute custom SQL queries. + +## SQL Editor +With the built-in SQL editor, you access linting and auto-complete features typical of modern SQL editors. + +![SQL editor](https://res.cloudinary.com/pipedreamin/image/upload/v1717388521/sql-editor_jn2rpn.png) + +## Schema Explorer +When querying a database, you need to understand the schema of the tables you're working with. The schema explorer provides a visual interface to explore the tables in your database, view their columns, and understand the relationships between them. + +- Once you connect your account with one of the [supported database apps](#supported-databases), we automatically fetch and display the details of the database schema below +- You can **view the columns of a table**, their data types, and relationships between tables +- You can also **search and filter** the set of tables that are listed in your schema based on table or column name + +![SQL Schema Explorer](https://res.cloudinary.com/pipedreamin/image/upload/v1717388737/Screenshot_2024-06-02_at_9.23.46_PM_q5lbxo.png) + +## Prepared Statements +Prepared statements let you safely execute SQL queries with dynamic inputs that are automatically defined as parameters, in order to help prevent SQL injection attacks. + +To reference dynamic data in a SQL query, simply use the standard `{{ }}` notation just like any other code step in Pipedream. For example, + +```sql +select * + from products + where name = {{steps.get_product_info.$return_value.name}} + and created_at > {{steps.get_product_info.$return_value.created_at}} +``` + +**Prepared statement:** +![Prepared statement](https://res.cloudinary.com/pipedreamin/image/upload/v1722568229/prepared-statement_lh1cat.png) + +**Computed statement:** +![Computed statement](https://res.cloudinary.com/pipedreamin/image/upload/v1722568229/computed-statement_pth31z.png) + + +When you include step references in your SQL query, Pipedream automatically converts your query to a prepared statement using placeholders with an array of params. + + +Below your query input, you can toggle between the computed and prepared statements: + + + + +## Getting Started + + +### Select the Execute SQL Query action +- From the step selector in the builder, select the **Query a Database** action for the [relevant database app](#supported-databases) + +![Select the Execute SQL Query action](https://res.cloudinary.com/pipedreamin/image/upload/v1718918862/database-actions-step-selector_sbrnzi.png) + +### Connect your account +- If you already have a connected account, select it from the dropdown. Otherwise, click **Connect Account** to configure the database connection. +- Follow the prompts to connect your account, then click **Test connection** to ensure Pipedream can successfully connect to your database + +![Connect and test your account](https://res.cloudinary.com/pipedreamin/image/upload/v1717387003/test-connection-mysql_bizpqk.png) + +### Explore the schema +- Once you've successfully connected your account, you can explore the [database schema](#schema-explorer) to understand the tables and columns in your database + +### Write and execute your query +- Write your SQL query in the editor — read more about [prepared statements](#prepared-statements) above to reference dynamic data in your query + + + +### Supported Databases +The [SQL editor](#sql-editor), [schema explorer](#schema-explorer), and support for [prepared statements](#prepared-statements) are currently supported for these datbase apps: +- [MySQL](https://pipedream.com/apps/mysql) +- [PostgreSQL](https://pipedream.com/apps/postgresql) +- [Snowflake](https://pipedream.com/apps/snowflake) + +Need to query a different database type? Let us know! \ No newline at end of file diff --git a/docs-v2/pages/workflows/data-management/destinations/_meta.tsx b/docs-v2/pages/workflows/data-management/destinations/_meta.tsx new file mode 100644 index 0000000000000..36cf8794ff41c --- /dev/null +++ b/docs-v2/pages/workflows/data-management/destinations/_meta.tsx @@ -0,0 +1,8 @@ +export default { + "index": "Overview", + "email": "Email", + "http": "HTTP", + "emit": "Emit", + "sse": "SSE", + "s3": "S3", +} as const diff --git a/docs-v2/pages/destinations/email.mdx b/docs-v2/pages/workflows/data-management/destinations/email.mdx similarity index 80% rename from docs-v2/pages/destinations/email.mdx rename to docs-v2/pages/workflows/data-management/destinations/email.mdx index 8897b1322b0fa..4a14254b64123 100644 --- a/docs-v2/pages/destinations/email.mdx +++ b/docs-v2/pages/workflows/data-management/destinations/email.mdx @@ -2,7 +2,7 @@ The Email Destination allows you send an email to _yourself_ — the email address tied to the account you signed up with — at any step of a workflow. -You can use this to email yourself when you receive a specific event, for example when a user signs up on your app. You can send yourself an email when a cron job finishes running, or when a job fails. Anywhere you need an email notification, you can use the Email Destination! +You can use this to email yourself when you receive a specific event, for example when a user signs up on your app. You can send yourself an email when a cron job finishes running, or when a job fails. Anywhere you need an email notification, you can use the Email Destination! @@ -15,7 +15,7 @@ You can use this to email yourself when you receive a specific event, for exampl ### Using `$.send.email` in workflows -You can send data to an Email Destination in [Node.js code steps](/code/nodejs/), too, using the `$.send.email()` function. **This allows you to send emails to yourself programmatically, if you need more control than actions provide**. +You can send data to an Email Destination in [Node.js code steps](/workflows/building-workflows/code/nodejs/), too, using the `$.send.email()` function. **This allows you to send emails to yourself programmatically, if you need more control than actions provide**. `$.send.email()` takes the same parameters as the corresponding action: @@ -37,9 +37,9 @@ Like with any `$.send` function, you can use `$.send.email()` conditionally, wit ### Using `$.send.email` in component actions -If you're authoring a [component action](/components#actions), you can deliver data to an email destination using `$.send.email`. +If you're authoring a [component action](/workflows/contributing/components/#actions), you can deliver data to an email destination using `$.send.email`. -`$.send.email` functions the same as [`$.send.email` in workflow code steps](#using-send-email-in-workflows): +`$.send.email` functions the same as [`$.send.email` in workflow code steps](#using-sendemail-in-workflows): ```javascript export default defineComponent({ @@ -56,4 +56,3 @@ export default defineComponent({ ## Delivery details All emails come from **notifications@pipedream.com**. - diff --git a/docs-v2/pages/destinations/emit.mdx b/docs-v2/pages/workflows/data-management/destinations/emit.mdx similarity index 87% rename from docs-v2/pages/destinations/emit.mdx rename to docs-v2/pages/workflows/data-management/destinations/emit.mdx index e07d4588d852e..2a693efd57e72 100644 --- a/docs-v2/pages/destinations/emit.mdx +++ b/docs-v2/pages/workflows/data-management/destinations/emit.mdx @@ -1,13 +1,11 @@ # Emit events -Like [event sources](/sources/), workflows can emit events. These events can trigger other workflows, or be consumed using Pipedream's [REST API](/api/rest/#get-workflow-emits). - - +Like [event sources](/workflows/building-workflows/triggers/), workflows can emit events. These events can trigger other workflows, or be consumed using Pipedream's [REST API](/rest-api/#get-workflow-emits). ## Using `$.send.emit()` in workflows -You can emit arbitrary events from any [Node.js code steps](/code/nodejs/) using `$.send.emit()`. +You can emit arbitrary events from any [Node.js code steps](/workflows/building-workflows/code/nodejs/) using `$.send.emit()`. ```javascript export default defineComponent({ @@ -49,9 +47,9 @@ export default defineComponent({ ## Using `$.send.emit()` in component actions -If you're authoring a [component action](/components#actions), you can emit data using `$.send.emit()`. +If you're authoring a [component action](/workflows/contributing/components/#actions), you can emit data using `$.send.emit()`. -`$.send.emit()` functions the same as [`$.send.emit()` in workflow code steps](#using-send-emit-in-workflows): +`$.send.emit()` functions the same as [`$.send.emit()` in workflow code steps](#using-sendemit-in-workflows): ```javascript export default defineComponent({ @@ -92,7 +90,7 @@ Here's how to configure a workflow to listen for emitted events. 1. Currently, you can't select emitted events as a workflow trigger from the Pipedream UI. We'll show you how add the trigger via API. First, pick an existing workflow where you'd like to receive emitted events. **If you want to start with a [new workflow](https://pipedream.com/new), just select the HTTP / Webhook trigger**. 2. This workflow is called the **listener**. The workflow where you'll use `$.send.emit()` is called the **emitter**. If you haven't created the emitter workflow yet, [do that now](https://pipedream.com/new). 3. Get the workflow IDs of both the listener and emitter workflows. **You'll find the workflow ID in the workflow's URL in your browser bar — it's the `p_abc123` in `https://pipedream.com/@username/p_abc123/`**. -4. You can use the Pipedream REST API to configure the listener to receive events from the emitter. We call this [creating a subscription](/api/rest/#listen-for-events-from-another-source-or-workflow). If your listener's ID is `p_abc123` and your emitter's ID is `p_def456`, you can run the following command to create this subscription: +4. You can use the Pipedream REST API to configure the listener to receive events from the emitter. We call this [creating a subscription](/rest-api/#listen-for-events-from-another-source-or-workflow). If your listener's ID is `p_abc123` and your emitter's ID is `p_def456`, you can run the following command to create this subscription: ```bash curl "https://api.pipedream.com/v1/subscriptions?emitter_id=dc_def456&listener_id=p_abc123" \ @@ -113,17 +111,16 @@ export default defineComponent({ }); ``` -This should trigger your listener, and you should see the same event in [the event inspector](/workflows/events/inspect/#the-inspector). +This should trigger your listener, and you should see the same event in [the event inspector](/workflows/building-workflows/inspect/#the-inspector). **Note**: Please upvote [this issue](https://github.com/PipedreamHQ/pipedream/issues/682) to see support for _adding_ emitted events as a workflow trigger in the UI. ## Consuming emitted events via REST API -`$.send.emit()` can emit any data you'd like. You can retrieve that data using Pipedream's REST API endpoint for [retrieving emitted events](/api/rest/#get-workflow-emits). +`$.send.emit()` can emit any data you'd like. You can retrieve that data using Pipedream's REST API endpoint for [retrieving emitted events](/rest-api/#get-workflow-emits). This can be helpful when you want a workflow to process data asynchronously using a workflow. You can save the results of your workflow with `$.send.emit()`, and only retrieve the results in batch when you need to using the REST API. ## Emit logs / troubleshooting Below your code step, you'll see both the data that was sent in the emit. If you ran `$.send.emit()` multiple times within the same code step, you'll see the data that was emitted for each. - diff --git a/docs-v2/pages/workflows/data-management/destinations/http.mdx b/docs-v2/pages/workflows/data-management/destinations/http.mdx new file mode 100644 index 0000000000000..591d238aa0dd4 --- /dev/null +++ b/docs-v2/pages/workflows/data-management/destinations/http.mdx @@ -0,0 +1,123 @@ +import Callout from '@/components/Callout' +import PublicIPs from '@/components/PublicIPs' + +# HTTP + +HTTP Destinations allow you to send data to another HTTP endpoint URL outside of Pipedream. This can be an endpoint you own and operate, or a URL tied to a service you use (for example, a [Slack Incoming Webhook](https://api.slack.com/incoming-webhooks)). + +## Using `$.send.http` in workflows + +You can send HTTP requests in [Node.js code steps](/workflows/building-workflows/code/nodejs/) using `$.send.http()`. + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + $.send.http({ + method: "POST", + url: "[YOUR URL HERE]", + data: { + name: "Luke Skywalker", + }, + }); + } +}); +``` + +`$.send.http()` accepts an object with all of the following properties: + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + $.send.http({ + method, // Required, HTTP method, a string, e.g. POST, GET + url, // Required, the URL to send the HTTP request to + data, // HTTP payload + headers, // An object containing custom headers, e.g. { "Content-Type": "application/json" } + params, // An object containing query string parameters as key-value pairs + auth, // An object that contains a username and password property, for HTTP basic auth + }); + } +}); + +``` + +**Destination delivery is asynchronous**: the HTTP requests are sent after your workflow finishes. This means **you cannot write code that operates on the HTTP response**. These HTTP requests **do not** count against your workflow's compute time. + +If you iterate over an array of values and send an HTTP request for each: + +```javascript + +export default defineComponent({ + async run({ steps, $ }) { + const names = ["Luke", "Han", "Leia", "Obi Wan"]; + names.forEach((name) => { + $.send.http({ + method: "POST", + url: "[YOUR URL HERE]", + data: { + name, + }, + }); + }); + } +}); +``` + +you won't have to `await` the execution of the HTTP requests in your workflow. We'll collect every `$.send.http()` call and defer those HTTP requests, sending them after your workflow finishes. + +## Using `$.send.http` in component actions + +If you're authoring a [component action](/workflows/contributing/components/#actions), you can deliver data to an HTTP destination using `$.send.http`. + +`$.send.http` functions the same as [`$.send.http` in workflow code steps](#using-sendhttp-in-workflows): + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + $.send.http({ + method: "GET", + url: "https://example.com" + }) + } +}); +``` + +## HTTP Destination delivery + +HTTP Destination delivery is handled asynchronously, separate from the execution of a workflow. However, we deliver the specified payload to HTTP destinations for every event sent to your workflow. + +Generally, this means it should only take a few seconds for us to send the event to the destination you specify. In some cases, delivery will take longer. + +The time it takes to make HTTP requests sent with `$.send.http()` does not count against your workflow quota. + +## HTTP request and response logs + +Below your code step, you'll see both the data that was sent in the HTTP request, and the HTTP response that was issued. If you issue multiple HTTP requests, we'll show the request and response data for each. + +## What if I need to access the HTTP response in my workflow? + +Since HTTP requests sent with `$.send.http()` are sent asynchronously, after your workflow runs, **you cannot access the HTTP response in your workflow**. + +If you need to access the HTTP response data in your workflow, [use `axios`](/workflows/building-workflows/code/nodejs/http-requests/) or another HTTP client. + +## Timeout + +The timeout on HTTP request sent with `$.send.http()` is currently **5 seconds**. This time includes DNS resolution, connecting to the host, writing the request body, server processing, and reading the response body. + +Any requests that exceed 5 seconds will yield a `timeout` error. + +## Retries + +Currently, Pipedream will not retry any failed request. If your HTTP destination endpoint is down, or returns an error response, we'll display that response in the observability associated with the Destination in the relevant step. + +## IP addresses for Pipedream HTTP requests + + +These IP addresses are tied to **requests sent with `$.send.http` only, not other HTTP requests made from workflows**. To whitelist standard HTTP requests from Pipedream workflows, [use VPCs](/workflows/vpc/). + + +When you make an HTTP request using `$.send.http()`, the traffic will come from one of the following IP addresses: + + + +This list may change over time. If you've previously whitelisted these IP addresses and are having trouble sending HTTP requests to your target service, please check to ensure this list matches your firewall rules. diff --git a/docs-v2/pages/workflows/data-management/destinations/index.mdx b/docs-v2/pages/workflows/data-management/destinations/index.mdx new file mode 100644 index 0000000000000..2a6e95ea6814d --- /dev/null +++ b/docs-v2/pages/workflows/data-management/destinations/index.mdx @@ -0,0 +1,99 @@ +# Destinations + +**Destinations**, like [actions](/workflows/contributing/components/#actions), abstract the delivery and connection logic required to send events to services like Amazon S3, or targets like HTTP and email. + +However, Destinations are different than actions in two ways: + +- Events are delivered to the Destinations asynchronously, after your workflow completes. This means you don't wait for network I/O (e.g. for HTTP requests or connection overhead for data warehouses) within your workflow code, so you can process more events faster. +- In the case of data stores like S3, you typically don't want to send every event on its own. This can be costly and carries little benefit. Instead, you typically want to batch a collection of events together, sending the batch at some frequency. Destinations handle that batching for relevant services. + +The docs below discuss features common to all Destinations. See the [docs for a given destination](#available-destinations) for information specific to those destinations. + + + +## Available Destinations + +- [HTTP](/workflows/data-management/destinations/http/) +- [Email](/workflows/data-management/destinations/email/) +- [S3](/workflows/data-management/destinations/s3/) +- [SSE](/workflows/data-management/destinations/sse/) +- [Emit to another listener](/workflows/data-management/destinations/emit/) + +## Using destinations + +### Using destinations in workflows + +You can send data to Destinations in [Node.js code steps](/workflows/building-workflows/code/nodejs/), too, using `$.send` functions. + +`$.send` is an object provided by Pipedream that exposes destination-specific functions like `$.send.http()`, `$.send.s3()`, and more. This allows you to send data to destinations programmatically, if you need more control than the default actions provide. + +Let's use `$.send.http()` to send an HTTP POST request like we did in the Action example above. [Add a new action](/workflows/building-workflows/actions/), then search for "**Run custom code**": + +Create a new HTTP endpoint URL (try creating a new Pipedream workflow and adding an HTTP trigger), and add the code below to your code step, with the URL you created: + +```javascript +export default defineComponent({ + async run({ steps, $}) { + $.send.http({ + method: "POST", + url: "[YOUR URL HERE]", + data: { + name: "Luke Skywalker", + }, + }); + } +}) +``` + +See the docs for the [HTTP destination](/workflows/data-management/destinations/http/) to learn more about all the options you can pass to the `$.send.http()` function. + +Again, it's important to remember that **Destination delivery is asynchronous**. If you iterate over an array of values and send an HTTP request for each: + +```javascript +export default defineComponent({ + async run({ steps, $}) { + const names = ["Luke", "Han", "Leia", "Obi Wan"]; + for (const name of names) { + $.send.http({ + method: "POST", + url: "[YOUR URL HERE]", + data: { + name, + }, + }); + } + } +}) +``` + +you won't have to `await` the execution of the HTTP requests in your workflow. We'll collect every `$.send.http()` call and defer those HTTP requests, sending them after your workflow finishes. + +### Using destinations in actions + +If you're authoring a [component action](/workflows/contributing/components/#actions), you can deliver data to destinations, too. `$.send` isn't directly available to actions like it is for workflow code steps. Instead, you use `$.send` to access the destination-specific functions: + +```javascript +export default { + name: "Action Demo", + key: "action_demo", + version: "0.0.1", + type: "action", + async run({ $ }) { + $.send.http({ + method: "POST", + url: "[YOUR URL HERE]", + data: { + name: "Luke Skywalker", + }, + }); + } +} +``` + +[See the component action API docs](/workflows/contributing/components/api/#actions) for more details. + +## Asynchronous Delivery + +Events are delivered to destinations _asynchronously_ — that is, separate from the execution of your workflow. **This means you're not waiting for network or connection I/O in the middle of your function, which can be costly**. + +Some destination payloads, like HTTP, are delivered within seconds. For other destinations, like S3 and SQL, we collect individual events into a batch and send the batch to the destination. See the [docs for a specific destination](#available-destinations) for the relevant batch delivery frequency. diff --git a/docs-v2/pages/destinations/s3.mdx b/docs-v2/pages/workflows/data-management/destinations/s3.mdx similarity index 92% rename from docs-v2/pages/destinations/s3.mdx rename to docs-v2/pages/workflows/data-management/destinations/s3.mdx index 1aa3d446565d1..93f27e0fcce2b 100644 --- a/docs-v2/pages/destinations/s3.mdx +++ b/docs-v2/pages/workflows/data-management/destinations/s3.mdx @@ -6,9 +6,9 @@ import PublicIPs from '@/components/PublicIPs' ## Using `$.send.s3` in workflows -You can send data to an S3 Destination in [Node.js code steps](/code/nodejs/) using `$.send.s3()`. +You can send data to an S3 Destination in [Node.js code steps](/workflows/building-workflows/code/nodejs/) using `$.send.s3()`. -`$.send.s3()` takes the following parameters: +`$.send.s3()` takes the following parameters: ```javascript $.send.s3({ @@ -22,9 +22,9 @@ Like with any `$.send` function, you can use `$.send.s3()` conditionally, within ## Using `$.send.s3` in component actions -If you're authoring a [component action](/components#actions), you can deliver data to an S3 destination using `$.send.s3`. +If you're authoring a [component action](/workflows/contributing/components/#actions), you can deliver data to an S3 destination using `$.send.s3`. -`$.send.s3` functions the same as [`$.send.s3` in workflow code steps](#using-send-s3-in-workflows): +`$.send.s3` functions the same as [`$.send.s3` in workflow code steps](#using-sends3-in-workflows): ```javascript async run({ $ }) { @@ -101,4 +101,3 @@ S3 provides a mechanism to [limit operations only from specific IP addresses](ht This list may change over time. If you've previously whitelisted these IP addresses and are having trouble uploading S3 objects, please check to ensure this list matches your firewall rules. - diff --git a/docs-v2/pages/destinations/sse.mdx b/docs-v2/pages/workflows/data-management/destinations/sse.mdx similarity index 94% rename from docs-v2/pages/destinations/sse.mdx rename to docs-v2/pages/workflows/data-management/destinations/sse.mdx index faa09e684900e..affc3170c2daa 100644 --- a/docs-v2/pages/destinations/sse.mdx +++ b/docs-v2/pages/workflows/data-management/destinations/sse.mdx @@ -1,6 +1,6 @@ # Server-Sent Events (SSE) -Pipedream supports [Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) (SSE) as a destination, enabling you to send events from a workflow directly to a client subscribed to the event stream. +Pipedream supports [Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) (SSE) as a destination, enabling you to send events from a workflow directly to a client subscribed to the event stream. ## What is SSE? @@ -16,7 +16,7 @@ Beyond web browsers, any program that's able to create an [`EventSource` interfa ## Sending data to an SSE Destination in workflows -You can send data to an SSE Destination in [Node.js code steps](/code/nodejs/) using the `$.send.sse()` function. +You can send data to an SSE Destination in [Node.js code steps](/workflows/building-workflows/code/nodejs/) using the `$.send.sse()` function. 1. Add a new step to your workflow 2. Select the option to **Run custom code** and choose the Node.js runtime. @@ -28,7 +28,7 @@ export default defineComponent({ $.send.sse({ channel: "events", // Required, corresponds to the event in the SSE spec payload: { // Required, the event payload - name: "Luke Skywalker" + name: "Luke Skywalker" } }); } @@ -61,7 +61,7 @@ you won't have to `await` the execution of the SSE Destination requests in your ## Using `$.send.sse` in component actions -If you're authoring a [component action](/components#actions), you can send events to an SSE destination using `$.send.sse`. +If you're authoring a [component action](/workflows/contributing/components/#actions), you can send events to an SSE destination using `$.send.sse`. `$.send.sse` functions the same as [`$.send.sse` in workflow code steps](#sending-data-to-an-sse-destination-in-workflows): @@ -86,7 +86,7 @@ Once you've sent events to an SSE Destination, you can start receiving a stream First, it's important to note that all events sent to an SSE destination within a workflow are sent to an SSE event stream specific to that workflow. The event stream is tied to the workflow's ID, which you can find by examining the URL of the pipeline in the Pipedream UI. For example, the `p_aBcDeF` in this URL is the pipeline ID: -![Pipeline ID](./images/pipeline-id.png) +![Pipeline ID](/images/destinations/pipeline-id.png) **Note that the `p_` prefix is part of the workflow ID**. @@ -110,7 +110,7 @@ If you've already sent events to your SSE destination, you should see those even It's easy to setup a simple webpage to `console.log()` all events from an event stream. You can find a lot more examples of how to work with SSE on the web, but this should help you understand the basic concepts. -You'll need to create two files in the same directory on your machine: an `index.html` file for the HTML. +You'll need to create two files in the same directory on your machine: an `index.html` file for the HTML. **index.html** diff --git a/docs-v2/pages/workflows/data-management/file-stores/_meta.tsx b/docs-v2/pages/workflows/data-management/file-stores/_meta.tsx new file mode 100644 index 0000000000000..f9e7d5f2243d3 --- /dev/null +++ b/docs-v2/pages/workflows/data-management/file-stores/_meta.tsx @@ -0,0 +1,3 @@ +export default { + "index": "Overview", +} as const diff --git a/docs-v2/pages/workflows/data-management/file-stores/index.mdx b/docs-v2/pages/workflows/data-management/file-stores/index.mdx new file mode 100644 index 0000000000000..254febe8a18a2 --- /dev/null +++ b/docs-v2/pages/workflows/data-management/file-stores/index.mdx @@ -0,0 +1,333 @@ +import Callout from '@/components/Callout' + +# File Stores + + +In Preview + +File Stores are available in Preview. There may be changes to allowed limits in the future. + +If you have any feedback on File Stores, please let us know in our [community](https://pipedream.com/support). + + +File Stores are a filesystem that are scoped to a Project. All workflows within the same Project have access to the File Stores. + +You can interact with these files through the Pipedream Dashboard or programmatically through your Project's workflows. + +Unlike files stored within a workflow's `/tmp` directory which are subject to deletion between executions, File Stores are separate cloud storage. Files within a File Store can be long term storage accessible by your workflows. + +![File Stores are scoped to Projects, and workflows within the Project can interact with files stored](https://res.cloudinary.com/pipedreamin/image/upload/v1700156062/docs/docs/Project%20Files/Untitled_7_cn9njj.png) + + + +## Managing File Stores from the Dashboard + +You can access a File Store by opening the Project and selecting the *File Store* on the left hand navigation menu. + +![Opening a project's file store in the Pipedream Dashboard.](https://res.cloudinary.com/pipedreamin/image/upload/v1698934897/docs/docs/Project%20Files/CleanShot_2023-11-02_at_10.21.15_2x_z5q5nt.png) + +### Uploading files to the File Store + +To upload a file, select *New* then select *File*: + +![Opening the new file pop-up in a Project's File Store](https://res.cloudinary.com/pipedreamin/image/upload/v1698934655/docs/docs/Project%20Files/CleanShot_2023-11-02_at_10.16.13_fqjuv4.gif) + +Then in the new pop-up, you can either drag and drop or browser your computer to stage a file for uploading: + +![Choose to either drag and drop a file to upload or browse your local filesystem to upload the file](https://res.cloudinary.com/pipedreamin/image/upload/v1698934655/docs/docs/Project%20Files/CleanShot_2023-11-02_at_10.16.19_2x_w7z8wv.png) + +Now that the file(s) are staged for uploaded. Click *Upload* to upload them: + +![Confirm the upload to the file store](https://res.cloudinary.com/pipedreamin/image/upload/v1698934657/docs/docs/Project%20Files/CleanShot_2023-11-02_at_10.16.49_2x_ha4scn.png) + +Finally, click *Done* to close the upload pop-up: + +![Closing the file store upload modal](https://res.cloudinary.com/pipedreamin/image/upload/v1698934659/docs/docs/Project%20Files/CleanShot_2023-11-02_at_10.17.01_2x_xmfqfi.png) + +You should now see your file is uploaded and available for use within your Project: + +![The file is now uploaded to the File Store](https://res.cloudinary.com/pipedreamin/image/upload/v1698935114/docs/docs/Project%20Files/CleanShot_2023-11-02_at_10.24.56_2x_ogoh5t.png) + +### Deleting files from the File Store + +You can delete individual files from a File Store by clicking the three dot menu on the far right of the file and selecting *Delete*. + +![Deleting a Project File from the Dashboard](https://res.cloudinary.com/pipedreamin/image/upload/v1698855610/docs/docs/Project%20Files/CleanShot_2023-11-01_at_12.19.20_lb4ddt.png) + +After confirming that you want to delete the file, it will be permanently deleted. + + +File deletion is permanent + +Once a file is deleted, it's not possible to recover it. Please take care when deleting files from File Stores. + + +## Managing File Stores from Workflows + +Files uploaded to a File Store are accessible by workflows within that same project. + +You can access these files programmatically using the `$.files` helper within Node.js code steps. + + + +File Stores are scoped to Projects + +Only workflows within the same project as the File Store can access the files. Workflows outside of the project will not be able to access that project's File Store. + + +### Listing files in the File Store + +The `$.files.dir()` method allows you to list files and directories within the Project's File Store. By default it will list the files at the root directory. + +Here's an example of how to iterate over the files in the root directory and open them as `File` instances: + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + // list all contents of the root File Stores directory in this project + const dirs = $.files.dir(); + let files = []; + + for await(const dir of dirs) { + // if this is a file, let's open it + if(dir.isFile()) { + files.push(dir.path) + } + } + + return files + }, +}) +``` + +### Opening files + +To interact with a file uploaded to the File Store, you'll first need to open it. + +Given there's a file in the File Store called `example.png`, you can open it using the `$.files.open()` method: + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + // Open the file by it's path in the File Store + const file = $.files.open('example.png') + // Log the S3 url to access the file publicly + return await file.toUrl() + }, +}) +``` + +Once the file has been opened, you can [read, write, delete the file and more](/workflows/projects/file-stores/reference/). + +### Uploading files to File Stores + +You can upload files using Node.js code in your workflows, either from URLs, from the `/tmp` directory in your workflows or directly from streams for high memory efficency. + +#### Uploading files from URLs + +`File.fromUrl()` can upload a file from a public URL to the File Store. + +First open a new file at a specific path in the File Store, and then pass a URL to the `fromUrl` method on that new file: + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + // Upload a file to the File Store by a URL + const file = await $.files.open('pipedream.png').fromUrl('https://res.cloudinary.com/pipedreamin/image/upload/t_logo48x48/v1597038956/docs/HzP2Yhq8_400x400_1_sqhs70.jpg') + + // display the uploaded file's URL from the File Store: + console.log(await file.toUrl()) + }, +}) +``` + +#### Uploading files from the workflow's `/tmp` directory + +`File.fromFile()` can upload a file stored within the workflow's `/tmp` directory to the File Store. + +First open a new file at a specific path in the File Store, and then pass a URL to the `fromFile` method on that new file: + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + // Upload a file to the File Store from the local /tmp/ directory + const file = await $.files.open('recording.mp3').fromFile('/tmp/recording.mp3') + + // Display the URL to the File Store hosted file + console.log(await file.toUrl()) + }, +}) +``` + +#### Uploading files using streams + +File Stores also support streaming to write large files. `File.createWriteStream()` creates a write stream for the file to upload to. Then you can pair this stream with a download stream from another remote location: + +```javascript +import { pipeline } from 'stream/promises'; +import got from 'got' + +export default defineComponent({ + async run({ steps, $ }) { + const writeStream = await $.files.open('logo.png').createWriteStream() + + const readStream = got.stream('https://pdrm.co/logo') + + await pipeline(readStream, writeStream); + }, +}) +``` + +Additionally, you can pass a `ReadableStream` instance directly to a File instance: + +```javascript +import got from 'got' + +export default defineComponent({ + async run({ steps, $ }) { + // Start a new read stream + const readStream = got.stream('https://pdrm.co/logo') + + // Populate the file's content from the read stream + await $.files.open("logo.png").fromReadableStream(readStream) + }, +}) +``` + + +(Recommended) Pass the contentLength if possible + +If possible, pass a `contentLength` argument, then File Store will be able to efficiently stream to use less memory. Without a `contentLength` argument, the entire file will need to be downloaded to `/tmp/` until it can be uploaded to the File store. + + + +### Downloading files + +File Stores live in cloud storage by default, but files can be downloaded to your workflows individually. + +#### Downloading files to the workflow's `/tmp` directory + +First open a new file at a specific path in the File Store, and then call the `toFile()` method to download the file to the given path: + +```javascript +import fs from 'fs'; + +export default defineComponent({ + async run({ steps, $ }) { + // Download a file from the File Store to the local /tmp/ directory + const file = await $.files.open('recording.mp3').toFile('/tmp/README.md') + + // read the file version of the file stored in /tmp + return (await fs.promises.readFile('/tmp/README.md')).toString() + }, +}) +``` + + +Only the `/tmp/` directory is readable and writable + +Make sure that your path to `toFile(path)` includes + + +### Passing files between steps + +Files can be passed between steps. Pipedream will automatically serialize the file as a JSON _description_ of the file. Then when you access the file as a step export as a prop in a Node.js code step, then you can interact with the `File` instance directly. + +For example, if you have a file stored at the path `logo.png` within your File Store, then within a Node.js code step you can open it: + +```javascript +// "open_file" Node.js code step +export default defineComponent({ + async run({ steps, $ }) { + // Return data to use it in future steps + const file = $.files.open('logo.png') + + return file + }, +}) +``` + +Then in a downstream code step, you can use it via the `steps` path: + +```javascript +// "get_file_url" Node.js code step +export default defineComponent({ + async run({ steps, $ }) { + // steps.open_file.$return_value is automatically parsed back into a File instance: + return await steps.open_file.$return_value.toUrl() + }, +}) +``` + + +Files descriptions are compatible with other workflow helpers + +Files can also be used with `$.flow.suspend()` and `$.flow.delay()`. + + +#### Handling lists of files + +One limitation of the automatic parsing of files between steps is that it currently doesn't automatically handle lists of files between steps. + +For example, if you have a step that returns an array of `File` instances: + +```javascript +// "open_files" Node.js code step +export default defineComponent({ + async run({ steps, $ }) { + // Return data to use it in future steps + const file1 = $.files.open('vue-logo.svg') + const file2 = $.files.open('react-logo.svg') + + return [file1, file] + }, +}) +``` + +Then you'll need to use `$.files.openDescriptor` to parse the JSON definition of the files back into `File` instances: + +```javascript +// "parse_files" Node.js code step +export default defineComponent({ + async run({ steps, $ }) { + const files = steps.open_files.$return_value.map(object => $.files.openDescriptor(object)) + + // log the URL to the first File + console.log(await files[0].toUrl()); + }, +}) +``` + +### Deleting files + +You can call `delete()` on the file to delete it from the File Store. + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + // Open the file and delete it + const file = await $.files.open('example.png').delete() + console.log('File deleted.') + }, +}) +``` + + +Deleting files is irreversible + +It's not possible to restore deleted files. Please take care when deleting files. + + +## FAQ + +### Are there size limits for files within File Stores? + +At this time no, but File Stores are in preview and are subject to change. + +### Are helpers available for Python to download, upload and manage files? + +At this time no, only Node.js includes a helper to interact with the File Store programmatically within workflows. + +### Are File Stores generally available? + +At this time File Stores are only available to Advanced plan and above subscribed workspaces. You can change your plan within the [pricing page](https://pipedream.com/pricing). diff --git a/docs-v2/pages/workflows/data-management/file-stores/reference.mdx b/docs-v2/pages/workflows/data-management/file-stores/reference.mdx new file mode 100644 index 0000000000000..16791feca680f --- /dev/null +++ b/docs-v2/pages/workflows/data-management/file-stores/reference.mdx @@ -0,0 +1,247 @@ +import Callout from '@/components/Callout' + +# File Stores Node.js Reference + +The File Stores Node.js helper allows you to manage files within Code Steps and Action components. + + + +## `$.files` + +The `$.files` helper is the main module to interact with the Project's File Store. It can instatiate new files, open files from descriptors and list the contents of the File Store. + +### `$.files.open(path)` + +*Sync.* Opens a file from the relative `path`. If the file doesn't exist, a new empty file is created. + +### `$.files.openDescriptor(fileDescriptor)` + +*Sync.* Creates a new `File` from the JSON friendly description of a file. Useful for recreating a `File` from a step export. + +For example, export a `File` as a step export which will render the `File` as JSON: + +```javascript +// create_file +// Creates a new Project File and uploads an image to it +export default defineComponent({ + async run({ steps, $ }) { + // create the new file and upload the contents to it from a URL + const file = await $.files.open("imgur.png").fromUrl("https://i.imgur.com/TVIPgNq.png") + // return the file as a step export + return file + }, +} +``` + +Then in a downstream step recreate the `File` instance from the step export friendly _description_: + +```javascript +// download_file +// Opens a file downloaded from a previous step, and saves it. +export default defineComponent({ + async run({ steps, $ }) { + // Convert the the description of the file back into a File instance + const file = $.files.openDescriptor(steps.create_file.$return_value) + // Download the file to the local /tmp directory + await $.file.download('/tmp/example.png') + console.log("File downloaded to /tmp") + }, +}) + +``` + +### `$.files.dir(?path)` + +*Sync.* Lists the files & directories at the given `path`. By default it will list the files at the root directory. + +Here's an example of how to iterate over the files in the root directory and open them as `File` instances: + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + // list all contents of the root File Stores directory in this project + const dirs = $.files.dir(); + let files = []; + + for await(const dir of dirs) { + // if this is a file, let's open it + if(dir.isFile()) { + files.push(await $.files.open(dir.path)) + } + } + + return files + }, +}) +``` + +Each iteratee of `$.files.dir()` will contain the following properties: + +* `isDirectory()` - `true` if this instance is a directory. +* `isFile()` - `true` if this instance is a file. +* `path` - The path to the file. +* `size` - The size of the file in bytes. +* `modifiedAt` - The last modified at timestamp. + +## `File` + +This class describes an instance of a single file within a File Store. + +When using `$.files.open` or `$.files.openDescriptor`, you'll create a new instance of a `File` with helper methods that give you more flexibility to perform programatic actions with the file. + +### `File.toUrl()` + +*Async.* The pre-signed GET URL to retrieve the file. + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + // Retrieve the pre-signed GET URL for logo.png + const url = await $.files.open('logo.png').toUrl() + + return url + }, +}) + +``` + + +Pre-signed GET URLs are short lived. + +The `File.toUrl()` will expire after 30 minutes. + + +### `File.toFile(path)` + +*Async.* Downloads the file to the local path in the current workflow. If the file doesn't exist, a new one will be created at the path specified. + + +Only `/tmp` is writable in workflow environments + +Only the `/tmp` directory is writable in your workflow's exection environment. So you must download your file to the `/tmp` directory. + + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + // Download the file in the File Store to the workflow's /tmp/ directory + await $.files.open('logo.png').toFile("/tmp/logo.png") + }, +}) + +``` + +### `File.toBuffer()` + +*Async.* Downloads the file as a Buffer to create readable or writeable streams. + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + // opens a file at the path "hello.txt" and downloads it as a Buffer + const buffer = await $.files.open('hello.txt').toBuffer() + // Logs the contents of the Buffer as a string + console.log(buffer.toString()) + }, +}) +``` + +### `File.fromFile(localFilePath, ?contentType)` + +*Async.* Uploads a file from the file at the `/tmp` local path. For example, if `localFilePath` is given `/tmp/recording.mp3`, it will upload that file to the current File Store File instance. + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + // Upload a file to the File Store from the local /tmp/ directory + const file = await $.files.open('recording.mp3').fromFile('/tmp/recording.mp3') + + console.log(file.url) + }, +}) +``` + +### `File.fromUrl(url)` + +*Async.* Accepts a `url` to read from. + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + // Upload a file to the File Store by a URL + const file = await $.files.open('pipedream.png').fromUrl('https://res.cloudinary.com/pipedreamin/image/upload/t_logo48x48/v1597038956/docs/HzP2Yhq8_400x400_1_sqhs70.jpg') + + console.log(file.url) + }, +}) +``` + +### `File.createWriteStream(?contentType, ?contentLength)` + +*Async.* Creates a write stream to populate the file with. + + +Pass the content length if possible + +The `contentLength` argument is optional, however we do recommend passing it. Otherwise the entire file will need to be written to the local `/tmp` before it can be uploaded to the File store. + + +```javascript +import { pipeline } from 'stream/promises'; +import got from 'got' + +export default defineComponent({ + async run({ steps, $ }) { + const writeStream = await $.files.open('logo.png').createWriteStream("image/png", 2153) + + const readStream = got.stream('https://pdrm.co/logo') + + await pipeline(readStream, writeStream); + }, +}) +``` + +### `File.fromReadableStream(?contentType, ?contentLength)` + +*Async.* Populates a file's contents from the `ReadableStream`. + + +Pass the content length if possible + +The `contentLength` argument is optional, however we do recommend passing it. Otherwise the entire file will need to be written to the local `/tmp` before it can be uploaded to the File store. + + +```javascript +import got from 'got' + +export default defineComponent({ + async run({ steps, $ }) { + // Start a new read stream + const readStream = got.stream('https://pdrm.co/logo') + + // Populate the file's content from the read stream + await $.files.open("logo.png").fromReadableStream(readStream, "image/png", 2153) + }, +}) +``` + +### `File.delete()` + +*Async.* Deletes the Project File. + +```javascript +export default defineComponent({ + async run({ steps, $ }) { + // Open the Project File and delete it + const file = await $.files.open('example.png').delete() + + console.log('File deleted.') + }, +}) +``` + + +Deleting files is irreversible + +It's not possible to restore deleted files. Please take care when deleting files. + diff --git a/docs-v2/pages/workflows/domains.mdx b/docs-v2/pages/workflows/domains.mdx index 07eee9f4ba9e7..8d0da7c81cd7d 100644 --- a/docs-v2/pages/workflows/domains.mdx +++ b/docs-v2/pages/workflows/domains.mdx @@ -1,6 +1,6 @@ # Custom Domains -By default, all new [Pipedream HTTP endpoints](/workflows/steps/triggers/#http) are hosted on the **{process.env.ENDPOINT_BASE_URL}** domain. But you can configure any domain you want: instead of `https://endpoint.m.pipedream.net`, the endpoint would be available on `https://endpoint.example.com`. +By default, all new [Pipedream HTTP endpoints](/workflows/building-workflows/triggers/#http) are hosted on the **{process.env.ENDPOINT_BASE_URL}** domain. But you can configure any domain you want: instead of `https://endpoint.m.pipedream.net`, the endpoint would be available on `https://endpoint.example.com`. ## Configuring a new custom domain @@ -32,11 +32,11 @@ Before you move on, make sure you have access to manage DNS records for your dom #### A note on domain wildcards -Note that the records referenced above use the wildcard (`*`) for the host portion of the domain. When you configure DNS records in [step 3](#_3-add-your-dns-records), this allows you to point all traffic for a specific domain to Pipedream and create any number of Pipedream HTTP endpoints that will work with your domain. +Note that the records referenced above use the wildcard (`*`) for the host portion of the domain. When you configure DNS records in [step 3](#3-add-your-dns-records), this allows you to point all traffic for a specific domain to Pipedream and create any number of Pipedream HTTP endpoints that will work with your domain. ### 2. Reach out to Pipedream Support -Once you've chosen your domain, [reach out to Pipedream Support](https://pipedream.com/support) and let us know what domain you'd like to configure for your workspace. We'll configure a TLS/SSL certificate for that domain, and give you two DNS CNAME records to add for that domain in [step 3](#_3-add-your-dns-records). +Once you've chosen your domain and are in an [eligible plan](https://pipedream.com/pricing), [reach out to Pipedream Support](https://pipedream.com/support) and let us know what domain you'd like to configure for your workspace. We'll configure a TLS/SSL certificate for that domain, and give you two DNS CNAME records to add for that domain in [step 3](#3-add-your-dns-records). ### 3. Add your DNS records diff --git a/docs-v2/pages/environment-variables.mdx b/docs-v2/pages/workflows/environment-variables.mdx similarity index 84% rename from docs-v2/pages/environment-variables.mdx rename to docs-v2/pages/workflows/environment-variables.mdx index 5ab87eb184222..4271eed10e2cf 100644 --- a/docs-v2/pages/environment-variables.mdx +++ b/docs-v2/pages/workflows/environment-variables.mdx @@ -6,12 +6,12 @@ Environment variables (env vars) enable you to separate secrets and other static You shouldn't include API keys or other sensitive data directly in your workflow's code. By referencing the value of an environment variable instead, your workflow includes a reference to that variable — for example, `process.env.API_KEY` instead of the API key itself. -You can reference env vars and secrets in [workflow code](/code/) or in the object explorer when passing data to steps, and you can define them either globally for the entire workspace, or scope them to individual projects. +You can reference env vars and secrets in [workflow code](/workflows/building-workflows/code/) or in the object explorer when passing data to steps, and you can define them either globally for the entire workspace, or scope them to individual projects. | Scope | Description | | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Workspace** | All environment variables are available to all workflows within the workspace. All workspace members can manage workspace-wide variables [in the UI](https://pipedream.com/settings/env-vars). | -| **Project** | Environment variables defined within a project are only accessible to the workflows within that project. Only workspace members who have [access to the project](/projects/#access-controls) can manage project variables. | +| **Project** | Environment variables defined within a project are only accessible to the workflows within that project. Only workspace members who have [access to the project](/workflows/projects/access-controls) can manage project variables. | ## Creating and updating environment variables @@ -73,7 +73,7 @@ Reference the same environment variable in Python: ```python import os -print(os.environ[API_KEY]) +print(os.environ["API_KEY"]) ``` Variable names are case-sensitive. Use the key name you defined when referencing your variable in `process.env`. @@ -104,7 +104,7 @@ When referencing env vars directly in code within your Pipedream workflow, you c ## Referencing environment variables in actions -[Actions](/components#actions) are pre-built code steps that let you provide input in a form, selecting the correct params to send to the action. +[Actions](/workflows/contributing/components/#actions) are pre-built code steps that let you provide input in a form, selecting the correct params to send to the action. You can reference the value of environment variables using `{{process.env.YOUR_ENV_VAR}}`. You'll see a list of your environment variables in the object explorer when selecting a variable to pass to a step. @@ -114,7 +114,11 @@ You can reference the value of environment variables using `{{process.env.YOUR_E src="https://res.cloudinary.com/pipedreamin/image/upload/v1708551738/env-vars-feb-2024/env-vars-object-explorer-v2_x9afzl.png" /> -## Frequently Asked Questions + + [Private components](/workflows/contributing/components/#using-components) (actions or triggers) do not have direct access to workspace or project variables as public components or code steps. Add a prop specifically for the variable you need. For sensitive data like API keys, [configure the prop as a secret](/workflows/contributing/components/api/#props). In your prop configuration, set the value to `{{process.env.YOUR_ENV_VAR}}` to securely reference the environment variable. + + +## FAQ ### What if I define the same variable key in my workspace env vars and project env vars? @@ -122,7 +126,7 @@ The project-scoped variable will take priority if the same variable key exists a ### What happens if I share a workflow that references an environment variable? -If you [share a workflow](/workflows/sharing/) that references an environment variable, **only the reference is included, and not the actual value**. +If you [share a workflow](/workflows/building-workflows/sharing/) that references an environment variable, **only the reference is included, and not the actual value**. ## Limits diff --git a/docs-v2/pages/workflows/errors.mdx b/docs-v2/pages/workflows/errors.mdx deleted file mode 100644 index 2565cc149a5f3..0000000000000 --- a/docs-v2/pages/workflows/errors.mdx +++ /dev/null @@ -1,140 +0,0 @@ -import VideoPlayer from "@/components/VideoPlayer"; - -# Handling errors - -By default, [Pipedream sends an email](#default-system-emails) when a workflow throws an unhandled error. But you can - -- Send error notifications to Slack -- Handle errors from one workflow in a specific way -- Fetch errors asynchronously using the REST API, instead of handling the event in real-time - -These docs describe the default error behavior, and how to handle custom use cases like these. - -Before you jump into the examples below, remember that all Pipedream workflows are just code. You can always use the built-in error handling logic native to your programming language, for example: using JavaScript's [`try / catch` statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch). In the future, Pipedream plans to support this kind of error-handling for built-in actions, as well. - -## Default system emails - -Any time your workflow throws an unhandled error, you'll receive an email like this: - -![Example error email](https://res.cloudinary.com/pipedreamin/image/upload/v1656630943/docs/Screen_Shot_2022-06-29_at_6.20.42_PM_kmsjbr.png) - -This email includes a link to the error so you can see the data and logs associated with the run. When you inspect the data in the Pipedream UI, you'll see details on the error below the step that threw the error, e.g. the full stack trace. - -### Duplicate errors do not trigger duplicate emails - -High-volume errors can lead to lots of notifications, so Pipedream only sends at most one email, per error, per workflow, per 24 hour period. - -For example, if your workflow throws a `TypeError`, we'll send you an email, but if it continues to throw that same `TypeError`, we won't email you about the duplicate errors until the next day. If a different workflow throws a `TypeError`, you **will** receive an email about that. - -## Test mode vs. live mode - -When you're editing and testing your workflow, any unhandled errors will **not** raise errors as emails, nor are they forwarded to [error listeners](#process-errors-with-custom-logic-instead-of-email). Error notifications are only sent when a deployed workflow encounters an error on a live event. - -## Process errors with custom logic, instead of email - -Pipedream exposes a global stream of all errors, raised from all workflows. You can subscribe to this stream, triggering a workflow on every event. This lets you handle errors in a custom way. Instead of sending all errors to email, you can send them to Slack, Discord, AWS, or any other service, and handle them in any custom way. - -Watch this video to learn more, or check out the step-by-step docs below. - - - -
- -1. [Create a new workflow](https://pipedream.com/@/new/build?v=2). You do not need to add a trigger, since the workflow will be triggered on errors, which we'll configure next. -2. Create a subscription with the following configuration: - -- `emitter_id`: your workspace ID, found in your [Account Settings](https://pipedream.com/settings/account). -- `listener_id`: The [workflow ID](/troubleshooting/#where-do-i-find-my-workflow-s-id) from step #1 -- `event_name`: `$errors` - -For example, you can make this request with your Pipedream API key using `cURL`: - -```bash -curl -X POST \ - 'https://api.pipedream.com/v1/subscriptions?emitter_id=o_abc123&event_name=$errors&listener_id=p_abc123' \ - -H "Authorization: Bearer " -``` - -3. Generate an error in a live version of any workflow (errors raised while you're testing your workflow [do not send errors to the `$errors` stream](#test-mode-vs-live-mode)). You should see this error trigger the workflow in step #1. From there, you can build any logic you want to handle errors across workflows. - -### Duplicate errors _do_ trigger duplicate error events on custom workflows - -Unlike [the default system emails](#duplicate-errors-do-not-trigger-duplicate-emails), duplicate errors are sent to any workflow listening to the `$errors` stream. - -## Handle errors for one workflow using custom logic - -Every time a workflow throws an error, it emits an event to the `$errors` stream for that workflow. You can create [a subscription](/api/rest/#listen-for-events-from-another-source-or-workflow) that delivers these errors to a Pipedream workflow, webhook, and more. - -Let's walk through an end-to-end example: - -1. Pick the workflow whose errors you'd like to handle and note its [workflow ID](/troubleshooting/#where-do-i-find-my-workflow-s-id). -1. [Create a new workflow](https://pipedream.com/@/new/build?v=2). You do not need to add a trigger, since the workflow will be triggered on errors, which we'll configure next. Note the ID for this workflow, as well. -1. Here, you're going to add the errors from the workflow in step #1 as a trigger for the workflow you created in step #2. In other words, errors from workflow #1 will trigger workflow #2. - -Make the following request to the Pipedream API, replacing the `emitter_id` with the ID of workflow #1, and the `listener_id` with the ID of workflow #2. - -```bash -curl 'https://api.pipedream.com/v1/subscriptions?emitter_id=p_workflow1&listener_id=p_workflow2&event_name=$errors' \ - -X POST \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" -``` - -4. Generate an error in a live version of any workflow (errors raised while you're testing your workflow [do not send errors to the `$errors` stream](#test-mode-vs-live-mode)). You should see this error trigger the workflow in step #1. From there, you can build any logic you want to handle errors across workflows. - -## Poll the REST API for workflow errors - -Pipedream provides a REST API endpoint to [list the most recent 100 workflow errors](/api/rest/#get-workflow-errors) for any given workflow. For example, to list the errors from workflow `p_abc123`, run: - -```bash -curl 'https://api.pipedream.com/v1/workflows/p_abc123/$errors/event_summaries?expand=event' \ - -H 'Authorization: Bearer ' -``` - -By including the `expand=event` query string param, Pipedream will return the full error data, along with the original event that triggered your workflow: - -```json -{ - "page_info": { - "total_count": 100, - "start_cursor": "1606370816223-0", - "end_cursor": "1606370816223-0", - "count": 1 - }, - "data": [ - { - "id": "1606370816223-0", - "indexed_at_ms": 1606370816223, - "event": { - "original_event": { - "name": "Luke", - "title": "Jedi" - }, - "original_context": { - "id": "1kodJIW7jVnKfvB2yp1OoPrtbFk", - "ts": "2020-11-26T06:06:44.652Z", - "workflow_id": "p_abc123", - "deployment_id": "d_abc123", - "source_type": "SDK", - "verified": false, - "owner_id": "u_abc123", - "platform_version": "3.1.20" - }, - "error": { - "code": "InternalFailure", - "cellId": "c_abc123", - "ts": "2020-11-26T06:06:56.077Z", - "stack": " at Request.extractError ..." - } - }, - "metadata": { - "emitter_id": "p_abc123", - "emit_id": "1kodKnAdWGeJyhqYbqyW6lEXVAo", - "name": "$errors" - } - } - ] -} -``` - -By listing these errors, you may be able to replay them against your workflow programmatically. For example, if your workflow is triggered by HTTP requests, you can send an HTTP request with the data found in `event.original_event` (see the example above) for every event that errored. diff --git a/docs-v2/pages/workflows/event-history.mdx b/docs-v2/pages/workflows/event-history.mdx new file mode 100644 index 0000000000000..ad1830588de03 --- /dev/null +++ b/docs-v2/pages/workflows/event-history.mdx @@ -0,0 +1,99 @@ +import Callout from '@/components/Callout' +import Image from 'next/image' +import ArcadeEmbed from '@/components/ArcadeEmbed' + +# Event History + +Monitor all workflow events and their stack traces in one centralized view under the [**Event History**](https://pipedream.com/event-history) section in the dashboard. + +Within the **Event History**, you'll be able to filter your events by workflow, execution status, within a specific time range. + + + +## Filtering Events + +The filters at the top of the screen allow you to search all events processed by your workflows. + +You can filter by the event's **Status**, **time of initiation** or by the **Workflow name**. + + +The filters are scoped to the current [workspace](/workflows/workspaces/). If you're not seeing the events or workflow you're expecting, try [switching workspaces](/workflows/workspaces/#switching-between-workspaces). + + +### Filtering by status + +- The **Status** filter controls which events are shown by their status +- For example selecting the **Success** status, you'll see all workflow events that were successfully executed + +
+ +Only showing workflow events by current execution status + +#### All failed workflow executions + +- You can view all failed workflow executions by applying the **Error** status filter +- This will only display the failed workflow executions in the selected time period +- This view in particular is helpful for identifying trends of errors, or workflows with persistent problems + +Viewing all failed workflow executions by the filter in the Event History + +#### All paused workflow executions + +- Workflow executions that are currently in a suspended state from `$.flow.delay` or `$.flow.suspend` will be shown when this filter is selected + +Only showing workflows that are currently paused + + +If you're using `setTimeout` or `sleep` in Node.js or Python steps, the event will not be considered **Paused**. Using those language native execution holding controls leaves your workflow in a **Executing** state. + + +### Within a timeframe + +- Filtering by time frame will include workflow events that _began_ execution within the defined range +- Using this dropdown, you can select between convenient time ranges, or specify a custom range on the right side + +How to filter events by a timerange + +### Filtering by workflow + +You can also filter events by a specific workflow. You can search by the workflow's name in the search bar in the top right. + +![Search by workflow name in the search bar](https://res.cloudinary.com/pipedreamin/image/upload/v1683747588/docs/docs/event%20histories/CleanShot_2023-05-10_at_15.39.30_2x_yoa1k6.png) + +Alternatively, you can filter by workflow from a specific event. First, open the menu on the far right, then select **Filter By Workflow**. Then only events processed by that workflow will appear. + +![Filtering events by workflow by selecting the workflow on the right](https://res.cloudinary.com/pipedreamin/image/upload/v1683747695/docs/docs/event%20histories/CleanShot_2023-05-10_at_15.41.20_2x_ulvdns.png) + +## Inspecting events + +- Clicking on an individual event will open a panel that displays the steps executed, their individual configurations, as well as the overall performance and results of the entire workflow. +- The top of the event history details will display details including the overall status of that particular event execution and errors if any. +- If there is an error message, the link at the bottom of the error message will link to the corresponding workflow step that threw the error. +- From here you can easily **Build from event** or **Replay event** + +![Viewing individual event executions](https://res.cloudinary.com/pipedreamin/image/upload/v1683748495/docs/docs/event%20histories/CleanShot_2023-05-10_at_15.53.44_2x_t30gsb.png) + +## Bulk actions +You can select multiple events and perform bulk actions on them. + +- **Replay**: Replays the selected events. This is useful for example when you have multiple errored events that you want to execute again after fixing a bug in your workflow. +- **Delete**: Deletes the selected events. This may be useful if you have certain events you want to scrub from the event history, or when you've successfully replayed events that had originally errored. + + + + + +When you replay multiple events at once, they'll be replayed in the order they were originally executed. This means the first event that came in will be replayed first, followed by the second, and so on. + + +## Limits + +The number of events recorded and available for viewing in the Event History depends on your plan. [Please see the pricing page](https://pipedream.com/pricing) for more details. + +## FAQ + +### Is Event History available on all plans? + +Yes, event history is available for all workspace plans, including free plans. However, the length of searchable or viewable history changes depending on your plan. [Please see the pricing page](https://pipedream.com/pricing) for more details. diff --git a/docs-v2/pages/workflows/events.mdx b/docs-v2/pages/workflows/events.mdx deleted file mode 100644 index 779e2daf3f09a..0000000000000 --- a/docs-v2/pages/workflows/events.mdx +++ /dev/null @@ -1,120 +0,0 @@ -import Callout from '@/components/Callout' - -# Events - -Events trigger workflow executions. The event that triggers your workflow depends on the trigger you select for your workflow: - -- [HTTP triggers](/workflows/steps/triggers/#http) invoke your workflow on HTTP requests. -- [Cron triggers](/workflows/steps/triggers/#schedule) invoke your workflow on a time schedule (e.g., on an interval). -- [Email triggers](/workflows/steps/triggers/#email) invoke your workflow on inbound emails. -- [Event sources](/workflows/steps/triggers/#app-based-triggers) invoke your workflow on events from apps like Twitter, Google Calendar, and more. - ---- - - - -## Selecting a test event - -When you test any step in your workflow, Pipedream passes the test event you select in the trigger step: - -![Selecting a test event in the trigger](https://res.cloudinary.com/pipedreamin/image/upload/v1648758487/docs/components/CleanShot_2022-03-31_at_16.24.34_pb9jzt.png) - -You can select any event you've previously sent to your trigger as your test event, or send a new one. - -## Examining event data - -When you select an event, you'll see [the incoming event data](#event-format) and the [event context](#steps-trigger-context) for that event: - -![The event and context in a trigger initiation](https://res.cloudinary.com/pipedreamin/image/upload/v1648759141/docs/components/CleanShot_2022-03-31_at_16.30.37_jwwwdy.png) - -Pipedream parses your incoming data and exposes it in the variable [`steps.trigger.event`](#event-format), which you can access in any [workflow step](/workflows/steps/). - -## Copying references to event data - -When you're [examining event data](#examining-event-data), you'll commonly want to copy the name of the variable that points to the data you need to reference in another step. - -Hover over the property whose data you want to reference, and click the **Copy Path** button to its right: - -![Copy an event path](https://res.cloudinary.com/pipedreamin/image/upload/v1648759215/docs/components/CleanShot_2022-03-31_at_16.39.56_lsus2o.gif) - -## Copying the values of event data - -You can also copy the value of specific properties of your event data. Hover over the property whose data you want to copy, and click the **Copy Value** button to its right: - -![Copy event attribute value](https://res.cloudinary.com/pipedreamin/image/upload/v1648759275/docs/components/CleanShot_2022-03-31_at_16.41.02_xgzcsa.gif) - -## Event format - -When you send an event to your workflow, Pipedream takes the trigger data — for example, the HTTP payload, headers, etc. — and adds our own Pipedream metadata to it. - -**This data is exposed in the `steps.trigger.event` variable. You can reference this variable in any step of your workflow**. - -You can reference your event data in any [code](/code/) or [action](/components#actions) step. See those docs or the general [docs on passing data between steps](/workflows/steps/) for more information. - -The specific shape of `steps.trigger.event` depends on the trigger type: - -### HTTP - -| Property | Description | -| ----------- | :---------------------------------------------------: | -| `body` | A string or object representation of the HTTP payload | -| `client_ip` | IP address of the client that made the request | -| `headers` | HTTP headers, represented as an object | -| `method` | HTTP method | -| `path` | HTTP request path | -| `query` | Query string | -| `url` | Request host + path | - -### Cron Scheduler - -| Property | Description | -| --------------------- | :---------------------------------------------------------------------------------------------: | -| `interval_seconds` | The number of seconds between scheduled executions | -| `cron` | When you've configured a custom cron schedule, the cron string | -| `timestamp` | The epoch timestamp when the workflow ran | -| `timezone_configured` | An object with formatted datetime data for the given execution, tied to the schedule's timezone | -| `timezone_utc` | An object with formatted datetime data for the given execution, tied to the UTC timezone | - -### Email - -We use Amazon SES to receive emails for the email trigger. You can find the shape of the event in the [SES docs](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-notifications-contents.html). - -## `steps.trigger.context` - -`steps.trigger.event` contain your event's **data**. `steps.trigger.context` contains _metadata_ about the workflow and the execution tied to this event. - -You can use the data in `steps.trigger.context` to uniquely identify the Pipedream event ID, the timestamp at which the event invoked the workflow, and more: - -| Property | Description | -| ------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------: | -| `deployment_id` | A globally-unique string representing the current version of the workflow | -| `emitter_id` | The ID of the workflow trigger that emitted this event, e.g. the [event source](/sources) ID. | -| `id` | A unique, Pipedream-provided identifier for the event that triggered this workflow | -| `owner_id` | The Pipedream-assigned [workspace ID](/workspaces/#finding-your-workspace-s-id) that owns the workflow | -| `platform_version` | The version of the Pipedream execution environment this event ran on | -| `replay` | A boolean, whether the event was replayed via the UI | -| `trace_id` | Holds the same value for all executions tied to an original event. [See below for more details](#how-do-i-retrieve-the-execution-id-for-a-workflow). | -| `ts` | The ISO 8601 timestamp at which the event invoked the workflow | -| `workflow_id` | The workflow ID | -| `workflow_name` | The workflow name | - -### How do I retrieve the execution ID for a workflow? - -Pipedream exposes two identifies for workflow executions: one for the execution, and one for the "trace". - -`steps.trigger.context.id` should be unique for every execution of a workflow. - -`steps.trigger.context.trace_id` will hold the same value for all executions tied to the same original event, e.g. if you have auto-retry enabled and it retries a workflow three times, the `id` will change, but the `trace_id` will remain the same. For example, if you call `$.flow.suspend()` on a workflow, we run a new execution after the suspend, so you'd see two total executions: `id` will be unique before and after the suspend, but `trace_id` will be the same. - -You may notice other properties in `context`. These are used internally by Pipedream, and are subject to change. - -## Limits on events queue - -On the Free and Basic plans, only the last 100 events are retained for each workflow. After 100 events have been processed, Pipedream will delete the oldest event data as new events arrive, keeping only the last 100 events. - -The Advanced and Business tiers have an upgraded amount of events in the queue, [please see the pricing page](https://pipedream.com/pricing) for more details. - - -For an extended history of events across all of your workflows, included processed events, with the ability to filter by status and time range, please see the [Event History](/event-history/). - - diff --git a/docs-v2/pages/workflows/flow-control.mdx b/docs-v2/pages/workflows/flow-control.mdx deleted file mode 100644 index 6292f6a921c04..0000000000000 --- a/docs-v2/pages/workflows/flow-control.mdx +++ /dev/null @@ -1,74 +0,0 @@ -import VideoPlayer from "@/components/VideoPlayer"; - -# Flow Control - -Use Pipedream built-in functions to filter, delay, and perform other common flow control operations. - -## Delay - - - -Sometimes you need to wait a specific amount of time before the next step of your workflow proceeds. Pipedream supports this in one of two ways: - -1. The built-in **Delay** actions -2. The `$.flow.delay` function in Node.js - -{process.env.DELAY_MIN_MAX_TIME}. For example, we at Pipedream use this functionality to delay welcome emails to our customers until they've had a chance to use the product. - -### Delay actions - -You can pause your workflow without writing code using the **Delay** actions: - -1. Click the **+** button below any step -2. Search for the **Delay** app -3. Select the **Delay Workflow** action -4. Configure the action to delay any amount of time, up to one year - -![Workflow delay step](https://res.cloudinary.com/pipedreamin/image/upload/v1651551140/docs/Screen_Shot_2022-05-02_at_8.26.46_PM_hfhil5.png) - -### `$.flow.delay` - -If you need to delay a workflow within Node.js code, or you need detailed control over how delays occur, [see the docs on `$.flow.delay`](/code/nodejs/delay/). - -### The state of delayed executions - -Delayed executions can hold one of three states: - -- **Paused**: The execution is within the delay window, and the workflow is still paused -- **Resumed**: The workflow has been resumed at the end of its delay window automatically, or resumed manually -- **Cancelled**: The execution was cancelled manually - -You'll see the current state of an execution by [viewing its event data](/workflows/events/inspect/). - -### Cancelling or resuming execution manually - -The [**Delay** actions](#delay-actions) and [`$.flow.delay`](/code/nodejs/delay/) return two URLs each time they run: - -![Cancel and resume URLs](https://res.cloudinary.com/pipedreamin/image/upload/v1651551860/docs/Screen_Shot_2022-05-02_at_9.16.11_PM_ahw7tu.png) - -These URLs are specific to a single execution of your workflow. While the workflow is paused, you can load these in your browser or send an HTTP request to either: - -- Hitting the `cancel_url` will immediately cancel that execution -- Hitting the `resume_url` will immediately resume that execution early - -If you use [`$.flow.delay`](/code/nodejs/delay/), you can send these URLs to your own system to handle cancellation / resumption. You can even email your customers to let them cancel / resume workflows that run on their behalf. - -## Filter - - - -Use the Filter action to quickly stop or continue workflows on certain conditions. - -![Create a filter action](https://res.cloudinary.com/pipedreamin/image/upload/v1651603927/docs/animations/CleanShot_2022-05-03_at_13.51.17_za1skw.gif) - -Add a filter action to your workflow by searching for the **Filter** app in a new step. - -The **Filter** app includes several built-in actions: Continue Workflow on Condition, Exit Workflow on Condition and Exit Workflow on Custom Condition. - -In each of these actions, the **Value** is the subject of the condition, and the **Condition** is the operand to compare the value against. - -For example, to only process orders with a `status = ready` - -### Continue Workflow on Condition - -With this action, only when values that _pass_ a set condition will the workflow continue to execute steps after this filter. diff --git a/docs-v2/pages/workflows/git.mdx b/docs-v2/pages/workflows/git.mdx new file mode 100644 index 0000000000000..0ece3087c74ec --- /dev/null +++ b/docs-v2/pages/workflows/git.mdx @@ -0,0 +1,306 @@ +import Callout from '@/components/Callout' +import PipedreamCode from "@/components/PipedreamCode"; + +# GitHub Sync + +When GitHub Syncing is enabled on your project, Pipedream will serialize your workflows and synchronize changes to a GitHub repo. + +Capabilities include: + +- Bi-directional GitHub sync (push and pull changes) +- Edit in development branches +- Track commit and merge history +- Link users to commits +- Merge from Pipedream or create PRs and merge from GitHub +- Edit in Pipedream or use a local editor and synchronize via GitHub (e.g., edit code, find and replace across multiple steps or workflows) +- Organize workflows into projects with support for nested folders + +## Getting Started + +### Create a new project and enable GitHub Sync + +A project may contain one or more workflows and may be further organized using nested folders. Each project may be synchronized to a single GitHub repo. + +- Go to `https://pipedream.com/projects` +- Create a new project +- Enter a project name and check the box to **Configure GitHub Sync** + - To use **OAuth** + - Select a connected account, GitHub scope and repo name + - Pipedream will automatically create a new, empty repo in GitHub + - ![Enabling GitHub on a Pipedream project](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F6b546c6b-2f90-4ec4-9188-320c01576259%2FUntitled.png?id=5db64a5f-e762-431e-bfb7-cdd495ad458c&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=860&userId=&cache=v2) + - To use **Deploy Keys** + - Create a new repo in GitHub + - Follow the instructions to configure the deploy key + - Test your setup and create a new project + - ![Enabling GitHub sync with a Deploy Key](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F6be64329-bb8b-43eb-a278-a11ad93113c0%2FUntitled.png?id=37f9afd8-ba14-431b-bd40-1846421440b6&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=860&userId=&cache=v2) + +### Create a branch to edit a project + + +Branches are required to make changes + +All changes to resources in a project must be made in a development branch. + +Examples of changes include creating, editing, deleting, enabling, disabling and renaming workflows. This also includes changing workflow settings like concurrency, VPC assignment and auto-retries. + + +To edit a git-backed project you must create a development branch by clicking **Edit > Create Branch** + +![Creating a new git backed development branch in a workflow](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fcf1e386b-7674-4843-8709-f1d5eef8ef00%2FUntitled.png?id=3af32b86-6ca2-4051-98cc-de31940eb609&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) + +Next, name the branch and click **Create**: + +![Enter your desired branch name, and click create to create the branch](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F5d404d86-3f35-4db8-a2e8-e8d7bdb3e2c0%2FUntitled.png?id=33ebf62f-cde3-43fb-a76b-5c869338226f&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) + +To exit development mode without merging to production, click **Exit Development Mode**: + +![Exiting the branch without committing the changes](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F7e72331c-54b8-453a-ae47-36f4b2355fac%2FUntitled.png?id=a55ea908-d904-4218-bfd3-72e568fee6ea&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) + +Your changes will be saved to the branch, if you choose to revisit them later. + +### Merge changes to production + +Once you've committed your changes, you can deploy your changes by merging them into the `production` branch through the Pipedream UI or GitHub. + +When you merge a Git-backed project to production, all modified resources in the project will be deployed. Multiple workflows may be deployed, modified, or deleted in production through a single merge action. + +#### Merge via the Pipedream UI + +To merge changes to production, click on **Merge to production:** + +![Click on the Merge to production button in the top right of the UI to merge the changes](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F7ffe8211-90de-4512-824a-f3cc1b5a9382%2FUntitled.png?id=0043c13f-ade3-4e0e-b9ae-a98b9f293885&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) + +Pipedream will present a diff between the development branch and the `production`. Validate your changes and click **Merge to production** to complete the merge: + +![In the confirmation modal, click Merge to production to confirm the changes](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fac27c067-f948-424c-9d6e-6360a759730c%2FUntitled.png?id=19f8d51c-82fb-47d6-a183-b84e2173b72d&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) + +#### Create a Pull Request in GitHub + +To create a pull request in GitHub, either choose Open GitHub pull request from the git-actions menu in Pipedream or in GitHub: + +Opening a PR request in Pipedream + +You can also review and merge changes directly from GitHub using the standard pull request process. + + +Pull request reviews cannot be required + +PR reviews cannot be required. That feature is on the roadmap for the Business tier. + + +### Commit changes + +To commit changes without merging to production, select **Commit Changes** from the Git Actions menu: + +![Select commit changes from the dropdown menu to make a commit](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F2523c3fb-d832-4e99-b5cc-5c3b7275c9fe%2FUntitled.png?id=9f871c2d-12f4-4484-a76f-b0d83c4d8ee9&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) + +You can review the diff and enter a commit message: + +![Preview your changes and approve them with a commit message](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F01f0b4f8-36d6-43a8-8568-99183a7a1d4c%2FUntitled.png?id=77f5b85a-c14d-47aa-8cbe-2e3d5ea64786&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) + +### Pull changes and resolve conflicts + +If remote changes are detected, you'll be prompted to pull the changes: + +![Click the Pull `` to pull in the latest changes to your current branch](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fac13c163-7816-4e67-bb4a-ccbfb97471c4%2FUntitled.png?id=434ad559-bf3c-4e9c-96a3-c784122793a9&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) + +Pipedream will attempt to automatically merge changes. If there are conflicts, you will be prompted to manually resolve it: + +![Example of a commit that requires manual resolution to continue with the commit](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F7849edd0-ee5f-47e9-9a23-b245435faf2b%2FUntitled.png?id=35308854-6572-4575-8fef-6531986fdb7f&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) + +### Move existing workflows to projects + + +Not available for v1 workflows + +Legacy (v1) workflows are not supported in projects. + + +First, select the workflow(s) you want to move from the [workflows listing page](https://pipedream.com/workflows) and click **Move** in the top action menu: + +![Select your workflows you'd like to transfer to a project, then click the Move button in the top right](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F70c4da87-2aa3-435d-9226-c29fcc1cd881%2FUntitled.png?id=10fbba0c-2a92-49da-b7f1-22b1b46fb96c&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) + +Then, select the project to move the selected workflows to: + +![Select which project to move the selected workflows into in the dropdown in the top right of the screen](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F5fcb3357-9957-4307-aac9-e28ed59f85b0%2FUntitled.png?id=7fca27aa-28ec-4bcc-940d-9b66d1d692be&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) + + +Undeployed changes are automatically assigned a development branch + +If any moved workflows have undeployed changes, those changes will staged in a branch prefixed with `undeployed-changes` (e.g., `undeployed-changes-27361`). + + +### Use the changelog + +The changelog tracks all git activity (for projects with GitHub sync enabled). If you encounter an error merging your project, go to the changelog and explore the log details to help you troubleshoot issues in your workflows: + +![Opening the changelog of commits on the left hand menu](https://pipedream.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fe7043a8c-597c-4722-86f6-464f582faf6f%2FUntitled.png?id=6e2f00f1-c768-4d13-9800-d6361afbe26d&table=block&spaceId=6e16aa4c-a31f-4db8-a947-0d80bcdcf984&width=2000&userId=&cache=v2) + +### Local development + +Projects that use GitHub sync may be edited outside of Pipedream. You can edit and commit directly via GitHub’s UI or clone the repo locally and use your preferred editor (e.g., VSCode). + +To test external edits in Pipedream: + +1. Commit and push local changes to your development branch in GitHub +2. Open the project in Pipedream's UI and load your development branch +3. Use the Git Actions menu to pull changes from GitHub + +## Known Issues + +Below are a list of known issues that do not currently have solutions, but are in progress: + +- Project branches on Pipedream cannot be deleted. +- If a workflow uses an action that has been deprecated, merging to production will fail. +- Legacy (v1) workflows are not supported in projects. +- Self-hosted GitHub Server instances are not yet supported. [Please contact us for help](https://pipedream.com/support). +- Workflow attachments are not supported + +## GitHub Enterprise Cloud + +If your repository is hosted on an GitHub Enterprise account, you can allow Pipedream's address range to sync your project changes. + +[Follow the directions here](https://docs.github.com/en/enterprise-cloud@latest/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/managing-allowed-ip-addresses-for-your-organization) and add the following IP range: + +
+  {process.env.PD_EGRESS_IP_RANGE}
+
+ + +GitHub Sync is available on Business plan + +To use this public IP address and connect to GitHub Enterprise Cloud hosted repositories, you'll need to have a Pipedream Business plan. [View our plans](https://pipedream.com/pricing). + + +## FAQ + +### How are Pipedream workflows synchronized to GitHub? + +Pipedream will serialize your project's workflows and their configuration into a standard YAML format for storage in GitHub. + +Then Pipedream will commit your changes to your connected GitHub account. + +### Do you have a definition of this YAML? + +Not yet, please stay tuned! + +### Can I sync multiple workflows to a single GitHub Repository? + +Yes, _projects_ are synced to a single GitHub Repository which allows you to store multiple workflows into a single GitHub Repository for easier organization and management. + +### Can I use this feature to develop workflows locally? + +Yes, you can use the GitHub Syncing feature to develop your workflows from YAML files checked into your Pipedream connected GitHub Repository. + +Then pushing changes to the `production` branch will trigger a deploy for your Pipedream workflows. + +### Why am I seeing the error "could not resolve step[index].uses: component-key@version" when merging to production? +This error occurs when a workflow references a [private component](/workflows/contributing/components/#using-private-actions) without properly prefixing the component key with your workspace name in the `workflow.yaml` configuration file. Pipedream requires this prefix to correctly identify and resolve components specific to your workspace. + +For example, if you modified a [registry action](/workflows/contributing/) and published it privately, the correct component key should be formatted as `@workspacename/component-key@version` (e.g., `@pipedream/github-update-issue@0.1.0`). + +To resolve this error: + +1. Clone your repository locally and create a development branch. +2. Locate the error in your `workflow.yaml` file where the component key is specified. +3. Add your workspace name prefix to the component key, ensuring it follows the format `@workspacename/component-key@version`. +4. Commit your changes and push them to your repository. +5. Open your project in the Pipedream UI and select your development branch. +6. Click on **Merge to Production** and verify the deployment success in the [Changelog](/workflows/git/#use-the-changelog). +7. If the issue persists, [reach out to Pipedream Support](https://pipedream.com/support) for further assistance. + +### Why am I seeing an error about "private auth mismatch" when trying to merge a branch to production? +![Private Auth Mismatch](https://res.cloudinary.com/pipedreamin/image/upload/v1704258143/private_auth_mismatch_kzdd7e.png) + +This error occurs when **both** of the below conditions are met: +1. The referenced workflow is using a connected account that's not shared with the entire workspace +2. The change was merged from outside the Pipedream UI (via github.com or locally) + +Since Pipedream can't verify the person who merged that change should have access to use the connected account in a workflow in this case, we block these deploys. + +To resolve this error: +1. Make sure all the connected accounts in the project's workflows are [accessible to the entire workspace](/integrations/connected-accounts/#access-control) +2. Re-trigger a sync with Pipedream by making a nominal change to the workflow **from outside the Pipedream UI** (via github.com or locally), then merge that change to production + +### Can I sync an existing GitHub Repository with workflows to a new Pipedream Project? + +No, at this time it's not possible because of how resources are connected during the bootstrapping process from the workflow YAML specification. +However, this is on our roadmap, [please subscribe to this issue](https://github.com/PipedreamHQ/pipedream/issues/9255) for the latest details. + +### Migrating Github Repositories + +You can migrate Pipedream project's Github repository to a new repository, while preserving history. You may want to do this when migrating a repository from a personal Github account to an organization account, without affecting the workflows within the Pipedream project. + +#### Assumptions +- **Current GitHub Repository**: `previous_github_repo` +- **New GitHub Repository**: `new_github_repo` +- Basic familiarity with git and GitHub +- Access to a local terminal (e.g., Bash, Zsh, PowerShell) +- Necessary permissions to modify both the Pipedream project and associated GitHub repositories + +#### Steps + +1. **Access Project Settings in Pipedream:** + - Navigate to your Pipedream project. + - Use the dropdown menu on the "Edit" button in the top right corner to access `previous_github_repo` in GitHub. + + ![Accessing GitHub repository](https://res.cloudinary.com/pipedreamin/image/upload/v1714749205/Screenshot_2024-05-03_at_12.12.36_PM_sxkrng.png) + +2. **Clone the Current Repository Locally:** + ``` + git clone previous_github_repo_clone_url + ``` +3. Reset GitHub Sync in Pipedream: + - In Pipedream, go to your project settings. + - Click on "Reset GitHub Connection". + + ![Resetting GitHub connection](https://res.cloudinary.com/pipedreamin/image/upload/v1714749323/Screenshot_2024-05-03_at_12.15.03_PM_icdppb.png) + +4. Set Up New repository connection: + - Configure the project's GitHub repository to use `new_github_repo`. +5. Clone the new repository locally: + ``` + git clone new_github_repo_clone_url + cd new_github_repo + ``` +6. Link to the old repository: + ``` + git remote add old_github_repo previous_github_repo_clone_url + git fetch --all + ``` +7. Prepare for migration: + - Create and switch to a new branch for migration: + ``` + git checkout -b migration + ``` + - Merge the main branch of `old_github_repo` into migration, allowing for unrelated histories: + ``` + git merge --allow-unrelated-histories old_github_repo/production + # Resolve any conflicts, such as in README.md + git commit + ``` +8. Finalize the migration: + - Optionally push the `migration` branch to the remote: + ``` + git push --set-upstream origin migration + ``` + - Switch to the `production` branch and merge: + ``` + git checkout production + git merge --no-ff migration + git push + ``` +9. Cleanup: + - Remove the connection to the old repository: + ``` + git remote remove old_github_repo + ``` + - Optionally, you may now safely delete `previous_github_repo` from GitHub. + +### How does the `production` branch work? + +Anything merged to the `production` branch will be deployed to your production workflows on Pipedream. + +From a design perspective, we want to let you manage any branching strategy on your end, since you may be making commits to the repo outside of Pipedream. Once we support managing Pipedream workflows in a monorepo, where you may have other changes, we wanted to use a branch that didn't conflict with a conventional main branch (like `main` or `master`). + +In the future, we also plan to support you changing the default branch name. diff --git a/docs-v2/pages/workflows/index.mdx b/docs-v2/pages/workflows/index.mdx index a981e9ed9c2ee..27315ae2064e9 100644 --- a/docs-v2/pages/workflows/index.mdx +++ b/docs-v2/pages/workflows/index.mdx @@ -1,15 +1,101 @@ import VideoPlayer from '@/components/VideoPlayer'; +import Callout from '@/components/Callout' # What are workflows? -Workflows make it easy to integrate your apps, data, and APIs - all with no servers or infrastructure to manage. They're sequences of linear [steps](https://pipedream.com/docs/workflows/steps) [triggered by an event](/workflows/steps/triggers), like an HTTP request, or new rows in a Google sheet. +Workflows make it easy to integrate your apps, data, and APIs - all with no servers or infrastructure to manage. They're sequences of [steps](/workflows/#steps) [triggered by an event](/workflows/building-workflows/triggers/), like an HTTP request, or new rows in a Google sheet. -You can use [pre-built actions](/workflows/steps/actions/) or custom [Node.js](https://pipedream.com/docs/code/nodejs/), [Python](https://pipedream.com/docs/code/python/), [Golang](https://pipedream.com/docs/code/go/), or [Bash](https://pipedream.com/docs/code/bash/) code in workflows and connect to any of our 1,600+ integrated apps. +You can use [pre-built actions](/workflows/building-workflows/actions/) or custom [Node.js](/workflows/building-workflows/code/nodejs/), [Python](/workflows/building-workflows/code/python/), [Golang](/workflows/building-workflows/code/go/), or [Bash](/workflows/building-workflows/code/bash/) code in workflows and connect to any of our {process.env.PUBLIC_APPS} integrated apps. Read [our quickstart](/quickstart/) or watch our videos on [Pipedream University](https://pipedream.com/university) to learn more. -## How do I create a new workflow? +## Steps -To create a new workflow, first [create a project](/projects/). Then click **New**, title your workflow, and start adding steps. +Steps are the building blocks you use to create workflows. + +- Use [triggers](/workflows/building-workflows/triggers/), [code](/workflows/building-workflows/code/), and [pre-built actions](/workflows/contributing/components/#actions) +- Steps are run linearly, in the order they appear in your workflow +- You can pass data between steps using [the `steps` object](#step-exports) +- Observe the logs, errors, timing, and other execution details for every step + +### Triggers + +Every workflow begins with a [trigger](/workflows/building-workflows/triggers/) step. Trigger steps initiate the execution of a workflow; i.e., workflows execute on each trigger event. For example, you can create an [HTTP trigger](/workflows/building-workflows/triggers/#http) to accept HTTP requests. We give you a unique URL where you can send HTTP requests, and your workflow is executed on each request. + +You can add [multiple triggers](/workflows/building-workflows/triggers/#can-i-add-multiple-triggers-to-a-workflow) to a workflow, allowing you to run it on distinct events. + +### Code, Actions + +[Actions](/workflows/contributing/components/#actions) and [code](/workflows/building-workflows/code/) steps drive the logic of your workflow. Anytime your workflow runs, Pipedream executes each step of your workflow in order. Actions are prebuilt code steps that let you connect to hundreds of APIs without writing code. When you need more control than the default actions provide, code steps let you write any custom Node.js code. + +Code and action steps cannot precede triggers, since they'll have no data to operate on. + +Once you save a workflow, we deploy it to our servers. Each event triggers the workflow code, whether you have the workflow open in your browser, or not. + +## Step Names + +Steps have names, which appear at the top of the step: + +![The name of the step is on the top of the step](/images/workflows/steps/step-name.png) + +When you [share data between steps](#step-exports), you'll use this name to reference that shared data. For example, `steps.trigger.event` contains the event that triggered your workflow. If you exported a property called `myData` from this code step, you'd reference that in other steps using `steps.code.myData`. See the docs on [step exports](#step-exports) to learn more. + +You can rename a step by clicking on its name and typing a new one in its place: + +![Renaming a code step to "get_data"](https://res.cloudinary.com/pipedreamin/image/upload/v1647959120/docs/components/CleanShot_2022-03-22_at_10.24.32_zfxrwd.gif) + +After changing a step name, you'll need to update any references to the old step. In this example, you'd now reference this step as `steps.get_data`. + + +Step names cannot contain spaces or dashes. Please use underscores or camel casing for your step names, like `getData` or `get_data`. + + +## Passing data to steps from the workflow builder + +You can generate form based inputs for steps using `props`. This allows the step reuse in across many workflows with different provided arguments - all without changing code. + +Learn more about using `props` in our [Node.js code step documentation.](/workflows/building-workflows/code/nodejs/#passing-props-to-code-steps) + + +Passing props from the workflow builder to workflow steps are only available in Node.js code steps. + +We do not currently offer this feature for Python, Bash or Go powered code steps. + + +## Step Exports + +Step exports allow you to pass data between steps. Any data exported from a step must be JSON serializable; the data must be able to stored as JSON so it can be read by downstream steps. + +For examples of supported data types in your steps language, see the examples below. + +* [Node.js (Javascript)](/workflows/building-workflows/code/nodejs/#sharing-data-between-steps) +* [Python](/workflows/building-workflows/code/python/#sharing-data-between-steps) +* [Bash](/workflows/building-workflows/code/bash/#sharing-data-between-steps) +* [Go](/workflows/building-workflows/code/go/#sharing-data-between-steps) + +## Step Notes + +Pipedream lets you add notes to individual steps in your workflow so you can include helpful context to other workspace members or even yourself, and you can even write markdown! + +![Viewing step notes](/images/workflows/steps/step-notes.png) + +### Adding or editing a note +1. Enter build mode on any workflow +2. Click into the overflow menu (3 dots) at the top right of any step +3. Select **Add note** (or **Edit note** if making changes to an existing note) +4. Add any text or markdown, then click **Update** + +![Add note](https://res.cloudinary.com/pipedreamin/image/upload/v1698167274/add_note_kvvxju.png) + +![Adding step notes](/images/workflows/steps/adding-step-note.gif) + +### Showing notes +Any step that has a note will have a **Note** section in the top panel in the editor pane. + +![Viewing step notes](/images/workflows/steps/step-notes.png) + +### Current limitations + +- Step notes are only accessible in Build mode, not in the Inspector. diff --git a/docs-v2/pages/workflows/inspect.mdx b/docs-v2/pages/workflows/inspect.mdx deleted file mode 100644 index 1b14721abf2a9..0000000000000 --- a/docs-v2/pages/workflows/inspect.mdx +++ /dev/null @@ -1,39 +0,0 @@ -import VideoPlayer from "@/components/VideoPlayer"; - -# Inspect Events - - - -[The inspector](#the-inspector) lists the events you send to a [workflow](/workflows/). Once you choose a [trigger](/workflows/steps/triggers/) and send events to it, you'll see those events in the inspector, to the left of your workflow. - -Clicking on an event from the list lets you [review the incoming event data and workflow execution logs](/workflows/events/#examining-event-data) for that event. - -You can use the inspector to replay events, delete them, and more. - -## The inspector - -The inspector lists your workflow's events: - -![Event inspector in a deployed workflow](https://res.cloudinary.com/pipedreamin/image/upload/v1648759565/docs/components/CleanShot_2022-03-31_at_16.45.52_vwwhaj.png) - -## Event Duration - -The duration shown when clicking an individual event notes the time it took to run your code, in addition to the time it took Pipedream to handle the execution of that code and manage its output. Specifically, - -**Duration = Time Your Code Ran + Pipedream Execution Time** - -## Replaying and deleting events - -Hover over an event, and you'll see two buttons: - -![Replaying and deleting events](https://res.cloudinary.com/pipedreamin/image/upload/v1648759778/docs/components/CleanShot_2022-03-31_at_16.49.24_ska5vo.gif) - -The blue button with the arrow **replays** the event against the newest version of your workflow. The red button with the X **deletes** the event. - -## Messages - -Any `console.log()` statements or other output of code steps is attached to the associated code cells. But [`$.flow.exit()`](/code/nodejs/#ending-a-workflow-early) or [errors](/code/nodejs/#errors) end a workflow's execution, so their details appear in the inspector. - -## Limits - -Pipedream retains a limited history of events for a given workflow. See the [limits docs](/limits/#event-execution-history) for more information. \ No newline at end of file diff --git a/docs-v2/pages/limits.mdx b/docs-v2/pages/workflows/limits.mdx similarity index 81% rename from docs-v2/pages/limits.mdx rename to docs-v2/pages/workflows/limits.mdx index 4e0c782e12433..4bc6bebde4be6 100644 --- a/docs-v2/pages/limits.mdx +++ b/docs-v2/pages/workflows/limits.mdx @@ -18,14 +18,10 @@ The limit of active workflows depends on your current plan. [See our pricing pag ## Daily Credits Limit -Free Pipedream accounts are limited to {process.env.DAILY_CREDITS_LIMIT} [credits](/pricing/#credits) per day. Paid plans do not have a daily credit limit. +Free Pipedream accounts are limited on daily credits. Paid plans do not have a credit limit. You can view your credits usage at the bottom-left of [the Pipedream UI](https://pipedream.com). -
- -
- You can also see more detailed usage in [Billing and Usage Settings](https://pipedream.com/settings/billing). Here you'll find your usage for the last 30 days, broken out by day, by resource (e.g. your source / workflow). Your included credits count is reset daily at 00:00 (midnight) UTC. @@ -52,12 +48,15 @@ Depending on your plan, Pipedream sets limits on: You'll find your workspace's limits in the **Data Stores** section of usage dashboard in the bottom-left of [the Pipedream UI](https://pipedream.com).
- +
## HTTP Triggers -The following limits apply to [HTTP triggers](/workflows/steps/triggers/#http). +The following limits apply to [HTTP triggers](/workflows/building-workflows/triggers/#http). ### HTTP Request Body Size @@ -67,8 +66,8 @@ Your endpoint will issue a `413 Payload Too Large` status code when the body of **Pipedream supports two different ways to bypass this limit**. Both of these interfaces support uploading data up to `5TB`, though you may encounter other platform limits. -- You can send large HTTP payloads by passing the `pipedream_upload_body=1` query string or an `x-pd-upload-body: 1` HTTP header in your HTTP request. [Read more here](/workflows/steps/triggers/#sending-large-payloads). -- You can upload multiple large files, like images and videos, using the [large file upload interface](/workflows/steps/triggers/#large-file-support). +- You can send large HTTP payloads by passing the `pipedream_upload_body=1` query string or an `x-pd-upload-body: 1` HTTP header in your HTTP request. [Read more here](/workflows/building-workflows/triggers/#sending-large-payloads). +- You can upload multiple large files, like images and videos, using the [large file upload interface](/workflows/building-workflows/triggers/#large-file-support). ### QPS (Queries Per Second) @@ -82,17 +81,17 @@ We'll also accept short bursts of traffic, as long as you remain close to an ave ## Email Triggers -Currently, most of the [limits that apply to HTTP triggers](#http-triggers) also apply to [email triggers](/workflows/steps/triggers/#email). +Currently, most of the [limits that apply to HTTP triggers](#http-triggers) also apply to [email triggers](/workflows/building-workflows/triggers/#email). The only limit that differs between email and HTTP triggers is the payload size: the total size of an email sent to a workflow - its body, headers, and attachments - is limited to {process.env.EMAIL_PAYLOAD_SIZE_LIMIT}. ## Memory -By default, workflows run with {process.env.MEMORY_LIMIT} of memory. You can modify a workflow's memory [in your workflow's Settings](/workflows/settings/#memory), up to {process.env.MEMORY_ABSOLUTE_LIMIT}. +By default, workflows run with {process.env.MEMORY_LIMIT} of memory. You can modify a workflow's memory [in your workflow's Settings](/workflows/building-workflows/settings/#memory), up to {process.env.MEMORY_ABSOLUTE_LIMIT}. Increasing your workflow's memory gives you a proportional increase in CPU. If your workflow is limited by memory or compute, increasing your workflow's memory can reduce its overall runtime and make it more performant. -**Pipedream charges credits proportional to your memory configuration**. [Read more here](/pricing/#how-does-workflow-memory-affect-credits). +**Pipedream charges credits proportional to your memory configuration**. [Read more here](/pricing/faq/#how-does-workflow-memory-affect-credits). ## Disk @@ -118,11 +117,11 @@ You can increase the timeout limit, up to a max value set by your plan: | Free tiers | 300 seconds (5 min) | | Paid tiers | 750 seconds (12.5 min) | -Events that trigger a **Timeout** error will appear in red in the [Inspector](/workflows/events/inspect/). You'll see the timeout error, also in red, in the cell at which the code timed out. +Events that trigger a **Timeout** error will appear in red in the [Inspector](/workflows/building-workflows/inspect/). You'll see the timeout error, also in red, in the cell at which the code timed out. -### Event / Execution History +### Event History -The [Inspector](/workflows/events/inspect/#the-inspector) shows the execution history for a given workflow. Events have a limited retention period, depending on your plan: +The [Inspector](/workflows/building-workflows/inspect/#the-inspector) shows the execution history for a given workflow. Events have a limited retention period, depending on your plan: | Tier | Events retained per workflow | | :--------: | :------------------------------------------------------------------------------: | @@ -133,11 +132,10 @@ The execution details for a specific event also expires after {process.env.INSPE ### Logs, Step Exports, and other observability -The total size of `console.log()` statements, [step exports](/workflows/steps/#step-exports), and the original event data sent to the workflow cannot exceed a combined size of {process.env.FUNCTION_PAYLOAD_LIMIT}. If you produce logs or step exports larger than this - for example, passing around large API responses, CSVs, or other data - you may encounter a **Function Payload Limit Exceeded** in your workflow. +The total size of `console.log()` statements, [step exports](/workflows/#step-exports), and the original event data sent to the workflow cannot exceed a combined size of {process.env.FUNCTION_PAYLOAD_LIMIT}. If you produce logs or step exports larger than this - for example, passing around large API responses, CSVs, or other data - you may encounter a **Function Payload Limit Exceeded** in your workflow. This limit cannot be raised. ## Acceptable Use We ask that you abide by our [Acceptable Use](https://pipedream.com/terms/#b-acceptable-use) policy. In short this means: don't use Pipedream to break the law; don't abuse the platform; and don't use the platform to harm others. - diff --git a/docs-v2/pages/workflows/managing/images/add-new-env-var.png b/docs-v2/pages/workflows/managing/images/add-new-env-var.png deleted file mode 100644 index 7cdfea9eae72a..0000000000000 Binary files a/docs-v2/pages/workflows/managing/images/add-new-env-var.png and /dev/null differ diff --git a/docs-v2/pages/workflows/managing/images/add-remove-env-var.png b/docs-v2/pages/workflows/managing/images/add-remove-env-var.png deleted file mode 100644 index 5d6ce43249499..0000000000000 Binary files a/docs-v2/pages/workflows/managing/images/add-remove-env-var.png and /dev/null differ diff --git a/docs-v2/pages/workflows/managing/images/attachment-file-data.png b/docs-v2/pages/workflows/managing/images/attachment-file-data.png deleted file mode 100644 index cfea2a7c323d0..0000000000000 Binary files a/docs-v2/pages/workflows/managing/images/attachment-file-data.png and /dev/null differ diff --git a/docs-v2/pages/workflows/managing/images/edit-environment.png b/docs-v2/pages/workflows/managing/images/edit-environment.png deleted file mode 100644 index 860998686487b..0000000000000 Binary files a/docs-v2/pages/workflows/managing/images/edit-environment.png and /dev/null differ diff --git a/docs-v2/pages/workflows/managing/images/env-var-error.png b/docs-v2/pages/workflows/managing/images/env-var-error.png deleted file mode 100644 index 401992d6b1ff3..0000000000000 Binary files a/docs-v2/pages/workflows/managing/images/env-var-error.png and /dev/null differ diff --git a/docs-v2/pages/workflows/managing/images/env-vars-object-explorer.png b/docs-v2/pages/workflows/managing/images/env-vars-object-explorer.png deleted file mode 100644 index 2e7fef119a0cf..0000000000000 Binary files a/docs-v2/pages/workflows/managing/images/env-vars-object-explorer.png and /dev/null differ diff --git a/docs-v2/pages/workflows/managing/images/env-vars.gif b/docs-v2/pages/workflows/managing/images/env-vars.gif deleted file mode 100644 index 24b4efd38596b..0000000000000 Binary files a/docs-v2/pages/workflows/managing/images/env-vars.gif and /dev/null differ diff --git a/docs-v2/pages/workflows/managing/images/params-hamburger-menu.png b/docs-v2/pages/workflows/managing/images/params-hamburger-menu.png deleted file mode 100644 index 6f070d61b64ed..0000000000000 Binary files a/docs-v2/pages/workflows/managing/images/params-hamburger-menu.png and /dev/null differ diff --git a/docs-v2/pages/workflows/managing/images/shared-with-me.png b/docs-v2/pages/workflows/managing/images/shared-with-me.png deleted file mode 100644 index 0c2d00ca39d2a..0000000000000 Binary files a/docs-v2/pages/workflows/managing/images/shared-with-me.png and /dev/null differ diff --git a/docs-v2/pages/workflows/managing/images/workflow-settings.png b/docs-v2/pages/workflows/managing/images/workflow-settings.png deleted file mode 100644 index 859dad03dfa43..0000000000000 Binary files a/docs-v2/pages/workflows/managing/images/workflow-settings.png and /dev/null differ diff --git a/docs-v2/pages/workflows/projects/_meta.tsx b/docs-v2/pages/workflows/projects/_meta.tsx new file mode 100644 index 0000000000000..090dea761c1aa --- /dev/null +++ b/docs-v2/pages/workflows/projects/_meta.tsx @@ -0,0 +1,5 @@ +export default { + "index": "Overview", + "access-controls": "Access Controls", + "secrets": "Variables and Secrets", +} as const diff --git a/docs-v2/pages/workflows/projects/access-controls.mdx b/docs-v2/pages/workflows/projects/access-controls.mdx new file mode 100644 index 0000000000000..29483d2dc5e28 --- /dev/null +++ b/docs-v2/pages/workflows/projects/access-controls.mdx @@ -0,0 +1,73 @@ +import Callout from "@/components/Callout"; + +# Access Controls + +The [projects list view](https://pipedream.com/projects) contains **Owner** and **Access** columns. + +**Owner** indicates who within the workspace owns each project. This is typically the person who created the project. + +![Project Listing (Owner)](/images/projects/project-listing-owner.png) + + + Projects created before February 2024 don't automatically have owners, which + has no functional impact. + + +**Access** indicates which workspace members have access to each project, and this can be displayed as "me", "Workspace", or "N members". + +![Project Listing (Access)](/images/projects/project-listing-access.png) + +## Permissions + +Workspace owners and admins are able to perform all actions in projects, whereas workspace members are restricted from performing certain actions in projects. + +| Operation | Project creator | Workspace members | +| ------------------------------------------------------------ | :-------------: | :---------------: | +| View in [projects listing](https://pipedream.com/projects) | ✅ | ✅ | +| View in [Event History](https://pipedream.com/event-history) | ✅ | ✅ | +| View in global search | ✅ | ✅ | +| Manage project workflows | ✅ | ✅ | +| Manage project files | ✅ | ✅ | +| Manage project variables | ✅ | ✅ | +| Manage member access | ✅ | ❌ | +| Manage GitHub Sync settings | ✅ | ❌ | +| Delete project | ✅ | ❌ | + + + **Workspace admins and owners have the same permissions as project creators + for all projects in the workspace.** + + +## Managing access + + + By default, all projects are accessible to all workspace members. Workspaces + on the [Business plan](https://pipedream.com/pricing) can restrict access for + individual projects to specific workspace members. + + +You can easily modify the access rules for a project directly from the [project list view](https://pipedream.com/projects), either by clicking the access badge in the project row (fig 1) or clicking the 3 dots to open the action menu, then selecting **Manage Access** (fig 2). + +Via the access badge (fig 1): + +![Click the access badge to manage access](/images/projects/access-badge-click.png) + +Via the action menu (fig 2): + +![Click manage access from the action menu](/images/projects/manage-access-overflow-menu.png) + +From here, a slideout drawer reveals the access management configuration: + +![Manage access slideout workspace access](/images/projects/slideout-workspace-share.png) + +Toggle the **Restrict access to this project** switch to manage access: + +![Manage access slideout restricted](/images/projects/slideout-restricted.png) + +Select specific members of the workspace to grant access: + +![Manage access slideout showing member dropdown](/images/projects/slideout-member-dropdown.png) + +You can always see who has access and remove access if necessary: + +![Manage access showing members with access](/images/projects/slideout-member-list.png) diff --git a/docs-v2/pages/workflows/projects/index.mdx b/docs-v2/pages/workflows/projects/index.mdx new file mode 100644 index 0000000000000..6198e80d6aca3 --- /dev/null +++ b/docs-v2/pages/workflows/projects/index.mdx @@ -0,0 +1,80 @@ +import Callout from "@/components/Callout"; + +# Projects + +A workspace can contain one or more _projects_. Projects are a way to organize your workflows into specific groupings or categories. + +
+
+ How workspaces are organized +
+ +## Getting started with projects + +### Creating projects + +To create a new project, first [open the Projects section in the dashboard](https://pipedream.com/projects). + +Then click **Create project** to start a new project. + +Enter in your desired name for the project in the prompt, then click **Create**. + +That's it, you now have a dedicated new project created within your workspace. Now you can create workflows within this project, or move workflows into it or create folders for further organization. + +### Creating folders and workflows in projects + +Within a given project, you can create folders for your workflows. + +Open your project, and then click the **New** button for a dropdown to create a workflow in your current project. + + +Helpful hotkeys to speed up your development + +- `C then F` creates a new folder. +- `C then W` creates a new workflow. + + + +Folders can also contain sub-folders, which allows you to create a filing system to organize your workflows. + +### Moving workflows into folders + +To move workflows into folders, simply drag and drop the workflow into the folder. + +You can move workflows or folders up a level by dragging and dropping the workflow to the folder icon at the top of the list. + +### Importing workflows into projects + + + This only applies to Pipedream accounts that created workflows before the + projects feature was released. + + +To import a workflow from the general **Workflows** area of your dashboard into a project: + +1. Open the Workflows area in the dashboard +2. Select one or more workflows you'd like to import into a project +3. Click *Move* in the top right and select a project to move them to + +![Moving a workflow to a project in the Workflows area of the dashboard](/images/projects/import-workflows-into-projects.png) + +### Moving workflows between projects + +To move a workflow from one project to another project, first check the workflow and then click **Move** to open a dropdown of projects. Select the project to move this workflow to, and click **Move** once more to complete the move. + +![How to move workflows from one project to another in the Pipedream dashboard.](https://res.cloudinary.com/pipedreamin/image/upload/v1695662665/docs/docs/workflows/projects/CleanShot_2023-09-25_at_13.23.38_2x_dyrtlv.png) + + +Github Sync limitation + +At this time it's not possible to move workflows out of GitHub Synchronized Projects. + + + +## Finding your project's ID + +Visit your project's **Settings** and copy the project ID. diff --git a/docs-v2/pages/workflows/projects/secrets.mdx b/docs-v2/pages/workflows/projects/secrets.mdx new file mode 100644 index 0000000000000..9df7b0b11f2bd --- /dev/null +++ b/docs-v2/pages/workflows/projects/secrets.mdx @@ -0,0 +1,13 @@ +import Callout from "@/components/Callout"; + +# Project variables and secrets + +Environment variables defined at the global workspace level are accessible to all workspace members and workflows within the workspace. To restrict access to sensitive variables or secrets, define them at the project-level and [configure access controls for the project](/workflows/projects/access-controls#managing-access). + +[See here](/workflows/environment-variables/) for info on creating, managing, and using environment variables and secrets. + + + **Project variables override workspace variables**. When the same variable is + defined at both the workspace and project levels (for example, + `process.env.BASE_DOMAIN`), the **project** variable takes precedence. + diff --git a/docs-v2/pages/workflows/settings.mdx b/docs-v2/pages/workflows/settings.mdx deleted file mode 100644 index 1994504dfa87c..0000000000000 --- a/docs-v2/pages/workflows/settings.mdx +++ /dev/null @@ -1,217 +0,0 @@ -import Callout from "@/components/Callout"; - -# Settings - -You can control workflow-specific settings in your workflow's **Settings**: - -1. Visit your workflow -2. Select the _..._ menu at the top-right and click **Settings**: - -
-Click on the ... menu at the top-right and select Settings -
- -## Enable Workflow - -If you'd like to pause your workflow from executing completely, you can disable it or reenable it here. - -## Error Handling - -By default, you'll receive notifications when your workflow throws an unhandled error. See the [error docs](/workflows/errors/) for more detail on these notifications. - -You can disable these notifications for your workflow by disabling the **Notify me on errors** toggle: - -Notify me on errors toggle - -
-Notify me on errors toggle -
- -## Auto-retry Errors - -Customers on the [Advanced Plan](https://pipedream.com/pricing) can automatically retry workflows on errors. If any step in your workflow throws an error, Pipedream will retry the workflow from that failed step, re-rerunning the step up to 8 times over a 10 hour span with an [exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) strategy. - -On error, the step will export a `$summary` property that tells you how many times the step has been retried, and an `$attempt` object with the following properties: - -1. `error` — All the details of the error the step threw — the error, the stack, etc. -2. `cancel_url` — You can call this URL to cancel the retry -3. `rerun_url` — You can call this URL to proceed with the execution immediately -4. `resume_ts` — An ISO 8601 timestamp that tells you the timestamp of the next retry - -
-Step exports for failed auto-retry -
- -If the step execution succeeds during any retry, the execution will proceed to the next step of the workflow. - -If the step fails on all 8 retries and throws a final error, you'll receive [an error notification](/workflows/errors/) through your standard notification channel. - -### Send error notifications on the first error - -By default, if a step fails on all 8 retries, and throws a final error, you'll receive [an error notification](/workflows/errors/) through your standard notification channel. But sometimes you need to investigate errors as soon as they happen. If you're connecting to your database, and receive an error that the DB is down, you may want to investigate that immediately. - -On any workflow with auto-retry enabled, you can optionally choose to **Send notification on first error**. This is disabled by default so you don't get emails for transient errors, but you can enable for critical workflows where you want visibility into all errors. - -For custom control over error handling, you can implement error logic in code steps (e.g. `try` / `catch` statements in Node.js code), or [create your own custom error workflow](/workflows/errors/#process-errors-with-custom-logic-instead-of-email). - -## Data Retention Controls - -By default, Pipedream stores exports, logs, and other data tied to workflow executions. You can view these logs in two places: - -1. [The workflow inspector](/workflows/events/inspect/#the-inspector) -2. [Event History](/event-history) - -But if you're processing sensitive data, you may not want to store those logs. You can **Disable data retention** in your workflow settings to disable **all** logging. Since Pipedream stores no workflow logs with this setting enabled, you'll see no logs in the inspector or event history UI. - -![Disable data retention setting](/images/settings/disable-data-retention-settings.png) - -Refer to our [pricing page](https://pipedream.com/pricing) to understand the latest limits based on your plan. - -### Constraints - -- **Data Retention Controls do not apply to sources**: Even with data retention disabled on your workflow, Pipedream will still log inbound events for the source. -- **No events will be shown in the UI**: When data retention is disabled for your workflow, the Pipedream UI will not show any new events in the inspector or Event History for that workflow. - - -**Avoid surfacing events in the builder** - -Even with data retention disabled on your workflow, the builder will still surface inbound events when in build mode. To avoid surfacing potentially sensitive data here as well, refer to [these docs](/workflows/steps/triggers/#pipedream-specific-request-parameters). - - - -## Execution Controls - -### Execution Timeout Limit - -Workflows have a default [execution limit](/limits/#time-per-execution), which defines the time the workflow can run for a single execution until it's timed out. - -If your workflow times out, and needs to run for longer than the [default limit](/limits/#time-per-execution), you can change that limit here. - -### Memory - -By default, workflows run with {process.env.MEMORY_LIMIT} of memory. If you're processing a lot of data in memory, you might need to raise that limit. Here, you can increase the memory of your workflow up to {process.env.MEMORY_ABSOLUTE_LIMIT}. - -**Pipedream charges credits proportional to your memory configuration**. When you modify your memory settings, Pipedream will show you the number of credits you'll be charged per execution. [Read more here](/pricing/#how-does-workflow-memory-affect-credits). - -### Concurrency and Throttling - -[Manage the concurrency and rate](/workflows/concurrency-and-throttling/) at which events from a source trigger your workflow code. - -## Eliminate cold starts - -A **cold start** refers to the delay between the invocation of workflow and the execution of the workflow code. Cold starts happen when Pipedream spins up a new [execution environment](/privacy-and-security/#execution-environment) to handle incoming requests. - -Specifically, cold starts occur on the first request to your workflow after a period of inactivity (roughly 5 minutes), or if your initial worker is already busy and a new worker needs to be initialized. In these cases, Pipedream creates a new execution environment to process your event. **Initializing this environment takes a few seconds, which delays the execution of this first event**. - -You can reduce cold starts by configuring a number of dedicated **workers**: - -1. Visit your workflow's **Settings** -2. Under **Execution Controls**, select the toggle to **Eliminate cold starts** -3. Configure [the appropriate number of workers](#how-many-workers-should-i-configure) for your use case - -
-Warm workers configuration -
- -When you configure workers for a specific workflow, Pipedream initializes dedicated workers — virtual machines that run Pipedream's [execution environment](/privacy-and-security/#execution-environment). [It can take a few minutes](#how-long-does-it-take-to-spin-up-a-dedicated-worker) for new dedicated workers to deploy. Once deployed, these workers are available at all times to respond to workflow executions, with no cold starts. - - -You may need to configure [multiple dedicated workers](#how-many-workers-should-i-configure) to handle multiple, concurrent requests. - -Pipedream also performs some initialization operations on new workflow runs, so you may still observe a small startup time (typically around 50ms per workflow step) on dedicated workers. - - - -### When should I configure dedicated workers? - -You should configure dedicated workers when you need to process requests as soon as possible, with no latency. - -For example, you may build an HTTP-triggered workflow that returns a synchronous HTTP response to a user, without delay. Or you may be building a Slack bot and need to respond to Slack's webhook within a few seconds. Since these workflows need to respond quickly, they're good cases to use dedicated workers. - -### How many workers should I configure? - -Incoming requests are handled by a single worker, one at a time. If you only receive one request a minute, and the workflow finishes execution in a few seconds, you may only need one worker. - -But you might have a higher-volume app that receives two concurrent requests. In that case, Pipedream spins up **two** workers to handle each request. - -For many user-facing (even internal) applications, the number of requests over time can be modeled with a [Poisson distrubution](https://en.wikipedia.org/wiki/Poisson_distribution). You can use that distribution to estimate the number of workers you need at an average time, or set it higher if you want to ensure a specific percentage of requests hit a dedicated worker. You can also save a record of all workflow runs to your own database, with the timestamp they ran ([see `steps.trigger.context.ts`](/workflows/events/#steps-trigger-context)), and look at your own pattern of requests, to compute the optimal number of workers. - -### Do compute budgets apply to dedicated workers? - -No, compute budgets do not apply to dedicated workers, they only apply to credits incurred by compute from running workflows, sources, etc. - -### How long does it take to spin up a dedicated worker? - -It can take 5-10 minutes for Pipedream to fully configure a new dedicated worker. Before that time, you may still observe cold starts with new incoming requests. - -### Pricing for dedicated workers - -You're charged {process.env.WARM_WORKERS_CREDITS_PER_INTERVAL} credits for every {process.env.WARM_WORKERS_INTERVAL} a dedicated worker is live, per worker, per {process.env.MEMORY_LIMIT} memory. You can view the credits used by dedicated workers [in your billing settings](https://pipedream.com/settings/billing): - -
-Warm workers credit usage in billing -
- -For example, if you run a single dedicated worker for 24 hours, that would cost 720 credits: - -``` -5 credits per 10 min -* 6 10-min periods per hour -* 24 hours -= 720 credits -``` - -{process.env.WARM_WORKERS_INTERVAL} is the _minimum_ interval that Pipedream charges for usage. If you have a dedicated worker live for 1 minute, Pipedream will still charge {process.env.WARM_WORKERS_CREDITS_PER_INTERVAL} credits. - -Additionally, any change to dedicated worker configuration, (including worklow deploys) will result in an extra {process.env.WARM_WORKERS_CREDITS_PER_INTERVAL} credits of usage. - -
-Dedicated worker overlap -
- -## Attachments - -Sometimes, you'll need to reference static files in your workflow, like a CSV. Files uploaded in the **Attachments** section can be referenced in your workflow under the `steps.trigger.context.attachments` object. - -For example, if you upload a file named `test.csv`, Pipedream will expose the _file path_ of the uploaded file at `steps.trigger.context.attachments["test.csv"]`. You can read the contents of the file using `fs.readFileSync`: - -```javascript -import fs from "fs"; - -export default defineComponent({ - async run({ steps, $ }) { - const fileData = fs - .readFileSync(steps.trigger.context.attachments["test.csv"]) - .toString(); - console.log(fileData); - }, -}); -``` - -### Limits - -Each attachment is limited to `25MB` in size. The total size of all attachments within a single workflow cannot exceed `200MB`. diff --git a/docs-v2/pages/workflows/sharing.mdx b/docs-v2/pages/workflows/sharing.mdx deleted file mode 100644 index 5cb4bfe00c1c6..0000000000000 --- a/docs-v2/pages/workflows/sharing.mdx +++ /dev/null @@ -1,111 +0,0 @@ -import { Cards, Card } from 'nextra/components' -import Callout from '@/components/Callout' -import RocketShip from '@/components/RocketShip' - -# Sharing Workflows - -You can share your workflows as templates with other Pipedream accounts with a unique shareable link. - -Creating a share link for your workflow will allow anyone with the link to create a template version of your workflow in their own Pipedream account. This will allow others to use your workflow with their own Pipedream account and also their own connected accounts. - -[Here's an example of a workflow](https://pipedream.com/new?h=tch_OYWfjz) that sends you a daily SMS message with today's schedule: - -![Daily Schedule SMS Reminder workflow](https://res.cloudinary.com/pipedreamin/image/upload/v1685116771/docs/docs/share%20workflows/New_Project_6_n63kju.png) - -Click **Deploy to Pipedream** below to create a copy of this workflow in your own Pipedream account. - - - } title="Deploy to Pipedream" href="https://pipedream.com/new?h=tch_OYWfjz" /> - - -The copied workflow includes the same trigger, steps, and connected account configuration, but it has a separate event history and versioning from the original. - -## Creating a share link for a workflow - -To share a workflow, open the **Builder** for the workflow. Then in the top right menu, select **Create Share Link**. - -![Click "Create Share Link" in the workflow's settings within the builder to generate a sharable link](https://res.cloudinary.com/pipedreamin/image/upload/v1685119418/docs/docs/share%20workflows/CleanShot_2023-05-26_at_12.42.22_p4q3dr.png) - -Now you can define which prop values should be included in this shareable link. - -## Including props - -Optionally, you can include the actual individual prop configurations as well. This helps speed up workflow development if the workflow relies on specific prop values to function properly. - -You can choose to **Include all** prop values if you'd like, or only select specific props. - -For the daily schedule reminder workflow, we included the props for filtering Google Calendar events, but we did _not_ include the SMS number to send the message to. This is because the end user of this workflow will use their own phone number instead: - -![Sharing a workflow that will send a daily SMS message of your Google Calendar schedule for today](https://res.cloudinary.com/pipedreamin/image/upload/v1685113542/docs/docs/share%20workflows/CleanShot_2023-05-26_at_11.05.16_hebqpl.png) - - -Connected Accounts are not shared - -Shared workflow links do not include your own connected accounts. Instead, in this new workflow, the user of your workflow link is prompted to connect their own accounts. - - -## Versioning - -When you create a shared link for your workflow, that link is frozen to the version of your workflow at the time the link was created. - -If changes are made to the original workflow, the changes will _not_ be included in the shared workflow link, nor in any workflows copied from the original shared link. - -[Generate a new share link](#creating-a-share-link-for-a-workflow) to include new changes to a workflow. - - -**Share links persist** - -You can create multiple share links for the same workflow with different prop configurations, or even different steps. - -Share links will not expire or be overridden. - - -## Frequently Asked Questions - -### If changes are made to the original workflow, will copied versions of the workflow from the shared link also change? - -No, workflows copied from a shared link will have separate version histories from the original workflow. You can modify your original workflow and it will not affect copied workflows. - -### Will my connected accounts be shared with the workflow? - -No, your connected accounts are not shared. Instead, copied workflows display a slot in actions that require a connected account, so the user of the copied workflow can provide their own accounts instead. - -For example, if one of your steps relies on a Slack connected account to send a message, then the copied workflow will display the need to connect a Slack account. - -### I haven't made any changes to my workflow, but if I generate another shared link will it override my original link? - -No, if the steps and prop configuration of the workflow is exactly the same, then the shared link URL will also be exactly the same. - -The shared workflow link is determined by the configuration of your workflow, it's not a randomly generated ID. - -### Will generating new shared links disable or delete old links? - -No, each link you generate will be available even if you create new versions based on changes or included props from the original workflow. - -### What plan is this feature available on? - -Sharing workflows via link is available on all plans, including the Free tier plans. - -### Do users of my workflow need to have a subscription? - -To copy a workflow, a subscription is not required. However, the copied workflow is subject to the current workspace's plan limits. - -For example, if a workflow requires more connected accounts than what's available on the [Free tier](/pricing/#free-tier), then users of your workflow will require a plan to run the workflow properly. - -### Will copies of my workflow use my credits? - -No. Copied workflows have entirely separate versioning, connected accounts, and billing. Sharing workflow copies is free, and the user of the copy usage is responsible for credit usage. Your original workflow is entirely separate from the copy. - -### How can I transfer all of my workflows from one account to another? - -It's only possible to share a single workflow at time with a link at this time. - -If you're trying to migrate all resources from one workspace to another [please contact us for help](mailto:support@pipedream.com). - -### Can I share my v1 workflows? - -No, v1 workflows are not shareable. If you need to share a v1 workflow, please see our [migration guide](/migrate-from-v1/) to convert it into a modern v2 workflow. - -### Are step notes included when I share a workflow? - -Yes any [step notes](/workflows/steps/#step-notes) you've added to your workflow are included in the copied version. diff --git a/docs-v2/pages/workflows/steps.mdx b/docs-v2/pages/workflows/steps.mdx deleted file mode 100644 index 92b9bc168cb00..0000000000000 --- a/docs-v2/pages/workflows/steps.mdx +++ /dev/null @@ -1,94 +0,0 @@ -import Callout from '@/components/Callout' - -# Steps - -Steps are the building blocks you use to create workflows. - -- Use [triggers](/workflows/steps/triggers/), [code](/code/), and [pre-built actions](/docs/components#actions) -- Steps are run linearly, in the order they appear in your workflow -- You can pass data between steps using [the `steps` object](#step-exports) -- Observe the logs, errors, timing, and other execution details for every step - -## Types of Steps - -### Triggers - -Every workflow begins with a [trigger](/workflows/steps/triggers/) step. Trigger steps initiate the execution of a workflow; i.e., workflows execute on each trigger event. For example, you can create an [HTTP trigger](/workflows/steps/triggers/#http) to accept HTTP requests. We give you a unique URL where you can send HTTP requests, and your workflow is executed on each requesingle st. - -You can add [multiple triggers](/workflows/steps/triggers/#can-i-add-multiple-triggers-to-a-workflow) to a workflow, allowing you to run it on distinct events. - -### Code, Actions - -[Actions](/components#actions) and [code](/code/) steps drive the logic of your workflow. Anytime your workflow runs, Pipedream executes each step of your workflow in order. Actions are prebuilt code steps that let you connect to hundreds of APIs without writing code. When you need more control than the default actions provide, code steps let you write any custom Node.js code. - -Code and action steps cannot precede triggers, since they'll have no data to operate on. - -Once you save a workflow, we deploy it to our servers. Each event triggers the workflow code, whether you have the workflow open in your browser, or not. - -## Step Names - -Steps have names, which appear at the top of the step: - -![The name of the step is on the top of the step](https://res.cloudinary.com/pipedreamin/image/upload/v1647958883/docs/components/CleanShot_2022-03-22_at_10.20.52_2x_ngo5r5.png) - -When you [share data between steps](#step-exports), you'll use this name to reference that shared data. For example, `steps.trigger.event` contains the event that triggered your workflow. If you exported a property called `myData` from this code step, you'd reference that in other steps using `steps.code.myData`. See the docs on [step exports](#step-exports) to learn more. - -You can rename a step by clicking on its name and typing a new one in its place: - -![Renaming a code step to "get_data"](https://res.cloudinary.com/pipedreamin/image/upload/v1647959120/docs/components/CleanShot_2022-03-22_at_10.24.32_zfxrwd.gif) - -After changing a step name, you'll need to update any references to the old step. In this example, you'd now reference this step as `steps.get_data`. - - -Step names cannot contain spaces or dashes. Please use underscores or camel casing for your step names, like `getData` or `get_data`. - - -## Passing data to steps from the workflow builder - -You can generate form based inputs for steps using `props`. This allows the step reuse in across many workflows with different provided arguments - all without changing code. - -Learn more about using `props` in our [Node.js code step documentation.](/code/nodejs/#passing-props-to-code-steps) - - -Passing props from the workflow builder to workflow steps are only available in Node.js code steps. - -We do not currently offer this feature for Python, Bash or Go powered code steps. - - -## Step Exports - -Step exports allow you to pass data between steps. Any data exported from a step must be JSON serializable; the data must be able to stored as JSON so it can be read by downstream steps. - -For examples of supported data types in your steps language, see the examples below. - -* [Node.js (Javascript)](/code/nodejs/#sharing-data-between-steps) -* [Python](/code/python/#sharing-data-between-steps) -* [Bash](/code/bash/#sharing-data-between-steps) -* [Go](/code/go/#sharing-data-between-steps) - -## Step Notes - -Pipedream lets you add notes to individual steps in your workflow so you can include helpful context to other workspace members or even yourself, and you can even write markdown! - -![Viewing step notes](/images/steps/step-notes-example.png) - -### Adding or editing a note -1. Enter build mode on any workflow -2. Click into the overflow menu (3 dots) at the top right of any step -3. Select **Add note** (or **Edit note** if making changes to an existing note) -4. Add any text or markdown, then click **Update** - -![Add note](https://res.cloudinary.com/pipedreamin/image/upload/v1698167274/add_note_kvvxju.png) - -![Adding step notes](/images/steps/adding-step-note.gif) - -### Showing notes -1. Any step that has a note will indicate this with a special icon (shown below) -2. Click on the icon to hide or show the note - -![View notes](/images/steps/show-hide-note.gif) - -### Current limitations - -- Step notes are only accessible in Build mode, not in the Inspector - diff --git a/docs-v2/pages/workflows/triggers.mdx b/docs-v2/pages/workflows/triggers.mdx deleted file mode 100644 index bcd3146fa1f2d..0000000000000 --- a/docs-v2/pages/workflows/triggers.mdx +++ /dev/null @@ -1,609 +0,0 @@ -import Image from 'next/image' -import Callout from '@/components/Callout' -import PipedreamImg from '@/components/PipedreamImg' -import VideoPlayer from "@/components/VideoPlayer"; - -# Triggers - - - -**Triggers** define the type of event that runs your workflow. For example, HTTP triggers expose a URL where you can send any HTTP requests. Pipedream will run your workflow on each request. The Scheduler trigger runs your workflow on a schedule, like a cron job. - -Today, we support the following triggers: - -- [Triggers for apps like Twitter, GitHub, and more](#app-based-triggers) -- [HTTP / Webhook](#http) -- [Schedule](#schedule) -- [Email](#email) -- [RSS](#rss) - -If there's a specific trigger you'd like supported, please [let us know](https://pipedream.com/support/). - -## App-based Triggers - -You can trigger a workflow on events from apps like Twitter, Google Calendar, and more using [event sources](/sources/). Event sources run as separate resources from your workflow, which allows you to trigger _multiple_ workflows using the same source. Here, we'll refer to event sources and workflow triggers interchangeably. - -When you create a workflow, you'll see your available triggers: - -![App triggers](/images/triggers/app-triggers.png) - -Search by **app name** to find triggers associated with your app. For Google Calendar, for example, you can run your workflow every time a new event is **added** to your calendar, each time an event **starts**, **ends**, and more: - -![Google Calendar sources](/images/triggers/google-calendar-triggers.png) - -Once you select your trigger, you'll be asked to connect any necessary accounts (for example, Google Calendar sources require you authorize Pipedream access to your Google account), and enter the values for any configuration settings. - -Some sources are configured to retrieve an initial set of events when they're created. Others require you to generate events in the app to trigger your workflow. If your source generates an initial set of events, you'll see them appear in the **Select events** dropdown in the **Select Event** step: - -![Choose an event to test your workflow steps against](https://res.cloudinary.com/pipedreamin/image/upload/v1647957381/docs/components/CleanShot_2022-03-22_at_09.55.57_2x_upj35r.png) - -Then you can select a specific test event and manually trigger your workflow with that event data by clicking **Send Test Event**. Now you're ready to build your workflow with the selected test event. - -### What's the difference between an event source and a trigger? - -You'll notice the docs use the terms **event source** and **trigger** interchangeably above. It's useful to clarify the distinction in the context of workflows. - -[Event sources](/sources/) run code that collects events from some app or service and emits events as the source produces them. An event source can be used to **trigger** any number of workflows. - -For example, you might create a single source to listen for new Twitter mentions for a keyword, then trigger multiple workflows each time a new tweet is found: one to [send new tweets to Slack](https://pipedream.com/@pravin/twitter-mentions-slack-p_dDCA5e/edit), another to [save those tweets to an Amazon S3 bucket](https://pipedream.com/@dylan/twitter-to-s3-p_KwCZGA/readme), etc. - -**This model allows you to separate the data produced by a service (the event source) from the logic to process those events in different contexts (the workflow)**. - -Moreover, you can access events emitted by sources using Pipedream's [SSE](/api/sse/) and [REST APIs](/api/rest/). This allows you to access these events in your own app, outside Pipedream's platform. - -### Can I add multiple triggers to a workflow? - -Yes, you can add any number of triggers to a workflow. Click the top right menu in the trigger step and select **Add trigger**. - -![Add multiple triggers to a workflow](/images/triggers/add-multi-trigger.gif) - -### Shape of the `steps.trigger.event` object - -In all workflows, you have access to [event data](/workflows/events/#event-format) using the variable `steps.trigger.event`. - -The shape of the event is specific to the source. For example, RSS sources produce events with a `url` and `title` property representing the data provided by new items from a feed. Google Calendar sources produce events with a meeting title, start date, etc. - -## HTTP - -When you select the **HTTP** trigger: - -![HTTP API Trigger](https://res.cloudinary.com/pipedreamin/image/upload/v1647894504/docs/components/CleanShot_2022-03-21_at_16.27.45_2x_klxmpz.png) - -Pipedream creates a URL endpoint specific to your workflow: - -![HTTP API trigger URL](https://res.cloudinary.com/pipedreamin/image/upload/v1647894654/docs/components/CleanShot_2022-03-21_at_16.30.48_2x_nh7shg.png) - -You can send any HTTP requests to this endpoint, from anywhere on the web. You can configure the endpoint as the destination URL for a webhook or send HTTP traffic from your application - we'll accept any [valid HTTP request](#valid-requests). - - - -Pipedream also supports [custom domains](/workflows/domains). This lets you host endpoints on `https://endpoint.yourdomain.com` instead of the default \`{process.env.ENDPOINT_BASE_URL}\` domain. - - -### Accessing HTTP request data - -You can access properties of the HTTP request, like the method, payload, headers, and more, in [the `event` object](/workflows/events/#event-format), accessible in any [code](/code/) or [action](/components#actions) step. - -### Valid Requests - -You can send a request to your endpoint using any valid HTTP method: `GET`, `POST`, `HEAD`, and more. - -We default to generating HTTPS URLs in the UI, but will accept HTTP requests against the same endpoint URL. - -You can send data to any path on this host, with any query string parameters. You can access the full URL in the `event` object if you'd like to write code that interprets requests with different URLs differently. - -You can send data of any [Media Type](https://www.iana.org/assignments/media-types/media-types.xhtml) in the body of your request. - -The primary limit we impose is on the size of the request body: we'll issue a `413 Payload Too Large` status when the body [exceeds our specified limit](#request-entity-too-large). - -### Custom domains - -To configure endpoints on your own domain, e.g. `endpoint.yourdomain.com` instead of the default `{process.env.ENDPOINT_BASE_URL}` domain, see the [custom domains](/workflows/domains) docs. - -### How Pipedream handles JSON payloads - -When you send JSON in the HTTP payload, or when JSON data is sent in the payload from a webhook provider, **Pipedream converts that JSON to its equivalent JavaScript object**. The trigger data can be referenced using [the `steps` object](#shape-of-the-stepstriggerevent-object). - -In the [Inspector](/workflows/events/inspect/), we present `steps.trigger.event` cleanly, indenting nested properties, to make the payload easy to read. Since `steps.trigger.event` is a JavaScript object, it's easy to reference and manipulate properties of the payload using dot-notation. - -### How Pipedream handles `multipart/form-data` - -When you send [form data](https://ec.haxx.se/http/http-multipart) to Pipedream using a `Content-Type` of `multipart/form-data`, Pipedream parses the payload and converts it to a JavaScript object with a property per form field. For example, if you send a request with two fields: - -```bash -curl -F 'name=Leia' -F 'title=General' https://myendpoint.m.pipedream.net -``` - -Pipedream will convert that to a JavaScript object, `event.body`, with the following shape: - -```json -{ - name: "Leia", - title: "General", -} -``` - -### How Pipedream handles HTTP headers - -HTTP request headers will be available in the `steps.trigger.event.headers` steps export in your downstream steps. - -Pipedream will automatically lowercase header keys for consistency. - -### Pipedream-specific request parameters - -These params can be set as headers or query string parameters on any request to a Pipedream HTTP endpoint. - -#### `x-pd-nostore` - -Set to `1` to prevent logging any data for this execution. Pipedream will execute all steps of the workflow, but no data will be logged to Pipedream. No event will show up in the inspector or the Event History UI. - -If you need to disable logging for _all_ requests, use the workflow's [Data Retention controls](/workflows/settings/#data-retention-controls), instead. - -#### `x-pd-notrigger` - -Set to `1` to send an event to the workflow for testing. Pipedream will **not** trigger the production version of the workflow, but will display the event in the [list of test events](/workflows/events/#selecting-a-test-event) on the HTTP trigger. - -#### Limits - -You can send any content, up to the [HTTP payload size limit](/limits/#http-request-body-size), as a part of the form request. The content of uploaded images or other binary files does not contribute to this limit — the contents of the file will be uploaded at a Pipedream URL you have access to within your source or workflow. See the section on [Large File Support](#large-file-support) for more detail. - -### Sending large payloads - -_If you're uploading files, like images or videos, you should use the [large file upload interface](#large-file-support), instead_. - -By default, the body of HTTP requests sent to a source or workflow is limited to {process.env.PAYLOAD_SIZE_LIMIT}. **But you can send an HTTP payload of any size to a [workflow](/workflows/) or an [event source](/sources/) by including the `pipedream_upload_body=1` query string or an `x-pd-upload-body: 1` HTTP header in your request**. - -```bash -curl -d '{ "name": "Yoda" }' \ - https://endpoint.m.pipedream.net\?pipedream_upload_body\=1 - -curl -d '{ "name": "Yoda" }' \ - -H "x-pd-upload-body: 1" \ - https://endpoint.m.pipedream.net -``` - -In workflows, Pipedream saves the raw payload data in a file whose URL you can reference in the variable `steps.trigger.event.body.raw_body_url`. - -![Raw body URL in the event data under steps.trigger.event.body.raw_body_url](https://res.cloudinary.com/pipedreamin/image/upload/v1647895357/docs/components/CleanShot_2022-03-21_at_16.42.01_2x_w6dmqk.png) - -Within your workflow, you can download the contents of this data using the **Send HTTP Request** action, or [by saving the data as a file to the `/tmp` directory](/code/nodejs/working-with-files/). - -#### Example: Download the HTTP payload using the Send HTTP Request action - -_Note: you can only download payloads at most {process.env.FUNCTION_PAYLOAD_LIMIT} in size using this method. Otherwise, you may encounter a [Function Payload Limit Exceeded](/troubleshooting/#function-payload-limit-exceeded) error._ - -You can download the HTTP payload using the **Send HTTP Request** action. [Copy this workflow to see how this works](https://pipedream.com/@dylburger/example-download-http-payload-p_6lC1ynx/edit). - -This will return the data in the variable `steps.send_http_request.$return_value`: - -![HTTP response data](/images/triggers/send_http_request_action.png) - -#### Example: Download the HTTP payload to the `/tmp` directory - -[This workflow](https://pipedream.com/@dylburger/example-download-raw-body-to-tmp-p_YyCoqPb/edit) downloads the HTTP payload, saving it as a file to the [`/tmp` directory](/code/nodejs/working-with-files/#the-tmp-directory). - -```javascript -import stream from "stream"; -import { promisify } from "util"; -import fs from "fs"; -import got from "got"; - -const pipeline = promisify(stream.pipeline); -await pipeline( - got.stream(steps.trigger.event.body.raw_body_url), - fs.createWriteStream(`/tmp/raw_body`) -); -``` - -You can [read this file](/code/nodejs/working-with-files/#reading-a-file-from-tmp) in subsequent steps of your workflow. - -#### How the payload data is saved - -Your raw payload is saved to a Pipedream-owned [Amazon S3 bucket](https://aws.amazon.com/s3/). Pipedream generates a [signed URL](https://docs.aws.amazon.com/AmazonS3/latest/dev/ShareObjectPreSignedURL.html) that allows you to access to that file for up to 30 minutes. After 30 minutes, the signed URL will be invalidated, and the file will be deleted. - -#### Limits - -**You can upload payloads up to 5TB in size**. However, payloads that large may trigger [other Pipedream limits](/limits/). Please [reach out](https://pipedream.com/support/) with any specific questions or issues. - -### Large File Support - -_This interface is best used for uploading large files, like images or videos. If you're sending JSON or other data directly in the HTTP payload, and encountering a **Request Entity Too Large** error, review the section above for [sending large payloads](#sending-large-payloads)_. - -You can upload any file to a [workflow](/workflows/) or an [event source](/sources/) by making a `multipart/form-data` HTTP request with the file as one of the form parts. **Pipedream saves that file to a Pipedream-owned [Amazon S3 bucket](https://aws.amazon.com/s3/), generating a [signed URL](https://docs.aws.amazon.com/AmazonS3/latest/dev/ShareObjectPreSignedURL.html) that allows you to access to that file for up to 30 minutes**. After 30 minutes, the signed URL will be invalidated, and the file will be deleted. - -In workflows, these file URLs are provided in the `steps.trigger.event.body` variable, so you can download the file using the URL within your workflow, or pass the URL on to another third-party system for it to process. - -![Raw file URL in event data](/images/triggers/file-upload-urls.png) - -Within your workflow, you can download the contents of this data using the **Send HTTP Request** action, or [by saving the data as a file to the `/tmp` directory](/code/nodejs/working-with-files/). - -#### Example: upload a file using `cURL` - -For example, you can upload an image to a workflow using `cURL`: - -```bash -curl -F 'image=@my_image.png' https://myendpoint.m.pipedream.net -``` - -The `-F` tells `cURL` we're sending form data, with a single "part": a field named `image`, with the content of the image as the value (the `@` allows `cURL` to reference a local file). - -When you send this image to a workflow, Pipedream [parses the form data](#how-pipedream-handles-multipart-form-data) and converts it to a JavaScript object, `event.body`. Select the event from the [inspector](/workflows/events/inspect/#the-inspector), and you'll see the `image` property under `event.body`: - -![Image form data](/images/triggers/image_form_data.png) - -When you upload a file as a part of the form request, Pipedream saves it to a Pipedream-owned [Amazon S3 bucket](https://aws.amazon.com/s3/), generating a [signed URL](https://docs.aws.amazon.com/AmazonS3/latest/dev/ShareObjectPreSignedURL.html) that allows you to access to that file for up to 30 minutes. After 30 minutes, the signed URL will be invalidated, and the file will be deleted. - -Within the `image` property of `event.body`, you'll see the value of this URL in the `url` property, along with the `filename` and `mimetype` of the file. Within your workflow, you can download the file, or pass the URL to a third party system to handle, and more. - -#### Example: Download this file to the `/tmp` directory - -[This workflow](https://pipedream.com/@dylburger/example-download-an-image-to-tmp-p_KwC2Ad/edit) downloads an image passed in the `image` field in the form request, saving it to the [`/tmp` directory](/code/nodejs/working-with-files/#the-tmp-directory). - -```javascript -import stream from "stream"; -import { promisify } from "util"; -import fs from "fs"; -import got from "got"; - -const pipeline = promisify(stream.pipeline); -await pipeline( - got.stream(steps.trigger.event.body.image.url), - fs.createWriteStream(`/tmp/${steps.trigger.event.body.image.filename}`) -); -``` - -#### Example: Upload image to your own Amazon S3 bucket - -[This workflow](https://pipedream.com/@dylburger/example-save-uploaded-file-to-amazon-s3-p_o7Cm9z/edit) streams the uploaded file to an Amazon S3 bucket you specify, allowing you to save the file to long-term storage. - -#### Limits - -Since large files are uploaded using a `Content-Type` of `multipart/form-data`, the limits that apply to [form data](#how-pipedream-handles-multipart-form-data) also apply here. - -The content of the file itself does not contribute to the HTTP payload limit imposed for forms. **You can upload files up to 5TB in size**. However, files that large may trigger [other Pipedream limits](/limits/). Please [reach out](https://pipedream.com/support/) with any specific questions or issues. - -### Cross-Origin HTTP Requests - -We return the following headers on HTTP `OPTIONS` requests: - -``` -Access-Control-Allow-Origin: * -Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE -``` - -Thus, your endpoint will accept [cross-origin HTTP requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) from any domain, using any standard HTTP method. - -### HTTP Responses - -#### Default HTTP response - -By default, when you send a [valid HTTP request](#valid-requests) to your endpoint URL, you should expect to receive a `200 OK` status code with the following payload: - -``` -

Success!

-

To customize this response, check out our docs here

-``` - -When you're processing HTTP requests, you often don't need to issue any special response to the client. We issue this default response so you don't have to write any code to do it yourself. - -#### Customizing the HTTP response - -If you need to issue a custom HTTP response from a workflow, you can either: - -- Use the **Return HTTP response** action, available on the **HTTP / Webhook** app, or -- **Use the `$.respond()` function in a Code or Action step**. - -#### Using the HTTP Response Action - -The HTTP Response action lets you return HTTP responses without the need to write code. You can customize the response status code, and optionally specify response headers and body. - -This action uses `$.respond()` and will always [respond immediately](#returning-a-response-immediately) when called in your workflow. A [response error](#errors-with-http-responses) will still occur if your workflow throws an Error before this action runs. - -#### Using custom code with `$.respond()` - -You can return HTTP responses in Node.js code with the `$.respond()` function. - -`$.respond()` takes a single argument: an object with properties that specify the body, headers, and HTTP status code you'd like to respond with: - -```javascript -defineComponent({ - async run({ steps, $ }) { - await $.respond({ - status: 200, - headers: { "my-custom-header": "value" }, - body: { message: "My custom response" }, // This can be any string, object, Buffer, or Readable stream - }); - }, -}); -``` - -The value of the `body` property can be either a string, object, a [Buffer](https://nodejs.org/api/buffer.html#buffer_buffer) (binary data), or a [Readable stream](https://nodejs.org/api/stream.html#stream_readable_streams). Attempting to return any other data may yield an error. - -In the case where you return a Readable stream: - -- You must `await` the `$.respond` function (`await $.respond({ ... }`) -- The stream must close and be finished reading within your [workflow execution timeout](/limits/#time-per-execution). -- You cannot return a Readable and use the [`immediate: true`](#returning-a-response-immediately) property of `$.respond`. - -#### Timing of `$.respond()` execution - -You may notice some response latency calling workflows that use `$.respond()` from your HTTP client. By default, `$.respond()` is called at the end of your workflow, after all other code is done executing, so it may take some time to issue the response back. - -If you need to issue an HTTP response in the middle of a workflow, see the section on [returning a response immediately](#returning-a-response-immediately). - -#### Returning a response immediately - -You can issue an HTTP response within a workflow, and continue the rest of the workflow execution, by setting the `immediate` property to `true`: - -```javascript -defineComponent({ - async run({ steps, $ }) { - await $.respond({ - immediate: true, - status: 200, - headers: { "my-custom-header": "value" }, - body: { message: "My custom response" }, - }); - }, -}); -``` - -Passing `immediate: true` tells `$.respond()` to issue a response back to the client at this point in the workflow. After the HTTP response has been issued, the remaining code in your workflow runs. - -This can be helpful, for example, when you're building a Slack bot. When you send a message to a bot, Slack requires a `200 OK` response be issued immediately, to confirm receipt: - -```javascript -defineComponent({ - async run({ steps, $ }) { - await $.respond({ - immediate: true, - status: 200, - body: "", - }); - }, -}); -``` - -Once you issue the response, you'll probably want to process the message from the user and respond back with another message or data requested by the user. - -[Here's an example workflow](https://pipedream.com/@dylburger/issue-http-response-immediately-continue-running-workflow-p_pWCWGJ) that shows how to use `immediate: true` and run code after the HTTP response is issued. - -#### Errors with HTTP Responses - -If you use `$.respond()` in a workflow, **you must always make sure `$.respond()` is called in your code**. If you make an HTTP request to a workflow, and run code where `$.respond()` is _not_ called, your endpoint URL will issue a `400 Bad Request` error with the following body: - -``` -No $.respond called in workflow -``` - -This might happen if: - -- You call `$.respond()` conditionally, where it does not run under certain conditions. -- Your workflow throws an Error before you run `$.respond()`. -- You return data in the `body` property that isn't a string, object, or Buffer. - -If you can't handle the `400 Bad Request` error in the application calling your workflow, you can implement `try` / `finally` logic to ensure `$.respond()` always gets called with some default message. For example: - -```javascript -defineComponent({ - async run({ steps, $ }) { - try { - // Your code here that might throw an exception or not run - throw new Error("Whoops, something unexpected happened."); - } finally { - await $.respond({ - status: 200, - body: { - msg: "Default response", - }, - }); - } - }, -}); -``` - -### Errors - -Occasionally, you may encounter errors when sending requests to your endpoint: - -#### Request Entity Too Large - -The endpoint will issue a `413 Payload Too Large` status code when the body of your request exceeds {process.env.PAYLOAD_SIZE_LIMIT}. - -In this case, the request will still appear in the inspector, with information on the error. - -#### API key does not exist - -Your API key is the host part of the endpoint, e.g. the `eniqtww30717` in `eniqtww30717.m.pipedream.net`. If you attempt to send a request to an endpoint that does not exist, we'll return a `404 Not Found` error. - -We'll also issue a 404 response on workflows with an HTTP trigger that have been disabled. - -#### Too Many Requests - -If you send too many requests to your HTTP source within a small period of time, we may issue a `429 Too Many Requests` response. [Review our limits](/limits/) to understand the conditions where you might be throttled. - -You can also [reach out](https://pipedream.com/support/) to inquire about raising this rate limit. - -If you control the application sending requests, you should implement [a backoff strategy](https://medium.com/clover-platform-blog/conquering-api-rate-limiting-dcac5552714d) to temporarily slow the rate of events. - -### Validating requests - -Since you have access to the entire request object, and can issue any HTTP response from a workflow, you can implement custom logic to validate requests using any [Node.js code](/code/nodejs/). - -For example, you can [require requests pass a specific secret in a header](https://pipedream.com/@dylburger/end-a-workflow-early-on-invalid-secret-p_YyCmmK/edit). Just copy the workflow and add your secret as the value of the the **Secret** param. Add the rest of your code in steps below this initial one. Requests must contain the secret: - -```bash -curl -H 'X-Pipedream-Secret: abc123' https://myendpoint.m.pipedream.net -``` - -Otherwise, the workflow will [end early](/code/nodejs/#ending-a-workflow-early). - -Since you can [run any code](/code/) in a workflow, you can implement more complex validation. For example, you could require JWT tokens and validate those tokens using the [`jsonwebtoken` package](https://www.npmjs.com/package/jsonwebtoken) at the start of your workflow. - -## Schedule - -Pipedream allows you to run hosted scheduled jobs — commonly-referred to as a "cron job" — [for free](/pricing/). You can think of workflows like scripts that run on a schedule. - -You can write scheduled job to send an HTTP request, send a scheduled email, run any Node.js or Python code, connect to any API, and much more. Pipedream manages the servers where these jobs run, so you don't have to worry about setting up a server of your own or operating some service just to run code on a schedule. You write the workflow, we take care of the rest. - -### Choosing a Schedule trigger - -To create a new scheduled job, create a new workflow and search for the **Schedule** trigger: - -![Cron Scheduler source](/images/triggers/schedule.png) - -By default, your trigger will be turned **Off**. **To enable it, select either of the scheduling options**: - -- **Every** : run the job every N days, hours, minutes (e.g. every 1 day, every 3 hours). -- **Cron Expression** : schedule your job using a cron expression. For example, the expression `0 0 * * *` will run the job every day at midnight. Cron expressions can be tied to any timezone. - -### Testing a scheduled job - -If you're running a scheduled job once a day, you probably don't want to wait until the next day's run to test your new code. You can manually run the workflow associated with a scheduled job at any time by pressing the **Run Now** button. - -### Job History - -You'll see the history of job executions under the **Job History** section of the [Inspector](/workflows/events/inspect/). - -Clicking on a specific job shows the execution details for that job — all the logs and observability associated with that run of the workflow. - -### Trigger a notification to an external service (email, Slack, etc.) - -You can send yourself a notification — for example, an email or a Slack message — at any point in a workflow by using the relevant [Action](/components#actions) or [Destination](/destinations/). - -If you'd like to email yourself when a job finishes successfully, you can use the [Email Destination](/destinations/email/). You can send yourself a Slack message using the Slack Action, or trigger an [HTTP request](/destinations/http/) to an external service. - -You can also [write code](/code/) to trigger any complex notification logic you'd like. - -### Troubleshooting your scheduled jobs - -When you run a scheduled job, you may need to troubleshoot errors or other execution issues. Pipedream offers built-in, step-level logs that show you detailed execution information that should aid troubleshooting. - -Any time a scheduled job runs, you'll see a new execution appear in the [Inspector](/workflows/events/inspect/). This shows you when the job ran, how long it took to run, and any errors that might have occurred. **Click on any of these lines in the Inspector to view the details for a given run**. - -Code steps show [logs](/code/nodejs/#logs) below the step itself. Any time you run `console.log()` or other functions that print output, you should see the logs appear directly below the step where the code ran. - -[Actions](/components#actions) and [Destinations](/destinations/) also show execution details relevant to the specific Action or Destination. For example, when you use the [HTTP Destination](/destinations/http/) to make an HTTP request, you'll see the HTTP request and response details tied to that Destination step: - -## Email - -When you select the **Email** trigger: - - - -Pipedream creates an email address specific to your workflow. Any email sent to this address triggers your workflow: - -![Custom workflow email address](/images/triggers/email-addr.png) - -As soon as you send an email to the workflow-specific address, Pipedream parses its body, headers, and attachments into a JavaScript object it exposes in the `steps.trigger.event` variable that you can access within your workflow. This transformation can take a few seconds to perform. Once done, Pipedream will immediately trigger your workflow with the transformed payload. - -[Read more about the shape of the email trigger event](/workflows/events/#email). - -### Sending large emails - -By default, you can send emails up to {process.env.EMAIL_PAYLOAD_SIZE_LIMIT} in total size (content, headers, attachments). Emails over this size will be rejected, and you will not see them appear in your workflow. - -**You can send emails up to `30MB` in size by sending emails to `[YOUR EMAIL ENDPOINT]@upload.pipedream.net`**. If your workflow-specific email address is `endpoint@pipedream.net`, your "large email address" is `endpoint@upload.pipedream.net`. - -Emails delivered to this address are uploaded to a private URL you have access to within your workflow, at the variable `steps.trigger.event.mail.content_url`. You can download and parse the email within your workflow using that URL. This content contains the _raw_ email. Unlike the standard email interface, you must parse this email on your own - see the examples below. - -#### Example: Download the email using the Send HTTP Request action - -_Note: you can only download emails at most {process.env.FUNCTION_PAYLOAD_LIMIT} in size using this method. Otherwise, you may encounter a [Function Payload Limit Exceeded](/troubleshooting/#function-payload-limit-exceeded) error._ - -You can download the email using the **Send HTTP Request** action. [Copy this workflow to see how this works](https://pipedream.com/new?h=tch_1AfMyl). - -This workflow also parses the contents of the email and exposes it as a JavaScript object using the [`mailparser` library](https://nodemailer.com/extras/mailparser/): - -```javascript -import { simpleParser } from "mailparser"; - -export default defineComponent({ - async run({ steps, $ }) { - return await simpleParser(steps.get_large_email_content.$return_value); - }, -}); -``` - -#### Example: Download the email to the `/tmp` directory, read it and parse it - -[This workflow](https://pipedream.com/new?h=tch_jPfaEJ) downloads the email, saving it as a file to the [`/tmp` directory](/code/nodejs/working-with-files/#the-tmp-directory). Then it reads the same file (as an example), and parses it using the [`mailparser` library](https://nodemailer.com/extras/mailparser/): - -```javascript -import stream from "stream"; -import { promisify } from "util"; -import fs from "fs"; -import got from "got"; -import { simpleParser } from "mailparser"; - -// To use previous step data, pass the `steps` object to the run() function -export default defineComponent({ - async run({ steps, $ }) { - const pipeline = promisify(stream.pipeline); - await pipeline( - got.stream(steps.trigger.event.mail.content_url), - fs.createWriteStream(`/tmp/raw_email`) - ); - - // Now read the file and parse its contents into the `parsed` variable - // See https://nodemailer.com/extras/mailparser/ for parsing options - const f = fs.readFileSync(`/tmp/raw_email`); - return await simpleParser(f); - }, -}); -``` - -#### How the email is saved - -Your email is saved to a Pipedream-owned [Amazon S3 bucket](https://aws.amazon.com/s3/). Pipedream generates a [signed URL](https://docs.aws.amazon.com/AmazonS3/latest/dev/ShareObjectPreSignedURL.html) that allows you to access to that file for up to 30 minutes. After 30 minutes, the signed URL will be invalidated, and the file will be deleted. - -### Email attachments - -You can attach any files to your email, up to [the total email size limit](/limits/#email-triggers). - -Attachments are stored in `steps.trigger.event.attachments`, which provides an array of attachment objects. Each attachment in that array exposes key properties: - -- `contentUrl`: a URL that hosts your attachment. You can [download this file to the `/tmp` directory](/code/nodejs/http-requests/#download-a-file-to-the-tmp-directory) and process it in your workflow. -- `content`: If the attachment contains text-based content, Pipedream renders the attachment in `content`, up to 10,000 bytes. -- `contentTruncated`: `true` if the attachment contained text-based content larger than 10,000 bytes. If `true`, the data in `content` will be truncated, and you should fetch the full attachment from `contentUrl`. - -### Appending metadata to the incoming email address with `+data` - -Pipedream provides a way to append metadata to incoming emails by adding a `+` sign to the incoming email key, followed by any arbitrary string: - -``` -myemailaddr+test@pipedream.net -``` - -Any emails sent to your workflow-specific email address will resolve to that address, triggering your workflow, no matter the data you add after the `+` sign. Sending an email to both of these addresses triggers the workflow with the address `myemailaddr@pipedream.net`: - -``` -myemailaddr+test@pipedream.net -myemailaddr+unsubscribe@pipedream.net -``` - -This allows you implement conditional logic in your workflow based on the data in that string. - -## RSS - -Choose the RSS trigger to watch an RSS feed for new items: - - - -This will create an RSS [event source](/sources/) that polls the feed for new items on the schedule you select. Every time a new item is found, your workflow will run. - -## Don't see a trigger you need? - -If you don't see a trigger you'd like us to support, please [let us know](https://pipedream.com/support/). - -## Troubleshooting - -### I'm receiving an `Expired Token` error when trying to read an email attachment - -Email attachments are saved to S3, and are accessible in your workflows over [pre-signed URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html). - -If the presigned URL for the attachment has expired, then you'll need to send another email to create a brand new pre-signed URL. - -If you're using email attachments in combination with [`$.flow.delay`](/code/nodejs/delay/) or [`$.flow.rerun`](/code/nodejs/rerun/) which introduces a gap of time between steps in your workflow, then there's a chance the email attachment's URL will expire. - -To overcome this, we suggest uploading your email attachments to your Project's [File Store](/projects/file-stores/) for persistent storage. \ No newline at end of file diff --git a/docs-v2/pages/workflows/vpc.mdx b/docs-v2/pages/workflows/vpc.mdx index 3f095d02f820d..b7dbe881ced18 100644 --- a/docs-v2/pages/workflows/vpc.mdx +++ b/docs-v2/pages/workflows/vpc.mdx @@ -4,10 +4,6 @@ import VideoPlayer from '@/components/VideoPlayer'; -Every Pipedream workflow is deployed to its own virtual machine in AWS. This means your workflow's execution environment has its own RAM and disk, isolated from other users’ workflows. - -However, outbound traffic shares the same network as other AWS services deployed in the `us-east-1` region. That means network requests from your workflows (e.g. an HTTP request or a connection to a database) originate from the standard range of AWS IP addresses. - Pipedream VPCs enable you to run workflows in dedicated and isolated networks with static outbound egress IP addresses that are unique to your workspace (unlike other platforms that provide static IPs common to all customers on the platform). Outbound network requests from workflows that run in a VPC will originate from these static IP addresses, so you can whitelist access to sensitive resources (like databases and APIs) with confidence that the requests will only originate from the Pipedream workflows in your workspace. @@ -52,19 +48,19 @@ To rename or delete a VPC, navigate to the [Virtual Private Cloud settings](http ## Self-hosting and VPC peering -If you're interested in running Pipedream workflows in your own infrastructure, or configure VPC peering to allow Pipedream to communicate to resources in a private network, please reach out to our [Enterprise Sales team](mailto:sales@pipedream.com). +If you're interested in running Pipedream workflows in your own infrastructure, or configure VPC peering to allow Pipedream to communicate to resources in a private network, please reach out to our [Sales team](mailto:sales@pipedream.com). ## Limitations -- Only workflows can run in VPCs (other resources like sources or data stores are not currently supported). For example, [sources](/sources/) cannot yet run in VPCs. +- Only workflows can run in VPCs (other resources like sources or data stores are not currently supported). For example, [sources](/workflows/building-workflows/triggers/) cannot yet run in VPCs. - Creating a new network can take up to 5 minutes. Deploying your first workflow into a new network and testing that workflow for the first time can take up to 1 min. Subsequent operations should be as fast as normal. - VPCs only provide static IPs for outbound network requests. This feature does not provide a static IP for or otherwise restrict inbound requests. - You can't set a default network for all new workflows in a workspace or project (you must select the network every time you create a new workflow). Please [reach out](https://pipedream.com/support) if you're interesting in imposing controls like this in your workspace. -- Workflows running in a VPC will still route specific requests routed through [the shared Pipedream network](/destinations/http/#ip-addresses-for-pipedream-http-requests): - - [`$.send.http()`](/destinations/http/) requests +- Workflows running in a VPC will still route specific requests routed through [the shared Pipedream network](/workflows/data-management/destinations/http/#ip-addresses-for-pipedream-http-requests): + - [`$.send.http()`](/workflows/data-management/destinations/http/) requests - Async options requests (these are requests that are made to populate options in drop down menus for actions while a building a workflow — e.g., the option to “select a Google Sheet” when using the “add row to Google Sheets” action) -## Frequently Asked Questions +## FAQ ### Will HTTP requests sent from Node.js, Python and the HTTP request steps use the assigned static IP address? diff --git a/docs-v2/pages/workflows/workspaces/_meta.tsx b/docs-v2/pages/workflows/workspaces/_meta.tsx new file mode 100644 index 0000000000000..74a6e04201267 --- /dev/null +++ b/docs-v2/pages/workflows/workspaces/_meta.tsx @@ -0,0 +1,5 @@ +export default { + "index": "Managing workspaces", + "sso": "Single-Sign On", + "domain-verification": "Domain Verification", +} as const diff --git a/docs-v2/pages/workflows/workspaces/domain-verification.mdx b/docs-v2/pages/workflows/workspaces/domain-verification.mdx new file mode 100644 index 0000000000000..8c175cd574a79 --- /dev/null +++ b/docs-v2/pages/workflows/workspaces/domain-verification.mdx @@ -0,0 +1,19 @@ +import Callout from '@/components/Callout' + +# Domain Verification + +Pipedream requires that you verify ownership of your email domain in order to [configure SAML SSO](/workflows/workspaces/sso/) for your workspace. If your email is `foo@example.com`, you need to verify ownership of `example.com`. If configuring Google OAuth (not SAML), you can disregard this section. + +## Getting started + +1. Navigate to the **[Verified Domains](https://pipedream.com/settings/domains)** section of your workspace settings +2. Enter the domain you'd like to use then click **Add Domain** +3. You'll see a modal with instructions for adding a `TXT` record in the DNS configuration for your domain +4. DNS changes may take between a few minutes and up to 72 hours to propagate. Once they're live, click the **Verify** button for the domain you've entered +5. Once Pipedream verifies the `TXT` record, we'll show a green checkmark on the domain + + +Make sure to verify all your domains. There's no limit on the number of domains you can verify for SSO, so if you use `example.com`, `example.net`, and `foo.example.com`, make sure to verify each one. + + +![Verified Domains](https://res.cloudinary.com/pipedreamin/image/upload/v1699938431/verified-domains_qcjpnb.png) diff --git a/docs-v2/pages/workflows/workspaces/index.mdx b/docs-v2/pages/workflows/workspaces/index.mdx new file mode 100644 index 0000000000000..179da66eff475 --- /dev/null +++ b/docs-v2/pages/workflows/workspaces/index.mdx @@ -0,0 +1,113 @@ +import Callout from '@/components/Callout' + +# Managing workspaces + +When you sign up for Pipedream, you'll either create a new workspace or join an existing one if you signed up from an invitation. + +You can create and join any number of workspaces. For example, you can create one to work alone and another to collaborate with your team. You can also start working alone, then easily add others into your existing workspace to work together on workflows you've already built out. + +Once you've created a new workspace, you can invite your team to create and edit workflows together, and organize them within projects and folders. + +## Creating a new workspace + +To create a new workspace, + +1. Open the dropdown menu in the top left of the Pipedream dashboard +2. Select **New workspace** +3. You'll be prompted to name the workspace (you can [change the name later](/workflows/workspaces/#renaming-a-workspace)) + +## Workspace settings + +Find your current [workspace settings](https://pipedream.com/settings/account) like current members, under the **Settings** navigation menu item on the left hand side. This is where you can manage your workspace settings, including the workspace name, members, and member permissions. + +### Inviting others to a join a workspace + +After opening your workspace settings, open the [Membership](https://pipedream.com/settings/users) tab. + +- Invite people to your workspace by entering their email address and then clicking **Send** +- Or create an invite link to more easily share with a larger group (you can limit access to only specific email domains) + +![Creating an invite link](https://res.cloudinary.com/pipedreamin/image/upload/v1688074217/Google_Chrome_-_Settings_-_Users_-_Pipedream_2023-06-29_at_2.28.12_PM_xy33fl.png) + +### Managing member permissions + +By default, new workspace members are assigned the **Member** level permission. + +**Members** will be able to perform general tasks like viewing, developing, and deploying workflows. + +However, only **Admins** will be able to manage workspace level settings, like changing member roles, renaming workspaces, and modifying Slack error notifications. + +#### Promoting a member to admin + +To promote a member to an admin level account in your workspace, click the 3 dots to the right of their email and select "Make Admin". + +![Promoting a member to admin](https://res.cloudinary.com/pipedreamin/image/upload/v1688075628/making_admin_btkbh7.gif) + +#### Demoting an admin to a member + +To demote an admin back to a member, click the 3 dots to the right of their email address and select "Remove Admin". + +![Demoting an admin to a member](https://res.cloudinary.com/pipedreamin/image/upload/v1688075628/removing_admin_wez5km.gif) + +### Finding your workspace's ID + +Visit your [workspace settings](https://pipedream.com/settings/account) and scroll down to the **API** section. You'll see your workspace ID here. + +### Requiring Two-Factor Authentication +As a workspace admin or owner on the [Business plan](https://pipedream.com/pricing), you're able to **require** that all members in your workspace must enable 2FA on their account. + +1. Open the Authentication tab in your [workspace settings](https://pipedream.com/settings/authentication) (you must be an admin or owner to make changes here) +2. Make sure you're in the [correct workspace](/workflows/workspaces/#switching-between-workspaces) +3. Click the toggle under **Require 2FA** — this will open a confirmation modal with some additional information +4. Once you enable the change in the modal, **all workspace members (including admins and owners) will immediately be required to configure 2FA on their account**. All new and existing workspace members will be required to set up 2FA the next time they sign in. + +![Require 2FA Toggle](https://res.cloudinary.com/pipedreamin/image/upload/v1695147277/require_2fa_xha6yc.png) + + +Anyone who is currently logged in to Pipedream will be temporarily signed out until they set up 2FA + +If anyone is actively making changes to a workflow, their session may be interrupted. We recommend enabling the 2FA requirement in off hours. + + + +### Configuring Single Sign-On (SSO) + +Workspaces on the Business plan can configure Single Sign-On, so your users can login to Pipedream using your identity provider. + +Pipedream supports SSO with Google, Okta, and any provider that supports the SAML protocol. See the guides below to configure SSO for your identity provider: + +- [Okta](./sso/okta) +- [Google](./sso/google/) +- [Other SAML provider](./sso/saml/) + +### SCIM + +Pipedream supports provisioning user accounts from your IdP via SCIM. Any workspace on the Business plan can configure Single Sign-On with SCIM. + +### Renaming a workspace + +To rename a workspace, open your [workspace settings](https://pipedream.com/settings/account) and navigate to the **General** tab. + +Click the save button to save the changes. + + +This action is only available to workspace **admins**. + + +### Deleting a workspace + +To delete a workspace, open your workspace settings and navigate to the **Danger Zone**. + +Click the **Delete workspace** button and confirm the action by entering in your workspace name and `delete my workspace` into the text prompt. + + +Deleting a workspace will delete all **sources**, **workflows**, and other resources in your workspace. + +Deleting a workspace is **irreversible** and permanent. + + +## Switching between workspaces + +To switch between workspaces, open the dropdown menu in the top left of the Pipedream dashboard. + +Select which workspace you'd like to start working within, and your Pipedream dashboard context will change to that workspace. diff --git a/docs-v2/pages/workflows/workspaces/sso/_meta.tsx b/docs-v2/pages/workflows/workspaces/sso/_meta.tsx new file mode 100644 index 0000000000000..c7401c2cb175d --- /dev/null +++ b/docs-v2/pages/workflows/workspaces/sso/_meta.tsx @@ -0,0 +1,6 @@ +export default { + "index": "Overview", + "google": "Google Workspace", + "okta": "Okta", + "saml": "SAML", +} as const diff --git a/docs-v2/pages/workspaces/sso/google.mdx b/docs-v2/pages/workflows/workspaces/sso/google.mdx similarity index 95% rename from docs-v2/pages/workspaces/sso/google.mdx rename to docs-v2/pages/workflows/workspaces/sso/google.mdx index 74a63cd9c0a41..44aca88b5eaec 100644 --- a/docs-v2/pages/workspaces/sso/google.mdx +++ b/docs-v2/pages/workflows/workspaces/sso/google.mdx @@ -6,7 +6,7 @@ Pipedream supports Single Sign-On (SSO) with Google Workspace. This guide shows ## Requirements -- SSO is only supported for [workspaces](/workspaces/) on the Business and Enterprise plans. Visit the [Pipedream pricing page](https://pipedream.com/pricing) to upgrade. +- SSO is only supported for [workspaces](/workflows/workspaces/) on the Business plan. Visit the [Pipedream pricing page](https://pipedream.com/pricing) to upgrade. - You need an administrator of your Pipedream workspace and someone who can [create SAML apps in Google Workspace](https://apps.google.com/supportwidget/articlehome?hl=en&article_url=https%3A%2F%2Fsupport.google.com%2Fa%2Fanswer%2F6087519%3Fhl%3Den&assistant_id=generic-unu&product_context=6087519&product_name=UnuFlow&trigger_context=a) to configure SSO. ## Configuration @@ -123,7 +123,7 @@ In the **Single Sign-On** section, select **SAML**, and add the URL from step 7
-Any user in your workspace can now log into Pipedream at [https://pipedream.com/auth/sso](https://pipedream.com/auth/sso) by entering your workspaces's name (found in your [Settings](https://pipedream.com/settings/account)). You can also access your SSO sign in URL directly by visiting [https://pipedream.com/auth/sso/your-workspace-name](https://pipedream.com/auth/sso), where `your-workspace-name` is the name of your workspace. +Any user in your workspace can now log into Pipedream at [https://pipedream.com/auth/sso](https://pipedream.com/auth/sso) by entering your workspaces's name (found in your [Settings](https://pipedream.com/settings/account)). You can also access your SSO sign in URL directly by visiting [https://pipedream.com/auth/org/your-workspace-name](https://pipedream.com/auth/org), where `your-workspace-name` is the name of your workspace. ## Important details diff --git a/docs-v2/pages/workflows/workspaces/sso/index.mdx b/docs-v2/pages/workflows/workspaces/sso/index.mdx new file mode 100644 index 0000000000000..107a321730876 --- /dev/null +++ b/docs-v2/pages/workflows/workspaces/sso/index.mdx @@ -0,0 +1,62 @@ +import Callout from '@/components/Callout' + +# Single Sign-On Overview + +Pipedream supports Single Sign-On (SSO) with [Okta](./sso/okta/), [Google](./sso/google/), or [any provider](./sso/saml/) that supports SAML or Google OAuth, which allows IT and workspace administrators easier controls to manage access and security. + +Using SSO with your Identity Provider (IdP) centralizes user login management and provides a single point of control for IT teams and employees. + +## Requirements for SSO + +- Your workspace must be on a [Business plan](https://pipedream.com/pricing) +- If using SAML, your Identity Provider must support SAML 2.0 +- Only workspace admins and owners can configure SSO +- Your workspace admin or owner must [verify ownership](#verifying-your-email-domain) of the SSO email domain + + +The below content is for workspace admins and owners. Only workspace admins and owners have access to add verified domains, set up SSO, and configure workspace login methods. + + +## Verifying your Email Domain + +In order to configure SAML SSO for your workspace, you first need to verify ownership of the email domain. If configuring Google OAuth (not SAML), you can skip this section. + +[Refer to the guide here](/workflows/workspaces/domain-verification/) to verify your email domain. + +## Setting up SSO + +Navigate to the [Authentication section](https://pipedream.com/settings/domains) in your workspace settings to get started. + +### SAML SSO + +1. First, make sure you've verified the domain(s) you intend to use for SSO ([see above](#verifying-your-email-domain)) +2. Click the **Enable SSO** toggle and select **SAML** +3. If setting up SAML SSO, you'll need to enter a metadata URL, which contains all the necessary configuration for Pipedream. Refer to the provider-specific docs for the detailed walk-through ([Okta](./sso/okta/), [Google Workspace](./sso/google/), [any other SAML provider](./sso/saml/)). +4. Click **Save** + +### Google OAuth + +1. Click the **Enable SSO** toggle and select **Google** +2. Enter the domain that you use with Google OAuth. For example, `vandalayindustries.com` +3. Click **Save** + +## Restricting Login Methods + +Once you've configured SSO for your workspace, you can restrict the allowed login methods for [non-workspace owners](#workspace-owners-can-always-sign-in-using-any-login-method). + +![Restrict Login Methods](https://res.cloudinary.com/pipedreamin/image/upload/v1699914460/Google_Chrome_-_Settings_-_Authentication_-_Pipedream_2023-11-13_at_2.27.08_PM_x1ahod.png) + + +| Login Method | Description | +| -- | -- | +| **Any login method** | Everyone in the workspace can sign in either using SSO or via the login method they used to create their account (email and password, Google OAuth, GitHub) | +| **SSO only** | Workspace members and admins must [sign in using SSO](https://pipedream.com/auth/sso) | +| **SSO with guests** | When siging in using a verified email domain, members and admins must [sign in using SSO](https://pipedream.com/auth/sso). If signing in with a different domain (`gmail.com` for example), members (guests) can sign in using any login method. | + +### Workspace owners can always sign in using any login method + +In order to ensure you don't get locked out of your Pipedream workspace in the event of an outage with your identity provider, workspace owners can always sign in via the login method they used to create the account (email and password, Google, or GitHub). + +### Login methods are enforced when signing in to pipedream.com + +This means if you are a member of 2 workspaces and one of them allows **any login method** but the other **requires SSO**, you will be required to sign in to Pipedream using SSO every time, independent of the workspace you are trying to access. diff --git a/docs-v2/pages/workspaces/sso/okta.mdx b/docs-v2/pages/workflows/workspaces/sso/okta.mdx similarity index 91% rename from docs-v2/pages/workspaces/sso/okta.mdx rename to docs-v2/pages/workflows/workspaces/sso/okta.mdx index 021261612e886..879c80285d350 100644 --- a/docs-v2/pages/workspaces/sso/okta.mdx +++ b/docs-v2/pages/workflows/workspaces/sso/okta.mdx @@ -7,7 +7,7 @@ Pipedream supports Single Sign-On (SSO) with Okta. This guide shows you how to c ## Requirements -- SSO is only supported for [workspaces](/workspaces/) on the Business and Enterprise plans. Visit the [Pipedream pricing page](https://pipedream.com/pricing) to upgrade. +- SSO is only supported for [workspaces](/workflows/workspaces/) on the Business plan. Visit the [Pipedream pricing page](https://pipedream.com/pricing) to upgrade. - You must be an administrator of your Pipedream workspace - You must have an Okta account @@ -73,7 +73,7 @@ Click the **Identity Provider metadata** link and copy the URL from your browser Visit your [Pipedream workspace authentication settings](https://pipedream.com/settings/authentication). Click the toggle to **Enable SSO**, then click **Edit SSO Configuration**, and add the metadata URL in the **SAML** section and click **Save**: -![Pipedream - SAML Metadata URL](https://res.cloudinary.com/pipedreamin/image/upload/v1708206217/docs/step-10_seqd43.png) +![Pipedream - SAML Metadata URL](https://res.cloudinary.com/pipedreamin/image/upload/v1712252695/pipedream.com_settings_authentication_3_dtt5fa.png) ### Assign the application to users @@ -84,7 +84,7 @@ Back in Okta, click on the **Assignments** tab of the Pipedream application. Cli Assign the application to the relevant users in Okta, and Pipedream will configure the associated accounts on our end. -Users configured in your Okta app can log into Pipedream at [https://pipedream.com/auth/sso](https://pipedream.com/auth/sso) by entering your workspaces's name (found in your [Settings](https://pipedream.com/settings/account)). You can also access your SSO sign in URL directly by visiting [https://pipedream.com/auth/sso/your-workspace-name](https://pipedream.com/auth/sso), where `your-workspace-name` is the name of your workspace. +Users configured in your Okta app can log into Pipedream at [https://pipedream.com/auth/sso](https://pipedream.com/auth/sso) by entering your workspaces's name (found in your [Settings](https://pipedream.com/settings/account)). You can also access your SSO sign in URL directly by visiting [https://pipedream.com/auth/org/your-workspace-name](https://pipedream.com/auth/org), where `your-workspace-name` is the name of your workspace. ## Important details diff --git a/docs-v2/pages/workspaces/sso/saml.mdx b/docs-v2/pages/workflows/workspaces/sso/saml.mdx similarity index 88% rename from docs-v2/pages/workspaces/sso/saml.mdx rename to docs-v2/pages/workflows/workspaces/sso/saml.mdx index df02479d4281c..40e1fc616619a 100644 --- a/docs-v2/pages/workspaces/sso/saml.mdx +++ b/docs-v2/pages/workflows/workspaces/sso/saml.mdx @@ -2,11 +2,11 @@ Pipedream supports Single Sign-On (SSO) with any identity provider that supports SAML 2.0. This guide shows you how to configure SSO in Pipedream to authenticate with your SAML provider. -If you use [Okta](/workspaces/sso/okta/) or [Google Workspace](/workspaces/sso/google/), please review the guides for those apps. +If you use [Okta](./okta/) or [Google Workspace](./google/), please review the guides for those apps. ## Requirements -- SSO is only supported for [workspaces](/workspaces/) on the Business and Enterprise plans. Visit the [Pipedream pricing page](https://pipedream.com/pricing) to upgrade. +- SSO is only supported for [workspaces](/workflows/workspaces/) on the Business plan. Visit the [Pipedream pricing page](https://pipedream.com/pricing) to upgrade. - You need an administrator of your Pipedream workspace and someone who can create SAML apps in your identity provider to configure SSO. ## SAML metadata @@ -32,12 +32,12 @@ Once you have a publicly-accessible URL that hosts your SAML metadata, visit you
Pipedream metadata URL
-Any user in your workspace can now log into Pipedream at [https://pipedream.com/auth/sso](https://pipedream.com/auth/sso) by entering your workspaces's name (found in your [Settings](https://pipedream.com/settings/account)). You can also access your SSO sign in URL directly by visiting [https://pipedream.com/auth/sso/your_workspace_name](https://pipedream.com/auth/sso), where `your_workspace_name` is the name of your workspace. +Any user in your workspace can now log into Pipedream at [https://pipedream.com/auth/sso](https://pipedream.com/auth/sso) by entering your workspaces's name (found in your [Settings](https://pipedream.com/settings/account)). You can also access your SSO sign in URL directly by visiting [https://pipedream.com/auth/org/your-workspace-name](https://pipedream.com/auth/org), where `your-workspace-name` is the name of your workspace. ## Important details diff --git a/docs-v2/pages/workspaces-and-credits-faq/_meta.json b/docs-v2/pages/workspaces-and-credits-faq/_meta.json deleted file mode 100644 index 5e6a93c3a56e9..0000000000000 --- a/docs-v2/pages/workspaces-and-credits-faq/_meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "index": { - "display": "hidden" - } -} diff --git a/docs-v2/pages/workspaces-and-credits-faq/index.mdx b/docs-v2/pages/workspaces-and-credits-faq/index.mdx deleted file mode 100644 index 1b7aa89f16047..0000000000000 --- a/docs-v2/pages/workspaces-and-credits-faq/index.mdx +++ /dev/null @@ -1,55 +0,0 @@ -# Workspaces, Credits, and Other New Features for Pipedream Orgs - -We're excited to announce a new set of foundational changes and features! In short — orgs are now [workspaces](/workspaces), invocations are now [credits](/pricing/#credits), and you have access to new features: [free source invocations](/pricing/#source-credit-usage), unlimited team members, and more. - - - -## Orgs are now workspaces - -We've renamed organizations to [workspaces](/workspaces), which is more consistent with the language other products use. Anywhere you used to see references to "organizations", you'll now see "workspaces". - -## Invocations are now credits - -Pipedream previously charged for invocations (number of workflow executions), but not on compute time (how long your executions run). We've combined these into a single metric: [credits](/pricing/#credits). Your workspace now has a default credit limit of 50,000 credits per month, an increase of 30,000 credits a month from the Team plan. - -## Source invocations are now free - -Previously, workflows often cost two invocations: one to run your source, and another for the workflow. Now, [most source executions are free](/pricing/#source-credit-usage), and you'll only be charged credits for workflow executions. - -## The Team plan is now the Advanced plan - -As a part of this rollout, we've released new paid plans, and are automatically upgrading your org to the Advanced Plan. We're grateful to have you as an early customer, and **we're honoring your old price for the next 12 months. You should see no impact to your monthly bill with this change**. If you do, please reach out to `support@pipedream.com`. In 12 months, your workspace will move to the standard Advanced plan pricing: currently $149 / month for 50,000 credits, $0.0004 per extra credit. - -### What's on the Advanced plan? - -You have access to everything on the Team plan, including: - -- 50,000 credits per month, 30,000 more than the Team plan -- Support for annual plans at 33% off the monthly price — you can change to an annual plan after we convert your org to a workspace -- 5 team members by default, $50 / user / month for extra users ($30 for annual plans) -- [Auto-retry](/workflows/settings/#auto-retry-errors) on workflow errors -- Slack error notifications - -A GitHub integration, folders to organize workflows, and other features are coming soon. - -## The Pro plan is now the Basic plan - -As part of this rollout, we've released new paid plans and are automatically upgrading your existing Pro plan to the Basic Plan. We're grateful to have you as an early customer, and **we're honoring your old price for the next 12 months. You should see no impact on your monthly bill with this change**. If you do, please reach out to `support@pipedream.com`. In 12 months, your workspace will move to the standard Basic plan pricing: currently $29 / month for 10,000 credits, $0.0008 per extra credit. - -### What's in the Basic plan? - -You have access to everything you had in the Pro plan. A GitHub integration, folders to organize workflows, and other features are coming soon. - -### Why the plan / pricing changes? - -Overall, we try to price our products far below the value we are providing, and try to do right by the existing customers who helped us get to where we are. - -We developed our old pricing when we first launched organizations, and had limited functionality. Today, we offer: - -- Better collaboration in workspaces -- Thousands of integrated apps, triggers, and actions -- Support for Node.js, Python, Go, and Bash -- Data stores, concurrency and execution controls, and automatic retry for failed executions. -- We'll be launching a GitHub integration, extended event history, warm workers, looping and branching, and much more this year. - -Our goal is to build a product that our customers feel is wildly underpriced for the value we are providing, and we are working every day to make that happen. If there are areas in which our product isn’t delivering, please reach out anytime at `support@pipedream.com`. diff --git a/docs-v2/pages/workspaces/_meta.json b/docs-v2/pages/workspaces/_meta.json deleted file mode 100644 index b0a057c9dd4bb..0000000000000 --- a/docs-v2/pages/workspaces/_meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "index": "Managing workspaces", - "sso": "Single-Sign On" -} diff --git a/docs-v2/pages/workspaces/images/context-switcher.png b/docs-v2/pages/workspaces/images/context-switcher.png deleted file mode 100644 index 69378be803b74..0000000000000 Binary files a/docs-v2/pages/workspaces/images/context-switcher.png and /dev/null differ diff --git a/docs-v2/pages/workspaces/images/create-an-org.gif b/docs-v2/pages/workspaces/images/create-an-org.gif deleted file mode 100644 index 4b8b1f5a1ab0a..0000000000000 Binary files a/docs-v2/pages/workspaces/images/create-an-org.gif and /dev/null differ diff --git a/docs-v2/pages/workspaces/index.mdx b/docs-v2/pages/workspaces/index.mdx deleted file mode 100644 index 09707427fbf93..0000000000000 --- a/docs-v2/pages/workspaces/index.mdx +++ /dev/null @@ -1,113 +0,0 @@ -import Callout from '@/components/Callout' - -# Managing workspaces - -When you sign up for Pipedream, you'll either create a new workspace or join an existing one if you signed up from an invitation. - -You can create and join any number of workspaces. For example, you can create one to work alone and another to collaborate with your team. You can also start working alone, then easily add others into your existing workspace to work together on workflows you've already built out. - -Once you've created a new workspace, you can invite your team to create and edit workflows together, and organize them within projects and folders. - -## Creating a new workspace - -To create a new workspace, - -1. Open the dropdown menu in the top left of the Pipedream dashboard -2. Select **New workspace** -3. You'll be prompted to name the workspace (you can [change the name later](/workspaces/#renaming-a-workspace)) - -## Workspace settings - -Find your current [workspace settings](https://pipedream.com/settings/account) like current members, under the **Settings** navigation menu item on the left hand side. This is where you can manage your workspace settings, including the workspace name, members, and member permissions. - -### Inviting others to a join a workspace - -After opening your workspace settings, open the [Membership](https://pipedream.com/settings/users) tab. - -- Invite people to your workspace by entering their email address and then clicking **Send** -- Or create an invite link to more easily share with a larger group (you can limit access to only specific email domains) - -![Creating an invite link](https://res.cloudinary.com/pipedreamin/image/upload/v1688074217/Google_Chrome_-_Settings_-_Users_-_Pipedream_2023-06-29_at_2.28.12_PM_xy33fl.png) - -### Managing member permissions - -By default, new workspace members are assigned the **Member** level permission. - -**Members** will be able to perform general tasks like viewing, developing, and deploying workflows. - -However, only **Admins** will be able to manage workspace level settings, like changing member roles, renaming workspaces, and modifying Slack error notifications. - -#### Promoting a member to admin - -To promote a member to an admin level account in your workspace, click the 3 dots to the right of their email and select "Make Admin". - -![Promoting a member to admin](https://res.cloudinary.com/pipedreamin/image/upload/v1688075628/making_admin_btkbh7.gif) - -#### Demoting an admin to a member - -To demote an admin back to a member, click the 3 dots to the right of their email address and select "Remove Admin". - -![Demoting an admin to a member](https://res.cloudinary.com/pipedreamin/image/upload/v1688075628/removing_admin_wez5km.gif) - -### Finding your workspace's ID - -Visit your [workspace settings](https://pipedream.com/settings/account) and scroll down to the **API** section. You'll see your workspace ID here. - -### Requiring Two-Factor Authentication -As a workspace admin or owner on the [Business plan](https://pipedream.com/pricing), you're able to **require** that all members in your workspace must enable 2FA on their account. - -1. Open the Authentication tab in your [workspace settings](https://pipedream.com/settings/authentication) (you must be an admin or owner to make changes here) -2. Make sure you're in the [correct workspace](/workspaces/#switching-between-workspaces) -3. Click the toggle under **Require 2FA** — this will open a confirmation modal with some additional information -4. Once you enable the change in the modal, **all workspace members (including admins and owners) will immediately be required to configure 2FA on their account**. All new and existing workspace members will be required to set up 2FA the next time they sign in. - -![Require 2FA Toggle](https://res.cloudinary.com/pipedreamin/image/upload/v1695147277/require_2fa_xha6yc.png) - - -Anyone who is currently logged in to Pipedream will be temporarily signed out until they set up 2FA - -If anyone is actively making changes to a workflow, their session may be interrupted. We recommend enabling the 2FA requirement in off hours. - - - -### Configuring Single Sign-On (SSO) - -Workspaces on the Business and Enterprise plans can configure Single Sign-On, so your users can login to Pipedream using your identity provider. - -Pipedream supports SSO with Google, Okta, and any provider that supports the SAML protocol. See the guides below to configure SSO for your identity provider: - -- [Okta](/workspaces/sso/okta/) -- [Google](/workspaces/sso/google/) -- [Other SAML provider](/workspaces/sso/saml/) - -### SCIM - -Pipedream supports provisioning user accounts from your IdP via SCIM. Any workspace on the Business and Enterprise plans can configure Single Sign-On with SCIM. - -### Renaming a workspace - -To rename a workspace, open your [workspace settings](https://pipedream.com/settings/account) and navigate to the **General** tab. - -Click the save button to save the changes. - - -This action is only available to workspace **admins**. - - -### Deleting a workspace - -To delete a workspace, open your workspace settings and navigate to the **Danger Zone**. - -Click the **Delete workspace** button and confirm the action by entering in your workspace name and `delete my workspace` into the text prompt. - - -Deleting a workspace will delete all **sources**, **workflows**, and other resources in your workspace. - -Deleting a workspace is **irreversible** and permanent. - - -## Switching between workspaces - -To switch between workspaces, open the dropdown menu in the top left of the Pipedream dashboard. - -Select which workspace you'd like to start working within, and your Pipedream dashboard context will change to that workspace. diff --git a/docs-v2/pages/workspaces/sso/_meta.json b/docs-v2/pages/workspaces/sso/_meta.json deleted file mode 100644 index 8e5177d772c4d..0000000000000 --- a/docs-v2/pages/workspaces/sso/_meta.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "index": "Overview", - "google": "Google Workspace", - "okta": "Okta", - "saml": "SAML" -} diff --git a/docs-v2/pages/workspaces/sso/index.mdx b/docs-v2/pages/workspaces/sso/index.mdx deleted file mode 100644 index 09e9ca81ee097..0000000000000 --- a/docs-v2/pages/workspaces/sso/index.mdx +++ /dev/null @@ -1,72 +0,0 @@ -import Callout from '@/components/Callout' - -# Single Sign-On Overview - -Pipedream supports Single Sign-On (SSO) with [Okta](./okta), [Google](./google), or [any provider](./saml) that supports SAML or Google OAuth, which allows IT and workspace administrators easier controls to manage access and security. - -Using SSO with your Identity Provider (IdP) centralizes user login management and provides a single point of control for IT teams and employees. - -## Requirements for SSO - -- Your workspace must be on a [Business or Enterprise plan](https://pipedream.com/pricing) -- If using SAML, your Identity Provider must support SAML 2.0 -- Only workspace admins and owners can configure SSO -- Your workspace admin or owner must [verify ownership](#verifying-your-email-domain) of the SSO email domain - - -The below content is for workspace admins and owners. Only workspace admins and owners have access to add verified domains, set up SSO, and configure workspace login methods. - - -## Verifying your Email Domain - -In order to configure SAML SSO for your workspace, you first need to verify ownership of the email domain. If configuring Google OAuth (not SAML), you can skip this section. - -1. Navigate to the [Verified Domains](https://pipedream.com/settings/domains) section of your workspace settings -2. Enter the domain you'd like to use for SSO (for example, `example.com`), then click **Add Domain** -3. You'll see a modal with instructions for adding a `TXT` record in the DNS configuration for your domain -4. DNS changes may take between a few minutes and up to 72 hours to propagate. Once they're live, click the **Verify** button for the domain you've entered. -5. Once Pipedream verifies the `TXT` record, we'll show a green checkmark on the domain - -![Verified Domains](https://res.cloudinary.com/pipedreamin/image/upload/v1699938431/verified-domains_qcjpnb.png) - - -Make sure to verify all your domains. There's no limit on the number of domains you can verify for SSO, so if you use `example.com`, `example.net`, and `foo.example.com`, make sure to verify each one. - - -## Setting up SSO - -Navigate to the [Authentication section](https://pipedream.com/settings/domains) in your workspace settings to get started. - -### SAML SSO - -1. First, make sure you've verified the domain(s) you intend to use for SSO ([see above](#verifying-your-email-domain)) -2. Click the **Enable SSO** toggle and select **SAML** -3. If setting up SAML SSO, you'll need to enter a metadata URL, which contains all the necessary configuration for Pipedream. Refer to the provider-specific docs for the detailed walk-through ([Okta](./okta), [Google Workspace](./google), [any other SAML provider](./saml)). -4. Click **Save** - -### Google OAuth - -1. Click the **Enable SSO** toggle and select **Google** -2. Enter the domain that you use with Google OAuth. For example, `vandalayindustries.com` -3. Click **Save** - -## Restricting Login Methods - -Once you've configured SSO for your workspace, you can restrict the allowed login methods for [non-workspace owners](#workspace-owners-can-always-sign-in-using-any-login-method). - -![Restrict Login Methods](https://res.cloudinary.com/pipedreamin/image/upload/v1699914460/Google_Chrome_-_Settings_-_Authentication_-_Pipedream_2023-11-13_at_2.27.08_PM_x1ahod.png) - - -| Login Method | Description | -| -- | -- | -| **Any login method** | Everyone in the workspace can sign in either using SSO or via the login method they used to create their account (email and password, Google OAuth, GitHub) | -| **SSO only** | Workspace members and admins must [sign in using SSO](https://pipedream.com/auth/sso) | -| **SSO with guests** | When siging in using a verified email domain, members and admins must [sign in using SSO](https://pipedream.com/auth/sso). If signing in with a different domain (`gmail.com` for example), members (guests) can sign in using any login method. | - -### Workspace owners can always sign in using any login method - -In order to ensure you don't get locked out of your Pipedream workspace in the event of an outage with your identity provider, workspace owners can always sign in via the login method they used to create the account (email and password, Google, or GitHub). - -### Login methods are enforced when signing in to pipedream.com - -This means if you are a member of 2 workspaces and one of them allows **any login method** but the other **requires SSO**, you will be required to sign in to Pipedream using SSO every time, independent of the workspace you are trying to access. diff --git a/docs-v2/pages/your-first-workflow/images/console-log-test.png b/docs-v2/pages/your-first-workflow/images/console-log-test.png deleted file mode 100644 index 11bcf82890b64..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/console-log-test.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/create-rb.png b/docs-v2/pages/your-first-workflow/images/create-rb.png deleted file mode 100644 index 92142ebd3dd46..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/create-rb.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/destination-details.png b/docs-v2/pages/your-first-workflow/images/destination-details.png deleted file mode 100644 index 7693cdcaa6f51..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/destination-details.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/empty-title-description.png b/docs-v2/pages/your-first-workflow/images/empty-title-description.png deleted file mode 100644 index 1533298ec52d1..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/empty-title-description.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/event-body.png b/docs-v2/pages/your-first-workflow/images/event-body.png deleted file mode 100644 index 761f2096d1f19..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/event-body.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/event-headers-expanded.png b/docs-v2/pages/your-first-workflow/images/event-headers-expanded.png deleted file mode 100644 index ee60542347f6a..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/event-headers-expanded.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/event-headers.png b/docs-v2/pages/your-first-workflow/images/event-headers.png deleted file mode 100644 index 07861f3f69ad3..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/event-headers.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/event-new-key.png b/docs-v2/pages/your-first-workflow/images/event-new-key.png deleted file mode 100644 index 2a33ca56975da..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/event-new-key.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/example-event.png b/docs-v2/pages/your-first-workflow/images/example-event.png deleted file mode 100644 index 4edf692870109..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/example-event.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/first-pipeline-request.png b/docs-v2/pages/your-first-workflow/images/first-pipeline-request.png deleted file mode 100644 index df8d96d2c32f1..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/first-pipeline-request.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/hurlit.png b/docs-v2/pages/your-first-workflow/images/hurlit.png deleted file mode 100644 index 6c63077848723..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/hurlit.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/inspector-destinations.png b/docs-v2/pages/your-first-workflow/images/inspector-destinations.png deleted file mode 100644 index e665db99ef071..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/inspector-destinations.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/lodash-mean.png b/docs-v2/pages/your-first-workflow/images/lodash-mean.png deleted file mode 100644 index 175e7b4abd44e..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/lodash-mean.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/new-code-cell.png b/docs-v2/pages/your-first-workflow/images/new-code-cell.png deleted file mode 100644 index 3e06d409c169a..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/new-code-cell.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/new-destination.png b/docs-v2/pages/your-first-workflow/images/new-destination.png deleted file mode 100644 index f9fea4965b454..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/new-destination.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/new-pipeline-url.png b/docs-v2/pages/your-first-workflow/images/new-pipeline-url.png deleted file mode 100644 index a1c378fdd4b0e..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/new-pipeline-url.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/new-pipeline.png b/docs-v2/pages/your-first-workflow/images/new-pipeline.png deleted file mode 100644 index 0bf9a1387757f..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/new-pipeline.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/new-step.png b/docs-v2/pages/your-first-workflow/images/new-step.png deleted file mode 100644 index 8147b2d483333..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/new-step.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/new-title-description.png b/docs-v2/pages/your-first-workflow/images/new-title-description.png deleted file mode 100644 index 3f306aa2c1597..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/new-title-description.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/new-url.png b/docs-v2/pages/your-first-workflow/images/new-url.png deleted file mode 100644 index 0cdfda154a861..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/new-url.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/rb-url.png b/docs-v2/pages/your-first-workflow/images/rb-url.png deleted file mode 100644 index dfe41ed6b3387..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/rb-url.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/response-from-destination.png b/docs-v2/pages/your-first-workflow/images/response-from-destination.png deleted file mode 100644 index 03c688de50e51..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/response-from-destination.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/save.png b/docs-v2/pages/your-first-workflow/images/save.png deleted file mode 100644 index 7aaef25105f07..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/save.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/sent-to-destination.png b/docs-v2/pages/your-first-workflow/images/sent-to-destination.png deleted file mode 100644 index bf7179e9b9e7a..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/sent-to-destination.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/source.png b/docs-v2/pages/your-first-workflow/images/source.png deleted file mode 100644 index aac10306ed081..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/source.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/sql-destination-details.png b/docs-v2/pages/your-first-workflow/images/sql-destination-details.png deleted file mode 100644 index 5708541b805da..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/sql-destination-details.png and /dev/null differ diff --git a/docs-v2/pages/your-first-workflow/images/sql-tab.png b/docs-v2/pages/your-first-workflow/images/sql-tab.png deleted file mode 100644 index 33ddd6dba4ab5..0000000000000 Binary files a/docs-v2/pages/your-first-workflow/images/sql-tab.png and /dev/null differ diff --git a/docs-v2/pnpm-lock.yaml b/docs-v2/pnpm-lock.yaml deleted file mode 100644 index afa36b4e63104..0000000000000 --- a/docs-v2/pnpm-lock.yaml +++ /dev/null @@ -1,2172 +0,0 @@ -lockfileVersion: 5.3 - -specifiers: - '@types/node': 18.11.10 - next: ^13.0.6 - nextra: latest - nextra-theme-docs: latest - react: ^18.2.0 - react-dom: ^18.2.0 - typescript: ^4.9.3 - -dependencies: - next: 13.0.6_react-dom@18.2.0+react@18.2.0 - nextra: 2.2.14_f26ff3bd08f1cd28b0f73422c76f5ffd - nextra-theme-docs: 2.2.14_d8d66b9d2170cddb63c39dddec8541b9 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - -devDependencies: - '@types/node': 18.11.10 - typescript: 4.9.3 - -packages: - - /@babel/runtime/7.20.6: - resolution: {integrity: sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==} - engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.13.11 - dev: false - - /@headlessui/react/1.7.10_react-dom@18.2.0+react@18.2.0: - resolution: {integrity: sha512-1m66h/5eayTEZVT2PI13/2PG3EVC7a9XalmUtVSC8X76pcyKYMuyX1XAL2RUtCr8WhoMa/KrDEyoeU5v+kSQOw==} - engines: {node: '>=10'} - peerDependencies: - react: ^16 || ^17 || ^18 - react-dom: ^16 || ^17 || ^18 - dependencies: - client-only: 0.0.1 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - dev: false - - /@mdx-js/mdx/2.2.1: - resolution: {integrity: sha512-hZ3ex7exYLJn6FfReq8yTvA6TE53uW9UHJQM9IlSauOuS55J9y8RtA7W+dzp6Yrzr00/U1sd7q+Wf61q6SfiTQ==} - dependencies: - '@types/estree-jsx': 1.0.0 - '@types/mdx': 2.0.3 - estree-util-build-jsx: 2.2.0 - estree-util-is-identifier-name: 2.0.1 - estree-util-to-js: 1.1.0 - estree-walker: 3.0.1 - hast-util-to-estree: 2.1.0 - markdown-extensions: 1.1.1 - periscopic: 3.0.4 - remark-mdx: 2.1.5 - remark-parse: 10.0.1 - remark-rehype: 10.1.0 - unified: 10.1.2 - unist-util-position-from-estree: 1.1.1 - unist-util-stringify-position: 3.0.2 - unist-util-visit: 4.1.1 - vfile: 5.3.6 - transitivePeerDependencies: - - supports-color - dev: false - - /@mdx-js/react/2.2.1_react@18.2.0: - resolution: {integrity: sha512-YdXcMcEnqZhzql98RNrqYo9cEhTTesBiCclEtoiQUbJwx87q9453GTapYU6kJ8ZZ2ek1Vp25SiAXEFy5O/eAPw==} - peerDependencies: - react: '>=16' - dependencies: - '@types/mdx': 2.0.3 - '@types/react': 18.0.25 - react: 18.2.0 - dev: false - - /@napi-rs/simple-git-android-arm-eabi/0.1.8: - resolution: {integrity: sha512-JJCejHBB1G6O8nxjQLT4quWCcvLpC3oRdJJ9G3MFYSCoYS8i1bWCWeU+K7Br+xT+D6s1t9q8kNJAwJv9Ygpi0g==} - engines: {node: '>= 10'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-android-arm64/0.1.8: - resolution: {integrity: sha512-mraHzwWBw3tdRetNOS5KnFSjvdAbNBnjFLA8I4PwTCPJj3Q4txrigcPp2d59cJ0TC51xpnPXnZjYdNwwSI9g6g==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-darwin-arm64/0.1.8: - resolution: {integrity: sha512-ufy/36eI/j4UskEuvqSH7uXtp3oXeLDmjQCfKJz3u5Vx98KmOMKrqAm2H81AB2WOtCo5mqS6PbBeUXR8BJX8lQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-darwin-x64/0.1.8: - resolution: {integrity: sha512-Vb21U+v3tPJNl+8JtIHHT8HGe6WZ8o1Tq3f6p+Jx9Cz71zEbcIiB9FCEMY1knS/jwQEOuhhlI9Qk7d4HY+rprA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-linux-arm-gnueabihf/0.1.8: - resolution: {integrity: sha512-6BPTJ7CzpSm2t54mRLVaUr3S7ORJfVJoCk2rQ8v8oDg0XAMKvmQQxOsAgqKBo9gYNHJnqrOx3AEuEgvB586BuQ==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-linux-arm64-gnu/0.1.8: - resolution: {integrity: sha512-qfESqUCAA/XoQpRXHptSQ8gIFnETCQt1zY9VOkplx6tgYk9PCeaX4B1Xuzrh3eZamSCMJFn+1YB9Ut8NwyGgAA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-linux-arm64-musl/0.1.8: - resolution: {integrity: sha512-G80BQPpaRmQpn8dJGHp4I2/YVhWDUNJwcCrJAtAdbKFDCMyCHJBln2ERL/+IEUlIAT05zK/c1Z5WEprvXEdXow==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-linux-x64-gnu/0.1.8: - resolution: {integrity: sha512-NI6o1sZYEf6vPtNWJAm9w8BxJt+LlSFW0liSjYe3lc3e4dhMfV240f0ALeqlwdIldRPaDFwZSJX5/QbS7nMzhw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-linux-x64-musl/0.1.8: - resolution: {integrity: sha512-wljGAEOW41er45VTiU8kXJmO480pQKzsgRCvPlJJSCaEVBbmo6XXbFIXnZy1a2J3Zyy2IOsRB4PVkUZaNuPkZQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-win32-arm64-msvc/0.1.8: - resolution: {integrity: sha512-QuV4QILyKPfbWHoQKrhXqjiCClx0SxbCTVogkR89BwivekqJMd9UlMxZdoCmwLWutRx4z9KmzQqokvYI5QeepA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git-win32-x64-msvc/0.1.8: - resolution: {integrity: sha512-UzNS4JtjhZhZ5hRLq7BIUq+4JOwt1ThIKv11CsF1ag2l99f0123XvfEpjczKTaa94nHtjXYc2Mv9TjccBqYOew==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/simple-git/0.1.8: - resolution: {integrity: sha512-BvOMdkkofTz6lEE35itJ/laUokPhr/5ToMGlOH25YnhLD2yN1KpRAT4blW9tT8281/1aZjW3xyi73bs//IrDKA==} - engines: {node: '>= 10'} - optionalDependencies: - '@napi-rs/simple-git-android-arm-eabi': 0.1.8 - '@napi-rs/simple-git-android-arm64': 0.1.8 - '@napi-rs/simple-git-darwin-arm64': 0.1.8 - '@napi-rs/simple-git-darwin-x64': 0.1.8 - '@napi-rs/simple-git-linux-arm-gnueabihf': 0.1.8 - '@napi-rs/simple-git-linux-arm64-gnu': 0.1.8 - '@napi-rs/simple-git-linux-arm64-musl': 0.1.8 - '@napi-rs/simple-git-linux-x64-gnu': 0.1.8 - '@napi-rs/simple-git-linux-x64-musl': 0.1.8 - '@napi-rs/simple-git-win32-arm64-msvc': 0.1.8 - '@napi-rs/simple-git-win32-x64-msvc': 0.1.8 - dev: false - - /@next/env/13.0.6: - resolution: {integrity: sha512-yceT6DCHKqPRS1cAm8DHvDvK74DLIkDQdm5iV+GnIts8h0QbdHvkUIkdOvQoOODgpr6018skbmSQp12z5OWIQQ==} - dev: false - - /@next/swc-android-arm-eabi/13.0.6: - resolution: {integrity: sha512-FGFSj3v2Bluw8fD/X+1eXIEB0PhoJE0zfutsAauRhmNpjjZshLDgoXMWm1jTRL/04K/o9gwwO2+A8+sPVCH1uw==} - engines: {node: '>= 10'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: false - optional: true - - /@next/swc-android-arm64/13.0.6: - resolution: {integrity: sha512-7MgbtU7kimxuovVsd7jSJWMkIHBDBUsNLmmlkrBRHTvgzx5nDBXogP0hzZm7EImdOPwVMPpUHRQMBP9mbsiJYQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: false - optional: true - - /@next/swc-darwin-arm64/13.0.6: - resolution: {integrity: sha512-AUVEpVTxbP/fxdFsjVI9d5a0CFn6NVV7A/RXOb0Y+pXKIIZ1V5rFjPwpYfIfyOo2lrqgehMNQcyMRoTrhq04xg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@next/swc-darwin-x64/13.0.6: - resolution: {integrity: sha512-SasCDJlshglsPnbzhWaIF6VEGkQy2NECcAOxPwaPr0cwbbt4aUlZ7QmskNzgolr5eAjFS/xTr7CEeKJtZpAAtQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@next/swc-freebsd-x64/13.0.6: - resolution: {integrity: sha512-6Lbxd9gAdXneTkwHyYW/qtX1Tdw7ND9UbiGsGz/SP43ZInNWnW6q0au4hEVPZ9bOWWRKzcVoeTBdoMpQk9Hx9w==} - engines: {node: '>= 10'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-arm-gnueabihf/13.0.6: - resolution: {integrity: sha512-wNdi5A519e1P+ozEuYOhWPzzE6m1y7mkO6NFwn6watUwO0X9nZs7fT9THmnekvmFQpaZ6U+xf2MQ9poQoCh6jQ==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-arm64-gnu/13.0.6: - resolution: {integrity: sha512-e8KTRnleQY1KLk5PwGV5hrmvKksCc74QRpHl5ffWnEEAtL2FE0ave5aIkXqErsPdXkiKuA/owp3LjQrP+/AH7Q==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-arm64-musl/13.0.6: - resolution: {integrity: sha512-/7RF03C3mhjYpHN+pqOolgME3guiHU5T3TsejuyteqyEyzdEyLHod+jcYH6ft7UZ71a6TdOewvmbLOtzHW2O8A==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-x64-gnu/13.0.6: - resolution: {integrity: sha512-kxyEXnYHpOEkFnmrlwB1QlzJtjC6sAJytKcceIyFUHbCaD3W/Qb5tnclcnHKTaFccizZRePXvV25Ok/eUSpKTw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-x64-musl/13.0.6: - resolution: {integrity: sha512-N0c6gubS3WW1oYYgo02xzZnNatfVQP/CiJq2ax+DJ55ePV62IACbRCU99TZNXXg+Kos6vNW4k+/qgvkvpGDeyA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-win32-arm64-msvc/13.0.6: - resolution: {integrity: sha512-QjeMB2EBqBFPb/ac0CYr7GytbhUkrG4EwFWbcE0vsRp4H8grt25kYpFQckL4Jak3SUrp7vKfDwZ/SwO7QdO8vw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@next/swc-win32-ia32-msvc/13.0.6: - resolution: {integrity: sha512-EQzXtdqRTcmhT/tCq81rIwE36Y3fNHPInaCuJzM/kftdXfa0F+64y7FAoMO13npX8EG1+SamXgp/emSusKrCXg==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@next/swc-win32-x64-msvc/13.0.6: - resolution: {integrity: sha512-pSkqZ//UP/f2sS9T7IvHLfEWDPTX0vRyXJnAUNisKvO3eF3e1xdhDX7dix/X3Z3lnN4UjSwOzclAI87JFbOwmQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@popperjs/core/2.11.6: - resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==} - dev: false - - /@swc/helpers/0.4.14: - resolution: {integrity: sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==} - dependencies: - tslib: 2.4.1 - dev: false - - /@types/acorn/4.0.6: - resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} - dependencies: - '@types/estree': 1.0.0 - dev: false - - /@types/debug/4.1.7: - resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} - dependencies: - '@types/ms': 0.7.31 - dev: false - - /@types/estree-jsx/1.0.0: - resolution: {integrity: sha512-3qvGd0z8F2ENTGr/GG1yViqfiKmRfrXVx5sJyHGFu3z7m5g5utCQtGp/g29JnjflhtQJBv1WDQukHiT58xPcYQ==} - dependencies: - '@types/estree': 1.0.0 - dev: false - - /@types/estree/1.0.0: - resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} - dev: false - - /@types/hast/2.3.4: - resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==} - dependencies: - '@types/unist': 2.0.6 - dev: false - - /@types/js-yaml/4.0.5: - resolution: {integrity: sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==} - dev: false - - /@types/katex/0.11.1: - resolution: {integrity: sha512-DUlIj2nk0YnJdlWgsFuVKcX27MLW0KbKmGVoUHmFr+74FYYNUDAaj9ZqTADvsbE8rfxuVmSFc7KczYn5Y09ozg==} - dev: false - - /@types/mdast/3.0.10: - resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==} - dependencies: - '@types/unist': 2.0.6 - dev: false - - /@types/mdx/2.0.3: - resolution: {integrity: sha512-IgHxcT3RC8LzFLhKwP3gbMPeaK7BM9eBH46OdapPA7yvuIUJ8H6zHZV53J8hGZcTSnt95jANt+rTBNUUc22ACQ==} - dev: false - - /@types/ms/0.7.31: - resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} - dev: false - - /@types/node/18.11.10: - resolution: {integrity: sha512-juG3RWMBOqcOuXC643OAdSA525V44cVgGV6dUDuiFtss+8Fk5x1hI93Rsld43VeJVIeqlP9I7Fn9/qaVqoEAuQ==} - dev: true - - /@types/prop-types/15.7.5: - resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} - dev: false - - /@types/react/18.0.25: - resolution: {integrity: sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==} - dependencies: - '@types/prop-types': 15.7.5 - '@types/scheduler': 0.16.2 - csstype: 3.1.1 - dev: false - - /@types/scheduler/0.16.2: - resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} - dev: false - - /@types/unist/2.0.6: - resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} - dev: false - - /acorn-jsx/5.3.2_acorn@8.8.1: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - acorn: 8.8.1 - dev: false - - /acorn/8.8.1: - resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: false - - /ansi-styles/3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - dependencies: - color-convert: 1.9.3 - dev: false - - /arch/2.2.0: - resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} - dev: false - - /arg/1.0.0: - resolution: {integrity: sha512-Wk7TEzl1KqvTGs/uyhmHO/3XLd3t1UeU4IstvPXVzGPM522cTjqjNZ99esCkcL52sjqjo8e8CTBcWhkxvGzoAw==} - dev: false - - /argparse/1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - dependencies: - sprintf-js: 1.0.3 - dev: false - - /argparse/2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: false - - /astring/1.8.3: - resolution: {integrity: sha512-sRpyiNrx2dEYIMmUXprS8nlpRg2Drs8m9ElX9vVEXaCB4XEAJhKfs7IcX0IwShjuOAjLR6wzIrgoptz1n19i1A==} - hasBin: true - dev: false - - /bail/2.0.2: - resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - dev: false - - /caniuse-lite/1.0.30001435: - resolution: {integrity: sha512-kdCkUTjR+v4YAJelyiDTqiu82BDr4W4CP5sgTA0ZBmqn30XfS2ZghPLMowik9TPhS+psWJiUNxsqLyurDbmutA==} - dev: false - - /ccount/2.0.1: - resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - dev: false - - /chalk/2.3.0: - resolution: {integrity: sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==} - engines: {node: '>=4'} - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 4.5.0 - dev: false - - /character-entities-html4/2.1.0: - resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} - dev: false - - /character-entities-legacy/3.0.0: - resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - dev: false - - /character-entities/2.0.2: - resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - dev: false - - /character-reference-invalid/2.0.1: - resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} - dev: false - - /client-only/0.0.1: - resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - dev: false - - /clipboardy/1.2.2: - resolution: {integrity: sha512-16KrBOV7bHmHdxcQiCvfUFYVFyEah4FI8vYT1Fr7CGSA4G+xBWMEfUEQJS1hxeHGtI9ju1Bzs9uXSbj5HZKArw==} - engines: {node: '>=4'} - dependencies: - arch: 2.2.0 - execa: 0.8.0 - dev: false - - /clsx/1.2.1: - resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} - engines: {node: '>=6'} - dev: false - - /color-convert/1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - dependencies: - color-name: 1.1.3 - dev: false - - /color-name/1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: false - - /comma-separated-tokens/2.0.3: - resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - dev: false - - /commander/8.3.0: - resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} - engines: {node: '>= 12'} - dev: false - - /compute-scroll-into-view/2.0.4: - resolution: {integrity: sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g==} - dev: false - - /cross-spawn/5.1.0: - resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} - dependencies: - lru-cache: 4.1.5 - shebang-command: 1.2.0 - which: 1.3.1 - dev: false - - /csstype/3.1.1: - resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} - dev: false - - /debug/4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - dev: false - - /decode-named-character-reference/1.0.2: - resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} - dependencies: - character-entities: 2.0.2 - dev: false - - /dequal/2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - dev: false - - /diff/5.1.0: - resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} - engines: {node: '>=0.3.1'} - dev: false - - /escape-string-regexp/1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - dev: false - - /escape-string-regexp/5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - dev: false - - /esprima/4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - dev: false - - /estree-util-attach-comments/2.1.0: - resolution: {integrity: sha512-rJz6I4L0GaXYtHpoMScgDIwM0/Vwbu5shbMeER596rB2D1EWF6+Gj0e0UKzJPZrpoOc87+Q2kgVFHfjAymIqmw==} - dependencies: - '@types/estree': 1.0.0 - dev: false - - /estree-util-build-jsx/2.2.0: - resolution: {integrity: sha512-apsfRxF9uLrqosApvHVtYZjISPvTJ+lBiIydpC+9wE6cF6ssbhnjyQLqaIjgzGxvC2Hbmec1M7g91PoBayYoQQ==} - dependencies: - '@types/estree-jsx': 1.0.0 - estree-util-is-identifier-name: 2.0.1 - estree-walker: 3.0.1 - dev: false - - /estree-util-is-identifier-name/2.0.1: - resolution: {integrity: sha512-rxZj1GkQhY4x1j/CSnybK9cGuMFQYFPLq0iNyopqf14aOVLFtMv7Esika+ObJWPWiOHuMOAHz3YkWoLYYRnzWQ==} - dev: false - - /estree-util-to-js/1.1.0: - resolution: {integrity: sha512-490lbfCcpLk+ofK6HCgqDfYs4KAfq6QVvDw3+Bm1YoKRgiOjKiKYGAVQE1uwh7zVxBgWhqp4FDtp5SqunpUk1A==} - dependencies: - '@types/estree-jsx': 1.0.0 - astring: 1.8.3 - source-map: 0.7.4 - dev: false - - /estree-util-value-to-estree/1.3.0: - resolution: {integrity: sha512-Y+ughcF9jSUJvncXwqRageavjrNPAI+1M/L3BI3PyLp1nmgYTGUXU6t5z1Y7OWuThoDdhPME07bQU+d5LxdJqw==} - engines: {node: '>=12.0.0'} - dependencies: - is-plain-obj: 3.0.0 - dev: false - - /estree-util-visit/1.2.0: - resolution: {integrity: sha512-wdsoqhWueuJKsh5hqLw3j8lwFqNStm92VcwtAOAny8g/KS/l5Y8RISjR4k5W6skCj3Nirag/WUCMS0Nfy3sgsg==} - dependencies: - '@types/estree-jsx': 1.0.0 - '@types/unist': 2.0.6 - dev: false - - /estree-walker/3.0.1: - resolution: {integrity: sha512-woY0RUD87WzMBUiZLx8NsYr23N5BKsOMZHhu2hoNRVh6NXGfoiT1KOL8G3UHlJAnEDGmfa5ubNA/AacfG+Kb0g==} - dev: false - - /execa/0.8.0: - resolution: {integrity: sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==} - engines: {node: '>=4'} - dependencies: - cross-spawn: 5.1.0 - get-stream: 3.0.0 - is-stream: 1.1.0 - npm-run-path: 2.0.2 - p-finally: 1.0.0 - signal-exit: 3.0.7 - strip-eof: 1.0.0 - dev: false - - /extend-shallow/2.0.1: - resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} - engines: {node: '>=0.10.0'} - dependencies: - is-extendable: 0.1.1 - dev: false - - /extend/3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - dev: false - - /flexsearch/0.7.31: - resolution: {integrity: sha512-XGozTsMPYkm+6b5QL3Z9wQcJjNYxp0CYn3U1gO7dwD6PAqU1SVWZxI9CCg3z+ml3YfqdPnrBehaBrnH2AGKbNA==} - dev: false - - /focus-visible/5.2.0: - resolution: {integrity: sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==} - dev: false - - /get-stream/3.0.0: - resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==} - engines: {node: '>=4'} - dev: false - - /git-up/7.0.0: - resolution: {integrity: sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==} - dependencies: - is-ssh: 1.4.0 - parse-url: 8.1.0 - dev: false - - /git-url-parse/13.1.0: - resolution: {integrity: sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA==} - dependencies: - git-up: 7.0.0 - dev: false - - /github-slugger/2.0.0: - resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} - dev: false - - /graceful-fs/4.2.10: - resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} - dev: false - - /gray-matter/4.0.3: - resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} - engines: {node: '>=6.0'} - dependencies: - js-yaml: 3.14.1 - kind-of: 6.0.3 - section-matter: 1.0.0 - strip-bom-string: 1.0.0 - dev: false - - /has-flag/2.0.0: - resolution: {integrity: sha512-P+1n3MnwjR/Epg9BBo1KT8qbye2g2Ou4sFumihwt6I4tsUX7jnLcX4BTOSKg/B1ZrIYMN9FcEnG4x5a7NB8Eng==} - engines: {node: '>=0.10.0'} - dev: false - - /hash-obj/4.0.0: - resolution: {integrity: sha512-FwO1BUVWkyHasWDW4S8o0ssQXjvyghLV2rfVhnN36b2bbcj45eGiuzdn9XOvOpjV3TKQD7Gm2BWNXdE9V4KKYg==} - engines: {node: '>=12'} - dependencies: - is-obj: 3.0.0 - sort-keys: 5.0.0 - type-fest: 1.4.0 - dev: false - - /hast-util-from-parse5/7.1.1: - resolution: {integrity: sha512-R6PoNcUs89ZxLJmMWsVbwSWuz95/9OriyQZ3e2ybwqGsRXzhA6gv49rgGmQvLbZuSNDv9fCg7vV7gXUsvtUFaA==} - dependencies: - '@types/hast': 2.3.4 - '@types/unist': 2.0.6 - hastscript: 7.2.0 - property-information: 6.2.0 - vfile: 5.3.6 - vfile-location: 4.0.1 - web-namespaces: 2.0.1 - dev: false - - /hast-util-is-element/2.1.3: - resolution: {integrity: sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA==} - dependencies: - '@types/hast': 2.3.4 - '@types/unist': 2.0.6 - dev: false - - /hast-util-parse-selector/3.1.1: - resolution: {integrity: sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==} - dependencies: - '@types/hast': 2.3.4 - dev: false - - /hast-util-to-estree/2.1.0: - resolution: {integrity: sha512-Vwch1etMRmm89xGgz+voWXvVHba2iiMdGMKmaMfYt35rbVtFDq8JNwwAIvi8zHMkO6Gvqo9oTMwJTmzVRfXh4g==} - dependencies: - '@types/estree': 1.0.0 - '@types/estree-jsx': 1.0.0 - '@types/hast': 2.3.4 - '@types/unist': 2.0.6 - comma-separated-tokens: 2.0.3 - estree-util-attach-comments: 2.1.0 - estree-util-is-identifier-name: 2.0.1 - hast-util-whitespace: 2.0.0 - mdast-util-mdx-expression: 1.3.1 - mdast-util-mdxjs-esm: 1.3.0 - property-information: 6.2.0 - space-separated-tokens: 2.0.2 - style-to-object: 0.3.0 - unist-util-position: 4.0.3 - zwitch: 2.0.4 - transitivePeerDependencies: - - supports-color - dev: false - - /hast-util-to-text/3.1.2: - resolution: {integrity: sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw==} - dependencies: - '@types/hast': 2.3.4 - '@types/unist': 2.0.6 - hast-util-is-element: 2.1.3 - unist-util-find-after: 4.0.1 - dev: false - - /hast-util-whitespace/2.0.0: - resolution: {integrity: sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg==} - dev: false - - /hastscript/7.2.0: - resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==} - dependencies: - '@types/hast': 2.3.4 - comma-separated-tokens: 2.0.3 - hast-util-parse-selector: 3.1.1 - property-information: 6.2.0 - space-separated-tokens: 2.0.2 - dev: false - - /inline-style-parser/0.1.1: - resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} - dev: false - - /intersection-observer/0.12.2: - resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} - dev: false - - /is-alphabetical/2.0.1: - resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} - dev: false - - /is-alphanumerical/2.0.1: - resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} - dependencies: - is-alphabetical: 2.0.1 - is-decimal: 2.0.1 - dev: false - - /is-buffer/2.0.5: - resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} - engines: {node: '>=4'} - dev: false - - /is-decimal/2.0.1: - resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} - dev: false - - /is-extendable/0.1.1: - resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} - engines: {node: '>=0.10.0'} - dev: false - - /is-hexadecimal/2.0.1: - resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} - dev: false - - /is-obj/3.0.0: - resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} - engines: {node: '>=12'} - dev: false - - /is-plain-obj/3.0.0: - resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} - engines: {node: '>=10'} - dev: false - - /is-plain-obj/4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - dev: false - - /is-reference/3.0.0: - resolution: {integrity: sha512-Eo1W3wUoHWoCoVM4GVl/a+K0IgiqE5aIo4kJABFyMum1ZORlPkC+UC357sSQUL5w5QCE5kCC9upl75b7+7CY/Q==} - dependencies: - '@types/estree': 1.0.0 - dev: false - - /is-ssh/1.4.0: - resolution: {integrity: sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==} - dependencies: - protocols: 2.0.1 - dev: false - - /is-stream/1.1.0: - resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} - engines: {node: '>=0.10.0'} - dev: false - - /isexe/2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: false - - /js-tokens/4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: false - - /js-yaml/3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - dev: false - - /js-yaml/4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - dependencies: - argparse: 2.0.1 - dev: false - - /jsonc-parser/3.2.0: - resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} - dev: false - - /katex/0.13.24: - resolution: {integrity: sha512-jZxYuKCma3VS5UuxOx/rFV1QyGSl3Uy/i0kTJF3HgQ5xMinCQVF8Zd4bMY/9aI9b9A2pjIBOsjSSm68ykTAr8w==} - hasBin: true - dependencies: - commander: 8.3.0 - dev: false - - /katex/0.15.6: - resolution: {integrity: sha512-UpzJy4yrnqnhXvRPhjEuLA4lcPn6eRngixW7Q3TJErjg3Aw2PuLFBzTkdUb89UtumxjhHTqL3a5GDGETMSwgJA==} - hasBin: true - dependencies: - commander: 8.3.0 - dev: false - - /katex/0.16.4: - resolution: {integrity: sha512-WudRKUj8yyBeVDI4aYMNxhx5Vhh2PjpzQw1GRu/LVGqL4m1AxwD1GcUp0IMbdJaf5zsjtj8ghP0DOQRYhroNkw==} - hasBin: true - dependencies: - commander: 8.3.0 - dev: false - - /kind-of/6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - dev: false - - /kleur/4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - dev: false - - /lodash.get/4.4.2: - resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} - dev: false - - /longest-streak/3.1.0: - resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - dev: false - - /loose-envify/1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - dependencies: - js-tokens: 4.0.0 - dev: false - - /lru-cache/4.1.5: - resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} - dependencies: - pseudomap: 1.0.2 - yallist: 2.1.2 - dev: false - - /markdown-extensions/1.1.1: - resolution: {integrity: sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==} - engines: {node: '>=0.10.0'} - dev: false - - /markdown-table/3.0.3: - resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} - dev: false - - /match-sorter/6.3.1: - resolution: {integrity: sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==} - dependencies: - '@babel/runtime': 7.20.6 - remove-accents: 0.4.2 - dev: false - - /mdast-util-definitions/5.1.1: - resolution: {integrity: sha512-rQ+Gv7mHttxHOBx2dkF4HWTg+EE+UR78ptQWDylzPKaQuVGdG4HIoY3SrS/pCp80nZ04greFvXbVFHT+uf0JVQ==} - dependencies: - '@types/mdast': 3.0.10 - '@types/unist': 2.0.6 - unist-util-visit: 4.1.1 - dev: false - - /mdast-util-find-and-replace/2.2.1: - resolution: {integrity: sha512-SobxkQXFAdd4b5WmEakmkVoh18icjQRxGy5OWTCzgsLRm1Fu/KCtwD1HIQSsmq5ZRjVH0Ehwg6/Fn3xIUk+nKw==} - dependencies: - escape-string-regexp: 5.0.0 - unist-util-is: 5.1.1 - unist-util-visit-parents: 5.1.1 - dev: false - - /mdast-util-from-markdown/1.2.0: - resolution: {integrity: sha512-iZJyyvKD1+K7QX1b5jXdE7Sc5dtoTry1vzV28UZZe8Z1xVnB/czKntJ7ZAkG0tANqRnBF6p3p7GpU1y19DTf2Q==} - dependencies: - '@types/mdast': 3.0.10 - '@types/unist': 2.0.6 - decode-named-character-reference: 1.0.2 - mdast-util-to-string: 3.1.0 - micromark: 3.1.0 - micromark-util-decode-numeric-character-reference: 1.0.0 - micromark-util-decode-string: 1.0.2 - micromark-util-normalize-identifier: 1.0.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - unist-util-stringify-position: 3.0.2 - uvu: 0.5.6 - transitivePeerDependencies: - - supports-color - dev: false - - /mdast-util-gfm-autolink-literal/1.0.2: - resolution: {integrity: sha512-FzopkOd4xTTBeGXhXSBU0OCDDh5lUj2rd+HQqG92Ld+jL4lpUfgX2AT2OHAVP9aEeDKp7G92fuooSZcYJA3cRg==} - dependencies: - '@types/mdast': 3.0.10 - ccount: 2.0.1 - mdast-util-find-and-replace: 2.2.1 - micromark-util-character: 1.1.0 - dev: false - - /mdast-util-gfm-footnote/1.0.1: - resolution: {integrity: sha512-p+PrYlkw9DeCRkTVw1duWqPRHX6Ywh2BNKJQcZbCwAuP/59B0Lk9kakuAd7KbQprVO4GzdW8eS5++A9PUSqIyw==} - dependencies: - '@types/mdast': 3.0.10 - mdast-util-to-markdown: 1.3.0 - micromark-util-normalize-identifier: 1.0.0 - dev: false - - /mdast-util-gfm-strikethrough/1.0.2: - resolution: {integrity: sha512-T/4DVHXcujH6jx1yqpcAYYwd+z5lAYMw4Ls6yhTfbMMtCt0PHY4gEfhW9+lKsLBtyhUGKRIzcUA2FATVqnvPDA==} - dependencies: - '@types/mdast': 3.0.10 - mdast-util-to-markdown: 1.3.0 - dev: false - - /mdast-util-gfm-table/1.0.6: - resolution: {integrity: sha512-uHR+fqFq3IvB3Rd4+kzXW8dmpxUhvgCQZep6KdjsLK4O6meK5dYZEayLtIxNus1XO3gfjfcIFe8a7L0HZRGgag==} - dependencies: - '@types/mdast': 3.0.10 - markdown-table: 3.0.3 - mdast-util-from-markdown: 1.2.0 - mdast-util-to-markdown: 1.3.0 - transitivePeerDependencies: - - supports-color - dev: false - - /mdast-util-gfm-task-list-item/1.0.1: - resolution: {integrity: sha512-KZ4KLmPdABXOsfnM6JHUIjxEvcx2ulk656Z/4Balw071/5qgnhz+H1uGtf2zIGnrnvDC8xR4Fj9uKbjAFGNIeA==} - dependencies: - '@types/mdast': 3.0.10 - mdast-util-to-markdown: 1.3.0 - dev: false - - /mdast-util-gfm/2.0.1: - resolution: {integrity: sha512-42yHBbfWIFisaAfV1eixlabbsa6q7vHeSPY+cg+BBjX51M8xhgMacqH9g6TftB/9+YkcI0ooV4ncfrJslzm/RQ==} - dependencies: - mdast-util-from-markdown: 1.2.0 - mdast-util-gfm-autolink-literal: 1.0.2 - mdast-util-gfm-footnote: 1.0.1 - mdast-util-gfm-strikethrough: 1.0.2 - mdast-util-gfm-table: 1.0.6 - mdast-util-gfm-task-list-item: 1.0.1 - mdast-util-to-markdown: 1.3.0 - transitivePeerDependencies: - - supports-color - dev: false - - /mdast-util-math/2.0.2: - resolution: {integrity: sha512-8gmkKVp9v6+Tgjtq6SYx9kGPpTf6FVYRa53/DLh479aldR9AyP48qeVOgNZ5X7QUK7nOy4yw7vg6mbiGcs9jWQ==} - dependencies: - '@types/mdast': 3.0.10 - longest-streak: 3.1.0 - mdast-util-to-markdown: 1.3.0 - dev: false - - /mdast-util-mdx-expression/1.3.1: - resolution: {integrity: sha512-TTb6cKyTA1RD+1su1iStZ5PAv3rFfOUKcoU5EstUpv/IZo63uDX03R8+jXjMEhcobXnNOiG6/ccekvVl4eV1zQ==} - dependencies: - '@types/estree-jsx': 1.0.0 - '@types/hast': 2.3.4 - '@types/mdast': 3.0.10 - mdast-util-from-markdown: 1.2.0 - mdast-util-to-markdown: 1.3.0 - transitivePeerDependencies: - - supports-color - dev: false - - /mdast-util-mdx-jsx/2.1.0: - resolution: {integrity: sha512-KzgzfWMhdteDkrY4mQtyvTU5bc/W4ppxhe9SzelO6QUUiwLAM+Et2Dnjjprik74a336kHdo0zKm7Tp+n6FFeRg==} - dependencies: - '@types/estree-jsx': 1.0.0 - '@types/hast': 2.3.4 - '@types/mdast': 3.0.10 - ccount: 2.0.1 - mdast-util-to-markdown: 1.3.0 - parse-entities: 4.0.0 - stringify-entities: 4.0.3 - unist-util-remove-position: 4.0.1 - unist-util-stringify-position: 3.0.2 - vfile-message: 3.1.3 - dev: false - - /mdast-util-mdx/2.0.0: - resolution: {integrity: sha512-M09lW0CcBT1VrJUaF/PYxemxxHa7SLDHdSn94Q9FhxjCQfuW7nMAWKWimTmA3OyDMSTH981NN1csW1X+HPSluw==} - dependencies: - mdast-util-mdx-expression: 1.3.1 - mdast-util-mdx-jsx: 2.1.0 - mdast-util-mdxjs-esm: 1.3.0 - transitivePeerDependencies: - - supports-color - dev: false - - /mdast-util-mdxjs-esm/1.3.0: - resolution: {integrity: sha512-7N5ihsOkAEGjFotIX9p/YPdl4TqUoMxL4ajNz7PbT89BqsdWJuBC9rvgt6wpbwTZqWWR0jKWqQbwsOWDBUZv4g==} - dependencies: - '@types/estree-jsx': 1.0.0 - '@types/hast': 2.3.4 - '@types/mdast': 3.0.10 - mdast-util-from-markdown: 1.2.0 - mdast-util-to-markdown: 1.3.0 - transitivePeerDependencies: - - supports-color - dev: false - - /mdast-util-to-hast/12.2.4: - resolution: {integrity: sha512-a21xoxSef1l8VhHxS1Dnyioz6grrJkoaCUgGzMD/7dWHvboYX3VW53esRUfB5tgTyz4Yos1n25SPcj35dJqmAg==} - dependencies: - '@types/hast': 2.3.4 - '@types/mdast': 3.0.10 - mdast-util-definitions: 5.1.1 - micromark-util-sanitize-uri: 1.1.0 - trim-lines: 3.0.1 - unist-builder: 3.0.0 - unist-util-generated: 2.0.0 - unist-util-position: 4.0.3 - unist-util-visit: 4.1.1 - dev: false - - /mdast-util-to-markdown/1.3.0: - resolution: {integrity: sha512-6tUSs4r+KK4JGTTiQ7FfHmVOaDrLQJPmpjD6wPMlHGUVXoG9Vjc3jIeP+uyBWRf8clwB2blM+W7+KrlMYQnftA==} - dependencies: - '@types/mdast': 3.0.10 - '@types/unist': 2.0.6 - longest-streak: 3.1.0 - mdast-util-to-string: 3.1.0 - micromark-util-decode-string: 1.0.2 - unist-util-visit: 4.1.1 - zwitch: 2.0.4 - dev: false - - /mdast-util-to-string/3.1.0: - resolution: {integrity: sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==} - dev: false - - /micromark-core-commonmark/1.0.6: - resolution: {integrity: sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==} - dependencies: - decode-named-character-reference: 1.0.2 - micromark-factory-destination: 1.0.0 - micromark-factory-label: 1.0.2 - micromark-factory-space: 1.0.0 - micromark-factory-title: 1.0.2 - micromark-factory-whitespace: 1.0.0 - micromark-util-character: 1.1.0 - micromark-util-chunked: 1.0.0 - micromark-util-classify-character: 1.0.0 - micromark-util-html-tag-name: 1.1.0 - micromark-util-normalize-identifier: 1.0.0 - micromark-util-resolve-all: 1.0.0 - micromark-util-subtokenize: 1.0.2 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - uvu: 0.5.6 - dev: false - - /micromark-extension-gfm-autolink-literal/1.0.3: - resolution: {integrity: sha512-i3dmvU0htawfWED8aHMMAzAVp/F0Z+0bPh3YrbTPPL1v4YAlCZpy5rBO5p0LPYiZo0zFVkoYh7vDU7yQSiCMjg==} - dependencies: - micromark-util-character: 1.1.0 - micromark-util-sanitize-uri: 1.1.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - uvu: 0.5.6 - dev: false - - /micromark-extension-gfm-footnote/1.0.4: - resolution: {integrity: sha512-E/fmPmDqLiMUP8mLJ8NbJWJ4bTw6tS+FEQS8CcuDtZpILuOb2kjLqPEeAePF1djXROHXChM/wPJw0iS4kHCcIg==} - dependencies: - micromark-core-commonmark: 1.0.6 - micromark-factory-space: 1.0.0 - micromark-util-character: 1.1.0 - micromark-util-normalize-identifier: 1.0.0 - micromark-util-sanitize-uri: 1.1.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - uvu: 0.5.6 - dev: false - - /micromark-extension-gfm-strikethrough/1.0.4: - resolution: {integrity: sha512-/vjHU/lalmjZCT5xt7CcHVJGq8sYRm80z24qAKXzaHzem/xsDYb2yLL+NNVbYvmpLx3O7SYPuGL5pzusL9CLIQ==} - dependencies: - micromark-util-chunked: 1.0.0 - micromark-util-classify-character: 1.0.0 - micromark-util-resolve-all: 1.0.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - uvu: 0.5.6 - dev: false - - /micromark-extension-gfm-table/1.0.5: - resolution: {integrity: sha512-xAZ8J1X9W9K3JTJTUL7G6wSKhp2ZYHrFk5qJgY/4B33scJzE2kpfRL6oiw/veJTbt7jiM/1rngLlOKPWr1G+vg==} - dependencies: - micromark-factory-space: 1.0.0 - micromark-util-character: 1.1.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - uvu: 0.5.6 - dev: false - - /micromark-extension-gfm-tagfilter/1.0.1: - resolution: {integrity: sha512-Ty6psLAcAjboRa/UKUbbUcwjVAv5plxmpUTy2XC/3nJFL37eHej8jrHrRzkqcpipJliuBH30DTs7+3wqNcQUVA==} - dependencies: - micromark-util-types: 1.0.2 - dev: false - - /micromark-extension-gfm-task-list-item/1.0.3: - resolution: {integrity: sha512-PpysK2S1Q/5VXi72IIapbi/jliaiOFzv7THH4amwXeYXLq3l1uo8/2Be0Ac1rEwK20MQEsGH2ltAZLNY2KI/0Q==} - dependencies: - micromark-factory-space: 1.0.0 - micromark-util-character: 1.1.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - uvu: 0.5.6 - dev: false - - /micromark-extension-gfm/2.0.1: - resolution: {integrity: sha512-p2sGjajLa0iYiGQdT0oelahRYtMWvLjy8J9LOCxzIQsllMCGLbsLW+Nc+N4vi02jcRJvedVJ68cjelKIO6bpDA==} - dependencies: - micromark-extension-gfm-autolink-literal: 1.0.3 - micromark-extension-gfm-footnote: 1.0.4 - micromark-extension-gfm-strikethrough: 1.0.4 - micromark-extension-gfm-table: 1.0.5 - micromark-extension-gfm-tagfilter: 1.0.1 - micromark-extension-gfm-task-list-item: 1.0.3 - micromark-util-combine-extensions: 1.0.0 - micromark-util-types: 1.0.2 - dev: false - - /micromark-extension-math/2.0.2: - resolution: {integrity: sha512-cFv2B/E4pFPBBFuGgLHkkNiFAIQv08iDgPH2HCuR2z3AUgMLecES5Cq7AVtwOtZeRrbA80QgMUk8VVW0Z+D2FA==} - dependencies: - '@types/katex': 0.11.1 - katex: 0.13.24 - micromark-factory-space: 1.0.0 - micromark-util-character: 1.1.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - uvu: 0.5.6 - dev: false - - /micromark-extension-mdx-expression/1.0.3: - resolution: {integrity: sha512-TjYtjEMszWze51NJCZmhv7MEBcgYRgb3tJeMAJ+HQCAaZHHRBaDCccqQzGizR/H4ODefP44wRTgOn2vE5I6nZA==} - dependencies: - micromark-factory-mdx-expression: 1.0.6 - micromark-factory-space: 1.0.0 - micromark-util-character: 1.1.0 - micromark-util-events-to-acorn: 1.2.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - uvu: 0.5.6 - dev: false - - /micromark-extension-mdx-jsx/1.0.3: - resolution: {integrity: sha512-VfA369RdqUISF0qGgv2FfV7gGjHDfn9+Qfiv5hEwpyr1xscRj/CiVRkU7rywGFCO7JwJ5L0e7CJz60lY52+qOA==} - dependencies: - '@types/acorn': 4.0.6 - estree-util-is-identifier-name: 2.0.1 - micromark-factory-mdx-expression: 1.0.6 - micromark-factory-space: 1.0.0 - micromark-util-character: 1.1.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - uvu: 0.5.6 - vfile-message: 3.1.3 - dev: false - - /micromark-extension-mdx-md/1.0.0: - resolution: {integrity: sha512-xaRAMoSkKdqZXDAoSgp20Azm0aRQKGOl0RrS81yGu8Hr/JhMsBmfs4wR7m9kgVUIO36cMUQjNyiyDKPrsv8gOw==} - dependencies: - micromark-util-types: 1.0.2 - dev: false - - /micromark-extension-mdxjs-esm/1.0.3: - resolution: {integrity: sha512-2N13ol4KMoxb85rdDwTAC6uzs8lMX0zeqpcyx7FhS7PxXomOnLactu8WI8iBNXW8AVyea3KIJd/1CKnUmwrK9A==} - dependencies: - micromark-core-commonmark: 1.0.6 - micromark-util-character: 1.1.0 - micromark-util-events-to-acorn: 1.2.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - unist-util-position-from-estree: 1.1.1 - uvu: 0.5.6 - vfile-message: 3.1.3 - dev: false - - /micromark-extension-mdxjs/1.0.0: - resolution: {integrity: sha512-TZZRZgeHvtgm+IhtgC2+uDMR7h8eTKF0QUX9YsgoL9+bADBpBY6SiLvWqnBlLbCEevITmTqmEuY3FoxMKVs1rQ==} - dependencies: - acorn: 8.8.1 - acorn-jsx: 5.3.2_acorn@8.8.1 - micromark-extension-mdx-expression: 1.0.3 - micromark-extension-mdx-jsx: 1.0.3 - micromark-extension-mdx-md: 1.0.0 - micromark-extension-mdxjs-esm: 1.0.3 - micromark-util-combine-extensions: 1.0.0 - micromark-util-types: 1.0.2 - dev: false - - /micromark-factory-destination/1.0.0: - resolution: {integrity: sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==} - dependencies: - micromark-util-character: 1.1.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - dev: false - - /micromark-factory-label/1.0.2: - resolution: {integrity: sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==} - dependencies: - micromark-util-character: 1.1.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - uvu: 0.5.6 - dev: false - - /micromark-factory-mdx-expression/1.0.6: - resolution: {integrity: sha512-WRQIc78FV7KrCfjsEf/sETopbYjElh3xAmNpLkd1ODPqxEngP42eVRGbiPEQWpRV27LzqW+XVTvQAMIIRLPnNA==} - dependencies: - micromark-factory-space: 1.0.0 - micromark-util-character: 1.1.0 - micromark-util-events-to-acorn: 1.2.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - unist-util-position-from-estree: 1.1.1 - uvu: 0.5.6 - vfile-message: 3.1.3 - dev: false - - /micromark-factory-space/1.0.0: - resolution: {integrity: sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==} - dependencies: - micromark-util-character: 1.1.0 - micromark-util-types: 1.0.2 - dev: false - - /micromark-factory-title/1.0.2: - resolution: {integrity: sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==} - dependencies: - micromark-factory-space: 1.0.0 - micromark-util-character: 1.1.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - uvu: 0.5.6 - dev: false - - /micromark-factory-whitespace/1.0.0: - resolution: {integrity: sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==} - dependencies: - micromark-factory-space: 1.0.0 - micromark-util-character: 1.1.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - dev: false - - /micromark-util-character/1.1.0: - resolution: {integrity: sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==} - dependencies: - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - dev: false - - /micromark-util-chunked/1.0.0: - resolution: {integrity: sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==} - dependencies: - micromark-util-symbol: 1.0.1 - dev: false - - /micromark-util-classify-character/1.0.0: - resolution: {integrity: sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==} - dependencies: - micromark-util-character: 1.1.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - dev: false - - /micromark-util-combine-extensions/1.0.0: - resolution: {integrity: sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==} - dependencies: - micromark-util-chunked: 1.0.0 - micromark-util-types: 1.0.2 - dev: false - - /micromark-util-decode-numeric-character-reference/1.0.0: - resolution: {integrity: sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==} - dependencies: - micromark-util-symbol: 1.0.1 - dev: false - - /micromark-util-decode-string/1.0.2: - resolution: {integrity: sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==} - dependencies: - decode-named-character-reference: 1.0.2 - micromark-util-character: 1.1.0 - micromark-util-decode-numeric-character-reference: 1.0.0 - micromark-util-symbol: 1.0.1 - dev: false - - /micromark-util-encode/1.0.1: - resolution: {integrity: sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==} - dev: false - - /micromark-util-events-to-acorn/1.2.0: - resolution: {integrity: sha512-WWp3bf7xT9MppNuw3yPjpnOxa8cj5ACivEzXJKu0WwnjBYfzaBvIAT9KfeyI0Qkll+bfQtfftSwdgTH6QhTOKw==} - dependencies: - '@types/acorn': 4.0.6 - '@types/estree': 1.0.0 - estree-util-visit: 1.2.0 - micromark-util-types: 1.0.2 - uvu: 0.5.6 - vfile-location: 4.0.1 - vfile-message: 3.1.3 - dev: false - - /micromark-util-html-tag-name/1.1.0: - resolution: {integrity: sha512-BKlClMmYROy9UiV03SwNmckkjn8QHVaWkqoAqzivabvdGcwNGMMMH/5szAnywmsTBUzDsU57/mFi0sp4BQO6dA==} - dev: false - - /micromark-util-normalize-identifier/1.0.0: - resolution: {integrity: sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==} - dependencies: - micromark-util-symbol: 1.0.1 - dev: false - - /micromark-util-resolve-all/1.0.0: - resolution: {integrity: sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==} - dependencies: - micromark-util-types: 1.0.2 - dev: false - - /micromark-util-sanitize-uri/1.1.0: - resolution: {integrity: sha512-RoxtuSCX6sUNtxhbmsEFQfWzs8VN7cTctmBPvYivo98xb/kDEoTCtJQX5wyzIYEmk/lvNFTat4hL8oW0KndFpg==} - dependencies: - micromark-util-character: 1.1.0 - micromark-util-encode: 1.0.1 - micromark-util-symbol: 1.0.1 - dev: false - - /micromark-util-subtokenize/1.0.2: - resolution: {integrity: sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==} - dependencies: - micromark-util-chunked: 1.0.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - uvu: 0.5.6 - dev: false - - /micromark-util-symbol/1.0.1: - resolution: {integrity: sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==} - dev: false - - /micromark-util-types/1.0.2: - resolution: {integrity: sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==} - dev: false - - /micromark/3.1.0: - resolution: {integrity: sha512-6Mj0yHLdUZjHnOPgr5xfWIMqMWS12zDN6iws9SLuSz76W8jTtAv24MN4/CL7gJrl5vtxGInkkqDv/JIoRsQOvA==} - dependencies: - '@types/debug': 4.1.7 - debug: 4.3.4 - decode-named-character-reference: 1.0.2 - micromark-core-commonmark: 1.0.6 - micromark-factory-space: 1.0.0 - micromark-util-character: 1.1.0 - micromark-util-chunked: 1.0.0 - micromark-util-combine-extensions: 1.0.0 - micromark-util-decode-numeric-character-reference: 1.0.0 - micromark-util-encode: 1.0.1 - micromark-util-normalize-identifier: 1.0.0 - micromark-util-resolve-all: 1.0.0 - micromark-util-sanitize-uri: 1.1.0 - micromark-util-subtokenize: 1.0.2 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - uvu: 0.5.6 - transitivePeerDependencies: - - supports-color - dev: false - - /mri/1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} - dev: false - - /ms/2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: false - - /nanoid/3.3.4: - resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - dev: false - - /nanoid/4.0.1: - resolution: {integrity: sha512-udKGtCCUafD3nQtJg9wBhRP3KMbPglUsgV5JVsXhvyBs/oefqb4sqMEhKBBgqZncYowu58p1prsZQBYvAj/Gww==} - engines: {node: ^14 || ^16 || >=18} - hasBin: true - dev: false - - /next-mdx-remote/4.3.0_react-dom@18.2.0+react@18.2.0: - resolution: {integrity: sha512-fbxkY03pM2Wx5bDNTVKpYD5Hx3QVZGH+6xDtVIxlxXz4HTifP1yI2DrkDvxXbTz0SYGIbluRMIW81IOOa8pigA==} - engines: {node: '>=14', npm: '>=7'} - peerDependencies: - react: '>=16.x <=18.x' - react-dom: '>=16.x <=18.x' - dependencies: - '@mdx-js/mdx': 2.2.1 - '@mdx-js/react': 2.2.1_react@18.2.0 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - vfile: 5.3.6 - vfile-matter: 3.0.1 - transitivePeerDependencies: - - supports-color - dev: false - - /next-seo/5.14.1_f26ff3bd08f1cd28b0f73422c76f5ffd: - resolution: {integrity: sha512-NiJeQbxYP3z+EMp52q8k3Q+OfX2+Yv2WehERDj98r2wjXxL+woKpRBdsSVYolTD0Hm8IWs42SzaISE93RoQdOw==} - peerDependencies: - next: ^8.1.1-canary.54 || >=9.0.0 - react: '>=16.0.0' - react-dom: '>=16.0.0' - dependencies: - next: 13.0.6_react-dom@18.2.0+react@18.2.0 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - dev: false - - /next-themes/0.2.1_f26ff3bd08f1cd28b0f73422c76f5ffd: - resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} - peerDependencies: - next: '*' - react: '*' - react-dom: '*' - dependencies: - next: 13.0.6_react-dom@18.2.0+react@18.2.0 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - dev: false - - /next/13.0.6_react-dom@18.2.0+react@18.2.0: - resolution: {integrity: sha512-COvigvms2LRt1rrzfBQcMQ2GZd86Mvk1z+LOLY5pniFtL4VrTmhZ9salrbKfSiXbhsD01TrDdD68ec3ABDyscA==} - engines: {node: '>=14.6.0'} - hasBin: true - peerDependencies: - fibers: '>= 3.1.0' - node-sass: ^6.0.0 || ^7.0.0 - react: ^18.2.0 - react-dom: ^18.2.0 - sass: ^1.3.0 - peerDependenciesMeta: - fibers: - optional: true - node-sass: - optional: true - sass: - optional: true - dependencies: - '@next/env': 13.0.6 - '@swc/helpers': 0.4.14 - caniuse-lite: 1.0.30001435 - postcss: 8.4.14 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - styled-jsx: 5.1.0_react@18.2.0 - optionalDependencies: - '@next/swc-android-arm-eabi': 13.0.6 - '@next/swc-android-arm64': 13.0.6 - '@next/swc-darwin-arm64': 13.0.6 - '@next/swc-darwin-x64': 13.0.6 - '@next/swc-freebsd-x64': 13.0.6 - '@next/swc-linux-arm-gnueabihf': 13.0.6 - '@next/swc-linux-arm64-gnu': 13.0.6 - '@next/swc-linux-arm64-musl': 13.0.6 - '@next/swc-linux-x64-gnu': 13.0.6 - '@next/swc-linux-x64-musl': 13.0.6 - '@next/swc-win32-arm64-msvc': 13.0.6 - '@next/swc-win32-ia32-msvc': 13.0.6 - '@next/swc-win32-x64-msvc': 13.0.6 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - dev: false - - /nextra-theme-docs/2.2.14_d8d66b9d2170cddb63c39dddec8541b9: - resolution: {integrity: sha512-QQcHOcAXSfrpbSX3FqXgcQ2favKLnBAczqKWbSDVEtgHiUG6s7pVpxclpKm5F1c/fP47v19USRq3BL/SZ4JEIQ==} - peerDependencies: - next: '>=9.5.3' - nextra: 2.2.14 - react: '>=16.13.1' - react-dom: '>=16.13.1' - dependencies: - '@headlessui/react': 1.7.10_react-dom@18.2.0+react@18.2.0 - '@popperjs/core': 2.11.6 - clsx: 1.2.1 - flexsearch: 0.7.31 - focus-visible: 5.2.0 - git-url-parse: 13.1.0 - intersection-observer: 0.12.2 - match-sorter: 6.3.1 - next: 13.0.6_react-dom@18.2.0+react@18.2.0 - next-seo: 5.14.1_f26ff3bd08f1cd28b0f73422c76f5ffd - next-themes: 0.2.1_f26ff3bd08f1cd28b0f73422c76f5ffd - nextra: 2.2.14_f26ff3bd08f1cd28b0f73422c76f5ffd - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - scroll-into-view-if-needed: 3.0.4 - zod: 3.20.2 - dev: false - - /nextra/2.2.14_f26ff3bd08f1cd28b0f73422c76f5ffd: - resolution: {integrity: sha512-kToTiTiE4qrQsQ9snFRqCGLLSjKSFgFV/BJm3yp/SRmkmCr1WaWrlmUTAuXlxM7PREbNaZouNSOJ0hGS92rM8A==} - peerDependencies: - next: '>=9.5.3' - react: '>=16.13.1' - react-dom: '>=16.13.1' - dependencies: - '@mdx-js/mdx': 2.2.1 - '@mdx-js/react': 2.2.1_react@18.2.0 - '@napi-rs/simple-git': 0.1.8 - github-slugger: 2.0.0 - graceful-fs: 4.2.10 - gray-matter: 4.0.3 - katex: 0.16.4 - lodash.get: 4.4.2 - next: 13.0.6_react-dom@18.2.0+react@18.2.0 - next-mdx-remote: 4.3.0_react-dom@18.2.0+react@18.2.0 - p-limit: 3.1.0 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - rehype-katex: 6.0.2 - rehype-pretty-code: 0.9.2_shiki@0.12.1 - remark-gfm: 3.0.1 - remark-math: 5.1.1 - remark-reading-time: 2.0.1 - shiki: 0.12.1 - slash: 3.0.0 - title: 3.5.3 - unist-util-remove: 3.1.1 - unist-util-visit: 4.1.1 - transitivePeerDependencies: - - supports-color - dev: false - - /npm-run-path/2.0.2: - resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} - engines: {node: '>=4'} - dependencies: - path-key: 2.0.1 - dev: false - - /p-finally/1.0.0: - resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} - engines: {node: '>=4'} - dev: false - - /p-limit/3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - dependencies: - yocto-queue: 0.1.0 - dev: false - - /parse-entities/4.0.0: - resolution: {integrity: sha512-5nk9Fn03x3rEhGaX1FU6IDwG/k+GxLXlFAkgrbM1asuAFl3BhdQWvASaIsmwWypRNcZKHPYnIuOSfIWEyEQnPQ==} - dependencies: - '@types/unist': 2.0.6 - character-entities: 2.0.2 - character-entities-legacy: 3.0.0 - character-reference-invalid: 2.0.1 - decode-named-character-reference: 1.0.2 - is-alphanumerical: 2.0.1 - is-decimal: 2.0.1 - is-hexadecimal: 2.0.1 - dev: false - - /parse-numeric-range/1.3.0: - resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} - dev: false - - /parse-path/7.0.0: - resolution: {integrity: sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==} - dependencies: - protocols: 2.0.1 - dev: false - - /parse-url/8.1.0: - resolution: {integrity: sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==} - dependencies: - parse-path: 7.0.0 - dev: false - - /parse5/6.0.1: - resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - dev: false - - /path-key/2.0.1: - resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} - engines: {node: '>=4'} - dev: false - - /periscopic/3.0.4: - resolution: {integrity: sha512-SFx68DxCv0Iyo6APZuw/AKewkkThGwssmU0QWtTlvov3VAtPX+QJ4CadwSaz8nrT5jPIuxdvJWB4PnD2KNDxQg==} - dependencies: - estree-walker: 3.0.1 - is-reference: 3.0.0 - dev: false - - /picocolors/1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: false - - /postcss/8.4.14: - resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==} - engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.4 - picocolors: 1.0.0 - source-map-js: 1.0.2 - dev: false - - /property-information/6.2.0: - resolution: {integrity: sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==} - dev: false - - /protocols/2.0.1: - resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==} - dev: false - - /pseudomap/1.0.2: - resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} - dev: false - - /react-dom/18.2.0_react@18.2.0: - resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} - peerDependencies: - react: ^18.2.0 - dependencies: - loose-envify: 1.4.0 - react: 18.2.0 - scheduler: 0.23.0 - dev: false - - /react/18.2.0: - resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} - engines: {node: '>=0.10.0'} - dependencies: - loose-envify: 1.4.0 - dev: false - - /reading-time/1.5.0: - resolution: {integrity: sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==} - dev: false - - /regenerator-runtime/0.13.11: - resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} - dev: false - - /rehype-katex/6.0.2: - resolution: {integrity: sha512-C4gDAlS1+l0hJqctyiU64f9CvT00S03qV1T6HiMzbSuLBgWUtcqydWHY9OpKrm0SpkK16FNd62CDKyWLwV2ppg==} - dependencies: - '@types/hast': 2.3.4 - '@types/katex': 0.11.1 - hast-util-to-text: 3.1.2 - katex: 0.15.6 - rehype-parse: 8.0.4 - unified: 10.1.2 - unist-util-remove-position: 4.0.1 - unist-util-visit: 4.1.1 - dev: false - - /rehype-parse/8.0.4: - resolution: {integrity: sha512-MJJKONunHjoTh4kc3dsM1v3C9kGrrxvA3U8PxZlP2SjH8RNUSrb+lF7Y0KVaUDnGH2QZ5vAn7ulkiajM9ifuqg==} - dependencies: - '@types/hast': 2.3.4 - hast-util-from-parse5: 7.1.1 - parse5: 6.0.1 - unified: 10.1.2 - dev: false - - /rehype-pretty-code/0.9.2_shiki@0.12.1: - resolution: {integrity: sha512-l369pvBK6ihBEuy2+VDpHU+zbbY8I+Z4LiyIOunHAt3xyw6selaOFKc/DnX94jI5OJb3+NgjbOxXx2yaAypjZw==} - engines: {node: ^12.16.0 || >=13.2.0} - peerDependencies: - shiki: '*' - dependencies: - hash-obj: 4.0.0 - nanoid: 4.0.1 - parse-numeric-range: 1.3.0 - shiki: 0.12.1 - dev: false - - /remark-gfm/3.0.1: - resolution: {integrity: sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==} - dependencies: - '@types/mdast': 3.0.10 - mdast-util-gfm: 2.0.1 - micromark-extension-gfm: 2.0.1 - unified: 10.1.2 - transitivePeerDependencies: - - supports-color - dev: false - - /remark-math/5.1.1: - resolution: {integrity: sha512-cE5T2R/xLVtfFI4cCePtiRn+e6jKMtFDR3P8V3qpv8wpKjwvHoBA4eJzvX+nVrnlNy0911bdGmuspCSwetfYHw==} - dependencies: - '@types/mdast': 3.0.10 - mdast-util-math: 2.0.2 - micromark-extension-math: 2.0.2 - unified: 10.1.2 - dev: false - - /remark-mdx/2.1.5: - resolution: {integrity: sha512-A8vw5s+BgOa968Irt8BO7DfWJTE0Fe7Ge3hX8zzDB1DnwMZTNdK6qF2IcFao+/7nzk1vSysKcFp+3ku4vhMpaQ==} - dependencies: - mdast-util-mdx: 2.0.0 - micromark-extension-mdxjs: 1.0.0 - transitivePeerDependencies: - - supports-color - dev: false - - /remark-parse/10.0.1: - resolution: {integrity: sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==} - dependencies: - '@types/mdast': 3.0.10 - mdast-util-from-markdown: 1.2.0 - unified: 10.1.2 - transitivePeerDependencies: - - supports-color - dev: false - - /remark-reading-time/2.0.1: - resolution: {integrity: sha512-fy4BKy9SRhtYbEHvp6AItbRTnrhiDGbqLQTSYVbQPGuRCncU1ubSsh9p/W5QZSxtYcUXv8KGL0xBgPLyNJA1xw==} - dependencies: - estree-util-is-identifier-name: 2.0.1 - estree-util-value-to-estree: 1.3.0 - reading-time: 1.5.0 - unist-util-visit: 3.1.0 - dev: false - - /remark-rehype/10.1.0: - resolution: {integrity: sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==} - dependencies: - '@types/hast': 2.3.4 - '@types/mdast': 3.0.10 - mdast-util-to-hast: 12.2.4 - unified: 10.1.2 - dev: false - - /remove-accents/0.4.2: - resolution: {integrity: sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=} - dev: false - - /sade/1.8.1: - resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} - engines: {node: '>=6'} - dependencies: - mri: 1.2.0 - dev: false - - /scheduler/0.23.0: - resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} - dependencies: - loose-envify: 1.4.0 - dev: false - - /scroll-into-view-if-needed/3.0.4: - resolution: {integrity: sha512-s+/F50jwTOUt+u5oEIAzum9MN2lUQNvWBe/zfEsVQcbaERjGkKLq1s+2wCHkahMLC8nMLbzMVKivx9JhunXaZg==} - dependencies: - compute-scroll-into-view: 2.0.4 - dev: false - - /section-matter/1.0.0: - resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} - engines: {node: '>=4'} - dependencies: - extend-shallow: 2.0.1 - kind-of: 6.0.3 - dev: false - - /shebang-command/1.2.0: - resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} - engines: {node: '>=0.10.0'} - dependencies: - shebang-regex: 1.0.0 - dev: false - - /shebang-regex/1.0.0: - resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} - engines: {node: '>=0.10.0'} - dev: false - - /shiki/0.12.1: - resolution: {integrity: sha512-aieaV1m349rZINEBkjxh2QbBvFFQOlgqYTNtCal82hHj4dDZ76oMlQIX+C7ryerBTDiga3e5NfH6smjdJ02BbQ==} - dependencies: - jsonc-parser: 3.2.0 - vscode-oniguruma: 1.7.0 - vscode-textmate: 8.0.0 - dev: false - - /signal-exit/3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: false - - /slash/3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - dev: false - - /sort-keys/5.0.0: - resolution: {integrity: sha512-Pdz01AvCAottHTPQGzndktFNdbRA75BgOfeT1hH+AMnJFv8lynkPi42rfeEhpx1saTEI3YNMWxfqu0sFD1G8pw==} - engines: {node: '>=12'} - dependencies: - is-plain-obj: 4.1.0 - dev: false - - /source-map-js/1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} - dev: false - - /source-map/0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} - dev: false - - /space-separated-tokens/2.0.2: - resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - dev: false - - /sprintf-js/1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - dev: false - - /stringify-entities/4.0.3: - resolution: {integrity: sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==} - dependencies: - character-entities-html4: 2.1.0 - character-entities-legacy: 3.0.0 - dev: false - - /strip-bom-string/1.0.0: - resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} - engines: {node: '>=0.10.0'} - dev: false - - /strip-eof/1.0.0: - resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} - engines: {node: '>=0.10.0'} - dev: false - - /style-to-object/0.3.0: - resolution: {integrity: sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==} - dependencies: - inline-style-parser: 0.1.1 - dev: false - - /styled-jsx/5.1.0_react@18.2.0: - resolution: {integrity: sha512-/iHaRJt9U7T+5tp6TRelLnqBqiaIT0HsO0+vgyj8hK2KUk7aejFqRrumqPUlAqDwAj8IbS/1hk3IhBAAK/FCUQ==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@babel/core': '*' - babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' - peerDependenciesMeta: - '@babel/core': - optional: true - babel-plugin-macros: - optional: true - dependencies: - client-only: 0.0.1 - react: 18.2.0 - dev: false - - /supports-color/4.5.0: - resolution: {integrity: sha512-ycQR/UbvI9xIlEdQT1TQqwoXtEldExbCEAJgRo5YXlmSKjv6ThHnP9/vwGa1gr19Gfw+LkFd7KqYMhzrRC5JYw==} - engines: {node: '>=4'} - dependencies: - has-flag: 2.0.0 - dev: false - - /title/3.5.3: - resolution: {integrity: sha512-20JyowYglSEeCvZv3EZ0nZ046vLarO37prvV0mbtQV7C8DJPGgN967r8SJkqd3XK3K3lD3/Iyfp3avjfil8Q2Q==} - hasBin: true - dependencies: - arg: 1.0.0 - chalk: 2.3.0 - clipboardy: 1.2.2 - titleize: 1.0.0 - dev: false - - /titleize/1.0.0: - resolution: {integrity: sha1-fTUHIgYYMLpmF2MeDP0+oIOY2Vo=} - engines: {node: '>=0.10.0'} - dev: false - - /trim-lines/3.0.1: - resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - dev: false - - /trough/2.1.0: - resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==} - dev: false - - /tslib/2.4.1: - resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==} - dev: false - - /type-fest/1.4.0: - resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} - engines: {node: '>=10'} - dev: false - - /typescript/4.9.3: - resolution: {integrity: sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==} - engines: {node: '>=4.2.0'} - hasBin: true - dev: true - - /unified/10.1.2: - resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} - dependencies: - '@types/unist': 2.0.6 - bail: 2.0.2 - extend: 3.0.2 - is-buffer: 2.0.5 - is-plain-obj: 4.1.0 - trough: 2.1.0 - vfile: 5.3.6 - dev: false - - /unist-builder/3.0.0: - resolution: {integrity: sha512-GFxmfEAa0vi9i5sd0R2kcrI9ks0r82NasRq5QHh2ysGngrc6GiqD5CDf1FjPenY4vApmFASBIIlk/jj5J5YbmQ==} - dependencies: - '@types/unist': 2.0.6 - dev: false - - /unist-util-find-after/4.0.1: - resolution: {integrity: sha512-QO/PuPMm2ERxC6vFXEPtmAutOopy5PknD+Oq64gGwxKtk4xwo9Z97t9Av1obPmGU0IyTa6EKYUfTrK2QJS3Ozw==} - dependencies: - '@types/unist': 2.0.6 - unist-util-is: 5.1.1 - dev: false - - /unist-util-generated/2.0.0: - resolution: {integrity: sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw==} - dev: false - - /unist-util-is/5.1.1: - resolution: {integrity: sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ==} - dev: false - - /unist-util-position-from-estree/1.1.1: - resolution: {integrity: sha512-xtoY50b5+7IH8tFbkw64gisG9tMSpxDjhX9TmaJJae/XuxQ9R/Kc8Nv1eOsf43Gt4KV/LkriMy9mptDr7XLcaw==} - dependencies: - '@types/unist': 2.0.6 - dev: false - - /unist-util-position/4.0.3: - resolution: {integrity: sha512-p/5EMGIa1qwbXjA+QgcBXaPWjSnZfQ2Sc3yBEEfgPwsEmJd8Qh+DSk3LGnmOM4S1bY2C0AjmMnB8RuEYxpPwXQ==} - dependencies: - '@types/unist': 2.0.6 - dev: false - - /unist-util-remove-position/4.0.1: - resolution: {integrity: sha512-0yDkppiIhDlPrfHELgB+NLQD5mfjup3a8UYclHruTJWmY74je8g+CIFr79x5f6AkmzSwlvKLbs63hC0meOMowQ==} - dependencies: - '@types/unist': 2.0.6 - unist-util-visit: 4.1.1 - dev: false - - /unist-util-remove/3.1.1: - resolution: {integrity: sha512-kfCqZK5YVY5yEa89tvpl7KnBBHu2c6CzMkqHUrlOqaRgGOMp0sMvwWOVrbAtj03KhovQB7i96Gda72v/EFE0vw==} - dependencies: - '@types/unist': 2.0.6 - unist-util-is: 5.1.1 - unist-util-visit-parents: 5.1.1 - dev: false - - /unist-util-stringify-position/3.0.2: - resolution: {integrity: sha512-7A6eiDCs9UtjcwZOcCpM4aPII3bAAGv13E96IkawkOAW0OhH+yRxtY0lzo8KiHpzEMfH7Q+FizUmwp8Iqy5EWg==} - dependencies: - '@types/unist': 2.0.6 - dev: false - - /unist-util-visit-parents/4.1.1: - resolution: {integrity: sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==} - dependencies: - '@types/unist': 2.0.6 - unist-util-is: 5.1.1 - dev: false - - /unist-util-visit-parents/5.1.1: - resolution: {integrity: sha512-gks4baapT/kNRaWxuGkl5BIhoanZo7sC/cUT/JToSRNL1dYoXRFl75d++NkjYk4TAu2uv2Px+l8guMajogeuiw==} - dependencies: - '@types/unist': 2.0.6 - unist-util-is: 5.1.1 - dev: false - - /unist-util-visit/3.1.0: - resolution: {integrity: sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==} - dependencies: - '@types/unist': 2.0.6 - unist-util-is: 5.1.1 - unist-util-visit-parents: 4.1.1 - dev: false - - /unist-util-visit/4.1.1: - resolution: {integrity: sha512-n9KN3WV9k4h1DxYR1LoajgN93wpEi/7ZplVe02IoB4gH5ctI1AaF2670BLHQYbwj+pY83gFtyeySFiyMHJklrg==} - dependencies: - '@types/unist': 2.0.6 - unist-util-is: 5.1.1 - unist-util-visit-parents: 5.1.1 - dev: false - - /uvu/0.5.6: - resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} - engines: {node: '>=8'} - hasBin: true - dependencies: - dequal: 2.0.3 - diff: 5.1.0 - kleur: 4.1.5 - sade: 1.8.1 - dev: false - - /vfile-location/4.0.1: - resolution: {integrity: sha512-JDxPlTbZrZCQXogGheBHjbRWjESSPEak770XwWPfw5mTc1v1nWGLB/apzZxsx8a0SJVfF8HK8ql8RD308vXRUw==} - dependencies: - '@types/unist': 2.0.6 - vfile: 5.3.6 - dev: false - - /vfile-matter/3.0.1: - resolution: {integrity: sha512-CAAIDwnh6ZdtrqAuxdElUqQRQDQgbbIrYtDYI8gCjXS1qQ+1XdLoK8FIZWxJwn0/I+BkSSZpar3SOgjemQz4fg==} - dependencies: - '@types/js-yaml': 4.0.5 - is-buffer: 2.0.5 - js-yaml: 4.1.0 - dev: false - - /vfile-message/3.1.3: - resolution: {integrity: sha512-0yaU+rj2gKAyEk12ffdSbBfjnnj+b1zqTBv3OQCTn8yEB02bsPizwdBPrLJjHnK+cU9EMMcUnNv938XcZIkmdA==} - dependencies: - '@types/unist': 2.0.6 - unist-util-stringify-position: 3.0.2 - dev: false - - /vfile/5.3.6: - resolution: {integrity: sha512-ADBsmerdGBs2WYckrLBEmuETSPyTD4TuLxTrw0DvjirxW1ra4ZwkbzG8ndsv3Q57smvHxo677MHaQrY9yxH8cA==} - dependencies: - '@types/unist': 2.0.6 - is-buffer: 2.0.5 - unist-util-stringify-position: 3.0.2 - vfile-message: 3.1.3 - dev: false - - /vscode-oniguruma/1.7.0: - resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} - dev: false - - /vscode-textmate/8.0.0: - resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} - dev: false - - /web-namespaces/2.0.1: - resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} - dev: false - - /which/1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true - dependencies: - isexe: 2.0.0 - dev: false - - /yallist/2.1.2: - resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} - dev: false - - /yocto-queue/0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - dev: false - - /zod/3.20.2: - resolution: {integrity: sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==} - dev: false - - /zwitch/2.0.4: - resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - dev: false diff --git a/docs-v2/pages/images/daily-invocations-tooltip.png b/docs-v2/public/images/account/daily-invocations-tooltip.png similarity index 100% rename from docs-v2/pages/images/daily-invocations-tooltip.png rename to docs-v2/public/images/account/daily-invocations-tooltip.png diff --git a/docs-v2/pages/images/usage-by-resource.png b/docs-v2/public/images/account/usage-by-resource.png similarity index 100% rename from docs-v2/pages/images/usage-by-resource.png rename to docs-v2/public/images/account/usage-by-resource.png diff --git a/docs-v2/public/images/actions/update-action-button.png b/docs-v2/public/images/actions/update-action-button.png new file mode 100644 index 0000000000000..5b242f04efd5b Binary files /dev/null and b/docs-v2/public/images/actions/update-action-button.png differ diff --git a/docs-v2/public/images/auth/refresh-fields-after-connecting-slack.png b/docs-v2/public/images/auth/refresh-fields-after-connecting-slack.png new file mode 100644 index 0000000000000..e5f0eb5dd70b8 Binary files /dev/null and b/docs-v2/public/images/auth/refresh-fields-after-connecting-slack.png differ diff --git a/docs-v2/public/images/auth/slack-field-rendered.png b/docs-v2/public/images/auth/slack-field-rendered.png new file mode 100644 index 0000000000000..7fc8b1e784583 Binary files /dev/null and b/docs-v2/public/images/auth/slack-field-rendered.png differ diff --git a/docs-v2/pages/components/images/image-20200819210516311.png b/docs-v2/public/images/components/image-20200819210516311.png similarity index 100% rename from docs-v2/pages/components/images/image-20200819210516311.png rename to docs-v2/public/images/components/image-20200819210516311.png diff --git a/docs-v2/pages/components/images/image-20210326151557417.png b/docs-v2/public/images/components/image-20210326151557417.png similarity index 100% rename from docs-v2/pages/components/images/image-20210326151557417.png rename to docs-v2/public/images/components/image-20210326151557417.png diff --git a/docs-v2/pages/components/images/image-20210326151706682.png b/docs-v2/public/images/components/image-20210326151706682.png similarity index 100% rename from docs-v2/pages/components/images/image-20210326151706682.png rename to docs-v2/public/images/components/image-20210326151706682.png diff --git a/docs-v2/pages/components/images/image-20210326151930885.png b/docs-v2/public/images/components/image-20210326151930885.png similarity index 100% rename from docs-v2/pages/components/images/image-20210326151930885.png rename to docs-v2/public/images/components/image-20210326151930885.png diff --git a/docs-v2/pages/components/images/info-alert-prop-github.png b/docs-v2/public/images/components/info-alert-prop-github.png similarity index 100% rename from docs-v2/pages/components/images/info-alert-prop-github.png rename to docs-v2/public/images/components/info-alert-prop-github.png diff --git a/docs-v2/pages/components/images/quickstart/hello-world-1.gif b/docs-v2/public/images/components/quickstart/hello-world-1.gif similarity index 100% rename from docs-v2/pages/components/images/quickstart/hello-world-1.gif rename to docs-v2/public/images/components/quickstart/hello-world-1.gif diff --git a/docs-v2/pages/components/images/quickstart/hello-world-2.gif b/docs-v2/public/images/components/quickstart/hello-world-2.gif similarity index 100% rename from docs-v2/pages/components/images/quickstart/hello-world-2.gif rename to docs-v2/public/images/components/quickstart/hello-world-2.gif diff --git a/docs-v2/pages/components/images/quickstart/hello-world-3.gif b/docs-v2/public/images/components/quickstart/hello-world-3.gif similarity index 100% rename from docs-v2/pages/components/images/quickstart/hello-world-3.gif rename to docs-v2/public/images/components/quickstart/hello-world-3.gif diff --git a/docs-v2/pages/components/images/quickstart/hello-world-4.gif b/docs-v2/public/images/components/quickstart/hello-world-4.gif similarity index 100% rename from docs-v2/pages/components/images/quickstart/hello-world-4.gif rename to docs-v2/public/images/components/quickstart/hello-world-4.gif diff --git a/docs-v2/pages/components/images/quickstart/uncompressed/hello-world-1.gif b/docs-v2/public/images/components/quickstart/uncompressed/hello-world-1.gif similarity index 100% rename from docs-v2/pages/components/images/quickstart/uncompressed/hello-world-1.gif rename to docs-v2/public/images/components/quickstart/uncompressed/hello-world-1.gif diff --git a/docs-v2/pages/components/images/quickstart/uncompressed/hello-world-2.gif b/docs-v2/public/images/components/quickstart/uncompressed/hello-world-2.gif similarity index 100% rename from docs-v2/pages/components/images/quickstart/uncompressed/hello-world-2.gif rename to docs-v2/public/images/components/quickstart/uncompressed/hello-world-2.gif diff --git a/docs-v2/pages/components/images/quickstart/uncompressed/hello-world-3.gif b/docs-v2/public/images/components/quickstart/uncompressed/hello-world-3.gif similarity index 100% rename from docs-v2/pages/components/images/quickstart/uncompressed/hello-world-3.gif rename to docs-v2/public/images/components/quickstart/uncompressed/hello-world-3.gif diff --git a/docs-v2/pages/components/images/quickstart/uncompressed/hello-world-4.gif b/docs-v2/public/images/components/quickstart/uncompressed/hello-world-4.gif similarity index 100% rename from docs-v2/pages/components/images/quickstart/uncompressed/hello-world-4.gif rename to docs-v2/public/images/components/quickstart/uncompressed/hello-world-4.gif diff --git a/docs-v2/pages/components/images/spotify-$summary-example.png b/docs-v2/public/images/components/spotify-$summary-example.png similarity index 100% rename from docs-v2/pages/components/images/spotify-$summary-example.png rename to docs-v2/public/images/components/spotify-$summary-example.png diff --git a/docs-v2/pages/components/images/trello-board-example.png b/docs-v2/public/images/components/trello-board-example.png similarity index 100% rename from docs-v2/pages/components/images/trello-board-example.png rename to docs-v2/public/images/components/trello-board-example.png diff --git a/docs-v2/pages/components/images/trello-props.png b/docs-v2/public/images/components/trello-props.png similarity index 100% rename from docs-v2/pages/components/images/trello-props.png rename to docs-v2/public/images/components/trello-props.png diff --git a/docs-v2/public/images/components/v3/using-private-actions.png b/docs-v2/public/images/components/v3/using-private-actions.png new file mode 100644 index 0000000000000..4b4eea652cdf2 Binary files /dev/null and b/docs-v2/public/images/components/v3/using-private-actions.png differ diff --git a/docs-v2/public/images/components/v3/using-private-sources.png b/docs-v2/public/images/components/v3/using-private-sources.png new file mode 100644 index 0000000000000..02c0fc4993514 Binary files /dev/null and b/docs-v2/public/images/components/v3/using-private-sources.png differ diff --git a/docs-v2/public/images/control-flow/2024-07-21_20.51.37.gif b/docs-v2/public/images/control-flow/2024-07-21_20.51.37.gif new file mode 100644 index 0000000000000..49f5a9bc192d8 Binary files /dev/null and b/docs-v2/public/images/control-flow/2024-07-21_20.51.37.gif differ diff --git a/docs-v2/public/images/control-flow/2024-07-21_20.55.09.gif b/docs-v2/public/images/control-flow/2024-07-21_20.55.09.gif new file mode 100644 index 0000000000000..7e374f702cf22 Binary files /dev/null and b/docs-v2/public/images/control-flow/2024-07-21_20.55.09.gif differ diff --git a/docs-v2/public/images/control-flow/Inspect.gif b/docs-v2/public/images/control-flow/Inspect.gif new file mode 100644 index 0000000000000..4d6b5feacc936 Binary files /dev/null and b/docs-v2/public/images/control-flow/Inspect.gif differ diff --git a/docs-v2/public/images/control-flow/add_if_else.gif b/docs-v2/public/images/control-flow/add_if_else.gif new file mode 100644 index 0000000000000..aa89413917bef Binary files /dev/null and b/docs-v2/public/images/control-flow/add_if_else.gif differ diff --git a/docs-v2/public/images/control-flow/add_step_to_branch.gif b/docs-v2/public/images/control-flow/add_step_to_branch.gif new file mode 100644 index 0000000000000..4e78dc9100b90 Binary files /dev/null and b/docs-v2/public/images/control-flow/add_step_to_branch.gif differ diff --git a/docs-v2/public/images/control-flow/billing_1credit_a.png b/docs-v2/public/images/control-flow/billing_1credit_a.png new file mode 100644 index 0000000000000..433bb0177380e Binary files /dev/null and b/docs-v2/public/images/control-flow/billing_1credit_a.png differ diff --git a/docs-v2/public/images/control-flow/billing_1credit_b.png b/docs-v2/public/images/control-flow/billing_1credit_b.png new file mode 100644 index 0000000000000..0e1493ac5dff7 Binary files /dev/null and b/docs-v2/public/images/control-flow/billing_1credit_b.png differ diff --git a/docs-v2/public/images/control-flow/billing_3credits.png b/docs-v2/public/images/control-flow/billing_3credits.png new file mode 100644 index 0000000000000..68495811d8fe3 Binary files /dev/null and b/docs-v2/public/images/control-flow/billing_3credits.png differ diff --git a/docs-v2/public/images/control-flow/change_in_state.gif b/docs-v2/public/images/control-flow/change_in_state.gif new file mode 100644 index 0000000000000..4f21337cab30b Binary files /dev/null and b/docs-v2/public/images/control-flow/change_in_state.gif differ diff --git a/docs-v2/public/images/control-flow/delay-step-props.png b/docs-v2/public/images/control-flow/delay-step-props.png new file mode 100644 index 0000000000000..423f5bd031136 Binary files /dev/null and b/docs-v2/public/images/control-flow/delay-step-props.png differ diff --git a/docs-v2/public/images/control-flow/end_workflow.png b/docs-v2/public/images/control-flow/end_workflow.png new file mode 100644 index 0000000000000..e9a94f10fd08c Binary files /dev/null and b/docs-v2/public/images/control-flow/end_workflow.png differ diff --git a/docs-v2/public/images/control-flow/executed_path.png b/docs-v2/public/images/control-flow/executed_path.png new file mode 100644 index 0000000000000..c60d7adf6b7c3 Binary files /dev/null and b/docs-v2/public/images/control-flow/executed_path.png differ diff --git a/docs-v2/public/images/control-flow/execution_path_a_linear.png b/docs-v2/public/images/control-flow/execution_path_a_linear.png new file mode 100644 index 0000000000000..880c653023e17 Binary files /dev/null and b/docs-v2/public/images/control-flow/execution_path_a_linear.png differ diff --git a/docs-v2/public/images/control-flow/execution_path_b_nonlinear.png b/docs-v2/public/images/control-flow/execution_path_b_nonlinear.png new file mode 100644 index 0000000000000..0bec6e8f91294 Binary files /dev/null and b/docs-v2/public/images/control-flow/execution_path_b_nonlinear.png differ diff --git a/docs-v2/public/images/control-flow/ifelse-configuration.png b/docs-v2/public/images/control-flow/ifelse-configuration.png new file mode 100644 index 0000000000000..58fed5d8e5ed2 Binary files /dev/null and b/docs-v2/public/images/control-flow/ifelse-configuration.png differ diff --git a/docs-v2/public/images/control-flow/nesting.png b/docs-v2/public/images/control-flow/nesting.png new file mode 100644 index 0000000000000..9fbab992cb07a Binary files /dev/null and b/docs-v2/public/images/control-flow/nesting.png differ diff --git a/docs-v2/public/images/control-flow/out_of_date.gif b/docs-v2/public/images/control-flow/out_of_date.gif new file mode 100644 index 0000000000000..536d68d391025 Binary files /dev/null and b/docs-v2/public/images/control-flow/out_of_date.gif differ diff --git a/docs-v2/public/images/control-flow/parallel/01_trigger.gif b/docs-v2/public/images/control-flow/parallel/01_trigger.gif new file mode 100644 index 0000000000000..fa5576ba52a19 Binary files /dev/null and b/docs-v2/public/images/control-flow/parallel/01_trigger.gif differ diff --git a/docs-v2/public/images/control-flow/parallel/02_add_parallel.gif b/docs-v2/public/images/control-flow/parallel/02_add_parallel.gif new file mode 100644 index 0000000000000..b12abcb83398d Binary files /dev/null and b/docs-v2/public/images/control-flow/parallel/02_add_parallel.gif differ diff --git a/docs-v2/public/images/control-flow/parallel/03_configure_and_test.gif b/docs-v2/public/images/control-flow/parallel/03_configure_and_test.gif new file mode 100644 index 0000000000000..001f944b1f0b7 Binary files /dev/null and b/docs-v2/public/images/control-flow/parallel/03_configure_and_test.gif differ diff --git a/docs-v2/public/images/control-flow/parallel/04_add_steps.gif b/docs-v2/public/images/control-flow/parallel/04_add_steps.gif new file mode 100644 index 0000000000000..f48aa1a64f699 Binary files /dev/null and b/docs-v2/public/images/control-flow/parallel/04_add_steps.gif differ diff --git a/docs-v2/public/images/control-flow/parallel/05_test_end_phase.gif b/docs-v2/public/images/control-flow/parallel/05_test_end_phase.gif new file mode 100644 index 0000000000000..263f21125d519 Binary files /dev/null and b/docs-v2/public/images/control-flow/parallel/05_test_end_phase.gif differ diff --git a/docs-v2/public/images/control-flow/parallel/06_use_exports_in_parent_flow.gif b/docs-v2/public/images/control-flow/parallel/06_use_exports_in_parent_flow.gif new file mode 100644 index 0000000000000..e472567b8fd1e Binary files /dev/null and b/docs-v2/public/images/control-flow/parallel/06_use_exports_in_parent_flow.gif differ diff --git a/docs-v2/public/images/control-flow/parallel/07_deploy_and_run.gif b/docs-v2/public/images/control-flow/parallel/07_deploy_and_run.gif new file mode 100644 index 0000000000000..b0adc1ccc36df Binary files /dev/null and b/docs-v2/public/images/control-flow/parallel/07_deploy_and_run.gif differ diff --git a/docs-v2/public/images/control-flow/parallel/add_branch.png b/docs-v2/public/images/control-flow/parallel/add_branch.png new file mode 100644 index 0000000000000..d1ddc48472e04 Binary files /dev/null and b/docs-v2/public/images/control-flow/parallel/add_branch.png differ diff --git a/docs-v2/public/images/control-flow/parallel/add_parallel_block.png b/docs-v2/public/images/control-flow/parallel/add_parallel_block.png new file mode 100644 index 0000000000000..0dbe975150717 Binary files /dev/null and b/docs-v2/public/images/control-flow/parallel/add_parallel_block.png differ diff --git a/docs-v2/public/images/control-flow/parallel/parallel.png b/docs-v2/public/images/control-flow/parallel/parallel.png new file mode 100644 index 0000000000000..46520fd497c26 Binary files /dev/null and b/docs-v2/public/images/control-flow/parallel/parallel.png differ diff --git a/docs-v2/public/images/control-flow/parallel/rename_branch.png b/docs-v2/public/images/control-flow/parallel/rename_branch.png new file mode 100644 index 0000000000000..3a9f1c43b718a Binary files /dev/null and b/docs-v2/public/images/control-flow/parallel/rename_branch.png differ diff --git a/docs-v2/public/images/control-flow/passing_data.png b/docs-v2/public/images/control-flow/passing_data.png new file mode 100644 index 0000000000000..d6485f5a96b35 Binary files /dev/null and b/docs-v2/public/images/control-flow/passing_data.png differ diff --git a/docs-v2/public/images/control-flow/reason.png b/docs-v2/public/images/control-flow/reason.png new file mode 100644 index 0000000000000..4b07352dd8405 Binary files /dev/null and b/docs-v2/public/images/control-flow/reason.png differ diff --git a/docs-v2/public/images/control-flow/reference_end_exports.gif b/docs-v2/public/images/control-flow/reference_end_exports.gif new file mode 100644 index 0000000000000..fffa5dd2735ad Binary files /dev/null and b/docs-v2/public/images/control-flow/reference_end_exports.gif differ diff --git a/docs-v2/public/images/control-flow/referencing_data.png b/docs-v2/public/images/control-flow/referencing_data.png new file mode 100644 index 0000000000000..cd0ccf3be367f Binary files /dev/null and b/docs-v2/public/images/control-flow/referencing_data.png differ diff --git a/docs-v2/public/images/control-flow/return_response_after_block.png b/docs-v2/public/images/control-flow/return_response_after_block.png new file mode 100644 index 0000000000000..371f3016e119e Binary files /dev/null and b/docs-v2/public/images/control-flow/return_response_after_block.png differ diff --git a/docs-v2/public/images/control-flow/return_response_conditional.png b/docs-v2/public/images/control-flow/return_response_conditional.png new file mode 100644 index 0000000000000..fd8dd5cca805e Binary files /dev/null and b/docs-v2/public/images/control-flow/return_response_conditional.png differ diff --git a/docs-v2/public/images/control-flow/return_response_in_block.png b/docs-v2/public/images/control-flow/return_response_in_block.png new file mode 100644 index 0000000000000..9af13e1b46ab1 Binary files /dev/null and b/docs-v2/public/images/control-flow/return_response_in_block.png differ diff --git a/docs-v2/public/images/control-flow/rule_builder_groups.png b/docs-v2/public/images/control-flow/rule_builder_groups.png new file mode 100644 index 0000000000000..2401d206723dc Binary files /dev/null and b/docs-v2/public/images/control-flow/rule_builder_groups.png differ diff --git a/docs-v2/public/images/control-flow/rule_builder_multiple.png b/docs-v2/public/images/control-flow/rule_builder_multiple.png new file mode 100644 index 0000000000000..d09608642b8c2 Binary files /dev/null and b/docs-v2/public/images/control-flow/rule_builder_multiple.png differ diff --git a/docs-v2/public/images/control-flow/rule_builder_overview.png b/docs-v2/public/images/control-flow/rule_builder_overview.png new file mode 100644 index 0000000000000..d804fa7028026 Binary files /dev/null and b/docs-v2/public/images/control-flow/rule_builder_overview.png differ diff --git a/docs-v2/public/images/control-flow/rule_builder_simple.png b/docs-v2/public/images/control-flow/rule_builder_simple.png new file mode 100644 index 0000000000000..84660e2a3108b Binary files /dev/null and b/docs-v2/public/images/control-flow/rule_builder_simple.png differ diff --git a/docs-v2/public/images/control-flow/segment_delay.png b/docs-v2/public/images/control-flow/segment_delay.png new file mode 100644 index 0000000000000..81282a9b4157f Binary files /dev/null and b/docs-v2/public/images/control-flow/segment_delay.png differ diff --git a/docs-v2/public/images/control-flow/segment_linear.png b/docs-v2/public/images/control-flow/segment_linear.png new file mode 100644 index 0000000000000..28ee860ecbbc1 Binary files /dev/null and b/docs-v2/public/images/control-flow/segment_linear.png differ diff --git a/docs-v2/public/images/control-flow/segment_non_linear.png b/docs-v2/public/images/control-flow/segment_non_linear.png new file mode 100644 index 0000000000000..68495811d8fe3 Binary files /dev/null and b/docs-v2/public/images/control-flow/segment_non_linear.png differ diff --git a/docs-v2/public/images/control-flow/select_different_event.gif b/docs-v2/public/images/control-flow/select_different_event.gif new file mode 100644 index 0000000000000..2e4ad762e82d7 Binary files /dev/null and b/docs-v2/public/images/control-flow/select_different_event.gif differ diff --git a/docs-v2/public/images/control-flow/state_error.png b/docs-v2/public/images/control-flow/state_error.png new file mode 100644 index 0000000000000..627cfbf320446 Binary files /dev/null and b/docs-v2/public/images/control-flow/state_error.png differ diff --git a/docs-v2/public/images/control-flow/state_stale.png b/docs-v2/public/images/control-flow/state_stale.png new file mode 100644 index 0000000000000..9b367406a032e Binary files /dev/null and b/docs-v2/public/images/control-flow/state_stale.png differ diff --git a/docs-v2/public/images/control-flow/state_success.png b/docs-v2/public/images/control-flow/state_success.png new file mode 100644 index 0000000000000..bbc1670123c8b Binary files /dev/null and b/docs-v2/public/images/control-flow/state_success.png differ diff --git a/docs-v2/public/images/control-flow/switch/add_another_case.gif b/docs-v2/public/images/control-flow/switch/add_another_case.gif new file mode 100644 index 0000000000000..6f3ac9fd779eb Binary files /dev/null and b/docs-v2/public/images/control-flow/switch/add_another_case.gif differ diff --git a/docs-v2/public/images/control-flow/switch/add_switch.gif b/docs-v2/public/images/control-flow/switch/add_switch.gif new file mode 100644 index 0000000000000..6f399864f6663 Binary files /dev/null and b/docs-v2/public/images/control-flow/switch/add_switch.gif differ diff --git a/docs-v2/public/images/control-flow/switch/build_and_test_alternate_paths.gif b/docs-v2/public/images/control-flow/switch/build_and_test_alternate_paths.gif new file mode 100644 index 0000000000000..e988de80d5384 Binary files /dev/null and b/docs-v2/public/images/control-flow/switch/build_and_test_alternate_paths.gif differ diff --git a/docs-v2/public/images/control-flow/switch/deploy_and_test.gif b/docs-v2/public/images/control-flow/switch/deploy_and_test.gif new file mode 100644 index 0000000000000..39e8a42d2157b Binary files /dev/null and b/docs-v2/public/images/control-flow/switch/deploy_and_test.gif differ diff --git a/docs-v2/public/images/control-flow/switch/export_data_to_parent.gif b/docs-v2/public/images/control-flow/switch/export_data_to_parent.gif new file mode 100644 index 0000000000000..82330fa2e667b Binary files /dev/null and b/docs-v2/public/images/control-flow/switch/export_data_to_parent.gif differ diff --git a/docs-v2/public/images/control-flow/switch/switch-configuration.png b/docs-v2/public/images/control-flow/switch/switch-configuration.png new file mode 100644 index 0000000000000..fd01a7f64ab67 Binary files /dev/null and b/docs-v2/public/images/control-flow/switch/switch-configuration.png differ diff --git a/docs-v2/public/images/control-flow/switch/test_and_build_success_path.gif b/docs-v2/public/images/control-flow/switch/test_and_build_success_path.gif new file mode 100644 index 0000000000000..6fe707ceb04b0 Binary files /dev/null and b/docs-v2/public/images/control-flow/switch/test_and_build_success_path.gif differ diff --git a/docs-v2/public/images/control-flow/switch/test_different_trigger_events.gif b/docs-v2/public/images/control-flow/switch/test_different_trigger_events.gif new file mode 100644 index 0000000000000..d70c16d66ae65 Binary files /dev/null and b/docs-v2/public/images/control-flow/switch/test_different_trigger_events.gif differ diff --git a/docs-v2/public/images/control-flow/switch/trigger.gif b/docs-v2/public/images/control-flow/switch/trigger.gif new file mode 100644 index 0000000000000..ddeff4a0f4f0e Binary files /dev/null and b/docs-v2/public/images/control-flow/switch/trigger.gif differ diff --git a/docs-v2/public/images/control-flow/test_and_deploy.gif b/docs-v2/public/images/control-flow/test_and_deploy.gif new file mode 100644 index 0000000000000..c96e2e8d84f84 Binary files /dev/null and b/docs-v2/public/images/control-flow/test_and_deploy.gif differ diff --git a/docs-v2/public/images/control-flow/test_end_phase.gif b/docs-v2/public/images/control-flow/test_end_phase.gif new file mode 100644 index 0000000000000..7a0f72878ffa2 Binary files /dev/null and b/docs-v2/public/images/control-flow/test_end_phase.gif differ diff --git a/docs-v2/public/images/control-flow/trigger.gif b/docs-v2/public/images/control-flow/trigger.gif new file mode 100644 index 0000000000000..c3b74d5f03e54 Binary files /dev/null and b/docs-v2/public/images/control-flow/trigger.gif differ diff --git a/docs-v2/public/images/control-flow/unknown_execution_path.png b/docs-v2/public/images/control-flow/unknown_execution_path.png new file mode 100644 index 0000000000000..7e9cad0dc9976 Binary files /dev/null and b/docs-v2/public/images/control-flow/unknown_execution_path.png differ diff --git a/docs-v2/public/images/data-stores/add-or-update-a-single-record.png b/docs-v2/public/images/data-stores/add-or-update-a-single-record.png new file mode 100644 index 0000000000000..ad4421357a9b1 Binary files /dev/null and b/docs-v2/public/images/data-stores/add-or-update-a-single-record.png differ diff --git a/docs-v2/public/images/data-stores/configure-data-store-retrieve-record.png b/docs-v2/public/images/data-stores/configure-data-store-retrieve-record.png new file mode 100644 index 0000000000000..19b4ea6e9dd23 Binary files /dev/null and b/docs-v2/public/images/data-stores/configure-data-store-retrieve-record.png differ diff --git a/docs-v2/public/images/data-stores/configuring-data-store-update-action.png b/docs-v2/public/images/data-stores/configuring-data-store-update-action.png new file mode 100644 index 0000000000000..e79fb4136511a Binary files /dev/null and b/docs-v2/public/images/data-stores/configuring-data-store-update-action.png differ diff --git a/docs-v2/public/images/data-stores/delete-a-single-record.png b/docs-v2/public/images/data-stores/delete-a-single-record.png new file mode 100644 index 0000000000000..77f30f364cfaf Binary files /dev/null and b/docs-v2/public/images/data-stores/delete-a-single-record.png differ diff --git a/docs-v2/public/images/data-stores/get-a-record-action-selection.png b/docs-v2/public/images/data-stores/get-a-record-action-selection.png new file mode 100644 index 0000000000000..b3d16f0a5ff5a Binary files /dev/null and b/docs-v2/public/images/data-stores/get-a-record-action-selection.png differ diff --git a/docs-v2/public/images/data-stores/nodejs-example.png b/docs-v2/public/images/data-stores/nodejs-example.png new file mode 100644 index 0000000000000..60cff583ebb78 Binary files /dev/null and b/docs-v2/public/images/data-stores/nodejs-example.png differ diff --git a/docs-v2/public/images/data-stores/select-delete-a-record.png b/docs-v2/public/images/data-stores/select-delete-a-record.png new file mode 100644 index 0000000000000..423478b18e36e Binary files /dev/null and b/docs-v2/public/images/data-stores/select-delete-a-record.png differ diff --git a/docs-v2/public/images/data-stores/update-data-store-key-by-reference.png b/docs-v2/public/images/data-stores/update-data-store-key-by-reference.png new file mode 100644 index 0000000000000..6f76189fb6178 Binary files /dev/null and b/docs-v2/public/images/data-stores/update-data-store-key-by-reference.png differ diff --git a/docs-v2/pages/destinations/images/conditional-payload-expression.png b/docs-v2/public/images/destinations/conditional-payload-expression.png similarity index 100% rename from docs-v2/pages/destinations/images/conditional-payload-expression.png rename to docs-v2/public/images/destinations/conditional-payload-expression.png diff --git a/docs-v2/pages/destinations/images/dollar-event-body-payload.png b/docs-v2/public/images/destinations/dollar-event-body-payload.png similarity index 100% rename from docs-v2/pages/destinations/images/dollar-event-body-payload.png rename to docs-v2/public/images/destinations/dollar-event-body-payload.png diff --git a/docs-v2/pages/destinations/images/dollar-event-payload.png b/docs-v2/public/images/destinations/dollar-event-payload.png similarity index 100% rename from docs-v2/pages/destinations/images/dollar-event-payload.png rename to docs-v2/public/images/destinations/dollar-event-payload.png diff --git a/docs-v2/pages/destinations/images/email-payload.png b/docs-v2/public/images/destinations/email-payload.png similarity index 100% rename from docs-v2/pages/destinations/images/email-payload.png rename to docs-v2/public/images/destinations/email-payload.png diff --git a/docs-v2/pages/destinations/http/images/new-code-step.png b/docs-v2/public/images/destinations/new-code-step.png similarity index 100% rename from docs-v2/pages/destinations/http/images/new-code-step.png rename to docs-v2/public/images/destinations/new-code-step.png diff --git a/docs-v2/pages/destinations/http/images/new-code.png b/docs-v2/public/images/destinations/new-code.png similarity index 100% rename from docs-v2/pages/destinations/http/images/new-code.png rename to docs-v2/public/images/destinations/new-code.png diff --git a/docs-v2/pages/destinations/images/pipeline-id.png b/docs-v2/public/images/destinations/pipeline-id.png similarity index 100% rename from docs-v2/pages/destinations/images/pipeline-id.png rename to docs-v2/public/images/destinations/pipeline-id.png diff --git a/docs-v2/pages/destinations/http/images/webhook-action-params.png b/docs-v2/public/images/destinations/webhook-action-params.png similarity index 100% rename from docs-v2/pages/destinations/http/images/webhook-action-params.png rename to docs-v2/public/images/destinations/webhook-action-params.png diff --git a/docs-v2/pages/destinations/http/images/webhook-action.png b/docs-v2/public/images/destinations/webhook-action.png similarity index 100% rename from docs-v2/pages/destinations/http/images/webhook-action.png rename to docs-v2/public/images/destinations/webhook-action.png diff --git a/docs-v2/pages/examples/images/configured-task-scheduler-step.png b/docs-v2/public/images/examples/configured-task-scheduler-step.png similarity index 100% rename from docs-v2/pages/examples/images/configured-task-scheduler-step.png rename to docs-v2/public/images/examples/configured-task-scheduler-step.png diff --git a/docs-v2/pages/examples/images/create-task-scheduler.gif b/docs-v2/public/images/examples/create-task-scheduler.gif similarity index 100% rename from docs-v2/pages/examples/images/create-task-scheduler.gif rename to docs-v2/public/images/examples/create-task-scheduler.gif diff --git a/docs-v2/pages/examples/images/email-reference.png b/docs-v2/public/images/examples/email-reference.png similarity index 100% rename from docs-v2/pages/examples/images/email-reference.png rename to docs-v2/public/images/examples/email-reference.png diff --git a/docs-v2/pages/examples/images/endpoint.png b/docs-v2/public/images/examples/endpoint.png similarity index 100% rename from docs-v2/pages/examples/images/endpoint.png rename to docs-v2/public/images/examples/endpoint.png diff --git a/docs-v2/pages/examples/images/find-task-scheduler-step.gif b/docs-v2/public/images/examples/find-task-scheduler-step.gif similarity index 100% rename from docs-v2/pages/examples/images/find-task-scheduler-step.gif rename to docs-v2/public/images/examples/find-task-scheduler-step.gif diff --git a/docs-v2/pages/examples/images/select-task-scheduler-as-trigger.gif b/docs-v2/public/images/examples/select-task-scheduler-as-trigger.gif similarity index 100% rename from docs-v2/pages/examples/images/select-task-scheduler-as-trigger.gif rename to docs-v2/public/images/examples/select-task-scheduler-as-trigger.gif diff --git a/docs-v2/pages/examples/images/task-scheduler-event.png b/docs-v2/public/images/examples/task-scheduler-event.png similarity index 100% rename from docs-v2/pages/examples/images/task-scheduler-event.png rename to docs-v2/public/images/examples/task-scheduler-event.png diff --git a/docs-v2/pages/examples/images/toggle-trigger-step-on.gif b/docs-v2/public/images/examples/toggle-trigger-step-on.gif similarity index 100% rename from docs-v2/pages/examples/images/toggle-trigger-step-on.gif rename to docs-v2/public/images/examples/toggle-trigger-step-on.gif diff --git a/docs-v2/public/images/http/configure-slack-pre-built-action-props.png b/docs-v2/public/images/http/configure-slack-pre-built-action-props.png new file mode 100644 index 0000000000000..e2a9d711a351e Binary files /dev/null and b/docs-v2/public/images/http/configure-slack-pre-built-action-props.png differ diff --git a/docs-v2/public/images/http/connect-slack-account-to-http-request-action.png b/docs-v2/public/images/http/connect-slack-account-to-http-request-action.png new file mode 100644 index 0000000000000..c43e4ed7f3e84 Binary files /dev/null and b/docs-v2/public/images/http/connect-slack-account-to-http-request-action.png differ diff --git a/docs-v2/pages/pipedream-axios/images/default-axios-stack.png b/docs-v2/public/images/http/default-axios-stack.png similarity index 100% rename from docs-v2/pages/pipedream-axios/images/default-axios-stack.png rename to docs-v2/public/images/http/default-axios-stack.png diff --git a/docs-v2/pages/pipedream-axios/images/pipedream-axios-stack.png b/docs-v2/public/images/http/pipedream-axios-stack.png similarity index 100% rename from docs-v2/pages/pipedream-axios/images/pipedream-axios-stack.png rename to docs-v2/public/images/http/pipedream-axios-stack.png diff --git a/docs-v2/pages/pipedream-axios/images/pipedream-axios-success.png b/docs-v2/public/images/http/pipedream-axios-success.png similarity index 100% rename from docs-v2/pages/pipedream-axios/images/pipedream-axios-success.png rename to docs-v2/public/images/http/pipedream-axios-success.png diff --git a/docs-v2/public/images/http/select-an-app-inside-http-request-builder.png b/docs-v2/public/images/http/select-an-app-inside-http-request-builder.png new file mode 100644 index 0000000000000..50f6be639b558 Binary files /dev/null and b/docs-v2/public/images/http/select-an-app-inside-http-request-builder.png differ diff --git a/docs-v2/public/images/http/selecting-pre-built-actions.png b/docs-v2/public/images/http/selecting-pre-built-actions.png new file mode 100644 index 0000000000000..70acd6003980c Binary files /dev/null and b/docs-v2/public/images/http/selecting-pre-built-actions.png differ diff --git a/docs-v2/public/images/http/selecting-the-http-request-builder-action.png b/docs-v2/public/images/http/selecting-the-http-request-builder-action.png new file mode 100644 index 0000000000000..4b252a0bf19c0 Binary files /dev/null and b/docs-v2/public/images/http/selecting-the-http-request-builder-action.png differ diff --git a/docs-v2/public/images/http/selecting-the-slack-api-http-request-builder.png b/docs-v2/public/images/http/selecting-the-slack-api-http-request-builder.png new file mode 100644 index 0000000000000..09e57c7518f1d Binary files /dev/null and b/docs-v2/public/images/http/selecting-the-slack-api-http-request-builder.png differ diff --git a/docs-v2/public/images/http/viewing-authorization-configuration.png b/docs-v2/public/images/http/viewing-authorization-configuration.png new file mode 100644 index 0000000000000..7e88af722fc72 Binary files /dev/null and b/docs-v2/public/images/http/viewing-authorization-configuration.png differ diff --git a/docs-v2/pages/connected-accounts/images/add-new-app.png b/docs-v2/public/images/integrations/add-new-app.png similarity index 100% rename from docs-v2/pages/connected-accounts/images/add-new-app.png rename to docs-v2/public/images/integrations/add-new-app.png diff --git a/docs-v2/pages/connected-accounts/images/api-key.png b/docs-v2/public/images/integrations/api-key.png similarity index 100% rename from docs-v2/pages/connected-accounts/images/api-key.png rename to docs-v2/public/images/integrations/api-key.png diff --git a/docs-v2/pages/connected-accounts/images/connect-existing-account.png b/docs-v2/public/images/integrations/connect-existing-account.png similarity index 100% rename from docs-v2/pages/connected-accounts/images/connect-existing-account.png rename to docs-v2/public/images/integrations/connect-existing-account.png diff --git a/docs-v2/pages/connected-accounts/images/manage-connected-account.png b/docs-v2/public/images/integrations/manage-connected-account.png similarity index 100% rename from docs-v2/pages/connected-accounts/images/manage-connected-account.png rename to docs-v2/public/images/integrations/manage-connected-account.png diff --git a/docs-v2/pages/connected-accounts/images/search-for-slack.png b/docs-v2/public/images/integrations/search-for-slack.png similarity index 100% rename from docs-v2/pages/connected-accounts/images/search-for-slack.png rename to docs-v2/public/images/integrations/search-for-slack.png diff --git a/docs-v2/pages/connected-accounts/images/select-external-auth.png b/docs-v2/public/images/integrations/select-external-auth.png similarity index 100% rename from docs-v2/pages/connected-accounts/images/select-external-auth.png rename to docs-v2/public/images/integrations/select-external-auth.png diff --git a/docs-v2/pages/connected-accounts/images/slack-connect-account.png b/docs-v2/public/images/integrations/slack-connect-account.png similarity index 100% rename from docs-v2/pages/connected-accounts/images/slack-connect-account.png rename to docs-v2/public/images/integrations/slack-connect-account.png diff --git a/docs-v2/pages/connected-accounts/images/slack-token.png b/docs-v2/public/images/integrations/slack-token.png similarity index 100% rename from docs-v2/pages/connected-accounts/images/slack-token.png rename to docs-v2/public/images/integrations/slack-token.png diff --git a/docs-v2/pages/migrate-from-v1/images/app-props-example.png b/docs-v2/public/images/migrate-from-v1/app-props-example.png similarity index 100% rename from docs-v2/pages/migrate-from-v1/images/app-props-example.png rename to docs-v2/public/images/migrate-from-v1/app-props-example.png diff --git a/docs-v2/pages/migrate-from-v1/images/builder-mode-sample.png b/docs-v2/public/images/migrate-from-v1/builder-mode-sample.png similarity index 100% rename from docs-v2/pages/migrate-from-v1/images/builder-mode-sample.png rename to docs-v2/public/images/migrate-from-v1/builder-mode-sample.png diff --git a/docs-v2/pages/migrate-from-v1/images/custom-http-response-option.png b/docs-v2/public/images/migrate-from-v1/custom-http-response-option.png similarity index 100% rename from docs-v2/pages/migrate-from-v1/images/custom-http-response-option.png rename to docs-v2/public/images/migrate-from-v1/custom-http-response-option.png diff --git a/docs-v2/pages/migrate-from-v1/images/custom-string-prop.png b/docs-v2/public/images/migrate-from-v1/custom-string-prop.png similarity index 100% rename from docs-v2/pages/migrate-from-v1/images/custom-string-prop.png rename to docs-v2/public/images/migrate-from-v1/custom-string-prop.png diff --git a/docs-v2/pages/images/demo-poster.png b/docs-v2/public/images/migrate-from-v1/demo-poster.png similarity index 100% rename from docs-v2/pages/images/demo-poster.png rename to docs-v2/public/images/migrate-from-v1/demo-poster.png diff --git a/docs-v2/pages/migrate-from-v1/images/inspector-sample.png b/docs-v2/public/images/migrate-from-v1/inspector-sample.png similarity index 100% rename from docs-v2/pages/migrate-from-v1/images/inspector-sample.png rename to docs-v2/public/images/migrate-from-v1/inspector-sample.png diff --git a/docs-v2/pages/migrate-from-v1/images/new-builder-context-switcher.gif b/docs-v2/public/images/migrate-from-v1/new-builder-context-switcher.gif similarity index 100% rename from docs-v2/pages/migrate-from-v1/images/new-builder-context-switcher.gif rename to docs-v2/public/images/migrate-from-v1/new-builder-context-switcher.gif diff --git a/docs-v2/pages/migrate-from-v1/images/test-workflow-portions.png b/docs-v2/public/images/migrate-from-v1/test-workflow-portions.png similarity index 100% rename from docs-v2/pages/migrate-from-v1/images/test-workflow-portions.png rename to docs-v2/public/images/migrate-from-v1/test-workflow-portions.png diff --git a/docs-v2/pages/migrate-from-v1/images/testing-individual-events.gif b/docs-v2/public/images/migrate-from-v1/testing-individual-events.gif similarity index 100% rename from docs-v2/pages/migrate-from-v1/images/testing-individual-events.gif rename to docs-v2/public/images/migrate-from-v1/testing-individual-events.gif diff --git a/docs-v2/public/images/nodejs/ai-code-generation/generating-slack-actions-with-ai.png b/docs-v2/public/images/nodejs/ai-code-generation/generating-slack-actions-with-ai.png new file mode 100644 index 0000000000000..f85ea82e4c7e2 Binary files /dev/null and b/docs-v2/public/images/nodejs/ai-code-generation/generating-slack-actions-with-ai.png differ diff --git a/docs-v2/public/images/nodejs/configuration-error-example.png b/docs-v2/public/images/nodejs/configuration-error-example.png new file mode 100644 index 0000000000000..8727f0a836bfb Binary files /dev/null and b/docs-v2/public/images/nodejs/configuration-error-example.png differ diff --git a/docs-v2/public/images/nodejs/first-name-prop-example.png b/docs-v2/public/images/nodejs/first-name-prop-example.png new file mode 100644 index 0000000000000..e819f5faf78e9 Binary files /dev/null and b/docs-v2/public/images/nodejs/first-name-prop-example.png differ diff --git a/docs-v2/pages/projects/images/access-badge-click.png b/docs-v2/public/images/projects/access-badge-click.png similarity index 100% rename from docs-v2/pages/projects/images/access-badge-click.png rename to docs-v2/public/images/projects/access-badge-click.png diff --git a/docs-v2/public/images/projects/import-workflows-into-projects.png b/docs-v2/public/images/projects/import-workflows-into-projects.png new file mode 100644 index 0000000000000..a4bebe7843289 Binary files /dev/null and b/docs-v2/public/images/projects/import-workflows-into-projects.png differ diff --git a/docs-v2/pages/projects/images/manage-access-overflow-menu.png b/docs-v2/public/images/projects/manage-access-overflow-menu.png similarity index 100% rename from docs-v2/pages/projects/images/manage-access-overflow-menu.png rename to docs-v2/public/images/projects/manage-access-overflow-menu.png diff --git a/docs-v2/pages/projects/images/project-listing-access.png b/docs-v2/public/images/projects/project-listing-access.png similarity index 100% rename from docs-v2/pages/projects/images/project-listing-access.png rename to docs-v2/public/images/projects/project-listing-access.png diff --git a/docs-v2/pages/projects/images/project-listing-owner.png b/docs-v2/public/images/projects/project-listing-owner.png similarity index 100% rename from docs-v2/pages/projects/images/project-listing-owner.png rename to docs-v2/public/images/projects/project-listing-owner.png diff --git a/docs-v2/pages/projects/images/slideout-member-dropdown.png b/docs-v2/public/images/projects/slideout-member-dropdown.png similarity index 100% rename from docs-v2/pages/projects/images/slideout-member-dropdown.png rename to docs-v2/public/images/projects/slideout-member-dropdown.png diff --git a/docs-v2/pages/projects/images/slideout-member-list.png b/docs-v2/public/images/projects/slideout-member-list.png similarity index 100% rename from docs-v2/pages/projects/images/slideout-member-list.png rename to docs-v2/public/images/projects/slideout-member-list.png diff --git a/docs-v2/pages/projects/images/slideout-restricted.png b/docs-v2/public/images/projects/slideout-restricted.png similarity index 100% rename from docs-v2/pages/projects/images/slideout-restricted.png rename to docs-v2/public/images/projects/slideout-restricted.png diff --git a/docs-v2/pages/projects/images/slideout-workspace-share.png b/docs-v2/public/images/projects/slideout-workspace-share.png similarity index 100% rename from docs-v2/pages/projects/images/slideout-workspace-share.png rename to docs-v2/public/images/projects/slideout-workspace-share.png diff --git a/docs-v2/public/images/python/auth/connected-slack-account.png b/docs-v2/public/images/python/auth/connected-slack-account.png new file mode 100644 index 0000000000000..defb7cce56d63 Binary files /dev/null and b/docs-v2/public/images/python/auth/connected-slack-account.png differ diff --git a/docs-v2/public/images/python/auth/step-selector-python-example.png b/docs-v2/public/images/python/auth/step-selector-python-example.png new file mode 100644 index 0000000000000..62f5373f28e87 Binary files /dev/null and b/docs-v2/public/images/python/auth/step-selector-python-example.png differ diff --git a/docs-v2/public/images/python/print-logs.png b/docs-v2/public/images/python/print-logs.png new file mode 100644 index 0000000000000..d22ed0b76ff19 Binary files /dev/null and b/docs-v2/public/images/python/print-logs.png differ diff --git a/docs-v2/pages/quickstart/images/action-configuration-complete.png b/docs-v2/public/images/quickstart/action-configuration-complete.png similarity index 100% rename from docs-v2/pages/quickstart/images/action-configuration-complete.png rename to docs-v2/public/images/quickstart/action-configuration-complete.png diff --git a/docs-v2/pages/quickstart/images/action_diff.png b/docs-v2/public/images/quickstart/action_diff.png similarity index 100% rename from docs-v2/pages/quickstart/images/action_diff.png rename to docs-v2/public/images/quickstart/action_diff.png diff --git a/docs-v2/pages/quickstart/images/add-step.png b/docs-v2/public/images/quickstart/add-step.png similarity index 100% rename from docs-v2/pages/quickstart/images/add-step.png rename to docs-v2/public/images/quickstart/add-step.png diff --git a/docs-v2/pages/quickstart/images/add-timestamp.png b/docs-v2/public/images/quickstart/add-timestamp.png similarity index 100% rename from docs-v2/pages/quickstart/images/add-timestamp.png rename to docs-v2/public/images/quickstart/add-timestamp.png diff --git a/docs-v2/public/images/quickstart/add-trigger.png b/docs-v2/public/images/quickstart/add-trigger.png new file mode 100644 index 0000000000000..3296b37ebd5a7 Binary files /dev/null and b/docs-v2/public/images/quickstart/add-trigger.png differ diff --git a/docs-v2/pages/quickstart/images/add_action.png b/docs-v2/public/images/quickstart/add_action.png similarity index 100% rename from docs-v2/pages/quickstart/images/add_action.png rename to docs-v2/public/images/quickstart/add_action.png diff --git a/docs-v2/pages/quickstart/images/additional-props.png b/docs-v2/public/images/quickstart/additional-props.png similarity index 100% rename from docs-v2/pages/quickstart/images/additional-props.png rename to docs-v2/public/images/quickstart/additional-props.png diff --git a/docs-v2/pages/quickstart/images/autocomplete-message.png b/docs-v2/public/images/quickstart/autocomplete-message.png similarity index 100% rename from docs-v2/pages/quickstart/images/autocomplete-message.png rename to docs-v2/public/images/quickstart/autocomplete-message.png diff --git a/docs-v2/pages/quickstart/images/basic_workflow.png b/docs-v2/public/images/quickstart/basic_workflow.png similarity index 100% rename from docs-v2/pages/quickstart/images/basic_workflow.png rename to docs-v2/public/images/quickstart/basic_workflow.png diff --git a/docs-v2/pages/quickstart/images/changelog.png b/docs-v2/public/images/quickstart/changelog.png similarity index 100% rename from docs-v2/pages/quickstart/images/changelog.png rename to docs-v2/public/images/quickstart/changelog.png diff --git a/docs-v2/pages/quickstart/images/commit_changes_1.png b/docs-v2/public/images/quickstart/commit_changes_1.png similarity index 100% rename from docs-v2/pages/quickstart/images/commit_changes_1.png rename to docs-v2/public/images/quickstart/commit_changes_1.png diff --git a/docs-v2/pages/quickstart/images/commit_diff_1.png b/docs-v2/public/images/quickstart/commit_diff_1.png similarity index 100% rename from docs-v2/pages/quickstart/images/commit_diff_1.png rename to docs-v2/public/images/quickstart/commit_diff_1.png diff --git a/docs-v2/pages/quickstart/images/commit_diff_2.png b/docs-v2/public/images/quickstart/commit_diff_2.png similarity index 100% rename from docs-v2/pages/quickstart/images/commit_diff_2.png rename to docs-v2/public/images/quickstart/commit_diff_2.png diff --git a/docs-v2/pages/quickstart/images/configure-project.png b/docs-v2/public/images/quickstart/configure-project.png similarity index 100% rename from docs-v2/pages/quickstart/images/configure-project.png rename to docs-v2/public/images/quickstart/configure-project.png diff --git a/docs-v2/pages/quickstart/images/configure-workflow.png b/docs-v2/public/images/quickstart/configure-workflow.png similarity index 100% rename from docs-v2/pages/quickstart/images/configure-workflow.png rename to docs-v2/public/images/quickstart/configure-workflow.png diff --git a/docs-v2/pages/quickstart/images/configure_project.png b/docs-v2/public/images/quickstart/configure_project.png similarity index 100% rename from docs-v2/pages/quickstart/images/configure_project.png rename to docs-v2/public/images/quickstart/configure_project.png diff --git a/docs-v2/pages/quickstart/images/configure_project_1.png b/docs-v2/public/images/quickstart/configure_project_1.png similarity index 100% rename from docs-v2/pages/quickstart/images/configure_project_1.png rename to docs-v2/public/images/quickstart/configure_project_1.png diff --git a/docs-v2/pages/quickstart/images/configure_project_2.png b/docs-v2/public/images/quickstart/configure_project_2.png similarity index 100% rename from docs-v2/pages/quickstart/images/configure_project_2.png rename to docs-v2/public/images/quickstart/configure_project_2.png diff --git a/docs-v2/pages/quickstart/images/configure_workflow.png b/docs-v2/public/images/quickstart/configure_workflow.png similarity index 100% rename from docs-v2/pages/quickstart/images/configure_workflow.png rename to docs-v2/public/images/quickstart/configure_workflow.png diff --git a/docs-v2/public/images/quickstart/connect-google-sheets-account.png b/docs-v2/public/images/quickstart/connect-google-sheets-account.png new file mode 100644 index 0000000000000..5c23d34c0c4bc Binary files /dev/null and b/docs-v2/public/images/quickstart/connect-google-sheets-account.png differ diff --git a/docs-v2/pages/quickstart/images/copy-path.png b/docs-v2/public/images/quickstart/copy-path.png similarity index 100% rename from docs-v2/pages/quickstart/images/copy-path.png rename to docs-v2/public/images/quickstart/copy-path.png diff --git a/docs-v2/public/images/quickstart/create-http-trigger.png b/docs-v2/public/images/quickstart/create-http-trigger.png new file mode 100644 index 0000000000000..27eb86aa8b74f Binary files /dev/null and b/docs-v2/public/images/quickstart/create-http-trigger.png differ diff --git a/docs-v2/pages/quickstart/images/create-project.png b/docs-v2/public/images/quickstart/create-project.png similarity index 100% rename from docs-v2/pages/quickstart/images/create-project.png rename to docs-v2/public/images/quickstart/create-project.png diff --git a/docs-v2/public/images/quickstart/create-trigger.png b/docs-v2/public/images/quickstart/create-trigger.png new file mode 100644 index 0000000000000..3296b37ebd5a7 Binary files /dev/null and b/docs-v2/public/images/quickstart/create-trigger.png differ diff --git a/docs-v2/pages/quickstart/images/create_pr.png b/docs-v2/public/images/quickstart/create_pr.png similarity index 100% rename from docs-v2/pages/quickstart/images/create_pr.png rename to docs-v2/public/images/quickstart/create_pr.png diff --git a/docs-v2/pages/quickstart/create_project.png b/docs-v2/public/images/quickstart/create_project.png similarity index 100% rename from docs-v2/pages/quickstart/create_project.png rename to docs-v2/public/images/quickstart/create_project.png diff --git a/docs-v2/pages/quickstart/images/data-inserted.png b/docs-v2/public/images/quickstart/data-inserted.png similarity index 100% rename from docs-v2/pages/quickstart/images/data-inserted.png rename to docs-v2/public/images/quickstart/data-inserted.png diff --git a/docs-v2/pages/quickstart/images/deploy-workflow.png b/docs-v2/public/images/quickstart/deploy-workflow.png similarity index 100% rename from docs-v2/pages/quickstart/images/deploy-workflow.png rename to docs-v2/public/images/quickstart/deploy-workflow.png diff --git a/docs-v2/pages/quickstart/images/deployed-workflow.png b/docs-v2/public/images/quickstart/deployed-workflow.png similarity index 100% rename from docs-v2/pages/quickstart/images/deployed-workflow.png rename to docs-v2/public/images/quickstart/deployed-workflow.png diff --git a/docs-v2/pages/quickstart/images/edit_1.png b/docs-v2/public/images/quickstart/edit_1.png similarity index 100% rename from docs-v2/pages/quickstart/images/edit_1.png rename to docs-v2/public/images/quickstart/edit_1.png diff --git a/docs-v2/pages/quickstart/images/edit_2.png b/docs-v2/public/images/quickstart/edit_2.png similarity index 100% rename from docs-v2/pages/quickstart/images/edit_2.png rename to docs-v2/public/images/quickstart/edit_2.png diff --git a/docs-v2/pages/quickstart/images/edit_in_github.png b/docs-v2/public/images/quickstart/edit_in_github.png similarity index 100% rename from docs-v2/pages/quickstart/images/edit_in_github.png rename to docs-v2/public/images/quickstart/edit_in_github.png diff --git a/docs-v2/pages/quickstart/images/edit_production.png b/docs-v2/public/images/quickstart/edit_production.png similarity index 100% rename from docs-v2/pages/quickstart/images/edit_production.png rename to docs-v2/public/images/quickstart/edit_production.png diff --git a/docs-v2/pages/quickstart/images/empty-sheet.png b/docs-v2/public/images/quickstart/empty-sheet.png similarity index 100% rename from docs-v2/pages/quickstart/images/empty-sheet.png rename to docs-v2/public/images/quickstart/empty-sheet.png diff --git a/docs-v2/pages/quickstart/images/expand-sentiment-results.png b/docs-v2/public/images/quickstart/expand-sentiment-results.png similarity index 100% rename from docs-v2/pages/quickstart/images/expand-sentiment-results.png rename to docs-v2/public/images/quickstart/expand-sentiment-results.png diff --git a/docs-v2/public/images/quickstart/generate-test-event.png b/docs-v2/public/images/quickstart/generate-test-event.png new file mode 100644 index 0000000000000..b48f8a93ed84b Binary files /dev/null and b/docs-v2/public/images/quickstart/generate-test-event.png differ diff --git a/docs-v2/public/images/quickstart/github-sync-v3/commit-changes-2.png b/docs-v2/public/images/quickstart/github-sync-v3/commit-changes-2.png new file mode 100644 index 0000000000000..079312d56b6e9 Binary files /dev/null and b/docs-v2/public/images/quickstart/github-sync-v3/commit-changes-2.png differ diff --git a/docs-v2/public/images/quickstart/github-sync-v3/commit-changes.png b/docs-v2/public/images/quickstart/github-sync-v3/commit-changes.png new file mode 100644 index 0000000000000..73f3ad9ebbff5 Binary files /dev/null and b/docs-v2/public/images/quickstart/github-sync-v3/commit-changes.png differ diff --git a/docs-v2/public/images/quickstart/github-sync-v3/commit-diff-2.png b/docs-v2/public/images/quickstart/github-sync-v3/commit-diff-2.png new file mode 100644 index 0000000000000..e3d9c04a1ea72 Binary files /dev/null and b/docs-v2/public/images/quickstart/github-sync-v3/commit-diff-2.png differ diff --git a/docs-v2/public/images/quickstart/github-sync-v3/commit-diff.png b/docs-v2/public/images/quickstart/github-sync-v3/commit-diff.png new file mode 100644 index 0000000000000..65c388c861b0d Binary files /dev/null and b/docs-v2/public/images/quickstart/github-sync-v3/commit-diff.png differ diff --git a/docs-v2/public/images/quickstart/github-sync-v3/merge-to-prod-2.png b/docs-v2/public/images/quickstart/github-sync-v3/merge-to-prod-2.png new file mode 100644 index 0000000000000..2e065e6098521 Binary files /dev/null and b/docs-v2/public/images/quickstart/github-sync-v3/merge-to-prod-2.png differ diff --git a/docs-v2/public/images/quickstart/github-sync-v3/merge-to-prod.png b/docs-v2/public/images/quickstart/github-sync-v3/merge-to-prod.png new file mode 100644 index 0000000000000..c173cebd3c242 Binary files /dev/null and b/docs-v2/public/images/quickstart/github-sync-v3/merge-to-prod.png differ diff --git a/docs-v2/public/images/quickstart/github-sync-v3/sample-workflow.png b/docs-v2/public/images/quickstart/github-sync-v3/sample-workflow.png new file mode 100644 index 0000000000000..211c5f5db10ad Binary files /dev/null and b/docs-v2/public/images/quickstart/github-sync-v3/sample-workflow.png differ diff --git a/docs-v2/public/images/quickstart/github-sync-v3/view-branch.png b/docs-v2/public/images/quickstart/github-sync-v3/view-branch.png new file mode 100644 index 0000000000000..a049a0cedd77b Binary files /dev/null and b/docs-v2/public/images/quickstart/github-sync-v3/view-branch.png differ diff --git a/docs-v2/pages/quickstart/images/google-oauth.png b/docs-v2/public/images/quickstart/google-oauth.png similarity index 100% rename from docs-v2/pages/quickstart/images/google-oauth.png rename to docs-v2/public/images/quickstart/google-oauth.png diff --git a/docs-v2/pages/quickstart/images/google-permissions.png b/docs-v2/public/images/quickstart/google-permissions.png similarity index 100% rename from docs-v2/pages/quickstart/images/google-permissions.png rename to docs-v2/public/images/quickstart/google-permissions.png diff --git a/docs-v2/pages/quickstart/images/has-headers.png b/docs-v2/public/images/quickstart/has-headers.png similarity index 100% rename from docs-v2/pages/quickstart/images/has-headers.png rename to docs-v2/public/images/quickstart/has-headers.png diff --git a/docs-v2/pages/quickstart/images/inspect-executions.png b/docs-v2/public/images/quickstart/inspect-executions.png similarity index 100% rename from docs-v2/pages/quickstart/images/inspect-executions.png rename to docs-v2/public/images/quickstart/inspect-executions.png diff --git a/docs-v2/public/images/quickstart/inspect-trigger-event.png b/docs-v2/public/images/quickstart/inspect-trigger-event.png new file mode 100644 index 0000000000000..d2914f9594100 Binary files /dev/null and b/docs-v2/public/images/quickstart/inspect-trigger-event.png differ diff --git a/docs-v2/pages/quickstart/images/live-test.png b/docs-v2/public/images/quickstart/live-test.png similarity index 100% rename from docs-v2/pages/quickstart/images/live-test.png rename to docs-v2/public/images/quickstart/live-test.png diff --git a/docs-v2/pages/quickstart/images/merge_pr.png b/docs-v2/public/images/quickstart/merge_pr.png similarity index 100% rename from docs-v2/pages/quickstart/images/merge_pr.png rename to docs-v2/public/images/quickstart/merge_pr.png diff --git a/docs-v2/pages/quickstart/images/merge_to_production_1.png b/docs-v2/public/images/quickstart/merge_to_production_1.png similarity index 100% rename from docs-v2/pages/quickstart/images/merge_to_production_1.png rename to docs-v2/public/images/quickstart/merge_to_production_1.png diff --git a/docs-v2/pages/quickstart/images/merge_to_production_2.png b/docs-v2/public/images/quickstart/merge_to_production_2.png similarity index 100% rename from docs-v2/pages/quickstart/images/merge_to_production_2.png rename to docs-v2/public/images/quickstart/merge_to_production_2.png diff --git a/docs-v2/pages/quickstart/images/merging_to_production.png b/docs-v2/public/images/quickstart/merging_to_production.png similarity index 100% rename from docs-v2/pages/quickstart/images/merging_to_production.png rename to docs-v2/public/images/quickstart/merging_to_production.png diff --git a/docs-v2/pages/quickstart/images/message-completed.png b/docs-v2/public/images/quickstart/message-completed.png similarity index 100% rename from docs-v2/pages/quickstart/images/message-completed.png rename to docs-v2/public/images/quickstart/message-completed.png diff --git a/docs-v2/public/images/quickstart/new-code-step-added.png b/docs-v2/public/images/quickstart/new-code-step-added.png new file mode 100644 index 0000000000000..80f6ec4b3a4f3 Binary files /dev/null and b/docs-v2/public/images/quickstart/new-code-step-added.png differ diff --git a/docs-v2/pages/quickstart/images/new-workflow.png b/docs-v2/public/images/quickstart/new-workflow.png similarity index 100% rename from docs-v2/pages/quickstart/images/new-workflow.png rename to docs-v2/public/images/quickstart/new-workflow.png diff --git a/docs-v2/pages/quickstart/images/new_workflow.png b/docs-v2/public/images/quickstart/new_workflow.png similarity index 100% rename from docs-v2/pages/quickstart/images/new_workflow.png rename to docs-v2/public/images/quickstart/new_workflow.png diff --git a/docs-v2/pages/quickstart/images/pr_deployed.png b/docs-v2/public/images/quickstart/pr_deployed.png similarity index 100% rename from docs-v2/pages/quickstart/images/pr_deployed.png rename to docs-v2/public/images/quickstart/pr_deployed.png diff --git a/docs-v2/pages/quickstart/images/production.png b/docs-v2/public/images/quickstart/production.png similarity index 100% rename from docs-v2/pages/quickstart/images/production.png rename to docs-v2/public/images/quickstart/production.png diff --git a/docs-v2/public/images/quickstart/rename-code-step.gif b/docs-v2/public/images/quickstart/rename-code-step.gif new file mode 100644 index 0000000000000..520ebcdb2b417 Binary files /dev/null and b/docs-v2/public/images/quickstart/rename-code-step.gif differ diff --git a/docs-v2/pages/quickstart/images/rename-code-step.png b/docs-v2/public/images/quickstart/rename-code-step.png similarity index 100% rename from docs-v2/pages/quickstart/images/rename-code-step.png rename to docs-v2/public/images/quickstart/rename-code-step.png diff --git a/docs-v2/pages/quickstart/images/review_pr.png b/docs-v2/public/images/quickstart/review_pr.png similarity index 100% rename from docs-v2/pages/quickstart/images/review_pr.png rename to docs-v2/public/images/quickstart/review_pr.png diff --git a/docs-v2/public/images/quickstart/save-http-trigger.png b/docs-v2/public/images/quickstart/save-http-trigger.png new file mode 100644 index 0000000000000..f9f6bf1a08474 Binary files /dev/null and b/docs-v2/public/images/quickstart/save-http-trigger.png differ diff --git a/docs-v2/public/images/quickstart/select-add-single-row.png b/docs-v2/public/images/quickstart/select-add-single-row.png new file mode 100644 index 0000000000000..d23d6b5aeea3c Binary files /dev/null and b/docs-v2/public/images/quickstart/select-add-single-row.png differ diff --git a/docs-v2/public/images/quickstart/select-google-sheets-app.png b/docs-v2/public/images/quickstart/select-google-sheets-app.png new file mode 100644 index 0000000000000..dad4cda2b821b Binary files /dev/null and b/docs-v2/public/images/quickstart/select-google-sheets-app.png differ diff --git a/docs-v2/pages/quickstart/images/select-sheet-name.png b/docs-v2/public/images/quickstart/select-sheet-name.png similarity index 100% rename from docs-v2/pages/quickstart/images/select-sheet-name.png rename to docs-v2/public/images/quickstart/select-sheet-name.png diff --git a/docs-v2/pages/quickstart/images/select-spreadsheet.png b/docs-v2/public/images/quickstart/select-spreadsheet.png similarity index 100% rename from docs-v2/pages/quickstart/images/select-spreadsheet.png rename to docs-v2/public/images/quickstart/select-spreadsheet.png diff --git a/docs-v2/public/images/quickstart/sentiment-results.png b/docs-v2/public/images/quickstart/sentiment-results.png new file mode 100644 index 0000000000000..8b93f255dee73 Binary files /dev/null and b/docs-v2/public/images/quickstart/sentiment-results.png differ diff --git a/docs-v2/public/images/quickstart/step-selector-code.png b/docs-v2/public/images/quickstart/step-selector-code.png new file mode 100644 index 0000000000000..a3abaf16cae30 Binary files /dev/null and b/docs-v2/public/images/quickstart/step-selector-code.png differ diff --git a/docs-v2/public/images/quickstart/test-code-step.png b/docs-v2/public/images/quickstart/test-code-step.png new file mode 100644 index 0000000000000..0efeb0171fada Binary files /dev/null and b/docs-v2/public/images/quickstart/test-code-step.png differ diff --git a/docs-v2/pages/quickstart/images/test-successful.png b/docs-v2/public/images/quickstart/test-successful.png similarity index 100% rename from docs-v2/pages/quickstart/images/test-successful.png rename to docs-v2/public/images/quickstart/test-successful.png diff --git a/docs-v2/pages/quickstart/images/timestamp-added.png b/docs-v2/public/images/quickstart/timestamp-added.png similarity index 100% rename from docs-v2/pages/quickstart/images/timestamp-added.png rename to docs-v2/public/images/quickstart/timestamp-added.png diff --git a/docs-v2/public/images/quickstart/trigger-continue.png b/docs-v2/public/images/quickstart/trigger-continue.png new file mode 100644 index 0000000000000..3467c284831d7 Binary files /dev/null and b/docs-v2/public/images/quickstart/trigger-continue.png differ diff --git a/docs-v2/public/images/quickstart/unique-url.png b/docs-v2/public/images/quickstart/unique-url.png new file mode 100644 index 0000000000000..0575b818c2632 Binary files /dev/null and b/docs-v2/public/images/quickstart/unique-url.png differ diff --git a/docs-v2/public/images/quickstart/v3/async-props.png b/docs-v2/public/images/quickstart/v3/async-props.png new file mode 100644 index 0000000000000..8c5459b4bb0a6 Binary files /dev/null and b/docs-v2/public/images/quickstart/v3/async-props.png differ diff --git a/docs-v2/public/images/quickstart/v3/autocomplete-export.png b/docs-v2/public/images/quickstart/v3/autocomplete-export.png new file mode 100644 index 0000000000000..2ba1aae347743 Binary files /dev/null and b/docs-v2/public/images/quickstart/v3/autocomplete-export.png differ diff --git a/docs-v2/public/images/quickstart/v3/copy-sentiment-path.png b/docs-v2/public/images/quickstart/v3/copy-sentiment-path.png new file mode 100644 index 0000000000000..9a7e11e33915b Binary files /dev/null and b/docs-v2/public/images/quickstart/v3/copy-sentiment-path.png differ diff --git a/docs-v2/public/images/quickstart/v3/deploy-workflow.png b/docs-v2/public/images/quickstart/v3/deploy-workflow.png new file mode 100644 index 0000000000000..bbfed0bc8a4b9 Binary files /dev/null and b/docs-v2/public/images/quickstart/v3/deploy-workflow.png differ diff --git a/docs-v2/public/images/quickstart/v3/paste-sentiment-score.png b/docs-v2/public/images/quickstart/v3/paste-sentiment-score.png new file mode 100644 index 0000000000000..4ad8c66420771 Binary files /dev/null and b/docs-v2/public/images/quickstart/v3/paste-sentiment-score.png differ diff --git a/docs-v2/public/images/quickstart/v3/select-headers.png b/docs-v2/public/images/quickstart/v3/select-headers.png new file mode 100644 index 0000000000000..9fd9b9e0e5e70 Binary files /dev/null and b/docs-v2/public/images/quickstart/v3/select-headers.png differ diff --git a/docs-v2/public/images/quickstart/v3/select-sentiment-step.png b/docs-v2/public/images/quickstart/v3/select-sentiment-step.png new file mode 100644 index 0000000000000..5349a1ed7c25e Binary files /dev/null and b/docs-v2/public/images/quickstart/v3/select-sentiment-step.png differ diff --git a/docs-v2/public/images/quickstart/v3/select-spreadsheet-name.png b/docs-v2/public/images/quickstart/v3/select-spreadsheet-name.png new file mode 100644 index 0000000000000..2d1bc827c25b9 Binary files /dev/null and b/docs-v2/public/images/quickstart/v3/select-spreadsheet-name.png differ diff --git a/docs-v2/public/images/quickstart/v3/select-spreadsheet.png b/docs-v2/public/images/quickstart/v3/select-spreadsheet.png new file mode 100644 index 0000000000000..39aad5db35595 Binary files /dev/null and b/docs-v2/public/images/quickstart/v3/select-spreadsheet.png differ diff --git a/docs-v2/public/images/quickstart/v3/select-ts-export.png b/docs-v2/public/images/quickstart/v3/select-ts-export.png new file mode 100644 index 0000000000000..c5ebdae148022 Binary files /dev/null and b/docs-v2/public/images/quickstart/v3/select-ts-export.png differ diff --git a/docs-v2/public/images/quickstart/v3/successful-test-results.png b/docs-v2/public/images/quickstart/v3/successful-test-results.png new file mode 100644 index 0000000000000..9ebb32c98be54 Binary files /dev/null and b/docs-v2/public/images/quickstart/v3/successful-test-results.png differ diff --git a/docs-v2/public/images/quickstart/v3/timestamp.png b/docs-v2/public/images/quickstart/v3/timestamp.png new file mode 100644 index 0000000000000..84ec4ff8656f0 Binary files /dev/null and b/docs-v2/public/images/quickstart/v3/timestamp.png differ diff --git a/docs-v2/pages/quickstart/images/view_branch_on_github_1.png b/docs-v2/public/images/quickstart/view_branch_on_github_1.png similarity index 100% rename from docs-v2/pages/quickstart/images/view_branch_on_github_1.png rename to docs-v2/public/images/quickstart/view_branch_on_github_1.png diff --git a/docs-v2/pages/quickstart/images/view_branch_on_github_2.png b/docs-v2/public/images/quickstart/view_branch_on_github_2.png similarity index 100% rename from docs-v2/pages/quickstart/images/view_branch_on_github_2.png rename to docs-v2/public/images/quickstart/view_branch_on_github_2.png diff --git a/docs-v2/pages/rest-api/images/source-id.png b/docs-v2/public/images/rest-api/source-id.png similarity index 100% rename from docs-v2/pages/rest-api/images/source-id.png rename to docs-v2/public/images/rest-api/source-id.png diff --git a/docs-v2/pages/rest-api/images/webhook-proxy.png b/docs-v2/public/images/rest-api/webhook-proxy.png similarity index 100% rename from docs-v2/pages/rest-api/images/webhook-proxy.png rename to docs-v2/public/images/rest-api/webhook-proxy.png diff --git a/docs-v2/public/images/triggers/add-multiple-triggers.png b/docs-v2/public/images/triggers/add-multiple-triggers.png new file mode 100644 index 0000000000000..1c46b2f7c6e6d Binary files /dev/null and b/docs-v2/public/images/triggers/add-multiple-triggers.png differ diff --git a/docs-v2/public/images/triggers/add-trigger-button.png b/docs-v2/public/images/triggers/add-trigger-button.png new file mode 100644 index 0000000000000..0d95bb535b2b9 Binary files /dev/null and b/docs-v2/public/images/triggers/add-trigger-button.png differ diff --git a/docs-v2/public/images/triggers/email-trigger.png b/docs-v2/public/images/triggers/email-trigger.png new file mode 100644 index 0000000000000..79ec9bbb34237 Binary files /dev/null and b/docs-v2/public/images/triggers/email-trigger.png differ diff --git a/docs-v2/public/images/triggers/gcal-triggers.png b/docs-v2/public/images/triggers/gcal-triggers.png new file mode 100644 index 0000000000000..f9d0737824687 Binary files /dev/null and b/docs-v2/public/images/triggers/gcal-triggers.png differ diff --git a/docs-v2/public/images/triggers/http-trigger-url.png b/docs-v2/public/images/triggers/http-trigger-url.png new file mode 100644 index 0000000000000..b3c30b9b82cf8 Binary files /dev/null and b/docs-v2/public/images/triggers/http-trigger-url.png differ diff --git a/docs-v2/public/images/triggers/retrieve-large-payload.png b/docs-v2/public/images/triggers/retrieve-large-payload.png new file mode 100644 index 0000000000000..a5805a36af075 Binary files /dev/null and b/docs-v2/public/images/triggers/retrieve-large-payload.png differ diff --git a/docs-v2/public/images/triggers/select-a-trigger.png b/docs-v2/public/images/triggers/select-a-trigger.png new file mode 100644 index 0000000000000..f44082ed79dd2 Binary files /dev/null and b/docs-v2/public/images/triggers/select-a-trigger.png differ diff --git a/docs-v2/public/images/triggers/select-an-event.png b/docs-v2/public/images/triggers/select-an-event.png new file mode 100644 index 0000000000000..0baad2f053759 Binary files /dev/null and b/docs-v2/public/images/triggers/select-an-event.png differ diff --git a/docs-v2/public/images/triggers/select-email-trigger.png b/docs-v2/public/images/triggers/select-email-trigger.png new file mode 100644 index 0000000000000..324edd1085058 Binary files /dev/null and b/docs-v2/public/images/triggers/select-email-trigger.png differ diff --git a/docs-v2/public/images/triggers/select-http-trigger.png b/docs-v2/public/images/triggers/select-http-trigger.png new file mode 100644 index 0000000000000..dda1e2c4f697c Binary files /dev/null and b/docs-v2/public/images/triggers/select-http-trigger.png differ diff --git a/docs-v2/public/images/triggers/select-rss-trigger.png b/docs-v2/public/images/triggers/select-rss-trigger.png new file mode 100644 index 0000000000000..530086617ba7f Binary files /dev/null and b/docs-v2/public/images/triggers/select-rss-trigger.png differ diff --git a/docs-v2/public/images/triggers/select-schedule-trigger.png b/docs-v2/public/images/triggers/select-schedule-trigger.png new file mode 100644 index 0000000000000..9aca826a4d9c5 Binary files /dev/null and b/docs-v2/public/images/triggers/select-schedule-trigger.png differ diff --git a/docs-v2/public/images/workflows/actions/update-action-button.png b/docs-v2/public/images/workflows/actions/update-action-button.png new file mode 100644 index 0000000000000..5b242f04efd5b Binary files /dev/null and b/docs-v2/public/images/workflows/actions/update-action-button.png differ diff --git a/docs/docs/pipedream-axios/images/default-axios-stack.png b/docs-v2/public/images/workflows/default-axios-stack.png similarity index 100% rename from docs/docs/pipedream-axios/images/default-axios-stack.png rename to docs-v2/public/images/workflows/default-axios-stack.png diff --git a/docs-v2/public/images/workflows/delay/delay-step-props.png b/docs-v2/public/images/workflows/delay/delay-step-props.png new file mode 100644 index 0000000000000..423f5bd031136 Binary files /dev/null and b/docs-v2/public/images/workflows/delay/delay-step-props.png differ diff --git a/docs/docs/pipedream-axios/images/pipedream-axios-stack.png b/docs-v2/public/images/workflows/pipedream-axios-stack.png similarity index 100% rename from docs/docs/pipedream-axios/images/pipedream-axios-stack.png rename to docs-v2/public/images/workflows/pipedream-axios-stack.png diff --git a/docs/docs/pipedream-axios/images/pipedream-axios-success.png b/docs-v2/public/images/workflows/pipedream-axios-success.png similarity index 100% rename from docs/docs/pipedream-axios/images/pipedream-axios-success.png rename to docs-v2/public/images/workflows/pipedream-axios-success.png diff --git a/docs-v2/public/images/workflows/select-an-event.png b/docs-v2/public/images/workflows/select-an-event.png new file mode 100644 index 0000000000000..0baad2f053759 Binary files /dev/null and b/docs-v2/public/images/workflows/select-an-event.png differ diff --git a/docs-v2/public/images/workflows/sharing/create-share-link.png b/docs-v2/public/images/workflows/sharing/create-share-link.png new file mode 100644 index 0000000000000..6f33800dcefe5 Binary files /dev/null and b/docs-v2/public/images/workflows/sharing/create-share-link.png differ diff --git a/docs-v2/public/images/workflows/sharing/publish-as-template.png b/docs-v2/public/images/workflows/sharing/publish-as-template.png new file mode 100644 index 0000000000000..55865b90530a6 Binary files /dev/null and b/docs-v2/public/images/workflows/sharing/publish-as-template.png differ diff --git a/docs-v2/public/images/workflows/sharing/sharing-workflow-button.png b/docs-v2/public/images/workflows/sharing/sharing-workflow-button.png new file mode 100644 index 0000000000000..cec5a3ccca0b2 Binary files /dev/null and b/docs-v2/public/images/workflows/sharing/sharing-workflow-button.png differ diff --git a/docs/docs/workflows/steps/images/adding-step-note.gif b/docs-v2/public/images/workflows/steps/adding-step-note.gif similarity index 100% rename from docs/docs/workflows/steps/images/adding-step-note.gif rename to docs-v2/public/images/workflows/steps/adding-step-note.gif diff --git a/docs-v2/public/images/workflows/steps/step-name.png b/docs-v2/public/images/workflows/steps/step-name.png new file mode 100644 index 0000000000000..df84a63d5f970 Binary files /dev/null and b/docs-v2/public/images/workflows/steps/step-name.png differ diff --git a/docs-v2/public/images/workflows/steps/step-notes.png b/docs-v2/public/images/workflows/steps/step-notes.png new file mode 100644 index 0000000000000..86d5cdbd99f71 Binary files /dev/null and b/docs-v2/public/images/workflows/steps/step-notes.png differ diff --git a/docs-v2/public/images/workflows/triggers/add-multiple-triggers.png b/docs-v2/public/images/workflows/triggers/add-multiple-triggers.png new file mode 100644 index 0000000000000..1c46b2f7c6e6d Binary files /dev/null and b/docs-v2/public/images/workflows/triggers/add-multiple-triggers.png differ diff --git a/docs-v2/public/images/workflows/triggers/add-trigger-button.png b/docs-v2/public/images/workflows/triggers/add-trigger-button.png new file mode 100644 index 0000000000000..0d95bb535b2b9 Binary files /dev/null and b/docs-v2/public/images/workflows/triggers/add-trigger-button.png differ diff --git a/docs-v2/public/images/workflows/triggers/email-trigger.png b/docs-v2/public/images/workflows/triggers/email-trigger.png new file mode 100644 index 0000000000000..79ec9bbb34237 Binary files /dev/null and b/docs-v2/public/images/workflows/triggers/email-trigger.png differ diff --git a/docs-v2/public/images/workflows/triggers/gcal-triggers.png b/docs-v2/public/images/workflows/triggers/gcal-triggers.png new file mode 100644 index 0000000000000..f9d0737824687 Binary files /dev/null and b/docs-v2/public/images/workflows/triggers/gcal-triggers.png differ diff --git a/docs-v2/public/images/workflows/triggers/http-trigger-url.png b/docs-v2/public/images/workflows/triggers/http-trigger-url.png new file mode 100644 index 0000000000000..b3c30b9b82cf8 Binary files /dev/null and b/docs-v2/public/images/workflows/triggers/http-trigger-url.png differ diff --git a/docs-v2/public/images/workflows/triggers/retrieve-large-payload.png b/docs-v2/public/images/workflows/triggers/retrieve-large-payload.png new file mode 100644 index 0000000000000..a5805a36af075 Binary files /dev/null and b/docs-v2/public/images/workflows/triggers/retrieve-large-payload.png differ diff --git a/docs-v2/public/images/workflows/triggers/select-a-trigger.png b/docs-v2/public/images/workflows/triggers/select-a-trigger.png new file mode 100644 index 0000000000000..f44082ed79dd2 Binary files /dev/null and b/docs-v2/public/images/workflows/triggers/select-a-trigger.png differ diff --git a/docs-v2/public/images/workflows/triggers/select-email-trigger.png b/docs-v2/public/images/workflows/triggers/select-email-trigger.png new file mode 100644 index 0000000000000..324edd1085058 Binary files /dev/null and b/docs-v2/public/images/workflows/triggers/select-email-trigger.png differ diff --git a/docs-v2/public/images/workflows/triggers/select-http-trigger.png b/docs-v2/public/images/workflows/triggers/select-http-trigger.png new file mode 100644 index 0000000000000..dda1e2c4f697c Binary files /dev/null and b/docs-v2/public/images/workflows/triggers/select-http-trigger.png differ diff --git a/docs-v2/public/images/workflows/triggers/select-rss-trigger.png b/docs-v2/public/images/workflows/triggers/select-rss-trigger.png new file mode 100644 index 0000000000000..530086617ba7f Binary files /dev/null and b/docs-v2/public/images/workflows/triggers/select-rss-trigger.png differ diff --git a/docs-v2/public/images/workflows/triggers/select-schedule-trigger.png b/docs-v2/public/images/workflows/triggers/select-schedule-trigger.png new file mode 100644 index 0000000000000..9aca826a4d9c5 Binary files /dev/null and b/docs-v2/public/images/workflows/triggers/select-schedule-trigger.png differ diff --git a/docs-v2/styles/globals.css b/docs-v2/styles/globals.css index 7733a4d1817bb..bda4e3de65750 100644 --- a/docs-v2/styles/globals.css +++ b/docs-v2/styles/globals.css @@ -47,3 +47,11 @@ html.dark { .DocSearch input.DocSearch-Input:focus-visible { box-shadow: none; } + +.highlightHeaderRowTable th { + @apply bg-gray-50 dark:bg-gray-800; +} + +.highlightHeaderRowTable td:last-child { + white-space: nowrap; +} \ No newline at end of file diff --git a/docs-v2/theme.config.tsx b/docs-v2/theme.config.tsx index 216e5f35be7bc..4045ba38fddbe 100644 --- a/docs-v2/theme.config.tsx +++ b/docs-v2/theme.config.tsx @@ -1,19 +1,33 @@ import React from "react"; import { useRouter } from "next/router"; -import { DocsThemeConfig } from "nextra-theme-docs"; +import { + DocsThemeConfig, useConfig, +} from "nextra-theme-docs"; -import PipedreamCode from "./components/PipedreamCode"; import PipedreamLink from "./components/PipedreamLink"; import PipedreamTextLogo from "./components/PipedreamTextLogo"; import SlackLogo from "./components/SlackLogo"; import DocSearch from "./components/DocSearch"; const config: DocsThemeConfig = { - head: null, + head: function Head() { + const router = useRouter() + const { frontMatter } = useConfig() + + return ( + <> + + {router && } + + + + ) + }, // Custom components that replace the default MDX components components: { "a": PipedreamLink, - "code": PipedreamCode, }, logo: PipedreamTextLogo, logoLink: "https://pipedream.com", @@ -24,16 +38,18 @@ const config: DocsThemeConfig = { link: "https://pipedream.com/support", icon: SlackLogo, }, - docsRepositoryBase: "https://github.com/PipedreamHQ/pipedream/docs-v2", + docsRepositoryBase: "https://github.com/PipedreamHQ/pipedream/blob/master/docs-v2", footer: { - text: ( + content: ( Pipedream, Inc. {new Date().getFullYear()} {" "} ), }, - primaryHue: 153, // Pipedream green - primarySaturation: 100, + color: { + hue: 153, // Pipedream green + saturation: 100, + }, feedback: { content: null, // By default this showed a Discord support icon, this was the recommended way to disable it }, @@ -41,20 +57,6 @@ const config: DocsThemeConfig = { autoCollapse: true, defaultMenuCollapseLevel: 1, }, - useNextSeoProps() { - const { route } = useRouter(); - return { - titleTemplate: "%s - Pipedream", - description: "Workflow automation for developers", - canonical: `https://pipedream.com/docs${ route === '/' ? '' : route}`, - additionalLinkTags: [ - { - href: "/docs/favicon.ico", - rel: "icon", - }, - ], - }; - }, search: { component: DocSearch, }, diff --git a/docs-v2/utils/imageLoader.js b/docs-v2/utils/imageLoader.js new file mode 100644 index 0000000000000..274c3820dfe8b --- /dev/null +++ b/docs-v2/utils/imageLoader.js @@ -0,0 +1,5 @@ +export default function myImageLoader({ + src, width, quality, +}) { + return `https://pipedream.com${src}?w=${width}&q=${quality || 75}`; +} diff --git a/docs-v2/validate-mdx-links.mjs b/docs-v2/validate-mdx-links.mjs new file mode 100755 index 0000000000000..411d3242059a4 --- /dev/null +++ b/docs-v2/validate-mdx-links.mjs @@ -0,0 +1,309 @@ +import { promises as fs } from "fs"; +import { + dirname, join, relative, basename, +} from "path"; +import chalk from "chalk"; + +// Convert header text to anchor link format +function headerToAnchor(headerText) { + // First remove any Markdown links - replace [text](url) with just text + const textWithoutLinks = headerText.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); + + return textWithoutLinks + .toLowerCase() + // Remove backticks which are just formatting + .replace(/`/g, "") + // Keep underscores but remove other special characters + .replace(/[^a-z0-9_\s-]/g, "") + .trim() + // Convert spaces to hyphens + .replace(/\s+/g, "-"); +} + +// Convert relative link to absolute path +function resolveRelativeLink(relativeLink, currentFilePath) { + // If it's just an anchor link (#something), keep it relative to current file + if (relativeLink.startsWith("#")) { + const basePath = "/" + relative("pages", currentFilePath).replace(/\.mdx$/, ""); + return `${basePath}${relativeLink}`; + } + + const dirPath = dirname(currentFilePath); + const absolutePath = join(dirPath, relativeLink); + return "/" + relative("pages", absolutePath); +} + +// Normalize path handling +function normalizePath(path) { + // Special case: root path + if (path === "/") return "/"; + + // Remove trailing slash unless it's the root + if (path.endsWith("/") && path !== "/") { + path = path.slice(0, -1); + } + return path; +} + +// Find all MDX files recursively +async function findMdxFiles(dir) { + const files = await fs.readdir(dir, { + withFileTypes: true, + }); + const mdxFiles = []; + + for (const file of files) { + const filePath = join(dir, file.name); + if (file.isDirectory()) { + mdxFiles.push(...await findMdxFiles(filePath)); + } else if (file.name.endsWith(".mdx")) { + mdxFiles.push(filePath); + } + } + + return mdxFiles; +} + +// Extract links and their line numbers from MDX content +function extractLinks(content, filePath) { + const links = []; + const lines = content.split("\n"); + const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; + + lines.forEach((line, index) => { + let match; + while ((match = linkRegex.exec(line)) !== null) { + const [ + , , link, + ] = match; + // Only process internal links + if (link.startsWith("/") || link.startsWith("#") || !link.includes("://")) { + let normalizedLink; + if (link.startsWith("#")) { + // For same-file anchors, we'll check both with and without the file path + const basePath = "/" + relative("pages", filePath).replace(/\.mdx$/, ""); + normalizedLink = `${basePath}${link}`; + } else { + normalizedLink = link.startsWith("/") + ? link + : resolveRelativeLink(link, filePath); + } + links.push({ + originalLink: link, + link: normalizedLink, + lineNumber: index + 1, + }); + } + } + }); + + return links; +} + +// Extract valid anchors from MDX content +function extractAnchors(content, filePath) { + const anchors = new Set(); + const lines = content.split("\n"); + const headerRegex = /^#{1,6}\s+(.+)$/; + + // Calculate the base path for this file + const relativePath = relative("pages", filePath); + const basePath = "/" + relativePath.replace(/\.mdx$/, ""); + const baseDir = dirname(basePath); + + // For basePath /core/workflows/code/nodejs.mdx -> /core/workflows/code/nodejs + const normalizedBasePath = normalizePath(basePath); + anchors.add(normalizedBasePath.toLowerCase()); + + // For index files, also add the directory path + const isIndexFile = basename(filePath) === "index.mdx"; + if (isIndexFile) { + const dirPath = baseDir === "." + ? "/" + : baseDir; + anchors.add(dirPath.toLowerCase()); + } + + // Process all headers in the file + lines.forEach((line) => { + const match = line.match(headerRegex); + if (match) { + const headerText = match[1].trim(); + const anchor = headerToAnchor(headerText); + + // For headers, we need to track: + // 1. Simple #anchor for same-file references + anchors.add(`#${anchor}`.toLowerCase()); + + // 2. Full path versions for cross-file references + const anchorPaths = [ + `${normalizedBasePath}#${anchor}`, + `${normalizedBasePath}/#${anchor}`, + ]; + + // For index files, also add anchors at the directory level + if (isIndexFile) { + const dirPath = baseDir === "." + ? "/" + : baseDir; + anchorPaths.push( + `${dirPath}#${anchor}`, + `${dirPath}/#${anchor}`, + ); + } + + // Add all variants to our set of valid anchors + anchorPaths.forEach((path) => { + anchors.add(path.toLowerCase()); + }); + } + }); + + if (process.env.DEBUG) { + console.log(`File: ${filePath}`); + console.log("Valid anchors:", Array.from(anchors)); + } + + return anchors; +} + +// Try to find MDX file in direct or index format +async function findMdxFile(basePath) { + basePath = normalizePath(basePath); + + // Try direct .mdx file first + const directPath = join("pages", basePath + ".mdx"); + try { + await fs.access(directPath); + return directPath; + } catch (err) { + // Then try index.mdx + const indexPath = join("pages", basePath, "index.mdx"); + try { + await fs.access(indexPath); + return indexPath; + } catch (err) { + return null; + } + } +} + +async function main() { + try { + const mdxFiles = await findMdxFiles("pages"); + const linkMap = new Map(); + const validAnchors = new Set(); + const fileAnchorsMap = new Map(); // Track anchors by file + + // First pass: collect all links and generate valid anchors + console.log("Processing MDX files..."); + for (const filePath of mdxFiles) { + const content = await fs.readFile(filePath, "utf8"); + + // Extract and store links + const links = extractLinks(content, filePath); + if (links.length > 0) { + linkMap.set(filePath, links); + } + + // Extract and store anchors + const fileAnchors = extractAnchors(content, filePath); + fileAnchorsMap.set(filePath, fileAnchors); + for (const anchor of fileAnchors) { + validAnchors.add(anchor); + } + } + + // Second pass: validate all links + let brokenLinksFound = false; + + for (const [ + file, + links, + ] of linkMap) { + // Get anchors for the current file + const currentFileAnchors = fileAnchorsMap.get(file); + + for (const { + originalLink, link, lineNumber, + } of links) { + if (originalLink.startsWith("#")) { + // For same-file anchors, check both the simple #anchor and the full path + const anchorExists = currentFileAnchors.has(originalLink.toLowerCase()); + if (!anchorExists) { + brokenLinksFound = true; + console.log( + chalk.red("✗"), + `${chalk.yellow(file)}:${chalk.cyan(lineNumber)}`, + `Broken link: ${chalk.red(originalLink)} (anchor not found)`, + ); + } + continue; + } + + // Split link into path and anchor parts + const [ + path, + anchor, + ] = link.split("#"); + const normalizedPath = normalizePath(path); + + // First verify the file exists + const targetFile = await findMdxFile(normalizedPath); + if (!targetFile && anchor) { + brokenLinksFound = true; + console.log( + chalk.red("✗"), + `${chalk.yellow(file)}:${chalk.cyan(lineNumber)}`, + `Broken link: ${chalk.red(link)} (file not found)`, + ); + continue; + } + + // Then check anchor if present + if (anchor) { + // Generate all possible variants of how this anchor might appear + const variations = [ + `${normalizedPath}#${anchor}`, + `${normalizedPath}/#${anchor}`, + // For index files, also check directory-level anchors + basename(targetFile) === "index.mdx" + ? `${dirname(normalizedPath)}#${anchor}` + : null, + basename(targetFile) === "index.mdx" + ? `${dirname(normalizedPath)}/#${anchor}` + : null, + ].filter(Boolean).map((v) => v.toLowerCase()); + + if (process.env.DEBUG) { + console.log("\nChecking link:", link); + console.log("Checking variations:", variations); + console.log("Against anchors:", Array.from(validAnchors)); + } + + const anchorExists = variations.some((v) => validAnchors.has(v)); + + if (!anchorExists) { + brokenLinksFound = true; + console.log( + chalk.red("✗"), + `${chalk.yellow(file)}:${chalk.cyan(lineNumber)}`, + `Broken link: ${chalk.red(originalLink)} (anchor not found)`, + ); + } + } + } + } + + if (brokenLinksFound) { + console.log(chalk.red("\n✗ Broken links found!")); + process.exit(1); + } + console.log(chalk.green("\n✓ All links are valid!")); + } catch (error) { + console.error(chalk.red("Error:"), error.message); + process.exit(1); + } +} + +main(); diff --git a/docs-v2/vercel.json b/docs-v2/vercel.json index 87fc811a1fe20..e58f7232f38f8 100644 --- a/docs-v2/vercel.json +++ b/docs-v2/vercel.json @@ -1,298 +1,4 @@ { - "rewrites": [ - { - "source": "/docs/v2", - "destination": "/" - }, - { - "source": "/docs/v2/(.*)", - "destination": "/$1" - }, - { - "source": "/docs/(.*)", - "destination": "/$1" - }, - { - "source": "/docs-v2/(.*)", - "destination": "/$1" - }, - { - "source": "/docs/assets/(.*)", - "destination": "/assets/$1" - } - ], - "redirects": [ - { - "source": "/", - "destination": "/docs/" - }, - { - "source": "/docs-v2", - "destination": "/docs/" - }, - { - "source": "/docs-v2/(.*)", - "destination": "/docs/$1" - }, - { - "source": "/docs/v2", - "destination": "/docs" - }, - { - "source": "/docs/v2/(.*)", - "destination": "/docs/$1" - }, - { - "source": "/docs/notebook/actions/", - "destination": "/workflows/steps/actions/" - }, - { - "source": "/docs/cron", - "destination": "/workflows/steps/triggers" - }, - { - "source": "/docs/notebook", - "destination": "/workflows/steps" - }, - { - "source": "/docs/workflows/fork", - "destination": "/workflows/copy" - }, - { - "source": "/docs/notebook/fork", - "destination": "/workflows/copy" - }, - { - "source": "/docs/notebook/inspector/", - "destination": "/workflows/events/inspect/" - }, - { - "source": "/docs/notebook/destinations/s3/", - "destination": "/destinations/s3/" - }, - { - "source": "/docs/notebook/destinations/sse/", - "destination": "/destinations/sse/" - }, - { - "source": "/docs/notebook/destinations/snowflake/", - "destination": "/destinations/snowflake/" - }, - { - "source": "/docs/notebook/destinations/http/", - "destination": "/destinations/http/" - }, - { - "source": "/docs/notebook/destinations/email/", - "destination": "/destinations/email/" - }, - { - "source": "/docs/notebook/destinations/", - "destination": "/destinations/" - }, - { - "source": "/docs/notebook/code/", - "destination": "/workflows/steps/code/" - }, - { - "source": "/docs/notebook/observability/", - "destination": "/workflows/events/inspect/" - }, - { - "source": "/docs/notebook/sources/", - "destination": "/workflows/steps/triggers/" - }, - { - "source": "/docs/security/", - "destination": "/docs/privacy-and-security/" - }, - { - "source": "/docs/notebook/sql/", - "destination": "/destinations/" - }, - { - "source": "/docs/what-is-pipedream/", - "destination": "/" - }, - { - "source": "/docs/apps/intercom", - "destination": "https://pipedream.com/apps/intercom" - }, - { - "source": "/docs/apps/zoho-books", - "destination": "https://pipedream.com/apps/zoho_books" - }, - { - "source": "/docs/apps/zoho-crm", - "destination": "https://pipedream.com/apps/zoho_crm" - }, - { - "source": "/docs/apps/zoho-mail", - "destination": "https://pipedream.com/apps/zoho_mail" - }, - { - "source": "/docs/apps/all-apps", - "destination": "https://pipedream.com/apps" - }, - { - "source": "/docs/support", - "destination": "https://pipedream.com/support" - }, - { - "source": "/docs/workflows/steps/code/async/", - "destination": "/docs/code/nodejs/async/" - }, - { - "source": "/docs/workflows/steps/code/state", - "destination": "https://pipedream.com/docs/v1/workflows/steps/code/state" - }, - { - "source": "/docs/workflows/steps/params/", - "destination": "/docs/workflows/steps/using-props/" - }, - { - "source": "/docs/pricing/#developer-tier", - "destination": "/docs/pricing/#free-tier" - }, - { - "source": "/docs/apps/discord/", - "destination": "https://pipedream.com/apps/discord#getting-started" - }, - { - "source": "/docs/apps/hubspot/", - "destination": "https://pipedream.com/apps/hubspot#getting-started" - }, - { - "source": "/docs/apps/intercom/", - "destination": "https://pipedream.com/apps/intercom#getting-started" - }, - { - "source": "/docs/apps/servicenow/", - "destination": "https://pipedream.com/apps/servicenow#getting-started" - }, - { - "source": "/docs/apps/slack/", - "destination": "https://pipedream.com/apps/slack#getting-started" - }, - { - "source": "/docs/apps/strava/", - "destination": "https://pipedream.com/apps/strava#getting-started" - }, - { - "source": "/docs/apps/zoho_books/", - "destination": "https://pipedream.com/apps/zoho_books#getting-started" - }, - { - "source": "/docs/apps/zoho_crm/", - "destination": "https://pipedream.com/apps/zoho-crm#getting-started" - }, - { - "source": "/docs/apps/zoho_mail/", - "destination": "https://pipedream.com/apps/zoho_mail#getting-started" - }, - { - "source": "/docs/apps/zoom/", - "destination": "https://pipedream.com/apps/zoom#getting-started" - }, - { - "source": "/docs/apps/all-apps/", - "destination": "/docs/apps/" - }, - { - "source": "/docs/workflows/events/cold-starts/", - "destination": "/workflows/settings/#eliminate-cold-starts" - }, - { - "source": "/docs/workflows/examples/waiting-to-execute-next-step-of-workflow/", - "destination": "/docs/code/nodejs/delay/" - }, - { - "source": "/docs/workflows/networking/", - "destination": "/docs/workflows/vpc/" - }, - { - "source": "/docs/code/python/#making-a-get-request", - "destination": "/docs/code/python/http-requests/#making-a-get-request" - }, - { - "source": "/docs/code/python/#making-a-post-request", - "destination": "/docs/code/python/http-requests/#making-a-post-request" - }, - { - "source": "/docs/code/python/#sending-files", - "destination": "/docs/code/python/http-requests/#sending-files" - }, - { - "source": "/docs/code/python/#writing-a-file-to-tmp", - "destination": "/docs/code/python/working-with-files/#writing-a-file-to-tmp" - }, - { - "source": "/docs/code/python/#reading-a-file-from-tmp", - "destination": "/docs/code/python/working-with-files/#reading-a-file-from-tmp" - }, - { - "source": "/docs/code/python/#listing-files-in-tmp", - "destination": "/docs/code/python/working-with-files/#listing-files-in-tmp" - }, - { - "source": "/docs/api", - "destination": "/docs/rest-api" - }, - { - "source": "/docs/api/rest", - "destination": "/docs/rest-api" - }, - { - "source": "/docs/api/rest/(.*)", - "destination": "/docs/rest-api/$1" - }, - { - "source": "/docs/api/auth", - "destination": "/docs/rest-api/auth" - }, - { - "source": "/docs/components/quickstart/nodejs/actions", - "destination": "/docs/components/actions-quickstart" - }, - { - "source": "/docs/components/quickstart/nodejs/sources", - "destination": "/docs/components/sources-quickstart" - }, - { - "source": "/docs/github-sync", - "destination": "/docs/quickstart/github-sync" - }, - { - "source": "/docs/workflows/events/inspect", - "destination": "/docs/workflows/inspect" - }, - { - "source": "/docs/workflows/steps/triggers", - "destination": "/docs/workflows/triggers" - }, - { - "source": "/docs/workflows/steps/actions", - "destination": "/docs/workflows/actions" - }, - { - "source": "/docs/workspaces/okta", - "destination": "/docs/workspaces/sso/okta" - }, - { - "source": "/docs/workspaces/google", - "destination": "/docs/workspaces/sso/google" - }, - { - "source": "/docs/workspaces/saml", - "destination": "/docs/workspaces/sso/saml" - }, - { - "source": "/docs/workspaces/saml", - "destination": "/docs/workspaces/sso/saml" - }, - { - "source": "/docs/workflows/built-in-functions", - "destination": "/docs/workflows/flow-control" - } - ] + "installCommand": "npx pnpm@9.14.2 install --frozen-lockfile=false", + "buildCommand": "npx pnpm@9.14.2 run build" } diff --git a/docs-v2/yarn.lock b/docs-v2/yarn.lock index c5d1e26aedf97..035c9529b7bd8 100644 --- a/docs-v2/yarn.lock +++ b/docs-v2/yarn.lock @@ -2,1192 +2,188 @@ # yarn lockfile v1 -"@aashutoshrathi/word-wrap@^1.2.3": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" - integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== - -"@algolia/autocomplete-core@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz#1d56482a768c33aae0868c8533049e02e8961be7" - integrity sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw== - dependencies: - "@algolia/autocomplete-plugin-algolia-insights" "1.9.3" - "@algolia/autocomplete-shared" "1.9.3" - -"@algolia/autocomplete-plugin-algolia-insights@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz#9b7f8641052c8ead6d66c1623d444cbe19dde587" - integrity sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg== - dependencies: - "@algolia/autocomplete-shared" "1.9.3" - -"@algolia/autocomplete-preset-algolia@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.9.3.tgz#64cca4a4304cfcad2cf730e83067e0c1b2f485da" - integrity sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA== - dependencies: - "@algolia/autocomplete-shared" "1.9.3" - -"@algolia/autocomplete-shared@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz#2e22e830d36f0a9cf2c0ccd3c7f6d59435b77dfa" - integrity sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ== - -"@algolia/cache-browser-local-storage@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.22.1.tgz#14b6dc9abc9e3a304a5fffb063d15f30af1032d1" - integrity sha512-Sw6IAmOCvvP6QNgY9j+Hv09mvkvEIDKjYW8ow0UDDAxSXy664RBNQk3i/0nt7gvceOJ6jGmOTimaZoY1THmU7g== - dependencies: - "@algolia/cache-common" "4.22.1" - -"@algolia/cache-common@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.22.1.tgz#c625dff4bc2a74e79f9aed67b4e053b0ef1b3ec1" - integrity sha512-TJMBKqZNKYB9TptRRjSUtevJeQVXRmg6rk9qgFKWvOy8jhCPdyNZV1nB3SKGufzvTVbomAukFR8guu/8NRKBTA== - -"@algolia/cache-in-memory@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.22.1.tgz#858a3d887f521362e87d04f3943e2810226a0d71" - integrity sha512-ve+6Ac2LhwpufuWavM/aHjLoNz/Z/sYSgNIXsinGofWOysPilQZPUetqLj8vbvi+DHZZaYSEP9H5SRVXnpsNNw== - dependencies: - "@algolia/cache-common" "4.22.1" - -"@algolia/client-account@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.22.1.tgz#a7fb8b66b9a4f0a428e1426b2561144267d76d43" - integrity sha512-k8m+oegM2zlns/TwZyi4YgCtyToackkOpE+xCaKCYfBfDtdGOaVZCM5YvGPtK+HGaJMIN/DoTL8asbM3NzHonw== - dependencies: - "@algolia/client-common" "4.22.1" - "@algolia/client-search" "4.22.1" - "@algolia/transporter" "4.22.1" - -"@algolia/client-analytics@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.22.1.tgz#506558740b4d49b1b1e3393861f729a8ce921851" - integrity sha512-1ssi9pyxyQNN4a7Ji9R50nSdISIumMFDwKNuwZipB6TkauJ8J7ha/uO60sPJFqQyqvvI+px7RSNRQT3Zrvzieg== - dependencies: - "@algolia/client-common" "4.22.1" - "@algolia/client-search" "4.22.1" - "@algolia/requester-common" "4.22.1" - "@algolia/transporter" "4.22.1" - -"@algolia/client-common@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.22.1.tgz#042b19c1b6157c485fa1b551349ab313944d2b05" - integrity sha512-IvaL5v9mZtm4k4QHbBGDmU3wa/mKokmqNBqPj0K7lcR8ZDKzUorhcGp/u8PkPC/e0zoHSTvRh7TRkGX3Lm7iOQ== - dependencies: - "@algolia/requester-common" "4.22.1" - "@algolia/transporter" "4.22.1" - -"@algolia/client-personalization@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.22.1.tgz#ff088d797648224fb582e9fe5828f8087835fa3d" - integrity sha512-sl+/klQJ93+4yaqZ7ezOttMQ/nczly/3GmgZXJ1xmoewP5jmdP/X/nV5U7EHHH3hCUEHeN7X1nsIhGPVt9E1cQ== - dependencies: - "@algolia/client-common" "4.22.1" - "@algolia/requester-common" "4.22.1" - "@algolia/transporter" "4.22.1" - -"@algolia/client-search@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.22.1.tgz#508cc6ab3d1f4e9c02735a630d4dff6fbb8514a2" - integrity sha512-yb05NA4tNaOgx3+rOxAmFztgMTtGBi97X7PC3jyNeGiwkAjOZc2QrdZBYyIdcDLoI09N0gjtpClcackoTN0gPA== - dependencies: - "@algolia/client-common" "4.22.1" - "@algolia/requester-common" "4.22.1" - "@algolia/transporter" "4.22.1" - -"@algolia/logger-common@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.22.1.tgz#79cf4cd295de0377a94582c6aaac59b1ded731d9" - integrity sha512-OnTFymd2odHSO39r4DSWRFETkBufnY2iGUZNrMXpIhF5cmFE8pGoINNPzwg02QLBlGSaLqdKy0bM8S0GyqPLBg== - -"@algolia/logger-console@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.22.1.tgz#0355345f6940f67aaa78ae9b81c06e44e49f2336" - integrity sha512-O99rcqpVPKN1RlpgD6H3khUWylU24OXlzkavUAMy6QZd1776QAcauE3oP8CmD43nbaTjBexZj2nGsBH9Tc0FVA== - dependencies: - "@algolia/logger-common" "4.22.1" - -"@algolia/requester-browser-xhr@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.22.1.tgz#f04df6fe9690a071b267c77d26b83a3be9280361" - integrity sha512-dtQGYIg6MteqT1Uay3J/0NDqD+UciHy3QgRbk7bNddOJu+p3hzjTRYESqEnoX/DpEkaNYdRHUKNylsqMpgwaEw== - dependencies: - "@algolia/requester-common" "4.22.1" - -"@algolia/requester-common@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.22.1.tgz#27be35f3718aafcb6b388ff9c3aa2defabd559ff" - integrity sha512-dgvhSAtg2MJnR+BxrIFqlLtkLlVVhas9HgYKMk2Uxiy5m6/8HZBL40JVAMb2LovoPFs9I/EWIoFVjOrFwzn5Qg== - -"@algolia/requester-node-http@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.22.1.tgz#589a6fa828ad0f325e727a6fcaf4e1a2343cc62b" - integrity sha512-JfmZ3MVFQkAU+zug8H3s8rZ6h0ahHZL/SpMaSasTCGYR5EEJsCc8SI5UZ6raPN2tjxa5bxS13BRpGSBUens7EA== - dependencies: - "@algolia/requester-common" "4.22.1" - -"@algolia/transporter@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.22.1.tgz#8843841b857dc021668f31647aa557ff19cd9cb1" - integrity sha512-kzWgc2c9IdxMa3YqA6TN0NW5VrKYYW/BELIn7vnLyn+U/RFdZ4lxxt9/8yq3DKV5snvoDzzO4ClyejZRdV3lMQ== - dependencies: - "@algolia/cache-common" "4.22.1" - "@algolia/logger-common" "4.22.1" - "@algolia/requester-common" "4.22.1" - -"@alloc/quick-lru@^5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" - integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== - -"@ampproject/remapping@^2.2.0": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" - integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== - dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" - -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.23.5": - version "7.23.5" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" - integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== - dependencies: - "@babel/highlight" "^7.23.4" - chalk "^2.4.2" - -"@babel/code-frame@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-8.0.0-alpha.6.tgz#dae7c177a7aa35d4e67f2442f9bf5e4e548827bc" - integrity sha512-w29LUiFqVMe+Ybp/+xtQKAp6rpxjHxtCW909I0OMHvFKzrGB0K+1yQGRzXhTqrd4f7ZRRgzkAxdr0EOEbHt3dA== - dependencies: - "@babel/highlight" "^8.0.0-alpha.6" - chalk "^5.3.0" - -"@babel/compat-data@^7.23.5": - version "7.23.5" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98" - integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== - -"@babel/compat-data@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-8.0.0-alpha.6.tgz#619acf0bb8ca27a8ec94067b53b6c0351a88bd68" - integrity sha512-Oam3YCPXzA5nII8+uqO1nur3PxJF3CcQpFJXHPAXJHhjIKvABCodYacDk8x/FQKBmBiUaMqGlEQ04m3H3rWqlA== - -"@babel/core@^7.11.6", "@babel/core@^7.12.3": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.9.tgz#b028820718000f267870822fec434820e9b1e4d1" - integrity sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw== - dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.23.5" - "@babel/generator" "^7.23.6" - "@babel/helper-compilation-targets" "^7.23.6" - "@babel/helper-module-transforms" "^7.23.3" - "@babel/helpers" "^7.23.9" - "@babel/parser" "^7.23.9" - "@babel/template" "^7.23.9" - "@babel/traverse" "^7.23.9" - "@babel/types" "^7.23.9" - convert-source-map "^2.0.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.3" - semver "^6.3.1" - -"@babel/core@^8.0.0-alpha.1": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-8.0.0-alpha.6.tgz#8e3d485b959d14713edf151ef4d2a24bf680a4cd" - integrity sha512-7wfyFNMnB3VkjKrhgLKEbKHs776ZMzaZmtCI9JlmuvoYje17DPxbbWE+1OR2Yzq0K5wK0KtrNUKUCptpH3UMwg== - dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^8.0.0-alpha.6" - "@babel/generator" "^8.0.0-alpha.6" - "@babel/helper-compilation-targets" "^8.0.0-alpha.6" - "@babel/helper-module-transforms" "^8.0.0-alpha.6" - "@babel/helpers" "^8.0.0-alpha.6" - "@babel/parser" "^8.0.0-alpha.6" - "@babel/template" "^8.0.0-alpha.6" - "@babel/traverse" "^8.0.0-alpha.6" - "@babel/types" "^8.0.0-alpha.6" - convert-source-map "^2.0.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.3" - semver "^7.3.4" - -"@babel/eslint-parser@^8.0.0-alpha.1": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-8.0.0-alpha.6.tgz#086a21b3b16c799eb8e58467a211e69d4feda307" - integrity sha512-DppLATH1QipJGifK8x9y5ljMJQe0C+sbS5/Ph1ak0PSdp5mtTojnqv/r0D3UlW9GS/xvgYxrg9GobVksd5HWeg== - dependencies: - eslint-scope "^7.1.1" - eslint-visitor-keys "^3.3.0" - semver "^7.3.4" - -"@babel/generator@^7.23.6", "@babel/generator@^7.7.2": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e" - integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw== - dependencies: - "@babel/types" "^7.23.6" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" - jsesc "^2.5.1" - -"@babel/generator@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-8.0.0-alpha.6.tgz#c049e54e880133a6b63dbcdde1c59055c7232d14" - integrity sha512-aw/8HUdJOWtazTC5LcpQ2KWze1tcxyxgVFCwgkm0ehtfb62LvxU1GAeiay2nZUJIo3EtbMPlp7Pn9CEa5YRn+w== - dependencies: - "@babel/types" "^8.0.0-alpha.6" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" - jsesc "^3.0.2" - -"@babel/helper-compilation-targets@^7.23.6": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" - integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== - dependencies: - "@babel/compat-data" "^7.23.5" - "@babel/helper-validator-option" "^7.23.5" - browserslist "^4.22.2" - lru-cache "^5.1.1" - semver "^6.3.1" - -"@babel/helper-compilation-targets@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-8.0.0-alpha.6.tgz#aed29d5b0002b7126ee7b0d58b362a6a64df1d48" - integrity sha512-TWNrF91LNs9/0bIg0xlRy/CzUMfjVmp/J5EzvbVp3Jvow+PzWKs/aACersP/WBu3dom1L+K6NS8+V1Howvo/Hw== - dependencies: - "@babel/compat-data" "^8.0.0-alpha.6" - "@babel/helper-validator-option" "^8.0.0-alpha.6" - browserslist "^4.22.2" - lru-cache "^7.14.1" - semver "^7.3.4" - -"@babel/helper-environment-visitor@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" - integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== - -"@babel/helper-environment-visitor@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-8.0.0-alpha.6.tgz#bbf006cf58538dbcdc75d1c1430f9df45db18220" - integrity sha512-R4eQAqH3TgXU1BfWZ96VvCqMWbVz3mz7LSlu093VbmWeEHwYbJb2EQp8jOfNmQJuZfiTz+ExJ6MS+AEbbDX3Lw== - -"@babel/helper-function-name@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" - integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== - dependencies: - "@babel/template" "^7.22.15" - "@babel/types" "^7.23.0" - -"@babel/helper-function-name@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-8.0.0-alpha.6.tgz#2dbd95a030a7faf630382ca044da7480a04e6b49" - integrity sha512-CChtgzZPQfKqBBxYdtiLahCRXbJafZmjh8E7rSGUtqQcwI7asbnuA7OJIXKh4avP15hI48JbHoHbTOHQ+IlRfw== - dependencies: - "@babel/template" "^8.0.0-alpha.6" - "@babel/types" "^8.0.0-alpha.6" - -"@babel/helper-hoist-variables@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" - integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-hoist-variables@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-8.0.0-alpha.6.tgz#bff016883d186cb462d3bc2ce0fedf7130e044dc" - integrity sha512-z3ifm6Ad4ASTeRpDSDsct9fhsoO6RWXGu1utVfrujJezPpC4jvU3FbNFXxVRIGd/2kpYxbVAR8vF0Pq8g7ZbUg== - dependencies: - "@babel/types" "^8.0.0-alpha.6" - -"@babel/helper-module-imports@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" - integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== - dependencies: - "@babel/types" "^7.22.15" - -"@babel/helper-module-imports@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-8.0.0-alpha.6.tgz#06d0b7bf90c378435d1ee9796b14e5ad15b85c7f" - integrity sha512-/BZ4df4w4904Rp/ZCvjziO2I29uHmLgttC5OFxla5AjNu9eaINhSLSWGYK/4PhY3zCa2cc7an/BjS59jrzmjMA== - dependencies: - "@babel/types" "^8.0.0-alpha.6" - -"@babel/helper-module-transforms@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" - integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ== - dependencies: - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-module-imports" "^7.22.15" - "@babel/helper-simple-access" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/helper-validator-identifier" "^7.22.20" - -"@babel/helper-module-transforms@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-8.0.0-alpha.6.tgz#4e628a62e394510e4ff2b40a8e7d9ae2ff784b59" - integrity sha512-NRG3LB9AXng09w+6ix1BV+45yoKdqBO64fpqpzUYk7rCzvpyxMSVvnJfdxmJnxgobd5/bkMS4UB7ejEj3rWm3g== - dependencies: - "@babel/helper-environment-visitor" "^8.0.0-alpha.6" - "@babel/helper-module-imports" "^8.0.0-alpha.6" - "@babel/helper-simple-access" "^8.0.0-alpha.6" - "@babel/helper-split-export-declaration" "^8.0.0-alpha.6" - "@babel/helper-validator-identifier" "^8.0.0-alpha.6" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" - integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== - -"@babel/helper-simple-access@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" - integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-simple-access@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-8.0.0-alpha.6.tgz#dc55bf5bb3708e654d6d75248005e147fe9bbf77" - integrity sha512-MPgqNIbMC9uy18pPn+9Z3Uk9WzDRrAAFl4h/lwm91bZ1KqN+EBy2J/VRO/9D/ppfxjwJ8bGma6zyqP6WJMe/1Q== - dependencies: - "@babel/types" "^8.0.0-alpha.6" - -"@babel/helper-split-export-declaration@^7.22.6": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" - integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-split-export-declaration@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-8.0.0-alpha.6.tgz#f3e99b729b9a809f5dba6f3e9ed191047a246561" - integrity sha512-myKGzQaQRWiaP6IhdZQapzzHylz8JMzX2z6No8PDfDLz0tq6reD8dZF6lP5std9Bc05BNOQ2KgugX/ICPEsclg== - dependencies: - "@babel/types" "^8.0.0-alpha.6" - -"@babel/helper-string-parser@^7.23.4": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" - integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== - -"@babel/helper-string-parser@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-8.0.0-alpha.6.tgz#92f09a33448c8ee7e5636f8728a4db68b0149d07" - integrity sha512-v5PJQdsB76ivdtM+1iwKjKiWyyq62VGB7UoiG/1mE7XO2nxO9+RxRdRmaUkZ/BnoGR530fYrJ8HcKGaBQ6qkQA== - -"@babel/helper-validator-identifier@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" - integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== - -"@babel/helper-validator-identifier@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-alpha.6.tgz#d1224fb1968cd9be24810e8f7782f806c51075eb" - integrity sha512-a0YnAx4TJCgds07M2v0A3WeyZUfls2YOgNHCb1akGKlz5KdmTCqf2N2hw86YhjY6oDqb6Omb8gFKz/3TOKRevQ== - -"@babel/helper-validator-option@^7.23.5": - version "7.23.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" - integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== - -"@babel/helper-validator-option@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-8.0.0-alpha.6.tgz#de6c868fb5fd7c5e914dd944a04581c6efa7d468" - integrity sha512-jHHD6msYmhaMHHLGLbUVyy9UPyAOSvjBTF3qXuBnTg1ZRyuh5z+srbpfagh1tv8igfq15h+PPexA0kGqOnzi8Q== - -"@babel/helpers@^7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.9.tgz#c3e20bbe7f7a7e10cb9b178384b4affdf5995c7d" - integrity sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ== - dependencies: - "@babel/template" "^7.23.9" - "@babel/traverse" "^7.23.9" - "@babel/types" "^7.23.9" - -"@babel/helpers@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-8.0.0-alpha.6.tgz#c035394f6a3fc9d0634e23fbd11557a7bdb6f014" - integrity sha512-F3xxu2ZhsE+zcFd/cMD66wxkytXhnD8U9SHfY4H4bbMPRKks+sDxiHk2yhbkCQU1OIWm7l8PuA3lZ/B/hUlKSg== - dependencies: - "@babel/template" "^8.0.0-alpha.6" - "@babel/traverse" "^8.0.0-alpha.6" - "@babel/types" "^8.0.0-alpha.6" - -"@babel/highlight@^7.23.4": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" - integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== - dependencies: - "@babel/helper-validator-identifier" "^7.22.20" - chalk "^2.4.2" - js-tokens "^4.0.0" - -"@babel/highlight@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-8.0.0-alpha.6.tgz#bc909c182dd13530bfbfbf6d7cf77dd10faf1f84" - integrity sha512-YzJ5blZXak7Gk6ZgV9a4B9bAkGzl594ZxmBaFFZkstA5hE1iOBTlUW4tkGKz2uomL7FLLW4qHJF3QjQLHKi7iw== - dependencies: - "@babel/helper-validator-identifier" "^8.0.0-alpha.6" - chalk "^5.3.0" - js-tokens "^8.0.0" - -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" - integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== - -"@babel/parser@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-8.0.0-alpha.6.tgz#8fbbae764253efd6b7c84b697978b16d573f4cf7" - integrity sha512-AJ6gWeO6hR2NXitqoRykzmDACsSWHWWDIsKXMTr8FOAte0hWrPeinjftra5iKT3R6pcTOb1Z1UKxlUn6yWHAmg== - -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-bigint@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" - integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.8.3": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-import-meta@^7.8.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" - integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-jsx@^7.7.2": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz#8f2e4f8a9b5f9aa16067e142c1ac9cd9f810f473" - integrity sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.8.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-top-level-await@^7.8.3": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-typescript@^7.7.2": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz#24f460c85dbbc983cd2b9c4994178bcc01df958f" - integrity sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/runtime@^7.23.2", "@babel/runtime@^7.23.8": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7" - integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw== - dependencies: - regenerator-runtime "^0.14.0" - -"@babel/template@^7.22.15", "@babel/template@^7.23.9", "@babel/template@^7.3.3": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.23.9.tgz#f881d0487cba2828d3259dcb9ef5005a9731011a" - integrity sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA== - dependencies: - "@babel/code-frame" "^7.23.5" - "@babel/parser" "^7.23.9" - "@babel/types" "^7.23.9" - -"@babel/template@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-8.0.0-alpha.6.tgz#297ed62932e9b27b604598692694c9619783963d" - integrity sha512-K/EP33UFTLdopfhsy1wraInMn0MfhwyAB8+qMCPwYHu/U1mpox43i7z6IrnbtYHPH8RxUWrolVHRt73qiGmIqQ== - dependencies: - "@babel/code-frame" "^8.0.0-alpha.6" - "@babel/parser" "^8.0.0-alpha.6" - "@babel/types" "^8.0.0-alpha.6" - -"@babel/traverse@^7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.9.tgz#2f9d6aead6b564669394c5ce0f9302bb65b9d950" - integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg== - dependencies: - "@babel/code-frame" "^7.23.5" - "@babel/generator" "^7.23.6" - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-function-name" "^7.23.0" - "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.23.9" - "@babel/types" "^7.23.9" - debug "^4.3.1" - globals "^11.1.0" - -"@babel/traverse@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-8.0.0-alpha.6.tgz#0ab8594d77ae975b2c37e83fbdc28693f8e70c2b" - integrity sha512-EjUMbnUrSRyi9+h5GRhJwvkkwEZRzIKhe/dk6NaT6kHt8aCPcGKUf7ZilESHPtCUvI8jgx5ONDQWqxVlRoaqgw== - dependencies: - "@babel/code-frame" "^8.0.0-alpha.6" - "@babel/generator" "^8.0.0-alpha.6" - "@babel/helper-environment-visitor" "^8.0.0-alpha.6" - "@babel/helper-function-name" "^8.0.0-alpha.6" - "@babel/helper-hoist-variables" "^8.0.0-alpha.6" - "@babel/helper-split-export-declaration" "^8.0.0-alpha.6" - "@babel/parser" "^8.0.0-alpha.6" - "@babel/types" "^8.0.0-alpha.6" - debug "^4.3.1" - globals "^13.5.0" - -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.23.9", "@babel/types@^7.3.3": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.9.tgz#1dd7b59a9a2b5c87f8b41e52770b5ecbf492e002" - integrity sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q== - dependencies: - "@babel/helper-string-parser" "^7.23.4" - "@babel/helper-validator-identifier" "^7.22.20" - to-fast-properties "^2.0.0" - -"@babel/types@^8.0.0-alpha.6": - version "8.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-8.0.0-alpha.6.tgz#7d3065ab4518e3f65205fccc905c91ca6a44206b" - integrity sha512-JIcFklQmKMM1TD+1bEyB1QQq450YUoKe42YzdS1IIncZiqfnR0o6x8U+QaW75T9zCoTzy3JsLNBgZ6ebHWLwlA== - dependencies: - "@babel/helper-string-parser" "^8.0.0-alpha.6" - "@babel/helper-validator-identifier" "^8.0.0-alpha.6" - to-fast-properties "^3.0.0" - -"@bcoe/v8-coverage@^0.2.3": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" - integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== - -"@braintree/sanitize-url@^6.0.1": - version "6.0.4" - resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783" - integrity sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A== - -"@docsearch/css@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.5.2.tgz#610f47b48814ca94041df969d9fcc47b91fc5aac" - integrity sha512-SPiDHaWKQZpwR2siD0KQUwlStvIAnEyK6tAE2h2Wuoq8ue9skzhlyVQ1ddzOxX6khULnAALDiR/isSF3bnuciA== - -"@docsearch/react@3": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.5.2.tgz#2e6bbee00eb67333b64906352734da6aef1232b9" - integrity sha512-9Ahcrs5z2jq/DcAvYtvlqEBHImbm4YJI8M9y0x6Tqg598P40HTEkX7hsMcIuThI+hTFxRGZ9hll0Wygm2yEjng== - dependencies: - "@algolia/autocomplete-core" "1.9.3" - "@algolia/autocomplete-preset-algolia" "1.9.3" - "@docsearch/css" "3.5.2" - algoliasearch "^4.19.1" - -"@emnapi/runtime@^0.45.0": - version "0.45.0" - resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-0.45.0.tgz#e754de04c683263f34fd0c7f32adfe718bbe4ddd" - integrity sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w== - dependencies: - tslib "^2.4.0" - -"@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" - integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== - dependencies: - eslint-visitor-keys "^3.3.0" - -"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.0", "@eslint-community/regexpp@^4.6.1": - version "4.10.0" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" - integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== - -"@eslint/eslintrc@^2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" - integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^9.6.0" - globals "^13.19.0" - ignore "^5.2.0" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.1.2" - strip-json-comments "^3.1.1" - -"@eslint/eslintrc@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.0.1.tgz#ee628808e945cd7782df05ce50eece525cb48e7a" - integrity sha512-xXm39r1RgOSmPCqlhn+E10KPJ7JKrpuBwsAVw/++5dS/Sa4GAi0smby0r0wfTN4gNpkk9iij2hssJMXHSmQ89w== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^10.0.1" - globals "^13.19.0" - ignore "^5.2.0" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.1.2" - strip-json-comments "^3.1.1" - -"@eslint/js@8.56.0", "@eslint/js@^8.35.0": - version "8.56.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b" - integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== - -"@headlessui/react@^1.7.17": - version "1.7.18" - resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.18.tgz#30af4634d2215b2ca1aa29d07f33d02bea82d9d7" - integrity sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ== - dependencies: - "@tanstack/react-virtual" "^3.0.0-beta.60" - client-only "^0.0.1" - -"@humanwhocodes/config-array@^0.11.13": - version "0.11.14" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" - integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== - dependencies: - "@humanwhocodes/object-schema" "^2.0.2" - debug "^4.3.1" - minimatch "^3.0.5" - -"@humanwhocodes/module-importer@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" - integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== - -"@humanwhocodes/object-schema@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" - integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== - -"@img/sharp-darwin-arm64@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.2.tgz#0a52a82c2169112794dac2c71bfba9e90f7c5bd1" - integrity sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w== - optionalDependencies: - "@img/sharp-libvips-darwin-arm64" "1.0.1" - -"@img/sharp-darwin-x64@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.2.tgz#982e26bb9d38a81f75915c4032539aed621d1c21" - integrity sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg== - optionalDependencies: - "@img/sharp-libvips-darwin-x64" "1.0.1" - -"@img/sharp-libvips-darwin-arm64@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.1.tgz#81e83ffc2c497b3100e2f253766490f8fad479cd" - integrity sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw== - -"@img/sharp-libvips-darwin-x64@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.1.tgz#fc1fcd9d78a178819eefe2c1a1662067a83ab1d6" - integrity sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog== - -"@img/sharp-libvips-linux-arm64@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.1.tgz#26eb8c556a9b0db95f343fc444abc3effb67ebcf" - integrity sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA== - -"@img/sharp-libvips-linux-arm@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.1.tgz#2a377b959ff7dd6528deee486c25461296a4fa8b" - integrity sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ== - -"@img/sharp-libvips-linux-s390x@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.1.tgz#af28ac9ba929204467ecdf843330d791e9421e10" - integrity sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ== - -"@img/sharp-libvips-linux-x64@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.1.tgz#4273d182aa51912e655e1214ea47983d7c1f7f8d" - integrity sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw== - -"@img/sharp-libvips-linuxmusl-arm64@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.1.tgz#d150c92151cea2e8d120ad168b9c358d09c77ce8" - integrity sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg== - -"@img/sharp-libvips-linuxmusl-x64@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.1.tgz#e297c1a4252c670d93b0f9e51fca40a7a5b6acfd" - integrity sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw== - -"@img/sharp-linux-arm64@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.2.tgz#af3409f801a9bee1d11d0c7e971dcd6180f80022" - integrity sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew== - optionalDependencies: - "@img/sharp-libvips-linux-arm64" "1.0.1" - -"@img/sharp-linux-arm@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.2.tgz#181f7466e6ac074042a38bfb679eb82505e17083" - integrity sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA== - optionalDependencies: - "@img/sharp-libvips-linux-arm" "1.0.1" - -"@img/sharp-linux-s390x@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.2.tgz#9c171f49211f96fba84410b3e237b301286fa00f" - integrity sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA== - optionalDependencies: - "@img/sharp-libvips-linux-s390x" "1.0.1" - -"@img/sharp-linux-x64@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.2.tgz#b956dfc092adc58c2bf0fae2077e6f01a8b2d5d7" - integrity sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A== - optionalDependencies: - "@img/sharp-libvips-linux-x64" "1.0.1" - -"@img/sharp-linuxmusl-arm64@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.2.tgz#10e0ec5a79d1234c6a71df44c9f3b0bef0bc0f15" - integrity sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA== - optionalDependencies: - "@img/sharp-libvips-linuxmusl-arm64" "1.0.1" - -"@img/sharp-linuxmusl-x64@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.2.tgz#29e0030c24aa27c38201b1fc84e3d172899fcbe0" - integrity sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A== - optionalDependencies: - "@img/sharp-libvips-linuxmusl-x64" "1.0.1" - -"@img/sharp-wasm32@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.2.tgz#38d7c740a22de83a60ad1e6bcfce17462b0d4230" - integrity sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ== - dependencies: - "@emnapi/runtime" "^0.45.0" - -"@img/sharp-win32-ia32@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.2.tgz#09456314e223f68e5417c283b45c399635c16202" - integrity sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g== - -"@img/sharp-win32-x64@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz#148e96dfd6e68747da41a311b9ee4559bb1b1471" - integrity sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg== - -"@isaacs/cliui@^8.0.2": - version "8.0.2" - resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" - integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== - dependencies: - string-width "^5.1.2" - string-width-cjs "npm:string-width@^4.2.0" - strip-ansi "^7.0.1" - strip-ansi-cjs "npm:strip-ansi@^6.0.1" - wrap-ansi "^8.1.0" - wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" - -"@istanbuljs/load-nyc-config@^1.0.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" - integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== - dependencies: - camelcase "^5.3.1" - find-up "^4.1.0" - get-package-type "^0.1.0" - js-yaml "^3.13.1" - resolve-from "^5.0.0" - -"@istanbuljs/schema@^0.1.2": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" - integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== - -"@jest/console@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" - integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== - dependencies: - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - jest-message-util "^29.7.0" - jest-util "^29.7.0" - slash "^3.0.0" - -"@jest/core@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f" - integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== - dependencies: - "@jest/console" "^29.7.0" - "@jest/reporters" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - ansi-escapes "^4.2.1" - chalk "^4.0.0" - ci-info "^3.2.0" - exit "^0.1.2" - graceful-fs "^4.2.9" - jest-changed-files "^29.7.0" - jest-config "^29.7.0" - jest-haste-map "^29.7.0" - jest-message-util "^29.7.0" - jest-regex-util "^29.6.3" - jest-resolve "^29.7.0" - jest-resolve-dependencies "^29.7.0" - jest-runner "^29.7.0" - jest-runtime "^29.7.0" - jest-snapshot "^29.7.0" - jest-util "^29.7.0" - jest-validate "^29.7.0" - jest-watcher "^29.7.0" - micromatch "^4.0.4" - pretty-format "^29.7.0" - slash "^3.0.0" - strip-ansi "^6.0.0" - -"@jest/environment@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" - integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== - dependencies: - "@jest/fake-timers" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - jest-mock "^29.7.0" - -"@jest/expect-utils@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" - integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== - dependencies: - jest-get-type "^29.6.3" - -"@jest/expect@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2" - integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== - dependencies: - expect "^29.7.0" - jest-snapshot "^29.7.0" - -"@jest/fake-timers@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" - integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== +"@antfu/install-pkg@^0.4.0": + version "0.4.1" + resolved "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.4.1.tgz" + integrity sha512-T7yB5QNG29afhWVkVq7XeIMBa5U/vs9mX69YqayXypPRmYzUmzwnYltplHmPtZ4HPCn+sQKeXW8I47wCbuBOjw== dependencies: - "@jest/types" "^29.6.3" - "@sinonjs/fake-timers" "^10.0.2" - "@types/node" "*" - jest-message-util "^29.7.0" - jest-mock "^29.7.0" - jest-util "^29.7.0" + package-manager-detector "^0.2.0" + tinyexec "^0.3.0" -"@jest/globals@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" - integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== - dependencies: - "@jest/environment" "^29.7.0" - "@jest/expect" "^29.7.0" - "@jest/types" "^29.6.3" - jest-mock "^29.7.0" +"@antfu/utils@^0.7.10": + version "0.7.10" + resolved "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz" + integrity sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww== -"@jest/reporters@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" - integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== - dependencies: - "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - "@jridgewell/trace-mapping" "^0.3.18" - "@types/node" "*" - chalk "^4.0.0" - collect-v8-coverage "^1.0.0" - exit "^0.1.2" - glob "^7.1.3" - graceful-fs "^4.2.9" - istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^6.0.0" - istanbul-lib-report "^3.0.0" - istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.1.3" - jest-message-util "^29.7.0" - jest-util "^29.7.0" - jest-worker "^29.7.0" - slash "^3.0.0" - string-length "^4.0.1" - strip-ansi "^6.0.0" - v8-to-istanbul "^9.0.1" - -"@jest/schemas@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" - integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== - dependencies: - "@sinclair/typebox" "^0.27.8" - -"@jest/source-map@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4" - integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== - dependencies: - "@jridgewell/trace-mapping" "^0.3.18" - callsites "^3.0.0" - graceful-fs "^4.2.9" - -"@jest/test-result@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.7.0.tgz#8db9a80aa1a097bb2262572686734baed9b1657c" - integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== +"@braintree/sanitize-url@^7.0.1": + version "7.1.0" + resolved "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.0.tgz" + integrity sha512-o+UlMLt49RvtCASlOMW0AkHnabN9wR9rwCCherxO0yG4Npy34GkvrAqdXQvrhNs+jh+gkK8gB8Lf05qL/O7KWg== + +"@chevrotain/cst-dts-gen@11.0.3": + version "11.0.3" + resolved "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz" + integrity sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ== + dependencies: + "@chevrotain/gast" "11.0.3" + "@chevrotain/types" "11.0.3" + lodash-es "4.17.21" + +"@chevrotain/gast@11.0.3": + version "11.0.3" + resolved "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz" + integrity sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q== + dependencies: + "@chevrotain/types" "11.0.3" + lodash-es "4.17.21" + +"@chevrotain/regexp-to-ast@11.0.3": + version "11.0.3" + resolved "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz" + integrity sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA== + +"@chevrotain/types@11.0.3": + version "11.0.3" + resolved "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz" + integrity sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ== + +"@chevrotain/utils@11.0.3": + version "11.0.3" + resolved "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz" + integrity sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ== + +"@docsearch/react@^3.6.1": + version "3.8.0" + resolved "file:../node_modules/.pnpm/@docsearch+react@3.8.0_@algolia+client-search@5.15.0_@types+react@18.3.12_react-dom@18.3.1_re_z3ibnkp2dpsff4fmbjybcokbae/node_modules/@docsearch/react" + dependencies: + "@algolia/autocomplete-core" "1.17.7" + "@algolia/autocomplete-preset-algolia" "1.17.7" + "@docsearch/css" "3.8.0" + algoliasearch "^5.12.0" + +"@floating-ui/core@^1.6.0": + version "1.6.8" + resolved "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz" + integrity sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA== + dependencies: + "@floating-ui/utils" "^0.2.8" + +"@floating-ui/dom@^1.0.0": + version "1.6.12" + resolved "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz" + integrity sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w== + dependencies: + "@floating-ui/core" "^1.6.0" + "@floating-ui/utils" "^0.2.8" + +"@floating-ui/react-dom@^2.1.2": + version "2.1.2" + resolved "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz" + integrity sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A== dependencies: - "@jest/console" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/istanbul-lib-coverage" "^2.0.0" - collect-v8-coverage "^1.0.0" + "@floating-ui/dom" "^1.0.0" -"@jest/test-sequencer@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz#6cef977ce1d39834a3aea887a1726628a6f072ce" - integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== +"@floating-ui/react@^0.26.16": + version "0.26.28" + resolved "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz" + integrity sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw== dependencies: - "@jest/test-result" "^29.7.0" - graceful-fs "^4.2.9" - jest-haste-map "^29.7.0" - slash "^3.0.0" + "@floating-ui/react-dom" "^2.1.2" + "@floating-ui/utils" "^0.2.8" + tabbable "^6.0.0" -"@jest/transform@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" - integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== - dependencies: - "@babel/core" "^7.11.6" - "@jest/types" "^29.6.3" - "@jridgewell/trace-mapping" "^0.3.18" - babel-plugin-istanbul "^6.1.1" - chalk "^4.0.0" - convert-source-map "^2.0.0" - fast-json-stable-stringify "^2.1.0" - graceful-fs "^4.2.9" - jest-haste-map "^29.7.0" - jest-regex-util "^29.6.3" - jest-util "^29.7.0" - micromatch "^4.0.4" - pirates "^4.0.4" - slash "^3.0.0" - write-file-atomic "^4.0.2" - -"@jest/types@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" - integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== - dependencies: - "@jest/schemas" "^29.6.3" - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^17.0.8" - chalk "^4.0.0" +"@floating-ui/utils@^0.2.8": + version "0.2.8" + resolved "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz" + integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig== -"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" - integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== +"@formatjs/intl-localematcher@^0.5.4": + version "0.5.8" + resolved "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.8.tgz" + integrity sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg== dependencies: - "@jridgewell/set-array" "^1.0.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/resolve-uri@^3.1.0": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" - integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== - -"@jridgewell/set-array@^1.0.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" - integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== - -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": - version "1.4.15" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" - integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + tslib "2" -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.22" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz#72a621e5de59f5f1ef792d0793a82ee20f645e4c" - integrity sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw== +"@headlessui/react@^2.1.2": + version "2.2.0" + resolved "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz" + integrity sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ== dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" + "@floating-ui/react" "^0.26.16" + "@react-aria/focus" "^3.17.1" + "@react-aria/interactions" "^3.21.3" + "@tanstack/react-virtual" "^3.8.1" -"@mdx-js/mdx@^2.2.1", "@mdx-js/mdx@^2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-2.3.0.tgz#d65d8c3c28f3f46bb0e7cb3bf7613b39980671a9" - integrity sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA== +"@iconify/types@^2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz" + integrity sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg== + +"@iconify/utils@^2.1.32": + version "2.1.33" + resolved "https://registry.npmjs.org/@iconify/utils/-/utils-2.1.33.tgz" + integrity sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw== + dependencies: + "@antfu/install-pkg" "^0.4.0" + "@antfu/utils" "^0.7.10" + "@iconify/types" "^2.0.0" + debug "^4.3.6" + kolorist "^1.8.0" + local-pkg "^0.5.0" + mlly "^1.7.1" + +"@mdx-js/mdx@^3.0.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.0.tgz" + integrity sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw== dependencies: + "@types/estree" "^1.0.0" "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" "@types/mdx" "^2.0.0" - estree-util-build-jsx "^2.0.0" - estree-util-is-identifier-name "^2.0.0" - estree-util-to-js "^1.1.0" + collapse-white-space "^2.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + estree-util-scope "^1.0.0" estree-walker "^3.0.0" - hast-util-to-estree "^2.0.0" - markdown-extensions "^1.0.0" - periscopic "^3.0.0" - remark-mdx "^2.0.0" - remark-parse "^10.0.0" - remark-rehype "^10.0.0" - unified "^10.0.0" - unist-util-position-from-estree "^1.0.0" - unist-util-stringify-position "^3.0.0" - unist-util-visit "^4.0.0" - vfile "^5.0.0" - -"@mdx-js/react@^2.2.1", "@mdx-js/react@^2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-2.3.0.tgz#4208bd6d70f0d0831def28ef28c26149b03180b3" - integrity sha512-zQH//gdOmuu7nt2oJR29vFhDv88oGPmVw6BggmrHeMI+xgEkp1B2dX9/bMBSYtK0dyLX/aOmesKS09g222K1/g== + hast-util-to-jsx-runtime "^2.0.0" + markdown-extensions "^2.0.0" + recma-build-jsx "^1.0.0" + recma-jsx "^1.0.0" + recma-stringify "^1.0.0" + rehype-recma "^1.0.0" + remark-mdx "^3.0.0" + remark-parse "^11.0.0" + remark-rehype "^11.0.0" + source-map "^0.7.0" + unified "^11.0.0" + unist-util-position-from-estree "^2.0.0" + unist-util-stringify-position "^4.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + +"@mdx-js/react@^3.0.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz" + integrity sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ== dependencies: "@types/mdx" "^2.0.0" - "@types/react" ">=16" - -"@napi-rs/simple-git-android-arm-eabi@0.1.16": - version "0.1.16" - resolved "https://registry.yarnpkg.com/@napi-rs/simple-git-android-arm-eabi/-/simple-git-android-arm-eabi-0.1.16.tgz#36b752f84a7e75a9dada3d8b307817f0b015a57d" - integrity sha512-dbrCL0Pl5KZG7x7tXdtVsA5CO6At5ohDX3myf5xIYn9kN4jDFxsocl8bNt6Vb/hZQoJd8fI+k5VlJt+rFhbdVw== - -"@napi-rs/simple-git-android-arm64@0.1.16": - version "0.1.16" - resolved "https://registry.yarnpkg.com/@napi-rs/simple-git-android-arm64/-/simple-git-android-arm64-0.1.16.tgz#f84d9e2fdae91bb810b55ffc30a42ce5fe020c76" - integrity sha512-xYz+TW5J09iK8SuTAKK2D5MMIsBUXVSs8nYp7HcMi8q6FCRO7yJj96YfP9PvKsc/k64hOyqGmL5DhCzY9Cu1FQ== -"@napi-rs/simple-git-darwin-arm64@0.1.16": - version "0.1.16" - resolved "https://registry.yarnpkg.com/@napi-rs/simple-git-darwin-arm64/-/simple-git-darwin-arm64-0.1.16.tgz#8d995a920146c320bf13b32d1b1654f44beaa16b" - integrity sha512-XfgsYqxhUE022MJobeiX563TJqyQyX4FmYCnqrtJwAfivESVeAJiH6bQIum8dDEYMHXCsG7nL8Ok0Dp8k2m42g== +"@mermaid-js/parser@^0.3.0": + version "0.3.0" + resolved "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.3.0.tgz" + integrity sha512-HsvL6zgE5sUPGgkIDlmAWR1HTNHz2Iy11BAWPTa4Jjabkpguy4Ze2gzfLrg6pdRuBvFwgUYyxiaNqZwrEEXepA== + dependencies: + langium "3.0.0" "@napi-rs/simple-git-darwin-x64@0.1.16": version "0.1.16" - resolved "https://registry.yarnpkg.com/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.16.tgz#7cc7155392c62f885d248af5f720e108d0aad2b5" + resolved "https://registry.npmjs.org/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.16.tgz" integrity sha512-tkEVBhD6vgRCbeWsaAQqM3bTfpIVGeitamPPRVSbsq8qgzJ5Dx6ZedH27R7KSsA/uao7mZ3dsrNLXbu1Wy5MzA== -"@napi-rs/simple-git-linux-arm-gnueabihf@0.1.16": - version "0.1.16" - resolved "https://registry.yarnpkg.com/@napi-rs/simple-git-linux-arm-gnueabihf/-/simple-git-linux-arm-gnueabihf-0.1.16.tgz#d5135543d372e0571d7c19928e75751eb407d7dd" - integrity sha512-R6VAyNnp/yRaT7DV1Ao3r67SqTWDa+fNq2LrNy0Z8gXk2wB9ZKlrxFtLPE1WSpWknWtyRDLpRlsorh7Evk7+7w== - -"@napi-rs/simple-git-linux-arm64-gnu@0.1.16": - version "0.1.16" - resolved "https://registry.yarnpkg.com/@napi-rs/simple-git-linux-arm64-gnu/-/simple-git-linux-arm64-gnu-0.1.16.tgz#4e293005b2fd62d1eb399b50e53d983378c19fb7" - integrity sha512-LAGI0opFKw/HBMCV2qIBK3uWSEW9h4xd2ireZKLJy8DBPymX6NrWIamuxYNyCuACnFdPRxR4LaRFy4J5ZwuMdw== - -"@napi-rs/simple-git-linux-arm64-musl@0.1.16": - version "0.1.16" - resolved "https://registry.yarnpkg.com/@napi-rs/simple-git-linux-arm64-musl/-/simple-git-linux-arm64-musl-0.1.16.tgz#679edd2c6d88de6aa35993401722ade04595869b" - integrity sha512-I57Ph0F0Yn2KW93ep+V1EzKhACqX0x49vvSiapqIsdDA2PifdEWLc1LJarBolmK7NKoPqKmf6lAKKO9lhiZzkg== - -"@napi-rs/simple-git-linux-x64-gnu@0.1.16": - version "0.1.16" - resolved "https://registry.yarnpkg.com/@napi-rs/simple-git-linux-x64-gnu/-/simple-git-linux-x64-gnu-0.1.16.tgz#b33054b14a88335f19261b812f65f8d567e7d199" - integrity sha512-AZYYFY2V7hlcQASPEOWyOa3e1skzTct9QPzz0LiDM3f/hCFY/wBaU2M6NC5iG3d2Kr38heuyFS/+JqxLm5WaKA== - -"@napi-rs/simple-git-linux-x64-musl@0.1.16": - version "0.1.16" - resolved "https://registry.yarnpkg.com/@napi-rs/simple-git-linux-x64-musl/-/simple-git-linux-x64-musl-0.1.16.tgz#8cfc8f5f35951dacae86e72b5535ea401f868b7a" - integrity sha512-9TyMcYSBJwjT8jwjY9m24BZbu7ozyWTjsmYBYNtK3B0Um1Ov6jthSNneLVvouQ6x+k3Ow+00TiFh6bvmT00r8g== - -"@napi-rs/simple-git-win32-arm64-msvc@0.1.16": - version "0.1.16" - resolved "https://registry.yarnpkg.com/@napi-rs/simple-git-win32-arm64-msvc/-/simple-git-win32-arm64-msvc-0.1.16.tgz#e6b220574421695f4c05be4e065b1fd46ffb7007" - integrity sha512-uslJ1WuAHCYJWui6xjsyT47SjX6KOHDtClmNO8hqKz1pmDSNY7AjyUY8HxvD1lK9bDnWwc4JYhikS9cxCqHybw== - -"@napi-rs/simple-git-win32-x64-msvc@0.1.16": - version "0.1.16" - resolved "https://registry.yarnpkg.com/@napi-rs/simple-git-win32-x64-msvc/-/simple-git-win32-x64-msvc-0.1.16.tgz#4ec44d57fc2c069544ffb923a2871d81d5db7cfc" - integrity sha512-SoEaVeCZCDF1MP+M9bMSXsZWgEjk4On9GWADO5JOulvzR1bKjk0s9PMHwe/YztR9F0sJzrCxwtvBZowhSJsQPg== - "@napi-rs/simple-git@^0.1.9": version "0.1.16" - resolved "https://registry.yarnpkg.com/@napi-rs/simple-git/-/simple-git-0.1.16.tgz#c48d03b27956ddcd2c886a5e3d5c8bdc0d7ad5fe" + resolved "https://registry.npmjs.org/@napi-rs/simple-git/-/simple-git-0.1.16.tgz" integrity sha512-C5wRPw9waqL2jk3jEDeJv+f7ScuO3N0a39HVdyFLkwKxHH4Sya4ZbzZsu2JLi6eEqe7RuHipHL6mC7B2OfYZZw== optionalDependencies: "@napi-rs/simple-git-android-arm-eabi" "0.1.16" @@ -1202,461 +198,448 @@ "@napi-rs/simple-git-win32-arm64-msvc" "0.1.16" "@napi-rs/simple-git-win32-x64-msvc" "0.1.16" -"@next/env@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/env/-/env-13.5.6.tgz#c1148e2e1aa166614f05161ee8f77ded467062bc" - integrity sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw== - -"@next/eslint-plugin-next@14.1.0", "@next/eslint-plugin-next@^14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-14.1.0.tgz#29b041233fac7417e22eefa4146432d5cd910820" - integrity sha512-x4FavbNEeXx/baD/zC/SdrvkjSby8nBn8KcCREqk6UuwvwoAPZmaV8TFCAuo/cpovBRTIY67mHhe86MQQm/68Q== +"@next/eslint-plugin-next@^14.2.5": + version "14.2.19" + resolved "file:../node_modules/.pnpm/@next+eslint-plugin-next@14.2.19/node_modules/@next/eslint-plugin-next" dependencies: glob "10.3.10" -"@next/swc-darwin-arm64@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.6.tgz#b15d139d8971360fca29be3bdd703c108c9a45fb" - integrity sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA== - -"@next/swc-darwin-x64@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.6.tgz#9c72ee31cc356cb65ce6860b658d807ff39f1578" - integrity sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA== - -"@next/swc-linux-arm64-gnu@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.6.tgz#59f5f66155e85380ffa26ee3d95b687a770cfeab" - integrity sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg== - -"@next/swc-linux-arm64-musl@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.6.tgz#f012518228017052736a87d69bae73e587c76ce2" - integrity sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q== - -"@next/swc-linux-x64-gnu@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.6.tgz#339b867a7e9e7ee727a700b496b269033d820df4" - integrity sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw== - -"@next/swc-linux-x64-musl@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.6.tgz#ae0ae84d058df758675830bcf70ca1846f1028f2" - integrity sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ== - -"@next/swc-win32-arm64-msvc@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.6.tgz#a5cc0c16920485a929a17495064671374fdbc661" - integrity sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg== - -"@next/swc-win32-ia32-msvc@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.6.tgz#6a2409b84a2cbf34bf92fe714896455efb4191e4" - integrity sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg== - -"@next/swc-win32-x64-msvc@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.6.tgz#4a3e2a206251abc729339ba85f60bc0433c2865d" - integrity sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ== - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@pkgjs/parseargs@^0.11.0": - version "0.11.0" - resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" - integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== - -"@pkgr/core@^0.1.0": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" - integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== - -"@popperjs/core@^2.11.8": - version "2.11.8" - resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" - integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== - -"@putout/babel@^2.0.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@putout/babel/-/babel-2.2.0.tgz#8fd3f587ca3c19c6822969b8110829e37e8a432c" - integrity sha512-SSZFFiWrszC6x5J4anX2IrMKSipIL2IwrZHmjXFAThTwrlVmx5H8jcQbKhhkMz9bXMFRtKIlmXp/Zy5VD9tEDg== - -"@putout/compare@^14.0.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@putout/compare/-/compare-14.1.0.tgz#4f31ff4b0b829f89d08116effbfbffa72181e1e8" - integrity sha512-ISxdPgWA2evPAJ1HDo9UrtJKCX1ENMvU3YAANc5IxkjhS7kIHSvJQ0+i+JYl+o4TI9BXBPhNMKTmL6m3Lrev7Q== - dependencies: - "@putout/babel" "^2.0.0" - "@putout/engine-parser" "^10.0.0" - "@putout/operate" "^12.0.0" - debug "^4.1.1" - jessy "^3.0.0" - nessy "^4.0.0" - -"@putout/engine-parser@^10.0.0": - version "10.1.0" - resolved "https://registry.yarnpkg.com/@putout/engine-parser/-/engine-parser-10.1.0.tgz#c5e5ba1c9c70b3e8d9992f2dfc75fe98ac28fb08" - integrity sha512-5zyOJrbdFMwCJHd38nY4KV/Ttb5jVhDZZ+xedZROPLeJIRCY+KiZnt+qK4l0O6EugHnlhyKhe24VJOp48Xs9bA== - dependencies: - "@putout/babel" "^2.0.0" - "@putout/printer" "^8.0.0" - "@putout/recast" "^1.12.1" - estree-to-babel "^9.0.0" - nano-memoize "^3.0.11" - once "^1.4.0" - try-catch "^3.0.0" - -"@putout/eslint-config@^8.0.0": - version "8.0.3" - resolved "https://registry.yarnpkg.com/@putout/eslint-config/-/eslint-config-8.0.3.tgz#e789c0fb2cf34b4133ec0c2520b16f6805da3e26" - integrity sha512-1iaGTiGRFNC6xj1rrclEglKfbui1mAApntVfxXSRFMMiBjXaVblp7dL9c8lEM37tgQb4c/cVdbFcCjD7BX1Zcg== - dependencies: - "@eslint/js" "^8.35.0" - "@stylistic/eslint-plugin-js" "^1.0.0" - -"@putout/eslint@^3.0.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@putout/eslint/-/eslint-3.1.0.tgz#400a83396b17b4b750586209d7805fbbfd71cd40" - integrity sha512-2JEv2wj5nV/EnXBX1zXFO9Q1zMiFMOO2pXMTAoqmjG6q5ZA15mQB/dVGdBy7iZWHrXoLsGw+wC0Qq1UoGKwQMA== - dependencies: - find-up "^7.0.0" - try-to-catch "^3.0.1" - -"@putout/operate@^12.0.0": - version "12.3.0" - resolved "https://registry.yarnpkg.com/@putout/operate/-/operate-12.3.0.tgz#b19da683a01c08de39cf0af9195ce8efae7d202a" - integrity sha512-OnKEC9mlMxlP0107i7+ZaA3PJUfjEuCF23lZ/W9qrU0E7IEojoZPTAsD0pG6dzn/crd1oMOB9ozNfcBMtfI9mg== +"@pipedream/sdk@^0.1.9": + version "0.1.9" + resolved "file:../node_modules/.pnpm/@pipedream+sdk@0.1.9/node_modules/@pipedream/sdk" dependencies: - "@putout/babel" "^2.0.0" + simple-oauth2 "^5.1.0" -"@putout/operator-json@^2.0.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@putout/operator-json/-/operator-json-2.1.0.tgz#db6cbdd5e8e66fd0385b419ed5356728ae7673fe" - integrity sha512-RXQ/BL3FJ6yZEYOIIEk2jhbODJCjPVt2n8NLMBOy3ye1AdKATiU889nMfzPgvtPJyglv5/tc24AxFlAswA5wng== - dependencies: - remove-blank-lines "^1.4.1" - -"@putout/printer@^8.0.0": - version "8.5.0" - resolved "https://registry.yarnpkg.com/@putout/printer/-/printer-8.5.0.tgz#13b7fc06b87fdbf97191a8631f25edd3f3b863bb" - integrity sha512-YHJgvZLIA4YI3ypeyg1vxvs+HzKI1sXQ2fzGoQe2XT0dYuwLFlMG72rJQRo2Yi1u3ws/cMsq3Pcqeju7hDXhsA== - dependencies: - "@putout/babel" "^2.0.0" - "@putout/compare" "^14.0.0" - "@putout/operate" "^12.0.0" - "@putout/operator-json" "^2.0.0" - fullstore "^3.0.0" - just-snake-case "^3.2.0" - parse-import-specifiers "^1.0.1" - rendy "^4.0.0" - -"@putout/recast@^1.12.1": - version "1.13.0" - resolved "https://registry.yarnpkg.com/@putout/recast/-/recast-1.13.0.tgz#34b4bc3f1eaba6b9a8eb044966f68b9193b6f5b4" - integrity sha512-UILta9MHeFmlxs19SC/c3ivPZ6SEhoeYDxRXN/B44SgG8RG7wlLHvEy/Np0kT8Q2vbSot7ee788IaWJUolCXZA== - dependencies: - assert "^2.0.0" - ast-types "^0.16.1" - esprima "~4.0.0" - source-map "~0.6.1" - tslib "^2.0.1" - -"@rushstack/eslint-patch@^1.3.3": - version "1.7.2" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz#2d4260033e199b3032a08b41348ac10de21c47e9" - integrity sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA== - -"@sinclair/typebox@^0.27.8": - version "0.27.8" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" - integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== - -"@sinonjs/commons@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" - integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== +"@react-aria/focus@^3.17.1": + version "3.19.0" + resolved "https://registry.npmjs.org/@react-aria/focus/-/focus-3.19.0.tgz" + integrity sha512-hPF9EXoUQeQl1Y21/rbV2H4FdUR2v+4/I0/vB+8U3bT1CJ+1AFj1hc/rqx2DqEwDlEwOHN+E4+mRahQmlybq0A== dependencies: - type-detect "4.0.8" + "@react-aria/interactions" "^3.22.5" + "@react-aria/utils" "^3.26.0" + "@react-types/shared" "^3.26.0" + "@swc/helpers" "^0.5.0" + clsx "^2.0.0" -"@sinonjs/fake-timers@^10.0.2": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" - integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== +"@react-aria/interactions@^3.21.3", "@react-aria/interactions@^3.22.5": + version "3.22.5" + resolved "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.5.tgz" + integrity sha512-kMwiAD9E0TQp+XNnOs13yVJghiy8ET8L0cbkeuTgNI96sOAp/63EJ1FSrDf17iD8sdjt41LafwX/dKXW9nCcLQ== dependencies: - "@sinonjs/commons" "^3.0.0" + "@react-aria/ssr" "^3.9.7" + "@react-aria/utils" "^3.26.0" + "@react-types/shared" "^3.26.0" + "@swc/helpers" "^0.5.0" -"@stylistic/eslint-plugin-js@1.6.2", "@stylistic/eslint-plugin-js@^1.0.0", "@stylistic/eslint-plugin-js@^1.6.2": - version "1.6.2" - resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.6.2.tgz#f0ad5a8cf1ce901626e3e6b08a02d94a628d7c12" - integrity sha512-ndT6X2KgWGxv8101pdMOxL8pihlYIHcOv3ICd70cgaJ9exwkPn8hJj4YQwslxoAlre1TFHnXd/G1/hYXgDrjIA== +"@react-aria/ssr@^3.9.7": + version "3.9.7" + resolved "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz" + integrity sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg== dependencies: - "@types/eslint" "^8.56.2" - acorn "^8.11.3" - escape-string-regexp "^4.0.0" - eslint-visitor-keys "^3.4.3" - espree "^9.6.1" + "@swc/helpers" "^0.5.0" -"@stylistic/eslint-plugin-jsx@^1.0.0": - version "1.6.2" - resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin-jsx/-/eslint-plugin-jsx-1.6.2.tgz#846f6ae87feb089e3aa0f1c1adbb018b56fc5ea4" - integrity sha512-hbbouazSJbHD/fshBIOLh9JgtSphKNoTCfHLSNBjAkXLK+GR4i2jhEZZF9P0mtXrNuy2WWInmpq/g0pfWBmSBA== +"@react-aria/utils@^3.26.0": + version "3.26.0" + resolved "https://registry.npmjs.org/@react-aria/utils/-/utils-3.26.0.tgz" + integrity sha512-LkZouGSjjQ0rEqo4XJosS4L3YC/zzQkfRM3KoqK6fUOmUJ9t0jQ09WjiF+uOoG9u+p30AVg3TrZRUWmoTS+koQ== dependencies: - "@stylistic/eslint-plugin-js" "^1.6.2" - "@types/eslint" "^8.56.2" - estraverse "^5.3.0" - picomatch "^4.0.1" + "@react-aria/ssr" "^3.9.7" + "@react-stately/utils" "^3.10.5" + "@react-types/shared" "^3.26.0" + "@swc/helpers" "^0.5.0" + clsx "^2.0.0" -"@stylistic/eslint-plugin-ts@^1.0.0": - version "1.6.2" - resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-1.6.2.tgz#e8327ec54c264cb5bd17cd58b05264035655960c" - integrity sha512-FizV58em0OjO/xFHRIy/LJJVqzxCNmYC/xVtKDf8aGDRgZpLo+lkaBKfBrbMkAGzhBKbYj+iLEFI4WEl6aVZGQ== - dependencies: - "@stylistic/eslint-plugin-js" "1.6.2" - "@types/eslint" "^8.56.2" - "@typescript-eslint/utils" "^6.21.0" +"@react-stately/utils@^3.10.5": + version "3.10.5" + resolved "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.5.tgz" + integrity sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ== + dependencies: + "@swc/helpers" "^0.5.0" + +"@react-types/shared@^3.26.0": + version "3.26.0" + resolved "https://registry.npmjs.org/@react-types/shared/-/shared-3.26.0.tgz" + integrity sha512-6FuPqvhmjjlpEDLTiYx29IJCbCNWPlsyO+ZUmCUXzhUv2ttShOXfw8CmeHWHftT/b2KweAWuzqSlfeXPR76jpw== + +"@shikijs/core@1.24.0": + version "1.24.0" + resolved "https://registry.npmjs.org/@shikijs/core/-/core-1.24.0.tgz" + integrity sha512-6pvdH0KoahMzr6689yh0QJ3rCgF4j1XsXRHNEeEN6M4xJTfQ6QPWrmHzIddotg+xPJUPEPzYzYCKzpYyhTI6Gw== + dependencies: + "@shikijs/engine-javascript" "1.24.0" + "@shikijs/engine-oniguruma" "1.24.0" + "@shikijs/types" "1.24.0" + "@shikijs/vscode-textmate" "^9.3.0" + "@types/hast" "^3.0.4" + hast-util-to-html "^9.0.3" + +"@shikijs/engine-javascript@1.24.0": + version "1.24.0" + resolved "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.24.0.tgz" + integrity sha512-ZA6sCeSsF3Mnlxxr+4wGEJ9Tto4RHmfIS7ox8KIAbH0MTVUkw3roHPHZN+LlJMOHJJOVupe6tvuAzRpN8qK1vA== + dependencies: + "@shikijs/types" "1.24.0" + "@shikijs/vscode-textmate" "^9.3.0" + oniguruma-to-es "0.7.0" + +"@shikijs/engine-oniguruma@1.24.0": + version "1.24.0" + resolved "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.24.0.tgz" + integrity sha512-Eua0qNOL73Y82lGA4GF5P+G2+VXX9XnuUxkiUuwcxQPH4wom+tE39kZpBFXfUuwNYxHSkrSxpB1p4kyRW0moSg== + dependencies: + "@shikijs/types" "1.24.0" + "@shikijs/vscode-textmate" "^9.3.0" + +"@shikijs/twoslash@^1.0.0": + version "1.24.0" + resolved "https://registry.npmjs.org/@shikijs/twoslash/-/twoslash-1.24.0.tgz" + integrity sha512-ELyIoD54dFDlb4eGt5sy54WhFeJ39N1hR9W7ADwHWn3XH7cOPjj320EPCh2t76fIoLb0auD46tVLQVVMn93qsA== + dependencies: + "@shikijs/core" "1.24.0" + "@shikijs/types" "1.24.0" + twoslash "^0.2.12" + +"@shikijs/types@1.24.0": + version "1.24.0" + resolved "https://registry.npmjs.org/@shikijs/types/-/types-1.24.0.tgz" + integrity sha512-aptbEuq1Pk88DMlCe+FzXNnBZ17LCiLIGWAeCWhoFDzia5Q5Krx3DgnULLiouSdd6+LUM39XwXGppqYE0Ghtug== + dependencies: + "@shikijs/vscode-textmate" "^9.3.0" + "@types/hast" "^3.0.4" + +"@shikijs/vscode-textmate@^9.3.0": + version "9.3.0" + resolved "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz" + integrity sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA== + +"@swc/counter@^0.1.3": + version "0.1.3" + resolved "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz" + integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== -"@swc/helpers@0.5.2": - version "0.5.2" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d" - integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw== +"@swc/helpers@^0.5.0": + version "0.5.5" dependencies: + "@swc/counter" "^0.1.3" tslib "^2.4.0" -"@tanstack/react-virtual@^3.0.0-beta.60": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.0.4.tgz#32a90aa6faa2eeebb5f4ca561d26bae9f3435e45" - integrity sha512-tiqKW/e2MJVCr7/pRUXulpkyxllaOclkHNfhKTo4pmHjJIqnhMfwIjc1Q1R0Un3PI3kQywywu/791c8z9u0qeA== +"@tanstack/react-virtual@^3.8.1": + version "3.10.9" + resolved "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.9.tgz" + integrity sha512-OXO2uBjFqA4Ibr2O3y0YMnkrRWGVNqcvHQXmGvMu6IK8chZl3PrDxFXdGZ2iZkSrKh3/qUYoFqYe+Rx23RoU0g== dependencies: - "@tanstack/virtual-core" "3.0.0" + "@tanstack/virtual-core" "3.10.9" -"@tanstack/virtual-core@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.0.0.tgz#637bee36f0cabf96a1d436887c90f138a7e9378b" - integrity sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg== +"@tanstack/virtual-core@3.10.9": + version "3.10.9" + resolved "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.9.tgz" + integrity sha512-kBknKOKzmeR7lN+vSadaKWXaLS0SZZG+oqpQ/k80Q6g9REn6zRHS/ZYdrIzHnpHgy/eWs00SujveUN/GJT2qTw== -"@theguild/remark-mermaid@^0.0.5": - version "0.0.5" - resolved "https://registry.yarnpkg.com/@theguild/remark-mermaid/-/remark-mermaid-0.0.5.tgz#0f95671d247381f416e528e937be08bb7a695224" - integrity sha512-e+ZIyJkEv9jabI4m7q29wZtZv+2iwPGsXJ2d46Zi7e+QcFudiyuqhLhHG/3gX3ZEB+hxTch+fpItyMS8jwbIcw== +"@theguild/remark-mermaid@^0.1.3": + version "0.1.3" + resolved "https://registry.npmjs.org/@theguild/remark-mermaid/-/remark-mermaid-0.1.3.tgz" + integrity sha512-2FjVlaaKXK7Zj7UJAgOVTyaahn/3/EAfqYhyXg0BfDBVUl+lXcoIWRaxzqfnDr2rv8ax6GsC5mNh6hAaT86PDw== dependencies: - mermaid "^10.2.2" + mermaid "^11.0.0" unist-util-visit "^5.0.0" -"@theguild/remark-npm2yarn@^0.2.0": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@theguild/remark-npm2yarn/-/remark-npm2yarn-0.2.1.tgz#63bf5a8c85d7fe505d4808812dbc56d9c2ce00f8" - integrity sha512-jUTFWwDxtLEFtGZh/TW/w30ySaDJ8atKWH8dq2/IiQF61dPrGfETpl0WxD0VdBfuLOeU14/kop466oBSRO/5CA== +"@theguild/remark-npm2yarn@^0.3.2": + version "0.3.3" + resolved "https://registry.npmjs.org/@theguild/remark-npm2yarn/-/remark-npm2yarn-0.3.3.tgz" + integrity sha512-ma6DvR03gdbvwqfKx1omqhg9May/VYGdMHvTzB4VuxkyS7KzfZ/lzrj43hmcsggpMje0x7SADA/pcMph0ejRnA== dependencies: - npm-to-yarn "^2.1.0" + npm-to-yarn "^3.0.0" unist-util-visit "^5.0.0" "@types/acorn@^4.0.0": version "4.0.6" - resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.6.tgz#d61ca5480300ac41a7d973dd5b84d0a591154a22" + resolved "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz" integrity sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ== dependencies: "@types/estree" "*" -"@types/babel__core@^7.1.14": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" - integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== - dependencies: - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - "@types/babel__generator" "*" - "@types/babel__template" "*" - "@types/babel__traverse" "*" +"@types/d3-array@*": + version "3.2.1" + resolved "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz" + integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== -"@types/babel__generator@*": - version "7.6.8" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.8.tgz#f836c61f48b1346e7d2b0d93c6dacc5b9535d3ab" - integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw== +"@types/d3-axis@*": + version "3.0.6" + resolved "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz" + integrity sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw== dependencies: - "@babel/types" "^7.0.0" + "@types/d3-selection" "*" -"@types/babel__template@*": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" - integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== +"@types/d3-brush@*": + version "3.0.6" + resolved "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz" + integrity sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A== dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" + "@types/d3-selection" "*" -"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.5.tgz#7b7502be0aa80cc4ef22978846b983edaafcd4dd" - integrity sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ== - dependencies: - "@babel/types" "^7.20.7" +"@types/d3-chord@*": + version "3.0.6" + resolved "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz" + integrity sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg== -"@types/d3-scale-chromatic@^3.0.0": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz#fc0db9c10e789c351f4c42d96f31f2e4df8f5644" - integrity sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw== +"@types/d3-color@*": + version "3.1.3" + resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz" + integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== -"@types/d3-scale@^4.0.3": - version "4.0.8" - resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb" - integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== +"@types/d3-contour@*": + version "3.0.6" + resolved "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz" + integrity sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg== dependencies: - "@types/d3-time" "*" + "@types/d3-array" "*" + "@types/geojson" "*" -"@types/d3-time@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be" - integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw== +"@types/d3-delaunay@*": + version "6.0.4" + resolved "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz" + integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw== -"@types/debug@^4.0.0": - version "4.1.12" - resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" - integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== - dependencies: - "@types/ms" "*" +"@types/d3-dispatch@*": + version "3.0.6" + resolved "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz" + integrity sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ== -"@types/eslint@^8.56.2": - version "8.56.2" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.2.tgz#1c72a9b794aa26a8b94ad26d5b9aa51c8a6384bb" - integrity sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw== +"@types/d3-drag@*": + version "3.0.7" + resolved "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz" + integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ== dependencies: - "@types/estree" "*" - "@types/json-schema" "*" + "@types/d3-selection" "*" -"@types/estree-jsx@^1.0.0": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@types/estree-jsx/-/estree-jsx-1.0.4.tgz#8d34b43444887dde8a73af530f772f23e1d3287c" - integrity sha512-5idy3hvI9lAMqsyilBM+N+boaCf1MgoefbDxN6KEO5aK17TOHwFAYT9sjxzeKAiIWRUBgLxmZ9mPcnzZXtTcRQ== - dependencies: - "@types/estree" "*" +"@types/d3-dsv@*": + version "3.0.7" + resolved "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz" + integrity sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g== -"@types/estree@*", "@types/estree@^1.0.0": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" - integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/d3-ease@*": + version "3.0.2" + resolved "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz" + integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== -"@types/graceful-fs@^4.1.3": - version "4.1.9" - resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" - integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== +"@types/d3-fetch@*": + version "3.0.7" + resolved "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz" + integrity sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA== dependencies: - "@types/node" "*" + "@types/d3-dsv" "*" -"@types/hast@^2.0.0": - version "2.3.10" - resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.10.tgz#5c9d9e0b304bbb8879b857225c5ebab2d81d7643" - integrity sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw== - dependencies: - "@types/unist" "^2" +"@types/d3-force@*": + version "3.0.10" + resolved "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz" + integrity sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw== -"@types/hast@^3.0.0": +"@types/d3-format@*": version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" - integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== - dependencies: - "@types/unist" "*" + resolved "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz" + integrity sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g== -"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" - integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== - -"@types/istanbul-lib-report@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" - integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== +"@types/d3-geo@*": + version "3.1.0" + resolved "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz" + integrity sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ== dependencies: - "@types/istanbul-lib-coverage" "*" + "@types/geojson" "*" + +"@types/d3-hierarchy@*": + version "3.1.7" + resolved "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz" + integrity sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg== -"@types/istanbul-reports@^3.0.0": +"@types/d3-interpolate@*": version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" - integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== + resolved "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz" + integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== dependencies: - "@types/istanbul-lib-report" "*" + "@types/d3-color" "*" -"@types/js-yaml@^4.0.0": - version "4.0.9" - resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2" - integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg== +"@types/d3-path@*": + version "3.1.0" + resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz" + integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ== -"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.9": - version "7.0.15" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" - integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/d3-polygon@*": + version "3.0.2" + resolved "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz" + integrity sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA== + +"@types/d3-quadtree@*": + version "3.0.6" + resolved "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz" + integrity sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg== + +"@types/d3-random@*": + version "3.0.3" + resolved "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz" + integrity sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ== + +"@types/d3-scale-chromatic@*": + version "3.1.0" + resolved "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz" + integrity sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ== + +"@types/d3-scale@*": + version "4.0.8" + resolved "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz" + integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== + dependencies: + "@types/d3-time" "*" + +"@types/d3-selection@*": + version "3.0.11" + resolved "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz" + integrity sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w== + +"@types/d3-shape@*": + version "3.1.6" + resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz" + integrity sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time-format@*": + version "4.0.3" + resolved "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz" + integrity sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg== + +"@types/d3-time@*": + version "3.0.4" + resolved "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz" + integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== + +"@types/d3-timer@*": + version "3.0.2" + resolved "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz" + integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== + +"@types/d3-transition@*": + version "3.0.9" + resolved "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz" + integrity sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-zoom@*": + version "3.0.8" + resolved "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz" + integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw== + dependencies: + "@types/d3-interpolate" "*" + "@types/d3-selection" "*" + +"@types/d3@^7.4.3": + version "7.4.3" + resolved "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz" + integrity sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww== + dependencies: + "@types/d3-array" "*" + "@types/d3-axis" "*" + "@types/d3-brush" "*" + "@types/d3-chord" "*" + "@types/d3-color" "*" + "@types/d3-contour" "*" + "@types/d3-delaunay" "*" + "@types/d3-dispatch" "*" + "@types/d3-drag" "*" + "@types/d3-dsv" "*" + "@types/d3-ease" "*" + "@types/d3-fetch" "*" + "@types/d3-force" "*" + "@types/d3-format" "*" + "@types/d3-geo" "*" + "@types/d3-hierarchy" "*" + "@types/d3-interpolate" "*" + "@types/d3-path" "*" + "@types/d3-polygon" "*" + "@types/d3-quadtree" "*" + "@types/d3-random" "*" + "@types/d3-scale" "*" + "@types/d3-scale-chromatic" "*" + "@types/d3-selection" "*" + "@types/d3-shape" "*" + "@types/d3-time" "*" + "@types/d3-time-format" "*" + "@types/d3-timer" "*" + "@types/d3-transition" "*" + "@types/d3-zoom" "*" + +"@types/debug@^4.0.0": + version "4.1.12" + resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + +"@types/estree-jsx@^1.0.0": + version "1.0.4" + resolved "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.4.tgz" + integrity sha512-5idy3hvI9lAMqsyilBM+N+boaCf1MgoefbDxN6KEO5aK17TOHwFAYT9sjxzeKAiIWRUBgLxmZ9mPcnzZXtTcRQ== + dependencies: + "@types/estree" "*" + +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.5" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + +"@types/geojson@*": + version "7946.0.14" + resolved "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz" + integrity sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg== -"@types/json5@^0.0.29": - version "0.0.29" - resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" - integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/hast@^3.0.0", "@types/hast@^3.0.4": + version "3.0.4" + resolved "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + dependencies: + "@types/unist" "*" "@types/katex@^0.16.0": version "0.16.7" - resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.16.7.tgz#03ab680ab4fa4fbc6cb46ecf987ecad5d8019868" + resolved "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz" integrity sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ== -"@types/mdast@^3.0.0": - version "3.0.15" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5" - integrity sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ== - dependencies: - "@types/unist" "^2" - "@types/mdast@^4.0.0": version "4.0.3" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.3.tgz#1e011ff013566e919a4232d1701ad30d70cab333" + resolved "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz" integrity sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg== dependencies: "@types/unist" "*" "@types/mdx@^2.0.0": version "2.0.11" - resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.11.tgz#21f4c166ed0e0a3a733869ba04cd8daea9834b8e" + resolved "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.11.tgz" integrity sha512-HM5bwOaIQJIQbAYfax35HCKxx7a3KrK3nBtIqJgSOitivTD1y3oW9P3rxY9RkXYPUk7y/AjAohfHKmFpGE79zw== "@types/ms@*": version "0.7.34" - resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" + resolved "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz" integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== -"@types/node@*": - version "20.11.19" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.19.tgz#b466de054e9cb5b3831bee38938de64ac7f81195" - integrity sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ== +"@types/nlcst@^2.0.0": + version "2.0.3" + resolved "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz" + integrity sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA== dependencies: - undici-types "~5.26.4" + "@types/unist" "*" "@types/node@18.11.10": version "18.11.10" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.10.tgz#4c64759f3c2343b7e6c4b9caf761c7a3a05cee34" - integrity sha512-juG3RWMBOqcOuXC643OAdSA525V44cVgGV6dUDuiFtss+8Fk5x1hI93Rsld43VeJVIeqlP9I7Fn9/qaVqoEAuQ== + resolved "file:../node_modules/.pnpm/@types+node@18.11.10/node_modules/@types/node" "@types/prop-types@*": version "15.7.11" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563" + resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz" integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng== "@types/react@>=16": version "18.2.55" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.55.tgz#38141821b7084404b5013742bc4ae08e44da7a67" + resolved "https://registry.npmjs.org/@types/react/-/react-18.2.55.tgz" integrity sha512-Y2Tz5P4yz23brwm2d7jNon39qoAtMMmalOQv6+fEFt1mT+FcM3D841wDpoUvFXhaYenuROCy3FZYqdTjM7qVyA== dependencies: "@types/prop-types" "*" @@ -1665,1107 +648,283 @@ "@types/scheduler@*": version "0.16.8" - resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" + resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz" integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A== -"@types/semver@^7.3.12", "@types/semver@^7.5.0": - version "7.5.7" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.7.tgz#326f5fdda70d13580777bcaa1bc6fa772a5aef0e" - integrity sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg== - -"@types/stack-utils@^2.0.0": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" - integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== - -"@types/unist@*", "@types/unist@^3.0.0": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20" - integrity sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ== +"@types/trusted-types@^2.0.7": + version "2.0.7" + resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== -"@types/unist@^2", "@types/unist@^2.0.0": +"@types/unist@*", "@types/unist@^2.0.0": version "2.0.10" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc" + resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz" integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA== -"@types/yargs-parser@*": - version "21.0.3" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" - integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== - -"@types/yargs@^17.0.8": - version "17.0.32" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.32.tgz#030774723a2f7faafebf645f4e5a48371dca6229" - integrity sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog== - dependencies: - "@types/yargs-parser" "*" - -"@typescript-eslint/eslint-plugin@7.0.2", "@typescript-eslint/eslint-plugin@^7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz#c13a34057be425167cc4a765158c46fdf2fd981d" - integrity sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg== - dependencies: - "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "7.0.2" - "@typescript-eslint/type-utils" "7.0.2" - "@typescript-eslint/utils" "7.0.2" - "@typescript-eslint/visitor-keys" "7.0.2" - debug "^4.3.4" - graphemer "^1.4.0" - ignore "^5.2.4" - natural-compare "^1.4.0" - semver "^7.5.4" - ts-api-utils "^1.0.1" - -"@typescript-eslint/eslint-plugin@^7.0.1": - version "7.0.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz#407daffe09d964d57aceaf3ac51846359fbe61b0" - integrity sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ== - dependencies: - "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "7.0.1" - "@typescript-eslint/type-utils" "7.0.1" - "@typescript-eslint/utils" "7.0.1" - "@typescript-eslint/visitor-keys" "7.0.1" - debug "^4.3.4" +"@types/unist@^3.0.0": + version "3.0.3" + resolved "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + +"@typescript-eslint/eslint-plugin@^7.18.0": + version "7.18.0" + resolved "file:../node_modules/.pnpm/@typescript-eslint+eslint-plugin@7.18.0_@typescript-eslint+parser@7.18.0_eslint@8.57.1_typesc_oxern26j7d7jrxgqlfw37bwkjm/node_modules/@typescript-eslint/eslint-plugin" + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "7.18.0" + "@typescript-eslint/type-utils" "7.18.0" + "@typescript-eslint/utils" "7.18.0" + "@typescript-eslint/visitor-keys" "7.18.0" graphemer "^1.4.0" - ignore "^5.2.4" + ignore "^5.3.1" natural-compare "^1.4.0" - semver "^7.5.4" - ts-api-utils "^1.0.1" - -"@typescript-eslint/parser@7.0.2", "@typescript-eslint/parser@^7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.0.2.tgz#95c31233d343db1ca1df8df7811b5b87ca7b1a68" - integrity sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q== - dependencies: - "@typescript-eslint/scope-manager" "7.0.2" - "@typescript-eslint/types" "7.0.2" - "@typescript-eslint/typescript-estree" "7.0.2" - "@typescript-eslint/visitor-keys" "7.0.2" - debug "^4.3.4" - -"@typescript-eslint/parser@^5.4.2 || ^6.0.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" - integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== - dependencies: - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - debug "^4.3.4" - -"@typescript-eslint/parser@^7.0.1": - version "7.0.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.0.1.tgz#e9c61d9a5e32242477d92756d36086dc40322eed" - integrity sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ== - dependencies: - "@typescript-eslint/scope-manager" "7.0.1" - "@typescript-eslint/types" "7.0.1" - "@typescript-eslint/typescript-estree" "7.0.1" - "@typescript-eslint/visitor-keys" "7.0.1" - debug "^4.3.4" - -"@typescript-eslint/scope-manager@5.62.0": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" - integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== - dependencies: - "@typescript-eslint/types" "5.62.0" - "@typescript-eslint/visitor-keys" "5.62.0" - -"@typescript-eslint/scope-manager@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" - integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== - dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - -"@typescript-eslint/scope-manager@7.0.1": - version "7.0.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz#611ec8e78c70439b152a805e1b10aaac36de7c00" - integrity sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w== - dependencies: - "@typescript-eslint/types" "7.0.1" - "@typescript-eslint/visitor-keys" "7.0.1" - -"@typescript-eslint/scope-manager@7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz#6ec4cc03752758ddd1fdaae6fbd0ed9a2ca4fe63" - integrity sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g== - dependencies: - "@typescript-eslint/types" "7.0.2" - "@typescript-eslint/visitor-keys" "7.0.2" - -"@typescript-eslint/type-utils@7.0.1": - version "7.0.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz#0fba92c1f81cad561d7b3adc812aa1cc0e35cdae" - integrity sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw== - dependencies: - "@typescript-eslint/typescript-estree" "7.0.1" - "@typescript-eslint/utils" "7.0.1" - debug "^4.3.4" - ts-api-utils "^1.0.1" - -"@typescript-eslint/type-utils@7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz#a7fc0adff0c202562721357e7478207d380a757b" - integrity sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ== - dependencies: - "@typescript-eslint/typescript-estree" "7.0.2" - "@typescript-eslint/utils" "7.0.2" - debug "^4.3.4" - ts-api-utils "^1.0.1" - -"@typescript-eslint/types@5.62.0": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" - integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== - -"@typescript-eslint/types@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" - integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== - -"@typescript-eslint/types@7.0.1": - version "7.0.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.0.1.tgz#dcfabce192db5b8bf77ea3c82cfaabe6e6a3c901" - integrity sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg== - -"@typescript-eslint/types@7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.0.2.tgz#b6edd108648028194eb213887d8d43ab5750351c" - integrity sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA== - -"@typescript-eslint/typescript-estree@5.62.0": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" - integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== - dependencies: - "@typescript-eslint/types" "5.62.0" - "@typescript-eslint/visitor-keys" "5.62.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" + ts-api-utils "^1.3.0" -"@typescript-eslint/typescript-estree@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" - integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== +"@typescript-eslint/parser@^7.18.0": + version "7.18.0" + resolved "file:../node_modules/.pnpm/@typescript-eslint+parser@7.18.0_eslint@8.57.1_typescript@5.6.3/node_modules/@typescript-eslint/parser" dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/scope-manager" "7.18.0" + "@typescript-eslint/types" "7.18.0" + "@typescript-eslint/typescript-estree" "7.18.0" + "@typescript-eslint/visitor-keys" "7.18.0" debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - minimatch "9.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" -"@typescript-eslint/typescript-estree@7.0.1": - version "7.0.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz#1d52ac03da541693fa5bcdc13ad655def5046faf" - integrity sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig== +"@typescript/vfs@^1.6.0": + version "1.6.0" + resolved "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.0.tgz" + integrity sha512-hvJUjNVeBMp77qPINuUvYXj4FyWeeMMKZkxEATEU3hqBAQ7qdTBCUFT7Sp0Zu0faeEtFf+ldXxMEDr/bk73ISg== dependencies: - "@typescript-eslint/types" "7.0.1" - "@typescript-eslint/visitor-keys" "7.0.1" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - minimatch "9.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" + debug "^4.1.1" -"@typescript-eslint/typescript-estree@7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz#3c6dc8a3b9799f4ef7eca0d224ded01974e4cb39" - integrity sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw== - dependencies: - "@typescript-eslint/types" "7.0.2" - "@typescript-eslint/visitor-keys" "7.0.2" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - minimatch "9.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" - -"@typescript-eslint/utils@7.0.1": - version "7.0.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.0.1.tgz#b8ceac0ba5fef362b4a03a33c0e1fedeea3734ed" - integrity sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@types/json-schema" "^7.0.12" - "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "7.0.1" - "@typescript-eslint/types" "7.0.1" - "@typescript-eslint/typescript-estree" "7.0.1" - semver "^7.5.4" - -"@typescript-eslint/utils@7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.0.2.tgz#8756123054cd934c8ba7db6a6cffbc654b10b5c4" - integrity sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@types/json-schema" "^7.0.12" - "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "7.0.2" - "@typescript-eslint/types" "7.0.2" - "@typescript-eslint/typescript-estree" "7.0.2" - semver "^7.5.4" - -"@typescript-eslint/utils@^5.10.0": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" - integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@types/json-schema" "^7.0.9" - "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.62.0" - "@typescript-eslint/types" "5.62.0" - "@typescript-eslint/typescript-estree" "5.62.0" - eslint-scope "^5.1.1" - semver "^7.3.7" - -"@typescript-eslint/utils@^6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" - integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@types/json-schema" "^7.0.12" - "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" - semver "^7.5.4" - -"@typescript-eslint/visitor-keys@5.62.0": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" - integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== - dependencies: - "@typescript-eslint/types" "5.62.0" - eslint-visitor-keys "^3.3.0" - -"@typescript-eslint/visitor-keys@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" - integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== - dependencies: - "@typescript-eslint/types" "6.21.0" - eslint-visitor-keys "^3.4.1" - -"@typescript-eslint/visitor-keys@7.0.1": - version "7.0.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz#864680ac5a8010ec4814f8a818e57595f79f464e" - integrity sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw== - dependencies: - "@typescript-eslint/types" "7.0.1" - eslint-visitor-keys "^3.4.1" - -"@typescript-eslint/visitor-keys@7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz#2899b716053ad7094962beb895d11396fc12afc7" - integrity sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ== - dependencies: - "@typescript-eslint/types" "7.0.2" - eslint-visitor-keys "^3.4.1" - -"@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.2.0": +"@ungap/structured-clone@^1.0.0": version "1.2.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@vercel/analytics@^1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@vercel/analytics/-/analytics-1.2.2.tgz#715d8f203a170c06ba36b363e03b048c03060d5d" - integrity sha512-X0rctVWkQV1e5Y300ehVNqpOfSOufo7ieA5PIdna8yX/U7Vjz0GFsGf4qvAhxV02uQ2CVt7GYcrFfddXXK2Y4A== - dependencies: - server-only "^0.0.1" +"@vercel/analytics@^1.3.1": + version "1.4.1" + resolved "file:../node_modules/.pnpm/@vercel+analytics@1.4.1_next@14.2.19_@babel+core@7.26.0_@opentelemetry+api@1.9.0_babel-plugin_s2ppkwmadwk45vy2br36rcfknu/node_modules/@vercel/analytics" -acorn-jsx@^5.0.0, acorn-jsx@^5.3.2: +acorn-jsx@^5.0.0: version "5.3.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.0.0, acorn@^8.11.3, acorn@^8.5.0, acorn@^8.9.0: - version "8.11.3" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" - integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== - -ajv@^6.12.4: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -algoliasearch@^4.19.1: - version "4.22.1" - resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.22.1.tgz#f10fbecdc7654639ec20d62f109c1b3a46bc6afc" - integrity sha512-jwydKFQJKIx9kIZ8Jm44SdpigFwRGPESaxZBaHSV0XWN2yBJAOT4mT7ppvlrpA4UGzz92pqFnVKr/kaZXrcreg== - dependencies: - "@algolia/cache-browser-local-storage" "4.22.1" - "@algolia/cache-common" "4.22.1" - "@algolia/cache-in-memory" "4.22.1" - "@algolia/client-account" "4.22.1" - "@algolia/client-analytics" "4.22.1" - "@algolia/client-common" "4.22.1" - "@algolia/client-personalization" "4.22.1" - "@algolia/client-search" "4.22.1" - "@algolia/logger-common" "4.22.1" - "@algolia/logger-console" "4.22.1" - "@algolia/requester-browser-xhr" "4.22.1" - "@algolia/requester-common" "4.22.1" - "@algolia/requester-node-http" "4.22.1" - "@algolia/transporter" "4.22.1" - -align-spaces@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/align-spaces/-/align-spaces-1.0.4.tgz#a7f6f4414b156116dfb2896c71dfca064631a7fe" - integrity sha512-JPl93xFbsX4OY7VFKjerJ9cjaelmKo1wt1EP0ScrKI578vro1WhGy+w9C0nAFup8qYADgAS2FvMb7uLPStTB6g== - -ansi-escapes@^4.2.1: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - -ansi-escapes@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-6.2.0.tgz#8a13ce75286f417f1963487d86ba9f90dccf9947" - integrity sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw== - dependencies: - type-fest "^3.0.0" - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== - -ansi-sequence-parser@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz#e0aa1cdcbc8f8bb0b5bca625aac41f5f056973cf" - integrity sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg== - -ansi-styles@^3.1.0, ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -ansi-styles@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" - integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== - -ansi-styles@^6.0.0, ansi-styles@^6.1.0, ansi-styles@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== - -any-promise@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== - -anymatch@^3.0.3, anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -arch@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" - integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== - -arg@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/arg/-/arg-1.0.0.tgz#444d885a4e25b121640b55155ef7cd03975d6050" - integrity sha512-Wk7TEzl1KqvTGs/uyhmHO/3XLd3t1UeU4IstvPXVzGPM522cTjqjNZ99esCkcL52sjqjo8e8CTBcWhkxvGzoAw== +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.0.0, acorn@^8.14.0: + version "8.14.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz" + integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== -arg@^5.0.2: +arg@^5.0.0: version "5.0.2" - resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" - integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== argparse@^1.0.7: version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== dependencies: sprintf-js "~1.0.2" -argparse@^2.0.1: +array-iterate@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -aria-query@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" - integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== - dependencies: - dequal "^2.0.3" - -array-buffer-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" - integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== - dependencies: - call-bind "^1.0.5" - is-array-buffer "^3.0.4" - -array-includes@^3.1.6, array-includes@^3.1.7: - version "3.1.7" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda" - integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - get-intrinsic "^1.2.1" - is-string "^1.0.7" - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -array.prototype.filter@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array.prototype.filter/-/array.prototype.filter-1.0.3.tgz#423771edeb417ff5914111fff4277ea0624c0d0e" - integrity sha512-VizNcj/RGJiUyQBgzwxzE5oHdeuXY5hSbbmKMlphj1cy1Vl7Pn2asCGbSrru6hSQjmCzqTBPVWAF/whmEOVHbw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-array-method-boxes-properly "^1.0.0" - is-string "^1.0.7" - -array.prototype.findlastindex@^1.2.3: - version "1.2.4" - resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.4.tgz#d1c50f0b3a9da191981ff8942a0aedd82794404f" - integrity sha512-hzvSHUshSpCflDR1QMUBLHGHP1VIEBegT4pix9H/Z92Xw3ySoy6c2qh7lJWTJnRJ8JCZ9bJNCgTyYaJGcJu6xQ== - dependencies: - call-bind "^1.0.5" - define-properties "^1.2.1" - es-abstract "^1.22.3" - es-errors "^1.3.0" - es-shim-unscopables "^1.0.2" - -array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" - integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" - -array.prototype.flatmap@^1.3.1, array.prototype.flatmap@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" - integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" - -array.prototype.tosorted@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz#c8c89348337e51b8a3c48a9227f9ce93ceedcba8" - integrity sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg== - dependencies: - call-bind "^1.0.5" - define-properties "^1.2.1" - es-abstract "^1.22.3" - es-errors "^1.1.0" - es-shim-unscopables "^1.0.2" - -arraybuffer.prototype.slice@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" - integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== - dependencies: - array-buffer-byte-length "^1.0.1" - call-bind "^1.0.5" - define-properties "^1.2.1" - es-abstract "^1.22.3" - es-errors "^1.2.1" - get-intrinsic "^1.2.3" - is-array-buffer "^3.0.4" - is-shared-array-buffer "^1.0.2" - -assert@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-2.1.0.tgz#6d92a238d05dc02e7427c881fb8be81c8448b2dd" - integrity sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw== - dependencies: - call-bind "^1.0.2" - is-nan "^1.3.2" - object-is "^1.1.5" - object.assign "^4.1.4" - util "^0.12.5" - -ast-types-flow@^0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" - integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== - -ast-types@^0.16.1: - version "0.16.1" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.16.1.tgz#7a9da1617c9081bc121faafe91711b4c8bb81da2" - integrity sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg== - dependencies: - tslib "^2.0.1" + resolved "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz" + integrity sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg== astring@^1.8.0: version "1.8.6" - resolved "https://registry.yarnpkg.com/astring/-/astring-1.8.6.tgz#2c9c157cf1739d67561c56ba896e6948f6b93731" + resolved "https://registry.npmjs.org/astring/-/astring-1.8.6.tgz" integrity sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg== -asynciterator.prototype@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz#8c5df0514936cdd133604dfcc9d3fb93f09b2b62" - integrity sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg== - dependencies: - has-symbols "^1.0.3" - -autoprefixer@^10.4.17: - version "10.4.17" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.17.tgz#35cd5695cbbe82f536a50fa025d561b01fdec8be" - integrity sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg== +autoprefixer@^10.4.19: + version "10.4.20" + resolved "file:../node_modules/.pnpm/autoprefixer@10.4.20_postcss@8.4.49/node_modules/autoprefixer" dependencies: - browserslist "^4.22.2" - caniuse-lite "^1.0.30001578" + browserslist "^4.23.3" + caniuse-lite "^1.0.30001646" fraction.js "^4.3.7" normalize-range "^0.1.2" - picocolors "^1.0.0" + picocolors "^1.0.1" postcss-value-parser "^4.2.0" -available-typed-arrays@^1.0.5, available-typed-arrays@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz#ac812d8ce5a6b976d738e1c45f08d0b00bc7d725" - integrity sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg== - -axe-core@=4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" - integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== - -axobject-query@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" - integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg== - dependencies: - dequal "^2.0.3" - -babel-jest@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" - integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== - dependencies: - "@jest/transform" "^29.7.0" - "@types/babel__core" "^7.1.14" - babel-plugin-istanbul "^6.1.1" - babel-preset-jest "^29.6.3" - chalk "^4.0.0" - graceful-fs "^4.2.9" - slash "^3.0.0" - -babel-plugin-istanbul@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" - integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@istanbuljs/load-nyc-config" "^1.0.0" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-instrument "^5.0.4" - test-exclude "^6.0.0" - -babel-plugin-jest-hoist@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" - integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== - dependencies: - "@babel/template" "^7.3.3" - "@babel/types" "^7.3.3" - "@types/babel__core" "^7.1.14" - "@types/babel__traverse" "^7.0.6" - -babel-preset-current-node-syntax@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" - integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== - dependencies: - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-bigint" "^7.8.3" - "@babel/plugin-syntax-class-properties" "^7.8.3" - "@babel/plugin-syntax-import-meta" "^7.8.3" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.8.3" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-top-level-await" "^7.8.3" - -babel-preset-jest@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" - integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== - dependencies: - babel-plugin-jest-hoist "^29.6.3" - babel-preset-current-node-syntax "^1.0.0" - bail@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" + resolved "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz" integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - -braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -browserslist@^4.22.2: - version "4.23.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" - integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== - dependencies: - caniuse-lite "^1.0.30001587" - electron-to-chromium "^1.4.668" - node-releases "^2.0.14" - update-browserslist-db "^1.0.13" - -bser@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" - integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== - dependencies: - node-int64 "^0.4.0" - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -builtin-modules@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" - integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== - -busboy@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" - integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== - dependencies: - streamsearch "^1.1.0" - -call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" - integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== +better-react-mathjax@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/better-react-mathjax/-/better-react-mathjax-2.0.3.tgz" + integrity sha512-wfifT8GFOKb1TWm2+E50I6DJpLZ5kLbch283Lu043EJtwSv0XvZDjr4YfR4d2MjAhqP6SH4VjjrKgbX8R00oCQ== dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - set-function-length "^1.2.1" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camelcase-css@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" - integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== - -camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -camelcase@^6.2.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" - integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== - -caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001578, caniuse-lite@^1.0.30001587: - version "1.0.30001587" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz#a0bce920155fa56a1885a69c74e1163fc34b4881" - integrity sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA== + mathjax-full "^3.2.2" ccount@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + resolved "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz" integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== -chalk@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" - integrity sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q== - dependencies: - ansi-styles "^3.1.0" - escape-string-regexp "^1.0.5" - supports-color "^4.0.0" - -chalk@5.3.0, chalk@^5.3.0: +chalk@^5.0.0, chalk@^5.3.0: version "5.3.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" - integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== - -chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^4.0.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -char-regex@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" - integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + resolved "file:../node_modules/.pnpm/chalk@5.3.0/node_modules/chalk" character-entities-html4@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" + resolved "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz" integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== character-entities-legacy@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" + resolved "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz" integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== character-entities@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" + resolved "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz" integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== character-reference-invalid@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" + resolved "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz" integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== -chokidar@^3.5.3: - version "3.6.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -ci-info@^3.2.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" - integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== - -cjs-module-lexer@^1.0.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" - integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== - -cli-cursor@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" - integrity sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg== - dependencies: - restore-cursor "^4.0.0" - -cli-truncate@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-4.0.0.tgz#6cc28a2924fee9e25ce91e973db56c7066e6172a" - integrity sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA== +chevrotain-allstar@~0.3.0: + version "0.3.1" + resolved "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz" + integrity sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw== dependencies: - slice-ansi "^5.0.0" - string-width "^7.0.0" - -client-only@0.0.1, client-only@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" - integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== + lodash-es "^4.17.21" -clipboardy@1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-1.2.2.tgz#2ce320b9ed9be1514f79878b53ff9765420903e2" - integrity sha512-16KrBOV7bHmHdxcQiCvfUFYVFyEah4FI8vYT1Fr7CGSA4G+xBWMEfUEQJS1hxeHGtI9ju1Bzs9uXSbj5HZKArw== +chevrotain@^11.0.0, chevrotain@~11.0.3: + version "11.0.3" + resolved "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz" + integrity sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw== dependencies: - arch "^2.1.0" - execa "^0.8.0" + "@chevrotain/cst-dts-gen" "11.0.3" + "@chevrotain/gast" "11.0.3" + "@chevrotain/regexp-to-ast" "11.0.3" + "@chevrotain/types" "11.0.3" + "@chevrotain/utils" "11.0.3" + lodash-es "4.17.21" -cliui@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" - integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== +clipboardy@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/clipboardy/-/clipboardy-4.0.0.tgz" + integrity sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w== dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.1" - wrap-ansi "^7.0.0" + execa "^8.0.1" + is-wsl "^3.1.0" + is64bit "^2.0.0" clsx@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb" + resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz" integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg== -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== - -collect-v8-coverage@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9" - integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - -color-name@^1.0.0, color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-string@^1.9.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" - integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" - integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== - dependencies: - color-convert "^2.0.1" - color-string "^1.9.0" - -colorette@^2.0.20: - version "2.0.20" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" - integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== +collapse-white-space@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz" + integrity sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw== comma-separated-tokens@^2.0.0: version "2.0.3" - resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" + resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz" integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== -commander@11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906" - integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== +commander@^8.3.0: + version "8.3.0" + resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== commander@7: version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== -commander@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" - integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== - -commander@^8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" - integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== +commander@9.2.0: + version "9.2.0" + resolved "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz" + integrity sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w== compute-scroll-into-view@^3.0.2: version "3.1.0" - resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz#753f11d972596558d8fe7c6bcbc8497690ab4c87" + resolved "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz" integrity sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg== -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -convert-source-map@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" - integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +confbox@^0.1.8: + version "0.1.8" + resolved "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz" + integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w== cose-base@^1.0.0: version "1.0.3" - resolved "https://registry.yarnpkg.com/cose-base/-/cose-base-1.0.3.tgz#650334b41b869578a543358b80cda7e0abe0a60a" + resolved "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz" integrity sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg== dependencies: layout-base "^1.0.0" -create-jest@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" - integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== - dependencies: - "@jest/types" "^29.6.3" - chalk "^4.0.0" - exit "^0.1.2" - graceful-fs "^4.2.9" - jest-config "^29.7.0" - jest-util "^29.7.0" - prompts "^2.0.1" - -cross-spawn@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" - integrity sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A== +cose-base@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz" + integrity sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g== dependencies: - lru-cache "^4.0.1" - shebang-command "^1.2.0" - which "^1.2.9" + layout-base "^2.0.0" -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.3: version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" which "^2.0.1" -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - csstype@^3.0.2: version "3.1.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== cytoscape-cose-bilkent@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz#762fa121df9930ffeb51a495d87917c570ac209b" + resolved "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz" integrity sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ== dependencies: cose-base "^1.0.0" -cytoscape@^3.28.1: - version "3.28.1" - resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.28.1.tgz#f32c3e009bdf32d47845a16a4cd2be2bbc01baf7" - integrity sha512-xyItz4O/4zp9/239wCcH8ZcFuuZooEeF8KHRmzjDfGdXsj3OG9MFSMA0pJE0uX3uCN/ygof6hHf4L7lst+JaDg== +cytoscape-fcose@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz" + integrity sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ== dependencies: - heap "^0.2.6" - lodash "^4.17.21" + cose-base "^2.2.0" -"d3-array@1 - 2": - version "2.12.1" - resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81" - integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== - dependencies: - internmap "^1.0.0" +cytoscape@^3.2.0, cytoscape@^3.29.2: + version "3.30.4" + resolved "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.4.tgz" + integrity sha512-OxtlZwQl1WbwMmLiyPSEBuzeTIQnwZhJYYWFzZ2PhEHVFwpeaqNIkUzSiso00D98qk60l8Gwon2RP304d3BJ1A== -"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0: +d3-array@^3.2.0, "d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3: version "3.2.4" - resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" + resolved "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz" integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== dependencies: internmap "1 - 2" +"d3-array@1 - 2": + version "2.12.1" + resolved "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz" + integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== + dependencies: + internmap "^1.0.0" + d3-axis@3: version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322" + resolved "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz" integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw== d3-brush@3: version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c" + resolved "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz" integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ== dependencies: d3-dispatch "1 - 3" @@ -2776,38 +935,38 @@ d3-brush@3: d3-chord@3: version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966" + resolved "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz" integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g== dependencies: d3-path "1 - 3" "d3-color@1 - 3", d3-color@3: version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz" integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== d3-contour@4: version "4.0.2" - resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-4.0.2.tgz#bb92063bc8c5663acb2422f99c73cbb6c6ae3bcc" + resolved "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz" integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA== dependencies: d3-array "^3.2.0" d3-delaunay@6: version "6.0.4" - resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz#98169038733a0a5babbeda55054f795bb9e4a58b" + resolved "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz" integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A== dependencies: delaunator "5" "d3-dispatch@1 - 3", d3-dispatch@3: version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" + resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz" integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== "d3-drag@2 - 3", d3-drag@3: version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" + resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz" integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== dependencies: d3-dispatch "1 - 3" @@ -2815,7 +974,7 @@ d3-delaunay@6: "d3-dsv@1 - 3", d3-dsv@3: version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73" + resolved "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz" integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q== dependencies: commander "7" @@ -2824,19 +983,19 @@ d3-delaunay@6: "d3-ease@1 - 3", d3-ease@3: version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz" integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== d3-fetch@3: version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22" + resolved "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz" integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw== dependencies: d3-dsv "1 - 3" d3-force@3: version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4" + resolved "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz" integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg== dependencies: d3-dispatch "1 - 3" @@ -2845,72 +1004,72 @@ d3-force@3: "d3-format@1 - 3", d3-format@3: version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + resolved "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz" integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== d3-geo@3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.1.0.tgz#74fd54e1f4cebd5185ac2039217a98d39b0a4c0e" - integrity sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA== + version "3.1.1" + resolved "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz" + integrity sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q== dependencies: d3-array "2.5.0 - 3" d3-hierarchy@3: version "3.1.2" - resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6" + resolved "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz" integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA== "d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3: version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz" integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== dependencies: d3-color "1 - 3" +d3-path@^3.1.0, "d3-path@1 - 3", d3-path@3: + version "3.1.0" + resolved "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + d3-path@1: version "1.0.9" - resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" + resolved "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz" integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== -"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" - integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== - d3-polygon@3: version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398" + resolved "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz" integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg== "d3-quadtree@1 - 3", d3-quadtree@3: version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f" + resolved "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz" integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw== d3-random@3: version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4" + resolved "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz" integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ== d3-sankey@^0.12.3: version "0.12.3" - resolved "https://registry.yarnpkg.com/d3-sankey/-/d3-sankey-0.12.3.tgz#b3c268627bd72e5d80336e8de6acbfec9d15d01d" + resolved "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz" integrity sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ== dependencies: d3-array "1 - 2" d3-shape "^1.2.0" d3-scale-chromatic@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#15b4ceb8ca2bb0dcb6d1a641ee03d59c3b62376a" - integrity sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g== + version "3.1.0" + resolved "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz" + integrity sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ== dependencies: d3-color "1 - 3" d3-interpolate "1 - 3" d3-scale@4: version "4.0.2" - resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" + resolved "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz" integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== dependencies: d3-array "2.10.0 - 3" @@ -2921,45 +1080,45 @@ d3-scale@4: "d3-selection@2 - 3", d3-selection@3: version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" + resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz" integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== -d3-shape@3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" - integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== - dependencies: - d3-path "^3.1.0" - d3-shape@^1.2.0: version "1.3.7" - resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" + resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz" integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw== dependencies: d3-path "1" +d3-shape@3: + version "3.2.0" + resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + dependencies: + d3-path "^3.1.0" + "d3-time-format@2 - 4", d3-time-format@4: version "4.1.0" - resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + resolved "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz" integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== dependencies: d3-time "1 - 3" "d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3: version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" + resolved "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz" integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== dependencies: d3-array "2 - 3" "d3-timer@1 - 3", d3-timer@3: version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz" integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== "d3-transition@2 - 3", d3-transition@3: version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" + resolved "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz" integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== dependencies: d3-color "1 - 3" @@ -2970,7 +1129,7 @@ d3-shape@^1.2.0: d3-zoom@3: version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" + resolved "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz" integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== dependencies: d3-dispatch "1 - 3" @@ -2979,10 +1138,10 @@ d3-zoom@3: d3-selection "2 - 3" d3-transition "2 - 3" -d3@^7.4.0, d3@^7.8.2: - version "7.8.5" - resolved "https://registry.yarnpkg.com/d3/-/d3-7.8.5.tgz#fde4b760d4486cdb6f0cc8e2cbff318af844635c" - integrity sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA== +d3@^7.9.0: + version "7.9.0" + resolved "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz" + integrity sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA== dependencies: d3-array "3" d3-axis "3" @@ -3015,363 +1174,102 @@ d3@^7.4.0, d3@^7.8.2: d3-transition "3" d3-zoom "3" -dagre-d3-es@7.0.10: - version "7.0.10" - resolved "https://registry.yarnpkg.com/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz#19800d4be674379a3cd8c86a8216a2ac6827cadc" - integrity sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A== +dagre-d3-es@7.0.11: + version "7.0.11" + resolved "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz" + integrity sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw== dependencies: - d3 "^7.8.2" + d3 "^7.9.0" lodash-es "^4.17.21" -damerau-levenshtein@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" - integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== - -dayjs@^1.11.7: - version "1.11.10" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" - integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== - -debug@4.3.4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" +dayjs@^1.11.10: + version "1.11.13" + resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz" + integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== -debug@^3.2.7: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== +debug@^4.0.0, debug@^4.1.1, debug@^4.3.6: + version "4.3.7" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== dependencies: - ms "^2.1.1" + ms "^2.1.3" decode-named-character-reference@^1.0.0: version "1.0.2" - resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" + resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz" integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== dependencies: character-entities "^2.0.0" -dedent@^1.0.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.1.tgz#4f3fc94c8b711e9bb2800d185cd6ad20f2a90aff" - integrity sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg== - -deep-is@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== - -deepmerge@^4.2.2: - version "4.3.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" - integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== - -define-data-property@^1.0.1, define-data-property@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - -define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" - integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== - dependencies: - define-data-property "^1.0.1" - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - delaunator@5: version "5.0.1" - resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.1.tgz#39032b08053923e924d6094fe2cde1a99cc51278" + resolved "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz" integrity sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw== dependencies: robust-predicates "^3.0.2" -dequal@^2.0.0, dequal@^2.0.3: +dequal@^2.0.0: version "2.0.3" - resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== -detect-libc@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" - integrity sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw== - -detect-newline@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" - integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== - devlop@^1.0.0, devlop@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + resolved "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz" integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== dependencies: dequal "^2.0.0" -didyoumean@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" - integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== - -diff-sequences@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" - integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== - -diff@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" - integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -dlv@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" - integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== - -doctrine@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" - integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== - dependencies: - esutils "^2.0.2" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -dompurify@^3.0.5: - version "3.0.8" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.8.tgz#e0021ab1b09184bc8af7e35c7dd9063f43a8a437" - integrity sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ== - -eastasianwidth@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" - integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== - -electron-to-chromium@^1.4.668: - version "1.4.673" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.673.tgz#1f077d9a095761804aec7ec6346c3f4b69b56534" - integrity sha512-zjqzx4N7xGdl5468G+vcgzDhaHkaYgVcf9MqgexcTqsl2UHSCmOj/Bi3HAprg4BZCpC7HyD8a6nZl6QAZf72gw== - -elkjs@^0.9.0: - version "0.9.1" - resolved "https://registry.yarnpkg.com/elkjs/-/elkjs-0.9.1.tgz#fd1524b3f0bed72dc65ba107ae91bcf04b5582bd" - integrity sha512-JWKDyqAdltuUcyxaECtYG6H4sqysXSLeoXuGUBfRNESMTkj+w+qdb0jya8Z/WI0jVd03WQtCGhS6FOFtlhD5FQ== - -emittery@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" - integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== - -emoji-regex@^10.3.0: - version "10.3.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.3.0.tgz#76998b9268409eb3dae3de989254d456e70cfe23" - integrity sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emoji-regex@^9.2.2: - version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== +dompurify@^3.2.1: + version "3.2.2" + resolved "https://registry.npmjs.org/dompurify/-/dompurify-3.2.2.tgz" + integrity sha512-YMM+erhdZ2nkZ4fTNRTSI94mb7VG7uVF5vj5Zde7tImgnhZE3R6YW/IACGIHb2ux+QkEXMhe591N+5jWOmL4Zw== + optionalDependencies: + "@types/trusted-types" "^2.0.7" -enhanced-resolve@^5.12.0, enhanced-resolve@^5.15.0: - version "5.15.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" - integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" +emoji-regex-xs@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz" + integrity sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg== entities@^4.4.0: version "4.5.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.22.4: - version "1.22.4" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.4.tgz#26eb2e7538c3271141f5754d31aabfdb215f27bf" - integrity sha512-vZYJlk2u6qHYxBOTjAeg7qUxHdNfih64Uu2J8QqWgXZ2cri0ZpJAkzDUK/q593+mvKwlxyaxr6F1Q+3LKoQRgg== - dependencies: - array-buffer-byte-length "^1.0.1" - arraybuffer.prototype.slice "^1.0.3" - available-typed-arrays "^1.0.6" - call-bind "^1.0.7" - es-define-property "^1.0.0" - es-errors "^1.3.0" - es-set-tostringtag "^2.0.2" - es-to-primitive "^1.2.1" - function.prototype.name "^1.1.6" - get-intrinsic "^1.2.4" - get-symbol-description "^1.0.2" - globalthis "^1.0.3" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.1" - internal-slot "^1.0.7" - is-array-buffer "^3.0.4" - is-callable "^1.2.7" - is-negative-zero "^2.0.2" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - is-string "^1.0.7" - is-typed-array "^1.1.13" - is-weakref "^1.0.2" - object-inspect "^1.13.1" - object-keys "^1.1.1" - object.assign "^4.1.5" - regexp.prototype.flags "^1.5.2" - safe-array-concat "^1.1.0" - safe-regex-test "^1.0.3" - string.prototype.trim "^1.2.8" - string.prototype.trimend "^1.0.7" - string.prototype.trimstart "^1.0.7" - typed-array-buffer "^1.0.1" - typed-array-byte-length "^1.0.0" - typed-array-byte-offset "^1.0.0" - typed-array-length "^1.0.4" - unbox-primitive "^1.0.2" - which-typed-array "^1.1.14" - -es-array-method-boxes-properly@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" - integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== - -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" - -es-errors@^1.0.0, es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -es-iterator-helpers@^1.0.12, es-iterator-helpers@^1.0.15: - version "1.0.17" - resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.17.tgz#123d1315780df15b34eb181022da43e734388bb8" - integrity sha512-lh7BsUqelv4KUbR5a/ZTaGGIMLCjPGPqJ6q+Oq24YP0RdyptX1uzm4vvaqzk7Zx3bpl/76YLTTDj9L7uYQ92oQ== - dependencies: - asynciterator.prototype "^1.0.0" - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.22.4" - es-errors "^1.3.0" - es-set-tostringtag "^2.0.2" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - globalthis "^1.0.3" - has-property-descriptors "^1.0.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - internal-slot "^1.0.7" - iterator.prototype "^1.1.2" - safe-array-concat "^1.1.0" - -es-set-tostringtag@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz#11f7cc9f63376930a5f20be4915834f4bc74f9c9" - integrity sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q== - dependencies: - get-intrinsic "^1.2.2" - has-tostringtag "^1.0.0" - hasown "^2.0.0" - -es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" - integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== +esast-util-from-estree@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz" + integrity sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ== dependencies: - hasown "^2.0.0" + "@types/estree-jsx" "^1.0.0" + devlop "^1.0.0" + estree-util-visit "^2.0.0" + unist-util-position-from-estree "^2.0.0" -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== +esast-util-from-js@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz" + integrity sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw== dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -escalade@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" - integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - -escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + "@types/estree-jsx" "^1.0.0" + acorn "^8.0.0" + esast-util-from-estree "^2.0.0" + vfile-message "^4.0.0" escape-string-regexp@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz" integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== -eslint-compat-utils@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/eslint-compat-utils/-/eslint-compat-utils-0.1.2.tgz#f45e3b5ced4c746c127cf724fb074cd4e730d653" - integrity sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg== - -eslint-compat-utils@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/eslint-compat-utils/-/eslint-compat-utils-0.4.1.tgz#498d9dad03961174a283f7741838a3fbe4a34e89" - integrity sha512-5N7ZaJG5pZxUeNNJfUchurLVrunD1xJvyg5kYOIVF8kg1f3ajTikmAu/5fZ9w100omNPOoMjngRszh/Q/uFGMg== - dependencies: - semver "^7.5.4" - -eslint-config-next@^14.1.0: - version "14.1.0" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-14.1.0.tgz#7e309d426b8afacaba3b32fdbb02ba220b6d0a97" - integrity sha512-SBX2ed7DoRFXC6CQSLc/SbLY9Ut6HxNB2wPTcoIWjUMd7aF7O/SIE7111L8FdZ9TXsNV4pulUDnfthpyPtbFUg== +eslint-config-next@^14.2.5: + version "14.2.19" + resolved "file:../node_modules/.pnpm/eslint-config-next@14.2.19_eslint@8.57.1_typescript@5.6.3/node_modules/eslint-config-next" dependencies: - "@next/eslint-plugin-next" "14.1.0" + "@next/eslint-plugin-next" "14.2.19" "@rushstack/eslint-patch" "^1.3.3" - "@typescript-eslint/parser" "^5.4.2 || ^6.0.0" + "@typescript-eslint/eslint-plugin" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0" eslint-import-resolver-node "^0.3.6" eslint-import-resolver-typescript "^3.5.2" eslint-plugin-import "^2.28.1" @@ -3379,147 +1277,45 @@ eslint-config-next@^14.1.0: eslint-plugin-react "^7.33.2" eslint-plugin-react-hooks "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" -eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: - version "0.3.9" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" - integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== - dependencies: - debug "^3.2.7" - is-core-module "^2.13.0" - resolve "^1.22.4" - -eslint-import-resolver-typescript@^3.5.2: - version "3.6.1" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz#7b983680edd3f1c5bce1a5829ae0bc2d57fe9efa" - integrity sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg== - dependencies: - debug "^4.3.4" - enhanced-resolve "^5.12.0" - eslint-module-utils "^2.7.4" - fast-glob "^3.3.1" - get-tsconfig "^4.5.0" - is-core-module "^2.11.0" - is-glob "^4.0.3" - -eslint-module-utils@^2.7.4, eslint-module-utils@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz#e439fee65fc33f6bba630ff621efc38ec0375c49" - integrity sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw== - dependencies: - debug "^3.2.7" - -eslint-plugin-es-x@^7.5.0: - version "7.5.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-es-x/-/eslint-plugin-es-x-7.5.0.tgz#d08d9cd155383e35156c48f736eb06561d07ba92" - integrity sha512-ODswlDSO0HJDzXU0XvgZ3lF3lS3XAZEossh15Q2UHjwrJggWeBoKqqEsLTZLXl+dh5eOAozG0zRcYtuE35oTuQ== - dependencies: - "@eslint-community/eslint-utils" "^4.1.2" - "@eslint-community/regexpp" "^4.6.0" - eslint-compat-utils "^0.1.2" - -eslint-plugin-import@^2.28.1: - version "2.29.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643" - integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw== - dependencies: - array-includes "^3.1.7" - array.prototype.findlastindex "^1.2.3" - array.prototype.flat "^1.3.2" - array.prototype.flatmap "^1.3.2" - debug "^3.2.7" - doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.9" - eslint-module-utils "^2.8.0" - hasown "^2.0.0" - is-core-module "^2.13.1" - is-glob "^4.0.3" - minimatch "^3.1.2" - object.fromentries "^2.0.7" - object.groupby "^1.0.1" - object.values "^1.1.7" - semver "^6.3.1" - tsconfig-paths "^3.15.0" - eslint-plugin-jest@^27.9.0: version "27.9.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz#7c98a33605e1d8b8442ace092b60e9919730000b" - integrity sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug== + resolved "file:../node_modules/.pnpm/eslint-plugin-jest@27.9.0_@typescript-eslint+eslint-plugin@7.18.0_@typescript-eslint+parser@7_vqmloybghkwetkuwh4oxl47f6q/node_modules/eslint-plugin-jest" dependencies: "@typescript-eslint/utils" "^5.10.0" -eslint-plugin-jsonc@^2.13.0: - version "2.13.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.13.0.tgz#e05f88d3671c08ca96e87b5be6a4cfe8d66e6746" - integrity sha512-2wWdJfpO/UbZzPDABuUVvlUQjfMJa2p2iQfYt/oWxOMpXCcjuiMUSaA02gtY/Dbu82vpaSqc+O7Xq6ECHwtIxA== +eslint-plugin-jsonc@^2.16.0: + version "2.18.2" + resolved "file:../node_modules/.pnpm/eslint-plugin-jsonc@2.18.2_eslint@8.57.1/node_modules/eslint-plugin-jsonc" dependencies: "@eslint-community/eslint-utils" "^4.2.0" - eslint-compat-utils "^0.4.0" + eslint-compat-utils "^0.6.0" + eslint-json-compat-utils "^0.2.1" espree "^9.6.1" graphemer "^1.4.0" jsonc-eslint-parser "^2.0.4" natural-compare "^1.4.0" synckit "^0.6.0" -eslint-plugin-jsx-a11y@^6.7.1: - version "6.8.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz#2fa9c701d44fcd722b7c771ec322432857fcbad2" - integrity sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA== - dependencies: - "@babel/runtime" "^7.23.2" - aria-query "^5.3.0" - array-includes "^3.1.7" - array.prototype.flatmap "^1.3.2" - ast-types-flow "^0.0.8" - axe-core "=4.7.0" - axobject-query "^3.2.1" - damerau-levenshtein "^1.0.8" - emoji-regex "^9.2.2" - es-iterator-helpers "^1.0.15" - hasown "^2.0.0" - jsx-ast-utils "^3.3.5" - language-tags "^1.0.9" - minimatch "^3.1.2" - object.entries "^1.1.7" - object.fromentries "^2.0.7" - -eslint-plugin-n@^17.0.0-0: - version "17.0.0-1" - resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-17.0.0-1.tgz#98353c101a60156028fcd2e7418b5146e24c86fa" - integrity sha512-eJnqP02Kix/hmwOLWIYlGx2hXUiGX3YRhW50nim4PzduFhM8Mka76b/sA8u+pkJWr78e5dl1vJMfavsifUFuxQ== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - enhanced-resolve "^5.15.0" - eslint-plugin-es-x "^7.5.0" - get-tsconfig "^4.7.0" - globals "^13.24.0" - ignore "^5.2.4" - is-builtin-module "^3.2.1" - is-core-module "^2.12.1" - minimatch "^9.0.0" - semver "^7.5.3" - eslint-plugin-pipedream@^0.2.4: version "0.2.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-pipedream/-/eslint-plugin-pipedream-0.2.4.tgz#2d556900abb393b9a1471ed528154c2fd2026381" - integrity sha512-mKgRf5DFJnxcDantRh0b7CoSNRqPiDZMlAP9Ab/Pha8Uq7ZseIEiRGtWOJwp9tHSZnNOe1+MCN1X6yXnWC39sA== + resolved "file:../node_modules/.pnpm/eslint-plugin-pipedream@0.2.4/node_modules/eslint-plugin-pipedream" -eslint-plugin-putout@^22.4.0: - version "22.4.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-putout/-/eslint-plugin-putout-22.4.0.tgz#6eecc3cccdd32bd262bb9a277e89f46084629655" - integrity sha512-TmMvTqEFJSgxO0tpBuf5ZrgRfbqDJRUWgHUc9OpaGdPumGggvLioAV8REGvOvvBTyx17fyMnePXu5QXPXJmY7w== +eslint-plugin-putout@^22.10.0: + version "22.10.0" + resolved "file:../node_modules/.pnpm/eslint-plugin-putout@22.10.0_eslint@8.57.1_putout@36.13.1_eslint@8.57.1_typescript@5.6.3_/node_modules/eslint-plugin-putout" dependencies: "@babel/core" "^8.0.0-alpha.1" "@babel/eslint-parser" "^8.0.0-alpha.1" "@eslint/eslintrc" "^3.0.0" "@putout/engine-parser" "^10.0.0" "@putout/eslint" "^3.0.0" - "@putout/eslint-config" "^8.0.0" - "@stylistic/eslint-plugin-jsx" "^1.0.0" - "@stylistic/eslint-plugin-ts" "^1.0.0" + "@putout/eslint-config" "^9.0.0" + "@stylistic/eslint-plugin-jsx" "^2.1.0" + "@stylistic/eslint-plugin-ts" "^2.1.0" "@typescript-eslint/eslint-plugin" "^7.0.1" "@typescript-eslint/parser" "^7.0.1" align-spaces "^1.0.0" - eslint-plugin-n "^17.0.0-0" + eslint-plugin-n "^17.0.0" eslint-plugin-react "^7.32.2" parse-import-specifiers "^1.0.1" synckit "^0.9.0" @@ -3527,69 +1323,15 @@ eslint-plugin-putout@^22.4.0: try-to-catch "^3.0.1" typescript "^5.0.4" -"eslint-plugin-react-hooks@^4.5.0 || 5.0.0-canary-7118f5dd7-20230705": - version "4.6.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" - integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== - -eslint-plugin-react@^7.32.2, eslint-plugin-react@^7.33.2: - version "7.33.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz#69ee09443ffc583927eafe86ffebb470ee737608" - integrity sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw== - dependencies: - array-includes "^3.1.6" - array.prototype.flatmap "^1.3.1" - array.prototype.tosorted "^1.1.1" - doctrine "^2.1.0" - es-iterator-helpers "^1.0.12" - estraverse "^5.3.0" - jsx-ast-utils "^2.4.1 || ^3.0.0" - minimatch "^3.1.2" - object.entries "^1.1.6" - object.fromentries "^2.0.6" - object.hasown "^1.1.2" - object.values "^1.1.6" - prop-types "^15.8.1" - resolve "^2.0.0-next.4" - semver "^6.3.1" - string.prototype.matchall "^4.0.8" - -eslint-scope@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - -eslint-scope@^7.1.1, eslint-scope@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" - integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: - version "3.4.3" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" - integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== - -eslint-visitor-keys@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb" - integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw== - -eslint@^8.56.0: - version "8.56.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.56.0.tgz#4957ce8da409dc0809f99ab07a1b94832ab74b15" - integrity sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ== +eslint@^8.57.0: + version "8.57.1" + resolved "file:../node_modules/.pnpm/eslint@8.57.1/node_modules/eslint" dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.56.0" - "@humanwhocodes/config-array" "^0.11.13" + "@eslint/js" "8.57.1" + "@humanwhocodes/config-array" "^0.13.0" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" "@ungap/structured-clone" "^1.2.0" @@ -3624,90 +1366,55 @@ eslint@^8.56.0: strip-ansi "^6.0.1" text-table "^0.2.0" -espree@^10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-10.0.1.tgz#600e60404157412751ba4a6f3a2ee1a42433139f" - integrity sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww== - dependencies: - acorn "^8.11.3" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^4.0.0" - -espree@^9.0.0, espree@^9.6.0, espree@^9.6.1: - version "9.6.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" - integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== - dependencies: - acorn "^8.9.0" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.1" +esm@^3.2.25: + version "3.2.25" + resolved "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz" + integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== -esprima@^4.0.0, esprima@~4.0.0: +esprima@^4.0.0: version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.4.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" - integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -estree-to-babel@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/estree-to-babel/-/estree-to-babel-9.0.0.tgz#2bcf6638a0cd0f82c71214674d1e96c50fe54e0e" - integrity sha512-+17i9q/yMgq/QKMj/4XnXCMmwNGeQ7TS2vjgJfmtyKNpQEUeDyawcx7KNaU/SPk9AzdJpjEitz/jJyck6hB7Tw== - dependencies: - "@putout/babel" "^2.0.0" - -estree-util-attach-comments@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/estree-util-attach-comments/-/estree-util-attach-comments-2.1.1.tgz#ee44f4ff6890ee7dfb3237ac7810154c94c63f84" - integrity sha512-+5Ba/xGGS6mnwFbXIuQiDPTbuTxuMCooq3arVv7gPZtYpjp+VXH/NkHAP35OOefPhNG/UGqU3vt/LTABwcHX0w== +estree-util-attach-comments@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz" + integrity sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw== dependencies: "@types/estree" "^1.0.0" -estree-util-build-jsx@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/estree-util-build-jsx/-/estree-util-build-jsx-2.2.2.tgz#32f8a239fb40dc3f3dca75bb5dcf77a831e4e47b" - integrity sha512-m56vOXcOBuaF+Igpb9OPAy7f9w9OIkb5yhjsZuaPm7HoGi4oTOQi0h2+yZ+AtKklYFZ+rPC4n0wYCJCEU1ONqg== +estree-util-build-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz" + integrity sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ== dependencies: "@types/estree-jsx" "^1.0.0" - estree-util-is-identifier-name "^2.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" estree-walker "^3.0.0" estree-util-is-identifier-name@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-2.1.0.tgz#fb70a432dcb19045e77b05c8e732f1364b4b49b2" + resolved "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-2.1.0.tgz" integrity sha512-bEN9VHRyXAUOjkKVQVvArFym08BTWB0aJPppZZr0UNyAqWsLaVfAqP7hbaTJjzHifmB5ebnR8Wm7r7yGN/HonQ== estree-util-is-identifier-name@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz#0b5ef4c4ff13508b34dcd01ecfa945f61fce5dbd" + resolved "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz" integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg== -estree-util-to-js@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/estree-util-to-js/-/estree-util-to-js-1.2.0.tgz#0f80d42443e3b13bd32f7012fffa6f93603f4a36" - integrity sha512-IzU74r1PK5IMMGZXUVZbmiu4A1uhiPgW5hm1GjcOfr4ZzHaMPpLNJjR7HjXiIOzi25nZDrgFTobHTkV5Q6ITjA== +estree-util-scope@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz" + integrity sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ== + dependencies: + "@types/estree" "^1.0.0" + devlop "^1.0.0" + +estree-util-to-js@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz" + integrity sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg== dependencies: "@types/estree-jsx" "^1.0.0" astring "^1.8.0" @@ -3715,40 +1422,35 @@ estree-util-to-js@^1.1.0: estree-util-value-to-estree@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/estree-util-value-to-estree/-/estree-util-value-to-estree-1.3.0.tgz#1d3125594b4d6680f666644491e7ac1745a3df49" + resolved "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-1.3.0.tgz" integrity sha512-Y+ughcF9jSUJvncXwqRageavjrNPAI+1M/L3BI3PyLp1nmgYTGUXU6t5z1Y7OWuThoDdhPME07bQU+d5LxdJqw== dependencies: is-plain-obj "^3.0.0" -estree-util-visit@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/estree-util-visit/-/estree-util-visit-1.2.1.tgz#8bc2bc09f25b00827294703835aabee1cc9ec69d" - integrity sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw== +estree-util-value-to-estree@^3.0.1: + version "3.2.1" + resolved "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.2.1.tgz" + integrity sha512-Vt2UOjyPbNQQgT5eJh+K5aATti0OjCIAGc9SgMdOFYbohuifsWclR74l0iZTJwePMgWYdX1hlVS+dedH9XV8kw== + dependencies: + "@types/estree" "^1.0.0" + +estree-util-visit@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz" + integrity sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww== dependencies: "@types/estree-jsx" "^1.0.0" - "@types/unist" "^2.0.0" + "@types/unist" "^3.0.0" estree-walker@^3.0.0: version "3.0.3" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz" integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== dependencies: "@types/estree" "^1.0.0" -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -eventemitter3@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" - integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== - -execa@8.0.1: +execa@^8.0.1: version "8.0.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" - integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== dependencies: cross-spawn "^7.0.3" get-stream "^8.0.1" @@ -3760,395 +1462,51 @@ execa@8.0.1: signal-exit "^4.1.0" strip-final-newline "^3.0.0" -execa@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.8.0.tgz#d8d76bbc1b55217ed190fd6dd49d3c774ecfc8da" - integrity sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA== - dependencies: - cross-spawn "^5.0.1" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -execa@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.0" - human-signals "^2.1.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.1" - onetime "^5.1.2" - signal-exit "^3.0.3" - strip-final-newline "^2.0.0" - -exit@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" - integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== - -expect@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" - integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== - dependencies: - "@jest/expect-utils" "^29.7.0" - jest-get-type "^29.6.3" - jest-matcher-utils "^29.7.0" - jest-message-util "^29.7.0" - jest-util "^29.7.0" - extend-shallow@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + resolved "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz" integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== dependencies: is-extendable "^0.1.0" extend@^3.0.0: version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.1: - version "3.3.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== - -fastq@^1.6.0: - version "1.17.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" - integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== - dependencies: - reusify "^1.0.4" - -fb-watchman@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" - integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== - dependencies: - bser "2.1.1" - -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== - dependencies: - flat-cache "^3.0.4" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -find-up@^4.0.0, find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -find-up@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-7.0.0.tgz#e8dec1455f74f78d888ad65bf7ca13dd2b4e66fb" - integrity sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g== - dependencies: - locate-path "^7.2.0" - path-exists "^5.0.0" - unicorn-magic "^0.1.0" - -flat-cache@^3.0.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" - integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== +fault@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz" + integrity sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ== dependencies: - flatted "^3.2.9" - keyv "^4.5.3" - rimraf "^3.0.2" + format "^0.2.0" -flatted@^3.2.9: - version "3.2.9" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" - integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== - -flexsearch@^0.7.31: +flexsearch@^0.7.43: version "0.7.43" - resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.7.43.tgz#34f89b36278a466ce379c5bf6fb341965ed3f16c" - integrity sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg== - -focus-visible@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/focus-visible/-/focus-visible-5.2.0.tgz#3a9e41fccf587bd25dcc2ef045508284f0a4d6b3" - integrity sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ== - -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - -foreground-child@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" - integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== - dependencies: - cross-spawn "^7.0.0" - signal-exit "^4.0.1" - -fraction.js@^4.3.7: - version "4.3.7" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" - integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@^2.3.2, fsevents@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== -fullstore@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/fullstore/-/fullstore-3.0.0.tgz#680d5fe282c5f51f67900dbad3fd53e62c954770" - integrity sha512-EEIdG+HWpyygWRwSLIZy+x4u0xtghjHNfhQb0mI5825Mmjq6oFESFUY0hoZigEgd3KH8GX+ZOCK9wgmOiS7VBQ== - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" - integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - functions-have-names "^1.2.3" - -functions-have-names@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" - integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== - -gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-east-asian-width@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz#5e6ebd9baee6fb8b7b6bd505221065f0cd91f64e" - integrity sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA== - -get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" - -get-package-type@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" - integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== - -get-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" - integrity sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ== - -get-stream@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +format@^0.2.0: + version "0.2.2" + resolved "https://registry.npmjs.org/format/-/format-0.2.2.tgz" + integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== get-stream@^8.0.1: version "8.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz" integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== -get-symbol-description@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" - integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== - dependencies: - call-bind "^1.0.5" - es-errors "^1.3.0" - get-intrinsic "^1.2.4" - -get-tsconfig@^4.5.0, get-tsconfig@^4.7.0: - version "4.7.2" - resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.7.2.tgz#0dcd6fb330391d46332f4c6c1bf89a6514c2ddce" - integrity sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A== - dependencies: - resolve-pkg-maps "^1.0.0" - -git-up@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/git-up/-/git-up-7.0.0.tgz#bace30786e36f56ea341b6f69adfd83286337467" - integrity sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ== - dependencies: - is-ssh "^1.4.0" - parse-url "^8.1.0" - -git-url-parse@^13.1.0: - version "13.1.1" - resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-13.1.1.tgz#664bddf0857c6a75b3c1f0ae6239abb08a1486d4" - integrity sha512-PCFJyeSSdtnbfhSNRw9Wk96dDCNx+sogTe4YNXeXSJxt7xz5hvXekuRn9JX7m+Mf4OscCu8h+mtAl3+h5Fo8lQ== - dependencies: - git-up "^7.0.0" - github-slugger@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-2.0.0.tgz#52cf2f9279a21eb6c59dd385b410f0c0adda8f1a" + resolved "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz" integrity sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw== -glob-parent@^5.1.2, glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - -glob@10.3.10, glob@^10.3.10: - version "10.3.10" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" - integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== - dependencies: - foreground-child "^3.1.0" - jackspeak "^2.3.5" - minimatch "^9.0.1" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry "^1.10.1" - -glob@^7.1.3, glob@^7.1.4: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globals@^13.19.0, globals@^13.24.0, globals@^13.5.0: - version "13.24.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" - integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== - dependencies: - type-fest "^0.20.2" - -globalthis@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" - integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== - dependencies: - define-properties "^1.1.3" - -globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" - -graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.9: +graceful-fs@^4.2.11: version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -graphemer@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" - integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== - gray-matter@^4.0.3: version "4.0.3" - resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.3.tgz#e893c064825de73ea1f5f7d88c7a9f7274288798" + resolved "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz" integrity sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q== dependencies: js-yaml "^3.13.1" @@ -4156,69 +1514,14 @@ gray-matter@^4.0.3: section-matter "^1.0.0" strip-bom-string "^1.0.0" -has-bigints@^1.0.1, has-bigints@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" - integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== - -has-flag@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" - integrity sha512-P+1n3MnwjR/Epg9BBo1KT8qbye2g2Ou4sFumihwt6I4tsUX7jnLcX4BTOSKg/B1ZrIYMN9FcEnG4x5a7NB8Eng== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.1, has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" - integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== - -has-symbols@^1.0.2, has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-tostringtag@^1.0.0, has-tostringtag@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" - -hash-obj@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/hash-obj/-/hash-obj-4.0.0.tgz#3fafeb0b5f17994441dbe04efbdee82e26b74c8c" - integrity sha512-FwO1BUVWkyHasWDW4S8o0ssQXjvyghLV2rfVhnN36b2bbcj45eGiuzdn9XOvOpjV3TKQD7Gm2BWNXdE9V4KKYg== - dependencies: - is-obj "^3.0.0" - sort-keys "^5.0.0" - type-fest "^1.0.2" - -hasown@^2.0.0, hasown@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.1.tgz#26f48f039de2c0f8d3356c223fb8d50253519faa" - integrity sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA== - dependencies: - function-bind "^1.1.2" +hachure-fill@^0.5.2: + version "0.5.2" + resolved "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz" + integrity sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg== hast-util-from-dom@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/hast-util-from-dom/-/hast-util-from-dom-5.0.0.tgz#d32edd25bf28f4b178b5ae318f8d05762e67bd16" + resolved "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.0.tgz" integrity sha512-d6235voAp/XR3Hh5uy7aGLbM3S4KamdW0WEgOaU1YoewnuYw4HXb5eRtv9g65m/RFGEfUY1Mw4UqCc5Y8L4Stg== dependencies: "@types/hast" "^3.0.0" @@ -4227,7 +1530,7 @@ hast-util-from-dom@^5.0.0: hast-util-from-html-isomorphic@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz#b31baee386a899a2472326a3c5692f29f86d1d3c" + resolved "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz" integrity sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw== dependencies: "@types/hast" "^3.0.0" @@ -4237,7 +1540,7 @@ hast-util-from-html-isomorphic@^2.0.0: hast-util-from-html@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/hast-util-from-html/-/hast-util-from-html-2.0.1.tgz#9cd38ee81bf40b2607368b92a04b0905fa987488" + resolved "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.1.tgz" integrity sha512-RXQBLMl9kjKVNkJTIO6bZyb2n+cUH8LFaSSzo82jiLT6Tfc+Pt7VQCS+/h3YwG4jaNE2TA2sdJisGWR+aJrp0g== dependencies: "@types/hast" "^3.0.0" @@ -4249,7 +1552,7 @@ hast-util-from-html@^2.0.0: hast-util-from-parse5@^8.0.0: version "8.0.1" - resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz#654a5676a41211e14ee80d1b1758c399a0327651" + resolved "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz" integrity sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ== dependencies: "@types/hast" "^3.0.0" @@ -4263,21 +1566,21 @@ hast-util-from-parse5@^8.0.0: hast-util-is-element@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz#6e31a6532c217e5b533848c7e52c9d9369ca0932" + resolved "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz" integrity sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g== dependencies: "@types/hast" "^3.0.0" hast-util-parse-selector@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27" + resolved "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz" integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A== dependencies: "@types/hast" "^3.0.0" hast-util-raw@^9.0.0: version "9.0.2" - resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-9.0.2.tgz#39b4a4886bd9f0a5dd42e86d02c966c2c152884c" + resolved "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.2.tgz" integrity sha512-PldBy71wO9Uq1kyaMch9AHIghtQvIwxBUkv823pKmkTM3oV1JxtsTNYdevMxvUHqcnOAuO65JKU2+0NOxc2ksA== dependencies: "@types/hast" "^3.0.0" @@ -4294,30 +1597,70 @@ hast-util-raw@^9.0.0: web-namespaces "^2.0.0" zwitch "^2.0.0" -hast-util-to-estree@^2.0.0: - version "2.3.3" - resolved "https://registry.yarnpkg.com/hast-util-to-estree/-/hast-util-to-estree-2.3.3.tgz#da60142ffe19a6296923ec222aba73339c8bf470" - integrity sha512-ihhPIUPxN0v0w6M5+IiAZZrn0LH2uZomeWwhn7uP7avZC6TE7lIiEh2yBMPr5+zi1aUCXq6VoYRgs2Bw9xmycQ== +hast-util-to-estree@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.0.tgz" + integrity sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw== dependencies: "@types/estree" "^1.0.0" "@types/estree-jsx" "^1.0.0" - "@types/hast" "^2.0.0" - "@types/unist" "^2.0.0" + "@types/hast" "^3.0.0" comma-separated-tokens "^2.0.0" - estree-util-attach-comments "^2.0.0" - estree-util-is-identifier-name "^2.0.0" - hast-util-whitespace "^2.0.0" - mdast-util-mdx-expression "^1.0.0" - mdast-util-mdxjs-esm "^1.0.0" + devlop "^1.0.0" + estree-util-attach-comments "^3.0.0" + estree-util-is-identifier-name "^3.0.0" + hast-util-whitespace "^3.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" property-information "^6.0.0" space-separated-tokens "^2.0.0" - style-to-object "^0.4.1" - unist-util-position "^4.0.0" + style-to-object "^0.4.0" + unist-util-position "^5.0.0" zwitch "^2.0.0" +hast-util-to-estree@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.0.tgz" + integrity sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw== + dependencies: + "@types/estree" "^1.0.0" + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + estree-util-attach-comments "^3.0.0" + estree-util-is-identifier-name "^3.0.0" + hast-util-whitespace "^3.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + style-to-object "^0.4.0" + unist-util-position "^5.0.0" + zwitch "^2.0.0" + +hast-util-to-html@^9.0.3: + version "9.0.3" + resolved "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz" + integrity sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + comma-separated-tokens "^2.0.0" + hast-util-whitespace "^3.0.0" + html-void-elements "^3.0.0" + mdast-util-to-hast "^13.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + stringify-entities "^4.0.0" + zwitch "^2.0.4" + hast-util-to-jsx-runtime@^2.0.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz#3ed27caf8dc175080117706bf7269404a0aa4f7c" + resolved "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz" integrity sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ== dependencies: "@types/estree" "^1.0.0" @@ -4338,7 +1681,7 @@ hast-util-to-jsx-runtime@^2.0.0: hast-util-to-parse5@^8.0.0: version "8.0.0" - resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz#477cd42d278d4f036bc2ea58586130f6f39ee6ed" + resolved "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz" integrity sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw== dependencies: "@types/hast" "^3.0.0" @@ -4349,9 +1692,16 @@ hast-util-to-parse5@^8.0.0: web-namespaces "^2.0.0" zwitch "^2.0.0" +hast-util-to-string@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz" + integrity sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A== + dependencies: + "@types/hast" "^3.0.0" + hast-util-to-text@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/hast-util-to-text/-/hast-util-to-text-4.0.0.tgz#7f33a45d0bf7981ead44e82d9d8d75f511b3642f" + resolved "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.0.tgz" integrity sha512-EWiE1FSArNBPUo1cKWtzqgnuRQwEeQbQtnFJRYV1hb1BWDgrAlBU0ExptvZMM/KSA82cDpm2sFGf3Dmc5Mza3w== dependencies: "@types/hast" "^3.0.0" @@ -4359,21 +1709,16 @@ hast-util-to-text@^4.0.0: hast-util-is-element "^3.0.0" unist-util-find-after "^5.0.0" -hast-util-whitespace@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz#0ec64e257e6fc216c7d14c8a1b74d27d650b4557" - integrity sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng== - hast-util-whitespace@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" + resolved "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz" integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== dependencies: "@types/hast" "^3.0.0" hastscript@^8.0.0: version "8.0.0" - resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-8.0.0.tgz#4ef795ec8dee867101b9f23cc830d4baf4fd781a" + resolved "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz" integrity sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw== dependencies: "@types/hast" "^3.0.0" @@ -4382,1246 +1727,248 @@ hastscript@^8.0.0: property-information "^6.0.0" space-separated-tokens "^2.0.0" -heap@^0.2.6: - version "0.2.7" - resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.7.tgz#1e6adf711d3f27ce35a81fe3b7bd576c2260a8fc" - integrity sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg== - -html-escaper@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" - integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== - -html-url-attributes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/html-url-attributes/-/html-url-attributes-3.0.0.tgz#fc4abf0c3fb437e2329c678b80abb3c62cff6f08" - integrity sha512-/sXbVCWayk6GDVg3ctOX6nxaVj7So40FcFAnWlWGNAB1LpYKcV5Cd10APjPjW80O7zYW2MsjBV4zZ7IZO5fVow== - -html-void-elements@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" - integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== - -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== - -human-signals@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" - integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== - -iconv-lite@0.6: - version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -ignore@^5.2.0, ignore@^5.2.4: - version "5.3.1" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" - integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== - -import-fresh@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-local@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" - integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== - dependencies: - pkg-dir "^4.2.0" - resolve-cwd "^3.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inline-style-parser@0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" - integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== - -inline-style-parser@0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.2.2.tgz#d498b4e6de0373458fc610ff793f6b14ebf45633" - integrity sha512-EcKzdTHVe8wFVOGEYXiW9WmJXPjqi1T+234YpJr98RiFYKHV3cdy1+3mkTE+KHTHxFFLH51SfaGOoUdW+v7ViQ== - -internal-slot@^1.0.5, internal-slot@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" - integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== - dependencies: - es-errors "^1.3.0" - hasown "^2.0.0" - side-channel "^1.0.4" - -"internmap@1 - 2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" - integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== - -internmap@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" - integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== - -intersection-observer@^0.12.2: - version "0.12.2" - resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.12.2.tgz#4a45349cc0cd91916682b1f44c28d7ec737dc375" - integrity sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg== - -is-alphabetical@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" - integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== - -is-alphanumerical@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" - integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== - dependencies: - is-alphabetical "^2.0.0" - is-decimal "^2.0.0" - -is-arguments@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-array-buffer@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" - integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.1" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== - -is-arrayish@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" - integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== - -is-async-function@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" - integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== - dependencies: - has-tostringtag "^1.0.0" - -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== - dependencies: - has-bigints "^1.0.1" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-buffer@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" - integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== - -is-builtin-module@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" - integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== - dependencies: - builtin-modules "^3.3.0" - -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - -is-core-module@^2.11.0, is-core-module@^2.12.1, is-core-module@^2.13.0, is-core-module@^2.13.1: - version "2.13.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" - integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== - dependencies: - hasown "^2.0.0" - -is-date-object@^1.0.1, is-date-object@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== - dependencies: - has-tostringtag "^1.0.0" - -is-decimal@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" - integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== - -is-extendable@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-finalizationregistry@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" - integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== - dependencies: - call-bind "^1.0.2" - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-fullwidth-code-point@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88" - integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== - -is-fullwidth-code-point@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz#9609efced7c2f97da7b60145ef481c787c7ba704" - integrity sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA== - dependencies: - get-east-asian-width "^1.0.0" - -is-generator-fn@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" - integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== - -is-generator-function@^1.0.10, is-generator-function@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" - integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== - dependencies: - has-tostringtag "^1.0.0" - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-hexadecimal@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" - integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== - -is-map@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" - integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== - -is-nan@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" - integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - -is-negative-zero@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" - integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== - -is-number-object@^1.0.4: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" - integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== - dependencies: - has-tostringtag "^1.0.0" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-obj@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-3.0.0.tgz#b0889f1f9f8cb87e87df53a8d1230a2250f8b9be" - integrity sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ== - -is-path-inside@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - -is-plain-obj@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" - integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== - -is-plain-obj@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" - integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== - -is-reference@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-3.0.2.tgz#154747a01f45cd962404ee89d43837af2cba247c" - integrity sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg== - dependencies: - "@types/estree" "*" - -is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-set@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" - integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== - -is-shared-array-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" - integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== - dependencies: - call-bind "^1.0.2" - -is-ssh@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.4.0.tgz#4f8220601d2839d8fa624b3106f8e8884f01b8b2" - integrity sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ== - dependencies: - protocols "^2.0.1" - -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" - integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== - -is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== - dependencies: - has-tostringtag "^1.0.0" - -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== - dependencies: - has-symbols "^1.0.2" - -is-typed-array@^1.1.10, is-typed-array@^1.1.13, is-typed-array@^1.1.3, is-typed-array@^1.1.9: - version "1.1.13" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" - integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== - dependencies: - which-typed-array "^1.1.14" - -is-weakmap@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" - integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== - -is-weakref@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" - integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== - dependencies: - call-bind "^1.0.2" - -is-weakset@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d" - integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" - -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" - integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== - -istanbul-lib-instrument@^5.0.4: - version "5.2.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" - integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== - dependencies: - "@babel/core" "^7.12.3" - "@babel/parser" "^7.14.7" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-coverage "^3.2.0" - semver "^6.3.0" - -istanbul-lib-instrument@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz#71e87707e8041428732518c6fb5211761753fbdf" - integrity sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA== - dependencies: - "@babel/core" "^7.12.3" - "@babel/parser" "^7.14.7" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-coverage "^3.2.0" - semver "^7.5.4" - -istanbul-lib-report@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" - integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== - dependencies: - istanbul-lib-coverage "^3.0.0" - make-dir "^4.0.0" - supports-color "^7.1.0" - -istanbul-lib-source-maps@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" - integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== - dependencies: - debug "^4.1.1" - istanbul-lib-coverage "^3.0.0" - source-map "^0.6.1" - -istanbul-reports@^3.1.3: - version "3.1.6" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.6.tgz#2544bcab4768154281a2f0870471902704ccaa1a" - integrity sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg== - dependencies: - html-escaper "^2.0.0" - istanbul-lib-report "^3.0.0" - -iterator.prototype@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" - integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== - dependencies: - define-properties "^1.2.1" - get-intrinsic "^1.2.1" - has-symbols "^1.0.3" - reflect.getprototypeof "^1.0.4" - set-function-name "^2.0.1" - -jackspeak@^2.3.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" - integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== - dependencies: - "@isaacs/cliui" "^8.0.2" - optionalDependencies: - "@pkgjs/parseargs" "^0.11.0" - -jessy@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/jessy/-/jessy-3.1.1.tgz#b9b080552abeff8dfb8068f2e8e399f46f8544e1" - integrity sha512-Eivuwu3H8qfm4DldbyBci4RJMgoPK3pT3BCzIWNrGPOatkl4jh91PO4LZp7N2zIz8jQlYqs5bPKOkf138jRYqw== - -jest-changed-files@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" - integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== - dependencies: - execa "^5.0.0" - jest-util "^29.7.0" - p-limit "^3.1.0" - -jest-circus@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.7.0.tgz#b6817a45fcc835d8b16d5962d0c026473ee3668a" - integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== - dependencies: - "@jest/environment" "^29.7.0" - "@jest/expect" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - co "^4.6.0" - dedent "^1.0.0" - is-generator-fn "^2.0.0" - jest-each "^29.7.0" - jest-matcher-utils "^29.7.0" - jest-message-util "^29.7.0" - jest-runtime "^29.7.0" - jest-snapshot "^29.7.0" - jest-util "^29.7.0" - p-limit "^3.1.0" - pretty-format "^29.7.0" - pure-rand "^6.0.0" - slash "^3.0.0" - stack-utils "^2.0.3" - -jest-cli@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.7.0.tgz#5592c940798e0cae677eec169264f2d839a37995" - integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== - dependencies: - "@jest/core" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/types" "^29.6.3" - chalk "^4.0.0" - create-jest "^29.7.0" - exit "^0.1.2" - import-local "^3.0.2" - jest-config "^29.7.0" - jest-util "^29.7.0" - jest-validate "^29.7.0" - yargs "^17.3.1" +html-void-elements@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz" + integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== -jest-config@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.7.0.tgz#bcbda8806dbcc01b1e316a46bb74085a84b0245f" - integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== - dependencies: - "@babel/core" "^7.11.6" - "@jest/test-sequencer" "^29.7.0" - "@jest/types" "^29.6.3" - babel-jest "^29.7.0" - chalk "^4.0.0" - ci-info "^3.2.0" - deepmerge "^4.2.2" - glob "^7.1.3" - graceful-fs "^4.2.9" - jest-circus "^29.7.0" - jest-environment-node "^29.7.0" - jest-get-type "^29.6.3" - jest-regex-util "^29.6.3" - jest-resolve "^29.7.0" - jest-runner "^29.7.0" - jest-util "^29.7.0" - jest-validate "^29.7.0" - micromatch "^4.0.4" - parse-json "^5.2.0" - pretty-format "^29.7.0" - slash "^3.0.0" - strip-json-comments "^3.1.1" - -jest-diff@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" - integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== - dependencies: - chalk "^4.0.0" - diff-sequences "^29.6.3" - jest-get-type "^29.6.3" - pretty-format "^29.7.0" +human-signals@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz" + integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== -jest-docblock@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a" - integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== +iconv-lite@0.6: + version "0.6.3" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== dependencies: - detect-newline "^3.0.0" + safer-buffer ">= 2.1.2 < 3.0.0" -jest-each@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1" - integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== - dependencies: - "@jest/types" "^29.6.3" - chalk "^4.0.0" - jest-get-type "^29.6.3" - jest-util "^29.7.0" - pretty-format "^29.7.0" +inline-style-parser@0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz" + integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== -jest-environment-node@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" - integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== - dependencies: - "@jest/environment" "^29.7.0" - "@jest/fake-timers" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - jest-mock "^29.7.0" - jest-util "^29.7.0" +inline-style-parser@0.2.2: + version "0.2.2" + resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.2.tgz" + integrity sha512-EcKzdTHVe8wFVOGEYXiW9WmJXPjqi1T+234YpJr98RiFYKHV3cdy1+3mkTE+KHTHxFFLH51SfaGOoUdW+v7ViQ== -jest-get-type@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" - integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== +internmap@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz" + integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== -jest-haste-map@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" - integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== - dependencies: - "@jest/types" "^29.6.3" - "@types/graceful-fs" "^4.1.3" - "@types/node" "*" - anymatch "^3.0.3" - fb-watchman "^2.0.0" - graceful-fs "^4.2.9" - jest-regex-util "^29.6.3" - jest-util "^29.7.0" - jest-worker "^29.7.0" - micromatch "^4.0.4" - walker "^1.0.8" - optionalDependencies: - fsevents "^2.3.2" +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== -jest-leak-detector@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" - integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== - dependencies: - jest-get-type "^29.6.3" - pretty-format "^29.7.0" +is-alphabetical@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz" + integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== -jest-matcher-utils@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" - integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== +is-alphanumerical@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz" + integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== dependencies: - chalk "^4.0.0" - jest-diff "^29.7.0" - jest-get-type "^29.6.3" - pretty-format "^29.7.0" + is-alphabetical "^2.0.0" + is-decimal "^2.0.0" -jest-message-util@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" - integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== - dependencies: - "@babel/code-frame" "^7.12.13" - "@jest/types" "^29.6.3" - "@types/stack-utils" "^2.0.0" - chalk "^4.0.0" - graceful-fs "^4.2.9" - micromatch "^4.0.4" - pretty-format "^29.7.0" - slash "^3.0.0" - stack-utils "^2.0.3" +is-decimal@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz" + integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== -jest-mock@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" - integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== - dependencies: - "@jest/types" "^29.6.3" - "@types/node" "*" - jest-util "^29.7.0" +is-docker@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz" + integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== -jest-pnp-resolver@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" - integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== +is-extendable@^0.1.0: + version "0.1.1" + resolved "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz" + integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== -jest-regex-util@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" - integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== +is-hexadecimal@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz" + integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== -jest-resolve-dependencies@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz#1b04f2c095f37fc776ff40803dc92921b1e88428" - integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== +is-inside-container@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz" + integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== dependencies: - jest-regex-util "^29.6.3" - jest-snapshot "^29.7.0" + is-docker "^3.0.0" -jest-resolve@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30" - integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== - dependencies: - chalk "^4.0.0" - graceful-fs "^4.2.9" - jest-haste-map "^29.7.0" - jest-pnp-resolver "^1.2.2" - jest-util "^29.7.0" - jest-validate "^29.7.0" - resolve "^1.20.0" - resolve.exports "^2.0.0" - slash "^3.0.0" - -jest-runner@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e" - integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== - dependencies: - "@jest/console" "^29.7.0" - "@jest/environment" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - emittery "^0.13.1" - graceful-fs "^4.2.9" - jest-docblock "^29.7.0" - jest-environment-node "^29.7.0" - jest-haste-map "^29.7.0" - jest-leak-detector "^29.7.0" - jest-message-util "^29.7.0" - jest-resolve "^29.7.0" - jest-runtime "^29.7.0" - jest-util "^29.7.0" - jest-watcher "^29.7.0" - jest-worker "^29.7.0" - p-limit "^3.1.0" - source-map-support "0.5.13" - -jest-runtime@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.7.0.tgz#efecb3141cf7d3767a3a0cc8f7c9990587d3d817" - integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== - dependencies: - "@jest/environment" "^29.7.0" - "@jest/fake-timers" "^29.7.0" - "@jest/globals" "^29.7.0" - "@jest/source-map" "^29.6.3" - "@jest/test-result" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - cjs-module-lexer "^1.0.0" - collect-v8-coverage "^1.0.0" - glob "^7.1.3" - graceful-fs "^4.2.9" - jest-haste-map "^29.7.0" - jest-message-util "^29.7.0" - jest-mock "^29.7.0" - jest-regex-util "^29.6.3" - jest-resolve "^29.7.0" - jest-snapshot "^29.7.0" - jest-util "^29.7.0" - slash "^3.0.0" - strip-bom "^4.0.0" - -jest-snapshot@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5" - integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== - dependencies: - "@babel/core" "^7.11.6" - "@babel/generator" "^7.7.2" - "@babel/plugin-syntax-jsx" "^7.7.2" - "@babel/plugin-syntax-typescript" "^7.7.2" - "@babel/types" "^7.3.3" - "@jest/expect-utils" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - babel-preset-current-node-syntax "^1.0.0" - chalk "^4.0.0" - expect "^29.7.0" - graceful-fs "^4.2.9" - jest-diff "^29.7.0" - jest-get-type "^29.6.3" - jest-matcher-utils "^29.7.0" - jest-message-util "^29.7.0" - jest-util "^29.7.0" - natural-compare "^1.4.0" - pretty-format "^29.7.0" - semver "^7.5.3" +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== -jest-util@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" - integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== - dependencies: - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - ci-info "^3.2.0" - graceful-fs "^4.2.9" - picomatch "^2.2.3" +is-plain-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== -jest-validate@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" - integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== - dependencies: - "@jest/types" "^29.6.3" - camelcase "^6.2.0" - chalk "^4.0.0" - jest-get-type "^29.6.3" - leven "^3.1.0" - pretty-format "^29.7.0" +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== -jest-watcher@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.7.0.tgz#7810d30d619c3a62093223ce6bb359ca1b28a2f2" - integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== +is-wsl@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz" + integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw== dependencies: - "@jest/test-result" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - ansi-escapes "^4.2.1" - chalk "^4.0.0" - emittery "^0.13.1" - jest-util "^29.7.0" - string-length "^4.0.1" + is-inside-container "^1.0.0" -jest-worker@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" - integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== +is64bit@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/is64bit/-/is64bit-2.0.0.tgz" + integrity sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw== dependencies: - "@types/node" "*" - jest-util "^29.7.0" - merge-stream "^2.0.0" - supports-color "^8.0.0" + system-architecture "^0.1.0" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== jest@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" - integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== + resolved "file:../node_modules/.pnpm/jest@29.7.0_@types+node@18.11.10_babel-plugin-macros@3.1.0/node_modules/jest" dependencies: "@jest/core" "^29.7.0" "@jest/types" "^29.6.3" import-local "^3.0.2" jest-cli "^29.7.0" -jiti@^1.19.1: - version "1.21.0" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" - integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== - -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-tokens@^8.0.0: - version "8.0.3" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-8.0.3.tgz#1c407ec905643603b38b6be6977300406ec48775" - integrity sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw== - js-yaml@^3.13.1: version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== dependencies: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^4.0.0, js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -jsesc@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" - integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== - -json-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" - integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== - -json-parse-even-better-errors@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== - -json5@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" - integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== - dependencies: - minimist "^1.2.0" - -json5@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - -jsonc-eslint-parser@^2.0.4: - version "2.4.0" - resolved "https://registry.yarnpkg.com/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.0.tgz#74ded53f9d716e8d0671bd167bf5391f452d5461" - integrity sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg== - dependencies: - acorn "^8.5.0" - eslint-visitor-keys "^3.0.0" - espree "^9.0.0" - semver "^7.3.5" - -jsonc-parser@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a" - integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA== - -"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: - version "3.3.5" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" - integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== - dependencies: - array-includes "^3.1.6" - array.prototype.flat "^1.3.1" - object.assign "^4.1.4" - object.values "^1.1.6" - -just-snake-case@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/just-snake-case/-/just-snake-case-3.2.0.tgz#f8eea0c97e4057b92a2fc1e5d4d8d9975a54486a" - integrity sha512-iugHP9bSE0jOq3BzN0W0rdu/OOkFirPe8FtUw6v9y37UlbUDgJ1x4xiGNfUhI6mV9dc/paaifyiyn+F+mrm8gw== - katex@^0.16.0, katex@^0.16.9: version "0.16.9" - resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.9.tgz#bc62d8f7abfea6e181250f85a56e4ef292dcb1fa" + resolved "https://registry.npmjs.org/katex/-/katex-0.16.9.tgz" integrity sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ== dependencies: commander "^8.3.0" -keyv@^4.5.3: - version "4.5.4" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" - integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== - dependencies: - json-buffer "3.0.1" - -khroma@^2.0.0: +khroma@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/khroma/-/khroma-2.1.0.tgz#45f2ce94ce231a437cf5b63c2e886e6eb42bbbb1" + resolved "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz" integrity sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw== kind-of@^6.0.0, kind-of@^6.0.2: version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -kleur@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" - integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== - -kleur@^4.0.3: - version "4.1.5" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" - integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== - -language-subtag-registry@^0.3.20: - version "0.3.22" - resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" - integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== +kolorist@^1.8.0: + version "1.8.0" + resolved "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz" + integrity sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ== -language-tags@^1.0.9: - version "1.0.9" - resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777" - integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== +langium@3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/langium/-/langium-3.0.0.tgz" + integrity sha512-+Ez9EoiByeoTu/2BXmEaZ06iPNXM6thWJp02KfBO/raSMyCJ4jw7AkWWa+zBCTm0+Tw1Fj9FOxdqSskyN5nAwg== dependencies: - language-subtag-registry "^0.3.20" + chevrotain "~11.0.3" + chevrotain-allstar "~0.3.0" + vscode-languageserver "~9.0.1" + vscode-languageserver-textdocument "~1.0.11" + vscode-uri "~3.0.8" layout-base@^1.0.0: version "1.0.2" - resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-1.0.2.tgz#1291e296883c322a9dd4c5dd82063721b53e26e2" + resolved "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz" integrity sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg== -leven@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" - integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== - -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - -lilconfig@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.0.0.tgz#f8067feb033b5b74dab4602a5f5029420be749bc" - integrity sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g== - -lilconfig@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" - integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== - -lilconfig@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.0.tgz#aabf03fd46934d0566d75b4b64ce41a2cdea1167" - integrity sha512-p3cz0JV5vw/XeouBU3Ldnp+ZkBjE+n8ydJ4mcwBrOiXXPqNlrzGBqWs9X4MWF7f+iKUBu794Y8Hh8yawiJbCjw== - -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - -lint-staged@^15.2.2: - version "15.2.2" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.2.tgz#ad7cbb5b3ab70e043fa05bff82a09ed286bc4c5f" - integrity sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw== - dependencies: - chalk "5.3.0" - commander "11.1.0" - debug "4.3.4" - execa "8.0.1" - lilconfig "3.0.0" - listr2 "8.0.1" - micromatch "4.0.5" - pidtree "0.6.0" - string-argv "0.3.2" - yaml "2.3.4" - -listr2@8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.0.1.tgz#4d3f50ae6cec3c62bdf0e94f5c2c9edebd4b9c34" - integrity sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA== - dependencies: - cli-truncate "^4.0.0" - colorette "^2.0.20" - eventemitter3 "^5.0.1" - log-update "^6.0.0" - rfdc "^1.3.0" - wrap-ansi "^9.0.0" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -locate-path@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-7.2.0.tgz#69cb1779bd90b35ab1e771e1f2f89a202c2a8a8a" - integrity sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA== - dependencies: - p-locate "^6.0.0" - -lodash-es@^4.17.21: +layout-base@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz" + integrity sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg== + +lint-staged@^15.2.7: + version "15.2.10" + resolved "file:../node_modules/.pnpm/lint-staged@15.2.10/node_modules/lint-staged" + dependencies: + chalk "~5.3.0" + commander "~12.1.0" + debug "~4.3.6" + execa "~8.0.1" + lilconfig "~3.1.2" + listr2 "~8.2.4" + micromatch "~4.0.8" + pidtree "~0.6.0" + string-argv "~0.3.2" + yaml "~2.5.0" + +local-pkg@^0.5.0: + version "0.5.1" + resolved "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz" + integrity sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ== + dependencies: + mlly "^1.7.3" + pkg-types "^1.2.1" + +lodash-es@^4.17.21, lodash-es@4.17.21: version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== -lodash.get@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== - -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -log-update@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-6.0.0.tgz#0ddeb7ac6ad658c944c1de902993fce7c33f5e59" - integrity sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw== - dependencies: - ansi-escapes "^6.2.0" - cli-cursor "^4.0.0" - slice-ansi "^7.0.0" - strip-ansi "^7.1.0" - wrap-ansi "^9.0.0" - longest-streak@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" + resolved "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz" integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== -loose-envify@^1.1.0, loose-envify@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lru-cache@^4.0.1: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -lru-cache@^7.14.1: - version "7.18.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" - integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== - -"lru-cache@^9.1.1 || ^10.0.0": - version "10.2.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" - integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== - -make-dir@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" - integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== - dependencies: - semver "^7.5.3" - -makeerror@1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" - integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== - dependencies: - tmpl "1.0.5" - -markdown-extensions@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-1.1.1.tgz#fea03b539faeaee9b4ef02a3769b455b189f7fc3" - integrity sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q== +markdown-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz" + integrity sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q== markdown-table@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd" - integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw== + version "3.0.4" + resolved "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz" + integrity sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw== -match-sorter@^6.3.1: - version "6.3.4" - resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.4.tgz#afa779d8e922c81971fbcb4781c7003ace781be7" - integrity sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg== - dependencies: - "@babel/runtime" "^7.23.8" - remove-accents "0.5.0" +marked@^13.0.2: + version "13.0.3" + resolved "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz" + integrity sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA== -mdast-util-definitions@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz#9910abb60ac5d7115d6819b57ae0bcef07a3f7a7" - integrity sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA== +mathjax-full@^3.2.2: + version "3.2.2" + resolved "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.2.tgz" + integrity sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w== dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - unist-util-visit "^4.0.0" + esm "^3.2.25" + mhchemparser "^4.1.0" + mj-context-menu "^0.6.1" + speech-rule-engine "^4.0.6" -mdast-util-find-and-replace@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz#cc2b774f7f3630da4bd592f61966fecade8b99b1" - integrity sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw== +mdast-util-find-and-replace@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz" + integrity sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA== dependencies: - "@types/mdast" "^3.0.0" + "@types/mdast" "^4.0.0" escape-string-regexp "^5.0.0" - unist-util-is "^5.0.0" - unist-util-visit-parents "^5.0.0" - -mdast-util-from-markdown@^1.0.0, mdast-util-from-markdown@^1.1.0, mdast-util-from-markdown@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0" - integrity sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - decode-named-character-reference "^1.0.0" - mdast-util-to-string "^3.1.0" - micromark "^3.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-decode-string "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - unist-util-stringify-position "^3.0.0" - uvu "^0.5.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" mdast-util-from-markdown@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz#52f14815ec291ed061f2922fd14d6689c810cb88" + resolved "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz" integrity sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA== dependencies: "@types/mdast" "^4.0.0" @@ -5637,87 +1984,117 @@ mdast-util-from-markdown@^2.0.0: micromark-util-types "^2.0.0" unist-util-stringify-position "^4.0.0" -mdast-util-gfm-autolink-literal@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz#67a13abe813d7eba350453a5333ae1bc0ec05c06" - integrity sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA== +mdast-util-from-markdown@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz" + integrity sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + mdast-util-to-string "^4.0.0" + micromark "^4.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-decode-string "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-stringify-position "^4.0.0" + +mdast-util-frontmatter@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz" + integrity sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + escape-string-regexp "^5.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + micromark-extension-frontmatter "^2.0.0" + +mdast-util-gfm-autolink-literal@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz" + integrity sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ== dependencies: - "@types/mdast" "^3.0.0" + "@types/mdast" "^4.0.0" ccount "^2.0.0" - mdast-util-find-and-replace "^2.0.0" - micromark-util-character "^1.0.0" + devlop "^1.0.0" + mdast-util-find-and-replace "^3.0.0" + micromark-util-character "^2.0.0" -mdast-util-gfm-footnote@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz#ce5e49b639c44de68d5bf5399877a14d5020424e" - integrity sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ== +mdast-util-gfm-footnote@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz" + integrity sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ== dependencies: - "@types/mdast" "^3.0.0" - mdast-util-to-markdown "^1.3.0" - micromark-util-normalize-identifier "^1.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.1.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" -mdast-util-gfm-strikethrough@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz#5470eb105b483f7746b8805b9b989342085795b7" - integrity sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ== +mdast-util-gfm-strikethrough@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz" + integrity sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg== dependencies: - "@types/mdast" "^3.0.0" - mdast-util-to-markdown "^1.3.0" + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" -mdast-util-gfm-table@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz#3552153a146379f0f9c4c1101b071d70bbed1a46" - integrity sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg== +mdast-util-gfm-table@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz" + integrity sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg== dependencies: - "@types/mdast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" markdown-table "^3.0.0" - mdast-util-from-markdown "^1.0.0" - mdast-util-to-markdown "^1.3.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" -mdast-util-gfm-task-list-item@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz#b280fcf3b7be6fd0cc012bbe67a59831eb34097b" - integrity sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ== +mdast-util-gfm-task-list-item@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz" + integrity sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ== dependencies: - "@types/mdast" "^3.0.0" - mdast-util-to-markdown "^1.3.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" -mdast-util-gfm@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz#e92f4d8717d74bdba6de57ed21cc8b9552e2d0b6" - integrity sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg== - dependencies: - mdast-util-from-markdown "^1.0.0" - mdast-util-gfm-autolink-literal "^1.0.0" - mdast-util-gfm-footnote "^1.0.0" - mdast-util-gfm-strikethrough "^1.0.0" - mdast-util-gfm-table "^1.0.0" - mdast-util-gfm-task-list-item "^1.0.0" - mdast-util-to-markdown "^1.0.0" - -mdast-util-math@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/mdast-util-math/-/mdast-util-math-2.0.2.tgz#19a06a81f31643f48cc805e7c31edb7ce739242c" - integrity sha512-8gmkKVp9v6+Tgjtq6SYx9kGPpTf6FVYRa53/DLh479aldR9AyP48qeVOgNZ5X7QUK7nOy4yw7vg6mbiGcs9jWQ== +mdast-util-gfm@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz" + integrity sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw== dependencies: - "@types/mdast" "^3.0.0" - longest-streak "^3.0.0" - mdast-util-to-markdown "^1.3.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-gfm-autolink-literal "^2.0.0" + mdast-util-gfm-footnote "^2.0.0" + mdast-util-gfm-strikethrough "^2.0.0" + mdast-util-gfm-table "^2.0.0" + mdast-util-gfm-task-list-item "^2.0.0" + mdast-util-to-markdown "^2.0.0" -mdast-util-mdx-expression@^1.0.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-1.3.2.tgz#d027789e67524d541d6de543f36d51ae2586f220" - integrity sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA== +mdast-util-math@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz" + integrity sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w== dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - mdast-util-from-markdown "^1.0.0" - mdast-util-to-markdown "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + longest-streak "^3.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.1.0" + unist-util-remove-position "^5.0.0" mdast-util-mdx-expression@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz#4968b73724d320a379110d853e943a501bfd9d87" + resolved "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz" integrity sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw== dependencies: "@types/estree-jsx" "^1.0.0" @@ -5727,27 +2104,9 @@ mdast-util-mdx-expression@^2.0.0: mdast-util-from-markdown "^2.0.0" mdast-util-to-markdown "^2.0.0" -mdast-util-mdx-jsx@^2.0.0: - version "2.1.4" - resolved "https://registry.yarnpkg.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-2.1.4.tgz#7c1f07f10751a78963cfabee38017cbc8b7786d1" - integrity sha512-DtMn9CmVhVzZx3f+optVDF8yFgQVt7FghCRNdlIaS3X5Bnym3hZwPbg/XW86vdpKjlc1PVj26SpnLGeJBXD3JA== - dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - ccount "^2.0.0" - mdast-util-from-markdown "^1.1.0" - mdast-util-to-markdown "^1.3.0" - parse-entities "^4.0.0" - stringify-entities "^4.0.0" - unist-util-remove-position "^4.0.0" - unist-util-stringify-position "^3.0.0" - vfile-message "^3.0.0" - mdast-util-mdx-jsx@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.0.tgz#5f7f204cf3f380cba1a8441142406eede1bc7660" + resolved "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.0.tgz" integrity sha512-A8AJHlR7/wPQ3+Jre1+1rq040fX9A4Q1jG8JxmSNp/PLPHg80A6475wxTp3KzHpApFH6yWxFotHrJQA3dXP6/w== dependencies: "@types/estree-jsx" "^1.0.0" @@ -5764,31 +2123,20 @@ mdast-util-mdx-jsx@^3.0.0: unist-util-stringify-position "^4.0.0" vfile-message "^4.0.0" -mdast-util-mdx@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-mdx/-/mdast-util-mdx-2.0.1.tgz#49b6e70819b99bb615d7223c088d295e53bb810f" - integrity sha512-38w5y+r8nyKlGvNjSEqWrhG0w5PmnRA+wnBvm+ulYCct7nsGYhFVb0lljS9bQav4psDAS1eGkP2LMVcZBi/aqw== - dependencies: - mdast-util-from-markdown "^1.0.0" - mdast-util-mdx-expression "^1.0.0" - mdast-util-mdx-jsx "^2.0.0" - mdast-util-mdxjs-esm "^1.0.0" - mdast-util-to-markdown "^1.0.0" - -mdast-util-mdxjs-esm@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-1.3.1.tgz#645d02cd607a227b49721d146fd81796b2e2d15b" - integrity sha512-SXqglS0HrEvSdUEfoXFtcg7DRl7S2cwOXc7jkuusG472Mmjag34DUDeOJUZtl+BVnyeO1frIgVpHlNRWc2gk/w== +mdast-util-mdx@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz" + integrity sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w== dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - mdast-util-from-markdown "^1.0.0" - mdast-util-to-markdown "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + mdast-util-to-markdown "^2.0.0" mdast-util-mdxjs-esm@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz#019cfbe757ad62dd557db35a695e7314bcc9fa97" + resolved "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz" integrity sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg== dependencies: "@types/estree-jsx" "^1.0.0" @@ -5798,40 +2146,18 @@ mdast-util-mdxjs-esm@^2.0.0: mdast-util-from-markdown "^2.0.0" mdast-util-to-markdown "^2.0.0" -mdast-util-phrasing@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz#c7c21d0d435d7fb90956038f02e8702781f95463" - integrity sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg== - dependencies: - "@types/mdast" "^3.0.0" - unist-util-is "^5.0.0" - mdast-util-phrasing@^4.0.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz#7cc0a8dec30eaf04b7b1a9661a92adb3382aa6e3" + resolved "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz" integrity sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w== dependencies: "@types/mdast" "^4.0.0" unist-util-is "^6.0.0" -mdast-util-to-hast@^12.1.0: - version "12.3.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz#045d2825fb04374e59970f5b3f279b5700f6fb49" - integrity sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw== - dependencies: - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - mdast-util-definitions "^5.0.0" - micromark-util-sanitize-uri "^1.1.0" - trim-lines "^3.0.0" - unist-util-generated "^2.0.0" - unist-util-position "^4.0.0" - unist-util-visit "^4.0.0" - -mdast-util-to-hast@^13.0.0: - version "13.1.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz#1ae54d903150a10fe04d59f03b2b95fd210b2124" - integrity sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA== +mdast-util-to-hast@^13.0.0, mdast-util-to-hast@^13.2.0: + version "13.2.0" + resolved "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz" + integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA== dependencies: "@types/hast" "^3.0.0" "@types/mdast" "^4.0.0" @@ -5843,23 +2169,9 @@ mdast-util-to-hast@^13.0.0: unist-util-visit "^5.0.0" vfile "^6.0.0" -mdast-util-to-markdown@^1.0.0, mdast-util-to-markdown@^1.3.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz#c13343cb3fc98621911d33b5cd42e7d0731171c6" - integrity sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - longest-streak "^3.0.0" - mdast-util-phrasing "^3.0.0" - mdast-util-to-string "^3.0.0" - micromark-util-decode-string "^1.0.0" - unist-util-visit "^4.0.0" - zwitch "^2.0.0" - mdast-util-to-markdown@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz#9813f1d6e0cdaac7c244ec8c6dabfdb2102ea2b4" + resolved "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz" integrity sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ== dependencies: "@types/mdast" "^4.0.0" @@ -5871,80 +2183,67 @@ mdast-util-to-markdown@^2.0.0: unist-util-visit "^5.0.0" zwitch "^2.0.0" -mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz#66f7bb6324756741c5f47a53557f0cbf16b6f789" - integrity sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg== +mdast-util-to-markdown@^2.1.0: + version "2.1.2" + resolved "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz" + integrity sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA== dependencies: - "@types/mdast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + longest-streak "^3.0.0" + mdast-util-phrasing "^4.0.0" + mdast-util-to-string "^4.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-decode-string "^2.0.0" + unist-util-visit "^5.0.0" + zwitch "^2.0.0" mdast-util-to-string@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814" + resolved "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz" integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg== dependencies: "@types/mdast" "^4.0.0" merge-stream@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -mermaid@^10.2.2: - version "10.8.0" - resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.8.0.tgz#557123be494e216a9173bbaca3b5be5551428644" - integrity sha512-9CzfSreRjdDJxX796+jW4zjEq0DVw5xVF0nWsqff8OTbrt+ml0TZ5PyYUjjUZJa2NYxYJZZXewEquxGiM8qZEA== - dependencies: - "@braintree/sanitize-url" "^6.0.1" - "@types/d3-scale" "^4.0.3" - "@types/d3-scale-chromatic" "^3.0.0" - cytoscape "^3.28.1" +mermaid@^11.0.0: + version "11.4.1" + resolved "https://registry.npmjs.org/mermaid/-/mermaid-11.4.1.tgz" + integrity sha512-Mb01JT/x6CKDWaxigwfZYuYmDZ6xtrNwNlidKZwkSrDaY9n90tdrJTV5Umk+wP1fZscGptmKFXHsXMDEVZ+Q6A== + dependencies: + "@braintree/sanitize-url" "^7.0.1" + "@iconify/utils" "^2.1.32" + "@mermaid-js/parser" "^0.3.0" + "@types/d3" "^7.4.3" + cytoscape "^3.29.2" cytoscape-cose-bilkent "^4.1.0" - d3 "^7.4.0" + cytoscape-fcose "^2.2.0" + d3 "^7.9.0" d3-sankey "^0.12.3" - dagre-d3-es "7.0.10" - dayjs "^1.11.7" - dompurify "^3.0.5" - elkjs "^0.9.0" - khroma "^2.0.0" + dagre-d3-es "7.0.11" + dayjs "^1.11.10" + dompurify "^3.2.1" + katex "^0.16.9" + khroma "^2.1.0" lodash-es "^4.17.21" - mdast-util-from-markdown "^1.3.0" - non-layered-tidy-tree-layout "^2.0.2" - stylis "^4.1.3" + marked "^13.0.2" + roughjs "^4.6.6" + stylis "^4.3.1" ts-dedent "^2.2.0" - uuid "^9.0.0" - web-worker "^1.2.0" + uuid "^9.0.1" -micromark-core-commonmark@^1.0.0, micromark-core-commonmark@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8" - integrity sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw== - dependencies: - decode-named-character-reference "^1.0.0" - micromark-factory-destination "^1.0.0" - micromark-factory-label "^1.0.0" - micromark-factory-space "^1.0.0" - micromark-factory-title "^1.0.0" - micromark-factory-whitespace "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-chunked "^1.0.0" - micromark-util-classify-character "^1.0.0" - micromark-util-html-tag-name "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-subtokenize "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.1" - uvu "^0.5.0" +mhchemparser@^4.1.0: + version "4.2.1" + resolved "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.2.1.tgz" + integrity sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ== micromark-core-commonmark@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz#50740201f0ee78c12a675bf3e68ffebc0bf931a3" + resolved "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz" integrity sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA== dependencies: decode-named-character-reference "^1.0.0" @@ -5964,195 +2263,187 @@ micromark-core-commonmark@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-extension-gfm-autolink-literal@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz#5853f0e579bbd8ef9e39a7c0f0f27c5a063a66e7" - integrity sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg== +micromark-extension-frontmatter@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz" + integrity sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg== dependencies: - micromark-util-character "^1.0.0" - micromark-util-sanitize-uri "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" + fault "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -micromark-extension-gfm-footnote@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz#05e13034d68f95ca53c99679040bc88a6f92fe2e" - integrity sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q== - dependencies: - micromark-core-commonmark "^1.0.0" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-sanitize-uri "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-extension-gfm-strikethrough@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz#c8212c9a616fa3bf47cb5c711da77f4fdc2f80af" - integrity sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw== - dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-classify-character "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-extension-gfm-table@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz#dcb46074b0c6254c3fc9cc1f6f5002c162968008" - integrity sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-extension-gfm-tagfilter@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz#aa7c4dd92dabbcb80f313ebaaa8eb3dac05f13a7" - integrity sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g== +micromark-extension-gfm-autolink-literal@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz" + integrity sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw== dependencies: - micromark-util-types "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -micromark-extension-gfm-task-list-item@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz#b52ce498dc4c69b6a9975abafc18f275b9dde9f4" - integrity sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ== +micromark-extension-gfm-footnote@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz" + integrity sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw== dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -micromark-extension-gfm@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz#e517e8579949a5024a493e49204e884aa74f5acf" - integrity sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ== - dependencies: - micromark-extension-gfm-autolink-literal "^1.0.0" - micromark-extension-gfm-footnote "^1.0.0" - micromark-extension-gfm-strikethrough "^1.0.0" - micromark-extension-gfm-table "^1.0.0" - micromark-extension-gfm-tagfilter "^1.0.0" - micromark-extension-gfm-task-list-item "^1.0.0" - micromark-util-combine-extensions "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-extension-math@^2.0.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/micromark-extension-math/-/micromark-extension-math-2.1.2.tgz#52c70cc8266cd20ada1ef5a479bfed9a19b789bf" - integrity sha512-es0CcOV89VNS9wFmyn+wyFTKweXGW4CEvdaAca6SWRWPyYCbBisnjaHLjWO4Nszuiud84jCpkHsqAJoa768Pvg== +micromark-extension-gfm-strikethrough@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz" + integrity sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw== + dependencies: + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-table@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.0.tgz" + integrity sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g== + dependencies: + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-tagfilter@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz" + integrity sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg== + dependencies: + micromark-util-types "^2.0.0" + +micromark-extension-gfm-task-list-item@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz" + integrity sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw== + dependencies: + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz" + integrity sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w== + dependencies: + micromark-extension-gfm-autolink-literal "^2.0.0" + micromark-extension-gfm-footnote "^2.0.0" + micromark-extension-gfm-strikethrough "^2.0.0" + micromark-extension-gfm-table "^2.0.0" + micromark-extension-gfm-tagfilter "^2.0.0" + micromark-extension-gfm-task-list-item "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-math@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz" + integrity sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg== dependencies: "@types/katex" "^0.16.0" + devlop "^1.0.0" katex "^0.16.0" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -micromark-extension-mdx-expression@^1.0.0: - version "1.0.8" - resolved "https://registry.yarnpkg.com/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-1.0.8.tgz#5bc1f5fd90388e8293b3ef4f7c6f06c24aff6314" - integrity sha512-zZpeQtc5wfWKdzDsHRBY003H2Smg+PUi2REhqgIhdzAa5xonhP03FcXxqFSerFiNUr5AWmHpaNPQTBVOS4lrXw== +micromark-extension-mdx-expression@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.0.tgz" + integrity sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ== dependencies: "@types/estree" "^1.0.0" - micromark-factory-mdx-expression "^1.0.0" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-events-to-acorn "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-extension-mdx-jsx@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-1.0.5.tgz#e72d24b7754a30d20fb797ece11e2c4e2cae9e82" - integrity sha512-gPH+9ZdmDflbu19Xkb8+gheqEDqkSpdCEubQyxuz/Hn8DOXiXvrXeikOoBA71+e8Pfi0/UYmU3wW3H58kr7akA== + devlop "^1.0.0" + micromark-factory-mdx-expression "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-mdx-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.1.tgz" + integrity sha512-vNuFb9czP8QCtAQcEJn0UJQJZA8Dk6DXKBqx+bg/w0WGuSxDxNr7hErW89tHUY31dUW4NqEOWwmEUNhjTFmHkg== dependencies: "@types/acorn" "^4.0.0" "@types/estree" "^1.0.0" - estree-util-is-identifier-name "^2.0.0" - micromark-factory-mdx-expression "^1.0.0" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - vfile-message "^3.0.0" - -micromark-extension-mdx-md@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/micromark-extension-mdx-md/-/micromark-extension-mdx-md-1.0.1.tgz#595d4b2f692b134080dca92c12272ab5b74c6d1a" - integrity sha512-7MSuj2S7xjOQXAjjkbjBsHkMtb+mDGVW6uI2dBL9snOBCbZmoNgDAeZ0nSn9j3T42UE/g2xVNMn18PJxZvkBEA== + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + micromark-factory-mdx-expression "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + vfile-message "^4.0.0" + +micromark-extension-mdx-md@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz" + integrity sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ== dependencies: - micromark-util-types "^1.0.0" + micromark-util-types "^2.0.0" -micromark-extension-mdxjs-esm@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-1.0.5.tgz#e4f8be9c14c324a80833d8d3a227419e2b25dec1" - integrity sha512-xNRBw4aoURcyz/S69B19WnZAkWJMxHMT5hE36GtDAyhoyn/8TuAeqjFJQlwk+MKQsUD7b3l7kFX+vlfVWgcX1w== +micromark-extension-mdxjs-esm@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz" + integrity sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A== dependencies: "@types/estree" "^1.0.0" - micromark-core-commonmark "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-events-to-acorn "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - unist-util-position-from-estree "^1.1.0" - uvu "^0.5.0" - vfile-message "^3.0.0" - -micromark-extension-mdxjs@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs/-/micromark-extension-mdxjs-1.0.1.tgz#f78d4671678d16395efeda85170c520ee795ded8" - integrity sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q== + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-position-from-estree "^2.0.0" + vfile-message "^4.0.0" + +micromark-extension-mdxjs@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz" + integrity sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ== dependencies: acorn "^8.0.0" acorn-jsx "^5.0.0" - micromark-extension-mdx-expression "^1.0.0" - micromark-extension-mdx-jsx "^1.0.0" - micromark-extension-mdx-md "^1.0.0" - micromark-extension-mdxjs-esm "^1.0.0" - micromark-util-combine-extensions "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-destination@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz#eb815957d83e6d44479b3df640f010edad667b9f" - integrity sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" + micromark-extension-mdx-expression "^3.0.0" + micromark-extension-mdx-jsx "^3.0.0" + micromark-extension-mdx-md "^2.0.0" + micromark-extension-mdxjs-esm "^3.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-types "^2.0.0" micromark-factory-destination@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz#857c94debd2c873cba34e0445ab26b74f6a6ec07" + resolved "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz" integrity sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA== dependencies: micromark-util-character "^2.0.0" micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-factory-label@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz#cc95d5478269085cfa2a7282b3de26eb2e2dec68" - integrity sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - micromark-factory-label@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz#17c5c2e66ce39ad6f4fc4cbf40d972f9096f726a" + resolved "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz" integrity sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw== dependencies: devlop "^1.0.0" @@ -6160,49 +2451,32 @@ micromark-factory-label@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-factory-mdx-expression@^1.0.0: - version "1.0.9" - resolved "https://registry.yarnpkg.com/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-1.0.9.tgz#57ba4571b69a867a1530f34741011c71c73a4976" - integrity sha512-jGIWzSmNfdnkJq05c7b0+Wv0Kfz3NJ3N4cBjnbO4zjXIlxJr+f8lk+5ZmwFvqdAbUy2q6B5rCY//g0QAAaXDWA== +micromark-factory-mdx-expression@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.2.tgz" + integrity sha512-5E5I2pFzJyg2CtemqAbcyCktpHXuJbABnsb32wX2U8IQKhhVFBqkcZR5LRm1WVoFqa4kTueZK4abep7wdo9nrw== dependencies: "@types/estree" "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-events-to-acorn "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - unist-util-position-from-estree "^1.0.0" - uvu "^0.5.0" - vfile-message "^3.0.0" - -micromark-factory-space@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf" - integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-types "^1.0.0" + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-position-from-estree "^2.0.0" + vfile-message "^4.0.0" micromark-factory-space@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz#5e7afd5929c23b96566d0e1ae018ae4fcf81d030" + resolved "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz" integrity sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg== dependencies: micromark-util-character "^2.0.0" micromark-util-types "^2.0.0" -micromark-factory-title@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz#dd0fe951d7a0ac71bdc5ee13e5d1465ad7f50ea1" - integrity sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - micromark-factory-title@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz#726140fc77892af524705d689e1cf06c8a83ea95" + resolved "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz" integrity sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A== dependencies: micromark-factory-space "^2.0.0" @@ -6210,19 +2484,9 @@ micromark-factory-title@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-factory-whitespace@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz#798fb7489f4c8abafa7ca77eed6b5745853c9705" - integrity sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - micromark-factory-whitespace@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz#9e92eb0f5468083381f923d9653632b3cfb5f763" + resolved "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz" integrity sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA== dependencies: micromark-factory-space "^2.0.0" @@ -6230,97 +2494,48 @@ micromark-factory-whitespace@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-util-character@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc" - integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg== - dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - micromark-util-character@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.0.tgz#31320ace16b4644316f6bf057531689c71e2aee1" + resolved "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz" integrity sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ== dependencies: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-util-chunked@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz#37a24d33333c8c69a74ba12a14651fd9ea8a368b" - integrity sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ== - dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-chunked@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz#e51f4db85fb203a79dbfef23fd41b2f03dc2ef89" + resolved "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz" integrity sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg== dependencies: micromark-util-symbol "^2.0.0" -micromark-util-classify-character@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz#6a7f8c8838e8a120c8e3c4f2ae97a2bff9190e9d" - integrity sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - micromark-util-classify-character@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz#8c7537c20d0750b12df31f86e976d1d951165f34" + resolved "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz" integrity sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw== dependencies: micromark-util-character "^2.0.0" micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-util-combine-extensions@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz#192e2b3d6567660a85f735e54d8ea6e3952dbe84" - integrity sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA== - dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-types "^1.0.0" - micromark-util-combine-extensions@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz#75d6ab65c58b7403616db8d6b31315013bfb7ee5" + resolved "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz" integrity sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ== dependencies: micromark-util-chunked "^2.0.0" micromark-util-types "^2.0.0" -micromark-util-decode-numeric-character-reference@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz#b1e6e17009b1f20bc652a521309c5f22c85eb1c6" - integrity sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw== - dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-decode-numeric-character-reference@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz#2698bbb38f2a9ba6310e359f99fcb2b35a0d2bd5" + resolved "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz" integrity sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ== dependencies: micromark-util-symbol "^2.0.0" -micromark-util-decode-string@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz#dc12b078cba7a3ff690d0203f95b5d5537f2809c" - integrity sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ== - dependencies: - decode-named-character-reference "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-decode-string@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz#7dfa3a63c45aecaa17824e656bcdb01f9737154a" + resolved "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz" integrity sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA== dependencies: decode-named-character-reference "^1.0.0" @@ -6328,99 +2543,56 @@ micromark-util-decode-string@^2.0.0: micromark-util-decode-numeric-character-reference "^2.0.0" micromark-util-symbol "^2.0.0" -micromark-util-encode@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz#92e4f565fd4ccb19e0dcae1afab9a173bbeb19a5" - integrity sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw== - micromark-util-encode@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz#0921ac7953dc3f1fd281e3d1932decfdb9382ab1" + resolved "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz" integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA== -micromark-util-events-to-acorn@^1.0.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-1.2.3.tgz#a4ab157f57a380e646670e49ddee97a72b58b557" - integrity sha512-ij4X7Wuc4fED6UoLWkmo0xJQhsktfNh1J0m8g4PbIMPlx+ek/4YdW5mvbye8z/aZvAPUoxgXHrwVlXAPKMRp1w== +micromark-util-events-to-acorn@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.2.tgz" + integrity sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA== dependencies: "@types/acorn" "^4.0.0" "@types/estree" "^1.0.0" - "@types/unist" "^2.0.0" - estree-util-visit "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - vfile-message "^3.0.0" - -micromark-util-html-tag-name@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz#48fd7a25826f29d2f71479d3b4e83e94829b3588" - integrity sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q== + "@types/unist" "^3.0.0" + devlop "^1.0.0" + estree-util-visit "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + vfile-message "^4.0.0" micromark-util-html-tag-name@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz#ae34b01cbe063363847670284c6255bb12138ec4" + resolved "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz" integrity sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw== -micromark-util-normalize-identifier@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz#7a73f824eb9f10d442b4d7f120fecb9b38ebf8b7" - integrity sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q== - dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-normalize-identifier@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz#91f9a4e65fe66cc80c53b35b0254ad67aa431d8b" + resolved "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz" integrity sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w== dependencies: micromark-util-symbol "^2.0.0" -micromark-util-resolve-all@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz#4652a591ee8c8fa06714c9b54cd6c8e693671188" - integrity sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA== - dependencies: - micromark-util-types "^1.0.0" - micromark-util-resolve-all@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz#189656e7e1a53d0c86a38a652b284a252389f364" + resolved "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz" integrity sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA== dependencies: micromark-util-types "^2.0.0" -micromark-util-sanitize-uri@^1.0.0, micromark-util-sanitize-uri@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz#613f738e4400c6eedbc53590c67b197e30d7f90d" - integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-encode "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-sanitize-uri@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz#ec8fbf0258e9e6d8f13d9e4770f9be64342673de" + resolved "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz" integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw== dependencies: micromark-util-character "^2.0.0" micromark-util-encode "^2.0.0" micromark-util-symbol "^2.0.0" -micromark-util-subtokenize@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz#941c74f93a93eaf687b9054aeb94642b0e92edb1" - integrity sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A== - dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - micromark-util-subtokenize@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.0.tgz#9f412442d77e0c5789ffdf42377fa8a2bcbdf581" + resolved "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.0.tgz" integrity sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg== dependencies: devlop "^1.0.0" @@ -6428,52 +2600,19 @@ micromark-util-subtokenize@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-util-symbol@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142" - integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag== - micromark-util-symbol@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz#12225c8f95edf8b17254e47080ce0862d5db8044" + resolved "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz" integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw== -micromark-util-types@^1.0.0, micromark-util-types@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283" - integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg== - micromark-util-types@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.0.tgz#63b4b7ffeb35d3ecf50d1ca20e68fc7caa36d95e" + resolved "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz" integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w== -micromark@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.2.0.tgz#1af9fef3f995ea1ea4ac9c7e2f19c48fd5c006e9" - integrity sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA== - dependencies: - "@types/debug" "^4.0.0" - debug "^4.0.0" - decode-named-character-reference "^1.0.0" - micromark-core-commonmark "^1.0.1" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-chunked "^1.0.0" - micromark-util-combine-extensions "^1.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-encode "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-sanitize-uri "^1.0.0" - micromark-util-subtokenize "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.1" - uvu "^0.5.0" - micromark@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.0.tgz#84746a249ebd904d9658cfabc1e8e5f32cbc6249" + resolved "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz" integrity sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ== dependencies: "@types/debug" "^4.0.0" @@ -6491,419 +2630,173 @@ micromark@^4.0.0: micromark-util-resolve-all "^2.0.0" micromark-util-sanitize-uri "^2.0.0" micromark-util-subtokenize "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromatch@4.0.5, micromatch@^4.0.4, micromatch@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== - dependencies: - braces "^3.0.2" - picomatch "^2.3.1" - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -mimic-fn@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" - integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== - -minimatch@9.0.3, minimatch@^9.0.0, minimatch@^9.0.1: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.0, minimist@^1.2.6: - version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": - version "7.0.4" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" - integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== +mimic-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== -mri@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" - integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== +mj-context-menu@^0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz" + integrity sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA== -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +mlly@^1.7.1, mlly@^1.7.2, mlly@^1.7.3: + version "1.7.3" + resolved "https://registry.npmjs.org/mlly/-/mlly-1.7.3.tgz" + integrity sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A== + dependencies: + acorn "^8.14.0" + pathe "^1.1.2" + pkg-types "^1.2.1" + ufo "^1.5.4" -ms@^2.1.1: +ms@^2.1.3: version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -mz@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" - integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== - dependencies: - any-promise "^1.0.0" - object-assign "^4.0.1" - thenify-all "^1.0.0" - -nano-memoize@^3.0.11: - version "3.0.16" - resolved "https://registry.yarnpkg.com/nano-memoize/-/nano-memoize-3.0.16.tgz#454100602713973ac8639bde301e255dd54920ea" - integrity sha512-JyK96AKVGAwVeMj3MoMhaSXaUNqgMbCRSQB3trUV8tYZfWEzqUBKdK1qJpfuNXgKeHOx1jv/IEYTM659ly7zUA== - -nanoid@^3.3.6, nanoid@^3.3.7: - version "3.3.7" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" - integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== - -nessy@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/nessy/-/nessy-4.0.0.tgz#863d1a8d4267e1b2de80cf3bc6911f256fca7065" - integrity sha512-XH4zOfmpxJhxXIp0Eb4vtJDtxfSjcbjY89/Rt64BNpkiBQ1mNumJWwDGq1kXWluCDQCu5LSrwABi58lWcfsWDQ== - -next-mdx-remote@^4.2.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/next-mdx-remote/-/next-mdx-remote-4.4.1.tgz#96b16e2adc54dbcd0a7f204a9a3c3fd269d41abf" - integrity sha512-1BvyXaIou6xy3XoNF4yaMZUCb6vD2GTAa5ciOa6WoO+gAUTYsb1K4rI/HSC2ogAWLrb/7VSV52skz07vOzmqIQ== - dependencies: - "@mdx-js/mdx" "^2.2.1" - "@mdx-js/react" "^2.2.1" - vfile "^5.3.0" - vfile-matter "^3.0.1" - -next-seo@^6.0.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/next-seo/-/next-seo-6.5.0.tgz#5ccfbcfaced9d296499aa88f074b9e82e252a9c8" - integrity sha512-MfzUeWTN/x/rsKp/1n0213eojO97lIl0unxqbeCY+6pAucViHDA8GSLRRcXpgjsSmBxfCFdfpu7LXbt4ANQoNQ== +negotiator@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz" + integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== -next-themes@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.2.1.tgz#0c9f128e847979daf6c67f70b38e6b6567856e45" - integrity sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A== +next-themes@^0.4.0: + version "0.4.3" + resolved "https://registry.npmjs.org/next-themes/-/next-themes-0.4.3.tgz" + integrity sha512-nG84VPkTdUHR2YeD89YchvV4I9RbiMAql3GiLEQlPvq1ioaqPaIReK+yMRdg/zgiXws620qS1rU30TiWmmG9lA== -next@^13.0.6: - version "13.5.6" - resolved "https://registry.yarnpkg.com/next/-/next-13.5.6.tgz#e964b5853272236c37ce0dd2c68302973cf010b1" - integrity sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw== +next@^14.2.5, next@>=13: + version "14.2.19" + resolved "file:../node_modules/.pnpm/next@14.2.19_@babel+core@7.26.0_@opentelemetry+api@1.9.0_babel-plugin-macros@3.1.0_react-dom@_h2flbsnle6df42ep6g6xmuqvvu/node_modules/next" dependencies: - "@next/env" "13.5.6" - "@swc/helpers" "0.5.2" + "@next/env" "14.2.19" + "@swc/helpers" "0.5.5" busboy "1.6.0" - caniuse-lite "^1.0.30001406" + caniuse-lite "^1.0.30001579" + graceful-fs "^4.2.11" postcss "8.4.31" styled-jsx "5.1.1" - watchpack "2.4.0" optionalDependencies: - "@next/swc-darwin-arm64" "13.5.6" - "@next/swc-darwin-x64" "13.5.6" - "@next/swc-linux-arm64-gnu" "13.5.6" - "@next/swc-linux-arm64-musl" "13.5.6" - "@next/swc-linux-x64-gnu" "13.5.6" - "@next/swc-linux-x64-musl" "13.5.6" - "@next/swc-win32-arm64-msvc" "13.5.6" - "@next/swc-win32-ia32-msvc" "13.5.6" - "@next/swc-win32-x64-msvc" "13.5.6" + "@next/swc-darwin-arm64" "14.2.19" + "@next/swc-darwin-x64" "14.2.19" + "@next/swc-linux-arm64-gnu" "14.2.19" + "@next/swc-linux-arm64-musl" "14.2.19" + "@next/swc-linux-x64-gnu" "14.2.19" + "@next/swc-linux-x64-musl" "14.2.19" + "@next/swc-win32-arm64-msvc" "14.2.19" + "@next/swc-win32-ia32-msvc" "14.2.19" + "@next/swc-win32-x64-msvc" "14.2.19" nextra-theme-docs@latest: - version "2.13.3" - resolved "https://registry.yarnpkg.com/nextra-theme-docs/-/nextra-theme-docs-2.13.3.tgz#32057ca14e4f5b821f681e43dfcdb0b5bc46bc91" - integrity sha512-B6xrnR86Gg4GzV56AomSwtmvSyAvnJz1xKOGGav1XKxkwvC8QeI17jdt/CqiKyIObJ+5bLqSFiKhaAZ5DYQP3g== + version "3.2.4" + resolved "https://registry.npmjs.org/nextra-theme-docs/-/nextra-theme-docs-3.2.4.tgz" + integrity sha512-3fg7zMHInuvSDURRJjh6UrbdqkK8uLs8RNriY38kVukWLvaVP2f6mmVJKIYqxVv6qAKWEzDLTr4dlJCY81eXuQ== dependencies: - "@headlessui/react" "^1.7.17" - "@popperjs/core" "^2.11.8" + "@headlessui/react" "^2.1.2" clsx "^2.0.0" escape-string-regexp "^5.0.0" - flexsearch "^0.7.31" - focus-visible "^5.2.0" - git-url-parse "^13.1.0" - intersection-observer "^0.12.2" - match-sorter "^6.3.1" - next-seo "^6.0.0" - next-themes "^0.2.1" + flexsearch "^0.7.43" + next-themes "^0.4.0" scroll-into-view-if-needed "^3.1.0" zod "^3.22.3" -nextra@latest: - version "2.13.3" - resolved "https://registry.yarnpkg.com/nextra/-/nextra-2.13.3.tgz#0b24d17fc5581a9b50eede8a195ea33b9aba4920" - integrity sha512-OBVuyQKh+oqrbVt0AosgNYnuReWuNrtJVEN7q18b/oEg2wEpuiq3UJfmIvGgOdNYc3zv3OYrzbcq7IhwtdHHEw== +nextra@3.2.4, nextra@latest: + version "3.2.4" + resolved "https://registry.npmjs.org/nextra/-/nextra-3.2.4.tgz" + integrity sha512-xvQuPVtRoJTz4ynIbEkxYkEtviIX699lt4coij2IMmafYrBNaD0Ofj93jIz7VngYxyT9f4gWSiwqNgoIlnbsjQ== dependencies: - "@headlessui/react" "^1.7.17" - "@mdx-js/mdx" "^2.3.0" - "@mdx-js/react" "^2.3.0" + "@formatjs/intl-localematcher" "^0.5.4" + "@headlessui/react" "^2.1.2" + "@mdx-js/mdx" "^3.0.0" + "@mdx-js/react" "^3.0.0" "@napi-rs/simple-git" "^0.1.9" - "@theguild/remark-mermaid" "^0.0.5" - "@theguild/remark-npm2yarn" "^0.2.0" + "@shikijs/twoslash" "^1.0.0" + "@theguild/remark-mermaid" "^0.1.3" + "@theguild/remark-npm2yarn" "^0.3.2" + better-react-mathjax "^2.0.3" clsx "^2.0.0" + estree-util-to-js "^2.0.0" + estree-util-value-to-estree "^3.0.1" github-slugger "^2.0.0" graceful-fs "^4.2.11" gray-matter "^4.0.3" + hast-util-to-estree "^3.1.0" katex "^0.16.9" - lodash.get "^4.4.2" - next-mdx-remote "^4.2.1" - p-limit "^3.1.0" + mdast-util-from-markdown "^2.0.1" + mdast-util-gfm "^3.0.0" + mdast-util-to-hast "^13.2.0" + negotiator "^1.0.0" + p-limit "^6.0.0" rehype-katex "^7.0.0" - rehype-pretty-code "0.9.11" + rehype-pretty-code "0.14.0" rehype-raw "^7.0.0" - remark-gfm "^3.0.1" - remark-math "^5.1.1" + remark-frontmatter "^5.0.0" + remark-gfm "^4.0.0" + remark-math "^6.0.0" remark-reading-time "^2.0.1" - shiki "^0.14.3" - slash "^3.0.0" - title "^3.5.3" + remark-smartypants "^3.0.0" + shiki "^1.0.0" + slash "^5.1.0" + title "^4.0.0" unist-util-remove "^4.0.0" unist-util-visit "^5.0.0" + yaml "^2.3.2" zod "^3.22.3" + zod-validation-error "^3.0.0" -node-int64@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" - integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== - -node-releases@^2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" - integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== - -non-layered-tidy-tree-layout@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz#57d35d13c356643fc296a55fb11ac15e74da7804" - integrity sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw== - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== - -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw== - dependencies: - path-key "^2.0.0" - -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== +nlcst-to-string@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz" + integrity sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA== dependencies: - path-key "^3.0.0" + "@types/nlcst" "^2.0.0" npm-run-path@^5.1.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.2.0.tgz#224cdd22c755560253dd71b83a1ef2f758b2e955" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz" integrity sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg== dependencies: path-key "^4.0.0" -npm-to-yarn@^2.1.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/npm-to-yarn/-/npm-to-yarn-2.2.1.tgz#048843a6630621daffc6a239dfc89698b8abf7e8" - integrity sha512-O/j/ROyX0KGLG7O6Ieut/seQ0oiTpHF2tXAcFbpdTLQFiaNtkyTXXocM1fwpaa60dg1qpWj0nHlbNhx6qwuENQ== - -object-assign@^4.0.1, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-hash@^3.0.0: +npm-to-yarn@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" - integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== - -object-inspect@^1.13.1: - version "1.13.1" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" - integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== - -object-is@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" - integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.4, object.assign@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" - integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== - dependencies: - call-bind "^1.0.5" - define-properties "^1.2.1" - has-symbols "^1.0.3" - object-keys "^1.1.1" - -object.entries@^1.1.6, object.entries@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.7.tgz#2b47760e2a2e3a752f39dd874655c61a7f03c131" - integrity sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - -object.fromentries@^2.0.6, object.fromentries@^2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.7.tgz#71e95f441e9a0ea6baf682ecaaf37fa2a8d7e616" - integrity sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - -object.groupby@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.2.tgz#494800ff5bab78fd0eff2835ec859066e00192ec" - integrity sha512-bzBq58S+x+uo0VjurFT0UktpKHOZmv4/xePiOA1nbB9pMqpGK7rUPNgf+1YC+7mE+0HzhTMqNUuCqvKhj6FnBw== - dependencies: - array.prototype.filter "^1.0.3" - call-bind "^1.0.5" - define-properties "^1.2.1" - es-abstract "^1.22.3" - es-errors "^1.0.0" - -object.hasown@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.3.tgz#6a5f2897bb4d3668b8e79364f98ccf971bda55ae" - integrity sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA== - dependencies: - define-properties "^1.2.0" - es-abstract "^1.22.1" - -object.values@^1.1.6, object.values@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.7.tgz#617ed13272e7e1071b43973aa1655d9291b8442a" - integrity sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - -once@^1.3.0, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -onetime@^5.1.0, onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" + resolved "https://registry.npmjs.org/npm-to-yarn/-/npm-to-yarn-3.0.0.tgz" + integrity sha512-76YnmsbfrYp0tMsWxM0RNX0Vs+x8JxpJGu6B/jDn4lW8+laiTcKmKi9MeMh4UikO4RkJ1oqURoDy9bXJmMXS6A== onetime@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" + resolved "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz" integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== dependencies: mimic-fn "^4.0.0" -optionator@^0.9.3: - version "0.9.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" - integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== - dependencies: - "@aashutoshrathi/word-wrap" "^1.2.3" - deep-is "^0.1.3" - fast-levenshtein "^2.0.6" - levn "^0.4.1" - prelude-ls "^1.2.1" - type-check "^0.4.0" - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^3.0.2, p-limit@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-limit@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-4.0.0.tgz#914af6544ed32bfa54670b061cafcbd04984b644" - integrity sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ== - dependencies: - yocto-queue "^1.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== +oniguruma-to-es@0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-0.7.0.tgz" + integrity sha512-HRaRh09cE0gRS3+wi2zxekB+I5L8C/gN60S+vb11eADHUaB/q4u8wGGOX3GvwvitG8ixaeycZfeoyruKQzUgNg== dependencies: - p-limit "^2.2.0" + emoji-regex-xs "^1.0.0" + regex "^5.0.2" + regex-recursion "^4.3.0" -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - -p-locate@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-6.0.0.tgz#3da9a49d4934b901089dca3302fa65dc5a05c04f" - integrity sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw== +p-limit@^6.0.0: + version "6.1.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-6.1.0.tgz" + integrity sha512-H0jc0q1vOzlEk0TqAKXKZxdl7kX3OFUzCnNVUnq5Pc3DGo0kpeaMuPqxQn235HibwBEb0/pm9dgKTjXy66fBkg== dependencies: - p-limit "^4.0.0" + yocto-queue "^1.1.1" -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" +package-manager-detector@^0.2.0: + version "0.2.7" + resolved "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.7.tgz" + integrity sha512-g4+387DXDKlZzHkP+9FLt8yKj8+/3tOkPv7DVTJGGRm00RkEWgqbFstX1mXJ4M0VDYhUqsTOiISqNOJnhAu3PQ== parse-entities@^4.0.0: version "4.0.1" - resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.1.tgz#4e2a01111fb1c986549b944af39eeda258fc9e4e" + resolved "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz" integrity sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w== dependencies: "@types/unist" "^2.0.0" @@ -6915,291 +2808,99 @@ parse-entities@^4.0.0: is-decimal "^2.0.0" is-hexadecimal "^2.0.0" -parse-import-specifiers@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/parse-import-specifiers/-/parse-import-specifiers-1.0.2.tgz#2219ecdbb1b40b664e697aa1510d134a7e5d8522" - integrity sha512-MzJKeFIsoY0cTv/Y41TZipso3aYMz4+jpI1jCPhA9os6McI1cUUhI88WtmzdZ2ghfhuZ+1YF8lzx29eIhMexlA== - -parse-json@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== +parse-latin@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz" + integrity sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ== dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" + "@types/nlcst" "^2.0.0" + "@types/unist" "^3.0.0" + nlcst-to-string "^4.0.0" + unist-util-modify-children "^4.0.0" + unist-util-visit-children "^3.0.0" + vfile "^6.0.0" parse-numeric-range@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz#7c63b61190d61e4d53a1197f0c83c47bb670ffa3" + resolved "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz" integrity sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ== -parse-path@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/parse-path/-/parse-path-7.0.0.tgz#605a2d58d0a749c8594405d8cc3a2bf76d16099b" - integrity sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog== - dependencies: - protocols "^2.0.0" - -parse-url@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-8.1.0.tgz#972e0827ed4b57fc85f0ea6b0d839f0d8a57a57d" - integrity sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w== - dependencies: - parse-path "^7.0.0" - parse5@^7.0.0: version "7.1.2" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + resolved "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz" integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== dependencies: entities "^4.4.0" -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-exists@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-5.0.0.tgz#a6aad9489200b21fab31e49cf09277e5116fb9e7" - integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-key@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== +path-data-parser@^0.1.0, path-data-parser@0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz" + integrity sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w== -path-key@^3.0.0, path-key@^3.1.0: +path-key@^3.1.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-key@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" + resolved "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz" integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-scurry@^1.10.1: - version "1.10.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" - integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== - dependencies: - lru-cache "^9.1.1 || ^10.0.0" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -periscopic@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/periscopic/-/periscopic-3.1.0.tgz#7e9037bf51c5855bd33b48928828db4afa79d97a" - integrity sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw== - dependencies: - "@types/estree" "^1.0.0" - estree-walker "^3.0.0" - is-reference "^3.0.0" - -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -picomatch@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.1.tgz#68c26c8837399e5819edce48590412ea07f17a07" - integrity sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg== - -pidtree@0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" - integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== - -pify@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== - -pirates@^4.0.1, pirates@^4.0.4: - version "4.0.6" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" - integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== - -pkg-dir@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - -postcss-import@^15.1.0: - version "15.1.0" - resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" - integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== - dependencies: - postcss-value-parser "^4.0.0" - read-cache "^1.0.0" - resolve "^1.1.7" - -postcss-js@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2" - integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== - dependencies: - camelcase-css "^2.0.1" - -postcss-load-config@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" - integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== - dependencies: - lilconfig "^3.0.0" - yaml "^2.3.4" - -postcss-nested@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c" - integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ== - dependencies: - postcss-selector-parser "^6.0.11" +pathe@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz" + integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== -postcss-selector-parser@^6.0.11: - version "6.0.15" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz#11cc2b21eebc0b99ea374ffb9887174855a01535" - integrity sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw== +pkg-types@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.1.tgz" + integrity sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw== dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" + confbox "^0.1.8" + mlly "^1.7.2" + pathe "^1.1.2" -postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" - integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== +points-on-curve@^0.2.0, points-on-curve@0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz" + integrity sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A== -postcss@8.4.31: - version "8.4.31" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" - integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== +points-on-path@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz" + integrity sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g== dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" + path-data-parser "0.1.0" + points-on-curve "0.2.0" -postcss@^8.4.23, postcss@^8.4.35: - version "8.4.35" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.35.tgz#60997775689ce09011edf083a549cea44aabe2f7" - integrity sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA== +postcss@^8.4.40: + version "8.4.49" + resolved "file:../node_modules/.pnpm/postcss@8.4.49/node_modules/postcss" dependencies: nanoid "^3.3.7" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== - -prettier@^3.2.5: - version "3.2.5" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" - integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== - -pretty-format@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" - integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== - dependencies: - "@jest/schemas" "^29.6.3" - ansi-styles "^5.0.0" - react-is "^18.0.0" - -prompts@^2.0.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" - integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== - dependencies: - kleur "^3.0.3" - sisteransi "^1.0.5" + picocolors "^1.1.1" + source-map-js "^1.2.1" -prop-types@^15.8.1: - version "15.8.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" - integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.13.1" +prettier@^3.3.3: + version "3.3.3" + resolved "file:../node_modules/.pnpm/prettier@3.3.3/node_modules/prettier" property-information@^6.0.0: version "6.4.1" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.4.1.tgz#de8b79a7415fd2107dfbe65758bb2cc9dfcf60ac" + resolved "https://registry.npmjs.org/property-information/-/property-information-6.4.1.tgz" integrity sha512-OHYtXfu5aI2sS2LWFSN5rgJjrQ4pCy8i1jubJLe2QvMF8JJ++HXTUIVWFLfXJoaOfvYYjk2SN8J2wFUWIGXT4w== -protocols@^2.0.0, protocols@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/protocols/-/protocols-2.0.1.tgz#8f155da3fc0f32644e83c5782c8e8212ccf70a86" - integrity sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q== - -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== - -punycode@^2.1.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" - integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== - -pure-rand@^6.0.0: - version "6.0.4" - resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.4.tgz#50b737f6a925468679bff00ad20eade53f37d5c7" - integrity sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA== - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -react-dom@^18.2.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" - integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== +"react-dom@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom@^18 || ^19 || ^19.0.0-rc", react-dom@^18.3.1, react-dom@>=16.8.0, react-dom@>=18: + version "18.3.1" + resolved "file:../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom" dependencies: loose-envify "^1.1.0" - scheduler "^0.23.0" - -react-is@^16.13.1: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-is@^18.0.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" - integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + scheduler "^0.23.2" react-markdown@^9.0.1: version "9.0.1" - resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-9.0.1.tgz#c05ddbff67fd3b3f839f8c648e6fb35d022397d1" - integrity sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg== + resolved "file:../node_modules/.pnpm/react-markdown@9.0.1_@types+react@18.3.12_react@18.3.1/node_modules/react-markdown" dependencies: "@types/hast" "^3.0.0" devlop "^1.0.0" @@ -7212,63 +2913,79 @@ react-markdown@^9.0.1: unist-util-visit "^5.0.0" vfile "^6.0.0" -react@^18.2.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" - integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== +"react@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react@^18 || ^19 || ^19.0.0-rc", react@^18.2.0, react@^18.3.1, react@>=16, react@>=16.8, react@>=16.8.0, react@>=18: + version "18.3.1" + resolved "file:../node_modules/.pnpm/react@18.3.1/node_modules/react" dependencies: loose-envify "^1.1.0" -read-cache@^1.0.0: +reading-time@^1.3.0: + version "1.5.0" + resolved "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz" + integrity sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg== + +recma-build-jsx@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" - integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== + resolved "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz" + integrity sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew== dependencies: - pify "^2.3.0" + "@types/estree" "^1.0.0" + estree-util-build-jsx "^3.0.0" + vfile "^6.0.0" -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== +recma-jsx@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.0.tgz" + integrity sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q== dependencies: - picomatch "^2.2.1" + acorn-jsx "^5.0.0" + estree-util-to-js "^2.0.0" + recma-parse "^1.0.0" + recma-stringify "^1.0.0" + unified "^11.0.0" -reading-time@^1.3.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/reading-time/-/reading-time-1.5.0.tgz#d2a7f1b6057cb2e169beaf87113cc3411b5bc5bb" - integrity sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg== +recma-parse@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz" + integrity sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ== + dependencies: + "@types/estree" "^1.0.0" + esast-util-from-js "^2.0.0" + unified "^11.0.0" + vfile "^6.0.0" -reflect.getprototypeof@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.5.tgz#e0bd28b597518f16edaf9c0e292c631eb13e0674" - integrity sha512-62wgfC8dJWrmxv44CA36pLDnP6KKl3Vhxb7PL+8+qrrFMMoJij4vgiMP8zV4O8+CBMXY1mHxI5fITGHXFHVmQQ== - dependencies: - call-bind "^1.0.5" - define-properties "^1.2.1" - es-abstract "^1.22.3" - es-errors "^1.0.0" - get-intrinsic "^1.2.3" - globalthis "^1.0.3" - which-builtin-type "^1.1.3" - -regenerator-runtime@^0.14.0: - version "0.14.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" - integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== - -regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" - integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== - dependencies: - call-bind "^1.0.6" - define-properties "^1.2.1" - es-errors "^1.3.0" - set-function-name "^2.0.1" +recma-stringify@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz" + integrity sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g== + dependencies: + "@types/estree" "^1.0.0" + estree-util-to-js "^2.0.0" + unified "^11.0.0" + vfile "^6.0.0" + +regex-recursion@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/regex-recursion/-/regex-recursion-4.3.0.tgz" + integrity sha512-5LcLnizwjcQ2ALfOj95MjcatxyqF5RPySx9yT+PaXu3Gox2vyAtLDjHB8NTJLtMGkvyau6nI3CfpwFCjPUIs/A== + dependencies: + regex-utilities "^2.3.0" + +regex-utilities@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz" + integrity sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng== + +regex@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/regex/-/regex-5.0.2.tgz" + integrity sha512-/pczGbKIQgfTMRV0XjABvc5RzLqQmwqxLHdQao2RTXPk+pmTXB2P0IaUHYdYyk412YLwUIkaeMd5T+RzVgTqnQ== + dependencies: + regex-utilities "^2.3.0" rehype-katex@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/rehype-katex/-/rehype-katex-7.0.0.tgz#f5e9e2825981175a7b0a4d58ed9816c33576dfed" + resolved "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.0.tgz" integrity sha512-h8FPkGE00r2XKU+/acgqwWUlyzve1IiOKwsEkg4pDL3k48PiE0Pt+/uLtVHDVkN1yA4iurZN6UES8ivHVEQV6Q== dependencies: "@types/hast" "^3.0.0" @@ -7279,64 +2996,88 @@ rehype-katex@^7.0.0: unist-util-visit-parents "^6.0.0" vfile "^6.0.0" -rehype-pretty-code@0.9.11: - version "0.9.11" - resolved "https://registry.yarnpkg.com/rehype-pretty-code/-/rehype-pretty-code-0.9.11.tgz#742017cbcfd5bd85466dfedd65b33a954aff7f2a" - integrity sha512-Eq90eCYXQJISktfRZ8PPtwc5SUyH6fJcxS8XOMnHPUQZBtC6RYo67gGlley9X2nR8vlniPj0/7oCDEYHKQa/oA== +rehype-parse@^9.0.0: + version "9.0.1" + resolved "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz" + integrity sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag== + dependencies: + "@types/hast" "^3.0.0" + hast-util-from-html "^2.0.0" + unified "^11.0.0" + +rehype-pretty-code@0.14.0: + version "0.14.0" + resolved "https://registry.npmjs.org/rehype-pretty-code/-/rehype-pretty-code-0.14.0.tgz" + integrity sha512-hBeKF/Wkkf3zyUS8lal9RCUuhypDWLQc+h9UrP9Pav25FUm/AQAVh4m5gdvJxh4Oz+U+xKvdsV01p1LdvsZTiQ== dependencies: - "@types/hast" "^2.0.0" - hash-obj "^4.0.0" + "@types/hast" "^3.0.4" + hast-util-to-string "^3.0.0" parse-numeric-range "^1.3.0" + rehype-parse "^9.0.0" + unified "^11.0.5" + unist-util-visit "^5.0.0" rehype-raw@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-7.0.0.tgz#59d7348fd5dbef3807bbaa1d443efd2dd85ecee4" + resolved "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz" integrity sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww== dependencies: "@types/hast" "^3.0.0" hast-util-raw "^9.0.0" vfile "^6.0.0" -remark-gfm@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-3.0.1.tgz#0b180f095e3036545e9dddac0e8df3fa5cfee54f" - integrity sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig== +rehype-recma@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz" + integrity sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw== dependencies: - "@types/mdast" "^3.0.0" - mdast-util-gfm "^2.0.0" - micromark-extension-gfm "^2.0.0" - unified "^10.0.0" + "@types/estree" "^1.0.0" + "@types/hast" "^3.0.0" + hast-util-to-estree "^3.0.0" -remark-math@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/remark-math/-/remark-math-5.1.1.tgz#459e798d978d4ca032e745af0bac81ddcdf94964" - integrity sha512-cE5T2R/xLVtfFI4cCePtiRn+e6jKMtFDR3P8V3qpv8wpKjwvHoBA4eJzvX+nVrnlNy0911bdGmuspCSwetfYHw== +remark-frontmatter@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz" + integrity sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ== dependencies: - "@types/mdast" "^3.0.0" - mdast-util-math "^2.0.0" - micromark-extension-math "^2.0.0" - unified "^10.0.0" + "@types/mdast" "^4.0.0" + mdast-util-frontmatter "^2.0.0" + micromark-extension-frontmatter "^2.0.0" + unified "^11.0.0" -remark-mdx@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/remark-mdx/-/remark-mdx-2.3.0.tgz#efe678025a8c2726681bde8bf111af4a93943db4" - integrity sha512-g53hMkpM0I98MU266IzDFMrTD980gNF3BJnkyFcmN+dD873mQeD5rdMO3Y2X+x8umQfbSE0PcoEDl7ledSA+2g== +remark-gfm@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz" + integrity sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-gfm "^3.0.0" + micromark-extension-gfm "^3.0.0" + remark-parse "^11.0.0" + remark-stringify "^11.0.0" + unified "^11.0.0" + +remark-math@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz" + integrity sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA== dependencies: - mdast-util-mdx "^2.0.0" - micromark-extension-mdxjs "^1.0.0" + "@types/mdast" "^4.0.0" + mdast-util-math "^3.0.0" + micromark-extension-math "^3.0.0" + unified "^11.0.0" -remark-parse@^10.0.0: - version "10.0.2" - resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.2.tgz#ca241fde8751c2158933f031a4e3efbaeb8bc262" - integrity sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw== +remark-mdx@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.0.tgz" + integrity sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA== dependencies: - "@types/mdast" "^3.0.0" - mdast-util-from-markdown "^1.0.0" - unified "^10.0.0" + mdast-util-mdx "^3.0.0" + micromark-extension-mdxjs "^3.0.0" remark-parse@^11.0.0: version "11.0.0" - resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-11.0.0.tgz#aa60743fcb37ebf6b069204eb4da304e40db45a1" + resolved "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz" integrity sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA== dependencies: "@types/mdast" "^4.0.0" @@ -7346,7 +3087,7 @@ remark-parse@^11.0.0: remark-reading-time@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/remark-reading-time/-/remark-reading-time-2.0.1.tgz#fe8bb8e420db7678dc749385167adb4fc99318f7" + resolved "https://registry.npmjs.org/remark-reading-time/-/remark-reading-time-2.0.1.tgz" integrity sha512-fy4BKy9SRhtYbEHvp6AItbRTnrhiDGbqLQTSYVbQPGuRCncU1ubSsh9p/W5QZSxtYcUXv8KGL0xBgPLyNJA1xw== dependencies: estree-util-is-identifier-name "^2.0.0" @@ -7354,19 +3095,9 @@ remark-reading-time@^2.0.1: reading-time "^1.3.0" unist-util-visit "^3.1.0" -remark-rehype@^10.0.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-10.1.0.tgz#32dc99d2034c27ecaf2e0150d22a6dcccd9a6279" - integrity sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw== - dependencies: - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - mdast-util-to-hast "^12.1.0" - unified "^10.0.0" - remark-rehype@^11.0.0: version "11.1.0" - resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-11.1.0.tgz#d5f264f42bcbd4d300f030975609d01a1697ccdc" + resolved "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.0.tgz" integrity sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g== dependencies: "@types/hast" "^3.0.0" @@ -7375,879 +3106,354 @@ remark-rehype@^11.0.0: unified "^11.0.0" vfile "^6.0.0" -remove-accents@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.5.0.tgz#77991f37ba212afba162e375b627631315bed687" - integrity sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A== - -remove-blank-lines@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/remove-blank-lines/-/remove-blank-lines-1.4.1.tgz#c07a00a76889cfb50b072924453c4a2a455cf000" - integrity sha512-NEs3uvzpaZscL9qFGIHMO7iFy45/nRQC0bBeIMys8UDJT5CX/OcgDeRpcmwXGcr9Ez+IYZka7w0xhA9pEs7Cag== - -rendy@^4.0.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/rendy/-/rendy-4.1.3.tgz#cd5015c9e0ab1cf88713d50064a7920f87ee1e42" - integrity sha512-ljyvSWWFaPvncY+C/0GlpRh6f2Ufe1fhZwAVkqTHi/EvZjWM1PumqWJzFY5dBRBvXGnxxvWzDasK7UihddJfmg== - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +remark-smartypants@^3.0.0: + version "3.0.2" + resolved "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz" + integrity sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA== + dependencies: + retext "^9.0.0" + retext-smartypants "^6.0.0" + unified "^11.0.4" + unist-util-visit "^5.0.0" -resolve-cwd@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" - integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== +remark-stringify@^11.0.0: + version "11.0.0" dependencies: - resolve-from "^5.0.0" + "@types/mdast" "^4.0.0" + mdast-util-to-markdown "^2.0.0" + unified "^11.0.0" -resolve-from@^4.0.0: +retext-latin@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-from@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - -resolve-pkg-maps@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" - integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== - -resolve.exports@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" - integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== - -resolve@^1.1.7, resolve@^1.20.0, resolve@^1.22.2, resolve@^1.22.4: - version "1.22.8" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + resolved "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz" + integrity sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA== dependencies: - is-core-module "^2.13.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" + "@types/nlcst" "^2.0.0" + parse-latin "^7.0.0" + unified "^11.0.0" -resolve@^2.0.0-next.4: - version "2.0.0-next.5" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" - integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== +retext-smartypants@^6.0.0: + version "6.2.0" + resolved "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz" + integrity sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ== dependencies: - is-core-module "^2.13.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" + "@types/nlcst" "^2.0.0" + nlcst-to-string "^4.0.0" + unist-util-visit "^5.0.0" -restore-cursor@^4.0.0: +retext-stringify@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-4.0.0.tgz#519560a4318975096def6e609d44100edaa4ccb9" - integrity sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg== + resolved "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz" + integrity sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA== dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rfdc@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f" - integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg== + "@types/nlcst" "^2.0.0" + nlcst-to-string "^4.0.0" + unified "^11.0.0" -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== +retext@^9.0.0: + version "9.0.0" + resolved "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz" + integrity sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA== dependencies: - glob "^7.1.3" + "@types/nlcst" "^2.0.0" + retext-latin "^4.0.0" + retext-stringify "^4.0.0" + unified "^11.0.0" robust-predicates@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" - integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -rw@1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" - integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== - -sade@^1.7.3: - version "1.8.1" - resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" - integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== - dependencies: - mri "^1.1.0" + resolved "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz" + integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== -safe-array-concat@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.0.tgz#8d0cae9cb806d6d1c06e08ab13d847293ebe0692" - integrity sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg== +roughjs@^4.6.6: + version "4.6.6" + resolved "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz" + integrity sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ== dependencies: - call-bind "^1.0.5" - get-intrinsic "^1.2.2" - has-symbols "^1.0.3" - isarray "^2.0.5" + hachure-fill "^0.5.2" + path-data-parser "^0.1.0" + points-on-curve "^0.2.0" + points-on-path "^0.2.1" -safe-regex-test@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" - integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== - dependencies: - call-bind "^1.0.6" - es-errors "^1.3.0" - is-regex "^1.1.4" +rw@1: + version "1.3.3" + resolved "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz" + integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -scheduler@^0.23.0: - version "0.23.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" - integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== - dependencies: - loose-envify "^1.1.0" - scroll-into-view-if-needed@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz#fa9524518c799b45a2ef6bbffb92bcad0296d01f" + resolved "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz" integrity sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ== dependencies: compute-scroll-into-view "^3.0.2" section-matter@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" + resolved "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz" integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA== dependencies: extend-shallow "^2.0.1" kind-of "^6.0.0" -semver@^6.3.0, semver@^6.3.1: - version "6.3.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.5.3, semver@^7.5.4: - version "7.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" - integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== - dependencies: - lru-cache "^6.0.0" - -server-only@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/server-only/-/server-only-0.0.1.tgz#0f366bb6afb618c37c9255a314535dc412cd1c9e" - integrity sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA== - -set-function-length@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.1.tgz#47cc5945f2c771e2cf261c6737cf9684a2a5e425" - integrity sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g== - dependencies: - define-data-property "^1.1.2" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.3" - gopd "^1.0.1" - has-property-descriptors "^1.0.1" - -set-function-name@^2.0.0, set-function-name@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a" - integrity sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA== - dependencies: - define-data-property "^1.0.1" - functions-have-names "^1.2.3" - has-property-descriptors "^1.0.0" - -sharp@^0.33.2: - version "0.33.2" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.2.tgz#fcd52f2c70effa8a02160b1bfd989a3de55f2dfb" - integrity sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ== +sharp@^0.33.4: + version "0.33.5" + resolved "file:../node_modules/.pnpm/sharp@0.33.5/node_modules/sharp" dependencies: color "^4.2.3" - detect-libc "^2.0.2" - semver "^7.5.4" + detect-libc "^2.0.3" + semver "^7.6.3" optionalDependencies: - "@img/sharp-darwin-arm64" "0.33.2" - "@img/sharp-darwin-x64" "0.33.2" - "@img/sharp-libvips-darwin-arm64" "1.0.1" - "@img/sharp-libvips-darwin-x64" "1.0.1" - "@img/sharp-libvips-linux-arm" "1.0.1" - "@img/sharp-libvips-linux-arm64" "1.0.1" - "@img/sharp-libvips-linux-s390x" "1.0.1" - "@img/sharp-libvips-linux-x64" "1.0.1" - "@img/sharp-libvips-linuxmusl-arm64" "1.0.1" - "@img/sharp-libvips-linuxmusl-x64" "1.0.1" - "@img/sharp-linux-arm" "0.33.2" - "@img/sharp-linux-arm64" "0.33.2" - "@img/sharp-linux-s390x" "0.33.2" - "@img/sharp-linux-x64" "0.33.2" - "@img/sharp-linuxmusl-arm64" "0.33.2" - "@img/sharp-linuxmusl-x64" "0.33.2" - "@img/sharp-wasm32" "0.33.2" - "@img/sharp-win32-ia32" "0.33.2" - "@img/sharp-win32-x64" "0.33.2" - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg== - dependencies: - shebang-regex "^1.0.0" + "@img/sharp-darwin-arm64" "0.33.5" + "@img/sharp-darwin-x64" "0.33.5" + "@img/sharp-libvips-darwin-arm64" "1.0.4" + "@img/sharp-libvips-darwin-x64" "1.0.4" + "@img/sharp-libvips-linux-arm" "1.0.5" + "@img/sharp-libvips-linux-arm64" "1.0.4" + "@img/sharp-libvips-linux-s390x" "1.0.4" + "@img/sharp-libvips-linux-x64" "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64" "1.0.4" + "@img/sharp-libvips-linuxmusl-x64" "1.0.4" + "@img/sharp-linux-arm" "0.33.5" + "@img/sharp-linux-arm64" "0.33.5" + "@img/sharp-linux-s390x" "0.33.5" + "@img/sharp-linux-x64" "0.33.5" + "@img/sharp-linuxmusl-arm64" "0.33.5" + "@img/sharp-linuxmusl-x64" "0.33.5" + "@img/sharp-wasm32" "0.33.5" + "@img/sharp-win32-ia32" "0.33.5" + "@img/sharp-win32-x64" "0.33.5" shebang-command@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: shebang-regex "^3.0.0" -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== - shebang-regex@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shiki@^0.14.3: - version "0.14.7" - resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.14.7.tgz#c3c9e1853e9737845f1d2ef81b31bcfb07056d4e" - integrity sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg== +shiki@^1.0.0, shiki@^1.3.0: + version "1.24.0" + resolved "https://registry.npmjs.org/shiki/-/shiki-1.24.0.tgz" + integrity sha512-qIneep7QRwxRd5oiHb8jaRzH15V/S8F3saCXOdjwRLgozZJr5x2yeBhQtqkO3FSzQDwYEFAYuifg4oHjpDghrg== dependencies: - ansi-sequence-parser "^1.1.0" - jsonc-parser "^3.2.0" - vscode-oniguruma "^1.7.0" - vscode-textmate "^8.0.0" + "@shikijs/core" "1.24.0" + "@shikijs/engine-javascript" "1.24.0" + "@shikijs/engine-oniguruma" "1.24.0" + "@shikijs/types" "1.24.0" + "@shikijs/vscode-textmate" "^9.3.0" + "@types/hast" "^3.0.4" -side-channel@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.5.tgz#9a84546599b48909fb6af1211708d23b1946221b" - integrity sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ== - dependencies: - call-bind "^1.0.6" - es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" - -signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -signal-exit@^4.0.1, signal-exit@^4.1.0: +signal-exit@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" - integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== - dependencies: - is-arrayish "^0.3.1" - -sisteransi@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" - integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -slice-ansi@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" - integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== - dependencies: - ansi-styles "^6.0.0" - is-fullwidth-code-point "^4.0.0" - -slice-ansi@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-7.1.0.tgz#cd6b4655e298a8d1bdeb04250a433094b347b9a9" - integrity sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg== - dependencies: - ansi-styles "^6.2.1" - is-fullwidth-code-point "^5.0.0" - -sort-keys@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-5.0.0.tgz#5d775f8ae93ecc29bc7312bbf3acac4e36e3c446" - integrity sha512-Pdz01AvCAottHTPQGzndktFNdbRA75BgOfeT1hH+AMnJFv8lynkPi42rfeEhpx1saTEI3YNMWxfqu0sFD1G8pw== - dependencies: - is-plain-obj "^4.0.0" - -source-map-js@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== - -source-map-support@0.5.13: - version "0.5.13" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" - integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +slash@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz" + integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg== source-map@^0.7.0: version "0.7.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== space-separated-tokens@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz" integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== +speech-rule-engine@^4.0.6: + version "4.0.7" + resolved "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.0.7.tgz" + integrity sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g== + dependencies: + commander "9.2.0" + wicked-good-xpath "1.3.0" + xmldom-sre "0.1.31" + sprintf-js@~1.0.2: version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -stack-utils@^2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" - integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== - dependencies: - escape-string-regexp "^2.0.0" - -streamsearch@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" - integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== - -string-argv@0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" - integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== - -string-length@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" - integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== - dependencies: - char-regex "^1.0.2" - strip-ansi "^6.0.0" - -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^5.0.1, string-width@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== - dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" - -string-width@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.1.0.tgz#d994252935224729ea3719c49f7206dc9c46550a" - integrity sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw== - dependencies: - emoji-regex "^10.3.0" - get-east-asian-width "^1.0.0" - strip-ansi "^7.1.0" - -string.prototype.matchall@^4.0.8: - version "4.0.10" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100" - integrity sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - get-intrinsic "^1.2.1" - has-symbols "^1.0.3" - internal-slot "^1.0.5" - regexp.prototype.flags "^1.5.0" - set-function-name "^2.0.0" - side-channel "^1.0.4" - -string.prototype.trim@^1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz#f9ac6f8af4bd55ddfa8895e6aea92a96395393bd" - integrity sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - -string.prototype.trimend@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz#1bb3afc5008661d73e2dc015cd4853732d6c471e" - integrity sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - -string.prototype.trimstart@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz#d4cdb44b83a4737ffbac2d406e405d43d0184298" - integrity sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - stringify-entities@^4.0.0: version "4.0.3" - resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.3.tgz#cfabd7039d22ad30f3cc435b0ca2c1574fc88ef8" + resolved "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz" integrity sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g== dependencies: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^7.0.1, strip-ansi@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== - dependencies: - ansi-regex "^6.0.1" - strip-bom-string@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" + resolved "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz" integrity sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g== -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== - -strip-bom@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" - integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== - -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q== - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - strip-final-newline@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" + resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz" integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== -strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -style-to-object@^0.4.1: +style-to-object@^0.4.0: version "0.4.4" - resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.4.4.tgz#266e3dfd56391a7eefb7770423612d043c3f33ec" + resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz" integrity sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg== dependencies: inline-style-parser "0.1.1" style-to-object@^1.0.0: version "1.0.5" - resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-1.0.5.tgz#5e918349bc3a39eee3a804497d97fcbbf2f0d7c0" + resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.5.tgz" integrity sha512-rDRwHtoDD3UMMrmZ6BzOW0naTjMsVZLIjsGleSKS/0Oz+cgCfAPRspaqJuE8rDzpKha/nEvnM0IF4seEAZUTKQ== dependencies: inline-style-parser "0.2.2" -styled-jsx@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f" - integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw== - dependencies: - client-only "0.0.1" - -stylis@^4.1.3: - version "4.3.1" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.1.tgz#ed8a9ebf9f76fe1e12d462f5cc3c4c980b23a7eb" - integrity sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ== - -sucrase@^3.32.0: - version "3.35.0" - resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263" - integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== - dependencies: - "@jridgewell/gen-mapping" "^0.3.2" - commander "^4.0.0" - glob "^10.3.10" - lines-and-columns "^1.1.6" - mz "^2.7.0" - pirates "^4.0.1" - ts-interface-checker "^0.1.9" - -supports-color@^4.0.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" - integrity sha512-ycQR/UbvI9xIlEdQT1TQqwoXtEldExbCEAJgRo5YXlmSKjv6ThHnP9/vwGa1gr19Gfw+LkFd7KqYMhzrRC5JYw== - dependencies: - has-flag "^2.0.0" - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.0.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +stylis@^4.3.1: + version "4.3.4" + resolved "https://registry.npmjs.org/stylis/-/stylis-4.3.4.tgz" + integrity sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now== -synckit@^0.6.0: - version "0.6.2" - resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.6.2.tgz#e1540b97825f2855f7170b98276e8463167f33eb" - integrity sha512-Vhf+bUa//YSTYKseDiiEuQmhGCoIF3CVBhunm3r/DQnYiGT4JssmnKQc44BIyOZRK2pKjXXAgbhfmbeoC9CJpA== - dependencies: - tslib "^2.3.1" +system-architecture@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz" + integrity sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA== -synckit@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.9.0.tgz#5b33b458b3775e4466a5b377fba69c63572ae449" - integrity sha512-7RnqIMq572L8PeEzKeBINYEJDDxpcH8JEgLwUqBd3TkofhFRbkq4QLR0u+36avGAhCRbk2nnmjcW9SE531hPDg== - dependencies: - "@pkgr/core" "^0.1.0" - tslib "^2.6.2" +tabbable@^6.0.0: + version "6.2.0" + resolved "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz" + integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== -tailwindcss@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.1.tgz#f512ca5d1dd4c9503c7d3d28a968f1ad8f5c839d" - integrity sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA== +tailwindcss@^3.4.7: + version "3.4.16" + resolved "file:../node_modules/.pnpm/tailwindcss@3.4.16/node_modules/tailwindcss" dependencies: "@alloc/quick-lru" "^5.2.0" arg "^5.0.2" - chokidar "^3.5.3" + chokidar "^3.6.0" didyoumean "^1.2.2" dlv "^1.1.3" - fast-glob "^3.3.0" + fast-glob "^3.3.2" glob-parent "^6.0.2" is-glob "^4.0.3" - jiti "^1.19.1" - lilconfig "^2.1.0" - micromatch "^4.0.5" + jiti "^1.21.6" + lilconfig "^3.1.3" + micromatch "^4.0.8" normalize-path "^3.0.0" object-hash "^3.0.0" - picocolors "^1.0.0" - postcss "^8.4.23" + picocolors "^1.1.1" + postcss "^8.4.47" postcss-import "^15.1.0" postcss-js "^4.0.1" - postcss-load-config "^4.0.1" - postcss-nested "^6.0.1" - postcss-selector-parser "^6.0.11" - resolve "^1.22.2" - sucrase "^3.32.0" - -tapable@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" - integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== - -test-exclude@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" - integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== - dependencies: - "@istanbuljs/schema" "^0.1.2" - glob "^7.1.4" - minimatch "^3.0.4" - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - -thenify-all@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" - integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== - dependencies: - thenify ">= 3.1.0 < 4" - -"thenify@>= 3.1.0 < 4": - version "3.3.1" - resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" - integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== - dependencies: - any-promise "^1.0.0" - -title@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/title/-/title-3.5.3.tgz#b338d701a3d949db6b49b2c86f409f9c2f36cd91" - integrity sha512-20JyowYglSEeCvZv3EZ0nZ046vLarO37prvV0mbtQV7C8DJPGgN967r8SJkqd3XK3K3lD3/Iyfp3avjfil8Q2Q== - dependencies: - arg "1.0.0" - chalk "2.3.0" - clipboardy "1.2.2" - titleize "1.0.0" - -titleize@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/titleize/-/titleize-1.0.0.tgz#7d350722061830ba6617631e0cfd3ea08398d95a" - integrity sha512-TARUb7z1pGvlLxgPk++7wJ6aycXF3GJ0sNSBTAsTuJrQG5QuZlkUQP+zl+nbjAh4gMX9yDw9ZYklMd7vAfJKEw== - -tmpl@1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" - integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== - -to-fast-properties@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-3.0.1.tgz#c7d507fdccc306c1e6782069db6b0e6fcc1a93f5" - integrity sha512-/wtNi1tW1F3nf0OL6AqVxGw9Tr1ET70InMhJuVxPwFdGqparF0nQ4UWGLf2DsoI2bFDtthlBnALncZpUzOnsUw== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + postcss-load-config "^4.0.2" + postcss-nested "^6.2.0" + postcss-selector-parser "^6.1.2" + resolve "^1.22.8" + sucrase "^3.35.0" + +tinyexec@^0.3.0: + version "0.3.1" + resolved "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz" + integrity sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ== + +title@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/title/-/title-4.0.1.tgz" + integrity sha512-xRnPkJx9nvE5MF6LkB5e8QJjE2FW8269wTu/LQdf7zZqBgPly0QJPf/CWAo7srj5so4yXfoLEdCFgurlpi47zg== dependencies: - is-number "^7.0.0" + arg "^5.0.0" + chalk "^5.0.0" + clipboardy "^4.0.0" trim-lines@^3.0.0: version "3.0.1" - resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" + resolved "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz" integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== trough@^2.0.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" + resolved "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz" integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== -try-catch@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/try-catch/-/try-catch-3.0.1.tgz#93abdca71ce148a08adb49e08dbd491cd485164d" - integrity sha512-91yfXw1rr/P6oLpHSyHDOHm0vloVvUoo9FVdw8YwY05QjJQG9OT0LUxe2VRAzmHG+0CUOmI3nhxDUMLxDN/NEQ== - -try-to-catch@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/try-to-catch/-/try-to-catch-3.0.1.tgz#81ccacb2abd9ef0f313a99eae7752fccf1d17c09" - integrity sha512-hOY83V84Hx/1sCzDSaJA+Xz2IIQOHRvjxzt+F0OjbQGPZ6yLPLArMA0gw/484MlfUkQbCpKYMLX3VDCAjWKfzQ== - -ts-api-utils@^1.0.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.2.1.tgz#f716c7e027494629485b21c0df6180f4d08f5e8b" - integrity sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA== - ts-dedent@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5" + resolved "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz" integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ== -ts-interface-checker@^0.1.9: - version "0.1.13" - resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" - integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== - -tsconfig-paths@^3.15.0: - version "3.15.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" - integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== - dependencies: - "@types/json5" "^0.0.29" - json5 "^1.0.2" - minimist "^1.2.6" - strip-bom "^3.0.0" - -tslib@^1.8.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tslib@^2.0.1, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.6.2: +tslib@^2.4.0, tslib@2: version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== -tsutils@^3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== - dependencies: - tslib "^1.8.1" - -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - -type-detect@4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" - integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== - -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - -type-fest@^1.0.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" - integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== - -type-fest@^3.0.0: - version "3.13.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.13.1.tgz#bb744c1f0678bea7543a2d1ec24e83e68e8c8706" - integrity sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g== - -typed-array-buffer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.1.tgz#0608ffe6bca71bf15a45bff0ca2604107a1325f5" - integrity sha512-RSqu1UEuSlrBhHTWC8O9FnPjOduNs4M7rJ4pRKoEjtx1zUNOPN2sSXHLDX+Y2WPbHIxbvg4JFo2DNAEfPIKWoQ== - dependencies: - call-bind "^1.0.6" - es-errors "^1.3.0" - is-typed-array "^1.1.13" - -typed-array-byte-length@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz#d787a24a995711611fb2b87a4052799517b230d0" - integrity sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA== - dependencies: - call-bind "^1.0.2" - for-each "^0.3.3" - has-proto "^1.0.1" - is-typed-array "^1.1.10" +twoslash-protocol@0.2.12: + version "0.2.12" + resolved "https://registry.npmjs.org/twoslash-protocol/-/twoslash-protocol-0.2.12.tgz" + integrity sha512-5qZLXVYfZ9ABdjqbvPc4RWMr7PrpPaaDSeaYY55vl/w1j6H6kzsWK/urAEIXlzYlyrFmyz1UbwIt+AA0ck+wbg== -typed-array-byte-offset@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz#cbbe89b51fdef9cd6aaf07ad4707340abbc4ea0b" - integrity sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg== +twoslash@^0.2.12: + version "0.2.12" + resolved "https://registry.npmjs.org/twoslash/-/twoslash-0.2.12.tgz" + integrity sha512-tEHPASMqi7kqwfJbkk7hc/4EhlrKCSLcur+TcvYki3vhIfaRMXnXjaYFgXpoZRbT6GdprD4tGuVBEmTpUgLBsw== dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - for-each "^0.3.3" - has-proto "^1.0.1" - is-typed-array "^1.1.10" + "@typescript/vfs" "^1.6.0" + twoslash-protocol "0.2.12" -typed-array-length@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" - integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== +typescript-eslint@^7.18.0: + version "7.18.0" + resolved "file:../node_modules/.pnpm/typescript-eslint@7.18.0_eslint@8.57.1_typescript@5.6.3/node_modules/typescript-eslint" dependencies: - call-bind "^1.0.2" - for-each "^0.3.3" - is-typed-array "^1.1.9" + "@typescript-eslint/eslint-plugin" "7.18.0" + "@typescript-eslint/parser" "7.18.0" + "@typescript-eslint/utils" "7.18.0" -typescript-eslint@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-7.0.2.tgz#2ec8849e3c9b2b56e70851ab22d95003a1d5270a" - integrity sha512-Nsb+Dfi897ErE3CtVJYBECBQWPGEpCXLqLCQarBhFtyJsHnhA7O39GmtAmN3dmZ6bIp8tP5T+AOUrEdE07SBVg== - dependencies: - "@typescript-eslint/eslint-plugin" "7.0.2" - "@typescript-eslint/parser" "7.0.2" +typescript@*, typescript@^5.5.4: + version "5.6.3" + resolved "file:../node_modules/.pnpm/typescript@5.6.3/node_modules/typescript" -typescript@^5.0.4, typescript@^5.3.3: - version "5.3.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" - integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== +ufo@^1.5.4: + version "1.5.4" + resolved "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz" + integrity sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ== -unbox-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" - integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== +unified@^11.0.0: + version "11.0.5" + resolved "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz" + integrity sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA== dependencies: - call-bind "^1.0.2" - has-bigints "^1.0.2" - has-symbols "^1.0.3" - which-boxed-primitive "^1.0.2" - -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== - -unicorn-magic@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.1.0.tgz#1bb9a51c823aaf9d73a8bfcd3d1a23dde94b0ce4" - integrity sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ== + "@types/unist" "^3.0.0" + bail "^2.0.0" + devlop "^1.0.0" + extend "^3.0.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^6.0.0" -unified@^10.0.0: - version "10.1.2" - resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df" - integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q== +unified@^11.0.4: + version "11.0.5" + resolved "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz" + integrity sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA== dependencies: - "@types/unist" "^2.0.0" + "@types/unist" "^3.0.0" bail "^2.0.0" + devlop "^1.0.0" extend "^3.0.0" - is-buffer "^2.0.0" is-plain-obj "^4.0.0" trough "^2.0.0" - vfile "^5.0.0" + vfile "^6.0.0" -unified@^11.0.0: - version "11.0.4" - resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.4.tgz#f4be0ac0fe4c88cb873687c07c64c49ed5969015" - integrity sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ== +unified@^11.0.5: + version "11.0.5" + resolved "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz" + integrity sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA== dependencies: "@types/unist" "^3.0.0" bail "^2.0.0" @@ -8259,63 +3465,51 @@ unified@^11.0.0: unist-util-find-after@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz#3fccc1b086b56f34c8b798e1ff90b5c54468e896" + resolved "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz" integrity sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ== dependencies: "@types/unist" "^3.0.0" unist-util-is "^6.0.0" -unist-util-generated@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.1.tgz#e37c50af35d3ed185ac6ceacb6ca0afb28a85cae" - integrity sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A== - unist-util-is@^5.0.0: version "5.2.1" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.2.1.tgz#b74960e145c18dcb6226bc57933597f5486deae9" + resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz" integrity sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw== dependencies: "@types/unist" "^2.0.0" unist-util-is@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" + resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz" integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== dependencies: "@types/unist" "^3.0.0" -unist-util-position-from-estree@^1.0.0, unist-util-position-from-estree@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/unist-util-position-from-estree/-/unist-util-position-from-estree-1.1.2.tgz#8ac2480027229de76512079e377afbcabcfcce22" - integrity sha512-poZa0eXpS+/XpoQwGwl79UUdea4ol2ZuCYguVaJS4qzIOMDzbqz8a3erUCOmubSZkaOuGamb3tX790iwOIROww== +unist-util-modify-children@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz" + integrity sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw== dependencies: - "@types/unist" "^2.0.0" + "@types/unist" "^3.0.0" + array-iterate "^2.0.0" -unist-util-position@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-4.0.4.tgz#93f6d8c7d6b373d9b825844645877c127455f037" - integrity sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg== +unist-util-position-from-estree@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz" + integrity sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ== dependencies: - "@types/unist" "^2.0.0" + "@types/unist" "^3.0.0" unist-util-position@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" + resolved "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz" integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== dependencies: "@types/unist" "^3.0.0" -unist-util-remove-position@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-4.0.2.tgz#a89be6ea72e23b1a402350832b02a91f6a9afe51" - integrity sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ== - dependencies: - "@types/unist" "^2.0.0" - unist-util-visit "^4.0.0" - unist-util-remove-position@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz#fea68a25658409c9460408bc6b4991b965b52163" + resolved "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz" integrity sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q== dependencies: "@types/unist" "^3.0.0" @@ -8323,46 +3517,38 @@ unist-util-remove-position@^5.0.0: unist-util-remove@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/unist-util-remove/-/unist-util-remove-4.0.0.tgz#94b7d6bbd24e42d2f841e947ed087be5c82b222e" + resolved "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-4.0.0.tgz" integrity sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg== dependencies: "@types/unist" "^3.0.0" unist-util-is "^6.0.0" unist-util-visit-parents "^6.0.0" -unist-util-stringify-position@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d" - integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg== - dependencies: - "@types/unist" "^2.0.0" - unist-util-stringify-position@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" + resolved "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz" integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== dependencies: "@types/unist" "^3.0.0" +unist-util-visit-children@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz" + integrity sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA== + dependencies: + "@types/unist" "^3.0.0" + unist-util-visit-parents@^4.0.0: version "4.1.1" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz#e83559a4ad7e6048a46b1bdb22614f2f3f4724f2" + resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz" integrity sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw== dependencies: "@types/unist" "^2.0.0" unist-util-is "^5.0.0" -unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" - integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" - unist-util-visit-parents@^6.0.0: version "6.0.1" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" + resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz" integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== dependencies: "@types/unist" "^3.0.0" @@ -8370,336 +3556,130 @@ unist-util-visit-parents@^6.0.0: unist-util-visit@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-3.1.0.tgz#9420d285e1aee938c7d9acbafc8e160186dbaf7b" + resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-3.1.0.tgz" integrity sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA== dependencies: "@types/unist" "^2.0.0" unist-util-is "^5.0.0" unist-util-visit-parents "^4.0.0" -unist-util-visit@^4.0.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2" - integrity sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" - unist-util-visit-parents "^5.1.1" - unist-util-visit@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" + resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz" integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== dependencies: "@types/unist" "^3.0.0" unist-util-is "^6.0.0" unist-util-visit-parents "^6.0.0" -update-browserslist-db@^1.0.13: - version "1.0.13" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" - integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -util-deprecate@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -util@^0.12.5: - version "0.12.5" - resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" - integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== - dependencies: - inherits "^2.0.3" - is-arguments "^1.0.4" - is-generator-function "^1.0.7" - is-typed-array "^1.1.3" - which-typed-array "^1.1.2" - -uuid@^9.0.0: +uuid@^9.0.1: version "9.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== -uvu@^0.5.0: - version "0.5.6" - resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df" - integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA== - dependencies: - dequal "^2.0.0" - diff "^5.0.0" - kleur "^4.0.3" - sade "^1.7.3" - -v8-to-istanbul@^9.0.1: - version "9.2.0" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz#2ed7644a245cddd83d4e087b9b33b3e62dfd10ad" - integrity sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA== - dependencies: - "@jridgewell/trace-mapping" "^0.3.12" - "@types/istanbul-lib-coverage" "^2.0.1" - convert-source-map "^2.0.0" - vfile-location@^5.0.0: version "5.0.2" - resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.2.tgz#220d9ca1ab6f8b2504a4db398f7ebc149f9cb464" + resolved "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.2.tgz" integrity sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg== dependencies: "@types/unist" "^3.0.0" vfile "^6.0.0" -vfile-matter@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/vfile-matter/-/vfile-matter-3.0.1.tgz#85e26088e43aa85c04d42ffa3693635fa2bc5624" - integrity sha512-CAAIDwnh6ZdtrqAuxdElUqQRQDQgbbIrYtDYI8gCjXS1qQ+1XdLoK8FIZWxJwn0/I+BkSSZpar3SOgjemQz4fg== - dependencies: - "@types/js-yaml" "^4.0.0" - is-buffer "^2.0.0" - js-yaml "^4.0.0" - -vfile-message@^3.0.0: - version "3.1.4" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea" - integrity sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw== - dependencies: - "@types/unist" "^2.0.0" - unist-util-stringify-position "^3.0.0" - vfile-message@^4.0.0: version "4.0.2" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" + resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz" integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== dependencies: "@types/unist" "^3.0.0" unist-util-stringify-position "^4.0.0" -vfile@^5.0.0, vfile@^5.3.0: - version "5.3.7" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.7.tgz#de0677e6683e3380fafc46544cfe603118826ab7" - integrity sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g== - dependencies: - "@types/unist" "^2.0.0" - is-buffer "^2.0.0" - unist-util-stringify-position "^3.0.0" - vfile-message "^3.0.0" - vfile@^6.0.0: version "6.0.1" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.1.tgz#1e8327f41eac91947d4fe9d237a2dd9209762536" + resolved "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz" integrity sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw== dependencies: "@types/unist" "^3.0.0" unist-util-stringify-position "^4.0.0" vfile-message "^4.0.0" -vscode-oniguruma@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz#439bfad8fe71abd7798338d1cd3dc53a8beea94b" - integrity sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA== +vscode-jsonrpc@8.2.0: + version "8.2.0" + resolved "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz" + integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== -vscode-textmate@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-8.0.0.tgz#2c7a3b1163ef0441097e0b5d6389cd5504b59e5d" - integrity sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg== - -walker@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" - integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== +vscode-languageserver-protocol@3.17.5: + version "3.17.5" + resolved "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz" + integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== dependencies: - makeerror "1.0.12" + vscode-jsonrpc "8.2.0" + vscode-languageserver-types "3.17.5" + +vscode-languageserver-textdocument@~1.0.11: + version "1.0.12" + resolved "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz" + integrity sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA== -watchpack@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== +vscode-languageserver-types@3.17.5: + version "3.17.5" + resolved "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz" + integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== + +vscode-languageserver@~9.0.1: + version "9.0.1" + resolved "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz" + integrity sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g== dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" + vscode-languageserver-protocol "3.17.5" + +vscode-uri@~3.0.8: + version "3.0.8" + resolved "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz" + integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== web-namespaces@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" + resolved "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz" integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== -web-worker@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.3.0.tgz#e5f2df5c7fe356755a5fb8f8410d4312627e6776" - integrity sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA== - -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" - -which-builtin-type@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.3.tgz#b1b8443707cc58b6e9bf98d32110ff0c2cbd029b" - integrity sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw== - dependencies: - function.prototype.name "^1.1.5" - has-tostringtag "^1.0.0" - is-async-function "^2.0.0" - is-date-object "^1.0.5" - is-finalizationregistry "^1.0.2" - is-generator-function "^1.0.10" - is-regex "^1.1.4" - is-weakref "^1.0.2" - isarray "^2.0.5" - which-boxed-primitive "^1.0.2" - which-collection "^1.0.1" - which-typed-array "^1.1.9" - -which-collection@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" - integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== - dependencies: - is-map "^2.0.1" - is-set "^2.0.1" - is-weakmap "^2.0.1" - is-weakset "^2.0.1" - -which-typed-array@^1.1.14, which-typed-array@^1.1.2, which-typed-array@^1.1.9: - version "1.1.14" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.14.tgz#1f78a111aee1e131ca66164d8bdc3ab062c95a06" - integrity sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg== - dependencies: - available-typed-arrays "^1.0.6" - call-bind "^1.0.5" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.1" - -which@^1.2.9: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - which@^2.0.1: version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" - integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== - dependencies: - ansi-styles "^6.1.0" - string-width "^5.0.1" - strip-ansi "^7.0.1" - -wrap-ansi@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.0.tgz#1a3dc8b70d85eeb8398ddfb1e4a02cd186e58b3e" - integrity sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q== - dependencies: - ansi-styles "^6.2.1" - string-width "^7.0.0" - strip-ansi "^7.1.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -write-file-atomic@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" - integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== - dependencies: - imurmurhash "^0.1.4" - signal-exit "^3.0.7" - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +wicked-good-xpath@1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz" + integrity sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw== -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== +xmldom-sre@0.1.31: + version "0.1.31" + resolved "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz" + integrity sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw== -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yaml@^2.3.2: + version "2.4.5" + resolved "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz" + integrity sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg== -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml@2.3.4, yaml@^2.3.4: - version "2.3.4" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" - integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== - -yargs-parser@^21.1.1: - version "21.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== - -yargs@^17.3.1: - version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" - integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1" - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +yocto-queue@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz" + integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g== -yocto-queue@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" - integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== +zod-validation-error@^3.0.0: + version "3.4.0" + resolved "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz" + integrity sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ== -zod@^3.22.3: +zod@^3.18.0, zod@^3.22.3: version "3.22.4" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" + resolved "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz" integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg== -zwitch@^2.0.0: +zwitch@^2.0.0, zwitch@^2.0.4: version "2.0.4" - resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + resolved "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz" integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 60ce88d46d8b1..0000000000000 --- a/docs/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -yarn-error.log -node_modules -.DS_Store -docs/.vuepress/dist diff --git a/docs/.tool-versions b/docs/.tool-versions deleted file mode 100644 index eedff58c83cc6..0000000000000 --- a/docs/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -nodejs 16.16.0 diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index aacbc08ddb26c..0000000000000 --- a/docs/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Pipedream Docs - -## Modifying docs and testing locally - -First, install the dependencies for the repo: - -```bash -yarn install -``` - -Then, run the Vuepress app locally: - -```bash -yarn docs:dev -``` - -This should run a local development server on `http://localhost:8080/`. When you make changes to the Markdown files in the repo, the app should hot reload and refresh the browser automatically. - -And that's it! You're ready to develop Pipedream documentation 🏄 - -## The Pull Request process - -When contributing new code to this repo, please do so on a new Git branch, and submit a pull request to merge changes on that branch into `master`. - -A member of the Pipedream team will automatically be notified when you open a pull request. You can also reach out to us on [our community](https://pipedream.com/community/c/dev/11) or [Slack](https://pipedream.com/support). - -### Spellchecking - -A spellchecker is automatically run on Markdown files in PRs. If you see this check fail when you open a PR, check the output for misspelled words: - -```text -Misspelled words: - README.md: html>body>p --------------------------------------------------------------------------------- -lkjsdflkjsdflkjsdflk --------------------------------------------------------------------------------- - -!!!Spelling check failed!!! -Files in repository contain spelling errors -``` - -Some technical words (like Pipedream or JavaScript) aren't in an English dictionary. If the spellchecker fails on a real word, please add it to the `.wordlist.txt` file. - -The spellchecker configuration can be found in `.spellcheck.yml`. The spellchecking is handled by [this GitHub action](https://github.com/rojopolis/spellcheck-github-actions), which uses [PySpelling](https://facelessuser.github.io/pyspelling/). diff --git a/docs/docs/.vuepress/components/AlphaFeatureNotice.vue b/docs/docs/.vuepress/components/AlphaFeatureNotice.vue deleted file mode 100644 index 79c9c6603365f..0000000000000 --- a/docs/docs/.vuepress/components/AlphaFeatureNotice.vue +++ /dev/null @@ -1,31 +0,0 @@ - - - - - diff --git a/docs/docs/.vuepress/components/BetaFeatureNotice.vue b/docs/docs/.vuepress/components/BetaFeatureNotice.vue deleted file mode 100644 index a394518b39a91..0000000000000 --- a/docs/docs/.vuepress/components/BetaFeatureNotice.vue +++ /dev/null @@ -1,31 +0,0 @@ - - - - - diff --git a/docs/docs/.vuepress/components/Footer.vue b/docs/docs/.vuepress/components/Footer.vue deleted file mode 100644 index 96530a2a235d3..0000000000000 --- a/docs/docs/.vuepress/components/Footer.vue +++ /dev/null @@ -1,15 +0,0 @@ - diff --git a/docs/docs/.vuepress/components/GuideLink.vue b/docs/docs/.vuepress/components/GuideLink.vue deleted file mode 100644 index 22870a58bd47a..0000000000000 --- a/docs/docs/.vuepress/components/GuideLink.vue +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - diff --git a/docs/docs/.vuepress/components/LanguageLink.vue b/docs/docs/.vuepress/components/LanguageLink.vue deleted file mode 100644 index bec36d76156ca..0000000000000 --- a/docs/docs/.vuepress/components/LanguageLink.vue +++ /dev/null @@ -1,17 +0,0 @@ - - diff --git a/docs/docs/.vuepress/components/PythonMappings.vue b/docs/docs/.vuepress/components/PythonMappings.vue deleted file mode 100644 index eb29e15d9cc5e..0000000000000 --- a/docs/docs/.vuepress/components/PythonMappings.vue +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - diff --git a/docs/docs/.vuepress/components/VideoPlayer.vue b/docs/docs/.vuepress/components/VideoPlayer.vue deleted file mode 100644 index 507ba84ac5bcb..0000000000000 --- a/docs/docs/.vuepress/components/VideoPlayer.vue +++ /dev/null @@ -1,37 +0,0 @@ - - - - - diff --git a/docs/docs/.vuepress/components/python-mappings.json b/docs/docs/.vuepress/components/python-mappings.json deleted file mode 100644 index 0c8f9591ebc0d..0000000000000 --- a/docs/docs/.vuepress/components/python-mappings.json +++ /dev/null @@ -1,1078 +0,0 @@ -{ - "ShopifyAPI": "shopify", - "google-cloud-bigquery": "bigquery", - "carbon3d-client": "carbon3d", - "python-telegram-bot": "telegram", - "pyAFQ": "AFQ", - "agpy": "AG_fft_tools", - "pexpect": "screen", - "Adafruit_Libraries": "Adafruit", - "Zope2": "webdav", - "py_Asterisk": "Asterisk", - "bitbucket_jekyll_hook": "BB_jekyll_hook", - "Banzai_NGS": "Banzai", - "BeautifulSoup": "BeautifulSoupTests", - "biopython": "BioSQL", - "BuildbotEightStatusShields": "BuildbotStatusShields", - "ExtensionClass": "MethodObject", - "pycryptodome": "Crypto", - "pycryptodomex": "Cryptodome", - "51degrees_mobile_detector_v3_wrapper": "FiftyOneDegrees", - "pyfunctional": "functional", - "GeoBasesDev": "GeoBases", - "ipython": "IPython", - "astro_kittens": "Kittens", - "python_Levenshtein": "Levenshtein", - "MySQL-python": "MySQLdb", - "PyOpenGL": "OpenGL", - "pyOpenSSL": "OpenSSL", - "Pillow": "PIL", - "astLib": "PyWCSTools", - "astro_pyxis": "Pyxides", - "PySide": "pysideuic", - "s3cmd": "S3", - "pystick": "SCons", - "PyStemmer": "Stemmer", - "topzootools": "TopZooTools", - "DocumentTemplate": "TreeDisplay", - "aspose_pdf_java_for_python": "WorkingWithDocumentConversion", - "auto_adjust_display_brightness": "aadb", - "abakaffe_cli": "abakaffe", - "abiosgaming.py": "abiosgaming", - "abiquo_api": "abiquo", - "abl.cssprocessor": "abl", - "abl.robot": "abl", - "abl.util": "abl", - "abl.vpath": "abl", - "abo_generator": "abo", - "abris": "abris_transform", - "abstract.jwrotator": "abstract", - "abu.admin": "abu", - "AC_Flask_HipChat": "ac_flask", - "anikom15": "acg", - "acme.dchat": "acme", - "acme.hello": "acme", - "acted.projects": "acted", - "ActionServer": "action", - "actionbar.panel": "actionbar", - "afn": "activehomed", - "ActivePapers.Py": "activepapers", - "address_book_lansry": "address_book", - "adi.commons": "adi", - "adi.devgen": "adi", - "adi.fullscreen": "adi", - "adi.init": "adi", - "adi.playlist": "adi", - "adi.samplecontent": "adi", - "adi.slickstyle": "adi", - "adi.suite": "adi", - "adi.trash": "adi", - "aDict2": "adict", - "aditam.agent": "aditam", - "aditam.core": "aditam", - "adium_sh": "adiumsh", - "AdjectorClient": "adjector", - "AdjectorTracPlugin": "adjector", - "Banner_Ad_Toolkit": "adkit", - "django_admin_tools": "admin_tools", - "adminish_categories": "adminishcategories", - "django_admin_sortable": "adminsortable", - "adspygoogle.adwords": "adspygoogle", - "agtl": "advancedcaching", - "Adytum_PyMonitor": "adytum", - "affinitic.docpyflakes": "affinitic", - "affinitic.recipe.fakezope2eggs": "affinitic", - "affinitic.simplecookiecuttr": "affinitic", - "affinitic.verifyinterface": "affinitic", - "affinitic.zamqp": "affinitic", - "afpy.xap": "afpy", - "agate_sql": "agatesql", - "ageliaco.recipe.csvconfig": "ageliaco", - "agent.http": "agent_http", - "Agora_Client": "agora", - "Agora_Fountain": "agora", - "Agora_Fragment": "agora", - "Agora_Planner": "agora", - "Agora_Service_Provider": "agora", - "agoraplex.themes.sphinx": "agoraplex", - "agsci.blognewsletter": "agsci", - "agx.core": "agx", - "agx.dev": "agx", - "agx.generator.buildout": "agx", - "agx.generator.dexterity": "agx", - "agx.generator.generator": "agx", - "agx.generator.plone": "agx", - "agx.generator.pyegg": "agx", - "agx.generator.sql": "agx", - "agx.generator.uml": "agx", - "agx.generator.zca": "agx", - "agx.transform.uml2fs": "agx", - "agx.transform.xmi2uml": "agx", - "aimes.bundle": "aimes", - "aimes.skeleton": "aimes", - "aio.app": "aio", - "aio.config": "aio", - "aio.core": "aio", - "aio.signals": "aio", - "aio_hs2": "aiohs2", - "aio_routes": "aioroutes", - "aio_s3": "aios3", - "airbrake_flask": "airbrake", - "airship_icloud": "airship", - "airship_steamcloud": "airship", - "edgegrid_python": "akamai", - "alation_api": "alation", - "alba_client_python": "alba_client", - "alburnum_maas_client": "alburnum", - "alchemist.audit": "alchemist", - "alchemist.security": "alchemist", - "alchemist.traversal": "alchemist", - "alchemist.ui": "alchemist", - "alchemyapi_python": "alchemyapi", - "alerta_server": "alerta", - "Alexandria_Upload_Utils": "alexandria_upload", - "alibaba_python_sdk": "alibaba", - "aliyun_python_sdk": "aliyun", - "alicloudcli": "aliyuncli", - "aliyun_python_sdk_acs": "aliyunsdkacs", - "aliyun_python_sdk_batchcompute": "aliyunsdkbatchcompute", - "aliyun_python_sdk_bsn": "aliyunsdkbsn", - "aliyun_python_sdk_bss": "aliyunsdkbss", - "aliyun_python_sdk_cdn": "aliyunsdkcdn", - "aliyun_python_sdk_cms": "aliyunsdkcms", - "aliyun_python_sdk_core": "aliyunsdkcore", - "aliyun_python_sdk_crm": "aliyunsdkcrm", - "aliyun_python_sdk_cs": "aliyunsdkcs", - "aliyun_python_sdk_drds": "aliyunsdkdrds", - "aliyun_python_sdk_ecs": "aliyunsdkecs", - "aliyun_python_sdk_ess": "aliyunsdkess", - "aliyun_python_sdk_ft": "aliyunsdkft", - "aliyun_python_sdk_mts": "aliyunsdkmts", - "aliyun_python_sdk_ocs": "aliyunsdkocs", - "aliyun_python_sdk_oms": "aliyunsdkoms", - "aliyun_python_sdk_ossadmin": "aliyunsdkossadmin", - "aliyun_python_sdk_r_kvstore": "aliyunsdkr-kvstore", - "aliyun_python_sdk_ram": "aliyunsdkram", - "aliyun_python_sdk_rds": "aliyunsdkrds", - "aliyun_python_sdk_risk": "aliyunsdkrisk", - "aliyun_python_sdk_ros": "aliyunsdkros", - "aliyun_python_sdk_slb": "aliyunsdkslb", - "aliyun_python_sdk_sts": "aliyunsdksts", - "aliyun_python_sdk_ubsms": "aliyunsdkubsms", - "aliyun_python_sdk_yundun": "aliyunsdkyundun", - "AllAttachmentsMacro": "allattachments", - "allocine_wrapper": "allocine", - "django_allowedsites": "allowedsites", - "alm.solrindex": "alm", - "aloft.py": "aloft", - "alpaca": "alpacalib", - "alphabetic_simple": "alphabetic", - "alphasms_client": "alphasms", - "altered.states": "altered", - "alterootheme.busycity": "alterootheme", - "alterootheme.intensesimplicity": "alterootheme", - "alterootheme.lazydays": "alterootheme", - "alurinium_image_processing": "alurinium", - "alx": "alxlib", - "amara3_iri": "amara3", - "amara3_xml": "amara3", - "AmazonAPIWrapper": "amazon", - "python_amazon_simple_product_api": "amazon", - "ambikesh1349_1": "ambikesh1349-1", - "AmbilightParty": "ambilight", - "amifs_core": "amifs", - "ami_organizer": "amiorganizer", - "amitu.lipy": "amitu", - "amitu_putils": "amitu", - "amitu_websocket_client": "amitu", - "amitu_zutils": "amitu", - "AMLT_learn": "amltlearn", - "amocrm_api": "amocrm", - "amqp_dispatcher": "amqpdispatcher", - "AMQP_Storm": "amqpstorm", - "analytics_python": "analytics", - "AnalyzeDirectory": "analyzedir", - "ancientsolutions_crypttools": "ancientsolutions", - "anderson.paginator": "anderson_paginator", - "android_resource_remover": "android_clean_app", - "AnelPowerControl": "anel_power_control", - "angus_sdk_python": "angus", - "Annalist": "annalist_root", - "ANNOgesic": "annogesiclib", - "ansible_role_apply": "ansible-role-apply", - "ansible_playbook_debugger": "ansibledebugger", - "ansible_docgen": "ansibledocgen", - "ansible_flow": "ansibleflow", - "ansible_inventory_grapher": "ansibleinventorygrapher", - "ansible_lint": "ansiblelint", - "ansible_roles_graph": "ansiblerolesgraph", - "ansible_tools": "ansibletools", - "anthill.exampletheme": "anthill", - "anthill.skinner": "anthill", - "anthill.tal.macrorenderer": "anthill", - "AnthraxDojoFrontend": "anthrax", - "AnthraxHTMLInput": "anthrax", - "AnthraxImage": "anthrax", - "antiweb": "antisphinx", - "antispoofing.evaluation": "antispoofing", - "antlr4_python2_runtime": "antlr4", - "antlr4_python3_runtime": "antlr4", - "antlr4_python_alt": "antlr4", - "anybox.buildbot.openerp": "anybox", - "anybox.nose.odoo": "anybox", - "anybox.paster.odoo": "anybox", - "anybox.paster.openerp": "anybox", - "anybox.recipe.sysdeps": "anybox", - "anybox.scripts.odoo": "anybox", - "google_api_python_client": "googleapiclient", - "google_apitools": "apitools", - "arpm": "apm", - "django_appdata": "app_data", - "django_appconf": "appconf", - "AppDynamicsDownloader": "appd", - "AppDynamicsREST": "appd", - "appdynamics_bindeps_linux_x64": "appdynamics_bindeps", - "appdynamics_bindeps_linux_x86": "appdynamics_bindeps", - "appdynamics_bindeps_osx_x64": "appdynamics_bindeps", - "appdynamics_proxysupport_linux_x64": "appdynamics_proxysupport", - "appdynamics_proxysupport_linux_x86": "appdynamics_proxysupport", - "appdynamics_proxysupport_osx_x64": "appdynamics_proxysupport", - "Appium_Python_Client": "appium", - "applibase": "appliapps", - "broadwick": "appserver", - "archetypes.kss": "archetypes", - "archetypes.multilingual": "archetypes", - "archetypes.schemaextender": "archetypes", - "ansible_role_manager": "arm", - "armor_api": "armor", - "armstrong.apps.related_content": "armstrong", - "armstrong.apps.series": "armstrong", - "armstrong.cli": "armstrong", - "armstrong.core.arm_access": "armstrong", - "armstrong.core.arm_layout": "armstrong", - "armstrong.core.arm_sections": "armstrong", - "armstrong.core.arm_wells": "armstrong", - "armstrong.dev": "armstrong", - "armstrong.esi": "armstrong", - "armstrong.hatband": "armstrong", - "armstrong.templates.standard": "armstrong", - "armstrong.utils.backends": "armstrong", - "armstrong.utils.celery": "armstrong", - "arstecnica.raccoon.autobahn": "arstecnica", - "arstecnica.sqlalchemy.async": "arstecnica", - "article_downloader": "article-downloader", - "artifact_cli": "artifactcli", - "arvados_python_client": "arvados", - "arvados_cwl_runner": "arvados_cwl", - "arvados_node_manager": "arvnodeman", - "AsanaToGithub": "asana_to_github", - "AsciiBinaryConverter": "asciibinary", - "AdvancedSearchDiscovery": "asd", - "askbot_tuan": "askbot", - "askbot_tuanpa": "askbot", - "asnhistory_redis": "asnhistory", - "aspen_jinja2": "aspen_jinja2_renderer", - "aspen_tornado": "aspen_tornado_engine", - "asprise_ocr_sdk_python_api": "asprise_ocr_api", - "aspy.refactor_imports": "aspy", - "aspy.yaml": "aspy", - "asterisk_ami": "asterisk", - "add_asts": "asts", - "asymmetricbase.enum": "asymmetricbase", - "asymmetricbase.fields": "asymmetricbase", - "asymmetricbase.logging": "asymmetricbase", - "asymmetricbase.utils": "asymmetricbase", - "asyncio_irc": "asyncirc", - "asyncmongoorm_je": "asyncmongoorm", - "asyncssh_unofficial": "asyncssh", - "athletelistyy": "athletelist", - "automium": "atm", - "atmosphere_python_client": "atmosphere", - "gdata": "atom", - "AtomicWrite": "atomic", - "atomisator.db": "atomisator", - "atomisator.enhancers": "atomisator", - "atomisator.feed": "atomisator", - "atomisator.indexer": "atomisator", - "atomisator.outputs": "atomisator", - "atomisator.parser": "atomisator", - "atomisator.readers": "atomisator", - "atreal.cmfeditions.unlocker": "atreal", - "atreal.filestorage.common": "atreal", - "atreal.layouts": "atreal", - "atreal.mailservices": "atreal", - "atreal.massloader": "atreal", - "atreal.monkeyplone": "atreal", - "atreal.override.albumview": "atreal", - "atreal.richfile.preview": "atreal", - "atreal.richfile.qualifier": "atreal", - "atreal.usersinout": "atreal", - "atsim.potentials": "atsim", - "attract_sdk": "attractsdk", - "audio.bitstream": "audio", - "audio.coders": "audio", - "audio.filters": "audio", - "audio.fourier": "audio", - "audio.frames": "audio", - "audio.lp": "audio", - "audio.psychoacoustics": "audio", - "audio.quantizers": "audio", - "audio.shrink": "audio", - "audio.wave": "audio", - "auf_refer": "aufrefer", - "auslfe.formonline.content": "auslfe", - "auspost_apis": "auspost", - "auth0_python": "auth0", - "AuthServerClient": "auth_server_client", - "AuthorizeSauce": "authorize", - "AuthzPolicyPlugin": "authzpolicy", - "autobahn_rce": "autobahn", - "geonode_avatar": "avatar", - "android_webview": "awebview", - "azure_common": "azure", - "azure_mgmt_common": "azure", - "azure_mgmt_compute": "azure", - "azure_mgmt_network": "azure", - "azure_mgmt_nspkg": "azure", - "azure_mgmt_resource": "azure", - "azure_mgmt_storage": "azure", - "azure_nspkg": "azure", - "azure_servicebus": "azure", - "azure_servicemanagement_legacy": "azure", - "azure_storage": "azure", - "b2g_commands": "b2gcommands", - "b2gperf_v1.3": "b2gperf", - "b2gperf_v1.4": "b2gperf", - "b2gperf_v2.0": "b2gperf", - "b2gperf_v2.1": "b2gperf", - "b2gperf_v2.2": "b2gperf", - "b2gpopulate_v1.3": "b2gpopulate", - "b2gpopulate_v1.4": "b2gpopulate", - "b2gpopulate_v2.0": "b2gpopulate", - "b2gpopulate_v2.1": "b2gpopulate", - "b2gpopulate_v2.2": "b2gpopulate", - "b3j0f.annotation": "b3j0f", - "b3j0f.aop": "b3j0f", - "b3j0f.conf": "b3j0f", - "b3j0f.sync": "b3j0f", - "b3j0f.utils": "b3j0f", - "Babel": "babel", - "BabelGladeExtractor": "babelglade", - "backplane2_pyclient": "backplane", - "backport_collections": "backport_abcoll", - "backports.functools_lru_cache": "backports", - "backports.inspect": "backports", - "backports.pbkdf2": "backports", - "backports.shutil_get_terminal_size": "backports", - "backports.socketpair": "backports", - "backports.ssl": "backports", - "backports.ssl_match_hostname": "backports", - "backports.statistics": "backports", - "badgekit_api_client": "badgekit", - "BadLinksPlugin": "badlinks", - "bael.project": "bael", - "baidupy": "baidu", - "buildtools": "balrog", - "baluhn_redux": "baluhn", - "bamboo.pantrybell": "bamboo", - "bamboo.scaffold": "bamboo", - "bamboo.setuptools_version": "bamboo", - "bamboo_data": "bamboo", - "bamboo_server": "bamboo", - "bambu_codemirror": "bambu", - "bambu_dataportability": "bambu", - "bambu_enqueue": "bambu", - "bambu_faq": "bambu", - "bambu_ffmpeg": "bambu", - "bambu_grids": "bambu", - "bambu_international": "bambu", - "bambu_jwplayer": "bambu", - "bambu_minidetect": "bambu", - "bambu_navigation": "bambu", - "bambu_notifications": "bambu", - "bambu_payments": "bambu", - "bambu_pusher": "bambu", - "bambu_saas": "bambu", - "bambu_sites": "bambu", - "Bananas": "banana", - "banana.maya": "banana", - "bangtext": "bang", - "barcode_generator": "barcode", - "bark_ssg": "bark", - "BarkingOwl": "barking_owl", - "bart_py": "bart", - "basalt_tasks": "basalt", - "base_62": "base62", - "basemap_Jim": "basemap", - "bash_toolbelt": "bash", - "Python_Bash_Utils": "bashutils", - "BasicHttp": "basic_http", - "basil_daq": "basil", - "azure_batch_apps": "batchapps", - "python_bcrypt": "bcrypt", - "Beaker": "beaker", - "beets": "beetsplug", - "begins": "begin", - "bench_it": "benchit", - "beproud.utils": "beproud", - "burrito_fillings": "bfillings", - "BigJob": "pilot", - "billboard.py": "billboard", - "anaconda_build": "binstar_build_client", - "anaconda_client": "binstar_client", - "biocommons.dev": "biocommons", - "birdhousebuilder.recipe.conda": "birdhousebuilder", - "birdhousebuilder.recipe.docker": "birdhousebuilder", - "birdhousebuilder.recipe.redis": "birdhousebuilder", - "birdhousebuilder.recipe.supervisor": "birdhousebuilder", - "pymeshio": "blender26-meshio", - "borg.localrole": "borg", - "bagofwords": "bow", - "bpython": "bpdb", - "bisque_api": "bqapi", - "django_braces": "braces", - "briefs_caster": "briefscaster", - "brisa_media_server_plugins": "brisa_media_server/plugins", - "brkt_sdk": "brkt_requests", - "broadcast_logging": "broadcastlogging", - "brocade_tool": "brocadetool", - "bronto_python": "bronto", - "Brownie": "brownie", - "browsermob_proxy": "browsermobproxy", - "brubeck_mysql": "brubeckmysql", - "brubeck_oauth": "brubeckoauth", - "brubeck_service": "brubeckservice", - "brubeck_uploader": "brubeckuploader", - "beautifulsoup4": "bs4", - "pymongo": "gridfs", - "bst.pygasus.core": "bst", - "bst.pygasus.datamanager": "bst", - "bst.pygasus.demo": "bst", - "bst.pygasus.i18n": "bst", - "bst.pygasus.resources": "bst", - "bst.pygasus.scaffolding": "bst", - "bst.pygasus.security": "bst", - "bst.pygasus.session": "bst", - "bst.pygasus.wsgi": "bst", - "btable_py": "btable", - "bananatag_api": "btapi", - "btce_api": "btceapi", - "btce_bot": "btcebot", - "btsync.py": "btsync", - "buck.pprint": "buck", - "bud.nospam": "bud", - "budy_api": "budy", - "buffer_alpaca": "buffer", - "bug.gd": "buggd", - "bugle_sites": "bugle", - "bug_spots": "bugspots", - "python_bugzilla": "bugzilla", - "bugzscout_py": "bugzscout", - "ajk_ios_buildTools": "buildTools", - "BuildNotify": "buildnotifylib", - "buildout.bootstrap": "buildout", - "buildout.disablessl": "buildout", - "buildout.dumppickedversions": "buildout", - "buildout.dumppickedversions2": "buildout", - "buildout.dumprequirements": "buildout", - "buildout.eggnest": "buildout", - "buildout.eggscleaner": "buildout", - "buildout.eggsdirectories": "buildout", - "buildout.eggtractor": "buildout", - "buildout.extensionscripts": "buildout", - "buildout.locallib": "buildout", - "buildout.packagename": "buildout", - "buildout.recipe.isolation": "buildout", - "buildout.removeaddledeggs": "buildout", - "buildout.requirements": "buildout", - "buildout.sanitycheck": "buildout", - "buildout.sendpickedversions": "buildout", - "buildout.threatlevel": "buildout", - "buildout.umask": "buildout", - "buildout.variables": "buildout", - "buildbot_slave": "buildslave", - "pies2overrides": "xmlrpc", - "bumper_lib": "bumper", - "bumple_downloader": "bumple", - "bundesliga_cli": "bundesliga", - "bundlemanager": "bundlemaker", - "burp_ui": "burpui", - "busyflow.pivotal": "busyflow", - "buttercms_django": "buttercms-django", - "buzz_python_client": "buzz", - "buildout_versions_checker": "bvc", - "bvg_grabber": "bvggrabber", - "BYONDTools": "byond", - "Bugzilla_ETL": "bzETL", - "bugzillatools": "bzlib", - "bzr": "bzrlib", - "bzr_automirror": "bzrlib", - "bzr_bash_completion": "bzrlib", - "bzr_colo": "bzrlib", - "bzr_killtrailing": "bzrlib", - "bzr_pqm": "bzrlib", - "c2c.cssmin": "c2c", - "c2c.recipe.closurecompile": "c2c", - "c2c.recipe.cssmin": "c2c", - "c2c.recipe.jarfile": "c2c", - "c2c.recipe.msgfmt": "c2c", - "c2c.recipe.pkgversions": "c2c", - "c2c.sqlalchemy.rest": "c2c", - "c2c.versions": "c2c", - "c2c.recipe.facts": "c2c_recipe_facts", - "cabalgata_silla_de_montar": "cabalgata", - "cabalgata_zookeeper": "cabalgata", - "django_cache_utils": "cache_utils", - "django_recaptcha": "captcha", - "Cartridge": "cartridge", - "cassandra_driver": "cassandra", - "CassandraLauncher": "cassandralauncher", - "42qucc": "cc42", - "Cerberus": "cerberus", - "cfn-lint": "cfnlint", - "Chameleon": "chameleon", - "charm_tools": "charmtools", - "PyChef": "chef", - "c8d": "chip8", - "python_cjson": "cjson", - "django_classy_tags": "classytags", - "ConcurrentLogHandler": "cloghandler", - "virtualenv_clone": "clonevirtualenv", - "al_cloudinsight": "cloud-insight", - "adminapi": "cloud_admin", - "python_cloudservers": "cloudservers", - "cerebrod": "tasksitter", - "django_cms": "cms", - "ba_colander": "colander", - "ansicolors": "colors", - "bf_lc3": "compile", - "docker_compose": "compose", - "django_compressor": "compressor", - "futures": "concurrent", - "ConfigArgParse": "configargparse", - "PyContracts": "contracts", - "weblogo": "weblogolib", - "Couchapp": "couchapp", - "CouchDB": "couchdb", - "couchdb_python_curl": "couchdbcurl", - "coursera_dl": "courseradownloader", - "cow_framework": "cow", - "python_creole": "creole", - "Creoleparser": "creoleparser", - "django_crispy_forms": "crispy_forms", - "python_crontab": "crontab", - "tff": "ctff", - "pycups": "cups", - "elasticsearch_curator": "curator", - "pycurl": "curl", - "python_daemon": "daemon", - "DARE": "dare", - "python_dateutil": "dateutil", - "DAWG": "dawg", - "python_debian": "debian", - "python-decouple": "decouple", - "webunit": "demo", - "PySynth": "pysynth_samp", - "juju_deployer": "deployer", - "filedepot": "depot", - "tg.devtools": "devtools", - "2gis": "dgis", - "pyDHTMLParser": "dhtmlparser", - "python_digitalocean": "digitalocean", - "discord.py": "discord", - "ez_setup": "distribute_setup", - "Distutils2": "distutils2", - "Django": "django", - "amitu_hstore": "django_hstore", - "django_bower": "djangobower", - "django_celery": "djcelery", - "django_kombu": "djkombu", - "djorm_ext_pgarray": "djorm_pgarray", - "dnspython": "dns", - "ansible_docgenerator": "docgen", - "docker_py": "docker", - "dogpile.cache": "dogpile", - "dogpile.core": "dogpile", - "dogapi": "dogshell", - "pydot": "dot_parser", - "pydot2": "dot_parser", - "pydot3k": "dot_parser", - "python-dotenv": "dotenv", - "dpkt_fix": "dpkt", - "python_ldap": "ldif", - "django_durationfield": "durationfield", - "datazilla": "dzclient", - "easybuild_framework": "easybuild", - "python_editor": "editor", - "azure_elasticluster": "elasticluster", - "azure_elasticluster_current": "elasticluster", - "pyelftools": "elftools", - "Elixir": "elixir", - "empy": "emlib", - "pyenchant": "enchant", - "cssutils": "encutils", - "python_engineio": "engineio", - "enum34": "enum", - "pyephem": "ephem", - "abl.errorreporter": "errorreporter", - "beaker_es_plot": "esplot", - "adrest": "example", - "tweepy": "examples", - "pycassa": "ez_setup", - "Fabric": "fabric", - "Faker": "faker", - "python_fedora": "fedora", - "ailove_django_fias": "fias", - "51degrees_mobile_detector": "fiftyone_degrees", - "five.customerize": "five", - "five.globalrequest": "five", - "five.intid": "five", - "five.localsitemanager": "five", - "five.pt": "five", - "android_flasher": "flasher", - "Flask": "flask", - "Frozen_Flask": "flask_frozen", - "Flask_And_Redis": "flask_redis", - "Flask_Bcrypt": "flaskext", - "vnc2flv": "flvscreen", - "django_followit": "followit", - "pyforge": "forge", - "FormEncode": "formencode", - "django_formtools": "formtools", - "4ch": "fourch", - "allegrordf": "franz", - "freetype_py": "freetype", - "python_frontmatter": "frontmatter", - "ftp_cloudfs": "ftpcloudfs", - "librabbitmq": "funtests", - "fusepy": "fuse", - "Fuzzy": "fuzzy", - "tiddlyweb": "gabbi", - "3d_wallet_generator": "gen_3dwallet", - "android_gendimen": "gendimen", - "Genshi": "genshi", - "python_geohash": "quadtree", - "GeoNode": "geonode", - "gsconfig": "geoserver", - "Geraldo": "geraldo", - "django_getenv": "getenv", - "gevent_websocket": "geventwebsocket", - "python_gflags": "gflags", - "GitPython": "git", - "PyGithub": "github", - "github3.py": "github3", - "git_py": "gitpy", - "globusonline_transfer_api_client": "globusonline", - "protobuf": "google", - "grace_dizmo": "grace-dizmo", - "anovelmous_grammar": "grammar", - "graphenelib": "grapheneapi", - "scales": "greplin", - "grokcore.component": "grokcore", - "gsutil": "gslib", - "PyHamcrest": "hamcrest", - "HARPy": "harpy", - "PyHawk_with_a_single_extra_commit": "hawk", - "django_haystack": "haystack", - "mercurial": "hgext", - "hg_git": "hggit", - "python_hglib": "hglib", - "pisa": "sx", - "amarokHola": "hola", - "Hoover": "hoover", - "python_hostlist": "hostlist", - "nosehtmloutput": "htmloutput", - "django_hvad": "hvad", - "hydra-core": "hydra", - "199Fix": "i99fix", - "python_igraph": "igraph", - "IMDbPY": "imdb", - "impyla": "impala", - "ambition_inmemorystorage": "inmemorystorage", - "backport_ipaddress": "ipaddress", - "jaraco.timing": "jaraco", - "jaraco.util": "jaraco", - "Jinja2": "jinja2", - "jira_cli": "jiracli", - "johnny_cache": "johnny", - "JPype1": "jpypex", - "django_jsonfield": "jsonfield", - "aino_jstools": "jstools", - "jupyter_pip": "jupyterpip", - "PyJWT": "jwt", - "asana_kazoo": "kazoo", - "line_profiler": "kernprof", - "python_keyczar": "keyczar", - "django_keyedcache": "keyedcache", - "python_keystoneclient": "keystoneclient", - "kickstart": "kickstarter", - "krbV": "krbv", - "kss.core": "kss", - "Kuyruk": "kuyruk", - "AdvancedLangConv": "langconv", - "lava_utils_interface": "lava", - "lazr.authentication": "lazr", - "lazr.restfulclient": "lazr", - "lazr.uri": "lazr", - "adpasswd": "ldaplib", - "2or3": "lib2or3", - "3to2": "lib3to2", - "Aito": "libaito", - "bugs_everywhere": "libbe", - "bucket": "libbucket", - "apache_libcloud": "libcloud", - "future": "winreg", - "generateDS": "libgenerateDS", - "mitmproxy": "libmproxy", - "7lk_ocr_deploy": "libsvm", - "lisa_server": "lisa", - "aspose_words_java_for_python": "loadingandsaving", - "locustio": "locust", - "Logbook": "logbook", - "buildbot_status_logentries": "logentries", - "logilab_mtconverter": "logilab", - "python_magic": "magic", - "Mako": "mako", - "ManifestDestiny": "manifestparser", - "marionette_client": "marionette", - "Markdown": "markdown", - "pytest_marks": "marks", - "MarkupSafe": "markupsafe", - "pymavlink": "mavnative", - "python_memcached": "memcache", - "AllPairs": "metacomm", - "Metafone": "metaphone", - "metlog_py": "metlog", - "Mezzanine": "mezzanine", - "sqlalchemy_migrate": "migrate", - "python_mimeparse": "mimeparse", - "minitage.paste": "minitage", - "minitage.recipe.common": "minitage", - "android_missingdrawables": "missingdrawables", - "2lazy2rest": "mkrst_themes", - "mockredispy": "mockredis", - "python_modargs": "modargs", - "django_model_utils": "model_utils", - "asposebarcode": "models", - "asposestorage": "models", - "moksha.common": "moksha", - "moksha.hub": "moksha", - "moksha.wsgi": "moksha", - "py_moneyed": "moneyed", - "MongoAlchemy": "mongoalchemy", - "MonthDelta": "monthdelta", - "Mopidy": "mopidy", - "MoPyTools": "mopytools", - "django_mptt": "mptt", - "python-mpv": "mpv", - "mr.bob": "mrbob", - "msgpack_python": "msgpack", - "aino_mutations": "mutations", - "amazon_mws": "mws", - "mysql_connector_repackaged": "mysql", - "django_native_tags": "native_tags", - "ndg_httpsclient": "ndg", - "trytond_nereid": "nereid", - "baojinhuan": "nested", - "Amauri": "nester", - "abofly": "nester", - "bssm_pythonSig": "nester", - "python_novaclient": "novaclient", - "alauda_django_oauth": "oauth2_provider", - "oauth2client": "oauth2client", - "odfpy": "odf", - "Parsley": "ometa", - "python_openid": "openid", - "ali_opensearch": "opensearchsdk", - "oslo.i18n": "oslo_i18n", - "oslo.serialization": "oslo_serialization", - "oslo.utils": "oslo_utils", - "alioss": "oss", - "aliyun_python_sdk_oss": "oss", - "aliyunoss": "oss", - "cashew": "output", - "OWSLib": "owslib", - "nwdiag": "rackdiag", - "paho_mqtt": "paho", - "django_paintstore": "paintstore", - "django_parler": "parler", - "PasteScript": "paste", - "forked_path": "path", - "path.py": "path", - "patricia-trie": "patricia", - "Paver": "paver", - "ProxyTypes": "peak", - "anderson.picasso": "picasso", - "django-picklefield": "picklefield", - "pivotal_py": "pivotal", - "peewee": "pwiz", - "plivo": "plivoxml", - "plone.alterego": "plone", - "plone.api": "plone", - "plone.app.blob": "plone", - "plone.app.collection": "plone", - "plone.app.content": "plone", - "plone.app.contentlisting": "plone", - "plone.app.contentmenu": "plone", - "plone.app.contentrules": "plone", - "plone.app.contenttypes": "plone", - "plone.app.controlpanel": "plone", - "plone.app.customerize": "plone", - "plone.app.dexterity": "plone", - "plone.app.discussion": "plone", - "plone.app.event": "plone", - "plone.app.folder": "plone", - "plone.app.i18n": "plone", - "plone.app.imaging": "plone", - "plone.app.intid": "plone", - "plone.app.layout": "plone", - "plone.app.linkintegrity": "plone", - "plone.app.locales": "plone", - "plone.app.lockingbehavior": "plone", - "plone.app.multilingual": "plone", - "plone.app.portlets": "plone", - "plone.app.querystring": "plone", - "plone.app.redirector": "plone", - "plone.app.registry": "plone", - "plone.app.relationfield": "plone", - "plone.app.textfield": "plone", - "plone.app.theming": "plone", - "plone.app.users": "plone", - "plone.app.uuid": "plone", - "plone.app.versioningbehavior": "plone", - "plone.app.viewletmanager": "plone", - "plone.app.vocabularies": "plone", - "plone.app.widgets": "plone", - "plone.app.workflow": "plone", - "plone.app.z3cform": "plone", - "plone.autoform": "plone", - "plone.batching": "plone", - "plone.behavior": "plone", - "plone.browserlayer": "plone", - "plone.caching": "plone", - "plone.contentrules": "plone", - "plone.dexterity": "plone", - "plone.event": "plone", - "plone.folder": "plone", - "plone.formwidget.namedfile": "plone", - "plone.formwidget.recurrence": "plone", - "plone.i18n": "plone", - "plone.indexer": "plone", - "plone.intelligenttext": "plone", - "plone.keyring": "plone", - "plone.locking": "plone", - "plone.memoize": "plone", - "plone.namedfile": "plone", - "plone.outputfilters": "plone", - "plone.portlet.collection": "plone", - "plone.portlet.static": "plone", - "plone.portlets": "plone", - "plone.protect": "plone", - "plone.recipe.zope2install": "plone", - "plone.registry": "plone", - "plone.resource": "plone", - "plone.resourceeditor": "plone", - "plone.rfc822": "plone", - "plone.scale": "plone", - "plone.schema": "plone", - "plone.schemaeditor": "plone", - "plone.session": "plone", - "plone.stringinterp": "plone", - "plone.subrequest": "plone", - "plone.supermodel": "plone", - "plone.synchronize": "plone", - "plone.theme": "plone", - "plone.transformchain": "plone", - "plone.uuid": "plone", - "plone.z3cform": "plone", - "plonetheme.barceloneta": "plonetheme", - "pypng": "png", - "django_polymorphic": "polymorphic", - "python_postmark": "postmark", - "bash_powerprompt": "powerprompt", - "django-prefetch": "prefetch", - "AndrewList": "printList", - "progressbar2": "progressbar", - "progressbar33": "progressbar", - "django_oauth2_provider": "provider", - "pure_sasl": "puresasl", - "pylzma": "py7zlib", - "pyAMI_core": "pyAMI", - "arsespyder": "pyarsespyder", - "asdf": "pyasdf", - "aspell_python_ctypes": "pyaspell", - "pybbm": "pybb", - "pybloomfiltermmap": "pybloomfilter", - "Pyccuracy": "pyccuracy", - "PyCK": "pyck", - "python_crfsuite": "pycrfsuite", - "PyDispatcher": "pydispatch", - "pygeocoder": "pygeolib", - "Pygments": "pygments", - "python_graph_core": "pygraph", - "pyjon.utils": "pyjon", - "python_jsonrpc": "pyjsonrpc", - "Pykka": "pykka", - "PyLogo": "pylogo", - "adhocracy_Pylons": "pylons", - "libmagic": "pymagic", - "Amalwebcrawler": "pymycraawler", - "AbakaffeNotifier": "pynma", - "Pyphen": "pyphen", - "AEI": "pyrimaa", - "adhocracy_pysqlite": "pysqlite2", - "pysqlite": "pysqlite2", - "python_gettext": "pythongettext", - "python_json_logger": "pythonjsonlogger", - "PyUtilib": "pyutilib", - "Cython": "pyximport", - "qserve": "qs", - "django_quickapi": "quickapi", - "nose_quickunit": "quickunit", - "radical.pilot": "radical", - "radical.utils": "radical", - "readability_lxml": "readability", - "gnureadline": "readline", - "django_recaptcha_works": "recaptcha_works", - "RelStorage": "relstorage", - "django_reportapi": "reportapi", - "Requests": "requests", - "requirements_parser": "requirements", - "djangorestframework": "rest_framework", - "py_restclient": "restclient", - "async_retrial": "retrial", - "django_reversion": "reversion", - "rhaptos2.common": "rhaptos2", - "robotframework": "robot", - "django_robots": "robots", - "rosdep": "rosdep2", - "RSFile": "rsbackends", - "ruamel.base": "ruamel", - "pysaml2": "xmlenc", - "saga_python": "saga", - "aws-sam-translator": "samtranslator", - "libsass": "sassutils", - "alex_sayhi": "sayhi", - "scalr": "scalrtools", - "scikits.talkbox": "scikits", - "scratchpy": "scratch", - "pyScss": "scss", - "dict.sorted": "sdict", - "android_sdk_updater": "sdk_updater", - "django_sekizai": "sekizai", - "pysendfile": "sendfile", - "pyserial": "serial", - "astor": "setuputils", - "pyshp": "shapefile", - "Shapely": "shapely", - "ahonya_sika": "sika", - "pysingleton": "singleton", - "scikit_bio": "skbio", - "scikit_learn": "sklearn", - "slackclient": "slack", - "unicode_slugify": "slugify", - "smk_python_sdk": "smarkets", - "ctypes_snappy": "snappy", - "gevent_socketio": "socketio", - "sockjs_tornado": "sockjs", - "SocksiPy_branch": "socks", - "solrpy": "solr", - "Solution": "solution", - "sorl_thumbnail": "sorl", - "South": "south", - "Sphinx": "sphinx", - "ATD_document": "sphinx_pypi_upload", - "sphinxcontrib_programoutput": "sphinxcontrib", - "SQLAlchemy": "sqlalchemy", - "atlas": "src", - "auto_mix_prep": "src", - "bw_stats_toolkit": "stats_toolkit", - "dogstatsd_python": "statsd", - "python_stdnum": "stdnum", - "StoneageHTML": "stoneagehtml", - "django_storages": "storages", - "mox": "stubout", - "suds_jurko": "suds", - "python_swiftclient": "swiftclient", - "pytabix": "test", - "django_taggit": "taggit", - "django_tastypie": "tastypie", - "teamcity_messages": "teamcity", - "pyTelegramBotAPI": "telebot", - "Tempita": "tempita", - "Tenjin": "tenjin", - "python_termstyle": "termstyle", - "treeherder_client": "thclient", - "django_threaded_multihost": "threaded_multihost", - "3color_Press": "threecolor", - "pytidylib": "tidylib", - "3lwg": "tlw", - "toredis_fork": "toredis", - "tornado_redis": "tornadoredis", - "ansible_tower_cli": "tower_cli", - "Trac": "tracopt", - "android_localization_helper": "translation_helper", - "django_treebeard": "treebeard", - "trytond_stock": "trytond", - "tsuru_circus": "tsuru", - "python_tvrage": "tvrage", - "tw2.core": "tw2", - "tw2.d3": "tw2", - "tw2.dynforms": "tw2", - "tw2.excanvas": "tw2", - "tw2.forms": "tw2", - "tw2.jit": "tw2", - "tw2.jqplugins.flot": "tw2", - "tw2.jqplugins.gritter": "tw2", - "tw2.jqplugins.ui": "tw2", - "tw2.jquery": "tw2", - "tw2.sqla": "tw2", - "Twisted": "twisted", - "python_twitter": "twitter", - "transifex_client": "txclib", - "115wangpan": "u115", - "Unidecode": "unidecode", - "ansible_universe": "universe", - "pyusb": "usb", - "useless.pipes": "useless", - "auth_userpass": "userpass", - "automakesetup.py": "utilities", - "aino_utkik": "utkik", - "uWSGI": "uwsgidecorators", - "ab": "valentine", - "configobj": "validate", - "chartio": "version", - "ar_virtualenv_api": "virtualenvapi", - "brocade_plugins": "vyatta", - "WebOb": "webob", - "websocket_client": "websocket", - "WebTest": "webtest", - "Werkzeug": "werkzeug", - "wheezy.caching": "wheezy", - "wheezy.core": "wheezy", - "wheezy.http": "wheezy", - "tiddlywebwiki": "wikklytext", - "pywinrm": "winrm", - "Alfred_Workflow": "workflow", - "WSME": "wsmeext", - "WTForms": "wtforms", - "wtf_peewee": "wtfpeewee", - "pyxdg": "xdg", - "pytest_xdist": "xdist", - "xmpppy": "xmpp", - "XStatic_Font_Awesome": "xstatic", - "XStatic_jQuery": "xstatic", - "XStatic_jquery_ui": "xstatic", - "PyYAML": "yaml", - "z3c.autoinclude": "z3c", - "z3c.caching": "z3c", - "z3c.form": "z3c", - "z3c.formwidget.query": "z3c", - "z3c.objpath": "z3c", - "z3c.pt": "z3c", - "z3c.relationfield": "z3c", - "z3c.traverser": "z3c", - "z3c.zcmlhook": "z3c", - "pyzmq": "zmq", - "zopyx.textindexng3": "zopyx" -} \ No newline at end of file diff --git a/docs/docs/.vuepress/config.js b/docs/docs/.vuepress/config.js deleted file mode 100644 index 857506085e678..0000000000000 --- a/docs/docs/.vuepress/config.js +++ /dev/null @@ -1,34 +0,0 @@ -const path = require("path"); -const webpack = require("webpack"); -const themeConfig = require("./configs/themeConfig"); - -module.exports = { - title: "", - head: [ - ["link", { rel: "icon", href: "/favicon.ico" }], - ['meta', { name: 'version', content: 'latest' }] - ], - description: "Pipedream Documentation - Connect APIs, remarkably fast", - base: "/docs/", - plugins: [ - [ - "vuepress-plugin-canonical", - { - baseURL: "https://pipedream.com/docs", // base url for your canonical link, optional, default: '' - stripExtension: true, - }, - ], - "check-md", - "tabs", - ['vuepress-plugin-code-copy', { - color: '#34d28b', - backgroundColor: '#34d28b', - backgroundTransition: false, - successText: 'Copied' - }] - ], - themeConfig, - postcss: { - plugins: [require("autoprefixer"), require("tailwindcss")], - } -}; diff --git a/docs/docs/.vuepress/configs/envVars.js b/docs/docs/.vuepress/configs/envVars.js deleted file mode 100644 index 2260bf3c58021..0000000000000 --- a/docs/docs/.vuepress/configs/envVars.js +++ /dev/null @@ -1,42 +0,0 @@ -module.exports = { - PIPEDREAM_BASE_URL: "https://pipedream.com", - API_BASE_URL: "https://api.pipedream.com/v1", - SQL_API_BASE_URL: "https://rt.pipedream.com/sql", - ENDPOINT_BASE_URL: "*.m.pipedream.net", - PAYLOAD_SIZE_LIMIT: "512KB", - MEMORY_LIMIT: "256MB", - MEMORY_ABSOLUTE_LIMIT: "10GB", - EMAIL_PAYLOAD_SIZE_LIMIT: "30MB", - MAX_WORKFLOW_EXECUTION_LIMIT: "750", - base_credits_price: { - memory: 256, - seconds: 30, - }, - DATA_STORES_MAX_KEYS: "1,024", - DAILY_CREDITS_LIMIT: "25", - DAILY_TESTING_LIMIT: "30 minutes", - INSPECTOR_EVENT_EXPIRY_DAYS: "365", - FUNCTION_PAYLOAD_LIMIT: "6MB", - DAILY_INVOCATIONS_LIMIT: "333", - FREE_ORG_DAILY_INVOCATIONS_LIMIT: "66", - PRICE_PER_INVOCATION: "0.0002", - FREE_MONTHLY_INVOCATIONS: "10,000", - PRO_MONTHLY_INVOCATIONS: "20,000", - TEAM_MONTHLY_INVOCATIONS: "20,000", - TEAM_MEMBER_LIMIT: "5", - PRO_MONTHLY_PRICE: "$19", - TEAM_MONTHLY_PRICE: "$19", - DEFAULT_WORKFLOW_QUEUE_SIZE: "100", - MAX_WORKFLOW_QUEUE_SIZE: "10,000", - NODE_VERSION: "18", - PYTHON_VERSION: "3.9", - GO_LANG_VERSION: "1.21.5", - CONFIGURED_PROPS_SIZE_LIMIT: "64KB", - SERVICE_DB_SIZE_LIMIT: "60KB", - TMP_SIZE_LIMIT: "2GB", - DELAY_MIN_MAX_TIME: - "You can pause your workflow for as little as one millisecond, or as long as one year", - PUBLIC_APPS: "1,700", - WARM_WORKERS_INTERVAL: "10 minutes", - WARM_WORKERS_CREDITS_PER_INTERVAL: 5, -}; diff --git a/docs/docs/.vuepress/configs/navConfig.js b/docs/docs/.vuepress/configs/navConfig.js deleted file mode 100644 index df827632c2390..0000000000000 --- a/docs/docs/.vuepress/configs/navConfig.js +++ /dev/null @@ -1,58 +0,0 @@ -module.exports = { - left: [ - { - text: "Documentation", - link: "/", - }, - { - text: "Quickstart", - link: "/quickstart/", - variant: "primary", - }, - { - text: "Guides", - link: "/guides/", - }, - { - text: "Reference", - items: [ - { text: "Components API", link: "/components/api/" }, - { text: "CLI", link: "/cli/install/" }, - { text: "REST API", link: "/api/" }, - { text: "Limits", link: "/limits/" }, - { text: "Security & Privacy", link: "/privacy-and-security/"}, - { text: "Handling Cold Starts", link: "/workflows/settings/#eliminate-cold-starts"}, - ], - }, - ], - right: [ - { - text: "Support", - link: "https://pipedream.com/support", - internal: true, - }, - { - text: "Pricing", - link: "/pricing/", - }, - { - text: "v2", - className: "docs-version", - ariaLabel: "Docs Version Menu", - items: [ - { - text: "v2", - link: "https://pipedream.com/docs", - internal: true, - }, - { - text: "v1", - link: "https://pipedream.com/docs/v1", - internal: true, - badge: "Deprecated", - badgeVariation: "danger", - }, - ], - }, - ] -}; diff --git a/docs/docs/.vuepress/configs/sidebarConfig.js b/docs/docs/.vuepress/configs/sidebarConfig.js deleted file mode 100644 index c430e8813c498..0000000000000 --- a/docs/docs/.vuepress/configs/sidebarConfig.js +++ /dev/null @@ -1,216 +0,0 @@ -// NEW NAV - -const docsNav = [ - { - title: "Quickstart", - children: [ - { - title: "Develop Workflows", - path: "/quickstart/", - }, - { - title: "Use GitHub Sync", - path: "/quickstart/github-sync/", - }, - ] - }, - "/workspaces/", - { - title: "Projects", - children: [ - "/projects/", - "/projects/git/", - { - title: "File Stores", - type: "group", - children: [{ title: "File Stores", path: "/projects/file-stores/" }, { title: "Node.js Reference", path: "/projects/file-stores/reference/" }], - }, - ] - }, - { - title: "Workflows", - children: [ - "/workflows/", - "/workflows/steps/", - "/workflows/steps/triggers/", - "/workflows/steps/actions/", - "/workflows/steps/using-props/", - "/workflows/events/", - "/workflows/events/inspect/", - "/workflows/flow-control/", - "/workflows/errors/", - "/workflows/concurrency-and-throttling/", - "/workflows/settings/", - "/workflows/vpc/", - "/workflows/domains/", - "/workflows/sharing/", - "/migrate-from-v1/", - ], - }, - "/event-history/", - "/sources/", - { - title: "Connected Accounts", - children: [ - "/connected-accounts/", - "/connected-accounts/api/", - "/connected-accounts/external-auth/", - ], - }, - ["/data-stores/", "Data Stores"], - { - title: "Code", - children: [ - "/code/", - { - title: "Node.js", - type: "group", - collapsable: false, - sidebarDepth: 2, - children: [ - "/code/nodejs/", - "/code/nodejs/ai-code-generation/", - "/code/nodejs/auth/", - "/code/nodejs/http-requests/", - "/code/nodejs/working-with-files/", - "/code/nodejs/using-data-stores/", - "/code/nodejs/delay/", - "/code/nodejs/rerun/", - "/environment-variables/", - "/code/nodejs/async/", - "/code/nodejs/sharing-code/", - "/code/nodejs/browser-automation/", - { - title: "Reference", - path: "/components/api/#run" - } - ], - }, - { - title: "Python", - type: "group", - collapsable: false, - sidebarDepth: 2, - children: [ - "/code/python/", - "/code/python/auth/", - "/code/python/http-requests/", - "/code/python/working-with-files/", - "/code/python/using-data-stores/", - "/code/python/delay/", - "/code/python/rerun/", - "/code/python/import-mappings/", - "/code/python/faqs/", - ], - }, - "/code/go/", - { - title: "Bash", - type: "group", - children: ["/code/bash/", "/code/bash/http-requests/"], - }, - "/destinations/", - "/environment-variables/", - ], - }, - "/http/", - { - title: "Integrations", - type: "group", - children: [ - "/apps/", - "/apps/contributing/", - { - title: "Components", - type: "group", - collapsable: false, - children: [ - "/components/", - "/components/quickstart/nodejs/actions/", - "/components/quickstart/nodejs/sources/", - "/pipedream-axios/", - "/components/typescript/", - "/components/guidelines/", - ], - }, - ], - }, - ["/troubleshooting/", "Troubleshooting"], - ["/user-settings/", "Settings"], - { - title: "Single-Sign On (SSO)", - children: [ - "/workspaces/sso/", - "/workspaces/sso/okta/", - "/workspaces/sso/google/", - "/workspaces/sso/saml/", - ], - }, -]; - -const referenceNav = [ - { - title: "Components API", - children: ["/components/api/"], - }, - { - title: "CLI", - type: "group", - children: ["/cli/install/", "/cli/login/", "/cli/reference/"], - }, - { - title: "API", - type: "group", - children: [ - "/api/", - "/api/auth/", - "/api/rest/", - "/api/rest/webhooks/", - "/api/rest/rss/", - "/api/sse/", - ], - }, - "/limits/", - { - title: "Security and Privacy", - children: [ - "/privacy-and-security/", - "/privacy-and-security/best-practices/", - "/abuse/", - "/privacy-and-security/pgp-key/", - "/subprocessors/", - ], - }, - "/workflows/settings/#eliminate-cold-starts", -]; - -const pricingNav = ["/pricing/"]; - -module.exports = { - // reference nav - "/components/api/": referenceNav, - "/pipedream-axios/": referenceNav, - "/api/": referenceNav, - "/api/auth/": referenceNav, - "/api/rest/": referenceNav, - "/api/rest/webhooks/": referenceNav, - "/api/rest/rss/": referenceNav, - "/api/rest/workflow-errors/": referenceNav, - "/api/sse/": referenceNav, - "/scheduling-future-tasks/": referenceNav, - "/cli/install/": referenceNav, - "/cli/login/": referenceNav, - "/cli/reference/": referenceNav, - "/limits/": referenceNav, - // security nav - "/privacy-and-security/": referenceNav, - "/privacy-and-reference/pgp-key/": referenceNav, - "/privacy-and-reference/best-practices/": referenceNav, - "/subprocessors/": referenceNav, - "/workflows/settings/#eliminate-cold-starts": referenceNav, - "/abuse/": referenceNav, - // pricing nav - "/pricing/": pricingNav, - // main nav - "/": docsNav, -}; diff --git a/docs/docs/.vuepress/configs/themeConfig.js b/docs/docs/.vuepress/configs/themeConfig.js deleted file mode 100644 index f858191059a0c..0000000000000 --- a/docs/docs/.vuepress/configs/themeConfig.js +++ /dev/null @@ -1,59 +0,0 @@ -const navConfig = require("./navConfig"); -const sidebarConfig = require("./sidebarConfig"); -const envVars = require("./envVars"); - -module.exports = { - algolia: { - appId: "XY28M447C5", - apiKey: "9d9169458128b3d60c22bb04da4431c7", - indexName: "pipedream", - algoliaOptions: { - facetFilters: ["version:latest"], - }, - }, - searchPlaceholder: "Search...", - logo: "https://res.cloudinary.com/pipedreamin/image/upload/t_logo48x48/v1597038956/docs/HzP2Yhq8_400x400_1_sqhs70.jpg", - repo: "PipedreamHQ/pipedream", - nav: navConfig, - - // if your docs are not at the root of the repo: - docsDir: "docs/docs", - editLinks: true, - // custom text for edit link. Defaults to "Edit this page" - editLinkText: "Improve this page", - sidebarDepth: 3, - sidebar: sidebarConfig, - // languages - icons: { - nodejs: { - only_icon: - "https://res.cloudinary.com/pipedreamin/image/upload/v1646761316/docs/icons/icons8-nodejs_aax6wn.svg", - with_title: - "https://res.cloudinary.com/pipedreamin/image/upload/v1646761316/docs/icons/icons8-nodejs_aax6wn.svg", - }, - python: { - only_icon: - "https://res.cloudinary.com/pipedreamin/image/upload/v1646763734/docs/icons/icons8-python_ypgmya.svg", - with_title: - "https://res.cloudinary.com/pipedreamin/image/upload/v1647356607/docs/icons/python-logo-generic_k3o5w2.svg", - }, - go: { - only_icon: - "https://res.cloudinary.com/pipedreamin/image/upload/v1646763751/docs/icons/Go-Logo_Blue_zhkchv.svg", - with_title: - "https://res.cloudinary.com/pipedreamin/image/upload/v1646763751/docs/icons/Go-Logo_Blue_zhkchv.svg", - }, - bash: { - only_icon: - "https://res.cloudinary.com/pipedreamin/image/upload/v1646763756/docs/icons/full_colored_dark_nllzkl.svg", - with_title: - "https://res.cloudinary.com/pipedreamin/image/upload/v1647356698/docs/icons/full_colored_dark_1_-svg_vyfnv7.svg", - }, - native: { - only_icon: "https://res.cloudinary.com/pipedreamin/image/upload/v1569526159/icons/pipedream_x6plab.svg", - with_title: "https://res.cloudinary.com/pipedreamin/image/upload/v1569526159/icons/pipedream_x6plab.svg" - } - }, - // environment variables - ...envVars, -}; diff --git a/docs/docs/.vuepress/enhanceApp.js b/docs/docs/.vuepress/enhanceApp.js deleted file mode 100644 index e20ecef17fc82..0000000000000 --- a/docs/docs/.vuepress/enhanceApp.js +++ /dev/null @@ -1,114 +0,0 @@ -import VueGtm from "vue-gtm"; - -// fork from vue-router@3.0.2 -// src/util/scroll.js -function getElementPosition(el) { - const docEl = document.documentElement - const docRect = docEl.getBoundingClientRect() - const elRect = el.getBoundingClientRect() - return { - x: elRect.left - docRect.left, - y: elRect.top - docRect.top, - } -} - -/** - * Fix broken Vuepress scrolling to internal #links - * - * @param {String} to - * @returns void - */ -function scrollToAnchor(to) { - const targetAnchor = to.hash.slice(1) - const targetElement = document.getElementById(targetAnchor) || document.querySelector(`[name='${targetAnchor}']`) - - if (targetElement) { - return window.scrollTo({ - top: getElementPosition(targetElement).y, - behavior: 'smooth', - }) - } else { - return false - } -} - -export default ({ - Vue, // the version of Vue being used in the VuePress app - options, // the options for the root Vue instance - router, // the router instance for the app - siteData, // site metadata -}) => { - if (typeof window !== "undefined") { - Vue.use(VueGtm, { - id: "GTM-KBDH3DB", - enabled: true, - debug: false, - vueRouter: router, - }); - } - - // Adapted from https://github.com/vuepress/vuepress-community/blob/7feb5c514090b6901cd7d9998f4dd858e0055b7a/packages/vuepress-plugin-smooth-scroll/src/enhanceApp.ts#L21 - // With a bug fix for handling long pages - router.options.scrollBehavior = (to, from, savedPosition) => { - if (typeof window === "undefined") { - return; - } - - if (savedPosition) { - return window.scrollTo({ - top: savedPosition.y, - behavior: 'smooth', - }) - } else if (to.hash) { - if (Vue.$vuepress.$get('disableScrollBehavior')) { - return false - } - const scrollResult = scrollToAnchor(to) - - if (scrollResult) { - return scrollResult - } else { - window.onload = () => { - scrollToAnchor(to) - } - } - } else { - return window.scrollTo({ - top: 0, - behavior: 'smooth', - }) - } - } - - router.addRoutes([ - { path: "/cron", redirect: "/workflows/steps/triggers" }, - { path: "/notebook", redirect: "/workflows/steps" }, - { path: "/workflows/fork", redirect: "/workflows/copy" }, - { path: "/notebook/fork", redirect: "/workflows/copy" }, - { path: "/notebook/inspector/", redirect: "/workflows/events/inspect/" }, - { path: "/notebook/destinations/s3/", redirect: "/destinations/s3/" }, - { path: "/notebook/destinations/sse/", redirect: "/destinations/sse/" }, - { - path: "/notebook/destinations/snowflake/", - redirect: "/destinations/snowflake/", - }, - { path: "/notebook/destinations/http/", redirect: "/destinations/http/" }, - { path: "/notebook/destinations/email/", redirect: "/destinations/email/" }, - { path: "/notebook/destinations/", redirect: "/destinations/" }, - { path: "/notebook/code/", redirect: "/workflows/steps/code/" }, - { - path: "/notebook/observability/", - redirect: "/workflows/events/inspect/", - }, - { path: "/notebook/actions/", redirect: "/workflows/steps/actions/" }, - { path: "/notebook/sources/", redirect: "/workflows/steps/triggers/" }, - { path: "/notebook/sql/", redirect: "/destinations/triggers/" }, - { path: "/what-is-pipedream/", redirect: "/" }, - { - path: "/docs/apps/all-apps", - redirect: "https://pipedream.com/apps", - }, - { path: "/workflows/steps/code/", redirect: '/code/nodejs/'} - - ]); -}; diff --git a/docs/docs/.vuepress/public/favicon.ico b/docs/docs/.vuepress/public/favicon.ico deleted file mode 100644 index b854b21bb03af..0000000000000 Binary files a/docs/docs/.vuepress/public/favicon.ico and /dev/null differ diff --git a/docs/docs/.vuepress/public/pipedream.svg b/docs/docs/.vuepress/public/pipedream.svg deleted file mode 100644 index ca61e0c2d013e..0000000000000 --- a/docs/docs/.vuepress/public/pipedream.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/docs/docs/.vuepress/styles/index.styl b/docs/docs/.vuepress/styles/index.styl deleted file mode 100644 index a06ac76a0b8bc..0000000000000 --- a/docs/docs/.vuepress/styles/index.styl +++ /dev/null @@ -1,98 +0,0 @@ -@require '~vuepress-plugin-tabs/dist/themes/default.styl' - -body - font-family system-ui,BlinkMacSystemFont,-apple-system, sans-serif, Helvetica, Roboto, "Helvetica Neue", Arial, sans-serif - -$hoverColor = #34d28b -$borderColor = #fff -$bgColor = #fff -$sidebarColor = #000 -$headerColor = #343c56 -$textColor = rgba(0,0,0,.84) -$primaryColor = green; - -html, -body, -.navbar, -.sidebar, -.navbar .links, -.theme-container .navbar, -.theme-container .sidebar - background-color $bgColor - -body .content .default - color $textColor - -.nav-item - color: $headerColor - - @media (max-width: 719px) - color $headerColor - -.nav-item a:hover - color: $hoverColor - - @media (max-width: 719px) - color $hoverColor - -div .sidebar-button svg - color: $sidebarColor - -.navbar .site-name - color $sidebarColor - margin-left: -25px - margin-top: 1px - -p, ul:not(ul ul, ul ol), ol:not(ol ul, ol ol) { - display: block - margin-block-start: 1em - margin-block-end: 1em - margin-inline-start: 0px - margin-inline-end: 0px -} - -*:not(li)>ul { - margin-top: 1em; -} - -/** - * Copy code to clipboard style fixes - */ -.code-copy { - width: 0; - height: 0; - overflow-y: hidden; -} - - -/** - * Fix margin bottom spacing on tips with li's - */ -.custom-block li:last-of-type { - margin-bottom: 1rem; -} - - - -.pd-copy-workflow { - background-color: #35d28b; - size: 1em; - padding: 0.5em 1em 0.5em 1em; - color: #fff; - - display: inline-flex; - align-items: center; - font-weight: 800; - text-decoration: none !important; - - img { - display: inline-block; - } - - border-bottom: none !important; -} - -.pd-copy-workflow:hover { - text-decoration: none !important; - color: #fff !important; -} diff --git a/docs/docs/.vuepress/theme/LICENSE b/docs/docs/.vuepress/theme/LICENSE deleted file mode 100644 index 15f1f7e7a490f..0000000000000 --- a/docs/docs/.vuepress/theme/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2018-present, Yuxi (Evan) You - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/docs/docs/.vuepress/theme/components/AlgoliaSearchBox.vue b/docs/docs/.vuepress/theme/components/AlgoliaSearchBox.vue deleted file mode 100644 index 7b2a5807cfbe8..0000000000000 --- a/docs/docs/.vuepress/theme/components/AlgoliaSearchBox.vue +++ /dev/null @@ -1,171 +0,0 @@ - - - - - diff --git a/docs/docs/.vuepress/theme/components/DropdownLink.vue b/docs/docs/.vuepress/theme/components/DropdownLink.vue deleted file mode 100644 index 4b659a1ffbf1e..0000000000000 --- a/docs/docs/.vuepress/theme/components/DropdownLink.vue +++ /dev/null @@ -1,266 +0,0 @@ - - - - - diff --git a/docs/docs/.vuepress/theme/components/DropdownTransition.vue b/docs/docs/.vuepress/theme/components/DropdownTransition.vue deleted file mode 100644 index eeaf12b5c3e41..0000000000000 --- a/docs/docs/.vuepress/theme/components/DropdownTransition.vue +++ /dev/null @@ -1,33 +0,0 @@ - - - - - diff --git a/docs/docs/.vuepress/theme/components/Home.vue b/docs/docs/.vuepress/theme/components/Home.vue deleted file mode 100644 index 8cc0519bc96a4..0000000000000 --- a/docs/docs/.vuepress/theme/components/Home.vue +++ /dev/null @@ -1,175 +0,0 @@ - - - - - diff --git a/docs/docs/.vuepress/theme/components/NavLink.vue b/docs/docs/.vuepress/theme/components/NavLink.vue deleted file mode 100644 index b2c1ae2be4f71..0000000000000 --- a/docs/docs/.vuepress/theme/components/NavLink.vue +++ /dev/null @@ -1,100 +0,0 @@ - - - diff --git a/docs/docs/.vuepress/theme/components/NavLinks.vue b/docs/docs/.vuepress/theme/components/NavLinks.vue deleted file mode 100644 index bdf1fd3a5d94b..0000000000000 --- a/docs/docs/.vuepress/theme/components/NavLinks.vue +++ /dev/null @@ -1,188 +0,0 @@ - - - - - diff --git a/docs/docs/.vuepress/theme/components/Navbar.vue b/docs/docs/.vuepress/theme/components/Navbar.vue deleted file mode 100644 index d099067459bfc..0000000000000 --- a/docs/docs/.vuepress/theme/components/Navbar.vue +++ /dev/null @@ -1,184 +0,0 @@ - - - - - diff --git a/docs/docs/.vuepress/theme/components/NavbarGrid.vue b/docs/docs/.vuepress/theme/components/NavbarGrid.vue deleted file mode 100644 index c4d34d4970986..0000000000000 --- a/docs/docs/.vuepress/theme/components/NavbarGrid.vue +++ /dev/null @@ -1,112 +0,0 @@ - - - - diff --git a/docs/docs/.vuepress/theme/components/Page.vue b/docs/docs/.vuepress/theme/components/Page.vue deleted file mode 100644 index ee548f50e8094..0000000000000 --- a/docs/docs/.vuepress/theme/components/Page.vue +++ /dev/null @@ -1,37 +0,0 @@ - - - - - diff --git a/docs/docs/.vuepress/theme/components/PageEdit.vue b/docs/docs/.vuepress/theme/components/PageEdit.vue deleted file mode 100644 index de5c39ae650ec..0000000000000 --- a/docs/docs/.vuepress/theme/components/PageEdit.vue +++ /dev/null @@ -1,166 +0,0 @@ - - - - - diff --git a/docs/docs/.vuepress/theme/components/PageNav.vue b/docs/docs/.vuepress/theme/components/PageNav.vue deleted file mode 100644 index 4c19aae5f88bd..0000000000000 --- a/docs/docs/.vuepress/theme/components/PageNav.vue +++ /dev/null @@ -1,163 +0,0 @@ - - - - - diff --git a/docs/docs/.vuepress/theme/components/Sidebar.vue b/docs/docs/.vuepress/theme/components/Sidebar.vue deleted file mode 100644 index 93aaff28bd4fa..0000000000000 --- a/docs/docs/.vuepress/theme/components/Sidebar.vue +++ /dev/null @@ -1,89 +0,0 @@ - - - - - diff --git a/docs/docs/.vuepress/theme/components/SidebarButton.vue b/docs/docs/.vuepress/theme/components/SidebarButton.vue deleted file mode 100644 index 3f54afd5590e0..0000000000000 --- a/docs/docs/.vuepress/theme/components/SidebarButton.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - diff --git a/docs/docs/.vuepress/theme/components/SidebarGroup.vue b/docs/docs/.vuepress/theme/components/SidebarGroup.vue deleted file mode 100644 index d7f192946aed9..0000000000000 --- a/docs/docs/.vuepress/theme/components/SidebarGroup.vue +++ /dev/null @@ -1,141 +0,0 @@ - - - - - diff --git a/docs/docs/.vuepress/theme/components/SidebarLink.vue b/docs/docs/.vuepress/theme/components/SidebarLink.vue deleted file mode 100644 index ec1be898cdb11..0000000000000 --- a/docs/docs/.vuepress/theme/components/SidebarLink.vue +++ /dev/null @@ -1,158 +0,0 @@ - - - diff --git a/docs/docs/.vuepress/theme/components/SidebarLinks.vue b/docs/docs/.vuepress/theme/components/SidebarLinks.vue deleted file mode 100644 index 1096358b001e0..0000000000000 --- a/docs/docs/.vuepress/theme/components/SidebarLinks.vue +++ /dev/null @@ -1,90 +0,0 @@ - - - diff --git a/docs/docs/.vuepress/theme/components/svgs/code-icon.vue b/docs/docs/.vuepress/theme/components/svgs/code-icon.vue deleted file mode 100644 index bf29a02c477f6..0000000000000 --- a/docs/docs/.vuepress/theme/components/svgs/code-icon.vue +++ /dev/null @@ -1,14 +0,0 @@ - diff --git a/docs/docs/.vuepress/theme/components/svgs/component-icon.vue b/docs/docs/.vuepress/theme/components/svgs/component-icon.vue deleted file mode 100644 index 74728ed7d7c6c..0000000000000 --- a/docs/docs/.vuepress/theme/components/svgs/component-icon.vue +++ /dev/null @@ -1,19 +0,0 @@ - - diff --git a/docs/docs/.vuepress/theme/components/svgs/integration-icon.vue b/docs/docs/.vuepress/theme/components/svgs/integration-icon.vue deleted file mode 100644 index 003249d2bcca7..0000000000000 --- a/docs/docs/.vuepress/theme/components/svgs/integration-icon.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/docs/docs/.vuepress/theme/components/svgs/step-icon.vue b/docs/docs/.vuepress/theme/components/svgs/step-icon.vue deleted file mode 100644 index 6f5aaddcf64ad..0000000000000 --- a/docs/docs/.vuepress/theme/components/svgs/step-icon.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/docs/docs/.vuepress/theme/components/svgs/trigger-icon.vue b/docs/docs/.vuepress/theme/components/svgs/trigger-icon.vue deleted file mode 100644 index bcc707b7a74fe..0000000000000 --- a/docs/docs/.vuepress/theme/components/svgs/trigger-icon.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/docs/docs/.vuepress/theme/components/svgs/workflow-icon.vue b/docs/docs/.vuepress/theme/components/svgs/workflow-icon.vue deleted file mode 100644 index 8bd7a425e213e..0000000000000 --- a/docs/docs/.vuepress/theme/components/svgs/workflow-icon.vue +++ /dev/null @@ -1,13 +0,0 @@ - diff --git a/docs/docs/.vuepress/theme/global-components/Badge.vue b/docs/docs/.vuepress/theme/global-components/Badge.vue deleted file mode 100644 index 53951f9d505df..0000000000000 --- a/docs/docs/.vuepress/theme/global-components/Badge.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - diff --git a/docs/docs/.vuepress/theme/index.js b/docs/docs/.vuepress/theme/index.js deleted file mode 100644 index baaf102ba7767..0000000000000 --- a/docs/docs/.vuepress/theme/index.js +++ /dev/null @@ -1,59 +0,0 @@ -const path = require('path') - -// Theme API. -module.exports = (options, ctx) => { - const { themeConfig, siteConfig } = ctx - - // resolve algolia - const isAlgoliaSearch = ( - themeConfig.algolia - || Object - .keys(siteConfig.locales && themeConfig.locales || {}) - .some(base => themeConfig.locales[base].algolia) - ) - - const enableSmoothScroll = themeConfig.smoothScroll === true - - return { - alias () { - return { - '@AlgoliaSearchBox': isAlgoliaSearch - ? path.resolve(__dirname, 'components/AlgoliaSearchBox.vue') - : path.resolve(__dirname, 'noopModule.js') - } - }, - - plugins: [ - ['@vuepress/active-header-links', options.activeHeaderLinks], - '@vuepress/search', - '@vuepress/plugin-nprogress', - ['container', { - type: 'tip', - defaultTitle: { - '/': 'TIP', - '/zh/': '提示' - } - }], - ['container', { - type: 'warning', - defaultTitle: { - '/': 'WARNING', - '/zh/': '注意' - } - }], - ['container', { - type: 'danger', - defaultTitle: { - '/': 'WARNING', - '/zh/': '警告' - } - }], - ['container', { - type: 'details', - before: info => `
${info ? `${info}` : ''}\n`, - after: () => '
\n' - }], - ['smooth-scroll', enableSmoothScroll] - ] - } -} diff --git a/docs/docs/.vuepress/theme/layouts/404.vue b/docs/docs/.vuepress/theme/layouts/404.vue deleted file mode 100644 index 2cbfa0f179572..0000000000000 --- a/docs/docs/.vuepress/theme/layouts/404.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - diff --git a/docs/docs/.vuepress/theme/layouts/Layout.vue b/docs/docs/.vuepress/theme/layouts/Layout.vue deleted file mode 100644 index 3298070997d0c..0000000000000 --- a/docs/docs/.vuepress/theme/layouts/Layout.vue +++ /dev/null @@ -1,151 +0,0 @@ - - - diff --git a/docs/docs/.vuepress/theme/noopModule.js b/docs/docs/.vuepress/theme/noopModule.js deleted file mode 100644 index b1c6ea436a540..0000000000000 --- a/docs/docs/.vuepress/theme/noopModule.js +++ /dev/null @@ -1 +0,0 @@ -export default {} diff --git a/docs/docs/.vuepress/theme/styles/arrow.styl b/docs/docs/.vuepress/theme/styles/arrow.styl deleted file mode 100644 index 20bffc0dc8733..0000000000000 --- a/docs/docs/.vuepress/theme/styles/arrow.styl +++ /dev/null @@ -1,22 +0,0 @@ -@require './config' - -.arrow - display inline-block - width 0 - height 0 - &.up - border-left 4px solid transparent - border-right 4px solid transparent - border-bottom 6px solid $arrowBgColor - &.down - border-left 4px solid transparent - border-right 4px solid transparent - border-top 6px solid $arrowBgColor - &.right - border-top 4px solid transparent - border-bottom 4px solid transparent - border-left 6px solid $arrowBgColor - &.left - border-top 4px solid transparent - border-bottom 4px solid transparent - border-right 6px solid $arrowBgColor diff --git a/docs/docs/.vuepress/theme/styles/code.styl b/docs/docs/.vuepress/theme/styles/code.styl deleted file mode 100644 index 9d3aa9a54130c..0000000000000 --- a/docs/docs/.vuepress/theme/styles/code.styl +++ /dev/null @@ -1,137 +0,0 @@ -{$contentClass} - code - color lighten($textColor, 20%) - padding 0.25rem 0.5rem - margin 0 - font-size 0.85em - background-color rgba(27,31,35,0.05) - border-radius 3px - .token - &.deleted - color #EC5975 - &.inserted - color $accentColor - -{$contentClass} - pre, pre[class*="language-"] - line-height 1.4 - padding 1.25rem 1.5rem - margin 0.85rem 0 - background-color $codeBgColor - border-radius 6px - overflow auto - code - color #fff - padding 0 - background-color transparent - border-radius 0 - -div[class*="language-"] - position relative - background-color $codeBgColor - border-radius 6px - .highlight-lines - user-select none - padding-top 1.3rem - position absolute - top 0 - left 0 - width 100% - line-height 1.4 - .highlighted - background-color rgba(0, 0, 0, 66%) - pre, pre[class*="language-"] - background transparent - position relative - z-index 1 - &::before - position absolute - z-index 3 - top 0.8em - right 1em - font-size 0.75rem - color rgba(255, 255, 255, 0.4) - &:not(.line-numbers-mode) - .line-numbers-wrapper - display none - &.line-numbers-mode - .highlight-lines .highlighted - position relative - &:before - content ' ' - position absolute - z-index 3 - left 0 - top 0 - display block - width $lineNumbersWrapperWidth - height 100% - background-color rgba(0, 0, 0, 66%) - pre - padding-left $lineNumbersWrapperWidth + 1 rem - vertical-align middle - .line-numbers-wrapper - position absolute - top 0 - width $lineNumbersWrapperWidth - text-align center - color rgba(255, 255, 255, 0.3) - padding 1.25rem 0 - line-height 1.4 - br - user-select none - .line-number - position relative - z-index 4 - user-select none - font-size 0.85em - &::after - content '' - position absolute - z-index 2 - top 0 - left 0 - width $lineNumbersWrapperWidth - height 100% - border-radius 6px 0 0 6px - border-right 1px solid rgba(0, 0, 0, 66%) - background-color $codeBgColor - - -for lang in $codeLang - div{'[class~="language-' + lang + '"]'} - &:before - content ('' + lang) - -div[class~="language-javascript"] - &:before - content "js" - -div[class~="language-typescript"] - &:before - content "ts" - -div[class~="language-markup"] - &:before - content "html" - -div[class~="language-markdown"] - &:before - content "md" - -div[class~="language-json"]:before - content "json" - -div[class~="language-ruby"]:before - content "rb" - -div[class~="language-python"]:before - content "py" - -div[class~="language-bash"]:before - content "sh" - -div[class~="language-php"]:before - content "php" - -@import '~prismjs/themes/prism-tomorrow.css' diff --git a/docs/docs/.vuepress/theme/styles/config.styl b/docs/docs/.vuepress/theme/styles/config.styl deleted file mode 100644 index 9e403210fd385..0000000000000 --- a/docs/docs/.vuepress/theme/styles/config.styl +++ /dev/null @@ -1 +0,0 @@ -$contentClass = '.theme-default-content' diff --git a/docs/docs/.vuepress/theme/styles/custom-blocks.styl b/docs/docs/.vuepress/theme/styles/custom-blocks.styl deleted file mode 100644 index 5b868166a434c..0000000000000 --- a/docs/docs/.vuepress/theme/styles/custom-blocks.styl +++ /dev/null @@ -1,44 +0,0 @@ -.custom-block - .custom-block-title - font-weight 600 - margin-bottom -0.4rem - &.tip, &.warning, &.danger - padding .1rem 1.5rem - border-left-width .5rem - border-left-style solid - margin 1rem 0 - &.tip - background-color #f3f5f7 - border-color #42b983 - &.warning - background-color rgba(255,229,100,.3) - border-color darken(#ffe564, 35%) - color darken(#ffe564, 70%) - .custom-block-title - color darken(#ffe564, 50%) - a - color $textColor - &.danger - background-color #ffe6e6 - border-color darken(red, 20%) - color darken(red, 70%) - .custom-block-title - color darken(red, 40%) - a - color $textColor - &.details - display block - position relative - border-radius 2px - margin 1.6em 0 - padding 1.6em - background-color #eee - h4 - margin-top 0 - figure, p - &:last-child - margin-bottom 0 - padding-bottom 0 - summary - outline none - cursor pointer diff --git a/docs/docs/.vuepress/theme/styles/index.styl b/docs/docs/.vuepress/theme/styles/index.styl deleted file mode 100644 index 7ad6e8f6e44c3..0000000000000 --- a/docs/docs/.vuepress/theme/styles/index.styl +++ /dev/null @@ -1,214 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - ul, ol { - list-style: revert; - } -} - -@require './config' -@require './code' -@require './custom-blocks' -@require './arrow' -@require './wrapper' -@require './toc' -@require './navbar' - -html, body - padding 0 - margin 0 - background-color #fff - -body - font-family -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif - -webkit-font-smoothing antialiased - -moz-osx-font-smoothing grayscale - font-size 16px - color $textColor - -.page - padding-left $sidebarWidth - -.navbar - display flex - justify-content space-between - position fixed - z-index 20 - top 0 - left 0 - right 0 - height $navbarHeight - background-color #fff - box-sizing border-box - border-bottom 1px solid $borderColor - -.sidebar-mask - position fixed - z-index 9 - top 0 - left 0 - width 100vw - height 100vh - display none - -.sidebar - font-size 16px - background-color #fff - width $sidebarWidth - position fixed - z-index 10 - margin 0 - top $navbarHeight - left 0 - bottom 0 - box-sizing border-box - border-right 1px solid $borderColor - overflow-y auto - -{$contentClass}:not(.custom) - @extend $wrapper - > *:first-child - margin-top $navbarHeight - - a:hover - text-decoration underline - - p.demo - padding 1rem 1.5rem - border 1px solid #ddd - border-radius 4px - - img - max-width 100% - -{$contentClass}.custom - padding 0 - margin 0 - - img - max-width 100% - -a - font-weight 500 - color $accentColor - text-decoration none - -p a code - font-weight 400 - color $accentColor - -kbd - background #eee - border solid 0.15rem #ddd - border-bottom solid 0.25rem #ddd - border-radius 0.15rem - padding 0 0.15em - -blockquote - font-size 1rem - color #999; - border-left .2rem solid #dfe2e5 - margin 1rem 0 - padding .25rem 0 .25rem 1rem - - & > p - margin 0 - -ul, ol - padding-left 1.2em - -strong - font-weight 600 - -h1, h2, h3, h4, h5, h6 - font-weight 600 - line-height 1.25 - - {$contentClass}:not(.custom) > & - margin-top (0.5rem - $navbarHeight) - padding-top ($navbarHeight + 1rem) - margin-bottom 0 - - &:first-child - margin-top -1.5rem - margin-bottom 1rem - - + p, + pre, + .custom-block - margin-top 2rem - - &:hover .header-anchor - opacity: 1 - -h1 - font-size 2.2rem - -h2 - font-size 1.65rem - padding-bottom .3rem - border-bottom 1px solid $borderColor - -h3 - font-size 1.35rem - -a.header-anchor - font-size 0.85em - float left - margin-left -0.87em - padding-right 0.23em - margin-top 0.125em - opacity 0 - - &:hover - text-decoration none - -code, kbd, .line-number - font-family source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace - -p, ul, ol - line-height 1.7 - -hr - border 0 - border-top 1px solid $borderColor - -table - border-collapse collapse - margin 1rem 0 - display: block - overflow-x: auto - -tr - border-top 1px solid #dfe2e5 - - &:nth-child(2n) - background-color #f6f8fa - -th, td - border 1px solid #dfe2e5 - padding .6em 1em - -.theme-container - &.sidebar-open - .sidebar-mask - display: block - - &.no-navbar - {$contentClass}:not(.custom) > h1, h2, h3, h4, h5, h6 - margin-top 1.5rem - padding-top 0 - - .sidebar - top 0 - - -@media (min-width: ($MQMobile + 1px)) - .theme-container.no-sidebar - .sidebar - display none - - .page - padding-left 0 - -@require 'mobile.styl' diff --git a/docs/docs/.vuepress/theme/styles/mobile.styl b/docs/docs/.vuepress/theme/styles/mobile.styl deleted file mode 100644 index f5bd32739d774..0000000000000 --- a/docs/docs/.vuepress/theme/styles/mobile.styl +++ /dev/null @@ -1,37 +0,0 @@ -@require './config' - -$mobileSidebarWidth = $sidebarWidth * 0.82 - -// narrow desktop / iPad -@media (max-width: $MQNarrow) - .sidebar - font-size 15px - width $mobileSidebarWidth - .page - padding-left $mobileSidebarWidth - -// wide mobile -@media (max-width: $MQMobile) - .sidebar - top 0 - padding-top $navbarHeight - transform translateX(-100%) - transition transform .2s ease - .page - padding-left 0 - .theme-container - &.sidebar-open - .sidebar - transform translateX(0) - &.no-navbar - .sidebar - padding-top: 0 - -// narrow mobile -@media (max-width: $MQMobileNarrow) - h1 - font-size 1.9rem - {$contentClass} - div[class*="language-"] - margin 0.85rem -1.5rem - border-radius 0 diff --git a/docs/docs/.vuepress/theme/styles/navbar.styl b/docs/docs/.vuepress/theme/styles/navbar.styl deleted file mode 100644 index 597ecd80c0f26..0000000000000 --- a/docs/docs/.vuepress/theme/styles/navbar.styl +++ /dev/null @@ -1,34 +0,0 @@ -.nav-link - .badge - font-size: 0.8em - font-weight: 800 - text-transform: uppercase - - // generated from https://www.cssportal.com/css-text-gradient-generator/ - .primary - background: #3EAF7C; - background: -webkit-linear-gradient(to right, #3EAF7C 25%, #8F33B5 100%); - background: -moz-linear-gradient(to right, #3EAF7C 25%, #8F33B5 100%); - background: linear-gradient(to right, #3EAF7C 25%, #8F33B5 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - -.nav-link.primary - color: #3EAF7C; - // color: #fff - // padding: 0.6em; - // border-radius: 0.375em; - -.nav-link.primary:hover - // text-decoration: none; - // border-bottom: none !important; - // margin-bottom: auto !important; - // color: #fff !important; - -.nav-link.primary:visited - // text-decoration: none; - // border-bottom: none !important; - // margin-bottom: auto !important; - // color: #fff !important; - - diff --git a/docs/docs/.vuepress/theme/styles/toc.styl b/docs/docs/.vuepress/theme/styles/toc.styl deleted file mode 100644 index d3e71069ba79f..0000000000000 --- a/docs/docs/.vuepress/theme/styles/toc.styl +++ /dev/null @@ -1,3 +0,0 @@ -.table-of-contents - .badge - vertical-align middle diff --git a/docs/docs/.vuepress/theme/styles/wrapper.styl b/docs/docs/.vuepress/theme/styles/wrapper.styl deleted file mode 100644 index a99262c71ab37..0000000000000 --- a/docs/docs/.vuepress/theme/styles/wrapper.styl +++ /dev/null @@ -1,9 +0,0 @@ -$wrapper - max-width $contentWidth - margin 0 auto - padding 2rem 2.5rem - @media (max-width: $MQNarrow) - padding 2rem - @media (max-width: $MQMobileNarrow) - padding 1.5rem - diff --git a/docs/docs/.vuepress/theme/util/index.js b/docs/docs/.vuepress/theme/util/index.js deleted file mode 100644 index 92fcd3b311af4..0000000000000 --- a/docs/docs/.vuepress/theme/util/index.js +++ /dev/null @@ -1,244 +0,0 @@ -export const hashRE = /#.*$/ -export const extRE = /\.(md|html)$/ -export const endingSlashRE = /\/$/ -export const outboundRE = /^[a-z]+:/i - -export function normalize (path) { - return decodeURI(path) - .replace(hashRE, '') - .replace(extRE, '') -} - -export function getHash (path) { - const match = path.match(hashRE) - if (match) { - return match[0] - } -} - -export function isExternal (path) { - return outboundRE.test(path) -} - -export function isMailto (path) { - return /^mailto:/.test(path) -} - -export function isTel (path) { - return /^tel:/.test(path) -} - -export function ensureExt (path) { - if (isExternal(path)) { - return path - } - const hashMatch = path.match(hashRE) - const hash = hashMatch ? hashMatch[0] : '' - const normalized = normalize(path) - - if (endingSlashRE.test(normalized)) { - return path - } - return normalized + '.html' + hash -} - -export function isActive (route, path) { - const routeHash = decodeURIComponent(route.hash) - const linkHash = getHash(path) - if (linkHash && routeHash !== linkHash) { - return false - } - const routePath = normalize(route.path) - const pagePath = normalize(path) - return routePath === pagePath -} - -export function resolvePage (pages, rawPath, base) { - if (isExternal(rawPath)) { - return { - type: 'external', - path: rawPath - } - } - if (base) { - rawPath = resolvePath(rawPath, base) - } - const path = normalize(rawPath) - for (let i = 0; i < pages.length; i++) { - if (normalize(pages[i].regularPath) === path) { - return Object.assign({}, pages[i], { - type: 'page', - path: ensureExt(pages[i].path) - }) - } - } - console.error(`[vuepress] No matching page found for sidebar item "${rawPath}"`) - return {} -} - -function resolvePath (relative, base, append) { - const firstChar = relative.charAt(0) - if (firstChar === '/') { - return relative - } - - if (firstChar === '?' || firstChar === '#') { - return base + relative - } - - const stack = base.split('/') - - // remove trailing segment if: - // - not appending - // - appending to trailing slash (last segment is empty) - if (!append || !stack[stack.length - 1]) { - stack.pop() - } - - // resolve relative path - const segments = relative.replace(/^\//, '').split('/') - for (let i = 0; i < segments.length; i++) { - const segment = segments[i] - if (segment === '..') { - stack.pop() - } else if (segment !== '.') { - stack.push(segment) - } - } - - // ensure leading slash - if (stack[0] !== '') { - stack.unshift('') - } - - return stack.join('/') -} - -/** - * @param { Page } page - * @param { string } regularPath - * @param { SiteData } site - * @param { string } localePath - * @returns { SidebarGroup } - */ -export function resolveSidebarItems (page, regularPath, site, localePath) { - const { pages, themeConfig } = site - - const localeConfig = localePath && themeConfig.locales - ? themeConfig.locales[localePath] || themeConfig - : themeConfig - - const pageSidebarConfig = page.frontmatter.sidebar || localeConfig.sidebar || themeConfig.sidebar - if (pageSidebarConfig === 'auto') { - return resolveHeaders(page) - } - - const sidebarConfig = localeConfig.sidebar || themeConfig.sidebar - if (!sidebarConfig) { - return [] - } else { - const { base, config } = resolveMatchingConfig(regularPath, sidebarConfig) - if (config === 'auto') { - return resolveHeaders(page) - } - return config - ? config.map(item => resolveItem(item, pages, base)) - : [] - } -} - -/** - * @param { Page } page - * @returns { SidebarGroup } - */ -function resolveHeaders (page) { - const headers = groupHeaders(page.headers || []) - return [{ - type: 'group', - collapsable: false, - title: page.title, - path: null, - children: headers.map(h => ({ - type: 'auto', - title: h.title, - basePath: page.path, - path: page.path + '#' + h.slug, - children: h.children || [] - })) - }] -} - -export function groupHeaders (headers) { - // group h3s under h2 - headers = headers.map(h => Object.assign({}, h)) - let lastH2 - headers.forEach(h => { - if (h.level === 2) { - lastH2 = h - } else if (lastH2) { - (lastH2.children || (lastH2.children = [])).push(h) - } - }) - return headers.filter(h => h.level === 2) -} - -export function resolveNavLinkItem (linkItem) { - return Object.assign(linkItem, { - type: linkItem.items && linkItem.items.length ? 'links' : 'link' - }) -} - -/** - * @param { Route } route - * @param { Array | Array | [link: string]: SidebarConfig } config - * @returns { base: string, config: SidebarConfig } - */ -export function resolveMatchingConfig (regularPath, config) { - if (Array.isArray(config)) { - return { - base: '/', - config: config - } - } - for (const base in config) { - if (ensureEndingSlash(regularPath).indexOf(encodeURI(base)) === 0) { - return { - base, - config: config[base] - } - } - } - return {} -} - -function ensureEndingSlash (path) { - return /(\.html|\/)$/.test(path) - ? path - : path + '/' -} - -function resolveItem (item, pages, base, groupDepth = 1) { - if (typeof item === 'string') { - return resolvePage(pages, item, base) - } else if (Array.isArray(item)) { - return Object.assign(resolvePage(pages, item[0], base), { - title: item[1] - }) - } else { - const children = item.children || [] - if (children.length === 0 && item.path) { - return Object.assign(resolvePage(pages, item.path, base), { - title: item.title - }) - } - return { - type: 'group', - path: item.path, - title: item.title, - sidebarDepth: item.sidebarDepth, - initialOpenGroupIndex: item.initialOpenGroupIndex, - children: children.map(child => resolveItem(child, pages, base, groupDepth + 1)), - collapsable: item.collapsable !== false - } - } -} diff --git a/docs/docs/README.md b/docs/docs/README.md deleted file mode 100644 index f74adc328c81b..0000000000000 --- a/docs/docs/README.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -prev: false -next: false ---- - -# Introduction to Pipedream - -Pipedream is the fastest way to automate any process that connects APIs. Build and run workflows with code-level control when you need it, and no code when you don't. - -The Pipedream platform includes: - -- A [serverless runtime](/code/) and [workflow service](/workflows/) -- Source-available [triggers](/workflows/steps/triggers/) and [actions](/workflows/steps/actions/) for [hundreds of integrated apps](https://pipedream.com/explore/) -- One-click [OAuth and key-based authentication](/connected-accounts/) for more than 1000 APIs (use tokens directly in code or with pre-built actions) - -Watch a demo or review our [quickstart guide](/quickstart/): - - - -## Getting Started - -To get started, [sign up for a free account](https://pipedream.com/auth/signup) (no credit card required) and follow our [quickstart guide](/quickstart/) to create your first workflow. - -![build, test,deploy](https://res.cloudinary.com/pipedreamin/image/upload/v1672810771/mjckfcgsoxs4vccutdbj.png) - -Once you understand the basics of workflow development, learn how to get more out of Pipedream: - -- [Use code in workflows](/code/) -- [Develop custom actions](/components/quickstart/nodejs/actions/) -- [Develop custom triggers](/components/quickstart/nodejs/sources/) - -## Use Cases - -Pipedream supports use cases from prototype to production and is trusted by 200k+ developers from startups to Fortune 500 companies: - -![logos](https://res.cloudinary.com/pipedreamin/image/upload/v1612919944/homepage/logos_kcbviz.png) - -The platform processes billions of events and is built and [priced](https://pipedream.com/pricing/) for use at scale. [Our team](https://pipedream.com/about) has built internet scale applications and managed data pipelines in excess of 10 million events per second (EPS) at startups and high-growth environments like BrightRoll, Yahoo!, Affirm and Instacart. - -Our [community](https://pipedream.com/community) uses Pipedream for a wide variety of use cases including: - -- Connecting SaaS apps -- General API orchestration and automation -- Database automations ([reach out](https://pipedream.com/community) to learn about connecting to resources behind a firewall) -- Custom notifications and alerting -- Mobile and JAMstack backends -- Rate limiting, request smoothing -- Event queueing and concurrency management -- Webhook inspection and routing -- Prototyping and demos - -## Source-available - -Pipedream maintains a [source-available component registry](https://github.com/pipedreamhq/pipedream/) on GitHub so you can avoid writing boilerplate code for common API integrations. Use components as no code building blocks in workflows, or use them to scaffold code that you can customize. You can also [create a PR to contribute new components](/apps/contributing/#contribution-process) via GitHub. - -## Contributing - -We hope is that by providing a generous free tier, you will not only get value from Pipedream, but you will give back to help us improve the product for the entire community and grow the platform by: - -- [Contributing components](/apps/contributing/) to the [Pipedream registry](https://github.com/pipedreamhq/pipedream) or sharing via your own GitHub repo -- Asking and answering questions in our [public community](https://pipedream.com/community/) -- [Reporting bugs](https://pipedream.com/community/c/bugs/9) and [requesting features](https://github.com/PipedreamHQ/pipedream/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=%5BFEATURE%5D+) that help us build a better product -- Following us on [Twitter](https://twitter.com/pipedream), starring our [GitHub repo](https://github.com/PipedreamHQ/pipedream) and subscribing to our [YouTube channel](https://www.youtube.com/c/pipedreamhq) -- Recommending us to your friends and colleagues - -Learn about [all the ways you can contribute](https://pipedream.com/contributing). - -## Support & Community - -If you have any questions or feedback, please [reach out in our community forum](https://pipedream.com/community). - -## Service Status - -Pipedream operates a status page at [https://status.pipedream.com](https://status.pipedream.com). That page displays the uptime history and current status of every Pipedream service. - -When incidents occur, updates are published to the **#incidents** channel of [Pipedream's Slack Community](https://pipedream.com/support) and to the [@PipedreamStatus](https://twitter.com/PipedreamStatus) account on Twitter. On the status page itself, you can also subscribe to updates directly. diff --git a/docs/docs/abuse/README.md b/docs/docs/abuse/README.md deleted file mode 100644 index decaf96031cb8..0000000000000 --- a/docs/docs/abuse/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Reporting abuse - -If you suspect Pipedream resources are being used for illegal purposes, or otherwise violate [the Pipedream Terms](https://pipedream.com/terms), please reach out to abuse@pipedream.com. - -In your abuse report, please provide as many details as possible, including: - -- The specific issue you're seeing, and what date / time it started / you observed it. -- Relevant Pipedream resources involved in the abuse, for example: the HTTP endpoint to which traffic is being sent. -- Any logs / code involved in the abuse. For example, if you're encountering a Denial-of-service attack, please include any HTTP / networking logs related to the issue. If you're reporting malware that exfiltrates data to Pipedream, please include relevant code from that malware or links to relevant reports. \ No newline at end of file diff --git a/docs/docs/airtable/oauth-migration-2024-02/README.md b/docs/docs/airtable/oauth-migration-2024-02/README.md deleted file mode 100644 index e8657ebf14970..0000000000000 --- a/docs/docs/airtable/oauth-migration-2024-02/README.md +++ /dev/null @@ -1,105 +0,0 @@ -# Update to the Airtable Integration on Pipedream (January 2024) - -Effective February 1st 2024, Airtable's API Key authentication method will be deprecated. To learn more about this change, please visit Airtable’s [dedicated support page](https://support.airtable.com/docs/airtable-api-key-deprecation-notice). - -### How will this impact my workflows? - -Starting February 1st 2024, all Pipedream steps using the legacy Airtable (API Key) integration including triggers and actions will no longer be able to authenticate with Airtable. - -### What do I need to do? -
- -1. **Reconnect your Airtable account**: - -- Visit the [accounts page in Pipedream](https://pipedream.com/accounts) -- Search for Airtable and connect your account -- This newer Pipedream integration uses OAuth instead of an API Key - -![Airtable Account Connection](https://res.cloudinary.com/dpenc2lit/image/upload/v1704392183/Screenshot_2024-01-04_at_10.16.12_AM_le364k.png) - -You can determine which workflows are connected to the legacy Airtable (API Key) app by expanding the account row on your Airtable (API Key) account connection. -![Airtable Accounts](https://res.cloudinary.com/dpenc2lit/image/upload/v1704347928/Screenshot_2024-01-03_at_9.58.43_PM_haaqlb.png) - -2. **Update Your Workflows**: - -- After reconnecting to Airtable via OAuth, you'll need to update your existing workflows that use the legacy Airtable app. -- Remove any legacy Airtable sources and re-add the source using the new Airtable app -- Remove any legacy Airtable actions and re-add them using the new Airtable app - -
- -3. **If you're using Airtable in code:** - -- Change any of your code steps to reference `airtable_oauth` instead of `airtable`. -- Modify your authorization headers accordingly - -In Node.js, you would modify your authorization header from this: - - `"Authorization": ${this.airtable.$auth.api_key}` - - to - -``` Authorization: `Bearer ${this.airtable_oauth.$auth.oauth_access_token}` ``` - -This is what your Node.js code step may have looked like before: - -``` javascript -import { axios } from "@pipedream/platform" -export default defineComponent({ - props: { - airtable: { - type: "app", - app: "airtable", - } - }, - async run({steps, $}) { - return await axios($, { - url: `https://api.airtable.com/v0/meta/whoami`, - headers: { - "Authorization": `${this.airtable.$auth.api_key}`, - "Content-Type": `application/json`, - }, - }) - }, -}) - -``` - -And here's an example of the updated code step that uses the updated app, **`airtable_oauth`** instead with the updated authentication method: - -``` javascript -import { axios } from "@pipedream/platform" -export default defineComponent({ - props: { - airtable_oauth: { - type: "app", - app: "airtable_oauth", - } - }, - async run({steps, $}) { - return await axios($, { - url: `https://api.airtable.com/v0/meta/whoami`, - headers: { - Authorization: `Bearer ${this.airtable_oauth.$auth.oauth_access_token}`, - }, - }) - }, -}) - -``` - -In Python, here's a snippet of what your code step might have looked like before: -``` python -def handler(pd: "pipedream"): - headers = {"X-Airtable-Api-Key": f'{pd.inputs["airtable"]["$auth"]["api_key"]}'} -``` - -And here's the updated Python code step, with **`airtable_oauth`** and the appropriate token and authorization headers: -``` python -def handler(pd: "pipedream"): - token = f'{pd.inputs["airtable_oauth"]["$auth"]["oauth_access_token"]}' - authorization = f'Bearer {token}' - headers = {"Authorization": authorization} -``` - -3. **Test and redeploy your workflows.** \ No newline at end of file diff --git a/docs/docs/api/README.md b/docs/docs/api/README.md deleted file mode 100644 index 7859f2099e54f..0000000000000 --- a/docs/docs/api/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# API Overview - -Pipedream currently offers [REST](/api/rest/) and [Server-sent Events (SSE)](/api/sse/) APIs. - -- Use the [REST API](/api/rest/) to create and manage sources, workflows, and - events -- Use the [SSE API](/api/sse/) to subscribe to real-time event streams for - sources - -